git/odb/pack: introduce v1 index format support

This commit is contained in:
Taylor Blau 2017-07-14 17:46:58 -06:00
parent 3e8d4879fa
commit 8bda7245ea
6 changed files with 223 additions and 1 deletions

@ -10,6 +10,26 @@ import (
const (
// V1Width is the total width of the header in V1.
V1Width = 0
// FanoutEntries is the number of entries in the fanout table.
FanoutEntries = 256
// FanoutEntryWidth is the width of each entry in the fanout table.
FanoutEntryWidth = 4
// FanoutWidth is the width of the entire fanout table.
FanoutWidth = FanoutEntries * FanoutEntryWidth
// OffsetV1Start is the location of the first object outside of the V1
// header.
OffsetV1Start = V1Width + FanoutWidth
// ObjectNameWidth is the width of a SHA1 object name.
ObjectNameWidth = 20
// ObjectSmallOffsetWidth is the width of the small offset encoded into
// each object.
ObjectSmallOffsetWidth = 4
// ObjectEntryV1Width is the width of one contiguous object entry in V1.
ObjectEntryV1Width = ObjectNameWidth + ObjectSmallOffsetWidth
)
var (
@ -61,10 +81,14 @@ func decodeIndexHeader(r io.ReaderAt) (IndexVersion, error) {
}
version := IndexVersion(binary.BigEndian.Uint32(vb))
switch version {
case V1:
return version, nil
}
return version, &UnsupportedVersionErr{uint32(version)}
}
return IndexVersion(0), nil
return V1, nil
}
// decodeIndexFanout decodes the fanout table given by "r" and beginning at the

