Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
18 changes: 15 additions & 3 deletions test/lib/LibCodeGen.addressConstantString.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,26 @@ contract LibCodeGenAddressConstantStringTest is Test {
);
}

/// The emitted literal is checksummed, so it round-trips back to the same
/// address rather than silently relying on an all-lowercase form.
/// The literal the declaration carries parses back to the address it was
/// generated from. The literal is sliced out of the emitted text, so the
/// round trip is a property of what the library wrote.
function testAddressConstantStringRoundTrips(address data) external view {
string memory emitted = LibCodeGen.addressConstantString(vm, "/// @dev Fuzz.", "FUZZ", data);
assertEq(
emitted, string.concat("\n/// @dev Fuzz.\naddress constant FUZZ = address(", vm.toString(data), ");\n")
);
assertEq(vm.parseAddress(vm.toString(data)), data);
assertEq(vm.parseAddress(LibCodeGenSlow.betweenSlow(emitted, "address(", ")")), data);
}

/// The emitted literal is EIP-55 checksummed. `solc` rejects a forty digit
/// hex literal whose case does not carry the checksum, so a generated file
/// holding an all-lowercase address does not compile. `vm.parseAddress`
/// accepts either form, so the round trip cannot tell them apart and this
/// is stated against a reference that derives the checksum from the
/// address's own bits.
function testAddressConstantStringChecksummed(address data) external view {
string memory emitted = LibCodeGen.addressConstantString(vm, "/// @dev Fuzz.", "FUZZ", data);
assertEq(LibCodeGenSlow.betweenSlow(emitted, "address(", ")"), LibCodeGenSlow.checksumAddressSlow(data));
}

/// A declaration of exactly the maximum length stays on one line. `forge fmt`
Expand Down
12 changes: 7 additions & 5 deletions test/lib/LibCodeGen.bytes32ConstantString.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,16 @@ contract LibCodeGenBytes32ConstantStringTest is Test {
);
}

/// The emitted value round-trips: the literal parses back to the same bytes32
/// rather than being truncated or reformatted.
/// The literal the declaration carries parses back to the value it was
/// generated from, rather than being truncated or reformatted. The literal
/// is sliced out of the emitted text, so the round trip is a property of
/// what the library wrote.
function testBytes32ConstantStringRoundTrips(bytes32 data) external view {
string memory emitted = LibCodeGen.bytes32ConstantString(vm, "/// @dev Fuzz.", "FUZZ", data);
assertEq(
LibCodeGen.bytes32ConstantString(vm, "/// @dev Fuzz.", "FUZZ", data),
string.concat("\n/// @dev Fuzz.\nbytes32 constant FUZZ = bytes32(", vm.toString(data), ");\n")
emitted, string.concat("\n/// @dev Fuzz.\nbytes32 constant FUZZ = bytes32(", vm.toString(data), ");\n")
);
assertEq(vm.parseBytes32(vm.toString(data)), data);
assertEq(vm.parseBytes32(LibCodeGenSlow.betweenSlow(emitted, "bytes32(", ")")), data);
}

/// A declaration of exactly the maximum length stays on one line. `forge fmt`
Expand Down
14 changes: 7 additions & 7 deletions test/lib/LibCodeGen.uint8ConstantString.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,14 @@ contract LibCodeGenUint8ConstantStringTest is Test {
);
}

/// The emitted literal parses back to the value it was generated from, so
/// the constant is not silently truncated or reformatted.
/// The literal the declaration carries parses back to the value it was
/// generated from, so the constant is not silently truncated or
/// reformatted. The literal is sliced out of the emitted text, so the round
/// trip is a property of what the library wrote.
function testUint8ConstantStringRoundTrips(uint8 data) external pure {
assertEq(
LibCodeGen.uint8ConstantString(vm, "/// @dev Fuzz.", "FUZZ", data),
string.concat("\n/// @dev Fuzz.\nuint8 constant FUZZ = ", vm.toString(uint256(data)), ";\n")
);
assertEq(vm.parseUint(vm.toString(uint256(data))), uint256(data));
string memory emitted = LibCodeGen.uint8ConstantString(vm, "/// @dev Fuzz.", "FUZZ", data);
assertEq(emitted, string.concat("\n/// @dev Fuzz.\nuint8 constant FUZZ = ", vm.toString(uint256(data)), ";\n"));
assertEq(vm.parseUint(LibCodeGenSlow.betweenSlow(emitted, "FUZZ = ", ";")), uint256(data));
}

