fix(tools): implement backslash escaping for contact names
Implement backslash based escaping in the command parser and autocompletion logic to handle contact names containing double quotes. This stops the parser from splitting nicknames into multiple tokens, which previously caused "Invalid usage" errors. The parser now sees \ as an escape character for quotes and spaces in _parse_args_helper, count_tokens and get_start. Autocomplete results are escaped when they contain spaces, and search prefixes are unescaped before matching. strip_arg_quotes() has also been updated to handle unescaping. Fixes: https://github.com/profanity-im/profanity/issues/1844 Signed-off-by: Michael Vetter <jubalh@iodoru.org>
This commit is contained in:
34
src/common.c
34
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, '"'));
|
||||
// Unescape and strip quotes
|
||||
GString* unescaped = g_string_new("");
|
||||
for (const char* p = input; *p; p++) {
|
||||
if (*p == '\\' && (*(p + 1) == '"' || *(p + 1) == '\\')) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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, '"'));
|
||||
}
|
||||
}
|
||||
|
||||
return unquoted;
|
||||
return g_string_free(unescaped, FALSE);
|
||||
}
|
||||
|
||||
gboolean
|
||||
|
||||
@@ -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) == '"' || *(p + 1) == '\\')) {
|
||||
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);
|
||||
|
||||
@@ -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 (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_strndup(curr_ch,
|
||||
0));
|
||||
token_size = 0;
|
||||
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,6 +264,11 @@ 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++;
|
||||
@@ -276,11 +300,21 @@ 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) {
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -390,6 +390,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 +495,15 @@ 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
|
||||
get_start__returns__first_of_one(void** state)
|
||||
{
|
||||
@@ -513,6 +564,16 @@ 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
|
||||
parse_options__returns__empty_hashmap_when_none(void** state)
|
||||
{
|
||||
|
||||
@@ -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,14 @@ 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 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 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);
|
||||
|
||||
@@ -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),
|
||||
@@ -206,6 +207,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 +217,14 @@ 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(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(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),
|
||||
|
||||
Reference in New Issue
Block a user