krz/gitbay
A CLI-first git forge.
clone: git clone https://gitbay.org/krz/gitbay.git
repo-descriptions: internal/protocol/tokenize.go · raw
1package protocol
2
3import (
4 "errors"
5 "fmt"
6 "strings"
7)
8
9// Tokenize splits an SSH exec command string into argv using POSIX
10// shell-word rules: whitespace separates words; single quotes preserve
11// everything literally; double quotes preserve everything except backslash
12// before \, ", or $; a bare backslash escapes the next character. There is
13// no expansion of any kind — no globbing, variables, or substitution. The
14// client's shell has already applied one layer of quoting before the string
15// reaches us.
16func Tokenize(s string) ([]string, error) {
17 var argv []string
18 var cur strings.Builder
19 inWord := false
20
21 i := 0
22 for i < len(s) {
23 c := s[i]
24 switch {
25 case c == ' ' || c == '\t' || c == '\n':
26 if inWord {
27 argv = append(argv, cur.String())
28 cur.Reset()
29 inWord = false
30 }
31 i++
32 case c == '\'':
33 inWord = true
34 end := strings.IndexByte(s[i+1:], '\'')
35 if end < 0 {
36 return nil, errors.New("unterminated single quote")
37 }
38 cur.WriteString(s[i+1 : i+1+end])
39 i += end + 2
40 case c == '"':
41 inWord = true
42 i++
43 closed := false
44 for i < len(s) {
45 c = s[i]
46 if c == '"' {
47 closed = true
48 i++
49 break
50 }
51 if c == '\\' && i+1 < len(s) {
52 switch s[i+1] {
53 case '\\', '"', '$', '`':
54 cur.WriteByte(s[i+1])
55 i += 2
56 continue
57 }
58 }
59 cur.WriteByte(c)
60 i++
61 }
62 if !closed {
63 return nil, errors.New("unterminated double quote")
64 }
65 case c == '\\':
66 if i+1 >= len(s) {
67 return nil, errors.New("trailing backslash")
68 }
69 inWord = true
70 cur.WriteByte(s[i+1])
71 i += 2
72 case c == '$' || c == '`' || c == ';' || c == '&' || c == '|' ||
73 c == '<' || c == '>' || c == '(' || c == ')' || c == '*' ||
74 c == '?' || c == '[' || c == '#' || c == '~':
75 // Unquoted shell metacharacters are rejected outright rather
76 // than passed through: there is no shell here, and silently
77 // treating them as literals would mask client quoting bugs.
78 return nil, fmt.Errorf("unquoted shell metacharacter %q", c)
79 default:
80 inWord = true
81 cur.WriteByte(c)
82 i++
83 }
84 }
85 if inWord {
86 argv = append(argv, cur.String())
87 }
88 return argv, nil
89}