diff options
Diffstat (limited to 'internal/format/packidx/write.go')
| -rw-r--r-- | internal/format/packidx/write.go | 158 |
1 files changed, 158 insertions, 0 deletions
diff --git a/internal/format/packidx/write.go b/internal/format/packidx/write.go new file mode 100644 index 00000000..2bcdb51a --- /dev/null +++ b/internal/format/packidx/write.go @@ -0,0 +1,158 @@ +package packidx + +import ( + "bufio" + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "math" + + "lindenii.org/go/furgit/object/id" +) + +// ErrInvalidEntries reports that +// entries supplied for an index write +// are unsorted, duplicated, or not representable +// in the pack index format. +var ErrInvalidEntries = errors.New("internal/format/packidx: invalid entries") + +// Entry is one object record for an index write. +type Entry struct { + // OID holds the object ID bytes; + // only the first hash-size bytes are meaningful. + OID [id.MaxObjectIDSize]byte + // Offset is the entry's pack file offset. + Offset uint64 + // CRC32 is the CRC32 of the entry's packed data. + CRC32 uint32 +} + +// Write writes one pack index over entries to w. +// +// entries must be sorted by object ID without duplicates. +// packHash must be the pack's trailer hash; +// Write panics when its length does not match the object format. +func Write(w io.Writer, objectFormat id.ObjectFormat, entries []Entry, packHash []byte) error { + hashSize := objectFormat.Size() + if hashSize == 0 { + return id.ErrInvalidObjectFormat + } + + if len(packHash) != hashSize { + panic("internal/format/packidx: invalid pack hash length") + } + + if len(entries) > math.MaxUint32 { + return fmt.Errorf("%w: too many entries", ErrInvalidEntries) + } + + for i := 1; i < len(entries); i++ { + if bytes.Compare(entries[i-1].OID[:hashSize], entries[i].OID[:hashSize]) >= 0 { + return fmt.Errorf("%w: not sorted by object ID", ErrInvalidEntries) + } + } + + hashImpl, err := objectFormat.New() + if err != nil { + return fmt.Errorf("internal/format/packidx: %w", err) + } + + bw := bufio.NewWriter(io.MultiWriter(w, hashImpl)) + sw := &stickyWriter{w: bw} + + sw.writeUint32(signature) + sw.writeUint32(version) + + var counts [256]uint32 + for i := range entries { + counts[entries[i].OID[0]]++ + } + + cumulative := uint32(0) + for _, count := range counts { + cumulative += count + sw.writeUint32(cumulative) + } + + for i := range entries { + sw.write(entries[i].OID[:hashSize]) + } + + for i := range entries { + sw.writeUint32(entries[i].CRC32) + } + + var largeOffsets []uint64 + + for i := range entries { + offset := entries[i].Offset + if offset < largeOffsetFlag { + sw.writeUint32(uint32(offset)) + + continue + } + + slot := len(largeOffsets) + if slot >= largeOffsetFlag { + return fmt.Errorf("%w: too many large offsets", ErrInvalidEntries) + } + + sw.writeUint32(largeOffsetFlag | uint32(slot)) + + largeOffsets = append(largeOffsets, offset) + } + + for _, offset := range largeOffsets { + sw.writeUint64(offset) + } + + sw.write(packHash) + + if sw.err != nil { + return fmt.Errorf("internal/format/packidx: %w", sw.err) + } + + err = bw.Flush() + if err != nil { + return fmt.Errorf("internal/format/packidx: %w", err) + } + + _, err = w.Write(hashImpl.Sum(nil)) + if err != nil { + return fmt.Errorf("internal/format/packidx: %w", err) + } + + return nil +} + +// stickyWriter forwards writes to w +// and retains the first error, +// turning subsequent writes into no-ops. +type stickyWriter struct { + w io.Writer + err error +} + +func (sw *stickyWriter) write(p []byte) { + if sw.err != nil { + return + } + + _, sw.err = sw.w.Write(p) +} + +func (sw *stickyWriter) writeUint32(v uint32) { + var buf [4]byte + + binary.BigEndian.PutUint32(buf[:], v) + sw.write(buf[:]) +} + +func (sw *stickyWriter) writeUint64(v uint64) { + var buf [8]byte + + binary.BigEndian.PutUint64(buf[:], v) + sw.write(buf[:]) +} |
