fast-uri 4.1.1 → 4.1.3
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/.github/workflows/ci.yml +9 -9
- package/.github/workflows/lock-threads.yml +1 -1
- package/.github/workflows/ossf-scorecard.yml +45 -0
- package/README.md +1 -0
- package/SECURITY.md +173 -0
- package/benchmark/benchmark.mjs +71 -0
- package/index.js +236 -42
- package/lib/schemes.js +384 -7
- package/lib/utils.js +302 -88
- package/package.json +1 -1
- package/test/component-safe-serialization.test.js +105 -0
- package/test/equal.test.js +43 -3
- package/test/fixtures/uri-js-parse.json +24 -1
- package/test/fixtures/uri-js-serialize.json +23 -1
- package/test/ipv6-canonical.test.js +34 -0
- package/test/ipv6-validation.test.js +90 -0
- package/test/malformed-percent.test.js +77 -0
- package/test/malformed-urn.test.js +61 -0
- package/test/parse.test.js +145 -0
- package/test/reserved-path-normalization.test.js +109 -0
- package/test/scheme-validation.test.js +122 -0
- package/test/security-normalization.test.js +101 -0
- package/test/security-utf8-encoding.test.js +0 -0
- package/test/security.test.js +211 -3
- package/test/serialize.test.js +144 -0
- package/test/uri-js-compatibility.test.js +0 -3
- package/test/uri-js.test.js +3 -3
- package/test/urn-full-input.test.js +29 -0
- package/test/websocket-query-preservation.test.js +24 -0
- package/types/index.d.ts +4 -0
- package/types/index.tst.ts +16 -0
package/index.js
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, normalizeQueryFragmentEncoding,
|
|
3
|
+
const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require('./lib/utils')
|
|
4
4
|
const { SCHEMES, getSchemeHandler } = 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
|
/**
|
|
7
22
|
* @template {import('./types/index').URIComponent|string} T
|
|
8
23
|
* @param {T} uri
|
|
@@ -26,7 +41,50 @@ function normalize (uri, options) {
|
|
|
26
41
|
*/
|
|
27
42
|
function resolve (baseURI, relativeURI, options) {
|
|
28
43
|
const schemelessOptions = options ? Object.assign({ scheme: 'null' }, options) : { scheme: 'null' }
|
|
29
|
-
const
|
|
44
|
+
const {
|
|
45
|
+
parsed: baseParsed,
|
|
46
|
+
malformedAuthorityOrPort: baseMalformed,
|
|
47
|
+
malformedPercentEncoding: baseMalformedPercentEncoding,
|
|
48
|
+
malformedSchemeSpecific: baseMalformedSchemeSpecific,
|
|
49
|
+
malformedHost: baseMalformedHost,
|
|
50
|
+
malformedScheme: baseMalformedScheme
|
|
51
|
+
} = parseWithStatus(baseURI, schemelessOptions)
|
|
52
|
+
const {
|
|
53
|
+
parsed: relativeParsed,
|
|
54
|
+
malformedAuthorityOrPort: relativeMalformed,
|
|
55
|
+
malformedPercentEncoding: relativeMalformedPercentEncoding,
|
|
56
|
+
malformedSchemeSpecific: relativeMalformedSchemeSpecific,
|
|
57
|
+
malformedHost: relativeMalformedHost,
|
|
58
|
+
malformedScheme: relativeMalformedScheme
|
|
59
|
+
} = parseWithStatus(relativeURI, schemelessOptions)
|
|
60
|
+
if (
|
|
61
|
+
baseMalformed ||
|
|
62
|
+
relativeMalformed ||
|
|
63
|
+
baseMalformedPercentEncoding ||
|
|
64
|
+
relativeMalformedPercentEncoding ||
|
|
65
|
+
baseMalformedSchemeSpecific ||
|
|
66
|
+
relativeMalformedSchemeSpecific ||
|
|
67
|
+
baseMalformedHost ||
|
|
68
|
+
relativeMalformedHost ||
|
|
69
|
+
baseMalformedScheme ||
|
|
70
|
+
relativeMalformedScheme
|
|
71
|
+
) {
|
|
72
|
+
throw new Error(baseParsed.error || relativeParsed.error || 'URI is malformed.')
|
|
73
|
+
}
|
|
74
|
+
const resolved = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true)
|
|
75
|
+
const resolvedSchemeHandler = getSchemeHandler((options && options.scheme) || resolved.scheme)
|
|
76
|
+
const resolvedHost = resolved.host
|
|
77
|
+
const resolvedHostIsIP = resolvedHost !== undefined && resolvedHost !== '' &&
|
|
78
|
+
(isIPv4(resolvedHost) || normalizeIPv6(resolvedHost).isIPV6)
|
|
79
|
+
canonicalizeHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP)
|
|
80
|
+
// Percent escapes in an ASCII reg-name are encoded data. The WHATWG hostname
|
|
81
|
+
// parser can reject them even though fast-uri preserves them safely as RFC
|
|
82
|
+
// 3986 data. A raw non-ASCII host must still fail closed if conversion fails.
|
|
83
|
+
const encodedASCIIHost = resolvedHost && resolvedHost.indexOf('%') !== -1 &&
|
|
84
|
+
!/\P{ASCII}/u.test(resolvedHost)
|
|
85
|
+
if (resolved.error && !encodedASCIIHost) {
|
|
86
|
+
throw new Error(resolved.error)
|
|
87
|
+
}
|
|
30
88
|
schemelessOptions.skipEscape = true
|
|
31
89
|
return serialize(resolved, schemelessOptions)
|
|
32
90
|
}
|
|
@@ -109,7 +167,7 @@ function equal (uriA, uriB, options) {
|
|
|
109
167
|
const normalizedA = normalizeComparableURI(uriA, options)
|
|
110
168
|
const normalizedB = normalizeComparableURI(uriB, options)
|
|
111
169
|
|
|
112
|
-
return normalizedA !== undefined && normalizedB !== undefined && normalizedA
|
|
170
|
+
return normalizedA !== undefined && normalizedB !== undefined && normalizedA === normalizedB
|
|
113
171
|
}
|
|
114
172
|
|
|
115
173
|
/**
|
|
@@ -132,30 +190,39 @@ function serialize (cmpts, opts) {
|
|
|
132
190
|
reference: cmpts.reference,
|
|
133
191
|
resourceName: cmpts.resourceName,
|
|
134
192
|
secure: cmpts.secure,
|
|
193
|
+
to: cmpts.to,
|
|
194
|
+
subject: cmpts.subject,
|
|
195
|
+
body: cmpts.body,
|
|
196
|
+
headers: cmpts.headers,
|
|
135
197
|
error: ''
|
|
136
198
|
}
|
|
137
199
|
const options = Object.assign({}, opts)
|
|
138
200
|
const uriTokens = []
|
|
139
201
|
|
|
202
|
+
if (component.scheme) {
|
|
203
|
+
component.scheme = decodeValidScheme(component.scheme)
|
|
204
|
+
}
|
|
205
|
+
|
|
140
206
|
// find scheme handler
|
|
141
207
|
const schemeHandler = getSchemeHandler(options.scheme || component.scheme)
|
|
142
208
|
|
|
143
209
|
// perform scheme specific serialization
|
|
144
210
|
if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options)
|
|
145
211
|
|
|
212
|
+
const hasAuthority = component.userinfo !== undefined || component.host !== undefined || component.port !== undefined
|
|
213
|
+
const pathNoScheme = !options.skipEscape && component.scheme === undefined && !hasAuthority
|
|
214
|
+
|
|
146
215
|
if (component.path !== undefined) {
|
|
147
216
|
if (!options.skipEscape) {
|
|
148
|
-
component.path =
|
|
149
|
-
|
|
150
|
-
if (component.scheme !== undefined) {
|
|
151
|
-
component.path = component.path.split('%3A').join(':')
|
|
152
|
-
}
|
|
217
|
+
component.path = serializePathEncoding(component.path, pathNoScheme)
|
|
153
218
|
} else {
|
|
154
219
|
component.path = normalizePercentEncoding(component.path)
|
|
155
220
|
}
|
|
156
221
|
}
|
|
157
222
|
|
|
158
223
|
if (options.reference !== 'suffix' && component.scheme) {
|
|
224
|
+
// Scheme handlers may replace the scheme during serialization.
|
|
225
|
+
component.scheme = decodeValidScheme(component.scheme)
|
|
159
226
|
uriTokens.push(component.scheme, ':')
|
|
160
227
|
}
|
|
161
228
|
|
|
@@ -178,6 +245,13 @@ function serialize (cmpts, opts) {
|
|
|
178
245
|
s = removeDotSegments(s)
|
|
179
246
|
}
|
|
180
247
|
|
|
248
|
+
// Dot-segment removal can expose a colon that was not originally in the
|
|
249
|
+
// first segment (for example, "./a:b"). Reapply path-noscheme encoding so
|
|
250
|
+
// the serialized relative reference cannot be reparsed as a URI scheme.
|
|
251
|
+
if (pathNoScheme) {
|
|
252
|
+
s = serializePathEncoding(s, true)
|
|
253
|
+
}
|
|
254
|
+
|
|
181
255
|
if (
|
|
182
256
|
authority === undefined &&
|
|
183
257
|
s[0] === '/' &&
|
|
@@ -191,11 +265,11 @@ function serialize (cmpts, opts) {
|
|
|
191
265
|
}
|
|
192
266
|
|
|
193
267
|
if (component.query !== undefined) {
|
|
194
|
-
uriTokens.push('?', component.query)
|
|
268
|
+
uriTokens.push('?', encodeQuery(component.query))
|
|
195
269
|
}
|
|
196
270
|
|
|
197
271
|
if (component.fragment !== undefined) {
|
|
198
|
-
uriTokens.push('#', component.fragment)
|
|
272
|
+
uriTokens.push('#', encodeFragment(component.fragment))
|
|
199
273
|
}
|
|
200
274
|
return uriTokens.join('')
|
|
201
275
|
}
|
|
@@ -206,6 +280,15 @@ const URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:
|
|
|
206
280
|
// with or without a scheme prefix, for the literal-backslash rejection below.
|
|
207
281
|
const AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/
|
|
208
282
|
|
|
283
|
+
// Captures the leading authority-introducer region after an optional scheme: a
|
|
284
|
+
// run of forward slashes, backslashes, and the characters the WHATWG URL parser
|
|
285
|
+
// removes before parsing (TAB U+0009, LF U+000A, CR U+000D). A valid introducer
|
|
286
|
+
// is exactly "//". Node treats "\" as "/" on special schemes and strips those
|
|
287
|
+
// characters first, so forms like "\\", "/\", "\/", "/<TAB>/", or a leading
|
|
288
|
+
// "<TAB>//" reach an authority in Node while fast-uri's URI_PARSE folds them into
|
|
289
|
+
// the path group (host confusion / SSRF / redirect bypass).
|
|
290
|
+
const AUTHORITY_INTRODUCER_REGION = /^(?:[^#/:?]+:)?([/\\\t\n\r]*)/
|
|
291
|
+
|
|
209
292
|
/**
|
|
210
293
|
* @param {import('./types/index').URIComponent} parsed
|
|
211
294
|
* @param {RegExpMatchArray} matches
|
|
@@ -223,10 +306,74 @@ function getParseError (parsed, matches) {
|
|
|
223
306
|
return undefined
|
|
224
307
|
}
|
|
225
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Checks percent syntax without decoding the represented octets. RFC 3986
|
|
311
|
+
* percent-encoding is byte-oriented, so sequences such as `%FF` are valid even
|
|
312
|
+
* though they are not independently valid UTF-8.
|
|
313
|
+
*
|
|
314
|
+
* @param {string|undefined} component
|
|
315
|
+
* @returns {boolean}
|
|
316
|
+
*/
|
|
317
|
+
function hasMalformedPercentEncoding (component) {
|
|
318
|
+
if (component === undefined) return false
|
|
319
|
+
|
|
320
|
+
let percent = component.indexOf('%')
|
|
321
|
+
while (percent !== -1) {
|
|
322
|
+
if (percent + 2 >= component.length || !/^[\da-f]{2}$/iu.test(component.slice(percent + 1, percent + 3))) {
|
|
323
|
+
return true
|
|
324
|
+
}
|
|
325
|
+
percent = component.indexOf('%', percent + 3)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return false
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* @param {RegExpMatchArray} matches
|
|
333
|
+
* @returns {boolean}
|
|
334
|
+
*/
|
|
335
|
+
function hasMalformedComponentPercentEncoding (matches) {
|
|
336
|
+
// Bracketed IP literals use a raw "%" as the zone separator for historical
|
|
337
|
+
// compatibility. Their parsing is intentionally left to normalizeIPv6.
|
|
338
|
+
const host = matches[4]
|
|
339
|
+
return hasMalformedPercentEncoding(matches[3]) ||
|
|
340
|
+
(host !== undefined && !(host[0] === '[' && host[host.length - 1] === ']') && hasMalformedPercentEncoding(host)) ||
|
|
341
|
+
hasMalformedPercentEncoding(matches[6]) ||
|
|
342
|
+
hasMalformedPercentEncoding(matches[7]) ||
|
|
343
|
+
hasMalformedPercentEncoding(matches[8])
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* @param {import('./types/index').URIComponent} parsed
|
|
348
|
+
* @param {import('./types/index').Options} options
|
|
349
|
+
* @param {{ domainHost?: boolean, unicodeSupport?: boolean }|undefined} schemeHandler
|
|
350
|
+
* @param {boolean} isIP
|
|
351
|
+
* @returns {boolean} whether host conversion failed
|
|
352
|
+
*/
|
|
353
|
+
function canonicalizeHost (parsed, options, schemeHandler, isIP) {
|
|
354
|
+
if (
|
|
355
|
+
!options.unicodeSupport &&
|
|
356
|
+
(!schemeHandler || !schemeHandler.unicodeSupport) &&
|
|
357
|
+
parsed.host &&
|
|
358
|
+
parsed.host[0] !== '[' &&
|
|
359
|
+
(options.domainHost || (schemeHandler && schemeHandler.domainHost)) &&
|
|
360
|
+
isIP === false &&
|
|
361
|
+
nonSimpleDomain(parsed.host)
|
|
362
|
+
) {
|
|
363
|
+
try {
|
|
364
|
+
parsed.host = new URL('http://' + parsed.host).hostname
|
|
365
|
+
} catch (e) {
|
|
366
|
+
parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e
|
|
367
|
+
return true
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return false
|
|
371
|
+
}
|
|
372
|
+
|
|
226
373
|
/**
|
|
227
374
|
* @param {string} uri
|
|
228
375
|
* @param {import('./types/index').Options} [opts]
|
|
229
|
-
* @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean }}
|
|
376
|
+
* @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
|
|
230
377
|
*/
|
|
231
378
|
function parseWithStatus (uri, opts) {
|
|
232
379
|
const options = Object.assign({}, opts)
|
|
@@ -242,6 +389,11 @@ function parseWithStatus (uri, opts) {
|
|
|
242
389
|
}
|
|
243
390
|
|
|
244
391
|
let malformedAuthorityOrPort = false
|
|
392
|
+
let malformedPercentEncoding = false
|
|
393
|
+
let malformedSchemeSpecific = false
|
|
394
|
+
let malformedHost = false
|
|
395
|
+
let malformedIPLiteral = false
|
|
396
|
+
let malformedScheme = false
|
|
245
397
|
|
|
246
398
|
let isIP = false
|
|
247
399
|
if (options.reference === 'suffix') {
|
|
@@ -265,11 +417,33 @@ function parseWithStatus (uri, opts) {
|
|
|
265
417
|
malformedAuthorityOrPort = true
|
|
266
418
|
}
|
|
267
419
|
|
|
420
|
+
// Reject a malformed or whitespace-smuggled authority introducer. fast-uri
|
|
421
|
+
// only recognizes a literal "//"; anything else in the leading separator run
|
|
422
|
+
// (a backslash, or a "//" that appears only after removing the TAB/LF/CR that
|
|
423
|
+
// Node strips) means the authority fast-uri parses differs from the one Node's
|
|
424
|
+
// URL resolves. Reject rather than rewrite, mirroring the literal-backslash
|
|
425
|
+
// guard above. Percent-encoded forms (%5C, %09) are untouched, valid data.
|
|
426
|
+
const introducerMatch = uri.match(AUTHORITY_INTRODUCER_REGION)
|
|
427
|
+
if (introducerMatch !== null) {
|
|
428
|
+
const region = introducerMatch[1]
|
|
429
|
+
const normalizedRegion = region.replace(/[\t\n\r]/g, '')
|
|
430
|
+
// Two or more leading separators introduce an authority.
|
|
431
|
+
if (normalizedRegion.length >= 2) {
|
|
432
|
+
if (normalizedRegion.slice(0, 2) !== '//') {
|
|
433
|
+
parsed.error = parsed.error || 'URI authority must not contain a literal backslash.'
|
|
434
|
+
malformedAuthorityOrPort = true
|
|
435
|
+
} else if (region.length !== normalizedRegion.length) {
|
|
436
|
+
parsed.error = parsed.error || 'URI authority introducer must not contain whitespace.'
|
|
437
|
+
malformedAuthorityOrPort = true
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
268
442
|
const matches = uri.match(URI_PARSE)
|
|
269
443
|
|
|
270
444
|
if (matches) {
|
|
271
445
|
// store each component
|
|
272
|
-
parsed.scheme = matches[1]
|
|
446
|
+
parsed.scheme = matches[1]
|
|
273
447
|
parsed.userinfo = matches[3]
|
|
274
448
|
parsed.host = matches[4]
|
|
275
449
|
parsed.port = parseInt(matches[5], 10)
|
|
@@ -277,6 +451,21 @@ function parseWithStatus (uri, opts) {
|
|
|
277
451
|
parsed.query = matches[7]
|
|
278
452
|
parsed.fragment = matches[8]
|
|
279
453
|
|
|
454
|
+
if (parsed.scheme !== undefined) {
|
|
455
|
+
const decodedScheme = unescape(parsed.scheme)
|
|
456
|
+
if (VALID_SCHEME.test(decodedScheme)) {
|
|
457
|
+
parsed.scheme = decodedScheme.toLowerCase()
|
|
458
|
+
} else {
|
|
459
|
+
parsed.error = parsed.error || MALFORMED_SCHEME_ERROR
|
|
460
|
+
malformedScheme = true
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
malformedPercentEncoding = hasMalformedComponentPercentEncoding(matches)
|
|
465
|
+
if (malformedPercentEncoding) {
|
|
466
|
+
parsed.error = parsed.error || 'URI contains malformed percent-encoding.'
|
|
467
|
+
}
|
|
468
|
+
|
|
280
469
|
// fix port number
|
|
281
470
|
if (isNaN(parsed.port)) {
|
|
282
471
|
parsed.port = matches[5]
|
|
@@ -291,9 +480,16 @@ function parseWithStatus (uri, opts) {
|
|
|
291
480
|
if (parsed.host) {
|
|
292
481
|
const ipv4result = isIPv4(parsed.host)
|
|
293
482
|
if (ipv4result === false) {
|
|
483
|
+
const bracketedIPLiteral = parsed.host[0] === '[' && parsed.host[parsed.host.length - 1] === ']'
|
|
294
484
|
const ipv6result = normalizeIPv6(parsed.host)
|
|
295
|
-
|
|
296
|
-
|
|
485
|
+
isIP = ipv6result.isIPV6 || ipv6result.isIPVFuture === true
|
|
486
|
+
malformedIPLiteral = bracketedIPLiteral && ipv6result.error === true
|
|
487
|
+
parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase()
|
|
488
|
+
|
|
489
|
+
if (malformedIPLiteral) {
|
|
490
|
+
parsed.error = parsed.error || 'URI host is malformed.'
|
|
491
|
+
malformedAuthorityOrPort = true
|
|
492
|
+
}
|
|
297
493
|
} else {
|
|
298
494
|
isIP = true
|
|
299
495
|
}
|
|
@@ -316,27 +512,14 @@ function parseWithStatus (uri, opts) {
|
|
|
316
512
|
// find scheme handler
|
|
317
513
|
const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme)
|
|
318
514
|
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
// if host component is a domain name
|
|
322
|
-
if (parsed.host && (options.domainHost || (schemeHandler && schemeHandler.domainHost)) && isIP === false && nonSimpleDomain(parsed.host)) {
|
|
323
|
-
// convert Unicode IDN -> ASCII IDN
|
|
324
|
-
try {
|
|
325
|
-
parsed.host = new URL('http://' + parsed.host).hostname
|
|
326
|
-
} catch (e) {
|
|
327
|
-
parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
// convert IRI -> URI
|
|
331
|
-
}
|
|
515
|
+
// convert Unicode IDN -> ASCII IDN when the effective scheme uses domain hosts
|
|
516
|
+
malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP)
|
|
332
517
|
|
|
333
518
|
if (!schemeHandler || (schemeHandler && !schemeHandler.skipNormalize)) {
|
|
334
519
|
if (uri.indexOf('%') !== -1) {
|
|
335
|
-
if (parsed.
|
|
336
|
-
parsed.
|
|
337
|
-
|
|
338
|
-
if (parsed.host !== undefined) {
|
|
339
|
-
parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP)
|
|
520
|
+
if (parsed.host !== undefined && !malformedIPLiteral) {
|
|
521
|
+
const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true)
|
|
522
|
+
parsed.host = reescapeHostDelimiters(host, isIP)
|
|
340
523
|
}
|
|
341
524
|
}
|
|
342
525
|
if (parsed.path) {
|
|
@@ -353,11 +536,14 @@ function parseWithStatus (uri, opts) {
|
|
|
353
536
|
// perform scheme specific parsing
|
|
354
537
|
if (schemeHandler && schemeHandler.parse) {
|
|
355
538
|
schemeHandler.parse(parsed, options)
|
|
539
|
+
if (schemeHandler === SCHEMES.urn && parsed.nid === undefined) {
|
|
540
|
+
malformedSchemeSpecific = true
|
|
541
|
+
}
|
|
356
542
|
}
|
|
357
543
|
} else {
|
|
358
544
|
parsed.error = parsed.error || 'URI can not be parsed.'
|
|
359
545
|
}
|
|
360
|
-
return { parsed, malformedAuthorityOrPort }
|
|
546
|
+
return { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme }
|
|
361
547
|
}
|
|
362
548
|
|
|
363
549
|
/**
|
|
@@ -381,13 +567,17 @@ function normalizeString (uri, opts) {
|
|
|
381
567
|
/**
|
|
382
568
|
* @param {string} uri
|
|
383
569
|
* @param {import('./types/index').Options} [opts]
|
|
384
|
-
* @returns {{ normalized: string, malformedAuthorityOrPort: boolean }}
|
|
570
|
+
* @returns {{ normalized: string, malformedAuthorityOrPort: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
|
|
385
571
|
*/
|
|
386
572
|
function normalizeStringWithStatus (uri, opts) {
|
|
387
|
-
const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts)
|
|
573
|
+
const { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = parseWithStatus(uri, opts)
|
|
388
574
|
return {
|
|
389
|
-
normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
|
|
390
|
-
malformedAuthorityOrPort
|
|
575
|
+
normalized: malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? uri : serialize(parsed, opts),
|
|
576
|
+
malformedAuthorityOrPort,
|
|
577
|
+
malformedPercentEncoding,
|
|
578
|
+
malformedSchemeSpecific,
|
|
579
|
+
malformedHost,
|
|
580
|
+
malformedScheme
|
|
391
581
|
}
|
|
392
582
|
}
|
|
393
583
|
|
|
@@ -397,14 +587,18 @@ function normalizeStringWithStatus (uri, opts) {
|
|
|
397
587
|
* @returns {string|undefined}
|
|
398
588
|
*/
|
|
399
589
|
function normalizeComparableURI (uri, opts) {
|
|
400
|
-
if (typeof uri
|
|
401
|
-
|
|
402
|
-
return malformedAuthorityOrPort ? undefined : normalized
|
|
590
|
+
if (typeof uri !== 'string' && typeof uri !== 'object') {
|
|
591
|
+
return undefined
|
|
403
592
|
}
|
|
404
593
|
|
|
405
|
-
|
|
406
|
-
|
|
594
|
+
let value
|
|
595
|
+
try {
|
|
596
|
+
value = typeof uri === 'string' ? uri : serialize(uri, opts)
|
|
597
|
+
} catch {
|
|
598
|
+
return undefined
|
|
407
599
|
}
|
|
600
|
+
const { normalized, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = normalizeStringWithStatus(value, opts)
|
|
601
|
+
return malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? undefined : normalized
|
|
408
602
|
}
|
|
409
603
|
|
|
410
604
|
const fastUri = {
|