From 807462b191a76a7c939743238c1f7ddbd125c24e Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 26 Sep 2025 16:13:15 -0400 Subject: [PATCH] Add `STInt32` as a new `SType` (#5788) This change adds `STInt32` as a new `SType` under the `STInteger` umbrella, with `SType` value `12`. This is the first and only `STInteger` type that supports negative values. --- include/xrpl/protocol/SField.h | 7 +- include/xrpl/protocol/STInteger.h | 2 + include/xrpl/protocol/STObject.h | 4 + include/xrpl/protocol/detail/sfields.macro | 6 ++ src/libxrpl/protocol/STInteger.cpp | 29 ++++++ src/libxrpl/protocol/STObject.cpp | 12 +++ src/libxrpl/protocol/STParsedJSON.cpp | 97 ++++++++++++++----- src/libxrpl/protocol/STVar.cpp | 3 + src/libxrpl/protocol/Serializer.cpp | 6 ++ src/test/protocol/STAccount_test.cpp | 17 ++++ src/test/protocol/STInteger_test.cpp | 28 ++++++ src/test/protocol/STParsedJSON_test.cpp | 105 ++++++++++++++++++++- src/test/protocol/types_test.cpp | 52 ---------- 13 files changed, 287 insertions(+), 81 deletions(-) delete mode 100644 src/test/protocol/types_test.cpp diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index 2f85cf3b7c..b6ae98b48f 100644 --- a/include/xrpl/protocol/SField.h +++ b/include/xrpl/protocol/SField.h @@ -72,8 +72,10 @@ class STCurrency; STYPE(STI_VL, 7) \ STYPE(STI_ACCOUNT, 8) \ STYPE(STI_NUMBER, 9) \ + STYPE(STI_INT32, 10) \ + STYPE(STI_INT64, 11) \ \ - /* 10-13 are reserved */ \ + /* 12-13 are reserved */ \ STYPE(STI_OBJECT, 14) \ STYPE(STI_ARRAY, 15) \ \ @@ -356,6 +358,9 @@ using SF_UINT256 = TypedField>; using SF_UINT384 = TypedField>; using SF_UINT512 = TypedField>; +using SF_INT32 = TypedField>; +using SF_INT64 = TypedField>; + using SF_ACCOUNT = TypedField; using SF_AMOUNT = TypedField; using SF_ISSUE = TypedField; diff --git a/include/xrpl/protocol/STInteger.h b/include/xrpl/protocol/STInteger.h index b259638774..154ee7f203 100644 --- a/include/xrpl/protocol/STInteger.h +++ b/include/xrpl/protocol/STInteger.h @@ -81,6 +81,8 @@ using STUInt16 = STInteger; using STUInt32 = STInteger; using STUInt64 = STInteger; +using STInt32 = STInteger; + template inline STInteger::STInteger(Integer v) : value_(v) { diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index b3cb561390..1c22b08aba 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -231,6 +231,8 @@ public: getFieldH192(SField const& field) const; uint256 getFieldH256(SField const& field) const; + std::int32_t + getFieldI32(SField const& field) const; AccountID getAccountID(SField const& field) const; @@ -365,6 +367,8 @@ public: void setFieldH256(SField const& field, uint256 const&); void + setFieldI32(SField const& field, std::int32_t); + void setFieldVL(SField const& field, Blob const&); void setFieldVL(SField const& field, Slice const&); diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 10fe015dac..f932ae2328 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -208,6 +208,12 @@ TYPED_SFIELD(sfAssetsMaximum, NUMBER, 3) TYPED_SFIELD(sfAssetsTotal, NUMBER, 4) TYPED_SFIELD(sfLossUnrealized, NUMBER, 5) +// int32 +// NOTE: Do not use `sfDummyInt32`. It's so far the only use of INT32 +// in this file and has been defined here for test only. +// TODO: Replace `sfDummyInt32` with actually useful field. +TYPED_SFIELD(sfDummyInt32, INT32, 1) // for tests only + // currency amount (common) TYPED_SFIELD(sfAmount, AMOUNT, 1) TYPED_SFIELD(sfBalance, AMOUNT, 2) diff --git a/src/libxrpl/protocol/STInteger.cpp b/src/libxrpl/protocol/STInteger.cpp index 5d6c1802cc..355fa4c113 100644 --- a/src/libxrpl/protocol/STInteger.cpp +++ b/src/libxrpl/protocol/STInteger.cpp @@ -249,4 +249,33 @@ STUInt64::getJson(JsonOptions) const return convertToString(value_, 16); // Convert to base 16 } +//------------------------------------------------------------------------------ + +template <> +STInteger::STInteger(SerialIter& sit, SField const& name) + : STInteger(name, sit.get32()) +{ +} + +template <> +SerializedTypeID +STInt32::getSType() const +{ + return STI_INT32; +} + +template <> +std::string +STInt32::getText() const +{ + return std::to_string(value_); +} + +template <> +Json::Value +STInt32::getJson(JsonOptions) const +{ + return value_; +} + } // namespace ripple diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp index 9c23898a74..77e5fd1ad9 100644 --- a/src/libxrpl/protocol/STObject.cpp +++ b/src/libxrpl/protocol/STObject.cpp @@ -647,6 +647,12 @@ STObject::getFieldH256(SField const& field) const return getFieldByValue(field); } +std::int32_t +STObject::getFieldI32(SField const& field) const +{ + return getFieldByValue(field); +} + AccountID STObject::getAccountID(SField const& field) const { @@ -761,6 +767,12 @@ STObject::setFieldH256(SField const& field, uint256 const& v) setFieldUsingSetValue(field, v); } +void +STObject::setFieldI32(SField const& field, std::int32_t v) +{ + setFieldUsingSetValue(field, v); +} + void STObject::setFieldV256(SField const& field, STVector256 const& v) { diff --git a/src/libxrpl/protocol/STParsedJSON.cpp b/src/libxrpl/protocol/STParsedJSON.cpp index 9fbe5e7f91..f99fec6b87 100644 --- a/src/libxrpl/protocol/STParsedJSON.cpp +++ b/src/libxrpl/protocol/STParsedJSON.cpp @@ -563,30 +563,6 @@ parseLeaf( break; } - case STI_UINT192: { - if (!value.isString()) - { - error = bad_type(json_name, fieldName); - return ret; - } - - uint192 num; - - if (auto const s = value.asString(); !num.parseHex(s)) - { - if (!s.empty()) - { - error = invalid_data(json_name, fieldName); - return ret; - } - - num.zero(); - } - - ret = detail::make_stvar(field, num); - break; - } - case STI_UINT160: { if (!value.isString()) { @@ -611,6 +587,30 @@ parseLeaf( break; } + case STI_UINT192: { + if (!value.isString()) + { + error = bad_type(json_name, fieldName); + return ret; + } + + uint192 num; + + if (auto const s = value.asString(); !num.parseHex(s)) + { + if (!s.empty()) + { + error = invalid_data(json_name, fieldName); + return ret; + } + + num.zero(); + } + + ret = detail::make_stvar(field, num); + break; + } + case STI_UINT256: { if (!value.isString()) { @@ -635,6 +635,52 @@ parseLeaf( break; } + case STI_INT32: + try + { + if (value.isString()) + { + ret = detail::make_stvar( + field, + beast::lexicalCastThrow( + value.asString())); + } + else if (value.isInt()) + { + // future-proofing - a static assert failure if the JSON + // library ever supports larger ints + // In such case, we will need additional bounds checks here + static_assert( + std::is_same_v); + ret = detail::make_stvar(field, value.asInt()); + } + else if (value.isUInt()) + { + auto const uintValue = value.asUInt(); + if (uintValue > + static_cast( + std::numeric_limits::max())) + { + error = out_of_range(json_name, fieldName); + return ret; + } + ret = detail::make_stvar( + field, static_cast(uintValue)); + } + else + { + error = bad_type(json_name, fieldName); + return ret; + } + } + catch (std::exception const&) + { + error = invalid_data(json_name, fieldName); + return ret; + } + + break; + case STI_VL: if (!value.isString()) { @@ -1120,8 +1166,7 @@ parseArray( Json::Value const objectFields(json[i][objectName]); std::stringstream ss; - ss << json_name << "." - << "[" << i << "]." << objectName; + ss << json_name << "." << "[" << i << "]." << objectName; auto ret = parseObject( ss.str(), objectFields, nameField, depth + 1, error); diff --git a/src/libxrpl/protocol/STVar.cpp b/src/libxrpl/protocol/STVar.cpp index 24954c4add..c46effb47e 100644 --- a/src/libxrpl/protocol/STVar.cpp +++ b/src/libxrpl/protocol/STVar.cpp @@ -208,6 +208,9 @@ STVar::constructST(SerializedTypeID id, int depth, Args&&... args) case STI_UINT256: construct(std::forward(args)...); return; + case STI_INT32: + construct(std::forward(args)...); + return; case STI_VECTOR256: construct(std::forward(args)...); return; diff --git a/src/libxrpl/protocol/Serializer.cpp b/src/libxrpl/protocol/Serializer.cpp index b8a68d28b8..098e68d2b2 100644 --- a/src/libxrpl/protocol/Serializer.cpp +++ b/src/libxrpl/protocol/Serializer.cpp @@ -83,6 +83,12 @@ Serializer::addInteger(std::uint64_t i) { return add64(i); } +template <> +int +Serializer::addInteger(std::int32_t i) +{ + return add32(i); +} int Serializer::addRaw(Blob const& vector) diff --git a/src/test/protocol/STAccount_test.cpp b/src/test/protocol/STAccount_test.cpp index 9476a47c5e..cc318b4458 100644 --- a/src/test/protocol/STAccount_test.cpp +++ b/src/test/protocol/STAccount_test.cpp @@ -122,10 +122,27 @@ struct STAccount_test : public beast::unit_test::suite } } + void + testAccountID() + { + auto const s = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + if (auto const parsed = parseBase58(s); BEAST_EXPECT(parsed)) + { + BEAST_EXPECT(toBase58(*parsed) == s); + } + + { + auto const s = + "âabcd1rNxp4h8apvRis6mJf9Sh8C6iRxfrDWNâabcdAVâ\xc2\x80\xc2\x8f"; + BEAST_EXPECT(!parseBase58(s)); + } + } + void run() override { testSTAccount(); + testAccountID(); } }; diff --git a/src/test/protocol/STInteger_test.cpp b/src/test/protocol/STInteger_test.cpp index f4572e49bd..6c4cdd6fcf 100644 --- a/src/test/protocol/STInteger_test.cpp +++ b/src/test/protocol/STInteger_test.cpp @@ -30,6 +30,7 @@ struct STInteger_test : public beast::unit_test::suite void testUInt8() { + testcase("UInt8"); STUInt8 u8(255); BEAST_EXPECT(u8.value() == 255); BEAST_EXPECT(u8.getText() == "255"); @@ -56,6 +57,7 @@ struct STInteger_test : public beast::unit_test::suite void testUInt16() { + testcase("UInt16"); STUInt16 u16(65535); BEAST_EXPECT(u16.value() == 65535); BEAST_EXPECT(u16.getText() == "65535"); @@ -80,6 +82,7 @@ struct STInteger_test : public beast::unit_test::suite void testUInt32() { + testcase("UInt32"); STUInt32 u32(4'294'967'295u); BEAST_EXPECT(u32.value() == 4'294'967'295u); BEAST_EXPECT(u32.getText() == "4294967295"); @@ -102,6 +105,7 @@ struct STInteger_test : public beast::unit_test::suite void testUInt64() { + testcase("UInt64"); STUInt64 u64(0xFFFFFFFFFFFFFFFFull); BEAST_EXPECT(u64.value() == 0xFFFFFFFFFFFFFFFFull); BEAST_EXPECT(u64.getText() == "18446744073709551615"); @@ -120,6 +124,29 @@ struct STInteger_test : public beast::unit_test::suite u64_2.getJson(JsonOptions::none) == "18446744073709551615"); } + void + testInt32() + { + testcase("Int32"); + { + int const minInt32 = -2147483648; + STInt32 i32(minInt32); + BEAST_EXPECT(i32.value() == minInt32); + BEAST_EXPECT(i32.getText() == "-2147483648"); + BEAST_EXPECT(i32.getSType() == STI_INT32); + BEAST_EXPECT(i32.getJson(JsonOptions::none) == minInt32); + } + + { + int const maxInt32 = 2147483647; + STInt32 i32(maxInt32); + BEAST_EXPECT(i32.value() == maxInt32); + BEAST_EXPECT(i32.getText() == "2147483647"); + BEAST_EXPECT(i32.getSType() == STI_INT32); + BEAST_EXPECT(i32.getJson(JsonOptions::none) == maxInt32); + } + } + void run() override { @@ -127,6 +154,7 @@ struct STInteger_test : public beast::unit_test::suite testUInt16(); testUInt32(); testUInt64(); + testInt32(); } }; diff --git a/src/test/protocol/STParsedJSON_test.cpp b/src/test/protocol/STParsedJSON_test.cpp index 9ecb4c0365..1e1e1fb9f4 100644 --- a/src/test/protocol/STParsedJSON_test.cpp +++ b/src/test/protocol/STParsedJSON_test.cpp @@ -736,6 +736,107 @@ class STParsedJSON_test : public beast::unit_test::suite } } + void + testInt32() + { + testcase("Int32"); + { + Json::Value j; + int const minInt32 = -2147483648; + j[sfDummyInt32] = minInt32; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) + BEAST_EXPECT(obj.object->getFieldI32(sfDummyInt32) == minInt32); + } + + // max value + { + Json::Value j; + int const maxInt32 = 2147483647; + j[sfDummyInt32] = maxInt32; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) + BEAST_EXPECT(obj.object->getFieldI32(sfDummyInt32) == maxInt32); + } + + // max uint value + { + Json::Value j; + unsigned int const maxUInt32 = 2147483647u; + j[sfDummyInt32] = maxUInt32; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) + BEAST_EXPECT( + obj.object->getFieldI32(sfDummyInt32) == + static_cast(maxUInt32)); + } + + // Test with string value + { + Json::Value j; + j[sfDummyInt32] = "2147483647"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) + BEAST_EXPECT( + obj.object->getFieldI32(sfDummyInt32) == 2147483647u); + } + + // Test with string negative value + { + Json::Value j; + int value = -2147483648; + j[sfDummyInt32] = std::to_string(value); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(obj.object.has_value()); + if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) + BEAST_EXPECT(obj.object->getFieldI32(sfDummyInt32) == value); + } + + // Test out of range value for int32 (negative) + { + Json::Value j; + j[sfDummyInt32] = "-2147483649"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test out of range value for int32 (positive) + { + Json::Value j; + j[sfDummyInt32] = 2147483648u; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test string value out of range + { + Json::Value j; + j[sfDummyInt32] = "2147483648"; + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test bad_type (arrayValue) + { + Json::Value j; + j[sfDummyInt32] = Json::Value(Json::arrayValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + + // Test bad_type (objectValue) + { + Json::Value j; + j[sfDummyInt32] = Json::Value(Json::objectValue); + STParsedJSONObject obj("Test", j); + BEAST_EXPECT(!obj.object.has_value()); + } + } + void testBlob() { @@ -1338,8 +1439,7 @@ class STParsedJSON_test : public beast::unit_test::suite issueJson["issuer"] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; j[sfAsset] = issueJson; STParsedJSONObject obj("Test", j); - if (BEAST_EXPECTS( - obj.object.has_value(), obj.error.toStyledString())) + if (BEAST_EXPECT(obj.object.has_value())) { BEAST_EXPECT(obj.object->isFieldPresent(sfAsset)); auto const& issueField = (*obj.object)[sfAsset]; @@ -2235,6 +2335,7 @@ class STParsedJSON_test : public beast::unit_test::suite testUInt160(); testUInt192(); testUInt256(); + testInt32(); testBlob(); testVector256(); testAccount(); diff --git a/src/test/protocol/types_test.cpp b/src/test/protocol/types_test.cpp deleted file mode 100644 index 8257d9c649..0000000000 --- a/src/test/protocol/types_test.cpp +++ /dev/null @@ -1,52 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012, 2013 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 - -namespace ripple { - -struct types_test : public beast::unit_test::suite -{ - void - testAccountID() - { - auto const s = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; - if (auto const parsed = parseBase58(s); BEAST_EXPECT(parsed)) - { - BEAST_EXPECT(toBase58(*parsed) == s); - } - - { - auto const s = - "âabcd1rNxp4h8apvRis6mJf9Sh8C6iRxfrDWNâabcdAVâ\xc2\x80\xc2\x8f"; - BEAST_EXPECT(!parseBase58(s)); - } - } - - void - run() override - { - testAccountID(); - } -}; - -BEAST_DEFINE_TESTSUITE(types, protocol, ripple); - -} // namespace ripple