From d0723d20d795b5725b15f4020a004f33d672bd2b Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Mon, 13 Oct 2025 22:30:52 +0100 Subject: [PATCH] str: allow escaping with prefix and suffix Allow `git_str_puts_escaped` to take an escaping prefix and an escaping suffix; this allows for more options, including the ability to better support escaping executed paths. --- src/util/str.c | 24 ++++++++++++++++++------ src/util/str.h | 8 +++++--- tests/util/gitstr.c | 22 +++++++++++++++++++--- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/util/str.c b/src/util/str.c index 0b07c8147..7b8d99bc5 100644 --- a/src/util/str.c +++ b/src/util/str.c @@ -1065,10 +1065,13 @@ int git_str_puts_escaped( git_str *buf, const char *string, const char *esc_chars, - const char *esc_with) + const char *esc_prefix, + const char *esc_suffix) { const char *scan; - size_t total = 0, esc_len = strlen(esc_with), count, alloclen; + size_t total = 0, count, alloclen; + size_t esc_prefix_len = esc_prefix ? strlen(esc_prefix) : 0; + size_t esc_suffix_len = esc_suffix ? strlen(esc_suffix) : 0; if (!string) return 0; @@ -1080,7 +1083,7 @@ int git_str_puts_escaped( scan += count; /* count run of escaped characters */ count = strspn(scan, esc_chars); - total += count * (esc_len + 1); + total += count * (esc_prefix_len + esc_suffix_len + 1); scan += count; } @@ -1096,13 +1099,22 @@ int git_str_puts_escaped( buf->size += count; for (count = strspn(scan, esc_chars); count > 0; --count) { - /* copy escape sequence */ - memmove(buf->ptr + buf->size, esc_with, esc_len); - buf->size += esc_len; + /* copy escape prefix sequence */ + if (esc_prefix) { + memmove(buf->ptr + buf->size, esc_prefix, esc_prefix_len); + buf->size += esc_prefix_len; + } + /* copy character to be escaped */ buf->ptr[buf->size] = *scan; buf->size++; scan++; + + /* copy escape suffix sequence */ + if (esc_suffix) { + memmove(buf->ptr + buf->size, esc_suffix, esc_suffix_len); + buf->size += esc_suffix_len; + } } } diff --git a/src/util/str.h b/src/util/str.h index 588e6fc22..ad9b892cd 100644 --- a/src/util/str.h +++ b/src/util/str.h @@ -268,21 +268,23 @@ int git_str_splice( * @param str String buffer to append data to * @param string String to escape and append * @param esc_chars Characters to be escaped - * @param esc_with String to insert in from of each found character + * @param esc_prefix String to insert as prefix of each found character + * @param esc_suffix String to insert as suffix of each found character * @return 0 on success, <0 on failure (probably allocation problem) */ extern int git_str_puts_escaped( git_str *str, const char *string, const char *esc_chars, - const char *esc_with); + const char *esc_prefix, + const char *esc_suffix); /** * Append string escaping characters that are regex special */ GIT_INLINE(int) git_str_puts_escape_regex(git_str *str, const char *string) { - return git_str_puts_escaped(str, string, "^.[]$()|*+?{}\\", "\\"); + return git_str_puts_escaped(str, string, "^.[]$()|*+?{}\\", "\\", NULL); } /** diff --git a/tests/util/gitstr.c b/tests/util/gitstr.c index aea35565b..37936b5a2 100644 --- a/tests/util/gitstr.c +++ b/tests/util/gitstr.c @@ -691,17 +691,33 @@ void test_gitstr__puts_escaped(void) git_str a = GIT_STR_INIT; git_str_clear(&a); - cl_git_pass(git_str_puts_escaped(&a, "this is a test", "", "")); + cl_git_pass(git_str_puts_escaped(&a, "this is a test", "", "", "")); cl_assert_equal_s("this is a test", a.ptr); git_str_clear(&a); - cl_git_pass(git_str_puts_escaped(&a, "this is a test", "t", "\\")); + cl_git_pass(git_str_puts_escaped(&a, "this is a test", "", NULL, NULL)); + cl_assert_equal_s("this is a test", a.ptr); + + git_str_clear(&a); + cl_git_pass(git_str_puts_escaped(&a, "this is a test", "t", "\\", "")); cl_assert_equal_s("\\this is a \\tes\\t", a.ptr); git_str_clear(&a); - cl_git_pass(git_str_puts_escaped(&a, "this is a test", "i ", "__")); + cl_git_pass(git_str_puts_escaped(&a, "this is a test", "t", "\\", NULL)); + cl_assert_equal_s("\\this is a \\tes\\t", a.ptr); + + git_str_clear(&a); + cl_git_pass(git_str_puts_escaped(&a, "this is a test", "i ", "__", NULL)); cl_assert_equal_s("th__is__ __is__ a__ test", a.ptr); + git_str_clear(&a); + cl_git_pass(git_str_puts_escaped(&a, "this is a test", "i ", "__", "!!")); + cl_assert_equal_s("th__i!!s__ !!__i!!s__ !!a__ !!test", a.ptr); + + git_str_clear(&a); + cl_git_pass(git_str_puts_escaped(&a, "this' is' an' escape! ", "'!", "'\\", "'")); + cl_assert_equal_s("this'\\'' is'\\'' an'\\'' escape'\\!' ", a.ptr); + git_str_clear(&a); cl_git_pass(git_str_puts_escape_regex(&a, "^match\\s*[A-Z]+.*")); cl_assert_equal_s("\\^match\\\\s\\*\\[A-Z\\]\\+\\.\\*", a.ptr);