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/index.js CHANGED
@@ -1,8 +1,23 @@
1
1
  'use strict'
2
2
 
3
- const { normalizeIPv6, normalizeIPv4, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters } = require('./lib/utils')
3
+ const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require('./lib/utils')
4
4
  const SCHEMES = require('./lib/schemes')
5
5
 
6
+ const VALID_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*$/u
7
+ const MALFORMED_SCHEME_ERROR = 'URI scheme is malformed.'
8
+
9
+ /**
10
+ * @param {string} scheme
11
+ * @returns {string}
12
+ */
13
+ function decodeValidScheme (scheme) {
14
+ const decodedScheme = unescape(String(scheme))
15
+ if (!VALID_SCHEME.test(decodedScheme)) {
16
+ throw new TypeError(MALFORMED_SCHEME_ERROR)
17
+ }
18
+ return decodedScheme
19
+ }
20
+
6
21
  function normalize (uri, options) {
7
22
  if (typeof uri === 'string') {
8
23
  uri = normalizeString(uri, options)
@@ -14,12 +29,50 @@ function normalize (uri, options) {
14
29
 
15
30
  function resolve (baseURI, relativeURI, options) {
16
31
  const schemelessOptions = Object.assign({ scheme: 'null' }, options)
17
- const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions)
18
- const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions)
19
- if (baseMalformed || relativeMalformed) {
32
+ const {
33
+ parsed: baseParsed,
34
+ malformedAuthorityOrPort: baseMalformed,
35
+ malformedPercentEncoding: baseMalformedPercentEncoding,
36
+ malformedSchemeSpecific: baseMalformedSchemeSpecific,
37
+ malformedHost: baseMalformedHost,
38
+ malformedScheme: baseMalformedScheme
39
+ } = parseWithStatus(baseURI, schemelessOptions)
40
+ const {
41
+ parsed: relativeParsed,
42
+ malformedAuthorityOrPort: relativeMalformed,
43
+ malformedPercentEncoding: relativeMalformedPercentEncoding,
44
+ malformedSchemeSpecific: relativeMalformedSchemeSpecific,
45
+ malformedHost: relativeMalformedHost,
46
+ malformedScheme: relativeMalformedScheme
47
+ } = parseWithStatus(relativeURI, schemelessOptions)
48
+ if (
49
+ baseMalformed ||
50
+ relativeMalformed ||
51
+ baseMalformedPercentEncoding ||
52
+ relativeMalformedPercentEncoding ||
53
+ baseMalformedSchemeSpecific ||
54
+ relativeMalformedSchemeSpecific ||
55
+ baseMalformedHost ||
56
+ relativeMalformedHost ||
57
+ baseMalformedScheme ||
58
+ relativeMalformedScheme
59
+ ) {
20
60
  throw new Error(baseParsed.error || relativeParsed.error || 'URI is malformed.')
21
61
  }
22
62
  const resolved = resolveComponents(baseParsed, relativeParsed, schemelessOptions, true)
63
+ const resolvedSchemeHandler = SCHEMES[((options && options.scheme) || resolved.scheme || '').toLowerCase()]
64
+ const resolvedHost = resolved.host
65
+ const resolvedHostIsIP = resolvedHost !== undefined && resolvedHost !== '' &&
66
+ (isIPv4(resolvedHost) || normalizeIPv6(resolvedHost).isIPV6)
67
+ canonicalizeHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP)
68
+ // Percent escapes in an ASCII reg-name are encoded data. The WHATWG hostname
69
+ // parser can reject them even though fast-uri preserves them safely as RFC
70
+ // 3986 data. A raw non-ASCII host must still fail closed if conversion fails.
71
+ const encodedASCIIHost = resolvedHost && resolvedHost.indexOf('%') !== -1 &&
72
+ !/\P{ASCII}/u.test(resolvedHost)
73
+ if (resolved.error && !encodedASCIIHost) {
74
+ throw new Error(resolved.error)
75
+ }
23
76
  return serialize(resolved, { ...schemelessOptions, skipEscape: true })
24
77
  }
25
78
 
@@ -87,7 +140,7 @@ function equal (uriA, uriB, options) {
87
140
  const normalizedA = normalizeComparableURI(uriA, options)
88
141
  const normalizedB = normalizeComparableURI(uriB, options)
89
142
 
90
- return normalizedA !== undefined && normalizedB !== undefined && normalizedA.toLowerCase() === normalizedB.toLowerCase()
143
+ return normalizedA !== undefined && normalizedB !== undefined && normalizedA === normalizedB
91
144
  }
92
145
 
93
146
  function serialize (cmpts, opts) {
@@ -110,25 +163,30 @@ function serialize (cmpts, opts) {
110
163
  const options = Object.assign({}, opts)
111
164
  const uriTokens = []
112
165
 
166
+ if (components.scheme) {
167
+ components.scheme = decodeValidScheme(components.scheme)
168
+ }
169
+
113
170
  // find scheme handler
114
171
  const schemeHandler = SCHEMES[(options.scheme || components.scheme || '').toLowerCase()]
115
172
 
116
173
  // perform scheme specific serialization
117
174
  if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(components, options)
118
175
 
176
+ const hasAuthority = components.userinfo !== undefined || components.host !== undefined || components.port !== undefined
177
+ const pathNoScheme = !options.skipEscape && components.scheme === undefined && !hasAuthority
178
+
119
179
  if (components.path !== undefined) {
120
180
  if (!options.skipEscape) {
121
- components.path = escapePreservingEscapes(components.path)
122
-
123
- if (components.scheme !== undefined) {
124
- components.path = components.path.split('%3A').join(':')
125
- }
181
+ components.path = serializePathEncoding(components.path, pathNoScheme)
126
182
  } else {
127
183
  components.path = normalizePercentEncoding(components.path)
128
184
  }
129
185
  }
130
186
 
131
187
  if (options.reference !== 'suffix' && components.scheme) {
188
+ // Scheme handlers may replace the scheme during serialization.
189
+ components.scheme = decodeValidScheme(components.scheme)
132
190
  uriTokens.push(components.scheme)
133
191
  uriTokens.push(':')
134
192
  }
@@ -152,6 +210,13 @@ function serialize (cmpts, opts) {
152
210
  s = removeDotSegments(s)
153
211
  }
154
212
 
213
+ // Dot-segment removal can expose a colon that was not originally in the
214
+ // first segment (for example, "./a:b"). Reapply path-noscheme encoding so
215
+ // the serialized relative reference cannot be reparsed as a URI scheme.
216
+ if (pathNoScheme) {
217
+ s = serializePathEncoding(s, true)
218
+ }
219
+
155
220
  if (authority === undefined) {
156
221
  s = s.replace(/^\/\//u, '/%2F') // don't allow the path to start with "//"
157
222
  }
@@ -161,29 +226,16 @@ function serialize (cmpts, opts) {
161
226
 
162
227
  if (components.query !== undefined) {
163
228
  uriTokens.push('?')
164
- uriTokens.push(components.query)
229
+ uriTokens.push(encodeQuery(components.query))
165
230
  }
166
231
 
167
232
  if (components.fragment !== undefined) {
168
233
  uriTokens.push('#')
169
- uriTokens.push(components.fragment)
234
+ uriTokens.push(encodeFragment(components.fragment))
170
235
  }
171
236
  return uriTokens.join('')
172
237
  }
173
238
 
174
- const hexLookUp = Array.from({ length: 127 }, (v, k) => /[^!"$&'()*+,\-.;=_`a-z{}~]/u.test(String.fromCharCode(k)))
175
-
176
- function nonSimpleDomain (value) {
177
- let code = 0
178
- for (let i = 0, len = value.length; i < len; ++i) {
179
- code = value.charCodeAt(i)
180
- if (code > 126 || hexLookUp[code]) {
181
- return true
182
- }
183
- }
184
- return false
185
- }
186
-
187
239
  const URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u
188
240
 
189
241
  // Captures the authority component (between "//" and the next "/", "?" or "#"),
@@ -211,6 +263,82 @@ function getParseError (parsed, matches) {
211
263
  return undefined
212
264
  }
213
265
 
266
+ /**
267
+ * Checks percent syntax without decoding the represented octets. RFC 3986
268
+ * percent-encoding is byte-oriented, so sequences such as `%FF` are valid even
269
+ * though they are not independently valid UTF-8.
270
+ *
271
+ * @param {string|undefined} component
272
+ * @returns {boolean}
273
+ */
274
+ /**
275
+ * @param {import('./types/index').URIComponent} parsed
276
+ * @param {import('./types/index').Options} options
277
+ * @param {{ domainHost?: boolean, unicodeSupport?: boolean }|undefined} schemeHandler
278
+ * @param {boolean} isIP
279
+ * @returns {boolean} whether host conversion failed
280
+ */
281
+ function canonicalizeHost (parsed, options, schemeHandler, isIP) {
282
+ if (
283
+ !options.unicodeSupport &&
284
+ (!schemeHandler || !schemeHandler.unicodeSupport) &&
285
+ parsed.host &&
286
+ !isIPLiteral(parsed.host) &&
287
+ (options.domainHost || (schemeHandler && schemeHandler.domainHost)) &&
288
+ isIP === false &&
289
+ nonSimpleDomain(parsed.host)
290
+ ) {
291
+ try {
292
+ parsed.host = new URL('http://' + parsed.host).hostname
293
+ } catch (e) {
294
+ parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e
295
+ return true
296
+ }
297
+ }
298
+ return false
299
+ }
300
+
301
+ function hasMalformedPercentEncoding (component) {
302
+ if (component === undefined) return false
303
+
304
+ let percent = component.indexOf('%')
305
+ while (percent !== -1) {
306
+ if (percent + 2 >= component.length || !/^[\da-f]{2}$/iu.test(component.slice(percent + 1, percent + 3))) {
307
+ return true
308
+ }
309
+ percent = component.indexOf('%', percent + 3)
310
+ }
311
+
312
+ return false
313
+ }
314
+
315
+ /**
316
+ * Whether the host is a bracketed IP literal (RFC 3986 `IP-literal`).
317
+ * An unterminated `[` is not a literal, so it must still be validated as a
318
+ * reg-name instead of being waved through as an IP.
319
+ *
320
+ * @param {string} host
321
+ * @returns {boolean}
322
+ */
323
+ function isIPLiteral (host) {
324
+ return host[0] === '[' && host[host.length - 1] === ']'
325
+ }
326
+
327
+ /**
328
+ * @param {RegExpMatchArray} matches
329
+ * @returns {boolean}
330
+ */
331
+ function hasMalformedComponentPercentEncoding (matches) {
332
+ // Bracketed IP literals use a raw "%" as the zone separator for historical
333
+ // compatibility. Their parsing is intentionally left to normalizeIPv6.
334
+ const host = matches[4]
335
+ return hasMalformedPercentEncoding(matches[3]) ||
336
+ (host !== undefined && !isIPLiteral(host) && hasMalformedPercentEncoding(host)) ||
337
+ hasMalformedPercentEncoding(matches[6]) ||
338
+ hasMalformedPercentEncoding(matches[7]) ||
339
+ hasMalformedPercentEncoding(matches[8])
340
+ }
341
+
214
342
  function parseWithStatus (uri, opts) {
215
343
  const options = Object.assign({}, opts)
216
344
  const parsed = {
@@ -222,8 +350,12 @@ function parseWithStatus (uri, opts) {
222
350
  query: undefined,
223
351
  fragment: undefined
224
352
  }
225
- const gotEncoding = uri.indexOf('%') !== -1
226
353
  let malformedAuthorityOrPort = false
354
+ let malformedPercentEncoding = false
355
+ let malformedSchemeSpecific = false
356
+ let malformedHost = false
357
+ let malformedIPLiteral = false
358
+ let malformedScheme = false
227
359
  let isIP = false
228
360
  if (options.reference === 'suffix') uri = (options.scheme ? options.scheme + ':' : '') + '//' + uri
229
361
 
@@ -274,6 +406,21 @@ function parseWithStatus (uri, opts) {
274
406
  parsed.query = matches[7]
275
407
  parsed.fragment = matches[8]
276
408
 
409
+ if (parsed.scheme !== undefined) {
410
+ const decodedScheme = unescape(parsed.scheme)
411
+ if (VALID_SCHEME.test(decodedScheme)) {
412
+ parsed.scheme = decodedScheme.toLowerCase()
413
+ } else {
414
+ parsed.error = parsed.error || MALFORMED_SCHEME_ERROR
415
+ malformedScheme = true
416
+ }
417
+ }
418
+
419
+ malformedPercentEncoding = hasMalformedComponentPercentEncoding(matches)
420
+ if (malformedPercentEncoding) {
421
+ parsed.error = parsed.error || 'URI contains malformed percent-encoding.'
422
+ }
423
+
277
424
  // fix port number
278
425
  if (isNaN(parsed.port)) {
279
426
  parsed.port = matches[5]
@@ -286,13 +433,20 @@ function parseWithStatus (uri, opts) {
286
433
  }
287
434
 
288
435
  if (parsed.host) {
289
- const ipv4result = normalizeIPv4(parsed.host)
290
- if (ipv4result.isIPV4 === false) {
291
- const ipv6result = normalizeIPv6(ipv4result.host, { isIPV4: false })
292
- parsed.host = ipv6result.host.toLowerCase()
293
- isIP = ipv6result.isIPV6
436
+ const ipv4result = isIPv4(parsed.host)
437
+ if (ipv4result === false) {
438
+ const bracketedIPLiteral = isIPLiteral(parsed.host)
439
+ const hasIPLiteralBracket = parsed.host.indexOf('[') !== -1 || parsed.host.indexOf(']') !== -1
440
+ const ipv6result = normalizeIPv6(parsed.host)
441
+ isIP = ipv6result.isIPV6 || ipv6result.isIPVFuture === true
442
+ malformedIPLiteral = hasIPLiteralBracket && (!bracketedIPLiteral || ipv6result.error === true)
443
+ parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase()
444
+
445
+ if (malformedIPLiteral) {
446
+ parsed.error = parsed.error || 'URI host is malformed.'
447
+ malformedAuthorityOrPort = true
448
+ }
294
449
  } else {
295
- parsed.host = ipv4result.host
296
450
  isIP = true
297
451
  }
298
452
  }
@@ -314,46 +468,38 @@ function parseWithStatus (uri, opts) {
314
468
  // find scheme handler
315
469
  const schemeHandler = SCHEMES[(options.scheme || parsed.scheme || '').toLowerCase()]
316
470
 
317
- // check if scheme can't handle IRIs
318
- if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport)) {
319
- // if host component is a domain name
320
- if (parsed.host && (options.domainHost || (schemeHandler && schemeHandler.domainHost)) && isIP === false && nonSimpleDomain(parsed.host)) {
321
- // convert Unicode IDN -> ASCII IDN
322
- try {
323
- parsed.host = new URL('http://' + parsed.host).hostname
324
- } catch (e) {
325
- parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e
326
- }
327
- }
328
- // convert IRI -> URI
471
+ // convert Unicode IDN -> ASCII IDN when the effective scheme uses domain hosts
472
+ if (!malformedIPLiteral) {
473
+ malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP)
329
474
  }
330
475
 
331
476
  if (!schemeHandler || (schemeHandler && !schemeHandler.skipNormalize)) {
332
- if (gotEncoding && parsed.scheme !== undefined) {
333
- parsed.scheme = unescape(parsed.scheme)
334
- }
335
- if (gotEncoding && parsed.userinfo !== undefined) {
336
- parsed.userinfo = unescape(parsed.userinfo)
337
- }
338
- if (gotEncoding && parsed.host !== undefined) {
339
- parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP)
477
+ if (parsed.host !== undefined && !malformedIPLiteral) {
478
+ const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true)
479
+ parsed.host = reescapeHostDelimiters(host, isIP)
340
480
  }
341
481
  if (parsed.path !== undefined && parsed.path.length) {
342
482
  parsed.path = normalizePathEncoding(parsed.path)
343
483
  }
484
+ if (parsed.query !== undefined && parsed.query.length) {
485
+ parsed.query = normalizeQueryFragmentEncoding(parsed.query)
486
+ }
344
487
  if (parsed.fragment !== undefined && parsed.fragment.length) {
345
- parsed.fragment = encodeURI(decodeURI(parsed.fragment))
488
+ parsed.fragment = normalizeQueryFragmentEncoding(parsed.fragment)
346
489
  }
347
490
  }
348
491
 
349
492
  // perform scheme specific parsing
350
493
  if (schemeHandler && schemeHandler.parse) {
351
494
  schemeHandler.parse(parsed, options)
495
+ if (schemeHandler === SCHEMES.urn && parsed.nid === undefined) {
496
+ malformedSchemeSpecific = true
497
+ }
352
498
  }
353
499
  } else {
354
500
  parsed.error = parsed.error || 'URI can not be parsed.'
355
501
  }
356
- return { parsed, malformedAuthorityOrPort }
502
+ return { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme }
357
503
  }
358
504
 
359
505
  function parse (uri, opts) {
@@ -365,22 +511,30 @@ function normalizeString (uri, opts) {
365
511
  }
366
512
 
367
513
  function normalizeStringWithStatus (uri, opts) {
368
- const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts)
514
+ const { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = parseWithStatus(uri, opts)
369
515
  return {
370
- normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
371
- malformedAuthorityOrPort
516
+ normalized: malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? uri : serialize(parsed, opts),
517
+ malformedAuthorityOrPort,
518
+ malformedPercentEncoding,
519
+ malformedSchemeSpecific,
520
+ malformedHost,
521
+ malformedScheme
372
522
  }
373
523
  }
374
524
 
375
525
  function normalizeComparableURI (uri, opts) {
376
- if (typeof uri === 'string') {
377
- const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts)
378
- return malformedAuthorityOrPort ? undefined : normalized
526
+ if (typeof uri !== 'string' && typeof uri !== 'object') {
527
+ return undefined
379
528
  }
380
529
 
381
- if (typeof uri === 'object') {
382
- return serialize(uri, opts)
530
+ let value
531
+ try {
532
+ value = typeof uri === 'string' ? uri : serialize(uri, opts)
533
+ } catch {
534
+ return undefined
383
535
  }
536
+ const { normalized, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = normalizeStringWithStatus(value, opts)
537
+ return malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? undefined : normalized
384
538
  }
385
539
 
386
540
  const fastUri = {
package/lib/schemes.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const UUID_REG = /^[\da-f]{8}\b-[\da-f]{4}\b-[\da-f]{4}\b-[\da-f]{4}\b-[\da-f]{12}$/iu
4
- const URN_REG = /([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-.:;=@]|%[\da-f]{2})+)/iu
4
+ const URN_REG = /^([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-./:;=@]|%[\da-f]{2})+)$/iu
5
5
 
6
6
  function isSecure (wsComponents) {
7
7
  return typeof wsComponents.secure === 'boolean' ? wsComponents.secure : String(wsComponents.scheme).toLowerCase() === 'wss'
@@ -61,9 +61,14 @@ function wsSerialize (wsComponents) {
61
61
 
62
62
  // reconstruct path from resource name
63
63
  if (wsComponents.resourceName) {
64
- const [path, query] = wsComponents.resourceName.split('?')
64
+ const queryIndex = wsComponents.resourceName.indexOf('?')
65
+ const path = queryIndex === -1
66
+ ? wsComponents.resourceName
67
+ : wsComponents.resourceName.slice(0, queryIndex)
65
68
  wsComponents.path = (path && path !== '/' ? path : undefined)
66
- wsComponents.query = query
69
+ wsComponents.query = queryIndex === -1
70
+ ? undefined
71
+ : wsComponents.resourceName.slice(queryIndex + 1)
67
72
  wsComponents.resourceName = undefined
68
73
  }
69
74
 
@@ -79,7 +84,7 @@ function urnParse (urnComponents, options) {
79
84
  return urnComponents
80
85
  }
81
86
  const matches = urnComponents.path.match(URN_REG)
82
- if (matches) {
87
+ if (matches && matches[0] === urnComponents.path) {
83
88
  const scheme = options.scheme || urnComponents.scheme || 'urn'
84
89
  urnComponents.nid = matches[1].toLowerCase()
85
90
  urnComponents.nss = matches[2]