Skip to content

Commit

Permalink
digest: promote blake3 to first-class digest
Browse files Browse the repository at this point in the history
The dual module approach for blake3 was slightly awkward. Since it
provides similar usability with a massive bump in performance, it's
extremely likely to land as a registered algorithm in the image-spec.

This PR removes the secondary module, which made it difficult to test as
a unit. This may break users who are using HEAD versions of the package.
For a new release, this will be backwards compatible. The other drawback
is that the zeebo/blake3 will now be a dependency but this can be
replaced transparently by the standard libary in the future.

In addition to promoting blake3, this makes a few style adjustments to
be in line with Go's style guidelines.

Signed-off-by: Stephen Day <[email protected]>
  • Loading branch information
stevvooe committed Nov 11, 2021
1 parent b3cf61a commit b941ada
Show file tree
Hide file tree
Showing 11 changed files with 202 additions and 271 deletions.
3 changes: 0 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,3 @@ jobs:
uses: actions/checkout@v2
- name: Test
run: go test -v ./...
- name: Test Blake3
run: go test -v ./...
working-directory: blake3
37 changes: 20 additions & 17 deletions algorithm.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,22 @@ const (
// project. Other digests may be used but this one is the primary storage
// digest.
Canonical = SHA256
// BLAKE3 is the blake3 algorithm with the default 256-bit output size
// github.com/opencontainers/go-digest/blake3 should be imported to make it available
BLAKE3 Algorithm = "blake3"
)

var (
algorithmRegexp = regexp.MustCompile(`^[a-z0-9]+([+._-][a-z0-9]+)*$`)
)

// CryptoHash is the interface that any hash algorithm must implement
// CryptoHash is the interface that any digest algorithm must implement
type CryptoHash interface {
// Available reports whether the given hash function is usable in the current binary.
// Available reports whether the given hash function is usable in the
// current binary.
Available() bool
// Size returns the length, in bytes, of a digest resulting from the given hash function.
// Size returns the length, in bytes, of a digest resulting from the given
// hash function.
Size() int
// New returns a new hash.Hash calculating the given hash function. If the hash function is not
// available, it may panic.
// New returns a new hash.Hash calculating the given hash function. If the
// hash function is not available, it may panic.
New() hash.Hash
}

Expand All @@ -69,29 +68,33 @@ var (
algorithmsLock sync.RWMutex
)

