diff --git a/cmake/RippledCompiler.cmake b/cmake/RippledCompiler.cmake index 4d16222cbe..bc3a62a48c 100644 --- a/cmake/RippledCompiler.cmake +++ b/cmake/RippledCompiler.cmake @@ -16,16 +16,13 @@ set(CMAKE_CXX_EXTENSIONS OFF) target_compile_definitions (common INTERFACE $<$:DEBUG _DEBUG> - #[===[ - NOTE: CMAKE release builds already have NDEBUG defined, so no need to add it - explicitly except for the special case of (profile ON) and (assert OFF). - Presumably this is because we don't want profile builds asserting unless - asserts were specifically requested. - ]===] - $<$,$>>:NDEBUG> - # TODO: Remove once we have migrated functions from OpenSSL 1.x to 3.x. - OPENSSL_SUPPRESS_DEPRECATED -) + $<$,$>>:NDEBUG>) + # ^^^^ NOTE: CMAKE release builds already have NDEBUG + # defined, so no need to add it explicitly except for + # this special case of (profile ON) and (assert OFF) + # -- presumably this is because we don't want profile + # builds asserting unless asserts were specifically + # requested if (MSVC) # remove existing exception flag since we set it to -EHa diff --git a/conan.lock b/conan.lock index 0f11f086b4..ec790e16ce 100644 --- a/conan.lock +++ b/conan.lock @@ -9,7 +9,7 @@ "rocksdb/10.0.1#85537f46e538974d67da0c3977de48ac%1756234304.347", "re2/20230301#dfd6e2bf050eb90ddd8729cfb4c844a4%1756234257.976", "protobuf/3.21.12#d927114e28de9f4691a6bbcdd9a529d1%1756234251.614", - "openssl/3.5.2#0c5a5e15ae569f45dff57adcf1770cf7%1756234259.61", + "openssl/1.1.1w#a8f0792d7c5121b954578a7149d23e03%1756223730.729", "nudb/2.0.9#c62cfd501e57055a7e0d8ee3d5e5427d%1756234237.107", "lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1756234228.999", "libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1756223727.64", diff --git a/conanfile.py b/conanfile.py index 01f61c5d4e..3146b887e0 100644 --- a/conanfile.py +++ b/conanfile.py @@ -27,7 +27,7 @@ class Xrpl(ConanFile): 'grpc/1.50.1', 'libarchive/3.8.1', 'nudb/2.0.9', - 'openssl/3.5.2', + 'openssl/1.1.1w', 'soci/4.0.3', 'zlib/1.3.1', ] diff --git a/include/xrpl/protocol/Permissions.h b/include/xrpl/protocol/Permissions.h index 924a5af0da..d3f5253cd0 100644 --- a/include/xrpl/protocol/Permissions.h +++ b/include/xrpl/protocol/Permissions.h @@ -74,6 +74,9 @@ public: Permission& operator=(Permission const&) = delete; + std::optional + getPermissionName(std::uint32_t const value) const; + std::optional getGranularValue(std::string const& name) const; diff --git a/include/xrpl/protocol/STParsedJSON.h b/include/xrpl/protocol/STParsedJSON.h index d655969030..9c770fe94d 100644 --- a/include/xrpl/protocol/STParsedJSON.h +++ b/include/xrpl/protocol/STParsedJSON.h @@ -54,34 +54,6 @@ public: Json::Value error; }; -/** Holds the serialized result of parsing an input JSON array. - This does validation and checking on the provided JSON. -*/ -class STParsedJSONArray -{ -public: - /** Parses and creates an STParsedJSON array. - The result of the parsing is stored in array and error. - Exceptions: - Does not throw. - @param name The name of the JSON field, used in diagnostics. - @param json The JSON-RPC to parse. - */ - STParsedJSONArray(std::string const& name, Json::Value const& json); - - STParsedJSONArray() = delete; - STParsedJSONArray(STParsedJSONArray const&) = delete; - STParsedJSONArray& - operator=(STParsedJSONArray const&) = delete; - ~STParsedJSONArray() = default; - - /** The STArray if the parse was successful. */ - std::optional array; - - /** On failure, an appropriate set of error values. */ - Json::Value error; -}; - } // namespace ripple #endif diff --git a/src/libxrpl/protocol/Permissions.cpp b/src/libxrpl/protocol/Permissions.cpp index eb0c0489d1..c9e32c5056 100644 --- a/src/libxrpl/protocol/Permissions.cpp +++ b/src/libxrpl/protocol/Permissions.cpp @@ -101,6 +101,22 @@ Permission::getInstance() return instance; } +std::optional +Permission::getPermissionName(std::uint32_t const value) const +{ + auto const permissionValue = static_cast(value); + if (auto const granular = getGranularName(permissionValue)) + return *granular; + + // not a granular permission, check if it maps to a transaction type + auto const txType = permissionToTxType(value); + if (auto const* item = TxFormats::getInstance().findByType(txType); + item != nullptr) + return item->getName(); + + return std::nullopt; +} + std::optional Permission::getGranularValue(std::string const& name) const { diff --git a/src/libxrpl/protocol/STInteger.cpp b/src/libxrpl/protocol/STInteger.cpp index a90e21491c..5d6c1802cc 100644 --- a/src/libxrpl/protocol/STInteger.cpp +++ b/src/libxrpl/protocol/STInteger.cpp @@ -62,8 +62,10 @@ STUInt8::getText() const if (transResultInfo(TER::fromInt(value_), token, human)) return human; + // LCOV_EXCL_START JLOG(debugLog().error()) << "Unknown result code in metadata: " << value_; + // LCOV_EXCL_STOP } return std::to_string(value_); @@ -80,8 +82,10 @@ STUInt8::getJson(JsonOptions) const if (transResultInfo(TER::fromInt(value_), token, human)) return token; + // LCOV_EXCL_START JLOG(debugLog().error()) << "Unknown result code in metadata: " << value_; + // LCOV_EXCL_STOP } return value_; @@ -171,6 +175,13 @@ template <> std::string STUInt32::getText() const { + if (getFName() == sfPermissionValue) + { + auto const permissionName = + Permission::getInstance().getPermissionName(value_); + if (permissionName) + return *permissionName; + } return std::to_string(value_); } @@ -180,23 +191,10 @@ STUInt32::getJson(JsonOptions) const { if (getFName() == sfPermissionValue) { - auto const permissionValue = - static_cast(value_); - auto const granular = - Permission::getInstance().getGranularName(permissionValue); - - if (granular) - { - return *granular; - } - else - { - auto const txType = - Permission::getInstance().permissionToTxType(value_); - auto item = TxFormats::getInstance().findByType(txType); - if (item != nullptr) - return item->getName(); - } + auto const permissionName = + Permission::getInstance().getPermissionName(value_); + if (permissionName) + return *permissionName; } return value_; diff --git a/src/libxrpl/protocol/STParsedJSON.cpp b/src/libxrpl/protocol/STParsedJSON.cpp index 02dfde3966..9fbe5e7f91 100644 --- a/src/libxrpl/protocol/STParsedJSON.cpp +++ b/src/libxrpl/protocol/STParsedJSON.cpp @@ -83,7 +83,8 @@ constexpr std:: return static_cast(value); } -static std::string +// LCOV_EXCL_START +static inline std::string make_name(std::string const& object, std::string const& field) { if (field.empty()) @@ -92,7 +93,7 @@ make_name(std::string const& object, std::string const& field) return object + "." + field; } -static Json::Value +static inline Json::Value not_an_object(std::string const& object, std::string const& field) { return RPC::make_error( @@ -100,20 +101,20 @@ not_an_object(std::string const& object, std::string const& field) "Field '" + make_name(object, field) + "' is not a JSON object."); } -static Json::Value +static inline Json::Value not_an_object(std::string const& object) { return not_an_object(object, ""); } -static Json::Value +static inline Json::Value not_an_array(std::string const& object) { return RPC::make_error( rpcINVALID_PARAMS, "Field '" + object + "' is not a JSON array."); } -static Json::Value +static inline Json::Value unknown_field(std::string const& object, std::string const& field) { return RPC::make_error( @@ -121,7 +122,7 @@ unknown_field(std::string const& object, std::string const& field) "Field '" + make_name(object, field) + "' is unknown."); } -static Json::Value +static inline Json::Value out_of_range(std::string const& object, std::string const& field) { return RPC::make_error( @@ -129,7 +130,7 @@ out_of_range(std::string const& object, std::string const& field) "Field '" + make_name(object, field) + "' is out of range."); } -static Json::Value +static inline Json::Value bad_type(std::string const& object, std::string const& field) { return RPC::make_error( @@ -137,7 +138,7 @@ bad_type(std::string const& object, std::string const& field) "Field '" + make_name(object, field) + "' has bad type."); } -static Json::Value +static inline Json::Value invalid_data(std::string const& object, std::string const& field) { return RPC::make_error( @@ -145,13 +146,13 @@ invalid_data(std::string const& object, std::string const& field) "Field '" + make_name(object, field) + "' has invalid data."); } -static Json::Value +static inline Json::Value invalid_data(std::string const& object) { return invalid_data(object, ""); } -static Json::Value +static inline Json::Value array_expected(std::string const& object, std::string const& field) { return RPC::make_error( @@ -159,7 +160,7 @@ array_expected(std::string const& object, std::string const& field) "Field '" + make_name(object, field) + "' must be a JSON array."); } -static Json::Value +static inline Json::Value string_expected(std::string const& object, std::string const& field) { return RPC::make_error( @@ -167,7 +168,7 @@ string_expected(std::string const& object, std::string const& field) "Field '" + make_name(object, field) + "' must be a string."); } -static Json::Value +static inline Json::Value too_deep(std::string const& object) { return RPC::make_error( @@ -175,7 +176,7 @@ too_deep(std::string const& object) "Field '" + object + "' exceeds nesting depth limit."); } -static Json::Value +static inline Json::Value singleton_expected(std::string const& object, unsigned int index) { return RPC::make_error( @@ -184,7 +185,7 @@ singleton_expected(std::string const& object, unsigned int index) "]' must be an object with a single key/object value."); } -static Json::Value +static inline Json::Value template_mismatch(SField const& sField) { return RPC::make_error( @@ -193,7 +194,7 @@ template_mismatch(SField const& sField) "' contents did not meet requirements for that type."); } -static Json::Value +static inline Json::Value non_object_in_array(std::string const& item, Json::UInt index) { return RPC::make_error( @@ -201,6 +202,7 @@ non_object_in_array(std::string const& item, Json::UInt index) "Item '" + item + "' at index " + std::to_string(index) + " is not an object. Arrays may only contain objects."); } +// LCOV_EXCL_STOP template static std::optional @@ -385,10 +387,13 @@ parseLeaf( auto const& field = SField::getField(fieldName); + // checked in parseObject if (field == sfInvalid) { + // LCOV_EXCL_START error = unknown_field(json_name, fieldName); return ret; + // LCOV_EXCL_STOP } switch (field.fieldType) @@ -760,6 +765,12 @@ parseLeaf( AccountID uAccount, uIssuer; Currency uCurrency; + if (!account && !currency && !issuer) + { + error = invalid_data(element_name); + return ret; + } + if (account) { // human account id @@ -1153,24 +1164,4 @@ STParsedJSONObject::STParsedJSONObject( object = parseObject(name, json, sfGeneric, 0, error); } -//------------------------------------------------------------------------------ - -STParsedJSONArray::STParsedJSONArray( - std::string const& name, - Json::Value const& json) -{ - using namespace STParsedJSONDetail; - auto arr = parseArray(name, json, sfGeneric, 0, error); - if (!arr) - array.reset(); - else - { - auto p = dynamic_cast(&arr->get()); - if (p == nullptr) - array.reset(); - else - array = std::move(*p); - } -} - } // namespace ripple diff --git a/src/test/protocol/STInteger_test.cpp b/src/test/protocol/STInteger_test.cpp new file mode 100644 index 0000000000..f4572e49bd --- /dev/null +++ b/src/test/protocol/STInteger_test.cpp @@ -0,0 +1,135 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include + +namespace ripple { + +struct STInteger_test : public beast::unit_test::suite +{ + void + testUInt8() + { + STUInt8 u8(255); + BEAST_EXPECT(u8.value() == 255); + BEAST_EXPECT(u8.getText() == "255"); + BEAST_EXPECT(u8.getSType() == STI_UINT8); + BEAST_EXPECT(u8.getJson(JsonOptions::none) == 255); + + // there is some special handling for sfTransactionResult + STUInt8 tr(sfTransactionResult, 0); + BEAST_EXPECT(tr.value() == 0); + BEAST_EXPECT( + tr.getText() == + "The transaction was applied. Only final in a validated ledger."); + BEAST_EXPECT(tr.getSType() == STI_UINT8); + BEAST_EXPECT(tr.getJson(JsonOptions::none) == "tesSUCCESS"); + + // invalid transaction result + STUInt8 tr2(sfTransactionResult, 255); + BEAST_EXPECT(tr2.value() == 255); + BEAST_EXPECT(tr2.getText() == "255"); + BEAST_EXPECT(tr2.getSType() == STI_UINT8); + BEAST_EXPECT(tr2.getJson(JsonOptions::none) == 255); + } + + void + testUInt16() + { + STUInt16 u16(65535); + BEAST_EXPECT(u16.value() == 65535); + BEAST_EXPECT(u16.getText() == "65535"); + BEAST_EXPECT(u16.getSType() == STI_UINT16); + BEAST_EXPECT(u16.getJson(JsonOptions::none) == 65535); + + // there is some special handling for sfLedgerEntryType + STUInt16 let(sfLedgerEntryType, ltACCOUNT_ROOT); + BEAST_EXPECT(let.value() == ltACCOUNT_ROOT); + BEAST_EXPECT(let.getText() == "AccountRoot"); + BEAST_EXPECT(let.getSType() == STI_UINT16); + BEAST_EXPECT(let.getJson(JsonOptions::none) == "AccountRoot"); + + // there is some special handling for sfTransactionType + STUInt16 tlt(sfTransactionType, ttPAYMENT); + BEAST_EXPECT(tlt.value() == ttPAYMENT); + BEAST_EXPECT(tlt.getText() == "Payment"); + BEAST_EXPECT(tlt.getSType() == STI_UINT16); + BEAST_EXPECT(tlt.getJson(JsonOptions::none) == "Payment"); + } + + void + testUInt32() + { + STUInt32 u32(4'294'967'295u); + BEAST_EXPECT(u32.value() == 4'294'967'295u); + BEAST_EXPECT(u32.getText() == "4294967295"); + BEAST_EXPECT(u32.getSType() == STI_UINT32); + BEAST_EXPECT(u32.getJson(JsonOptions::none) == 4'294'967'295u); + + // there is some special handling for sfPermissionValue + STUInt32 pv(sfPermissionValue, ttPAYMENT + 1); + BEAST_EXPECT(pv.value() == ttPAYMENT + 1); + BEAST_EXPECT(pv.getText() == "Payment"); + BEAST_EXPECT(pv.getSType() == STI_UINT32); + BEAST_EXPECT(pv.getJson(JsonOptions::none) == "Payment"); + STUInt32 pv2(sfPermissionValue, PaymentMint); + BEAST_EXPECT(pv2.value() == PaymentMint); + BEAST_EXPECT(pv2.getText() == "PaymentMint"); + BEAST_EXPECT(pv2.getSType() == STI_UINT32); + BEAST_EXPECT(pv2.getJson(JsonOptions::none) == "PaymentMint"); + } + + void + testUInt64() + { + STUInt64 u64(0xFFFFFFFFFFFFFFFFull); + BEAST_EXPECT(u64.value() == 0xFFFFFFFFFFFFFFFFull); + BEAST_EXPECT(u64.getText() == "18446744073709551615"); + BEAST_EXPECT(u64.getSType() == STI_UINT64); + + // By default, getJson returns hex string + auto jsonVal = u64.getJson(JsonOptions::none); + BEAST_EXPECT(jsonVal.isString()); + BEAST_EXPECT(jsonVal.asString() == "ffffffffffffffff"); + + STUInt64 u64_2(sfMaximumAmount, 0xFFFFFFFFFFFFFFFFull); + BEAST_EXPECT(u64_2.value() == 0xFFFFFFFFFFFFFFFFull); + BEAST_EXPECT(u64_2.getText() == "18446744073709551615"); + BEAST_EXPECT(u64_2.getSType() == STI_UINT64); + BEAST_EXPECT( + u64_2.getJson(JsonOptions::none) == "18446744073709551615"); + } + + void + run() override + { + testUInt8(); + testUInt16(); + testUInt32(); + testUInt64(); + } +}; + +BEAST_DEFINE_TESTSUITE(STInteger, protocol, ripple); + +} // namespace ripple diff --git a/src/test/protocol/STParsedJSON_test.cpp b/src/test/protocol/STParsedJSON_test.cpp index bd62196a99..9ecb4c0365 100644 --- a/src/test/protocol/STParsedJSON_test.cpp +++ b/src/test/protocol/STParsedJSON_test.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012, 2013 Ripple Labs Inc. + Copyright (c) 2025 Ripple Labs Inc. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -19,14 +19,17 @@ #include +#include #include +#include +#include +#include #include namespace ripple { class STParsedJSON_test : public beast::unit_test::suite { -public: bool parseJSONString(std::string const& json, Json::Value& to) { @@ -35,69 +38,1965 @@ public: } void - testParseJSONArrayWithInvalidChildrenObjects() + testUInt8() { - testcase("parse json array invalid children"); - try + testcase("UInt8"); { - /* - - STArray/STObject constructs don't really map perfectly to json - arrays/objects. - - STObject is an associative container, mapping fields to value, but - an STObject may also have a Field as its name, stored outside the - associative structure. The name is important, so to maintain - fidelity, it will take TWO json objects to represent them. - - */ - std::string faulty( - "{\"Template\":[{" - "\"ModifiedNode\":{\"Sequence\":1}, " - "\"DeletedNode\":{\"Sequence\":1}" - "}]}"); - - std::unique_ptr so; - Json::Value faultyJson; - bool parsedOK(parseJSONString(faulty, faultyJson)); - unexpected(!parsedOK, "failed to parse"); - STParsedJSONObject parsed("test", faultyJson); - BEAST_EXPECT(!parsed.object); + Json::Value j; + j[sfCloseResolution] = 255; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfCloseResolution)); + BEAST_EXPECT(obj.object->getFieldU8(sfCloseResolution) == 255); } - catch (std::runtime_error& e) + + // test with uint value { - std::string what(e.what()); - unexpected(what.find("First level children of `Template`") != 0); + Json::Value j; + j[sfCloseResolution] = 255u; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfCloseResolution)); + BEAST_EXPECT(obj.object->getFieldU8(sfCloseResolution) == 255); + } + + // Test with string value + { + Json::Value j; + j[sfCloseResolution] = "255"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfCloseResolution)); + BEAST_EXPECT(obj.object->getFieldU8(sfCloseResolution) == 255); + } + + // Test min value for uint8 + { + Json::Value j; + j[sfCloseResolution] = 0; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->getFieldU8(sfCloseResolution) == 0); + } + + // Test out of range value for UInt8 (negative) + { + Json::Value j; + j[sfCloseResolution] = -1; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test out of range value for UInt8 (too large) + { + Json::Value j; + j[sfCloseResolution] = 256; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test bad_type (not a string/int/uint) + { + Json::Value j; + j[sfCloseResolution] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test bad_type (not a string/int/uint) + { + Json::Value j; + j[sfCloseResolution] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); } } void - testParseJSONArray() + testUInt16() { - testcase("parse json array"); - std::string const json( - "{\"Template\":[{\"ModifiedNode\":{\"Sequence\":1}}]}"); - - Json::Value jsonObject; - bool parsedOK(parseJSONString(json, jsonObject)); - if (parsedOK) + testcase("UInt16"); + // Test with int value { - STParsedJSONObject parsed("test", jsonObject); - BEAST_EXPECT(parsed.object); - std::string const& serialized( - to_string(parsed.object->getJson(JsonOptions::none))); - BEAST_EXPECT(serialized == json); + Json::Value j; + j[sfLedgerEntryType] = 65535; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfLedgerEntryType)); + BEAST_EXPECT(obj.object->getFieldU16(sfLedgerEntryType) == 65535); } - else + + // Test with uint value { - fail("Couldn't parse json: " + json); + Json::Value j; + j[sfLedgerEntryType] = 65535u; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfLedgerEntryType)); + BEAST_EXPECT(obj.object->getFieldU16(sfLedgerEntryType) == 65535); + } + + // Test with string value + { + Json::Value j; + j[sfLedgerEntryType] = "65535"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfLedgerEntryType)); + BEAST_EXPECT(obj.object->getFieldU16(sfLedgerEntryType) == 65535); + } + + // Test min value for uint16 + { + Json::Value j; + j[sfLedgerEntryType] = 0; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->getFieldU16(sfLedgerEntryType) == 0); + } + + // Test out of range value for UInt16 (negative) + { + Json::Value j; + j[sfLedgerEntryType] = -1; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test out of range value for UInt16 (too large) + { + Json::Value j; + j[sfLedgerEntryType] = 65536; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test string value out of range + { + Json::Value j; + j[sfLedgerEntryType] = "65536"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test bad_type (not a string/int/uint) + { + Json::Value j; + j[sfLedgerEntryType] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test bad_type (not a string/int/uint) + { + Json::Value j; + j[sfLedgerEntryType] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid input for other field + { + Json::Value j; + j[sfTransferFee] = "Payment"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); } } void - testParseJSONEdgeCases() + testUInt32() { - testcase("parse json object"); + testcase("UInt32"); + { + Json::Value j; + j[sfNetworkID] = 4294967295u; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfNetworkID)); + BEAST_EXPECT(obj.object->getFieldU32(sfNetworkID) == 4294967295u); + } + + // Test with string value + { + Json::Value j; + j[sfNetworkID] = "4294967295"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfNetworkID)); + BEAST_EXPECT(obj.object->getFieldU32(sfNetworkID) == 4294967295u); + } + + // Test min value for uint32 + { + Json::Value j; + j[sfNetworkID] = 0; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->getFieldU32(sfNetworkID) == 0); + } + + // Test out of range value for uint32 (negative) + { + Json::Value j; + j[sfNetworkID] = -1; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test string value out of range + { + Json::Value j; + j[sfNetworkID] = "4294967296"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test bad_type (arrayValue) + { + Json::Value j; + j[sfNetworkID] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test bad_type (objectValue) + { + Json::Value j; + j[sfNetworkID] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testUInt64() + { + testcase("UInt64"); + { + Json::Value j; + j[sfIndexNext] = "ffffffffffffffff"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfIndexNext)); + BEAST_EXPECT( + obj.object->getFieldU64(sfIndexNext) == + 18446744073709551615ull); + } + + // Test min value for uint64 + { + Json::Value j; + j[sfIndexNext] = 0; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->getFieldU64(sfIndexNext) == 0ull); + } + + // Test out of range value for uint64 (negative) + { + Json::Value j; + j[sfIndexNext] = -1; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // NOTE: the JSON parser doesn't support > UInt32, so those values must + // be in hex + // Test string value out of range + // string is interpreted as hex + { + Json::Value j; + j[sfIndexNext] = "10000000000000000"; // uint64 max + 1 (in hex) + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test hex string value with 0x prefix (should fail) + { + Json::Value j; + j[sfIndexNext] = "0xabcdefabcdef"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test hex string value with invalid characters + { + Json::Value j; + j[sfIndexNext] = "abcdefga"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // test arrayValue + { + Json::Value j; + j[sfIndexNext] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // test objectValue + { + Json::Value j; + j[sfIndexNext] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testUInt128() + { + testcase("UInt128"); + { + Json::Value j; + j[sfEmailHash] = "0123456789ABCDEF0123456789ABCDEF"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfEmailHash)); + BEAST_EXPECT(obj.object->getFieldH128(sfEmailHash).size() == 16); + std::array expected = { + 0x01, + 0x23, + 0x45, + 0x67, + 0x89, + 0xAB, + 0xCD, + 0xEF, + 0x01, + 0x23, + 0x45, + 0x67, + 0x89, + 0xAB, + 0xCD, + 0xEF}; + BEAST_EXPECT( + obj.object->getFieldH128(sfEmailHash) == uint128{expected}); + } + + // Valid lowercase hex string for UInt128 + { + Json::Value j; + j[sfEmailHash] = "0123456789abcdef0123456789abcdef"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfEmailHash)); + BEAST_EXPECT(obj.object->getFieldH128(sfEmailHash).size() == 16); + } + + // Empty string for UInt128 (should be valid, all zero) + { + Json::Value j; + j[sfEmailHash] = ""; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfEmailHash)); + auto const& h128 = obj.object->getFieldH128(sfEmailHash); + BEAST_EXPECT(h128.size() == 16); + bool allZero = std::all_of( + h128.begin(), h128.end(), [](auto b) { return b == 0; }); + BEAST_EXPECT(allZero); + } + + // Odd-length hex string for UInt128 (should fail) + { + Json::Value j; + j[sfEmailHash] = "0123456789ABCDEF0123456789ABCDE"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Non-hex string for UInt128 (should fail) + { + Json::Value j; + j[sfEmailHash] = "nothexstring"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Hex string too short for UInt128 (should fail) + { + Json::Value j; + j[sfEmailHash] = "01234567"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Hex string too long for UInt128 (should fail) + { + Json::Value j; + j[sfEmailHash] = "0123456789ABCDEF0123456789ABCDEF00"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Array value for UInt128 (should fail) + { + Json::Value j; + j[sfEmailHash] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Object value for UInt128 (should fail) + { + Json::Value j; + j[sfEmailHash] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testUInt160() + { + testcase("UInt160"); + { + Json::Value j; + j[sfTakerPaysCurrency] = "0123456789ABCDEF0123456789ABCDEF01234567"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfTakerPaysCurrency)); + BEAST_EXPECT( + obj.object->getFieldH160(sfTakerPaysCurrency).size() == 20); + std::array expected = { + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23, + 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67}; + BEAST_EXPECT( + obj.object->getFieldH160(sfTakerPaysCurrency) == + uint160{expected}); + } + // Valid lowercase hex string for UInt160 + { + Json::Value j; + j[sfTakerPaysCurrency] = "0123456789abcdef0123456789abcdef01234567"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfTakerPaysCurrency)); + BEAST_EXPECT( + obj.object->getFieldH160(sfTakerPaysCurrency).size() == 20); + } + + // Empty string for UInt160 (should be valid, all zero) + { + Json::Value j; + j[sfTakerPaysCurrency] = ""; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfTakerPaysCurrency)); + auto const& h160 = obj.object->getFieldH160(sfTakerPaysCurrency); + BEAST_EXPECT(h160.size() == 20); + bool allZero = std::all_of( + h160.begin(), h160.end(), [](auto b) { return b == 0; }); + BEAST_EXPECT(allZero); + } + + // Non-hex string for UInt160 (should fail) + { + Json::Value j; + j[sfTakerPaysCurrency] = "nothexstring"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Hex string too short for UInt160 (should fail) + { + Json::Value j; + j[sfTakerPaysCurrency] = "01234567"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Hex string too long for UInt160 (should fail) + { + Json::Value j; + j[sfTakerPaysCurrency] = + "0123456789ABCDEF0123456789ABCDEF0123456789"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Array value for UInt160 (should fail) + { + Json::Value j; + j[sfTakerPaysCurrency] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Object value for UInt160 (should fail) + { + Json::Value j; + j[sfTakerPaysCurrency] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testUInt192() + { + testcase("UInt192"); + { + Json::Value j; + j[sfMPTokenIssuanceID] = + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfMPTokenIssuanceID)); + BEAST_EXPECT( + obj.object->getFieldH192(sfMPTokenIssuanceID).size() == 24); + std::array expected = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + BEAST_EXPECT( + obj.object->getFieldH192(sfMPTokenIssuanceID) == + uint192{expected}); + } + + // Valid lowercase hex string for UInt192 + { + Json::Value j; + j[sfMPTokenIssuanceID] = + "ffffffffffffffffffffffffffffffffffffffffffffffff"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfMPTokenIssuanceID)); + BEAST_EXPECT( + obj.object->getFieldH192(sfMPTokenIssuanceID).size() == 24); + } + + // Empty string for UInt192 (should be valid, all zero) + { + Json::Value j; + j[sfMPTokenIssuanceID] = ""; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfMPTokenIssuanceID)); + auto const& h192 = obj.object->getFieldH192(sfMPTokenIssuanceID); + BEAST_EXPECT(h192.size() == 24); + bool allZero = std::all_of( + h192.begin(), h192.end(), [](auto b) { return b == 0; }); + BEAST_EXPECT(allZero); + } + + // Odd-length hex string for UInt192 (should fail) + { + Json::Value j; + j[sfMPTokenIssuanceID] = + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDE"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Non-hex string for UInt192 (should fail) + { + Json::Value j; + j[sfMPTokenIssuanceID] = "nothexstring"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Hex string too short for UInt192 (should fail) + { + Json::Value j; + j[sfMPTokenIssuanceID] = "01234567"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Hex string too long for UInt192 (should fail) + { + Json::Value j; + j[sfMPTokenIssuanceID] = + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF00"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Array value for UInt192 (should fail) + { + Json::Value j; + j[sfMPTokenIssuanceID] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Object value for UInt192 (should fail) + { + Json::Value j; + j[sfMPTokenIssuanceID] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testUInt256() + { + testcase("UInt256"); + // Test with valid hex string for UInt256 + { + Json::Value j; + j[sfLedgerHash] = + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCD" + "EF"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfLedgerHash)); + BEAST_EXPECT(obj.object->getFieldH256(sfLedgerHash).size() == 32); + std::array expected = { + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF}; + BEAST_EXPECT( + obj.object->getFieldH256(sfLedgerHash) == uint256{expected}); + } + // Valid lowercase hex string for UInt256 + { + Json::Value j; + j[sfLedgerHash] = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcd" + "ef"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfLedgerHash)); + BEAST_EXPECT(obj.object->getFieldH256(sfLedgerHash).size() == 32); + } + + // Empty string for UInt256 (should be valid, all zero) + { + Json::Value j; + j[sfLedgerHash] = ""; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfLedgerHash)); + auto const& h256 = obj.object->getFieldH256(sfLedgerHash); + BEAST_EXPECT(h256.size() == 32); + bool allZero = std::all_of( + h256.begin(), h256.end(), [](auto b) { return b == 0; }); + BEAST_EXPECT(allZero); + } + + // Odd-length hex string for UInt256 (should fail) + { + Json::Value j; + j[sfLedgerHash] = + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCD" + "E"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Non-hex string for UInt256 (should fail) + { + Json::Value j; + j[sfLedgerHash] = "nothexstring"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Hex string too short for UInt256 (should fail) + { + Json::Value j; + j[sfLedgerHash] = "01234567"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Hex string too long for UInt256 (should fail) + { + Json::Value j; + j[sfLedgerHash] = + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCD" + "EF00"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Array value for UInt256 (should fail) + { + Json::Value j; + j[sfLedgerHash] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Object value for UInt256 (should fail) + { + Json::Value j; + j[sfLedgerHash] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testBlob() + { + testcase("Blob"); + // Test with valid hex string for blob + { + Json::Value j; + j[sfPublicKey] = "DEADBEEF"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfPublicKey)); + auto const& blob = obj.object->getFieldVL(sfPublicKey); + BEAST_EXPECT(blob.size() == 4); + BEAST_EXPECT(blob[0] == 0xDE); + BEAST_EXPECT(blob[1] == 0xAD); + BEAST_EXPECT(blob[2] == 0xBE); + BEAST_EXPECT(blob[3] == 0xEF); + } + + // Test empty string for blob (should be valid, size 0) + { + Json::Value j; + j[sfPublicKey] = ""; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfPublicKey)); + auto const& blob = obj.object->getFieldVL(sfPublicKey); + BEAST_EXPECT(blob.size() == 0); + } + + // Test lowercase hex string for blob + { + Json::Value j; + j[sfPublicKey] = "deadbeef"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfPublicKey)); + auto const& blob = obj.object->getFieldVL(sfPublicKey); + BEAST_EXPECT(blob.size() == 4); + BEAST_EXPECT(blob[0] == 0xDE); + BEAST_EXPECT(blob[1] == 0xAD); + BEAST_EXPECT(blob[2] == 0xBE); + BEAST_EXPECT(blob[3] == 0xEF); + } + + // Test non-hex string for blob (should fail) + { + Json::Value j; + j[sfPublicKey] = "XYZ123"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test array value for blob (should fail) + { + Json::Value j; + j[sfPublicKey] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test object value for blob (should fail) + { + Json::Value j; + j[sfPublicKey] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testVector256() + { + testcase("Vector256"); + // Test with valid array of hex strings for Vector256 + { + Json::Value j; + Json::Value arr(Json::arrayValue); + arr.append( + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCD" + "EF"); + arr.append( + "FEDCBA9876543210FEDCBA9876543210FEDCBA9876543210FEDCBA98765432" + "10"); + j[sfHashes] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfHashes)); + auto const& vec = obj.object->getFieldV256(sfHashes); + BEAST_EXPECT(vec.size() == 2); + BEAST_EXPECT(to_string(vec[0]) == arr[0u].asString()); + BEAST_EXPECT(to_string(vec[1]) == arr[1u].asString()); + } + // Test empty array for Vector256 (should be valid, size 0) + { + Json::Value j; + Json::Value arr(Json::arrayValue); + j[sfHashes] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfHashes)); + auto const& vec = obj.object->getFieldV256(sfHashes); + BEAST_EXPECT(vec.size() == 0); + } + + // Test array with invalid hex string (should fail) + { + Json::Value j; + Json::Value arr(Json::arrayValue); + arr.append("nothexstring"); + j[sfHashes] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test array with string of wrong length (should fail) + { + Json::Value j; + Json::Value arr(Json::arrayValue); + arr.append("0123456789ABCDEF"); // too short for uint256 + j[sfHashes] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test array with non-string element (should fail) + { + Json::Value j; + Json::Value arr(Json::arrayValue); + arr.append(12345); + j[sfHashes] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test non-array value for Vector256 (should fail) + { + Json::Value j; + j[sfHashes] = "notanarray"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test array with object element (should fail) + { + Json::Value j; + Json::Value arr(Json::arrayValue); + Json::Value objElem(Json::objectValue); + objElem["foo"] = "bar"; + arr.append(objElem); + j[sfHashes] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testAccount() + { + testcase("Account"); + // Test with valid base58 string for AccountID + { + Json::Value j; + j[sfAccount] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfAccount)); + auto const& acct = obj.object->getAccountID(sfAccount); + BEAST_EXPECT(acct.size() == 20); + BEAST_EXPECT( + toBase58(acct) == "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"); + } + + // Valid hex string for AccountID + { + Json::Value j; + j[sfAccount] = "000102030405060708090A0B0C0D0E0F10111213"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfAccount)); + auto const& acct = obj.object->getAccountID(sfAccount); + BEAST_EXPECT(acct.size() == 20); + } + + // Invalid base58 string for AccountID + { + Json::Value j; + j[sfAccount] = "notAValidBase58Account"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid hex string for AccountID (too short) + { + Json::Value j; + j[sfAccount] = "001122334455"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid hex string for AccountID (too long) + { + Json::Value j; + j[sfAccount] = "000102030405060708090A0B0C0D0E0F101112131415"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid hex string for AccountID (bad chars) + { + Json::Value j; + j[sfAccount] = "000102030405060708090A0B0C0D0E0F1011121G"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Empty string for AccountID (should fail) + { + Json::Value j; + j[sfAccount] = ""; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Array value for AccountID (should fail) + { + Json::Value j; + j[sfAccount] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Object value for AccountID (should fail) + { + Json::Value j; + j[sfAccount] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testCurrency() + { + testcase("Currency"); + // Test with valid ISO code for currency + { + Json::Value j; + j[sfBaseAsset] = "USD"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfBaseAsset)); + auto const& curr = obj.object->getFieldCurrency(sfBaseAsset); + BEAST_EXPECT(curr.currency().size() == 20); + } + + // Valid ISO code + { + Json::Value j; + j[sfBaseAsset] = "EUR"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfBaseAsset)); + auto const& curr = obj.object->getFieldCurrency(sfBaseAsset); + BEAST_EXPECT(curr.currency().size() == 20); + } + + // Valid hex string for currency + { + Json::Value j; + j[sfBaseAsset] = "0123456789ABCDEF01230123456789ABCDEF0123"; + STParsedJSONObject obj("Test", j); + if (BEAST_EXPECT(obj.object.has_value())) + { + BEAST_EXPECT(obj.object->isFieldPresent(sfBaseAsset)); + auto const& curr = obj.object->getFieldCurrency(sfBaseAsset); + BEAST_EXPECT(curr.currency().size() == 20); + } + } + + // Invalid ISO code (too long) + { + Json::Value j; + j[sfBaseAsset] = "USDD"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // lowercase ISO code + { + Json::Value j; + j[sfBaseAsset] = "usd"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfBaseAsset)); + auto const& curr = obj.object->getFieldCurrency(sfBaseAsset); + BEAST_EXPECT(curr.currency().size() == 20); + } + + // Invalid hex string (too short) + { + Json::Value j; + j[sfBaseAsset] = "0123456789AB"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid hex string (too long) + { + Json::Value j; + j[sfBaseAsset] = "0123456789ABCDEF0123456789"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Empty string for currency (should fail) + { + Json::Value j; + j[sfBaseAsset] = ""; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfBaseAsset)); + auto const& curr = obj.object->getFieldCurrency(sfBaseAsset); + BEAST_EXPECT(curr.currency().size() == 20); + } + + // Array value for currency (should fail) + { + Json::Value j; + j[sfBaseAsset] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Object value for currency (should fail) + { + Json::Value j; + j[sfBaseAsset] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testAmount() + { + testcase("Amount"); + // Test with string value for Amount + { + Json::Value j; + j[sfAmount] = "100000000000000000"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfAmount)); + BEAST_EXPECT( + obj.object->getFieldAmount(sfAmount) == + STAmount(100000000000000000ull)); + } + + // Test with int value for Amount + { + Json::Value j; + j[sfAmount] = 4294967295u; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfAmount)); + BEAST_EXPECT( + obj.object->getFieldAmount(sfAmount) == STAmount(4294967295u)); + } + + // Test with decimal string for Amount (should fail) + { + Json::Value j; + j[sfAmount] = "123.45"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with empty string for Amount (should fail) + { + Json::Value j; + j[sfAmount] = ""; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with non-numeric string for Amount (should fail) + { + Json::Value j; + j[sfAmount] = "notanumber"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with object value for Amount (should fail) + { + Json::Value j; + j[sfAmount] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testPathSet() + { + testcase("PathSet"); + // Valid test: single path with single element + { + Json::Value j; + Json::Value path(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem["account"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + elem["currency"] = "USD"; + elem["issuer"] = "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe"; + path.append(elem); + Json::Value pathset(Json::arrayValue); + pathset.append(path); + j[sfPaths] = pathset; + STParsedJSONObject obj("Test", j); + if (BEAST_EXPECT(obj.object.has_value())) + { + BEAST_EXPECT(obj.object->isFieldPresent(sfPaths)); + auto const& ps = obj.object->getFieldPathSet(sfPaths); + BEAST_EXPECT(!ps.empty()); + BEAST_EXPECT(ps.size() == 1); + BEAST_EXPECT(ps[0].size() == 1); + BEAST_EXPECT( + ps[0][0].getAccountID() == + parseBase58( + "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh")); + BEAST_EXPECT(to_string(ps[0][0].getCurrency()) == "USD"); + BEAST_EXPECT( + ps[0][0].getIssuerID() == + parseBase58( + "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe")); + } + } + + // Valid test: non-standard currency code + { + Json::Value j; + Json::Value path(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem["account"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + elem["currency"] = "0123456789ABCDEF01230123456789ABCDEF0123"; + elem["issuer"] = "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe"; + path.append(elem); + Json::Value pathset(Json::arrayValue); + pathset.append(path); + j[sfPaths] = pathset; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfPaths)); + auto const& ps = obj.object->getFieldPathSet(sfPaths); + BEAST_EXPECT(!ps.empty()); + } + + // Test with non-array value for PathSet (should fail) + { + Json::Value j; + j[sfPaths] = "notanarray"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with array containing non-array element (should fail) + { + Json::Value j; + Json::Value pathset(Json::arrayValue); + pathset.append("notanarray"); + j[sfPaths] = pathset; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with array containing array with non-object element (should + // fail) + { + Json::Value j; + Json::Value path(Json::arrayValue); + path.append("notanobject"); + Json::Value pathset(Json::arrayValue); + pathset.append(path); + j[sfPaths] = pathset; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with array containing array with object missing required keys + // (should fail) + { + Json::Value j; + Json::Value path(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem["foo"] = "bar"; // not a valid path element key + path.append(elem); + Json::Value pathset(Json::arrayValue); + pathset.append(path); + j[sfPaths] = pathset; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with array containing array with object with invalid account + // value (should fail) + { + Json::Value j; + Json::Value path(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem["account"] = "notAValidBase58Account"; + path.append(elem); + Json::Value pathset(Json::arrayValue); + pathset.append(path); + j[sfPaths] = pathset; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with account not string (should fail) + { + Json::Value j; + Json::Value path(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem["account"] = 12345; + path.append(elem); + Json::Value pathset(Json::arrayValue); + pathset.append(path); + j[sfPaths] = pathset; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with currency not string (should fail) + { + Json::Value j; + Json::Value path(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem["currency"] = 12345; + path.append(elem); + Json::Value pathset(Json::arrayValue); + pathset.append(path); + j[sfPaths] = pathset; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with non-standard currency not hex (should fail) + { + Json::Value j; + Json::Value path(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem["currency"] = "notAValidCurrency"; + path.append(elem); + Json::Value pathset(Json::arrayValue); + pathset.append(path); + j[sfPaths] = pathset; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with issuer not string (should fail) + { + Json::Value j; + Json::Value path(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem["issuer"] = 12345; + path.append(elem); + Json::Value pathset(Json::arrayValue); + pathset.append(path); + j[sfPaths] = pathset; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with issuer not base58 (should fail) + { + Json::Value j; + Json::Value path(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem["issuer"] = "notAValidBase58Account"; + path.append(elem); + Json::Value pathset(Json::arrayValue); + pathset.append(path); + j[sfPaths] = pathset; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testIssue() + { + testcase("Issue"); + // Valid Issue: currency and issuer as base58 + { + Json::Value j; + Json::Value issueJson(Json::objectValue); + issueJson["currency"] = "USD"; + issueJson["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + j[sfAsset] = issueJson; + STParsedJSONObject obj("Test", j); + if (BEAST_EXPECTS( + obj.object.has_value(), obj.error.toStyledString())) + { + BEAST_EXPECT(obj.object->isFieldPresent(sfAsset)); + auto const& issueField = (*obj.object)[sfAsset]; + auto const issue = issueField.value().get(); + BEAST_EXPECT(issue.currency.size() == 20); + BEAST_EXPECT(to_string(issue.currency) == "USD"); + BEAST_EXPECT(issue.account.size() == 20); + BEAST_EXPECT( + issue.account == + parseBase58( + "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh")); + } + } + + // Valid Issue: currency as hex + { + Json::Value j; + Json::Value issueJson(Json::objectValue); + issueJson["currency"] = "0123456789ABCDEF01230123456789ABCDEF0123"; + issueJson["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + j[sfAsset] = issueJson; + STParsedJSONObject obj("Test", j); + if (BEAST_EXPECT(obj.object.has_value())) + { + BEAST_EXPECT(obj.object->isFieldPresent(sfAsset)); + auto const& issueField = (*obj.object)[sfAsset]; + auto const issue = issueField.value().get(); + BEAST_EXPECT(issue.currency.size() == 20); + BEAST_EXPECT(issue.account.size() == 20); + } + } + + // Valid Issue: MPTID + { + Json::Value j; + Json::Value issueJson(Json::objectValue); + issueJson["mpt_issuance_id"] = + "0000000000000000000000004D5054494431323334234234"; + j[sfAsset] = issueJson; + STParsedJSONObject obj("Test", j); + if (BEAST_EXPECT(obj.object.has_value())) + { + BEAST_EXPECT(obj.object->isFieldPresent(sfAsset)); + auto const& issueField = (*obj.object)[sfAsset]; + auto const issue = issueField.value().get(); + BEAST_EXPECT(issue.getMptID().size() == 24); + } + } + + // Invalid Issue: missing currency + { + Json::Value j; + Json::Value issue(Json::objectValue); + issue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + j[sfAsset] = issue; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid Issue: missing issuer + { + Json::Value j; + Json::Value issue(Json::objectValue); + issue["currency"] = "USD"; + j[sfAsset] = issue; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid Issue: currency too long + { + Json::Value j; + Json::Value issue(Json::objectValue); + issue["currency"] = "USDD"; + issue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + j[sfAsset] = issue; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid Issue: issuer not base58 or hex + { + Json::Value j; + Json::Value issue(Json::objectValue); + issue["currency"] = "USD"; + issue["issuer"] = "notAValidIssuer"; + j[sfAsset] = issue; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid Issue: currency not string + { + Json::Value j; + Json::Value issue(Json::objectValue); + issue["currency"] = Json::Value(Json::arrayValue); + issue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + j[sfAsset] = issue; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid Issue: issuer not string + { + Json::Value j; + Json::Value issue(Json::objectValue); + issue["currency"] = "USD"; + issue["issuer"] = Json::Value(Json::objectValue); + j[sfAsset] = issue; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid Issue: not an object + { + Json::Value j; + j[sfAsset] = "notanobject"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testXChainBridge() + { + testcase("XChainBridge"); + // Valid XChainBridge + { + Json::Value j; + Json::Value bridge(Json::objectValue); + Json::Value issuingChainIssue(Json::objectValue); + issuingChainIssue["currency"] = "USD"; + issuingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + Json::Value lockingChainIssue(Json::objectValue); + lockingChainIssue["currency"] = "EUR"; + lockingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["LockingChainIssue"] = lockingChainIssue; + bridge["IssuingChainIssue"] = issuingChainIssue; + bridge["LockingChainDoor"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["IssuingChainDoor"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + j[sfXChainBridge] = bridge; + STParsedJSONObject obj("Test", j); + if (BEAST_EXPECT(obj.object.has_value())) + { + BEAST_EXPECT(obj.object->isFieldPresent(sfXChainBridge)); + auto const& bridgeField = (*obj.object)[sfXChainBridge]; + BEAST_EXPECT( + bridgeField->lockingChainIssue().currency.size() == 20); + BEAST_EXPECT( + bridgeField->issuingChainIssue().currency.size() == 20); + } + } + + // Valid XChainBridge: issues as hex currency + { + Json::Value j; + Json::Value bridge(Json::objectValue); + Json::Value issuingChainIssue(Json::objectValue); + issuingChainIssue["currency"] = + "0123456789ABCDEF01230123456789ABCDEF0123"; + issuingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + Json::Value lockingChainIssue(Json::objectValue); + lockingChainIssue["currency"] = + "0123456789ABCDEF01230123456789ABCDEF0123"; + lockingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["LockingChainIssue"] = lockingChainIssue; + bridge["IssuingChainIssue"] = issuingChainIssue; + bridge["LockingChainDoor"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["IssuingChainDoor"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + j[sfXChainBridge] = bridge; + STParsedJSONObject obj("Test", j); + if (BEAST_EXPECT(obj.object.has_value())) + { + BEAST_EXPECT(obj.object->isFieldPresent(sfXChainBridge)); + auto const& bridgeField = (*obj.object)[sfXChainBridge]; + BEAST_EXPECT( + bridgeField->lockingChainIssue().currency.size() == 20); + BEAST_EXPECT( + bridgeField->issuingChainIssue().currency.size() == 20); + } + } + + // Invalid XChainBridge: missing LockingChainIssue + { + Json::Value j; + Json::Value bridge(Json::objectValue); + Json::Value issuingChainIssue(Json::objectValue); + issuingChainIssue["currency"] = "USD"; + issuingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["IssuingChainIssue"] = issuingChainIssue; + bridge["LockingChainDoor"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["IssuingChainDoor"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + j[sfXChainBridge] = bridge; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid XChainBridge: missing IssuingChainIssue + { + Json::Value j; + Json::Value bridge(Json::objectValue); + Json::Value lockingChainIssue(Json::objectValue); + lockingChainIssue["currency"] = "EUR"; + lockingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["LockingChainIssue"] = lockingChainIssue; + bridge["LockingChainDoor"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["IssuingChainDoor"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + j[sfXChainBridge] = bridge; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid XChainBridge: missing LockingChainDoor + { + Json::Value j; + Json::Value bridge(Json::objectValue); + Json::Value issuingChainIssue(Json::objectValue); + issuingChainIssue["currency"] = "USD"; + issuingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["IssuingChainIssue"] = issuingChainIssue; + Json::Value lockingChainIssue(Json::objectValue); + lockingChainIssue["currency"] = "EUR"; + lockingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["LockingChainIssue"] = lockingChainIssue; + bridge["IssuingChainDoor"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + j[sfXChainBridge] = bridge; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid XChainBridge: missing IssuingChainDoor + { + Json::Value j; + Json::Value bridge(Json::objectValue); + Json::Value issuingChainIssue(Json::objectValue); + issuingChainIssue["currency"] = "USD"; + issuingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["IssuingChainIssue"] = issuingChainIssue; + Json::Value lockingChainIssue(Json::objectValue); + lockingChainIssue["currency"] = "EUR"; + lockingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["LockingChainIssue"] = lockingChainIssue; + bridge["LockingChainDoor"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + j[sfXChainBridge] = bridge; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid XChainBridge: IssuingChainIssue not an object + { + Json::Value j; + Json::Value bridge(Json::objectValue); + bridge["LockingChainIssue"] = "notanobject"; + bridge["IssuingChainIssue"] = "notanobject"; + j[sfXChainBridge] = bridge; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid XChainBridge: IssuingChainIssue missing currency + { + Json::Value j; + Json::Value bridge(Json::objectValue); + Json::Value asset(Json::objectValue); + asset["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + Json::Value lockingChainIssue(Json::objectValue); + lockingChainIssue["currency"] = "EUR"; + lockingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["LockingChainIssue"] = lockingChainIssue; + bridge["IssuingChainIssue"] = asset; + j[sfXChainBridge] = bridge; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid XChainBridge: asset missing issuer + { + Json::Value j; + Json::Value bridge(Json::objectValue); + Json::Value asset(Json::objectValue); + asset["currency"] = "USD"; + Json::Value lockingChainIssue(Json::objectValue); + lockingChainIssue["currency"] = "EUR"; + lockingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["LockingChainIssue"] = lockingChainIssue; + bridge["IssuingChainIssue"] = asset; + j[sfXChainBridge] = bridge; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid XChainBridge: asset issuer not base58 + { + Json::Value j; + Json::Value bridge(Json::objectValue); + Json::Value asset(Json::objectValue); + asset["currency"] = "USD"; + asset["issuer"] = "notAValidBase58Account"; + Json::Value lockingChainIssue(Json::objectValue); + lockingChainIssue["currency"] = "EUR"; + lockingChainIssue["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + bridge["LockingChainIssue"] = lockingChainIssue; + bridge["IssuingChainIssue"] = asset; + j[sfXChainBridge] = bridge; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid XChainBridge: not an object + { + Json::Value j; + j[sfXChainBridge] = "notanobject"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testNumber() + { + testcase("Number"); + // Valid integer value for STNumber + { + Json::Value j; + j[sfNumber] = 12345; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfNumber)); + BEAST_EXPECT( + obj.object->getFieldNumber(sfNumber).value() == + Number(12345, 0)); + } + + // Valid uint value for STNumber + { + Json::Value j; + j[sfNumber] = 12345u; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfNumber)); + BEAST_EXPECT( + obj.object->getFieldNumber(sfNumber).value() == + Number(12345, 0)); + } + + // Valid string integer value for STNumber + { + Json::Value j; + j[sfNumber] = "67890"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfNumber)); + BEAST_EXPECT( + obj.object->getFieldNumber(sfNumber).value() == + Number(67890, 0)); + } + + // Valid negative integer value for STNumber + { + Json::Value j; + j[sfNumber] = -42; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfNumber)); + BEAST_EXPECT( + obj.object->getFieldNumber(sfNumber).value() == Number(-42, 0)); + } + + // Valid string negative integer value for STNumber + { + Json::Value j; + j[sfNumber] = "-123"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfNumber)); + BEAST_EXPECT( + obj.object->getFieldNumber(sfNumber).value() == + Number(-123, 0)); + } + + // Valid floating point value for STNumber + { + Json::Value j; + j[sfNumber] = "3.14159"; + STParsedJSONObject obj("Test", j); + if (BEAST_EXPECT(obj.object.has_value())) + { + BEAST_EXPECT(obj.object->isFieldPresent(sfNumber)); + BEAST_EXPECT( + obj.object->getFieldNumber(sfNumber).value() == + Number(314159, -5)); + } + } + + // Invalid string value for STNumber (not a number) + { + Json::Value j; + j[sfNumber] = "notanumber"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid array value for STNumber + { + Json::Value j; + j[sfNumber] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Invalid object value for STNumber + { + Json::Value j; + j[sfNumber] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Empty string for STNumber (should fail) + { + Json::Value j; + j[sfNumber] = ""; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + + void + testObject() + { + testcase("Object"); + // Test with valid object for Object + { + Json::Value j; + Json::Value objVal(Json::objectValue); + objVal[sfTransactionResult] = 1; + j[sfTransactionMetaData] = objVal; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfTransactionMetaData)); + auto const& result = + obj.object->peekFieldObject(sfTransactionMetaData); + BEAST_EXPECT(result.getFieldU8(sfTransactionResult) == 1); + } + + // Test with non-object value for Object (should fail) + { + Json::Value j; + j[sfTransactionMetaData] = "notanobject"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with array value for Object (should fail) + { + Json::Value j; + Json::Value arr(Json::arrayValue); + arr.append(1); + j[sfTransactionMetaData] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with null value for Object (should fail) + { + Json::Value j; + j[sfTransactionMetaData] = Json::Value(Json::nullValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with max depth (should succeed) + // max depth is 64 + { + Json::Value j; + Json::Value obj(Json::objectValue); + Json::Value* current = &obj; + for (int i = 0; i < 63; ++i) + { + Json::Value next(Json::objectValue); + (*current)[sfTransactionMetaData] = next; + current = &((*current)[sfTransactionMetaData]); + } + (*current)[sfTransactionResult.getJsonName()] = 1; + j[sfTransactionMetaData] = obj; + STParsedJSONObject parsed("Test", j); + BEAST_EXPECT(parsed.object.has_value()); + BEAST_EXPECT(parsed.object->isFieldPresent(sfTransactionMetaData)); + } + + // Test with depth exceeding maxDepth (should fail) + { + Json::Value j; + Json::Value obj(Json::objectValue); + Json::Value* current = &obj; + for (int i = 0; i < 64; ++i) + { + Json::Value next(Json::objectValue); + (*current)[sfTransactionMetaData] = next; + current = &((*current)[sfTransactionMetaData]); + } + (*current)[sfTransactionResult.getJsonName()] = 1; + j[sfTransactionMetaData] = obj; + STParsedJSONObject parsed("Test", j); + BEAST_EXPECT(!parsed.object.has_value()); + } + } + + void + testArray() + { + testcase("Array"); + // Test with valid array for Array + { + Json::Value j; + Json::Value arr(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem[sfTransactionResult] = 2; + Json::Value elem2(Json::objectValue); + elem2[sfTransactionMetaData] = elem; + arr.append(elem2); + j[sfSignerEntries] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfSignerEntries)); + auto const& result = obj.object->getFieldArray(sfSignerEntries); + if (BEAST_EXPECT(result.size() == 1)) + { + BEAST_EXPECT(result[0].getFName() == sfTransactionMetaData); + BEAST_EXPECT(result[0].getJson(0) == elem); + } + } + + // Test with array containing non-object element (should fail) + { + Json::Value j; + Json::Value arr(Json::arrayValue); + arr.append("notanobject"); + j[sfSignerEntries] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with array containing object with invalid field (should fail) + { + Json::Value j; + Json::Value arr(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem["invalidField"] = 1; + arr.append(elem); + j[sfSignerEntries] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with array containing object with multiple keys (should fail) + { + Json::Value j; + Json::Value arr(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem[sfTransactionResult] = 2; + elem[sfNetworkID] = 3; + arr.append(elem); + j[sfSignerEntries] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with non-array value for Array (should fail) + { + Json::Value j; + j[sfSignerEntries] = "notanarray"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with array containing object with valid field but invalid value + // (should fail) + { + Json::Value j; + Json::Value arr(Json::arrayValue); + Json::Value elem(Json::objectValue); + elem[sfTransactionResult] = "notanint"; + arr.append(elem); + j[sfSignerEntries] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test with empty array for Array (should be valid) + { + Json::Value j; + Json::Value arr(Json::arrayValue); + j[sfSignerEntries] = arr; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + BEAST_EXPECT(obj.object->isFieldPresent(sfSignerEntries)); + } + + // Test with object provided but not object SField + { + Json::Value j; + Json::Value obj(Json::arrayValue); + obj.append(Json::Value(Json::objectValue)); + obj[0u][sfTransactionResult] = 1; + j[sfSignerEntries] = obj; + STParsedJSONObject parsed("Test", j); + BEAST_EXPECT(!parsed.object.has_value()); + } + + // Test invalid children + { + try + { + /* + + STArray/STObject constructs don't really map perfectly to json + arrays/objects. + + STObject is an associative container, mapping fields to value, + but an STObject may also have a Field as its name, stored + outside the associative structure. The name is important, so to + maintain fidelity, it will take TWO json objects to represent + them. + + */ + std::string faulty( + "{\"Template\":[{" + "\"ModifiedNode\":{\"Sequence\":1}, " + "\"DeletedNode\":{\"Sequence\":1}" + "}]}"); + + std::unique_ptr so; + Json::Value faultyJson; + bool parsedOK(parseJSONString(faulty, faultyJson)); + unexpected(!parsedOK, "failed to parse"); + STParsedJSONObject parsed("test", faultyJson); + BEAST_EXPECT(!parsed.object); + } + catch (std::runtime_error& e) + { + std::string what(e.what()); + unexpected( + what.find("First level children of `Template`") != 0); + } + } + } + + void + testEdgeCases() + { + testcase("General Invalid Cases"); + + { + Json::Value j; + j[sfLedgerEntry] = 1; // not a valid SField for STParsedJSON + } { std::string const goodJson(R"({"CloseResolution":19,"Method":250,)" @@ -328,9 +2227,26 @@ public: { // Instantiate a jtx::Env so debugLog writes are exercised. test::jtx::Env env(*this); - testParseJSONArrayWithInvalidChildrenObjects(); - testParseJSONArray(); - testParseJSONEdgeCases(); + testUInt8(); + testUInt16(); + testUInt32(); + testUInt64(); + testUInt128(); + testUInt160(); + testUInt192(); + testUInt256(); + testBlob(); + testVector256(); + testAccount(); + testCurrency(); + testAmount(); + testPathSet(); + testIssue(); + testXChainBridge(); + testNumber(); + testObject(); + testArray(); + testEdgeCases(); } }; diff --git a/src/xrpld/app/tx/detail/CreateOffer.cpp b/src/xrpld/app/tx/detail/CreateOffer.cpp index 3c4871c492..7d5aada642 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.cpp +++ b/src/xrpld/app/tx/detail/CreateOffer.cpp @@ -53,9 +53,12 @@ CreateOffer::checkExtraFeatures(PreflightContext const& ctx) std::uint32_t CreateOffer::getFlagsMask(PreflightContext const& ctx) { - if (ctx.rules.enabled(featurePermissionedDEX) && - ctx.tx.isFieldPresent(sfDomainID)) + // The tfOfferCreateMask is built assuming that PermissionedDEX is + // enabled + if (ctx.rules.enabled(featurePermissionedDEX)) return tfOfferCreateMask; + // If PermissionedDEX is not enabled, add tfHybrid to the mask, + // indicating it is not allowed. return tfOfferCreateMask | tfHybrid; } @@ -66,6 +69,10 @@ CreateOffer::preflight(PreflightContext const& ctx) auto& j = ctx.j; std::uint32_t const uTxFlags = tx.getFlags(); + + if (tx.isFlag(tfHybrid) && !tx.isFieldPresent(sfDomainID)) + return temINVALID_FLAG; + bool const bImmediateOrCancel(uTxFlags & tfImmediateOrCancel); bool const bFillOrKill(uTxFlags & tfFillOrKill); diff --git a/src/xrpld/app/tx/detail/DeleteAccount.cpp b/src/xrpld/app/tx/detail/DeleteAccount.cpp index 364a53b50b..565d938c83 100644 --- a/src/xrpld/app/tx/detail/DeleteAccount.cpp +++ b/src/xrpld/app/tx/detail/DeleteAccount.cpp @@ -44,8 +44,11 @@ DeleteAccount::checkExtraFeatures(PreflightContext const& ctx) if (!ctx.rules.enabled(featureDeletableAccounts)) return false; - return !ctx.tx.isFieldPresent(sfCredentialIDs) || - ctx.rules.enabled(featureCredentials); + if (ctx.tx.isFieldPresent(sfCredentialIDs) && + !ctx.rules.enabled(featureCredentials)) + return false; + + return true; } NotTEC diff --git a/src/xrpld/app/tx/detail/DepositPreauth.cpp b/src/xrpld/app/tx/detail/DepositPreauth.cpp index bbbec3cbe7..236b59a173 100644 --- a/src/xrpld/app/tx/detail/DepositPreauth.cpp +++ b/src/xrpld/app/tx/detail/DepositPreauth.cpp @@ -36,10 +36,12 @@ DepositPreauth::checkExtraFeatures(PreflightContext const& ctx) bool const authArrPresent = ctx.tx.isFieldPresent(sfAuthorizeCredentials); bool const unauthArrPresent = ctx.tx.isFieldPresent(sfUnauthorizeCredentials); - int const authCredPresent = - static_cast(authArrPresent) + static_cast(unauthArrPresent); + bool const authCredPresent = authArrPresent || unauthArrPresent; - return !authCredPresent || ctx.rules.enabled(featureCredentials); + if (authCredPresent && !ctx.rules.enabled(featureCredentials)) + return false; + + return true; } NotTEC diff --git a/src/xrpld/core/detail/semaphore.h b/src/xrpld/core/detail/semaphore.h index 3b64265bb1..fbeb79c66a 100644 --- a/src/xrpld/core/detail/semaphore.h +++ b/src/xrpld/core/detail/semaphore.h @@ -17,6 +17,34 @@ */ //============================================================================== +/** + * + * TODO: Remove ripple::basic_semaphore (and this file) and use + * std::counting_semaphore. + * + * Background: + * - PR: https://github.com/XRPLF/rippled/pull/5512/files + * - std::counting_semaphore had a bug fixed in both GCC and Clang: + * * GCC PR 104928: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=104928 + * * LLVM PR 79265: https://github.com/llvm/llvm-project/pull/79265 + * + * GCC: + * According to GCC Bugzilla PR104928 + * (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=104928#c15), the fix is + * scheduled for inclusion in GCC 16.0 (see comment #15, Target + * Milestone: 16.0). It is not included in GCC 14.x or earlier, and there is no + * indication that it will be backported to GCC 13.x or 14.x branches. + * + * Clang: + * The fix for is included in Clang 19.1.0+ + * + * Once the minimum compiler version is updated to > GCC 16.0 or Clang 19.1.0, + * we can remove this file. + * + * WARNING: Avoid using std::counting_semaphore until the minimum compiler + * version is updated. + */ + #ifndef RIPPLE_CORE_SEMAPHORE_H_INCLUDED #define RIPPLE_CORE_SEMAPHORE_H_INCLUDED