diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 06cb295b96a7fdee4b3bbca96695676a25f6a650..21d83edad22caca172189d4254ff49a32426a682 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -20,6 +20,7 @@ - cta/CTA#683 - Fix problems with field consistency in json logging - cta/CTA#688 - Fix tapeserver umask to allow directory creation in POSIX filesystems - cta/CTA#693 - Fix tapeserver tool regressions +- cta/CTA#704 - Fix special character encoding in json logging ### Continuous Integration - cta/CTA#615 - Going to xrdfs xattr API for EOS5 extended attribute tests (EOS >= 5.2.17) diff --git a/common/log/JsonTest.cpp b/common/log/JsonTest.cpp index a18f349fcee2325db8ae9f3c8a328880fb95438c..cf345a697b11f687162c123ecbe757d1d3e39e45 100644 --- a/common/log/JsonTest.cpp +++ b/common/log/JsonTest.cpp @@ -151,4 +151,49 @@ TEST_F(cta_log_JsonTest, testJsonPrinting) { ASSERT_EQ(0, jObjB.jsonGetValueProbe<int64_t>("ParamsB_null")); } +TEST_F(cta_log_JsonTest, testJsonStringEscape) { + using namespace cta::log; + + // Prepare the logger for inspection + StringLogger logger("dummy", "cta_log_JsonTest", cta::log::DEBUG); + LogContext logContext(logger); + logger.setLogFormat("json"); + + { + cta::log::ScopedParamContainer paramsA(logContext); + paramsA.add("key_\"", "value_\""); + paramsA.add("key_\\", "value_\\"); + paramsA.add("key_\b", "value_\b"); + paramsA.add("key_\n", "value_\n"); + paramsA.add("key_\f", "value_\f"); + paramsA.add("key_\r", "value_\r"); + paramsA.add("key_\t", "value_\t"); + paramsA.add("key_\x00", "value_\x00"); + paramsA.add("key_\x1f", "value_\x1f"); + paramsA.add("key_\x20", "value_\x20"); //This is a whitespace character + logContext.log(INFO, "Testing escaped values"); + } + + std::string logLine = logger.getLog(); + + JSONCObjectProbe jObj; + jObj.buildFromJSON(logLine); + + // Check that JSON is parsed correctly + ASSERT_NO_THROW(jObj.getJSON()); + + // Check expected keys and values + // Strings should be converted back to cpp with the escape characters correctly decoded + ASSERT_EQ("value_\"", jObj.jsonGetValueProbe<std::string>("key_\"")); + ASSERT_EQ("value_\\", jObj.jsonGetValueProbe<std::string>("key_\\")); + ASSERT_EQ("value_\b", jObj.jsonGetValueProbe<std::string>("key_\b")); + ASSERT_EQ("value_\n", jObj.jsonGetValueProbe<std::string>("key_\n")); + ASSERT_EQ("value_\f", jObj.jsonGetValueProbe<std::string>("key_\f")); + ASSERT_EQ("value_\r", jObj.jsonGetValueProbe<std::string>("key_\r")); + ASSERT_EQ("value_\t", jObj.jsonGetValueProbe<std::string>("key_\t")); + ASSERT_EQ("value_\x00", jObj.jsonGetValueProbe<std::string>("key_\x00")); + ASSERT_EQ("value_\x1f", jObj.jsonGetValueProbe<std::string>("key_\x1f")); + ASSERT_EQ("value_ ", jObj.jsonGetValueProbe<std::string>("key_ ")); +} + } // namespace unitTests diff --git a/common/log/Param.cpp b/common/log/Param.cpp index 5875624eb38dcaad34349495cb104d882ef51aa7..4111c3c1fc71a3795e972bde57f1a65c8c4513b4 100644 --- a/common/log/Param.cpp +++ b/common/log/Param.cpp @@ -19,6 +19,7 @@ #include <iostream> #include <iomanip> +#include <algorithm> namespace cta::log { @@ -61,14 +62,14 @@ std::string Param::getValueStr() const noexcept { //------------------------------------------------------------------------------ std::string Param::getKeyValueJSON() const noexcept { std::ostringstream oss; - oss << "\"" << m_name << "\":"; + oss << "\"" << stringFormattingJSON(m_name) << "\":"; if (m_value.has_value()) { std::visit([&oss](auto &&arg) { using T = std::decay_t<decltype(arg)>; if constexpr (std::is_same_v<T, bool>) { oss << (arg ? "true" : "false"); } else if constexpr (std::is_same_v<T, std::string>) { - oss << "\"" << arg << "\""; + oss << "\"" << stringFormattingJSON(arg) << "\""; } else if constexpr (std::is_integral_v<T>) { oss << arg; } else if constexpr (std::is_floating_point_v<T>) { @@ -88,4 +89,35 @@ void Param::setValue<ParamValType>(const ParamValType& value) noexcept { m_value = value; } +//------------------------------------------------------------------------------ +// stringFormattingJSON nested class +//------------------------------------------------------------------------------ +Param::stringFormattingJSON::stringFormattingJSON(const std::string& str) : m_value(str) {} + +//------------------------------------------------------------------------------ +// stringFormattingJSON << operator overload +//------------------------------------------------------------------------------ +std::ostream& operator<<(std::ostream& oss, const Param::stringFormattingJSON& fp) { + std::ostringstream oss_tmp; + for (char c : fp.m_value) { + switch (c) { + case '\"': oss_tmp << R"(\")"; break; + case '\\': oss_tmp << R"(\\)"; break; + case '\b': oss_tmp << R"(\b)"; break; + case '\f': oss_tmp << R"(\f)"; break; + case '\n': oss_tmp << R"(\n)"; break; + case '\r': oss_tmp << R"(\r)"; break; + case '\t': oss_tmp << R"(\t)"; break; + default: + if ('\x00' <= c && c <= '\x1f') { + oss_tmp << R"(\u)" << std::hex << std::setw(4) << std::setfill('0') << static_cast<unsigned int>(c); + } else { + oss_tmp << c; + } + } + } + oss << oss_tmp.str(); + return oss; +} + } // namespace cta::log diff --git a/common/log/Param.hpp b/common/log/Param.hpp index 5d6ead394f36bc7aad04d4c056d190f54c581d40..ad0f50261e01df7a99feb7505fdf1a180be1bc65 100644 --- a/common/log/Param.hpp +++ b/common/log/Param.hpp @@ -149,6 +149,19 @@ protected: */ ParamValType m_value; + /** + * Helper class to format string values in JSON + */ + class stringFormattingJSON { + public: + explicit stringFormattingJSON(const std::string& str); + friend std::ostream& operator<<(std::ostream& oss, const stringFormattingJSON& fp); + private: + const std::string & m_value; + }; + + friend std::ostream& operator<<(std::ostream& oss, const Param::stringFormattingJSON& fp); + /** * Helper class to format floating-point values */