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 no4// socket on a chain, the VM call is the transport. What it keeps is the part5// that made 9P useful, namely that every resource answers the same four6// questions (stat, walk, read, readdir), so a client written today can browse7// a file server deployed tomorrow.8//9// One interface, not two. 9P reads a directory with the same Tread it uses for10// a file, so splitting File from Dir would be less faithful, and a single11// 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 and16// Render returns a string.17// - Mtime is a block height, not a wall clock. It is the only clock that18// every validating node agrees on.19// - There is no open/clunk pair. Without a session there are no fids, so20// 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 frame22// for the callee, which is a confused-deputy hazard (compare r/gov/dao's23// 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 realm25// owns.26//27// See gno.land/p/moul/x/plan9/ns for the namespace that binds these trees28// 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 Research31// Center at Bell Labs; the name and the marks are theirs, and the copyright is32// held by the Plan 9 Foundation (https://p9f.org). This package is not33// affiliated with, endorsed by, or sponsored by them, and contains no Plan 934// 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 a36// chain. Full attribution: NOTICE.md at the root of moul/gno-contracts.37package ninep3839import (40 "errors"41 "path"42 "strconv"43 "strings"44)4546// Qid type bits, as in 9P2000.47const (48 QTFILE uint8 = 0x00 // a plain file49 QTTMP uint8 = 0x04 // not archived50 QTAUTH uint8 = 0x08 // authentication file51 QTMOUNT uint8 = 0x10 // mounted channel52 QTEXCL uint8 = 0x20 // exclusive use53 QTAPPEND uint8 = 0x40 // append only54 QTDIR uint8 = 0x80 // a directory55)5657// Perm holds the 9P mode word: the low nine bits are rwx for owner, group and58// other, the high bits are the DM* kind flags.59type Perm uint326061// Mode bits, as in 9P2000.62const (63 DMTMP Perm = 0x0400000064 DMAUTH Perm = 0x0800000065 DMMOUNT Perm = 0x1000000066 DMEXCL Perm = 0x2000000067 DMAPPEND Perm = 0x4000000068 DMDIR Perm = 0x800000006970 PermMask Perm = 0777 // the rwxrwxrwx bits7172 // Conventional defaults, matching what Plan 9's ramfs hands out.73 DirPerm Perm = DMDIR | 075574 FilePerm Perm = 064475)7677// IsDir reports whether the mode marks a directory.78func (p Perm) IsDir() bool { return p&DMDIR != 0 }7980// String renders the mode the way ls -l does: a kind letter then nine rwx81// bits. The kind letter is 'd' for a directory, 'a' for append-only, 'l' for82// exclusive-use, '-' otherwise.83func (p Perm) String() string {84 var b strings.Builder85 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}105106// Qid is the server's unique handle for a file. Path identifies the file107// within one server for its whole lifetime; Version increments on every write,108// so a client can tell "same file, changed" from "different file" without109// reading either.110type Qid struct {111 Type uint8112 Version uint32113 Path uint64114}115116// IsDir reports whether the qid marks a directory.117func (q Qid) IsDir() bool { return q.Type&QTDIR != 0 }118119// String renders the qid as Plan 9 does, "(path version type)", with the path120// 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}129130// Stat is 9P's directory entry, minus the fields that only mean something on a131// wire (type, dev) or on a host clock (atime).132type Stat struct {133 Qid Qid134 Mode Perm135 Mtime int64 // block height of the last write136 Length int64 // in bytes; zero for a directory, as in 9P137 Name string138 Uid string // owner; a bech32 address, or a well-known name139 Gid string // group140 Muid string // last writer141}142143// IsDir reports whether the entry is a directory.144func (s Stat) IsDir() bool { return s.Mode.IsDir() }145146// 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.Name150}151152// pad right-pads to n runes. ufmt has no width flags in gno, so padding is by153// hand everywhere in this suite.154func pad(s string, n int) string {155 for len(s) < n {156 s += " "157 }158 return s159}160161func lpad(s string, n int) string {162 for len(s) < n {163 s = " " + s164 }165 return s166}167168// File is a 9P file server's whole read surface. Every method must be free of169// side effects: a File is routinely reached across a realm boundary, where the170// running frame belongs to the CALLER, so mutating anything here would be both171// a VM error and a confused deputy.172type File interface {173 // Stat returns the entry for this file.174 Stat() Stat175 // Walk resolves exactly one path element. It returns ErrNotDir on a176 // plain file and ErrNotExist when the name is absent. It never sees177 // "." 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 count180 // 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. It183 // returns ErrNotDir on a plain file.184 ReadDir() ([]Stat, error)185}186187// 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 the190// caller's frame, so a foreign realm calling these would be trying to mutate191// 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 the193// chain itself, so the same code is testable off chain.194type Mutable interface {195 File196 Create(name string, perm Perm, now int64) (File, error)197 Remove(name string) error198 Write(off int64, data string, now int64) (int64, error)199 Truncate(size int64, now int64) error200}201202// 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)215216// MaxDepth bounds a walk. Resolution costs one cross-realm call per element217// per union member, so depth is capped rather than trusted.218const MaxDepth = 32219220// ValidName reports whether name is usable as a single path element. Plan 9221// rejects the empty name, "." and "..", and any name containing a slash.222func ValidName(name string) bool {223 if name == "" || name == "." || name == ".." {224 return false225 }226 return !strings.Contains(name, "/")227}228229// Clean returns p as a cleaned absolute path. "." and ".." are resolved230// lexically, before any server sees them, which is what makes ".." undo the231// name you typed rather than the directory you landed in (see "Lexical File232// Names in Plan 9").233func Clean(p string) string {234 if !strings.HasPrefix(p, "/") {235 p = "/" + p236 }237 return path.Clean(p)238}239240// 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}253254// 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 nil259 }260 return strings.Split(p[1:], "/")261}262263// Base returns the last element of p, or "/" for the root.264func Base(p string) string { return path.Base(Clean(p)) }265266// Dir returns p's parent.267func Dir(p string) string { return path.Dir(Clean(p)) }268269// Join appends name to dir.270func Join(dir, name string) string { return Clean(dir + "/" + name) }271272// 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, ErrTooDeep277 }278 for _, e := range elems {279 next, err := f.Walk(e)280 if err != nil {281 return nil, err282 }283 f = next284 }285 return f, nil286}287288// ReadAll reads a whole file.289func ReadAll(f File) (string, error) { return f.Read(0, -1) }290291// Slice applies 9P's read window to s: at most count bytes from off, with a292// 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 = 0297 }298 n := int64(len(s))299 if off >= n {300 return ""301 }302 end := n303 if count >= 0 && off+count < n {304 end = off + count305 }306 return s[off:end]307}308Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.