@openclaw/lobster 2026.6.33 → 2026.6.35

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.
Files changed (41) hide show
  1. package/node_modules/fast-uri/.github/dependabot.yml +42 -2
  2. package/node_modules/fast-uri/.github/workflows/ci.yml +54 -6
  3. package/node_modules/fast-uri/.github/workflows/lock-threads.yml +1 -1
  4. package/node_modules/fast-uri/.github/workflows/ossf-scorecard.yml +45 -0
  5. package/node_modules/fast-uri/README.md +10 -9
  6. package/node_modules/fast-uri/SECURITY.md +173 -0
  7. package/node_modules/fast-uri/benchmark/benchmark.mjs +71 -0
  8. package/node_modules/fast-uri/benchmark/encoder.mjs +54 -0
  9. package/node_modules/fast-uri/index.js +277 -45
  10. package/node_modules/fast-uri/lib/schemes.js +384 -7
  11. package/node_modules/fast-uri/lib/utils.js +458 -95
  12. package/node_modules/fast-uri/package.json +4 -4
  13. package/node_modules/fast-uri/test/component-safe-serialization.test.js +163 -0
  14. package/node_modules/fast-uri/test/equal.test.js +94 -3
  15. package/node_modules/fast-uri/test/fixtures/uri-js-parse.json +24 -1
  16. package/node_modules/fast-uri/test/fixtures/uri-js-serialize.json +23 -1
  17. package/node_modules/fast-uri/test/ipv6-canonical.test.js +34 -0
  18. package/node_modules/fast-uri/test/ipv6-validation.test.js +154 -0
  19. package/node_modules/fast-uri/test/malformed-percent.test.js +77 -0
  20. package/node_modules/fast-uri/test/malformed-urn.test.js +61 -0
  21. package/node_modules/fast-uri/test/parse.test.js +150 -3
  22. package/node_modules/fast-uri/test/query-fragment-normalization.test.js +33 -0
  23. package/node_modules/fast-uri/test/reserved-path-normalization.test.js +109 -0
  24. package/node_modules/fast-uri/test/scheme-normalization.test.js +16 -0
  25. package/node_modules/fast-uri/test/scheme-validation.test.js +122 -0
  26. package/node_modules/fast-uri/test/security-normalization.test.js +101 -0
  27. package/node_modules/fast-uri/test/security-utf8-encoding.test.js +0 -0
  28. package/node_modules/fast-uri/test/security.test.js +301 -3
  29. package/node_modules/fast-uri/test/serialize.test.js +144 -0
  30. package/node_modules/fast-uri/test/uri-js-compatibility.test.js +0 -3
  31. package/node_modules/fast-uri/test/uri-js.test.js +4 -4
  32. package/node_modules/fast-uri/test/urn-full-input.test.js +29 -0
  33. package/node_modules/fast-uri/test/util.test.js +9 -0
  34. package/node_modules/fast-uri/test/websocket-query-preservation.test.js +24 -0
  35. package/node_modules/fast-uri/tsconfig.json +8 -7
  36. package/node_modules/fast-uri/types/index.d.ts +4 -9
  37. package/node_modules/fast-uri/types/index.tst.ts +31 -0
  38. package/npm-shrinkwrap.json +5 -5
  39. package/package.json +4 -4
  40. package/node_modules/fast-uri/.github/workflows/package-manager-ci.yml +0 -24
  41. package/node_modules/fast-uri/types/index.test-d.ts +0 -17
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fast-uri",
3
3
  "description": "Dependency-free RFC 3986 URI toolbox",
4
- "version": "3.1.2",
4
+ "version": "4.1.4",
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 && npm run test:typescript",
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": "tsd"
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
- "tsd": "^0.33.0"
66
+ "tstyche": "^7.0.0"
67
67
  }
68
68
  }
