Skip to content

Commit e93d9c8

Browse files
Fix some false positive matches (#23)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent ddfeb8b commit e93d9c8

File tree

4 files changed

+155
-58
lines changed

4 files changed

+155
-58
lines changed

.github/workflows/main.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ jobs:
1010
fail-fast: false
1111
matrix:
1212
node-version:
13+
- 18
1314
- 16
15+
- 14
1416
steps:
15-
- uses: actions/checkout@v2
16-
- uses: actions/setup-node@v2
17+
- uses: actions/checkout@v3
18+
- uses: actions/setup-node@v3
1719
with:
1820
node-version: ${{ matrix.node-version }}
1921
- run: npm install

index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export default function semverRegex() {
2-
return /(?:(?<=^v?|\sv?)(?:(?:0|[1-9]\d{0,9}?)\.){2}(?:0|[1-9]\d{0,9}?)(?:-(?:0|[1-9]\d*?|[\da-z-]*?[a-z-][\da-z-]*?){0,100}?(?:\.(?:0|[1-9]\d*?|[\da-z-]*?[a-z-][\da-z-]*?))*?){0,100}?(?:\+[\da-z-]+?(?:\.[\da-z-]+?)*?){0,100}?\b){1,200}?/gi;
2+
return /(?<=^v?|\sv?)(?:(?:0|[1-9]\d{0,9}?)\.){2}(?:0|[1-9]\d{0,9})(?:-(?:--?|0|[1-9]\d*|\d*[a-z]+\d*)){0,100}(?=$| |\+|\.)(?:(?<=-\S+)(?:\.(?:--?|[\da-z-]*[a-z-]\d*|0|[1-9]\d*)){1,100}?)?(?!\.)(?:\+(?:[\da-z]\.?-?){1,100}?(?!\w))?(?!\+)/gi;
33
}

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
],
3535
"devDependencies": {
3636
"ava": "^4.2.0",
37-
"tsd": "^0.14.0",
38-
"xo": "^0.39.1"
37+
"tsd": "^0.20.0",
38+
"xo": "^0.49.0",
39+
"semver": "^7.3.7"
3940
}
4041
}

test.js

Lines changed: 147 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,108 @@
11
import test from 'ava';
2+
import semver from 'semver';
23
import semverRegex from './index.js';
34

