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 }