Skip to content

Commit

Permalink
Refactors out common Checksum concept (#399)
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanmoran committed Sep 27, 2022
1 parent cd69bd3 commit 246a747
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 121 deletions.
27 changes: 0 additions & 27 deletions cargo/are_checksums_equal.go

This file was deleted.

46 changes: 0 additions & 46 deletions cargo/are_checksums_equal_test.go

This file was deleted.

42 changes: 42 additions & 0 deletions cargo/checksum.go
@@ -0,0 +1,42 @@
package cargo

import (
"strings"
)

// Checksum represents a checksum algorithm and hash pair formatted as
// algorithm:hash.
type Checksum string

// Algorithm returns the algorithm portion of the checksum string. If that
// portion is missing, it defaults to "sha256".
func (c Checksum) Algorithm() string {
algorithm, _, found := strings.Cut(string(c), ":")
if !found {
return "sha256"
}

return algorithm
}

// Hash returns the hexidecimal encoded hash portion of the checksum string.
func (c Checksum) Hash() string {
_, hash, found := strings.Cut(string(c), ":")
if !found {
hash = string(c)
}

return hash
}

// EqualTo returns true only when the given checksum algorithms and hashes
// match.
func (c Checksum) Match(o Checksum) bool {
return strings.EqualFold(c.Algorithm(), o.Algorithm()) && c.Hash() == o.Hash()
}

// EqualTo returns true only when the given checksum formatted string
// algorithms and hashes match.
func (c Checksum) MatchString(o string) bool {
return c.Match(Checksum(o))
}
54 changes: 54 additions & 0 deletions cargo/checksum_test.go
@@ -0,0 +1,54 @@
package cargo_test

import (
"fmt"
"testing"

"github.com/paketo-buildpacks/packit/v2/cargo"
"github.com/sclevine/spec"

. "github.com/onsi/gomega"
)

func testChecksum(t *testing.T, context spec.G, it spec.S) {
Expect := NewWithT(t).Expect

context("Matching", func() {
type testCaseType struct {
c1 string
c2 string
result bool
}

for _, tc := range []testCaseType{
{"", "", true},
{"c", "c", true},
{"sha256:c", "c", true},
{"c", "sha256:c", true},
{"md5:c", "md5:c", true},
{"md5:c", ":c", false},
{":", ":", true},
{":c", ":c", true},
{"", "c", false},
{"c", "", false},
{"c", "z", false},
{"md5:c", "sha256:c", false},
{"md5:c", "md5:d", false},
{"md5:c:d", "md5:c:d", true},
{"md5:c", "md5:c:d", false},
{":", "::", false},
{":", ":::", false},
} {

// NOTE: we need to keep a "loop-local" variable to use in the "it
// function closure" below, otherwise the value of tc will simply be the
// last element in the slice every time the test is evaluated.
ca, cb, sb, result := cargo.Checksum(tc.c1), cargo.Checksum(tc.c2), tc.c2, tc.result

it(fmt.Sprintf("will check result %q == %q", ca, cb), func() {
Expect(ca.Match(cb)).To(Equal(result))
Expect(ca.MatchString(sb)).To(Equal(result))
})
}
})
}
2 changes: 1 addition & 1 deletion cargo/init_test.go
Expand Up @@ -15,7 +15,7 @@ func TestUnitCargo(t *testing.T) {
suite("DirectoryDuplicator", testDirectoryDuplicator)
suite("Transport", testTransport)
suite("ValidatedReader", testValidatedReader)
suite("AreChecksumsEqual", testAreChecksumsEqual)
suite("Checksum", testChecksum)
suite.Run(t)
}

Expand Down
21 changes: 7 additions & 14 deletions cargo/validated_reader.go
Expand Up @@ -9,14 +9,13 @@ import (
"fmt"
"hash"
"io"
"strings"
)

var ChecksumValidationError = errors.New("validation error: checksum does not match")

type ValidatedReader struct {
reader io.Reader
checksum string
checksum Checksum
hash hash.Hash
}

Expand All @@ -26,28 +25,22 @@ type errorHash struct {
err error
}

func NewValidatedReader(reader io.Reader, checksum string) ValidatedReader {
splitChecksum := strings.SplitN(checksum, ":", 2)
if len(splitChecksum) != 2 {
return ValidatedReader{hash: errorHash{err: fmt.Errorf(`malformed checksum %q: checksum should be formatted "algorithm:hash"`, checksum)}}
}

checksumValue := splitChecksum[1]

func NewValidatedReader(reader io.Reader, sum string) ValidatedReader {
var hash hash.Hash
checksum := Checksum(sum)

switch splitChecksum[0] {
switch checksum.Algorithm() {
case "sha256":
hash = sha256.New()
case "sha512":
hash = sha512.New()
default:
return ValidatedReader{hash: errorHash{err: fmt.Errorf("unsupported algorithm %q: the following algorithms are support [sha256, sha512]", splitChecksum[0])}}
return ValidatedReader{hash: errorHash{err: fmt.Errorf("unsupported algorithm %q: the following algorithms are supported [sha256, sha512]", checksum.Algorithm())}}
}

return ValidatedReader{
reader: reader,
checksum: checksumValue,
checksum: checksum,
hash: hash,
}
}
Expand Down Expand Up @@ -75,7 +68,7 @@ func (vr ValidatedReader) Read(p []byte) (int, error) {

if done {
sum := hex.EncodeToString(vr.hash.Sum(nil))
if sum != vr.checksum {
if sum != vr.checksum.Hash() {
return n, ChecksumValidationError
}

Expand Down
11 changes: 1 addition & 10 deletions cargo/validated_reader_test.go
Expand Up @@ -60,21 +60,12 @@ func testValidatedReader(t *testing.T, context spec.G, it spec.S) {
})

context("failure cases", func() {
context("there is a malformed checksum", func() {
it("returns an error", func() {
vr := cargo.NewValidatedReader(strings.NewReader("some-contents"), "malformed checksum")

_, err := io.Copy(buffer, vr)
Expect(err).To(MatchError(`malformed checksum "malformed checksum": checksum should be formatted "algorithm:hash"`))
})
})

context("there is an unsupported algorithm", func() {
it("returns an error", func() {
vr := cargo.NewValidatedReader(strings.NewReader("some-contents"), "magic:6e32ea34db1b3755d7dec972eb72c705338f0dd8e0be881d966963438fb2e800")

_, err := io.Copy(buffer, vr)
Expect(err).To(MatchError(`unsupported algorithm "magic": the following algorithms are support [sha256, sha512]`))
Expect(err).To(MatchError(`unsupported algorithm "magic": the following algorithms are supported [sha256, sha512]`))
})
})
})
Expand Down
3 changes: 3 additions & 0 deletions postal/buildpack.go
Expand Up @@ -6,8 +6,11 @@ import (
"time"

"github.com/BurntSushi/toml"
"github.com/paketo-buildpacks/packit/v2/cargo"
)

type Checksum = cargo.Checksum

// Dependency is a representation of a buildpack dependency.
type Dependency struct {
// CPE is the Common Platform Enumerator for the dependency. Used in legacy
Expand Down
45 changes: 22 additions & 23 deletions postal/service.go
Expand Up @@ -29,9 +29,10 @@ type Transport interface {
Drop(root, uri string) (io.ReadCloser, error)
}

//go:generate faux --interface MappingResolver --output fakes/mapping_resolver.go
// MappingResolver serves as the interface that looks up platform binding provided
// dependency mappings given a SHA256
//
//go:generate faux --interface MappingResolver --output fakes/mapping_resolver.go
type MappingResolver interface {
FindDependencyMapping(SHA256, platformDir string) (string, error)
}
Expand Down Expand Up @@ -199,18 +200,30 @@ func (s Service) GenerateBillOfMaterials(dependencies ...Dependency) []packit.BO
var entries []packit.BOMEntry
for _, dependency := range dependencies {

algorithm, hash := determineChecksum(dependency.Checksum, dependency.SHA256)
paketoSbomAlgorithm, err := paketosbom.GetBOMChecksumAlgorithm(algorithm)
checksum := Checksum(dependency.SHA256)
if len(dependency.Checksum) > 0 {
checksum = Checksum(dependency.Checksum)
}

hash := checksum.Hash()
paketoSbomAlgorithm, err := paketosbom.GetBOMChecksumAlgorithm(checksum.Algorithm())
// GetBOMChecksumAlgorithm will set algorithm to UNKNOWN if there is an error
if err != nil {
if err != nil || hash == "" {
paketoSbomAlgorithm = paketosbom.UNKNOWN
hash = ""
}

srcAlgorithm, srcHash := determineChecksum(dependency.SourceChecksum, dependency.SourceSHA256)
paketoSbomSrcAlgorithm, err := paketosbom.GetBOMChecksumAlgorithm(srcAlgorithm)
sourceChecksum := Checksum(dependency.SourceSHA256)
if len(dependency.Checksum) > 0 {
sourceChecksum = Checksum(dependency.SourceChecksum)
}

sourceHash := sourceChecksum.Hash()
paketoSbomSrcAlgorithm, err := paketosbom.GetBOMChecksumAlgorithm(sourceChecksum.Algorithm())
// GetBOMChecksumAlgorithm will set algorithm to UNKNOWN if there is an error
if err != nil {
srcHash = ""
if err != nil || sourceHash == "" {
paketoSbomSrcAlgorithm = paketosbom.UNKNOWN
sourceHash = ""
}

paketoBomMetadata := paketosbom.BOMMetadata{
Expand All @@ -223,7 +236,7 @@ func (s Service) GenerateBillOfMaterials(dependencies ...Dependency) []packit.BO
Source: paketosbom.BOMSource{
Checksum: paketosbom.BOMChecksum{
Algorithm: paketoSbomSrcAlgorithm,
Hash: srcHash,
Hash: sourceHash,
},
URI: dependency.Source,
},
Expand Down Expand Up @@ -255,17 +268,3 @@ func (s Service) GenerateBillOfMaterials(dependencies ...Dependency) []packit.BO

return entries
}

func determineChecksum(checksumField, sha256Field string) (string, string) {
// A well-formed checksum field (algorithm:hash) takes precedence over a SHA256 field
algorithm, hash, found := strings.Cut(checksumField, ":")
if found {
return algorithm, hash
}

if len(sha256Field) > 0 {
return "SHA256", sha256Field
}

return "", ""
}
39 changes: 39 additions & 0 deletions postal/service_test.go
Expand Up @@ -1227,6 +1227,45 @@ version = "this is super not semver"
})
})

context("when the checksum algorithm is unknown", func() {
it("generates a BOM with the empty/unknown checksum", func() {
entries := service.GenerateBillOfMaterials(
postal.Dependency{
ID: "some-entry",
Name: "Some Entry",
Checksum: "no-such-algo:some-hash",
Source: "some-source",
SourceChecksum: "no-such-algo:some-hash",
Stacks: []string{"some-stack"},
URI: "some-uri",
Version: "1.2.3",
},
)

Expect(entries).To(Equal([]packit.BOMEntry{
{
Name: "Some Entry",
Metadata: paketosbom.BOMMetadata{
Checksum: paketosbom.BOMChecksum{
Algorithm: paketosbom.UNKNOWN,
Hash: "",
},
Source: paketosbom.BOMSource{
Checksum: paketosbom.BOMChecksum{
Algorithm: paketosbom.UNKNOWN,
Hash: "",
},
URI: "some-source",
},

URI: "some-uri",
Version: "1.2.3",
},
},
}))
})
})

context("when there is no checksum or SHA256", func() {
it("generates a BOM with the empty/unknown checksum", func() {
entries := service.GenerateBillOfMaterials(
Expand Down

0 comments on commit 246a747

Please sign in to comment.