diff options
| -rw-r--r-- | go.mod | 5 | ||||
| -rw-r--r-- | go.sum | 2 | ||||
| -rw-r--r-- | rsh.go | 182 | ||||
| -rw-r--r-- | rsh_test.go | 93 | ||||
| -rw-r--r-- | testdata/fuzz/FuzzSplitTokens/5e6d51ab3f025967 | 2 |
5 files changed, 284 insertions, 0 deletions
@@ -0,0 +1,5 @@ +module roseh.moe/cmd/rsh + +go 1.26.5 + +require github.com/google/go-cmp v0.7.0 @@ -0,0 +1,2 @@ +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -0,0 +1,182 @@ +package main + +import ( + "bufio" + "fmt" + "os" +) + +type lexer struct { + insertConcat bool +} + +func (l *lexer) splitTokens(data []byte, atEOF bool) (advance int, token []byte, err error) { + n := 0 +Whitespace: + for ; n < len(data); n++ { + switch data[n] { + case ' ', '\t': + l.insertConcat = false + case '#': + i := n + 1 + for ; i < len(data) && data[i] != '\n'; i++ { + } + if i < len(data) { + return i + 1, []byte(";"), nil + } + if atEOF { + return i, nil, nil + } + return n, nil, nil + case '\\': + if n+1 < len(data) && data[n+1] == '\n' { + n++ + l.insertConcat = false + continue + } + fallthrough + default: + break Whitespace + } + } + data = data[n:] + if len(data) == 0 { + return n, nil, nil + } + switch data[0] { + case '$': + if l.insertConcat { + l.insertConcat = false + return n, []byte{}, nil + } + i := 1 + for ; i < len(data); i++ { + if b := data[i]; !(b == '_' || '0' <= b && b <= '9' || 'a' <= b && b <= 'z' || 'A' <= b && b <= 'Z') { + break + } + } + if i == len(data) && !atEOF { + return n, nil, nil + } + if i < len(data) && data[i] == '(' { + i++ + } else { + l.insertConcat = true + } + if i == 1 { + return 0, nil, fmt.Errorf(`invalid token "$"`) + } + return n + i, data[:i], nil + case '<', '>': + if len(data) == 1 && !atEOF { + return n, nil, nil + } + l.insertConcat = false + if len(data) > 1 && data[1] == '(' { + return n + 2, data[:2], nil + } + return n + 1, data[:1], nil + case '\'': + if l.insertConcat { + l.insertConcat = false + return n, []byte{}, nil + } + i := 1 + for ; i < len(data); i++ { + if data[i] == '\'' { + if i+1 == len(data) && !atEOF { + return n, nil, nil + } + if i+1 == len(data) || data[i+1] != '\'' { + break + } + } + } + if i == len(data) { + if atEOF { + return 0, nil, fmt.Errorf("unterminated string") + } + return n, nil, nil + } + i++ + l.insertConcat = true + return n + i, data[:i], nil + case '\n': + l.insertConcat = false + return n + 1, []byte(";"), nil + case '&': + if len(data) == 1 && !atEOF { + return n, nil, nil + } + l.insertConcat = false + if len(data) > 1 && data[1] == '&' { + return n + 2, data[:2], nil + } + return n + 1, data[:1], nil + case '|': + if len(data) == 1 && !atEOF { + return n, nil, nil + } + l.insertConcat = false + if len(data) > 1 && data[1] == '|' { + return n + 2, data[:2], nil + } + return n + 1, data[:1], nil + case ';': + l.insertConcat = false + return n + 1, data[:1], nil + case ')': + l.insertConcat = true + return n + 1, data[:1], nil + case '*': + if l.insertConcat { + l.insertConcat = false + return n, []byte{}, nil + } + l.insertConcat = true + return n + 1, data[:1], nil + default: + if l.insertConcat { + l.insertConcat = false + return n, []byte{}, nil + } + i := 1 + Word: + for ; i < len(data); i++ { + switch data[i] { + case '\\': + if i+1 == len(data) && !atEOF { + return n, nil, nil + } + if i+1 < len(data) && data[i+1] == '\n' { + break Word + } + case '&', '|', '*', ')', ';', '$', '<', '>', '\'', '#', ' ', '\t', '\n': + break Word + } + } + if i == len(data) && !atEOF { + return n, nil, nil + } + l.insertConcat = true + return n + i, data[:i], nil + } +} + +func splitTokens() bufio.SplitFunc { + return new(lexer).splitTokens +} + +func main() { + scanner := bufio.NewScanner(os.Stdin) + scanner.Split(splitTokens()) + var tokens []string + for scanner.Scan() { + tokens = append(tokens, scanner.Text()) + } + if err := scanner.Err(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + fmt.Printf("%q\n", tokens) +} diff --git a/rsh_test.go b/rsh_test.go new file mode 100644 index 0000000..988a2d8 --- /dev/null +++ b/rsh_test.go @@ -0,0 +1,93 @@ +package main + +import ( + "bufio" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestSplitTokens(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + desc string + input string + want []string + }{{ + desc: "SingleWord", + input: "a", + want: []string{"a"}, + }, { + desc: "SplitWords", + input: "a b c", + want: []string{"a", "b", "c"}, + }, { + desc: "Star", + input: "a*b", + want: []string{"a", "", "*", "", "b"}, + }, { + desc: "Background", + input: "a&b", + want: []string{"a", "&", "b"}, + }, { + desc: "Pipe", + input: "a|b", + want: []string{"a", "|", "b"}, + }, { + desc: "And", + input: "a&&b", + want: []string{"a", "&&", "b"}, + }, { + desc: "Newline", + input: "a\n", + want: []string{"a", ";"}, + }, { + desc: "CaptureOutputAsFile", + input: "a<(b)", + want: []string{"a", "<(", "b", ")"}, + }, { + desc: "Substitution", + input: "a$(b)c", + want: []string{"a", "", "$(", "b", ")", "", "c"}, + }, { + desc: "Comment", + input: "abc#de;fg", + want: []string{"abc"}, + }, { + desc: "CommentNewline", + input: "a#b\n", + want: []string{"a", ";"}, + }, { + desc: "BackslashEOF", + input: `a\`, + want: []string{`a\`}, + }} { + t.Run(tc.desc, func(t *testing.T) { + scanner := bufio.NewScanner(strings.NewReader(tc.input)) + scanner.Split(splitTokens()) + var got []string + for scanner.Scan() { + got = append(got, scanner.Text()) + } + if err := scanner.Err(); err != nil { + t.Fatalf("splitTokens failed: %s", err) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("splitTokens produced unexpected diff (-want +got):\n%s", diff) + } + }) + } +} + +func FuzzSplitTokens(f *testing.F) { + testcases := []string{"a", "a b c", "a*b", "a&b", "a|b", "a&&b", "a\n", "a<(b)", "a$(b)c", "abc#de;fg", "a#b\n", `a\`} + for _, tc := range testcases { + f.Add([]byte(tc)) + } + f.Fuzz(func(t *testing.T, input []byte) { + splitTokens()(input, true) + splitTokens()(input, false) + }) +} diff --git a/testdata/fuzz/FuzzSplitTokens/5e6d51ab3f025967 b/testdata/fuzz/FuzzSplitTokens/5e6d51ab3f025967 new file mode 100644 index 0000000..0f66b13 --- /dev/null +++ b/testdata/fuzz/FuzzSplitTokens/5e6d51ab3f025967 @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("'0000000") |
