From d3b6f7584885bece46c611cf2b4c669e4d1c4319 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Mon, 3 Apr 2017 08:57:33 +0200 Subject: [PATCH] sftp: Add SplitShellArgs --- src/restic/backend/sftp/split.go | 73 ++++++++++++++++++ src/restic/backend/sftp/split_test.go | 105 ++++++++++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 src/restic/backend/sftp/split.go create mode 100644 src/restic/backend/sftp/split_test.go diff --git a/src/restic/backend/sftp/split.go b/src/restic/backend/sftp/split.go new file mode 100644 index 000000000..9be4a5464 --- /dev/null +++ b/src/restic/backend/sftp/split.go @@ -0,0 +1,73 @@ +package sftp + +import ( + "errors" + "unicode" +) + +const data = `"foo" "bar" baz "test argument" another 'test arg' "last \" argument" 'another \" last argument'` + +// shellSplitter splits a command string into separater arguments. It supports +// single and double quoted strings. +type shellSplitter struct { + quote rune + lastChar rune +} + +func (s *shellSplitter) isSplitChar(c rune) bool { + // only test for quotes if the last char was not a backslash + if s.lastChar != '\\' { + + // quote ended + if s.quote != 0 && c == s.quote { + s.quote = 0 + return true + } + + // quote starts + if s.quote == 0 && (c == '"' || c == '\'') { + s.quote = c + return true + } + } + + s.lastChar = c + + // within quote + if s.quote != 0 { + return false + } + + // outside quote + return c == '\\' || unicode.IsSpace(c) +} + +// SplitShellArgs returns the list of arguments from a shell command string. +func SplitShellArgs(data string) (list []string, err error) { + s := &shellSplitter{} + + // derived from strings.SplitFunc + fieldStart := -1 // Set to -1 when looking for start of field. + for i, rune := range data { + if s.isSplitChar(rune) { + if fieldStart >= 0 { + list = append(list, data[fieldStart:i]) + fieldStart = -1 + } + } else if fieldStart == -1 { + fieldStart = i + } + } + if fieldStart >= 0 { // Last field might end at EOF. + list = append(list, data[fieldStart:]) + } + + switch s.quote { + case '\'': + return nil, errors.New("single-quoted string not terminated") + case '"': + return nil, errors.New("double-quoted string not terminated") + } + + return list, nil +} diff --git a/src/restic/backend/sftp/split_test.go b/src/restic/backend/sftp/split_test.go new file mode 100644 index 000000000..f2f3cd5f5 --- /dev/null +++ b/src/restic/backend/sftp/split_test.go @@ -0,0 +1,105 @@ +package sftp + +import ( + "reflect" + "testing" +) + +func TestShellSplitter(t *testing.T) { + var tests = []struct { + data string + want []string + }{ + { + `foo`, + []string{"foo"}, + }, + { + `'foo'`, + []string{"foo"}, + }, + { + `foo bar baz`, + []string{"foo", "bar", "baz"}, + }, + { + `foo 'bar' baz`, + []string{"foo", "bar", "baz"}, + }, + { + `foo 'bar box' baz`, + []string{"foo", "bar box", "baz"}, + }, + { + `"bar 'box'" baz`, + []string{"bar 'box'", "baz"}, + }, + { + `'bar "box"' baz`, + []string{`bar "box"`, "baz"}, + }, + { + `\"bar box baz`, + []string{`"bar`, "box", "baz"}, + }, + { + `"bar/foo/x" "box baz"`, + []string{"bar/foo/x", "box baz"}, + }, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + res, err := SplitShellArgs(test.data) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(res, test.want) { + t.Fatalf("wrong data returned, want:\n %#v\ngot:\n %#v", + test.want, res) + } + }) + } +} + +func TestShellSplitterInvalid(t *testing.T) { + var tests = []struct { + data string + err string + }{ + { + "foo'", + "single-quoted string not terminated", + }, + { + `foo"`, + "double-quoted string not terminated", + }, + { + "foo 'bar", + "single-quoted string not terminated", + }, + { + `foo "bar`, + "double-quoted string not terminated", + }, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + res, err := SplitShellArgs(test.data) + if err == nil { + t.Fatalf("expected error not found: %v", test.err) + } + + if err.Error() != test.err { + t.Fatalf("expected error not found, want:\n %q\ngot:\n %q", test.err, err.Error()) + } + + if len(res) > 0 { + t.Fatalf("splitter returned fields from invalid data: %v", res) + } + }) + } +}