// Command owned judges how the catalogue's directories and archives treat files they did not write (novox/hq // ADR 0422 §8, issue 1024). It refuses, naming the module, the resource and the reason: // // 1. a directory or archive without `unknown`, or with a value but move-out, module or program; // // 2. `move-out` on a path not under ${machine:account-home} or ${machine:agent-home}; // // 3. `knows` with an empty, absolute, `.`/`..`/empty-component or repeated path; // // 4. a path one module declares (a resource's path, or a fact with "home": true) lying inside a directory of // another module, after ${dir:} is resolved within each module and both homes are read as one root. // Archives are not containers for this rule. This holds for ~/.ssh what checks/sshdir held (ADR 0415 §6). // // go run . // // Exits 1 on a finding. package main import ( "encoding/json" "fmt" "os" "path" "path/filepath" "regexp" "sort" "strings" ) // Home is the one root both machine homes are read as. const Home = "${home}" var ( ofDir = regexp.MustCompile(`\$\{dir:([a-z0-9][a-z0-9_-]*)\}`) homeVar = strings.NewReplacer("${machine:account-home}", Home, "${machine:agent-home}", Home) values = map[string]bool{"move-out": true, "module": true, "program": true} ) // Finding is one refusal. type Finding struct { Rule int Module, ID, Why string } type resource struct { ID, Type, Path, Unknown string Knows []string HasKnows bool } type parsed struct { Module string Resources []resource Facts []string // paths of facts with "home": true, under the home } func load(file string) (*parsed, error) { raw, err := os.ReadFile(file) if err != nil { return nil, err } var m struct { Module string `json:"module"` Resources []struct { ID string `json:"id"` Type string `json:"type"` Path string `json:"path"` Unknown *string `json:"unknown"` Knows []string `json:"knows"` } `json:"resources"` Facts map[string]struct { Path string `json:"path"` Home bool `json:"home"` } `json:"facts"` } if err := json.Unmarshal(raw, &m); err != nil { return nil, fmt.Errorf("%s: %w", file, err) } out := &parsed{Module: m.Module} if out.Module == "" { out.Module = filepath.Base(filepath.Dir(file)) } for _, r := range m.Resources { x := resource{ID: r.ID, Type: r.Type, Path: r.Path, Knows: r.Knows, HasKnows: r.Knows != nil} if r.Unknown != nil { x.Unknown = *r.Unknown if x.Unknown == "" { x.Unknown = "(empty)" } } out.Resources = append(out.Resources, x) } keys := make([]string, 0, len(m.Facts)) for k := range m.Facts { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { if f := m.Facts[k]; f.Home && f.Path != "" { out.Facts = append(out.Facts, f.Path) } } return out, nil } // norm resolves ${dir:} from the module's own resources, reads both homes as one root and cleans the path. func norm(p string, dirs map[string]string) string { for i := 0; i < 8 && ofDir.MatchString(p); i++ { p = ofDir.ReplaceAllStringFunc(p, func(w string) string { if to, ok := dirs[ofDir.FindStringSubmatch(w)[1]]; ok { return to } return w }) } p = homeVar.Replace(p) if p == "" { return "" } return path.Clean(p) } func underHome(p string) bool { return strings.HasPrefix(p, Home+"/") } // badKnows says why a knows entry is refused, or "". func badKnows(k string) string { if k == "" { return "is empty" } if strings.HasPrefix(k, "/") { return "is absolute" } for _, c := range strings.Split(strings.TrimSuffix(k, "/"), "/") { if c == "" || c == "." || c == ".." { return "has an empty, . or .. component" } } return "" } type declared struct { module, id, path string } // Check reads every manifest under root/modules and answers what it refuses. func Check(root string) ([]Finding, error) { files, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json")) if err != nil { return nil, err } sort.Strings(files) var out []Finding var paths []declared type container struct{ module, path string } var containers []container for _, file := range files { m, err := load(file) if err != nil { return nil, err } dirs := map[string]string{} for _, r := range m.Resources { if r.ID != "" && r.Path != "" { dirs[r.ID] = r.Path } } for _, r := range m.Resources { p := norm(r.Path, dirs) if p != "" { paths = append(paths, declared{m.Module, r.ID, p}) } if r.Type != "directory" && r.Type != "archive" { continue } if r.Type == "directory" && p != "" { containers = append(containers, container{m.Module, p}) } add := func(rule int, why string) { out = append(out, Finding{rule, m.Module, r.ID, why}) } switch { case r.Unknown == "": add(1, "a "+r.Type+" without `unknown` (move-out, module or program)") case !values[r.Unknown]: add(1, fmt.Sprintf("`unknown` is %q, not move-out, module or program", r.Unknown)) case r.Unknown == "move-out" && !underHome(p): add(2, fmt.Sprintf("`move-out` on %q, which is not under ${machine:account-home} or ${machine:agent-home}", r.Path)) } seen := map[string]bool{} for _, k := range r.Knows { if why := badKnows(k); why != "" { add(3, fmt.Sprintf("`knows` entry %q %s", k, why)) continue } c := strings.TrimSuffix(k, "/") if seen[c] { add(3, fmt.Sprintf("`knows` entry %q is repeated", k)) } seen[c] = true } if r.HasKnows && len(r.Knows) == 0 { add(3, "`knows` is empty") } } for _, f := range m.Facts { if p := norm(Home+"/"+f, nil); p != "" { paths = append(paths, declared{m.Module, "fact " + f, p}) } } } for _, d := range paths { for _, c := range containers { if c.module != d.module && (d.path == c.path || strings.HasPrefix(d.path, c.path+"/")) { out = append(out, Finding{4, d.module, d.id, fmt.Sprintf("declares %q, inside a directory of %s (%q)", d.path, c.module, c.path)}) } } } return out, nil } func main() { root := "." if len(os.Args) > 1 { root = os.Args[1] } found, err := Check(root) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } for _, f := range found { fmt.Printf("%s: %s: %s (rule %d, novox/hq ADR 0422 §8, issue 1024)\n", f.Module, f.ID, f.Why, f.Rule) } if len(found) > 0 { os.Exit(1) } fmt.Println("owned paths: every directory and archive says how it treats unknown files; no module declares inside another's directory") }