From f97b2688e431913169bb4f1de1bed0b1383a3698 Mon Sep 17 00:00:00 2001 From: Yura Sorokin Date: Fri, 12 Dec 2025 01:31:34 +0100 Subject: [PATCH] PS-10245 feature: Implement receiving binlog events in GTID mode (part 1) https://perconadev.atlassian.net/browse/PS-10245 Implemented encoding functions for GTID sets ('binsrv::gtids::gtid_set') that can be used to prepare GTID set payloads passed to the 'gtid_set_arg' when calling 'mysql_binlog_open()'. Similarly to "util/byte_span_extractors.hpp" added new "util/byte_span_inserters.hpp" header that includes helper functions for encoding various primitives (fixed signed / unsigned integers, varlen signed / unsigned integers, packed signed / unsigned integers, byte arrays and spans). Added new 'byte_span_encoding_test' unit test that checks varlen conversions for both signed and unsigned integers of different sizes. 'gtid_set_test' unit test extended with encoding-decoding (roundtrip) checks for untagged, tagged, and mixed GTID sets. --- CMakeLists.txt | 2 + src/binsrv/event/gtid_log_body.cpp | 2 +- .../event/gtid_tagged_log_body_impl.cpp | 2 +- src/binsrv/gtids/gtid_set.cpp | 125 +++++++- src/binsrv/gtids/gtid_set.hpp | 4 + src/binsrv/gtids/tag.cpp | 23 ++ src/binsrv/gtids/tag.hpp | 7 + src/binsrv/gtids/uuid.cpp | 14 + src/binsrv/gtids/uuid.hpp | 7 + src/binsrv/s3_storage_backend.cpp | 2 +- src/util/bounded_string_storage.hpp | 2 +- src/util/byte_span_extractors.hpp | 104 +++---- src/util/byte_span_inserters.hpp | 276 ++++++++++++++++++ src/util/byte_span_packed_int_constants.hpp | 39 +++ src/util/command_line_helpers.cpp | 4 +- src/util/command_line_helpers.hpp | 4 +- src/util/nv_tuple_from_json.hpp | 2 +- src/util/nv_tuple_to_json.hpp | 2 +- src/util/semantic_version.cpp | 8 +- tests/CMakeLists.txt | 22 ++ tests/byte_span_encoding_test.cpp | 194 ++++++++++++ tests/gtid_set_test.cpp | 122 ++++++++ 22 files changed, 890 insertions(+), 77 deletions(-) create mode 100644 src/util/byte_span_inserters.hpp create mode 100644 src/util/byte_span_packed_int_constants.hpp create mode 100644 tests/byte_span_encoding_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3b04288..a90c0fb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -298,6 +298,8 @@ set(source_files src/util/byte_span_fwd.hpp src/util/byte_span.hpp src/util/byte_span_extractors.hpp + src/util/byte_span_inserters.hpp + src/util/byte_span_packed_int_constants.hpp src/util/command_line_helpers_fwd.hpp src/util/command_line_helpers.hpp diff --git a/src/binsrv/event/gtid_log_body.cpp b/src/binsrv/event/gtid_log_body.cpp index 3ff3ba9..44dc89f 100644 --- a/src/binsrv/event/gtid_log_body.cpp +++ b/src/binsrv/event/gtid_log_body.cpp @@ -97,7 +97,7 @@ gtid_log_body::gtid_log_body(util::const_byte_span portion) { util::extract_fixed_int_from_byte_span(remainder, commit_group_ticket_); } } - if (std::size(remainder) != 0U) { + if (!remainder.empty()) { util::exception_location().raise( "extra bytes in the gtid_log event body"); } diff --git a/src/binsrv/event/gtid_tagged_log_body_impl.cpp b/src/binsrv/event/gtid_tagged_log_body_impl.cpp index 004c6b1..f719629 100644 --- a/src/binsrv/event/gtid_tagged_log_body_impl.cpp +++ b/src/binsrv/event/gtid_tagged_log_body_impl.cpp @@ -134,7 +134,7 @@ generic_body_impl::generic_body_impl( last_seen_field_id = field_id; } - if (std::size(remainder) != 0U) { + if (!remainder.empty()) { util::exception_location().raise( "extra bytes in the gtid_log event body"); } diff --git a/src/binsrv/gtids/gtid_set.cpp b/src/binsrv/gtids/gtid_set.cpp index 3b25a22..224318a 100644 --- a/src/binsrv/gtids/gtid_set.cpp +++ b/src/binsrv/gtids/gtid_set.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include "binsrv/gtids/common_types.hpp" @@ -35,6 +36,7 @@ #include "util/byte_span_extractors.hpp" #include "util/byte_span_fwd.hpp" +#include "util/byte_span_inserters.hpp" #include "util/exception_location_helpers.hpp" namespace binsrv::gtids { @@ -73,7 +75,7 @@ gtid_set::gtid_set(util::const_byte_span portion) { // as a : // '0' for untagged GTIDs, // '1' for tagged GTIDs. - // MySQL developers also decided to duplicate this also in + // MySQL developers also decided to duplicate this in // the very first byte (byte 0). // To sum up, the extraction rules are the following: // - if the highest byte is equal to '1', extract bytes 1..6 and put them @@ -90,11 +92,10 @@ gtid_set::gtid_set(util::const_byte_span portion) { // tagged and untagget encodings) const auto header_parser{[](std::uint64_t value) { static constexpr std::size_t format_bit_width{8U}; - static constexpr std::size_t format_bit_position{ - std::numeric_limits::digits - format_bit_width}; - const auto gtid_format_field{ - static_cast(value >> format_bit_position)}; + const auto gtid_format_field{static_cast( + value >> + (std::numeric_limits::digits - format_bit_width))}; // if gtid_format_field is anything but 0 or 1 if ((gtid_format_field >> 1U) != 0U) { util::exception_location().raise( @@ -148,6 +149,118 @@ gtid_set::gtid_set(util::const_byte_span portion) { process_intervals(remainder, current_uuid, current_tag); } + + if (!remainder.empty()) { + util::exception_location().raise( + "extra bytes in the encoded gtid_set"); + } +} + +[[nodiscard]] bool gtid_set::contains_tags() const noexcept { + for (const auto &[current_uuid, current_tagged_gnos] : data_) { + for (const auto &[current_tag, current_gnos] : current_tagged_gnos) { + if (!current_tag.is_empty()) { + return true; + } + } + } + return false; +} + +[[nodiscard]] std::size_t gtid_set::calculate_encoded_size() const noexcept { + const auto tagged_flag{contains_tags()}; + // 8 bytes for the header (for both tahgged and untagged versions) + std::size_t result{sizeof(std::uint64_t)}; + for (const auto &[current_uuid, current_tagged_gnos] : data_) { + for (const auto &[current_tag, current_gnos] : current_tagged_gnos) { + // 16 bytes for UUID + result += uuid::calculate_encoded_size(); + if (tagged_flag) { + result += current_tag.calculate_encoded_size(); + } + // 8 bytes for the number of intervals + result += sizeof(std::uint64_t); + // 16 bytes for each interval + result += + boost::icl::interval_count(current_gnos) * 2U * sizeof(std::uint64_t); + } + } + return result; +} + +void gtid_set::encode_to(util::byte_span &destination) const { + const auto tagged_flag{contains_tags()}; + + util::byte_span remainder{destination}; + // skipping 8 bytes for the encoded header (number of tsids + tagged flag) + remainder = remainder.subspan(sizeof(std::uint64_t)); + + // a helper lambda to form encoded GTID set header (supports both + // tagged and untagget encodings) + const auto header_encoder{[](bool tagged, std::size_t number_of_tsids) { + static constexpr std::size_t format_bit_width{8U}; + if (!tagged) { + // ensuring that the value is less then 2^56 + if (number_of_tsids >= + (1ULL << (std::numeric_limits::digits - + format_bit_width))) { + util::exception_location().raise( + "the number of TSIDs in the untagged GTID set being encoded is too " + "large"); + } + return std::uint64_t{number_of_tsids}; + } + + // ensuring that the value is less then 2^48 + if (number_of_tsids >= + (1ULL << (std::numeric_limits::digits - + 2U * format_bit_width))) { + util::exception_location().raise( + "the number of TSIDs in the tagged GTID set being encoded is too " + "large"); + } + // shifting 1 to 48 bits + std::uint64_t result{1ULL << (std::numeric_limits::digits - + 2U * format_bit_width)}; + result |= std::uint64_t{number_of_tsids}; + result <<= format_bit_width; + result |= 1ULL; + return result; + }}; + + std::size_t number_of_tsids{0ULL}; + for (const auto &[current_uuid, current_tagged_gnos] : data_) { + for (const auto &[current_tag, current_gnos] : current_tagged_gnos) { + // 16 bytes for UUID + current_uuid.encode_to(remainder); + if (tagged_flag) { + // varlen bytes for tag size + // 1 byte for each character in the tag + current_tag.encode_to(remainder); + } + // 8 bytes for the number of intervals + util::insert_fixed_int_to_byte_span( + remainder, std::uint64_t{boost::icl::interval_count(current_gnos)}); + for (const auto &interval : current_gnos) { + // 16 bytes for each interval + util::insert_fixed_int_to_byte_span( + remainder, std::uint64_t{boost::icl::lower(interval)}); + // here we need to uncrement upper bound as we have a half-open interval + // in the encoded representation and use closed interval in + // boost::icl::interval_set + util::insert_fixed_int_to_byte_span( + remainder, std::uint64_t{boost::icl::upper(interval) + 1ULL}); + } + ++number_of_tsids; + } + } + + // writing header + util::byte_span header{destination}; + util::insert_fixed_int_to_byte_span( + header, header_encoder(tagged_flag, number_of_tsids)); + + destination = remainder; } [[nodiscard]] bool gtid_set::contains(const gtid &value) const noexcept { @@ -256,7 +369,7 @@ void gtid_set::process_intervals(util::const_byte_span &remainder, // TODO: validate that interval boundary values are increasing between // iterations - // here we need to decrement upper bound as we have a halp-open interval + // here we need to decrement upper bound as we have a half-open interval // in the encoded representation and use closed interval in the // gtid_set::add_interval() method --current_interval_upper; diff --git a/src/binsrv/gtids/gtid_set.hpp b/src/binsrv/gtids/gtid_set.hpp index d8217c2..cf48635 100644 --- a/src/binsrv/gtids/gtid_set.hpp +++ b/src/binsrv/gtids/gtid_set.hpp @@ -53,6 +53,10 @@ class gtid_set { ~gtid_set(); [[nodiscard]] bool is_empty() const noexcept { return data_.empty(); } + [[nodiscard]] bool contains_tags() const noexcept; + + [[nodiscard]] std::size_t calculate_encoded_size() const noexcept; + void encode_to(util::byte_span &destination) const; [[nodiscard]] bool contains(const gtid &value) const noexcept; diff --git a/src/binsrv/gtids/tag.cpp b/src/binsrv/gtids/tag.cpp index 12d2912..b6a1fbf 100644 --- a/src/binsrv/gtids/tag.cpp +++ b/src/binsrv/gtids/tag.cpp @@ -24,6 +24,8 @@ #include "binsrv/gtids/common_types.hpp" +#include "util/byte_span_fwd.hpp" +#include "util/byte_span_inserters.hpp" #include "util/exception_location_helpers.hpp" namespace binsrv::gtids { @@ -63,6 +65,27 @@ tag::tag(std::string_view name) { } } +[[nodiscard]] std::size_t tag::calculate_encoded_size() const noexcept { + // varlen bytes for tag size + // 1 byte for each character in the tag + const auto tag_size{get_size()}; + return util::calculate_varlen_int_size(tag_size) + tag_size; +} + +void tag::encode_to(util::byte_span &destination) const { + util::byte_span remainder{destination}; + if (!util::insert_varlen_int_to_byte_span_checked(remainder, get_size())) { + util::exception_location().raise( + "cannot encode tag length"); + } + if (!util::insert_byte_span_to_byte_span_checked( + remainder, util::const_byte_span{data_})) { + util::exception_location().raise( + "cannot encode tag data"); + } + destination = remainder; +} + std::ostream &operator<<(std::ostream &output, const tag &obj) { return output << obj.get_name(); } diff --git a/src/binsrv/gtids/tag.hpp b/src/binsrv/gtids/tag.hpp index d7c8fa5..d720c35 100644 --- a/src/binsrv/gtids/tag.hpp +++ b/src/binsrv/gtids/tag.hpp @@ -41,6 +41,13 @@ class tag { [[nodiscard]] bool is_empty() const noexcept { return data_.empty(); } + [[nodiscard]] std::size_t get_size() const noexcept { + return std::size(data_); + } + + [[nodiscard]] std::size_t calculate_encoded_size() const noexcept; + void encode_to(util::byte_span &destination) const; + private: tag_storage data_{}; }; diff --git a/src/binsrv/gtids/uuid.cpp b/src/binsrv/gtids/uuid.cpp index 11e3a78..91034c1 100644 --- a/src/binsrv/gtids/uuid.cpp +++ b/src/binsrv/gtids/uuid.cpp @@ -16,6 +16,7 @@ #include "binsrv/gtids/uuid.hpp" #include +#include #include #include #include @@ -29,6 +30,8 @@ #include "binsrv/gtids/common_types.hpp" +#include "util/byte_span_fwd.hpp" +#include "util/byte_span_inserters.hpp" #include "util/exception_location_helpers.hpp" namespace binsrv::gtids { @@ -56,6 +59,17 @@ uuid::uuid(const uuid_storage &data) { return boost::uuids::to_string(data_); } +void uuid::encode_to(util::byte_span &destination) const { + const util::const_byte_span data_span{ + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + reinterpret_cast(std::begin(data_)), + boost::uuids::uuid::static_size()}; + if (!util::insert_byte_span_to_byte_span_checked(destination, data_span)) { + util::exception_location().raise( + "cannot encode uuid"); + } +} + std::ostream &operator<<(std::ostream &output, const uuid &obj) { return output << obj.str(); } diff --git a/src/binsrv/gtids/uuid.hpp b/src/binsrv/gtids/uuid.hpp index 9c7c532..e61c8ee 100644 --- a/src/binsrv/gtids/uuid.hpp +++ b/src/binsrv/gtids/uuid.hpp @@ -25,6 +25,8 @@ #include "binsrv/gtids/common_types.hpp" +#include "util/byte_span_fwd.hpp" + namespace binsrv::gtids { class uuid { @@ -39,6 +41,11 @@ class uuid { [[nodiscard]] std::string str() const; + [[nodiscard]] static std::size_t calculate_encoded_size() noexcept { + return uuid_length; + } + void encode_to(util::byte_span &destination) const; + [[nodiscard]] friend auto operator<=>(const uuid &first, const uuid &second) noexcept = default; diff --git a/src/binsrv/s3_storage_backend.cpp b/src/binsrv/s3_storage_backend.cpp index c1e70b7..b8d63ac 100644 --- a/src/binsrv/s3_storage_backend.cpp +++ b/src/binsrv/s3_storage_backend.cpp @@ -408,7 +408,7 @@ s3_storage_backend::aws_context::list_objects( util::exception_location().raise( "encountered an object with unexpected prefix"); } - key.remove_prefix(prefix_str.size()); + key.remove_prefix(std::size(prefix_str)); } if (!key.empty()) { result.emplace(key, model_object.GetSize()); diff --git a/src/util/bounded_string_storage.hpp b/src/util/bounded_string_storage.hpp index 9e41f96..74895de 100644 --- a/src/util/bounded_string_storage.hpp +++ b/src/util/bounded_string_storage.hpp @@ -29,7 +29,7 @@ namespace util { template [[nodiscard]] std::string_view to_string_view(const bounded_string_storage &storage) noexcept { - return util::as_string_view(storage); + return as_string_view(storage); } template diff --git a/src/util/byte_span_extractors.hpp b/src/util/byte_span_extractors.hpp index 4c5c15f..69f9ec8 100644 --- a/src/util/byte_span_extractors.hpp +++ b/src/util/byte_span_extractors.hpp @@ -27,13 +27,14 @@ #include #include "util/byte_span_fwd.hpp" +#include "util/byte_span_packed_int_constants.hpp" namespace util { template requires std::unsigned_integral || std::same_as void extract_fixed_int_from_byte_span( - util::const_byte_span &remainder, T &value, + const_byte_span &remainder, T &value, std::size_t bytes_to_extract = sizeof(T)) noexcept { assert(bytes_to_extract != 0U); assert(bytes_to_extract <= sizeof(T)); @@ -57,7 +58,7 @@ void extract_fixed_int_from_byte_span( template requires std::unsigned_integral || std::same_as [[nodiscard]] bool extract_fixed_int_from_byte_span_checked( - util::const_byte_span &remainder, T &value, + const_byte_span &remainder, T &value, std::size_t bytes_to_extract = sizeof(T)) noexcept { if (bytes_to_extract > std::size(remainder)) { return false; @@ -69,7 +70,7 @@ template template requires std::signed_integral || std::same_as void extract_fixed_int_from_byte_span( - util::const_byte_span &remainder, T &value, + const_byte_span &remainder, T &value, [[maybe_unused]] std::size_t bytes_to_extract = sizeof(T)) noexcept { // signed version of the fixed int extractor currently does not support // partial byte extraction (when bytes_to_extract != sizeof(T)) because @@ -85,7 +86,7 @@ void extract_fixed_int_from_byte_span( template requires std::signed_integral || std::same_as [[nodiscard]] bool extract_fixed_int_from_byte_span_checked( - util::const_byte_span &remainder, T &value, + const_byte_span &remainder, T &value, std::size_t bytes_to_extract = sizeof(T)) noexcept { if (bytes_to_extract != sizeof(T) || bytes_to_extract > std::size(remainder)) { @@ -97,21 +98,21 @@ template template requires((std::integral && sizeof(T) == 1U) || std::same_as) -void extract_byte_span_from_byte_span(util::const_byte_span &remainder, +void extract_byte_span_from_byte_span(const_byte_span &remainder, std::span storage_span) noexcept { - assert(std::size(remainder) >= storage_span.size()); + assert(std::size(remainder) >= std::size(storage_span)); std::memcpy(std::data(storage_span), std::data(remainder), - storage_span.size()); + std::size(storage_span)); - remainder = remainder.subspan(storage_span.size()); + remainder = remainder.subspan(std::size(storage_span)); } template requires((std::integral && sizeof(T) == 1U) || std::same_as) [[nodiscard]] bool -extract_byte_span_from_byte_span_checked(util::const_byte_span &remainder, +extract_byte_span_from_byte_span_checked(const_byte_span &remainder, std::span storage_span) noexcept { - if (storage_span.size() > std::size(remainder)) { + if (std::size(storage_span) > std::size(remainder)) { return false; } extract_byte_span_from_byte_span(remainder, storage_span); @@ -120,7 +121,7 @@ extract_byte_span_from_byte_span_checked(util::const_byte_span &remainder, template requires((std::integral && sizeof(T) == 1U) || std::same_as) -void extract_byte_array_from_byte_span(util::const_byte_span &remainder, +void extract_byte_array_from_byte_span(const_byte_span &remainder, std::array &storage) noexcept { assert(std::size(remainder) >= N); std::memcpy(std::data(storage), std::data(remainder), N); @@ -131,7 +132,7 @@ void extract_byte_array_from_byte_span(util::const_byte_span &remainder, template requires((std::integral && sizeof(T) == 1U) || std::same_as) [[nodiscard]] bool -extract_byte_array_from_byte_span_checked(util::const_byte_span &remainder, +extract_byte_array_from_byte_span_checked(const_byte_span &remainder, std::array &storage) noexcept { if (N > std::size(remainder)) { return false; @@ -143,45 +144,35 @@ extract_byte_array_from_byte_span_checked(util::const_byte_span &remainder, template requires(std::unsigned_integral && sizeof(T) == sizeof(std::uint64_t)) [[nodiscard]] bool -extract_packed_int_from_byte_span_checked(util::const_byte_span &remainder, +extract_packed_int_from_byte_span_checked(const_byte_span &remainder, T &value) noexcept { // https://github.com/mysql/mysql-server/blob/mysql-8.0.43/mysys/pack.cc#L90 // https://github.com/mysql/mysql-server/blob/mysql-8.4.6/mysys/pack.cc#L90 - util::const_byte_span local_remainder{remainder}; + const_byte_span local_remainder{remainder}; std::uint8_t first_byte{}; - if (!util::extract_fixed_int_from_byte_span_checked(local_remainder, - first_byte)) { + if (!extract_fixed_int_from_byte_span_checked(local_remainder, first_byte)) { return false; } - static constexpr unsigned char max_marker{251U}; - static constexpr unsigned char double_marker{252U}; - static constexpr unsigned char triple_marker{253U}; - static constexpr unsigned char octuple_marker{254U}; - static constexpr unsigned char forbidden_marker{255U}; - - static constexpr std::size_t double_size{2U}; - static constexpr std::size_t triple_size{3U}; - static constexpr std::size_t octuple_size{8U}; std::uint64_t unpacked{}; bool result{true}; switch (first_byte) { - case max_marker: + case packed_int_max_marker: unpacked = std::numeric_limits::max(); break; - case double_marker: - result = util::extract_fixed_int_from_byte_span_checked( - local_remainder, unpacked, double_size); + case packed_int_double_marker: + result = extract_fixed_int_from_byte_span_checked(local_remainder, unpacked, + packed_int_double_size); break; - case triple_marker: - result = util::extract_fixed_int_from_byte_span_checked( - local_remainder, unpacked, triple_size); + case packed_int_triple_marker: + result = extract_fixed_int_from_byte_span_checked(local_remainder, unpacked, + packed_int_triple_size); break; - case octuple_marker: - result = util::extract_fixed_int_from_byte_span_checked( - local_remainder, unpacked, octuple_size); + case packed_int_octuple_marker: + result = extract_fixed_int_from_byte_span_checked(local_remainder, unpacked, + packed_int_octuple_size); break; - [[unlikely]] case forbidden_marker: + [[unlikely]] case packed_int_forbidden_marker: result = false; break; [[likely]] default: @@ -197,13 +188,12 @@ extract_packed_int_from_byte_span_checked(util::const_byte_span &remainder, template requires(std::unsigned_integral) [[nodiscard]] bool -extract_varlen_int_from_byte_span_checked(util::const_byte_span &remainder, +extract_varlen_int_from_byte_span_checked(const_byte_span &remainder, T &value) noexcept { // https://github.com/mysql/mysql-server/blob/mysql-8.4.6/libs/mysql/serialization/variable_length_integers.h#L141 - util::const_byte_span local_remainder{remainder}; + const_byte_span local_remainder{remainder}; std::uint8_t first_byte{}; - if (!util::extract_fixed_int_from_byte_span_checked(local_remainder, - first_byte)) { + if (!extract_fixed_int_from_byte_span_checked(local_remainder, first_byte)) { return false; } const std::size_t num_bytes{ @@ -211,24 +201,24 @@ extract_varlen_int_from_byte_span_checked(util::const_byte_span &remainder, if (num_bytes > std::size(remainder)) { return false; } - T data_cpy{static_cast(first_byte >> num_bytes)}; + T value_cpy{static_cast(first_byte >> num_bytes)}; if (num_bytes == 1U) { - value = data_cpy; + value = value_cpy; remainder = local_remainder; return true; } - std::uint64_t data_tmp{0ULL}; - extract_fixed_int_from_byte_span(local_remainder, data_tmp, num_bytes - 1U); - // If num_bytes <= 8, shift left by 8 - num_bytes. - // If num_bytes == 9, shift left by 8 - 9 + 1 = 0. - if (num_bytes < sizeof(data_tmp)) { - data_tmp <<= (sizeof(data_tmp) - num_bytes); + std::uint64_t value_tmp{0ULL}; + extract_fixed_int_from_byte_span(local_remainder, value_tmp, num_bytes - 1U); + // If num_bytes = 8, shift left by 8 - num_bytes. + // If num_bytes == 8 or num_bytes == 9, no shift is needed. + if (num_bytes < sizeof(value_tmp)) { + value_tmp <<= (sizeof(value_tmp) - num_bytes); } - if (data_tmp > std::numeric_limits::max()) { + if (value_tmp > std::numeric_limits::max()) { return false; } - data_cpy |= static_cast(data_tmp); - value = data_cpy; + value_cpy |= static_cast(value_tmp); + value = value_cpy; remainder = local_remainder; return true; } @@ -236,18 +226,18 @@ extract_varlen_int_from_byte_span_checked(util::const_byte_span &remainder, template requires(std::signed_integral) [[nodiscard]] bool -extract_varlen_int_from_byte_span_checked(util::const_byte_span &remainder, +extract_varlen_int_from_byte_span_checked(const_byte_span &remainder, T &value) noexcept { using unsigned_type = std::make_unsigned_t; unsigned_type data_tmp{}; - const auto result{ - extract_varlen_int_from_byte_span_checked(remainder, data_tmp)}; - if (!result) { + if (!extract_varlen_int_from_byte_span_checked(remainder, data_tmp)) { return false; } // 0 if positive, ~0 if negative - const unsigned_type sign_mask{ - (data_tmp & 1U) != 0U ? std::numeric_limits::max() : 0U}; + const unsigned_type sign_mask{(data_tmp & static_cast(1U)) != + unsigned_type{} + ? std::numeric_limits::max() + : unsigned_type{}}; // the result if it is nonnegative, or -(result + 1) if it is negative. data_tmp >>= 1U; // the result diff --git a/src/util/byte_span_inserters.hpp b/src/util/byte_span_inserters.hpp new file mode 100644 index 0000000..370bcea --- /dev/null +++ b/src/util/byte_span_inserters.hpp @@ -0,0 +1,276 @@ +// Copyright (c) 2023-2024 Percona and/or its affiliates. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0, +// as published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +#ifndef UTIL_BYTE_SPAN_INSERTERS_HPP +#define UTIL_BYTE_SPAN_INSERTERS_HPP + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "util/byte_span_fwd.hpp" +#include "util/byte_span_packed_int_constants.hpp" + +namespace util { + +template + requires std::unsigned_integral || std::same_as +void insert_fixed_int_to_byte_span( + byte_span &remainder, T value, + std::size_t bytes_to_insert = sizeof(T)) noexcept { + assert(bytes_to_insert != 0U); + assert(bytes_to_insert <= sizeof(T)); + assert(std::size(remainder) >= bytes_to_insert); + if constexpr (sizeof(T) != 1U) { + // A fixed-length unsigned integer stores its value in a series of + // bytes with the least significant byte first. + // TODO: in c++23 use std::byteswap() + T value_in_network_format{boost::endian::native_to_little(value)}; + std::memcpy(std::data(remainder), &value_in_network_format, + bytes_to_insert); + } else { + using underlying_type = std::underlying_type_t; + *std::data(remainder) = + static_cast(static_cast(value)); + } + remainder = remainder.subspan(bytes_to_insert); +} + +template + requires std::unsigned_integral || std::same_as +[[nodiscard]] bool insert_fixed_int_to_byte_span_checked( + byte_span &remainder, T value, + std::size_t bytes_to_insert = sizeof(T)) noexcept { + if (bytes_to_insert > std::size(remainder)) { + return false; + } + insert_fixed_int_to_byte_span(remainder, value, bytes_to_insert); + return true; +} + +template + requires std::signed_integral || std::same_as +void insert_fixed_int_to_byte_span( + byte_span &remainder, T value, + [[maybe_unused]] std::size_t bytes_to_insert = sizeof(T)) noexcept { + // signed version of the fixed int inserter currently does not support + // partial byte insertion (when bytes_to_insert != sizeof(T)) because + // it uses unsigned version underneath + assert(bytes_to_insert == sizeof(T)); + assert(std::size(remainder) >= bytes_to_insert); + using unsigned_type = std::make_unsigned_t; + const auto unsigned_value{static_cast(value)}; + insert_fixed_int_to_byte_span(remainder, unsigned_value); +} + +template + requires std::signed_integral || std::same_as +[[nodiscard]] bool insert_fixed_int_to_byte_span_checked( + byte_span &remainder, T value, + std::size_t bytes_to_insert = sizeof(T)) noexcept { + if (bytes_to_insert != sizeof(T) || bytes_to_insert > std::size(remainder)) { + return false; + } + insert_fixed_int_to_byte_span(remainder, value, bytes_to_insert); + return true; +} + +template + requires((std::integral && sizeof(T) == 1U) || std::same_as) +void insert_byte_span_to_byte_span(byte_span &remainder, + std::span storage_span) noexcept { + assert(std::size(remainder) >= std::size(storage_span)); + std::memcpy(std::data(remainder), std::data(storage_span), + std::size(storage_span)); + + remainder = remainder.subspan(std::size(storage_span)); +} + +template + requires((std::integral && sizeof(T) == 1U) || std::same_as) +[[nodiscard]] bool insert_byte_span_to_byte_span_checked( + byte_span &remainder, std::span storage_span) noexcept { + if (std::size(storage_span) > std::size(remainder)) { + return false; + } + insert_byte_span_to_byte_span(remainder, storage_span); + return true; +} + +template + requires((std::integral && sizeof(T) == 1U) || std::same_as) +void insert_byte_array_to_byte_span(byte_span &remainder, + const std::array &storage) noexcept { + assert(std::size(remainder) >= N); + std::memcpy(std::data(remainder), std::data(storage), N); + + remainder = remainder.subspan(N); +} + +template + requires((std::integral && sizeof(T) == 1U) || std::same_as) +[[nodiscard]] bool insert_byte_array_to_byte_span_checked( + byte_span &remainder, const std::array &storage) noexcept { + if (N > std::size(remainder)) { + return false; + } + insert_byte_array_to_byte_span(remainder, storage); + return true; +} + +template + requires(std::unsigned_integral && sizeof(T) == sizeof(std::uint64_t)) +[[nodiscard]] std::size_t calculate_packed_int_size(T value) noexcept { + // https://github.com/mysql/mysql-server/blob/mysql-8.0.43/mysys/pack.cc#L161 + // https://github.com/mysql/mysql-server/blob/mysql-8.4.6/mysys/pack.cc#L161 + if (value < packed_int_single_boundary) { + return 1U; + } + if (value < packed_int_double_boundary) { + return packed_int_double_size + 1U; + } + if (value < packed_int_triple_boundary) { + return packed_int_triple_size + 1U; + } + return packed_int_octuple_size + 1U; +} + +template + requires(std::unsigned_integral && sizeof(T) == sizeof(std::uint64_t)) +[[nodiscard]] bool insert_packed_int_to_byte_span_checked(byte_span &remainder, + T value) noexcept { + // https://github.com/mysql/mysql-server/blob/mysql-8.0.43/mysys/pack.cc#L129 + // https://github.com/mysql/mysql-server/blob/mysql-8.4.6/mysys/pack.cc#L129 + + if (value < packed_int_single_boundary) { + if (std::size(remainder) < 1U) { + return false; + } + insert_fixed_int_to_byte_span(remainder, value, 1); + } + /* 251 is reserved for NULL */ + if (value < packed_int_double_boundary) { + if (std::size(remainder) < 1U + packed_int_double_size) { + return false; + } + insert_fixed_int_to_byte_span(remainder, packed_int_double_marker); + insert_fixed_int_to_byte_span(remainder, value, packed_int_double_size); + } + if (value < packed_int_triple_boundary) { + if (std::size(remainder) < 1U + packed_int_triple_size) { + return false; + } + insert_fixed_int_to_byte_span(remainder, packed_int_triple_marker); + insert_fixed_int_to_byte_span(remainder, value, packed_int_triple_size); + } else { + if (std::size(remainder) < 1U + packed_int_octuple_size) { + return false; + } + insert_fixed_int_to_byte_span(remainder, packed_int_octuple_marker); + insert_fixed_int_to_byte_span(remainder, value, packed_int_octuple_size); + } + + return true; +} + +namespace detail { + +template + requires(std::signed_integral) +[[nodiscard]] auto get_signed_representation_as_unsigned(T value) noexcept { + using unsigned_type = std::make_unsigned_t; + // sign_mask is 0 if data >= 0 and ~0 if data < 0 + const auto sign_mask{value < static_cast(0) + ? std::numeric_limits::max() + : static_cast(0U)}; + auto result{static_cast(value)}; + result ^= sign_mask; + // insert sign bit as the least significant bit + result <<= 1U; + result |= + static_cast(sign_mask & static_cast(1U)); + return result; +} + +} // namespace detail + +template + requires(std::unsigned_integral) +[[nodiscard]] std::size_t calculate_varlen_int_size(T value) noexcept { + // https://github.com/mysql/mysql-server/blob/mysql-8.4.6/libs/mysql/serialization/variable_length_integers.h#L49 + static constexpr std::size_t magic_factor{575U}; + static constexpr std::size_t magic_shift{12U}; + + const auto bits_in_value{static_cast(std::bit_width(value))}; + return ((bits_in_value * magic_factor) >> magic_shift) + 1U; +} + +template + requires(std::signed_integral) +[[nodiscard]] std::size_t calculate_varlen_int_size(T value) noexcept { + return calculate_varlen_int_size( + detail::get_signed_representation_as_unsigned(value)); +} + +template + requires(std::unsigned_integral) +[[nodiscard]] bool insert_varlen_int_to_byte_span_checked(byte_span &remainder, + T value) noexcept { + // https://github.com/mysql/mysql-server/blob/mysql-8.4.6/libs/mysql/serialization/variable_length_integers.h#L89 + const auto num_bytes{calculate_varlen_int_size(value)}; + + if (std::size(remainder) < num_bytes) { + return false; + } + + std::uint64_t value_cpy{value}; + // creating a mask with " - 1" ones in lower digits, + // for instance '0000 0011' for == 3 or + // '0000 0000' for == 1 + const std::uint8_t first_byte{static_cast( + ((1ULL << (num_bytes - 1U)) - 1ULL) | (value_cpy << num_bytes))}; + insert_fixed_int_to_byte_span(remainder, first_byte); + + if (num_bytes == 1U) { + return true; + } + + // If num_bytes < 8, shift right by 8 - num_bytes. + // If num_bytes == 8 or num_bytes == 9, no shift is needed. + if (num_bytes < sizeof(value_cpy)) { + value_cpy >>= (sizeof(value_cpy) - num_bytes); + } + insert_fixed_int_to_byte_span(remainder, value_cpy, num_bytes - 1U); + + return true; +} + +template + requires(std::signed_integral) +[[nodiscard]] bool insert_varlen_int_to_byte_span_checked(byte_span &remainder, + T value) noexcept { + return insert_varlen_int_to_byte_span_checked( + remainder, detail::get_signed_representation_as_unsigned(value)); +} + +} // namespace util + +#endif // UTIL_BYTE_SPAN_INSERTERS_HPP diff --git a/src/util/byte_span_packed_int_constants.hpp b/src/util/byte_span_packed_int_constants.hpp new file mode 100644 index 0000000..e770fc9 --- /dev/null +++ b/src/util/byte_span_packed_int_constants.hpp @@ -0,0 +1,39 @@ +// Copyright (c) 2023-2024 Percona and/or its affiliates. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0, +// as published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +#ifndef UTIL_BYTE_SPAN_PACKED_INT_CONSTANTS_HPP +#define UTIL_BYTE_SPAN_PACKED_INT_CONSTANTS_HPP + +#include + +namespace util { + +inline constexpr std::uint64_t packed_int_single_boundary{251ULL}; +inline constexpr std::uint64_t packed_int_double_boundary{1ULL << 16U}; +inline constexpr std::uint64_t packed_int_triple_boundary{1ULL << 24U}; + +inline constexpr unsigned char packed_int_max_marker{251U}; +inline constexpr unsigned char packed_int_double_marker{252U}; +inline constexpr unsigned char packed_int_triple_marker{253U}; +inline constexpr unsigned char packed_int_octuple_marker{254U}; +inline constexpr unsigned char packed_int_forbidden_marker{255U}; + +inline constexpr std::size_t packed_int_double_size{2U}; +inline constexpr std::size_t packed_int_triple_size{3U}; +inline constexpr std::size_t packed_int_octuple_size{8U}; + +} // namespace util + +#endif // UTIL_BYTE_SPAN_PACKED_INT_CONSTANTS_HPP diff --git a/src/util/command_line_helpers.cpp b/src/util/command_line_helpers.cpp index 21cd17c..0860502 100644 --- a/src/util/command_line_helpers.cpp +++ b/src/util/command_line_helpers.cpp @@ -20,7 +20,7 @@ namespace util { -std::string extract_executable_name(util::command_line_arg_view cmd_args) { +std::string extract_executable_name(command_line_arg_view cmd_args) { std::string res; if (!cmd_args.empty()) { const std::filesystem::path executable_path{cmd_args[0U]}; @@ -34,7 +34,7 @@ std::string extract_executable_name(util::command_line_arg_view cmd_args) { } std::string -get_readable_command_line_arguments(util::command_line_arg_view cmd_args) { +get_readable_command_line_arguments(command_line_arg_view cmd_args) { std::string res; if (cmd_args.empty()) { return res; diff --git a/src/util/command_line_helpers.hpp b/src/util/command_line_helpers.hpp index c9df676..ec948ed 100644 --- a/src/util/command_line_helpers.hpp +++ b/src/util/command_line_helpers.hpp @@ -31,10 +31,10 @@ inline command_line_arg_view to_command_line_agg_view( } [[nodiscard]] std::string -extract_executable_name(util::command_line_arg_view cmd_args); +extract_executable_name(command_line_arg_view cmd_args); [[nodiscard]] std::string -get_readable_command_line_arguments(util::command_line_arg_view cmd_args); +get_readable_command_line_arguments(command_line_arg_view cmd_args); } // namespace util diff --git a/src/util/nv_tuple_from_json.hpp b/src/util/nv_tuple_from_json.hpp index bfe05d5..dc67f32 100644 --- a/src/util/nv_tuple_from_json.hpp +++ b/src/util/nv_tuple_from_json.hpp @@ -127,7 +127,7 @@ void nv_tuple_from_json(const boost::json::value &json_value, obj = boost::json::value_to>(json_value, extraction_ctx); } catch (const std::exception &ex) { - util::exception_location().raise( + exception_location().raise( "unable to extract \"" + extraction_ctx.to_string() + "\""); } } diff --git a/src/util/nv_tuple_to_json.hpp b/src/util/nv_tuple_to_json.hpp index aee7680..9a33dcb 100644 --- a/src/util/nv_tuple_to_json.hpp +++ b/src/util/nv_tuple_to_json.hpp @@ -68,7 +68,7 @@ void nv_tuple_to_json(boost::json::value &json_value, const detail::insertion_context insertion_ctx{}; json_value = boost::json::value_from(obj, insertion_ctx); } catch (const std::exception &ex) { - util::exception_location().raise( + exception_location().raise( "unable to insert into JSON object"); } } diff --git a/src/util/semantic_version.cpp b/src/util/semantic_version.cpp index bab2041..929ede9 100644 --- a/src/util/semantic_version.cpp +++ b/src/util/semantic_version.cpp @@ -31,7 +31,7 @@ semantic_version::semantic_version(std::string_view version_string) : major_{}, minor_{}, patch_{} { const auto dash_pos{version_string.find(extra_delimiter)}; if (dash_pos != std::string::npos) { - version_string.remove_suffix(version_string.size() - dash_pos); + version_string.remove_suffix(std::size(version_string) - dash_pos); } auto split_result{ std::ranges::views::split(version_string, component_delimiter)}; @@ -41,7 +41,7 @@ semantic_version::semantic_version(std::string_view version_string) const auto component_extractor{ [version_en](auto &cuttent_it, const std::string &name) -> std::uint8_t { if (cuttent_it == version_en) { - util::exception_location().raise( + exception_location().raise( "server_version does not have the \"" + name + "\" component"); } std::uint8_t result{}; @@ -50,7 +50,7 @@ semantic_version::semantic_version(std::string_view version_string) const auto [conversion_ptr, conversion_ec]{ std::from_chars(component_it, component_en, result)}; if (conversion_ec != std::errc{} || conversion_ptr != component_en) { - util::exception_location().raise( + exception_location().raise( "server_version \"" + name + "\" component is not a numeric value"); } @@ -61,7 +61,7 @@ semantic_version::semantic_version(std::string_view version_string) minor_ = component_extractor(version_it, "minor"); patch_ = component_extractor(version_it, "patch"); if (version_it != version_en) { - util::exception_location().raise( + exception_location().raise( "server_version has more than 3 components"); } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0da76fc..b9ac7ba 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,6 +15,14 @@ find_package(Boost REQUIRED COMPONENTS unit_test_framework) +set(byte_span_encoding_source_files + ${PROJECT_SOURCE_DIR}/src/util/byte_span_fwd.hpp + ${PROJECT_SOURCE_DIR}/src/util/byte_span.hpp + ${PROJECT_SOURCE_DIR}/src/util/byte_span_extractors.hpp + ${PROJECT_SOURCE_DIR}/src/util/byte_span_inserters.hpp + ${PROJECT_SOURCE_DIR}/src/util/byte_span_packed_int_constants.hpp +) + set(uuid_source_files ${PROJECT_SOURCE_DIR}/src/binsrv/gtids/uuid_fwd.hpp ${PROJECT_SOURCE_DIR}/src/binsrv/gtids/uuid.hpp @@ -39,6 +47,19 @@ set(gtid_set_source_files ${PROJECT_SOURCE_DIR}/src/binsrv/gtids/gtid_set.cpp ) +add_executable(byte_span_encoding_test byte_span_encoding_test.cpp ${byte_span_encoding_source_files}) +target_include_directories(byte_span_encoding_test PRIVATE "${PROJECT_SOURCE_DIR}/src") +target_link_libraries(byte_span_encoding_test + PRIVATE + binlog_server_compiler_flags + Boost::unit_test_framework +) +set_target_properties(byte_span_encoding_test PROPERTIES + CXX_STANDARD_REQUIRED YES + CXX_EXTENSIONS NO +) + + add_executable(uuid_test uuid_test.cpp ${uuid_source_files}) target_include_directories(uuid_test PRIVATE "${PROJECT_SOURCE_DIR}/src") target_link_libraries(uuid_test @@ -87,6 +108,7 @@ set_target_properties(gtid_set_test PROPERTIES CXX_EXTENSIONS NO ) +add_test(NAME byte_span_encoding_test COMMAND byte_span_encoding_test) add_test(NAME uuid_test COMMAND uuid_test) add_test(NAME tag_test COMMAND tag_test) add_test(NAME gtid_test COMMAND gtid_test) diff --git a/tests/byte_span_encoding_test.cpp b/tests/byte_span_encoding_test.cpp new file mode 100644 index 0000000..7ecd5ec --- /dev/null +++ b/tests/byte_span_encoding_test.cpp @@ -0,0 +1,194 @@ +// Copyright (c) 2023-2024 Percona and/or its affiliates. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0, +// as published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define BOOST_TEST_MODULE TagTests +// this include is needed as it provides the 'main()' function +// NOLINTNEXTLINE(misc-include-cleaner) +#include + +#include +#include + +#include + +#include + +#include "util/byte_span_extractors.hpp" +#include "util/byte_span_fwd.hpp" +#include "util/byte_span_inserters.hpp" + +struct mixin_fixture { + static constexpr std::size_t bits_in_byte{ + std::numeric_limits::digits}; + // varlen int uses 1 byte for each 7 bits + static constexpr std::size_t bit_group_size{bits_in_byte - 1U}; + + // the number of 7-bit groups in the given type + template + static constexpr std::size_t number_of_iterations = + sizeof(ValueType) * bits_in_byte / bit_group_size; + + // an auxiliary method that performs varlen int encoding / decoding of the + // provided value and compares the values + template static void check_roundtrip(ValueType value) { + const auto calculated_encoded_size{util::calculate_varlen_int_size(value)}; + using encoding_buffer_type = + std::array; + + encoding_buffer_type buffer; + util::byte_span encoding_portion{buffer}; + BOOST_CHECK( + util::insert_varlen_int_to_byte_span_checked(encoding_portion, value)); + + const auto actual_encoded_size{std::size(buffer) - + std::size(encoding_portion)}; + + BOOST_CHECK_EQUAL(actual_encoded_size, calculated_encoded_size); + + util::const_byte_span decoding_portion{buffer}; + ValueType restored_value; + BOOST_CHECK(util::extract_varlen_int_from_byte_span_checked( + decoding_portion, restored_value)); + + const auto actual_decoded_size{std::size(buffer) - + std::size(decoding_portion)}; + BOOST_CHECK_EQUAL(actual_decoded_size, calculated_encoded_size); + + BOOST_CHECK_EQUAL(restored_value, value); + } + + template + static std::pair + calculate_group_min_max_for_iteration(std::size_t iteration) { + ValueType group_min; + ValueType group_max; + if constexpr (std::is_unsigned_v) { + // at the very first iteration the min value is always 0 + group_min = static_cast(1ULL << (bit_group_size * iteration)); + if (iteration == number_of_iterations) { + // for the last iteration the max value is the max value for the given + // type - 1 + group_max = std::numeric_limits::max(); + } else { + group_max = + static_cast(1ULL << (bit_group_size * (iteration + 1U))); + } + --group_max; + } else { + // at the very first iteration the min value is always 0 + if (iteration == 0U) { + group_min = static_cast(1LL); + } else { + group_min = + static_cast(1ULL << (bit_group_size * iteration - 1U)); + } + + if (iteration == number_of_iterations) { + group_max = std::numeric_limits::max(); + } else { + group_max = static_cast( + 1ULL << (bit_group_size * (iteration + 1U) - 1U)); + } + --group_max; + } + return {group_min, group_max}; + } + + template + using stream_type_t = std::conditional_t, + std::int64_t, std::uint64_t>; + + template + static void print_and_check_single(ValueType value) { + BOOST_TEST_MESSAGE(" Value " + << static_cast>(value)); + check_roundtrip(value); + } + + template static void print_and_check(ValueType value) { + print_and_check_single(value); + if constexpr (std::is_signed_v) { + const auto negated_value{static_cast(-value)}; + print_and_check_single(negated_value); + } + } +}; + +using varlen_int_encoding_types = + boost::mpl::list; + +BOOST_FIXTURE_TEST_CASE_TEMPLATE(VarlenIntEncoding, ValueType, + varlen_int_encoding_types, mixin_fixture) { + using stream_type = stream_type_t; + + const ValueType delta{16ULL}; + + // each iteration represents a new 7-bit gtoup that adds one more byte to the + // encoding deliberately adding one more iteration for the last (shorter than + // 7-bit) group + for (std::size_t iteration{0U}; iteration <= number_of_iterations; + ++iteration) { + // calculating min and max values of the group + auto [group_min, group_max]{ + calculate_group_min_max_for_iteration(iteration)}; + // calculating the midpoint + const ValueType group_midpoint{std::midpoint(group_min, group_max)}; + BOOST_TEST_MESSAGE("Iteration " + << iteration << ", min " + << static_cast(group_min) << ", midpoint " + << static_cast(group_midpoint) << ", max " + << static_cast(group_max)); + + // checking values in the [group_min; group_min + delta] range + for (ValueType current_value{group_min}; + current_value <= static_cast(group_min + delta); + ++current_value) { + print_and_check(current_value); + } + + // checking values in the [group_midpoint - delta; group_midpoint + delta] + // range + for (ValueType current_value{ + static_cast(group_midpoint - delta)}; + current_value <= static_cast(group_midpoint + delta); + ++current_value) { + print_and_check(current_value); + } + + // checking values in the [group_max - delta; group_max] range + for (ValueType current_value{static_cast(group_max - delta)}; + current_value <= group_max; ++current_value) { + print_and_check(current_value); + } + } + // special values are checked separately here + BOOST_TEST_MESSAGE("Special iteration "); + print_and_check_single(ValueType{}); + print_and_check(std::numeric_limits::max()); + if constexpr (std::is_signed_v) { + print_and_check_single(std::numeric_limits::min()); + } +} diff --git a/tests/gtid_set_test.cpp b/tests/gtid_set_test.cpp index 5237110..1f098d1 100644 --- a/tests/gtid_set_test.cpp +++ b/tests/gtid_set_test.cpp @@ -20,6 +20,9 @@ #include +// needed for binsrv::gtids::gtid_set_storage +#include // IWYU pragma: keep + #define BOOST_TEST_MODULE GtidSetTests // this include is needed as it provides the 'main()' function // NOLINTNEXTLINE(misc-include-cleaner) @@ -29,10 +32,13 @@ #include +#include "binsrv/gtids/common_types.hpp" #include "binsrv/gtids/gtid.hpp" #include "binsrv/gtids/gtid_set.hpp" #include "binsrv/gtids/tag.hpp" +#include "util/byte_span_fwd.hpp" + static constexpr std::string_view first_uuid_sv{ "11111111-1111-1111-1111-111111111111"}; static constexpr std::string_view second_uuid_sv{ @@ -220,6 +226,122 @@ BOOST_AUTO_TEST_CASE(GtidSetClear) { BOOST_CHECK(gtids.is_empty()); } +BOOST_AUTO_TEST_CASE(GtidSetContainsTags) { + const binsrv::gtids::uuid first_uuid{first_uuid_sv}; + + const binsrv::gtids::tag first_tag{"alpha"}; + + binsrv::gtids::gtid_set gtids{}; + BOOST_CHECK(!gtids.contains_tags()); + + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) + gtids += binsrv::gtids::gtid{first_uuid, 1ULL}; + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) + BOOST_CHECK(!gtids.contains_tags()); + + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) + gtids += binsrv::gtids::gtid{first_uuid, first_tag, 1ULL}; + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) + BOOST_CHECK(gtids.contains_tags()); +} + +class gtid_set_encoding_fixture { +protected: + static void check_encoding_roundtrip(const binsrv::gtids::gtid_set >ids) { + const auto encoded_size{gtids.calculate_encoded_size()}; + binsrv::gtids::gtid_set_storage buffer(encoded_size); + util::byte_span destination{buffer}; + BOOST_CHECK_NO_THROW(gtids.encode_to(destination)); + BOOST_CHECK(destination.empty()); + + binsrv::gtids::gtid_set restored; + const util::const_byte_span source{buffer}; + BOOST_CHECK_NO_THROW(restored = binsrv::gtids::gtid_set{source}); + + BOOST_CHECK_EQUAL(gtids, restored); + } +}; + +BOOST_FIXTURE_TEST_CASE(GtidSetEncodingEmpty, gtid_set_encoding_fixture) { + const binsrv::gtids::gtid_set gtids{}; + check_encoding_roundtrip(gtids); +} + +BOOST_FIXTURE_TEST_CASE(GtidSetEncodingUntagged, gtid_set_encoding_fixture) { + const binsrv::gtids::uuid first_uuid{first_uuid_sv}; + const binsrv::gtids::uuid second_uuid{second_uuid_sv}; + + binsrv::gtids::gtid_set gtids{}; + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) + gtids += binsrv::gtids::gtid{first_uuid, 1ULL}; + gtids += binsrv::gtids::gtid{first_uuid, 2ULL}; + gtids += binsrv::gtids::gtid{first_uuid, 3ULL}; + gtids += binsrv::gtids::gtid{first_uuid, 5ULL}; + + gtids += binsrv::gtids::gtid{second_uuid, 12ULL}; + gtids += binsrv::gtids::gtid{second_uuid, 13ULL}; + gtids += binsrv::gtids::gtid{second_uuid, 15ULL}; + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) + + BOOST_CHECK(!gtids.contains_tags()); + check_encoding_roundtrip(gtids); +} + +BOOST_FIXTURE_TEST_CASE(GtidSetEncodingTagged, gtid_set_encoding_fixture) { + const binsrv::gtids::uuid first_uuid{first_uuid_sv}; + const binsrv::gtids::uuid second_uuid{second_uuid_sv}; + + const binsrv::gtids::tag first_tag{"alpha"}; + const binsrv::gtids::tag second_tag{"beta"}; + + binsrv::gtids::gtid_set gtids{}; + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) + gtids += binsrv::gtids::gtid{first_uuid, first_tag, 1ULL}; + gtids += binsrv::gtids::gtid{first_uuid, first_tag, 2ULL}; + gtids += binsrv::gtids::gtid{first_uuid, first_tag, 3ULL}; + gtids += binsrv::gtids::gtid{first_uuid, first_tag, 5ULL}; + + gtids += binsrv::gtids::gtid{second_uuid, second_tag, 12ULL}; + gtids += binsrv::gtids::gtid{second_uuid, second_tag, 13ULL}; + gtids += binsrv::gtids::gtid{second_uuid, second_tag, 15ULL}; + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) + + BOOST_CHECK(gtids.contains_tags()); + check_encoding_roundtrip(gtids); +} + +BOOST_FIXTURE_TEST_CASE(GtidSetEncodingMixed, gtid_set_encoding_fixture) { + const binsrv::gtids::uuid first_uuid{first_uuid_sv}; + const binsrv::gtids::uuid second_uuid{second_uuid_sv}; + + const binsrv::gtids::tag first_tag{"alpha"}; + const binsrv::gtids::tag second_tag{"beta"}; + + binsrv::gtids::gtid_set gtids{}; + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) + gtids += binsrv::gtids::gtid{first_uuid, 101ULL}; + gtids += binsrv::gtids::gtid{first_uuid, 102ULL}; + gtids += binsrv::gtids::gtid{first_uuid, 103ULL}; + gtids += binsrv::gtids::gtid{first_uuid, 105ULL}; + + gtids += binsrv::gtids::gtid{first_uuid, first_tag, 1ULL}; + gtids += binsrv::gtids::gtid{first_uuid, first_tag, 2ULL}; + gtids += binsrv::gtids::gtid{first_uuid, first_tag, 3ULL}; + gtids += binsrv::gtids::gtid{first_uuid, first_tag, 5ULL}; + + gtids += binsrv::gtids::gtid{second_uuid, 112ULL}; + gtids += binsrv::gtids::gtid{second_uuid, 113ULL}; + gtids += binsrv::gtids::gtid{second_uuid, 115ULL}; + + gtids += binsrv::gtids::gtid{second_uuid, second_tag, 12ULL}; + gtids += binsrv::gtids::gtid{second_uuid, second_tag, 13ULL}; + gtids += binsrv::gtids::gtid{second_uuid, second_tag, 15ULL}; + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) + + BOOST_CHECK(gtids.contains_tags()); + check_encoding_roundtrip(gtids); +} + BOOST_AUTO_TEST_CASE(GtidSetOstreamOperatorUntagged) { const binsrv::gtids::uuid first_uuid{first_uuid_sv}; const binsrv::gtids::uuid second_uuid{second_uuid_sv};