ZDDC/zddc/internal/zddc/zippolicy.go
2026-06-11 13:32:31 -05:00

170 lines
5.1 KiB
Go

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/<anyproject>/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), ".")
}