@limetech/lime-elements 40.0.3 → 40.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19825,6 +19825,9 @@ function requireUtils$1 () {
19825
19825
  /** @type {(value: string) => boolean} */
19826
19826
  const isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
19827
19827
 
19828
+ /** @type {(value: string) => boolean} */
19829
+ const isPort = RegExp.prototype.test.bind(/^\d*$/u);
19830
+
19828
19831
  /** @type {(value: string) => boolean} */
19829
19832
  const isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
19830
19833
 
@@ -19832,10 +19835,13 @@ function requireUtils$1 () {
19832
19835
  const isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
19833
19836
 
19834
19837
  /** @type {(value: string) => boolean} */
19835
- const isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
19838
+ const isPathCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/]$/u);
19836
19839
 
19837
19840
  /** @type {(value: string) => boolean} */
19838
- const isQueryFragmentCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/?]$/iu);
19841
+ const isQueryFragmentCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/?]$/u);
19842
+
19843
+ /** @type {(value: string) => boolean} */
19844
+ const isUserinfoCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:]$/u);
19839
19845
 
19840
19846
  /**
19841
19847
  * @param {Array<string>} input
@@ -19868,12 +19874,14 @@ function requireUtils$1 () {
19868
19874
  return acc
19869
19875
  }
19870
19876
 
19871
- /**
19872
- * @typedef {Object} GetIPV6Result
19873
- * @property {boolean} error - Indicates if there was an error parsing the IPv6 address.
19874
- * @property {string} address - The parsed IPv6 address.
19875
- * @property {string} [zone] - The zone identifier, if present.
19876
- */
19877
+ /** @type {(value: string) => boolean} */
19878
+ const isHextet = RegExp.prototype.test.bind(/^[\dA-Fa-f]{1,4}$/);
19879
+
19880
+ /** @type {(value: string) => boolean} */
19881
+ const isIPvFuture = RegExp.prototype.test.bind(/^[vV][\dA-Fa-f]+\.[A-Za-z\d\-._~!$&'()*+,;=:]+$/);
19882
+
19883
+ /** @type {(value: string) => boolean} */
19884
+ const isZoneCharacter = RegExp.prototype.test.bind(/^[A-Za-z\d\-._~]$/);
19877
19885
 
19878
19886
  /**
19879
19887
  * @param {string} value
@@ -19881,89 +19889,129 @@ function requireUtils$1 () {
19881
19889
  */
19882
19890
  const nonSimpleDomain = RegExp.prototype.test.bind(/[^!"$&'()*+,\-.;=_`a-z{}~]/u);
19883
19891
 
19892
+ /** @type {(value: string) => boolean} */
19893
+ const nonSimpleDomainChars = RegExp.prototype.test.bind(/[^\d!"$&'()*+,\-.;=_`a-z{}~]/u);
19894
+
19895
+ /** @type {(value: string) => boolean} */
19896
+ const numericLastLabel = RegExp.prototype.test.bind(/(?:^|\.)\d+$/u);
19897
+
19884
19898
  /**
19885
- * @param {Array<string>} buffer
19899
+ * True when `new URL('http://' + domain).hostname` may differ from `domain`,
19900
+ * i.e. when the caller must fall back to the WHATWG parser.
19901
+ *
19902
+ * Same purpose as `nonSimpleDomain`, but digits are allowed: for this character
19903
+ * set `hostname` is the identity, *except* that an all-numeric final label is
19904
+ * read as IPv4 shorthand ("1.2.3" -> "1.2.0.3", "127.1" -> "127.0.0.1",
19905
+ * "a.b.1" throws), which is why `nonSimpleDomain` excludes digits outright.
19906
+ * Rejecting that one shape lets the common `mail2.example.org` case stay on the
19907
+ * fast path.
19908
+ *
19909
+ * @param {string} domain - must already be lowercased
19886
19910
  * @returns {boolean}
19887
19911
  */
19888
- function consumeIsZone (buffer) {
19889
- buffer.length = 0;
19890
- return true
19912
+ function nonSimpleMailtoDomain (domain) {
19913
+ return nonSimpleDomainChars(domain) || numericLastLabel(domain)
19891
19914
  }
19892
19915
 
19893
19916
  /**
19894
- * @param {Array<string>} buffer
19895
- * @param {Array<string>} address
19896
- * @param {GetIPV6Result} output
19917
+ * @param {string} zone
19897
19918
  * @returns {boolean}
19898
19919
  */
19899
- function consumeHextets (buffer, address, output) {
19900
- if (buffer.length) {
19901
- const hex = stringArrayToHexStripped(buffer);
19902
- if (hex !== '') {
19903
- address.push(hex);
19904
- } else {
19905
- output.error = true;
19906
- return false
19920
+ function isZoneIdentifier (zone) {
19921
+ if (zone.length === 0) return false
19922
+
19923
+ for (let i = 0; i < zone.length; i++) {
19924
+ if (isZoneCharacter(zone[i])) continue
19925
+ if (zone[i] === '%' && i + 2 < zone.length && isHexPair(zone.slice(i + 1, i + 3))) {
19926
+ i += 2;
19927
+ continue
19907
19928
  }
19908
- buffer.length = 0;
19929
+ return false
19909
19930
  }
19931
+
19910
19932
  return true
19911
19933
  }
19912
19934
 
19913
19935
  /**
19914
- * @param {string} input
19915
- * @returns {GetIPV6Result}
19936
+ * Compresses the longest run of zero hextets to "::" per RFC 5952. A run of a
19937
+ * single zero hextet is left uncompressed. On ties the leftmost run wins.
19938
+ *
19939
+ * @param {string[]} hextets
19940
+ * @returns {string}
19916
19941
  */
19917
- function getIPV6 (input) {
19918
- let tokenCount = 0;
19919
- const output = { error: false, address: '', zone: '' };
19920
- /** @type {Array<string>} */
19921
- const address = [];
19922
- /** @type {Array<string>} */
19923
- const buffer = [];
19924
- let endipv6Encountered = false;
19925
- let endIpv6 = false;
19926
-
19927
- let consume = consumeHextets;
19928
-
19929
- for (let i = 0; i < input.length; i++) {
19930
- const cursor = input[i];
19931
- if (cursor === '[' || cursor === ']') { continue }
19932
- if (cursor === ':') {
19933
- if (endipv6Encountered === true) {
19934
- endIpv6 = true;
19935
- }
19936
- if (!consume(buffer, address, output)) { break }
19937
- if (++tokenCount > 7) {
19938
- // not valid
19939
- output.error = true;
19940
- break
19942
+ function compressIPv6ZeroRun (hextets) {
19943
+ let bestStart = -1;
19944
+ let bestLength = 0;
19945
+ let runStart = -1;
19946
+ let runLength = 0;
19947
+ for (let i = 0; i < hextets.length; i++) {
19948
+ if (hextets[i] === '0') {
19949
+ if (runStart === -1) runStart = i;
19950
+ runLength++;
19951
+ if (runLength > bestLength) {
19952
+ bestLength = runLength;
19953
+ bestStart = runStart;
19941
19954
  }
19942
- if (i > 0 && input[i - 1] === ':') {
19943
- endipv6Encountered = true;
19944
- }
19945
- address.push(':');
19946
- continue
19947
- } else if (cursor === '%') {
19948
- if (!consume(buffer, address, output)) { break }
19949
- // switch to zone detection
19950
- consume = consumeIsZone;
19951
19955
  } else {
19952
- buffer.push(cursor);
19953
- continue
19956
+ runStart = -1;
19957
+ runLength = 0;
19954
19958
  }
19955
19959
  }
19956
- if (buffer.length) {
19957
- if (consume === consumeIsZone) {
19958
- output.zone = buffer.join('');
19959
- } else if (endIpv6) {
19960
- address.push(buffer.join(''));
19961
- } else {
19962
- address.push(stringArrayToHexStripped(buffer));
19960
+
19961
+ if (bestLength < 2) return hextets.join(':')
19962
+
19963
+ const head = hextets.slice(0, bestStart).join(':');
19964
+ const tail = hextets.slice(bestStart + bestLength).join(':');
19965
+ return head + '::' + tail
19966
+ }
19967
+
19968
+ /**
19969
+ * Validates an IPv6 address against the alternatives in RFC 3986 section
19970
+ * 3.2.2 and returns the same address with leading hextet zeroes removed.
19971
+ * An embedded IPv4 address counts as two hextets and is only valid at the end.
19972
+ *
19973
+ * @param {string} input
19974
+ * @returns {string|undefined}
19975
+ */
19976
+ function normalizeIPv6Address (input) {
19977
+ const compression = input.indexOf('::');
19978
+ if (compression !== -1 && input.indexOf('::', compression + 1) !== -1) return undefined
19979
+
19980
+ const left = compression === -1 ? input.split(':') : input.slice(0, compression).split(':');
19981
+ const right = compression === -1 ? [] : input.slice(compression + 2).split(':');
19982
+ if (compression !== -1) {
19983
+ if (left.length === 1 && left[0] === '') left.length = 0;
19984
+ if (right.length === 1 && right[0] === '') right.length = 0;
19985
+ }
19986
+
19987
+ const parts = left.concat(right);
19988
+ let hextetCount = 0;
19989
+ for (let i = 0; i < parts.length; i++) {
19990
+ const part = parts[i];
19991
+ if (part === '') return undefined
19992
+
19993
+ if (part.indexOf('.') !== -1) {
19994
+ if (i !== parts.length - 1 || (compression !== -1 && right.length === 0) || !isIPv4(part)) return undefined
19995
+ hextetCount += 2;
19996
+ continue
19963
19997
  }
19998
+
19999
+ if (!isHextet(part)) return undefined
20000
+ parts[i] = parseInt(part, 16).toString(16);
20001
+ hextetCount++;
19964
20002
  }
19965
- output.address = address.join('');
19966
- return output
20003
+
20004
+ if (compression === -1) {
20005
+ if (hextetCount !== 8) return undefined
20006
+ return compressIPv6ZeroRun(parts)
20007
+ }
20008
+ if (hextetCount >= 8) return undefined
20009
+
20010
+ // expand "::" then re-compress the longest run for a canonical result
20011
+ const expanded = parts.slice(0, left.length);
20012
+ for (let i = hextetCount; i < 8; i++) expanded.push('0');
20013
+ for (let i = left.length; i < parts.length; i++) expanded.push(parts[i]);
20014
+ return compressIPv6ZeroRun(expanded)
19967
20015
  }
19968
20016
 
19969
20017
  /**
@@ -19971,26 +20019,49 @@ function requireUtils$1 () {
19971
20019
  * @property {string} host - The normalized host.
19972
20020
  * @property {string} [escapedHost] - The escaped host.
19973
20021
  * @property {boolean} isIPV6 - Indicates if the host is an IPv6 address.
20022
+ * @property {boolean} [isIPVFuture] - Indicates if the host is an IPvFuture literal.
20023
+ * @property {boolean} [error] - Indicates if a bracketed IP literal is malformed.
19974
20024
  */
19975
20025
 
19976
20026
  /**
20027
+ * Validates and normalizes a bracketed IP literal. Raw zone separators remain
20028
+ * accepted for backwards compatibility, while encoded separators and zone
20029
+ * contents follow RFC 6874.
20030
+ *
19977
20031
  * @param {string} host
19978
20032
  * @returns {NormalizeIPv6Result}
19979
20033
  */
19980
20034
  function normalizeIPv6 (host) {
19981
- if (findToken(host, ':') < 2) { return { host, isIPV6: false } }
19982
- const ipv6 = getIPV6(host);
20035
+ const bracketed = host[0] === '[' && host[host.length - 1] === ']';
20036
+ const hasBracket = host[0] === '[' || host[host.length - 1] === ']';
20037
+ if (hasBracket && !bracketed) return { host, isIPV6: false, error: true }
19983
20038
 
19984
- if (!ipv6.error) {
19985
- let newHost = ipv6.address;
19986
- let escapedHost = ipv6.address;
19987
- if (ipv6.zone) {
19988
- newHost += '%' + ipv6.zone;
19989
- escapedHost += '%25' + ipv6.zone;
19990
- }
19991
- return { host: newHost, isIPV6: true, escapedHost }
19992
- } else {
19993
- return { host, isIPV6: false }
20039
+ let input = bracketed ? host.slice(1, -1) : host;
20040
+ if (bracketed && isIPvFuture(input)) {
20041
+ input = input.toLowerCase();
20042
+ return { host: `[${input}]`, escapedHost: input, isIPV6: false, isIPVFuture: true }
20043
+ }
20044
+
20045
+ if (findToken(input, ':') < 2) {
20046
+ return { host, isIPV6: false, error: bracketed }
20047
+ }
20048
+
20049
+ let zoneIdentifier = '';
20050
+ const zoneSeparator = input.indexOf('%');
20051
+ if (zoneSeparator !== -1) {
20052
+ const separatorLength = input.slice(zoneSeparator, zoneSeparator + 3).toLowerCase() === '%25' ? 3 : 1;
20053
+ zoneIdentifier = input.slice(zoneSeparator + separatorLength);
20054
+ if (!isZoneIdentifier(zoneIdentifier)) return { host, isIPV6: false, error: true }
20055
+ input = input.slice(0, zoneSeparator);
20056
+ }
20057
+
20058
+ const address = normalizeIPv6Address(input);
20059
+ if (address === undefined) return { host, isIPV6: false, error: true }
20060
+
20061
+ return {
20062
+ host: address + (zoneIdentifier ? '%' + zoneIdentifier : ''),
20063
+ escapedHost: address + (zoneIdentifier ? '%25' + zoneIdentifier : ''),
20064
+ isIPV6: true
19994
20065
  }
19995
20066
  }
19996
20067
 
@@ -20116,7 +20187,7 @@ function requireUtils$1 () {
20116
20187
 
20117
20188
  /**
20118
20189
  * Normalizes percent escapes and optionally decodes only unreserved ASCII bytes.
20119
- * Reserved delimiters such as `%2F` and `%2E` stay escaped.
20190
+ * Reserved delimiters such as `%2F` stay escaped; `%2E` is unreserved.
20120
20191
  *
20121
20192
  * @param {string} input
20122
20193
  * @param {boolean} [decodeUnreserved=false]
@@ -20257,6 +20328,60 @@ function requireUtils$1 () {
20257
20328
  return output
20258
20329
  }
20259
20330
 
20331
+ /**
20332
+ * Serializes a path without rewriting reserved data. Raw RFC 3986 path
20333
+ * characters remain literal, valid escapes are preserved and uppercased, and
20334
+ * everything else is UTF-8 percent-encoded. In a path-noscheme, a colon in the
20335
+ * first segment must be escaped so the result cannot be parsed as a scheme.
20336
+ *
20337
+ * @param {string} input
20338
+ * @param {boolean} [pathNoScheme=false]
20339
+ * @returns {string}
20340
+ */
20341
+ function serializePathEncoding (input, pathNoScheme = false) {
20342
+ let output = '';
20343
+ let firstSegment = pathNoScheme && input[0] !== '/';
20344
+
20345
+ for (let i = 0; i < input.length; i++) {
20346
+ const ch = input[i];
20347
+ if (ch === '%' && i + 2 < input.length) {
20348
+ const hex = input.slice(i + 1, i + 3);
20349
+ if (isHexPair(hex)) {
20350
+ output += '%' + hex.toUpperCase();
20351
+ i += 2;
20352
+ continue
20353
+ }
20354
+ }
20355
+
20356
+ if (ch === '/') {
20357
+ firstSegment = false;
20358
+ }
20359
+
20360
+ if (isPathCharacter(ch) && (ch !== ':' || !firstSegment)) {
20361
+ output += ch;
20362
+ } else {
20363
+ const code = input.charCodeAt(i);
20364
+ if (code < 0x80) {
20365
+ output += BYTE_HEX[code];
20366
+ } else if (code < 0xD800 || code > 0xDFFF) {
20367
+ output += percentEncodeNonAscii(code);
20368
+ } else if (code <= 0xDBFF && i + 1 < input.length) {
20369
+ const low = input.charCodeAt(i + 1);
20370
+ if (low >= 0xDC00 && low <= 0xDFFF) {
20371
+ output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00));
20372
+ i++;
20373
+ } else {
20374
+ output += percentEncodeNonAscii(0xFFFD);
20375
+ }
20376
+ } else {
20377
+ output += percentEncodeNonAscii(0xFFFD);
20378
+ }
20379
+ }
20380
+ }
20381
+
20382
+ return output
20383
+ }
20384
+
20260
20385
  /**
20261
20386
  * Normalizes the percent-encoding of a query or fragment component.
20262
20387
  *
@@ -20314,6 +20439,85 @@ function requireUtils$1 () {
20314
20439
  return output
20315
20440
  }
20316
20441
 
20442
+ /**
20443
+ * Percent-encodes a URI component using its RFC 3986 literal character set.
20444
+ * Existing valid escapes are preserved and normalized to uppercase hex.
20445
+ *
20446
+ * @param {string} input
20447
+ * @param {(value: string) => boolean} isAllowed
20448
+ * @returns {string}
20449
+ */
20450
+ function encodeComponent (input, isAllowed) {
20451
+ let output = '';
20452
+
20453
+ for (let i = 0; i < input.length; i++) {
20454
+ const ch = input[i];
20455
+ if (ch === '%' && i + 2 < input.length) {
20456
+ const hex = input.slice(i + 1, i + 3);
20457
+ if (isHexPair(hex)) {
20458
+ output += '%' + hex.toUpperCase();
20459
+ i += 2;
20460
+ continue
20461
+ }
20462
+ }
20463
+
20464
+ if (isAllowed(ch)) {
20465
+ output += ch;
20466
+ } else {
20467
+ const code = input.charCodeAt(i);
20468
+ if (code < 0x80) {
20469
+ output += BYTE_HEX[code];
20470
+ } else if (code < 0xD800 || code > 0xDFFF) {
20471
+ output += percentEncodeNonAscii(code);
20472
+ } else if (code <= 0xDBFF && i + 1 < input.length) {
20473
+ const low = input.charCodeAt(i + 1);
20474
+ if (low >= 0xDC00 && low <= 0xDFFF) {
20475
+ output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00));
20476
+ i++;
20477
+ } else {
20478
+ output += percentEncodeNonAscii(0xFFFD);
20479
+ }
20480
+ } else {
20481
+ output += percentEncodeNonAscii(0xFFFD);
20482
+ }
20483
+ }
20484
+ }
20485
+
20486
+ return output
20487
+ }
20488
+
20489
+ /**
20490
+ * Encodes userinfo while preserving its RFC 3986 §3.2.1 literal characters.
20491
+ * In particular, authority delimiters such as `@`, `/`, `?`, and `#` are data.
20492
+ *
20493
+ * @param {string} input
20494
+ * @returns {string}
20495
+ */
20496
+ function encodeUserinfo (input) {
20497
+ return encodeComponent(input, isUserinfoCharacter)
20498
+ }
20499
+
20500
+ /**
20501
+ * Encodes query data using the RFC 3986 §3.4 grammar. A literal `#` must be
20502
+ * escaped because it would otherwise begin the fragment component.
20503
+ *
20504
+ * @param {string} input
20505
+ * @returns {string}
20506
+ */
20507
+ function encodeQuery (input) {
20508
+ return encodeComponent(input, isQueryFragmentCharacter)
20509
+ }
20510
+
20511
+ /**
20512
+ * Encodes fragment data using the RFC 3986 §3.5 grammar.
20513
+ *
20514
+ * @param {string} input
20515
+ * @returns {string}
20516
+ */
20517
+ function encodeFragment (input) {
20518
+ return encodeComponent(input, isQueryFragmentCharacter)
20519
+ }
20520
+
20317
20521
  /**
20318
20522
  * Escapes a component while preserving existing valid percent escapes.
20319
20523
  *
@@ -20363,15 +20567,21 @@ function requireUtils$1 () {
20363
20567
  const uriTokens = [];
20364
20568
 
20365
20569
  if (component.userinfo !== undefined) {
20366
- uriTokens.push(component.userinfo);
20570
+ uriTokens.push(encodeUserinfo(component.userinfo));
20367
20571
  uriTokens.push('@');
20368
20572
  }
20369
20573
 
20370
20574
  if (component.host !== undefined) {
20371
- let host = unescape(component.host);
20575
+ let host = component.host;
20372
20576
  if (!isIPv4(host)) {
20373
- const ipV6res = normalizeIPv6(host);
20374
- if (ipV6res.isIPV6 === true) {
20577
+ let ipV6res = normalizeIPv6(host);
20578
+ if (ipV6res.isIPV6 !== true && ipV6res.isIPVFuture !== true) {
20579
+ // Decode only unreserved bytes, once. In particular, keep %25 encoded
20580
+ // so it cannot introduce a second escape during recomposition.
20581
+ host = normalizePercentEncoding(host, true);
20582
+ ipV6res = normalizeIPv6(host);
20583
+ }
20584
+ if (ipV6res.isIPV6 === true || ipV6res.isIPVFuture === true) {
20375
20585
  host = `[${ipV6res.escapedHost}]`;
20376
20586
  } else {
20377
20587
  host = reescapeHostDelimiters(host, false);
@@ -20381,19 +20591,30 @@ function requireUtils$1 () {
20381
20591
  }
20382
20592
 
20383
20593
  if (typeof component.port === 'number' || typeof component.port === 'string') {
20594
+ const port = String(component.port);
20595
+ if (!isPort(port)) {
20596
+ throw new TypeError('URI port is malformed.')
20597
+ }
20384
20598
  uriTokens.push(':');
20385
- uriTokens.push(String(component.port));
20599
+ uriTokens.push(port);
20386
20600
  }
20387
20601
 
20388
20602
  return uriTokens.length ? uriTokens.join('') : undefined
20389
20603
  }
20390
20604
  utils$1 = {
20605
+ BYTE_HEX,
20606
+ percentEncodeNonAscii,
20391
20607
  nonSimpleDomain,
20608
+ nonSimpleMailtoDomain,
20392
20609
  recomposeAuthority,
20393
20610
  reescapeHostDelimiters,
20394
20611
  normalizePercentEncoding,
20395
20612
  normalizePathEncoding,
20613
+ serializePathEncoding,
20396
20614
  normalizeQueryFragmentEncoding,
20615
+ encodeUserinfo,
20616
+ encodeQuery,
20617
+ encodeFragment,
20397
20618
  escapePreservingEscapes,
20398
20619
  removeDotSegments,
20399
20620
  isIPv4,
@@ -20411,11 +20632,11 @@ function requireSchemes$1 () {
20411
20632
  if (hasRequiredSchemes$1) return schemes$1;
20412
20633
  hasRequiredSchemes$1 = 1;
20413
20634
 
20414
- const { isUUID } = requireUtils$1();
20415
- const URN_REG = /([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-.:;=@]|%[\da-f]{2})+)/iu;
20635
+ const { isUUID, BYTE_HEX, percentEncodeNonAscii, nonSimpleMailtoDomain } = requireUtils$1();
20636
+ const URN_REG = /^([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-./:;=@]|%[\da-f]{2})+)$/iu;
20416
20637
 
20417
20638
  const supportedSchemeNames = /** @type {const} */ (['http', 'https', 'ws',
20418
- 'wss', 'urn', 'urn:uuid']);
20639
+ 'wss', 'urn', 'urn:uuid', 'mailto']);
20419
20640
 
20420
20641
  /** @typedef {supportedSchemeNames[number]} SchemeName */
20421
20642
 
@@ -20524,9 +20745,14 @@ function requireSchemes$1 () {
20524
20745
 
20525
20746
  // reconstruct path from resource name
20526
20747
  if (wsComponent.resourceName) {
20527
- const [path, query] = wsComponent.resourceName.split('?');
20748
+ const queryIndex = wsComponent.resourceName.indexOf('?');
20749
+ const path = queryIndex === -1
20750
+ ? wsComponent.resourceName
20751
+ : wsComponent.resourceName.slice(0, queryIndex);
20528
20752
  wsComponent.path = (path && path !== '/' ? path : undefined);
20529
- wsComponent.query = query;
20753
+ wsComponent.query = queryIndex === -1
20754
+ ? undefined
20755
+ : wsComponent.resourceName.slice(queryIndex + 1);
20530
20756
  wsComponent.resourceName = undefined;
20531
20757
  }
20532
20758
 
@@ -20543,7 +20769,7 @@ function requireSchemes$1 () {
20543
20769
  return urnComponent
20544
20770
  }
20545
20771
  const matches = urnComponent.path.match(URN_REG);
20546
- if (matches) {
20772
+ if (matches && matches[0] === urnComponent.path) {
20547
20773
  const scheme = options.scheme || urnComponent.scheme || 'urn';
20548
20774
  urnComponent.nid = matches[1].toLowerCase();
20549
20775
  urnComponent.nss = matches[2];
@@ -20632,18 +20858,389 @@ function requireSchemes$1 () {
20632
20858
  serialize: ws.serialize
20633
20859
  });
20634
20860
 
20635
- const urn = /** @type {SchemeHandler} */ ({
20636
- scheme: 'urn',
20637
- parse: urnParse,
20638
- serialize: urnSerialize,
20639
- skipNormalize: true
20640
- });
20861
+ const urn = /** @type {SchemeHandler} */ ({
20862
+ scheme: 'urn',
20863
+ parse: urnParse,
20864
+ serialize: urnSerialize,
20865
+ skipNormalize: true
20866
+ });
20867
+
20868
+ const urnuuid = /** @type {SchemeHandler} */ ({
20869
+ scheme: 'urn:uuid',
20870
+ parse: urnuuidParse,
20871
+ serialize: urnuuidSerialize,
20872
+ skipNormalize: true
20873
+ });
20874
+
20875
+ /**
20876
+ * Allow-sets for `encodeWithAllow`, each a `{ all, table, allowNonAscii }`
20877
+ * descriptor built by `allowSet`:
20878
+ *
20879
+ * - `LOCAL_PART` is the intersection of upstream uri-js's VCHAR and
20880
+ * NOT_PATH_NOSCHEME, minus ",": the serializer joins recipients with "," and
20881
+ * `mailtoParse` splits the path on it, so a comma left literal inside a local
20882
+ * part would silently become a recipient delimiter.
20883
+ * - `HFNAME` is the qchar allow-set used for header names/values.
20884
+ * - `DTEXT` is RFC 6068 dtext-no-obs restricted to characters that are also
20885
+ * safe in a URI path; `DTEXT_IRI` additionally permits non-ASCII for
20886
+ * `unicodeSupport`.
20887
+ */
20888
+ const LOCAL_PART_ALLOWED = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~!$'()*+=";
20889
+ const HFNAME_ALLOWED = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~!$'()*+,;:@";
20890
+ const DTEXT_ALLOWED = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~!$'()*+;:";
20891
+
20892
+ /**
20893
+ * @typedef {Object} AllowSet
20894
+ * @property {RegExp} all - matches a whole string needing no encoding at all.
20895
+ * @property {Uint8Array} table - per-ASCII-code allow lookup for the slow path.
20896
+ * @property {boolean} allowNonAscii - pass non-ASCII code points through (IRI).
20897
+ */
20898
+
20899
+ /**
20900
+ * `all` deliberately excludes "%" and (for the IRI variant) surrogates, so any
20901
+ * input needing escape normalization or surrogate repair falls to the slow path.
20902
+ *
20903
+ * @param {string} chars
20904
+ * @param {boolean} [allowNonAscii=false]
20905
+ * @returns {AllowSet}
20906
+ */
20907
+ function allowSet (chars, allowNonAscii = false) {
20908
+ const table = new Uint8Array(128);
20909
+ for (let i = 0; i < chars.length; i++) {
20910
+ table[chars.charCodeAt(i)] = 1;
20911
+ }
20912
+ const escaped = chars.replace(/[\\\]^-]/gu, '\\$&');
20913
+ const cls = allowNonAscii ? `[${escaped}]|[^\\0-\\x7F\\uD800-\\uDFFF]` : `[${escaped}]`;
20914
+ return { all: new RegExp(`^(?:${cls})*$`, 'u'), table, allowNonAscii }
20915
+ }
20916
+
20917
+ const LOCAL_PART = allowSet(LOCAL_PART_ALLOWED);
20918
+ const HFNAME = allowSet(HFNAME_ALLOWED);
20919
+ const DTEXT = allowSet(DTEXT_ALLOWED);
20920
+ const DTEXT_IRI = allowSet(DTEXT_ALLOWED, true);
20921
+
20922
+ const HEX_PAIR = /^[\da-f]{2}$/iu;
20923
+ const MAILTO_DOMAIN_LITERAL = /^\[[\x21-\x5A\x5E-\x7E]*\]$/u;
20924
+ const MAILTO_DOMAIN_ERROR = 'URI mailto has an invalid recipient domain.';
20925
+ const HAS_SURROGATE = /[\uD800-\uDFFF]/u;
20926
+
20927
+ /**
20928
+ * @param {string} str
20929
+ * @returns {string}
20930
+ */
20931
+ function decodeHex (str) {
20932
+ if (typeof str !== 'string' || str.indexOf('%') === -1) {
20933
+ return str
20934
+ }
20935
+ try {
20936
+ return decodeURIComponent(str)
20937
+ } catch {
20938
+ return str
20939
+ }
20940
+ }
20941
+
20942
+ /**
20943
+ * Replaces lone surrogates with U+FFFD, leaving valid pairs intact. Only called
20944
+ * when `HAS_SURROGATE` matched, so the scan cost is not on the common path.
20945
+ * (`String.prototype.toWellFormed` would do this natively but is Node 20+.)
20946
+ *
20947
+ * @param {string} input
20948
+ * @returns {string}
20949
+ */
20950
+ function replaceLoneSurrogates (input) {
20951
+ let result = '';
20952
+ for (let i = 0; i < input.length; i++) {
20953
+ const code = input.charCodeAt(i);
20954
+ if (code >= 0xD800 && code <= 0xDBFF && i + 1 < input.length) {
20955
+ const low = input.charCodeAt(i + 1);
20956
+ if (low >= 0xDC00 && low <= 0xDFFF) {
20957
+ result += input[i] + input[i + 1];
20958
+ i++;
20959
+ continue
20960
+ }
20961
+ }
20962
+ result += (code >= 0xD800 && code <= 0xDFFF) ? '\uFFFD' : input[i];
20963
+ }
20964
+ return result
20965
+ }
20966
+
20967
+ /**
20968
+ * Percent-encodes everything outside `set`, preserving existing valid escapes
20969
+ * (uppercased) and repairing lone surrogates.
20970
+ *
20971
+ * ASCII uses `BYTE_HEX` rather than `encodeURIComponent`. That is equivalent
20972
+ * only because every character `encodeURIComponent` leaves unescaped
20973
+ * (`A-Za-z0-9-_.!~*'()`) is present in all of the allow-sets above, so no such
20974
+ * character ever reaches the encode branch. Re-check if a set is narrowed.
20975
+ *
20976
+ * @param {string} input
20977
+ * @param {AllowSet} set
20978
+ * @returns {string}
20979
+ */
20980
+ function encodeWithAllow (input, set) {
20981
+ if (set.all.test(input)) {
20982
+ return input
20983
+ }
20984
+
20985
+ const table = set.table;
20986
+ let result = '';
20987
+ for (let i = 0; i < input.length; i++) {
20988
+ const code = input.charCodeAt(i);
20989
+
20990
+ if (code < 0x80) {
20991
+ if (table[code] === 1) {
20992
+ result += input[i];
20993
+ } else if (code === 0x25 && i + 2 < input.length && HEX_PAIR.test(input.slice(i + 1, i + 3))) {
20994
+ result += '%' + input.slice(i + 1, i + 3).toUpperCase();
20995
+ i += 2;
20996
+ } else {
20997
+ result += BYTE_HEX[code];
20998
+ }
20999
+ continue
21000
+ }
21001
+
21002
+ if (code < 0xD800 || code > 0xDFFF) {
21003
+ result += set.allowNonAscii ? input[i] : percentEncodeNonAscii(code);
21004
+ continue
21005
+ }
21006
+
21007
+ if (code <= 0xDBFF && i + 1 < input.length) {
21008
+ const low = input.charCodeAt(i + 1);
21009
+ if (low >= 0xDC00 && low <= 0xDFFF) {
21010
+ result += set.allowNonAscii
21011
+ ? input[i] + input[i + 1]
21012
+ : percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00));
21013
+ i++;
21014
+ continue
21015
+ }
21016
+ }
21017
+
21018
+ result += set.allowNonAscii ? '\uFFFD' : percentEncodeNonAscii(0xFFFD);
21019
+ }
21020
+ return result
21021
+ }
21022
+
21023
+ /**
21024
+ * @param {string} domain
21025
+ * @param {import('../types/index').Options} [options]
21026
+ * @param {{error?:string}} [component]
21027
+ * @returns {string}
21028
+ */
21029
+ function mailtoNormalizeDomain (domain, options, component) {
21030
+ const normalizedDomain = String(domain).toLowerCase();
21031
+
21032
+ if (MAILTO_DOMAIN_LITERAL.test(normalizedDomain)) {
21033
+ return normalizedDomain
21034
+ }
21035
+
21036
+ // `hostname` is the identity for these, so skip constructing a WHATWG URL.
21037
+ if (normalizedDomain !== '' && !nonSimpleMailtoDomain(normalizedDomain)) {
21038
+ return normalizedDomain
21039
+ }
21040
+
21041
+ try {
21042
+ const parsedDomain = new URL('http://' + normalizedDomain);
21043
+ if (
21044
+ parsedDomain.username ||
21045
+ parsedDomain.password ||
21046
+ parsedDomain.port ||
21047
+ parsedDomain.pathname !== '/' ||
21048
+ parsedDomain.search ||
21049
+ parsedDomain.hash ||
21050
+ !parsedDomain.hostname
21051
+ ) {
21052
+ throw new Error(MAILTO_DOMAIN_ERROR)
21053
+ }
21054
+
21055
+ return options && options.unicodeSupport
21056
+ ? normalizedDomain
21057
+ : parsedDomain.hostname
21058
+ } catch {
21059
+ if (component) {
21060
+ component.error = component.error || MAILTO_DOMAIN_ERROR;
21061
+ }
21062
+ return normalizedDomain
21063
+ }
21064
+ }
21065
+
21066
+ /**
21067
+ * Percent-encodes everything in a recipient domain that is not `dtext-no-obs`
21068
+ * (RFC 6068). Serialize-only: `mailtoNormalizeDomain` returns the domain
21069
+ * verbatim when validation fails, and `MAILTO_DOMAIN_LITERAL` accepts the full
21070
+ * RFC 5321 dcontent range (which includes "?", "#", "&" and "/"), so without
21071
+ * this an attacker-supplied recipient could inject header fields or a fragment
21072
+ * into the serialized URI. `mailtoParse` deliberately reports the raw domain
21073
+ * instead, so it must not use this.
21074
+ * @param {string} domain
21075
+ * @param {import('../types/index').Options} [options]
21076
+ * @returns {string}
21077
+ */
21078
+ function mailtoEncodeDomain (domain, options) {
21079
+ const set = options && options.unicodeSupport ? DTEXT_IRI : DTEXT;
21080
+
21081
+ // Keep the delimiters of a domain literal, encode its contents.
21082
+ if (domain.length > 1 && domain[0] === '[' && domain[domain.length - 1] === ']') {
21083
+ return '[' + encodeWithAllow(domain.slice(1, -1), set) + ']'
21084
+ }
21085
+
21086
+ return encodeWithAllow(domain, set)
21087
+ }
21088
+
21089
+ /**
21090
+ * @param {import('../types/index').URIComponent} component
21091
+ * @param {import('../types/index').Options} options
21092
+ * @returns {import('../types/index').URIComponent}
21093
+ */
21094
+ function mailtoParse (component, options) {
21095
+ const mailtoComponent = component;
21096
+
21097
+ // The handler sets `skipNormalize`, so path and query arrive raw. Everything
21098
+ // here goes through `decodeHex`, which subsumes the generic normalizers --
21099
+ // except that they also fold lone surrogates to U+FFFD, so do that here.
21100
+ let rawPath = mailtoComponent.path;
21101
+ let rawQuery = mailtoComponent.query;
21102
+ if (rawPath && HAS_SURROGATE.test(rawPath)) rawPath = replaceLoneSurrogates(rawPath);
21103
+ if (rawQuery && HAS_SURROGATE.test(rawQuery)) rawQuery = replaceLoneSurrogates(rawQuery);
21104
+
21105
+ const to = rawPath
21106
+ ? (rawPath.indexOf(',') === -1 ? [rawPath] : rawPath.split(','))
21107
+ : [];
21108
+ mailtoComponent.path = undefined;
21109
+
21110
+ if (rawQuery) {
21111
+ // Null prototype: header names come from untrusted input, so `headers[name]`
21112
+ // must not resolve to inherited members ("constructor", "toString", ...),
21113
+ // and "__proto__" has to land as a plain own property instead of hitting
21114
+ // Object.prototype's setter and being silently dropped. Allocated lazily so
21115
+ // the common subject/body-only URI does not pay for it.
21116
+ /** @type {Record<string,string>|null} */
21117
+ let headers = null;
21118
+ let start = 0;
21119
+ while (start <= rawQuery.length) {
21120
+ let end = rawQuery.indexOf('&', start);
21121
+ if (end === -1) end = rawQuery.length;
21122
+ const token = rawQuery.slice(start, end);
21123
+ start = end + 1;
21124
+
21125
+ const eqIdx = token.indexOf('=');
21126
+ if (eqIdx !== token.lastIndexOf('=')) {
21127
+ mailtoComponent.error = mailtoComponent.error || 'URI mailto has malformed header fields.';
21128
+ continue
21129
+ }
21130
+ const name = eqIdx === -1 ? token : token.slice(0, eqIdx);
21131
+ const value = eqIdx === -1 ? '' : token.slice(eqIdx + 1);
21132
+ if (name === 'to') {
21133
+ const addrs = value.split(',');
21134
+ for (let j = 0; j < addrs.length; j++) to.push(addrs[j]);
21135
+ continue
21136
+ }
21137
+ if (name === 'subject') {
21138
+ mailtoComponent.subject = decodeHex(value);
21139
+ continue
21140
+ }
21141
+ if (name === 'body') {
21142
+ mailtoComponent.body = decodeHex(value);
21143
+ continue
21144
+ }
21145
+ if (headers === null) headers = /** @type {Record<string,string>} */ (Object.create(null));
21146
+ headers[decodeHex(name)] = decodeHex(value);
21147
+ }
21148
+ if (headers !== null) mailtoComponent.headers = headers;
21149
+ }
21150
+
21151
+ mailtoComponent.query = undefined;
21152
+
21153
+ for (let i = 0; i < to.length; i++) {
21154
+ const rawAddr = to[i];
21155
+ const atIdx = rawAddr.lastIndexOf('@');
21156
+ if (atIdx < 0) {
21157
+ mailtoComponent.error = mailtoComponent.error || MAILTO_DOMAIN_ERROR;
21158
+ to[i] = decodeHex(rawAddr);
21159
+ continue
21160
+ }
21161
+ const local = decodeHex(rawAddr.slice(0, atIdx));
21162
+ const domain = mailtoNormalizeDomain(decodeHex(rawAddr.slice(atIdx + 1)), options, mailtoComponent);
21163
+ to[i] = local + '@' + domain;
21164
+ }
21165
+
21166
+ if (to.length) mailtoComponent.to = to;
21167
+
21168
+ return mailtoComponent
21169
+ }
21170
+
21171
+ /**
21172
+ * @param {import('../types/index').URIComponent} component
21173
+ * @param {import('../types/index').Options} options
21174
+ * @returns {import('../types/index').URIComponent}
21175
+ */
21176
+ function mailtoSerialize (component, options) {
21177
+ const mailtoComponent = component;
21178
+ const to = Array.isArray(mailtoComponent.to) ? mailtoComponent.to.slice() : [];
21179
+ if (to.length) {
21180
+ for (let i = 0; i < to.length; i++) {
21181
+ const addr = String(to[i]);
21182
+ const atIdx = addr.lastIndexOf('@');
21183
+ const rawLocal = atIdx >= 0 ? addr.slice(0, atIdx) : addr;
21184
+ const rawDomain = atIdx >= 0 ? addr.slice(atIdx + 1) : '';
21185
+ const local = encodeWithAllow(rawLocal, LOCAL_PART);
21186
+ const decodedDomain = decodeHex(rawDomain);
21187
+ // `component` is intentionally not passed: index.js's `serialize` writes
21188
+ // the error onto a private copy and returns only a string, so an error
21189
+ // recorded here would be unreachable. Safety comes from encoding instead.
21190
+ const normalizedDomain = mailtoNormalizeDomain(decodedDomain, options);
21191
+ to[i] = local + '@' + mailtoEncodeDomain(normalizedDomain, options);
21192
+ }
21193
+ // Every recipient is now fully encoded and only the joining commas are
21194
+ // literal, so bypass index.js's generic encoder, whose allow-set is
21195
+ // narrower than upstream's and would over-encode.
21196
+ mailtoComponent.path = to.join(',');
21197
+ options.skipEscape = true;
21198
+ } else {
21199
+ // No recipients: drop any caller-supplied path, matching upstream uri-js.
21200
+ // `to` is the only recipient source, and `mailtoParse` always moves the
21201
+ // path into it, so a mailto component never legitimately carries one.
21202
+ mailtoComponent.path = undefined;
21203
+ }
21204
+
21205
+ const headers = mailtoComponent.headers && typeof mailtoComponent.headers === 'object'
21206
+ ? Object.assign(Object.create(null), mailtoComponent.headers)
21207
+ : Object.create(null);
21208
+
21209
+ if (mailtoComponent.subject) headers.subject = mailtoComponent.subject;
21210
+ if (mailtoComponent.body) headers.body = mailtoComponent.body;
21211
+ mailtoComponent.headers = headers;
21212
+
21213
+ // `headers` has a null prototype and was filled via Object.assign, so every
21214
+ // enumerable key is an own property and no hasOwnProperty guard is needed.
21215
+ let query = '';
21216
+ let count = 0;
21217
+ for (const name in headers) {
21218
+ if (count++ !== 0) query += '&';
21219
+ query += encodeWithAllow(name, HFNAME) + '=' + encodeWithAllow(String(headers[name]), HFNAME);
21220
+ }
21221
+
21222
+ if (count !== 0) {
21223
+ mailtoComponent.query = query;
21224
+ } else {
21225
+ mailtoComponent.headers = undefined;
21226
+ }
21227
+
21228
+ return mailtoComponent
21229
+ }
20641
21230
 
20642
- const urnuuid = /** @type {SchemeHandler} */ ({
20643
- scheme: 'urn:uuid',
20644
- parse: urnuuidParse,
20645
- serialize: urnuuidSerialize,
20646
- skipNormalize: true
21231
+ const mailto = /** @type {SchemeHandler} */ ({
21232
+ scheme: 'mailto',
21233
+ parse: mailtoParse,
21234
+ serialize: mailtoSerialize,
21235
+ domainHost: false,
21236
+ unicodeSupport: true,
21237
+ // `mailtoParse` re-derives every component from the raw path/query via
21238
+ // `decodeHex`, which subsumes the generic normalizers, so running them first
21239
+ // is wasted work (~28% of parsing a URI with header fields).
21240
+ skipNormalize: true,
21241
+ // A recipient list has no dot segments to remove, and `removeDotSegments`
21242
+ // would rewrite a "./"-prefixed local part.
21243
+ absolutePath: true
20647
21244
  });
20648
21245
 
20649
21246
  const SCHEMES = /** @type {Record<SchemeName, SchemeHandler>} */ ({
@@ -20652,7 +21249,8 @@ function requireSchemes$1 () {
20652
21249
  ws,
20653
21250
  wss,
20654
21251
  urn,
20655
- 'urn:uuid': urnuuid
21252
+ 'urn:uuid': urnuuid,
21253
+ mailto
20656
21254
  });
20657
21255
 
20658
21256
  Object.setPrototypeOf(SCHEMES, null);
@@ -20685,9 +21283,24 @@ function requireFastUri$1 () {
20685
21283
  if (hasRequiredFastUri$1) return fastUri$1.exports;
20686
21284
  hasRequiredFastUri$1 = 1;
20687
21285
 
20688
- const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, normalizeQueryFragmentEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = requireUtils$1();
21286
+ const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = requireUtils$1();
20689
21287
  const { SCHEMES, getSchemeHandler } = requireSchemes$1();
20690
21288
 
21289
+ const VALID_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*$/u;
21290
+ const MALFORMED_SCHEME_ERROR = 'URI scheme is malformed.';
21291
+
21292
+ /**
21293
+ * @param {string} scheme
21294
+ * @returns {string}
21295
+ */
21296
+ function decodeValidScheme (scheme) {
21297
+ const decodedScheme = unescape(String(scheme));
21298
+ if (!VALID_SCHEME.test(decodedScheme)) {
21299
+ throw new TypeError(MALFORMED_SCHEME_ERROR)
21300
+ }
21301
+ return decodedScheme
21302
+ }
21303
+
20691
21304
  /**
20692
21305
  * @template {import('./types/index').URIComponent|string} T
20693
21306
  * @param {T} uri
@@ -20711,12 +21324,50 @@ function requireFastUri$1 () {
20711
21324
  */
20712
21325
  function resolve (baseURI, relativeURI, options) {
20713
21326
  const schemelessOptions = options ? Object.assign({ scheme: 'null' }, options) : { scheme: 'null' };
20714
- const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions);
20715
- const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions);
20716
- if (baseMalformed || relativeMalformed) {
21327
+ const {
21328
+ parsed: baseParsed,
21329
+ malformedAuthorityOrPort: baseMalformed,
21330
+ malformedPercentEncoding: baseMalformedPercentEncoding,
21331
+ malformedSchemeSpecific: baseMalformedSchemeSpecific,
21332
+ malformedHost: baseMalformedHost,
21333
+ malformedScheme: baseMalformedScheme
21334
+ } = parseWithStatus(baseURI, schemelessOptions);
21335
+ const {
21336
+ parsed: relativeParsed,
21337
+ malformedAuthorityOrPort: relativeMalformed,
21338
+ malformedPercentEncoding: relativeMalformedPercentEncoding,
21339
+ malformedSchemeSpecific: relativeMalformedSchemeSpecific,
21340
+ malformedHost: relativeMalformedHost,
21341
+ malformedScheme: relativeMalformedScheme
21342
+ } = parseWithStatus(relativeURI, schemelessOptions);
21343
+ if (
21344
+ baseMalformed ||
21345
+ relativeMalformed ||
21346
+ baseMalformedPercentEncoding ||
21347
+ relativeMalformedPercentEncoding ||
21348
+ baseMalformedSchemeSpecific ||
21349
+ relativeMalformedSchemeSpecific ||
21350
+ baseMalformedHost ||
21351
+ relativeMalformedHost ||
21352
+ baseMalformedScheme ||
21353
+ relativeMalformedScheme
21354
+ ) {
20717
21355
  throw new Error(baseParsed.error || relativeParsed.error || 'URI is malformed.')
20718
21356
  }
20719
21357
  const resolved = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true);
21358
+ const resolvedSchemeHandler = getSchemeHandler((options && options.scheme) || resolved.scheme);
21359
+ const resolvedHost = resolved.host;
21360
+ const resolvedHostIsIP = resolvedHost !== undefined && resolvedHost !== '' &&
21361
+ (isIPv4(resolvedHost) || normalizeIPv6(resolvedHost).isIPV6);
21362
+ canonicalizeHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP);
21363
+ // Percent escapes in an ASCII reg-name are encoded data. The WHATWG hostname
21364
+ // parser can reject them even though fast-uri preserves them safely as RFC
21365
+ // 3986 data. A raw non-ASCII host must still fail closed if conversion fails.
21366
+ const encodedASCIIHost = resolvedHost && resolvedHost.indexOf('%') !== -1 &&
21367
+ !/\P{ASCII}/u.test(resolvedHost);
21368
+ if (resolved.error && !encodedASCIIHost) {
21369
+ throw new Error(resolved.error)
21370
+ }
20720
21371
  schemelessOptions.skipEscape = true;
20721
21372
  return serialize(resolved, schemelessOptions)
20722
21373
  }
@@ -20799,7 +21450,7 @@ function requireFastUri$1 () {
20799
21450
  const normalizedA = normalizeComparableURI(uriA, options);
20800
21451
  const normalizedB = normalizeComparableURI(uriB, options);
20801
21452
 
20802
- return normalizedA !== undefined && normalizedB !== undefined && normalizedA.toLowerCase() === normalizedB.toLowerCase()
21453
+ return normalizedA !== undefined && normalizedB !== undefined && normalizedA === normalizedB
20803
21454
  }
20804
21455
 
20805
21456
  /**
@@ -20822,30 +21473,39 @@ function requireFastUri$1 () {
20822
21473
  reference: cmpts.reference,
20823
21474
  resourceName: cmpts.resourceName,
20824
21475
  secure: cmpts.secure,
21476
+ to: cmpts.to,
21477
+ subject: cmpts.subject,
21478
+ body: cmpts.body,
21479
+ headers: cmpts.headers,
20825
21480
  error: ''
20826
21481
  };
20827
21482
  const options = Object.assign({}, opts);
20828
21483
  const uriTokens = [];
20829
21484
 
21485
+ if (component.scheme) {
21486
+ component.scheme = decodeValidScheme(component.scheme);
21487
+ }
21488
+
20830
21489
  // find scheme handler
20831
21490
  const schemeHandler = getSchemeHandler(options.scheme || component.scheme);
20832
21491
 
20833
21492
  // perform scheme specific serialization
20834
21493
  if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
20835
21494
 
21495
+ const hasAuthority = component.userinfo !== undefined || component.host !== undefined || component.port !== undefined;
21496
+ const pathNoScheme = !options.skipEscape && component.scheme === undefined && !hasAuthority;
21497
+
20836
21498
  if (component.path !== undefined) {
20837
21499
  if (!options.skipEscape) {
20838
- component.path = escapePreservingEscapes(component.path);
20839
-
20840
- if (component.scheme !== undefined) {
20841
- component.path = component.path.split('%3A').join(':');
20842
- }
21500
+ component.path = serializePathEncoding(component.path, pathNoScheme);
20843
21501
  } else {
20844
21502
  component.path = normalizePercentEncoding(component.path);
20845
21503
  }
20846
21504
  }
20847
21505
 
20848
21506
  if (options.reference !== 'suffix' && component.scheme) {
21507
+ // Scheme handlers may replace the scheme during serialization.
21508
+ component.scheme = decodeValidScheme(component.scheme);
20849
21509
  uriTokens.push(component.scheme, ':');
20850
21510
  }
20851
21511
 
@@ -20868,6 +21528,13 @@ function requireFastUri$1 () {
20868
21528
  s = removeDotSegments(s);
20869
21529
  }
20870
21530
 
21531
+ // Dot-segment removal can expose a colon that was not originally in the
21532
+ // first segment (for example, "./a:b"). Reapply path-noscheme encoding so
21533
+ // the serialized relative reference cannot be reparsed as a URI scheme.
21534
+ if (pathNoScheme) {
21535
+ s = serializePathEncoding(s, true);
21536
+ }
21537
+
20871
21538
  if (
20872
21539
  authority === undefined &&
20873
21540
  s[0] === '/' &&
@@ -20881,11 +21548,11 @@ function requireFastUri$1 () {
20881
21548
  }
20882
21549
 
20883
21550
  if (component.query !== undefined) {
20884
- uriTokens.push('?', component.query);
21551
+ uriTokens.push('?', encodeQuery(component.query));
20885
21552
  }
20886
21553
 
20887
21554
  if (component.fragment !== undefined) {
20888
- uriTokens.push('#', component.fragment);
21555
+ uriTokens.push('#', encodeFragment(component.fragment));
20889
21556
  }
20890
21557
  return uriTokens.join('')
20891
21558
  }
@@ -20922,10 +21589,93 @@ function requireFastUri$1 () {
20922
21589
  return undefined
20923
21590
  }
20924
21591
 
21592
+ /**
21593
+ * Checks percent syntax without decoding the represented octets. RFC 3986
21594
+ * percent-encoding is byte-oriented, so sequences such as `%FF` are valid even
21595
+ * though they are not independently valid UTF-8.
21596
+ *
21597
+ * @param {string|undefined} component
21598
+ * @returns {boolean}
21599
+ */
21600
+ function hasMalformedPercentEncoding (component) {
21601
+ if (component === undefined) return false
21602
+
21603
+ let percent = component.indexOf('%');
21604
+ while (percent !== -1) {
21605
+ if (percent + 2 >= component.length || !/^[\da-f]{2}$/iu.test(component.slice(percent + 1, percent + 3))) {
21606
+ return true
21607
+ }
21608
+ percent = component.indexOf('%', percent + 3);
21609
+ }
21610
+
21611
+ return false
21612
+ }
21613
+
21614
+ /**
21615
+ * Whether the host is a bracketed IP literal (RFC 3986 `IP-literal`).
21616
+ * An unterminated `[` is not a literal, so it must still be validated as a
21617
+ * reg-name instead of being waved through as an IP.
21618
+ *
21619
+ * @param {string} host
21620
+ * @returns {boolean}
21621
+ */
21622
+ function isIPLiteral (host) {
21623
+ return host[0] === '[' && host[host.length - 1] === ']'
21624
+ }
21625
+
21626
+ /**
21627
+ * @param {RegExpMatchArray} matches
21628
+ * @returns {boolean}
21629
+ */
21630
+ function hasMalformedComponentPercentEncoding (matches) {
21631
+ // Bracketed IP literals use a raw "%" as the zone separator for historical
21632
+ // compatibility. Their parsing is intentionally left to normalizeIPv6.
21633
+ const host = matches[4];
21634
+ return hasMalformedPercentEncoding(matches[3]) ||
21635
+ (host !== undefined && !isIPLiteral(host) && hasMalformedPercentEncoding(host)) ||
21636
+ hasMalformedPercentEncoding(matches[6]) ||
21637
+ hasMalformedPercentEncoding(matches[7]) ||
21638
+ hasMalformedPercentEncoding(matches[8])
21639
+ }
21640
+
21641
+ /**
21642
+ * @param {import('./types/index').URIComponent} parsed
21643
+ * @param {import('./types/index').Options} options
21644
+ * @param {{ domainHost?: boolean, unicodeSupport?: boolean }|undefined} schemeHandler
21645
+ * @param {boolean} isIP
21646
+ * @returns {boolean} whether host conversion failed
21647
+ */
21648
+ function canonicalizeHost (parsed, options, schemeHandler, isIP) {
21649
+ const bracketedIPLiteral =
21650
+ parsed.host &&
21651
+ parsed.host[0] === '[' &&
21652
+ parsed.host[parsed.host.length - 1] === ']';
21653
+
21654
+ if (
21655
+ !options.unicodeSupport &&
21656
+ (!schemeHandler || !schemeHandler.unicodeSupport) &&
21657
+ parsed.host &&
21658
+ !isIPLiteral(parsed.host) &&
21659
+ (options.domainHost || (schemeHandler && schemeHandler.domainHost)) &&
21660
+ isIP === false &&
21661
+ !bracketedIPLiteral &&
21662
+ nonSimpleDomain(parsed.host)
21663
+ ) {
21664
+ try {
21665
+ parsed.host = new URL('http://' + parsed.host).hostname;
21666
+ } catch (e) {
21667
+ parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e;
21668
+ return true
21669
+ }
21670
+ }
21671
+
21672
+ return false
21673
+ }
21674
+
20925
21675
  /**
20926
21676
  * @param {string} uri
20927
21677
  * @param {import('./types/index').Options} [opts]
20928
- * @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean }}
21678
+ * @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
20929
21679
  */
20930
21680
  function parseWithStatus (uri, opts) {
20931
21681
  const options = Object.assign({}, opts);
@@ -20941,6 +21691,11 @@ function requireFastUri$1 () {
20941
21691
  };
20942
21692
 
20943
21693
  let malformedAuthorityOrPort = false;
21694
+ let malformedPercentEncoding = false;
21695
+ let malformedSchemeSpecific = false;
21696
+ let malformedHost = false;
21697
+ let malformedIPLiteral = false;
21698
+ let malformedScheme = false;
20944
21699
 
20945
21700
  let isIP = false;
20946
21701
  if (options.reference === 'suffix') {
@@ -20990,7 +21745,7 @@ function requireFastUri$1 () {
20990
21745
 
20991
21746
  if (matches) {
20992
21747
  // store each component
20993
- parsed.scheme = matches[1] === undefined ? undefined : matches[1].toLowerCase();
21748
+ parsed.scheme = matches[1];
20994
21749
  parsed.userinfo = matches[3];
20995
21750
  parsed.host = matches[4];
20996
21751
  parsed.port = parseInt(matches[5], 10);
@@ -20998,6 +21753,21 @@ function requireFastUri$1 () {
20998
21753
  parsed.query = matches[7];
20999
21754
  parsed.fragment = matches[8];
21000
21755
 
21756
+ if (parsed.scheme !== undefined) {
21757
+ const decodedScheme = unescape(parsed.scheme);
21758
+ if (VALID_SCHEME.test(decodedScheme)) {
21759
+ parsed.scheme = decodedScheme.toLowerCase();
21760
+ } else {
21761
+ parsed.error = parsed.error || MALFORMED_SCHEME_ERROR;
21762
+ malformedScheme = true;
21763
+ }
21764
+ }
21765
+
21766
+ malformedPercentEncoding = hasMalformedComponentPercentEncoding(matches);
21767
+ if (malformedPercentEncoding) {
21768
+ parsed.error = parsed.error || 'URI contains malformed percent-encoding.';
21769
+ }
21770
+
21001
21771
  // fix port number
21002
21772
  if (isNaN(parsed.port)) {
21003
21773
  parsed.port = matches[5];
@@ -21012,9 +21782,17 @@ function requireFastUri$1 () {
21012
21782
  if (parsed.host) {
21013
21783
  const ipv4result = isIPv4(parsed.host);
21014
21784
  if (ipv4result === false) {
21785
+ const bracketedIPLiteral = isIPLiteral(parsed.host);
21786
+ const hasIPLiteralBracket = parsed.host.indexOf('[') !== -1 || parsed.host.indexOf(']') !== -1;
21015
21787
  const ipv6result = normalizeIPv6(parsed.host);
21016
- parsed.host = ipv6result.host.toLowerCase();
21017
- isIP = ipv6result.isIPV6;
21788
+ isIP = ipv6result.isIPV6 || ipv6result.isIPVFuture === true;
21789
+ malformedIPLiteral = hasIPLiteralBracket && (!bracketedIPLiteral || ipv6result.error === true);
21790
+ parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase();
21791
+
21792
+ if (malformedIPLiteral) {
21793
+ parsed.error = parsed.error || 'URI host is malformed.';
21794
+ malformedAuthorityOrPort = true;
21795
+ }
21018
21796
  } else {
21019
21797
  isIP = true;
21020
21798
  }
@@ -21037,27 +21815,16 @@ function requireFastUri$1 () {
21037
21815
  // find scheme handler
21038
21816
  const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme);
21039
21817
 
21040
- // check if scheme can't handle IRIs
21041
- if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport)) {
21042
- // if host component is a domain name
21043
- if (parsed.host && (options.domainHost || (schemeHandler && schemeHandler.domainHost)) && isIP === false && nonSimpleDomain(parsed.host)) {
21044
- // convert Unicode IDN -> ASCII IDN
21045
- try {
21046
- parsed.host = new URL('http://' + parsed.host).hostname;
21047
- } catch (e) {
21048
- parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e;
21049
- }
21050
- }
21051
- // convert IRI -> URI
21818
+ // convert Unicode IDN -> ASCII IDN when the effective scheme uses domain hosts
21819
+ if (!malformedIPLiteral) {
21820
+ malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP);
21052
21821
  }
21053
21822
 
21054
21823
  if (!schemeHandler || (schemeHandler && !schemeHandler.skipNormalize)) {
21055
21824
  if (uri.indexOf('%') !== -1) {
21056
- if (parsed.scheme !== undefined) {
21057
- parsed.scheme = unescape(parsed.scheme);
21058
- }
21059
- if (parsed.host !== undefined) {
21060
- parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
21825
+ if (parsed.host !== undefined && !malformedIPLiteral) {
21826
+ const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
21827
+ parsed.host = reescapeHostDelimiters(host, isIP);
21061
21828
  }
21062
21829
  }
21063
21830
  if (parsed.path) {
@@ -21074,11 +21841,14 @@ function requireFastUri$1 () {
21074
21841
  // perform scheme specific parsing
21075
21842
  if (schemeHandler && schemeHandler.parse) {
21076
21843
  schemeHandler.parse(parsed, options);
21844
+ if (schemeHandler === SCHEMES.urn && parsed.nid === undefined) {
21845
+ malformedSchemeSpecific = true;
21846
+ }
21077
21847
  }
21078
21848
  } else {
21079
21849
  parsed.error = parsed.error || 'URI can not be parsed.';
21080
21850
  }
21081
- return { parsed, malformedAuthorityOrPort }
21851
+ return { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme }
21082
21852
  }
21083
21853
 
21084
21854
  /**
@@ -21102,13 +21872,17 @@ function requireFastUri$1 () {
21102
21872
  /**
21103
21873
  * @param {string} uri
21104
21874
  * @param {import('./types/index').Options} [opts]
21105
- * @returns {{ normalized: string, malformedAuthorityOrPort: boolean }}
21875
+ * @returns {{ normalized: string, malformedAuthorityOrPort: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
21106
21876
  */
21107
21877
  function normalizeStringWithStatus (uri, opts) {
21108
- const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
21878
+ const { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = parseWithStatus(uri, opts);
21109
21879
  return {
21110
- normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
21111
- malformedAuthorityOrPort
21880
+ normalized: malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? uri : serialize(parsed, opts),
21881
+ malformedAuthorityOrPort,
21882
+ malformedPercentEncoding,
21883
+ malformedSchemeSpecific,
21884
+ malformedHost,
21885
+ malformedScheme
21112
21886
  }
21113
21887
  }
21114
21888
 
@@ -21118,14 +21892,18 @@ function requireFastUri$1 () {
21118
21892
  * @returns {string|undefined}
21119
21893
  */
21120
21894
  function normalizeComparableURI (uri, opts) {
21121
- if (typeof uri === 'string') {
21122
- const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts);
21123
- return malformedAuthorityOrPort ? undefined : normalized
21895
+ if (typeof uri !== 'string' && typeof uri !== 'object') {
21896
+ return undefined
21124
21897
  }
21125
21898
 
21126
- if (typeof uri === 'object') {
21127
- return serialize(uri, opts)
21899
+ let value;
21900
+ try {
21901
+ value = typeof uri === 'string' ? uri : serialize(uri, opts);
21902
+ } catch {
21903
+ return undefined
21128
21904
  }
21905
+ const { normalized, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = normalizeStringWithStatus(value, opts);
21906
+ return malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? undefined : normalized
21129
21907
  }
21130
21908
 
21131
21909
  const fastUri = {
@@ -35097,6 +35875,9 @@ function requireUtils () {
35097
35875
  /** @type {(value: string) => boolean} */
35098
35876
  const isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
35099
35877
 
35878
+ /** @type {(value: string) => boolean} */
35879
+ const isPort = RegExp.prototype.test.bind(/^\d*$/u);
35880
+
35100
35881
  /** @type {(value: string) => boolean} */
35101
35882
  const isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
35102
35883
 
@@ -35104,7 +35885,36 @@ function requireUtils () {
35104
35885
  const isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
35105
35886
 
35106
35887
  /** @type {(value: string) => boolean} */
35107
- const isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
35888
+ const isPathCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/]$/u);
35889
+
35890
+ /** @type {(value: string) => boolean} */
35891
+ const isQueryFragmentCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/?]$/u);
35892
+
35893
+ /** @type {(value: string) => boolean} */
35894
+ const isUserinfoCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:]$/u);
35895
+
35896
+ const BYTE_HEX = new Array(256);
35897
+ {
35898
+ const HEX_DIGITS = '0123456789ABCDEF';
35899
+ for (let i = 0; i < 256; i++) {
35900
+ BYTE_HEX[i] = '%' + HEX_DIGITS[i >> 4] + HEX_DIGITS[i & 0xF];
35901
+ }
35902
+ }
35903
+ function percentEncodeNonAscii (cp) {
35904
+ if (cp < 0x800) {
35905
+ return BYTE_HEX[0xC0 | (cp >> 6)] +
35906
+ BYTE_HEX[0x80 | (cp & 0x3F)]
35907
+ }
35908
+ if (cp < 0x10000) {
35909
+ return BYTE_HEX[0xE0 | (cp >> 12)] +
35910
+ BYTE_HEX[0x80 | ((cp >> 6) & 0x3F)] +
35911
+ BYTE_HEX[0x80 | (cp & 0x3F)]
35912
+ }
35913
+ return BYTE_HEX[0xF0 | (cp >> 18)] +
35914
+ BYTE_HEX[0x80 | ((cp >> 12) & 0x3F)] +
35915
+ BYTE_HEX[0x80 | ((cp >> 6) & 0x3F)] +
35916
+ BYTE_HEX[0x80 | (cp & 0x3F)]
35917
+ }
35108
35918
 
35109
35919
  /**
35110
35920
  * @param {Array<string>} input
@@ -35137,12 +35947,14 @@ function requireUtils () {
35137
35947
  return acc
35138
35948
  }
35139
35949
 
35140
- /**
35141
- * @typedef {Object} GetIPV6Result
35142
- * @property {boolean} error - Indicates if there was an error parsing the IPv6 address.
35143
- * @property {string} address - The parsed IPv6 address.
35144
- * @property {string} [zone] - The zone identifier, if present.
35145
- */
35950
+ /** @type {(value: string) => boolean} */
35951
+ const isHextet = RegExp.prototype.test.bind(/^[\dA-Fa-f]{1,4}$/);
35952
+
35953
+ /** @type {(value: string) => boolean} */
35954
+ const isIPvFuture = RegExp.prototype.test.bind(/^[vV][\dA-Fa-f]+\.[A-Za-z\d\-._~!$&'()*+,;=:]+$/);
35955
+
35956
+ /** @type {(value: string) => boolean} */
35957
+ const isZoneCharacter = RegExp.prototype.test.bind(/^[A-Za-z\d\-._~]$/);
35146
35958
 
35147
35959
  /**
35148
35960
  * @param {string} value
@@ -35151,88 +35963,104 @@ function requireUtils () {
35151
35963
  const nonSimpleDomain = RegExp.prototype.test.bind(/[^!"$&'()*+,\-.;=_`a-z{}~]/u);
35152
35964
 
35153
35965
  /**
35154
- * @param {Array<string>} buffer
35966
+ * @param {string} zone
35155
35967
  * @returns {boolean}
35156
35968
  */
35157
- function consumeIsZone (buffer) {
35158
- buffer.length = 0;
35969
+ function isZoneIdentifier (zone) {
35970
+ if (zone.length === 0) return false
35971
+
35972
+ for (let i = 0; i < zone.length; i++) {
35973
+ if (isZoneCharacter(zone[i])) continue
35974
+ if (zone[i] === '%' && i + 2 < zone.length && isHexPair(zone.slice(i + 1, i + 3))) {
35975
+ i += 2;
35976
+ continue
35977
+ }
35978
+ return false
35979
+ }
35980
+
35159
35981
  return true
35160
35982
  }
35161
35983
 
35162
35984
  /**
35163
- * @param {Array<string>} buffer
35164
- * @param {Array<string>} address
35165
- * @param {GetIPV6Result} output
35166
- * @returns {boolean}
35985
+ * Compresses the longest run of zero hextets to "::" per RFC 5952. A run of a
35986
+ * single zero hextet is left uncompressed. On ties the leftmost run wins.
35987
+ *
35988
+ * @param {string[]} hextets
35989
+ * @returns {string}
35167
35990
  */
35168
- function consumeHextets (buffer, address, output) {
35169
- if (buffer.length) {
35170
- const hex = stringArrayToHexStripped(buffer);
35171
- if (hex !== '') {
35172
- address.push(hex);
35991
+ function compressIPv6ZeroRun (hextets) {
35992
+ let bestStart = -1;
35993
+ let bestLength = 0;
35994
+ let runStart = -1;
35995
+ let runLength = 0;
35996
+ for (let i = 0; i < hextets.length; i++) {
35997
+ if (hextets[i] === '0') {
35998
+ if (runStart === -1) runStart = i;
35999
+ runLength++;
36000
+ if (runLength > bestLength) {
36001
+ bestLength = runLength;
36002
+ bestStart = runStart;
36003
+ }
35173
36004
  } else {
35174
- output.error = true;
35175
- return false
36005
+ runStart = -1;
36006
+ runLength = 0;
35176
36007
  }
35177
- buffer.length = 0;
35178
36008
  }
35179
- return true
36009
+
36010
+ if (bestLength < 2) return hextets.join(':')
36011
+
36012
+ const head = hextets.slice(0, bestStart).join(':');
36013
+ const tail = hextets.slice(bestStart + bestLength).join(':');
36014
+ return head + '::' + tail
35180
36015
  }
35181
36016
 
35182
36017
  /**
36018
+ * Validates an IPv6 address against the alternatives in RFC 3986 section
36019
+ * 3.2.2 and returns the same address with leading hextet zeroes removed.
36020
+ * An embedded IPv4 address counts as two hextets and is only valid at the end.
36021
+ *
35183
36022
  * @param {string} input
35184
- * @returns {GetIPV6Result}
36023
+ * @returns {string|undefined}
35185
36024
  */
35186
- function getIPV6 (input) {
35187
- let tokenCount = 0;
35188
- const output = { error: false, address: '', zone: '' };
35189
- /** @type {Array<string>} */
35190
- const address = [];
35191
- /** @type {Array<string>} */
35192
- const buffer = [];
35193
- let endipv6Encountered = false;
35194
- let endIpv6 = false;
35195
-
35196
- let consume = consumeHextets;
35197
-
35198
- for (let i = 0; i < input.length; i++) {
35199
- const cursor = input[i];
35200
- if (cursor === '[' || cursor === ']') { continue }
35201
- if (cursor === ':') {
35202
- if (endipv6Encountered === true) {
35203
- endIpv6 = true;
35204
- }
35205
- if (!consume(buffer, address, output)) { break }
35206
- if (++tokenCount > 7) {
35207
- // not valid
35208
- output.error = true;
35209
- break
35210
- }
35211
- if (i > 0 && input[i - 1] === ':') {
35212
- endipv6Encountered = true;
35213
- }
35214
- address.push(':');
35215
- continue
35216
- } else if (cursor === '%') {
35217
- if (!consume(buffer, address, output)) { break }
35218
- // switch to zone detection
35219
- consume = consumeIsZone;
35220
- } else {
35221
- buffer.push(cursor);
36025
+ function normalizeIPv6Address (input) {
36026
+ const compression = input.indexOf('::');
36027
+ if (compression !== -1 && input.indexOf('::', compression + 1) !== -1) return undefined
36028
+
36029
+ const left = compression === -1 ? input.split(':') : input.slice(0, compression).split(':');
36030
+ const right = compression === -1 ? [] : input.slice(compression + 2).split(':');
36031
+ if (compression !== -1) {
36032
+ if (left.length === 1 && left[0] === '') left.length = 0;
36033
+ if (right.length === 1 && right[0] === '') right.length = 0;
36034
+ }
36035
+
36036
+ const parts = left.concat(right);
36037
+ let hextetCount = 0;
36038
+ for (let i = 0; i < parts.length; i++) {
36039
+ const part = parts[i];
36040
+ if (part === '') return undefined
36041
+
36042
+ if (part.indexOf('.') !== -1) {
36043
+ if (i !== parts.length - 1 || (compression !== -1 && right.length === 0) || !isIPv4(part)) return undefined
36044
+ hextetCount += 2;
35222
36045
  continue
35223
36046
  }
36047
+
36048
+ if (!isHextet(part)) return undefined
36049
+ parts[i] = parseInt(part, 16).toString(16);
36050
+ hextetCount++;
35224
36051
  }
35225
- if (buffer.length) {
35226
- if (consume === consumeIsZone) {
35227
- output.zone = buffer.join('');
35228
- } else if (endIpv6) {
35229
- address.push(buffer.join(''));
35230
- } else {
35231
- address.push(stringArrayToHexStripped(buffer));
35232
- }
36052
+
36053
+ if (compression === -1) {
36054
+ if (hextetCount !== 8) return undefined
36055
+ return compressIPv6ZeroRun(parts)
35233
36056
  }
35234
- output.address = address.join('');
35235
- return output
36057
+ if (hextetCount >= 8) return undefined
36058
+
36059
+ // expand "::" then re-compress the longest run for a canonical result
36060
+ const expanded = parts.slice(0, left.length);
36061
+ for (let i = hextetCount; i < 8; i++) expanded.push('0');
36062
+ for (let i = left.length; i < parts.length; i++) expanded.push(parts[i]);
36063
+ return compressIPv6ZeroRun(expanded)
35236
36064
  }
35237
36065
 
35238
36066
  /**
@@ -35240,26 +36068,49 @@ function requireUtils () {
35240
36068
  * @property {string} host - The normalized host.
35241
36069
  * @property {string} [escapedHost] - The escaped host.
35242
36070
  * @property {boolean} isIPV6 - Indicates if the host is an IPv6 address.
36071
+ * @property {boolean} [isIPVFuture] - Indicates if the host is an IPvFuture literal.
36072
+ * @property {boolean} [error] - Indicates if a bracketed IP literal is malformed.
35243
36073
  */
35244
36074
 
35245
36075
  /**
36076
+ * Validates and normalizes a bracketed IP literal. Raw zone separators remain
36077
+ * accepted for backwards compatibility, while encoded separators and zone
36078
+ * contents follow RFC 6874.
36079
+ *
35246
36080
  * @param {string} host
35247
36081
  * @returns {NormalizeIPv6Result}
35248
36082
  */
35249
36083
  function normalizeIPv6 (host) {
35250
- if (findToken(host, ':') < 2) { return { host, isIPV6: false } }
35251
- const ipv6 = getIPV6(host);
36084
+ const bracketed = host[0] === '[' && host[host.length - 1] === ']';
36085
+ const hasBracket = host[0] === '[' || host[host.length - 1] === ']';
36086
+ if (hasBracket && !bracketed) return { host, isIPV6: false, error: true }
35252
36087
 
35253
- if (!ipv6.error) {
35254
- let newHost = ipv6.address;
35255
- let escapedHost = ipv6.address;
35256
- if (ipv6.zone) {
35257
- newHost += '%' + ipv6.zone;
35258
- escapedHost += '%25' + ipv6.zone;
35259
- }
35260
- return { host: newHost, isIPV6: true, escapedHost }
35261
- } else {
35262
- return { host, isIPV6: false }
36088
+ let input = bracketed ? host.slice(1, -1) : host;
36089
+ if (bracketed && isIPvFuture(input)) {
36090
+ input = input.toLowerCase();
36091
+ return { host: `[${input}]`, escapedHost: input, isIPV6: false, isIPVFuture: true }
36092
+ }
36093
+
36094
+ if (findToken(input, ':') < 2) {
36095
+ return { host, isIPV6: false, error: bracketed }
36096
+ }
36097
+
36098
+ let zoneIdentifier = '';
36099
+ const zoneSeparator = input.indexOf('%');
36100
+ if (zoneSeparator !== -1) {
36101
+ const separatorLength = input.slice(zoneSeparator, zoneSeparator + 3).toLowerCase() === '%25' ? 3 : 1;
36102
+ zoneIdentifier = input.slice(zoneSeparator + separatorLength);
36103
+ if (!isZoneIdentifier(zoneIdentifier)) return { host, isIPV6: false, error: true }
36104
+ input = input.slice(0, zoneSeparator);
36105
+ }
36106
+
36107
+ const address = normalizeIPv6Address(input);
36108
+ if (address === undefined) return { host, isIPV6: false, error: true }
36109
+
36110
+ return {
36111
+ host: address + (zoneIdentifier ? '%' + zoneIdentifier : ''),
36112
+ escapedHost: address + (zoneIdentifier ? '%25' + zoneIdentifier : ''),
36113
+ isIPV6: true
35263
36114
  }
35264
36115
  }
35265
36116
 
@@ -35350,97 +36201,298 @@ function requireUtils () {
35350
36201
  }
35351
36202
  }
35352
36203
 
35353
- // Rule 2E: Move normal path segment to output
35354
- if ((nextSlash = input.indexOf('/', 1)) === -1) {
35355
- output.push(input);
35356
- break
36204
+ // Rule 2E: Move normal path segment to output
36205
+ if ((nextSlash = input.indexOf('/', 1)) === -1) {
36206
+ output.push(input);
36207
+ break
36208
+ } else {
36209
+ output.push(input.slice(0, nextSlash));
36210
+ input = input.slice(nextSlash);
36211
+ }
36212
+ }
36213
+
36214
+ return output.join('')
36215
+ }
36216
+
36217
+ /**
36218
+ * Re-escape RFC 3986 gen-delims that must not appear literally in the host.
36219
+ * After the URI regex parses, these characters cannot be literal in the host
36220
+ * field, so any that appear after decoding came from percent-encoding and
36221
+ * must be restored to prevent authority structure changes.
36222
+ *
36223
+ * @param {string} host
36224
+ * @param {boolean} isIP - true for IPv4/IPv6 hosts (skip colon re-escaping)
36225
+ * @returns {string}
36226
+ */
36227
+ const HOST_DELIMS = { '@': '%40', '/': '%2F', '?': '%3F', '#': '%23', ':': '%3A' };
36228
+ const HOST_DELIM_RE = /[@/?#:]/g;
36229
+ const HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
36230
+
36231
+ function reescapeHostDelimiters (host, isIP) {
36232
+ const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
36233
+ re.lastIndex = 0;
36234
+ return host.replace(re, (ch) => HOST_DELIMS[ch])
36235
+ }
36236
+
36237
+ /**
36238
+ * Normalizes percent escapes and optionally decodes only unreserved ASCII bytes.
36239
+ * Reserved delimiters such as `%2F` stay escaped; `%2E` is unreserved.
36240
+ *
36241
+ * @param {string} input
36242
+ * @param {boolean} [decodeUnreserved=false]
36243
+ * @returns {string}
36244
+ */
36245
+ function normalizePercentEncoding (input, decodeUnreserved = false) {
36246
+ if (input.indexOf('%') === -1) {
36247
+ return input
36248
+ }
36249
+
36250
+ let output = '';
36251
+
36252
+ for (let i = 0; i < input.length; i++) {
36253
+ if (input[i] === '%' && i + 2 < input.length) {
36254
+ const hex = input.slice(i + 1, i + 3);
36255
+ if (isHexPair(hex)) {
36256
+ const normalizedHex = hex.toUpperCase();
36257
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
36258
+
36259
+ if (decodeUnreserved && isUnreserved(decoded)) {
36260
+ output += decoded;
36261
+ } else {
36262
+ output += '%' + normalizedHex;
36263
+ }
36264
+
36265
+ i += 2;
36266
+ continue
36267
+ }
36268
+ }
36269
+
36270
+ output += input[i];
36271
+ }
36272
+
36273
+ return output
36274
+ }
36275
+
36276
+ /**
36277
+ * Normalizes path data without turning reserved escapes into live path syntax.
36278
+ * Valid escapes are uppercased, raw unsafe characters are escaped, and only
36279
+ * unreserved bytes that are not `.` are decoded.
36280
+ *
36281
+ * @param {string} input
36282
+ * @returns {string}
36283
+ */
36284
+ function normalizePathEncoding (input) {
36285
+ let output = '';
36286
+
36287
+ for (let i = 0; i < input.length; i++) {
36288
+ const ch = input[i];
36289
+ if (ch === '%' && i + 2 < input.length) {
36290
+ const hex = input.slice(i + 1, i + 3);
36291
+ if (isHexPair(hex)) {
36292
+ const normalizedHex = hex.toUpperCase();
36293
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
36294
+
36295
+ if (decoded !== '.' && isUnreserved(decoded)) {
36296
+ output += decoded;
36297
+ } else {
36298
+ output += '%' + normalizedHex;
36299
+ }
36300
+
36301
+ i += 2;
36302
+ continue
36303
+ }
36304
+ }
36305
+
36306
+ if (isPathCharacter(ch)) {
36307
+ output += ch;
35357
36308
  } else {
35358
- output.push(input.slice(0, nextSlash));
35359
- input = input.slice(nextSlash);
36309
+ const code = input.charCodeAt(i);
36310
+ if (code < 0x80) {
36311
+ output += isEscapeSafe(code) ? ch : BYTE_HEX[code];
36312
+ } else if (code < 0xD800 || code > 0xDFFF) {
36313
+ output += percentEncodeNonAscii(code);
36314
+ } else if (code <= 0xDBFF && i + 1 < input.length) {
36315
+ const low = input.charCodeAt(i + 1);
36316
+ if (low >= 0xDC00 && low <= 0xDFFF) {
36317
+ output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00));
36318
+ i++;
36319
+ } else {
36320
+ output += percentEncodeNonAscii(0xFFFD);
36321
+ }
36322
+ } else {
36323
+ output += percentEncodeNonAscii(0xFFFD);
36324
+ }
35360
36325
  }
35361
36326
  }
35362
36327
 
35363
- return output.join('')
36328
+ return output
35364
36329
  }
35365
36330
 
35366
36331
  /**
35367
- * Re-escape RFC 3986 gen-delims that must not appear literally in the host.
35368
- * After the URI regex parses, these characters cannot be literal in the host
35369
- * field, so any that appear after decoding came from percent-encoding and
35370
- * must be restored to prevent authority structure changes.
36332
+ * Serializes a path without rewriting reserved data. Raw RFC 3986 path
36333
+ * characters remain literal, valid escapes are preserved and uppercased, and
36334
+ * everything else is UTF-8 percent-encoded. In a path-noscheme, a colon in the
36335
+ * first segment must be escaped so the result cannot be parsed as a scheme.
35371
36336
  *
35372
- * @param {string} host
35373
- * @param {boolean} isIP - true for IPv4/IPv6 hosts (skip colon re-escaping)
36337
+ * @param {string} input
36338
+ * @param {boolean} [pathNoScheme=false]
35374
36339
  * @returns {string}
35375
36340
  */
35376
- const HOST_DELIMS = { '@': '%40', '/': '%2F', '?': '%3F', '#': '%23', ':': '%3A' };
35377
- const HOST_DELIM_RE = /[@/?#:]/g;
35378
- const HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
36341
+ function serializePathEncoding (input, pathNoScheme = false) {
36342
+ let output = '';
36343
+ let firstSegment = pathNoScheme && input[0] !== '/';
35379
36344
 
35380
- function reescapeHostDelimiters (host, isIP) {
35381
- const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
35382
- re.lastIndex = 0;
35383
- return host.replace(re, (ch) => HOST_DELIMS[ch])
36345
+ for (let i = 0; i < input.length; i++) {
36346
+ const ch = input[i];
36347
+ if (ch === '%' && i + 2 < input.length) {
36348
+ const hex = input.slice(i + 1, i + 3);
36349
+ if (isHexPair(hex)) {
36350
+ output += '%' + hex.toUpperCase();
36351
+ i += 2;
36352
+ continue
36353
+ }
36354
+ }
36355
+
36356
+ if (ch === '/') {
36357
+ firstSegment = false;
36358
+ }
36359
+
36360
+ if (isPathCharacter(ch) && (ch !== ':' || !firstSegment)) {
36361
+ output += ch;
36362
+ } else {
36363
+ const code = input.charCodeAt(i);
36364
+ if (code < 0x80) {
36365
+ output += BYTE_HEX[code];
36366
+ } else if (code < 0xD800 || code > 0xDFFF) {
36367
+ output += percentEncodeNonAscii(code);
36368
+ } else if (code <= 0xDBFF && i + 1 < input.length) {
36369
+ const low = input.charCodeAt(i + 1);
36370
+ if (low >= 0xDC00 && low <= 0xDFFF) {
36371
+ output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00));
36372
+ i++;
36373
+ } else {
36374
+ output += percentEncodeNonAscii(0xFFFD);
36375
+ }
36376
+ } else {
36377
+ output += percentEncodeNonAscii(0xFFFD);
36378
+ }
36379
+ }
36380
+ }
36381
+
36382
+ return output
35384
36383
  }
35385
36384
 
35386
36385
  /**
35387
- * Normalizes percent escapes and optionally decodes only unreserved ASCII bytes.
35388
- * Reserved delimiters such as `%2F` and `%2E` stay escaped.
36386
+ * Percent-encodes a URI component using its RFC 3986 literal character set.
36387
+ * Existing valid escapes are preserved and normalized to uppercase hex.
35389
36388
  *
35390
36389
  * @param {string} input
35391
- * @param {boolean} [decodeUnreserved=false]
36390
+ * @param {(value: string) => boolean} isAllowed
35392
36391
  * @returns {string}
35393
36392
  */
35394
- function normalizePercentEncoding (input, decodeUnreserved = false) {
35395
- if (input.indexOf('%') === -1) {
35396
- return input
35397
- }
35398
-
36393
+ function encodeComponent (input, isAllowed) {
35399
36394
  let output = '';
35400
36395
 
35401
36396
  for (let i = 0; i < input.length; i++) {
35402
- if (input[i] === '%' && i + 2 < input.length) {
36397
+ const ch = input[i];
36398
+ if (ch === '%' && i + 2 < input.length) {
35403
36399
  const hex = input.slice(i + 1, i + 3);
35404
36400
  if (isHexPair(hex)) {
35405
- const normalizedHex = hex.toUpperCase();
35406
- const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
35407
-
35408
- if (decodeUnreserved && isUnreserved(decoded)) {
35409
- output += decoded;
35410
- } else {
35411
- output += '%' + normalizedHex;
35412
- }
35413
-
36401
+ output += '%' + hex.toUpperCase();
35414
36402
  i += 2;
35415
36403
  continue
35416
36404
  }
35417
36405
  }
35418
36406
 
35419
- output += input[i];
36407
+ if (isAllowed(ch)) {
36408
+ output += ch;
36409
+ } else {
36410
+ const code = input.charCodeAt(i);
36411
+ if (code < 0x80) {
36412
+ output += BYTE_HEX[code];
36413
+ } else if (code < 0xD800 || code > 0xDFFF) {
36414
+ output += percentEncodeNonAscii(code);
36415
+ } else if (code <= 0xDBFF && i + 1 < input.length) {
36416
+ const low = input.charCodeAt(i + 1);
36417
+ if (low >= 0xDC00 && low <= 0xDFFF) {
36418
+ output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00));
36419
+ i++;
36420
+ } else {
36421
+ output += percentEncodeNonAscii(0xFFFD);
36422
+ }
36423
+ } else {
36424
+ output += percentEncodeNonAscii(0xFFFD);
36425
+ }
36426
+ }
35420
36427
  }
35421
36428
 
35422
36429
  return output
35423
36430
  }
35424
36431
 
35425
36432
  /**
35426
- * Normalizes path data without turning reserved escapes into live path syntax.
35427
- * Valid escapes are uppercased, raw unsafe characters are escaped, and only
35428
- * unreserved bytes that are not `.` are decoded.
36433
+ * Encodes userinfo while preserving its RFC 3986 §3.2.1 literal characters.
36434
+ * In particular, authority delimiters such as `@`, `/`, `?`, and `#` are data.
35429
36435
  *
35430
36436
  * @param {string} input
35431
36437
  * @returns {string}
35432
36438
  */
35433
- function normalizePathEncoding (input) {
36439
+ function encodeUserinfo (input) {
36440
+ return encodeComponent(input, isUserinfoCharacter)
36441
+ }
36442
+
36443
+ /**
36444
+ * Encodes query data using the RFC 3986 §3.4 grammar. A literal `#` must be
36445
+ * escaped because it would otherwise begin the fragment component.
36446
+ *
36447
+ * @param {string} input
36448
+ * @returns {string}
36449
+ */
36450
+ function encodeQuery (input) {
36451
+ return encodeComponent(input, isQueryFragmentCharacter)
36452
+ }
36453
+
36454
+ /**
36455
+ * Encodes fragment data using the RFC 3986 §3.5 grammar.
36456
+ *
36457
+ * @param {string} input
36458
+ * @returns {string}
36459
+ */
36460
+ function encodeFragment (input) {
36461
+ return encodeComponent(input, isQueryFragmentCharacter)
36462
+ }
36463
+
36464
+ function isEscapeSafe (cp) {
36465
+ return (
36466
+ (cp >= 0x30 && cp <= 0x39) ||
36467
+ (cp >= 0x41 && cp <= 0x5A) ||
36468
+ (cp >= 0x61 && cp <= 0x7A) ||
36469
+ cp === 0x2A || cp === 0x2B || cp === 0x2D || cp === 0x2E ||
36470
+ cp === 0x2F || cp === 0x40 || cp === 0x5F
36471
+ )
36472
+ }
36473
+
36474
+ /**
36475
+ * Normalizes the percent-encoding of a query or fragment component.
36476
+ *
36477
+ * Like `normalizePathEncoding`, but uses the query/fragment character set
36478
+ * (which additionally allows `?`) and decodes `.` since it has no dot-segment
36479
+ * meaning outside of a path.
36480
+ *
36481
+ * @param {string} input
36482
+ * @returns {string}
36483
+ */
36484
+ function normalizeQueryFragmentEncoding (input) {
35434
36485
  let output = '';
35435
36486
 
35436
36487
  for (let i = 0; i < input.length; i++) {
35437
- if (input[i] === '%' && i + 2 < input.length) {
36488
+ const ch = input[i];
36489
+ if (ch === '%' && i + 2 < input.length) {
35438
36490
  const hex = input.slice(i + 1, i + 3);
35439
36491
  if (isHexPair(hex)) {
35440
36492
  const normalizedHex = hex.toUpperCase();
35441
36493
  const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
35442
36494
 
35443
- if (decoded !== '.' && isUnreserved(decoded)) {
36495
+ if (isUnreserved(decoded)) {
35444
36496
  output += decoded;
35445
36497
  } else {
35446
36498
  output += '%' + normalizedHex;
@@ -35451,10 +36503,25 @@ function requireUtils () {
35451
36503
  }
35452
36504
  }
35453
36505
 
35454
- if (isPathCharacter(input[i])) {
35455
- output += input[i];
36506
+ if (isQueryFragmentCharacter(ch)) {
36507
+ output += ch;
35456
36508
  } else {
35457
- output += escape(input[i]);
36509
+ const code = input.charCodeAt(i);
36510
+ if (code < 0x80) {
36511
+ output += isEscapeSafe(code) ? ch : BYTE_HEX[code];
36512
+ } else if (code < 0xD800 || code > 0xDFFF) {
36513
+ output += percentEncodeNonAscii(code);
36514
+ } else if (code <= 0xDBFF && i + 1 < input.length) {
36515
+ const low = input.charCodeAt(i + 1);
36516
+ if (low >= 0xDC00 && low <= 0xDFFF) {
36517
+ output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00));
36518
+ i++;
36519
+ } else {
36520
+ output += percentEncodeNonAscii(0xFFFD);
36521
+ }
36522
+ } else {
36523
+ output += percentEncodeNonAscii(0xFFFD);
36524
+ }
35458
36525
  }
35459
36526
  }
35460
36527
 
@@ -35494,15 +36561,21 @@ function requireUtils () {
35494
36561
  const uriTokens = [];
35495
36562
 
35496
36563
  if (component.userinfo !== undefined) {
35497
- uriTokens.push(component.userinfo);
36564
+ uriTokens.push(encodeUserinfo(component.userinfo));
35498
36565
  uriTokens.push('@');
35499
36566
  }
35500
36567
 
35501
36568
  if (component.host !== undefined) {
35502
- let host = unescape(component.host);
36569
+ let host = component.host;
35503
36570
  if (!isIPv4(host)) {
35504
- const ipV6res = normalizeIPv6(host);
35505
- if (ipV6res.isIPV6 === true) {
36571
+ let ipV6res = normalizeIPv6(host);
36572
+ if (ipV6res.isIPV6 !== true && ipV6res.isIPVFuture !== true) {
36573
+ // Decode only unreserved bytes, once. In particular, keep %25 encoded
36574
+ // so it cannot introduce a second escape during recomposition.
36575
+ host = normalizePercentEncoding(host, true);
36576
+ ipV6res = normalizeIPv6(host);
36577
+ }
36578
+ if (ipV6res.isIPV6 === true || ipV6res.isIPVFuture === true) {
35506
36579
  host = `[${ipV6res.escapedHost}]`;
35507
36580
  } else {
35508
36581
  host = reescapeHostDelimiters(host, false);
@@ -35512,8 +36585,12 @@ function requireUtils () {
35512
36585
  }
35513
36586
 
35514
36587
  if (typeof component.port === 'number' || typeof component.port === 'string') {
36588
+ const port = String(component.port);
36589
+ if (!isPort(port)) {
36590
+ throw new TypeError('URI port is malformed.')
36591
+ }
35515
36592
  uriTokens.push(':');
35516
- uriTokens.push(String(component.port));
36593
+ uriTokens.push(port);
35517
36594
  }
35518
36595
 
35519
36596
  return uriTokens.length ? uriTokens.join('') : undefined
@@ -35524,6 +36601,11 @@ function requireUtils () {
35524
36601
  reescapeHostDelimiters,
35525
36602
  normalizePercentEncoding,
35526
36603
  normalizePathEncoding,
36604
+ serializePathEncoding,
36605
+ normalizeQueryFragmentEncoding,
36606
+ encodeUserinfo,
36607
+ encodeQuery,
36608
+ encodeFragment,
35527
36609
  escapePreservingEscapes,
35528
36610
  removeDotSegments,
35529
36611
  isIPv4,
@@ -35542,7 +36624,7 @@ function requireSchemes () {
35542
36624
  hasRequiredSchemes = 1;
35543
36625
 
35544
36626
  const { isUUID } = requireUtils();
35545
- const URN_REG = /([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-.:;=@]|%[\da-f]{2})+)/iu;
36627
+ const URN_REG = /^([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-./:;=@]|%[\da-f]{2})+)$/iu;
35546
36628
 
35547
36629
  const supportedSchemeNames = /** @type {const} */ (['http', 'https', 'ws',
35548
36630
  'wss', 'urn', 'urn:uuid']);
@@ -35654,9 +36736,14 @@ function requireSchemes () {
35654
36736
 
35655
36737
  // reconstruct path from resource name
35656
36738
  if (wsComponent.resourceName) {
35657
- const [path, query] = wsComponent.resourceName.split('?');
36739
+ const queryIndex = wsComponent.resourceName.indexOf('?');
36740
+ const path = queryIndex === -1
36741
+ ? wsComponent.resourceName
36742
+ : wsComponent.resourceName.slice(0, queryIndex);
35658
36743
  wsComponent.path = (path && path !== '/' ? path : undefined);
35659
- wsComponent.query = query;
36744
+ wsComponent.query = queryIndex === -1
36745
+ ? undefined
36746
+ : wsComponent.resourceName.slice(queryIndex + 1);
35660
36747
  wsComponent.resourceName = undefined;
35661
36748
  }
35662
36749
 
@@ -35673,7 +36760,7 @@ function requireSchemes () {
35673
36760
  return urnComponent
35674
36761
  }
35675
36762
  const matches = urnComponent.path.match(URN_REG);
35676
- if (matches) {
36763
+ if (matches && matches[0] === urnComponent.path) {
35677
36764
  const scheme = options.scheme || urnComponent.scheme || 'urn';
35678
36765
  urnComponent.nid = matches[1].toLowerCase();
35679
36766
  urnComponent.nss = matches[2];
@@ -35815,9 +36902,24 @@ function requireFastUri () {
35815
36902
  if (hasRequiredFastUri) return fastUri.exports;
35816
36903
  hasRequiredFastUri = 1;
35817
36904
 
35818
- const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = requireUtils();
36905
+ const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = requireUtils();
35819
36906
  const { SCHEMES, getSchemeHandler } = requireSchemes();
35820
36907
 
36908
+ const VALID_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*$/u;
36909
+ const MALFORMED_SCHEME_ERROR = 'URI scheme is malformed.';
36910
+
36911
+ /**
36912
+ * @param {string} scheme
36913
+ * @returns {string}
36914
+ */
36915
+ function decodeValidScheme (scheme) {
36916
+ const decodedScheme = unescape(String(scheme));
36917
+ if (!VALID_SCHEME.test(decodedScheme)) {
36918
+ throw new TypeError(MALFORMED_SCHEME_ERROR)
36919
+ }
36920
+ return decodedScheme
36921
+ }
36922
+
35821
36923
  /**
35822
36924
  * @template {import('./types/index').URIComponent|string} T
35823
36925
  * @param {T} uri
@@ -35841,12 +36943,50 @@ function requireFastUri () {
35841
36943
  */
35842
36944
  function resolve (baseURI, relativeURI, options) {
35843
36945
  const schemelessOptions = options ? Object.assign({ scheme: 'null' }, options) : { scheme: 'null' };
35844
- const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions);
35845
- const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions);
35846
- if (baseMalformed || relativeMalformed) {
36946
+ const {
36947
+ parsed: baseParsed,
36948
+ malformedAuthorityOrPort: baseMalformed,
36949
+ malformedPercentEncoding: baseMalformedPercentEncoding,
36950
+ malformedSchemeSpecific: baseMalformedSchemeSpecific,
36951
+ malformedHost: baseMalformedHost,
36952
+ malformedScheme: baseMalformedScheme
36953
+ } = parseWithStatus(baseURI, schemelessOptions);
36954
+ const {
36955
+ parsed: relativeParsed,
36956
+ malformedAuthorityOrPort: relativeMalformed,
36957
+ malformedPercentEncoding: relativeMalformedPercentEncoding,
36958
+ malformedSchemeSpecific: relativeMalformedSchemeSpecific,
36959
+ malformedHost: relativeMalformedHost,
36960
+ malformedScheme: relativeMalformedScheme
36961
+ } = parseWithStatus(relativeURI, schemelessOptions);
36962
+ if (
36963
+ baseMalformed ||
36964
+ relativeMalformed ||
36965
+ baseMalformedPercentEncoding ||
36966
+ relativeMalformedPercentEncoding ||
36967
+ baseMalformedSchemeSpecific ||
36968
+ relativeMalformedSchemeSpecific ||
36969
+ baseMalformedHost ||
36970
+ relativeMalformedHost ||
36971
+ baseMalformedScheme ||
36972
+ relativeMalformedScheme
36973
+ ) {
35847
36974
  throw new Error(baseParsed.error || relativeParsed.error || 'URI is malformed.')
35848
36975
  }
35849
36976
  const resolved = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true);
36977
+ const resolvedSchemeHandler = getSchemeHandler((options && options.scheme) || resolved.scheme);
36978
+ const resolvedHost = resolved.host;
36979
+ const resolvedHostIsIP = resolvedHost !== undefined && resolvedHost !== '' &&
36980
+ (isIPv4(resolvedHost) || normalizeIPv6(resolvedHost).isIPV6);
36981
+ canonicalizeHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP);
36982
+ // Percent escapes in an ASCII reg-name are encoded data. The WHATWG hostname
36983
+ // parser can reject them even though fast-uri preserves them safely as RFC
36984
+ // 3986 data. A raw non-ASCII host must still fail closed if conversion fails.
36985
+ const encodedASCIIHost = resolvedHost && resolvedHost.indexOf('%') !== -1 &&
36986
+ !/\P{ASCII}/u.test(resolvedHost);
36987
+ if (resolved.error && !encodedASCIIHost) {
36988
+ throw new Error(resolved.error)
36989
+ }
35850
36990
  schemelessOptions.skipEscape = true;
35851
36991
  return serialize(resolved, schemelessOptions)
35852
36992
  }
@@ -35929,7 +37069,7 @@ function requireFastUri () {
35929
37069
  const normalizedA = normalizeComparableURI(uriA, options);
35930
37070
  const normalizedB = normalizeComparableURI(uriB, options);
35931
37071
 
35932
- return normalizedA !== undefined && normalizedB !== undefined && normalizedA.toLowerCase() === normalizedB.toLowerCase()
37072
+ return normalizedA !== undefined && normalizedB !== undefined && normalizedA === normalizedB
35933
37073
  }
35934
37074
 
35935
37075
  /**
@@ -35957,25 +37097,30 @@ function requireFastUri () {
35957
37097
  const options = Object.assign({}, opts);
35958
37098
  const uriTokens = [];
35959
37099
 
37100
+ if (component.scheme) {
37101
+ component.scheme = decodeValidScheme(component.scheme);
37102
+ }
37103
+
35960
37104
  // find scheme handler
35961
37105
  const schemeHandler = getSchemeHandler(options.scheme || component.scheme);
35962
37106
 
35963
37107
  // perform scheme specific serialization
35964
37108
  if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
35965
37109
 
37110
+ const hasAuthority = component.userinfo !== undefined || component.host !== undefined || component.port !== undefined;
37111
+ const pathNoScheme = !options.skipEscape && component.scheme === undefined && !hasAuthority;
37112
+
35966
37113
  if (component.path !== undefined) {
35967
37114
  if (!options.skipEscape) {
35968
- component.path = escapePreservingEscapes(component.path);
35969
-
35970
- if (component.scheme !== undefined) {
35971
- component.path = component.path.split('%3A').join(':');
35972
- }
37115
+ component.path = serializePathEncoding(component.path, pathNoScheme);
35973
37116
  } else {
35974
37117
  component.path = normalizePercentEncoding(component.path);
35975
37118
  }
35976
37119
  }
35977
37120
 
35978
37121
  if (options.reference !== 'suffix' && component.scheme) {
37122
+ // Scheme handlers may replace the scheme during serialization.
37123
+ component.scheme = decodeValidScheme(component.scheme);
35979
37124
  uriTokens.push(component.scheme, ':');
35980
37125
  }
35981
37126
 
@@ -35998,6 +37143,13 @@ function requireFastUri () {
35998
37143
  s = removeDotSegments(s);
35999
37144
  }
36000
37145
 
37146
+ // Dot-segment removal can expose a colon that was not originally in the
37147
+ // first segment (for example, "./a:b"). Reapply path-noscheme encoding so
37148
+ // the serialized relative reference cannot be reparsed as a URI scheme.
37149
+ if (pathNoScheme) {
37150
+ s = serializePathEncoding(s, true);
37151
+ }
37152
+
36001
37153
  if (
36002
37154
  authority === undefined &&
36003
37155
  s[0] === '/' &&
@@ -36011,11 +37163,11 @@ function requireFastUri () {
36011
37163
  }
36012
37164
 
36013
37165
  if (component.query !== undefined) {
36014
- uriTokens.push('?', component.query);
37166
+ uriTokens.push('?', encodeQuery(component.query));
36015
37167
  }
36016
37168
 
36017
37169
  if (component.fragment !== undefined) {
36018
- uriTokens.push('#', component.fragment);
37170
+ uriTokens.push('#', encodeFragment(component.fragment));
36019
37171
  }
36020
37172
  return uriTokens.join('')
36021
37173
  }
@@ -36052,10 +37204,86 @@ function requireFastUri () {
36052
37204
  return undefined
36053
37205
  }
36054
37206
 
37207
+ /**
37208
+ * Checks percent syntax without decoding the represented octets. RFC 3986
37209
+ * percent-encoding is byte-oriented, so sequences such as `%FF` are valid even
37210
+ * though they are not independently valid UTF-8.
37211
+ *
37212
+ * @param {string|undefined} component
37213
+ * @returns {boolean}
37214
+ */
37215
+ function hasMalformedPercentEncoding (component) {
37216
+ if (component === undefined) return false
37217
+
37218
+ let percent = component.indexOf('%');
37219
+ while (percent !== -1) {
37220
+ if (percent + 2 >= component.length || !/^[\da-f]{2}$/iu.test(component.slice(percent + 1, percent + 3))) {
37221
+ return true
37222
+ }
37223
+ percent = component.indexOf('%', percent + 3);
37224
+ }
37225
+
37226
+ return false
37227
+ }
37228
+
37229
+ /**
37230
+ * Whether the host is a bracketed IP literal (RFC 3986 `IP-literal`).
37231
+ * An unterminated `[` is not a literal, so it must still be validated as a
37232
+ * reg-name instead of being waved through as an IP.
37233
+ *
37234
+ * @param {string} host
37235
+ * @returns {boolean}
37236
+ */
37237
+ function isIPLiteral (host) {
37238
+ return host[0] === '[' && host[host.length - 1] === ']'
37239
+ }
37240
+
37241
+ /**
37242
+ * @param {RegExpMatchArray} matches
37243
+ * @returns {boolean}
37244
+ */
37245
+ function hasMalformedComponentPercentEncoding (matches) {
37246
+ // Bracketed IP literals use a raw "%" as the zone separator for historical
37247
+ // compatibility. Their parsing is intentionally left to normalizeIPv6.
37248
+ const host = matches[4];
37249
+ return hasMalformedPercentEncoding(matches[3]) ||
37250
+ (host !== undefined && !isIPLiteral(host) && hasMalformedPercentEncoding(host)) ||
37251
+ hasMalformedPercentEncoding(matches[6]) ||
37252
+ hasMalformedPercentEncoding(matches[7]) ||
37253
+ hasMalformedPercentEncoding(matches[8])
37254
+ }
37255
+
37256
+ /**
37257
+ * @param {import('./types/index').URIComponent} parsed
37258
+ * @param {import('./types/index').Options} options
37259
+ * @param {{ domainHost?: boolean, unicodeSupport?: boolean }|undefined} schemeHandler
37260
+ * @param {boolean} isIP
37261
+ * @returns {boolean} whether host conversion failed
37262
+ */
37263
+ function canonicalizeHost (parsed, options, schemeHandler, isIP) {
37264
+ if (
37265
+ !options.unicodeSupport &&
37266
+ (!schemeHandler || !schemeHandler.unicodeSupport) &&
37267
+ parsed.host &&
37268
+ !isIPLiteral(parsed.host) &&
37269
+ (options.domainHost || (schemeHandler && schemeHandler.domainHost)) &&
37270
+ isIP === false &&
37271
+ nonSimpleDomain(parsed.host)
37272
+ ) {
37273
+ try {
37274
+ parsed.host = new URL('http://' + parsed.host).hostname;
37275
+ } catch (e) {
37276
+ parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e;
37277
+ return true
37278
+ }
37279
+ }
37280
+ return false
37281
+ }
37282
+
36055
37283
  /**
36056
37284
  * @param {string} uri
36057
37285
  * @param {import('./types/index').Options} [opts]
36058
- * @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean }}
37286
+ * @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
36059
37287
  */
36060
37288
  function parseWithStatus (uri, opts) {
36061
37289
  const options = Object.assign({}, opts);
@@ -36071,6 +37299,11 @@ function requireFastUri () {
36071
37299
  };
36072
37300
 
36073
37301
  let malformedAuthorityOrPort = false;
37302
+ let malformedPercentEncoding = false;
37303
+ let malformedSchemeSpecific = false;
37304
+ let malformedHost = false;
37305
+ let malformedIPLiteral = false;
37306
+ let malformedScheme = false;
36074
37307
 
36075
37308
  let isIP = false;
36076
37309
  if (options.reference === 'suffix') {
@@ -36128,6 +37361,21 @@ function requireFastUri () {
36128
37361
  parsed.query = matches[7];
36129
37362
  parsed.fragment = matches[8];
36130
37363
 
37364
+ if (parsed.scheme !== undefined) {
37365
+ const decodedScheme = unescape(parsed.scheme);
37366
+ if (VALID_SCHEME.test(decodedScheme)) {
37367
+ parsed.scheme = decodedScheme.toLowerCase();
37368
+ } else {
37369
+ parsed.error = parsed.error || MALFORMED_SCHEME_ERROR;
37370
+ malformedScheme = true;
37371
+ }
37372
+ }
37373
+
37374
+ malformedPercentEncoding = hasMalformedComponentPercentEncoding(matches);
37375
+ if (malformedPercentEncoding) {
37376
+ parsed.error = parsed.error || 'URI contains malformed percent-encoding.';
37377
+ }
37378
+
36131
37379
  // fix port number
36132
37380
  if (isNaN(parsed.port)) {
36133
37381
  parsed.port = matches[5];
@@ -36142,9 +37390,17 @@ function requireFastUri () {
36142
37390
  if (parsed.host) {
36143
37391
  const ipv4result = isIPv4(parsed.host);
36144
37392
  if (ipv4result === false) {
37393
+ const bracketedIPLiteral = isIPLiteral(parsed.host);
37394
+ const hasIPLiteralBracket = parsed.host.indexOf('[') !== -1 || parsed.host.indexOf(']') !== -1;
36145
37395
  const ipv6result = normalizeIPv6(parsed.host);
36146
- parsed.host = ipv6result.host.toLowerCase();
36147
- isIP = ipv6result.isIPV6;
37396
+ isIP = ipv6result.isIPV6 || ipv6result.isIPVFuture === true;
37397
+ malformedIPLiteral = hasIPLiteralBracket && (!bracketedIPLiteral || ipv6result.error === true);
37398
+ parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase();
37399
+
37400
+ if (malformedIPLiteral) {
37401
+ parsed.error = parsed.error || 'URI host is malformed.';
37402
+ malformedAuthorityOrPort = true;
37403
+ }
36148
37404
  } else {
36149
37405
  isIP = true;
36150
37406
  }
@@ -36167,49 +37423,40 @@ function requireFastUri () {
36167
37423
  // find scheme handler
36168
37424
  const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme);
36169
37425
 
36170
- // check if scheme can't handle IRIs
36171
- if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport)) {
36172
- // if host component is a domain name
36173
- if (parsed.host && (options.domainHost || (schemeHandler && schemeHandler.domainHost)) && isIP === false && nonSimpleDomain(parsed.host)) {
36174
- // convert Unicode IDN -> ASCII IDN
36175
- try {
36176
- parsed.host = new URL('http://' + parsed.host).hostname;
36177
- } catch (e) {
36178
- parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e;
36179
- }
36180
- }
36181
- // convert IRI -> URI
37426
+ // convert Unicode IDN -> ASCII IDN when the effective scheme uses domain hosts
37427
+ if (!malformedIPLiteral) {
37428
+ malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP);
36182
37429
  }
36183
37430
 
36184
37431
  if (!schemeHandler || (schemeHandler && !schemeHandler.skipNormalize)) {
36185
37432
  if (uri.indexOf('%') !== -1) {
36186
- if (parsed.scheme !== undefined) {
36187
- parsed.scheme = unescape(parsed.scheme);
36188
- }
36189
- if (parsed.host !== undefined) {
36190
- parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
37433
+ if (parsed.host !== undefined && !malformedIPLiteral) {
37434
+ const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
37435
+ parsed.host = reescapeHostDelimiters(host, isIP);
36191
37436
  }
36192
37437
  }
36193
37438
  if (parsed.path) {
36194
37439
  parsed.path = normalizePathEncoding(parsed.path);
36195
37440
  }
37441
+ if (parsed.query) {
37442
+ parsed.query = normalizeQueryFragmentEncoding(parsed.query);
37443
+ }
36196
37444
  if (parsed.fragment) {
36197
- try {
36198
- parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
36199
- } catch {
36200
- parsed.error = parsed.error || 'URI malformed';
36201
- }
37445
+ parsed.fragment = normalizeQueryFragmentEncoding(parsed.fragment);
36202
37446
  }
36203
37447
  }
36204
37448
 
36205
37449
  // perform scheme specific parsing
36206
37450
  if (schemeHandler && schemeHandler.parse) {
36207
37451
  schemeHandler.parse(parsed, options);
37452
+ if (schemeHandler === SCHEMES.urn && parsed.nid === undefined) {
37453
+ malformedSchemeSpecific = true;
37454
+ }
36208
37455
  }
36209
37456
  } else {
36210
37457
  parsed.error = parsed.error || 'URI can not be parsed.';
36211
37458
  }
36212
- return { parsed, malformedAuthorityOrPort }
37459
+ return { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme }
36213
37460
  }
36214
37461
 
36215
37462
  /**
@@ -36233,13 +37480,17 @@ function requireFastUri () {
36233
37480
  /**
36234
37481
  * @param {string} uri
36235
37482
  * @param {import('./types/index').Options} [opts]
36236
- * @returns {{ normalized: string, malformedAuthorityOrPort: boolean }}
37483
+ * @returns {{ normalized: string, malformedAuthorityOrPort: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
36237
37484
  */
36238
37485
  function normalizeStringWithStatus (uri, opts) {
36239
- const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
37486
+ const { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = parseWithStatus(uri, opts);
36240
37487
  return {
36241
- normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
36242
- malformedAuthorityOrPort
37488
+ normalized: malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? uri : serialize(parsed, opts),
37489
+ malformedAuthorityOrPort,
37490
+ malformedPercentEncoding,
37491
+ malformedSchemeSpecific,
37492
+ malformedHost,
37493
+ malformedScheme
36243
37494
  }
36244
37495
  }
36245
37496
 
@@ -36249,14 +37500,18 @@ function requireFastUri () {
36249
37500
  * @returns {string|undefined}
36250
37501
  */
36251
37502
  function normalizeComparableURI (uri, opts) {
36252
- if (typeof uri === 'string') {
36253
- const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts);
36254
- return malformedAuthorityOrPort ? undefined : normalized
37503
+ if (typeof uri !== 'string' && typeof uri !== 'object') {
37504
+ return undefined
36255
37505
  }
36256
37506
 
36257
- if (typeof uri === 'object') {
36258
- return serialize(uri, opts)
37507
+ let value;
37508
+ try {
37509
+ value = typeof uri === 'string' ? uri : serialize(uri, opts);
37510
+ } catch {
37511
+ return undefined
36259
37512
  }
37513
+ const { normalized, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = normalizeStringWithStatus(value, opts);
37514
+ return malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? undefined : normalized
36260
37515
  }
36261
37516
 
36262
37517
  const fastUri$1 = {