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

rc.gno

12.87 Kb · 563 lines
  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 cannot
  4// trust. Everything here operates on a gno.land/p/moul/x/plan9/ns namespace
  5// and returns text, so one realm's Render becomes a file browser and one
  6// 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 every
 11// mutating command, which is what lets a realm expose the shell through
 12// Render, where mutating anything would be a bug, while the same code backs a
 13// crossing Exec that may write.
 14//
 15// Quoting follows rc: single quotes, with ” inside a quoted string standing
 16// for one literal quote. Commands are separated by ';' or newlines, and
 17// quoting is respected when splitting them.
 18//
 19// On the first error the run STOPS and returns it. A realm should let that
 20// error panic, so a half-applied command line reverts with the transaction
 21// 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 Research
 24// Center at Bell Labs; the name and the marks are theirs, and the copyright is
 25// held by the Plan 9 Foundation (https://p9f.org). This package is not
 26// affiliated with, endorsed by, or sponsored by them, and contains no Plan 9
 27// 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 a
 29// chain. Full attribution: NOTICE.md at the root of moul/gno-contracts.
 30package rc
 31
 32import (
 33	"errors"
 34	"strconv"
 35	"strings"
 36
 37	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)
 41
 42// Mode decides whether mutating commands are allowed.
 43type Mode uint8
 44
 45// Shell modes.
 46const (
 47	ReadOnly  Mode = 0
 48	ReadWrite Mode = 1
 49)
 50
 51var (
 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)
 61
 62// MaxCommands bounds one run, so a single call cannot loop the VM.
 63const MaxCommands = 32
 64
 65// Shell runs commands against one namespace.
 66type Shell struct {
 67	ns   *ns.Ns
 68	mode Mode
 69	now  func() int64
 70}
 71
 72// New returns a shell over n. now supplies the block height stamped on writes
 73// 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}
 77
 78// Ns returns the namespace the shell operates on.
 79func (s *Shell) Ns() *ns.Ns { return s.ns }
 80
 81func (s *Shell) clock() int64 {
 82	if s.now == nil {
 83		return 0
 84	}
 85	return s.now()
 86}
 87
 88func (s *Shell) writable() error {
 89	if s.mode != ReadWrite {
 90		return ErrReadOnly
 91	}
 92	return nil
 93}
 94
 95// Run executes a command line and returns its output. Execution stops at the
 96// first error.
 97func (s *Shell) Run(line string) (string, error) {
 98	cmds, err := splitCommands(line)
 99	if err != nil {
100		return "", err
101	}
102	if len(cmds) > MaxCommands {
103		return "", errors.New("too many commands in one line")
104	}
105	var out strings.Builder
106	for _, c := range cmds {
107		toks, err := tokenize(c)
108		if err != nil {
109			return out.String(), err
110		}
111		if len(toks) == 0 {
112			continue
113		}
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(), nil
121}
122
123func (s *Shell) run1(toks []string) (string, error) {
124	cmd, args := toks[0], toks[1:]
125	switch cmd {
126	case "help":
127		return help, nil
128	case "pwd":
129		return s.ns.Cwd() + "\n", nil
130	case "cd":
131		return "", s.cd(args)
132	case "ns":
133		return s.ns.String(), nil
134	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 "", ErrUnknown
154}
155
156const help = `bind [-a|-b|-c] new old   make new visible at old (-b before, -a after, -c creates)
157cat file...               print files
158cd [dir]                  change directory
159echo [-n] word... [>|>> file]
160ls [-l] [-u] [path...]    list; -l long, -u collapse union duplicates
161mkdir path...             create directories
162mount                     synonym for bind
163ns                        print the namespace
164pwd                       print the working directory
165rm path...                remove files and empty directories
166stat path...              print the 9P stat of each path
167unmount [new] old         undo a binding
168walk path                 show how each element of path resolves
169`
170
171func (s *Shell) cd(args []string) error {
172	if len(args) == 0 {
173		return s.ns.Cd("/")
174	}
175	if len(args) > 1 {
176		return ErrUsage
177	}
178	return s.ns.Cd(args[0])
179}
180
181func (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 = true
189				case 'u':
190					unique = true
191				default:
192					return "", ErrUsage
193				}
194			}
195			continue
196		}
197		paths = append(paths, a)
198	}
199	if len(paths) == 0 {
200		paths = []string{s.ns.Cwd()}
201	}
202	var out strings.Builder
203	for i, p := range paths {
204		st, err := s.ns.Stat(p)
205		if err != nil {
206			return out.String(), err
207		}
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			continue
217		}
218		ents, err := s.ns.ReadDir(p, unique)
219		if err != nil {
220			return out.String(), err
221		}
222		for _, e := range ents {
223			out.WriteString(entryLine(e, long))
224		}
225	}
226	return out.String(), nil
227}
228
229func entryLine(st ninep.Stat, long bool) string {
230	if long {
231		return st.Line() + "\n"
232	}
233	name := st.Name
234	if st.IsDir() && name != "/" {
235		name += "/"
236	}
237	return name + "\n"
238}
239
240func (s *Shell) cat(args []string) (string, error) {
241	if len(args) == 0 {
242		return "", ErrUsage
243	}
244	var out strings.Builder
245	for _, p := range args {
246		data, err := s.ns.ReadFile(p)
247		if err != nil {
248			return out.String(), err
249		}
250		out.WriteString(data)
251	}
252	return out.String(), nil
253}
254
255func (s *Shell) stat(args []string) (string, error) {
256	if len(args) == 0 {
257		args = []string{s.ns.Cwd()}
258	}
259	var out strings.Builder
260	for _, p := range args {
261		st, err := s.ns.Stat(p)
262		if err != nil {
263			return out.String(), err
264		}
265		members, err := s.ns.Resolve(p)
266		if err != nil {
267			return out.String(), err
268		}
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(), nil
277}
278
279// walk shows the resolution of each element, which is where a namespace stops
280// 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 "", ErrUsage
284	}
285	abs := s.ns.Abs(args[0])
286	var out strings.Builder
287	prefix := "/"
288	report := func(p string) error {
289		st, err := s.ns.Stat(p)
290		if err != nil {
291			return err
292		}
293		members, err := s.ns.Resolve(p)
294		if err != nil {
295			return err
296		}
297		out.WriteString(pad(p, 24) + " " + st.Qid.String() + " " +
298			st.Mode.String() + " union=" + strconv.Itoa(len(members)) + "\n")
299		return nil
300	}
301	if err := report("/"); err != nil {
302		return out.String(), err
303	}
304	for _, e := range ninep.Elems(abs) {
305		prefix = ninep.Join(prefix, e)
306		if err := report(prefix); err != nil {
307			return out.String(), err
308		}
309	}
310	return out.String(), nil
311}
312
313func pad(s string, n int) string {
314	for len(s) < n {
315		s += " "
316	}
317	return s
318}
319
320func (s *Shell) bind(verb string, args []string) error {
321	if err := s.writable(); err != nil {
322		return err
323	}
324	flag := ns.MREPL
325	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.MBEFORE
332				case 'a':
333					flag = flag&^0x3 | ns.MAFTER
334				case 'c':
335					flag |= ns.MCREATE
336				default:
337					return ErrUsage
338				}
339			}
340			continue
341		}
342		rest = append(rest, a)
343	}
344	if len(rest) != 2 {
345		return ErrUsage
346	}
347	return s.ns.BindVerb(verb, rest[0], rest[1], flag)
348}
349
350func (s *Shell) unmount(args []string) error {
351	if err := s.writable(); err != nil {
352		return err
353	}
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 ErrUsage
361}
362
363func (s *Shell) mkdir(args []string) error {
364	if err := s.writable(); err != nil {
365		return err
366	}
367	if len(args) == 0 {
368		return ErrUsage
369	}
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 err
375		}
376		if _, err := target.Create(ninep.Base(abs), ninep.DirPerm, s.clock()); err != nil {
377			return err
378		}
379	}
380	return nil
381}
382
383func (s *Shell) rm(args []string) error {
384	if err := s.writable(); err != nil {
385		return err
386	}
387	if len(args) == 0 {
388		return ErrUsage
389	}
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 err
396		}
397		found, removed := false, false
398		for _, m := range members {
399			if _, err := m.Walk(name); err != nil {
400				continue
401			}
402			found = true
403			mu, ok := m.(ninep.Mutable)
404			if !ok {
405				continue // a read-only server: try the next union member
406			}
407			if err := mu.Remove(name); err != nil {
408				return err
409			}
410			removed = true
411			break
412		}
413		switch {
414		case removed:
415		case found:
416			return ninep.ErrReadOnly
417		default:
418			return ninep.ErrNotExist
419		}
420	}
421	return nil
422}
423
424func (s *Shell) echo(args []string) (string, error) {
425	newline := true
426	words := []string{}
427	redirect, appendTo, dest := false, false, ""
428	i := 0
429	for i < len(args) {
430		a := args[i]
431		switch {
432		case a == "-n" && len(words) == 0 && !redirect:
433			newline = false
434		case a == ">" || a == ">>":
435			if i+1 >= len(args) {
436				return "", ErrUsage
437			}
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, nil
451	}
452	if err := s.writable(); err != nil {
453		return "", err
454	}
455	return "", s.writeTo(dest, text, appendTo)
456}
457
458// writeTo creates or opens dest and writes text, honouring the namespace's
459// 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 terr
467		}
468		created, cerr := target.Create(ninep.Base(abs), ninep.FilePerm, s.clock())
469		if cerr != nil {
470			return cerr
471		}
472		f = created
473	} else if err != nil {
474		return err
475	}
476	mu, ok := f.(ninep.Mutable)
477	if !ok {
478		return ninep.ErrReadOnly
479	}
480	off := int64(0)
481	if appendTo {
482		off = mu.Stat().Length
483	} else if err := mu.Truncate(0, s.clock()); err != nil {
484		return err
485	}
486	_, err = mu.Write(off, text, s.clock())
487	return err
488}
489
490// splitCommands splits on ';' and newlines, respecting rc quoting.
491func splitCommands(line string) ([]string, error) {
492	out := []string{}
493	var b strings.Builder
494	quoted := false
495	for i := 0; i < len(line); i++ {
496		c := line[i]
497		if c == '\'' {
498			quoted = !quoted
499		}
500		if !quoted && (c == ';' || c == '\n') {
501			out = append(out, b.String())
502			b.Reset()
503			continue
504		}
505		b.WriteByte(c)
506	}
507	if quoted {
508		return nil, ErrQuote
509	}
510	out = append(out, b.String())
511	return out, nil
512}
513
514// tokenize splits one command into words using rc's quoting rules.
515func tokenize(s string) ([]string, error) {
516	toks := []string{}
517	i := 0
518	for i < len(s) {
519		for i < len(s) && (s[i] == ' ' || s[i] == '\t') {
520			i++
521		}
522		if i >= len(s) {
523			break
524		}
525		var b strings.Builder
526		quoted := false
527		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 += 2
533					continue
534				}
535				quoted = !quoted
536				i++
537				continue
538			}
539			if !quoted && (c == ' ' || c == '\t') {
540				break
541			}
542			b.WriteByte(c)
543			i++
544		}
545		if quoted {
546			return nil, ErrQuote
547		}
548		toks = append(toks, b.String())
549	}
550	return toks, nil
551}
552
553// 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 that
555// 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), fs
563}