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}