// RegisterAlgorithm may be called to dynamically register an algorithm. The implementation is a CryptoHash, and
// the regex is meant to match the hash portion of the algorithm. If a duplicate algorithm is already registered,
// the return value is false, otherwise if registration was successful the return value is true.
// RegisterAlgorithm may be called to dynamically register an algorithm. The
// implementation is a CryptoHash, and the regex is meant to match the hash
// portion of the algorithm. If a duplicate algorithm is already registered, the
// return value is false, otherwise if registration was successful the return
// value is true.
//
// The algorithm encoding format must be based on hex.
//
// The algorithm name must be conformant to the BNF specification in the OCI image-spec, otherwise the function
// will panic.
// The algorithm name must be conformant to the BNF specification in the OCI
// image-spec, otherwise the function will panic.
func RegisterAlgorithm(algorithm Algorithm, implementation CryptoHash) bool {
algorithmsLock.Lock()
defer algorithmsLock.Unlock()

if !algorithmRegexp.MatchString(string(algorithm)) {
panic(fmt.Sprintf("Algorithm %s has a name which does not fit within the allowed grammar", algorithm))
panic(fmt.Sprintf("algorithm %s has a name which does not fit within the allowed grammar", algorithm))
}

if _, ok := algorithms[algorithm]; ok {
return false
}

algorithms[algorithm] = implementation
// We can do this since the Digest function below only implements a hex digest. If we open this in the future
// we need to allow for alternative digest algorithms to be implemented and for the user to pass their own

// We can do this since the Digest function below only implements a hex
// digest. If we open this in the future we need to allow for alternative
// digest algorithms to be implemented and for the user to pass their own
// custom regexp.
anchoredEncodedRegexps[algorithm] = hexDigestRegex(implementation)
return true
Expand Down Expand Up @@ -166,7 +169,7 @@ func (a Algorithm) Hash() hash.Hash {
if !a.Available() {
// Empty algorithm string is invalid
if a == "" {
panic(fmt.Sprintf("empty digest algorithm, validate before calling Algorithm.Hash()"))
panic("empty digest algorithm, validate before calling Algorithm.Hash()")
}

// NOTE(stevvooe): A missing hash is usually a programming error that
Expand Down
13 changes: 10 additions & 3 deletions blake3/blake3.go → blake3.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,24 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blake3
package digest

import (
"hash"

"github.com/opencontainers/go-digest"
"github.com/zeebo/blake3"
)

const (
// Blake3 is the blake3 algorithm with the default 256-bit output size
Blake3 Algorithm = "blake3"

// BLAKE3 is deprecated. Use the symbol "Blake3" instead.
BLAKE3 = Blake3
)

func init() {
digest.RegisterAlgorithm(digest.BLAKE3, &blake3hash{})
RegisterAlgorithm(Blake3, &blake3hash{})
}

type blake3hash struct{}
Expand Down
38 changes: 0 additions & 38 deletions blake3/blake3_test.go

This file was deleted.

12 changes: 0 additions & 12 deletions blake3/go.mod

This file was deleted.

157 changes: 157 additions & 0 deletions digest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright 2019, 2020 OCI Contributors
// Copyright 2017 Docker, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package digest

import (
"testing"
)

func TestParseDigest(t *testing.T) {
for _, testcase := range []struct {
input string
err error
algorithm Algorithm
encoded string
}{
{
input: "sha256:e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b",
algorithm: "sha256",
encoded: "e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b",
},
{
input: "sha384:d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d",
algorithm: "sha384",
encoded: "d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d",
},
{
input: "blake3:af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262",
algorithm: "blake3",
encoded: "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262",
},
{
// empty
input: "",
err: ErrDigestInvalidFormat,
},
{
// whitespace only
input: " ",
err: ErrDigestInvalidFormat,
},
{
// empty hex
input: "sha256:",
err: ErrDigestInvalidFormat,
},
{
// hex with correct length, but whitespace only
input: "sha256: ",
err: ErrDigestInvalidFormat,
},
{
// empty hex
input: ":",
err: ErrDigestInvalidFormat,
},
{
// just hex
input: "d41d8cd98f00b204e9800998ecf8427e",
err: ErrDigestInvalidFormat,
},
{
// not hex
input: "sha256:d41d8cd98f00b204e9800m98ecf8427e",
err: ErrDigestInvalidLength,
},
{
// too short
input: "sha256:abcdef0123456789",
err: ErrDigestInvalidLength,
},
{
// too short (from different algorithm)
input: "sha512:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
err: ErrDigestInvalidLength,
},
{
input: "foo:d41d8cd98f00b204e9800998ecf8427e",
err: ErrDigestUnsupported,
},
{
// repeated separators
input: "sha384__foo+bar:d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d",
err: ErrDigestInvalidFormat,
},
{
// ensure that we parse, but we don't have support for the algorithm
input: "sha384.foo+bar:d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d",
algorithm: "sha384.foo+bar",
encoded: "d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d",
err: ErrDigestUnsupported,
},
{
input: "sha384_foo+bar:d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d",
algorithm: "sha384_foo+bar",
encoded: "d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d",
err: ErrDigestUnsupported,
},
{
input: "sha256+b64:LCa0a2j_xo_5m0U8HTBBNBNCLXBkg7-g-YpeiGJm564",
algorithm: "sha256+b64",
encoded: "LCa0a2j_xo_5m0U8HTBBNBNCLXBkg7-g-YpeiGJm564",
err: ErrDigestUnsupported,
},
{
input: "sha256:E58FCF7418D4390DEC8E8FB69D88C06EC07039D651FEDD3AA72AF9972E7D046B",
err: ErrDigestInvalidFormat,
},
} {
t.Run(testcase.input, func(t *testing.T) {
digest, err := Parse(testcase.input)
if err != testcase.err {
t.Fatalf("error differed from expected while parsing %q: %v != %v", testcase.input, err, testcase.err)
}

if testcase.err != nil {
return
}

if digest.Algorithm() != testcase.algorithm {
t.Fatalf("incorrect algorithm for parsed digest: %q != %q", digest.Algorithm(), testcase.algorithm)
}

if digest.Encoded() != testcase.encoded {
t.Fatalf("incorrect hex for parsed digest: %q != %q", digest.Encoded(), testcase.encoded)
}

// Parse string return value and check equality
newParsed, err := Parse(digest.String())

if err != nil {
t.Fatalf("unexpected error parsing input %q: %v", testcase.input, err)
}

if newParsed != digest {
t.Fatalf("expected equal: %q != %q", newParsed, digest)
}

newFromHex := NewDigestFromEncoded(newParsed.Algorithm(), newParsed.Encoded())
if newFromHex != digest {
t.Fatalf("%v != %v", newFromHex, digest)
}
})
}
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
module github.com/opencontainers/go-digest

go 1.13

require github.com/zeebo/blake3 v0.2.0
6 changes: 4 additions & 2 deletions blake3/go.sum → go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.1.1 h1:Nbsts7DdKThRHHd+YNlqiGlRqGEF2bE2eXN+xQ1hsEs=
github.com/zeebo/blake3 v0.1.1/go.mod h1:G9pM4qQwjRzF1/v7+vabMj/c5mWpGZ2Wzo3Eb4z0pb4=
github.com/zeebo/blake3 v0.2.0 h1:1SGx3IvKWFUU/xl+/7kjdcjjMcvVSm+3dMo/N42afC8=
github.com/zeebo/blake3 v0.2.0/go.mod h1:G9pM4qQwjRzF1/v7+vabMj/c5mWpGZ2Wzo3Eb4z0pb4=
github.com/zeebo/pcg v1.0.0 h1:dt+dx+HvX8g7Un32rY9XWoYnd0NmKmrIzpHF7qiTDj0=
github.com/zeebo/pcg v1.0.0/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
golang.org/x/sys v0.0.0-20201014080544-cc95f250f6bc h1:HVFDs9bKvTxP6bh1Rj9MCSo+UmafQtI8ZWDPVwVk9g4=
golang.org/x/sys v0.0.0-20201014080544-cc95f250f6bc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Loading

0 comments on commit b941ada

Please sign in to comment.