diff --git a/bucket.go b/bucket.go index 9724893..15dd2d7 100644 --- a/bucket.go +++ b/bucket.go @@ -22,11 +22,19 @@ func (b bucket[K, V]) location(key K) uint64 { } func (b bucket[K, V]) get(key K) (value V, found bool) { + if b.capacity == 0 { + return + } + slot := b.slots[b.location(key)] return slot.value, slot.occupied && b.compare(slot.key, key) } func (b *bucket[K, V]) drop(key K) (occupied bool) { + if b.capacity == 0 { + return + } + slot := &b.slots[b.location(key)] if slot.occupied && b.compare(slot.key, key) { @@ -45,6 +53,10 @@ func (b *bucket[K, V]) resize(capacity uint64) { } func (b bucket[K, V]) update(key K, value V) (updated bool) { + if b.capacity == 0 { + return + } + slot := &b.slots[b.location(key)] if slot.occupied && b.compare(slot.key, key) { @@ -56,6 +68,10 @@ func (b bucket[K, V]) update(key K, value V) (updated bool) { } func (b *bucket[K, V]) evict(insertion entry[K, V]) (evicted entry[K, V], eviction bool) { + if b.capacity == 0 { + return insertion, true + } + slot := &b.slots[b.location(insertion.key)] if !slot.occupied { diff --git a/cuckoo_fuzz_test.go b/cuckoo_fuzz_test.go index 8c9f427..fd97b2f 100644 --- a/cuckoo_fuzz_test.go +++ b/cuckoo_fuzz_test.go @@ -1,12 +1,11 @@ package cuckoo_test import ( - "bytes" - "encoding/binary" "maps" "testing" "github.com/stretchr/testify/assert" + go_fuzz_utils "github.com/trailofbits/go-fuzz-utils" "git.maximhutz.com/tools/go-cuckoo" ) @@ -20,49 +19,60 @@ func offsetHash(seed uint32) cuckoo.Hash[uint32] { } } +type fuzzStep struct { + drop bool + key, value uint32 +} + +type fuzzScenario struct { + seedA, seedB uint32 + steps []fuzzStep +} + func FuzzInsertLookup(f *testing.F) { - f.Fuzz(func(t *testing.T, data []byte, seedA, seedB uint32) { + f.Fuzz(func(t *testing.T, data []byte) { + var scenario fuzzScenario assert := assert.New(t) + if tp, err := go_fuzz_utils.NewTypeProvider(data); err != nil { + return + } else if err := tp.Fill(&scenario); err != nil { + return + } + + if scenario.seedA == scenario.seedB { + return + } + actual := cuckoo.NewCustomTable[uint32, uint32]( - offsetHash(seedA), - offsetHash(seedB), + offsetHash(scenario.seedA), + offsetHash(scenario.seedB), func(a, b uint32) bool { return a == b }, ) expected := map[uint32]uint32{} - if seedA == seedB { - return - } - - r := bytes.NewReader(data) - var key, value uint32 - var drop bool - for binary.Read(r, binary.LittleEndian, &key) == nil && - binary.Read(r, binary.LittleEndian, &value) == nil { - - if drop { - err := actual.Drop(key) + for _, step := range scenario.steps { + if step.drop { + err := actual.Drop(step.key) assert.NoError(err) - delete(expected, key) + delete(expected, step.key) - _, err = actual.Get(key) + _, err = actual.Get(step.key) assert.Error(err) } else { - err := actual.Put(key, value) + err := actual.Put(step.key, step.value) assert.NoError(err) - expected[key] = value + expected[step.key] = step.value - found, err := actual.Get(key) + found, err := actual.Get(step.key) assert.NoError(err) - assert.Equal(value, found) + assert.Equal(step.value, found) } - assert.Equal(expected, maps.Collect(actual.Entries())) - drop = !drop + assert.Equal(expected, maps.Collect(actual.Entries())) } }) } diff --git a/go.mod b/go.mod index 492bb0f..79459b8 100644 --- a/go.mod +++ b/go.mod @@ -7,5 +7,6 @@ require github.com/stretchr/testify v1.11.1 require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/trailofbits/go-fuzz-utils v0.0.0-20260318143407-0907cafe7589 gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c4c1710..9c6f565 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/trailofbits/go-fuzz-utils v0.0.0-20260318143407-0907cafe7589 h1:UmBZCTPdDYore2IEHN+U4eIqEaRq6METh9pKiPumkqc= +github.com/trailofbits/go-fuzz-utils v0.0.0-20260318143407-0907cafe7589/go.mod h1:zh+T+w9XT/3o4E0WLEGCdmLJ8Yqx/zY3o538tQY3OjY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/table.go b/table.go index a978902..9b64def 100644 --- a/table.go +++ b/table.go @@ -28,7 +28,7 @@ func (t Table[K, V]) Size() int { } func log2(n uint64) (m int) { - return bits.Len64(n) - 1 + return max(0, bits.Len64(n)-1) } func (t Table[K, V]) maxEvictions() int { @@ -36,6 +36,12 @@ func (t Table[K, V]) maxEvictions() int { } func (t Table[K, V]) load() float64 { + // When there are no slots in the table, we still treat the load as 100%. + // Every slot in the table is full. + if t.TotalCapacity() == 0 { + return 1.0 + } + return float64(t.Size()) / float64(t.TotalCapacity()) } @@ -57,6 +63,22 @@ func (t *Table[K, V]) resize(capacity uint64) error { return nil } +func (t *Table[K, V]) grow() error { + var newCapacity uint64 + + if t.TotalCapacity() == 0 { + newCapacity = 1 + } else { + newCapacity = t.bucketA.capacity * t.growthFactor + } + + return t.resize(newCapacity) +} + +func (t *Table[K, V]) shrink() error { + return t.resize(t.bucketA.capacity / t.growthFactor) +} + // Get fetches the value for a key in the [Table]. Returns an error if no value // is found. func (t Table[K, V]) Get(key K) (value V, err error) { @@ -102,7 +124,7 @@ func (t *Table[K, V]) Put(key K, value V) (err error) { return fmt.Errorf("bad hash: resize on load %d/%d = %f", t.Size(), t.TotalCapacity(), t.load()) } - if err := t.resize(t.growthFactor * t.bucketA.capacity); err != nil { + if err := t.grow(); err != nil { return err } @@ -116,7 +138,7 @@ func (t *Table[K, V]) Drop(key K) (err error) { t.bucketB.drop(key) if t.load() < t.minLoadFactor { - return t.resize(t.bucketA.capacity / t.growthFactor) + return t.shrink() } return nil diff --git a/testdata/fuzz/FuzzInsertLookup/663e746d1518e2f5 b/testdata/fuzz/FuzzInsertLookup/663e746d1518e2f5 new file mode 100644 index 0000000..fd87a4f --- /dev/null +++ b/testdata/fuzz/FuzzInsertLookup/663e746d1518e2f5 @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("B000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000") diff --git a/testdata/fuzz/FuzzInsertLookup/7753b9a0c9a15ea7 b/testdata/fuzz/FuzzInsertLookup/7753b9a0c9a15ea7 deleted file mode 100644 index 12889a1..0000000 --- a/testdata/fuzz/FuzzInsertLookup/7753b9a0c9a15ea7 +++ /dev/null @@ -1,4 +0,0 @@ -go test fuzz v1 -[]byte("00000000000000000000000000000000000000000000000000000000000000000000000000000000") -uint32(51) -uint32(38)