diff --git a/README.md b/README.md index 3eb17caf..2f581d80 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ The framework is a collection of specialized modules that build upon one another | **`core`** | **Foundation** | Epoll Reactor, io_uring Proactor, TCP/UDP/ICMP/Raw & Unix Sockets, Thread Pools, Lock-Free Queues & Allocator. | | **`fabric`**| **Network Control** | Netlink Interface Manager, ARP client, ICMP Ping/Traceroute & Path MTU discovery, DNS/mDNS/DoT Resolver. | | **`crypto`**| **Security** | TLS/DTLS Sockets & Streams, OpenSSL Wrappers, HMAC, Digital Signatures, Base64. | -| **`data`** | **Serialization** | High-perf JSON (DOM/SAX), MessagePack, Zlib Streams. | +| **`data`** | **Serialization** | High-perf JSON (DOM/SAX), MessagePack, CBOR, Zlib Streams. | | **`services`**| **Protocols** | HTTP/1.1 & HTTPS (Client/Server), SMTP/SMTPS, message composition. | --- diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt index 945f5aa5..61e993c8 100644 --- a/data/CMakeLists.txt +++ b/data/CMakeLists.txt @@ -11,6 +11,7 @@ set(PUBLIC_HEADERS include/join/sax_error.hpp include/join/sax.hpp include/join/pack.hpp + include/join/cbor.hpp include/join/json.hpp include/join/streambuf.hpp include/join/zstream.hpp diff --git a/data/include/join/cbor.hpp b/data/include/join/cbor.hpp new file mode 100644 index 00000000..1a727c0b --- /dev/null +++ b/data/include/join/cbor.hpp @@ -0,0 +1,1459 @@ +/** + * MIT License + * + * Copyright (c) 2026 Mathieu Rabine + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef JOIN_DATA_CBOR_HPP +#define JOIN_DATA_CBOR_HPP + +// dconv. +#include + +// libjoin. +#include +#include +#include +#include +#include + +// C++. +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// C. +#include +#include + +namespace join +{ + static_assert (std::numeric_limits::is_iec559 && std::numeric_limits::is_iec559, + "CBOR requires IEEE 754 floating point numbers"); + + /** + * @brief CBOR serialization mode. + */ + enum class CborWriteMode + { + Definite, /**< containers written with their length. */ + Indefinite, /**< containers written with an indefinite length. */ + }; + + /** + * @brief CBOR writer class. + */ + template + class BasicCborWriter + { + public: + /** + * @brief create instance. + * @param document CBOR document to create. + */ + explicit BasicCborWriter (std::ostream& document) + : _out (document.rdbuf ()) + { + } + + /** + * @brief copy constructor. + * @param other object to copy. + */ + BasicCborWriter (const BasicCborWriter& other) = delete; + + /** + * @brief copy assignment. + * @param other object to copy. + * @return a reference of the current object. + */ + BasicCborWriter& operator= (const BasicCborWriter& other) = delete; + + /** + * @brief move constructor. + * @param other object to move. + */ + BasicCborWriter (BasicCborWriter&& other) = delete; + + /** + * @brief move assignment. + * @param other object to move. + * @return a reference of the current object. + */ + BasicCborWriter& operator= (BasicCborWriter&& other) = delete; + + /** + * @brief destroy instance. + */ + ~BasicCborWriter () = default; + + /** + * @brief set null value. + * @return 0 on success, -1 otherwise. + */ + int setNull () + { + append (0xf6); + endValue (); + return 0; + } + + /** + * @brief set boolean value. + * @param value boolean value to set. + * @return 0 on success, -1 otherwise. + */ + int setBool (bool value) + { + if (value) + { + append (0xf5); + } + else + { + append (0xf4); + } + endValue (); + return 0; + } + + /** + * @brief set integer value. + * @param value integer value to set. + * @return 0 on success, -1 otherwise. + */ + int setInt (int32_t value) + { + return setInt64 (value); + } + + /** + * @brief set unsigned integer value. + * @param value unsigned integer value to set. + * @return 0 on success, -1 otherwise. + */ + int setUint (uint32_t value) + { + writeHead (0x00, value); + endValue (); + return 0; + } + + /** + * @brief set 64 bits integer value. + * @param value 64 bits integer value to set. + * @return 0 on success, -1 otherwise. + */ + int setInt64 (int64_t value) + { + if (value < 0) + { + writeHead (0x20, ~static_cast (value)); + } + else + { + writeHead (0x00, static_cast (value)); + } + endValue (); + return 0; + } + + /** + * @brief set unsigned 64 bits integer value. + * @param value unsigned 64 bits integer value to set. + * @return 0 on success, -1 otherwise. + */ + int setUint64 (uint64_t value) + { + writeHead (0x00, value); + endValue (); + return 0; + } + + /** + * @brief set real value. + * @param value real value to set. + * @return 0 on success, -1 otherwise. + */ + int setDouble (double value) + { + float f = static_cast (value); + if (std::bit_cast (static_cast (f)) == std::bit_cast (value)) + { + append (0xfa); + pack (f); + } + else + { + append (0xfb); + pack (value); + } + endValue (); + return 0; + } + + /** + * @brief set string value. + * @param value string value to set. + * @return 0 on success, -1 otherwise. + */ + int setString (std::string_view value) + { + writeHead (0x60, value.size ()); + append (value.data (), value.size ()); + endValue (); + return 0; + } + + /** + * @brief set binary value. + * @param value binary value to set. + * @param subtype binary subtype, not written. + * @return 0 on success, -1 otherwise. + */ + int setBinary (std::span value, [[maybe_unused]] std::optional subtype = std::nullopt) + { + writeHead (0x40, value.size ()); + append (reinterpret_cast (value.data ()), value.size ()); + endValue (); + return 0; + } + + /** + * @brief start array of unknown size. + * @return 0 on success, -1 otherwise. + */ + int startArray () + { + static_assert (WriteMode == CborWriteMode::Indefinite, "definite length containers require a size"); + append (0x9f); + ++_depth; + return 0; + } + + /** + * @brief start array. + * @param size array size. + * @return 0 on success, -1 otherwise. + */ + int startArray ([[maybe_unused]] uint32_t size) + { + if constexpr (WriteMode == CborWriteMode::Indefinite) + { + append (0x9f); + } + else + { + writeHead (0x80, size); + } + ++_depth; + return 0; + } + + /** + * @brief stop array. + * @return 0 on success, -1 otherwise. + */ + int stopArray () + { + if (JOIN_UNLIKELY (_depth == 0)) + { + join::lastError = make_error_code (SaxErrc::InvalidParent); + reset (); + return -1; + } + if constexpr (WriteMode == CborWriteMode::Indefinite) + { + append (0xff); + } + --_depth; + endValue (); + return 0; + } + + /** + * @brief start object of unknown size. + * @return 0 on success, -1 otherwise. + */ + int startObject () + { + static_assert (WriteMode == CborWriteMode::Indefinite, "definite length containers require a size"); + append (0xbf); + ++_depth; + return 0; + } + + /** + * @brief start object. + * @param size object size. + * @return 0 on success, -1 otherwise. + */ + int startObject ([[maybe_unused]] uint32_t size) + { + if constexpr (WriteMode == CborWriteMode::Indefinite) + { + append (0xbf); + } + else + { + writeHead (0xa0, size); + } + ++_depth; + return 0; + } + + /** + * @brief get object members in writing order. + * @param object object to write. + * @return object members in insertion order. + */ + template + const View& members (const View& object) const noexcept + { + return object; + } + + /** + * @brief set scalar key. + * @param key object key. + * @return 0 on success, -1 otherwise. + */ + template + int setKey ([[maybe_unused]] Key key) + { + if (JOIN_UNLIKELY (_depth == 0)) + { + join::lastError = make_error_code (SaxErrc::InvalidParent); + reset (); + return -1; + } + + using Type = ScalarType; + + if constexpr (std::same_as) + { + return setNull (); + } + else if constexpr (std::same_as) + { + return setBool (static_cast (key)); + } + else if constexpr (std::same_as) + { + return setInt (static_cast (key)); + } + else if constexpr (std::same_as) + { + return setUint (static_cast (key)); + } + else if constexpr (std::same_as) + { + return setInt64 (static_cast (key)); + } + else if constexpr (std::same_as) + { + return setUint64 (static_cast (key)); + } + else + { + return setDouble (static_cast (key)); + } + } + + /** + * @brief set string key. + * @param key object key. + * @return 0 on success, -1 otherwise. + */ + int setKey (std::string_view key) + { + if (JOIN_UNLIKELY (_depth == 0)) + { + join::lastError = make_error_code (SaxErrc::InvalidParent); + reset (); + return -1; + } + return setString (key); + } + + /** + * @brief set binary key. + * @param key object key. + * @param subtype binary subtype, not written. + * @return 0 on success, -1 otherwise. + */ + int setKey (std::span key, std::optional subtype = std::nullopt) + { + if (JOIN_UNLIKELY (_depth == 0)) + { + join::lastError = make_error_code (SaxErrc::InvalidParent); + reset (); + return -1; + } + return setBinary (key, subtype); + } + + /** + * @brief start an array or object key. + * @return 0 on success, -1 otherwise. + */ + int startKey () + { + if (JOIN_UNLIKELY (_depth == 0)) + { + join::lastError = make_error_code (SaxErrc::InvalidParent); + reset (); + return -1; + } + return 0; + } + + /** + * @brief stop object. + * @return 0 on success, -1 otherwise. + */ + int stopObject () + { + if (JOIN_UNLIKELY (_depth == 0)) + { + join::lastError = make_error_code (SaxErrc::InvalidParent); + reset (); + return -1; + } + if constexpr (WriteMode == CborWriteMode::Indefinite) + { + append (0xff); + } + --_depth; + endValue (); + return 0; + } + + /** + * @brief flush buffered data to output stream. + */ + void flush () + { + if (_size) + { + _out->sputn (_buffer, _size); + _size = 0; + } + } + + /** + * @brief reset the writer state, discarding buffered data. + */ + void reset () noexcept + { + _size = 0; + _depth = 0; + } + + protected: + /** + * @brief append character to output stream in batch. + * @param data character to append to output stream. + */ + inline void append (char data) + { + if (JOIN_UNLIKELY (_size == sizeof (_buffer))) + { + flush (); + } + _buffer[_size++] = data; + } + + /** + * @brief append data to output stream in batch. + * @param data data to append to output stream. + * @param size data size. + */ + inline void append (const char* data, size_t size) + { + if (JOIN_UNLIKELY (size == 0)) + { + return; + } + if (JOIN_UNLIKELY (size > sizeof (_buffer) - _size)) + { + flush (); + if (size >= sizeof (_buffer)) + { + _out->sputn (data, size); + return; + } + } + std::memcpy (_buffer + _size, data, size); + _size += size; + } + + /** + * @brief pack value into stream. + * @param value value to pack. + */ + template + void pack (Type value) + { + append (reinterpret_cast (&swap (value)), sizeof (value)); + } + + /** + * @brief write a data item head with the shortest argument encoding. + * @param major major type, already shifted. + * @param value head argument. + */ + inline void writeHead (uint8_t major, uint64_t value) + { + if (value < 24) + { + append (static_cast (major | value)); + } + else if (value < (1ULL << 8)) + { + append (static_cast (major | 24)); + pack (static_cast (value)); + } + else if (value < (1ULL << 16)) + { + append (static_cast (major | 25)); + pack (static_cast (value)); + } + else if (value < (1ULL << 32)) + { + append (static_cast (major | 26)); + pack (static_cast (value)); + } + else + { + append (static_cast (major | 27)); + pack (value); + } + } + + /** + * @brief mark the end of a value, flushing the document once complete. + */ + inline void endValue () + { + if (_depth == 0) + { + flush (); + } + } + + /// underlying output stream. + std::streambuf* _out; + + /// output buffer. + char _buffer[1024] = {}; + + /// output buffer size. + size_t _size = 0; + + /// number of open containers. + size_t _depth = 0; + }; + + /// CBOR writer with definite length containers. + using CborWriter = BasicCborWriter; + + /// CBOR writer with indefinite length containers. + using CborStreamWriter = BasicCborWriter; + + /** + * @brief CBOR deserialization mode. + */ + enum class CborReadMode : uint32_t + { + None = 0, /**< no read mode set. */ + ValidateEncoding = 1L << 0, /**< validate UTF-8 encoding of text strings. */ + StopParsingOnDone = 1L << 1, /**< stop parsing on done. */ + }; + + /** + * @brief perform binary AND on CborReadMode. + * @param a bitset. + * @param b other bitset. + * @return bitset result of binary AND on CborReadMode. + */ + constexpr CborReadMode operator& (CborReadMode a, CborReadMode b) noexcept + { + return CborReadMode (static_cast (a) & static_cast (b)); + } + + /** + * @brief perform binary OR on CborReadMode. + * @param a bitset. + * @param b other bitset. + * @return bitset result of binary OR on CborReadMode. + */ + constexpr CborReadMode operator| (CborReadMode a, CborReadMode b) noexcept + { + return CborReadMode (static_cast (a) | static_cast (b)); + } + + /** + * @brief perform binary XOR on CborReadMode. + * @param a bitset. + * @param b other bitset. + * @return bitset result of binary XOR on CborReadMode. + */ + constexpr CborReadMode operator^ (CborReadMode a, CborReadMode b) noexcept + { + return CborReadMode (static_cast (a) ^ static_cast (b)); + } + + /** + * @brief perform binary NOT on CborReadMode. + * @param a bitset. + * @return bitset result of binary NOT on CborReadMode. + */ + constexpr CborReadMode operator~(CborReadMode a) noexcept + { + return CborReadMode (~static_cast (a)); + } + + /** + * @brief perform binary AND assignment on CborReadMode. + * @param a bitset. + * @param b other bitset. + * @return reference to updated bitset. + */ + constexpr const CborReadMode& operator&= (CborReadMode& a, CborReadMode b) noexcept + { + return a = a & b; + } + + /** + * @brief perform binary OR assignment on CborReadMode. + * @param a bitset. + * @param b other bitset. + * @return reference to updated bitset. + */ + constexpr const CborReadMode& operator|= (CborReadMode& a, CborReadMode b) noexcept + { + return a = a | b; + } + + /** + * @brief perform binary XOR assignment on CborReadMode. + * @param a bitset. + * @param b other bitset. + * @return reference to updated bitset. + */ + constexpr const CborReadMode& operator^= (CborReadMode& a, CborReadMode b) noexcept + { + return a = a ^ b; + } + + /** + * @brief CBOR reader class. + */ + template + class BasicCborReader + { + public: + /** + * @brief default constructor. + */ + BasicCborReader () = default; + + /** + * @brief copy constructor. + * @param other object to copy. + */ + BasicCborReader (const BasicCborReader& other) = delete; + + /** + * @brief copy assignment. + * @param other object to copy. + * @return a reference of the current object. + */ + BasicCborReader& operator= (const BasicCborReader& other) = delete; + + /** + * @brief move constructor. + * @param other object to move. + */ + BasicCborReader (BasicCborReader&& other) = delete; + + /** + * @brief move assignment. + * @param other object to move. + * @return a reference of the current object. + */ + BasicCborReader& operator= (BasicCborReader&& other) = delete; + + /** + * @brief destroy instance. + */ + ~BasicCborReader () = default; + + /** + * @brief deserialize a document. + * @param document document to parse. + * @param length The length of the document to parse. + * @param handler handler receiving the document. + * @return 0 on success, -1 otherwise. + */ + template + int deserialize (const char* document, size_t length, Handler& handler) + { + dconv::StringProbe in (document, length); + return read (in, handler); + } + + /** + * @brief deserialize a document. + * @param first The first character of the document to parse. + * @param last The last character of the document to parse. + * @param handler handler receiving the document. + * @return 0 on success, -1 otherwise. + */ + template + int deserialize (const char* first, const char* last, Handler& handler) + { + dconv::StringProbe in (first, last); + return read (in, handler); + } + + /** + * @brief deserialize a document. + * @param document document to parse. + * @param handler handler receiving the document. + * @return 0 on success, -1 otherwise. + */ + template + int deserialize (std::string_view document, Handler& handler) + { + dconv::StringProbe in (document.data (), document.size ()); + return read (in, handler); + } + + /** + * @brief deserialize a document. + * @param document document to deserialize. + * @param handler handler receiving the document. + * @return 0 on success, -1 otherwise. + */ + template + int deserialize (std::istream& document, Handler& handler) + { + dconv::StreamProbe in (document); + return read (in, handler); + } + + protected: + /** + * @brief parse a document into a value. + * @param document document to parse. + * @param value value receiving the document. + * @return 0 on success, -1 otherwise. + */ + template + int read (ProbeType& document, BasicValue& value) + { + int result = read (document, ValueBuilder (value)); + if (JOIN_UNLIKELY (result != 0)) + { + value = nullptr; + } + return result; + } + + /** + * @brief parse a document, resetting the handler on failure. + * @param document document to parse. + * @param handler handler receiving the document. + * @return 0 on success, -1 otherwise. + */ + template + int read (ProbeType& document, Handler&& handler) + { + try + { + if (JOIN_LIKELY (readValue (document, handler, 0) == 0)) + { + if constexpr ((ReadMode & CborReadMode::StopParsingOnDone) != CborReadMode::None) + { + return 0; + } + + if (JOIN_LIKELY (document.peek () == std::char_traits::eof ())) + { + return 0; + } + + join::lastError = make_error_code (SaxErrc::ExtraData); + } + } + catch (...) + { + handler.reset (); + throw; + } + handler.reset (); + return -1; + } + + /** + * @brief parse a data item, skipping its tags. + * @param document document to parse. + * @param handler handler receiving the document. + * @param depth number of enclosing containers. + * @param isKey indicate whether the element to parse is a key or not. + * @return 0 on success, -1 otherwise. + */ + template + int readValue (ProbeType& document, Handler& handler, size_t depth, bool isKey = false) + { + uint8_t head; + if (JOIN_UNLIKELY (readHead (document, head) == -1)) + { + return -1; + } + + while ((head >> 5) == 6) + { + uint64_t tag; + if (JOIN_UNLIKELY (readArgument (document, head & 0x1f, tag) == -1)) + { + return -1; + } + if (JOIN_UNLIKELY (readHead (document, head) == -1)) + { + return -1; + } + } + + uint8_t info = head & 0x1f; + + switch (head >> 5) + { + case 0: + return readUnsigned (document, handler, info, isKey); + + case 1: + return readNegative (document, handler, info, isKey); + + case 2: + return readString (document, handler, info, isKey); + + case 3: + return readString (document, handler, info, isKey); + + case 4: + return readArray (document, handler, info, depth, isKey); + + case 5: + return readObject (document, handler, info, depth, isKey); + + default: + return readSimple (document, handler, info, isKey); + } + } + + /** + * @brief parse an unsigned integer. + * @param document document to parse. + * @param handler handler receiving the document. + * @param info head additional information. + * @param isKey indicate whether the element to parse is a key or not. + * @return 0 on success, -1 otherwise. + */ + template + int readUnsigned (ProbeType& document, Handler& handler, uint8_t info, bool isKey) + { + uint64_t value; + if (JOIN_UNLIKELY (readArgument (document, info, value) == -1)) + { + return -1; + } + + if (value <= std::numeric_limits::max ()) + { + auto number = static_cast (value); + return isKey ? handler.setKey (number) : handler.setUint (number); + } + + return isKey ? handler.setKey (value) : handler.setUint64 (value); + } + + /** + * @brief parse a negative integer. + * @param document document to parse. + * @param handler handler receiving the document. + * @param info head additional information. + * @param isKey indicate whether the element to parse is a key or not. + * @return 0 on success, -1 otherwise. + */ + template + int readNegative (ProbeType& document, Handler& handler, uint8_t info, bool isKey) + { + uint64_t value; + if (JOIN_UNLIKELY (readArgument (document, info, value) == -1)) + { + return -1; + } + + if (JOIN_UNLIKELY (value > static_cast (std::numeric_limits::max ()))) + { + join::lastError = make_error_code (SaxErrc::InvalidValue); + return -1; + } + + int64_t number = -1 - static_cast (value); + + if (number >= std::numeric_limits::min ()) + { + auto small = static_cast (number); + return isKey ? handler.setKey (small) : handler.setInt (small); + } + + return isKey ? handler.setKey (number) : handler.setInt64 (number); + } + + /** + * @brief parse a byte or text string. + * @param document document to parse. + * @param handler handler receiving the document. + * @param info head additional information. + * @param isKey indicate whether the element to parse is a key or not. + * @return 0 on success, -1 otherwise. + */ + template + int readString (ProbeType& document, Handler& handler, uint8_t info, bool isKey) + { + if (info == 31) + { + return readChunks (document, handler, isKey); + } + + uint64_t len; + if (JOIN_UNLIKELY (readArgument (document, info, len) == -1)) + { + return -1; + } + + if (JOIN_UNLIKELY (len > std::numeric_limits::max ())) + { + join::lastError = make_error_code (SaxErrc::InvalidValue); + return -1; + } + + if constexpr (std::is_same_v) + { + if (JOIN_UNLIKELY (static_cast (document.end () - document.tell ()) < len)) + { + join::lastError = make_error_code (SaxErrc::Truncated); + return -1; + } + + const char* data = document.tell (); + if (JOIN_UNLIKELY (validateString (data, len) == -1)) + { + return -1; + } + document.seek (data + len); + return setString (handler, data, len, isKey); + } + + thread_local std::string cache; + std::string output = std::move (cache); + output.clear (); + if (JOIN_UNLIKELY (readBytes (document, static_cast (len), output) == -1)) + { + return -1; + } + + if (JOIN_UNLIKELY (validateString (output.data (), output.size ()) == -1)) + { + return -1; + } + + int result = setString (handler, output.data (), output.size (), isKey); + cache = std::move (output); + return result; + } + + /** + * @brief parse an indefinite length byte or text string, concatenating its chunks. + * @param document document to parse. + * @param handler handler receiving the document. + * @param isKey indicate whether the element to parse is a key or not. + * @return 0 on success, -1 otherwise. + */ + template + int readChunks (ProbeType& document, Handler& handler, bool isKey) + { + thread_local std::string cache; + std::string output = std::move (cache); + output.clear (); + + while (!document.getIf (0xff)) + { + uint8_t head; + if (JOIN_UNLIKELY (readHead (document, head) == -1)) + { + return -1; + } + + if (JOIN_UNLIKELY ((head >> 5) != (Text ? 3 : 2) || (head & 0x1f) == 31)) + { + join::lastError = make_error_code (SaxErrc::InvalidValue); + return -1; + } + + uint64_t len; + if (JOIN_UNLIKELY (readArgument (document, head & 0x1f, len) == -1)) + { + return -1; + } + + if (JOIN_UNLIKELY (len > std::numeric_limits::max ())) + { + join::lastError = make_error_code (SaxErrc::InvalidValue); + return -1; + } + + size_t pos = output.size (); + if (JOIN_UNLIKELY (readBytes (document, static_cast (len), output) == -1)) + { + return -1; + } + + if (JOIN_UNLIKELY (validateString (output.data () + pos, output.size () - pos) == -1)) + { + return -1; + } + } + + int result = setString (handler, output.data (), output.size (), isKey); + cache = std::move (output); + return result; + } + + /** + * @brief validate the UTF-8 encoding of a text string when required by the read mode. + * @param data string data. + * @param len string length. + * @return 0 on success, -1 otherwise. + */ + template + static int validateString ([[maybe_unused]] const char* data, [[maybe_unused]] size_t len) noexcept + { + if constexpr (Text && (ReadMode & CborReadMode::ValidateEncoding) != CborReadMode::None) + { + if (JOIN_UNLIKELY (utf8::validate (data, data + len) == -1)) + { + join::lastError = make_error_code (SaxErrc::InvalidEncoding); + return -1; + } + } + return 0; + } + + /** + * @brief deliver a byte or text string to the handler. + * @param handler handler receiving the document. + * @param data string data. + * @param len string length. + * @param isKey indicate whether the element is a key or not. + * @return 0 on success, -1 otherwise. + */ + template + static int setString (Handler& handler, const char* data, size_t len, bool isKey) + { + if constexpr (Text) + { + std::string_view value (data, len); + return isKey ? handler.setKey (value) : handler.setString (value); + } + else + { + std::span value (reinterpret_cast (data), len); + return isKey ? handler.setKey (value) : handler.setBinary (value); + } + } + + /** + * @brief parse an array. + * @param document document to parse. + * @param handler handler receiving the document. + * @param info head additional information. + * @param depth number of enclosing containers. + * @param isKey indicate whether the element to parse is a key or not. + * @return 0 on success, -1 otherwise. + */ + template + __attribute__ ((noinline)) int readArray (ProbeType& document, Handler& handler, uint8_t info, size_t depth, + bool isKey) + { + if (JOIN_UNLIKELY (depth == saxMaxDepth)) + { + join::lastError = make_error_code (SaxErrc::StackOverflow); + return -1; + } + + if (isKey && JOIN_UNLIKELY (handler.startKey () == -1)) + { + return -1; + } + + if (info == 31) + { + if (JOIN_UNLIKELY (handler.startArray () == -1)) + { + return -1; + } + + while (!document.getIf (0xff)) + { + if (JOIN_UNLIKELY (readValue (document, handler, depth + 1) == -1)) + { + return -1; + } + } + + return handler.stopArray (); + } + + uint64_t len; + if (JOIN_UNLIKELY (readArgument (document, info, len) == -1)) + { + return -1; + } + + if (JOIN_UNLIKELY (len > std::numeric_limits::max ())) + { + join::lastError = make_error_code (SaxErrc::InvalidValue); + return -1; + } + + if (JOIN_UNLIKELY (handler.startArray (static_cast (len)) == -1)) + { + return -1; + } + + while (len) + { + if (JOIN_UNLIKELY (readValue (document, handler, depth + 1) == -1)) + { + return -1; + } + + --len; + } + + return handler.stopArray (); + } + + /** + * @brief parse a map. + * @param document document to parse. + * @param handler handler receiving the document. + * @param info head additional information. + * @param depth number of enclosing containers. + * @param isKey indicate whether the element to parse is a key or not. + * @return 0 on success, -1 otherwise. + */ + template + __attribute__ ((noinline)) int readObject (ProbeType& document, Handler& handler, uint8_t info, size_t depth, + bool isKey) + { + if (JOIN_UNLIKELY (depth == saxMaxDepth)) + { + join::lastError = make_error_code (SaxErrc::StackOverflow); + return -1; + } + + if (isKey && JOIN_UNLIKELY (handler.startKey () == -1)) + { + return -1; + } + + if (info == 31) + { + if (JOIN_UNLIKELY (handler.startObject () == -1)) + { + return -1; + } + + while (!document.getIf (0xff)) + { + if (JOIN_UNLIKELY (readValue (document, handler, depth + 1, true) == -1)) + { + return -1; + } + + if (JOIN_UNLIKELY (document.getIf (0xff))) + { + join::lastError = make_error_code (SaxErrc::InvalidKey); + return -1; + } + + if (JOIN_UNLIKELY (readValue (document, handler, depth + 1) == -1)) + { + return -1; + } + } + + return handler.stopObject (); + } + + uint64_t len; + if (JOIN_UNLIKELY (readArgument (document, info, len) == -1)) + { + return -1; + } + + if (JOIN_UNLIKELY (len > std::numeric_limits::max ())) + { + join::lastError = make_error_code (SaxErrc::InvalidValue); + return -1; + } + + if (JOIN_UNLIKELY (handler.startObject (static_cast (len)) == -1)) + { + return -1; + } + + while (len) + { + if (JOIN_UNLIKELY (readValue (document, handler, depth + 1, true) == -1)) + { + return -1; + } + + if (JOIN_UNLIKELY (readValue (document, handler, depth + 1) == -1)) + { + return -1; + } + + --len; + } + + return handler.stopObject (); + } + + /** + * @brief parse a simple value or a floating point number. + * @param document document to parse. + * @param handler handler receiving the document. + * @param info head additional information. + * @param isKey indicate whether the element to parse is a key or not. + * @return 0 on success, -1 otherwise. + */ + template + int readSimple (ProbeType& document, Handler& handler, uint8_t info, bool isKey) + { + switch (info) + { + case 20: + return isKey ? handler.setKey (false) : handler.setBool (false); + + case 21: + return isKey ? handler.setKey (true) : handler.setBool (true); + + case 22: + case 23: + return isKey ? handler.setKey (nullptr) : handler.setNull (); + + case 25: + { + uint16_t half; + if (JOIN_UNLIKELY (unpack (document, half) == -1)) + { + return -1; + } + double number = decodeHalf (half); + return isKey ? handler.setKey (number) : handler.setDouble (number); + } + + case 26: + { + double number; + if (JOIN_UNLIKELY (unpack (document, number) == -1)) + { + return -1; + } + return isKey ? handler.setKey (number) : handler.setDouble (number); + } + + case 27: + { + double number; + if (JOIN_UNLIKELY (unpack (document, number) == -1)) + { + return -1; + } + return isKey ? handler.setKey (number) : handler.setDouble (number); + } + + default: + join::lastError = make_error_code (SaxErrc::InvalidValue); + return -1; + } + } + + /** + * @brief read a data item head. + * @param document document to parse. + * @param head head read. + * @return 0 on success, -1 otherwise. + */ + template + static int readHead (ProbeType& document, uint8_t& head) + { + int ch = document.get (); + if (JOIN_UNLIKELY (ch == std::char_traits::eof ())) + { + join::lastError = make_error_code (SaxErrc::Truncated); + return -1; + } + head = static_cast (ch); + return 0; + } + + /** + * @brief read a data item head argument. + * @param document document to parse. + * @param info head additional information. + * @param value argument read. + * @return 0 on success, -1 otherwise. + */ + template + static int readArgument (ProbeType& document, uint8_t info, uint64_t& value) + { + switch (info) + { + case 24: + return unpack (document, value); + + case 25: + return unpack (document, value); + + case 26: + return unpack (document, value); + + case 27: + return unpack (document, value); + + default: + if (JOIN_UNLIKELY (info > 27)) + { + join::lastError = make_error_code (SaxErrc::InvalidValue); + return -1; + } + value = info; + return 0; + } + } + + /** + * @brief decode a half precision floating point number. + * @param half half precision bits. + * @return decoded number. + */ + static double decodeHalf (uint16_t half) noexcept + { + int exponent = (half >> 10) & 0x1f; + int mantissa = half & 0x3ff; + double value; + + if (exponent == 0) + { + value = std::ldexp (mantissa, -24); + } + else if (exponent != 31) + { + value = std::ldexp (mantissa + 1024, exponent - 25); + } + else if (mantissa == 0) + { + value = std::numeric_limits::infinity (); + } + else + { + value = std::numeric_limits::quiet_NaN (); + } + + return (half & 0x8000) ? -value : value; + } + + /** + * @brief read bytes by chunks. + * @param document document to parse. + * @param len number of bytes to read. + * @param output string receiving the bytes. + * @return 0 on success, -1 otherwise. + */ + template + static int readBytes (ProbeType& document, uint32_t len, std::string& output) + { + while (len) + { + size_t pos = output.size (); + size_t chunk = std::min (len, 65536); + output.resize (pos + chunk); + if (JOIN_UNLIKELY (document.read (&output[pos], chunk) != chunk)) + { + join::lastError = make_error_code (SaxErrc::Truncated); + return -1; + } + len -= chunk; + } + return 0; + } + + /** + * @brief unpack value from stream. + * @param document document to parse. + * @param output unpacked value. + * @return 0 on success, -1 otherwise. + */ + template + static int unpack (ProbeType& document, Output& output) + requires std::is_arithmetic_v + { + Type value; + if (JOIN_UNLIKELY (document.read (reinterpret_cast (&value), sizeof (value)) != sizeof (value))) + { + join::lastError = make_error_code (SaxErrc::Truncated); + return -1; + } + output = static_cast (swap (value)); + return 0; + } + }; + + /// CBOR reader. + using CborReader = BasicCborReader; +} + +#endif diff --git a/data/include/join/pack.hpp b/data/include/join/pack.hpp index d5dab45f..6eaf647c 100644 --- a/data/include/join/pack.hpp +++ b/data/include/join/pack.hpp @@ -684,6 +684,10 @@ namespace join */ inline void append (const char* data, uint32_t size) { + if (JOIN_UNLIKELY (size == 0)) + { + return; + } if (JOIN_UNLIKELY (size > sizeof (_buffer) - _size)) { flush (); diff --git a/data/tests/CMakeLists.txt b/data/tests/CMakeLists.txt index 92d14d07..3d785adb 100644 --- a/data/tests/CMakeLists.txt +++ b/data/tests/CMakeLists.txt @@ -47,6 +47,31 @@ target_link_libraries(pack_roundtrip.gtest ${JOIN_DATA} GTest::gtest_main) add_test(NAME pack_roundtrip.gtest COMMAND pack_roundtrip.gtest) install(TARGETS pack_roundtrip.gtest RUNTIME DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}/test) +add_executable(cbor_reader.gtest cbor_reader_test.cpp) +target_link_libraries(cbor_reader.gtest ${JOIN_DATA} GTest::gtest_main) +add_test(NAME cbor_reader.gtest COMMAND cbor_reader.gtest) +install(TARGETS cbor_reader.gtest RUNTIME DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}/test) + +add_executable(cbor_writer.gtest cbor_writer_test.cpp) +target_link_libraries(cbor_writer.gtest ${JOIN_DATA} GTest::gtest_main) +add_test(NAME cbor_writer.gtest COMMAND cbor_writer.gtest) +install(TARGETS cbor_writer.gtest RUNTIME DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}/test) + +add_executable(cbor_stream_writer.gtest cbor_stream_writer_test.cpp) +target_link_libraries(cbor_stream_writer.gtest ${JOIN_DATA} GTest::gtest_main) +add_test(NAME cbor_stream_writer.gtest COMMAND cbor_stream_writer.gtest) +install(TARGETS cbor_stream_writer.gtest RUNTIME DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}/test) + +add_executable(cbor_roundtrip.gtest cbor_roundtrip_test.cpp) +target_link_libraries(cbor_roundtrip.gtest ${JOIN_DATA} GTest::gtest_main) +add_test(NAME cbor_roundtrip.gtest COMMAND cbor_roundtrip.gtest) +install(TARGETS cbor_roundtrip.gtest RUNTIME DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}/test) + +add_executable(cbor_stream_roundtrip.gtest cbor_stream_roundtrip_test.cpp) +target_link_libraries(cbor_stream_roundtrip.gtest ${JOIN_DATA} GTest::gtest_main) +add_test(NAME cbor_stream_roundtrip.gtest COMMAND cbor_stream_roundtrip.gtest) +install(TARGETS cbor_stream_roundtrip.gtest RUNTIME DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}/test) + add_executable(json_error.gtest json_error_test.cpp) target_link_libraries(json_error.gtest ${JOIN_DATA} GTest::gtest_main) add_test(NAME json_error.gtest COMMAND json_error.gtest) diff --git a/data/tests/cbor_reader_test.cpp b/data/tests/cbor_reader_test.cpp new file mode 100644 index 00000000..205d03b8 --- /dev/null +++ b/data/tests/cbor_reader_test.cpp @@ -0,0 +1,701 @@ +/** + * MIT License + * + * Copyright (c) 2026 Mathieu Rabine + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// libjoin. +#include + +// libraries. +#include + +// C++. +#include +#include +#include +#include +#include +#include +#include + +// C. +#include + +using join::Value; +using join::SaxErrc; +using join::CborReader; +using join::CborStreamWriter; + +/** + * @brief convert hexadecimal digits to bytes. + * @param digits hexadecimal digits. + * @return bytes. + */ +static std::string hex (std::string_view digits) +{ + std::string bytes; + for (size_t i = 0; i + 1 < digits.size (); i += 2) + { + bytes += static_cast (std::stoi (std::string (digits.substr (i, 2)), nullptr, 16)); + } + return bytes; +} + +/** + * @brief handler recording the events it receives. + */ +struct Recorder : public join::SaxHandler +{ + int setNull () + { + return record ("null"); + } + + int setBool (bool value) + { + return record (value ? "true" : "false"); + } + + int setInt (int32_t value) + { + return record ("i:" + std::to_string (value)); + } + + int setUint (uint32_t value) + { + return record ("u:" + std::to_string (value)); + } + + int setInt64 (int64_t value) + { + return record ("I:" + std::to_string (value)); + } + + int setUint64 (uint64_t value) + { + return record ("U:" + std::to_string (value)); + } + + int setDouble (double value) + { + doubles.push_back (value); + return record ("d"); + } + + int setString (std::string_view value) + { + return record ("s:" + std::string (value)); + } + + int setBinary (std::span value, std::optional = std::nullopt) + { + return record ("b:" + std::to_string (value.size ())); + } + + int startArray (uint32_t size = 0) + { + return record ("[" + std::to_string (size)); + } + + int stopArray () + { + return record ("]"); + } + + int startObject (uint32_t size = 0) + { + return record ("{" + std::to_string (size)); + } + + int stopObject () + { + return record ("}"); + } + + int setKey (std::nullptr_t) + { + return record ("knull"); + } + + int setKey (bool value) + { + return record (value ? "ktrue" : "kfalse"); + } + + int setKey (int32_t value) + { + return record ("ki:" + std::to_string (value)); + } + + int setKey (uint32_t value) + { + return record ("ku:" + std::to_string (value)); + } + + int setKey (int64_t value) + { + return record ("kI:" + std::to_string (value)); + } + + int setKey (uint64_t value) + { + return record ("kU:" + std::to_string (value)); + } + + int setKey (double value) + { + doubles.push_back (value); + return record ("kd"); + } + + int setKey (std::string_view value) + { + return record ("ks:" + std::string (value)); + } + + int setKey (std::span value, std::optional = std::nullopt) + { + return record ("kb:" + std::to_string (value.size ())); + } + + int startKey () + { + return record ("k"); + } + + int record (const std::string& event) + { + log += log.empty () ? event : " " + event; + return 0; + } + + std::string log; + std::vector doubles; +}; + +/** + * @brief handler rejecting array and object keys. + */ +struct RejectKey : public join::SaxHandler +{ + int startKey () + { + return -1; + } +}; + +/** + * @brief handler throwing on strings. + */ +struct Throwing : public join::SaxHandler +{ + int setString (std::string_view) + { + throw std::range_error ("user"); + } +}; + +/** + * @brief handler parsing the strings it receives as nested documents. + */ +struct Nested : public join::SaxHandler +{ + int setString (std::string_view value) + { + if (value.starts_with ('\x81')) + { + Nested inner; + std::stringstream stream{std::string (value)}; + if (CborReader ().deserialize (stream, inner) == -1) + { + return -1; + } + strings.insert (strings.end (), inner.strings.begin (), inner.strings.end ()); + } + strings.emplace_back (value); + return 0; + } + + std::vector strings; +}; + +/** + * @brief parse a document from a string and from a stream. + * @param document document to parse. + * @return events recorded, identical for both inputs, or "error". + */ +static std::string events (const std::string& document) +{ + Recorder fromString, fromStream; + std::stringstream stream (document); + if (CborReader ().deserialize (document, fromString) != 0 || CborReader ().deserialize (stream, fromStream) != 0 || + fromString.log != fromStream.log) + { + return "error"; + } + return fromString.log; +} + +/** + * @brief parse a document expected to fail from a string and from a stream. + * @param document document to parse. + * @return error, identical for both inputs, or an empty error code. + */ +static std::error_code error (const std::string& document) +{ + join::SaxHandler handler; + std::stringstream stream (document); + if (CborReader ().deserialize (stream, handler) != -1) + { + return {}; + } + std::error_code fromStream = join::lastError; + if (CborReader ().deserialize (document, handler) != -1 || join::lastError != fromStream) + { + return {}; + } + return fromStream; +} + +/** + * @brief Test deserialize method. + */ +TEST (CborReader, deserialize) +{ + std::stringstream stream; + Value value; + + std::string str = hex ("8101"); + char data[] = {'\x81', '\x01', '\x00'}; + + stream.str (str); + ASSERT_EQ (CborReader ().deserialize (stream, value), 0) << join::lastError.message (); + ASSERT_EQ (value.size (), 1); + ASSERT_EQ (value[0], 1); + + ASSERT_EQ (CborReader ().deserialize (str, value), 0) << join::lastError.message (); + ASSERT_EQ (value[0], 1); + + ASSERT_EQ (CborReader ().deserialize (data, sizeof (data) - 1, value), 0) << join::lastError.message (); + ASSERT_EQ (value[0], 1); + + ASSERT_EQ (CborReader ().deserialize (&data[0], &data[sizeof (data) - 1], value), 0) << join::lastError.message (); + ASSERT_EQ (value[0], 1); + + ASSERT_EQ (CborReader ().deserialize (&data[0], &data[sizeof (data)], value), -1); + ASSERT_EQ (join::lastError, SaxErrc::ExtraData); + ASSERT_TRUE (value.isNull ()); + + ASSERT_EQ (error (""), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("18")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("19")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("1a")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("1b")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("38")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("39")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("3a")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("3b")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("58")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("59")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("5a")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("5b")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("78")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("79")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("7a")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("7b")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("98")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("99")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("9a")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("9b")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("b8")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("b9")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("ba")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("bb")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("d8")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("d9")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("da")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("db")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("f9")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("fa")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("fb")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("c0")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("81")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("82f6")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("9f")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("9ff6")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("a1")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("a1f6")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("bf")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("bff6")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("6461")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("4461")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("5f")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("7f")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("5f41")), SaxErrc::Truncated); + ASSERT_EQ (error (hex ("7f61")), SaxErrc::Truncated); + + std::string nested = std::string (256, '\x81') + '\x01'; + ASSERT_EQ (CborReader ().deserialize (nested, value), 0) << join::lastError.message (); + ASSERT_EQ (error ('\x81' + nested), SaxErrc::StackOverflow); + ASSERT_EQ (error (std::string (257, '\x9f')), SaxErrc::StackOverflow); + std::string objects; + for (int i = 0; i < 257; ++i) + { + objects += hex ("a16161"); + } + ASSERT_EQ (error (objects + '\x01'), SaxErrc::StackOverflow); + + std::stringstream out; + CborStreamWriter writer (out); + ASSERT_EQ (CborReader ().deserialize (nested, writer), 0) << join::lastError.message (); + ASSERT_EQ (out.str (), std::string (256, '\x9f') + '\x01' + std::string (256, '\xff')); + + Throwing throwing; + ASSERT_THROW (CborReader ().deserialize (hex ("816161"), throwing), std::range_error); + + join::ValueBuilder builder (value); + for (size_t depth = 0; depth < join::saxMaxDepth; ++depth) + { + builder.startArray (); + } + ASSERT_EQ (CborReader ().deserialize (hex ("8101"), builder), -1); + for (size_t depth = 0; depth < join::saxMaxDepth; ++depth) + { + builder.startArray (); + } + ASSERT_EQ (CborReader ().deserialize (hex ("9f01ff"), builder), -1); + for (size_t depth = 0; depth < join::saxMaxDepth; ++depth) + { + builder.startArray (); + } + ASSERT_EQ (CborReader ().deserialize (hex ("a10101"), builder), -1); + for (size_t depth = 0; depth < join::saxMaxDepth; ++depth) + { + builder.startArray (); + } + ASSERT_EQ (CborReader ().deserialize (hex ("bf0101ff"), builder), -1); + + using Stopping = join::BasicCborReader; + std::stringstream messages (hex ("0102f5")); + 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 (hex ("0000"), value), 0) << join::lastError.message (); + + using Strict = join::BasicCborReader; + std::stringstream strict (hex ("616161ff")); + 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); +} + +/** + * @brief Test CBOR parsing of the RFC 8949 appendix A examples. + */ +TEST (CborReader, pass) +{ + ASSERT_EQ (events (hex ("00")), "u:0"); + ASSERT_EQ (events (hex ("0a")), "u:10"); + ASSERT_EQ (events (hex ("17")), "u:23"); + ASSERT_EQ (events (hex ("1818")), "u:24"); + ASSERT_EQ (events (hex ("1864")), "u:100"); + ASSERT_EQ (events (hex ("1903e8")), "u:1000"); + ASSERT_EQ (events (hex ("1a000f4240")), "u:1000000"); + ASSERT_EQ (events (hex ("1affffffff")), "u:4294967295"); + ASSERT_EQ (events (hex ("1b0000000100000000")), "U:4294967296"); + ASSERT_EQ (events (hex ("1b000000e8d4a51000")), "U:1000000000000"); + ASSERT_EQ (events (hex ("1bffffffffffffffff")), "U:18446744073709551615"); + ASSERT_EQ (events (hex ("20")), "i:-1"); + ASSERT_EQ (events (hex ("29")), "i:-10"); + ASSERT_EQ (events (hex ("3863")), "i:-100"); + ASSERT_EQ (events (hex ("3903e7")), "i:-1000"); + ASSERT_EQ (events (hex ("3a7fffffff")), "i:-2147483648"); + ASSERT_EQ (events (hex ("3a80000000")), "I:-2147483649"); + ASSERT_EQ (events (hex ("3b7fffffffffffffff")), "I:-9223372036854775808"); + ASSERT_EQ (events (hex ("f4")), "false"); + ASSERT_EQ (events (hex ("f5")), "true"); + ASSERT_EQ (events (hex ("f6")), "null"); + ASSERT_EQ (events (hex ("f7")), "null"); + ASSERT_EQ (events (hex ("80")), "[0 ]"); + ASSERT_EQ (events (hex ("83010203")), "[3 u:1 u:2 u:3 ]"); + ASSERT_EQ (events (hex ("8301820203820405")), "[3 u:1 [2 u:2 u:3 ] [2 u:4 u:5 ] ]"); + ASSERT_EQ (events (hex ("a0")), "{0 }"); + ASSERT_EQ (events (hex ("a201020304")), "{2 ku:1 u:2 ku:3 u:4 }"); + ASSERT_EQ (events (hex ("a26161016162820203")), "{2 ks:a u:1 ks:b [2 u:2 u:3 ] }"); + ASSERT_EQ (events (hex ("826161a161626163")), "[2 s:a {1 ks:b s:c } ]"); + ASSERT_EQ (events (hex ("9fff")), "[0 ]"); + ASSERT_EQ (events (hex ("9f018202039f0405ffff")), "[0 u:1 [2 u:2 u:3 ] [0 u:4 u:5 ] ]"); + ASSERT_EQ (events (hex ("9f01820203820405ff")), "[0 u:1 [2 u:2 u:3 ] [2 u:4 u:5 ] ]"); + ASSERT_EQ (events (hex ("83018202039f0405ff")), "[3 u:1 [2 u:2 u:3 ] [0 u:4 u:5 ] ]"); + ASSERT_EQ (events (hex ("83019f0203ff820405")), "[3 u:1 [0 u:2 u:3 ] [2 u:4 u:5 ] ]"); + ASSERT_EQ (events (hex ("bf61610161629f0203ffff")), "{0 ks:a u:1 ks:b [0 u:2 u:3 ] }"); + ASSERT_EQ (events (hex ("826161bf61626163ff")), "[2 s:a {0 ks:b s:c } ]"); + ASSERT_EQ (events (hex ("bf6346756ef563416d7421ff")), "{0 ks:Fun true ks:Amt i:-2 }"); +} + +/** + * @brief Test CBOR parsing fail. + */ +TEST (CborReader, fail) +{ + ASSERT_EQ (error (hex ("1c")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("1d")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("1e")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("1f")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("3c")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("3f")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("5c")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("7c")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("9c")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("bc")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("dc")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("df")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("fc")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("fe")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("ff")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("e0")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("f3")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("f800")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("f820")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("f8ff")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("c0ff")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("9fc0ff")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("3b8000000000000000")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("3bffffffffffffffff")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("5b0000000100000000")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("7b0000000100000000")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("9b0000000100000000")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("bb0000000100000000")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("5f6161ff")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("7f4161ff")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("5f5fffff")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("7f7fffff")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("5f5cff")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("7f7bffffffffffffffff")), SaxErrc::InvalidValue); + ASSERT_EQ (error (hex ("5f5b0000000100000000")), SaxErrc::InvalidValue); + + ASSERT_EQ (error (hex ("bf6161ff")), SaxErrc::InvalidKey); + ASSERT_EQ (error (hex ("0000")), SaxErrc::ExtraData); +} + +/** + * @brief Test CBOR parsing of floating point numbers. + */ +TEST (CborReader, dbl) +{ + auto number = [] (const char* doc) { + Recorder recorder; + EXPECT_EQ (CborReader ().deserialize (hex (doc), recorder), 0) << doc; + return recorder.doubles.size () == 1 ? recorder.doubles[0] : -1.0; + }; + + ASSERT_EQ (number ("f90000"), 0.0); + ASSERT_TRUE (std::signbit (number ("f98000"))); + ASSERT_EQ (number ("f93c00"), 1.0); + ASSERT_EQ (number ("f93e00"), 1.5); + ASSERT_EQ (number ("f97bff"), 65504.0); + ASSERT_EQ (number ("f90001"), 5.960464477539063e-8); + ASSERT_EQ (number ("f90400"), 6.103515625e-5); + ASSERT_EQ (number ("f9c400"), -4.0); + ASSERT_EQ (number ("f97c00"), std::numeric_limits::infinity ()); + ASSERT_EQ (number ("f9fc00"), -std::numeric_limits::infinity ()); + ASSERT_TRUE (std::isnan (number ("f97e00"))); + ASSERT_EQ (number ("fa47c35000"), 100000.0); + ASSERT_EQ (number ("fa7f7fffff"), 3.4028234663852886e+38); + ASSERT_EQ (number ("fa7f800000"), std::numeric_limits::infinity ()); + ASSERT_EQ (number ("fb3ff199999999999a"), 1.1); + ASSERT_EQ (number ("fb7e37e43c8800759c"), 1.0e+300); + ASSERT_TRUE (std::isnan (number ("fb7ff8000000000000"))); + ASSERT_EQ (events (hex ("83f93c00fa3fc00000fb3ff199999999999a")), "[3 d d d ]"); +} + +/** + * @brief Test CBOR parsing of text strings. + */ +TEST (CborReader, str) +{ + ASSERT_EQ (events (hex ("60")), "s:"); + ASSERT_EQ (events (hex ("6161")), "s:a"); + ASSERT_EQ (events (hex ("6449455446")), "s:IETF"); + ASSERT_EQ (events (hex ("62225c")), "s:\"\\"); + ASSERT_EQ (events (hex ("63e6b0b4")), "s:\xe6\xb0\xb4"); + ASSERT_EQ (events (hex ("7fff")), "s:"); + ASSERT_EQ (events (hex ("7f657374726561646d696e67ff")), "s:streaming"); + ASSERT_EQ (events (hex ("7f6060ff")), "s:"); + + std::string big (100000, 'x'); + ASSERT_EQ (events (hex ("7a000186a0") + big), "s:" + big); + ASSERT_EQ (events (hex ("7f7a000186a0") + big + hex ("6178ff")), "s:" + big + "x"); + + Nested nested; + std::stringstream stream (hex ("7f67817f61616162ffff")); + ASSERT_EQ (CborReader ().deserialize (stream, nested), 0) << join::lastError.message (); + ASSERT_EQ (nested.strings, std::vector ({"ab", hex ("817f61616162ff")})); +} + +/** + * @brief Test CBOR parsing of byte strings. + */ +TEST (CborReader, bin) +{ + Value value; + + ASSERT_EQ (events (hex ("40")), "b:0"); + ASSERT_EQ (events (hex ("4401020304")), "b:4"); + ASSERT_EQ (events (hex ("5fff")), "b:0"); + ASSERT_EQ (events (hex ("5f42010243030405ff")), "b:5"); + + ASSERT_EQ (CborReader ().deserialize (hex ("5f42010243030405ff"), value), 0) << join::lastError.message (); + ASSERT_TRUE (value.isBinary ()); + ASSERT_EQ (std::vector (value.getBinary ().begin (), value.getBinary ().end ()), + std::vector ({1, 2, 3, 4, 5})); + ASSERT_FALSE (value.getSubtype ().has_value ()); + + std::string big (100000, 'x'); + ASSERT_EQ (events (hex ("5a000186a0") + big), "b:100000"); +} + +/** + * @brief Test CBOR parsing of tags. + */ +TEST (CborReader, tag) +{ + ASSERT_EQ (events (hex ("c074323031332d30332d32315432303a30343a30305a")), "s:2013-03-21T20:04:00Z"); + ASSERT_EQ (events (hex ("c11a514b67b0")), "u:1363896240"); + ASSERT_EQ (events (hex ("c1fb41d452d9ec200000")), "d"); + ASSERT_EQ (events (hex ("d74401020304")), "b:4"); + ASSERT_EQ (events (hex ("d818456449455446")), "b:5"); + ASSERT_EQ (events (hex ("d82076687474703a2f2f7777772e6578616d706c652e636f6d")), "s:http://www.example.com"); + ASSERT_EQ (events (hex ("d9d9f7c101")), "u:1"); + ASSERT_EQ (events (std::string (100000, '\xc0') + '\x01'), "u:1"); + ASSERT_EQ (events (hex ("a1c10102")), "{1 ku:1 u:2 }"); +} + +/** + * @brief Test CBOR parsing of map keys. + */ +TEST (CborReader, key) +{ + ASSERT_EQ (events (hex ("a1f601")), "{1 knull u:1 }"); + ASSERT_EQ (events (hex ("a1f401")), "{1 kfalse u:1 }"); + ASSERT_EQ (events (hex ("a1f501")), "{1 ktrue u:1 }"); + ASSERT_EQ (events (hex ("a1f701")), "{1 knull u:1 }"); + ASSERT_EQ (events (hex ("a10101")), "{1 ku:1 u:1 }"); + ASSERT_EQ (events (hex ("a11b000000010000000001")), "{1 kU:4294967296 u:1 }"); + ASSERT_EQ (events (hex ("a12001")), "{1 ki:-1 u:1 }"); + ASSERT_EQ (events (hex ("a13b000000010000000001")), "{1 kI:-4294967297 u:1 }"); + ASSERT_EQ (events (hex ("a3f93c0001fa3fc0000002fb3ff199999999999a03")), "{3 kd u:1 kd u:2 kd u:3 }"); + ASSERT_EQ (events (hex ("a1616101")), "{1 ks:a u:1 }"); + ASSERT_EQ (events (hex ("a142000101")), "{1 kb:2 u:1 }"); + ASSERT_EQ (events (hex ("a17f6161ff01")), "{1 ks:a u:1 }"); + ASSERT_EQ (events (hex ("a15f4100ff01")), "{1 kb:1 u:1 }"); + ASSERT_EQ (events (hex ("a1810102")), "{1 k [1 u:1 ] u:2 }"); + ASSERT_EQ (events (hex ("a1a1010203")), "{1 k {1 ku:1 u:2 } u:3 }"); + ASSERT_EQ (events (hex ("a19fff01")), "{1 k [0 ] u:1 }"); + ASSERT_EQ (events (hex ("a1bfff01")), "{1 k {0 } u:1 }"); + ASSERT_EQ (events (hex ("bf9f01ff02ff")), "{0 k [0 u:1 ] u:2 }"); + + Value value; + ASSERT_EQ (CborReader ().deserialize (hex ("a3f501616101810102"), value), 0) << join::lastError.message (); + ASSERT_TRUE (value.isObject ()); + ASSERT_TRUE (value.contains (true)); + ASSERT_TRUE (value.contains ("a")); + + RejectKey rejectKey; + ASSERT_EQ (CborReader ().deserialize (hex ("a1810102"), rejectKey), -1); + ASSERT_EQ (CborReader ().deserialize (hex ("a1a1010203"), rejectKey), -1); + ASSERT_EQ (CborReader ().deserialize (hex ("a19fff01"), rejectKey), -1); + ASSERT_EQ (CborReader ().deserialize (hex ("a1bfff01"), rejectKey), -1); +} + +/** + * @brief Test CBOR parsing with UTF-8 validation. + */ +TEST (CborReader, encoding) +{ + using Validator = join::BasicCborReader; + + join::SaxHandler handler; + std::stringstream stream; + + ASSERT_EQ (Validator ().deserialize (hex ("63e6b0b4"), handler), 0) << join::lastError.message (); + stream.clear (); + stream.str (hex ("63e6b0b4")); + ASSERT_EQ (Validator ().deserialize (stream, handler), 0) << join::lastError.message (); + + ASSERT_EQ (Validator ().deserialize (hex ("7f62c3a9ff"), handler), 0) << join::lastError.message (); + stream.clear (); + stream.str (hex ("7f62c3a9ff")); + ASSERT_EQ (Validator ().deserialize (stream, handler), 0) << join::lastError.message (); + + ASSERT_EQ (Validator ().deserialize (hex ("41ff"), handler), 0) << join::lastError.message (); + stream.clear (); + stream.str (hex ("41ff")); + ASSERT_EQ (Validator ().deserialize (stream, handler), 0) << join::lastError.message (); + + ASSERT_EQ (Validator ().deserialize (hex ("5f41ff41feff"), handler), 0) << join::lastError.message (); + stream.clear (); + stream.str (hex ("5f41ff41feff")); + ASSERT_EQ (Validator ().deserialize (stream, handler), 0) << join::lastError.message (); + + ASSERT_EQ (Validator ().deserialize (hex ("61ff"), handler), -1); + ASSERT_EQ (join::lastError, SaxErrc::InvalidEncoding); + stream.clear (); + stream.str (hex ("61ff")); + ASSERT_EQ (Validator ().deserialize (stream, handler), -1); + ASSERT_EQ (join::lastError, SaxErrc::InvalidEncoding); + + ASSERT_EQ (Validator ().deserialize (hex ("7f61c361a9ff"), handler), -1); + ASSERT_EQ (join::lastError, SaxErrc::InvalidEncoding); + stream.clear (); + stream.str (hex ("7f61c361a9ff")); + ASSERT_EQ (Validator ().deserialize (stream, handler), -1); + ASSERT_EQ (join::lastError, SaxErrc::InvalidEncoding); + + ASSERT_EQ (Validator ().deserialize (hex ("a161ff01"), handler), -1); + ASSERT_EQ (join::lastError, SaxErrc::InvalidEncoding); + stream.clear (); + stream.str (hex ("a161ff01")); + ASSERT_EQ (Validator ().deserialize (stream, handler), -1); + ASSERT_EQ (join::lastError, SaxErrc::InvalidEncoding); + + ASSERT_EQ (events (hex ("61ff")), "s:\xff"); + + Value value; + ASSERT_EQ (value.deserialize (hex ("6449455446")), 0) << join::lastError.message (); + ASSERT_EQ (value.getString (), "IETF"); +} + +/** + * @brief main function. + */ +int main (int argc, char** argv) +{ + testing::InitGoogleTest (&argc, argv); + return RUN_ALL_TESTS (); +} diff --git a/data/tests/cbor_roundtrip_test.cpp b/data/tests/cbor_roundtrip_test.cpp new file mode 100644 index 00000000..5084a45d --- /dev/null +++ b/data/tests/cbor_roundtrip_test.cpp @@ -0,0 +1,346 @@ +/** + * MIT License + * + * Copyright (c) 2026 Mathieu Rabine + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// libjoin. +#include + +// libraries. +#include + +// C++. +#include +#include +#include + +using join::Value; +using join::CborReader; +using join::CborWriter; + +/** + * @brief convert hexadecimal digits to bytes. + * @param digits hexadecimal digits. + * @return bytes. + */ +static std::string hex (std::string_view digits) +{ + std::string bytes; + for (size_t i = 0; i + 1 < digits.size (); i += 2) + { + bytes += static_cast (std::stoi (std::string (digits.substr (i, 2)), nullptr, 16)); + } + return bytes; +} + +/** + * @brief Test CBOR round trip. + */ +TEST (Cbor, roundtrip) +{ + Value value; + CborReader reader; + + std::stringstream stream; + CborWriter writer (stream); + + stream.clear (); + stream.str (hex ("81f6")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("81f6")); + + stream.clear (); + stream.str (hex ("81f5")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("81f5")); + + stream.clear (); + stream.str (hex ("81f4")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("81f4")); + + stream.clear (); + stream.str (hex ("8100")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("8100")); + + stream.clear (); + stream.str (hex ("8163666f6f")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("8163666f6f")); + + stream.clear (); + stream.str (hex ("80")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("80")); + + stream.clear (); + stream.str (hex ("a0")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("a0")); + + stream.clear (); + stream.str (hex ("820001")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("820001")); + + stream.clear (); + stream.str (hex ("a163666f6f63626172")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("a163666f6f63626172")); + + stream.clear (); + stream.str (hex ("a26161f663666f6f63626172")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("a26161f663666f6f63626172")); + + stream.clear (); + stream.str (hex ("8120")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("8120")); + + stream.clear (); + stream.str (hex ("813a7fffffff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("813a7fffffff")); + + stream.clear (); + stream.str (hex ("813b112210f47de98114")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("813b112210f47de98114")); + + stream.clear (); + stream.str (hex ("813b7fffffffffffffff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("813b7fffffffffffffff")); + + stream.clear (); + stream.str (hex ("8101")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("8101")); + + stream.clear (); + stream.str (hex ("811a7fffffff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("811a7fffffff")); + + stream.clear (); + stream.str (hex ("811affffffff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("811affffffff")); + + stream.clear (); + stream.str (hex ("811b112210f47de98115")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("811b112210f47de98115")); + + stream.clear (); + stream.str (hex ("811b7fffffffffffffff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("811b7fffffffffffffff")); + + stream.clear (); + stream.str (hex ("811bffffffffffffffff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("811bffffffffffffffff")); + + stream.clear (); + stream.str (hex ("81fa00000000")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("81fa00000000")); + + stream.clear (); + stream.str (hex ("81fa80000000")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("81fa80000000")); + + stream.clear (); + stream.str (hex ("81fb3ff3c083126e978d")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("81fb3ff3c083126e978d")); + + stream.clear (); + stream.str (hex ("81fbbff3c083126e978d")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("81fbbff3c083126e978d")); + + stream.clear (); + stream.str (hex ("81fb0000000000000001")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("81fb0000000000000001")); + + stream.clear (); + stream.str (hex ("81fb000fffffffffffff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("81fb000fffffffffffff")); + + stream.clear (); + stream.str (hex ("81fb0010000000000000")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("81fb0010000000000000")); + + stream.clear (); + stream.str (hex ("81fb7fefffffffffffff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("81fb7fefffffffffffff")); + + stream.clear (); + stream.str (hex ("8143010203")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("8143010203")); + + stream.clear (); + stream.str (hex ("a10102")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("a10102")); + + stream.clear (); + stream.str (hex ("81fa7f800000")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("81fa7f800000")); + + stream.clear (); + stream.str (hex ("81faff800000")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("81faff800000")); + + stream.clear (); + stream.str (hex ("81fa7fc00000")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("81fa7fc00000")); + + stream.clear (); + stream.str (hex ("81faffc00000")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("81faffc00000")); +} + +/** + * @brief main function. + */ +int main (int argc, char** argv) +{ + testing::InitGoogleTest (&argc, argv); + return RUN_ALL_TESTS (); +} diff --git a/data/tests/cbor_stream_roundtrip_test.cpp b/data/tests/cbor_stream_roundtrip_test.cpp new file mode 100644 index 00000000..d3c33b92 --- /dev/null +++ b/data/tests/cbor_stream_roundtrip_test.cpp @@ -0,0 +1,346 @@ +/** + * MIT License + * + * Copyright (c) 2026 Mathieu Rabine + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// libjoin. +#include + +// libraries. +#include + +// C++. +#include +#include +#include + +using join::Value; +using join::CborReader; +using join::CborStreamWriter; + +/** + * @brief convert hexadecimal digits to bytes. + * @param digits hexadecimal digits. + * @return bytes. + */ +static std::string hex (std::string_view digits) +{ + std::string bytes; + for (size_t i = 0; i + 1 < digits.size (); i += 2) + { + bytes += static_cast (std::stoi (std::string (digits.substr (i, 2)), nullptr, 16)); + } + return bytes; +} + +/** + * @brief Test CBOR stream round trip. + */ +TEST (CborStream, roundtrip) +{ + Value value; + CborReader reader; + + std::stringstream stream; + CborStreamWriter writer (stream); + + stream.clear (); + stream.str (hex ("9ff6ff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9ff6ff")); + + stream.clear (); + stream.str (hex ("9ff5ff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9ff5ff")); + + stream.clear (); + stream.str (hex ("9ff4ff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9ff4ff")); + + stream.clear (); + stream.str (hex ("9f00ff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9f00ff")); + + stream.clear (); + stream.str (hex ("9f63666f6fff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9f63666f6fff")); + + stream.clear (); + stream.str (hex ("9fff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9fff")); + + stream.clear (); + stream.str (hex ("bfff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("bfff")); + + stream.clear (); + stream.str (hex ("9f0001ff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9f0001ff")); + + stream.clear (); + stream.str (hex ("bf63666f6f63626172ff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("bf63666f6f63626172ff")); + + stream.clear (); + stream.str (hex ("bf6161f663666f6f63626172ff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("bf6161f663666f6f63626172ff")); + + stream.clear (); + stream.str (hex ("9f20ff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9f20ff")); + + stream.clear (); + stream.str (hex ("9f3a7fffffffff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9f3a7fffffffff")); + + stream.clear (); + stream.str (hex ("9f3b112210f47de98114ff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9f3b112210f47de98114ff")); + + stream.clear (); + stream.str (hex ("9f3b7fffffffffffffffff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9f3b7fffffffffffffffff")); + + stream.clear (); + stream.str (hex ("9f01ff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9f01ff")); + + stream.clear (); + stream.str (hex ("9f1a7fffffffff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9f1a7fffffffff")); + + stream.clear (); + stream.str (hex ("9f1affffffffff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9f1affffffffff")); + + stream.clear (); + stream.str (hex ("9f1b112210f47de98115ff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9f1b112210f47de98115ff")); + + stream.clear (); + stream.str (hex ("9f1b7fffffffffffffffff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9f1b7fffffffffffffffff")); + + stream.clear (); + stream.str (hex ("9f1bffffffffffffffffff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9f1bffffffffffffffffff")); + + stream.clear (); + stream.str (hex ("9ffa00000000ff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9ffa00000000ff")); + + stream.clear (); + stream.str (hex ("9ffa80000000ff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9ffa80000000ff")); + + stream.clear (); + stream.str (hex ("9ffb3ff3c083126e978dff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9ffb3ff3c083126e978dff")); + + stream.clear (); + stream.str (hex ("9ffbbff3c083126e978dff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9ffbbff3c083126e978dff")); + + stream.clear (); + stream.str (hex ("9ffb0000000000000001ff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9ffb0000000000000001ff")); + + stream.clear (); + stream.str (hex ("9ffb000fffffffffffffff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9ffb000fffffffffffffff")); + + stream.clear (); + stream.str (hex ("9ffb0010000000000000ff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9ffb0010000000000000ff")); + + stream.clear (); + stream.str (hex ("9ffb7fefffffffffffffff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9ffb7fefffffffffffffff")); + + stream.clear (); + stream.str (hex ("9f43010203ff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9f43010203ff")); + + stream.clear (); + stream.str (hex ("bf0102ff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("bf0102ff")); + + stream.clear (); + stream.str (hex ("9ffa7f800000ff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9ffa7f800000ff")); + + stream.clear (); + stream.str (hex ("9ffaff800000ff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9ffaff800000ff")); + + stream.clear (); + stream.str (hex ("9ffa7fc00000ff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9ffa7fc00000ff")); + + stream.clear (); + stream.str (hex ("9ffaffc00000ff")); + ASSERT_NE (reader.deserialize (stream, value), -1) << join::lastError.message (); + stream.str (""); + stream.clear (); + ASSERT_NE (value.serialize (writer), -1) << join::lastError.message (); + EXPECT_EQ (stream.str (), hex ("9ffaffc00000ff")); +} + +/** + * @brief main function. + */ +int main (int argc, char** argv) +{ + testing::InitGoogleTest (&argc, argv); + return RUN_ALL_TESTS (); +} diff --git a/data/tests/cbor_stream_writer_test.cpp b/data/tests/cbor_stream_writer_test.cpp new file mode 100644 index 00000000..f6e2bc7d --- /dev/null +++ b/data/tests/cbor_stream_writer_test.cpp @@ -0,0 +1,575 @@ +/** + * MIT License + * + * Copyright (c) 2026 Mathieu Rabine + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// libjoin. +#include +#include + +// libraries. +#include + +// C++. +#include +#include +#include +#include +#include + +// C. +#include + +using join::CborStreamWriter; +using join::Value; +using join::Object; +using join::SaxErrc; + +/** + * @brief Test setNull method. + */ +TEST (CborStreamWriter, setNull) +{ + std::stringstream stream; + CborStreamWriter cborStreamWriter (stream); + + stream.str (""); + EXPECT_EQ (cborStreamWriter.setNull (), 0); + EXPECT_EQ (stream.str (), std::string ({'\xF6'})); +} + +/** + * @brief Test setBool method. + */ +TEST (CborStreamWriter, setBool) +{ + std::stringstream stream; + CborStreamWriter cborStreamWriter (stream); + + stream.str (""); + EXPECT_EQ (cborStreamWriter.setBool (true), 0); + EXPECT_EQ (stream.str (), std::string ({'\xF5'})); + + stream.str (""); + EXPECT_EQ (cborStreamWriter.setBool (false), 0); + EXPECT_EQ (stream.str (), std::string ({'\xF4'})); +} + +/** + * @brief Test setInt method. + */ +TEST (CborStreamWriter, setInt) +{ + std::stringstream stream; + CborStreamWriter cborStreamWriter (stream); + + auto write = [&] (int32_t value) { + stream.str (""); + EXPECT_EQ (cborStreamWriter.setInt (value), 0); + return stream.str (); + }; + + EXPECT_EQ (write (0), std::string ({'\x00'})); + EXPECT_EQ (write (23), std::string ({'\x17'})); + EXPECT_EQ (write (24), std::string ({'\x18', '\x18'})); + EXPECT_EQ (write (std::numeric_limits::max ()), std::string ({'\x1A', '\x7F', '\xFF', '\xFF', '\xFF'})); + EXPECT_EQ (write (-1), std::string ({'\x20'})); + EXPECT_EQ (write (-24), std::string ({'\x37'})); + EXPECT_EQ (write (-25), std::string ({'\x38', '\x18'})); + EXPECT_EQ (write (-256), std::string ({'\x38', '\xFF'})); + EXPECT_EQ (write (-257), std::string ({'\x39', '\x01', '\x00'})); + EXPECT_EQ (write (-1000), std::string ({'\x39', '\x03', '\xE7'})); + EXPECT_EQ (write (-65537), std::string ({'\x3A', '\x00', '\x01', '\x00', '\x00'})); + EXPECT_EQ (write (std::numeric_limits::min ()), std::string ({'\x3A', '\x7F', '\xFF', '\xFF', '\xFF'})); +} + +/** + * @brief Test setUint method. + */ +TEST (CborStreamWriter, setUint) +{ + std::stringstream stream; + CborStreamWriter cborStreamWriter (stream); + + auto write = [&] (uint32_t value) { + stream.str (""); + EXPECT_EQ (cborStreamWriter.setUint (value), 0); + return stream.str (); + }; + + EXPECT_EQ (write (0), std::string ({'\x00'})); + EXPECT_EQ (write (23), std::string ({'\x17'})); + EXPECT_EQ (write (24), std::string ({'\x18', '\x18'})); + EXPECT_EQ (write (255), std::string ({'\x18', '\xFF'})); + EXPECT_EQ (write (256), std::string ({'\x19', '\x01', '\x00'})); + EXPECT_EQ (write (1000), std::string ({'\x19', '\x03', '\xE8'})); + EXPECT_EQ (write (65535), std::string ({'\x19', '\xFF', '\xFF'})); + EXPECT_EQ (write (65536), std::string ({'\x1A', '\x00', '\x01', '\x00', '\x00'})); + EXPECT_EQ (write (std::numeric_limits::max ()), std::string ({'\x1A', '\xFF', '\xFF', '\xFF', '\xFF'})); +} + +/** + * @brief Test setInt64 method. + */ +TEST (CborStreamWriter, setInt64) +{ + std::stringstream stream; + CborStreamWriter cborStreamWriter (stream); + + auto write = [&] (int64_t value) { + stream.str (""); + EXPECT_EQ (cborStreamWriter.setInt64 (value), 0); + return stream.str (); + }; + + EXPECT_EQ (write (-1), std::string ({'\x20'})); + EXPECT_EQ (write (-4294967296LL), std::string ({'\x3A', '\xFF', '\xFF', '\xFF', '\xFF'})); + EXPECT_EQ (write (-4294967297LL), + std::string ({'\x3B', '\x00', '\x00', '\x00', '\x01', '\x00', '\x00', '\x00', '\x00'})); + EXPECT_EQ (write (std::numeric_limits::min ()), + std::string ({'\x3B', '\x7F', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF'})); + EXPECT_EQ (write (4294967296LL), + std::string ({'\x1B', '\x00', '\x00', '\x00', '\x01', '\x00', '\x00', '\x00', '\x00'})); + EXPECT_EQ (write (std::numeric_limits::max ()), + std::string ({'\x1B', '\x7F', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF'})); +} + +/** + * @brief Test setUint64 method. + */ +TEST (CborStreamWriter, setUint64) +{ + std::stringstream stream; + CborStreamWriter cborStreamWriter (stream); + + auto write = [&] (uint64_t value) { + stream.str (""); + EXPECT_EQ (cborStreamWriter.setUint64 (value), 0); + return stream.str (); + }; + + EXPECT_EQ (write (0), std::string ({'\x00'})); + EXPECT_EQ (write (4294967295ULL), std::string ({'\x1A', '\xFF', '\xFF', '\xFF', '\xFF'})); + EXPECT_EQ (write (4294967296ULL), + std::string ({'\x1B', '\x00', '\x00', '\x00', '\x01', '\x00', '\x00', '\x00', '\x00'})); + EXPECT_EQ (write (std::numeric_limits::max ()), + std::string ({'\x1B', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF'})); +} + +/** + * @brief Test setDouble method. + */ +TEST (CborStreamWriter, setDouble) +{ + std::stringstream stream; + CborStreamWriter cborStreamWriter (stream); + + auto write = [&] (double value) { + stream.str (""); + EXPECT_EQ (cborStreamWriter.setDouble (value), 0); + return stream.str (); + }; + + EXPECT_EQ (write (0.0), std::string ({'\xFA', '\x00', '\x00', '\x00', '\x00'})); + EXPECT_EQ (write (-0.0), std::string ({'\xFA', '\x80', '\x00', '\x00', '\x00'})); + EXPECT_EQ (write (1.5), std::string ({'\xFA', '\x3F', '\xC0', '\x00', '\x00'})); + EXPECT_EQ (write (1.1), std::string ({'\xFB', '\x3F', '\xF1', '\x99', '\x99', '\x99', '\x99', '\x99', '\x9A'})); + EXPECT_EQ (write (std::numeric_limits::infinity ()), + std::string ({'\xFA', '\x7F', '\x80', '\x00', '\x00'})); + EXPECT_EQ (write (-std::numeric_limits::infinity ()), + std::string ({'\xFA', '\xFF', '\x80', '\x00', '\x00'})); + EXPECT_EQ (write (std::numeric_limits::quiet_NaN ()), + std::string ({'\xFA', '\x7F', '\xC0', '\x00', '\x00'})); + EXPECT_EQ (write (std::bit_cast (uint64_t (0x7ff8000000000001))), + std::string ({'\xFB', '\x7F', '\xF8', '\x00', '\x00', '\x00', '\x00', '\x00', '\x01'})); + EXPECT_EQ (write (std::numeric_limits::max ()), std::string ({'\xFA', '\x7F', '\x7F', '\xFF', '\xFF'})); + EXPECT_EQ (write (std::nextafter (static_cast (std::numeric_limits::max ()), + std::numeric_limits::infinity ())), + std::string ({'\xFB', '\x47', '\xEF', '\xFF', '\xFF', '\xE0', '\x00', '\x00', '\x01'})); + EXPECT_EQ (write (std::numeric_limits::denorm_min ()), + std::string ({'\xFA', '\x00', '\x00', '\x00', '\x01'})); + EXPECT_EQ (write (std::numeric_limits::denorm_min ()), + std::string ({'\xFB', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x01'})); + EXPECT_EQ (write (-std::numeric_limits::quiet_NaN ()), + std::string ({'\xFA', '\xFF', '\xC0', '\x00', '\x00'})); + EXPECT_EQ (write (std::bit_cast (uint64_t (0x7ff4000000000000))), + std::string ({'\xFB', '\x7F', '\xF4', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00'})); +} + +/** + * @brief Test setString method. + */ +TEST (CborStreamWriter, setString) +{ + std::stringstream stream; + CborStreamWriter cborStreamWriter (stream); + + stream.str (""); + EXPECT_EQ (cborStreamWriter.setString (""), 0); + EXPECT_EQ (stream.str (), std::string ({'\x60'})); + + stream.str (""); + EXPECT_EQ (cborStreamWriter.setString (std::string_view ()), 0); + EXPECT_EQ (stream.str (), std::string ({'\x60'})); + + stream.str (""); + EXPECT_EQ (cborStreamWriter.setString ("IETF"), 0); + EXPECT_EQ (stream.str (), std::string ({'\x64', 'I', 'E', 'T', 'F'})); + + stream.str (""); + EXPECT_EQ (cborStreamWriter.setString (std::string (23, 'x')), 0); + EXPECT_EQ (stream.str (), std::string ({'\x77'}) + std::string (23, 'x')); + + stream.str (""); + EXPECT_EQ (cborStreamWriter.setString (std::string (24, 'x')), 0); + EXPECT_EQ (stream.str (), std::string ({'\x78', '\x18'}) + std::string (24, 'x')); + + stream.str (""); + EXPECT_EQ (cborStreamWriter.setString (std::string (256, 'x')), 0); + EXPECT_EQ (stream.str (), std::string ({'\x79', '\x01', '\x00'}) + std::string (256, 'x')); + + stream.str (""); + EXPECT_EQ (cborStreamWriter.setString (std::string (65536, 'x')), 0); + EXPECT_EQ (stream.str (), std::string ({'\x7A', '\x00', '\x01', '\x00', '\x00'}) + std::string (65536, 'x')); +} + +/** + * @brief Test setBinary method. + */ +TEST (CborStreamWriter, setBinary) +{ + std::stringstream stream; + CborStreamWriter cborStreamWriter (stream); + const std::vector data (65536, 0x78); + const uint8_t bytes[] = {0x01, 0x02, 0x03, 0x04}; + + stream.str (""); + EXPECT_EQ (cborStreamWriter.setBinary (std::span ()), 0); + EXPECT_EQ (stream.str (), std::string ({'\x40'})); + + stream.str (""); + EXPECT_EQ (cborStreamWriter.setBinary (bytes), 0); + EXPECT_EQ (stream.str (), std::string ({'\x44', '\x01', '\x02', '\x03', '\x04'})); + + stream.str (""); + EXPECT_EQ (cborStreamWriter.setBinary (bytes, -1), 0); + EXPECT_EQ (stream.str (), std::string ({'\x44', '\x01', '\x02', '\x03', '\x04'})); + + stream.str (""); + EXPECT_EQ (cborStreamWriter.setBinary (std::span (data.data (), 256)), 0); + EXPECT_EQ (stream.str (), std::string ({'\x59', '\x01', '\x00'}) + std::string (256, 'x')); + + stream.str (""); + EXPECT_EQ (cborStreamWriter.setBinary (data), 0); + EXPECT_EQ (stream.str (), std::string ({'\x5A', '\x00', '\x01', '\x00', '\x00'}) + std::string (65536, 'x')); +} + +/** + * @brief Test startArray method. + */ +TEST (CborStreamWriter, startArray) +{ + std::stringstream stream; + CborStreamWriter cborStreamWriter (stream); + + auto array = [&] (uint32_t size) { + stream.str (""); + EXPECT_EQ (cborStreamWriter.startArray (size), 0); + EXPECT_EQ (cborStreamWriter.stopArray (), 0); + return stream.str (); + }; + + EXPECT_EQ (array (0), std::string ({'\x9F', '\xFF'})); + EXPECT_EQ (array (23), std::string ({'\x9F', '\xFF'})); + EXPECT_EQ (array (24), std::string ({'\x9F', '\xFF'})); + EXPECT_EQ (array (256), std::string ({'\x9F', '\xFF'})); + EXPECT_EQ (array (65536), std::string ({'\x9F', '\xFF'})); + + stream.str (""); + EXPECT_EQ (cborStreamWriter.startArray (), 0); + EXPECT_EQ (cborStreamWriter.stopArray (), 0); + EXPECT_EQ (stream.str (), std::string ({'\x9F', '\xFF'})); +} + +/** + * @brief Test stopArray method. + */ +TEST (CborStreamWriter, stopArray) +{ + std::stringstream stream; + CborStreamWriter cborStreamWriter (stream); + + EXPECT_EQ (cborStreamWriter.stopArray (), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + + EXPECT_EQ (cborStreamWriter.startArray (1), 0); + EXPECT_EQ (cborStreamWriter.setNull (), 0); + EXPECT_EQ (cborStreamWriter.stopArray (), 0); + EXPECT_EQ (stream.str (), std::string ({'\x9F', '\xF6', '\xFF'})); +} + +/** + * @brief Test startObject method. + */ +TEST (CborStreamWriter, startObject) +{ + std::stringstream stream; + CborStreamWriter cborStreamWriter (stream); + + auto object = [&] (uint32_t size) { + stream.str (""); + EXPECT_EQ (cborStreamWriter.startObject (size), 0); + EXPECT_EQ (cborStreamWriter.stopObject (), 0); + return stream.str (); + }; + + EXPECT_EQ (object (0), std::string ({'\xBF', '\xFF'})); + EXPECT_EQ (object (23), std::string ({'\xBF', '\xFF'})); + EXPECT_EQ (object (24), std::string ({'\xBF', '\xFF'})); + EXPECT_EQ (object (256), std::string ({'\xBF', '\xFF'})); + EXPECT_EQ (object (65536), std::string ({'\xBF', '\xFF'})); + + stream.str (""); + EXPECT_EQ (cborStreamWriter.startObject (), 0); + EXPECT_EQ (cborStreamWriter.stopObject (), 0); + EXPECT_EQ (stream.str (), std::string ({'\xBF', '\xFF'})); +} + +/** + * @brief Test members method. + */ +TEST (CborStreamWriter, members) +{ + std::stringstream stream; + CborStreamWriter cborStreamWriter (stream); + Value members; + members["b"] = 1; + members["a"] = 2; + const Object& object = members.getObject (); + EXPECT_EQ (&cborStreamWriter.members (object), &object); +} + +/** + * @brief Test setKey method. + */ +TEST (CborStreamWriter, setKey) +{ + std::stringstream stream; + CborStreamWriter cborStreamWriter (stream); + + auto key = [&] (auto... value) { + stream.str (""); + cborStreamWriter.reset (); + cborStreamWriter.startObject (1); + int result = cborStreamWriter.setKey (value...); + cborStreamWriter.flush (); + return result; + }; + + EXPECT_EQ (cborStreamWriter.setKey (nullptr), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + EXPECT_EQ (cborStreamWriter.setKey (true), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + EXPECT_EQ (cborStreamWriter.setKey (int32_t (-2)), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + EXPECT_EQ (cborStreamWriter.setKey (uint32_t (3)), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + EXPECT_EQ (cborStreamWriter.setKey (int64_t (-4)), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + EXPECT_EQ (cborStreamWriter.setKey (uint64_t (5)), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + EXPECT_EQ (cborStreamWriter.setKey (6.5), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + EXPECT_EQ (cborStreamWriter.setKey (std::string_view ("seven")), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + EXPECT_EQ (cborStreamWriter.setKey ("eight"), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + EXPECT_EQ (cborStreamWriter.setKey (std::span ()), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + + EXPECT_EQ (key (nullptr), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\xF6'})); + + EXPECT_EQ (key (true), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\xF5'})); + + EXPECT_EQ (key (false), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\xF4'})); + + EXPECT_EQ (key (42), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\x18', '\x2A'})); + + EXPECT_EQ (key (-10), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\x29'})); + + EXPECT_EQ (key (uint32_t (300)), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\x19', '\x01', '\x2C'})); + + EXPECT_EQ (key (int64_t (-10)), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\x29'})); + + EXPECT_EQ (key (uint64_t (42)), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\x18', '\x2A'})); + + EXPECT_EQ (key (1.5), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\xFA', '\x3F', '\xC0', '\x00', '\x00'})); + + EXPECT_EQ (key ("a"), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\x61', 'a'})); + + EXPECT_EQ (key (std::string_view ("seven")), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\x65', 's', 'e', 'v', 'e', 'n'})); + + const uint8_t bytes[] = {0x00, 0xff}; + EXPECT_EQ (key (std::span (bytes, sizeof (bytes))), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\x42', '\x00', '\xFF'})); + + EXPECT_EQ (key (std::span (bytes, sizeof (bytes)), -1), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\x42', '\x00', '\xFF'})); +} + +/** + * @brief Test startKey method. + */ +TEST (CborStreamWriter, startKey) +{ + std::stringstream stream; + CborStreamWriter cborStreamWriter (stream); + + EXPECT_EQ (cborStreamWriter.startKey (), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + + EXPECT_EQ (cborStreamWriter.startObject (1), 0); + EXPECT_EQ (cborStreamWriter.startKey (), 0); +} + +/** + * @brief Test stopObject method. + */ +TEST (CborStreamWriter, stopObject) +{ + std::stringstream stream; + CborStreamWriter cborStreamWriter (stream); + + EXPECT_EQ (cborStreamWriter.stopObject (), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + + EXPECT_EQ (cborStreamWriter.startObject (1), 0); + EXPECT_EQ (cborStreamWriter.setKey (nullptr), 0); + EXPECT_EQ (cborStreamWriter.setNull (), 0); + EXPECT_EQ (cborStreamWriter.stopObject (), 0); + EXPECT_EQ (stream.str (), std::string ({'\xBF', '\xF6', '\xF6', '\xFF'})); +} + +/** + * @brief Test flush method. + */ +TEST (CborStreamWriter, flush) +{ + std::stringstream stream; + CborStreamWriter cborStreamWriter (stream); + + EXPECT_EQ (cborStreamWriter.startArray (2), 0); + EXPECT_EQ (cborStreamWriter.setNull (), 0); + EXPECT_EQ (stream.str (), ""); + cborStreamWriter.flush (); + EXPECT_EQ (stream.str (), std::string ({'\x9F', '\xF6'})); + cborStreamWriter.flush (); + EXPECT_EQ (stream.str (), std::string ({'\x9F', '\xF6'})); + EXPECT_EQ (cborStreamWriter.setNull (), 0); + EXPECT_EQ (cborStreamWriter.stopArray (), 0); + EXPECT_EQ (stream.str (), std::string ({'\x9F', '\xF6', '\xF6', '\xFF'})); + + stream.str (""); + EXPECT_EQ (cborStreamWriter.startArray (1100), 0); + for (int i = 0; i < 1100; ++i) + { + EXPECT_EQ (cborStreamWriter.setNull (), 0); + } + EXPECT_EQ (stream.str ().size (), 1024u); + EXPECT_EQ (cborStreamWriter.stopArray (), 0); + EXPECT_EQ (stream.str ().size (), 1102u); +} + +/** + * @brief Test reset method. + */ +TEST (CborStreamWriter, reset) +{ + std::stringstream stream; + CborStreamWriter cborStreamWriter (stream); + + EXPECT_EQ (cborStreamWriter.startArray (2), 0); + EXPECT_EQ (cborStreamWriter.setNull (), 0); + cborStreamWriter.reset (); + EXPECT_EQ (stream.str (), ""); + EXPECT_EQ (cborStreamWriter.setNull (), 0); + EXPECT_EQ (stream.str (), std::string ({'\xF6'})); +} + +/** + * @brief Test writing a whole document. + */ +TEST (CborStreamWriter, document) +{ + Value value; + value.pushBack (nullptr); + value.pushBack (true); + value.pushBack (int32_t (1)); + value.pushBack (uint32_t (2)); + value.pushBack (int64_t (3)); + value.pushBack (uint64_t (4)); + value.pushBack (1.23456789e-13); + value.pushBack ("test"); + + std::stringstream stream; + CborStreamWriter cborStreamWriter (stream); + + stream.str (""); + EXPECT_EQ (value.serialize (cborStreamWriter), 0) << join::lastError.message (); + EXPECT_EQ (stream.str (), + std::string ({'\x9F', '\xF6', '\xF5', '\x01', '\x02', '\x03', '\x04', '\xFB', '\x3D', '\x41', '\x5F', + '\xFF', '\xE5', '\x3A', '\x68', '\x5D', '\x64', 't', 'e', 's', 't', '\xFF'})); + + value = nullptr; + value["a"] = 1; + stream.str (""); + EXPECT_EQ (value.serialize (cborStreamWriter), 0) << join::lastError.message (); + EXPECT_EQ (stream.str (), std::string ({'\xBF', '\x61', 'a', '\x01', '\xFF'})); + + Value key; + key.pushBack (1); + value = Object (); + value.insert (key, 1); + stream.str (""); + EXPECT_EQ (value.serialize (cborStreamWriter), 0) << join::lastError.message (); + EXPECT_EQ (stream.str (), std::string ({'\xBF', '\x9F', '\x01', '\xFF', '\x01', '\xFF'})); +} + +/** + * @brief main function. + */ +int main (int argc, char** argv) +{ + testing::InitGoogleTest (&argc, argv); + return RUN_ALL_TESTS (); +} diff --git a/data/tests/cbor_writer_test.cpp b/data/tests/cbor_writer_test.cpp new file mode 100644 index 00000000..c3919e61 --- /dev/null +++ b/data/tests/cbor_writer_test.cpp @@ -0,0 +1,563 @@ +/** + * MIT License + * + * Copyright (c) 2026 Mathieu Rabine + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// libjoin. +#include +#include + +// libraries. +#include + +// C++. +#include +#include +#include +#include +#include + +// C. +#include + +using join::CborWriter; +using join::Value; +using join::Object; +using join::SaxErrc; + +/** + * @brief Test setNull method. + */ +TEST (CborWriter, setNull) +{ + std::stringstream stream; + CborWriter cborWriter (stream); + + stream.str (""); + EXPECT_EQ (cborWriter.setNull (), 0); + EXPECT_EQ (stream.str (), std::string ({'\xF6'})); +} + +/** + * @brief Test setBool method. + */ +TEST (CborWriter, setBool) +{ + std::stringstream stream; + CborWriter cborWriter (stream); + + stream.str (""); + EXPECT_EQ (cborWriter.setBool (true), 0); + EXPECT_EQ (stream.str (), std::string ({'\xF5'})); + + stream.str (""); + EXPECT_EQ (cborWriter.setBool (false), 0); + EXPECT_EQ (stream.str (), std::string ({'\xF4'})); +} + +/** + * @brief Test setInt method. + */ +TEST (CborWriter, setInt) +{ + std::stringstream stream; + CborWriter cborWriter (stream); + + auto write = [&] (int32_t value) { + stream.str (""); + EXPECT_EQ (cborWriter.setInt (value), 0); + return stream.str (); + }; + + EXPECT_EQ (write (0), std::string ({'\x00'})); + EXPECT_EQ (write (23), std::string ({'\x17'})); + EXPECT_EQ (write (24), std::string ({'\x18', '\x18'})); + EXPECT_EQ (write (std::numeric_limits::max ()), std::string ({'\x1A', '\x7F', '\xFF', '\xFF', '\xFF'})); + EXPECT_EQ (write (-1), std::string ({'\x20'})); + EXPECT_EQ (write (-24), std::string ({'\x37'})); + EXPECT_EQ (write (-25), std::string ({'\x38', '\x18'})); + EXPECT_EQ (write (-256), std::string ({'\x38', '\xFF'})); + EXPECT_EQ (write (-257), std::string ({'\x39', '\x01', '\x00'})); + EXPECT_EQ (write (-1000), std::string ({'\x39', '\x03', '\xE7'})); + EXPECT_EQ (write (-65537), std::string ({'\x3A', '\x00', '\x01', '\x00', '\x00'})); + EXPECT_EQ (write (std::numeric_limits::min ()), std::string ({'\x3A', '\x7F', '\xFF', '\xFF', '\xFF'})); +} + +/** + * @brief Test setUint method. + */ +TEST (CborWriter, setUint) +{ + std::stringstream stream; + CborWriter cborWriter (stream); + + auto write = [&] (uint32_t value) { + stream.str (""); + EXPECT_EQ (cborWriter.setUint (value), 0); + return stream.str (); + }; + + EXPECT_EQ (write (0), std::string ({'\x00'})); + EXPECT_EQ (write (23), std::string ({'\x17'})); + EXPECT_EQ (write (24), std::string ({'\x18', '\x18'})); + EXPECT_EQ (write (255), std::string ({'\x18', '\xFF'})); + EXPECT_EQ (write (256), std::string ({'\x19', '\x01', '\x00'})); + EXPECT_EQ (write (1000), std::string ({'\x19', '\x03', '\xE8'})); + EXPECT_EQ (write (65535), std::string ({'\x19', '\xFF', '\xFF'})); + EXPECT_EQ (write (65536), std::string ({'\x1A', '\x00', '\x01', '\x00', '\x00'})); + EXPECT_EQ (write (std::numeric_limits::max ()), std::string ({'\x1A', '\xFF', '\xFF', '\xFF', '\xFF'})); +} + +/** + * @brief Test setInt64 method. + */ +TEST (CborWriter, setInt64) +{ + std::stringstream stream; + CborWriter cborWriter (stream); + + auto write = [&] (int64_t value) { + stream.str (""); + EXPECT_EQ (cborWriter.setInt64 (value), 0); + return stream.str (); + }; + + EXPECT_EQ (write (-1), std::string ({'\x20'})); + EXPECT_EQ (write (-4294967296LL), std::string ({'\x3A', '\xFF', '\xFF', '\xFF', '\xFF'})); + EXPECT_EQ (write (-4294967297LL), + std::string ({'\x3B', '\x00', '\x00', '\x00', '\x01', '\x00', '\x00', '\x00', '\x00'})); + EXPECT_EQ (write (std::numeric_limits::min ()), + std::string ({'\x3B', '\x7F', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF'})); + EXPECT_EQ (write (4294967296LL), + std::string ({'\x1B', '\x00', '\x00', '\x00', '\x01', '\x00', '\x00', '\x00', '\x00'})); + EXPECT_EQ (write (std::numeric_limits::max ()), + std::string ({'\x1B', '\x7F', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF'})); +} + +/** + * @brief Test setUint64 method. + */ +TEST (CborWriter, setUint64) +{ + std::stringstream stream; + CborWriter cborWriter (stream); + + auto write = [&] (uint64_t value) { + stream.str (""); + EXPECT_EQ (cborWriter.setUint64 (value), 0); + return stream.str (); + }; + + EXPECT_EQ (write (0), std::string ({'\x00'})); + EXPECT_EQ (write (4294967295ULL), std::string ({'\x1A', '\xFF', '\xFF', '\xFF', '\xFF'})); + EXPECT_EQ (write (4294967296ULL), + std::string ({'\x1B', '\x00', '\x00', '\x00', '\x01', '\x00', '\x00', '\x00', '\x00'})); + EXPECT_EQ (write (std::numeric_limits::max ()), + std::string ({'\x1B', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF'})); +} + +/** + * @brief Test setDouble method. + */ +TEST (CborWriter, setDouble) +{ + std::stringstream stream; + CborWriter cborWriter (stream); + + auto write = [&] (double value) { + stream.str (""); + EXPECT_EQ (cborWriter.setDouble (value), 0); + return stream.str (); + }; + + EXPECT_EQ (write (0.0), std::string ({'\xFA', '\x00', '\x00', '\x00', '\x00'})); + EXPECT_EQ (write (-0.0), std::string ({'\xFA', '\x80', '\x00', '\x00', '\x00'})); + EXPECT_EQ (write (1.5), std::string ({'\xFA', '\x3F', '\xC0', '\x00', '\x00'})); + EXPECT_EQ (write (1.1), std::string ({'\xFB', '\x3F', '\xF1', '\x99', '\x99', '\x99', '\x99', '\x99', '\x9A'})); + EXPECT_EQ (write (std::numeric_limits::infinity ()), + std::string ({'\xFA', '\x7F', '\x80', '\x00', '\x00'})); + EXPECT_EQ (write (-std::numeric_limits::infinity ()), + std::string ({'\xFA', '\xFF', '\x80', '\x00', '\x00'})); + EXPECT_EQ (write (std::numeric_limits::quiet_NaN ()), + std::string ({'\xFA', '\x7F', '\xC0', '\x00', '\x00'})); + EXPECT_EQ (write (std::bit_cast (uint64_t (0x7ff8000000000001))), + std::string ({'\xFB', '\x7F', '\xF8', '\x00', '\x00', '\x00', '\x00', '\x00', '\x01'})); + EXPECT_EQ (write (std::numeric_limits::max ()), std::string ({'\xFA', '\x7F', '\x7F', '\xFF', '\xFF'})); + EXPECT_EQ (write (std::nextafter (static_cast (std::numeric_limits::max ()), + std::numeric_limits::infinity ())), + std::string ({'\xFB', '\x47', '\xEF', '\xFF', '\xFF', '\xE0', '\x00', '\x00', '\x01'})); + EXPECT_EQ (write (std::numeric_limits::denorm_min ()), + std::string ({'\xFA', '\x00', '\x00', '\x00', '\x01'})); + EXPECT_EQ (write (std::numeric_limits::denorm_min ()), + std::string ({'\xFB', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x01'})); + EXPECT_EQ (write (-std::numeric_limits::quiet_NaN ()), + std::string ({'\xFA', '\xFF', '\xC0', '\x00', '\x00'})); + EXPECT_EQ (write (std::bit_cast (uint64_t (0x7ff4000000000000))), + std::string ({'\xFB', '\x7F', '\xF4', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00'})); +} + +/** + * @brief Test setString method. + */ +TEST (CborWriter, setString) +{ + std::stringstream stream; + CborWriter cborWriter (stream); + + stream.str (""); + EXPECT_EQ (cborWriter.setString (""), 0); + EXPECT_EQ (stream.str (), std::string ({'\x60'})); + + stream.str (""); + EXPECT_EQ (cborWriter.setString (std::string_view ()), 0); + EXPECT_EQ (stream.str (), std::string ({'\x60'})); + + stream.str (""); + EXPECT_EQ (cborWriter.setString ("IETF"), 0); + EXPECT_EQ (stream.str (), std::string ({'\x64', 'I', 'E', 'T', 'F'})); + + stream.str (""); + EXPECT_EQ (cborWriter.setString (std::string (23, 'x')), 0); + EXPECT_EQ (stream.str (), std::string ({'\x77'}) + std::string (23, 'x')); + + stream.str (""); + EXPECT_EQ (cborWriter.setString (std::string (24, 'x')), 0); + EXPECT_EQ (stream.str (), std::string ({'\x78', '\x18'}) + std::string (24, 'x')); + + stream.str (""); + EXPECT_EQ (cborWriter.setString (std::string (256, 'x')), 0); + EXPECT_EQ (stream.str (), std::string ({'\x79', '\x01', '\x00'}) + std::string (256, 'x')); + + stream.str (""); + EXPECT_EQ (cborWriter.setString (std::string (65536, 'x')), 0); + EXPECT_EQ (stream.str (), std::string ({'\x7A', '\x00', '\x01', '\x00', '\x00'}) + std::string (65536, 'x')); +} + +/** + * @brief Test setBinary method. + */ +TEST (CborWriter, setBinary) +{ + std::stringstream stream; + CborWriter cborWriter (stream); + const std::vector data (65536, 0x78); + const uint8_t bytes[] = {0x01, 0x02, 0x03, 0x04}; + + stream.str (""); + EXPECT_EQ (cborWriter.setBinary (std::span ()), 0); + EXPECT_EQ (stream.str (), std::string ({'\x40'})); + + stream.str (""); + EXPECT_EQ (cborWriter.setBinary (bytes), 0); + EXPECT_EQ (stream.str (), std::string ({'\x44', '\x01', '\x02', '\x03', '\x04'})); + + stream.str (""); + EXPECT_EQ (cborWriter.setBinary (bytes, -1), 0); + EXPECT_EQ (stream.str (), std::string ({'\x44', '\x01', '\x02', '\x03', '\x04'})); + + stream.str (""); + EXPECT_EQ (cborWriter.setBinary (std::span (data.data (), 256)), 0); + EXPECT_EQ (stream.str (), std::string ({'\x59', '\x01', '\x00'}) + std::string (256, 'x')); + + stream.str (""); + EXPECT_EQ (cborWriter.setBinary (data), 0); + EXPECT_EQ (stream.str (), std::string ({'\x5A', '\x00', '\x01', '\x00', '\x00'}) + std::string (65536, 'x')); +} + +/** + * @brief Test startArray method. + */ +TEST (CborWriter, startArray) +{ + std::stringstream stream; + CborWriter cborWriter (stream); + + auto array = [&] (uint32_t size) { + stream.str (""); + EXPECT_EQ (cborWriter.startArray (size), 0); + EXPECT_EQ (cborWriter.stopArray (), 0); + return stream.str (); + }; + + EXPECT_EQ (array (0), std::string ({'\x80'})); + EXPECT_EQ (array (23), std::string ({'\x97'})); + EXPECT_EQ (array (24), std::string ({'\x98', '\x18'})); + EXPECT_EQ (array (256), std::string ({'\x99', '\x01', '\x00'})); + EXPECT_EQ (array (65536), std::string ({'\x9A', '\x00', '\x01', '\x00', '\x00'})); +} + +/** + * @brief Test stopArray method. + */ +TEST (CborWriter, stopArray) +{ + std::stringstream stream; + CborWriter cborWriter (stream); + + EXPECT_EQ (cborWriter.stopArray (), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + + EXPECT_EQ (cborWriter.startArray (1), 0); + EXPECT_EQ (cborWriter.setNull (), 0); + EXPECT_EQ (cborWriter.stopArray (), 0); + EXPECT_EQ (stream.str (), std::string ({'\x81', '\xF6'})); +} + +/** + * @brief Test startObject method. + */ +TEST (CborWriter, startObject) +{ + std::stringstream stream; + CborWriter cborWriter (stream); + + auto object = [&] (uint32_t size) { + stream.str (""); + EXPECT_EQ (cborWriter.startObject (size), 0); + EXPECT_EQ (cborWriter.stopObject (), 0); + return stream.str (); + }; + + EXPECT_EQ (object (0), std::string ({'\xA0'})); + EXPECT_EQ (object (23), std::string ({'\xB7'})); + EXPECT_EQ (object (24), std::string ({'\xB8', '\x18'})); + EXPECT_EQ (object (256), std::string ({'\xB9', '\x01', '\x00'})); + EXPECT_EQ (object (65536), std::string ({'\xBA', '\x00', '\x01', '\x00', '\x00'})); +} + +/** + * @brief Test members method. + */ +TEST (CborWriter, members) +{ + std::stringstream stream; + CborWriter cborWriter (stream); + Value members; + members["b"] = 1; + members["a"] = 2; + const Object& object = members.getObject (); + EXPECT_EQ (&cborWriter.members (object), &object); +} + +/** + * @brief Test setKey method. + */ +TEST (CborWriter, setKey) +{ + std::stringstream stream; + CborWriter cborWriter (stream); + + auto key = [&] (auto... value) { + stream.str (""); + cborWriter.reset (); + cborWriter.startObject (1); + int result = cborWriter.setKey (value...); + cborWriter.flush (); + return result; + }; + + EXPECT_EQ (cborWriter.setKey (nullptr), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + EXPECT_EQ (cborWriter.setKey (true), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + EXPECT_EQ (cborWriter.setKey (int32_t (-2)), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + EXPECT_EQ (cborWriter.setKey (uint32_t (3)), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + EXPECT_EQ (cborWriter.setKey (int64_t (-4)), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + EXPECT_EQ (cborWriter.setKey (uint64_t (5)), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + EXPECT_EQ (cborWriter.setKey (6.5), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + EXPECT_EQ (cborWriter.setKey (std::string_view ("seven")), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + EXPECT_EQ (cborWriter.setKey ("eight"), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + EXPECT_EQ (cborWriter.setKey (std::span ()), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + + EXPECT_EQ (key (nullptr), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\xF6'})); + + EXPECT_EQ (key (true), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\xF5'})); + + EXPECT_EQ (key (false), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\xF4'})); + + EXPECT_EQ (key (42), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\x18', '\x2A'})); + + EXPECT_EQ (key (-10), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\x29'})); + + EXPECT_EQ (key (uint32_t (300)), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\x19', '\x01', '\x2C'})); + + EXPECT_EQ (key (int64_t (-10)), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\x29'})); + + EXPECT_EQ (key (uint64_t (42)), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\x18', '\x2A'})); + + EXPECT_EQ (key (1.5), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\xFA', '\x3F', '\xC0', '\x00', '\x00'})); + + EXPECT_EQ (key ("a"), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\x61', 'a'})); + + EXPECT_EQ (key (std::string_view ("seven")), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\x65', 's', 'e', 'v', 'e', 'n'})); + + const uint8_t bytes[] = {0x00, 0xff}; + EXPECT_EQ (key (std::span (bytes, sizeof (bytes))), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\x42', '\x00', '\xFF'})); + + EXPECT_EQ (key (std::span (bytes, sizeof (bytes)), -1), 0); + EXPECT_EQ (stream.str ().substr (1), std::string ({'\x42', '\x00', '\xFF'})); +} + +/** + * @brief Test startKey method. + */ +TEST (CborWriter, startKey) +{ + std::stringstream stream; + CborWriter cborWriter (stream); + + EXPECT_EQ (cborWriter.startKey (), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + + EXPECT_EQ (cborWriter.startObject (1), 0); + EXPECT_EQ (cborWriter.startKey (), 0); +} + +/** + * @brief Test stopObject method. + */ +TEST (CborWriter, stopObject) +{ + std::stringstream stream; + CborWriter cborWriter (stream); + + EXPECT_EQ (cborWriter.stopObject (), -1); + EXPECT_EQ (join::lastError, SaxErrc::InvalidParent); + + EXPECT_EQ (cborWriter.startObject (0), 0); + EXPECT_EQ (cborWriter.stopObject (), 0); + EXPECT_EQ (stream.str (), std::string ({'\xA0'})); +} + +/** + * @brief Test flush method. + */ +TEST (CborWriter, flush) +{ + std::stringstream stream; + CborWriter cborWriter (stream); + + EXPECT_EQ (cborWriter.startArray (2), 0); + EXPECT_EQ (cborWriter.setNull (), 0); + EXPECT_EQ (stream.str (), ""); + cborWriter.flush (); + EXPECT_EQ (stream.str (), std::string ({'\x82', '\xF6'})); + cborWriter.flush (); + EXPECT_EQ (stream.str (), std::string ({'\x82', '\xF6'})); + EXPECT_EQ (cborWriter.setNull (), 0); + EXPECT_EQ (cborWriter.stopArray (), 0); + EXPECT_EQ (stream.str (), std::string ({'\x82', '\xF6', '\xF6'})); + + stream.str (""); + EXPECT_EQ (cborWriter.startArray (1100), 0); + for (int i = 0; i < 1100; ++i) + { + EXPECT_EQ (cborWriter.setNull (), 0); + } + EXPECT_EQ (stream.str ().size (), 1024u); + EXPECT_EQ (cborWriter.stopArray (), 0); + EXPECT_EQ (stream.str ().size (), 1103u); +} + +/** + * @brief Test reset method. + */ +TEST (CborWriter, reset) +{ + std::stringstream stream; + CborWriter cborWriter (stream); + + EXPECT_EQ (cborWriter.startArray (2), 0); + EXPECT_EQ (cborWriter.setNull (), 0); + cborWriter.reset (); + EXPECT_EQ (stream.str (), ""); + EXPECT_EQ (cborWriter.setNull (), 0); + EXPECT_EQ (stream.str (), std::string ({'\xF6'})); +} + +/** + * @brief Test writing a whole document. + */ +TEST (CborWriter, document) +{ + Value value; + value.pushBack (nullptr); + value.pushBack (true); + value.pushBack (int32_t (1)); + value.pushBack (uint32_t (2)); + value.pushBack (int64_t (3)); + value.pushBack (uint64_t (4)); + value.pushBack (1.23456789e-13); + value.pushBack ("test"); + + std::stringstream stream; + CborWriter cborWriter (stream); + + stream.str (""); + EXPECT_EQ (value.serialize (cborWriter), 0) << join::lastError.message (); + EXPECT_EQ (stream.str (), + std::string ({'\x88', '\xF6', '\xF5', '\x01', '\x02', '\x03', '\x04', '\xFB', '\x3D', '\x41', '\x5F', + '\xFF', '\xE5', '\x3A', '\x68', '\x5D', '\x64', 't', 'e', 's', 't'})); + + value = nullptr; + value["a"] = 1; + stream.str (""); + EXPECT_EQ (value.serialize (cborWriter), 0) << join::lastError.message (); + EXPECT_EQ (stream.str (), std::string ({'\xA1', '\x61', 'a', '\x01'})); + + Value key; + key.pushBack (1); + value = Object (); + value.insert (key, 1); + stream.str (""); + EXPECT_EQ (value.serialize (cborWriter), 0) << join::lastError.message (); + EXPECT_EQ (stream.str (), std::string ({'\xA1', '\x81', '\x01', '\x01'})); +} + +/** + * @brief main function. + */ +int main (int argc, char** argv) +{ + testing::InitGoogleTest (&argc, argv); + return RUN_ALL_TESTS (); +} diff --git a/data/tests/pack_writer_test.cpp b/data/tests/pack_writer_test.cpp index 4cf14e7c..19d83bcb 100644 --- a/data/tests/pack_writer_test.cpp +++ b/data/tests/pack_writer_test.cpp @@ -406,6 +406,10 @@ TEST (PackWriter, setString) std::stringstream stream; PackWriter packWriter (stream); + stream.str (""); + EXPECT_EQ (packWriter.setString (std::string_view ()), 0); + EXPECT_EQ (stream.str (), std::string ({'\xA0'})); + stream.str (""); EXPECT_EQ (packWriter.setString (std::string (31, 'x')), 0); EXPECT_EQ (stream.str ().compare (0, 6, std::string ({'\xBF', '\x78', '\x78', '\x78', '\x78', '\x78'})), 0); @@ -461,6 +465,10 @@ TEST (PackWriter, setBinary) PackWriter packWriter (stream); const std::vector data (65536, 0x78); + stream.str (""); + EXPECT_EQ (packWriter.setBinary (std::span ()), 0); + EXPECT_EQ (stream.str (), std::string ({'\xC4', '\x00'})); + stream.str (""); EXPECT_EQ (packWriter.setBinary (std::span (data.data (), 255)), 0); EXPECT_EQ (stream.str ().compare (0, 4, std::string ({'\xC4', '\xFF', '\x78', '\x78'})), 0);