// Package virtfs provides an embedded domain-specific language for declaring // synthetic filesystems. It is designed for building 9P namespaces where the // entire filesystem structure is declared statically but populated dynamically. // // Handlers are plain closures that capture their state at binding time. // There is no context type parameter — each handler already knows its target. // // Example usage: // // users := getUserList() // spec := DirNode("/", // FileNode("version", 0444, Read(func() ([]byte, error) { // return []byte("1.0\n"), nil // })), // Each("{user}", func() ([]FsNodeDecl, error) { // var out []FsNodeDecl // for _, u := range users { // user := u // out = append(out, FsNodeDecl{ // Name: user.Name, // Children: []FsNodeDecl{ // FileNode("name", 0444, Read(func() ([]byte, error) { // return []byte(user.Name + "\n"), nil // })), // }, // }) // } // return out, nil // }), // ) // // tree := BuildTree(spec) package virtfs import ( "context" "os" ) // FsNodeDecl describes one node in a filesystem namespace. // Handlers are closures — they capture their target at construction time. type FsNodeDecl struct { // Identity. Name string Aliases []string // alternate names that resolve to this node UID string // "" = inherit from parent GID string // "" = inherit from parent Mode os.FileMode // 0 = inherit parent default Desc string // human-readable description // Tstat — metadata override. Stat func() os.FileInfo // Twalk — children (implies directory; at most one set). Children []FsNodeDecl // static, known at compile time Bindings func() ([]FsNodeDecl, error) // Each: dynamic children // Tread (non-blocking) + Twrite. Read func() ([]byte, error) Write func(data []byte) error // Tread — blocking variants (at most one set). BlockOnce func(ctx context.Context, base string) ([]byte, string, error) Stream func(ctx context.Context, base string) ([]byte, string, error) // Rdwr — atomic write-then-read: write triggers computation, read returns result once. Rdwr func(ctx context.Context, data []byte) ([]byte, error) // Tremove + Trename. Remove func() error Rename func(newName string) error } // NodeOption configures a FsNodeDecl at construction time. type NodeOption func(*FsNodeDecl) // ── EDSL constructors ──────────────────────────────────────────── // DirNode declares a static directory node. func DirNode(name string, args ...any) FsNodeDecl { d := FsNodeDecl{Name: name} for _, a := range args { switch v := a.(type) { case FsNodeDecl: d.Children = append(d.Children, v) case []FsNodeDecl: d.Children = append(d.Children, v...) case NodeOption: v(&d) } } return d } // Each declares a directory whose children are produced dynamically. func Each(name string, list func() ([]FsNodeDecl, error), opts ...NodeOption) FsNodeDecl { d := FsNodeDecl{Name: name, Bindings: list} for _, o := range opts { o(&d) } return d } // FileNode declares a file node (non-directory). func FileNode(name string, mode os.FileMode, opts ...NodeOption) FsNodeDecl { d := FsNodeDecl{Name: name, Mode: mode} for _, o := range opts { o(&d) } return d } // ── Options ────────────────────────────────────────────────────── // Read sets the read handler for a file node. func Read(fn func() ([]byte, error)) NodeOption { return func(d *FsNodeDecl) { d.Read = fn } } // Write sets the write handler for a file node. func Write(fn func([]byte) error) NodeOption { return func(d *FsNodeDecl) { d.Write = fn } } // Stream sets a streaming read handler that blocks indefinitely. // readFn returns (data, nextBase, error) given the current base. // signal returns a channel that is closed when new data may be available. // The framework blocks on signal until readFn returns non-empty data. func Stream(readFn func(base string) ([]byte, string, error), signal func() <-chan struct{}) NodeOption { return func(d *FsNodeDecl) { d.Stream = func(ctx context.Context, base string) ([]byte, string, error) { for { // Capture the signal channel BEFORE reading. A writer that // appends between our read and our wait closes this channel, // so the subsequent select returns immediately instead of // blocking on an already-superseded channel (lost wakeup). sig := signal() data, nextBase, err := readFn(base) if err != nil { return nil, base, err } if len(data) > 0 { return data, nextBase, nil } base = nextBase select { case <-sig: case <-ctx.Done(): return nil, base, nil } } } } } // StreamRaw sets a raw streaming read handler with custom blocking logic. func StreamRaw(fn func(context.Context, string) ([]byte, string, error)) NodeOption { return func(d *FsNodeDecl) { d.Stream = fn } } // BlockOnce sets a blocking read handler that returns once per open. // readFn returns (data, hash, error) — the current value. // signal returns a channel that is closed when the value may have changed. // The framework blocks until readFn returns a hash different from base. func BlockOnce(readFn func() ([]byte, string, error), signal func() <-chan struct{}) NodeOption { return func(d *FsNodeDecl) { d.BlockOnce = func(ctx context.Context, base string) ([]byte, string, error) { for { // Capture the signal channel before reading, closing the // lost-wakeup window (see Stream above). sig := signal() data, hash, err := readFn() if err != nil { return nil, base, err } if base == "" { base = hash } if hash != base { return data, hash, nil } select { case <-sig: case <-ctx.Done(): return nil, "", nil // timeout: empty response, client re-opens } } } } } // BlockOnceRaw sets a blocking read handler with custom blocking logic. // Use for queue-style consumers where there is no "current value" to compare. func BlockOnceRaw(fn func(context.Context, string) ([]byte, string, error)) NodeOption { return func(d *FsNodeDecl) { d.BlockOnce = fn } } // Rdwr sets an atomic write-then-read handler. // Write triggers computation; the subsequent read returns the result once. func Rdwr(fn func(context.Context, []byte) ([]byte, error)) NodeOption { return func(d *FsNodeDecl) { d.Rdwr = fn } } // StatOverride sets a custom stat handler. func StatOverride(fn func() os.FileInfo) NodeOption { return func(d *FsNodeDecl) { d.Stat = fn } } // UID sets the owner for a node. func UID(uid string) NodeOption { return func(d *FsNodeDecl) { d.UID = uid } } // GID sets the group for a node. func GID(gid string) NodeOption { return func(d *FsNodeDecl) { d.GID = gid } } // Doc sets a human-readable description for the node. func Doc(desc string) NodeOption { return func(d *FsNodeDecl) { d.Desc = desc } }