fast-uri 2.4.0 → 2.4.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/README.md CHANGED
@@ -15,6 +15,8 @@ Dependency free RFC 3986 URI toolbox.
15
15
 
16
16
  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:
17
17
 
18
+ Malformed authorities and out-of-range ports are reported through the parsed component's `error` field. `normalize()` leaves malformed string inputs unchanged, and `equal()` returns `false` when either string input is malformed.
19
+
18
20
  * `scheme` (string)
19
21
  Indicates the scheme that the URI should be treated as, overriding the URI's normal scheme parsing behavior.
20
22
 
@@ -69,6 +71,17 @@ uri.resolve("uri://a/b/c/d?q", "../../g")
69
71
  "uri://a/g"
70
72
  ```
71
73
 
74
+ ### Normalize
75
+
76
+ ```js
77
+ const uri = require('fast-uri')
78
+ uri.normalize('http://example.com/a%2Fb')
79
+ // Output
80
+ "http://example.com/a%2Fb"
81
+ ```
82
+
83
+ Reserved path escapes such as `%2F` and `%2E` are preserved as path data during normalization and comparison.
84
+
72
85
  ### Equal
73
86
 
74
87
  ```js
package/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  'use strict'
2
2
 
3
- const { normalizeIPv6, normalizeIPv4, removeDotSegments, recomposeAuthority, normalizeComponentEncoding } = require('./lib/utils')
3
+ const { normalizeIPv6, normalizeIPv4, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters } = require('./lib/utils')
4
4
  const SCHEMES = require('./lib/schemes')
5
5
 
6
6
  function normalize (uri, options) {
7
7
  if (typeof uri === 'string') {
8
- uri = serialize(parse(uri, options), options)
8
+ uri = normalizeString(uri, options)
9
9
  } else if (typeof uri === 'object') {
10
10
  uri = parse(serialize(uri, options), options)
11
11
  }
@@ -79,21 +79,10 @@ function resolveComponents (base, relative, options, skipNormalization) {
79
79
  }
80
80
 
81
81
  function equal (uriA, uriB, options) {
82
- if (typeof uriA === 'string') {
83
- uriA = unescape(uriA)
84
- uriA = serialize(normalizeComponentEncoding(parse(uriA, options), true), { ...options, skipEscape: true })
85
- } else if (typeof uriA === 'object') {
86
- uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true })
87
- }
88
-
89
- if (typeof uriB === 'string') {
90
- uriB = unescape(uriB)
91
- uriB = serialize(normalizeComponentEncoding(parse(uriB, options), true), { ...options, skipEscape: true })
92
- } else if (typeof uriB === 'object') {
93
- uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true })
94
- }
82
+ const normalizedA = normalizeComparableURI(uriA, options)
83
+ const normalizedB = normalizeComparableURI(uriB, options)
95
84
 
96
- return uriA.toLowerCase() === uriB.toLowerCase()
85
+ return normalizedA !== undefined && normalizedB !== undefined && normalizedA.toLowerCase() === normalizedB.toLowerCase()
97
86
  }
98
87
 
