fast-uri 3.0.6 → 3.1.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/utils.js CHANGED
@@ -1,61 +1,108 @@
1
1
  'use strict'
2
2
 
3
- const { HEX } = require('./scopedChars')
3
+ /** @type {(value: string) => boolean} */
4
+ const isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu)
4
5
 
5
- const IPV4_REG = /^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u
6
+ /** @type {(value: string) => boolean} */
7
+ const isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u)
6
8
 
7
- function normalizeIPv4 (host) {
8
- if (findToken(host, '.') < 3) { return { host, isIPV4: false } }
9
- const matches = host.match(IPV4_REG) || []
10
- const [address] = matches
11
- if (address) {
12
- return { host: stripLeadingZeros(address, '.'), isIPV4: true }
13
- } else {
14
- return { host, isIPV4: false }
15
- }
16
- }
9
+ /** @type {(value: string) => boolean} */
10
+ const isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu)
11
+
12
+ /** @type {(value: string) => boolean} */
13
+ const isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu)
14
+
15
+ /** @type {(value: string) => boolean} */
16
+ const isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu)
17
17
 
18
18
  /**
19
- * @param {string[]} input
20
- * @param {boolean} [keepZero=false]
21
- * @returns {string|undefined}
19
+ * @param {Array<string>} input
20
+ * @returns {string}
22
21
  */
