From 67578b8f50f6006f5cfb205e688a1a7ca1b4f802 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 22 Sep 2025 11:17:13 -0400 Subject: [PATCH 1/4] Review feedback from @mvadari - Clean up some of the flag and extra feature checks for readability --- src/xrpld/app/tx/detail/CreateOffer.cpp | 10 ++++++++-- src/xrpld/app/tx/detail/DeleteAccount.cpp | 7 +++++-- src/xrpld/app/tx/detail/DepositPreauth.cpp | 8 +++++--- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/xrpld/app/tx/detail/CreateOffer.cpp b/src/xrpld/app/tx/detail/CreateOffer.cpp index 5bd2752222..86750eb51d 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.cpp +++ b/src/xrpld/app/tx/detail/CreateOffer.cpp @@ -56,9 +56,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; } @@ -70,6 +73,9 @@ CreateOffer::preflight(PreflightContext const& ctx) 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 From 6b8a5894476cf06b823b2b4075b7d77b54a6ad81 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 22 Sep 2025 14:00:31 -0400 Subject: [PATCH 2/4] test: Add STInteger and STParsedJSON tests (#5726) This change is to improve code coverage (and to simplify #5720 and #5725); there is otherwise no change in functionality. The change adds basic tests for `STInteger` and `STParsedJSON`, so it becomes easier to test smaller changes to the types, as well as removes `STParsedJSONArray`, since it is not used anywhere (including in Clio). --- include/xrpl/protocol/Permissions.h | 3 + include/xrpl/protocol/STParsedJSON.h | 28 - src/libxrpl/protocol/Permissions.cpp | 16 + src/libxrpl/protocol/STInteger.cpp | 32 +- src/libxrpl/protocol/STParsedJSON.cpp | 61 +- src/test/protocol/STInteger_test.cpp | 135 ++ src/test/protocol/STParsedJSON_test.cpp | 2018 ++++++++++++++++++++++- 7 files changed, 2162 insertions(+), 131 deletions(-) create mode 100644 src/test/protocol/STInteger_test.cpp diff --git a/include/xrpl/protocol/Permissions.h b/include/xrpl/protocol/Permissions.h index cf49ff7382..2eca441124 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 781799f128..6a4b0678e0 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(); } }; From 08b136528e078a691ece1033a072132234f52943 Mon Sep 17 00:00:00 2001 From: Bart Date: Mon, 22 Sep 2025 20:27:02 +0200 Subject: [PATCH 3/4] Revert "Update Conan dependencies: OpenSSL" (#5807) This change reverts #5617, because it will require extensive testing that will take up more time than we have before the next scheduled release. Reverting this change does not mean we are abandoning it. We aim to pick it back up once there's a sufficient time window to allow for testing on multiple distros running a mixture of OpenSSL 1.x and 3.x. --- cmake/RippledCompiler.cmake | 17 +++++++---------- conan.lock | 2 +- conanfile.py | 2 +- 3 files changed, 9 insertions(+), 12 deletions(-) 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', ] From 73ff54143d0a9b378cf3612b9ba9ac3751928551 Mon Sep 17 00:00:00 2001 From: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com> Date: Tue, 23 Sep 2025 12:26:26 +0100 Subject: [PATCH 4/4] docs: Add warning about using std::counting_semaphore (#5595) This adds a comment to avoid using `std::counting_semaphore` until the minimum compiler versions of GCC and Clang have been updated to no longer contain the bug that is present in older compilers. --- src/xrpld/core/detail/semaphore.h | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) 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