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-utils.ts b/src/url-utils.ts index ee67e3a..ed379d3 100644 --- a/src/url-utils.ts +++ b/src/url-utils.ts @@ -233,7 +233,7 @@ export function hostnameEncodeCallback(input: string): string { if (input === '') { return input; } - if (/[#%/:<>?@[\]\\|]/g.test(input)) { + if (input[0] !== '[' && /[#%/:<>?@[\]\\|]/g.test(input)) { throw(new TypeError(`Invalid hostname '${input}'`)); } const url = new URL('https://example.com'); diff --git a/urlpatterntestdata.json b/urlpatterntestdata.json index 294b223..5a3eae5 100644 --- a/urlpatterntestdata.json +++ b/urlpatterntestdata.json @@ -2085,6 +2085,55 @@ "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": [ "https://foo{{@}}example.com" ], "inputs": [ "https://foo@example.com" ],