Carry a candidate module's named paths, so the builder's fetch for a candidate names them (issue 1758, ADR 0499 §2, the builder lane's request)

This commit is contained in:
2026-10-22 18:06:49 +02:00
parent 8c3bfad86b
commit 72a477eb1c
2 changed files with 60 additions and 1 deletions
+40
View File
@@ -0,0 +1,40 @@
package delivery
import (
"strings"
"testing"
)
// A candidate's module carries the git objects of its named paths at the preview (novox/hq ADR 0499 clause 2), so the
// builder's fetch request for a candidate names them; an older candidate decodes without them, and a path that is not
// clean and relative, or names no object, is refused.
func TestACandidatesModuleCarriesItsNamedPaths(t *testing.T) {
c := sampleCandidate()
paths := map[string]string{"src": strings.Repeat("a", 40), "package.json": strings.Repeat("b", 40)}
c.Pulls[0].Fps = map[string]CandidateModule{"mesh-mail": {FP2: SourcePrefix2 + strings.Repeat("1", 64),
Manifest: strings.Repeat("2", 64), Paths: paths}}
c.FPSet = FingerprintSet(c.Pulls)
data, err := EncodeCandidate(c)
if err != nil {
t.Fatal(err)
}
back, err := DecodeCandidate(data)
if err != nil || back.Pulls[0].Fps["mesh-mail"].Paths["src"] != paths["src"] {
t.Fatalf("decoded %+v, %v", back.Pulls[0].Fps, err)
}
old := sampleCandidate()
old.Pulls[0].Fps = map[string]CandidateModule{"mesh-mail": {FP2: SourcePrefix2 + strings.Repeat("1", 64)}}
old.FPSet = FingerprintSet(old.Pulls)
if data, err := EncodeCandidate(old); err != nil {
t.Fatal(err)
} else if b, err := DecodeCandidate(data); err != nil || b.Pulls[0].Fps["mesh-mail"].Paths != nil {
t.Fatalf("an older candidate: %+v, %v", b, err)
}
for _, bad := range []map[string]string{{"../x": "a"}, {"/src": "a"}, {"": "a"}, {"src": ""}} {
c.Pulls[0].Fps = map[string]CandidateModule{"mesh-mail": {FP2: SourcePrefix2 + strings.Repeat("1", 64), Paths: bad}}
c.FPSet = FingerprintSet(c.Pulls)
if _, err := EncodeCandidate(c); err == nil {
t.Errorf("%v was accepted", bad)
}
}
}
+20 -1
View File
@@ -277,6 +277,9 @@ type CandidateModule struct {
FP2 string `json:"fp2,omitempty"`
None2 string `json:"none2,omitempty"`
Manifest string `json:"manifest,omitempty"`
// Paths are, of a module whose every artifact names its paths, each named path's git object id at the preview
// (novox/hq ADR 0499 clause 2), so the builder's fetch request for the candidate names them. Absent otherwise.
Paths map[string]string `json:"paths,omitempty"`
}
func (m CandidateModule) check() error {
@@ -286,6 +289,9 @@ func (m CandidateModule) check() error {
case m.FP2 == "" && m.None2 == "":
return fmt.Errorf("delivery: a module has neither fp2 nor none2")
}
if err := checkPathObjects(m.Paths); err != nil {
return err
}
return checkNewForm(m.FP2, m.Manifest)
}
@@ -357,7 +363,7 @@ func (c Candidate) check() error {
if err := m.check(); err != nil {
return fmt.Errorf("delivery: the candidate's module %q: %w", module, err)
}
if before, ok := seen[module]; ok && before != m {
if before, ok := seen[module]; ok && !reflect.DeepEqual(before, m) {
return fmt.Errorf("delivery: two pull requests of the candidate give the module %q different fingerprints", module)
}
seen[module] = m
@@ -425,6 +431,19 @@ type KeptInputs2 struct {
Paths map[string]string `json:"paths,omitempty"`
}
// checkPathObjects refuses a named path that is not clean and relative to the module's directory, or names no object.
func checkPathObjects(paths map[string]string) error {
for p, object := range paths {
if p == "" || p != path.Clean(p) || strings.HasPrefix(p, "/") || p == ".." || strings.HasPrefix(p, "../") {
return fmt.Errorf("delivery: the named path %q is not a clean path inside the module's directory", p)
}
if object == "" {
return fmt.Errorf("delivery: the named path %q names no git object", p)
}
}
return nil
}
// checkNamed refuses kept named paths that do not compute to the kept tree with the module's manifest digest.
func checkNamed(tree, manifest string, paths map[string]string, of string) error {
if len(paths) == 0 {