diff options
| author | 2026-06-12 05:12:37 +0000 | |
|---|---|---|
| committer | 2026-06-12 05:12:37 +0000 | |
| commit | ea5467c799ce4d1088f352ae600b43d4dd17147b (patch) | |
| tree | 9b49a91282532398b62cba8eee52c581050c8544 /internal/format | |
| parent | internal/format/packidx: Use repo.SeedHistory (diff) | |
internal/format/packrev: Add tests
Diffstat (limited to 'internal/format')
| -rw-r--r-- | internal/format/packrev/helpers_test.go | 75 | ||||
| -rw-r--r-- | internal/format/packrev/packrev_test.go | 160 | ||||
| -rw-r--r-- | internal/format/packrev/write_test.go | 135 |
3 files changed, 370 insertions, 0 deletions
diff --git a/internal/format/packrev/helpers_test.go b/internal/format/packrev/helpers_test.go new file mode 100644 index 00000000..2ec2f434 --- /dev/null +++ b/internal/format/packrev/helpers_test.go @@ -0,0 +1,75 @@ +package packrev_test + +import ( + "cmp" + "slices" + "testing" + + "lindenii.org/go/furgit/internal/format/packidx" + "lindenii.org/go/furgit/internal/testgit" + "lindenii.org/go/furgit/object/id" + "lindenii.org/go/lgo/intconv" +) + +// makeGitPack seeds a repository, +// packs the seeded objects with git pack-objects +// including a reverse index, +// and returns the artifact path prefix. +func makeGitPack(t *testing.T, objectFormat id.ObjectFormat) string { + t.Helper() + + repo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectFormat}) + if err != nil { + t.Fatalf("NewRepo: %v", err) + } + + seeded, err := repo.SeedHistory(t) + if err != nil { + t.Fatalf("SeedHistory: %v", err) + } + + prefix, err := repo.PackObjects(t, slices.Values(seeded.All()), testgit.PackObjectsOptions{RevIndex: true}) + if err != nil { + t.Fatalf("PackObjects: %v", err) + } + + return prefix +} + +// packOrderPositions derives the pack-offset-order index positions +// from one parsed pack index. +func packOrderPositions(t *testing.T, idx *packidx.Packidx) []uint32 { + t.Helper() + + type pair struct { + offset uint64 + position uint32 + } + + pairs := make([]pair, 0, idx.NumObjects()) + + for pos := range idx.NumObjects() { + offset, err := idx.OffsetAt(pos) + if err != nil { + t.Fatalf("OffsetAt(%d): %v", pos, err) + } + + position, err := intconv.IntToUint32(pos) + if err != nil { + t.Fatalf("IntToUint32(%d): %v", pos, err) + } + + pairs = append(pairs, pair{offset: offset, position: position}) + } + + slices.SortFunc(pairs, func(a, b pair) int { + return cmp.Compare(a.offset, b.offset) + }) + + positions := make([]uint32, 0, len(pairs)) + for _, p := range pairs { + positions = append(positions, p.position) + } + + return positions +} diff --git a/internal/format/packrev/packrev_test.go b/internal/format/packrev/packrev_test.go new file mode 100644 index 00000000..b644e15e --- /dev/null +++ b/internal/format/packrev/packrev_test.go @@ -0,0 +1,160 @@ +package packrev_test + +import ( + "bytes" + "encoding/binary" + "errors" + "os" + "testing" + + "lindenii.org/go/furgit/internal/format/packidx" + "lindenii.org/go/furgit/internal/format/packrev" + "lindenii.org/go/furgit/object/id" +) + +func TestParseGitReverseIndex(t *testing.T) { + t.Parallel() + + for _, objectFormat := range id.SupportedObjectFormats() { + t.Run(objectFormat.String(), func(t *testing.T) { + t.Parallel() + + prefix := makeGitPack(t, objectFormat) + + revData, err := os.ReadFile(prefix + ".rev") //nolint:gosec + if err != nil { + t.Fatalf("ReadFile: %v", err) + } + + rev, err := packrev.Parse(revData, objectFormat) + if err != nil { + t.Fatalf("Parse: %v", err) + } + + idxData, err := os.ReadFile(prefix + ".idx") //nolint:gosec + if err != nil { + t.Fatalf("ReadFile: %v", err) + } + + idx, err := packidx.Parse(idxData, objectFormat.Size()) + if err != nil { + t.Fatalf("packidx.Parse: %v", err) + } + + if rev.NumObjects() != idx.NumObjects() { + t.Fatalf("NumObjects = %d, want %d", rev.NumObjects(), idx.NumObjects()) + } + + packData, err := os.ReadFile(prefix + ".pack") //nolint:gosec + if err != nil { + t.Fatalf("ReadFile: %v", err) + } + + packTrailer := packData[len(packData)-objectFormat.Size():] + if !bytes.Equal(rev.PackHash(), packTrailer) { + t.Fatalf("PackHash does not match pack trailer") + } + + want := packOrderPositions(t, &idx) + + for packOrder, wantPosition := range want { + position, err := rev.PositionAt(packOrder) + if err != nil { + t.Fatalf("PositionAt(%d): %v", packOrder, err) + } + + if position != int(wantPosition) { + t.Fatalf("PositionAt(%d) = %d, want %d", packOrder, position, wantPosition) + } + } + }) + } +} + +func TestParseMalformed(t *testing.T) { + t.Parallel() + + for _, objectFormat := range id.SupportedObjectFormats() { + t.Run(objectFormat.String(), func(t *testing.T) { + t.Parallel() + + valid := writeSyntheticRev(t, objectFormat, []uint32{2, 0, 1, 3}) + + corrupt := func(mutate func(data []byte) []byte) []byte { + return mutate(bytes.Clone(valid)) + } + + cases := []struct { + name string + data []byte + }{ + {name: "empty", data: []byte{}}, + {name: "truncated", data: corrupt(func(d []byte) []byte { return d[:10] })}, + { + name: "bad signature", + data: corrupt(func(d []byte) []byte { + d[0] ^= 0xff + + return d + }), + }, + { + name: "bad version", + data: corrupt(func(d []byte) []byte { + d[7] = 2 + + return d + }), + }, + { + name: "hash function mismatch", + data: corrupt(func(d []byte) []byte { + d[11] ^= 0xff + + return d + }), + }, + { + name: "position table size not 32-bit multiple", + data: corrupt(func(d []byte) []byte { return append(d, 0xde, 0xad) }), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + _, err := packrev.Parse(tc.data, objectFormat) + if !errors.Is(err, packrev.ErrMalformedReverseIndex) { + t.Fatalf("Parse error = %v, want ErrMalformedReverseIndex", err) + } + }) + } + }) + } +} + +func TestPositionAtMalformed(t *testing.T) { + t.Parallel() + + for _, objectFormat := range id.SupportedObjectFormats() { + t.Run(objectFormat.String(), func(t *testing.T) { + t.Parallel() + + data := writeSyntheticRev(t, objectFormat, []uint32{2, 0, 1, 3}) + + // Corrupt the first stored position to one past the object count. + binary.BigEndian.PutUint32(data[12:], 4) + + rev, err := packrev.Parse(data, objectFormat) + if err != nil { + t.Fatalf("Parse: %v", err) + } + + _, err = rev.PositionAt(0) + if !errors.Is(err, packrev.ErrMalformedReverseIndex) { + t.Fatalf("PositionAt error = %v, want ErrMalformedReverseIndex", err) + } + }) + } +} diff --git a/internal/format/packrev/write_test.go b/internal/format/packrev/write_test.go new file mode 100644 index 00000000..b5c1fcb9 --- /dev/null +++ b/internal/format/packrev/write_test.go @@ -0,0 +1,135 @@ +package packrev_test + +import ( + "bytes" + "errors" + "os" + "testing" + + "lindenii.org/go/furgit/internal/format/packidx" + "lindenii.org/go/furgit/internal/format/packrev" + "lindenii.org/go/furgit/object/id" +) + +// writeSyntheticRev writes one reverse index over positions +// with a fixed fake pack hash. +func writeSyntheticRev(t *testing.T, objectFormat id.ObjectFormat, positions []uint32) []byte { + t.Helper() + + packHash := bytes.Repeat([]byte{0x5a}, objectFormat.Size()) + + var buf bytes.Buffer + + err := packrev.Write(&buf, objectFormat, positions, packHash) + if err != nil { + t.Fatalf("Write: %v", err) + } + + return buf.Bytes() +} + +func TestWriteRoundTrip(t *testing.T) { + t.Parallel() + + for _, objectFormat := range id.SupportedObjectFormats() { + t.Run(objectFormat.String(), func(t *testing.T) { + t.Parallel() + + positions := []uint32{8, 6, 7, 5, 3, 0, 4, 1, 2} + data := writeSyntheticRev(t, objectFormat, positions) + + rev, err := packrev.Parse(data, objectFormat) + if err != nil { + t.Fatalf("Parse: %v", err) + } + + if rev.NumObjects() != len(positions) { + t.Fatalf("NumObjects = %d, want %d", rev.NumObjects(), len(positions)) + } + + if !bytes.Equal(rev.PackHash(), bytes.Repeat([]byte{0x5a}, objectFormat.Size())) { + t.Fatalf("PackHash mismatch") + } + + for packOrder, want := range positions { + position, err := rev.PositionAt(packOrder) + if err != nil { + t.Fatalf("PositionAt(%d): %v", packOrder, err) + } + + if position != int(want) { + t.Fatalf("PositionAt(%d) = %d, want %d", packOrder, position, want) + } + } + }) + } +} + +func TestWriteMatchesGit(t *testing.T) { + t.Parallel() + + for _, objectFormat := range id.SupportedObjectFormats() { + t.Run(objectFormat.String(), func(t *testing.T) { + t.Parallel() + + prefix := makeGitPack(t, objectFormat) + + gitData, err := os.ReadFile(prefix + ".rev") //nolint:gosec + if err != nil { + t.Fatalf("ReadFile: %v", err) + } + + idxData, err := os.ReadFile(prefix + ".idx") //nolint:gosec + if err != nil { + t.Fatalf("ReadFile: %v", err) + } + + idx, err := packidx.Parse(idxData, objectFormat.Size()) + if err != nil { + t.Fatalf("packidx.Parse: %v", err) + } + + positions := packOrderPositions(t, &idx) + + var buf bytes.Buffer + + err = packrev.Write(&buf, objectFormat, positions, idx.PackHash()) + if err != nil { + t.Fatalf("Write: %v", err) + } + + if !bytes.Equal(buf.Bytes(), gitData) { + t.Fatalf("Write output differs from git's reverse index (%d vs %d bytes)", buf.Len(), len(gitData)) + } + }) + } +} + +func TestWriteInvalidPositions(t *testing.T) { + t.Parallel() + + for _, objectFormat := range id.SupportedObjectFormats() { + t.Run(objectFormat.String(), func(t *testing.T) { + t.Parallel() + + packHash := bytes.Repeat([]byte{0x5a}, objectFormat.Size()) + + err := packrev.Write(&bytes.Buffer{}, objectFormat, []uint32{0, 5}, packHash) + if !errors.Is(err, packrev.ErrInvalidPositions) { + t.Fatalf("Write error = %v, want ErrInvalidPositions", err) + } + }) + } +} + +func TestWriteBadPackHashPanics(t *testing.T) { + t.Parallel() + + defer func() { + if recover() == nil { + t.Fatalf("Write with short pack hash: expected panic") + } + }() + + _ = packrev.Write(&bytes.Buffer{}, id.ObjectFormatSHA256, nil, []byte{0x01}) +} |