4-
const fixtures = [
5+
const validStrings = [
56
'0.0.0',
67
'0.10.0',
78
'v1.0.0',
89
'0.0.0-foo',
10+
'0.0.0-foo-bar-baz',
911
'1.2.3-4',
1012
'2.7.2+asdf',
1113
'1.2.3-a.b.c.10.d.5',
1214
'2.7.2-foo+bar',
1315
'1.2.3-alpha.10.beta',
1416
'1.2.3-alpha.10.beta+build.unicorn.rainbow',
15-
'foo 0.0.0 bar 0.0.0',
16-
'99999.99999.99999'
17+
'99999.99999.99999',
18+
19+
// Pulled from https://regex101.com/r/vkijKf/1/
20+
'0.0.4',
21+
'1.2.3',
22+
'10.20.30',
23+
'1.1.2-prerelease+meta',
24+
'1.1.2+meta',
25+
'1.1.2+meta-valid',
26+
'1.0.0-alpha',
27+
'1.0.0-beta',
28+
'1.0.0-alpha.beta',
29+
'1.0.0-alpha.beta.1',
30+
'1.0.0-alpha.1',
31+
'1.0.0-alpha0.valid',
32+
'1.0.0-alpha.va1id',
33+
'1.0.0-alpha.0valid',
34+
'1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay',
35+
'1.0.0-rc.1+build.1',
36+
'2.0.0-rc.1+build.123',
37+
'1.2.3-beta',
38+
'10.2.3-DEV-SNAPSHOT',
39+
'1.2.3-SNAPSHOT-123',
40+
'1.0.0',
41+
'2.0.0',
42+
'1.1.7',
43+
'2.0.0+build.1848',
44+
'2.0.1-alpha.1227',
45+
'1.0.0-alpha+beta',
46+
'1.2.3----RC-SNAPSHOT.12.9.1--.12+788',
47+
'1.2.3----R-S.12.9.1--.12+meta',
48+
'1.2.3----RC-SNAPSHOT.12.9.1--.12',
49+
'1.0.0+0.build.1-rc.10000aaa-kk-0.1',
50+
// '99999999999999999999999.999999999999999999.99999999999999999', // Too long
51+
'1.0.0-0A.is.legal',
52+
];
53+
54+
const invalidStrings = [
55+
'1',
56+
'1.2',
57+
'1.2.3-0123',
58+
'1.2.3-0123.0123',
59+
'1.1.2+.123',
60+
'+invalid',
61+
'-invalid',
62+
'-invalid+invalid',
63+
'-invalid.01',
64+
'alpha',
65+
'alpha.beta',
66+
'alpha.beta.1',
67+
'alpha.1',
68+
'alpha+beta',
69+
'alpha_beta',
70+
'alpha.',
71+
'alpha..',
72+
'beta',
73+
'1.0.0-alpha_beta',
74+
'-alpha.',
75+
'1.0.0-alpha..',
76+
'1.0.0-alpha..1',
77+
'1.0.0-alpha...1',
78+
'1.0.0-alpha....1',
79+
'1.0.0-alpha.....1',
80+
'1.0.0-alpha......1',
81+
'1.0.0-alpha.......1',
82+
'01.1.1',
83+
'1.01.1',
84+
'1.1.01',
85+
'1.2',
86+
'1.2.3.DEV',
87+
'1.2-SNAPSHOT',
88+
'1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788',
89+
'1.2-RC-SNAPSHOT',
90+
'-1.0.3-gamma+b7718',
91+
'+justmeta',
92+
'9.8.7+meta+meta',
93+
'9.8.7-whatever+meta+meta',
94+
'99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12',
95+
'1.0.0-beta@beta',
1796
];
1897

1998
test('matches semver versions on test', t => {
20-
for (const fixture of fixtures) {
99+
for (const fixture of validStrings) {
21100
t.regex(fixture, semverRegex());
101+
t.true(semver.valid(fixture) !== null);
102+
103+
if (!fixture.startsWith('v')) { // Should we trim v prefix?
104+
t.deepEqual(fixture.match(semverRegex()), [fixture]);
105+
}
22106
}
23107

24108
t.notRegex('0.88', semverRegex());
@@ -30,59 +114,18 @@ test('matches semver versions on test', t => {
30114
test('returns semver on match', t => {
31115
t.deepEqual('0.0.0'.match(semverRegex()), ['0.0.0']);
32116
t.deepEqual('foo 0.0.0 bar 0.1.1'.match(semverRegex()), ['0.0.0', '0.1.1']);
117+
t.deepEqual('1.2.3-alpha.10.beta'.match(semverRegex()), ['1.2.3-alpha.10.beta']);
118+
t.deepEqual('0.0.0-foo-bar alpha.beta.1 1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788 1.0.0-alpha+beta 1.2.3----RC-SNAPSHOT.12.9.1--.12+788 1.2 1.2.3-4'.match(semverRegex()), ['0.0.0-foo-bar', '1.0.0-alpha+beta', '1.2.3----RC-SNAPSHOT.12.9.1--.12+788', '1.2.3-4']);
33119
});
34120

35121
test('#7, does not return tag prefix', t => {
36122
t.deepEqual('v0.0.0'.match(semverRegex()), ['0.0.0']);
37123
});
38124

39125
test('#14, does not match sub-strings of longer semver-similar strings, respect [email protected] clause 9', t => {
40-
// TODO: Some of these are disabled as we need to improve the regex.
41-
const invalidStrings = [
42-
'1',
43-
'1.2',
44-
// '1.2.3-0123',
45-
// '1.2.3-0123.0123',
46-
// '1.1.2+.123',
47-
'+invalid',
48-
'-invalid',
49-
'-invalid+invalid',
50-
'-invalid.01',
51-
'alpha',
52-
'alpha.beta',
53-
'alpha.beta.1',
54-
'alpha.1',
55-
'alpha+beta',
56-
'alpha_beta',
57-
'alpha.',
58-
'alpha..',
59-
'beta',
60-
// '1.0.0-alpha_beta',
61-
'-alpha.',
62-
// '1.0.0-alpha..',
63-
// '1.0.0-alpha..1',
64-
// '1.0.0-alpha...1',
65-
// '1.0.0-alpha....1',
66-
// '1.0.0-alpha.....1',
67-
// '1.0.0-alpha......1',
68-
// '1.0.0-alpha.......1',
69-
'01.1.1',
70-
'1.01.1',
71-
'1.1.01',
72-
'1.2',
73-
// '1.2.3.DEV',
74-
'1.2-SNAPSHOT',
75-
// '1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788',
76-
'1.2-RC-SNAPSHOT',
77-
'-1.0.3-gamma+b7718',
78-
'+justmeta'
79-
// '9.8.7+meta+meta',
80-
// '9.8.7-whatever+meta+meta',
81-
// '99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12'
82-
];
83-
84126
for (const string of invalidStrings) {
85127
t.notRegex(string, semverRegex());
128+
t.true(semver.valid(string) === null);
86129
}
87130
});
88131

@@ -93,26 +136,27 @@ test('#18, allow 0 as numeric identifier', t => {
93136
'1.2.0-alpha.10.beta+build.unicorn.rainbow',
94137
'1.2.3-0.10.beta+build.unicorn.rainbow',
95138
'1.2.3-alpha.0.beta+build.unicorn.rainbow',
96-
'1.2.3-alpha.10.0+build.unicorn.rainbow'
139+
'1.2.3-alpha.10.0+build.unicorn.rainbow',
97140
]) {
98141
t.regex(string, semverRegex());
142+
t.true(semver.valid(string) !== null);
99143
}
100144
});
101145

102146
// If tests take longer than a second, it's stuck on this and we have catatrophic backtracking.
103147
test('invalid version does not cause catatrophic backtracking', t => {
104148
t.regex(
105149
'v1.1.3-0aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa$',
106-
semverRegex()
150+
semverRegex(),
107151
);
108152

109-
const postfix = '.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'.repeat(99999);
153+
const postfix = '.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'.repeat(99_999);
110154
t.regex(
111155
`v1.1.3-0aa${postfix}$`,
112-
semverRegex()
156+
semverRegex(),
113157
);
114158

115-
for (let index = 1; index <= 50000; index++) {
159+
for (let index = 1; index <= 50_000; index++) {
116160
const start = Date.now();
117161
const fixture = `0.0.0-0${'.-------'.repeat(index)}@`;
118162
semverRegex().test(fixture);
@@ -127,4 +171,54 @@ test('invalid version does not cause catatrophic backtracking', t => {
127171
const difference = Date.now() - start;
128172
t.true(difference < 20, `Execution time: ${difference}`);
129173
}
174+
175+
for (let index = 1; index <= 30; index++) {
176+
// Attack string generated by https://devina.io/redos-checker
177+
const start = Date.now();
178+
const fixtures = [
179+
'0.0.1-i' + '--i-'.repeat(index) + '\u0000',
180+
'0' + ' 0.1.0-i0'.repeat(index) + '.1.1+1' + '1'.repeat(index) + 'A',
181+
'1.0.1--' + '-'.repeat(index) + '\u0000',
182+
'g' + ' 0.0.1-i+'.repeat(index) + 'a' + 'v0'.repeat(index) + '\u0000',
183+
];
184+
for (const fixture of fixtures) {
185+
semverRegex().test(fixture);
186+
}
187+
188+
const difference = Date.now() - start;
189+
t.true(difference < 20, `Execution time: ${difference}`);
190+
}
191+
192+
for (let index = 1; index <= 100; index++) {
193+
const start = Date.now();
194+
const shuffle = array => array.sort(() => Math.random() - 0.5);
195+
// Adapted from https://gist.github.com/6174/6062387
196+
const rndstr = (() => {
197+
const gen = (min, max) => max++ && Array.from({length: max - min}).map((s, i) => String.fromCodePoint(min + i));
198+
const sets = {
199+
num: gen(48, 57),
200+
alphaLower: gen(97, 122),
201+
alphaUpper: gen(65, 90),
202+
special: [...'~!@#$%^&*()_+-=[]{}|;:\'",./<>?'],
203+
};
204+
function * iter(length, set) {
205+
if (set.length === 0) {
206+
set = Object.values(sets).flat();
207+
}
208+
209+
for (let i = 0; i < length; i++) {
210+
yield set[Math.trunc(Math.random() * set.length)];
211+
}
212+
}
213+
214+
return Object.assign(((length, ...set) => [...iter(length, set.flat())].join('')), sets);
215+
})();
216+
const fuzz = Array.from({length: 100}).map(() => rndstr(100 * Math.random(), rndstr.alphaUpper, rndstr.special, rndstr.alphaLower, rndstr.num));
217+
const fixture = shuffle(Array.from({length: index}).map(() => [validStrings, invalidStrings, fuzz]).flat(2)).join(' ');
218+
219+
semverRegex().test(fixture);
220+
221+
const difference = Date.now() - start;
222+
t.true(difference < 50, `Execution time: ${difference}`);
223+
}
130224
});

0 commit comments

Comments
 (0)