diff --git a/go/catalogue/build.go b/go/catalogue/build.go index e36de7f..f4fcd25 100644 --- a/go/catalogue/build.go +++ b/go/catalogue/build.go @@ -394,6 +394,45 @@ func (b *Build) problems(module string) []string { "%s: %q mirrors %q, which names no tag or digest", module, a.Name, a.From)) } problems = append(problems, upstreamFileProblems(module, a)...) + problems = append(problems, pathsProblems(module, a)...) + } + return problems +} + +// pathsProblems is what is wrong with an artifact's `paths` (novox/hq ADR 0499 clause 1): named on a kind whose build +// source is derived or named otherwise, or a path that leaves the module's directory, is its whole directory, or is a +// pattern. +func pathsProblems(module string, a Artifact) []string { + if a.Paths == nil { + return nil + } + switch { + case a.Kind == ArtifactBundle && a.Language == "go": + return []string{fmt.Sprintf("%s: %q is a Go program and names paths: its build source is derived from the "+ + "package it compiles (novox/hq ADR 0267 clause 1, ADR 0499 clause 1)", module, a.Name)} + case a.Kind != ArtifactBundle && a.Kind != ArtifactPackage: + return []string{fmt.Sprintf("%s: %q is a %q and names paths; only a package or a bundle in a language other "+ + "than Go names the paths it is built from (novox/hq ADR 0499 clause 1)", module, a.Name, a.Kind)} + case len(a.Paths) == 0: + return []string{fmt.Sprintf("%s: %q names no path in paths; leave the word out to build from the module's "+ + "directory", module, a.Name)} + } + var problems []string + for _, p := range a.Paths { + clean := path.Clean(p) + switch { + case strings.TrimSpace(p) == "": + problems = append(problems, fmt.Sprintf("%s: %q names an empty path", module, a.Name)) + case strings.HasPrefix(p, "/") || clean == ".." || strings.HasPrefix(clean, "../"): + problems = append(problems, fmt.Sprintf("%s: %q names the path %q, which leaves the module's directory", + module, a.Name, p)) + case clean == ".": + problems = append(problems, fmt.Sprintf("%s: %q names the module's whole directory (%q) as a path; leave "+ + "the word out for that", module, a.Name, p)) + case strings.ContainsAny(p, "*?[]{}\\"): + problems = append(problems, fmt.Sprintf("%s: %q names %q, which is a pattern; paths names files and "+ + "directories", module, a.Name, p)) + } } return problems } diff --git a/go/catalogue/manifest.go b/go/catalogue/manifest.go index 870ea4d..0c721c7 100644 --- a/go/catalogue/manifest.go +++ b/go/catalogue/manifest.go @@ -1191,6 +1191,17 @@ type Artifact struct { // (novox/hq ADR 0193). Absent for nearly every bundle, which is then one file per entrypoint. External []string `json:"external,omitempty"` + // Paths are the files and directories, relative to the module's directory, a package or a bundle in a language + // other than Go is built from (novox/hq ADR 0499 clause 1): with the module's manifest they are its build source, + // and a module whose every artifact names them takes its fingerprint's tree from them alone (clause 2). Absent means + // the module's directory, as before. + // + // **Named, because the build source of a package sharing a repository cannot be derived.** A Go program's is + // (ADR 0267 clause 1), an archive's `from` names it and an image's is the context its manifest names, so each of those + // refuses the word. A path stays inside the module's directory and is no pattern: a build source that could reach + // out of it, or grow with a directory listing, would be one no fingerprint pins. + Paths []string `json:"paths,omitempty"` + // Env is what a tools bundle is given on a machine (novox/hq ADR 0192): words and their values, // paths and constants composed with ${dir:…} and ${port:…} exactly as a container's environment // is, never a secret's content. The node's runtime hands it to this bundle and to no other. diff --git a/go/catalogue/paths_test.go b/go/catalogue/paths_test.go new file mode 100644 index 0000000..e355383 --- /dev/null +++ b/go/catalogue/paths_test.go @@ -0,0 +1,46 @@ +package catalogue + +import ( + "strings" + "testing" +) + +// An artifact names the paths it is built from with `paths` (novox/hq ADR 0499 clause 1): a package, or a bundle in a +// language other than Go, may; a Go program's artifact, an archive, an image and an upstream artifact may not, and the +// refusal names the artifact and its kind. Each path stays inside the module's directory: no `..`, no absolute path, +// no pattern. +func TestAnArtifactNamesThePathsItIsBuiltFrom(t *testing.T) { + m, err := ParseManifest([]byte(`{"module":"mesh-sdk","version":"1","slug":"sdk","build":{"artifacts":[ + {"name":"lib","kind":"package","language":"typescript", + "paths":["src","test","package.json","package-lock.json","tsconfig.json"]}]},"resources":[]}`)) + if err != nil { + t.Fatalf("a package naming its paths was refused: %v", err) + } + if got := strings.Join(m.Build.Artifacts[0].Paths, ","); got != "src,test,package.json,package-lock.json,tsconfig.json" { + t.Fatalf("the paths were not kept as written: %q", got) + } + if _, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[ + {"name":"tools","kind":"bundle","language":"typescript","entrypoints":["index.js"],"loads":["index.js"],"paths":["src","package.json"]}]}}`)); err != nil { + t.Fatalf("a TypeScript bundle naming its paths was refused: %v", err) + } + + for _, c := range []struct{ artifact, says string }{ + {`{"name":"x","kind":"bundle","language":"go","system":"linux-amd64","paths":["cmd"]}`, `"x" is a Go program`}, + {`{"name":"x","kind":"archive","from":"files","paths":["files"]}`, `"x" is a "archive"`}, + {`{"name":"x","kind":"image","from":"Dockerfile","paths":["src"]}`, `"x" is a "image"`}, + {`{"name":"x","kind":"upstream","from":"nats:2.10","paths":["src"]}`, `"x" is a "upstream"`}, + {`{"name":"x","kind":"package","language":"typescript","paths":["../other"]}`, "leaves the module's directory"}, + {`{"name":"x","kind":"package","language":"typescript","paths":["src/../../x"]}`, "leaves the module's directory"}, + {`{"name":"x","kind":"package","language":"typescript","paths":["/etc"]}`, "leaves the module's directory"}, + {`{"name":"x","kind":"package","language":"typescript","paths":["src/*.ts"]}`, "is a pattern"}, + {`{"name":"x","kind":"package","language":"typescript","paths":["src/[ab]"]}`, "is a pattern"}, + {`{"name":"x","kind":"package","language":"typescript","paths":[""]}`, "names an empty path"}, + {`{"name":"x","kind":"package","language":"typescript","paths":["."]}`, "names the module's whole directory"}, + {`{"name":"x","kind":"package","language":"typescript","paths":[]}`, "names no path"}, + } { + _, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[` + c.artifact + `]}}`)) + if err == nil || !strings.Contains(err.Error(), c.says) { + t.Errorf("%s: %v, wanted a refusal saying %q", c.artifact, err, c.says) + } + } +}