@ -8,6 +8,21 @@ import (
"github.com/stretchr/testify/assert"
)
func TestDecodeIndexV1(t *testing.T) {
idx, err := DecodeIndex(bytes.NewReader(make([]byte, FanoutWidth)))
assert.NoError(t, err)
assert.Equal(t, V1, idx.version)
assert.EqualValues(t, 0, idx.Count())
}
func TestDecodeIndexV1InvalidFanout(t *testing.T) {
idx, err := DecodeIndex(bytes.NewReader(make([]byte, FanoutWidth-1)))
assert.Equal(t, ErrShortFanout, err)
assert.Nil(t, idx)
}
func TestDecodeIndexUnsupportedVersion(t *testing.T) {
buf := make([]byte, 0, 4+4)
buf = append(buf, 0xff, 0x74, 0x4f, 0x63)

52
git/odb/pack/index_v1.go Normal file

@ -0,0 +1,52 @@
package pack
import (
"bytes"
"encoding/binary"
)
const (
// V1 is an instance of IndexVersion corresponding to the V1 index file
// format.
V1 IndexVersion = 1
)
// v1Search implements the IndexVersion.Search method for V1 packfiles.
func v1Search(idx *Index, name []byte, at int64) (*IndexEntry, int, error) {
var sha [20]byte
if _, err := idx.readAt(sha[:], v1ShaOffset(at)); err != nil {
return nil, 0, err
}
cmp := bytes.Compare(name, sha[:])
if cmp != 0 {
return nil, cmp, nil
}
var offs [4]byte
if _, err := idx.readAt(offs[:], v1EntryOffset(at)); err != nil {
return nil, 0, err
}
return &IndexEntry{
PackOffset: uint64(binary.BigEndian.Uint32(offs[:])),
}, 0, nil
}
// v1ShaOffset returns the location of the SHA1 of an object given at "at".
func v1ShaOffset(at int64) int64 {
// Skip forward until the desired entry.
return v1EntryOffset(at) +
// Skip past the 4-byte object offset in the desired entry to
// the SHA1.
ObjectSmallOffsetWidth
}
// v1EntryOffset returns the location of the packfile offset for the object
// given at "at".
func v1EntryOffset(at int64) int64 {
// Skip the L1 fanout table
return OffsetV1Start +
// Skip the object entries before the one located at "at"
(ObjectEntryV1Width * at)
}

@ -0,0 +1,98 @@
package pack
import (
"bytes"
"encoding/binary"
"testing"
"github.com/stretchr/testify/assert"
)
var (
V1IndexFanout = make([]uint32, FanoutEntries)
V1IndexSmallEntry = []byte{
0x0, 0x0, 0x0, 0x1,
0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1,
0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1,
}
V1IndexSmallSha = V1IndexSmallEntry[4:]
V1IndexMediumEntry = []byte{
0x0, 0x0, 0x0, 0x2,
0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2,
0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2,
}
V1IndexMediumSha = V1IndexMediumEntry[4:]
V1IndexLargeEntry = []byte{
0x0, 0x0, 0x0, 0x3,
0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3,
0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3,
}
V1IndexLargeSha = V1IndexLargeEntry[4:]
V1Index = &Index{
fanout: V1IndexFanout,
version: V1,
}
)
func TestIndexV1SearchExact(t *testing.T) {
e, cmp, err := V1.Search(V1Index, V1IndexMediumSha, 1)
assert.NoError(t, err)
assert.Equal(t, 0, cmp)
assert.EqualValues(t, 2, e.PackOffset)
}
func TestIndexV1SearchSmall(t *testing.T) {
e, cmp, err := V1.Search(V1Index, V1IndexMediumSha, 0)
assert.NoError(t, err)
assert.Equal(t, 1, cmp)
assert.Nil(t, e)
}
func TestIndexV1SearchBig(t *testing.T) {
e, cmp, err := V1.Search(V1Index, V1IndexMediumSha, 2)
assert.NoError(t, err)
assert.Equal(t, -1, cmp)
assert.Nil(t, e)
}
func TestIndexV1SearchOutOfBounds(t *testing.T) {
e, cmp, err := V1.Search(V1Index, V1IndexMediumSha, 10)
assert.Nil(t, e)
assert.Equal(t, 0, cmp)
assert.Equal(t, ErrIndexOutOfBounds, err)
}
func init() {
V1IndexFanout[1] = 1
V1IndexFanout[2] = 2
V1IndexFanout[3] = 3
for i := 3; i < len(V1IndexFanout); i++ {
V1IndexFanout[i] = 3
}
fanout := make([]byte, FanoutWidth)
for i, n := range V1IndexFanout {
binary.BigEndian.PutUint32(fanout[i*FanoutEntryWidth:], n)
}
buf := make([]byte, 0, OffsetV1Start+(3*ObjectEntryV1Width))
buf = append(buf, fanout...)
buf = append(buf, V1IndexSmallEntry...)
buf = append(buf, V1IndexMediumEntry...)
buf = append(buf, V1IndexLargeEntry...)
V1Index.f = bytes.NewReader(buf)
}

@ -1,6 +1,7 @@
package pack
import (
"errors"
"fmt"
)
@ -16,5 +17,33 @@ const (
// Width returns the width of the header given in the respective version.
func (v IndexVersion) Width() int64 {
switch v {
case V1:
return V1Width
}
panic(fmt.Sprintf("git/odb/pack: width unknown for pack version %d", v))
}
var (
// ErrIndexOutOfBounds is an error returned when the object lookup "at"
// (see: Search() below) is out of bounds.
ErrIndexOutOfBounds = errors.New("git/odb/pack: index is out of bounds")
)
// Search searches index "idx" for an object given by "name" at location "at".
//
// If will return the object if it was found, or a comparison determining
// whether to search above or below next.
//
// Otherwise, it will return an error.
func (v IndexVersion) Search(idx *Index, name []byte, at int64) (*IndexEntry, int, error) {
if at > int64(idx.Count()) {
return nil, 0, ErrIndexOutOfBounds
}
switch v {
case V1:
return v1Search(idx, name, at)
}
return nil, 0, &UnsupportedVersionErr{Got: uint32(v)}
}

@ -6,6 +6,10 @@ import (
"github.com/stretchr/testify/assert"
)
func TestIndexVersionWidthV1(t *testing.T) {
assert.EqualValues(t, 0, V1.Width())
}
func TestIndexVersionWidthPanicsOnUnknownVersion(t *testing.T) {
v := IndexVersion(5)