diff --git a/README.md b/README.md index 402866b..941d7bc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ Binary <> JSON conversion using ABIs. Compatible with languages which can interface to C; see [src/abieos.h](src/abieos.h). -Alpha release. Feedback requested. +## Important or breaking changes +`main` branch is considered stable. No explicit releases are made. Important or possibly breaking changes are listed below. View linked PR for additional details. +* **Janurary 2025 via PR [#37](https://github.com/AntelopeIO/abieos/pull/37)**: When converting from JSON, `name`s must be valid Antelope names (no more than 13 characters, a-z.12345 only, etc). Most use cases are unaffected by this change. Consuming state_history's JSON ABI via state_history's websocket will require a minor change in user code. ## Packing transactions diff --git a/include/eosio/abi.hpp b/include/eosio/abi.hpp index 0e2b718..57c0590 100644 --- a/include/eosio/abi.hpp +++ b/include/eosio/abi.hpp @@ -14,6 +14,7 @@ #include "time.hpp" #include "bytes.hpp" #include "asset.hpp" +#include "murmur.hpp" namespace eosio { @@ -169,9 +170,79 @@ struct abi_def { might_not_exist> action_results{}; }; +/* Allows for table names that are not Antelope names (longer than 13 chars, invalid chars, etc) when loading ABI from JSON. These + * invalid names are transformed in to a valid Antelope name via a hash. + * This is intended strictly to ingest state_history's non-compliant ABI sent over the state_history websocket. + */ +struct abi_def_nonstrict_table_name : abi_def {}; + EOSIO_REFLECT(abi_def, version, types, structs, actions, tables, ricardian_clauses, error_messages, abi_extensions, variants, action_results); +template +void hashed_name_for_name_from_json(name& nm, S& stream) { + const auto s = stream.get_string(); + const auto n = try_string_to_name_strict(s); + if(n) + nm = name(n.value()); + else + nm = name(murmur64(s.data(), s.size())); +} + +template +void hashed_name_for_name_from_json(T&, S&) { + check( false, "should be unreachable" ); +} + +template +void table_def_array_nonstrict_name_from_json(std::vector& tbl_defs, S& stream) { + stream.get_start_array(); + while (true) { + auto t = stream.peek_token(); + if (t.get().type == json_token_type::type_end_array) + break; + auto& tbl_def = tbl_defs.emplace_back(); + from_json_object(stream, [&](std::string_view key) { + bool found = false; + eosio::for_each_field([&](std::string_view member_name, auto member) { + if (!found && key == member_name) { + if (member_name == "name") + hashed_name_for_name_from_json(member(&tbl_def), stream); + else + from_json(member(&tbl_def), stream); + found = true; + } + }); + if (!found) + from_json_skip_value(stream); + }); + } + stream.get_end_array(); +} + +template +void table_def_array_nonstrict_name_from_json(T&, S&) { + check( false, "should be unreachable" ); +} + +template +void from_json(abi_def_nonstrict_table_name& obj, S& stream) { + from_json_object(stream, [&](std::string_view key) { + bool found = false; + eosio::for_each_field([&](std::string_view member_name, auto member) { + if (!found && key == member_name) { + if (member_name == "tables") + table_def_array_nonstrict_name_from_json(member(&obj), stream); + else + from_json(member(&obj), stream); + found = true; + } + }); + if (!found) + from_json_skip_value(stream); + }); +} + struct abi_type; struct abi_field { diff --git a/include/eosio/name.hpp b/include/eosio/name.hpp index 7a22e16..7985573 100644 --- a/include/eosio/name.hpp +++ b/include/eosio/name.hpp @@ -4,7 +4,6 @@ #include "check.hpp" #include "operators.hpp" #include "reflection.hpp" -#include "murmur.hpp" #include namespace eosio { @@ -140,21 +139,12 @@ struct name { } }; -// TODO this seems weird and the name is misleading -// and I don't think this has ever been truly constexpr -inline constexpr uint64_t hash_name( std::string_view str ) { - auto r = try_string_to_name_strict(str); - if( r ) return r.value(); - return murmur64( str.data(), str.size() ); -} - EOSIO_REFLECT(name, value); EOSIO_COMPARE(name); template void from_json(name& obj, S& stream) { - auto r = stream.get_string(); - obj = name(hash_name(r)); + obj = name(string_to_name_strict(stream.get_string())); } template @@ -170,7 +160,6 @@ inline namespace literals { template inline constexpr name operator""_n() { return name(string_to_name_strict()); } - inline constexpr name operator""_h(const char* s, size_t) { return name( hash_name(s) ); } #if defined(__clang__) # pragma clang diagnostic pop #endif diff --git a/src/test.cpp b/src/test.cpp index 1668c41..83b8909 100644 --- a/src/test.cpp +++ b/src/test.cpp @@ -574,6 +574,25 @@ void check_error(abieos_context* context, const std::string& s, F f) { check_except(s, [&] { check_context(context, f()); }); } +/* abieos' C interface, used by check_types() below, uses abi_def which disallows the invalid + * table names SHIP's ABI contains. For purposes of this test, scrub all the tables from the ABI + * before loading it; they aren't used. + * The check_ship_abi_load() test checks abi_def vs abi_def_nonstrict_table_name behavior on the unmodified + * ship.abi.cpp contents. + */ +rapidjson::StringBuffer scrub_shipabi_tables(const char* const s) { + rapidjson::Document document; + document.Parse(s); + check(document.IsObject(), "unexpected ship abi"); + check(document["tables"].IsArray(), "unexpected ship abi"); + document["tables"].GetArray().Clear(); + + rapidjson::StringBuffer buff; + rapidjson::Writer writer(buff); + document.Accept(writer); + return buff; +} + void check_types() { auto context = check(abieos_create()); auto token = check_context(context, abieos_string_to_name(context, "eosio.token")); @@ -582,7 +601,7 @@ void check_types() { auto testKvAbiName = check_context(context, abieos_string_to_name(context, "testkv.abi")); check_context(context, abieos_set_abi(context, 0, transactionAbi)); check_context(context, abieos_set_abi(context, 1, packedTransactionAbi)); - check_context(context, abieos_set_abi(context, 2, state_history_plugin_abi)); + check_context(context, abieos_set_abi(context, 2, scrub_shipabi_tables(state_history_plugin_abi).GetString())); check_context(context, abieos_set_abi_hex(context, token, tokenHexAbi)); check_context(context, abieos_set_abi(context, testAbiName, testAbi)); check_context(context, abieos_set_abi_hex(context, testHexAbiName, testHexAbi)); @@ -830,9 +849,8 @@ void check_types() { check_type(context, 0, "name", R"("ab.cd.ef.1234")"); check_type(context, 0, "name", R"("..ab.cd.ef..")", R"("..ab.cd.ef")"); check_type(context, 0, "name", R"("zzzzzzzzzzzz")"); - // todo: should json conversion fall back to hash? reenable this error? - // check_error(context, "thirteenth character in name cannot be a letter that comes after j", - // [&] { return abieos_json_to_bin(context, 0, "name", R"("zzzzzzzzzzzzz")"); }); + check_error(context, "thirteenth character in name cannot be a letter that comes after j", + [&] { return abieos_json_to_bin(context, 0, "name", R"("zzzzzzzzzzzzz")"); }); check_error(context, "expected string containing name", [&] { return abieos_json_to_bin(context, 0, "name", "true"); }); check_type(context, 0, "bytes", R"("")"); @@ -1241,10 +1259,30 @@ void check_types() { abieos_destroy(context); } +void check_ship_abi_load() { + { + eosio::abi_def def; + std::string ship_abi_copy = state_history_plugin_abi; + eosio::json_token_stream stream(ship_abi_copy.data()); + try { + from_json(def, stream); + throw std::runtime_error("abi_def shouldn't have loaded ship ABI"); + } catch (std::exception& e) {} + } + { + eosio::abi_def_nonstrict_table_name def; + std::string ship_abi_copy = state_history_plugin_abi; + eosio::json_token_stream stream(ship_abi_copy.data()); + from_json(def, stream); + } +} + int main() { try { check_types(); - printf("\ncheck_types ok\n\n"); + printf("\ncheck_types ok\n"); + check_ship_abi_load(); + printf("\ncheck_ship_abi_load ok\n\n"); return 0; } catch (std::exception& e) { printf("error: %s\n", e.what());