diff options
author | 2022-06-25 04:59:49 -0700 | |
---|---|---|
committer | 2022-06-25 04:59:49 -0700 | |
commit | 31cfcf2c9f40520dac72530ec62e765d3a0de221 (patch) | |
tree | 95a0bce1586135aad775b83661770fd6349c8a4d | |
parent | 5bd5678ba38e168153d5ea1f093a344af1ae458d (diff) | |
download | bun-31cfcf2c9f40520dac72530ec62e765d3a0de221.tar.gz bun-31cfcf2c9f40520dac72530ec62e765d3a0de221.tar.zst bun-31cfcf2c9f40520dac72530ec62e765d3a0de221.zip |
Better `node:url` polyfill
-rw-r--r-- | src/bun.js/javascript.zig | 31 | ||||
-rw-r--r-- | src/bun.js/url.exports.js | 2 | ||||
-rw-r--r-- | src/node-fallbacks/url.js | 808 |
3 files changed, 832 insertions, 9 deletions
diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 255d50e33..b9b01cc1e 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -828,6 +828,17 @@ pub const VirtualMachine = struct { .hash = 0, }; }, + .@"node:url" => { + return ResolvedSource{ + .allocator = null, + .source_code = ZigString.init( + @as(string, @embedFile("url.exports.js")), + ), + .specifier = ZigString.init("node:url"), + .source_url = ZigString.init("node:url"), + .hash = 0, + }; + }, .@"bun:sqlite" => { return ResolvedSource{ .allocator = null, @@ -2674,21 +2685,22 @@ pub const JSPrivateDataTag = JSPrivateDataPtr.Tag; pub const HardcodedModule = enum { @"bun:ffi", + @"bun:jsc", @"bun:main", - @"node:fs", - @"node:path", - @"detect-libc", @"bun:sqlite", - @"bun:jsc", + @"detect-libc", + @"node:fs", + @"node:fs/promises", @"node:module", + @"node:path", @"node:perf_hooks", - @"ws", + @"node:streams/consumer", + @"node:streams/web", @"node:timers", @"node:timers/promises", - @"node:streams/web", - @"node:streams/consumer", - @"node:fs/promises", + @"node:url", @"undici", + @"ws", pub const Map = bun.ComptimeStringMap( HardcodedModule, @@ -2712,6 +2724,7 @@ pub const HardcodedModule = enum { .{ "node:streams/web", HardcodedModule.@"node:streams/web" }, .{ "node:timers", HardcodedModule.@"node:timers" }, .{ "node:timers/promises", HardcodedModule.@"node:timers/promises" }, + .{ "node:url", HardcodedModule.@"node:url" }, .{ "path", HardcodedModule.@"node:path" }, .{ "undici", HardcodedModule.@"undici" }, .{ "ws", HardcodedModule.@"ws" }, @@ -2739,6 +2752,7 @@ pub const HardcodedModule = enum { .{ "node:streams/web", "node:streams/web" }, .{ "node:timers", "node:timers" }, .{ "node:timers/promises", "node:timers/promises" }, + .{ "node:url", "node:url" }, .{ "path", "node:path" }, .{ "perf_hooks", "node:perf_hooks" }, .{ "streams/consumer", "node:streams/consumer" }, @@ -2746,6 +2760,7 @@ pub const HardcodedModule = enum { .{ "timers", "node:timers" }, .{ "timers/promises", "node:timers/promises" }, .{ "undici", "undici" }, + .{ "url", "node:url" }, .{ "ws", "ws" }, .{ "ws/lib/websocket", "ws" }, }, diff --git a/src/bun.js/url.exports.js b/src/bun.js/url.exports.js new file mode 100644 index 000000000..e1e2be331 --- /dev/null +++ b/src/bun.js/url.exports.js @@ -0,0 +1,2 @@ +"use strict";const{URL:F,URLSearchParams:M,[Symbol.for("Bun.lazy")]:S}=globalThis;function it(s){return typeof s=="string"}function D(s){return typeof s=="object"&&s!==null}function I(s){return s===null}function E(s){return s==null}function ft(s){return s===void 0}function m(){this.protocol=null,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}var tt=/^([a-z0-9.+-]+:)/i,st=/:[0-9]*$/,ht=/^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,et=["<",">",'"',"`"," ","\r",` +`," "],rt=["{","}","|","\\","^","`"].concat(et),B=["'"].concat(rt),G=["%","/","?",";","#"].concat(B),J=["/","?","#"],ot=255,K=/^[+a-z0-9A-Z_-]{0,63}$/,at=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,nt={javascript:!0,"javascript:":!0},N={javascript:!0,"javascript:":!0},R={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0},Z={parse(s){var r=decodeURIComponent;return(s+"").replace(/\+/g," ").split("&").filter(Boolean).reduce(function(t,o,a){var l=o.split("="),f=r(l[0]||""),h=r(l[1]||""),g=t[f];return t[f]=g===void 0?h:[].concat(g,h),t},{})},stringify(s){var r=encodeURIComponent;return Object.keys(s||{}).reduce(function(t,o){return[].concat(s[o]).forEach(function(a){t.push(r(o)+"="+r(a))}),t},[]).join("&").replace(/\s/g,"+")}};function A(s,r,t){if(s&&D(s)&&s instanceof m)return s;var o=new m;return o.parse(s,r,t),o}m.prototype.parse=function(s,r,t){if(!isString(s))throw new TypeError("Parameter 'url' must be a string, not "+typeof s);var o=s.indexOf("?"),a=o!==-1&&o<s.indexOf("#")?"?":"#",l=s.split(a),f=/\\/g;l[0]=l[0].replace(f,"/"),s=l.join(a);var h=s;if(h=h.trim(),!t&&s.split("#").length===1){var g=ht.exec(h);if(g)return this.path=h,this.href=h,this.pathname=g[1],g[2]?(this.search=g[2],r?this.query=Z.parse(this.search.substr(1)):this.query=this.search.substr(1)):r&&(this.search="",this.query={}),this}var c=tt.exec(h);if(c){c=c[0];var v=c.toLowerCase();this.protocol=v,h=h.substr(c.length)}if(t||c||h.match(/^\/\/[^@\/]+@[^@\/]+/)){var j=h.substr(0,2)==="//";j&&!(c&&N[c])&&(h=h.substr(2),this.slashes=!0)}if(!N[c]&&(j||c&&!R[c])){for(var u=-1,n=0;n<J.length;n++){var b=h.indexOf(J[n]);b!==-1&&(u===-1||b<u)&&(u=b)}var P,p;u===-1?p=h.lastIndexOf("@"):p=h.lastIndexOf("@",u),p!==-1&&(P=h.slice(0,p),h=h.slice(p+1),this.auth=decodeURIComponent(P)),u=-1;for(var n=0;n<G.length;n++){var b=h.indexOf(G[n]);b!==-1&&(u===-1||b<u)&&(u=b)}u===-1&&(u=h.length),this.host=h.slice(0,u),h=h.slice(u),this.parseHost(),this.hostname=this.hostname||"";var C=this.hostname[0]==="["&&this.hostname[this.hostname.length-1]==="]";if(!C)for(var e=this.hostname.split(/\./),n=0,i=e.length;n<i;n++){var d=e[n];if(!!d&&!d.match(K)){for(var y="",x=0,_=d.length;x<_;x++)d.charCodeAt(x)>127?y+="x":y+=d[x];if(!y.match(K)){var q=e.slice(0,n),O=e.slice(n+1),U=d.match(at);U&&(q.push(U[1]),O.unshift(U[2])),O.length&&(h="/"+O.join(".")+h),this.hostname=q.join(".");break}}}this.hostname.length>ot?this.hostname="":this.hostname=this.hostname.toLowerCase(),C||(this.hostname=new F(`https://${this.hostname}`).hostname);var w=this.port?":"+this.port:"",H=this.hostname||"";this.host=H+w,this.href+=this.host,C&&(this.hostname=this.hostname.substr(1,this.hostname.length-2),h[0]!=="/"&&(h="/"+h))}if(!nt[v])for(var n=0,i=B.length;n<i;n++){var L=B[n];if(h.indexOf(L)!==-1){var z=encodeURIComponent(L);z===L&&(z=escape(L)),h=h.split(L).join(z)}}var $=h.indexOf("#");$!==-1&&(this.hash=h.substr($),h=h.slice(0,$));var T=h.indexOf("?");if(T!==-1?(this.search=h.substr(T),this.query=h.substr(T+1),r&&(this.query=Z.parse(this.query)),h=h.slice(0,T)):r&&(this.search="",this.query={}),h&&(this.pathname=h),R[v]&&this.hostname&&!this.pathname&&(this.pathname="/"),this.pathname||this.search){var w=this.pathname||"",Q=this.search||"";this.path=w+Q}return this.href=this.format(),this};function V(s){return isString(s)&&(s=A(s)),s instanceof m?s.format():m.prototype.format.call(s)}m.prototype.format=function(){var s=this.auth||"";s&&(s=encodeURIComponent(s),s=s.replace(/%3A/i,":"),s+="@");var r=this.protocol||"",t=this.pathname||"",o=this.hash||"",a=!1,l="";this.host?a=s+this.host:this.hostname&&(a=s+(this.hostname.indexOf(":")===-1?this.hostname:"["+this.hostname+"]"),this.port&&(a+=":"+this.port)),this.query&&D(this.query)&&Object.keys(this.query).length&&(l=Z.stringify(this.query));var f=this.search||l&&"?"+l||"";return r&&r.substr(-1)!==":"&&(r+=":"),this.slashes||(!r||R[r])&&a!==!1?(a="//"+(a||""),t&&t.charAt(0)!=="/"&&(t="/"+t)):a||(a=""),o&&o.charAt(0)!=="#"&&(o="#"+o),f&&f.charAt(0)!=="?"&&(f="?"+f),t=t.replace(/[?#]/g,function(h){return encodeURIComponent(h)}),f=f.replace("#","%23"),r+a+t+f+o};function W(s,r){return A(s,!1,!0).resolve(r)}m.prototype.resolve=function(s){return this.resolveObject(A(s,!1,!0)).format()};function X(s,r){return s?A(s,!1,!0).resolveObject(r):r}m.prototype.resolveObject=function(s){if(isString(s)){var r=new m;r.parse(s,!1,!0),s=r}for(var t=new m,o=Object.keys(this),a=0;a<o.length;a++){var l=o[a];t[l]=this[l]}if(t.hash=s.hash,s.href==="")return t.href=t.format(),t;if(s.slashes&&!s.protocol){for(var f=Object.keys(s),h=0;h<f.length;h++){var g=f[h];g!=="protocol"&&(t[g]=s[g])}return R[t.protocol]&&t.hostname&&!t.pathname&&(t.path=t.pathname="/"),t.href=t.format(),t}if(s.protocol&&s.protocol!==t.protocol){if(!R[s.protocol]){for(var c=Object.keys(s),v=0;v<c.length;v++){var j=c[v];t[j]=s[j]}return t.href=t.format(),t}if(t.protocol=s.protocol,!s.host&&!N[s.protocol]){for(var i=(s.pathname||"").split("/");i.length&&!(s.host=i.shift()););s.host||(s.host=""),s.hostname||(s.hostname=""),i[0]!==""&&i.unshift(""),i.length<2&&i.unshift(""),t.pathname=i.join("/")}else t.pathname=s.pathname;if(t.search=s.search,t.query=s.query,t.host=s.host||"",t.auth=s.auth,t.hostname=s.hostname||s.host,t.port=s.port,t.pathname||t.search){var u=t.pathname||"",n=t.search||"";t.path=u+n}return t.slashes=t.slashes||s.slashes,t.href=t.format(),t}var b=t.pathname&&t.pathname.charAt(0)==="/",P=s.host||s.pathname&&s.pathname.charAt(0)==="/",p=P||b||t.host&&s.pathname,C=p,e=t.pathname&&t.pathname.split("/")||[],i=s.pathname&&s.pathname.split("/")||[],d=t.protocol&&!R[t.protocol];if(d&&(t.hostname="",t.port=null,t.host&&(e[0]===""?e[0]=t.host:e.unshift(t.host)),t.host="",s.protocol&&(s.hostname=null,s.port=null,s.host&&(i[0]===""?i[0]=s.host:i.unshift(s.host)),s.host=null),p=p&&(i[0]===""||e[0]==="")),P)t.host=s.host||s.host===""?s.host:t.host,t.hostname=s.hostname||s.hostname===""?s.hostname:t.hostname,t.search=s.search,t.query=s.query,e=i;else if(i.length)e||(e=[]),e.pop(),e=e.concat(i),t.search=s.search,t.query=s.query;else if(!E(s.search)){if(d){t.hostname=t.host=e.shift();var y=t.host&&t.host.indexOf("@")>0?t.host.split("@"):!1;y&&(t.auth=y.shift(),t.host=t.hostname=y.shift())}return t.search=s.search,t.query=s.query,(!I(t.pathname)||!I(t.search))&&(t.path=(t.pathname?t.pathname:"")+(t.search?t.search:"")),t.href=t.format(),t}if(!e.length)return t.pathname=null,t.search?t.path="/"+t.search:t.path=null,t.href=t.format(),t;for(var x=e.slice(-1)[0],_=(t.host||s.host||e.length>1)&&(x==="."||x==="..")||x==="",q=0,O=e.length;O>=0;O--)x=e[O],x==="."?e.splice(O,1):x===".."?(e.splice(O,1),q++):q&&(e.splice(O,1),q--);if(!p&&!C)for(;q--;q)e.unshift("..");p&&e[0]!==""&&(!e[0]||e[0].charAt(0)!=="/")&&e.unshift(""),_&&e.join("/").substr(-1)!=="/"&&e.push("");var U=e[0]===""||e[0]&&e[0].charAt(0)==="/";if(d){t.hostname=t.host=U?"":e.length?e.shift():"";var y=t.host&&t.host.indexOf("@")>0?t.host.split("@"):!1;y&&(t.auth=y.shift(),t.host=t.hostname=y.shift())}return p=p||t.host&&e.length,p&&!U&&e.unshift(""),e.length?t.pathname=e.join("/"):(t.pathname=null,t.path=null),(!I(t.pathname)||!I(t.search))&&(t.path=(t.pathname?t.pathname:"")+(t.search?t.search:"")),t.auth=s.auth||t.auth,t.slashes=t.slashes||s.slashes,t.href=t.format(),t},m.prototype.parseHost=function(){var s=this.host,r=st.exec(s);r&&(r=r[0],r!==":"&&(this.port=r.substr(1)),s=s.substr(0,s.length-r.length)),s&&(this.hostname=s)};var Y,k;S&&(Y=S("pathToFileURL"),k=S("fileURLToPath"));var ut={parse:A,resolve:W,resolveObject:X,format:V,Url:m,pathToFileURL:Y,fileURLToPath:k,URL:F,URLSearchParams:M};"use strict";export{F as URL,M as URLSearchParams,m as Url,ut as default,k as fileURLToPath,V as format,A as parse,Y as pathToFileURL,W as resolve,X as resolveObject}; diff --git a/src/node-fallbacks/url.js b/src/node-fallbacks/url.js index 6c5da9748..fcc8b4db4 100644 --- a/src/node-fallbacks/url.js +++ b/src/node-fallbacks/url.js @@ -1 +1,807 @@ -export * from "url"; +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +"use strict"; +const { URL, URLSearchParams, [Symbol.for("Bun.lazy")]: lazy } = globalThis; + +function util_isString(arg) { + return typeof arg === "string"; +} +function util_isObject(arg) { + return typeof arg === "object" && arg !== null; +} +function util_isNull(arg) { + return arg === null; +} +function util_isNullOrUndefined(arg) { + return arg == null; +} +function util_isUndefined(arg) { + return arg === void 0; +} + +function Url() { + this.protocol = null; + 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. +var protocolPattern = /^([a-z0-9.+-]+:)/i, + portPattern = /:[0-9]*$/, + // Special case for a simple path URL + simplePathPattern = /^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/, + // RFC 2396: characters reserved for delimiting URLs. + // We actually just auto-escape these. + delims = ["<", ">", '"', "`", " ", "\r", "\n", "\t"], + // RFC 2396: characters not allowed for various reasons. + unwise = ["{", "}", "|", "\\", "^", "`"].concat(delims), + // Allowed by RFCs, but cause of XSS attacks. Always escape these. + autoEscape = ["'"].concat(unwise), + // 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. + nonHostChars = ["%", "/", "?", ";", "#"].concat(autoEscape), + hostEndingChars = ["/", "?", "#"], + hostnameMaxLen = 255, + hostnamePartPattern = /^[+a-z0-9A-Z_-]{0,63}$/, + hostnamePartStart = /^([+a-z0-9A-Z_-]{0,63})(.*)$/, + // protocols that can allow "unsafe" and "unwise" chars. + unsafeProtocol = { + javascript: true, + "javascript:": true, + }, + // protocols that never have a hostname. + hostlessProtocol = { + javascript: true, + "javascript:": true, + }, + // protocols that always contain a // bit. + slashedProtocol = { + http: true, + https: true, + ftp: true, + gopher: true, + file: true, + "http:": true, + "https:": true, + "ftp:": true, + "gopher:": true, + "file:": true, + }; + +// https://github.com/Cap32/tiny-querystring/blob/master/tiny-querystring.js +var querystring = { + parse(str) { + var decode = decodeURIComponent; + return (str + "") + .replace(/\+/g, " ") + .split("&") + .filter(Boolean) + .reduce(function (obj, item, index) { + var ref = item.split("="); + var key = decode(ref[0] || ""); + var val = decode(ref[1] || ""); + var prev = obj[key]; + obj[key] = prev === undefined ? val : [].concat(prev, val); + return obj; + }, {}); + }, + stringify(obj) { + var encode = encodeURIComponent; + return Object.keys(obj || {}) + .reduce(function (arr, key) { + [].concat(obj[key]).forEach(function (v) { + arr.push(encode(key) + "=" + encode(v)); + }); + return arr; + }, []) + .join("&") + .replace(/\s/g, "+"); + }, +}; + +function urlParse(url, parseQueryString, slashesDenoteHost) { + if (url && util_isObject(url) && url instanceof Url) return url; + + var u = new Url(); + u.parse(url, parseQueryString, slashesDenoteHost); + return u; +} + +Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { + if (!isString(url)) { + 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); + } + + // 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 = new URL(`https://${this.hostname}`).hostname; + } + + var p = this.port ? ":" + this.port : ""; + var h = this.hostname || ""; + this.host = h + p; + this.href += this.host; + + // 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; + } + } + } + + // 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); + } + } + + // 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 = "/"; + } + + //to support http.request + if (this.pathname || this.search) { + var p = this.pathname || ""; + var s = this.search || ""; + this.path = p + s; + } + + // finally, reconstruct the href based on what has been validated. + this.href = this.format(); + return this; +}; + +// format a parsed object into a url string +function urlFormat(obj) { + // 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 (isString(obj)) obj = urlParse(obj); + 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; + } + } + + if ( + this.query && + util_isObject(this.query) && + Object.keys(this.query).length + ) { + query = querystring.stringify(this.query); + } + + var search = this.search || (query && "?" + query) || ""; + + 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 (hash && hash.charAt(0) !== "#") hash = "#" + hash; + if (search && search.charAt(0) !== "?") search = "?" + search; + + pathname = pathname.replace(/[?#]/g, function (match) { + return encodeURIComponent(match); + }); + search = search.replace("#", "%23"); + + return protocol + host + pathname + search + hash; +}; + +function urlResolve(source, relative) { + return urlParse(source, false, true).resolve(relative); +} + +Url.prototype.resolve = function (relative) { + return this.resolveObject(urlParse(relative, false, true)).format(); +}; + +function urlResolveObject(source, relative) { + if (!source) return relative; + return urlParse(source, false, true).resolveObject(relative); +} + +Url.prototype.resolveObject = function (relative) { + if (isString(relative)) { + var rel = new Url(); + rel.parse(relative, false, true); + relative = rel; + } + + 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]; + } + + // 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(); + 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]; + } + + //urlParse appends trailing / to urls like http://www.example.com + if ( + slashedProtocol[result.protocol] && + result.hostname && + !result.pathname + ) { + result.path = result.pathname = "/"; + } + + result.href = result.format(); + return result; + } + + 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 + // to begin with, then we MUST have a path. + // 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(); + return result; + } + + result.protocol = relative.protocol; + if (!relative.host && !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 = ""; + if (relPath[0] !== "") relPath.unshift(""); + if (relPath.length < 2) relPath.unshift(""); + 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.slashes = result.slashes || relative.slashes; + result.href = result.format(); + 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]; + + // 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.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); + } + relative.host = null; + } + 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; + srcPath = relPath; + // fall through to the dot-handling below. + } else if (relPath.length) { + // 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 (!util_isNullOrUndefined(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 especially 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.search = relative.search; + result.query = relative.query; + //to support http.request + if (!util_isNull(result.pathname) || !util_isNull(result.search)) { + result.path = + (result.pathname ? result.pathname : "") + + (result.search ? result.search : ""); + } + result.href = result.format(); + return result; + } + + if (!srcPath.length) { + // 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(); + return result; + } + + // 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) && + (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 + var up = 0; + for (var i = srcPath.length; i >= 0; i--) { + last = srcPath[i]; + if (last === ".") { + srcPath.splice(i, 1); + } else if (last === "..") { + srcPath.splice(i, 1); + up++; + } else if (up) { + srcPath.splice(i, 1); + up--; + } + } + + // if the path is allowed to go above the root, restore leading ..s + if (!mustEndAbs && !removeAllDots) { + for (; up--; up) { + srcPath.unshift(".."); + } + } + + if ( + mustEndAbs && + srcPath[0] !== "" && + (!srcPath[0] || srcPath[0].charAt(0) !== "/") + ) { + srcPath.unshift(""); + } + + if (hasTrailingSlash && srcPath.join("/").substr(-1) !== "/") { + srcPath.push(""); + } + + var isAbsolute = + srcPath[0] === "" || (srcPath[0] && srcPath[0].charAt(0) === "/"); + + // 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 especially 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(); + } + } + + 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("/"); + } + + //to support request.http + if (!util_isNull(result.pathname) || !util_isNull(result.search)) { + result.path = + (result.pathname ? result.pathname : "") + + (result.search ? result.search : ""); + } + result.auth = relative.auth || result.auth; + result.slashes = result.slashes || relative.slashes; + result.href = result.format(); + return result; +}; + +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); + } + host = host.substr(0, host.length - port.length); + } + if (host) this.hostname = host; +}; + +export { URL, URLSearchParams }; +export { urlParse as parse }; +export { urlResolve as resolve }; +export { urlResolveObject as resolveObject }; +export { urlFormat as format }; +export { Url as Url }; + +export var pathToFileURL; +export var fileURLToPath; + +if (lazy) { + pathToFileURL = lazy("pathToFileURL"); + fileURLToPath = lazy("fileURLToPath"); +} + +export default { + parse: urlParse, + resolve: urlResolve, + resolveObject: urlResolveObject, + format: urlFormat, + Url: Url, + pathToFileURL: pathToFileURL, + fileURLToPath: fileURLToPath, + URL, + URLSearchParams, +}; |