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