fast-uri 2.4.4 → 2.4.6

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