Skip to content
File

Blob: src/node/internal/legacy_url.ts

typescript1024 lines
1// Copyright (c) 2026 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5import { validateString, validateObject } from 'node-internal:validators';
6import {
7 CHAR_SPACE,
8 CHAR_TAB,
9 CHAR_CARRIAGE_RETURN,
10 CHAR_LINE_FEED,
11 CHAR_NO_BREAK_SPACE,
12 CHAR_ZERO_WIDTH_NOBREAK_SPACE,
13 CHAR_HASH,
14 CHAR_FORWARD_SLASH,
15 CHAR_LEFT_SQUARE_BRACKET,
16 CHAR_RIGHT_SQUARE_BRACKET,
17 CHAR_LEFT_ANGLE_BRACKET,
18 CHAR_RIGHT_ANGLE_BRACKET,
19 CHAR_LEFT_CURLY_BRACKET,
20 CHAR_RIGHT_CURLY_BRACKET,
21 CHAR_QUESTION_MARK,
22 CHAR_DOUBLE_QUOTE,
23 CHAR_SINGLE_QUOTE,
24 CHAR_PERCENT,
25 CHAR_SEMICOLON,
26 CHAR_BACKWARD_SLASH,
27 CHAR_CIRCUMFLEX_ACCENT,
28 CHAR_GRAVE_ACCENT,
29 CHAR_VERTICAL_LINE,
30 CHAR_AT,
31 CHAR_COLON,
32} from 'node-internal:constants';
33import {
34 parse as querystringParse,
35 stringify as querystringStringify,
36 encodeStr,
37 hexTable,
38} from 'node-internal:internal_querystring';
39import {
40 slashedProtocol,
41 hostlessProtocol,
42 unsafeProtocol,
43} from 'node-internal:internal_url';
44import {
45 ERR_INVALID_URL,
46 ERR_INVALID_ARG_TYPE,
47} from 'node-internal:internal_errors';
48import { default as urlUtil } from 'node-internal:url';
49import { spliceOne } from 'node-internal:internal_utils';
50import type { URLFormatOptions } from 'node:url';
51 
52// Reference: RFC 3986, RFC 1808, RFC 2396
53 
54// define these here so at least they only have to be
55// compiled once on the first module load.
56const protocolPattern = /^[a-z0-9.+-]+:/i;
57const portPattern = /:[0-9]*$/;
58const hostPattern = /^\/\/[^@/]+@[^@/]+/;
59 
60// Special case for a simple path URL
61const simplePathPattern = /^(\/\/?(?!\/)[^?\s]*)(\?[^\s]*)?$/;
62 
63const hostnameMaxLen = 255;
64 
65// This prevents some common spoofing bugs due to our use of IDNA toASCII. For
66// compatibility, the set of characters we use here is the *intersection* of
67// "forbidden host code point" in the WHATWG URL Standard [1] and the
68// characters in the host parsing loop in Url.prototype.parse, with the
69// following additions:
70//
71// - ':' since this could cause a "protocol spoofing" bug
72// - '@' since this could cause parts of the hostname to be confused with auth
73// - '[' and ']' since this could cause a non-IPv6 hostname to be interpreted
74// as IPv6 by isIpv6Hostname above
75//
76// [1]: https://url.spec.whatwg.org/#forbidden-host-code-point
77const forbiddenHostChars = /[\0\t\n\r #%/:<>?@[\\\]^|]/;
78// For IPv6, permit '[', ']', and ':'.
79const forbiddenHostCharsIpv6 = /[\0\t\n\r #%/<>?@\\^|]/;
80 
81function getHostname(self: typeof Url, rest: string, hostname: string): string {
82 for (let i = 0; i < hostname.length; ++i) {
83 const code = hostname.charCodeAt(i);
84 const isValid =
85 code !== CHAR_FORWARD_SLASH &&
86 code !== CHAR_BACKWARD_SLASH &&
87 code !== CHAR_HASH &&
88 code !== CHAR_QUESTION_MARK &&
89 code !== CHAR_COLON;
90 
91 if (!isValid) {
92 self.hostname = hostname.slice(0, i);
93 return `/${hostname.slice(i)}${rest}`;
94 }
95 }
96 return rest;
97}
98 
99function isIpv6Hostname(hostname: string): boolean {
100 return (
101 hostname.charCodeAt(0) === CHAR_LEFT_SQUARE_BRACKET &&
102 hostname.charCodeAt(hostname.length - 1) === CHAR_RIGHT_SQUARE_BRACKET
103 );
104}
105 
106// Escaped characters. Use empty strings to fill up unused entries.
107// Using Array is faster than Object/Map
108// prettier-ignore
109const escapedCodes = [
110 /* 0 - 9 */ '', '', '', '', '', '', '', '', '', '%09',
111 /* 10 - 19 */ '%0A', '', '', '%0D', '', '', '', '', '', '',
112 /* 20 - 29 */ '', '', '', '', '', '', '', '', '', '',
113 /* 30 - 39 */ '', '', '%20', '', '%22', '', '', '', '', '%27',
114 /* 40 - 49 */ '', '', '', '', '', '', '', '', '', '',
115 /* 50 - 59 */ '', '', '', '', '', '', '', '', '', '',
116 /* 60 - 69 */ '%3C', '', '%3E', '', '', '', '', '', '', '',
117 /* 70 - 79 */ '', '', '', '', '', '', '', '', '', '',
118 /* 80 - 89 */ '', '', '', '', '', '', '', '', '', '',
119 /* 90 - 99 */ '', '', '%5C', '', '%5E', '', '%60', '', '', '',
120 /* 100 - 109 */ '', '', '', '', '', '', '', '', '', '',
121 /* 110 - 119 */ '', '', '', '', '', '', '', '', '', '',
122 /* 120 - 125 */ '', '', '', '%7B', '%7C', '%7D',
123];
124 
125// Automatically escape all delimiters and unwise characters from RFC 2396.
126// Also escape single quotes in case of an XSS attack.
127// Return the escaped string.
128function autoEscapeStr(rest: string): string {
129 let escaped = '';
130 let lastEscapedPos = 0;
131 for (let i = 0; i < rest.length; ++i) {
132 // `escaped` contains substring up to the last escaped character.
133 const escapedChar = escapedCodes[rest.charCodeAt(i)];
134 if (escapedChar) {
135 // Concat if there are ordinary characters in the middle.
136 if (i > lastEscapedPos) escaped += rest.slice(lastEscapedPos, i);
137 escaped += escapedChar;
138 lastEscapedPos = i + 1;
139 }
140 }
141 if (lastEscapedPos === 0)
142 // Nothing has been escaped.
143 return rest;
144 
145 // There are ordinary characters at the end.
146 if (lastEscapedPos < rest.length) escaped += rest.slice(lastEscapedPos);
147 
148 return escaped;
149}
150 
151export const Url = function Url(this: Record<string, null>) {
152 this.protocol = null;
153 this.slashes = null;
154 this.auth = null;
155 this.host = null;
156 this.port = null;
157 this.hostname = null;
158 this.hash = null;
159 this.search = null;
160 this.query = null;
161 this.pathname = null;
162 this.path = null;
163 this.href = null;
164} as unknown as {
165 protocol?: string | null | undefined;
166 slashes?: boolean | null | undefined;
167 auth?: string | null | undefined;
168 host?: string | null | undefined;
169 port?: string | null | undefined;
170 hostname?: string | null | undefined;
171 hash?: string | null | undefined;
172 search?: string | null | undefined;
173 query?: Record<string, unknown> | string | null | undefined;
174 pathname?: string | null | undefined;
175 path?: string | null | undefined;
176 href?: string | null | undefined;
177 
178 new (): typeof Url;
179 prototype: typeof Url;
180 
181 parse(
182 url: string | typeof Url,
183 parseQueryString?: boolean,
184 slashesDenoteHost?: boolean
185 ): typeof Url;
186 parseHost(): void;
187 format(): string;
188 resolve(from: string): string;
189 resolveObject(input: string | typeof Url): typeof Url;
190 parseHost(): void;
191};
192 
193Url.prototype.parse = function parse(
194 this: typeof Url,
195 url: string | typeof Url,
196 parseQueryString?: boolean,
197 slashesDenoteHost?: boolean
198): typeof Url {
199 validateString(url, 'url');
200 
201 // Copy chrome, IE, opera backslash-handling behavior.
202 // Backslashes before the query string get converted to forward slashes
203 // See: https://code.google.com/p/chromium/issues/detail?id=25916
204 let hasHash = false;
205 let hasAt = false;
206 let start = -1;
207 let end = -1;
208 let rest = '';
209 let lastPos = 0;
210 for (let i = 0, inWs = false, split = false; i < url.length; ++i) {
211 const code = url.charCodeAt(i);
212 
213 // Find first and last non-whitespace characters for trimming
214 const isWs =
215 code < 33 ||
216 code === CHAR_NO_BREAK_SPACE ||
217 code === CHAR_ZERO_WIDTH_NOBREAK_SPACE;
218 if (start === -1) {
219 if (isWs) continue;
220 lastPos = start = i;
221 } else if (inWs) {
222 if (!isWs) {
223 end = -1;
224 inWs = false;
225 }
226 } else if (isWs) {
227 end = i;
228 inWs = true;
229 }
230 
231 // Only convert backslashes while we haven't seen a split character
232 if (!split) {
233 switch (code) {
234 case CHAR_AT:
235 hasAt = true;
236 break;
237 case CHAR_HASH:
238 hasHash = true;
239 split = true;
240 break;
241 case CHAR_QUESTION_MARK:
242 split = true;
243 break;
244 case CHAR_BACKWARD_SLASH:
245 if (i - lastPos > 0) rest += url.slice(lastPos, i);
246 rest += '/';
247 lastPos = i + 1;
248 break;
249 }
250 } else if (!hasHash && code === CHAR_HASH) {
251 hasHash = true;
252 }
253 }
254 
255 // Check if string was non-empty (including strings with only whitespace)
256 if (start !== -1) {
257 if (lastPos === start) {
258 // We didn't convert any backslashes
259 
260 if (end === -1) {
261 if (start === 0) rest = url;
262 else rest = url.slice(start);
263 } else {
264 rest = url.slice(start, end);
265 }
266 } else if (end === -1 && lastPos < url.length) {
267 // We converted some backslashes and have only part of the entire string
268 rest += url.slice(lastPos);
269 } else if (end !== -1 && lastPos < end) {
270 // We converted some backslashes and have only part of the entire string
271 rest += url.slice(lastPos, end);
272 }
273 }
274 
275 if (!slashesDenoteHost && !hasHash && !hasAt) {
276 // Try fast path regexp
277 const simplePath = simplePathPattern.exec(rest);
278 if (simplePath) {
279 this.path = rest;
280 this.href = rest;
281 this.pathname = simplePath[1];
282 if (simplePath[2]) {
283 this.search = simplePath[2];
284 if (parseQueryString) {
285 this.query = querystringParse(this.search.slice(1));
286 } else {
287 this.query = this.search.slice(1);
288 }
289 } else if (parseQueryString) {
290 this.search = null;
291 this.query = { __proto__: null };
292 }
293 return this;
294 }
295 }
296 
297 let proto: string | RegExpExecArray | null = protocolPattern.exec(rest);
298 let lowerProto;
299 if (proto) {
300 proto = proto[0];
301 lowerProto = proto.toLowerCase();
302 this.protocol = lowerProto;
303 rest = rest.slice(proto.length);
304 }
305 
306 // Figure out if it's got a host
307 // user@server is *always* interpreted as a hostname, and url
308 // resolution will treat //foo/bar as host=foo,path=bar because that's
309 // how the browser resolves relative URLs.
310 let slashes;
311 if (slashesDenoteHost || proto || hostPattern.test(rest)) {
312 slashes =
313 rest.charCodeAt(0) === CHAR_FORWARD_SLASH &&
314 rest.charCodeAt(1) === CHAR_FORWARD_SLASH;
315 if (slashes && !(proto && hostlessProtocol.has(lowerProto as string))) {
316 rest = rest.slice(2);
317 this.slashes = true;
318 }
319 }
320 
321 if (
322 !hostlessProtocol.has(lowerProto as string) &&
323 (slashes || (proto && !slashedProtocol.has(proto)))
324 ) {
325 // there's a hostname.
326 // the first instance of /, ?, ;, or # ends the host.
327 //
328 // If there is an @ in the hostname, then non-host chars *are* allowed
329 // to the left of the last @ sign, unless some host-ending character
330 // comes *before* the @-sign.
331 // URLs are obnoxious.
332 //
333 // ex:
334 // http://a@b@c/ => user:a@b host:c
335 // http://a@b?@c => user:a host:b path:/?@c
336 
337 let hostEnd = -1;
338 let atSign = -1;
339 let nonHost = -1;
340 for (let i = 0; i < rest.length; ++i) {
341 switch (rest.charCodeAt(i)) {
342 case CHAR_TAB:
343 case CHAR_LINE_FEED:
344 case CHAR_CARRIAGE_RETURN:
345 // WHATWG URL removes tabs, newlines, and carriage returns. Let's do that too.
346 rest = rest.slice(0, i) + rest.slice(i + 1);
347 i -= 1;
348 break;
349 case CHAR_SPACE:
350 case CHAR_DOUBLE_QUOTE:
351 case CHAR_PERCENT:
352 case CHAR_SINGLE_QUOTE:
353 case CHAR_SEMICOLON:
354 case CHAR_LEFT_ANGLE_BRACKET:
355 case CHAR_RIGHT_ANGLE_BRACKET:
356 case CHAR_BACKWARD_SLASH:
357 case CHAR_CIRCUMFLEX_ACCENT:
358 case CHAR_GRAVE_ACCENT:
359 case CHAR_LEFT_CURLY_BRACKET:
360 case CHAR_VERTICAL_LINE:
361 case CHAR_RIGHT_CURLY_BRACKET:
362 // Characters that are never ever allowed in a hostname from RFC 2396
363 if (nonHost === -1) nonHost = i;
364 break;
365 case CHAR_HASH:
366 case CHAR_FORWARD_SLASH:
367 case CHAR_QUESTION_MARK:
368 // Find the first instance of any host-ending characters
369 if (nonHost === -1) nonHost = i;
370 hostEnd = i;
371 break;
372 case CHAR_AT:
373 // At this point, either we have an explicit point where the
374 // auth portion cannot go past, or the last @ char is the decider.
375 atSign = i;
376 nonHost = -1;
377 break;
378 }
379 if (hostEnd !== -1) break;
380 }
381 start = 0;
382 if (atSign !== -1) {
383 this.auth = decodeURIComponent(rest.slice(0, atSign));
384 start = atSign + 1;
385 }
386 if (nonHost === -1) {
387 this.host = rest.slice(start);
388 rest = '';
389 } else {
390 this.host = rest.slice(start, nonHost);
391 rest = rest.slice(nonHost);
392 }
393 
394 // pull out port.
395 this.parseHost();
396 
397 // We've indicated that there is a hostname,
398 // so even if it's empty, it has to be present.
399 if (typeof this.hostname !== 'string') this.hostname = '';
400 
401 const hostname = this.hostname;
402 
403 // If hostname begins with [ and ends with ]
404 // assume that it's an IPv6 address.
405 const ipv6Hostname = isIpv6Hostname(hostname);
406 
407 // validate a little.
408 if (!ipv6Hostname) {
409 rest = getHostname(this, rest, hostname);
410 }
411 
412 if (this.hostname.length > hostnameMaxLen) {
413 this.hostname = '';
414 } else {
415 // Hostnames are always lower case.
416 this.hostname = this.hostname.toLowerCase();
417 }
418 
419 if (this.hostname !== '') {
420 if (ipv6Hostname) {
421 if (forbiddenHostCharsIpv6.test(this.hostname)) {
422 throw new ERR_INVALID_URL(url);
423 }
424 } else {
425 // IDNA Support: Returns a punycoded representation of "domain".
426 // It only converts parts of the domain name that
427 // have non-ASCII characters, i.e. it doesn't matter if
428 // you call it with a domain that already is ASCII-only.
429 this.hostname = urlUtil.toASCII(this.hostname);
430 
431 // Prevent two potential routes of hostname spoofing.
432 // 1. If this.hostname is empty, it must have become empty due to toASCII
433 // since we checked this.hostname above.
434 // 2. If any of forbiddenHostChars appears in this.hostname, it must have
435 // also gotten in due to toASCII. This is since getHostname would have
436 // filtered them out otherwise.
437 // Rather than trying to correct this by moving the non-host part into
438 // the pathname as we've done in getHostname, throw an exception to
439 // convey the severity of this issue.
440 if (this.hostname === '' || forbiddenHostChars.test(this.hostname)) {
441 throw new ERR_INVALID_URL(url);
442 }
443 }
444 }
445 
446 const p = this.port ? ':' + this.port : '';
447 const h = this.hostname || '';
448 this.host = h + p;
449 
450 // strip [ and ] from the hostname
451 // the host field still retains them, though
452 if (ipv6Hostname) {
453 this.hostname = this.hostname.slice(1, -1);
454 if (rest[0] !== '/') {
455 rest = '/' + rest;
456 }
457 }
458 }
459 
460 // Now rest is set to the post-host stuff.
461 // Chop off any delim chars.
462 if (!unsafeProtocol.has(lowerProto as string)) {
463 // First, make 100% sure that any "autoEscape" chars get
464 // escaped, even if encodeURIComponent doesn't think they
465 // need to be.
466 rest = autoEscapeStr(rest);
467 }
468 
469 let questionIdx = -1;
470 let hashIdx = -1;
471 for (let i = 0; i < rest.length; ++i) {
472 const code = rest.charCodeAt(i);
473 if (code === CHAR_HASH) {
474 this.hash = rest.slice(i);
475 hashIdx = i;
476 break;
477 } else if (code === CHAR_QUESTION_MARK && questionIdx === -1) {
478 questionIdx = i;
479 }
480 }
481 
482 if (questionIdx !== -1) {
483 if (hashIdx === -1) {
484 this.search = rest.slice(questionIdx);
485 this.query = rest.slice(questionIdx + 1);
486 } else {
487 this.search = rest.slice(questionIdx, hashIdx);
488 this.query = rest.slice(questionIdx + 1, hashIdx);
489 }
490 if (parseQueryString) {
491 this.query = querystringParse(this.query);
492 }
493 } else if (parseQueryString) {
494 // No query string, but parseQueryString still requested
495 this.search = null;
496 this.query = { __proto__: null };
497 }
498 
499 const useQuestionIdx =
500 questionIdx !== -1 && (hashIdx === -1 || questionIdx < hashIdx);
501 const firstIdx = useQuestionIdx ? questionIdx : hashIdx;
502 if (firstIdx === -1) {
503 if (rest.length > 0) this.pathname = rest;
504 } else if (firstIdx > 0) {
505 this.pathname = rest.slice(0, firstIdx);
506 }
507 if (
508 slashedProtocol.has(lowerProto as string) &&
509 this.hostname &&
510 !this.pathname
511 ) {
512 this.pathname = '/';
513 }
514 
515 // To support http.request
516 if (this.pathname || this.search) {
517 const p = this.pathname || '';
518 const s = this.search || '';
519 this.path = p + s;
520 }
521 
522 // Finally, reconstruct the href based on what has been validated.
523 this.href = this.format();
524 return this;
525};
526 
527// These characters do not need escaping:
528// ! - . _ ~
529// ' ( ) * :
530// digits
531// alpha (uppercase)
532// alpha (lowercase)
533// prettier-ignore
534const noEscapeAuth = new Int8Array([
535 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x00 - 0x0F
536 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x10 - 0x1F
537 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, // 0x20 - 0x2F
538 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, // 0x30 - 0x3F
539 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x40 - 0x4F
540 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, // 0x50 - 0x5F
541 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60 - 0x6F
542 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, // 0x70 - 0x7F
543]);
544 
545Url.prototype.format = function format(this: typeof Url): string {
546 let auth = this.auth || '';
547 if (auth) {
548 auth = encodeStr(auth, noEscapeAuth, hexTable);
549 auth += '@';
550 }
551 
552 let protocol = this.protocol || '';
553 let pathname = this.pathname || '';
554 let hash = this.hash || '';
555 let host = '';
556 let query = '';
557 
558 if (this.host) {
559 host = auth + this.host;
560 } else if (this.hostname) {
561 host =
562 auth +
563 (this.hostname.includes(':') && !isIpv6Hostname(this.hostname)
564 ? '[' + this.hostname + ']'
565 : this.hostname);
566 if (this.port) {
567 host += ':' + this.port;
568 }
569 }
570 
571 if (this.query !== null && typeof this.query === 'object') {
572 query = querystringStringify(this.query);
573 }
574 
575 let search = this.search || (query && '?' + query) || '';
576 
577 if (protocol && protocol.charCodeAt(protocol.length - 1) !== 58 /* : */)
578 protocol += ':';
579 
580 let newPathname = '';
581 let lastPos = 0;
582 for (let i = 0; i < pathname.length; ++i) {
583 switch (pathname.charCodeAt(i)) {
584 case CHAR_HASH:
585 if (i - lastPos > 0) newPathname += pathname.slice(lastPos, i);
586 newPathname += '%23';
587 lastPos = i + 1;
588 break;
589 case CHAR_QUESTION_MARK:
590 if (i - lastPos > 0) newPathname += pathname.slice(lastPos, i);
591 newPathname += '%3F';
592 lastPos = i + 1;
593 break;
594 }
595 }
596 if (lastPos > 0) {
597 if (lastPos !== pathname.length)
598 pathname = newPathname + pathname.slice(lastPos);
599 else pathname = newPathname;
600 }
601 
602 // Only the slashedProtocols get the //. Not mailto:, xmpp:, etc.
603 // unless they had them to begin with.
604 if (this.slashes || slashedProtocol.has(protocol)) {
605 if (this.slashes || host) {
606 if (pathname && pathname.charCodeAt(0) !== CHAR_FORWARD_SLASH)
607 pathname = '/' + pathname;
608 host = '//' + host;
609 } else if (
610 protocol.length >= 4 &&
611 protocol.charCodeAt(0) === 102 /* f */ &&
612 protocol.charCodeAt(1) === 105 /* i */ &&
613 protocol.charCodeAt(2) === 108 /* l */ &&
614 protocol.charCodeAt(3) === 101 /* e */
615 ) {
616 host = '//';
617 }
618 }
619 
620 search = search.replace(/#/g, '%23');
621 
622 if (hash && hash.charCodeAt(0) !== CHAR_HASH) hash = '#' + hash;
623 if (search && search.charCodeAt(0) !== CHAR_QUESTION_MARK)
624 search = '?' + search;
625 
626 return protocol + host + pathname + search + hash;
627};
628 
629export function resolve(source: typeof Url | string, relative: string): string {
630 return parse(source, false, true).resolve(relative);
631}
632 
633Url.prototype.resolveObject = function resolveObject(
634 relative: string | typeof Url
635): typeof Url {
636 if (typeof relative === 'string') {
637 const rel = new Url();
638 rel.parse(relative, false, true);
639 relative = rel;
640 }
641 
642 const result = new Url();
643 Object.assign(result, this);
644 
645 // Hash is always overridden, no matter what.
646 // even href="" will remove it.
647 result.hash = relative.hash;
648 
649 // If the relative url is empty, then there's nothing left to do here.
650 if (relative.href === '') {
651 result.href = result.format();
652 return result;
653 }
654 
655 // Hrefs like //foo/bar always cut to the protocol.
656 if (relative.slashes && !relative.protocol) {
657 // Take everything except the protocol from relative
658 const relativeWithoutProtocol = Object.keys(relative).reduce((acc, key) => {
659 if (key !== 'protocol') {
660 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
661 // @ts-expect-error
662 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
663 acc[key] = relative[key];
664 }
665 return acc;
666 }, {});
667 Object.assign(result, relativeWithoutProtocol);
668 
669 // urlParse appends trailing / to urls like http://www.example.com
670 if (
671 slashedProtocol.has(result.protocol as string) &&
672 result.hostname &&
673 !result.pathname
674 ) {
675 result.path = result.pathname = '/';
676 }
677 
678 result.href = result.format();
679 return result;
680 }
681 
682 if (relative.protocol && relative.protocol !== result.protocol) {
683 // If it's a known url protocol, then changing
684 // the protocol does weird things
685 // first, if it's not file:, then we MUST have a host,
686 // and if there was a path
687 // to begin with, then we MUST have a path.
688 // if it is file:, then the host is dropped,
689 // because that's known to be hostless.
690 // anything else is assumed to be absolute.
691 if (!slashedProtocol.has(relative.protocol)) {
692 Object.assign(result, relative);
693 result.href = result.format();
694 return result;
695 }
696 
697 result.protocol = relative.protocol;
698 if (
699 !relative.host &&
700 !/^file:?$/.test(relative.protocol) &&
701 !hostlessProtocol.has(relative.protocol)
702 ) {
703 const relPath = (relative.pathname || '').split('/');
704 while (relPath.length && !(relative.host = relPath.shift())) {
705 // keep this empty.
706 }
707 relative.host ||= '';
708 relative.hostname ||= '';
709 if (relPath[0] !== '') relPath.unshift('');
710 if (relPath.length < 2) relPath.unshift('');
711 result.pathname = relPath.join('/');
712 } else {
713 result.pathname = relative.pathname;
714 }
715 result.search = relative.search;
716 result.query = relative.query;
717 result.host = relative.host || '';
718 result.auth = relative.auth;
719 result.hostname = relative.hostname || relative.host;
720 result.port = relative.port;
721 // To support http.request
722 if (result.pathname || result.search) {
723 const p = result.pathname || '';
724 const s = result.search || '';
725 result.path = p + s;
726 }
727 result.slashes ||= relative.slashes;
728 result.href = result.format();
729 return result;
730 }
731 
732 const isSourceAbs = result.pathname && result.pathname.charAt(0) === '/';
733 const isRelAbs =
734 relative.host || (relative.pathname && relative.pathname.charAt(0) === '/');
735 let mustEndAbs =
736 !!isRelAbs ||
737 !!isSourceAbs ||
738 (!!result.host && !!relative.pathname) ||
739 false;
740 const removeAllDots = mustEndAbs;
741 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
742 let srcPath = (result.pathname && result.pathname.split('/')) || [];
743 
744 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
745 const relPath = (relative.pathname && relative.pathname.split('/')) || [];
746 const noLeadingSlashes =
747 result.protocol && !slashedProtocol.has(result.protocol);
748 
749 // If the url is a non-slashed url, then relative
750 // links like ../.. should be able
751 // to crawl up to the hostname, as well. This is strange.
752 // result.protocol has already been set by now.
753 // Later on, put the first path part into the host field.
754 if (noLeadingSlashes) {
755 result.hostname = '';
756 result.port = null;
757 if (result.host) {
758 if (srcPath[0] === '') srcPath[0] = result.host;
759 else srcPath.unshift(result.host);
760 }
761 result.host = '';
762 if (relative.protocol) {
763 relative.hostname = null;
764 relative.port = null;
765 result.auth = null;
766 if (relative.host) {
767 if (relPath[0] === '') relPath[0] = relative.host;
768 else relPath.unshift(relative.host);
769 }
770 relative.host = null;
771 }
772 mustEndAbs &&= relPath[0] === '' || srcPath[0] === '';
773 }
774 
775 if (isRelAbs) {
776 // it's absolute.
777 if (relative.host || relative.host === '') {
778 if (result.host !== relative.host) result.auth = null;
779 result.host = relative.host;
780 result.port = relative.port;
781 }
782 if (relative.hostname || relative.hostname === '') {
783 if (result.hostname !== relative.hostname) result.auth = null;
784 result.hostname = relative.hostname;
785 }
786 result.search = relative.search;
787 result.query = relative.query;
788 srcPath = relPath;
789 // Fall through to the dot-handling below.
790 } else if (relPath.length) {
791 // it's relative
792 // throw away the existing file, and take the new path instead.
793 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
794 srcPath ||= [];
795 srcPath.pop();
796 srcPath = srcPath.concat(relPath);
797 result.search = relative.search;
798 result.query = relative.query;
799 } else if (relative.search !== null && relative.search !== undefined) {
800 // Just pull out the search.
801 // like href='?foo'.
802 // Put this after the other two cases because it simplifies the booleans
803 if (noLeadingSlashes) {
804 result.hostname = result.host = srcPath.shift();
805 // Occasionally the auth can get stuck only in host.
806 // This especially happens in cases like
807 // url.resolveObject('mailto:local1@domain1', 'local2@domain2')
808 const authInHost =
809 result.host && result.host.indexOf('@') > 0 && result.host.split('@');
810 if (authInHost) {
811 result.auth = authInHost.shift();
812 result.host = result.hostname = authInHost.shift();
813 }
814 }
815 result.search = relative.search;
816 result.query = relative.query;
817 // To support http.request
818 
819 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
820 if (result.pathname !== null || result.search !== null) {
821 result.path =
822 (result.pathname ? result.pathname : '') +
823 (result.search ? result.search : '');
824 }
825 result.href = result.format();
826 return result;
827 }
828 
829 if (!srcPath.length) {
830 // No path at all. All other things were already handled above.
831 result.pathname = null;
832 // To support http.request
833 if (result.search) {
834 result.path = '/' + result.search;
835 } else {
836 result.path = null;
837 }
838 result.href = result.format();
839 return result;
840 }
841 
842 // If a url ENDs in . or .., then it must get a trailing slash.
843 // however, if it ends in anything else non-slashy,
844 // then it must NOT get a trailing slash.
845 let last = srcPath.slice(-1)[0];
846 const hasTrailingSlash =
847 ((result.host || relative.host || srcPath.length > 1) &&
848 (last === '.' || last === '..')) ||
849 last === '';
850 
851 // Strip single dots, resolve double dots to parent dir
852 // if the path tries to go above the root, `up` ends up > 0
853 let up = 0;
854 for (let i = srcPath.length - 1; i >= 0; i--) {
855 last = srcPath[i];
856 if (last === '.') {
857 spliceOne(srcPath, i);
858 } else if (last === '..') {
859 spliceOne(srcPath, i);
860 up++;
861 } else if (up) {
862 spliceOne(srcPath, i);
863 up--;
864 }
865 }
866 
867 // If the path is allowed to go above the root, restore leading ..s
868 if (!mustEndAbs && !removeAllDots) {
869 while (up--) {
870 srcPath.unshift('..');
871 }
872 }
873 
874 if (
875 mustEndAbs &&
876 srcPath[0] !== '' &&
877 (!srcPath[0] || srcPath[0].charAt(0) !== '/')
878 ) {
879 srcPath.unshift('');
880 }
881 
882 if (hasTrailingSlash && srcPath.join('/').slice(-1) !== '/') {
883 srcPath.push('');
884 }
885 
886 const isAbsolute =
887 srcPath[0] === '' || (srcPath[0] && srcPath[0].charAt(0) === '/');
888 
889 // put the host back
890 if (noLeadingSlashes) {
891 result.hostname = result.host = isAbsolute
892 ? ''
893 : srcPath.length
894 ? srcPath.shift()
895 : '';
896 // Occasionally the auth can get stuck only in host.
897 // This especially happens in cases like
898 // url.resolveObject('mailto:local1@domain1', 'local2@domain2')
899 const authInHost =
900 result.host && result.host.indexOf('@') > 0
901 ? result.host.split('@')
902 : false;
903 if (authInHost) {
904 result.auth = authInHost.shift();
905 result.host = result.hostname = authInHost.shift();
906 }
907 }
908 
909 mustEndAbs ||= Boolean(result.host && srcPath.length);
910 
911 if (mustEndAbs && !isAbsolute) {
912 srcPath.unshift('');
913 }
914 
915 if (!srcPath.length) {
916 result.pathname = null;
917 result.path = null;
918 } else {
919 result.pathname = srcPath.join('/');
920 }
921 
922 // To support request.http
923 if (result.pathname !== null || result.search !== null) {
924 result.path =
925 (result.pathname ? result.pathname : '') +
926 (result.search ? result.search : '');
927 }
928 result.auth = relative.auth || result.auth;
929 result.slashes ||= relative.slashes;
930 result.href = result.format();
931 return result;
932};
933 
934Url.prototype.resolve = function resolve(
935 this: typeof Url,
936 relative: string
937): string {
938 return this.resolveObject(parse(relative, false, true)).format();
939};
940 
941Url.prototype.parseHost = function parseHost(this: typeof Url): void {
942 let host = this.host as string;
943 let port: RegExpExecArray | string | null = portPattern.exec(host);
944 if (port) {
945 port = port[0];
946 if (port !== ':') {
947 this.port = port.slice(1);
948 }
949 host = host.slice(0, host.length - port.length);
950 }
951 if (host) this.hostname = host;
952};
953 
954export function parse(
955 url: typeof Url | string,
956 parseQueryString?: boolean,
957 slashesDenoteHost?: boolean
958): typeof Url {
959 if (url instanceof Url) return url;
960 
961 const urlObject = new Url();
962 urlObject.parse(url, parseQueryString, slashesDenoteHost);
963 return urlObject;
964}
965 
966// Format a parsed object into a url string
967export function format(
968 urlObject: typeof Url | URL | string | null,
969 options?: URLFormatOptions
970): string {
971 // Ensure it's an object, and not a string url.
972 // If it's an object, this is a no-op.
973 // this way, you can call urlParse() on strings
974 // to clean up potentially wonky urls.
975 if (typeof urlObject === 'string') {
976 urlObject = parse(urlObject);
977 } else if (typeof urlObject !== 'object' || urlObject === null) {
978 throw new ERR_INVALID_ARG_TYPE(
979 'urlObject',
980 ['Object', 'string'],
981 urlObject
982 );
983 } else if (urlObject instanceof URL) {
984 let fragment = true;
985 let unicode = false;
986 let search = true;
987 let auth = true;
988 
989 if (options) {
990 validateObject(options, 'options');
991 
992 if (options.fragment != null) {
993 fragment = Boolean(options.fragment); // eslint-disable-line @typescript-eslint/no-unnecessary-type-conversion
994 }
995 
996 if (options.unicode != null) {
997 unicode = Boolean(options.unicode); // eslint-disable-line @typescript-eslint/no-unnecessary-type-conversion
998 }
999 
1000 if (options.search != null) {
1001 search = Boolean(options.search); // eslint-disable-line @typescript-eslint/no-unnecessary-type-conversion
1002 }
1003 
1004 if (options.auth != null) {
1005 auth = Boolean(options.auth); // eslint-disable-line @typescript-eslint/no-unnecessary-type-conversion
1006 }
1007 }
1008 
1009 return urlUtil.format(urlObject.href, fragment, unicode, search, auth);
1010 }
1011 
1012 return Url.prototype.format.call(urlObject);
1013}
1014 
1015export function resolveObject(
1016 source: string | typeof Url,
1017 relative: string | typeof Url
1018): typeof Url | string {
1019 if (!source) {
1020 return relative;
1021 }
1022 return parse(source, false, true).resolveObject(relative);
1023}