package pack import ( "bytes" "io" "github.com/git-lfs/git-lfs/errors" ) // Index stores information about the location of objects in a corresponding // packfile. type Index struct { // version is the encoding version used by this index. // // Currently, versions 1 and 2 are supported. version IndexVersion // fanout is the L1 fanout table stored in this index. For a given index // "i" into the array, the value stored at that index specifies the // number of objects in the packfile/index that are lexicographically // less than or equal to that index. // // See: https://github.com/git/git/blob/v2.13.0/Documentation/technical/pack-format.txt#L41-L45 fanout []uint32 // r is the underlying set of encoded data comprising this index file. r io.ReaderAt } // Count returns the number of objects in the packfile. func (i *Index) Count() int { return int(i.fanout[255]) } // Close closes the packfile index if the underlying data stream is closeable. // If so, it returns any error involved in closing. func (i *Index) Close() error { if close, ok := i.r.(io.Closer); ok { return close.Close() } return nil } var ( // errNotFound is an error returned by Index.Entry() (see: below) when // an object cannot be found in the index. errNotFound = errors.New("git/odb/pack: object not found in index") ) // IsNotFound returns whether a given error represents a missing object in the // index. func IsNotFound(err error) bool { return err == errNotFound } // Entry returns an entry containing the offset of a given SHA1 "name". // // Entry operates in O(log(n))-time in the worst case, where "n" is the number // of objects that begin with the first byte of "name". // // If the entry cannot be found, (nil, ErrNotFound) will be returned. If there // was an error searching for or parsing an entry, it will be returned as (nil, // err). // // Otherwise, (entry, nil) will be returned. func (i *Index) Entry(name []byte) (*IndexEntry, error) { var last *bounds bounds := i.bounds(name) for bounds.Left() < bounds.Right() { if last.Equal(bounds) { // If the bounds are unchanged, that means either that // the object does not exist in the packfile, or the // fanout table is corrupt. // // Either way, we won't be able to find the object. // Return immediately to prevent infinite looping. return nil, errNotFound } last = bounds // Find the midpoint between the upper and lower bounds. mid := bounds.Left() + ((bounds.Right() - bounds.Left()) / 2) got, err := i.version.Name(i, mid) if err != nil { return nil, err } if cmp := bytes.Compare(name, got); cmp == 0 { // If "cmp" is zero, that means the object at that index // "at" had a SHA equal to the one given by name, and we // are done. return i.version.Entry(i, mid) } else if cmp < 0 { // If the comparison is less than 0, we searched past // the desired object, so limit the upper bound of the // search to the midpoint. bounds = bounds.WithRight(mid) } else if cmp > 0 { // Likewise, if the comparison is greater than 0, we // searched below the desired object. Modify the bounds // accordingly. bounds = bounds.WithLeft(mid) } } return nil, errNotFound } // readAt is a convenience method that allow reading into the underlying data // source from other callers within this package. func (i *Index) readAt(p []byte, at int64) (n int, err error) { return i.r.ReadAt(p, at) } // bounds returns the initial bounds for a given name using the fanout table to // limit search results. func (i *Index) bounds(name []byte) *bounds { var left, right int64 if name[0] == 0 { // If the lower bound is 0, there are no objects before it, // start at the beginning of the index file. left = 0 } else { // Otherwise, make the lower bound the slot before the given // object. left = int64(i.fanout[name[0]-1]) } if name[0] == 255 { // As above, if the upper bound is the max byte value, make the // upper bound the last object in the list. right = int64(i.Count()) } else { // Otherwise, make the upper bound the first object which is not // within the given slot. right = int64(i.fanout[name[0]+1]) } return newBounds(left, right) }