Skip to content

Commit

Permalink
Merge pull request #2 from soramitsu/feature/enum-codecs
Browse files Browse the repository at this point in the history
Enum codecs
  • Loading branch information
Harrm authored Nov 7, 2021
2 parents c9f6355 + 5d9161a commit c55e98f
Show file tree
Hide file tree
Showing 23 changed files with 479 additions and 243 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
matrix:
options:
- name: "Linux: gcc-9"
run: ./scripts/build.sh -DCMAKE_CXX_COMPIILER=g++-9
run: ./scripts/build.sh -DCMAKE_CXX_COMPILER=g++-9
- name: "Linux: clang-10"
run: ./scripts/build.sh -DCMAKE_CXX_COMPILER=clang++-10
name: "${{ matrix.options.name }}"
Expand Down
20 changes: 16 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ cmake_minimum_required(VERSION 3.12)
include(${CMAKE_CURRENT_LIST_DIR}/cmake/HunterGate.cmake)
set(HUNTER_STATUS_DEBUG ON)
HunterGate(
URL https://github.com/soramitsu/soramitsu-hunter/archive/9ca72322e8d9de70d360dc7f371b223d32999123.zip
SHA1 37cc1150526fb9c5dcf197f918e5d9aab611823f
URL https://github.com/soramitsu/soramitsu-hunter/archive/tags/v0.23.257-soramitsu17.tar.gz
SHA1 c7ccd337314b27485b75d0f0f5d5b42e7e3c2629
)

project(Scale CXX)
project(Scale LANGUAGES CXX VERSION 1.0.0)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
Expand All @@ -33,7 +33,7 @@ add_subdirectory(src)
if (BUILD_TESTS)
enable_testing()
add_subdirectory(test ${CMAKE_BINARY_DIR}/tests_bin)
endif()
endif ()

###############################################################################
# INSTALLATION
Expand All @@ -55,6 +55,18 @@ install(
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

include(CMakePackageConfigHelpers)

write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/scaleConfigVersion.cmake
COMPATIBILITY SameMajorVersion
)

install(
FILES ${CMAKE_CURRENT_BINARY_DIR}/scaleConfigVersion.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/scale
)

