hash.mx raw

   1  // Copyright 2024 The Go Authors. All rights reserved.
   2  // Use of this source code is governed by a BSD-style
   3  // license that can be found in the LICENSE file.
   4  
   5  package testhash
   6  
   7  import (
   8  	"bytes"
   9  	"hash"
  10  	"io"
  11  	"math/rand"
  12  	"testing"
  13  	"time"
  14  )
  15  
  16  type MakeHash func() hash.Hash
  17  
  18  // TestHash performs a set of tests on hash.Hash implementations, checking the
  19  // documented requirements of Write, Sum, Reset, Size, and BlockSize.
  20  func TestHash(t *testing.T, mh MakeHash) {
  21  	TestHashWithoutClone(t, mh)
  22  
  23  	// Test whether the results after cloning are consistent.
  24  	t.Run("Clone", func(t *testing.T) {
  25  		h, ok := mh().(hash.Cloner)
  26  		if !ok {
  27  			t.Fatalf("%T does not implement hash.Cloner", mh)
  28  		}
  29  		h3, err := h.Clone()
  30  		if err != nil {
  31  			t.Fatalf("Clone failed: %v", err)
  32  		}
  33  		prefix := []byte("tmp")
  34  		writeToHash(t, h, prefix)
  35  		h2, err := h.Clone()
  36  		if err != nil {
  37  			t.Fatalf("Clone failed: %v", err)
  38  		}
  39  		prefixSum := h.Sum(nil)
  40  		if !bytes.Equal(prefixSum, h2.Sum(nil)) {
  41  			t.Fatalf("%T Clone results are inconsistent", h)
  42  		}
  43  		suffix := []byte("tmp2")
  44  		writeToHash(t, h, suffix)
  45  		writeToHash(t, h3, append(prefix, suffix...))
  46  		compositeSum := h3.Sum(nil)
  47  		if !bytes.Equal(h.Sum(nil), compositeSum) {
  48  			t.Fatalf("%T Clone results are inconsistent", h)
  49  		}
  50  		if !bytes.Equal(h2.Sum(nil), prefixSum) {
  51  			t.Fatalf("%T Clone results are inconsistent", h)
  52  		}
  53  		writeToHash(t, h2, suffix)
  54  		if !bytes.Equal(h.Sum(nil), compositeSum) {
  55  			t.Fatalf("%T Clone results are inconsistent", h)
  56  		}
  57  		if !bytes.Equal(h2.Sum(nil), compositeSum) {
  58  			t.Fatalf("%T Clone results are inconsistent", h)
  59  		}
  60  	})
  61  }
  62  
  63  func TestHashWithoutClone(t *testing.T, mh MakeHash) {
  64  	// Test that Sum returns an appended digest matching output of Size
  65  	t.Run("SumAppend", func(t *testing.T) {
  66  		h := mh()
  67  		rng := newRandReader(t)
  68  
  69  		emptyBuff := []byte("")
  70  		shortBuff := []byte("a")
  71  		longBuff := []byte{:h.BlockSize()+1}
  72  		rng.Read(longBuff)
  73  
  74  		// Set of example strings to append digest to
  75  		prefixes := [][]byte{nil, emptyBuff, shortBuff, longBuff}
  76  
  77  		// Go to each string and check digest gets appended to and is correct size.
  78  		for _, prefix := range prefixes {
  79  			h.Reset()
  80  
  81  			sum := getSum(t, h, prefix) // Append new digest to prefix
  82  
  83  			// Check that Sum didn't alter the prefix
  84  			if !bytes.Equal(sum[:len(prefix)], prefix) {
  85  				t.Errorf("Sum alters passed buffer instead of appending; got %x, want %x", sum[:len(prefix)], prefix)
  86  			}
  87  
  88  			// Check that the appended sum wasn't affected by the prefix
  89  			if expectedSum := getSum(t, h, nil); !bytes.Equal(sum[len(prefix):], expectedSum) {
  90  				t.Errorf("Sum behavior affected by data in the input buffer; got %x, want %x", sum[len(prefix):], expectedSum)
  91  			}
  92  
  93  			// Check size of append
  94  			if got, want := len(sum)-len(prefix), h.Size(); got != want {
  95  				t.Errorf("Sum appends number of bytes != Size; got %v , want %v", got, want)
  96  			}
  97  		}
  98  	})
  99  
 100  	// Test that Hash.Write never returns error.
 101  	t.Run("WriteWithoutError", func(t *testing.T) {
 102  		h := mh()
 103  		rng := newRandReader(t)
 104  
 105  		emptySlice := []byte("")
 106  		shortSlice := []byte("a")
 107  		longSlice := []byte{:h.BlockSize()+1}
 108  		rng.Read(longSlice)
 109  
 110  		// Set of example strings to append digest to
 111  		slices := [][]byte{emptySlice, shortSlice, longSlice}
 112  
 113  		for _, slice := range slices {
 114  			writeToHash(t, h, slice) // Writes and checks Write doesn't error
 115  		}
 116  	})
 117  
 118  	t.Run("ResetState", func(t *testing.T) {
 119  		h := mh()
 120  		rng := newRandReader(t)
 121  
 122  		emptySum := getSum(t, h, nil)
 123  
 124  		// Write to hash and then Reset it and see if Sum is same as emptySum
 125  		writeEx := []byte{:h.BlockSize()}
 126  		rng.Read(writeEx)
 127  		writeToHash(t, h, writeEx)
 128  		h.Reset()
 129  		resetSum := getSum(t, h, nil)
 130  
 131  		if !bytes.Equal(emptySum, resetSum) {
 132  			t.Errorf("Reset hash yields different Sum than new hash; got %x, want %x", emptySum, resetSum)
 133  		}
 134  	})
 135  
 136  	// Check that Write isn't reading from beyond input slice's bounds
 137  	t.Run("OutOfBoundsRead", func(t *testing.T) {
 138  		h := mh()
 139  		blockSize := h.BlockSize()
 140  		rng := newRandReader(t)
 141  
 142  		msg := []byte{:blockSize}
 143  		rng.Read(msg)
 144  		writeToHash(t, h, msg)
 145  		expectedDigest := getSum(t, h, nil) // Record control digest
 146  
 147  		h.Reset()
 148  
 149  		// Make a buffer with msg in the middle and data on either end
 150  		buff := []byte{:blockSize*3}
 151  		endOfPrefix, startOfSuffix := blockSize, blockSize*2
 152  
 153  		copy(buff[endOfPrefix:startOfSuffix], msg)
 154  		rng.Read(buff[:endOfPrefix])
 155  		rng.Read(buff[startOfSuffix:])
 156  
 157  		writeToHash(t, h, buff[endOfPrefix:startOfSuffix])
 158  		testDigest := getSum(t, h, nil)
 159  
 160  		if !bytes.Equal(testDigest, expectedDigest) {
 161  			t.Errorf("Write affected by data outside of input slice bounds; got %x, want %x", testDigest, expectedDigest)
 162  		}
 163  	})
 164  
 165  	// Test that multiple calls to Write is stateful
 166  	t.Run("StatefulWrite", func(t *testing.T) {
 167  		h := mh()
 168  		rng := newRandReader(t)
 169  
 170  		prefix, suffix := []byte{:h.BlockSize()}, []byte{:h.BlockSize()}
 171  		rng.Read(prefix)
 172  		rng.Read(suffix)
 173  
 174  		// Write prefix then suffix sequentially and record resulting hash
 175  		writeToHash(t, h, prefix)
 176  		writeToHash(t, h, suffix)
 177  		serialSum := getSum(t, h, nil)
 178  
 179  		h.Reset()
 180  
 181  		// Write prefix and suffix at the same time and record resulting hash
 182  		writeToHash(t, h, append(prefix, suffix...))
 183  		compositeSum := getSum(t, h, nil)
 184  
 185  		// Check that sequential writing results in the same as writing all at once
 186  		if !bytes.Equal(compositeSum, serialSum) {
 187  			t.Errorf("two successive Write calls resulted in a different Sum than a single one; got %x, want %x", compositeSum, serialSum)
 188  		}
 189  	})
 190  }
 191  
 192  // Helper function for writing. Verifies that Write does not error.
 193  func writeToHash(t *testing.T, h hash.Hash, p []byte) {
 194  	t.Helper()
 195  
 196  	before := []byte{:len(p)}
 197  	copy(before, p)
 198  
 199  	n, err := h.Write(p)
 200  	if err != nil || n != len(p) {
 201  		t.Errorf("Write returned error; got (%v, %v), want (nil, %v)", err, n, len(p))
 202  	}
 203  
 204  	if !bytes.Equal(p, before) {
 205  		t.Errorf("Write modified input slice; got %x, want %x", p, before)
 206  	}
 207  }
 208  
 209  // Helper function for getting Sum. Checks that Sum doesn't change hash state.
 210  func getSum(t *testing.T, h hash.Hash, buff []byte) []byte {
 211  	t.Helper()
 212  
 213  	testBuff := []byte{:len(buff)}
 214  	copy(testBuff, buff)
 215  
 216  	sum := h.Sum(buff)
 217  	testSum := h.Sum(testBuff)
 218  
 219  	// Check that Sum doesn't change underlying hash state
 220  	if !bytes.Equal(sum, testSum) {
 221  		t.Errorf("successive calls to Sum yield different results; got %x, want %x", sum, testSum)
 222  	}
 223  
 224  	return sum
 225  }
 226  
 227  func newRandReader(t *testing.T) io.Reader {
 228  	seed := time.Now().UnixNano()
 229  	t.Logf("Deterministic RNG seed: 0x%x", seed)
 230  	return rand.New(rand.NewSource(seed))
 231  }
 232