170 lines
5.1 KiB
Go
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), ".")
|
|
}
|