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/rc/v0

Package
Open in gnoweb ↗

Overview

Kind
Pure package
Name
v0
Namespace
moul / x / plan9 / rc
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/rc/v0
gno
0.9

Files (4)

  • README.mdmarkdown
  • gnomod.tomltoml
  • rc.gnogno
  • rc_test.gnogno
rc.gnogno
1// Package rc is a small shell over a Plan 9 namespace.2//3// It exists because a namespace you cannot inspect is a namespace you cannot4// trust. Everything here operates on a gno.land/p/moul/x/plan9/ns namespace5// and returns text, so one realm's Render becomes a file browser and one6// transaction becomes a shell command:7//8//	Exec("bind -a /srv/dev /dev; echo hello > /tmp/greeting")9//10// The mode split is the security model. A Shell in ReadOnly mode refuses every11// mutating command, which is what lets a realm expose the shell through12// Render, where mutating anything would be a bug, while the same code backs a13// crossing Exec that may write.14//15// Quoting follows rc: single quotes, with ” inside a quoted string standing16// for one literal quote. Commands are separated by ';' or newlines, and17// quoting is respected when splitting them.18//19// On the first error the run STOPS and returns it. A realm should let that20// error panic, so a half-applied command line reverts with the transaction21// rather than leaving the namespace in a state nobody asked for.22//23// NOTICE. Plan 9 from Bell Labs is the work of the Computing Science Research24// Center at Bell Labs; the name and the marks are theirs, and the copyright is25// held by the Plan 9 Foundation (https://p9f.org). This package is not26// affiliated with, endorsed by, or sponsored by them, and contains no Plan 927// code: it borrows the vocabulary so that the design reads without a glossary,28// and it is an homage, asking what that ecosystem's spirit looks like on a29// chain. Full attribution: NOTICE.md at the root of moul/gno-contracts.30package rc3132import (33	"errors"34	"strconv"35	"strings"3637	memfs "gno.land/p/moul/x/plan9/memfs/v0"38	ninep "gno.land/p/moul/x/plan9/ninep/v0"39	ns "gno.land/p/moul/x/plan9/ns/v0"40)4142// Mode decides whether mutating commands are allowed.43type Mode uint84445// Shell modes.46const (47	ReadOnly  Mode = 048	ReadWrite Mode = 149)5051var (52	// ErrReadOnly is returned when a mutating command runs in ReadOnly mode.53	ErrReadOnly = errors.New("read-only shell")54	// ErrUsage reports a malformed command line.55	ErrUsage = errors.New("usage")56	// ErrUnknown reports a command this shell does not have.57	ErrUnknown = errors.New("command not found")58	// ErrQuote reports an unterminated quoted string.59	ErrQuote = errors.New("unterminated '")60)6162// MaxCommands bounds one run, so a single call cannot loop the VM.63const MaxCommands = 326465// Shell runs commands against one namespace.66type Shell struct {67	ns   *ns.Ns68	mode Mode69	now  func() int6470}7172// New returns a shell over n. now supplies the block height stamped on writes73// and may be nil in ReadOnly mode.74func New(n *ns.Ns, mode Mode, now func() int64) *Shell {75	return &Shell{ns: n, mode: mode, now: now}76}7778// Ns returns the namespace the shell operates on.79func (s *Shell) Ns() *ns.Ns { return s.ns }8081func (s *Shell) clock() int64 {82	if s.now == nil {83		return 084	}85	return s.now()86}8788func (s *Shell) writable() error {89	if s.mode != ReadWrite {90		return ErrReadOnly91	}92	return nil93}9495// Run executes a command line and returns its output. Execution stops at the96// first error.97func (s *Shell) Run(line string) (string, error) {98	cmds, err := splitCommands(line)99	if err != nil {100		return "", err101	}102	if len(cmds) > MaxCommands {103		return "", errors.New("too many commands in one line")104	}105	var out strings.Builder106	for _, c := range cmds {107		toks, err := tokenize(c)108		if err != nil {109			return out.String(), err110		}111		if len(toks) == 0 {112			continue113		}114		text, err := s.run1(toks)115		out.WriteString(text)116		if err != nil {117			return out.String(), errors.New(toks[0] + ": " + err.Error())118		}119	}120	return out.String(), nil121}122123func (s *Shell) run1(toks []string) (string, error) {124	cmd, args := toks[0], toks[1:]125	switch cmd {126	case "help":127		return help, nil128	case "pwd":129		return s.ns.Cwd() + "\n", nil130	case "cd":131		return "", s.cd(args)132	case "ns":133		return s.ns.String(), nil134	case "ls":135		return s.ls(args)136	case "cat":137		return s.cat(args)138	case "stat":139		return s.stat(args)140	case "walk":141		return s.walk(args)142	case "bind", "mount":143		return "", s.bind(cmd, args)144	case "unmount", "unbind":145		return "", s.unmount(args)146	case "mkdir":147		return "", s.mkdir(args)148	case "rm":149		return "", s.rm(args)150	case "echo":151		return s.echo(args)152	}153	return "", ErrUnknown154}155156const help = `bind [-a|-b|-c] new old   make new visible at old (-b before, -a after, -c creates)157cat file...               print files158cd [dir]                  change directory159echo [-n] word... [>|>> file]160ls [-l] [-u] [path...]    list; -l long, -u collapse union duplicates161mkdir path...             create directories162mount                     synonym for bind163ns                        print the namespace164pwd                       print the working directory165rm path...                remove files and empty directories166stat path...              print the 9P stat of each path167unmount [new] old         undo a binding168walk path                 show how each element of path resolves169`170171func (s *Shell) cd(args []string) error {172	if len(args) == 0 {173		return s.ns.Cd("/")174	}175	if len(args) > 1 {176		return ErrUsage177	}178	return s.ns.Cd(args[0])179}180181func (s *Shell) ls(args []string) (string, error) {182	long, unique, paths := false, false, []string{}183	for _, a := range args {184		if strings.HasPrefix(a, "-") && len(a) > 1 {185			for _, r := range a[1:] {186				switch r {187				case 'l':188					long = true189				case 'u':190					unique = true191				default:192					return "", ErrUsage193				}194			}195			continue196		}197		paths = append(paths, a)198	}199	if len(paths) == 0 {200		paths = []string{s.ns.Cwd()}201	}202	var out strings.Builder203	for i, p := range paths {204		st, err := s.ns.Stat(p)205		if err != nil {206			return out.String(), err207		}208		if len(paths) > 1 {209			if i > 0 {210				out.WriteString("\n")211			}212			out.WriteString(s.ns.Abs(p) + ":\n")213		}214		if !st.IsDir() {215			out.WriteString(entryLine(st, long))216			continue217		}218		ents, err := s.ns.ReadDir(p, unique)219		if err != nil {220			return out.String(), err221		}222		for _, e := range ents {223			out.WriteString(entryLine(e, long))224		}225	}226	return out.String(), nil227}228229func entryLine(st ninep.Stat, long bool) string {230	if long {231		return st.Line() + "\n"232	}233	name := st.Name234	if st.IsDir() && name != "/" {235		name += "/"236	}237	return name + "\n"238}239240func (s *Shell) cat(args []string) (string, error) {241	if len(args) == 0 {242		return "", ErrUsage243	}244	var out strings.Builder245	for _, p := range args {246		data, err := s.ns.ReadFile(p)247		if err != nil {248			return out.String(), err249		}250		out.WriteString(data)251	}252	return out.String(), nil253}254255func (s *Shell) stat(args []string) (string, error) {256	if len(args) == 0 {257		args = []string{s.ns.Cwd()}258	}259	var out strings.Builder260	for _, p := range args {261		st, err := s.ns.Stat(p)262		if err != nil {263			return out.String(), err264		}265		members, err := s.ns.Resolve(p)266		if err != nil {267			return out.String(), err268		}269		out.WriteString(s.ns.Abs(p) + " " + st.Qid.String() +270			" " + st.Mode.String() +271			" uid=" + st.Uid +272			" length=" + strconv.FormatInt(st.Length, 10) +273			" mtime=" + strconv.FormatInt(st.Mtime, 10) +274			" union=" + strconv.Itoa(len(members)) + "\n")275	}276	return out.String(), nil277}278279// walk shows the resolution of each element, which is where a namespace stops280// being magic: the union width column says exactly when a bind took effect.281func (s *Shell) walk(args []string) (string, error) {282	if len(args) != 1 {283		return "", ErrUsage284	}285	abs := s.ns.Abs(args[0])286	var out strings.Builder287	prefix := "/"288	report := func(p string) error {289		st, err := s.ns.Stat(p)290		if err != nil {291			return err292		}293		members, err := s.ns.Resolve(p)294		if err != nil {295			return err296		}297		out.WriteString(pad(p, 24) + " " + st.Qid.String() + " " +298			st.Mode.String() + " union=" + strconv.Itoa(len(members)) + "\n")299		return nil300	}301	if err := report("/"); err != nil {302		return out.String(), err303	}304	for _, e := range ninep.Elems(abs) {305		prefix = ninep.Join(prefix, e)306		if err := report(prefix); err != nil {307			return out.String(), err308		}309	}310	return out.String(), nil311}312313func pad(s string, n int) string {314	for len(s) < n {315		s += " "316	}317	return s318}319320func (s *Shell) bind(verb string, args []string) error {321	if err := s.writable(); err != nil {322		return err323	}324	flag := ns.MREPL325	rest := []string{}326	for _, a := range args {327		if strings.HasPrefix(a, "-") && len(a) > 1 {328			for _, r := range a[1:] {329				switch r {330				case 'b':331					flag = flag&^0x3 | ns.MBEFORE332				case 'a':333					flag = flag&^0x3 | ns.MAFTER334				case 'c':335					flag |= ns.MCREATE336				default:337					return ErrUsage338				}339			}340			continue341		}342		rest = append(rest, a)343	}344	if len(rest) != 2 {345		return ErrUsage346	}347	return s.ns.BindVerb(verb, rest[0], rest[1], flag)348}349350func (s *Shell) unmount(args []string) error {351	if err := s.writable(); err != nil {352		return err353	}354	switch len(args) {355	case 1:356		return s.ns.Unmount("", args[0])357	case 2:358		return s.ns.Unmount(args[0], args[1])359	}360	return ErrUsage361}362363func (s *Shell) mkdir(args []string) error {364	if err := s.writable(); err != nil {365		return err366	}367	if len(args) == 0 {368		return ErrUsage369	}370	for _, p := range args {371		abs := s.ns.Abs(p)372		target, err := s.ns.CreateTarget(ninep.Dir(abs))373		if err != nil {374			return err375		}376		if _, err := target.Create(ninep.Base(abs), ninep.DirPerm, s.clock()); err != nil {377			return err378		}379	}380	return nil381}382383func (s *Shell) rm(args []string) error {384	if err := s.writable(); err != nil {385		return err386	}387	if len(args) == 0 {388		return ErrUsage389	}390	for _, p := range args {391		abs := s.ns.Abs(p)392		name := ninep.Base(abs)393		members, err := s.ns.Resolve(ninep.Dir(abs))394		if err != nil {395			return err396		}397		found, removed := false, false398		for _, m := range members {399			if _, err := m.Walk(name); err != nil {400				continue401			}402			found = true403			mu, ok := m.(ninep.Mutable)404			if !ok {405				continue // a read-only server: try the next union member406			}407			if err := mu.Remove(name); err != nil {408				return err409			}410			removed = true411			break412		}413		switch {414		case removed:415		case found:416			return ninep.ErrReadOnly417		default:418			return ninep.ErrNotExist419		}420	}421	return nil422}423424func (s *Shell) echo(args []string) (string, error) {425	newline := true426	words := []string{}427	redirect, appendTo, dest := false, false, ""428	i := 0429	for i < len(args) {430		a := args[i]431		switch {432		case a == "-n" && len(words) == 0 && !redirect:433			newline = false434		case a == ">" || a == ">>":435			if i+1 >= len(args) {436				return "", ErrUsage437			}438			redirect, appendTo, dest = true, a == ">>", args[i+1]439			i++440		default:441			words = append(words, a)442		}443		i++444	}445	text := strings.Join(words, " ")446	if newline {447		text += "\n"448	}449	if !redirect {450		return text, nil451	}452	if err := s.writable(); err != nil {453		return "", err454	}455	return "", s.writeTo(dest, text, appendTo)456}457458// writeTo creates or opens dest and writes text, honouring the namespace's459// MCREATE rules when the file has to be created.460func (s *Shell) writeTo(dest, text string, appendTo bool) error {461	abs := s.ns.Abs(dest)462	f, err := s.ns.Open(abs)463	if err == ninep.ErrNotExist {464		target, terr := s.ns.CreateTarget(ninep.Dir(abs))465		if terr != nil {466			return terr467		}468		created, cerr := target.Create(ninep.Base(abs), ninep.FilePerm, s.clock())469		if cerr != nil {470			return cerr471		}472		f = created473	} else if err != nil {474		return err475	}476	mu, ok := f.(ninep.Mutable)477	if !ok {478		return ninep.ErrReadOnly479	}480	off := int64(0)481	if appendTo {482		off = mu.Stat().Length483	} else if err := mu.Truncate(0, s.clock()); err != nil {484		return err485	}486	_, err = mu.Write(off, text, s.clock())487	return err488}489490// splitCommands splits on ';' and newlines, respecting rc quoting.491func splitCommands(line string) ([]string, error) {492	out := []string{}493	var b strings.Builder494	quoted := false495	for i := 0; i < len(line); i++ {496		c := line[i]497		if c == '\'' {498			quoted = !quoted499		}500		if !quoted && (c == ';' || c == '\n') {501			out = append(out, b.String())502			b.Reset()503			continue504		}505		b.WriteByte(c)506	}507	if quoted {508		return nil, ErrQuote509	}510	out = append(out, b.String())511	return out, nil512}513514// tokenize splits one command into words using rc's quoting rules.515func tokenize(s string) ([]string, error) {516	toks := []string{}517	i := 0518	for i < len(s) {519		for i < len(s) && (s[i] == ' ' || s[i] == '\t') {520			i++521		}522		if i >= len(s) {523			break524		}525		var b strings.Builder526		quoted := false527		for i < len(s) {528			c := s[i]529			if c == '\'' {530				if quoted && i+1 < len(s) && s[i+1] == '\'' {531					b.WriteByte('\'')532					i += 2533					continue534				}535				quoted = !quoted536				i++537				continue538			}539			if !quoted && (c == ' ' || c == '\t') {540				break541			}542			b.WriteByte(c)543			i++544		}545		if quoted {546			return nil, ErrQuote547		}548		toks = append(toks, b.String())549	}550	return toks, nil551}552553// NewMemShell is the convenience a realm or a test wants: a fresh ram root,554// a namespace over it, and a shell. It exists here rather than in ns so that555// ns keeps no dependency on a particular file server.556func NewMemShell(uid string, mode Mode, now func() int64) (*Shell, *memfs.FS) {557	h := int64(0)558	if now != nil {559		h = now()560	}561	fs := memfs.New(uid, h)562	return New(ns.New(fs.Root()), mode, now), fs563}564

Functions

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

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