-
Notifications
You must be signed in to change notification settings - Fork 12.4k
Add toUint, toInt and hexToUint to Strings #5166
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 16 commits
b2eedbe
efd2f30
bc42b25
07f4b44
40ba631
07ec518
95fb0db
f263819
f51fbe6
52a301b
027859e
a91a999
86abf5a
6dca3cb
a7a6e9e
ec9a659
568dc7b
0292c31
aea4a14
cf78a9f
26cec97
3a7f904
4d18729
c7a7c94
d6319e8
b3bf461
2ab63b7
231b93b
24f1490
43f0dc1
7b7c1fd
2abfa49
f433e6d
27c7c0d
75e1e4c
4f48757
1ec1e3f
53d72d7
c5790f8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| 'openzeppelin-solidity': minor | ||
| --- | ||
|
|
||
| `Strings`: Add `parseUint`, `parseInt`, `parseHex` and `parseAddress` to parse strings into numbers and addresses. Also provide variant of these function that parse substrings, and `tryXxx` variants that do not revert on invalid input. | ||
|
cairoeth marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,12 +4,15 @@ | |
| pragma solidity ^0.8.20; | ||
|
|
||
| import {Math} from "./math/Math.sol"; | ||
| import {SafeCast} from "./math/SafeCast.sol"; | ||
| import {SignedMath} from "./math/SignedMath.sol"; | ||
|
|
||
| /** | ||
| * @dev String operations. | ||
| */ | ||
| library Strings { | ||
| using SafeCast for *; | ||
|
|
||
| bytes16 private constant HEX_DIGITS = "0123456789abcdef"; | ||
| uint8 private constant ADDRESS_LENGTH = 20; | ||
|
|
||
|
|
@@ -18,6 +21,16 @@ library Strings { | |
| */ | ||
| error StringsInsufficientHexLength(uint256 value, uint256 length); | ||
|
|
||
| /** | ||
| * @dev The string being parsed contains characters that are not in scope of the given base. | ||
| */ | ||
| error StringsInvalidChar(); | ||
|
|
||
| /** | ||
| * @dev The string being parsed is not a properly formated address. | ||
|
Amxx marked this conversation as resolved.
Outdated
|
||
| */ | ||
| error StringsInvalidAddressFormat(); | ||
|
|
||
| /** | ||
| * @dev Converts a `uint256` to its ASCII `string` decimal representation. | ||
| */ | ||
|
|
@@ -115,4 +128,247 @@ library Strings { | |
| function equal(string memory a, string memory b) internal pure returns (bool) { | ||
| return bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b)); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Parse a decimal string and returns the value as a `uint256`. | ||
| * | ||
| * This function will revert if: | ||
| * - the string contains any character that is not in [0-9]. | ||
| * - the result does not fit in a `uint256`. | ||
|
Amxx marked this conversation as resolved.
Outdated
|
||
| */ | ||
| function parseUint(string memory input) internal pure returns (uint256) { | ||
| return parseUint(input, 0, bytes(input).length); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Variant of {parseUint} that parses a substring of `input` located between position `begin` (included) and | ||
| * `end` (excluded). | ||
| */ | ||
| function parseUint(string memory input, uint256 begin, uint256 end) internal pure returns (uint256) { | ||
| (bool success, uint256 value) = tryParseUint(input, begin, end); | ||
| if (!success) revert StringsInvalidChar(); | ||
| return value; | ||
| } | ||
|
|
||
| /** | ||
| * @dev Variant of {parseUint-string} that returns false if the parsing fails because of an invalid character. | ||
| * | ||
| * This function will still revert if the result does not fit in a `uint256` | ||
|
Amxx marked this conversation as resolved.
Outdated
|
||
| */ | ||
| function tryParseUint(string memory input) internal pure returns (bool success, uint256 value) { | ||
| return tryParseUint(input, 0, bytes(input).length); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Variant of {parseUint-string-uint256-uint256} that returns false if the parsing fails because of an invalid | ||
| * character. | ||
| * | ||
| * This function will still revert if the result does not fit in a `uint256` | ||
|
Amxx marked this conversation as resolved.
Outdated
|
||
| */ | ||
| function tryParseUint( | ||
| string memory input, | ||
| uint256 begin, | ||
| uint256 end | ||
| ) internal pure returns (bool success, uint256 value) { | ||
| bytes memory buffer = bytes(input); | ||
|
ernestognw marked this conversation as resolved.
|
||
|
|
||
| uint256 result = 0; | ||
| for (uint256 i = begin; i < end; ++i) { | ||
| uint8 chr = _tryParseChr(buffer[i]); | ||
| if (chr > 9) return (false, 0); | ||
| result *= 10; | ||
| result += chr; | ||
| } | ||
| return (true, result); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Parse a decimal string and returns the value as a `int256`. | ||
| * | ||
| * This function will revert if: | ||
| * - the string contains any character (outside the prefix) that is not in [0-9]. | ||
| * - the result does not fit in a `int256`. | ||
|
Amxx marked this conversation as resolved.
Outdated
|
||
| */ | ||
|
Amxx marked this conversation as resolved.
|
||
| function parseInt(string memory input) internal pure returns (int256) { | ||
| return parseInt(input, 0, bytes(input).length); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Variant of {parseInt-string} that parses a substring of `input` located between position `begin` (included) and | ||
| * `end` (excluded). | ||
| */ | ||
| function parseInt(string memory input, uint256 begin, uint256 end) internal pure returns (int256) { | ||
| (bool success, int256 value) = tryParseInt(input, begin, end); | ||
| if (!success) revert StringsInvalidChar(); | ||
| return value; | ||
| } | ||
|
|
||
| /** | ||
| * @dev Variant of {parseInt-string} that returns false if the parsing fails because of an invalid character. | ||
| * | ||
| * This function will still revert if the result does not fit in a `int256` | ||
|
Amxx marked this conversation as resolved.
Outdated
|
||
| */ | ||
| function tryParseInt(string memory input) internal pure returns (bool success, int256 value) { | ||
| return tryParseInt(input, 0, bytes(input).length); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Variant of {parseInt-string-uint256-uint256} that returns false if the parsing fails because of an invalid | ||
| * character. | ||
| * | ||
| * This function will still revert if the result does not fit in a `int256` | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this is a
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is consistent with all the other
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The revert is caused by result *= 10;
result += factor * int8(chr);overflowing. Note that this is done in a loop. Any branch that you add here will be multiplied by the number of characteres in the substring. If you can find a nice and cheap way to return false, please feel free to change that
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the revert could also happen for
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it affect all variants (3) of this function. (uint, int, hex)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks all for your comments. I think the contract is currently well-documented anyway, and updating to a version that doesn't revert (in the future) is the kind of breaking change that'd be fine in a minor version. I wouldn't block merging
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alternatively, we can check that the length of the string in Similarly for
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ernestognw zero prefix would create false positives. I'm not sure we can rule them out. |
||
| */ | ||
| function tryParseInt( | ||
| string memory input, | ||
| uint256 begin, | ||
| uint256 end | ||
| ) internal pure returns (bool success, int256 value) { | ||
| bytes memory buffer = bytes(input); | ||
|
|
||
| // check presence of a negative sign. | ||
| bool isNegative = bytes1(unsafeReadBytesOffset(buffer, begin)) == 0x2d; | ||
|
Amxx marked this conversation as resolved.
Outdated
|
||
| int8 factor = isNegative ? int8(-1) : int8(1); | ||
| uint256 offset = isNegative.toUint(); | ||
|
|
||
| int256 result = 0; | ||
| for (uint256 i = begin + offset; i < end; ++i) { | ||
| uint8 chr = _tryParseChr(buffer[i]); | ||
| if (chr > 9) return (false, 0); | ||
| result *= 10; | ||
| result += factor * int8(chr); | ||
|
Amxx marked this conversation as resolved.
Outdated
|
||
| } | ||
| return (true, result); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Parse a hexadecimal string (with or without "0x" prefix), and returns the value as a `uint256`. | ||
| * | ||
| * This function will revert if: | ||
| * - the string contains any character (outside the prefix) that is not in [0-9a-fA-F]. | ||
| * - the result does not fit in a `uint256`. | ||
|
Amxx marked this conversation as resolved.
Outdated
|
||
| */ | ||
| function parseHex(string memory input) internal pure returns (uint256) { | ||
| return parseHex(input, 0, bytes(input).length); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Variant of {parseHex} that parses a substring of `input` located between position `begin` (included) and | ||
| * `end` (excluded). | ||
| */ | ||
| function parseHex(string memory input, uint256 begin, uint256 end) internal pure returns (uint256) { | ||
| (bool success, uint256 value) = tryParseHex(input, begin, end); | ||
| if (!success) revert StringsInvalidChar(); | ||
| return value; | ||
| } | ||
|
|
||
| /** | ||
| * @dev Variant of {parseHex-string} that returns false if the parsing fails because of an invalid character. | ||
| * | ||
| * This function will still revert if the result does not fit in a `uint256` | ||
|
Amxx marked this conversation as resolved.
Outdated
|
||
| */ | ||
| function tryParseHex(string memory input) internal pure returns (bool success, uint256 value) { | ||
| return tryParseHex(input, 0, bytes(input).length); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Variant of {parseHex-string-uint256-uint256} that returns false if the parsing fails because of an | ||
| * invalid character. | ||
| * | ||
| * This function will still revert if the result does not fit in a `uint256` | ||
|
Amxx marked this conversation as resolved.
Outdated
|
||
| */ | ||
| function tryParseHex( | ||
| string memory input, | ||
| uint256 begin, | ||
| uint256 end | ||
| ) internal pure returns (bool success, uint256 value) { | ||
| bytes memory buffer = bytes(input); | ||
|
|
||
| // skip 0x prefix if present | ||
| bool hasPrefix = bytes2(unsafeReadBytesOffset(buffer, begin)) == 0x3078; | ||
|
Amxx marked this conversation as resolved.
Outdated
|
||
| uint256 offset = hasPrefix.toUint() * 2; | ||
|
|
||
| uint256 result = 0; | ||
| for (uint256 i = begin + offset; i < end; ++i) { | ||
| uint8 chr = _tryParseChr(buffer[i]); | ||
| if (chr > 15) return (false, 0); | ||
| result *= 16; | ||
| result += chr; | ||
|
arr00 marked this conversation as resolved.
Outdated
|
||
| } | ||
| return (true, result); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Parse a hexadecimal string (with or without "0x" prefix), and returns the value as an `address`. | ||
| * | ||
| * This function will revert if: | ||
| * - the string is not formated as `(0x)?[0-9a-fA-F]{40}` | ||
|
Amxx marked this conversation as resolved.
Outdated
|
||
| */ | ||
| function parseAddress(string memory input) internal pure returns (address) { | ||
| return parseAddress(input, 0, bytes(input).length); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Variant of {parseAddress} that parses a substring of `input` located between position `begin` (included) and | ||
| * `end` (excluded). | ||
| */ | ||
| function parseAddress(string memory input, uint256 begin, uint256 end) internal pure returns (address) { | ||
| (bool success, address value) = tryParseAddress(input, begin, end); | ||
| if (!success) revert StringsInvalidAddressFormat(); | ||
| return value; | ||
| } | ||
|
|
||
| /** | ||
| * @dev Variant of {parseAddress-string} that returns false if the parsing fails because input is not a properly | ||
| * formated address. | ||
|
Amxx marked this conversation as resolved.
Outdated
|
||
| */ | ||
| function tryParseAddress(string memory input) internal pure returns (bool success, address value) { | ||
| return tryParseAddress(input, 0, bytes(input).length); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Variant of {parseAddress-string-uint256-uint256} that returns false if the parsing fails because input is not a properly | ||
| * formated address. | ||
|
Amxx marked this conversation as resolved.
Outdated
|
||
| */ | ||
| function tryParseAddress( | ||
| string memory input, | ||
| uint256 begin, | ||
| uint256 end | ||
| ) internal pure returns (bool success, address value) { | ||
| // check that input is the correct length | ||
| bool hasPrefix = bytes2(unsafeReadBytesOffset(bytes(input), begin)) == 0x3078; | ||
| uint256 expectedLength = 40 + hasPrefix.toUint() * 2; | ||
|
|
||
| if (end - begin == expectedLength) { | ||
| // length garantees that this does not overflow, and value2 is at most type(uint160).max | ||
| (bool s, uint256 v) = tryParseHex(input, begin, end); | ||
| return (s, address(uint160(v))); | ||
| } else { | ||
| return (false, address(0)); | ||
| } | ||
| } | ||
|
|
||
| // TODO: documentation. | ||
| function unsafeReadBytesOffset(bytes memory buffer, uint256 offset) internal pure returns (bytes32 value) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a bytes operation so it shouldn't be in the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where should that go ? A
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that the same function exists in RSA.sol
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this function was present in other places but always private.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. private, and marked as memory safe, because the private calls are all enforcing the safety. But we are starting to use it in many places ... so having 3 private implementation of the same functions (with different names?) doesn't feel right.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Right... That was the issue. That using memory-unsafe functions disables some optimizations globally, so they were important to avoid.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So do we want to have:
?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extra option is:
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure that the last option is a safe recommendation. I'm too stupid regarding how compiler optimizations work so feel free to disregard, but I wonder if guaranteeing the safetiness with a private function will have a negative effect on the so-called "‘stack-to-memory’ mover" |
||
| assembly ("memory-safe") { | ||
|
Amxx marked this conversation as resolved.
Outdated
|
||
| value := mload(add(buffer, add(0x20, offset))) | ||
| } | ||
| } | ||
|
|
||
| function _tryParseChr(bytes1 chr) private pure returns (uint8) { | ||
| uint8 value = uint8(chr); | ||
|
|
||
| // Try to parse `chr`: | ||
| // - Case 1: [0-9] | ||
| // - Case 2: [a-f] | ||
| // - Case 2: [A-F] | ||
|
Amxx marked this conversation as resolved.
Outdated
|
||
| // - otherwise not supported | ||
| unchecked { | ||
| if (value > 47 && value < 58) value -= 48; | ||
| else if (value > 96 && value < 103) value -= 87; | ||
| else if (value > 64 && value < 71) value -= 55; | ||
| else return type(uint8).max; | ||
| } | ||
|
|
||
| return value; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| pragma solidity ^0.8.20; | ||
|
|
||
| import {Test} from "forge-std/Test.sol"; | ||
|
|
||
| import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; | ||
|
|
||
| contract StringsTest is Test { | ||
| using Strings for *; | ||
|
|
||
| function testParse(uint256 value) external { | ||
| assertEq(value, value.toString().parseUint()); | ||
| } | ||
|
|
||
| function testParseSigned(int256 value) external { | ||
| assertEq(value, value.toStringSigned().parseInt()); | ||
| } | ||
|
|
||
| function testParseHex(uint256 value) external { | ||
| assertEq(value, value.toHexString().parseHex()); | ||
| } | ||
|
|
||
| function testParseChecksumHex(address value) external { | ||
| assertEq(value, value.toChecksumHexString().parseAddress()); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.