diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c570e5..22eff89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ env: jobs: MacOS: - runs-on: macos-13 + runs-on: macos-latest steps: - uses: actions/checkout@v2 - uses: actions/cache@v2 @@ -33,9 +33,9 @@ jobs: fail-fast: false matrix: options: - - name: "Linux: gcc-12" - run: ./scripts/build.sh -DCMAKE_CXX_COMPILER=g++-12 - - name: "Linux: clang-14" + - name: "Linux: gcc" + run: ./scripts/build.sh -DCMAKE_CXX_COMPILER=g++ + - name: "Linux: clang" run: ./scripts/build.sh -DCMAKE_CXX_COMPILER=clang++ name: "${{ matrix.options.name }}" runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index a0c3471..241838c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,3 @@ **/build-*/ **/cmake-build-*/ Testing - -# Auto generated files -/include/scale/definitions.hpp \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index a7c70a1..92561c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,7 @@ cmake_minimum_required(VERSION 3.12) option(JAM_COMPATIBLE "Build compatible with JAM-codec" OFF) option(CUSTOM_CONFIG_SUPPORT "Support custom config of streams" OFF) +set(MAX_AGGREGATE_FIELDS 20 CACHE STRING "Max number of aggregates fields (1..1000); for generation") option(BUILD_TESTS "Whether to include the test suite in build" OFF) @@ -32,11 +33,7 @@ if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.27") endif () if (PACKAGE_MANAGER STREQUAL "hunter") - include(cmake/HunterGate.cmake) - HunterGate( - URL https://github.com/qdrvm/hunter/archive/refs/tags/v0.25.3-qdrvm28.tar.gz - SHA1 a4f1b0f42464e07790b7f90b783a822d71be6c6d - ) + include("cmake/Hunter/init.cmake") endif () if(BUILD_TESTS) @@ -56,7 +53,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) if (PACKAGE_MANAGER STREQUAL "hunter") hunter_add_package(Boost) find_package(Boost) -else() +else() find_package(Boost CONFIG REQUIRED COMPONENTS endian multiprecision) endif () @@ -65,22 +62,9 @@ if (PACKAGE_MANAGER STREQUAL "hunter") endif () find_package(qtils CONFIG REQUIRED) -set(DEFINITION_PATH "${CMAKE_CURRENT_SOURCE_DIR}/include/scale/definitions.hpp") -if (NOT EXISTS "${CMAKE_BINARY_DIR}/definition.d" OR NOT EXISTS "${DEFINITION_PATH}") - get_filename_component(LABEL "${CMAKE_BINARY_DIR}" NAME) - file(WRITE "${DEFINITION_PATH}.${LABEL}" - "// This header was generated by cmake (${LABEL})\n" - "// IMPORTANT: Dont modify this file manually!\n") - if (JAM_COMPATIBLE) - file(APPEND "${DEFINITION_PATH}.${LABEL}" "#define JAM_COMPATIBILITY_ENABLED\n") - endif () - if (CUSTOM_CONFIG_SUPPORT) - file(APPEND "${DEFINITION_PATH}.${LABEL}" "#define CUSTOM_CONFIG_ENABLED\n") - endif () - file(RENAME "${DEFINITION_PATH}.${LABEL}" "${DEFINITION_PATH}") - message(STATUS "include/scale/definitions.hpp has generated") - file(WRITE "${CMAKE_BINARY_DIR}/definition.d" "include/scale/definitions.hpp has generated") -endif () +SET(JAM_COMPATIBILITY_ENABLED "${JAM_COMPATIBLE}") +set(CUSTOM_CONFIG_ENABLED "${CUSTOM_CONFIG_SUPPORT}") +configure_file("${CMAKE_SOURCE_DIR}/include/scale/definitions.hpp.in" "${CMAKE_BINARY_DIR}/include/scale/definitions.hpp") add_subdirectory(src) @@ -107,6 +91,11 @@ install(TARGETS scale EXPORT scaleConfig install( DIRECTORY ${CMAKE_SOURCE_DIR}/include/scale DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + FILES_MATCHING PATTERN "*.hpp" +) +install( + DIRECTORY ${CMAKE_BINARY_DIR}/include/scale + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ) include(CMakePackageConfigHelpers) diff --git a/README.md b/README.md index 1fb63c7..80406dc 100644 --- a/README.md +++ b/README.md @@ -114,13 +114,13 @@ try { ## Convenience functions Convenience functions ```c++ -template +template outcome::result> encode(T &&t); -template +template outcome::result decode(const RangeOfBytes auto& span) -template +template outcome::result decode(ScaleDecoderStream &s) ``` are wrappers over ```<<``` and ```>>``` operators described above. diff --git a/cmake/HunterGate.cmake b/cmake/Hunter/HunterGate.cmake similarity index 100% rename from cmake/HunterGate.cmake rename to cmake/Hunter/HunterGate.cmake diff --git a/cmake/Hunter/config.cmake b/cmake/Hunter/config.cmake new file mode 100644 index 0000000..6f2ec40 --- /dev/null +++ b/cmake/Hunter/config.cmake @@ -0,0 +1,24 @@ +# Template for a custom hunter configuration Useful when there is a need for a +# non-default version or arguments of a dependency, or when a project not +# registered in soramitsu-hunter should be added. +# +# hunter_config( +# package-name +# VERSION 0.0.0-package-version +# CMAKE_ARGS +# CMAKE_VARIABLE=value +# ) +# +# hunter_config( +# package-name +# URL https://github.com/organization/repository/archive/hash.tar.gz +# SHA1 1234567890abcdef1234567890abcdef12345678 +# CMAKE_ARGS +# CMAKE_VARIABLE=value +# ) + +hunter_config( + qtils + URL https://github.com/qdrvm/qtils/archive/16e7c819dd50af2f64e2d319b918d0d815332266.tar.gz + SHA1 71989938b5c8b7650eaf1a8195c2b52c5a8c250b +) diff --git a/cmake/Hunter/hunter-gate-url.cmake b/cmake/Hunter/hunter-gate-url.cmake new file mode 100644 index 0000000..d9e33b9 --- /dev/null +++ b/cmake/Hunter/hunter-gate-url.cmake @@ -0,0 +1,5 @@ +HunterGate( + URL https://github.com/qdrvm/hunter/archive/refs/tags/v0.25.3-qdrvm29.tar.gz + SHA1 025920fa980ba81a150deaa534a0248dde25fd54 + LOCAL +) \ No newline at end of file diff --git a/cmake/Hunter/init.cmake b/cmake/Hunter/init.cmake new file mode 100644 index 0000000..22361f6 --- /dev/null +++ b/cmake/Hunter/init.cmake @@ -0,0 +1,42 @@ +# specify GITHUB_HUNTER_TOKEN and GITHUB_HUNTER_USERNAME to automatically upload binary cache to github.com/qdrvm/hunter-binary-cache +# https://hunter.readthedocs.io/en/latest/user-guides/hunter-user/github-cache-server.html +string(COMPARE EQUAL "$ENV{GITHUB_HUNTER_USERNAME}" "" username_is_empty) +string(COMPARE EQUAL "$ENV{GITHUB_HUNTER_TOKEN}" "" password_is_empty) + +# binary cache can be uploaded to qdrvm/hunter-binary-cache so others will not build same dependencies twice +if (NOT password_is_empty AND NOT username_is_empty) + option(HUNTER_RUN_UPLOAD "Upload cache binaries" YES) + message("Binary cache uploading is ENABLED.") +else () + option(HUNTER_RUN_UPLOAD "Upload cache binaries" NO) + message(AUTHOR_WARNING " Binary cache uploading is DISABLED. + Define environment variables GITHUB_HUNTER_USERNAME and GITHUB_HUNTER_TOKEN + for binary cache activation. To generate github token follow the instructions: + https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens + Make sure `read:packages` and `write:packages` permissions are granted (step 7 in instructions).") +endif () + +set(HUNTER_PASSWORDS_PATH + "${CMAKE_CURRENT_LIST_DIR}/passwords.cmake" + CACHE FILEPATH "Hunter passwords" +) + +set(HUNTER_CACHE_SERVERS + "https://github.com/qdrvm/hunter-binary-cache" + CACHE STRING "Binary cache server" +) + +# https://hunter.readthedocs.io/en/latest/reference/user-variables.html#hunter-use-cache-servers +# set( +# HUNTER_USE_CACHE_SERVERS NO +# CACHE STRING "Disable binary cache" +# ) + +# https://hunter.readthedocs.io/en/latest/reference/user-variables.html#hunter-status-debug +# set( +# HUNTER_STATUS_DEBUG ON +# CACHE STRING "Enable output lot of info for debugging" +# ) + +include(${CMAKE_CURRENT_LIST_DIR}/HunterGate.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/hunter-gate-url.cmake) diff --git a/cmake/Hunter/passwords.cmake b/cmake/Hunter/passwords.cmake new file mode 100644 index 0000000..00266ec --- /dev/null +++ b/cmake/Hunter/passwords.cmake @@ -0,0 +1,11 @@ +hunter_upload_password( + # REPO_OWNER + REPO = https://github.com/forexample/hunter-cache + REPO_OWNER "qdrvm" + REPO "hunter-binary-cache" + + # USERNAME = warchant + USERNAME "$ENV{GITHUB_HUNTER_USERNAME}" + + # PASSWORD = GitHub token saved as a secure environment variable + PASSWORD "$ENV{GITHUB_HUNTER_TOKEN}" +) diff --git a/include/scale/definitions.hpp.in b/include/scale/definitions.hpp.in new file mode 100644 index 0000000..a6bc41e --- /dev/null +++ b/include/scale/definitions.hpp.in @@ -0,0 +1,17 @@ +/** + * Copyright Soramitsu Co., Ltd. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#cmakedefine JAM_COMPATIBILITY_ENABLED +#cmakedefine CUSTOM_CONFIG_ENABLED + +namespace scale::detail { + // clang-format off + constexpr static size_t MAX_FIELD_NUM = @MAX_AGGREGATE_FIELDS@; + // clang-format on +} // namespace scale::detail \ No newline at end of file diff --git a/include/scale/detail/.gitignore b/include/scale/detail/.gitignore new file mode 100644 index 0000000..907aa31 --- /dev/null +++ b/include/scale/detail/.gitignore @@ -0,0 +1,5 @@ +# Ignored because part of this file is generated by CMake. +# If changes to the base content need to be committed, they must be committed explicitly, +# except for the section between `-BEGIN-GENERATED-SECTION-` and `-END-GENERATED-SECTION-`, +# which is automatically generated and should not be committed. +aggregate.hpp diff --git a/include/scale/detail/aggregate.hpp.in b/include/scale/detail/aggregate.hpp.in new file mode 100644 index 0000000..98242da --- /dev/null +++ b/include/scale/detail/aggregate.hpp.in @@ -0,0 +1,31 @@ +/** + * Copyright Soramitsu Co., Ltd. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +namespace scale::detail { + + template + decltype(auto) decompose_and_apply(SimpleCodeableAggregate auto &&v, + const F &f) { + constexpr auto N = field_number_of; + // clang-format off + if constexpr (N == 0) { + return f(); + // -BEGIN-GENERATED-SECTION- + // -END-GENERATED-SECTION- + } else { + // We mustn't fall in here + static_assert(N <= MAX_FIELD_NUM, "Inconsistent value of MAX_FIELD_NUM"); + static_assert(N > MAX_FIELD_NUM, "No code for cover aggregate with such big amount of fields"); + } + // clang-format on + } + +} // namespace scale::detail diff --git a/include/scale/detail/compact_integer.hpp b/include/scale/detail/compact_integer.hpp index 5a4fbdf..e179f16 100644 --- a/include/scale/detail/compact_integer.hpp +++ b/include/scale/detail/compact_integer.hpp @@ -6,8 +6,6 @@ #pragma once -#include -#include #include #include @@ -17,6 +15,7 @@ #include #include #include +#include namespace scale::detail { @@ -28,13 +27,16 @@ namespace scale::detail { constexpr static size_t kMinBigInteger = (1ul << 30u); /// Returns the compact encoded length for the given value. - size_t compactLen(std::unsigned_integral auto val) { + size_t lengthOfEncodedCompactInteger(CompactCompatible auto val) { if (val < kMinUint16) return 1; if (val < kMinUint32) return 2; if (val < kMinBigInteger) return 4; - size_t counter = 1; - while ((val >>= 8)) ++counter; - return counter; + if constexpr (std::unsigned_integral) { + return 1 + std::bit_width(val); + } else { + // number of bytes required to represent value + return 1 + msb(val) / 8; + } } /** @@ -45,9 +47,10 @@ namespace scale::detail { * @return byte array representation of value as compact-integer */ template - requires(std::integral or std::is_same_v) - void encodeCompactInteger(T integer, S &s) { - boost::multiprecision::cpp_int value{integer}; + requires CompactCompatible> + and std::derived_from, ScaleEncoderStream> + void encodeCompactInteger(T &&value, S &stream) { + constexpr auto is_integral = std::unsigned_integral>; // cannot encode negative numbers // there is no description how to encode compact negative numbers @@ -56,27 +59,47 @@ namespace scale::detail { } if (value < kMinUint16) { - uint8_t v = (value.convert_to() << 2u) | 0b00; - return encodeInteger(v, s); + uint8_t v; + if constexpr (is_integral) { + v = (static_cast(value) << 2u) | 0b00; + } else { + v = (value.template convert_to() << 2u) | 0b00; + } + return encodeInteger(v, stream); } - else if (value < kMinUint32) { + if (value < kMinUint32) { // only values from [kMinUint16, kMinUint32) can be put here - uint16_t v = (value.convert_to() << 2u) | 0b01; - return encodeInteger(v, s); + uint16_t v; + if constexpr (is_integral) { + v = (static_cast(value) << 2u) | 0b01; + } else { + v = (value.template convert_to() << 2u) | 0b01; + } + return encodeInteger(v, stream); } - else if (value < kMinBigInteger) { + if (value < kMinBigInteger) { // only values from [kMinUint32, kMinBigInteger) can be put here - uint32_t v = (value.convert_to() << 2u) | 0b10; - return encodeInteger(v, s); + uint32_t v; + if constexpr (is_integral) { + v = (static_cast(value) << 2u) | 0b10; + } else { + v = (value.template convert_to() << 2u) | 0b10; + } + return encodeInteger(v, stream); } - // number of bytes required to represent value - size_t significant_bytes_n = msb(value) / 8 + 1; + size_t significant_bytes_n; + if constexpr (is_integral) { + significant_bytes_n = std::bit_width(value); + } else { + // number of bytes required to represent value + significant_bytes_n = msb(value) / 8 + 1; - if (significant_bytes_n > 67) { - raise(EncodeError::COMPACT_INTEGER_TOO_BIG); + if (significant_bytes_n > 67) { + raise(EncodeError::COMPACT_INTEGER_TOO_BIG); + } } // The upper 6 bits of the header are used to encode the number of bytes @@ -91,20 +114,20 @@ namespace scale::detail { // to the result of the previous operations. uint8_t header = ((significant_bytes_n - 4) << 2u) | 0b11; - s << header; + stream << header; for (auto v = value; v != 0; v >>= 8) { // push back the least significant byte - s << static_cast(v & 0xff); + stream << static_cast(v & 0xff); } } - template - requires std::is_same_v - T decodeCompactInteger(S &stream) { + template + requires std::derived_from, ScaleDecoderStream> + boost::multiprecision::uint1024_t decodeCompactInteger(S &stream) { auto first_byte = stream.nextByte(); - const uint8_t flag = (first_byte)&0b00000011u; + const uint8_t flag = first_byte & 0b00000011u; size_t number = 0u; @@ -117,7 +140,7 @@ namespace scale::detail { case 0b01u: { auto second_byte = stream.nextByte(); - number = (static_cast((first_byte)&0b11111100u) + number = (static_cast(first_byte & 0b11111100u) + static_cast(second_byte) * 256u) >> 2u; if ((number >> 6) == 0) { @@ -129,15 +152,15 @@ namespace scale::detail { case 0b10u: { number = first_byte; size_t multiplier = 256u; - if (!stream.hasMore(3u)) { + if (not stream.hasMore(3u)) { raise(DecodeError::NOT_ENOUGH_DATA); } for (auto i = 0u; i < 3u; ++i) { // we assured that there are 3 more bytes, // no need to make checks in a loop - number += (stream.nextByte()) * multiplier; - multiplier = multiplier << 8u; + number += stream.nextByte() * multiplier; + multiplier <<= 8u; } number = number >> 2u; if ((number >> 14) == 0) { @@ -148,17 +171,17 @@ namespace scale::detail { case 0b11: { auto bytes_count = ((first_byte) >> 2u) + 4u; - if (!stream.hasMore(bytes_count)) { + if (not stream.hasMore(bytes_count)) { raise(DecodeError::NOT_ENOUGH_DATA); } - CompactInteger multiplier{1u}; - CompactInteger value = 0; + boost::multiprecision::uint1024_t multiplier{1u}; + boost::multiprecision::uint1024_t value{0}; // we assured that there are m more bytes, // no need to make checks in a loop for (auto i = 0u; i < bytes_count; ++i) { - value += (stream.nextByte()) * multiplier; - multiplier *= 256u; + value += stream.nextByte() * multiplier; + multiplier <<= 8u; } if (value.is_zero()) { raise(DecodeError::REDUNDANT_COMPACT_ENCODING); @@ -174,7 +197,7 @@ namespace scale::detail { UNREACHABLE } - return CompactInteger{number}; + return number; } /** @@ -186,8 +209,9 @@ namespace scale::detail { */ template requires std::unsigned_integral - T decodeCompactInteger(S &s) { - auto integer = decodeCompactInteger(s); + and std::derived_from, ScaleDecoderStream> + T decodeCompactInteger(S &stream) { + auto integer = decodeCompactInteger(stream); if (not integer.is_zero() and msb(integer) >= std::numeric_limits::digits) { raise(DecodeError::DECODED_VALUE_OVERFLOWS_TARGET); @@ -195,5 +219,4 @@ namespace scale::detail { return static_cast(integer); } - } // namespace scale::detail diff --git a/include/scale/detail/custom_decomposing.hpp b/include/scale/detail/custom_decomposing.hpp new file mode 100644 index 0000000..42b6ed6 --- /dev/null +++ b/include/scale/detail/custom_decomposing.hpp @@ -0,0 +1,36 @@ +/** + * Copyright Soramitsu Co., Ltd. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @brief Defines a custom decomposition mechanism and function application to + * an object, allowing easy customization of encoding (e.g., using only specific + * fields or changing their order). + * + * @param Self The name of the class where this macro is used. + * @param ... The list of class members in the desired order that will + * participate in decomposition. + * + * Example usage: + * @code + * struct Point { + * int x, y; + * + * SCALE_CUSTOM_DECOMPOSITION(Point, x, y); + * }; + * @endcode + */ +#define SCALE_CUSTOM_DECOMPOSITION(Self, ...) \ + private: \ + decltype(auto) _custom_decompose_and_apply(auto &&f) { \ + return std::forward(f)(__VA_ARGS__); \ + } \ + decltype(auto) _custom_decompose_and_apply(auto &&f) const { \ + return std::forward(f)(__VA_ARGS__); \ + } \ + template \ + requires std::is_same_v, Self> \ + friend decltype(auto) decompose_and_apply(V &&v, F &&f) { \ + return std::forward(v)._custom_decompose_and_apply(std::forward(f)); \ + } diff --git a/include/scale/detail/fixed_width_integer.hpp b/include/scale/detail/fixed_width_integer.hpp index ae5b75a..27f28b2 100644 --- a/include/scale/detail/fixed_width_integer.hpp +++ b/include/scale/detail/fixed_width_integer.hpp @@ -6,37 +6,179 @@ #pragma once -#include -#include -#include -#include - -#include - -#include -#include -#include - -namespace scale::detail { - /** - * encodeInteger encodes any integer type to little-endian representation - * @tparam T integer type - * @tparam S output stream type - * @param value integer value - * @return byte array representation of value - */ - template , - typename = std::enable_if_t::value>> - void encodeInteger(T value, S &out) { // no need to take integers by && +#include + +#include +#include + +#include + +namespace scale { + class ScaleEncoderStream; + class ScaleDecoderStream; +} // namespace scale + +namespace scale { + + /** + * @brief Concept for big fixed-width integer types. + * + * This concept checks if the given type is one of the predefined large + * integer types. + */ + template + concept BigFixedWidthInteger = + std::is_same_v, uint128_t> + or std::is_same_v, uint256_t> + or std::is_same_v, uint512_t> + or std::is_same_v, uint1024_t>; + + /** + * @brief Traits for determining the size of fixed-width integer types. + * + * This primary template handles standard integral types. + */ + template + struct FixedWidthIntegerTraits; + + /** + * @brief Specialization for built-in integral types. + */ + template + struct FixedWidthIntegerTraits { + static constexpr size_t bytes = sizeof(T); + static constexpr size_t bits = bytes * 8; + }; + + /** + * @brief Specialization for Boost multiprecision integer types. + * + * Computes the number of bytes required to store the value, + * excluding any metadata or auxiliary data. + */ + template + struct FixedWidthIntegerTraits>> { + private: + // Compute count significant bytes needed to store value, + // except any metadata and auxiliaries + static constexpr size_t compute_bytes() { + T temp{}; + return temp.backend().size() * sizeof(temp.backend().limbs()[0]); + } + + public: + static constexpr size_t bytes = compute_bytes(); + static constexpr size_t bits = bytes * 8; + }; + + namespace detail { + /** + * @brief Converts a value from one type to another using static_cast. + * @tparam To Target type. + * @tparam From Source type. + * @param value Value to convert. + * @return Converted value. + */ + template + requires std::is_convertible_v + To convert_to(From value) { + return static_cast(value); + } + + /** + * @brief Specialized conversion for Boost multiprecision numbers. + * @tparam To Target type. + * @tparam From Boost multiprecision number type. + * @param value Value to convert. + * @return Converted value. + * @throws std::system_error if conversion results in data loss. + */ + template + requires boost::multiprecision::is_number::value + To convert_to(const From &value) { + try { + return value.template convert_to(); + } catch (const std::runtime_error &e) { + // scale::decode catches std::system_errors + throw std::system_error{ + make_error_code(std::errc::value_too_large), + "This integer conversion would lead to information loss"}; + } + } + } // namespace detail + + /** + * @brief Encodes an integer to little-endian representation. + * @tparam T Integer type. + * @param value Integer value to encode. + * @param stream Output stream where encoded data is written. + */ + template + requires std::is_integral_v> + void encodeInteger(T value, ScaleEncoderStream &stream) { + using I = std::remove_cvref_t; constexpr size_t size = sizeof(I); constexpr size_t bits = size * 8; boost::endian::endian_buffer buf{}; - buf = value; // cannot initialize, only assign + buf = value; // Assign value to endian buffer for (size_t i = 0; i < size; ++i) { - // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) - out << buf.data()[i]; + stream << buf.data()[i]; // Write each byte to the stream + } + } + + /** + * @brief Encodes large fixed-width integers to little-endian format. + * @tparam S Type of the SCALE encoder stream. + * @param v Integer value to encode. + * @param s Encoder stream. + */ + template + requires std::derived_from, ScaleEncoderStream> + void encodeInteger(const BigFixedWidthInteger auto &v, S &s) { + using Integer = std::remove_cvref_t; + static constexpr auto bits = FixedWidthIntegerTraits::bits; + for (size_t i = 0; i < bits; i += 8) { + s.putByte(detail::convert_to((v >> i) & 0xFFu)); } } -} // namespace scale::detail + + /** + * @brief Decodes an integer from little-endian representation. + * @tparam T Integer type. + * @param value Reference to store the decoded integer. + * @param stream Input stream from which data is read. + */ + template + requires std::is_integral_v> + void decodeInteger(T &value, ScaleDecoderStream &stream) { + using I = std::remove_cvref_t; + constexpr size_t size = sizeof(I); + constexpr size_t bits = size * 8; + boost::endian::endian_buffer buf{}; + buf = value; // Assign initial value + for (size_t i = 0; i < size; ++i) { + stream >> buf.data()[i]; // Read each byte from the stream + } + } + + /** + * @brief Decodes large fixed-width integers from little-endian format. + * @tparam S Type of the SCALE decoder stream. + * @param v Reference to store the decoded integer. + * @param s Decoder stream. + */ + template + requires std::derived_from, ScaleDecoderStream> + void decodeInteger(BigFixedWidthInteger auto &v, S &s) { + using Integer = std::remove_cvref_t; + static constexpr auto bits = FixedWidthIntegerTraits::bits; + + v = 0; + for (size_t i = 0; i < bits; i += 8) { + uint8_t byte; + s >> byte; + v |= Integer(byte) << i; + } + } + +} // namespace scale diff --git a/include/scale/detail/jam_compact_integer.hpp b/include/scale/detail/jam_compact_integer.hpp index cb73456..0394bd5 100644 --- a/include/scale/detail/jam_compact_integer.hpp +++ b/include/scale/detail/jam_compact_integer.hpp @@ -17,6 +17,15 @@ namespace scale::detail { + /// Returns the compact encoded length for the given value. + size_t lengthOfEncodedJamCompactInteger(CompactCompatible auto value) { + if constexpr (std::unsigned_integral) { + return 1 + (std::bit_width(value) - 1) / 7; + } else { + return 1 + msb(value - 1) / 7; + } + } + /** * Encodes any integer type to jam-compact-integer representation * @tparam T integer type @@ -25,11 +34,16 @@ namespace scale::detail { * @return byte array representation of value as jam-compact-integer */ template - requires(std::unsigned_integral or std::is_same_v) - void encodeJamCompactInteger(T integer, S &s) { + requires CompactCompatible> + and std::derived_from, ScaleEncoderStream> + void encodeJamCompactInteger(T &&integer, S &stream) { + constexpr auto is_integral = std::unsigned_integral>; + size_t value; - if constexpr (std::is_same_v) { + if constexpr (is_integral) { + value = static_cast(integer); + } else { // cannot encode negative numbers // there is no description how to encode compact negative numbers if (integer < 0) { @@ -43,12 +57,10 @@ namespace scale::detail { } value = integer.template convert_to(); } - } else { - value = static_cast(integer); } if (value < 0x80) { - s << static_cast(value); + stream << static_cast(value); return; } @@ -67,7 +79,7 @@ namespace scale::detail { } for (auto byte : bytes) { - s << byte; + stream << byte; if (--len == 0) break; } } @@ -79,12 +91,12 @@ namespace scale::detail { * @param value integer value * @return value according jam-compact-integer representation */ - template - requires std::unsigned_integral or std::is_same_v - T decodeJamCompactInteger(auto &s) { + template + requires std::derived_from, ScaleDecoderStream> + boost::multiprecision::uint128_t decodeJamCompactInteger(S &stream) { uint8_t byte; - s >> byte; + stream >> byte; if (byte == 0) { return 0; @@ -100,27 +112,20 @@ namespace scale::detail { val_mask >>= 1; val_bits &= val_mask; - if ((len_bits & static_cast(0x80)) == 0) { // no more significant bytes + if ((len_bits & static_cast(0x80)) + == 0) { // no more significant bytes value |= static_cast(val_bits) << (8 * i); break; } len_bits <<= 1; - s >> byte; + stream >> byte; value |= static_cast(byte) << (8 * i); } if (val_bits == 0 and (byte & ~val_mask) == 0) { raise(DecodeError::REDUNDANT_COMPACT_ENCODING); } - - if constexpr (not std::is_same_v - and not std::is_same_v) { - if (value > std::numeric_limits::max()) { - raise(DecodeError::DECODED_VALUE_OVERFLOWS_TARGET); - } - } - - return static_cast(value); + return value; } } // namespace scale::detail diff --git a/include/scale/encode_append.hpp b/include/scale/encode_append.hpp index 4b1e1f4..871cd0e 100644 --- a/include/scale/encode_append.hpp +++ b/include/scale/encode_append.hpp @@ -7,6 +7,7 @@ #pragma once #include +#include #include namespace scale { @@ -16,16 +17,15 @@ namespace scale { */ struct EncodeOpaqueValue { ConstSpanOfBytes v; - }; - template > - Stream &operator<<(Stream &s, const EncodeOpaqueValue &value) { - for (auto &&it = value.v.begin(), end = value.v.end(); it != end; ++it) { - s << *it; + friend ScaleEncoderStream &operator<<(ScaleEncoderStream &s, + const EncodeOpaqueValue &value) { + for (auto &item : value.v) { + s << item; + } + return s; } - return s; - } + }; /** * Adds an EncodeOpaqueValue to a scale encoded vector of EncodeOpaqueValue's. diff --git a/include/scale/enum_traits.hpp b/include/scale/enum_traits.hpp index 4d26389..49d9362 100644 --- a/include/scale/enum_traits.hpp +++ b/include/scale/enum_traits.hpp @@ -14,6 +14,8 @@ namespace scale { + class ScaleDecoderStream; + /** * Description of an enum type * Two specialization choices: @@ -24,12 +26,11 @@ namespace scale { * @tparam E the enum type */ template + requires std::is_enum_v struct [[deprecated( - "Check the doc comment to see the specialization options")]] enum_traits - final { - static_assert(std::is_enum_v); - - // to easily detect an unspecialized enum_traits + "Check the doc comment to see the specialization options")]] // + enum_traits final { + // Used to easily detect an unspecialized enum_traits static constexpr bool is_default = true; }; @@ -63,19 +64,17 @@ namespace scale { typename = decltype(E_traits::valid_values)> constexpr bool is_valid_enum_value(std::underlying_type_t value) noexcept { const auto &valid_values = E_traits::valid_values; - return std::find(std::begin(valid_values), - std::end(valid_values), - static_cast(value)) + return std::ranges::find(valid_values, static_cast(value)) != std::end(valid_values); } - template , - typename = std::enable_if_t::is_default>> + template + requires enum_traits>::is_default [[deprecated( "Please specialize scale::enum_traits for your enum so it can be " - "validated during decoding")]] constexpr bool - is_valid_enum_value(std::underlying_type_t value) noexcept { + "validated during decoding")]] + constexpr bool is_valid_enum_value( + std::underlying_type_t> value) noexcept { return true; } @@ -85,12 +84,10 @@ namespace scale { * @param v value of enum type * @return reference to stream */ - template , - typename = std::enable_if_t, - typename = std::enable_if_t>> - S &operator>>(S &s, T &v) { + template + requires std::is_enum_v> + ScaleDecoderStream &operator>>(ScaleDecoderStream &s, T &v) { + using E = std::decay_t; std::underlying_type_t value; s >> value; if (is_valid_enum_value(value)) { diff --git a/include/scale/scale.hpp b/include/scale/scale.hpp index 62558fe..86aa9df 100644 --- a/include/scale/scale.hpp +++ b/include/scale/scale.hpp @@ -9,29 +9,13 @@ #include #include -#include #include +#include +#include +#include #include #include -#define SCALE_EMPTY_DECODER(TargetType) \ - template > \ - Stream &operator>>(Stream &s, TargetType &) { \ - return s; \ - } - -#define SCALE_EMPTY_ENCODER(TargetType) \ - template > \ - Stream &operator<<(Stream &s, const TargetType &) { \ - return s; \ - } - -#define SCALE_EMPTY_CODER(TargetType) \ - SCALE_EMPTY_ENCODER(TargetType) \ - SCALE_EMPTY_DECODER(TargetType) - namespace scale { template outcome::result> outcomeCatch(F &&f) { @@ -53,10 +37,10 @@ namespace scale { * @param args data to encode * @return encoded data */ - template - outcome::result> encode(Args &&...args) { + template + outcome::result> encode(T &&v) { ScaleEncoderStream s{}; - OUTCOME_TRY(encode(s, std::forward(args)...)); + OUTCOME_TRY(encode(s, std::forward(v))); return s.to_vector(); } template @@ -70,7 +54,7 @@ namespace scale { * @param span of bytes with encoded data * @return decoded T */ - template + template outcome::result decode(ConstSpanOfBytes data) { ScaleDecoderStream s(data); return decode(s); @@ -85,4 +69,24 @@ namespace scale { outcome::result decode(ScaleDecoderStream &s, T &t) { return outcomeCatch([&] { s >> t; }); } + +#ifdef CUSTOM_CONFIG_ENABLED + template + requires(not std::derived_from, ScaleEncoderStream>) + outcome::result encode(const T &v, const auto &config) { + ScaleEncoderStream s(config); + OUTCOME_TRY(encode(s, v)); + return outcome::success(s.to_vector()); + } + + template + requires(not std::derived_from, ScaleEncoderStream>) + outcome::result decode(ConstSpanOfBytes bytes, const auto &config) { + ScaleDecoderStream s(bytes, config); + T t; + OUTCOME_TRY(decode(s, t)); + return outcome::success(std::move(t)); + } +#endif + } // namespace scale diff --git a/include/scale/scale_decoder_stream.hpp b/include/scale/scale_decoder_stream.hpp index ed83511..4fa14bd 100644 --- a/include/scale/scale_decoder_stream.hpp +++ b/include/scale/scale_decoder_stream.hpp @@ -14,6 +14,10 @@ #include #include +#include +#include +#include + #ifdef __has_include #if __has_include() #include @@ -23,6 +27,7 @@ #include #include +#include #include #ifdef JAM_COMPATIBILITY_ENABLED #include @@ -52,20 +57,49 @@ namespace scale { #endif template + requires CompactCompatible T decodeCompact() { - scale::CompactInteger big = + auto integer = #ifdef JAM_COMPATIBILITY_ENABLED - detail::decodeJamCompactInteger(*this); + detail::decodeJamCompactInteger(*this); #else - detail::decodeCompactInteger(*this); + detail::decodeCompactInteger(*this); #endif - if (not big.is_zero() and msb(big) >= std::numeric_limits::digits) { - raise(DecodeError::TOO_MANY_ITEMS); + if constexpr (std::is_integral_v) { + if (not integer.is_zero() + and msb(integer) >= std::numeric_limits::digits) { + raise(DecodeError::DECODED_VALUE_OVERFLOWS_TARGET); + } } - return static_cast(big); + return static_cast(integer); } - size_t decodeLength(); + /** + * @brief scale-decodes aggregate + * @param v aggregate for decoding to + * @return reference to stream + */ + ScaleDecoderStream &operator>>(SimpleCodeableAggregate auto &v) + requires(not qtils::is_tagged_v) + { + return detail::decompose_and_apply( + v, [&](auto &...args) -> ScaleDecoderStream & { + return (*this >> ... >> args); + }); + } + + /** + * @brief scale-decodes custom decomposable object + * @param v object for decoding to + * @return reference to stream + */ + ScaleDecoderStream &operator>>(CustomDecomposable auto &v) + requires(not qtils::is_tagged_v) + { + return decompose_and_apply(v, [&](auto &...args) -> ScaleDecoderStream & { + return (*this >> ... >> args); + }); + } /** * @brief scale-decodes pair of values @@ -75,10 +109,13 @@ namespace scale { * @return reference to stream */ template + requires(not std::is_reference_v and not std::is_reference_v) ScaleDecoderStream &operator>>(std::pair &p) { - static_assert(!std::is_reference_v && !std::is_reference_v); - return *this >> const_cast &>(p.first) // NOLINT - >> const_cast &>(p.second); // NOLINT + using mutableF = std::remove_cvref_t; + using mutableS = std::remove_cvref_t; + return *this // + >> const_cast(p.first) // NOLINT + >> const_cast(p.second); // NOLINT } /** @@ -145,12 +182,10 @@ namespace scale { * @param v value to decode * @return reference to stream */ - template + template + requires std::is_default_constructible_v> ScaleDecoderStream &operator>>(std::shared_ptr &v) { - using mutableT = std::remove_const_t; - - static_assert(std::is_default_constructible_v); - + using mutableT = std::remove_cvref_t; v = std::make_shared(); return *this >> const_cast(*v); // NOLINT } @@ -161,12 +196,10 @@ namespace scale { * @param v value to decode * @return reference to stream */ - template + template + requires std::is_default_constructible_v> ScaleDecoderStream &operator>>(std::unique_ptr &v) { - using mutableT = std::remove_const_t; - - static_assert(std::is_default_constructible_v); - + using mutableT = std::remove_cvref_t; v = std::make_unique(); return *this >> const_cast(*v); // NOLINT } @@ -177,10 +210,10 @@ namespace scale { * @param v value of integral type * @return reference to stream */ - template , - typename = std::enable_if_t>> + template + requires std::is_integral_v> ScaleDecoderStream &operator>>(T &v) { + using I = std::decay_t; // check bool if constexpr (std::is_same_v) { v = decodeBool(); @@ -202,34 +235,45 @@ namespace scale { return *this; } + /** + * @brief scale-decodes any integral type including bool + * @tparam T integral type + * @param v value of integral type + * @return reference to stream + */ + ScaleDecoderStream &operator>>(BigFixedWidthInteger auto &v) { + decodeInteger(v, *this); + return *this; + } + /** * @brief scale-decodes any optional value * @tparam T type of optional value * @param v optional value reference * @return reference to stream */ - template + template + requires std::is_default_constructible_v> ScaleDecoderStream &operator>>(std::optional &v) { - using mutableT = std::remove_const_t; - - static_assert(std::is_default_constructible_v); + using mutableT = std::remove_cvref_t; - // optional bool is special case of optional values - // it is encoded as one byte instead of two - // as described in specification + // Special case for `std::optional` if constexpr (std::is_same_v) { v = decodeOptionalBool(); return *this; } - // detect if optional has value + + // Check if the optional contains a value bool has_value = false; *this >> has_value; - if (!has_value) { - v.reset(); + + if (not has_value) { + v.reset(); // Reset the optional if it has no value return *this; } - // decode value - v.emplace(); + + // Decode the value + v.emplace(); // Initialize the object inside the optional return *this >> const_cast(*v); // NOLINT } @@ -238,15 +282,20 @@ namespace scale { * @param v compact integer reference * @return */ - ScaleDecoderStream &operator>>(CompactInteger &v); + ScaleDecoderStream &operator>>(CompactInteger auto &v) { + v = decodeCompact>(); + return *this; + } /** * @brief scale-decodes to any static (fixed-size) collection * @param collection decoding collection to * @return reference to stream */ - ScaleDecoderStream &operator>>(StaticCollection auto &container) { - for (auto &item : container) { + ScaleDecoderStream &operator>>(StaticCollection auto &collection) + requires(not qtils::is_tagged_v) + { + for (auto &item : collection) { *this >> item; } return *this; @@ -258,8 +307,10 @@ namespace scale { * @param collection decoding collection to * @return reference to stream */ - ScaleDecoderStream &operator>>(ResizeableCollection auto &collection) { - auto item_count = decodeLength(); + ScaleDecoderStream &operator>>(ResizeableCollection auto &collection) + requires(not qtils::is_tagged_v) + { + auto item_count = decodeCompact(); if (item_count > collection.max_size()) { raise(DecodeError::TOO_MANY_ITEMS); } @@ -282,7 +333,7 @@ namespace scale { * @return reference to stream */ ScaleDecoderStream &operator>>(std::vector &collection) { - auto item_count = decodeLength(); + auto item_count = decodeCompact(); if (item_count > collection.max_size()) { raise(DecodeError::TOO_MANY_ITEMS); } @@ -316,10 +367,12 @@ namespace scale { * space first and push element by element back while decoding) * @return reference to stream */ - ScaleDecoderStream &operator>>(ExtensibleBackCollection auto &collection) { + ScaleDecoderStream &operator>>(ExtensibleBackCollection auto &collection) + requires(not qtils::is_tagged_v) + { using size_type = typename std::decay_t::size_type; - auto item_count = decodeLength(); + auto item_count = decodeCompact(); if (item_count > collection.max_size()) { raise(DecodeError::TOO_MANY_ITEMS); } @@ -344,13 +397,14 @@ namespace scale { * decoding) * @return reference to stream */ - ScaleDecoderStream &operator>>( - RandomExtensibleCollection auto &collection) { + ScaleDecoderStream &operator>>(RandomExtensibleCollection auto &collection) + requires(not qtils::is_tagged_v) + { using size_type = typename std::decay_t::size_type; using value_type = typename std::decay_t::value_type; - auto item_count = decodeLength(); + auto item_count = decodeCompact(); if (item_count > collection.max_size()) { raise(DecodeError::TOO_MANY_ITEMS); } @@ -404,7 +458,9 @@ namespace scale { template void decodeElementOfTuple(std::tuple &v) { - using T = std::remove_const_t>>; + using T = std::remove_cvref_t>>; + static_assert(std::is_default_constructible_v, + "Type of each tuple member must be default constructible"); *this >> const_cast(std::get(v)); // NOLINT if constexpr (sizeof...(Ts) > I + 1) { decodeElementOfTuple(v); @@ -412,11 +468,13 @@ namespace scale { } template + requires(I < sizeof...(Ts)) void tryDecodeAsOneOfVariant(std::variant &v, size_t i) { - using T = std::remove_const_t>>; - static_assert(std::is_default_constructible_v); + using T = std::remove_cvref_t>>; + static_assert(std::is_default_constructible_v, + "All types of variant must be default constructible"); if (I == i) { - T val; + T val{}; *this >> val; v = std::forward(val); return; @@ -429,8 +487,9 @@ namespace scale { #ifdef USE_BOOST_VARIANT template void tryDecodeAsOneOfVariant(boost::variant &v, size_t i) { - using T = std::remove_const_t>>; - static_assert(std::is_default_constructible_v); + using T = std::remove_cvref_t>>; + static_assert(std::is_default_constructible_v, + "All types of variant must be default constructible"); if (I == i) { T val; *this >> val; diff --git a/include/scale/scale_encoder_stream.hpp b/include/scale/scale_encoder_stream.hpp index ac3f215..2fbcc19 100644 --- a/include/scale/scale_encoder_stream.hpp +++ b/include/scale/scale_encoder_stream.hpp @@ -14,6 +14,8 @@ #include #include +#include + #ifdef __has_include #if __has_include() #include @@ -23,6 +25,7 @@ #include #include +#include #include #ifdef JAM_COMPATIBILITY_ENABLED #include @@ -71,24 +74,63 @@ namespace scale { /** * @return vector of bytes containing encoded data */ - std::vector to_vector() const; + [[nodiscard]] std::vector to_vector() const; /** * Get amount of encoded data written to the stream * @return size in bytes */ - size_t size() const; + [[nodiscard]] size_t size() const; + + [[nodiscard]] auto begin() const { + return stream_.begin(); + } + [[nodiscard]] auto end() const { + return stream_.end(); + } + + /** + * @brief scale-encodes aggregate + * @param v aggregate to encode + * @return reference to stream + */ + ScaleEncoderStream &operator<<(const SimpleCodeableAggregate auto &v) + requires(not qtils::is_tagged_v) + { + return detail::decompose_and_apply( + v, [&](const auto &...args) -> ScaleEncoderStream & { + return (*this << ... << args); + }); + } + + /** + * @brief scale-encodes custom decomposable object + * @param v object to encode + * @return reference to stream + */ + ScaleEncoderStream &operator<<(const CustomDecomposable auto &v) + requires(not qtils::is_tagged_v) + { + return decompose_and_apply( + v, [&](const auto &...args) -> ScaleEncoderStream & { + return (*this << ... << args); + }); + } /** * @brief scale-encodes range * @param collection range to encode * @return reference to stream */ - ScaleEncoderStream &operator<<(const DynamicCollection auto &collection) { + ScaleEncoderStream &operator<<(const DynamicCollection auto &collection) + requires(not qtils::is_tagged_v) + { return encodeDynamicCollection(collection); } - ScaleEncoderStream &operator<<(const StaticCollection auto &collection) { + ScaleEncoderStream &operator<<(const StaticCollection auto &collection) + requires(not qtils::is_tagged_v) + { return encodeStaticCollection(collection); } @@ -155,7 +197,7 @@ namespace scale { * @param v value to encode * @return reference to stream */ - template + template ScaleEncoderStream &operator<<(const std::shared_ptr &v) { if (v == nullptr) { raise(EncodeError::DEREF_NULLPOINTER); @@ -169,7 +211,7 @@ namespace scale { * @param v value to encode * @return reference to stream */ - template + template ScaleEncoderStream &operator<<(const std::unique_ptr &v) { if (v == nullptr) { raise(EncodeError::DEREF_NULLPOINTER); @@ -183,7 +225,7 @@ namespace scale { * @param v value to encode * @return reference to stream */ - template + template ScaleEncoderStream &operator<<(const std::optional &v) { // optional bool is a special case of optional values // it should be encoded using one byte instead of two @@ -212,7 +254,7 @@ namespace scale { * @param v value to encode * @return reference to stream; */ - template + template ScaleEncoderStream &operator<<(const std::reference_wrapper &v) { return *this << static_cast(v); } @@ -232,7 +274,7 @@ namespace scale { * @return reference to stream */ ScaleEncoderStream &operator<<(const std::vector &v) { - *this << CompactInteger{v.size()}; + *this << Length(v.size()); for (bool el : v) { *this << el; } @@ -245,10 +287,12 @@ namespace scale { * @param v value of integral type * @return reference to stream */ - template , - typename = std::enable_if_t>> - ScaleEncoderStream &operator<<(T &&v) { + template + requires std::is_integral_v> + ScaleEncoderStream &operator<<(T &&v) + requires(not qtils::is_tagged_v) + { + using I = std::decay_t; // encode bool if constexpr (std::is_same_v) { uint8_t byte = (v ? 1u : 0u); @@ -260,7 +304,21 @@ namespace scale { return putByte(static_cast(v)); } // encode any other integer - detail::encodeInteger(v, *this); + encodeInteger(v, *this); + return *this; + } + + /** + * @brief scale-encodes any fixed-width integer type + * @param v value of integral type + * @return reference to stream + */ + ScaleEncoderStream &operator<<(const BigFixedWidthInteger auto &v) { + constexpr auto bits = + FixedWidthIntegerTraits>::bits; + for (size_t i = 0; i < bits; i += 8) { + putByte(detail::convert_to((v >> i) & 0xFFu)); + } return *this; } @@ -269,7 +327,15 @@ namespace scale { * @param v value to encode * @return reference to stream */ - ScaleEncoderStream &operator<<(const CompactInteger &v); + ScaleEncoderStream &operator<<(CompactInteger auto &&v) { + auto &&val = untagged(v); +#ifdef JAM_COMPATIBILITY_ENABLED + detail::encodeJamCompactInteger(std::forward(val), *this); +#else + detail::encodeCompactInteger(std::forward(val), *this); +#endif + return *this; + } protected: template @@ -313,7 +379,7 @@ namespace scale { */ ScaleEncoderStream &encodeDynamicCollection( const std::ranges::sized_range auto &collection) { - *this << CompactInteger{collection.size()}; + *this << Length(collection.size()); for (const auto &item : collection) { *this << item; } @@ -356,12 +422,10 @@ namespace scale { * @param v value of the enum type * @return reference to stream */ - template , - typename = std::enable_if_t, - typename = std::enable_if_t>> - S &operator<<(S &s, const T &v) { + template + requires std::is_enum_v> + ScaleEncoderStream &operator<<(ScaleEncoderStream &s, const T &v) { + using E = std::decay_t; return s << static_cast>(v); } diff --git a/include/scale/types.hpp b/include/scale/types.hpp index 543adc9..9af5357 100644 --- a/include/scale/types.hpp +++ b/include/scale/types.hpp @@ -12,9 +12,20 @@ #include #include +#include + +#include namespace scale { + class ScaleEncoderStream; + class ScaleDecoderStream; + + using uint128_t = boost::multiprecision::uint128_t; + using uint256_t = boost::multiprecision::uint256_t; + using uint512_t = boost::multiprecision::uint512_t; + using uint1024_t = boost::multiprecision::uint1024_t; + /// @brief convenience alias for arrays of bytes using ByteArray = std::vector; @@ -24,8 +35,97 @@ namespace scale { /// @brief convenience alias for mutable span of bytes using MutSpanOfBytes = std::span; - /// @brief represents compact integer value - using CompactInteger = boost::multiprecision::cpp_int; + namespace detail { + struct CompactIntegerTag; + + template + constexpr bool is_unsigned_backend = not Backend().sign(); + + template + concept unsigned_multiprecision_integer = + boost::multiprecision::is_unsigned_number::value; + + template + struct is_compact_integer : std::false_type {}; + + template + struct is_compact_integer> + : std::true_type {}; + } // namespace detail + + /// @brief Concept defining a valid Compact type + template + concept CompactCompatible = + std::unsigned_integral or detail::unsigned_multiprecision_integer; + + /// @brief Represents a compact integer value + template + requires CompactCompatible + using Compact = qtils::Tagged; + + /// @brief Concept that checks if a type is a Compact integer + template + concept CompactInteger = + detail::is_compact_integer>::value; + + using Length = Compact; + + template + inline T convert_to(const CompactCompatible auto &&value) { + constexpr auto is_integral = + std::unsigned_integral>; + if constexpr (is_integral) { + return static_cast(value); + } else { + return value.template convert_to(); + } + } + + template + requires CompactCompatible> + struct CompactReflection { + private: + template + explicit CompactReflection(U &&value) + : temp_storage( + std::is_lvalue_reference_v + ? std::nullopt + : std::optional>(std::forward(value))), + ref(temp_storage ? *temp_storage : value) {} + + template + requires CompactCompatible> + friend decltype(auto) as_compact(U &&value); + + public: + CompactReflection(const CompactReflection &) = delete; + CompactReflection &operator=(const CompactReflection &) = delete; + CompactReflection(CompactReflection &&) = delete; + CompactReflection &operator=(CompactReflection &&) = delete; + + friend ScaleEncoderStream &operator<<(ScaleEncoderStream &stream, + const CompactReflection &value) { + return stream << Compact>(value.ref); + } + friend ScaleDecoderStream &operator>>(ScaleDecoderStream &stream, + const CompactReflection &value) { + Compact> tmp; + stream >> tmp; + value.ref = untagged(tmp); + return stream; + } + + private: + std::optional> temp_storage; + T &ref; + }; + + template + requires CompactCompatible> + decltype(auto) as_compact(T &&value) { + return CompactReflection( + std::forward(value)); + } /// @brief OptionalBool is internal extended bool type enum class OptionalBool : uint8_t { @@ -34,43 +134,75 @@ namespace scale { OPT_FALSE = 2u, }; - template - struct __is_derived_of_span_impl { - template - static constexpr std::true_type test(const std::span *); - static constexpr std::false_type test(...); - using type = decltype(test(std::declval())); - }; + namespace detail { + struct ArgHelper { + template + operator T() const { + return T{}; + } + }; - template - using __is_derived_of_span = typename __is_derived_of_span_impl::type; + template + requires std::is_aggregate_v + constexpr bool is_constructible_with_n_def_args_impl( + std::index_sequence) { + return requires { T{(void(Indices), ArgHelper{})...}; }; + } + + template + constexpr bool is_constructible_with_n_def_args_v = + is_constructible_with_n_def_args_impl(std::make_index_sequence{}); + + template + constexpr int field_number_of_impl() { + if constexpr (std::is_empty_v) { + return 0; + } else if constexpr (is_constructible_with_n_def_args_v) { + return field_number_of_impl(); + } else { + return N; + } + } + + template + constexpr size_t field_number_of = + field_number_of_impl>(); + + template + concept is_std_array = + requires { typename std::tuple_size>::type; } + and std::same_as< + std::remove_cvref_t, + std::array::value_type, + std::tuple_size>::value>>; + } // namespace detail template - concept SomeSpan = __is_derived_of_span::value // - and requires(T) { T::extent; }; + concept SomeSpan = + std::derived_from>; - template - concept HasSomeInsertMethod = - requires(T v) { v.insert(v.end(), *v.begin()); } - or requires(T v) { v.insert_after(v.end(), *v.begin()); }; + template + concept HasSomeInsertMethod = requires(T v) { + v.insert(v.end(), *v.begin()); + } or requires(T v) { v.insert_after(v.end(), *v.begin()); }; - template + template concept HasResizeMethod = requires(T v) { v.resize(v.size()); }; - template + template concept HasReserveMethod = requires(T v) { v.reserve(v.size()); }; - template + template concept HasEmplaceMethod = requires(T v) { v.emplace(*v.begin()); }; - template + template concept HasEmplaceBackMethod = requires(T v) { v.emplace_back(*v.begin()); }; - template + template concept ImplicitlyDefinedAsStatic = not(SomeSpan) and // not(HasSomeInsertMethod); - template + template concept ImplicitlyDefinedAsDynamic = not(SomeSpan) and // HasSomeInsertMethod; @@ -82,27 +214,48 @@ namespace scale { concept DynamicSpan = SomeSpan // and (T::extent == std::dynamic_extent); - template + template concept StaticCollection = std::ranges::range and (ImplicitlyDefinedAsStatic // or StaticSpan); - template + template concept DynamicCollection = std::ranges::sized_range and (ImplicitlyDefinedAsDynamic // or DynamicSpan); - template + template concept ResizeableCollection = DynamicCollection // and HasResizeMethod; - template + template concept ExtensibleBackCollection = DynamicCollection // and not(HasResizeMethod) // and HasEmplaceBackMethod; - template + template concept RandomExtensibleCollection = DynamicCollection // and HasEmplaceMethod; + template + concept SimpleCodeableAggregate = + std::is_aggregate_v> // + and (not DynamicCollection) // + and (not std::is_array_v) // + and (not detail::is_std_array) // + and (detail::field_number_of <= detail::MAX_FIELD_NUM); + + template + struct HasDecomposeAndApply : std::false_type {}; + + template + struct HasDecomposeAndApply(), [](auto &&...) {}))>> + : std::true_type {}; + + template + concept CustomDecomposable = + not SimpleCodeableAggregate and HasDecomposeAndApply::value; + } // namespace scale diff --git a/scripts/generate_aggregate_hpp.sh b/scripts/generate_aggregate_hpp.sh new file mode 100755 index 0000000..021f866 --- /dev/null +++ b/scripts/generate_aggregate_hpp.sh @@ -0,0 +1,92 @@ +#!/bin/sh + +# This script modifies a specified C++ source file by replacing a generated section. +# It reads the file, finds the markers '-BEGIN-GENERATED-SECTION-' and '-END-GENERATED-SECTION-', +# generates C++ code based on a given integer N, and writes the modified content to the output file. +# If the markers are not found, the script exits with an error. +# If output file contents matches generated file contents, it won't be modified. +# Usage: ./generate_aggregate_hpp.sh +# Where is the path to the template C++ source file, is an integer between 0 and 1000, +# and is a path to the output file. + +# Validate input +if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then + echo "Usage: $0 " + exit 1 +fi + +PATH_HPP_IN="$1" +N="$2" +PATH_HPP_OUT="$3" +mkdir -p $(dirname "$PATH_HPP_OUT") + +if [ ! -f "$PATH_HPP_IN" ]; then + echo "Error: File '$PATH_HPP_IN' not found." + exit 1 +fi + +if echo "$N" | grep -q "[^0-9]" || [ "$N" -lt 0 ] || [ "$N" -gt 1000 ]; then + echo "N must be an integer between 0 and 1000" + exit 1 +fi + +# Find the section markers +BEGIN_LINE=$(awk '/-BEGIN-GENERATED-SECTION-/ {print NR; exit}' "$PATH_HPP_IN") +END_LINE=$(awk '/-END-GENERATED-SECTION-/ {print NR; exit}' "$PATH_HPP_IN") + +if [ -z "$BEGIN_LINE" ] || [ -z "$END_LINE" ]; then + echo "Error: Required markers not found in the file." + exit 1 +fi + +# Create temporary file +tempfile=$(mktemp) || { echo "Failed to create temporary file"; exit 1; } + +# Trap to ensure the temporary file is deleted in case of errors +trap 'rm -f "$tempfile"' EXIT + +# Write the modified content to the temporary file +awk -v begin="$BEGIN_LINE" -v end="$END_LINE" -v n="$N" ' +BEGIN { in_generated_section = 0; } +{ + if (NR == begin) { + print; # Print the BEGIN marker + in_generated_section = 1; + for (i = 1; i <= n; i++) { + printf " } else if constexpr (N == %d) {\n", i; + printf " auto &["; + for (j = 1; j <= i; j++) { + printf "v%d%s", j, (j < i ? ", " : ""); + } + printf "] = v;\n"; + printf " return f("; + for (j = 1; j <= i; j++) { + printf "v%d%s", j, (j < i ? ", " : ""); + } + printf ");\n"; + } + next; + } + if (NR == end) { + print; # Print the END marker + in_generated_section = 0; + next; + } + if (!in_generated_section) { + print; + } +}' "$PATH_HPP_IN" > "$tempfile" || { echo "Error processing file"; exit 1; } + +# Check if the file has changed before overwriting +if ! cmp -s "$tempfile" "$PATH_HPP_OUT"; then + mv "$tempfile" "$PATH_HPP_OUT" + echo "File '$PATH_HPP_OUT' successfully updated." +else + rm -f "$tempfile" + echo "No changes detected." +fi + +# Remove trap on success +trap - EXIT + +exit 0 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 24049fd..9871f93 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,12 +1,26 @@ +set(SCRIPT_PATH "${CMAKE_SOURCE_DIR}/scripts/generate_aggregate_hpp.sh") +set(AGGREGATE_HPP_IN "${CMAKE_SOURCE_DIR}/include/scale/detail/aggregate.hpp.in") +set(AGGREGATE_HPP "${CMAKE_BINARY_DIR}/include/scale/detail/aggregate.hpp") +add_custom_command( + OUTPUT ${AGGREGATE_HPP} + COMMAND ${CMAKE_COMMAND} -E echo "Running: ${SCRIPT_PATH} '${AGGREGATE_HPP_IN}' '${MAX_AGGREGATE_FIELDS}' '${AGGREGATE_HPP}'" + COMMAND ${SCRIPT_PATH} ${AGGREGATE_HPP_IN} ${MAX_AGGREGATE_FIELDS} ${AGGREGATE_HPP} + DEPENDS ${SCRIPT_PATH} ${AGGREGATE_HPP_IN} + COMMENT "Generating include/scale/detail/aggregate.hpp" + VERBATIM +) + add_library(scale encode_append.cpp scale_decoder_stream.cpp scale_encoder_stream.cpp scale_error.cpp + ${AGGREGATE_HPP} ) target_include_directories(scale PUBLIC - $ + $ + $ $ ) target_link_libraries(scale diff --git a/src/encode_append.cpp b/src/encode_append.cpp index 94dac3f..038ec35 100644 --- a/src/encode_append.cpp +++ b/src/encode_append.cpp @@ -7,8 +7,11 @@ #include #include -#include +#ifdef JAM_COMPATIBILITY_ENABLED #include +#else +#include +#endif namespace scale { @@ -23,10 +26,15 @@ namespace scale { */ outcome::result> extract_length_data( const std::vector &data) { - OUTCOME_TRY(len, scale::decode(data)); - auto new_len = (len + 1).convert_to(); - auto encoded_len = detail::compactLen(len.convert_to()); - auto encoded_new_len = detail::compactLen(new_len); + OUTCOME_TRY(len, scale::decode>(data)); + auto new_len = len + 1; +#ifdef JAM_COMPATIBILITY_ENABLED + auto encoded_len = detail::lengthOfEncodedJamCompactInteger(untagged(len)); + auto encoded_new_len = detail::lengthOfEncodedJamCompactInteger(new_len); +#else + auto encoded_len = detail::lengthOfEncodedCompactInteger(untagged(len)); + auto encoded_new_len = detail::lengthOfEncodedCompactInteger(new_len); +#endif return std::make_tuple(new_len, encoded_len, encoded_new_len); } @@ -45,7 +53,7 @@ namespace scale { const auto &[new_len, encoded_len, encoded_new_len] = extract_tuple; auto replace_len = [new_len = new_len](std::vector &dest) { - auto e = scale::encode(CompactInteger{new_len}).value(); + auto e = scale::encode(Length(new_len)).value(); std::move(e.begin(), e.end(), dest.begin()); }; diff --git a/src/scale_decoder_stream.cpp b/src/scale_decoder_stream.cpp index 31ee17e..3191fd3 100644 --- a/src/scale_decoder_stream.cpp +++ b/src/scale_decoder_stream.cpp @@ -7,18 +7,6 @@ #include namespace scale { - size_t ScaleDecoderStream::decodeLength() { -#ifdef JAM_COMPATIBILITY_ENABLED - size_t size = detail::decodeJamCompactInteger(*this); -#else - size_t size = detail::decodeCompactInteger(*this); -#endif - if (not hasMore(size)) { - raise(DecodeError::NOT_ENOUGH_DATA); - } - return size; - } - std::optional ScaleDecoderStream::decodeOptionalBool() { auto byte = nextByte(); switch (static_cast(byte)) { @@ -44,11 +32,6 @@ namespace scale { } } - ScaleDecoderStream &ScaleDecoderStream::operator>>(CompactInteger &v) { - v = decodeCompact(); - return *this; - } - ScaleDecoderStream &ScaleDecoderStream::operator>>(BitVec &v) { auto size = decodeCompact(); if (not hasMore((size + 7) / 8)) { diff --git a/src/scale_encoder_stream.cpp b/src/scale_encoder_stream.cpp index 1ab2d4c..a2bf684 100644 --- a/src/scale_encoder_stream.cpp +++ b/src/scale_encoder_stream.cpp @@ -28,7 +28,7 @@ namespace scale { } ScaleEncoderStream &ScaleEncoderStream::operator<<(const BitVec &v) { - *this << CompactInteger{v.bits.size()}; + *this << Length(v.bits.size()); size_t i = 0; uint8_t byte = 0; for (auto bit : v.bits) { @@ -55,18 +55,6 @@ namespace scale { return *this; } -#ifdef JAM_COMPATIBILITY_ENABLED - ScaleEncoderStream &ScaleEncoderStream::operator<<(const CompactInteger &v) { - detail::encodeJamCompactInteger(v, *this); - return *this; - } -#else - ScaleEncoderStream &ScaleEncoderStream::operator<<(const CompactInteger &v) { - detail::encodeCompactInteger(v, *this); - return *this; - } -#endif - ScaleEncoderStream &ScaleEncoderStream::encodeOptionalBool( const std::optional &v) { auto result = OptionalBool::OPT_TRUE; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9d420ff..00f2575 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -27,11 +27,10 @@ function(addtest test_name) ${ARGN} ) target_link_libraries(${test_name} - GTest::gtest GTest::gmock_main ) target_include_directories(${test_name} PRIVATE - ${PROJECT_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/include ) set(xml_output "--gtest_output=xml:${CMAKE_BINARY_DIR}/xunit/xunit-${test_name}.xml") add_test( @@ -155,6 +154,13 @@ target_link_libraries(scale_encode_counter_test scale ) +addtest(scale_custom_decomposing_test + scale_custom_decomposing_test.cpp +) +target_link_libraries(scale_custom_decomposing_test + scale +) + if (CUSTOM_CONFIG_SUPPORT) addtest(scale_tune_test scale_tune_test.cpp diff --git a/test/installation/CMakeLists.txt b/test/installation/CMakeLists.txt index 765c2a6..85c75d4 100644 --- a/test/installation/CMakeLists.txt +++ b/test/installation/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.12) -include(../../cmake/HunterGate.cmake) +include(../../cmake/Hunter/HunterGate.cmake) HunterGate( URL https://github.com/qdrvm/hunter/archive/refs/tags/v0.23.257-qdrvm10.tar.gz diff --git a/test/scale_array_test.cpp b/test/scale_array_test.cpp index 76b8db3..bcd1bba 100644 --- a/test/scale_array_test.cpp +++ b/test/scale_array_test.cpp @@ -27,8 +27,8 @@ void testArray() { Array testee; std::fill(testee.begin(), testee.end(), value); - auto data = EXPECT_OK(encode(testee)); - auto result = EXPECT_OK(decode(data)); + ASSERT_OUTCOME_SUCCESS(data, encode(testee)); + ASSERT_OUTCOME_SUCCESS(result, decode(data)); EXPECT_EQ(testee, result); } diff --git a/test/scale_boolean_test.cpp b/test/scale_boolean_test.cpp index c259a5f..ab0a522 100644 --- a/test/scale_boolean_test.cpp +++ b/test/scale_boolean_test.cpp @@ -41,20 +41,16 @@ struct ThreeBooleans { bool b3 = false; }; -template > -Stream &operator>>(Stream &s, ThreeBooleans &v) { - return s >> v.b1 >> v.b2 >> v.b3; -} - /** * @given byte array containing values {0, 1, 2} * @when scale::decode function is applied sequentially * @then it returns false, true and kUnexpectedValue error correspondingly, * and in the end no more bytes left in stream */ -TEST(Scale, fixedwidthDecodeBoolFail) { +TEST(ScaleBoolTest, fixedwidthDecodeBoolFail) { auto bytes = ByteArray{0, 1, 2}; - EXPECT_EC(scale::decode(bytes), DecodeError::UNEXPECTED_VALUE); + ASSERT_OUTCOME_ERROR(scale::decode(bytes), + DecodeError::UNEXPECTED_VALUE); } /** @@ -63,9 +59,9 @@ TEST(Scale, fixedwidthDecodeBoolFail) { * @then it returns false, true and kUnexpectedValue error correspondingly, * and in the end no more bytes left in stream */ -TEST(Scale, fixedwidthDecodeBoolSuccess) { +TEST(ScaleBoolTest, fixedwidthDecodeBoolSuccess) { auto bytes = ByteArray{0, 1, 0}; - auto res = EXPECT_OK(scale::decode(bytes)); + ASSERT_OUTCOME_SUCCESS(res, scale::decode(bytes)); ASSERT_EQ(res.b1, false); ASSERT_EQ(res.b2, true); ASSERT_EQ(res.b3, false); diff --git a/test/scale_collection_test.cpp b/test/scale_collection_test.cpp index 3989030..14626ad 100644 --- a/test/scale_collection_test.cpp +++ b/test/scale_collection_test.cpp @@ -13,6 +13,7 @@ using scale::CompactInteger; using scale::decode; using scale::DecodeError; using scale::encode; +using scale::Length; using scale::ScaleDecoderStream; using scale::ScaleEncoderStream; @@ -33,7 +34,7 @@ auto encodeLen = [](size_t i) { */ TEST(CollectionTest, encodeCollectionOf80) { for (size_t length = 60; length <= 130; ++length) { - ByteArray collection(length); + ByteArray collection; collection.reserve(length); for (auto i = 0; i < length; ++i) { collection.push_back(i % 256); @@ -136,7 +137,16 @@ TEST(CollectionTest, encodeCollectionUint16) { ASSERT_TRUE(std::ranges::equal(out, match)); } -struct TestStruct : std::vector {}; +struct TestStruct : public std::vector { + // friend ScaleEncoderStream &operator<<(ScaleEncoderStream &s, + // const TestStruct &test_struct) { + // return s << static_cast &>(test_struct); + // } + // friend ScaleDecoderStream &operator>>(ScaleDecoderStream &s, + // TestStruct &test_struct) { + // return s >> static_cast &>(test_struct); + // } +}; /** * @given collection of items of type uint16_t, derived from std::vector @@ -282,7 +292,7 @@ TEST(CollectionTest, encodeLongCollectionUint16) { auto &&out = s.to_vector(); auto stream = ScaleDecoderStream(out); - CompactInteger res{}; + Length res{}; ASSERT_NO_THROW(stream >> res); ASSERT_EQ(res, length); @@ -324,7 +334,7 @@ TEST(CollectionTest, encodeVeryLongCollectionUint8) { auto &&out = s.to_vector(); auto stream = ScaleDecoderStream(out); - CompactInteger bi{}; + Length bi{}; ASSERT_NO_THROW(stream >> bi); ASSERT_EQ(bi, 1048576); @@ -438,8 +448,13 @@ TEST(CollectionTest, encodeExplicitlyDefinedAsDynamic) { decoded.begin(), decoded.end(), collection.begin(), collection.end())); } -struct ImplicitlyDefinedAsStatic : public std::array { - using Collection = std::array; +struct ImplicitlyDefinedAsStatic : public std::vector { + using Collection = std::vector; + using Collection::Collection; + + private: + using std::vector::insert; + using std::vector::emplace; }; TEST(CollectionTest, encodeImplicitlyDefinedAsStatic) { diff --git a/test/scale_compact_test.cpp b/test/scale_compact_test.cpp index 832cc93..cd277cc 100644 --- a/test/scale_compact_test.cpp +++ b/test/scale_compact_test.cpp @@ -9,20 +9,22 @@ #include using scale::ByteArray; -using scale::CompactInteger; using scale::decode; +using scale::encode; using scale::ScaleDecoderStream; using scale::ScaleEncoderStream; +// Maximum available weight integer +using Compact = scale::Compact; + /** * value parameterized tests */ class CompactTest - : public ::testing::TestWithParam> { + : public testing::TestWithParam> { public: - static std::pair pair(CompactInteger v, - ByteArray m) { - return std::make_pair(CompactInteger(std::move(v)), std::move(m)); + static std::pair pair(Compact v, ByteArray m) { + return std::make_pair(Compact(std::move(v)), std::move(m)); } protected: @@ -35,9 +37,9 @@ class CompactTest * @then encoded value matches predefined buffer */ TEST_P(CompactTest, EncodeSuccess) { - const auto &[value, match] = GetParam(); - ASSERT_NO_THROW(s << value) << "Exception while encoding"; - ASSERT_EQ(s.to_vector(), match) << "Encoding fail"; + const auto &[value, expected] = GetParam(); + ASSERT_OUTCOME_SUCCESS(actual, encode(value)); + ASSERT_EQ(actual, expected) << "Encoding fail"; } /** @@ -46,17 +48,16 @@ TEST_P(CompactTest, EncodeSuccess) { * @then decoded value matches predefined value */ TEST_P(CompactTest, DecodeSuccess) { - const auto &[value_match, bytes] = GetParam(); - ScaleDecoderStream s(bytes); - CompactInteger v{}; - ASSERT_NO_THROW(s >> v) << "Exception while decoding"; - ASSERT_EQ(v, value_match) << "Decoding fail"; + const auto &[expected, bytes] = GetParam(); + ASSERT_OUTCOME_SUCCESS(actual, decode(bytes)); + ASSERT_EQ(actual, expected) << "Decoding fail"; } #ifdef JAM_COMPATIBILITY_ENABLED #define BIGGEST_INT_FOR_COMPACT_REPRESENTATION \ - ((CompactInteger(1) << (8 * sizeof(size_t))) - 1) + ((Compact(1) << (8 * sizeof(size_t))) - 1) + INSTANTIATE_TEST_SUITE_P( CompactTestCases, CompactTest, @@ -89,92 +90,58 @@ INSTANTIATE_TEST_SUITE_P( #else -#define BIGGEST_INT_FOR_COMPACT_REPRESENTATION \ - (CompactInteger(1) << (8 * 67)) - 1 +#define BIGGEST_INT_FOR_COMPACT_REPRESENTATION ((Compact(1) << (8 * 67)) - 1) INSTANTIATE_TEST_SUITE_P( CompactTestCases, CompactTest, ::testing::Values( - // 0 is min compact integer value, negative values are not allowed + // clang-format off + // 0: 0 is min compact integer value, negative values are not allowed CompactTest::pair(0, {0}), - // 1 is encoded as 4 + // 1: 1 is encoded as 4 CompactTest::pair(1, {4}), - // max 1 byte value + // 2: max 1 byte value CompactTest::pair(63, {252}), - // min 2 bytes value + // 3: min 2 bytes value CompactTest::pair(64, {1, 1}), - // some 2 bytes value + // 4: some 2 bytes value CompactTest::pair(255, {253, 3}), - // some 2 bytes value + // 5: some 2 bytes value CompactTest::pair(511, {253, 7}), - // max 2 bytes value + // 6: max 2 bytes value CompactTest::pair(16383, {253, 255}), - // min 4 bytes value + // 7: min 4 bytes value CompactTest::pair(16384, {2, 0, 1, 0}), - // some 4 bytes value + // 8: some 4 bytes value CompactTest::pair(65535, {254, 255, 3, 0}), - // max 4 bytes value - CompactTest::pair(1073741823ul, {254, 255, 255, 255}), - // some multibyte integer - CompactTest::pair( - CompactInteger("1234567890123456789012345678901234567890"), - {0b110111, - 210, - 10, - 63, - 206, - 150, - 95, - 188, - 172, - 184, - 243, - 219, - 192, - 117, - 32, - 201, - 160, - 3}), - // min multibyte integer - CompactTest::pair(1073741824, {3, 0, 0, 0, 64}), - // max multibyte integer: 2^536 - 1 + // 9: max 4 bytes value + CompactTest::pair(1073741823ul, {0b1111'1110, 0xff, 0xff, 0xff}), + // 10: min multibyte integer + CompactTest::pair(1073741824, {0b0000'0011, 0x00, 0x00, 0x00, 0b0100'0000}), + // 11: some multibyte integer + CompactTest::pair(1ull<<35, {0b0000'0111, 0x00, 0x00, 0x00, 0x00, 0b0000'1000 }), + // 12: some multibyte integer + CompactTest::pair((1ull<<35)+1, {0b0000'0111, 0x01, 0x00, 0x00, 0x00, 0b0000'1000 }), + // 13: max multibyte integer: 2^536 - 1 CompactTest::pair(BIGGEST_INT_FOR_COMPACT_REPRESENTATION, std::vector(68, 0xFF)))); #endif -/** - * Negative tests - */ - -/** - * @given a negative value -1 - * (negative values are not supported by compact encoding) - * @when trying to encode this value - * @then obtain error - */ -TEST(ScaleCompactTest, EncodeNegativeIntegerFails) { - CompactInteger v(-1); - ScaleEncoderStream out{}; - ASSERT_ANY_THROW((out << v)); - ASSERT_EQ(out.to_vector().size(), 0); // nothing was written to buffer -} - /** * @given a CompactInteger value exceeding the range supported by scale - * @when encode it a directly as CompactInteger + * @when encode it directly as CompactInteger * @then obtain kValueIsTooBig error */ TEST(ScaleCompactTest, EncodeOutOfRangeBigIntegerFails) { // try to encode out of range big integer value MAX_BIGINT + 1 // too big value, even for big integer case // we are going to have kValueIsTooBig error - CompactInteger v = BIGGEST_INT_FOR_COMPACT_REPRESENTATION + 1; + Compact v = BIGGEST_INT_FOR_COMPACT_REPRESENTATION + 1; ScaleEncoderStream out; - ASSERT_ANY_THROW((out << v)); // value is too big, it isn't encoded + ASSERT_ANY_THROW(out << v); // value is too big, it isn't encoded ASSERT_EQ(out.to_vector().size(), 0); // nothing was written to buffer } @@ -183,9 +150,10 @@ TEST(ScaleCompactTest, EncodeOutOfRangeBigIntegerFails) { * @when apply decodeInteger * @then get kNotEnoughData error */ -TEST(Scale, compactDecodeBigIntegerError) { - auto bytes = ByteArray{255, 255, 255, 255}; - EXPECT_EC(decode(bytes), scale::DecodeError::NOT_ENOUGH_DATA); +TEST(ScaleCompactTest, compactDecodeBigIntegerError) { + auto bytes = ByteArray{0xff, 0xff, 0xff, 0xff}; + ASSERT_OUTCOME_ERROR(decode(bytes), + scale::DecodeError::NOT_ENOUGH_DATA); } /** @@ -193,10 +161,10 @@ TEST(Scale, compactDecodeBigIntegerError) { * @when decode compact * @then error */ -struct RedundantCompactTest : ::testing::TestWithParam {}; +struct RedundantCompactTest : testing::TestWithParam {}; TEST_P(RedundantCompactTest, DecodeError) { - EXPECT_EC(scale::decode(GetParam()), - scale::DecodeError::REDUNDANT_COMPACT_ENCODING); + ASSERT_OUTCOME_ERROR(scale::decode(GetParam()), + scale::DecodeError::REDUNDANT_COMPACT_ENCODING); } #ifdef JAM_COMPATIBILITY_ENABLED @@ -206,22 +174,22 @@ INSTANTIATE_TEST_SUITE_P( RedundantCompactTest, ::testing::Values( // clang-format off - /* 1 */ ByteArray{0b10000000, 0b00000000}, - /* 2 */ ByteArray{0b10000000, 0b00111111}, - /* 3 */ ByteArray{0b11000000, 0b00000000, 0b00000000}, - /* 4 */ ByteArray{0b11000000, 0b11111111, 0b00011111}, - /* 5 */ ByteArray{0b11100000, 0b00000000, 0b00000000, 0b00000000}, - /* 6 */ ByteArray{0b11100000, 0b11111111, 0b11111111, 0b00001111}, - /* 7 */ ByteArray{0b11110000, 0b00000000, 0b00000000, 0b00000000, 0b00000000}, - /* 8 */ ByteArray{0b11110000, 0b11111111, 0b11111111, 0b11111111, 0b00000111}, - /* 9 */ ByteArray{0b11111000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000}, - /* 10 */ ByteArray{0b11111000, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b00000011}, - /* 11 */ ByteArray{0b11111100, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000}, - /* 12 */ ByteArray{0b11111100, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b00000001}, - /* 13 */ ByteArray{0b11111110, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000}, - /* 14 */ ByteArray{0b11111110, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b00000000}, - /* 15 */ ByteArray{0b11111111, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000}, - /* 16 */ ByteArray{0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b00000000} + /* 0 */ ByteArray{0b10000000, 0b00000000}, + /* 1 */ ByteArray{0b10000000, 0b00111111}, + /* 2 */ ByteArray{0b11000000, 0b00000000, 0b00000000}, + /* 3 */ ByteArray{0b11000000, 0b11111111, 0b00011111}, + /* 4 */ ByteArray{0b11100000, 0b00000000, 0b00000000, 0b00000000}, + /* 5 */ ByteArray{0b11100000, 0b11111111, 0b11111111, 0b00001111}, + /* 6 */ ByteArray{0b11110000, 0b00000000, 0b00000000, 0b00000000, 0b00000000}, + /* 7 */ ByteArray{0b11110000, 0b11111111, 0b11111111, 0b11111111, 0b00000111}, + /* 8 */ ByteArray{0b11111000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000}, + /* 9 */ ByteArray{0b11111000, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b00000011}, + /* 10 */ ByteArray{0b11111100, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000}, + /* 11 */ ByteArray{0b11111100, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b00000001}, + /* 12 */ ByteArray{0b11111110, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000}, + /* 13 */ ByteArray{0b11111110, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b00000000}, + /* 14 */ ByteArray{0b11111111, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000}, + /* 15 */ ByteArray{0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b00000000} // // clang-format on )); diff --git a/test/scale_convenience_functions_test.cpp b/test/scale_convenience_functions_test.cpp index 23647d9..06bb864 100644 --- a/test/scale_convenience_functions_test.cpp +++ b/test/scale_convenience_functions_test.cpp @@ -20,16 +20,6 @@ struct TestStruct { } }; -template > -Stream &operator<<(Stream &s, const TestStruct &test_struct) { - return s << test_struct.a << test_struct.b; -} - -template > -Stream &operator>>(Stream &s, TestStruct &test_struct) { - return s >> test_struct.a >> test_struct.b; -} - /** * @given encoded TestStruct * @when it is decoded back @@ -38,8 +28,8 @@ Stream &operator>>(Stream &s, TestStruct &test_struct) { TEST(ScaleConvenienceFuncsTest, EncodeSingleValidArgTest) { TestStruct s1{"some_string", 42}; - auto encoded = EXPECT_OK(encode(s1)); - auto decoded = EXPECT_OK(decode(encoded)); + ASSERT_OUTCOME_SUCCESS(encoded, encode(s1)); + ASSERT_OUTCOME_SUCCESS(decoded, decode(encoded)); ASSERT_EQ(decoded, s1); } @@ -53,8 +43,9 @@ TEST(ScaleConvenienceFuncsTest, EncodeSeveralValidArgTest) { std::string expected_string = "some_string"; int expected_int = 42; - auto encoded = EXPECT_OK(encode(expected_string, expected_int)); - auto decoded = EXPECT_OK(decode(encoded)); + ASSERT_OUTCOME_SUCCESS(encoded, + encode(std::tie(expected_string, expected_int))); + ASSERT_OUTCOME_SUCCESS(decoded, decode(encoded)); ASSERT_EQ(decoded.a, expected_string); ASSERT_EQ(decoded.b, expected_int); diff --git a/test/scale_custom_decomposing_test.cpp b/test/scale_custom_decomposing_test.cpp new file mode 100644 index 0000000..c8ca8de --- /dev/null +++ b/test/scale_custom_decomposing_test.cpp @@ -0,0 +1,66 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +using scale::decode; +using scale::encode; + +struct CustomDecomposableObject { + CustomDecomposableObject() : a(0xff), b(0xff), c(0xff), d(0xff), e(0xff) {} + CustomDecomposableObject( + uint8_t a, uint8_t b, uint8_t c, uint8_t d, uint8_t e) + : a(a), b(b), c(c), d(d), e(e) {}; + + uint8_t a; + uint8_t b; + uint8_t c; + uint8_t d; + uint8_t e; + + bool operator==(const CustomDecomposableObject &other) const = default; + + SCALE_CUSTOM_DECOMPOSITION(CustomDecomposableObject, b, c, d); +}; + +TEST(CustomDecomposable, encode) { + CustomDecomposableObject x(1, 2, 3, 4, 5); + + std::vector expected = {x.b, x.c, x.d}; + + { + scale::ScaleEncoderStream s; + s << x; + auto actual = s.to_vector(); + EXPECT_EQ(expected, actual); + } + + { + ASSERT_OUTCOME_SUCCESS(actual, scale::encode(x)); + EXPECT_EQ(expected, actual); + } +} + +TEST(CustomDecomposable, decode) { + std::vector data = {1, 2, 3}; + + CustomDecomposableObject expected(0xff, 1, 2, 3, 0xff); + + { + scale::ScaleDecoderStream s(data); + CustomDecomposableObject actual{0xff, 0xff, 0xff, 0xff, 0xff}; + s >> actual; + EXPECT_EQ(expected, actual); + } + + { + ASSERT_OUTCOME_SUCCESS(actual, + scale::decode(data)); + EXPECT_EQ(expected, actual); + } +} diff --git a/test/scale_encode_append_test.cpp b/test/scale_encode_append_test.cpp index 3cc4612..05353ce 100644 --- a/test/scale_encode_append_test.cpp +++ b/test/scale_encode_append_test.cpp @@ -5,27 +5,32 @@ */ #include + +#include + #include #include -namespace scale { - using Values = std::vector; +using scale::append_or_new_vec; +using scale::ByteArray; - TEST(EncodeAppend, Empty) { - auto value = 1; - ByteArray out; - EXPECT_FALSE( - append_or_new_vec(out, scale::encode(value).value()).has_error()); - EXPECT_EQ(out, scale::encode(Values{value}).value()); - } +using Values = std::vector; + +TEST(EncodeAppend, Expend_from0_to1024) { + Values values; + + auto expandable = scale::encode(values).value(); + + uint16_t value = 0; + for ([[maybe_unused]] auto i : std::views::iota(0, 1024)) { + values.emplace_back(++value); + + auto expending = scale::encode(value).value(); + ASSERT_OUTCOME_SUCCESS(append_or_new_vec(expandable, expending)); + + auto direct_encoded = scale::encode(values).value(); - TEST(EncodeAppend, Append) { - Values values{0, 1, 2, 3, 4}; - auto value = 5; - auto out = scale::encode(values).value(); - values.emplace_back(value); - EXPECT_FALSE( - append_or_new_vec(out, scale::encode(value).value()).has_error()); - EXPECT_EQ(out, scale::encode(values).value()); + ASSERT_EQ(expandable.size(), direct_encoded.size()); + EXPECT_EQ(expandable, direct_encoded); } -} // namespace scale +} diff --git a/test/scale_encode_counter_test.cpp b/test/scale_encode_counter_test.cpp index f63463e..99c0303 100644 --- a/test/scale_encode_counter_test.cpp +++ b/test/scale_encode_counter_test.cpp @@ -22,11 +22,6 @@ struct TestStruct { std::string y; }; -template > -Stream &operator<<(Stream &s, const TestStruct &v) { - return s << v.x << v.y; -} - // helper for same kind checks #define SIZE(bytes) ASSERT_EQ(s.size(), bytes) diff --git a/test/scale_enum_test.cpp b/test/scale_enum_test.cpp index d85ac10..c5142ab 100644 --- a/test/scale_enum_test.cpp +++ b/test/scale_enum_test.cpp @@ -13,7 +13,6 @@ using scale::ScaleEncoderStream; template class EnumTest : public ::testing::Test { - public: protected: const static std::string enum_name; const static std::vector values; @@ -71,7 +70,6 @@ TYPED_TEST(EnumTest, CorrectEncoding) { template class InvalidEnumTest : public ::testing::Test { - public: protected: const static std::string enum_name; const static std::vector> invalid_values; diff --git a/test/scale_fixed_test.cpp b/test/scale_fixed_test.cpp index fb907e3..32e0a78 100644 --- a/test/scale_fixed_test.cpp +++ b/test/scale_fixed_test.cpp @@ -314,3 +314,50 @@ INSTANTIATE_TEST_SUITE_P(Uint64TestCases, Uint64Test, ::testing::Values(Uint64Test::make_pair( 578437695752307201ull, {1, 2, 3, 4, 5, 6, 7, 8}))); + +/** + * @brief class for testing uint128_t encode and decode + */ +class Uint128Test : public IntegerTest {}; + +/** + * @given a number and match buffer + * @when given number being encoded by ScaleEncoderStream + * @then resulting buffer matches predefined one + */ +TEST_P(Uint128Test, EncodeSuccess) { + auto [value, match] = GetParam(); + ScaleEncoderStream s; + ASSERT_NO_THROW((s << value)); + ASSERT_EQ(s.to_vector(), match); +} + +/** + * @given encoded sequence and match number + * @when a number is decoded from given bytes by ScaleDecoderStream + * @then resulting number matches predefined one + */ +TEST_P(Uint128Test, DecodeSuccess) { + auto [value, match] = GetParam(); + ScaleDecoderStream s(match); + value_type v{0}; + ASSERT_NO_THROW((s >> v)); + ASSERT_EQ(v, value); +} + +INSTANTIATE_TEST_SUITE_P( + Uint128TestCases, + Uint128Test, + ::testing::Values( + // clang-format off + Uint128Test::make_pair( + scale::uint128_t("1"), // 1 + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}), + Uint128Test::make_pair( + scale::uint128_t("18446744073709551616"), // 2^64 + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}), + Uint128Test::make_pair( // 2^128-1 (uint128 max) + scale::uint128_t("340282366920938463463374607431768211455"), + {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}) + // clang-format on + )); diff --git a/test/scale_optional_test.cpp b/test/scale_optional_test.cpp index d1ab023..224ed7e 100644 --- a/test/scale_optional_test.cpp +++ b/test/scale_optional_test.cpp @@ -160,11 +160,6 @@ struct FourOptBools { std::optional b4; }; -template > -Stream &operator>>(Stream &s, FourOptBools &v) { - return s >> v.b1 >> v.b2 >> v.b3 >> v.b4; -} - /** * @given byte array containing series of encoded optional bool values * where last byte is incorrect for optional bool type @@ -174,7 +169,8 @@ Stream &operator>>(Stream &s, FourOptBools &v) { TEST(Scale, DecodeOptionalBoolFail) { auto bytes = ByteArray{0, 1, 2, 3}; - EXPECT_EC(decode(bytes), DecodeError::UNEXPECTED_VALUE); + ASSERT_OUTCOME_ERROR(decode(bytes), + DecodeError::UNEXPECTED_VALUE); } /** @@ -186,7 +182,7 @@ TEST(Scale, DecodeOptionalBoolSuccess) { auto bytes = ByteArray{0, 1, 2, 1}; using optbool = std::optional; - auto res = EXPECT_OK(decode(bytes)); + ASSERT_OUTCOME_SUCCESS(res, decode(bytes)); ASSERT_EQ(res.b1, std::nullopt); ASSERT_EQ(res.b2, optbool(true)); ASSERT_EQ(res.b3, optbool(false)); @@ -213,10 +209,10 @@ TEST(Scale, DecodeNullopt) { ByteArray encoded_nullopt{0}; using OptionalInt = std::optional; - auto int_opt = EXPECT_OK(decode(encoded_nullopt)); + ASSERT_OUTCOME_SUCCESS(int_opt, decode(encoded_nullopt)); EXPECT_EQ(int_opt, std::nullopt); using OptionalTuple = std::optional>; - auto tuple_opt = EXPECT_OK(decode(encoded_nullopt)); + ASSERT_OUTCOME_SUCCESS(tuple_opt, decode(encoded_nullopt)); EXPECT_EQ(tuple_opt, std::nullopt); } diff --git a/test/scale_tune_test.cpp b/test/scale_tune_test.cpp index 62045a4..4156d22 100644 --- a/test/scale_tune_test.cpp +++ b/test/scale_tune_test.cpp @@ -5,17 +5,19 @@ */ #include +#include #include #ifndef CUSTOM_CONFIG_ENABLED #error \ -"This file should not be compiled, because custom config support is not enabed" + "This file should not be compiled, because custom config support is not enabed" #endif using scale::ByteArray; -using scale::CompactInteger; +using scale::Compact; using scale::decode; using scale::encode; +using scale::Length; using scale::ScaleDecoderStream; using scale::ScaleEncoderStream; @@ -36,7 +38,7 @@ struct Object { auto mul = s.getConfig().multi; auto add = s.getConfig().add; - s << CompactInteger{x.buff.size()}; + s << Length(x.buff.size()); for (uint8_t i : x.buff) { uint8_t x = i * mul + add; s << x; @@ -47,9 +49,9 @@ struct Object { friend ScaleDecoderStream &operator>>(ScaleDecoderStream &s, Object &x) { auto mul = s.getConfig().multi; auto add = s.getConfig().add; - CompactInteger size; + Length size; s >> size; - x.buff.resize(size.convert_to()); + x.buff.resize(untagged(size)); for (uint8_t &i : x.buff) { uint8_t x; s >> x; diff --git a/test/scale_tuple_test.cpp b/test/scale_tuple_test.cpp index fed7c3d..385028b 100644 --- a/test/scale_tuple_test.cpp +++ b/test/scale_tuple_test.cpp @@ -56,7 +56,7 @@ TEST(Scale, EncodeDecodeTupleSuccess) { tuple_type_t tuple = std::make_tuple(uint8_t(1), uint16_t(3), uint8_t(2), uint32_t(4)); - auto actual_bytes = EXPECT_OK(scale::encode(tuple)); - auto decoded = EXPECT_OK(scale::decode(actual_bytes)); + ASSERT_OUTCOME_SUCCESS(actual_bytes, scale::encode(tuple)); + ASSERT_OUTCOME_SUCCESS(decoded, scale::decode(actual_bytes)); ASSERT_EQ(decoded, tuple); } diff --git a/vcpkg-overlay/qtils/portfile.cmake b/vcpkg-overlay/qtils/portfile.cmake index e4653e3..31b18f3 100644 --- a/vcpkg-overlay/qtils/portfile.cmake +++ b/vcpkg-overlay/qtils/portfile.cmake @@ -2,8 +2,8 @@ vcpkg_check_linkage(ONLY_STATIC_LIBRARY) vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO qdrvm/qtils - REF de62454d12e250574577232ee7ede4406c4c22cb - SHA512 7b91ea268fb2c9204c1b584d2b433bd93d8d1946ba5d05ddf119b955fdfdb76abbe47c0009a4b186f7968320b99044abfd58315a128c98cf65f57c870ba51112 + REF 16e7c819dd50af2f64e2d319b918d0d815332266 + SHA512 02c613ee2870b8b5956f7a0494a5a49b9ba499801541dc16d5fc915480111da56e6dd998ca7d43803197006b9a74ec8201ae9952de97810849a7e208e8f4b0dc ) vcpkg_cmake_configure(SOURCE_PATH "${SOURCE_PATH}") vcpkg_cmake_install()