Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

synfs.gno

6.60 Kb · 231 lines
  1// Package synfs builds a synthetic, read-only ninep file server whose contents
  2// are computed by a function at read time.
  3//
  4// This is the package that makes the rest of the suite adoptable. Plan 9's
  5// device drivers are not storage, they are code behind a name: reading
  6// /dev/time runs a function. A realm that wants to publish its state as a
  7// 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 hand
 16// to another realm's namespace: every method is free of side effects, so it
 17// 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 on
 20// every block, so a version would be a lie; clients that need change detection
 21// should read the file.
 22//
 23// NOTICE. Plan 9 from Bell Labs is the work of the Computing Science Research
 24// Center at Bell Labs; the name and the marks are theirs, and the copyright is
 25// held by the Plan 9 Foundation (https://p9f.org). This package is not
 26// affiliated with, endorsed by, or sponsored by them, and contains no Plan 9
 27// 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 a
 29// chain. Full attribution: NOTICE.md at the root of moul/gno-contracts.
 30package synfs
 31
 32import (
 33	"gno.land/p/nt/avl/v0"
 34
 35	ninep "gno.land/p/moul/x/plan9/ninep/v0"
 36)
 37
 38// ReadFn computes a whole file.
 39type ReadFn func() string
 40
 41// RangeFn serves a 9P read window directly, for files that are cheaper (or
 42// only possible) to generate a slice at a time, such as an endless one.
 43type RangeFn func(off, count int64) (string, error)
 44
 45// Tree owns the qid allocator, the clock and the owner name shared by every
 46// node in one synthetic server.
 47type Tree struct {
 48	root  *Dir
 49	next  uint64
 50	clock func() int64
 51	uid   string
 52}
 53
 54// New returns a tree whose root directory is named name, owned by uid, using
 55// 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 t
 61}
 62
 63// Root returns the tree's root directory.
 64func (t *Tree) Root() *Dir { return t.root }
 65
 66// Uid returns the owner stamped on every node.
 67func (t *Tree) Uid() string { return t.uid }
 68
 69func (t *Tree) now() int64 {
 70	if t.clock == nil {
 71		return 0
 72	}
 73	return t.clock()
 74}
 75
 76func (t *Tree) qid(dir bool) ninep.Qid {
 77	t.next++
 78	qt := ninep.QTFILE
 79	if dir {
 80		qt = ninep.QTDIR
 81	}
 82	return ninep.Qid{Type: qt, Version: 0, Path: t.next}
 83}
 84
 85// NewDir returns a detached directory belonging to this tree. Attach it with
 86// 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}
 96
 97// Dir is a synthetic directory. Its children are held in an avl tree, so a
 98// listing is ordered by name and identical on every node.
 99type Dir struct {
100	tree     *Tree
101	name     string
102	perm     ninep.Perm
103	qid      ninep.Qid
104	children *avl.Tree // name -> ninep.File
105}
106
107// Add attaches a read-only file computed by fn, mode 0444. It returns the
108// directory, so calls chain.
109func (d *Dir) Add(name string, fn ReadFn) *Dir {
110	return d.AddPerm(name, 0444, fn)
111}
112
113// AddPerm is Add with an explicit mode.
114func (d *Dir) AddPerm(name string, perm ninep.Perm, fn ReadFn) *Dir {
115	whole := fn
116	return d.AddRange(name, perm, func(off, count int64) (string, error) {
117		return ninep.Slice(whole(), off, count), nil
118	})
119}
120
121// AddRange attaches a file that serves a read window itself. Use it for a file
122// 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 d
135}
136
137// 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 d
144}
145
146// AddFile attaches any ninep.File under the given name, which is how a
147// 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 d
154}
155
156// Names returns the directory's entries, in order. Useful for a file that
157// 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 false
163	})
164	return out
165}
166
167// 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}
179
180// Walk implements ninep.File.
181func (d *Dir) Walk(name string) (ninep.File, error) {
182	if !ninep.ValidName(name) {
183		return nil, ninep.ErrBadName
184	}
185	v := d.children.Get(name)
186	if v == nil {
187		return nil, ninep.ErrNotExist
188	}
189	return v.(ninep.File), nil
190}
191
192// Read implements ninep.File.
193func (d *Dir) Read(off, count int64) (string, error) { return "", ninep.ErrIsDir }
194
195// 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 false
201	})
202	return out, nil
203}
204
205type file struct {
206	tree *Tree
207	name string
208	perm ninep.Perm
209	qid  ninep.Qid
210	fn   RangeFn
211}
212
213func (f *file) Stat() ninep.Stat {
214	// A synthetic file has no stored length. 9P servers for such files report
215	// 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}
226
227func (f *file) Walk(name string) (ninep.File, error) { return nil, ninep.ErrNotDir }
228
229func (f *file) Read(off, count int64) (string, error) { return f.fn(off, count) }
230
231func (f *file) ReadDir() ([]ninep.Stat, error) { return nil, ninep.ErrNotDir }