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

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}