mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-12-16 19:34:37 -05:00
228 lines
5.5 KiB
Go
228 lines
5.5 KiB
Go
|
/*
|
||
|
Copyright (c) Edgeless Systems GmbH
|
||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||
|
*/
|
||
|
|
||
|
// sums creates and combines sha256sums files.
|
||
|
package sums
|
||
|
|
||
|
import (
|
||
|
"bufio"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"path"
|
||
|
"regexp"
|
||
|
"sort"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
var sumRegexp = regexp.MustCompile(`^[a-f0-9]{64}$`)
|
||
|
|
||
|
const (
|
||
|
sumLength = 64
|
||
|
minLineLength = sumLength + 2 // 64 hex + 2 space
|
||
|
notFound = -1 // index not found
|
||
|
)
|
||
|
|
||
|
// Create creates a sha256sums file from a list of digests.
|
||
|
// The digests going in are expected to be in the oci format "sha256:hex".
|
||
|
func Create(refs []PinnedImageReference, out io.Writer) error {
|
||
|
coalescedRefs, err := coalesceRefs(refs)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
for _, ref := range coalescedRefs {
|
||
|
if _, err := fmt.Fprintf(out, "%s %s\n", ref.Sum(), ref.Reference()); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Merge merges multiple sha256sums files into one.
|
||
|
func Merge(refs [][]PinnedImageReference, out io.Writer) error {
|
||
|
mergedRefs := make([]PinnedImageReference, 0)
|
||
|
for _, refs := range refs {
|
||
|
mergedRefs = append(mergedRefs, refs...)
|
||
|
}
|
||
|
return Create(mergedRefs, out)
|
||
|
}
|
||
|
|
||
|
// Parse parses a sha256sums file.
|
||
|
func Parse(in io.Reader) ([]PinnedImageReference, error) {
|
||
|
scanner := newLineScanner(in)
|
||
|
return parseLines(scanner)
|
||
|
}
|
||
|
|
||
|
// PinnedImageReference contains the components of a pinned image reference.
|
||
|
type PinnedImageReference struct {
|
||
|
Registry string
|
||
|
Prefix string
|
||
|
Name string
|
||
|
Tag string
|
||
|
Digest string
|
||
|
}
|
||
|
|
||
|
// Reference returns the string representation of the reference (without the digest).
|
||
|
func (r PinnedImageReference) Reference() string {
|
||
|
var base string
|
||
|
if r.Prefix == "" {
|
||
|
base = path.Join(r.Registry, r.Name)
|
||
|
} else {
|
||
|
base = path.Join(r.Registry, r.Prefix, r.Name)
|
||
|
}
|
||
|
var tag string
|
||
|
if r.Tag != "" {
|
||
|
tag = ":" + r.Tag
|
||
|
}
|
||
|
return base + tag
|
||
|
}
|
||
|
|
||
|
// Sum returns the string representation of the digest.
|
||
|
// The digest is expected to be in the oci format "sha256:hex".
|
||
|
// The resulting Sum is only the hex part.
|
||
|
func (r PinnedImageReference) Sum() string {
|
||
|
return r.Digest[len("sha256:"):]
|
||
|
}
|
||
|
|
||
|
// coalesceRefs coalesces the image references.
|
||
|
// It sorts the references and removes duplicates.
|
||
|
// If conflicting digests are found, an error is returned.
|
||
|
func coalesceRefs(refs []PinnedImageReference) ([]PinnedImageReference, error) {
|
||
|
sortRefs(refs)
|
||
|
uniqueRefs := make([]PinnedImageReference, 0, len(refs))
|
||
|
var prev PinnedImageReference
|
||
|
for _, ref := range refs {
|
||
|
equal, err := compareRefs(prev, ref)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
if !equal {
|
||
|
uniqueRefs = append(uniqueRefs, ref)
|
||
|
prev = ref
|
||
|
}
|
||
|
}
|
||
|
return uniqueRefs, nil
|
||
|
}
|
||
|
|
||
|
func sortRefs(refs []PinnedImageReference) {
|
||
|
sort.Slice(refs, func(i, j int) bool {
|
||
|
if refs[i].Registry != refs[j].Registry {
|
||
|
return refs[i].Registry < refs[j].Registry
|
||
|
}
|
||
|
if refs[i].Prefix != refs[j].Prefix {
|
||
|
return refs[i].Prefix < refs[j].Prefix
|
||
|
}
|
||
|
if refs[i].Name != refs[j].Name {
|
||
|
return refs[i].Name < refs[j].Name
|
||
|
}
|
||
|
if refs[i].Tag != refs[j].Tag {
|
||
|
return refs[i].Tag < refs[j].Tag
|
||
|
}
|
||
|
return refs[i].Digest < refs[j].Digest
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// compareRefs compares two references.
|
||
|
// references are equal if they have the same registry, prefix, name, tag and digest.
|
||
|
// If refs only differ in digest, they are inconsistent and an error is returned.
|
||
|
func compareRefs(a, b PinnedImageReference) (equal bool, err error) {
|
||
|
if a.Registry != b.Registry {
|
||
|
return false, nil
|
||
|
}
|
||
|
if a.Prefix != b.Prefix {
|
||
|
return false, nil
|
||
|
}
|
||
|
if a.Name != b.Name {
|
||
|
return false, nil
|
||
|
}
|
||
|
if a.Tag != b.Tag {
|
||
|
return false, nil
|
||
|
}
|
||
|
if a.Digest != b.Digest {
|
||
|
return false, errors.New("conflicting digests")
|
||
|
}
|
||
|
return true, nil
|
||
|
}
|
||
|
|
||
|
func parseLines(scanner scanner) ([]PinnedImageReference, error) {
|
||
|
var refs []PinnedImageReference
|
||
|
for scanner.Scan() {
|
||
|
line := scanner.Text()
|
||
|
if line == "" {
|
||
|
continue
|
||
|
}
|
||
|
ref, err := parseLine(line)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
refs = append(refs, ref)
|
||
|
}
|
||
|
return refs, nil
|
||
|
}
|
||
|
|
||
|
// parseLine parses a line from a sha256sums file.
|
||
|
// The line is expected to be in the format "hex reference".
|
||
|
func parseLine(line string) (PinnedImageReference, error) {
|
||
|
parts := strings.Split(line, " ")
|
||
|
if len(parts) != 2 {
|
||
|
return PinnedImageReference{}, fmt.Errorf("line does not have exactly 2 parts separated by two spaces: %q", line)
|
||
|
}
|
||
|
return refFromSumAndRef(parts[0], parts[1])
|
||
|
}
|
||
|
|
||
|
func refFromSumAndRef(sum, ref string) (PinnedImageReference, error) {
|
||
|
if !sumRegexp.MatchString(sum) {
|
||
|
return PinnedImageReference{}, fmt.Errorf("invalid sum: %q", sum)
|
||
|
}
|
||
|
|
||
|
// last colon is separator between name and tag
|
||
|
var base, tag string
|
||
|
tagSep := strings.LastIndexByte(ref, ':')
|
||
|
if tagSep == notFound {
|
||
|
base = ref
|
||
|
} else {
|
||
|
base = ref[:tagSep]
|
||
|
tag = ref[tagSep+1:]
|
||
|
}
|
||
|
|
||
|
// first slash is separator between registry and full name
|
||
|
registrySep := strings.IndexByte(base, '/')
|
||
|
if registrySep == notFound {
|
||
|
return PinnedImageReference{}, fmt.Errorf("invalid reference: missing registry %q", ref)
|
||
|
}
|
||
|
|
||
|
registry := base[:registrySep]
|
||
|
fullName := base[registrySep+1:]
|
||
|
|
||
|
// last slash is separator between prefix and short name
|
||
|
var prefix, name string
|
||
|
nameSep := strings.LastIndexByte(fullName, '/')
|
||
|
if nameSep == notFound {
|
||
|
name = fullName
|
||
|
} else {
|
||
|
prefix = fullName[:nameSep]
|
||
|
name = fullName[nameSep+1:]
|
||
|
}
|
||
|
|
||
|
return PinnedImageReference{
|
||
|
Registry: registry,
|
||
|
Prefix: prefix,
|
||
|
Name: name,
|
||
|
Tag: tag,
|
||
|
Digest: "sha256:" + sum,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
func newLineScanner(r io.Reader) scanner {
|
||
|
scanner := bufio.NewScanner(r)
|
||
|
scanner.Split(bufio.ScanLines)
|
||
|
return scanner
|
||
|
}
|
||
|
|
||
|
type scanner interface {
|
||
|
Scan() bool
|
||
|
Text() string
|
||
|
}
|