package virtfs import ( "context" "fmt" "os" "strings" "sync" ) // BuildTree walks an FsNodeDecl spec and produces a *Tree. func BuildTree(spec FsNodeDecl) *Tree { validate(&spec, "") return build(&spec, "", "") } // ── validation ─────────────────────────────────────────────────── func validate(d *FsNodeDecl, path string) { hasChildren := len(d.Children) > 0 hasBindings := d.Bindings != nil hasFileHandlers := d.Read != nil || d.Write != nil || d.BlockOnce != nil || d.Stream != nil || d.Rdwr != nil isDir := hasChildren name := d.Name fullPath := path + "/" + name if path == "" { fullPath = name } if isDir && hasFileHandlers { panic(fmt.Sprintf("BuildTree: %q: directory node cannot have file handlers", fullPath)) } var blocking int if d.BlockOnce != nil { blocking++ } if d.Stream != nil { blocking++ } if d.Rdwr != nil { blocking++ } if blocking > 1 { panic(fmt.Sprintf("BuildTree: %q: at most one of BlockOnce, Stream, Rdwr may be set", fullPath)) } // Template nodes must have Bindings set. if strings.HasPrefix(name, "{") && strings.HasSuffix(name, "}") && !hasBindings { panic(fmt.Sprintf("BuildTree: %q: template node %q requires Bindings", fullPath, name)) } // Validate children recursively. for i := range d.Children { validate(&d.Children[i], fullPath) } // Validate binding children if we can enumerate them (skip for dynamic). _ = hasBindings } // build ──────────────────────────────────────────────────────── func build(d *FsNodeDecl, parentUid, parentGid string) *Tree { uid := d.UID if uid == "" { uid = parentUid } gid := d.GID if gid == "" { gid = parentGid } mode := d.Mode hasChildren := len(d.Children) > 0 isDir := hasChildren || d.Bindings != nil if isDir { if mode == 0 { mode = 0755 } t := &Tree{perm: mode} t.listFn = func(_ []string) ([]os.DirEntry, error) { return listDir(d, uid, gid) } t.readdirFn = func(_ []string, name string) ([]os.DirEntry, error) { return readdirDir(d, uid, gid, name) } t.statFn = func(_ []string, name string) (os.FileInfo, error) { return statDir(d, uid, gid, name) } t.openFn = func(_ []string, name string) (File, error) { return openDir(d, uid, gid, name) } t.deleteFn = func(_ []string, name string) error { return deleteDir(d, name) } t.createFn = func(_ []string, name string, _ os.FileMode) error { return fmt.Errorf("create not supported") } t.renameFn = func(_ []string, old, new string) error { return renameDir(d, old, new) } return t } // Leaf node. if mode == 0 { mode = 0444 } t := &Tree{} t.listFn = func(_ []string) ([]os.DirEntry, error) { return nil, fmt.Errorf("%s: is a file", d.Name) } t.openFn = func(_ []string, name string) (File, error) { return openLeaf(d, uid, gid, mode, name) } return t } // ── directory ops ──────────────────────────────────────────────── func listDir(d *FsNodeDecl, uid, gid string) ([]os.DirEntry, error) { var entries []os.DirEntry // If this directory has Bindings (from Each), enumerate them directly. if d.Bindings != nil && len(d.Children) == 0 { nodes, err := d.Bindings() if err != nil { return nil, err } for _, n := range nodes { m := n.Mode if m == 0 { m = d.Mode } if len(n.Children) > 0 || n.Bindings != nil { entries = append(entries, dirEntry(n.Name, 0755)) } else { entries = append(entries, fileEntry(n.Name, m)) } } return entries, nil } for _, c := range d.Children { if isTemplateName(c.Name) { if c.Bindings != nil { nodes, err := c.Bindings() if err != nil { continue } for _, n := range nodes { if len(n.Children) > 0 || n.Bindings != nil { entries = append(entries, dirEntry(n.Name, 0755)) } else { entries = append(entries, fileEntry(n.Name, c.Mode)) } } } continue } m := c.Mode if m == 0 { m = 0755 } hasFileHandlers := c.Read != nil || c.Write != nil || c.BlockOnce != nil || c.Stream != nil || c.Rdwr != nil isChildDir := len(c.Children) > 0 || (c.Bindings != nil && !hasFileHandlers) if isChildDir { entries = append(entries, dirEntry(c.Name, m)) } else { entries = append(entries, fileEntry(c.Name, m)) } } return entries, nil } func readdirDir(d *FsNodeDecl, uid, gid, name string) ([]os.DirEntry, error) { child := findChild(d, name) if child == nil { return nil, fmt.Errorf("%s: not found", name) } return listDir(child, resolveUID(child, uid), resolveGID(child, gid)) } func statDir(d *FsNodeDecl, uid, gid, name string) (os.FileInfo, error) { if name == "" || name == "." { m := d.Mode if m == 0 { m = 0755 } return &SyntheticFileInfo{Name_: d.Name, Mode_: m | os.ModeDir, IsDir_: true, UID_: uid, GID_: gid}, nil } // Traverse the path step-by-step to accumulate inherited UID/GID. child, inheritedUID, inheritedGID := findChildWithInheritance(d, uid, gid, name) if child == nil { return nil, fmt.Errorf("%s: not found", name) } if child.Stat != nil { return child.Stat(), nil } cm := child.Mode if cm == 0 { cm = 0755 } cu := resolveUID(child, inheritedUID) cg := resolveGID(child, inheritedGID) hasFileHandlers := child.Read != nil || child.Write != nil || child.BlockOnce != nil || child.Stream != nil || child.Rdwr != nil isChildDir := len(child.Children) > 0 || (child.Bindings != nil && !hasFileHandlers) if isChildDir { return &SyntheticFileInfo{Name_: name, Mode_: cm | os.ModeDir, IsDir_: true, UID_: cu, GID_: cg}, nil } return &SyntheticFileInfo{Name_: name, Mode_: cm, UID_: cu, GID_: cg}, nil } func openDir(d *FsNodeDecl, uid, gid string, name string) (File, error) { child := findChild(d, name) if child == nil { return nil, fmt.Errorf("%s: not found", name) } sub := build(child, resolveUID(child, uid), resolveGID(child, gid)) return sub.Open("") } func deleteDir(d *FsNodeDecl, name string) error { child := findChild(d, name) if child == nil { return fmt.Errorf("%s: not found", name) } if child.Remove != nil { return child.Remove() } return fmt.Errorf("%s: not removable", name) } func renameDir(d *FsNodeDecl, old, new string) error { child := findChild(d, old) if child == nil { return fmt.Errorf("%s: not found", old) } if child.Rename != nil { return child.Rename(new) } return fmt.Errorf("rename not supported") } // ── leaf ops ───────────────────────────────────────────────────── func openLeaf(d *FsNodeDecl, uid, gid string, mode os.FileMode, name string) (File, error) { notBlocking := func(context.Context, string) ([]byte, string, error) { return nil, "", fmt.Errorf("blocking read not supported") } readFn := func() ([]byte, error) { if d.Read != nil { return d.Read() } return []byte{}, nil } writeFn := func(data []byte) error { if d.Write != nil { return d.Write(data) } return fmt.Errorf("%s: read-only", d.Name) } var blockFn func(context.Context, string) ([]byte, string, error) var isBlocking, isStreaming, isRdwr bool switch { case d.BlockOnce != nil: blockFn = d.BlockOnce isBlocking = true case d.Stream != nil: blockFn = d.Stream isStreaming = true case d.Rdwr != nil: fn := d.Rdwr var stateMu sync.Mutex var writeMu sync.Mutex var result []byte var reqCancel context.CancelFunc blockFn = notBlocking readFn = func() ([]byte, error) { stateMu.Lock() if result != nil { out := make([]byte, len(result)) copy(out, result) stateMu.Unlock() return out, nil } stateMu.Unlock() if d.Read != nil { return d.Read() } return nil, nil } writeFn = func(data []byte) error { writeMu.Lock() defer writeMu.Unlock() ctx, cancel := context.WithCancel(context.Background()) stateMu.Lock() reqCancel = cancel stateMu.Unlock() r, err := fn(ctx, data) stateMu.Lock() reqCancel = nil if err == nil { result = append(result[:0], r...) } stateMu.Unlock() cancel() return err } closeFn := func() error { stateMu.Lock() cancel := reqCancel stateMu.Unlock() if cancel != nil { cancel() } return nil } isRdwr = true return &fileConfig{ StatFn: func() (os.FileInfo, error) { if d.Stat != nil { return d.Stat(), nil } data, _ := readFn() return &SyntheticFileInfo{Name_: name, Mode_: mode, Size_: int64(len(data)), UID_: uid, GID_: gid}, nil }, ReadFn: readFn, WriteFn: writeFn, CloseFn: closeFn, BlockingReadFn: blockFn, BlockingRead_: isBlocking, StreamMode_: isStreaming, Rdwr_: isRdwr, }, nil default: blockFn = notBlocking } return &fileConfig{ StatFn: func() (os.FileInfo, error) { if d.Stat != nil { return d.Stat(), nil } data, _ := readFn() return &SyntheticFileInfo{Name_: name, Mode_: mode, Size_: int64(len(data)), UID_: uid, GID_: gid}, nil }, ReadFn: readFn, WriteFn: writeFn, BlockingReadFn: blockFn, BlockingRead_: isBlocking, StreamMode_: isStreaming, Rdwr_: isRdwr, }, nil } // ── helpers ────────────────────────────────────────────────────── // findChildWithInheritance traverses the path and returns the final child // along with the accumulated inherited UID/GID from intermediate directories. func findChildWithInheritance(d *FsNodeDecl, uid, gid, name string) (*FsNodeDecl, string, string) { // Handle nested paths by traversing step-by-step. if i := strings.IndexByte(name, '/'); i >= 0 { prefix := name[:i] rest := name[i+1:] child := findChildSingle(d, prefix) if child == nil { return nil, "", "" } // Inherit UID/GID from this intermediate directory. childUID := resolveUID(child, uid) childGID := resolveGID(child, gid) return findChildWithInheritance(child, childUID, childGID, rest) } // Final segment — return the child and the inherited UID/GID. child := findChildSingle(d, name) return child, uid, gid } // findChildSingle finds a direct child by name (no path traversal). func findChildSingle(d *FsNodeDecl, name string) *FsNodeDecl { // If this directory has Bindings and no Children (from Each with non-template name), // look up children in the bindings directly. if d.Bindings != nil && len(d.Children) == 0 { entries, err := d.Bindings() if err != nil { return nil } for i := range entries { if entries[i].Name == name || matchAlias(entries[i].Aliases, name) { return &entries[i] } } return nil } // Static children. for i := range d.Children { if !isTemplateName(d.Children[i].Name) && d.Children[i].Name == name { return &d.Children[i] } } // Dynamic children (Each): find matching entry by name/alias. for i := range d.Children { if isTemplateName(d.Children[i].Name) && d.Children[i].Bindings != nil { entries, err := d.Children[i].Bindings() if err != nil { continue } for j := range entries { if entries[j].Name == name || matchAlias(entries[j].Aliases, name) { return &entries[j] } } } } return nil } func findChild(d *FsNodeDecl, name string) *FsNodeDecl { // Handle nested paths recursively. if i := strings.IndexByte(name, '/'); i >= 0 { prefix := name[:i] rest := name[i+1:] child := findChild(d, prefix) if child == nil { return nil } return findChild(child, rest) } return findChildSingle(d, name) } func matchAlias(aliases []string, name string) bool { for _, a := range aliases { if a == name { return true } } return false } func resolveUID(d *FsNodeDecl, parent string) string { if d.UID != "" { return d.UID } return parent } func resolveGID(d *FsNodeDecl, parent string) string { if d.GID != "" { return d.GID } return parent } func isTemplateName(name string) bool { return len(name) > 2 && name[0] == '{' && name[len(name)-1] == '}' }