diff --git a/internal/bsr/internal/checksum/sha256sums.go b/internal/bsr/internal/checksum/sha256sums.go new file mode 100644 index 000000000..24d968f84 --- /dev/null +++ b/internal/bsr/internal/checksum/sha256sums.go @@ -0,0 +1,73 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package checksum + +import ( + "bufio" + "fmt" + "io" + "strings" +) + +const ( + sha256sumSep = " " + sha256sumBinarySep = " *" + sha256HexLength = 64 +) + +// Sha256Sums is a map of file names with their corresponding SHA256SUM. +type Sha256Sums map[string][]byte + +// LoadSha256Sums reads from an io.Reader to populate the Sha256Sums map The +// reader is expected to return the sums in a format that is compatible with +// sha256sum, that is each line should contain the hex encoded sum followed by +// a separator and then the file name. The separator should either be two +// spaces (" ") or a space an asterisks (" *"). The former indicates a text +// file and the later a binary file. However, there is no difference between +// these for GNU systems. Therefore this also makes no distinction between the +// separators and checks for either. +// +// See: https://man7.org/linux/man-pages/man1/sha256sum.1.html +func LoadSha256Sums(r io.Reader) (Sha256Sums, error) { + const op = "checksum.LoadSha256Sums" + + s := make(Sha256Sums) + scanner := bufio.NewScanner(r) + for scanner.Scan() { + var sum, file string + var ok bool + + sum, file, ok = strings.Cut(scanner.Text(), sha256sumSep) + if !ok { + sum, file, ok = strings.Cut(scanner.Text(), sha256sumBinarySep) + } + if !ok { + return nil, fmt.Errorf("%s: improperly formated line", op) + } + + if len(sum) != sha256HexLength { + return nil, fmt.Errorf("%s: improperly formated line", op) + } + + if _, dup := s[file]; dup { + return nil, fmt.Errorf("%s: duplicate file", op) + } + + s[file] = []byte(sum) + } + + return s, nil +} + +// Sum returns the sum for the provided file, or an error if there is no sum. +func (s Sha256Sums) Sum(f string) ([]byte, error) { + const op = "checksum.(Sha256Sums).Sum" + + sum, ok := s[f] + if !ok { + return nil, fmt.Errorf("%s: no sum for file %s", op, f) + } + + return sum, nil +} diff --git a/internal/bsr/internal/checksum/sha256sums_test.go b/internal/bsr/internal/checksum/sha256sums_test.go new file mode 100644 index 000000000..c91b639b5 --- /dev/null +++ b/internal/bsr/internal/checksum/sha256sums_test.go @@ -0,0 +1,119 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package checksum_test + +import ( + "fmt" + "io" + "strings" + "testing" + + "github.com/hashicorp/boundary/internal/bsr/internal/checksum" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLoadSha256Sums(t *testing.T) { + cases := []struct { + name string + r io.Reader + want checksum.Sha256Sums + wantErr error + }{ + { + "valid-multi-line-text", + strings.NewReader(`ffe65f1d98fafedea3514adc956c8ada5980c6c5d2552fd61f48401aefd5c00e file1 +7849ccae8fa91bb281a118041abf921636e68853059329ac68fccd8d518cea53 file2 +`), + checksum.Sha256Sums{ + "file1": []byte("ffe65f1d98fafedea3514adc956c8ada5980c6c5d2552fd61f48401aefd5c00e"), + "file2": []byte("7849ccae8fa91bb281a118041abf921636e68853059329ac68fccd8d518cea53"), + }, + nil, + }, + { + "valid-multi-line-binary", + strings.NewReader(`ffe65f1d98fafedea3514adc956c8ada5980c6c5d2552fd61f48401aefd5c00e *file1 +7849ccae8fa91bb281a118041abf921636e68853059329ac68fccd8d518cea53 *file2 +`), + checksum.Sha256Sums{ + "file1": []byte("ffe65f1d98fafedea3514adc956c8ada5980c6c5d2552fd61f48401aefd5c00e"), + "file2": []byte("7849ccae8fa91bb281a118041abf921636e68853059329ac68fccd8d518cea53"), + }, + nil, + }, + { + "valid-multi-line-text-and-binary", + strings.NewReader(`ffe65f1d98fafedea3514adc956c8ada5980c6c5d2552fd61f48401aefd5c00e file1 +7849ccae8fa91bb281a118041abf921636e68853059329ac68fccd8d518cea53 *file2 +`), + checksum.Sha256Sums{ + "file1": []byte("ffe65f1d98fafedea3514adc956c8ada5980c6c5d2552fd61f48401aefd5c00e"), + "file2": []byte("7849ccae8fa91bb281a118041abf921636e68853059329ac68fccd8d518cea53"), + }, + nil, + }, + { + "valid-multi-line-binary-and-text", + strings.NewReader(`ffe65f1d98fafedea3514adc956c8ada5980c6c5d2552fd61f48401aefd5c00e *file1 +7849ccae8fa91bb281a118041abf921636e68853059329ac68fccd8d518cea53 file2 +`), + checksum.Sha256Sums{ + "file1": []byte("ffe65f1d98fafedea3514adc956c8ada5980c6c5d2552fd61f48401aefd5c00e"), + "file2": []byte("7849ccae8fa91bb281a118041abf921636e68853059329ac68fccd8d518cea53"), + }, + nil, + }, + { + "invalid-single-space", + strings.NewReader(`ffe65f1d98fafedea3514adc956c8ada5980c6c5d2552fd61f48401aefd5c00e file1`), + nil, + fmt.Errorf("checksum.LoadSha256Sums: improperly formated line"), + }, + { + "invalid-no-file", + strings.NewReader(`ffe65f1d98fafedea3514adc956c8ada5980c6c5d2552fd61f48401aefd5c00e`), + nil, + fmt.Errorf("checksum.LoadSha256Sums: improperly formated line"), + }, + { + "invalid-no-sum-file", + strings.NewReader(`file1`), + nil, + fmt.Errorf("checksum.LoadSha256Sums: improperly formated line"), + }, + { + "invalid-hash-too-short", + strings.NewReader(`ffe65f1d98fafedea3514adc956c8ada5980c6c5d2552fd61f48401aefd5c00 file1`), + nil, + fmt.Errorf("checksum.LoadSha256Sums: improperly formated line"), + }, + { + "invalid-hash-too-long", + strings.NewReader(`ffe65f1d98fafedea3514adc956c8ada5980c6c5d2552fd61f48401aefd5c00ef file1`), + nil, + fmt.Errorf("checksum.LoadSha256Sums: improperly formated line"), + }, + { + "invalid-duplicate-file-name", + strings.NewReader(`ffe65f1d98fafedea3514adc956c8ada5980c6c5d2552fd61f48401aefd5c00e file1 +7849ccae8fa91bb281a118041abf921636e68853059329ac68fccd8d518cea53 file1 +`), + nil, + fmt.Errorf("checksum.LoadSha256Sums: duplicate file"), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := checksum.LoadSha256Sums(tc.r) + if tc.wantErr != nil { + require.EqualError(t, err, tc.wantErr.Error()) + return + } + require.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } +}