From 8082bd5a2dbd2c80d964cd46fb78f9ab22656ba8 Mon Sep 17 00:00:00 2001 From: David Dworken Date: Tue, 19 Dec 2023 15:01:15 -0800 Subject: [PATCH] Add support for single quotes in search queries, and add a heuristic to avoid consuming unclosed quotes --- client/lib/lib.go | 28 ++++++++++++++++++++++++---- client/lib/lib_test.go | 29 +++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/client/lib/lib.go b/client/lib/lib.go index bcde3a4..b2c69cb 100644 --- a/client/lib/lib.go +++ b/client/lib/lib.go @@ -984,9 +984,10 @@ func splitEscaped(query string, separator rune, maxSplit int) []string { var tokens []string splits := 1 runeQuery := []rune(query) - isInQuotedString := false + isInDoubleQuotedString := false + isInSingleQuotedString := false for i := 0; i < len(runeQuery); i++ { - if (maxSplit < 0 || splits < maxSplit) && runeQuery[i] == separator && !isInQuotedString { + if (maxSplit < 0 || splits < maxSplit) && runeQuery[i] == separator && !isInSingleQuotedString && !isInDoubleQuotedString { tokens = append(tokens, string(token)) token = token[:0] splits++ @@ -999,8 +1000,10 @@ func splitEscaped(query string, separator rune, maxSplit int) []string { } i++ token = append(token, runeQuery[i]) - } else if runeQuery[i] == '"' { - isInQuotedString = !isInQuotedString + } else if runeQuery[i] == '"' && !isInSingleQuotedString && !heuristicIgnoreUnclosedQuote(isInDoubleQuotedString, '"', runeQuery, i) { + isInDoubleQuotedString = !isInDoubleQuotedString + } else if runeQuery[i] == '\'' && !isInDoubleQuotedString && !heuristicIgnoreUnclosedQuote(isInSingleQuotedString, '\'', runeQuery, i) { + isInSingleQuotedString = !isInSingleQuotedString } else { token = append(token, runeQuery[i]) } @@ -1009,6 +1012,23 @@ func splitEscaped(query string, separator rune, maxSplit int) []string { return tokens } +func heuristicIgnoreUnclosedQuote(isCurrentlyInQuotedString bool, quoteType rune, query []rune, idx int) bool { + if isCurrentlyInQuotedString { + // We're already in a quoted string, so the heuristic doesn't apply + return false + } + idx++ + for idx < len(query) { + if query[idx] == quoteType { + // There is a close quote, so the heuristic doesn't apply + return false + } + idx++ + } + // There is no unclosed quote, so we apply the heuristic and ignore the single quote + return true +} + func containsUnescaped(query string, token string) bool { runeQuery := []rune(query) for i := 0; i < len(runeQuery); i++ { diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index 29b8177..dbc644b 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -269,24 +269,49 @@ func TestSplitEscaped(t *testing.T) { limit int expected []string }{ + // Basic tests {"foo bar", ' ', 2, []string{"foo", "bar"}}, {"foo bar baz", ' ', 2, []string{"foo", "bar baz"}}, {"foo bar baz", ' ', 3, []string{"foo", "bar", "baz"}}, {"foo bar baz", ' ', 1, []string{"foo bar baz"}}, {"foo bar baz", ' ', -1, []string{"foo", "bar", "baz"}}, + // Tests for escaping {"foo\\ bar baz", ' ', -1, []string{"foo bar", "baz"}}, {"foo\\bar baz", ' ', -1, []string{"foobar", "baz"}}, {"foo\\bar baz foob", ' ', 2, []string{"foobar", "baz foob"}}, {"foo\\ bar\\ baz", ' ', -1, []string{"foo bar baz"}}, {"foo\\ bar\\ baz", ' ', -1, []string{"foo bar ", "baz"}}, + // Tests for single quotes + {"'foo bar'", ' ', -1, []string{"foo bar"}}, + {"'foo bar' ' '", ' ', -1, []string{"foo bar", " "}}, + {"'foo bar baz' and", ' ', -1, []string{"foo bar baz", "and"}}, + {"'foo bar baz", ' ', -1, []string{"'foo", "bar", "baz"}}, + {"'foo bar baz\\''", ' ', -1, []string{"foo bar baz'"}}, + {"cwd:'foo bar :baz\\''", ':', -1, []string{"cwd", "foo bar :baz'"}}, + {"cwd:'foo bar :baz\\''", ' ', -1, []string{"cwd:foo bar :baz'"}}, + // Tests for double quotes {"\"foo bar\"", ' ', -1, []string{"foo bar"}}, {"\"foo bar\" \" \"", ' ', -1, []string{"foo bar", " "}}, {"\"foo bar baz\" and", ' ', -1, []string{"foo bar baz", "and"}}, - {"\"foo bar baz\" and", ' ', -1, []string{"foo bar baz", "and"}}, - {"\"foo bar baz", ' ', -1, []string{"foo bar baz"}}, + {"\"foo bar baz", ' ', -1, []string{"\"foo", "bar", "baz"}}, {"\"foo bar baz\\\"\"", ' ', -1, []string{"foo bar baz\""}}, {"cwd:\"foo bar :baz\\\"\"", ':', -1, []string{"cwd", "foo bar :baz\""}}, {"cwd:\"foo bar :baz\\\"\"", ' ', -1, []string{"cwd:foo bar :baz\""}}, + // Tests for complex quotes + {"\"foo'bar\"", ' ', -1, []string{"foo'bar"}}, + {"'foo\"bar'", ' ', -1, []string{"foo\"bar"}}, + {"\"foo'bar", ' ', -1, []string{"\"foo'bar"}}, + {"'foo\"bar", ' ', -1, []string{"'foo\"bar"}}, + {"\"foo'\\\"bar\"", ' ', -1, []string{"foo'\"bar"}}, + {"'foo\"\\'bar'", ' ', -1, []string{"foo\"'bar"}}, + {"''", ' ', -1, []string{""}}, + {"\"\"", ' ', -1, []string{""}}, + {"\\\"", ' ', -1, []string{"\""}}, + {"\\'", ' ', -1, []string{"'"}}, + // Tests the behavior of quotes with + {"it's", ' ', -1, []string{"it's"}}, + {"'foo bar", ' ', -1, []string{"'foo", "bar"}}, + // Tests for various complex/interesting escaping {"ls \\-foo", ' ', -1, []string{"ls", "\\-foo"}}, {"ls \\-foo \\a \\\\", ' ', -1, []string{"ls", "\\-foo", "a", "\\\\"}}, {"foo:bar", ':', -1, []string{"foo", "bar"}},