fast-uri 4.2.0 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/schemes.js CHANGED
@@ -290,6 +290,7 @@ const DTEXT_IRI = allowSet(DTEXT_ALLOWED, true)
290
290
  const HEX_PAIR = /^[\da-f]{2}$/iu
291
291
  const MAILTO_DOMAIN_LITERAL = /^\[[\x21-\x5A\x5E-\x7E]*\]$/u
292
292
  const MAILTO_DOMAIN_ERROR = 'URI mailto has an invalid recipient domain.'
293
+ const MAILTO_AUTHORITY_ERROR = 'URI mailto must not have an authority component.'
293
294
  const HAS_SURROGATE = /[\uD800-\uDFFF]/u
294
295
 
295
296
  /**
@@ -519,6 +520,27 @@ function mailtoParse (component, options) {
519
520
 
520
521
  mailtoComponent.query = undefined
521
522
 
523
+ // RFC 6068 defines no authority for the `mailto` scheme. A `mailto://` input
524
+ // that retains an authority and also carries recipients (from the path or a
525
+ // `to=` hfield) folds a leading "/" into the recipient local part on the
526
+ // next parse; the generic serializer then reinserts a "/" before that path
527
+ // on every pass, so serialize(parse(x)) is not a fixpoint and a
528
+ // renormalize-to-fixpoint loop accretes one "%2F" per pass. Drop the
529
+ // retained authority so the parse -> serialize round trip is stable. A bare
530
+ // `mailto://host` with no recipients is left as-is: it is idempotent and host
531
+ // normalization for skip-normalize schemes stays intact.
532
+ if (
533
+ to.length > 0 &&
534
+ (mailtoComponent.userinfo !== undefined ||
535
+ mailtoComponent.host !== undefined ||
536
+ mailtoComponent.port !== undefined)
537
+ ) {
538
+ mailtoComponent.userinfo = undefined
539
+ mailtoComponent.host = undefined
540
+ mailtoComponent.port = undefined
541
+ mailtoComponent.error = mailtoComponent.error || MAILTO_AUTHORITY_ERROR
542
+ }
543
+
522
544
  for (let i = 0; i < to.length; i++) {
523
545
  const rawAddr = to[i]
524
546
  const atIdx = rawAddr.lastIndexOf('@')
@@ -583,6 +605,14 @@ function mailtoSerialize (component, options) {
583
605
  mailtoComponent.headers = headers
584
606
 
585
607
  if (to.length) {
608
+ // RFC 6068 defines no authority for the `mailto` scheme. If an
609
+ // application constructs a component with both an authority and recipients,
610
+ // drop the authority before the generic serializer can insert a "/" path
611
+ // separator that becomes part of the first recipient on the next parse.
612
+ mailtoComponent.userinfo = undefined
613
+ mailtoComponent.host = undefined
614
+ mailtoComponent.port = undefined
615
+
586
616
  for (let i = 0; i < to.length; i++) {
587
617
  const addr = String(to[i])
588
618
  const atIdx = addr.lastIndexOf('@')
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.0",
4
+ "version": "4.2.1",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
7
7
  "types": "types/index.d.ts",
@@ -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
+ })