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
22 changes: 16 additions & 6 deletions src/lib/LibFs.sol
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,18 @@ library LibFs {
/// it found it: nothing is created, unlinked or written unless there is
/// content to write.
///
/// Anything already at the path is unlinked before the write, so a symlink
/// The path is unlinked until it holds nothing, then written, so a symlink
/// there is replaced by a regular file rather than written through to its
/// target, including a symlink whose target does not exist, and the path
/// does not exist between the unlink and the write.
/// Any manual changes to the generated file, or any other existing file at
/// that path, are lost.
/// target, whether or not that target exists, and the path does not exist
/// between the last unlink and the write. Taking a live symlink off the
/// path takes what it resolves to with it, because that is what the unlink
/// acts on first.
/// Any manual changes to the generated file, any other existing file at
/// that path, and whatever a symlink at that path resolves to, are lost.
///
/// A directory at the path, and a symlink at the path that resolves to a
/// directory, are the cases this cannot unlink, and both revert rather than
/// being written into or through.
///
/// The whole file is written on every call, so the same arguments always
/// produce the same bytes. The prefix and bytecode hash constant are always
Expand Down Expand Up @@ -150,7 +156,11 @@ library LibFs {
string.concat(LibCodeGen.filePrefix(), LibCodeGen.bytecodeHashConstantString(vm, instance), body);
//forge-lint: disable-next-line(unsafe-cheatcode)
vm.createDir(dir, true);
if (isPresent(vm, path)) {
// `vm.removeFile` resolves the path before it acts, so on a live symlink
// it takes what the link points at and leaves the link, now dangling.
// Every pass removes something the next one no longer finds, so this
// ends with the path holding nothing.
while (isPresent(vm, path)) {
//forge-lint: disable-next-line(unsafe-cheatcode)
vm.removeFile(path);
}
Expand Down
44 changes: 44 additions & 0 deletions test/src/lib/LibFs.isPresent.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,48 @@ contract LibFsIsPresentTest is Test {
remove(targetName);
remove(controlFileName);
}

/// A symlink at the generated path whose target exists is replaced by a
/// regular file holding the generated content, and the target does not
/// receive it: the write goes to the path, not through it. The target is
/// removed along the way, which is what makes the path free to hold a
/// regular file.
///
/// The content is compared against a second contract generated at a path
/// that held nothing, so the claim is that the two cases produce the same
/// file rather than that some particular bytes appear.
function testBuildFileForContractReplacesLiveSymlink() external {
string memory name = "LibFsIsPresentLive";
string memory linkName = "LibFsIsPresentLive.sol";
string memory targetName = "LibFsIsPresentLiveTarget.txt";
string memory controlName = "LibFsIsPresentLiveControl";
string memory controlFileName = "LibFsIsPresentLiveControl.sol";
remove(linkName);
remove(targetName);
remove(controlFileName);

vm.writeFile(pathFor(targetName), "SENTINEL");
symlink(linkName, targetName);
assertEq(LibFs.pathForContract(name), pathFor(linkName), "the link is not where the write goes");
assertEq(readlink(linkName).exitCode, 0, "the path under test is not a symlink");
assertTrue(vm.exists(pathFor(linkName)), "the link does not resolve");
assertEq(vm.readFile(pathFor(targetName)), "SENTINEL", "the link target is not the seeded file");

string memory body = "\n// live\n";
LibFs.buildFileForContract(vm, address(this), name, body);
LibFs.buildFileForContract(vm, address(this), controlName, body);

bool linkIsStillASymlink = readlink(linkName).exitCode == 0;
bool targetExists = vm.exists(pathFor(targetName));
string memory written = vm.readFile(pathFor(linkName));
string memory control = vm.readFile(pathFor(controlFileName));

remove(linkName);
remove(targetName);
remove(controlFileName);

assertFalse(linkIsStillASymlink, "the path is still a symlink");
assertFalse(targetExists, "the link target survived the write");
assertEq(written, control, "the file at the path is not what a write to a path holding nothing produces");
}
}
Loading