fast-uri 2.4.4 → 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/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,70 @@ 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
+ parsed.host[0] !== '[' &&
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
+ * @param {RegExpMatchArray} matches
317
+ * @returns {boolean}
318
+ */
319
+ function hasMalformedComponentPercentEncoding (matches) {
320
+ // Bracketed IP literals use a raw "%" as the zone separator for historical
321
+ // compatibility. Their parsing is intentionally left to normalizeIPv6.
322
+ const host = matches[4]
323
+ return hasMalformedPercentEncoding(matches[3]) ||
324
+ (host !== undefined && !(host[0] === '[' && host[host.length - 1] === ']') && hasMalformedPercentEncoding(host)) ||
325
+ hasMalformedPercentEncoding(matches[6]) ||
326
+ hasMalformedPercentEncoding(matches[7]) ||
327
+ hasMalformedPercentEncoding(matches[8])
328
+ }
329
+
214
330
  function parseWithStatus (uri, opts) {
215
331
  const options = Object.assign({}, opts)
216
332
  const parsed = {
@@ -222,8 +338,12 @@ function parseWithStatus (uri, opts) {
222
338
  query: undefined,
223
339
  fragment: undefined
224
340
  }
225
- const gotEncoding = uri.indexOf('%') !== -1
226
341
  let malformedAuthorityOrPort = false
342
+ let malformedPercentEncoding = false
343
+ let malformedSchemeSpecific = false
344
+ let malformedHost = false
345
+ let malformedIPLiteral = false
346
+ let malformedScheme = false
227
347
  let isIP = false
228
348
  if (options.reference === 'suffix') uri = (options.scheme ? options.scheme + ':' : '') + '//' + uri
229
349
 
@@ -274,6 +394,21 @@ function parseWithStatus (uri, opts) {
274
394
  parsed.query = matches[7]
275
395
  parsed.fragment = matches[8]
276
396
 
397
+ if (parsed.scheme !== undefined) {
398
+ const decodedScheme = unescape(parsed.scheme)
399
+ if (VALID_SCHEME.test(decodedScheme)) {
400
+ parsed.scheme = decodedScheme.toLowerCase()
401
+ } else {
402
+ parsed.error = parsed.error || MALFORMED_SCHEME_ERROR
403
+ malformedScheme = true
404
+ }
405
+ }
406
+
407
+ malformedPercentEncoding = hasMalformedComponentPercentEncoding(matches)
408
+ if (malformedPercentEncoding) {
409
+ parsed.error = parsed.error || 'URI contains malformed percent-encoding.'
410
+ }
411
+
277
412
  // fix port number
278
413
  if (isNaN(parsed.port)) {
279
414
  parsed.port = matches[5]
@@ -286,13 +421,19 @@ function parseWithStatus (uri, opts) {
286
421
  }
287
422
 
288
423
  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
424
+ const ipv4result = isIPv4(parsed.host)
425
+ if (ipv4result === false) {
426
+ const bracketedIPLiteral = parsed.host[0] === '[' && parsed.host[parsed.host.length - 1] === ']'
427
+ const ipv6result = normalizeIPv6(parsed.host)
428
+ isIP = ipv6result.isIPV6 || ipv6result.isIPVFuture === true
429
+ malformedIPLiteral = bracketedIPLiteral && ipv6result.error === true
430
+ parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase()
431
+
432
+ if (malformedIPLiteral) {
433
+ parsed.error = parsed.error || 'URI host is malformed.'
434
+ malformedAuthorityOrPort = true
435
+ }
294
436
  } else {
295
- parsed.host = ipv4result.host
296
437
  isIP = true
297
438
  }
298
439
  }
@@ -314,46 +455,36 @@ function parseWithStatus (uri, opts) {
314
455
  // find scheme handler
315
456
  const schemeHandler = SCHEMES[(options.scheme || parsed.scheme || '').toLowerCase()]
316
457
 
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
329
- }
458
+ // convert Unicode IDN -> ASCII IDN when the effective scheme uses domain hosts
459
+ malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP)
330
460
 
331
461
  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)
462
+ if (parsed.host !== undefined && !malformedIPLiteral) {
463
+ const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true)
464
+ parsed.host = reescapeHostDelimiters(host, isIP)
340
465
  }
341
466
  if (parsed.path !== undefined && parsed.path.length) {
342
467
  parsed.path = normalizePathEncoding(parsed.path)
343
468
  }
469
+ if (parsed.query !== undefined && parsed.query.length) {
470
+ parsed.query = normalizeQueryFragmentEncoding(parsed.query)
471
+ }
344
472
  if (parsed.fragment !== undefined && parsed.fragment.length) {
345
- parsed.fragment = encodeURI(decodeURI(parsed.fragment))
473
+ parsed.fragment = normalizeQueryFragmentEncoding(parsed.fragment)
346
474
  }
347
475
  }
348
476
 
349
477
  // perform scheme specific parsing
350
478
  if (schemeHandler && schemeHandler.parse) {
351
479
  schemeHandler.parse(parsed, options)
480
+ if (schemeHandler === SCHEMES.urn && parsed.nid === undefined) {
481
+ malformedSchemeSpecific = true
482
+ }
352
483
  }
353
484
  } else {
354
485
  parsed.error = parsed.error || 'URI can not be parsed.'
355
486
  }
356
- return { parsed, malformedAuthorityOrPort }
487
+ return { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme }
357
488
  }
358
489
 
359
490
  function parse (uri, opts) {
@@ -365,22 +496,30 @@ function normalizeString (uri, opts) {
365
496
  }
366
497
 
367
498
  function normalizeStringWithStatus (uri, opts) {
368
- const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts)
499
+ const { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = parseWithStatus(uri, opts)
369
500
  return {
370
- normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
371
- malformedAuthorityOrPort
501
+ normalized: malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? uri : serialize(parsed, opts),
502
+ malformedAuthorityOrPort,
503
+ malformedPercentEncoding,
504
+ malformedSchemeSpecific,
505
+ malformedHost,
506
+ malformedScheme
372
507
  }
373
508
  }
374
509
 
375
510
  function normalizeComparableURI (uri, opts) {
376
- if (typeof uri === 'string') {
377
- const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts)
378
- return malformedAuthorityOrPort ? undefined : normalized
511
+ if (typeof uri !== 'string' && typeof uri !== 'object') {
512
+ return undefined
379
513
  }
380
514
 
381
- if (typeof uri === 'object') {
382
- return serialize(uri, opts)
515
+ let value
516
+ try {
517
+ value = typeof uri === 'string' ? uri : serialize(uri, opts)
518
+ } catch {
519
+ return undefined
383
520
  }
521
+ const { normalized, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = normalizeStringWithStatus(value, opts)
522
+ return malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? undefined : normalized
384
523
  }
385
524
 
386
525
  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]