package packidx import ( "encoding/binary" "errors" "fmt" "lindenii.org/go/furgit/object/id" "lindenii.org/go/lgo/intconv" ) // ErrMalformedPackIndex reports that // a pack index is truncated, // has a bad signature or unsupported version, // or has inconsistent tables. var ErrMalformedPackIndex = errors.New("internal/format/packidx: malformed pack index") const ( signature = 0xff744f63 version = 2 headerLen = 8 fanoutLen = 256 * 4 // largeOffsetFlag marks one 32-bit offset table entry // as an index into the 64-bit offset table. largeOffsetFlag = 0x80000000 ) // Packidx is one parsed pack index view over borrowed bytes. // // Labels: Deps-Borrowed, Life-Parent, MT-Safe. type Packidx struct { // data is the entire pack index payload. data []byte // hashSize is the object ID size of the index's object format. hashSize int // numObjects is the object count from the last fanout entry. numObjects int // namesOff, crcOff, off32Off, and off64Off are // the byte offsets of the object ID, CRC32, // 32-bit offset, and 64-bit offset tables. namesOff int crcOff int off32Off int off64Off int // off64Count is the number of 64-bit offset table entries. off64Count uint64 } // Parse parses one pack index from data. // // hashSize must be the object ID size // of the pack's object format; // Parse panics on implausible hash sizes. func Parse(data []byte, hashSize int) (Packidx, error) { var zero Packidx if hashSize <= 0 || hashSize > id.MaxObjectIDSize { panic("internal/format/packidx: invalid hash size") } if len(data) < headerLen+fanoutLen+2*hashSize { return zero, fmt.Errorf("%w: truncated", ErrMalformedPackIndex) } if binary.BigEndian.Uint32(data) != signature { return zero, fmt.Errorf("%w: bad signature", ErrMalformedPackIndex) } if binary.BigEndian.Uint32(data[4:]) != version { return zero, fmt.Errorf("%w: unsupported version", ErrMalformedPackIndex) } prev := uint32(0) for i := range 256 { count := binary.BigEndian.Uint32(data[headerLen+4*i:]) if count < prev { return zero, fmt.Errorf("%w: non-monotonic fanout", ErrMalformedPackIndex) } prev = count } numObjects := uint64(prev) hashSize64 := uint64(hashSize) namesOff := uint64(headerLen + fanoutLen) crcOff := namesOff + numObjects*hashSize64 off32Off := crcOff + 4*numObjects off64Off := off32Off + 4*numObjects minTotal := off64Off + 2*hashSize64 dataLen, err := intconv.IntToUint64(len(data)) if err != nil { return zero, fmt.Errorf("%w: %w", ErrMalformedPackIndex, err) } if dataLen < minTotal { return zero, fmt.Errorf("%w: tables exceed index size", ErrMalformedPackIndex) } off64Bytes := dataLen - minTotal if off64Bytes%8 != 0 { return zero, fmt.Errorf("%w: trailing table size not a 64-bit offset multiple", ErrMalformedPackIndex) } off64Count := off64Bytes / 8 if off64Count > numObjects { return zero, fmt.Errorf("%w: more 64-bit offsets than objects", ErrMalformedPackIndex) } idx := Packidx{ data: data, hashSize: hashSize, off64Count: off64Count, } idx.numObjects, err = intconv.Uint64ToInt(numObjects) if err != nil { return zero, fmt.Errorf("%w: %w", ErrMalformedPackIndex, err) } idx.namesOff, err = intconv.Uint64ToInt(namesOff) if err != nil { return zero, fmt.Errorf("%w: %w", ErrMalformedPackIndex, err) } idx.crcOff, err = intconv.Uint64ToInt(crcOff) if err != nil { return zero, fmt.Errorf("%w: %w", ErrMalformedPackIndex, err) } idx.off32Off, err = intconv.Uint64ToInt(off32Off) if err != nil { return zero, fmt.Errorf("%w: %w", ErrMalformedPackIndex, err) } idx.off64Off, err = intconv.Uint64ToInt(off64Off) if err != nil { return zero, fmt.Errorf("%w: %w", ErrMalformedPackIndex, err) } return idx, nil } // NumObjects returns the number of objects in the index. func (idx *Packidx) NumObjects() int { return idx.numObjects } // PackHash returns the pack hash recorded in the index trailer. // // Labels: Life-Parent, Mut-No. func (idx *Packidx) PackHash() []byte { return idx.data[len(idx.data)-2*idx.hashSize : len(idx.data)-idx.hashSize] } // OIDAt returns the object ID bytes at one index position. // Positions follow object ID sort order. // // OIDAt panics when pos is out of range. // // Labels: Life-Parent, Mut-No. func (idx *Packidx) OIDAt(pos int) []byte { idx.checkPos(pos) start := idx.namesOff + pos*idx.hashSize return idx.data[start : start+idx.hashSize] } // CRCAt returns the CRC32 of the packed entry data // at one index position. // // CRCAt panics when pos is out of range. func (idx *Packidx) CRCAt(pos int) uint32 { idx.checkPos(pos) return binary.BigEndian.Uint32(idx.data[idx.crcOff+4*pos:]) } // checkPos panics when pos is not a valid index position. // // An out-of-range position is a caller bug // that slice bounds checking would not catch, // since the tables share one data slice; // an unchecked access would silently read other tables' bytes. func (idx *Packidx) checkPos(pos int) { if pos < 0 || pos >= idx.numObjects { panic("internal/format/packidx: index position out of range") } }