99
88
  function serialize (cmpts, opts) {
@@ -124,13 +113,13 @@ function serialize (cmpts, opts) {
124
113
 
125
114
  if (components.path !== undefined) {
126
115
  if (!options.skipEscape) {
127
- components.path = escape(components.path)
116
+ components.path = escapePreservingEscapes(components.path)
128
117
 
129
118
  if (components.scheme !== undefined) {
130
119
  components.path = components.path.split('%3A').join(':')
131
120
  }
132
121
  } else {
133
- components.path = unescape(components.path)
122
+ components.path = normalizePercentEncoding(components.path)
134
123
  }
135
124
  }
136
125
 
@@ -192,7 +181,19 @@ function nonSimpleDomain (value) {
192
181
 
193
182
  const URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u
194
183
 
195
- function parse (uri, opts) {
184
+ function getParseError (parsed, matches) {
185
+ if (matches[2] !== undefined && parsed.path && parsed.path[0] !== '/') {
186
+ return 'URI path must start with "/" when authority is present.'
187
+ }
188
+
189
+ if (typeof parsed.port === 'number' && (parsed.port < 0 || parsed.port > 65535)) {
190
+ return 'URI port is malformed.'
191
+ }
192
+
193
+ return undefined
194
+ }
195
+
196
+ function parseWithStatus (uri, opts) {
196
197
  const options = Object.assign({}, opts)
197
198
  const parsed = {
198
199
  scheme: undefined,
@@ -204,6 +205,8 @@ function parse (uri, opts) {
204
205
  fragment: undefined
205
206
  }
206
207
  const gotEncoding = uri.indexOf('%') !== -1
208
+ let malformedAuthorityOrPort = false
209
+ let isIP = false
207
210
  if (options.reference === 'suffix') uri = (options.scheme ? options.scheme + ':' : '') + '//' + uri
208
211
 
209
212
  const matches = uri.match(URI_PARSE)
@@ -222,12 +225,22 @@ function parse (uri, opts) {
222
225
  if (isNaN(parsed.port)) {
223
226
  parsed.port = matches[5]
224
227
  }
228
+
229
+ const parseError = getParseError(parsed, matches)
230
+ if (parseError !== undefined) {
231
+ parsed.error = parsed.error || parseError
232
+ malformedAuthorityOrPort = true
233
+ }
234
+
225
235
  if (parsed.host) {
226
236
  const ipv4result = normalizeIPv4(parsed.host)
227
237
  if (ipv4result.isIPV4 === false) {
228
- parsed.host = normalizeIPv6(ipv4result.host, { isIPV4: false }).host.toLowerCase()
238
+ const ipv6result = normalizeIPv6(ipv4result.host, { isIPV4: false })
239
+ parsed.host = ipv6result.host.toLowerCase()
240
+ isIP = ipv6result.isIPV6
229
241
  } else {
230
242
  parsed.host = ipv4result.host
243
+ isIP = true
231
244
  }
232
245
  }
233
246
  if (parsed.scheme === undefined && parsed.userinfo === undefined && parsed.host === undefined && parsed.port === undefined && !parsed.path && parsed.query === undefined) {
@@ -251,7 +264,7 @@ function parse (uri, opts) {
251
264
  // check if scheme can't handle IRIs
252
265
  if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport)) {
253
266
  // if host component is a domain name
254
- if (parsed.host && (options.domainHost || (schemeHandler && schemeHandler.domainHost)) && nonSimpleDomain(parsed.host)) {
267
+ if (parsed.host && (options.domainHost || (schemeHandler && schemeHandler.domainHost)) && isIP === false && nonSimpleDomain(parsed.host)) {
255
268
  // convert Unicode IDN -> ASCII IDN
256
269
  try {
257
270
  parsed.host = URL.domainToASCII(parsed.host.toLowerCase())
@@ -270,10 +283,10 @@ function parse (uri, opts) {
270
283
  parsed.userinfo = unescape(parsed.userinfo)
271
284
  }
272
285
  if (gotEncoding && parsed.host !== undefined) {
273
- parsed.host = unescape(parsed.host)
286
+ parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP)
274
287
  }
275
288
  if (parsed.path !== undefined && parsed.path.length) {
276
- parsed.path = escape(unescape(parsed.path))
289
+ parsed.path = normalizePathEncoding(parsed.path)
277
290
  }
278
291
  if (parsed.fragment !== undefined && parsed.fragment.length) {
279
292
  parsed.fragment = encodeURI(decodeURI(parsed.fragment))
@@ -287,10 +300,38 @@ function parse (uri, opts) {
287
300
  } else {
288
301
  parsed.error = parsed.error || 'URI can not be parsed.'
289
302
  }
290
- return parsed
303
+ return { parsed, malformedAuthorityOrPort }
304
+ }
305
+
306
+ function parse (uri, opts) {
307
+ return parseWithStatus(uri, opts).parsed
308
+ }
309
+
310
+ function normalizeString (uri, opts) {
311
+ return normalizeStringWithStatus(uri, opts).normalized
312
+ }
313
+
314
+ function normalizeStringWithStatus (uri, opts) {
315
+ const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts)
316
+ return {
317
+ normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
318
+ malformedAuthorityOrPort
319
+ }
320
+ }
321
+
322
+ function normalizeComparableURI (uri, opts) {
323
+ if (typeof uri === 'string') {
324
+ const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts)
325
+ return malformedAuthorityOrPort ? undefined : normalized
326
+ }
327
+
328
+ if (typeof uri === 'object') {
329
+ return serialize(uri, opts)
330
+ }
291
331
  }
292
332
 
293
333
  const fastUri = {
334
+ SCHEMES,
294
335
  normalize,
295
336
  resolve,
296
337
  resolveComponents,
package/lib/utils.js CHANGED
@@ -13,14 +13,20 @@ function normalizeIPv4 (host) {
13
13
  }
14
14
  }
15
15
 
16
- function stringToHexStripped (input) {
16
+ /**
17
+ * @param {string[]} input
18
+ * @param {boolean} [keepZero=false]
19
+ * @returns {string|undefined}
20
+ */
21
+ function stringArrayToHexStripped (input, keepZero = false) {
17
22
  let acc = ''
18
23
  let strip = true
19
24
  for (const c of input) {
20
- if (c !== '0' && strip === true) strip = false
21
25
  if (HEX[c] === undefined) return undefined
26
+ if (c !== '0' && strip === true) strip = false
22
27
  if (!strip) acc += c
23
28
  }
29
+ if (keepZero && acc.length === 0) acc = '0'
24
30
  return acc
25
31
  }
26
32
 
@@ -36,7 +42,7 @@ function getIPV6 (input) {
36
42
  function consume () {
37
43
  if (buffer.length) {
38
44
  if (isZone === false) {
39
- const hex = stringToHexStripped(buffer.join(''))
45
+ const hex = stringArrayToHexStripped(buffer)
40
46
  if (hex !== undefined) {
41
47
  address.push(hex)
42
48
  } else {
@@ -83,7 +89,7 @@ function getIPV6 (input) {
83
89
  } else if (endIpv6) {
84
90
  address.push(buffer.join(''))
85
91
  } else {
86
- address.push(stringToHexStripped(buffer.join('')))
92
+ address.push(stringArrayToHexStripped(buffer))
87
93
  }
88
94
  }
89
95
  output.address = address.join('')
@@ -170,27 +176,131 @@ function removeDotSegments (input) {
170
176
  return output.join('')
171
177
  }
172
178
 
173
- function normalizeComponentEncoding (components, esc) {
174
- const func = esc !== true ? escape : unescape
175
- if (components.scheme !== undefined) {
176
- components.scheme = func(components.scheme)
177
- }
178
- if (components.userinfo !== undefined) {
179
- components.userinfo = func(components.userinfo)
180
- }
181
- if (components.host !== undefined) {
182
- components.host = func(components.host)
179
+ /**
180
+ * Re-escape RFC 3986 gen-delims that must not appear literally in the host.
181
+ * After the URI regex parses, these characters cannot be literal in the host
182
+ * field, so any that appear after decoding came from percent-encoding and
183
+ * must be restored to prevent authority structure changes.
184
+ *
185
+ * @param {string} host
186
+ * @param {boolean} isIP - true for IPv4/IPv6 hosts (skip colon re-escaping)
187
+ * @returns {string}
188
+ */
189
+ const HOST_DELIMS = { '@': '%40', '/': '%2F', '?': '%3F', '#': '%23', ':': '%3A' }
190
+ const HOST_DELIM_RE = /[@/?#:]/g
191
+ const HOST_DELIM_NO_COLON_RE = /[@/?#]/g
192
+
193
+ function reescapeHostDelimiters (host, isIP) {
194
+ const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE
195
+ re.lastIndex = 0
196
+ return host.replace(re, (ch) => HOST_DELIMS[ch])
197
+ }
198
+
199
+ const isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu)
200
+ const isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu)
201
+ const isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu)
202
+
203
+ /**
204
+ * Normalizes percent escapes and optionally decodes only unreserved ASCII bytes.
205
+ * Reserved delimiters such as `%2F` and `%2E` stay escaped.
206
+ *
207
+ * @param {string} input
208
+ * @param {boolean} [decodeUnreserved=false]
209
+ * @returns {string}
210
+ */
211
+ function normalizePercentEncoding (input, decodeUnreserved = false) {
212
+ if (input.indexOf('%') === -1) {
213
+ return input
183
214
  }
184
- if (components.path !== undefined) {
185
- components.path = func(components.path)
215
+
216
+ let output = ''
217
+
218
+ for (let i = 0; i < input.length; i++) {
219
+ if (input[i] === '%' && i + 2 < input.length) {
220
+ const hex = input.slice(i + 1, i + 3)
221
+ if (isHexPair(hex)) {
222
+ const normalizedHex = hex.toUpperCase()
223
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16))
224
+
225
+ if (decodeUnreserved && isUnreserved(decoded)) {
226
+ output += decoded
227
+ } else {
228
+ output += '%' + normalizedHex
229
+ }
230
+
231
+ i += 2
232
+ continue
233
+ }
234
+ }
235
+
236
+ output += input[i]
186
237
  }
187
- if (components.query !== undefined) {
188
- components.query = func(components.query)
238
+
239
+ return output
240
+ }
241
+
242
+ /**
243
+ * Normalizes path data without turning reserved escapes into live path syntax.
244
+ * Valid escapes are uppercased, raw unsafe characters are escaped, and only
245
+ * unreserved bytes that are not `.` are decoded.
246
+ *
247
+ * @param {string} input
248
+ * @returns {string}
249
+ */
250
+ function normalizePathEncoding (input) {
251
+ let output = ''
252
+
253
+ for (let i = 0; i < input.length; i++) {
254
+ if (input[i] === '%' && i + 2 < input.length) {
255
+ const hex = input.slice(i + 1, i + 3)
256
+ if (isHexPair(hex)) {
257
+ const normalizedHex = hex.toUpperCase()
258
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16))
259
+
260
+ if (decoded !== '.' && isUnreserved(decoded)) {
261
+ output += decoded
262
+ } else {
263
+ output += '%' + normalizedHex
264
+ }
265
+
266
+ i += 2
267
+ continue
268
+ }
269
+ }
270
+
271
+ if (isPathCharacter(input[i])) {
272
+ output += input[i]
273
+ } else {
274
+ output += escape(input[i])
275
+ }
189
276
  }
190
- if (components.fragment !== undefined) {
191
- components.fragment = func(components.fragment)
277
+
278
+ return output
279
+ }
280
+
281
+ /**
282
+ * Escapes a component while preserving existing valid percent escapes.
283
+ *
284
+ * @param {string} input
285
+ * @returns {string}
286
+ */
287
+ function escapePreservingEscapes (input) {
288
+ let output = ''
289
+
290
+ for (let i = 0; i < input.length; i++) {
291
+ if (input[i] === '%' && i + 2 < input.length) {
292
+ const hex = input.slice(i + 1, i + 3)
293
+ if (isHexPair(hex)) {
294
+ output += '%' + hex.toUpperCase()
295
+ i += 2
296
+ continue
297
+ }
298
+ }
299
+
300
+ output += escape(input[i])
192
301
  }
193
- return components
302
+
303
+ return output
194
304
  }
195
305
 
196
306
  function recomposeAuthority (components, options) {
@@ -212,7 +322,7 @@ function recomposeAuthority (components, options) {
212
322
  if (ipV6res.isIPV6 === true) {
213
323
  host = `[${ipV6res.escapedHost}]`
214
324
  } else {
215
- host = components.host
325
+ host = reescapeHostDelimiters(host, false)
216
326
  }
217
327
  }
218
328
  uriTokens.push(host)
@@ -228,9 +338,12 @@ function recomposeAuthority (components, options) {
228
338
 
229
339
  module.exports = {
230
340
  recomposeAuthority,
231
- normalizeComponentEncoding,
341
+ reescapeHostDelimiters,
342
+ normalizePercentEncoding,
343
+ normalizePathEncoding,
344
+ escapePreservingEscapes,
232
345
  removeDotSegments,
233
346
  normalizeIPv4,
234
347
  normalizeIPv6,
235
- stringToHexStripped
348
+ stringArrayToHexStripped
236
349
  }
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": "2.4.0",
4
+ "version": "2.4.1",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
7
7
  "types": "types/index.d.ts",
@@ -0,0 +1,40 @@
1
+ 'use strict'
2
+
3
+ const tap = require('tap')
4
+ const test = tap.test
5
+ const fastURI = require('..')
6
+
7
+ test('parse preserves reserved path escapes as data', (t) => {
8
+ const components = fastURI.parse('http://example.com/a%2Fb/public/%2e%2e/admin')
9
+
10
+ t.equal(components.path, '/a%2Fb/public/%2E%2E/admin')
11
+ t.end()
12
+ })
13
+
14
+ test('normalize preserves percent-encoded path separators and dot segments', (t) => {
15
+ t.equal(
16
+ fastURI.normalize('http://example.com/public/%2e%2e/admin'),
17
+ 'http://example.com/public/%2E%2E/admin'
18
+ )
19
+
20
+ t.equal(
21
+ fastURI.normalize('http://example.com/a%2Fb'),
22
+ 'http://example.com/a%2Fb'
23
+ )
24
+
25
+ t.end()
26
+ })
27
+
28
+ test('equal does not treat reserved path escapes as live path syntax', (t) => {
29
+ t.equal(
30
+ fastURI.equal('http://example.com/public/%2e%2e/admin', 'http://example.com/admin', {}),
31
+ false
32
+ )
33
+
34
+ t.equal(
35
+ fastURI.equal('http://example.com/a%2Fb', 'http://example.com/a/b', {}),
36
+ false
37
+ )
38
+
39
+ t.end()
40
+ })
@@ -0,0 +1,134 @@
1
+ 'use strict'
2
+
3
+ const tap = require('tap')
4
+ const test = tap.test
5
+ const fastURI = require('..')
6
+
7
+ test('parse marks malformed authority and port inputs as errors', (t) => {
8
+ const malformedCases = [
9
+ {
10
+ input: 'http://[::1]foo',
11
+ expectedError: 'URI path must start with "/" when authority is present.'
12
+ },
13
+ {
14
+ input: 'http://[::1]:80abc/path',
15
+ expectedError: 'URI path must start with "/" when authority is present.'
16
+ },
17
+ {
18
+ input: 'http://example.com:80abc/path',
19
+ expectedError: 'URI path must start with "/" when authority is present.'
20
+ },
21
+ {
22
+ input: 'http://[::1]:65536',
23
+ expectedError: 'URI port is malformed.'
24
+ }
25
+ ]
26
+
27
+ t.plan(malformedCases.length)
28
+
29
+ malformedCases.forEach(({ input, expectedError }) => {
30
+ t.equal(fastURI.parse(input).error, expectedError, input)
31
+ })
32
+ })
33
+
34
+ test('normalize does not canonicalize malformed URLs into different valid URLs', (t) => {
35
+ const malformedCases = [
36
+ 'http://[::1]foo',
37
+ 'http://[::1]:80abc/path',
38
+ 'http://example.com:80abc/path',
39
+ 'http://[::1]:65536'
40
+ ]
41
+
42
+ t.plan(malformedCases.length)
43
+
44
+ malformedCases.forEach((input) => {
45
+ t.equal(fastURI.normalize(input), input, input)
46
+ })
47
+ })
48
+
49
+ test('equal returns false when either side is malformed', (t) => {
50
+ const malformedPairs = [
51
+ ['http://[::1]foo', 'http://[::1]/foo'],
52
+ ['http://[::1]:80abc/path', 'http://[::1]/abc/path'],
53
+ ['http://example.com:80abc/path', 'http://example.com/abc/path'],
54
+ ['http://[::1]:65536', 'http://[::1]:65536/']
55
+ ]
56
+
57
+ t.plan(malformedPairs.length)
58
+
59
+ malformedPairs.forEach(([left, right]) => {
60
+ t.equal(fastURI.equal(left, right), false, `${left} != ${right}`)
61
+ })
62
+ })
63
+
64
+ test('normalize preserves encoded authority delimiters in host', (t) => {
65
+ const cases = [
66
+ ['http://trusted.com%40evil.com/', 'http://trusted.com%40evil.com/'],
67
+ ['http://example.com%3A8080/', 'http://example.com%3A8080/'],
68
+ ['http://example.com%2Fevil.com/path', 'http://example.com%2Fevil.com/path'],
69
+ ['http://example.com%23fragment/path', 'http://example.com%23fragment/path'],
70
+ ['http://example.com%3Fq=evil/path', 'http://example.com%3Fq=evil/path'],
71
+ ['http://user%3Apass%40evil.com/', 'http://user%3Apass%40evil.com/'],
72
+ ['http://user@trusted.com%40evil.com/', 'http://user@trusted.com%40evil.com/'],
73
+ ['https://trusted.com%40evil.com/', 'https://trusted.com%40evil.com/'],
74
+ ['ws://trusted.com%40evil.com/chat', 'ws://trusted.com%40evil.com/chat'],
75
+ ['wss://trusted.com%40evil.com/chat', 'wss://trusted.com%40evil.com/chat']
76
+ ]
77
+
78
+ t.plan(cases.length)
79
+
80
+ cases.forEach(([input, expected]) => {
81
+ t.equal(fastURI.normalize(input), expected, input)
82
+ })
83
+ })
84
+
85
+ test('parse preserves encoded authority delimiters in host', (t) => {
86
+ const cases = [
87
+ ['http://trusted.com%40evil.com/', 'trusted.com%40evil.com'],
88
+ ['http://example.com%3A8080/', 'example.com%3A8080'],
89
+ ['http://user%3Apass%40evil.com/', 'user%3Apass%40evil.com']
90
+ ]
91
+
92
+ t.plan(cases.length)
93
+
94
+ cases.forEach(([input, expectedHost]) => {
95
+ t.equal(fastURI.parse(input).host, expectedHost, input)
96
+ })
97
+ })
98
+
99
+ test('equal returns false when encoded delimiters differ from live delimiters', (t) => {
100
+ const pairs = [
101
+ ['http://trusted.com%40evil.com/', 'http://trusted.com@evil.com/'],
102
+ ['http://example.com%3A8080/', 'http://example.com:8080/']
103
+ ]
104
+
105
+ t.plan(pairs.length)
106
+
107
+ pairs.forEach(([left, right]) => {
108
+ t.equal(fastURI.equal(left, right, {}), false, `${left} != ${right}`)
109
+ })
110
+ })
111
+
112
+ test('resolve preserves encoded authority delimiters', (t) => {
113
+ const result = fastURI.resolve('http://base.com/', '//trusted.com%40evil.com/path')
114
+ const parsed = fastURI.parse(result)
115
+
116
+ t.plan(1)
117
+ t.not(parsed.host, 'evil.com', '//trusted.com%40evil.com/path')
118
+ })
119
+
120
+ test('serialize escapes authority delimiters in host field', (t) => {
121
+ const result = fastURI.serialize({ scheme: 'http', host: 'trusted.com@evil.com', path: '/' })
122
+ const parsed = fastURI.parse(result)
123
+
124
+ t.plan(1)
125
+ t.not(parsed.host, 'evil.com', 'host: trusted.com@evil.com')
126
+ })
127
+
128
+ test('normalize does not double-decode %2540 into a live @', (t) => {
129
+ const result = fastURI.normalize('http://trusted.com%2540evil.com/')
130
+ const parsed = fastURI.parse(result)
131
+
132
+ t.plan(1)
133
+ t.not(parsed.host, 'trusted.com@evil.com', 'http://trusted.com%2540evil.com/')
134
+ })