current commit
261105.1 · url: use indexed appends in search params getAll
'use strict';

const {
  Array,
  ArrayIsArray,
  ArrayPrototypeJoin,
  ArrayPrototypeMap,
  ArrayPrototypePush,
  Boolean,
  Int8Array,
  IteratorPrototype,
  Number,
  ObjectDefineProperties,
  ObjectSetPrototypeOf,
  ReflectGetOwnPropertyDescriptor,
  ReflectOwnKeys,
  SafeMap,
  SafeSet,
  StringPrototypeCharAt,
  StringPrototypeCharCodeAt,
  StringPrototypeCodePointAt,
  StringPrototypeIncludes,
  StringPrototypeIndexOf,
  StringPrototypeSlice,
  StringPrototypeStartsWith,
  StringPrototypeToWellFormed,
  Symbol,
  SymbolIterator,
  SymbolToStringTag,
  TypedArrayPrototypeGetBuffer,
  TypedArrayPrototypeGetByteLength,
  TypedArrayPrototypeGetByteOffset,
  decodeURIComponent,
} = primordials;

const { URLPattern } = internalBinding('url_pattern');
const { inspect } = require('internal/util/inspect');
const {
  encodeStr,
  hexTable,
  isHexTable,
} = require('internal/querystring');

const {
  getConstructorOf,
  removeColors,
  kEnumerableProperty,
  kEmptyObject,
  SideEffectFreeRegExpPrototypeSymbolReplace,
  isWindows,
} = require('internal/util');

const {
  platform,
} = require('internal/process/per_thread');

const {
  markTransferMode,
} = require('internal/worker/js_transferable');

const {
  codes: {
    ERR_ARG_NOT_ITERABLE,
    ERR_INVALID_ARG_TYPE,
    ERR_INVALID_ARG_VALUE,
    ERR_INVALID_FILE_URL_HOST,
    ERR_INVALID_FILE_URL_PATH,
    ERR_INVALID_THIS,
    ERR_INVALID_TUPLE,
    ERR_INVALID_URL,
    ERR_INVALID_URL_SCHEME,
    ERR_MISSING_ARGS,
    ERR_NO_CRYPTO,
  },
} = require('internal/errors');
const {
  CHAR_BACKWARD_SLASH,
  CHAR_FORWARD_SLASH,
  CHAR_LOWERCASE_A,
  CHAR_LOWERCASE_Z,
  CHAR_COLON,
} = require('internal/constants');
const path = require('path');
const { Buffer } = require('buffer');

const {
  validateFunction,
  validateObject,
  kValidateObjectAllowObjects,
} = require('internal/validators');

let percentDecode;
function lazyPercentDecode(input) {
  percentDecode ??= require('internal/data_url').percentDecode;
  return percentDecode(input);
}

const querystring = require('querystring');

const bindingUrl = internalBinding('url');

const FORWARD_SLASH = /\//g;

const contextForInspect = Symbol('context');

// `unsafeProtocol`, `hostlessProtocol` and `slashedProtocol` is
// deliberately moved to `internal/url` rather than `url`.
// Workers does not bootstrap URL module. Therefore, `SafeSet`
// is not initialized on bootstrap. This case breaks the
// test-require-delete-array-iterator test.

// 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 updateActions = {
  kProtocol: 0,
  kHost: 1,
  kHostname: 2,
  kPort: 3,
  kUsername: 4,
  kPassword: 5,
  kPathname: 6,
  kSearch: 7,
  kHash: 8,
  kHref: 9,
};
let blob;
let cryptoRandom;

function lazyBlob() {
  blob ??= require('internal/blob');
  return blob;
}

function lazyCryptoRandom() {
  try {
    cryptoRandom ??= require('internal/crypto/random');
  } catch {
    // If Node.js built without crypto support, we'll fall
    // through here and handle it later.
  }
  return cryptoRandom;
}

/**
 * Copy href and the latest `urlComponents` snapshot into a URLContext.
 * Property assignment order matches the historical URLContext fields so
 * `util.inspect(..., { showHidden: true })` stays stable.
 * @param {object} ctx
 * @param {string} href
 */
function setURLContextFromBinding(ctx, href) {
  const c = bindingUrl.urlComponents;
  ctx.href = href;
  ctx.protocol_end = c[0];
  ctx.username_end = c[1];
  ctx.host_start = c[2];
  ctx.host_end = c[3];
  ctx.pathname_start = c[5];
  ctx.search_start = c[6];
  ctx.hash_start = c[7];
  ctx.port = c[4];
  ctx.scheme_type = c[8];
}

// This class provides the internal state of a URL object. An instance of this
// class is stored in every URL object and is accessed internally by setters
// and getters. It roughly corresponds to the concept of a URL record in the
// URL Standard, with a few differences. It is also the object transported to
// the C++ binding.
// Refs: https://url.spec.whatwg.org/#concept-url
//
// scheme_type refers to ada::scheme::type:
//   HTTP = 0, NOT_SPECIAL = 1, HTTPS = 2, WS = 3, FTP = 4, WSS = 5, FILE = 6
class URLContext {
  // This is the maximum value uint32_t can get.
  // Ada uses uint32_t(-1) for declaring omitted values.
  static #omitted = 4294967295;

  /**
   * @param {string} [href] Parsed href. When omitted, create an empty context
   *   (used by `URL.parse` on invalid input). When provided, `bindingUrl.parse`
   *   / `update` has just written `urlComponents`.
   */
  constructor(href) {
    if (href === undefined) {
      this.href = '';
      this.protocol_end = 0;
      this.username_end = 0;
      this.host_start = 0;
      this.host_end = 0;
      this.pathname_start = 0;
      this.search_start = 0;
      this.hash_start = 0;
      this.port = 0;
      this.scheme_type = 1;
      return;
    }
    setURLContextFromBinding(this, href);
  }

  get hasPort() {
    return this.port !== URLContext.#omitted;
  }

  get hasSearch() {
    return this.search_start !== URLContext.#omitted;
  }

  get hasHash() {
    return this.hash_start !== URLContext.#omitted;
  }
}

let setURLSearchParamsModified;
let setURLSearchParamsContext;
let getURLSearchParamsList;
let setURLSearchParams;
/**
 * Brand check for URL instances created by this realm's URL constructor.
 *
 * Unlike `isURL()`, which duck-types so that URL objects coming from other
 * implementations are recognized, this cannot be satisfied by an ordinary
 * object that merely exposes `href` and `protocol`. Use it where treating an
 * attacker-supplied plain object as a URL would be a security concern.
 * @type {(value: unknown) => value is URL}
 */
