Skip to content

Commit

Permalink
*: Replace implicit hex hash encoding with Algorithm-based encoding
Browse files Browse the repository at this point in the history
The docs for 'Algorithm' (now 'algorithm') made it clear that the
algorithm identifier was intended to cover both the hash and encoding
algorithms.  Stephen confirmed this interpretation in recent comments
[1,2] as well.  The idea is that a future algorithm may chose a
non-hex encoding like base 64 [1].

The previous implementation, on the other hand, baked the hex encoding
into key locations (e.g. in NewDigestFromBytes and Digest.Validate).
This commit makes the encoding internal to Agorithm instances, adding
a new Encoding interface and an Algorithm.Encoding method.  In order
support external algorithms with different encoding, I've defined an
Algorithm interface with the public API and made renamed the local
implementation of that interface to 'algorithm'.  And now that
Algorithm is a stand-alone interface, I've made the
identifier-to-Algorithm registry public by renaming 'algorithms' to
'Algorithm'.

I've also split out the flag binding into its own AlgorithmFlag
structure, because the old Algorithm.Set pointer assignment no longer
works now that Algorithm is an interface.  Having a separate interface
for the flag.Value also simplifies the Algorithm interface, which
makes it easier to write external Algorithm implementations while
still benefiting from the flag.Value helper.

API changes:

* Additions

  * Algorithms, a newly-public registry of known algorithm
    identifiers.
  * Algorithm.HashSize, which allows us to avoid a
    previously-hardcoded hex assumption in Digest.Validate().
  * Algorithm.Encoding, which allows us to implement NewDigester and
    avoid a previously-hardcoded hex assumption in NewDigestFromBytes.
  * Digest.Hash, as a better name for Digest.Hex.
  * Encoding, a new interface backing Algorithm.Encoding.
  * Hex, the lowercase base 16 Encoding.
  * NewDigester, as a more flexible replacement for NewDigest.
  * NewDigestFromHash, as a better name for NewDigestFromHex.

* Adjustments

  * Algorithm.Hash now returns nil on unavailable algorithms, to
    mirror the old API for Algorithm.Digester.

* Deprecations

  * NewDigest, because NewDigester is a more flexible form of the same
    thing that is just about as compact.
  * NewDigestFromHex, because NewDigestFromHash is a better name for
    this function.
  * Digest.Hex, because Hash is a better name for this method.

* Removals

  * Algorithm.Set, because it is not possible to support this now that
    Algorithm is an interface.

I also switched to black-box testing (with digest_test package names)
because I was getting Hex-undefined errors with white-box testing, and
we don't actually need any white-box access in our test suite.

[1]: opencontainers#3 (comment)
[2]: opencontainers#6 (comment)

Signed-off-by: W. Trevor King <wking@tremily.us>
  • Loading branch information
wking committed Mar 7, 2017
1 parent aa2ec05 commit f97edc8
Show file tree
Hide file tree
Showing 12 changed files with 442 additions and 199 deletions.
177 changes: 105 additions & 72 deletions algorithm.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,70 @@ import (
"io"
)

// Algorithm identifies and implementation of a digester by an identifier.
type Algorithm interface {
// Available returns true if the digest type is available for use. If this
// returns false, Digester and Hash will return nil.
Available() bool

// String returns the canonical algorithm identifier for this algorithm.
String() string

// Size returns number of bytes in the raw (unencoded) hash. This
// gives the size of the hash space. For SHA-256, this will be 32.
Size() int

// HashSize returns number of bytes in the encoded hash. For
// algorithms which use a base b encoding, HashSize will Size() * 8 /
// log2(b), possibly rounded up depending on padding.
HashSize() int

// Encoding returns the algorithm's Encoding.
Encoding() Encoding

// Digester returns a new digester for the algorithm.
Digester() Digester

// Hash returns a new hash as used by the algorithm.
Hash() hash.Hash

// FromReader returns the digest of the reader using the algorithm.
FromReader(rd io.Reader) (Digest, error)

// FromBytes digests the input and returns a Digest.
FromBytes(p []byte) Digest

// FromString digests the string input and returns a Digest.
FromString(s string) Digest
}