install(
EXPORT scaleConfig
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/scale
Expand Down
164 changes: 75 additions & 89 deletions include/scale/detail/fixed_width_integer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,108 +13,94 @@

#include <boost/endian/arithmetic.hpp>

#include <scale/scale_error.hpp>
#include <scale/outcome/outcome_throw.hpp>
#include <scale/scale_error.hpp>
#include <scale/unreachable.hpp>

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 <class T, class S, typename I = std::decay_t<T>,
typename = std::enable_if_t<std::is_integral<I>::value>>
void encodeInteger(T value, S &out) { // no need to take integers by &&
constexpr size_t size = sizeof(I);
constexpr size_t bits = size * 8;
boost::endian::endian_buffer<boost::endian::order::little, I, bits> buf{};
buf = value; // cannot initialize, only assign
for (size_t i = 0; i < size; ++i) {
// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
out << buf.data()[i];
/**
* 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 <class T,
class S,
typename I = std::decay_t<T>,
typename = std::enable_if_t<std::is_integral<I>::value>>
void encodeInteger(T value, S &out) { // no need to take integers by &&
constexpr size_t size = sizeof(I);
constexpr size_t bits = size * 8;
boost::endian::endian_buffer<boost::endian::order::little, I, bits> buf{};
buf = value; // cannot initialize, only assign
for (size_t i = 0; i < size; ++i) {
// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
out << buf.data()[i];
}
}
}

/**
* @brief decodeInteger function decodes integer from stream
* @tparam T integer type
* @param stream source stream
* @return decoded value or error
*/
template <class T, class S, typename I = std::decay_t<T>,
typename = std::enable_if_t<std::is_integral_v<I>>>
I decodeInteger(S &stream) {
constexpr size_t size = sizeof(I);
static_assert(size <= 8);
/**
* @brief decodeInteger function decodes integer from stream
* @tparam T integer type
* @param stream source stream
* @return decoded value or error
*/
template <class T,
class S,
typename I = std::decay_t<T>,
typename = std::enable_if_t<std::is_integral_v<I>>>
I decodeInteger(S &stream) {
constexpr size_t size = sizeof(I);
static_assert(size <= 8);

// clang-format off
// sign bit = 2^(num_bits - 1)
static constexpr std::array<uint64_t, 8> sign_bit = {
0x80, // 1 byte
0x8000, // 2 bytes
0x800000, // 3 bytes
0x80000000, // 4 bytes
0x8000000000, // 5 bytes
0x800000000000, // 6 bytes
0x80000000000000, // 7 bytes
0x8000000000000000 // 8 bytes
static constexpr auto sign_bit = [](size_t num_bytes) -> uint64_t {
return 0x80ul << (num_bytes * 8);
};

static constexpr std::array<uint64_t, 8> multiplier = {
0x1, // 2^0
0x100, // 2^8
0x10000, // 2^16
0x1000000, // 2^24
0x100000000, // 2^32
0x10000000000, // 2^40
0x1000000000000, // 2^48
0x100000000000000 // 2^56
static constexpr auto multiplier = [](size_t num_bytes) -> uint64_t {
return 0x1ul << (num_bytes * 8);
};
// clang-format on

if (!stream.hasMore(size)) {
raise(DecodeError::NOT_ENOUGH_DATA);
UNREACHABLE
}

// get integer as 4 bytes from little-endian stream
// and represent it as native-endian unsigned int eger
uint64_t v = 0u;

for (size_t i = 0; i < size; ++i) {
// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index)
v += multiplier[i] * static_cast<uint64_t>(stream.nextByte());
if (!stream.hasMore(size)) {
raise(DecodeError::NOT_ENOUGH_DATA);
UNREACHABLE
}

// get integer as 8 bytes from little-endian stream
// and represent it as native-endian unsigned integer
uint64_t v = 0u;

for (size_t i = 0; i < size; ++i) {
v += multiplier(i) * static_cast<uint64_t>(stream.nextByte());
}
// now we have an uint64 native-endian value
// which can store either a signed or an unsigned value

// if the value is actually unsigned, we know that is not greater than
// the max value for type T, so static_cast<T>(v) is safe

// if it is signed and positive, it is also ok
// we can be sure that it is less than max_value<T>/2.
// To check if it is negative we check if the sign bit is present
// in the unsigned representation. (which is true when the value is greater
// than 2^(size_in_bits-1)
bool is_positive_signed = v < sign_bit(size - 1);
if (std::is_unsigned<I>() || is_positive_signed) {
return static_cast<I>(v);
}

// T is a signed integer type and the value v is negative.
// A value is negative, which means that (-x),
// where (-x) is positive, is smaller than sign_bits[size-1].
// Find this x, safely cast to a positive signed and negate the result.
// the bitwise negation operation affects higher bits as well,
// but it doesn't spoil the result.
// static_cast to a smaller type cuts these higher bits off.
I sv = -static_cast<I>((~v) + 1);
return sv;
}
// now we have uint64 native-endian value
// which can be signed or unsigned under the cover

// if it is unsigned, we know that is not greater than max value for type T
// so static_cast<T>(v) is safe

// if it is signed, but positive it is also ok
// we can be sure that it is less than max_value<T>/2
// to check whether is is negative we check if the sign bit present
// in unsigned form it means that value is more than
// a value 2^(bits_number-1)
bool is_positive_signed = v < sign_bit[size - 1];
if (std::is_unsigned<I>() || is_positive_signed) {
return static_cast<I>(v);
}

// T is signed integer type and the value v is negative
// value is negative signed means ( - x )
// where x is positive unsigned < sign_bits[size-1]
// find this x, safely cast to signed and negate result
// the bitwise negation operation affects higher bits as well
// but it doesn't spoil the result
// static_cast to smaller size cuts them off
T sv = -static_cast<I>((~v) + 1);

return sv;
}
} // namespace scale::detail

#endif // SCALE_SCALE_UTIL_HPP
104 changes: 104 additions & 0 deletions include/scale/enum_traits.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* Copyright Soramitsu Co., Ltd. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

#ifndef SCALE_ENUM_TRAITS_HPP
#define SCALE_ENUM_TRAITS_HPP

#include <type_traits>

#include <scale/outcome/outcome_throw.hpp>
#include <scale/scale_error.hpp>

namespace scale {

/**
* Description of an enum type
* Two specialization choices:
* - min_value and max_value convertible to std::underlying_type_t<E>
* - a container of std::underlying_type_t<E> named valid_values, listing
* valid values
* @note check macros below for specialization convenience
* @tparam E the enum type
*/
template <typename E>
struct [[deprecated(
"Check the doc comment to see the specialization options")]] enum_traits
final {
static_assert(std::is_enum_v<E>);

// to easily detect an unspecialized enum_traits
static constexpr bool is_default = true;
};

#define SCALE_DEFINE_ENUM_VALUE_RANGE(enum_namespace, enum_name, min, max) \
template <> \
struct scale::enum_traits<enum_namespace::enum_name> final { \
using underlying = std::underlying_type_t<enum_namespace::enum_name>; \
static constexpr underlying min_value = static_cast<underlying>((min)); \
static constexpr underlying max_value = static_cast<underlying>((max)); \
};

// Mind that values should be enum constants, not numbers
#define SCALE_DEFINE_ENUM_VALUE_LIST(enum_namespace, enum_name, ...) \
template <> \
struct scale::enum_traits<enum_namespace::enum_name> final { \
static constexpr enum_namespace::enum_name valid_values[] = {__VA_ARGS__}; \
};

template <typename T,
typename E = std::decay_t<T>,
typename E_traits = enum_traits<E>,
std::underlying_type_t<E> Min = E_traits::min_value,
std::underlying_type_t<E> Max = E_traits::max_value>
constexpr bool is_valid_enum_value(std::underlying_type_t<E> value) noexcept {
return value >= Min && value <= Max;
}

template <typename T,
typename E = std::decay_t<T>,
typename E_traits = enum_traits<E>,
typename = decltype(E_traits::valid_values)>
constexpr bool is_valid_enum_value(std::underlying_type_t<E> value) noexcept {
const auto &valid_values = E_traits::valid_values;
return std::find(std::begin(valid_values),
std::end(valid_values),
static_cast<E>(value))
!= std::end(valid_values);
}

template <typename T,
typename E = std::decay_t<T>,
typename = std::enable_if_t<enum_traits<E>::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<E> value) noexcept {
return true;
}

/**
* @brief scale-decodes any enum type as underlying type
* @tparam T enum type
* @param v value of enum type
* @return reference to stream
*/
template <typename T,
typename S,
typename E = std::decay_t<T>,
typename = std::enable_if_t<S::is_decoder_stream>,
typename = std::enable_if_t<std::is_enum_v<E>>>
S &operator>>(S &s, T &v) {
std::underlying_type_t<E> value;
s >> value;
if (is_valid_enum_value<E>(value)) {
v = static_cast<E>(value);
return s;
}
raise(DecodeError::INVALID_ENUM_VALUE);
}

} // namespace scale

#endif // SCALE_ENUM_TRAITS_HPP
3 changes: 2 additions & 1 deletion include/scale/scale.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <scale/outcome/outcome.hpp>
#include <scale/scale_decoder_stream.hpp>
#include <scale/scale_encoder_stream.hpp>
#include <scale/enum_traits.hpp>

#define SCALE_EMPTY_DECODER(TargetType) \
template <typename Stream, \
Expand Down Expand Up @@ -49,7 +50,7 @@ namespace scale {
} catch (std::system_error &e) {
return outcome::failure(e.code());
}
return s.data();
return s.to_vector();
}

/**
Expand Down
19 changes: 18 additions & 1 deletion include/scale/scale_encoder_stream.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ namespace scale {
/**
* @return vector of bytes containing encoded data
*/
std::vector<uint8_t> data() const;
std::vector<uint8_t> to_vector() const;

/**
* Get amount of encoded data written to the stream
Expand Down Expand Up @@ -285,6 +285,23 @@ namespace scale {
size_t bytes_written_;
};

/**
* @brief scale-encodes any enum type as its underlying type
* Defined outside ScaleEncoderStream to allow custom overloads for
* specific enum types.
* @tparam T enum type
* @param v value of the enum type
* @return reference to stream
*/
template <typename S,
typename T,
typename E = std::decay_t<T>,
typename = std::enable_if_t<S::is_encoder_stream>,
typename = std::enable_if_t<std::is_enum_v<E>>>
S &operator<<(S &s, const T &v) {
return s << static_cast<std::underlying_type_t<E>>(v);
}

} // namespace scale

#endif // SCALE_CORE_SCALE_SCALE_ENCODER_STREAM_HPP
1 change: 1 addition & 0 deletions include/scale/scale_error.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ namespace scale {
UNEXPECTED_VALUE, ///< unexpected value
TOO_MANY_ITEMS, ///< too many items, cannot address them in memory
WRONG_TYPE_INDEX, ///< wrong type index, cannot decode variant
INVALID_ENUM_VALUE ///< enum value which doesn't belong to the enum
};

} // namespace scale
Expand Down
Loading

0 comments on commit c55e98f

Please sign in to comment.