fast-uri 2.4.3 → 2.4.5

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.
package/lib/utils.js CHANGED
@@ -2,16 +2,8 @@
2
2
 
3
3
  const { HEX } = require('./scopedChars')
4
4
 
5
- function normalizeIPv4 (host) {
6
- if (findToken(host, '.') < 3) { return { host, isIPV4: false } }
7
- const matches = host.match(/^(\b[01]?\d{1,2}|\b2[0-4]\d|\b25[0-5])(\.([01]?\d{1,2}|2[0-4]\d|25[0-5])){3}$/u) || []
8
- const [address] = matches
9
- if (address) {
10
- return { host: stripLeadingZeros(address, '.'), isIPV4: true }
11
- } else {
12
- return { host, isIPV4: false }
13
- }
14
- }
5
+ /** @type {(value: string) => boolean} */
6
+ 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)
15
7
 
16
8
  /**
17
9
  * @param {string[]} input
@@ -30,110 +22,172 @@ function stringArrayToHexStripped (input, keepZero = false) {
30
22
  return acc
31
23
  }
32
24
 
33
- function getIPV6 (input) {
34
- let tokenCount = 0
35
- const output = { error: false, address: '', zone: '' }
36
- const address = []
37
- const buffer = []
38
- let isZone = false
39
- let endipv6Encountered = false
40
- let endIpv6 = false
41
-
42
- function consume () {
43
- if (buffer.length) {
44
- if (isZone === false) {
45
- const hex = stringArrayToHexStripped(buffer)
46
- if (hex !== undefined) {
47
- address.push(hex)
48
- } else {
49
- output.error = true
50
- return false
51
- }
52
- }
53
- buffer.length = 0
54
- }
55
- return true
56
- }
25
+ /** @type {(value: string) => boolean} */
26
+ const isHextet = RegExp.prototype.test.bind(/^[\dA-Fa-f]{1,4}$/)
57
27
 
