memfs.gno
7.12 Kb · 282 lines
1// Package memfs is a RAM file server: Plan 9's ramfs, in a realm's heap.
2//
3// It is the reference implementation of gno.land/p/moul/x/plan9/ninep's File
4// and Mutable, and the tree a namespace server hands out as a user's private
5// root. Children live in an avl.Tree, so a directory listing is ordered by
6// name and therefore identical on every validating node; a map would make
7// Render a consensus bug.
8//
9// Mutable is IN-REALM ONLY. A non-crossing method runs in the caller's frame,
10// so a foreign realm calling Create or Write here would be mutating objects it
11// does not own. Reads (the ninep.File half) are safe from anywhere, which is
12// what makes a memfs tree mountable into somebody else's namespace.
13//
14// The caller supplies the clock (a block height) on every mutation rather than
15// the tree reading chain state itself, so the same tree is exercisable in a
16// plain unit test.
17//
18// NOTICE. Plan 9 from Bell Labs is the work of the Computing Science Research
19// Center at Bell Labs; the name and the marks are theirs, and the copyright is
20// held by the Plan 9 Foundation (https://p9f.org). This package is not
21// affiliated with, endorsed by, or sponsored by them, and contains no Plan 9
22// code: it borrows the vocabulary so that the design reads without a glossary,
23// and it is an homage, asking what that ecosystem's spirit looks like on a
24// chain. Full attribution: NOTICE.md at the root of moul/gno-contracts.
25package memfs
26
27import (
28 "strings"
29
30 "gno.land/p/nt/avl/v0"
31
32 ninep "gno.land/p/moul/x/plan9/ninep/v0"
33)
34
35// FS is a mutable in-memory file tree.
36type FS struct {
37 root *node
38 next uint64 // qid path allocator, unique for this server's lifetime
39 uid string
40}
41
42type node struct {
43 fs *FS
44 name string
45 perm ninep.Perm
46 qid ninep.Qid
47 data string
48 children *avl.Tree // name -> *node; nil for a plain file
49 mtime int64
50 uid string
51 gid string
52 muid string
53}
54
55// New returns an empty file server owned by uid, with its root created at
56// block height now.
57func New(uid string, now int64) *FS {
58 fs := &FS{uid: uid}
59 fs.root = fs.newNode("/", ninep.DirPerm, now)
60 return fs
61}
62
63// Root returns the server's root directory.
64func (fs *FS) Root() ninep.File { return fs.root }
65
66// RootMutable returns the root as a Mutable. Only call it from the realm that
67// owns this FS.
68func (fs *FS) RootMutable() ninep.Mutable { return fs.root }
69
70// Uid returns the owner this server stamps on new files.
71func (fs *FS) Uid() string { return fs.uid }
72
73func (fs *FS) newNode(name string, perm ninep.Perm, now int64) *node {
74 fs.next++
75 qt := ninep.QTFILE
76 var kids *avl.Tree
77 if perm.IsDir() {
78 qt = ninep.QTDIR
79 kids = avl.NewTree()
80 }
81 return &node{
82 fs: fs,
83 name: name,
84 perm: perm,
85 qid: ninep.Qid{Type: qt, Version: 0, Path: fs.next},
86 children: kids,
87 mtime: now,
88 uid: fs.uid,
89 gid: fs.uid,
90 muid: fs.uid,
91 }
92}
93
94// Stat implements ninep.File.
95func (n *node) Stat() ninep.Stat {
96 length := int64(len(n.data))
97 if n.perm.IsDir() {
98 length = 0 // 9P reports zero for a directory
99 }
100 return ninep.Stat{
101 Qid: n.qid,
102 Mode: n.perm,
103 Mtime: n.mtime,
104 Length: length,
105 Name: n.name,
106 Uid: n.uid,
107 Gid: n.gid,
108 Muid: n.muid,
109 }
110}
111
112// Walk implements ninep.File.
113func (n *node) Walk(name string) (ninep.File, error) {
114 if !n.perm.IsDir() {
115 return nil, ninep.ErrNotDir
116 }
117 if !ninep.ValidName(name) {
118 return nil, ninep.ErrBadName
119 }
120 v := n.children.Get(name) // avl.Get returns ONE value in gno
121 if v == nil {
122 return nil, ninep.ErrNotExist
123 }
124 return v.(*node), nil
125}
126
127// Read implements ninep.File.
128func (n *node) Read(off, count int64) (string, error) {
129 if n.perm.IsDir() {
130 return "", ninep.ErrIsDir
131 }
132 return ninep.Slice(n.data, off, count), nil
133}
134
135// ReadDir implements ninep.File.
136func (n *node) ReadDir() ([]ninep.Stat, error) {
137 if !n.perm.IsDir() {
138 return nil, ninep.ErrNotDir
139 }
140 out := []ninep.Stat{}
141 n.children.Iterate("", "", func(_ string, v any) bool {
142 out = append(out, v.(*node).Stat())
143 return false
144 })
145 return out, nil
146}
147
148// Create implements ninep.Mutable.
149func (n *node) Create(name string, perm ninep.Perm, now int64) (ninep.File, error) {
150 if !n.perm.IsDir() {
151 return nil, ninep.ErrNotDir
152 }
153 if !ninep.ValidName(name) {
154 return nil, ninep.ErrBadName
155 }
156 if n.children.Has(name) {
157 return nil, ninep.ErrExist
158 }
159 child := n.fs.newNode(name, perm, now)
160 n.children.Set(name, child)
161 n.touch(now)
162 return child, nil
163}
164
165// Remove implements ninep.Mutable. A non-empty directory is refused, as in
166// Plan 9.
167func (n *node) Remove(name string) error {
168 if !n.perm.IsDir() {
169 return ninep.ErrNotDir
170 }
171 v := n.children.Get(name)
172 if v == nil {
173 return ninep.ErrNotExist
174 }
175 child := v.(*node)
176 if child.perm.IsDir() && child.children.Size() > 0 {
177 return ninep.ErrNotEmpty
178 }
179 n.children.Remove(name)
180 n.touch(n.mtime)
181 return nil
182}
183
184// Write implements ninep.Mutable. Writing past the end extends the file with
185// NUL bytes, as a 9P server does.
186func (n *node) Write(off int64, data string, now int64) (int64, error) {
187 if n.perm.IsDir() {
188 return 0, ninep.ErrIsDir
189 }
190 if off < 0 {
191 off = 0
192 }
193 if n.perm&ninep.DMAPPEND != 0 {
194 off = int64(len(n.data))
195 }
196 cur := n.data
197 if off > int64(len(cur)) {
198 cur += strings.Repeat("\x00", int(off)-len(cur))
199 }
200 end := off + int64(len(data))
201 tail := ""
202 if end < int64(len(cur)) {
203 tail = cur[end:]
204 }
205 n.data = cur[:off] + data + tail
206 n.touch(now)
207 return int64(len(data)), nil
208}
209
210// Truncate implements ninep.Mutable.
211func (n *node) Truncate(size int64, now int64) error {
212 if n.perm.IsDir() {
213 return ninep.ErrIsDir
214 }
215 if size < 0 {
216 size = 0
217 }
218 switch {
219 case size < int64(len(n.data)):
220 n.data = n.data[:size]
221 case size > int64(len(n.data)):
222 n.data += strings.Repeat("\x00", int(size)-len(n.data))
223 }
224 n.touch(now)
225 return nil
226}
227
228// touch bumps the qid version and the mtime. A client that kept a qid can tell
229// the file changed without reading it, which is the whole point of Qid.Version.
230func (n *node) touch(now int64) {
231 n.qid.Version++
232 n.mtime = now
233 n.muid = n.fs.uid
234}
235
236// MkdirAll creates p and every missing parent, and returns the leaf. An
237// existing directory is not an error; an existing plain file on the path is.
238func (fs *FS) MkdirAll(p string, now int64) (ninep.File, error) {
239 cur := fs.root
240 for _, e := range ninep.Elems(p) {
241 v := cur.children.Get(e)
242 if v == nil {
243 f, err := cur.Create(e, ninep.DirPerm, now)
244 if err != nil {
245 return nil, err
246 }
247 cur = f.(*node)
248 continue
249 }
250 cur = v.(*node)
251 if !cur.perm.IsDir() {
252 return nil, ninep.ErrNotDir
253 }
254 }
255 return cur, nil
256}
257
258// WriteFile creates or replaces the file at p, creating parent directories as
259// needed. It is the seeding helper a realm wants at init.
260func (fs *FS) WriteFile(p, data string, now int64) error {
261 dir, err := fs.MkdirAll(ninep.Dir(p), now)
262 if err != nil {
263 return err
264 }
265 name := ninep.Base(p)
266 d := dir.(*node)
267 v := d.children.Get(name)
268 if v == nil {
269 f, err := d.Create(name, ninep.FilePerm, now)
270 if err != nil {
271 return err
272 }
273 _, err = f.(*node).Write(0, data, now)
274 return err
275 }
276 target := v.(*node)
277 if err := target.Truncate(0, now); err != nil {
278 return err
279 }
280 _, err = target.Write(0, data, now)
281 return err
282}