ninep.gno
9.69 Kb · 307 lines
1// Package ninep is a Plan 9 shaped file abstraction for gno.
2//
3// It implements the SEMANTICS of 9P2000, not its wire format: there is no
4// socket on a chain, the VM call is the transport. What it keeps is the part
5// that made 9P useful, namely that every resource answers the same four
6// questions (stat, walk, read, readdir), so a client written today can browse
7// a file server deployed tomorrow.
8//
9// One interface, not two. 9P reads a directory with the same Tread it uses for
10// a file, so splitting File from Dir would be less faithful, and a single
11// interface means no type assertion across a realm boundary.
12//
13// Deliberate divergences from 9P2000, all of them forced:
14//
15// - Data is a string, not []byte. Every consumer on this chain is text and
16// Render returns a string.
17// - Mtime is a block height, not a wall clock. It is the only clock that
18// every validating node agrees on.
19// - There is no open/clunk pair. Without a session there are no fids, so
20// Walk returns the file itself and nothing has to be released.
21// - File is READ-ONLY. A crossing method would mint the caller's realm frame
22// for the callee, which is a confused-deputy hazard (compare r/gov/dao's
23// Executor, and p/nt/grc20's deliberate refusal to make Teller crossing).
24// Mutation lives in Mutable, which is only safe on a tree your own realm
25// owns.
26//
27// See gno.land/p/moul/x/plan9/ns for the namespace that binds these trees
28// together, and gno.land/p/moul/x/plan9/memfs for the reference server.
29//
30// NOTICE. Plan 9 from Bell Labs is the work of the Computing Science Research
31// Center at Bell Labs; the name and the marks are theirs, and the copyright is
32// held by the Plan 9 Foundation (https://p9f.org). This package is not
33// affiliated with, endorsed by, or sponsored by them, and contains no Plan 9
34// code: it borrows the vocabulary so that the design reads without a glossary,
35// and it is an homage, asking what that ecosystem's spirit looks like on a
36// chain. Full attribution: NOTICE.md at the root of moul/gno-contracts.
37package ninep
38
39import (
40 "errors"
41 "path"
42 "strconv"
43 "strings"
44)
45
46// Qid type bits, as in 9P2000.
47const (
48 QTFILE uint8 = 0x00 // a plain file
49 QTTMP uint8 = 0x04 // not archived
50 QTAUTH uint8 = 0x08 // authentication file
51 QTMOUNT uint8 = 0x10 // mounted channel
52 QTEXCL uint8 = 0x20 // exclusive use
53 QTAPPEND uint8 = 0x40 // append only
54 QTDIR uint8 = 0x80 // a directory
55)
56
57// Perm holds the 9P mode word: the low nine bits are rwx for owner, group and
58// other, the high bits are the DM* kind flags.
59type Perm uint32
60
61// Mode bits, as in 9P2000.
62const (
63 DMTMP Perm = 0x04000000
64 DMAUTH Perm = 0x08000000
65 DMMOUNT Perm = 0x10000000
66 DMEXCL Perm = 0x20000000
67 DMAPPEND Perm = 0x40000000
68 DMDIR Perm = 0x80000000
69
70 PermMask Perm = 0777 // the rwxrwxrwx bits
71
72 // Conventional defaults, matching what Plan 9's ramfs hands out.
73 DirPerm Perm = DMDIR | 0755
74 FilePerm Perm = 0644
75)
76
77// IsDir reports whether the mode marks a directory.
78func (p Perm) IsDir() bool { return p&DMDIR != 0 }
79
80// String renders the mode the way ls -l does: a kind letter then nine rwx
81// bits. The kind letter is 'd' for a directory, 'a' for append-only, 'l' for
82// exclusive-use, '-' otherwise.
83func (p Perm) String() string {
84 var b strings.Builder
85 switch {
86 case p&DMDIR != 0:
87 b.WriteByte('d')
88 case p&DMAPPEND != 0:
89 b.WriteByte('a')
90 case p&DMEXCL != 0:
91 b.WriteByte('l')
92 default:
93 b.WriteByte('-')
94 }
95 const rwx = "rwxrwxrwx"
96 for i := 0; i < 9; i++ {
97 if p&(1<<uint(8-i)) != 0 {
98 b.WriteByte(rwx[i])
99 } else {
100 b.WriteByte('-')
101 }
102 }
103 return b.String()
104}
105
106// Qid is the server's unique handle for a file. Path identifies the file
107// within one server for its whole lifetime; Version increments on every write,
108// so a client can tell "same file, changed" from "different file" without
109// reading either.
110type Qid struct {
111 Type uint8
112 Version uint32
113 Path uint64
114}
115
116// IsDir reports whether the qid marks a directory.
117func (q Qid) IsDir() bool { return q.Type&QTDIR != 0 }
118
119// String renders the qid as Plan 9 does, "(path version type)", with the path
120// in hex.
121func (q Qid) String() string {
122 kind := "f"
123 if q.IsDir() {
124 kind = "d"
125 }
126 return "(" + strconv.FormatUint(q.Path, 16) + " " +
127 strconv.FormatUint(uint64(q.Version), 10) + " " + kind + ")"
128}
129
130// Stat is 9P's directory entry, minus the fields that only mean something on a
131// wire (type, dev) or on a host clock (atime).
132type Stat struct {
133 Qid Qid
134 Mode Perm
135 Mtime int64 // block height of the last write
136 Length int64 // in bytes; zero for a directory, as in 9P
137 Name string
138 Uid string // owner; a bech32 address, or a well-known name
139 Gid string // group
140 Muid string // last writer
141}
142
143// IsDir reports whether the entry is a directory.
144func (s Stat) IsDir() bool { return s.Mode.IsDir() }
145
146// Line renders the entry the way ls -l does.
147func (s Stat) Line() string {
148 return s.Mode.String() + " " + pad(s.Uid, 12) + " " + pad(s.Gid, 8) + " " +
149 lpad(strconv.FormatInt(s.Length, 10), 7) + " " + s.Name
150}
151
152// pad right-pads to n runes. ufmt has no width flags in gno, so padding is by
153// hand everywhere in this suite.
154func pad(s string, n int) string {
155 for len(s) < n {
156 s += " "
157 }
158 return s
159}
160
161func lpad(s string, n int) string {
162 for len(s) < n {
163 s = " " + s
164 }
165 return s
166}
167
168// File is a 9P file server's whole read surface. Every method must be free of
169// side effects: a File is routinely reached across a realm boundary, where the
170// running frame belongs to the CALLER, so mutating anything here would be both
171// a VM error and a confused deputy.
172type File interface {
173 // Stat returns the entry for this file.
174 Stat() Stat
175 // Walk resolves exactly one path element. It returns ErrNotDir on a
176 // plain file and ErrNotExist when the name is absent. It never sees
177 // "." or "..": both are removed lexically before resolution starts.
178 Walk(name string) (File, error)
179 // Read returns at most count bytes starting at off. A negative count
180 // means "to the end". It returns ErrIsDir on a directory.
181 Read(off, count int64) (string, error)
182 // ReadDir returns the directory's entries in a deterministic order. It
183 // returns ErrNotDir on a plain file.
184 ReadDir() ([]Stat, error)
185}
186
187// Mutable is the write half, kept out of File on purpose.
188//
189// It is NOT safe across a realm boundary: a non-crossing method runs in the
190// caller's frame, so a foreign realm calling these would be trying to mutate
191// objects it does not own. Only call Mutable on a tree your own realm created.
192// The caller supplies now (a block height) rather than the tree reading the
193// chain itself, so the same code is testable off chain.
194type Mutable interface {
195 File
196 Create(name string, perm Perm, now int64) (File, error)
197 Remove(name string) error
198 Write(off int64, data string, now int64) (int64, error)
199 Truncate(size int64, now int64) error
200}
201
202// Plan 9 error strings, kept lowercase and verbatim where they exist.
203var (
204 ErrNotExist = errors.New("file does not exist")
205 ErrNotDir = errors.New("not a directory")
206 ErrIsDir = errors.New("is a directory")
207 ErrExist = errors.New("file already exists")
208 ErrPerm = errors.New("permission denied")
209 ErrNoCreate = errors.New("create prohibited")
210 ErrReadOnly = errors.New("read-only file server")
211 ErrBadName = errors.New("bad character in file name")
212 ErrNotEmpty = errors.New("directory not empty")
213 ErrTooDeep = errors.New("path too deep")
214)
215
216// MaxDepth bounds a walk. Resolution costs one cross-realm call per element
217// per union member, so depth is capped rather than trusted.
218const MaxDepth = 32
219
220// ValidName reports whether name is usable as a single path element. Plan 9
221// rejects the empty name, "." and "..", and any name containing a slash.
222func ValidName(name string) bool {
223 if name == "" || name == "." || name == ".." {
224 return false
225 }
226 return !strings.Contains(name, "/")
227}
228
229// Clean returns p as a cleaned absolute path. "." and ".." are resolved
230// lexically, before any server sees them, which is what makes ".." undo the
231// name you typed rather than the directory you landed in (see "Lexical File
232// Names in Plan 9").
233func Clean(p string) string {
234 if !strings.HasPrefix(p, "/") {
235 p = "/" + p
236 }
237 return path.Clean(p)
238}
239
240// Abs resolves p against cwd, then cleans it.
241func Abs(cwd, p string) string {
242 if p == "" {
243 return Clean(cwd)
244 }
245 if strings.HasPrefix(p, "/") {
246 return Clean(p)
247 }
248 if cwd == "" {
249 cwd = "/"
250 }
251 return Clean(cwd + "/" + p)
252}
253
254// Elems splits a cleaned absolute path into its elements. The root yields nil.
255func Elems(p string) []string {
256 p = Clean(p)
257 if p == "/" {
258 return nil
259 }
260 return strings.Split(p[1:], "/")
261}
262
263// Base returns the last element of p, or "/" for the root.
264func Base(p string) string { return path.Base(Clean(p)) }
265
266// Dir returns p's parent.
267func Dir(p string) string { return path.Dir(Clean(p)) }
268
269// Join appends name to dir.
270func Join(dir, name string) string { return Clean(dir + "/" + name) }
271
272// Walk resolves elems from f, one element at a time. It is the plain,
273// namespace-free walk: no binds, no unions. Use ns.Ns for those.
274func Walk(f File, elems []string) (File, error) {
275 if len(elems) > MaxDepth {
276 return nil, ErrTooDeep
277 }
278 for _, e := range elems {
279 next, err := f.Walk(e)
280 if err != nil {
281 return nil, err
282 }
283 f = next
284 }
285 return f, nil
286}
287
288// ReadAll reads a whole file.
289func ReadAll(f File) (string, error) { return f.Read(0, -1) }
290
291// Slice applies 9P's read window to s: at most count bytes from off, with a
292// negative count meaning "to the end". An offset past the end reads empty,
293// which is what makes a read loop terminate rather than fail.
294func Slice(s string, off, count int64) string {
295 if off < 0 {
296 off = 0
297 }
298 n := int64(len(s))
299 if off >= n {
300 return ""
301 }
302 end := n
303 if count >= 0 && off+count < n {
304 end = off + count
305 }
306 return s[off:end]
307}