// algorithm identifies and implementation of a digester by an identifier.
// Note the that this defines both the hash algorithm used and the string
// encoding.
type Algorithm string
type algorithm struct {
name string
hash crypto.Hash
encoding Encoding
}

// supported digest types
const (
SHA256 Algorithm = "sha256" // sha256 with hex encoding
SHA384 Algorithm = "sha384" // sha384 with hex encoding
SHA512 Algorithm = "sha512" // sha512 with hex encoding
var (
SHA256 = algorithm{
name: "sha256",
hash: crypto.SHA256,
encoding: Hex,
}

SHA384 = algorithm{
name: "sha384",
hash: crypto.SHA384,
encoding: Hex,
}

SHA512 = algorithm{
name: "sha512",
hash: crypto.SHA512,
encoding: Hex,
}

// Canonical is the primary digest algorithm used with the distribution
// project. Other digests may be used but this one is the primary storage
Expand All @@ -39,94 +93,73 @@ const (
)

var (
// TODO(stevvooe): Follow the pattern of the standard crypto package for
// registration of digests. Effectively, we are a registerable set and
// common symbol access.

// algorithms maps values to hash.Hash implementations. Other algorithms
// may be available but they cannot be calculated by the digest package.
algorithms = map[Algorithm]crypto.Hash{
SHA256: crypto.SHA256,
SHA384: crypto.SHA384,
SHA512: crypto.SHA512,
// Algorithms is a registerable set of Algorithm instances.
Algorithms = map[string]Algorithm{
"sha256": SHA256,
"sha384": SHA384,
"sha512": SHA512,
}
)

// Available returns true if the digest type is available for use. If this
// returns false, Digester and Hash will return nil.
func (a Algorithm) Available() bool {
h, ok := algorithms[a]
if !ok {
return false
}

// Available returns true if the algorithm hash is available for
// use. If this returns false, Digester and Hash will return nil.
func (a algorithm) Available() bool {
// check availability of the hash, as well
return h.Available()
return a.hash.Available()
}

func (a Algorithm) String() string {
return string(a)
func (a algorithm) String() string {
return a.name
}

// Size returns number of bytes returned by the hash.
func (a Algorithm) Size() int {
h, ok := algorithms[a]
if !ok {
return 0
}
return h.Size()
// Size returns number of bytes in the raw (unencoded) hash. This
// gives the size of the hash space. For SHA-256, this will be 32.
func (a algorithm) Size() int {
return a.hash.Size()
}

// Set implemented to allow use of Algorithm as a command line flag.
func (a *Algorithm) Set(value string) error {
if value == "" {
*a = Canonical
} else {
// just do a type conversion, support is queried with Available.
*a = Algorithm(value)
// HashSize returns number of bytes in the encoded hash. For
// algorithms which use a base b encoding, HashSize will Size() * 8 /
// log2(b), possibly rounded up depending on padding.
func (a algorithm) HashSize() int {
if a.encoding == Hex {
return 2 * a.Size()
}
panic(fmt.Sprintf("unrecognized encoding %v", a.encoding))
}

// Encoding returns the algorithm's Encoding.
func (a algorithm) Encoding() Encoding {
return a.encoding
}

// Digester returns a new digester for the algorithm. If the algorithm
// is unavailable, nil will be returned. This can be checked by
// calling Available before calling Digester.
func (a algorithm) Digester() Digester {
if !a.Available() {
return ErrDigestUnsupported
return nil
}

return nil
}

// Digester returns a new digester for the specified algorithm. If the algorithm
// does not have a digester implementation, nil will be returned. This can be
// checked by calling Available before calling Digester.
func (a Algorithm) Digester() Digester {
return &digester{
alg: a,
hash: a.Hash(),
name: a.name,
hash: a.Hash(),
encoding: a.encoding,
}
}

// Hash returns a new hash as used by the algorithm. If not available, the
// method will panic. Check Algorithm.Available() before calling.
func (a Algorithm) Hash() hash.Hash {
// Hash returns a new hash as used by the algorithm. If the algorithm
// is unavailable, nil will be returned. This can be checked by
// calling Available before calling Hash().
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()"))
}

// NOTE(stevvooe): A missing hash is usually a programming error that
// must be resolved at compile time. We don't import in the digest
// package to allow users to choose their hash implementation (such as
// when using stevvooe/resumable or a hardware accelerated package).
//
// Applications that may want to resolve the hash at runtime should
// call Algorithm.Available before call Algorithm.Hash().
panic(fmt.Sprintf("%v not available (make sure it is imported)", a))
return nil
}

return algorithms[a].New()
return a.hash.New()
}

// FromReader returns the digest of the reader using the algorithm.
func (a Algorithm) FromReader(rd io.Reader) (Digest, error) {
func (a algorithm) FromReader(rd io.Reader) (Digest, error) {
digester := a.Digester()

if _, err := io.Copy(digester.Hash(), rd); err != nil {
Expand All @@ -137,7 +170,7 @@ func (a Algorithm) FromReader(rd io.Reader) (Digest, error) {
}

// FromBytes digests the input and returns a Digest.
func (a Algorithm) FromBytes(p []byte) Digest {
func (a algorithm) FromBytes(p []byte) Digest {
digester := a.Digester()

if _, err := digester.Hash().Write(p); err != nil {
Expand All @@ -153,6 +186,6 @@ func (a Algorithm) FromBytes(p []byte) Digest {
}

// FromString digests the string input and returns a Digest.
func (a Algorithm) FromString(s string) Digest {
func (a algorithm) FromString(s string) Digest {
return a.FromBytes([]byte(s))
}
45 changes: 45 additions & 0 deletions algorithm_flag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2017 go-digest contributors
//
// 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

type AlgorithmFlag struct {
Algorithm Algorithm
}

// String implements the flag.Value interface for Algorithms.
// https://golang.org/pkg/flag/#Value
func (flag *AlgorithmFlag) String() string {
if flag.Algorithm == nil {
return "unset"
}
return flag.Algorithm.String()
}

// Set implements the flag.Value interface for Algorithms.
// https://golang.org/pkg/flag/#Value
func (flag *AlgorithmFlag) Set(value string) error {
if value == "" {
flag.Algorithm = Canonical
} else {
alg, ok := Algorithms[value]
if !ok || !alg.Available() {
return ErrDigestUnsupported
}

flag.Algorithm = alg
}

return nil
}
77 changes: 77 additions & 0 deletions algorithm_flag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright 2017 go-digest contributors
//
// 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_test

import (
_ "crypto/sha256"
_ "crypto/sha512"
"flag"
"strings"
"testing"

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

func TestFlagInterface(t *testing.T) {
var (
algFlag digest.AlgorithmFlag
flagSet flag.FlagSet
)

flagSet.Var(&algFlag, "algorithm", "set the digest algorithm")
for _, testcase := range []struct {
Name string
Args []string
Err error
Expected digest.Algorithm
}{
{
Name: "Invalid",
Args: []string{"-algorithm", "bean"},
Err: digest.ErrDigestUnsupported,
},
{
Name: "Default",
Args: []string{"unrelated"},
Expected: digest.SHA256,
},
{
Name: "Other",
Args: []string{"-algorithm", "sha512"},
Expected: digest.SHA512,
},
} {
t.Run(testcase.Name, func(t *testing.T) {
algFlag = digest.AlgorithmFlag{
Algorithm: digest.Canonical,
}
if err := flagSet.Parse(testcase.Args); err != testcase.Err {
if testcase.Err == nil {
t.Fatal("unexpected error", err)
}

// check that flag package returns correct error
if !strings.Contains(err.Error(), testcase.Err.Error()) {
t.Fatalf("unexpected error: %v != %v", err, testcase.Err)
}
return
}

if algFlag.Algorithm != testcase.Expected {
t.Fatalf("unexpected algorithm: %v != %v", algFlag.Algorithm, testcase.Expected)
}
})
}
}
Loading

0 comments on commit f97edc8

Please sign in to comment.