diff --git a/libraries/sysiolib/core/sysio/basic_name.hpp b/libraries/sysiolib/core/sysio/basic_name.hpp new file mode 100644 index 000000000..8e484c4e1 --- /dev/null +++ b/libraries/sysiolib/core/sysio/basic_name.hpp @@ -0,0 +1,148 @@ +#pragma once +/** + * @file sysio/basic_name.hpp + * @brief Generic MSB-first packed 64-bit identifier (contract side). + * + * basic_name is the shared core behind sysio::name and + * sysio::slug_name. It mirrors the host-side fc::basic_name, but with CDT + * semantics: per-character validation via sysio::check (WASM has no + * exceptions) in a constexpr constructor, so `_n` / `_s` literals are checked + * at compile time. + * + * Traits is the policy that specialises the template; it must satisfy the + * basic_name_traits concept (declared below). alphabet[0] is the pad symbol; + * zero_terminates selects how to_string() treats a symbol-0 slot — a hard + * terminator (slug-style) or an ordinary interior character (name's '.'). + * + * The symbol width is derived (the minimal bits to index the alphabet); + * symbols are packed most-significant-first, the final symbol narrowed if + * max_len * width would exceed 64. + * + * @see fc::basic_name (host-side mirror) + */ + +#include "check.hpp" +#include "serialize.hpp" + +#include +#include +#include +#include +#include +#include + +namespace sysio { + +/// Compile-time contract for a basic_name Traits policy: an alphabet and a +/// length, a zero_terminates flag steering to_string(), and the three +/// sysio::check messages. Enforced in place of a prose list of requirements. +template +concept basic_name_traits = + requires { + { Traits::max_len } -> std::convertible_to; + { Traits::alphabet } -> std::convertible_to; + { Traits::zero_terminates } -> std::convertible_to; + { Traits::bad_char_message } -> std::convertible_to; + { Traits::too_long_message } -> std::convertible_to; + { Traits::bad_final_symbol_message } -> std::convertible_to; + } + && Traits::max_len > 0 + && std::string_view{ Traits::alphabet }.size() > 0; + +template +struct basic_name { + uint64_t value = 0; + + constexpr basic_name() = default; + constexpr explicit basic_name( uint64_t v ) : value(v) {} + + /// Per-character validated string constructor. sysio::check-throws on an + /// over-long string, an out-of-alphabet character, or a final symbol too + /// wide for its (possibly narrowed) slot. constexpr — so an invalid + /// `_n` / `_s` literal is a compile error. + constexpr explicit basic_name( std::string_view str ) : value(0) { + // sysio::check is not constexpr — invoke it only on the failure path so a + // valid `_n` / `_s` literal still constant-evaluates (a bad one reaches + // check and is therefore a compile error). + if ( str.size() > static_cast(Traits::max_len) ) + sysio::check( false, Traits::too_long_message ); + const int n = static_cast(str.size()); + for ( int i = 0; i < Traits::max_len && i < n; ++i ) { + const uint64_t sym = symbol( str[i] ); + if ( sym > width_mask(i) ) + sysio::check( false, Traits::bad_final_symbol_message ); + value |= sym << shift(i); + } + } + + constexpr uint64_t to_uint64_t() const { return value; } + constexpr bool empty() const { return value == 0; } + constexpr bool good() const { return value != 0; } + constexpr explicit operator bool() const { return value != 0; } + + std::string to_string() const { + std::string s; + for ( int i = 0; i < Traits::max_len; ++i ) { + const uint64_t sym = (value >> shift(i)) & width_mask(i); + // A zero-terminated alphabet (slug-style) ends at the first symbol-0 + // slot; for name, symbol 0 ('.') is an ordinary interior character. + if ( Traits::zero_terminates && sym == 0 ) + break; + s.push_back( character( sym ) ); + } + if ( !Traits::zero_terminates ) { + const char pad = character(0); + while ( !s.empty() && s.back() == pad ) + s.pop_back(); + } + return s; + } + + /// character -> symbol; sysio::check-throws on a character outside the + /// alphabet. (sysio::name re-exposes this as char_to_value.) + static constexpr uint64_t symbol( char c ) { + const std::string_view a = Traits::alphabet; + for ( std::size_t s = 0; s < a.size(); ++s ) + if ( a[s] == c ) return static_cast(s); + sysio::check( false, Traits::bad_char_message ); + return 0; // unreachable + } + /// symbol -> character; out-of-range symbols decode as the pad (alphabet[0]). + static constexpr char character( uint64_t s ) { + const std::string_view a = Traits::alphabet; + return s < a.size() ? a[s] : a[0]; + } + + // Total order on the packed value; MSB-first packing makes it match the + // decoded string's lexicographic order. Defaulted <=> / == synthesize the + // four relational operators and !=. + friend constexpr std::strong_ordering operator<=>( basic_name a, basic_name b ) = default; + friend constexpr bool operator==( basic_name a, basic_name b ) = default; + + SYSLIB_SERIALIZE( basic_name, (value) ) + +private: + // --- symbol width: minimal bits to index the alphabet --- + static constexpr int symbol_bits( std::size_t alphabet_size ) { + int b = 0; + while ( (std::size_t{1} << b) < alphabet_size ) ++b; + return b; + } + static constexpr int bits = symbol_bits( Traits::alphabet.size() ); + static constexpr int total_bits = Traits::max_len * bits < 64 + ? Traits::max_len * bits : 64; + static_assert( (Traits::max_len - 1) * bits < 64, + "basic_name: symbol layout does not fit in 64 bits" ); + + // --- MSB-first bit layout; the final symbol absorbs any shortfall --- + static constexpr uint32_t shift( int i ) { + const int s = total_bits - bits * (i + 1); + return s > 0 ? static_cast(s) : 0u; + } + static constexpr uint64_t width_mask( int i ) { + const int w = (i == Traits::max_len - 1) ? total_bits - bits * i : bits; + return (static_cast(1) << w) - 1; + } +}; + +} // namespace sysio diff --git a/libraries/sysiolib/core/sysio/name.hpp b/libraries/sysiolib/core/sysio/name.hpp index e787a9333..564858cb8 100644 --- a/libraries/sysiolib/core/sysio/name.hpp +++ b/libraries/sysiolib/core/sysio/name.hpp @@ -4,8 +4,7 @@ */ #pragma once -#include "check.hpp" -#include "serialize.hpp" +#include "basic_name.hpp" #include "reflect.hpp" #include @@ -26,96 +25,54 @@ namespace sysio { * @brief SYSIO Name Type */ + /// Alphabet + length traits for the SYSIO account-name encoding: up to 13 + /// base-32 symbols over ".12345a-z". Drives sysio::basic_name. + struct sysio_name_traits { + static constexpr int max_len = 13; + static constexpr std::string_view alphabet = ".12345abcdefghijklmnopqrstuvwxyz"; + // Symbol 0 ('.') is an ordinary interior character, not a terminator. + static constexpr bool zero_terminates = false; + static constexpr const char* bad_char_message = + "character is not in allowed character set for names"; + static constexpr const char* too_long_message = + "string is too long to be a valid name"; + static constexpr const char* bad_final_symbol_message = + "thirteenth character in name cannot be a letter that comes after j"; + }; + /** * Wraps a %uint64_t to ensure it is only passed to methods that expect a %name. - * Ensures value is only passed to methods that expect a %name and that no mathematical - * operations occur. Also enables specialization of print + * + * The packed encoding and value semantics (constructors, comparisons, + * to_string, serialization) live in sysio::basic_name; name adds the + * contract-side surface: raw, print, length, prefix()/suffix(), + * write_as_string. * * @ingroup name */ - struct name { - public: - enum class raw : uint64_t {}; + struct name : basic_name { + using base = basic_name; - /** - * Construct a new name - * - * @brief Construct a new name object defaulting to a value of 0 - * - */ - constexpr name() : value(0) {} + /// Scoped enumerated alias of uint64_t, for the raw packed value. + enum class raw : uint64_t {}; - /** - * Construct a new name given a unit64_t value - * - * @brief Construct a new name object initialising value with v - * @param v - The unit64_t value - * - */ - constexpr explicit name( uint64_t v ) - :value(v) - {} + using base::base; // name(uint64_t), name(std::string_view) + constexpr name() = default; /** * Construct a new name given a scoped enumerated type of raw (uint64_t). - * - * @brief Construct a new name object initialising value with r - * @param r - The raw value which is a scoped enumerated type of unit64_t - * */ - constexpr explicit name( name::raw r ) - :value(static_cast(r)) - {} - - /** - * Construct a new name given an string. - * - * @brief Construct a new name object initialising value with str - * @param str - The string value which validated then converted to unit64_t - * - */ - constexpr explicit name( std::string_view str ) - :value(0) - { - if( str.size() > 13 ) { - sysio::check( false, "string is too long to be a valid name" ); - } - if( str.empty() ) { - return; - } - - auto n = std::min( (uint32_t)str.size(), (uint32_t)12u ); - for( decltype(n) i = 0; i < n; ++i ) { - value <<= 5; - value |= char_to_value( str[i] ); - } - value <<= ( 4 + 5*(12 - n) ); - if( str.size() == 13 ) { - uint64_t v = char_to_value( str[12] ); - if( v > 0x0Full ) { - sysio::check(false, "thirteenth character in name cannot be a letter that comes after j"); - } - value |= v; - } - } + constexpr explicit name( name::raw r ) : base( static_cast(r) ) {} /** - * Converts a %name Base32 symbol into its corresponding value + * Converts a %name Base32 symbol into its corresponding value. + * Throws via sysio::check if the character is not in the allowed set. * * @param c - Character to be converted - * @return constexpr char - Converted value + * @return constexpr uint8_t - Converted value */ static constexpr uint8_t char_to_value( char c ) { - if( c == '.') - return 0; - else if( c >= '1' && c <= '5' ) - return (c - '1') + 1; - else if( c >= 'a' && c <= 'z' ) - return (c - 'a') + 6; - else - sysio::check( false, "character is not in allowed character set for names" ); - - return 0; // control flow will never reach here; just added to suppress warning + return static_cast( base::symbol(c) ); } /** @@ -210,13 +167,6 @@ namespace sysio { */ constexpr operator raw()const { return raw(value); } - /** - * Explicit cast to bool of the uint64_t value of the name - * - * @return Returns true if the name is set to the default value of 0 else true. - */ - constexpr explicit operator bool()const { return value != 0; } - /** * Writes the %name as a string to the provided char buffer * @@ -248,17 +198,6 @@ namespace sysio { return begin; } - /** - * Returns the name as a string. - * - * @brief Returns the name value as a string by calling write_as_string() and returning the buffer produced by write_as_string() - */ - std::string to_string()const { - char buffer[13]; - auto end = write_as_string( buffer, buffer + sizeof(buffer) ); - return {buffer, end}; - } - /** * Prints an names as base32 encoded string * @@ -268,40 +207,11 @@ namespace sysio { internal_use_do_not_use::printn(value); } - /// @cond INTERNAL - - /** - * Equivalency operator. Returns true if a == b (are the same) - * - * @return boolean - true if both provided %name values are the same - */ - friend constexpr bool operator == ( const name& a, const name& b ) { - return a.value == b.value; - } - - /** - * Inverted equivalency operator. Returns true if a != b (are different) - * - * @return boolean - true if both provided %name values are not the same - */ - friend constexpr bool operator != ( const name& a, const name& b ) { - return a.value != b.value; - } - - /** - * Less than operator. Returns true if a < b. - * - * @return boolean - true if %name `a` is less than `b` - */ - friend constexpr bool operator < ( const name& a, const name& b ) { - return a.value < b.value; - } - - /// @endcond - - uint64_t value = 0; - CDT_REFLECT(value); + // name's own serialization: an exact-match operator<<(ds, const name&) + // must exist, else the generic bluegrass::meta field-iterator is chosen + // and rejects name as a non-aggregate. (basic_name has its own, used by + // slug_name, which is the alias type itself rather than a derived type.) SYSLIB_SERIALIZE( name, (value) ) }; diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index ca296c9b4..4f9b729b5 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -24,6 +24,7 @@ add_cdt_unit_test(crypto_ext_tests) add_cdt_unit_test(datastream_tests) add_cdt_unit_test(fixed_bytes_tests) add_cdt_unit_test(name_tests) +add_cdt_unit_test(basic_name_tests) add_cdt_unit_test(rope_tests) add_cdt_unit_test(serialize_tests) add_cdt_unit_test(string_tests1) diff --git a/tests/unit/basic_name_tests.cpp b/tests/unit/basic_name_tests.cpp new file mode 100644 index 000000000..20b402d65 --- /dev/null +++ b/tests/unit/basic_name_tests.cpp @@ -0,0 +1,130 @@ +/** + * @file + * @copyright defined in sysio.cdt/LICENSE.txt + * + * Unit tests for sysio::basic_name — the generic MSB-first packed + * identifier behind sysio::name. These exercise basic_name through a + * slug-style Traits policy (zero_terminates = true): the configuration a + * contract code/slug field uses. sysio::name itself is covered by + * name_tests.cpp. + */ + +#include +#include +#include + +#include +#include + +using sysio::basic_name; +using sysio::basic_name_traits; + +namespace { + +// A slug-style policy: alphabet [A-Z0-9_], up to 8 symbols, zero-terminated. +// Mirrors the opp contract slug_name encoding (symbol 0 = '\0' pad/terminator; +// 1-26 = A-Z; 27-36 = 0-9; 37 = '_'). +struct test_slug_traits { + static constexpr int max_len = 8; + static constexpr char alphabet_storage[] = + "\0ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"; + static constexpr std::string_view alphabet{ alphabet_storage, + sizeof(alphabet_storage) - 1 }; + static constexpr bool zero_terminates = true; + static constexpr const char* bad_char_message = + "slug: character is not in [A-Z0-9_]"; + static constexpr const char* too_long_message = + "slug: string is longer than 8 characters"; + static constexpr const char* bad_final_symbol_message = + "slug: final symbol does not fit its slot"; +}; +using test_slug = basic_name; + +// A deliberately incomplete policy — missing zero_terminates — used only to +// confirm the basic_name_traits concept rejects it. +struct incomplete_traits { + static constexpr int max_len = 8; + static constexpr std::string_view alphabet = "ABC"; + static constexpr const char* bad_char_message = "x"; + static constexpr const char* too_long_message = "x"; + static constexpr const char* bad_final_symbol_message = "x"; +}; + +} // namespace + +// basic_name_traits concept: real policies satisfy it, an incomplete one does not. +SYSIO_TEST_BEGIN(basic_name_test_concept) + static_assert( basic_name_traits ); + static_assert( basic_name_traits ); + static_assert( !basic_name_traits ); + CHECK_EQUAL( basic_name_traits, true ) + CHECK_EQUAL( basic_name_traits, false ) +SYSIO_TEST_END + +// A slug round-trips: string -> packed uint64 -> string. +SYSIO_TEST_BEGIN(basic_name_test_slug_roundtrip) + CHECK_EQUAL( test_slug{""}.to_string(), "" ) + CHECK_EQUAL( test_slug{"ETH"}.to_string(), "ETH" ) + CHECK_EQUAL( test_slug{"USDC"}.to_string(), "USDC" ) + CHECK_EQUAL( test_slug{"WIRE"}.to_string(), "WIRE" ) + CHECK_EQUAL( test_slug{"PRIMARY"}.to_string(), "PRIMARY" ) + CHECK_EQUAL( test_slug{"ETHEREUM"}.to_string(), "ETHEREUM" ) // exactly 8 + CHECK_EQUAL( test_slug{"A_B"}.to_string(), "A_B" ) + CHECK_EQUAL( test_slug{"V1"}.to_string(), "V1" ) + + // the string constructor is constexpr — usable in _s-style literals + static_assert( test_slug{""}.value == 0 ); + static_assert( test_slug{"ETH"}.value != 0 ); +SYSIO_TEST_END + +// zero_terminates: to_string() ends at the first symbol-0 slot, so a raw value +// with an *interior* zero decodes to just the prefix — no embedded NUL. (With +// zero_terminates = false this value would decode to "A\0B".) +SYSIO_TEST_BEGIN(basic_name_test_slug_zero_terminates) + // 6-bit slots, MSB-first: slot i sits at bit (42 - 6*i). + // [A, , B] — symbol A = 1, symbol B = 2. + constexpr uint64_t a_gap_b = (uint64_t{1} << 42) | (uint64_t{2} << 30); + CHECK_EQUAL( test_slug{a_gap_b}.to_string(), "A" ) + + // trailing-zero slots drop away the same way + CHECK_EQUAL( test_slug{ uint64_t{1} << 42 }.to_string(), "A" ) + CHECK_EQUAL( test_slug{0}.to_string(), "" ) +SYSIO_TEST_END + +// Ordering / equality come from basic_name's defaulted operator<=> / operator==. +SYSIO_TEST_BEGIN(basic_name_test_slug_compare) + CHECK_EQUAL( test_slug{"ETH"} == test_slug{"ETH"}, true ) + CHECK_EQUAL( test_slug{"ETH"} != test_slug{"SOL"}, true ) + CHECK_EQUAL( test_slug{"ABC"} < test_slug{"ABD"}, true ) + CHECK_EQUAL( test_slug{"AAA"} <= test_slug{"AAA"}, true ) + CHECK_EQUAL( test_slug{"ZZZ"} > test_slug{"AAA"}, true ) + + static_assert( test_slug{"ETH"} == test_slug{"ETH"} ); + static_assert( test_slug{"ABC"} < test_slug{"ABD"} ); +SYSIO_TEST_END + +// Validation: the constexpr constructor sysio::check-throws on bad input. +SYSIO_TEST_BEGIN(basic_name_test_slug_validation) + CHECK_ASSERT( "slug: character is not in [A-Z0-9_]", + ([]() { test_slug{"eth"}; }) ) // lowercase + CHECK_ASSERT( "slug: character is not in [A-Z0-9_]", + ([]() { test_slug{"ETH-1"}; }) ) // '-' not in alphabet + CHECK_ASSERT( "slug: string is longer than 8 characters", + ([]() { test_slug{"TOOLONG12"}; }) ) // 9 characters +SYSIO_TEST_END + +int main(int argc, char* argv[]) { + bool verbose = false; + if( argc >= 2 && std::strcmp( argv[1], "-v" ) == 0 ) { + verbose = true; + } + silence_output(!verbose); + + SYSIO_TEST(basic_name_test_concept) + SYSIO_TEST(basic_name_test_slug_roundtrip) + SYSIO_TEST(basic_name_test_slug_zero_terminates) + SYSIO_TEST(basic_name_test_slug_compare) + SYSIO_TEST(basic_name_test_slug_validation) + + return has_failed(); +}