Browse Source

url: update WHATWG URL API to latest spec

- Update to spec
  - Add opaque hosts
  - File state did not correctly deal with lack of base URL
  - Cleanup API for file and non-special URLs
  - Allow % and IPv6 addresses in non-special URL hosts
  - Use specific names for percent-encode sets
  - Add empty host concept for file and non-special URLs
  - Clarify IPv6 serializer
- Fix existing mistakes
  - Add missing ':' to forbidden host code point list.
  - Correct IPv4 parser empty label behavior
- Maintain type equivalence in URLContext with spec
  - scheme, username, and password should always be strings
  - host, port, query, and fragment may be strings or null
  - Align scheme state more closely with the spec
  - Make sure the `special` variable is always synced with
    URL_FLAG_SPECIAL.

PR-URL: https://github.com/nodejs/node/pull/12523
Fixes: https://github.com/nodejs/node/issues/10608
Fixes: https://github.com/nodejs/node/issues/10634
Refs: https://github.com/whatwg/url/pull/185
Refs: https://github.com/whatwg/url/pull/225
Refs: https://github.com/whatwg/url/pull/224
Refs: https://github.com/whatwg/url/pull/218
Refs: https://github.com/whatwg/url/pull/243
Refs: https://github.com/whatwg/url/pull/260
Refs: https://github.com/whatwg/url/pull/268
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Daijiro Wachi <daijiro.wachi@gmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
v6
Timothy Gu 8 years ago
parent
commit
b2870a4f8c
  1. 28
      doc/api/url.md
  2. 177
      lib/internal/url.js
  3. 245
      src/node_url.cc
  4. 466
      test/fixtures/url-setter-tests.js
  5. 584
      test/fixtures/url-tests.js

28
doc/api/url.md

@ -1053,23 +1053,25 @@ located within the structure of the URL. The WHATWG URL Standard uses a more
selective and fine grained approach to selecting encoded characters than that selective and fine grained approach to selecting encoded characters than that
used by the older [`url.parse()`][] and [`url.format()`][] methods. used by the older [`url.parse()`][] and [`url.format()`][] methods.
The WHATWG algorithm defines three "encoding sets" that describe ranges of The WHATWG algorithm defines three "percent-encode sets" that describe ranges
characters that must be percent-encoded: of characters that must be percent-encoded:
* The *simple encode set* includes code points in range U+0000 to U+001F * The *C0 control percent-encode set* includes code points in range U+0000 to
(inclusive) and all code points greater than U+007E. U+001F (inclusive) and all code points greater than U+007E.
* The *default encode set* includes the *simple encode set* and code points * The *path percent-encode set* includes the *C0 control percent-encode set*
U+0020, U+0022, U+0023, U+003C, U+003E, U+003F, U+0060, U+007B, and U+007D. 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 *default encode set* and code points * The *userinfo encode set* includes the *path percent-encode set* and code
U+002F, U+003A, U+003B, U+003D, U+0040, U+005B, U+005C, U+005D, U+005E, and points U+002F, U+003A, U+003B, U+003D, U+0040, U+005B, U+005C, U+005D,
U+007C. U+005E, and U+007C.
The *simple encode set* is used primary for URL fragments and certain specific The *userinfo percent-encode set* is used exclusively for username and
conditions for the path. The *userinfo encode set* is used specifically for passwords encoded within the URL. The *path percent-encode set* is used for the
username and passwords encoded within the URL. The *default encode set* is used path of most URLs. The *C0 control percent-encode set* is used for all
for all other cases. 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 When non-ASCII characters appear within a hostname, the hostname is encoded
using the [Punycode][] algorithm. Note, however, that a hostname *may* contain using the [Punycode][] algorithm. Note, however, that a hostname *may* contain

177
lib/internal/url.js

