fast-uri 3.1.2 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +49 -1
- package/benchmark/encoder.mjs +54 -0
- package/lib/utils.js +87 -6
- package/package.json +4 -4
- package/test/equal.test.js +51 -0
- package/test/security-utf8-encoding.test.js +0 -0
- package/tsconfig.json +8 -7
- package/types/index.d.ts +0 -9
- package/types/index.tst.ts +15 -0
- package/.github/workflows/package-manager-ci.yml +0 -24
- package/types/index.test-d.ts +0 -17
package/.github/workflows/ci.yml
CHANGED
|
@@ -23,6 +23,54 @@ permissions:
|
|
|
23
23
|
contents: read
|
|
24
24
|
|
|
25
25
|
jobs:
|
|
26
|
+
type-tests:
|
|
27
|
+
name: Type Tests
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
strategy:
|
|
30
|
+
matrix:
|
|
31
|
+
node-version: ['20', '22', '24', '26']
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v6
|
|
34
|
+
with:
|
|
35
|
+
persist-credentials: false
|
|
36
|
+
|
|
37
|
+
- uses: actions/setup-node@v6
|
|
38
|
+
with:
|
|
39
|
+
node-version: ${{ matrix.node-version }}
|
|
40
|
+
cache: 'npm'
|
|
41
|
+
cache-dependency-path: package.json
|
|
42
|
+
check-latest: true
|
|
43
|
+
|
|
44
|
+
- name: Install dependencies
|
|
45
|
+
run: npm install --ignore-scripts
|
|
46
|
+
|
|
47
|
+
- name: Run type tests
|
|
48
|
+
run: npm run test:typescript
|
|
49
|
+
|
|
50
|
+
unit-tests:
|
|
51
|
+
name: Unit Tests
|
|
52
|
+
runs-on: ubuntu-latest
|
|
53
|
+
strategy:
|
|
54
|
+
matrix:
|
|
55
|
+
node-version: ['16', '18', '20', '22', '24', '26']
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/checkout@v6
|
|
58
|
+
with:
|
|
59
|
+
persist-credentials: false
|
|
60
|
+
|
|
61
|
+
- uses: actions/setup-node@v6
|
|
62
|
+
with:
|
|
63
|
+
node-version: ${{ matrix.node-version }}
|
|
64
|
+
cache: 'npm'
|
|
65
|
+
cache-dependency-path: package.json
|
|
66
|
+
check-latest: true
|
|
67
|
+
|
|
68
|
+
- name: Install dependencies
|
|
69
|
+
run: npm install --ignore-scripts
|
|
70
|
+
|
|
71
|
+
- name: Run unit tests
|
|
72
|
+
run: npm test
|
|
73
|
+
|
|
26
74
|
test-regression-check-node10:
|
|
27
75
|
name: Test compatibility with Node.js 10
|
|
28
76
|
runs-on: ubuntu-latest
|
|
@@ -103,4 +151,4 @@ jobs:
|
|
|
103
151
|
with:
|
|
104
152
|
license-check: true
|
|
105
153
|
lint: true
|
|
106
|
-
node-versions: '["16", "18", "20", "22", "24"]'
|
|
154
|
+
node-versions: '["16", "18", "20", "22", "24", "26"]'
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Bench } from 'tinybench'
|
|
2
|
+
import { fastUri } from '../index.js'
|
|
3
|
+
|
|
4
|
+
const { serialize, normalize, parse } = fastUri
|
|
5
|
+
|
|
6
|
+
const asciiPath = '/users/profile/settings/account'
|
|
7
|
+
const asciiPathWithSpaces = '/users/john doe/profile page'
|
|
8
|
+
const cjkPath = '/ユーザー/プロフィール/設定'
|
|
9
|
+
const mixedPath = '/users/日本/profile-📱-emoji/設定'
|
|
10
|
+
const emojiPath = '/🚀/🌍/🎉/end'
|
|
11
|
+
const longAsciiPath = '/' + 'a/b/c/d/e/f/g/h/'.repeat(20)
|
|
12
|
+
const longUtf8Path = '/' + '日本/中文/한국/'.repeat(20)
|
|
13
|
+
|
|
14
|
+
const bench = new Bench({ name: 'encoder bench', time: 1000 })
|
|
15
|
+
|
|
16
|
+
bench.add('serialize ascii path', () => {
|
|
17
|
+
serialize({ scheme: 'http', host: 'example.com', path: asciiPath })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
bench.add('serialize ascii path with spaces', () => {
|
|
21
|
+
serialize({ scheme: 'http', host: 'example.com', path: asciiPathWithSpaces })
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
bench.add('serialize cjk path', () => {
|
|
25
|
+
serialize({ scheme: 'http', host: 'example.com', path: cjkPath })
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
bench.add('serialize mixed path', () => {
|
|
29
|
+
serialize({ scheme: 'http', host: 'example.com', path: mixedPath })
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
bench.add('serialize emoji path', () => {
|
|
33
|
+
serialize({ scheme: 'http', host: 'example.com', path: emojiPath })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
bench.add('serialize long ascii path', () => {
|
|
37
|
+
serialize({ scheme: 'http', host: 'example.com', path: longAsciiPath })
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
bench.add('serialize long utf8 path', () => {
|
|
41
|
+
serialize({ scheme: 'http', host: 'example.com', path: longUtf8Path })
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
bench.add('normalize utf8 uri', () => {
|
|
45
|
+
normalize('http://example.com/日本/profile')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
bench.add('parse utf8 uri', () => {
|
|
49
|
+
parse('http://example.com/日本/profile')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
await bench.run()
|
|
53
|
+
console.log(bench.name)
|
|
54
|
+
console.table(bench.table())
|
package/lib/utils.js
CHANGED
|
@@ -331,6 +331,55 @@ function normalizePercentEncoding (input, decodeUnreserved = false) {
|
|
|
331
331
|
return output
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
+
/** Pre-built `%XX` strings for every byte. Avoids per-call hex math/concat. */
|
|
335
|
+
const BYTE_HEX = new Array(256)
|
|
336
|
+
{
|
|
337
|
+
const HEX_DIGITS = '0123456789ABCDEF'
|
|
338
|
+
for (let i = 0; i < 256; i++) {
|
|
339
|
+
BYTE_HEX[i] = '%' + HEX_DIGITS[i >> 4] + HEX_DIGITS[i & 0xF]
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Mirrors the pass-through set of the deprecated `escape()` global:
|
|
345
|
+
* `A-Z a-z 0-9 * + - . / @ _`. Keeps drop-in semantics for ASCII input.
|
|
346
|
+
*
|
|
347
|
+
* @param {number} cp
|
|
348
|
+
* @returns {boolean}
|
|
349
|
+
*/
|
|
350
|
+
function isEscapeSafe (cp) {
|
|
351
|
+
return (
|
|
352
|
+
(cp >= 0x30 && cp <= 0x39) ||
|
|
353
|
+
(cp >= 0x41 && cp <= 0x5A) ||
|
|
354
|
+
(cp >= 0x61 && cp <= 0x7A) ||
|
|
355
|
+
cp === 0x2A || cp === 0x2B || cp === 0x2D || cp === 0x2E ||
|
|
356
|
+
cp === 0x2F || cp === 0x40 || cp === 0x5F
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* RFC 3986 percent-encode a non-ASCII Unicode code point as UTF-8 bytes.
|
|
362
|
+
* Caller handles the ASCII branch inline for speed.
|
|
363
|
+
*
|
|
364
|
+
* @param {number} cp - Unicode code point (>= 0x80, <= 0x10FFFF)
|
|
365
|
+
* @returns {string}
|
|
366
|
+
*/
|
|
367
|
+
function percentEncodeNonAscii (cp) {
|
|
368
|
+
if (cp < 0x800) {
|
|
369
|
+
return BYTE_HEX[0xC0 | (cp >> 6)] +
|
|
370
|
+
BYTE_HEX[0x80 | (cp & 0x3F)]
|
|
371
|
+
}
|
|
372
|
+
if (cp < 0x10000) {
|
|
373
|
+
return BYTE_HEX[0xE0 | (cp >> 12)] +
|
|
374
|
+
BYTE_HEX[0x80 | ((cp >> 6) & 0x3F)] +
|
|
375
|
+
BYTE_HEX[0x80 | (cp & 0x3F)]
|
|
376
|
+
}
|
|
377
|
+
return BYTE_HEX[0xF0 | (cp >> 18)] +
|
|
378
|
+
BYTE_HEX[0x80 | ((cp >> 12) & 0x3F)] +
|
|
379
|
+
BYTE_HEX[0x80 | ((cp >> 6) & 0x3F)] +
|
|
380
|
+
BYTE_HEX[0x80 | (cp & 0x3F)]
|
|
381
|
+
}
|
|
382
|
+
|
|
334
383
|
/**
|
|
335
384
|
* Normalizes path data without turning reserved escapes into live path syntax.
|
|
336
385
|
* Valid escapes are uppercased, raw unsafe characters are escaped, and only
|
|
@@ -343,7 +392,8 @@ function normalizePathEncoding (input) {
|
|
|
343
392
|
let output = ''
|
|
344
393
|
|
|
345
394
|
for (let i = 0; i < input.length; i++) {
|
|
346
|
-
|
|
395
|
+
const ch = input[i]
|
|
396
|
+
if (ch === '%' && i + 2 < input.length) {
|
|
347
397
|
const hex = input.slice(i + 1, i + 3)
|
|
348
398
|
if (isHexPair(hex)) {
|
|
349
399
|
const normalizedHex = hex.toUpperCase()
|
|
@@ -360,10 +410,25 @@ function normalizePathEncoding (input) {
|
|
|
360
410
|
}
|
|
361
411
|
}
|
|
362
412
|
|
|
363
|
-
if (isPathCharacter(
|
|
364
|
-
output +=
|
|
413
|
+
if (isPathCharacter(ch)) {
|
|
414
|
+
output += ch
|
|
365
415
|
} else {
|
|
366
|
-
|
|
416
|
+
const code = input.charCodeAt(i)
|
|
417
|
+
if (code < 0x80) {
|
|
418
|
+
output += isEscapeSafe(code) ? ch : BYTE_HEX[code]
|
|
419
|
+
} else if (code < 0xD800 || code > 0xDFFF) {
|
|
420
|
+
output += percentEncodeNonAscii(code)
|
|
421
|
+
} else if (code <= 0xDBFF && i + 1 < input.length) {
|
|
422
|
+
const low = input.charCodeAt(i + 1)
|
|
423
|
+
if (low >= 0xDC00 && low <= 0xDFFF) {
|
|
424
|
+
output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
|
|
425
|
+
i++
|
|
426
|
+
} else {
|
|
427
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
431
|
+
}
|
|
367
432
|
}
|
|
368
433
|
}
|
|
369
434
|
|
|
@@ -380,7 +445,8 @@ function escapePreservingEscapes (input) {
|
|
|
380
445
|
let output = ''
|
|
381
446
|
|
|
382
447
|
for (let i = 0; i < input.length; i++) {
|
|
383
|
-
|
|
448
|
+
const ch = input[i]
|
|
449
|
+
if (ch === '%' && i + 2 < input.length) {
|
|
384
450
|
const hex = input.slice(i + 1, i + 3)
|
|
385
451
|
if (isHexPair(hex)) {
|
|
386
452
|
output += '%' + hex.toUpperCase()
|
|
@@ -389,7 +455,22 @@ function escapePreservingEscapes (input) {
|
|
|
389
455
|
}
|
|
390
456
|
}
|
|
391
457
|
|
|
392
|
-
|
|
458
|
+
const code = input.charCodeAt(i)
|
|
459
|
+
if (code < 0x80) {
|
|
460
|
+
output += isEscapeSafe(code) ? ch : BYTE_HEX[code]
|
|
461
|
+
} else if (code < 0xD800 || code > 0xDFFF) {
|
|
462
|
+
output += percentEncodeNonAscii(code)
|
|
463
|
+
} else if (code <= 0xDBFF && i + 1 < input.length) {
|
|
464
|
+
const low = input.charCodeAt(i + 1)
|
|
465
|
+
if (low >= 0xDC00 && low <= 0xDFFF) {
|
|
466
|
+
output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
|
|
467
|
+
i++
|
|
468
|
+
} else {
|
|
469
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
470
|
+
}
|
|
471
|
+
} else {
|
|
472
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
473
|
+
}
|
|
393
474
|
}
|
|
394
475
|
|
|
395
476
|
return output
|
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
|
+
"version": "4.0.0",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
7
7
|
"types": "types/index.d.ts",
|
|
@@ -48,14 +48,14 @@
|
|
|
48
48
|
"scripts": {
|
|
49
49
|
"lint": "eslint",
|
|
50
50
|
"lint:fix": "eslint --fix",
|
|
51
|
-
"test": "npm run test:unit
|
|
51
|
+
"test": "npm run test:unit",
|
|
52
52
|
"test:browser:chromium": "playwright-test ./test/* --runner tape --browser=chromium",
|
|
53
53
|
"test:browser:firefox": "playwright-test ./test/* --runner tape --browser=firefox",
|
|
54
54
|
"test:browser:webkit": "playwright-test ./test/* --runner tape --browser=webkit",
|
|
55
55
|
"test:browser": "npm run test:browser:chromium && npm run test:browser:firefox && npm run test:browser:webkit",
|
|
56
56
|
"test:unit": "tape test/**/*.js",
|
|
57
57
|
"test:unit:dev": "npm run test:unit -- --coverage-report=html",
|
|
58
|
-
"test:typescript": "
|
|
58
|
+
"test:typescript": "tstyche"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"ajv": "^8.16.0",
|
|
@@ -63,6 +63,6 @@
|
|
|
63
63
|
"neostandard": "^0.13.0",
|
|
64
64
|
"playwright-test": "^14.1.12",
|
|
65
65
|
"tape": "^5.8.1",
|
|
66
|
-
"
|
|
66
|
+
"tstyche": "^7.0.0"
|
|
67
67
|
}
|
|
68
68
|
}
|
package/test/equal.test.js
CHANGED
|
@@ -115,3 +115,54 @@ test('URI Equals tolerates malformed fragments', (t) => {
|
|
|
115
115
|
)
|
|
116
116
|
t.end()
|
|
117
117
|
})
|
|
118
|
+
|
|
119
|
+
test('URI Equals — raw UTF-8 char equals its percent-encoded UTF-8 form', (t) => {
|
|
120
|
+
const suite = [
|
|
121
|
+
// BMP characters
|
|
122
|
+
{ pair: ['http://example.com/日本', 'http://example.com/%E6%97%A5%E6%9C%AC'], result: true },
|
|
123
|
+
{ pair: ['http://example.com/café', 'http://example.com/caf%C3%A9'], result: true },
|
|
124
|
+
{ pair: ['http://example.com/€', 'http://example.com/%E2%82%AC'], result: true },
|
|
125
|
+
{ pair: ['http://example.com/中文', 'http://example.com/%E4%B8%AD%E6%96%87'], result: true },
|
|
126
|
+
// supplementary plane (emoji, surrogate pairs)
|
|
127
|
+
{ pair: ['http://example.com/🚀', 'http://example.com/%F0%9F%9A%80'], result: true },
|
|
128
|
+
{ pair: ['http://example.com/😀', 'http://example.com/%F0%9F%98%80'], result: true },
|
|
129
|
+
// mixed encoded + raw in same path
|
|
130
|
+
{ pair: ['http://example.com/users/日本/profile', 'http://example.com/users/%E6%97%A5%E6%9C%AC/profile'], result: true },
|
|
131
|
+
{ pair: ['http://example.com/a/€/b', 'http://example.com/a/%E2%82%AC/b'], result: true }
|
|
132
|
+
]
|
|
133
|
+
runTest(t, suite)
|
|
134
|
+
t.end()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('URI Equals — percent-encoded UTF-8 is case-insensitive in hex digits', (t) => {
|
|
138
|
+
const suite = [
|
|
139
|
+
{ pair: ['http://example.com/caf%C3%A9', 'http://example.com/caf%c3%a9'], result: true },
|
|
140
|
+
{ pair: ['http://example.com/%E6%97%A5', 'http://example.com/%e6%97%a5'], result: true },
|
|
141
|
+
{ pair: ['http://example.com/%F0%9F%9A%80', 'http://example.com/%f0%9f%9a%80'], result: true }
|
|
142
|
+
]
|
|
143
|
+
runTest(t, suite)
|
|
144
|
+
t.end()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('URI Equals — different Unicode code points are not equal', (t) => {
|
|
148
|
+
const suite = [
|
|
149
|
+
{ pair: ['http://example.com/日本', 'http://example.com/中文'], result: false },
|
|
150
|
+
{ pair: ['http://example.com/café', 'http://example.com/cafe'], result: false },
|
|
151
|
+
{ pair: ['http://example.com/🚀', 'http://example.com/🎉'], result: false }
|
|
152
|
+
]
|
|
153
|
+
runTest(t, suite)
|
|
154
|
+
t.end()
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('URI Equals — Latin-1 byte is not equal to UTF-8 multi-byte encoding', (t) => {
|
|
158
|
+
// Parser-differential guard: %C3%A9 (UTF-8 for "é") must NOT equal %E9
|
|
159
|
+
// (raw Latin-1 byte). The old escape() emitted single Latin-1 bytes for
|
|
160
|
+
// non-ASCII, which would have made these incorrectly compare equal in a
|
|
161
|
+
// round-tripped URI.
|
|
162
|
+
const suite = [
|
|
163
|
+
{ pair: ['http://example.com/caf%C3%A9', 'http://example.com/caf%E9'], result: false },
|
|
164
|
+
{ pair: ['http://example.com/%E6%97%A5', 'http://example.com/%E6'], result: false }
|
|
165
|
+
]
|
|
166
|
+
runTest(t, suite)
|
|
167
|
+
t.end()
|
|
168
|
+
})
|
|
Binary file
|
package/tsconfig.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"allowJs": true,
|
|
4
|
+
"checkJs": true,
|
|
5
|
+
"strict": true,
|
|
6
|
+
"noImplicitAny": true,
|
|
7
|
+
"target": "es2022",
|
|
8
|
+
"noEmit": true
|
|
9
|
+
}
|
|
9
10
|
}
|
package/types/index.d.ts
CHANGED
|
@@ -28,15 +28,6 @@ declare namespace fastUri {
|
|
|
28
28
|
nid?: string;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/**
|
|
32
|
-
* @deprecated Use Options instead
|
|
33
|
-
*/
|
|
34
|
-
export type options = Options
|
|
35
|
-
/**
|
|
36
|
-
* @deprecated Use URIComponent instead
|
|
37
|
-
*/
|
|
38
|
-
export type URIComponents = URIComponent
|
|
39
|
-
|
|
40
31
|
export function normalize (uri: string, opts?: Options): string
|
|
41
32
|
export function normalize (uri: URIComponent, opts?: Options): URIComponent
|
|
42
33
|
export function normalize (uri: any, opts?: Options): any
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import uri, {
|
|
2
|
+
type URIComponent,
|
|
3
|
+
} from '..'
|
|
4
|
+
import { expect } from 'tstyche'
|
|
5
|
+
|
|
6
|
+
const parsed = uri.parse('foo')
|
|
7
|
+
expect(parsed).type.toBe<URIComponent>()
|
|
8
|
+
|
|
9
|
+
const parsed2 = uri.parse('foo', {
|
|
10
|
+
domainHost: true,
|
|
11
|
+
scheme: 'https',
|
|
12
|
+
unicodeSupport: false
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
expect(parsed2).type.toBe<URIComponent>()
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
name: package-manager-ci
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches:
|
|
6
|
-
- main
|
|
7
|
-
- next
|
|
8
|
-
- 'v*'
|
|
9
|
-
paths-ignore:
|
|
10
|
-
- 'docs/**'
|
|
11
|
-
- '*.md'
|
|
12
|
-
pull_request:
|
|
13
|
-
paths-ignore:
|
|
14
|
-
- 'docs/**'
|
|
15
|
-
- '*.md'
|
|
16
|
-
|
|
17
|
-
permissions:
|
|
18
|
-
contents: read
|
|
19
|
-
|
|
20
|
-
jobs:
|
|
21
|
-
test:
|
|
22
|
-
permissions:
|
|
23
|
-
contents: read
|
|
24
|
-
uses: fastify/workflows/.github/workflows/plugins-ci-package-manager.yml@v6
|
package/types/index.test-d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import uri, { URIComponents, URIComponent, Options, options } from '..'
|
|
2
|
-
import { expectDeprecated, expectType } from 'tsd'
|
|
3
|
-
|
|
4
|
-
const parsed = uri.parse('foo')
|
|
5
|
-
expectType<URIComponents>(parsed)
|
|
6
|
-
const parsed2 = uri.parse('foo', {
|
|
7
|
-
domainHost: true,
|
|
8
|
-
scheme: 'https',
|
|
9
|
-
unicodeSupport: false
|
|
10
|
-
})
|
|
11
|
-
expectType<URIComponents>(parsed2)
|
|
12
|
-
|
|
13
|
-
expectType<URIComponent>({} as URIComponents)
|
|
14
|
-
expectDeprecated({} as URIComponents)
|
|
15
|
-
|
|
16
|
-
expectType<Options>({} as options)
|
|
17
|
-
expectDeprecated({} as options)
|