Move the source fingerprint into one function the builder and the indexer both call (issue 1203)
This commit is contained in:
@@ -27,6 +27,9 @@
|
||||
// `merge.<group>.<heads hash>` (the router's ask whole), its owed keys `<repository>.<number>.<head>.<act>` that the
|
||||
// forge bridge performs, and the teller's answer under the ask's own key (gate.go).
|
||||
//
|
||||
// **The source fingerprint** (issue 1203; ADR 0437 §5): one function of named inputs that the controller's builder and
|
||||
// the indexer both call, so they agree byte for byte (fingerprint.go).
|
||||
//
|
||||
// Standard library only, and this module's own asks package.
|
||||
package delivery
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SourceInputs are what a module's source fingerprint is of (novox/hq ADR 0437 §5): the module, the git tree of
|
||||
// its directory, its bases (references, of which only the digest counts), the root tree of each repository its
|
||||
// artifacts read (by artifact), each artifact's toolchain line, and the reason the source does not pin the build
|
||||
// when it does not (a build that resolves packages when built).
|
||||
type SourceInputs struct {
|
||||
Module string
|
||||
Tree string
|
||||
Bases []string
|
||||
Contexts map[string]string
|
||||
Toolchains map[string]string
|
||||
Unpinned string
|
||||
}
|
||||
|
||||
// SourceFingerprint is the module's source fingerprint, `src1:` and the hex sha256 of its inputs' lines, or none
|
||||
// and the reason when the source does not pin the build: Unpinned when set, "no tree" when Tree is empty. It is the
|
||||
// one function the controller's builder and the indexer both call, so they agree byte for byte. The lines are
|
||||
// "module <m>" and "tree <t>", then "base <digest>" (once each), "context <artifact> <tree>" and "toolchain
|
||||
// <artifact> <line>" sorted, all joined by a newline.
|
||||
func SourceFingerprint(in SourceInputs) (fp, none string) {
|
||||
if in.Unpinned != "" {
|
||||
return "", in.Unpinned
|
||||
}
|
||||
if in.Tree == "" {
|
||||
return "", "no tree"
|
||||
}
|
||||
lines := []string{"module " + in.Module, "tree " + in.Tree}
|
||||
bases := map[string]bool{}
|
||||
for _, b := range in.Bases {
|
||||
bases[ReferenceDigest(b)] = true
|
||||
}
|
||||
for b := range bases {
|
||||
lines = append(lines, "base "+b)
|
||||
}
|
||||
for a, t := range in.Contexts {
|
||||
lines = append(lines, "context "+a+" "+t)
|
||||
}
|
||||
for a, t := range in.Toolchains {
|
||||
lines = append(lines, "toolchain "+a+" "+t)
|
||||
}
|
||||
sort.Strings(lines[2:])
|
||||
sum := sha256.Sum256([]byte(strings.Join(lines, "\n")))
|
||||
return SourcePrefix + hex.EncodeToString(sum[:]), ""
|
||||
}
|
||||
|
||||
// ReferenceDigest is a reference's digest, `sha256:...`, so the registry address a base was copied into does not
|
||||
// enter the fingerprint; the reference itself when it carries none.
|
||||
func ReferenceDigest(reference string) string {
|
||||
if _, digest, pinned := strings.Cut(reference, "@"); pinned && digest != "" {
|
||||
return digest
|
||||
}
|
||||
return reference
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func inputs() SourceInputs {
|
||||
return SourceInputs{
|
||||
Module: "mesh-mail", Tree: "t-module",
|
||||
Bases: []string{"registry.example/base@sha256:aaa", "registry.example/other@sha256:bbb"},
|
||||
Contexts: map[string]string{"mail": "t-root", "tool": "t-root2"},
|
||||
Toolchains: map[string]string{"mail": "go sha256:ccc 0123456789abcdef", "tool": "go sha256:ccc fedcba9876543210"},
|
||||
}
|
||||
}
|
||||
|
||||
func mustFP(t *testing.T, in SourceInputs) string {
|
||||
t.Helper()
|
||||
fp, none := SourceFingerprint(in)
|
||||
if fp == "" || none != "" {
|
||||
t.Fatalf("fingerprint = %q, none = %q", fp, none)
|
||||
}
|
||||
return fp
|
||||
}
|
||||
|
||||
func TestFingerprintForm(t *testing.T) {
|
||||
fp := mustFP(t, inputs())
|
||||
hex := strings.TrimPrefix(fp, SourcePrefix)
|
||||
if SourcePrefix != "src1:" || hex == fp || len(hex) != 64 || strings.Trim(hex, "0123456789abcdef") != "" {
|
||||
t.Errorf("fingerprint %q is not src1: and 64 hex digits", fp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFingerprintOrderAndDuplicates(t *testing.T) { // 1203/AC2
|
||||
want := mustFP(t, inputs())
|
||||
in := inputs()
|
||||
in.Bases = []string{in.Bases[1], in.Bases[0]}
|
||||
if got := mustFP(t, in); got != want {
|
||||
t.Errorf("the order of bases changes it: %s", got)
|
||||
}
|
||||
in = inputs()
|
||||
in.Bases = append(in.Bases, in.Bases[0])
|
||||
if got := mustFP(t, in); got != want {
|
||||
t.Errorf("a base given twice changes it: %s", got)
|
||||
}
|
||||
in = inputs()
|
||||
in.Bases = append(in.Bases, "other.registry.example/base@sha256:aaa")
|
||||
if got := mustFP(t, in); got != want {
|
||||
t.Errorf("two references to one digest change it: %s", got)
|
||||
}
|
||||
// Maps have no order, but build them in many insertion orders anyway.
|
||||
for i := 0; i < 20; i++ {
|
||||
in = inputs()
|
||||
in.Contexts = map[string]string{}
|
||||
in.Toolchains = map[string]string{}
|
||||
for _, k := range []string{"tool", "mail"} {
|
||||
in.Contexts[k] = inputs().Contexts[k]
|
||||
in.Toolchains[k] = inputs().Toolchains[k]
|
||||
}
|
||||
if got := mustFP(t, in); got != want {
|
||||
t.Fatalf("map order changes it: %s", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFingerprintChanges(t *testing.T) { // 1203/AC3
|
||||
want := mustFP(t, inputs())
|
||||
for name, f := range map[string]func(*SourceInputs){
|
||||
"module": func(in *SourceInputs) { in.Module = "other" },
|
||||
"tree": func(in *SourceInputs) { in.Tree = "t-other" },
|
||||
"base": func(in *SourceInputs) { in.Bases[0] = "registry.example/base@sha256:zzz" },
|
||||
"a base added": func(in *SourceInputs) { in.Bases = append(in.Bases, "x@sha256:new") },
|
||||
"context": func(in *SourceInputs) { in.Contexts["mail"] = "t-changed" },
|
||||
"context artifact": func(in *SourceInputs) { in.Contexts["extra"] = "t-root" },
|
||||
"toolchain": func(in *SourceInputs) { in.Toolchains["tool"] = "go sha256:ccc changed" },
|
||||
} {
|
||||
in := inputs()
|
||||
f(&in)
|
||||
if got := mustFP(t, in); got == want {
|
||||
t.Errorf("a changed %s keeps the fingerprint", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFingerprintNone(t *testing.T) { // 1203/AC4
|
||||
in := inputs()
|
||||
in.Unpinned = "the build resolves packages when built"
|
||||
fp, none := SourceFingerprint(in)
|
||||
if fp != "" || none != in.Unpinned {
|
||||
t.Errorf("unpinned: fp = %q, none = %q", fp, none)
|
||||
}
|
||||
in = inputs()
|
||||
in.Tree = ""
|
||||
fp, none = SourceFingerprint(in)
|
||||
if fp != "" || none != "no tree" {
|
||||
t.Errorf("no tree: fp = %q, none = %q", fp, none)
|
||||
}
|
||||
in = inputs()
|
||||
in.Tree, in.Unpinned = "", "the build resolves packages when built"
|
||||
if _, none = SourceFingerprint(in); none != in.Unpinned {
|
||||
t.Errorf("the reason for unpinned stands over no tree: %q", none)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReferenceDigest(t *testing.T) {
|
||||
for in, want := range map[string]string{
|
||||
"registry.example/base@sha256:aaa": "sha256:aaa",
|
||||
"registry.example/base": "registry.example/base",
|
||||
"registry.example/base@": "registry.example/base@",
|
||||
"": "",
|
||||
} {
|
||||
if got := ReferenceDigest(in); got != want {
|
||||
t.Errorf("ReferenceDigest(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user