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) }