126 lines
4.9 KiB
Go
126 lines
4.9 KiB
Go
package declaration
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"regexp"
|
|
)
|
|
|
|
// The three names of a file travelling by digest (novox/hq ADR 0486 §1, §2, §5), shared by the node-engine that
|
|
// reads them and the controller that writes them.
|
|
const (
|
|
// FileReferenceThreshold is the size above which a file travels by digest: a file of this many bytes or fewer
|
|
// stays in the declaration as it is.
|
|
FileReferenceThreshold = 64 * 1024
|
|
// FileBucket is the object bucket of the bus that holds the contents, each under the hex of its sha256 digest
|
|
// (ObjectName). The controller alone writes it; a node-engine reads an object by name.
|
|
FileBucket = "declaration-files"
|
|
// FileReferencesCapability is what a node-engine reports when it reads a file given as a digest and size.
|
|
FileReferencesCapability = "file-references"
|
|
)
|
|
|
|
var digestForm = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)
|
|
|
|
// DigestOf is the digest a reference names for a content: "sha256:<64 lower-case hex>".
|
|
func DigestOf(content []byte) string {
|
|
sum := sha256.Sum256(content)
|
|
return "sha256:" + hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
// ObjectName is the name of the object holding the content a digest names: the hex without its "sha256:" prefix.
|
|
func ObjectName(digest string) (string, error) {
|
|
if !digestForm.MatchString(digest) {
|
|
return "", fmt.Errorf("digest %q is not sha256:<64 lower-case hex characters>", digest)
|
|
}
|
|
return digest[len("sha256:"):], nil
|
|
}
|
|
|
|
// IsReference reports whether the file gives its content as a digest and size instead of carrying it.
|
|
func (f *File) IsReference() bool { return f.Digest != "" }
|
|
|
|
// checkReference judges the digest and size, and what a reference may not be given with.
|
|
func (f *File) checkReference(where string) []string {
|
|
if f.Digest == "" && f.Size == 0 {
|
|
return nil
|
|
}
|
|
var problems []string
|
|
if !digestForm.MatchString(f.Digest) {
|
|
problems = append(problems, fmt.Sprintf("%s: digest %q; a file by reference names its content as "+
|
|
"sha256:<64 lower-case hex characters>", where, f.Digest))
|
|
}
|
|
if f.Size <= 0 {
|
|
problems = append(problems, fmt.Sprintf("%s: size %d; a file by reference states its size in bytes, "+
|
|
"more than zero", where, f.Size))
|
|
}
|
|
if f.Content != "" || f.Bytes != "" || f.Sealed != "" || f.VaultSealed != nil || len(f.Secrets) > 0 {
|
|
problems = append(problems, where+": a file by reference carries its content in the object its digest "+
|
|
"names, so it says no content, bytes, sealed, vault-sealed or secrets")
|
|
}
|
|
if f.Into != "" {
|
|
problems = append(problems, where+": a file by reference is written whole; a file written into says its "+
|
|
"part in content")
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// Body is the bytes the file's content stands for: its content, or its bytes decoded.
|
|
func (f *File) Body() ([]byte, error) {
|
|
if f.Bytes == "" {
|
|
return []byte(f.Content), nil
|
|
}
|
|
b, err := base64.StdEncoding.DecodeString(f.Bytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("file %s: bytes are not base64: %w", f.ID, err)
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
// TravelsByDigest reports whether the controller may put this file's content in the bucket: it is plain content
|
|
// or bytes (not sealed, not filled with secrets, not written into another file) of more than FileReferenceThreshold
|
|
// bytes.
|
|
func (f *File) TravelsByDigest() bool {
|
|
if f.IsReference() || f.Sealed != "" || f.VaultSealed != nil || len(f.Secrets) > 0 || f.Into != "" {
|
|
return false
|
|
}
|
|
if f.Bytes != "" {
|
|
// DecodedLen counts padding, so the exact length is taken off the decoded bytes.
|
|
body, err := f.Body()
|
|
return err == nil && len(body) > FileReferenceThreshold
|
|
}
|
|
return len(f.Content) > FileReferenceThreshold
|
|
}
|
|
|
|
// AsReference is the file as it is sent to an engine that reports FileReferencesCapability: a copy with the digest
|
|
// and size in place of the content, and the content the controller puts into the bucket under ObjectName(digest).
|
|
// A file that does not travel by digest is refused, not passed through, so a caller cannot send a small file empty.
|
|
func (f *File) AsReference() (File, []byte, error) {
|
|
if !f.TravelsByDigest() {
|
|
return File{}, nil, fmt.Errorf("file %s does not travel by digest", f.ID)
|
|
}
|
|
body, err := f.Body()
|
|
if err != nil {
|
|
return File{}, nil, err
|
|
}
|
|
ref := *f
|
|
ref.Content, ref.Bytes = "", ""
|
|
ref.Digest, ref.Size = DigestOf(body), int64(len(body))
|
|
return ref, body, nil
|
|
}
|
|
|
|
// Verify checks what was fetched against the digest and size the file names; the node-engine writes the file only
|
|
// after it returns nil. A mismatch is the apply's failure, never a partial apply (ADR 0486 §1).
|
|
func (f *File) Verify(body []byte) error {
|
|
if !f.IsReference() {
|
|
return fmt.Errorf("file %s is not given by reference", f.ID)
|
|
}
|
|
if int64(len(body)) != f.Size {
|
|
return fmt.Errorf("file %s: the object is %d bytes and the declaration says %d", f.ID, len(body), f.Size)
|
|
}
|
|
if got := DigestOf(body); got != f.Digest {
|
|
return fmt.Errorf("file %s: the object hashes to %s and the declaration names %s", f.ID, got, f.Digest)
|
|
}
|
|
return nil
|
|
}
|