Skip to content

Commit c7474e0

Browse files
committed
fix: support ipv6 addresses
1 parent 0dd873e commit c7474e0

2 files changed

Lines changed: 106 additions & 2 deletions

File tree

src/index.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ const QUESTION_MARK = 63
1313
const PLUS = 43
1414
const BACKSLASH = 92
1515
const UNDERSCORE = 95
16+
const OPEN_BRACKET = 91
17+
const CLOSE_BRACKET = 93
1618

1719
const enum TokenType {
1820
Literal,
@@ -95,6 +97,12 @@ function parsePattern(pattern: string): Array<Token> {
9597
const tokens: Array<Token> = []
9698
let i = 0
9799
const length = pattern.length
100+
// Tracks whether the cursor is inside a `[...]` group, used for IPv6
101+
// host literals like `http://[2001:db8::1]/path`. Inside brackets,
102+
// `:` and `*` are treated as literal characters so that hextets
103+
// beginning with hex letters (e.g. `:db8`) are not mis-parsed as
104+
// path parameters.
105+
let inBrackets = false
98106

99107
while (i < length) {
100108
const code = pattern.charCodeAt(i)
@@ -103,7 +111,7 @@ function parsePattern(pattern: string): Array<Token> {
103111
// Escaped character — consume the backslash and the next character
104112
// as a literal. Fall through to the literal branch below by not
105113
// advancing `i` here (the literal branch handles it).
106-
} else if (code === ASTERISK) {
114+
} else if (code === ASTERISK && !inBrackets) {
107115
tokens.push({
108116
type: TokenType.Wildcard,
109117
nextLiteral: undefined,
@@ -112,6 +120,7 @@ function parsePattern(pattern: string): Array<Token> {
112120
continue
113121
} else if (
114122
code === COLON &&
123+
!inBrackets &&
115124
i + 1 < length &&
116125
isIdentStartCode(pattern.charCodeAt(i + 1))
117126
) {
@@ -157,12 +166,25 @@ function parsePattern(pattern: string): Array<Token> {
157166
continue
158167
}
159168

160-
if (charCode === ASTERISK) {
169+
if (charCode === OPEN_BRACKET) {
170+
inBrackets = true
171+
i++
172+
continue
173+
}
174+
175+
if (charCode === CLOSE_BRACKET) {
176+
inBrackets = false
177+
i++
178+
continue
179+
}
180+
181+
if (charCode === ASTERISK && !inBrackets) {
161182
break
162183
}
163184

164185
if (
165186
charCode === COLON &&
187+
!inBrackets &&
166188
i + 1 < length &&
167189
isIdentStartCode(pattern.charCodeAt(i + 1))
168190
) {

tests/ipv6.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { matchPattern, MatchResult } from '#src/index.js'
2+
import {
3+
NO_MATCH,
4+
MATCHES_WITH_PARAMS,
5+
MATCHES_WITHOUT_PARAMS,
6+
} from '#tests/utils.js'
7+
8+
it.each<
9+
[
10+
Parameters<typeof matchPattern>[0],
11+
Parameters<typeof matchPattern>[1],
12+
MatchResult,
13+
]
14+
>([
15+
/* Literal IPv6 addresses */
16+
['http://[::1]/', 'http://[::1]/', MATCHES_WITHOUT_PARAMS],
17+
['http://[::1]/', new URL('http://[::1]/'), MATCHES_WITHOUT_PARAMS],
18+
['http://[::1]', new URL('http://[::1]'), MATCHES_WITHOUT_PARAMS],
19+
[
20+
'http://[2001:db8::1]/path',
21+
'http://[2001:db8::1]/path',
22+
MATCHES_WITHOUT_PARAMS,
23+
],
24+
[
25+
'http://[2001:db8::a1]/x',
26+
'http://[2001:db8::a1]/x',
27+
MATCHES_WITHOUT_PARAMS,
28+
],
29+
['http://[fe80::abcd]/', 'http://[fe80::abcd]/', MATCHES_WITHOUT_PARAMS],
30+
[
31+
'http://[2001:db8:85a3::8a2e:370:7334]/',
32+
'http://[2001:db8:85a3::8a2e:370:7334]/',
33+
MATCHES_WITHOUT_PARAMS,
34+
],
35+
36+
/* IPv6 with port */
37+
['http://[::1]:8080/path', 'http://[::1]:8080/path', MATCHES_WITHOUT_PARAMS],
38+
[
39+
'http://[::1]:*/path',
40+
'http://[::1]:8080/path',
41+
MATCHES_WITH_PARAMS({ '0': '8080' }),
42+
],
43+
44+
/* IPv6 zone identifier (URL-encoded `%`) */
45+
[
46+
'http://[fe80::1%25eth0]/',
47+
'http://[fe80::1%25eth0]/',
48+
MATCHES_WITHOUT_PARAMS,
49+
],
50+
51+
/* Path parameters after an IPv6 host */
52+
[
53+
'http://[::1]/user/:id',
54+
new URL('http://[::1]/user/123'),
55+
MATCHES_WITH_PARAMS({ id: '123' }),
56+
],
57+
[
58+
'http://[::1]/:resource/:id',
59+
'http://[::1]/user/123',
60+
MATCHES_WITH_PARAMS({ resource: 'user', id: '123' }),
61+
],
62+
[
63+
'http://[2001:db8::1]/user/:id',
64+
'http://[2001:db8::1]/user/456',
65+
MATCHES_WITH_PARAMS({ id: '456' }),
66+
],
67+
68+
/* Wildcards alongside an IPv6 host */
69+
[
70+
'http://[::1]/*',
71+
new URL('http://[::1]/foo/bar'),
72+
MATCHES_WITH_PARAMS({ '0': 'foo/bar' }),
73+
],
74+
['*://[::1]/path', 'http://[::1]/path', MATCHES_WITH_PARAMS({ '0': 'http' })],
75+
['http://*/path', 'http://[::1]/path', MATCHES_WITH_PARAMS({ '0': '[::1]' })],
76+
77+
/* Mismatches */
78+
['http://[::1]/path', 'http://[::2]/path', NO_MATCH],
79+
['http://[::1]/path', 'http://[::1]/other', NO_MATCH],
80+
])('matches %j against %j', (pattern, input, expectedResult) => {
81+
expect(matchPattern(pattern, input)).toEqual(expectedResult)
82+
})

0 commit comments

Comments
 (0)