@ -8,6 +8,8 @@ const {
const binding = process.binding('url'); const binding = process.binding('url');
const context = Symbol('context'); const context = Symbol('context');
const cannotBeBase = Symbol('cannot-be-base'); const cannotBeBase = Symbol('cannot-be-base');
const cannotHaveUsernamePasswordPort =
Symbol('cannot-have-username-password-port');
const special = Symbol('special'); const special = Symbol('special');
const searchParams = Symbol('query'); const searchParams = Symbol('query');
const querystring = require('querystring'); const querystring = require('querystring');
@ -42,7 +44,7 @@ const kOpaqueOrigin = 'null';
// - https://html.spec.whatwg.org/multipage/browsers.html#ascii-serialisation-of-an-origin // - https://html.spec.whatwg.org/multipage/browsers.html#ascii-serialisation-of-an-origin
function serializeTupleOrigin(scheme, host, port, unicode = true) { function serializeTupleOrigin(scheme, host, port, unicode = true) {
const unicodeHost = unicode ? domainToUnicode(host) : host; const unicodeHost = unicode ? domainToUnicode(host) : host;
return `${scheme}//${unicodeHost}${port == null ? '' : `:${port}`}`; return `${scheme}//${unicodeHost}${port === null ? '' : `:${port}`}`;
} }
// This class provides the internal state of a URL object. An instance of this // This class provides the internal state of a URL object. An instance of this
@ -54,14 +56,14 @@ function serializeTupleOrigin(scheme, host, port, unicode = true) {
class URLContext { class URLContext {
constructor() { constructor() {
this.flags = 0; this.flags = 0;
this.scheme = undefined; this.scheme = ':';
this.username = undefined; this.username = '';
this.password = undefined; this.password = '';
this.host = undefined; this.host = null;
this.port = undefined; this.port = null;
this.path = []; this.path = [];
this.query = undefined; this.query = null;
this.fragment = undefined; this.fragment = null;
} }
} }
@ -70,10 +72,10 @@ function onParseComplete(flags, protocol, username, password,
var ctx = this[context]; var ctx = this[context];
ctx.flags = flags; ctx.flags = flags;
ctx.scheme = protocol; ctx.scheme = protocol;
ctx.username = username; ctx.username = (flags & binding.URL_FLAGS_HAS_USERNAME) !== 0 ? username : '';
ctx.password = password; ctx.password = (flags & binding.URL_FLAGS_HAS_PASSWORD) !== 0 ? password : '';
ctx.port = port; ctx.port = port;
ctx.path = path; ctx.path = (flags & binding.URL_FLAGS_HAS_PATH) !== 0 ? path : [];
ctx.query = query; ctx.query = query;
ctx.fragment = fragment; ctx.fragment = fragment;
ctx.host = host; ctx.host = host;
@ -101,52 +103,37 @@ function parse(url, input, base) {
function onParseProtocolComplete(flags, protocol, username, password, function onParseProtocolComplete(flags, protocol, username, password,
host, port, path, query, fragment) { host, port, path, query, fragment) {
const newIsSpecial = (flags & binding.URL_FLAGS_SPECIAL) !== 0;
const s = this[special];
const ctx = this[context]; const ctx = this[context];
if ((s && !newIsSpecial) || (!s && newIsSpecial)) { if ((flags & binding.URL_FLAGS_SPECIAL) !== 0) {
return;
}
if (protocol === 'file:' &&
(ctx.username || ctx.password || ctx.port !== undefined)) {
return;
}
if (ctx.scheme === 'file:' && !ctx.host) {
return;
}
if (newIsSpecial) {
ctx.flags |= binding.URL_FLAGS_SPECIAL; ctx.flags |= binding.URL_FLAGS_SPECIAL;
} else { } else {
ctx.flags &= ~binding.URL_FLAGS_SPECIAL; ctx.flags &= ~binding.URL_FLAGS_SPECIAL;
} }
if (protocol) {
ctx.scheme = protocol; ctx.scheme = protocol;
ctx.flags |= binding.URL_FLAGS_HAS_SCHEME;
} else {
ctx.flags &= ~binding.URL_FLAGS_HAS_SCHEME;
}
} }
function onParseHostComplete(flags, protocol, username, password, function onParseHostComplete(flags, protocol, username, password,
host, port, path, query, fragment) { host, port, path, query, fragment) {
const ctx = this[context]; const ctx = this[context];
if (host) { if ((flags & binding.URL_FLAGS_HAS_HOST) !== 0) {
ctx.host = host; ctx.host = host;
ctx.flags |= binding.URL_FLAGS_HAS_HOST; ctx.flags |= binding.URL_FLAGS_HAS_HOST;
} else { } else {
ctx.host = null;
ctx.flags &= ~binding.URL_FLAGS_HAS_HOST; ctx.flags &= ~binding.URL_FLAGS_HAS_HOST;
} }
if (port !== undefined) if (port !== null)
ctx.port = port; ctx.port = port;
} }
function onParseHostnameComplete(flags, protocol, username, password, function onParseHostnameComplete(flags, protocol, username, password,
host, port, path, query, fragment) { host, port, path, query, fragment) {
const ctx = this[context]; const ctx = this[context];
if (host) { if ((flags & binding.URL_FLAGS_HAS_HOST) !== 0) {
ctx.host = host; ctx.host = host;
ctx.flags |= binding.URL_FLAGS_HAS_HOST; ctx.flags |= binding.URL_FLAGS_HAS_HOST;
} else { } else {
ctx.host = null;
ctx.flags &= ~binding.URL_FLAGS_HAS_HOST; ctx.flags &= ~binding.URL_FLAGS_HAS_HOST;
} }
} }
@ -159,29 +146,29 @@ function onParsePortComplete(flags, protocol, username, password,
function onParsePathComplete(flags, protocol, username, password, function onParsePathComplete(flags, protocol, username, password,
host, port, path, query, fragment) { host, port, path, query, fragment) {
const ctx = this[context]; const ctx = this[context];
if (path) { if ((flags & binding.URL_FLAGS_HAS_PATH) !== 0) {
ctx.path = path; ctx.path = path;
ctx.flags |= binding.URL_FLAGS_HAS_PATH; ctx.flags |= binding.URL_FLAGS_HAS_PATH;
} else { } else {
ctx.path = [];
ctx.flags &= ~binding.URL_FLAGS_HAS_PATH; ctx.flags &= ~binding.URL_FLAGS_HAS_PATH;
} }
// The C++ binding may set host to empty string.
if ((flags & binding.URL_FLAGS_HAS_HOST) !== 0) {
ctx.host = host;
ctx.flags |= binding.URL_FLAGS_HAS_HOST;
}
} }
function onParseSearchComplete(flags, protocol, username, password, function onParseSearchComplete(flags, protocol, username, password,
host, port, path, query, fragment) { host, port, path, query, fragment) {
const ctx = this[context]; this[context].query = query;
ctx.query = query;
} }
function onParseHashComplete(flags, protocol, username, password, function onParseHashComplete(flags, protocol, username, password,
host, port, path, query, fragment) { host, port, path, query, fragment) {
const ctx = this[context]; this[context].fragment = fragment;
if (fragment) {
ctx.fragment = fragment;
ctx.flags |= binding.URL_FLAGS_HAS_FRAGMENT;
} else {
ctx.flags &= ~binding.URL_FLAGS_HAS_FRAGMENT;
}
} }
function getEligibleConstructor(obj) { function getEligibleConstructor(obj) {
@ -214,6 +201,14 @@ class URL {
return (this[context].flags & binding.URL_FLAGS_CANNOT_BE_BASE) !== 0; return (this[context].flags & binding.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) { [util.inspect.custom](depth, opts) {
if (this == null || if (this == null ||
Object.getPrototypeOf(this[context]) !== URLContext.prototype) { Object.getPrototypeOf(this[context]) !== URLContext.prototype) {
@ -235,7 +230,7 @@ class URL {
obj.origin = this.origin; obj.origin = this.origin;
obj.protocol = this.protocol; obj.protocol = this.protocol;
obj.username = this.username; obj.username = this.username;
obj.password = (opts.showHidden || ctx.password == null) ? obj.password = (opts.showHidden || ctx.password === '') ?
this.password : '--------'; this.password : '--------';
obj.host = this.host; obj.host = this.host;
obj.hostname = this.hostname; obj.hostname = this.hostname;
@ -270,14 +265,11 @@ Object.defineProperties(URL.prototype, {
auth: true auth: true
}, options); }, options);
const ctx = this[context]; const ctx = this[context];
var ret; var ret = ctx.scheme;
if (this.protocol) if (ctx.host !== null) {
ret = this.protocol;
if (ctx.host !== undefined) {
ret += '//'; ret += '//';
const has_username = typeof ctx.username === 'string'; const has_username = ctx.username !== '';
const has_password = typeof ctx.password === 'string' && const has_password = ctx.password !== '';
ctx.password !== '';
if (options.auth && (has_username || has_password)) { if (options.auth && (has_username || has_password)) {
if (has_username) if (has_username)
ret += ctx.username; ret += ctx.username;
@ -292,9 +284,9 @@ Object.defineProperties(URL.prototype, {
} }
if (this.pathname) if (this.pathname)
ret += this.pathname; ret += this.pathname;
if (options.search && typeof ctx.query === 'string') if (options.search && ctx.query !== null)
ret += `?${ctx.query}`; ret += `?${ctx.query}`;
if (options.fragment && typeof ctx.fragment === 'string') if (options.fragment && ctx.fragment !== null)
ret += `#${ctx.fragment}`; ret += `#${ctx.fragment}`;
return ret; return ret;
} }
@ -363,7 +355,12 @@ Object.defineProperties(URL.prototype, {
scheme = `${scheme}`; scheme = `${scheme}`;
if (scheme.length === 0) if (scheme.length === 0)
return; return;
binding.parse(scheme, binding.kSchemeStart, null, this[context], const ctx = this[context];
if (ctx.scheme === 'file:' &&
(ctx.host === '' || ctx.host === null)) {
return;
}
binding.parse(scheme, binding.kSchemeStart, null, ctx,
onParseProtocolComplete.bind(this)); onParseProtocolComplete.bind(this));
} }
}, },
@ -371,16 +368,16 @@ Object.defineProperties(URL.prototype, {
enumerable: true, enumerable: true,
configurable: true, configurable: true,
get() { get() {
return this[context].username || ''; return this[context].username;
}, },
set(username) { set(username) {
// toUSVString is not needed. // toUSVString is not needed.
username = `${username}`; username = `${username}`;
if (!this.hostname) if (this[cannotHaveUsernamePasswordPort])
return; return;
const ctx = this[context]; const ctx = this[context];
if (!username) { if (username === '') {
ctx.username = null; ctx.username = '';
ctx.flags &= ~binding.URL_FLAGS_HAS_USERNAME; ctx.flags &= ~binding.URL_FLAGS_HAS_USERNAME;
return; return;
} }
@ -392,16 +389,16 @@ Object.defineProperties(URL.prototype, {
enumerable: true, enumerable: true,
configurable: true, configurable: true,
get() { get() {
return this[context].password || ''; return this[context].password;
}, },
set(password) { set(password) {
// toUSVString is not needed. // toUSVString is not needed.
password = `${password}`; password = `${password}`;
if (!this.hostname) if (this[cannotHaveUsernamePasswordPort])
return; return;
const ctx = this[context]; const ctx = this[context];
if (!password) { if (password === '') {
ctx.password = null; ctx.password = '';
ctx.flags &= ~binding.URL_FLAGS_HAS_PASSWORD; ctx.flags &= ~binding.URL_FLAGS_HAS_PASSWORD;
return; return;
} }
@ -415,7 +412,7 @@ Object.defineProperties(URL.prototype, {
get() { get() {
const ctx = this[context]; const ctx = this[context];
var ret = ctx.host || ''; var ret = ctx.host || '';
if (ctx.port !== undefined) if (ctx.port !== null)
ret += `:${ctx.port}`; ret += `:${ctx.port}`;
return ret; return ret;
}, },
@ -423,15 +420,8 @@ Object.defineProperties(URL.prototype, {
const ctx = this[context]; const ctx = this[context];
// toUSVString is not needed. // toUSVString is not needed.
host = `${host}`; host = `${host}`;
if (this[cannotBeBase] || if (this[cannotBeBase]) {
(this[special] && host.length === 0)) { // Cannot set the host if cannot-be-base is set
// Cannot set the host if cannot-be-base is set or
// scheme is special and host length is zero
return;
}
if (!host) {
ctx.host = null;
ctx.flags &= ~binding.URL_FLAGS_HAS_HOST;
return; return;
} }
binding.parse(host, binding.kHost, null, ctx, binding.parse(host, binding.kHost, null, ctx,
@ -448,15 +438,8 @@ Object.defineProperties(URL.prototype, {
const ctx = this[context]; const ctx = this[context];
// toUSVString is not needed. // toUSVString is not needed.
host = `${host}`; host = `${host}`;
if (this[cannotBeBase] || if (this[cannotBeBase]) {
(this[special] && host.length === 0)) { // Cannot set the host if cannot-be-base is set
// Cannot set the host if cannot-be-base is set or
// scheme is special and host length is zero
return;
}
if (!host) {
ctx.host = null;
ctx.flags &= ~binding.URL_FLAGS_HAS_HOST;
return; return;
} }
binding.parse(host, binding.kHostname, null, ctx, binding.parse(host, binding.kHostname, null, ctx,
@ -468,17 +451,16 @@ Object.defineProperties(URL.prototype, {
configurable: true, configurable: true,
get() { get() {
const port = this[context].port; const port = this[context].port;
return port === undefined ? '' : String(port); return port === null ? '' : String(port);
}, },
set(port) { set(port) {
// toUSVString is not needed. // toUSVString is not needed.
port = `${port}`; port = `${port}`;
const ctx = this[context]; if (this[cannotHaveUsernamePasswordPort])
if (!ctx.host || this[cannotBeBase] ||
this.protocol === 'file:')
return; return;
const ctx = this[context];
if (port === '') { if (port === '') {
ctx.port = undefined; ctx.port = null;
return; return;
} }
binding.parse(port, binding.kPort, null, ctx, binding.parse(port, binding.kPort, null, ctx,
@ -492,7 +474,9 @@ Object.defineProperties(URL.prototype, {
const ctx = this[context]; const ctx = this[context];
if (this[cannotBeBase]) if (this[cannotBeBase])
return ctx.path[0]; return ctx.path[0];
return ctx.path !== undefined ? `/${ctx.path.join('/')}` : ''; if (ctx.path.length === 0)
return '';
return `/${ctx.path.join('/')}`;
}, },
set(path) { set(path) {
// toUSVString is not needed. // toUSVString is not needed.
@ -507,13 +491,15 @@ Object.defineProperties(URL.prototype, {
enumerable: true, enumerable: true,
configurable: true, configurable: true,
get() { get() {
const ctx = this[context]; const { query } = this[context];
return !ctx.query ? '' : `?${ctx.query}`; if (query === null || query === '')
return '';
return `?${query}`;
}, },
set(search) { set(search) {
const ctx = this[context]; const ctx = this[context];
search = toUSVString(search); search = toUSVString(search);
if (!search) { if (search === '') {
ctx.query = null; ctx.query = null;
ctx.flags &= ~binding.URL_FLAGS_HAS_QUERY; ctx.flags &= ~binding.URL_FLAGS_HAS_QUERY;
} else { } else {
@ -539,8 +525,10 @@ Object.defineProperties(URL.prototype, {
enumerable: true, enumerable: true,
configurable: true, configurable: true,
get() { get() {
const ctx = this[context]; const { fragment } = this[context];
return !ctx.fragment ? '' : `#${ctx.fragment}`; if (fragment === null || fragment === '')
return '';
return `#${fragment}`;
}, },
set(hash) { set(hash) {
const ctx = this[context]; const ctx = this[context];
@ -553,6 +541,7 @@ Object.defineProperties(URL.prototype, {
} }
if (hash[0] === '#') hash = hash.slice(1); if (hash[0] === '#') hash = hash.slice(1);
ctx.fragment = ''; ctx.fragment = '';
ctx.flags |= binding.URL_FLAGS_HAS_FRAGMENT;
binding.parse(hash, binding.kFragment, null, ctx, binding.parse(hash, binding.kFragment, null, ctx,
onParseHashComplete.bind(this)); onParseHashComplete.bind(this));
} }
@ -1384,10 +1373,10 @@ function constructUrl(flags, protocol, username, password,
var ctx = new URLContext(); var ctx = new URLContext();
ctx.flags = flags; ctx.flags = flags;
ctx.scheme = protocol; ctx.scheme = protocol;
ctx.username = username; ctx.username = (flags & binding.URL_FLAGS_HAS_USERNAME) !== 0 ? username : '';
ctx.password = password; ctx.password = (flags & binding.URL_FLAGS_HAS_PASSWORD) !== 0 ? password : '';
ctx.port = port; ctx.port = port;
ctx.path = path; ctx.path = (flags & binding.URL_FLAGS_HAS_PATH) !== 0 ? path : [];
ctx.query = query; ctx.query = query;
ctx.fragment = fragment; ctx.fragment = fragment;
ctx.host = host; ctx.host = host;

245
src/node_url.cc

@ -59,10 +59,12 @@ static const char kEOL = -1;
// Used in ToUSVString(). // Used in ToUSVString().
static const char16_t kUnicodeReplacementCharacter = 0xFFFD; static const char16_t kUnicodeReplacementCharacter = 0xFFFD;
// https://url.spec.whatwg.org/#concept-host
union url_host_value { union url_host_value {
std::string domain; std::string domain;
uint32_t ipv4; uint32_t ipv4;
uint16_t ipv6[8]; uint16_t ipv6[8];
std::string opaque;
~url_host_value() {} ~url_host_value() {}
}; };
@ -70,7 +72,8 @@ enum url_host_type {
HOST_TYPE_FAILED = -1, HOST_TYPE_FAILED = -1,
HOST_TYPE_DOMAIN = 0, HOST_TYPE_DOMAIN = 0,
HOST_TYPE_IPV4 = 1, HOST_TYPE_IPV4 = 1,
HOST_TYPE_IPV6 = 2 HOST_TYPE_IPV6 = 2,
HOST_TYPE_OPAQUE = 3,
}; };
struct url_host { struct url_host {
@ -151,6 +154,13 @@ static inline T ASCIILowercase(T ch) {
return IsASCIIAlpha(ch) ? (ch | 0x20) : 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 // https://url.spec.whatwg.org/#windows-drive-letter
TWO_CHAR_STRING_TEST(8, IsWindowsDriveLetter, TWO_CHAR_STRING_TEST(8, IsWindowsDriveLetter,
(IsASCIIAlpha(ch1) && (ch2 == ':' || ch2 == '|'))) (IsASCIIAlpha(ch1) && (ch2 == ':' || ch2 == '|')))
@ -206,7 +216,7 @@ static const char* hex[256] = {
"%F8", "%F9", "%FA", "%FB", "%FC", "%FD", "%FE", "%FF" "%F8", "%F9", "%FA", "%FB", "%FC", "%FD", "%FE", "%FF"
}; };
static const uint8_t SIMPLE_ENCODE_SET[32] = { static const uint8_t C0_CONTROL_ENCODE_SET[32] = {
// 00 01 02 03 04 05 06 07 // 00 01 02 03 04 05 06 07
0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
// 08 09 0A 0B 0C 0D 0E 0F // 08 09 0A 0B 0C 0D 0E 0F
@ -273,7 +283,7 @@ static const uint8_t SIMPLE_ENCODE_SET[32] = {
0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80
}; };
static const uint8_t DEFAULT_ENCODE_SET[32] = { static const uint8_t PATH_ENCODE_SET[32] = {
// 00 01 02 03 04 05 06 07 // 00 01 02 03 04 05 06 07
0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80,
// 08 09 0A 0B 0C 0D 0E 0F // 08 09 0A 0B 0C 0D 0E 0F
@ -756,8 +766,8 @@ static url_host_type ParseIPv4Host(url_host* host,
if (ch == '.' || ch == kEOL) { if (ch == '.' || ch == kEOL) {
if (++parts > 4) if (++parts > 4)
goto end; goto end;
if (pointer - mark == 0) if (pointer == mark)
break; goto end;
int64_t n = ParseNumber(mark, pointer); int64_t n = ParseNumber(mark, pointer);
if (n < 0) if (n < 0)
goto end; goto end;
@ -797,9 +807,32 @@ static url_host_type ParseIPv4Host(url_host* host,
return 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, static url_host_type ParseHost(url_host* host,
const char* input, const char* input,
size_t length, size_t length,
bool is_special,
bool unicode = false) { bool unicode = false) {
url_host_type type = HOST_TYPE_FAILED; url_host_type type = HOST_TYPE_FAILED;
const char* pointer = input; const char* pointer = input;
@ -814,6 +847,9 @@ static url_host_type ParseHost(url_host* host,
return ParseIPv6Host(host, ++pointer, length - 2); return ParseIPv6Host(host, ++pointer, length - 2);
} }
if (!is_special)
return ParseOpaqueHost(host, input, length);
// First, we have to percent decode // First, we have to percent decode
PercentDecode(input, length, &decoded); PercentDecode(input, length, &decoded);
@ -824,10 +860,7 @@ static url_host_type ParseHost(url_host* host,
// If any of the following characters are still present, we have to fail // If any of the following characters are still present, we have to fail
for (size_t n = 0; n < decoded.size(); n++) { for (size_t n = 0; n < decoded.size(); n++) {
const char ch = decoded[n]; const char ch = decoded[n];
if (ch == 0x00 || ch == 0x09 || ch == 0x0a || ch == 0x0d || if (IsForbiddenHostCodePoint(ch)) {
ch == 0x20 || ch == '#' || ch == '%' || ch == '/' ||
ch == '?' || ch == '@' || ch == '[' || ch == '\\' ||
ch == ']') {
goto end; goto end;
} }
} }
@ -907,14 +940,17 @@ static url_host_type WriteHost(url_host* host, std::string* dest) {
uint16_t* start = &host->value.ipv6[0]; uint16_t* start = &host->value.ipv6[0];
uint16_t* compress_pointer = uint16_t* compress_pointer =
FindLongestZeroSequence(start, 8); FindLongestZeroSequence(start, 8);
bool ignore0 = false;
for (int n = 0; n <= 7; n++) { for (int n = 0; n <= 7; n++) {
uint16_t* piece = &host->value.ipv6[n]; uint16_t* piece = &host->value.ipv6[n];
if (ignore0 && *piece == 0)
continue;
else if (ignore0)
ignore0 = false;
if (compress_pointer == piece) { if (compress_pointer == piece) {
*dest += n == 0 ? "::" : ":"; *dest += n == 0 ? "::" : ":";
while (*piece == 0 && ++n < 8) ignore0 = true;
piece = &host->value.ipv6[n]; continue;
if (n == 8)
break;
} }
char buf[5]; char buf[5];
char* buffer = buf; char* buffer = buf;
@ -926,6 +962,9 @@ static url_host_type WriteHost(url_host* host, std::string* dest) {
*dest += ']'; *dest += ']';
break; break;
} }
case HOST_TYPE_OPAQUE:
*dest = host->value.opaque;
break;
case HOST_TYPE_FAILED: case HOST_TYPE_FAILED:
break; break;
} }
@ -934,11 +973,14 @@ static url_host_type WriteHost(url_host* host, std::string* dest) {
static bool ParseHost(std::string* input, static bool ParseHost(std::string* input,
std::string* output, std::string* output,
bool is_special,
bool unicode = false) { bool unicode = false) {
if (input->length() == 0) if (input->length() == 0) {
output->clear();
return true; return true;
}
url_host host{{""}, HOST_TYPE_DOMAIN}; url_host host{{""}, HOST_TYPE_DOMAIN};
ParseHost(&host, input->c_str(), input->length(), unicode); ParseHost(&host, input->c_str(), input->length(), is_special, unicode);
if (host.type == HOST_TYPE_FAILED) if (host.type == HOST_TYPE_FAILED)
return false; return false;
WriteHost(&host, output); WriteHost(&host, output);
@ -1006,6 +1048,12 @@ static inline void HarvestContext(Environment* env,
context->flags |= URL_FLAGS_SPECIAL; context->flags |= URL_FLAGS_SPECIAL;
if (_flags & URL_FLAGS_CANNOT_BE_BASE) if (_flags & URL_FLAGS_CANNOT_BE_BASE)
context->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<Value> scheme = GET(env, context_obj, "scheme"); Local<Value> scheme = GET(env, context_obj, "scheme");
if (scheme->IsString()) { if (scheme->IsString()) {
@ -1015,6 +1063,23 @@ static inline void HarvestContext(Environment* env,
Local<Value> port = GET(env, context_obj, "port"); Local<Value> port = GET(env, context_obj, "port");
if (port->IsInt32()) if (port->IsInt32())
context->port = port->Int32Value(env->context()).FromJust(); context->port = port->Int32Value(env->context()).FromJust();
if (context->flags & URL_FLAGS_HAS_USERNAME) {
Local<Value> 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<Value> password = GET(env, context_obj, "password");
CHECK(password->IsString());
Utf8Value value(env->isolate(), password);
context->password.assign(*value, value.length());
}
Local<Value> 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" // Single dot segment can be ".", "%2e", or "%2E"
@ -1077,7 +1142,6 @@ void URL::Parse(const char* input,
bool atflag = false; bool atflag = false;
bool sbflag = false; bool sbflag = false;
bool uflag = false; bool uflag = false;
bool base_is_file = false;
int wskip = 0; int wskip = 0;
std::string buffer; std::string buffer;
@ -1137,25 +1201,40 @@ void URL::Parse(const char* input,
case kScheme: case kScheme:
if (IsASCIIAlphanumeric(ch) || ch == '+' || ch == '-' || ch == '.') { if (IsASCIIAlphanumeric(ch) || ch == '+' || ch == '-' || ch == '.') {
buffer += ASCIILowercase(ch); buffer += ASCIILowercase(ch);
p++;
continue;
} else if (ch == ':' || (has_state_override && ch == kEOL)) { } else if (ch == ':' || (has_state_override && ch == kEOL)) {
if (buffer.size() > 0) { if (has_state_override && buffer.size() == 0) {
url->flags |= URL_FLAGS_TERMINATED;
return;
}
buffer += ':'; buffer += ':';
url->scheme = buffer;
} else if (has_state_override) { 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; url->flags |= URL_FLAGS_TERMINATED;
return; return;
} }
if (IsSpecial(url->scheme)) {
// File scheme && (host == empty or null) check left to JS-land
// as it can be done before even entering C++ binding.
}
url->scheme = buffer;
if (new_is_special) {
url->flags |= URL_FLAGS_SPECIAL; url->flags |= URL_FLAGS_SPECIAL;
special = true; special = true;
} else { } else {
url->flags &= ~URL_FLAGS_SPECIAL; url->flags &= ~URL_FLAGS_SPECIAL;
special = false;
} }
buffer.clear();
if (has_state_override) if (has_state_override)
return; return;
buffer.clear();
if (url->scheme == "file:") { if (url->scheme == "file:") {
state = kFile; state = kFile;
} else if (special && } else if (special &&
@ -1195,6 +1274,7 @@ void URL::Parse(const char* input,
special = true; special = true;
} else { } else {
url->flags &= ~URL_FLAGS_SPECIAL; url->flags &= ~URL_FLAGS_SPECIAL;
special = false;
} }
if (base->flags & URL_FLAGS_HAS_PATH) { if (base->flags & URL_FLAGS_HAS_PATH) {
url->flags |= URL_FLAGS_HAS_PATH; url->flags |= URL_FLAGS_HAS_PATH;
@ -1246,6 +1326,7 @@ void URL::Parse(const char* input,
special = true; special = true;
} else { } else {
url->flags &= ~URL_FLAGS_SPECIAL; url->flags &= ~URL_FLAGS_SPECIAL;
special = false;
} }
switch (ch) { switch (ch) {
case kEOL: case kEOL:
@ -1414,6 +1495,10 @@ void URL::Parse(const char* input,
ch == '?' || ch == '?' ||
ch == '#' || ch == '#' ||
special_back_slash) { special_back_slash) {
if (atflag && buffer.size() == 0) {
url->flags |= URL_FLAGS_FAILED;
return;
}
p -= buffer.size() + 1 + wskip; p -= buffer.size() + 1 + wskip;
buffer.clear(); buffer.clear();
state = kHost; state = kHost;
@ -1423,13 +1508,16 @@ void URL::Parse(const char* input,
break; break;
case kHost: case kHost:
case kHostname: case kHostname:
if (ch == ':' && !sbflag) { if (has_state_override && url->scheme == "file:") {
if (special && buffer.size() == 0) { state = kFileHost;
continue;
} else if (ch == ':' && !sbflag) {
if (buffer.size() == 0) {
url->flags |= URL_FLAGS_FAILED; url->flags |= URL_FLAGS_FAILED;
return; return;
} }
url->flags |= URL_FLAGS_HAS_HOST; url->flags |= URL_FLAGS_HAS_HOST;
if (!ParseHost(&buffer, &url->host)) { if (!ParseHost(&buffer, &url->host, special)) {
url->flags |= URL_FLAGS_FAILED; url->flags |= URL_FLAGS_FAILED;
return; return;
} }
@ -1448,8 +1536,15 @@ void URL::Parse(const char* input,
url->flags |= URL_FLAGS_FAILED; url->flags |= URL_FLAGS_FAILED;
return; 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; url->flags |= URL_FLAGS_HAS_HOST;
if (!ParseHost(&buffer, &url->host)) { if (!ParseHost(&buffer, &url->host, special)) {
url->flags |= URL_FLAGS_FAILED; url->flags |= URL_FLAGS_FAILED;
return; return;
} }
@ -1463,7 +1558,7 @@ void URL::Parse(const char* input,
sbflag = true; sbflag = true;
if (ch == ']') if (ch == ']')
sbflag = false; sbflag = false;
buffer += ASCIILowercase(ch); buffer += ch;
} }
break; break;
case kPort: case kPort:
@ -1508,12 +1603,12 @@ void URL::Parse(const char* input,
} }
break; break;
case kFile: case kFile:
base_is_file = ( url->scheme = "file:";
has_base && if (ch == '/' || ch == '\\') {
base->scheme == "file:"); state = kFileSlash;
} else if (has_base && base->scheme == "file:") {
switch (ch) { switch (ch) {
case kEOL: case kEOL:
if (base_is_file) {
if (base->flags & URL_FLAGS_HAS_HOST) { if (base->flags & URL_FLAGS_HAS_HOST) {
url->flags |= URL_FLAGS_HAS_HOST; url->flags |= URL_FLAGS_HAS_HOST;
url->host = base->host; url->host = base->host;
@ -1527,15 +1622,7 @@ void URL::Parse(const char* input,
url->query = base->query; url->query = base->query;
} }
break; break;
}
state = kPath;
continue;
case '\\':
case '/':
state = kFileSlash;
break;
case '?': case '?':
if (base_is_file) {
if (base->flags & URL_FLAGS_HAS_HOST) { if (base->flags & URL_FLAGS_HAS_HOST) {
url->flags |= URL_FLAGS_HAS_HOST; url->flags |= URL_FLAGS_HAS_HOST;
url->host = base->host; url->host = base->host;
@ -1545,11 +1632,10 @@ void URL::Parse(const char* input,
url->path = base->path; url->path = base->path;
} }
url->flags |= URL_FLAGS_HAS_QUERY; url->flags |= URL_FLAGS_HAS_QUERY;
url->query.clear();
state = kQuery; state = kQuery;
break; break;
}
case '#': case '#':
if (base_is_file) {
if (base->flags & URL_FLAGS_HAS_HOST) { if (base->flags & URL_FLAGS_HAS_HOST) {
url->flags |= URL_FLAGS_HAS_HOST; url->flags |= URL_FLAGS_HAS_HOST;
url->host = base->host; url->host = base->host;
@ -1562,12 +1648,12 @@ void URL::Parse(const char* input,
url->flags |= URL_FLAGS_HAS_QUERY; url->flags |= URL_FLAGS_HAS_QUERY;
url->query = base->query; url->query = base->query;
} }
url->flags |= URL_FLAGS_HAS_FRAGMENT;
url->fragment.clear();
state = kFragment; state = kFragment;
break; break;
}
default: default:
if (base_is_file && if ((!IsWindowsDriveLetter(ch, p[1]) ||
(!IsWindowsDriveLetter(ch, p[1]) ||
end - p == 1 || end - p == 1 ||
(p[2] != '/' && (p[2] != '/' &&
p[2] != '\\' && p[2] != '\\' &&
@ -1586,6 +1672,10 @@ void URL::Parse(const char* input,
state = kPath; state = kPath;
continue; continue;
} }
} else {
state = kPath;
continue;
}
break; break;
case kFileSlash: case kFileSlash:
if (ch == '/' || ch == '\\') { if (ch == '/' || ch == '\\') {
@ -1597,8 +1687,13 @@ void URL::Parse(const char* input,
url->flags |= URL_FLAGS_HAS_PATH; url->flags |= URL_FLAGS_HAS_PATH;
url->path.push_back(base->path[0]); url->path.push_back(base->path[0]);
} else { } else {
if (base->flags & URL_FLAGS_HAS_HOST) {
url->flags |= URL_FLAGS_HAS_HOST; url->flags |= URL_FLAGS_HAS_HOST;
url->host = base->host; url->host = base->host;
} else {
url->flags &= ~URL_FLAGS_HAS_HOST;
url->host.clear();
}
} }
} }
state = kPath; state = kPath;
@ -1611,19 +1706,28 @@ void URL::Parse(const char* input,
ch == '\\' || ch == '\\' ||
ch == '?' || ch == '?' ||
ch == '#') { ch == '#') {
if (buffer.size() == 2 && if (!has_state_override &&
buffer.size() == 2 &&
IsWindowsDriveLetter(buffer)) { IsWindowsDriveLetter(buffer)) {
state = kPath; state = kPath;
} else if (buffer.size() == 0) { } else if (buffer.size() == 0) {
url->flags |= URL_FLAGS_HAS_HOST;
url->host.clear();
if (has_state_override)
return;
state = kPathStart; state = kPathStart;
} else { } else {
if (buffer != "localhost") { std::string host;
url->flags |= URL_FLAGS_HAS_HOST; if (!ParseHost(&buffer, &host, special)) {
if (!ParseHost(&buffer, &url->host)) {
url->flags |= URL_FLAGS_FAILED; url->flags |= URL_FLAGS_FAILED;
return; return;
} }
} if (host == "localhost")
host.clear();
url->flags |= URL_FLAGS_HAS_HOST;
url->host = host;
if (has_state_override)
return;
buffer.clear(); buffer.clear();
state = kPathStart; state = kPathStart;
} }
@ -1664,17 +1768,20 @@ void URL::Parse(const char* input,
url->flags |= URL_FLAGS_HAS_PATH; url->flags |= URL_FLAGS_HAS_PATH;
url->path.push_back(""); url->path.push_back("");
} }
} else if (IsSingleDotSegment(buffer)) { } else if (IsSingleDotSegment(buffer) &&
if (ch != '/' && !special_back_slash) { ch != '/' && !special_back_slash) {
url->flags |= URL_FLAGS_HAS_PATH; url->flags |= URL_FLAGS_HAS_PATH;
url->path.push_back(""); url->path.push_back("");
}
} else if (!IsSingleDotSegment(buffer)) { } else if (!IsSingleDotSegment(buffer)) {
if (url->scheme == "file:" && if (url->scheme == "file:" &&
url->path.empty() && url->path.empty() &&
buffer.size() == 2 && buffer.size() == 2 &&
IsWindowsDriveLetter(buffer)) { IsWindowsDriveLetter(buffer)) {
url->flags &= ~URL_FLAGS_HAS_HOST; if ((url->flags & URL_FLAGS_HAS_HOST) &&
!url->host.empty()) {
url->host.clear();
url->flags |= URL_FLAGS_HAS_HOST;
}
buffer[1] = ':'; buffer[1] = ':';
} }
url->flags |= URL_FLAGS_HAS_PATH; url->flags |= URL_FLAGS_HAS_PATH;
@ -1697,7 +1804,7 @@ void URL::Parse(const char* input,
state = kFragment; state = kFragment;
} }
} else { } else {
AppendOrEscape(&buffer, ch, DEFAULT_ENCODE_SET); AppendOrEscape(&buffer, ch, PATH_ENCODE_SET);
} }
break; break;
case kCannotBeBase: case kCannotBeBase:
@ -1712,7 +1819,7 @@ void URL::Parse(const char* input,
if (url->path.size() == 0) if (url->path.size() == 0)
url->path.push_back(""); url->path.push_back("");
if (url->path.size() > 0 && ch != kEOL) if (url->path.size() > 0 && ch != kEOL)
AppendOrEscape(&url->path[0], ch, SIMPLE_ENCODE_SET); AppendOrEscape(&url->path[0], ch, C0_CONTROL_ENCODE_SET);
} }
break; break;
case kQuery: case kQuery:
@ -1735,7 +1842,7 @@ void URL::Parse(const char* input,
case 0: case 0:
break; break;
default: default:
AppendOrEscape(&buffer, ch, SIMPLE_ENCODE_SET); AppendOrEscape(&buffer, ch, C0_CONTROL_ENCODE_SET);
} }
break; break;
default: default:
@ -1800,17 +1907,18 @@ static void Parse(Environment* env,
// Define the return value placeholders // Define the return value placeholders
const Local<Value> undef = Undefined(isolate); const Local<Value> undef = Undefined(isolate);
const Local<Value> null = Null(isolate);
if (!(url.flags & URL_FLAGS_FAILED)) { if (!(url.flags & URL_FLAGS_FAILED)) {
Local<Value> argv[9] = { Local<Value> argv[9] = {
undef, undef,
undef, undef,
undef, undef,
undef, undef,
null, // host defaults to null
null, // port defaults to null
undef, undef,
undef, null, // query defaults to null
undef, null, // fragment defaults to null
undef,
undef,
}; };
SetArgs(env, argv, &url); SetArgs(env, argv, &url);
(void)cb->Call(context, recv, arraysize(argv), argv); (void)cb->Call(context, recv, arraysize(argv), argv);
@ -1914,7 +2022,8 @@ static void DomainToASCII(const FunctionCallbackInfo<Value>& args) {
Utf8Value value(env->isolate(), args[0]); Utf8Value value(env->isolate(), args[0]);
url_host host{{""}, HOST_TYPE_DOMAIN}; url_host host{{""}, HOST_TYPE_DOMAIN};
ParseHost(&host, *value, value.length()); // Assuming the host is used for a special scheme.
ParseHost(&host, *value, value.length(), true);
if (host.type == HOST_TYPE_FAILED) { if (host.type == HOST_TYPE_FAILED) {
args.GetReturnValue().Set(FIXED_ONE_BYTE_STRING(env->isolate(), "")); args.GetReturnValue().Set(FIXED_ONE_BYTE_STRING(env->isolate(), ""));
return; return;
@ -1934,7 +2043,8 @@ static void DomainToUnicode(const FunctionCallbackInfo<Value>& args) {
Utf8Value value(env->isolate(), args[0]); Utf8Value value(env->isolate(), args[0]);
url_host host{{""}, HOST_TYPE_DOMAIN}; url_host host{{""}, HOST_TYPE_DOMAIN};
ParseHost(&host, *value, value.length(), true); // Assuming the host is used for a special scheme.
ParseHost(&host, *value, value.length(), true, true);
if (host.type == HOST_TYPE_FAILED) { if (host.type == HOST_TYPE_FAILED) {
args.GetReturnValue().Set(FIXED_ONE_BYTE_STRING(env->isolate(), "")); args.GetReturnValue().Set(FIXED_ONE_BYTE_STRING(env->isolate(), ""));
return; return;
@ -1957,6 +2067,7 @@ const Local<Value> URL::ToObject(Environment* env) const {
Context::Scope context_scope(context); Context::Scope context_scope(context);
const Local<Value> undef = Undefined(isolate); const Local<Value> undef = Undefined(isolate);
const Local<Value> null = Null(isolate);
if (context_.flags & URL_FLAGS_FAILED) if (context_.flags & URL_FLAGS_FAILED)
return Local<Value>(); return Local<Value>();
@ -1966,11 +2077,11 @@ const Local<Value> URL::ToObject(Environment* env) const {
undef, undef,
undef, undef,
undef, undef,
null, // host defaults to null
null, // port defaults to null
undef, undef,
undef, null, // query defaults to null
undef, null, // fragment defaults to null
undef,
undef,
}; };
SetArgs(env, argv, &context_); SetArgs(env, argv, &context_);

466
test/fixtures/url-setter-tests.js

@ -370,14 +370,14 @@ module.exports =
"username": "wario" "username": "wario"
} }
}, },
// { {
// "href": "file://test/", "href": "file://test/",
// "new_value": "test", "new_value": "test",
// "expected": { "expected": {
// "href": "file://test/", "href": "file://test/",
// "username": "" "username": ""
// } }
// } }
], ],
"password": [ "password": [
{ {
@ -473,14 +473,14 @@ module.exports =
"password": "bowser" "password": "bowser"
} }
}, },
// { {
// "href": "file://test/", "href": "file://test/",
// "new_value": "test", "new_value": "test",
// "expected": { "expected": {
// "href": "file://test/", "href": "file://test/",
// "password": "" "password": ""
// } }
// } }
], ],
"host": [ "host": [
{ {
@ -493,33 +493,33 @@ module.exports =
"hostname": "x" "hostname": "x"
} }
}, },
// { {
// "href": "sc://x/", "href": "sc://x/",
// "new_value": "\u0009", "new_value": "\u0009",
// "expected": { "expected": {
// "href": "sc:///", "href": "sc:///",
// "host": "", "host": "",
// "hostname": "" "hostname": ""
// } }
// }, },
// { {
// "href": "sc://x/", "href": "sc://x/",
// "new_value": "\u000A", "new_value": "\u000A",
// "expected": { "expected": {
// "href": "sc:///", "href": "sc:///",
// "host": "", "host": "",
// "hostname": "" "hostname": ""
// } }
// }, },
// { {
// "href": "sc://x/", "href": "sc://x/",
// "new_value": "\u000D", "new_value": "\u000D",
// "expected": { "expected": {
// "href": "sc:///", "href": "sc:///",
// "host": "", "host": "",
// "hostname": "" "hostname": ""
// } }
// }, },
{ {
"href": "sc://x/", "href": "sc://x/",
"new_value": " ", "new_value": " ",
@ -529,33 +529,33 @@ module.exports =
"hostname": "x" "hostname": "x"
} }
}, },
// { {
// "href": "sc://x/", "href": "sc://x/",
// "new_value": "#", "new_value": "#",
// "expected": { "expected": {
// "href": "sc:///", "href": "sc:///",
// "host": "", "host": "",
// "hostname": "" "hostname": ""
// } }
// }, },
// { {
// "href": "sc://x/", "href": "sc://x/",
// "new_value": "/", "new_value": "/",
// "expected": { "expected": {
// "href": "sc:///", "href": "sc:///",
// "host": "", "host": "",
// "hostname": "" "hostname": ""
// } }
// }, },
// { {
// "href": "sc://x/", "href": "sc://x/",
// "new_value": "?", "new_value": "?",
// "expected": { "expected": {
// "href": "sc:///", "href": "sc:///",
// "host": "", "host": "",
// "hostname": "" "hostname": ""
// } }
// }, },
{ {
"href": "sc://x/", "href": "sc://x/",
"new_value": "@", "new_value": "@",
@ -565,15 +565,15 @@ module.exports =
"hostname": "x" "hostname": "x"
} }
}, },
// { {
// "href": "sc://x/", "href": "sc://x/",
// "new_value": "ß", "new_value": "ß",
// "expected": { "expected": {
// "href": "sc://%C3%9F/", "href": "sc://%C3%9F/",
// "host": "%C3%9F", "host": "%C3%9F",
// "hostname": "%C3%9F" "hostname": "%C3%9F"
// } }
// }, },
{ {
"comment": "IDNA Nontransitional_Processing", "comment": "IDNA Nontransitional_Processing",
"href": "https://x/", "href": "https://x/",
@ -916,56 +916,56 @@ module.exports =
"hostname": "example.net" "hostname": "example.net"
} }
}, },
// { {
// "href": "file://y/", "href": "file://y/",
// "new_value": "x:123", "new_value": "x:123",
// "expected": { "expected": {
// "href": "file://y/", "href": "file://y/",
// "host": "y", "host": "y",
// "hostname": "y", "hostname": "y",
// "port": "" "port": ""
// } }
// }, },
// { {
// "href": "file://y/", "href": "file://y/",
// "new_value": "loc%41lhost", "new_value": "loc%41lhost",
// "expected": { "expected": {
// "href": "file:///", "href": "file:///",
// "host": "", "host": "",
// "hostname": "", "hostname": "",
// "port": "" "port": ""
// } }
// }, },
// { {
// "href": "file://hi/x", "href": "file://hi/x",
// "new_value": "", "new_value": "",
// "expected": { "expected": {
// "href": "file:///x", "href": "file:///x",
// "host": "", "host": "",
// "hostname": "", "hostname": "",
// "port": "" "port": ""
// } }
// }, },
// { {
// "href": "sc://test@test/", "href": "sc://test@test/",
// "new_value": "", "new_value": "",
// "expected": { "expected": {
// "href": "sc://test@test/", "href": "sc://test@test/",
// "host": "test", "host": "test",
// "hostname": "test", "hostname": "test",
// "username": "test" "username": "test"
// } }
// }, },
// { {
// "href": "sc://test:12/", "href": "sc://test:12/",
// "new_value": "", "new_value": "",
// "expected": { "expected": {
// "href": "sc://test:12/", "href": "sc://test:12/",
// "host": "test:12", "host": "test:12",
// "hostname": "test", "hostname": "test",
// "port": "12" "port": "12"
// } }
// } }
], ],
"hostname": [ "hostname": [
{ {
@ -978,33 +978,33 @@ module.exports =
"hostname": "x" "hostname": "x"
} }
}, },
// { {
// "href": "sc://x/", "href": "sc://x/",
// "new_value": "\u0009", "new_value": "\u0009",
// "expected": { "expected": {
// "href": "sc:///", "href": "sc:///",
// "host": "", "host": "",
// "hostname": "" "hostname": ""
// } }
// }, },
// { {
// "href": "sc://x/", "href": "sc://x/",
// "new_value": "\u000A", "new_value": "\u000A",
// "expected": { "expected": {
// "href": "sc:///", "href": "sc:///",
// "host": "", "host": "",
// "hostname": "" "hostname": ""
// } }
// }, },
// { {
// "href": "sc://x/", "href": "sc://x/",
// "new_value": "\u000D", "new_value": "\u000D",
// "expected": { "expected": {
// "href": "sc:///", "href": "sc:///",
// "host": "", "host": "",
// "hostname": "" "hostname": ""
// } }
// }, },
{ {
"href": "sc://x/", "href": "sc://x/",
"new_value": " ", "new_value": " ",
@ -1014,33 +1014,33 @@ module.exports =
"hostname": "x" "hostname": "x"
} }
}, },
// { {
// "href": "sc://x/", "href": "sc://x/",
// "new_value": "#", "new_value": "#",
// "expected": { "expected": {
// "href": "sc:///", "href": "sc:///",
// "host": "", "host": "",
// "hostname": "" "hostname": ""
// } }
// }, },
// { {
// "href": "sc://x/", "href": "sc://x/",
// "new_value": "/", "new_value": "/",
// "expected": { "expected": {
// "href": "sc:///", "href": "sc:///",
// "host": "", "host": "",
// "hostname": "" "hostname": ""
// } }
// }, },
// { {
// "href": "sc://x/", "href": "sc://x/",
// "new_value": "?", "new_value": "?",
// "expected": { "expected": {
// "href": "sc:///", "href": "sc:///",
// "host": "", "host": "",
// "hostname": "" "hostname": ""
// } }
// }, },
{ {
"href": "sc://x/", "href": "sc://x/",
"new_value": "@", "new_value": "@",
@ -1250,56 +1250,56 @@ module.exports =
"hostname": "example.net" "hostname": "example.net"
} }
}, },
// { {
// "href": "file://y/", "href": "file://y/",
// "new_value": "x:123", "new_value": "x:123",
// "expected": { "expected": {
// "href": "file://y/", "href": "file://y/",
// "host": "y", "host": "y",
// "hostname": "y", "hostname": "y",
// "port": "" "port": ""
// } }
// }, },
// { {
// "href": "file://y/", "href": "file://y/",
// "new_value": "loc%41lhost", "new_value": "loc%41lhost",
// "expected": { "expected": {
// "href": "file:///", "href": "file:///",
// "host": "", "host": "",
// "hostname": "", "hostname": "",
// "port": "" "port": ""
// } }
// }, },
// { {
// "href": "file://hi/x", "href": "file://hi/x",
// "new_value": "", "new_value": "",
// "expected": { "expected": {
// "href": "file:///x", "href": "file:///x",
// "host": "", "host": "",
// "hostname": "", "hostname": "",
// "port": "" "port": ""
// } }
// }, },
// { {
// "href": "sc://test@test/", "href": "sc://test@test/",
// "new_value": "", "new_value": "",
// "expected": { "expected": {
// "href": "sc://test@test/", "href": "sc://test@test/",
// "host": "test", "host": "test",
// "hostname": "test", "hostname": "test",
// "username": "test" "username": "test"
// } }
// }, },
// { {
// "href": "sc://test:12/", "href": "sc://test:12/",
// "new_value": "", "new_value": "",
// "expected": { "expected": {
// "href": "sc://test:12/", "href": "sc://test:12/",
// "host": "test:12", "host": "test:12",
// "hostname": "test", "hostname": "test",
// "port": "12" "port": "12"
// } }
// } }
], ],
"port": [ "port": [
{ {

584
test/fixtures/url-tests.js

@ -1045,16 +1045,16 @@ module.exports =
"search": "", "search": "",
"hash": "" "hash": ""
}, },
// { {
// "input": "file://example:1/", "input": "file://example:1/",
// "base": "about:blank", "base": "about:blank",
// "failure": true "failure": true
// }, },
// { {
// "input": "file://example:test/", "input": "file://example:test/",
// "base": "about:blank", "base": "about:blank",
// "failure": true "failure": true
// }, },
{ {
"input": "file://example%/", "input": "file://example%/",
"base": "about:blank", "base": "about:blank",
@ -3674,35 +3674,35 @@ module.exports =
"search": "", "search": "",
"hash": "" "hash": ""
}, },
// { {
// "input": "https://faß.ExAmPlE/", "input": "https://faß.ExAmPlE/",
// "base": "about:blank", "base": "about:blank",
// "href": "https://xn--fa-hia.example/", "href": "https://xn--fa-hia.example/",
// "origin": "https://faß.example", "origin": "https://faß.example",
// "protocol": "https:", "protocol": "https:",
// "username": "", "username": "",
// "password": "", "password": "",
// "host": "xn--fa-hia.example", "host": "xn--fa-hia.example",
// "hostname": "xn--fa-hia.example", "hostname": "xn--fa-hia.example",
// "port": "", "port": "",
// "pathname": "/", "pathname": "/",
// "search": "", "search": "",
// "hash": "" "hash": ""
// }, },
// { {
// "input": "sc://faß.ExAmPlE/", "input": "sc://faß.ExAmPlE/",
// "base": "about:blank", "base": "about:blank",
// "href": "sc://fa%C3%9F.ExAmPlE/", "href": "sc://fa%C3%9F.ExAmPlE/",
// "protocol": "sc:", "protocol": "sc:",
// "username": "", "username": "",
// "password": "", "password": "",
// "host": "fa%C3%9F.ExAmPlE", "host": "fa%C3%9F.ExAmPlE",
// "hostname": "fa%C3%9F.ExAmPlE", "hostname": "fa%C3%9F.ExAmPlE",
// "port": "", "port": "",
// "pathname": "/", "pathname": "/",
// "search": "", "search": "",
// "hash": "" "hash": ""
// }, },
"Invalid escaped characters should fail and the percents should be escaped. https://www.w3.org/Bugs/Public/show_bug.cgi?id=24191", "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", "input": "http://%zz%66%a.com",
@ -4362,37 +4362,37 @@ module.exports =
"search": "", "search": "",
"hash": "" "hash": ""
}, },
// "# unknown schemes and their hosts", "# unknown schemes and their hosts",
// { {
// "input": "sc://ñ.test/", "input": "sc://ñ.test/",
// "base": "about:blank", "base": "about:blank",
// "href": "sc://%C3%B1.test/", "href": "sc://%C3%B1.test/",
// "origin": "null", "origin": "null",
// "protocol": "sc:", "protocol": "sc:",
// "username": "", "username": "",
// "password": "", "password": "",
// "host": "%C3%B1.test", "host": "%C3%B1.test",
// "hostname": "%C3%B1.test", "hostname": "%C3%B1.test",
// "port": "", "port": "",
// "pathname": "/", "pathname": "/",
// "search": "", "search": "",
// "hash": "" "hash": ""
// }, },
// { {
// "input": "sc://\u001F!\"$&'()*+,-.;<=>^_`{|}~/", "input": "sc://\u001F!\"$&'()*+,-.;<=>^_`{|}~/",
// "base": "about:blank", "base": "about:blank",
// "href": "sc://%1F!\"$&'()*+,-.;<=>^_`{|}~/", "href": "sc://%1F!\"$&'()*+,-.;<=>^_`{|}~/",
// "origin": "null", "origin": "null",
// "protocol": "sc:", "protocol": "sc:",
// "username": "", "username": "",
// "password": "", "password": "",
// "host": "%1F!\"$&'()*+,-.;<=>^_`{|}~", "host": "%1F!\"$&'()*+,-.;<=>^_`{|}~",
// "hostname": "%1F!\"$&'()*+,-.;<=>^_`{|}~", "hostname": "%1F!\"$&'()*+,-.;<=>^_`{|}~",
// "port": "", "port": "",
// "pathname": "/", "pathname": "/",
// "search": "", "search": "",
// "hash": "" "hash": ""
// }, },
{ {
"input": "sc://\u0000/", "input": "sc://\u0000/",
"base": "about:blank", "base": "about:blank",
@ -4403,40 +4403,40 @@ module.exports =
"base": "about:blank", "base": "about:blank",
"failure": true "failure": true
}, },
// { {
// "input": "sc://%/", "input": "sc://%/",
// "base": "about:blank", "base": "about:blank",
// "href": "sc://%/", "href": "sc://%/",
// "protocol": "sc:", "protocol": "sc:",
// "username": "", "username": "",
// "password": "", "password": "",
// "host": "%", "host": "%",
// "hostname": "%", "hostname": "%",
// "port": "", "port": "",
// "pathname": "/", "pathname": "/",
// "search": "", "search": "",
// "hash": "" "hash": ""
// }, },
// { {
// "input": "sc://@/", "input": "sc://@/",
// "base": "about:blank", "base": "about:blank",
// "failure": true "failure": true
// }, },
// { {
// "input": "sc://te@s:t@/", "input": "sc://te@s:t@/",
// "base": "about:blank", "base": "about:blank",
// "failure": true "failure": true
// }, },
// { {
// "input": "sc://:/", "input": "sc://:/",
// "base": "about:blank", "base": "about:blank",
// "failure": true "failure": true
// }, },
// { {
// "input": "sc://:12/", "input": "sc://:12/",
// "base": "about:blank", "base": "about:blank",
// "failure": true "failure": true
// }, },
{ {
"input": "sc://[/", "input": "sc://[/",
"base": "about:blank", "base": "about:blank",
@ -4452,21 +4452,21 @@ module.exports =
"base": "about:blank", "base": "about:blank",
"failure": true "failure": true
}, },
// { {
// "input": "x", "input": "x",
// "base": "sc://ñ", "base": "sc://ñ",
// "href": "sc://%C3%B1/x", "href": "sc://%C3%B1/x",
// "origin": "null", "origin": "null",
// "protocol": "sc:", "protocol": "sc:",
// "username": "", "username": "",
// "password": "", "password": "",
// "host": "%C3%B1", "host": "%C3%B1",
// "hostname": "%C3%B1", "hostname": "%C3%B1",
// "port": "", "port": "",
// "pathname": "/x", "pathname": "/x",
// "search": "", "search": "",
// "hash": "" "hash": ""
// }, },
"# unknown schemes and backslashes", "# unknown schemes and backslashes",
{ {
"input": "sc:\\../", "input": "sc:\\../",
@ -5327,20 +5327,20 @@ module.exports =
"search": "", "search": "",
"hash": "" "hash": ""
}, },
// { {
// "input": "/..//localhost//pig", "input": "/..//localhost//pig",
// "base": "file://lion/", "base": "file://lion/",
// "href": "file://lion/localhost//pig", "href": "file://lion/localhost//pig",
// "protocol": "file:", "protocol": "file:",
// "username": "", "username": "",
// "password": "", "password": "",
// "host": "lion", "host": "lion",
// "hostname": "lion", "hostname": "lion",
// "port": "", "port": "",
// "pathname": "/localhost//pig", "pathname": "/localhost//pig",
// "search": "", "search": "",
// "hash": "" "hash": ""
// }, },
{ {
"input": "file://", "input": "file://",
"base": "file://ape/", "base": "file://ape/",
@ -5356,34 +5356,34 @@ module.exports =
"hash": "" "hash": ""
}, },
"# File URLs with non-empty hosts", "# File URLs with non-empty hosts",
// { {
// "input": "/rooibos", "input": "/rooibos",
// "base": "file://tea/", "base": "file://tea/",
// "href": "file://tea/rooibos", "href": "file://tea/rooibos",
// "protocol": "file:", "protocol": "file:",
// "username": "", "username": "",
// "password": "", "password": "",
// "host": "tea", "host": "tea",
// "hostname": "tea", "hostname": "tea",
// "port": "", "port": "",
// "pathname": "/rooibos", "pathname": "/rooibos",
// "search": "", "search": "",
// "hash": "" "hash": ""
// }, },
// { {
// "input": "/?chai", "input": "/?chai",
// "base": "file://tea/", "base": "file://tea/",
// "href": "file://tea/?chai", "href": "file://tea/?chai",
// "protocol": "file:", "protocol": "file:",
// "username": "", "username": "",
// "password": "", "password": "",
// "host": "tea", "host": "tea",
// "hostname": "tea", "hostname": "tea",
// "port": "", "port": "",
// "pathname": "/", "pathname": "/",
// "search": "?chai", "search": "?chai",
// "hash": "" "hash": ""
// }, },
"# Windows drive letter quirk with not empty host", "# Windows drive letter quirk with not empty host",
{ {
"input": "file://example.net/C:/", "input": "file://example.net/C:/",
@ -5567,109 +5567,109 @@ module.exports =
"failure": true "failure": true
}, },
"# Non-special-URL path tests", "# Non-special-URL path tests",
// { {
// "input": "sc://ñ", "input": "sc://ñ",
// "base": "about:blank", "base": "about:blank",
// "href": "sc://%C3%B1", "href": "sc://%C3%B1",
// "origin": "null", "origin": "null",
// "protocol": "sc:", "protocol": "sc:",
// "username": "", "username": "",
// "password": "", "password": "",
// "host": "%C3%B1", "host": "%C3%B1",
// "hostname": "%C3%B1", "hostname": "%C3%B1",
// "port": "", "port": "",
// "pathname": "", "pathname": "",
// "search": "", "search": "",
// "hash": "" "hash": ""
// }, },
// { {
// "input": "sc://ñ?x", "input": "sc://ñ?x",
// "base": "about:blank", "base": "about:blank",
// "href": "sc://%C3%B1?x", "href": "sc://%C3%B1?x",
// "origin": "null", "origin": "null",
// "protocol": "sc:", "protocol": "sc:",
// "username": "", "username": "",
// "password": "", "password": "",
// "host": "%C3%B1", "host": "%C3%B1",
// "hostname": "%C3%B1", "hostname": "%C3%B1",
// "port": "", "port": "",
// "pathname": "", "pathname": "",
// "search": "?x", "search": "?x",
// "hash": "" "hash": ""
// }, },
// { {
// "input": "sc://ñ#x", "input": "sc://ñ#x",
// "base": "about:blank", "base": "about:blank",
// "href": "sc://%C3%B1#x", "href": "sc://%C3%B1#x",
// "origin": "null", "origin": "null",
// "protocol": "sc:", "protocol": "sc:",
// "username": "", "username": "",
// "password": "", "password": "",
// "host": "%C3%B1", "host": "%C3%B1",
// "hostname": "%C3%B1", "hostname": "%C3%B1",
// "port": "", "port": "",
// "pathname": "", "pathname": "",
// "search": "", "search": "",
// "hash": "#x" "hash": "#x"
// }, },
// { {
// "input": "#x", "input": "#x",
// "base": "sc://ñ", "base": "sc://ñ",
// "href": "sc://%C3%B1#x", "href": "sc://%C3%B1#x",
// "origin": "null", "origin": "null",
// "protocol": "sc:", "protocol": "sc:",
// "username": "", "username": "",
// "password": "", "password": "",
// "host": "%C3%B1", "host": "%C3%B1",
// "hostname": "%C3%B1", "hostname": "%C3%B1",
// "port": "", "port": "",
// "pathname": "", "pathname": "",
// "search": "", "search": "",
// "hash": "#x" "hash": "#x"
// }, },
// { {
// "input": "?x", "input": "?x",
// "base": "sc://ñ", "base": "sc://ñ",
// "href": "sc://%C3%B1?x", "href": "sc://%C3%B1?x",
// "origin": "null", "origin": "null",
// "protocol": "sc:", "protocol": "sc:",
// "username": "", "username": "",
// "password": "", "password": "",
// "host": "%C3%B1", "host": "%C3%B1",
// "hostname": "%C3%B1", "hostname": "%C3%B1",
// "port": "", "port": "",
// "pathname": "", "pathname": "",
// "search": "?x", "search": "?x",
// "hash": "" "hash": ""
// }, },
// { {
// "input": "sc://?", "input": "sc://?",
// "base": "about:blank", "base": "about:blank",
// "href": "sc://?", "href": "sc://?",
// "protocol": "sc:", "protocol": "sc:",
// "username": "", "username": "",
// "password": "", "password": "",
// "host": "", "host": "",
// "hostname": "", "hostname": "",
// "port": "", "port": "",
// "pathname": "", "pathname": "",
// "search": "", "search": "",
// "hash": "" "hash": ""
// }, },
// { {
// "input": "sc://#", "input": "sc://#",
// "base": "about:blank", "base": "about:blank",
// "href": "sc://#", "href": "sc://#",
// "protocol": "sc:", "protocol": "sc:",
// "username": "", "username": "",
// "password": "", "password": "",
// "host": "", "host": "",
// "hostname": "", "hostname": "",
// "port": "", "port": "",
// "pathname": "", "pathname": "",
// "search": "", "search": "",
// "hash": "" "hash": ""
// }, },
{ {
"input": "///", "input": "///",
"base": "sc://x/", "base": "sc://x/",
@ -5893,34 +5893,34 @@ module.exports =
"hash": "" "hash": ""
}, },
"# percent encoded hosts in non-special-URLs", "# percent encoded hosts in non-special-URLs",
// { {
// "input": "non-special://%E2%80%A0/", "input": "non-special://%E2%80%A0/",
// "base": "about:blank", "base": "about:blank",
// "href": "non-special://%E2%80%A0/", "href": "non-special://%E2%80%A0/",
// "protocol": "non-special:", "protocol": "non-special:",
// "username": "", "username": "",
// "password": "", "password": "",
// "host": "%E2%80%A0", "host": "%E2%80%A0",
// "hostname": "%E2%80%A0", "hostname": "%E2%80%A0",
// "port": "", "port": "",
// "pathname": "/", "pathname": "/",
// "search": "", "search": "",
// "hash": "" "hash": ""
// }, },
// { {
// "input": "non-special://H%4fSt/path", "input": "non-special://H%4fSt/path",
// "base": "about:blank", "base": "about:blank",
// "href": "non-special://H%4fSt/path", "href": "non-special://H%4fSt/path",
// "protocol": "non-special:", "protocol": "non-special:",
// "username": "", "username": "",
// "password": "", "password": "",
// "host": "H%4fSt", "host": "H%4fSt",
// "hostname": "H%4fSt", "hostname": "H%4fSt",
// "port": "", "port": "",
// "pathname": "/path", "pathname": "/path",
// "search": "", "search": "",
// "hash": "" "hash": ""
// }, },
"# IPv6 in non-special-URLs", "# IPv6 in non-special-URLs",
{ {
"input": "non-special://[1:2:0:0:5:0:0:0]/", "input": "non-special://[1:2:0:0:5:0:0:0]/",

Loading…
Cancel
Save