carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package runs
2
3import (
4 "errors"
5 "strings"
6 "unicode"
7 "unicode/utf8"
8)
9
10// A `run` service credential carries the command prefixes it may run
11// (service-runs spec § 1). These are the limits on that list, and the one
12// function that decides whether an argv is inside it. The console mints
13// with CheckCommandPrefixes, reads a stored list back through it, and gates
14// a run's create on CommandGranted.
15const (
16 MaxCommandPrefixes = 32
17 MaxCommandWordBytes = 128
18)
19
20var (
21 ErrNoCommands = errors.New("runs: no command prefixes")
22 ErrTooManyCommands = errors.New("runs: more than 32 command prefixes")
23 ErrEmptyCommand = errors.New("runs: a command prefix has no words")
24 ErrEmptyCommandWord = errors.New("runs: a command prefix holds an empty word")
25 ErrCommandWordTooLong = errors.New("runs: a command word over 128 bytes")
26 ErrCommandWordChars = errors.New("runs: a command word holds a space, a comma or a character that cannot be printed")
27)
28
29// CheckCommandPrefixes is the shape every stored command list has: at least
30// one prefix and at most MaxCommandPrefixes, each with at least one word,
31// each word 1 to MaxCommandWordBytes bytes. A zero-word prefix is refused
32// because it would match every argv.
33//
34// A word is also valid UTF-8 made only of printable characters, with no
35// whitespace and no comma (validCommandWord). Whitespace and commas are the
36// text form's separators, so a word holding one would come back as a
37// different grant when a listing is copied into a new mint; a control
38// character could hide a prefix in a terminal listing.
39func CheckCommandPrefixes(prefixes [][]string) error {
40 if len(prefixes) == 0 {
41 return ErrNoCommands
42 }
43 if len(prefixes) > MaxCommandPrefixes {
44 return ErrTooManyCommands
45 }
46 for _, p := range prefixes {
47 if len(p) == 0 {
48 return ErrEmptyCommand
49 }
50 for _, w := range p {
51 if w == "" {
52 return ErrEmptyCommandWord
53 }
54 if len(w) > MaxCommandWordBytes {
55 return ErrCommandWordTooLong
56 }
57 if !validCommandWord(w) {
58 return ErrCommandWordChars
59 }
60 }
61 }
62 return nil
63}
64
65// validCommandWord: valid UTF-8, every rune printable, none a space or a
66// comma. U+FFFD is refused too: it is what invalid UTF-8 becomes once a
67// JSON decoder has replaced it, so refusing it makes a mangled word fail
68// the same way at the API door as at the form.
69func validCommandWord(w string) bool {
70 if !utf8.ValidString(w) {
71 return false
72 }
73 for _, r := range w {
74 if r == ',' || r == utf8.RuneError || unicode.IsSpace(r) || !unicode.IsPrint(r) {
75 return false
76 }
77 }
78 return true
79}
80
81// CommandGranted reports whether argv starts with every word of at least one
82// prefix, compared word for word and exactly: no globbing, no case folding,
83// no substring. It fails closed: a zero-word prefix matches nothing, and so
84// does an empty argv.
85func CommandGranted(argv []string, prefixes [][]string) bool {
86 for _, p := range prefixes {
87 if len(p) == 0 || len(argv) < len(p) {
88 continue
89 }
90 match := true
91 for i, w := range p {
92 if argv[i] != w {
93 match = false
94 break
95 }
96 }
97 if match {
98 return true
99 }
100 }
101 return false
102}
103
104// ParseCommandList reads the text form both mint doors take, the CLI's
105// --commands flag and the console form's field: prefixes separated by
106// commas, a prefix's words by spaces. "import classic,import sync" is two
107// prefixes of two words each. An entry with no words is dropped, the way an
108// empty rung entry is, so a trailing comma grants nothing rather than
109// everything. It returns nil when nothing was named; validation is
110// CheckCommandPrefixes's.
111func ParseCommandList(s string) [][]string {
112 var out [][]string
113 for _, entry := range strings.Split(s, ",") {
114 if words := strings.Fields(entry); len(words) > 0 {
115 out = append(out, words)
116 }
117 }
118 return out
119}
120
121// UnreadableCommands is what a listing shows, in place of a command list,
122// for a run credential whose stored list cannot be read. The console and the
123// CLI both print it.
124const UnreadableCommands = "(unreadable — refused at sign-in)"
125
126// FormatCommandList is a command list for a person to read:
127// "import classic, import sync".
128func FormatCommandList(prefixes [][]string) string {
129 parts := make([]string, len(prefixes))
130 for i, p := range prefixes {
131 parts[i] = strings.Join(p, " ")
132 }
133 return strings.Join(parts, ", ")
134}
135