// Copyright 2018-2026 the Deno authors. MIT license.
// 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.

(function () {
const { core, primordials } = __bootstrap;
const {
  ArrayPrototypeConcat,
  ArrayPrototypeJoin,
  ArrayPrototypePop,
  ArrayPrototypePush,
  ArrayPrototypeShift,
  ArrayPrototypeSlice,
  ArrayPrototypeSplice,
  ArrayPrototypeUnshift,
  FunctionPrototypeCall,
  Int8Array,
  ObjectCreate,
  ObjectKeys,
  ObjectPrototypeIsPrototypeOf,
  RegExpPrototypeExec,
  RegExpPrototypeTest,
  SafeRegExp,
  SafeSet,
  SetPrototypeHas,
  StringPrototypeCharAt,
  StringPrototypeCharCodeAt,
  StringPrototypeCodePointAt,
  StringPrototypeIncludes,
  StringPrototypeIndexOf,
  StringPrototypeReplace,
  StringPrototypeSlice,
  StringPrototypeSplit,
  StringPrototypeStartsWith,
  StringPrototypeToLowerCase,
  TypedArrayPrototypeGetByteLength,
  TypedArrayPrototypeGetByteOffset,
  TypedArrayPrototypeGetBuffer,
  TypedArrayPrototypeSubarray,
  Uint8Array,
  decodeURIComponent,
} = primordials;
const {
  ERR_INVALID_ARG_TYPE,
  ERR_INVALID_ARG_VALUE,
  ERR_INVALID_FILE_URL_HOST,
  ERR_INVALID_FILE_URL_PATH,
  ERR_INVALID_URL,
  ERR_INVALID_URL_SCHEME,
} = core.loadExtScript("ext:deno_node/internal/errors.ts");
const { validateString } = core.loadExtScript(
  "ext:deno_node/internal/validators.mjs",
);
const {
  CHAR_0,
  CHAR_9,
  CHAR_AT,
  CHAR_BACKWARD_SLASH,
  CHAR_CARRIAGE_RETURN,
  CHAR_CIRCUMFLEX_ACCENT,
  CHAR_DOT,
  CHAR_DOUBLE_QUOTE,
  CHAR_FORM_FEED,
  CHAR_FORWARD_SLASH,
  CHAR_GRAVE_ACCENT,
  CHAR_HASH,
  CHAR_HYPHEN_MINUS,
  CHAR_LEFT_ANGLE_BRACKET,
  CHAR_LEFT_CURLY_BRACKET,
  CHAR_LEFT_SQUARE_BRACKET,
  CHAR_LINE_FEED,
  CHAR_LOWERCASE_A,
  CHAR_LOWERCASE_Z,
  CHAR_NO_BREAK_SPACE,
  CHAR_PERCENT,
  CHAR_PLUS,
  CHAR_QUESTION_MARK,
  CHAR_RIGHT_ANGLE_BRACKET,
  CHAR_RIGHT_CURLY_BRACKET,
  CHAR_RIGHT_SQUARE_BRACKET,
  CHAR_SEMICOLON,
  CHAR_SINGLE_QUOTE,
  CHAR_SPACE,
  CHAR_TAB,
  CHAR_UNDERSCORE,
  CHAR_UPPERCASE_A,
  CHAR_UPPERCASE_Z,
  CHAR_VERTICAL_LINE,
  CHAR_ZERO_WIDTH_NOBREAK_SPACE,
} = core.loadExtScript("ext:deno_node/path/_constants.ts");
const lazyPath = core.createLazyLoader("node:path");
const {
  domainToASCII: idnaToASCII,
  domainToUnicode: idnaToUnicode,
} = core.loadExtScript("ext:deno_node/internal/idna.ts");
const { isWindows, osType } = core.loadExtScript("ext:deno_node/_util/os.ts");
let _Buffer;
function lazyBuffer() {
  return _Buffer ??=
    core.loadExtScript("ext:deno_node/internal/buffer.mjs").Buffer;
}
const { encodeStr, hexTable } = core.loadExtScript(
  "ext:deno_node/internal/querystring.ts",
);
const querystring = core.loadExtScript("ext:deno_node/querystring.js").default;
const { URL, URLSearchParams } = core.loadExtScript("ext:deno_web/00_url.js");
const { urlToHttpOptions } = core.loadExtScript(
  "ext:deno_node/internal/url.ts",
);

const forwardSlashRegEx = new SafeRegExp(/\//g);
const percentRegEx = new SafeRegExp(/%/g);
const backslashRegEx = new SafeRegExp(/\\/g);
const newlineRegEx = new SafeRegExp(/\n/g);
const carriageReturnRegEx = new SafeRegExp(/\r/g);
const tabRegEx = new SafeRegExp(/\t/g);
const caretRegEx = new SafeRegExp(/\^/g);
const leftBracketRegEx = new SafeRegExp(/\[/g);
const rightBracketRegEx = new SafeRegExp(/]/g);
const pipeRegEx = new SafeRegExp(/\|/g);
const tildeRegEx = new SafeRegExp(/~/g);
const hashRegEx = new SafeRegExp(/#/g);
// Reference: RFC 3986, RFC 1808, RFC 2396

// define these here so at least they only have to be
// compiled once on the first module load.
const protocolPattern = new SafeRegExp(/^[a-z0-9.+-]+:/i);
const fileProtocolPattern = new SafeRegExp(/^file:?$/);
const portPattern = new SafeRegExp(/:[0-9]*$/);
const hostPattern = new SafeRegExp(/^\/\/[^@/]+@[^@/]+/);
// Special case for a simple path URL
const simplePathPattern = new SafeRegExp(/^(\/\/?(?!\/)[^?\s]*)(\?[^\s]*)?$/);
// Protocols that can allow "unsafe" and "unwise" chars.
const unsafeProtocol = new SafeSet(["javascript", "javascript:"]);
// Protocols that never have a hostname.
const hostlessProtocol = new SafeSet(["javascript", "javascript:"]);
// Protocols that always contain a // bit.
const slashedProtocol = new SafeSet([
  "http",
  "http:",
  "https",
  "https:",
  "ftp",
  "ftp:",
  "gopher",
  "gopher:",
  "file",
  "file:",
  "ws",
  "ws:",
  "wss",
  "wss:",
]);

const hostnameMaxLen = 255;

// These characters do not need escaping:
// ! - . _ ~
// ' ( ) * :
// digits
// alpha (uppercase)
// alpha (lowercase)
// deno-fmt-ignore
const noEscapeAuth = new Int8Array([
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x00 - 0x0F
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x10 - 0x1F
  0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, // 0x20 - 0x2F
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, // 0x30 - 0x3F
  0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x40 - 0x4F
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, // 0x50 - 0x5F
  0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60 - 0x6F
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0,  // 0x70 - 0x7F
]);

// This prevents some common spoofing bugs due to our use of IDNA toASCII. For
// compatibility, the set of characters we use here is the *intersection* of
// "forbidden host code point" in the WHATWG URL Standard [1] and the
// characters in the host parsing loop in Url.prototype.parse, with the
// following additions:
//
// - ':' since this could cause a "protocol spoofing" bug
// - '@' since this could cause parts of the hostname to be confused with auth
// - '[' and ']' since this could cause a non-IPv6 hostname to be interpreted
//   as IPv6 by isIpv6Hostname above
//
// [1]: https://url.spec.whatwg.org/#forbidden-host-code-point
const forbiddenHostChars = new SafeRegExp(/[\0\t\n\r #%/:<>?@[\\\]^|]/);
// For IPv6, permit '[', ']', and ':'.
const forbiddenHostCharsIpv6 = new SafeRegExp(/[\0\t\n\r #%/<>?@\\^|]/);

const _url = URL;

// Legacy URL API
class Url {
  public protocol: string | null;
  public slashes: boolean | null;
  public auth: string | null;
  public host: string | null;
  public port: string | null;
  public hostname: string | null;
  public hash: string | null;
  public search: string | null;
  public query: string | ParsedUrlQuery | null;
  public pathname: string | null;
  public path: string | null;
  public href: string | null;
  [key: string]: unknown;

  constructor() {
    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;
  }

  #parseHost() {
    let host = this.host || "";
    let port: RegExpExecArray | null | string = RegExpPrototypeExec(
      portPattern,
      host,
    );
    if (port) {
      port = port[0];
      if (port !== ":") {
        this.port = StringPrototypeSlice(port, 1);
      }
      host = StringPrototypeSlice(host, 0, host.length - port.length);
    }
    if (host) this.hostname = host;
  }

  public resolve(relative: string) {
    return this.resolveObject(parse(relative, false, true)).format();
  }

  public resolveObject(relative: string | Url) {
    if (typeof relative === "string") {
      const rel = new Url();
      rel.urlParse(relative, false, true);
      relative = rel;
    }

    const result = new Url();
    const tkeys = ObjectKeys(this);
    for (let tk = 0; tk < tkeys.length; tk++) {
      const 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
      const rkeys = ObjectKeys(relative);
      for (let rk = 0; rk < rkeys.length; rk++) {
        const rkey = rkeys[rk];
        if (rkey !== "protocol") result[rkey] = relative[rkey];
      }

      // urlParse appends trailing / to urls like http://www.example.com
      if (
        result.protocol &&
        SetPrototypeHas(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 (!SetPrototypeHas(slashedProtocol, relative.protocol)) {
        const keys = ObjectKeys(relative);
        for (let v = 0; v < keys.length; v++) {
          const k = keys[v];
          result[k] = relative[k];
        }
        result.href = result.format();
        return result;
      }

      result.protocol = relative.protocol;
      if (
        !relative.host &&
        !RegExpPrototypeTest(fileProtocolPattern, relative.protocol) &&
        !SetPrototypeHas(hostlessProtocol, relative.protocol)
      ) {
        const relPath = StringPrototypeSplit(relative.pathname || "", "/");
        while (
          relPath.length &&
          !(relative.host = ArrayPrototypeShift(relPath) || null)
        );
        if (!relative.host) relative.host = "";
        if (!relative.hostname) relative.hostname = "";
        if (relPath[0] !== "") ArrayPrototypeUnshift(relPath, "");
        if (relPath.length < 2) ArrayPrototypeUnshift(relPath, "");
        result.pathname = ArrayPrototypeJoin(relPath, "/");
      } 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) {
        const p = result.pathname || "";
        const s = result.search || "";
        result.path = p + s;
      }
      result.slashes = result.slashes || relative.slashes;
      result.href = result.format();
      return result;
    }

    const isSourceAbs = result.pathname &&
      StringPrototypeCharAt(result.pathname, 0) === "/";
    const isRelAbs = relative.host ||
      (relative.pathname &&
        StringPrototypeCharAt(relative.pathname, 0) === "/");
    let mustEndAbs: string | boolean | number | null = isRelAbs ||
      isSourceAbs || (result.host && relative.pathname);
    const removeAllDots = mustEndAbs;
    let srcPath = (result.pathname &&
      StringPrototypeSplit(result.pathname, "/")) || [];
    const relPath = (relative.pathname &&
      StringPrototypeSplit(relative.pathname, "/")) || [];
    const noLeadingSlashes = result.protocol &&
      !SetPrototypeHas(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 (noLeadingSlashes) {
      result.hostname = "";
      result.port = null;
      if (result.host) {
        if (srcPath[0] === "") srcPath[0] = result.host;
        else ArrayPrototypeUnshift(srcPath, 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;
          else ArrayPrototypeUnshift(relPath, relative.host);
        }
        relative.host = null;
      }
      mustEndAbs = mustEndAbs && (relPath[0] === "" || srcPath[0] === "");
    }

    if (isRelAbs) {
      // it's absolute.
      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;
      // 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 = [];
      ArrayPrototypePop(srcPath);
      srcPath = ArrayPrototypeConcat(srcPath, relPath);
      result.search = relative.search;
      result.query = relative.query;
    } 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 (noLeadingSlashes) {
        result.hostname = result.host = ArrayPrototypeShift(srcPath) || null;
        // Occasionally the auth can get stuck only in host.
        // This especially happens in cases like
        // url.resolveObject('mailto:local1@domain1', 'local2@domain2')
        const authInHost = result.host &&
          StringPrototypeIndexOf(result.host, "@") > 0 &&
          StringPrototypeSplit(result.host, "@");
        if (authInHost) {
          result.auth = ArrayPrototypeShift(authInHost) || null;
          result.host = result.hostname = ArrayPrototypeShift(authInHost) ||
            null;
        }
      }
      result.search = relative.search;
      result.query = relative.query;
      // To support http.request
      if (result.pathname !== null || result.search !== null) {
        result.path = (result.pathname ? result.pathname : "") +
          (result.search ? result.search : "");
      }
      result.href = result.format();
      return result;
    }

    if (!srcPath.length) {
      // No path at all. All other things were already handled 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.
    let last = ArrayPrototypeSlice(srcPath, -1)[0];
    const 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
    let up = 0;
    for (let i = srcPath.length - 1; i >= 0; i--) {
      last = srcPath[i];
      if (last === ".") {
        ArrayPrototypeSplice(srcPath, i, 1);
      } else if (last === "..") {
        ArrayPrototypeSplice(srcPath, i, 1);
        up++;
      } else if (up) {
        ArrayPrototypeSplice(srcPath, i, 1);
        up--;
      }
    }

    // If the path is allowed to go above the root, restore leading ..s
    if (!mustEndAbs && !removeAllDots) {
      while (up--) {
        ArrayPrototypeUnshift(srcPath, "..");
      }
    }

    if (
      mustEndAbs &&
      srcPath[0] !== "" &&
      (!srcPath[0] || StringPrototypeCharAt(srcPath[0], 0) !== "/")
    ) {
      ArrayPrototypeUnshift(srcPath, "");
    }

    if (
      hasTrailingSlash &&
      StringPrototypeSlice(ArrayPrototypeJoin(srcPath, "/"), -1) !== "/"
    ) {
      ArrayPrototypePush(srcPath, "");
    }

    const isAbsolute = srcPath[0] === "" ||
      (srcPath[0] && StringPrototypeCharAt(srcPath[0], 0) === "/");

    // put the host back
    if (noLeadingSlashes) {
      result.hostname = result.host = isAbsolute
        ? ""
        : srcPath.length
        ? ArrayPrototypeShift(srcPath) || null
        : "";
      // Occasionally the auth can get stuck only in host.
      // This especially happens in cases like
      // url.resolveObject('mailto:local1@domain1', 'local2@domain2')
      const authInHost =
        result.host && StringPrototypeIndexOf(result.host, "@") > 0
          ? StringPrototypeSplit(result.host, "@")
          : false;
      if (authInHost) {
        result.auth = ArrayPrototypeShift(authInHost) || null;
        result.host = result.hostname = ArrayPrototypeShift(authInHost) ||
          null;
      }
    }

    mustEndAbs = mustEndAbs || (result.host && srcPath.length);

    if (mustEndAbs && !isAbsolute) {
      ArrayPrototypeUnshift(srcPath, "");
    }

    if (!srcPath.length) {
      result.pathname = null;
      result.path = null;
    } else {
      result.pathname = ArrayPrototypeJoin(srcPath, "/");
    }

    // To support request.http
    if (result.pathname !== null || result.search !== null) {
      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;
  }

  format() {
    let auth = this.auth || "";
    if (auth) {
      auth = encodeStr(auth, noEscapeAuth, hexTable);
      auth += "@";
    }

    let protocol = this.protocol || "";
    let pathname = this.pathname || "";
    let hash = this.hash || "";
    let host = "";
    let query = "";

    if (this.host) {
      host = auth + this.host;
    } else if (this.hostname) {
      host = auth +
        (StringPrototypeIncludes(this.hostname, ":") &&
            !isIpv6Hostname(this.hostname)
          ? "[" + this.hostname + "]"
          : this.hostname);
      if (this.port) {
        host += ":" + this.port;
      }
    }

    if (this.query !== null && typeof this.query === "object") {
      query = querystring.stringify(this.query);
    }

    let search = this.search || (query && "?" + query) || "";

    if (
      protocol &&
      StringPrototypeCharCodeAt(protocol, protocol.length - 1) !== 58 /* : */
    ) {
      protocol += ":";
    }

    let newPathname = "";
    let lastPos = 0;
    for (let i = 0; i < pathname.length; ++i) {
      switch (StringPrototypeCharCodeAt(pathname, i)) {
        case CHAR_HASH:
          if (i - lastPos > 0) {
            newPathname += StringPrototypeSlice(pathname, lastPos, i);
          }
          newPathname += "%23";
          lastPos = i + 1;
          break;
        case CHAR_QUESTION_MARK:
          if (i - lastPos > 0) {
            newPathname += StringPrototypeSlice(pathname, lastPos, i);
          }
          newPathname += "%3F";
          lastPos = i + 1;
          break;
      }
    }
    if (lastPos > 0) {
      if (lastPos !== pathname.length) {
        pathname = newPathname + StringPrototypeSlice(pathname, lastPos);
      } else pathname = newPathname;
    }

    // Only the slashedProtocols get the //.  Not mailto:, xmpp:, etc.
    // unless they had them to begin with.
    if (this.slashes || SetPrototypeHas(slashedProtocol, protocol)) {
      if (this.slashes || host) {
        if (
          pathname &&
          StringPrototypeCharCodeAt(pathname, 0) !== CHAR_FORWARD_SLASH
        ) {
          pathname = "/" + pathname;
        }
        host = "//" + host;
      } else if (
        protocol.length >= 4 &&
        StringPrototypeCharCodeAt(protocol, 0) === 102 /* f */ &&
        StringPrototypeCharCodeAt(protocol, 1) === 105 /* i */ &&
        StringPrototypeCharCodeAt(protocol, 2) === 108 /* l */ &&
        StringPrototypeCharCodeAt(protocol, 3) === 101 /* e */
      ) {
        host = "//";
      }
    }

    search = StringPrototypeReplace(search, hashRegEx, "%23");

    if (hash && StringPrototypeCharCodeAt(hash, 0) !== CHAR_HASH) {
      hash = "#" + hash;
    }
    if (
      search && StringPrototypeCharCodeAt(search, 0) !== CHAR_QUESTION_MARK
    ) {
      search = "?" + search;
    }

    return protocol + host + pathname + search + hash;
  }

  public urlParse(
    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
    let hasHash = 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 = StringPrototypeCharCodeAt(url, i);

      // Find first and last non-whitespace characters for trimming
      const isWs = code === CHAR_SPACE ||
        code === CHAR_TAB ||
        code === CHAR_CARRIAGE_RETURN ||
        code === CHAR_LINE_FEED ||
        code === CHAR_FORM_FEED ||
        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;
      }

      // Only convert backslashes while we haven't seen a split character
      if (!split) {
        switch (code) {
          case CHAR_HASH:
            hasHash = true;
          // Fall through
          case CHAR_QUESTION_MARK:
            split = true;
            break;
          case CHAR_BACKWARD_SLASH:
            if (i - lastPos > 0) {
              rest += StringPrototypeSlice(url, lastPos, i);
            }
            rest += "/";
            lastPos = i + 1;
            break;
        }
      } else if (!hasHash && code === CHAR_HASH) {
        hasHash = true;
      }
    }

    // Check if string was non-empty (including strings with only whitespace)
    if (start !== -1) {
      if (lastPos === start) {
        // We didn't convert any backslashes

        if (end === -1) {
          if (start === 0) rest = url;
          else rest = StringPrototypeSlice(url, start);
        } else {
          rest = StringPrototypeSlice(url, start, end);
        }
      } else if (end === -1 && lastPos < url.length) {
        // We converted some backslashes and have only part of the entire string
        rest += StringPrototypeSlice(url, lastPos);
      } else if (end !== -1 && lastPos < end) {
        // We converted some backslashes and have only part of the entire string
        rest += StringPrototypeSlice(url, lastPos, end);
      }
    }

    if (!slashesDenoteHost && !hasHash) {
      // Try fast path regexp
      const simplePath = RegExpPrototypeExec(simplePathPattern, 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(
              StringPrototypeSlice(this.search, 1),
            );
          } else {
            this.query = StringPrototypeSlice(this.search, 1);
          }
        } else if (parseQueryString) {
          this.search = null;
          this.query = ObjectCreate(null);
        }
        return this;
      }
    }

    let proto: RegExpExecArray | null | string = RegExpPrototypeExec(
      protocolPattern,
      rest,
    );
    let lowerProto = "";
    if (proto) {
      proto = proto[0];
      lowerProto = StringPrototypeToLowerCase(proto);
      this.protocol = lowerProto;
      rest = StringPrototypeSlice(rest, 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.
    let slashes;
    if (
      slashesDenoteHost || proto || RegExpPrototypeTest(hostPattern, rest)
    ) {
      slashes = StringPrototypeCharCodeAt(rest, 0) === CHAR_FORWARD_SLASH &&
        StringPrototypeCharCodeAt(rest, 1) === CHAR_FORWARD_SLASH;
      if (
        slashes && !(proto && SetPrototypeHas(hostlessProtocol, lowerProto))
      ) {
        rest = StringPrototypeSlice(rest, 2);
        this.slashes = true;
      }
    }

    if (
      !SetPrototypeHas(hostlessProtocol, lowerProto) &&
      (slashes || (proto && !SetPrototypeHas(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:b path:/?@c

      let hostEnd = -1;
      let atSign = -1;
      let nonHost = -1;
      for (let i = 0; i < rest.length; ++i) {
        switch (StringPrototypeCharCodeAt(rest, i)) {
          case CHAR_TAB:
          case CHAR_LINE_FEED:
          case CHAR_CARRIAGE_RETURN:
          case CHAR_SPACE:
          case CHAR_DOUBLE_QUOTE:
          case CHAR_PERCENT:
          case CHAR_SINGLE_QUOTE:
          case CHAR_SEMICOLON:
          case CHAR_LEFT_ANGLE_BRACKET:
          case CHAR_RIGHT_ANGLE_BRACKET:
          case CHAR_BACKWARD_SLASH:
          case CHAR_CIRCUMFLEX_ACCENT:
          case CHAR_GRAVE_ACCENT:
          case CHAR_LEFT_CURLY_BRACKET:
          case CHAR_VERTICAL_LINE:
          case CHAR_RIGHT_CURLY_BRACKET:
            // Characters that are never ever allowed in a hostname from RFC 2396
            if (nonHost === -1) nonHost = i;
            break;
          case CHAR_HASH:
          case CHAR_FORWARD_SLASH:
          case CHAR_QUESTION_MARK:
            // Find the first instance of any host-ending characters
            if (nonHost === -1) nonHost = i;
            hostEnd = i;
            break;
          case CHAR_AT:
            // At this point, either we have an explicit point where the
            // auth portion cannot go past, or the last @ char is the decider.
            atSign = i;
            nonHost = -1;
            break;
        }
        if (hostEnd !== -1) break;
      }
      start = 0;
      if (atSign !== -1) {
        this.auth = decodeURIComponent(StringPrototypeSlice(rest, 0, atSign));
        start = atSign + 1;
      }
      if (nonHost === -1) {
        this.host = StringPrototypeSlice(rest, start);
        rest = "";
      } else {
        this.host = StringPrototypeSlice(rest, start, nonHost);
        rest = StringPrototypeSlice(rest, nonHost);
      }

      // pull out port.
      this.#parseHost();

      // We've indicated that there is a hostname,
      // so even if it's empty, it has to be present.
      if (typeof this.hostname !== "string") this.hostname = "";

      const hostname = this.hostname;

      // If hostname begins with [ and ends with ]
      // assume that it's an IPv6 address.
      const ipv6Hostname = isIpv6Hostname(hostname);

      // validate a little.
      if (!ipv6Hostname) {
        rest = getHostname(this, rest, hostname);
      }

      if (this.hostname.length > hostnameMaxLen) {
        this.hostname = "";
      } else {
        // Hostnames are always lower case.
        this.hostname = StringPrototypeToLowerCase(this.hostname);
      }

      if (this.hostname !== "") {
        if (ipv6Hostname) {
          if (RegExpPrototypeTest(forbiddenHostCharsIpv6, this.hostname)) {
            throw new ERR_INVALID_URL(url);
          }
        } else {
          // 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.

          // Use lenient mode (`true`) to try to support even non-compliant
          // URLs.
          this.hostname = idnaToASCII(this.hostname);

          // Prevent two potential routes of hostname spoofing.
          // 1. If this.hostname is empty, it must have become empty due to toASCII
          //    since we checked this.hostname above.
          // 2. If any of forbiddenHostChars appears in this.hostname, it must have
          //    also gotten in due to toASCII. This is since getHostname would have
          //    filtered them out otherwise.
          // Rather than trying to correct this by moving the non-host part into
          // the pathname as we've done in getHostname, throw an exception to
          // convey the severity of this issue.
          if (
            this.hostname === "" ||
            RegExpPrototypeTest(forbiddenHostChars, this.hostname)
          ) {
            throw new ERR_INVALID_URL(url);
          }
        }
      }

      const p = this.port ? ":" + this.port : "";
      const h = this.hostname || "";
      this.host = h + p;

      // strip [ and ] from the hostname
      // the host field still retains them, though
      if (ipv6Hostname) {
        this.hostname = StringPrototypeSlice(this.hostname, 1, -1);
        if (rest[0] !== "/") {
          rest = "/" + rest;
        }
      }
    }

    // Now rest is set to the post-host stuff.
    // Chop off any delim chars.
    if (!SetPrototypeHas(unsafeProtocol, lowerProto)) {
      // First, make 100% sure that any "autoEscape" chars get
      // escaped, even if encodeURIComponent doesn't think they
      // need to be.
      rest = autoEscapeStr(rest);
    }

    let questionIdx = -1;
    let hashIdx = -1;
    for (let i = 0; i < rest.length; ++i) {
      const code = StringPrototypeCharCodeAt(rest, i);
      if (code === CHAR_HASH) {
        this.hash = StringPrototypeSlice(rest, i);
        hashIdx = i;
        break;
      } else if (code === CHAR_QUESTION_MARK && questionIdx === -1) {
        questionIdx = i;
      }
    }

    if (questionIdx !== -1) {
      if (hashIdx === -1) {
        this.search = StringPrototypeSlice(rest, questionIdx);
        this.query = StringPrototypeSlice(rest, questionIdx + 1);
      } else {
        this.search = StringPrototypeSlice(rest, questionIdx, hashIdx);
        this.query = StringPrototypeSlice(rest, questionIdx + 1, hashIdx);
      }
      if (parseQueryString) {
        this.query = querystring.parse(this.query);
      }
    } else if (parseQueryString) {
      // No query string, but parseQueryString still requested
      this.search = null;
      this.query = ObjectCreate(null);
    }

    const useQuestionIdx = questionIdx !== -1 &&
      (hashIdx === -1 || questionIdx < hashIdx);
    const firstIdx = useQuestionIdx ? questionIdx : hashIdx;
    if (firstIdx === -1) {
      if (rest.length > 0) this.pathname = rest;
    } else if (firstIdx > 0) {
      this.pathname = StringPrototypeSlice(rest, 0, firstIdx);
    }
    if (
      SetPrototypeHas(slashedProtocol, lowerProto) && this.hostname &&
      !this.pathname
    ) {
      this.pathname = "/";
    }

    // To support http.request
    if (this.pathname || this.search) {
      const p = this.pathname || "";
      const s = this.search || "";
      this.path = p + s;
    }

    // Finally, reconstruct the href based on what has been validated.
    this.href = this.format();
    return this;
  }
}

interface UrlObject {
  auth?: string | null | undefined;
  hash?: string | null | undefined;
  host?: string | null | undefined;
  hostname?: string | null | undefined;
  href?: string | null | undefined;
  pathname?: string | null | undefined;
  protocol?: string | null | undefined;
  search?: string | null | undefined;
  slashes?: boolean | null | undefined;
  port?: string | number | null | undefined;
  query?: string | null | ParsedUrlQueryInput | undefined;
}

function format(
  urlObject: string | URL | Url | UrlObject,
  options?: {
    auth: boolean;
    fragment: boolean;
    search: boolean;
    unicode: boolean;
  },
): string {
  if (typeof urlObject === "string") {
    urlObject = parse(urlObject, true, false);
  } else if (typeof urlObject !== "object" || urlObject === null) {
    throw new ERR_INVALID_ARG_TYPE(
      "urlObject",
      ["Object", "string"],
      urlObject,
    );
  } else if (ObjectPrototypeIsPrototypeOf(URL.prototype, urlObject)) {
    return formatWhatwg(urlObject, options);
  }

  return FunctionPrototypeCall(Url.prototype.format, urlObject);
}

/**
 * The URL object has both a `toString()` method and `href` property that return string serializations of the URL.
 * These are not, however, customizable in any way.
 * This method allows for basic customization of the output.
 * @see Tested in `parallel/test-url-format-whatwg.js`.
 * @param urlObject
 * @param options
 * @param options.auth `true` if the serialized URL string should include the username and password, `false` otherwise. **Default**: `true`.
 * @param options.fragment `true` if the serialized URL string should include the fragment, `false` otherwise. **Default**: `true`.
 * @param options.search `true` if the serialized URL string should include the search query, **Default**: `true`.
 * @param options.unicode `true` if Unicode characters appearing in the host component of the URL string should be encoded directly as opposed to being Punycode encoded. **Default**: `false`.
 * @returns a customizable serialization of a URL `String` representation of a `WHATWG URL` object.
 */
function formatWhatwg(
  urlObject: string | URL,
  options?: {
    auth: boolean;
    fragment: boolean;
    search: boolean;
    unicode: boolean;
  },
): string {
  if (typeof urlObject === "string") {
    urlObject = new URL(urlObject);
  }
  if (options) {
    if (typeof options !== "object") {
      throw new ERR_INVALID_ARG_TYPE("options", "object", options);
    }
  }

  options = {
    auth: true,
    fragment: true,
    search: true,
    unicode: false,
    ...options,
  };

  let ret = urlObject.protocol;
  if (urlObject.host !== null) {
    ret += "//";
    const hasUsername = !!urlObject.username;
    const hasPassword = !!urlObject.password;
    if (options.auth && (hasUsername || hasPassword)) {
      if (hasUsername) {
        ret += urlObject.username;
      }
      if (hasPassword) {
        ret += `:${urlObject.password}`;
      }
      ret += "@";
    }
    ret += options.unicode
      ? domainToUnicode(urlObject.hostname)
      : urlObject.hostname;
    if (urlObject.port) {
      ret += `:${urlObject.port}`;
    }
  }

  ret += urlObject.pathname;

  if (options.search && urlObject.search) {
    ret += urlObject.search;
  }
  if (options.fragment && urlObject.hash) {
    ret += urlObject.hash;
  }

  return ret;
}

function isIpv6Hostname(hostname: string) {
  return (
    StringPrototypeCharCodeAt(hostname, 0) === CHAR_LEFT_SQUARE_BRACKET &&
    StringPrototypeCharCodeAt(hostname, hostname.length - 1) ===
      CHAR_RIGHT_SQUARE_BRACKET
  );
}

function getHostname(self: Url, rest: string, hostname: string) {
  for (let i = 0; i < hostname.length; ++i) {
    const code = StringPrototypeCharCodeAt(hostname, i);
    const isValid = (code >= CHAR_LOWERCASE_A && code <= CHAR_LOWERCASE_Z) ||
      code === CHAR_DOT ||
      (code >= CHAR_UPPERCASE_A && code <= CHAR_UPPERCASE_Z) ||
      (code >= CHAR_0 && code <= CHAR_9) ||
      code === CHAR_HYPHEN_MINUS ||
      code === CHAR_PLUS ||
      code === CHAR_UNDERSCORE ||
      code > 127;

    // Invalid host character
    if (!isValid) {
      self.hostname = StringPrototypeSlice(hostname, 0, i);
      return `/${StringPrototypeSlice(hostname, i)}${rest}`;
    }
  }
  return rest;
}

// Escaped characters. Use empty strings to fill up unused entries.
// Using Array is faster than Object/Map
// deno-fmt-ignore
const escapedCodes = [
  /* 0 - 9 */ "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  "%09",
  /* 10 - 19 */ "%0A",
  "",
  "",
  "%0D",
  "",
  "",
  "",
  "",
  "",
  "",
  /* 20 - 29 */ "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  /* 30 - 39 */ "",
  "",
  "%20",
  "",
  "%22",
  "",
  "",
  "",
  "",
  "%27",
  /* 40 - 49 */ "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  /* 50 - 59 */ "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  /* 60 - 69 */ "%3C",
  "",
  "%3E",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  /* 70 - 79 */ "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  /* 80 - 89 */ "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  /* 90 - 99 */ "",
  "",
  "%5C",
  "",
  "%5E",
  "",
  "%60",
  "",
  "",
  "",
  /* 100 - 109 */ "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  /* 110 - 119 */ "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  /* 120 - 125 */ "",
  "",
  "",
  "%7B",
  "%7C",
  "%7D"
];

// Automatically escape all delimiters and unwise characters from RFC 2396.
// Also escape single quotes in case of an XSS attack.
// Return the escaped string.
function autoEscapeStr(rest: string) {
  let escaped = "";
  let lastEscapedPos = 0;
  for (let i = 0; i < rest.length; ++i) {
    // `escaped` contains substring up to the last escaped character.
    const escapedChar = escapedCodes[StringPrototypeCharCodeAt(rest, i)];
    if (escapedChar) {
      // Concat if there are ordinary characters in the middle.
      if (i > lastEscapedPos) {
        escaped += StringPrototypeSlice(rest, lastEscapedPos, i);
      }
      escaped += escapedChar;
      lastEscapedPos = i + 1;
    }
  }
  if (lastEscapedPos === 0) {
    // Nothing has been escaped.
    return rest;
  }

  // There are ordinary characters at the end.
  if (lastEscapedPos < rest.length) {
    escaped += StringPrototypeSlice(rest, lastEscapedPos);
  }

  return escaped;
}

/**
 * The url.urlParse() method takes a URL string, parses it, and returns a URL object.
 *
 * @see Tested in `parallel/test-url-parse-format.js`.
 * @param url The URL string to parse.
 * @param parseQueryString If `true`, the query property will always be set to an object returned by the querystring module's parse() method. If false,
 * the query property on the returned URL object will be an unparsed, undecoded string. Default: false.
 * @param slashesDenoteHost If `true`, the first token after the literal string // and preceding the next / will be interpreted as the host
 */
function parse(
  url: string | Url,
  parseQueryString: boolean,
  slashesDenoteHost: boolean,
) {
  if (ObjectPrototypeIsPrototypeOf(Url.prototype, url)) return url;

  const urlObject = new Url();
  urlObject.urlParse(url, parseQueryString, slashesDenoteHost);
  return urlObject;
}

/** The url.resolve() method resolves a target URL relative to a base URL in a manner similar to that of a Web browser resolving an anchor tag HREF.
 * @see https://nodejs.org/api/url.html#urlresolvefrom-to
 * @legacy
 */
function resolve(from: string, to: string) {
  return parse(from, false, true).resolve(to);
}

function resolveObject(source: string | Url, relative: string) {
  if (!source) return relative;
  return parse(source, false, true).resolveObject(relative);
}

/**
 * The url.domainToASCII() takes an arbitrary domain and attempts to convert it into an IDN
 *
 * @param domain The domain to convert to an IDN
 * @see https://www.rfc-editor.org/rfc/rfc3490#section-4
 */
function domainToASCII(domain: string) {
  return idnaToASCII(domain);
}

/**
 * The url.domainToUnicode() takes an IDN and attempts to convert it into unicode
 *
 * @param domain The IDN to convert to Unicode
 * @see https://www.rfc-editor.org/rfc/rfc3490#section-4
 */
function domainToUnicode(domain: string) {
  return idnaToUnicode(domain);
}

/**
 * This function ensures the correct decodings of percent-encoded characters as well as ensuring a cross-platform valid absolute path string.
 * @see Tested in `parallel/test-fileurltopath.js`.
 * @param path The file URL string or URL object to convert to a path.
 * @returns The fully-resolved platform-specific Node.js file path.
 */
function fileURLToPath(path: string | URL): string {
  if (typeof path === "string") path = new URL(path);
  else if (!ObjectPrototypeIsPrototypeOf(URL.prototype, path)) {
    throw new ERR_INVALID_ARG_TYPE("path", ["string", "URL"], path);
  }
  if (path.protocol !== "file:") {
    throw new ERR_INVALID_URL_SCHEME("file");
  }
  return isWindows ? getPathFromURLWin(path) : getPathFromURLPosix(path);
}

// https://url.spec.whatwg.org/#percent-decode
function isHexCharByte(byte: number): boolean {
  // 0-9 A-F a-f
  return (byte >= 0x30 && byte <= 0x39) || (byte >= 0x41 && byte <= 0x46) ||
    (byte >= 0x61 && byte <= 0x66);
}

function hexByteToNumber(byte: number): number {
  return (
    // 0-9
    byte >= 0x30 && byte <= 0x39
      ? (byte - 48)
      // Convert to uppercase: ((byte & 0xDF) - 65) + 10
      : ((byte & 0xDF) - 55)
  );
}

// Decodes a percent-encoded byte sequence to raw bytes without interpreting
// the result as UTF-8. https://url.spec.whatwg.org/#percent-decode
function percentDecode(input: Uint8Array): Uint8Array {
  const length = input.length;
  const output = new Uint8Array(length);
  let j = 0;
  for (let i = 0; i < length; ++i) {
    const byte = input[i];
    if (byte !== 0x25) {
      output[j++] = byte;
    } else if (
      byte === 0x25 &&
      !(isHexCharByte(input[i + 1]) && isHexCharByte(input[i + 2]))
    ) {
      output[j++] = 0x25;
    } else {
      output[j++] = (hexByteToNumber(input[i + 1]) << 4) |
        hexByteToNumber(input[i + 2]);
      i += 2;
    }
  }
  return length === j ? output : TypedArrayPrototypeSubarray(output, 0, j);
}

/**
 * Like `fileURLToPath`, but returns the path as a `Buffer` of raw bytes
 * obtained by percent-decoding the URL without interpreting the result as
 * UTF-8. This allows recovering paths that contain non-Unicode byte sequences.
 * @see Tested in `parallel/test-fileurltopathbuffer.js`.
 */
function fileURLToPathBuffer(
  path: string | URL,
  options: { windows?: boolean } = { __proto__: null },
) {
  const windows = options?.windows;
  if (typeof path === "string") path = new URL(path);
  else if (!ObjectPrototypeIsPrototypeOf(URL.prototype, path)) {
    throw new ERR_INVALID_ARG_TYPE("path", ["string", "URL"], path);
  }
  if (path.protocol !== "file:") {
    throw new ERR_INVALID_URL_SCHEME("file");
  }
  return (windows ?? isWindows)
    ? getPathBufferFromURLWin(path)
    : getPathBufferFromURLPosix(path);
}

function getPathBufferFromURLPosix(url: URL) {
  if (url.hostname !== "") {
    throw new ERR_INVALID_FILE_URL_HOST(osType);
  }
  const Buffer = lazyBuffer();
  const u8 = percentDecode(Buffer.from(url.pathname, "utf8"));
  return Buffer.from(
    TypedArrayPrototypeGetBuffer(u8),
    TypedArrayPrototypeGetByteOffset(u8),
    TypedArrayPrototypeGetByteLength(u8),
  );
}

function getPathBufferFromURLWin(url: URL) {
  const Buffer = lazyBuffer();
  const hostname = url.hostname;
  const pathname = StringPrototypeReplace(
    url.pathname,
    forwardSlashRegEx,
    "\\",
  );
  const u8 = percentDecode(Buffer.from(pathname, "utf8"));
  const decodedPathname = Buffer.from(
    TypedArrayPrototypeGetBuffer(u8),
    TypedArrayPrototypeGetByteOffset(u8),
    TypedArrayPrototypeGetByteLength(u8),
  );
  if (hostname !== "") {
    const prefix = Buffer.from("\\\\", "ascii");
    const domain = Buffer.from(domainToUnicode(hostname), "utf8");
    // `concat` is a `node:buffer` static method on `Buffer`
    // deno-lint-ignore deno-internal/prefer-primordials
    return Buffer.concat([prefix, domain, decodedPathname]);
  }
  const letter = decodedPathname[1] | 0x20;
  const sep = decodedPathname[2];
  if (
    letter < CHAR_LOWERCASE_A ||
    letter > CHAR_LOWERCASE_Z || // a..z A..Z
    sep !== 0x3a // :
  ) {
    throw new ERR_INVALID_FILE_URL_PATH("must be absolute", url);
  }
  return TypedArrayPrototypeSubarray(decodedPathname, 1);
}

function getPathFromURLWin(url: URL): string {
  const hostname = url.hostname;
  let pathname = url.pathname;
  for (let n = 0; n < pathname.length; n++) {
    if (pathname[n] === "%") {
      const third = StringPrototypeCodePointAt(pathname, n + 2)! | 0x20;
      if (
        (pathname[n + 1] === "2" && third === 102) || // 2f 2F /
        (pathname[n + 1] === "5" && third === 99) // 5c 5C \
      ) {
        throw new ERR_INVALID_FILE_URL_PATH(
          "must not include encoded \\ or / characters",
          url,
        );
      }
    }
  }

  pathname = StringPrototypeReplace(pathname, forwardSlashRegEx, "\\");
  pathname = decodeURIComponent(pathname);
  if (hostname !== "") {
    // TODO(bartlomieju): add support for punycode encodings
    return `\\\\${hostname}${pathname}`;
  } else {
    // Otherwise, it's a local path that requires a drive letter
    const letter = StringPrototypeCodePointAt(pathname, 1)! | 0x20;
    const sep = pathname[2];
    if (
      letter < CHAR_LOWERCASE_A ||
      letter > CHAR_LOWERCASE_Z || // a..z A..Z
      sep !== ":"
    ) {
      throw new ERR_INVALID_FILE_URL_PATH("must be absolute", url);
    }
    return StringPrototypeSlice(pathname, 1);
  }
}

function getPathFromURLPosix(url: URL): string {
  if (url.hostname !== "") {
    throw new ERR_INVALID_FILE_URL_HOST(osType);
  }
  const pathname = url.pathname;
  for (let n = 0; n < pathname.length; n++) {
    if (pathname[n] === "%") {
      const third = StringPrototypeCodePointAt(pathname, n + 2)! | 0x20;
      if (pathname[n + 1] === "2" && third === 102) {
        throw new ERR_INVALID_FILE_URL_PATH(
          "must not include encoded / characters",
          url,
        );
      }
    }
  }
  return decodeURIComponent(pathname);
}

/**
 *  The following characters are percent-encoded when converting from file path
 *  to URL:
 *  - %: The percent character is the only character not encoded by the
 *       `pathname` setter.
 *  - \: Backslash is encoded on non-windows platforms since it's a valid
 *       character but the `pathname` setters replaces it by a forward slash.
 *  - LF: The newline character is stripped out by the `pathname` setter.
 *        (See whatwg/url#419)
 *  - CR: The carriage return character is also stripped out by the `pathname`
 *        setter.
 *  - TAB: The tab character is also stripped out by the `pathname` setter.
 */
function encodePathChars(
  filepath: string,
  options: { windows?: boolean },
): string {
  const windows = options.windows;
  if (StringPrototypeIncludes(filepath, "%")) {
    filepath = StringPrototypeReplace(filepath, percentRegEx, "%25");
  }
  // In posix, backslash is a valid character in paths:
  if (!(windows ?? isWindows) && StringPrototypeIncludes(filepath, "\\")) {
    filepath = StringPrototypeReplace(filepath, backslashRegEx, "%5C");
  }
  if (StringPrototypeIncludes(filepath, "\n")) {
    filepath = StringPrototypeReplace(filepath, newlineRegEx, "%0A");
  }
  if (StringPrototypeIncludes(filepath, "\r")) {
    filepath = StringPrototypeReplace(filepath, carriageReturnRegEx, "%0D");
  }
  if (StringPrototypeIncludes(filepath, "\t")) {
    filepath = StringPrototypeReplace(filepath, tabRegEx, "%09");
  }
  if (StringPrototypeIncludes(filepath, "^")) {
    filepath = StringPrototypeReplace(filepath, caretRegEx, "%5E");
  }
  if (StringPrototypeIncludes(filepath, "[")) {
    filepath = StringPrototypeReplace(filepath, leftBracketRegEx, "%5B");
  }
  if (StringPrototypeIncludes(filepath, "]")) {
    filepath = StringPrototypeReplace(filepath, rightBracketRegEx, "%5D");
  }
  if (StringPrototypeIncludes(filepath, "|")) {
    filepath = StringPrototypeReplace(filepath, pipeRegEx, "%7C");
  }
  if (StringPrototypeIncludes(filepath, "~")) {
    filepath = StringPrototypeReplace(filepath, tildeRegEx, "%7E");
  }
  return filepath;
}

/**
 * This function ensures that `filepath` is resolved absolutely, and that the URL control characters are correctly encoded when converting into a File URL.
 * @see Tested in `parallel/test-url-pathtofileurl.js`.
 * @param filepath The file path string to convert to a file URL.
 * @param options The options.
 * @returns The file URL object.
 */
function pathToFileURL(
  filepath: string,
  options: { windows?: boolean } = { __proto__: null },
): URL {
  validateString(filepath, "path");
  const windows = options?.windows;
  const outURL = new URL("file://");
  const isWin = windows ?? isWindows;
  // Extended Win32 path format (\\?\C:\...) is treated as a regular drive
  // path after stripping the \\?\ prefix. Extended UNC (\\?\UNC\server\...)
  // still goes through the UNC branch, with the \\?\UNC\ prefix stripped.
  if (
    isWin && StringPrototypeStartsWith(filepath, "\\\\?\\") &&
    !StringPrototypeStartsWith(filepath, "\\\\?\\UNC\\")
  ) {
    const stripped = StringPrototypeSlice(filepath, 4);
    outURL.pathname = encodePathChars(stripped, { windows });
    return outURL;
  }
  if (isWin && StringPrototypeStartsWith(filepath, "\\\\")) {
    // UNC path format: \\server\share\resource
    // Extended UNC path format: \\?\UNC\server\share\resource
    const isExtendedUNC = StringPrototypeStartsWith(filepath, "\\\\?\\UNC\\");
    const prefixLength = isExtendedUNC ? 8 : 2;
    const hostnameEndIndex = StringPrototypeIndexOf(
      filepath,
      "\\",
      prefixLength,
    );
    if (hostnameEndIndex === -1) {
      throw new ERR_INVALID_ARG_VALUE(
        "filepath",
        filepath,
        "Missing UNC resource path",
      );
    }
    if (hostnameEndIndex === prefixLength) {
      throw new ERR_INVALID_ARG_VALUE(
        "filepath",
        filepath,
        "Empty UNC servername",
      );
    }
    const hostname = StringPrototypeSlice(
      filepath,
      prefixLength,
      hostnameEndIndex,
    );
    const rest = StringPrototypeSlice(filepath, hostnameEndIndex + 1);

    outURL.hostname = idnaToASCII(hostname);
    if (outURL.hostname === "" && hostname !== "") {
      throw new ERR_INVALID_URL(filepath);
    }
    outURL.pathname = encodePathChars(
      StringPrototypeReplace(rest, backslashRegEx, "/"),
      {
        windows,
      },
    );
  } else {
    const path = lazyPath();
    let resolved = (windows ?? isWindows)
      ? path.win32.resolve(filepath)
      : path.posix.resolve(filepath);
    // path.resolve strips trailing slashes so we must add them back
    const filePathLast = StringPrototypeCharCodeAt(
      filepath,
      filepath.length - 1,
    );
    if (
      (filePathLast === CHAR_FORWARD_SLASH ||
        ((windows ?? isWindows) && filePathLast === CHAR_BACKWARD_SLASH)) &&
      resolved[resolved.length - 1] !== path.sep
    ) {
      resolved += "/";
    }

    outURL.pathname = encodePathChars(resolved, { windows });
  }
  return outURL;
}

const URLSearchParams_ = URLSearchParams;

return {
  URL: _url,
  URLSearchParams: URLSearchParams_,
  urlToHttpOptions,
  Url,
  parse,
  format,
  resolve,
  resolveObject,
  domainToASCII,
  domainToUnicode,
  fileURLToPath,
  fileURLToPathBuffer,
  pathToFileURL,
};
})();
