aboutsummaryrefslogtreecommitdiff
package packfile_test

import (
	"bytes"
	"errors"
	"math"
	"testing"

	"lindenii.org/go/furgit/internal/format/packfile"
	"lindenii.org/go/furgit/object/id"
)

func TestParseEntryHeaderBase(t *testing.T) {
	t.Parallel()

	// Commit entry of size 85:
	// 0x95 has the continuation bit set, type 1, and size low bits 0x5;
	// 0x05 contributes 5<<4.
	header, err := packfile.ParseEntryHeader([]byte{0x95, 0x05}, 20)
	if err != nil {
		t.Fatalf("ParseEntryHeader: %v", err)
	}

	if header.Type != packfile.EntryTypeCommit {
		t.Fatalf("ParseEntryHeader type = %d, want %d", header.Type, packfile.EntryTypeCommit)
	}

	if header.Size != 85 {
		t.Fatalf("ParseEntryHeader size = %d, want 85", header.Size)
	}

	if header.HeaderLen != 2 {
		t.Fatalf("ParseEntryHeader header len = %d, want 2", header.HeaderLen)
	}
}

func TestParseEntryHeaderRefDelta(t *testing.T) {
	t.Parallel()

	for _, objectFormat := range id.SupportedObjectFormats() {
		t.Run(objectFormat.String(), func(t *testing.T) {
			t.Parallel()

			hashSize := objectFormat.Size()

			base := bytes.Repeat([]byte{0xab}, hashSize)
			data := append([]byte{0x73}, base...)

			header, err := packfile.ParseEntryHeader(data, hashSize)
			if err != nil {
				t.Fatalf("ParseEntryHeader: %v", err)
			}

			if header.Type != packfile.EntryTypeRefDelta {
				t.Fatalf("ParseEntryHeader type = %d, want %d", header.Type, packfile.EntryTypeRefDelta)
			}

			if header.Size != 3 {
				t.Fatalf("ParseEntryHeader size = %d, want 3", header.Size)
			}

			if header.HeaderLen != 1+hashSize {
				t.Fatalf("ParseEntryHeader header len = %d, want %d", header.HeaderLen, 1+hashSize)
			}

			if !bytes.Equal(header.RefBase[:hashSize], base) {
				t.Fatalf("ParseEntryHeader ref base mismatch")
			}
		})
	}
}

func TestParseEntryHeaderOfsDelta(t *testing.T) {
	t.Parallel()

	header, err := packfile.ParseEntryHeader([]byte{0x65, 0x80, 0x00}, 20)
	if err != nil {
		t.Fatalf("ParseEntryHeader: %v", err)
	}

	if header.Type != packfile.EntryTypeOfsDelta {
		t.Fatalf("ParseEntryHeader type = %d, want %d", header.Type, packfile.EntryTypeOfsDelta)
	}

	if header.OfsDistance != 128 {
		t.Fatalf("ParseEntryHeader distance = %d, want 128", header.OfsDistance)
	}

	if header.HeaderLen != 3 {
		t.Fatalf("ParseEntryHeader header len = %d, want 3", header.HeaderLen)
	}
}

func TestParseEntryHeaderMalformed(t *testing.T) {
	t.Parallel()

	cases := []struct {
		name string
		data []byte
	}{
		{name: "empty", data: []byte{}},
		{name: "truncated type/size", data: []byte{0x95}},
		{
			name: "size overflow",
			data: append([]byte{0x9f}, bytes.Repeat([]byte{0xff}, 9)...),
		},
		{
			name: "overlong type/size",
			data: append([]byte{0x95}, append(bytes.Repeat([]byte{0x80}, 9), 0x00)...),
		},
		{name: "truncated ref-delta base", data: []byte{0x73, 0xab, 0xab}},
		{name: "truncated ofs distance", data: []byte{0x65, 0x80}},
		{name: "type invalid", data: []byte{0x05}},
		{name: "type future", data: []byte{0x55}},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			_, err := packfile.ParseEntryHeader(tc.data, 20)
			if !errors.Is(err, packfile.ErrMalformedEntryHeader) {
				t.Fatalf("ParseEntryHeader error = %v, want ErrMalformedEntryHeader", err)
			}
		})
	}
}

