Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### Breaking changes

- `Strings`: The `escapeJSON` function now escapes all control characters in the range U+0000 to U+001F per RFC-4627. Previously only backspace, tab, newline, form feed, carriage return, double quote, and backslash were escaped. Input strings containing any other control character (e.g. null `0x00`) or raw bytes in U+0001–U+001F will now produce different, longer output (e.g. `\u0000` for null).
- `ERC1155`: Performing batch transfers with exactly one id/value in the batch no-longer calls `IERC1155Receiver.onERC1155Received`. `IERC1155Receiver.onERC1155BatchReceived` is called instead (with arrays of length one). ([#6170](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/6170))
- `ERC1967Proxy` and `TransparentUpgradeableProxy`: Mandate initialization during construction. Deployment now reverts with `ERC1967ProxyUninitialized` if an initialize call is not provided. Developers that rely on the previous behavior and want to disable this check can do so by overriding the internal `_unsafeAllowUninitialized` function to return true. ([#5906](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5906))
- `ERC721` and `ERC1155`: Prevent setting an operator for `address(0)`. In the case of `ERC721` this type of operator allowance could lead to obfuscated mint permission. ([#6171](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/6171))
Expand Down
34 changes: 21 additions & 13 deletions contracts/utils/Strings.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,6 @@ library Strings {

bytes16 private constant HEX_DIGITS = "0123456789abcdef";
uint8 private constant ADDRESS_LENGTH = 20;
uint256 private constant SPECIAL_CHARS_LOOKUP =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not keep that lookup table, and extend it to cover all control chars?

uint256 private constant SPECIAL_CHARS_LOOKUP =
        (0xffffffff) | // first 32 bytes corresponding to the control characters
        (1 << 0x22) | // double quote
        (1 << 0x5c); // backslash

(1 << 0x08) | // backspace
(1 << 0x09) | // tab
(1 << 0x0a) | // newline
(1 << 0x0c) | // form feed
(1 << 0x0d) | // carriage return
(1 << 0x22) | // double quote
(1 << 0x5c); // backslash

/**
* @dev The `value` string doesn't fit in the specified `length`.
Expand Down Expand Up @@ -457,18 +449,22 @@ library Strings {
*
* WARNING: This function should only be used in double quoted JSON strings. Single quotes are not escaped.
*
* NOTE: This function escapes all unicode characters, and not just the ones in ranges defined in section 2.5 of
* RFC-4627 (U+0000 to U+001F, U+0022 and U+005C). ECMAScript's `JSON.parse` does recover escaped unicode
* characters that are not in this range, but other tooling may provide different results.
* NOTE: This function escapes backslashes (including those in \uXXXX sequences) and the characters in ranges
* defined in section 2.5 of RFC-4627 (U+0000 to U+001F, U+0022 and U+005C). All control characters in U+0000
* to U+001F are escaped (\b, \t, \n, \f, \r use short form; others use \u00XX). ECMAScript's `JSON.parse` does
* recover escaped unicode characters that are not in this range, but other tooling may provide different results.
*/
function escapeJSON(string memory input) internal pure returns (string memory) {
bytes memory buffer = bytes(input);
bytes memory output = new bytes(2 * buffer.length); // worst case scenario
bytes memory output = new bytes(2 * buffer.length); // worst case scenario excluding control characters
uint256 outputLength = 0;

for (uint256 i = 0; i < buffer.length; ++i) {
bytes1 char = bytes1(_unsafeReadBytesOffset(buffer, i));
if (((SPECIAL_CHARS_LOOKUP & (1 << uint8(char))) != 0)) {
uint8 c = uint8(char);
bool isControl = c < 0x20;
bool isQuoteOrBackslash = (char == 0x22) || (char == 0x5c);
if (isControl || isQuoteOrBackslash) {
output[outputLength++] = "\\";
if (char == 0x08) output[outputLength++] = "b";
else if (char == 0x09) output[outputLength++] = "t";
Expand All @@ -479,6 +475,18 @@ library Strings {
else if (char == 0x22) {
// solhint-disable-next-line quotes
output[outputLength++] = '"';
} else {
// Worst case scenario is now 6 bytes per control character.
// Increase length by 4 bytes
assembly ("memory-safe") {
mstore(output, add(mload(output), 4))
}
// U+0000 to U+001F without short form: output \u00XX
output[outputLength++] = "u";
output[outputLength++] = "0";
output[outputLength++] = "0";
output[outputLength++] = bytes1(HEX_DIGITS[c >> 4]);
output[outputLength++] = bytes1(HEX_DIGITS[c & 0x0f]);
}
} else {
output[outputLength++] = char;
Expand Down
8 changes: 7 additions & 1 deletion test/utils/Strings.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,13 @@ describe('Strings', function () {
});

describe('Escape JSON string', function () {
for (const input of ['', 'a', '{"a":"b/c"}', 'a\tb\nc\\d"e\rf/g\fh\bi'])
for (const input of [
'',
'a',
'{"a":"b/c"}',
'a\tb\nc\\d"e\rf/g\fh\bi',
'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f',
])
it(`escape ${JSON.stringify(input)}`, async function () {
await expect(this.mock.$escapeJSON(input)).to.eventually.equal(JSON.stringify(input).slice(1, -1));
});
Expand Down
Loading