From 27bad050ae51b7bc93cd2f71a58af3aa70d4dcd3 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 2 Oct 2026 00:36:33 +0200 Subject: [PATCH 1/4] Expose sequence length decoding in C++ Deserializer Signed-off-by: Michael Ilyin --- include/zenoh/api/ext/serialization.hxx | 21 +++++++--- tests/universal/serialization.cxx | 55 +++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/include/zenoh/api/ext/serialization.hxx b/include/zenoh/api/ext/serialization.hxx index 2075364a..6c3345ae 100644 --- a/include/zenoh/api/ext/serialization.hxx +++ b/include/zenoh/api/ext/serialization.hxx @@ -87,6 +87,20 @@ class Deserializer : public Copyable<::ze_deserializer_t> { template T deserialize(zenoh::ZResult* err = nullptr); + /// @brief Read a sequence length without allocating or deserializing its elements. + /// The elements can then be read individually with deserialize(), after checking an application-defined limit. + /// This reads only the length prefix and does not check that all elements are present. + /// @param err if not null, the result code will be written to this location, otherwise ZException exception + /// will be thrown in case of error (or the process will abort if exceptions are disabled). + /// @return The sequence length, or zero on failure when err is provided. Check err to distinguish failure + /// from an empty sequence. + size_t deserialize_sequence_length(zenoh::ZResult* err = nullptr) { + size_t length = 0; + auto result = ::ze_deserializer_deserialize_sequence_length(interop::as_copyable_c_ptr(*this), &length); + __ZENOH_RESULT_CHECK(result, err, "Failed to read sequence length"); + return result == Z_OK ? length : 0; + } + /// @brief Checks if deserializer has parsed all the data. /// @return `true` if there is no more data to parse, `false` otherwise. bool is_done() const { return ::ze_deserializer_is_done(&this->_0); } @@ -301,11 +315,8 @@ bool __zenoh_deserialize_with_deserializer(zenoh::ext::Deserializer& deserialize deserialize_with_deserializer(deserializer, value.second, err); } -#define _ZENOH_DESERIALIZE_SEQUENCE_BEGIN \ - size_t len; \ - __ZENOH_RESULT_CHECK( \ - ::ze_deserializer_deserialize_sequence_length(zenoh::interop::as_copyable_c_ptr(deserializer), &len), err, \ - "Deserialization failure:: Failed to read sequence length"); \ +#define _ZENOH_DESERIALIZE_SEQUENCE_BEGIN \ + size_t len = deserializer.deserialize_sequence_length(err); \ if (err != nullptr && *err != Z_OK) return false; #define _ZENOH_DESERIALIZE_SEQUENCE_END return (err == nullptr || *err == Z_OK); diff --git a/tests/universal/serialization.cxx b/tests/universal/serialization.cxx index 8df7ff2e..6db1eb78 100644 --- a/tests/universal/serialization.cxx +++ b/tests/universal/serialization.cxx @@ -69,6 +69,59 @@ void serialize_container() { assert(zenoh_test_serialization(m2)); } +void deserialize_sequence_length_test() { + const std::vector values = {"first", "second"}; + auto bytes = ext::serialize(std::make_tuple(values, uint16_t(500))); + ext::Deserializer deserializer(bytes); + ZResult err = Z_EINVAL; + assert(deserializer.deserialize_sequence_length(&err) == values.size()); + assert(err == Z_OK); + assert(!deserializer.is_done()); + for (const auto& value : values) { + assert(deserializer.deserialize() == value); + } + // Reading the sequence length must leave both its elements and the following value available. + assert(deserializer.deserialize() == 500); + assert(deserializer.is_done()); + + auto empty = ext::serialize(std::vector{}); + ext::Deserializer empty_deserializer(empty); + assert(empty_deserializer.deserialize_sequence_length(&err) == 0); + assert(err == Z_OK); + assert(empty_deserializer.is_done()); + + // A length-only prefix advertising 1,048,576 elements can be read and rejected + // against a limit before allocating a container or attempting to read any elements. + Bytes length_only(std::vector{0x80, 0x80, 0x40}); + ext::Deserializer bounded_deserializer(length_only); + const auto length = bounded_deserializer.deserialize_sequence_length(); + assert(length == 1048576); + assert(length > 16); + assert(bounded_deserializer.is_done()); +} + +void deserialize_sequence_length_error_test() { + // Missing and truncated length prefixes fail in both error-reporting modes. + for (const auto& wire : {std::vector{}, std::vector{0x81}}) { + Bytes bytes(wire); + ext::Deserializer deserializer(bytes); + ZResult err = Z_OK; + assert(deserializer.deserialize_sequence_length(&err) == 0); + assert(err != Z_OK); +#ifdef __cpp_exceptions + ext::Deserializer throwing_deserializer(bytes); + bool caught = false; + try { + throwing_deserializer.deserialize_sequence_length(); + } catch (const ZException& exception) { + caught = true; + assert(exception.e == err); + } + assert(caught); +#endif + } +} + struct CustomStruct { std::vector vd; int32_t i; @@ -124,6 +177,8 @@ int main(int argc, char** argv) { serialize_primitive(); serialize_tuple(); serialize_container(); + deserialize_sequence_length_test(); + deserialize_sequence_length_error_test(); serialize_custom(); binary_format_test(); } From 28ab6a7e97a53284547e5e497a0628f7bd6e7df9 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 2 Oct 2026 01:24:59 +0200 Subject: [PATCH 2/4] Remove redundant sequence length assertion Signed-off-by: Michael Ilyin --- tests/universal/serialization.cxx | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/universal/serialization.cxx b/tests/universal/serialization.cxx index 6db1eb78..2b524897 100644 --- a/tests/universal/serialization.cxx +++ b/tests/universal/serialization.cxx @@ -96,7 +96,6 @@ void deserialize_sequence_length_test() { ext::Deserializer bounded_deserializer(length_only); const auto length = bounded_deserializer.deserialize_sequence_length(); assert(length == 1048576); - assert(length > 16); assert(bounded_deserializer.is_done()); } From f95c5da418a116808d2e1d776ff0374c83d2767a Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 2 Oct 2026 01:27:55 +0200 Subject: [PATCH 3/4] Preserve the sequence length deserialization error message Signed-off-by: Michael Ilyin --- include/zenoh/api/ext/serialization.hxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/zenoh/api/ext/serialization.hxx b/include/zenoh/api/ext/serialization.hxx index 6c3345ae..5bafc64c 100644 --- a/include/zenoh/api/ext/serialization.hxx +++ b/include/zenoh/api/ext/serialization.hxx @@ -97,7 +97,7 @@ class Deserializer : public Copyable<::ze_deserializer_t> { size_t deserialize_sequence_length(zenoh::ZResult* err = nullptr) { size_t length = 0; auto result = ::ze_deserializer_deserialize_sequence_length(interop::as_copyable_c_ptr(*this), &length); - __ZENOH_RESULT_CHECK(result, err, "Failed to read sequence length"); + __ZENOH_RESULT_CHECK(result, err, "Deserialization failure:: Failed to read sequence length"); return result == Z_OK ? length : 0; } From f8b244b639d8620baa244a7e1d88404d1b23e348 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 22 Oct 2026 12:46:20 +0200 Subject: [PATCH 4/4] Expose sequence length encoding in C++ Serializer --- include/zenoh/api/ext/serialization.hxx | 13 +++++++++-- tests/universal/serialization.cxx | 29 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/include/zenoh/api/ext/serialization.hxx b/include/zenoh/api/ext/serialization.hxx index 5bafc64c..4a99677f 100644 --- a/include/zenoh/api/ext/serialization.hxx +++ b/include/zenoh/api/ext/serialization.hxx @@ -57,6 +57,16 @@ class Serializer : public Owned<::ze_owned_serializer_t> { template void serialize(const T& value, ZResult* err = nullptr); + /// @brief Write a sequence length without serializing its elements. + /// The elements can then be written individually with serialize(). The caller must write exactly length elements. + /// @param length number of elements in the sequence. + /// @param err if not null, the result code will be written to this location, otherwise ZException exception + /// will be thrown in case of error (or the process will abort if exceptions are disabled). + void serialize_sequence_length(size_t length, ZResult* err = nullptr) { + __ZENOH_RESULT_CHECK(::ze_serializer_serialize_sequence_length(interop::as_loaned_c_ptr(*this), length), err, + "Failed to serialize sequence length"); + } + /// @brief Finalize serialization and return the underlying `Bytes` object. /// @return underlying `Bytes` object. Bytes finish() && { @@ -197,8 +207,7 @@ bool __zenoh_serialize_with_serializer(zenoh::ext::Serializer& serializer, const template bool __serialize_sequence_with_serializer(zenoh::ext::Serializer& serializer, It begin, It end, size_t n, ZResult* err) { - __ZENOH_RESULT_CHECK(::ze_serializer_serialize_sequence_length(zenoh::interop::as_loaned_c_ptr(serializer), n), err, - "Failed to serialize sequence length"); + serializer.serialize_sequence_length(n, err); if (err != nullptr && *err != Z_OK) { return false; } diff --git a/tests/universal/serialization.cxx b/tests/universal/serialization.cxx index 2b524897..e41808d2 100644 --- a/tests/universal/serialization.cxx +++ b/tests/universal/serialization.cxx @@ -69,6 +69,34 @@ void serialize_container() { assert(zenoh_test_serialization(m2)); } +void serialize_sequence_length_test() { + const std::vector values = {"first", "second"}; + for (bool report_error : {false, true}) { + ext::Serializer serializer; + ZResult err = Z_EINVAL; + serializer.serialize_sequence_length(values.size(), report_error ? &err : nullptr); + if (report_error) assert(err == Z_OK); + for (const auto& value : values) serializer.serialize(value); + serializer.serialize(uint16_t(500)); + auto bytes = std::move(serializer).finish(); + assert(bytes.as_vector() == ext::serialize(std::make_tuple(values, uint16_t(500))).as_vector()); + ext::Deserializer deserializer(bytes); + assert(deserializer.deserialize>() == values); + assert(deserializer.deserialize() == 500); + assert(deserializer.is_done()); + } + + ext::Serializer empty_serializer; + empty_serializer.serialize_sequence_length(0); + assert(std::move(empty_serializer).finish().as_vector() == ext::serialize(std::vector{}).as_vector()); + + // Writing a length alone must not add element data. + ext::Serializer length_only_serializer; + length_only_serializer.serialize_sequence_length(1048576); + const std::vector expected_prefix = {0x80, 0x80, 0x40}; + assert(std::move(length_only_serializer).finish().as_vector() == expected_prefix); +} + void deserialize_sequence_length_test() { const std::vector values = {"first", "second"}; auto bytes = ext::serialize(std::make_tuple(values, uint16_t(500))); @@ -176,6 +204,7 @@ int main(int argc, char** argv) { serialize_primitive(); serialize_tuple(); serialize_container(); + serialize_sequence_length_test(); deserialize_sequence_length_test(); deserialize_sequence_length_error_test(); serialize_custom();