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
14 changes: 12 additions & 2 deletions src/lib/LibRainDeploySnapshot.sol
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ library LibRainDeploySnapshot {
return tagForVersion(vm.parseTomlString(vm.readFile("foundry.toml"), ".package.version"));
}

/// Whether `subject` is three non-empty runs of digits joined by exactly
/// two `separator`s.
/// Whether `subject` is three numbers joined by exactly two `separator`s,
/// each written without a leading zero.
///
/// The ONE definition of the release-version shape. It is asked with `.`
/// for a version out of `foundry.toml` and with `_` for the directory that
Expand All @@ -115,6 +115,16 @@ library LibRainDeploySnapshot {
separators++;
digitsInComponent = 0;
} else if (char >= "0" && char <= "9") {
// A component is one number, so it has one spelling. `01` and
// `1` are the same release and would freeze to two directories,
// neither of which `SnapshotAlreadyFrozen` sees as the other,
// and which `recordPrecedes` cannot order because they compare
// equal as versions. `i` is at least 1 wherever
// `digitsInComponent == 1`, because that digit was read at an
// earlier index.
if (digitsInComponent == 1 && subjectBytes[i - 1] == "0") {
return false;
}
digitsInComponent++;
} else {
return false;
Expand Down
29 changes: 29 additions & 0 deletions test/src/lib/LibRainDeploySnapshot.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,25 @@ contract LibRainDeploySnapshotTest is Test {
assertFalse(LibRainDeploySnapshot.isTag("collision-guard"));
}

/// A component has ONE spelling. A padded component is the same release as
/// its unpadded form: it freezes to a second directory the immutability
/// check does not recognise as the first, and the two compare equal as
/// versions so nothing can order them either.
function testIsStrictTripleRefusesLeadingZeros() external pure {
assertFalse(LibRainDeploySnapshot.isStrictTriple("0.01.5", "."));
assertFalse(LibRainDeploySnapshot.isStrictTriple("01.1.5", "."));
assertFalse(LibRainDeploySnapshot.isStrictTriple("0.1.05", "."));
assertFalse(LibRainDeploySnapshot.isStrictTriple("00.0.0", "."));
assertFalse(LibRainDeploySnapshot.isTag("0_01_5"));

// The boundary: a single zero IS the number zero, and `0.0.0` is a real
// version.
assertTrue(LibRainDeploySnapshot.isStrictTriple("0.0.0", "."));
assertTrue(LibRainDeploySnapshot.isStrictTriple("0.10.0", "."));
assertTrue(LibRainDeploySnapshot.isStrictTriple("10.0.100", "."));
assertTrue(LibRainDeploySnapshot.isTag("0_0_0"));
}

/// EVERY version a freeze accepts MUST produce a directory the record
/// recognises as a release. A version that could be frozen to a directory
/// the record then ignores is exactly the orphan snapshot
Expand Down Expand Up @@ -185,6 +204,16 @@ contract LibRainDeploySnapshotTest is Test {
}
}

/// The refusal MUST be reachable through the release path, naming the
/// version, rather than only through the predicate.
function testTagForVersionRefusesLeadingZeros() external {
string[3] memory bad = ["0.01.5", "01.1.5", "0.1.05"];
for (uint256 i = 0; i < bad.length; i++) {
vm.expectRevert(abi.encodeWithSelector(UnreleasableVersion.selector, bad[i]));
this.externalTagForVersion(bad[i]);
}
}

/// The tag read from `foundry.toml` MUST go through the same guard, so a
/// repo cannot reach a release path with a version the guard would refuse.
function testDeployTagUsesTheGuardedConversion() external view {
Expand Down
Loading