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