From ae7fc692d1bd0defb5f30d59b503153508e9f168 Mon Sep 17 00:00:00 2001 From: Ben Kelly Date: Thu, 6 Jan 2022 21:16:37 +0000 Subject: [PATCH] Support constructor strings with ipv6 addresses. (#37) This commit includes change to address both: https://github.com/WICG/urlpattern/issues/113 https://github.com/WICG/urlpattern/issues/115 --- src/url-pattern-parser.ts | 21 ++++++- src/url-pattern.ts | 8 ++- src/url-utils.ts | 33 +++++++++- urlpatterntestdata.json | 128 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 187 insertions(+), 3 deletions(-) diff --git a/src/url-pattern-parser.ts b/src/url-pattern-parser.ts index b5b1f41..48f33c2 100644 --- a/src/url-pattern-parser.ts +++ b/src/url-pattern-parser.ts @@ -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; @@ -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); } @@ -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; diff --git a/src/url-pattern.ts b/src/url-pattern.ts index 8c904c4..e55918f 100644 --- a/src/url-pattern.ts +++ b/src/url-pattern.ts @@ -13,12 +13,14 @@ import { canonicalizeSearch, canonicalizeUsername, defaultPortForProtocol, + treatAsIPv6Hostname, isAbsolutePathname, isSpecialScheme, protocolEncodeCallback, usernameEncodeCallback, passwordEncodeCallback, hostnameEncodeCallback, + ipv6HostnameEncodeCallback, portEncodeCallback, standardURLPathnameEncodeCallback, pathURLPathnameEncodeCallback, @@ -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); diff --git a/src/url-utils.ts b/src/url-utils.ts index ee67e3a..887481f 100644 --- a/src/url-utils.ts +++ b/src/url-utils.ts @@ -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', @@ -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) { @@ -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; diff --git a/urlpatterntestdata.json b/urlpatterntestdata.json index 294b223..7c81c7f 100644 --- a/urlpatterntestdata.json +++ b/urlpatterntestdata.json @@ -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" ],