From fda47558d541ed63fbc2b89da3454e3e6e32aa49 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 26 Sep 2026 18:25:28 +0200 Subject: [PATCH] A collision is two places, not two spellings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit checkResources compared paths as written, so ${dir:state}/server.env — the same characters in every module, a different directory in each — refused the first two placed modules that met. Paths are placed before they are compared, under the default root, which keeps every real collision: distinct modules' places are distinct under any one root, and a module stating another's placed root is caught because a pathless directory now owns its placed path in the comparison too. --- internal/catalogue/dir_into_test.go | 22 ++++++++++++++++++++++ internal/catalogue/resolve.go | 21 ++++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/internal/catalogue/dir_into_test.go b/internal/catalogue/dir_into_test.go index b1eeaaf..d990d26 100644 --- a/internal/catalogue/dir_into_test.go +++ b/internal/catalogue/dir_into_test.go @@ -221,6 +221,28 @@ func TestPlaceIsValidatedAtTheManifest(t *testing.T) { } } +func TestTwoModulesPlacedRootsAreNoCollision(t *testing.T) { + a := Manifest{Module: "gitea", Resources: []map[string]any{ + {"id": "state", "type": "directory", "place": "."}, + {"id": "env", "type": "file", "path": "${dir:state}/server.env", "content": ""}, + }} + b := Manifest{Module: "nextcloud", Resources: []map[string]any{ + {"id": "state", "type": "directory", "place": "."}, + {"id": "env", "type": "file", "path": "${dir:state}/server.env", "content": ""}, + }} + if got := checkResources([]Manifest{a, b}); len(got) != 0 { + t.Fatalf("alike templates are different places; got %v", got) + } + // And the real collision is still caught: a module stating another's placed root. + c := Manifest{Module: "squatter", Resources: []map[string]any{ + {"id": "nest", "type": "directory", "path": "/var/lib/gitea"}, + }} + got := checkResources([]Manifest{a, c}) + if len(got) != 1 || !strings.Contains(got[0], `"/var/lib/gitea"`) { + t.Fatalf("a stated path on a placed root collides; got %v", got) + } +} + func shallowCopy(resource map[string]any) map[string]any { copied := map[string]any{} for k, v := range resource { diff --git a/internal/catalogue/resolve.go b/internal/catalogue/resolve.go index 5d3e333..d825359 100644 --- a/internal/catalogue/resolve.go +++ b/internal/catalogue/resolve.go @@ -705,11 +705,30 @@ func checkResources(modules []Manifest) []string { ownedPath := map[string]string{} // path → owning module, for the access check below for _, m := range modules { + // Compared placed, not as written (novox/hq ADR 0112): ${dir:state} is the same six + // characters in every module and a different directory in each — two modules' templates + // being spelled alike is not two modules owning one path. The default root serves the + // comparison: a collision is within one node, and any one root keeps distinct modules' + // places distinct. A reference that cannot be placed is left as written — naming what + // does not exist is the manifest's own problem, refused where it was made. + dirs := dirsFor(m, Rendering{}) for _, r := range m.Resources { for _, field := range []string{"path", "unit", "name", "package"} { value, ok := r[field].(string) if !ok || value == "" { - continue + if field == "path" && fmt.Sprint(r["type"]) == "directory" { + // A pathless directory owns its placed path — a module stating that + // very path is exactly the collision this exists to catch. + value = dirs[fmt.Sprint(r["id"])] + } + if value == "" { + continue + } + } + if field == "path" { + if placed, err := dirFill(value, dirs, m.Module); err == nil { + value = placed + } } key := field + " " + value if other, taken := owner[key]; taken && other != m.Module { -- 2.54.0