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
26 changes: 24 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const QUESTION_MARK = 63
const PLUS = 43
const BACKSLASH = 92
const UNDERSCORE = 95
const OPEN_BRACKET = 91
const CLOSE_BRACKET = 93

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

while (i < length) {
const code = pattern.charCodeAt(i)
Expand All @@ -103,7 +111,7 @@ function parsePattern(pattern: string): Array<Token> {
// Escaped character — consume the backslash and the next character
// as a literal. Fall through to the literal branch below by not
// advancing `i` here (the literal branch handles it).
} else if (code === ASTERISK) {
} else if (code === ASTERISK && !inBrackets) {
tokens.push({
type: TokenType.Wildcard,
nextLiteral: undefined,
Expand All @@ -112,6 +120,7 @@ function parsePattern(pattern: string): Array<Token> {
continue
} else if (
code === COLON &&
!inBrackets &&
i + 1 < length &&
isIdentStartCode(pattern.charCodeAt(i + 1))
) {
Expand Down Expand Up @@ -157,12 +166,25 @@ function parsePattern(pattern: string): Array<Token> {
continue
}

if (charCode === ASTERISK) {
if (charCode === OPEN_BRACKET) {
inBrackets = true
i++
continue
}

if (charCode === CLOSE_BRACKET) {
inBrackets = false
i++
continue
}

if (charCode === ASTERISK && !inBrackets) {
break
}

if (
charCode === COLON &&
!inBrackets &&
i + 1 < length &&
isIdentStartCode(pattern.charCodeAt(i + 1))
) {
Expand Down
82 changes: 82 additions & 0 deletions tests/ipv6.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { matchPattern, MatchResult } from '#src/index.js'
import {
NO_MATCH,
MATCHES_WITH_PARAMS,
MATCHES_WITHOUT_PARAMS,
} from '#tests/utils.js'

it.each<
[
Parameters<typeof matchPattern>[0],
Parameters<typeof matchPattern>[1],
MatchResult,
]
>([
/* Literal IPv6 addresses */
['http://[::1]/', 'http://[::1]/', MATCHES_WITHOUT_PARAMS],
['http://[::1]/', new URL('http://[::1]/'), MATCHES_WITHOUT_PARAMS],
['http://[::1]', new URL('http://[::1]'), MATCHES_WITHOUT_PARAMS],
[
'http://[2001:db8::1]/path',
'http://[2001:db8::1]/path',
MATCHES_WITHOUT_PARAMS,
],
[
'http://[2001:db8::a1]/x',
'http://[2001:db8::a1]/x',
MATCHES_WITHOUT_PARAMS,
],
['http://[fe80::abcd]/', 'http://[fe80::abcd]/', MATCHES_WITHOUT_PARAMS],
[
'http://[2001:db8:85a3::8a2e:370:7334]/',
'http://[2001:db8:85a3::8a2e:370:7334]/',
MATCHES_WITHOUT_PARAMS,
],

/* IPv6 with port */
['http://[::1]:8080/path', 'http://[::1]:8080/path', MATCHES_WITHOUT_PARAMS],
[
'http://[::1]:*/path',
'http://[::1]:8080/path',
MATCHES_WITH_PARAMS({ '0': '8080' }),
],

/* IPv6 zone identifier (URL-encoded `%`) */
[
'http://[fe80::1%25eth0]/',
'http://[fe80::1%25eth0]/',
MATCHES_WITHOUT_PARAMS,
],

/* Path parameters after an IPv6 host */
[
'http://[::1]/user/:id',
new URL('http://[::1]/user/123'),
MATCHES_WITH_PARAMS({ id: '123' }),
],
[
'http://[::1]/:resource/:id',
'http://[::1]/user/123',
MATCHES_WITH_PARAMS({ resource: 'user', id: '123' }),
],
[
'http://[2001:db8::1]/user/:id',
'http://[2001:db8::1]/user/456',
MATCHES_WITH_PARAMS({ id: '456' }),
],

/* Wildcards alongside an IPv6 host */
[
'http://[::1]/*',
new URL('http://[::1]/foo/bar'),
MATCHES_WITH_PARAMS({ '0': 'foo/bar' }),
],
['*://[::1]/path', 'http://[::1]/path', MATCHES_WITH_PARAMS({ '0': 'http' })],
['http://*/path', 'http://[::1]/path', MATCHES_WITH_PARAMS({ '0': '[::1]' })],

/* Mismatches */
['http://[::1]/path', 'http://[::2]/path', NO_MATCH],
['http://[::1]/path', 'http://[::1]/other', NO_MATCH],
])('matches %j against %j', (pattern, input, expectedResult) => {
expect(matchPattern(pattern, input)).toEqual(expectedResult)
})
Loading