Fix BLI_str_escape with control characters, add unit tests

This commit is contained in:
Campbell Barton 2020-12-10 13:33:55 +11:00
parent 65f139117d
commit 7fc1d76037
3 changed files with 96 additions and 39 deletions

View File

@ -85,7 +85,7 @@ size_t BLI_vsnprintf_rlen(char *__restrict buffer,
char *BLI_sprintfN(const char *__restrict format, ...) ATTR_WARN_UNUSED_RESULT
ATTR_NONNULL(1) ATTR_MALLOC ATTR_PRINTF_FORMAT(1, 2);
size_t BLI_str_escape(char *__restrict dst, const char *__restrict src, const size_t maxncpy)
size_t BLI_str_escape(char *__restrict dst, const char *__restrict src, const size_t dst_maxncpy)
ATTR_NONNULL();
size_t BLI_str_format_int_grouped(char dst[16], int num) ATTR_NONNULL();

View File

@ -317,49 +317,40 @@ char *BLI_sprintfN(const char *__restrict format, ...)
return n;
}
/* match pythons string escaping, assume double quotes - (")
* TODO: should be used to create RNA animation paths.
* TODO: support more fancy string escaping. current code is primitive
* this basically is an ascii version of PyUnicode_EncodeUnicodeEscape()
* which is a useful reference. */
size_t BLI_str_escape(char *__restrict dst, const char *__restrict src, const size_t maxncpy)
/**
* This roughly matches C and Python's string escaping with double quotes - `"`.
*
* Since every character may need escaping,
* it's common to create a buffer twice as large as the input.
*
* \param dst: The destination string, at least \a dst_maxncpy, typically `(strlen(src) * 2) + 1`.
* \param src: The un-escaped source string.
* \param dst_maxncpy: The maximum number of bytes allowable to copy.
*
* \note This is used for creating animation paths in blend files.
*/
size_t BLI_str_escape(char *__restrict dst, const char *__restrict src, const size_t dst_maxncpy)
{
BLI_assert(dst_maxncpy != 0);
size_t len = 0;
BLI_assert(maxncpy != 0);
while (len < maxncpy) {
switch (*src) {
case '\0':
goto escape_finish;
case '\\':
case '"':
ATTR_FALLTHROUGH;
/* less common but should also be support */
case '\t':
case '\n':
case '\r':
if (len + 1 < maxncpy) {
*dst++ = '\\';
len++;
}
else {
/* not enough space to escape */
break;
}
ATTR_FALLTHROUGH;
default:
*dst = *src;
for (; (len < dst_maxncpy) && (*src != '\0'); dst++, src++, len++) {
char c = *src;
if (ELEM(c, '\\', '"') || /* Use as-is. */
((c == '\t') && ((void)(c = 't'), true)) || /* Tab. */
((c == '\n') && ((void)(c = 'n'), true)) || /* Newline. */
((c == '\r') && ((void)(c = 'r'), true))) /* Carriage return. */
{
if (UNLIKELY(len + 1 >= dst_maxncpy)) {
/* Not enough space to escape. */
break;
}
*dst++ = '\\';
len++;
}
dst++;
src++;
len++;
*dst = c;
}
escape_finish:
*dst = '\0';
return len;

View File

@ -802,3 +802,69 @@ TEST_F(StringCasecmpNatural, TextAndNumbers)
testReturnsLessThanZeroForAll(negative);
testReturnsMoreThanZeroForAll(positive);
}
/* BLI_str_escape */
class StringEscape : public testing::Test {
protected:
StringEscape()
{
}
using CompareWordsArray = vector<std::array<const char *, 2>>;
void testEscapeWords(const CompareWordsArray &items)
{
size_t dst_test_len;
char dst_test[64];
for (const auto &item : items) {
/* Escape the string. */
dst_test_len = BLI_str_escape(dst_test, item[0], SIZE_MAX);
EXPECT_STREQ(dst_test, item[1]);
EXPECT_EQ(dst_test_len, strlen(dst_test));
}
}
};
TEST_F(StringEscape, Simple)
{
const CompareWordsArray equal{
{"", ""},
{"/", "/"},
{"'", "'"},
{"?", "?"},
};
const CompareWordsArray escaped{
{"\\", "\\\\"},
{"A\\", "A\\\\"},
{"\\A", "\\\\A"},
{"A\\B", "A\\\\B"},
{"?", "?"},
{"\"\\", "\\\"\\\\"},
{"\\\"", "\\\\\\\""},
{"\"\\\"", "\\\"\\\\\\\""},
{"\"\"\"", "\\\"\\\"\\\""},
{"\\\\\\", "\\\\\\\\\\\\"},
};
testEscapeWords(equal);
testEscapeWords(escaped);
}
TEST_F(StringEscape, Control)
{
const CompareWordsArray escaped{
{"\n", "\\n"},
{"\r", "\\r"},
{"\t", "\\t"},
{"A\n", "A\\n"},
{"\nA", "\\nA"},
{"\n\r\t", "\\n\\r\\t"},
{"\n_\r_\t", "\\n_\\r_\\t"},
{"\n\\\r\\\t", "\\n\\\\\\r\\\\\\t"},
};
testEscapeWords(escaped);
}