ollie/virtfs/tree.go

237 lines
6.2 KiB
Go

package virtfs
import (
"fmt"
"os"
"path/filepath"
)
// Tree is a synthetic filesystem node.
//
// Two usage modes:
//
// 1. EDSL (BuildTree): Returns a single root node. Children are resolved lazily
// via closures over the FsNodeDecl spec. The children map is NOT populated;
// all path resolution happens through the handler closures (openFn, statFn, etc).
//
// 2. Manual (Mount): Children are attached explicitly via Mount(). The split()
// method routes path lookups through the children map before falling through
// to handler closures.
//
// In production, only mode 1 is used. Mode 2 exists for composability and testing.
type Tree struct {
Tree []string // backing dirs (nil in EDSL trees)
perm os.FileMode
listFn func([]string) ([]os.DirEntry, error)
createFn func([]string, string, os.FileMode) error
deleteFn func([]string, string) error
renameFn func([]string, string, string) error
openFn func([]string, string) (File, error)
statFn func([]string, string) (os.FileInfo, error)
readdirFn func([]string, string) ([]os.DirEntry, error)
children map[string]*Tree
Data any // user-attached state
}
// treeOption configures a Tree.
type treeOption func(*Tree)
// withLister overrides how the tree lists its entries.
func withLister(fn func(dirs []string) ([]os.DirEntry, error)) treeOption {
return func(d *Tree) { d.listFn = fn }
}
// withCreator overrides file creation logic.
func withCreator(fn func(dirs []string, name string, perm os.FileMode) error) treeOption {
return func(d *Tree) { d.createFn = fn }
}
// withDeleter overrides file deletion logic.
func withDeleter(fn func(dirs []string, name string) error) treeOption {
return func(d *Tree) { d.deleteFn = fn }
}
// withRenamer overrides rename logic.
func withRenamer(fn func(dirs []string, old, new string) error) treeOption {
return func(d *Tree) { d.renameFn = fn }
}
// withOpener overrides how entries are opened.
func withOpener(fn func(dirs []string, name string) (File, error)) treeOption {
return func(d *Tree) { d.openFn = fn }
}
// withStat overrides how entries are stat'd.
func withStat(fn func(dirs []string, name string) (os.FileInfo, error)) treeOption {
return func(d *Tree) { d.statFn = fn }
}
// withReaddir overrides subdirectory listing.
func withReaddir(fn func(dirs []string, name string) ([]os.DirEntry, error)) treeOption {
return func(d *Tree) { d.readdirFn = fn }
}
// newTree creates a Tree. In EDSL usage, dirs is nil and all behavior
// is provided via options.
func newTree(dirs []string, perm os.FileMode, opts ...treeOption) *Tree {
d := &Tree{Tree: dirs, perm: perm}
for _, o := range opts {
o(d)
}
return d
}
// Mount attaches a child Tree at the given name.
func (d *Tree) Mount(name string, child *Tree) {
if d.children == nil {
d.children = make(map[string]*Tree)
}
d.children[name] = child
}
// Child returns the child mounted at name, or nil.
func (d *Tree) Child(name string) *Tree {
if d.children == nil {
return nil
}
return d.children[name]
}
// Children returns all mounted children.
func (d *Tree) Children() map[string]*Tree {
return d.children
}
// split checks if name routes to a child tree.
func (d *Tree) split(name string) (*Tree, string, bool) {
if d.children == nil {
return nil, "", false
}
if child, ok := d.children[name]; ok {
return child, "", true
}
if i := indexOf(name, '/'); i >= 0 {
prefix := name[:i]
if child, ok := d.children[prefix]; ok {
return child, name[i+1:], true
}
}
return nil, "", false
}
func indexOf(s string, b byte) int {
for i := range len(s) {
if s[i] == b {
return i
}
}
return -1
}
func (d *Tree) Stat(name string) (os.FileInfo, error) {
if child, rest, ok := d.split(name); ok {
if rest == "" {
return &SyntheticFileInfo{Name_: name, Mode_: PermChildDir, IsDir_: true}, nil
}
return child.Stat(rest)
}
if d.statFn != nil {
return d.statFn(d.Tree, name)
}
if d.openFn != nil {
e, err := d.openFn(d.Tree, name)
if err != nil {
return nil, err
}
return e.Stat()
}
return nil, fmt.Errorf("%s: not found", name)
}
func (d *Tree) List() ([]os.DirEntry, error) {
if d.listFn != nil {
entries, err := d.listFn(d.Tree)
if err != nil {
return nil, err
}
for name := range d.children {
entries = append(entries, dirEntry(name, PermChildDir))
}
return entries, nil
}
// No list function and no backing dirs — return children only.
var result []os.DirEntry
for name := range d.children {
result = append(result, dirEntry(name, PermChildDir))
}
return result, nil
}
func (d *Tree) Open(name string) (File, error) {
if child, rest, ok := d.split(name); ok {
if rest == "" {
return nil, fmt.Errorf("%s: is a directory", name)
}
return child.Open(rest)
}
if d.openFn != nil {
return d.openFn(d.Tree, name)
}
return nil, fmt.Errorf("%s: not found", name)
}
func (d *Tree) Create(name string) error {
if child, rest, ok := d.split(name); ok && rest != "" {
return child.Create(rest)
}
if d.createFn != nil {
return d.createFn(d.Tree, name, d.perm)
}
return fmt.Errorf("create not supported")
}
func (d *Tree) Delete(name string) error {
if child, rest, ok := d.split(name); ok && rest != "" {
return child.Delete(rest)
}
if d.deleteFn != nil {
return d.deleteFn(d.Tree, name)
}
return fmt.Errorf("delete not supported")
}
func (d *Tree) Rename(old, new string) error {
if child, rest, ok := d.split(old); ok && rest != "" {
return child.Rename(rest, new)
}
if d.renameFn != nil {
return d.renameFn(d.Tree, old, new)
}
return fmt.Errorf("rename not supported")
}
// Readdir lists entries in a subdirectory.
func (d *Tree) Readdir(rel string) ([]os.DirEntry, error) {
if child, rest, ok := d.split(rel); ok {
if rest == "" {
return child.List()
}
return child.Readdir(rest)
}
if d.readdirFn != nil {
return d.readdirFn(d.Tree, rel)
}
return nil, fmt.Errorf("%s: not found", rel)
}
// MkdirAll creates a directory relative to the first backing dir.
func (d *Tree) MkdirAll(rel string) error {
if child, rest, ok := d.split(rel); ok && rest != "" {
return child.MkdirAll(rest)
}
if len(d.Tree) == 0 {
return fmt.Errorf("no backing directory")
}
return os.MkdirAll(filepath.Join(d.Tree[0], rel), PermMkdir)
}