@@ -0,0 +1,163 @@
1
+ 'use strict'
2
+
3
+ const test = require('tape')
4
+ const fastURI = require('..')
5
+
6
+ test('userinfo serialization cannot terminate the authority', (t) => {
7
+ const uri = fastURI.serialize({
8
+ scheme: 'http',
9
+ userinfo: 'attacker.example/',
10
+ host: 'trusted.example',
11
+ path: '/'
12
+ })
13
+
14
+ t.equal(uri, 'http://attacker.example%2F@trusted.example/', 'slash is encoded as userinfo data')
15
+
16
+ const reparsed = fastURI.parse(uri)
17
+ t.equal(reparsed.host, 'trusted.example', 'reparsing retains the supplied host')
18
+ t.equal(reparsed.userinfo, 'attacker.example%2F', 'encoded slash remains in userinfo')
19
+ t.equal(
20
+ fastURI.serialize({ userinfo: 'user@/?:#[]\\', host: 'example.test' }),
21
+ '//user%40%2F%3F:%23%5B%5D%5C@example.test',
22
+ 'all userinfo component delimiters are encoded as data'
23
+ )
24
+ t.end()
25
+ })
26
+
27
+ test('port serialization rejects non-digit values', (t) => {
28
+ const malformedPorts = [
29
+ '@127.0.0.1:8124',
30
+ '8080@evil.example',
31
+ '8080/path',
32
+ '8080?query',
33
+ '8080#fragment',
34
+ '8080:9000',
35
+ '-1',
36
+ '1.5',
37
+ 1.5,
38
+ NaN,
39
+ Infinity,
40
+ '\u0661'
41
+ ]
42
+
43
+ for (const port of malformedPorts) {
44
+ t.throws(
45
+ () => fastURI.serialize({ scheme: 'http', host: 'trusted.example', port, path: '/app' }),
46
+ /URI port is malformed\./,
47
+ String(port)
48
+ )
49
+ }
50
+
51
+ t.throws(
52
+ () => fastURI.normalize({ scheme: 'http', host: 'trusted.example', port: '@evil.example' }),
53
+ /URI port is malformed\./,
54
+ 'object normalization rejects a malformed port'
55
+ )
56
+ t.equal(
57
+ fastURI.equal(
58
+ { scheme: 'http', host: 'trusted.example', port: '@evil.example' },
59
+ { scheme: 'http', host: 'trusted.example', port: '@evil.example' }
60
+ ),
61
+ false,
62
+ 'object equality fails closed for a malformed port'
63
+ )
64
+ t.end()
65
+ })
66
+
67
+ test('port serialization preserves RFC 3986 digit values', (t) => {
68
+ const validPorts = [
69
+ [8080, '8080'],
70
+ ['8080', '8080'],
71
+ ['00080', '00080'],
72
+ ['', '']
73
+ ]
74
+
75
+ for (const [port, expected] of validPorts) {
76
+ t.equal(
77
+ fastURI.serialize({ scheme: 'uri', host: 'example.test', port }),
78
+ `uri://example.test:${expected}`,
79
+ JSON.stringify(port)
80
+ )
81
+ }
82
+ t.end()
83
+ })
84
+
85
+ test('query and fragment serialization cannot inject component delimiters', (t) => {
86
+ const uri = fastURI.serialize({
87
+ scheme: 'x',
88
+ path: '/resource',
89
+ query: 'key=value#still-query',
90
+ fragment: 'first#still-fragment'
91
+ })
92
+
93
+ t.equal(
94
+ uri,
95
+ 'x:/resource?key=value%23still-query#first%23still-fragment',
96
+ 'raw hashes are encoded within their supplied components'
97
+ )
98
+
99
+ const reparsed = fastURI.parse(uri)
100
+ t.equal(reparsed.query, 'key=value%23still-query', 'hash remains query data')
101
+ t.equal(reparsed.fragment, 'first%23still-fragment', 'hash remains fragment data')
102
+ t.end()
103
+ })
104
+
105
+ test('component serializers preserve each RFC 3986 literal character set', (t) => {
106
+ const unreservedAndSubDelims = "AZaz09-._~!$&'()*+,;="
107
+ const userinfo = unreservedAndSubDelims + ':'
108
+ const queryOrFragment = unreservedAndSubDelims + ':@/?'
109
+
110
+ t.equal(
111
+ fastURI.serialize({ userinfo, host: 'example.test' }),
112
+ '//' + userinfo + '@example.test',
113
+ 'userinfo literals are preserved'
114
+ )
115
+ t.equal(
116
+ fastURI.serialize({ query: queryOrFragment }),
117
+ '?' + queryOrFragment,
118
+ 'query literals are preserved'
119
+ )
120
+ t.equal(
121
+ fastURI.serialize({ fragment: queryOrFragment }),
122
+ '#' + queryOrFragment,
123
+ 'fragment literals are preserved'
124
+ )
125
+ t.end()
126
+ })
127
+
128
+ test('component serializers preserve escapes and encode Unicode as UTF-8', (t) => {
129
+ t.equal(
130
+ fastURI.serialize({ userinfo: 'café/%2f%GG', host: 'example.test' }),
131
+ '//caf%C3%A9%2F%2F%25GG@example.test',
132
+ 'userinfo preserves valid escapes, uppercases hex, and escapes malformed percent data'
133
+ )
134
+ t.equal(
135
+ fastURI.serialize({ query: '日本/%2f#%GG' }),
136
+ '?%E6%97%A5%E6%9C%AC/%2F%23%25GG',
137
+ 'query uses UTF-8 and does not double encode valid escapes'
138
+ )
139
+ t.equal(
140
+ fastURI.serialize({ fragment: '😀/%3f#%GG' }),
141
+ '#%F0%9F%98%80/%3F%23%25GG',
142
+ 'fragment uses UTF-8 and does not double encode valid escapes'
143
+ )
144
+ t.equal(
145
+ fastURI.serialize({ query: 'Kſ' }),
146
+ '?%E2%84%AA%C5%BF',
147
+ 'Unicode characters that case-fold to ASCII are still UTF-8 encoded'
148
+ )
149
+ t.end()
150
+ })
151
+
152
+ test('parsed URI serialization remains stable', (t) => {
153
+ const input = 'x://user:pass@example.test/a%2Fb?x=a/b?c&y=%23#frag/a?b%23'
154
+ const serialized = fastURI.serialize(fastURI.parse(input))
155
+
156
+ t.equal(serialized, input, 'an already normalized parsed URI round-trips unchanged')
157
+ t.equal(
158
+ fastURI.serialize(fastURI.parse(serialized)),
159
+ serialized,
160
+ 'repeated parse and serialize cycles are stable'
161
+ )
162
+ t.end()
163
+ })
@@ -21,6 +21,36 @@ test('URI Equals', (t) => {
21
21
  t.end()
22
22
  })
