diff --git a/src/common.c b/src/common.c index eb1d1e8a0..65ec0db42 100644 --- a/src/common.c +++ b/src/common.c @@ -543,26 +543,32 @@ _get_file_or_linked(gchar* loc) char* strip_arg_quotes(const char* const input) { - char* unquoted = strdup(input); - if (unquoted == NULL) { + if (input == NULL) { return NULL; } - // Remove starting quote if it exists - if (strchr(unquoted, '"')) { - if (strchr(unquoted, ' ') + 1 == strchr(unquoted, '"')) { - memmove(strchr(unquoted, '"'), strchr(unquoted, '"') + 1, strchr(unquoted, '\0') - strchr(unquoted, '"')); - } - } - - // Remove ending quote if it exists - if (strchr(unquoted, '"')) { - if (strchr(unquoted, '\0') - 1 == strchr(unquoted, '"')) { - memmove(strchr(unquoted, '"'), strchr(unquoted, '"') + 1, strchr(unquoted, '\0') - strchr(unquoted, '"')); + // Unescape and strip quotes + GString* unescaped = g_string_new(""); + for (const char* p = input; *p; p++) { + if (*p == '\\' && (*(p + 1) != '\0')) { + p++; + g_string_append_c(unescaped, *p); + } else if (*p == '"') { + // Only strip if it's the first char or preceded by a space + if (p == input || *(p - 1) == ' ') { + continue; + } + // Or if it's the last char + if (*(p + 1) == '\0') { + continue; + } + g_string_append_c(unescaped, *p); + } else { + g_string_append_c(unescaped, *p); } } - return unquoted; + return g_string_free(unescaped, FALSE); } gboolean diff --git a/src/tools/autocomplete.c b/src/tools/autocomplete.c index cbbe856db..990df83f7 100644 --- a/src/tools/autocomplete.c +++ b/src/tools/autocomplete.c @@ -234,7 +234,16 @@ autocomplete_complete(Autocomplete ac, const gchar* search_str, gboolean quote, FREE_SET_NULL(ac->search_str); } - ac->search_str = strdup(search_str); + // unescape search string + GString* unescaped = g_string_new(""); + for (const char* p = search_str; *p; p++) { + if (*p == '\\' && (*(p + 1) != '\0')) { + p++; + } + g_string_append_c(unescaped, *p); + } + ac->search_str = g_string_free(unescaped, FALSE); + found = _search(ac, ac->items, quote, NEXT); return found; @@ -403,7 +412,15 @@ _search(Autocomplete ac, GList* curr, gboolean quote, search_direction direction // if contains space, quote before returning if (quote && g_strrstr(curr->data, " ")) { - return g_strdup_printf("\"%s\"", (gchar*)curr->data); + GString* escaped = g_string_new("\""); + for (const char* p = curr->data; *p; p++) { + if (*p == '"' || *p == '\\') { + g_string_append_c(escaped, '\\'); + } + g_string_append_c(escaped, *p); + } + g_string_append_c(escaped, '"'); + return g_string_free(escaped, FALSE); // otherwise just return the string } else { return strdup(curr->data); diff --git a/src/tools/parser.c b/src/tools/parser.c index 940680887..ab5c77cb2 100644 --- a/src/tools/parser.c +++ b/src/tools/parser.c @@ -32,8 +32,7 @@ _parse_args_helper(const char* const inp, int min, int max, gboolean* result, gb gboolean in_token = FALSE; gboolean in_freetext = FALSE; gboolean in_quotes = FALSE; - char* token_start = ©[0]; - int token_size = 0; + GString* current_token = g_string_new(""); int num_tokens = 0; GSList* tokens = NULL; @@ -42,6 +41,23 @@ _parse_args_helper(const char* const inp, int min, int max, gboolean* result, gb gchar* curr_ch = g_utf8_offset_to_pointer(copy, i); gunichar curr_uni = g_utf8_get_char(curr_ch); + if (curr_uni == '\\' && (i + 1 < inp_size)) { + if (!in_token) { + in_token = TRUE; + if (with_freetext) { + num_tokens++; + } + if (with_freetext && (num_tokens == max + 1)) { + in_freetext = TRUE; + } + } + i++; + gchar* next_ch = g_utf8_offset_to_pointer(copy, i); + gunichar next_uni = g_utf8_get_char(next_ch); + g_string_append_unichar(current_token, next_uni); + continue; + } + if (!in_token) { if (curr_uni == ' ') { continue; @@ -55,58 +71,61 @@ _parse_args_helper(const char* const inp, int min, int max, gboolean* result, gb } else if (curr_uni == '"') { in_quotes = TRUE; i++; - gchar* next_ch = g_utf8_next_char(curr_ch); - gunichar next_uni = g_utf8_get_char(next_ch); - - if (next_uni == '"') { - tokens = g_slist_append(tokens, g_strndup(curr_ch, - 0)); - token_size = 0; + if (i < inp_size) { + gchar* next_ch = g_utf8_offset_to_pointer(copy, i); + gunichar next_uni = g_utf8_get_char(next_ch); + + if (next_uni == '"') { + tokens = g_slist_append(tokens, g_strdup("")); + in_token = FALSE; + in_quotes = FALSE; + } else if (next_uni == '\\' && (i + 1 < inp_size)) { + i++; + gchar* escaped_ch = g_utf8_offset_to_pointer(copy, i); + gunichar escaped_uni = g_utf8_get_char(escaped_ch); + g_string_append_unichar(current_token, escaped_uni); + } else { + g_string_append_unichar(current_token, next_uni); + } + } else { + // trailing quote + tokens = g_slist_append(tokens, g_strdup("")); in_token = FALSE; in_quotes = FALSE; - } else { - token_start = next_ch; - token_size += g_unichar_to_utf8(next_uni, NULL); } } - if (curr_uni == '"') { - gchar* next_ch = g_utf8_next_char(curr_ch); - token_start = next_ch; - } else { - token_start = curr_ch; - token_size += g_unichar_to_utf8(curr_uni, NULL); + if (in_token && curr_uni != '"') { + g_string_append_unichar(current_token, curr_uni); } } } else { if (in_quotes) { if (curr_uni == '"') { - tokens = g_slist_append(tokens, g_strndup(token_start, - token_size)); - token_size = 0; + tokens = g_slist_append(tokens, g_string_free(current_token, FALSE)); + current_token = g_string_new(""); in_token = FALSE; in_quotes = FALSE; } else { - if (curr_uni != '"') { - token_size += g_unichar_to_utf8(curr_uni, NULL); - } + g_string_append_unichar(current_token, curr_uni); } } else { if (with_freetext && in_freetext) { - token_size += g_unichar_to_utf8(curr_uni, NULL); + g_string_append_unichar(current_token, curr_uni); } else if (curr_uni == ' ') { - tokens = g_slist_append(tokens, g_strndup(token_start, - token_size)); - token_size = 0; + tokens = g_slist_append(tokens, g_string_free(current_token, FALSE)); + current_token = g_string_new(""); in_token = FALSE; } else { - token_size += g_unichar_to_utf8(curr_uni, NULL); + g_string_append_unichar(current_token, curr_uni); } } } } if (in_token) { - tokens = g_slist_append(tokens, g_strndup(token_start, token_size)); + tokens = g_slist_append(tokens, g_string_free(current_token, FALSE)); + } else { + g_string_free(current_token, TRUE); } int num = g_slist_length(tokens) - 1; @@ -245,9 +264,23 @@ count_tokens(const char* const string) gchar* curr_ch = g_utf8_offset_to_pointer(string, i); gunichar curr_uni = g_utf8_get_char(curr_ch); + if (curr_uni == '\\' && (i + 1 < length)) { + i++; + continue; + } + if (curr_uni == ' ') { if (!in_quotes) { num_tokens++; + while (i + 1 < length) { + gchar* next_ch = g_utf8_offset_to_pointer(string, i + 1); + gunichar next_uni = g_utf8_get_char(next_ch); + if (next_uni == ' ') { + i++; + } else { + break; + } + } } } else if (curr_uni == '"') { if (in_quotes) { @@ -276,15 +309,37 @@ get_start(const char* const string, int tokens) gchar* curr_ch = g_utf8_offset_to_pointer(string, i); gunichar curr_uni = g_utf8_get_char(curr_ch); + if (curr_uni == '\\' && (i + 1 < length)) { + if (num_tokens < tokens) { + g_string_append_unichar(result, curr_uni); + } + i++; + curr_ch = g_utf8_offset_to_pointer(string, i); + curr_uni = g_utf8_get_char(curr_ch); + if (num_tokens < tokens) { + g_string_append_unichar(result, curr_uni); + } + continue; + } + if (num_tokens < tokens) { - auto_gchar gchar* uni_char = g_malloc(7); - int len = g_unichar_to_utf8(curr_uni, uni_char); - uni_char[len] = '\0'; - g_string_append(result, uni_char); + g_string_append_unichar(result, curr_uni); } if (curr_uni == ' ') { if (!in_quotes) { num_tokens++; + while (i + 1 < length) { + gchar* next_ch = g_utf8_offset_to_pointer(string, i + 1); + gunichar next_uni = g_utf8_get_char(next_ch); + if (next_uni == ' ') { + if (num_tokens <= tokens) { + g_string_append_unichar(result, next_uni); + } + i++; + } else { + break; + } + } } } else if (curr_uni == '"') { if (in_quotes) { diff --git a/tests/unittests/test_common.c b/tests/unittests/test_common.c index 6a7bbb9f7..1458155b3 100644 --- a/tests/unittests/test_common.c +++ b/tests/unittests/test_common.c @@ -336,6 +336,18 @@ strip_arg_quotes__returns__stripped_both(void** state) free(result); } +void +strip_arg_quotes__unescapes(void** state) +{ + char* input = "\"Thor \\\"The Thunderer\\\" Odinson\""; + + char* result = strip_arg_quotes(input); + + assert_string_equal("Thor \"The Thunderer\" Odinson", result); + + free(result); +} + void valid_tls_policy_option__is__correct_for_various_inputs(void** state) { diff --git a/tests/unittests/test_common.h b/tests/unittests/test_common.h index c3e97bf84..b552aa9ef 100644 --- a/tests/unittests/test_common.h +++ b/tests/unittests/test_common.h @@ -43,6 +43,7 @@ void strip_arg_quotes__returns__original_when_no_quotes(void** state); void strip_arg_quotes__returns__stripped_first(void** state); void strip_arg_quotes__returns__stripped_last(void** state); void strip_arg_quotes__returns__stripped_both(void** state); +void strip_arg_quotes__unescapes(void** state); void prof_occurrences__tests__partial(void** state); void prof_occurrences__tests__whole(void** state); diff --git a/tests/unittests/tools/test_autocomplete.c b/tests/unittests/tools/test_autocomplete.c index 45f03ebec..8c81c45bc 100644 --- a/tests/unittests/tools/test_autocomplete.c +++ b/tests/unittests/tools/test_autocomplete.c @@ -345,3 +345,31 @@ autocomplete_complete__returns__regular_ascii(void** state) autocomplete_free(ac); free(result); } + +void +autocomplete_complete__handles__escaped_spaces(void** state) +{ + Autocomplete ac = autocomplete_new(); + autocomplete_add(ac, "Thor Odinson"); + + char* result = autocomplete_complete(ac, "Thor\\ ", TRUE, FALSE); + + assert_string_equal("\"Thor Odinson\"", result); + + autocomplete_free(ac); + free(result); +} + +void +autocomplete_complete__handles__escaped_quotes(void** state) +{ + Autocomplete ac = autocomplete_new(); + autocomplete_add(ac, "Thor \"The Thunderer\" Odinson"); + + char* result = autocomplete_complete(ac, "Thor \\\"", TRUE, FALSE); + + assert_string_equal("\"Thor \\\"The Thunderer\\\" Odinson\"", result); + + autocomplete_free(ac); + free(result); +} diff --git a/tests/unittests/tools/test_autocomplete.h b/tests/unittests/tools/test_autocomplete.h index 09ec200d3..c923eb8e0 100644 --- a/tests/unittests/tools/test_autocomplete.h +++ b/tests/unittests/tools/test_autocomplete.h @@ -4,6 +4,8 @@ void autocomplete_complete__returns__null_when_empty(void** state); void autocomplete_reset__updates__after_create(void** state); void autocomplete_complete__returns__null_after_create(void** state); +void autocomplete_complete__handles__escaped_spaces(void** state); +void autocomplete_complete__handles__escaped_quotes(void** state); void autocomplete_create_list__returns__null_after_create(void** state); void autocomplete_add__updates__one_and_complete(void** state); void autocomplete_complete__returns__first_of_two(void** state); diff --git a/tests/unittests/tools/test_parser.c b/tests/unittests/tools/test_parser.c index 3a42c1694..13248184e 100644 --- a/tests/unittests/tools/test_parser.c +++ b/tests/unittests/tools/test_parser.c @@ -342,6 +342,20 @@ parse_args_with_freetext__returns__quoted_freetext(void** state) g_strfreev(args); } +void +parse_args_with_freetext__returns__quoted_start_of_freetext(void** state) +{ + char* inp = "/cmd arg1 \"arg2 with space\" more text"; + gboolean result = FALSE; + gchar** args = parse_args_with_freetext(inp, 1, 2, &result); + + assert_true(result); + assert_int_equal(2, g_strv_length(args)); + assert_string_equal("arg1", args[0]); + assert_string_equal("\"arg2 with space\" more text", args[1]); + g_strfreev(args); +} + void parse_args_with_freetext__returns__third_arg_quoted(void** state) { @@ -390,6 +404,48 @@ parse_args_with_freetext__returns__second_and_third_arg_quoted(void** state) g_strfreev(args); } +void +parse_args__returns__escaped_quotes(void** state) +{ + char* inp = "/cmd \"Thor \\\"The Thunderer\\\" Odinson\" arg2"; + gboolean result = FALSE; + gchar** args = parse_args(inp, 2, 2, &result); + + assert_true(result); + assert_int_equal(2, g_strv_length(args)); + assert_string_equal("Thor \"The Thunderer\" Odinson", args[0]); + assert_string_equal("arg2", args[1]); + g_strfreev(args); +} + +void +parse_args__returns__escaped_spaces(void** state) +{ + char* inp = "/cmd Thor\\ The\\ Thunderer\\ Odinson arg2"; + gboolean result = FALSE; + gchar** args = parse_args(inp, 2, 2, &result); + + assert_true(result); + assert_int_equal(2, g_strv_length(args)); + assert_string_equal("Thor The Thunderer Odinson", args[0]); + assert_string_equal("arg2", args[1]); + g_strfreev(args); +} + +void +parse_args__returns__escaped_backslash(void** state) +{ + char* inp = "/cmd \"Thor \\\\ Odinson\" arg2"; + gboolean result = FALSE; + gchar** args = parse_args(inp, 2, 2, &result); + + assert_true(result); + assert_int_equal(2, g_strv_length(args)); + assert_string_equal("Thor \\ Odinson", args[0]); + assert_string_equal("arg2", args[1]); + g_strfreev(args); +} + void count_tokens__returns__one_token(void** state) { @@ -453,6 +509,24 @@ count_tokens__returns__two_tokens_both_quoted(void** state) assert_int_equal(2, result); } +void +count_tokens__handles__escapes(void** state) +{ + char* inp = "one\\ two \"three \\\" four\""; + int result = count_tokens(inp); + + assert_int_equal(2, result); +} + +void +count_tokens__handles__multiple_spaces(void** state) +{ + char* inp = "one two"; + int result = count_tokens(inp); + + assert_int_equal(2, result); +} + void get_start__returns__first_of_one(void** state) { @@ -513,6 +587,26 @@ get_start__returns__first_two_of_three_first_and_second_quoted(void** state) g_free(result); } +void +get_start__handles__escapes(void** state) +{ + char* inp = "one\\ two three"; + char* result = get_start(inp, 2); + + assert_string_equal("one\\ two ", result); + g_free(result); +} + +void +get_start__handles__multiple_spaces(void** state) +{ + char* inp = "one two"; + char* result = get_start(inp, 2); + + assert_string_equal("one ", result); + g_free(result); +} + void parse_options__returns__empty_hashmap_when_none(void** state) { diff --git a/tests/unittests/tools/test_parser.h b/tests/unittests/tools/test_parser.h index 49cfe7840..f06331420 100644 --- a/tests/unittests/tools/test_parser.h +++ b/tests/unittests/tools/test_parser.h @@ -29,6 +29,9 @@ void parse_args_with_freetext__returns__quoted_freetext(void** state); void parse_args_with_freetext__returns__third_arg_quoted(void** state); void parse_args_with_freetext__returns__second_arg_quoted(void** state); void parse_args_with_freetext__returns__second_and_third_arg_quoted(void** state); +void parse_args__returns__escaped_quotes(void** state); +void parse_args__returns__escaped_spaces(void** state); +void parse_args__returns__escaped_backslash(void** state); void count_tokens__returns__one_token(void** state); void count_tokens__returns__one_token_quoted_no_whitespace(void** state); void count_tokens__returns__one_token_quoted_with_whitespace(void** state); @@ -36,12 +39,16 @@ void count_tokens__returns__two_tokens(void** state); void count_tokens__returns__two_tokens_first_quoted(void** state); void count_tokens__returns__two_tokens_second_quoted(void** state); void count_tokens__returns__two_tokens_both_quoted(void** state); +void count_tokens__handles__escapes(void** state); +void count_tokens__handles__multiple_spaces(void** state); void get_start__returns__first_of_one(void** state); void get_start__returns__first_of_two(void** state); void get_start__returns__first_two_of_three(void** state); void get_start__returns__first_two_of_three_first_quoted(void** state); void get_start__returns__first_two_of_three_second_quoted(void** state); void get_start__returns__first_two_of_three_first_and_second_quoted(void** state); +void get_start__handles__escapes(void** state); +void get_start__handles__multiple_spaces(void** state); void parse_options__returns__empty_hashmap_when_none(void** state); void parse_options__returns__error_when_opt1_no_val(void** state); void parse_options__returns__map_when_one(void** state); diff --git a/tests/unittests/unittests.c b/tests/unittests/unittests.c index f1d1464ab..17e3774e3 100644 --- a/tests/unittests/unittests.c +++ b/tests/unittests/unittests.c @@ -101,6 +101,7 @@ main(int argc, char* argv[]) cmocka_unit_test(strip_arg_quotes__returns__stripped_first), cmocka_unit_test(strip_arg_quotes__returns__stripped_last), cmocka_unit_test(strip_arg_quotes__returns__stripped_both), + cmocka_unit_test(strip_arg_quotes__unescapes), cmocka_unit_test(format_call_external_argv__tests__table_driven), cmocka_unit_test(unique_filename_from_url__tests__table_driven), cmocka_unit_test(string_to_verbosity__returns__correct_values), @@ -119,6 +120,8 @@ main(int argc, char* argv[]) cmocka_unit_test(autocomplete_complete__returns__null_when_empty), cmocka_unit_test(autocomplete_reset__updates__after_create), cmocka_unit_test(autocomplete_complete__returns__null_after_create), + cmocka_unit_test(autocomplete_complete__handles__escaped_spaces), + cmocka_unit_test(autocomplete_complete__handles__escaped_quotes), cmocka_unit_test(autocomplete_create_list__returns__null_after_create), cmocka_unit_test(autocomplete_add__updates__one_and_complete), cmocka_unit_test(autocomplete_complete__returns__first_of_two), @@ -206,6 +209,9 @@ main(int argc, char* argv[]) cmocka_unit_test(parse_args_with_freetext__returns__third_arg_quoted), cmocka_unit_test(parse_args_with_freetext__returns__second_arg_quoted), cmocka_unit_test(parse_args_with_freetext__returns__second_and_third_arg_quoted), + cmocka_unit_test(parse_args__returns__escaped_quotes), + cmocka_unit_test(parse_args__returns__escaped_spaces), + cmocka_unit_test(parse_args__returns__escaped_backslash), cmocka_unit_test(count_tokens__returns__one_token), cmocka_unit_test(count_tokens__returns__one_token_quoted_no_whitespace), cmocka_unit_test(count_tokens__returns__one_token_quoted_with_whitespace), @@ -213,12 +219,16 @@ main(int argc, char* argv[]) cmocka_unit_test(count_tokens__returns__two_tokens_first_quoted), cmocka_unit_test(count_tokens__returns__two_tokens_second_quoted), cmocka_unit_test(count_tokens__returns__two_tokens_both_quoted), + cmocka_unit_test(count_tokens__handles__escapes), + cmocka_unit_test(count_tokens__handles__multiple_spaces), cmocka_unit_test(get_start__returns__first_of_one), cmocka_unit_test(get_start__returns__first_of_two), cmocka_unit_test(get_start__returns__first_two_of_three), cmocka_unit_test(get_start__returns__first_two_of_three_first_quoted), cmocka_unit_test(get_start__returns__first_two_of_three_second_quoted), cmocka_unit_test(get_start__returns__first_two_of_three_first_and_second_quoted), + cmocka_unit_test(get_start__handles__escapes), + cmocka_unit_test(get_start__handles__multiple_spaces), cmocka_unit_test(parse_options__returns__empty_hashmap_when_none), cmocka_unit_test(parse_options__returns__error_when_opt1_no_val), cmocka_unit_test(parse_options__returns__map_when_one),