58
- for (let i = 0; i < input.length; i++) {
59
- const cursor = input[i]
60
- if (cursor === '[' || cursor === ']') { continue }
61
- if (cursor === ':') {
62
- if (endipv6Encountered === true) {
63
- endIpv6 = true
64
- }
65
- if (!consume()) { break }
66
- tokenCount++
67
- address.push(':')
68
- if (tokenCount > 7) {
69
- // not valid
70
- output.error = true
71
- break
72
- }
73
- if (i - 1 >= 0 && input[i - 1] === ':') {
74
- endipv6Encountered = true
75
- }
76
- continue
77
- } else if (cursor === '%') {
78
- if (!consume()) { break }
79
- // switch to zone detection
80
- isZone = true
81
- } else {
82
- buffer.push(cursor)
28
+ /** @type {(value: string) => boolean} */
29
+ const isIPvFuture = RegExp.prototype.test.bind(/^[vV][\dA-Fa-f]+\.[A-Za-z\d\-._~!$&'()*+,;=:]+$/)
30
+
31
+ /** @type {(value: string) => boolean} */
32
+ const isZoneCharacter = RegExp.prototype.test.bind(/^[A-Za-z\d\-._~]$/)
33
+
34
+ /**
35
+ * @param {string} value
36
+ * @returns {boolean}
37
+ */
38
+ /** @type {(value: string) => boolean} */
39
+ const nonSimpleDomain = RegExp.prototype.test.bind(/[^!"$&'()*+,\-.;=_`a-z{}~]/u)
40
+
41
+ /**
42
+ * @param {string} zone
43
+ * @returns {boolean}
44
+ */
45
+ function isZoneIdentifier (zone) {
46
+ if (zone.length === 0) return false
47
+
48
+ for (let i = 0; i < zone.length; i++) {
49
+ if (isZoneCharacter(zone[i])) continue
50
+ if (zone[i] === '%' && i + 2 < zone.length && isHexPair(zone.slice(i + 1, i + 3))) {
51
+ i += 2
83
52
  continue
84
53
  }
54
+ return false
85
55
  }
86
- if (buffer.length) {
87
- if (isZone) {
88
- output.zone = buffer.join('')
89
- } else if (endIpv6) {
90
- address.push(buffer.join(''))
56
+
57
+ return true
58
+ }
59
+
60
+ /**
61
+ * Compresses the longest run of zero hextets to "::" per RFC 5952. A run of a
62
+ * single zero hextet is left uncompressed. On ties the leftmost run wins.
63
+ *
64
+ * @param {string[]} hextets
65
+ * @returns {string}
66
+ */
67
+ function compressIPv6ZeroRun (hextets) {
68
+ let bestStart = -1
69
+ let bestLength = 0
70
+ let runStart = -1
71
+ let runLength = 0
72
+ for (let i = 0; i < hextets.length; i++) {
73
+ if (hextets[i] === '0') {
74
+ if (runStart === -1) runStart = i
75
+ runLength++
76
+ if (runLength > bestLength) {
77
+ bestLength = runLength
78
+ bestStart = runStart
79
+ }
91
80
  } else {
92
- address.push(stringArrayToHexStripped(buffer))
81
+ runStart = -1
82
+ runLength = 0
93
83
  }
94
84
  }
95
- output.address = address.join('')
96
- return output
85
+
86
+ if (bestLength < 2) return hextets.join(':')
87
+
88
+ const head = hextets.slice(0, bestStart).join(':')
89
+ const tail = hextets.slice(bestStart + bestLength).join(':')
90
+ return head + '::' + tail
97
91
  }
98
92
 
99
- function normalizeIPv6 (host, opts = {}) {
100
- if (findToken(host, ':') < 2) { return { host, isIPV6: false } }
101
- const ipv6 = getIPV6(host)
93
+ /**
94
+ * Validates an IPv6 address against the alternatives in RFC 3986 section
95
+ * 3.2.2 and returns the same address with leading hextet zeroes removed.
96
+ * An embedded IPv4 address counts as two hextets and is only valid at the end.
97
+ *
98
+ * @param {string} input
99
+ * @returns {string|undefined}
100
+ */
101
+ function normalizeIPv6Address (input) {
102
+ const compression = input.indexOf('::')
103
+ if (compression !== -1 && input.indexOf('::', compression + 1) !== -1) return undefined
104
+
105
+ const left = compression === -1 ? input.split(':') : input.slice(0, compression).split(':')
106
+ const right = compression === -1 ? [] : input.slice(compression + 2).split(':')
107
+ if (compression !== -1) {
108
+ if (left.length === 1 && left[0] === '') left.length = 0
109
+ if (right.length === 1 && right[0] === '') right.length = 0
110
+ }
111
+
112
+ const parts = left.concat(right)
113
+ let hextetCount = 0
114
+ for (let i = 0; i < parts.length; i++) {
115
+ const part = parts[i]
116
+ if (part === '') return undefined
102
117
 
103
- if (!ipv6.error) {
104
- let newHost = ipv6.address
105
- let escapedHost = ipv6.address
106
- if (ipv6.zone) {
107
- newHost += '%' + ipv6.zone
108
- escapedHost += '%25' + ipv6.zone
118
+ if (part.indexOf('.') !== -1) {
119
+ if (i !== parts.length - 1 || (compression !== -1 && right.length === 0) || !isIPv4(part)) return undefined
120
+ hextetCount += 2
121
+ continue
109
122
  }
110
- return { host: newHost, escapedHost, isIPV6: true }
111
- } else {
112
- return { host, isIPV6: false }
123
+
124
+ if (!isHextet(part)) return undefined
125
+ parts[i] = parseInt(part, 16).toString(16)
126
+ hextetCount++
127
+ }
128
+
129
+ if (compression === -1) {
130
+ if (hextetCount !== 8) return undefined
131
+ return compressIPv6ZeroRun(parts)
113
132
  }
133
+ if (hextetCount >= 8) return undefined
134
+
135
+ // expand "::" then re-compress the longest run for a canonical result
136
+ const expanded = parts.slice(0, left.length)
137
+ for (let i = hextetCount; i < 8; i++) expanded.push('0')
138
+ for (let i = left.length; i < parts.length; i++) expanded.push(parts[i])
139
+ return compressIPv6ZeroRun(expanded)
114
140
  }
115
141
 
116
- function stripLeadingZeros (str, token) {
117
- let out = ''
118
- let skip = true
119
- const l = str.length
120
- for (let i = 0; i < l; i++) {
121
- const c = str[i]
122
- if (c === '0' && skip) {
123
- if ((i + 1 <= l && str[i + 1] === token) || i + 1 === l) {
124
- out += c
125
- skip = false
126
- }
127
- } else {
128
- if (c === token) {
129
- skip = true
130
- } else {
131
- skip = false
132
- }
133
- out += c
134
- }
142
+ /**
143
+ * @typedef {Object} NormalizeIPv6Result
144
+ * @property {string} host - The normalized host.
145
+ * @property {string} [escapedHost] - The escaped host.
146
+ * @property {boolean} isIPV6 - Indicates if the host is an IPv6 address.
147
+ * @property {boolean} [isIPVFuture] - Indicates if the host is an IPvFuture literal.
148
+ * @property {boolean} [error] - Indicates if a bracketed IP literal is malformed.
149
+ */
150
+
151
+ /**
152
+ * Validates and normalizes a bracketed IP literal. Raw zone separators remain
153
+ * accepted for backwards compatibility, while encoded separators and zone
154
+ * contents follow RFC 6874.
155
+ *
156
+ * @param {string} host
157
+ * @returns {NormalizeIPv6Result}
158
+ */
159
+ function normalizeIPv6 (host) {
160
+ const bracketed = host[0] === '[' && host[host.length - 1] === ']'
161
+ const hasBracket = host[0] === '[' || host[host.length - 1] === ']'
162
+ if (hasBracket && !bracketed) return { host, isIPV6: false, error: true }
163
+
164
+ let input = bracketed ? host.slice(1, -1) : host
165
+ if (bracketed && isIPvFuture(input)) {
166
+ input = input.toLowerCase()
167
+ return { host: `[${input}]`, escapedHost: input, isIPV6: false, isIPVFuture: true }
168
+ }
169
+
170
+ if (findToken(input, ':') < 2) {
171
+ return { host, isIPV6: false, error: bracketed }
172
+ }
173
+
174
+ let zoneIdentifier = ''
175
+ const zoneSeparator = input.indexOf('%')
176
+ if (zoneSeparator !== -1) {
177
+ const separatorLength = input.slice(zoneSeparator, zoneSeparator + 3).toLowerCase() === '%25' ? 3 : 1
178
+ zoneIdentifier = input.slice(zoneSeparator + separatorLength)
179
+ if (!isZoneIdentifier(zoneIdentifier)) return { host, isIPV6: false, error: true }
180
+ input = input.slice(0, zoneSeparator)
181
+ }
182
+
183
+ const address = normalizeIPv6Address(input)
184
+ if (address === undefined) return { host, isIPV6: false, error: true }
185
+
186
+ return {
187
+ host: address + (zoneIdentifier ? '%' + zoneIdentifier : ''),
188
+ escapedHost: address + (zoneIdentifier ? '%25' + zoneIdentifier : ''),
189
+ isIPV6: true
135
190
  }
136
- return out
137
191
  }
138
192
 
139
193
  function findToken (str, token) {
@@ -198,11 +252,40 @@ function reescapeHostDelimiters (host, isIP) {
198
252
 
199
253
  const isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu)
200
254
  const isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu)
201
- const isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu)
255
+ const isPathCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/]$/u)
256
+
257
+ /** @type {(value: string) => boolean} */
258
+ const isQueryFragmentCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/?]$/u)
259
+
260
+ /** @type {(value: string) => boolean} */
261
+ const isUserinfoCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:]$/u)
262
+
263
+ const BYTE_HEX = new Array(256)
264
+ {
265
+ const HEX_DIGITS = '0123456789ABCDEF'
266
+ for (let i = 0; i < 256; i++) {
267
+ BYTE_HEX[i] = '%' + HEX_DIGITS[i >> 4] + HEX_DIGITS[i & 0xF]
268
+ }
269
+ }
270
+ function percentEncodeNonAscii (cp) {
271
+ if (cp < 0x800) {
272
+ return BYTE_HEX[0xC0 | (cp >> 6)] +
273
+ BYTE_HEX[0x80 | (cp & 0x3F)]
274
+ }
275
+ if (cp < 0x10000) {
276
+ return BYTE_HEX[0xE0 | (cp >> 12)] +
277
+ BYTE_HEX[0x80 | ((cp >> 6) & 0x3F)] +
278
+ BYTE_HEX[0x80 | (cp & 0x3F)]
279
+ }
280
+ return BYTE_HEX[0xF0 | (cp >> 18)] +
281
+ BYTE_HEX[0x80 | ((cp >> 12) & 0x3F)] +
282
+ BYTE_HEX[0x80 | ((cp >> 6) & 0x3F)] +
283
+ BYTE_HEX[0x80 | (cp & 0x3F)]
284
+ }
202
285
 
203
286
  /**
204
287
  * Normalizes percent escapes and optionally decodes only unreserved ASCII bytes.
205
- * Reserved delimiters such as `%2F` and `%2E` stay escaped.
288
+ * Reserved delimiters such as `%2F` stay escaped; `%2E` is unreserved.
206
289
  *
207
290
  * @param {string} input
208
291
  * @param {boolean} [decodeUnreserved=false]
@@ -251,7 +334,8 @@ function normalizePathEncoding (input) {
251
334
  let output = ''
252
335
 
253
336
  for (let i = 0; i < input.length; i++) {
254
- if (input[i] === '%' && i + 2 < input.length) {
337
+ const ch = input[i]
338
+ if (ch === '%' && i + 2 < input.length) {
255
339
  const hex = input.slice(i + 1, i + 3)
256
340
  if (isHexPair(hex)) {
257
341
  const normalizedHex = hex.toUpperCase()
@@ -268,10 +352,225 @@ function normalizePathEncoding (input) {
268
352
  }
269
353
  }
270
354
 
271
- if (isPathCharacter(input[i])) {
272
- output += input[i]
355
+ if (isPathCharacter(ch)) {
356
+ output += ch
273
357
  } else {
274
- output += escape(input[i])
358
+ const code = input.charCodeAt(i)
359
+ if (code < 0x80) {
360
+ output += isEscapeSafe(code) ? ch : BYTE_HEX[code]
361
+ } else if (code < 0xD800 || code > 0xDFFF) {
362
+ output += percentEncodeNonAscii(code)
363
+ } else if (code <= 0xDBFF && i + 1 < input.length) {
364
+ const low = input.charCodeAt(i + 1)
365
+ if (low >= 0xDC00 && low <= 0xDFFF) {
366
+ output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
367
+ i++
368
+ } else {
369
+ output += percentEncodeNonAscii(0xFFFD)
370
+ }
371
+ } else {
372
+ output += percentEncodeNonAscii(0xFFFD)
373
+ }
374
+ }
375
+ }
376
+
377
+ return output
378
+ }
379
+
380
+ /**
381
+ * Serializes a path without rewriting reserved data. Raw RFC 3986 path
382
+ * characters remain literal, valid escapes are preserved and uppercased, and
383
+ * everything else is UTF-8 percent-encoded. In a path-noscheme, a colon in the
384
+ * first segment must be escaped so the result cannot be parsed as a scheme.
385
+ *
386
+ * @param {string} input
387
+ * @param {boolean} [pathNoScheme=false]
388
+ * @returns {string}
389
+ */
390
+ function serializePathEncoding (input, pathNoScheme = false) {
391
+ let output = ''
392
+ let firstSegment = pathNoScheme && input[0] !== '/'
393
+
394
+ for (let i = 0; i < input.length; i++) {
395
+ const ch = input[i]
396
+ if (ch === '%' && i + 2 < input.length) {
397
+ const hex = input.slice(i + 1, i + 3)
398
+ if (isHexPair(hex)) {
399
+ output += '%' + hex.toUpperCase()
400
+ i += 2
401
+ continue
402
+ }
403
+ }
404
+
405
+ if (ch === '/') {
406
+ firstSegment = false
407
+ }
408
+
409
+ if (isPathCharacter(ch) && (ch !== ':' || !firstSegment)) {
410
+ output += ch
411
+ } else {
412
+ const code = input.charCodeAt(i)
413
+ if (code < 0x80) {
414
+ output += BYTE_HEX[code]
415
+ } else if (code < 0xD800 || code > 0xDFFF) {
416
+ output += percentEncodeNonAscii(code)
417
+ } else if (code <= 0xDBFF && i + 1 < input.length) {
418
+ const low = input.charCodeAt(i + 1)
419
+ if (low >= 0xDC00 && low <= 0xDFFF) {
420
+ output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
421
+ i++
422
+ } else {
423
+ output += percentEncodeNonAscii(0xFFFD)
424
+ }
425
+ } else {
426
+ output += percentEncodeNonAscii(0xFFFD)
427
+ }
428
+ }
429
+ }
430
+
431
+ return output
432
+ }
433
+
434
+ /**
435
+ * Percent-encodes a URI component using its RFC 3986 literal character set.
436
+ * Existing valid escapes are preserved and normalized to uppercase hex.
437
+ *
438
+ * @param {string} input
439
+ * @param {(value: string) => boolean} isAllowed
440
+ * @returns {string}
441
+ */
442
+ function encodeComponent (input, isAllowed) {
443
+ let output = ''
444
+
445
+ for (let i = 0; i < input.length; i++) {
446
+ const ch = input[i]
447
+ if (ch === '%' && i + 2 < input.length) {
448
+ const hex = input.slice(i + 1, i + 3)
449
+ if (isHexPair(hex)) {
450
+ output += '%' + hex.toUpperCase()
451
+ i += 2
452
+ continue
453
+ }
454
+ }
455
+
456
+ if (isAllowed(ch)) {
457
+ output += ch
458
+ } else {
459
+ const code = input.charCodeAt(i)
460
+ if (code < 0x80) {
461
+ output += BYTE_HEX[code]
462
+ } else if (code < 0xD800 || code > 0xDFFF) {
463
+ output += percentEncodeNonAscii(code)
464
+ } else if (code <= 0xDBFF && i + 1 < input.length) {
465
+ const low = input.charCodeAt(i + 1)
466
+ if (low >= 0xDC00 && low <= 0xDFFF) {
467
+ output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
468
+ i++
469
+ } else {
470
+ output += percentEncodeNonAscii(0xFFFD)
471
+ }
472
+ } else {
473
+ output += percentEncodeNonAscii(0xFFFD)
474
+ }
475
+ }
476
+ }
477
+
478
+ return output
479
+ }
480
+
481
+ /**
482
+ * Encodes userinfo while preserving its RFC 3986 §3.2.1 literal characters.
483
+ * In particular, authority delimiters such as `@`, `/`, `?`, and `#` are data.
484
+ *
485
+ * @param {string} input
486
+ * @returns {string}
487
+ */
488
+ function encodeUserinfo (input) {
489
+ return encodeComponent(input, isUserinfoCharacter)
490
+ }
491
+
492
+ /**
493
+ * Encodes query data using the RFC 3986 §3.4 grammar. A literal `#` must be
494
+ * escaped because it would otherwise begin the fragment component.
495
+ *
496
+ * @param {string} input
497
+ * @returns {string}
498
+ */
499
+ function encodeQuery (input) {
500
+ return encodeComponent(input, isQueryFragmentCharacter)
501
+ }
502
+
503
+ /**
504
+ * Encodes fragment data using the RFC 3986 §3.5 grammar.
505
+ *
506
+ * @param {string} input
507
+ * @returns {string}
508
+ */
509
+ function encodeFragment (input) {
510
+ return encodeComponent(input, isQueryFragmentCharacter)
511
+ }
512
+
513
+ function isEscapeSafe (cp) {
514
+ return (
515
+ (cp >= 0x30 && cp <= 0x39) ||
516
+ (cp >= 0x41 && cp <= 0x5A) ||
517
+ (cp >= 0x61 && cp <= 0x7A) ||
518
+ cp === 0x2A || cp === 0x2B || cp === 0x2D || cp === 0x2E ||
519
+ cp === 0x2F || cp === 0x40 || cp === 0x5F
520
+ )
521
+ }
522
+
523
+ /**
524
+ * Normalizes the percent-encoding of a query or fragment component.
525
+ *
526
+ * Like `normalizePathEncoding`, but uses the query/fragment character set
527
+ * (which additionally allows `?`) and decodes `.` since it has no dot-segment
528
+ * meaning outside of a path.
529
+ *
530
+ * @param {string} input
531
+ * @returns {string}
532
+ */
533
+ function normalizeQueryFragmentEncoding (input) {
534
+ let output = ''
535
+
536
+ for (let i = 0; i < input.length; i++) {
537
+ const ch = input[i]
538
+ if (ch === '%' && i + 2 < input.length) {
539
+ const hex = input.slice(i + 1, i + 3)
540
+ if (isHexPair(hex)) {
541
+ const normalizedHex = hex.toUpperCase()
542
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16))
543
+
544
+ if (isUnreserved(decoded)) {
545
+ output += decoded
546
+ } else {
547
+ output += '%' + normalizedHex
548
+ }
549
+
550
+ i += 2
551
+ continue
552
+ }
553
+ }
554
+
555
+ if (isQueryFragmentCharacter(ch)) {
556
+ output += ch
557
+ } else {
558
+ const code = input.charCodeAt(i)
559
+ if (code < 0x80) {
560
+ output += isEscapeSafe(code) ? ch : BYTE_HEX[code]
561
+ } else if (code < 0xD800 || code > 0xDFFF) {
562
+ output += percentEncodeNonAscii(code)
563
+ } else if (code <= 0xDBFF && i + 1 < input.length) {
564
+ const low = input.charCodeAt(i + 1)
565
+ if (low >= 0xDC00 && low <= 0xDFFF) {
566
+ output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
567
+ i++
568
+ } else {
569
+ output += percentEncodeNonAscii(0xFFFD)
570
+ }
571
+ } else {
572
+ output += percentEncodeNonAscii(0xFFFD)
573
+ }
275
574
  }
276
575
  }
277
576
 
@@ -307,19 +606,21 @@ function recomposeAuthority (components, options) {
307
606
  const uriTokens = []
308
607
 
309
608
  if (components.userinfo !== undefined) {
310
- uriTokens.push(components.userinfo)
609
+ uriTokens.push(encodeUserinfo(components.userinfo))
311
610
  uriTokens.push('@')
312
611
  }
313
612
 
314
613
  if (components.host !== undefined) {
315
- let host = unescape(components.host)
316
- const ipV4res = normalizeIPv4(host)
317
-
318
- if (ipV4res.isIPV4) {
319
- host = ipV4res.host
320
- } else {
321
- const ipV6res = normalizeIPv6(ipV4res.host, { isIPV4: false })
322
- if (ipV6res.isIPV6 === true) {
614
+ let host = components.host
615
+ if (!isIPv4(host)) {
616
+ let ipV6res = normalizeIPv6(host)
617
+ if (ipV6res.isIPV6 !== true && ipV6res.isIPVFuture !== true) {
618
+ // Decode only unreserved bytes, once. In particular, keep %25 encoded
619
+ // so it cannot introduce a second escape during recomposition.
620
+ host = normalizePercentEncoding(host, true)
621
+ ipV6res = normalizeIPv6(host)
622
+ }
623
+ if (ipV6res.isIPV6 === true || ipV6res.isIPVFuture === true) {
323
624
  host = `[${ipV6res.escapedHost}]`
324
625
  } else {
325
626
  host = reescapeHostDelimiters(host, false)
@@ -337,13 +638,20 @@ function recomposeAuthority (components, options) {
337
638
  };
338
639
 
339
640
  module.exports = {
641
+ nonSimpleDomain,
340
642
  recomposeAuthority,
341
643
  reescapeHostDelimiters,
342
644
  normalizePercentEncoding,
343
645
  normalizePathEncoding,
646
+ serializePathEncoding,
647
+ normalizeQueryFragmentEncoding,
648
+ encodeUserinfo,
649
+ encodeQuery,
650
+ encodeFragment,
344
651
  escapePreservingEscapes,
345
652
  removeDotSegments,
346
- normalizeIPv4,
653
+ isIPv4,
347
654
  normalizeIPv6,
348
- stringArrayToHexStripped
655
+ stringArrayToHexStripped,
656
+ isIPvFuture
349
657
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fast-uri",
3
3
  "description": "Dependency free RFC 3986 URI toolbox",
4
- "version": "2.4.3",
4
+ "version": "2.4.5",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
7
7
  "types": "types/index.d.ts",
@@ -31,6 +31,7 @@
31
31
  "snazzy": "^9.0.0",
32
32
  "standard": "^17.0.0",
33
33
  "tap": "^16.0.0",
34
+ "tape": "^5.8.1",
34
35
  "tsd": "^0.31.0",
35
36
  "uri-js": "^4.4.1"
36
37
  }
@@ -57,6 +57,18 @@ test('compatibility Parse', (t) => {
57
57
  // 'mailto:user@%E7%B4%8D%E8%B1%86.example.org?subject=Test&body=NATTO'
58
58
  ]
59
59
  toParse.forEach((x) => {
60
+ if (x === '//10.10.000.10') {
61
+ t.skip('Skipping //10.10.000.10 as it is not a valid URI per URI spec: https://datatracker.ietf.org/doc/html/rfc5954#section-4.1')
62
+ return
63
+ }
64
+ // Malformed bracketed IP literals are rejected by fast-uri without being
65
+ // rewritten; uri-js silently accepts them, so pin the expected output.
66
+ if (x === '//[2001:dbZ::1]:80' || x === '//[2606:2800:220:1:248:1893:25c8:1946:43209]') {
67
+ const parsed = fastifyURI.parse(x)
68
+ t.equal(parsed.error, 'URI host is malformed.', 'Compatibility parse: ' + x)
69
+ t.equal(parsed.host, x.slice(2, x.indexOf(']') + 1).toLowerCase(), 'Compatibility parse host: ' + x)
70
+ return
71
+ }
60
72
  t.same(fastifyURI.parse(x), urijs.parse(x), 'Compatibility parse: ' + x)
61
73
  })
62
74
  t.end()