23
- function stringArrayToHexStripped (input, keepZero = false) {
22
+ function stringArrayToHexStripped (input) {
24
23
  let acc = ''
25
- let strip = true
26
- for (const c of input) {
27
- if (HEX[c] === undefined) return undefined
28
- if (c !== '0' && strip === true) strip = false
29
- if (!strip) acc += c
24
+ let code = 0
25
+ let i = 0
26
+
27
+ for (i = 0; i < input.length; i++) {
28
+ code = input[i].charCodeAt(0)
29
+ if (code === 48) {
30
+ continue
31
+ }
32
+ if (!((code >= 48 && code <= 57) || (code >= 65 && code <= 70) || (code >= 97 && code <= 102))) {
33
+ return ''
34
+ }
35
+ acc += input[i]
36
+ break
37
+ }
38
+
39
+ for (i += 1; i < input.length; i++) {
40
+ code = input[i].charCodeAt(0)
41
+ if (!((code >= 48 && code <= 57) || (code >= 65 && code <= 70) || (code >= 97 && code <= 102))) {
42
+ return ''
43
+ }
44
+ acc += input[i]
30
45
  }
31
- if (keepZero && acc.length === 0) acc = '0'
32
46
  return acc
33
47
  }
34
48
 
49
+ /**
50
+ * @typedef {Object} GetIPV6Result
51
+ * @property {boolean} error - Indicates if there was an error parsing the IPv6 address.
52
+ * @property {string} address - The parsed IPv6 address.
53
+ * @property {string} [zone] - The zone identifier, if present.
54
+ */
55
+
56
+ /**
57
+ * @param {string} value
58
+ * @returns {boolean}
59
+ */
60
+ const nonSimpleDomain = RegExp.prototype.test.bind(/[^!"$&'()*+,\-.;=_`a-z{}~]/u)
61
+
62
+ /**
63
+ * @param {Array<string>} buffer
64
+ * @returns {boolean}
65
+ */
66
+ function consumeIsZone (buffer) {
67
+ buffer.length = 0
68
+ return true
69
+ }
70
+
71
+ /**
72
+ * @param {Array<string>} buffer
73
+ * @param {Array<string>} address
74
+ * @param {GetIPV6Result} output
75
+ * @returns {boolean}
76
+ */
77
+ function consumeHextets (buffer, address, output) {
78
+ if (buffer.length) {
79
+ const hex = stringArrayToHexStripped(buffer)
80
+ if (hex !== '') {
81
+ address.push(hex)
82
+ } else {
83
+ output.error = true
84
+ return false
85
+ }
86
+ buffer.length = 0
87
+ }
88
+ return true
89
+ }
90
+
91
+ /**
92
+ * @param {string} input
93
+ * @returns {GetIPV6Result}
94
+ */
35
95
  function getIPV6 (input) {
36
96
  let tokenCount = 0
37
97
  const output = { error: false, address: '', zone: '' }
98
+ /** @type {Array<string>} */
38
99
  const address = []
100
+ /** @type {Array<string>} */
39
101
  const buffer = []
40
- let isZone = false
41
102
  let endipv6Encountered = false
42
103
  let endIpv6 = false
43
104
 
44
- function consume () {
45
- if (buffer.length) {
46
- if (isZone === false) {
47
- const hex = stringArrayToHexStripped(buffer)
48
- if (hex !== undefined) {
49
- address.push(hex)
50
- } else {
51
- output.error = true
52
- return false
53
- }
54
- }
55
- buffer.length = 0
56
- }
57
- return true
58
- }
105
+ let consume = consumeHextets
59
106
 
60
107
  for (let i = 0; i < input.length; i++) {
61
108
  const cursor = input[i]
@@ -64,29 +111,28 @@ function getIPV6 (input) {
64
111
  if (endipv6Encountered === true) {
65
112
  endIpv6 = true
66
113
  }
67
- if (!consume()) { break }
68
- tokenCount++
69
- address.push(':')
70
- if (tokenCount > 7) {
114
+ if (!consume(buffer, address, output)) { break }
115
+ if (++tokenCount > 7) {
71
116
  // not valid
72
117
  output.error = true
73
118
  break
74
119
  }
75
- if (i - 1 >= 0 && input[i - 1] === ':') {
120
+ if (i > 0 && input[i - 1] === ':') {
76
121
  endipv6Encountered = true
77
122
  }
123
+ address.push(':')
78
124
  continue
79
125
  } else if (cursor === '%') {
80
- if (!consume()) { break }
126
+ if (!consume(buffer, address, output)) { break }
81
127
  // switch to zone detection
82
- isZone = true
128
+ consume = consumeIsZone
83
129
  } else {
84
130
  buffer.push(cursor)
85
131
  continue
86
132
  }
87
133
  }
88
134
  if (buffer.length) {
89
- if (isZone) {
135
+ if (consume === consumeIsZone) {
90
136
  output.zone = buffer.join('')
91
137
  } else if (endIpv6) {
92
138
  address.push(buffer.join(''))
@@ -98,6 +144,17 @@ function getIPV6 (input) {
98
144
  return output
99
145
  }
100
146
 
147
+ /**
148
+ * @typedef {Object} NormalizeIPv6Result
149
+ * @property {string} host - The normalized host.
150
+ * @property {string} [escapedHost] - The escaped host.
151
+ * @property {boolean} isIPV6 - Indicates if the host is an IPv6 address.
152
+ */
153
+
154
+ /**
155
+ * @param {string} host
156
+ * @returns {NormalizeIPv6Result}
157
+ */
101
158
  function normalizeIPv6 (host) {
102
159
  if (findToken(host, ':') < 2) { return { host, isIPV6: false } }
103
160
  const ipv6 = getIPV6(host)
@@ -109,35 +166,17 @@ function normalizeIPv6 (host) {
109
166
  newHost += '%' + ipv6.zone
110
167
  escapedHost += '%25' + ipv6.zone
111
168
  }
112
- return { host: newHost, escapedHost, isIPV6: true }
169
+ return { host: newHost, isIPV6: true, escapedHost }
113
170
  } else {
114
171
  return { host, isIPV6: false }
115
172
  }
116
173
  }
117
174
 
118
- function stripLeadingZeros (str, token) {
119
- let out = ''
120
- let skip = true
121
- const l = str.length
122
- for (let i = 0; i < l; i++) {
123
- const c = str[i]
124
- if (c === '0' && skip) {
125
- if ((i + 1 <= l && str[i + 1] === token) || i + 1 === l) {
126
- out += c
127
- skip = false
128
- }
129
- } else {
130
- if (c === token) {
131
- skip = true
132
- } else {
133
- skip = false
134
- }
135
- out += c
136
- }
137
- }
138
- return out
139
- }
140
-
175
+ /**
176
+ * @param {string} str
177
+ * @param {string} token
178
+ * @returns {number}
179
+ */
141
180
  function findToken (str, token) {
142
181
  let ind = 0
143
182
  for (let i = 0; i < str.length; i++) {
@@ -146,99 +185,238 @@ function findToken (str, token) {
146
185
  return ind
147
186
  }
148
187
 
149
- const RDS1 = /^\.\.?\//u
150
- const RDS2 = /^\/\.(?:\/|$)/u
151
- const RDS3 = /^\/\.\.(?:\/|$)/u
152
- const RDS5 = /^\/?(?:.|\n)*?(?=\/|$)/u
153
-
154
- function removeDotSegments (input) {
188
+ /**
189
+ * @param {string} path
190
+ * @returns {string}
191
+ *
192
+ * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4
193
+ */
194
+ function removeDotSegments (path) {
195
+ let input = path
155
196
  const output = []
197
+ let nextSlash = -1
198
+ let len = 0
156
199
 
157
- while (input.length) {
158
- if (input.match(RDS1)) {
159
- input = input.replace(RDS1, '')
160
- } else if (input.match(RDS2)) {
161
- input = input.replace(RDS2, '/')
162
- } else if (input.match(RDS3)) {
163
- input = input.replace(RDS3, '/')
164
- output.pop()
165
- } else if (input === '.' || input === '..') {
166
- input = ''
167
- } else {
168
- const im = input.match(RDS5)
169
- if (im) {
170
- const s = im[0]
171
- input = input.slice(s.length)
172
- output.push(s)
200
+ // eslint-disable-next-line no-cond-assign
201
+ while (len = input.length) {
202
+ if (len === 1) {
203
+ if (input === '.') {
204
+ break
205
+ } else if (input === '/') {
206
+ output.push('/')
207
+ break
173
208
  } else {
174
- throw new Error('Unexpected dot segment condition')
209
+ output.push(input)
210
+ break
211
+ }
212
+ } else if (len === 2) {
213
+ if (input[0] === '.') {
214
+ if (input[1] === '.') {
215
+ break
216
+ } else if (input[1] === '/') {
217
+ input = input.slice(2)
218
+ continue
219
+ }
220
+ } else if (input[0] === '/') {
221
+ if (input[1] === '.' || input[1] === '/') {
222
+ output.push('/')
223
+ break
224
+ }
225
+ }
226
+ } else if (len === 3) {
227
+ if (input === '/..') {
228
+ if (output.length !== 0) {
229
+ output.pop()
230
+ }
231
+ output.push('/')
232
+ break
233
+ }
234
+ }
235
+ if (input[0] === '.') {
236
+ if (input[1] === '.') {
237
+ if (input[2] === '/') {
238
+ input = input.slice(3)
239
+ continue
240
+ }
241
+ } else if (input[1] === '/') {
242
+ input = input.slice(2)
243
+ continue
175
244
  }
245
+ } else if (input[0] === '/') {
246
+ if (input[1] === '.') {
247
+ if (input[2] === '/') {
248
+ input = input.slice(2)
249
+ continue
250
+ } else if (input[2] === '.') {
251
+ if (input[3] === '/') {
252
+ input = input.slice(3)
253
+ if (output.length !== 0) {
254
+ output.pop()
255
+ }
256
+ continue
257
+ }
258
+ }
259
+ }
260
+ }
261
+
262
+ // Rule 2E: Move normal path segment to output
263
+ if ((nextSlash = input.indexOf('/', 1)) === -1) {
264
+ output.push(input)
265
+ break
266
+ } else {
267
+ output.push(input.slice(0, nextSlash))
268
+ input = input.slice(nextSlash)
176
269
  }
177
270
  }
271
+
178
272
  return output.join('')
179
273
  }
180
274
 
181
- function normalizeComponentEncoding (components, esc) {
182
- const func = esc !== true ? escape : unescape
183
- if (components.scheme !== undefined) {
184
- components.scheme = func(components.scheme)
185
- }
186
- if (components.userinfo !== undefined) {
187
- components.userinfo = func(components.userinfo)
188
- }
189
- if (components.host !== undefined) {
190
- components.host = func(components.host)
275
+ /**
276
+ * Normalizes percent escapes and optionally decodes only unreserved ASCII bytes.
277
+ * Reserved delimiters such as `%2F` and `%2E` stay escaped.
278
+ *
279
+ * @param {string} input
280
+ * @param {boolean} [decodeUnreserved=false]
281
+ * @returns {string}
282
+ */
283
+ function normalizePercentEncoding (input, decodeUnreserved = false) {
284
+ if (input.indexOf('%') === -1) {
285
+ return input
191
286
  }
192
- if (components.path !== undefined) {
193
- components.path = func(components.path)
287
+
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
+ const normalizedHex = hex.toUpperCase()
295
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16))
296
+
297
+ if (decodeUnreserved && isUnreserved(decoded)) {
298
+ output += decoded
299
+ } else {
300
+ output += '%' + normalizedHex
301
+ }
302
+
303
+ i += 2
304
+ continue
305
+ }
306
+ }
307
+
308
+ output += input[i]
194
309
  }
195
- if (components.query !== undefined) {
196
- components.query = func(components.query)
310
+
311
+ return output
312
+ }
313
+
314
+ /**
315
+ * Normalizes path data without turning reserved escapes into live path syntax.
316
+ * Valid escapes are uppercased, raw unsafe characters are escaped, and only
317
+ * unreserved bytes that are not `.` are decoded.
318
+ *
319
+ * @param {string} input
320
+ * @returns {string}
321
+ */
322
+ function normalizePathEncoding (input) {
323
+ let output = ''
324
+
325
+ for (let i = 0; i < input.length; i++) {
326
+ if (input[i] === '%' && i + 2 < input.length) {
327
+ const hex = input.slice(i + 1, i + 3)
328
+ if (isHexPair(hex)) {
329
+ const normalizedHex = hex.toUpperCase()
330
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16))
331
+
332
+ if (decoded !== '.' && isUnreserved(decoded)) {
333
+ output += decoded
334
+ } else {
335
+ output += '%' + normalizedHex
336
+ }
337
+
338
+ i += 2
339
+ continue
340
+ }
341
+ }
342
+
343
+ if (isPathCharacter(input[i])) {
344
+ output += input[i]
345
+ } else {
346
+ output += escape(input[i])
347
+ }
197
348
  }
198
- if (components.fragment !== undefined) {
199
- components.fragment = func(components.fragment)
349
+
350
+ return output
351
+ }
352
+
353
+ /**
354
+ * Escapes a component while preserving existing valid percent escapes.
355
+ *
356
+ * @param {string} input
357
+ * @returns {string}
358
+ */
359
+ function escapePreservingEscapes (input) {
360
+ let output = ''
361
+
362
+ for (let i = 0; i < input.length; i++) {
363
+ if (input[i] === '%' && i + 2 < input.length) {
364
+ const hex = input.slice(i + 1, i + 3)
365
+ if (isHexPair(hex)) {
366
+ output += '%' + hex.toUpperCase()
367
+ i += 2
368
+ continue
369
+ }
370
+ }
371
+
372
+ output += escape(input[i])
200
373
  }
201
- return components
374
+
375
+ return output
202
376
  }
203
377
 
204
- function recomposeAuthority (components) {
378
+ /**
379
+ * @param {import('../types/index').URIComponent} component
380
+ * @returns {string|undefined}
381
+ */
382
+ function recomposeAuthority (component) {
205
383
  const uriTokens = []
206
384
 
207
- if (components.userinfo !== undefined) {
208
- uriTokens.push(components.userinfo)
385
+ if (component.userinfo !== undefined) {
386
+ uriTokens.push(component.userinfo)
209
387
  uriTokens.push('@')
210
388
  }
211
389
 
212
- if (components.host !== undefined) {
213
- let host = unescape(components.host)
214
- const ipV4res = normalizeIPv4(host)
215
-
216
- if (ipV4res.isIPV4) {
217
- host = ipV4res.host
218
- } else {
219
- const ipV6res = normalizeIPv6(ipV4res.host)
390
+ if (component.host !== undefined) {
391
+ let host = unescape(component.host)
392
+ if (!isIPv4(host)) {
393
+ const ipV6res = normalizeIPv6(host)
220
394
  if (ipV6res.isIPV6 === true) {
221
395
  host = `[${ipV6res.escapedHost}]`
222
396
  } else {
223
- host = components.host
397
+ host = component.host
224
398
  }
225
399
  }
226
400
  uriTokens.push(host)
227
401
  }
228
402
 
229
- if (typeof components.port === 'number' || typeof components.port === 'string') {
403
+ if (typeof component.port === 'number' || typeof component.port === 'string') {
230
404
  uriTokens.push(':')
231
- uriTokens.push(String(components.port))
405
+ uriTokens.push(String(component.port))
232
406
  }
233
407
 
234
408
  return uriTokens.length ? uriTokens.join('') : undefined
235
409
  };
236
410
 
237
411
  module.exports = {
412
+ nonSimpleDomain,
238
413
  recomposeAuthority,
239
- normalizeComponentEncoding,
414
+ normalizePercentEncoding,
415
+ normalizePathEncoding,
416
+ escapePreservingEscapes,
240
417
  removeDotSegments,
241
- normalizeIPv4,
418
+ isIPv4,
419
+ isUUID,
242
420
  normalizeIPv6,
243
421
  stringArrayToHexStripped
244
422
  }
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": "3.0.6",
4
+ "version": "3.1.1",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
7
7
  "types": "types/index.d.ts",
@@ -46,23 +46,23 @@
46
46
  }
47
47
  ],
48
48
  "scripts": {
49
- "bench": "node benchmark.js",
50
49
  "lint": "eslint",
51
50
  "lint:fix": "eslint --fix",
52
51
  "test": "npm run test:unit && npm run test:typescript",
52
+ "test:browser:chromium": "playwright-test ./test/* --runner tape --browser=chromium",
53
+ "test:browser:firefox": "playwright-test ./test/* --runner tape --browser=firefox",
54
+ "test:browser:webkit": "playwright-test ./test/* --runner tape --browser=webkit",
55
+ "test:browser": "npm run test:browser:chromium && npm run test:browser:firefox && npm run test:browser:webkit",
53
56
  "test:unit": "tape test/**/*.js",
54
57
  "test:unit:dev": "npm run test:unit -- --coverage-report=html",
55
58
  "test:typescript": "tsd"
56
59
  },
57
60
  "devDependencies": {
58
- "@fastify/pre-commit": "^2.1.0",
59
61
  "ajv": "^8.16.0",
60
- "benchmark": "^2.1.4",
61
- "coveralls": "^3.1.1",
62
62
  "eslint": "^9.17.0",
63
- "neostandard": "^0.12.0",
63
+ "neostandard": "^0.13.0",
64
+ "playwright-test": "^14.1.12",
64
65
  "tape": "^5.8.1",
65
- "tsd": "^0.31.0",
66
- "uri-js": "^4.4.1"
66
+ "tsd": "^0.33.0"
67
67
  }
68
68
  }
package/test/ajv.test.js CHANGED
@@ -1,9 +1,13 @@
1
+ 'use strict'
2
+
3
+ const test = require('tape')
4
+ const fastURI = require('..')
5
+
1
6
  const AJV = require('ajv')
2
- const fastUri = require('../')
7
+
3
8
  const ajv = new AJV({
4
- uriResolver: fastUri // comment this line to see it works with uri-js
9
+ uriResolver: fastURI // comment this line to see it works with uri-js
5
10
  })
6
- const test = require('tape')
7
11
 
8
12
  test('ajv', t => {
9
13
  t.plan(1)
@@ -1,9 +1,9 @@
1
1
  'use strict'
2
2
 
3
3
  const test = require('tape')
4
- const URI = require('../')
4
+ const fastURI = require('..')
5
5
 
6
- const fn = URI.equal
6
+ const fn = fastURI.equal
7
7
  const runTest = (t, suite) => {
8
8
  suite.forEach(s => {
9
9
  const operator = s.result ? '==' : '!='
@@ -65,6 +65,11 @@ test('URN Equals', (t) => {
65
65
  // t.equal(URI.equal('urn:foo:a123%2C456', 'URN:FOO:a123%2c456'), true)
66
66
 
67
67
  runTest(t, suite)
68
+
69
+ t.throws(() => {
70
+ fn('urn:', 'urn:FOO:a123,456')
71
+ }, 'URN without nid cannot be serialized')
72
+
68
73
  t.end()
69
74
  })
70
75