Skip to content

Commit d0d82f6

Browse files
committed
Add SHA256 helper with LF-normalized hash
1 parent 4eee0f3 commit d0d82f6

File tree

2 files changed

+144
-0
lines changed

2 files changed

+144
-0
lines changed

utils/sha256.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package utils
2+
3+
import (
4+
"crypto/sha256"
5+
"io"
6+
)
7+
8+
// ComputeSHA256AndLF reads from reader until EOF and returns:
9+
// 1. the SHA256 of the original bytes
10+
// 2. the SHA256 after normalizing CRLF to LF
11+
func ComputeSHA256AndLF(reader io.Reader) (raw [sha256.Size]byte, lf [sha256.Size]byte, err error) {
12+
rawHash := sha256.New()
13+
lfHash := sha256.New()
14+
lfWriter := Dos2UnixWriter(lfHash)
15+
16+
if _, err = io.Copy(io.MultiWriter(rawHash, lfWriter), reader); err != nil {
17+
_ = lfWriter.Close()
18+
return raw, lf, err
19+
}
20+
if err = lfWriter.Close(); err != nil {
21+
return raw, lf, err
22+
}
23+
24+
copy(raw[:], rawHash.Sum(nil))
25+
copy(lf[:], lfHash.Sum(nil))
26+
return raw, lf, nil
27+
}

utils/sha256_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package utils
2+
3+
import (
4+
"crypto/sha256"
5+
"errors"
6+
"io"
7+
"strings"
8+
"testing"
9+
)
10+
11+
func TestComputeSHA256AndLF(t *testing.T) {
12+
t.Parallel()
13+
14+
tests := []struct {
15+
name string
16+
input string
17+
chunkSizes []int
18+
wantLF string
19+
}{
20+
{
21+
name: "empty",
22+
input: "",
23+
wantLF: "",
24+
},
25+
{
26+
name: "plain text",
27+
input: "alpha\nbeta\ngamma",
28+
wantLF: "alpha\nbeta\ngamma",
29+
},
30+
{
31+
name: "crlf normalized across chunks",
32+
input: "a\r\nb\r\nc\r\n",
33+
chunkSizes: []int{2, 1, 2, 1, 1},
34+
wantLF: "a\nb\nc\n",
35+
},
36+
{
37+
name: "lone cr preserved",
38+
input: "a\rb\r\nc\r",
39+
chunkSizes: []int{1, 1, 2, 1, 1},
40+
wantLF: "a\rb\nc\r",
41+
},
42+
}
43+
44+
for _, tt := range tests {
45+
tt := tt
46+
t.Run(tt.name, func(t *testing.T) {
47+
t.Parallel()
48+
49+
reader := io.Reader(strings.NewReader(tt.input))
50+
if len(tt.chunkSizes) > 0 {
51+
reader = &chunkedReader{
52+
data: []byte(tt.input),
53+
chunkSizes: tt.chunkSizes,
54+
}
55+
}
56+
57+
raw, lf, err := ComputeSHA256AndLF(reader)
58+
if err != nil {
59+
t.Fatalf("ComputeSHA256AndLF failed: %v", err)
60+
}
61+
62+
wantRaw := sha256.Sum256([]byte(tt.input))
63+
wantLF := sha256.Sum256([]byte(tt.wantLF))
64+
if raw != wantRaw {
65+
t.Fatalf("raw hash mismatch: got %x want %x", raw, wantRaw)
66+
}
67+
if lf != wantLF {
68+
t.Fatalf("LF hash mismatch: got %x want %x", lf, wantLF)
69+
}
70+
})
71+
}
72+
}
73+
74+
func TestComputeSHA256AndLF_ReadError(t *testing.T) {
75+
t.Parallel()
76+
77+
wantErr := errors.New("boom")
78+
_, _, err := ComputeSHA256AndLF(errorReader{err: wantErr})
79+
if !errors.Is(err, wantErr) {
80+
t.Fatalf("expected %v, got %v", wantErr, err)
81+
}
82+
}
83+
84+
type chunkedReader struct {
85+
data []byte
86+
chunkSizes []int
87+
offset int
88+
index int
89+
}
90+
91+
func (r *chunkedReader) Read(p []byte) (int, error) {
92+
if r.offset >= len(r.data) {
93+
return 0, io.EOF
94+
}
95+
96+
size := len(p)
97+
if r.index < len(r.chunkSizes) && r.chunkSizes[r.index] < size {
98+
size = r.chunkSizes[r.index]
99+
}
100+
remaining := len(r.data) - r.offset
101+
if size > remaining {
102+
size = remaining
103+
}
104+
105+
copy(p, r.data[r.offset:r.offset+size])
106+
r.offset += size
107+
r.index++
108+
return size, nil
109+
}
110+
111+
type errorReader struct {
112+
err error
113+
}
114+
115+
func (r errorReader) Read(_ []byte) (int, error) {
116+
return 0, r.err
117+
}

0 commit comments

Comments
 (0)