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

v0 source pure

Package ninep is a Plan 9 shaped file abstraction for gno.

Readme View source

gno.land/p/moul/x/plan9/ninep/v0

A 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:

  • Data is a 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.
  • There is no 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.

Overview

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:

  • Data is a string, not []byte. Every consumer on this chain is text and Render returns a string.
  • Mtime is a block height, not a wall clock. It is the only clock that every validating node agrees on.
  • There is no open/clunk pair. Without a session there are no fids, so Walk returns the file itself and nothing has to be released.
  • File is READ-ONLY. A crossing method would mint the caller's realm frame for the callee, which is a confused-deputy hazard (compare r/gov/dao's Executor, and p/nt/grc20's deliberate refusal to make Teller crossing). Mutation lives in Mutable, which is only safe on a tree your own realm owns.

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.

Constants 3

const QTFILE, QTTMP, QTAUTH, QTMOUNT, QTEXCL, QTAPPEND, QTDIR

1const (
2	QTFILE   uint8 = 0x00 // a plain file
3	QTTMP    uint8 = 0x04 // not archived
4	QTAUTH   uint8 = 0x08 // authentication file
5	QTMOUNT  uint8 = 0x10 // mounted channel
6	QTEXCL   uint8 = 0x20 // exclusive use
7	QTAPPEND uint8 = 0x40 // append only
8	QTDIR    uint8 = 0x80 // a directory
9)
source

Qid type bits, as in 9P2000.

const MaxDepth

1const MaxDepth = 32
source

MaxDepth bounds a walk. Resolution costs one cross-realm call per element per union member, so depth is capped rather than trusted.

Variables 1

var ErrNotExist, ErrNotDir, ErrIsDir, ErrExist, ErrPerm, ErrNoCreate, ErrReadOnly, ErrBadName, ErrNotEmpty, ErrTooDeep

 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)
source

Plan 9 error strings, kept lowercase and verbatim where they exist.

Functions 10

func Abs

1func Abs(cwd, p string) string
source

Abs resolves p against cwd, then cleans it.

func Base

1func Base(p string) string
source

Base returns the last element of p, or "/" for the root.

func Clean

1func Clean(p string) string
source

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").

func Dir

1func Dir(p string) string
source

Dir returns p's parent.

func Elems

1func Elems(p string) []string
source

Elems splits a cleaned absolute path into its elements. The root yields nil.

func Join

1func Join(dir, name string) string
source

Join appends name to dir.

func ReadAll

1func ReadAll(f File) (string, error)
source

ReadAll reads a whole file.

func Slice

1func Slice(s string, off, count int64) string
source

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.

func ValidName

1func ValidName(name string) bool
source

ValidName reports whether name is usable as a single path element. Plan 9 rejects the empty name, "." and "..", and any name containing a slash.

func Walk

1func Walk(f File, elems []string) (File, error)
source

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.

Types 5

type File

interface
 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}
source

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.

type Mutable

interface
1type Mutable interface {
2	File
3	Create(name string, perm Perm, now int64) (File, error)
4	Remove(name string) error
5	Write(off int64, data string, now int64) (int64, error)
6	Truncate(size int64, now int64) error
7}
source

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.

type Perm

ident
1type Perm uint32
source

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.

Methods on Perm

func IsDir

method on Perm
1func (p Perm) IsDir() bool
source

IsDir reports whether the mode marks a directory.

func String

method on Perm
1func (p Perm) String() string
source

String renders the mode the way ls -l does: a kind letter then nine rwx bits. The kind letter is 'd' for a directory, 'a' for append-only, 'l' for exclusive-use, '-' otherwise.

type Qid

struct
1type Qid struct {
2	Type    uint8
3	Version uint32
4	Path    uint64
5}
source

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.

Methods on Qid

func IsDir

method on Qid
1func (q Qid) IsDir() bool
source

IsDir reports whether the qid marks a directory.

func String

method on Qid
1func (q Qid) String() string
source

String renders the qid as Plan 9 does, "(path version type)", with the path in hex.

type Stat

struct
 1type Stat struct {
 2	Qid    Qid
 3	Mode   Perm
 4	Mtime  int64 // block height of the last write
 5	Length int64 // in bytes; zero for a directory, as in 9P
 6	Name   string
 7	Uid    string // owner; a bech32 address, or a well-known name
 8	Gid    string // group
 9	Muid   string // last writer
10}
source

Stat is 9P's directory entry, minus the fields that only mean something on a wire (type, dev) or on a host clock (atime).

Methods on Stat

func IsDir

method on Stat
1func (s Stat) IsDir() bool
source

IsDir reports whether the entry is a directory.

func Line

method on Stat
1func (s Stat) Line() string
source

Line renders the entry the way ls -l does.

Imports 4

  • errors stdlib
  • path stdlib
  • strconv stdlib
  • strings stdlib

Source Files 4