From 6e496c60991ca4664f806446a0632cb45211c625 Mon Sep 17 00:00:00 2001 From: Don Isaac Date: Tue, 14 Jan 2025 17:24:22 -0800 Subject: [PATCH] test-url-is-url-internal --- src/bun.js/bindings/BunObject.cpp | 7 +- src/bun.js/bindings/ErrorCode.cpp | 7 + src/bun.js/bindings/ErrorCode.h | 2 + src/bun.js/bindings/ErrorCode.ts | 1 + src/bun.js/bindings/bindings.cpp | 26 + src/bun.js/node/node_util_binding.zig | 11 + src/codegen/generate-node-errors.ts | 1 + src/js/builtins.d.ts | 8 + src/js/internal/util.ts | 5 + src/js/node/url.ts | 348 +++++++++----- test/js/bun/plugin/plugins.test.ts | 8 +- test/js/node/harness.ts | 17 +- .../test-url-format-invalid-input.js | 32 ++ .../test/parallel/test-url-is-url-internal.js | 22 + .../test/parallel/test-url-parse-query.js | 102 ++++ .../node/test/parallel/test-url-relative.js | 443 ++++++++++++++++++ 16 files changed, 906 insertions(+), 134 deletions(-) create mode 100644 src/js/internal/util.ts create mode 100644 test/js/node/test/parallel/needs-test/test-url-format-invalid-input.js create mode 100644 test/js/node/test/parallel/test-url-is-url-internal.js create mode 100644 test/js/node/test/parallel/test-url-parse-query.js create mode 100644 test/js/node/test/parallel/test-url-relative.js diff --git a/src/bun.js/bindings/BunObject.cpp b/src/bun.js/bindings/BunObject.cpp index 2dc702a59a229a..2a6fb5b0f3c515 100644 --- a/src/bun.js/bindings/BunObject.cpp +++ b/src/bun.js/bindings/BunObject.cpp @@ -604,7 +604,8 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj url = WTF::URL(arg0.toWTFString(globalObject)); RETURN_IF_EXCEPTION(scope, {}); } else { - throwTypeError(globalObject, scope, "Argument must be a URL"_s); + Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "url"_s, "string"_s, arg0); + // throwTypeError(globalObject, scope, "Argument must be a URL"_s); return {}; } } else { @@ -612,7 +613,9 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj } if (UNLIKELY(!url.protocolIsFile())) { - throwTypeError(globalObject, scope, "Argument must be a file URL"_s); + // throwTypeError(globalObject, scope, "Argument must be a file URL"_s); + Bun::ERR::INVALID_URL_SCHEME(scope, globalObject, "file"_s); + // Bun::ERR:ErrorCode::ERR_INVALID_URL return {}; } diff --git a/src/bun.js/bindings/ErrorCode.cpp b/src/bun.js/bindings/ErrorCode.cpp index 8852628317e4bf..b56616315d5343 100644 --- a/src/bun.js/bindings/ErrorCode.cpp +++ b/src/bun.js/bindings/ErrorCode.cpp @@ -536,6 +536,13 @@ JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobal return {}; } +JSC::EncodedJSValue INVALID_URL_SCHEME(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& expectedScheme) +{ + auto message = makeString("The URL must be of scheme "_s, expectedScheme); + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_INVALID_URL_SCHEME, message)); + return {}; +} + JSC::EncodedJSValue UNKNOWN_ENCODING(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::StringView encoding) { auto message = makeString("Unknown encoding: "_s, encoding); diff --git a/src/bun.js/bindings/ErrorCode.h b/src/bun.js/bindings/ErrorCode.h index d06bb8e4a2e823..f23f6e14620c88 100644 --- a/src/bun.js/bindings/ErrorCode.h +++ b/src/bun.js/bindings/ErrorCode.h @@ -1,3 +1,4 @@ +// To add a new error code, put it in ErrorCode.ts #pragma once #include "ZigGlobalObject.h" @@ -79,6 +80,7 @@ JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObjec JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral name, JSC::JSValue value, const WTF::String& reason = "is invalid"_s); JSC::EncodedJSValue INVALID_ARG_VALUE_RangeError(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral name, JSC::JSValue value, const WTF::String& reason = "is invalid"_s); JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue name, JSC::JSValue value, const WTF::String& reason = "is invalid"_s); +JSC::EncodedJSValue INVALID_URL_SCHEME(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& expectedScheme); JSC::EncodedJSValue UNKNOWN_ENCODING(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::StringView encoding); JSC::EncodedJSValue INVALID_STATE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& statemsg); JSC::EncodedJSValue STRING_TOO_LONG(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject); diff --git a/src/bun.js/bindings/ErrorCode.ts b/src/bun.js/bindings/ErrorCode.ts index bfe08a4f789743..9628814726ad77 100644 --- a/src/bun.js/bindings/ErrorCode.ts +++ b/src/bun.js/bindings/ErrorCode.ts @@ -41,6 +41,7 @@ export default [ ["MODULE_NOT_FOUND", Error], ["ERR_ILLEGAL_CONSTRUCTOR", TypeError], ["ERR_INVALID_URL", TypeError], + ["ERR_INVALID_URL_SCHEME", TypeError], ["ERR_BUFFER_TOO_LARGE", RangeError], ["ERR_BROTLI_INVALID_PARAM", RangeError], ["ERR_UNKNOWN_ENCODING", TypeError], diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index d553e86f4a34d7..9b5d67a52b3e29 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -6308,3 +6308,29 @@ extern "C" EncodedJSValue Bun__JSObject__getCodePropertyVMInquiry(JSC::JSGlobalO return JSValue::encode(slot.getPureResult()); } + +using StackCodeType = JSC::StackVisitor::Frame::CodeType; +CPP_DECL bool Bun__util__isInsideNodeModules(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame) +{ + JSC::VM& vm = globalObject->vm(); + bool inNodeModules = false; + JSC::StackVisitor::visit(callFrame, vm, [&](JSC::StackVisitor& visitor) -> WTF::IterationStatus { + if (Zig::isImplementationVisibilityPrivate(visitor) || visitor->isNativeCalleeFrame()) { + return WTF::IterationStatus::Continue; + } + + if (visitor->hasLineAndColumnInfo()) { + String sourceURL = Zig::sourceURL(visitor); + if (sourceURL.startsWith("node:"_s) || sourceURL.startsWith("bun:"_s)) + return WTF::IterationStatus::Continue; + if (sourceURL.startsWith("bun:"_s) || sourceURL.contains("node_modules"_s)) + inNodeModules = true; + + return WTF::IterationStatus::Done; + } + + return WTF::IterationStatus::Continue; + }); + + return inNodeModules; +} diff --git a/src/bun.js/node/node_util_binding.zig b/src/bun.js/node/node_util_binding.zig index cbcf25f09b3746..123cc08ee0aebc 100644 --- a/src/bun.js/node/node_util_binding.zig +++ b/src/bun.js/node/node_util_binding.zig @@ -132,6 +132,17 @@ pub fn extractedSplitNewLinesFastPathStringsOnly(globalThis: *JSC.JSGlobalObject }; } +extern fn Bun__util__isInsideNodeModules(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) bool; +pub fn isInsideNodeModules(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const res = Bun__util__isInsideNodeModules(globalObject, callframe); + return JSC.JSValue.jsBoolean(res); +} + +// pub const isInsideNodeModules = Bun__util__isInsideNodeModules; +// pub fn isInsideNodeModules(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { +// // globalThis.vm(). +// const caller = callframe.getCallerSrcLoc(); +// } fn split( comptime encoding: bun.strings.EncodingNonAscii, globalThis: *JSC.JSGlobalObject, diff --git a/src/codegen/generate-node-errors.ts b/src/codegen/generate-node-errors.ts index e4c807be702bfb..fb777b9197c2f4 100644 --- a/src/codegen/generate-node-errors.ts +++ b/src/codegen/generate-node-errors.ts @@ -13,6 +13,7 @@ let zig = ``; enumHeader = ` // clang-format off // Generated by: src/codegen/generate-node-errors.ts +// Input: src/bun.js/bindings/ErrorCode.ts #pragma once #include diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index aa38f45245c615..c7559c2f3374ef 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -99,6 +99,14 @@ declare function $getMapIteratorInternalField(): TODO; declare function $getSetIteratorInternalField(): TODO; declare function $getProxyInternalField(): TODO; declare function $idWithProfile(): TODO; +/** + * True for `JSCell`s. That is, this is roughly equivalent to this JS code: + * ```js + * typeof obj === "object" && obj !== null + * ``` + * @see [JSCell.h](https://github.com/oven-sh/WebKit/blob/main/Source/JavaScriptCore/runtime/JSCell.h) + * @see [JIT implementation](https://github.com/oven-sh/WebKit/blob/433f7598bf3537a295d0af5ffd83b9a307abec4e/Source/JavaScriptCore/jit/JITOpcodes.cpp#L311) + */ declare function $isObject(obj: unknown): obj is object; declare function $isArray(obj: unknown): obj is any[]; declare function $isCallable(fn: unknown): fn is CallableFunction; diff --git a/src/js/internal/util.ts b/src/js/internal/util.ts new file mode 100644 index 00000000000000..32db0807c34557 --- /dev/null +++ b/src/js/internal/util.ts @@ -0,0 +1,5 @@ +const isInsideNodeModules: () => boolean = $newZigFunction("node_util_binding.zig", "isInsideNodeModules", 0); + +export default { + isInsideNodeModules, +}; diff --git a/src/js/node/url.ts b/src/js/node/url.ts index d969ab08ff27ca..b92e2c7befafe2 100644 --- a/src/js/node/url.ts +++ b/src/js/node/url.ts @@ -26,6 +26,7 @@ const { URL, URLSearchParams } = globalThis; const [domainToASCII, domainToUnicode] = $cpp("NodeURL.cpp", "Bun::createNodeURLBinding"); const { urlToHttpOptions } = require("internal/url"); +const { validateString } = require("internal/validators"); function Url() { this.protocol = null; @@ -97,44 +98,113 @@ var protocolPattern = /^([a-z0-9.+-]+:)/i, "file:": true, }; -function urlParse(url, parseQueryString, slashesDenoteHost) { - if (url && typeof url === "object" && url instanceof Url) { - return url; - } +let urlParseWarned = false; +function urlParse( + url: string | URL | Url, // really has unknown type but intellisense is nice + parseQueryString?: boolean, + slashesDenoteHost?: boolean, +) { + if (!urlParseWarned && !require("internal/util").isInsideNodeModules(100, true)) { + urlParseWarned = true; + process.emitWarning( + "`url.parse()` behavior is not standardized and prone to " + + "errors that have security implications. Use the WHATWG URL API " + + "instead. CVEs are not issued for `url.parse()` vulnerabilities.", + "[DEP1069] DeprecationWarning", + "DEP1069", + ); + } + + if ($isObject(url) && url instanceof Url) return url; var u = new Url(); - u.parse(url, parseQueryString, slashesDenoteHost); + try { + u.parse(url, parseQueryString, slashesDenoteHost); + } catch (e) { + $putByIdDirect(e, "input", url); + throw e; + } 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); - } +Url.prototype.parse = function parse(url: string, parseQueryString?: boolean, slashesDenoteHost?: boolean) { + validateString(url, "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); + let hasHash = false; + let hasAt = false; + let start = -1; + let end = -1; + let rest = ""; + let lastPos = 0; + for (let i = 0, inWs = false, split = false; i < url.length; ++i) { + const code = url.charCodeAt(i); + + // Find first and last non-whitespace characters for trimming + const isWs = code < 33 || code === Char.NO_BREAK_SPACE || code === Char.ZERO_WIDTH_NOBREAK_SPACE; + if (start === -1) { + if (isWs) continue; + lastPos = start = i; + } else if (inWs) { + if (!isWs) { + end = -1; + inWs = false; + } + } else if (isWs) { + end = i; + inWs = true; + } - var rest = url; + // Only convert backslashes while we haven't seen a split character + if (!split) { + switch (code) { + case Char.AT: + hasAt = true; + break; + case Char.HASH: + hasHash = true; + // Fall through + case Char.QUESTION_MARK: + split = true; + break; + case Char.BACKWARD_SLASH: + if (i - lastPos > 0) rest += url.slice(lastPos, i); + rest += "/"; + lastPos = i + 1; + break; + } + } else if (!hasHash && code === Char.HASH) { + hasHash = true; + } + } - /* - * trim before proceeding. - * This is to support parse stuff like " http://foo.com \n" - */ - rest = rest.trim(); + // Check if string was non-empty (including strings with only whitespace) + if (start !== -1) { + if (lastPos === start) { + // We didn't convert any backslashes - if (!slashesDenoteHost && url.split("#").length === 1) { + if (end === -1) { + if (start === 0) rest = url; + else rest = url.slice(start); + } else { + rest = url.slice(start, end); + } + } else if (end === -1 && lastPos < url.length) { + // We converted some backslashes and have only part of the entire string + rest += url.slice(lastPos); + } else if (end !== -1 && lastPos < end) { + // We converted some backslashes and have only part of the entire string + rest += url.slice(lastPos, end); + } + } + + if (!slashesDenoteHost && !hasHash && !hasAt) { // Try fast path regexp - var simplePath = simplePathPattern.exec(rest); + const simplePath = simplePathPattern.exec(rest); if (simplePath) { this.path = rest; this.href = rest; @@ -142,24 +212,24 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { if (simplePath[2]) { this.search = simplePath[2]; if (parseQueryString) { - this.query = new URLSearchParams(this.search.substr(1)).toJSON(); + this.query = new URLSearchParams(this.search.slice(1)).toJSON(); } else { - this.query = this.search.substr(1); + this.query = this.search.slice(1); } } else if (parseQueryString) { - this.search = ""; - this.query = {}; + this.search = null; + this.query = { __proto__: null }; } return this; } } - var proto = protocolPattern.exec(rest); + var proto: any = protocolPattern.exec(rest); if (proto) { proto = proto[0]; var lowerProto = proto.toLowerCase(); this.protocol = lowerProto; - rest = rest.substr(proto.length); + rest = rest.substring(proto.length); } /* @@ -254,7 +324,10 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { * we've indicated that there is a hostname, * so even if it's empty, it has to be present. */ - this.hostname = this.hostname || ""; + if (typeof this.hostname !== "string") { + this.hostname = ""; + } + const hostname = this.hostname; /* * if hostname begins with [ and ends with ] @@ -264,43 +337,7 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { // 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; - } - } - } + rest = getHostname(this, rest, hostname, url); } if (this.hostname.length > hostnameMaxLen) { @@ -330,7 +367,7 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { * the host field still retains them, though */ if (ipv6Hostname) { - this.hostname = this.hostname.substr(1, this.hostname.length - 2); + this.hostname = this.hostname.slice(1, this.hostname.length - 2); if (rest[0] !== "/") { rest = "/" + rest; } @@ -369,8 +406,8 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { } var qm = rest.indexOf("?"); if (qm !== -1) { - this.search = rest.substr(qm); - this.query = rest.substr(qm + 1); + this.search = rest.substring(qm); + this.query = rest.substring(qm + 1); if (parseQueryString) { const query = this.query; this.query = new URLSearchParams(query).toJSON(); @@ -378,7 +415,7 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { rest = rest.slice(0, qm); } else if (parseQueryString) { // no query string, but parseQueryString still requested - this.search = ""; + this.search = null; this.query = {}; } if (rest) { @@ -400,24 +437,54 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { return this; }; +let warnInvalidPort = true; +function getHostname(self, rest, hostname: string, url) { + for (let i = 0; i < hostname.length; ++i) { + const code = hostname.charCodeAt(i); + const isValid = + code !== Char.FORWARD_SLASH && + code !== Char.BACKWARD_SLASH && + code !== Char.HASH && + code !== Char.QUESTION_MARK && + code !== Char.COLON; + + if (!isValid) { + // If leftover starts with :, then it represents an invalid port. + // But url.parse() is lenient about it for now. + // Issue a warning and continue. + if (warnInvalidPort && code === Char.COLON) { + const detail = `The URL ${url} is invalid. Future versions of Node.js will throw an error.`; + process.emitWarning(detail, "DeprecationWarning", "DEP0170"); + warnInvalidPort = false; + } + self.hostname = hostname.slice(0, i); + return `/${hostname.slice(i)}${rest}`; + } + } + return rest; +} + // format a parsed object into a url string -function urlFormat(obj) { +function urlFormat(urlObject) { /* * 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); + if (typeof urlObject === "string") { + urlObject = urlParse(urlObject); + } else if (typeof urlObject !== "object" || urlObject === null) { + throw $ERR_INVALID_ARG_TYPE("urlObject", ["Object", "string"], urlObject); } - if (!(obj instanceof Url)) { - return Url.prototype.format.$call(obj); + + if (!(urlObject instanceof Url)) { + return Url.prototype.format.$call(urlObject); } - return obj.format(); + return urlObject.format(); } -Url.prototype.format = function () { +Url.prototype.format = function format() { var auth = this.auth || ""; if (auth) { auth = encodeURIComponent(auth); @@ -493,7 +560,7 @@ function urlResolveObject(source, relative) { return urlParse(source, false, true).resolveObject(relative); } -Url.prototype.resolveObject = function (relative) { +Url.prototype.resolveObject = function resolveObject(relative) { if (typeof relative === "string") { var rel = new Url(); rel.parse(relative, false, true); @@ -562,21 +629,18 @@ Url.prototype.resolveObject = function (relative) { } result.protocol = relative.protocol; - if (!relative.host && !hostlessProtocol[relative.protocol]) { - var relPath = (relative.pathname || "").split("/"); + if ( + !relative.host && + !(relative.protocol === "file" || relative.protocol === "file:") && + !hostlessProtocol[relative.protocol] + ) { + let 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(""); - } + relative.host ||= ""; + relative.hostname ||= ""; + if (relPath[0] !== "") relPath.unshift(""); + if (relPath.length < 2) relPath.unshift(""); + result.pathname = relPath.join("/"); } else { result.pathname = relative.pathname; @@ -598,13 +662,13 @@ Url.prototype.resolveObject = function (relative) { 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]; + const isSourceAbs = result.pathname && result.pathname.charAt(0) === "/"; + const isRelAbs = relative.host || (relative.pathname && relative.pathname.charAt(0) === "/"); + let mustEndAbs = isRelAbs || isSourceAbs || (result.host && relative.pathname); + const removeAllDots = mustEndAbs; + let srcPath = (result.pathname && result.pathname.split("/")) || []; + const relPath = (relative.pathname && relative.pathname.split("/")) || []; + const psychotic = result.protocol && !slashedProtocol[result.protocol]; /* * if the url is a non-slashed url, then relative @@ -617,16 +681,14 @@ Url.prototype.resolveObject = function (relative) { result.hostname = ""; result.port = null; if (result.host) { - if (srcPath[0] === "") { - srcPath[0] = result.host; - } else { - srcPath.unshift(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; + result.auth = null; if (relative.host) { if (relPath[0] === "") { relPath[0] = relative.host; @@ -636,13 +698,20 @@ Url.prototype.resolveObject = function (relative) { } relative.host = null; } - mustEndAbs = mustEndAbs && (relPath[0] === "" || srcPath[0] === ""); + 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; + if (relative.host || relative.host === "") { + if (result.host !== relative.host) result.auth = null; + result.host = relative.host; + result.port = relative.port; + } + if (relative.hostname || relative.hostname === "") { + if (result.hostname !== relative.hostname) result.auth = null; + result.hostname = relative.hostname; + } result.search = relative.search; result.query = relative.query; srcPath = relPath; @@ -652,22 +721,19 @@ Url.prototype.resolveObject = function (relative) { * it's relative * throw away the existing file, and take the new path instead. */ - if (!srcPath) { - srcPath = []; - } + srcPath ||= []; srcPath.pop(); srcPath = srcPath.concat(relPath); result.search = relative.search; result.query = relative.query; - } else if (relative.search != null) { + } else if (relative.search != null && relative.search !== undefined) { /* * just pull out the search. * like href='?foo'. * Put this after the other two cases because it simplifies the booleans */ if (psychotic) { - result.host = srcPath.shift(); - result.hostname = result.host; + result.hostname = result.host = srcPath.shift(); /* * occationaly the auth can get stuck only in host * this especially happens in cases like @@ -676,15 +742,16 @@ Url.prototype.resolveObject = function (relative) { var authInHost = result.host && result.host.indexOf("@") > 0 ? result.host.split("@") : false; if (authInHost) { result.auth = authInHost.shift(); - result.hostname = authInHost.shift(); - result.host = result.hostname; + result.hostname = result.host = 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.path = + (result.pathname ? result.pathname : "") + // force line break + (result.search ? result.search : ""); } result.href = result.format(); return result; @@ -712,8 +779,10 @@ Url.prototype.resolveObject = function (relative) { * 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 === ""; + // prettier-ignore + var hasTrailingSlash = ( + ((result.host || relative.host || srcPath.length > 1) && + (last === "." || last === "..")) || last === ""); /* * strip single dots, resolve double dots to parent dir @@ -762,12 +831,11 @@ Url.prototype.resolveObject = function (relative) { var authInHost = result.host && result.host.indexOf("@") > 0 ? result.host.split("@") : false; if (authInHost) { result.auth = authInHost.shift(); - result.hostname = authInHost.shift(); - result.host = result.hostname; + result.hostname = result.host = authInHost.shift(); } } - mustEndAbs = mustEndAbs || (result.host && srcPath.length); + mustEndAbs ||= result.host && srcPath.length; if (mustEndAbs && !isAbsolute) { srcPath.unshift(""); @@ -782,7 +850,9 @@ Url.prototype.resolveObject = function (relative) { // to support request.http if (result.pathname !== null || result.search !== null) { - result.path = (result.pathname ? result.pathname : "") + (result.search ? result.search : ""); + // prettier-ignore + result.path = (result.pathname ? result.pathname : "") + + (result.search ? result.search : ""); } result.auth = relative.auth || result.auth; result.slashes = result.slashes || relative.slashes; @@ -790,21 +860,41 @@ Url.prototype.resolveObject = function (relative) { return result; }; -Url.prototype.parseHost = function () { +Url.prototype.parseHost = function parseHost() { var host = this.host; var port = portPattern.exec(host); if (port) { port = port[0]; if (port !== ":") { - this.port = port.substr(1); + this.port = port.slice(1); } - host = host.substr(0, host.length - port.length); - } - if (host) { - this.hostname = host; + host = host.slice(0, host.length - port.length); } + if (host) this.hostname = host; }; +/** + * Add new characters as needed from + * [here](https://github.com/nodejs/node/blob/main/lib/internal/constants.js). + * + * @note Do not move to another file, otherwise const enums will be imported as an object + * instead of being inlined. + */ +// prettier-ignore +const enum Char { + // non-alphabetic characters + AT = 64, // @ + COLON = 58, // : + BACKWARD_SLASH = 92, // \ + FORWARD_SLASH = 47, // / + HASH = 35, // # + QUESTION_MARK = 63, // ? + + // whitespace + NO_BREAK_SPACE = 160, // \u00A0 + ZERO_WIDTH_NOBREAK_SPACE = 65279, // \uFEFF +} + export default { parse: urlParse, resolve: urlResolve, diff --git a/test/js/bun/plugin/plugins.test.ts b/test/js/bun/plugin/plugins.test.ts index d544fb953bd805..2da1afa169f98d 100644 --- a/test/js/bun/plugin/plugins.test.ts +++ b/test/js/bun/plugin/plugins.test.ts @@ -187,7 +187,7 @@ plugin({ // This is to test that it works when imported from a separate file import "../../third_party/svelte"; import "./module-plugins"; -import { render as svelteRender } from 'svelte/server'; +import { render as svelteRender } from "svelte/server"; describe("require", () => { it("SSRs `

