krz/gitbay
A CLI-first git forge.
clone: git clone https://gitbay.org/krz/gitbay.git
repo-descriptions: internal/protocol/tokenize_test.go · raw
1package protocol
2
3import (
4 "reflect"
5 "strings"
6 "testing"
7)
8
9func TestTokenize(t *testing.T) {
10 cases := []struct {
11 in string
12 want []string
13 }{
14 {`whoami --json`, []string{"whoami", "--json"}},
15 {`repo create krz/newthing --private`, []string{"repo", "create", "krz/newthing", "--private"}},
16 {`git-upload-pack '/krz/hutch.git'`, []string{"git-upload-pack", "/krz/hutch.git"}},
17 {`issue create --title 'a b c'`, []string{"issue", "create", "--title", "a b c"}},
18 {`issue create --title "a \"b\" c"`, []string{"issue", "create", "--title", `a "b" c`}},
19 {`a\ b`, []string{"a b"}},
20 {`'it''s'`, []string{"its"}},
21 {`"don't"`, []string{"don't"}},
22 {" spaced \t out ", []string{"spaced", "out"}},
23 {`""`, []string{""}},
24 {``, nil},
25 {`--message "line1\nliteral"`, []string{"--message", `line1\nliteral`}},
26 }
27 for _, tc := range cases {
28 got, err := Tokenize(tc.in)
29 if err != nil {
30 t.Errorf("Tokenize(%q) error: %v", tc.in, err)
31 continue
32 }
33 if !reflect.DeepEqual(got, tc.want) {
34 t.Errorf("Tokenize(%q) = %#v, want %#v", tc.in, got, tc.want)
35 }
36 }
37}
38
39func TestTokenizeRejects(t *testing.T) {
40 bad := []string{
41 `echo $(rm -rf /)`,
42 "`id`",
43 `a; b`,
44 `a | b`,
45 `a > f`,
46 `a & b`,
47 `'unterminated`,
48 `"unterminated`,
49 `trailing\`,
50 `glob *`,
51 `~root`,
52 }
53 for _, in := range bad {
54 if got, err := Tokenize(in); err == nil {
55 t.Errorf("Tokenize(%q) = %#v, want error", in, got)
56 }
57 }
58}
59
60// shellQuote quotes one word the way a POSIX client shell would.
61func shellQuote(w string) string {
62 return "'" + strings.ReplaceAll(w, "'", `'\''`) + "'"
63}
64
65// FuzzTokenizeRoundTrip checks that any argv, single-quoted as a client
66// shell would emit it, tokenizes back to the identical argv.
67func FuzzTokenizeRoundTrip(f *testing.F) {
68 f.Add("whoami", "--json", "")
69 f.Add("issue create", "--title", "a 'quoted' \"title\" with $pecial\\chars")
70 f.Add("répo", "\t", "\n\n")
71 f.Fuzz(func(t *testing.T, a, b, c string) {
72 want := []string{a, b, c}
73 quoted := make([]string, len(want))
74 for i, w := range want {
75 quoted[i] = shellQuote(w)
76 }
77 got, err := Tokenize(strings.Join(quoted, " "))
78 if err != nil {
79 t.Fatalf("Tokenize error on %q: %v", strings.Join(quoted, " "), err)
80 }
81 if !reflect.DeepEqual(got, want) {
82 t.Fatalf("round trip: got %#v, want %#v", got, want)
83 }
84 })
85}
86
87// FuzzTokenizeNoPanic feeds arbitrary bytes; Tokenize must return, never panic.
88func FuzzTokenizeNoPanic(f *testing.F) {
89 f.Add(`repo create 'x`)
90 f.Add(`\\\'\"`)
91 f.Fuzz(func(t *testing.T, s string) {
92 _, _ = Tokenize(s)
93 })
94}