diff --git a/go/delivery/module_paths_test.go b/go/delivery/module_paths_test.go new file mode 100644 index 0000000..1579762 --- /dev/null +++ b/go/delivery/module_paths_test.go @@ -0,0 +1,33 @@ +package delivery + +import ( + "strings" + "testing" +) + +// The indexer's modules value carries each artifact's named paths (novox/hq ADR 0499; the lead's decision L8 of R4e): +// the checker tells from it whether a change touches lib's paths without reading a manifest. An older value has none. +func TestAModulesValueCarriesEachArtifactsNamedPaths(t *testing.T) { + m := Module{FP2: SourcePrefix2 + strings.Repeat("a", 64), Repository: "novox/mesh-sdk", Commit: "c", + ArtifactPaths: map[string][]string{"lib": {"package-lock.json", "package.json", "src", "test", "tsconfig.json"}}} + data, err := EncodeModule(m) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(data), `"paths":{"lib":[`) { + t.Fatalf("the value carries no paths: %s", data) + } + back, err := DecodeModule(data) + if err != nil || len(back.ArtifactPaths["lib"]) != 5 || back.ArtifactPaths["lib"][2] != "src" { + t.Fatalf("decoded %+v, %v", back.ArtifactPaths, err) + } + if old, err := DecodeModule([]byte(`{"v":1,"fp":"src1:x","repository":"a/b","commit":"c"}`)); err != nil || old.ArtifactPaths != nil { + t.Fatalf("an older value: %+v, %v", old, err) + } + for _, bad := range []map[string][]string{{"": {"src"}}, {"lib": {}}, {"lib": {"../x"}}, {"lib": {""}}} { + m.ArtifactPaths = bad + if _, err := EncodeModule(m); err == nil { + t.Errorf("%v was accepted", bad) + } + } +} diff --git a/go/delivery/source.go b/go/delivery/source.go index 9e79b6d..64d0eca 100644 --- a/go/delivery/source.go +++ b/go/delivery/source.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "path" "reflect" "sort" "strings" @@ -399,6 +400,11 @@ type Module struct { // builds the fingerprint only with these. Empty when FP2 is empty or the module has no toolchain; a value written // before has none (issue 1615). Toolchains map[string]ToolchainUsed `json:"toolchains,omitempty"` + // ArtifactPaths are, of each artifact that names the paths it is built from, those paths as its manifest names + // them, by artifact (novox/hq ADR 0499 clause 1): the indexer fills them from each catalogue.Artifact.Paths it reads, + // so the checker tells whether a change touches an artifact's build source without reading a manifest (the lead's + // decision L8 of R4e). Absent for an artifact that names none, and in a value written before. + ArtifactPaths map[string][]string `json:"paths,omitempty"` } // RootPath is Module.Path of a module whose manifest is its repository's root. @@ -459,6 +465,16 @@ func (m Module) check() error { if m.Inputs2 != nil && m.Manifest == "" { return fmt.Errorf("delivery: a module keeps new-form inputs but has no manifest digest") } + for artifact, paths := range m.ArtifactPaths { + if artifact == "" || len(paths) == 0 { + return fmt.Errorf("delivery: the module's named paths of the artifact %q are empty or name no artifact", artifact) + } + for _, p := range paths { + if p == "" || p != path.Clean(p) || strings.HasPrefix(p, "/") || p == ".." || strings.HasPrefix(p, "../") { + return fmt.Errorf("delivery: the artifact %q names the path %q, not a clean path inside the module's directory", artifact, p) + } + } + } if m.Inputs2 != nil { if err := checkNamed(m.Inputs2.Tree, m.Manifest, m.Inputs2.Paths, "the module's kept inputs"); err != nil { return err