diff --git a/internal/bsr/internal/checksum/checksum.go b/internal/bsr/internal/checksum/checksum.go index ab101b357..0e9300989 100644 --- a/internal/bsr/internal/checksum/checksum.go +++ b/internal/bsr/internal/checksum/checksum.go @@ -8,13 +8,13 @@ package checksum import ( "context" + "crypto/sha256" "errors" "fmt" "io" "io/fs" "github.com/hashicorp/boundary/internal/bsr/internal/is" - "github.com/hashicorp/go-kms-wrapping/v2/extras/crypto" ) const ( @@ -45,9 +45,9 @@ type File struct { // std interfaces that to not take a context as an arg. ctx context.Context - // Performs the checksuming, provides implementations for Write and - // WriteString - *crypto.Sha256SumWriter + // Performs the checksuming, provides implementations for Write, + // WriteString, and WriteAndClose + *Sha256SumWriter checksumWriter io.Writer @@ -66,7 +66,7 @@ func NewFile(ctx context.Context, f writerFile, cs io.Writer) (*File, error) { return nil, fmt.Errorf("%s: missing checksum writer: %w", op, errInvalidParameter) } - wrapped, err := crypto.NewSha256SumWriter(ctx, f) + wrapped, err := NewSha256SumWriter(ctx, f, sha256.New()) if err != nil { return nil, err } @@ -107,7 +107,7 @@ func (f *File) Close() error { closeErrors = errors.Join(closeErrors, fmt.Errorf("%s: %w", op, err)) } - sum, err := f.Sha256SumWriter.Sum(f.ctx, crypto.WithHexEncoding(true)) + sum, err := f.Sha256SumWriter.Sum(f.ctx, WithHexEncoding(true)) if err != nil { closeErrors = errors.Join(closeErrors, fmt.Errorf("%s: %w", op, err)) return closeErrors @@ -133,12 +133,13 @@ func (f *File) WriteAndClose(b []byte) (int, error) { return 0, closeErrors } - n, err := f.underlying.WriteAndClose(b) + // f.Sha256SumWriter will close f.underlying + n, err := f.Sha256SumWriter.WriteAndClose(b) if err != nil { - return 0, fmt.Errorf("%s: %w", op, err) + closeErrors = errors.Join(closeErrors, fmt.Errorf("%s: %w", op, err)) } - sum, err := f.Sha256SumWriter.Sum(f.ctx, crypto.WithHexEncoding(true)) + sum, err := f.Sha256SumWriter.Sum(f.ctx, WithHexEncoding(true)) if err != nil { closeErrors = errors.Join(closeErrors, fmt.Errorf("%s: %w", op, err)) return 0, closeErrors diff --git a/internal/bsr/internal/checksum/checksum_test.go b/internal/bsr/internal/checksum/checksum_test.go index 225c33ae3..ff89cd082 100644 --- a/internal/bsr/internal/checksum/checksum_test.go +++ b/internal/bsr/internal/checksum/checksum_test.go @@ -87,7 +87,7 @@ func TestFile(t *testing.T) { testString, testSum + " test\n", nil, - errors.New("checksum.(File).Close: crypto.(Sha256SumWriter).Close: close error"), + errors.New("checksum.(File).Close: checksum.(Sha256SumWriter).Close: close error"), true, }, { diff --git a/internal/bsr/internal/checksum/options.go b/internal/bsr/internal/checksum/options.go new file mode 100644 index 000000000..42bd67782 --- /dev/null +++ b/internal/bsr/internal/checksum/options.go @@ -0,0 +1,151 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package checksum + +import ( + "errors" +) + +// Option - a type that wraps an interface for compile-time safety but can +// contain an option for this package or for wrappers implementing this +// interface. +type Option func() interface{} + +// getOpts iterates the inbound Options and returns a struct +func getOpts(opt ...Option) (*options, error) { + // First, separate out options into local and global + opts := getDefaultOptions() + for _, o := range opt { + if o == nil { + continue + } + iface := o() + switch to := iface.(type) { + case OptionFunc: + if err := to(&opts); err != nil { + return nil, err + } + default: + return nil, errors.New("option passed into util wrapping options handler" + + " that is not from this package; this is likely due to the wrapper being" + + " invoked as a plugin but options being sent from a specific wrapper package;" + + " use WithConfigMap to send options via the plugin interface") + } + + } + return &opts, nil +} + +// OptionFunc holds a function with local options +type OptionFunc func(*options) error + +// options = how options are represented +type options struct { + withPrefix string + withPrk []byte + withEd25519 bool + withBase64Encoding bool + withBase58Encoding bool + withMarshaledSigInfo bool + withSalt []byte + withInfo []byte + WithHexEncoding bool +} + +func getDefaultOptions() options { + return options{} +} + +// WithPrefix allows an optional prefix to be specified for the data returned +func WithPrefix(prefix string) Option { + return func() interface{} { + return OptionFunc(func(o *options) error { + o.withPrefix = prefix + return nil + }) + } +} + +// WithPrk allows an optional PRK (pseudorandom key) to be specified for an +// operation. If you're using this option with HmacSha256, you might consider +// using HmacSha256WithPrk instead. +func WithPrk(prk []byte) Option { + return func() interface{} { + return OptionFunc(func(o *options) error { + o.withPrk = prk + return nil + }) + } +} + +// WithEd25519 allows an optional request to use ed25519 during the operation +func WithEd25519() Option { + return func() interface{} { + return OptionFunc(func(o *options) error { + o.withEd25519 = true + return nil + }) + } +} + +// WithBase64Encoding allows an optional request to base64 encode the data returned +func WithBase64Encoding() Option { + return func() interface{} { + return OptionFunc(func(o *options) error { + o.withBase64Encoding = true + return nil + }) + } +} + +// WithBase58Encoding allows an optional request to base58 encode the data returned +func WithBase58Encoding() Option { + return func() interface{} { + return OptionFunc(func(o *options) error { + o.withBase58Encoding = true + return nil + }) + } +} + +// WithMarshaledSigInfo allows an optional request to wrap the returned data into +// a marshaled wrapping.SigInfo protobuf +func WithMarshaledSigInfo() Option { + return func() interface{} { + return OptionFunc(func(o *options) error { + o.withMarshaledSigInfo = true + return nil + }) + } +} + +// WithSalt allows optional salt to be specified for an operation. +func WithSalt(salt []byte) Option { + return func() interface{} { + return OptionFunc(func(o *options) error { + o.withSalt = salt + return nil + }) + } +} + +// WithInfo allows optional info to be specified for an operation. +func WithInfo(info []byte) Option { + return func() interface{} { + return OptionFunc(func(o *options) error { + o.withInfo = info + return nil + }) + } +} + +// WithHexEncoding allows an optional request to use hex encoding. +func WithHexEncoding(withHexEncoding bool) Option { + return func() interface{} { + return OptionFunc(func(o *options) error { + o.WithHexEncoding = withHexEncoding + return nil + }) + } +} diff --git a/internal/bsr/internal/checksum/options_test.go b/internal/bsr/internal/checksum/options_test.go new file mode 100644 index 000000000..e20cf99bc --- /dev/null +++ b/internal/bsr/internal/checksum/options_test.go @@ -0,0 +1,116 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package checksum + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_getOpts(t *testing.T) { + t.Parallel() + assert := assert.New(t) + require := require.New(t) + t.Run("WithPrefix", func(t *testing.T) { + testOpts, err := getOpts() + require.NoError(err) + assert.Empty(testOpts.withPrefix) + + opts := getDefaultOptions() + opts.withPrefix = "test" + testOpts, err = getOpts(WithPrefix("test")) + require.NoError(err) + assert.EqualValues(opts, *testOpts) + }) + t.Run("WithPrk", func(t *testing.T) { + testOpts, err := getOpts() + require.NoError(err) + assert.Empty(testOpts.withPrk) + + opts := getDefaultOptions() + opts.withPrk = []byte("test") + testOpts, err = getOpts(WithPrk([]byte("test"))) + require.NoError(err) + assert.EqualValues(opts, *testOpts) + }) + t.Run("WithEd25519", func(t *testing.T) { + testOpts, err := getOpts() + require.NoError(err) + assert.False(testOpts.withEd25519) + + opts := getDefaultOptions() + opts.withEd25519 = true + testOpts, err = getOpts(WithEd25519()) + require.NoError(err) + assert.EqualValues(opts, *testOpts) + }) + t.Run("WithBase64Encoding", func(t *testing.T) { + testOpts, err := getOpts() + require.NoError(err) + assert.False(testOpts.withBase64Encoding) + + opts := getDefaultOptions() + opts.withBase64Encoding = true + testOpts, err = getOpts(WithBase64Encoding()) + require.NoError(err) + assert.EqualValues(opts, *testOpts) + }) + t.Run("WithBase58Encoding", func(t *testing.T) { + testOpts, err := getOpts() + require.NoError(err) + assert.False(testOpts.withBase58Encoding) + + opts := getDefaultOptions() + opts.withBase58Encoding = true + testOpts, err = getOpts(WithBase58Encoding()) + require.NoError(err) + assert.EqualValues(opts, *testOpts) + }) + t.Run("WithMarshaledSigInfo", func(t *testing.T) { + testOpts, err := getOpts() + require.NoError(err) + assert.False(testOpts.withMarshaledSigInfo) + + opts := getDefaultOptions() + opts.withMarshaledSigInfo = true + testOpts, err = getOpts(WithMarshaledSigInfo()) + require.NoError(err) + assert.EqualValues(opts, *testOpts) + }) + t.Run("WithSalt", func(t *testing.T) { + testOpts, err := getOpts() + require.NoError(err) + assert.Empty(testOpts.withSalt) + + opts := getDefaultOptions() + opts.withSalt = []byte("test") + testOpts, err = getOpts(WithSalt([]byte("test"))) + require.NoError(err) + assert.EqualValues(opts, *testOpts) + }) + t.Run("WithInfo", func(t *testing.T) { + testOpts, err := getOpts() + require.NoError(err) + assert.Empty(testOpts.withInfo) + + opts := getDefaultOptions() + opts.withInfo = []byte("test") + testOpts, err = getOpts(WithInfo([]byte("test"))) + require.NoError(err) + assert.EqualValues(opts, *testOpts) + }) + t.Run("WithHexEncoding", func(t *testing.T) { + testOpts, err := getOpts() + require.NoError(err) + assert.False(testOpts.WithHexEncoding) + + opts := getDefaultOptions() + opts.WithHexEncoding = true + testOpts, err = getOpts(WithHexEncoding(true)) + require.NoError(err) + assert.EqualValues(opts, *testOpts) + }) +} diff --git a/internal/bsr/internal/checksum/sha256sums.go b/internal/bsr/internal/checksum/sha256sums.go index 2096c5e42..d8e2127dd 100644 --- a/internal/bsr/internal/checksum/sha256sums.go +++ b/internal/bsr/internal/checksum/sha256sums.go @@ -5,11 +5,25 @@ package checksum import ( "bufio" + "context" + "encoding/hex" + "errors" "fmt" + "hash" "io" "strings" + "sync" + + "github.com/hashicorp/boundary/internal/storage" + "github.com/hashicorp/boundary/internal/util" ) +// ErrShortWrite represents an error where the number of written bytes between two writers do not match +var ErrShortWrite = errors.New("short write") + +// ErrInvalidParameter represents an error where an invalid parameter value was provided +var ErrInvalidParameter = errors.New("invalid parameter") + const ( sha256sumSep = " " sha256sumBinarySep = " *" @@ -70,3 +84,114 @@ func (s Sha256Sums) Sum(f string) ([]byte, error) { return sum, nil } + +// Sha256SumWriter is used to write to a hash and produce a sum. +// It implements io.WriterCloser, io.StringWriter and a storage.Writer. +type Sha256SumWriter struct { + l sync.Mutex + hash hash.Hash + underlying storage.Writer +} + +// NewSha256SumWriter creates a new Sha256SumWriter +func NewSha256SumWriter(ctx context.Context, file storage.Writer, hash hash.Hash) (*Sha256SumWriter, error) { + const op = "checksum.NewSha256SumWriter" + if util.IsNil(file) { + return nil, fmt.Errorf("%s: missing writer: %w", op, ErrInvalidParameter) + } + if util.IsNil(hash) { + return nil, fmt.Errorf("%s: missing hash: %w", op, ErrInvalidParameter) + } + return &Sha256SumWriter{ + hash: hash, + underlying: file, + }, nil +} + +// Write will write the bytes to the hash. Implements the required io.Writer +// func. +func (w *Sha256SumWriter) Write(b []byte) (int, error) { + const op = "checksum.(Sha256SumWriter).Write" + w.l.Lock() + defer w.l.Unlock() + n, err := w.hash.Write(b) + if err != nil { + return n, fmt.Errorf("%s: %w", op, err) + } + m, err := w.underlying.Write(b) + if err != nil { + return m, fmt.Errorf("%s: %w", op, err) + } + if n != m { + return m, ErrShortWrite + } + return n, nil +} + +// WriteString will write the string to hash. +func (w *Sha256SumWriter) WriteString(s string) (int, error) { + const op = "checksum.(Sha256SumWriter).WriteString" + n, err := w.hash.Write([]byte(s)) + if err != nil { + return n, fmt.Errorf("%s: %w", op, err) + } + m, err := w.underlying.Write([]byte(s)) + if err != nil { + return m, fmt.Errorf("%s: %w", op, err) + } + if n != m { + return m, ErrShortWrite + } + return n, nil +} + +// WriteAndClose will write the bytes to the hash and then will the Close +func (w *Sha256SumWriter) WriteAndClose(b []byte) (int, error) { + const op = "checksum.(Sha256SumWriter).WriteAndClose" + w.l.Lock() + defer w.l.Unlock() + n, err := w.hash.Write(b) + if err != nil { + return n, fmt.Errorf("%s: %w", op, err) + } + m, err := w.underlying.WriteAndClose(b) + if err != nil { + return m, fmt.Errorf("%s: %w", op, err) + } + if n != m { + return m, ErrShortWrite + } + return n, nil +} + +// Close checks to see if the Sha256SumWriter implements the optional io.Closer +// and if so, then Close() is called; otherwise this is a noop +func (w *Sha256SumWriter) Close() error { + const op = "checksum.(Sha256SumWriter).Close" + var i interface{} = w.underlying + if v, ok := i.(io.Closer); ok { + if err := v.Close(); err != nil { + return fmt.Errorf("%s: %w", op, err) + } + } + return nil +} + +// Sum will sum the hash. Options supported: WithHexEncoding +func (w *Sha256SumWriter) Sum(_ context.Context, opt ...Option) ([]byte, error) { + const op = "checksum.(Sha256SumWriter).Sum" + opts, err := getOpts(opt...) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + w.l.Lock() + defer w.l.Unlock() + h := w.hash.Sum(nil) + switch { + case opts.WithHexEncoding: + encodedHex := hex.EncodeToString(h[:]) + return []byte(encodedHex), nil + default: + return h, nil + } +} diff --git a/internal/bsr/internal/checksum/sha256sums_test.go b/internal/bsr/internal/checksum/sha256sums_test.go index aa372b859..36e94aa9f 100644 --- a/internal/bsr/internal/checksum/sha256sums_test.go +++ b/internal/bsr/internal/checksum/sha256sums_test.go @@ -4,16 +4,440 @@ package checksum_test import ( + "context" + "crypto/sha256" + "encoding/hex" "fmt" + "hash" "io" "strings" "testing" "github.com/hashicorp/boundary/internal/bsr/internal/checksum" + "github.com/hashicorp/boundary/internal/storage" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +var _ storage.Writer = (*testFile)(nil) +var _ hash.Hash = (*testHash)(nil) + +type testFile struct { + b strings.Builder + closed bool + + withWrittenBytes int + withWriteError error +} + +func (t *testFile) Write(b []byte) (int, error) { + if t.withWriteError != nil || t.withWrittenBytes > 0 { + return t.withWrittenBytes, t.withWriteError + } + return t.b.Write(b) +} + +func (t *testFile) WriteString(s string) (int, error) { + if t.withWriteError != nil || t.withWrittenBytes > 0 { + return t.withWrittenBytes, t.withWriteError + } + return t.b.WriteString(s) +} + +func (t *testFile) WriteAndClose(b []byte) (int, error) { + if t.withWriteError != nil || t.withWrittenBytes > 0 { + return t.withWrittenBytes, t.withWriteError + } + t.closed = true + return t.b.Write(b) +} + +func (t *testFile) Close() error { + t.closed = true + return nil +} + +type testHash struct { + withWrittenBytes int + withWriteError error +} + +func (t *testHash) Write(b []byte) (int, error) { + return t.withWrittenBytes, t.withWriteError +} + +func (t *testHash) Sum(b []byte) []byte { + return nil +} + +func (t *testHash) Reset() {} + +func (t *testHash) Size() int { + return 0 +} + +func (t *testHash) BlockSize() int { + return 0 +} + +func Test_NewSha256SumWriter(t *testing.T) { + t.Parallel() + cases := []struct { + name string + writer storage.Writer + hash hash.Hash + expectedErr string + }{ + { + name: "missing writer", + expectedErr: "checksum.NewSha256SumWriter: missing writer: invalid parameter", + }, + { + name: "missing hash", + writer: &testFile{}, + expectedErr: "checksum.NewSha256SumWriter: missing hash: invalid parameter", + }, + { + name: "success", + writer: &testFile{}, + hash: sha256.New(), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + w, err := checksum.NewSha256SumWriter(context.Background(), tc.writer, tc.hash) + if tc.expectedErr != "" { + require.Error(err) + assert.Nil(w) + assert.ErrorContains(err, tc.expectedErr) + return + } + require.NoError(err) + assert.NotNil(w) + }) + } +} + +func TestSha256SumWriter_Write(t *testing.T) { + t.Parallel() + cases := []struct { + name string + writer *checksum.Sha256SumWriter + expectedErr string + data []byte + }{ + { + name: "with-hash-error", + writer: func() *checksum.Sha256SumWriter { + writer, err := checksum.NewSha256SumWriter(context.Background(), &testFile{}, &testHash{ + withWriteError: fmt.Errorf("failed hash write"), + }) + require.NoError(t, err) + return writer + }(), + data: []byte{}, + expectedErr: "failed hash write", + }, + { + name: "with-file-error", + writer: func() *checksum.Sha256SumWriter { + writer, err := checksum.NewSha256SumWriter(context.Background(), &testFile{ + withWriteError: fmt.Errorf("failed file write"), + }, &testHash{ + withWrittenBytes: 10, + }) + require.NoError(t, err) + return writer + }(), + data: []byte{}, + expectedErr: "failed file write", + }, + { + name: "with-short-write-error", + writer: func() *checksum.Sha256SumWriter { + writer, err := checksum.NewSha256SumWriter(context.Background(), &testFile{ + withWrittenBytes: 9, + }, &testHash{ + withWrittenBytes: 10, + }) + require.NoError(t, err) + return writer + }(), + data: []byte{}, + expectedErr: "short write", + }, + { + name: "success", + writer: func() *checksum.Sha256SumWriter { + writer, err := checksum.NewSha256SumWriter(context.Background(), &testFile{}, sha256.New()) + require.NoError(t, err) + return writer + }(), + data: []byte("hello_world"), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + _, err := tc.writer.Write(tc.data) + if tc.expectedErr != "" { + require.Error(err) + assert.ErrorContains(err, tc.expectedErr) + return + } + require.NoError(err) + }) + } +} + +func TestSha256SumWriter_WriteString(t *testing.T) { + t.Parallel() + cases := []struct { + name string + writer *checksum.Sha256SumWriter + expectedErr string + }{ + { + name: "with-hash-error", + writer: func() *checksum.Sha256SumWriter { + writer, err := checksum.NewSha256SumWriter(context.Background(), &testFile{}, &testHash{ + withWriteError: fmt.Errorf("failed hash write"), + }) + require.NoError(t, err) + return writer + }(), + expectedErr: "failed hash write", + }, + { + name: "with-file-error", + writer: func() *checksum.Sha256SumWriter { + writer, err := checksum.NewSha256SumWriter(context.Background(), &testFile{ + withWriteError: fmt.Errorf("failed file write"), + }, &testHash{ + withWrittenBytes: 10, + }) + require.NoError(t, err) + return writer + }(), + expectedErr: "failed file write", + }, + { + name: "with-short-write-error", + writer: func() *checksum.Sha256SumWriter { + writer, err := checksum.NewSha256SumWriter(context.Background(), &testFile{ + withWrittenBytes: 9, + }, &testHash{ + withWrittenBytes: 10, + }) + require.NoError(t, err) + return writer + }(), + expectedErr: "short write", + }, + { + name: "success", + writer: func() *checksum.Sha256SumWriter { + writer, err := checksum.NewSha256SumWriter(context.Background(), &testFile{ + withWrittenBytes: 10, + }, &testHash{ + withWrittenBytes: 10, + }) + require.NoError(t, err) + return writer + }(), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + _, err := tc.writer.WriteString("") + if tc.expectedErr != "" { + require.Error(err) + assert.ErrorContains(err, tc.expectedErr) + return + } + require.NoError(err) + }) + } +} + +func TestSha256SumWriter_WriteAndClose(t *testing.T) { + t.Parallel() + cases := []struct { + name string + writer *checksum.Sha256SumWriter + expectedErr string + }{ + { + name: "with-hash-error", + writer: func() *checksum.Sha256SumWriter { + writer, err := checksum.NewSha256SumWriter(context.Background(), &testFile{}, &testHash{ + withWriteError: fmt.Errorf("failed hash write"), + }) + require.NoError(t, err) + return writer + }(), + expectedErr: "failed hash write", + }, + { + name: "with-file-error", + writer: func() *checksum.Sha256SumWriter { + writer, err := checksum.NewSha256SumWriter(context.Background(), &testFile{ + withWriteError: fmt.Errorf("failed file write"), + }, &testHash{ + withWrittenBytes: 10, + }) + require.NoError(t, err) + return writer + }(), + expectedErr: "failed file write", + }, + { + name: "with-short-write-error", + writer: func() *checksum.Sha256SumWriter { + writer, err := checksum.NewSha256SumWriter(context.Background(), &testFile{ + withWrittenBytes: 9, + }, &testHash{ + withWrittenBytes: 10, + }) + require.NoError(t, err) + return writer + }(), + expectedErr: "short write", + }, + { + name: "success", + writer: func() *checksum.Sha256SumWriter { + writer, err := checksum.NewSha256SumWriter(context.Background(), &testFile{ + withWrittenBytes: 10, + }, &testHash{ + withWrittenBytes: 10, + }) + require.NoError(t, err) + return writer + }(), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + _, err := tc.writer.WriteAndClose([]byte{}) + if tc.expectedErr != "" { + require.Error(err) + assert.ErrorContains(err, tc.expectedErr) + return + } + require.NoError(err) + }) + } +} + +func TestSha256SumWriter_Sum(t *testing.T) { + t.Parallel() + testCtx := context.Background() + testBytes := []byte("test-bytes") + tests := []struct { + name string + data []byte + sumWriter *checksum.Sha256SumWriter + opt []checksum.Option + wantSum []byte + wantErr bool + wantErrIs error + wantErrContains string + }{ + { + name: "success", + data: testBytes, + sumWriter: func() *checksum.Sha256SumWriter { + w, err := checksum.NewSha256SumWriter(testCtx, &testFile{}, sha256.New()) + require.NoError(t, err) + return w + }(), + wantSum: func() []byte { + hasher := sha256.New() + _, err := hasher.Write(testBytes) + require.NoError(t, err) + _, err = hasher.Write(testBytes) + require.NoError(t, err) + return hasher.Sum(nil) + }(), + }, + { + name: "success-with-hex-encoding", + data: testBytes, + sumWriter: func() *checksum.Sha256SumWriter { + w, err := checksum.NewSha256SumWriter(testCtx, &testFile{}, sha256.New()) + require.NoError(t, err) + return w + }(), + opt: []checksum.Option{checksum.WithHexEncoding(true)}, + wantSum: func() []byte { + hasher := sha256.New() + _, err := hasher.Write(testBytes) + require.NoError(t, err) + _, err = hasher.Write(testBytes) + require.NoError(t, err) + h := hasher.Sum(nil) + return []byte(hex.EncodeToString(h[:])) + }(), + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + _, err := tc.sumWriter.Write(tc.data) + require.NoError(err) + _, err = tc.sumWriter.WriteString(string(tc.data)) + require.NoError(err) + sum, err := tc.sumWriter.Sum(testCtx, tc.opt...) + if tc.wantErr { + require.Error(err) + assert.Empty(sum) + if tc.wantErrIs != nil { + assert.ErrorIs(err, tc.wantErrIs) + } + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.Equal(tc.wantSum, sum) + require.NoError(tc.sumWriter.Close()) + }) + } + + t.Run("success-with-closer", func(t *testing.T) { + c := testFile{ + b: strings.Builder{}, + closed: false, + } + w, err := checksum.NewSha256SumWriter(testCtx, &c, sha256.New()) + require.NoError(t, err) + + hasher := sha256.New() + _, err = hasher.Write(testBytes) + require.NoError(t, err) + _, err = hasher.Write(testBytes) + require.NoError(t, err) + wantSum := hasher.Sum(nil) + + assert, require := assert.New(t), require.New(t) + _, err = w.Write(testBytes) + require.NoError(err) + _, err = w.WriteString(string(testBytes)) + require.NoError(err) + sum, err := w.Sum(testCtx) + require.NoError(err) + assert.Equal(wantSum, sum) + require.NoError(w.Close()) + + require.True(c.closed) + }) +} + func TestLoadSha256Sums(t *testing.T) { cases := []struct { name string