Skip to content

Commit

Permalink
Support constructor strings with ipv6 addresses. (#37)
Browse files Browse the repository at this point in the history
This commit includes change to address both:

whatwg/urlpattern#113
whatwg/urlpattern#115
  • Loading branch information
wanderview committed Jan 6, 2022
1 parent aa22a93 commit 7b77ab9
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 3 deletions.
21 changes: 20 additions & 1 deletion src/url-pattern-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export class Parser {
// The current nest depth of `{ }` pattern groupings.
private groupDepth: number = 0;

// The current nesting depth of `[ ]` in hostname patterns.
private hostnameIPv6BracketDepth: number = 0;

// True if we should apply parse rules as if this is a "standard" URL. If
// false then this is treated as a "not a base URL".
private shouldTreatAsStandardURL: boolean = false;
Expand Down Expand Up @@ -242,8 +245,16 @@ export class Parser {
break;

case State.HOSTNAME:
// Track whether we are inside ipv6 address brackets.
if (this.isIPv6Open()) {
this.hostnameIPv6BracketDepth += 1;
} else if (this.isIPv6Close()) {
this.hostnameIPv6BracketDepth -= 1;
}

// If we find a `:` then we transition to the port component state.
if (this.isPortPrefix()) {
// However, we ignore `:` when parsing an ipv6 address.
if (this.isPortPrefix() && !this.hostnameIPv6BracketDepth) {
this.changeState(State.PORT, /*skip=*/1);
}

Expand Down Expand Up @@ -468,6 +479,14 @@ export class Parser {
return this.tokenList[this.tokenIndex].type == 'CLOSE';
}

private isIPv6Open(): boolean {
return this.isNonSpecialPatternChar(this.tokenIndex, '[');
}

private isIPv6Close(): boolean {
return this.isNonSpecialPatternChar(this.tokenIndex, ']');
}

private makeComponentString(): string {
const token: LexToken = this.tokenList[this.tokenIndex];
const componentCharStart = this.safeToken(this.componentStart).index;
Expand Down
8 changes: 7 additions & 1 deletion src/url-pattern.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import {
canonicalizeSearch,
canonicalizeUsername,
defaultPortForProtocol,
treatAsIPv6Hostname,
isAbsolutePathname,
isSpecialScheme,
protocolEncodeCallback,
usernameEncodeCallback,
passwordEncodeCallback,
hostnameEncodeCallback,
ipv6HostnameEncodeCallback,
portEncodeCallback,
standardURLPathnameEncodeCallback,
pathURLPathnameEncodeCallback,
Expand Down Expand Up @@ -311,7 +313,11 @@ export class URLPattern {
break;
case 'hostname':
Object.assign(options, HOSTNAME_OPTIONS);
options.encodePart = hostnameEncodeCallback;
if (treatAsIPv6Hostname(pattern)) {
options.encodePart = ipv6HostnameEncodeCallback;
} else {
options.encodePart = hostnameEncodeCallback;
}
break;
case 'port':
Object.assign(options, DEFAULT_OPTIONS);
Expand Down
33 changes: 32 additions & 1 deletion src/url-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,23 @@ function maybeStripSuffix(value: string, suffix: string): string {
return value;
}

export function treatAsIPv6Hostname(value: string | undefined): boolean {
if (!value || value.length < 2) {
return false;
}

if (value[0] === '[') {
return true;
}

if ((value[0] === '\\' || value[0] === '{') &&
value[1] === '[') {
return true;
}

return false;
}

export const SPECIAL_SCHEMES = [
'ftp',
'file',
Expand Down Expand Up @@ -124,7 +141,11 @@ export function canonicalizeHostname(hostname: string, isPattern: boolean) {
if (isPattern || hostname === '') {
return hostname;
}
return hostnameEncodeCallback(hostname);
if (treatAsIPv6Hostname(hostname)) {
return ipv6HostnameEncodeCallback(hostname);
} else {
return hostnameEncodeCallback(hostname);
}
}

export function canonicalizePassword(password: string, isPattern: boolean) {
Expand Down Expand Up @@ -241,6 +262,16 @@ export function hostnameEncodeCallback(input: string): string {
return url.hostname;
}

export function ipv6HostnameEncodeCallback(input: string): string {
if (input === '') {
return input;
}
if (/[^0-9a-fA-F[\]:]/g.test(input)) {
throw(new TypeError(`Invalid IPv6 hostname '${input}'`));
}
return input.toLowerCase();
}

export function portEncodeCallback(input: string): string {
if (input === '') {
return input;
Expand Down
128 changes: 128 additions & 0 deletions urlpatterntestdata.json
Original file line number Diff line number Diff line change
Expand Up @@ -2085,6 +2085,134 @@
"pathname": { "input": "/data:channel.html", "groups": {} }
}
},
{
"pattern": [ "http://[\\:\\:1]/" ],
"inputs": [ "http://[::1]/" ],
"exactly_empty_components": [ "username", "password", "port", "search",
"hash" ],
"expected_obj": {
"protocol": "http",
"hostname": "[\\:\\:1]",
"pathname": "/"
},
"expected_match": {
"protocol": { "input": "http", "groups": {} },
"hostname": { "input": "[::1]", "groups": {} },
"pathname": { "input": "/", "groups": {} }
}
},
{
"pattern": [ "http://[\\:\\:1]:8080/" ],
"inputs": [ "http://[::1]:8080/" ],
"exactly_empty_components": [ "username", "password", "search", "hash" ],
"expected_obj": {
"protocol": "http",
"hostname": "[\\:\\:1]",
"port": "8080",
"pathname": "/"
},
"expected_match": {
"protocol": { "input": "http", "groups": {} },
"hostname": { "input": "[::1]", "groups": {} },
"port": { "input": "8080", "groups": {} },
"pathname": { "input": "/", "groups": {} }
}
},
{
"pattern": [ "http://[\\:\\:a]/" ],
"inputs": [ "http://[::a]/" ],
"exactly_empty_components": [ "username", "password", "port", "search",
"hash" ],
"expected_obj": {
"protocol": "http",
"hostname": "[\\:\\:a]",
"pathname": "/"
},
"expected_match": {
"protocol": { "input": "http", "groups": {} },
"hostname": { "input": "[::a]", "groups": {} },
"pathname": { "input": "/", "groups": {} }
}
},
{
"pattern": [ "http://[:address]/" ],
"inputs": [ "http://[::1]/" ],
"exactly_empty_components": [ "username", "password", "port", "search",
"hash" ],
"expected_obj": {
"protocol": "http",
"hostname": "[:address]",
"pathname": "/"
},
"expected_match": {
"protocol": { "input": "http", "groups": {} },
"hostname": { "input": "[::1]", "groups": { "address": "::1" }},
"pathname": { "input": "/", "groups": {} }
}
},
{
"pattern": [ "http://[\\:\\:AB\\::num]/" ],
"inputs": [ "http://[::ab:1]/" ],
"exactly_empty_components": [ "username", "password", "port", "search",
"hash" ],
"expected_obj": {
"protocol": "http",
"hostname": "[\\:\\:ab\\::num]",
"pathname": "/"
},
"expected_match": {
"protocol": { "input": "http", "groups": {} },
"hostname": { "input": "[::ab:1]", "groups": { "num": "1" }},
"pathname": { "input": "/", "groups": {} }
}
},
{
"pattern": [{ "hostname": "[\\:\\:AB\\::num]" }],
"inputs": [{ "hostname": "[::ab:1]" }],
"expected_obj": {
"hostname": "[\\:\\:ab\\::num]"
},
"expected_match": {
"hostname": { "input": "[::ab:1]", "groups": { "num": "1" }}
}
},
{
"pattern": [{ "hostname": "[\\:\\:xY\\::num]" }],
"expected_obj": "error"
},
{
"pattern": [{ "hostname": "{[\\:\\:ab\\::num]}" }],
"inputs": [{ "hostname": "[::ab:1]" }],
"expected_match": {
"hostname": { "input": "[::ab:1]", "groups": { "num": "1" }}
}
},
{
"pattern": [{ "hostname": "{[\\:\\:fé\\::num]}" }],
"expected_obj": "error"
},
{
"pattern": [{ "hostname": "{[\\:\\::num\\:1]}" }],
"inputs": [{ "hostname": "[::ab:1]" }],
"expected_match": {
"hostname": { "input": "[::ab:1]", "groups": { "num": "ab" }}
}
},
{
"pattern": [{ "hostname": "{[\\:\\::num\\:fé]}" }],
"expected_obj": "error"
},
{
"pattern": [{ "hostname": "[*\\:1]" }],
"inputs": [{ "hostname": "[::ab:1]" }],
"expected_match": {
"hostname": { "input": "[::ab:1]", "groups": { "0": "::ab" }}
}
},
{
"pattern": [{ "hostname": "*\\:1]" }],
"expected_obj": "error"
},
{
"pattern": [ "https://foo{{@}}example.com" ],
"inputs": [ "https://foo@example.com" ],
Expand Down

0 comments on commit 7b77ab9

Please sign in to comment.