• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const {
4  Array,
5  Number,
6  ObjectCreate,
7  ObjectDefineProperties,
8  ObjectDefineProperty,
9  ObjectGetOwnPropertySymbols,
10  ObjectGetPrototypeOf,
11  ObjectKeys,
12  ReflectGetOwnPropertyDescriptor,
13  ReflectOwnKeys,
14  Symbol,
15  SymbolIterator,
16  SymbolToStringTag,
17} = primordials;
18
19const { inspect } = require('internal/util/inspect');
20const {
21  encodeStr,
22  hexTable,
23  isHexTable
24} = require('internal/querystring');
25
26const { getConstructorOf, removeColors } = require('internal/util');
27const {
28  ERR_ARG_NOT_ITERABLE,
29  ERR_INVALID_ARG_TYPE,
30  ERR_INVALID_ARG_VALUE,
31  ERR_INVALID_CALLBACK,
32  ERR_INVALID_FILE_URL_HOST,
33  ERR_INVALID_FILE_URL_PATH,
34  ERR_INVALID_THIS,
35  ERR_INVALID_TUPLE,
36  ERR_INVALID_URL,
37  ERR_INVALID_URL_SCHEME,
38  ERR_MISSING_ARGS
39} = require('internal/errors').codes;
40const {
41  CHAR_AMPERSAND,
42  CHAR_BACKWARD_SLASH,
43  CHAR_EQUAL,
44  CHAR_FORWARD_SLASH,
45  CHAR_LOWERCASE_A,
46  CHAR_LOWERCASE_Z,
47  CHAR_PERCENT,
48  CHAR_PLUS
49} = require('internal/constants');
50const path = require('path');
51
52// Lazy loaded for startup performance.
53let querystring;
54
55const { platform } = process;
56const isWindows = platform === 'win32';
57
58const {
59  domainToASCII: _domainToASCII,
60  domainToUnicode: _domainToUnicode,
61  encodeAuth,
62  toUSVString: _toUSVString,
63  parse,
64  setURLConstructor,
65  URL_FLAGS_CANNOT_BE_BASE,
66  URL_FLAGS_HAS_FRAGMENT,
67  URL_FLAGS_HAS_HOST,
68  URL_FLAGS_HAS_PASSWORD,
69  URL_FLAGS_HAS_PATH,
70  URL_FLAGS_HAS_QUERY,
71  URL_FLAGS_HAS_USERNAME,
72  URL_FLAGS_IS_DEFAULT_SCHEME_PORT,
73  URL_FLAGS_SPECIAL,
74  kFragment,
75  kHost,
76  kHostname,
77  kPathStart,
78  kPort,
79  kQuery,
80  kSchemeStart
81} = internalBinding('url');
82
83const context = Symbol('context');
84const cannotBeBase = Symbol('cannot-be-base');
85const cannotHaveUsernamePasswordPort =
86    Symbol('cannot-have-username-password-port');
87const special = Symbol('special');
88const searchParams = Symbol('query');
89const kFormat = Symbol('format');
90
91// https://tc39.github.io/ecma262/#sec-%iteratorprototype%-object
92const IteratorPrototype = ObjectGetPrototypeOf(
93  ObjectGetPrototypeOf([][SymbolIterator]())
94);
95
96const unpairedSurrogateRe =
97    /(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])/;
98function toUSVString(val) {
99  const str = `${val}`;
100  // As of V8 5.5, `str.search()` (and `unpairedSurrogateRe[@@search]()`) are
101  // slower than `unpairedSurrogateRe.exec()`.
102  const match = unpairedSurrogateRe.exec(str);
103  if (!match)
104    return str;
105  return _toUSVString(str, match.index);
106}
107
108// Refs: https://html.spec.whatwg.org/multipage/browsers.html#concept-origin-opaque
109const kOpaqueOrigin = 'null';
110
111// Refs: https://html.spec.whatwg.org/multipage/browsers.html#ascii-serialisation-of-an-origin
112function serializeTupleOrigin(scheme, host, port) {
113  return `${scheme}//${host}${port === null ? '' : `:${port}`}`;
114}
115
116// This class provides the internal state of a URL object. An instance of this
117// class is stored in every URL object and is accessed internally by setters
118// and getters. It roughly corresponds to the concept of a URL record in the
119// URL Standard, with a few differences. It is also the object transported to
120// the C++ binding.
121// Refs: https://url.spec.whatwg.org/#concept-url
122class URLContext {
123  constructor() {
124    this.flags = 0;
125    this.scheme = ':';
126    this.username = '';
127    this.password = '';
128    this.host = null;
129    this.port = null;
130    this.path = [];
131    this.query = null;
132    this.fragment = null;
133  }
134}
135
136class URLSearchParams {
137  // URL Standard says the default value is '', but as undefined and '' have
138  // the same result, undefined is used to prevent unnecessary parsing.
139  // Default parameter is necessary to keep URLSearchParams.length === 0 in
140  // accordance with Web IDL spec.
141  constructor(init = undefined) {
142    if (init === null || init === undefined) {
143      this[searchParams] = [];
144    } else if (typeof init === 'object' || typeof init === 'function') {
145      const method = init[SymbolIterator];
146      if (method === this[SymbolIterator]) {
147        // While the spec does not have this branch, we can use it as a
148        // shortcut to avoid having to go through the costly generic iterator.
149        const childParams = init[searchParams];
150        this[searchParams] = childParams.slice();
151      } else if (method !== null && method !== undefined) {
152        if (typeof method !== 'function') {
153          throw new ERR_ARG_NOT_ITERABLE('Query pairs');
154        }
155
156        // Sequence<sequence<USVString>>
157        // Note: per spec we have to first exhaust the lists then process them
158        const pairs = [];
159        for (const pair of init) {
160          if ((typeof pair !== 'object' && typeof pair !== 'function') ||
161              pair === null ||
162              typeof pair[SymbolIterator] !== 'function') {
163            throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]');
164          }
165          const convertedPair = [];
166          for (const element of pair)
167            convertedPair.push(toUSVString(element));
168          pairs.push(convertedPair);
169        }
170
171        this[searchParams] = [];
172        for (const pair of pairs) {
173          if (pair.length !== 2) {
174            throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]');
175          }
176          this[searchParams].push(pair[0], pair[1]);
177        }
178      } else {
179        // Record<USVString, USVString>
180        // Need to use reflection APIs for full spec compliance.
181        this[searchParams] = [];
182        const keys = ReflectOwnKeys(init);
183        for (let i = 0; i < keys.length; i++) {
184          const key = keys[i];
185          const desc = ReflectGetOwnPropertyDescriptor(init, key);
186          if (desc !== undefined && desc.enumerable) {
187            const typedKey = toUSVString(key);
188            const typedValue = toUSVString(init[key]);
189            this[searchParams].push(typedKey, typedValue);
190          }
191        }
192      }
193    } else {
194      // USVString
195      init = toUSVString(init);
196      if (init[0] === '?') init = init.slice(1);
197      initSearchParams(this, init);
198    }
199
200    // "associated url object"
201    this[context] = null;
202  }
203
204  [inspect.custom](recurseTimes, ctx) {
205    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
206      throw new ERR_INVALID_THIS('URLSearchParams');
207    }
208
209    if (typeof recurseTimes === 'number' && recurseTimes < 0)
210      return ctx.stylize('[Object]', 'special');
211
212    const separator = ', ';
213    const innerOpts = { ...ctx };
214    if (recurseTimes !== null) {
215      innerOpts.depth = recurseTimes - 1;
216    }
217    const innerInspect = (v) => inspect(v, innerOpts);
218
219    const list = this[searchParams];
220    const output = [];
221    for (let i = 0; i < list.length; i += 2)
222      output.push(`${innerInspect(list[i])} => ${innerInspect(list[i + 1])}`);
223
224    const length = output.reduce(
225      (prev, cur) => prev + removeColors(cur).length + separator.length,
226      -separator.length
227    );
228    if (length > ctx.breakLength) {
229      return `${this.constructor.name} {\n  ${output.join(',\n  ')} }`;
230    } else if (output.length) {
231      return `${this.constructor.name} { ${output.join(separator)} }`;
232    }
233    return `${this.constructor.name} {}`;
234  }
235}
236
237function onParseComplete(flags, protocol, username, password,
238                         host, port, path, query, fragment) {
239  const ctx = this[context];
240  ctx.flags = flags;
241  ctx.scheme = protocol;
242  ctx.username = (flags & URL_FLAGS_HAS_USERNAME) !== 0 ? username : '';
243  ctx.password = (flags & URL_FLAGS_HAS_PASSWORD) !== 0 ? password : '';
244  ctx.port = port;
245  ctx.path = (flags & URL_FLAGS_HAS_PATH) !== 0 ? path : [];
246  ctx.query = query;
247  ctx.fragment = fragment;
248  ctx.host = host;
249  if (!this[searchParams]) { // Invoked from URL constructor
250    this[searchParams] = new URLSearchParams();
251    this[searchParams][context] = this;
252  }
253  initSearchParams(this[searchParams], query);
254}
255
256function onParseError(flags, input) {
257  throw new ERR_INVALID_URL(input);
258}
259
260function onParseProtocolComplete(flags, protocol, username, password,
261                                 host, port, path, query, fragment) {
262  const ctx = this[context];
263  if ((flags & URL_FLAGS_SPECIAL) !== 0) {
264    ctx.flags |= URL_FLAGS_SPECIAL;
265  } else {
266    ctx.flags &= ~URL_FLAGS_SPECIAL;
267  }
268  ctx.scheme = protocol;
269  ctx.port = port;
270}
271
272function onParseHostnameComplete(flags, protocol, username, password,
273                                 host, port, path, query, fragment) {
274  const ctx = this[context];
275  if ((flags & URL_FLAGS_HAS_HOST) !== 0) {
276    ctx.host = host;
277    ctx.flags |= URL_FLAGS_HAS_HOST;
278  } else {
279    ctx.host = null;
280    ctx.flags &= ~URL_FLAGS_HAS_HOST;
281  }
282}
283
284function onParsePortComplete(flags, protocol, username, password,
285                             host, port, path, query, fragment) {
286  this[context].port = port;
287}
288
289function onParseHostComplete(flags, protocol, username, password,
290                             host, port, path, query, fragment) {
291  onParseHostnameComplete.apply(this, arguments);
292  if (port !== null || ((flags & URL_FLAGS_IS_DEFAULT_SCHEME_PORT) !== 0))
293    onParsePortComplete.apply(this, arguments);
294}
295
296function onParsePathComplete(flags, protocol, username, password,
297                             host, port, path, query, fragment) {
298  const ctx = this[context];
299  if ((flags & URL_FLAGS_HAS_PATH) !== 0) {
300    ctx.path = path;
301    ctx.flags |= URL_FLAGS_HAS_PATH;
302  } else {
303    ctx.path = [];
304    ctx.flags &= ~URL_FLAGS_HAS_PATH;
305  }
306
307  // The C++ binding may set host to empty string.
308  if ((flags & URL_FLAGS_HAS_HOST) !== 0) {
309    ctx.host = host;
310    ctx.flags |= URL_FLAGS_HAS_HOST;
311  }
312}
313
314function onParseSearchComplete(flags, protocol, username, password,
315                               host, port, path, query, fragment) {
316  this[context].query = query;
317}
318
319function onParseHashComplete(flags, protocol, username, password,
320                             host, port, path, query, fragment) {
321  this[context].fragment = fragment;
322}
323
324class URL {
325  constructor(input, base) {
326    // toUSVString is not needed.
327    input = `${input}`;
328    let base_context;
329    if (base !== undefined) {
330      base_context = new URL(base)[context];
331    }
332    this[context] = new URLContext();
333    parse(input, -1, base_context, undefined, onParseComplete.bind(this),
334          onParseError);
335  }
336
337  get [special]() {
338    return (this[context].flags & URL_FLAGS_SPECIAL) !== 0;
339  }
340
341  get [cannotBeBase]() {
342    return (this[context].flags & URL_FLAGS_CANNOT_BE_BASE) !== 0;
343  }
344
345  // https://url.spec.whatwg.org/#cannot-have-a-username-password-port
346  get [cannotHaveUsernamePasswordPort]() {
347    const { host, scheme } = this[context];
348    return ((host == null || host === '') ||
349            this[cannotBeBase] ||
350            scheme === 'file:');
351  }
352
353  [inspect.custom](depth, opts) {
354    if (this == null ||
355        ObjectGetPrototypeOf(this[context]) !== URLContext.prototype) {
356      throw new ERR_INVALID_THIS('URL');
357    }
358
359    if (typeof depth === 'number' && depth < 0)
360      return this;
361
362    const ctor = getConstructorOf(this);
363
364    const obj = ObjectCreate({
365      constructor: ctor === null ? URL : ctor
366    });
367
368    obj.href = this.href;
369    obj.origin = this.origin;
370    obj.protocol = this.protocol;
371    obj.username = this.username;
372    obj.password = this.password;
373    obj.host = this.host;
374    obj.hostname = this.hostname;
375    obj.port = this.port;
376    obj.pathname = this.pathname;
377    obj.search = this.search;
378    obj.searchParams = this.searchParams;
379    obj.hash = this.hash;
380
381    if (opts.showHidden) {
382      obj.cannotBeBase = this[cannotBeBase];
383      obj.special = this[special];
384      obj[context] = this[context];
385    }
386
387    return inspect(obj, opts);
388  }
389}
390
391ObjectDefineProperties(URL.prototype, {
392  [kFormat]: {
393    enumerable: false,
394    configurable: false,
395    // eslint-disable-next-line func-name-matching
396    value: function format(options) {
397      if (options && typeof options !== 'object')
398        throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
399      options = {
400        fragment: true,
401        unicode: false,
402        search: true,
403        auth: true,
404        ...options
405      };
406      const ctx = this[context];
407      let ret = ctx.scheme;
408      if (ctx.host !== null) {
409        ret += '//';
410        const has_username = ctx.username !== '';
411        const has_password = ctx.password !== '';
412        if (options.auth && (has_username || has_password)) {
413          if (has_username)
414            ret += ctx.username;
415          if (has_password)
416            ret += `:${ctx.password}`;
417          ret += '@';
418        }
419        ret += options.unicode ?
420          domainToUnicode(this.hostname) : this.hostname;
421        if (ctx.port !== null)
422          ret += `:${ctx.port}`;
423      } else if (ctx.scheme === 'file:') {
424        ret += '//';
425      }
426      if (this.pathname)
427        ret += this.pathname;
428      if (options.search && ctx.query !== null)
429        ret += `?${ctx.query}`;
430      if (options.fragment && ctx.fragment !== null)
431        ret += `#${ctx.fragment}`;
432      return ret;
433    }
434  },
435  [SymbolToStringTag]: {
436    configurable: true,
437    value: 'URL'
438  },
439  toString: {
440    // https://heycam.github.io/webidl/#es-stringifier
441    writable: true,
442    enumerable: true,
443    configurable: true,
444    // eslint-disable-next-line func-name-matching
445    value: function toString() {
446      return this[kFormat]({});
447    }
448  },
449  href: {
450    enumerable: true,
451    configurable: true,
452    get() {
453      return this[kFormat]({});
454    },
455    set(input) {
456      // toUSVString is not needed.
457      input = `${input}`;
458      parse(input, -1, undefined, undefined, onParseComplete.bind(this),
459            onParseError);
460    }
461  },
462  origin: {  // readonly
463    enumerable: true,
464    configurable: true,
465    get() {
466      // Refs: https://url.spec.whatwg.org/#concept-url-origin
467      const ctx = this[context];
468      switch (ctx.scheme) {
469        case 'blob:':
470          if (ctx.path.length > 0) {
471            try {
472              return (new URL(ctx.path[0])).origin;
473            } catch {
474              // Fall through... do nothing
475            }
476          }
477          return kOpaqueOrigin;
478        case 'ftp:':
479        case 'gopher:':
480        case 'http:':
481        case 'https:':
482        case 'ws:':
483        case 'wss:':
484          return serializeTupleOrigin(ctx.scheme, ctx.host, ctx.port);
485      }
486      return kOpaqueOrigin;
487    }
488  },
489  protocol: {
490    enumerable: true,
491    configurable: true,
492    get() {
493      return this[context].scheme;
494    },
495    set(scheme) {
496      // toUSVString is not needed.
497      scheme = `${scheme}`;
498      if (scheme.length === 0)
499        return;
500      const ctx = this[context];
501      if (ctx.scheme === 'file:' &&
502          (ctx.host === '' || ctx.host === null)) {
503        return;
504      }
505      parse(scheme, kSchemeStart, null, ctx,
506            onParseProtocolComplete.bind(this));
507    }
508  },
509  username: {
510    enumerable: true,
511    configurable: true,
512    get() {
513      return this[context].username;
514    },
515    set(username) {
516      // toUSVString is not needed.
517      username = `${username}`;
518      if (this[cannotHaveUsernamePasswordPort])
519        return;
520      const ctx = this[context];
521      if (username === '') {
522        ctx.username = '';
523        ctx.flags &= ~URL_FLAGS_HAS_USERNAME;
524        return;
525      }
526      ctx.username = encodeAuth(username);
527      ctx.flags |= URL_FLAGS_HAS_USERNAME;
528    }
529  },
530  password: {
531    enumerable: true,
532    configurable: true,
533    get() {
534      return this[context].password;
535    },
536    set(password) {
537      // toUSVString is not needed.
538      password = `${password}`;
539      if (this[cannotHaveUsernamePasswordPort])
540        return;
541      const ctx = this[context];
542      if (password === '') {
543        ctx.password = '';
544        ctx.flags &= ~URL_FLAGS_HAS_PASSWORD;
545        return;
546      }
547      ctx.password = encodeAuth(password);
548      ctx.flags |= URL_FLAGS_HAS_PASSWORD;
549    }
550  },
551  host: {
552    enumerable: true,
553    configurable: true,
554    get() {
555      const ctx = this[context];
556      let ret = ctx.host || '';
557      if (ctx.port !== null)
558        ret += `:${ctx.port}`;
559      return ret;
560    },
561    set(host) {
562      const ctx = this[context];
563      // toUSVString is not needed.
564      host = `${host}`;
565      if (this[cannotBeBase]) {
566        // Cannot set the host if cannot-be-base is set
567        return;
568      }
569      parse(host, kHost, null, ctx, onParseHostComplete.bind(this));
570    }
571  },
572  hostname: {
573    enumerable: true,
574    configurable: true,
575    get() {
576      return this[context].host || '';
577    },
578    set(host) {
579      const ctx = this[context];
580      // toUSVString is not needed.
581      host = `${host}`;
582      if (this[cannotBeBase]) {
583        // Cannot set the host if cannot-be-base is set
584        return;
585      }
586      parse(host, kHostname, null, ctx, onParseHostnameComplete.bind(this));
587    }
588  },
589  port: {
590    enumerable: true,
591    configurable: true,
592    get() {
593      const port = this[context].port;
594      return port === null ? '' : String(port);
595    },
596    set(port) {
597      // toUSVString is not needed.
598      port = `${port}`;
599      if (this[cannotHaveUsernamePasswordPort])
600        return;
601      const ctx = this[context];
602      if (port === '') {
603        ctx.port = null;
604        return;
605      }
606      parse(port, kPort, null, ctx, onParsePortComplete.bind(this));
607    }
608  },
609  pathname: {
610    enumerable: true,
611    configurable: true,
612    get() {
613      const ctx = this[context];
614      if (this[cannotBeBase])
615        return ctx.path[0];
616      if (ctx.path.length === 0)
617        return '';
618      return `/${ctx.path.join('/')}`;
619    },
620    set(path) {
621      // toUSVString is not needed.
622      path = `${path}`;
623      if (this[cannotBeBase])
624        return;
625      parse(path, kPathStart, null, this[context],
626            onParsePathComplete.bind(this));
627    }
628  },
629  search: {
630    enumerable: true,
631    configurable: true,
632    get() {
633      const { query } = this[context];
634      if (query === null || query === '')
635        return '';
636      return `?${query}`;
637    },
638    set(search) {
639      const ctx = this[context];
640      search = toUSVString(search);
641      if (search === '') {
642        ctx.query = null;
643        ctx.flags &= ~URL_FLAGS_HAS_QUERY;
644      } else {
645        if (search[0] === '?') search = search.slice(1);
646        ctx.query = '';
647        ctx.flags |= URL_FLAGS_HAS_QUERY;
648        if (search) {
649          parse(search, kQuery, null, ctx, onParseSearchComplete.bind(this));
650        }
651      }
652      initSearchParams(this[searchParams], search);
653    }
654  },
655  searchParams: {  // readonly
656    enumerable: true,
657    configurable: true,
658    get() {
659      return this[searchParams];
660    }
661  },
662  hash: {
663    enumerable: true,
664    configurable: true,
665    get() {
666      const { fragment } = this[context];
667      if (fragment === null || fragment === '')
668        return '';
669      return `#${fragment}`;
670    },
671    set(hash) {
672      const ctx = this[context];
673      // toUSVString is not needed.
674      hash = `${hash}`;
675      if (!hash) {
676        ctx.fragment = null;
677        ctx.flags &= ~URL_FLAGS_HAS_FRAGMENT;
678        return;
679      }
680      if (hash[0] === '#') hash = hash.slice(1);
681      ctx.fragment = '';
682      ctx.flags |= URL_FLAGS_HAS_FRAGMENT;
683      parse(hash, kFragment, null, ctx, onParseHashComplete.bind(this));
684    }
685  },
686  toJSON: {
687    writable: true,
688    enumerable: true,
689    configurable: true,
690    // eslint-disable-next-line func-name-matching
691    value: function toJSON() {
692      return this[kFormat]({});
693    }
694  }
695});
696
697function update(url, params) {
698  if (!url)
699    return;
700
701  const ctx = url[context];
702  const serializedParams = params.toString();
703  if (serializedParams) {
704    ctx.query = serializedParams;
705    ctx.flags |= URL_FLAGS_HAS_QUERY;
706  } else {
707    ctx.query = null;
708    ctx.flags &= ~URL_FLAGS_HAS_QUERY;
709  }
710}
711
712function initSearchParams(url, init) {
713  if (!init) {
714    url[searchParams] = [];
715    return;
716  }
717  url[searchParams] = parseParams(init);
718}
719
720// application/x-www-form-urlencoded parser
721// Ref: https://url.spec.whatwg.org/#concept-urlencoded-parser
722function parseParams(qs) {
723  const out = [];
724  let pairStart = 0;
725  let lastPos = 0;
726  let seenSep = false;
727  let buf = '';
728  let encoded = false;
729  let encodeCheck = 0;
730  let i;
731  for (i = 0; i < qs.length; ++i) {
732    const code = qs.charCodeAt(i);
733
734    // Try matching key/value pair separator
735    if (code === CHAR_AMPERSAND) {
736      if (pairStart === i) {
737        // We saw an empty substring between pair separators
738        lastPos = pairStart = i + 1;
739        continue;
740      }
741
742      if (lastPos < i)
743        buf += qs.slice(lastPos, i);
744      if (encoded)
745        buf = querystring.unescape(buf);
746      out.push(buf);
747
748      // If `buf` is the key, add an empty value.
749      if (!seenSep)
750        out.push('');
751
752      seenSep = false;
753      buf = '';
754      encoded = false;
755      encodeCheck = 0;
756      lastPos = pairStart = i + 1;
757      continue;
758    }
759
760    // Try matching key/value separator (e.g. '=') if we haven't already
761    if (!seenSep && code === CHAR_EQUAL) {
762      // Key/value separator match!
763      if (lastPos < i)
764        buf += qs.slice(lastPos, i);
765      if (encoded)
766        buf = querystring.unescape(buf);
767      out.push(buf);
768
769      seenSep = true;
770      buf = '';
771      encoded = false;
772      encodeCheck = 0;
773      lastPos = i + 1;
774      continue;
775    }
776
777    // Handle + and percent decoding.
778    if (code === CHAR_PLUS) {
779      if (lastPos < i)
780        buf += qs.slice(lastPos, i);
781      buf += ' ';
782      lastPos = i + 1;
783    } else if (!encoded) {
784      // Try to match an (valid) encoded byte (once) to minimize unnecessary
785      // calls to string decoding functions
786      if (code === CHAR_PERCENT) {
787        encodeCheck = 1;
788      } else if (encodeCheck > 0) {
789        if (isHexTable[code] === 1) {
790          if (++encodeCheck === 3) {
791            querystring = require('querystring');
792            encoded = true;
793          }
794        } else {
795          encodeCheck = 0;
796        }
797      }
798    }
799  }
800
801  // Deal with any leftover key or value data
802
803  // There is a trailing &. No more processing is needed.
804  if (pairStart === i)
805    return out;
806
807  if (lastPos < i)
808    buf += qs.slice(lastPos, i);
809  if (encoded)
810    buf = querystring.unescape(buf);
811  out.push(buf);
812
813  // If `buf` is the key, add an empty value.
814  if (!seenSep)
815    out.push('');
816
817  return out;
818}
819
820// Adapted from querystring's implementation.
821// Ref: https://url.spec.whatwg.org/#concept-urlencoded-byte-serializer
822const noEscape = [
823/*
824  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F
825*/
826  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x00 - 0x0F
827  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x10 - 0x1F
828  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, // 0x20 - 0x2F
829  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 0x30 - 0x3F
830  0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x40 - 0x4F
831  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, // 0x50 - 0x5F
832  0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60 - 0x6F
833  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0  // 0x70 - 0x7F
834];
835
836// Special version of hexTable that uses `+` for U+0020 SPACE.
837const paramHexTable = hexTable.slice();
838paramHexTable[0x20] = '+';
839
840// application/x-www-form-urlencoded serializer
841// Ref: https://url.spec.whatwg.org/#concept-urlencoded-serializer
842function serializeParams(array) {
843  const len = array.length;
844  if (len === 0)
845    return '';
846
847  const firstEncodedParam = encodeStr(array[0], noEscape, paramHexTable);
848  const firstEncodedValue = encodeStr(array[1], noEscape, paramHexTable);
849  let output = `${firstEncodedParam}=${firstEncodedValue}`;
850
851  for (let i = 2; i < len; i += 2) {
852    const encodedParam = encodeStr(array[i], noEscape, paramHexTable);
853    const encodedValue = encodeStr(array[i + 1], noEscape, paramHexTable);
854    output += `&${encodedParam}=${encodedValue}`;
855  }
856
857  return output;
858}
859
860// Mainly to mitigate func-name-matching ESLint rule
861function defineIDLClass(proto, classStr, obj) {
862  // https://heycam.github.io/webidl/#dfn-class-string
863  ObjectDefineProperty(proto, SymbolToStringTag, {
864    writable: false,
865    enumerable: false,
866    configurable: true,
867    value: classStr
868  });
869
870  // https://heycam.github.io/webidl/#es-operations
871  for (const key of ObjectKeys(obj)) {
872    ObjectDefineProperty(proto, key, {
873      writable: true,
874      enumerable: true,
875      configurable: true,
876      value: obj[key]
877    });
878  }
879  for (const key of ObjectGetOwnPropertySymbols(obj)) {
880    ObjectDefineProperty(proto, key, {
881      writable: true,
882      enumerable: false,
883      configurable: true,
884      value: obj[key]
885    });
886  }
887}
888
889// for merge sort
890function merge(out, start, mid, end, lBuffer, rBuffer) {
891  const sizeLeft = mid - start;
892  const sizeRight = end - mid;
893  let l, r, o;
894
895  for (l = 0; l < sizeLeft; l++)
896    lBuffer[l] = out[start + l];
897  for (r = 0; r < sizeRight; r++)
898    rBuffer[r] = out[mid + r];
899
900  l = 0;
901  r = 0;
902  o = start;
903  while (l < sizeLeft && r < sizeRight) {
904    if (lBuffer[l] <= rBuffer[r]) {
905      out[o++] = lBuffer[l++];
906      out[o++] = lBuffer[l++];
907    } else {
908      out[o++] = rBuffer[r++];
909      out[o++] = rBuffer[r++];
910    }
911  }
912  while (l < sizeLeft)
913    out[o++] = lBuffer[l++];
914  while (r < sizeRight)
915    out[o++] = rBuffer[r++];
916}
917
918defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', {
919  append(name, value) {
920    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
921      throw new ERR_INVALID_THIS('URLSearchParams');
922    }
923    if (arguments.length < 2) {
924      throw new ERR_MISSING_ARGS('name', 'value');
925    }
926
927    name = toUSVString(name);
928    value = toUSVString(value);
929    this[searchParams].push(name, value);
930    update(this[context], this);
931  },
932
933  delete(name) {
934    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
935      throw new ERR_INVALID_THIS('URLSearchParams');
936    }
937    if (arguments.length < 1) {
938      throw new ERR_MISSING_ARGS('name');
939    }
940
941    const list = this[searchParams];
942    name = toUSVString(name);
943    for (let i = 0; i < list.length;) {
944      const cur = list[i];
945      if (cur === name) {
946        list.splice(i, 2);
947      } else {
948        i += 2;
949      }
950    }
951    update(this[context], this);
952  },
953
954  get(name) {
955    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
956      throw new ERR_INVALID_THIS('URLSearchParams');
957    }
958    if (arguments.length < 1) {
959      throw new ERR_MISSING_ARGS('name');
960    }
961
962    const list = this[searchParams];
963    name = toUSVString(name);
964    for (let i = 0; i < list.length; i += 2) {
965      if (list[i] === name) {
966        return list[i + 1];
967      }
968    }
969    return null;
970  },
971
972  getAll(name) {
973    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
974      throw new ERR_INVALID_THIS('URLSearchParams');
975    }
976    if (arguments.length < 1) {
977      throw new ERR_MISSING_ARGS('name');
978    }
979
980    const list = this[searchParams];
981    const values = [];
982    name = toUSVString(name);
983    for (let i = 0; i < list.length; i += 2) {
984      if (list[i] === name) {
985        values.push(list[i + 1]);
986      }
987    }
988    return values;
989  },
990
991  has(name) {
992    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
993      throw new ERR_INVALID_THIS('URLSearchParams');
994    }
995    if (arguments.length < 1) {
996      throw new ERR_MISSING_ARGS('name');
997    }
998
999    const list = this[searchParams];
1000    name = toUSVString(name);
1001    for (let i = 0; i < list.length; i += 2) {
1002      if (list[i] === name) {
1003        return true;
1004      }
1005    }
1006    return false;
1007  },
1008
1009  set(name, value) {
1010    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
1011      throw new ERR_INVALID_THIS('URLSearchParams');
1012    }
1013    if (arguments.length < 2) {
1014      throw new ERR_MISSING_ARGS('name', 'value');
1015    }
1016
1017    const list = this[searchParams];
1018    name = toUSVString(name);
1019    value = toUSVString(value);
1020
1021    // If there are any name-value pairs whose name is `name`, in `list`, set
1022    // the value of the first such name-value pair to `value` and remove the
1023    // others.
1024    let found = false;
1025    for (let i = 0; i < list.length;) {
1026      const cur = list[i];
1027      if (cur === name) {
1028        if (!found) {
1029          list[i + 1] = value;
1030          found = true;
1031          i += 2;
1032        } else {
1033          list.splice(i, 2);
1034        }
1035      } else {
1036        i += 2;
1037      }
1038    }
1039
1040    // Otherwise, append a new name-value pair whose name is `name` and value
1041    // is `value`, to `list`.
1042    if (!found) {
1043      list.push(name, value);
1044    }
1045
1046    update(this[context], this);
1047  },
1048
1049  sort() {
1050    const a = this[searchParams];
1051    const len = a.length;
1052
1053    if (len <= 2) {
1054      // Nothing needs to be done.
1055    } else if (len < 100) {
1056      // 100 is found through testing.
1057      // Simple stable in-place insertion sort
1058      // Derived from v8/src/js/array.js
1059      for (let i = 2; i < len; i += 2) {
1060        const curKey = a[i];
1061        const curVal = a[i + 1];
1062        let j;
1063        for (j = i - 2; j >= 0; j -= 2) {
1064          if (a[j] > curKey) {
1065            a[j + 2] = a[j];
1066            a[j + 3] = a[j + 1];
1067          } else {
1068            break;
1069          }
1070        }
1071        a[j + 2] = curKey;
1072        a[j + 3] = curVal;
1073      }
1074    } else {
1075      // Bottom-up iterative stable merge sort
1076      const lBuffer = new Array(len);
1077      const rBuffer = new Array(len);
1078      for (let step = 2; step < len; step *= 2) {
1079        for (let start = 0; start < len - 2; start += 2 * step) {
1080          const mid = start + step;
1081          let end = mid + step;
1082          end = end < len ? end : len;
1083          if (mid > end)
1084            continue;
1085          merge(a, start, mid, end, lBuffer, rBuffer);
1086        }
1087      }
1088    }
1089
1090    update(this[context], this);
1091  },
1092
1093  // https://heycam.github.io/webidl/#es-iterators
1094  // Define entries here rather than [Symbol.iterator] as the function name
1095  // must be set to `entries`.
1096  entries() {
1097    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
1098      throw new ERR_INVALID_THIS('URLSearchParams');
1099    }
1100
1101    return createSearchParamsIterator(this, 'key+value');
1102  },
1103
1104  forEach(callback, thisArg = undefined) {
1105    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
1106      throw new ERR_INVALID_THIS('URLSearchParams');
1107    }
1108    if (typeof callback !== 'function') {
1109      throw new ERR_INVALID_CALLBACK(callback);
1110    }
1111
1112    let list = this[searchParams];
1113
1114    let i = 0;
1115    while (i < list.length) {
1116      const key = list[i];
1117      const value = list[i + 1];
1118      callback.call(thisArg, value, key, this);
1119      // In case the URL object's `search` is updated
1120      list = this[searchParams];
1121      i += 2;
1122    }
1123  },
1124
1125  // https://heycam.github.io/webidl/#es-iterable
1126  keys() {
1127    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
1128      throw new ERR_INVALID_THIS('URLSearchParams');
1129    }
1130
1131    return createSearchParamsIterator(this, 'key');
1132  },
1133
1134  values() {
1135    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
1136      throw new ERR_INVALID_THIS('URLSearchParams');
1137    }
1138
1139    return createSearchParamsIterator(this, 'value');
1140  },
1141
1142  // https://heycam.github.io/webidl/#es-stringifier
1143  // https://url.spec.whatwg.org/#urlsearchparams-stringification-behavior
1144  toString() {
1145    if (!this || !this[searchParams] || this[searchParams][searchParams]) {
1146      throw new ERR_INVALID_THIS('URLSearchParams');
1147    }
1148
1149    return serializeParams(this[searchParams]);
1150  }
1151});
1152
1153// https://heycam.github.io/webidl/#es-iterable-entries
1154ObjectDefineProperty(URLSearchParams.prototype, SymbolIterator, {
1155  writable: true,
1156  configurable: true,
1157  value: URLSearchParams.prototype.entries
1158});
1159
1160// https://heycam.github.io/webidl/#dfn-default-iterator-object
1161function createSearchParamsIterator(target, kind) {
1162  const iterator = ObjectCreate(URLSearchParamsIteratorPrototype);
1163  iterator[context] = {
1164    target,
1165    kind,
1166    index: 0
1167  };
1168  return iterator;
1169}
1170
1171// https://heycam.github.io/webidl/#dfn-iterator-prototype-object
1172const URLSearchParamsIteratorPrototype = ObjectCreate(IteratorPrototype);
1173
1174defineIDLClass(URLSearchParamsIteratorPrototype, 'URLSearchParams Iterator', {
1175  next() {
1176    if (!this ||
1177        ObjectGetPrototypeOf(this) !== URLSearchParamsIteratorPrototype) {
1178      throw new ERR_INVALID_THIS('URLSearchParamsIterator');
1179    }
1180
1181    const {
1182      target,
1183      kind,
1184      index
1185    } = this[context];
1186    const values = target[searchParams];
1187    const len = values.length;
1188    if (index >= len) {
1189      return {
1190        value: undefined,
1191        done: true
1192      };
1193    }
1194
1195    const name = values[index];
1196    const value = values[index + 1];
1197    this[context].index = index + 2;
1198
1199    let result;
1200    if (kind === 'key') {
1201      result = name;
1202    } else if (kind === 'value') {
1203      result = value;
1204    } else {
1205      result = [name, value];
1206    }
1207
1208    return {
1209      value: result,
1210      done: false
1211    };
1212  },
1213  [inspect.custom](recurseTimes, ctx) {
1214    if (this == null || this[context] == null || this[context].target == null)
1215      throw new ERR_INVALID_THIS('URLSearchParamsIterator');
1216
1217    if (typeof recurseTimes === 'number' && recurseTimes < 0)
1218      return ctx.stylize('[Object]', 'special');
1219
1220    const innerOpts = { ...ctx };
1221    if (recurseTimes !== null) {
1222      innerOpts.depth = recurseTimes - 1;
1223    }
1224    const {
1225      target,
1226      kind,
1227      index
1228    } = this[context];
1229    const output = target[searchParams].slice(index).reduce((prev, cur, i) => {
1230      const key = i % 2 === 0;
1231      if (kind === 'key' && key) {
1232        prev.push(cur);
1233      } else if (kind === 'value' && !key) {
1234        prev.push(cur);
1235      } else if (kind === 'key+value' && !key) {
1236        prev.push([target[searchParams][index + i - 1], cur]);
1237      }
1238      return prev;
1239    }, []);
1240    const breakLn = inspect(output, innerOpts).includes('\n');
1241    const outputStrs = output.map((p) => inspect(p, innerOpts));
1242    let outputStr;
1243    if (breakLn) {
1244      outputStr = `\n  ${outputStrs.join(',\n  ')}`;
1245    } else {
1246      outputStr = ` ${outputStrs.join(', ')}`;
1247    }
1248    return `${this[SymbolToStringTag]} {${outputStr} }`;
1249  }
1250});
1251
1252function domainToASCII(domain) {
1253  if (arguments.length < 1)
1254    throw new ERR_MISSING_ARGS('domain');
1255
1256  // toUSVString is not needed.
1257  return _domainToASCII(`${domain}`);
1258}
1259
1260function domainToUnicode(domain) {
1261  if (arguments.length < 1)
1262    throw new ERR_MISSING_ARGS('domain');
1263
1264  // toUSVString is not needed.
1265  return _domainToUnicode(`${domain}`);
1266}
1267
1268// Utility function that converts a URL object into an ordinary
1269// options object as expected by the http.request and https.request
1270// APIs.
1271function urlToOptions(url) {
1272  const options = {
1273    protocol: url.protocol,
1274    hostname: typeof url.hostname === 'string' && url.hostname.startsWith('[') ?
1275      url.hostname.slice(1, -1) :
1276      url.hostname,
1277    hash: url.hash,
1278    search: url.search,
1279    pathname: url.pathname,
1280    path: `${url.pathname || ''}${url.search || ''}`,
1281    href: url.href
1282  };
1283  if (url.port !== '') {
1284    options.port = Number(url.port);
1285  }
1286  if (url.username || url.password) {
1287    options.auth = `${url.username}:${url.password}`;
1288  }
1289  return options;
1290}
1291
1292const forwardSlashRegEx = /\//g;
1293
1294function getPathFromURLWin32(url) {
1295  const hostname = url.hostname;
1296  let pathname = url.pathname;
1297  for (let n = 0; n < pathname.length; n++) {
1298    if (pathname[n] === '%') {
1299      const third = pathname.codePointAt(n + 2) | 0x20;
1300      if ((pathname[n + 1] === '2' && third === 102) || // 2f 2F /
1301          (pathname[n + 1] === '5' && third === 99)) {  // 5c 5C \
1302        throw new ERR_INVALID_FILE_URL_PATH(
1303          'must not include encoded \\ or / characters'
1304        );
1305      }
1306    }
1307  }
1308  pathname = pathname.replace(forwardSlashRegEx, '\\');
1309  pathname = decodeURIComponent(pathname);
1310  if (hostname !== '') {
1311    // If hostname is set, then we have a UNC path
1312    // Pass the hostname through domainToUnicode just in case
1313    // it is an IDN using punycode encoding. We do not need to worry
1314    // about percent encoding because the URL parser will have
1315    // already taken care of that for us. Note that this only
1316    // causes IDNs with an appropriate `xn--` prefix to be decoded.
1317    return `\\\\${domainToUnicode(hostname)}${pathname}`;
1318  }
1319  // Otherwise, it's a local path that requires a drive letter
1320  const letter = pathname.codePointAt(1) | 0x20;
1321  const sep = pathname[2];
1322  if (letter < CHAR_LOWERCASE_A || letter > CHAR_LOWERCASE_Z ||   // a..z A..Z
1323      (sep !== ':')) {
1324    throw new ERR_INVALID_FILE_URL_PATH('must be absolute');
1325  }
1326  return pathname.slice(1);
1327}
1328
1329function getPathFromURLPosix(url) {
1330  if (url.hostname !== '') {
1331    throw new ERR_INVALID_FILE_URL_HOST(platform);
1332  }
1333  const pathname = url.pathname;
1334  for (let n = 0; n < pathname.length; n++) {
1335    if (pathname[n] === '%') {
1336      const third = pathname.codePointAt(n + 2) | 0x20;
1337      if (pathname[n + 1] === '2' && third === 102) {
1338        throw new ERR_INVALID_FILE_URL_PATH(
1339          'must not include encoded / characters'
1340        );
1341      }
1342    }
1343  }
1344  return decodeURIComponent(pathname);
1345}
1346
1347function fileURLToPath(path) {
1348  if (typeof path === 'string')
1349    path = new URL(path);
1350  else if (!isURLInstance(path))
1351    throw new ERR_INVALID_ARG_TYPE('path', ['string', 'URL'], path);
1352  if (path.protocol !== 'file:')
1353    throw new ERR_INVALID_URL_SCHEME('file');
1354  return isWindows ? getPathFromURLWin32(path) : getPathFromURLPosix(path);
1355}
1356
1357// The following characters are percent-encoded when converting from file path
1358// to URL:
1359// - %: The percent character is the only character not encoded by the
1360//        `pathname` setter.
1361// - \: Backslash is encoded on non-windows platforms since it's a valid
1362//      character but the `pathname` setters replaces it by a forward slash.
1363// - LF: The newline character is stripped out by the `pathname` setter.
1364//       (See whatwg/url#419)
1365// - CR: The carriage return character is also stripped out by the `pathname`
1366//       setter.
1367// - TAB: The tab character is also stripped out by the `pathname` setter.
1368const percentRegEx = /%/g;
1369const backslashRegEx = /\\/g;
1370const newlineRegEx = /\n/g;
1371const carriageReturnRegEx = /\r/g;
1372const tabRegEx = /\t/g;
1373
1374function encodePathChars(filepath) {
1375  if (filepath.includes('%'))
1376    filepath = filepath.replace(percentRegEx, '%25');
1377  // In posix, backslash is a valid character in paths:
1378  if (!isWindows && filepath.includes('\\'))
1379    filepath = filepath.replace(backslashRegEx, '%5C');
1380  if (filepath.includes('\n'))
1381    filepath = filepath.replace(newlineRegEx, '%0A');
1382  if (filepath.includes('\r'))
1383    filepath = filepath.replace(carriageReturnRegEx, '%0D');
1384  if (filepath.includes('\t'))
1385    filepath = filepath.replace(tabRegEx, '%09');
1386  return filepath;
1387}
1388
1389function pathToFileURL(filepath) {
1390  const outURL = new URL('file://');
1391  if (isWindows && filepath.startsWith('\\\\')) {
1392    // UNC path format: \\server\share\resource
1393    const paths = filepath.split('\\');
1394    if (paths.length <= 3) {
1395      throw new ERR_INVALID_ARG_VALUE(
1396        'filepath',
1397        filepath,
1398        'Missing UNC resource path'
1399      );
1400    }
1401    const hostname = paths[2];
1402    if (hostname.length === 0) {
1403      throw new ERR_INVALID_ARG_VALUE(
1404        'filepath',
1405        filepath,
1406        'Empty UNC servername'
1407      );
1408    }
1409    outURL.hostname = domainToASCII(hostname);
1410    outURL.pathname = encodePathChars(paths.slice(3).join('/'));
1411  } else {
1412    let resolved = path.resolve(filepath);
1413    // path.resolve strips trailing slashes so we must add them back
1414    const filePathLast = filepath.charCodeAt(filepath.length - 1);
1415    if ((filePathLast === CHAR_FORWARD_SLASH ||
1416         (isWindows && filePathLast === CHAR_BACKWARD_SLASH)) &&
1417        resolved[resolved.length - 1] !== path.sep)
1418      resolved += '/';
1419    outURL.pathname = encodePathChars(resolved);
1420  }
1421  return outURL;
1422}
1423
1424function isURLInstance(fileURLOrPath) {
1425  return fileURLOrPath != null && fileURLOrPath.href && fileURLOrPath.origin;
1426}
1427
1428function toPathIfFileURL(fileURLOrPath) {
1429  if (!isURLInstance(fileURLOrPath))
1430    return fileURLOrPath;
1431  return fileURLToPath(fileURLOrPath);
1432}
1433
1434function constructUrl(flags, protocol, username, password,
1435                      host, port, path, query, fragment) {
1436  const ctx = new URLContext();
1437  ctx.flags = flags;
1438  ctx.scheme = protocol;
1439  ctx.username = (flags & URL_FLAGS_HAS_USERNAME) !== 0 ? username : '';
1440  ctx.password = (flags & URL_FLAGS_HAS_PASSWORD) !== 0 ? password : '';
1441  ctx.port = port;
1442  ctx.path = (flags & URL_FLAGS_HAS_PATH) !== 0 ? path : [];
1443  ctx.query = query;
1444  ctx.fragment = fragment;
1445  ctx.host = host;
1446
1447  const url = ObjectCreate(URL.prototype);
1448  url[context] = ctx;
1449  const params = new URLSearchParams();
1450  url[searchParams] = params;
1451  params[context] = url;
1452  initSearchParams(params, query);
1453  return url;
1454}
1455setURLConstructor(constructUrl);
1456
1457module.exports = {
1458  toUSVString,
1459  fileURLToPath,
1460  pathToFileURL,
1461  toPathIfFileURL,
1462  isURLInstance,
1463  URL,
1464  URLSearchParams,
1465  domainToASCII,
1466  domainToUnicode,
1467  urlToOptions,
1468  formatSymbol: kFormat,
1469  searchParamsSymbol: searchParams,
1470  encodeStr
1471};
1472