1// Package synfs builds a synthetic, read-only ninep file server whose contents2// are computed by a function at read time.3//4// This is the package that makes the rest of the suite adoptable. Plan 9's5// device drivers are not storage, they are code behind a name: reading6// /dev/time runs a function. A realm that wants to publish its state as a7// browsable tree does the same thing here, in a few lines, and gets ls, cat,8// stat and mountability for free:9//10// t := synfs.New("dev", "g1...", func() int64 { return runtime.ChainHeight() })11// t.Root().12// Add("sysname", func() string { return runtime.ChainID() }).13// Add("height", func() string { return strconv.FormatInt(runtime.ChainHeight(), 10) })14//15// The tree is READ-ONLY by construction, which is what makes it safe to hand16// to another realm's namespace: every method is free of side effects, so it17// can be called from a frame that does not own these objects.18//19// A synthetic file reports Qid.Version 0 forever. Its contents can change on20// every block, so a version would be a lie; clients that need change detection21// should read the file.22//23// NOTICE. Plan 9 from Bell Labs is the work of the Computing Science Research24// Center at Bell Labs; the name and the marks are theirs, and the copyright is25// held by the Plan 9 Foundation (https://p9f.org). This package is not26// affiliated with, endorsed by, or sponsored by them, and contains no Plan 927// code: it borrows the vocabulary so that the design reads without a glossary,28// and it is an homage, asking what that ecosystem's spirit looks like on a29// chain. Full attribution: NOTICE.md at the root of moul/gno-contracts.30package synfs3132import (33 "gno.land/p/nt/avl/v0"3435 ninep "gno.land/p/moul/x/plan9/ninep/v0"36)3738// ReadFn computes a whole file.39type ReadFn func() string4041// RangeFn serves a 9P read window directly, for files that are cheaper (or42// only possible) to generate a slice at a time, such as an endless one.43type RangeFn func(off, count int64) (string, error)4445// Tree owns the qid allocator, the clock and the owner name shared by every46// node in one synthetic server.47type Tree struct {48 root *Dir49 next uint6450 clock func() int6451 uid string52}5354// New returns a tree whose root directory is named name, owned by uid, using55// clock (normally the block height) as every node's mtime. clock may be nil,56// in which case the mtime is zero.57func New(name, uid string, clock func() int64) *Tree {58 t := &Tree{clock: clock, uid: uid}59 t.root = t.NewDir(name)60 return t61}6263// Root returns the tree's root directory.64func (t *Tree) Root() *Dir { return t.root }6566// Uid returns the owner stamped on every node.67func (t *Tree) Uid() string { return t.uid }6869func (t *Tree) now() int64 {70 if t.clock == nil {71 return 072 }73 return t.clock()74}7576func (t *Tree) qid(dir bool) ninep.Qid {77 t.next++78 qt := ninep.QTFILE79 if dir {80 qt = ninep.QTDIR81 }82 return ninep.Qid{Type: qt, Version: 0, Path: t.next}83}8485// NewDir returns a detached directory belonging to this tree. Attach it with86// AddDir.87func (t *Tree) NewDir(name string) *Dir {88 return &Dir{89 tree: t,90 name: name,91 perm: ninep.DMDIR | 0555,92 qid: t.qid(true),93 children: avl.NewTree(),94 }95}9697// Dir is a synthetic directory. Its children are held in an avl tree, so a98// listing is ordered by name and identical on every node.99type Dir struct {100 tree *Tree101 name string102 perm ninep.Perm103 qid ninep.Qid104 children *avl.Tree // name -> ninep.File105}106107// Add attaches a read-only file computed by fn, mode 0444. It returns the108// directory, so calls chain.109func (d *Dir) Add(name string, fn ReadFn) *Dir {110 return d.AddPerm(name, 0444, fn)111}112113// AddPerm is Add with an explicit mode.114func (d *Dir) AddPerm(name string, perm ninep.Perm, fn ReadFn) *Dir {115 whole := fn116 return d.AddRange(name, perm, func(off, count int64) (string, error) {117 return ninep.Slice(whole(), off, count), nil118 })119}120121// AddRange attaches a file that serves a read window itself. Use it for a file122// with no natural end, where materialising the whole thing would be wrong.123func (d *Dir) AddRange(name string, perm ninep.Perm, fn RangeFn) *Dir {124 if !ninep.ValidName(name) {125 panic("synfs: invalid file name: " + name)126 }127 d.children.Set(name, &file{128 tree: d.tree,129 name: name,130 perm: perm &^ ninep.DMDIR,131 qid: d.tree.qid(false),132 fn: fn,133 })134 return d135}136137// AddDir attaches a subdirectory built with Tree.NewDir.138func (d *Dir) AddDir(sub *Dir) *Dir {139 if !ninep.ValidName(sub.name) {140 panic("synfs: invalid directory name: " + sub.name)141 }142 d.children.Set(sub.name, sub)143 return d144}145146// AddFile attaches any ninep.File under the given name, which is how a147// synthetic tree splices in a tree served by something else.148func (d *Dir) AddFile(name string, f ninep.File) *Dir {149 if !ninep.ValidName(name) {150 panic("synfs: invalid file name: " + name)151 }152 d.children.Set(name, f)153 return d154}155156// Names returns the directory's entries, in order. Useful for a file that157// wants to describe its own directory, as /dev/drivers does.158func (d *Dir) Names() []string {159 out := []string{}160 d.children.Iterate("", "", func(k string, _ any) bool {161 out = append(out, k)162 return false163 })164 return out165}166167// Stat implements ninep.File.168func (d *Dir) Stat() ninep.Stat {169 return ninep.Stat{170 Qid: d.qid,171 Mode: d.perm,172 Mtime: d.tree.now(),173 Name: d.name,174 Uid: d.tree.uid,175 Gid: d.tree.uid,176 Muid: d.tree.uid,177 }178}179180// Walk implements ninep.File.181func (d *Dir) Walk(name string) (ninep.File, error) {182 if !ninep.ValidName(name) {183 return nil, ninep.ErrBadName184 }185 v := d.children.Get(name)186 if v == nil {187 return nil, ninep.ErrNotExist188 }189 return v.(ninep.File), nil190}191192// Read implements ninep.File.193func (d *Dir) Read(off, count int64) (string, error) { return "", ninep.ErrIsDir }194195// ReadDir implements ninep.File.196func (d *Dir) ReadDir() ([]ninep.Stat, error) {197 out := []ninep.Stat{}198 d.children.Iterate("", "", func(_ string, v any) bool {199 out = append(out, v.(ninep.File).Stat())200 return false201 })202 return out, nil203}204205type file struct {206 tree *Tree207 name string208 perm ninep.Perm209 qid ninep.Qid210 fn RangeFn211}212213func (f *file) Stat() ninep.Stat {214 // A synthetic file has no stored length. 9P servers for such files report215 // zero rather than run the generator just to measure it, and so does this.216 return ninep.Stat{217 Qid: f.qid,218 Mode: f.perm,219 Mtime: f.tree.now(),220 Name: f.name,221 Uid: f.tree.uid,222 Gid: f.tree.uid,223 Muid: f.tree.uid,224 }225}226227func (f *file) Walk(name string) (ninep.File, error) { return nil, ninep.ErrNotDir }228229func (f *file) Read(off, count int64) (string, error) { return f.fn(off, count) }230231func (f *file) ReadDir() ([]ninep.Stat, error) { return nil, ninep.ErrNotDir }232Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.