From b4b5d0b1ba8746513cf60478f3de8f8c13cc89d1 Mon Sep 17 00:00:00 2001 From: George Evmenov Date: Mon, 26 Oct 2026 15:50:37 +0300 Subject: [PATCH 1/4] Add std::bitset support with native, packed-byte and string encodings --- README.md | 1 + docs/content/standard_containers.md | 9 +- include/rfl/avro/Parser.hpp | 5 + include/rfl/avro/Reader.hpp | 3 + include/rfl/avro/Writer.hpp | 8 +- include/rfl/avro/to_schema.hpp | 1 + include/rfl/boost_serialization/Parser.hpp | 10 + include/rfl/bson/Parser.hpp | 9 + include/rfl/capnproto/Parser.hpp | 5 + include/rfl/capnproto/to_schema.hpp | 1 + include/rfl/cbor/Parser.hpp | 9 + include/rfl/cbor/Reader.hpp | 8 + include/rfl/cbor/Writer.hpp | 6 + include/rfl/cereal/Parser.hpp | 9 + include/rfl/csv/read.hpp | 12 ++ include/rfl/flexbuf/Parser.hpp | 9 + include/rfl/internal/bitset.hpp | 69 +++++++ include/rfl/internal/is_bitset.hpp | 21 ++ include/rfl/msgpack/Parser.hpp | 5 + include/rfl/parsing/Parser.hpp | 1 + include/rfl/parsing/ParserBitset.hpp | 66 ++++++ include/rfl/parsing/tabular/ArrowTypes.hpp | 36 ++++ include/rfl/ubjson/Parser.hpp | 5 + include/rfl/yas/Parser.hpp | 6 + tests/avro/test_bitset.cpp | 22 ++ tests/avro/test_bitset_schema.cpp | 18 ++ tests/avro/test_bytestring.cpp | 7 + tests/boost_serialization/test_bitset.cpp | 85 ++++++++ tests/bson/test_bitset.cpp | 22 ++ tests/capnproto/test_bitset.cpp | 26 +++ tests/capnproto/test_bitset_schema.cpp | 18 ++ tests/cbor/test_bitset.cpp | 53 +++++ tests/cereal/test_bitset.cpp | 227 +++++++++++++++++++++ tests/check_bitset.hpp | 189 +++++++++++++++++ tests/cli/test_bitset.cpp | 24 +++ tests/csv/test_bitset.cpp | 44 ++++ tests/env/test_bitset.cpp | 26 +++ tests/flexbuffers/test_bitset.cpp | 24 +++ tests/generic/test_bitset.cpp | 23 +++ tests/json/test_bitset.cpp | 32 +++ tests/msgpack/test_bitset.cpp | 22 ++ tests/parquet/test_bitset.cpp | 26 +++ tests/toml/test_bitset.cpp | 22 ++ tests/ubjson/test_bitset.cpp | 22 ++ tests/xml/test_bitset.cpp | 24 +++ tests/yaml/test_bitset.cpp | 22 ++ tests/yas/test_bitset.cpp | 53 +++++ 47 files changed, 1340 insertions(+), 5 deletions(-) create mode 100644 include/rfl/internal/bitset.hpp create mode 100644 include/rfl/internal/is_bitset.hpp create mode 100644 include/rfl/parsing/ParserBitset.hpp create mode 100644 tests/avro/test_bitset.cpp create mode 100644 tests/avro/test_bitset_schema.cpp create mode 100644 tests/boost_serialization/test_bitset.cpp create mode 100644 tests/bson/test_bitset.cpp create mode 100644 tests/capnproto/test_bitset.cpp create mode 100644 tests/capnproto/test_bitset_schema.cpp create mode 100644 tests/cbor/test_bitset.cpp create mode 100644 tests/cereal/test_bitset.cpp create mode 100644 tests/check_bitset.hpp create mode 100644 tests/cli/test_bitset.cpp create mode 100644 tests/csv/test_bitset.cpp create mode 100644 tests/env/test_bitset.cpp create mode 100644 tests/flexbuffers/test_bitset.cpp create mode 100644 tests/generic/test_bitset.cpp create mode 100644 tests/json/test_bitset.cpp create mode 100644 tests/msgpack/test_bitset.cpp create mode 100644 tests/parquet/test_bitset.cpp create mode 100644 tests/toml/test_bitset.cpp create mode 100644 tests/ubjson/test_bitset.cpp create mode 100644 tests/xml/test_bitset.cpp create mode 100644 tests/yaml/test_bitset.cpp create mode 100644 tests/yas/test_bitset.cpp diff --git a/README.md b/README.md index 04f727360..29a7399a4 100644 --- a/README.md +++ b/README.md @@ -616,6 +616,7 @@ reflect-cpp supports the following containers from the C++ standard library: - `std::array` - `std::atomic` - `std::atomic_flag` +- `std::bitset` - `std::deque` - `std::expected` - `std::chrono::duration` diff --git a/docs/content/standard_containers.md b/docs/content/standard_containers.md index 2f954a0fb..cf6bc5e51 100644 --- a/docs/content/standard_containers.md +++ b/docs/content/standard_containers.md @@ -51,12 +51,19 @@ This will also be represented as follows: ``` All other supported standard containers -(other than `std::variant`, `std::optional`, `std::unique_ptr`, `std::shared_ptr` and `std::expected`) +(other than `std::bitset`, `std::variant`, `std::optional`, `std::unique_ptr`, `std::shared_ptr` and `std::expected`) will be represented as arrays. Containers for which the `value_type` is a key-value-pair will be represented as arrays of pairs. `std::expected` is an exception to this: it is serialized as its value type, or as an object with a single `error` field. Refer to the [std::expected](../expected) section for details. +`std::bitset` uses the native representation in Cereal, YAS, CBOR, and +Boost.Serialization (packed in the first three, a string in Boost). Other +binary formats use `ceil(N / 8)` packed bytes: bit zero is the high bit of the +first byte, and unused low bits are zero. Other formats use exactly `N` binary +characters in `to_string()` order. The shared parsers reject incorrect lengths, +invalid characters, and nonzero padding; native adapters retain native validation. + ## Typed map keys in binary formats MessagePack, Cereal, YAS, and Boost.Serialization serialize the keys of the diff --git a/include/rfl/avro/Parser.hpp b/include/rfl/avro/Parser.hpp index 599ddc811..eaa484d02 100644 --- a/include/rfl/avro/Parser.hpp +++ b/include/rfl/avro/Parser.hpp @@ -14,6 +14,11 @@ namespace rfl { namespace parsing { +template +struct Parser, ProcessorsType> + : ParserBitset {}; + /// AVRO requires us to explicitly set all fields. Because /// of that, we require all of the fields and then set them to nullptr, if /// necessary. diff --git a/include/rfl/avro/Reader.hpp b/include/rfl/avro/Reader.hpp index e76028a55..a5b002c11 100644 --- a/include/rfl/avro/Reader.hpp +++ b/include/rfl/avro/Reader.hpp @@ -86,6 +86,9 @@ struct Reader { if (err) { return error("Could not cast to bytestring."); } + if (size == 0) { + return VectorType{}; + } const auto data = internal::ptr_cast(ptr); return VectorType(data, data + size); } else if constexpr (std::is_same, bool>()) { diff --git a/include/rfl/avro/Writer.hpp b/include/rfl/avro/Writer.hpp index 02389e1be..38962baeb 100644 --- a/include/rfl/avro/Writer.hpp +++ b/include/rfl/avro/Writer.hpp @@ -329,10 +329,10 @@ class RFL_API Writer { } else if constexpr (concepts::MutableContiguousByteContainer) { auto var = _var; - if (!var.data()) { - return; - } - int result = avro_value_set_bytes(_val, var.data(), var.size()); + // Avro requires a non-null pointer even for an empty bytes value. + typename Type::value_type empty{}; + auto* data = var.size() == 0 ? &empty : var.data(); + int result = avro_value_set_bytes(_val, data, var.size()); if (result != 0) { throw std::runtime_error( "Error setting bytestring value: " + std::to_string(result) + ": " + diff --git a/include/rfl/avro/to_schema.hpp b/include/rfl/avro/to_schema.hpp index 1865f6a31..e6eb8cef1 100644 --- a/include/rfl/avro/to_schema.hpp +++ b/include/rfl/avro/to_schema.hpp @@ -6,6 +6,7 @@ #include "../Processors.hpp" #include "../common.hpp" #include "../parsing/schema/make.hpp" +#include "Parser.hpp" #include "Reader.hpp" #include "Schema.hpp" #include "Writer.hpp" diff --git a/include/rfl/boost_serialization/Parser.hpp b/include/rfl/boost_serialization/Parser.hpp index e71cceeb5..4ccb2cc31 100644 --- a/include/rfl/boost_serialization/Parser.hpp +++ b/include/rfl/boost_serialization/Parser.hpp @@ -2,8 +2,10 @@ #define RFL_BOOST_SERIALIZATION_PARSER_HPP_ #include +#include #include #include +#include #include #include #include @@ -63,6 +65,14 @@ struct Parser, } }; +template +struct Parser, + boost_serialization::Writer, std::bitset, + ProcessorsType> + : ParserBasicType, + boost_serialization::Writer, std::bitset, + ProcessorsType> {}; + template requires(std::is_arithmetic_v && !std::is_same_v && !std::is_same_v) diff --git a/include/rfl/bson/Parser.hpp b/include/rfl/bson/Parser.hpp index e3973a460..b3c5b0226 100644 --- a/include/rfl/bson/Parser.hpp +++ b/include/rfl/bson/Parser.hpp @@ -10,6 +10,15 @@ namespace rfl::parsing { +template +struct Parser, ProcessorsType> + : ParserBitset {}; + +} // namespace rfl::parsing + +namespace rfl::parsing { + /// bson_oid_t needs to be treated as a special case, otherwise it will be read /// as a struct. template diff --git a/include/rfl/capnproto/Parser.hpp b/include/rfl/capnproto/Parser.hpp index 02261f3fe..157f4d848 100644 --- a/include/rfl/capnproto/Parser.hpp +++ b/include/rfl/capnproto/Parser.hpp @@ -13,6 +13,11 @@ namespace rfl { namespace parsing { +template +struct Parser, ProcessorsType> + : ParserBitset {}; + /// Cap'n proto requires us to explicitly set all fields. Because /// of that, we require all of the fields and then set them to nullptr, if /// necessary. diff --git a/include/rfl/capnproto/to_schema.hpp b/include/rfl/capnproto/to_schema.hpp index 2a6e96a2f..28e9d3bab 100644 --- a/include/rfl/capnproto/to_schema.hpp +++ b/include/rfl/capnproto/to_schema.hpp @@ -13,6 +13,7 @@ #include "../parsing/schema/Type.hpp" #include "../parsing/schema/ValidationType.hpp" #include "../parsing/schema/make.hpp" +#include "Parser.hpp" #include "Reader.hpp" #include "Schema.hpp" #include "Writer.hpp" diff --git a/include/rfl/cbor/Parser.hpp b/include/rfl/cbor/Parser.hpp index 8bcd9150e..2c467a7fc 100644 --- a/include/rfl/cbor/Parser.hpp +++ b/include/rfl/cbor/Parser.hpp @@ -5,6 +5,15 @@ #include "Reader.hpp" #include "Writer.hpp" +namespace rfl::parsing { + +template +struct Parser, ProcessorsType> + : ParserBasicType, + ProcessorsType> {}; + +} // namespace rfl::parsing + namespace rfl::cbor { template diff --git a/include/rfl/cbor/Reader.hpp b/include/rfl/cbor/Reader.hpp index d9ddf237f..05f49414f 100644 --- a/include/rfl/cbor/Reader.hpp +++ b/include/rfl/cbor/Reader.hpp @@ -25,6 +25,7 @@ #include "../Result.hpp" #include "../Vectorstring.hpp" #include "../always_false.hpp" +#include "../internal/is_bitset.hpp" #include "../internal/ptr_cast.hpp" namespace rfl::cbor { @@ -122,6 +123,13 @@ class Reader { } return _var.val_->as(); + } else if constexpr (internal::is_bitset_v) { + try { + return _var.val_->template as(); + } catch (const std::exception& e) { + return error(e.what()); + } + } else if constexpr (std::is_same, rfl::Bytestring>() || std::is_same, diff --git a/include/rfl/cbor/Writer.hpp b/include/rfl/cbor/Writer.hpp index 2a93b1522..c8005bbb7 100644 --- a/include/rfl/cbor/Writer.hpp +++ b/include/rfl/cbor/Writer.hpp @@ -9,6 +9,7 @@ #pragma GCC diagnostic ignored "-Wmaybe-uninitialized" #endif #endif +#include #include #ifdef __GNUC__ #ifndef __clang__ @@ -24,6 +25,7 @@ #include "../Vectorstring.hpp" #include "../always_false.hpp" #include "../common.hpp" +#include "../internal/is_bitset.hpp" namespace rfl::cbor { @@ -179,6 +181,10 @@ class RFL_API Writer { if constexpr (std::is_same, std::string>()) { encoder_->string_value(_var); + } else if constexpr (internal::is_bitset_v) { + // Native jsoncons conversion retains the CBOR base16 semantic tag. + jsoncons::json(_var).dump(*encoder_); + } else if constexpr (std::is_same, rfl::Bytestring>() || std::is_same, diff --git a/include/rfl/cereal/Parser.hpp b/include/rfl/cereal/Parser.hpp index ad66b7704..09ba7fa6c 100644 --- a/include/rfl/cereal/Parser.hpp +++ b/include/rfl/cereal/Parser.hpp @@ -2,6 +2,8 @@ #define RFL_CEREAL_PARSER_HPP_ #include +#include +#include #include #include #include @@ -53,6 +55,13 @@ struct Parser, ProcessorsType> } }; +// Delegate to Cereal's tagged, packed encoding through the reader and writer, +// retaining normal error handling and parent metadata. +template +struct Parser, ProcessorsType> + : ParserBasicType, + ProcessorsType> {}; + template requires(std::is_arithmetic_v && !std::is_same_v && !std::is_same_v) diff --git a/include/rfl/csv/read.hpp b/include/rfl/csv/read.hpp index effbc9413..4de8e390e 100644 --- a/include/rfl/csv/read.hpp +++ b/include/rfl/csv/read.hpp @@ -26,6 +26,7 @@ //#include "../concepts.hpp" #include "../internal/wrap_in_rfl_array_t.hpp" #include "../parsing/tabular/ArrowReader.hpp" +#include "../parsing/tabular/make_arrow_schema.hpp" #include "Settings.hpp" namespace rfl::csv { @@ -58,6 +59,17 @@ Result> read( std::vector({_settings.null_string}); convert_options.strings_can_be_null = true; + // Preserve textual columns before Arrow's inference can discard leading + // zeroes (in particular, the fixed-length text representation of bitsets). + using Row = typename T::value_type; + const auto schema = parsing::tabular::make_arrow_schema< + named_tuple_t, parsing::tabular::SerializationType::csv>(); + for (const auto& field : schema->fields()) { + if (field->type()->Equals(arrow::utf8())) { + convert_options.column_types[field->name()] = field->type(); + } + } + auto parse_options = arrow::csv::ParseOptions::Defaults(); parse_options.delimiter = _settings.delimiter; parse_options.quoting = _settings.quoting; diff --git a/include/rfl/flexbuf/Parser.hpp b/include/rfl/flexbuf/Parser.hpp index dd7020fec..a6a764959 100644 --- a/include/rfl/flexbuf/Parser.hpp +++ b/include/rfl/flexbuf/Parser.hpp @@ -5,6 +5,15 @@ #include "Reader.hpp" #include "Writer.hpp" +namespace rfl::parsing { + +template +struct Parser, ProcessorsType> + : ParserBitset {}; + +} // namespace rfl::parsing + namespace rfl { namespace flexbuf { diff --git a/include/rfl/internal/bitset.hpp b/include/rfl/internal/bitset.hpp new file mode 100644 index 000000000..4e54cfc3a --- /dev/null +++ b/include/rfl/internal/bitset.hpp @@ -0,0 +1,69 @@ +#ifndef RFL_INTERNAL_BITSET_HPP_ +#define RFL_INTERNAL_BITSET_HPP_ + +#include +#include +#include +#include + +#include "../Result.hpp" + +namespace rfl::internal { + +template +Result> bitset_from_string(const std::string_view _str) { + if (_str.size() != N) { + return error("Expected a bitset string of length " + std::to_string(N) + + ", got " + std::to_string(_str.size()) + "."); + } + std::bitset bits; + for (std::size_t i = 0; i < N; ++i) { + if (_str[i] != '0' && _str[i] != '1') { + return error("Bitset strings may contain only '0' and '1'."); + } + bits[N - 1 - i] = _str[i] == '1'; + } + return bits; +} + +// Bit zero occupies the most significant bit of the first byte, matching +// Cereal's packed payload. Never access the implementation's bitset storage. +template +Bytes bitset_to_bytes(const std::bitset& _bits) { + Bytes bytes{}; + if constexpr (requires { bytes.resize(0); }) { + bytes.resize(N / 8 + (N % 8 != 0)); + } + for (std::size_t i = 0; i < N; ++i) { + if (_bits[i]) { + bytes[i / 8] |= static_cast(0x80u >> (i % 8)); + } + } + return bytes; +} + +template +Result> bitset_from_bytes(const Bytes& _bytes) { + constexpr auto size = N / 8 + (N % 8 != 0); + if (_bytes.size() != size) { + return error("Expected " + std::to_string(size) + + " packed bitset bytes, got " + std::to_string(_bytes.size()) + + "."); + } + if constexpr (N % 8 != 0) { + constexpr auto mask = (1u << (8 - N % 8)) - 1; + if ((static_cast(_bytes.back()) & mask) != 0) { + return error("Packed bitset padding bits must be zero."); + } + } + std::bitset bits; + for (std::size_t i = 0; i < N; ++i) { + bits[i] = + (static_cast(_bytes[i / 8]) & (0x80u >> (i % 8))) != 0; + } + return bits; +} + +} // namespace rfl::internal + +#endif diff --git a/include/rfl/internal/is_bitset.hpp b/include/rfl/internal/is_bitset.hpp new file mode 100644 index 000000000..7b82a6d63 --- /dev/null +++ b/include/rfl/internal/is_bitset.hpp @@ -0,0 +1,21 @@ +#ifndef RFL_INTERNAL_IS_BITSET_HPP_ +#define RFL_INTERNAL_IS_BITSET_HPP_ + +#include +#include +#include + +namespace rfl::internal { + +template +struct is_bitset : std::false_type {}; + +template +struct is_bitset> : std::true_type {}; + +template +inline constexpr bool is_bitset_v = is_bitset>::value; + +} // namespace rfl::internal + +#endif diff --git a/include/rfl/msgpack/Parser.hpp b/include/rfl/msgpack/Parser.hpp index fdb9d0ee4..1db5abae9 100644 --- a/include/rfl/msgpack/Parser.hpp +++ b/include/rfl/msgpack/Parser.hpp @@ -9,6 +9,11 @@ namespace rfl { namespace parsing { +template +struct Parser, ProcessorsType> + : ParserBitset {}; + /// msgpack-c requires us to explicitly set the number of fields in advance. /// Because of that, we require all of the fields and then set them to nullptr, /// if necessary. diff --git a/include/rfl/parsing/Parser.hpp b/include/rfl/parsing/Parser.hpp index 62a49eb22..dc0f72104 100644 --- a/include/rfl/parsing/Parser.hpp +++ b/include/rfl/parsing/Parser.hpp @@ -1,6 +1,7 @@ #ifndef RFL_PARSING_PARSER_HPP_ #define RFL_PARSING_PARSER_HPP_ +#include "ParserBitset.hpp" #include "Parser_base.hpp" #include "Parser_default.hpp" #include "Parser_named_tuple.hpp" diff --git a/include/rfl/parsing/ParserBitset.hpp b/include/rfl/parsing/ParserBitset.hpp new file mode 100644 index 000000000..bc3dfbe19 --- /dev/null +++ b/include/rfl/parsing/ParserBitset.hpp @@ -0,0 +1,66 @@ +#ifndef RFL_PARSING_PARSER_BITSET_HPP_ +#define RFL_PARSING_PARSER_BITSET_HPP_ + +#include +#include +#include +#include +#include + +#include "../Result.hpp" +#include "../internal/bitset.hpp" +#include "Parser_base.hpp" +#include "schema/Type.hpp" + +namespace rfl::parsing { + +// Text formats use strings; binary formats can select Bytestring. Both +// representations use the normal parser path to retain parent metadata. +template +struct ParserBitset { + using Type = std::bitset; + using ReprParser = Parser; + + static Result read(const R& _r, + const typename R::InputVarType& _var) noexcept { + return ReprParser::read(_r, _var).and_then([](const auto& _value) { + if constexpr (std::is_same_v) { + return internal::bitset_from_string(_value); + } else { + return internal::bitset_from_bytes(_value); + } + }); + } + + template + static void write(const W& _w, const Type& _bits, const P& _parent) { + if constexpr (std::is_same_v) { + ReprParser::write(_w, _bits.to_string(), _parent); + } else { + ReprParser::write(_w, internal::bitset_to_bytes(_bits), + _parent); + } + } + + static schema::Type to_schema( + std::map* _definitions) { + if constexpr (std::is_same_v) { + // Unlike $, the final assertion also rejects a trailing newline. + return schema::Type{schema::Type::Validated{ + .type_ = Ref::make(schema::Type::String{}), + .validation_ = schema::ValidationType{schema::ValidationType::Regex{ + "^[01]{" + std::to_string(N) + "}(?![\\s\\S])"}}}}; + } else { + return ReprParser::to_schema(_definitions); + } + } +}; + +template +struct Parser, ProcessorsType> + : ParserBitset {}; + +} // namespace rfl::parsing + +#endif diff --git a/include/rfl/parsing/tabular/ArrowTypes.hpp b/include/rfl/parsing/tabular/ArrowTypes.hpp index 12f556581..906e25c1c 100644 --- a/include/rfl/parsing/tabular/ArrowTypes.hpp +++ b/include/rfl/parsing/tabular/ArrowTypes.hpp @@ -23,6 +23,7 @@ #include #include "../../Box.hpp" +#include "../../Bytestring.hpp" #include "../../NamedTuple.hpp" #include "../../Ref.hpp" #include "../../Rename.hpp" @@ -31,6 +32,7 @@ #include "../../concepts.hpp" #include "../../enums.hpp" #include "../../internal/StringLiteral.hpp" +#include "../../internal/bitset.hpp" #include "../../internal/has_reflection_type_v.hpp" #include "../../internal/ptr_cast.hpp" #include "../../named_tuple_t.hpp" @@ -837,6 +839,40 @@ struct ArrowTypes { } }; +// CSV stores the fixed-length text form; Parquet stores a binary column. +template +struct ArrowTypes, _s> + : ArrowTypes, + _s> { + using Representation = + std::conditional_t<_s == SerializationType::csv, std::string, Bytestring>; + using Base = ArrowTypes; + using ArrayType = typename Base::ArrayType; + using BuilderType = typename Base::BuilderType; + + static void add_to_builder(const std::bitset& _bits, + BuilderType* _builder) { + if constexpr (_s == SerializationType::csv) { + Base::add_to_builder(_bits.to_string(), _builder); + } else { + Base::add_to_builder(internal::bitset_to_bytes(_bits), + _builder); + } + } + + static Result> get_value(const Ref& _chunk, + const int64_t _ix) { + return Base::get_value(_chunk, _ix).and_then([](const auto& _value) { + if constexpr (_s == SerializationType::csv) { + return internal::bitset_from_string(_value); + } else { + return internal::bitset_from_bytes(_value); + } + }); + } +}; + template struct ArrowTypes, _s> { enum class TimeUnit { day, second, milli, micro, nano, string }; diff --git a/include/rfl/ubjson/Parser.hpp b/include/rfl/ubjson/Parser.hpp index ce65680b7..fa8de2b44 100644 --- a/include/rfl/ubjson/Parser.hpp +++ b/include/rfl/ubjson/Parser.hpp @@ -9,6 +9,11 @@ namespace rfl::parsing { +template +struct Parser, ProcessorsType> + : ParserBitset {}; + /// UBJSON requires us to explicitly set the number of fields in advance. /// Because of that, we require all of the fields and then set them to nullptr, /// if necessary. diff --git a/include/rfl/yas/Parser.hpp b/include/rfl/yas/Parser.hpp index 3f9a622be..b153d6772 100644 --- a/include/rfl/yas/Parser.hpp +++ b/include/rfl/yas/Parser.hpp @@ -2,11 +2,13 @@ #define RFL_YAS_PARSER_HPP_ #include +#include #include #include #include #include #include +#include #include "../Generic.hpp" #include "../NamedTuple.hpp" @@ -56,6 +58,10 @@ struct Parser, ProcessorsType> } }; +template +struct Parser, ProcessorsType> + : ParserBasicType, ProcessorsType> {}; + template requires((std::is_integral_v || std::is_same_v || std::is_same_v) && diff --git a/tests/avro/test_bitset.cpp b/tests/avro/test_bitset.cpp new file mode 100644 index 000000000..618bda8d2 --- /dev/null +++ b/tests/avro/test_bitset.cpp @@ -0,0 +1,22 @@ +#include + +#include "../check_bitset.hpp" + +namespace test_bitset_avro { + +const auto write = [](const auto& value) { return rfl::avro::write(value); }; +const auto read = [](const auto& value) { + return rfl::avro::read(value); +}; + +TEST(avro, bitset_sizes_and_patterns) { + test_bitset_formats::check_sizes(write, read); +} + +TEST(avro, bitset_nested) { test_bitset_formats::check_nested(write, read); } + +TEST(avro, bitset_representation_and_validation) { + test_bitset_formats::check_packed(write, read); +} + +} // namespace test_bitset_avro diff --git a/tests/avro/test_bitset_schema.cpp b/tests/avro/test_bitset_schema.cpp new file mode 100644 index 000000000..63162ac5b --- /dev/null +++ b/tests/avro/test_bitset_schema.cpp @@ -0,0 +1,18 @@ +// Include the schema entry point alone: it must select the packed parser. +#include + +#include +#include + +namespace test_bitset_schema_avro { + +struct Record { + std::bitset<9> bits; +}; + +TEST(avro, bitset_standalone_schema) { + const auto schema = rfl::avro::to_schema(); + EXPECT_NE(schema.str().find(R"("bytes")"), std::string::npos); +} + +} // namespace test_bitset_schema_avro diff --git a/tests/avro/test_bytestring.cpp b/tests/avro/test_bytestring.cpp index 29fe59e86..5ad5a7a12 100644 --- a/tests/avro/test_bytestring.cpp +++ b/tests/avro/test_bytestring.cpp @@ -15,4 +15,11 @@ TEST(avro, test_bytestring) { write_and_read(test); } + +TEST(avro, test_empty_bytestring) { + const auto bytes = rfl::avro::write(TestStruct{}); + const auto decoded = rfl::avro::read(bytes); + ASSERT_TRUE(decoded) << decoded.error().what(); + EXPECT_TRUE(decoded->bytestring.empty()); +} } // namespace test_bytestring diff --git a/tests/boost_serialization/test_bitset.cpp b/tests/boost_serialization/test_bitset.cpp new file mode 100644 index 000000000..e071e7298 --- /dev/null +++ b/tests/boost_serialization/test_bitset.cpp @@ -0,0 +1,85 @@ +#include +#include +#include +#include +#include +#include + +#include "../check_bitset.hpp" + +namespace test_bitset_boost { + +constexpr auto flags = boost::archive::no_header | boost::archive::no_tracking; +const auto write = [](const auto& value) { + return rfl::boost_serialization::write(value); +}; +const auto read = [](const auto& bytes) { + return rfl::boost_serialization::read(bytes); +}; +const auto native_write = [](const auto& value) { + std::ostringstream stream; + { + boost::archive::binary_oarchive archive(stream, flags); + archive << value; + } + const auto bytes = stream.str(); + return std::vector(bytes.begin(), bytes.end()); +}; +const auto native_read = [](const auto& bytes) { + std::istringstream stream(std::string(bytes.begin(), bytes.end())); + boost::archive::binary_iarchive archive(stream, flags); + T value; + archive >> value; + return value; +}; + +TEST(boost_serialization, bitset_native_interop) { + test_bitset_formats::check_native_sizes(write, read, native_write, + native_read); +} + +TEST(boost_serialization, bitset_nested) { + test_bitset_formats::check_nested(write, read); + test_bitset_formats::check_nested( + [](const auto& value) { + return rfl::boost_serialization::write(value); + }, + [](const auto& bytes) { + return rfl::boost_serialization::read(bytes); + }); + test_bitset_formats::roundtrip(std::map>{{7, {258}}}, + write, read); + test_bitset_formats::roundtrip( + std::unordered_map, int>{{{258}, 7}}, write, read); +} + +TEST(boost_serialization, bitset_invalid_character) { + auto bytes = write(std::bitset<9>{258}); + bytes.back() = 'x'; + EXPECT_FALSE(rfl::boost_serialization::read>(bytes)); +} + +TEST(boost_serialization, bitset_text_archive) { + const std::bitset<9> bits{258}; + std::ostringstream reflected; + { + boost::archive::text_oarchive archive(reflected); + rfl::boost_serialization::write(archive, + bits); + } + std::ostringstream native; + { + boost::archive::text_oarchive archive(native); + archive << bits; + } + EXPECT_EQ(reflected.str(), native.str()); + std::istringstream stream(native.str()); + boost::archive::text_iarchive archive(stream); + const auto decoded = rfl::boost_serialization::read_from_archive< + std::bitset<9>, boost::archive::text_iarchive, + boost::archive::text_oarchive>(archive); + ASSERT_TRUE(decoded) << decoded.error().what(); + EXPECT_EQ(*decoded, bits); +} + +} // namespace test_bitset_boost diff --git a/tests/bson/test_bitset.cpp b/tests/bson/test_bitset.cpp new file mode 100644 index 000000000..839129c46 --- /dev/null +++ b/tests/bson/test_bitset.cpp @@ -0,0 +1,22 @@ +#include + +#include "../check_bitset.hpp" + +namespace test_bitset_bson { + +const auto write = [](const auto& value) { return rfl::bson::write(value); }; +const auto read = [](const auto& value) { + return rfl::bson::read(value); +}; + +TEST(bson, bitset_sizes_and_patterns) { + test_bitset_formats::check_sizes(write, read); +} + +TEST(bson, bitset_nested) { test_bitset_formats::check_nested(write, read); } + +TEST(bson, bitset_representation_and_validation) { + test_bitset_formats::check_packed(write, read); +} + +} // namespace test_bitset_bson diff --git a/tests/capnproto/test_bitset.cpp b/tests/capnproto/test_bitset.cpp new file mode 100644 index 000000000..3fc2ae960 --- /dev/null +++ b/tests/capnproto/test_bitset.cpp @@ -0,0 +1,26 @@ +#include + +#include "../check_bitset.hpp" + +namespace test_bitset_capnproto { + +const auto write = [](const auto& value) { + return rfl::capnproto::write(value); +}; +const auto read = [](const auto& value) { + return rfl::capnproto::read(value); +}; + +TEST(capnproto, bitset_sizes_and_patterns) { + test_bitset_formats::check_sizes(write, read); +} + +TEST(capnproto, bitset_nested) { + test_bitset_formats::check_nested(write, read); +} + +TEST(capnproto, bitset_representation_and_validation) { + test_bitset_formats::check_packed(write, read); +} + +} // namespace test_bitset_capnproto diff --git a/tests/capnproto/test_bitset_schema.cpp b/tests/capnproto/test_bitset_schema.cpp new file mode 100644 index 000000000..736dad755 --- /dev/null +++ b/tests/capnproto/test_bitset_schema.cpp @@ -0,0 +1,18 @@ +// Include the schema entry point alone: it must select the packed parser. +#include + +#include +#include + +namespace test_bitset_schema_capnproto { + +struct Record { + std::bitset<9> bits; +}; + +TEST(capnproto, bitset_standalone_schema) { + const auto schema = rfl::capnproto::to_schema(); + EXPECT_NE(schema.str().find(R"(Data)"), std::string::npos); +} + +} // namespace test_bitset_schema_capnproto diff --git a/tests/cbor/test_bitset.cpp b/tests/cbor/test_bitset.cpp new file mode 100644 index 000000000..3e36c0628 --- /dev/null +++ b/tests/cbor/test_bitset.cpp @@ -0,0 +1,53 @@ +#include +#include + +#include "../check_bitset.hpp" + +namespace test_bitset_cbor { + +const auto write = [](const auto& value) { return rfl::cbor::write(value); }; +const auto read = [](const auto& value) { + return rfl::cbor::read(value); +}; +const auto native_write = [](const auto& value) { + std::vector bytes; + jsoncons::cbor::encode_cbor(value, bytes); + return bytes; +}; +const auto native_read = [](const auto& bytes) { + return jsoncons::cbor::decode_cbor(bytes); +}; + +TEST(cbor, bitset_native_interop) { + test_bitset_formats::check_native_sizes(write, read, native_write, + native_read); +} + +TEST(cbor, bitset_nested) { test_bitset_formats::check_nested(write, read); } + +TEST(cbor, bitset_native_encoding_and_validation) { + // Tag 23 (expected base16 conversion), two bytes, bits 1 and 8 set. + const std::vector expected{char(0xd7), char(0x42), char(0x40), + char(0x80)}; + EXPECT_EQ(write(std::bitset<9>{258}), expected); + EXPECT_EQ(write(std::bitset<256>{}).size(), 35u); + + for (const auto& value : {jsoncons::json(false), jsoncons::json("not hex"), + jsoncons::json(jsoncons::byte_string_arg, + std::vector{0x80})}) { + EXPECT_FALSE(rfl::cbor::read>(native_write(value))); + } + // Keep native acceptance of integers, hexadecimal strings, and byte strings, + // including ignored trailing bytes/padding; do not impose a different codec. + for (const auto& value : + {jsoncons::json(258), jsoncons::json("4080"), + jsoncons::json(jsoncons::byte_string_arg, + std::vector{0x40, 0xff, 0xaa})}) { + const auto bytes = native_write(value); + const auto decoded = rfl::cbor::read>(bytes); + ASSERT_TRUE(decoded) << decoded.error().what(); + EXPECT_EQ(*decoded, native_read.template operator()>(bytes)); + } +} + +} // namespace test_bitset_cbor diff --git a/tests/cereal/test_bitset.cpp b/tests/cereal/test_bitset.cpp new file mode 100644 index 000000000..7e5518faa --- /dev/null +++ b/tests/cereal/test_bitset.cpp @@ -0,0 +1,227 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace test_bitset { + +using OutputArchive = ::cereal::PortableBinaryOutputArchive; +using InputArchive = ::cereal::PortableBinaryInputArchive; + +template +class CerealBitset : public testing::Test {}; + +using Sizes = testing::Types, std::bitset<1>, std::bitset<7>, + std::bitset<8>, std::bitset<9>, std::bitset<64>, + std::bitset<256>>; +TYPED_TEST_SUITE(CerealBitset, Sizes); + +template +std::vector patterns() { + std::vector values{Bits{}, Bits{}.set()}; + Bits alternating; + for (std::size_t i = 0; i < alternating.size(); i += 2) { + alternating.set(i); + } + values.push_back(alternating); + values.push_back(~alternating); + // Single bits exercise both ends and every byte/word boundary independently. + for (std::size_t i = 0; i < alternating.size(); ++i) { + values.push_back(Bits{}.set(i)); + } + return values; +} + +template +void roundtrip(const T& input) { + const auto decoded = + rfl::cereal::read(rfl::cereal::write(input)); + ASSERT_TRUE(decoded) << decoded.error().what(); + EXPECT_EQ(*decoded, input); +} + +TYPED_TEST(CerealBitset, PackedNativeInterop) { + for (const auto options : {OutputArchive::Options::LittleEndian(), + OutputArchive::Options::BigEndian()}) { + for (const auto& bits : patterns()) { + SCOPED_TRACE(bits.to_string()); + std::ostringstream native; + { + OutputArchive archive(native, options); + archive(bits); + } + std::ostringstream reflected; + { + OutputArchive archive(reflected, options); + rfl::cereal::write(bits, archive); + } + const auto bytes = reflected.str(); + EXPECT_EQ(bytes, native.str()); + // Portable binary's endian header + representation tag + packed payload. + ASSERT_EQ(bytes.size(), 2 + (bits.size() + 7) / 8); + EXPECT_EQ(static_cast(bytes[1]), 3); + for (std::size_t i = 0; i < bits.size(); ++i) { + const auto byte = static_cast(bytes[2 + i / 8]); + EXPECT_EQ((byte & (0x80 >> (i % 8))) != 0, bits[i]); + } + if (bits.size() % 8 != 0) { + const auto padding_mask = (1u << (8 - bits.size() % 8)) - 1; + EXPECT_EQ(static_cast(bytes.back()) & padding_mask, 0u); + } + const auto decoded = rfl::cereal::read(native.str()); + ASSERT_TRUE(decoded) << decoded.error().what(); + EXPECT_EQ(*decoded, bits); + std::istringstream stream(bytes); + InputArchive archive(stream); + TypeParam native_decoded; + archive(native_decoded); + EXPECT_EQ(native_decoded, bits); + roundtrip(bits); + } + } +} + +template +struct Nested { + int head; + Bits bits; + std::array fixed; + std::vector sequence; + std::map typed_map; + std::map string_map; + std::optional optional; + std::variant choice; + int tail; + + bool operator==(const Nested&) const = default; +}; + +TYPED_TEST(CerealBitset, ParentMetadata) { + using Bits = TypeParam; + const auto bits = Bits{}.set(); + const Nested input{17, + bits, + {Bits{}, bits}, + {bits, Bits{}}, + {{7, bits}, {9, Bits{}}}, + {{"first", bits}, {"second", Bits{}}}, + bits, + bits, + 42}; + roundtrip(input); + roundtrip, rfl::NoFieldNames>(input); + roundtrip(std::variant{std::in_place_index<1>, bits}); + roundtrip(std::unordered_map{{bits, 42}}); + + // Check the library's map-like object container as well. + rfl::Object object; + object.insert(std::make_pair(std::string("descriptor"), bits)); + const auto decoded_object = + rfl::cereal::read>(rfl::cereal::write(object)); + ASSERT_TRUE(decoded_object) << decoded_object.error().what(); + ASSERT_TRUE(decoded_object->get("descriptor")); + EXPECT_EQ(decoded_object->get("descriptor").value(), bits); + + // Verify the typed map's count and keys against native Cereal, not just a + // roundtrip through matching reflect-cpp readers and writers. + std::ostringstream native; + { + OutputArchive archive(native); + archive(input.typed_map); + } + const auto bytes = rfl::cereal::write(input.typed_map); + EXPECT_EQ(std::string(bytes.begin(), bytes.end()), native.str()); + std::istringstream stream(native.str()); + const auto decoded_map = rfl::cereal::read>(stream); + ASSERT_TRUE(decoded_map) << decoded_map.error().what(); + EXPECT_EQ(*decoded_map, input.typed_map); + + // The variant discriminant must precede the bitset representation tag. + std::ostringstream variant_native; + { + OutputArchive archive(variant_native); + archive(std::size_t{1}, bits, std::uint32_t{123}); + } + std::ostringstream variant_reflected; + { + OutputArchive archive(variant_reflected); + rfl::cereal::write(std::variant{bits}, archive); + archive(std::uint32_t{123}); + } + EXPECT_EQ(variant_reflected.str(), variant_native.str()); + std::istringstream variant_stream(variant_native.str()); + InputArchive archive(variant_stream); + const auto choice = rfl::cereal::read>(archive); + ASSERT_TRUE(choice) << choice.error().what(); + ASSERT_EQ(choice->index(), 1u); + EXPECT_EQ(std::get<1>(*choice), bits); + std::uint32_t tail; + archive(tail); + EXPECT_EQ(tail, 123u); +} + +TYPED_TEST(CerealBitset, MalformedInput) { + auto bytes = rfl::cereal::write(TypeParam{}.set()); + for (std::size_t size = 0; size < bytes.size(); ++size) { + SCOPED_TRACE(size); + const auto decoded = rfl::cereal::read(bytes.data(), size); + ASSERT_FALSE(decoded); + EXPECT_NE(decoded.error().what().find("Cereal read error:"), + std::string::npos); + } + bytes[1] = static_cast(0xff); + const auto invalid = rfl::cereal::read(bytes); + ASSERT_FALSE(invalid); + EXPECT_NE(invalid.error().what().find("Invalid bitset data representation"), + std::string::npos); +} + +TEST(cereal, bitset_nested_errors) { + struct Record { + std::bitset<9> descriptor; + }; + auto record_bytes = rfl::cereal::write(Record{}); + record_bytes.pop_back(); + const auto record = rfl::cereal::read(record_bytes); + ASSERT_FALSE(record); + EXPECT_NE(record.error().what().find("descriptor"), std::string::npos); + + using Bits = std::bitset<9>; + using Vector = std::vector; + auto vector_bytes = rfl::cereal::write(Vector{Bits{}, Bits{}}); + vector_bytes.pop_back(); + const auto vector = rfl::cereal::read(vector_bytes); + ASSERT_FALSE(vector); + EXPECT_NE(vector.error().what().find("Cereal read error:"), + std::string::npos); + + using Map = std::map; + auto map_bytes = rfl::cereal::write(Map{{7, Bits{}}}); + map_bytes.pop_back(); + EXPECT_FALSE(rfl::cereal::read(map_bytes)); + + using Variant = std::variant; + auto variant_bytes = rfl::cereal::write(Variant{Bits{}}); + variant_bytes.pop_back(); + EXPECT_FALSE(rfl::cereal::read(variant_bytes)); + + // An invalid tag inside a container must propagate as a Result error too. + auto invalid_tag = rfl::cereal::write(std::array{Bits{}}); + invalid_tag[1] = static_cast(0xff); + EXPECT_FALSE((rfl::cereal::read>(invalid_tag))); +} + +} // namespace test_bitset diff --git a/tests/check_bitset.hpp b/tests/check_bitset.hpp new file mode 100644 index 000000000..76e7206a7 --- /dev/null +++ b/tests/check_bitset.hpp @@ -0,0 +1,189 @@ +#ifndef CHECK_BITSET_HPP_ +#define CHECK_BITSET_HPP_ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace test_bitset_formats { + +template +struct Record { + T bits; + int tail = 42; + bool operator==(const Record&) const = default; +}; + +template +void roundtrip(const T& input, Write write, Read read) { + const auto decoded = read.template operator()(write(input)); + ASSERT_TRUE(decoded) << decoded.error().what(); + EXPECT_EQ(*decoded, input); +} + +struct Nested { + std::bitset<0> empty; + std::array, 2> fixed; + std::vector> descriptors; + std::map> lookup; + std::optional> optional; + std::variant> choice; + int tail; + bool operator==(const Nested&) const = default; +}; + +template +void check_nested(Write write, Read read) { + const Nested input{{}, + {std::bitset<7>{1}, std::bitset<7>{64}}, + {std::bitset<256>{}.set(255), std::bitset<256>{}.set()}, + {{"a", std::bitset<9>{257}}, {"b", {}}}, + std::bitset<1>{1}, + std::bitset<64>{}.set(63), + 42}; + roundtrip(input, write, read); +} + +// Verify compatibility independently of reflect-cpp's reader/writer pair. +template +void check_native(Write write, Read read, NativeWrite native_write, + NativeRead native_read) { + using Bits = std::bitset; + std::vector patterns{Bits{}, Bits{}.set()}; + for (std::size_t i = 0; i < N; ++i) patterns.push_back(Bits{}.set(i)); + Bits alternating; + for (std::size_t i = 0; i < N; i += 2) alternating.set(i); + patterns.push_back(alternating); + patterns.push_back(~alternating); + for (const auto& bits : patterns) { + SCOPED_TRACE(bits.to_string()); + const auto expected = native_write(bits); + const auto actual = write(bits); + EXPECT_EQ(actual, expected); + const auto decoded = read.template operator()(expected); + ASSERT_TRUE(decoded) << decoded.error().what(); + EXPECT_EQ(*decoded, bits); + EXPECT_EQ(native_read.template operator()(actual), bits); + } + const auto bytes = write(Bits{}.set()); + for (std::size_t size = 0; size < bytes.size(); ++size) { + SCOPED_TRACE(size); + auto truncated = bytes; + truncated.resize(size); + EXPECT_FALSE(read.template operator()(truncated)); + } +} + +template +void check_native_sizes(Write write, Read read, NativeWrite native_write, + NativeRead native_read) { + check_native<0>(write, read, native_write, native_read); + check_native<1>(write, read, native_write, native_read); + check_native<7>(write, read, native_write, native_read); + check_native<8>(write, read, native_write, native_read); + check_native<9>(write, read, native_write, native_read); + check_native<64>(write, read, native_write, native_read); + check_native<256>(write, read, native_write, native_read); +} + +template +void check_size(Write write, Read read) { + SCOPED_TRACE(N); + using Bits = std::bitset; + std::vector patterns{Bits{}, Bits{}.set()}; + Bits alternating; + for (std::size_t i = 0; i < N; i += 2) alternating.set(i); + patterns.push_back(alternating); + patterns.push_back(~alternating); + for (std::size_t i = 0; i < N; ++i) patterns.push_back(Bits{}.set(i)); + for (const auto& bits : patterns) { + SCOPED_TRACE(bits.to_string()); + roundtrip(Record{bits}, write, read); + } +} + +template +void check_sizes(Write write, Read read) { + check_size<0>(write, read); + check_size<1>(write, read); + check_size<7>(write, read); + check_size<8>(write, read); + check_size<9>(write, read); + check_size<64>(write, read); + check_size<256>(write, read); +} + +template +void check_text(Write write, Read read) { + const Record> bits{std::bitset<9>{258}}; + const auto text = read.template operator()>(write(bits)); + ASSERT_TRUE(text) << text.error().what(); + EXPECT_EQ(text->bits, "100000010"); + for (const std::string bad : {"", "1", "1000000010", "10000000x", " 00000001", + "10000000\n", "100000001\n"}) { + EXPECT_FALSE(read.template operator()>>( + write(Record{bad}))) + << bad; + } + EXPECT_FALSE(read.template operator()>>( + write(Record{"0"}))); +} + +template +void check_packed(Write write, Read read) { + using Byte = typename Representation::value_type; + const Record> bits{std::bitset<9>{258}}; + const auto packed = + read.template operator()>(write(bits)); + ASSERT_TRUE(packed) << packed.error().what(); + const Representation expected{static_cast(0x40), + static_cast(0x80)}; + EXPECT_EQ(packed->bits, expected); + const auto restored = read.template operator()>>( + write(Record{expected})); + ASSERT_TRUE(restored) << restored.error().what(); + EXPECT_EQ(*restored, bits); + + auto bad_padding = expected; + bad_padding.back() |= static_cast(1); + EXPECT_FALSE(read.template operator()>>( + write(Record{bad_padding}))); + if constexpr (requires(Representation value) { value.resize(0); }) { + for (const auto size : {0u, 1u, 3u}) { + Representation bad(size); + EXPECT_FALSE(read.template operator()>>( + write(Record{bad}))); + } + const Record> descriptor{ + std::bitset<256>{}.set(0).set(7).set(8).set(254).set(255)}; + Representation expected_descriptor(32); + expected_descriptor[0] = static_cast(0x81); + expected_descriptor[1] = static_cast(0x80); + expected_descriptor[31] = static_cast(0x03); + const auto big = + read.template operator()>(write(descriptor)); + ASSERT_TRUE(big) << big.error().what(); + EXPECT_EQ(big->bits, expected_descriptor); + roundtrip(descriptor, write, read); + for (const auto size : {31u, 33u}) { + EXPECT_FALSE(read.template operator()>>( + write(Record{Representation(size)}))); + } + EXPECT_FALSE(read.template operator()>>( + write(Record{Representation(1)}))); + } +} + +} // namespace test_bitset_formats + +#endif diff --git a/tests/cli/test_bitset.cpp b/tests/cli/test_bitset.cpp new file mode 100644 index 000000000..6c5b12338 --- /dev/null +++ b/tests/cli/test_bitset.cpp @@ -0,0 +1,24 @@ +#include + +#include +#include + +namespace test_cli_bitset { + +struct Config { + std::bitset<9> bits; + std::bitset<0> empty; +}; + +TEST(cli, bitset) { + const char* args[] = {"program", "--bits=100000001", "--empty="}; + const auto parsed = rfl::cli::read(3, const_cast(args)); + ASSERT_TRUE(parsed) << parsed.error().what(); + EXPECT_EQ(parsed->bits, std::bitset<9>{257}); + args[1] = "--bits=10000000x"; + EXPECT_FALSE(rfl::cli::read(3, const_cast(args))); + args[1] = "--bits=10000001"; + EXPECT_FALSE(rfl::cli::read(3, const_cast(args))); +} + +} // namespace test_cli_bitset diff --git a/tests/csv/test_bitset.cpp b/tests/csv/test_bitset.cpp new file mode 100644 index 000000000..7cfdbd44b --- /dev/null +++ b/tests/csv/test_bitset.cpp @@ -0,0 +1,44 @@ +#include + +#include "../check_bitset.hpp" + +namespace test_bitset_csv { + +const auto write = [](const auto& value) { + return rfl::csv::write(std::vector{value}); +}; +const auto read = [](const auto& bytes) -> rfl::Result { + return rfl::csv::read>(bytes).and_then( + [](const auto& rows) -> rfl::Result { + if (rows.size() != 1) return rfl::error("Expected one row."); + return rows.front(); + }); +}; + +TEST(csv, bitset_sizes_and_patterns) { + test_bitset_formats::check_sizes(write, read); +} + +TEST(csv, bitset_representation_and_validation) { + test_bitset_formats::check_text(write, read); +} + +TEST(csv, bitset_optional_and_renamed_columns) { + struct Row { + rfl::Rename<"descriptor", std::bitset<9>> bits; + std::optional> optional_bits; + std::string label; + }; + const std::vector input{{std::bitset<9>{2}, std::bitset<9>{1}, "000123"}, + {std::bitset<9>{256}, std::nullopt, "000456"}}; + const auto decoded = rfl::csv::read>(rfl::csv::write(input)); + ASSERT_TRUE(decoded) << decoded.error().what(); + ASSERT_EQ(decoded->size(), input.size()); + for (std::size_t i = 0; i < input.size(); ++i) { + EXPECT_EQ((*decoded)[i].bits.value(), input[i].bits.value()); + EXPECT_EQ((*decoded)[i].optional_bits, input[i].optional_bits); + EXPECT_EQ((*decoded)[i].label, input[i].label); + } +} + +} // namespace test_bitset_csv diff --git a/tests/env/test_bitset.cpp b/tests/env/test_bitset.cpp new file mode 100644 index 000000000..5f5c73abb --- /dev/null +++ b/tests/env/test_bitset.cpp @@ -0,0 +1,26 @@ +#include +#include + +#include "write_and_read.hpp" + +namespace test_env_bitset { + +struct Config { + std::bitset<9> bits; + std::bitset<0> empty; +}; + +TEST(env, bitset) { + const test_env::ScopedEnvironment environment; + const Config input{std::bitset<9>{257}, {}}; + rfl::env::write(input); + const auto parsed = rfl::env::read(); + ASSERT_TRUE(parsed) << parsed.error().what(); + EXPECT_EQ(parsed->bits, input.bits); + test_env::portable_setenv("BITS", "10000000x"); + EXPECT_FALSE(rfl::env::read()); + test_env::portable_setenv("BITS", "10000001"); + EXPECT_FALSE(rfl::env::read()); +} + +} // namespace test_env_bitset diff --git a/tests/flexbuffers/test_bitset.cpp b/tests/flexbuffers/test_bitset.cpp new file mode 100644 index 000000000..51ecd680c --- /dev/null +++ b/tests/flexbuffers/test_bitset.cpp @@ -0,0 +1,24 @@ +#include + +#include "../check_bitset.hpp" + +namespace test_bitset_flexbuffers { + +const auto write = [](const auto& value) { return rfl::flexbuf::write(value); }; +const auto read = [](const auto& value) { + return rfl::flexbuf::read(value); +}; + +TEST(flexbuffers, bitset_sizes_and_patterns) { + test_bitset_formats::check_sizes(write, read); +} + +TEST(flexbuffers, bitset_nested) { + test_bitset_formats::check_nested(write, read); +} + +TEST(flexbuffers, bitset_representation_and_validation) { + test_bitset_formats::check_packed(write, read); +} + +} // namespace test_bitset_flexbuffers diff --git a/tests/generic/test_bitset.cpp b/tests/generic/test_bitset.cpp new file mode 100644 index 000000000..ba92dcb51 --- /dev/null +++ b/tests/generic/test_bitset.cpp @@ -0,0 +1,23 @@ +#include +#include + +#include "../check_bitset.hpp" + +namespace test_bitset_generic { + +const auto write = [](const auto& value) { return rfl::generic::write(value); }; +const auto read = [](const auto& value) { + return rfl::generic::read(value); +}; + +TEST(generic, bitset_sizes_and_patterns) { + test_bitset_formats::check_sizes(write, read); +} + +TEST(generic, bitset_nested) { test_bitset_formats::check_nested(write, read); } + +TEST(generic, bitset_representation_and_validation) { + test_bitset_formats::check_text(write, read); +} + +} // namespace test_bitset_generic diff --git a/tests/json/test_bitset.cpp b/tests/json/test_bitset.cpp new file mode 100644 index 000000000..c74477c1b --- /dev/null +++ b/tests/json/test_bitset.cpp @@ -0,0 +1,32 @@ +#include + +#include "../check_bitset.hpp" + +namespace test_bitset_json { + +const auto write = [](const auto& value) { return rfl::json::write(value); }; +const auto read = [](const auto& value) { + return rfl::json::read(value); +}; + +TEST(json, bitset_sizes_and_patterns) { + test_bitset_formats::check_sizes(write, read); +} + +TEST(json, bitset_nested) { test_bitset_formats::check_nested(write, read); } + +TEST(json, bitset_representation_and_validation) { + test_bitset_formats::check_text(write, read); +} + +TEST(json, bitset_schema) { + const auto schema = rfl::json::to_schema>(); + EXPECT_NE(schema.find(R"pattern("pattern":"^[01]{9}(?![\\s\\S])")pattern"), + std::string::npos); + EXPECT_EQ(rfl::json::write(std::bitset<9>{1}), R"("000000001")"); + EXPECT_FALSE(rfl::json::read>("null")); + EXPECT_FALSE(rfl::json::read>("1")); + EXPECT_FALSE(rfl::json::read>(R"("00000000\u0000")")); +} + +} // namespace test_bitset_json diff --git a/tests/msgpack/test_bitset.cpp b/tests/msgpack/test_bitset.cpp new file mode 100644 index 000000000..067fd3b3c --- /dev/null +++ b/tests/msgpack/test_bitset.cpp @@ -0,0 +1,22 @@ +#include + +#include "../check_bitset.hpp" + +namespace test_bitset_msgpack { + +const auto write = [](const auto& value) { return rfl::msgpack::write(value); }; +const auto read = [](const auto& value) { + return rfl::msgpack::read(value); +}; + +TEST(msgpack, bitset_sizes_and_patterns) { + test_bitset_formats::check_sizes(write, read); +} + +TEST(msgpack, bitset_nested) { test_bitset_formats::check_nested(write, read); } + +TEST(msgpack, bitset_representation_and_validation) { + test_bitset_formats::check_packed(write, read); +} + +} // namespace test_bitset_msgpack diff --git a/tests/parquet/test_bitset.cpp b/tests/parquet/test_bitset.cpp new file mode 100644 index 000000000..40b33f2bc --- /dev/null +++ b/tests/parquet/test_bitset.cpp @@ -0,0 +1,26 @@ +#include + +#include "../check_bitset.hpp" + +namespace test_bitset_parquet { + +const auto write = [](const auto& value) { + return rfl::parquet::write(std::vector{value}); +}; +const auto read = [](const auto& bytes) -> rfl::Result { + return rfl::parquet::read>(bytes).and_then( + [](const auto& rows) -> rfl::Result { + if (rows.size() != 1) return rfl::error("Expected one row."); + return rows.front(); + }); +}; + +TEST(parquet, bitset_sizes_and_patterns) { + test_bitset_formats::check_sizes(write, read); +} + +TEST(parquet, bitset_representation_and_validation) { + test_bitset_formats::check_packed(write, read); +} + +} // namespace test_bitset_parquet diff --git a/tests/toml/test_bitset.cpp b/tests/toml/test_bitset.cpp new file mode 100644 index 000000000..e00246f47 --- /dev/null +++ b/tests/toml/test_bitset.cpp @@ -0,0 +1,22 @@ +#include + +#include "../check_bitset.hpp" + +namespace test_bitset_toml { + +const auto write = [](const auto& value) { return rfl::toml::write(value); }; +const auto read = [](const auto& value) { + return rfl::toml::read(value); +}; + +TEST(toml, bitset_sizes_and_patterns) { + test_bitset_formats::check_sizes(write, read); +} + +TEST(toml, bitset_nested) { test_bitset_formats::check_nested(write, read); } + +TEST(toml, bitset_representation_and_validation) { + test_bitset_formats::check_text(write, read); +} + +} // namespace test_bitset_toml diff --git a/tests/ubjson/test_bitset.cpp b/tests/ubjson/test_bitset.cpp new file mode 100644 index 000000000..5dcc74a8b --- /dev/null +++ b/tests/ubjson/test_bitset.cpp @@ -0,0 +1,22 @@ +#include + +#include "../check_bitset.hpp" + +namespace test_bitset_ubjson { + +const auto write = [](const auto& value) { return rfl::ubjson::write(value); }; +const auto read = [](const auto& value) { + return rfl::ubjson::read(value); +}; + +TEST(ubjson, bitset_sizes_and_patterns) { + test_bitset_formats::check_sizes(write, read); +} + +TEST(ubjson, bitset_nested) { test_bitset_formats::check_nested(write, read); } + +TEST(ubjson, bitset_representation_and_validation) { + test_bitset_formats::check_packed(write, read); +} + +} // namespace test_bitset_ubjson diff --git a/tests/xml/test_bitset.cpp b/tests/xml/test_bitset.cpp new file mode 100644 index 000000000..42144e855 --- /dev/null +++ b/tests/xml/test_bitset.cpp @@ -0,0 +1,24 @@ +#include + +#include "../check_bitset.hpp" + +namespace test_bitset_xml { + +const auto write = [](const auto& value) { + return rfl::xml::write<"root">(value); +}; +const auto read = [](const auto& value) { + return rfl::xml::read(value); +}; + +TEST(xml, bitset_sizes_and_patterns) { + test_bitset_formats::check_sizes(write, read); +} + +TEST(xml, bitset_nested) { test_bitset_formats::check_nested(write, read); } + +TEST(xml, bitset_representation_and_validation) { + test_bitset_formats::check_text(write, read); +} + +} // namespace test_bitset_xml diff --git a/tests/yaml/test_bitset.cpp b/tests/yaml/test_bitset.cpp new file mode 100644 index 000000000..344aa1249 --- /dev/null +++ b/tests/yaml/test_bitset.cpp @@ -0,0 +1,22 @@ +#include + +#include "../check_bitset.hpp" + +namespace test_bitset_yaml { + +const auto write = [](const auto& value) { return rfl::yaml::write(value); }; +const auto read = [](const auto& value) { + return rfl::yaml::read(value); +}; + +TEST(yaml, bitset_sizes_and_patterns) { + test_bitset_formats::check_sizes(write, read); +} + +TEST(yaml, bitset_nested) { test_bitset_formats::check_nested(write, read); } + +TEST(yaml, bitset_representation_and_validation) { + test_bitset_formats::check_text(write, read); +} + +} // namespace test_bitset_yaml diff --git a/tests/yas/test_bitset.cpp b/tests/yas/test_bitset.cpp new file mode 100644 index 000000000..7b303b17b --- /dev/null +++ b/tests/yas/test_bitset.cpp @@ -0,0 +1,53 @@ +#include +#include +#include + +#include "../check_bitset.hpp" + +namespace test_bitset_yas { + +const auto write = [](const auto& value) { return rfl::yas::write(value); }; +const auto read = [](const auto& value) { + return rfl::yas::read(value); +}; +const auto native_write = [](const auto& value) { + yas::mem_ostream stream; + yas::binary_oarchive archive(stream); + archive & value; + const auto bytes = stream.get_intrusive_buffer(); + return std::vector(bytes.data, bytes.data + bytes.size); +}; +const auto native_read = [](const auto& bytes) { + yas::mem_istream stream(bytes.data(), bytes.size()); + yas::binary_iarchive archive(stream); + T value; + archive & value; + return value; +}; + +TEST(yas, bitset_native_interop) { + test_bitset_formats::check_native_sizes(write, read, native_write, + native_read); +} + +TEST(yas, bitset_nested) { + test_bitset_formats::check_nested(write, read); + test_bitset_formats::check_nested( + [](const auto& value) { + return rfl::yas::write(value); + }, + [](const auto& bytes) { + return rfl::yas::read(bytes); + }); + test_bitset_formats::roundtrip(std::map>{{7, {258}}}, + write, read); + test_bitset_formats::roundtrip( + std::unordered_map, int>{{{258}, 7}}, write, read); +} + +TEST(yas, bitset_invalid_size) { + EXPECT_FALSE(rfl::yas::read>(native_write(std::bitset<8>{}))); + EXPECT_FALSE(rfl::yas::read>(native_write(std::bitset<1>{}))); +} + +} // namespace test_bitset_yas From 5123c57a6a13bdf32d29b716cffa251f10e2c3c4 Mon Sep 17 00:00:00 2001 From: George Evmenov Date: Mon, 26 Oct 2026 17:30:41 +0300 Subject: [PATCH 2/4] Fix zero-length bitset environment test on Windows --- tests/env/test_bitset.cpp | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/tests/env/test_bitset.cpp b/tests/env/test_bitset.cpp index 5f5c73abb..30dedf544 100644 --- a/tests/env/test_bitset.cpp +++ b/tests/env/test_bitset.cpp @@ -1,4 +1,5 @@ #include +#include #include #include "write_and_read.hpp" @@ -7,12 +8,11 @@ namespace test_env_bitset { struct Config { std::bitset<9> bits; - std::bitset<0> empty; }; TEST(env, bitset) { const test_env::ScopedEnvironment environment; - const Config input{std::bitset<9>{257}, {}}; + const Config input{std::bitset<9>{257}}; rfl::env::write(input); const auto parsed = rfl::env::read(); ASSERT_TRUE(parsed) << parsed.error().what(); @@ -23,4 +23,27 @@ TEST(env, bitset) { EXPECT_FALSE(rfl::env::read()); } +TEST(env, bitset_zero_length) { + struct EmptyConfig { + std::bitset<0> empty; + }; + const test_env::ScopedEnvironment environment; + test_env::portable_setenv("EMPTY", "previous"); + rfl::env::write(EmptyConfig{}); + const auto parsed = rfl::env::read(); +#ifdef _WIN32 + // The Windows CRT removes environment variables assigned an empty string. + EXPECT_EQ(std::getenv("EMPTY"), nullptr); + ASSERT_FALSE(parsed); + EXPECT_NE(parsed.error().what().find("EMPTY"), std::string::npos); +#else + ASSERT_NE(std::getenv("EMPTY"), nullptr); + EXPECT_STREQ(std::getenv("EMPTY"), ""); + ASSERT_TRUE(parsed) << parsed.error().what(); + EXPECT_EQ(parsed->empty, std::bitset<0>{}); +#endif + test_env::portable_setenv("EMPTY", "0"); + EXPECT_FALSE(rfl::env::read()); +} + } // namespace test_env_bitset From a56ed55eb3c336e1f187c75f372a9d070a3b3d74 Mon Sep 17 00:00:00 2001 From: George Evmenov Date: Mon, 26 Oct 2026 18:19:25 +0300 Subject: [PATCH 3/4] Avoid GCC array-bounds warnings in bitset error messages --- include/rfl/internal/bitset.hpp | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/include/rfl/internal/bitset.hpp b/include/rfl/internal/bitset.hpp index 4e54cfc3a..2ed688361 100644 --- a/include/rfl/internal/bitset.hpp +++ b/include/rfl/internal/bitset.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "../Result.hpp" @@ -13,8 +14,12 @@ namespace rfl::internal { template Result> bitset_from_string(const std::string_view _str) { if (_str.size() != N) { - return error("Expected a bitset string of length " + std::to_string(N) + - ", got " + std::to_string(_str.size()) + "."); + std::string message = "Expected a bitset string of length "; + message += std::to_string(N); + message += ", got "; + message += std::to_string(_str.size()); + message += "."; + return error(std::move(message)); } std::bitset bits; for (std::size_t i = 0; i < N; ++i) { @@ -46,9 +51,12 @@ template Result> bitset_from_bytes(const Bytes& _bytes) { constexpr auto size = N / 8 + (N % 8 != 0); if (_bytes.size() != size) { - return error("Expected " + std::to_string(size) + - " packed bitset bytes, got " + std::to_string(_bytes.size()) + - "."); + std::string message = "Expected "; + message += std::to_string(size); + message += " packed bitset bytes, got "; + message += std::to_string(_bytes.size()); + message += "."; + return error(std::move(message)); } if constexpr (N % 8 != 0) { constexpr auto mask = (1u << (8 - N % 8)) - 1; From 13d00c3adfa4c1ca921199380263ff30b32fae40 Mon Sep 17 00:00:00 2001 From: George Evmenov Date: Mon, 26 Oct 2026 18:19:25 +0300 Subject: [PATCH 4/4] Fix standalone YAS parser header include order --- include/rfl/yas/Parser.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/include/rfl/yas/Parser.hpp b/include/rfl/yas/Parser.hpp index b153d6772..c966f2b21 100644 --- a/include/rfl/yas/Parser.hpp +++ b/include/rfl/yas/Parser.hpp @@ -8,7 +8,6 @@ #include #include #include -#include #include "../Generic.hpp" #include "../NamedTuple.hpp"