PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

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

Package
Open in gnoweb ↗

Overview

Kind
Pure package
Name
v0
Namespace
moul / x / plan9 / ns
Files
4 (README)(gnomod.toml)
Exported functions
n/a — not supported for pure packages by the node (vm/qfuncs)
Module
gno.land/p/moul/x/plan9/ns/v0
gno
0.9

Files (4)

  • README.mdmarkdown
  • gnomod.tomltoml
  • ns.gnogno
  • ns_test.gnogno
ns.gnogno
1// Package ns is a Plan 9 namespace: a private, mutable mount table over2// gno.land/p/moul/x/plan9/ninep file trees.3//4// This is the idea the rest of the suite exists for. Plan 9's leverage does5// not come from "everything is a file", it comes from every process owning its6// own name-to-resource mapping, so a name can be REPLACED and any service can7// be composed, shadowed, sandboxed or mocked without the program knowing. Gno8// has one global tree and no way to hold a view of it; an Ns is that view.9//10// Bind flags follow bind(2): MREPL replaces, MBEFORE splices the new directory11// in front of the old one, MAFTER behind it, and MCREATE (OR'd onto any of12// them) marks the union member that new files are created in.13//14// Two Plan 9 properties are load-bearing and reproduced deliberately:15//16//   - A union is TOP LEVEL ONLY. Binding /a onto /b unions the two directories17//     at /b; /b/c resolves in whichever member won the walk, and is not itself18//     a union unless something is bound there too.19//   - A bind captures a CHANNEL, not a name. The source is resolved once, at20//     bind time, and the resulting file is what is stored. Rebinding the21//     source afterwards does not retroactively move the target.22//23// ".." is removed lexically before resolution starts and never reaches a24// server, per "Lexical File Names in Plan 9, or Getting Dot-Dot Right".25//26// NOTICE. Plan 9 from Bell Labs is the work of the Computing Science Research27// Center at Bell Labs; the name and the marks are theirs, and the copyright is28// held by the Plan 9 Foundation (https://p9f.org). This package is not29// affiliated with, endorsed by, or sponsored by them, and contains no Plan 930// code: it borrows the vocabulary so that the design reads without a glossary,31// and it is an homage, asking what that ecosystem's spirit looks like on a32// chain. Full attribution: NOTICE.md at the root of moul/gno-contracts.33package ns3435import (36	"errors"37	"strings"3839	"gno.land/p/nt/avl/v0"4041	ninep "gno.land/p/moul/x/plan9/ninep/v0"42)4344// Flag is a bind(2) mount flag. The low two bits pick the mode; MCREATE is45// OR'd onto it.46type Flag uint324748// Mount flags, with Plan 9's values.49const (50	MREPL   Flag = 0x0000 // replace the old file with the new one51	MBEFORE Flag = 0x0001 // union, new directory first52	MAFTER  Flag = 0x0002 // union, new directory last53	MCREATE Flag = 0x0004 // creates in this union go here54)5556func (f Flag) mode() Flag    { return f & 0x0003 }57func (f Flag) creates() bool { return f&MCREATE != 0 }5859// String renders the flag as the command-line letters bind(1) uses.60func (f Flag) String() string {61	s := ""62	switch f.mode() {63	case MBEFORE:64		s = "-b"65	case MAFTER:66		s = "-a"67	}68	if f.creates() {69		if s == "" {70			s = "-c"71		} else {72			s += "c"73		}74	}75	return s76}7778// Limits. Resolution costs one call per element per union member, and those79// calls may cross a realm boundary, so both are capped rather than trusted.80const (81	MaxUnion = 8 // members in one union82	MaxOps   = 6483)8485var (86	// ErrNoRoot means the namespace has nothing bound at "/".87	ErrNoRoot = errors.New("no root in namespace")88	// ErrWideUnion means a bind would exceed MaxUnion members.89	ErrWideUnion = errors.New("union too wide")90	// ErrTooManyOps means the namespace has reached MaxOps bindings.91	ErrTooManyOps = errors.New("too many namespace operations")92	// ErrNotBound means unmount was asked to remove something that is not93	// bound at that name.94	ErrNotBound = errors.New("not bound")95)9697// member is one element of a union. A nil file is the placeholder standing for98// "whatever walking to this name would have found", which is how MBEFORE and99// MAFTER keep the original directory in the union.100type member struct {101	file   ninep.File102	create bool103}104105type entry struct {106	members []member107}108109// Op records one bind or mount, in application order, so that String can print110// the namespace the way Plan 9's ns(1) does.111type Op struct {112	Verb   string // "bind" or "mount"113	Flag   Flag114	Source string115	Target string116}117118// Ns is one namespace: a mount table, a working directory, and the ordered119// list of operations that produced them.120type Ns struct {121	table *avl.Tree // cleaned target path -> *entry122	ops   []Op123	cwd   string124}125126// New returns a namespace whose root is the given file. The root is the one127// binding that cannot be expressed as a bind of something else, so it is128// installed directly.129func New(root ninep.File) *Ns {130	n := &Ns{table: avl.NewTree(), cwd: "/"}131	n.table.Set("/", &entry{members: []member{{file: root, create: true}}})132	return n133}134135// Cwd returns the working directory.136func (n *Ns) Cwd() string { return n.cwd }137138// Cd sets the working directory, which must resolve to a directory.139func (n *Ns) Cd(p string) error {140	abs := n.Abs(p)141	st, err := n.Stat(abs)142	if err != nil {143		return err144	}145	if !st.IsDir() {146		return ninep.ErrNotDir147	}148	n.cwd = abs149	return nil150}151152// Abs resolves p against the working directory.153func (n *Ns) Abs(p string) string { return ninep.Abs(n.cwd, p) }154155func (n *Ns) entry(p string) *entry {156	v := n.table.Get(p)157	if v == nil {158		return nil159	}160	return v.(*entry)161}162163// expand substitutes the walk result for the placeholder member.164func expand(ms []member, under []member) []member {165	out := []member{}166	for _, m := range ms {167		if m.file == nil {168			out = append(out, under...)169			continue170		}171		out = append(out, m)172	}173	return out174}175176// resolve walks p and returns the union found there, richest form: members177// still carry their create bit, and bound reports whether a mount entry178// applied at the final element.179func (n *Ns) resolve(p string) ([]member, error) {180	root := n.entry("/")181	if root == nil {182		return nil, ErrNoRoot183	}184	cur := expand(root.members, nil)185	if len(cur) == 0 {186		return nil, ErrNoRoot187	}188	elems := ninep.Elems(ninep.Clean(p))189	if len(elems) > ninep.MaxDepth {190		return nil, ninep.ErrTooDeep191	}192	prefix := ""193	for _, e := range elems {194		// First match wins. Walking INTO a union does not produce a union195		// of the members' subdirectories: in Plan 9 a union is top level196		// only, and a walk returns one channel.197		walked := []member{}198		for _, m := range cur {199			f, err := m.file.Walk(e)200			if err == nil {201				walked = append(walked, member{file: f})202				break203			}204		}205		prefix += "/" + e206		next := walked207		if ent := n.entry(prefix); ent != nil {208			next = expand(ent.members, walked)209		}210		if len(next) == 0 {211			return nil, ninep.ErrNotExist212		}213		cur = next214	}215	return cur, nil216}217218// Resolve returns every union member visible at p, in search order.219func (n *Ns) Resolve(p string) ([]ninep.File, error) {220	ms, err := n.resolve(n.Abs(p))221	if err != nil {222		return nil, err223	}224	out := []ninep.File{}225	for _, m := range ms {226		out = append(out, m.file)227	}228	return out, nil229}230231// Open returns the file a name resolves to: the first member of its union,232// which is the one a walk through this name would reach.233func (n *Ns) Open(p string) (ninep.File, error) {234	fs, err := n.Resolve(p)235	if err != nil {236		return nil, err237	}238	return fs[0], nil239}240241// Stat returns the entry for p, with the name replaced by the last element of242// the path as asked for, so that a union member's own name never leaks.243func (n *Ns) Stat(p string) (ninep.Stat, error) {244	f, err := n.Open(p)245	if err != nil {246		return ninep.Stat{}, err247	}248	st := f.Stat()249	st.Name = ninep.Base(n.Abs(p))250	return st, nil251}252253// ReadFile reads the whole file at p.254func (n *Ns) ReadFile(p string) (string, error) {255	f, err := n.Open(p)256	if err != nil {257		return "", err258	}259	return ninep.ReadAll(f)260}261262// ReadDir lists p. A union directory is the CONCATENATION of its members'263// contents, as in Plan 9, so duplicate names can appear and shadowing is264// visible. Pass unique to collapse them first-wins instead, which is the set265// of names a walk can actually reach.266func (n *Ns) ReadDir(p string, unique bool) ([]ninep.Stat, error) {267	ms, err := n.resolve(n.Abs(p))268	if err != nil {269		return nil, err270	}271	if len(ms) == 1 && !ms[0].file.Stat().IsDir() {272		return nil, ninep.ErrNotDir273	}274	out := []ninep.Stat{}275	seen := map[string]bool{}276	for _, m := range ms {277		ents, err := m.file.ReadDir()278		if err != nil {279			continue // a non-directory member contributes nothing280		}281		for _, e := range ents {282			if unique {283				if seen[e.Name] {284					continue285				}286				seen[e.Name] = true287			}288			out = append(out, e)289		}290	}291	return out, nil292}293294// CreateTarget returns the directory that a create at p should happen in.295//296// For a plain directory that is just the directory. For a union it is the297// first member carrying MCREATE, and if no member has it, creation is refused,298// which is bind(2)'s rule.299func (n *Ns) CreateTarget(p string) (ninep.Mutable, error) {300	abs := n.Abs(p)301	ms, err := n.resolve(abs)302	if err != nil {303		return nil, err304	}305	isUnion := n.entry(abs) != nil && len(ms) > 1306	for _, m := range ms {307		if isUnion && !m.create {308			continue309		}310		mu, ok := m.file.(ninep.Mutable)311		if !ok {312			continue // a read-only server, for instance a mounted realm313		}314		if !mu.Stat().IsDir() {315			continue316		}317		return mu, nil318	}319	if isUnion {320		return nil, ninep.ErrNoCreate321	}322	return nil, ninep.ErrPerm323}324325// Bind makes source visible at target, as bind(1) does. Both names are326// resolved in THIS namespace, and the source is resolved once: what is stored327// is the file it names today, not the name.328func (n *Ns) Bind(source, target string, flag Flag) error {329	return n.BindVerb("bind", source, target, flag)330}331332// BindVerb is Bind with the verb that String should print. Plan 9 spells the333// same operation "bind" or "mount" depending on whether the source is a name334// or a channel, and ns(1) echoes back whichever was used.335func (n *Ns) BindVerb(verb, source, target string, flag Flag) error {336	ms, err := n.resolve(n.Abs(source))337	if err != nil {338		return err339	}340	files := []ninep.File{}341	for _, m := range ms {342		files = append(files, m.file)343	}344	return n.graft(verb, source, target, flag, files)345}346347// Mount grafts a file tree that has no name in this namespace yet, which is348// how a service posted by another realm gets in. source is a label, used only349// when printing the namespace.350func (n *Ns) Mount(f ninep.File, source, target string, flag Flag) error {351	return n.graft("mount", source, target, flag, []ninep.File{f})352}353354func (n *Ns) graft(verb, source, target string, flag Flag, files []ninep.File) error {355	if len(n.ops) >= MaxOps {356		return ErrTooManyOps357	}358	abs := n.Abs(target)359	// bind(1) requires the target to exist: you can only rebind a name that360	// already resolves to something.361	if _, err := n.resolve(abs); err != nil {362		return err363	}364	add := []member{}365	for _, f := range files {366		add = append(add, member{file: f, create: flag.creates()})367	}368369	ent := n.entry(abs)370	var members []member371	switch {372	case ent == nil:373		// The name has never been bound: its current contents are the374		// placeholder. It does NOT carry MCREATE, which is why creating375		// in a freshly unioned directory is refused until some member is376		// bound with -c.377		members = []member{{file: nil}}378	default:379		members = ent.members380	}381	switch flag.mode() {382	case MBEFORE:383		members = append(add, members...)384	case MAFTER:385		members = append(members, add...)386	default: // MREPL: the old file is gone, not unioned387		members = add388	}389	if len(members) > MaxUnion {390		return ErrWideUnion391	}392	n.table.Set(abs, &entry{members: members})393	n.ops = append(n.ops, Op{Verb: verb, Flag: flag, Source: source, Target: abs})394	return nil395}396397// Unmount undoes bindings at target. With an empty source it removes every398// binding there, restoring the name to whatever it resolved to originally.399func (n *Ns) Unmount(source, target string) error {400	abs := n.Abs(target)401	ent := n.entry(abs)402	if ent == nil {403		return ErrNotBound404	}405	if source == "" {406		n.table.Remove(abs)407		n.dropOps(abs, "")408		return nil409	}410	ms, err := n.resolve(n.Abs(source))411	if err != nil {412		return err413	}414	kept := []member{}415	removed := false416	for _, m := range ent.members {417		drop := false418		for _, s := range ms {419			if m.file != nil && m.file == s.file {420				drop = true421				break422			}423		}424		if drop {425			removed = true426			continue427		}428		kept = append(kept, m)429	}430	if !removed {431		return ErrNotBound432	}433	if onlyPlaceholders(kept) {434		// Nothing but "whatever was here originally" is left, so the entry435		// is a no-op: drop it rather than leave a phantom binding behind.436		n.table.Remove(abs)437	} else {438		n.table.Set(abs, &entry{members: kept})439	}440	n.dropOps(abs, source)441	return nil442}443444func onlyPlaceholders(ms []member) bool {445	for _, m := range ms {446		if m.file != nil {447			return false448		}449	}450	return true451}452453func (n *Ns) dropOps(target, source string) {454	kept := []Op{}455	for _, op := range n.ops {456		if op.Target == target && (source == "" || op.Source == source) {457			continue458		}459		kept = append(kept, op)460	}461	n.ops = kept462}463464// Ops returns the bindings in application order.465func (n *Ns) Ops() []Op { return n.ops }466467// String prints the namespace the way Plan 9's ns(1) does: one line per468// binding, in the order they were applied, then the working directory.469func (n *Ns) String() string {470	var b strings.Builder471	for _, op := range n.ops {472		b.WriteString(op.Verb)473		if f := op.Flag.String(); f != "" {474			b.WriteString(" " + f)475		}476		b.WriteString(" " + quote(op.Source) + " " + quote(op.Target) + "\n")477	}478	b.WriteString("cd " + quote(n.cwd) + "\n")479	return b.String()480}481482// quote applies rc quoting: single quotes when the word contains a space or a483// quote, with an embedded quote doubled.484func quote(s string) string {485	if s != "" && !strings.ContainsAny(s, " \t'") {486		return s487	}488	return "'" + strings.ReplaceAll(s, "'", "''") + "'"489}490

Functions

not supported for pure packages by the node (vm/qfuncs)

Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.