func TestParseEntryHeaderBadHashSize(t *testing.T) {
	t.Parallel()

	for _, hashSize := range []int{-1, 0, id.MaxObjectIDSize + 1} {
		func() {
			defer func() {
				if recover() == nil {
					t.Fatalf("ParseEntryHeader hash size %d: expected panic", hashSize)
				}
			}()

			_, _ = packfile.ParseEntryHeader([]byte{0x95, 0x05}, hashSize)
		}()
	}
}

func TestTypeSizeRoundTrip(t *testing.T) {
	t.Parallel()

	baseTypes := []packfile.EntryType{
		packfile.EntryTypeCommit,
		packfile.EntryTypeTree,
		packfile.EntryTypeBlob,
		packfile.EntryTypeTag,
	}

	sizes := []uint64{
		0, 1, 15, 16, 127, 128, 1 << 20, 1 << 57, math.MaxUint64,
	}

	for _, entryType := range baseTypes {
		for _, size := range sizes {
			data := packfile.AppendTypeSize(nil, entryType, size)
			if len(data) > packfile.MaxTypeSizeLen {
				t.Fatalf("AppendTypeSize(%d, %d) length = %d, want <= %d",
					entryType, size, len(data), packfile.MaxTypeSizeLen)
			}

			header, err := packfile.ParseEntryHeader(data, 20)
			if err != nil {
				t.Fatalf("ParseEntryHeader: %v", err)
			}

			if header.Type != entryType {
				t.Fatalf("round trip type = %d, want %d", header.Type, entryType)
			}

			if header.Size != size {
				t.Fatalf("round trip size = %d, want %d", header.Size, size)
			}

			if header.HeaderLen != len(data) {
				t.Fatalf("round trip header len = %d, want %d", header.HeaderLen, len(data))
			}
		}
	}
}

func TestRefDeltaHeaderRoundTrip(t *testing.T) {
	t.Parallel()

	for _, objectFormat := range id.SupportedObjectFormats() {
		t.Run(objectFormat.String(), func(t *testing.T) {
			t.Parallel()

			hashSize := objectFormat.Size()

			base := bytes.Repeat([]byte{0xcd}, hashSize)
			data := packfile.AppendTypeSize(nil, packfile.EntryTypeRefDelta, 42)
			data = append(data, base...)

			header, err := packfile.ParseEntryHeader(data, hashSize)
			if err != nil {
				t.Fatalf("ParseEntryHeader: %v", err)
			}

			if !bytes.Equal(header.RefBase[:hashSize], base) {
				t.Fatalf("round trip ref base mismatch")
			}

			if header.HeaderLen != len(data) {
				t.Fatalf("round trip header len = %d, want %d", header.HeaderLen, len(data))
			}
		})
	}
}

func TestOfsDeltaHeaderRoundTrip(t *testing.T) {
	t.Parallel()

	data := packfile.AppendTypeSize(nil, packfile.EntryTypeOfsDelta, 7)
	data = packfile.AppendOfsDeltaDistance(data, 123456)

	header, err := packfile.ParseEntryHeader(data, 20)
	if err != nil {
		t.Fatalf("ParseEntryHeader: %v", err)
	}

	if header.OfsDistance != 123456 {
		t.Fatalf("round trip distance = %d, want 123456", header.OfsDistance)
	}

	if header.HeaderLen != len(data) {
		t.Fatalf("round trip header len = %d, want %d", header.HeaderLen, len(data))
	}
}

func TestMaxEntryHeaderLenBounds(t *testing.T) {
	t.Parallel()

	for _, objectFormat := range id.SupportedObjectFormats() {
		hashSize := objectFormat.Size()

		maxLen := packfile.MaxEntryHeaderLen(hashSize)
		if maxLen < packfile.MaxTypeSizeLen+hashSize {
			t.Fatalf("MaxEntryHeaderLen(%d) = %d cannot fit a ref-delta header", hashSize, maxLen)
		}

		if maxLen < packfile.MaxTypeSizeLen+packfile.MaxOfsDeltaDistanceLen {
			t.Fatalf("MaxEntryHeaderLen(%d) = %d cannot fit an ofs-delta header", hashSize, maxLen)
		}
	}
}