Skip to content
Merged
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
109 changes: 109 additions & 0 deletions test/LibConvert.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,113 @@ contract LibConvertTest is Test {
LibConvert.unsafeTo16BitBytes(us)
);
}

/// The conversion hands back the buffer it was given rather than a copy of
/// it, and rewrites the shared length prefix from a count of words to a
/// count of bytes. Both halves of that are what make the source unsafe to
/// use afterwards, so both are pinned here.
function testUnsafeToBytesAliasesTheSourceAndRewritesItsLengthPrefix(uint256[] memory us) public pure {
uint256 sourcePointer;
assembly ("memory-safe") {
sourcePointer := us
}
uint256 wordLength = us.length;

bytes memory bs = LibConvert.unsafeToBytes(us);

uint256 bytesPointer;
assembly ("memory-safe") {
bytesPointer := bs
}

assertEq(bytesPointer, sourcePointer);
assertEq(bs.length, wordLength * 32);

uint256 prefix;
assembly ("memory-safe") {
prefix := mload(sourcePointer)
}
assertEq(prefix, wordLength * 32);
}

/// The 16 bit packing allocates its own result, so unlike `unsafeToBytes` it
/// reads the source and leaves it intact.
function testUnsafeTo16BitBytesLeavesTheSourceIntact(uint256[] memory us) public pure {
uint256[] memory expected = new uint256[](us.length);
for (uint256 i = 0; i < us.length; i++) {
expected[i] = us[i];
}

bytes memory bs = LibConvert.unsafeTo16BitBytes(us);
assertEq(bs.length, expected.length * 2);

assertEq(us.length, expected.length);
for (uint256 i = 0; i < expected.length; i++) {
assertEq(us[i], expected[i]);
}
}

/// The packing loop writes whole words at two byte offsets, so it has to
/// stop before it runs off the end of what it allocated. Comparing the
/// result value alone cannot see a write past that end, because such a write
/// lands beyond the length the comparison reads. Sentinels are placed
/// immediately above the allocation instead.
function testUnsafeTo16BitBytesWritesNothingPastItsAllocation(uint256[] memory us) public pure {
checkWritesNothingPastAllocation(us);
}

/// Lengths whose packed data exactly fills whole words, where a single byte
/// written past the end of the data lands outside the allocation rather than
/// in its padding.
function testUnsafeTo16BitBytesWritesNothingPastItsAllocationAtWordBoundaries() public pure {
uint256[] memory lengths = new uint256[](4);
lengths[0] = 0;
lengths[1] = 16;
lengths[2] = 32;
lengths[3] = 48;

for (uint256 j = 0; j < lengths.length; j++) {
uint256[] memory us = new uint256[](lengths[j]);
for (uint256 i = 0; i < us.length; i++) {
us[i] = type(uint256).max;
}
checkWritesNothingPastAllocation(us);
}
}

function checkWritesNothingPastAllocation(uint256[] memory us) internal pure {
uint256 sentinel = uint256(keccak256("rain.lib.typecast.canary"));
uint256 expectedLength = us.length * 2;

// Sentinels sit directly above the allocation the conversion is about to
// make, which is the free pointer plus a length word plus the data
// rounded up to whole words.
uint256 canary;
assembly {
canary := add(mload(0x40), add(0x20, and(add(mul(mload(us), 2), 0x1f), not(0x1f))))
mstore(canary, sentinel)
mstore(add(canary, 0x20), sentinel)
mstore(add(canary, 0x40), sentinel)
mstore(add(canary, 0x60), sentinel)
}

bytes memory bs = LibConvert.unsafeTo16BitBytes(us);

uint256 c0;
uint256 c1;
uint256 c2;
uint256 c3;
assembly {
c0 := mload(canary)
c1 := mload(add(canary, 0x20))
c2 := mload(add(canary, 0x40))
c3 := mload(add(canary, 0x60))
}

assertEq(bs.length, expectedLength);
assertEq(c0, sentinel);
assertEq(c1, sentinel);
assertEq(c2, sentinel);
assertEq(c3, sentinel);
}
}
Loading