Skip to content

Commit b423060

Browse files
authored
✨ SemVerLib (#1411)
1 parent 70a2e1b commit b423060

File tree

5 files changed

+362
-0
lines changed

5 files changed

+362
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ utils
100100
├─ SSTORE2 — "Library for cheaper reads and writes to persistent storage"
101101
├─ SafeCastLib — "Library for integer casting that reverts on overflow"
102102
├─ SafeTransferLib — "Safe ERC20/ETH transfer lib that handles missing return values"
103+
├─ SemVerLib — "Library for comparing SemVers"
103104
├─ SignatureCheckerLib — "Library for verification of ECDSA and ERC1271 signatures"
104105
├─ UUPSUpgradeable — "UUPS proxy mixin"
105106
├─ UpgradeableBeacon — "Upgradeable beacon for ERC1967 beacon proxies"

docs/utils/semverlib.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# SemVerLib
2+
3+
Library for comparing SemVers.
4+
5+
6+
7+
8+
9+
10+
<!-- customintro:start --><!-- customintro:end -->
11+
12+
## Comparison
13+
14+
### cmp(bytes32,bytes32)
15+
16+
```solidity
17+
function cmp(bytes32 a, bytes32 b) internal pure returns (int256 result)
18+
```
19+
20+
Returns -1 if `a < b`, 0 if `a == b`, 1 if `a > b`.
21+
For efficiency, this is a forgiving, non-reverting parser:
22+
- Early returns if a strict order can be determined.
23+
- Skips the first byte if it is `v` (case insensitive).
24+
- If a strict order cannot be determined, returns 0.
25+
To convert a regular string to a small string (bytes32), use `LibString.toSmallString`.

src/Milady.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import "./utils/ReentrancyGuard.sol";
6262
import "./utils/SSTORE2.sol";
6363
import "./utils/SafeCastLib.sol";
6464
import "./utils/SafeTransferLib.sol";
65+
import "./utils/SemVerLib.sol";
6566
import "./utils/SignatureCheckerLib.sol";
6667
import "./utils/UUPSUpgradeable.sol";
6768
import "./utils/UpgradeableBeacon.sol";

src/utils/SemVerLib.sol

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.4;
3+
4+
/// @notice Library for comparing SemVers.
5+
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/SemVerLib.sol)
6+
library SemVerLib {
7+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
8+
/* COMPARISON */
9+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
10+
11+
/// @dev Returns -1 if `a < b`, 0 if `a == b`, 1 if `a > b`.
12+
/// For efficiency, this is a forgiving, non-reverting parser:
13+
/// - Early returns if a strict order can be determined.
14+
/// - Skips the first byte if it is `v` (case insensitive).
15+
/// - If a strict order cannot be determined, returns 0.
16+
/// To convert a regular string to a small string (bytes32), use `LibString.toSmallString`.
17+
function cmp(bytes32 a, bytes32 b) internal pure returns (int256 result) {
18+
/// @solidity memory-safe-assembly
19+
assembly {
20+
function mmp(i_, a_) -> _r, _o {
21+
for { _o := i_ } iszero(gt(sub(byte(_o, a_), 48), 9)) { _o := add(1, _o) } {
22+
_r := add(mul(10, _r), sub(byte(_o, a_), 48))
23+
}
24+
}
25+
function pre(i_, a_) -> hasNonDigit_, _r, _o {
26+
mstore(0x00, 0)
27+
for { _o := i_ } 1 { _o := add(1, _o) } {
28+
let c_ := byte(_o, a_)
29+
if and(1, shr(c_, 0x480000000001)) { break } // '\x00', '.', '+'
30+
let digit_ := sub(c_, 48)
31+
hasNonDigit_ := or(hasNonDigit_, gt(digit_, 9))
32+
_r := add(mul(10, _r), digit_)
33+
mstore8(sub(_o, i_), c_)
34+
}
35+
mstore(shl(5, hasNonDigit_), _r) // Overwrite if it's numeric.
36+
_r := mload(0x00)
37+
}
38+
let x, i := mmp(eq(118, or(32, byte(0, a))), a) // 'v', 'V'
39+
let y, j := mmp(eq(118, or(32, byte(0, b))), b) // 'v', 'V'
40+
result := sub(gt(x, y), lt(x, y))
41+
for {} 1 {} {
42+
let u := eq(byte(i, a), 46) // `.`
43+
let v := eq(byte(j, b), 46) // `.`
44+
if iszero(lt(result, or(u, v))) { break }
45+
if u { u, i := mmp(add(i, 1), a) } // `.`
46+
if v { v, j := mmp(add(j, 1), b) } // `.`
47+
result := sub(gt(u, v), lt(u, v))
48+
}
49+
if iszero(result) {
50+
let u := eq(byte(i, a), 45) // `-`
51+
let v := eq(byte(j, b), 45) // `-`
52+
result := sub(lt(u, v), gt(u, v))
53+
for {} lt(result, u) {} {
54+
u, x, i := pre(add(i, 1), a)
55+
v, y, j := pre(add(j, 1), b)
56+
result := sub(gt(u, v), lt(u, v))
57+
if result { break }
58+
result := sub(gt(x, y), lt(x, y))
59+
if result { break }
60+
u := eq(byte(i, a), 46) // `.`
61+
v := eq(byte(j, b), 46) // `.`
62+
result := sub(gt(u, v), lt(u, v))
63+
}
64+
}
65+
}
66+
}
67+
}

