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
134 changes: 134 additions & 0 deletions src/lib/LibFs.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,62 @@ import {LibCodeGen} from "./LibCodeGen.sol";
/// path, so it is a cross repo contract rather than an internal detail.
string constant GENERATED_DIR = "src/generated";

/// Thrown when a tag is not a single path segment drawn from the tag alphabet.
/// Such a tag cannot be interpolated into a directory path.
/// @param tag The rejected tag.
error InvalidTag(string tag);

/// @title LibFs
/// @notice A library for file system operations related to code generation.
/// @dev Uses foundry's Vm cheat codes for file operations. Notably standardizes
/// the placement and idempotent creation of generated files.
library LibFs {
/// Reverts unless `tag` is drawn from the tag alphabet: at least one
/// character, each of them an ASCII letter, a digit, `_` or `$`. That is
/// the Solidity identifier alphabet without the rule against a leading
/// digit, because a tag names a directory rather than a declaration and the
/// release tags this org freezes are `<major>_<minor>_<patch>`, which opens
/// with one. Restricting a tag to that alphabet is what makes it safe to
/// interpolate into a path: no character in the set is a path separator,
/// none of them is `.`, so no tag is `.` or `..` and none of them reaches
/// past the single directory it names.
/// @param tag The tag to check.
function requireTag(string memory tag) internal pure {
bytes memory tagBytes = bytes(tag);
if (tagBytes.length == 0) {
revert InvalidTag(tag);
}
for (uint256 i = 0; i < tagBytes.length; i++) {
bytes1 char = tagBytes[i];
bool isLetter = (char >= 0x41 && char <= 0x5A) || (char >= 0x61 && char <= 0x7A);
bool isDigit = char >= 0x30 && char <= 0x39;
bool isUnderscoreOrDollar = char == 0x5F || char == 0x24;
if (!(isLetter || isDigit || isUnderscoreOrDollar)) {
revert InvalidTag(tag);
}
}
}

/// @notice Constructs the directory that a tag's generated files live in.
///
/// Reverts unless `tag` is drawn from the tag alphabet, so every directory
/// this function returns is a direct child of `GENERATED_DIR`. The check is
/// here rather than at the write because the directory is what carries the
/// tag out of this library: a caller that takes the returned directory and
/// does its own IO with it gets the same confinement
/// `buildFileForTaggedContract` does, and there is no tag for which this
/// library produces a directory at all without producing a safe one.
///
/// An accepted tag is interpolated verbatim, so it reaches the directory
/// byte for byte and is never quoted, escaped, trimmed, case folded or
/// truncated.
/// @param tag The tag, interpolated verbatim.
/// @return The directory as a string.
function dirForTag(string memory tag) internal pure returns (string memory) {
requireTag(tag);
return string.concat(GENERATED_DIR, "/", tag);
}

/// @notice Constructs the file path for a contract's generated file.
///
/// Reverts unless `contractName` is a Solidity identifier, so every path
Expand Down Expand Up @@ -55,6 +106,35 @@ library LibFs {
return string.concat(dir, "/", contractName, ".sol");
}

/// @notice Constructs the file path for a contract's generated file inside a
/// tag's directory, which is the layout per release deploy pin snapshots
/// use.
///
/// Reverts unless `tag` is drawn from the tag alphabet and `contractName`
/// is a Solidity identifier, so every path this function returns is exactly
/// two segments inside `GENERATED_DIR`: neither argument can express a path
/// separator, `.` or `..`, so neither of them can add a segment, remove
/// one, or leave the directory. The tag is checked first, so a call that
/// gets both wrong names the tag.
///
/// Both accepted arguments are interpolated verbatim, so they reach the
/// path byte for byte and are never quoted, escaped, trimmed, case folded
/// or truncated.
/// @dev This is `pathForContractIn` applied to `dirForTag(tag)`, so the name
/// rule and the verbatim interpolation of the name are exactly
/// `pathForContract`'s, one directory deeper.
/// @param tag The tag whose directory the file lives in, interpolated
/// verbatim.
/// @param contractName The name of the contract, interpolated verbatim.
/// @return The file path as a string.
function pathForTaggedContract(string memory tag, string memory contractName)
internal
pure
returns (string memory)
{
return pathForContractIn(dirForTag(tag), contractName);
}

/// @notice True if anything occupies `path`, including a symlink whose
/// target does not exist.
/// @dev `vm.exists` answers for whatever the path resolves to, so it reports
Expand Down Expand Up @@ -190,4 +270,58 @@ library LibFs {
//forge-lint: disable-next-line(unsafe-cheatcode)
vm.writeFile(path, content);
}

/// @notice Builds a file for a generated contract at
/// `pathForTaggedContract(tag, contractName)`.
///
/// `tag` must be drawn from the tag alphabet and `contractName` must be a
/// Solidity identifier, which `pathForTaggedContract` requires of every
/// path it returns, so the file is always two segments inside
/// `GENERATED_DIR` and a rejected tag or name reverts before any cheatcode
/// is reached.
///
/// The tag's directory is created if it does not exist, along with
/// `GENERATED_DIR` itself, so the first generation for a tag does not need
/// it committed already.
///
/// 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, whether or not that target exists, and the path does not exist
/// between the last unlink and the write.
/// 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.
///
/// 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
/// included, further content is provided in the body parameter, which is
/// expected to be generated by `LibCodeGen` by the caller.
///
/// The file lands in the calling project's repo, so the licence it is under
/// and the copyright holder it names come from the caller and are subject to
/// `LibCodeGen.filePrefix`'s rule for them.
/// @dev This is the `dir` overload of `buildFileForContract` applied to
/// `dirForTag(tag)`, so everything that overload states holds here, and the
/// only thing this function adds is that the directory is not the caller's
/// to choose: it is derived from a tag that `dirForTag` refuses unless it
/// names exactly one directory inside `GENERATED_DIR`.
/// @param vm The Vm instance for file operations.
/// @param instance The contract instance whose bytecode hash is to be
/// included.
/// @param tag The tag whose directory the file lives in.
/// @param contractName The name of the contract.
/// @param spdxLicenseIdentifier The SPDX licence identifier the written file
/// declares.
/// @param copyrightText The copyright text the written file declares.
/// @param body The body of the contract file to be written.
function buildFileForTaggedContract(
Vm vm,
address instance,
string memory tag,
string memory contractName,
string memory spdxLicenseIdentifier,
string memory copyrightText,
string memory body
) internal {
buildFileForContract(vm, instance, dirForTag(tag), contractName, spdxLicenseIdentifier, copyrightText, body);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
28 changes: 28 additions & 0 deletions test/concrete/LibFsExternal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,32 @@ contract LibFsExternal {
function pathForContract(string memory contractName) external pure returns (string memory) {
return LibFs.pathForContract(contractName);
}

function buildFileForTaggedContract(
Vm vm,
address instance,
string memory tag,
string memory contractName,
string memory spdxLicenseIdentifier,
string memory copyrightText,
string memory body
) external {
LibFs.buildFileForTaggedContract(vm, instance, tag, contractName, spdxLicenseIdentifier, copyrightText, body);
}

function pathForTaggedContract(string memory tag, string memory contractName)
external
pure
returns (string memory)
{
return LibFs.pathForTaggedContract(tag, contractName);
}

function dirForTag(string memory tag) external pure returns (string memory) {
return LibFs.dirForTag(tag);
}

function requireTag(string memory tag) external pure {
LibFs.requireTag(tag);
}
}
44 changes: 44 additions & 0 deletions test/lib/LibCodeGenSlow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,17 @@ library LibCodeGenSlow {
return haystack.length;
}

/// Copies `len` bytes out of `data` starting at `start`, so a test can name
/// each region of a constructed path independently rather than rebuilding
/// it with the same `string.concat` the library uses and asserting it
/// equals itself.
function sliceSlow(bytes memory data, uint256 start, uint256 len) internal pure returns (bytes memory out) {
out = new bytes(len);
for (uint256 i = 0; i < len; i++) {
out[i] = data[start + i];
}
}

/// 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
Expand Down Expand Up @@ -279,6 +290,39 @@ library LibCodeGenSlow {
return true;
}

/// True if `tag` is drawn from the tag alphabet, decided by membership of
/// the written out alphabet rather than by arithmetic. The tag alphabet is
/// the identifier alphabet with no rule about the first character, which is
/// exactly `SLOW_TAIL_ALPHABET`: a tail character is any character an
/// identifier admits at all.
function isTagSlow(string memory tag) internal pure returns (bool) {
bytes memory tagBytes = bytes(tag);
if (tagBytes.length == 0) {
return false;
}
for (uint256 i = 0; i < tagBytes.length; i++) {
if (!containsSlow(SLOW_TAIL_ALPHABET, tagBytes[i])) {
return false;
}
}
return true;
}

/// Folds arbitrary bytes into a tag, so that the accepted half of the tag
/// domain can be fuzzed at all. Unlike `nameFromSeedSlow` the first
/// character is drawn from the same alphabet as the rest, because a tag has
/// no rule about its first character, so the digit opening tags such as
/// `0_1_1` is reachable here.
function tagFromSeedSlow(bytes memory seed) internal pure returns (string memory) {
bytes memory alphabet = bytes(SLOW_TAIL_ALPHABET);
uint256 length = seed.length == 0 ? 1 : seed.length;
bytes memory tag = new bytes(length);
for (uint256 i = 0; i < length; i++) {
tag[i] = alphabet[(seed.length == 0 ? 0 : uint256(uint8(seed[i]))) % alphabet.length];
}
return string(tag);
}

/// Folds arbitrary bytes into a name that is a Solidity identifier, so that
/// the accepted half of the domain can be fuzzed at all. Random bytes are
/// essentially never an identifier, so fuzzing names directly only ever
Expand Down
Loading
Loading