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}