diff --git a/go/delivery/candidate_paths_test.go b/go/delivery/candidate_paths_test.go new file mode 100644 index 0000000..b8f2472 --- /dev/null +++ b/go/delivery/candidate_paths_test.go @@ -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) + } + } +} diff --git a/go/delivery/source.go b/go/delivery/source.go index 64d0eca..145b4e2 100644 --- a/go/delivery/source.go +++ b/go/delivery/source.go @@ -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 {