diff --git a/include/evmc/hex.hpp b/include/evmc/hex.hpp index 476a622b2..9a07ca213 100644 --- a/include/evmc/hex.hpp +++ b/include/evmc/hex.hpp @@ -5,9 +5,9 @@ #include #include +#include #include #include -#include namespace evmc { @@ -17,55 +17,106 @@ using bytes = std::basic_string; /// String view of uint8_t chars. using bytes_view = std::basic_string_view; -/// Hex decoding error codes. -enum class hex_errc -{ - /// Invalid hex digit encountered during decoding. - invalid_hex_digit = 1, - /// Input contains incomplete hex byte (length is odd). - incomplete_hex_byte_pair = 2, -}; +/// Encode a byte to a hex string. +inline std::string hex(uint8_t b) noexcept +{ + static constexpr auto hex_digits = "0123456789abcdef"; + return {hex_digits[b >> 4], hex_digits[b & 0xf]}; +} -/// Obtains a reference to the static error category object for hex errors. -const std::error_category& hex_category() noexcept; +/// Encodes bytes as hex string. +inline std::string hex(bytes_view bs) +{ + std::string str; + str.reserve(bs.size() * 2); + for (const auto b : bs) + str += hex(b); + return str; +} -/// Creates error_code object out of a hex error code value. -inline std::error_code make_error_code(hex_errc errc) noexcept +namespace internal_hex +{ +/// Extracts the nibble value out of a hex digit. +/// Returns -1 in case of invalid hex digit. +inline constexpr int from_hex_digit(char h) noexcept { - return {static_cast(errc), hex_category()}; + if (h >= '0' && h <= '9') + return h - '0'; + else if (h >= 'a' && h <= 'f') + return h - 'a' + 10; + else if (h >= 'A' && h <= 'F') + return h - 'A' + 10; + else + return -1; } -/// Hex decoding exception. -struct hex_error : std::system_error +/// The constexpr variant of std::isspace(). +inline constexpr bool isspace(char ch) noexcept { - using system_error::system_error; -}; + // Implementation taken from LLVM's libc. + return ch == ' ' || (static_cast(ch) - '\t') < 5; +} -/// Encode a byte to a hex string. -inline std::string hex(uint8_t b) noexcept +template +inline constexpr bool from_hex(std::string_view hex, OutputIt result) noexcept { - static constexpr auto hex_chars = "0123456789abcdef"; - return {hex_chars[b >> 4], hex_chars[b & 0xf]}; + // Omit the optional 0x prefix. + if (hex.size() >= 2 && hex[0] == '0' && hex[1] == 'x') + hex.remove_prefix(2); + + constexpr int empty_mark = -1; + int hi_nibble = empty_mark; + for (const auto h : hex) + { + if (isspace(h)) + continue; + + const int v = from_hex_digit(h); + if (v < 0) + return false; + + if (hi_nibble == empty_mark) + { + hi_nibble = v << 4; + } + else + { + *result++ = static_cast(hi_nibble | v); + hi_nibble = empty_mark; + } + } + + return hi_nibble == empty_mark; } +} // namespace internal_hex /// Validates hex encoded string. -std::error_code validate_hex(std::string_view hex) noexcept; - -/// Decodes hex encoded string to bytes. /// -/// Throws hex_error with the appropriate error code. -bytes from_hex(std::string_view hex); +/// @return True if the input is valid hex. +inline bool validate_hex(std::string_view hex) noexcept +{ + struct noop_output_iterator + { + uint8_t sink = {}; + uint8_t& operator*() noexcept { return sink; } + noop_output_iterator operator++(int) noexcept { return *this; } // NOLINT(cert-dcl21-cpp) + }; -/// Encodes bytes as hex string. -std::string hex(bytes_view bs); -} // namespace evmc + return internal_hex::from_hex(hex, noop_output_iterator{}); +} -namespace std +/// Decodes hex encoded string to bytes. +/// +/// In case the input is invalid the returned value is std::nullopt. +/// This can happen if a non-hex digit or odd number of digits is encountered. +/// Whitespace in the input is ignored. +inline std::optional from_hex(std::string_view hex) { -/// Template specialization of std::is_error_code_enum for evmc::hex_errc. -/// This enabled implicit conversions from evmc::hex_errc to std::error_code. -template <> -struct is_error_code_enum : true_type -{}; -} // namespace std + bytes bs; + bs.reserve(hex.size() / 2); + if (!internal_hex::from_hex(hex, std::back_inserter(bs))) + return {}; + return bs; +} +} // namespace evmc diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 32b5757ee..dfaf29cd8 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -13,7 +13,6 @@ target_compile_features(evmc_cpp INTERFACE cxx_std_17) target_include_directories(evmc_cpp INTERFACE $$) target_link_libraries(evmc_cpp INTERFACE evmc::evmc) -add_subdirectory(hex) add_subdirectory(instructions) add_subdirectory(loader) add_subdirectory(mocked_host) diff --git a/lib/hex/CMakeLists.txt b/lib/hex/CMakeLists.txt deleted file mode 100644 index f753649f8..000000000 --- a/lib/hex/CMakeLists.txt +++ /dev/null @@ -1,18 +0,0 @@ -# EVMC: Ethereum Client-VM Connector API. -# Copyright 2021 The EVMC Authors. -# Licensed under the Apache License, Version 2.0. - -add_library( - hex STATIC - ${EVMC_INCLUDE_DIR}/evmc/hex.hpp - hex.cpp -) - -add_library(evmc::hex ALIAS hex) -target_compile_features(hex PUBLIC cxx_std_17) -target_include_directories(hex PUBLIC $$) -set_target_properties(hex PROPERTIES POSITION_INDEPENDENT_CODE TRUE) - -if(EVMC_INSTALL) - install(TARGETS hex EXPORT evmcTargets DESTINATION ${CMAKE_INSTALL_LIBDIR}) -endif() diff --git a/lib/hex/hex.cpp b/lib/hex/hex.cpp deleted file mode 100644 index 458bd2565..000000000 --- a/lib/hex/hex.cpp +++ /dev/null @@ -1,122 +0,0 @@ -// EVMC: Ethereum Client-VM Connector API. -// Copyright 2021 The EVMC Authors. -// Licensed under the Apache License, Version 2.0. - -#include -#include - -namespace evmc -{ -namespace -{ -inline int from_hex_digit(char h) -{ - if (h >= '0' && h <= '9') - return h - '0'; - else if (h >= 'a' && h <= 'f') - return h - 'a' + 10; - else if (h >= 'A' && h <= 'F') - return h - 'A' + 10; - else - throw hex_error{hex_errc::invalid_hex_digit}; -} - -template -inline void from_hex(std::string_view hex, OutputIt result) -{ - // TODO: This can be implemented with hex_decode_iterator and std::copy. - - // Omit the optional 0x prefix. - const auto hex_begin = - (hex.size() >= 2 && hex[0] == '0' && hex[1] == 'x') ? hex.begin() + 2 : hex.begin(); - - constexpr int empty_byte_mark = -1; - int b = empty_byte_mark; - for (auto it = hex_begin; it != hex.end(); ++it) - { - const auto h = *it; - if (std::isspace(h)) - continue; - - const int v = from_hex_digit(h); - if (b == empty_byte_mark) - { - b = v << 4; - } - else - { - *result++ = static_cast(b | v); - b = empty_byte_mark; - } - } - - if (b != empty_byte_mark) - throw hex_error{hex_errc::incomplete_hex_byte_pair}; -} - -struct hex_category_impl : std::error_category -{ - const char* name() const noexcept final { return "hex"; } - - std::string message(int ev) const final - { - switch (static_cast(ev)) - { - case hex_errc::invalid_hex_digit: - return "invalid hex digit"; - case hex_errc::incomplete_hex_byte_pair: - return "incomplete hex byte pair"; - default: - return "unknown error"; - } - } -}; -} // namespace - -const std::error_category& hex_category() noexcept -{ - // Create static category object. This involves mutex-protected dynamic initialization. - // Because of the C++ CWG defect 253, the {} syntax is used. - static const hex_category_impl category_instance{}; - - return category_instance; -} - -std::error_code validate_hex(std::string_view hex) noexcept -{ - struct noop_output_iterator - { - uint8_t sink = {}; - uint8_t& operator*() noexcept { return sink; } - noop_output_iterator operator++(int) noexcept { return *this; } // NOLINT(cert-dcl21-cpp) - }; - - try - { - from_hex(hex, noop_output_iterator{}); - return {}; - } - catch (const hex_error& e) - { - return e.code(); - } -} - -bytes from_hex(std::string_view hex) -{ - bytes bs; - bs.reserve(hex.size() / 2); - from_hex(hex, std::back_inserter(bs)); - return bs; -} - -std::string hex(bytes_view bs) -{ - std::string str; - str.reserve(bs.size() * 2); - for (const auto b : bs) - str += hex(b); - return str; -} - -} // namespace evmc diff --git a/lib/tooling/CMakeLists.txt b/lib/tooling/CMakeLists.txt index b6a3f7f58..d19cd0c2e 100644 --- a/lib/tooling/CMakeLists.txt +++ b/lib/tooling/CMakeLists.txt @@ -5,7 +5,7 @@ add_library(tooling STATIC) add_library(evmc::tooling ALIAS tooling) target_compile_features(tooling PUBLIC cxx_std_17) -target_link_libraries(tooling PUBLIC evmc::evmc_cpp evmc::mocked_host evmc::hex) +target_link_libraries(tooling PUBLIC evmc::evmc_cpp evmc::mocked_host) target_sources( tooling PRIVATE diff --git a/lib/tooling/run.cpp b/lib/tooling/run.cpp index 8eceb6abf..7dac8edc7 100644 --- a/lib/tooling/run.cpp +++ b/lib/tooling/run.cpp @@ -71,8 +71,13 @@ int run(evmc::VM& vm, out << (create ? "Creating and executing on " : "Executing on ") << rev << " with " << gas << " gas limit\n"; - const auto code = from_hex(code_hex); - const auto input = from_hex(input_hex); + auto opt_code = from_hex(code_hex); + auto opt_input = from_hex(input_hex); + if (!opt_code || !opt_input) + throw std::invalid_argument{"invalid hex"}; + + const auto& code = *opt_code; + const auto& input = *opt_input; MockedHost host; diff --git a/test/tools/CMakeLists.txt b/test/tools/CMakeLists.txt index 6f26b1b10..6f4fa46ae 100644 --- a/test/tools/CMakeLists.txt +++ b/test/tools/CMakeLists.txt @@ -54,13 +54,13 @@ set_tests_properties(${PROJECT_NAME}/evmc-tool/explicit_empty_input PROPERTIES P add_evmc_tool_test( invalid_hex_code "--vm $ run 0x600" - "code: \\(incomplete hex byte pair\\) OR \\(File does not exist: 0x600\\)" + "code: \\(invalid hex\\) OR \\(File does not exist: 0x600\\)" ) add_evmc_tool_test( invalid_hex_input "--vm $ run 0x --input aa0y" - "--input: \\(invalid hex digit\\) OR \\(File does not exist: aa0y\\)" + "--input: \\(invalid hex\\) OR \\(File does not exist: aa0y\\)" ) add_evmc_tool_test( @@ -78,7 +78,7 @@ add_evmc_tool_test( add_evmc_tool_test( invalid_code_file "--vm $ run ${CMAKE_CURRENT_SOURCE_DIR}/invalid_code.evm" - "Error: invalid hex digit" + "Error: invalid hex" ) add_evmc_tool_test( diff --git a/test/unittests/CMakeLists.txt b/test/unittests/CMakeLists.txt index 16dd818dc..14f68a8d2 100644 --- a/test/unittests/CMakeLists.txt +++ b/test/unittests/CMakeLists.txt @@ -36,7 +36,6 @@ target_link_libraries( evmc::instructions evmc::evmc_cpp evmc::tooling - evmc::hex GTest::gtest_main ) target_include_directories(evmc-unittests PRIVATE ${PROJECT_SOURCE_DIR}) diff --git a/test/unittests/example_vm_test.cpp b/test/unittests/example_vm_test.cpp index 97fe2e036..b62272c33 100644 --- a/test/unittests/example_vm_test.cpp +++ b/test/unittests/example_vm_test.cpp @@ -17,7 +17,7 @@ struct Output { evmc::bytes bytes; - explicit Output(const char* output_hex) noexcept : bytes{evmc::from_hex(output_hex)} {} + explicit Output(const char* output_hex) noexcept : bytes{evmc::from_hex(output_hex).value()} {} friend bool operator==(const evmc::result& result, const Output& expected) noexcept { @@ -43,10 +43,10 @@ class example_vm : public testing::Test evmc::result execute_in_example_vm(int64_t gas, const char* code_hex, - const char* input_hex = "") noexcept + const char* input_hex = "") { - const auto code = evmc::from_hex(code_hex); - const auto input = evmc::from_hex(input_hex); + const auto code = evmc::from_hex(code_hex).value(); + const auto input = evmc::from_hex(input_hex).value(); msg.gas = gas; msg.input_data = input.data(); @@ -150,7 +150,7 @@ TEST_F(example_vm, revert_undefined) TEST_F(example_vm, call) { // pseudo-Yul: call(3, 3, 3, 3, 3, 3, 3) return(0, msize()) - const auto expected_output = evmc::from_hex("aabbcc"); + const auto expected_output = evmc::from_hex("aabbcc").value(); host.call_result.output_data = expected_output.data(); host.call_result.output_size = expected_output.size(); const auto r = execute_in_example_vm(100, "6003808080808080f1596000f3"); diff --git a/test/unittests/hex_test.cpp b/test/unittests/hex_test.cpp index 4720e480c..fb14cadd1 100644 --- a/test/unittests/hex_test.cpp +++ b/test/unittests/hex_test.cpp @@ -4,6 +4,7 @@ #include #include +#include using namespace evmc; @@ -39,31 +40,18 @@ TEST(hex, from_hex) EXPECT_EQ(from_hex("00bc01000C"), (bytes{0x00, 0xbc, 0x01, 0x00, 0x0c})); } -static std::error_code catch_hex_error(const char* input) -{ - try - { - from_hex(input); - } - catch (const hex_error& e) - { - return e.code(); - } - return {}; -} - TEST(hex, from_hex_odd_length) { - EXPECT_EQ(catch_hex_error("0"), hex_errc::incomplete_hex_byte_pair); - EXPECT_EQ(catch_hex_error("1"), hex_errc::incomplete_hex_byte_pair); - EXPECT_EQ(catch_hex_error("123"), hex_errc::incomplete_hex_byte_pair); + EXPECT_EQ(from_hex("0"), std::nullopt); + EXPECT_EQ(from_hex("1"), std::nullopt); + EXPECT_EQ(from_hex("123"), std::nullopt); } TEST(hex, from_hex_not_hex_digit) { - EXPECT_EQ(catch_hex_error("0g"), hex_errc::invalid_hex_digit); - EXPECT_EQ(catch_hex_error("000h"), hex_errc::invalid_hex_digit); - EXPECT_EQ(catch_hex_error("ffffffzz"), hex_errc::invalid_hex_digit); + EXPECT_EQ(from_hex("0g"), std::nullopt); + EXPECT_EQ(from_hex("000h"), std::nullopt); + EXPECT_EQ(from_hex("ffffffzz"), std::nullopt); } TEST(hex, from_hex_0x_prefix) @@ -71,10 +59,10 @@ TEST(hex, from_hex_0x_prefix) EXPECT_EQ(from_hex("0x"), bytes{}); EXPECT_EQ(from_hex("0x00"), bytes{0x00}); EXPECT_EQ(from_hex("0x01020304"), (bytes{0x01, 0x02, 0x03, 0x04})); - EXPECT_EQ(catch_hex_error("0x123"), hex_errc::incomplete_hex_byte_pair); - EXPECT_EQ(catch_hex_error("00x"), hex_errc::invalid_hex_digit); - EXPECT_EQ(catch_hex_error("00x0"), hex_errc::invalid_hex_digit); - EXPECT_EQ(catch_hex_error("0x001y"), hex_errc::invalid_hex_digit); + EXPECT_EQ(from_hex("0x123"), std::nullopt); + EXPECT_EQ(from_hex("00x"), std::nullopt); + EXPECT_EQ(from_hex("00x0"), std::nullopt); + EXPECT_EQ(from_hex("0x001y"), std::nullopt); } TEST(hex, from_hex_skip_whitespace) @@ -86,34 +74,35 @@ TEST(hex, from_hex_skip_whitespace) TEST(hex, validate_hex) { - EXPECT_FALSE(validate_hex("")); - EXPECT_FALSE(validate_hex("0x")); - EXPECT_FALSE(validate_hex("01")); - EXPECT_EQ(validate_hex("0"), hex_errc::incomplete_hex_byte_pair); - EXPECT_EQ(validate_hex("WXYZ"), hex_errc::invalid_hex_digit); -} - -TEST(hex, hex_error_code) -{ - std::error_code ec1 = hex_errc::invalid_hex_digit; - EXPECT_EQ(ec1.value(), 1); - EXPECT_EQ(ec1.message(), "invalid hex digit"); - - std::error_code ec2 = hex_errc::incomplete_hex_byte_pair; - EXPECT_EQ(ec2.value(), 2); - EXPECT_EQ(ec2.message(), "incomplete hex byte pair"); -} - -TEST(hex, hex_category_inspection) -{ - EXPECT_STREQ(hex_category().name(), "hex"); + EXPECT_TRUE(validate_hex("")); + EXPECT_TRUE(validate_hex("0x")); + EXPECT_TRUE(validate_hex("01")); + EXPECT_FALSE(validate_hex("0")); + EXPECT_FALSE(validate_hex("WXYZ")); } -TEST(hex, hex_category_comparison) +TEST(hex, isspace) { - std::error_code ec1 = hex_errc::invalid_hex_digit; - EXPECT_EQ(ec1.category(), hex_category()); + // Test internal isspace() compliance with std::isspace(). + // The https://en.cppreference.com/w/cpp/string/byte/isspace has the list of "space" characters. - std::error_code ec2 = hex_errc::incomplete_hex_byte_pair; - EXPECT_EQ(ec2.category(), hex_category()); + for (int i = int{std::numeric_limits::min()}; i <= std::numeric_limits::max(); ++i) + { + const auto c = static_cast(i); + EXPECT_EQ(evmc::internal_hex::isspace(c), (std::isspace(c) != 0)); + switch (c) + { + case ' ': + case '\f': + case '\n': + case '\r': + case '\t': + case '\v': + EXPECT_TRUE(evmc::internal_hex::isspace(c)); + break; + default: + EXPECT_FALSE(evmc::internal_hex::isspace(c)); + break; + } + } } diff --git a/tools/evmc/main.cpp b/tools/evmc/main.cpp index 703d81539..72f02ab96 100644 --- a/tools/evmc/main.cpp +++ b/tools/evmc/main.cpp @@ -15,8 +15,7 @@ namespace /// @todo The file content is expected to be a hex string but not validated. std::string load_hex(const std::string& str) { - const auto error_code = evmc::validate_hex(str); - if (!error_code) + if (evmc::validate_hex(str)) return str; // Must be a file path. @@ -30,9 +29,8 @@ struct HexValidator : public CLI::Validator { name_ = "HEX"; func_ = [](const std::string& str) -> std::string { - const auto error_code = evmc::validate_hex(str); - if (error_code) - return error_code.message(); + if (!evmc::validate_hex(str)) + return "invalid hex"; return {}; }; }