Hello world!

` with Svelte", () => { @@ -476,7 +476,11 @@ describe("errors", () => { return new Response(result); }, }); - const { default: text } = await import(`http://${server.hostname}:${server.port}/hey.txt`); + const sleep = ms => new Promise(res => setTimeout(() => res("timeout"), ms)); + const text = await Promise.race([ + import(`http://${server.hostname}:${server.port}/hey.txt`).then(mod => mod.default) as Promise, + sleep(2_500), + ]); expect(text).toBe(result); }); }); diff --git a/test/js/node/harness.ts b/test/js/node/harness.ts index c13b3a5afc4ce2..52bfb3c33a8039 100644 --- a/test/js/node/harness.ts +++ b/test/js/node/harness.ts @@ -315,12 +315,27 @@ if (normalized.includes("node/test/parallel")) { return (activeSuite = contexts[key] ??= createContext(key)); } - async function test(label: string | Function, fn?: Function | undefined) { + async function test( + label: string | Function, + optionsOrFn: Record | Function, + fn?: Function | undefined, + ) { + let options = optionsOrFn; + if (arguments.length === 2) { + assertNode.equal(typeof optionsOrFn, "function", "Second argument to test() must be a function."); + fn = optionsOrFn as Function; + options = {}; + } if (typeof fn !== "function" && typeof label === "function") { fn = label; label = fn.name; + options = {}; } + const ctx = getContext(); + const { skip } = options; + + if (skip) return; try { ctx.testStack.push(label as string); await fn(); diff --git a/test/js/node/test/parallel/needs-test/test-url-format-invalid-input.js b/test/js/node/test/parallel/needs-test/test-url-format-invalid-input.js new file mode 100644 index 00000000000000..7ccb472a8d03a9 --- /dev/null +++ b/test/js/node/test/parallel/needs-test/test-url-format-invalid-input.js @@ -0,0 +1,32 @@ +'use strict'; + +require('../../common'); + +const assert = require('node:assert'); +const url = require('node:url'); +const { test } = require('node:test'); + +test('format invalid input', () => { + const throwsObjsAndReportTypes = [ + undefined, + null, + true, + false, + 0, + function() {}, + Symbol('foo'), + ]; + + for (const urlObject of throwsObjsAndReportTypes) { + console.log(urlObject) + assert.throws(function runFormat() { + url.format(urlObject); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }); + } + assert.strictEqual(url.format(''), ''); + assert.strictEqual(url.format({}), ''); +}); + diff --git a/test/js/node/test/parallel/test-url-is-url-internal.js b/test/js/node/test/parallel/test-url-is-url-internal.js new file mode 100644 index 00000000000000..9936b6c330bbf0 --- /dev/null +++ b/test/js/node/test/parallel/test-url-is-url-internal.js @@ -0,0 +1,22 @@ +// NOTE (@DonIsaac) this file tests node internals, which Bun does not match. +// We aim for API compatability, but make no guarantees about internals. +// to. +// // Flags: --expose-internals +// 'use strict'; + +// require('../common'); + +// const { URL, parse } = require('node:url'); +// const assert = require('node:assert'); +// const { isURL } = require('internal/url'); +// const { test } = require('node:test'); + +// test('isURL', () => { +// assert.strictEqual(isURL(new URL('https://www.nodejs.org')), true); +// assert.strictEqual(isURL(parse('https://www.nodejs.org')), false); +// assert.strictEqual(isURL({ +// href: 'https://www.nodejs.org', +// protocol: 'https:', +// path: '/', +// }), false); +// }); diff --git a/test/js/node/test/parallel/test-url-parse-query.js b/test/js/node/test/parallel/test-url-parse-query.js new file mode 100644 index 00000000000000..c9074d74281ee6 --- /dev/null +++ b/test/js/node/test/parallel/test-url-parse-query.js @@ -0,0 +1,102 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const url = require('url'); + +process.emitWarning = () => {} +function createWithNoPrototype(properties = []) { + const noProto = { __proto__: null }; + properties.forEach((property) => { + noProto[property.key] = property.value; + }); + return noProto; +} + +function check(actual, expected) { + // NOTE: Node creates a new object with no prototype when parsing queries. + // Their query parsing logic is written in JS. We re-use URLSearchParams + // from WebCore, which is spec-compliant with newer standards. + // assert.notStrictEqual(Object.getPrototypeOf(actual), Object.prototype); + assert.deepStrictEqual(Object.keys(actual).sort(), + Object.keys(expected).sort()); + Object.keys(expected).forEach(function(key) { + assert.deepStrictEqual( + actual[key], + expected[key], + `actual[${key}] !== expected[${key}]: ${actual[key]} !== ${expected[key]}` + ); + }); +} + +const parseTestsWithQueryString = { + '/foo/bar?baz=quux#frag': { + href: '/foo/bar?baz=quux#frag', + hash: '#frag', + search: '?baz=quux', + query: createWithNoPrototype([{ key: 'baz', value: 'quux' }]), + pathname: '/foo/bar', + path: '/foo/bar?baz=quux' + }, + 'http://example.com': { + href: 'http://example.com/', + protocol: 'http:', + slashes: true, + host: 'example.com', + hostname: 'example.com', + query: createWithNoPrototype(), + search: null, + pathname: '/', + path: '/' + }, + '/example': { + protocol: null, + slashes: null, + auth: undefined, + host: null, + port: null, + hostname: null, + hash: null, + search: null, + query: createWithNoPrototype(), + pathname: '/example', + path: '/example', + href: '/example' + }, + '/example?query=value': { + protocol: null, + slashes: null, + auth: undefined, + host: null, + port: null, + hostname: null, + hash: null, + search: '?query=value', + query: createWithNoPrototype([{ key: 'query', value: 'value' }]), + pathname: '/example', + path: '/example?query=value', + href: '/example?query=value' + } +}; +for (const u in parseTestsWithQueryString) { + const actual = url.parse(u, true); + const expected = Object.assign(new url.Url(), parseTestsWithQueryString[u]); + for (const i in actual) { + if (actual[i] === null && expected[i] === undefined) { + expected[i] = null; + } + } + + const properties = Object.keys(actual).sort(); + assert.deepStrictEqual(properties, Object.keys(expected).sort()); + properties.forEach((property) => { + if (property === 'query') { + check(actual[property], expected[property]); + } else { + assert.deepStrictEqual( + actual[property], + expected[property], + `${u}\n\nactual['${property}'] !== expected['${property}']: ${actual[property]} !== ${expected[property]}` + ); + } + }); +} diff --git a/test/js/node/test/parallel/test-url-relative.js b/test/js/node/test/parallel/test-url-relative.js new file mode 100644 index 00000000000000..2751ec3b512584 --- /dev/null +++ b/test/js/node/test/parallel/test-url-relative.js @@ -0,0 +1,443 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const inspect = require('util').inspect; +const url = require('url'); + +// When source is false +assert.strictEqual(url.resolveObject('', 'foo'), 'foo'); + +// [from, path, expected] +const relativeTests = [ + ['/foo/bar/baz', 'quux', '/foo/bar/quux'], + ['/foo/bar/baz', 'quux/asdf', '/foo/bar/quux/asdf'], + ['/foo/bar/baz', 'quux/baz', '/foo/bar/quux/baz'], + ['/foo/bar/baz', '../quux/baz', '/foo/quux/baz'], + ['/foo/bar/baz', '/bar', '/bar'], + ['/foo/bar/baz/', 'quux', '/foo/bar/baz/quux'], + ['/foo/bar/baz/', 'quux/baz', '/foo/bar/baz/quux/baz'], + ['/foo/bar/baz', '../../../../../../../../quux/baz', '/quux/baz'], + ['/foo/bar/baz', '../../../../../../../quux/baz', '/quux/baz'], + ['/foo', '.', '/'], + ['/foo', '..', '/'], + ['/foo/', '.', '/foo/'], + ['/foo/', '..', '/'], + ['/foo/bar', '.', '/foo/'], + ['/foo/bar', '..', '/'], + ['/foo/bar/', '.', '/foo/bar/'], + ['/foo/bar/', '..', '/foo/'], + ['foo/bar', '../../../baz', '../../baz'], + ['foo/bar/', '../../../baz', '../baz'], + ['http://example.com/b//c//d;p?q#blarg', 'https:#hash2', 'https:///#hash2'], + ['http://example.com/b//c//d;p?q#blarg', + 'https:/p/a/t/h?s#hash2', + 'https://p/a/t/h?s#hash2'], + ['http://example.com/b//c//d;p?q#blarg', + 'https://u:p@h.com/p/a/t/h?s#hash2', + 'https://u:p@h.com/p/a/t/h?s#hash2'], + ['http://example.com/b//c//d;p?q#blarg', + 'https:/a/b/c/d', + 'https://a/b/c/d'], + ['http://example.com/b//c//d;p?q#blarg', + 'http:#hash2', + 'http://example.com/b//c//d;p?q#hash2'], + ['http://example.com/b//c//d;p?q#blarg', + 'http:/p/a/t/h?s#hash2', + 'http://example.com/p/a/t/h?s#hash2'], + ['http://example.com/b//c//d;p?q#blarg', + 'http://u:p@h.com/p/a/t/h?s#hash2', + 'http://u:p@h.com/p/a/t/h?s#hash2'], + ['http://example.com/b//c//d;p?q#blarg', + 'http:/a/b/c/d', + 'http://example.com/a/b/c/d'], + ['/foo/bar/baz', '/../etc/passwd', '/etc/passwd'], + ['http://localhost', 'file:///Users/foo', 'file:///Users/foo'], + ['http://localhost', 'file://foo/Users', 'file://foo/Users'], + ['https://registry.npmjs.org', '@foo/bar', 'https://registry.npmjs.org/@foo/bar'], +]; +for (let i = 0; i < relativeTests.length; i++) { + const relativeTest = relativeTests[i]; + + const a = url.resolve(relativeTest[0], relativeTest[1]); + const e = relativeTest[2]; + assert.strictEqual(a, e, + `resolve(${relativeTest[0]}, ${relativeTest[1]})` + + ` == ${e}\n actual=${a}`); +} + +// +// Tests below taken from Chiron +// http://code.google.com/p/chironjs/source/browse/trunk/src/test/http/url.js +// +// Copyright (c) 2002-2008 Kris Kowal +// used with permission under MIT License +// +// Changes marked with @isaacs + +const bases = [ + 'http://a/b/c/d;p?q', + 'http://a/b/c/d;p?q=1/2', + 'http://a/b/c/d;p=1/2?q', + 'fred:///s//a/b/c', + 'http:///s//a/b/c', +]; + +// [to, from, result] +const relativeTests2 = [ + // http://lists.w3.org/Archives/Public/uri/2004Feb/0114.html + ['../c', 'foo:a/b', 'foo:c'], + ['foo:.', 'foo:a', 'foo:'], + ['/foo/../../../bar', 'zz:abc', 'zz:/bar'], + ['/foo/../bar', 'zz:abc', 'zz:/bar'], + // @isaacs Disagree. Not how web browsers resolve this. + ['foo/../../../bar', 'zz:abc', 'zz:bar'], + // ['foo/../../../bar', 'zz:abc', 'zz:../../bar'], // @isaacs Added + ['foo/../bar', 'zz:abc', 'zz:bar'], + ['zz:.', 'zz:abc', 'zz:'], + ['/.', bases[0], 'http://a/'], + ['/.foo', bases[0], 'http://a/.foo'], + ['.foo', bases[0], 'http://a/b/c/.foo'], + + // http://gbiv.com/protocols/uri/test/rel_examples1.html + // examples from RFC 2396 + ['g:h', bases[0], 'g:h'], + ['g', bases[0], 'http://a/b/c/g'], + ['./g', bases[0], 'http://a/b/c/g'], + ['g/', bases[0], 'http://a/b/c/g/'], + ['/g', bases[0], 'http://a/g'], + ['//g', bases[0], 'http://g/'], + // Changed with RFC 2396bis + // ('?y', bases[0], 'http://a/b/c/d;p?y'], + ['?y', bases[0], 'http://a/b/c/d;p?y'], + ['g?y', bases[0], 'http://a/b/c/g?y'], + // Changed with RFC 2396bis + // ('#s', bases[0], CURRENT_DOC_URI + '#s'], + ['#s', bases[0], 'http://a/b/c/d;p?q#s'], + ['g#s', bases[0], 'http://a/b/c/g#s'], + ['g?y#s', bases[0], 'http://a/b/c/g?y#s'], + [';x', bases[0], 'http://a/b/c/;x'], + ['g;x', bases[0], 'http://a/b/c/g;x'], + ['g;x?y#s', bases[0], 'http://a/b/c/g;x?y#s'], + // Changed with RFC 2396bis + // ('', bases[0], CURRENT_DOC_URI], + ['', bases[0], 'http://a/b/c/d;p?q'], + ['.', bases[0], 'http://a/b/c/'], + ['./', bases[0], 'http://a/b/c/'], + ['..', bases[0], 'http://a/b/'], + ['../', bases[0], 'http://a/b/'], + ['../g', bases[0], 'http://a/b/g'], + ['../..', bases[0], 'http://a/'], + ['../../', bases[0], 'http://a/'], + ['../../g', bases[0], 'http://a/g'], + ['../../../g', bases[0], ('http://a/../g', 'http://a/g')], + ['../../../../g', bases[0], ('http://a/../../g', 'http://a/g')], + // Changed with RFC 2396bis + // ('/./g', bases[0], 'http://a/./g'], + ['/./g', bases[0], 'http://a/g'], + // Changed with RFC 2396bis + // ('/../g', bases[0], 'http://a/../g'], + ['/../g', bases[0], 'http://a/g'], + ['g.', bases[0], 'http://a/b/c/g.'], + ['.g', bases[0], 'http://a/b/c/.g'], + ['g..', bases[0], 'http://a/b/c/g..'], + ['..g', bases[0], 'http://a/b/c/..g'], + ['./../g', bases[0], 'http://a/b/g'], + ['./g/.', bases[0], 'http://a/b/c/g/'], + ['g/./h', bases[0], 'http://a/b/c/g/h'], + ['g/../h', bases[0], 'http://a/b/c/h'], + ['g;x=1/./y', bases[0], 'http://a/b/c/g;x=1/y'], + ['g;x=1/../y', bases[0], 'http://a/b/c/y'], + ['g?y/./x', bases[0], 'http://a/b/c/g?y/./x'], + ['g?y/../x', bases[0], 'http://a/b/c/g?y/../x'], + ['g#s/./x', bases[0], 'http://a/b/c/g#s/./x'], + ['g#s/../x', bases[0], 'http://a/b/c/g#s/../x'], + ['http:g', bases[0], ('http:g', 'http://a/b/c/g')], + ['http:', bases[0], ('http:', bases[0])], + // Not sure where this one originated + ['/a/b/c/./../../g', bases[0], 'http://a/a/g'], + + // http://gbiv.com/protocols/uri/test/rel_examples2.html + // slashes in base URI's query args + ['g', bases[1], 'http://a/b/c/g'], + ['./g', bases[1], 'http://a/b/c/g'], + ['g/', bases[1], 'http://a/b/c/g/'], + ['/g', bases[1], 'http://a/g'], + ['//g', bases[1], 'http://g/'], + // Changed in RFC 2396bis + // ('?y', bases[1], 'http://a/b/c/?y'], + ['?y', bases[1], 'http://a/b/c/d;p?y'], + ['g?y', bases[1], 'http://a/b/c/g?y'], + ['g?y/./x', bases[1], 'http://a/b/c/g?y/./x'], + ['g?y/../x', bases[1], 'http://a/b/c/g?y/../x'], + ['g#s', bases[1], 'http://a/b/c/g#s'], + ['g#s/./x', bases[1], 'http://a/b/c/g#s/./x'], + ['g#s/../x', bases[1], 'http://a/b/c/g#s/../x'], + ['./', bases[1], 'http://a/b/c/'], + ['../', bases[1], 'http://a/b/'], + ['../g', bases[1], 'http://a/b/g'], + ['../../', bases[1], 'http://a/'], + ['../../g', bases[1], 'http://a/g'], + + // http://gbiv.com/protocols/uri/test/rel_examples3.html + // slashes in path params + // all of these changed in RFC 2396bis + ['g', bases[2], 'http://a/b/c/d;p=1/g'], + ['./g', bases[2], 'http://a/b/c/d;p=1/g'], + ['g/', bases[2], 'http://a/b/c/d;p=1/g/'], + ['g?y', bases[2], 'http://a/b/c/d;p=1/g?y'], + [';x', bases[2], 'http://a/b/c/d;p=1/;x'], + ['g;x', bases[2], 'http://a/b/c/d;p=1/g;x'], + ['g;x=1/./y', bases[2], 'http://a/b/c/d;p=1/g;x=1/y'], + ['g;x=1/../y', bases[2], 'http://a/b/c/d;p=1/y'], + ['./', bases[2], 'http://a/b/c/d;p=1/'], + ['../', bases[2], 'http://a/b/c/'], + ['../g', bases[2], 'http://a/b/c/g'], + ['../../', bases[2], 'http://a/b/'], + ['../../g', bases[2], 'http://a/b/g'], + + // http://gbiv.com/protocols/uri/test/rel_examples4.html + // double and triple slash, unknown scheme + ['g:h', bases[3], 'g:h'], + ['g', bases[3], 'fred:///s//a/b/g'], + ['./g', bases[3], 'fred:///s//a/b/g'], + ['g/', bases[3], 'fred:///s//a/b/g/'], + ['/g', bases[3], 'fred:///g'], // May change to fred:///s//a/g + ['//g', bases[3], 'fred://g'], // May change to fred:///s//g + ['//g/x', bases[3], 'fred://g/x'], // May change to fred:///s//g/x + ['///g', bases[3], 'fred:///g'], + ['./', bases[3], 'fred:///s//a/b/'], + ['../', bases[3], 'fred:///s//a/'], + ['../g', bases[3], 'fred:///s//a/g'], + + ['../../', bases[3], 'fred:///s//'], + ['../../g', bases[3], 'fred:///s//g'], + ['../../../g', bases[3], 'fred:///s/g'], + // May change to fred:///s//a/../../../g + ['../../../../g', bases[3], 'fred:///g'], + + // http://gbiv.com/protocols/uri/test/rel_examples5.html + // double and triple slash, well-known scheme + ['g:h', bases[4], 'g:h'], + ['g', bases[4], 'http:///s//a/b/g'], + ['./g', bases[4], 'http:///s//a/b/g'], + ['g/', bases[4], 'http:///s//a/b/g/'], + ['/g', bases[4], 'http:///g'], // May change to http:///s//a/g + ['//g', bases[4], 'http://g/'], // May change to http:///s//g + ['//g/x', bases[4], 'http://g/x'], // May change to http:///s//g/x + ['///g', bases[4], 'http:///g'], + ['./', bases[4], 'http:///s//a/b/'], + ['../', bases[4], 'http:///s//a/'], + ['../g', bases[4], 'http:///s//a/g'], + ['../../', bases[4], 'http:///s//'], + ['../../g', bases[4], 'http:///s//g'], + // May change to http:///s//a/../../g + ['../../../g', bases[4], 'http:///s/g'], + // May change to http:///s//a/../../../g + ['../../../../g', bases[4], 'http:///g'], + + // From Dan Connelly's tests in http://www.w3.org/2000/10/swap/uripath.py + ['bar:abc', 'foo:xyz', 'bar:abc'], + ['../abc', 'http://example/x/y/z', 'http://example/x/abc'], + ['http://example/x/abc', 'http://example2/x/y/z', 'http://example/x/abc'], + ['../r', 'http://ex/x/y/z', 'http://ex/x/r'], + ['q/r', 'http://ex/x/y', 'http://ex/x/q/r'], + ['q/r#s', 'http://ex/x/y', 'http://ex/x/q/r#s'], + ['q/r#s/t', 'http://ex/x/y', 'http://ex/x/q/r#s/t'], + ['ftp://ex/x/q/r', 'http://ex/x/y', 'ftp://ex/x/q/r'], + ['', 'http://ex/x/y', 'http://ex/x/y'], + ['', 'http://ex/x/y/', 'http://ex/x/y/'], + ['', 'http://ex/x/y/pdq', 'http://ex/x/y/pdq'], + ['z/', 'http://ex/x/y/', 'http://ex/x/y/z/'], + ['#Animal', + 'file:/swap/test/animal.rdf', + 'file:/swap/test/animal.rdf#Animal'], + ['../abc', 'file:/e/x/y/z', 'file:/e/x/abc'], + ['/example/x/abc', 'file:/example2/x/y/z', 'file:/example/x/abc'], + ['../r', 'file:/ex/x/y/z', 'file:/ex/x/r'], + ['/r', 'file:/ex/x/y/z', 'file:/r'], + ['q/r', 'file:/ex/x/y', 'file:/ex/x/q/r'], + ['q/r#s', 'file:/ex/x/y', 'file:/ex/x/q/r#s'], + ['q/r#', 'file:/ex/x/y', 'file:/ex/x/q/r#'], + ['q/r#s/t', 'file:/ex/x/y', 'file:/ex/x/q/r#s/t'], + ['ftp://ex/x/q/r', 'file:/ex/x/y', 'ftp://ex/x/q/r'], + ['', 'file:/ex/x/y', 'file:/ex/x/y'], + ['', 'file:/ex/x/y/', 'file:/ex/x/y/'], + ['', 'file:/ex/x/y/pdq', 'file:/ex/x/y/pdq'], + ['z/', 'file:/ex/x/y/', 'file:/ex/x/y/z/'], + ['file://meetings.example.com/cal#m1', + 'file:/devel/WWW/2000/10/swap/test/reluri-1.n3', + 'file://meetings.example.com/cal#m1'], + ['file://meetings.example.com/cal#m1', + 'file:/home/connolly/w3ccvs/WWW/2000/10/swap/test/reluri-1.n3', + 'file://meetings.example.com/cal#m1'], + ['./#blort', 'file:/some/dir/foo', 'file:/some/dir/#blort'], + ['./#', 'file:/some/dir/foo', 'file:/some/dir/#'], + // Ryan Lee + ['./', 'http://example/x/abc.efg', 'http://example/x/'], + + + // Graham Klyne's tests + // http://www.ninebynine.org/Software/HaskellUtils/Network/UriTest.xls + // 01-31 are from Connelly's cases + + // 32-49 + ['./q:r', 'http://ex/x/y', 'http://ex/x/q:r'], + ['./p=q:r', 'http://ex/x/y', 'http://ex/x/p=q:r'], + ['?pp/rr', 'http://ex/x/y?pp/qq', 'http://ex/x/y?pp/rr'], + ['y/z', 'http://ex/x/y?pp/qq', 'http://ex/x/y/z'], + ['local/qual@domain.org#frag', + 'mailto:local', + 'mailto:local/qual@domain.org#frag'], + ['more/qual2@domain2.org#frag', + 'mailto:local/qual1@domain1.org', + 'mailto:local/more/qual2@domain2.org#frag'], + ['y?q', 'http://ex/x/y?q', 'http://ex/x/y?q'], + ['/x/y?q', 'http://ex?p', 'http://ex/x/y?q'], + ['c/d', 'foo:a/b', 'foo:a/c/d'], + ['/c/d', 'foo:a/b', 'foo:/c/d'], + ['', 'foo:a/b?c#d', 'foo:a/b?c'], + ['b/c', 'foo:a', 'foo:b/c'], + ['../b/c', 'foo:/a/y/z', 'foo:/a/b/c'], + ['./b/c', 'foo:a', 'foo:b/c'], + ['/./b/c', 'foo:a', 'foo:/b/c'], + ['../../d', 'foo://a//b/c', 'foo://a/d'], + ['.', 'foo:a', 'foo:'], + ['..', 'foo:a', 'foo:'], + + // 50-57[cf. TimBL comments -- + // http://lists.w3.org/Archives/Public/uri/2003Feb/0028.html, + // http://lists.w3.org/Archives/Public/uri/2003Jan/0008.html) + ['abc', 'http://example/x/y%2Fz', 'http://example/x/abc'], + ['../../x%2Fabc', 'http://example/a/x/y/z', 'http://example/a/x%2Fabc'], + ['../x%2Fabc', 'http://example/a/x/y%2Fz', 'http://example/a/x%2Fabc'], + ['abc', 'http://example/x%2Fy/z', 'http://example/x%2Fy/abc'], + ['q%3Ar', 'http://ex/x/y', 'http://ex/x/q%3Ar'], + ['/x%2Fabc', 'http://example/x/y%2Fz', 'http://example/x%2Fabc'], + ['/x%2Fabc', 'http://example/x/y/z', 'http://example/x%2Fabc'], + ['/x%2Fabc', 'http://example/x/y%2Fz', 'http://example/x%2Fabc'], + + // 70-77 + ['local2@domain2', 'mailto:local1@domain1?query1', 'mailto:local2@domain2'], + ['local2@domain2?query2', + 'mailto:local1@domain1', + 'mailto:local2@domain2?query2'], + ['local2@domain2?query2', + 'mailto:local1@domain1?query1', + 'mailto:local2@domain2?query2'], + ['?query2', 'mailto:local@domain?query1', 'mailto:local@domain?query2'], + ['local@domain?query2', 'mailto:?query1', 'mailto:local@domain?query2'], + ['?query2', 'mailto:local@domain?query1', 'mailto:local@domain?query2'], + ['http://example/a/b?c/../d', 'foo:bar', 'http://example/a/b?c/../d'], + ['http://example/a/b#c/../d', 'foo:bar', 'http://example/a/b#c/../d'], + + // 82-88 + // @isaacs Disagree. Not how browsers do it. + // ['http:this', 'http://example.org/base/uri', 'http:this'], + // @isaacs Added + ['http:this', 'http://example.org/base/uri', 'http://example.org/base/this'], + ['http:this', 'http:base', 'http:this'], + ['.//g', 'f:/a', 'f://g'], + ['b/c//d/e', 'f://example.org/base/a', 'f://example.org/base/b/c//d/e'], + ['m2@example.ord/c2@example.org', + 'mid:m@example.ord/c@example.org', + 'mid:m@example.ord/m2@example.ord/c2@example.org'], + ['mini1.xml', + 'file:///C:/DEV/Haskell/lib/HXmlToolbox-3.01/examples/', + 'file:///C:/DEV/Haskell/lib/HXmlToolbox-3.01/examples/mini1.xml'], + ['../b/c', 'foo:a/y/z', 'foo:a/b/c'], + + // changing auth + ['http://diff:auth@www.example.com', + 'http://asdf:qwer@www.example.com', + 'http://diff:auth@www.example.com/'], + + // changing port + ['https://example.com:81/', + 'https://example.com:82/', + 'https://example.com:81/'], + + // https://github.com/nodejs/node/issues/1435 + ['https://another.host.com/', + 'https://user:password@example.org/', + 'https://another.host.com/'], + ['//another.host.com/', + 'https://user:password@example.org/', + 'https://another.host.com/'], + ['http://another.host.com/', + 'https://user:password@example.org/', + 'http://another.host.com/'], + ['mailto:another.host.com', + 'mailto:user@example.org', + 'mailto:another.host.com'], + ['https://example.com/foo', + 'https://user:password@example.com', + 'https://user:password@example.com/foo'], + + // No path at all + ['#hash1', '#hash2', '#hash1'], +]; +for (let i = 0; i < relativeTests2.length; i++) { + const relativeTest = relativeTests2[i]; + + const a = url.resolve(relativeTest[1], relativeTest[0]); + const e = url.format(relativeTest[2]); + assert.strictEqual(a, e, + `resolve(${relativeTest[0]}, ${relativeTest[1]})` + + ` == ${e}\n actual=${a}`); +} + +// If format and parse are inverse operations then +// resolveObject(parse(x), y) == parse(resolve(x, y)) + +// format: [from, path, expected] +for (let i = 0; i < relativeTests.length; i++) { + const relativeTest = relativeTests[i]; + + let actual = url.resolveObject(url.parse(relativeTest[0]), relativeTest[1]); + let expected = url.parse(relativeTest[2]); + + + assert.deepStrictEqual(actual, expected); + + expected = relativeTest[2]; + actual = url.format(actual); + + assert.strictEqual(actual, expected, + `format(${actual}) == ${expected}\n` + + `actual: ${actual}`); + +} + +// format: [to, from, result] +// the test: ['.//g', 'f:/a', 'f://g'] is a fundamental problem +// url.parse('f:/a') does not have a host +// url.resolve('f:/a', './/g') does not have a host because you have moved +// down to the g directory. i.e. f: //g, however when this url is parsed +// f:// will indicate that the host is g which is not the case. +// it is unclear to me how to keep this information from being lost +// it may be that a pathname of ////g should collapse to /g but this seems +// to be a lot of work for an edge case. Right now I remove the test +if (relativeTests2[181][0] === './/g' && + relativeTests2[181][1] === 'f:/a' && + relativeTests2[181][2] === 'f://g') { + relativeTests2.splice(181, 1); +} +for (let i = 0; i < relativeTests2.length; i++) { + const relativeTest = relativeTests2[i]; + + let actual = url.resolveObject(url.parse(relativeTest[1]), relativeTest[0]); + let expected = url.parse(relativeTest[2]); + + assert.deepStrictEqual( + actual, + expected, + `expected ${inspect(expected)} but got ${inspect(actual)}` + ); + + expected = url.format(relativeTest[2]); + actual = url.format(actual); + + assert.strictEqual(actual, expected, + `format(${relativeTest[1]}) == ${expected}\n` + + `actual: ${actual}`); +}