const QTFILE, QTTMP, QTAUTH, QTMOUNT, QTEXCL, QTAPPEND, QTDIR
Qid type bits, as in 9P2000.
Package ninep is a Plan 9 shaped file abstraction for gno.
gno.land/p/moul/x/plan9/ninep/v0A Plan 9 shaped file abstraction for gno: File, Mutable, Qid, Stat,
Perm, the 9P error set, and lexical path handling.
1import ninep "gno.land/p/moul/x/plan9/ninep/v0"
2
3type File interface {
4 Stat() Stat
5 Walk(name string) (File, error) // exactly one element
6 Read(off, count int64) (string, error)
7 ReadDir() ([]Stat, error)
8}
This implements the semantics of 9P2000, not its wire format. There is no socket on a chain: the VM call is the transport. What survives the translation is the part that made 9P useful, namely that every resource answers the same four questions, so a client written today can browse a file server deployed tomorrow.
One interface, not two. 9P reads a directory with the same Tread it uses
for a file, so splitting File from Dir would be less faithful, and a single
interface means no type assertion across a realm boundary.
File is read-only, on purpose. A crossing write method would mint the
caller's realm frame for the callee, which is the confused-deputy shape that
r/gov/dao's Executor relies on deliberately and p/nt/grc20's Teller
refuses deliberately. So mutation lives in a separate Mutable, which is only
safe on a tree your own realm owns. That is what makes it safe to hand a File
to a stranger's namespace.
Deliberate divergences, each one forced:
string, not []byte. Every consumer on this chain is text and
Render returns a string.Mtime is a block height. It is the only clock every validating node
agrees on.open/clunk. Without a session there are no fids, so Walk
returns the file itself and nothing has to be released... never reaches a server. Clean resolves it lexically first, per
Lexical File Names in Plan 9, so ..
undoes the name you typed rather than the directory you landed in.MaxDepth caps a walk at 32 elements. Resolution can cost one cross-realm
call per element, so depth is bounded rather than trusted.Used by memfs (a RAM server), synfs (a
computed server), ns (namespaces) and
rc (the shell). Design and analysis:
moul/gno-contracts#136.
Not affiliated with Plan 9. Plan 9 from Bell Labs is the work of the Computing Science Research Center at Bell Labs; the name and the marks are theirs, and the copyright is held by the Plan 9 Foundation. This package borrows the vocabulary and none of the code: it is an independent homage, asking what that ecosystem's spirit looks like on a chain. Full attribution: NOTICE.
Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.
🧪 Highly experimental — potentially vibe-coded. Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: DISCLAIMER.
Package ninep is a Plan 9 shaped file abstraction for gno.
It implements the SEMANTICS of 9P2000, not its wire format: there is no socket on a chain, the VM call is the transport. What it keeps is the part that made 9P useful, namely that every resource answers the same four questions (stat, walk, read, readdir), so a client written today can browse a file server deployed tomorrow.
One interface, not two. 9P reads a directory with the same Tread it uses for a file, so splitting File from Dir would be less faithful, and a single interface means no type assertion across a realm boundary.
Deliberate divergences from 9P2000, all of them forced:
See gno.land/p/moul/x/plan9/ns for the namespace that binds these trees together, and gno.land/p/moul/x/plan9/memfs for the reference server.
NOTICE. Plan 9 from Bell Labs is the work of the Computing Science Research Center at Bell Labs; the name and the marks are theirs, and the copyright is held by the Plan 9 Foundation (https://p9f.org). This package is not affiliated with, endorsed by, or sponsored by them, and contains no Plan 9 code: it borrows the vocabulary so that the design reads without a glossary, and it is an homage, asking what that ecosystem's spirit looks like on a chain. Full attribution: NOTICE.md at the root of moul/gno-contracts.
Qid type bits, as in 9P2000.
MaxDepth bounds a walk. Resolution costs one cross-realm call per element per union member, so depth is capped rather than trusted.
1const (
2 DMTMP Perm = 0x04000000
3 DMAUTH Perm = 0x08000000
4 DMMOUNT Perm = 0x10000000
5 DMEXCL Perm = 0x20000000
6 DMAPPEND Perm = 0x40000000
7 DMDIR Perm = 0x80000000
8
9 PermMask Perm = 0777 // the rwxrwxrwx bits
10
11 // Conventional defaults, matching what Plan 9's ramfs hands out.
12 DirPerm Perm = DMDIR | 0755
13 FilePerm Perm = 0644
14)Mode bits, as in 9P2000.
1var (
2 ErrNotExist = errors.New("file does not exist")
3 ErrNotDir = errors.New("not a directory")
4 ErrIsDir = errors.New("is a directory")
5 ErrExist = errors.New("file already exists")
6 ErrPerm = errors.New("permission denied")
7 ErrNoCreate = errors.New("create prohibited")
8 ErrReadOnly = errors.New("read-only file server")
9 ErrBadName = errors.New("bad character in file name")
10 ErrNotEmpty = errors.New("directory not empty")
11 ErrTooDeep = errors.New("path too deep")
12)Plan 9 error strings, kept lowercase and verbatim where they exist.
Abs resolves p against cwd, then cleans it.
Base returns the last element of p, or "/" for the root.
Clean returns p as a cleaned absolute path. "." and ".." are resolved lexically, before any server sees them, which is what makes ".." undo the name you typed rather than the directory you landed in (see "Lexical File Names in Plan 9").
Dir returns p's parent.
Elems splits a cleaned absolute path into its elements. The root yields nil.
Join appends name to dir.
ReadAll reads a whole file.
Slice applies 9P's read window to s: at most count bytes from off, with a negative count meaning "to the end". An offset past the end reads empty, which is what makes a read loop terminate rather than fail.
ValidName reports whether name is usable as a single path element. Plan 9 rejects the empty name, "." and "..", and any name containing a slash.
Walk resolves elems from f, one element at a time. It is the plain, namespace-free walk: no binds, no unions. Use ns.Ns for those.
1type File interface {
2 // Stat returns the entry for this file.
3 Stat() Stat
4 // Walk resolves exactly one path element. It returns ErrNotDir on a
5 // plain file and ErrNotExist when the name is absent. It never sees
6 // "." or "..": both are removed lexically before resolution starts.
7 Walk(name string) (File, error)
8 // Read returns at most count bytes starting at off. A negative count
9 // means "to the end". It returns ErrIsDir on a directory.
10 Read(off, count int64) (string, error)
11 // ReadDir returns the directory's entries in a deterministic order. It
12 // returns ErrNotDir on a plain file.
13 ReadDir() ([]Stat, error)
14}File is a 9P file server's whole read surface. Every method must be free of side effects: a File is routinely reached across a realm boundary, where the running frame belongs to the CALLER, so mutating anything here would be both a VM error and a confused deputy.
Mutable is the write half, kept out of File on purpose.
It is NOT safe across a realm boundary: a non-crossing method runs in the caller's frame, so a foreign realm calling these would be trying to mutate objects it does not own. Only call Mutable on a tree your own realm created. The caller supplies now (a block height) rather than the tree reading the chain itself, so the same code is testable off chain.
Perm holds the 9P mode word: the low nine bits are rwx for owner, group and other, the high bits are the DM* kind flags.
Qid is the server's unique handle for a file. Path identifies the file within one server for its whole lifetime; Version increments on every write, so a client can tell "same file, changed" from "different file" without reading either.
Stat is 9P's directory entry, minus the fields that only mean something on a wire (type, dev) or on a host clock (atime).