package zddc import ( "io/fs" "os" "path" "strings" ) // PolicyTree is a set of .zddc documents addressed by directory path relative // to a mount point, with "*" as the any-segment wildcard. It is the in-memory // form of a per-depth default tree or an operator .zddc.zip dropped at a // directory: mounting the tree at directory D means key "working" governs // D/working/, "*/mdl" governs D//mdl/, and "" is D's own .zddc. // // Resolution mirrors the paths: cascade — a literal segment beats "*" — so a // .zddc.zip and a paths: block compose identically. A .zddc.zip can therefore // be dropped at ANY level to contribute a whole policy subtree; combined with // inherit:false in its resolved .zddc it becomes a self-contained island. type PolicyTree map[string]ZddcFile // AnyPlaceholder is the on-disk directory name standing in for the "*" wildcard // in the embedded default-tree source (internal/zddc/defaults/), so the repo // holds no shell-/go:embed-hostile literal "*" directories. Operator .zddc.zip // bundles use "*" directly. const AnyPlaceholder = "_any_" // segsOf splits a "/"-joined member-dir key into segments ("" → no segments). func segsOf(key string) []string { if key == "" { return nil } return strings.Split(key, "/") } // resolveTreeDir returns the member-dir key governing segs: same length, each // key segment literal-equal or "*", most-literal wins (a literal beats "*" at // the earliest differing position — matching the paths: literal-first rule). func (t PolicyTree) resolveTreeDir(segs []string) (string, bool) { bestKey := "" var bestSegs []string found := false for key := range t { ks := segsOf(key) if len(ks) != len(segs) { continue } match := true for i := range ks { if ks[i] != "*" && ks[i] != segs[i] { match = false break } } if !match { continue } if !found || moreLiteral(ks, bestSegs) { bestKey, bestSegs, found = key, ks, true } } return bestKey, found } // moreLiteral reports whether a is more specific than b: at the earliest // position where one is literal and the other "*", the literal wins. func moreLiteral(a, b []string) bool { for i := range a { al, bl := a[i] != "*", b[i] != "*" if al != bl { return al } } return false } // Along returns the .zddc documents this tree contributes along relSegs — one // per cascade level from the mount root (the empty prefix) down to the full // path, in root→leaf order (matching PolicyChain.Levels indexing). Levels with // no governing member contribute nothing. func (t PolicyTree) Along(relSegs []string) []ZddcFile { var out []ZddcFile for k := 0; k <= len(relSegs); k++ { if key, ok := t.resolveTreeDir(relSegs[:k]); ok { out = append(out, t[key]) } } return out } // Assemble folds a flat per-depth tree into a single nested ZddcFile whose // Paths map mirrors the tree — the inverse of authoring policy as per-depth // files. The result is exactly what a single nested-paths: .zddc would parse // to, so the cascade walker consumes it unchanged: the embedded defaults and // any operator .zddc.zip both become ordinary paths:-bearing ZddcFiles. func (t PolicyTree) Assemble() ZddcFile { return assembleTree(map[string]ZddcFile(t)) } // assembleTree recursively builds the nested node for a set of members keyed by // path relative to this node ("" = this node's own content; "head/rest" = a // descendant). Children are grouped by first segment and recursed into Paths. func assembleTree(members map[string]ZddcFile) ZddcFile { node := members[""] groups := map[string]map[string]ZddcFile{} for key, zf := range members { if key == "" { continue } head, rest, _ := strings.Cut(key, "/") if groups[head] == nil { groups[head] = map[string]ZddcFile{} } groups[head][rest] = zf } if len(groups) > 0 { node.Paths = make(map[string]ZddcFile, len(groups)) for head, sub := range groups { node.Paths[head] = assembleTree(sub) } } return node } // LoadPolicyTreeFromFS loads a per-depth .zddc tree rooted at `root` within // fsys, mapping the AnyPlaceholder directory to the "*" wildcard. Keys are // member dirs relative to root ("" for root/.zddc). Works on an embed.FS, an // os.DirFS, or a zip's fs.FS. func LoadPolicyTreeFromFS(fsys fs.FS, root string) (PolicyTree, error) { out := PolicyTree{} err := fs.WalkDir(fsys, root, func(p string, d fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } if d.IsDir() || d.Name() != ".zddc" { return nil } rel := strings.TrimPrefix(path.Dir(p), root) rel = strings.Trim(rel, "/") key := "" if rel != "" { parts := strings.Split(rel, "/") for i, s := range parts { if s == AnyPlaceholder { parts[i] = "*" } } key = strings.Join(parts, "/") } data, err := fs.ReadFile(fsys, p) if err != nil { return err } zf, err := parseBytes(data) if err != nil { return err } out[key] = zf return nil }) if err != nil { return nil, err } return out, nil } // LoadPolicyTreeFromDir is the on-disk convenience over LoadPolicyTreeFromFS. func LoadPolicyTreeFromDir(fsDir string) (PolicyTree, error) { return LoadPolicyTreeFromFS(os.DirFS(fsDir), ".") }