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