diff --git a/data/include/join/json.hpp b/data/include/join/json.hpp index abd67d6d..110eea4e 100644 --- a/data/include/join/json.hpp +++ b/data/include/join/json.hpp @@ -880,7 +880,7 @@ namespace join * @param data data to append to output stream. * @param size data size. */ - inline void append (const char* data, uint32_t size) + inline void append (const char* data, size_t size) { if (JOIN_UNLIKELY (size > sizeof (_buffer) - _size)) { diff --git a/data/include/join/pack.hpp b/data/include/join/pack.hpp index 97290bee..7c7d41c5 100644 --- a/data/include/join/pack.hpp +++ b/data/include/join/pack.hpp @@ -32,6 +32,7 @@ #include #include #include +#include #include // C++. @@ -691,47 +692,134 @@ namespace join size_t _depth = 0; }; + /** + * @brief MessagePack deserialization mode. + */ + enum class PackReadMode : uint32_t + { + None = 0, /**< no read mode set. */ + ValidateEncoding = 1L << 0, /**< validate UTF-8 encoding of strings. */ + StopParsingOnDone = 1L << 1, /**< stop parsing on done. */ + }; + + /** + * @brief perform binary AND on PackReadMode. + * @param a bitset. + * @param b other bitset. + * @return bitset result of binary AND on PackReadMode. + */ + constexpr PackReadMode operator& (PackReadMode a, PackReadMode b) noexcept + { + return PackReadMode (static_cast (a) & static_cast (b)); + } + + /** + * @brief perform binary OR on PackReadMode. + * @param a bitset. + * @param b other bitset. + * @return bitset result of binary OR on PackReadMode. + */ + constexpr PackReadMode operator| (PackReadMode a, PackReadMode b) noexcept + { + return PackReadMode (static_cast (a) | static_cast (b)); + } + + /** + * @brief perform binary XOR on PackReadMode. + * @param a bitset. + * @param b other bitset. + * @return bitset result of binary XOR on PackReadMode. + */ + constexpr PackReadMode operator^ (PackReadMode a, PackReadMode b) noexcept + { + return PackReadMode (static_cast (a) ^ static_cast (b)); + } + + /** + * @brief perform binary NOT on PackReadMode. + * @param a bitset. + * @return bitset result of binary NOT on PackReadMode. + */ + constexpr PackReadMode operator~(PackReadMode a) noexcept + { + return PackReadMode (~static_cast (a)); + } + + /** + * @brief perform binary AND assignment on PackReadMode. + * @param a bitset. + * @param b other bitset. + * @return reference to updated bitset. + */ + constexpr const PackReadMode& operator&= (PackReadMode& a, PackReadMode b) noexcept + { + return a = a & b; + } + + /** + * @brief perform binary OR assignment on PackReadMode. + * @param a bitset. + * @param b other bitset. + * @return reference to updated bitset. + */ + constexpr const PackReadMode& operator|= (PackReadMode& a, PackReadMode b) noexcept + { + return a = a | b; + } + + /** + * @brief perform binary XOR assignment on PackReadMode. + * @param a bitset. + * @param b other bitset. + * @return reference to updated bitset. + */ + constexpr const PackReadMode& operator^= (PackReadMode& a, PackReadMode b) noexcept + { + return a = a ^ b; + } + /** * @brief message pack reader class. */ - class PackReader + template + class BasicPackReader { public: /** * @brief default constructor. */ - PackReader () = default; + BasicPackReader () = default; /** * @brief copy constructor. * @param other object to copy. */ - PackReader (const PackReader& other) = delete; + BasicPackReader (const BasicPackReader& other) = delete; /** * @brief copy assignment. * @param other object to copy. * @return a reference of the current object. */ - PackReader& operator= (const PackReader& other) = delete; + BasicPackReader& operator= (const BasicPackReader& other) = delete; /** * @brief move constructor. * @param other object to move. */ - PackReader (PackReader&& other) = delete; + BasicPackReader (BasicPackReader&& other) = delete; /** * @brief move assignment. * @param other object to move. * @return a reference of the current object. */ - PackReader& operator= (PackReader&& other) = delete; + BasicPackReader& operator= (BasicPackReader&& other) = delete; /** * @brief destroy instance. */ - ~PackReader () = default; + ~BasicPackReader () = default; /** * @brief deserialize a document. @@ -818,6 +906,11 @@ namespace join { if (JOIN_LIKELY (readValue (document, handler, 0) == 0)) { + if constexpr ((ReadMode & PackReadMode::StopParsingOnDone) != PackReadMode::None) + { + return 0; + } + if (JOIN_LIKELY (document.peek () == std::char_traits::eof ())) { return 0; @@ -1155,6 +1248,10 @@ namespace join } std::string_view output (document.tell (), len); + if (JOIN_UNLIKELY (validateString (output) == -1)) + { + return -1; + } document.seek (document.tell () + len); return isKey ? handler.setKey (output) : handler.setString (output); } @@ -1167,11 +1264,34 @@ namespace join return -1; } + if (JOIN_UNLIKELY (validateString (output) == -1)) + { + return -1; + } + int result = isKey ? handler.setKey (output) : handler.setString (output); cache = std::move (output); return result; } + /** + * @brief validate the UTF-8 encoding of a string when required by the read mode. + * @param value string to validate. + * @return 0 on success, -1 otherwise. + */ + static int validateString ([[maybe_unused]] std::string_view value) noexcept + { + if constexpr ((ReadMode & PackReadMode::ValidateEncoding) != PackReadMode::None) + { + if (JOIN_UNLIKELY (utf8::validate (value.data (), value.data () + value.size ()) == -1)) + { + join::lastError = make_error_code (SaxErrc::InvalidEncoding); + return -1; + } + } + return 0; + } + /** * @brief parse binary data. * @param document document to parse. @@ -1581,6 +1701,9 @@ namespace join return ((c >= 0x80) && (c <= 0x8f)) || (c == 0xde) || (c == 0xdf); } }; + + /// MessagePack reader. + using PackReader = BasicPackReader; } #endif diff --git a/data/include/join/sax_error.hpp b/data/include/join/sax_error.hpp index 304acab5..99ea87f4 100644 --- a/data/include/join/sax_error.hpp +++ b/data/include/join/sax_error.hpp @@ -43,6 +43,7 @@ namespace join InvalidParent, /**< Parent not an array nor an object. */ InvalidKey, /**< Key is invalid. */ InvalidValue, /**< Value is invalid. */ + InvalidEncoding, /**< Character encoding is invalid. */ ExtraData, /**< Extra data detected. */ Truncated /**< Data is truncated. */ }; diff --git a/data/src/sax_error.cpp b/data/src/sax_error.cpp index 1b255fd9..3fa15df1 100644 --- a/data/src/sax_error.cpp +++ b/data/src/sax_error.cpp @@ -53,6 +53,8 @@ std::string SaxCategory::message (int code) const return "key is invalid"; case SaxErrc::InvalidValue: return "value is invalid"; + case SaxErrc::InvalidEncoding: + return "character encoding is invalid"; case SaxErrc::ExtraData: return "extra data detected"; case SaxErrc::Truncated: diff --git a/data/tests/pack_reader_test.cpp b/data/tests/pack_reader_test.cpp index a7f7bc57..71f890a7 100644 --- a/data/tests/pack_reader_test.cpp +++ b/data/tests/pack_reader_test.cpp @@ -209,6 +209,25 @@ TEST (PackReader, deserialize) builder.startArray (); } ASSERT_EQ (PackReader ().deserialize (std::string (1, '\x80'), builder), -1); + + using Stopping = join::BasicPackReader; + std::stringstream messages (std::string ({'\x01', '\x02', '\xc3'})); + ASSERT_EQ (Stopping ().deserialize (messages, value), 0) << join::lastError.message (); + ASSERT_EQ (value, 1); + ASSERT_EQ (Stopping ().deserialize (messages, value), 0) << join::lastError.message (); + ASSERT_EQ (value, 2); + ASSERT_EQ (Stopping ().deserialize (messages, value), 0) << join::lastError.message (); + ASSERT_EQ (value, true); + ASSERT_EQ (Stopping ().deserialize (messages, value), -1); + ASSERT_EQ (join::lastError, SaxErrc::Truncated); + ASSERT_EQ (Stopping ().deserialize (std::string ({'\x00', '\x00'}), value), 0) << join::lastError.message (); + + using Strict = join::BasicPackReader; + std::stringstream strict (std::string ({'\xa1', 'a', '\xa1', '\xff'})); + ASSERT_EQ (Strict ().deserialize (strict, value), 0) << join::lastError.message (); + ASSERT_EQ (value.getString (), "a"); + ASSERT_EQ (Strict ().deserialize (strict, value), -1); + ASSERT_EQ (join::lastError, SaxErrc::InvalidEncoding); } /** @@ -1570,6 +1589,39 @@ TEST (PackReader, key) ASSERT_TRUE (value.contains (-10)); } +/** + * @brief Test MessagePack parsing with UTF-8 validation. + */ +TEST (PackReader, encoding) +{ + using Validator = join::BasicPackReader; + + for (const std::string& document : {std::string ({'\xa2', '\xc3', '\xa9'}), std::string ({'\xc4', '\x01', '\xff'})}) + { + std::stringstream stream (document); + join::SaxHandler handler; + ASSERT_EQ (Validator ().deserialize (document, handler), 0) << join::lastError.message (); + ASSERT_EQ (Validator ().deserialize (stream, handler), 0) << join::lastError.message (); + } + + for (const std::string& document : {std::string ({'\xa1', '\xff'}), std::string ({'\x81', '\xa1', '\xff', '\x01'})}) + { + std::stringstream stream (document); + join::SaxHandler handler; + ASSERT_EQ (Validator ().deserialize (document, handler), -1); + ASSERT_EQ (join::lastError, SaxErrc::InvalidEncoding); + ASSERT_EQ (Validator ().deserialize (stream, handler), -1); + ASSERT_EQ (join::lastError, SaxErrc::InvalidEncoding); + } + + Value value; + ASSERT_EQ (PackReader ().deserialize (std::string ({'\xa1', '\xff'}), value), 0) << join::lastError.message (); + ASSERT_EQ (value.getString (), "\xff"); + ASSERT_EQ (value.deserialize (std::string ({'\xa4', 'I', 'E', 'T', 'F'})), 0) + << join::lastError.message (); + ASSERT_EQ (value.getString (), "IETF"); +} + /** * @brief main function. */ diff --git a/data/tests/sax_error_test.cpp b/data/tests/sax_error_test.cpp index fa007dac..e55641fd 100644 --- a/data/tests/sax_error_test.cpp +++ b/data/tests/sax_error_test.cpp @@ -50,6 +50,8 @@ TEST (SaxCategory, message) "parent not an array nor an object"); EXPECT_STREQ (SaxCategory ().message (static_cast (SaxErrc::InvalidKey)).c_str (), "key is invalid"); EXPECT_STREQ (SaxCategory ().message (static_cast (SaxErrc::InvalidValue)).c_str (), "value is invalid"); + EXPECT_STREQ (SaxCategory ().message (static_cast (SaxErrc::InvalidEncoding)).c_str (), + "character encoding is invalid"); EXPECT_STREQ (SaxCategory ().message (static_cast (SaxErrc::ExtraData)).c_str (), "extra data detected"); EXPECT_STREQ (SaxCategory ().message (static_cast (SaxErrc::Truncated)).c_str (), "data is truncated"); } @@ -64,7 +66,8 @@ TEST (SaxCategory, default_error_condition) EXPECT_EQ (SaxCategory ().default_error_condition (2).message (), "parent not an array nor an object"); EXPECT_EQ (SaxCategory ().default_error_condition (3).message (), "key is invalid"); EXPECT_EQ (SaxCategory ().default_error_condition (4).message (), "value is invalid"); - EXPECT_EQ (SaxCategory ().default_error_condition (5).message (), "extra data detected"); + EXPECT_EQ (SaxCategory ().default_error_condition (5).message (), "character encoding is invalid"); + EXPECT_EQ (SaxCategory ().default_error_condition (6).message (), "extra data detected"); } /**