From 3639d0c026a770b40129dbfc6738ec8502428719 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Mon, 27 Nov 2017 20:53:16 -0800 Subject: [PATCH] url: adding WHATWG URL support Implements WHATWG URL support. Example: ``` var u = new url.URL('http://example.org'); ``` Many, many other commits improving the implementation have been squashed into this backport PR. They are not listed separately here for brevity. Backport-PR-URL: https://github.com/nodejs/node/pull/17365 PR-URL: https://github.com/nodejs/node/pull/7448 Reviewed-By: Ilkka Myller --- .../url/legacy-vs-whatwg-url-get-prop.js | 97 + benchmark/url/legacy-vs-whatwg-url-parse.js | 57 + ...legacy-vs-whatwg-url-searchparams-parse.js | 51 + ...cy-vs-whatwg-url-searchparams-serialize.js | 53 + .../url/legacy-vs-whatwg-url-serialize.js | 59 + benchmark/url/url-searchparams-iteration.js | 61 + benchmark/url/url-searchparams-read.js | 58 + benchmark/url/url-searchparams-sort.js | 48 + benchmark/url/usvstring.js | 28 + benchmark/url/whatwg-url-idna.js | 45 + benchmark/url/whatwg-url-properties.js | 75 + doc/api/url.md | 937 ++- lib/internal/bootstrap_node.js | 4 + lib/internal/querystring.js | 29 + lib/internal/url.js | 1428 ++++ lib/internal/util.js | 15 + lib/querystring.js | 5 +- lib/url.js | 34 +- lib/util.js | 20 +- node.gyp | 5 + src/env.h | 1 + src/node_url.cc | 2223 ++++++ src/node_url.h | 191 + test/cctest/test_url.cc | 106 + test/fixtures/url-searchparams.js | 77 + test/fixtures/url-setter-tests-additional.js | 237 + test/fixtures/url-setter-tests.js | 1823 +++++ test/fixtures/url-tests-additional.js | 36 + test/fixtures/url-tests.js | 6565 +++++++++++++++++ .../parallel/test-url-domain-ascii-unicode.js | 31 + test/parallel/test-whatwg-url-constructor.js | 143 + test/parallel/test-whatwg-url-domainto.js | 51 + test/parallel/test-whatwg-url-historical.js | 46 + test/parallel/test-whatwg-url-inspect.js | 66 + test/parallel/test-whatwg-url-origin.js | 52 + test/parallel/test-whatwg-url-parsing.js | 62 + test/parallel/test-whatwg-url-properties.js | 162 + .../test-whatwg-url-searchparams-append.js | 73 + ...est-whatwg-url-searchparams-constructor.js | 248 + .../test-whatwg-url-searchparams-delete.js | 92 + .../test-whatwg-url-searchparams-entries.js | 34 + .../test-whatwg-url-searchparams-foreach.js | 56 + .../test-whatwg-url-searchparams-get.js | 55 + .../test-whatwg-url-searchparams-getall.js | 60 + .../test-whatwg-url-searchparams-has.js | 58 + .../test-whatwg-url-searchparams-inspect.js | 29 + .../test-whatwg-url-searchparams-keys.js | 35 + .../test-whatwg-url-searchparams-set.js | 59 + .../test-whatwg-url-searchparams-sort.js | 105 + ...est-whatwg-url-searchparams-stringifier.js | 132 + .../test-whatwg-url-searchparams-values.js | 35 + test/parallel/test-whatwg-url-searchparams.js | 106 + test/parallel/test-whatwg-url-setters.js | 126 + test/parallel/test-whatwg-url-toascii.js | 85 + test/parallel/test-whatwg-url-tojson.js | 17 + test/parallel/test-whatwg-url-tostringtag.js | 32 + tools/doc/type-parser.js | 2 + 57 files changed, 16345 insertions(+), 75 deletions(-) create mode 100644 benchmark/url/legacy-vs-whatwg-url-get-prop.js create mode 100644 benchmark/url/legacy-vs-whatwg-url-parse.js create mode 100644 benchmark/url/legacy-vs-whatwg-url-searchparams-parse.js create mode 100644 benchmark/url/legacy-vs-whatwg-url-searchparams-serialize.js create mode 100644 benchmark/url/legacy-vs-whatwg-url-serialize.js create mode 100644 benchmark/url/url-searchparams-iteration.js create mode 100644 benchmark/url/url-searchparams-read.js create mode 100644 benchmark/url/url-searchparams-sort.js create mode 100644 benchmark/url/usvstring.js create mode 100644 benchmark/url/whatwg-url-idna.js create mode 100644 benchmark/url/whatwg-url-properties.js create mode 100644 lib/internal/querystring.js create mode 100644 lib/internal/url.js create mode 100644 src/node_url.cc create mode 100644 src/node_url.h create mode 100644 test/cctest/test_url.cc create mode 100644 test/fixtures/url-searchparams.js create mode 100644 test/fixtures/url-setter-tests-additional.js create mode 100644 test/fixtures/url-setter-tests.js create mode 100644 test/fixtures/url-tests-additional.js create mode 100644 test/fixtures/url-tests.js create mode 100644 test/parallel/test-url-domain-ascii-unicode.js create mode 100644 test/parallel/test-whatwg-url-constructor.js create mode 100644 test/parallel/test-whatwg-url-domainto.js create mode 100644 test/parallel/test-whatwg-url-historical.js create mode 100644 test/parallel/test-whatwg-url-inspect.js create mode 100644 test/parallel/test-whatwg-url-origin.js create mode 100644 test/parallel/test-whatwg-url-parsing.js create mode 100644 test/parallel/test-whatwg-url-properties.js create mode 100644 test/parallel/test-whatwg-url-searchparams-append.js create mode 100644 test/parallel/test-whatwg-url-searchparams-constructor.js create mode 100644 test/parallel/test-whatwg-url-searchparams-delete.js create mode 100644 test/parallel/test-whatwg-url-searchparams-entries.js create mode 100644 test/parallel/test-whatwg-url-searchparams-foreach.js create mode 100644 test/parallel/test-whatwg-url-searchparams-get.js create mode 100644 test/parallel/test-whatwg-url-searchparams-getall.js create mode 100644 test/parallel/test-whatwg-url-searchparams-has.js create mode 100644 test/parallel/test-whatwg-url-searchparams-inspect.js create mode 100644 test/parallel/test-whatwg-url-searchparams-keys.js create mode 100644 test/parallel/test-whatwg-url-searchparams-set.js create mode 100644 test/parallel/test-whatwg-url-searchparams-sort.js create mode 100644 test/parallel/test-whatwg-url-searchparams-stringifier.js create mode 100644 test/parallel/test-whatwg-url-searchparams-values.js create mode 100644 test/parallel/test-whatwg-url-searchparams.js create mode 100644 test/parallel/test-whatwg-url-setters.js create mode 100644 test/parallel/test-whatwg-url-toascii.js create mode 100644 test/parallel/test-whatwg-url-tojson.js create mode 100644 test/parallel/test-whatwg-url-tostringtag.js diff --git a/benchmark/url/legacy-vs-whatwg-url-get-prop.js b/benchmark/url/legacy-vs-whatwg-url-get-prop.js new file mode 100644 index 00000000000000..229a4e60652b64 --- /dev/null +++ b/benchmark/url/legacy-vs-whatwg-url-get-prop.js @@ -0,0 +1,97 @@ +'use strict'; +const common = require('../common.js'); +const url = require('url'); +const URL = url.URL; +const assert = require('assert'); +const inputs = require('../fixtures/url-inputs.js').urls; + +const bench = common.createBenchmark(main, { + type: Object.keys(inputs), + method: ['legacy', 'whatwg'], + n: [1e5] +}); + +// At the time of writing, when using a passed property name to index +// the object, Crankshaft would generate a LoadKeyedGeneric even when it +// remains a constant in the function, so here we must use the literal +// instead to get a LoadNamedField. +function useLegacy(n, input) { + const obj = url.parse(input); + const noDead = { + protocol: obj.protocol, + auth: obj.auth, + host: obj.host, + hostname: obj.hostname, + port: obj.port, + pathname: obj.pathname, + search: obj.search, + hash: obj.hash + }; + // It's necessary to assign the values to an object + // to avoid loop invariant code motion. + bench.start(); + for (var i = 0; i < n; i += 1) { + noDead.protocol = obj.protocol; + noDead.auth = obj.auth; + noDead.host = obj.host; + noDead.hostname = obj.hostname; + noDead.port = obj.port; + noDead.pathname = obj.pathname; + noDead.search = obj.search; + noDead.hash = obj.hash; + } + bench.end(n); + return noDead; +} + +function useWHATWG(n, input) { + const obj = new URL(input); + const noDead = { + protocol: obj.protocol, + auth: `${obj.username}:${obj.password}`, + host: obj.host, + hostname: obj.hostname, + port: obj.port, + pathname: obj.pathname, + search: obj.search, + hash: obj.hash + }; + bench.start(); + for (var i = 0; i < n; i += 1) { + noDead.protocol = obj.protocol; + noDead.auth = `${obj.username}:${obj.password}`; + noDead.host = obj.host; + noDead.hostname = obj.hostname; + noDead.port = obj.port; + noDead.pathname = obj.pathname; + noDead.search = obj.search; + noDead.hash = obj.hash; + } + bench.end(n); + return noDead; +} + +function main(conf) { + const type = conf.type; + const n = conf.n | 0; + const method = conf.method; + + const input = inputs[type]; + if (!input) { + throw new Error('Unknown input type'); + } + + var noDead; // Avoid dead code elimination. + switch (method) { + case 'legacy': + noDead = useLegacy(n, input); + break; + case 'whatwg': + noDead = useWHATWG(n, input); + break; + default: + throw new Error('Unknown method'); + } + + assert.ok(noDead); +} diff --git a/benchmark/url/legacy-vs-whatwg-url-parse.js b/benchmark/url/legacy-vs-whatwg-url-parse.js new file mode 100644 index 00000000000000..ec386b7b85597d --- /dev/null +++ b/benchmark/url/legacy-vs-whatwg-url-parse.js @@ -0,0 +1,57 @@ +'use strict'; +const common = require('../common.js'); +const url = require('url'); +const URL = url.URL; +const assert = require('assert'); +const inputs = require('../fixtures/url-inputs.js').urls; + +const bench = common.createBenchmark(main, { + type: Object.keys(inputs), + method: ['legacy', 'whatwg'], + n: [1e5] +}); + +function useLegacy(n, input) { + var noDead = url.parse(input); + bench.start(); + for (var i = 0; i < n; i += 1) { + noDead = url.parse(input); + } + bench.end(n); + return noDead; +} + +function useWHATWG(n, input) { + var noDead = new URL(input); + bench.start(); + for (var i = 0; i < n; i += 1) { + noDead = new URL(input); + } + bench.end(n); + return noDead; +} + +function main(conf) { + const type = conf.type; + const n = conf.n | 0; + const method = conf.method; + + const input = inputs[type]; + if (!input) { + throw new Error('Unknown input type'); + } + + var noDead; // Avoid dead code elimination. + switch (method) { + case 'legacy': + noDead = useLegacy(n, input); + break; + case 'whatwg': + noDead = useWHATWG(n, input); + break; + default: + throw new Error('Unknown method'); + } + + assert.ok(noDead); +} diff --git a/benchmark/url/legacy-vs-whatwg-url-searchparams-parse.js b/benchmark/url/legacy-vs-whatwg-url-searchparams-parse.js new file mode 100644 index 00000000000000..b4a80af4e5eabd --- /dev/null +++ b/benchmark/url/legacy-vs-whatwg-url-searchparams-parse.js @@ -0,0 +1,51 @@ +'use strict'; +const common = require('../common.js'); +const { URLSearchParams } = require('url'); +const querystring = require('querystring'); +const inputs = require('../fixtures/url-inputs.js').searchParams; + +const bench = common.createBenchmark(main, { + type: Object.keys(inputs), + method: ['legacy', 'whatwg'], + n: [1e6] +}); + +function useLegacy(n, input) { + querystring.parse(input); + bench.start(); + for (var i = 0; i < n; i += 1) { + querystring.parse(input); + } + bench.end(n); +} + +function useWHATWG(n, input) { + new URLSearchParams(input); + bench.start(); + for (var i = 0; i < n; i += 1) { + new URLSearchParams(input); + } + bench.end(n); +} + +function main(conf) { + const type = conf.type; + const n = conf.n | 0; + const method = conf.method; + + const input = inputs[type]; + if (!input) { + throw new Error('Unknown input type'); + } + + switch (method) { + case 'legacy': + useLegacy(n, input); + break; + case 'whatwg': + useWHATWG(n, input); + break; + default: + throw new Error('Unknown method'); + } +} diff --git a/benchmark/url/legacy-vs-whatwg-url-searchparams-serialize.js b/benchmark/url/legacy-vs-whatwg-url-searchparams-serialize.js new file mode 100644 index 00000000000000..2b8d2c36a810b3 --- /dev/null +++ b/benchmark/url/legacy-vs-whatwg-url-searchparams-serialize.js @@ -0,0 +1,53 @@ +'use strict'; +const common = require('../common.js'); +const { URLSearchParams } = require('url'); +const querystring = require('querystring'); +const inputs = require('../fixtures/url-inputs.js').searchParams; + +const bench = common.createBenchmark(main, { + type: Object.keys(inputs), + method: ['legacy', 'whatwg'], + n: [1e6] +}); + +function useLegacy(n, input, prop) { + const obj = querystring.parse(input); + querystring.stringify(obj); + bench.start(); + for (var i = 0; i < n; i += 1) { + querystring.stringify(obj); + } + bench.end(n); +} + +function useWHATWG(n, input, prop) { + const obj = new URLSearchParams(input); + obj.toString(); + bench.start(); + for (var i = 0; i < n; i += 1) { + obj.toString(); + } + bench.end(n); +} + +function main(conf) { + const type = conf.type; + const n = conf.n | 0; + const method = conf.method; + + const input = inputs[type]; + if (!input) { + throw new Error('Unknown input type'); + } + + switch (method) { + case 'legacy': + useLegacy(n, input); + break; + case 'whatwg': + useWHATWG(n, input); + break; + default: + throw new Error('Unknown method'); + } +} diff --git a/benchmark/url/legacy-vs-whatwg-url-serialize.js b/benchmark/url/legacy-vs-whatwg-url-serialize.js new file mode 100644 index 00000000000000..35b459a10c0e0b --- /dev/null +++ b/benchmark/url/legacy-vs-whatwg-url-serialize.js @@ -0,0 +1,59 @@ +'use strict'; +const common = require('../common.js'); +const url = require('url'); +const URL = url.URL; +const assert = require('assert'); +const inputs = require('../fixtures/url-inputs.js').urls; + +const bench = common.createBenchmark(main, { + type: Object.keys(inputs), + method: ['legacy', 'whatwg'], + n: [1e5] +}); + +function useLegacy(n, input, prop) { + const obj = url.parse(input); + var noDead = url.format(obj); + bench.start(); + for (var i = 0; i < n; i += 1) { + noDead = url.format(obj); + } + bench.end(n); + return noDead; +} + +function useWHATWG(n, input, prop) { + const obj = new URL(input); + var noDead = obj.toString(); + bench.start(); + for (var i = 0; i < n; i += 1) { + noDead = obj.toString(); + } + bench.end(n); + return noDead; +} + +function main(conf) { + const type = conf.type; + const n = conf.n | 0; + const method = conf.method; + + const input = inputs[type]; + if (!input) { + throw new Error('Unknown input type'); + } + + var noDead; // Avoid dead code elimination. + switch (method) { + case 'legacy': + noDead = useLegacy(n, input); + break; + case 'whatwg': + noDead = useWHATWG(n, input); + break; + default: + throw new Error('Unknown method'); + } + + assert.ok(noDead); +} diff --git a/benchmark/url/url-searchparams-iteration.js b/benchmark/url/url-searchparams-iteration.js new file mode 100644 index 00000000000000..0f4b71a0a183dd --- /dev/null +++ b/benchmark/url/url-searchparams-iteration.js @@ -0,0 +1,61 @@ +'use strict'; +const common = require('../common.js'); +const assert = require('assert'); +const { URLSearchParams } = require('url'); + +const bench = common.createBenchmark(main, { + method: ['forEach', 'iterator'], + n: [1e6] +}); + +const str = 'one=single&two=first&three=first&two=2nd&three=2nd&three=3rd'; + +function forEach(n) { + const params = new URLSearchParams(str); + const noDead = []; + const cb = (val, key) => { + noDead[0] = key; + noDead[1] = val; + }; + + bench.start(); + for (var i = 0; i < n; i += 1) + params.forEach(cb); + bench.end(n); + + assert.strictEqual(noDead[0], 'three'); + assert.strictEqual(noDead[1], '3rd'); +} + +function iterator(n) { + const params = new URLSearchParams(str); + const noDead = []; + + bench.start(); + for (var i = 0; i < n; i += 1) { + for (const pair of params) { + noDead[0] = pair[0]; + noDead[1] = pair[1]; + } + } + bench.end(n); + + assert.strictEqual(noDead[0], 'three'); + assert.strictEqual(noDead[1], '3rd'); +} + +function main(conf) { + const method = conf.method; + const n = conf.n | 0; + + switch (method) { + case 'forEach': + forEach(n); + break; + case 'iterator': + iterator(n); + break; + default: + throw new Error('Unknown method'); + } +} diff --git a/benchmark/url/url-searchparams-read.js b/benchmark/url/url-searchparams-read.js new file mode 100644 index 00000000000000..762ffcca03d69d --- /dev/null +++ b/benchmark/url/url-searchparams-read.js @@ -0,0 +1,58 @@ +'use strict'; +const common = require('../common.js'); +const { URLSearchParams } = require('url'); + +const bench = common.createBenchmark(main, { + method: ['get', 'getAll', 'has'], + param: ['one', 'two', 'three', 'nonexistent'], + n: [2e7] +}); + +const str = 'one=single&two=first&three=first&two=2nd&three=2nd&three=3rd'; + +function get(n, param) { + const params = new URLSearchParams(str); + + bench.start(); + for (var i = 0; i < n; i += 1) + params.get(param); + bench.end(n); +} + +function getAll(n, param) { + const params = new URLSearchParams(str); + + bench.start(); + for (var i = 0; i < n; i += 1) + params.getAll(param); + bench.end(n); +} + +function has(n, param) { + const params = new URLSearchParams(str); + + bench.start(); + for (var i = 0; i < n; i += 1) + params.has(param); + bench.end(n); +} + +function main(conf) { + const method = conf.method; + const param = conf.param; + const n = conf.n | 0; + + switch (method) { + case 'get': + get(n, param); + break; + case 'getAll': + getAll(n, param); + break; + case 'has': + has(n, param); + break; + default: + throw new Error('Unknown method'); + } +} diff --git a/benchmark/url/url-searchparams-sort.js b/benchmark/url/url-searchparams-sort.js new file mode 100644 index 00000000000000..677ce511cf3ea2 --- /dev/null +++ b/benchmark/url/url-searchparams-sort.js @@ -0,0 +1,48 @@ +'use strict'; +const common = require('../common.js'); +const URLSearchParams = require('url').URLSearchParams; + +const inputs = { + empty: '', + sorted: 'a&b&c&d&e&f&g&h&i&j&k&l&m&n&o&p&q&r&s&t&u&v&w&x&y&z', + almostsorted: 'a&b&c&d&e&f&g&i&h&j&k&l&m&n&o&p&q&r&s&t&u&w&v&x&y&z', + reversed: 'z&y&x&w&v&u&t&s&r&q&p&o&n&m&l&k&j&i&h&g&f&e&d&c&b&a', + random: 'm&t&d&c&z&v&a&n&p&y&u&o&h&l&f&j&e&q&b&i&s&x&k&w&r&g', + // 8 parameters + short: 'm&t&d&c&z&v&a&n', + // 88 parameters + long: 'g&r&t&h&s&r&d&w&b&n&h&k&x&m&k&h&o&e&x&c&c&g&e&b&p&p&s&n&j&b&y&z&' + + 'u&l&o&r&w&a&u&l&m&f&j&q&p&f&e&y&e&n&e&l&m&w&u&w&t&n&t&q&v&y&c&o&' + + 'k&f&j&i&l&m&g&j&d&i&z&q&p&x&q&q&d&n&y&w&g&i&v&r' +}; + +function getParams(str) { + const out = []; + for (const key of str.split('&')) { + out.push(key, ''); + } + return out; +} + +const bench = common.createBenchmark(main, { + type: Object.keys(inputs), + n: [1e6] +}, { + flags: ['--expose-internals'] +}); + +function main(conf) { + const searchParams = require('internal/url').searchParamsSymbol; + const input = inputs[conf.type]; + const n = conf.n | 0; + const params = new URLSearchParams(); + const array = getParams(input); + + var i; + bench.start(); + for (i = 0; i < n; i++) { + params[searchParams] = array.slice(); + params.sort(); + } + bench.end(n); +} diff --git a/benchmark/url/usvstring.js b/benchmark/url/usvstring.js new file mode 100644 index 00000000000000..40a945037385cf --- /dev/null +++ b/benchmark/url/usvstring.js @@ -0,0 +1,28 @@ +'use strict'; +const common = require('../common.js'); + +const inputs = { + valid: 'adsfadsfadsf', + validsurr: '\uda23\ude23\uda1f\udfaa\ud800\udfff\uda23\ude23\uda1f\udfaa' + + '\ud800\udfff', + someinvalid: 'asasfdfasd\uda23', + allinvalid: '\udc45\uda23 \udf00\udc00 \udfaa\uda12 \udc00\udfaa', + nonstring: { toString() { return 'asdf'; } } +}; +const bench = common.createBenchmark(main, { + input: Object.keys(inputs), + n: [5e7] +}, { + flags: ['--expose-internals'] +}); + +function main(conf) { + const { toUSVString } = require('internal/url'); + const str = inputs[conf.input]; + const n = conf.n | 0; + + bench.start(); + for (var i = 0; i < n; i++) + toUSVString(str); + bench.end(n); +} diff --git a/benchmark/url/whatwg-url-idna.js b/benchmark/url/whatwg-url-idna.js new file mode 100644 index 00000000000000..3d0ea3dc8fe516 --- /dev/null +++ b/benchmark/url/whatwg-url-idna.js @@ -0,0 +1,45 @@ +'use strict'; +const common = require('../common.js'); +const { domainToASCII, domainToUnicode } = require('url'); + +const inputs = { + empty: { + ascii: '', + unicode: '' + }, + none: { + ascii: 'passports', + unicode: 'passports' + }, + some: { + ascii: 'Paßstraße', + unicode: 'xn--Pastrae-1vae' + }, + all: { + ascii: '他们不说中文', + unicode: 'xn--ihqwczyycu19kkg2c' + }, + nonstring: { + ascii: { toString() { return ''; } }, + unicode: { toString() { return ''; } } + } +}; + +const bench = common.createBenchmark(main, { + input: Object.keys(inputs), + to: ['ascii', 'unicode'], + n: [5e6] +}); + +function main(conf) { + const n = conf.n | 0; + const to = conf.to; + const input = inputs[conf.input][to]; + const method = to === 'ascii' ? domainToASCII : domainToUnicode; + + bench.start(); + for (var i = 0; i < n; i++) { + method(input); + } + bench.end(n); +} diff --git a/benchmark/url/whatwg-url-properties.js b/benchmark/url/whatwg-url-properties.js new file mode 100644 index 00000000000000..3a865d2335ab3c --- /dev/null +++ b/benchmark/url/whatwg-url-properties.js @@ -0,0 +1,75 @@ +'use strict'; +const common = require('../common.js'); +const URL = require('url').URL; +const inputs = require('../fixtures/url-inputs.js').urls; + +const bench = common.createBenchmark(main, { + input: Object.keys(inputs), + prop: ['href', 'origin', 'protocol', + 'username', 'password', 'host', 'hostname', 'port', + 'pathname', 'search', 'searchParams', 'hash'], + n: [3e5] +}); + +function setAndGet(n, url, prop, alternative) { + const old = url[prop]; + bench.start(); + for (var i = 0; i < n; i += 1) { + url[prop] = n % 2 === 0 ? alternative : old; // set + url[prop]; // get + } + bench.end(n); +} + +function get(n, url, prop) { + bench.start(); + for (var i = 0; i < n; i += 1) { + url[prop]; // get + } + bench.end(n); +} + +const alternatives = { + href: 'http://user:pass@foo.bar.com:21/aaa/zzz?l=25#test', + protocol: 'https:', + username: 'user2', + password: 'pass2', + host: 'foo.bar.net:22', + hostname: 'foo.bar.org', + port: '23', + pathname: '/aaa/bbb', + search: '?k=99', + hash: '#abcd' +}; + +function getAlternative(prop) { + return alternatives[prop]; +} + +function main(conf) { + const n = conf.n | 0; + const input = inputs[conf.input]; + const url = new URL(input); + const prop = conf.prop; + + switch (prop) { + case 'protocol': + case 'username': + case 'password': + case 'host': + case 'hostname': + case 'port': + case 'pathname': + case 'search': + case 'hash': + case 'href': + setAndGet(n, url, prop, getAlternative(prop)); + break; + case 'origin': + case 'searchParams': + get(n, url, prop); + break; + default: + throw new Error('Unknown prop'); + } +} diff --git a/doc/api/url.md b/doc/api/url.md index efb0a8103ccafe..ee7f75c9032641 100644 --- a/doc/api/url.md +++ b/doc/api/url.md @@ -17,52 +17,803 @@ A URL string is a structured string containing multiple meaningful components. When parsed, a URL object is returned containing properties for each of these components. -The following details each of the components of a parsed URL. The example -`'http://user:pass@host.com:8080/p/a/t/h?query=string#hash'` is used to -illustrate each. +The `url` module provides two APIs for working with URLs: a legacy API that is +Node.js specific, and a newer API that implements the same +[WHATWG URL Standard][] used by web browsers. + +*Note*: While the Legacy API has not been deprecated, it is maintained solely +for backwards compatibility with existing applications. New application code +should use the WHATWG API. + +A comparison between the WHATWG and Legacy APIs is provided below. Above the URL +`'http://user:pass@sub.host.com:8080/p/a/t/h?query=string#hash'`, properties of +an object returned by the legacy `url.parse()` are shown. Below it are +properties of a WHATWG `URL` object. + +*Note*: WHATWG URL's `origin` property includes `protocol` and `host`, but not +`username` or `password`. ```txt -┌─────────────────────────────────────────────────────────────────────────────┐ -│ href │ -├──────────┬┬───────────┬─────────────────┬───────────────────────────┬───────┤ -│ protocol ││ auth │ host │ path │ hash │ -│ ││ ├──────────┬──────┼──────────┬────────────────┤ │ -│ ││ │ hostname │ port │ pathname │ search │ │ -│ ││ │ │ │ ├─┬──────────────┤ │ -│ ││ │ │ │ │ │ query │ │ -" http: // user:pass @ host.com : 8080 /p/a/t/h ? query=string #hash " -│ ││ │ │ │ │ │ │ │ -└──────────┴┴───────────┴──────────┴──────┴──────────┴─┴──────────────┴───────┘ +┌─────────────────────────────────────────────────────────────────────────────────────────────┐ +│ href │ +├──────────┬──┬─────────────────────┬─────────────────────┬───────────────────────────┬───────┤ +│ protocol │ │ auth │ host │ path │ hash │ +│ │ │ ├──────────────┬──────┼──────────┬────────────────┤ │ +│ │ │ │ hostname │ port │ pathname │ search │ │ +│ │ │ │ │ │ ├─┬──────────────┤ │ +│ │ │ │ │ │ │ │ query │ │ +" https: // user : pass @ sub.host.com : 8080 /p/a/t/h ? query=string #hash " +│ │ │ │ │ hostname │ port │ │ │ │ +│ │ │ │ ├──────────────┴──────┤ │ │ │ +│ protocol │ │ username │ password │ host │ │ │ │ +├──────────┴──┼──────────┴──────────┼─────────────────────┤ │ │ │ +│ origin │ │ origin │ pathname │ search │ hash │ +├─────────────┴─────────────────────┴─────────────────────┴──────────┴────────────────┴───────┤ +│ href │ +└─────────────────────────────────────────────────────────────────────────────────────────────┘ (all spaces in the "" line should be ignored -- they are purely for formatting) ``` -### urlObject.href +Parsing the URL string using the WHATWG API: -The `href` property is the full URL string that was parsed with both the -`protocol` and `host` components converted to lower-case. +```js +const { URL } = require('url'); +const myURL = + new URL('https://user:pass@sub.host.com:8080/p/a/t/h?query=string#hash'); +``` -For example: `'http://user:pass@host.com:8080/p/a/t/h?query=string#hash'` +*Note*: In Web Browsers, the WHATWG `URL` class is a global that is always +available. In Node.js, however, the `URL` class must be accessed via +`require('url').URL`. -### urlObject.protocol +Parsing the URL string using the Legacy API: + +```js +const url = require('url'); +const myURL = + url.parse('https://user:pass@sub.host.com:8080/p/a/t/h?query=string#hash'); +``` -The `protocol` property identifies the URL's lower-cased protocol scheme. +## The WHATWG URL API + -For example: `'http:'` +### Class: URL -### urlObject.slashes +Browser-compatible `URL` class, implemented by following the WHATWG URL +Standard. [Examples of parsed URLs][] may be found in the Standard itself. -The `slashes` property is a `boolean` with a value of `true` if two ASCII -forward-slash characters (`/`) are required following the colon in the -`protocol`. +*Note*: In accordance with browser conventions, all properties of `URL` objects +are implemented as getters and setters on the class prototype, rather than as +data properties on the object itself. Thus, unlike [legacy urlObject][]s, using +the `delete` keyword on any properties of `URL` objects (e.g. `delete +myURL.protocol`, `delete myURL.pathname`, etc) has no effect but will still +return `true`. -### urlObject.host +#### Constructor: new URL(input[, base]) -The `host` property is the full lower-cased host portion of the URL, including -the `port` if specified. +* `input` {string} The input URL to parse +* `base` {string|URL} The base URL to resolve against if the `input` is not + absolute. + +Creates a new `URL` object by parsing the `input` relative to the `base`. If +`base` is passed as a string, it will be parsed equivalent to `new URL(base)`. + +```js +const { URL } = require('url'); +const myURL = new URL('/foo', 'https://example.org/'); +// https://example.org/foo +``` + +A `TypeError` will be thrown if the `input` or `base` are not valid URLs. Note +that an effort will be made to coerce the given values into strings. For +instance: + +```js +const { URL } = require('url'); +const myURL = new URL({ toString: () => 'https://example.org/' }); +// https://example.org/ +``` + +Unicode characters appearing within the hostname of `input` will be +automatically converted to ASCII using the [Punycode][] algorithm. + +```js +const { URL } = require('url'); +const myURL = new URL('https://你好你好'); +// https://xn--6qqa088eba/ +``` + +*Note*: This feature is only available if the `node` executable was compiled +with [ICU][] enabled. If not, the domain names are passed through unchanged. + +#### url.hash + +* {string} + +Gets and sets the fragment portion of the URL. + +```js +const { URL } = require('url'); +const myURL = new URL('https://example.org/foo#bar'); +console.log(myURL.hash); +// Prints #bar + +myURL.hash = 'baz'; +console.log(myURL.href); +// Prints https://example.org/foo#baz +``` + +Invalid URL characters included in the value assigned to the `hash` property +are [percent-encoded][]. Note that the selection of which characters to +percent-encode may vary somewhat from what the [`url.parse()`][] and +[`url.format()`][] methods would produce. + +#### url.host + +* {string} + +Gets and sets the host portion of the URL. + +```js +const { URL } = require('url'); +const myURL = new URL('https://example.org:81/foo'); +console.log(myURL.host); +// Prints example.org:81 + +myURL.host = 'example.com:82'; +console.log(myURL.href); +// Prints https://example.com:82/foo +``` + +Invalid host values assigned to the `host` property are ignored. + +#### url.hostname + +* {string} + +Gets and sets the hostname portion of the URL. The key difference between +`url.host` and `url.hostname` is that `url.hostname` does *not* include the +port. + +```js +const { URL } = require('url'); +const myURL = new URL('https://example.org:81/foo'); +console.log(myURL.hostname); +// Prints example.org + +myURL.hostname = 'example.com:82'; +console.log(myURL.href); +// Prints https://example.com:81/foo +``` + +Invalid hostname values assigned to the `hostname` property are ignored. + +#### url.href + +* {string} + +Gets and sets the serialized URL. + +```js +const { URL } = require('url'); +const myURL = new URL('https://example.org/foo'); +console.log(myURL.href); +// Prints https://example.org/foo + +myURL.href = 'https://example.com/bar'; +console.log(myURL.href); +// Prints https://example.com/bar +``` + +Getting the value of the `href` property is equivalent to calling +[`url.toString()`][]. + +Setting the value of this property to a new value is equivalent to creating a +new `URL` object using [`new URL(value)`][`new URL()`]. Each of the `URL` +object's properties will be modified. + +If the value assigned to the `href` property is not a valid URL, a `TypeError` +will be thrown. + +#### url.origin + +* {string} + +Gets the read-only serialization of the URL's origin. + +```js +const { URL } = require('url'); +const myURL = new URL('https://example.org/foo/bar?baz'); +console.log(myURL.origin); +// Prints https://example.org +``` + +```js +const { URL } = require('url'); +const idnURL = new URL('https://你好你好'); +console.log(idnURL.origin); +// Prints https://xn--6qqa088eba + +console.log(idnURL.hostname); +// Prints xn--6qqa088eba +``` + +#### url.password + +* {string} + +Gets and sets the password portion of the URL. + +```js +const { URL } = require('url'); +const myURL = new URL('https://abc:xyz@example.com'); +console.log(myURL.password); +// Prints xyz + +myURL.password = '123'; +console.log(myURL.href); +// Prints https://abc:123@example.com +``` + +Invalid URL characters included in the value assigned to the `password` property +are [percent-encoded][]. Note that the selection of which characters to +percent-encode may vary somewhat from what the [`url.parse()`][] and +[`url.format()`][] methods would produce. + +#### url.pathname + +* {string} + +Gets and sets the path portion of the URL. + +```js +const { URL } = require('url'); +const myURL = new URL('https://example.org/abc/xyz?123'); +console.log(myURL.pathname); +// Prints /abc/xyz + +myURL.pathname = '/abcdef'; +console.log(myURL.href); +// Prints https://example.org/abcdef?123 +``` + +Invalid URL characters included in the value assigned to the `pathname` +property are [percent-encoded][]. Note that the selection of which characters +to percent-encode may vary somewhat from what the [`url.parse()`][] and +[`url.format()`][] methods would produce. + +#### url.port + +* {string} + +Gets and sets the port portion of the URL. + +```js +const { URL } = require('url'); +const myURL = new URL('https://example.org:8888'); +console.log(myURL.port); +// Prints 8888 + +// Default ports are automatically transformed to the empty string +// (HTTPS protocol's default port is 443) +myURL.port = '443'; +console.log(myURL.port); +// Prints the empty string +console.log(myURL.href); +// Prints https://example.org/ + +myURL.port = 1234; +console.log(myURL.port); +// Prints 1234 +console.log(myURL.href); +// Prints https://example.org:1234/ + +// Completely invalid port strings are ignored +myURL.port = 'abcd'; +console.log(myURL.port); +// Prints 1234 + +// Leading numbers are treated as a port number +myURL.port = '5678abcd'; +console.log(myURL.port); +// Prints 5678 + +// Non-integers are truncated +myURL.port = 1234.5678; +console.log(myURL.port); +// Prints 1234 + +// Out-of-range numbers are ignored +myURL.port = 1e10; +console.log(myURL.port); +// Prints 1234 +``` + +The port value may be set as either a number or as a String containing a number +in the range `0` to `65535` (inclusive). Setting the value to the default port +of the `URL` objects given `protocol` will result in the `port` value becoming +the empty string (`''`). + +If an invalid string is assigned to the `port` property, but it begins with a +number, the leading number is assigned to `port`. Otherwise, or if the number +lies outside the range denoted above, it is ignored. + +#### url.protocol + +* {string} + +Gets and sets the protocol portion of the URL. + +```js +const { URL } = require('url'); +const myURL = new URL('https://example.org'); +console.log(myURL.protocol); +// Prints https: + +myURL.protocol = 'ftp'; +console.log(myURL.href); +// Prints ftp://example.org/ +``` + +Invalid URL protocol values assigned to the `protocol` property are ignored. + +#### url.search + +* {string} + +Gets and sets the serialized query portion of the URL. + +```js +const { URL } = require('url'); +const myURL = new URL('https://example.org/abc?123'); +console.log(myURL.search); +// Prints ?123 + +myURL.search = 'abc=xyz'; +console.log(myURL.href); +// Prints https://example.org/abc?abc=xyz +``` + +Any invalid URL characters appearing in the value assigned the `search` +property will be [percent-encoded][]. Note that the selection of which +characters to percent-encode may vary somewhat from what the [`url.parse()`][] +and [`url.format()`][] methods would produce. + +#### url.searchParams + +* {URLSearchParams} + +Gets the [`URLSearchParams`][] object representing the query parameters of the +URL. This property is read-only; to replace the entirety of query parameters of +the URL, use the [`url.search`][] setter. See [`URLSearchParams`][] +documentation for details. + +#### url.username + +* {string} + +Gets and sets the username portion of the URL. + +```js +const { URL } = require('url'); +const myURL = new URL('https://abc:xyz@example.com'); +console.log(myURL.username); +// Prints abc + +myURL.username = '123'; +console.log(myURL.href); +// Prints https://123:xyz@example.com/ +``` + +Any invalid URL characters appearing in the value assigned the `username` +property will be [percent-encoded][]. Note that the selection of which +characters to percent-encode may vary somewhat from what the [`url.parse()`][] +and [`url.format()`][] methods would produce. + +#### url.toString() + +* Returns: {string} + +The `toString()` method on the `URL` object returns the serialized URL. The +value returned is equivalent to that of [`url.href`][] and [`url.toJSON()`][]. + +Because of the need for standard compliance, this method does not allow users +to customize the serialization process of the URL. + +#### url.toJSON() + +* Returns: {string} + +The `toJSON()` method on the `URL` object returns the serialized URL. The +value returned is equivalent to that of [`url.href`][] and +[`url.toString()`][]. + +This method is automatically called when an `URL` object is serialized +with [`JSON.stringify()`][]. + +```js +const { URL } = require('url'); +const myURLs = [ + new URL('https://www.example.com'), + new URL('https://test.example.org') +]; +console.log(JSON.stringify(myURLs)); +// Prints ["https://www.example.com/","https://test.example.org/"] +``` + +### Class: URLSearchParams + + +The `URLSearchParams` API provides read and write access to the query of a +`URL`. The `URLSearchParams` class can also be used standalone with one of the +four following constructors. + +The WHATWG `URLSearchParams` interface and the [`querystring`][] module have +similar purpose, but the purpose of the [`querystring`][] module is more +general, as it allows the customization of delimiter characters (`&` and `=`). +On the other hand, this API is designed purely for URL query strings. + +```js +const { URL, URLSearchParams } = require('url'); + +const myURL = new URL('https://example.org/?abc=123'); +console.log(myURL.searchParams.get('abc')); +// Prints 123 + +myURL.searchParams.append('abc', 'xyz'); +console.log(myURL.href); +// Prints https://example.org/?abc=123&abc=xyz + +myURL.searchParams.delete('abc'); +myURL.searchParams.set('a', 'b'); +console.log(myURL.href); +// Prints https://example.org/?a=b + +const newSearchParams = new URLSearchParams(myURL.searchParams); +// The above is equivalent to +// const newSearchParams = new URLSearchParams(myURL.search); + +newSearchParams.append('a', 'c'); +console.log(myURL.href); +// Prints https://example.org/?a=b +console.log(newSearchParams.toString()); +// Prints a=b&a=c + +// newSearchParams.toString() is implicitly called +myURL.search = newSearchParams; +console.log(myURL.href); +// Prints https://example.org/?a=b&a=c +newSearchParams.delete('a'); +console.log(myURL.href); +// Prints https://example.org/?a=b&a=c +``` + +#### Constructor: new URLSearchParams() + +Instantiate a new empty `URLSearchParams` object. + +#### Constructor: new URLSearchParams(string) + +* `string` {string} A query string + +Parse the `string` as a query string, and use it to instantiate a new +`URLSearchParams` object. A leading `'?'`, if present, is ignored. + +```js +const { URLSearchParams } = require('url'); +let params; + +params = new URLSearchParams('user=abc&query=xyz'); +console.log(params.get('user')); +// Prints 'abc' +console.log(params.toString()); +// Prints 'user=abc&query=xyz' + +params = new URLSearchParams('?user=abc&query=xyz'); +console.log(params.toString()); +// Prints 'user=abc&query=xyz' +``` + +#### Constructor: new URLSearchParams(obj) + + +* `obj` {Object} An object representing a collection of key-value pairs + +Instantiate a new `URLSearchParams` object with a query hash map. The key and +value of each property of `obj` are always coerced to strings. + +*Note*: Unlike [`querystring`][] module, duplicate keys in the form of array +values are not allowed. Arrays are stringified using [`array.toString()`][], +which simply joins all array elements with commas. + +```js +const { URLSearchParams } = require('url'); +const params = new URLSearchParams({ + user: 'abc', + query: ['first', 'second'] +}); +console.log(params.getAll('query')); +// Prints [ 'first,second' ] +console.log(params.toString()); +// Prints 'user=abc&query=first%2Csecond' +``` + +#### Constructor: new URLSearchParams(iterable) + + +* `iterable` {Iterable} An iterable object whose elements are key-value pairs + +Instantiate a new `URLSearchParams` object with an iterable map in a way that +is similar to [`Map`][]'s constructor. `iterable` can be an Array or any +iterable object. That means `iterable` can be another `URLSearchParams`, in +which case the constructor will simply create a clone of the provided +`URLSearchParams`. Elements of `iterable` are key-value pairs, and can +themselves be any iterable object. + +Duplicate keys are allowed. + +```js +const { URLSearchParams } = require('url'); +let params; + +// Using an array +params = new URLSearchParams([ + ['user', 'abc'], + ['query', 'first'], + ['query', 'second'] +]); +console.log(params.toString()); +// Prints 'user=abc&query=first&query=second' + +// Using a Map object +const map = new Map(); +map.set('user', 'abc'); +map.set('query', 'xyz'); +params = new URLSearchParams(map); +console.log(params.toString()); +// Prints 'user=abc&query=xyz' + +// Using a generator function +function* getQueryPairs() { + yield ['user', 'abc']; + yield ['query', 'first']; + yield ['query', 'second']; +} +params = new URLSearchParams(getQueryPairs()); +console.log(params.toString()); +// Prints 'user=abc&query=first&query=second' + +// Each key-value pair must have exactly two elements +new URLSearchParams([ + ['user', 'abc', 'error'] +]); +// Throws TypeError [ERR_INVALID_TUPLE]: +// Each query pair must be an iterable [name, value] tuple +``` + +#### urlSearchParams.append(name, value) + +* `name` {string} +* `value` {string} + +Append a new name-value pair to the query string. + +#### urlSearchParams.delete(name) + +* `name` {string} + +Remove all name-value pairs whose name is `name`. + +#### urlSearchParams.entries() + +* Returns: {Iterator} + +Returns an ES6 Iterator over each of the name-value pairs in the query. +Each item of the iterator is a JavaScript Array. The first item of the Array +is the `name`, the second item of the Array is the `value`. + +Alias for [`urlSearchParams[@@iterator]()`][`urlSearchParams@@iterator()`]. + +#### urlSearchParams.forEach(fn[, thisArg]) + +* `fn` {Function} Function invoked for each name-value pair in the query. +* `thisArg` {Object} Object to be used as `this` value for when `fn` is called + +Iterates over each name-value pair in the query and invokes the given function. + +```js +const { URL } = require('url'); +const myURL = new URL('https://example.org/?a=b&c=d'); +myURL.searchParams.forEach((value, name, searchParams) => { + console.log(name, value, myURL.searchParams === searchParams); +}); +// Prints: +// a b true +// c d true +``` + +#### urlSearchParams.get(name) + +* `name` {string} +* Returns: {string} or `null` if there is no name-value pair with the given + `name`. + +Returns the value of the first name-value pair whose name is `name`. If there +are no such pairs, `null` is returned. + +#### urlSearchParams.getAll(name) + +* `name` {string} +* Returns: {Array} + +Returns the values of all name-value pairs whose name is `name`. If there are +no such pairs, an empty array is returned. + +#### urlSearchParams.has(name) + +* `name` {string} +* Returns: {boolean} + +Returns `true` if there is at least one name-value pair whose name is `name`. + +#### urlSearchParams.keys() + +* Returns: {Iterator} + +Returns an ES6 Iterator over the names of each name-value pair. + +```js +const { URLSearchParams } = require('url'); +const params = new URLSearchParams('foo=bar&foo=baz'); +for (const name of params.keys()) { + console.log(name); +} +// Prints: +// foo +// foo +``` + +#### urlSearchParams.set(name, value) + +* `name` {string} +* `value` {string} + +Sets the value in the `URLSearchParams` object associated with `name` to +`value`. If there are any pre-existing name-value pairs whose names are `name`, +set the first such pair's value to `value` and remove all others. If not, +append the name-value pair to the query string. + +```js +const { URLSearchParams } = require('url'); + +const params = new URLSearchParams(); +params.append('foo', 'bar'); +params.append('foo', 'baz'); +params.append('abc', 'def'); +console.log(params.toString()); +// Prints foo=bar&foo=baz&abc=def + +params.set('foo', 'def'); +params.set('xyz', 'opq'); +console.log(params.toString()); +// Prints foo=def&abc=def&xyz=opq +``` + +#### urlSearchParams.sort() + + +Sort all existing name-value pairs in-place by their names. Sorting is done +with a [stable sorting algorithm][], so relative order between name-value pairs +with the same name is preserved. + +This method can be used, in particular, to increase cache hits. + +```js +const { URLSearchParams } = require('url'); +const params = new URLSearchParams('query[]=abc&type=search&query[]=123'); +params.sort(); +console.log(params.toString()); +// Prints query%5B%5D=abc&query%5B%5D=123&type=search +``` + +#### urlSearchParams.toString() + +* Returns: {string} + +Returns the search parameters serialized as a string, with characters +percent-encoded where necessary. + +#### urlSearchParams.values() + +* Returns: {Iterator} + +Returns an ES6 Iterator over the values of each name-value pair. + +#### urlSearchParams\[@@iterator\]() + +* Returns: {Iterator} -For example: `'host.com:8080'` +Returns an ES6 Iterator over each of the name-value pairs in the query string. +Each item of the iterator is a JavaScript Array. The first item of the Array +is the `name`, the second item of the Array is the `value`. -### urlObject.auth +Alias for [`urlSearchParams.entries()`][]. + +```js +const { URLSearchParams } = require('url'); +const params = new URLSearchParams('foo=bar&xyz=baz'); +for (const [name, value] of params) { + console.log(name, value); +} +// Prints: +// foo bar +// xyz baz +``` + +### url.domainToASCII(domain) + + +* `domain` {string} +* Returns: {string} + +Returns the [Punycode][] ASCII serialization of the `domain`. If `domain` is an +invalid domain, the empty string is returned. + +It performs the inverse operation to [`url.domainToUnicode()`][]. + +```js +const url = require('url'); +console.log(url.domainToASCII('español.com')); +// Prints xn--espaol-zwa.com +console.log(url.domainToASCII('中文.com')); +// Prints xn--fiq228c.com +console.log(url.domainToASCII('xn--iñvalid.com')); +// Prints an empty string +``` + +### url.domainToUnicode(domain) + + +* `domain` {string} +* Returns: {string} + +Returns the Unicode serialization of the `domain`. If `domain` is an invalid +domain, the empty string is returned. + +It performs the inverse operation to [`url.domainToASCII()`][]. + +```js +const url = require('url'); +console.log(url.domainToUnicode('xn--espaol-zwa.com')); +// Prints español.com +console.log(url.domainToUnicode('xn--fiq228c.com')); +// Prints 中文.com +console.log(url.domainToUnicode('xn--iñvalid.com')); +// Prints an empty string +``` + +## Legacy URL API + +### Legacy urlObject + +The legacy urlObject (`require('url').Url`) is created and returned by the +`url.parse()` function. + +#### urlObject.auth The `auth` property is the username and password portion of the URL, also referred to as "userinfo". This string subset follows the `protocol` and @@ -72,12 +823,33 @@ with the `[:{password}]` portion being optional. For example: `'user:pass'` -### urlObject.hostname +#### urlObject.hash + +The `hash` property consists of the "fragment" portion of the URL including +the leading ASCII hash (`#`) character. + +For example: `'#hash'` + +#### urlObject.host + +The `host` property is the full lower-cased host portion of the URL, including +the `port` if specified. + +For example: `'sub.host.com:8080'` + +#### urlObject.hostname The `hostname` property is the lower-cased host name portion of the `host` component *without* the `port` included. -For example: `'host.com'` +For example: `'sub.host.com'` + +#### urlObject.href + +The `href` property is the full URL string that was parsed with both the +`protocol` and `host` components converted to lower-case. + +For example: `'http://user:pass@sub.host.com:8080/p/a/t/h?query=string#hash'` ### urlObject.port @@ -126,14 +898,22 @@ For example: `'query=string'` or `{'query': 'string'}` If returned as a string, no decoding of the query string is performed. If returned as an object, both keys and values are decoded. -### urlObject.hash +#### urlObject.search -The `hash` property consists of the "fragment" portion of the URL including -the leading ASCII hash (`#`) character. +The `search` property consists of the entire "query string" portion of the +URL, including the leading ASCII question mark (`?`) character. -For example: `'#hash'` +For example: `'?query=string'` -## url.format(urlObject) +No decoding of the query string is performed. + +#### urlObject.slashes + +The `slashes` property is a `boolean` with a value of `true` if two ASCII +forward-slash characters (`/`) are required following the colon in the +`protocol`. + +### url.format(urlObject) @@ -201,7 +981,7 @@ The formatting process operates as follows: * `result` is returned. -## url.parse(urlString[, parseQueryString[, slashesDenoteHost]]) +### url.parse(urlString[, parseQueryString[, slashesDenoteHost]]) @@ -220,7 +1000,7 @@ added: v0.1.25 The `url.parse()` method takes a URL string, parses it, and returns a URL object. -## url.resolve(from, to) +### url.resolve(from, to) @@ -239,11 +1019,18 @@ url.resolve('http://example.com/', '/one'); // 'http://example.com/one' url.resolve('http://example.com/one', '/two'); // 'http://example.com/two' ``` -## Escaped Characters + +## Percent-Encoding in URLs + +URLs are permitted to only contain a certain range of characters. Any character +falling outside of that range must be encoded. How such characters are encoded, +and which characters to encode depends entirely on where the character is +located within the structure of the URL. -URLs are only permitted to contain a certain range of characters. Spaces (`' '`) -and the following characters will be automatically escaped in the -properties of URL objects: +### Legacy API + +Within the Legacy API, spaces (`' '`) and the following characters will be +automatically escaped in the properties of URL objects: ```txt < > " ` \r \n \t { } | \ ^ ' @@ -252,6 +1039,68 @@ properties of URL objects: For example, the ASCII space character (`' '`) is encoded as `%20`. The ASCII forward slash (`/`) character is encoded as `%3C`. +### WHATWG API + +The [WHATWG URL Standard][] uses a more selective and fine grained approach to +selecting encoded characters than that used by the Legacy API. + +The WHATWG algorithm defines three "percent-encode sets" that describe ranges +of characters that must be percent-encoded: + +* The *C0 control percent-encode set* includes code points in range U+0000 to + U+001F (inclusive) and all code points greater than U+007E. + +* The *path percent-encode set* includes the *C0 control percent-encode set* + and code points U+0020, U+0022, U+0023, U+003C, U+003E, U+003F, U+0060, + U+007B, and U+007D. + +* The *userinfo encode set* includes the *path percent-encode set* and code + points U+002F, U+003A, U+003B, U+003D, U+0040, U+005B, U+005C, U+005D, + U+005E, and U+007C. + +The *userinfo percent-encode set* is used exclusively for username and +passwords encoded within the URL. The *path percent-encode set* is used for the +path of most URLs. The *C0 control percent-encode set* is used for all +other cases, including URL fragments in particular, but also host and path +under certain specific conditions. + +When non-ASCII characters appear within a hostname, the hostname is encoded +using the [Punycode][] algorithm. Note, however, that a hostname *may* contain +*both* Punycode encoded and percent-encoded characters. For example: + +```js +const { URL } = require('url'); +const myURL = new URL('https://%CF%80.com/foo'); +console.log(myURL.href); +// Prints https://xn--1xa.com/foo +console.log(myURL.origin); +// Prints https://π.com +``` + [`Error`]: errors.html#errors_class_error -[`querystring`]: querystring.html +[`JSON.stringify()`]: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify +[`Map`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map [`TypeError`]: errors.html#errors_class_typeerror +[`URLSearchParams`]: #url_class_urlsearchparams +[`array.toString()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toString +[`new URL()`]: #url_constructor_new_url_input_base +[`querystring`]: querystring.html +[`require('url').format()`]: #url_url_format_url_options +[`url.domainToASCII()`]: #url_url_domaintoascii_domain +[`url.domainToUnicode()`]: #url_url_domaintounicode_domain +[`url.format()`]: #url_url_format_urlobject +[`url.href`]: #url_url_href +[`url.parse()`]: #url_url_parse_urlstring_parsequerystring_slashesdenotehost +[`url.search`]: #url_url_search +[`url.toJSON()`]: #url_url_tojson +[`url.toString()`]: #url_url_tostring +[`urlSearchParams.entries()`]: #url_urlsearchparams_entries +[`urlSearchParams@@iterator()`]: #url_urlsearchparams_iterator +[ICU]: intl.html#intl_options_for_building_node_js +[Punycode]: https://tools.ietf.org/html/rfc5891#section-4.4 +[WHATWG URL Standard]: https://url.spec.whatwg.org/ +[WHATWG URL]: #url_the_whatwg_url_api +[examples of parsed URLs]: https://url.spec.whatwg.org/#example-url-parsing +[legacy urlObject]: #url_legacy_urlobject +[percent-encoded]: #whatwg-percent-encoding +[stable sorting algorithm]: https://en.wikipedia.org/wiki/Sorting_algorithm#Stability diff --git a/lib/internal/bootstrap_node.js b/lib/internal/bootstrap_node.js index 310dc9dd029375..4b5a500adc598e 100644 --- a/lib/internal/bootstrap_node.js +++ b/lib/internal/bootstrap_node.js @@ -62,6 +62,10 @@ _process.setupRawDebug(); + // Ensure setURLConstructor() is called before the native + // URL::ToObject() method is used. + NativeModule.require('internal/url'); + Object.defineProperty(process, 'argv0', { enumerable: true, configurable: false, diff --git a/lib/internal/querystring.js b/lib/internal/querystring.js new file mode 100644 index 00000000000000..d1684418097100 --- /dev/null +++ b/lib/internal/querystring.js @@ -0,0 +1,29 @@ +'use strict'; + +const hexTable = new Array(256); +for (var i = 0; i < 256; ++i) + hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase(); + +const isHexTable = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 32 - 47 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 + 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 64 - 79 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 80 - 95 + 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 96 - 111 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 112 - 127 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 128 ... + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // ... 256 +]; + +module.exports = { + hexTable, + isHexTable +}; diff --git a/lib/internal/url.js b/lib/internal/url.js new file mode 100644 index 00000000000000..91a03ec99a87fd --- /dev/null +++ b/lib/internal/url.js @@ -0,0 +1,1428 @@ +'use strict'; + +const util = require('util'); +const { + hexTable, + isHexTable +} = require('internal/querystring'); + +const { getConstructorOf } = require('internal/util'); +const querystring = require('querystring'); + +const { platform } = process; +const isWindows = platform === 'win32'; + +const { + domainToASCII: _domainToASCII, + domainToUnicode: _domainToUnicode, + encodeAuth, + toUSVString: _toUSVString, + parse: _parse, + setURLConstructor, + URL_FLAGS_CANNOT_BE_BASE, + URL_FLAGS_HAS_FRAGMENT, + URL_FLAGS_HAS_HOST, + URL_FLAGS_HAS_PASSWORD, + URL_FLAGS_HAS_PATH, + URL_FLAGS_HAS_QUERY, + URL_FLAGS_HAS_USERNAME, + URL_FLAGS_SPECIAL, + kFragment, + kHost, + kHostname, + kPathStart, + kPort, + kQuery, + kSchemeStart +} = process.binding('url'); + +const context = Symbol('context'); +const cannotBeBase = Symbol('cannot-be-base'); +const cannotHaveUsernamePasswordPort = + Symbol('cannot-have-username-password-port'); +const special = Symbol('special'); +const searchParams = Symbol('query'); +const kFormat = Symbol('format'); + +// https://tc39.github.io/ecma262/#sec-%iteratorprototype%-object +const IteratorPrototype = Object.getPrototypeOf( + Object.getPrototypeOf([][Symbol.iterator]()) +); + +const unpairedSurrogateRe = + /(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])/; +function toUSVString(val) { + const str = `${val}`; + // As of V8 5.5, `str.search()` (and `unpairedSurrogateRe[@@search]()`) are + // slower than `unpairedSurrogateRe.exec()`. + const match = unpairedSurrogateRe.exec(str); + if (!match) + return str; + return _toUSVString(str, match.index); +} + +// Refs: https://html.spec.whatwg.org/multipage/browsers.html#concept-origin-opaque +const kOpaqueOrigin = 'null'; + +// Refs: https://html.spec.whatwg.org/multipage/browsers.html#ascii-serialisation-of-an-origin +function serializeTupleOrigin(scheme, host, port) { + return `${scheme}//${host}${port === null ? '' : `:${port}`}`; +} + +// This class provides the internal state of a URL object. An instance of this +// class is stored in every URL object and is accessed internally by setters +// and getters. It roughly corresponds to the concept of a URL record in the +// URL Standard, with a few differences. It is also the object transported to +// the C++ binding. +// Refs: https://url.spec.whatwg.org/#concept-url +class URLContext { + constructor() { + this.flags = 0; + this.scheme = ':'; + this.username = ''; + this.password = ''; + this.host = null; + this.port = null; + this.path = []; + this.query = null; + this.fragment = null; + } +} + +class URLSearchParams { + // URL Standard says the default value is '', but as undefined and '' have + // the same result, undefined is used to prevent unnecessary parsing. + // Default parameter is necessary to keep URLSearchParams.length === 0 in + // accordance with Web IDL spec. + constructor(init = undefined) { + if (init === null || init === undefined) { + this[searchParams] = []; + } else if ((typeof init === 'object' && init !== null) || + typeof init === 'function') { + const method = init[Symbol.iterator]; + if (method === this[Symbol.iterator]) { + // While the spec does not have this branch, we can use it as a + // shortcut to avoid having to go through the costly generic iterator. + const childParams = init[searchParams]; + this[searchParams] = childParams.slice(); + } else if (method !== null && method !== undefined) { + if (typeof method !== 'function') { + throw new TypeError('Query pairs must be iterable'); + } + + // sequence> + // Note: per spec we have to first exhaust the lists then process them + const pairs = []; + for (const pair of init) { + if ((typeof pair !== 'object' && typeof pair !== 'function') || + pair === null || + typeof pair[Symbol.iterator] !== 'function') { + throw new TypeError( + 'Each query pair must be an iterable [name, value] tuple'); + } + const convertedPair = []; + for (const element of pair) + convertedPair.push(toUSVString(element)); + pairs.push(convertedPair); + } + + this[searchParams] = []; + for (const pair of pairs) { + if (pair.length !== 2) { + throw new TypeError( + 'Each query pair must be an iterable [name, value] tuple'); + } + this[searchParams].push(pair[0], pair[1]); + } + } else { + // record + // Need to use reflection APIs for full spec compliance. + this[searchParams] = []; + const keys = Reflect.ownKeys(init); + for (var i = 0; i < keys.length; i++) { + const key = keys[i]; + const desc = Reflect.getOwnPropertyDescriptor(init, key); + if (desc !== undefined && desc.enumerable) { + const typedKey = toUSVString(key); + const typedValue = toUSVString(init[key]); + this[searchParams].push(typedKey, typedValue); + } + } + } + } else { + // USVString + init = toUSVString(init); + if (init[0] === '?') init = init.slice(1); + initSearchParams(this, init); + } + + // "associated url object" + this[context] = null; + } + + [util.inspect.custom](recurseTimes, ctx) { + if (!this || !this[searchParams] || this[searchParams][searchParams]) { + throw new TypeError('Value of "this" must be of type URLSearchParams'); + } + + if (typeof recurseTimes === 'number' && recurseTimes < 0) + return ctx.stylize('[Object]', 'special'); + + var separator = ', '; + var innerOpts = util._extend({}, ctx); + if (recurseTimes !== null) { + innerOpts.depth = recurseTimes - 1; + } + var innerInspect = (v) => util.inspect(v, innerOpts); + + var list = this[searchParams]; + var output = []; + for (var i = 0; i < list.length; i += 2) + output.push(`${innerInspect(list[i])} => ${innerInspect(list[i + 1])}`); + + var colorRe = /\u001b\[\d\d?m/g; + var length = output.reduce( + (prev, cur) => prev + cur.replace(colorRe, '').length + separator.length, + -separator.length + ); + if (length > ctx.breakLength) { + return `${this.constructor.name} {\n ${output.join(',\n ')} }`; + } else if (output.length) { + return `${this.constructor.name} { ${output.join(separator)} }`; + } else { + return `${this.constructor.name} {}`; + } + } +} + +function onParseComplete(flags, protocol, username, password, + host, port, path, query, fragment) { + var ctx = this[context]; + ctx.flags = flags; + ctx.scheme = protocol; + ctx.username = (flags & URL_FLAGS_HAS_USERNAME) !== 0 ? username : ''; + ctx.password = (flags & URL_FLAGS_HAS_PASSWORD) !== 0 ? password : ''; + ctx.port = port; + ctx.path = (flags & URL_FLAGS_HAS_PATH) !== 0 ? path : []; + ctx.query = query; + ctx.fragment = fragment; + ctx.host = host; + if (!this[searchParams]) { // invoked from URL constructor + this[searchParams] = new URLSearchParams(); + this[searchParams][context] = this; + } + initSearchParams(this[searchParams], query); +} + +function onParseError(flags, input) { + const error = new TypeError(`Invalid URL: ${input}`); + error.input = input; + throw error; +} + +// Reused by URL constructor and URL#href setter. +function parse(url, input, base) { + const base_context = base ? base[context] : undefined; + url[context] = new URLContext(); + _parse(input.trim(), -1, base_context, undefined, + onParseComplete.bind(url), onParseError); +} + +function onParseProtocolComplete(flags, protocol, username, password, + host, port, path, query, fragment) { + const ctx = this[context]; + if ((flags & URL_FLAGS_SPECIAL) !== 0) { + ctx.flags |= URL_FLAGS_SPECIAL; + } else { + ctx.flags &= ~URL_FLAGS_SPECIAL; + } + ctx.scheme = protocol; + ctx.port = port; +} + +function onParseHostComplete(flags, protocol, username, password, + host, port, path, query, fragment) { + const ctx = this[context]; + if ((flags & URL_FLAGS_HAS_HOST) !== 0) { + ctx.host = host; + ctx.flags |= URL_FLAGS_HAS_HOST; + } else { + ctx.host = null; + ctx.flags &= ~URL_FLAGS_HAS_HOST; + } + if (port !== null) + ctx.port = port; +} + +function onParseHostnameComplete(flags, protocol, username, password, + host, port, path, query, fragment) { + const ctx = this[context]; + if ((flags & URL_FLAGS_HAS_HOST) !== 0) { + ctx.host = host; + ctx.flags |= URL_FLAGS_HAS_HOST; + } else { + ctx.host = null; + ctx.flags &= ~URL_FLAGS_HAS_HOST; + } +} + +function onParsePortComplete(flags, protocol, username, password, + host, port, path, query, fragment) { + this[context].port = port; +} + +function onParsePathComplete(flags, protocol, username, password, + host, port, path, query, fragment) { + const ctx = this[context]; + if ((flags & URL_FLAGS_HAS_PATH) !== 0) { + ctx.path = path; + ctx.flags |= URL_FLAGS_HAS_PATH; + } else { + ctx.path = []; + ctx.flags &= ~URL_FLAGS_HAS_PATH; + } + + // The C++ binding may set host to empty string. + if ((flags & URL_FLAGS_HAS_HOST) !== 0) { + ctx.host = host; + ctx.flags |= URL_FLAGS_HAS_HOST; + } +} + +function onParseSearchComplete(flags, protocol, username, password, + host, port, path, query, fragment) { + this[context].query = query; +} + +function onParseHashComplete(flags, protocol, username, password, + host, port, path, query, fragment) { + this[context].fragment = fragment; +} + +class URL { + constructor(input, base) { + // toUSVString is not needed. + input = `${input}`; + if (base !== undefined && + (!base[searchParams] || !base[searchParams][searchParams])) { + base = new URL(base); + } + parse(this, input, base); + } + + get [special]() { + return (this[context].flags & URL_FLAGS_SPECIAL) !== 0; + } + + get [cannotBeBase]() { + return (this[context].flags & URL_FLAGS_CANNOT_BE_BASE) !== 0; + } + + // https://url.spec.whatwg.org/#cannot-have-a-username-password-port + get [cannotHaveUsernamePasswordPort]() { + const { host, scheme } = this[context]; + return ((host == null || host === '') || + this[cannotBeBase] || + scheme === 'file:'); + } + + [util.inspect.custom](depth, opts) { + if (this == null || + Object.getPrototypeOf(this[context]) !== URLContext.prototype) { + throw new TypeError('Value of "this" must be of type URL'); + } + + if (typeof depth === 'number' && depth < 0) + return opts.stylize('[Object]', 'special'); + + var ctor = getConstructorOf(this); + + var obj = Object.create({ + constructor: ctor === null ? URL : ctor + }); + + obj.href = this.href; + obj.origin = this.origin; + obj.protocol = this.protocol; + obj.username = this.username; + obj.password = this.password; + obj.host = this.host; + obj.hostname = this.hostname; + obj.port = this.port; + obj.pathname = this.pathname; + obj.search = this.search; + obj.searchParams = this.searchParams; + obj.hash = this.hash; + + if (opts.showHidden) { + obj.cannotBeBase = this[cannotBeBase]; + obj.special = this[special]; + obj[context] = this[context]; + } + + return util.inspect(obj, opts); + } +} + +Object.defineProperties(URL.prototype, { + [kFormat]: { + enumerable: false, + configurable: false, + // eslint-disable-next-line func-name-matching + value: function format(options) { + if (options && typeof options !== 'object') + throw new TypeError('The "options" argument must be of type Object'); + options = util._extend({ + fragment: true, + unicode: false, + search: true, + auth: true + }, options); + const ctx = this[context]; + var ret = ctx.scheme; + if (ctx.host !== null) { + ret += '//'; + const has_username = ctx.username !== ''; + const has_password = ctx.password !== ''; + if (options.auth && (has_username || has_password)) { + if (has_username) + ret += ctx.username; + if (has_password) + ret += `:${ctx.password}`; + ret += '@'; + } + ret += options.unicode ? + domainToUnicode(this.host) : this.host; + } else if (ctx.scheme === 'file:') { + ret += '//'; + } + if (this.pathname) + ret += this.pathname; + if (options.search && ctx.query !== null) + ret += `?${ctx.query}`; + if (options.fragment && ctx.fragment !== null) + ret += `#${ctx.fragment}`; + return ret; + } + }, + [Symbol.toStringTag]: { + configurable: true, + value: 'URL' + }, + toString: { + // https://heycam.github.io/webidl/#es-stringifier + writable: true, + enumerable: true, + configurable: true, + // eslint-disable-next-line func-name-matching + value: function toString() { + return this[kFormat]({}); + } + }, + href: { + enumerable: true, + configurable: true, + get() { + return this[kFormat]({}); + }, + set(input) { + // toUSVString is not needed. + input = `${input}`; + parse(this, input); + } + }, + origin: { // readonly + enumerable: true, + configurable: true, + get() { + // Refs: https://url.spec.whatwg.org/#concept-url-origin + const ctx = this[context]; + switch (ctx.scheme) { + case 'blob:': + if (ctx.path.length > 0) { + try { + return (new URL(ctx.path[0])).origin; + } catch (err) { + // fall through... do nothing + } + } + return kOpaqueOrigin; + case 'ftp:': + case 'gopher:': + case 'http:': + case 'https:': + case 'ws:': + case 'wss:': + return serializeTupleOrigin(ctx.scheme, ctx.host, ctx.port); + } + return kOpaqueOrigin; + } + }, + protocol: { + enumerable: true, + configurable: true, + get() { + return this[context].scheme; + }, + set(scheme) { + // toUSVString is not needed. + scheme = `${scheme}`; + if (scheme.length === 0) + return; + const ctx = this[context]; + if (ctx.scheme === 'file:' && + (ctx.host === '' || ctx.host === null)) { + return; + } + _parse(scheme, kSchemeStart, null, ctx, + onParseProtocolComplete.bind(this)); + } + }, + username: { + enumerable: true, + configurable: true, + get() { + return this[context].username; + }, + set(username) { + // toUSVString is not needed. + username = `${username}`; + if (this[cannotHaveUsernamePasswordPort]) + return; + const ctx = this[context]; + if (username === '') { + ctx.username = ''; + ctx.flags &= ~URL_FLAGS_HAS_USERNAME; + return; + } + ctx.username = encodeAuth(username); + ctx.flags |= URL_FLAGS_HAS_USERNAME; + } + }, + password: { + enumerable: true, + configurable: true, + get() { + return this[context].password; + }, + set(password) { + // toUSVString is not needed. + password = `${password}`; + if (this[cannotHaveUsernamePasswordPort]) + return; + const ctx = this[context]; + if (password === '') { + ctx.password = ''; + ctx.flags &= ~URL_FLAGS_HAS_PASSWORD; + return; + } + ctx.password = encodeAuth(password); + ctx.flags |= URL_FLAGS_HAS_PASSWORD; + } + }, + host: { + enumerable: true, + configurable: true, + get() { + const ctx = this[context]; + var ret = ctx.host || ''; + if (ctx.port !== null) + ret += `:${ctx.port}`; + return ret; + }, + set(host) { + const ctx = this[context]; + // toUSVString is not needed. + host = `${host}`; + if (this[cannotBeBase]) { + // Cannot set the host if cannot-be-base is set + return; + } + _parse(host, kHost, null, ctx, onParseHostComplete.bind(this)); + } + }, + hostname: { + enumerable: true, + configurable: true, + get() { + return this[context].host || ''; + }, + set(host) { + const ctx = this[context]; + // toUSVString is not needed. + host = `${host}`; + if (this[cannotBeBase]) { + // Cannot set the host if cannot-be-base is set + return; + } + _parse(host, kHostname, null, ctx, onParseHostnameComplete.bind(this)); + } + }, + port: { + enumerable: true, + configurable: true, + get() { + const port = this[context].port; + return port === null ? '' : String(port); + }, + set(port) { + // toUSVString is not needed. + port = `${port}`; + if (this[cannotHaveUsernamePasswordPort]) + return; + const ctx = this[context]; + if (port === '') { + ctx.port = null; + return; + } + _parse(port, kPort, null, ctx, onParsePortComplete.bind(this)); + } + }, + pathname: { + enumerable: true, + configurable: true, + get() { + const ctx = this[context]; + if (this[cannotBeBase]) + return ctx.path[0]; + if (ctx.path.length === 0) + return ''; + return `/${ctx.path.join('/')}`; + }, + set(path) { + // toUSVString is not needed. + path = `${path}`; + if (this[cannotBeBase]) + return; + _parse(path, kPathStart, null, this[context], + onParsePathComplete.bind(this)); + } + }, + search: { + enumerable: true, + configurable: true, + get() { + const { query } = this[context]; + if (query === null || query === '') + return ''; + return `?${query}`; + }, + set(search) { + const ctx = this[context]; + search = toUSVString(search); + if (search === '') { + ctx.query = null; + ctx.flags &= ~URL_FLAGS_HAS_QUERY; + } else { + if (search[0] === '?') search = search.slice(1); + ctx.query = ''; + ctx.flags |= URL_FLAGS_HAS_QUERY; + if (search) { + _parse(search, kQuery, null, ctx, onParseSearchComplete.bind(this)); + } + } + initSearchParams(this[searchParams], search); + } + }, + searchParams: { // readonly + enumerable: true, + configurable: true, + get() { + return this[searchParams]; + } + }, + hash: { + enumerable: true, + configurable: true, + get() { + const { fragment } = this[context]; + if (fragment === null || fragment === '') + return ''; + return `#${fragment}`; + }, + set(hash) { + const ctx = this[context]; + // toUSVString is not needed. + hash = `${hash}`; + if (!hash) { + ctx.fragment = null; + ctx.flags &= ~URL_FLAGS_HAS_FRAGMENT; + return; + } + if (hash[0] === '#') hash = hash.slice(1); + ctx.fragment = ''; + ctx.flags |= URL_FLAGS_HAS_FRAGMENT; + _parse(hash, kFragment, null, ctx, onParseHashComplete.bind(this)); + } + }, + toJSON: { + writable: true, + enumerable: true, + configurable: true, + // eslint-disable-next-line func-name-matching + value: function toJSON() { + return this[kFormat]({}); + } + } +}); + +function update(url, params) { + if (!url) + return; + + const ctx = url[context]; + const serializedParams = params.toString(); + if (serializedParams) { + ctx.query = serializedParams; + ctx.flags |= URL_FLAGS_HAS_QUERY; + } else { + ctx.query = null; + ctx.flags &= ~URL_FLAGS_HAS_QUERY; + } +} + +function initSearchParams(url, init) { + if (!init) { + url[searchParams] = []; + return; + } + url[searchParams] = parseParams(init); +} + +// application/x-www-form-urlencoded parser +// Ref: https://url.spec.whatwg.org/#concept-urlencoded-parser +function parseParams(qs) { + const out = []; + var pairStart = 0; + var lastPos = 0; + var seenSep = false; + var buf = ''; + var encoded = false; + var encodeCheck = 0; + var i; + for (i = 0; i < qs.length; ++i) { + const code = qs.charCodeAt(i); + + // Try matching key/value pair separator + if (code === 38/*&*/) { + if (pairStart === i) { + // We saw an empty substring between pair separators + lastPos = pairStart = i + 1; + continue; + } + + if (lastPos < i) + buf += qs.slice(lastPos, i); + if (encoded) + buf = querystring.unescape(buf); + out.push(buf); + + // If `buf` is the key, add an empty value. + if (!seenSep) + out.push(''); + + seenSep = false; + buf = ''; + encoded = false; + encodeCheck = 0; + lastPos = pairStart = i + 1; + continue; + } + + // Try matching key/value separator (e.g. '=') if we haven't already + if (!seenSep && code === 61/*=*/) { + // Key/value separator match! + if (lastPos < i) + buf += qs.slice(lastPos, i); + if (encoded) + buf = querystring.unescape(buf); + out.push(buf); + + seenSep = true; + buf = ''; + encoded = false; + encodeCheck = 0; + lastPos = i + 1; + continue; + } + + // Handle + and percent decoding. + if (code === 43/*+*/) { + if (lastPos < i) + buf += qs.slice(lastPos, i); + buf += ' '; + lastPos = i + 1; + } else if (!encoded) { + // Try to match an (valid) encoded byte (once) to minimize unnecessary + // calls to string decoding functions + if (code === 37/*%*/) { + encodeCheck = 1; + } else if (encodeCheck > 0) { + // eslint-disable-next-line no-extra-boolean-cast + if (!!isHexTable[code]) { + if (++encodeCheck === 3) + encoded = true; + } else { + encodeCheck = 0; + } + } + } + } + + // Deal with any leftover key or value data + + // There is a trailing &. No more processing is needed. + if (pairStart === i) + return out; + + if (lastPos < i) + buf += qs.slice(lastPos, i); + if (encoded) + buf = querystring.unescape(buf); + out.push(buf); + + // If `buf` is the key, add an empty value. + if (!seenSep) + out.push(''); + + return out; +} + +// Adapted from querystring's implementation. +// Ref: https://url.spec.whatwg.org/#concept-urlencoded-byte-serializer +const noEscape = [ +//0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x00 - 0x0F + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x10 - 0x1F + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, // 0x20 - 0x2F + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 0x30 - 0x3F + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x40 - 0x4F + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, // 0x50 - 0x5F + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60 - 0x6F + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0 // 0x70 - 0x7F +]; + +// Special version of hexTable that uses `+` for U+0020 SPACE. +const paramHexTable = hexTable.slice(); +paramHexTable[0x20] = '+'; + +function escapeParam(str) { + const len = str.length; + if (len === 0) + return ''; + + var out = ''; + var lastPos = 0; + + for (var i = 0; i < len; i++) { + var c = str.charCodeAt(i); + + // ASCII + if (c < 0x80) { + if (noEscape[c] === 1) + continue; + if (lastPos < i) + out += str.slice(lastPos, i); + lastPos = i + 1; + out += paramHexTable[c]; + continue; + } + + if (lastPos < i) + out += str.slice(lastPos, i); + + // Multi-byte characters ... + if (c < 0x800) { + lastPos = i + 1; + out += paramHexTable[0xC0 | (c >> 6)] + + paramHexTable[0x80 | (c & 0x3F)]; + continue; + } + if (c < 0xD800 || c >= 0xE000) { + lastPos = i + 1; + out += paramHexTable[0xE0 | (c >> 12)] + + paramHexTable[0x80 | ((c >> 6) & 0x3F)] + + paramHexTable[0x80 | (c & 0x3F)]; + continue; + } + // Surrogate pair + ++i; + var c2; + if (i < len) + c2 = str.charCodeAt(i) & 0x3FF; + else { + // This branch should never happen because all URLSearchParams entries + // should already be converted to USVString. But, included for + // completion's sake anyway. + c2 = 0; + } + lastPos = i + 1; + c = 0x10000 + (((c & 0x3FF) << 10) | c2); + out += paramHexTable[0xF0 | (c >> 18)] + + paramHexTable[0x80 | ((c >> 12) & 0x3F)] + + paramHexTable[0x80 | ((c >> 6) & 0x3F)] + + paramHexTable[0x80 | (c & 0x3F)]; + } + if (lastPos === 0) + return str; + if (lastPos < len) + return out + str.slice(lastPos); + return out; +} + +// application/x-www-form-urlencoded serializer +// Ref: https://url.spec.whatwg.org/#concept-urlencoded-serializer +function serializeParams(array) { + const len = array.length; + if (len === 0) + return ''; + + var output = `${escapeParam(array[0])}=${escapeParam(array[1])}`; + for (var i = 2; i < len; i += 2) + output += `&${escapeParam(array[i])}=${escapeParam(array[i + 1])}`; + return output; +} + +// Mainly to mitigate func-name-matching ESLint rule +function defineIDLClass(proto, classStr, obj) { + // https://heycam.github.io/webidl/#dfn-class-string + Object.defineProperty(proto, Symbol.toStringTag, { + writable: false, + enumerable: false, + configurable: true, + value: classStr + }); + + // https://heycam.github.io/webidl/#es-operations + for (const key of Object.keys(obj)) { + Object.defineProperty(proto, key, { + writable: true, + enumerable: true, + configurable: true, + value: obj[key] + }); + } + for (const key of Object.getOwnPropertySymbols(obj)) { + Object.defineProperty(proto, key, { + writable: true, + enumerable: false, + configurable: true, + value: obj[key] + }); + } +} + +// for merge sort +function merge(out, start, mid, end, lBuffer, rBuffer) { + const sizeLeft = mid - start; + const sizeRight = end - mid; + var l, r, o; + + for (l = 0; l < sizeLeft; l++) + lBuffer[l] = out[start + l]; + for (r = 0; r < sizeRight; r++) + rBuffer[r] = out[mid + r]; + + l = 0; + r = 0; + o = start; + while (l < sizeLeft && r < sizeRight) { + if (lBuffer[l] <= rBuffer[r]) { + out[o++] = lBuffer[l++]; + out[o++] = lBuffer[l++]; + } else { + out[o++] = rBuffer[r++]; + out[o++] = rBuffer[r++]; + } + } + while (l < sizeLeft) + out[o++] = lBuffer[l++]; + while (r < sizeRight) + out[o++] = rBuffer[r++]; +} + +defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', { + append(name, value) { + if (!this || !this[searchParams] || this[searchParams][searchParams]) { + throw new TypeError('Value of "this" must be of type URLSearchParams'); + } + if (arguments.length < 2) { + throw new TypeError('The "name" and "value" arguments must be specified'); + } + + name = toUSVString(name); + value = toUSVString(value); + this[searchParams].push(name, value); + update(this[context], this); + }, + + delete(name) { + if (!this || !this[searchParams] || this[searchParams][searchParams]) { + throw new TypeError('Value of "this" must be of type URLSearchParams'); + } + if (arguments.length < 1) { + throw new TypeError('The "name" argument must be specified'); + } + + const list = this[searchParams]; + name = toUSVString(name); + for (var i = 0; i < list.length;) { + const cur = list[i]; + if (cur === name) { + list.splice(i, 2); + } else { + i += 2; + } + } + update(this[context], this); + }, + + get(name) { + if (!this || !this[searchParams] || this[searchParams][searchParams]) { + throw new TypeError('Value of "this" must be of type URLSearchParams'); + } + if (arguments.length < 1) { + throw new TypeError('The "name" argument must be specified'); + } + + const list = this[searchParams]; + name = toUSVString(name); + for (var i = 0; i < list.length; i += 2) { + if (list[i] === name) { + return list[i + 1]; + } + } + return null; + }, + + getAll(name) { + if (!this || !this[searchParams] || this[searchParams][searchParams]) { + throw new TypeError('Value of "this" must be of type URLSearchParams'); + } + if (arguments.length < 1) { + throw new TypeError('The "name" argument must be specified'); + } + + const list = this[searchParams]; + const values = []; + name = toUSVString(name); + for (var i = 0; i < list.length; i += 2) { + if (list[i] === name) { + values.push(list[i + 1]); + } + } + return values; + }, + + has(name) { + if (!this || !this[searchParams] || this[searchParams][searchParams]) { + throw new TypeError('Value of "this" must be of type URLSearchParams'); + } + if (arguments.length < 1) { + throw new TypeError('The "name" argument must be specified'); + } + + const list = this[searchParams]; + name = toUSVString(name); + for (var i = 0; i < list.length; i += 2) { + if (list[i] === name) { + return true; + } + } + return false; + }, + + set(name, value) { + if (!this || !this[searchParams] || this[searchParams][searchParams]) { + throw new TypeError('Value of "this" must be of type URLSearchParams'); + } + if (arguments.length < 2) { + throw new TypeError('The "name" and "value" arguments must be specified'); + } + + const list = this[searchParams]; + name = toUSVString(name); + value = toUSVString(value); + + // If there are any name-value pairs whose name is `name`, in `list`, set + // the value of the first such name-value pair to `value` and remove the + // others. + var found = false; + for (var i = 0; i < list.length;) { + const cur = list[i]; + if (cur === name) { + if (!found) { + list[i + 1] = value; + found = true; + i += 2; + } else { + list.splice(i, 2); + } + } else { + i += 2; + } + } + + // Otherwise, append a new name-value pair whose name is `name` and value + // is `value`, to `list`. + if (!found) { + list.push(name, value); + } + + update(this[context], this); + }, + + sort() { + const a = this[searchParams]; + const len = a.length; + + if (len <= 2) { + // Nothing needs to be done. + } else if (len < 100) { + // 100 is found through testing. + // Simple stable in-place insertion sort + // Derived from v8/src/js/array.js + for (var i = 2; i < len; i += 2) { + var curKey = a[i]; + var curVal = a[i + 1]; + var j; + for (j = i - 2; j >= 0; j -= 2) { + if (a[j] > curKey) { + a[j + 2] = a[j]; + a[j + 3] = a[j + 1]; + } else { + break; + } + } + a[j + 2] = curKey; + a[j + 3] = curVal; + } + } else { + // Bottom-up iterative stable merge sort + const lBuffer = new Array(len); + const rBuffer = new Array(len); + for (var step = 2; step < len; step *= 2) { + for (var start = 0; start < len - 2; start += 2 * step) { + var mid = start + step; + var end = mid + step; + end = end < len ? end : len; + if (mid > end) + continue; + merge(a, start, mid, end, lBuffer, rBuffer); + } + } + } + + update(this[context], this); + }, + + // https://heycam.github.io/webidl/#es-iterators + // Define entries here rather than [Symbol.iterator] as the function name + // must be set to `entries`. + entries() { + if (!this || !this[searchParams] || this[searchParams][searchParams]) { + throw new TypeError('Value of "this" must be of type URLSearchParams'); + } + + return createSearchParamsIterator(this, 'key+value'); + }, + + forEach(callback, thisArg = undefined) { + if (!this || !this[searchParams] || this[searchParams][searchParams]) { + throw new TypeError('Value of "this" must be of type URLSearchParams'); + } + if (typeof callback !== 'function') { + throw new TypeError('Callback must be a function'); + } + + let list = this[searchParams]; + + var i = 0; + while (i < list.length) { + const key = list[i]; + const value = list[i + 1]; + callback.call(thisArg, value, key, this); + // in case the URL object's `search` is updated + list = this[searchParams]; + i += 2; + } + }, + + // https://heycam.github.io/webidl/#es-iterable + keys() { + if (!this || !this[searchParams] || this[searchParams][searchParams]) { + throw new TypeError('Value of "this" must be of type URLSearchParams'); + } + + return createSearchParamsIterator(this, 'key'); + }, + + values() { + if (!this || !this[searchParams] || this[searchParams][searchParams]) { + throw new TypeError('Value of "this" must be of type URLSearchParams'); + } + + return createSearchParamsIterator(this, 'value'); + }, + + // https://heycam.github.io/webidl/#es-stringifier + // https://url.spec.whatwg.org/#urlsearchparams-stringification-behavior + toString() { + if (!this || !this[searchParams] || this[searchParams][searchParams]) { + throw new TypeError('Value of "this" must be of type URLSearchParams'); + } + + return serializeParams(this[searchParams]); + } +}); + +// https://heycam.github.io/webidl/#es-iterable-entries +Object.defineProperty(URLSearchParams.prototype, Symbol.iterator, { + writable: true, + configurable: true, + value: URLSearchParams.prototype.entries +}); + +// https://heycam.github.io/webidl/#dfn-default-iterator-object +function createSearchParamsIterator(target, kind) { + const iterator = Object.create(URLSearchParamsIteratorPrototype); + iterator[context] = { + target, + kind, + index: 0 + }; + return iterator; +} + +// https://heycam.github.io/webidl/#dfn-iterator-prototype-object +const URLSearchParamsIteratorPrototype = Object.create(IteratorPrototype); + +defineIDLClass(URLSearchParamsIteratorPrototype, 'URLSearchParamsIterator', { + next() { + if (!this || + Object.getPrototypeOf(this) !== URLSearchParamsIteratorPrototype) { + throw new TypeError( + 'Value of "this" must be of type URLSearchParamsIterator'); + } + + const { + target, + kind, + index + } = this[context]; + const values = target[searchParams]; + const len = values.length; + if (index >= len) { + return { + value: undefined, + done: true + }; + } + + const name = values[index]; + const value = values[index + 1]; + this[context].index = index + 2; + + let result; + if (kind === 'key') { + result = name; + } else if (kind === 'value') { + result = value; + } else { + result = [name, value]; + } + + return { + value: result, + done: false + }; + }, + [util.inspect.custom](recurseTimes, ctx) { + if (this == null || this[context] == null || this[context].target == null) + throw new TypeError( + 'Value of "this" must be of type URLSearchParamsIterator'); + + if (typeof recurseTimes === 'number' && recurseTimes < 0) + return ctx.stylize('[Object]', 'special'); + + const innerOpts = util._extend({}, ctx); + if (recurseTimes !== null) { + innerOpts.depth = recurseTimes - 1; + } + const { + target, + kind, + index + } = this[context]; + const output = target[searchParams].slice(index).reduce((prev, cur, i) => { + const key = i % 2 === 0; + if (kind === 'key' && key) { + prev.push(cur); + } else if (kind === 'value' && !key) { + prev.push(cur); + } else if (kind === 'key+value' && !key) { + prev.push([target[searchParams][index + i - 1], cur]); + } + return prev; + }, []); + const breakLn = util.inspect(output, innerOpts).includes('\n'); + const outputStrs = output.map((p) => util.inspect(p, innerOpts)); + let outputStr; + if (breakLn) { + outputStr = `\n ${outputStrs.join(',\n ')}`; + } else { + outputStr = ` ${outputStrs.join(', ')}`; + } + return `${this[Symbol.toStringTag]} {${outputStr} }`; + } +}); + +function domainToASCII(domain) { + if (arguments.length < 1) + throw new TypeError('The "domain" argument must be specified'); + + // toUSVString is not needed. + return _domainToASCII(`${domain}`); +} + +function domainToUnicode(domain) { + if (arguments.length < 1) + throw new TypeError('The "domain" argument must be specified'); + + // toUSVString is not needed. + return _domainToUnicode(`${domain}`); +} + +// Utility function that converts a URL object into an ordinary +// options object as expected by the http.request and https.request +// APIs. +function urlToOptions(url) { + var options = { + protocol: url.protocol, + hostname: url.hostname, + hash: url.hash, + search: url.search, + pathname: url.pathname, + path: `${url.pathname}${url.search}`, + href: url.href + }; + if (url.port !== '') { + options.port = Number(url.port); + } + if (url.username || url.password) { + options.auth = `${url.username}:${url.password}`; + } + return options; +} + +function getPathFromURLWin32(url) { + var hostname = url.hostname; + var pathname = url.pathname; + for (var n = 0; n < pathname.length; n++) { + if (pathname[n] === '%') { + var third = pathname.codePointAt(n + 2) | 0x20; + if ((pathname[n + 1] === '2' && third === 102) || // 2f 2F / + (pathname[n + 1] === '5' && third === 99)) { // 5c 5C \ + return new TypeError( + 'File URL path must not include encoded \\ or / characters'); + } + } + } + pathname = decodeURIComponent(pathname); + if (hostname !== '') { + // If hostname is set, then we have a UNC path + // Pass the hostname through domainToUnicode just in case + // it is an IDN using punycode encoding. We do not need to worry + // about percent encoding because the URL parser will have + // already taken care of that for us. Note that this only + // causes IDNs with an appropriate `xn--` prefix to be decoded. + return `//${domainToUnicode(hostname)}${pathname}`; + } else { + // Otherwise, it's a local path that requires a drive letter + var letter = pathname.codePointAt(1) | 0x20; + var sep = pathname[2]; + if (letter < 97 || letter > 122 || // a..z A..Z + (sep !== ':')) { + return new TypeError('File URL path must be absolute'); + } + return pathname.slice(1); + } +} + +function getPathFromURLPosix(url) { + if (url.hostname !== '') { + return new TypeError( + `File URL host must be "localhost" or empty on ${platform}`); + } + var pathname = url.pathname; + for (var n = 0; n < pathname.length; n++) { + if (pathname[n] === '%') { + var third = pathname.codePointAt(n + 2) | 0x20; + if (pathname[n + 1] === '2' && third === 102) { + return new TypeError( + 'File URL path must not include encoded / characters'); + } + } + } + return decodeURIComponent(pathname); +} + +function getPathFromURL(path) { + if (path == null || !path[searchParams] || + !path[searchParams][searchParams]) { + return path; + } + if (path.protocol !== 'file:') + return new TypeError('The URL must be of scheme file'); + return isWindows ? getPathFromURLWin32(path) : getPathFromURLPosix(path); +} + +// We percent-encode % character when converting from file path to URL, +// as this is the only character that won't be percent encoded by +// default URL percent encoding when pathname is set. +const percentRegEx = /%/g; +function getURLFromFilePath(filepath) { + const tmp = new URL('file://'); + if (filepath.includes('%')) + filepath = filepath.replace(percentRegEx, '%25'); + tmp.pathname = filepath; + return tmp; +} + +function NativeURL(ctx) { + this[context] = ctx; +} +NativeURL.prototype = URL.prototype; + +function constructUrl(flags, protocol, username, password, + host, port, path, query, fragment) { + var ctx = new URLContext(); + ctx.flags = flags; + ctx.scheme = protocol; + ctx.username = (flags & URL_FLAGS_HAS_USERNAME) !== 0 ? username : ''; + ctx.password = (flags & URL_FLAGS_HAS_PASSWORD) !== 0 ? password : ''; + ctx.port = port; + ctx.path = (flags & URL_FLAGS_HAS_PATH) !== 0 ? path : []; + ctx.query = query; + ctx.fragment = fragment; + ctx.host = host; + const url = new NativeURL(ctx); + url[searchParams] = new URLSearchParams(); + url[searchParams][context] = url; + initSearchParams(url[searchParams], query); + return url; +} +setURLConstructor(constructUrl); + +module.exports = { + toUSVString, + getPathFromURL, + getURLFromFilePath, + URL, + URLSearchParams, + domainToASCII, + domainToUnicode, + urlToOptions, + formatSymbol: kFormat, + searchParamsSymbol: searchParams +}; diff --git a/lib/internal/util.js b/lib/internal/util.js index 58aa011c08c6b9..f66698295f33e7 100644 --- a/lib/internal/util.js +++ b/lib/internal/util.js @@ -6,6 +6,21 @@ const prefix = `(${process.release.name}:${process.pid}) `; exports.getHiddenValue = binding.getHiddenValue; exports.setHiddenValue = binding.setHiddenValue; +exports.getConstructorOf = function getConstructorOf(obj) { + while (obj) { + var descriptor = Object.getOwnPropertyDescriptor(obj, 'constructor'); + if (descriptor !== undefined && + typeof descriptor.value === 'function' && + descriptor.value.name !== '') { + return descriptor.value; + } + + obj = Object.getPrototypeOf(obj); + } + + return null; +}; + // The `buffer` module uses this. Defining it here instead of in the public // `util` module makes it accessible without having to `require('util')` there. exports.customInspectSymbol = Symbol('util.inspect.custom'); diff --git a/lib/querystring.js b/lib/querystring.js index 5ccb5fa77b320f..668628cdc386f6 100644 --- a/lib/querystring.js +++ b/lib/querystring.js @@ -1,5 +1,6 @@ 'use strict'; +const { hexTable } = require('internal/querystring'); const QueryString = module.exports = { unescapeBuffer, // `unescape()` is a JS global, so we need to use a different local name @@ -116,10 +117,6 @@ function qsUnescape(s, decodeSpaces) { } -const hexTable = []; -for (var i = 0; i < 256; ++i) - hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase(); - // These characters do not need escaping when generating query strings: // ! - . _ ~ // ' ( ) * diff --git a/lib/url.js b/lib/url.js index f084fd372afaf9..980a2b2a1f00f2 100644 --- a/lib/url.js +++ b/lib/url.js @@ -3,12 +3,17 @@ const { toASCII } = process.binding('config').hasIntl ? process.binding('icu') : require('punycode'); -exports.parse = urlParse; -exports.resolve = urlResolve; -exports.resolveObject = urlResolveObject; -exports.format = urlFormat; +const { hexTable } = require('internal/querystring'); -exports.Url = Url; +// WHATWG URL implementation provided by internal/url +const { + URL, + URLSearchParams, + domainToASCII, + domainToUnicode +} = require('internal/url'); + +// Original url.parse() API function Url() { this.protocol = null; @@ -942,10 +947,6 @@ function spliceOne(list, index) { list.pop(); } -var hexTable = new Array(256); -for (var i = 0; i < 256; ++i) - hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase(); - // These characters do not need escaping: // ! - . _ ~ // ' ( ) * : @@ -1017,3 +1018,18 @@ function encodeAuth(str) { return out + str.slice(lastPos); return out; } + +module.exports = { + // Original API + Url, + parse: urlParse, + resolve: urlResolve, + resolveObject: urlResolveObject, + format: urlFormat, + + // WHATWG API + URL, + URLSearchParams, + domainToASCII, + domainToUnicode +}; diff --git a/lib/util.js b/lib/util.js index 2a3a5cb797f176..8c476d59e2034b 100644 --- a/lib/util.js +++ b/lib/util.js @@ -279,22 +279,6 @@ function arrayToHash(array) { } -function getConstructorOf(obj) { - while (obj) { - var descriptor = Object.getOwnPropertyDescriptor(obj, 'constructor'); - if (descriptor !== undefined && - typeof descriptor.value === 'function' && - descriptor.value.name !== '') { - return descriptor.value; - } - - obj = Object.getPrototypeOf(obj); - } - - return null; -} - - function ensureDebugIsInitialized() { if (Debug === undefined) { const runInDebugContext = require('vm').runInDebugContext; @@ -454,12 +438,12 @@ function formatValue(ctx, value, recurseTimes) { // Can't do the same for DataView because it has a non-primitive // .buffer property that we need to recurse for. if (binding.isArrayBuffer(value) || binding.isSharedArrayBuffer(value)) { - return `${getConstructorOf(value).name}` + + return `${internalUtil.getConstructorOf(value).name}` + ` { byteLength: ${formatNumber(ctx, value.byteLength)} }`; } } - var constructor = getConstructorOf(value); + var constructor = internalUtil.getConstructorOf(value); var base = '', empty = false, braces; var formatter = formatObject; diff --git a/node.gyp b/node.gyp index 1b17d991ae3244..ae05382bc7396a 100644 --- a/node.gyp +++ b/node.gyp @@ -86,9 +86,11 @@ 'lib/internal/process/stdio.js', 'lib/internal/process/warning.js', 'lib/internal/process.js', + 'lib/internal/querystring.js', 'lib/internal/readline.js', 'lib/internal/repl.js', 'lib/internal/socket_list.js', + 'lib/internal/url.js', 'lib/internal/util.js', 'lib/internal/v8_prof_polyfill.js', 'lib/internal/v8_prof_processor.js', @@ -162,6 +164,7 @@ 'src/node_main.cc', 'src/node_os.cc', 'src/node_revert.cc', + 'src/node_url.cc', 'src/node_util.cc', 'src/node_v8.cc', 'src/node_stat_watcher.cc', @@ -588,6 +591,7 @@ '<(OBJ_PATH)/node.<(OBJ_SUFFIX)', '<(OBJ_PATH)/node_buffer.<(OBJ_SUFFIX)', '<(OBJ_PATH)/node_i18n.<(OBJ_SUFFIX)', + '<(OBJ_PATH)/node_url.<(OBJ_SUFFIX)', '<(OBJ_PATH)/debug-agent.<(OBJ_SUFFIX)', '<(OBJ_PATH)/util.<(OBJ_SUFFIX)', '<(OBJ_PATH)/string_bytes.<(OBJ_SUFFIX)', @@ -611,6 +615,7 @@ 'sources': [ 'test/cctest/test_base64.cc', 'test/cctest/test_util.cc', + 'test/cctest/test_url.cc' ], 'sources!': [ diff --git a/src/env.h b/src/env.h index 940c97fe4ddbf1..c4489857161b75 100644 --- a/src/env.h +++ b/src/env.h @@ -252,6 +252,7 @@ namespace node { V(tls_wrap_constructor_template, v8::FunctionTemplate) \ V(tty_constructor_template, v8::FunctionTemplate) \ V(udp_constructor_function, v8::Function) \ + V(url_constructor_function, v8::Function) \ V(write_wrap_constructor_function, v8::Function) \ class Environment; diff --git a/src/node_url.cc b/src/node_url.cc new file mode 100644 index 00000000000000..49c005a23bc2c7 --- /dev/null +++ b/src/node_url.cc @@ -0,0 +1,2223 @@ +#include "node_url.h" +#include "node_internals.h" +#include "base-object-inl.h" +#include "node_i18n.h" + +#include +#include +#include +#include + +namespace node { + +using v8::Array; +using v8::Context; +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::HandleScope; +using v8::Integer; +using v8::Isolate; +using v8::Local; +using v8::MaybeLocal; +using v8::Null; +using v8::Object; +using v8::String; +using v8::TryCatch; +using v8::Undefined; +using v8::Value; + +#define GET(env, obj, name) \ + obj->Get(env->context(), \ + OneByteString(env->isolate(), name)).ToLocalChecked() + +#define GET_AND_SET(env, obj, name, data, flag) \ + { \ + Local val = GET(env, obj, #name); \ + if (val->IsString()) { \ + Utf8Value value(env->isolate(), val.As()); \ + data->name = *value; \ + data->flags |= flag; \ + } \ + } + +#define UTF8STRING(isolate, str) \ + String::NewFromUtf8(isolate, str.c_str(), v8::NewStringType::kNormal) \ + .ToLocalChecked() + +namespace url { + +// https://url.spec.whatwg.org/#eof-code-point +static const char kEOL = -1; + +// Used in ToUSVString(). +static const char16_t kUnicodeReplacementCharacter = 0xFFFD; + +// https://url.spec.whatwg.org/#concept-host +union url_host_value { + std::string domain; + uint32_t ipv4; + uint16_t ipv6[8]; + std::string opaque; + ~url_host_value() {} +}; + +enum url_host_type { + HOST_TYPE_FAILED = -1, + HOST_TYPE_DOMAIN = 0, + HOST_TYPE_IPV4 = 1, + HOST_TYPE_IPV6 = 2, + HOST_TYPE_OPAQUE = 3, +}; + +struct url_host { + url_host_value value; + enum url_host_type type; +}; + +#define ARGS(XX) \ + XX(ARG_FLAGS) \ + XX(ARG_PROTOCOL) \ + XX(ARG_USERNAME) \ + XX(ARG_PASSWORD) \ + XX(ARG_HOST) \ + XX(ARG_PORT) \ + XX(ARG_PATH) \ + XX(ARG_QUERY) \ + XX(ARG_FRAGMENT) + +#define ERR_ARGS(XX) \ + XX(ERR_ARG_FLAGS) \ + XX(ERR_ARG_INPUT) \ + +enum url_cb_args { +#define XX(name) name, + ARGS(XX) +#undef XX +}; + +enum url_error_cb_args { +#define XX(name) name, + ERR_ARGS(XX) +#undef XX +}; + +#define CHAR_TEST(bits, name, expr) \ + template \ + static inline bool name(const T ch) { \ + static_assert(sizeof(ch) >= (bits) / 8, \ + "Character must be wider than " #bits " bits"); \ + return (expr); \ + } + +#define TWO_CHAR_STRING_TEST(bits, name, expr) \ + template \ + static inline bool name(const T ch1, const T ch2) { \ + static_assert(sizeof(ch1) >= (bits) / 8, \ + "Character must be wider than " #bits " bits"); \ + return (expr); \ + } \ + template \ + static inline bool name(const std::basic_string& str) { \ + static_assert(sizeof(str[0]) >= (bits) / 8, \ + "Character must be wider than " #bits " bits"); \ + return str.length() >= 2 && name(str[0], str[1]); \ + } + +// https://infra.spec.whatwg.org/#ascii-tab-or-newline +CHAR_TEST(8, IsASCIITabOrNewline, (ch == '\t' || ch == '\n' || ch == '\r')) + +// https://infra.spec.whatwg.org/#c0-control-or-space +CHAR_TEST(8, IsC0ControlOrSpace, (ch >= '\0' && ch <= ' ')) + +// https://infra.spec.whatwg.org/#ascii-digit +CHAR_TEST(8, IsASCIIDigit, (ch >= '0' && ch <= '9')) + +// https://infra.spec.whatwg.org/#ascii-hex-digit +CHAR_TEST(8, IsASCIIHexDigit, (IsASCIIDigit(ch) || + (ch >= 'A' && ch <= 'F') || + (ch >= 'a' && ch <= 'f'))) + +// https://infra.spec.whatwg.org/#ascii-alpha +CHAR_TEST(8, IsASCIIAlpha, ((ch >= 'A' && ch <= 'Z') || + (ch >= 'a' && ch <= 'z'))) + +// https://infra.spec.whatwg.org/#ascii-alphanumeric +CHAR_TEST(8, IsASCIIAlphanumeric, (IsASCIIDigit(ch) || IsASCIIAlpha(ch))) + +// https://infra.spec.whatwg.org/#ascii-lowercase +template +static inline T ASCIILowercase(T ch) { + return IsASCIIAlpha(ch) ? (ch | 0x20) : ch; +} + +// https://url.spec.whatwg.org/#forbidden-host-code-point +CHAR_TEST(8, IsForbiddenHostCodePoint, + ch == '\0' || ch == '\t' || ch == '\n' || ch == '\r' || + ch == ' ' || ch == '#' || ch == '%' || ch == '/' || + ch == ':' || ch == '?' || ch == '@' || ch == '[' || + ch == '\\' || ch == ']') + +// https://url.spec.whatwg.org/#windows-drive-letter +TWO_CHAR_STRING_TEST(8, IsWindowsDriveLetter, + (IsASCIIAlpha(ch1) && (ch2 == ':' || ch2 == '|'))) + +// https://url.spec.whatwg.org/#normalized-windows-drive-letter +TWO_CHAR_STRING_TEST(8, IsNormalizedWindowsDriveLetter, + (IsASCIIAlpha(ch1) && ch2 == ':')) + +// If a UTF-16 character is a low/trailing surrogate. +CHAR_TEST(16, IsUnicodeTrail, (ch & 0xFC00) == 0xDC00) + +// If a UTF-16 character is a surrogate. +CHAR_TEST(16, IsUnicodeSurrogate, (ch & 0xF800) == 0xD800) + +// If a UTF-16 surrogate is a low/trailing one. +CHAR_TEST(16, IsUnicodeSurrogateTrail, (ch & 0x400) != 0) + +#undef CHAR_TEST +#undef TWO_CHAR_STRING_TEST + +static const char* hex[256] = { + "%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07", + "%08", "%09", "%0A", "%0B", "%0C", "%0D", "%0E", "%0F", + "%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17", + "%18", "%19", "%1A", "%1B", "%1C", "%1D", "%1E", "%1F", + "%20", "%21", "%22", "%23", "%24", "%25", "%26", "%27", + "%28", "%29", "%2A", "%2B", "%2C", "%2D", "%2E", "%2F", + "%30", "%31", "%32", "%33", "%34", "%35", "%36", "%37", + "%38", "%39", "%3A", "%3B", "%3C", "%3D", "%3E", "%3F", + "%40", "%41", "%42", "%43", "%44", "%45", "%46", "%47", + "%48", "%49", "%4A", "%4B", "%4C", "%4D", "%4E", "%4F", + "%50", "%51", "%52", "%53", "%54", "%55", "%56", "%57", + "%58", "%59", "%5A", "%5B", "%5C", "%5D", "%5E", "%5F", + "%60", "%61", "%62", "%63", "%64", "%65", "%66", "%67", + "%68", "%69", "%6A", "%6B", "%6C", "%6D", "%6E", "%6F", + "%70", "%71", "%72", "%73", "%74", "%75", "%76", "%77", + "%78", "%79", "%7A", "%7B", "%7C", "%7D", "%7E", "%7F", + "%80", "%81", "%82", "%83", "%84", "%85", "%86", "%87", + "%88", "%89", "%8A", "%8B", "%8C", "%8D", "%8E", "%8F", + "%90", "%91", "%92", "%93", "%94", "%95", "%96", "%97", + "%98", "%99", "%9A", "%9B", "%9C", "%9D", "%9E", "%9F", + "%A0", "%A1", "%A2", "%A3", "%A4", "%A5", "%A6", "%A7", + "%A8", "%A9", "%AA", "%AB", "%AC", "%AD", "%AE", "%AF", + "%B0", "%B1", "%B2", "%B3", "%B4", "%B5", "%B6", "%B7", + "%B8", "%B9", "%BA", "%BB", "%BC", "%BD", "%BE", "%BF", + "%C0", "%C1", "%C2", "%C3", "%C4", "%C5", "%C6", "%C7", + "%C8", "%C9", "%CA", "%CB", "%CC", "%CD", "%CE", "%CF", + "%D0", "%D1", "%D2", "%D3", "%D4", "%D5", "%D6", "%D7", + "%D8", "%D9", "%DA", "%DB", "%DC", "%DD", "%DE", "%DF", + "%E0", "%E1", "%E2", "%E3", "%E4", "%E5", "%E6", "%E7", + "%E8", "%E9", "%EA", "%EB", "%EC", "%ED", "%EE", "%EF", + "%F0", "%F1", "%F2", "%F3", "%F4", "%F5", "%F6", "%F7", + "%F8", "%F9", "%FA", "%FB", "%FC", "%FD", "%FE", "%FF" +}; + +static const uint8_t C0_CONTROL_ENCODE_SET[32] = { + // 00 01 02 03 04 05 06 07 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 08 09 0A 0B 0C 0D 0E 0F + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 10 11 12 13 14 15 16 17 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 18 19 1A 1B 1C 1D 1E 1F + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 20 21 22 23 24 25 26 27 + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 28 29 2A 2B 2C 2D 2E 2F + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 30 31 32 33 34 35 36 37 + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 38 39 3A 3B 3C 3D 3E 3F + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 40 41 42 43 44 45 46 47 + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 48 49 4A 4B 4C 4D 4E 4F + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 50 51 52 53 54 55 56 57 + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 58 59 5A 5B 5C 5D 5E 5F + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 60 61 62 63 64 65 66 67 + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 68 69 6A 6B 6C 6D 6E 6F + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 70 71 72 73 74 75 76 77 + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 78 79 7A 7B 7C 7D 7E 7F + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x80, + // 80 81 82 83 84 85 86 87 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 88 89 8A 8B 8C 8D 8E 8F + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 90 91 92 93 94 95 96 97 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 98 99 9A 9B 9C 9D 9E 9F + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // A0 A1 A2 A3 A4 A5 A6 A7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // A8 A9 AA AB AC AD AE AF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // B0 B1 B2 B3 B4 B5 B6 B7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // B8 B9 BA BB BC BD BE BF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // C0 C1 C2 C3 C4 C5 C6 C7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // C8 C9 CA CB CC CD CE CF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // D0 D1 D2 D3 D4 D5 D6 D7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // D8 D9 DA DB DC DD DE DF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // E0 E1 E2 E3 E4 E5 E6 E7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // E8 E9 EA EB EC ED EE EF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // F0 F1 F2 F3 F4 F5 F6 F7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // F8 F9 FA FB FC FD FE FF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80 +}; + +static const uint8_t PATH_ENCODE_SET[32] = { + // 00 01 02 03 04 05 06 07 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 08 09 0A 0B 0C 0D 0E 0F + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 10 11 12 13 14 15 16 17 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 18 19 1A 1B 1C 1D 1E 1F + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 20 21 22 23 24 25 26 27 + 0x01 | 0x00 | 0x04 | 0x08 | 0x00 | 0x00 | 0x00 | 0x00, + // 28 29 2A 2B 2C 2D 2E 2F + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 30 31 32 33 34 35 36 37 + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 38 39 3A 3B 3C 3D 3E 3F + 0x00 | 0x00 | 0x00 | 0x00 | 0x10 | 0x00 | 0x40 | 0x80, + // 40 41 42 43 44 45 46 47 + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 48 49 4A 4B 4C 4D 4E 4F + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 50 51 52 53 54 55 56 57 + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 58 59 5A 5B 5C 5D 5E 5F + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 60 61 62 63 64 65 66 67 + 0x01 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 68 69 6A 6B 6C 6D 6E 6F + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 70 71 72 73 74 75 76 77 + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 78 79 7A 7B 7C 7D 7E 7F + 0x00 | 0x00 | 0x00 | 0x08 | 0x00 | 0x20 | 0x00 | 0x80, + // 80 81 82 83 84 85 86 87 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 88 89 8A 8B 8C 8D 8E 8F + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 90 91 92 93 94 95 96 97 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 98 99 9A 9B 9C 9D 9E 9F + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // A0 A1 A2 A3 A4 A5 A6 A7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // A8 A9 AA AB AC AD AE AF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // B0 B1 B2 B3 B4 B5 B6 B7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // B8 B9 BA BB BC BD BE BF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // C0 C1 C2 C3 C4 C5 C6 C7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // C8 C9 CA CB CC CD CE CF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // D0 D1 D2 D3 D4 D5 D6 D7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // D8 D9 DA DB DC DD DE DF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // E0 E1 E2 E3 E4 E5 E6 E7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // E8 E9 EA EB EC ED EE EF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // F0 F1 F2 F3 F4 F5 F6 F7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // F8 F9 FA FB FC FD FE FF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80 +}; + +static const uint8_t USERINFO_ENCODE_SET[32] = { + // 00 01 02 03 04 05 06 07 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 08 09 0A 0B 0C 0D 0E 0F + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 10 11 12 13 14 15 16 17 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 18 19 1A 1B 1C 1D 1E 1F + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 20 21 22 23 24 25 26 27 + 0x01 | 0x00 | 0x04 | 0x08 | 0x00 | 0x00 | 0x00 | 0x00, + // 28 29 2A 2B 2C 2D 2E 2F + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x80, + // 30 31 32 33 34 35 36 37 + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 38 39 3A 3B 3C 3D 3E 3F + 0x00 | 0x00 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 40 41 42 43 44 45 46 47 + 0x01 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 48 49 4A 4B 4C 4D 4E 4F + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 50 51 52 53 54 55 56 57 + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 58 59 5A 5B 5C 5D 5E 5F + 0x00 | 0x00 | 0x00 | 0x08 | 0x10 | 0x20 | 0x40 | 0x00, + // 60 61 62 63 64 65 66 67 + 0x01 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 68 69 6A 6B 6C 6D 6E 6F + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 70 71 72 73 74 75 76 77 + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 78 79 7A 7B 7C 7D 7E 7F + 0x00 | 0x00 | 0x00 | 0x08 | 0x10 | 0x20 | 0x00 | 0x80, + // 80 81 82 83 84 85 86 87 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 88 89 8A 8B 8C 8D 8E 8F + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 90 91 92 93 94 95 96 97 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 98 99 9A 9B 9C 9D 9E 9F + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // A0 A1 A2 A3 A4 A5 A6 A7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // A8 A9 AA AB AC AD AE AF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // B0 B1 B2 B3 B4 B5 B6 B7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // B8 B9 BA BB BC BD BE BF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // C0 C1 C2 C3 C4 C5 C6 C7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // C8 C9 CA CB CC CD CE CF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // D0 D1 D2 D3 D4 D5 D6 D7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // D8 D9 DA DB DC DD DE DF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // E0 E1 E2 E3 E4 E5 E6 E7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // E8 E9 EA EB EC ED EE EF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // F0 F1 F2 F3 F4 F5 F6 F7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // F8 F9 FA FB FC FD FE FF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80 +}; + +static const uint8_t QUERY_ENCODE_SET[32] = { + // 00 01 02 03 04 05 06 07 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 08 09 0A 0B 0C 0D 0E 0F + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 10 11 12 13 14 15 16 17 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 18 19 1A 1B 1C 1D 1E 1F + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 20 21 22 23 24 25 26 27 + 0x01 | 0x00 | 0x04 | 0x08 | 0x00 | 0x00 | 0x00 | 0x00, + // 28 29 2A 2B 2C 2D 2E 2F + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 30 31 32 33 34 35 36 37 + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 38 39 3A 3B 3C 3D 3E 3F + 0x00 | 0x00 | 0x00 | 0x00 | 0x10 | 0x00 | 0x40 | 0x00, + // 40 41 42 43 44 45 46 47 + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 48 49 4A 4B 4C 4D 4E 4F + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 50 51 52 53 54 55 56 57 + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 58 59 5A 5B 5C 5D 5E 5F + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 60 61 62 63 64 65 66 67 + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 68 69 6A 6B 6C 6D 6E 6F + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 70 71 72 73 74 75 76 77 + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00, + // 78 79 7A 7B 7C 7D 7E 7F + 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x80, + // 80 81 82 83 84 85 86 87 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 88 89 8A 8B 8C 8D 8E 8F + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 90 91 92 93 94 95 96 97 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // 98 99 9A 9B 9C 9D 9E 9F + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // A0 A1 A2 A3 A4 A5 A6 A7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // A8 A9 AA AB AC AD AE AF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // B0 B1 B2 B3 B4 B5 B6 B7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // B8 B9 BA BB BC BD BE BF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // C0 C1 C2 C3 C4 C5 C6 C7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // C8 C9 CA CB CC CD CE CF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // D0 D1 D2 D3 D4 D5 D6 D7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // D8 D9 DA DB DC DD DE DF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // E0 E1 E2 E3 E4 E5 E6 E7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // E8 E9 EA EB EC ED EE EF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // F0 F1 F2 F3 F4 F5 F6 F7 + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + // F8 F9 FA FB FC FD FE FF + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80 +}; + +static inline bool BitAt(const uint8_t a[], const uint8_t i) { + return !!(a[i >> 3] & (1 << (i & 7))); +} + +// Appends ch to str. If ch position in encode_set is set, the ch will +// be percent-encoded then appended. +static inline void AppendOrEscape(std::string* str, + const unsigned char ch, + const uint8_t encode_set[]) { + if (BitAt(encode_set, ch)) + *str += hex[ch]; + else + *str += ch; +} + +template +static inline unsigned hex2bin(const T ch) { + if (ch >= '0' && ch <= '9') + return ch - '0'; + if (ch >= 'A' && ch <= 'F') + return 10 + (ch - 'A'); + if (ch >= 'a' && ch <= 'f') + return 10 + (ch - 'a'); + return static_cast(-1); +} + +static inline void PercentDecode(const char* input, + size_t len, + std::string* dest) { + if (len == 0) + return; + dest->reserve(len); + const char* pointer = input; + const char* end = input + len; + + while (pointer < end) { + const char ch = pointer[0]; + const size_t remaining = end - pointer - 1; + if (ch != '%' || remaining < 2 || + (ch == '%' && + (!IsASCIIHexDigit(pointer[1]) || + !IsASCIIHexDigit(pointer[2])))) { + *dest += ch; + pointer++; + continue; + } else { + unsigned a = hex2bin(pointer[1]); + unsigned b = hex2bin(pointer[2]); + char c = static_cast(a * 16 + b); + *dest += c; + pointer += 3; + } + } +} + +#define SPECIALS(XX) \ + XX("ftp:", 21) \ + XX("file:", -1) \ + XX("gopher:", 70) \ + XX("http:", 80) \ + XX("https:", 443) \ + XX("ws:", 80) \ + XX("wss:", 443) + +static inline bool IsSpecial(std::string scheme) { +#define XX(name, _) if (scheme == name) return true; + SPECIALS(XX); +#undef XX + return false; +} + +// https://url.spec.whatwg.org/#start-with-a-windows-drive-letter +static inline bool StartsWithWindowsDriveLetter(const char* p, + const char* end) { + const size_t length = end - p; + return length >= 2 && + IsWindowsDriveLetter(p[0], p[1]) && + (length == 2 || + p[2] == '/' || + p[2] == '\\' || + p[2] == '?' || + p[2] == '#'); +} + +static inline int NormalizePort(std::string scheme, int p) { +#define XX(name, port) if (scheme == name && p == port) return -1; + SPECIALS(XX); +#undef XX + return p; +} + +#if defined(NODE_HAVE_I18N_SUPPORT) +static inline bool ToUnicode(const std::string& input, std::string* output) { + MaybeStackBuffer buf; + if (i18n::ToUnicode(&buf, input.c_str(), input.length()) < 0) + return false; + output->assign(*buf, buf.length()); + return true; +} + +static inline bool ToASCII(const std::string& input, std::string* output) { + MaybeStackBuffer buf; + if (i18n::ToASCII(&buf, input.c_str(), input.length()) < 0) + return false; + output->assign(*buf, buf.length()); + return true; +} +#else +// Intentional non-ops if ICU is not present. +static inline bool ToUnicode(const std::string& input, std::string* output) { + *output = input; + return true; +} + +static inline bool ToASCII(const std::string& input, std::string* output) { + *output = input; + return true; +} +#endif + +static url_host_type ParseIPv6Host(url_host* host, + const char* input, + size_t length) { + url_host_type type = HOST_TYPE_FAILED; + for (unsigned n = 0; n < 8; n++) + host->value.ipv6[n] = 0; + uint16_t* piece_pointer = &host->value.ipv6[0]; + uint16_t* last_piece = piece_pointer + 8; + uint16_t* compress_pointer = nullptr; + const char* pointer = input; + const char* end = pointer + length; + unsigned value, len, swaps, numbers_seen; + char ch = pointer < end ? pointer[0] : kEOL; + if (ch == ':') { + if (length < 2 || pointer[1] != ':') + goto end; + pointer += 2; + ch = pointer < end ? pointer[0] : kEOL; + piece_pointer++; + compress_pointer = piece_pointer; + } + while (ch != kEOL) { + if (piece_pointer > last_piece) + goto end; + if (ch == ':') { + if (compress_pointer != nullptr) + goto end; + pointer++; + ch = pointer < end ? pointer[0] : kEOL; + piece_pointer++; + compress_pointer = piece_pointer; + continue; + } + value = 0; + len = 0; + while (len < 4 && IsASCIIHexDigit(ch)) { + value = value * 0x10 + hex2bin(ch); + pointer++; + ch = pointer < end ? pointer[0] : kEOL; + len++; + } + switch (ch) { + case '.': + if (len == 0) + goto end; + pointer -= len; + ch = pointer < end ? pointer[0] : kEOL; + if (piece_pointer > last_piece - 2) + goto end; + numbers_seen = 0; + while (ch != kEOL) { + value = 0xffffffff; + if (numbers_seen > 0) { + if (ch == '.' && numbers_seen < 4) { + pointer++; + ch = pointer < end ? pointer[0] : kEOL; + } else { + goto end; + } + } + if (!IsASCIIDigit(ch)) + goto end; + while (IsASCIIDigit(ch)) { + unsigned number = ch - '0'; + if (value == 0xffffffff) { + value = number; + } else if (value == 0) { + goto end; + } else { + value = value * 10 + number; + } + if (value > 255) + goto end; + pointer++; + ch = pointer < end ? pointer[0] : kEOL; + } + *piece_pointer = *piece_pointer * 0x100 + value; + numbers_seen++; + if (numbers_seen == 2 || numbers_seen == 4) + piece_pointer++; + } + if (numbers_seen != 4) + goto end; + continue; + case ':': + pointer++; + ch = pointer < end ? pointer[0] : kEOL; + if (ch == kEOL) + goto end; + break; + case kEOL: + break; + default: + goto end; + } + *piece_pointer = value; + piece_pointer++; + } + + if (compress_pointer != nullptr) { + swaps = piece_pointer - compress_pointer; + piece_pointer = last_piece - 1; + while (piece_pointer != &host->value.ipv6[0] && swaps > 0) { + uint16_t temp = *piece_pointer; + uint16_t* swap_piece = compress_pointer + swaps - 1; + *piece_pointer = *swap_piece; + *swap_piece = temp; + piece_pointer--; + swaps--; + } + } else if (compress_pointer == nullptr && + piece_pointer != last_piece) { + goto end; + } + type = HOST_TYPE_IPV6; + end: + host->type = type; + return type; +} + +static inline int64_t ParseNumber(const char* start, const char* end) { + unsigned R = 10; + if (end - start >= 2 && start[0] == '0' && (start[1] | 0x20) == 'x') { + start += 2; + R = 16; + } + if (end - start == 0) { + return 0; + } else if (R == 10 && end - start > 1 && start[0] == '0') { + start++; + R = 8; + } + const char* p = start; + + while (p < end) { + const char ch = p[0]; + switch (R) { + case 8: + if (ch < '0' || ch > '7') + return -1; + break; + case 10: + if (!IsASCIIDigit(ch)) + return -1; + break; + case 16: + if (!IsASCIIHexDigit(ch)) + return -1; + break; + } + p++; + } + return strtoll(start, NULL, R); +} + +static url_host_type ParseIPv4Host(url_host* host, + const char* input, + size_t length) { + url_host_type type = HOST_TYPE_DOMAIN; + const char* pointer = input; + const char* mark = input; + const char* end = pointer + length; + int parts = 0; + uint32_t val = 0; + uint64_t numbers[4]; + int tooBigNumbers = 0; + if (length == 0) + goto end; + + while (pointer <= end) { + const char ch = pointer < end ? pointer[0] : kEOL; + const int remaining = end - pointer - 1; + if (ch == '.' || ch == kEOL) { + if (++parts > 4) + goto end; + if (pointer == mark) + goto end; + int64_t n = ParseNumber(mark, pointer); + if (n < 0) + goto end; + + if (n > 255) { + tooBigNumbers++; + } + numbers[parts - 1] = n; + mark = pointer + 1; + if (ch == '.' && remaining == 0) + break; + } + pointer++; + } + CHECK_GT(parts, 0); + + // If any but the last item in numbers is greater than 255, return failure. + // If the last item in numbers is greater than or equal to + // 256^(5 - the number of items in numbers), return failure. + if (tooBigNumbers > 1 || + (tooBigNumbers == 1 && numbers[parts - 1] <= 255) || + numbers[parts - 1] >= pow(256, static_cast(5 - parts))) { + type = HOST_TYPE_FAILED; + goto end; + } + + type = HOST_TYPE_IPV4; + val = numbers[parts - 1]; + for (int n = 0; n < parts - 1; n++) { + double b = 3 - n; + val += numbers[n] * pow(256, b); + } + + host->value.ipv4 = val; + end: + host->type = type; + return type; +} + +static url_host_type ParseOpaqueHost(url_host* host, + const char* input, + size_t length) { + url_host_type type = HOST_TYPE_OPAQUE; + std::string output; + output.reserve(length * 3); + for (size_t i = 0; i < length; i++) { + const char ch = input[i]; + if (ch != '%' && IsForbiddenHostCodePoint(ch)) { + type = HOST_TYPE_FAILED; + goto end; + } else { + AppendOrEscape(&output, ch, C0_CONTROL_ENCODE_SET); + } + } + + host->value.opaque = output; + end: + host->type = type; + return type; +} + +static url_host_type ParseHost(url_host* host, + const char* input, + size_t length, + bool is_special, + bool unicode = false) { + url_host_type type = HOST_TYPE_FAILED; + const char* pointer = input; + std::string decoded; + + if (length == 0) + goto end; + + if (pointer[0] == '[') { + if (pointer[length - 1] != ']') + goto end; + return ParseIPv6Host(host, ++pointer, length - 2); + } + + if (!is_special) + return ParseOpaqueHost(host, input, length); + + // First, we have to percent decode + PercentDecode(input, length, &decoded); + + // Then we have to punycode toASCII + if (!ToASCII(decoded, &decoded)) + goto end; + + // If any of the following characters are still present, we have to fail + for (size_t n = 0; n < decoded.size(); n++) { + const char ch = decoded[n]; + if (IsForbiddenHostCodePoint(ch)) { + goto end; + } + } + + // Check to see if it's an IPv4 IP address + type = ParseIPv4Host(host, decoded.c_str(), decoded.length()); + if (type == HOST_TYPE_IPV4 || type == HOST_TYPE_FAILED) + goto end; + + // If the unicode flag is set, run the result through punycode ToUnicode + if (unicode && !ToUnicode(decoded, &decoded)) + goto end; + + // It's not an IPv4 or IPv6 address, it must be a domain + type = HOST_TYPE_DOMAIN; + host->value.domain = decoded; + + end: + host->type = type; + return type; +} + +// Locates the longest sequence of 0 segments in an IPv6 address +// in order to use the :: compression when serializing +static inline uint16_t* FindLongestZeroSequence(uint16_t* values, + size_t len) { + uint16_t* start = values; + uint16_t* end = start + len; + uint16_t* result = nullptr; + + uint16_t* current = nullptr; + unsigned counter = 0, longest = 1; + + while (start < end) { + if (*start == 0) { + if (current == nullptr) + current = start; + counter++; + } else { + if (counter > longest) { + longest = counter; + result = current; + } + counter = 0; + current = nullptr; + } + start++; + } + if (counter > longest) + result = current; + return result; +} + +static url_host_type WriteHost(url_host* host, std::string* dest) { + dest->clear(); + switch (host->type) { + case HOST_TYPE_DOMAIN: + *dest = host->value.domain; + break; + case HOST_TYPE_IPV4: { + dest->reserve(15); + uint32_t value = host->value.ipv4; + for (int n = 0; n < 4; n++) { + char buf[4]; + char* buffer = buf; + snprintf(buffer, sizeof(buf), "%d", value % 256); + dest->insert(0, buf); + if (n < 3) + dest->insert(0, 1, '.'); + value /= 256; + } + break; + } + case HOST_TYPE_IPV6: { + dest->reserve(41); + *dest+= '['; + uint16_t* start = &host->value.ipv6[0]; + uint16_t* compress_pointer = + FindLongestZeroSequence(start, 8); + bool ignore0 = false; + for (int n = 0; n <= 7; n++) { + uint16_t* piece = &host->value.ipv6[n]; + if (ignore0 && *piece == 0) + continue; + else if (ignore0) + ignore0 = false; + if (compress_pointer == piece) { + *dest += n == 0 ? "::" : ":"; + ignore0 = true; + continue; + } + char buf[5]; + char* buffer = buf; + snprintf(buffer, sizeof(buf), "%x", *piece); + *dest += buf; + if (n < 7) + *dest += ':'; + } + *dest += ']'; + break; + } + case HOST_TYPE_OPAQUE: + *dest = host->value.opaque; + break; + case HOST_TYPE_FAILED: + break; + } + return host->type; +} + +static bool ParseHost(std::string* input, + std::string* output, + bool is_special, + bool unicode = false) { + if (input->length() == 0) { + output->clear(); + return true; + } + url_host host{{""}, HOST_TYPE_DOMAIN}; + ParseHost(&host, input->c_str(), input->length(), is_special, unicode); + if (host.type == HOST_TYPE_FAILED) + return false; + WriteHost(&host, output); + return true; +} + +static inline void Copy(Environment* env, + Local ary, + std::vector* vec) { + const int32_t len = ary->Length(); + if (len == 0) + return; // nothing to copy + vec->reserve(len); + for (int32_t n = 0; n < len; n++) { + Local val = ary->Get(env->context(), n).ToLocalChecked(); + if (val->IsString()) { + Utf8Value value(env->isolate(), val.As()); + vec->push_back(std::string(*value, value.length())); + } + } +} + +static inline Local Copy(Environment* env, + const std::vector& vec) { + Isolate* isolate = env->isolate(); + Local ary = Array::New(isolate, vec.size()); + for (size_t n = 0; n < vec.size(); n++) + ary->Set(env->context(), n, UTF8STRING(isolate, vec[n])).FromJust(); + return ary; +} + +static inline void HarvestBase(Environment* env, + struct url_data* base, + Local base_obj) { + Local context = env->context(); + Local flags = GET(env, base_obj, "flags"); + if (flags->IsInt32()) + base->flags = flags->Int32Value(context).FromJust(); + + Local scheme = GET(env, base_obj, "scheme"); + base->scheme = Utf8Value(env->isolate(), scheme).out(); + + GET_AND_SET(env, base_obj, username, base, URL_FLAGS_HAS_USERNAME); + GET_AND_SET(env, base_obj, password, base, URL_FLAGS_HAS_PASSWORD); + GET_AND_SET(env, base_obj, host, base, URL_FLAGS_HAS_HOST); + GET_AND_SET(env, base_obj, query, base, URL_FLAGS_HAS_QUERY); + GET_AND_SET(env, base_obj, fragment, base, URL_FLAGS_HAS_FRAGMENT); + Local port = GET(env, base_obj, "port"); + if (port->IsInt32()) + base->port = port->Int32Value(context).FromJust(); + Local path = GET(env, base_obj, "path"); + if (path->IsArray()) { + base->flags |= URL_FLAGS_HAS_PATH; + Copy(env, path.As(), &(base->path)); + } +} + +static inline void HarvestContext(Environment* env, + struct url_data* context, + Local context_obj) { + Local flags = GET(env, context_obj, "flags"); + if (flags->IsInt32()) { + int32_t _flags = flags->Int32Value(env->context()).FromJust(); + if (_flags & URL_FLAGS_SPECIAL) + context->flags |= URL_FLAGS_SPECIAL; + if (_flags & URL_FLAGS_CANNOT_BE_BASE) + context->flags |= URL_FLAGS_CANNOT_BE_BASE; + if (_flags & URL_FLAGS_HAS_USERNAME) + context->flags |= URL_FLAGS_HAS_USERNAME; + if (_flags & URL_FLAGS_HAS_PASSWORD) + context->flags |= URL_FLAGS_HAS_PASSWORD; + if (_flags & URL_FLAGS_HAS_HOST) + context->flags |= URL_FLAGS_HAS_HOST; + } + Local scheme = GET(env, context_obj, "scheme"); + if (scheme->IsString()) { + Utf8Value value(env->isolate(), scheme); + context->scheme.assign(*value, value.length()); + } + Local port = GET(env, context_obj, "port"); + if (port->IsInt32()) + context->port = port->Int32Value(env->context()).FromJust(); + if (context->flags & URL_FLAGS_HAS_USERNAME) { + Local username = GET(env, context_obj, "username"); + CHECK(username->IsString()); + Utf8Value value(env->isolate(), username); + context->username.assign(*value, value.length()); + } + if (context->flags & URL_FLAGS_HAS_PASSWORD) { + Local password = GET(env, context_obj, "password"); + CHECK(password->IsString()); + Utf8Value value(env->isolate(), password); + context->password.assign(*value, value.length()); + } + Local host = GET(env, context_obj, "host"); + if (host->IsString()) { + Utf8Value value(env->isolate(), host); + context->host.assign(*value, value.length()); + } +} + +// Single dot segment can be ".", "%2e", or "%2E" +static inline bool IsSingleDotSegment(std::string str) { + switch (str.size()) { + case 1: + return str == "."; + case 3: + return str[0] == '%' && + str[1] == '2' && + ASCIILowercase(str[2]) == 'e'; + default: + return false; + } +} + +// Double dot segment can be: +// "..", ".%2e", ".%2E", "%2e.", "%2E.", +// "%2e%2e", "%2E%2E", "%2e%2E", or "%2E%2e" +static inline bool IsDoubleDotSegment(std::string str) { + switch (str.size()) { + case 2: + return str == ".."; + case 4: + if (str[0] != '.' && str[0] != '%') + return false; + return ((str[0] == '.' && + str[1] == '%' && + str[2] == '2' && + ASCIILowercase(str[3]) == 'e') || + (str[0] == '%' && + str[1] == '2' && + ASCIILowercase(str[2]) == 'e' && + str[3] == '.')); + case 6: + return (str[0] == '%' && + str[1] == '2' && + ASCIILowercase(str[2]) == 'e' && + str[3] == '%' && + str[4] == '2' && + ASCIILowercase(str[5]) == 'e'); + default: + return false; + } +} + +static inline void ShortenUrlPath(struct url_data* url) { + if (url->path.empty()) return; + if (url->path.size() == 1 && url->scheme == "file:" && + IsNormalizedWindowsDriveLetter(url->path[0])) return; + url->path.pop_back(); +} + +void URL::Parse(const char* input, + size_t len, + enum url_parse_state state_override, + struct url_data* url, + bool has_url, + const struct url_data* base, + bool has_base) { + const char* p = input; + const char* end = input + len; + + if (!has_url) { + for (const char* ptr = p; ptr < end; ptr++) { + if (IsC0ControlOrSpace(*ptr)) + p++; + else + break; + } + for (const char* ptr = end - 1; ptr >= p; ptr--) { + if (IsC0ControlOrSpace(*ptr)) + end--; + else + break; + } + len = end - p; + } + + std::string whitespace_stripped; + whitespace_stripped.reserve(len); + for (const char* ptr = p; ptr < end; ptr++) + if (!IsASCIITabOrNewline(*ptr)) + whitespace_stripped += *ptr; + + input = whitespace_stripped.c_str(); + len = whitespace_stripped.size(); + p = input; + end = input + len; + + bool atflag = false; + bool sbflag = false; + bool uflag = false; + + std::string buffer; + url->scheme.reserve(len); + url->username.reserve(len); + url->password.reserve(len); + url->host.reserve(len); + url->path.reserve(len); + url->query.reserve(len); + url->fragment.reserve(len); + buffer.reserve(len); + + // Set the initial parse state. + const bool has_state_override = state_override != kUnknownState; + enum url_parse_state state = has_state_override ? state_override : + kSchemeStart; + + if (state < kSchemeStart || state > kFragment) { + url->flags |= URL_FLAGS_INVALID_PARSE_STATE; + return; + } + + while (p <= end) { + const char ch = p < end ? p[0] : kEOL; + bool special = (url->flags & URL_FLAGS_SPECIAL); + bool cannot_be_base; + const bool special_back_slash = (special && ch == '\\'); + + switch (state) { + case kSchemeStart: + if (IsASCIIAlpha(ch)) { + buffer += ASCIILowercase(ch); + state = kScheme; + } else if (!has_state_override) { + state = kNoScheme; + continue; + } else { + url->flags |= URL_FLAGS_FAILED; + return; + } + break; + case kScheme: + if (IsASCIIAlphanumeric(ch) || ch == '+' || ch == '-' || ch == '.') { + buffer += ASCIILowercase(ch); + } else if (ch == ':' || (has_state_override && ch == kEOL)) { + if (has_state_override && buffer.size() == 0) { + url->flags |= URL_FLAGS_TERMINATED; + return; + } + buffer += ':'; + + bool new_is_special = IsSpecial(buffer); + + if (has_state_override) { + if ((special != new_is_special) || + ((buffer == "file:") && + ((url->flags & URL_FLAGS_HAS_USERNAME) || + (url->flags & URL_FLAGS_HAS_PASSWORD) || + (url->port != -1)))) { + url->flags |= URL_FLAGS_TERMINATED; + return; + } + + // File scheme && (host == empty or null) check left to JS-land + // as it can be done before even entering C++ binding. + } + + url->scheme = buffer; + url->port = NormalizePort(url->scheme, url->port); + if (new_is_special) { + url->flags |= URL_FLAGS_SPECIAL; + special = true; + } else { + url->flags &= ~URL_FLAGS_SPECIAL; + special = false; + } + buffer.clear(); + if (has_state_override) + return; + if (url->scheme == "file:") { + state = kFile; + } else if (special && + has_base && + url->scheme == base->scheme) { + state = kSpecialRelativeOrAuthority; + } else if (special) { + state = kSpecialAuthoritySlashes; + } else if (p[1] == '/') { + state = kPathOrAuthority; + p++; + } else { + url->flags |= URL_FLAGS_CANNOT_BE_BASE; + url->flags |= URL_FLAGS_HAS_PATH; + url->path.push_back(""); + state = kCannotBeBase; + } + } else if (!has_state_override) { + buffer.clear(); + state = kNoScheme; + p = input; + continue; + } else { + url->flags |= URL_FLAGS_FAILED; + return; + } + break; + case kNoScheme: + cannot_be_base = has_base && (base->flags & URL_FLAGS_CANNOT_BE_BASE); + if (!has_base || (cannot_be_base && ch != '#')) { + url->flags |= URL_FLAGS_FAILED; + return; + } else if (cannot_be_base && ch == '#') { + url->scheme = base->scheme; + if (IsSpecial(url->scheme)) { + url->flags |= URL_FLAGS_SPECIAL; + special = true; + } else { + url->flags &= ~URL_FLAGS_SPECIAL; + special = false; + } + if (base->flags & URL_FLAGS_HAS_PATH) { + url->flags |= URL_FLAGS_HAS_PATH; + url->path = base->path; + } + if (base->flags & URL_FLAGS_HAS_QUERY) { + url->flags |= URL_FLAGS_HAS_QUERY; + url->query = base->query; + } + if (base->flags & URL_FLAGS_HAS_FRAGMENT) { + url->flags |= URL_FLAGS_HAS_FRAGMENT; + url->fragment = base->fragment; + } + url->flags |= URL_FLAGS_CANNOT_BE_BASE; + state = kFragment; + } else if (has_base && + base->scheme != "file:") { + state = kRelative; + continue; + } else { + url->scheme = "file:"; + url->flags |= URL_FLAGS_SPECIAL; + special = true; + state = kFile; + continue; + } + break; + case kSpecialRelativeOrAuthority: + if (ch == '/' && p[1] == '/') { + state = kSpecialAuthorityIgnoreSlashes; + p++; + } else { + state = kRelative; + continue; + } + break; + case kPathOrAuthority: + if (ch == '/') { + state = kAuthority; + } else { + state = kPath; + continue; + } + break; + case kRelative: + url->scheme = base->scheme; + if (IsSpecial(url->scheme)) { + url->flags |= URL_FLAGS_SPECIAL; + special = true; + } else { + url->flags &= ~URL_FLAGS_SPECIAL; + special = false; + } + switch (ch) { + case kEOL: + if (base->flags & URL_FLAGS_HAS_USERNAME) { + url->flags |= URL_FLAGS_HAS_USERNAME; + url->username = base->username; + } + if (base->flags & URL_FLAGS_HAS_PASSWORD) { + url->flags |= URL_FLAGS_HAS_PASSWORD; + url->password = base->password; + } + if (base->flags & URL_FLAGS_HAS_HOST) { + url->flags |= URL_FLAGS_HAS_HOST; + url->host = base->host; + } + if (base->flags & URL_FLAGS_HAS_QUERY) { + url->flags |= URL_FLAGS_HAS_QUERY; + url->query = base->query; + } + if (base->flags & URL_FLAGS_HAS_PATH) { + url->flags |= URL_FLAGS_HAS_PATH; + url->path = base->path; + } + url->port = base->port; + break; + case '/': + state = kRelativeSlash; + break; + case '?': + if (base->flags & URL_FLAGS_HAS_USERNAME) { + url->flags |= URL_FLAGS_HAS_USERNAME; + url->username = base->username; + } + if (base->flags & URL_FLAGS_HAS_PASSWORD) { + url->flags |= URL_FLAGS_HAS_PASSWORD; + url->password = base->password; + } + if (base->flags & URL_FLAGS_HAS_HOST) { + url->flags |= URL_FLAGS_HAS_HOST; + url->host = base->host; + } + if (base->flags & URL_FLAGS_HAS_PATH) { + url->flags |= URL_FLAGS_HAS_PATH; + url->path = base->path; + } + url->port = base->port; + state = kQuery; + break; + case '#': + if (base->flags & URL_FLAGS_HAS_USERNAME) { + url->flags |= URL_FLAGS_HAS_USERNAME; + url->username = base->username; + } + if (base->flags & URL_FLAGS_HAS_PASSWORD) { + url->flags |= URL_FLAGS_HAS_PASSWORD; + url->password = base->password; + } + if (base->flags & URL_FLAGS_HAS_HOST) { + url->flags |= URL_FLAGS_HAS_HOST; + url->host = base->host; + } + if (base->flags & URL_FLAGS_HAS_QUERY) { + url->flags |= URL_FLAGS_HAS_QUERY; + url->query = base->query; + } + if (base->flags & URL_FLAGS_HAS_PATH) { + url->flags |= URL_FLAGS_HAS_PATH; + url->path = base->path; + } + url->port = base->port; + state = kFragment; + break; + default: + if (special_back_slash) { + state = kRelativeSlash; + } else { + if (base->flags & URL_FLAGS_HAS_USERNAME) { + url->flags |= URL_FLAGS_HAS_USERNAME; + url->username = base->username; + } + if (base->flags & URL_FLAGS_HAS_PASSWORD) { + url->flags |= URL_FLAGS_HAS_PASSWORD; + url->password = base->password; + } + if (base->flags & URL_FLAGS_HAS_HOST) { + url->flags |= URL_FLAGS_HAS_HOST; + url->host = base->host; + } + if (base->flags & URL_FLAGS_HAS_PATH) { + url->flags |= URL_FLAGS_HAS_PATH; + url->path = base->path; + ShortenUrlPath(url); + } + url->port = base->port; + state = kPath; + continue; + } + } + break; + case kRelativeSlash: + if (IsSpecial(url->scheme) && (ch == '/' || ch == '\\')) { + state = kSpecialAuthorityIgnoreSlashes; + } else if (ch == '/') { + state = kAuthority; + } else { + if (base->flags & URL_FLAGS_HAS_USERNAME) { + url->flags |= URL_FLAGS_HAS_USERNAME; + url->username = base->username; + } + if (base->flags & URL_FLAGS_HAS_PASSWORD) { + url->flags |= URL_FLAGS_HAS_PASSWORD; + url->password = base->password; + } + if (base->flags & URL_FLAGS_HAS_HOST) { + url->flags |= URL_FLAGS_HAS_HOST; + url->host = base->host; + } + url->port = base->port; + state = kPath; + continue; + } + break; + case kSpecialAuthoritySlashes: + state = kSpecialAuthorityIgnoreSlashes; + if (ch == '/' && p[1] == '/') { + p++; + } else { + continue; + } + break; + case kSpecialAuthorityIgnoreSlashes: + if (ch != '/' && ch != '\\') { + state = kAuthority; + continue; + } + break; + case kAuthority: + if (ch == '@') { + if (atflag) { + buffer.reserve(buffer.size() + 3); + buffer.insert(0, "%40"); + } + atflag = true; + const size_t blen = buffer.size(); + if (blen > 0 && buffer[0] != ':') { + url->flags |= URL_FLAGS_HAS_USERNAME; + } + for (size_t n = 0; n < blen; n++) { + const char bch = buffer[n]; + if (bch == ':') { + url->flags |= URL_FLAGS_HAS_PASSWORD; + if (!uflag) { + uflag = true; + continue; + } + } + if (uflag) { + AppendOrEscape(&url->password, bch, USERINFO_ENCODE_SET); + } else { + AppendOrEscape(&url->username, bch, USERINFO_ENCODE_SET); + } + } + buffer.clear(); + } else if (ch == kEOL || + ch == '/' || + ch == '?' || + ch == '#' || + special_back_slash) { + if (atflag && buffer.size() == 0) { + url->flags |= URL_FLAGS_FAILED; + return; + } + p -= buffer.size() + 1; + buffer.clear(); + state = kHost; + } else { + buffer += ch; + } + break; + case kHost: + case kHostname: + if (has_state_override && url->scheme == "file:") { + state = kFileHost; + continue; + } else if (ch == ':' && !sbflag) { + if (buffer.size() == 0) { + url->flags |= URL_FLAGS_FAILED; + return; + } + url->flags |= URL_FLAGS_HAS_HOST; + if (!ParseHost(&buffer, &url->host, special)) { + url->flags |= URL_FLAGS_FAILED; + return; + } + buffer.clear(); + state = kPort; + if (state_override == kHostname) { + return; + } + } else if (ch == kEOL || + ch == '/' || + ch == '?' || + ch == '#' || + special_back_slash) { + p--; + if (special && buffer.size() == 0) { + url->flags |= URL_FLAGS_FAILED; + return; + } + if (has_state_override && + buffer.size() == 0 && + ((url->username.size() > 0 || url->password.size() > 0) || + url->port != -1)) { + url->flags |= URL_FLAGS_TERMINATED; + return; + } + url->flags |= URL_FLAGS_HAS_HOST; + if (!ParseHost(&buffer, &url->host, special)) { + url->flags |= URL_FLAGS_FAILED; + return; + } + buffer.clear(); + state = kPathStart; + if (has_state_override) { + return; + } + } else { + if (ch == '[') + sbflag = true; + if (ch == ']') + sbflag = false; + buffer += ch; + } + break; + case kPort: + if (IsASCIIDigit(ch)) { + buffer += ch; + } else if (has_state_override || + ch == kEOL || + ch == '/' || + ch == '?' || + ch == '#' || + special_back_slash) { + if (buffer.size() > 0) { + unsigned port = 0; + // the condition port <= 0xffff prevents integer overflow + for (size_t i = 0; port <= 0xffff && i < buffer.size(); i++) + port = port * 10 + buffer[i] - '0'; + if (port > 0xffff) { + // TODO(TimothyGu): This hack is currently needed for the host + // setter since it needs access to hostname if it is valid, and + // if the FAILED flag is set the entire response to JS layer + // will be empty. + if (state_override == kHost) + url->port = -1; + else + url->flags |= URL_FLAGS_FAILED; + return; + } + // the port is valid + url->port = NormalizePort(url->scheme, static_cast(port)); + buffer.clear(); + } else if (has_state_override) { + // TODO(TimothyGu): Similar case as above. + if (state_override == kHost) + url->port = -1; + else + url->flags |= URL_FLAGS_TERMINATED; + return; + } + state = kPathStart; + continue; + } else { + url->flags |= URL_FLAGS_FAILED; + return; + } + break; + case kFile: + url->scheme = "file:"; + if (ch == '/' || ch == '\\') { + state = kFileSlash; + } else if (has_base && base->scheme == "file:") { + switch (ch) { + case kEOL: + if (base->flags & URL_FLAGS_HAS_HOST) { + url->flags |= URL_FLAGS_HAS_HOST; + url->host = base->host; + } + if (base->flags & URL_FLAGS_HAS_PATH) { + url->flags |= URL_FLAGS_HAS_PATH; + url->path = base->path; + } + if (base->flags & URL_FLAGS_HAS_QUERY) { + url->flags |= URL_FLAGS_HAS_QUERY; + url->query = base->query; + } + break; + case '?': + if (base->flags & URL_FLAGS_HAS_HOST) { + url->flags |= URL_FLAGS_HAS_HOST; + url->host = base->host; + } + if (base->flags & URL_FLAGS_HAS_PATH) { + url->flags |= URL_FLAGS_HAS_PATH; + url->path = base->path; + } + url->flags |= URL_FLAGS_HAS_QUERY; + url->query.clear(); + state = kQuery; + break; + case '#': + if (base->flags & URL_FLAGS_HAS_HOST) { + url->flags |= URL_FLAGS_HAS_HOST; + url->host = base->host; + } + if (base->flags & URL_FLAGS_HAS_PATH) { + url->flags |= URL_FLAGS_HAS_PATH; + url->path = base->path; + } + if (base->flags & URL_FLAGS_HAS_QUERY) { + url->flags |= URL_FLAGS_HAS_QUERY; + url->query = base->query; + } + url->flags |= URL_FLAGS_HAS_FRAGMENT; + url->fragment.clear(); + state = kFragment; + break; + default: + if (!StartsWithWindowsDriveLetter(p, end)) { + if (base->flags & URL_FLAGS_HAS_HOST) { + url->flags |= URL_FLAGS_HAS_HOST; + url->host = base->host; + } + if (base->flags & URL_FLAGS_HAS_PATH) { + url->flags |= URL_FLAGS_HAS_PATH; + url->path = base->path; + } + ShortenUrlPath(url); + } + state = kPath; + continue; + } + } else { + state = kPath; + continue; + } + break; + case kFileSlash: + if (ch == '/' || ch == '\\') { + state = kFileHost; + } else { + if (has_base && + base->scheme == "file:" && + !StartsWithWindowsDriveLetter(p, end)) { + if (IsNormalizedWindowsDriveLetter(base->path[0])) { + url->flags |= URL_FLAGS_HAS_PATH; + url->path.push_back(base->path[0]); + } else { + if (base->flags & URL_FLAGS_HAS_HOST) { + url->flags |= URL_FLAGS_HAS_HOST; + url->host = base->host; + } else { + url->flags &= ~URL_FLAGS_HAS_HOST; + url->host.clear(); + } + } + } + state = kPath; + continue; + } + break; + case kFileHost: + if (ch == kEOL || + ch == '/' || + ch == '\\' || + ch == '?' || + ch == '#') { + if (!has_state_override && + buffer.size() == 2 && + IsWindowsDriveLetter(buffer)) { + state = kPath; + } else if (buffer.size() == 0) { + url->flags |= URL_FLAGS_HAS_HOST; + url->host.clear(); + if (has_state_override) + return; + state = kPathStart; + } else { + std::string host; + if (!ParseHost(&buffer, &host, special)) { + url->flags |= URL_FLAGS_FAILED; + return; + } + if (host == "localhost") + host.clear(); + url->flags |= URL_FLAGS_HAS_HOST; + url->host = host; + if (has_state_override) + return; + buffer.clear(); + state = kPathStart; + } + continue; + } else { + buffer += ch; + } + break; + case kPathStart: + if (IsSpecial(url->scheme)) { + state = kPath; + if (ch != '/' && ch != '\\') { + continue; + } + } else if (!has_state_override && ch == '?') { + url->flags |= URL_FLAGS_HAS_QUERY; + url->query.clear(); + state = kQuery; + } else if (!has_state_override && ch == '#') { + url->flags |= URL_FLAGS_HAS_FRAGMENT; + url->fragment.clear(); + state = kFragment; + } else if (ch != kEOL) { + state = kPath; + if (ch != '/') { + continue; + } + } + break; + case kPath: + if (ch == kEOL || + ch == '/' || + special_back_slash || + (!has_state_override && (ch == '?' || ch == '#'))) { + if (IsDoubleDotSegment(buffer)) { + ShortenUrlPath(url); + if (ch != '/' && !special_back_slash) { + url->flags |= URL_FLAGS_HAS_PATH; + url->path.push_back(""); + } + } else if (IsSingleDotSegment(buffer) && + ch != '/' && !special_back_slash) { + url->flags |= URL_FLAGS_HAS_PATH; + url->path.push_back(""); + } else if (!IsSingleDotSegment(buffer)) { + if (url->scheme == "file:" && + url->path.empty() && + buffer.size() == 2 && + IsWindowsDriveLetter(buffer)) { + if ((url->flags & URL_FLAGS_HAS_HOST) && + !url->host.empty()) { + url->host.clear(); + url->flags |= URL_FLAGS_HAS_HOST; + } + buffer[1] = ':'; + } + url->flags |= URL_FLAGS_HAS_PATH; + std::string segment(buffer.c_str(), buffer.size()); + url->path.push_back(segment); + } + buffer.clear(); + if (url->scheme == "file:" && + (ch == kEOL || + ch == '?' || + ch == '#')) { + while (url->path.size() > 1 && url->path[0].length() == 0) { + url->path.erase(url->path.begin()); + } + } + if (ch == '?') { + url->flags |= URL_FLAGS_HAS_QUERY; + state = kQuery; + } else if (ch == '#') { + state = kFragment; + } + } else { + AppendOrEscape(&buffer, ch, PATH_ENCODE_SET); + } + break; + case kCannotBeBase: + switch (ch) { + case '?': + state = kQuery; + break; + case '#': + state = kFragment; + break; + default: + if (url->path.size() == 0) + url->path.push_back(""); + if (url->path.size() > 0 && ch != kEOL) + AppendOrEscape(&url->path[0], ch, C0_CONTROL_ENCODE_SET); + } + break; + case kQuery: + if (ch == kEOL || (!has_state_override && ch == '#')) { + url->flags |= URL_FLAGS_HAS_QUERY; + url->query = buffer; + buffer.clear(); + if (ch == '#') + state = kFragment; + } else { + AppendOrEscape(&buffer, ch, QUERY_ENCODE_SET); + } + break; + case kFragment: + switch (ch) { + case kEOL: + url->flags |= URL_FLAGS_HAS_FRAGMENT; + url->fragment = buffer; + break; + case 0: + break; + default: + AppendOrEscape(&buffer, ch, C0_CONTROL_ENCODE_SET); + } + break; + default: + url->flags |= URL_FLAGS_INVALID_PARSE_STATE; + return; + } + + p++; + } +} // NOLINT(readability/fn_size) + +static inline void SetArgs(Environment* env, + Local argv[], + const struct url_data* url) { + Isolate* isolate = env->isolate(); + argv[ARG_FLAGS] = Integer::NewFromUnsigned(isolate, url->flags); + argv[ARG_PROTOCOL] = OneByteString(isolate, url->scheme.c_str()); + if (url->flags & URL_FLAGS_HAS_USERNAME) + argv[ARG_USERNAME] = UTF8STRING(isolate, url->username); + if (url->flags & URL_FLAGS_HAS_PASSWORD) + argv[ARG_PASSWORD] = UTF8STRING(isolate, url->password); + if (url->flags & URL_FLAGS_HAS_HOST) + argv[ARG_HOST] = UTF8STRING(isolate, url->host); + if (url->flags & URL_FLAGS_HAS_QUERY) + argv[ARG_QUERY] = UTF8STRING(isolate, url->query); + if (url->flags & URL_FLAGS_HAS_FRAGMENT) + argv[ARG_FRAGMENT] = UTF8STRING(isolate, url->fragment); + if (url->port > -1) + argv[ARG_PORT] = Integer::New(isolate, url->port); + if (url->flags & URL_FLAGS_HAS_PATH) + argv[ARG_PATH] = Copy(env, url->path); +} + +static void Parse(Environment* env, + Local recv, + const char* input, + const size_t len, + enum url_parse_state state_override, + Local base_obj, + Local context_obj, + Local cb, + Local error_cb) { + Isolate* isolate = env->isolate(); + Local context = env->context(); + HandleScope handle_scope(isolate); + Context::Scope context_scope(context); + + const bool has_context = context_obj->IsObject(); + const bool has_base = base_obj->IsObject(); + + struct url_data base; + struct url_data url; + if (has_context) + HarvestContext(env, &url, context_obj.As()); + if (has_base) + HarvestBase(env, &base, base_obj.As()); + + URL::Parse(input, len, state_override, &url, has_context, &base, has_base); + if ((url.flags & URL_FLAGS_INVALID_PARSE_STATE) || + ((state_override != kUnknownState) && + (url.flags & URL_FLAGS_TERMINATED))) + return; + + // Define the return value placeholders + const Local undef = Undefined(isolate); + const Local null = Null(isolate); + if (!(url.flags & URL_FLAGS_FAILED)) { + Local argv[9] = { + undef, + undef, + undef, + undef, + null, // host defaults to null + null, // port defaults to null + undef, + null, // query defaults to null + null, // fragment defaults to null + }; + SetArgs(env, argv, &url); + cb->Call(context, recv, arraysize(argv), argv).FromMaybe(Local()); + } else if (error_cb->IsFunction()) { + Local argv[2] = { undef, undef }; + argv[ERR_ARG_FLAGS] = Integer::NewFromUnsigned(isolate, url.flags); + argv[ERR_ARG_INPUT] = + String::NewFromUtf8(env->isolate(), + input, + v8::NewStringType::kNormal).ToLocalChecked(); + error_cb.As()->Call(context, recv, arraysize(argv), argv) + .FromMaybe(Local()); + } +} + +static void Parse(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_GE(args.Length(), 5); + CHECK(args[0]->IsString()); // input + CHECK(args[2]->IsUndefined() || // base context + args[2]->IsNull() || + args[2]->IsObject()); + CHECK(args[3]->IsUndefined() || // context + args[3]->IsNull() || + args[3]->IsObject()); + CHECK(args[4]->IsFunction()); // complete callback + CHECK(args[5]->IsUndefined() || args[5]->IsFunction()); // error callback + + Utf8Value input(env->isolate(), args[0]); + enum url_parse_state state_override = kUnknownState; + if (args[1]->IsNumber()) { + state_override = static_cast( + args[1]->Uint32Value(env->context()).FromJust()); + } + + Parse(env, args.This(), + *input, input.length(), + state_override, + args[2], + args[3], + args[4].As(), + args[5]); +} + +static void EncodeAuthSet(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_GE(args.Length(), 1); + CHECK(args[0]->IsString()); + Utf8Value value(env->isolate(), args[0]); + std::string output; + const size_t len = value.length(); + output.reserve(len); + for (size_t n = 0; n < len; n++) { + const char ch = (*value)[n]; + AppendOrEscape(&output, ch, USERINFO_ENCODE_SET); + } + args.GetReturnValue().Set( + String::NewFromUtf8(env->isolate(), + output.c_str(), + v8::NewStringType::kNormal).ToLocalChecked()); +} + +static void ToUSVString(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_GE(args.Length(), 2); + CHECK(args[0]->IsString()); + CHECK(args[1]->IsNumber()); + + TwoByteValue value(env->isolate(), args[0]); + const size_t n = value.length(); + + const int64_t start = args[1]->IntegerValue(env->context()).FromJust(); + CHECK_GE(start, 0); + + for (size_t i = start; i < n; i++) { + char16_t c = value[i]; + if (!IsUnicodeSurrogate(c)) { + continue; + } else if (IsUnicodeSurrogateTrail(c) || i == n - 1) { + value[i] = kUnicodeReplacementCharacter; + } else { + char16_t d = value[i + 1]; + if (IsUnicodeTrail(d)) { + i++; + } else { + value[i] = kUnicodeReplacementCharacter; + } + } + } + + args.GetReturnValue().Set( + String::NewFromTwoByte(env->isolate(), + *value, + v8::NewStringType::kNormal, + n).ToLocalChecked()); +} + +static void DomainToASCII(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_GE(args.Length(), 1); + CHECK(args[0]->IsString()); + Utf8Value value(env->isolate(), args[0]); + + url_host host{{""}, HOST_TYPE_DOMAIN}; + // Assuming the host is used for a special scheme. + ParseHost(&host, *value, value.length(), true); + if (host.type == HOST_TYPE_FAILED) { + args.GetReturnValue().Set(FIXED_ONE_BYTE_STRING(env->isolate(), "")); + return; + } + std::string out; + WriteHost(&host, &out); + args.GetReturnValue().Set( + String::NewFromUtf8(env->isolate(), + out.c_str(), + v8::NewStringType::kNormal).ToLocalChecked()); +} + +static void DomainToUnicode(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_GE(args.Length(), 1); + CHECK(args[0]->IsString()); + Utf8Value value(env->isolate(), args[0]); + + url_host host{{""}, HOST_TYPE_DOMAIN}; + // Assuming the host is used for a special scheme. + ParseHost(&host, *value, value.length(), true, true); + if (host.type == HOST_TYPE_FAILED) { + args.GetReturnValue().Set(FIXED_ONE_BYTE_STRING(env->isolate(), "")); + return; + } + std::string out; + WriteHost(&host, &out); + args.GetReturnValue().Set( + String::NewFromUtf8(env->isolate(), + out.c_str(), + v8::NewStringType::kNormal).ToLocalChecked()); +} + +std::string URL::ToFilePath() const { + if (context_.scheme != "file:") { + return ""; + } + +#ifdef _WIN32 + const char* slash = "\\"; + auto is_slash = [] (char ch) { + return ch == '/' || ch == '\\'; + }; +#else + const char* slash = "/"; + auto is_slash = [] (char ch) { + return ch == '/'; + }; + if ((context_.flags & URL_FLAGS_HAS_HOST) && + context_.host.length() > 0) { + return ""; + } +#endif + std::string decoded_path; + for (const std::string& part : context_.path) { + std::string decoded; + PercentDecode(part.c_str(), part.length(), &decoded); + for (char& ch : decoded) { + if (is_slash(ch)) { + return ""; + } + } + decoded_path += slash + decoded; + } + +#ifdef _WIN32 + // TODO(TimothyGu): Use "\\?\" long paths on Windows. + + // If hostname is set, then we have a UNC path. Pass the hostname through + // ToUnicode just in case it is an IDN using punycode encoding. We do not + // need to worry about percent encoding because the URL parser will have + // already taken care of that for us. Note that this only causes IDNs with an + // appropriate `xn--` prefix to be decoded. + if ((context_.flags & URL_FLAGS_HAS_HOST) && + context_.host.length() > 0) { + std::string unicode_host; + if (!ToUnicode(context_.host, &unicode_host)) { + return ""; + } + return "\\\\" + unicode_host + decoded_path; + } + // Otherwise, it's a local path that requires a drive letter. + if (decoded_path.length() < 3) { + return ""; + } + if (decoded_path[2] != ':' || + !IsASCIIAlpha(decoded_path[1])) { + return ""; + } + // Strip out the leading '\'. + return decoded_path.substr(1); +#else + return decoded_path; +#endif +} + +// This function works by calling out to a JS function that creates and +// returns the JS URL object. Be mindful of the JS<->Native boundary +// crossing that is required. +const Local URL::ToObject(Environment* env) const { + Isolate* isolate = env->isolate(); + Local context = env->context(); + Context::Scope context_scope(context); + + const Local undef = Undefined(isolate); + const Local null = Null(isolate); + + if (context_.flags & URL_FLAGS_FAILED) + return Local(); + + Local argv[9] = { + undef, + undef, + undef, + undef, + null, // host defaults to null + null, // port defaults to null + undef, + null, // query defaults to null + null, // fragment defaults to null + }; + SetArgs(env, argv, &context_); + + TryCatch try_catch(isolate); + + // The SetURLConstructor method must have been called already to + // set the constructor function used below. SetURLConstructor is + // called automatically when the internal/url.js module is loaded + // during the internal/bootstrap_node.js processing. + MaybeLocal ret = + env->url_constructor_function() + ->Call(env->context(), undef, 9, argv); + + if (ret.IsEmpty()) { + ClearFatalExceptionHandlers(env); + FatalException(isolate, try_catch); + } + + return ret.ToLocalChecked(); +} + +static void SetURLConstructor(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsFunction()); + env->set_url_constructor_function(args[0].As()); +} + +static void Init(Local target, + Local unused, + Local context, + void* priv) { + Environment* env = Environment::GetCurrent(context); + env->SetMethod(target, "parse", Parse); + env->SetMethod(target, "encodeAuth", EncodeAuthSet); + env->SetMethod(target, "toUSVString", ToUSVString); + env->SetMethod(target, "domainToASCII", DomainToASCII); + env->SetMethod(target, "domainToUnicode", DomainToUnicode); + env->SetMethod(target, "setURLConstructor", SetURLConstructor); + +#define XX(name, _) NODE_DEFINE_CONSTANT(target, name); + FLAGS(XX) +#undef XX + +#define XX(name) NODE_DEFINE_CONSTANT(target, name); + PARSESTATES(XX) +#undef XX +} +} // namespace url +} // namespace node + +NODE_MODULE_CONTEXT_AWARE_BUILTIN(url, node::url::Init) diff --git a/src/node_url.h b/src/node_url.h new file mode 100644 index 00000000000000..6b526d15b07703 --- /dev/null +++ b/src/node_url.h @@ -0,0 +1,191 @@ +#ifndef SRC_NODE_URL_H_ +#define SRC_NODE_URL_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "node.h" +#include "env-inl.h" + +#include + +namespace node { +namespace url { + +using v8::Local; +using v8::Value; + + +#define PARSESTATES(XX) \ + XX(kSchemeStart) \ + XX(kScheme) \ + XX(kNoScheme) \ + XX(kSpecialRelativeOrAuthority) \ + XX(kPathOrAuthority) \ + XX(kRelative) \ + XX(kRelativeSlash) \ + XX(kSpecialAuthoritySlashes) \ + XX(kSpecialAuthorityIgnoreSlashes) \ + XX(kAuthority) \ + XX(kHost) \ + XX(kHostname) \ + XX(kPort) \ + XX(kFile) \ + XX(kFileSlash) \ + XX(kFileHost) \ + XX(kPathStart) \ + XX(kPath) \ + XX(kCannotBeBase) \ + XX(kQuery) \ + XX(kFragment) + +#define FLAGS(XX) \ + XX(URL_FLAGS_NONE, 0) \ + XX(URL_FLAGS_FAILED, 0x01) \ + XX(URL_FLAGS_CANNOT_BE_BASE, 0x02) \ + XX(URL_FLAGS_INVALID_PARSE_STATE, 0x04) \ + XX(URL_FLAGS_TERMINATED, 0x08) \ + XX(URL_FLAGS_SPECIAL, 0x10) \ + XX(URL_FLAGS_HAS_USERNAME, 0x20) \ + XX(URL_FLAGS_HAS_PASSWORD, 0x40) \ + XX(URL_FLAGS_HAS_HOST, 0x80) \ + XX(URL_FLAGS_HAS_PATH, 0x100) \ + XX(URL_FLAGS_HAS_QUERY, 0x200) \ + XX(URL_FLAGS_HAS_FRAGMENT, 0x400) + +enum url_parse_state { + kUnknownState = -1, +#define XX(name) name, + PARSESTATES(XX) +#undef XX +}; + +enum url_flags { +#define XX(name, val) name = val, + FLAGS(XX) +#undef XX +}; + +struct url_data { + int32_t flags = URL_FLAGS_NONE; + int port = -1; + std::string scheme; + std::string username; + std::string password; + std::string host; + std::string query; + std::string fragment; + std::vector path; +}; + +class URL { + public: + static void Parse(const char* input, + size_t len, + enum url_parse_state state_override, + struct url_data* url, + bool has_url, + const struct url_data* base, + bool has_base); + + URL(const char* input, const size_t len) { + Parse(input, len, kUnknownState, &context_, false, nullptr, false); + } + + URL(const char* input, const size_t len, const URL* base) { + if (base != nullptr) + Parse(input, len, kUnknownState, + &context_, false, + &(base->context_), true); + else + Parse(input, len, kUnknownState, &context_, false, nullptr, false); + } + + URL(const char* input, const size_t len, + const char* base, const size_t baselen) { + if (base != nullptr && baselen > 0) { + URL _base(base, baselen); + Parse(input, len, kUnknownState, + &context_, false, + &(_base.context_), true); + } else { + Parse(input, len, kUnknownState, &context_, false, nullptr, false); + } + } + + explicit URL(std::string input) : + URL(input.c_str(), input.length()) {} + + URL(std::string input, const URL* base) : + URL(input.c_str(), input.length(), base) {} + + URL(std::string input, const URL& base) : + URL(input.c_str(), input.length(), &base) {} + + URL(std::string input, std::string base) : + URL(input.c_str(), input.length(), base.c_str(), base.length()) {} + + int32_t flags() { + return context_.flags; + } + + int port() { + return context_.port; + } + + const std::string& protocol() const { + return context_.scheme; + } + + const std::string& username() const { + return context_.username; + } + + const std::string& password() const { + return context_.password; + } + + const std::string& host() const { + return context_.host; + } + + const std::string& query() const { + return context_.query; + } + + const std::string& fragment() const { + return context_.fragment; + } + + std::string path() const { + std::string ret; + for (auto i = context_.path.begin(); i != context_.path.end(); i++) { + ret += '/'; + ret += *i; + } + return ret; + } + + // Get the path of the file: URL in a format consumable by native file system + // APIs. Returns an empty string if something went wrong. + std::string ToFilePath() const; + + const Local ToObject(Environment* env) const; + + URL(const URL&) = default; + URL& operator=(const URL&) = default; + URL(URL&&) = default; + URL& operator=(URL&&) = default; + + URL() : URL("") {} + + private: + struct url_data context_; +}; + +} // namespace url + +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // SRC_NODE_URL_H_ diff --git a/test/cctest/test_url.cc b/test/cctest/test_url.cc new file mode 100644 index 00000000000000..0b80d44caad807 --- /dev/null +++ b/test/cctest/test_url.cc @@ -0,0 +1,106 @@ +#include "node_url.h" +#include "node_i18n.h" + +#include "gtest/gtest.h" + +using node::url::URL; +using node::url::URL_FLAGS_FAILED; + +class URLTest : public ::testing::Test { + protected: + void SetUp() override { +#if defined(NODE_HAVE_I18N_SUPPORT) + std::string icu_data_dir; + node::i18n::InitializeICUDirectory(icu_data_dir); +#endif + } + + void TearDown() override {} +}; + +TEST_F(URLTest, Simple) { + URL simple("https://example.org:81/a/b/c?query#fragment"); + + EXPECT_FALSE(simple.flags() & URL_FLAGS_FAILED); + EXPECT_EQ(simple.protocol(), "https:"); + EXPECT_EQ(simple.host(), "example.org"); + EXPECT_EQ(simple.port(), 81); + EXPECT_EQ(simple.path(), "/a/b/c"); + EXPECT_EQ(simple.query(), "query"); + EXPECT_EQ(simple.fragment(), "fragment"); +} + +TEST_F(URLTest, Simple2) { + const char* input = "https://example.org:81/a/b/c?query#fragment"; + URL simple(input, strlen(input)); + + EXPECT_FALSE(simple.flags() & URL_FLAGS_FAILED); + EXPECT_EQ(simple.protocol(), "https:"); + EXPECT_EQ(simple.host(), "example.org"); + EXPECT_EQ(simple.port(), 81); + EXPECT_EQ(simple.path(), "/a/b/c"); + EXPECT_EQ(simple.query(), "query"); + EXPECT_EQ(simple.fragment(), "fragment"); +} + +TEST_F(URLTest, NoBase1) { + URL error("123noscheme"); + EXPECT_TRUE(error.flags() & URL_FLAGS_FAILED); +} + +TEST_F(URLTest, Base1) { + URL base("http://example.org/foo/bar"); + ASSERT_FALSE(base.flags() & URL_FLAGS_FAILED); + + URL simple("../baz", &base); + EXPECT_FALSE(simple.flags() & URL_FLAGS_FAILED); + EXPECT_EQ(simple.protocol(), "http:"); + EXPECT_EQ(simple.host(), "example.org"); + EXPECT_EQ(simple.path(), "/baz"); +} + +TEST_F(URLTest, Base2) { + URL simple("../baz", "http://example.org/foo/bar"); + + EXPECT_FALSE(simple.flags() & URL_FLAGS_FAILED); + EXPECT_EQ(simple.protocol(), "http:"); + EXPECT_EQ(simple.host(), "example.org"); + EXPECT_EQ(simple.path(), "/baz"); +} + +TEST_F(URLTest, Base3) { + const char* input = "../baz"; + const char* base = "http://example.org/foo/bar"; + + URL simple(input, strlen(input), base, strlen(base)); + + EXPECT_FALSE(simple.flags() & URL_FLAGS_FAILED); + EXPECT_EQ(simple.protocol(), "http:"); + EXPECT_EQ(simple.host(), "example.org"); + EXPECT_EQ(simple.path(), "/baz"); +} + +TEST_F(URLTest, ToFilePath) { +#define T(url, path) EXPECT_EQ(path, URL(url).ToFilePath()) + T("http://example.org/foo/bar", ""); + +#ifdef _WIN32 + T("file:///C:/Program%20Files/", "C:\\Program Files\\"); + T("file:///C:/a/b/c?query#fragment", "C:\\a\\b\\c"); + T("file://host/path/a/b/c?query#fragment", "\\\\host\\path\\a\\b\\c"); + T("file://xn--weird-prdj8vva.com/host/a", "\\\\wͪ͊eiͬ͋rd.com\\host\\a"); + T("file:///C:/a%2Fb", ""); + T("file:///", ""); + T("file:///home", ""); +#else + T("file:///", "/"); + T("file:///home/user?query#fragment", "/home/user"); + T("file:///home/user/?query#fragment", "/home/user/"); + T("file:///home/user/%20space", "/home/user/ space"); + T("file:///home/us%5Cer", "/home/us\\er"); + T("file:///home/us%2Fer", ""); + T("file://host/path", ""); +#endif + +#undef T +} diff --git a/test/fixtures/url-searchparams.js b/test/fixtures/url-searchparams.js new file mode 100644 index 00000000000000..918af678bc563e --- /dev/null +++ b/test/fixtures/url-searchparams.js @@ -0,0 +1,77 @@ +module.exports = [ + ['', '', []], + [ + 'foo=918854443121279438895193', + 'foo=918854443121279438895193', + [['foo', '918854443121279438895193']] + ], + ['foo=bar', 'foo=bar', [['foo', 'bar']]], + ['foo=bar&foo=quux', 'foo=bar&foo=quux', [['foo', 'bar'], ['foo', 'quux']]], + ['foo=1&bar=2', 'foo=1&bar=2', [['foo', '1'], ['bar', '2']]], + [ + "my%20weird%20field=q1!2%22'w%245%267%2Fz8)%3F", + 'my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F', + [['my weird field', 'q1!2"\'w$5&7/z8)?']] + ], + ['foo%3Dbaz=bar', 'foo%3Dbaz=bar', [['foo=baz', 'bar']]], + ['foo=baz=bar', 'foo=baz%3Dbar', [['foo', 'baz=bar']]], + [ + 'str=foo&arr=1&somenull&arr=2&undef=&arr=3', + 'str=foo&arr=1&somenull=&arr=2&undef=&arr=3', + [ + ['str', 'foo'], + ['arr', '1'], + ['somenull', ''], + ['arr', '2'], + ['undef', ''], + ['arr', '3'] + ] + ], + [' foo = bar ', '+foo+=+bar+', [[' foo ', ' bar ']]], + ['foo=%zx', 'foo=%25zx', [['foo', '%zx']]], + ['foo=%EF%BF%BD', 'foo=%EF%BF%BD', [['foo', '\ufffd']]], + // See: https://github.com/joyent/node/issues/3058 + ['foo&bar=baz', 'foo=&bar=baz', [['foo', ''], ['bar', 'baz']]], + ['a=b&c&d=e', 'a=b&c=&d=e', [['a', 'b'], ['c', ''], ['d', 'e']]], + ['a=b&c=&d=e', 'a=b&c=&d=e', [['a', 'b'], ['c', ''], ['d', 'e']]], + ['a=b&=c&d=e', 'a=b&=c&d=e', [['a', 'b'], ['', 'c'], ['d', 'e']]], + ['a=b&=&d=e', 'a=b&=&d=e', [['a', 'b'], ['', ''], ['d', 'e']]], + ['&&foo=bar&&', 'foo=bar', [['foo', 'bar']]], + ['&', '', []], + ['&&&&', '', []], + ['&=&', '=', [['', '']]], + ['&=&=', '=&=', [['', ''], ['', '']]], + ['=', '=', [['', '']]], + ['+', '+=', [[' ', '']]], + ['+=', '+=', [[' ', '']]], + ['+&', '+=', [[' ', '']]], + ['=+', '=+', [['', ' ']]], + ['+=&', '+=', [[' ', '']]], + ['a&&b', 'a=&b=', [['a', ''], ['b', '']]], + ['a=a&&b=b', 'a=a&b=b', [['a', 'a'], ['b', 'b']]], + ['&a', 'a=', [['a', '']]], + ['&=', '=', [['', '']]], + ['a&a&', 'a=&a=', [['a', ''], ['a', '']]], + ['a&a&a&', 'a=&a=&a=', [['a', ''], ['a', ''], ['a', '']]], + ['a&a&a&a&', 'a=&a=&a=&a=', [['a', ''], ['a', ''], ['a', ''], ['a', '']]], + ['a=&a=value&a=', 'a=&a=value&a=', [['a', ''], ['a', 'value'], ['a', '']]], + ['foo%20bar=baz%20quux', 'foo+bar=baz+quux', [['foo bar', 'baz quux']]], + ['+foo=+bar', '+foo=+bar', [[' foo', ' bar']]], + ['a+', 'a+=', [['a ', '']]], + ['=a+', '=a+', [['', 'a ']]], + ['a+&', 'a+=', [['a ', '']]], + ['=a+&', '=a+', [['', 'a ']]], + ['%20+', '++=', [[' ', '']]], + ['=%20+', '=++', [['', ' ']]], + ['%20+&', '++=', [[' ', '']]], + ['=%20+&', '=++', [['', ' ']]], + [ + // fake percent encoding + 'foo=%©ar&baz=%A©uux&xyzzy=%©ud', + 'foo=%25%C2%A9ar&baz=%25A%C2%A9uux&xyzzy=%25%C2%A9ud', + [['foo', '%©ar'], ['baz', '%A©uux'], ['xyzzy', '%©ud']] + ], + // always preserve order of key-value pairs + ['a=1&b=2&a=3', 'a=1&b=2&a=3', [['a', '1'], ['b', '2'], ['a', '3']]], + ['?a', '%3Fa=', [['?a', '']]] +]; diff --git a/test/fixtures/url-setter-tests-additional.js b/test/fixtures/url-setter-tests-additional.js new file mode 100644 index 00000000000000..b27ae336a28776 --- /dev/null +++ b/test/fixtures/url-setter-tests-additional.js @@ -0,0 +1,237 @@ +module.exports = { + 'username': [ + { + 'comment': 'Surrogate pair', + 'href': 'https://github.com/', + 'new_value': '\uD83D\uDE00', + 'expected': { + 'href': 'https://%F0%9F%98%80@github.com/', + 'username': '%F0%9F%98%80' + } + }, + { + 'comment': 'Unpaired low surrogate 1', + 'href': 'https://github.com/', + 'new_value': '\uD83D', + 'expected': { + 'href': 'https://%EF%BF%BD@github.com/', + 'username': '%EF%BF%BD' + } + }, + { + 'comment': 'Unpaired low surrogate 2', + 'href': 'https://github.com/', + 'new_value': '\uD83Dnode', + 'expected': { + 'href': 'https://%EF%BF%BDnode@github.com/', + 'username': '%EF%BF%BDnode' + } + }, + { + 'comment': 'Unpaired high surrogate 1', + 'href': 'https://github.com/', + 'new_value': '\uDE00', + 'expected': { + 'href': 'https://%EF%BF%BD@github.com/', + 'username': '%EF%BF%BD' + } + }, + { + 'comment': 'Unpaired high surrogate 2', + 'href': 'https://github.com/', + 'new_value': '\uDE00node', + 'expected': { + 'href': 'https://%EF%BF%BDnode@github.com/', + 'username': '%EF%BF%BDnode' + } + } + ], + 'password': [ + { + 'comment': 'Surrogate pair', + 'href': 'https://github.com/', + 'new_value': '\uD83D\uDE00', + 'expected': { + 'href': 'https://:%F0%9F%98%80@github.com/', + 'password': '%F0%9F%98%80' + } + }, + { + 'comment': 'Unpaired low surrogate 1', + 'href': 'https://github.com/', + 'new_value': '\uD83D', + 'expected': { + 'href': 'https://:%EF%BF%BD@github.com/', + 'password': '%EF%BF%BD' + } + }, + { + 'comment': 'Unpaired low surrogate 2', + 'href': 'https://github.com/', + 'new_value': '\uD83Dnode', + 'expected': { + 'href': 'https://:%EF%BF%BDnode@github.com/', + 'password': '%EF%BF%BDnode' + } + }, + { + 'comment': 'Unpaired high surrogate 1', + 'href': 'https://github.com/', + 'new_value': '\uDE00', + 'expected': { + 'href': 'https://:%EF%BF%BD@github.com/', + 'password': '%EF%BF%BD' + } + }, + { + 'comment': 'Unpaired high surrogate 2', + 'href': 'https://github.com/', + 'new_value': '\uDE00node', + 'expected': { + 'href': 'https://:%EF%BF%BDnode@github.com/', + 'password': '%EF%BF%BDnode' + } + } + ], + 'pathname': [ + { + 'comment': 'Surrogate pair', + 'href': 'https://github.com/', + 'new_value': '/\uD83D\uDE00', + 'expected': { + 'href': 'https://github.com/%F0%9F%98%80', + 'pathname': '/%F0%9F%98%80' + } + }, + { + 'comment': 'Unpaired low surrogate 1', + 'href': 'https://github.com/', + 'new_value': '/\uD83D', + 'expected': { + 'href': 'https://github.com/%EF%BF%BD', + 'pathname': '/%EF%BF%BD' + } + }, + { + 'comment': 'Unpaired low surrogate 2', + 'href': 'https://github.com/', + 'new_value': '/\uD83Dnode', + 'expected': { + 'href': 'https://github.com/%EF%BF%BDnode', + 'pathname': '/%EF%BF%BDnode' + } + }, + { + 'comment': 'Unpaired high surrogate 1', + 'href': 'https://github.com/', + 'new_value': '/\uDE00', + 'expected': { + 'href': 'https://github.com/%EF%BF%BD', + 'pathname': '/%EF%BF%BD' + } + }, + { + 'comment': 'Unpaired high surrogate 2', + 'href': 'https://github.com/', + 'new_value': '/\uDE00node', + 'expected': { + 'href': 'https://github.com/%EF%BF%BDnode', + 'pathname': '/%EF%BF%BDnode' + } + } + ], + 'search': [ + { + 'comment': 'Surrogate pair', + 'href': 'https://github.com/', + 'new_value': '\uD83D\uDE00', + 'expected': { + 'href': 'https://github.com/?%F0%9F%98%80', + 'search': '?%F0%9F%98%80' + } + }, + { + 'comment': 'Unpaired low surrogate 1', + 'href': 'https://github.com/', + 'new_value': '\uD83D', + 'expected': { + 'href': 'https://github.com/?%EF%BF%BD', + 'search': '?%EF%BF%BD' + } + }, + { + 'comment': 'Unpaired low surrogate 2', + 'href': 'https://github.com/', + 'new_value': '\uD83Dnode', + 'expected': { + 'href': 'https://github.com/?%EF%BF%BDnode', + 'search': '?%EF%BF%BDnode' + } + }, + { + 'comment': 'Unpaired high surrogate 1', + 'href': 'https://github.com/', + 'new_value': '\uDE00', + 'expected': { + 'href': 'https://github.com/?%EF%BF%BD', + 'search': '?%EF%BF%BD' + } + }, + { + 'comment': 'Unpaired high surrogate 2', + 'href': 'https://github.com/', + 'new_value': '\uDE00node', + 'expected': { + 'href': 'https://github.com/?%EF%BF%BDnode', + 'search': '?%EF%BF%BDnode' + } + } + ], + 'hash': [ + { + 'comment': 'Surrogate pair', + 'href': 'https://github.com/', + 'new_value': '\uD83D\uDE00', + 'expected': { + 'href': 'https://github.com/#%F0%9F%98%80', + 'hash': '#%F0%9F%98%80' + } + }, + { + 'comment': 'Unpaired low surrogate 1', + 'href': 'https://github.com/', + 'new_value': '\uD83D', + 'expected': { + 'href': 'https://github.com/#%EF%BF%BD', + 'hash': '#%EF%BF%BD' + } + }, + { + 'comment': 'Unpaired low surrogate 2', + 'href': 'https://github.com/', + 'new_value': '\uD83Dnode', + 'expected': { + 'href': 'https://github.com/#%EF%BF%BDnode', + 'hash': '#%EF%BF%BDnode' + } + }, + { + 'comment': 'Unpaired high surrogate 1', + 'href': 'https://github.com/', + 'new_value': '\uDE00', + 'expected': { + 'href': 'https://github.com/#%EF%BF%BD', + 'hash': '#%EF%BF%BD' + } + }, + { + 'comment': 'Unpaired high surrogate 2', + 'href': 'https://github.com/', + 'new_value': '\uDE00node', + 'expected': { + 'href': 'https://github.com/#%EF%BF%BDnode', + 'hash': '#%EF%BF%BDnode' + } + } + ] +}; diff --git a/test/fixtures/url-setter-tests.js b/test/fixtures/url-setter-tests.js new file mode 100644 index 00000000000000..6f769eaec7543d --- /dev/null +++ b/test/fixtures/url-setter-tests.js @@ -0,0 +1,1823 @@ +'use strict'; + +/* The following tests are copied from WPT. Modifications to them should be + upstreamed first. Refs: + https://github.com/w3c/web-platform-tests/blob/b30abaecf4/url/setters_tests.json + License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html +*/ +module.exports = +{ + "comment": [ + "## Tests for setters of https://url.spec.whatwg.org/#urlutils-members", + "", + "This file contains a JSON object.", + "Other than 'comment', each key is an attribute of the `URL` interface", + "defined in WHATWG’s URL Standard.", + "The values are arrays of test case objects for that attribute.", + "", + "To run a test case for the attribute `attr`:", + "", + "* Create a new `URL` object with the value for the 'href' key", + " the constructor single parameter. (Without a base URL.)", + " This must not throw.", + "* Set the attribute `attr` to (invoke its setter with)", + " with the value of for 'new_value' key.", + "* The value for the 'expected' key is another object.", + " For each `key` / `value` pair of that object,", + " get the attribute `key` (invoke its getter).", + " The returned string must be equal to `value`.", + "", + "Note: the 'href' setter is already covered by urltestdata.json." + ], + "protocol": [ + { + "comment": "The empty string is not a valid scheme. Setter leaves the URL unchanged.", + "href": "a://example.net", + "new_value": "", + "expected": { + "href": "a://example.net", + "protocol": "a:" + } + }, + { + "href": "a://example.net", + "new_value": "b", + "expected": { + "href": "b://example.net", + "protocol": "b:" + } + }, + { + "href": "javascript:alert(1)", + "new_value": "defuse", + "expected": { + "href": "defuse:alert(1)", + "protocol": "defuse:" + } + }, + { + "comment": "Upper-case ASCII is lower-cased", + "href": "a://example.net", + "new_value": "B", + "expected": { + "href": "b://example.net", + "protocol": "b:" + } + }, + { + "comment": "Non-ASCII is rejected", + "href": "a://example.net", + "new_value": "é", + "expected": { + "href": "a://example.net", + "protocol": "a:" + } + }, + { + "comment": "No leading digit", + "href": "a://example.net", + "new_value": "0b", + "expected": { + "href": "a://example.net", + "protocol": "a:" + } + }, + { + "comment": "No leading punctuation", + "href": "a://example.net", + "new_value": "+b", + "expected": { + "href": "a://example.net", + "protocol": "a:" + } + }, + { + "href": "a://example.net", + "new_value": "bC0+-.", + "expected": { + "href": "bc0+-.://example.net", + "protocol": "bc0+-.:" + } + }, + { + "comment": "Only some punctuation is acceptable", + "href": "a://example.net", + "new_value": "b,c", + "expected": { + "href": "a://example.net", + "protocol": "a:" + } + }, + { + "comment": "Non-ASCII is rejected", + "href": "a://example.net", + "new_value": "bé", + "expected": { + "href": "a://example.net", + "protocol": "a:" + } + }, + { + "comment": "Can’t switch from URL containing username/password/port to file", + "href": "http://test@example.net", + "new_value": "file", + "expected": { + "href": "http://test@example.net/", + "protocol": "http:" + } + }, + { + "href": "gopher://example.net:1234", + "new_value": "file", + "expected": { + "href": "gopher://example.net:1234/", + "protocol": "gopher:" + } + }, + { + "href": "wss://x:x@example.net:1234", + "new_value": "file", + "expected": { + "href": "wss://x:x@example.net:1234/", + "protocol": "wss:" + } + }, + { + "comment": "Can’t switch from file URL with no host", + "href": "file://localhost/", + "new_value": "http", + "expected": { + "href": "file:///", + "protocol": "file:" + } + }, + { + "href": "file:///test", + "new_value": "gopher", + "expected": { + "href": "file:///test", + "protocol": "file:" + } + }, + { + "href": "file:", + "new_value": "wss", + "expected": { + "href": "file:///", + "protocol": "file:" + } + }, + { + "comment": "Can’t switch from special scheme to non-special", + "href": "http://example.net", + "new_value": "b", + "expected": { + "href": "http://example.net/", + "protocol": "http:" + } + }, + { + "href": "file://hi/path", + "new_value": "s", + "expected": { + "href": "file://hi/path", + "protocol": "file:" + } + }, + { + "href": "https://example.net", + "new_value": "s", + "expected": { + "href": "https://example.net/", + "protocol": "https:" + } + }, + { + "href": "ftp://example.net", + "new_value": "test", + "expected": { + "href": "ftp://example.net/", + "protocol": "ftp:" + } + }, + { + "comment": "Cannot-be-a-base URL doesn’t have a host, but URL in a special scheme must.", + "href": "mailto:me@example.net", + "new_value": "http", + "expected": { + "href": "mailto:me@example.net", + "protocol": "mailto:" + } + }, + { + "comment": "Can’t switch from non-special scheme to special", + "href": "ssh://me@example.net", + "new_value": "http", + "expected": { + "href": "ssh://me@example.net", + "protocol": "ssh:" + } + }, + { + "href": "ssh://me@example.net", + "new_value": "gopher", + "expected": { + "href": "ssh://me@example.net", + "protocol": "ssh:" + } + }, + { + "href": "ssh://me@example.net", + "new_value": "file", + "expected": { + "href": "ssh://me@example.net", + "protocol": "ssh:" + } + }, + { + "href": "ssh://example.net", + "new_value": "file", + "expected": { + "href": "ssh://example.net", + "protocol": "ssh:" + } + }, + { + "href": "nonsense:///test", + "new_value": "https", + "expected": { + "href": "nonsense:///test", + "protocol": "nonsense:" + } + }, + { + "comment": "Stuff after the first ':' is ignored", + "href": "http://example.net", + "new_value": "https:foo : bar", + "expected": { + "href": "https://example.net/", + "protocol": "https:" + } + }, + { + "comment": "Stuff after the first ':' is ignored", + "href": "data:text/html,

Test", + "new_value": "view-source+data:foo : bar", + "expected": { + "href": "view-source+data:text/html,

Test", + "protocol": "view-source+data:" + } + }, + { + "comment": "Port is set to null if it is the default for new scheme.", + "href": "http://foo.com:443/", + "new_value": "https", + "expected": { + "href": "https://foo.com/", + "protocol": "https:", + "port": "" + } + } + ], + "username": [ + { + "comment": "No host means no username", + "href": "file:///home/you/index.html", + "new_value": "me", + "expected": { + "href": "file:///home/you/index.html", + "username": "" + } + }, + { + "comment": "No host means no username", + "href": "unix:/run/foo.socket", + "new_value": "me", + "expected": { + "href": "unix:/run/foo.socket", + "username": "" + } + }, + { + "comment": "Cannot-be-a-base means no username", + "href": "mailto:you@example.net", + "new_value": "me", + "expected": { + "href": "mailto:you@example.net", + "username": "" + } + }, + { + "href": "javascript:alert(1)", + "new_value": "wario", + "expected": { + "href": "javascript:alert(1)", + "username": "" + } + }, + { + "href": "http://example.net", + "new_value": "me", + "expected": { + "href": "http://me@example.net/", + "username": "me" + } + }, + { + "href": "http://:secret@example.net", + "new_value": "me", + "expected": { + "href": "http://me:secret@example.net/", + "username": "me" + } + }, + { + "href": "http://me@example.net", + "new_value": "", + "expected": { + "href": "http://example.net/", + "username": "" + } + }, + { + "href": "http://me:secret@example.net", + "new_value": "", + "expected": { + "href": "http://:secret@example.net/", + "username": "" + } + }, + { + "comment": "UTF-8 percent encoding with the userinfo encode set.", + "href": "http://example.net", + "new_value": "\u0000\u0001\t\n\r\u001f !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~\u007f\u0080\u0081Éé", + "expected": { + "href": "http://%00%01%09%0A%0D%1F%20!%22%23$%&'()*+,-.%2F09%3A%3B%3C%3D%3E%3F%40AZ%5B%5C%5D%5E_%60az%7B%7C%7D~%7F%C2%80%C2%81%C3%89%C3%A9@example.net/", + "username": "%00%01%09%0A%0D%1F%20!%22%23$%&'()*+,-.%2F09%3A%3B%3C%3D%3E%3F%40AZ%5B%5C%5D%5E_%60az%7B%7C%7D~%7F%C2%80%C2%81%C3%89%C3%A9" + } + }, + { + "comment": "Bytes already percent-encoded are left as-is.", + "href": "http://example.net", + "new_value": "%c3%89té", + "expected": { + "href": "http://%c3%89t%C3%A9@example.net/", + "username": "%c3%89t%C3%A9" + } + }, + { + "href": "sc:///", + "new_value": "x", + "expected": { + "href": "sc:///", + "username": "" + } + }, + { + "href": "javascript://x/", + "new_value": "wario", + "expected": { + "href": "javascript://wario@x/", + "username": "wario" + } + }, + { + "href": "file://test/", + "new_value": "test", + "expected": { + "href": "file://test/", + "username": "" + } + } + ], + "password": [ + { + "comment": "No host means no password", + "href": "file:///home/me/index.html", + "new_value": "secret", + "expected": { + "href": "file:///home/me/index.html", + "password": "" + } + }, + { + "comment": "No host means no password", + "href": "unix:/run/foo.socket", + "new_value": "secret", + "expected": { + "href": "unix:/run/foo.socket", + "password": "" + } + }, + { + "comment": "Cannot-be-a-base means no password", + "href": "mailto:me@example.net", + "new_value": "secret", + "expected": { + "href": "mailto:me@example.net", + "password": "" + } + }, + { + "href": "http://example.net", + "new_value": "secret", + "expected": { + "href": "http://:secret@example.net/", + "password": "secret" + } + }, + { + "href": "http://me@example.net", + "new_value": "secret", + "expected": { + "href": "http://me:secret@example.net/", + "password": "secret" + } + }, + { + "href": "http://:secret@example.net", + "new_value": "", + "expected": { + "href": "http://example.net/", + "password": "" + } + }, + { + "href": "http://me:secret@example.net", + "new_value": "", + "expected": { + "href": "http://me@example.net/", + "password": "" + } + }, + { + "comment": "UTF-8 percent encoding with the userinfo encode set.", + "href": "http://example.net", + "new_value": "\u0000\u0001\t\n\r\u001f !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~\u007f\u0080\u0081Éé", + "expected": { + "href": "http://:%00%01%09%0A%0D%1F%20!%22%23$%&'()*+,-.%2F09%3A%3B%3C%3D%3E%3F%40AZ%5B%5C%5D%5E_%60az%7B%7C%7D~%7F%C2%80%C2%81%C3%89%C3%A9@example.net/", + "password": "%00%01%09%0A%0D%1F%20!%22%23$%&'()*+,-.%2F09%3A%3B%3C%3D%3E%3F%40AZ%5B%5C%5D%5E_%60az%7B%7C%7D~%7F%C2%80%C2%81%C3%89%C3%A9" + } + }, + { + "comment": "Bytes already percent-encoded are left as-is.", + "href": "http://example.net", + "new_value": "%c3%89té", + "expected": { + "href": "http://:%c3%89t%C3%A9@example.net/", + "password": "%c3%89t%C3%A9" + } + }, + { + "href": "sc:///", + "new_value": "x", + "expected": { + "href": "sc:///", + "password": "" + } + }, + { + "href": "javascript://x/", + "new_value": "bowser", + "expected": { + "href": "javascript://:bowser@x/", + "password": "bowser" + } + }, + { + "href": "file://test/", + "new_value": "test", + "expected": { + "href": "file://test/", + "password": "" + } + } + ], + "host": [ + { + "comment": "Non-special scheme", + "href": "sc://x/", + "new_value": "\u0000", + "expected": { + "href": "sc://x/", + "host": "x", + "hostname": "x" + } + }, + { + "href": "sc://x/", + "new_value": "\u0009", + "expected": { + "href": "sc:///", + "host": "", + "hostname": "" + } + }, + { + "href": "sc://x/", + "new_value": "\u000A", + "expected": { + "href": "sc:///", + "host": "", + "hostname": "" + } + }, + { + "href": "sc://x/", + "new_value": "\u000D", + "expected": { + "href": "sc:///", + "host": "", + "hostname": "" + } + }, + { + "href": "sc://x/", + "new_value": " ", + "expected": { + "href": "sc://x/", + "host": "x", + "hostname": "x" + } + }, + { + "href": "sc://x/", + "new_value": "#", + "expected": { + "href": "sc:///", + "host": "", + "hostname": "" + } + }, + { + "href": "sc://x/", + "new_value": "/", + "expected": { + "href": "sc:///", + "host": "", + "hostname": "" + } + }, + { + "href": "sc://x/", + "new_value": "?", + "expected": { + "href": "sc:///", + "host": "", + "hostname": "" + } + }, + { + "href": "sc://x/", + "new_value": "@", + "expected": { + "href": "sc://x/", + "host": "x", + "hostname": "x" + } + }, + { + "href": "sc://x/", + "new_value": "ß", + "expected": { + "href": "sc://%C3%9F/", + "host": "%C3%9F", + "hostname": "%C3%9F" + } + }, + { + "comment": "IDNA Nontransitional_Processing", + "href": "https://x/", + "new_value": "ß", + "expected": { + "href": "https://xn--zca/", + "host": "xn--zca", + "hostname": "xn--zca" + } + }, + { + "comment": "Cannot-be-a-base means no host", + "href": "mailto:me@example.net", + "new_value": "example.com", + "expected": { + "href": "mailto:me@example.net", + "host": "" + } + }, + { + "comment": "Cannot-be-a-base means no password", + "href": "data:text/plain,Stuff", + "new_value": "example.net", + "expected": { + "href": "data:text/plain,Stuff", + "host": "" + } + }, + { + "href": "http://example.net", + "new_value": "example.com:8080", + "expected": { + "href": "http://example.com:8080/", + "host": "example.com:8080", + "hostname": "example.com", + "port": "8080" + } + }, + { + "comment": "Port number is unchanged if not specified in the new value", + "href": "http://example.net:8080", + "new_value": "example.com", + "expected": { + "href": "http://example.com:8080/", + "host": "example.com:8080", + "hostname": "example.com", + "port": "8080" + } + }, + { + "comment": "Port number is unchanged if not specified", + "href": "http://example.net:8080", + "new_value": "example.com:", + "expected": { + "href": "http://example.com:8080/", + "host": "example.com:8080", + "hostname": "example.com", + "port": "8080" + } + }, + { + "comment": "The empty host is not valid for special schemes", + "href": "http://example.net", + "new_value": "", + "expected": { + "href": "http://example.net/", + "host": "example.net" + } + }, + { + "comment": "The empty host is OK for non-special schemes", + "href": "view-source+http://example.net/foo", + "new_value": "", + "expected": { + "href": "view-source+http:///foo", + "host": "" + } + }, + { + "comment": "Path-only URLs can gain a host", + "href": "a:/foo", + "new_value": "example.net", + "expected": { + "href": "a://example.net/foo", + "host": "example.net" + } + }, + { + "comment": "IPv4 address syntax is normalized", + "href": "http://example.net", + "new_value": "0x7F000001:8080", + "expected": { + "href": "http://127.0.0.1:8080/", + "host": "127.0.0.1:8080", + "hostname": "127.0.0.1", + "port": "8080" + } + }, + { + "comment": "IPv6 address syntax is normalized", + "href": "http://example.net", + "new_value": "[::0:01]:2", + "expected": { + "href": "http://[::1]:2/", + "host": "[::1]:2", + "hostname": "[::1]", + "port": "2" + } + }, + { + "comment": "Default port number is removed", + "href": "http://example.net", + "new_value": "example.com:80", + "expected": { + "href": "http://example.com/", + "host": "example.com", + "hostname": "example.com", + "port": "" + } + }, + { + "comment": "Default port number is removed", + "href": "https://example.net", + "new_value": "example.com:443", + "expected": { + "href": "https://example.com/", + "host": "example.com", + "hostname": "example.com", + "port": "" + } + }, + { + "comment": "Default port number is only removed for the relevant scheme", + "href": "https://example.net", + "new_value": "example.com:80", + "expected": { + "href": "https://example.com:80/", + "host": "example.com:80", + "hostname": "example.com", + "port": "80" + } + }, + { + "comment": "Stuff after a / delimiter is ignored", + "href": "http://example.net/path", + "new_value": "example.com/stuff", + "expected": { + "href": "http://example.com/path", + "host": "example.com", + "hostname": "example.com", + "port": "" + } + }, + { + "comment": "Stuff after a / delimiter is ignored", + "href": "http://example.net/path", + "new_value": "example.com:8080/stuff", + "expected": { + "href": "http://example.com:8080/path", + "host": "example.com:8080", + "hostname": "example.com", + "port": "8080" + } + }, + { + "comment": "Stuff after a ? delimiter is ignored", + "href": "http://example.net/path", + "new_value": "example.com?stuff", + "expected": { + "href": "http://example.com/path", + "host": "example.com", + "hostname": "example.com", + "port": "" + } + }, + { + "comment": "Stuff after a ? delimiter is ignored", + "href": "http://example.net/path", + "new_value": "example.com:8080?stuff", + "expected": { + "href": "http://example.com:8080/path", + "host": "example.com:8080", + "hostname": "example.com", + "port": "8080" + } + }, + { + "comment": "Stuff after a # delimiter is ignored", + "href": "http://example.net/path", + "new_value": "example.com#stuff", + "expected": { + "href": "http://example.com/path", + "host": "example.com", + "hostname": "example.com", + "port": "" + } + }, + { + "comment": "Stuff after a # delimiter is ignored", + "href": "http://example.net/path", + "new_value": "example.com:8080#stuff", + "expected": { + "href": "http://example.com:8080/path", + "host": "example.com:8080", + "hostname": "example.com", + "port": "8080" + } + }, + { + "comment": "Stuff after a \\ delimiter is ignored for special schemes", + "href": "http://example.net/path", + "new_value": "example.com\\stuff", + "expected": { + "href": "http://example.com/path", + "host": "example.com", + "hostname": "example.com", + "port": "" + } + }, + { + "comment": "Stuff after a \\ delimiter is ignored for special schemes", + "href": "http://example.net/path", + "new_value": "example.com:8080\\stuff", + "expected": { + "href": "http://example.com:8080/path", + "host": "example.com:8080", + "hostname": "example.com", + "port": "8080" + } + }, + { + "comment": "\\ is not a delimiter for non-special schemes, but still forbidden in hosts", + "href": "view-source+http://example.net/path", + "new_value": "example.com\\stuff", + "expected": { + "href": "view-source+http://example.net/path", + "host": "example.net", + "hostname": "example.net", + "port": "" + } + }, + { + "comment": "Anything other than ASCII digit stops the port parser in a setter but is not an error", + "href": "view-source+http://example.net/path", + "new_value": "example.com:8080stuff2", + "expected": { + "href": "view-source+http://example.com:8080/path", + "host": "example.com:8080", + "hostname": "example.com", + "port": "8080" + } + }, + { + "comment": "Anything other than ASCII digit stops the port parser in a setter but is not an error", + "href": "http://example.net/path", + "new_value": "example.com:8080stuff2", + "expected": { + "href": "http://example.com:8080/path", + "host": "example.com:8080", + "hostname": "example.com", + "port": "8080" + } + }, + { + "comment": "Anything other than ASCII digit stops the port parser in a setter but is not an error", + "href": "http://example.net/path", + "new_value": "example.com:8080+2", + "expected": { + "href": "http://example.com:8080/path", + "host": "example.com:8080", + "hostname": "example.com", + "port": "8080" + } + }, + { + "comment": "Port numbers are 16 bit integers", + "href": "http://example.net/path", + "new_value": "example.com:65535", + "expected": { + "href": "http://example.com:65535/path", + "host": "example.com:65535", + "hostname": "example.com", + "port": "65535" + } + }, + { + "comment": "Port numbers are 16 bit integers, overflowing is an error. Hostname is still set, though.", + "href": "http://example.net/path", + "new_value": "example.com:65536", + "expected": { + "href": "http://example.com/path", + "host": "example.com", + "hostname": "example.com", + "port": "" + } + }, + { + "comment": "Broken IPv6", + "href": "http://example.net/", + "new_value": "[google.com]", + "expected": { + "href": "http://example.net/", + "host": "example.net", + "hostname": "example.net" + } + }, + { + "href": "http://example.net/", + "new_value": "[::1.2.3.4x]", + "expected": { + "href": "http://example.net/", + "host": "example.net", + "hostname": "example.net" + } + }, + { + "href": "http://example.net/", + "new_value": "[::1.2.3.]", + "expected": { + "href": "http://example.net/", + "host": "example.net", + "hostname": "example.net" + } + }, + { + "href": "http://example.net/", + "new_value": "[::1.2.]", + "expected": { + "href": "http://example.net/", + "host": "example.net", + "hostname": "example.net" + } + }, + { + "href": "http://example.net/", + "new_value": "[::1.]", + "expected": { + "href": "http://example.net/", + "host": "example.net", + "hostname": "example.net" + } + }, + { + "href": "file://y/", + "new_value": "x:123", + "expected": { + "href": "file://y/", + "host": "y", + "hostname": "y", + "port": "" + } + }, + { + "href": "file://y/", + "new_value": "loc%41lhost", + "expected": { + "href": "file:///", + "host": "", + "hostname": "", + "port": "" + } + }, + { + "href": "file://hi/x", + "new_value": "", + "expected": { + "href": "file:///x", + "host": "", + "hostname": "", + "port": "" + } + }, + { + "href": "sc://test@test/", + "new_value": "", + "expected": { + "href": "sc://test@test/", + "host": "test", + "hostname": "test", + "username": "test" + } + }, + { + "href": "sc://test:12/", + "new_value": "", + "expected": { + "href": "sc://test:12/", + "host": "test:12", + "hostname": "test", + "port": "12" + } + } + ], + "hostname": [ + { + "comment": "Non-special scheme", + "href": "sc://x/", + "new_value": "\u0000", + "expected": { + "href": "sc://x/", + "host": "x", + "hostname": "x" + } + }, + { + "href": "sc://x/", + "new_value": "\u0009", + "expected": { + "href": "sc:///", + "host": "", + "hostname": "" + } + }, + { + "href": "sc://x/", + "new_value": "\u000A", + "expected": { + "href": "sc:///", + "host": "", + "hostname": "" + } + }, + { + "href": "sc://x/", + "new_value": "\u000D", + "expected": { + "href": "sc:///", + "host": "", + "hostname": "" + } + }, + { + "href": "sc://x/", + "new_value": " ", + "expected": { + "href": "sc://x/", + "host": "x", + "hostname": "x" + } + }, + { + "href": "sc://x/", + "new_value": "#", + "expected": { + "href": "sc:///", + "host": "", + "hostname": "" + } + }, + { + "href": "sc://x/", + "new_value": "/", + "expected": { + "href": "sc:///", + "host": "", + "hostname": "" + } + }, + { + "href": "sc://x/", + "new_value": "?", + "expected": { + "href": "sc:///", + "host": "", + "hostname": "" + } + }, + { + "href": "sc://x/", + "new_value": "@", + "expected": { + "href": "sc://x/", + "host": "x", + "hostname": "x" + } + }, + { + "comment": "Cannot-be-a-base means no host", + "href": "mailto:me@example.net", + "new_value": "example.com", + "expected": { + "href": "mailto:me@example.net", + "host": "" + } + }, + { + "comment": "Cannot-be-a-base means no password", + "href": "data:text/plain,Stuff", + "new_value": "example.net", + "expected": { + "href": "data:text/plain,Stuff", + "host": "" + } + }, + { + "href": "http://example.net:8080", + "new_value": "example.com", + "expected": { + "href": "http://example.com:8080/", + "host": "example.com:8080", + "hostname": "example.com", + "port": "8080" + } + }, + { + "comment": "The empty host is not valid for special schemes", + "href": "http://example.net", + "new_value": "", + "expected": { + "href": "http://example.net/", + "host": "example.net" + } + }, + { + "comment": "The empty host is OK for non-special schemes", + "href": "view-source+http://example.net/foo", + "new_value": "", + "expected": { + "href": "view-source+http:///foo", + "host": "" + } + }, + { + "comment": "Path-only URLs can gain a host", + "href": "a:/foo", + "new_value": "example.net", + "expected": { + "href": "a://example.net/foo", + "host": "example.net" + } + }, + { + "comment": "IPv4 address syntax is normalized", + "href": "http://example.net:8080", + "new_value": "0x7F000001", + "expected": { + "href": "http://127.0.0.1:8080/", + "host": "127.0.0.1:8080", + "hostname": "127.0.0.1", + "port": "8080" + } + }, + { + "comment": "IPv6 address syntax is normalized", + "href": "http://example.net", + "new_value": "[::0:01]", + "expected": { + "href": "http://[::1]/", + "host": "[::1]", + "hostname": "[::1]", + "port": "" + } + }, + { + "comment": "Stuff after a : delimiter is ignored", + "href": "http://example.net/path", + "new_value": "example.com:8080", + "expected": { + "href": "http://example.com/path", + "host": "example.com", + "hostname": "example.com", + "port": "" + } + }, + { + "comment": "Stuff after a : delimiter is ignored", + "href": "http://example.net:8080/path", + "new_value": "example.com:", + "expected": { + "href": "http://example.com:8080/path", + "host": "example.com:8080", + "hostname": "example.com", + "port": "8080" + } + }, + { + "comment": "Stuff after a / delimiter is ignored", + "href": "http://example.net/path", + "new_value": "example.com/stuff", + "expected": { + "href": "http://example.com/path", + "host": "example.com", + "hostname": "example.com", + "port": "" + } + }, + { + "comment": "Stuff after a ? delimiter is ignored", + "href": "http://example.net/path", + "new_value": "example.com?stuff", + "expected": { + "href": "http://example.com/path", + "host": "example.com", + "hostname": "example.com", + "port": "" + } + }, + { + "comment": "Stuff after a # delimiter is ignored", + "href": "http://example.net/path", + "new_value": "example.com#stuff", + "expected": { + "href": "http://example.com/path", + "host": "example.com", + "hostname": "example.com", + "port": "" + } + }, + { + "comment": "Stuff after a \\ delimiter is ignored for special schemes", + "href": "http://example.net/path", + "new_value": "example.com\\stuff", + "expected": { + "href": "http://example.com/path", + "host": "example.com", + "hostname": "example.com", + "port": "" + } + }, + { + "comment": "\\ is not a delimiter for non-special schemes, but still forbidden in hosts", + "href": "view-source+http://example.net/path", + "new_value": "example.com\\stuff", + "expected": { + "href": "view-source+http://example.net/path", + "host": "example.net", + "hostname": "example.net", + "port": "" + } + }, + { + "comment": "Broken IPv6", + "href": "http://example.net/", + "new_value": "[google.com]", + "expected": { + "href": "http://example.net/", + "host": "example.net", + "hostname": "example.net" + } + }, + { + "href": "http://example.net/", + "new_value": "[::1.2.3.4x]", + "expected": { + "href": "http://example.net/", + "host": "example.net", + "hostname": "example.net" + } + }, + { + "href": "http://example.net/", + "new_value": "[::1.2.3.]", + "expected": { + "href": "http://example.net/", + "host": "example.net", + "hostname": "example.net" + } + }, + { + "href": "http://example.net/", + "new_value": "[::1.2.]", + "expected": { + "href": "http://example.net/", + "host": "example.net", + "hostname": "example.net" + } + }, + { + "href": "http://example.net/", + "new_value": "[::1.]", + "expected": { + "href": "http://example.net/", + "host": "example.net", + "hostname": "example.net" + } + }, + { + "href": "file://y/", + "new_value": "x:123", + "expected": { + "href": "file://y/", + "host": "y", + "hostname": "y", + "port": "" + } + }, + { + "href": "file://y/", + "new_value": "loc%41lhost", + "expected": { + "href": "file:///", + "host": "", + "hostname": "", + "port": "" + } + }, + { + "href": "file://hi/x", + "new_value": "", + "expected": { + "href": "file:///x", + "host": "", + "hostname": "", + "port": "" + } + }, + { + "href": "sc://test@test/", + "new_value": "", + "expected": { + "href": "sc://test@test/", + "host": "test", + "hostname": "test", + "username": "test" + } + }, + { + "href": "sc://test:12/", + "new_value": "", + "expected": { + "href": "sc://test:12/", + "host": "test:12", + "hostname": "test", + "port": "12" + } + } + ], + "port": [ + { + "href": "http://example.net", + "new_value": "8080", + "expected": { + "href": "http://example.net:8080/", + "host": "example.net:8080", + "hostname": "example.net", + "port": "8080" + } + }, + { + "comment": "Port number is removed if empty is the new value", + "href": "http://example.net:8080", + "new_value": "", + "expected": { + "href": "http://example.net/", + "host": "example.net", + "hostname": "example.net", + "port": "" + } + }, + { + "comment": "Default port number is removed", + "href": "http://example.net:8080", + "new_value": "80", + "expected": { + "href": "http://example.net/", + "host": "example.net", + "hostname": "example.net", + "port": "" + } + }, + { + "comment": "Default port number is removed", + "href": "https://example.net:4433", + "new_value": "443", + "expected": { + "href": "https://example.net/", + "host": "example.net", + "hostname": "example.net", + "port": "" + } + }, + { + "comment": "Default port number is only removed for the relevant scheme", + "href": "https://example.net", + "new_value": "80", + "expected": { + "href": "https://example.net:80/", + "host": "example.net:80", + "hostname": "example.net", + "port": "80" + } + }, + { + "comment": "Stuff after a / delimiter is ignored", + "href": "http://example.net/path", + "new_value": "8080/stuff", + "expected": { + "href": "http://example.net:8080/path", + "host": "example.net:8080", + "hostname": "example.net", + "port": "8080" + } + }, + { + "comment": "Stuff after a ? delimiter is ignored", + "href": "http://example.net/path", + "new_value": "8080?stuff", + "expected": { + "href": "http://example.net:8080/path", + "host": "example.net:8080", + "hostname": "example.net", + "port": "8080" + } + }, + { + "comment": "Stuff after a # delimiter is ignored", + "href": "http://example.net/path", + "new_value": "8080#stuff", + "expected": { + "href": "http://example.net:8080/path", + "host": "example.net:8080", + "hostname": "example.net", + "port": "8080" + } + }, + { + "comment": "Stuff after a \\ delimiter is ignored for special schemes", + "href": "http://example.net/path", + "new_value": "8080\\stuff", + "expected": { + "href": "http://example.net:8080/path", + "host": "example.net:8080", + "hostname": "example.net", + "port": "8080" + } + }, + { + "comment": "Anything other than ASCII digit stops the port parser in a setter but is not an error", + "href": "view-source+http://example.net/path", + "new_value": "8080stuff2", + "expected": { + "href": "view-source+http://example.net:8080/path", + "host": "example.net:8080", + "hostname": "example.net", + "port": "8080" + } + }, + { + "comment": "Anything other than ASCII digit stops the port parser in a setter but is not an error", + "href": "http://example.net/path", + "new_value": "8080stuff2", + "expected": { + "href": "http://example.net:8080/path", + "host": "example.net:8080", + "hostname": "example.net", + "port": "8080" + } + }, + { + "comment": "Anything other than ASCII digit stops the port parser in a setter but is not an error", + "href": "http://example.net/path", + "new_value": "8080+2", + "expected": { + "href": "http://example.net:8080/path", + "host": "example.net:8080", + "hostname": "example.net", + "port": "8080" + } + }, + { + "comment": "Port numbers are 16 bit integers", + "href": "http://example.net/path", + "new_value": "65535", + "expected": { + "href": "http://example.net:65535/path", + "host": "example.net:65535", + "hostname": "example.net", + "port": "65535" + } + }, + { + "comment": "Port numbers are 16 bit integers, overflowing is an error", + "href": "http://example.net:8080/path", + "new_value": "65536", + "expected": { + "href": "http://example.net:8080/path", + "host": "example.net:8080", + "hostname": "example.net", + "port": "8080" + } + }, + { + "comment": "Port numbers are 16 bit integers, overflowing is an error", + "href": "non-special://example.net:8080/path", + "new_value": "65536", + "expected": { + "href": "non-special://example.net:8080/path", + "host": "example.net:8080", + "hostname": "example.net", + "port": "8080" + } + }, + { + "href": "file://test/", + "new_value": "12", + "expected": { + "href": "file://test/", + "port": "" + } + }, + { + "href": "file://localhost/", + "new_value": "12", + "expected": { + "href": "file:///", + "port": "" + } + }, + { + "href": "non-base:value", + "new_value": "12", + "expected": { + "href": "non-base:value", + "port": "" + } + }, + { + "href": "sc:///", + "new_value": "12", + "expected": { + "href": "sc:///", + "port": "" + } + }, + { + "href": "sc://x/", + "new_value": "12", + "expected": { + "href": "sc://x:12/", + "port": "12" + } + }, + { + "href": "javascript://x/", + "new_value": "12", + "expected": { + "href": "javascript://x:12/", + "port": "12" + } + } + ], + "pathname": [ + { + "comment": "Cannot-be-a-base don’t have a path", + "href": "mailto:me@example.net", + "new_value": "/foo", + "expected": { + "href": "mailto:me@example.net", + "pathname": "me@example.net" + } + }, + { + "href": "unix:/run/foo.socket?timeout=10", + "new_value": "/var/log/../run/bar.socket", + "expected": { + "href": "unix:/var/run/bar.socket?timeout=10", + "pathname": "/var/run/bar.socket" + } + }, + { + "href": "https://example.net#nav", + "new_value": "home", + "expected": { + "href": "https://example.net/home#nav", + "pathname": "/home" + } + }, + { + "href": "https://example.net#nav", + "new_value": "../home", + "expected": { + "href": "https://example.net/home#nav", + "pathname": "/home" + } + }, + { + "comment": "\\ is a segment delimiter for 'special' URLs", + "href": "http://example.net/home?lang=fr#nav", + "new_value": "\\a\\%2E\\b\\%2e.\\c", + "expected": { + "href": "http://example.net/a/c?lang=fr#nav", + "pathname": "/a/c" + } + }, + { + "comment": "\\ is *not* a segment delimiter for non-'special' URLs", + "href": "view-source+http://example.net/home?lang=fr#nav", + "new_value": "\\a\\%2E\\b\\%2e.\\c", + "expected": { + "href": "view-source+http://example.net/\\a\\%2E\\b\\%2e.\\c?lang=fr#nav", + "pathname": "/\\a\\%2E\\b\\%2e.\\c" + } + }, + { + "comment": "UTF-8 percent encoding with the default encode set. Tabs and newlines are removed.", + "href": "a:/", + "new_value": "\u0000\u0001\t\n\r\u001f !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~\u007f\u0080\u0081Éé", + "expected": { + "href": "a:/%00%01%1F%20!%22%23$%&'()*+,-./09:;%3C=%3E%3F@AZ[\\]^_%60az%7B|%7D~%7F%C2%80%C2%81%C3%89%C3%A9", + "pathname": "/%00%01%1F%20!%22%23$%&'()*+,-./09:;%3C=%3E%3F@AZ[\\]^_%60az%7B|%7D~%7F%C2%80%C2%81%C3%89%C3%A9" + } + }, + { + "comment": "Bytes already percent-encoded are left as-is, including %2E outside dotted segments.", + "href": "http://example.net", + "new_value": "%2e%2E%c3%89té", + "expected": { + "href": "http://example.net/%2e%2E%c3%89t%C3%A9", + "pathname": "/%2e%2E%c3%89t%C3%A9" + } + }, + { + "comment": "? needs to be encoded", + "href": "http://example.net", + "new_value": "?", + "expected": { + "href": "http://example.net/%3F", + "pathname": "/%3F" + } + }, + { + "comment": "# needs to be encoded", + "href": "http://example.net", + "new_value": "#", + "expected": { + "href": "http://example.net/%23", + "pathname": "/%23" + } + }, + { + "comment": "? needs to be encoded, non-special scheme", + "href": "sc://example.net", + "new_value": "?", + "expected": { + "href": "sc://example.net/%3F", + "pathname": "/%3F" + } + }, + { + "comment": "# needs to be encoded, non-special scheme", + "href": "sc://example.net", + "new_value": "#", + "expected": { + "href": "sc://example.net/%23", + "pathname": "/%23" + } + }, + { + "comment": "File URLs and (back)slashes", + "href": "file://monkey/", + "new_value": "\\\\", + "expected": { + "href": "file://monkey/", + "pathname": "/" + } + }, + { + "comment": "File URLs and (back)slashes", + "href": "file:///unicorn", + "new_value": "//\\/", + "expected": { + "href": "file:///", + "pathname": "/" + } + }, + { + "comment": "File URLs and (back)slashes", + "href": "file:///unicorn", + "new_value": "//monkey/..//", + "expected": { + "href": "file:///", + "pathname": "/" + } + } + ], + "search": [ + { + "href": "https://example.net#nav", + "new_value": "lang=fr", + "expected": { + "href": "https://example.net/?lang=fr#nav", + "search": "?lang=fr" + } + }, + { + "href": "https://example.net?lang=en-US#nav", + "new_value": "lang=fr", + "expected": { + "href": "https://example.net/?lang=fr#nav", + "search": "?lang=fr" + } + }, + { + "href": "https://example.net?lang=en-US#nav", + "new_value": "?lang=fr", + "expected": { + "href": "https://example.net/?lang=fr#nav", + "search": "?lang=fr" + } + }, + { + "href": "https://example.net?lang=en-US#nav", + "new_value": "??lang=fr", + "expected": { + "href": "https://example.net/??lang=fr#nav", + "search": "??lang=fr" + } + }, + { + "href": "https://example.net?lang=en-US#nav", + "new_value": "?", + "expected": { + "href": "https://example.net/?#nav", + "search": "" + } + }, + { + "href": "https://example.net?lang=en-US#nav", + "new_value": "", + "expected": { + "href": "https://example.net/#nav", + "search": "" + } + }, + { + "href": "https://example.net?lang=en-US", + "new_value": "", + "expected": { + "href": "https://example.net/", + "search": "" + } + }, + { + "href": "https://example.net", + "new_value": "", + "expected": { + "href": "https://example.net/", + "search": "" + } + }, + { + "comment": "UTF-8 percent encoding with the query encode set. Tabs and newlines are removed.", + "href": "a:/", + "new_value": "\u0000\u0001\t\n\r\u001f !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~\u007f\u0080\u0081Éé", + "expected": { + "href": "a:/?%00%01%1F%20!%22%23$%&'()*+,-./09:;%3C=%3E?@AZ[\\]^_`az{|}~%7F%C2%80%C2%81%C3%89%C3%A9", + "search": "?%00%01%1F%20!%22%23$%&'()*+,-./09:;%3C=%3E?@AZ[\\]^_`az{|}~%7F%C2%80%C2%81%C3%89%C3%A9" + } + }, + { + "comment": "Bytes already percent-encoded are left as-is", + "href": "http://example.net", + "new_value": "%c3%89té", + "expected": { + "href": "http://example.net/?%c3%89t%C3%A9", + "search": "?%c3%89t%C3%A9" + } + } + ], + "hash": [ + { + "href": "https://example.net", + "new_value": "main", + "expected": { + "href": "https://example.net/#main", + "hash": "#main" + } + }, + { + "href": "https://example.net#nav", + "new_value": "main", + "expected": { + "href": "https://example.net/#main", + "hash": "#main" + } + }, + { + "href": "https://example.net?lang=en-US", + "new_value": "##nav", + "expected": { + "href": "https://example.net/?lang=en-US##nav", + "hash": "##nav" + } + }, + { + "href": "https://example.net?lang=en-US#nav", + "new_value": "#main", + "expected": { + "href": "https://example.net/?lang=en-US#main", + "hash": "#main" + } + }, + { + "href": "https://example.net?lang=en-US#nav", + "new_value": "#", + "expected": { + "href": "https://example.net/?lang=en-US#", + "hash": "" + } + }, + { + "href": "https://example.net?lang=en-US#nav", + "new_value": "", + "expected": { + "href": "https://example.net/?lang=en-US", + "hash": "" + } + }, + { + "comment": "Simple percent-encoding; nuls, tabs, and newlines are removed", + "href": "a:/", + "new_value": "\u0000\u0001\t\n\r\u001f !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~\u007f\u0080\u0081Éé", + "expected": { + "href": "a:/#%01%1F !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~%7F%C2%80%C2%81%C3%89%C3%A9", + "hash": "#%01%1F !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~%7F%C2%80%C2%81%C3%89%C3%A9" + } + }, + { + "comment": "Bytes already percent-encoded are left as-is", + "href": "http://example.net", + "new_value": "%c3%89té", + "expected": { + "href": "http://example.net/#%c3%89t%C3%A9", + "hash": "#%c3%89t%C3%A9" + } + }, + { + "href": "javascript:alert(1)", + "new_value": "castle", + "expected": { + "href": "javascript:alert(1)#castle", + "hash": "#castle" + } + } + ] +} diff --git a/test/fixtures/url-tests-additional.js b/test/fixtures/url-tests-additional.js new file mode 100644 index 00000000000000..c1c640f4bb4b7d --- /dev/null +++ b/test/fixtures/url-tests-additional.js @@ -0,0 +1,36 @@ +'use strict'; + +// This file contains test cases not part of the WPT + +module.exports = [ + { + // surrogate pair + 'url': 'https://github.com/nodejs/\uD83D\uDE00node', + 'protocol': 'https:', + 'pathname': '/nodejs/%F0%9F%98%80node' + }, + { + // unpaired low surrogate + 'url': 'https://github.com/nodejs/\uD83D', + 'protocol': 'https:', + 'pathname': '/nodejs/%EF%BF%BD' + }, + { + // unpaired low surrogate + 'url': 'https://github.com/nodejs/\uD83Dnode', + 'protocol': 'https:', + 'pathname': '/nodejs/%EF%BF%BDnode' + }, + { + // unmatched high surrogate + 'url': 'https://github.com/nodejs/\uDE00', + 'protocol': 'https:', + 'pathname': '/nodejs/%EF%BF%BD' + }, + { + // unmatched high surrogate + 'url': 'https://github.com/nodejs/\uDE00node', + 'protocol': 'https:', + 'pathname': '/nodejs/%EF%BF%BDnode' + } +]; diff --git a/test/fixtures/url-tests.js b/test/fixtures/url-tests.js new file mode 100644 index 00000000000000..48f77fe0774d64 --- /dev/null +++ b/test/fixtures/url-tests.js @@ -0,0 +1,6565 @@ +'use strict'; + +/* The following tests are copied from WPT. Modifications to them should be + upstreamed first. Refs: + https://github.com/w3c/web-platform-tests/blob/11757f1/url/urltestdata.json + License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html +*/ +module.exports = +[ + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/segments.js", + { + "input": "http://example\t.\norg", + "base": "http://example.org/foo/bar", + "href": "http://example.org/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://user:pass@foo:21/bar;par?b#c", + "base": "http://example.org/foo/bar", + "href": "http://user:pass@foo:21/bar;par?b#c", + "origin": "http://foo:21", + "protocol": "http:", + "username": "user", + "password": "pass", + "host": "foo:21", + "hostname": "foo", + "port": "21", + "pathname": "/bar;par", + "search": "?b", + "hash": "#c" + }, + { + "input": "https://test:@test", + "base": "about:blank", + "href": "https://test@test/", + "origin": "https://test", + "protocol": "https:", + "username": "test", + "password": "", + "host": "test", + "hostname": "test", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://:@test", + "base": "about:blank", + "href": "https://test/", + "origin": "https://test", + "protocol": "https:", + "username": "", + "password": "", + "host": "test", + "hostname": "test", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "non-special://test:@test/x", + "base": "about:blank", + "href": "non-special://test@test/x", + "origin": "null", + "protocol": "non-special:", + "username": "test", + "password": "", + "host": "test", + "hostname": "test", + "port": "", + "pathname": "/x", + "search": "", + "hash": "" + }, + { + "input": "non-special://:@test/x", + "base": "about:blank", + "href": "non-special://test/x", + "origin": "null", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "test", + "hostname": "test", + "port": "", + "pathname": "/x", + "search": "", + "hash": "" + }, + { + "input": "http:foo.com", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/foo.com", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/foo.com", + "search": "", + "hash": "" + }, + { + "input": "\t :foo.com \n", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:foo.com", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:foo.com", + "search": "", + "hash": "" + }, + { + "input": " foo.com ", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/foo.com", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/foo.com", + "search": "", + "hash": "" + }, + { + "input": "a:\t foo.com", + "base": "http://example.org/foo/bar", + "href": "a: foo.com", + "origin": "null", + "protocol": "a:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": " foo.com", + "search": "", + "hash": "" + }, + { + "input": "http://f:21/ b ? d # e ", + "base": "http://example.org/foo/bar", + "href": "http://f:21/%20b%20?%20d%20# e", + "origin": "http://f:21", + "protocol": "http:", + "username": "", + "password": "", + "host": "f:21", + "hostname": "f", + "port": "21", + "pathname": "/%20b%20", + "search": "?%20d%20", + "hash": "# e" + }, + { + "input": "lolscheme:x x#x x", + "base": "about:blank", + "href": "lolscheme:x x#x x", + "protocol": "lolscheme:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "x x", + "search": "", + "hash": "#x x" + }, + { + "input": "http://f:/c", + "base": "http://example.org/foo/bar", + "href": "http://f/c", + "origin": "http://f", + "protocol": "http:", + "username": "", + "password": "", + "host": "f", + "hostname": "f", + "port": "", + "pathname": "/c", + "search": "", + "hash": "" + }, + { + "input": "http://f:0/c", + "base": "http://example.org/foo/bar", + "href": "http://f:0/c", + "origin": "http://f:0", + "protocol": "http:", + "username": "", + "password": "", + "host": "f:0", + "hostname": "f", + "port": "0", + "pathname": "/c", + "search": "", + "hash": "" + }, + { + "input": "http://f:00000000000000/c", + "base": "http://example.org/foo/bar", + "href": "http://f:0/c", + "origin": "http://f:0", + "protocol": "http:", + "username": "", + "password": "", + "host": "f:0", + "hostname": "f", + "port": "0", + "pathname": "/c", + "search": "", + "hash": "" + }, + { + "input": "http://f:00000000000000000000080/c", + "base": "http://example.org/foo/bar", + "href": "http://f/c", + "origin": "http://f", + "protocol": "http:", + "username": "", + "password": "", + "host": "f", + "hostname": "f", + "port": "", + "pathname": "/c", + "search": "", + "hash": "" + }, + { + "input": "http://f:b/c", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://f: /c", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://f:\n/c", + "base": "http://example.org/foo/bar", + "href": "http://f/c", + "origin": "http://f", + "protocol": "http:", + "username": "", + "password": "", + "host": "f", + "hostname": "f", + "port": "", + "pathname": "/c", + "search": "", + "hash": "" + }, + { + "input": "http://f:fifty-two/c", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://f:999999/c", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "non-special://f:999999/c", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://f: 21 / b ? d # e ", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "" + }, + { + "input": " \t", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "" + }, + { + "input": ":foo.com/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:foo.com/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:foo.com/", + "search": "", + "hash": "" + }, + { + "input": ":foo.com\\", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:foo.com/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:foo.com/", + "search": "", + "hash": "" + }, + { + "input": ":", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:", + "search": "", + "hash": "" + }, + { + "input": ":a", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:a", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:a", + "search": "", + "hash": "" + }, + { + "input": ":/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:/", + "search": "", + "hash": "" + }, + { + "input": ":\\", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:/", + "search": "", + "hash": "" + }, + { + "input": ":#", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:#", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:", + "search": "", + "hash": "" + }, + { + "input": "#", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "#/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "#/" + }, + { + "input": "#\\", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#\\", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "#\\" + }, + { + "input": "#;?", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#;?", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "#;?" + }, + { + "input": "?", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar?", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": ":23", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:23", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:23", + "search": "", + "hash": "" + }, + { + "input": "/:23", + "base": "http://example.org/foo/bar", + "href": "http://example.org/:23", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/:23", + "search": "", + "hash": "" + }, + { + "input": "::", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/::", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/::", + "search": "", + "hash": "" + }, + { + "input": "::23", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/::23", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/::23", + "search": "", + "hash": "" + }, + { + "input": "foo://", + "base": "http://example.org/foo/bar", + "href": "foo://", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "", + "search": "", + "hash": "" + }, + { + "input": "http://a:b@c:29/d", + "base": "http://example.org/foo/bar", + "href": "http://a:b@c:29/d", + "origin": "http://c:29", + "protocol": "http:", + "username": "a", + "password": "b", + "host": "c:29", + "hostname": "c", + "port": "29", + "pathname": "/d", + "search": "", + "hash": "" + }, + { + "input": "http::@c:29", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:@c:29", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:@c:29", + "search": "", + "hash": "" + }, + { + "input": "http://&a:foo(b]c@d:2/", + "base": "http://example.org/foo/bar", + "href": "http://&a:foo(b%5Dc@d:2/", + "origin": "http://d:2", + "protocol": "http:", + "username": "&a", + "password": "foo(b%5Dc", + "host": "d:2", + "hostname": "d", + "port": "2", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://::@c@d:2", + "base": "http://example.org/foo/bar", + "href": "http://:%3A%40c@d:2/", + "origin": "http://d:2", + "protocol": "http:", + "username": "", + "password": "%3A%40c", + "host": "d:2", + "hostname": "d", + "port": "2", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://foo.com:b@d/", + "base": "http://example.org/foo/bar", + "href": "http://foo.com:b@d/", + "origin": "http://d", + "protocol": "http:", + "username": "foo.com", + "password": "b", + "host": "d", + "hostname": "d", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://foo.com/\\@", + "base": "http://example.org/foo/bar", + "href": "http://foo.com//@", + "origin": "http://foo.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo.com", + "hostname": "foo.com", + "port": "", + "pathname": "//@", + "search": "", + "hash": "" + }, + { + "input": "http:\\\\foo.com\\", + "base": "http://example.org/foo/bar", + "href": "http://foo.com/", + "origin": "http://foo.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo.com", + "hostname": "foo.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:\\\\a\\b:c\\d@foo.com\\", + "base": "http://example.org/foo/bar", + "href": "http://a/b:c/d@foo.com/", + "origin": "http://a", + "protocol": "http:", + "username": "", + "password": "", + "host": "a", + "hostname": "a", + "port": "", + "pathname": "/b:c/d@foo.com/", + "search": "", + "hash": "" + }, + { + "input": "foo:/", + "base": "http://example.org/foo/bar", + "href": "foo:/", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "foo:/bar.com/", + "base": "http://example.org/foo/bar", + "href": "foo:/bar.com/", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/bar.com/", + "search": "", + "hash": "" + }, + { + "input": "foo://///////", + "base": "http://example.org/foo/bar", + "href": "foo://///////", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "///////", + "search": "", + "hash": "" + }, + { + "input": "foo://///////bar.com/", + "base": "http://example.org/foo/bar", + "href": "foo://///////bar.com/", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "///////bar.com/", + "search": "", + "hash": "" + }, + { + "input": "foo:////://///", + "base": "http://example.org/foo/bar", + "href": "foo:////://///", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "//://///", + "search": "", + "hash": "" + }, + { + "input": "c:/foo", + "base": "http://example.org/foo/bar", + "href": "c:/foo", + "origin": "null", + "protocol": "c:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/foo", + "search": "", + "hash": "" + }, + { + "input": "//foo/bar", + "base": "http://example.org/foo/bar", + "href": "http://foo/bar", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/bar", + "search": "", + "hash": "" + }, + { + "input": "http://foo/path;a??e#f#g", + "base": "http://example.org/foo/bar", + "href": "http://foo/path;a??e#f#g", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/path;a", + "search": "??e", + "hash": "#f#g" + }, + { + "input": "http://foo/abcd?efgh?ijkl", + "base": "http://example.org/foo/bar", + "href": "http://foo/abcd?efgh?ijkl", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/abcd", + "search": "?efgh?ijkl", + "hash": "" + }, + { + "input": "http://foo/abcd#foo?bar", + "base": "http://example.org/foo/bar", + "href": "http://foo/abcd#foo?bar", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/abcd", + "search": "", + "hash": "#foo?bar" + }, + { + "input": "[61:24:74]:98", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/[61:24:74]:98", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/[61:24:74]:98", + "search": "", + "hash": "" + }, + { + "input": "http:[61:27]/:foo", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/[61:27]/:foo", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/[61:27]/:foo", + "search": "", + "hash": "" + }, + { + "input": "http://[1::2]:3:4", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://2001::1", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://2001::1]", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://2001::1]:80", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://[2001::1]", + "base": "http://example.org/foo/bar", + "href": "http://[2001::1]/", + "origin": "http://[2001::1]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[2001::1]", + "hostname": "[2001::1]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[::127.0.0.1]", + "base": "http://example.org/foo/bar", + "href": "http://[::7f00:1]/", + "origin": "http://[::7f00:1]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[::7f00:1]", + "hostname": "[::7f00:1]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[0:0:0:0:0:0:13.1.68.3]", + "base": "http://example.org/foo/bar", + "href": "http://[::d01:4403]/", + "origin": "http://[::d01:4403]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[::d01:4403]", + "hostname": "[::d01:4403]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[2001::1]:80", + "base": "http://example.org/foo/bar", + "href": "http://[2001::1]/", + "origin": "http://[2001::1]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[2001::1]", + "hostname": "[2001::1]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/example.com/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/example.com/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "ftp:/example.com/", + "base": "http://example.org/foo/bar", + "href": "ftp://example.com/", + "origin": "ftp://example.com", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https:/example.com/", + "base": "http://example.org/foo/bar", + "href": "https://example.com/", + "origin": "https://example.com", + "protocol": "https:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "madeupscheme:/example.com/", + "base": "http://example.org/foo/bar", + "href": "madeupscheme:/example.com/", + "origin": "null", + "protocol": "madeupscheme:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "file:/example.com/", + "base": "http://example.org/foo/bar", + "href": "file:///example.com/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "file://example:1/", + "base": "about:blank", + "failure": true + }, + { + "input": "file://example:test/", + "base": "about:blank", + "failure": true + }, + { + "input": "file://example%/", + "base": "about:blank", + "failure": true + }, + { + "input": "file://[example]/", + "base": "about:blank", + "failure": true + }, + { + "input": "ftps:/example.com/", + "base": "http://example.org/foo/bar", + "href": "ftps:/example.com/", + "origin": "null", + "protocol": "ftps:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "gopher:/example.com/", + "base": "http://example.org/foo/bar", + "href": "gopher://example.com/", + "origin": "gopher://example.com", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws:/example.com/", + "base": "http://example.org/foo/bar", + "href": "ws://example.com/", + "origin": "ws://example.com", + "protocol": "ws:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss:/example.com/", + "base": "http://example.org/foo/bar", + "href": "wss://example.com/", + "origin": "wss://example.com", + "protocol": "wss:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "data:/example.com/", + "base": "http://example.org/foo/bar", + "href": "data:/example.com/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "javascript:/example.com/", + "base": "http://example.org/foo/bar", + "href": "javascript:/example.com/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "mailto:/example.com/", + "base": "http://example.org/foo/bar", + "href": "mailto:/example.com/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "http:example.com/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/example.com/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/example.com/", + "search": "", + "hash": "" + }, + { + "input": "ftp:example.com/", + "base": "http://example.org/foo/bar", + "href": "ftp://example.com/", + "origin": "ftp://example.com", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https:example.com/", + "base": "http://example.org/foo/bar", + "href": "https://example.com/", + "origin": "https://example.com", + "protocol": "https:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "madeupscheme:example.com/", + "base": "http://example.org/foo/bar", + "href": "madeupscheme:example.com/", + "origin": "null", + "protocol": "madeupscheme:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "ftps:example.com/", + "base": "http://example.org/foo/bar", + "href": "ftps:example.com/", + "origin": "null", + "protocol": "ftps:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "gopher:example.com/", + "base": "http://example.org/foo/bar", + "href": "gopher://example.com/", + "origin": "gopher://example.com", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws:example.com/", + "base": "http://example.org/foo/bar", + "href": "ws://example.com/", + "origin": "ws://example.com", + "protocol": "ws:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss:example.com/", + "base": "http://example.org/foo/bar", + "href": "wss://example.com/", + "origin": "wss://example.com", + "protocol": "wss:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "data:example.com/", + "base": "http://example.org/foo/bar", + "href": "data:example.com/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "javascript:example.com/", + "base": "http://example.org/foo/bar", + "href": "javascript:example.com/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "mailto:example.com/", + "base": "http://example.org/foo/bar", + "href": "mailto:example.com/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "/a/b/c", + "base": "http://example.org/foo/bar", + "href": "http://example.org/a/b/c", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/a/b/c", + "search": "", + "hash": "" + }, + { + "input": "/a/ /c", + "base": "http://example.org/foo/bar", + "href": "http://example.org/a/%20/c", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/a/%20/c", + "search": "", + "hash": "" + }, + { + "input": "/a%2fc", + "base": "http://example.org/foo/bar", + "href": "http://example.org/a%2fc", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/a%2fc", + "search": "", + "hash": "" + }, + { + "input": "/a/%2f/c", + "base": "http://example.org/foo/bar", + "href": "http://example.org/a/%2f/c", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/a/%2f/c", + "search": "", + "hash": "" + }, + { + "input": "#β", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#%CE%B2", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "#%CE%B2" + }, + { + "input": "data:text/html,test#test", + "base": "http://example.org/foo/bar", + "href": "data:text/html,test#test", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "text/html,test", + "search": "", + "hash": "#test" + }, + { + "input": "tel:1234567890", + "base": "http://example.org/foo/bar", + "href": "tel:1234567890", + "origin": "null", + "protocol": "tel:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "1234567890", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/file.html", + { + "input": "file:c:\\foo\\bar.html", + "base": "file:///tmp/mock/path", + "href": "file:///c:/foo/bar.html", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/c:/foo/bar.html", + "search": "", + "hash": "" + }, + { + "input": " File:c|////foo\\bar.html", + "base": "file:///tmp/mock/path", + "href": "file:///c:////foo/bar.html", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/c:////foo/bar.html", + "search": "", + "hash": "" + }, + { + "input": "C|/foo/bar", + "base": "file:///tmp/mock/path", + "href": "file:///C:/foo/bar", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "/C|\\foo\\bar", + "base": "file:///tmp/mock/path", + "href": "file:///C:/foo/bar", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "//C|/foo/bar", + "base": "file:///tmp/mock/path", + "href": "file:///C:/foo/bar", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "//server/file", + "base": "file:///tmp/mock/path", + "href": "file://server/file", + "protocol": "file:", + "username": "", + "password": "", + "host": "server", + "hostname": "server", + "port": "", + "pathname": "/file", + "search": "", + "hash": "" + }, + { + "input": "\\\\server\\file", + "base": "file:///tmp/mock/path", + "href": "file://server/file", + "protocol": "file:", + "username": "", + "password": "", + "host": "server", + "hostname": "server", + "port": "", + "pathname": "/file", + "search": "", + "hash": "" + }, + { + "input": "/\\server/file", + "base": "file:///tmp/mock/path", + "href": "file://server/file", + "protocol": "file:", + "username": "", + "password": "", + "host": "server", + "hostname": "server", + "port": "", + "pathname": "/file", + "search": "", + "hash": "" + }, + { + "input": "file:///foo/bar.txt", + "base": "file:///tmp/mock/path", + "href": "file:///foo/bar.txt", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/foo/bar.txt", + "search": "", + "hash": "" + }, + { + "input": "file:///home/me", + "base": "file:///tmp/mock/path", + "href": "file:///home/me", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/home/me", + "search": "", + "hash": "" + }, + { + "input": "//", + "base": "file:///tmp/mock/path", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "///", + "base": "file:///tmp/mock/path", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "///test", + "base": "file:///tmp/mock/path", + "href": "file:///test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "", + "hash": "" + }, + { + "input": "file://test", + "base": "file:///tmp/mock/path", + "href": "file://test/", + "protocol": "file:", + "username": "", + "password": "", + "host": "test", + "hostname": "test", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file://localhost", + "base": "file:///tmp/mock/path", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file://localhost/", + "base": "file:///tmp/mock/path", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file://localhost/test", + "base": "file:///tmp/mock/path", + "href": "file:///test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "", + "hash": "" + }, + { + "input": "test", + "base": "file:///tmp/mock/path", + "href": "file:///tmp/mock/test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/tmp/mock/test", + "search": "", + "hash": "" + }, + { + "input": "file:test", + "base": "file:///tmp/mock/path", + "href": "file:///tmp/mock/test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/tmp/mock/test", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/path.js", + { + "input": "http://example.com/././foo", + "base": "about:blank", + "href": "http://example.com/foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/./.foo", + "base": "about:blank", + "href": "http://example.com/.foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/.foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/.", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/./", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar/..", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar/../", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/..bar", + "base": "about:blank", + "href": "http://example.com/foo/..bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/..bar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar/../ton", + "base": "about:blank", + "href": "http://example.com/foo/ton", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/ton", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar/../ton/../../a", + "base": "about:blank", + "href": "http://example.com/a", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/a", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/../../..", + "base": "about:blank", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/../../../ton", + "base": "about:blank", + "href": "http://example.com/ton", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/ton", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/%2e", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/%2e%2", + "base": "about:blank", + "href": "http://example.com/foo/%2e%2", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/%2e%2", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/%2e./%2e%2e/.%2e/%2e.bar", + "base": "about:blank", + "href": "http://example.com/%2e.bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%2e.bar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com////../..", + "base": "about:blank", + "href": "http://example.com//", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "//", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar//../..", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar//..", + "base": "about:blank", + "href": "http://example.com/foo/bar/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/bar/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo", + "base": "about:blank", + "href": "http://example.com/foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/%20foo", + "base": "about:blank", + "href": "http://example.com/%20foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%20foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo%", + "base": "about:blank", + "href": "http://example.com/foo%", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo%2", + "base": "about:blank", + "href": "http://example.com/foo%2", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%2", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo%2zbar", + "base": "about:blank", + "href": "http://example.com/foo%2zbar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%2zbar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo%2©zbar", + "base": "about:blank", + "href": "http://example.com/foo%2%C3%82%C2%A9zbar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%2%C3%82%C2%A9zbar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo%41%7a", + "base": "about:blank", + "href": "http://example.com/foo%41%7a", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%41%7a", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo\t\u0091%91", + "base": "about:blank", + "href": "http://example.com/foo%C2%91%91", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%C2%91%91", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo%00%51", + "base": "about:blank", + "href": "http://example.com/foo%00%51", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%00%51", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/(%28:%3A%29)", + "base": "about:blank", + "href": "http://example.com/(%28:%3A%29)", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/(%28:%3A%29)", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/%3A%3a%3C%3c", + "base": "about:blank", + "href": "http://example.com/%3A%3a%3C%3c", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%3A%3a%3C%3c", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo\tbar", + "base": "about:blank", + "href": "http://example.com/foobar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foobar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com\\\\foo\\\\bar", + "base": "about:blank", + "href": "http://example.com//foo//bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "//foo//bar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/%7Ffp3%3Eju%3Dduvgw%3Dd", + "base": "about:blank", + "href": "http://example.com/%7Ffp3%3Eju%3Dduvgw%3Dd", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%7Ffp3%3Eju%3Dduvgw%3Dd", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/@asdf%40", + "base": "about:blank", + "href": "http://example.com/@asdf%40", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/@asdf%40", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/你好你好", + "base": "about:blank", + "href": "http://example.com/%E4%BD%A0%E5%A5%BD%E4%BD%A0%E5%A5%BD", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%E4%BD%A0%E5%A5%BD%E4%BD%A0%E5%A5%BD", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/‥/foo", + "base": "about:blank", + "href": "http://example.com/%E2%80%A5/foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%E2%80%A5/foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com//foo", + "base": "about:blank", + "href": "http://example.com/%EF%BB%BF/foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%EF%BB%BF/foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/‮/foo/‭/bar", + "base": "about:blank", + "href": "http://example.com/%E2%80%AE/foo/%E2%80%AD/bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%E2%80%AE/foo/%E2%80%AD/bar", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/relative.js", + { + "input": "http://www.google.com/foo?bar=baz#", + "base": "about:blank", + "href": "http://www.google.com/foo?bar=baz#", + "origin": "http://www.google.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.google.com", + "hostname": "www.google.com", + "port": "", + "pathname": "/foo", + "search": "?bar=baz", + "hash": "" + }, + { + "input": "http://www.google.com/foo?bar=baz# »", + "base": "about:blank", + "href": "http://www.google.com/foo?bar=baz# %C2%BB", + "origin": "http://www.google.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.google.com", + "hostname": "www.google.com", + "port": "", + "pathname": "/foo", + "search": "?bar=baz", + "hash": "# %C2%BB" + }, + { + "input": "data:test# »", + "base": "about:blank", + "href": "data:test# %C2%BB", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "test", + "search": "", + "hash": "# %C2%BB" + }, + { + "input": "http://www.google.com", + "base": "about:blank", + "href": "http://www.google.com/", + "origin": "http://www.google.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.google.com", + "hostname": "www.google.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://192.0x00A80001", + "base": "about:blank", + "href": "http://192.168.0.1/", + "origin": "http://192.168.0.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.0.1", + "hostname": "192.168.0.1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://www/foo%2Ehtml", + "base": "about:blank", + "href": "http://www/foo%2Ehtml", + "origin": "http://www", + "protocol": "http:", + "username": "", + "password": "", + "host": "www", + "hostname": "www", + "port": "", + "pathname": "/foo%2Ehtml", + "search": "", + "hash": "" + }, + { + "input": "http://www/foo/%2E/html", + "base": "about:blank", + "href": "http://www/foo/html", + "origin": "http://www", + "protocol": "http:", + "username": "", + "password": "", + "host": "www", + "hostname": "www", + "port": "", + "pathname": "/foo/html", + "search": "", + "hash": "" + }, + { + "input": "http://user:pass@/", + "base": "about:blank", + "failure": true + }, + { + "input": "http://%25DOMAIN:foobar@foodomain.com/", + "base": "about:blank", + "href": "http://%25DOMAIN:foobar@foodomain.com/", + "origin": "http://foodomain.com", + "protocol": "http:", + "username": "%25DOMAIN", + "password": "foobar", + "host": "foodomain.com", + "hostname": "foodomain.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:\\\\www.google.com\\foo", + "base": "about:blank", + "href": "http://www.google.com/foo", + "origin": "http://www.google.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.google.com", + "hostname": "www.google.com", + "port": "", + "pathname": "/foo", + "search": "", + "hash": "" + }, + { + "input": "http://foo:80/", + "base": "about:blank", + "href": "http://foo/", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://foo:81/", + "base": "about:blank", + "href": "http://foo:81/", + "origin": "http://foo:81", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo:81", + "hostname": "foo", + "port": "81", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "httpa://foo:80/", + "base": "about:blank", + "href": "httpa://foo:80/", + "origin": "null", + "protocol": "httpa:", + "username": "", + "password": "", + "host": "foo:80", + "hostname": "foo", + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://foo:-80/", + "base": "about:blank", + "failure": true + }, + { + "input": "https://foo:443/", + "base": "about:blank", + "href": "https://foo/", + "origin": "https://foo", + "protocol": "https:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://foo:80/", + "base": "about:blank", + "href": "https://foo:80/", + "origin": "https://foo:80", + "protocol": "https:", + "username": "", + "password": "", + "host": "foo:80", + "hostname": "foo", + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ftp://foo:21/", + "base": "about:blank", + "href": "ftp://foo/", + "origin": "ftp://foo", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ftp://foo:80/", + "base": "about:blank", + "href": "ftp://foo:80/", + "origin": "ftp://foo:80", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "foo:80", + "hostname": "foo", + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "gopher://foo:70/", + "base": "about:blank", + "href": "gopher://foo/", + "origin": "gopher://foo", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "gopher://foo:443/", + "base": "about:blank", + "href": "gopher://foo:443/", + "origin": "gopher://foo:443", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "foo:443", + "hostname": "foo", + "port": "443", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws://foo:80/", + "base": "about:blank", + "href": "ws://foo/", + "origin": "ws://foo", + "protocol": "ws:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws://foo:81/", + "base": "about:blank", + "href": "ws://foo:81/", + "origin": "ws://foo:81", + "protocol": "ws:", + "username": "", + "password": "", + "host": "foo:81", + "hostname": "foo", + "port": "81", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws://foo:443/", + "base": "about:blank", + "href": "ws://foo:443/", + "origin": "ws://foo:443", + "protocol": "ws:", + "username": "", + "password": "", + "host": "foo:443", + "hostname": "foo", + "port": "443", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws://foo:815/", + "base": "about:blank", + "href": "ws://foo:815/", + "origin": "ws://foo:815", + "protocol": "ws:", + "username": "", + "password": "", + "host": "foo:815", + "hostname": "foo", + "port": "815", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss://foo:80/", + "base": "about:blank", + "href": "wss://foo:80/", + "origin": "wss://foo:80", + "protocol": "wss:", + "username": "", + "password": "", + "host": "foo:80", + "hostname": "foo", + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss://foo:81/", + "base": "about:blank", + "href": "wss://foo:81/", + "origin": "wss://foo:81", + "protocol": "wss:", + "username": "", + "password": "", + "host": "foo:81", + "hostname": "foo", + "port": "81", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss://foo:443/", + "base": "about:blank", + "href": "wss://foo/", + "origin": "wss://foo", + "protocol": "wss:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss://foo:815/", + "base": "about:blank", + "href": "wss://foo:815/", + "origin": "wss://foo:815", + "protocol": "wss:", + "username": "", + "password": "", + "host": "foo:815", + "hostname": "foo", + "port": "815", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/example.com/", + "base": "about:blank", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ftp:/example.com/", + "base": "about:blank", + "href": "ftp://example.com/", + "origin": "ftp://example.com", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https:/example.com/", + "base": "about:blank", + "href": "https://example.com/", + "origin": "https://example.com", + "protocol": "https:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "madeupscheme:/example.com/", + "base": "about:blank", + "href": "madeupscheme:/example.com/", + "origin": "null", + "protocol": "madeupscheme:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "file:/example.com/", + "base": "about:blank", + "href": "file:///example.com/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "ftps:/example.com/", + "base": "about:blank", + "href": "ftps:/example.com/", + "origin": "null", + "protocol": "ftps:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "gopher:/example.com/", + "base": "about:blank", + "href": "gopher://example.com/", + "origin": "gopher://example.com", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws:/example.com/", + "base": "about:blank", + "href": "ws://example.com/", + "origin": "ws://example.com", + "protocol": "ws:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss:/example.com/", + "base": "about:blank", + "href": "wss://example.com/", + "origin": "wss://example.com", + "protocol": "wss:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "data:/example.com/", + "base": "about:blank", + "href": "data:/example.com/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "javascript:/example.com/", + "base": "about:blank", + "href": "javascript:/example.com/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "mailto:/example.com/", + "base": "about:blank", + "href": "mailto:/example.com/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "http:example.com/", + "base": "about:blank", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ftp:example.com/", + "base": "about:blank", + "href": "ftp://example.com/", + "origin": "ftp://example.com", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https:example.com/", + "base": "about:blank", + "href": "https://example.com/", + "origin": "https://example.com", + "protocol": "https:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "madeupscheme:example.com/", + "base": "about:blank", + "href": "madeupscheme:example.com/", + "origin": "null", + "protocol": "madeupscheme:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "ftps:example.com/", + "base": "about:blank", + "href": "ftps:example.com/", + "origin": "null", + "protocol": "ftps:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "gopher:example.com/", + "base": "about:blank", + "href": "gopher://example.com/", + "origin": "gopher://example.com", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws:example.com/", + "base": "about:blank", + "href": "ws://example.com/", + "origin": "ws://example.com", + "protocol": "ws:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss:example.com/", + "base": "about:blank", + "href": "wss://example.com/", + "origin": "wss://example.com", + "protocol": "wss:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "data:example.com/", + "base": "about:blank", + "href": "data:example.com/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "javascript:example.com/", + "base": "about:blank", + "href": "javascript:example.com/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "mailto:example.com/", + "base": "about:blank", + "href": "mailto:example.com/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/segments-userinfo-vs-host.html", + { + "input": "http:@www.example.com", + "base": "about:blank", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/@www.example.com", + "base": "about:blank", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://@www.example.com", + "base": "about:blank", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:a:b@www.example.com", + "base": "about:blank", + "href": "http://a:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/a:b@www.example.com", + "base": "about:blank", + "href": "http://a:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://a:b@www.example.com", + "base": "about:blank", + "href": "http://a:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://@pple.com", + "base": "about:blank", + "href": "http://pple.com/", + "origin": "http://pple.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "pple.com", + "hostname": "pple.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http::b@www.example.com", + "base": "about:blank", + "href": "http://:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/:b@www.example.com", + "base": "about:blank", + "href": "http://:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://:b@www.example.com", + "base": "about:blank", + "href": "http://:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/:@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http://user@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http:@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http:/@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http://@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "https:@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http:a:b@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http:/a:b@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http://a:b@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http::@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http:a:@www.example.com", + "base": "about:blank", + "href": "http://a@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/a:@www.example.com", + "base": "about:blank", + "href": "http://a@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://a:@www.example.com", + "base": "about:blank", + "href": "http://a@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://www.@pple.com", + "base": "about:blank", + "href": "http://www.@pple.com/", + "origin": "http://pple.com", + "protocol": "http:", + "username": "www.", + "password": "", + "host": "pple.com", + "hostname": "pple.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:@:www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http:/@:www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http://@:www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http://:@www.example.com", + "base": "about:blank", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# Others", + { + "input": "/", + "base": "http://www.example.com/test", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "/test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": ".", + "base": "http://www.example.com/test", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "..", + "base": "http://www.example.com/test", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": "./test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": "../test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": "../aaa/test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/aaa/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/aaa/test.txt", + "search": "", + "hash": "" + }, + { + "input": "../../test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": "中/test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/%E4%B8%AD/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/%E4%B8%AD/test.txt", + "search": "", + "hash": "" + }, + { + "input": "http://www.example2.com", + "base": "http://www.example.com/test", + "href": "http://www.example2.com/", + "origin": "http://www.example2.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example2.com", + "hostname": "www.example2.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "//www.example2.com", + "base": "http://www.example.com/test", + "href": "http://www.example2.com/", + "origin": "http://www.example2.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example2.com", + "hostname": "www.example2.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:...", + "base": "http://www.example.com/test", + "href": "file:///...", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/...", + "search": "", + "hash": "" + }, + { + "input": "file:..", + "base": "http://www.example.com/test", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:a", + "base": "http://www.example.com/test", + "href": "file:///a", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/a", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/host.html", + "Basic canonicalization, uppercase should be converted to lowercase", + { + "input": "http://ExAmPlE.CoM", + "base": "http://other.com/", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://example example.com", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://Goo%20 goo%7C|.com", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://[]", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://[:]", + "base": "http://other.com/", + "failure": true + }, + "U+3000 is mapped to U+0020 (space) which is disallowed", + { + "input": "http://GOO\u00a0\u3000goo.com", + "base": "http://other.com/", + "failure": true + }, + "Other types of space (no-break, zero-width, zero-width-no-break) are name-prepped away to nothing. U+200B, U+2060, and U+FEFF, are ignored", + { + "input": "http://GOO\u200b\u2060\ufeffgoo.com", + "base": "http://other.com/", + "href": "http://googoo.com/", + "origin": "http://googoo.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "googoo.com", + "hostname": "googoo.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Leading and trailing C0 control or space", + { + "input": "\u0000\u001b\u0004\u0012 http://example.com/\u001f \u000d ", + "base": "about:blank", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Ideographic full stop (full-width period for Chinese, etc.) should be treated as a dot. U+3002 is mapped to U+002E (dot)", + { + "input": "http://www.foo。bar.com", + "base": "http://other.com/", + "href": "http://www.foo.bar.com/", + "origin": "http://www.foo.bar.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.foo.bar.com", + "hostname": "www.foo.bar.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Invalid unicode characters should fail... U+FDD0 is disallowed; %ef%b7%90 is U+FDD0", + { + "input": "http://\ufdd0zyx.com", + "base": "http://other.com/", + "failure": true + }, + "This is the same as previous but escaped", + { + "input": "http://%ef%b7%90zyx.com", + "base": "http://other.com/", + "failure": true + }, + "U+FFFD", + { + "input": "https://\ufffd", + "base": "about:blank", + "failure": true + }, + { + "input": "https://%EF%BF%BD", + "base": "about:blank", + "failure": true + }, + { + "input": "https://x/\ufffd?\ufffd#\ufffd", + "base": "about:blank", + "href": "https://x/%EF%BF%BD?%EF%BF%BD#%EF%BF%BD", + "origin": "https://x", + "protocol": "https:", + "username": "", + "password": "", + "host": "x", + "hostname": "x", + "port": "", + "pathname": "/%EF%BF%BD", + "search": "?%EF%BF%BD", + "hash": "#%EF%BF%BD" + }, + "Test name prepping, fullwidth input should be converted to ASCII and NOT IDN-ized. This is 'Go' in fullwidth UTF-8/UTF-16.", + { + "input": "http://Go.com", + "base": "http://other.com/", + "href": "http://go.com/", + "origin": "http://go.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "go.com", + "hostname": "go.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "URL spec forbids the following. https://www.w3.org/Bugs/Public/show_bug.cgi?id=24257", + { + "input": "http://%41.com", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://%ef%bc%85%ef%bc%94%ef%bc%91.com", + "base": "http://other.com/", + "failure": true + }, + "...%00 in fullwidth should fail (also as escaped UTF-8 input)", + { + "input": "http://%00.com", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://%ef%bc%85%ef%bc%90%ef%bc%90.com", + "base": "http://other.com/", + "failure": true + }, + "Basic IDN support, UTF-8 and UTF-16 input should be converted to IDN", + { + "input": "http://你好你好", + "base": "http://other.com/", + "href": "http://xn--6qqa088eba/", + "origin": "http://xn--6qqa088eba", + "protocol": "http:", + "username": "", + "password": "", + "host": "xn--6qqa088eba", + "hostname": "xn--6qqa088eba", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://faß.ExAmPlE/", + "base": "about:blank", + "href": "https://xn--fa-hia.example/", + "origin": "https://xn--fa-hia.example", + "protocol": "https:", + "username": "", + "password": "", + "host": "xn--fa-hia.example", + "hostname": "xn--fa-hia.example", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "sc://faß.ExAmPlE/", + "base": "about:blank", + "href": "sc://fa%C3%9F.ExAmPlE/", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "fa%C3%9F.ExAmPlE", + "hostname": "fa%C3%9F.ExAmPlE", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Invalid escaped characters should fail and the percents should be escaped. https://www.w3.org/Bugs/Public/show_bug.cgi?id=24191", + { + "input": "http://%zz%66%a.com", + "base": "http://other.com/", + "failure": true + }, + "If we get an invalid character that has been escaped.", + { + "input": "http://%25", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://hello%00", + "base": "http://other.com/", + "failure": true + }, + "Escaped numbers should be treated like IP addresses if they are.", + { + "input": "http://%30%78%63%30%2e%30%32%35%30.01", + "base": "http://other.com/", + "href": "http://192.168.0.1/", + "origin": "http://192.168.0.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.0.1", + "hostname": "192.168.0.1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://%30%78%63%30%2e%30%32%35%30.01%2e", + "base": "http://other.com/", + "href": "http://192.168.0.1/", + "origin": "http://192.168.0.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.0.1", + "hostname": "192.168.0.1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://192.168.0.257", + "base": "http://other.com/", + "failure": true + }, + "Invalid escaping in hosts causes failure", + { + "input": "http://%3g%78%63%30%2e%30%32%35%30%2E.01", + "base": "http://other.com/", + "failure": true + }, + "A space in a host causes failure", + { + "input": "http://192.168.0.1 hello", + "base": "http://other.com/", + "failure": true + }, + { + "input": "https://x x:12", + "base": "about:blank", + "failure": true + }, + "Fullwidth and escaped UTF-8 fullwidth should still be treated as IP", + { + "input": "http://0Xc0.0250.01", + "base": "http://other.com/", + "href": "http://192.168.0.1/", + "origin": "http://192.168.0.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.0.1", + "hostname": "192.168.0.1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Domains with empty labels", + { + "input": "http://./", + "base": "about:blank", + "href": "http://./", + "origin": "http://.", + "protocol": "http:", + "username": "", + "password": "", + "host": ".", + "hostname": ".", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://../", + "base": "about:blank", + "href": "http://../", + "origin": "http://..", + "protocol": "http:", + "username": "", + "password": "", + "host": "..", + "hostname": "..", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://0..0x300/", + "base": "about:blank", + "href": "http://0..0x300/", + "origin": "http://0..0x300", + "protocol": "http:", + "username": "", + "password": "", + "host": "0..0x300", + "hostname": "0..0x300", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Broken IPv6", + { + "input": "http://[www.google.com]/", + "base": "about:blank", + "failure": true + }, + { + "input": "http://[google.com]", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://[::1.2.3.4x]", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://[::1.2.3.]", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://[::1.2.]", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://[::1.]", + "base": "http://other.com/", + "failure": true + }, + "Misc Unicode", + { + "input": "http://foo:💩@example.com/bar", + "base": "http://other.com/", + "href": "http://foo:%F0%9F%92%A9@example.com/bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "foo", + "password": "%F0%9F%92%A9", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/bar", + "search": "", + "hash": "" + }, + "# resolving a fragment against any scheme succeeds", + { + "input": "#", + "base": "test:test", + "href": "test:test#", + "origin": "null", + "protocol": "test:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "test", + "search": "", + "hash": "" + }, + { + "input": "#x", + "base": "mailto:x@x.com", + "href": "mailto:x@x.com#x", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "x@x.com", + "search": "", + "hash": "#x" + }, + { + "input": "#x", + "base": "data:,", + "href": "data:,#x", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": ",", + "search": "", + "hash": "#x" + }, + { + "input": "#x", + "base": "about:blank", + "href": "about:blank#x", + "origin": "null", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "blank", + "search": "", + "hash": "#x" + }, + { + "input": "#", + "base": "test:test?test", + "href": "test:test?test#", + "origin": "null", + "protocol": "test:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "test", + "search": "?test", + "hash": "" + }, + "# multiple @ in authority state", + { + "input": "https://@test@test@example:800/", + "base": "http://doesnotmatter/", + "href": "https://%40test%40test@example:800/", + "origin": "https://example:800", + "protocol": "https:", + "username": "%40test%40test", + "password": "", + "host": "example:800", + "hostname": "example", + "port": "800", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://@@@example", + "base": "http://doesnotmatter/", + "href": "https://%40%40@example/", + "origin": "https://example", + "protocol": "https:", + "username": "%40%40", + "password": "", + "host": "example", + "hostname": "example", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "non-az-09 characters", + { + "input": "http://`{}:`{}@h/`{}?`{}", + "base": "http://doesnotmatter/", + "href": "http://%60%7B%7D:%60%7B%7D@h/%60%7B%7D?`{}", + "origin": "http://h", + "protocol": "http:", + "username": "%60%7B%7D", + "password": "%60%7B%7D", + "host": "h", + "hostname": "h", + "port": "", + "pathname": "/%60%7B%7D", + "search": "?`{}", + "hash": "" + }, + "# Credentials in base", + { + "input": "/some/path", + "base": "http://user@example.org/smth", + "href": "http://user@example.org/some/path", + "origin": "http://example.org", + "protocol": "http:", + "username": "user", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/some/path", + "search": "", + "hash": "" + }, + { + "input": "", + "base": "http://user:pass@example.org:21/smth", + "href": "http://user:pass@example.org:21/smth", + "origin": "http://example.org:21", + "protocol": "http:", + "username": "user", + "password": "pass", + "host": "example.org:21", + "hostname": "example.org", + "port": "21", + "pathname": "/smth", + "search": "", + "hash": "" + }, + { + "input": "/some/path", + "base": "http://user:pass@example.org:21/smth", + "href": "http://user:pass@example.org:21/some/path", + "origin": "http://example.org:21", + "protocol": "http:", + "username": "user", + "password": "pass", + "host": "example.org:21", + "hostname": "example.org", + "port": "21", + "pathname": "/some/path", + "search": "", + "hash": "" + }, + "# a set of tests designed by zcorpan for relative URLs with unknown schemes", + { + "input": "i", + "base": "sc:sd", + "failure": true + }, + { + "input": "i", + "base": "sc:sd/sd", + "failure": true + }, + { + "input": "i", + "base": "sc:/pa/pa", + "href": "sc:/pa/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/pa/i", + "search": "", + "hash": "" + }, + { + "input": "i", + "base": "sc://ho/pa", + "href": "sc://ho/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "ho", + "hostname": "ho", + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "i", + "base": "sc:///pa/pa", + "href": "sc:///pa/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/pa/i", + "search": "", + "hash": "" + }, + { + "input": "../i", + "base": "sc:sd", + "failure": true + }, + { + "input": "../i", + "base": "sc:sd/sd", + "failure": true + }, + { + "input": "../i", + "base": "sc:/pa/pa", + "href": "sc:/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "../i", + "base": "sc://ho/pa", + "href": "sc://ho/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "ho", + "hostname": "ho", + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "../i", + "base": "sc:///pa/pa", + "href": "sc:///i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "/i", + "base": "sc:sd", + "failure": true + }, + { + "input": "/i", + "base": "sc:sd/sd", + "failure": true + }, + { + "input": "/i", + "base": "sc:/pa/pa", + "href": "sc:/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "/i", + "base": "sc://ho/pa", + "href": "sc://ho/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "ho", + "hostname": "ho", + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "/i", + "base": "sc:///pa/pa", + "href": "sc:///i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "?i", + "base": "sc:sd", + "failure": true + }, + { + "input": "?i", + "base": "sc:sd/sd", + "failure": true + }, + { + "input": "?i", + "base": "sc:/pa/pa", + "href": "sc:/pa/pa?i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/pa/pa", + "search": "?i", + "hash": "" + }, + { + "input": "?i", + "base": "sc://ho/pa", + "href": "sc://ho/pa?i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "ho", + "hostname": "ho", + "port": "", + "pathname": "/pa", + "search": "?i", + "hash": "" + }, + { + "input": "?i", + "base": "sc:///pa/pa", + "href": "sc:///pa/pa?i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/pa/pa", + "search": "?i", + "hash": "" + }, + { + "input": "#i", + "base": "sc:sd", + "href": "sc:sd#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "sd", + "search": "", + "hash": "#i" + }, + { + "input": "#i", + "base": "sc:sd/sd", + "href": "sc:sd/sd#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "sd/sd", + "search": "", + "hash": "#i" + }, + { + "input": "#i", + "base": "sc:/pa/pa", + "href": "sc:/pa/pa#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/pa/pa", + "search": "", + "hash": "#i" + }, + { + "input": "#i", + "base": "sc://ho/pa", + "href": "sc://ho/pa#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "ho", + "hostname": "ho", + "port": "", + "pathname": "/pa", + "search": "", + "hash": "#i" + }, + { + "input": "#i", + "base": "sc:///pa/pa", + "href": "sc:///pa/pa#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/pa/pa", + "search": "", + "hash": "#i" + }, + "# make sure that relative URL logic works on known typically non-relative schemes too", + { + "input": "about:/../", + "base": "about:blank", + "href": "about:/", + "origin": "null", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "data:/../", + "base": "about:blank", + "href": "data:/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "javascript:/../", + "base": "about:blank", + "href": "javascript:/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "mailto:/../", + "base": "about:blank", + "href": "mailto:/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# unknown schemes and their hosts", + { + "input": "sc://ñ.test/", + "base": "about:blank", + "href": "sc://%C3%B1.test/", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%C3%B1.test", + "hostname": "%C3%B1.test", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "sc://\u001F!\"$&'()*+,-.;<=>^_`{|}~/", + "base": "about:blank", + "href": "sc://%1F!\"$&'()*+,-.;<=>^_`{|}~/", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%1F!\"$&'()*+,-.;<=>^_`{|}~", + "hostname": "%1F!\"$&'()*+,-.;<=>^_`{|}~", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "sc://\u0000/", + "base": "about:blank", + "failure": true + }, + { + "input": "sc:// /", + "base": "about:blank", + "failure": true + }, + { + "input": "sc://%/", + "base": "about:blank", + "href": "sc://%/", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%", + "hostname": "%", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "sc://@/", + "base": "about:blank", + "failure": true + }, + { + "input": "sc://te@s:t@/", + "base": "about:blank", + "failure": true + }, + { + "input": "sc://:/", + "base": "about:blank", + "failure": true + }, + { + "input": "sc://:12/", + "base": "about:blank", + "failure": true + }, + { + "input": "sc://[/", + "base": "about:blank", + "failure": true + }, + { + "input": "sc://\\/", + "base": "about:blank", + "failure": true + }, + { + "input": "sc://]/", + "base": "about:blank", + "failure": true + }, + { + "input": "x", + "base": "sc://ñ", + "href": "sc://%C3%B1/x", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%C3%B1", + "hostname": "%C3%B1", + "port": "", + "pathname": "/x", + "search": "", + "hash": "" + }, + "# unknown schemes and backslashes", + { + "input": "sc:\\../", + "base": "about:blank", + "href": "sc:\\../", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "\\../", + "search": "", + "hash": "" + }, + "# unknown scheme with path looking like a password", + { + "input": "sc::a@example.net", + "base": "about:blank", + "href": "sc::a@example.net", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": ":a@example.net", + "search": "", + "hash": "" + }, + "# unknown scheme with bogus percent-encoding", + { + "input": "wow:%NBD", + "base": "about:blank", + "href": "wow:%NBD", + "origin": "null", + "protocol": "wow:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "%NBD", + "search": "", + "hash": "" + }, + { + "input": "wow:%1G", + "base": "about:blank", + "href": "wow:%1G", + "origin": "null", + "protocol": "wow:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "%1G", + "search": "", + "hash": "" + }, + "# Hosts and percent-encoding", + { + "input": "ftp://example.com%80/", + "base": "about:blank", + "failure": true + }, + { + "input": "ftp://example.com%A0/", + "base": "about:blank", + "failure": true + }, + { + "input": "https://example.com%80/", + "base": "about:blank", + "failure": true + }, + { + "input": "https://example.com%A0/", + "base": "about:blank", + "failure": true + }, + { + "input": "ftp://%e2%98%83", + "base": "about:blank", + "href": "ftp://xn--n3h/", + "origin": "ftp://xn--n3h", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "xn--n3h", + "hostname": "xn--n3h", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://%e2%98%83", + "base": "about:blank", + "href": "https://xn--n3h/", + "origin": "https://xn--n3h", + "protocol": "https:", + "username": "", + "password": "", + "host": "xn--n3h", + "hostname": "xn--n3h", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# tests from jsdom/whatwg-url designed for code coverage", + { + "input": "http://127.0.0.1:10100/relative_import.html", + "base": "about:blank", + "href": "http://127.0.0.1:10100/relative_import.html", + "origin": "http://127.0.0.1:10100", + "protocol": "http:", + "username": "", + "password": "", + "host": "127.0.0.1:10100", + "hostname": "127.0.0.1", + "port": "10100", + "pathname": "/relative_import.html", + "search": "", + "hash": "" + }, + { + "input": "http://facebook.com/?foo=%7B%22abc%22", + "base": "about:blank", + "href": "http://facebook.com/?foo=%7B%22abc%22", + "origin": "http://facebook.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "facebook.com", + "hostname": "facebook.com", + "port": "", + "pathname": "/", + "search": "?foo=%7B%22abc%22", + "hash": "" + }, + { + "input": "https://localhost:3000/jqueryui@1.2.3", + "base": "about:blank", + "href": "https://localhost:3000/jqueryui@1.2.3", + "origin": "https://localhost:3000", + "protocol": "https:", + "username": "", + "password": "", + "host": "localhost:3000", + "hostname": "localhost", + "port": "3000", + "pathname": "/jqueryui@1.2.3", + "search": "", + "hash": "" + }, + "# tab/LF/CR", + { + "input": "h\tt\nt\rp://h\to\ns\rt:9\t0\n0\r0/p\ta\nt\rh?q\tu\ne\rry#f\tr\na\rg", + "base": "about:blank", + "href": "http://host:9000/path?query#frag", + "origin": "http://host:9000", + "protocol": "http:", + "username": "", + "password": "", + "host": "host:9000", + "hostname": "host", + "port": "9000", + "pathname": "/path", + "search": "?query", + "hash": "#frag" + }, + "# Stringification of URL.searchParams", + { + "input": "?a=b&c=d", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar?a=b&c=d", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "?a=b&c=d", + "searchParams": "a=b&c=d", + "hash": "" + }, + { + "input": "??a=b&c=d", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar??a=b&c=d", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "??a=b&c=d", + "searchParams": "%3Fa=b&c=d", + "hash": "" + }, + "# Scheme only", + { + "input": "http:", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "searchParams": "", + "hash": "" + }, + { + "input": "http:", + "base": "https://example.org/foo/bar", + "failure": true + }, + { + "input": "sc:", + "base": "https://example.org/foo/bar", + "href": "sc:", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "", + "search": "", + "searchParams": "", + "hash": "" + }, + "# Percent encoding of fragments", + { + "input": "http://foo.bar/baz?qux#foo\bbar", + "base": "about:blank", + "href": "http://foo.bar/baz?qux#foo%08bar", + "origin": "http://foo.bar", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo.bar", + "hostname": "foo.bar", + "port": "", + "pathname": "/baz", + "search": "?qux", + "searchParams": "qux=", + "hash": "#foo%08bar" + }, + "# IPv4 parsing (via https://github.com/nodejs/node/pull/10317)", + { + "input": "http://192.168.257", + "base": "http://other.com/", + "href": "http://192.168.1.1/", + "origin": "http://192.168.1.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.1.1", + "hostname": "192.168.1.1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://192.168.257.com", + "base": "http://other.com/", + "href": "http://192.168.257.com/", + "origin": "http://192.168.257.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.257.com", + "hostname": "192.168.257.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://256", + "base": "http://other.com/", + "href": "http://0.0.1.0/", + "origin": "http://0.0.1.0", + "protocol": "http:", + "username": "", + "password": "", + "host": "0.0.1.0", + "hostname": "0.0.1.0", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://256.com", + "base": "http://other.com/", + "href": "http://256.com/", + "origin": "http://256.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "256.com", + "hostname": "256.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://999999999", + "base": "http://other.com/", + "href": "http://59.154.201.255/", + "origin": "http://59.154.201.255", + "protocol": "http:", + "username": "", + "password": "", + "host": "59.154.201.255", + "hostname": "59.154.201.255", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://999999999.com", + "base": "http://other.com/", + "href": "http://999999999.com/", + "origin": "http://999999999.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "999999999.com", + "hostname": "999999999.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://10000000000", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://10000000000.com", + "base": "http://other.com/", + "href": "http://10000000000.com/", + "origin": "http://10000000000.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "10000000000.com", + "hostname": "10000000000.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://4294967295", + "base": "http://other.com/", + "href": "http://255.255.255.255/", + "origin": "http://255.255.255.255", + "protocol": "http:", + "username": "", + "password": "", + "host": "255.255.255.255", + "hostname": "255.255.255.255", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://4294967296", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://0xffffffff", + "base": "http://other.com/", + "href": "http://255.255.255.255/", + "origin": "http://255.255.255.255", + "protocol": "http:", + "username": "", + "password": "", + "host": "255.255.255.255", + "hostname": "255.255.255.255", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://0xffffffff1", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://256.256.256.256", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://256.256.256.256.256", + "base": "http://other.com/", + "href": "http://256.256.256.256.256/", + "origin": "http://256.256.256.256.256", + "protocol": "http:", + "username": "", + "password": "", + "host": "256.256.256.256.256", + "hostname": "256.256.256.256.256", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://0x.0x.0", + "base": "about:blank", + "href": "https://0.0.0.0/", + "origin": "https://0.0.0.0", + "protocol": "https:", + "username": "", + "password": "", + "host": "0.0.0.0", + "hostname": "0.0.0.0", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "More IPv4 parsing (via https://github.com/jsdom/whatwg-url/issues/92)", + { + "input": "https://0x100000000/test", + "base": "about:blank", + "failure": true + }, + { + "input": "https://256.0.0.1/test", + "base": "about:blank", + "failure": true + }, + "# file URLs containing percent-encoded Windows drive letters (shouldn't work)", + { + "input": "file:///C%3A/", + "base": "about:blank", + "href": "file:///C%3A/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C%3A/", + "search": "", + "hash": "" + }, + { + "input": "file:///C%7C/", + "base": "about:blank", + "href": "file:///C%7C/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C%7C/", + "search": "", + "hash": "" + }, + "# file URLs relative to other file URLs (via https://github.com/jsdom/whatwg-url/pull/60)", + { + "input": "pix/submit.gif", + "base": "file:///C:/Users/Domenic/Dropbox/GitHub/tmpvar/jsdom/test/level2/html/files/anchor.html", + "href": "file:///C:/Users/Domenic/Dropbox/GitHub/tmpvar/jsdom/test/level2/html/files/pix/submit.gif", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/Users/Domenic/Dropbox/GitHub/tmpvar/jsdom/test/level2/html/files/pix/submit.gif", + "search": "", + "hash": "" + }, + { + "input": "..", + "base": "file:///C:/", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "..", + "base": "file:///", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# More file URL tests by zcorpan and annevk", + { + "input": "/", + "base": "file:///C:/a/b", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "//d:", + "base": "file:///C:/a/b", + "href": "file:///d:", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/d:", + "search": "", + "hash": "" + }, + { + "input": "//d:/..", + "base": "file:///C:/a/b", + "href": "file:///d:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/d:/", + "search": "", + "hash": "" + }, + { + "input": "..", + "base": "file:///ab:/", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "..", + "base": "file:///1:/", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "", + "base": "file:///test?test#test", + "href": "file:///test?test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "?test", + "hash": "" + }, + { + "input": "file:", + "base": "file:///test?test#test", + "href": "file:///test?test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "?test", + "hash": "" + }, + { + "input": "?x", + "base": "file:///test?test#test", + "href": "file:///test?x", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "?x", + "hash": "" + }, + { + "input": "file:?x", + "base": "file:///test?test#test", + "href": "file:///test?x", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "?x", + "hash": "" + }, + { + "input": "#x", + "base": "file:///test?test#test", + "href": "file:///test?test#x", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "?test", + "hash": "#x" + }, + { + "input": "file:#x", + "base": "file:///test?test#test", + "href": "file:///test?test#x", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "?test", + "hash": "#x" + }, + "# File URLs and many (back)slashes", + { + "input": "file:\\\\//", + "base": "about:blank", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:\\\\\\\\", + "base": "about:blank", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:\\\\\\\\?fox", + "base": "about:blank", + "href": "file:///?fox", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "?fox", + "hash": "" + }, + { + "input": "file:\\\\\\\\#guppy", + "base": "about:blank", + "href": "file:///#guppy", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "#guppy" + }, + { + "input": "file://spider///", + "base": "about:blank", + "href": "file://spider/", + "protocol": "file:", + "username": "", + "password": "", + "host": "spider", + "hostname": "spider", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:\\\\localhost//", + "base": "about:blank", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:///localhost//cat", + "base": "about:blank", + "href": "file:///localhost//cat", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/localhost//cat", + "search": "", + "hash": "" + }, + { + "input": "file://\\/localhost//cat", + "base": "about:blank", + "href": "file:///localhost//cat", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/localhost//cat", + "search": "", + "hash": "" + }, + { + "input": "file://localhost//a//../..//", + "base": "about:blank", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "/////mouse", + "base": "file:///elephant", + "href": "file:///mouse", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/mouse", + "search": "", + "hash": "" + }, + { + "input": "\\//pig", + "base": "file://lion/", + "href": "file:///pig", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/pig", + "search": "", + "hash": "" + }, + { + "input": "\\/localhost//pig", + "base": "file://lion/", + "href": "file:///pig", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/pig", + "search": "", + "hash": "" + }, + { + "input": "//localhost//pig", + "base": "file://lion/", + "href": "file:///pig", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/pig", + "search": "", + "hash": "" + }, + { + "input": "/..//localhost//pig", + "base": "file://lion/", + "href": "file://lion/localhost//pig", + "protocol": "file:", + "username": "", + "password": "", + "host": "lion", + "hostname": "lion", + "port": "", + "pathname": "/localhost//pig", + "search": "", + "hash": "" + }, + { + "input": "file://", + "base": "file://ape/", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# File URLs with non-empty hosts", + { + "input": "/rooibos", + "base": "file://tea/", + "href": "file://tea/rooibos", + "protocol": "file:", + "username": "", + "password": "", + "host": "tea", + "hostname": "tea", + "port": "", + "pathname": "/rooibos", + "search": "", + "hash": "" + }, + { + "input": "/?chai", + "base": "file://tea/", + "href": "file://tea/?chai", + "protocol": "file:", + "username": "", + "password": "", + "host": "tea", + "hostname": "tea", + "port": "", + "pathname": "/", + "search": "?chai", + "hash": "" + }, + "# Windows drive letter handling with the 'file:' base URL", + { + "input": "C|", + "base": "file://host/dir/file", + "href": "file:///C:", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:", + "search": "", + "hash": "" + }, + { + "input": "C|#", + "base": "file://host/dir/file", + "href": "file:///C:#", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:", + "search": "", + "hash": "" + }, + { + "input": "C|?", + "base": "file://host/dir/file", + "href": "file:///C:?", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:", + "search": "", + "hash": "" + }, + { + "input": "C|/", + "base": "file://host/dir/file", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "C|\n/", + "base": "file://host/dir/file", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "C|\\", + "base": "file://host/dir/file", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "C", + "base": "file://host/dir/file", + "href": "file://host/dir/C", + "protocol": "file:", + "username": "", + "password": "", + "host": "host", + "hostname": "host", + "port": "", + "pathname": "/dir/C", + "search": "", + "hash": "" + }, + { + "input": "C|a", + "base": "file://host/dir/file", + "href": "file://host/dir/C|a", + "protocol": "file:", + "username": "", + "password": "", + "host": "host", + "hostname": "host", + "port": "", + "pathname": "/dir/C|a", + "search": "", + "hash": "" + }, + "# Windows drive letter quirk in the file slash state", + { + "input": "/c:/foo/bar", + "base": "file:///c:/baz/qux", + "href": "file:///c:/foo/bar", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/c:/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "/c|/foo/bar", + "base": "file:///c:/baz/qux", + "href": "file:///c:/foo/bar", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/c:/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "file:\\c:\\foo\\bar", + "base": "file:///c:/baz/qux", + "href": "file:///c:/foo/bar", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/c:/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "/c:/foo/bar", + "base": "file://host/path", + "href": "file:///c:/foo/bar", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/c:/foo/bar", + "search": "", + "hash": "" + }, + "# Windows drive letter quirk with not empty host", + { + "input": "file://example.net/C:/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "file://1.2.3.4/C:/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "file://[1::8]/C:/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + "# Windows drive letter quirk (no host)", + { + "input": "file:/C|/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "file://C|/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + "# file URLs without base URL by Rimas Misevičius", + { + "input": "file:", + "base": "about:blank", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:?q=v", + "base": "about:blank", + "href": "file:///?q=v", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "?q=v", + "hash": "" + }, + { + "input": "file:#frag", + "base": "about:blank", + "href": "file:///#frag", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "#frag" + }, + "# IPv6 tests", + { + "input": "http://[1:0::]", + "base": "http://example.net/", + "href": "http://[1::]/", + "origin": "http://[1::]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[1::]", + "hostname": "[1::]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[0:1:2:3:4:5:6:7:8]", + "base": "http://example.net/", + "failure": true + }, + { + "input": "https://[0::0::0]", + "base": "about:blank", + "failure": true + }, + { + "input": "https://[0:.0]", + "base": "about:blank", + "failure": true + }, + { + "input": "https://[0:0:]", + "base": "about:blank", + "failure": true + }, + { + "input": "https://[0:1:2:3:4:5:6:7.0.0.0.1]", + "base": "about:blank", + "failure": true + }, + { + "input": "https://[0:1.00.0.0.0]", + "base": "about:blank", + "failure": true + }, + { + "input": "https://[0:1.290.0.0.0]", + "base": "about:blank", + "failure": true + }, + { + "input": "https://[0:1.23.23]", + "base": "about:blank", + "failure": true + }, + "# Empty host", + { + "input": "http://?", + "base": "about:blank", + "failure": true + }, + { + "input": "http://#", + "base": "about:blank", + "failure": true + }, + "Port overflow (2^32 + 81)", + { + "input": "http://f:4294967377/c", + "base": "http://example.org/", + "failure": true + }, + "Port overflow (2^64 + 81)", + { + "input": "http://f:18446744073709551697/c", + "base": "http://example.org/", + "failure": true + }, + "Port overflow (2^128 + 81)", + { + "input": "http://f:340282366920938463463374607431768211537/c", + "base": "http://example.org/", + "failure": true + }, + "# Non-special-URL path tests", + { + "input": "sc://ñ", + "base": "about:blank", + "href": "sc://%C3%B1", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%C3%B1", + "hostname": "%C3%B1", + "port": "", + "pathname": "", + "search": "", + "hash": "" + }, + { + "input": "sc://ñ?x", + "base": "about:blank", + "href": "sc://%C3%B1?x", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%C3%B1", + "hostname": "%C3%B1", + "port": "", + "pathname": "", + "search": "?x", + "hash": "" + }, + { + "input": "sc://ñ#x", + "base": "about:blank", + "href": "sc://%C3%B1#x", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%C3%B1", + "hostname": "%C3%B1", + "port": "", + "pathname": "", + "search": "", + "hash": "#x" + }, + { + "input": "#x", + "base": "sc://ñ", + "href": "sc://%C3%B1#x", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%C3%B1", + "hostname": "%C3%B1", + "port": "", + "pathname": "", + "search": "", + "hash": "#x" + }, + { + "input": "?x", + "base": "sc://ñ", + "href": "sc://%C3%B1?x", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%C3%B1", + "hostname": "%C3%B1", + "port": "", + "pathname": "", + "search": "?x", + "hash": "" + }, + { + "input": "sc://?", + "base": "about:blank", + "href": "sc://?", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "", + "search": "", + "hash": "" + }, + { + "input": "sc://#", + "base": "about:blank", + "href": "sc://#", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "", + "search": "", + "hash": "" + }, + { + "input": "///", + "base": "sc://x/", + "href": "sc:///", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "////", + "base": "sc://x/", + "href": "sc:////", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "//", + "search": "", + "hash": "" + }, + { + "input": "////x/", + "base": "sc://x/", + "href": "sc:////x/", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "//x/", + "search": "", + "hash": "" + }, + { + "input": "tftp://foobar.com/someconfig;mode=netascii", + "base": "about:blank", + "href": "tftp://foobar.com/someconfig;mode=netascii", + "origin": "null", + "protocol": "tftp:", + "username": "", + "password": "", + "host": "foobar.com", + "hostname": "foobar.com", + "port": "", + "pathname": "/someconfig;mode=netascii", + "search": "", + "hash": "" + }, + { + "input": "telnet://user:pass@foobar.com:23/", + "base": "about:blank", + "href": "telnet://user:pass@foobar.com:23/", + "origin": "null", + "protocol": "telnet:", + "username": "user", + "password": "pass", + "host": "foobar.com:23", + "hostname": "foobar.com", + "port": "23", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ut2004://10.10.10.10:7777/Index.ut2", + "base": "about:blank", + "href": "ut2004://10.10.10.10:7777/Index.ut2", + "origin": "null", + "protocol": "ut2004:", + "username": "", + "password": "", + "host": "10.10.10.10:7777", + "hostname": "10.10.10.10", + "port": "7777", + "pathname": "/Index.ut2", + "search": "", + "hash": "" + }, + { + "input": "redis://foo:bar@somehost:6379/0?baz=bam&qux=baz", + "base": "about:blank", + "href": "redis://foo:bar@somehost:6379/0?baz=bam&qux=baz", + "origin": "null", + "protocol": "redis:", + "username": "foo", + "password": "bar", + "host": "somehost:6379", + "hostname": "somehost", + "port": "6379", + "pathname": "/0", + "search": "?baz=bam&qux=baz", + "hash": "" + }, + { + "input": "rsync://foo@host:911/sup", + "base": "about:blank", + "href": "rsync://foo@host:911/sup", + "origin": "null", + "protocol": "rsync:", + "username": "foo", + "password": "", + "host": "host:911", + "hostname": "host", + "port": "911", + "pathname": "/sup", + "search": "", + "hash": "" + }, + { + "input": "git://github.com/foo/bar.git", + "base": "about:blank", + "href": "git://github.com/foo/bar.git", + "origin": "null", + "protocol": "git:", + "username": "", + "password": "", + "host": "github.com", + "hostname": "github.com", + "port": "", + "pathname": "/foo/bar.git", + "search": "", + "hash": "" + }, + { + "input": "irc://myserver.com:6999/channel?passwd", + "base": "about:blank", + "href": "irc://myserver.com:6999/channel?passwd", + "origin": "null", + "protocol": "irc:", + "username": "", + "password": "", + "host": "myserver.com:6999", + "hostname": "myserver.com", + "port": "6999", + "pathname": "/channel", + "search": "?passwd", + "hash": "" + }, + { + "input": "dns://fw.example.org:9999/foo.bar.org?type=TXT", + "base": "about:blank", + "href": "dns://fw.example.org:9999/foo.bar.org?type=TXT", + "origin": "null", + "protocol": "dns:", + "username": "", + "password": "", + "host": "fw.example.org:9999", + "hostname": "fw.example.org", + "port": "9999", + "pathname": "/foo.bar.org", + "search": "?type=TXT", + "hash": "" + }, + { + "input": "ldap://localhost:389/ou=People,o=JNDITutorial", + "base": "about:blank", + "href": "ldap://localhost:389/ou=People,o=JNDITutorial", + "origin": "null", + "protocol": "ldap:", + "username": "", + "password": "", + "host": "localhost:389", + "hostname": "localhost", + "port": "389", + "pathname": "/ou=People,o=JNDITutorial", + "search": "", + "hash": "" + }, + { + "input": "git+https://github.com/foo/bar", + "base": "about:blank", + "href": "git+https://github.com/foo/bar", + "origin": "null", + "protocol": "git+https:", + "username": "", + "password": "", + "host": "github.com", + "hostname": "github.com", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "urn:ietf:rfc:2648", + "base": "about:blank", + "href": "urn:ietf:rfc:2648", + "origin": "null", + "protocol": "urn:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "ietf:rfc:2648", + "search": "", + "hash": "" + }, + { + "input": "tag:joe@example.org,2001:foo/bar", + "base": "about:blank", + "href": "tag:joe@example.org,2001:foo/bar", + "origin": "null", + "protocol": "tag:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "joe@example.org,2001:foo/bar", + "search": "", + "hash": "" + }, + "# percent encoded hosts in non-special-URLs", + { + "input": "non-special://%E2%80%A0/", + "base": "about:blank", + "href": "non-special://%E2%80%A0/", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "%E2%80%A0", + "hostname": "%E2%80%A0", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "non-special://H%4fSt/path", + "base": "about:blank", + "href": "non-special://H%4fSt/path", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "H%4fSt", + "hostname": "H%4fSt", + "port": "", + "pathname": "/path", + "search": "", + "hash": "" + }, + "# IPv6 in non-special-URLs", + { + "input": "non-special://[1:2:0:0:5:0:0:0]/", + "base": "about:blank", + "href": "non-special://[1:2:0:0:5::]/", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "[1:2:0:0:5::]", + "hostname": "[1:2:0:0:5::]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "non-special://[1:2:0:0:0:0:0:3]/", + "base": "about:blank", + "href": "non-special://[1:2::3]/", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "[1:2::3]", + "hostname": "[1:2::3]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "non-special://[1:2::3]:80/", + "base": "about:blank", + "href": "non-special://[1:2::3]:80/", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "[1:2::3]:80", + "hostname": "[1:2::3]", + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "non-special://[:80/", + "base": "about:blank", + "failure": true + }, + { + "input": "blob:https://example.com:443/", + "base": "about:blank", + "href": "blob:https://example.com:443/", + "protocol": "blob:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "https://example.com:443/", + "search": "", + "hash": "" + }, + { + "input": "blob:d3958f5c-0777-0845-9dcf-2cb28783acaf", + "base": "about:blank", + "href": "blob:d3958f5c-0777-0845-9dcf-2cb28783acaf", + "protocol": "blob:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "d3958f5c-0777-0845-9dcf-2cb28783acaf", + "search": "", + "hash": "" + }, + "Invalid IPv4 radix digits", + { + "input": "http://0177.0.0.0189", + "base": "about:blank", + "href": "http://0177.0.0.0189/", + "protocol": "http:", + "username": "", + "password": "", + "host": "0177.0.0.0189", + "hostname": "0177.0.0.0189", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://0x7f.0.0.0x7g", + "base": "about:blank", + "href": "http://0x7f.0.0.0x7g/", + "protocol": "http:", + "username": "", + "password": "", + "host": "0x7f.0.0.0x7g", + "hostname": "0x7f.0.0.0x7g", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://0X7F.0.0.0X7G", + "base": "about:blank", + "href": "http://0x7f.0.0.0x7g/", + "protocol": "http:", + "username": "", + "password": "", + "host": "0x7f.0.0.0x7g", + "hostname": "0x7f.0.0.0x7g", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Invalid IPv4 portion of IPv6 address", + { + "input": "http://[::127.0.0.0.1]", + "base": "about:blank", + "failure": true + }, + "Uncompressed IPv6 addresses with 0", + { + "input": "http://[0:1:0:1:0:1:0:1]", + "base": "about:blank", + "href": "http://[0:1:0:1:0:1:0:1]/", + "protocol": "http:", + "username": "", + "password": "", + "host": "[0:1:0:1:0:1:0:1]", + "hostname": "[0:1:0:1:0:1:0:1]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[1:0:1:0:1:0:1:0]", + "base": "about:blank", + "href": "http://[1:0:1:0:1:0:1:0]/", + "protocol": "http:", + "username": "", + "password": "", + "host": "[1:0:1:0:1:0:1:0]", + "hostname": "[1:0:1:0:1:0:1:0]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Percent-encoded query and fragment", + { + "input": "http://example.org/test?\u0022", + "base": "about:blank", + "href": "http://example.org/test?%22", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%22", + "hash": "" + }, + { + "input": "http://example.org/test?\u0023", + "base": "about:blank", + "href": "http://example.org/test?#", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "", + "hash": "" + }, + { + "input": "http://example.org/test?\u003C", + "base": "about:blank", + "href": "http://example.org/test?%3C", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%3C", + "hash": "" + }, + { + "input": "http://example.org/test?\u003E", + "base": "about:blank", + "href": "http://example.org/test?%3E", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%3E", + "hash": "" + }, + { + "input": "http://example.org/test?\u2323", + "base": "about:blank", + "href": "http://example.org/test?%E2%8C%A3", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%E2%8C%A3", + "hash": "" + }, + { + "input": "http://example.org/test?%23%23", + "base": "about:blank", + "href": "http://example.org/test?%23%23", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%23%23", + "hash": "" + }, + { + "input": "http://example.org/test?%GH", + "base": "about:blank", + "href": "http://example.org/test?%GH", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%GH", + "hash": "" + }, + { + "input": "http://example.org/test?a#%EF", + "base": "about:blank", + "href": "http://example.org/test?a#%EF", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?a", + "hash": "#%EF" + }, + { + "input": "http://example.org/test?a#%GH", + "base": "about:blank", + "href": "http://example.org/test?a#%GH", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?a", + "hash": "#%GH" + }, + "Bad bases", + { + "input": "test-a.html", + "base": "a", + "failure": true + }, + { + "input": "test-a-slash.html", + "base": "a/", + "failure": true + }, + { + "input": "test-a-slash-slash.html", + "base": "a//", + "failure": true + }, + { + "input": "test-a-colon.html", + "base": "a:", + "failure": true + }, + { + "input": "test-a-colon-slash.html", + "base": "a:/", + "href": "a:/test-a-colon-slash.html", + "protocol": "a:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test-a-colon-slash.html", + "search": "", + "hash": "" + }, + { + "input": "test-a-colon-slash-slash.html", + "base": "a://", + "href": "a:///test-a-colon-slash-slash.html", + "protocol": "a:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test-a-colon-slash-slash.html", + "search": "", + "hash": "" + }, + { + "input": "test-a-colon-b.html", + "base": "a:b", + "failure": true + }, + { + "input": "test-a-colon-slash-b.html", + "base": "a:/b", + "href": "a:/test-a-colon-slash-b.html", + "protocol": "a:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test-a-colon-slash-b.html", + "search": "", + "hash": "" + }, + { + "input": "test-a-colon-slash-slash-b.html", + "base": "a://b", + "href": "a://b/test-a-colon-slash-slash-b.html", + "protocol": "a:", + "username": "", + "password": "", + "host": "b", + "hostname": "b", + "port": "", + "pathname": "/test-a-colon-slash-slash-b.html", + "search": "", + "hash": "" + }, + "Null code point in fragment", + { + "input": "http://example.org/test?a#b\u0000c", + "base": "about:blank", + "href": "http://example.org/test?a#bc", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?a", + "hash": "#bc" + } +] diff --git a/test/parallel/test-url-domain-ascii-unicode.js b/test/parallel/test-url-domain-ascii-unicode.js new file mode 100644 index 00000000000000..49259a7ab0f4c4 --- /dev/null +++ b/test/parallel/test-url-domain-ascii-unicode.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasIntl) + common.skip('missing Intl'); + +const strictEqual = require('assert').strictEqual; +const url = require('url'); + +const domainToASCII = url.domainToASCII; +const domainToUnicode = url.domainToUnicode; + +const domainWithASCII = [ + ['ıíd', 'xn--d-iga7r'], + ['يٴ', 'xn--mhb8f'], + ['www.ϧƽəʐ.com', 'www.xn--cja62apfr6c.com'], + ['новини.com', 'xn--b1amarcd.com'], + ['名がドメイン.com', 'xn--v8jxj3d1dzdz08w.com'], + ['افغانستا.icom.museum', 'xn--mgbaal8b0b9b2b.icom.museum'], + ['الجزائر.icom.fake', 'xn--lgbbat1ad8j.icom.fake'], + ['भारत.org', 'xn--h2brj9c.org'] +]; + +domainWithASCII.forEach((pair) => { + const domain = pair[0]; + const ascii = pair[1]; + const domainConvertedToASCII = domainToASCII(domain); + strictEqual(domainConvertedToASCII, ascii); + const asciiConvertedToUnicode = domainToUnicode(ascii); + strictEqual(asciiConvertedToUnicode, domain); +}); diff --git a/test/parallel/test-whatwg-url-constructor.js b/test/parallel/test-whatwg-url-constructor.js new file mode 100644 index 00000000000000..16bcac74cc6bcd --- /dev/null +++ b/test/parallel/test-whatwg-url-constructor.js @@ -0,0 +1,143 @@ +'use strict'; +const common = require('../common'); +if (!common.hasIntl) { + // A handful of the tests fail when ICU is not included. + common.skip('missing Intl'); +} + +const fixtures = require('../common/fixtures'); +const { URL, URLSearchParams } = require('url'); +const { test, assert_equals, assert_true, assert_throws } = + require('../common/wpt'); + +const request = { + response: require(fixtures.path('url-tests')) +}; + +/* The following tests are copied from WPT. Modifications to them should be + upstreamed first. Refs: + https://github.com/w3c/web-platform-tests/blob/8791bed/url/url-constructor.html + License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html +*/ +/* eslint-disable */ +function runURLConstructorTests() { + // var setup = async_test("Loading data…") + // setup.step(function() { + // var request = new XMLHttpRequest() + // request.open("GET", "urltestdata.json") + // request.send() + // request.responseType = "json" + // request.onload = setup.step_func(function() { + runURLTests(request.response) + // setup.done() + // }) + // }) +} + +function bURL(url, base) { + return new URL(url, base || "about:blank") +} + + +function runURLTests(urltests) { + for(var i = 0, l = urltests.length; i < l; i++) { + var expected = urltests[i] + if (typeof expected === "string") continue // skip comments + + test(function() { + if (expected.failure) { + assert_throws(new TypeError(), function() { + bURL(expected.input, expected.base) + }) + return + } + + var url = bURL(expected.input, expected.base) + assert_equals(url.href, expected.href, "href") + assert_equals(url.protocol, expected.protocol, "protocol") + assert_equals(url.username, expected.username, "username") + assert_equals(url.password, expected.password, "password") + assert_equals(url.host, expected.host, "host") + assert_equals(url.hostname, expected.hostname, "hostname") + assert_equals(url.port, expected.port, "port") + assert_equals(url.pathname, expected.pathname, "pathname") + assert_equals(url.search, expected.search, "search") + if ("searchParams" in expected) { + assert_true("searchParams" in url) + assert_equals(url.searchParams.toString(), expected.searchParams, "searchParams") + } + assert_equals(url.hash, expected.hash, "hash") + }, "Parsing: <" + expected.input + "> against <" + expected.base + ">") + } +} + +function runURLSearchParamTests() { + test(function() { + var url = bURL('http://example.org/?a=b') + assert_true("searchParams" in url) + var searchParams = url.searchParams + assert_true(url.searchParams === searchParams, 'Object identity should hold.') + }, 'URL.searchParams getter') + + test(function() { + var url = bURL('http://example.org/?a=b') + assert_true("searchParams" in url) + var searchParams = url.searchParams + assert_equals(searchParams.toString(), 'a=b') + + searchParams.set('a', 'b') + assert_equals(url.searchParams.toString(), 'a=b') + assert_equals(url.search, '?a=b') + url.search = '' + assert_equals(url.searchParams.toString(), '') + assert_equals(url.search, '') + assert_equals(searchParams.toString(), '') + }, 'URL.searchParams updating, clearing') + + test(function() { + 'use strict' + var urlString = 'http://example.org' + var url = bURL(urlString) + assert_throws(TypeError(), function() { url.searchParams = new URLSearchParams(urlString) }) + }, 'URL.searchParams setter, invalid values') + + test(function() { + var url = bURL('http://example.org/file?a=b&c=d') + assert_true("searchParams" in url) + var searchParams = url.searchParams + assert_equals(url.search, '?a=b&c=d') + assert_equals(searchParams.toString(), 'a=b&c=d') + + // Test that setting 'search' propagates to the URL object's query object. + url.search = 'e=f&g=h' + assert_equals(url.search, '?e=f&g=h') + assert_equals(searchParams.toString(), 'e=f&g=h') + + // ..and same but with a leading '?'. + url.search = '?e=f&g=h' + assert_equals(url.search, '?e=f&g=h') + assert_equals(searchParams.toString(), 'e=f&g=h') + + // And in the other direction, altering searchParams propagates + // back to 'search'. + searchParams.append('i', ' j ') + assert_equals(url.search, '?e=f&g=h&i=+j+') + assert_equals(url.searchParams.toString(), 'e=f&g=h&i=+j+') + assert_equals(searchParams.get('i'), ' j ') + + searchParams.set('e', 'updated') + assert_equals(url.search, '?e=updated&g=h&i=+j+') + assert_equals(searchParams.get('e'), 'updated') + + var url2 = bURL('http://example.org/file??a=b&c=d') + assert_equals(url2.search, '??a=b&c=d') + assert_equals(url2.searchParams.toString(), '%3Fa=b&c=d') + + url2.href = 'http://example.org/file??a=b' + assert_equals(url2.search, '??a=b') + assert_equals(url2.searchParams.toString(), '%3Fa=b') + }, 'URL.searchParams and URL.search setters, update propagation') +} +runURLSearchParamTests() +runURLConstructorTests() +/* eslint-enable */ diff --git a/test/parallel/test-whatwg-url-domainto.js b/test/parallel/test-whatwg-url-domainto.js new file mode 100644 index 00000000000000..f8029d8b5d66a4 --- /dev/null +++ b/test/parallel/test-whatwg-url-domainto.js @@ -0,0 +1,51 @@ +'use strict'; +const common = require('../common'); + +if (!common.hasIntl) + common.skip('missing Intl'); + +const assert = require('assert'); +const { domainToASCII, domainToUnicode } = require('url'); + +// Tests below are not from WPT. +const tests = require('../fixtures/url-idna.js'); +const wptToASCIITests = require('../fixtures/url-toascii.js'); + +{ + assert.throws(() => domainToASCII(), /^TypeError: The "domain" argument must be specified$/); + assert.throws(() => domainToUnicode(), /^TypeError: The "domain" argument must be specified$/); + assert.strictEqual(domainToASCII(undefined), 'undefined'); + assert.strictEqual(domainToUnicode(undefined), 'undefined'); +} + +{ + for (const [i, { ascii, unicode }] of tests.entries()) { + assert.strictEqual(ascii, domainToASCII(unicode), + `domainToASCII(${i + 1})`); + assert.strictEqual(unicode, domainToUnicode(ascii), + `domainToUnicode(${i + 1})`); + assert.strictEqual(ascii, domainToASCII(domainToUnicode(ascii)), + `domainToASCII(domainToUnicode(${i + 1}))`); + assert.strictEqual(unicode, domainToUnicode(domainToASCII(unicode)), + `domainToUnicode(domainToASCII(${i + 1}))`); + } +} + +{ + for (const [i, test] of wptToASCIITests.entries()) { + if (typeof test === 'string') + continue; // skip comments + const { comment, input, output } = test; + let caseComment = `Case ${i + 1}`; + if (comment) + caseComment += ` (${comment})`; + if (output === null) { + assert.strictEqual(domainToASCII(input), '', caseComment); + assert.strictEqual(domainToUnicode(input), '', caseComment); + } else { + assert.strictEqual(domainToASCII(input), output, caseComment); + const roundtripped = domainToASCII(domainToUnicode(input)); + assert.strictEqual(roundtripped, output, caseComment); + } + } +} diff --git a/test/parallel/test-whatwg-url-historical.js b/test/parallel/test-whatwg-url-historical.js new file mode 100644 index 00000000000000..466949cd322d37 --- /dev/null +++ b/test/parallel/test-whatwg-url-historical.js @@ -0,0 +1,46 @@ +'use strict'; +const common = require('../common'); +if (!common.hasIntl) { + // A handful of the tests fail when ICU is not included. + common.skip('missing Intl'); +} + +const URL = require('url').URL; +const { test, assert_equals, assert_throws } = require('../common/wpt'); + +/* The following tests are copied from WPT. Modifications to them should be + upstreamed first. Refs: + https://github.com/w3c/web-platform-tests/blob/8791bed/url/historical.html + License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html +*/ +/* eslint-disable */ +// var objects = [ +// [function() { return window.location }, "location object"], +// [function() { return document.createElement("a") }, "a element"], +// [function() { return document.createElement("area") }, "area element"], +// ]; + +// objects.forEach(function(o) { +// test(function() { +// var object = o[0](); +// assert_false("searchParams" in object, +// o[1] + " should not have a searchParams attribute"); +// }, "searchParams on " + o[1]); +// }); + +test(function() { + var url = new URL("./foo", "http://www.example.org"); + assert_equals(url.href, "http://www.example.org/foo"); + assert_throws(new TypeError(), function() { + url.href = "./bar"; + }); +}, "Setting URL's href attribute and base URLs"); + +test(function() { + assert_equals(URL.domainToASCII, undefined); +}, "URL.domainToASCII should be undefined"); + +test(function() { + assert_equals(URL.domainToUnicode, undefined); +}, "URL.domainToUnicode should be undefined"); +/* eslint-enable */ diff --git a/test/parallel/test-whatwg-url-inspect.js b/test/parallel/test-whatwg-url-inspect.js new file mode 100644 index 00000000000000..5758b39b8af83d --- /dev/null +++ b/test/parallel/test-whatwg-url-inspect.js @@ -0,0 +1,66 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasIntl) { + // A handful of the tests fail when ICU is not included. + common.skip('missing Intl'); +} + +const util = require('util'); +const URL = require('url').URL; +const assert = require('assert'); + +// Tests below are not from WPT. +const url = new URL('https://username:password@host.name:8080/path/name/?que=ry#hash'); + +assert.strictEqual( + util.inspect(url), + `URL { + href: 'https://username:password@host.name:8080/path/name/?que=ry#hash', + origin: 'https://host.name:8080', + protocol: 'https:', + username: 'username', + password: 'password', + host: 'host.name:8080', + hostname: 'host.name', + port: '8080', + pathname: '/path/name/', + search: '?que=ry', + searchParams: URLSearchParams { 'que' => 'ry' }, + hash: '#hash' }`); + +assert.strictEqual( + util.inspect(url, { showHidden: true }), + `URL { + href: 'https://username:password@host.name:8080/path/name/?que=ry#hash', + origin: 'https://host.name:8080', + protocol: 'https:', + username: 'username', + password: 'password', + host: 'host.name:8080', + hostname: 'host.name', + port: '8080', + pathname: '/path/name/', + search: '?que=ry', + searchParams: URLSearchParams { 'que' => 'ry' }, + hash: '#hash', + cannotBeBase: false, + special: true, + [Symbol(context)]:\x20 + URLContext { + flags: 2032, + scheme: 'https:', + username: 'username', + password: 'password', + host: 'host.name', + port: 8080, + path: [ 'path', 'name', '', [length]: 3 ], + query: 'que=ry', + fragment: 'hash' } }`); + +assert.strictEqual( + util.inspect({ a: url }, { depth: 0 }), + '{ a: [Object] }'); + +class MyURL extends URL {} +assert(util.inspect(new MyURL(url.href)).startsWith('MyURL {')); diff --git a/test/parallel/test-whatwg-url-origin.js b/test/parallel/test-whatwg-url-origin.js new file mode 100644 index 00000000000000..8a5ad6d3154f8f --- /dev/null +++ b/test/parallel/test-whatwg-url-origin.js @@ -0,0 +1,52 @@ +'use strict'; +const common = require('../common'); +if (!common.hasIntl) { + // A handful of the tests fail when ICU is not included. + common.skip('missing Intl'); +} + +const fixtures = require('../common/fixtures'); +const URL = require('url').URL; +const { test, assert_equals } = require('../common/wpt'); + +const request = { + response: require(fixtures.path('url-tests')) +}; + +/* The following tests are copied from WPT. Modifications to them should be + upstreamed first. Refs: + https://github.com/w3c/web-platform-tests/blob/8791bed/url/url-origin.html + License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html +*/ +/* eslint-disable */ +function runURLOriginTests() { + // var setup = async_test("Loading data…") + // setup.step(function() { + // var request = new XMLHttpRequest() + // request.open("GET", "urltestdata.json") + // request.send() + // request.responseType = "json" + // request.onload = setup.step_func(function() { + runURLTests(request.response) + // setup.done() + // }) + // }) +} + +function bURL(url, base) { + return new URL(url, base || "about:blank") +} + +function runURLTests(urltests) { + for(var i = 0, l = urltests.length; i < l; i++) { + var expected = urltests[i] + if (typeof expected === "string" || !("origin" in expected)) continue + test(function() { + var url = bURL(expected.input, expected.base) + assert_equals(url.origin, expected.origin, "origin") + }, "Origin parsing: <" + expected.input + "> against <" + expected.base + ">") + } +} + +runURLOriginTests() +/* eslint-enable */ diff --git a/test/parallel/test-whatwg-url-parsing.js b/test/parallel/test-whatwg-url-parsing.js new file mode 100644 index 00000000000000..104f25ff4380bb --- /dev/null +++ b/test/parallel/test-whatwg-url-parsing.js @@ -0,0 +1,62 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasIntl) { + // A handful of the tests fail when ICU is not included. + common.skip('missing Intl'); +} + +const URL = require('url').URL; +const assert = require('assert'); +const fixtures = require('../common/fixtures'); + +// Tests below are not from WPT. +const tests = require(fixtures.path('url-tests')); +const failureTests = tests.filter((test) => test.failure).concat([ + { input: '' }, + { input: 'test' }, + { input: undefined }, + { input: 0 }, + { input: true }, + { input: false }, + { input: null }, + { input: new Date() }, + { input: new RegExp() }, + { input: () => {} } +]); + +const expectedError = (err) => /^TypeError: Invalid URL: /.test(err.toString()); + +for (const test of failureTests) { + assert.throws( + () => new URL(test.input, test.base), + (error) => { + if (!expectedError(error)) + return false; + + // The input could be processed, so we don't do strict matching here + const match = (`${error}`).match(/Invalid URL: (.*)$/); + if (!match) { + return false; + } + return error.input === match[1]; + }); +} + +const additional_tests = + require(fixtures.path('url-tests-additional.js')); + +for (const test of additional_tests) { + const url = new URL(test.url); + if (test.href) assert.strictEqual(url.href, test.href); + if (test.origin) assert.strictEqual(url.origin, test.origin); + if (test.protocol) assert.strictEqual(url.protocol, test.protocol); + if (test.username) assert.strictEqual(url.username, test.username); + if (test.password) assert.strictEqual(url.password, test.password); + if (test.hostname) assert.strictEqual(url.hostname, test.hostname); + if (test.host) assert.strictEqual(url.host, test.host); + if (test.port !== undefined) assert.strictEqual(url.port, test.port); + if (test.pathname) assert.strictEqual(url.pathname, test.pathname); + if (test.search) assert.strictEqual(url.search, test.search); + if (test.hash) assert.strictEqual(url.hash, test.hash); +} diff --git a/test/parallel/test-whatwg-url-properties.js b/test/parallel/test-whatwg-url-properties.js new file mode 100644 index 00000000000000..d6caae511aed47 --- /dev/null +++ b/test/parallel/test-whatwg-url-properties.js @@ -0,0 +1,162 @@ +// Flags: --expose-internals +'use strict'; + +require('../common'); +const URL = require('url').URL; +const assert = require('assert'); +const urlToOptions = require('internal/url').urlToOptions; + +// Tests below are not from WPT. +const url = new URL('http://user:pass@foo.bar.com:21/aaa/zzz?l=24#test'); +const oldParams = url.searchParams; // for test of [SameObject] + +// To retrieve enumerable but not necessarily own properties, +// we need to use the for-in loop. +const props = []; +for (const prop in url) { + props.push(prop); +} + +// See: https://url.spec.whatwg.org/#api +// https://heycam.github.io/webidl/#es-attributes +// https://heycam.github.io/webidl/#es-stringifier +const expected = ['toString', + 'href', 'origin', 'protocol', + 'username', 'password', 'host', 'hostname', 'port', + 'pathname', 'search', 'searchParams', 'hash', 'toJSON']; + +assert.deepStrictEqual(props, expected); + +// href is writable (not readonly) and is stringifier +assert.strictEqual(url.toString(), url.href); +url.href = 'http://user:pass@foo.bar.com:21/aaa/zzz?l=25#test'; +assert.strictEqual(url.href, + 'http://user:pass@foo.bar.com:21/aaa/zzz?l=25#test'); +assert.strictEqual(url.toString(), url.href); +// Return true because it's configurable, but because the properties +// are defined on the prototype per the spec, the deletion has no effect +assert.strictEqual((delete url.href), true); +assert.strictEqual(url.href, + 'http://user:pass@foo.bar.com:21/aaa/zzz?l=25#test'); +assert.strictEqual(url.searchParams, oldParams); // [SameObject] + +// searchParams is readonly. Under strict mode setting a +// non-writable property should throw. +// Note: this error message is subject to change in V8 updates +assert.throws( + () => url.origin = 'http://foo.bar.com:22', + /^TypeError: Cannot set property origin of \[object URL\] which has only a getter$/ +); +assert.strictEqual(url.origin, 'http://foo.bar.com:21'); +assert.strictEqual(url.toString(), + 'http://user:pass@foo.bar.com:21/aaa/zzz?l=25#test'); +assert.strictEqual((delete url.origin), true); +assert.strictEqual(url.origin, 'http://foo.bar.com:21'); + +// The following properties should be writable (not readonly) +url.protocol = 'https:'; +assert.strictEqual(url.protocol, 'https:'); +assert.strictEqual(url.toString(), + 'https://user:pass@foo.bar.com:21/aaa/zzz?l=25#test'); +assert.strictEqual((delete url.protocol), true); +assert.strictEqual(url.protocol, 'https:'); + +url.username = 'user2'; +assert.strictEqual(url.username, 'user2'); +assert.strictEqual(url.toString(), + 'https://user2:pass@foo.bar.com:21/aaa/zzz?l=25#test'); +assert.strictEqual((delete url.username), true); +assert.strictEqual(url.username, 'user2'); + +url.password = 'pass2'; +assert.strictEqual(url.password, 'pass2'); +assert.strictEqual(url.toString(), + 'https://user2:pass2@foo.bar.com:21/aaa/zzz?l=25#test'); +assert.strictEqual((delete url.password), true); +assert.strictEqual(url.password, 'pass2'); + +url.host = 'foo.bar.net:22'; +assert.strictEqual(url.host, 'foo.bar.net:22'); +assert.strictEqual(url.toString(), + 'https://user2:pass2@foo.bar.net:22/aaa/zzz?l=25#test'); +assert.strictEqual((delete url.host), true); +assert.strictEqual(url.host, 'foo.bar.net:22'); + +url.hostname = 'foo.bar.org'; +assert.strictEqual(url.hostname, 'foo.bar.org'); +assert.strictEqual(url.toString(), + 'https://user2:pass2@foo.bar.org:22/aaa/zzz?l=25#test'); +assert.strictEqual((delete url.hostname), true); +assert.strictEqual(url.hostname, 'foo.bar.org'); + +url.port = '23'; +assert.strictEqual(url.port, '23'); +assert.strictEqual(url.toString(), + 'https://user2:pass2@foo.bar.org:23/aaa/zzz?l=25#test'); +assert.strictEqual((delete url.port), true); +assert.strictEqual(url.port, '23'); + +url.pathname = '/aaa/bbb'; +assert.strictEqual(url.pathname, '/aaa/bbb'); +assert.strictEqual(url.toString(), + 'https://user2:pass2@foo.bar.org:23/aaa/bbb?l=25#test'); +assert.strictEqual((delete url.pathname), true); +assert.strictEqual(url.pathname, '/aaa/bbb'); + +url.search = '?k=99'; +assert.strictEqual(url.search, '?k=99'); +assert.strictEqual(url.toString(), + 'https://user2:pass2@foo.bar.org:23/aaa/bbb?k=99#test'); +assert.strictEqual((delete url.search), true); +assert.strictEqual(url.search, '?k=99'); + +url.hash = '#abcd'; +assert.strictEqual(url.hash, '#abcd'); +assert.strictEqual(url.toString(), + 'https://user2:pass2@foo.bar.org:23/aaa/bbb?k=99#abcd'); +assert.strictEqual((delete url.hash), true); +assert.strictEqual(url.hash, '#abcd'); + +// searchParams is readonly. Under strict mode setting a +// non-writable property should throw. +// Note: this error message is subject to change in V8 updates +assert.throws( + () => url.searchParams = '?k=88', + /^TypeError: Cannot set property searchParams of \[object URL\] which has only a getter$/ +); +assert.strictEqual(url.searchParams, oldParams); +assert.strictEqual(url.toString(), + 'https://user2:pass2@foo.bar.org:23/aaa/bbb?k=99#abcd'); +assert.strictEqual((delete url.searchParams), true); +assert.strictEqual(url.searchParams, oldParams); + +// Test urlToOptions +{ + const opts = + urlToOptions(new URL('http://user:pass@foo.bar.com:21/aaa/zzz?l=24#test')); + assert.strictEqual(opts instanceof URL, false); + assert.strictEqual(opts.protocol, 'http:'); + assert.strictEqual(opts.auth, 'user:pass'); + assert.strictEqual(opts.hostname, 'foo.bar.com'); + assert.strictEqual(opts.port, 21); + assert.strictEqual(opts.path, '/aaa/zzz?l=24'); + assert.strictEqual(opts.pathname, '/aaa/zzz'); + assert.strictEqual(opts.search, '?l=24'); + assert.strictEqual(opts.hash, '#test'); +} + +// Test special origins +[ + { expected: 'https://whatwg.org', + url: 'blob:https://whatwg.org/d0360e2f-caee-469f-9a2f-87d5b0456f6f' }, + { expected: 'ftp://example.org', url: 'ftp://example.org/foo' }, + { expected: 'gopher://gopher.quux.org', url: 'gopher://gopher.quux.org/1/' }, + { expected: 'http://example.org', url: 'http://example.org/foo' }, + { expected: 'https://example.org', url: 'https://example.org/foo' }, + { expected: 'ws://example.org', url: 'ws://example.org/foo' }, + { expected: 'wss://example.org', url: 'wss://example.org/foo' }, + { expected: 'null', url: 'file:///tmp/mock/path' }, + { expected: 'null', url: 'npm://nodejs/rules' } +].forEach((test) => { + assert.strictEqual(new URL(test.url).origin, test.expected); +}); diff --git a/test/parallel/test-whatwg-url-searchparams-append.js b/test/parallel/test-whatwg-url-searchparams-append.js new file mode 100644 index 00000000000000..6571f570588339 --- /dev/null +++ b/test/parallel/test-whatwg-url-searchparams-append.js @@ -0,0 +1,73 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const URLSearchParams = require('url').URLSearchParams; +const { test, assert_equals, assert_true } = require('../common/wpt'); + +/* The following tests are copied from WPT. Modifications to them should be + upstreamed first. Refs: + https://github.com/w3c/web-platform-tests/blob/8791bed/url/urlsearchparams-append.html + License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html +*/ +/* eslint-disable */ +test(function() { + var params = new URLSearchParams(); + params.append('a', 'b'); + assert_equals(params + '', 'a=b'); + params.append('a', 'b'); + assert_equals(params + '', 'a=b&a=b'); + params.append('a', 'c'); + assert_equals(params + '', 'a=b&a=b&a=c'); +}, 'Append same name'); +test(function() { + var params = new URLSearchParams(); + params.append('', ''); + assert_equals(params + '', '='); + params.append('', ''); + assert_equals(params + '', '=&='); +}, 'Append empty strings'); +test(function() { + var params = new URLSearchParams(); + params.append(null, null); + assert_equals(params + '', 'null=null'); + params.append(null, null); + assert_equals(params + '', 'null=null&null=null'); +}, 'Append null'); +test(function() { + var params = new URLSearchParams(); + params.append('first', 1); + params.append('second', 2); + params.append('third', ''); + params.append('first', 10); + assert_true(params.has('first'), 'Search params object has name "first"'); + assert_equals(params.get('first'), '1', 'Search params object has name "first" with value "1"'); + assert_equals(params.get('second'), '2', 'Search params object has name "second" with value "2"'); + assert_equals(params.get('third'), '', 'Search params object has name "third" with value ""'); + params.append('first', 10); + assert_equals(params.get('first'), '1', 'Search params object has name "first" with value "1"'); +}, 'Append multiple'); +/* eslint-enable */ + +// Tests below are not from WPT. +{ + const params = new URLSearchParams(); + assert.throws(() => { + params.append.call(undefined); + }, /^TypeError: Value of "this" must be of type URLSearchParams$/); + assert.throws(() => { + params.append('a'); + }, /^TypeError: The "name" and "value" arguments must be specified$/); + + const obj = { + toString() { throw new Error('toString'); }, + valueOf() { throw new Error('valueOf'); } + }; + const sym = Symbol(); + assert.throws(() => params.set(obj, 'b'), /^Error: toString$/); + assert.throws(() => params.set('a', obj), /^Error: toString$/); + assert.throws(() => params.set(sym, 'b'), + /^TypeError: Cannot convert a Symbol value to a string$/); + assert.throws(() => params.set('a', sym), + /^TypeError: Cannot convert a Symbol value to a string$/); +} diff --git a/test/parallel/test-whatwg-url-searchparams-constructor.js b/test/parallel/test-whatwg-url-searchparams-constructor.js new file mode 100644 index 00000000000000..a3e15875276087 --- /dev/null +++ b/test/parallel/test-whatwg-url-searchparams-constructor.js @@ -0,0 +1,248 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const URLSearchParams = require('url').URLSearchParams; +const { + test, assert_equals, assert_true, + assert_false, assert_throws, assert_array_equals +} = require('../common/wpt'); + +/* The following tests are copied from WPT. Modifications to them should be + upstreamed first. Refs: + https://github.com/w3c/web-platform-tests/blob/54c3502d7b/url/urlsearchparams-constructor.html + License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html +*/ +/* eslint-disable */ +var params; // Strict mode fix for WPT. +test(function() { + var params = new URLSearchParams(); + assert_equals(params + '', ''); + params = new URLSearchParams(''); + assert_equals(params + '', ''); + params = new URLSearchParams('a=b'); + assert_equals(params + '', 'a=b'); + params = new URLSearchParams(params); + assert_equals(params + '', 'a=b'); +}, 'Basic URLSearchParams construction'); + +test(function() { + var params = new URLSearchParams() + assert_equals(params.toString(), "") +}, "URLSearchParams constructor, no arguments") + +// test(() => { +// params = new URLSearchParams(DOMException.prototype); +// assert_equals(params.toString(), "INDEX_SIZE_ERR=1&DOMSTRING_SIZE_ERR=2&HIERARCHY_REQUEST_ERR=3&WRONG_DOCUMENT_ERR=4&INVALID_CHARACTER_ERR=5&NO_DATA_ALLOWED_ERR=6&NO_MODIFICATION_ALLOWED_ERR=7&NOT_FOUND_ERR=8&NOT_SUPPORTED_ERR=9&INUSE_ATTRIBUTE_ERR=10&INVALID_STATE_ERR=11&SYNTAX_ERR=12&INVALID_MODIFICATION_ERR=13&NAMESPACE_ERR=14&INVALID_ACCESS_ERR=15&VALIDATION_ERR=16&TYPE_MISMATCH_ERR=17&SECURITY_ERR=18&NETWORK_ERR=19&ABORT_ERR=20&URL_MISMATCH_ERR=21"A_EXCEEDED_ERR=22&TIMEOUT_ERR=23&INVALID_NODE_TYPE_ERR=24&DATA_CLONE_ERR=25") +// }, "URLSearchParams constructor, DOMException.prototype as argument") + +test(() => { + params = new URLSearchParams(''); + assert_true(params != null, 'constructor returned non-null value.'); + assert_equals(params.__proto__, URLSearchParams.prototype, 'expected URLSearchParams.prototype as prototype.'); +}, "URLSearchParams constructor, empty string as argument") + +test(() => { + params = new URLSearchParams({}); + assert_equals(params + '', ""); +}, 'URLSearchParams constructor, {} as argument'); + +test(function() { + var params = new URLSearchParams('a=b'); + assert_true(params != null, 'constructor returned non-null value.'); + assert_true(params.has('a'), 'Search params object has name "a"'); + assert_false(params.has('b'), 'Search params object has not got name "b"'); + var params = new URLSearchParams('a=b&c'); + assert_true(params != null, 'constructor returned non-null value.'); + assert_true(params.has('a'), 'Search params object has name "a"'); + assert_true(params.has('c'), 'Search params object has name "c"'); + var params = new URLSearchParams('&a&&& &&&&&a+b=& c&m%c3%b8%c3%b8'); + assert_true(params != null, 'constructor returned non-null value.'); + assert_true(params.has('a'), 'Search params object has name "a"'); + assert_true(params.has('a b'), 'Search params object has name "a b"'); + assert_true(params.has(' '), 'Search params object has name " "'); + assert_false(params.has('c'), 'Search params object did not have the name "c"'); + assert_true(params.has(' c'), 'Search params object has name " c"'); + assert_true(params.has('møø'), 'Search params object has name "møø"'); +}, 'URLSearchParams constructor, string.'); + +test(function() { + var seed = new URLSearchParams('a=b&c=d'); + var params = new URLSearchParams(seed); + assert_true(params != null, 'constructor returned non-null value.'); + assert_equals(params.get('a'), 'b'); + assert_equals(params.get('c'), 'd'); + assert_false(params.has('d')); + // The name-value pairs are copied when created; later updates + // should not be observable. + seed.append('e', 'f'); + assert_false(params.has('e')); + params.append('g', 'h'); + assert_false(seed.has('g')); +}, 'URLSearchParams constructor, object.'); + +test(function() { + var params = new URLSearchParams('a=b+c'); + assert_equals(params.get('a'), 'b c'); + params = new URLSearchParams('a+b=c'); + assert_equals(params.get('a b'), 'c'); +}, 'Parse +'); + +test(function() { + const testValue = '+15555555555'; + const params = new URLSearchParams(); + params.set('query', testValue); + var newParams = new URLSearchParams(params.toString()); + + assert_equals(params.toString(), 'query=%2B15555555555'); + assert_equals(params.get('query'), testValue); + assert_equals(newParams.get('query'), testValue); +}, 'Parse encoded +'); + +test(function() { + var params = new URLSearchParams('a=b c'); + assert_equals(params.get('a'), 'b c'); + params = new URLSearchParams('a b=c'); + assert_equals(params.get('a b'), 'c'); +}, 'Parse space'); + +test(function() { + var params = new URLSearchParams('a=b%20c'); + assert_equals(params.get('a'), 'b c'); + params = new URLSearchParams('a%20b=c'); + assert_equals(params.get('a b'), 'c'); +}, 'Parse %20'); + +test(function() { + var params = new URLSearchParams('a=b\0c'); + assert_equals(params.get('a'), 'b\0c'); + params = new URLSearchParams('a\0b=c'); + assert_equals(params.get('a\0b'), 'c'); +}, 'Parse \\0'); + +test(function() { + var params = new URLSearchParams('a=b%00c'); + assert_equals(params.get('a'), 'b\0c'); + params = new URLSearchParams('a%00b=c'); + assert_equals(params.get('a\0b'), 'c'); +}, 'Parse %00'); + +test(function() { + var params = new URLSearchParams('a=b\u2384'); + assert_equals(params.get('a'), 'b\u2384'); + params = new URLSearchParams('a\u2384b=c'); + assert_equals(params.get('a\u2384b'), 'c'); +}, 'Parse \u2384'); // Unicode Character 'COMPOSITION SYMBOL' (U+2384) + +test(function() { + var params = new URLSearchParams('a=b%e2%8e%84'); + assert_equals(params.get('a'), 'b\u2384'); + params = new URLSearchParams('a%e2%8e%84b=c'); + assert_equals(params.get('a\u2384b'), 'c'); +}, 'Parse %e2%8e%84'); // Unicode Character 'COMPOSITION SYMBOL' (U+2384) + +test(function() { + var params = new URLSearchParams('a=b\uD83D\uDCA9c'); + assert_equals(params.get('a'), 'b\uD83D\uDCA9c'); + params = new URLSearchParams('a\uD83D\uDCA9b=c'); + assert_equals(params.get('a\uD83D\uDCA9b'), 'c'); +}, 'Parse \uD83D\uDCA9'); // Unicode Character 'PILE OF POO' (U+1F4A9) + +test(function() { + var params = new URLSearchParams('a=b%f0%9f%92%a9c'); + assert_equals(params.get('a'), 'b\uD83D\uDCA9c'); + params = new URLSearchParams('a%f0%9f%92%a9b=c'); + assert_equals(params.get('a\uD83D\uDCA9b'), 'c'); +}, 'Parse %f0%9f%92%a9'); // Unicode Character 'PILE OF POO' (U+1F4A9) + +test(function() { + var params = new URLSearchParams([]); + assert_true(params != null, 'constructor returned non-null value.'); + params = new URLSearchParams([['a', 'b'], ['c', 'd']]); + assert_equals(params.get("a"), "b"); + assert_equals(params.get("c"), "d"); + assert_throws(new TypeError(), function() { new URLSearchParams([[1]]); }); + assert_throws(new TypeError(), function() { new URLSearchParams([[1,2,3]]); }); +}, "Constructor with sequence of sequences of strings"); + +[ + { "input": {"+": "%C2"}, "output": [["+", "%C2"]], "name": "object with +" }, + { "input": {c: "x", a: "?"}, "output": [["c", "x"], ["a", "?"]], "name": "object with two keys" }, + { "input": [["c", "x"], ["a", "?"]], "output": [["c", "x"], ["a", "?"]], "name": "array with two keys" }, + { "input": {"a\0b": "42", "c\uD83D": "23", "d\u1234": "foo"}, "output": [["a\0b", "42"], ["c\uFFFD", "23"], ["d\u1234", "foo"]], "name": "object with NULL, non-ASCII, and surrogate keys" } +].forEach((val) => { + test(() => { + let params = new URLSearchParams(val.input), + i = 0 + for (let param of params) { + assert_array_equals(param, val.output[i]) + i++ + } + }, "Construct with " + val.name) +}) + +test(() => { + params = new URLSearchParams() + params[Symbol.iterator] = function *() { + yield ["a", "b"] + } + let params2 = new URLSearchParams(params) + assert_equals(params2.get("a"), "b") +}, "Custom [Symbol.iterator]") +/* eslint-enable */ + +// Tests below are not from WPT. +function makeIterableFunc(array) { + return Object.assign(() => {}, { + [Symbol.iterator]() { + return array[Symbol.iterator](); + } + }); +} + +{ + const iterableError = /^TypeError: Query pairs must be iterable$/; + const tupleError = + /^TypeError: Each query pair must be an iterable \[name, value] tuple$/; + + let params; + params = new URLSearchParams(undefined); + assert.strictEqual(params.toString(), ''); + params = new URLSearchParams(null); + assert.strictEqual(params.toString(), ''); + params = new URLSearchParams( + makeIterableFunc([['key', 'val'], ['key2', 'val2']]) + ); + assert.strictEqual(params.toString(), 'key=val&key2=val2'); + params = new URLSearchParams( + makeIterableFunc([['key', 'val'], ['key2', 'val2']].map(makeIterableFunc)) + ); + assert.strictEqual(params.toString(), 'key=val&key2=val2'); + assert.throws(() => new URLSearchParams([[1]]), tupleError); + assert.throws(() => new URLSearchParams([[1, 2, 3]]), tupleError); + assert.throws(() => new URLSearchParams({ [Symbol.iterator]: 42 }), + iterableError); + assert.throws(() => new URLSearchParams([{}]), tupleError); + assert.throws(() => new URLSearchParams(['a']), tupleError); + assert.throws(() => new URLSearchParams([null]), tupleError); + assert.throws(() => new URLSearchParams([{ [Symbol.iterator]: 42 }]), + tupleError); +} + +{ + const obj = { + toString() { throw new Error('toString'); }, + valueOf() { throw new Error('valueOf'); } + }; + const sym = Symbol(); + const toStringError = /^Error: toString$/; + const symbolError = /^TypeError: Cannot convert a Symbol value to a string$/; + + assert.throws(() => new URLSearchParams({ a: obj }), toStringError); + assert.throws(() => new URLSearchParams([['a', obj]]), toStringError); + assert.throws(() => new URLSearchParams(sym), symbolError); + assert.throws(() => new URLSearchParams({ [sym]: 'a' }), symbolError); + assert.throws(() => new URLSearchParams({ a: sym }), symbolError); + assert.throws(() => new URLSearchParams([[sym, 'a']]), symbolError); + assert.throws(() => new URLSearchParams([['a', sym]]), symbolError); +} diff --git a/test/parallel/test-whatwg-url-searchparams-delete.js b/test/parallel/test-whatwg-url-searchparams-delete.js new file mode 100644 index 00000000000000..bd52a13e9174b3 --- /dev/null +++ b/test/parallel/test-whatwg-url-searchparams-delete.js @@ -0,0 +1,92 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const { URL, URLSearchParams } = require('url'); +const { test, assert_equals, assert_true, assert_false } = + require('../common/wpt'); + +/* The following tests are copied from WPT. Modifications to them should be + upstreamed first. Refs: + https://github.com/w3c/web-platform-tests/blob/70a0898763/url/urlsearchparams-delete.html + License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html +*/ +/* eslint-disable */ +test(function() { + var params = new URLSearchParams('a=b&c=d'); + params.delete('a'); + assert_equals(params + '', 'c=d'); + params = new URLSearchParams('a=a&b=b&a=a&c=c'); + params.delete('a'); + assert_equals(params + '', 'b=b&c=c'); + params = new URLSearchParams('a=a&=&b=b&c=c'); + params.delete(''); + assert_equals(params + '', 'a=a&b=b&c=c'); + params = new URLSearchParams('a=a&null=null&b=b'); + params.delete(null); + assert_equals(params + '', 'a=a&b=b'); + params = new URLSearchParams('a=a&undefined=undefined&b=b'); + params.delete(undefined); + assert_equals(params + '', 'a=a&b=b'); +}, 'Delete basics'); + +test(function() { + var params = new URLSearchParams(); + params.append('first', 1); + assert_true(params.has('first'), 'Search params object has name "first"'); + assert_equals(params.get('first'), '1', 'Search params object has name "first" with value "1"'); + params.delete('first'); + assert_false(params.has('first'), 'Search params object has no "first" name'); + params.append('first', 1); + params.append('first', 10); + params.delete('first'); + assert_false(params.has('first'), 'Search params object has no "first" name'); +}, 'Deleting appended multiple'); + +test(function() { + var url = new URL('http://example.com/?param1¶m2'); + url.searchParams.delete('param1'); + url.searchParams.delete('param2'); + assert_equals(url.href, 'http://example.com/', 'url.href does not have ?'); + assert_equals(url.search, '', 'url.search does not have ?'); +}, 'Deleting all params removes ? from URL'); + +test(function() { + var url = new URL('http://example.com/?'); + url.searchParams.delete('param1'); + assert_equals(url.href, 'http://example.com/', 'url.href does not have ?'); + assert_equals(url.search, '', 'url.search does not have ?'); +}, 'Removing non-existent param removes ? from URL'); +/* eslint-enable */ + +// Tests below are not from WPT. +{ + const params = new URLSearchParams(); + assert.throws(() => { + params.delete.call(undefined); + }, /^TypeError: Value of "this" must be of type URLSearchParams$/); + assert.throws(() => { + params.delete(); + }, /^TypeError: The "name" argument must be specified$/); + + const obj = { + toString() { throw new Error('toString'); }, + valueOf() { throw new Error('valueOf'); } + }; + const sym = Symbol(); + assert.throws(() => params.delete(obj), /^Error: toString$/); + assert.throws(() => params.delete(sym), + /^TypeError: Cannot convert a Symbol value to a string$/); +} + +// https://github.com/nodejs/node/issues/10480 +// Emptying searchParams should correctly update url's query +{ + const url = new URL('http://domain?var=1&var=2&var=3'); + for (const param of url.searchParams.keys()) { + url.searchParams.delete(param); + } + assert.strictEqual(url.searchParams.toString(), ''); + assert.strictEqual(url.search, ''); + assert.strictEqual(url.href, 'http://domain/'); +} diff --git a/test/parallel/test-whatwg-url-searchparams-entries.js b/test/parallel/test-whatwg-url-searchparams-entries.js new file mode 100644 index 00000000000000..4e73b92b517fd9 --- /dev/null +++ b/test/parallel/test-whatwg-url-searchparams-entries.js @@ -0,0 +1,34 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const URLSearchParams = require('url').URLSearchParams; + +// Tests below are not from WPT. +const params = new URLSearchParams('a=b&c=d'); +const entries = params.entries(); +assert.strictEqual(typeof entries[Symbol.iterator], 'function'); +assert.strictEqual(entries[Symbol.iterator](), entries); +assert.deepStrictEqual(entries.next(), { + value: ['a', 'b'], + done: false +}); +assert.deepStrictEqual(entries.next(), { + value: ['c', 'd'], + done: false +}); +assert.deepStrictEqual(entries.next(), { + value: undefined, + done: true +}); +assert.deepStrictEqual(entries.next(), { + value: undefined, + done: true +}); + +assert.throws(() => { + entries.next.call(undefined); +}, /^TypeError: Value of "this" must be of type URLSearchParamsIterator$/); +assert.throws(() => { + params.entries.call(undefined); +}, /^TypeError: Value of "this" must be of type URLSearchParams$/); diff --git a/test/parallel/test-whatwg-url-searchparams-foreach.js b/test/parallel/test-whatwg-url-searchparams-foreach.js new file mode 100644 index 00000000000000..06f21723a6cd2f --- /dev/null +++ b/test/parallel/test-whatwg-url-searchparams-foreach.js @@ -0,0 +1,56 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const { URL, URLSearchParams } = require('url'); +const { test, assert_array_equals, assert_unreached } = + require('../common/wpt'); + +/* The following tests are copied from WPT. Modifications to them should be + upstreamed first. Refs: + https://github.com/w3c/web-platform-tests/blob/a8b2b1e/url/urlsearchparams-foreach.html + License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html +*/ +/* eslint-disable */ +var i; // Strict mode fix for WPT. +test(function() { + var params = new URLSearchParams('a=1&b=2&c=3'); + var keys = []; + var values = []; + params.forEach(function(value, key) { + keys.push(key); + values.push(value); + }); + assert_array_equals(keys, ['a', 'b', 'c']); + assert_array_equals(values, ['1', '2', '3']); +}, "ForEach Check"); + +test(function() { + let a = new URL("http://a.b/c?a=1&b=2&c=3&d=4"); + let b = a.searchParams; + var c = []; + for (i of b) { + a.search = "x=1&y=2&z=3"; + c.push(i); + } + assert_array_equals(c[0], ["a","1"]); + assert_array_equals(c[1], ["y","2"]); + assert_array_equals(c[2], ["z","3"]); +}, "For-of Check"); + +test(function() { + let a = new URL("http://a.b/c"); + let b = a.searchParams; + for (i of b) { + assert_unreached(i); + } +}, "empty"); +/* eslint-enable */ + +// Tests below are not from WPT. +{ + const params = new URLSearchParams(); + assert.throws(() => { + params.forEach.call(undefined); + }, /^TypeError: Value of "this" must be of type URLSearchParams$/); +} diff --git a/test/parallel/test-whatwg-url-searchparams-get.js b/test/parallel/test-whatwg-url-searchparams-get.js new file mode 100644 index 00000000000000..b096a69a6071a0 --- /dev/null +++ b/test/parallel/test-whatwg-url-searchparams-get.js @@ -0,0 +1,55 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const URLSearchParams = require('url').URLSearchParams; +const { test, assert_equals, assert_true } = require('../common/wpt'); + +/* The following tests are copied from WPT. Modifications to them should be + upstreamed first. Refs: + https://github.com/w3c/web-platform-tests/blob/8791bed/url/urlsearchparams-get.html + License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html +*/ +/* eslint-disable */ +test(function() { + var params = new URLSearchParams('a=b&c=d'); + assert_equals(params.get('a'), 'b'); + assert_equals(params.get('c'), 'd'); + assert_equals(params.get('e'), null); + params = new URLSearchParams('a=b&c=d&a=e'); + assert_equals(params.get('a'), 'b'); + params = new URLSearchParams('=b&c=d'); + assert_equals(params.get(''), 'b'); + params = new URLSearchParams('a=&c=d&a=e'); + assert_equals(params.get('a'), ''); +}, 'Get basics'); + +test(function() { + var params = new URLSearchParams('first=second&third&&'); + assert_true(params != null, 'constructor returned non-null value.'); + assert_true(params.has('first'), 'Search params object has name "first"'); + assert_equals(params.get('first'), 'second', 'Search params object has name "first" with value "second"'); + assert_equals(params.get('third'), '', 'Search params object has name "third" with the empty value.'); + assert_equals(params.get('fourth'), null, 'Search params object has no "fourth" name and value.'); +}, 'More get() basics'); +/* eslint-enable */ + +// Tests below are not from WPT. +{ + const params = new URLSearchParams(); + assert.throws(() => { + params.get.call(undefined); + }, /^TypeError: Value of "this" must be of type URLSearchParams$/); + assert.throws(() => { + params.get(); + }, /^TypeError: The "name" argument must be specified$/); + + const obj = { + toString() { throw new Error('toString'); }, + valueOf() { throw new Error('valueOf'); } + }; + const sym = Symbol(); + assert.throws(() => params.get(obj), /^Error: toString$/); + assert.throws(() => params.get(sym), + /^TypeError: Cannot convert a Symbol value to a string$/); +} diff --git a/test/parallel/test-whatwg-url-searchparams-getall.js b/test/parallel/test-whatwg-url-searchparams-getall.js new file mode 100644 index 00000000000000..acf5108459cf61 --- /dev/null +++ b/test/parallel/test-whatwg-url-searchparams-getall.js @@ -0,0 +1,60 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const URLSearchParams = require('url').URLSearchParams; +const { test, assert_equals, assert_true, assert_array_equals } = + require('../common/wpt'); + +/* The following tests are copied from WPT. Modifications to them should be + upstreamed first. Refs: + https://github.com/w3c/web-platform-tests/blob/8791bed/url/urlsearchparams-getall.html + License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html +*/ +/* eslint-disable */ +test(function() { + var params = new URLSearchParams('a=b&c=d'); + assert_array_equals(params.getAll('a'), ['b']); + assert_array_equals(params.getAll('c'), ['d']); + assert_array_equals(params.getAll('e'), []); + params = new URLSearchParams('a=b&c=d&a=e'); + assert_array_equals(params.getAll('a'), ['b', 'e']); + params = new URLSearchParams('=b&c=d'); + assert_array_equals(params.getAll(''), ['b']); + params = new URLSearchParams('a=&c=d&a=e'); + assert_array_equals(params.getAll('a'), ['', 'e']); +}, 'getAll() basics'); + +test(function() { + var params = new URLSearchParams('a=1&a=2&a=3&a'); + assert_true(params.has('a'), 'Search params object has name "a"'); + var matches = params.getAll('a'); + assert_true(matches && matches.length == 4, 'Search params object has values for name "a"'); + assert_array_equals(matches, ['1', '2', '3', ''], 'Search params object has expected name "a" values'); + params.set('a', 'one'); + assert_equals(params.get('a'), 'one', 'Search params object has name "a" with value "one"'); + var matches = params.getAll('a'); + assert_true(matches && matches.length == 1, 'Search params object has values for name "a"'); + assert_array_equals(matches, ['one'], 'Search params object has expected name "a" values'); +}, 'getAll() multiples'); +/* eslint-enable */ + +// Tests below are not from WPT. +{ + const params = new URLSearchParams(); + assert.throws(() => { + params.getAll.call(undefined); + }, /^TypeError: Value of "this" must be of type URLSearchParams$/); + assert.throws(() => { + params.getAll(); + }, /^TypeError: The "name" argument must be specified$/); + + const obj = { + toString() { throw new Error('toString'); }, + valueOf() { throw new Error('valueOf'); } + }; + const sym = Symbol(); + assert.throws(() => params.getAll(obj), /^Error: toString$/); + assert.throws(() => params.getAll(sym), + /^TypeError: Cannot convert a Symbol value to a string$/); +} diff --git a/test/parallel/test-whatwg-url-searchparams-has.js b/test/parallel/test-whatwg-url-searchparams-has.js new file mode 100644 index 00000000000000..0fdd88af64c5af --- /dev/null +++ b/test/parallel/test-whatwg-url-searchparams-has.js @@ -0,0 +1,58 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const URLSearchParams = require('url').URLSearchParams; +const { test, assert_false, assert_true } = require('../common/wpt'); + +/* The following tests are copied from WPT. Modifications to them should be + upstreamed first. Refs: + https://github.com/w3c/web-platform-tests/blob/8791bed/url/urlsearchparams-has.html + License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html +*/ +/* eslint-disable */ +test(function() { + var params = new URLSearchParams('a=b&c=d'); + assert_true(params.has('a')); + assert_true(params.has('c')); + assert_false(params.has('e')); + params = new URLSearchParams('a=b&c=d&a=e'); + assert_true(params.has('a')); + params = new URLSearchParams('=b&c=d'); + assert_true(params.has('')); + params = new URLSearchParams('null=a'); + assert_true(params.has(null)); +}, 'Has basics'); + +test(function() { + var params = new URLSearchParams('a=b&c=d&&'); + params.append('first', 1); + params.append('first', 2); + assert_true(params.has('a'), 'Search params object has name "a"'); + assert_true(params.has('c'), 'Search params object has name "c"'); + assert_true(params.has('first'), 'Search params object has name "first"'); + assert_false(params.has('d'), 'Search params object has no name "d"'); + params.delete('first'); + assert_false(params.has('first'), 'Search params object has no name "first"'); +}, 'has() following delete()'); +/* eslint-enable */ + +// Tests below are not from WPT. +{ + const params = new URLSearchParams(); + assert.throws(() => { + params.has.call(undefined); + }, /^TypeError: Value of "this" must be of type URLSearchParams$/); + assert.throws(() => { + params.has(); + }, /^TypeError: The "name" argument must be specified$/); + + const obj = { + toString() { throw new Error('toString'); }, + valueOf() { throw new Error('valueOf'); } + }; + const sym = Symbol(); + assert.throws(() => params.has(obj), /^Error: toString$/); + assert.throws(() => params.has(sym), + /^TypeError: Cannot convert a Symbol value to a string$/); +} diff --git a/test/parallel/test-whatwg-url-searchparams-inspect.js b/test/parallel/test-whatwg-url-searchparams-inspect.js new file mode 100644 index 00000000000000..163fa185ede58d --- /dev/null +++ b/test/parallel/test-whatwg-url-searchparams-inspect.js @@ -0,0 +1,29 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const util = require('util'); +const URLSearchParams = require('url').URLSearchParams; + +// Tests below are not from WPT. +const sp = new URLSearchParams('?a=a&b=b&b=c'); +assert.strictEqual(util.inspect(sp), + "URLSearchParams { 'a' => 'a', 'b' => 'b', 'b' => 'c' }"); +assert.strictEqual(util.inspect(sp.keys()), + "URLSearchParamsIterator { 'a', 'b', 'b' }"); +assert.strictEqual(util.inspect(sp.values()), + "URLSearchParamsIterator { 'a', 'b', 'c' }"); +assert.strictEqual(util.inspect(sp.keys(), { breakLength: 1 }), + "URLSearchParamsIterator {\n 'a',\n 'b',\n 'b' }"); + +const iterator = sp.entries(); +assert.strictEqual(util.inspect(iterator), + "URLSearchParamsIterator { [ 'a', 'a' ], [ 'b', 'b' ], " + + "[ 'b', 'c' ] }"); +iterator.next(); +assert.strictEqual(util.inspect(iterator), + "URLSearchParamsIterator { [ 'b', 'b' ], [ 'b', 'c' ] }"); +iterator.next(); +iterator.next(); +assert.strictEqual(util.inspect(iterator), + 'URLSearchParamsIterator { }'); diff --git a/test/parallel/test-whatwg-url-searchparams-keys.js b/test/parallel/test-whatwg-url-searchparams-keys.js new file mode 100644 index 00000000000000..af044a260874ac --- /dev/null +++ b/test/parallel/test-whatwg-url-searchparams-keys.js @@ -0,0 +1,35 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const URLSearchParams = require('url').URLSearchParams; + +// Tests below are not from WPT. +const params = new URLSearchParams('a=b&c=d'); +const keys = params.keys(); + +assert.strictEqual(typeof keys[Symbol.iterator], 'function'); +assert.strictEqual(keys[Symbol.iterator](), keys); +assert.deepStrictEqual(keys.next(), { + value: 'a', + done: false +}); +assert.deepStrictEqual(keys.next(), { + value: 'c', + done: false +}); +assert.deepStrictEqual(keys.next(), { + value: undefined, + done: true +}); +assert.deepStrictEqual(keys.next(), { + value: undefined, + done: true +}); + +assert.throws(() => { + keys.next.call(undefined); +}, /^TypeError: Value of "this" must be of type URLSearchParamsIterator$/); +assert.throws(() => { + params.keys.call(undefined); +}, /^TypeError: Value of "this" must be of type URLSearchParams$/); diff --git a/test/parallel/test-whatwg-url-searchparams-set.js b/test/parallel/test-whatwg-url-searchparams-set.js new file mode 100644 index 00000000000000..8a6c31bbe66487 --- /dev/null +++ b/test/parallel/test-whatwg-url-searchparams-set.js @@ -0,0 +1,59 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const URLSearchParams = require('url').URLSearchParams; +const { test, assert_equals, assert_true } = require('../common/wpt'); + +/* The following tests are copied from WPT. Modifications to them should be + upstreamed first. Refs: + https://github.com/w3c/web-platform-tests/blob/8791bed/url/urlsearchparams-set.html + License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html +*/ +/* eslint-disable */ +test(function() { + var params = new URLSearchParams('a=b&c=d'); + params.set('a', 'B'); + assert_equals(params + '', 'a=B&c=d'); + params = new URLSearchParams('a=b&c=d&a=e'); + params.set('a', 'B'); + assert_equals(params + '', 'a=B&c=d') + params.set('e', 'f'); + assert_equals(params + '', 'a=B&c=d&e=f') +}, 'Set basics'); + +test(function() { + var params = new URLSearchParams('a=1&a=2&a=3'); + assert_true(params.has('a'), 'Search params object has name "a"'); + assert_equals(params.get('a'), '1', 'Search params object has name "a" with value "1"'); + params.set('first', 4); + assert_true(params.has('a'), 'Search params object has name "a"'); + assert_equals(params.get('a'), '1', 'Search params object has name "a" with value "1"'); + params.set('a', 4); + assert_true(params.has('a'), 'Search params object has name "a"'); + assert_equals(params.get('a'), '4', 'Search params object has name "a" with value "4"'); +}, 'URLSearchParams.set'); +/* eslint-enable */ + +// Tests below are not from WPT. +{ + const params = new URLSearchParams(); + assert.throws(() => { + params.set.call(undefined); + }, /^TypeError: Value of "this" must be of type URLSearchParams$/); + assert.throws(() => { + params.set('a'); + }, /^TypeError: The "name" and "value" arguments must be specified$/); + + const obj = { + toString() { throw new Error('toString'); }, + valueOf() { throw new Error('valueOf'); } + }; + const sym = Symbol(); + assert.throws(() => params.append(obj, 'b'), /^Error: toString$/); + assert.throws(() => params.append('a', obj), /^Error: toString$/); + assert.throws(() => params.append(sym, 'b'), + /^TypeError: Cannot convert a Symbol value to a string$/); + assert.throws(() => params.append('a', sym), + /^TypeError: Cannot convert a Symbol value to a string$/); +} diff --git a/test/parallel/test-whatwg-url-searchparams-sort.js b/test/parallel/test-whatwg-url-searchparams-sort.js new file mode 100644 index 00000000000000..1122f08dcc0434 --- /dev/null +++ b/test/parallel/test-whatwg-url-searchparams-sort.js @@ -0,0 +1,105 @@ +'use strict'; + +require('../common'); +const { URL, URLSearchParams } = require('url'); +const { test, assert_equals, assert_array_equals } = require('../common/wpt'); + +/* The following tests are copied from WPT. Modifications to them should be + upstreamed first. Refs: + https://github.com/w3c/web-platform-tests/blob/70a0898763/url/urlsearchparams-sort.html + License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html +*/ +/* eslint-disable */ +[ + { + "input": "z=b&a=b&z=a&a=a", + "output": [["a", "b"], ["a", "a"], ["z", "b"], ["z", "a"]] + }, + { + "input": "\uFFFD=x&\uFFFC&\uFFFD=a", + "output": [["\uFFFC", ""], ["\uFFFD", "x"], ["\uFFFD", "a"]] + }, + { + "input": "ffi&🌈", // 🌈 > code point, but < code unit because two code units + "output": [["🌈", ""], ["ffi", ""]] + }, + { + "input": "é&e\uFFFD&e\u0301", + "output": [["e\u0301", ""], ["e\uFFFD", ""], ["é", ""]] + }, + { + "input": "z=z&a=a&z=y&a=b&z=x&a=c&z=w&a=d&z=v&a=e&z=u&a=f&z=t&a=g", + "output": [["a", "a"], ["a", "b"], ["a", "c"], ["a", "d"], ["a", "e"], ["a", "f"], ["a", "g"], ["z", "z"], ["z", "y"], ["z", "x"], ["z", "w"], ["z", "v"], ["z", "u"], ["z", "t"]] + } +].forEach((val) => { + test(() => { + let params = new URLSearchParams(val.input), + i = 0 + params.sort() + for(let param of params) { + assert_array_equals(param, val.output[i]) + i++ + } + }, `Parse and sort: ${val.input}`) + + test(() => { + let url = new URL(`?${val.input}`, "https://example/") + url.searchParams.sort() + let params = new URLSearchParams(url.search), + i = 0 + for(let param of params) { + assert_array_equals(param, val.output[i]) + i++ + } + }, `URL parse and sort: ${val.input}`) +}) + +test(function() { + const url = new URL("http://example.com/?") + url.searchParams.sort() + assert_equals(url.href, "http://example.com/") + assert_equals(url.search, "") +}, "Sorting non-existent params removes ? from URL") +/* eslint-enable */ + +// Tests below are not from WPT. + +// Test bottom-up iterative stable merge sort +const tests = [{ input: '', output: [] }]; +const pairs = []; +for (let i = 10; i < 100; i++) { + pairs.push([`a${i}`, 'b']); + tests[0].output.push([`a${i}`, 'b']); +} +tests[0].input = pairs.sort(() => Math.random() > 0.5) + .map((pair) => pair.join('=')).join('&'); + +tests.push( + { + 'input': 'z=a&=b&c=d', + 'output': [['', 'b'], ['c', 'd'], ['z', 'a']] + } +); + +tests.forEach((val) => { + test(() => { + const params = new URLSearchParams(val.input); + let i = 0; + params.sort(); + for (const param of params) { + assert_array_equals(param, val.output[i]); + i++; + } + }, `Parse and sort: ${val.input}`); + + test(() => { + const url = new URL(`?${val.input}`, 'https://example/'); + url.searchParams.sort(); + const params = new URLSearchParams(url.search); + let i = 0; + for (const param of params) { + assert_array_equals(param, val.output[i]); + i++; + } + }, `URL parse and sort: ${val.input}`); +}); diff --git a/test/parallel/test-whatwg-url-searchparams-stringifier.js b/test/parallel/test-whatwg-url-searchparams-stringifier.js new file mode 100644 index 00000000000000..c355a2c9a9c29c --- /dev/null +++ b/test/parallel/test-whatwg-url-searchparams-stringifier.js @@ -0,0 +1,132 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const URLSearchParams = require('url').URLSearchParams; +const { test, assert_equals } = require('../common/wpt'); + +/* The following tests are copied from WPT. Modifications to them should be + upstreamed first. Refs: + https://github.com/w3c/web-platform-tests/blob/8791bed/url/urlsearchparams-stringifier.html + License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html +*/ +/* eslint-disable */ +test(function() { + var params = new URLSearchParams(); + params.append('a', 'b c'); + assert_equals(params + '', 'a=b+c'); + params.delete('a'); + params.append('a b', 'c'); + assert_equals(params + '', 'a+b=c'); +}, 'Serialize space'); + +test(function() { + var params = new URLSearchParams(); + params.append('a', ''); + assert_equals(params + '', 'a='); + params.append('a', ''); + assert_equals(params + '', 'a=&a='); + params.append('', 'b'); + assert_equals(params + '', 'a=&a=&=b'); + params.append('', ''); + assert_equals(params + '', 'a=&a=&=b&='); + params.append('', ''); + assert_equals(params + '', 'a=&a=&=b&=&='); +}, 'Serialize empty value'); + +test(function() { + var params = new URLSearchParams(); + params.append('', 'b'); + assert_equals(params + '', '=b'); + params.append('', 'b'); + assert_equals(params + '', '=b&=b'); +}, 'Serialize empty name'); + +test(function() { + var params = new URLSearchParams(); + params.append('', ''); + assert_equals(params + '', '='); + params.append('', ''); + assert_equals(params + '', '=&='); +}, 'Serialize empty name and value'); + +test(function() { + var params = new URLSearchParams(); + params.append('a', 'b+c'); + assert_equals(params + '', 'a=b%2Bc'); + params.delete('a'); + params.append('a+b', 'c'); + assert_equals(params + '', 'a%2Bb=c'); +}, 'Serialize +'); + +test(function() { + var params = new URLSearchParams(); + params.append('=', 'a'); + assert_equals(params + '', '%3D=a'); + params.append('b', '='); + assert_equals(params + '', '%3D=a&b=%3D'); +}, 'Serialize ='); + +test(function() { + var params = new URLSearchParams(); + params.append('&', 'a'); + assert_equals(params + '', '%26=a'); + params.append('b', '&'); + assert_equals(params + '', '%26=a&b=%26'); +}, 'Serialize &'); + +test(function() { + var params = new URLSearchParams(); + params.append('a', '*-._'); + assert_equals(params + '', 'a=*-._'); + params.delete('a'); + params.append('*-._', 'c'); + assert_equals(params + '', '*-._=c'); +}, 'Serialize *-._'); + +test(function() { + var params = new URLSearchParams(); + params.append('a', 'b%c'); + assert_equals(params + '', 'a=b%25c'); + params.delete('a'); + params.append('a%b', 'c'); + assert_equals(params + '', 'a%25b=c'); +}, 'Serialize %'); + +test(function() { + var params = new URLSearchParams(); + params.append('a', 'b\0c'); + assert_equals(params + '', 'a=b%00c'); + params.delete('a'); + params.append('a\0b', 'c'); + assert_equals(params + '', 'a%00b=c'); +}, 'Serialize \\0'); + +test(function() { + var params = new URLSearchParams(); + params.append('a', 'b\uD83D\uDCA9c'); + assert_equals(params + '', 'a=b%F0%9F%92%A9c'); + params.delete('a'); + params.append('a\uD83D\uDCA9b', 'c'); + assert_equals(params + '', 'a%F0%9F%92%A9b=c'); +}, 'Serialize \uD83D\uDCA9'); // Unicode Character 'PILE OF POO' (U+1F4A9) + +test(function() { + var params; + params = new URLSearchParams('a=b&c=d&&e&&'); + assert_equals(params.toString(), 'a=b&c=d&e='); + params = new URLSearchParams('a = b &a=b&c=d%20'); + assert_equals(params.toString(), 'a+=+b+&a=b&c=d+'); + // The lone '=' _does_ survive the roundtrip. + params = new URLSearchParams('a=&a=b'); + assert_equals(params.toString(), 'a=&a=b'); +}, 'URLSearchParams.toString'); +/* eslint-enable */ + +// Tests below are not from WPT. +{ + const params = new URLSearchParams(); + assert.throws(() => { + params.toString.call(undefined); + }, /^TypeError: Value of "this" must be of type URLSearchParams$/); +} diff --git a/test/parallel/test-whatwg-url-searchparams-values.js b/test/parallel/test-whatwg-url-searchparams-values.js new file mode 100644 index 00000000000000..2775231b8bda5d --- /dev/null +++ b/test/parallel/test-whatwg-url-searchparams-values.js @@ -0,0 +1,35 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const URLSearchParams = require('url').URLSearchParams; + +// Tests below are not from WPT. +const params = new URLSearchParams('a=b&c=d'); +const values = params.values(); + +assert.strictEqual(typeof values[Symbol.iterator], 'function'); +assert.strictEqual(values[Symbol.iterator](), values); +assert.deepStrictEqual(values.next(), { + value: 'b', + done: false +}); +assert.deepStrictEqual(values.next(), { + value: 'd', + done: false +}); +assert.deepStrictEqual(values.next(), { + value: undefined, + done: true +}); +assert.deepStrictEqual(values.next(), { + value: undefined, + done: true +}); + +assert.throws(() => { + values.next.call(undefined); +}, /^TypeError: Value of "this" must be of type URLSearchParamsIterator$/); +assert.throws(() => { + params.values.call(undefined); +}, /^TypeError: Value of "this" must be of type URLSearchParams$/); diff --git a/test/parallel/test-whatwg-url-searchparams.js b/test/parallel/test-whatwg-url-searchparams.js new file mode 100644 index 00000000000000..b6861273e71a0e --- /dev/null +++ b/test/parallel/test-whatwg-url-searchparams.js @@ -0,0 +1,106 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const { URL, URLSearchParams } = require('url'); +const fixtures = require('../common/fixtures'); + +// Tests below are not from WPT. +const serialized = 'a=a&a=1&a=true&a=undefined&a=null&a=%EF%BF%BD' + + '&a=%EF%BF%BD&a=%F0%9F%98%80&a=%EF%BF%BD%EF%BF%BD' + + '&a=%5Bobject+Object%5D'; +const values = ['a', 1, true, undefined, null, '\uD83D', '\uDE00', + '\uD83D\uDE00', '\uDE00\uD83D', {}]; +const normalizedValues = ['a', '1', 'true', 'undefined', 'null', '\uFFFD', + '\uFFFD', '\uD83D\uDE00', '\uFFFD\uFFFD', + '[object Object]']; + +const m = new URL('http://example.org'); +const sp = m.searchParams; + +assert(sp); +assert.strictEqual(sp.toString(), ''); +assert.strictEqual(m.search, ''); + +assert(!sp.has('a')); +values.forEach((i) => sp.set('a', i)); +assert(sp.has('a')); +assert.strictEqual(sp.get('a'), '[object Object]'); +sp.delete('a'); +assert(!sp.has('a')); + +m.search = ''; +assert.strictEqual(sp.toString(), ''); + +values.forEach((i) => sp.append('a', i)); +assert(sp.has('a')); +assert.strictEqual(sp.getAll('a').length, values.length); +assert.strictEqual(sp.get('a'), 'a'); + +assert.strictEqual(sp.toString(), serialized); + +assert.strictEqual(m.search, `?${serialized}`); + +assert.strictEqual(sp[Symbol.iterator], sp.entries); + +let key, val; +let n = 0; +for ([key, val] of sp) { + assert.strictEqual(key, 'a', n); + assert.strictEqual(val, normalizedValues[n], n); + n++; +} +n = 0; +for (key of sp.keys()) { + assert.strictEqual(key, 'a', n); + n++; +} +n = 0; +for (val of sp.values()) { + assert.strictEqual(val, normalizedValues[n], n); + n++; +} +n = 0; +sp.forEach(function(val, key, obj) { + assert.strictEqual(this, undefined, n); + assert.strictEqual(key, 'a', n); + assert.strictEqual(val, normalizedValues[n], n); + assert.strictEqual(obj, sp, n); + n++; +}); +sp.forEach(function() { + assert.strictEqual(this, m); +}, m); + +{ + const callbackErr = /^TypeError: Callback must be a function$/; + assert.throws(() => sp.forEach(), callbackErr); + assert.throws(() => sp.forEach(1), callbackErr); +} + +m.search = '?a=a&b=b'; +assert.strictEqual(sp.toString(), 'a=a&b=b'); + +const tests = require(fixtures.path('url-searchparams.js')); + +for (const [input, expected, parsed] of tests) { + if (input[0] !== '?') { + const sp = new URLSearchParams(input); + assert.strictEqual(String(sp), expected); + assert.deepStrictEqual(Array.from(sp), parsed); + + m.search = input; + assert.strictEqual(String(m.searchParams), expected); + assert.deepStrictEqual(Array.from(m.searchParams), parsed); + } + + { + const sp = new URLSearchParams(`?${input}`); + assert.strictEqual(String(sp), expected); + assert.deepStrictEqual(Array.from(sp), parsed); + + m.search = `?${input}`; + assert.strictEqual(String(m.searchParams), expected); + assert.deepStrictEqual(Array.from(m.searchParams), parsed); + } +} diff --git a/test/parallel/test-whatwg-url-setters.js b/test/parallel/test-whatwg-url-setters.js new file mode 100644 index 00000000000000..9a25de59e7e55f --- /dev/null +++ b/test/parallel/test-whatwg-url-setters.js @@ -0,0 +1,126 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasIntl) { + // A handful of the tests fail when ICU is not included. + common.skip('missing Intl'); +} + +const assert = require('assert'); +const URL = require('url').URL; +const { test, assert_equals } = require('../common/wpt'); +const fixtures = require('../common/fixtures'); + +const additionalTestCases = + require(fixtures.path('url-setter-tests-additional.js')); + +const request = { + response: require(fixtures.path('url-setter-tests')) +}; + +/* The following tests are copied from WPT. Modifications to them should be + upstreamed first. Refs: + https://github.com/w3c/web-platform-tests/blob/8791bed/url/url-setters.html + License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html +*/ +/* eslint-disable */ +function startURLSettersTests() { +// var setup = async_test("Loading data…") +// setup.step(function() { +// var request = new XMLHttpRequest() +// request.open("GET", "setters_tests.json") +// request.send() +// request.responseType = "json" +// request.onload = setup.step_func(function() { + runURLSettersTests(request.response) +// setup.done() +// }) +// }) +} + +function runURLSettersTests(all_test_cases) { + for (var attribute_to_be_set in all_test_cases) { + if (attribute_to_be_set == "comment") { + continue; + } + var test_cases = all_test_cases[attribute_to_be_set]; + for(var i = 0, l = test_cases.length; i < l; i++) { + var test_case = test_cases[i]; + var name = `Setting <${test_case.href}>.${attribute_to_be_set}` + + ` = '${test_case.new_value}'`; + if ("comment" in test_case) { + name += ` ${test_case.comment}`; + } + test(function() { + var url = new URL(test_case.href); + url[attribute_to_be_set] = test_case.new_value; + for (var attribute in test_case.expected) { + assert_equals(url[attribute], test_case.expected[attribute]) + } + }, `URL: ${name}`); + // test(function() { + // var url = document.createElement("a"); + // url.href = test_case.href; + // url[attribute_to_be_set] = test_case.new_value; + // for (var attribute in test_case.expected) { + // assert_equals(url[attribute], test_case.expected[attribute]) + // } + // }, ": " + name) + // test(function() { + // var url = document.createElement("area"); + // url.href = test_case.href; + // url[attribute_to_be_set] = test_case.new_value; + // for (var attribute in test_case.expected) { + // assert_equals(url[attribute], test_case.expected[attribute]) + // } + // }, ": " + name) + } + } +} + +startURLSettersTests() +/* eslint-enable */ + +// Tests below are not from WPT. + +{ + for (const attributeToBeSet in additionalTestCases) { + if (attributeToBeSet === 'comment') { + continue; + } + const testCases = additionalTestCases[attributeToBeSet]; + for (const testCase of testCases) { + let name = `Setting <${testCase.href}>.${attributeToBeSet}` + + ` = "${testCase.new_value}"`; + if ('comment' in testCase) { + name += ` ${testCase.comment}`; + } + test(function() { + const url = new URL(testCase.href); + url[attributeToBeSet] = testCase.new_value; + for (const attribute in testCase.expected) { + assert_equals(url[attribute], testCase.expected[attribute]); + } + }, `URL: ${name}`); + } + } +} + +{ + const url = new URL('http://example.com/'); + const obj = { + toString() { throw new Error('toString'); }, + valueOf() { throw new Error('valueOf'); } + }; + const sym = Symbol(); + for (const name of Reflect.ownKeys(Object.getPrototypeOf(url))) { + if (Object.getOwnPropertyDescriptor(Object.getPrototypeOf(url), name).set) { + assert.throws(() => url[name] = obj, + /^Error: toString$/, + `url.${name} = { toString() { throw ... } }`); + assert.throws(() => url[name] = sym, + /^TypeError: Cannot convert a Symbol value to a string$/, + `url.${name} = ${String(sym)}`); + } + } +} diff --git a/test/parallel/test-whatwg-url-toascii.js b/test/parallel/test-whatwg-url-toascii.js new file mode 100644 index 00000000000000..c85b092c1d250c --- /dev/null +++ b/test/parallel/test-whatwg-url-toascii.js @@ -0,0 +1,85 @@ +'use strict'; +const common = require('../common'); +if (!common.hasIntl) { + // A handful of the tests fail when ICU is not included. + common.skip('missing Intl'); +} + +const fixtures = require('../common/fixtures'); +const { URL } = require('url'); +const { test, assert_equals, assert_throws } = require('../common/wpt'); + +const request = { + response: require(fixtures.path('url-toascii')) +}; + +/* The following tests are copied from WPT. Modifications to them should be + upstreamed first. Refs: + https://github.com/w3c/web-platform-tests/blob/4839a0a804/url/toascii.window.js + License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html +*/ +/* eslint-disable */ +// async_test(t => { +// const request = new XMLHttpRequest() +// request.open("GET", "toascii.json") +// request.send() +// request.responseType = "json" +// request.onload = t.step_func_done(() => { + runTests(request.response) +// }) +// }, "Loading data…") + +function makeURL(type, input) { + input = "https://" + input + "/x" + if(type === "url") { + return new URL(input) + } else { + const url = document.createElement(type) + url.href = input + return url + } +} + +function runTests(tests) { + for(var i = 0, l = tests.length; i < l; i++) { + let hostTest = tests[i] + if (typeof hostTest === "string") { + continue // skip comments + } + const typeName = { "url": "URL", "a": "", "area": "" } + // ;["url", "a", "area"].forEach((type) => { + ;["url"].forEach((type) => { + test(() => { + if(hostTest.output !== null) { + const url = makeURL("url", hostTest.input) + assert_equals(url.host, hostTest.output) + assert_equals(url.hostname, hostTest.output) + assert_equals(url.pathname, "/x") + assert_equals(url.href, "https://" + hostTest.output + "/x") + } else { + if(type === "url") { + assert_throws(new TypeError, () => makeURL("url", hostTest.input)) + } else { + const url = makeURL(type, hostTest.input) + assert_equals(url.host, "") + assert_equals(url.hostname, "") + assert_equals(url.pathname, "") + assert_equals(url.href, "https://" + hostTest.input + "/x") + } + } + }, hostTest.input + " (using " + typeName[type] + ")") + ;["host", "hostname"].forEach((val) => { + test(() => { + const url = makeURL(type, "x") + url[val] = hostTest.input + if(hostTest.output !== null) { + assert_equals(url[val], hostTest.output) + } else { + assert_equals(url[val], "x") + } + }, hostTest.input + " (using " + typeName[type] + "." + val + ")") + }) + }) + } +} +/* eslint-enable */ diff --git a/test/parallel/test-whatwg-url-tojson.js b/test/parallel/test-whatwg-url-tojson.js new file mode 100644 index 00000000000000..8e9a30c7e017e4 --- /dev/null +++ b/test/parallel/test-whatwg-url-tojson.js @@ -0,0 +1,17 @@ +'use strict'; + +require('../common'); +const URL = require('url').URL; +const { test, assert_equals } = require('../common/wpt'); + +/* The following tests are copied from WPT. Modifications to them should be + upstreamed first. Refs: + https://github.com/w3c/web-platform-tests/blob/02585db/url/url-tojson.html + License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html +*/ +/* eslint-disable */ +test(() => { + const a = new URL("https://example.com/") + assert_equals(JSON.stringify(a), "\"https://example.com/\"") +}) +/* eslint-enable */ diff --git a/test/parallel/test-whatwg-url-tostringtag.js b/test/parallel/test-whatwg-url-tostringtag.js new file mode 100644 index 00000000000000..689056fd238dda --- /dev/null +++ b/test/parallel/test-whatwg-url-tostringtag.js @@ -0,0 +1,32 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const URL = require('url').URL; + +// Tests below are not from WPT. +const toString = Object.prototype.toString; + +const url = new URL('http://example.org'); +const sp = url.searchParams; +const spIterator = sp.entries(); + +const test = [ + [url, 'URL'], + [sp, 'URLSearchParams'], + [spIterator, 'URLSearchParamsIterator'], + // Web IDL spec says we have to return 'URLPrototype', but it is too + // expensive to implement; therefore, use Chrome's behavior for now, until + // spec is changed. + [Object.getPrototypeOf(url), 'URL'], + [Object.getPrototypeOf(sp), 'URLSearchParams'], + [Object.getPrototypeOf(spIterator), 'URLSearchParamsIterator'], +]; + +test.forEach(([obj, expected]) => { + assert.strictEqual(obj[Symbol.toStringTag], expected, + `${obj[Symbol.toStringTag]} !== ${expected}`); + const str = toString.call(obj); + assert.strictEqual(str, `[object ${expected}]`, + `${str} !== [object ${expected}]`); +}); diff --git a/tools/doc/type-parser.js b/tools/doc/type-parser.js index c2bca80641152e..ee50d48f61d45e 100644 --- a/tools/doc/type-parser.js +++ b/tools/doc/type-parser.js @@ -38,6 +38,8 @@ const typeMap = { 'http.IncomingMessage': 'http.html#http_class_http_incomingmessage', 'http.Server': 'http.html#http_class_http_server', 'http.ServerResponse': 'http.html#http_class_http_serverresponse', + 'URL': 'url.html#url_the_whatwg_url_api', + 'URLSearchParams': 'url.html#url_class_urlsearchparams' }; const arrayPart = /(?:\[])+$/;