23
23
 
24
+ test('URI Equals rejects malformed object input even when identical', (t) => {
25
+ const malformed = { scheme: 'http', host: 'example.com', port: 99999, path: '/x' }
26
+ t.equal(fn(malformed, malformed), false)
27
+ t.end()
28
+ })
29
+
30
+ test('URI Equals preserves case-sensitive components', (t) => {
31
+ const suite = [
32
+ { pair: ['http://example.com/Admin', 'http://example.com/admin'], result: false },
33
+ { pair: ['http://example.com/?token=SECRET', 'http://example.com/?token=secret'], result: false },
34
+ { pair: ['http://example.com/#Section', 'http://example.com/#section'], result: false },
35
+ { pair: ['http://User@example.com/', 'http://user@example.com/'], result: false },
36
+ { pair: ['urn:foo:CaseSensitive', 'urn:foo:casesensitive'], result: false },
37
+ { pair: ['ws://example.com/Chat', 'ws://example.com/chat'], result: false },
38
+ { pair: ['ws://example.com/?token=SECRET', 'ws://example.com/?token=secret'], result: false },
39
+ { pair: [{ scheme: 'http', host: 'example.com', path: '/Admin' }, { scheme: 'http', host: 'example.com', path: '/admin' }], result: false }
40
+ ]
41
+ runTest(t, suite)
42
+ t.end()
43
+ })
44
+
45
+ test('URI Equals folds normalized scheme and host case', (t) => {
46
+ const suite = [
47
+ { pair: ['HTTP://EXAMPLE.COM/resource', 'http://example.com/resource'], result: true },
48
+ { pair: [{ scheme: 'HTTP', host: 'EXAMPLE.COM', path: '/resource' }, { scheme: 'http', host: 'example.com', path: '/resource' }], result: true }
49
+ ]
50
+ runTest(t, suite)
51
+ t.end()
52
+ })
53
+
24
54
  // test('IRI Equals', (t) => {
