package delivery import ( "crypto/sha256" "encoding/hex" "fmt" "sort" "strings" ) // SourcePrefix2 opens every source fingerprint of the new form. const SourcePrefix2 = "src2:" // Toolchain2 is what an artifact's toolchain enters the new fingerprint as (novox/hq ADR 0405 clause 7): the // artifact's language, the installed toolchain's version and the build module's recipe version, in place of the // toolchain image's digest. A toolchain update so changes the fingerprints of that language's modules only. type Toolchain2 struct { Language string Version string Recipe string } // SourceInputs2 are what a module's source fingerprint of the new form is of (novox/hq ADR 0429 clause 5 as ADR 0437 // clause 5 changed it; ADR 0453 clause 6). It differs from SourceInputs in exactly four things: a base enters by its // module's fingerprint (src2:...), not its image digest; the builder enters by its own module's fingerprint, not its // executable's digest; an artifact that resolves packages when it is built enters by the hash of its lock file; and // an artifact's toolchain enters as Toolchain2. Module, Tree, Contexts and Unpinned are as in SourceInputs. // // Locks names the artifacts that resolve packages when built, each with the hash of its lock file; an entry with an // empty hash is such an artifact with no lock, which has no fingerprint. An artifact that resolves nothing is not in // Locks. type SourceInputs2 struct { Module string Tree string Bases []string Builder string Contexts map[string]string Locks map[string]string Toolchains map[string]Toolchain2 Unpinned string } // SourceFingerprint2 is the module's source fingerprint of the new form, `src2:` 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, "no lock" when an artifact resolves packages and has no lock. It is a pure function of its inputs: the order // of maps and a base given twice change nothing. The lines are "module ", "tree " and "builder ", then // "base " (once each), "context ", "lock " and "toolchain // ", sorted, all joined by a newline. func SourceFingerprint2(in SourceInputs2) (fp, none string) { if in.Unpinned != "" { return "", in.Unpinned } if in.Tree == "" { return "", "no tree" } for _, h := range in.Locks { if h == "" { return "", "no lock" } } lines := []string{"module " + in.Module, "tree " + in.Tree, "builder " + in.Builder} bases := map[string]bool{} for _, b := range in.Bases { bases[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, h := range in.Locks { lines = append(lines, "lock "+a+" "+h) } for a, t := range in.Toolchains { lines = append(lines, "toolchain "+a+" "+t.Language+" "+t.Version+" "+t.Recipe) } sort.Strings(lines[3:]) sum := sha256.Sum256([]byte(strings.Join(lines, "\n"))) return SourcePrefix2 + hex.EncodeToString(sum[:]), "" } // FingerprintKeyPart is a new-form fingerprint as a key part: its 64 hex digits without the prefix, since a bus key // may not hold ':'. It refuses a value that is not `src2:` and 64 lowercase hex digits, a src1 fingerprint included, // naming it. func FingerprintKeyPart(fp string) (string, error) { part, ok := strings.CutPrefix(fp, SourcePrefix2) if !ok || !isHex64(part) { return "", fmt.Errorf("delivery: %q is not a fingerprint of the new form, %s and 64 lowercase hex digits", fp, SourcePrefix2) } return part, nil } // ParseFingerprintKeyPart is the inverse of FingerprintKeyPart. It refuses a part that is not 64 lowercase hex // digits, a src1 fingerprint or a prefixed value included, naming it. func ParseFingerprintKeyPart(part string) (string, error) { if !isHex64(part) { return "", fmt.Errorf("delivery: %q is not the key part of a fingerprint, 64 lowercase hex digits", part) } return SourcePrefix2 + part, nil } func isHex64(s string) bool { return len(s) == 64 && strings.Trim(s, "0123456789abcdef") == "" }