fast-uri 4.2.0 → 4.2.2
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/release.yml +24 -0
- package/README.md +1 -1
- package/index.js +54 -14
- package/lib/schemes.js +34 -1
- package/lib/utils.js +35 -21
- package/package.json +2 -2
- package/test/c0-reference-security.test.js +137 -0
- package/test/host-encoding.test.js +60 -0
- package/test/mailto-authority-security.test.js +125 -0
- package/test/parse.test.js +7 -5
- package/test/security.test.js +2 -2
- package/test/uri-js.test.js +7 -5
- package/test/urn-full-input.test.js +18 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Release the package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
semver:
|
|
7
|
+
description: 'Release bump type'
|
|
8
|
+
required: true
|
|
9
|
+
type: choice
|
|
10
|
+
options:
|
|
11
|
+
- patch
|
|
12
|
+
- minor
|
|
13
|
+
- major
|
|
14
|
+
|
|
15
|
+
permissions: {}
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
release:
|
|
19
|
+
permissions:
|
|
20
|
+
id-token: write # required for npm provenance via OIDC
|
|
21
|
+
contents: write # required to push the release commit and the tag
|
|
22
|
+
uses: fastify/workflows/.github/workflows/reusable-release.yml@b1a11bce713bc857e52b2231fec3c89071ad3135
|
|
23
|
+
with:
|
|
24
|
+
semver: ${{ inputs.semver }}
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Dependency-free RFC 3986 URI toolbox.
|
|
|
13
13
|
|
|
14
14
|
All of the above functions can accept an additional options argument that is an object that can contain one or more of the following properties:
|
|
15
15
|
|
|
16
|
-
Malformed authorities
|
|
16
|
+
Malformed authorities, out-of-range ports, and raw leading or trailing C0 controls or spaces (U+0000–U+0020) are reported through the parsed component's `error` field. `normalize()` leaves these malformed string inputs unchanged, and `equal()` returns `false`. `resolve()` rejects them in either input and rejects HTTP(S) results with an empty host. Percent-encoded and interior component data remain valid.
|
|
17
17
|
|
|
18
18
|
* `scheme` (string)
|
|
19
19
|
Indicates the scheme that the URI should be treated as, overriding the URI's normal scheme parsing behavior.
|
package/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment,
|
|
3
|
+
const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, encodeHost, isIPv4, nonSimpleDomain } = require('./lib/utils')
|
|
4
4
|
const { SCHEMES, getSchemeHandler } = require('./lib/schemes')
|
|
5
5
|
|
|
6
6
|
const VALID_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*$/u
|
|
@@ -44,6 +44,7 @@ function resolve (baseURI, relativeURI, options) {
|
|
|
44
44
|
const {
|
|
45
45
|
parsed: baseParsed,
|
|
46
46
|
malformedAuthorityOrPort: baseMalformed,
|
|
47
|
+
malformedReferenceEdge: baseMalformedReferenceEdge,
|
|
47
48
|
malformedPercentEncoding: baseMalformedPercentEncoding,
|
|
48
49
|
malformedSchemeSpecific: baseMalformedSchemeSpecific,
|
|
49
50
|
malformedHost: baseMalformedHost,
|
|
@@ -52,6 +53,7 @@ function resolve (baseURI, relativeURI, options) {
|
|
|
52
53
|
const {
|
|
53
54
|
parsed: relativeParsed,
|
|
54
55
|
malformedAuthorityOrPort: relativeMalformed,
|
|
56
|
+
malformedReferenceEdge: relativeMalformedReferenceEdge,
|
|
55
57
|
malformedPercentEncoding: relativeMalformedPercentEncoding,
|
|
56
58
|
malformedSchemeSpecific: relativeMalformedSchemeSpecific,
|
|
57
59
|
malformedHost: relativeMalformedHost,
|
|
@@ -60,6 +62,8 @@ function resolve (baseURI, relativeURI, options) {
|
|
|
60
62
|
if (
|
|
61
63
|
baseMalformed ||
|
|
62
64
|
relativeMalformed ||
|
|
65
|
+
baseMalformedReferenceEdge ||
|
|
66
|
+
relativeMalformedReferenceEdge ||
|
|
63
67
|
baseMalformedPercentEncoding ||
|
|
64
68
|
relativeMalformedPercentEncoding ||
|
|
65
69
|
baseMalformedSchemeSpecific ||
|
|
@@ -74,6 +78,14 @@ function resolve (baseURI, relativeURI, options) {
|
|
|
74
78
|
const resolved = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true)
|
|
75
79
|
const resolvedSchemeHandler = getSchemeHandler((options && options.scheme) || resolved.scheme)
|
|
76
80
|
const resolvedHost = resolved.host
|
|
81
|
+
// Inputs are parsed scheme-neutrally during resolution, so enforce the final
|
|
82
|
+
// HTTP scheme's required host before serializing an invalid HTTP URI.
|
|
83
|
+
if (
|
|
84
|
+
(resolved.scheme === 'http' || resolved.scheme === 'https') &&
|
|
85
|
+
!resolvedHost
|
|
86
|
+
) {
|
|
87
|
+
throw new Error('HTTP URIs must have a host.')
|
|
88
|
+
}
|
|
77
89
|
const resolvedHostIsIP = resolvedHost !== undefined && resolvedHost !== '' &&
|
|
78
90
|
(isIPv4(resolvedHost) || normalizeIPv6(resolvedHost).isIPV6)
|
|
79
91
|
canonicalizeHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP)
|
|
@@ -86,6 +98,9 @@ function resolve (baseURI, relativeURI, options) {
|
|
|
86
98
|
throw new Error(resolved.error)
|
|
87
99
|
}
|
|
88
100
|
schemelessOptions.skipEscape = true
|
|
101
|
+
if (resolvedSchemeHandler && resolvedSchemeHandler.absolutePath) {
|
|
102
|
+
schemelessOptions.absolutePath = true
|
|
103
|
+
}
|
|
89
104
|
return serialize(resolved, schemelessOptions)
|
|
90
105
|
}
|
|
91
106
|
|
|
@@ -127,7 +142,10 @@ function resolveComponent (base, relative, options, skipNormalization) {
|
|
|
127
142
|
target.userinfo = relative.userinfo
|
|
128
143
|
copyHost(target, relative)
|
|
129
144
|
target.port = relative.port
|
|
130
|
-
|
|
145
|
+
const relativeSchemeHandler = getSchemeHandler(relative.scheme)
|
|
146
|
+
target.path = relativeSchemeHandler && relativeSchemeHandler.absolutePath
|
|
147
|
+
? relative.path || ''
|
|
148
|
+
: removeDotSegments(relative.path || '')
|
|
131
149
|
target.query = relative.query
|
|
132
150
|
} else {
|
|
133
151
|
if (relative.userinfo !== undefined || relative.host !== undefined || relative.port !== undefined) {
|
|
@@ -243,7 +261,8 @@ function serialize (cmpts, opts) {
|
|
|
243
261
|
uriTokens.push(component.scheme, ':')
|
|
244
262
|
}
|
|
245
263
|
|
|
246
|
-
const
|
|
264
|
+
const allowNonAsciiHost = options.unicodeSupport === true || Boolean(schemeHandler && schemeHandler.unicodeSupport)
|
|
265
|
+
const authority = recomposeAuthority(component, allowNonAsciiHost)
|
|
247
266
|
if (authority !== undefined) {
|
|
248
267
|
if (options.reference !== 'suffix') {
|
|
249
268
|
uriTokens.push('//')
|
|
@@ -409,7 +428,7 @@ function canonicalizeHost (parsed, options, schemeHandler, isIP) {
|
|
|
409
428
|
/**
|
|
410
429
|
* @param {string} uri
|
|
411
430
|
* @param {import('./types/index').Options} [opts]
|
|
412
|
-
* @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
|
|
431
|
+
* @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean, malformedReferenceEdge: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
|
|
413
432
|
*/
|
|
414
433
|
function parseWithStatus (uri, opts) {
|
|
415
434
|
const options = Object.assign({}, opts)
|
|
@@ -425,6 +444,12 @@ function parseWithStatus (uri, opts) {
|
|
|
425
444
|
}
|
|
426
445
|
|
|
427
446
|
let malformedAuthorityOrPort = false
|
|
447
|
+
// Check the caller's original reference before suffix mode prepends a scheme
|
|
448
|
+
// or authority marker and turns its leading edge into interior data.
|
|
449
|
+
const malformedReferenceEdge = uri.length !== 0 && (
|
|
450
|
+
uri.charCodeAt(0) <= 0x20 ||
|
|
451
|
+
uri.charCodeAt(uri.length - 1) <= 0x20
|
|
452
|
+
)
|
|
428
453
|
let malformedPercentEncoding = false
|
|
429
454
|
let malformedSchemeSpecific = false
|
|
430
455
|
let malformedHost = false
|
|
@@ -475,6 +500,10 @@ function parseWithStatus (uri, opts) {
|
|
|
475
500
|
}
|
|
476
501
|
}
|
|
477
502
|
|
|
503
|
+
if (malformedReferenceEdge) {
|
|
504
|
+
parsed.error = parsed.error || 'URI must not contain leading or trailing C0 controls or spaces.'
|
|
505
|
+
}
|
|
506
|
+
|
|
478
507
|
const matches = uri.match(URI_PARSE)
|
|
479
508
|
|
|
480
509
|
if (matches) {
|
|
@@ -564,14 +593,24 @@ function parseWithStatus (uri, opts) {
|
|
|
564
593
|
malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP)
|
|
565
594
|
}
|
|
566
595
|
|
|
567
|
-
if (
|
|
568
|
-
let host =
|
|
569
|
-
if (!isIP) {
|
|
596
|
+
if (parsed.host !== undefined && !malformedIPLiteral) {
|
|
597
|
+
let host = parsed.host
|
|
598
|
+
if (uri.indexOf('%') !== -1 && !isIP) {
|
|
599
|
+
host = normalizePercentEncoding(host, true)
|
|
570
600
|
// Fold reg-name case after decoding unreserved octets. The second
|
|
571
601
|
// pass only restores uppercase hex in escapes that remain encoded.
|
|
572
602
|
host = normalizePercentEncoding(host.toLowerCase())
|
|
573
603
|
}
|
|
574
|
-
|
|
604
|
+
// Resolution parses both inputs with the internal `null` scheme before
|
|
605
|
+
// the effective scheme is known. Preserve raw host characters for that
|
|
606
|
+
// intermediate step so the final scheme can apply domain canonicalization;
|
|
607
|
+
// the resolved authority is encoded when it is serialized.
|
|
608
|
+
if (options.scheme !== 'null') {
|
|
609
|
+
const allowNonAsciiHost = options.unicodeSupport === true || Boolean(schemeHandler && schemeHandler.unicodeSupport)
|
|
610
|
+
parsed.host = encodeHost(host, isIP, allowNonAsciiHost)
|
|
611
|
+
} else {
|
|
612
|
+
parsed.host = host
|
|
613
|
+
}
|
|
575
614
|
}
|
|
576
615
|
|
|
577
616
|
if (!schemeHandler || (schemeHandler && !schemeHandler.skipNormalize)) {
|
|
@@ -596,7 +635,7 @@ function parseWithStatus (uri, opts) {
|
|
|
596
635
|
} else {
|
|
597
636
|
parsed.error = parsed.error || 'URI can not be parsed.'
|
|
598
637
|
}
|
|
599
|
-
return { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme }
|
|
638
|
+
return { parsed, malformedAuthorityOrPort, malformedReferenceEdge, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme }
|
|
600
639
|
}
|
|
601
640
|
|
|
602
641
|
/**
|
|
@@ -620,13 +659,14 @@ function normalizeString (uri, opts) {
|
|
|
620
659
|
/**
|
|
621
660
|
* @param {string} uri
|
|
622
661
|
* @param {import('./types/index').Options} [opts]
|
|
623
|
-
* @returns {{ normalized: string, malformedAuthorityOrPort: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
|
|
662
|
+
* @returns {{ normalized: string, malformedAuthorityOrPort: boolean, malformedReferenceEdge: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
|
|
624
663
|
*/
|
|
625
664
|
function normalizeStringWithStatus (uri, opts) {
|
|
626
|
-
const { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = parseWithStatus(uri, opts)
|
|
665
|
+
const { parsed, malformedAuthorityOrPort, malformedReferenceEdge, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = parseWithStatus(uri, opts)
|
|
627
666
|
return {
|
|
628
|
-
normalized: malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? uri : serialize(parsed, opts),
|
|
667
|
+
normalized: malformedAuthorityOrPort || malformedReferenceEdge || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? uri : serialize(parsed, opts),
|
|
629
668
|
malformedAuthorityOrPort,
|
|
669
|
+
malformedReferenceEdge,
|
|
630
670
|
malformedPercentEncoding,
|
|
631
671
|
malformedSchemeSpecific,
|
|
632
672
|
malformedHost,
|
|
@@ -650,8 +690,8 @@ function normalizeComparableURI (uri, opts) {
|
|
|
650
690
|
} catch {
|
|
651
691
|
return undefined
|
|
652
692
|
}
|
|
653
|
-
const { normalized, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = normalizeStringWithStatus(value, opts)
|
|
654
|
-
return malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? undefined : normalized
|
|
693
|
+
const { normalized, malformedAuthorityOrPort, malformedReferenceEdge, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = normalizeStringWithStatus(value, opts)
|
|
694
|
+
return malformedAuthorityOrPort || malformedReferenceEdge || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? undefined : normalized
|
|
655
695
|
}
|
|
656
696
|
|
|
657
697
|
const fastUri = {
|
package/lib/schemes.js
CHANGED
|
@@ -230,7 +230,10 @@ const urn = /** @type {SchemeHandler} */ ({
|
|
|
230
230
|
scheme: 'urn',
|
|
231
231
|
parse: urnParse,
|
|
232
232
|
serialize: urnSerialize,
|
|
233
|
-
skipNormalize: true
|
|
233
|
+
skipNormalize: true,
|
|
234
|
+
// The NSS is opaque to generic URI path processing. In particular, RFC 2141
|
|
235
|
+
// and RFC 8141 do not treat slash-delimited dot segments as equivalent.
|
|
236
|
+
absolutePath: true
|
|
234
237
|
})
|
|
235
238
|
|
|
236
239
|
const urnuuid = /** @type {SchemeHandler} */ ({
|
|
@@ -290,6 +293,7 @@ const DTEXT_IRI = allowSet(DTEXT_ALLOWED, true)
|
|
|
290
293
|
const HEX_PAIR = /^[\da-f]{2}$/iu
|
|
291
294
|
const MAILTO_DOMAIN_LITERAL = /^\[[\x21-\x5A\x5E-\x7E]*\]$/u
|
|
292
295
|
const MAILTO_DOMAIN_ERROR = 'URI mailto has an invalid recipient domain.'
|
|
296
|
+
const MAILTO_AUTHORITY_ERROR = 'URI mailto must not have an authority component.'
|
|
293
297
|
const HAS_SURROGATE = /[\uD800-\uDFFF]/u
|
|
294
298
|
|
|
295
299
|
/**
|
|
@@ -519,6 +523,27 @@ function mailtoParse (component, options) {
|
|
|
519
523
|
|
|
520
524
|
mailtoComponent.query = undefined
|
|
521
525
|
|
|
526
|
+
// RFC 6068 defines no authority for the `mailto` scheme. A `mailto://` input
|
|
527
|
+
// that retains an authority and also carries recipients (from the path or a
|
|
528
|
+
// `to=` hfield) folds a leading "/" into the recipient local part on the
|
|
529
|
+
// next parse; the generic serializer then reinserts a "/" before that path
|
|
530
|
+
// on every pass, so serialize(parse(x)) is not a fixpoint and a
|
|
531
|
+
// renormalize-to-fixpoint loop accretes one "%2F" per pass. Drop the
|
|
532
|
+
// retained authority so the parse -> serialize round trip is stable. A bare
|
|
533
|
+
// `mailto://host` with no recipients is left as-is: it is idempotent and host
|
|
534
|
+
// normalization for skip-normalize schemes stays intact.
|
|
535
|
+
if (
|
|
536
|
+
to.length > 0 &&
|
|
537
|
+
(mailtoComponent.userinfo !== undefined ||
|
|
538
|
+
mailtoComponent.host !== undefined ||
|
|
539
|
+
mailtoComponent.port !== undefined)
|
|
540
|
+
) {
|
|
541
|
+
mailtoComponent.userinfo = undefined
|
|
542
|
+
mailtoComponent.host = undefined
|
|
543
|
+
mailtoComponent.port = undefined
|
|
544
|
+
mailtoComponent.error = mailtoComponent.error || MAILTO_AUTHORITY_ERROR
|
|
545
|
+
}
|
|
546
|
+
|
|
522
547
|
for (let i = 0; i < to.length; i++) {
|
|
523
548
|
const rawAddr = to[i]
|
|
524
549
|
const atIdx = rawAddr.lastIndexOf('@')
|
|
@@ -583,6 +608,14 @@ function mailtoSerialize (component, options) {
|
|
|
583
608
|
mailtoComponent.headers = headers
|
|
584
609
|
|
|
585
610
|
if (to.length) {
|
|
611
|
+
// RFC 6068 defines no authority for the `mailto` scheme. If an
|
|
612
|
+
// application constructs a component with both an authority and recipients,
|
|
613
|
+
// drop the authority before the generic serializer can insert a "/" path
|
|
614
|
+
// separator that becomes part of the first recipient on the next parse.
|
|
615
|
+
mailtoComponent.userinfo = undefined
|
|
616
|
+
mailtoComponent.host = undefined
|
|
617
|
+
mailtoComponent.port = undefined
|
|
618
|
+
|
|
586
619
|
for (let i = 0; i < to.length; i++) {
|
|
587
620
|
const addr = String(to[i])
|
|
588
621
|
const atIdx = addr.lastIndexOf('@')
|
package/lib/utils.js
CHANGED
|
@@ -24,6 +24,12 @@ const isQueryFragmentCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&
|
|
|
24
24
|
/** @type {(value: string) => boolean} */
|
|
25
25
|
const isUserinfoCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:]$/u)
|
|
26
26
|
|
|
27
|
+
/** @type {(value: string) => boolean} */
|
|
28
|
+
const isRegNameCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=]$/u)
|
|
29
|
+
|
|
30
|
+
/** @type {(value: string) => boolean} */
|
|
31
|
+
const isLiteralRegName = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=]*$/u)
|
|
32
|
+
|
|
27
33
|
/**
|
|
28
34
|
* @param {Array<string>} input
|
|
29
35
|
* @returns {string}
|
|
@@ -347,23 +353,27 @@ function removeDotSegments (path) {
|
|
|
347
353
|
}
|
|
348
354
|
|
|
349
355
|
/**
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
356
|
+
* Encodes a host according to the RFC 3986 host grammar. Valid IP literals
|
|
357
|
+
* have already been checked by `normalizeIPv6`, so their address separators
|
|
358
|
+
* remain literal. Every other host is a reg-name and may contain only
|
|
359
|
+
* unreserved characters, sub-delimiters, and valid percent escapes.
|
|
354
360
|
*
|
|
355
361
|
* @param {string} host
|
|
356
|
-
* @param {boolean} isIP - true for IPv4/IPv6
|
|
362
|
+
* @param {boolean} isIP - true for a validated IPv4/IPv6 host
|
|
363
|
+
* @param {boolean} [allowNonAscii=false] - preserve non-ASCII IRI characters
|
|
357
364
|
* @returns {string}
|
|
358
365
|
*/
|
|
359
|
-
const
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
366
|
+
const IP_HOST_DELIMS = { '@': '%40', '/': '%2F', '?': '%3F', '#': '%23' }
|
|
367
|
+
const IP_HOST_DELIM_RE = /[@/?#]/g
|
|
368
|
+
|
|
369
|
+
function encodeHost (host, isIP, allowNonAscii = false) {
|
|
370
|
+
if (!isIP) {
|
|
371
|
+
if (isLiteralRegName(host)) return host
|
|
372
|
+
return encodeComponent(host, isRegNameCharacter, allowNonAscii)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
IP_HOST_DELIM_RE.lastIndex = 0
|
|
376
|
+
return host.replace(IP_HOST_DELIM_RE, (ch) => IP_HOST_DELIMS[ch])
|
|
367
377
|
}
|
|
368
378
|
|
|
369
379
|
/**
|
|
@@ -626,9 +636,10 @@ function normalizeQueryFragmentEncoding (input) {
|
|
|
626
636
|
*
|
|
627
637
|
* @param {string} input
|
|
628
638
|
* @param {(value: string) => boolean} isAllowed
|
|
639
|
+
* @param {boolean} [allowNonAscii=false]
|
|
629
640
|
* @returns {string}
|
|
630
641
|
*/
|
|
631
|
-
function encodeComponent (input, isAllowed) {
|
|
642
|
+
function encodeComponent (input, isAllowed, allowNonAscii = false) {
|
|
632
643
|
let output = ''
|
|
633
644
|
|
|
634
645
|
for (let i = 0; i < input.length; i++) {
|
|
@@ -649,17 +660,19 @@ function encodeComponent (input, isAllowed) {
|
|
|
649
660
|
if (code < 0x80) {
|
|
650
661
|
output += BYTE_HEX[code]
|
|
651
662
|
} else if (code < 0xD800 || code > 0xDFFF) {
|
|
652
|
-
output += percentEncodeNonAscii(code)
|
|
663
|
+
output += allowNonAscii ? ch : percentEncodeNonAscii(code)
|
|
653
664
|
} else if (code <= 0xDBFF && i + 1 < input.length) {
|
|
654
665
|
const low = input.charCodeAt(i + 1)
|
|
655
666
|
if (low >= 0xDC00 && low <= 0xDFFF) {
|
|
656
|
-
output +=
|
|
667
|
+
output += allowNonAscii
|
|
668
|
+
? ch + input[i + 1]
|
|
669
|
+
: percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
|
|
657
670
|
i++
|
|
658
671
|
} else {
|
|
659
|
-
output += percentEncodeNonAscii(0xFFFD)
|
|
672
|
+
output += allowNonAscii ? '\uFFFD' : percentEncodeNonAscii(0xFFFD)
|
|
660
673
|
}
|
|
661
674
|
} else {
|
|
662
|
-
output += percentEncodeNonAscii(0xFFFD)
|
|
675
|
+
output += allowNonAscii ? '\uFFFD' : percentEncodeNonAscii(0xFFFD)
|
|
663
676
|
}
|
|
664
677
|
}
|
|
665
678
|
}
|
|
@@ -773,9 +786,10 @@ function recomposeZonedIPv6Host (component) {
|
|
|
773
786
|
|
|
774
787
|
/**
|
|
775
788
|
* @param {import('../types/index').URIComponent} component
|
|
789
|
+
* @param {boolean} [allowNonAscii=false] - preserve non-ASCII IRI characters
|
|
776
790
|
* @returns {string|undefined}
|
|
777
791
|
*/
|
|
778
|
-
function recomposeAuthority (component) {
|
|
792
|
+
function recomposeAuthority (component, allowNonAscii = false) {
|
|
779
793
|
const uriTokens = []
|
|
780
794
|
|
|
781
795
|
if (component.userinfo !== undefined) {
|
|
@@ -808,7 +822,7 @@ function recomposeAuthority (component) {
|
|
|
808
822
|
if (ipV6res.isIPV6 === true || ipV6res.isIPVFuture === true) {
|
|
809
823
|
host = `[${ipV6res.escapedHost}]`
|
|
810
824
|
} else {
|
|
811
|
-
host =
|
|
825
|
+
host = encodeHost(host, false, allowNonAscii)
|
|
812
826
|
}
|
|
813
827
|
}
|
|
814
828
|
}
|
|
@@ -833,7 +847,7 @@ module.exports = {
|
|
|
833
847
|
nonSimpleDomain,
|
|
834
848
|
nonSimpleMailtoDomain,
|
|
835
849
|
recomposeAuthority,
|
|
836
|
-
|
|
850
|
+
encodeHost,
|
|
837
851
|
normalizePercentEncoding,
|
|
838
852
|
normalizePathEncoding,
|
|
839
853
|
serializePathEncoding,
|
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": "4.2.
|
|
4
|
+
"version": "4.2.2",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
7
7
|
"types": "types/index.d.ts",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"ajv": "^8.16.0",
|
|
62
62
|
"eslint": "^9.17.0",
|
|
63
63
|
"neostandard": "^0.13.0",
|
|
64
|
-
"playwright-test": "^
|
|
64
|
+
"playwright-test": "^15.0.1",
|
|
65
65
|
"tape": "^5.8.1",
|
|
66
66
|
"tstyche": "^7.0.0"
|
|
67
67
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const test = require('tape')
|
|
4
|
+
const fastURI = require('..')
|
|
5
|
+
|
|
6
|
+
const EDGE_ERROR = 'URI must not contain leading or trailing C0 controls or spaces.'
|
|
7
|
+
const C0_OR_SPACE = Array.from({ length: 0x21 }, (_, code) => String.fromCharCode(code))
|
|
8
|
+
|
|
9
|
+
test('parse rejects leading and trailing C0 controls or spaces', (t) => {
|
|
10
|
+
t.plan(C0_OR_SPACE.length * 2)
|
|
11
|
+
|
|
12
|
+
for (const character of C0_OR_SPACE) {
|
|
13
|
+
t.equal(fastURI.parse(character + 'relative').error, EDGE_ERROR, `leading U+${hex(character)}`)
|
|
14
|
+
t.equal(fastURI.parse('relative' + character).error, EDGE_ERROR, `trailing U+${hex(character)}`)
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('normalize preserves references with leading or trailing C0 controls or spaces', (t) => {
|
|
19
|
+
t.plan(C0_OR_SPACE.length * 2)
|
|
20
|
+
|
|
21
|
+
for (const character of C0_OR_SPACE) {
|
|
22
|
+
const leading = character + '//evil.example/path'
|
|
23
|
+
const trailing = '//evil.example/path' + character
|
|
24
|
+
t.equal(fastURI.normalize(leading), leading, `leading U+${hex(character)}`)
|
|
25
|
+
t.equal(fastURI.normalize(trailing), trailing, `trailing U+${hex(character)}`)
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('equal rejects references with leading or trailing C0 controls or spaces', (t) => {
|
|
30
|
+
t.plan(C0_OR_SPACE.length * 2)
|
|
31
|
+
|
|
32
|
+
for (const character of C0_OR_SPACE) {
|
|
33
|
+
const leading = character + 'relative'
|
|
34
|
+
const trailing = 'relative' + character
|
|
35
|
+
t.equal(fastURI.equal(leading, leading), false, `leading U+${hex(character)}`)
|
|
36
|
+
t.equal(fastURI.equal(trailing, trailing), false, `trailing U+${hex(character)}`)
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('resolve rejects leading and trailing C0 controls or spaces in either input', (t) => {
|
|
41
|
+
t.plan(C0_OR_SPACE.length * 4)
|
|
42
|
+
|
|
43
|
+
for (const character of C0_OR_SPACE) {
|
|
44
|
+
t.throws(
|
|
45
|
+
() => fastURI.resolve(character + 'http://base.example/', 'relative'),
|
|
46
|
+
/leading or trailing C0 controls or spaces/,
|
|
47
|
+
`leading base U+${hex(character)}`
|
|
48
|
+
)
|
|
49
|
+
t.throws(
|
|
50
|
+
() => fastURI.resolve('http://base.example/' + character, 'relative'),
|
|
51
|
+
/leading or trailing C0 controls or spaces/,
|
|
52
|
+
`trailing base U+${hex(character)}`
|
|
53
|
+
)
|
|
54
|
+
t.throws(
|
|
55
|
+
() => fastURI.resolve('http://base.example/', character + '//evil.example/path'),
|
|
56
|
+
/URI (authority introducer must not contain whitespace|must not contain leading or trailing C0 controls or spaces)/,
|
|
57
|
+
`leading relative U+${hex(character)}`
|
|
58
|
+
)
|
|
59
|
+
t.throws(
|
|
60
|
+
() => fastURI.resolve('http://base.example/', '//evil.example/path' + character),
|
|
61
|
+
/leading or trailing C0 controls or spaces/,
|
|
62
|
+
`trailing relative U+${hex(character)}`
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('suffix parsing checks the original reference edges', (t) => {
|
|
68
|
+
const options = { reference: 'suffix', scheme: 'http' }
|
|
69
|
+
|
|
70
|
+
t.equal(fastURI.parse('\u0000example', options).error, EDGE_ERROR, 'leading edge before suffix prefixing')
|
|
71
|
+
t.equal(fastURI.parse('example\u0020', options).error, EDGE_ERROR, 'trailing edge before suffix prefixing')
|
|
72
|
+
t.equal(fastURI.normalize('\u0000example', options), '\u0000example', 'normalize preserves a malformed suffix')
|
|
73
|
+
t.equal(fastURI.equal('example\u0020', 'example\u0020', options), false, 'equal rejects a malformed suffix')
|
|
74
|
+
t.end()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('interior and percent-encoded C0 controls or spaces remain data', (t) => {
|
|
78
|
+
t.plan(C0_OR_SPACE.length * 6)
|
|
79
|
+
|
|
80
|
+
for (const character of C0_OR_SPACE) {
|
|
81
|
+
const encoded = percentEncode(character)
|
|
82
|
+
const interior = 'a' + character + 'b'
|
|
83
|
+
const leadingEncoded = encoded + '//evil.example/path'
|
|
84
|
+
const trailingEncoded = '//evil.example/path' + encoded
|
|
85
|
+
|
|
86
|
+
t.equal(fastURI.parse(interior).error, undefined, `interior U+${hex(character)}`)
|
|
87
|
+
t.equal(fastURI.normalize(interior), 'a' + encoded + 'b', `normalize interior U+${hex(character)}`)
|
|
88
|
+
t.equal(fastURI.parse(leadingEncoded).error, undefined, `encoded leading U+${hex(character)}`)
|
|
89
|
+
t.equal(fastURI.normalize(leadingEncoded), leadingEncoded, `normalize encoded leading U+${hex(character)}`)
|
|
90
|
+
t.equal(fastURI.parse(trailingEncoded).error, undefined, `encoded trailing U+${hex(character)}`)
|
|
91
|
+
t.equal(fastURI.normalize(trailingEncoded), trailingEncoded, `normalize encoded trailing U+${hex(character)}`)
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('structured component data remains trusted and is encoded', (t) => {
|
|
96
|
+
t.equal(fastURI.serialize({ path: '\u0000' }), '%00', 'serialize encodes a path-edge control')
|
|
97
|
+
t.equal(fastURI.normalize({ path: '\u0020' }).path, '%20', 'object normalization encodes a path-edge space')
|
|
98
|
+
t.end()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('resolve rejects an empty host for HTTP and HTTPS results', (t) => {
|
|
102
|
+
const cases = [
|
|
103
|
+
['http://base.example/', '///evil.example'],
|
|
104
|
+
['https://base.example/', '////evil.example'],
|
|
105
|
+
['http://base.example/', '//'],
|
|
106
|
+
['http:///evil.example', ''],
|
|
107
|
+
['http://base.example/', '///evil.example', { scheme: 'null' }]
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
t.plan(cases.length + 2)
|
|
111
|
+
for (const [base, relative, options] of cases) {
|
|
112
|
+
t.throws(
|
|
113
|
+
() => fastURI.resolve(base, relative, options),
|
|
114
|
+
/HTTP URIs must have a host/,
|
|
115
|
+
`${base} + ${relative}`
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
t.equal(
|
|
120
|
+
fastURI.resolve('http://base.example/', '//other.example/path'),
|
|
121
|
+
'http://other.example/path',
|
|
122
|
+
'a non-empty HTTP authority remains valid'
|
|
123
|
+
)
|
|
124
|
+
t.equal(
|
|
125
|
+
fastURI.resolve('uri://base.example/', '///path'),
|
|
126
|
+
'uri:///path',
|
|
127
|
+
'generic schemes may retain an empty authority'
|
|
128
|
+
)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
function hex (character) {
|
|
132
|
+
return character.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function percentEncode (character) {
|
|
136
|
+
return '%' + character.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0')
|
|
137
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const test = require('tape')
|
|
4
|
+
const fastURI = require('..')
|
|
5
|
+
|
|
6
|
+
const C0_OR_SPACE = Array.from({ length: 0x21 }, (_, code) => String.fromCharCode(code))
|
|
7
|
+
|
|
8
|
+
function percentEncode (character) {
|
|
9
|
+
return '%' + character.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
test('host normalization percent-encodes interior C0 controls and spaces', (t) => {
|
|
13
|
+
t.plan(C0_OR_SPACE.length * 3)
|
|
14
|
+
|
|
15
|
+
for (const character of C0_OR_SPACE) {
|
|
16
|
+
const input = `custom://a${character}b.example/path`
|
|
17
|
+
const encodedHost = `a${percentEncode(character)}b.example`
|
|
18
|
+
const expected = `custom://${encodedHost}/path`
|
|
19
|
+
|
|
20
|
+
t.equal(fastURI.parse(input).host, encodedHost, `parse U+${percentEncode(character).slice(1)}`)
|
|
21
|
+
t.equal(fastURI.normalize(input), expected, `normalize U+${percentEncode(character).slice(1)}`)
|
|
22
|
+
t.equal(
|
|
23
|
+
fastURI.resolve('custom://base.example/', `//a${character}b.example/path`),
|
|
24
|
+
expected,
|
|
25
|
+
`resolve U+${percentEncode(character).slice(1)}`
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('host serialization enforces the RFC 3986 reg-name grammar', (t) => {
|
|
31
|
+
const forbidden = '"<>[\\]^`{|}\x7F:/?#@%'
|
|
32
|
+
const expected = '%22%3C%3E%5B%5C%5D%5E%60%7B%7C%7D%7F%3A%2F%3F%23%40%25'
|
|
33
|
+
|
|
34
|
+
t.equal(
|
|
35
|
+
fastURI.serialize({ scheme: 'custom', host: forbidden, path: '/path' }),
|
|
36
|
+
`custom://${expected}/path`,
|
|
37
|
+
'forbidden ASCII host characters are percent-encoded'
|
|
38
|
+
)
|
|
39
|
+
t.equal(
|
|
40
|
+
fastURI.serialize({ scheme: 'custom', host: "AZaz09-._~!$&'()*+,;=%2f" }),
|
|
41
|
+
"custom://AZaz09-._~!$&'()*+,;=%2F",
|
|
42
|
+
'unreserved characters, sub-delimiters, and valid escapes are preserved'
|
|
43
|
+
)
|
|
44
|
+
t.end()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('host encoding preserves URI and IRI behavior', (t) => {
|
|
48
|
+
const input = 'custom://résumé.example/path'
|
|
49
|
+
const encoded = 'custom://r%C3%A9sum%C3%A9.example/path'
|
|
50
|
+
|
|
51
|
+
t.equal(fastURI.normalize(input), encoded, 'URI host uses UTF-8 percent-encoding')
|
|
52
|
+
t.equal(fastURI.normalize(input, { unicodeSupport: true }), input, 'IRI host preserves non-ASCII characters')
|
|
53
|
+
t.equal(fastURI.normalize(encoded), encoded, 'encoded host normalization is idempotent')
|
|
54
|
+
t.equal(
|
|
55
|
+
fastURI.normalize('custom://safe\r\nevil.example/path'),
|
|
56
|
+
'custom://safe%0D%0Aevil.example/path',
|
|
57
|
+
'CRLF is not emitted literally'
|
|
58
|
+
)
|
|
59
|
+
t.end()
|
|
60
|
+
})
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const test = require('tape')
|
|
4
|
+
const fastURI = require('..')
|
|
5
|
+
|
|
6
|
+
// GHSA-88VG-QFHM-JX5Q: `mailto://h/a` carries an authority (which RFC 6068 does
|
|
7
|
+
// not define for `mailto`) and a path. `mailtoParse` used to retain that
|
|
8
|
+
// authority and fold the path's leading "/" into the recipient local part; the
|
|
9
|
+
// generic serializer then reinserted a "/" separator on every pass, so
|
|
10
|
+
// serialize(parse(x)) was not a fixpoint and normalize() accreted one "%2F" per
|
|
11
|
+
// pass (unbounded growth for a renormalize-to-fixpoint consumer).
|
|
12
|
+
|
|
13
|
+
const authorityForms = [
|
|
14
|
+
['mailto://h/a', 'mailto:%2Fa@'],
|
|
15
|
+
['mailto://h/a@b', 'mailto:%2Fa@b'],
|
|
16
|
+
['mailto://h/', 'mailto:%2F@'],
|
|
17
|
+
['mailto://h/a,b', 'mailto:%2Fa@,b@'],
|
|
18
|
+
['mailto://user@h/a', 'mailto:%2Fa@'],
|
|
19
|
+
['mailto://h:8080/a', 'mailto:%2Fa@'],
|
|
20
|
+
['mailto://', 'mailto://'],
|
|
21
|
+
// recipients can come from a `to=` hfield even when the path is empty
|
|
22
|
+
['mailto://h?to=x@y', 'mailto:x@y'],
|
|
23
|
+
['mailto://h?to=x@y&to=a@b', 'mailto:x@y,a@b']
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
test('mailto with a retained authority and a path normalizes to a fixpoint', (t) => {
|
|
27
|
+
t.plan(authorityForms.length * 2)
|
|
28
|
+
|
|
29
|
+
for (const [input] of authorityForms) {
|
|
30
|
+
const once = fastURI.normalize(input)
|
|
31
|
+
const twice = fastURI.normalize(once)
|
|
32
|
+
|
|
33
|
+
t.equal(twice, once, input + ' normalize is idempotent')
|
|
34
|
+
t.equal(fastURI.equal(input, once), true, input + ' equals its normalized form')
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('mailto serialize(parse()) is a stable round trip for the authority form', (t) => {
|
|
39
|
+
t.plan(authorityForms.length)
|
|
40
|
+
|
|
41
|
+
for (const [input, expected] of authorityForms) {
|
|
42
|
+
t.equal(fastURI.serialize(fastURI.parse(input)), expected, input + ' round trips to a stable value')
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('mailto authority form does not grow without bound under renormalization', (t) => {
|
|
47
|
+
// A renormalize-to-fixpoint loop must terminate instead of growing forever.
|
|
48
|
+
// A single normalize call already terminates, so this guards the fixpoint.
|
|
49
|
+
const inputs = ['mailto://h/a', 'mailto://h/a@b', 'mailto://h/a,b']
|
|
50
|
+
|
|
51
|
+
t.plan(inputs.length * 2)
|
|
52
|
+
|
|
53
|
+
for (const input of inputs) {
|
|
54
|
+
const first = fastURI.normalize(input)
|
|
55
|
+
let s = input
|
|
56
|
+
let iters = 0
|
|
57
|
+
let stable = false
|
|
58
|
+
while (iters < 1000) {
|
|
59
|
+
const next = fastURI.normalize(s)
|
|
60
|
+
if (next === s) {
|
|
61
|
+
stable = true
|
|
62
|
+
break
|
|
63
|
+
}
|
|
64
|
+
s = next
|
|
65
|
+
iters++
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
t.equal(stable, true, input + ' reaches a fixpoint')
|
|
69
|
+
t.equal(s.length, first.length, input + ' does not grow')
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('mailto authority handling preserves host normalization for skip-normalize schemes', (t) => {
|
|
74
|
+
// Regression: a bare `mailto://host` with no path must keep its host and stay
|
|
75
|
+
// idempotent, so the generic host normalization path is unaffected.
|
|
76
|
+
const encodedHost = 'mailto://%41.com'
|
|
77
|
+
const literalHost = 'mailto://a.com'
|
|
78
|
+
|
|
79
|
+
t.equal(fastURI.parse(encodedHost).host, fastURI.parse(literalHost).host, 'parse results have consistent hosts')
|
|
80
|
+
t.equal(fastURI.normalize(encodedHost), fastURI.normalize(literalHost), 'normalize results are consistent')
|
|
81
|
+
t.equal(fastURI.equal(encodedHost, literalHost), true, 'encoded and literal hosts compare equal')
|
|
82
|
+
t.equal(fastURI.normalize(literalHost), literalHost, 'bare authority mailto is idempotent')
|
|
83
|
+
t.end()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('mailto authority form is reported as malformed on the parsed component', (t) => {
|
|
87
|
+
t.equal(
|
|
88
|
+
fastURI.parse('mailto://h/a').error,
|
|
89
|
+
'URI mailto must not have an authority component.',
|
|
90
|
+
'the retained authority is surfaced as an error'
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
// A valid, authority-free mailto must not be flagged.
|
|
94
|
+
t.equal(fastURI.parse('mailto:user@host.com').error, undefined, 'well-formed mailto has no error')
|
|
95
|
+
t.end()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('mailto serializer drops caller-supplied authority when recipients are present', (t) => {
|
|
99
|
+
t.equal(
|
|
100
|
+
fastURI.serialize({ scheme: 'mailto', host: 'h', to: ['a@b'] }),
|
|
101
|
+
'mailto:a@b',
|
|
102
|
+
'authority is dropped for direct recipients'
|
|
103
|
+
)
|
|
104
|
+
t.equal(
|
|
105
|
+
fastURI.serialize({ scheme: 'mailto', userinfo: 'u', host: 'h', port: 8080, to: ['a@b'] }),
|
|
106
|
+
'mailto:a@b',
|
|
107
|
+
'userinfo, host, and port are dropped for direct recipients'
|
|
108
|
+
)
|
|
109
|
+
t.equal(
|
|
110
|
+
fastURI.serialize({ scheme: 'mailto', host: 'h', headers: { to: 'a@b' } }),
|
|
111
|
+
'mailto:a@b',
|
|
112
|
+
'authority is dropped for recipients supplied through a to hfield'
|
|
113
|
+
)
|
|
114
|
+
t.equal(
|
|
115
|
+
fastURI.serialize({ scheme: 'mailto', host: 'h', to: ['a@b'], subject: 'hello' }),
|
|
116
|
+
'mailto:a@b?subject=hello',
|
|
117
|
+
'dropping authority preserves header fields'
|
|
118
|
+
)
|
|
119
|
+
t.equal(
|
|
120
|
+
fastURI.serialize({ scheme: 'mailto', host: 'h' }),
|
|
121
|
+
'mailto://h',
|
|
122
|
+
'bare authority without recipients remains idempotent'
|
|
123
|
+
)
|
|
124
|
+
t.end()
|
|
125
|
+
})
|
package/test/parse.test.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
const test = require('tape')
|
|
4
4
|
const fastURI = require('..')
|
|
5
5
|
|
|
6
|
+
const EDGE_ERROR = 'URI must not contain leading or trailing C0 controls or spaces.'
|
|
7
|
+
|
|
6
8
|
test('URI parse', (t) => {
|
|
7
9
|
let components
|
|
8
10
|
|
|
@@ -92,7 +94,7 @@ test('URI parse', (t) => {
|
|
|
92
94
|
|
|
93
95
|
// fragment with character tabulation
|
|
94
96
|
components = fastURI.parse('#\t')
|
|
95
|
-
t.equal(components.error,
|
|
97
|
+
t.equal(components.error, EDGE_ERROR, 'fragment with trailing tab is malformed')
|
|
96
98
|
t.equal(components.scheme, undefined, 'scheme')
|
|
97
99
|
// t.equal(components.authority, undefined, "authority");
|
|
98
100
|
t.equal(components.userinfo, undefined, 'userinfo')
|
|
@@ -104,7 +106,7 @@ test('URI parse', (t) => {
|
|
|
104
106
|
|
|
105
107
|
// fragment with line feed
|
|
106
108
|
components = fastURI.parse('#\n')
|
|
107
|
-
t.equal(components.error,
|
|
109
|
+
t.equal(components.error, EDGE_ERROR, 'fragment with trailing line feed is malformed')
|
|
108
110
|
t.equal(components.scheme, undefined, 'scheme')
|
|
109
111
|
// t.equal(components.authority, undefined, "authority");
|
|
110
112
|
t.equal(components.userinfo, undefined, 'userinfo')
|
|
@@ -116,7 +118,7 @@ test('URI parse', (t) => {
|
|
|
116
118
|
|
|
117
119
|
// fragment with line tabulation
|
|
118
120
|
components = fastURI.parse('#\v')
|
|
119
|
-
t.equal(components.error,
|
|
121
|
+
t.equal(components.error, EDGE_ERROR, 'fragment with trailing vertical tab is malformed')
|
|
120
122
|
t.equal(components.scheme, undefined, 'scheme')
|
|
121
123
|
// t.equal(components.authority, undefined, "authority");
|
|
122
124
|
t.equal(components.userinfo, undefined, 'userinfo')
|
|
@@ -128,7 +130,7 @@ test('URI parse', (t) => {
|
|
|
128
130
|
|
|
129
131
|
// fragment with form feed
|
|
130
132
|
components = fastURI.parse('#\f')
|
|
131
|
-
t.equal(components.error,
|
|
133
|
+
t.equal(components.error, EDGE_ERROR, 'fragment with trailing form feed is malformed')
|
|
132
134
|
t.equal(components.scheme, undefined, 'scheme')
|
|
133
135
|
// t.equal(components.authority, undefined, "authority");
|
|
134
136
|
t.equal(components.userinfo, undefined, 'userinfo')
|
|
@@ -140,7 +142,7 @@ test('URI parse', (t) => {
|
|
|
140
142
|
|
|
141
143
|
// fragment with carriage return
|
|
142
144
|
components = fastURI.parse('#\r')
|
|
143
|
-
t.equal(components.error,
|
|
145
|
+
t.equal(components.error, EDGE_ERROR, 'fragment with trailing carriage return is malformed')
|
|
144
146
|
t.equal(components.scheme, undefined, 'scheme')
|
|
145
147
|
// t.equal(components.authority, undefined, "authority");
|
|
146
148
|
t.equal(components.userinfo, undefined, 'userinfo')
|
package/test/security.test.js
CHANGED
|
@@ -202,8 +202,8 @@ test('resolve applies domain canonicalisation only when the effective scheme opt
|
|
|
202
202
|
t.plan(2)
|
|
203
203
|
t.equal(
|
|
204
204
|
fastURI.resolve('uri://trusted.example/', `//${host}/`),
|
|
205
|
-
|
|
206
|
-
'
|
|
205
|
+
'uri://ex%C2%ADample.com/',
|
|
206
|
+
'a generic URI encodes the host without applying domain canonicalisation'
|
|
207
207
|
)
|
|
208
208
|
t.equal(
|
|
209
209
|
fastURI.resolve('http://trusted.example/', `//${host}/`, { unicodeSupport: true }),
|
package/test/uri-js.test.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
const test = require('tape')
|
|
4
4
|
const fastURI = require('..')
|
|
5
5
|
|
|
6
|
+
const EDGE_ERROR = 'URI must not contain leading or trailing C0 controls or spaces.'
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* URI.js
|
|
8
10
|
*
|
|
@@ -126,7 +128,7 @@ test('URI Parsing', (t) => {
|
|
|
126
128
|
|
|
127
129
|
// fragment with character tabulation
|
|
128
130
|
components = fastURI.parse('#\t')
|
|
129
|
-
t.equal(components.error,
|
|
131
|
+
t.equal(components.error, EDGE_ERROR, 'fragment with trailing tab is malformed')
|
|
130
132
|
t.equal(components.scheme, undefined, 'scheme')
|
|
131
133
|
t.equal(components.userinfo, undefined, 'userinfo')
|
|
132
134
|
t.equal(components.host, undefined, 'host')
|
|
@@ -137,7 +139,7 @@ test('URI Parsing', (t) => {
|
|
|
137
139
|
|
|
138
140
|
// fragment with line feed
|
|
139
141
|
components = fastURI.parse('#\n')
|
|
140
|
-
t.equal(components.error,
|
|
142
|
+
t.equal(components.error, EDGE_ERROR, 'fragment with trailing line feed is malformed')
|
|
141
143
|
t.equal(components.scheme, undefined, 'scheme')
|
|
142
144
|
t.equal(components.userinfo, undefined, 'userinfo')
|
|
143
145
|
t.equal(components.host, undefined, 'host')
|
|
@@ -148,7 +150,7 @@ test('URI Parsing', (t) => {
|
|
|
148
150
|
|
|
149
151
|
// fragment with line tabulation
|
|
150
152
|
components = fastURI.parse('#\v')
|
|
151
|
-
t.equal(components.error,
|
|
153
|
+
t.equal(components.error, EDGE_ERROR, 'fragment with trailing vertical tab is malformed')
|
|
152
154
|
t.equal(components.scheme, undefined, 'scheme')
|
|
153
155
|
t.equal(components.userinfo, undefined, 'userinfo')
|
|
154
156
|
t.equal(components.host, undefined, 'host')
|
|
@@ -159,7 +161,7 @@ test('URI Parsing', (t) => {
|
|
|
159
161
|
|
|
160
162
|
// fragment with form feed
|
|
161
163
|
components = fastURI.parse('#\f')
|
|
162
|
-
t.equal(components.error,
|
|
164
|
+
t.equal(components.error, EDGE_ERROR, 'fragment with trailing form feed is malformed')
|
|
163
165
|
t.equal(components.scheme, undefined, 'scheme')
|
|
164
166
|
t.equal(components.userinfo, undefined, 'userinfo')
|
|
165
167
|
t.equal(components.host, undefined, 'host')
|
|
@@ -170,7 +172,7 @@ test('URI Parsing', (t) => {
|
|
|
170
172
|
|
|
171
173
|
// fragment with carriage return
|
|
172
174
|
components = fastURI.parse('#\r')
|
|
173
|
-
t.equal(components.error,
|
|
175
|
+
t.equal(components.error, EDGE_ERROR, 'fragment with trailing carriage return is malformed')
|
|
174
176
|
t.equal(components.scheme, undefined, 'scheme')
|
|
175
177
|
t.equal(components.userinfo, undefined, 'userinfo')
|
|
176
178
|
t.equal(components.host, undefined, 'host')
|
|
@@ -27,3 +27,21 @@ test('URN parsing preserves the complete RFC 2141 NSS', (t) => {
|
|
|
27
27
|
|
|
28
28
|
t.end()
|
|
29
29
|
})
|
|
30
|
+
|
|
31
|
+
test('URN serialization preserves NSS dot segments', (t) => {
|
|
32
|
+
const urnWithDotSegments = 'urn:example:a/b/../c'
|
|
33
|
+
const destructiveDotSegments = 'urn:example:../../../etc/passwd'
|
|
34
|
+
|
|
35
|
+
t.equal(fastURI.serialize(fastURI.parse(urnWithDotSegments)), urnWithDotSegments, 'parsed NSS round-trips unchanged')
|
|
36
|
+
t.equal(
|
|
37
|
+
fastURI.serialize({ scheme: 'urn', nid: 'example', nss: '../../../etc/passwd' }),
|
|
38
|
+
destructiveDotSegments,
|
|
39
|
+
'structured NSS retains leading dot segments'
|
|
40
|
+
)
|
|
41
|
+
t.equal(fastURI.normalize(urnWithDotSegments), urnWithDotSegments, 'normalization retains interior dot segments')
|
|
42
|
+
t.equal(fastURI.normalize(destructiveDotSegments), destructiveDotSegments, 'normalization retains leading dot segments')
|
|
43
|
+
t.equal(fastURI.resolve('', urnWithDotSegments), urnWithDotSegments, 'resolution retains NSS dot segments')
|
|
44
|
+
t.equal(fastURI.equal(urnWithDotSegments, 'urn:example:a/c'), false, 'distinct NSS values do not compare equal')
|
|
45
|
+
|
|
46
|
+
t.end()
|
|
47
|
+
})
|