25
55
  // // example from RFC 3987
26
56
  // t.equal(URI.equal('example://a/b/c/%7Bfoo%7D/ros\xE9', 'eXAMPLE://a/./b/../b/%63/%7bfoo%7d/ros%C3%A9', IRI_OPTION), true)
@@ -66,9 +96,7 @@ test('URN Equals', (t) => {
66
96
 
67
97
  runTest(t, suite)
68
98
 
69
- t.throws(() => {
70
- fn('urn:', 'urn:FOO:a123,456')
71
- }, 'URN without nid cannot be serialized')
99
+ t.equal(fn('urn:', 'urn:FOO:a123,456'), false, 'malformed URN fails equality safely')
72
100
 
73
101
  t.end()
74
102
  })
@@ -107,6 +135,18 @@ test('WSS Equal', (t) => {
107
135
  t.end()
108
136
  })
109
137
 
138
+ test('Mailto Equal normalizes domains without folding message data', (t) => {
139
+ const suite = [
140
+ { pair: ['mailto:user@EXAMPLE.ORG', 'mailto:user@example.org'], result: true },
141
+ { pair: ['mailto:User@example.org', 'mailto:user@example.org'], result: false },
142
+ { pair: ['mailto:user@example.org?subject=Hello', 'mailto:user@example.org?subject=hello'], result: false },
143
+ { pair: ['mailto:user@example.org?body=Hello', 'mailto:user@example.org?body=hello'], result: false },
144
+ { pair: ['mailto:user@example.org?x=Hello', 'mailto:user@example.org?x=hello'], result: false }
145
+ ]
146
+ runTest(t, suite)
147
+ t.end()
148
+ })
149
+
110
150
  test('URI Equals tolerates malformed fragments', (t) => {
111
151
  t.equal(
112
152
  fastURI.equal('http://example.com/#%E0%A4A', 'http://example.com/#%E0%A4A'),
@@ -115,3 +155,54 @@ test('URI Equals tolerates malformed fragments', (t) => {
115
155
  )
116
156
  t.end()
117
157
  })
158
+
159
+ test('URI Equals — raw UTF-8 char equals its percent-encoded UTF-8 form', (t) => {
160
+ const suite = [
161
+ // BMP characters
162
+ { pair: ['http://example.com/日本', 'http://example.com/%E6%97%A5%E6%9C%AC'], result: true },
163
+ { pair: ['http://example.com/café', 'http://example.com/caf%C3%A9'], result: true },
164
+ { pair: ['http://example.com/€', 'http://example.com/%E2%82%AC'], result: true },
165
+ { pair: ['http://example.com/中文', 'http://example.com/%E4%B8%AD%E6%96%87'], result: true },
166
+ // supplementary plane (emoji, surrogate pairs)
167
+ { pair: ['http://example.com/🚀', 'http://example.com/%F0%9F%9A%80'], result: true },
168
+ { pair: ['http://example.com/😀', 'http://example.com/%F0%9F%98%80'], result: true },
169
+ // mixed encoded + raw in same path
170
+ { pair: ['http://example.com/users/日本/profile', 'http://example.com/users/%E6%97%A5%E6%9C%AC/profile'], result: true },
171
+ { pair: ['http://example.com/a/€/b', 'http://example.com/a/%E2%82%AC/b'], result: true }
172
+ ]
173
+ runTest(t, suite)
174
+ t.end()
175
+ })
176
+
177
+ test('URI Equals — percent-encoded UTF-8 is case-insensitive in hex digits', (t) => {
178
+ const suite = [
179
+ { pair: ['http://example.com/caf%C3%A9', 'http://example.com/caf%c3%a9'], result: true },
180
+ { pair: ['http://example.com/%E6%97%A5', 'http://example.com/%e6%97%a5'], result: true },
181
+ { pair: ['http://example.com/%F0%9F%9A%80', 'http://example.com/%f0%9f%9a%80'], result: true }
182
+ ]
183
+ runTest(t, suite)
184
+ t.end()
185
+ })
186
+
187
+ test('URI Equals — different Unicode code points are not equal', (t) => {
188
+ const suite = [
189
+ { pair: ['http://example.com/日本', 'http://example.com/中文'], result: false },
190
+ { pair: ['http://example.com/café', 'http://example.com/cafe'], result: false },
191
+ { pair: ['http://example.com/🚀', 'http://example.com/🎉'], result: false }
192
+ ]
193
+ runTest(t, suite)
194
+ t.end()
195
+ })
196
+
197
+ test('URI Equals — Latin-1 byte is not equal to UTF-8 multi-byte encoding', (t) => {
198
+ // Parser-differential guard: %C3%A9 (UTF-8 for "é") must NOT equal %E9
199
+ // (raw Latin-1 byte). The old escape() emitted single Latin-1 bytes for
200
+ // non-ASCII, which would have made these incorrectly compare equal in a
201
+ // round-tripped URI.
202
+ const suite = [
203
+ { pair: ['http://example.com/caf%C3%A9', 'http://example.com/caf%E9'], result: false },
204
+ { pair: ['http://example.com/%E6%97%A5', 'http://example.com/%E6'], result: false }
205
+ ]
206
+ runTest(t, suite)
207
+ t.end()
208
+ })
@@ -53,6 +53,7 @@
53
53
  "host": "[2001:dbz::1]",
54
54
  "port": 80,
55
55
  "path": "",
56
+ "error": "URI host is malformed.",
56
57
  "reference": "relative"
57
58
  }
58
59
  ],
