@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.
- package/node_modules/fast-uri/.github/dependabot.yml +42 -2
- package/node_modules/fast-uri/.github/workflows/ci.yml +54 -6
- package/node_modules/fast-uri/.github/workflows/lock-threads.yml +1 -1
- package/node_modules/fast-uri/.github/workflows/ossf-scorecard.yml +45 -0
- package/node_modules/fast-uri/README.md +10 -9
- package/node_modules/fast-uri/SECURITY.md +173 -0
- package/node_modules/fast-uri/benchmark/benchmark.mjs +71 -0
- package/node_modules/fast-uri/benchmark/encoder.mjs +54 -0
- package/node_modules/fast-uri/index.js +277 -45
- package/node_modules/fast-uri/lib/schemes.js +384 -7
- package/node_modules/fast-uri/lib/utils.js +458 -95
- package/node_modules/fast-uri/package.json +4 -4
- package/node_modules/fast-uri/test/component-safe-serialization.test.js +163 -0
- package/node_modules/fast-uri/test/equal.test.js +94 -3
- package/node_modules/fast-uri/test/fixtures/uri-js-parse.json +24 -1
- package/node_modules/fast-uri/test/fixtures/uri-js-serialize.json +23 -1
- package/node_modules/fast-uri/test/ipv6-canonical.test.js +34 -0
- package/node_modules/fast-uri/test/ipv6-validation.test.js +154 -0
- package/node_modules/fast-uri/test/malformed-percent.test.js +77 -0
- package/node_modules/fast-uri/test/malformed-urn.test.js +61 -0
- package/node_modules/fast-uri/test/parse.test.js +150 -3
- package/node_modules/fast-uri/test/query-fragment-normalization.test.js +33 -0
- package/node_modules/fast-uri/test/reserved-path-normalization.test.js +109 -0
- package/node_modules/fast-uri/test/scheme-normalization.test.js +16 -0
- package/node_modules/fast-uri/test/scheme-validation.test.js +122 -0
- package/node_modules/fast-uri/test/security-normalization.test.js +101 -0
- package/node_modules/fast-uri/test/security-utf8-encoding.test.js +0 -0
- package/node_modules/fast-uri/test/security.test.js +301 -3
- package/node_modules/fast-uri/test/serialize.test.js +144 -0
- package/node_modules/fast-uri/test/uri-js-compatibility.test.js +0 -3
- package/node_modules/fast-uri/test/uri-js.test.js +4 -4
- package/node_modules/fast-uri/test/urn-full-input.test.js +29 -0
- package/node_modules/fast-uri/test/util.test.js +9 -0
- package/node_modules/fast-uri/test/websocket-query-preservation.test.js +24 -0
- package/node_modules/fast-uri/tsconfig.json +8 -7
- package/node_modules/fast-uri/types/index.d.ts +4 -9
- package/node_modules/fast-uri/types/index.tst.ts +31 -0
- package/npm-shrinkwrap.json +5 -5
- package/package.json +4 -4
- package/node_modules/fast-uri/.github/workflows/package-manager-ci.yml +0 -24
- package/node_modules/fast-uri/types/index.test-d.ts +0 -17
|
@@ -6,6 +6,9 @@ const isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\d
|
|
|
6
6
|
/** @type {(value: string) => boolean} */
|
|
7
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)
|
|
8
8
|
|
|
9
|
+
/** @type {(value: string) => boolean} */
|
|
10
|
+
const isPort = RegExp.prototype.test.bind(/^\d*$/u)
|
|
11
|
+
|
|
9
12
|
/** @type {(value: string) => boolean} */
|
|
10
13
|
const isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu)
|
|
11
14
|
|
|
@@ -13,7 +16,13 @@ const isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu)
|
|
|
13
16
|
const isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu)
|
|
14
17
|
|
|
15
18
|
/** @type {(value: string) => boolean} */
|
|
16
|
-
const isPathCharacter = RegExp.prototype.test.bind(/^[
|
|
19
|
+
const isPathCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/]$/u)
|
|
20
|
+
|
|
21
|
+
/** @type {(value: string) => boolean} */
|
|
22
|
+
const isQueryFragmentCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/?]$/u)
|
|
23
|
+
|
|
24
|
+
/** @type {(value: string) => boolean} */
|
|
25
|
+
const isUserinfoCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:]$/u)
|
|
17
26
|
|
|
18
27
|
/**
|
|
19
28
|
* @param {Array<string>} input
|
|
@@ -46,12 +55,14 @@ function stringArrayToHexStripped (input) {
|
|
|
46
55
|
return acc
|
|
47
56
|
}
|
|
48
57
|
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
/** @type {(value: string) => boolean} */
|
|
59
|
+
const isHextet = RegExp.prototype.test.bind(/^[\dA-Fa-f]{1,4}$/)
|
|
60
|
+
|
|
61
|
+
/** @type {(value: string) => boolean} */
|
|
62
|
+
const isIPvFuture = RegExp.prototype.test.bind(/^[vV][\dA-Fa-f]+\.[A-Za-z\d\-._~!$&'()*+,;=:]+$/)
|
|
63
|
+
|
|
64
|
+
/** @type {(value: string) => boolean} */
|
|
65
|
+
const isZoneCharacter = RegExp.prototype.test.bind(/^[A-Za-z\d\-._~]$/)
|
|
55
66
|
|
|
56
67
|
/**
|
|
57
68
|
* @param {string} value
|
|
@@ -59,89 +70,129 @@ function stringArrayToHexStripped (input) {
|
|
|
59
70
|
*/
|
|
60
71
|
const nonSimpleDomain = RegExp.prototype.test.bind(/[^!"$&'()*+,\-.;=_`a-z{}~]/u)
|
|
61
72
|
|
|
73
|
+
/** @type {(value: string) => boolean} */
|
|
74
|
+
const nonSimpleDomainChars = RegExp.prototype.test.bind(/[^\d!"$&'()*+,\-.;=_`a-z{}~]/u)
|
|
75
|
+
|
|
76
|
+
/** @type {(value: string) => boolean} */
|
|
77
|
+
const numericLastLabel = RegExp.prototype.test.bind(/(?:^|\.)\d+$/u)
|
|
78
|
+
|
|
62
79
|
/**
|
|
63
|
-
*
|
|
80
|
+
* True when `new URL('http://' + domain).hostname` may differ from `domain`,
|
|
81
|
+
* i.e. when the caller must fall back to the WHATWG parser.
|
|
82
|
+
*
|
|
83
|
+
* Same purpose as `nonSimpleDomain`, but digits are allowed: for this character
|
|
84
|
+
* set `hostname` is the identity, *except* that an all-numeric final label is
|
|
85
|
+
* read as IPv4 shorthand ("1.2.3" -> "1.2.0.3", "127.1" -> "127.0.0.1",
|
|
86
|
+
* "a.b.1" throws), which is why `nonSimpleDomain` excludes digits outright.
|
|
87
|
+
* Rejecting that one shape lets the common `mail2.example.org` case stay on the
|
|
88
|
+
* fast path.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} domain - must already be lowercased
|
|
64
91
|
* @returns {boolean}
|
|
65
92
|
*/
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
return true
|
|
93
|
+
function nonSimpleMailtoDomain (domain) {
|
|
94
|
+
return nonSimpleDomainChars(domain) || numericLastLabel(domain)
|
|
69
95
|
}
|
|
70
96
|
|
|
71
97
|
/**
|
|
72
|
-
* @param {
|
|
73
|
-
* @param {Array<string>} address
|
|
74
|
-
* @param {GetIPV6Result} output
|
|
98
|
+
* @param {string} zone
|
|
75
99
|
* @returns {boolean}
|
|
76
100
|
*/
|
|
77
|
-
function
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
101
|
+
function isZoneIdentifier (zone) {
|
|
102
|
+
if (zone.length === 0) return false
|
|
103
|
+
|
|
104
|
+
for (let i = 0; i < zone.length; i++) {
|
|
105
|
+
if (isZoneCharacter(zone[i])) continue
|
|
106
|
+
if (zone[i] === '%' && i + 2 < zone.length && isHexPair(zone.slice(i + 1, i + 3))) {
|
|
107
|
+
i += 2
|
|
108
|
+
continue
|
|
85
109
|
}
|
|
86
|
-
|
|
110
|
+
return false
|
|
87
111
|
}
|
|
112
|
+
|
|
88
113
|
return true
|
|
89
114
|
}
|
|
90
115
|
|
|
91
116
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
117
|
+
* Compresses the longest run of zero hextets to "::" per RFC 5952. A run of a
|
|
118
|
+
* single zero hextet is left uncompressed. On ties the leftmost run wins.
|
|
119
|
+
*
|
|
120
|
+
* @param {string[]} hextets
|
|
121
|
+
* @returns {string}
|
|
94
122
|
*/
|
|
95
|
-
function
|
|
96
|
-
let
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
for (let i = 0; i < input.length; i++) {
|
|
108
|
-
const cursor = input[i]
|
|
109
|
-
if (cursor === '[' || cursor === ']') { continue }
|
|
110
|
-
if (cursor === ':') {
|
|
111
|
-
if (endipv6Encountered === true) {
|
|
112
|
-
endIpv6 = true
|
|
123
|
+
function compressIPv6ZeroRun (hextets) {
|
|
124
|
+
let bestStart = -1
|
|
125
|
+
let bestLength = 0
|
|
126
|
+
let runStart = -1
|
|
127
|
+
let runLength = 0
|
|
128
|
+
for (let i = 0; i < hextets.length; i++) {
|
|
129
|
+
if (hextets[i] === '0') {
|
|
130
|
+
if (runStart === -1) runStart = i
|
|
131
|
+
runLength++
|
|
132
|
+
if (runLength > bestLength) {
|
|
133
|
+
bestLength = runLength
|
|
134
|
+
bestStart = runStart
|
|
113
135
|
}
|
|
114
|
-
if (!consume(buffer, address, output)) { break }
|
|
115
|
-
if (++tokenCount > 7) {
|
|
116
|
-
// not valid
|
|
117
|
-
output.error = true
|
|
118
|
-
break
|
|
119
|
-
}
|
|
120
|
-
if (i > 0 && input[i - 1] === ':') {
|
|
121
|
-
endipv6Encountered = true
|
|
122
|
-
}
|
|
123
|
-
address.push(':')
|
|
124
|
-
continue
|
|
125
|
-
} else if (cursor === '%') {
|
|
126
|
-
if (!consume(buffer, address, output)) { break }
|
|
127
|
-
// switch to zone detection
|
|
128
|
-
consume = consumeIsZone
|
|
129
136
|
} else {
|
|
130
|
-
|
|
131
|
-
|
|
137
|
+
runStart = -1
|
|
138
|
+
runLength = 0
|
|
132
139
|
}
|
|
133
140
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
|
|
142
|
+
if (bestLength < 2) return hextets.join(':')
|
|
143
|
+
|
|
144
|
+
const head = hextets.slice(0, bestStart).join(':')
|
|
145
|
+
const tail = hextets.slice(bestStart + bestLength).join(':')
|
|
146
|
+
return head + '::' + tail
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Validates an IPv6 address against the alternatives in RFC 3986 section
|
|
151
|
+
* 3.2.2 and returns the same address with leading hextet zeroes removed.
|
|
152
|
+
* An embedded IPv4 address counts as two hextets and is only valid at the end.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} input
|
|
155
|
+
* @returns {string|undefined}
|
|
156
|
+
*/
|
|
157
|
+
function normalizeIPv6Address (input) {
|
|
158
|
+
const compression = input.indexOf('::')
|
|
159
|
+
if (compression !== -1 && input.indexOf('::', compression + 1) !== -1) return undefined
|
|
160
|
+
|
|
161
|
+
const left = compression === -1 ? input.split(':') : input.slice(0, compression).split(':')
|
|
162
|
+
const right = compression === -1 ? [] : input.slice(compression + 2).split(':')
|
|
163
|
+
if (compression !== -1) {
|
|
164
|
+
if (left.length === 1 && left[0] === '') left.length = 0
|
|
165
|
+
if (right.length === 1 && right[0] === '') right.length = 0
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const parts = left.concat(right)
|
|
169
|
+
let hextetCount = 0
|
|
170
|
+
for (let i = 0; i < parts.length; i++) {
|
|
171
|
+
const part = parts[i]
|
|
172
|
+
if (part === '') return undefined
|
|
173
|
+
|
|
174
|
+
if (part.indexOf('.') !== -1) {
|
|
175
|
+
if (i !== parts.length - 1 || (compression !== -1 && right.length === 0) || !isIPv4(part)) return undefined
|
|
176
|
+
hextetCount += 2
|
|
177
|
+
continue
|
|
141
178
|
}
|
|
179
|
+
|
|
180
|
+
if (!isHextet(part)) return undefined
|
|
181
|
+
parts[i] = parseInt(part, 16).toString(16)
|
|
182
|
+
hextetCount++
|
|
142
183
|
}
|
|
143
|
-
|
|
144
|
-
|
|
184
|
+
|
|
185
|
+
if (compression === -1) {
|
|
186
|
+
if (hextetCount !== 8) return undefined
|
|
187
|
+
return compressIPv6ZeroRun(parts)
|
|
188
|
+
}
|
|
189
|
+
if (hextetCount >= 8) return undefined
|
|
190
|
+
|
|
191
|
+
// expand "::" then re-compress the longest run for a canonical result
|
|
192
|
+
const expanded = parts.slice(0, left.length)
|
|
193
|
+
for (let i = hextetCount; i < 8; i++) expanded.push('0')
|
|
194
|
+
for (let i = left.length; i < parts.length; i++) expanded.push(parts[i])
|
|
195
|
+
return compressIPv6ZeroRun(expanded)
|
|
145
196
|
}
|
|
146
197
|
|
|
147
198
|
/**
|
|
@@ -149,26 +200,49 @@ function getIPV6 (input) {
|
|
|
149
200
|
* @property {string} host - The normalized host.
|
|
150
201
|
* @property {string} [escapedHost] - The escaped host.
|
|
151
202
|
* @property {boolean} isIPV6 - Indicates if the host is an IPv6 address.
|
|
203
|
+
* @property {boolean} [isIPVFuture] - Indicates if the host is an IPvFuture literal.
|
|
204
|
+
* @property {boolean} [error] - Indicates if a bracketed IP literal is malformed.
|
|
152
205
|
*/
|
|
153
206
|
|
|
154
207
|
/**
|
|
208
|
+
* Validates and normalizes a bracketed IP literal. Raw zone separators remain
|
|
209
|
+
* accepted for backwards compatibility, while encoded separators and zone
|
|
210
|
+
* contents follow RFC 6874.
|
|
211
|
+
*
|
|
155
212
|
* @param {string} host
|
|
156
213
|
* @returns {NormalizeIPv6Result}
|
|
157
214
|
*/
|
|
158
215
|
function normalizeIPv6 (host) {
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
216
|
+
const bracketed = host[0] === '[' && host[host.length - 1] === ']'
|
|
217
|
+
const hasBracket = host[0] === '[' || host[host.length - 1] === ']'
|
|
218
|
+
if (hasBracket && !bracketed) return { host, isIPV6: false, error: true }
|
|
219
|
+
|
|
220
|
+
let input = bracketed ? host.slice(1, -1) : host
|
|
221
|
+
if (bracketed && isIPvFuture(input)) {
|
|
222
|
+
input = input.toLowerCase()
|
|
223
|
+
return { host: `[${input}]`, escapedHost: input, isIPV6: false, isIPVFuture: true }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (findToken(input, ':') < 2) {
|
|
227
|
+
return { host, isIPV6: false, error: bracketed }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let zoneIdentifier = ''
|
|
231
|
+
const zoneSeparator = input.indexOf('%')
|
|
232
|
+
if (zoneSeparator !== -1) {
|
|
233
|
+
const separatorLength = input.slice(zoneSeparator, zoneSeparator + 3).toLowerCase() === '%25' ? 3 : 1
|
|
234
|
+
zoneIdentifier = input.slice(zoneSeparator + separatorLength)
|
|
235
|
+
if (!isZoneIdentifier(zoneIdentifier)) return { host, isIPV6: false, error: true }
|
|
236
|
+
input = input.slice(0, zoneSeparator)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const address = normalizeIPv6Address(input)
|
|
240
|
+
if (address === undefined) return { host, isIPV6: false, error: true }
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
host: address + (zoneIdentifier ? '%' + zoneIdentifier : ''),
|
|
244
|
+
escapedHost: address + (zoneIdentifier ? '%25' + zoneIdentifier : ''),
|
|
245
|
+
isIPV6: true
|
|
172
246
|
}
|
|
173
247
|
}
|
|
174
248
|
|
|
@@ -218,7 +292,7 @@ function removeDotSegments (path) {
|
|
|
218
292
|
continue
|
|
219
293
|
}
|
|
220
294
|
} else if (input[0] === '/') {
|
|
221
|
-
if (input[1] === '.'
|
|
295
|
+
if (input[1] === '.') {
|
|
222
296
|
output.push('/')
|
|
223
297
|
break
|
|
224
298
|
}
|
|
@@ -294,7 +368,7 @@ function reescapeHostDelimiters (host, isIP) {
|
|
|
294
368
|
|
|
295
369
|
/**
|
|
296
370
|
* Normalizes percent escapes and optionally decodes only unreserved ASCII bytes.
|
|
297
|
-
* Reserved delimiters such as `%2F`
|
|
371
|
+
* Reserved delimiters such as `%2F` stay escaped; `%2E` is unreserved.
|
|
298
372
|
*
|
|
299
373
|
* @param {string} input
|
|
300
374
|
* @param {boolean} [decodeUnreserved=false]
|
|
@@ -331,6 +405,55 @@ function normalizePercentEncoding (input, decodeUnreserved = false) {
|
|
|
331
405
|
return output
|
|
332
406
|
}
|
|
333
407
|
|
|
408
|
+
/** Pre-built `%XX` strings for every byte. Avoids per-call hex math/concat. */
|
|
409
|
+
const BYTE_HEX = new Array(256)
|
|
410
|
+
{
|
|
411
|
+
const HEX_DIGITS = '0123456789ABCDEF'
|
|
412
|
+
for (let i = 0; i < 256; i++) {
|
|
413
|
+
BYTE_HEX[i] = '%' + HEX_DIGITS[i >> 4] + HEX_DIGITS[i & 0xF]
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Mirrors the pass-through set of the deprecated `escape()` global:
|
|
419
|
+
* `A-Z a-z 0-9 * + - . / @ _`. Keeps drop-in semantics for ASCII input.
|
|
420
|
+
*
|
|
421
|
+
* @param {number} cp
|
|
422
|
+
* @returns {boolean}
|
|
423
|
+
*/
|
|
424
|
+
function isEscapeSafe (cp) {
|
|
425
|
+
return (
|
|
426
|
+
(cp >= 0x30 && cp <= 0x39) ||
|
|
427
|
+
(cp >= 0x41 && cp <= 0x5A) ||
|
|
428
|
+
(cp >= 0x61 && cp <= 0x7A) ||
|
|
429
|
+
cp === 0x2A || cp === 0x2B || cp === 0x2D || cp === 0x2E ||
|
|
430
|
+
cp === 0x2F || cp === 0x40 || cp === 0x5F
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* RFC 3986 percent-encode a non-ASCII Unicode code point as UTF-8 bytes.
|
|
436
|
+
* Caller handles the ASCII branch inline for speed.
|
|
437
|
+
*
|
|
438
|
+
* @param {number} cp - Unicode code point (>= 0x80, <= 0x10FFFF)
|
|
439
|
+
* @returns {string}
|
|
440
|
+
*/
|
|
441
|
+
function percentEncodeNonAscii (cp) {
|
|
442
|
+
if (cp < 0x800) {
|
|
443
|
+
return BYTE_HEX[0xC0 | (cp >> 6)] +
|
|
444
|
+
BYTE_HEX[0x80 | (cp & 0x3F)]
|
|
445
|
+
}
|
|
446
|
+
if (cp < 0x10000) {
|
|
447
|
+
return BYTE_HEX[0xE0 | (cp >> 12)] +
|
|
448
|
+
BYTE_HEX[0x80 | ((cp >> 6) & 0x3F)] +
|
|
449
|
+
BYTE_HEX[0x80 | (cp & 0x3F)]
|
|
450
|
+
}
|
|
451
|
+
return BYTE_HEX[0xF0 | (cp >> 18)] +
|
|
452
|
+
BYTE_HEX[0x80 | ((cp >> 12) & 0x3F)] +
|
|
453
|
+
BYTE_HEX[0x80 | ((cp >> 6) & 0x3F)] +
|
|
454
|
+
BYTE_HEX[0x80 | (cp & 0x3F)]
|
|
455
|
+
}
|
|
456
|
+
|
|
334
457
|
/**
|
|
335
458
|
* Normalizes path data without turning reserved escapes into live path syntax.
|
|
336
459
|
* Valid escapes are uppercased, raw unsafe characters are escaped, and only
|
|
@@ -343,7 +466,8 @@ function normalizePathEncoding (input) {
|
|
|
343
466
|
let output = ''
|
|
344
467
|
|
|
345
468
|
for (let i = 0; i < input.length; i++) {
|
|
346
|
-
|
|
469
|
+
const ch = input[i]
|
|
470
|
+
if (ch === '%' && i + 2 < input.length) {
|
|
347
471
|
const hex = input.slice(i + 1, i + 3)
|
|
348
472
|
if (isHexPair(hex)) {
|
|
349
473
|
const normalizedHex = hex.toUpperCase()
|
|
@@ -360,16 +484,221 @@ function normalizePathEncoding (input) {
|
|
|
360
484
|
}
|
|
361
485
|
}
|
|
362
486
|
|
|
363
|
-
if (isPathCharacter(
|
|
364
|
-
output +=
|
|
487
|
+
if (isPathCharacter(ch)) {
|
|
488
|
+
output += ch
|
|
489
|
+
} else {
|
|
490
|
+
const code = input.charCodeAt(i)
|
|
491
|
+
if (code < 0x80) {
|
|
492
|
+
output += isEscapeSafe(code) ? ch : BYTE_HEX[code]
|
|
493
|
+
} else if (code < 0xD800 || code > 0xDFFF) {
|
|
494
|
+
output += percentEncodeNonAscii(code)
|
|
495
|
+
} else if (code <= 0xDBFF && i + 1 < input.length) {
|
|
496
|
+
const low = input.charCodeAt(i + 1)
|
|
497
|
+
if (low >= 0xDC00 && low <= 0xDFFF) {
|
|
498
|
+
output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
|
|
499
|
+
i++
|
|
500
|
+
} else {
|
|
501
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
502
|
+
}
|
|
503
|
+
} else {
|
|
504
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return output
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Serializes a path without rewriting reserved data. Raw RFC 3986 path
|
|
514
|
+
* characters remain literal, valid escapes are preserved and uppercased, and
|
|
515
|
+
* everything else is UTF-8 percent-encoded. In a path-noscheme, a colon in the
|
|
516
|
+
* first segment must be escaped so the result cannot be parsed as a scheme.
|
|
517
|
+
*
|
|
518
|
+
* @param {string} input
|
|
519
|
+
* @param {boolean} [pathNoScheme=false]
|
|
520
|
+
* @returns {string}
|
|
521
|
+
*/
|
|
522
|
+
function serializePathEncoding (input, pathNoScheme = false) {
|
|
523
|
+
let output = ''
|
|
524
|
+
let firstSegment = pathNoScheme && input[0] !== '/'
|
|
525
|
+
|
|
526
|
+
for (let i = 0; i < input.length; i++) {
|
|
527
|
+
const ch = input[i]
|
|
528
|
+
if (ch === '%' && i + 2 < input.length) {
|
|
529
|
+
const hex = input.slice(i + 1, i + 3)
|
|
530
|
+
if (isHexPair(hex)) {
|
|
531
|
+
output += '%' + hex.toUpperCase()
|
|
532
|
+
i += 2
|
|
533
|
+
continue
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (ch === '/') {
|
|
538
|
+
firstSegment = false
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (isPathCharacter(ch) && (ch !== ':' || !firstSegment)) {
|
|
542
|
+
output += ch
|
|
543
|
+
} else {
|
|
544
|
+
const code = input.charCodeAt(i)
|
|
545
|
+
if (code < 0x80) {
|
|
546
|
+
output += BYTE_HEX[code]
|
|
547
|
+
} else if (code < 0xD800 || code > 0xDFFF) {
|
|
548
|
+
output += percentEncodeNonAscii(code)
|
|
549
|
+
} else if (code <= 0xDBFF && i + 1 < input.length) {
|
|
550
|
+
const low = input.charCodeAt(i + 1)
|
|
551
|
+
if (low >= 0xDC00 && low <= 0xDFFF) {
|
|
552
|
+
output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
|
|
553
|
+
i++
|
|
554
|
+
} else {
|
|
555
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
556
|
+
}
|
|
557
|
+
} else {
|
|
558
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return output
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Normalizes the percent-encoding of a query or fragment component.
|
|
568
|
+
*
|
|
569
|
+
* Like `normalizePathEncoding`, but uses the query/fragment character set
|
|
570
|
+
* (which additionally allows `?`) and decodes `.` since it has no dot-segment
|
|
571
|
+
* meaning outside of a path.
|
|
572
|
+
*
|
|
573
|
+
* @param {string} input
|
|
574
|
+
* @returns {string}
|
|
575
|
+
*/
|
|
576
|
+
function normalizeQueryFragmentEncoding (input) {
|
|
577
|
+
let output = ''
|
|
578
|
+
|
|
579
|
+
for (let i = 0; i < input.length; i++) {
|
|
580
|
+
const ch = input[i]
|
|
581
|
+
if (ch === '%' && i + 2 < input.length) {
|
|
582
|
+
const hex = input.slice(i + 1, i + 3)
|
|
583
|
+
if (isHexPair(hex)) {
|
|
584
|
+
const normalizedHex = hex.toUpperCase()
|
|
585
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16))
|
|
586
|
+
|
|
587
|
+
if (isUnreserved(decoded)) {
|
|
588
|
+
output += decoded
|
|
589
|
+
} else {
|
|
590
|
+
output += '%' + normalizedHex
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
i += 2
|
|
594
|
+
continue
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (isQueryFragmentCharacter(ch)) {
|
|
599
|
+
output += ch
|
|
600
|
+
} else {
|
|
601
|
+
const code = input.charCodeAt(i)
|
|
602
|
+
if (code < 0x80) {
|
|
603
|
+
output += isEscapeSafe(code) ? ch : BYTE_HEX[code]
|
|
604
|
+
} else if (code < 0xD800 || code > 0xDFFF) {
|
|
605
|
+
output += percentEncodeNonAscii(code)
|
|
606
|
+
} else if (code <= 0xDBFF && i + 1 < input.length) {
|
|
607
|
+
const low = input.charCodeAt(i + 1)
|
|
608
|
+
if (low >= 0xDC00 && low <= 0xDFFF) {
|
|
609
|
+
output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
|
|
610
|
+
i++
|
|
611
|
+
} else {
|
|
612
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
613
|
+
}
|
|
614
|
+
} else {
|
|
615
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return output
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Percent-encodes a URI component using its RFC 3986 literal character set.
|
|
625
|
+
* Existing valid escapes are preserved and normalized to uppercase hex.
|
|
626
|
+
*
|
|
627
|
+
* @param {string} input
|
|
628
|
+
* @param {(value: string) => boolean} isAllowed
|
|
629
|
+
* @returns {string}
|
|
630
|
+
*/
|
|
631
|
+
function encodeComponent (input, isAllowed) {
|
|
632
|
+
let output = ''
|
|
633
|
+
|
|
634
|
+
for (let i = 0; i < input.length; i++) {
|
|
635
|
+
const ch = input[i]
|
|
636
|
+
if (ch === '%' && i + 2 < input.length) {
|
|
637
|
+
const hex = input.slice(i + 1, i + 3)
|
|
638
|
+
if (isHexPair(hex)) {
|
|
639
|
+
output += '%' + hex.toUpperCase()
|
|
640
|
+
i += 2
|
|
641
|
+
continue
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (isAllowed(ch)) {
|
|
646
|
+
output += ch
|
|
365
647
|
} else {
|
|
366
|
-
|
|
648
|
+
const code = input.charCodeAt(i)
|
|
649
|
+
if (code < 0x80) {
|
|
650
|
+
output += BYTE_HEX[code]
|
|
651
|
+
} else if (code < 0xD800 || code > 0xDFFF) {
|
|
652
|
+
output += percentEncodeNonAscii(code)
|
|
653
|
+
} else if (code <= 0xDBFF && i + 1 < input.length) {
|
|
654
|
+
const low = input.charCodeAt(i + 1)
|
|
655
|
+
if (low >= 0xDC00 && low <= 0xDFFF) {
|
|
656
|
+
output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
|
|
657
|
+
i++
|
|
658
|
+
} else {
|
|
659
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
660
|
+
}
|
|
661
|
+
} else {
|
|
662
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
663
|
+
}
|
|
367
664
|
}
|
|
368
665
|
}
|
|
369
666
|
|
|
370
667
|
return output
|
|
371
668
|
}
|
|
372
669
|
|
|
670
|
+
/**
|
|
671
|
+
* Encodes userinfo while preserving its RFC 3986 §3.2.1 literal characters.
|
|
672
|
+
* In particular, authority delimiters such as `@`, `/`, `?`, and `#` are data.
|
|
673
|
+
*
|
|
674
|
+
* @param {string} input
|
|
675
|
+
* @returns {string}
|
|
676
|
+
*/
|
|
677
|
+
function encodeUserinfo (input) {
|
|
678
|
+
return encodeComponent(input, isUserinfoCharacter)
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Encodes query data using the RFC 3986 §3.4 grammar. A literal `#` must be
|
|
683
|
+
* escaped because it would otherwise begin the fragment component.
|
|
684
|
+
*
|
|
685
|
+
* @param {string} input
|
|
686
|
+
* @returns {string}
|
|
687
|
+
*/
|
|
688
|
+
function encodeQuery (input) {
|
|
689
|
+
return encodeComponent(input, isQueryFragmentCharacter)
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Encodes fragment data using the RFC 3986 §3.5 grammar.
|
|
694
|
+
*
|
|
695
|
+
* @param {string} input
|
|
696
|
+
* @returns {string}
|
|
697
|
+
*/
|
|
698
|
+
function encodeFragment (input) {
|
|
699
|
+
return encodeComponent(input, isQueryFragmentCharacter)
|
|
700
|
+
}
|
|
701
|
+
|
|
373
702
|
/**
|
|
374
703
|
* Escapes a component while preserving existing valid percent escapes.
|
|
375
704
|
*
|
|
@@ -380,7 +709,8 @@ function escapePreservingEscapes (input) {
|
|
|
380
709
|
let output = ''
|
|
381
710
|
|
|
382
711
|
for (let i = 0; i < input.length; i++) {
|
|
383
|
-
|
|
712
|
+
const ch = input[i]
|
|
713
|
+
if (ch === '%' && i + 2 < input.length) {
|
|
384
714
|
const hex = input.slice(i + 1, i + 3)
|
|
385
715
|
if (isHexPair(hex)) {
|
|
386
716
|
output += '%' + hex.toUpperCase()
|
|
@@ -389,7 +719,22 @@ function escapePreservingEscapes (input) {
|
|
|
389
719
|
}
|
|
390
720
|
}
|
|
391
721
|
|
|
392
|
-
|
|
722
|
+
const code = input.charCodeAt(i)
|
|
723
|
+
if (code < 0x80) {
|
|
724
|
+
output += isEscapeSafe(code) ? ch : BYTE_HEX[code]
|
|
725
|
+
} else if (code < 0xD800 || code > 0xDFFF) {
|
|
726
|
+
output += percentEncodeNonAscii(code)
|
|
727
|
+
} else if (code <= 0xDBFF && i + 1 < input.length) {
|
|
728
|
+
const low = input.charCodeAt(i + 1)
|
|
729
|
+
if (low >= 0xDC00 && low <= 0xDFFF) {
|
|
730
|
+
output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
|
|
731
|
+
i++
|
|
732
|
+
} else {
|
|
733
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
734
|
+
}
|
|
735
|
+
} else {
|
|
736
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
737
|
+
}
|
|
393
738
|
}
|
|
394
739
|
|
|
395
740
|
return output
|
|
@@ -403,15 +748,21 @@ function recomposeAuthority (component) {
|
|
|
403
748
|
const uriTokens = []
|
|
404
749
|
|
|
405
750
|
if (component.userinfo !== undefined) {
|
|
406
|
-
uriTokens.push(component.userinfo)
|
|
751
|
+
uriTokens.push(encodeUserinfo(component.userinfo))
|
|
407
752
|
uriTokens.push('@')
|
|
408
753
|
}
|
|
409
754
|
|
|
410
755
|
if (component.host !== undefined) {
|
|
411
|
-
let host =
|
|
756
|
+
let host = component.host
|
|
412
757
|
if (!isIPv4(host)) {
|
|
413
|
-
|
|
414
|
-
if (ipV6res.isIPV6
|
|
758
|
+
let ipV6res = normalizeIPv6(host)
|
|
759
|
+
if (ipV6res.isIPV6 !== true && ipV6res.isIPVFuture !== true) {
|
|
760
|
+
// Decode only unreserved bytes, once. In particular, keep %25 encoded
|
|
761
|
+
// so it cannot introduce a second escape during recomposition.
|
|
762
|
+
host = normalizePercentEncoding(host, true)
|
|
763
|
+
ipV6res = normalizeIPv6(host)
|
|
764
|
+
}
|
|
765
|
+
if (ipV6res.isIPV6 === true || ipV6res.isIPVFuture === true) {
|
|
415
766
|
host = `[${ipV6res.escapedHost}]`
|
|
416
767
|
} else {
|
|
417
768
|
host = reescapeHostDelimiters(host, false)
|
|
@@ -421,19 +772,31 @@ function recomposeAuthority (component) {
|
|
|
421
772
|
}
|
|
422
773
|
|
|
423
774
|
if (typeof component.port === 'number' || typeof component.port === 'string') {
|
|
775
|
+
const port = String(component.port)
|
|
776
|
+
if (!isPort(port)) {
|
|
777
|
+
throw new TypeError('URI port is malformed.')
|
|
778
|
+
}
|
|
424
779
|
uriTokens.push(':')
|
|
425
|
-
uriTokens.push(
|
|
780
|
+
uriTokens.push(port)
|
|
426
781
|
}
|
|
427
782
|
|
|
428
783
|
return uriTokens.length ? uriTokens.join('') : undefined
|
|
429
784
|
};
|
|
430
785
|
|
|
431
786
|
module.exports = {
|
|
787
|
+
BYTE_HEX,
|
|
788
|
+
percentEncodeNonAscii,
|
|
432
789
|
nonSimpleDomain,
|
|
790
|
+
nonSimpleMailtoDomain,
|
|
433
791
|
recomposeAuthority,
|
|
434
792
|
reescapeHostDelimiters,
|
|
435
793
|
normalizePercentEncoding,
|
|
436
794
|
normalizePathEncoding,
|
|
795
|
+
serializePathEncoding,
|
|
796
|
+
normalizeQueryFragmentEncoding,
|
|
797
|
+
encodeUserinfo,
|
|
798
|
+
encodeQuery,
|
|
799
|
+
encodeFragment,
|
|
437
800
|
escapePreservingEscapes,
|
|
438
801
|
removeDotSegments,
|
|
439
802
|
isIPv4,
|