/// An empty comment emits no comment line rather than an empty one. Two
Expand Down
85 changes: 85 additions & 0 deletions test/lib/LibCodeGenSlow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ string constant SLOW_HEAD_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop
/// alphabet and the decimal digits.
string constant SLOW_TAIL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$0123456789";

/// @dev The lowercase hex digits, indexed by the nibble each one spells.
string constant SLOW_HEX_ALPHABET = "0123456789abcdef";

/// Thrown when a slice is asked for between delimiters that the text does not
/// carry in that order.
/// @param text The text that was searched.
/// @param open The delimiter the slice starts after.
/// @param close The delimiter the slice ends before.
error NoSlice(string text, string open, string close);

/// @title LibCodeGenSlow
/// @notice A deliberately naive reference for the constant declarations
/// `LibCodeGen` emits.
Expand Down Expand Up @@ -115,6 +125,81 @@ library LibCodeGenSlow {
);
}

/// The EIP-55 checksummed hex string for `data`: `0x`, then the forty
/// lowercase hex digits of the address, each letter uppercased when the
/// nibble at the same position of the keccak256 hash of those forty digits
/// is 8 or more. The digits come from the address's own bits rather than
/// from `vm.toString`, so this disagrees with the cheatcode instead of
/// restating it.
function checksumAddressSlow(address data) internal pure returns (string memory) {
bytes memory alphabet = bytes(SLOW_HEX_ALPHABET);
bytes memory lower = new bytes(40);
for (uint256 i = 0; i < 40; i++) {
lower[39 - i] = alphabet[(uint256(uint160(data)) >> (4 * i)) & 0x0F];
}

bytes32 hash = keccak256(lower);
bytes memory checksummed = new bytes(42);
checksummed[0] = "0";
checksummed[1] = "x";
for (uint256 i = 0; i < 40; i++) {
bytes1 digit = lower[i];
uint8 nibble = i % 2 == 0 ? uint8(hash[i / 2]) >> 4 : uint8(hash[i / 2]) & 0x0F;
checksummed[i + 2] = digit > "9" && nibble > 7 ? bytes1(uint8(digit) - 32) : digit;
}
return string(checksummed);
}

/// The index of the first `needle` in `haystack` at or after `from`, or
/// `haystack.length` when there is none.
function indexOfSlow(bytes memory haystack, bytes memory needle, uint256 from) internal pure returns (uint256) {
for (uint256 i = from; i + needle.length <= haystack.length; i++) {
bool matched = true;
for (uint256 j = 0; j < needle.length; j++) {
if (haystack[i + j] != needle[j]) {
matched = false;
break;
}
}
if (matched) {
return i;
}
}
return haystack.length;
}

/// The text between the first `open` in `text` and the first `close` after
/// it, so a test can state a property of the literal a declaration carries
/// rather than of a value the test formatted for itself. Reverts when
/// either delimiter is missing, so text that does not carry the literal at
/// all fails rather than yielding an empty slice.
function betweenSlow(string memory text, string memory open, string memory close)
internal
pure
returns (string memory)
{
bytes memory textBytes = bytes(text);
bytes memory openBytes = bytes(open);
bytes memory closeBytes = bytes(close);

uint256 openIndex = indexOfSlow(textBytes, openBytes, 0);
if (openIndex == textBytes.length) {
revert NoSlice(text, open, close);
}

uint256 start = openIndex + openBytes.length;
uint256 end = indexOfSlow(textBytes, closeBytes, start);
if (end == textBytes.length) {
revert NoSlice(text, open, close);
}

bytes memory slice = new bytes(end - start);
for (uint256 i = 0; i < slice.length; i++) {
slice[i] = textBytes[start + i];
}
return string(slice);
}

/// The length of the longest line in `text`, so a test can assert what
/// `forge fmt` would measure rather than what the library predicted.
function longestLineSlow(string memory text) internal pure returns (uint256) {
Expand Down
Loading