@@ -7,24 +7,46 @@ pragma solidity ^0.8.25;
77/// @param suite The key declared more than once.
88error DuplicateDeploySuite (string suite );
99
10- /// Thrown when a suite declares an EMPTY key.
10+ /// Thrown when a suite declares a key outside the key alphabet .
1111///
12- /// The empty string is the absent-sentinel: `RainDeployBroadcast.run()` reads
13- /// `DEPLOYMENT_SUITE` through `vm.envOr` with `string("")` as the default, so a
14- /// dispatch that left the suite input blank asks the registry for exactly this
15- /// key. A declaration free to answer it turns "told nothing" into a selection,
16- /// and `CREATE2` under a zero salt puts those bytes at their own permanent
17- /// address on every chain that dispatch reached.
12+ /// A key is a NAME, or a name and a release TAG joined by an at sign: the name
13+ /// lowercase letters and hyphens, the tag those plus digits and underscores,
14+ /// neither half empty and at most one at sign. Digits and underscores after the
15+ /// at sign only, which is where a tag needs them and where nothing else does.
1816///
19- /// Reserved on the DECLARATION rather than beside the substitution, for the
20- /// reason `NoDeployCandidates` is: a rule bound in the consumer is a rule most
17+ /// That is the kebab case a repo declares by hand, and it is what
18+ /// `LibRainDeploySnapshot` emits a released entry as — the candidate key the
19+ /// release was cut from, the at sign, and the record directory's tag, which
20+ /// `isTag` already holds to `X_Y_Z` — so a generated declaration needs no
21+ /// exemption from the rule a hand written one is held to. The at sign cannot
22+ /// appear in a name, so that emission carries exactly one however many releases
23+ /// a repo cuts.
24+ ///
25+ /// The key list is why an alphabet exists at all. `suiteNames()` joins the
26+ /// declared keys on `", "` for `UnknownDeploymentSuite`, which exists so a
27+ /// caller who does NOT already know the valid keys is told them; a key free to
28+ /// carry either of those characters renders as two, so the reader is told a
29+ /// different number of suites exist than do and is sent after a key that is
30+ /// declared nowhere. A list that cannot be read back as the set it names is the
31+ /// hardcoded string the registry exists to replace, spelled differently.
32+ ///
33+ /// The EMPTY key is refused by the same rule, and is the one refusal with a
34+ /// broadcast behind it: it is the value `RainDeployBroadcast.run()` substitutes
35+ /// for an absent `DEPLOYMENT_SUITE`, so a declaration allowed to answer it
36+ /// turns "told nothing" into a selection, and `CREATE2` under a zero salt puts
37+ /// those bytes at their own permanent address on every chain that dispatch
38+ /// reached. An alphabet admitting no zero byte key already says that; a length
39+ /// check beside it would be a second rule for one property, free to drift.
40+ ///
41+ /// Held on the DECLARATION rather than beside the substitution, for the reason
42+ /// `NoDeployCandidates` is: a rule bound in the consumer is a rule most
2143/// consumers do not run, and here every `suiteByName` caller pays for it rather
22- /// than only `run()`. Uniqueness does not already cover it — a lone empty key
23- /// collides with nothing.
44+ /// than only `run()`.
2445/// @param index Position in `allSuites()`: the released suites in declaration
25- /// order, then the candidates. The key itself names nothing, so the position is
46+ /// order, then the candidates. An empty key names nothing, so the position is
2647/// the only thing that can.
27- error EmptyDeploySuiteKey (uint256 index );
48+ /// @param suite The refused key.
49+ error InvalidDeploySuiteKey (uint256 index , string suite );
2850
2951/// Thrown when `DEPLOYMENT_SUITE` names no declared suite. Carries the valid
3052/// keys, because the whole point of a registry is that the answer is not one
@@ -79,11 +101,11 @@ error CandidateSourceMismatch(string suite, bytes32 storedCreationCodeHash, byte
79101/// make that comparison derived-against-derived, and a guard that compares a
80102/// value to itself is not a guard.
81103struct DeploySuite {
82- /// The key. Unique across every suite a repo declares, and never empty: it
83- /// is what `DEPLOYMENT_SUITE` selects for broadcasting, and the label every
84- /// verification error names. The empty string is the value an unset
85- /// `DEPLOYMENT_SUITE` arrives as, so it is reserved rather than declarable
86- /// — see `EmptyDeploySuiteKey` .
104+ /// The key. Unique across every suite a repo declares, and held to the key
105+ /// alphabet — see `InvalidDeploySuiteKey`. It is what
106+ /// `DEPLOYMENT_SUITE` selects for broadcasting, the label every
107+ /// verification error names, and what the valid-key list an unknown suite
108+ /// reports has to read back as .
87109 ///
88110 /// A repo with one contract and several frozen releases gives each release
89111 /// its own key, because each is separately deployable — a chain added after
@@ -253,6 +275,43 @@ abstract contract RainDeploySuitesBase {
253275 }
254276 }
255277
278+ /// Refuses a key the registry cannot carry — see `InvalidDeploySuiteKey`.
279+ ///
280+ /// Held against the WHOLE key rather than against a name a released key is
281+ /// derived from, because `releasedSuites()` declares finished keys: a rule
282+ /// that only knew the name half could not be asked about a released entry
283+ /// at all, and this is the one place every key a repo declares is read.
284+ /// @param index Position in `allSuites()`, for the refusal to name.
285+ /// @param suite The key to check.
286+ function checkSuiteKey (uint256 index , string memory suite ) internal pure {
287+ bytes memory key = bytes (suite);
288+ // Where the tag starts, and zero until an `@` is seen. Zero is not a
289+ // position a tag can start at, because an `@` opening the key leaves
290+ // the name half empty and is refused below.
291+ uint256 tagStart = 0 ;
292+
293+ for (uint256 i = 0 ; i < key.length ; i++ ) {
294+ bytes1 char = key[i];
295+ if (char == "@ " ) {
296+ if (i == 0 || tagStart != 0 ) {
297+ revert InvalidDeploySuiteKey (index, suite);
298+ }
299+ tagStart = i + 1 ;
300+ } else {
301+ bool nameAlphabet = (char >= "a " && char <= "z " ) || char == "- " ;
302+ bool tagAlphabet = (char >= "0 " && char <= "9 " ) || char == "_ " ;
303+ if (! (nameAlphabet || (tagStart != 0 && tagAlphabet))) {
304+ revert InvalidDeploySuiteKey (index, suite);
305+ }
306+ }
307+ }
308+
309+ // The empty key and a trailing at sign: the loop only refuses bytes that are there.
310+ if (tagStart == key.length ) {
311+ revert InvalidDeploySuiteKey (index, suite);
312+ }
313+ }
314+
256315 /// Every suite this repo declares: the released ones followed by the
257316 /// candidates. This is the verification set and the deploy registry, which
258317 /// are the same set because they are the same declaration.
@@ -261,13 +320,13 @@ abstract contract RainDeploySuitesBase {
261320 /// pay for the check and neither can be handed a registry that is ambiguous
262321 /// or that answers the absent-sentinel. One pass over the whole set, so a
263322 /// candidate colliding with another candidate is caught by the same code
264- /// that catches a candidate colliding with a release, and an empty key is
265- /// refused wherever in the declaration it was spelled — there is no second
266- /// rule to keep in step.
323+ /// that catches a candidate colliding with a release, and a key the
324+ /// alphabet refuses is refused wherever in the declaration it was spelled —
325+ /// there is no second rule to keep in step.
267326 ///
268- /// Unique AND non-empty , because neither implies the other: a lone empty
269- /// key collides with nothing, and it is the one key `run()` can be handed
270- /// by accident .
327+ /// Unique AND in the alphabet , because neither implies the other: a lone
328+ /// unreadable key collides with nothing, and two keys can be spelled
329+ /// perfectly and still be the same key .
271330 /// @return Every declared suite.
272331 function allSuites () internal pure returns (DeploySuite[] memory ) {
273332 DeploySuite[] memory released = releasedSuites ();
@@ -282,9 +341,7 @@ abstract contract RainDeploySuitesBase {
282341 }
283342
284343 for (uint256 i = 0 ; i < suites.length ; i++ ) {
285- if (bytes (suites[i].suite).length == 0 ) {
286- revert EmptyDeploySuiteKey (i);
287- }
344+ checkSuiteKey (i, suites[i].suite);
288345 for (uint256 j = i + 1 ; j < suites.length ; j++ ) {
289346 if (keccak256 (bytes (suites[i].suite)) == keccak256 (bytes (suites[j].suite))) {
290347 revert DuplicateDeploySuite (suites[i].suite);
@@ -296,6 +353,10 @@ abstract contract RainDeploySuitesBase {
296353 }
297354
298355 /// Every declared key, comma separated, for the unknown-suite error.
356+ ///
357+ /// Splitting the result on `", "` recovers exactly the declared keys,
358+ /// because the alphabet `allSuites` holds them to carries neither of those
359+ /// two characters anywhere but between two keys.
299360 /// @return The declared keys.
300361 function suiteNames () internal pure returns (string memory ) {
301362 DeploySuite[] memory suites = allSuites ();
0 commit comments