Skip to content

Commit

Permalink
typeddigest package
Browse files Browse the repository at this point in the history
  • Loading branch information
joonas-fi committed Sep 2, 2024
1 parent d50c716 commit 8c76e5c
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 0 deletions.
78 changes: 78 additions & 0 deletions pkg/typeddigest/typeddigest.go
Original file line number Diff line number Diff line change
@@ -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, '<alg>:' 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
}
60 changes: 60 additions & 0 deletions pkg/typeddigest/typeddigest_test.go
Original file line number Diff line number Diff line change
@@ -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, '<alg>:' 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")
}

0 comments on commit 8c76e5c

Please sign in to comment.