package virtfs import ( "fmt" "os" "path/filepath" "strings" ) // 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 } // 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 := strings.IndexByte(name, '/'); i >= 0 { prefix := name[:i] if child, ok := d.children[prefix]; ok { return child, name[i+1:], true } } return nil, "", false } 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 } defer e.Close() 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) }