ns.gno
13.38 Kb · 489 lines
1// Package ns is a Plan 9 namespace: a private, mutable mount table over
2// gno.land/p/moul/x/plan9/ninep file trees.
3//
4// This is the idea the rest of the suite exists for. Plan 9's leverage does
5// not come from "everything is a file", it comes from every process owning its
6// own name-to-resource mapping, so a name can be REPLACED and any service can
7// be composed, shadowed, sandboxed or mocked without the program knowing. Gno
8// has one global tree and no way to hold a view of it; an Ns is that view.
9//
10// Bind flags follow bind(2): MREPL replaces, MBEFORE splices the new directory
11// in front of the old one, MAFTER behind it, and MCREATE (OR'd onto any of
12// them) marks the union member that new files are created in.
13//
14// Two Plan 9 properties are load-bearing and reproduced deliberately:
15//
16// - A union is TOP LEVEL ONLY. Binding /a onto /b unions the two directories
17// at /b; /b/c resolves in whichever member won the walk, and is not itself
18// a union unless something is bound there too.
19// - A bind captures a CHANNEL, not a name. The source is resolved once, at
20// bind time, and the resulting file is what is stored. Rebinding the
21// source afterwards does not retroactively move the target.
22//
23// ".." is removed lexically before resolution starts and never reaches a
24// server, per "Lexical File Names in Plan 9, or Getting Dot-Dot Right".
25//
26// NOTICE. Plan 9 from Bell Labs is the work of the Computing Science Research
27// Center at Bell Labs; the name and the marks are theirs, and the copyright is
28// held by the Plan 9 Foundation (https://p9f.org). This package is not
29// affiliated with, endorsed by, or sponsored by them, and contains no Plan 9
30// code: it borrows the vocabulary so that the design reads without a glossary,
31// and it is an homage, asking what that ecosystem's spirit looks like on a
32// chain. Full attribution: NOTICE.md at the root of moul/gno-contracts.
33package ns
34
35import (
36 "errors"
37 "strings"
38
39 "gno.land/p/nt/avl/v0"
40
41 ninep "gno.land/p/moul/x/plan9/ninep/v0"
42)
43
44// Flag is a bind(2) mount flag. The low two bits pick the mode; MCREATE is
45// OR'd onto it.
46type Flag uint32
47
48// Mount flags, with Plan 9's values.
49const (
50 MREPL Flag = 0x0000 // replace the old file with the new one
51 MBEFORE Flag = 0x0001 // union, new directory first
52 MAFTER Flag = 0x0002 // union, new directory last
53 MCREATE Flag = 0x0004 // creates in this union go here
54)
55
56func (f Flag) mode() Flag { return f & 0x0003 }
57func (f Flag) creates() bool { return f&MCREATE != 0 }
58
59// String renders the flag as the command-line letters bind(1) uses.
60func (f Flag) String() string {
61 s := ""
62 switch f.mode() {
63 case MBEFORE:
64 s = "-b"
65 case MAFTER:
66 s = "-a"
67 }
68 if f.creates() {
69 if s == "" {
70 s = "-c"
71 } else {
72 s += "c"
73 }
74 }
75 return s
76}
77
78// Limits. Resolution costs one call per element per union member, and those
79// calls may cross a realm boundary, so both are capped rather than trusted.
80const (
81 MaxUnion = 8 // members in one union
82 MaxOps = 64
83)
84
85var (
86 // ErrNoRoot means the namespace has nothing bound at "/".
87 ErrNoRoot = errors.New("no root in namespace")
88 // ErrWideUnion means a bind would exceed MaxUnion members.
89 ErrWideUnion = errors.New("union too wide")
90 // ErrTooManyOps means the namespace has reached MaxOps bindings.
91 ErrTooManyOps = errors.New("too many namespace operations")
92 // ErrNotBound means unmount was asked to remove something that is not
93 // bound at that name.
94 ErrNotBound = errors.New("not bound")
95)
96
97// member is one element of a union. A nil file is the placeholder standing for
98// "whatever walking to this name would have found", which is how MBEFORE and
99// MAFTER keep the original directory in the union.
100type member struct {
101 file ninep.File
102 create bool
103}
104
105type entry struct {
106 members []member
107}
108
109// Op records one bind or mount, in application order, so that String can print
110// the namespace the way Plan 9's ns(1) does.
111type Op struct {
112 Verb string // "bind" or "mount"
113 Flag Flag
114 Source string
115 Target string
116}
117
118// Ns is one namespace: a mount table, a working directory, and the ordered
119// list of operations that produced them.
120type Ns struct {
121 table *avl.Tree // cleaned target path -> *entry
122 ops []Op
123 cwd string
124}
125
126// New returns a namespace whose root is the given file. The root is the one
127// binding that cannot be expressed as a bind of something else, so it is
128// installed directly.
129func New(root ninep.File) *Ns {
130 n := &Ns{table: avl.NewTree(), cwd: "/"}
131 n.table.Set("/", &entry{members: []member{{file: root, create: true}}})
132 return n
133}
134
135// Cwd returns the working directory.
136func (n *Ns) Cwd() string { return n.cwd }
137
138// Cd sets the working directory, which must resolve to a directory.
139func (n *Ns) Cd(p string) error {
140 abs := n.Abs(p)
141 st, err := n.Stat(abs)
142 if err != nil {
143 return err
144 }
145 if !st.IsDir() {
146 return ninep.ErrNotDir
147 }
148 n.cwd = abs
149 return nil
150}
151
152// Abs resolves p against the working directory.
153func (n *Ns) Abs(p string) string { return ninep.Abs(n.cwd, p) }
154
155func (n *Ns) entry(p string) *entry {
156 v := n.table.Get(p)
157 if v == nil {
158 return nil
159 }
160 return v.(*entry)
161}
162
163// expand substitutes the walk result for the placeholder member.
164func expand(ms []member, under []member) []member {
165 out := []member{}
166 for _, m := range ms {
167 if m.file == nil {
168 out = append(out, under...)
169 continue
170 }
171 out = append(out, m)
172 }
173 return out
174}
175
176// resolve walks p and returns the union found there, richest form: members
177// still carry their create bit, and bound reports whether a mount entry
178// applied at the final element.
179func (n *Ns) resolve(p string) ([]member, error) {
180 root := n.entry("/")
181 if root == nil {
182 return nil, ErrNoRoot
183 }
184 cur := expand(root.members, nil)
185 if len(cur) == 0 {
186 return nil, ErrNoRoot
187 }
188 elems := ninep.Elems(ninep.Clean(p))
189 if len(elems) > ninep.MaxDepth {
190 return nil, ninep.ErrTooDeep
191 }
192 prefix := ""
193 for _, e := range elems {
194 // First match wins. Walking INTO a union does not produce a union
195 // of the members' subdirectories: in Plan 9 a union is top level
196 // only, and a walk returns one channel.
197 walked := []member{}
198 for _, m := range cur {
199 f, err := m.file.Walk(e)
200 if err == nil {
201 walked = append(walked, member{file: f})
202 break
203 }
204 }
205 prefix += "/" + e
206 next := walked
207 if ent := n.entry(prefix); ent != nil {
208 next = expand(ent.members, walked)
209 }
210 if len(next) == 0 {
211 return nil, ninep.ErrNotExist
212 }
213 cur = next
214 }
215 return cur, nil
216}
217
218// Resolve returns every union member visible at p, in search order.
219func (n *Ns) Resolve(p string) ([]ninep.File, error) {
220 ms, err := n.resolve(n.Abs(p))
221 if err != nil {
222 return nil, err
223 }
224 out := []ninep.File{}
225 for _, m := range ms {
226 out = append(out, m.file)
227 }
228 return out, nil
229}
230
231// Open returns the file a name resolves to: the first member of its union,
232// which is the one a walk through this name would reach.
233func (n *Ns) Open(p string) (ninep.File, error) {
234 fs, err := n.Resolve(p)
235 if err != nil {
236 return nil, err
237 }
238 return fs[0], nil
239}
240
241// Stat returns the entry for p, with the name replaced by the last element of
242// the path as asked for, so that a union member's own name never leaks.
243func (n *Ns) Stat(p string) (ninep.Stat, error) {
244 f, err := n.Open(p)
245 if err != nil {
246 return ninep.Stat{}, err
247 }
248 st := f.Stat()
249 st.Name = ninep.Base(n.Abs(p))
250 return st, nil
251}
252
253// ReadFile reads the whole file at p.
254func (n *Ns) ReadFile(p string) (string, error) {
255 f, err := n.Open(p)
256 if err != nil {
257 return "", err
258 }
259 return ninep.ReadAll(f)
260}
261
262// ReadDir lists p. A union directory is the CONCATENATION of its members'
263// contents, as in Plan 9, so duplicate names can appear and shadowing is
264// visible. Pass unique to collapse them first-wins instead, which is the set
265// of names a walk can actually reach.
266func (n *Ns) ReadDir(p string, unique bool) ([]ninep.Stat, error) {
267 ms, err := n.resolve(n.Abs(p))
268 if err != nil {
269 return nil, err
270 }
271 if len(ms) == 1 && !ms[0].file.Stat().IsDir() {
272 return nil, ninep.ErrNotDir
273 }
274 out := []ninep.Stat{}
275 seen := map[string]bool{}
276 for _, m := range ms {
277 ents, err := m.file.ReadDir()
278 if err != nil {
279 continue // a non-directory member contributes nothing
280 }
281 for _, e := range ents {
282 if unique {
283 if seen[e.Name] {
284 continue
285 }
286 seen[e.Name] = true
287 }
288 out = append(out, e)
289 }
290 }
291 return out, nil
292}
293
294// CreateTarget returns the directory that a create at p should happen in.
295//
296// For a plain directory that is just the directory. For a union it is the
297// first member carrying MCREATE, and if no member has it, creation is refused,
298// which is bind(2)'s rule.
299func (n *Ns) CreateTarget(p string) (ninep.Mutable, error) {
300 abs := n.Abs(p)
301 ms, err := n.resolve(abs)
302 if err != nil {
303 return nil, err
304 }
305 isUnion := n.entry(abs) != nil && len(ms) > 1
306 for _, m := range ms {
307 if isUnion && !m.create {
308 continue
309 }
310 mu, ok := m.file.(ninep.Mutable)
311 if !ok {
312 continue // a read-only server, for instance a mounted realm
313 }
314 if !mu.Stat().IsDir() {
315 continue
316 }
317 return mu, nil
318 }
319 if isUnion {
320 return nil, ninep.ErrNoCreate
321 }
322 return nil, ninep.ErrPerm
323}
324
325// Bind makes source visible at target, as bind(1) does. Both names are
326// resolved in THIS namespace, and the source is resolved once: what is stored
327// is the file it names today, not the name.
328func (n *Ns) Bind(source, target string, flag Flag) error {
329 return n.BindVerb("bind", source, target, flag)
330}
331
332// BindVerb is Bind with the verb that String should print. Plan 9 spells the
333// same operation "bind" or "mount" depending on whether the source is a name
334// or a channel, and ns(1) echoes back whichever was used.
335func (n *Ns) BindVerb(verb, source, target string, flag Flag) error {
336 ms, err := n.resolve(n.Abs(source))
337 if err != nil {
338 return err
339 }
340 files := []ninep.File{}
341 for _, m := range ms {
342 files = append(files, m.file)
343 }
344 return n.graft(verb, source, target, flag, files)
345}
346
347// Mount grafts a file tree that has no name in this namespace yet, which is
348// how a service posted by another realm gets in. source is a label, used only
349// when printing the namespace.
350func (n *Ns) Mount(f ninep.File, source, target string, flag Flag) error {
351 return n.graft("mount", source, target, flag, []ninep.File{f})
352}
353
354func (n *Ns) graft(verb, source, target string, flag Flag, files []ninep.File) error {
355 if len(n.ops) >= MaxOps {
356 return ErrTooManyOps
357 }
358 abs := n.Abs(target)
359 // bind(1) requires the target to exist: you can only rebind a name that
360 // already resolves to something.
361 if _, err := n.resolve(abs); err != nil {
362 return err
363 }
364 add := []member{}
365 for _, f := range files {
366 add = append(add, member{file: f, create: flag.creates()})
367 }
368
369 ent := n.entry(abs)
370 var members []member
371 switch {
372 case ent == nil:
373 // The name has never been bound: its current contents are the
374 // placeholder. It does NOT carry MCREATE, which is why creating
375 // in a freshly unioned directory is refused until some member is
376 // bound with -c.
377 members = []member{{file: nil}}
378 default:
379 members = ent.members
380 }
381 switch flag.mode() {
382 case MBEFORE:
383 members = append(add, members...)
384 case MAFTER:
385 members = append(members, add...)
386 default: // MREPL: the old file is gone, not unioned
387 members = add
388 }
389 if len(members) > MaxUnion {
390 return ErrWideUnion
391 }
392 n.table.Set(abs, &entry{members: members})
393 n.ops = append(n.ops, Op{Verb: verb, Flag: flag, Source: source, Target: abs})
394 return nil
395}
396
397// Unmount undoes bindings at target. With an empty source it removes every
398// binding there, restoring the name to whatever it resolved to originally.
399func (n *Ns) Unmount(source, target string) error {
400 abs := n.Abs(target)
401 ent := n.entry(abs)
402 if ent == nil {
403 return ErrNotBound
404 }
405 if source == "" {
406 n.table.Remove(abs)
407 n.dropOps(abs, "")
408 return nil
409 }
410 ms, err := n.resolve(n.Abs(source))
411 if err != nil {
412 return err
413 }
414 kept := []member{}
415 removed := false
416 for _, m := range ent.members {
417 drop := false
418 for _, s := range ms {
419 if m.file != nil && m.file == s.file {
420 drop = true
421 break
422 }
423 }
424 if drop {
425 removed = true
426 continue
427 }
428 kept = append(kept, m)
429 }
430 if !removed {
431 return ErrNotBound
432 }
433 if onlyPlaceholders(kept) {
434 // Nothing but "whatever was here originally" is left, so the entry
435 // is a no-op: drop it rather than leave a phantom binding behind.
436 n.table.Remove(abs)
437 } else {
438 n.table.Set(abs, &entry{members: kept})
439 }
440 n.dropOps(abs, source)
441 return nil
442}
443
444func onlyPlaceholders(ms []member) bool {
445 for _, m := range ms {
446 if m.file != nil {
447 return false
448 }
449 }
450 return true
451}
452
453func (n *Ns) dropOps(target, source string) {
454 kept := []Op{}
455 for _, op := range n.ops {
456 if op.Target == target && (source == "" || op.Source == source) {
457 continue
458 }
459 kept = append(kept, op)
460 }
461 n.ops = kept
462}
463
464// Ops returns the bindings in application order.
465func (n *Ns) Ops() []Op { return n.ops }
466
467// String prints the namespace the way Plan 9's ns(1) does: one line per
468// binding, in the order they were applied, then the working directory.
469func (n *Ns) String() string {
470 var b strings.Builder
471 for _, op := range n.ops {
472 b.WriteString(op.Verb)
473 if f := op.Flag.String(); f != "" {
474 b.WriteString(" " + f)
475 }
476 b.WriteString(" " + quote(op.Source) + " " + quote(op.Target) + "\n")
477 }
478 b.WriteString("cd " + quote(n.cwd) + "\n")
479 return b.String()
480}
481
482// quote applies rc quoting: single quotes when the word contains a space or a
483// quote, with an embedded quote doubled.
484func quote(s string) string {
485 if s != "" && !strings.ContainsAny(s, " \t'") {
486 return s
487 }
488 return "'" + strings.ReplaceAll(s, "'", "''") + "'"
489}