// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause package hashx import ( "crypto/sha256" "encoding/binary" "hash" "math/rand" "testing" qt "github.com/frankban/quicktest" "tailscale.com/util/must" ) // naiveHash is an obviously correct implementation of Hash. type naiveHash struct { hash.Hash scratch [256]byte } func newNaive() *naiveHash { return &naiveHash{Hash: sha256.New()} } func (h *naiveHash) HashUint8(n uint8) { h.Write(append(h.scratch[:0], n)) } func (h *naiveHash) HashUint16(n uint16) { h.Write(binary.LittleEndian.AppendUint16(h.scratch[:0], n)) } func (h *naiveHash) HashUint32(n uint32) { h.Write(binary.LittleEndian.AppendUint32(h.scratch[:0], n)) } func (h *naiveHash) HashUint64(n uint64) { h.Write(binary.LittleEndian.AppendUint64(h.scratch[:0], n)) } func (h *naiveHash) HashBytes(b []byte) { h.Write(b) } func (h *naiveHash) HashString(s string) { h.Write(append(h.scratch[:0], s...)) } var bytes = func() (out []byte) { out = make([]byte, 130) for i := range out { out[i] = byte(i) } return out }() type hasher interface { HashUint8(uint8) HashUint16(uint16) HashUint32(uint32) HashUint64(uint64) HashBytes([]byte) HashString(string) } func hashSuite(h hasher) { for i := range 10 { for j := 0; j < 10; j++ { h.HashUint8(0x01) h.HashUint8(0x23) h.HashUint32(0x456789ab) h.HashUint8(0xcd) h.HashUint8(0xef) h.HashUint16(0x0123) h.HashUint32(0x456789ab) h.HashUint16(0xcdef) h.HashUint8(0x01) h.HashUint64(0x23456789abcdef01) h.HashUint16(0x2345) h.HashUint8(0x67) h.HashUint16(0x89ab) h.HashUint8(0xcd) } b := bytes[:(i+1)*13] if i%2 == 0 { h.HashBytes(b) } else { h.HashString(string(b)) } } } func Test(t *testing.T) { c := qt.New(t) h1 := must.Get(New512(sha256.New())) h2 := newNaive() hashSuite(h1) hashSuite(h2) c.Assert(h1.Sum(nil), qt.DeepEquals, h2.Sum(nil)) } func TestAllocations(t *testing.T) { c := qt.New(t) c.Run("Sum", func(c *qt.C) { h := must.Get(New512(sha256.New())) c.Assert(testing.AllocsPerRun(100, func() { var a [sha256.Size]byte h.Sum(a[:0]) }), qt.Equals, 0.0) }) c.Run("HashUint8", func(c *qt.C) { h := must.Get(New512(sha256.New())) c.Assert(testing.AllocsPerRun(100, func() { h.HashUint8(0x01) }), qt.Equals, 0.0) }) c.Run("HashUint16", func(c *qt.C) { h := must.Get(New512(sha256.New())) c.Assert(testing.AllocsPerRun(100, func() { h.HashUint16(0x0123) }), qt.Equals, 0.0) }) c.Run("HashUint32", func(c *qt.C) { h := must.Get(New512(sha256.New())) c.Assert(testing.AllocsPerRun(100, func() { h.HashUint32(0x01234567) }), qt.Equals, 0.0) }) c.Run("HashUint64", func(c *qt.C) { h := must.Get(New512(sha256.New())) c.Assert(testing.AllocsPerRun(100, func() { h.HashUint64(0x0123456789abcdef) }), qt.Equals, 0.0) }) c.Run("HashBytes", func(c *qt.C) { h := must.Get(New512(sha256.New())) c.Assert(testing.AllocsPerRun(100, func() { h.HashBytes(bytes) }), qt.Equals, 0.0) }) c.Run("HashString", func(c *qt.C) { h := must.Get(New512(sha256.New())) c.Assert(testing.AllocsPerRun(100, func() { h.HashString("abcdefghijklmnopqrstuvwxyz") }), qt.Equals, 0.0) }) } func Fuzz(f *testing.F) { f.Fuzz(func(t *testing.T, seed int64) { c := qt.New(t) execute := func(h hasher, r *rand.Rand) { for range r.Intn(256) { switch r.Intn(5) { case 0: n := uint8(r.Uint64()) h.HashUint8(n) case 1: n := uint16(r.Uint64()) h.HashUint16(n) case 2: n := uint32(r.Uint64()) h.HashUint32(n) case 3: n := uint64(r.Uint64()) h.HashUint64(n) case 4: b := make([]byte, r.Intn(256)) r.Read(b) h.HashBytes(b) } } } r1 := rand.New(rand.NewSource(seed)) r2 := rand.New(rand.NewSource(seed)) h1 := must.Get(New512(sha256.New())) h2 := newNaive() execute(h1, r1) execute(h2, r2) c.Assert(h1.Sum(nil), qt.DeepEquals, h2.Sum(nil)) execute(h1, r1) execute(h2, r2) c.Assert(h1.Sum(nil), qt.DeepEquals, h2.Sum(nil)) h1.Reset() h2.Reset() execute(h1, r1) execute(h2, r2) c.Assert(h1.Sum(nil), qt.DeepEquals, h2.Sum(nil)) }) } func Benchmark(b *testing.B) { var sum [sha256.Size]byte b.Run("Hash", func(b *testing.B) { b.ReportAllocs() h := must.Get(New512(sha256.New())) for range b.N { h.Reset() hashSuite(h) h.Sum(sum[:0]) } }) b.Run("Naive", func(b *testing.B) { b.ReportAllocs() h := newNaive() for range b.N { h.Reset() hashSuite(h) h.Sum(sum[:0]) } }) }