From 8c76e5cbfff262f33e75c3a3fd473e810758bd09 Mon Sep 17 00:00:00 2001 From: Joonas Loppi Date: Mon, 2 Sep 2024 12:54:08 +0300 Subject: [PATCH] `typeddigest` package --- pkg/typeddigest/typeddigest.go | 78 +++++++++++++++++++++++++++++ pkg/typeddigest/typeddigest_test.go | 60 ++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 pkg/typeddigest/typeddigest.go create mode 100644 pkg/typeddigest/typeddigest_test.go diff --git a/pkg/typeddigest/typeddigest.go b/pkg/typeddigest/typeddigest.go new file mode 100644 index 0000000..653e519 --- /dev/null +++ b/pkg/typeddigest/typeddigest.go @@ -0,0 +1,78 @@ +// A digest that contains the algoritm as a prefix. Example `sha256:d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592` +package typeddigest + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "strings" +) + +const ( + algSha256 = "sha256" +) + +type Hash struct { + alg string // "sha256" + digest []byte +} + +func (h *Hash) String() string { + return fmt.Sprintf("%s:%x", h.alg, h.digest) +} + +func (h *Hash) Equal(other *Hash) bool { + return h.alg == other.alg && bytes.Equal(h.digest, other.digest) +} + +func Parse(val string) (*Hash, error) { + withErr := func(err error) (*Hash, error) { return nil, fmt.Errorf("typeddigest.Parse: %w", err) } + + pos := strings.Index(val, ":") + if pos == -1 { + return withErr(errors.New("bad format, ':' prefix not found")) + } + + alg := val[:pos] + digestHex := val[pos+1:] + + if alg != algSha256 { + return withErr(fmt.Errorf("unsupported algorithm: %s", alg)) + } + + digest, err := hex.DecodeString(digestHex) + if err != nil { + return withErr(err) + } + + if expectedSize := sha256.Size; len(digest) != expectedSize { + return withErr(fmt.Errorf("wrong digest size: expected %d; got %d", expectedSize, len(digest))) + } + + return &Hash{alg, digest}, nil +} + +func DigesterForAlgOf(other *Hash) func(io.Reader) (*Hash, error) { + switch other.alg { + case algSha256: + return Sha256 + default: + return func(io.Reader) (*Hash, error) { + return nil, fmt.Errorf("typeddigest.DigesterForAlgOf: unsupported algorithm: %s", other.alg) + } + } +} + +func Sha256(input io.Reader) (*Hash, error) { + withErr := func(err error) (*Hash, error) { return nil, fmt.Errorf("typeddigest.Sha256: %w", err) } + + hash := sha256.New() + if _, err := io.Copy(hash, input); err != nil { + return withErr(err) + } + + return &Hash{algSha256, hash.Sum(nil)}, nil +} diff --git a/pkg/typeddigest/typeddigest_test.go b/pkg/typeddigest/typeddigest_test.go new file mode 100644 index 0000000..d8ece59 --- /dev/null +++ b/pkg/typeddigest/typeddigest_test.go @@ -0,0 +1,60 @@ +package typeddigest + +import ( + "fmt" + "strings" + "testing" + + "github.com/function61/gokit/testing/assert" +) + +func TestParse(t *testing.T) { + for _, tc := range []struct { + input string + output string + }{ + { + "sha256:d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592", + "ok", + }, + { + "sha256:88", + "ERROR: typeddigest.Parse: wrong digest size: expected 32; got 1", + }, + { + "md5:d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592", + "ERROR: typeddigest.Parse: unsupported algorithm: md5", + }, + { + "", + "ERROR: typeddigest.Parse: bad format, ':' prefix not found", + }, + { + "sha256:nothex", + "ERROR: typeddigest.Parse: encoding/hex: invalid byte: U+006E 'n'", + }, + } { + tc := tc // pin + t.Run(tc.input, func(t *testing.T) { + th, err := Parse(tc.input) + asOutput := func() string { + if err != nil { + return fmt.Sprintf("ERROR: %v", err) + } else { + // test stability + assert.EqualString(t, th.String(), tc.input) + return "ok" + } + }() + + assert.EqualString(t, asOutput, tc.output) + }) + } +} + +func TestSha256(t *testing.T) { + th, err := Sha256(strings.NewReader("The quick brown fox jumps over the lazy dog")) + assert.Ok(t, err) + + assert.EqualString(t, th.String(), "sha256:d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592") +}