@@ -242,6 +243,7 @@
242
243
  {
243
244
  "host": "[2606:2800:220:1:248:1893:25c8:1946:43209]",
244
245
  "path": "",
246
+ "error": "URI host is malformed.",
245
247
  "reference": "relative"
246
248
  }
247
249
  ],
@@ -497,5 +499,26 @@
497
499
  "subject": "Test",
498
500
  "body": "NATTO"
499
501
  }
502
+ ],
503
+ [
504
+ "mailto:addr1@an.example,addr2@an.example",
505
+ {
506
+ "scheme": "mailto",
507
+ "reference": "absolute",
508
+ "to": [
509
+ "addr1@an.example",
510
+ "addr2@an.example"
511
+ ]
512
+ }
513
+ ],
514
+ [
515
+ "mailto:user@example.org?",
516
+ {
517
+ "scheme": "mailto",
518
+ "reference": "absolute",
519
+ "to": [
520
+ "user@example.org"
521
+ ]
522
+ }
500
523
  ]
501
- ]
524
+ ]
@@ -116,5 +116,27 @@
116
116
  "path": "with:colon"
117
117
  },
118
118
  "scheme:with:colon"
119
+ ],
120
+ [
121
+ {
122
+ "scheme": "mailto",
123
+ "to": [
124
+ "chris@example.com"
125
+ ]
126
+ },
127
+ "mailto:chris@example.com"
128
+ ],
129
+ [
130
+ {
131
+ "scheme": "mailto",
132
+ "to": [
133
+ "joe@example.com"
134
+ ],
135
+ "headers": {
136
+ "cc": "bob@example.com"
137
+ },
138
+ "body": "hello"
139
+ },
140
+ "mailto:joe@example.com?cc=bob@example.com&body=hello"
119
141
  ]
