aboutsummaryrefslogtreecommitdiff
package bloom_test

import (
	"encoding/binary"
	"errors"
	"testing"

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

func validFilter(t *testing.T, format id.ObjectFormat) []byte {
	// TODO: maybe testgit should have something like this?
	t.Helper()

	builder, err := bloom.NewBuilder(format, 4, 2, make([]byte, format.Size()))
	if err != nil {
		t.Fatal(err)
	}

	return builder.Bytes()
}

func otherFormat(t *testing.T, format id.ObjectFormat) id.ObjectFormat {
	t.Helper()

	for _, candidate := range id.SupportedObjectFormats() {
		if candidate != format {
			return candidate
		}
	}

	t.Skip("only one supported object format")

	return id.ObjectFormatUnknown
}

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

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

			_, err := bloom.Parse(validFilter(t, format), format)
			if err != nil {
				t.Fatalf("Parse rejected a valid filter: %v", err)
			}
		})
	}
}

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

	cases := []struct {
		name   string
		mangle func(data []byte) []byte
	}{
		{"truncated", func(data []byte) []byte { return data[:bloom.HeaderLen-1] }},
		{"bad signature", func(data []byte) []byte {
			data[0] ^= 0xff

			return data
		}},
		{"bad version", func(data []byte) []byte {
			binary.BigEndian.PutUint32(data[4:], 99)

			return data
		}},
		{"non power of two", func(data []byte) []byte {
			binary.BigEndian.PutUint32(data[12:], 3)

			return data
		}},
		{"zero probe count", func(data []byte) []byte {
			binary.BigEndian.PutUint16(data[16:], 0)

			return data
		}},
		{"parameters exceed hash", func(data []byte) []byte {
			binary.BigEndian.PutUint32(data[12:], 1<<31)
			binary.BigEndian.PutUint16(data[16:], 30)

			return data
		}},
		{"nonzero padding", func(data []byte) []byte {
			data[20] = 1

			return data
		}},
		{"size disagrees", func(data []byte) []byte { return data[:len(data)-1] }},
	}

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

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

					data := tc.mangle(append([]byte(nil), validFilter(t, format)...))

					_, err := bloom.Parse(data, format)
					if !errors.Is(err, bloom.ErrMalformedBloomFilter) {
						t.Fatalf("Parse error = %v, want ErrMalformedBloomFilter", err)
					}
				})
			}
		})
	}
}

// TestVerifyDetectsCorruption checks that Verify accepts a sound filter
// and rejects one whose bucket bytes have been altered.
func TestVerifyDetectsCorruption(t *testing.T) {
	t.Parallel()

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

			data := validFilter(t, format)

			filter, err := bloom.Parse(data, format)
			if err != nil {
				t.Fatal(err)
			}

			err = filter.Verify()
			if err != nil {
				t.Fatalf("Verify on a sound filter: %v", err)
			}

			data[bloom.HeaderLen] ^= 0xff

			err = filter.Verify()
			if !errors.Is(err, bloom.ErrMalformedBloomFilter) {
				t.Fatalf("Verify error = %v, want ErrMalformedBloomFilter", err)
			}
		})
	}
}

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

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

			_, err := bloom.Parse(validFilter(t, format), otherFormat(t, format))
			if !errors.Is(err, bloom.ErrMalformedBloomFilter) {
				t.Fatalf("Parse error = %v, want ErrMalformedBloomFilter", err)
			}
		})
	}
}