-
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.
Show resolved
Hide 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
Show resolved
Hide resolved
|
||
| */ | ||
| 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
Show resolved
Hide resolved
|
||
| */ | ||
| 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
Show resolved
Hide resolved
|
||
| */ | ||
| 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
Show resolved
Hide resolved
|
||
| */ | ||
| 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.
Show resolved
Hide 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
Show resolved
Hide resolved
|
||
| */ | ||
Amxx marked this conversation as resolved.
Show resolved
Hide 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
Show resolved
Hide resolved
|
||
| */ | ||
| 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` | ||
|
||
| */ | ||
| 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
Show resolved
Hide resolved
|
||
| 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
Show resolved
Hide resolved
|
||
| } | ||
| 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
Show resolved
Hide resolved
|
||
| */ | ||
| 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
Show resolved
Hide resolved
|
||
| */ | ||
| 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
Show resolved
Hide resolved
|
||
| */ | ||
| 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
Show resolved
Hide resolved
|
||
| 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
Show resolved
Hide resolved
|
||
| } | ||
| 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
Show resolved
Hide resolved
|
||
| */ | ||
| 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
Show resolved
Hide resolved
|
||
| */ | ||
| 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
Show resolved
Hide resolved
|
||
| */ | ||
| 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) { | ||
|
||
| assembly ("memory-safe") { | ||
Amxx marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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
Show resolved
Hide resolved
|
||
| // - 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.