diff options
author | Petka Antonov <petka_antonov@hotmail.com> | 2015-03-23 02:59:32 +0200 |
---|---|---|
committer | Rod Vagg <rod@vagg.org> | 2015-05-01 23:12:52 -0700 |
commit | 3fd7fc429c394059113432312ed19decbafd8fc4 (patch) | |
tree | cb4298ac837b5ce0f85791f309fd6f5698a3ef91 /lib/url.js | |
parent | 57c4cc26e2ff3dd1210d79ae22a8fe5ce43fcef3 (diff) | |
download | android-node-v8-3fd7fc429c394059113432312ed19decbafd8fc4.tar.gz android-node-v8-3fd7fc429c394059113432312ed19decbafd8fc4.tar.bz2 android-node-v8-3fd7fc429c394059113432312ed19decbafd8fc4.zip |
url: significantly improve the performance of the url module
(landed by @rvagg)
PR-URL: https://github.com/iojs/io.js/pull/1561
Reviewed-By: Domenic Denicola <domenic@domenicdenicola.com>
Reviewed-By: Rod Vagg <rod@vagg.org>
Diffstat (limited to 'lib/url.js')
-rw-r--r-- | lib/url.js | 1566 |
1 files changed, 1048 insertions, 518 deletions
diff --git a/lib/url.js b/lib/url.js index 1683f90503..f5edd89333 100644 --- a/lib/url.js +++ b/lib/url.js @@ -10,60 +10,62 @@ exports.format = urlFormat; exports.Url = Url; function Url() { - this.protocol = null; + // For more efficient internal representation and laziness. + // The non-underscore versions of these properties are accessor functions + // defined on the prototype. + this._protocol = null; + this._port = -1; + this._query = null; + this._auth = null; + this._hostname = null; + this._host = null; + this._pathname = null; + this._hash = null; + this._search = null; + this._href = ''; + + this._prependSlash = false; + this._parsesQueryStrings = false; + this.slashes = null; - this.auth = null; - this.host = null; - this.port = null; - this.hostname = null; - this.hash = null; - this.search = null; - this.query = null; - this.pathname = null; - this.path = null; - this.href = null; } // Reference: RFC 3986, RFC 1808, RFC 2396 -// define these here so at least they only have to be -// compiled once on the first module load. -const protocolPattern = /^([a-z0-9.+-]+:)/i; -const portPattern = /:[0-9]*$/; - -// Special case for a simple path URL -const simplePathPattern = /^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/; +const _protocolCharacters = makeAsciiTable([ + [0x61, 0x7A] /*a-z*/, + [0x41, 0x5A] /*A-Z*/, + 0x2E /*'.'*/, 0x2B /*'+'*/, 0x2D /*'-'*/ +]); // RFC 2396: characters reserved for delimiting URLs. // We actually just auto-escape these. -const delims = ['<', '>', '"', '`', ' ', '\r', '\n', '\t']; - // RFC 2396: characters not allowed for various reasons. -const unwise = ['{', '}', '|', '\\', '^', '`'].concat(delims); - // Allowed by RFCs, but cause of XSS attacks. Always escape these. -const autoEscape = ['\''].concat(unwise); +const _autoEscape = [ + '<', '>', '\'', '`', ' ', '\r', '\n', '\t', '{', '}', '|', '\\', '^', '`', '"' +]; -// Characters that are never ever allowed in a hostname. -// Note that any invalid chars are also handled, but these -// are the ones that are *expected* to be seen, so we fast-path them. -const nonHostChars = ['%', '/', '?', ';', '#'].concat(autoEscape); -const hostEndingChars = ['/', '?', '#']; -const hostnameMaxLen = 255; -const hostnamePartPattern = /^[+a-z0-9A-Z_-]{0,63}$/; -const hostnamePartStart = /^([+a-z0-9A-Z_-]{0,63})(.*)$/; -// protocols that can allow "unsafe" and "unwise" chars. -const unsafeProtocol = { - 'javascript': true, - 'javascript:': true -}; -// protocols that never have a hostname. -const hostlessProtocol = { - 'javascript': true, - 'javascript:': true -}; -// protocols that always contain a // bit. -const slashedProtocol = { +const _autoEscapeMap = new Array(128); + +for (let i = 0, len = _autoEscapeMap.length; i < len; ++i) { + _autoEscapeMap[i] = ''; +} + +for (let i = 0, len = _autoEscape.length; i < len; ++i) { + let c = _autoEscape[i]; + let esc = encodeURIComponent(c); + if (esc === c) + esc = escape(c); + _autoEscapeMap[c.charCodeAt(0)] = esc; +} + +// Same as autoEscapeMap except \ is not escaped but is turned into /. +const _afterQueryAutoEscapeMap = _autoEscapeMap.slice(); +_autoEscapeMap[0x5C /*'\'*/] = '/'; + +// Protocols that always contain a // bit. +const _slashProtocols = { 'http': true, 'https': true, 'ftp': true, @@ -75,355 +77,180 @@ const slashedProtocol = { 'gopher:': true, 'file:': true }; + +const _autoEscapeCharacters = makeAsciiTable(_autoEscape.map(function(v) { + return v.charCodeAt(0); +})); + +// Characters that are never ever allowed in a hostname. +// Note that any invalid chars are also handled, but these +// are the ones that are *expected* to be seen, so we fast-path them. +const _hostEndingCharacters = makeAsciiTable( + ['#', '?', '/', '\\'].map(function(v) { + return v.charCodeAt(0); + })); +// If these characters end a host name, the path will not be prepended a /. +const _hostEndingCharactersNoPrependSlash = makeAsciiTable([ + '<', '>', '"', '`', ' ', '\r', '\n', '\t', '{', '}', '|', '^', '`', '\'', '%', + ';' +].map(function(v) { + return v.charCodeAt(0); +})); + const querystring = require('querystring'); +const unserializablePropertyNames = ['_protocol', '_port', '_href', '_query', + '_prependSlash', '_auth', '_hostname', + '_pathname', '_hash', '_search', + '_parsesQueryStrings', '_host']; +const noSerializePattern = new RegExp('^(?:' + + unserializablePropertyNames.join('|') + + ')$'); + function urlParse(url, parseQueryString, slashesDenoteHost) { if (url instanceof Url) return url; - var u = new Url; + var u = new Url(); u.parse(url, parseQueryString, slashesDenoteHost); return u; } -Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) { - if (typeof url !== 'string') { - throw new TypeError("Parameter 'url' must be a string, not " + typeof url); - } - - // Copy chrome, IE, opera backslash-handling behavior. - // Back slashes before the query string get converted to forward slashes - // See: https://code.google.com/p/chromium/issues/detail?id=25916 - var queryIndex = url.indexOf('?'), - splitter = - (queryIndex !== -1 && queryIndex < url.indexOf('#')) ? '?' : '#', - uSplit = url.split(splitter), - slashRegex = /\\/g; - uSplit[0] = uSplit[0].replace(slashRegex, '/'); - url = uSplit.join(splitter); - - var rest = url; - - // trim before proceeding. - // This is to support parse stuff like " http://foo.com \n" - rest = rest.trim(); - - if (!slashesDenoteHost && url.split('#').length === 1) { - // Try fast path regexp - var simplePath = simplePathPattern.exec(rest); - if (simplePath) { - this.path = rest; - this.href = rest; - this.pathname = simplePath[1]; - if (simplePath[2]) { - this.search = simplePath[2]; - if (parseQueryString) { - this.query = querystring.parse(this.search.substr(1)); - } else { - this.query = this.search.substr(1); - } - } else if (parseQueryString) { - this.search = ''; - this.query = {}; - } - return this; - } - } - - var proto = protocolPattern.exec(rest); - if (proto) { - proto = proto[0]; - var lowerProto = proto.toLowerCase(); - this.protocol = lowerProto; - rest = rest.substr(proto.length); +Url.prototype.parse = function(str, parseQueryString, hostDenotesSlash) { + if (typeof str !== 'string') { + throw new TypeError(`Parameter 'url' must be a string, not ` + + typeof str); } - - // figure out if it's got a host - // user@server is *always* interpreted as a hostname, and url - // resolution will treat //foo/bar as host=foo,path=bar because that's - // how the browser resolves relative URLs. - if (slashesDenoteHost || proto || rest.match(/^\/\/[^@\/]+@[^@\/]+/)) { - var slashes = rest.substr(0, 2) === '//'; - if (slashes && !(proto && hostlessProtocol[proto])) { - rest = rest.substr(2); - this.slashes = true; - } - } - - if (!hostlessProtocol[proto] && - (slashes || (proto && !slashedProtocol[proto]))) { - - // there's a hostname. - // the first instance of /, ?, ;, or # ends the host. - // - // If there is an @ in the hostname, then non-host chars *are* allowed - // to the left of the last @ sign, unless some host-ending character - // comes *before* the @-sign. - // URLs are obnoxious. - // - // ex: - // http://a@b@c/ => user:a@b host:c - // http://a@b?@c => user:a host:c path:/?@c - - // v0.12 TODO(isaacs): This is not quite how Chrome does things. - // Review our test case against browsers more comprehensively. - - // find the first instance of any hostEndingChars - var hostEnd = -1; - for (var i = 0; i < hostEndingChars.length; i++) { - var hec = rest.indexOf(hostEndingChars[i]); - if (hec !== -1 && (hostEnd === -1 || hec < hostEnd)) - hostEnd = hec; - } - - // at this point, either we have an explicit point where the - // auth portion cannot go past, or the last @ char is the decider. - var auth, atSign; - if (hostEnd === -1) { - // atSign can be anywhere. - atSign = rest.lastIndexOf('@'); - } else { - // atSign must be in auth portion. - // http://a@b/c@d => host:b auth:a path:/c@d - atSign = rest.lastIndexOf('@', hostEnd); - } - - // Now we have a portion which is definitely the auth. - // Pull that off. - if (atSign !== -1) { - auth = rest.slice(0, atSign); - rest = rest.slice(atSign + 1); - this.auth = decodeURIComponent(auth); - } - - // the host is the remaining to the left of the first non-host char - hostEnd = -1; - for (var i = 0; i < nonHostChars.length; i++) { - var hec = rest.indexOf(nonHostChars[i]); - if (hec !== -1 && (hostEnd === -1 || hec < hostEnd)) - hostEnd = hec; - } - // if we still have not hit it, then the entire thing is a host. - if (hostEnd === -1) - hostEnd = rest.length; - - this.host = rest.slice(0, hostEnd); - rest = rest.slice(hostEnd); - - // pull out port. - this.parseHost(); - - // we've indicated that there is a hostname, - // so even if it's empty, it has to be present. - this.hostname = this.hostname || ''; - - // if hostname begins with [ and ends with ] - // assume that it's an IPv6 address. - var ipv6Hostname = this.hostname[0] === '[' && - this.hostname[this.hostname.length - 1] === ']'; - - // validate a little. - if (!ipv6Hostname) { - var hostparts = this.hostname.split(/\./); - for (var i = 0, l = hostparts.length; i < l; i++) { - var part = hostparts[i]; - if (!part) continue; - if (!part.match(hostnamePartPattern)) { - var newpart = ''; - for (var j = 0, k = part.length; j < k; j++) { - if (part.charCodeAt(j) > 127) { - // we replace non-ASCII char with a temporary placeholder - // we need this to make sure size of hostname is not - // broken by replacing non-ASCII by nothing - newpart += 'x'; - } else { - newpart += part[j]; - } - } - // we test again with ASCII char only - if (!newpart.match(hostnamePartPattern)) { - var validParts = hostparts.slice(0, i); - var notHost = hostparts.slice(i + 1); - var bit = part.match(hostnamePartStart); - if (bit) { - validParts.push(bit[1]); - notHost.unshift(bit[2]); - } - if (notHost.length) { - rest = '/' + notHost.join('.') + rest; - } - this.hostname = validParts.join('.'); - break; - } - } - } - } - - if (this.hostname.length > hostnameMaxLen) { - this.hostname = ''; - } else { - // hostnames are always lower case. - this.hostname = this.hostname.toLowerCase(); - } - - if (!ipv6Hostname) { - // IDNA Support: Returns a punycoded representation of "domain". - // It only converts parts of the domain name that - // have non-ASCII characters, i.e. it doesn't matter if - // you call it with a domain that already is ASCII-only. - this.hostname = punycode.toASCII(this.hostname); - } - - var p = this.port ? ':' + this.port : ''; - var h = this.hostname || ''; - this.host = h + p; - - // strip [ and ] from the hostname - // the host field still retains them, though - if (ipv6Hostname) { - this.hostname = this.hostname.substr(1, this.hostname.length - 2); - if (rest[0] !== '/') { - rest = '/' + rest; - } - } + // The field's value must always be an actual boolean. + this._parsesQueryStrings = !!parseQueryString; + var start = 0; + var end = str.length - 1; + + // Trim leading and trailing ws. + while (str.charCodeAt(start) <= 0x20 /*' '*/) start++; + var trimmedStart = start; + while (str.charCodeAt(end) <= 0x20 /*' '*/) end--; + + start = this._parseProtocol(str, start, end); + + // Javascript doesn't have host. + if (this._protocol !== 'javascript') { + start = this._parseHost(str, trimmedStart, start, end, hostDenotesSlash); + var proto = this._protocol; + if (!this._hostname && + (this.slashes || (proto && !_slashProtocols[proto]))) + this._hostname = this._host = ''; } - // now rest is set to the post-host stuff. - // chop off any delim chars. - if (!unsafeProtocol[lowerProto]) { - - // First, make 100% sure that any "autoEscape" chars get - // escaped, even if encodeURIComponent doesn't think they - // need to be. - for (var i = 0, l = autoEscape.length; i < l; i++) { - var ae = autoEscape[i]; - if (rest.indexOf(ae) === -1) - continue; - var esc = encodeURIComponent(ae); - if (esc === ae) { - esc = escape(ae); - } - rest = rest.split(ae).join(esc); - } + if (start <= end) { + var ch = str.charCodeAt(start); + if (ch === 0x2F /*'/'*/ || ch === 0x5C /*'\'*/) + this._parsePath(str, start, end); + else if (ch === 0x3F /*'?'*/) + this._parseQuery(str, start, end); + else if (ch === 0x23 /*'#'*/) + this._parseHash(str, start, end); + else if (this._protocol !== 'javascript') + this._parsePath(str, start, end); + else // For javascript the pathname is just the rest of it. + this._pathname = str.slice(start, end + 1); } - - // chop off from the tail first. - var hash = rest.indexOf('#'); - if (hash !== -1) { - // got a fragment string. - this.hash = rest.substr(hash); - rest = rest.slice(0, hash); - } - var qm = rest.indexOf('?'); - if (qm !== -1) { - this.search = rest.substr(qm); - this.query = rest.substr(qm + 1); - if (parseQueryString) { - this.query = querystring.parse(this.query); - } - rest = rest.slice(0, qm); - } else if (parseQueryString) { - // no query string, but parseQueryString still requested - this.search = ''; - this.query = {}; - } - if (rest) this.pathname = rest; - if (slashedProtocol[lowerProto] && - this.hostname && !this.pathname) { - this.pathname = '/'; + if (!this._pathname && this._hostname && _slashProtocols[this._protocol]) + this._pathname = '/'; + + if (parseQueryString) { + var search = this._search; + if (search == null) + search = this._search = ''; + if (search.charCodeAt(0) === 0x3F /*'?'*/) + search = search.slice(1); + // This calls a setter function, there is no .query data property. + this.query = querystring.parse(search); } +}; - //to support http.request - if (this.pathname || this.search) { - var p = this.pathname || ''; - var s = this.search || ''; - this.path = p + s; - } +function urlResolve(source, relative) { + return urlParse(source, false, true).resolve(relative); +} - // finally, reconstruct the href based on what has been validated. - this.href = this.format(); - return this; +Url.prototype.resolve = function(relative) { + return this.resolveObject(urlParse(relative, false, true)).format(); }; -// format a parsed object into a url string +// Format a parsed object into a url string. function urlFormat(obj) { - // ensure it's an object, and not a string url. + // Ensure it's an object, and not a string url. // If it's an obj, this is a no-op. // this way, you can call url_format() on strings // to clean up potentially wonky urls. - if (typeof obj === 'string') obj = urlParse(obj); - - else if (typeof obj !== 'object' || obj === null) - throw new TypeError("Parameter 'urlObj' must be an object, not " + + if (typeof obj === 'string') { + obj = urlParse(obj); + } else if (typeof obj !== 'object' || obj === null) { + throw new TypeError('Parameter \'urlObj\' must be an object, not ' + obj === null ? 'null' : typeof obj); - - else if (!(obj instanceof Url)) return Url.prototype.format.call(obj); + } else if (!(obj instanceof Url)) { + return Url.prototype.format.call(obj); + } return obj.format(); } Url.prototype.format = function() { var auth = this.auth || ''; + if (auth) { auth = encodeURIComponent(auth); auth = auth.replace(/%3A/i, ':'); auth += '@'; } - var protocol = this.protocol || '', - pathname = this.pathname || '', - hash = this.hash || '', - host = false, - query = ''; - - if (this.host) { - host = auth + this.host; - } else if (this.hostname) { - host = auth + (this.hostname.indexOf(':') === -1 ? - this.hostname : - '[' + this.hostname + ']'); - if (this.port) { - host += ':' + this.port; - } + var protocol = this.protocol || ''; + var pathname = this.pathname || ''; + var hash = this.hash || ''; + var search = this.search || ''; + var query = ''; + var hostname = this.hostname || ''; + var port = this.port || ''; + var host = false; + var scheme = ''; + + // Cache the result of the getter function. + var q = this.query; + if (q !== null && typeof q === 'object') { + query = querystring.stringify(q); } - if (this.query !== null && - typeof this.query === 'object' && - Object.keys(this.query).length) { - query = querystring.stringify(this.query); + if (!search) { + search = query ? '?' + query : ''; } - var search = this.search || (query && ('?' + query)) || ''; + if (protocol && protocol.charCodeAt(protocol.length - 1) !== 0x3A /*':'*/) + protocol += ':'; - if (protocol && protocol.substr(-1) !== ':') protocol += ':'; - - // only the slashedProtocols get the //. Not mailto:, xmpp:, etc. - // unless they had them to begin with. - if (this.slashes || - (!protocol || slashedProtocol[protocol]) && host !== false) { - host = '//' + (host || ''); - if (pathname && pathname.charAt(0) !== '/') pathname = '/' + pathname; - } else if (!host) { - host = ''; + if (this.host) { + host = auth + this.host; + } else if (hostname) { + host = auth + hostname + (port ? ':' + port : ''); } - if (hash && hash.charAt(0) !== '#') hash = '#' + hash; - if (search && search.charAt(0) !== '?') search = '?' + search; + var slashes = this.slashes || + ((!protocol || _slashProtocols[protocol]) && host !== false); - pathname = pathname.replace(/[?#]/g, function(match) { - return encodeURIComponent(match); - }); - search = search.replace('#', '%23'); + if (protocol) scheme = protocol + (slashes ? '//' : ''); + else if (slashes) scheme = '//'; - return protocol + host + pathname + search + hash; -}; + if (slashes && pathname && pathname.charCodeAt(0) !== 0x2F /*'/'*/) { + pathname = '/' + pathname; + } + if (search && search.charCodeAt(0) !== 0x3F /*'?'*/) + search = '?' + search; + if (hash && hash.charCodeAt(0) !== 0x23 /*'#'*/) + hash = '#' + hash; -function urlResolve(source, relative) { - return urlParse(source, false, true).resolve(relative); -} + pathname = escapePathName(pathname); + search = escapeSearch(search); -Url.prototype.resolve = function(relative) { - return this.resolveObject(urlParse(relative, false, true)).format(); + return scheme + (host === false ? '' : host) + pathname + search + hash; }; function urlResolveObject(source, relative) { @@ -432,51 +259,35 @@ function urlResolveObject(source, relative) { } Url.prototype.resolveObject = function(relative) { - if (typeof relative === 'string') { - var rel = new Url(); - rel.parse(relative, false, true); - relative = rel; - } + if (typeof relative === 'string') + relative = urlParse(relative, false, true); - var result = new Url(); - var tkeys = Object.keys(this); - for (var tk = 0; tk < tkeys.length; tk++) { - var tkey = tkeys[tk]; - result[tkey] = this[tkey]; - } + var result = this._clone(); - // hash is always overridden, no matter what. - // even href="" will remove it. - result.hash = relative.hash; + // Hash is always overridden, no matter what. + // even href='' will remove it. + result._hash = relative._hash; - // if the relative url is empty, then there's nothing left to do here. - if (relative.href === '') { - result.href = result.format(); + // If the relative url is empty, then there's nothing left to do here. + if (!relative.href) { + result._href = ''; return result; } - // hrefs like //foo/bar always cut to the protocol. - if (relative.slashes && !relative.protocol) { - // take everything except the protocol from relative - var rkeys = Object.keys(relative); - for (var rk = 0; rk < rkeys.length; rk++) { - var rkey = rkeys[rk]; - if (rkey !== 'protocol') - result[rkey] = relative[rkey]; - } + // Hrefs like //foo/bar always cut to the protocol. + if (relative.slashes && !relative._protocol) { + relative._copyPropsTo(result, true); - //urlParse appends trailing / to urls like http://www.example.com - if (slashedProtocol[result.protocol] && - result.hostname && !result.pathname) { - result.path = result.pathname = '/'; + if (_slashProtocols[result._protocol] && + result._hostname && !result._pathname) { + result._pathname = '/'; } - - result.href = result.format(); + result._href = ''; return result; } - if (relative.protocol && relative.protocol !== result.protocol) { - // if it's a known url protocol, then changing + if (relative._protocol && relative._protocol !== result._protocol) { + // If it's a known url protocol, then changing // the protocol does weird things // first, if it's not file:, then we MUST have a host, // and if there was a path @@ -484,168 +295,147 @@ Url.prototype.resolveObject = function(relative) { // if it is file:, then the host is dropped, // because that's known to be hostless. // anything else is assumed to be absolute. - if (!slashedProtocol[relative.protocol]) { - var keys = Object.keys(relative); - for (var v = 0; v < keys.length; v++) { - var k = keys[v]; - result[k] = relative[k]; - } - result.href = result.format(); + if (!_slashProtocols[relative._protocol]) { + relative._copyPropsTo(result, false); + result._href = ''; return result; } - result.protocol = relative.protocol; - if (!relative.host && - !/^file:?$/.test(relative.protocol) && - !hostlessProtocol[relative.protocol]) { - var relPath = (relative.pathname || '').split('/'); - while (relPath.length && !(relative.host = relPath.shift())); - if (!relative.host) relative.host = ''; - if (!relative.hostname) relative.hostname = ''; + result._protocol = relative._protocol; + if (!relative._host && + !/^file:?$/.test(relative._protocol) && + relative._protocol !== 'javascript') { + var relPath = (relative._pathname || '').split('/'); + while (relPath.length && !(relative._host = relPath.shift())); + if (!relative._host) relative._host = ''; + if (!relative._hostname) relative._hostname = ''; if (relPath[0] !== '') relPath.unshift(''); if (relPath.length < 2) relPath.unshift(''); - result.pathname = relPath.join('/'); + result._pathname = relPath.join('/'); } else { - result.pathname = relative.pathname; - } - result.search = relative.search; - result.query = relative.query; - result.host = relative.host || ''; - result.auth = relative.auth; - result.hostname = relative.hostname || relative.host; - result.port = relative.port; - // to support http.request - if (result.pathname || result.search) { - var p = result.pathname || ''; - var s = result.search || ''; - result.path = p + s; + result._pathname = relative._pathname; } + + result._search = relative._search; + result._host = relative._host || ''; + result._auth = relative._auth; + result._hostname = relative._hostname || relative._host; + result._port = relative._port; result.slashes = result.slashes || relative.slashes; - result.href = result.format(); + result._href = ''; return result; } - var isSourceAbs = (result.pathname && result.pathname.charAt(0) === '/'), - isRelAbs = ( - relative.host || - relative.pathname && relative.pathname.charAt(0) === '/' - ), - mustEndAbs = (isRelAbs || isSourceAbs || - (result.host && relative.pathname)), - removeAllDots = mustEndAbs, - srcPath = result.pathname && result.pathname.split('/') || [], - relPath = relative.pathname && relative.pathname.split('/') || [], - psychotic = result.protocol && !slashedProtocol[result.protocol]; + var isSourceAbs = + (result._pathname && result._pathname.charCodeAt(0) === 0x2F /*'/'*/); + var isRelAbs = ( + relative._host || + (relative._pathname && + relative._pathname.charCodeAt(0) === 0x2F /*'/'*/)); + var mustEndAbs = (isRelAbs || + isSourceAbs || + (result._host && relative._pathname)); - // if the url is a non-slashed url, then relative + var removeAllDots = mustEndAbs; + + var srcPath = result._pathname && result._pathname.split('/') || []; + var relPath = relative._pathname && relative._pathname.split('/') || []; + var psychotic = result._protocol && !_slashProtocols[result._protocol]; + + // If the url is a non-slashed url, then relative // links like ../.. should be able // to crawl up to the hostname, as well. This is strange. // result.protocol has already been set by now. // Later on, put the first path part into the host field. if (psychotic) { - result.hostname = ''; - result.port = null; - if (result.host) { - if (srcPath[0] === '') srcPath[0] = result.host; - else srcPath.unshift(result.host); + result._hostname = ''; + result._port = -1; + if (result._host) { + if (srcPath[0] === '') srcPath[0] = result._host; + else srcPath.unshift(result._host); } - result.host = ''; - if (relative.protocol) { - relative.hostname = null; - relative.port = null; - if (relative.host) { - if (relPath[0] === '') relPath[0] = relative.host; - else relPath.unshift(relative.host); + result._host = ''; + if (relative._protocol) { + relative._hostname = ''; + relative._port = -1; + if (relative._host) { + if (relPath[0] === '') relPath[0] = relative._host; + else relPath.unshift(relative._host); } - relative.host = null; + relative._host = ''; } mustEndAbs = mustEndAbs && (relPath[0] === '' || srcPath[0] === ''); } if (isRelAbs) { // it's absolute. - result.host = (relative.host || relative.host === '') ? - relative.host : result.host; - result.hostname = (relative.hostname || relative.hostname === '') ? - relative.hostname : result.hostname; - result.search = relative.search; - result.query = relative.query; + result._host = relative._host ? relative._host : result._host; + result._hostname = + relative._hostname ? relative._hostname : result._hostname; + result._search = relative._search; srcPath = relPath; - // fall through to the dot-handling below. + // Fall through to the dot-handling below. } else if (relPath.length) { - // it's relative + // It's relative // throw away the existing file, and take the new path instead. if (!srcPath) srcPath = []; srcPath.pop(); srcPath = srcPath.concat(relPath); - result.search = relative.search; - result.query = relative.query; - } else if (relative.search !== null && relative.search !== undefined) { - // just pull out the search. + result._search = relative._search; + } else if (relative._search) { + // Just pull out the search. // like href='?foo'. // Put this after the other two cases because it simplifies the booleans if (psychotic) { - result.hostname = result.host = srcPath.shift(); - //occationaly the auth can get stuck only in host - //this especialy happens in cases like - //url.resolveObject('mailto:local1@domain1', 'local2@domain2') - var authInHost = result.host && result.host.indexOf('@') > 0 ? - result.host.split('@') : false; + result._hostname = result._host = srcPath.shift(); + // Occasionally the auth can get stuck only in host + // this especialy happens in cases like + // url.resolveObject('mailto:local1@domain1', 'local2@domain2'). + var authInHost = result._host && result._host.indexOf('@') > 0 ? + result._host.split('@') : false; if (authInHost) { - result.auth = authInHost.shift(); - result.host = result.hostname = authInHost.shift(); + result._auth = authInHost.shift(); + result._host = result._hostname = authInHost.shift(); } } - result.search = relative.search; - result.query = relative.query; - //to support http.request - if (result.pathname !== null || result.search !== null) { - result.path = (result.pathname ? result.pathname : '') + - (result.search ? result.search : ''); - } - result.href = result.format(); + result._search = relative._search; + result._href = ''; return result; } if (!srcPath.length) { - // no path at all. easy. + // No path at all. easy. // we've already handled the other stuff above. - result.pathname = null; - //to support http.request - if (result.search) { - result.path = '/' + result.search; - } else { - result.path = null; - } - result.href = result.format(); + result._pathname = null; + result._href = ''; return result; } - // if a url ENDs in . or .., then it must get a trailing slash. + // If a url ENDs in . or .., then it must get a trailing slash. // however, if it ends in anything else non-slashy, // then it must NOT get a trailing slash. var last = srcPath.slice(-1)[0]; var hasTrailingSlash = ( - (result.host || relative.host || srcPath.length > 1) && + (result._host || relative._host || srcPath.length > 1) && (last === '.' || last === '..') || last === ''); - // strip single dots, resolve double dots to parent dir - // if the path tries to go above the root, `up` ends up > 0 + // Strip single dots, resolve double dots to parent dir + // if the path tries to go above the root, `up` ends up > 0. var up = 0; for (var i = srcPath.length; i >= 0; i--) { last = srcPath[i]; if (last === '.') { - spliceOne(srcPath, i); + srcPath.splice(i, 1); } else if (last === '..') { - spliceOne(srcPath, i); + srcPath.splice(i, 1); up++; } else if (up) { - spliceOne(srcPath, i); + srcPath.splice(i, 1); up--; } } - // if the path is allowed to go above the root, restore leading ..s + // If the path is allowed to go above the root, restore leading ..s. if (!mustEndAbs && !removeAllDots) { for (; up--; up) { srcPath.unshift('..'); @@ -653,7 +443,7 @@ Url.prototype.resolveObject = function(relative) { } if (mustEndAbs && srcPath[0] !== '' && - (!srcPath[0] || srcPath[0].charAt(0) !== '/')) { + (!srcPath[0] || srcPath[0].charCodeAt(0) !== 0x2F /*'/'*/)) { srcPath.unshift(''); } @@ -662,63 +452,803 @@ Url.prototype.resolveObject = function(relative) { } var isAbsolute = srcPath[0] === '' || - (srcPath[0] && srcPath[0].charAt(0) === '/'); + (srcPath[0] && srcPath[0].charCodeAt(0) === 0x2F /*'/'*/); // put the host back if (psychotic) { - result.hostname = result.host = isAbsolute ? '' : - srcPath.length ? srcPath.shift() : ''; - //occationaly the auth can get stuck only in host - //this especialy happens in cases like - //url.resolveObject('mailto:local1@domain1', 'local2@domain2') - var authInHost = result.host && result.host.indexOf('@') > 0 ? - result.host.split('@') : false; + result._hostname = result._host = isAbsolute ? '' : + srcPath.length ? srcPath.shift() : ''; + // Occasionally the auth can get stuck only in host. + // This especialy happens in cases like + // url.resolveObject('mailto:local1@domain1', 'local2@domain2'). + var authInHost = result._host && result._host.indexOf('@') > 0 ? + result._host.split('@') : false; if (authInHost) { - result.auth = authInHost.shift(); - result.host = result.hostname = authInHost.shift(); + result._auth = authInHost.shift(); + result._host = result._hostname = authInHost.shift(); } } - mustEndAbs = mustEndAbs || (result.host && srcPath.length); + mustEndAbs = mustEndAbs || (result._host && srcPath.length); if (mustEndAbs && !isAbsolute) { srcPath.unshift(''); } - if (!srcPath.length) { - result.pathname = null; - result.path = null; - } else { - result.pathname = srcPath.join('/'); + result._pathname = srcPath.length === 0 ? null : srcPath.join('/'); + result._auth = relative._auth || result._auth; + result.slashes = result.slashes || relative.slashes; + result._href = ''; + return result; +}; + + +Url.prototype._parseProtocol = function(str, start, end) { + var needsLowerCasing = false; + var protocolCharacters = _protocolCharacters; + + for (var i = start; i <= end; ++i) { + var ch = str.charCodeAt(i); + + if (ch === 0x3A /*':'*/) { + if (i - start === 0) + return start; + var protocol = str.slice(start, i); + if (needsLowerCasing) protocol = protocol.toLowerCase(); + this._protocol = protocol; + return i + 1; + } else if (protocolCharacters[ch] === 1) { + if (ch < 0x61 /*'a'*/) + needsLowerCasing = true; + } else { + return start; + } + } + return start; +}; - //to support request.http - if (result.pathname !== null || result.search !== null) { - result.path = (result.pathname ? result.pathname : '') + - (result.search ? result.search : ''); +Url.prototype._parseAuth = function(str, start, end, decode) { + var auth = str.slice(start, end + 1); + if (decode) { + auth = decodeURIComponent(auth); } - result.auth = relative.auth || result.auth; - result.slashes = result.slashes || relative.slashes; - result.href = result.format(); - return result; + this._auth = auth; +}; + +Url.prototype._parsePort = function(str, start, end) { + // Distinguish between :0 and : (no port number at all). + var hadChars = false; + var hadValidPortTerminator = true; + var checkLeadingZeroes = false; + + for (var i = start; i <= end; ++i) { + var ch = str.charCodeAt(i); + + if (0x30 /*'0'*/ <= ch && ch <= 0x39 /*'9'*/) { + if (i === start && ch === 0x30 /*'0'*/) + checkLeadingZeroes = true; + hadChars = true; + } else { + hadValidPortTerminator = false; + if (_hostEndingCharacters[ch] === 1 || + _hostEndingCharactersNoPrependSlash[ch] === 1) { + hadValidPortTerminator = true; + } else { + this._port = -2; + } + break; + } + + } + + if (!hadChars || !hadValidPortTerminator) + return 0; + + var portEnd = i; + var port = str.slice(start, portEnd); + + if (checkLeadingZeroes) { + var hadNonZero = false; + for (var i = 0; i < port.length; ++i) { + if (port.charCodeAt(i) !== 0x30 /*'0'*/) { + hadNonZero = true; + break; + } + } + + if (hadNonZero) + port = -1; + else + port = '0'; + } + this._port = port; + return portEnd - start; +}; + +Url.prototype._hasValidPort = function() { + return typeof this._port === 'string'; +}; + +Url.prototype._parseHost = function(str, + trimmedStart, + start, + end, + slashesDenoteHost) { + var hostEndingCharacters = _hostEndingCharacters; + var first = str.charCodeAt(start); + var second = str.charCodeAt(start + 1); + if ((first === 0x2F /*'/'*/ || first === 0x5C /*'\'*/) && + (second === 0x2F /*'/'*/ || second === 0x5C /*'\'*/)) { + this.slashes = true; + + // The string starts with // or \\. + if (start === trimmedStart) { + // The string is just '//' or '\\'. + if (end - start === 1) return start; + // If slashes do not denote host and there is no auth, + // there is no host when the string starts with // or \\. + var hasAuth = + containsCharacter(str, + 0x40 /*'@'*/, + trimmedStart + 2, + hostEndingCharacters); + if (!hasAuth && !slashesDenoteHost) { + this.slashes = null; + return start; + } + } + // There is a host that starts after the //. + start += 2; + } + // If there is no slashes, there is no hostname if + // 1. there was no protocol at all. + else if (!this._protocol || + // 2. there was a protocol that requires slashes + // e.g. in 'http:asd' 'asd' is not a hostname. + _slashProtocols[this._protocol] + ) { + return start; + } + + var needsLowerCasing = false; + var idna = false; + var hostNameStart = start; + var hostNameEnd = end; + var lastCh = -1; + var portLength = 0; + var charsAfterDot = 0; + var authNeedsDecoding = false; + + var j = -1; + + // Find the last occurrence of an @-sign until hostending character is met + // also mark if decoding is needed for the auth portion. + for (var i = start; i <= end; ++i) { + var ch = str.charCodeAt(i); + if (ch === 0x40 /*'@'*/) + j = i; + else if (ch === 0x25 /*'%'*/) + authNeedsDecoding = true; + else if (hostEndingCharacters[ch] === 1) + break; + } + + // @-sign was found at index j, everything to the left from it + // is auth part. + if (j > -1) { + this._parseAuth(str, start, j - 1, authNeedsDecoding); + // Hostname starts after the last @-sign- + start = hostNameStart = j + 1; + } + + // Host name is starting with a [. + if (str.charCodeAt(start) === 0x5B /*'['*/) { + for (var i = start + 1; i <= end; ++i) { + var ch = str.charCodeAt(i); + + // Assume valid IP6 is between the brackets. + if (ch === 0x5D /*']'*/) { + if (str.charCodeAt(i + 1) === 0x3A /*':'*/) + portLength = this._parsePort(str, i + 2, end) + 1; + + var hostname = '[' + str.slice(start + 1, i).toLowerCase() + ']'; + this._hostname = hostname; + this._host = + this._hasValidPort() ? hostname + ':' + this._port : hostname; + this._pathname = '/'; + return i + portLength + 1; + } + } + // Empty hostname, [ starts a path. + return start; + } + var hostEndingCharactersNoPrependSlash = _hostEndingCharactersNoPrependSlash; + for (var i = start; i <= end; ++i) { + if (charsAfterDot > 62) { + this._hostname = this._host = str.slice(start, i); + return i; + } + var ch = str.charCodeAt(i); + + if (ch === 0x3A /*':'*/) { + portLength = this._parsePort(str, i + 1, end) + 1; + hostNameEnd = i - 1; + break; + } else if (ch < 0x61 /*'a'*/) { + if (ch === 0x2E /*'.'*/) { + // TODO(petkaantonov) This error is originally ignored: + // if (lastCh === 0x2E /*'.'*/ || lastCh === -1) { + // this.hostname = this.host = ''; + // return start; + // } + charsAfterDot = -1; + } else if (0x41 /*'A'*/ <= ch && ch <= 0x5A /*'Z'*/) { + needsLowerCasing = true; + } + // Valid characters other than ASCII letters -, _, +, 0-9. + else if (!(ch === 0x2D /*'-'*/ || + ch === 0x5F /*'_'*/ || + ch === 0x2B /*'+'*/ || + (0x30 /*'0'*/ <= ch && ch <= 0x39 /*'9'*/))) { + if (hostEndingCharacters[ch] === 0 && + hostEndingCharactersNoPrependSlash[ch] === 0) + this._prependSlash = true; + hostNameEnd = i - 1; + break; + } + } else if (ch >= 0x7B /*'{'*/) { + if (ch <= 0x7E /*'~'*/) { + if (hostEndingCharactersNoPrependSlash[ch] === 0) { + this._prependSlash = true; + } + hostNameEnd = i - 1; + break; + } + idna = true; + needsLowerCasing = true; + } + lastCh = ch; + charsAfterDot++; + } + + // TODO(petkaantonov) This error is originally ignored: + // if (lastCh === 0x2E /*'.'*/) + // hostNameEnd-- + + if (hostNameEnd + 1 !== start && + hostNameEnd - hostNameStart <= 256) { + var hostname = str.slice(hostNameStart, hostNameEnd + 1); + if (needsLowerCasing) + hostname = hostname.toLowerCase(); + if (idna) + hostname = punycode.toASCII(hostname); + + this._hostname = hostname; + this._host = + this._hasValidPort() ? hostname + ':' + this._port : hostname; + } + + return hostNameEnd + 1 + portLength; + +}; + +Url.prototype._copyPropsTo = function(target, noProtocol) { + if (!noProtocol) { + target._protocol = this._protocol; + } + // Forces getter recalculation. + target._href = null; + target._host = this._host; + target._hostname = this._hostname; + target._pathname = this._pathname; + target._search = this._search; + target._parsesQueryStrings = this._parsesQueryStrings; + target._prependSlash = this._prependSlash; + target._port = this._port; + target._auth = this._auth; + target.slashes = this.slashes; + target._query = this._query; + target._hash = this._hash; +}; + +Url.prototype._clone = function() { + var ret = new Url(); + ret._protocol = this._protocol; + ret._href = this._href; + ret._port = this._port; + ret._prependSlash = this._prependSlash; + ret._auth = this._auth; + ret._hostname = this._hostname; + ret._host = this._host; + ret._hash = this._hash; + ret._search = this._search; + ret._pathname = this._pathname; + ret._parsesQueryStrings = this._parsesQueryStrings; + ret._query = this._query; + ret.slashes = this.slashes; + return ret; +}; + +Url.prototype.toJSON = function() { + var ret = { + href: this.href, + pathname: this.pathname, + path: this.path, + protocol: this.protocol, + query: this.query, + port: this.port, + auth: this.auth, + hash: this.hash, + host: this.host, + hostname: this.hostname, + search: this.search, + slashes: this.slashes + }; + var keys = Object.keys(this); + for (var i = 0; i < keys.length; ++i) { + var key = keys[i]; + if (noSerializePattern.test(key)) continue; + ret[key] = this[key]; + + } + return ret; }; -Url.prototype.parseHost = function() { - var host = this.host; - var port = portPattern.exec(host); - if (port) { - port = port[0]; - if (port !== ':') { - this.port = port.substr(1); +Url.prototype._parsePath = function(str, start, end) { + var pathStart = start; + var pathEnd = end; + var escape = false; + var autoEscapeCharacters = _autoEscapeCharacters; + var prePath = this._port === -2 ? '/:' : ''; + + for (var i = start; i <= end; ++i) { + var ch = str.charCodeAt(i); + + if (ch === 0x23 /*'#'*/) { + this._parseHash(str, i, end); + pathEnd = i - 1; + break; + } else if (ch === 0x3F /*'?'*/) { + this._parseQuery(str, i, end); + pathEnd = i - 1; + break; + } else if (!escape && autoEscapeCharacters[ch] === 1) { + escape = true; } - host = host.substr(0, host.length - port.length); } - if (host) this.hostname = host; + + if (pathStart > pathEnd) { + this._pathname = prePath === '' ? '/' : prePath; + return; + } + + var path; + if (escape) { + path = getComponentEscaped(str, pathStart, pathEnd, false); + } else { + path = str.slice(pathStart, pathEnd + 1); + } + + this._pathname = prePath === '' ? + (this._prependSlash ? '/' + path : path) : prePath + path; }; -// About 1.5x faster than the two-arg version of Array#splice(). -function spliceOne(list, index) { - for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) - list[i] = list[k]; - list.pop(); +Url.prototype._parseQuery = function(str, start, end) { + var queryStart = start; + var queryEnd = end; + var escape = false; + var autoEscapeCharacters = _autoEscapeCharacters; + + for (var i = start; i <= end; ++i) { + var ch = str.charCodeAt(i); + + if (ch === 0x23 /*'#'*/) { + this._parseHash(str, i, end); + queryEnd = i - 1; + break; + } else if (!escape && autoEscapeCharacters[ch] === 1) + escape = true; + } + + if (queryStart > queryEnd) { + this._search = ''; + return; + } + + var query = escape ? + getComponentEscaped(str, queryStart, queryEnd, true) : + str.slice(queryStart, queryEnd + 1); + + this._search = query; +}; + +Url.prototype._parseHash = function(str, start, end) { + if (start > end) { + this._hash = ''; + return; + } + this._hash = getComponentEscaped(str, start, end, true); +}; + +Object.defineProperty(Url.prototype, 'port', { + get: function() { + if (this._hasValidPort()) + return this._port; + + return null; + }, + set: function(v) { + if (v === null) { + this._port = -1; + if (this._host) + this._host = null; + } else { + this._port = toPortNumber(v); + + if (this._hostname) + this._host = this._hostname + ':' + this._port; + else + this._host = ':' + this._port; + } + this._href = ''; + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(Url.prototype, 'query', { + get: function() { + var query = this._query; + if (query != null) + return query; + + var search = this._search; + + if (search) { + if (search.charCodeAt(0) === 0x3F /*'?'*/) + search = search.slice(1); + if (search !== '') { + this._query = search; + return search; + } + } + return search; + }, + set: function(v) { + if (typeof v === 'string') { + if (v !== '') this.search = '?' + v; + this._query = v; + } else if (v !== null && typeof v === 'object') { + var string = querystring.stringify(v); + this._search = string !== '' ? '?' + querystring.stringify(v) : ''; + this._query = v; + } else { + this._query = this._search = null; + } + this._href = ''; + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(Url.prototype, 'path', { + get: function() { + var p = this.pathname || ''; + var s = this.search || ''; + if (p || s) + return p + s; + return (p == null && s) ? ('/' + s) : null; + }, + set: function(v) { + if (v === null) { + this._pathname = this._search = null; + return; + } + var path = '' + v; + var matches = path.match(/([^\?]*)(\?.*)$/); + + if (matches) { + this.pathname = matches[1]; + this.search = matches[2]; + } else { + this.pathname = path; + this.search = null; + } + this._href = ''; + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(Url.prototype, 'protocol', { + get: function() { + var proto = this._protocol; + + if (typeof proto === 'string' && proto.length > 0) { + if (proto.charCodeAt(proto.length - 1) !== 0x3A/*':'*/) + return proto + ':'; + return proto; + } + return proto; + }, + set: function(v) { + if (v === null) { + this._protocol = null; + } else { + var proto = '' + v; + if (proto.length > 0) { + if (proto.charCodeAt(proto.length - 1) !== 0x3A /*':'*/) + proto = proto + ':'; + this._parseProtocol(proto, 0, proto.length - 1); + this._href = ''; + } + } + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(Url.prototype, 'href', { + get: function() { + var href = this._href; + if (!href) + href = this._href = this.format(); + return href; + }, + set: function(v) { + this._href = ''; + var parsesQueryStrings = this._parsesQueryStrings; + // Reset properties. + Url.call(this); + if (v !== null && v !== '') + this.parse('' + v, parsesQueryStrings, false); + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(Url.prototype, 'auth', { + get: function() { + return this._auth; + }, + set: function(v) { + this._auth = v === null ? null : '' + v; + this._href = ''; + }, + enumerable: true, + configurable: true +}); + +// host = hostname + port. +Object.defineProperty(Url.prototype, 'host', { + get: function() { + return this._host; + }, + set: function(v) { + if (v === null) { + this._port = -1; + this._hostname = this._host = null; + } else { + var host = '' + v; + var matches = host.match(/(.*):(.+)$/); + + if (matches) { + this._hostname = encodeURIComponent(matches[1]); + this._port = toPortNumber(matches[2]); + this._host = this._hostname + ':' + this._port; + } else { + this._port = -1; + this._hostname = this._host = encodeURIComponent(host); + } + this._href = ''; + } + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(Url.prototype, 'hostname', { + get: function() { + return this._hostname; + }, + set: function(v) { + if (v === null) { + this._hostname = null; + + if (this._hasValidPort()) + this._host = ':' + this._port; + else + this._host = null; + } else { + var hostname = encodeURIComponent('' + v); + this._hostname = hostname; + + if (this._hasValidPort()) + this._host = hostname + ':' + this._port; + else + this._host = hostname; + + this._href = ''; + } + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(Url.prototype, 'hash', { + get: function() { + return this._hash; + }, + set: function(v) { + if (v === null) { + this._hash = null; + } else { + var hash = '' + v; + if (hash.charCodeAt(0) !== 0x23 /*'#'*/) { + hash = '#' + hash; + } + this._hash = hash; + this._href = ''; + } + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(Url.prototype, 'search', { + get: function() { + return this._search; + }, + set: function(v) { + if (v === null) { + this._search = this._query = null; + } else { + var search = escapeSearch('' + v); + + if (search.charCodeAt(0) !== 0x3F /*'?'*/) + search = '?' + search; + + this._search = search; + + if (this._parsesQueryStrings) { + this.query = querystring.parse(search.slice(1)); + } + this._href = ''; + } + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(Url.prototype, 'pathname', { + get: function() { + return this._pathname; + }, + set: function(v) { + if (v === null) { + this._pathname = null; + } else { + var pathname = escapePathName('' + v).replace(/\\/g, '/'); + + if (this.hostname && + _slashProtocols[this._protocol] && + pathname.charCodeAt(0) !== 0x2F /*'/'*/) { + pathname = '/' + pathname; + } + + this._pathname = pathname; + this._href = ''; + } + }, + enumerable: true, + configurable: true +}); + +// Search `char1` (integer code for a character) in `string` +// starting from `fromIndex` and ending at `string.length - 1` +// or when a stop character is found. +function containsCharacter(string, char1, fromIndex, stopCharacterTable) { + var len = string.length; + for (var i = fromIndex; i < len; ++i) { + var ch = string.charCodeAt(i); + + if (ch === char1) + return true; + else if (stopCharacterTable[ch] === 1) + return false; + } + return false; +} + +// See if `char1` or `char2` (integer codes for characters) +// is contained in `string`. +function containsCharacter2(string, char1, char2) { + for (var i = 0, len = string.length; i < len; ++i) { + var ch = string.charCodeAt(i); + if (ch === char1 || ch === char2) + return true; + } + return false; +} + +// Makes an array of 128 uint8's which represent boolean values. +// Spec is an array of ascii code points or ascii code point ranges +// ranges are expressed as [start, end]. + +// For example, to create a table with the characters +// 0x30-0x39 (decimals '0' - '9') and +// 0x7A (lowercaseletter 'z') as `true`: + +// var a = makeAsciiTable([[0x30, 0x39], 0x7A]); +// a[0x30]; //1 +// a[0x15]; //0 +// a[0x35]; //1 +function makeAsciiTable(spec) { + var ret = new Uint8Array(128); + spec.forEach(function(item) { + if (typeof item === 'number') { + ret[item] = 1; + } else { + var start = item[0]; + var end = item[1]; + for (var j = start; j <= end; ++j) { + ret[j] = 1; + } + } + }); + + return ret; } + +function escapePathName(pathname) { + if (!containsCharacter2(pathname, 0x23 /*'#'*/, 0x3F /*'?'*/)) + return pathname; + + return pathname.replace(/[?#]/g, function(match) { + return encodeURIComponent(match); + }); +} + +function escapeSearch(search) { + if (!containsCharacter2(search, 0x23 /*'#'*/, -1)) + return search; + + return search.replace(/#/g, function(match) { + return encodeURIComponent(match); + }); +} + +function getComponentEscaped(str, start, end, isAfterQuery) { + var cur = start; + var i = start; + var ret = ''; + var autoEscapeMap = isAfterQuery ? _afterQueryAutoEscapeMap : _autoEscapeMap; + for (; i <= end; ++i) { + var ch = str.charCodeAt(i); + var escaped = autoEscapeMap[ch]; + + if (escaped !== '' && escaped !== undefined) { + if (cur < i) ret += str.slice(cur, i); + ret += escaped; + cur = i + 1; + } + } + if (cur < i + 1) ret += str.slice(cur, i); + return ret; +} + +function toPortNumber(v) { + v = parseInt(v); + if (!Number.isFinite(v)) + v = 0; + + v = Math.max(0, v) % 0x10000; + return '' + v; +} + +// Optimize back from normalized object caused by non-identifier keys. +function FakeConstructor() {} +FakeConstructor.prototype = _slashProtocols; +/*jshint nonew: false */ +new FakeConstructor(); |