Escape json key and values

This commit is contained in:
JCW
2026-05-05 15:14:03 +01:00
parent 13bdcee55d
commit f3df114918
2 changed files with 168 additions and 8 deletions

View File

@@ -27,6 +27,58 @@ concept HasToString = requires(std::remove_cvref_t<T> const& t) {
{ to_string(t) } -> std::convertible_to<std::string>;
};
/**
* @brief Escape a string for safe embedding in a JSON value.
*
* Escapes backslash, double-quote, and control characters (U+0000-U+001F)
* according to RFC 8259 section 7.
*/
inline void
appendEscapedJsonString(std::string& dest, std::string_view sv)
{
dest.reserve(dest.size() + sv.size() + 2);
dest += '"';
for (char const c : sv)
{
switch (c)
{
case '"':
dest += "\\\"";
break;
case '\\':
dest += "\\\\";
break;
case '\b':
dest += "\\b";
break;
case '\f':
dest += "\\f";
break;
case '\n':
dest += "\\n";
break;
case '\r':
dest += "\\r";
break;
case '\t':
dest += "\\t";
break;
default:
if (static_cast<unsigned char>(c) < 0x20)
{
fmt::format_to(
std::back_inserter(dest), "\\u{:04x}", static_cast<unsigned int>(c));
}
else
{
dest += c;
}
break;
}
}
dest += '"';
}
/**
* @brief Append a value formatted as a JSON fragment to @p dest.
*
@@ -58,11 +110,11 @@ appendJsonValue(std::string& dest, T const& value)
}
else if constexpr (HasToString<T>)
{
fmt::format_to(std::back_inserter(dest), "\"{}\"", to_string(value));
appendEscapedJsonString(dest, to_string(value));
}
else
{
fmt::format_to(std::back_inserter(dest), "\"{}\"", value);
appendEscapedJsonString(dest, fmt::format("{}", value));
}
}
@@ -78,7 +130,8 @@ appendJsonField(std::string& dest, std::string_view key, T const& value)
{
if (!dest.empty())
dest += ',';
fmt::format_to(std::back_inserter(dest), "\"{}\":", key);
appendEscapedJsonString(dest, key);
dest += ':';
appendJsonValue(dest, value);
}
@@ -111,15 +164,22 @@ class JsonLoggingPatternBuilder
{
std::string fields_;
static constexpr std::string_view kSUFFIX = ", \"message\": %v }";
static constexpr std::string_view kMESSAGE_FIELD = "\"message\": %v";
static std::string
extractFields(std::string_view pattern)
{
if (!pattern.empty() && pattern.front() == '{')
pattern.remove_prefix(1);
if (auto pos = pattern.rfind(kSUFFIX); pos != std::string_view::npos)
pattern = pattern.substr(0, pos);
// Strip the trailing message field: `"message": %v }` or `"message": %v }`
if (auto pos = pattern.rfind(kMESSAGE_FIELD); pos != std::string_view::npos)
{
// Also strip the leading ", " separator if present
auto end = pos;
if (end >= 2 && pattern.substr(end - 2, 2) == ", ")
end -= 2;
pattern = pattern.substr(0, end);
}
return std::string(pattern);
}
@@ -143,7 +203,13 @@ public:
[[nodiscard]] std::string
build() const
{
return "{" + fields_ + std::string(kSUFFIX);
std::string result = "{";
result += fields_;
if (!fields_.empty())
result += ", ";
result += kMESSAGE_FIELD;
result += " }";
return result;
}
};

View File

@@ -61,12 +61,106 @@ TEST(AppendJsonValue, appends_to_existing)
EXPECT_EQ(dest, "prefix:99");
}
// -- detail::appendEscapedJsonString ------------------------------------------
TEST(AppendEscapedJsonString, plain_string)
{
std::string dest;
detail::appendEscapedJsonString(dest, "hello");
EXPECT_EQ(dest, "\"hello\"");
}
TEST(AppendEscapedJsonString, escapes_double_quote)
{
std::string dest;
detail::appendEscapedJsonString(dest, R"(say "hi")");
EXPECT_EQ(dest, R"("say \"hi\"")");
}
TEST(AppendEscapedJsonString, escapes_backslash)
{
std::string dest;
detail::appendEscapedJsonString(dest, R"(a\b)");
EXPECT_EQ(dest, R"("a\\b")");
}
TEST(AppendEscapedJsonString, escapes_control_characters)
{
std::string dest;
detail::appendEscapedJsonString(dest, "line1\nline2\ttab");
EXPECT_EQ(dest, R"("line1\nline2\ttab")");
}
TEST(AppendEscapedJsonString, escapes_all_named_controls)
{
std::string dest;
detail::appendEscapedJsonString(dest, "\b\f\r");
EXPECT_EQ(dest, R"("\b\f\r")");
}
TEST(AppendEscapedJsonString, escapes_low_control_char_as_unicode)
{
std::string dest;
std::string input(1, '\x01');
detail::appendEscapedJsonString(dest, input);
EXPECT_EQ(dest, R"("\u0001")");
}
TEST(AppendEscapedJsonString, empty_string)
{
std::string dest;
detail::appendEscapedJsonString(dest, "");
EXPECT_EQ(dest, R"("")");
}
// -- appendJsonValue with escaping -------------------------------------------
TEST(AppendJsonValue, string_with_quotes_escaped)
{
std::string dest;
std::string const val = R"(say "hi")";
detail::appendJsonValue(dest, val);
EXPECT_EQ(dest, R"("say \"hi\"")");
}
TEST(AppendJsonValue, string_with_backslash_escaped)
{
std::string dest;
std::string const val = R"(path\to\file)";
detail::appendJsonValue(dest, val);
EXPECT_EQ(dest, R"("path\\to\\file")");
}
TEST(AppendJsonValue, string_with_newline_escaped)
{
std::string dest;
std::string const val = "line1\nline2";
detail::appendJsonValue(dest, val);
EXPECT_EQ(dest, R"("line1\nline2")");
}
// -- appendJsonField with key escaping ----------------------------------------
TEST(AppendJsonField, key_with_quotes_escaped)
{
std::string dest;
detail::appendJsonField(dest, R"(my"key)", 42);
EXPECT_EQ(dest, R"("my\"key":42)");
}
TEST(AppendJsonField, key_with_backslash_escaped)
{
std::string dest;
detail::appendJsonField(dest, R"(a\b)", true);
EXPECT_EQ(dest, R"("a\\b":true)");
}
// -- buildJsonPattern --------------------------------------------------------
TEST(BuildJsonPattern, no_params)
{
auto const pattern = buildJsonPattern("");
EXPECT_EQ(pattern, "{, \"message\": %v }");
EXPECT_EQ(pattern, "{\"message\": %v }");
}
TEST(BuildJsonPattern, single_string_field)