git-lfs/git/odb/pack/index.go
2017-09-19 14:07:37 -06:00

144 lines
4.1 KiB
Go

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)
}