Source file src/internal/testhash/hash.go

     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 := make([]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 := make([]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 := make([]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 := make([]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 := make([]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 := make([]byte, h.BlockSize()), make([]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 := make([]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 := make([]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  

View as plain text