From e08e393bd286b44cdc304313ed967c03ddfcf348 Mon Sep 17 00:00:00 2001 From: EJainDev Date: Fri, 9 Oct 2026 15:36:52 -0400 Subject: [PATCH 1/2] feat: add deserialization --- README.md | 199 ++- benchmarks/xml_serialization_benchmarks.cpp | 148 +- examples/CMakeLists.txt | 3 + examples/deserialization.cpp | 51 + include/serial_xml.cxx | 1367 ++++++++++++++++--- tests/test_serial_xml.cpp | 721 ++++++++++ 6 files changed, 2310 insertions(+), 179 deletions(-) create mode 100644 examples/deserialization.cpp diff --git a/README.md b/README.md index aef02e1..fba755c 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ ![CMake 4.3+](https://img.shields.io/badge/CMake-4.3%2B-orange) ![License](https://img.shields.io/badge/license-MIT-lightgray) -> Reflection based XML serialization for C++26 -- call `to_xml` on any object for a string serialization +> Reflection based XML serialization and deserialization for C++26 -SerialXML is a C++26 reflection based serialization library for XML. Behaviour is configurable via annotations on object members and object type declarations (`class/struct`). Errors are all compile time using `static_assert` messages so any invalid XML combination is caught at compile time. +SerialXML is a C++26 reflection based serialization library for XML. Behaviour is configurable via annotations on object members and object type declarations (`class/struct`). Invalid annotation combinations are diagnosed at compile time. Deserialization reports malformed XML, missing required members, and invalid values through `serial_xml::deserialization_error`. ## Table of Contents * [Quick Start](#quick-start) @@ -18,6 +18,7 @@ SerialXML is a C++26 reflection based serialization library for XML. Behaviour i * [Source](#install-from-source) * [Requirements](#requirements) * [Benchmarks](#benchmarks) +* [Deserialization](#deserialization) * [Annotations](#annotations) * [Common Confusion Points](#common-confusion-points) * [Configuring `to_xml`](#configuring-to_xml) @@ -49,6 +50,135 @@ int main() { That's all you need: one function call and SerialXML does the rest. +## Deserialization + +`serial_xml::from_xml(xml)` constructs a value-initialized `T` and reconstructs it +using the same member names, attributes, nesting, iteration, escaping, and mock +annotations as `to_xml`. `serial_xml::from_xml(xml)` uses an external mock +schema. Both accept an optional root-name override as their second argument. +For an existing object, including one without a default constructor, use +`serial_xml::from_xml(object, xml, fixed_name)`; omit `Schema` for the object's +own annotations. The in-place form snapshots the input so XML stored in an updated +field remains safe, and parses it before updating the object. A later conversion +error or throwing setter can leave earlier members updated. + +```cpp +struct Person { + [[= serial_xml::attribute]] int age; + std::string name; + [[= serial_xml::optional]] std::string nickname = "unknown"; +}; + +auto person = serial_xml::from_xml( + "Ekansh"); +// person.nickname remains "unknown". +``` + +Members are **required** unless marked `[[= serial_xml::optional]]`, skipped, or +omitted naturally by serialization. Unless explicitly marked `optional`, missing `std::optional` members are reset +to `std::nullopt`; missing automatically iterated `exclude_on_empty` members are +reset to an empty value. +An absent explicitly optional member keeps its initializer (or its current value +with the in-place API). An empty element counts as present: it produces an empty +string or range but fails numeric conversion. Fixed arrays require the exact +number of elements. Raw ranges may have no elements because their enclosing tag +is omitted. Skipped fields and unselected mock fields keep their initialized values. +Unknown attributes and child elements are ignored; duplicate matched singleton +elements and duplicate XML attributes are rejected. XML child/attribute order +is otherwise independent of C++ member order. + +### Leaf conversion with `from_string` + +Every non-unpacked leaf is converted through +`serial_xml::from_string(std::string_view)`. Built-in implementations cover +arithmetic types (including booleans), `std::string`, `std::optional`, pairs, +tuples, and owning STL sequences, sets, and maps. Numbers must consume the entire +input after trimming surrounding XML whitespace; overflow and invalid text throw +`deserialization_error`. Booleans accept `true`, `false`, `1`, and `0`; characters +use their numeric value, matching `to_xml`. Strings retain whitespace. + +Container conversion reads the standard formatted representations, such as +`[1, 2]`, `(1, "text")`, and `{"key": 3}`, including nested containers and quoted +strings. XML iteration handles `vector`, `array`, `inplace_vector`, `deque`, +`forward_list`, and `valarray` automatically. Use `iter` for other owning ranges. +Borrowed views (`span`, `string_view`, pointers) have no built-in reconstruction: +provide an implementation with an appropriate storage lifetime if you need them. + +A custom leaf type requires an explicit specialization, declared before the first +call that needs it. An unpacked class uses reflection instead. + +```cpp +struct Code { int value; }; + +template <> +Code serial_xml::from_string(std::string_view text) { + return {serial_xml::from_string(text)}; +} +``` + +`format` continues to determine whether a value is represented as text. +Deserialization passes that text to `from_string`; it does not try to invert +custom formatter functions or lossy format strings. Decimal zero padding works +with the default numeric parser. Custom prefixes, hexadecimal representations, +alignment fill, or other representations need a matching `from_string` +implementation. Lost precision cannot be recovered. + +### Setters and encapsulation + +Accessible const getters can be reconstructed through a matching non-static setter +with exactly one parameter of the same value type (references and cv qualifiers +are ignored). Supported conventions are `value()` → `value(T)` or `set_value(T)`, +`get_value()` → `set_value(T)`, and `getValue()` → `setValue(T)`. XML naming and +annotations come from the getter. Getters without a matching setter are ignored +by deserialization. Non-function members that cannot be assigned produce a +compile-time error; mark them `skip` to omit them. + +Use `[[= serial_xml::setter]]` to select a method independently, without requiring +a getter. It must be an accessible non-static method with one parameter. Its +parameter type controls reconstruction, and its own annotations control XML +naming, attributes, optionality, and iteration. By default `set_value` reads +`value` and `setValue` reads `value`; other method names are used literally. +`name` overrides this. Setters are ignored by `to_xml`. + +```cpp +class Account { + public: + [[= serial_xml::skip]] int balance() const { return balance_; } + [[= serial_xml::setter, = serial_xml::name{"balance"}]] + void deposit_balance(int amount) { balance_ = amount; } + private: + int balance_ = 0; +}; +``` + +An explicitly selected setter takes precedence over automatic reconstruction of +its matching getter, so it is invoked once. Setter annotations can also live on +a mock schema; placeholder methods do not need definitions. Ambiguous setter +overloads are diagnosed at compile time. + +### XML input and text annotations + +The reader supports UTF-8 XML 1.0, a UTF-8 BOM, XML declarations, quoted attributes, +self-closing elements, comments, processing instructions, CDATA, the five predefined +entities, and decimal/hexadecimal Unicode character references. It rejects +mismatched tags, invalid characters/UTF-8, unclosed structures, invalid numeric +values, missing required members, and multiple roots. DTDs and external entities +are unsupported. Namespace prefixes are matched literally; namespace declarations +are not resolved. Nesting is limited to 256 elements. Line endings and literal +attribute whitespace follow XML normalization rules. + +`raw` scalar fields read the parent's direct text; more than one raw scalar field +cannot be separated and causes an error. `raw` ranges read matching item tags +directly from the parent. Unpacked objects and automatically handled optionals +keep their tagged representation even with `raw`, following `to_xml` precedence. CDATA fields read the parent's CDATA sections in member +order, matching `to_xml`'s unwrapped CDATA output. Optional or externally split +CDATA fields can be ambiguous; use tagged string fields when boundaries must be +unambiguous. Standard tagged string fields can contain mixed ordinary text and +CDATA sections, which are concatenated. + +See [the deserialization example](examples/deserialization.cpp) for round trips, +custom string conversion, optional fields, and independent setters. + ## Installation ### CMake FetchContent (Recommended) @@ -93,18 +223,64 @@ C++ Standard | 26 | SIMD, Reflection, Annotations | ## Benchmarks -This library is the *fastest* XML serialization library. It is 8.3x faster than `boost_xml` and 1.9x faster than `pugixml` for struct to XML serialization. +The same executable compares serialization and deserialization with SerialXML, +Boost.Serialization XML archives, cereal XML archives, and pugixml. Every library +handles the same logical order: customer details, a shipping address, three line +items, and four integer tags. Each library reads its own serialized representation; +archive metadata and container tag names differ, so compare time per order rather +than XML bytes per second. -Benchmarks on my machine (single Intel Core 7 240H with 5600 MT/s DDR5): +Measured on October 9, 2026, with GCC 16.2.0, the Release preset (`-O3 -DNDEBUG`), +and an Intel Core 7 240H. Results are the median of five optimized runs from +alternating before/after pairs, each pinned to logical CPU 4 (a performance core), +with 100,000 measured iterations and 1,000 warm-up iterations per library and +operation. Lower times are better. -| Library | Iterations | Time per Iter (ns) | Total Time (ms) | +| Library | Serialization (ns/order) | Deserialization (ns/order) | XML input bytes | | --- | --- | --- | --- | -| SerialXML | 100000 | 822.085 | 82.208 | -| Boost.Serialization | 100000 | 7899.305 | 789.931 | -| Pugixml | 100000 | 1585.212 | 158.521 | -| Cereal | 100000 | 7945.590 | 794.559 | +| SerialXML | 830.308 | 849.361 | 708 | +| Boost.Serialization | 6959.951 | 14250.021 | 1111 | +| cereal | 7699.705 | 3195.171 | 936 | +| pugixml | 1523.398 | 936.498 | 671 | + +The optimized deserializer uses borrowed input views, an 8 KiB stack-backed +bump arena with heap fallback, stable append-only node lists, a single validated +scan for plain text, and field lookup specialized for constant schema names. +Destination ranges reserve their final size. Heap allocations for this order +dropped from 67 to 5. Returned strings and containers own their contents; the +temporary views and arena do not escape reconstruction. + +Five alternating before/after benchmark pairs on the same CPU measured SerialXML +deserialization at 3,887.278 ns before and 849.361 ns after: approximately **4.6× +faster**. Serialization measured 854.624 ns before and 830.308 ns after, with no +measured regression; its implementation is unchanged by these optimizations. +SerialXML beat all three existing comparators in every optimized run. Its median +deserialization time was approximately 9% lower than pugixml's, 3.8× faster than +cereal, and 16.8× faster than Boost. These local measurements of a small order +are not a general ranking across XML workloads. + +Deserialization includes XML parsing, numeric conversion, construction of an +owning result object, and cleanup. Input XML is prepared outside the timed loops. +All reconstructed fields are checked before timing; compiler barriers keep timed +results observable. The pugixml comparison uses handwritten field mapping for +valid input, whereas SerialXML also checks required members. Serialization +includes completion and destruction of output archives before capturing the XML; +these results supersede the historical table that captured archive output early. + +Reproduce with the repository's GCC 16 dev container: + +```sh +cmake --preset release-test-gcc -DBUILD_BENCHMARKS=ON +cmake --build --preset build-release-test +ctest --test-dir build/release --output-on-failure +taskset -c 4 ./build/release/benchmarks/xml_serialization_benchmarks 100000 1000 +``` -The main reason this library is so much faster is because all of the above ones use runtime DOM creation adding overhead. Pugixml, for example, uses two pass heap allocations while this library uses two `std::string` objects for each call to `to_xml`. Since this DOM is entirely evaluated at compile time, it also opens up opportunities for the compiler to optimize far better. +The two optional arguments specify measured and warm-up iteration counts. Omit +`taskset -c 4` or choose an available performance-core CPU on another machine. +Repeat the benchmark five times sequentially to compare median timings. The +executable reports both operations and the XML input sizes, and refuses to time +payloads that fail round-trip validation. ## Annotations @@ -122,6 +298,8 @@ This is the complete list of annotations: - `[[=iter{a, b}]]` -- For classes satisfying `std::ranges::range`, iterate through each member instead of directly calling `std::format`. The first (optional) parameter is the name of the tag for each element in the range. The second (optional) parameter is the name of the range tag enclosing each element. - `[[=no_iter]]` -- The opposite of `iter` to disable automatic iteration of STL ranges. See the confusion points for more information on STL handling. - `[[=format{"format_specifier"}]]` -- Add a format specifier in the call to `std::format` for that member. Do not prefix with a colon (`:`) as the library handles that on its own. `[[=format{format_function}]]` instead calls a custom formatting function that accepts the member value and returns a string-like value. +- `[[=setter]]` -- Mark a one-parameter method as an independent XML input property. +- `[[=optional]]` -- Allow a field or selected setter to be absent during reconstruction. - `[[=cdata]]` -- Emit the value inside `cdata` (``) tags. - `[[=exclude_on_empty]]` -- Do not emit any tags when the range is empty @@ -220,6 +398,7 @@ Set `-D BUILD_EXAMPLES=OFF` when configuring CMake to omit them from your build. | [`named_tags.cpp`](examples/named_tags.cpp) | Naming root elements, attributes, and child tags. | | [`skip_members.cpp`](examples/skip_members.cpp) | Excluding members from the output. | | [`nested_structs.cpp`](examples/nested_structs.cpp) | Recursive serialization of nested structs and nested attributes. | +| [`deserialization.cpp`](examples/deserialization.cpp) | Round trips, custom leaf conversion, optional reconstruction, and setters. | | [`stl_containers.cpp`](examples/stl_containers.cpp) | Automatic container iteration, `exclude_on_empty`, and `no_iter`. | | [`iteration.cpp`](examples/iteration.cpp) | Custom element and container names for ranges, including ranges of structs. | | [`optional_types.cpp`](examples/optional_types.cpp) | Omitting empty `std::optional` values and serializing present values. | diff --git a/benchmarks/xml_serialization_benchmarks.cpp b/benchmarks/xml_serialization_benchmarks.cpp index 2c16896..3d37f29 100644 --- a/benchmarks/xml_serialization_benchmarks.cpp +++ b/benchmarks/xml_serialization_benchmarks.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -16,7 +17,8 @@ namespace { // Each library serializes this same logical payload: an order with customer // details, line items, and an integer tag list. The generated XML differs in // incidental archive metadata and container naming, so benchmark results -// should be compared as serialization throughput rather than output size. +// should be compared as logical orders per second rather than XML bytes per second. +// Deserialization includes parsing, numeric conversion, and owning object construction. struct Address { std::string street; @@ -193,6 +195,84 @@ std::string pugixml_serialize(const Order& order) { return std::move(writer.output); } +std::string boost_serialize(const BoostOrder& order) { + std::ostringstream output; + { + boost::archive::xml_oarchive archive(output, boost::archive::no_header); + archive << boost::serialization::make_nvp("order", order); + } + return output.str(); +} + +std::string cereal_serialize(const CerealOrder& order) { + std::ostringstream output; + { + // cereal writes the XML document when the output archive is destroyed. + cereal::XMLOutputArchive archive(output); + archive(cereal::make_nvp("order", order)); + } + return output.str(); +} + +BoostOrder boost_deserialize(const std::string& xml) { + BoostOrder order{}; + std::istringstream input(xml); + { + boost::archive::xml_iarchive archive(input, boost::archive::no_header); + archive >> boost::serialization::make_nvp("order", order); + } + return order; +} + +CerealOrder cereal_deserialize(const std::string& xml) { + CerealOrder order{}; + std::istringstream input(xml); + cereal::XMLInputArchive archive(input); + archive(cereal::make_nvp("order", order)); + return order; +} + +Order pugixml_deserialize(const std::string& xml) { + pugi::xml_document document; + auto parsed = document.load_buffer(xml.data(), xml.size()); + if (!parsed) throw std::runtime_error(parsed.description()); + auto root = document.child("order"); + Order order{}; + order.id = root.child("id").text().as_ullong(); + order.customer = root.child("customer").text().as_string(); + auto address = root.child("shipping_address"); + order.shipping_address = {address.child("street").text().as_string(), + address.child("city").text().as_string(), + address.child("postal_code").text().as_string()}; + for (auto node : root.child("items").children("item")) { + order.items.push_back( + {node.child("sku").text().as_string(), node.child("description").text().as_string(), + node.child("quantity").text().as_int(), node.child("unit_price").text().as_double()}); + } + for (auto node : root.child("tags").children("tag")) { + order.tags.push_back(node.text().as_int()); + } + return order; +} + +template +void verify_order(std::string_view library, const T& actual, const Order& expected) { + bool matches = actual.id == expected.id && actual.customer == expected.customer && + actual.shipping_address.street == expected.shipping_address.street && + actual.shipping_address.city == expected.shipping_address.city && + actual.shipping_address.postal_code == expected.shipping_address.postal_code && + actual.tags == expected.tags && actual.items.size() == expected.items.size(); + if (matches) { + for (std::size_t i = 0; i < actual.items.size(); ++i) { + const auto& a = actual.items[i]; + const auto& e = expected.items[i]; + matches = matches && a.sku == e.sku && a.description == e.description && + a.quantity == e.quantity && a.unit_price == e.unit_price; + } + } + if (!matches) throw std::runtime_error(std::string(library) + " failed round-trip validation"); +} + template void benchmark(std::string_view name, Function&& function, std::size_t iterations, std::size_t warmup_iterations) { @@ -228,17 +308,35 @@ int main(int argc, char** argv) { std::size_t warmup_iterations = 1'000; if (argc > 1) { - iterations = std::stoull(argv[1]); + iterations = serial_xml::from_string(argv[1]); } if (argc > 2) { - warmup_iterations = std::stoull(argv[2]); + warmup_iterations = serial_xml::from_string(argv[2]); + } + + if (iterations == 0) { + std::println(std::cerr, "Iterations must be greater than zero"); + return 1; } const auto serial_order = make_serial_xml_order(); const auto boost_order = make_boost_order(); const auto cereal_order = make_cereal_order(); + // Build each library's own input once, outside all timed loops. + const auto serial_input = serial_xml::to_xml(serial_order, false, "order"); + const auto boost_input = boost_serialize(boost_order); + const auto cereal_input = cereal_serialize(cereal_order); + const auto pugi_input = pugixml_serialize(serial_order); + verify_order("serial_xml", serial_xml::from_xml(serial_input, "order"), serial_order); + verify_order("boost_xml", boost_deserialize(boost_input), serial_order); + verify_order("cereal_xml", cereal_deserialize(cereal_input), serial_order); + verify_order("pugixml", pugixml_deserialize(pugi_input), serial_order); + std::println("Round-trip validation passed for all four libraries."); + std::println("XML input bytes: serial_xml={}, boost_xml={}, cereal_xml={}, pugixml={}", + serial_input.size(), boost_input.size(), cereal_input.size(), pugi_input.size()); + std::println("Serialization (object -> complete XML):"); benchmark( "serial_xml", [&] { @@ -252,11 +350,7 @@ int main(int argc, char** argv) { benchmark( "boost_xml", [&] { - std::ostringstream output; - boost::archive::xml_oarchive archive(output, boost::archive::no_header); - archive << boost::serialization::make_nvp("order", boost_order); - - const auto xml = output.str(); + const auto xml = boost_serialize(boost_order); asm volatile("" : : "g"(xml.data()), "g"(xml.size()) : "memory"); }, @@ -265,11 +359,7 @@ int main(int argc, char** argv) { benchmark( "cereal_xml", [&] { - std::ostringstream output; - cereal::XMLOutputArchive archive(output); - archive(cereal::make_nvp("order", cereal_order)); - - const auto xml = output.str(); + const auto xml = cereal_serialize(cereal_order); asm volatile("" : : "g"(xml.data()), "g"(xml.size()) : "memory"); }, @@ -282,4 +372,34 @@ int main(int argc, char** argv) { asm volatile("" : : "g"(xml.data()), "g"(xml.size()) : "memory"); }, iterations, warmup_iterations); -} \ No newline at end of file + + std::println("Deserialization (XML -> new object, including parsing):"); + benchmark( + "serial_xml", + [&] { + auto order = serial_xml::from_xml(serial_input, "order"); + asm volatile("" : : "g"(&order) : "memory"); + }, + iterations, warmup_iterations); + benchmark( + "boost_xml", + [&] { + auto order = boost_deserialize(boost_input); + asm volatile("" : : "g"(&order) : "memory"); + }, + iterations, warmup_iterations); + benchmark( + "cereal_xml", + [&] { + auto order = cereal_deserialize(cereal_input); + asm volatile("" : : "g"(&order) : "memory"); + }, + iterations, warmup_iterations); + benchmark( + "pugixml", + [&] { + auto order = pugixml_deserialize(pugi_input); + asm volatile("" : : "g"(&order) : "memory"); + }, + iterations, warmup_iterations); +} diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 2ca1edb..5cbd975 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -36,3 +36,6 @@ target_link_libraries(optional_types PRIVATE serial_xml) add_executable(mocking mocking.cpp) target_link_libraries(mocking PRIVATE serial_xml) + +add_executable(deserialization deserialization.cpp) +target_link_libraries(deserialization PRIVATE serial_xml) diff --git a/examples/deserialization.cpp b/examples/deserialization.cpp new file mode 100644 index 0000000..3c8d07a --- /dev/null +++ b/examples/deserialization.cpp @@ -0,0 +1,51 @@ +import std; +import serial_xml; + +struct ProductCode { + int value; +}; + +template <> +ProductCode serial_xml::from_string(std::string_view text) { + if (!text.starts_with("SKU-")) throw serial_xml::deserialization_error("Expected SKU- code"); + return {serial_xml::from_string(text.substr(4))}; +} + +std::string format_code(const ProductCode& code) { return "SKU-" + std::to_string(code.value); } + +struct[[= serial_xml::name{"product"}]] Product { + [[= serial_xml::attribute]] int id; + std::string title; + [[= serial_xml::format{format_code}]] ProductCode code; + [[= serial_xml::iter{"tag"}]] std::vector tags; + [[= serial_xml::optional]] std::string note = "No note supplied"; +}; + +class Inventory { + public: + [[= serial_xml::skip]] int quantity() const { return quantity_; } + [[ = serial_xml::setter, = serial_xml::name{"quantity"} ]] void load_quantity(int value) { + quantity_ = value; + } + + private: + int quantity_ = 0; +}; + +int main() { + Product original{7, "Reflection & XML", {42}, {"C++", "XML"}, "In stock"}; + auto xml = serial_xml::to_xml(original); + auto restored = serial_xml::from_xml(xml); + std::println("{}", xml); + std::println("Restored {} with code {}", restored.title, restored.code.value); + + auto inventory = + serial_xml::from_xml("12"); + std::println("Quantity set through encapsulation: {}", inventory.quantity()); + + // In-place reconstruction preserves absent optional members' current values. + serial_xml::from_xml(restored, + "Next productSKU-43" + "example"); + std::println("{}: {}", restored.title, restored.note); +} diff --git a/include/serial_xml.cxx b/include/serial_xml.cxx index a5fc5a7..b4f6033 100644 --- a/include/serial_xml.cxx +++ b/include/serial_xml.cxx @@ -21,6 +21,12 @@ struct name { static constexpr bool is_empty() { return N == 1; } }; +struct setter_ {}; +export constexpr setter_ setter; + +struct optional_ {}; +export constexpr optional_ optional; + struct skip_ {}; export constexpr skip_ skip; @@ -177,6 +183,16 @@ inline decltype(auto) get_value(auto&& s) { return s.[:m:](); } +consteval bool is_annotated_setter(std::meta::info member) { + if (!std::meta::is_function(member) && !std::meta::is_nonstatic_data_member(member) && + !std::meta::is_variable(member)) + return false; + for (auto annotation : std::meta::annotations_of(member)) { + if (std::meta::type_of(annotation) == ^^decltype(serial_xml::setter)) return true; + } + return false; +} + template struct value_type { using t = std::remove_cvref_t; @@ -186,8 +202,16 @@ struct value_type { template requires(std::meta::is_function(m)) struct value_type { - using t = std::remove_cvref_t; - static constexpr auto m_t = std::meta::dealias(std::meta::return_type_of(m)); + static consteval std::meta::info reflected_type() { + if constexpr ((is_annotated_setter(m) || !std::meta::is_const(m)) && + std::meta::parameters_of(m).size() == 1) { + return std::meta::type_of(std::meta::parameters_of(m)[0]); + } else { + return std::meta::return_type_of(m); + } + } + using t = std::remove_cvref_t; + static constexpr auto m_t = std::meta::dealias(std::meta::remove_cvref(reflected_type())); }; template @@ -198,7 +222,8 @@ constexpr auto value_m_t = value_type::m_t; template consteval bool is_stl_handled() { - if constexpr (get_namespace() == ^^std) { + if constexpr (get_namespace() == + ^^std&& std::meta::has_template_arguments(std::meta::dealias(m))) { static constexpr auto m_t = std::meta::template_of(std::meta::dealias(m)); if constexpr (m_t == ^^std::vector || m_t == ^^std::array || m_t == ^^std::inplace_vector || @@ -355,6 +380,7 @@ consteval std::size_t identifier_hash(std::string_view identifier) { } consteval bool is_serializable_member(std::meta::info member) { + if (is_annotated_setter(member)) return false; if (std::meta::is_nonstatic_data_member(member) || std::meta::is_variable(member)) { return true; } @@ -532,177 +558,73 @@ consteval auto get_attribute_prefix() { return std::make_tuple(std::define_static_string(prefix), prefix.size()); } -template -consteval auto get_type() -> std::meta::info { - static_assert(simd_size >= 8, "SIMD mask size must be at least 8 elements."); - - if constexpr (simd_size <= 8) { - return ^^uint8_t; - } - if constexpr (simd_size <= 16) { - return ^^uint16_t; - } - if constexpr (simd_size <= 32) { - return ^^uint32_t; - } - return ^^uint64_t; -} - -template -class Allocator8ByteAligned { - public: - using value_type = T; - - Allocator8ByteAligned() noexcept = default; - - template - Allocator8ByteAligned(const Allocator8ByteAligned&) noexcept {} - - [[nodiscard]] T* allocate(const std::size_t n) { - if (n == 0) { - return nullptr; - } - - return static_cast(std::aligned_alloc(8, n * sizeof(T))); - } - - void deallocate(T* p, const std::size_t n) noexcept { std::free(p); } -}; - -template -constexpr bool operator==(const Allocator8ByteAligned&, - const Allocator8ByteAligned&) noexcept { - return true; -} - -// clang-format off -thread_local std::vector::size()>():], Allocator8ByteAligned::size()>():]>> - escape_flags; -// clang-format on +thread_local std::vector escape_flags; auto get_escape_bitmask(std::string_view input) -> std::size_t { using simd_t = std::simd::vec; - - std::size_t padded_size [[indeterminate]]; - if ((input.size() % simd_t::size()) != 0) { - padded_size = (input.size() / simd_t::size()) + 1; - } else { - padded_size = input.size() / simd_t::size(); - } - - using FlagsT = typename[:get_type():]; - escape_flags.resize(std::max(sizeof(uint64_t) / sizeof(FlagsT), padded_size)); - - std::size_t i{0}; - - static constexpr auto idx_seq = std::make_index_sequence{}; - - if (static_cast(simd_t::size()) <= input.size()) { - const auto loop_end = input.size() - simd_t::size(); - for (i = 0; i < loop_end; i += simd_t::size()) { - auto v = std::simd::unchecked_load(input.data() + i, simd_t::size()); - - auto mask = (v == '<') | (v == '>') | (v == '&') | (v == '"') | (v == '\''); - - template for (constexpr auto j : idx_seq) { - escape_flags[i / (sizeof(FlagsT) * 8)] |= mask[static_cast(j)] << j; + const auto blocks = (input.size() + 63) / 64; + escape_flags.assign(blocks, 0); + static constexpr auto indices = std::make_index_sequence{}; + for (std::size_t offset = 0; offset < input.size(); offset += simd_t::size()) { + const auto count = std::min(static_cast(simd_t::size()), input.size() - offset); + auto value = std::simd::partial_load(input.data() + offset, count); + auto mask = (value == '<') | (value == '>') | (value == '&') | (value == '"') | (value == '\''); + template for (constexpr auto i : indices) { + if (i < count && mask[static_cast(i)]) { + const auto index = offset + i; + escape_flags[index / 64] |= std::uint64_t{1} << (index % 64); } } - - const auto remaining = simd_t::size() - (i - input.size()); - auto v = std::simd::partial_load(input.data() + (input.size() - remaining), remaining); - - auto mask = (v == '<') | (v == '>') | (v == '&') | (v == '"') | (v == '\''); - - static constexpr auto n_idx_seq = std::make_index_sequence{}; - - template for (constexpr auto j : n_idx_seq) { - escape_flags[i / (sizeof(FlagsT) * 8)] |= mask[static_cast(j)] << j; - } - } else { - auto v = std::simd::partial_load(input.data(), input.size()); - - auto mask = (v == '<') | (v == '>') | (v == '&') | (v == '"') | (v == '\''); - - template for (constexpr auto j : idx_seq) { escape_flags[0] |= mask[static_cast(j)] << j; } } - - return padded_size; + return blocks; } -auto count_escapes(std::size_t padded_size) -> int { - using T = std::ranges::range_value_t; - - auto* escape_flags_ptr = reinterpret_cast(escape_flags.data()); - int count = 0; - for (int i = 0; - i < - static_cast(std::ceil(static_cast(padded_size) * sizeof(T) / sizeof(uint64_t))); - ++i) { - count += std::popcount(escape_flags_ptr[i]); - } +auto count_escapes(std::size_t blocks) -> std::size_t { + std::size_t count = 0; + for (std::size_t i = 0; i < blocks; ++i) count += std::popcount(escape_flags[i]); return count; } -auto copy_with_escapes(char* buf, std::string_view input, std::size_t padded_size) -> std::size_t { - using T = std::ranges::range_value_t; - - const char* original_buf = buf; - - auto* escape_flags_ptr = reinterpret_cast(escape_flags.data()); - - for (std::size_t i = 0; - i < - static_cast(std::ceil(static_cast(padded_size) * sizeof(T) / sizeof(uint64_t))); - ++i) { - std::size_t last = i * sizeof(std::uint64_t); - int prev_idx = 0; - int idx [[indeterminate]]; - - while (escape_flags_ptr[i] != 0) { - idx = std::countr_zero(escape_flags_ptr[i]); - - std::memcpy(buf, input.data() + last + prev_idx, idx - prev_idx); - buf += (idx - prev_idx); - - prev_idx = idx + 1; - - switch (input[last + idx]) { +auto copy_with_escapes(char* buffer, std::string_view input, std::size_t blocks) -> std::size_t { + char* original = buffer; + for (std::size_t block = 0; block < blocks; ++block) { + const auto offset = block * 64; + const auto count = std::min(64uz, input.size() - offset); + auto mask = escape_flags[block]; + std::size_t previous = 0; + while (mask) { + const auto index = static_cast(std::countr_zero(mask)); + std::memcpy(buffer, input.data() + offset + previous, index - previous); + buffer += index - previous; + std::string_view entity; + switch (input[offset + index]) { case '<': - std::memcpy(buf, "<", 4); - buf += 4; + entity = "<"; break; case '>': - std::memcpy(buf, ">", 4); - buf += 4; + entity = ">"; break; case '&': - std::memcpy(buf, "&", 5); - buf += 5; + entity = "&"; break; case '"': - std::memcpy(buf, """, 6); - buf += 6; + entity = """; break; case '\'': - std::memcpy(buf, "'", 6); - buf += 6; + entity = "'"; break; default: - throw std::logic_error("Unexpected character for escaping"); + std::unreachable(); } - - escape_flags_ptr[i] &= (escape_flags_ptr[i] - 1); - } - - if (static_cast(prev_idx) < 63) { - std::memcpy(buf, input.data() + last + prev_idx, - std::min(63uz - prev_idx, input.size() - last - prev_idx)); - buf += std::min(63uz - prev_idx, input.size() - last - prev_idx); + std::memcpy(buffer, entity.data(), entity.size()); + buffer += entity.size(); + previous = index + 1; + mask &= mask - 1; } + std::memcpy(buffer, input.data() + offset + previous, count - previous); + buffer += count - previous; } - - return buf - original_buf; + return static_cast(buffer - original); } template @@ -745,7 +667,7 @@ void add_attribute(std::string& result, std::string& buffer, const auto& value) buf += prefix_size; - auto [ptr, _] = std::to_chars(buf, buf + 20, value); + auto [ptr, _] = std::to_chars(buf, buf + 20, +value); *ptr = '"'; @@ -830,7 +752,7 @@ void add_child(std::string& result, std::string& buffer, const auto& value) { std::memcpy(ptr, closing_tag, closing_tag_size); - return (combined_size + (ptr - buf)); + return original_size + (combined_size + (ptr - buf)); }); } else if constexpr (std::is_integral_v) { static constexpr auto format_resize = 20 + combined_size; @@ -842,7 +764,7 @@ void add_child(std::string& result, std::string& buffer, const auto& value) { buf += opening_tag_size; - auto [ptr, _] = std::to_chars(buf, buf + 20, value); + auto [ptr, _] = std::to_chars(buf, buf + 20, +value); std::memcpy(ptr, closing_tag, closing_tag_size); @@ -911,13 +833,13 @@ auto handle_stl(std::string& result, std::string& buffer, const auto& value) -> static constexpr auto start = std::get<0>(tags); static constexpr auto start_size = std::get<1>(tags); static constexpr auto end = std::get<2>(tags); - if (value.size() > 0) { + if (!std::ranges::empty(value)) { if constexpr (!is_raw) { result += start; } static constexpr auto single_name = std::define_static_string("element"); - static constexpr auto item_m_t = std::meta::dealias(^^decltype(value[0])); + static constexpr auto item_m_t = std::meta::dealias(^^std::ranges::range_value_t); for (const auto& item : value) { if constexpr (is_unpack || @@ -1023,7 +945,7 @@ void to_xml(const T& value, std::string& result, std::string& buffer, bool first is_num(view_name[0]) || std::ranges::starts_with(view_name, std::string_view("xml"))), std::string("Invalid XML name: '") + std::string(view_name) + "'"); - if constexpr (is_std) { + if constexpr (is_std && std::meta::template_of(value_m_t) == ^^std::optional) { handle_stl( result, buffer, get_value(value)); } else { @@ -1142,6 +1064,1141 @@ auto to_xml(const T& value, bool first = true, const std::string& fixed_name = " return result; } +// String conversion deliberately does not reflect user-defined classes. Specialize +// from_string for leaf types whose representation is controlled by the caller. +export class deserialization_error : public std::runtime_error { + public: + using std::runtime_error::runtime_error; +}; + +namespace detail { +template +inline constexpr bool dependent_false = false; + +template +struct optional_type : std::false_type {}; +template +struct optional_type> : std::true_type { + using value_type = T; +}; + +template +struct scalar_type { + using type = T; +}; +template +struct scalar_type> { + using type = T; +}; + +template +struct array_type : std::false_type {}; +template +struct array_type> : std::true_type {}; + +template +struct valarray_type : std::false_type {}; +template +struct valarray_type> : std::true_type {}; + +inline std::string_view trim(std::string_view text) { + auto whitespace = [](char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; }; + while (!text.empty() && whitespace(text.front())) text.remove_prefix(1); + while (!text.empty() && whitespace(text.back())) text.remove_suffix(1); + return text; +} + +// std::format's range and tuple syntax, with nested brackets and quoted strings. +std::vector split_values(std::string_view text, char open, char close) { + text = trim(text); + if (text.size() < 2 || text.front() != open || text.back() != close) { + throw deserialization_error("Expected a bracketed string representation"); + } + text.remove_prefix(1); + text.remove_suffix(1); + std::vector values; + if (trim(text).empty()) return values; + std::vector brackets; + char quote = 0; + bool escaped = false; + std::size_t start = 0; + for (std::size_t i = 0; i < text.size(); ++i) { + char c = text[i]; + if (quote) { + if (escaped) + escaped = false; + else if (c == '\\') + escaped = true; + else if (c == quote) + quote = 0; + } else if (c == '"' || c == '\'') { + quote = c; + } else if (c == '[' || c == '(' || c == '{') { + brackets.push_back(c == '[' ? ']' : c == '(' ? ')' : '}'); + } else if (c == ']' || c == ')' || c == '}') { + if (brackets.empty() || brackets.back() != c) { + throw deserialization_error("Unbalanced string representation"); + } + brackets.pop_back(); + } else if (c == ',' && brackets.empty()) { + values.push_back(trim(text.substr(start, i - start))); + start = i + 1; + } + } + if (quote || !brackets.empty()) throw deserialization_error("Unbalanced string representation"); + values.push_back(trim(text.substr(start))); + for (auto value : values) { + if (value.empty()) throw deserialization_error("Empty item in string representation"); + } + return values; +} + +void append_codepoint(std::string& output, std::uint32_t c) { + if (c > 0x10FFFF || (c >= 0xD800 && c <= 0xDFFF)) + throw deserialization_error("Invalid Unicode escape in string representation"); + if (c < 0x80) + output += static_cast(c); + else if (c < 0x800) { + output += static_cast(0xC0 | (c >> 6)); + output += static_cast(0x80 | (c & 0x3F)); + } else if (c < 0x10000) { + output += static_cast(0xE0 | (c >> 12)); + output += static_cast(0x80 | ((c >> 6) & 0x3F)); + output += static_cast(0x80 | (c & 0x3F)); + } else { + output += static_cast(0xF0 | (c >> 18)); + output += static_cast(0x80 | ((c >> 12) & 0x3F)); + output += static_cast(0x80 | ((c >> 6) & 0x3F)); + output += static_cast(0x80 | (c & 0x3F)); + } +} + +std::string unquote(std::string_view text) { + if (text.size() < 2 || text.front() != '"' || text.back() != '"') return std::string(text); + std::string result; + for (std::size_t i = 1; i + 1 < text.size(); ++i) { + if (text[i] != '\\') { + if (text[i] == '"') throw deserialization_error("Unescaped quote in string representation"); + result += text[i]; + continue; + } + if (++i + 1 >= text.size()) throw deserialization_error("Invalid quoted string escape"); + switch (text[i]) { + case '\\': + result += '\\'; + break; + case '"': + result += '"'; + break; + case '\'': + result += '\''; + break; + case 'n': + result += '\n'; + break; + case 'r': + result += '\r'; + break; + case 't': + result += '\t'; + break; + case 'u': { + if (i + 1 >= text.size() || text[++i] != '{') + throw deserialization_error("Expected braced Unicode escape"); + auto end = text.find('}', i + 1); + if (end == std::string_view::npos || end + 1 >= text.size()) + throw deserialization_error("Unterminated Unicode escape"); + auto digits = text.substr(i + 1, end - i - 1); + std::uint32_t codepoint{}; + auto [ptr, error] = + std::from_chars(digits.data(), digits.data() + digits.size(), codepoint, 16); + if (digits.empty() || error != std::errc{} || ptr != digits.data() + digits.size()) + throw deserialization_error("Invalid Unicode escape"); + append_codepoint(result, codepoint); + i = end; + break; + } + default: + throw deserialization_error("Unsupported quoted string escape"); + } + } + return result; +} + +template +void append(T& container, E&& item) { + if constexpr (requires { container.push_back(std::forward(item)); }) { + container.push_back(std::forward(item)); + } else if constexpr (requires { container.insert(std::forward(item)); }) { + container.insert(std::forward(item)); + } else { + static_assert(dependent_false, "Deserialization requires an owning, writable container"); + } +} + +template +T make_range(std::size_t count, Parse parse) { + T result{}; + if constexpr (requires { T::capacity(); }) { + if (count > T::capacity()) + throw deserialization_error("Too many elements for fixed-capacity container"); + } + if constexpr (requires { result.reserve(count); }) result.reserve(count); + if constexpr (array_type::value) { + if (count != result.size()) throw deserialization_error("Wrong number of array elements"); + for (std::size_t i = 0; i < count; ++i) result[i] = parse(i); + } else if constexpr (valarray_type::value) { + result.resize(count); + for (std::size_t i = 0; i < count; ++i) result[i] = parse(i); + } else if constexpr (requires { result.before_begin(); }) { + auto tail = result.before_begin(); + for (std::size_t i = 0; i < count; ++i) tail = result.insert_after(tail, parse(i)); + } else { + for (std::size_t i = 0; i < count; ++i) append(result, parse(i)); + } + return result; +} +} // namespace detail + +export template +T from_string(std::string_view text); + +namespace detail { +template +T range_item(std::string_view text) { + if constexpr (std::same_as) { + return unquote(text); + } else if constexpr (std::same_as) { + if (text.size() >= 2 && text.front() == '\'' && text.back() == '\'') { + auto value = unquote(std::string("\"") + std::string(text.substr(1, text.size() - 2)) + "\""); + if (value.size() != 1) throw deserialization_error("Expected one character"); + return value.front(); + } + return from_string(text); + } else { + return from_string(text); + } +} + +template +T make_tuple(const std::vector& values, std::index_sequence) { + return T{range_item>(values[I])...}; +} +} // namespace detail + +export template +T from_string(std::string_view text) { + if constexpr (std::same_as) { + return std::string(text); + } else if constexpr (std::same_as) { + text = detail::trim(text); + if (text == "true" || text == "1") return true; + if (text == "false" || text == "0") return false; + throw deserialization_error("Invalid boolean value"); + } else if constexpr (std::is_arithmetic_v) { + text = detail::trim(text); + if (text.empty()) throw deserialization_error("Empty numeric value"); + T value{}; + auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value); + if (error != std::errc{} || end != text.data() + text.size() || text.empty()) { + throw deserialization_error("Invalid or out-of-range numeric value: " + std::string(text)); + } + return value; + } else if constexpr (detail::optional_type::value) { + return T{from_string(text)}; + } else if constexpr (requires { typename std::tuple_size::type; } && + !detail::array_type::value) { + auto values = detail::split_values(text, '(', ')'); + constexpr auto count = std::tuple_size_v; + if (values.size() != count) throw deserialization_error("Wrong number of tuple elements"); + return detail::make_tuple(values, std::make_index_sequence{}); + } else if constexpr (requires { typename T::value_type; } && + (std::ranges::range || detail::valarray_type::value)) { + // Associative maps use {key: value}; sequences use [value, ...]. + if constexpr (requires { typename T::mapped_type; }) { + auto values = detail::split_values(text, '{', '}'); + T result; + for (auto entry : values) { + // Locate the separator outside quoted strings and nested values. + std::size_t separator = std::string_view::npos; + char quote = 0; + int depth = 0; + bool escape = false; + for (std::size_t i = 0; i < entry.size(); ++i) { + char c = entry[i]; + if (quote) { + if (escape) + escape = false; + else if (c == '\\') + escape = true; + else if (c == quote) + quote = 0; + } else if (c == '"' || c == '\'') + quote = c; + else if (c == '[' || c == '(' || c == '{') + ++depth; + else if (c == ']' || c == ')' || c == '}') + --depth; + else if (c == ':' && depth == 0) { + separator = i; + break; + } + } + if (separator == std::string_view::npos) + throw deserialization_error("Missing map separator"); + result.emplace( + detail::range_item(detail::trim(entry.substr(0, separator))), + detail::range_item(detail::trim(entry.substr(separator + 1)))); + } + return result; + } else { + constexpr bool is_set = requires { typename T::key_type; }; + auto values = detail::split_values(text, is_set ? '{' : '[', is_set ? '}' : ']'); + return detail::make_range(values.size(), [&](std::size_t i) { + return detail::range_item(values[i]); + }); + } + } else { + static_assert(detail::dependent_false, + "Implement serial_xml::from_string(std::string_view) for this leaf type"); + } +} + +namespace detail { +// Text and names borrow the input or the reader's arena, which outlives reconstruction. +struct xml_text { + std::string_view value; + bool cdata = false; +}; +// A direct bump allocator avoids virtual allocation calls for the parser's small records. +class xml_arena final : public std::pmr::memory_resource { + alignas(std::max_align_t) std::array storage_; + std::size_t used_ = 0; + std::pmr::monotonic_buffer_resource overflow_; + + void* do_allocate(std::size_t size, std::size_t alignment) override { + return allocate_record(size, alignment); + } + void do_deallocate(void*, std::size_t, std::size_t) override {} + bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override { + return this == &other; + } + + public: + void* allocate_record(std::size_t size, std::size_t alignment) { + if (alignment <= alignof(std::max_align_t)) { + auto offset = (used_ + alignment - 1) & ~(alignment - 1); + if (offset <= storage_.size() && size <= storage_.size() - offset) { + used_ = offset + size; + return storage_.data() + offset; + } + } + return overflow_.allocate(size, alignment); + } +}; + +// Append-only arena storage keeps parsed nodes stable without reallocating or moving siblings. +// Its elements contain only borrowed views and other arena lists; the reader owns all storage. +template +class xml_list { + struct link { + link* next = nullptr; + T value; + template + explicit link(Args&&... args) : value(std::forward(args)...) {} + }; + link* first_ = nullptr; + link* last_ = nullptr; + + public: + struct iterator { + using value_type = T; + using difference_type = std::ptrdiff_t; + using iterator_category = std::forward_iterator_tag; + link* current = nullptr; + const T& operator*() const { return current->value; } + const T* operator->() const { return ¤t->value; } + iterator& operator++() { + current = current->next; + return *this; + } + iterator operator++(int) { + auto old = *this; + ++*this; + return old; + } + bool operator==(const iterator&) const = default; + }; + iterator begin() const { return {first_}; } + iterator end() const { return {}; } + bool empty() const { return first_ == nullptr; } + std::size_t size() const { return std::distance(begin(), end()); } + const T& front() const { return first_->value; } + template + T& emplace_back(xml_arena* resource, Args&&... args) { + static_assert(std::is_trivially_destructible_v); + auto* item = std::construct_at( + static_cast(resource->allocate_record(sizeof(link), alignof(link))), + std::forward(args)...); + if (last_) + last_->next = item; + else + first_ = item; + last_ = item; + return item->value; + } + void push_back(xml_arena* resource, const T& value) { emplace_back(resource, value); } +}; +struct xml_node { + std::string_view name; + xml_arena* resource; + xml_list> attributes; + xml_list children; + xml_list text; + + explicit xml_node(xml_arena* arena) : resource(arena) {} +}; + +// An XML 1.0 reader without DTD validation. External entities are unsupported; +// namespace names (including prefixes) are matched literally. +class xml_reader { + public: + explicit xml_reader(std::string_view input) : input_(input) {} + + inline xml_node read() { + if (input_.starts_with("\xEF\xBB\xBF")) position_ = 3; + whitespace(); + if (starts(" position_ + 5 && + trim(input_.substr(position_ + 5, 1)).empty()) + declaration(); + miscellaneous(); + xml_node root(&resource_); + element(root, 0); + miscellaneous(); + if (position_ != input_.size()) fail("Trailing content after root element"); + return root; + } + + private: + // Small documents allocate their temporary tree entirely on the stack. + // Large documents transparently spill into the monotonic resource's upstream allocator. + xml_arena resource_; + std::string_view input_; + std::size_t position_ = 0; + + [[noreturn]] void fail(std::string_view message) const { + throw deserialization_error(std::string(message) + " at byte " + std::to_string(position_)); + } + bool starts(std::string_view value) const { return input_.substr(position_).starts_with(value); } + [[gnu::always_inline]] inline void whitespace() { + while (position_ < input_.size() && (input_[position_] == ' ' || input_[position_] == '\t' || + input_[position_] == '\r' || input_[position_] == '\n')) + ++position_; + } + void expect(std::string_view value) { + if (!starts(value)) fail("Expected " + std::string(value)); + position_ += value.size(); + } + static bool valid_character(std::uint32_t c) { + return c == 9 || c == 10 || c == 13 || (c >= 0x20 && c <= 0xD7FF) || + (c >= 0xE000 && c <= 0xFFFD) || (c >= 0x10000 && c <= 0x10FFFF); + } + static bool plain_ascii(std::string_view value, bool entities) { + constexpr std::uint64_t high_bits = 0x8080808080808080ULL; + constexpr std::uint64_t spaces = 0x2020202020202020ULL; + constexpr std::uint64_t ampersands = 0x2626262626262626ULL; + constexpr std::uint64_t ones = 0x0101010101010101ULL; + std::size_t offset = 0; + for (; value.size() - offset >= sizeof(std::uint64_t); offset += sizeof(std::uint64_t)) { + std::uint64_t bytes; + std::memcpy(&bytes, value.data() + offset, sizeof(bytes)); + // Any non-ASCII byte or character below space needs the full validator. + if ((bytes & high_bits) || ((bytes - spaces) & ~bytes & high_bits)) return false; + if (entities) { + auto difference = bytes ^ ampersands; + if ((difference - ones) & ~difference & high_bits) return false; + } + } + for (; offset < value.size(); ++offset) { + auto c = static_cast(value[offset]); + if (c < 0x20 || c >= 0x80 || (entities && c == '&')) return false; + } + return true; + } + + std::string_view decode(std::string_view value, bool entities = true, bool attribute = false) { + if (plain_ascii(value, entities)) return value; + std::pmr::string output(&resource_); + bool changed = false; + auto begin_output = [&](std::size_t index) { + if (!changed) { + output.assign(value.substr(0, index)); + changed = true; + } + }; + for (std::size_t i = 0; i < value.size(); ++i) { + unsigned char c = value[i]; + if (c == '&' && entities) { + begin_output(i); + auto end = value.find(';', i + 1); + if (end == std::string_view::npos) fail("Unterminated entity reference"); + auto entity = value.substr(i + 1, end - i - 1); + if (entity == "amp") + output += '&'; + else if (entity == "lt") + output += '<'; + else if (entity == "gt") + output += '>'; + else if (entity == "quot") + output += '\"'; + else if (entity == "apos") + output += '\''; + else if (entity.starts_with('#')) { + entity.remove_prefix(1); + int base = 10; + if (entity.starts_with('x')) { + base = 16; + entity.remove_prefix(1); + } + std::uint32_t codepoint{}; + auto [ptr, error] = + std::from_chars(entity.data(), entity.data() + entity.size(), codepoint, base); + if (entity.empty() || error != std::errc{} || ptr != entity.data() + entity.size()) + fail("Invalid character reference"); + if (!valid_character(codepoint)) fail("Invalid XML character reference"); + // Encoded references need at most four UTF-8 bytes. + if (codepoint < 0x80) + output += static_cast(codepoint); + else { + unsigned count = codepoint < 0x800 ? 2 : codepoint < 0x10000 ? 3 : 4; + output += static_cast((count == 2 ? 0xC0 + : count == 3 ? 0xE0 + : 0xF0) | + (codepoint >> (6 * (count - 1)))); + for (unsigned j = count - 1; j > 0; --j) + output += static_cast(0x80 | ((codepoint >> (6 * (j - 1))) & 0x3F)); + } + } else + fail("Unknown entity reference"); + i = end; + } else if (c < 0x80) { + if (!valid_character(c)) fail("Invalid XML character"); + if (c == '\r' || (attribute && (c == '\n' || c == '\t'))) { + begin_output(i); + output += attribute ? ' ' : '\n'; + if (c == '\r' && i + 1 < value.size() && value[i + 1] == '\n') ++i; + } else if (changed) + output += static_cast(c); + } else { + unsigned length = c >= 0xC2 && c <= 0xDF ? 2 + : c >= 0xE0 && c <= 0xEF ? 3 + : c >= 0xF0 && c <= 0xF4 ? 4 + : 0; + if (!length || i + length > value.size()) fail("Invalid UTF-8"); + std::uint32_t codepoint = c & (0x7F >> length); + for (unsigned j = 1; j < length; ++j) { + auto next = static_cast(value[i + j]); + if ((next & 0xC0) != 0x80) fail("Invalid UTF-8"); + codepoint = (codepoint << 6) | (next & 0x3F); + } + if ((length == 2 && codepoint < 0x80) || (length == 3 && codepoint < 0x800) || + (length == 4 && codepoint < 0x10000) || !valid_character(codepoint)) + fail("Invalid UTF-8"); + if (changed) output.append(value.substr(i, length)); + i += length - 1; + } + } + if (!changed) return value; + auto* data = static_cast(resource_.allocate(output.size(), alignof(char))); + std::memcpy(data, output.data(), output.size()); + return {data, output.size()}; + } + // Keep ASCII scanning in the recursive parser; Unicode validation is a cold call. + [[gnu::always_inline]] inline std::string_view name() { + auto begin = position_; + static constexpr auto classes = [] { + std::array result{}; + for (unsigned c = 0; c < result.size(); ++c) { + bool first = + (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_' || c == ':' || c >= 0x80; + result[c] = + (first ? 1 : 0) | ((first || (c >= '0' && c <= '9') || c == '-' || c == '.') ? 2 : 0); + } + return result; + }(); + if (position_ == input_.size() || !(classes[static_cast(input_[position_])] & 1)) + fail("Invalid XML name"); + bool unicode = static_cast(input_[position_]) >= 0x80; + ++position_; + while (position_ < input_.size()) { + unsigned char c = input_[position_]; + unicode = unicode || c >= 0x80; + if (!(classes[c] & 2)) break; + ++position_; + } + auto result = input_.substr(begin, position_ - begin); + if (!unicode) return result; // The scanning loop already validated ASCII names. + validate_unicode_name(result); + return result; + } + void validate_unicode_name(std::string_view result) { + decode(result, false); + // XML NameStartChar/NameChar ranges are wider than ASCII but narrower than UTF-8. + for (std::size_t i = 0; i < result.size();) { + auto c = static_cast(result[i]); + std::uint32_t codepoint = c; + unsigned length = 1; + if (c >= 0x80) { + length = c < 0xE0 ? 2 : c < 0xF0 ? 3 : 4; + codepoint = c & (0x7F >> length); + for (unsigned j = 1; j < length; ++j) + codepoint = (codepoint << 6) | (static_cast(result[i + j]) & 0x3F); + } + bool start = + codepoint == ':' || codepoint == '_' || (codepoint >= 'A' && codepoint <= 'Z') || + (codepoint >= 'a' && codepoint <= 'z') || (codepoint >= 0xC0 && codepoint <= 0xD6) || + (codepoint >= 0xD8 && codepoint <= 0xF6) || (codepoint >= 0xF8 && codepoint <= 0x2FF) || + (codepoint >= 0x370 && codepoint <= 0x37D) || + (codepoint >= 0x37F && codepoint <= 0x1FFF) || + (codepoint >= 0x200C && codepoint <= 0x200D) || + (codepoint >= 0x2070 && codepoint <= 0x218F) || + (codepoint >= 0x2C00 && codepoint <= 0x2FEF) || + (codepoint >= 0x3001 && codepoint <= 0xD7FF) || + (codepoint >= 0xF900 && codepoint <= 0xFDCF) || + (codepoint >= 0xFDF0 && codepoint <= 0xFFFD) || + (codepoint >= 0x10000 && codepoint <= 0xEFFFF); + bool continuation = start || codepoint == '-' || codepoint == '.' || + (codepoint >= '0' && codepoint <= '9') || codepoint == 0xB7 || + (codepoint >= 0x300 && codepoint <= 0x36F) || + (codepoint >= 0x203F && codepoint <= 0x2040); + if (!(i == 0 ? start : continuation)) fail("Invalid XML name character"); + i += length; + } + } + void comment() { + expect("") + fail("Malformed XML comment"); + decode(input_.substr(position_, end - position_), false); + position_ = end + 3; + } + void declaration() { + expect("> attributes; + for (;;) { + auto before = position_; + whitespace(); + if (starts("?>")) { + position_ += 2; + break; + } + if (before == position_) fail("Missing whitespace in XML declaration"); + auto key = name(); + whitespace(); + expect("="); + whitespace(); + if (position_ == input_.size() || (input_[position_] != '\"' && input_[position_] != '\'')) + fail("Expected quoted XML declaration value"); + char quote = input_[position_++]; + auto end = input_.find(quote, position_); + if (end == std::string_view::npos) fail("Unterminated XML declaration"); + attributes.emplace_back(std::move(key), input_.substr(position_, end - position_)); + position_ = end + 1; + } + if (attributes.empty() || attributes[0].first != "version" || attributes[0].second != "1.0") + fail("XML declaration requires version 1.0"); + std::size_t index = 1; + if (index < attributes.size() && attributes[index].first == "encoding") { + auto encoding = attributes[index++].second; + for (char& c : encoding) + if (c >= 'a' && c <= 'z') c = static_cast(c - 'a' + 'A'); + if (encoding != "UTF-8") fail("Only UTF-8 XML is supported"); + } + if (index < attributes.size() && attributes[index].first == "standalone") { + auto value = attributes[index++].second; + if (value != "yes" && value != "no") fail("Invalid standalone declaration"); + } + if (index != attributes.size()) fail("Invalid XML declaration attributes"); + } + void processing_instruction() { + expect("= 'A' && c <= 'Z') c = static_cast(c - 'A' + 'a'); + if (target == "xml") fail("Reserved processing instruction target"); + auto end = input_.find("?>", position_); + if (end == std::string_view::npos) fail("Unterminated processing instruction"); + if (position_ != end && trim(input_.substr(position_, 1)).size()) + fail("Invalid processing instruction"); + decode(input_.substr(position_, end - position_), false); + position_ = end + 2; + } + void miscellaneous() { + for (;;) { + whitespace(); + if (starts("" + "<>&"'A😀]]>" + ""); + EXPECT_EQ(result.value, "<>&\"'A😀<&>"); + EXPECT_EQ(serial_xml::from_xml( + "line\r\nbreak") + .value, + "line\nbreak"); + struct EntityNumberInput { + int value; + }; + EXPECT_EQ(serial_xml::from_xml( + "\n \n 42\n \n") + .value, + 42); +} + +TEST(FromXml, RejectsMalformedXmlAndDuplicateMembers) { + struct MalformedInputRecord { + int value; + }; + for (auto xml : + {"", + "", + "1", + "1", + "1", + "11junk", + "", + "1", + "1", + "&unknown;", + "�", + "�", + "12", + "", + "", + "1", + "1", + "]]>", + "1"}) { + EXPECT_THROW(serial_xml::from_xml(xml), serial_xml::deserialization_error) + << xml; + } + struct MalformedTextInput { + std::string value; + }; + for (std::string bad : + {std::string("\xC0\x80"), std::string("\xED\xA0\x80"), std::string("\x01", 1)}) { + EXPECT_THROW(serial_xml::from_xml("" + bad + + ""), + serial_xml::deserialization_error); + } +} + +TEST(FromXml, OptionalValuesAndFormattedRangeItems) { + struct OptionalInputChild { + int value; + }; + struct OptionalInputRecord { + [[= serial_xml::attribute]] std::optional id; + std::optional title; + [[= serial_xml::unpack]] std::optional child; + std::optional number; + std::optional text; + [[= serial_xml::format{format_deserialization_leaf}]] std::vector leaves; + }; + OptionalInputRecord original{7, "text", OptionalInputChild{9}, 11, "<&>", {{13}, {14}}}; + auto xml = serial_xml::to_xml(original); + auto restored = serial_xml::from_xml(xml); + ASSERT_TRUE(restored.child.has_value()); + EXPECT_EQ(restored.child->value, 9); + EXPECT_EQ(restored.leaves, original.leaves); + EXPECT_EQ(restored.number, 11); + EXPECT_EQ(restored.text, "<&>"); + EXPECT_EQ(serial_xml::to_xml(restored), xml); + auto empty = serial_xml::from_xml( + ""); + EXPECT_FALSE(empty.id); + EXPECT_FALSE(empty.title); + EXPECT_FALSE(empty.child); + EXPECT_FALSE(empty.number); + EXPECT_FALSE(empty.text); + struct FormattedCdataNumberInput { + [[ = serial_xml::cdata, = serial_xml::format{"04d"} ]] int value; + }; + auto number_xml = serial_xml::to_xml(FormattedCdataNumberInput{16}); + EXPECT_EQ(serial_xml::from_xml(number_xml).value, 16); +} + +TEST(FromXml, NestedAndRangeSetters) { + struct SetterInputChild { + int value; + }; + class SetterInputRecord { + public: + [[ = serial_xml::setter, = serial_xml::name{"child"} ]] void load(SetterInputChild value) { + child = value; + } + [[ = serial_xml::setter, + = serial_xml::iter{"entry", "list"} ]] void set_values(const std::vector& value) { + values = value; + } + [[= serial_xml::skip]] SetterInputChild child{}; + [[= serial_xml::skip]] std::vector values; + }; + auto value = serial_xml::from_xml( + "7" + "89"); + EXPECT_EQ(value.child.value, 7); + EXPECT_EQ(value.values, (std::vector{8, 9})); + EXPECT_EQ(serial_xml::to_xml(value, false), ""); +} + +TEST(FromXml, ConstAnnotatedSetterAndReadonlyGetter) { + class ConstSetterInput { + public: + [[= serial_xml::setter]] int set_value(int input = 0) const { + value = input; + return input; + } + std::vector calculated() const { return {}; } + [[= serial_xml::skip]] mutable int value = 0; + }; + auto input = serial_xml::from_xml( + "7"); + EXPECT_EQ(input.value, 7); + // Setter methods never participate in serialization, even when const and callable with no + // arguments. + EXPECT_EQ(serial_xml::to_xml(input, false), ""); +} + +TEST(FromXml, AutomaticForwardListAndContainerLimits) { + struct ForwardListInput { + std::forward_list values; + }; + ForwardListInput original{{1, 2, 3}}; + auto xml = serial_xml::to_xml(original); + EXPECT_EQ(serial_xml::from_xml(xml).values, original.values); + struct LimitedInput { + std::inplace_vector values; + }; + EXPECT_THROW( + serial_xml::from_xml( + "12"), + serial_xml::deserialization_error); + struct ArrayInput { + std::array values; + }; + EXPECT_THROW(serial_xml::from_xml( + "1"), + serial_xml::deserialization_error); + EXPECT_THROW(serial_xml::from_xml(""), + serial_xml::deserialization_error); +} + +TEST(FromXml, DeclarationValidationAndDepthLimit) { + struct DeclarationInput {}; + EXPECT_NO_THROW(serial_xml::from_xml( + "")); + for (auto declaration : + {"", "", "", "", + "", "", + ""}) { + EXPECT_THROW( + serial_xml::from_xml(std::string(declaration) + ""), + serial_xml::deserialization_error); + } + std::string xml = ""; + for (int i = 0; i < 256; ++i) xml += ""; + for (int i = 0; i < 256; ++i) xml += ""; + xml += ""; + EXPECT_THROW(serial_xml::from_xml(xml), serial_xml::deserialization_error); +} + +TEST(FromXml, RawScalarAmbiguityAndAttributeNormalization) { + struct AmbiguousRawInput { + [[= serial_xml::raw]] std::string first; + [[= serial_xml::raw]] std::string second; + }; + EXPECT_THROW(serial_xml::from_xml("ab"), + serial_xml::deserialization_error); + struct NormalizedAttributeInput { + [[= serial_xml::attribute]] std::string text; + }; + auto result = serial_xml::from_xml( + ""); + EXPECT_EQ(result.text, "a b c\nd"); +} + +TEST(FromString, CharacterRangesAndMalformedQuotes) { + EXPECT_EQ(serial_xml::from_string>("['a', '\\n', '\\\'']"), + (std::vector{'a', '\n', '\''})); + EXPECT_THROW(serial_xml::from_string>("[\"a\" \"b\"]"), + serial_xml::deserialization_error); + EXPECT_THROW(serial_xml::from_string(std::string_view{}), serial_xml::deserialization_error); +} + +TEST(FromXml, AttributesWithContainerRepresentations) { + struct ContainerAttributeInput { + [[= serial_xml::attribute]] std::vector values; + [[= serial_xml::attribute]] std::optional> maybe; + }; + ContainerAttributeInput original{{1, 2}, std::vector{3, 4}}; + auto xml = serial_xml::to_xml(original, false); + EXPECT_EQ(xml, ""); + auto restored = serial_xml::from_xml(xml); + EXPECT_EQ(restored.values, original.values); + EXPECT_EQ(restored.maybe, original.maybe); +} + +TEST(FromString, DebugUnicodeEscapes) { + using Strings = std::vector; + EXPECT_EQ(serial_xml::from_string("[\"\\u{1}\", \"\\u{1f600}\"]"), + (Strings{std::string("\x01", 1), "😀"})); + auto values = Strings{std::string("\x01", 1), "quote\" newline\n"}; + EXPECT_EQ(serial_xml::from_string(std::format("{}", values)), values); + for (auto input : {"[\"\\u{}\"]", "[\"\\u{D800}\"]", "[\"\\u{110000}\"]", "[\"\\u{1\"]"}) { + EXPECT_THROW(serial_xml::from_string(input), serial_xml::deserialization_error); + } +} + +TEST(FromXml, UnicodeNamesAndProcessingInstructions) { + struct[[= serial_xml::name{"résumé"}]] UnicodeNameInput { + [[= serial_xml::name{"état"}]] int value; + }; + EXPECT_EQ(serial_xml::from_xml( + "<état>7") + .value, + 7); + EXPECT_THROW(serial_xml::from_xml("<\xC2\x80/>"), + serial_xml::deserialization_error); + std::string override_name = "override"; + EXPECT_EQ( + serial_xml::from_xml("<état>8", override_name) + .value, + 8); +} + +TEST(FromXml, EscapingAcrossSimdAndMaskBoundaries) { + struct EscapeBoundaryInput { + [[= serial_xml::attribute]] std::string attribute; + std::string text; + [[= serial_xml::raw]] std::string raw; + }; + auto reference_escape = [](std::string_view input) { + std::string output; + for (char c : input) { + switch (c) { + case '<': + output += "<"; + break; + case '>': + output += ">"; + break; + case '&': + output += "&"; + break; + case '"': + output += """; + break; + case '\'': + output += "'"; + break; + default: + output += c; + } + } + return output; + }; + for (std::size_t size = 0; size <= 150; ++size) { + std::string value; + for (std::size_t i = 0; i < size; ++i) value += "<&a>\"b'c"[i % 8]; + auto escaped = reference_escape(value); + EscapeBoundaryInput original{value, value, value}; + auto xml = serial_xml::to_xml(original, false); + EXPECT_EQ(xml, "" + escaped + + "" + escaped + "") + << size; + auto restored = serial_xml::from_xml(xml); + EXPECT_EQ(restored.attribute, value) << size; + EXPECT_EQ(restored.text, value) << size; + EXPECT_EQ(restored.raw, value) << size; + } + for (std::size_t position = 0; position < 140; ++position) { + std::string value(140, 'a'); + value[position] = '&'; + auto restored = serial_xml::from_xml( + serial_xml::to_xml(EscapeBoundaryInput{value, value, value})); + EXPECT_EQ(restored.text, value) << position; + } +} + +TEST(FromXml, RawPrecedenceAndOptionalRawDefaults) { + struct RawPrecedenceChild { + int value; + }; + struct RawPrecedenceInput { + [[= serial_xml::raw]] RawPrecedenceChild child; + [[= serial_xml::raw]] std::optional value; + }; + RawPrecedenceInput original{{7}, 8}; + auto restored = serial_xml::from_xml(serial_xml::to_xml(original)); + EXPECT_EQ(restored.child.value, 7); + EXPECT_EQ(restored.value, 8); + struct OptionalRawInput { + [[ = serial_xml::raw, = serial_xml::optional ]] std::string text = "default"; + [[ = serial_xml::raw, = serial_xml::optional, = serial_xml::iter{"item"} ]] std::vector + values{9}; + }; + auto absent = serial_xml::from_xml(""); + EXPECT_EQ(absent.text, "default"); + EXPECT_EQ(absent.values, (std::vector{9})); +} + +TEST(FromXml, StringInputWithLiteralRootOverride) { + struct StringRootOverrideInput { + int value; + }; + std::string xml = "42"; + const std::string const_xml = xml; + EXPECT_EQ(serial_xml::from_xml(xml, "record").value, 42); + EXPECT_EQ(serial_xml::from_xml(const_xml, "record").value, 42); + std::string_view view = xml; + EXPECT_EQ(serial_xml::from_xml(view, "record").value, 42); + EXPECT_EQ(xml, const_xml); +} + +TEST(FromXml, BorrowedParserTextProducesOwningResults) { + struct BorrowedTextInput { + std::string text; + std::vector values; + }; + std::string xml = + "long text & decoded text owned by result" + "first < valuesecond value" + ""; + auto result = serial_xml::from_xml(xml); + xml.assign(xml.size(), 'x'); + EXPECT_EQ(result.text, "long text & decoded text owned by result"); + EXPECT_EQ(result.values, (std::vector{"first < value", "second value"})); +} + +TEST(FromXml, ArenaOverflowPreservesDecodedValues) { + struct ArenaOverflowInput { + std::vector values; + }; + ArenaOverflowInput original; + for (int i = 0; i < 600; ++i) + original.values.push_back(std::to_string(i) + " long & escaped "); + auto restored = serial_xml::from_xml(serial_xml::to_xml(original)); + EXPECT_EQ(restored.values, original.values); +} + +TEST(FromXml, InPlaceInputMayAliasAnUpdatedField) { + struct AliasedInputRecord { + std::string source; + int value; + }; + AliasedInputRecord record{ + "replacement42", 0}; + serial_xml::from_xml(record, record.source); + EXPECT_EQ(record.source, "replacement"); + EXPECT_EQ(record.value, 42); +} + +TEST(FromXml, MixedTextNormalizationAndArenaLifetimes) { + struct MixedArenaInput { + [[= serial_xml::attribute]] std::string attribute; + std::string text; + }; + auto result = serial_xml::from_xml( + "" + "é😀\r\n]]>tail"); + EXPECT_EQ(result.attribute, "é😀 &\r"); + EXPECT_EQ(result.text, "é😀\n<&>tail"); +} + +TEST(FromXml, AsciiFastPathValidatesEveryByteAndWordBoundary) { + struct AsciiBoundaryInput { + std::string value; + }; + for (std::size_t position : {0u, 7u, 8u, 15u, 16u, 31u, 32u, 63u, 64u}) { + for (unsigned byte = 0; byte <= 255; ++byte) { + std::string text(96, 'a'); + text[position] = static_cast(byte); + auto xml = "" + text + ""; + bool valid = byte == 9 || byte == 10 || byte == 13 || (byte >= 32 && byte < 128); + valid = valid && byte != '<' && byte != '&'; + if (valid) { + if (byte == 13) text[position] = '\n'; + EXPECT_EQ(serial_xml::from_xml(xml).value, text) + << byte << ":" << position; + } else { + EXPECT_THROW(serial_xml::from_xml(xml), + serial_xml::deserialization_error) + << byte << ":" << position; + } + } + } +} + +TEST(FromXml, MalformedLateTextDoesNotUpdateInPlaceTarget) { + struct ArenaAtomicParseInput { + std::string first; + std::string second; + }; + ArenaAtomicParseInput original{"original first", "original second"}; + EXPECT_THROW( + serial_xml::from_xml(original, + "new firstinvalid " + "&bad;"), + serial_xml::deserialization_error); + EXPECT_EQ(original.first, "original first"); + EXPECT_EQ(original.second, "original second"); +} From 683e911a5c585a25c3b70f8d84e09da48344ec21 Mon Sep 17 00:00:00 2001 From: EJainDev Date: Fri, 9 Oct 2026 15:55:16 -0400 Subject: [PATCH 2/2] fix: slight edge case improvements --- include/serial_xml.cxx | 13 ++++++-- tests/test_serial_xml.cpp | 63 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/include/serial_xml.cxx b/include/serial_xml.cxx index b4f6033..e624cdc 100644 --- a/include/serial_xml.cxx +++ b/include/serial_xml.cxx @@ -1282,7 +1282,7 @@ T range_item(std::string_view text) { template T make_tuple(const std::vector& values, std::index_sequence) { - return T{range_item>(values[I])...}; + return T{range_item>>(values[I])...}; } } // namespace detail @@ -2129,6 +2129,13 @@ consteval bool explicitly_selected_setter() { return false; } +template +void invoke_setter(T& result, V& value) { + constexpr auto parameter = std::meta::type_of(std::meta::parameters_of(Setter)[0]); + using P = [:parameter:]; + result.[:Setter:](std::forward

(value)); +} + template void read_object(const xml_node& node, T& result) { using S = std::conditional_t, T, Schema>; @@ -2145,14 +2152,14 @@ void read_object(const xml_node& node, T& result) { [[maybe_unused]] constexpr auto checked = input_type(); using V = value_t; if (auto value = read_member(node, cdata_index, raw_scalar_seen)) - result.[:source:](std::move(*value)); + invoke_setter(result, *value); } else if constexpr (std::meta::is_function(source)) { constexpr auto setter = matching_setter(); if constexpr (setter != std::meta::info{}) { if constexpr (!explicitly_selected_setter()) { using V = value_t; if (auto value = read_member(node, cdata_index, raw_scalar_seen)) - result.[:setter:](std::move(*value)); + invoke_setter(result, *value); } } // Read-only getters are serialization-only and intentionally ignored. diff --git a/tests/test_serial_xml.cpp b/tests/test_serial_xml.cpp index fbb0275..0c5b9b5 100644 --- a/tests/test_serial_xml.cpp +++ b/tests/test_serial_xml.cpp @@ -923,6 +923,19 @@ TEST(FromXml, OwningRangesAndIteration) { serial_xml::deserialization_error); } +TEST(FromXml, IteratedMapsWithConstKeys) { + struct MapInputRecord { + [[= serial_xml::iter{"entry"}]] std::map numbers; + [[= serial_xml::iter{"entry"}]] std::map strings; + }; + MapInputRecord original{{{1, 2}, {3, 4}}, {{"a:b", 5}, {"hello world", 6}}}; + auto xml = serial_xml::to_xml(original, false); + auto result = serial_xml::from_xml(xml); + EXPECT_EQ(result.numbers, original.numbers); + EXPECT_EQ(result.strings, original.strings); + EXPECT_EQ(serial_xml::to_xml(result, false), xml); +} + TEST(FromXml, RawAndCdata) { struct RawTextInput { int number; @@ -988,6 +1001,56 @@ TEST(FromXml, EncapsulationWithAutomaticSetterMatching) { EXPECT_EQ(result.read_only(), 77); } +TEST(FromXml, AutomaticSettersPreserveParameterCategories) { + class ReferenceSetterInput { + public: + int number() const { return number_; } + void set_number(int& value) { number_ = value; } + std::string title() const { return title_; } + void set_title(std::string&& value) { title_ = std::move(value); } + std::string label() const { return label_; } + void set_label(const std::string& value) { label_ = value; } + + private: + int number_ = 0; + std::string title_; + std::string label_; + }; + auto result = serial_xml::from_xml( + "42moved" + ""); + EXPECT_EQ(result.number(), 42); + EXPECT_EQ(result.title(), "moved"); + EXPECT_EQ(result.label(), "copied"); +} + +TEST(FromXml, AnnotatedAndMockSettersPreserveParameterCategories) { + class AnnotatedReferenceInput { + public: + [[= serial_xml::setter]] void set_number(int& value) { number_ = value; } + [[= serial_xml::setter]] void set_title(std::string&& value) { title_ = std::move(value); } + [[= serial_xml::skip]] int number() const { return number_; } + [[= serial_xml::skip]] std::string title() const { return title_; } + + private: + int number_ = 0; + std::string title_; + }; + auto result = serial_xml::from_xml( + "42moved" + ""); + EXPECT_EQ(result.number(), 42); + EXPECT_EQ(result.title(), "moved"); + struct[[= serial_xml::name{"record"}]] ReferenceSetterSchema { + [[= serial_xml::setter]] void set_number(int); + [[= serial_xml::setter]] void set_title(std::string); + }; + auto mock_result = serial_xml::from_xml( + "7mock"); + EXPECT_EQ(mock_result.number(), 7); + EXPECT_EQ(mock_result.title(), "mock"); +} + class IndependentSetters { public: [[ = serial_xml::setter, = serial_xml::attribute,