let isURLInstance;
/**
 * Returns the canonical serialization of a URL from its private state.
 * @type {(value: URL) => string}
 */
let getURLHref;

class URLSearchParamsIterator {
  #target;
  #kind;
  #index;

  // https://heycam.github.io/webidl/#dfn-default-iterator-object
  constructor(target, kind) {
    this.#target = target;
    this.#kind = kind;
    this.#index = 0;
  }

  next() {
    if (typeof this !== 'object' || this === null || !(#target in this))
      throw new ERR_INVALID_THIS('URLSearchParamsIterator');

    const index = this.#index;
    const values = getURLSearchParamsList(this.#target);
    const len = values.length;
    if (index >= len) {
      return {
        value: undefined,
        done: true,
      };
    }

    const name = values[index];
    const value = values[index + 1];
    this.#index = index + 2;

    let result;
    if (this.#kind === 'key') {
      result = name;
    } else if (this.#kind === 'value') {
      result = value;
    } else {
      result = [name, value];
    }

    return {
      value: result,
      done: false,
    };
  }

  [inspect.custom](recurseTimes, ctx) {
    if (!this || typeof this !== 'object' || !(#target in this))
      throw new ERR_INVALID_THIS('URLSearchParamsIterator');

    if (typeof recurseTimes === 'number' && recurseTimes < 0)
      return ctx.stylize('[Object]', 'special');

    const innerOpts = { ...ctx };
    if (recurseTimes !== null) {
      innerOpts.depth = recurseTimes - 1;
    }
    const index = this.#index;
    const values = getURLSearchParamsList(this.#target);
    const output = [];
    for (let i = index; i < values.length; i++) {
      const isKey = ((i - index) % 2) === 0;
      if (this.#kind === 'key') {
        if (isKey) ArrayPrototypePush(output, values[i]);
      } else if (!isKey) {
        ArrayPrototypePush(output, this.#kind === 'value' ? values[i] : [values[i - 1], values[i]]);
      }
    }
    const hasBreak = StringPrototypeIncludes(inspect(output, innerOpts), '\n');
    const outputStrs = ArrayPrototypeMap(output, (p) => inspect(p, innerOpts));
    let outputStr;
    if (hasBreak) {
      outputStr = `\n  ${ArrayPrototypeJoin(outputStrs, ',\n  ')}`;
    } else {
      outputStr = ` ${ArrayPrototypeJoin(outputStrs, ', ')}`;
    }
    return `${this[SymbolToStringTag]} {${outputStr} }`;
  }
}

// https://heycam.github.io/webidl/#dfn-iterator-prototype-object
delete URLSearchParamsIterator.prototype.constructor;
ObjectSetPrototypeOf(URLSearchParamsIterator.prototype, IteratorPrototype);

ObjectDefineProperties(URLSearchParamsIterator.prototype, {
  [SymbolToStringTag]: { __proto__: null, configurable: true, value: 'URLSearchParams Iterator' },
  next: kEnumerableProperty,
});


class URLSearchParams {
  #searchParams = [];

  // "associated url object"
  #context;

  // Cached application/x-www-form-urlencoded serialization. Cleared on
  // mutation so repeated toString()/URL.href reads stay cheap.
  #serialized;

  static {
    setURLSearchParamsContext = (obj, ctx) => {
      obj.#context = ctx;
    };
    getURLSearchParamsList = (obj) => obj.#searchParams;
    setURLSearchParams = (obj, query) => {
      obj.#serialized = undefined;
      if (query === undefined) {
        obj.#searchParams = [];
      } else {
        obj.#searchParams = parseParams(query);
      }
    };
  }

  #markUpdated() {
    this.#serialized = undefined;
    if (this.#context) {
      setURLSearchParamsModified(this.#context);
    }
  }

  // URL Standard says the default value is '', but as undefined and '' have
  // the same result, undefined is used to prevent unnecessary parsing.
  // Default parameter is necessary to keep URLSearchParams.length === 0 in
  // accordance with Web IDL spec.
  constructor(init = undefined) {
    markTransferMode(this, false, false);

    if (init === undefined) {
      // Do nothing
    } else if (init !== null && (typeof init === 'object' || typeof init === 'function')) {
      const method = init[SymbolIterator];
      if (method === this[SymbolIterator] && #searchParams in init) {
        // While the spec does not have this branch, we can use it as a
        // shortcut to avoid having to go through the costly generic iterator.
        const childParams = init.#searchParams;
        this.#searchParams = childParams.slice();
        this.#serialized = init.#serialized;
      } else if (method != null) {
        // Sequence<sequence<USVString>>
        if (typeof method !== 'function') {
          throw new ERR_ARG_NOT_ITERABLE('Query pairs');
        }

        // The following implementation differs from the URL specification:
        // Sequences must first be converted from ECMAScript objects before
        // and operations are done on them, and the operation of converting
        // the sequences would first exhaust the iterators. If the iterator
        // returns something invalid in the middle, whether it would be called
        // after that would be an observable change to the users.
        // Exhausting the iterator and later converting them to USVString comes
        // with a significant cost (~40-80%). In order optimize URLSearchParams
        // creation duration, Node.js merges the iteration and converting
        // iterations into a single iteration.
        for (const pair of init) {
          if (pair == null) {
            throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]');
          } else if (ArrayIsArray(pair)) {
            // If innerSequence's size is not 2, then throw a TypeError.
            if (pair.length !== 2) {
              throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]');
            }
            // Append (innerSequence[0], innerSequence[1]) to query's list.
            ArrayPrototypePush(
              this.#searchParams,
              toUSVString(pair[0]),
              toUSVString(pair[1]),
            );
          } else {
            if (((typeof pair !== 'object' && typeof pair !== 'function') ||
                typeof pair[SymbolIterator] !== 'function')) {
              throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]');
            }

            let length = 0;

            for (const element of pair) {
              length++;
              ArrayPrototypePush(this.#searchParams, toUSVString(element));
            }

            // If innerSequence's size is not 2, then throw a TypeError.
            if (length !== 2) {
              throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]');
            }
          }
        }
      } else {
        // Record<USVString, USVString>
        // Need to use reflection APIs for full spec compliance.
        // Object keys are unique, but USVString normalization may merge
        // distinct keys. Lazily track normalized keys if a key changes.
        let visited;
        const keys = ReflectOwnKeys(init);
        for (let i = 0; i < keys.length; i++) {
          const key = keys[i];
          const desc = ReflectGetOwnPropertyDescriptor(init, key);
          if (desc !== undefined && desc.enumerable) {
            const typedKey = toUSVString(key);
            const typedValue = toUSVString(init[key]);

            if (visited === undefined && typedKey === key) {
              ArrayPrototypePush(this.#searchParams, typedKey, typedValue);
              continue;
            }

            if (visited === undefined) {
              visited = new SafeMap();
              for (let j = 0; j < this.#searchParams.length; j += 2) {
                visited.set(this.#searchParams[j], j + 1);
              }
            }

            const keyIdx = visited.get(typedKey);
            if (keyIdx !== undefined) {
              this.#searchParams[keyIdx] = typedValue;
            } else {
              visited.set(typedKey, ArrayPrototypePush(this.#searchParams,
                                                       typedKey,
                                                       typedValue) - 1);
            }
          }
        }
      }
    } else {
      // https://url.spec.whatwg.org/#dom-urlsearchparams-urlsearchparams
      init = toUSVString(init);
      this.#searchParams = init ? parseParams(init) : [];
    }
  }

  [inspect.custom](recurseTimes, ctx) {
    if (typeof this !== 'object' || this === null || !(#searchParams in this))
      throw new ERR_INVALID_THIS('URLSearchParams');

    if (typeof recurseTimes === 'number' && recurseTimes < 0)
      return ctx.stylize('[Object]', 'special');

    const separator = ', ';
    const innerOpts = { ...ctx };
    if (recurseTimes !== null) {
      innerOpts.depth = recurseTimes - 1;
    }
    const innerInspect = (v) => inspect(v, innerOpts);

    const list = this.#searchParams;
    const output = [];
    for (let i = 0; i < list.length; i += 2)
      ArrayPrototypePush(
        output,
        `${innerInspect(list[i])} => ${innerInspect(list[i + 1])}`);

    let length = -separator.length;
    for (let i = 0; i < output.length; i++) {
      length += removeColors(output[i]).length + separator.length;
    }
    if (length > ctx.breakLength) {
      return `${this.constructor.name} {\n` +
      `  ${ArrayPrototypeJoin(output, ',\n  ')} }`;
    } else if (output.length) {
      return `${this.constructor.name} { ` +
      `${ArrayPrototypeJoin(output, separator)} }`;
    }
    return `${this.constructor.name} {}`;
  }

  get size() {
    if (typeof this !== 'object' || this === null || !(#searchParams in this))
      throw new ERR_INVALID_THIS('URLSearchParams');
    return this.#searchParams.length / 2;
  }

  append(name, value) {
    if (typeof this !== 'object' || this === null || !(#searchParams in this))
      throw new ERR_INVALID_THIS('URLSearchParams');

    if (arguments.length < 2) {
      throw new ERR_MISSING_ARGS('name', 'value');
    }

    name = toUSVString(name);
    value = toUSVString(value);
    ArrayPrototypePush(this.#searchParams, name, value);
    this.#markUpdated();
  }

  delete(name, value = undefined) {
    if (typeof this !== 'object' || this === null || !(#searchParams in this))
      throw new ERR_INVALID_THIS('URLSearchParams');

    if (arguments.length < 1) {
      throw new ERR_MISSING_ARGS('name');
    }

    const list = this.#searchParams;
    name = toUSVString(name);
    const { length } = list;
    let write = 0;

    if (value !== undefined) {
      value = toUSVString(value);
      for (let i = 0; i < length; i += 2) {
        if (list[i] === name && list[i + 1] === value) {
          continue;
        }
        if (write !== i) {
          list[write] = list[i];
          list[write + 1] = list[i + 1];
        }
        write += 2;
      }
    } else {
      for (let i = 0; i < length; i += 2) {
        if (list[i] === name) {
          continue;
        }
        if (write !== i) {
          list[write] = list[i];
          list[write + 1] = list[i + 1];
        }
        write += 2;
      }
    }

    if (write !== length) {
      list.length = write;
      this.#serialized = undefined;
    }

    if (this.#context) {
      setURLSearchParamsModified(this.#context);
    }
  }

  get(name) {
    if (typeof this !== 'object' || this === null || !(#searchParams in this))
      throw new ERR_INVALID_THIS('URLSearchParams');

    if (arguments.length < 1) {
      throw new ERR_MISSING_ARGS('name');
    }

    const list = this.#searchParams;
    name = toUSVString(name);
    const { length } = list;
    for (let i = 0; i < length; i += 2) {
      if (list[i] === name) {
        return list[i + 1];
      }
    }
    return null;
  }

  getAll(name) {
    if (typeof this !== 'object' || this === null || !(#searchParams in this))
      throw new ERR_INVALID_THIS('URLSearchParams');

    if (arguments.length < 1) {
      throw new ERR_MISSING_ARGS('name');
    }

    const list = this.#searchParams;
    const values = [];
    name = toUSVString(name);
    const { length } = list;
    for (let i = 0; i < length; i += 2) {
      if (list[i] === name) {
        values[values.length] = list[i + 1];
      }
    }
    return values;
  }

  has(name, value = undefined) {
    if (typeof this !== 'object' || this === null || !(#searchParams in this))
      throw new ERR_INVALID_THIS('URLSearchParams');

    if (arguments.length < 1) {
      throw new ERR_MISSING_ARGS('name');
    }

    const list = this.#searchParams;
    name = toUSVString(name);

    if (value !== undefined) {
      value = toUSVString(value);
    }

    const { length } = list;
    for (let i = 0; i < length; i += 2) {
      if (list[i] === name) {
        if (value === undefined || list[i + 1] === value) {
          return true;
        }
      }
    }

    return false;
  }

  set(name, value) {
    if (typeof this !== 'object' || this === null || !(#searchParams in this))
      throw new ERR_INVALID_THIS('URLSearchParams');

    if (arguments.length < 2) {
      throw new ERR_MISSING_ARGS('name', 'value');
    }

    const list = this.#searchParams;
    name = toUSVString(name);
    value = toUSVString(value);
    const { length } = list;

    // If there are any name-value pairs whose name is `name`, in `list`, set
    // the value of the first such name-value pair to `value` and remove the
    // others.
    let found = false;
    let write = 0;
    for (let i = 0; i < length; i += 2) {
      const cur = list[i];
      let keep = true;
      if (cur === name) {
        if (!found) {
          list[write] = cur;
          list[write + 1] = value;
          found = true;
        } else {
          keep = false;
        }
      } else if (write !== i) {
        list[write] = cur;
        list[write + 1] = list[i + 1];
      }
      if (keep)
        write += 2;
    }

    if (found && write !== length) {
      list.length = write;
    }

    // Otherwise, append a new name-value pair whose name is `name` and value
    // is `value`, to `list`.
    if (!found) {
      ArrayPrototypePush(list, name, value);
    }

    this.#markUpdated();
  }

  sort() {
    if (typeof this !== 'object' || this === null || !(#searchParams in this))
      throw new ERR_INVALID_THIS('URLSearchParams');

    const a = this.#searchParams;
    const len = a.length;

    if (len <= 2) {
      // Nothing needs to be done.
    } else if (len < 100) {
      // 100 is found through testing.
      // Simple stable in-place insertion sort
      // Derived from v8/src/js/array.js
      for (let i = 2; i < len; i += 2) {
        const curKey = a[i];
        const curVal = a[i + 1];
        let j;
        for (j = i - 2; j >= 0; j -= 2) {
          if (a[j] > curKey) {
            a[j + 2] = a[j];
            a[j + 3] = a[j + 1];
          } else {
            break;
          }
        }
        a[j + 2] = curKey;
        a[j + 3] = curVal;
      }
    } else {
      // Bottom-up iterative stable merge sort
      const lBuffer = new Array(len);
      for (let step = 2; step < len; step *= 2) {
        for (let start = 0; start < len - 2; start += 2 * step) {
          const mid = start + step;
          let end = mid + step;
          end = end < len ? end : len;
          if (mid > end)
            continue;
          merge(a, start, mid, end, lBuffer);
        }
      }
    }

    this.#markUpdated();
  }

  // https://heycam.github.io/webidl/#es-iterators
  // Define entries here rather than [Symbol.iterator] as the function name
  // must be set to `entries`.
  entries() {
    if (typeof this !== 'object' || this === null || !(#searchParams in this))
      throw new ERR_INVALID_THIS('URLSearchParams');

    return new URLSearchParamsIterator(this, 'key+value');
  }

  forEach(callback, thisArg = undefined) {
    if (typeof this !== 'object' || this === null || !(#searchParams in this))
      throw new ERR_INVALID_THIS('URLSearchParams');

    validateFunction(callback, 'callback');

    let list = this.#searchParams;

    let i = 0;
    while (i < list.length) {
      const key = list[i];
      const value = list[i + 1];
      callback.call(thisArg, value, key, this);
      // In case the URL object's `search` is updated
      list = this.#searchParams;
      i += 2;
    }
  }

  // https://heycam.github.io/webidl/#es-iterable
  keys() {
    if (typeof this !== 'object' || this === null || !(#searchParams in this))
      throw new ERR_INVALID_THIS('URLSearchParams');

    return new URLSearchParamsIterator(this, 'key');
  }

  values() {
    if (typeof this !== 'object' || this === null || !(#searchParams in this))
      throw new ERR_INVALID_THIS('URLSearchParams');

    return new URLSearchParamsIterator(this, 'value');
  }

  // https://heycam.github.io/webidl/#es-stringifier
  // https://url.spec.whatwg.org/#urlsearchparams-stringification-behavior
  toString() {
    if (typeof this !== 'object' || this === null || !(#searchParams in this))
      throw new ERR_INVALID_THIS('URLSearchParams');

    if (this.#serialized !== undefined) {
      return this.#serialized;
    }
    const serialized = serializeParams(this.#searchParams);
    this.#serialized = serialized;
    return serialized;
  }
}

ObjectDefineProperties(URLSearchParams.prototype, {
  append: kEnumerableProperty,
  delete: kEnumerableProperty,
  get: kEnumerableProperty,
  getAll: kEnumerableProperty,
  has: kEnumerableProperty,
  set: kEnumerableProperty,
  size: kEnumerableProperty,
  sort: kEnumerableProperty,
  entries: kEnumerableProperty,
  forEach: kEnumerableProperty,
  keys: kEnumerableProperty,
  values: kEnumerableProperty,
  toString: kEnumerableProperty,
  [SymbolToStringTag]: { __proto__: null, configurable: true, value: 'URLSearchParams' },

  // https://heycam.github.io/webidl/#es-iterable-entries
  [SymbolIterator]: {
    __proto__: null,
    configurable: true,
    writable: true,
    value: URLSearchParams.prototype.entries,
  },
});

/**
 * Checks if a value has the shape of a WHATWG URL object.
 *
 * Using a symbol or instanceof would not be able to recognize URL objects
 * coming from other implementations (e.g. in Electron), so instead we are
 * checking some well known properties for a lack of a better test.
 *
 * We use `href` and `protocol` as they are the only properties that are
 * easy to retrieve and calculate due to the lazy nature of the getters.
 *
 * We check for `auth` and `path` attribute to distinguish legacy url instance with
 * WHATWG URL instance.
 * @param {*} self
 * @returns {self is URL}
 */
function isURL(self) {
  return Boolean(self?.href && self.protocol && self.auth === undefined && self.path === undefined);
}

/**
 * A unique symbol used as a private identifier to safely invoke the URL constructor
 * with a special parsing behavior. When passed as the third argument to the URL
 * constructor, it signals that the constructor should not throw an exception
 * for invalid URL inputs.
 */
const kParseURLSymbol = Symbol('kParseURL');
const kCreateURLFromPosixPathSymbol = Symbol('kCreateURLFromPosixPath');
const kCreateURLFromWindowsPathSymbol = Symbol('kCreateURLFromWindowsPath');

class URL {
  #context;
  #searchParams;
  #searchParamsModified;

  static {
    isURLInstance = (value) => typeof value === 'object' && value !== null && #context in value;

    getURLHref = (value) => {
      value.#ensureSearchParamsUpdated();
      return value.#context.href;
    };

    setURLSearchParamsModified = (obj) => {
      // When URLSearchParams changes, we lazily update URL on the next read/write for performance.
      obj.#searchParamsModified = true;

      // If URL has an existing search, remove it without cascading back to URLSearchParams.
      // Do this to avoid any internal confusion about whether URLSearchParams or URL is up-to-date.
      if (obj.#context.hasSearch) {
        obj.#updateContext(bindingUrl.update(obj.#context.href, updateActions.kSearch, ''));
      }
    };
  }

  constructor(input, base = undefined, parseSymbol = undefined) {
    if (arguments.length === 0) {
      throw new ERR_MISSING_ARGS('url');
    }

    // StringPrototypeToWellFormed is not needed.
    if (typeof input !== 'string') {
      input = `${input}`;
    }

    if (base !== undefined && typeof base !== 'string') {
      base = `${base}`;
    }

    let href;
    if (arguments.length < 3) {
      href = bindingUrl.parse(input, base, true);
    } else {
      const raiseException = parseSymbol !== kParseURLSymbol;
      const interpretAsWindowsPath = parseSymbol === kCreateURLFromWindowsPathSymbol;
      const pathToFileURL = interpretAsWindowsPath || (parseSymbol === kCreateURLFromPosixPathSymbol);
      href = pathToFileURL ?
        bindingUrl.pathToFileURL(input, interpretAsWindowsPath, base) :
        bindingUrl.parse(input, base, raiseException);
    }

    // Delay context allocation until parse finishes so invalid URLs that
    // throw do not pay for an unused URLContext. Initialize in one shot
    // from the binding snapshot instead of writing an empty context first.
    this.#context = href ? new URLContext(href) : new URLContext();
    markTransferMode(this, false, false);
  }

  static parse(input, base = undefined) {
    if (arguments.length === 0) {
      throw new ERR_MISSING_ARGS('url');
    }
    const parsedURLObject = new URL(input, base, kParseURLSymbol);
    return parsedURLObject.href ? parsedURLObject : null;
  }

  [inspect.custom](depth, opts) {
    if (typeof depth === 'number' && depth < 0)
      return this;

    const constructor = getConstructorOf(this) || URL;
    const obj = { __proto__: { constructor } };

    obj.href = this.href;
    obj.origin = this.origin;
    obj.protocol = this.protocol;
    obj.username = this.username;
    obj.password = this.password;
    obj.host = this.host;
    obj.hostname = this.hostname;
    obj.port = this.port;
    obj.pathname = this.pathname;
    obj.search = this.search;
    obj.searchParams = this.searchParams;
    obj.hash = this.hash;

    if (opts.showHidden) {
      obj[contextForInspect] = this.#context;
    }

    return `${constructor.name} ${inspect(obj, opts)}`;
  }

  #getSearchFromContext() {
    if (!this.#context.hasSearch) return '';
    let endsAt = this.#context.href.length;
    if (this.#context.hasHash) endsAt = this.#context.hash_start;
    if (endsAt - this.#context.search_start <= 1) return '';
    return StringPrototypeSlice(this.#context.href, this.#context.search_start, endsAt);
  }

  #getSearchFromParams() {
    if (!this.#searchParams?.size) return '';
    return `?${this.#searchParams}`;
  }

  #ensureSearchParamsUpdated() {
    // URL is updated lazily to greatly improve performance when URLSearchParams is updated repeatedly.
    // If URLSearchParams has been modified, reflect that back into URL, without cascading back.
    if (this.#searchParamsModified) {
      this.#searchParamsModified = false;
      this.#updateContext(bindingUrl.update(this.#context.href, updateActions.kSearch, this.#getSearchFromParams()));
    }
  }

  /**
   * Update the internal context state for URL.
   * @param {string} href New href string from `bindingUrl.update`.
   * @param {boolean} [shouldUpdateSearchParams] If the update has potential to update search params (href/search).
   */
  #updateContext(href, shouldUpdateSearchParams = false) {
    const previousSearch = shouldUpdateSearchParams && this.#searchParams &&
      (this.#searchParamsModified ? this.#getSearchFromParams() : this.#getSearchFromContext());

    setURLContextFromBinding(this.#context, href);

    if (this.#searchParams) {
      // If the search string has updated, URL becomes the source of truth, and we update URLSearchParams.
      // Only do this when we're expecting it to have changed, otherwise a change to hash etc.
      // would incorrectly compare the URLSearchParams state to the empty URL search state.
      if (shouldUpdateSearchParams) {
        const currentSearch = this.#getSearchFromContext();
        if (previousSearch !== currentSearch) {
          setURLSearchParams(this.#searchParams, currentSearch);
          this.#searchParamsModified = false;
        }
      }

      // If we have a URLSearchParams, ensure that URL is up-to-date with any modification to it.
      this.#ensureSearchParamsUpdated();
    }
  }

  toString() {
    // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
    this.#ensureSearchParamsUpdated();
    return this.#context.href;
  }

  get href() {
    // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
    this.#ensureSearchParamsUpdated();
    return this.#context.href;
  }

  set href(value) {
    value = `${value}`;
    const href = bindingUrl.update(this.#context.href, updateActions.kHref, value);
    if (!href) { throw new ERR_INVALID_URL(value); }
    this.#updateContext(href, true);
  }

  // readonly
  get origin() {
    const protocol = StringPrototypeSlice(this.#context.href, 0, this.#context.protocol_end);

    // Check if scheme_type is not `NOT_SPECIAL`
    if (this.#context.scheme_type !== 1) {
      // Check if scheme_type is `FILE`
      if (this.#context.scheme_type === 6) {
        return 'null';
      }
      return `${protocol}//${this.host}`;
    }

    if (protocol === 'blob:') {
      const path = this.pathname;
      if (path.length > 0) {
        try {
          const out = new URL(path);
          // Only return origin of scheme is `http` or `https`
          // Otherwise return a new opaque origin (null).
          if (out.#context.scheme_type === 0 || out.#context.scheme_type === 2) {
            return `${out.protocol}//${out.host}`;
          }
        } catch {
          // Do nothing.
        }
      }
    }

    return 'null';
  }

  get protocol() {
    return StringPrototypeSlice(this.#context.href, 0, this.#context.protocol_end);
  }

  set protocol(value) {
    const href = bindingUrl.update(this.#context.href, updateActions.kProtocol, `${value}`);
    if (href) {
      this.#updateContext(href);
    }
  }

  get username() {
    if (this.#context.protocol_end + 2 < this.#context.username_end) {
      return StringPrototypeSlice(this.#context.href, this.#context.protocol_end + 2, this.#context.username_end);
    }
    return '';
  }

  set username(value) {
    const href = bindingUrl.update(this.#context.href, updateActions.kUsername, `${value}`);
    if (href) {
      this.#updateContext(href);
    }
  }

  get password() {
    if (this.#context.host_start - this.#context.username_end > 0) {
      return StringPrototypeSlice(this.#context.href, this.#context.username_end + 1, this.#context.host_start);
    }
    return '';
  }

  set password(value) {
    const href = bindingUrl.update(this.#context.href, updateActions.kPassword, `${value}`);
    if (href) {
      this.#updateContext(href);
    }
  }

  get host() {
    let startsAt = this.#context.host_start;
    if (this.#context.href[startsAt] === '@') {
      startsAt++;
    }
    // If we have an empty host, then the space between components.host_end and
    // components.pathname_start may be occupied by /.
    if (startsAt === this.#context.host_end) {
      return '';
    }
    return StringPrototypeSlice(this.#context.href, startsAt, this.#context.pathname_start);
  }

  set host(value) {
    const href = bindingUrl.update(this.#context.href, updateActions.kHost, `${value}`);
    if (href) {
      this.#updateContext(href);
    }
  }

  get hostname() {
    let startsAt = this.#context.host_start;
    // host_start might be "@" if the URL has credentials
    if (this.#context.href[startsAt] === '@') {
      startsAt++;
    }
    return StringPrototypeSlice(this.#context.href, startsAt, this.#context.host_end);
  }

  set hostname(value) {
    const href = bindingUrl.update(this.#context.href, updateActions.kHostname, `${value}`);
    if (href) {
      this.#updateContext(href);
    }
  }

  get port() {
    if (this.#context.hasPort) {
      return `${this.#context.port}`;
    }
    return '';
  }

  set port(value) {
    const href = bindingUrl.update(this.#context.href, updateActions.kPort, `${value}`);
    if (href) {
      this.#updateContext(href);
    }
  }

  get pathname() {
    let endsAt;
    if (this.#context.hasSearch) {
      endsAt = this.#context.search_start;
    } else if (this.#context.hasHash) {
      endsAt = this.#context.hash_start;
    }
    return StringPrototypeSlice(this.#context.href, this.#context.pathname_start, endsAt);
  }

  set pathname(value) {
    const href = bindingUrl.update(this.#context.href, updateActions.kPathname, `${value}`);
    if (href) {
      this.#updateContext(href);
    }
  }

  get search() {
    // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
    this.#ensureSearchParamsUpdated();
    return this.#getSearchFromContext();
  }

  set search(value) {
    const href = bindingUrl.update(this.#context.href, updateActions.kSearch, StringPrototypeToWellFormed(`${value}`));
    if (href) {
      this.#updateContext(href, true);
    }
  }

  // readonly
  get searchParams() {
    // Create URLSearchParams on demand to greatly improve the URL performance.
    if (this.#searchParams == null) {
      this.#searchParams = new URLSearchParams(this.#getSearchFromContext());
      setURLSearchParamsContext(this.#searchParams, this);
      this.#searchParamsModified = false;
    }
    return this.#searchParams;
  }

  get hash() {
    if (!this.#context.hasHash || (this.#context.href.length - this.#context.hash_start <= 1)) {
      return '';
    }
    return StringPrototypeSlice(this.#context.href, this.#context.hash_start);
  }

  set hash(value) {
    const href = bindingUrl.update(this.#context.href, updateActions.kHash, `${value}`);
    if (href) {
      this.#updateContext(href);
    }
  }

  toJSON() {
    // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
    this.#ensureSearchParamsUpdated();
    return this.#context.href;
  }

  static canParse(url, base = undefined) {
    if (arguments.length === 0) {
      throw new ERR_MISSING_ARGS('url');
    }

    if (typeof url !== 'string') {
      url = `${url}`;
    }

    if (base !== undefined) {
      return bindingUrl.canParse(url, typeof base === 'string' ? base : `${base}`);
    }

    // It is important to differentiate the canParse call statements
    // since they resolve into different v8 fast api overloads.
    return bindingUrl.canParse(url);
  }
}

ObjectDefineProperties(URL.prototype, {
  [SymbolToStringTag]: { __proto__: null, configurable: true, value: 'URL' },
  toString: kEnumerableProperty,
  href: kEnumerableProperty,
  origin: kEnumerableProperty,
  protocol: kEnumerableProperty,
  username: kEnumerableProperty,
  password: kEnumerableProperty,
  host: kEnumerableProperty,
  hostname: kEnumerableProperty,
  port: kEnumerableProperty,
  pathname: kEnumerableProperty,
  search: kEnumerableProperty,
  searchParams: kEnumerableProperty,
  hash: kEnumerableProperty,
  toJSON: kEnumerableProperty,
});

ObjectDefineProperties(URL, {
  canParse: {
    __proto__: null,
    configurable: true,
    writable: true,
    enumerable: true,
  },
  parse: {
    __proto__: null,
    configurable: true,
    writable: true,
    enumerable: true,
  },
});

function installObjectURLMethods() {
  const bindingBlob = internalBinding('blob');

  function createObjectURL(obj) {
    const cryptoRandom = lazyCryptoRandom();
    if (cryptoRandom === undefined)
      throw new ERR_NO_CRYPTO();

    const blob = lazyBlob();
    if (!blob.isBlob(obj))
      throw new ERR_INVALID_ARG_TYPE('obj', 'Blob', obj);

    const id = cryptoRandom.randomUUID();

    bindingBlob.storeDataObject(id, obj[blob.kHandle], obj.size, obj.type);

    return `blob:nodedata:${id}`;
  }

  function revokeObjectURL(url) {
    if (arguments.length === 0) {
      throw new ERR_MISSING_ARGS('url');
    }

    bindingBlob.revokeObjectURL(`${url}`);
  }

  ObjectDefineProperties(URL, {
    createObjectURL: {
      __proto__: null,
      configurable: true,
      writable: true,
      enumerable: true,
      value: createObjectURL,
    },
    revokeObjectURL: {
      __proto__: null,
      configurable: true,
      writable: true,
      enumerable: true,
      value: revokeObjectURL,
    },
  });
}

function toUSVString(value) {
  return typeof value === 'string' ?
    StringPrototypeToWellFormed(value) :
    StringPrototypeToWellFormed(`${value}`);
}

function unescapeFormComponent(s) {
  try {
    return decodeURIComponent(s);
  } catch {
    return querystring.unescapeBuffer(s).toString();
  }
}

function hasPercentHex(s, start) {
  const end = s.length - 2;
  for (let i = start; i !== -1 && i < end;
    i = StringPrototypeIndexOf(s, '%', i + 1)) {
    if (isHexTable[StringPrototypeCharCodeAt(s, i + 1)] === 1 &&
        isHexTable[StringPrototypeCharCodeAt(s, i + 2)] === 1) {
      return true;
    }
  }
  return false;
}

function decodeFormComponent(qs, start, end) {
  if (start >= end) {
    return '';
  }
  const s = qs.slice(start, end);
  const plus = s.indexOf('+');
  const pct = s.indexOf('%');
  if (plus === -1 && pct === -1) {
    return s;
  }
  const replaced = plus === -1 ? s : s.replaceAll('+', ' ');
  // Only percent-decode when a complete %HH sequence exists. A lone '%' or
  // a '%' followed by a non-hex character must be left intact so later
  // serialization can encode the raw bytes.
  if (pct === -1 || !hasPercentHex(replaced, pct)) {
    return replaced;
  }
  return unescapeFormComponent(replaced);
}

// application/x-www-form-urlencoded parser
// Ref: https://url.spec.whatwg.org/#concept-urlencoded-parser
function parseParams(qs) {
  const len = qs.length;
  let i = qs[0] === '?' ? 1 : 0;
  if (i >= len) {
    return [];
  }

  const out = [];
  // Native indexOf/slice/push outperform primordials on this tight loop.
  const encoded = qs.indexOf('+', i) !== -1 || qs.indexOf('%', i) !== -1;
  while (i < len) {
    let amp = qs.indexOf('&', i);
    if (amp === -1) {
      amp = len;
    }
    if (amp !== i) {
      const eq = qs.indexOf('=', i);
      if (eq === -1 || eq > amp) {
        out.push(encoded ? decodeFormComponent(qs, i, amp) : qs.slice(i, amp), '');
      } else {
        out.push(
          encoded ? decodeFormComponent(qs, i, eq) : qs.slice(i, eq),
          encoded ? decodeFormComponent(qs, eq + 1, amp) : qs.slice(eq + 1, amp),
        );
      }
    }
    i = amp + 1;
  }
  return out;
}

// Adapted from querystring's implementation.
// Ref: https://url.spec.whatwg.org/#concept-urlencoded-byte-serializer
const noEscape = new Int8Array([
/*
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F
*/
  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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, // 0x20 - 0x2F
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 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, 0, 0,  // 0x70 - 0x7F
]);

// Special version of hexTable that uses `+` for U+0020 SPACE.
const paramHexTable = hexTable.slice();
paramHexTable[0x20] = '+';

// application/x-www-form-urlencoded serializer
// Ref: https://url.spec.whatwg.org/#concept-urlencoded-serializer
function serializeParams(array) {
  const len = array.length;
  if (len === 0)
    return '';

  if (len === 2) {
    return encodeStr(array[0], noEscape, paramHexTable) + '=' +
           encodeStr(array[1], noEscape, paramHexTable);
  }

  const pairs = new Array(len / 2);
  for (let i = 0, j = 0; i < len; i += 2, ++j) {
    pairs[j] = encodeStr(array[i], noEscape, paramHexTable) + '=' +
               encodeStr(array[i + 1], noEscape, paramHexTable);
  }
  return ArrayPrototypeJoin(pairs, '&');
}

// for merge sort
function merge(out, start, mid, end, lBuffer) {
  const sizeLeft = mid - start;
  for (let index = 0; index < sizeLeft; index++)
    lBuffer[index] = out[start + index];

  let left = 0;
  let right = mid;
  let output = start;
  while (left < sizeLeft && right < end) {
    if (lBuffer[left] <= out[right]) {
      out[output++] = lBuffer[left++];
      out[output++] = lBuffer[left++];
    } else {
      out[output++] = out[right++];
      out[output++] = out[right++];
    }
  }
  while (left < sizeLeft)
    out[output++] = lBuffer[left++];
}

function domainToASCII(domain) {
  if (arguments.length < 1)
    throw new ERR_MISSING_ARGS('domain');

  // StringPrototypeToWellFormed is not needed.
  return bindingUrl.domainToASCII(`${domain}`);
}

function domainToUnicode(domain) {
  if (arguments.length < 1)
    throw new ERR_MISSING_ARGS('domain');

  // StringPrototypeToWellFormed is not needed.
  return bindingUrl.domainToUnicode(`${domain}`);
}

/**
 * Utility function that converts a URL object into an ordinary options object
 * as expected by the `http.request` and `https.request` APIs.
 * @param {URL} url
 * @returns {Record<string, unknown>}
 */
function urlToHttpOptions(url) {
  validateObject(url, 'url', kValidateObjectAllowObjects);
  const { hostname, pathname, port, username, password, search } = url;
  const options = {
    __proto__: null,
    ...url, // In case the url object was extended by the user.
    protocol: url.protocol,
    hostname: hostname && hostname[0] === '[' ?
      StringPrototypeSlice(hostname, 1, -1) :
      hostname,
    hash: url.hash,
    search: search,
    pathname: pathname,
    path: `${pathname || ''}${search || ''}`,
    href: url.href,
  };
  if (port !== '') {
    options.port = Number(port);
  }
  if (username || password) {
    options.auth = `${decodeURIComponent(username)}:${decodeURIComponent(password)}`;
  }
  return options;
}

function getPathFromURLWin32(url) {
  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 = SideEffectFreeRegExpPrototypeSymbolReplace(FORWARD_SLASH, pathname, '\\');
  // Fast-path: if there is no percent-encoding, avoid decodeURIComponent.
  if (StringPrototypeIncludes(pathname, '%')) {
    pathname = decodeURIComponent(pathname);
  }
  if (hostname !== '') {
    // If hostname is set, then we have a UNC path
    // Pass the hostname through domainToUnicode just in case
    // it is an IDN using punycode encoding. We do not need to worry
    // about percent encoding because the URL parser will have
    // already taken care of that for us. Note that this only
    // causes IDNs with an appropriate `xn--` prefix to be decoded.
    return `\\\\${domainToUnicode(hostname)}${pathname}`;
  }
  // Otherwise, it's a local path that requires a drive letter
  const letter = StringPrototypeCodePointAt(pathname, 1) | 0x20;
  const sep = StringPrototypeCharAt(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 getPathBufferFromURLWin32(url) {
  const hostname = url.hostname;
  let pathname = url.pathname;
  // In the getPathFromURLWin32 variant, we scan the input for backslash (\)
  // and forward slash (/) characters, specifically looking for the ASCII/UTF8
  // encoding these and forbidding their use. This is a bit tricky
  // because these may conflict with non-UTF8 encodings. For instance,
  // in shift-jis, %5C identifies the symbol for the Japanese Yen and not the
  // backslash. If we have a url like file:///foo/%5c/bar, then we really have
  // no way of knowing if that %5c is meant to be a backslash \ or a yen sign.
  // Passing in an encoding option does not help since our Buffer encoding only
  // knows about certain specific text encodings and a single file path might
  // actually contain segments that use multiple encodings. It's tricky! So,
  // for this variation where we are producing a buffer, we won't scan for the
  // slashes at all, and instead will decode the bytes literally into the
  // returned Buffer. That said, that can also be tricky because, on windows,
  // the file path separator *is* the ASCII backslash. This is a known issue
  // on windows specific to the Shift-JIS encoding that we're not really going
  // to solve here. Instead, we're going to do the best we can and just
  // interpret the input url as a sequence of bytes.

  // Because we are converting to a Windows file path here, we need to replace
  // the explicit forward slash separators with backslashes. Note that this
  // intentionally disregards any percent-encoded forward slashes in the path.
  pathname = SideEffectFreeRegExpPrototypeSymbolReplace(FORWARD_SLASH, pathname, '\\');

  // Now, let's start to build our Buffer. We will initially start with a
  // Buffer allocated to fit in the entire string. Worst case there are no
  // percent encoded characters and we take the string as is. Any invalid
  // percent encodings, e.g. `%ZZ` are ignored and are passed through
  // literally.
  const decodedu8 = lazyPercentDecode(Buffer.from(pathname, 'utf8'));
  const decodedPathname = Buffer.from(TypedArrayPrototypeGetBuffer(decodedu8),
                                      TypedArrayPrototypeGetByteOffset(decodedu8),
                                      TypedArrayPrototypeGetByteLength(decodedu8));
  if (hostname !== '') {
    // If hostname is set, then we have a UNC path
    // Pass the hostname through domainToUnicode just in case
    // it is an IDN using punycode encoding. We do not need to worry
    // about percent encoding because the URL parser will have
    // already taken care of that for us. Note that this only
    // causes IDNs with an appropriate `xn--` prefix to be decoded.

    // This is a bit tricky because of the need to convert to a Buffer
    // followed by concatenation of the results.
    const prefix = Buffer.from('\\\\', 'ascii');
    const domain = Buffer.from(domainToUnicode(hostname), 'utf8');

    return Buffer.concat([prefix, domain, decodedPathname]);
  }
  // Otherwise, it's a local path that requires a drive letter
  // In this case we're only going to pay attention to the second and
  // third bytes in the decodedPathname. If first byte is either an ASCII
  // uppercase letter between 'A' and 'Z' or lowercase letter between
  // 'a' and 'z', and the second byte must be an ASCII `:` or the
  // operation will fail.

  const letter = decodedPathname[1] | 0x20;
  const sep = decodedPathname[2];

  if (letter < CHAR_LOWERCASE_A || letter > CHAR_LOWERCASE_Z ||   // a..z A..Z
      (sep !== CHAR_COLON)) {
    throw new ERR_INVALID_FILE_URL_PATH('must be absolute', url);
  }

  // Now, we'll just return everything except the first byte of
  // decodedPathname
  return decodedPathname.subarray(1);
}

function getPathFromURLPosix(url) {
  if (url.hostname !== '') {
    throw new ERR_INVALID_FILE_URL_HOST(platform);
  }
  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,
        );
      }
    }
  }
  // Fast-path: if there is no percent-encoding, avoid decodeURIComponent.
  return StringPrototypeIncludes(pathname, '%') ? decodeURIComponent(pathname) : pathname;
}

function getPathBufferFromURLPosix(url) {
  if (url.hostname !== '') {
    throw new ERR_INVALID_FILE_URL_HOST(platform);
  }
  const pathname = url.pathname;

  // In the getPathFromURLPosix variant, we scan the input for forward slash
  // (/) characters, specifically looking for the ASCII/UTF8 and forbidding
  // its use. This is a bit tricky because these may conflict with non-UTF8
  // encodings. Passing in an encoding option does not help since our Buffer
  // encoding only knows about certain specific text encodings and a single
  // file path might actually contain segments that use multiple encodings.
  // It's tricky! So, for this variation where we are producing a buffer, we
  // won't scan for the slashes at all, and instead will decode the bytes
  // literally into the returned Buffer. We're going to do the best we can and
  // just interpret the input url as a sequence of bytes.
  const u8 = lazyPercentDecode(Buffer.from(pathname, 'utf8'));
  return Buffer.from(TypedArrayPrototypeGetBuffer(u8),
                     TypedArrayPrototypeGetByteOffset(u8),
                     TypedArrayPrototypeGetByteLength(u8));
}

function fileURLToPath(path, options = kEmptyObject) {
  const windows = options?.windows;
  if (typeof path === 'string')
    path = new URL(path);
  else if (!isURL(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) ? getPathFromURLWin32(path) : getPathFromURLPosix(path);
}

// An alternative to fileURLToPath that outputs a Buffer
// instead of a string. The other fileURLToPath does not
// handle non-UTF8 encoded percent encodings at all, so
// converting to a Buffer is necessary in cases where the
// to string conversion would fail.
function fileURLToPathBuffer(path, options = kEmptyObject) {
  const windows = options?.windows;
  if (typeof path === 'string') {
    path = new URL(path);
  } else if (!isURL(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) ? getPathBufferFromURLWin32(path) : getPathBufferFromURLPosix(path);
}

function pathToFileURL(filepath, options = kEmptyObject) {
  const windows = options?.windows ?? isWindows;
  const isUNC = windows && StringPrototypeStartsWith(filepath, '\\\\');
  let resolved = isUNC ?
    filepath :
    (windows ? path.win32.resolve(filepath) : path.posix.resolve(filepath));
  if (isUNC || (windows && StringPrototypeStartsWith(resolved, '\\\\'))) {
    // UNC path format: \\server\share\resource
    // Handle extended UNC path and standard UNC path
    // "\\?\UNC\" path prefix should be ignored.
    // Ref: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
    const isExtendedUNC = StringPrototypeStartsWith(resolved, '\\\\?\\UNC\\');
    const prefixLength = isExtendedUNC ? 8 : 2;
    const hostnameEndIndex = StringPrototypeIndexOf(resolved, '\\', prefixLength);
    if (hostnameEndIndex === -1) {
      throw new ERR_INVALID_ARG_VALUE(
        'path',
        resolved,
        'Missing UNC resource path',
      );
    }
    if (hostnameEndIndex === 2) {
      throw new ERR_INVALID_ARG_VALUE(
        'path',
        resolved,
        'Empty UNC servername',
      );
    }
    const hostname = StringPrototypeSlice(resolved, prefixLength, hostnameEndIndex);
    return new URL(StringPrototypeSlice(resolved, hostnameEndIndex), hostname, kCreateURLFromWindowsPathSymbol);
  }
  // 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 += '/';

  return new URL(resolved, undefined, windows ? kCreateURLFromWindowsPathSymbol : kCreateURLFromPosixPathSymbol);
}

function toPathIfFileURL(fileURLOrPath) {
  if (!isURL(fileURLOrPath))
    return fileURLOrPath;
  return fileURLToPath(fileURLOrPath);
}

/**
 * This util takes a string containing a URL and return the URL origin,
 * its meant to avoid calls to `new URL` constructor.
 * @param {string} url
 * @returns {URL['origin']}
 */
function getURLOrigin(url) {
  return bindingUrl.getOrigin(url);
}

module.exports = {
  fileURLToPath,
  fileURLToPathBuffer,
  pathToFileURL,
  toPathIfFileURL,
  installObjectURLMethods,
  URL,
  URLPattern,
  URLSearchParams,
  URLParse: URL.parse,
  domainToASCII,
  domainToUnicode,
  urlToHttpOptions,
  encodeStr,
  isURL,
  isURLInstance,
  getURLHref,

  urlUpdateActions: updateActions,
  getURLOrigin,
  unsafeProtocol,
  hostlessProtocol,
  slashedProtocol,
};