test/SemVerLib.t.sol

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.4;
3+
4+
import "./utils/SoladyTest.sol";
5+
import {SemVerLib} from "../src/utils/SemVerLib.sol";
6+
import {LibString} from "../src/utils/LibString.sol";
7+
8+
contract SemVerLibTest is SoladyTest {
9+
function _checkEq(bytes32 a, bytes32 b) internal {
10+
assertEq(SemVerLib.cmp(a, b), 0);
11+
assertEq(SemVerLib.cmp(_addMeta(a), b), 0);
12+
assertEq(SemVerLib.cmp(a, _addMeta(b)), 0);
13+
assertEq(SemVerLib.cmp(_addMeta(a), _addMeta(b)), 0);
14+
}
15+
16+
function _checkLt(bytes32 a, bytes32 b) internal {
17+
assertEq(SemVerLib.cmp(a, b), -1);
18+
assertEq(SemVerLib.cmp(_addMeta(a), b), -1);
19+
assertEq(SemVerLib.cmp(a, _addMeta(b)), -1);
20+
assertEq(SemVerLib.cmp(_addMeta(a), _addMeta(b)), -1);
21+
(a, b) = (b, a);
22+
assertEq(SemVerLib.cmp(a, b), 1);
23+
assertEq(SemVerLib.cmp(_addMeta(a), b), 1);
24+
assertEq(SemVerLib.cmp(a, _addMeta(b)), 1);
25+
assertEq(SemVerLib.cmp(_addMeta(a), _addMeta(b)), 1);
26+
}
27+
28+
function _addMeta(bytes32 a) internal returns (bytes32) {
29+
bytes memory data = bytes(LibString.fromSmallString(a));
30+
if (data.length < 20) {
31+
data = abi.encodePacked(data, "+");
32+
if (_randomChance(2)) {
33+
data = abi.encodePacked(
34+
data, _truncateBytes(abi.encodePacked(_random()), _randomUniform() % 5)
35+
);
36+
}
37+
return LibString.toSmallString(string(data));
38+
} else {
39+
return a;
40+
}
41+
}
42+
43+
function _s(uint256 x) internal pure returns (bytes memory) {
44+
return bytes(LibString.toString(x));
45+
}
46+
47+
function _s(bytes memory x) internal pure returns (bytes32) {
48+
return LibString.toSmallString(string(x));
49+
}
50+
51+
function _s(bytes32 x) internal pure returns (bytes memory) {
52+
return bytes(LibString.fromSmallString(x));
53+
}
54+
55+
function _s(uint256[] memory x) internal pure returns (bytes32) {
56+
bytes memory buffer;
57+
for (uint256 i; i < x.length; ++i) {
58+
if (i != 0) {
59+
buffer = abi.encodePacked(buffer, ".", _s(x[i]));
60+
} else {
61+
buffer = abi.encodePacked(buffer, _s(x[i]));
62+
}
63+
}
64+
return _s(buffer);
65+
}
66+
67+
function _maybePrependV(bytes32 x) internal returns (bytes32) {
68+
if (_randomChance(2)) return x;
69+
if (_randomChance(2)) return _s(abi.encodePacked("v", _s(x)));
70+
return _s(abi.encodePacked("V", _s(x)));
71+
}
72+
73+
function testCmpMajorMinorPatch(bytes32) public {
74+
uint256 n = _bound(_randomUniform(), 0, 5);
75+
uint256[] memory a = new uint256[](n);
76+
uint256[] memory b = new uint256[](n);
77+
for (uint256 i; i < n; ++i) {
78+
a[i] = _bound(_random(), 0, 10000);
79+
b[i] = _bound(_random(), 0, 10000);
80+
}
81+
int256 expected = _cmpMajorMinorPatchOriginal(a, b);
82+
if (expected == 0) {
83+
_checkEq(_maybePrependV(_s(a)), _maybePrependV(_s(b)));
84+
} else if (expected == 1) {
85+
_checkLt(_maybePrependV(_s(b)), _maybePrependV(_s(a)));
86+
} else if (expected == -1) {
87+
_checkLt(_maybePrependV(_s(a)), _maybePrependV(_s(b)));
88+
} else {
89+
revert("Should never reach here.");
90+
}
91+
}
92+
93+
function _cmpMajorMinorPatchOriginal(uint256[] memory a, uint256[] memory b)
94+
internal
95+
pure
96+
returns (int256)
97+
{
98+
require(a.length == b.length, "Input arrays must have same lengths.");
99+
for (uint256 i; i < a.length; ++i) {
100+
if (a[i] > b[i]) return 1;
101+
if (a[i] < b[i]) return -1;
102+
}
103+
return 0;
104+
}
105+
106+
struct _CmpPreReleaseTemps {
107+
uint256 n;
108+
uint256[] a;
109+
uint256[] b;
110+
bool aIsNum;
111+
bool bIsNum;
112+
uint256 aNum;
113+
uint256 bNum;
114+
bytes aBuffer;
115+
bytes bBuffer;
116+
bytes aPreReleaseBuffer;
117+
bytes bPreReleaseBuffer;
118+
int256 lexoCmpResult;
119+
}
120+
121+
function testCmpPreRelease(bytes32) public {
122+
_CmpPreReleaseTemps memory t;
123+
t.n = _bound(_randomUniform(), 1, 3);
124+
t.a = new uint256[](t.n);
125+
t.b = new uint256[](t.n);
126+
for (uint256 i; i < t.n; ++i) {
127+
t.a[i] = _bound(_random(), 0, 200);
128+
t.b[i] = t.a[i];
129+
}
130+
t.aBuffer = _s(_maybePrependV(_s(t.a)));
131+
t.bBuffer = _s(_maybePrependV(_s(t.b)));
132+
t.aIsNum = _randomChance(2);
133+
t.bIsNum = _randomChance(2);
134+
135+
if (t.aIsNum) {
136+
t.aNum = _random() % (10 ** (32 - 1 - t.aBuffer.length));
137+
t.aBuffer = abi.encodePacked(t.aBuffer, "-", _s(t.aNum));
138+
} else {
139+
t.aNum = _random() % (10 ** (32 - 2 - t.aBuffer.length));
140+
t.aPreReleaseBuffer = abi.encodePacked(_s(t.aNum), "h");
141+
t.aBuffer = abi.encodePacked(t.aBuffer, "-", t.aPreReleaseBuffer);
142+
}
143+
144+
if (t.bIsNum) {
145+
t.bNum = _random() % (10 ** (32 - 1 - t.bBuffer.length));
146+
t.bBuffer = abi.encodePacked(t.bBuffer, "-", _s(t.bNum));
147+
} else {
148+
t.bNum = _random() % (10 ** (32 - 2 - t.bBuffer.length));
149+
t.bPreReleaseBuffer = abi.encodePacked(_s(t.bNum), "h");
150+
t.bBuffer = abi.encodePacked(t.bBuffer, "-", t.bPreReleaseBuffer);
151+
}
152+
153+
if (t.aIsNum && t.bIsNum) {
154+
if (t.aNum < t.bNum) {
155+
_checkLt(_s(t.aBuffer), _s(t.bBuffer));
156+
} else if (t.aNum > t.bNum) {
157+
_checkLt(_s(t.bBuffer), _s(t.aBuffer));
158+
} else {
159+
_checkEq(_s(t.aBuffer), _s(t.bBuffer));
160+
}
161+
} else if (t.aIsNum && !t.bIsNum) {
162+
_checkLt(_s(t.aBuffer), _s(t.bBuffer));
163+
} else if (!t.aIsNum && t.bIsNum) {
164+
_checkLt(_s(t.bBuffer), _s(t.aBuffer));
165+
} else if (!t.aIsNum && !t.bIsNum) {
166+
t.lexoCmpResult = _lexoCmp(t.aPreReleaseBuffer, t.bPreReleaseBuffer);
167+
if (t.lexoCmpResult == -1) {
168+
_checkLt(_s(t.aBuffer), _s(t.bBuffer));
169+
} else if (t.lexoCmpResult == 1) {
170+
_checkLt(_s(t.bBuffer), _s(t.aBuffer));
171+
} else {
172+
_checkEq(_s(t.aBuffer), _s(t.bBuffer));
173+
}
174+
}
175+
}
176+
177+
function _lexoCmp(bytes memory a, bytes memory b) internal pure returns (int256) {
178+
uint256 len = a.length < b.length ? a.length : b.length;
179+
for (uint256 i; i < len; ++i) {
180+
uint8 ac = uint8(a[i]);
181+
uint8 bc = uint8(b[i]);
182+
if (ac < bc) return -1;
183+
if (ac > bc) return 1;
184+
}
185+
if (a.length < b.length) return -1;
186+
if (a.length > b.length) return 1;
187+
return 0;
188+
}
189+
190+
function testCmp() public {
191+
// Compliant.
192+
_checkEq("1.0.0", "1.0.0");
193+
_checkLt("1.0.0", "1.0.1");
194+
_checkLt("1.0.0", "1.1.0");
195+
_checkLt("1.0.0", "1.1.1");
196+
_checkLt("1.0.0", "2.0.1");
197+
_checkLt("1.0.0", "2.1.0");
198+
_checkLt("1.0.0", "2.1.1");
199+
_checkLt("1.2.0", "2.1.1");
200+
_checkLt("1.2.999999", "2.1.1");
201+
_checkLt("1.9.999", "2.0.0");
202+
203+
// Forgiving.
204+
_checkLt("a", "1");
205+
_checkLt("a1", "1");
206+
_checkLt("!", "1");
207+
_checkEq("1", "1");
208+
_checkLt("1", "2");
209+
_checkLt("1a", "2");
210+
_checkLt("1a", "2a");
211+
_checkLt("1", "2a");
212+
_checkLt("", "2a");
213+
_checkEq("", "");
214+
215+
_checkEq("v1.2.3", "1.2.3");
216+
_checkLt("v1.2.2", "1.2.3");
217+
_checkLt("v1.2", "1.2.3");
218+
_checkEq("v1.2", "1.2.0");
219+
_checkEq("1.2.3", "v1.2.3");
220+
_checkLt("1.2.2", "v1.2.3");
221+
_checkLt("1.2", "v1.2.3");
222+
_checkEq("1.2", "v1.2.0");
223+
224+
_checkEq("1.2", "1.2.0");
225+
_checkLt("1.2.3-alpha", "1.2.3");
226+
_checkLt("1.2-alpha", "1.2.3");
227+
_checkEq("1.2.3-alpha", "1.2.3-alpha");
228+
_checkLt("1.2.3-alpha", "1.2.3-alpha.123");
229+
_checkLt("1.2.3-alpha.123", "1.2.3-alpha.124");
230+
_checkLt("1.2.3-alpha.123.z", "1.2.3-alpha.124");
231+
_checkLt("1.2.3-alpha.123.", "1.2.3-alpha.124");
232+
_checkLt("1.2.3-alpha.124", "1.2.3-alpha.124a");
233+
_checkLt("1.2.3-alpha.124", "1.2.3-alpha.12a");
234+
_checkLt("1.2.3-thequickbrownfoxjumpsover", "1.2.3-thequickbrownfoxjumpsover1");
235+
_checkLt("1.2.3-thequickbrownfoxjumpsover", "1.2.3-thequickbrownfoxjumpsover0");
236+
_checkEq("1.2.3-thequickbrownfoxjumpsover0", "1.2.3-thequickbrownfoxjumpsover0");
237+
_checkLt("1.2.3-99999999999999999999999999", "1.2.3-thequickbrownfoxjumpsover0");
238+
_checkLt("1.2.3-99999999999999999999999999", "1.2.3-t");
239+
_checkLt("1.2.3-alpha", "1.2.3-alpha.0");
240+
_checkLt("1.2-alpha", "1.2.3-alpha");
241+
242+
_checkLt("1.2.3-1", "1.2.3-a");
243+
_checkLt("1.2.3-1", "1.2.3-alpha");
244+
_checkLt("1.2.3-1.0", "1.2.3-1.a");
245+
246+
_checkLt("1.2.3-alpha", "1.2.3-alpha.0");
247+
_checkLt("1.2.3-alpha.1", "1.2.3-alpha.1.1");
248+
_checkLt("1.2.3-alpha.1.a", "1.2.3-alpha.1.a.0");
249+
250+
_checkEq("1.2.3-alpha+build", "1.2.3-alpha");
251+
_checkEq("1.2.3+build", "1.2.3");
252+
_checkLt("1.2.3-alpha", "1.2.3+build");
253+
254+
_checkLt("1..3", "1.1.3");
255+
_checkLt("1..4", "1.1.3"); // `1.0.4` < `1.1.3`.
256+
_checkEq("1.2.3a", "1.2.3a");
257+
_checkLt("1.2.3a", "1.2.4");
258+
259+
_checkEq("1..4", "1.0.4"); // confirm parsing is consistent
260+
_checkLt("1..4", "1.0.5"); // `1.0.4` < `1.0.5`
261+
_checkLt("1..4", "1.1"); // `1.0.4` < `1.1.0`
262+
_checkLt("1..", "1.0.1"); // `1.0.0` < `1.0.1` if final component is missing
263+
_checkEq("1.0.", "1.0.0"); // forgiving trailing dot
264+
265+
_checkEq("01.002.0003", "1.2.3");
266+
_checkEq("v01.2.03", "1.2.3");
267+
}
268+
}

0 commit comments

Comments
 (0)