diff --git a/test/lib/LibCodeGen.addressConstantString.t.sol b/test/lib/LibCodeGen.addressConstantString.t.sol index 2d61c06..18f4967 100644 --- a/test/lib/LibCodeGen.addressConstantString.t.sol +++ b/test/lib/LibCodeGen.addressConstantString.t.sol @@ -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` diff --git a/test/lib/LibCodeGen.bytes32ConstantString.t.sol b/test/lib/LibCodeGen.bytes32ConstantString.t.sol index dde10c6..0c86e15 100644 --- a/test/lib/LibCodeGen.bytes32ConstantString.t.sol +++ b/test/lib/LibCodeGen.bytes32ConstantString.t.sol @@ -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` diff --git a/test/lib/LibCodeGen.uint8ConstantString.t.sol b/test/lib/LibCodeGen.uint8ConstantString.t.sol index 98722cb..9496a41 100644 --- a/test/lib/LibCodeGen.uint8ConstantString.t.sol +++ b/test/lib/LibCodeGen.uint8ConstantString.t.sol @@ -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 diff --git a/test/lib/LibCodeGenSlow.sol b/test/lib/LibCodeGenSlow.sol index bd11101..f21d109 100644 --- a/test/lib/LibCodeGenSlow.sol +++ b/test/lib/LibCodeGenSlow.sol @@ -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. @@ -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) {