120
- ]
142
+ ]
@@ -0,0 +1,34 @@
1
+ 'use strict'
2
+
3
+ const test = require('tape')
4
+ const fastURI = require('..')
5
+
6
+ test('IPv6 hosts normalize to the RFC 5952 canonical form', (t) => {
7
+ const cases = [
8
+ ['http://[0:0:0:0:0:0:0:1]/', 'http://[::1]/'],
9
+ ['http://[0:0:0:0:0:0:0:0]/', 'http://[::]/'],
10
+ ['http://[2001:0db8:0000:0000:0000:0000:0000:0001]/', 'http://[2001:db8::1]/'],
11
+ ['http://[2001:0:0:0:0:0:0:1]/', 'http://[2001::1]/'],
12
+ ['http://[fe80:0:0:0:0:0:0:1]/', 'http://[fe80::1]/'],
13
+ ['http://[1:0:0:0:2:0:0:3]/', 'http://[1::2:0:0:3]/']
14
+ ]
15
+
16
+ for (const [uri, normalized] of cases) {
17
+ t.equal(fastURI.normalize(uri), normalized, `${uri} normalizes to ${normalized}`)
18
+ }
19
+ t.end()
20
+ })
21
+
22
+ test('IPv6 equal() matches the same address across compressed and expanded forms', (t) => {
23
+ const pairs = [
24
+ ['http://[::1]/', 'http://[0:0:0:0:0:0:0:1]/'],
25
+ ['http://[::]/', 'http://[0:0:0:0:0:0:0:0]/'],
26
+ ['http://[2001:db8::1]/', 'http://[2001:0db8:0000:0000:0000:0000:0000:0001]/'],
27
+ ['http://[1::2:0:0:3]/', 'http://[1:0:0:0:2:0:0:3]/']
28
+ ]
29
+
30
+ for (const [a, b] of pairs) {
31
+ t.equal(fastURI.equal(a, b), true, `${a} equals ${b}`)
32
+ }
33
+ t.end()
34
+ })
@@ -0,0 +1,154 @@
1
+ 'use strict'
2
+
3
+ const test = require('tape')
4
+ const fastURI = require('..')
5
+
6
+ const HOST_ERROR = 'URI host is malformed.'
7
+
8
+ const malformedLiterals = [
9
+ '::not-valid',
10
+ 'fc00::not-hex',
11
+ 'fe80::not-hex',
12
+ '1:2:3',
13
+ '1:2:3:4:5:6:7',
14
+ '1:2:3:4:5:6:7:8:9',
15
+ '1::2::3',
16
+ '1:::2',
17
+ ':::1',
18
+ '12345::',
19
+ '1:2:3:4:5:6:7::8',
20
+ '::ffff:192.0.2.999',
21
+ '::ffff:192.0.2',
22
+ '::ffff:192.168.001.1',
23
+ '::192.0.2.1:1',
24
+ '1:2:3:4:5:192.0.2.1::',
25
+ 'v.foo',
26
+ 'v1.',
27
+ 'v1.foo%25bar',
28
+ 'v1.K',
29
+ 'fe80::1%25',
30
+ 'fe80::1%25eth 0',
31
+ 'fe80::1%25eth%ZZ',
32
+ 'fe80::1%25K',
33
+ 'not-an-ip'
34
+ ]
35
+
36
+ test('malformed bracketed IP literals fail without being rewritten', (t) => {
37
+ for (const literal of malformedLiterals) {
38
+ const uri = `http://[${literal}]/private`
39
+ const parsed = fastURI.parse(uri)
40
+
41
+ t.equal(parsed.error, HOST_ERROR, `parse rejects ${literal}`)
42
+ t.equal(parsed.host, `[${literal.toLowerCase()}]`, `parse does not truncate ${literal}`)
43
+ t.equal(fastURI.normalize(uri), uri, `normalize preserves ${literal}`)
44
+ t.equal(fastURI.equal(uri, uri), false, `equal rejects ${literal}`)
45
+ }
46
+ t.end()
47
+ })
48
+
49
+ test('resolve throws for malformed bracketed IP literals', (t) => {
50
+ for (const literal of malformedLiterals) {
51
+ const uri = `http://[${literal}]/private`
52
+
53
+ t.throws(
54
+ () => fastURI.resolve(uri, 'child'),
55
+ /URI host is malformed\./,
56
+ `rejects malformed base ${literal}`
57
+ )
58
+ t.throws(
59
+ () => fastURI.resolve('http://example.com/', uri),
60
+ /URI host is malformed\./,
61
+ `rejects malformed relative input ${literal}`
62
+ )
63
+ }
64
+ t.end()
65
+ })
66
+
67
+ test('valid IPv6, IPvFuture, embedded IPv4, and zone forms normalize safely', (t) => {
68
+ const cases = [
69
+ ['http://[::]/', 'http://[::]/', '::'],
70
+ ['http://[::1]/', 'http://[::1]/', '::1'],
71
+ ['http://[1::]/', 'http://[1::]/', '1::'],
72
+ ['http://[2001:0DB8::0001]/', 'http://[2001:db8::1]/', '2001:db8::1'],
73
+ ['http://[0:0:0:0:0:0:0:0]/', 'http://[::]/', '::'],
74
+ ['http://[::ffff:192.0.2.1]/', 'http://[::ffff:192.0.2.1]/', '::ffff:192.0.2.1'],
75
+ ['http://[1:2:3:4:5:6:192.0.2.1]/', 'http://[1:2:3:4:5:6:192.0.2.1]/', '1:2:3:4:5:6:192.0.2.1'],
76
+ ['http://[fe80::A%25EN1]/', 'http://[fe80::a%25EN1]/', 'fe80::a%EN1'],
77
+ ['http://[fe80::a%en1]/', 'http://[fe80::a%25en1]/', 'fe80::a%en1'],
78
+ ['http://[fe80::a%25eth%2D0]/', 'http://[fe80::a%25eth%2D0]/', 'fe80::a%eth%2D0'],
79
+ ['http://[v1.example]/', 'http://[v1.example]/', '[v1.example]'],
80
+ ['http://[vF.A:b]/', 'http://[vf.a:b]/', '[vf.a:b]']
81
+ ]
82
+
83
+ for (const [uri, normalized, host] of cases) {
84
+ const parsed = fastURI.parse(uri)
85
+ t.equal(parsed.error, undefined, `${uri} parses without error`)
86
+ t.equal(parsed.host, host, `${uri} has the expected host`)
87
+ t.equal(fastURI.normalize(uri), normalized, `${uri} normalizes safely`)
88
+ }
89
+ t.end()
90
+ })
91
+
92
+ test('IPv6 zone identifiers are validated correctly', (t) => {
93
+ const valid = [
94
+ 'http://[fe80::1%25eth0]/',
95
+ 'http://[fe80::a%en1]/',
96
+ 'http://[fe80::a%25eth%2D0]/'
97
+ ]
98
+
99
+ for (const uri of valid) {
100
+ const parsed = fastURI.parse(uri)
101
+
102
+ t.equal(parsed.error, undefined, `${uri} parses without error`)
103
+ }
104
+
105
+ const malformed = [
106
+ 'http://[fe80::1%25]/',
107
+ 'http://[fe80::1%25eth 0]/',
108
+ 'http://[fe80::1%25eth%ZZ]/',
109
+ 'http://[fe80::1%25K]/'
110
+ ]
111
+
112
+ for (const uri of malformed) {
113
+ const parsed = fastURI.parse(uri)
114
+
115
+ t.equal(parsed.error, HOST_ERROR, `${uri} is rejected`)
116
+ t.equal(fastURI.normalize(uri), uri, `${uri} is not rewritten`)
117
+ t.equal(fastURI.equal(uri, uri), false, `${uri} is not comparable`)
118
+ }
119
+ t.end()
120
+ })
121
+
122
+ test('hosts with unbalanced or misplaced IP-literal brackets are rejected', (t) => {
123
+ const malformed = [
124
+ 'http://[fe80',
125
+ 'http://[',
126
+ 'http://[not-an-ip',
127
+ 'http://[\u65e5\u672c',
128
+ 'http://user@[@127.0.0.1:8123/admin',
129
+ 'http://user@]127.0.0.1:8123/admin',
130
+ 'http://user@prefix[@127.0.0.1:8123/admin',
131
+ 'http://user@prefix]@127.0.0.1:8123/admin'
132
+ ]
133
+ const modes = [
134
+ ['default', undefined],
135
+ ['Unicode', { unicodeSupport: true }]
136
+ ]
137
+
138
+ for (const [mode, options] of modes) {
139
+ for (const uri of malformed) {
140
+ const parsed = fastURI.parse(uri, options)
141
+ const message = `${mode} mode rejects ${uri}`
142
+
143
+ t.equal(parsed.error, HOST_ERROR, `parse ${message}`)
144
+ t.equal(fastURI.normalize(uri, options), uri, `normalize preserves ${uri} in ${mode} mode`)
145
+ t.equal(fastURI.equal(uri, uri, options), false, `equal ${message}`)
146
+ t.throws(
147
+ () => fastURI.resolve('http://example.com/', uri, options),
148
+ /URI host is malformed\./,
149
+ `resolve ${message}`
150
+ )
151
+ }
152
+ }
153
+ t.end()
154
+ })
@@ -0,0 +1,77 @@
1
+ 'use strict'
2
+
3
+ const test = require('tape')
4
+ const fastURI = require('..')
5
+
6
+ const MALFORMED_PERCENT_ERROR = 'URI contains malformed percent-encoding.'
7
+
8
+ const malformedComponents = [
9
+ ['path', 'x://example.test/a%'],
10
+ ['path with one trailing hex digit', 'x://example.test/a%2'],
11
+ ['path with non-hex digits', 'x://example.test/a%GG'],
12
+ ['host', 'x://exa%mple.test/path'],
13
+ ['userinfo', 'x://us%er@example.test/path'],
14
+ ['query', 'x://example.test/path?q=%G0'],
15
+ ['fragment', 'x://example.test/path#frag%1']
16
+ ]
17
+
18
+ test('parse reports malformed percent syntax in generic URI components', (t) => {
19
+ for (const [component, uri] of malformedComponents) {
20
+ t.equal(fastURI.parse(uri).error, MALFORMED_PERCENT_ERROR, component)
21
+ }
22
+ t.end()
23
+ })
24
+
25
+ test('normalize preserves malformed percent input unchanged', (t) => {
26
+ for (const [component, uri] of malformedComponents) {
27
+ t.equal(fastURI.normalize(uri), uri, component)
28
+ }
29
+ t.end()
30
+ })
31
+
32
+ test('equal does not equate malformed input with repaired valid input', (t) => {
33
+ const cases = [
34
+ ['x://example.test/a%', 'x://example.test/a%25'],
35
+ ['x://exa%mple.test/path', 'x://exa%25mple.test/path'],
36
+ ['x://us%er@example.test/path', 'x://us%25er@example.test/path'],
37
+ ['x://example.test/path?q=%', 'x://example.test/path?q=%25'],
38
+ ['x://example.test/path#%', 'x://example.test/path#%25']
39
+ ]
40
+
41
+ for (const [malformed, repaired] of cases) {
42
+ t.equal(fastURI.equal(malformed, repaired, {}), false, malformed)
43
+ }
44
+ t.end()
45
+ })
46
+
47
+ test('resolve rejects malformed percent syntax in either input', (t) => {
48
+ t.throws(
49
+ () => fastURI.resolve('x://example.test/base%', 'child'),
50
+ /URI contains malformed percent-encoding\./,
51
+ 'malformed base'
52
+ )
53
+ t.throws(
54
+ () => fastURI.resolve('x://example.test/base', 'child%'),
55
+ /URI contains malformed percent-encoding\./,
56
+ 'malformed relative reference'
57
+ )
58
+ t.end()
59
+ })
60
+
61
+ test('valid percent octets remain valid without UTF-8 validation', (t) => {
62
+ const uri = '/%ff?q=%80#%fe'
63
+ const parsed = fastURI.parse(uri)
64
+
65
+ t.equal(parsed.error, undefined, 'non-UTF-8 octets are valid percent syntax')
66
+ t.equal(parsed.path, '/%FF', 'path octet is preserved and hex is uppercased')
67
+ t.equal(parsed.query, 'q=%80', 'query octet is preserved')
68
+ t.equal(parsed.fragment, '%FE', 'fragment octet is preserved and hex is uppercased')
69
+ t.equal(fastURI.normalize(uri), '/%FF?q=%80#%FE', 'normalization preserves the octets')
70
+
71
+ const allGenericComponents = fastURI.parse('x://u%2f@exa%2fmple.test/%2f?q=%2f#%2f')
72
+ t.equal(allGenericComponents.error, undefined, 'valid escapes are accepted in every generic component')
73
+
74
+ const ipv6Zone = fastURI.parse('//[2001:db8::7%en0]')
75
+ t.equal(ipv6Zone.error, undefined, 'historically accepted raw IPv6 zone separator is unchanged')
76
+ t.end()
77
+ })