bare-url 2.5.1 → 2.5.3
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/CMakeLists.txt +1 -1
- package/README.md +1 -140
- package/binding.c +268 -190
- package/global.d.ts +12 -0
- package/index.d.ts +55 -0
- package/index.js +339 -74
- package/lib/url-search-params.d.ts +43 -0
- package/lib/url-search-params.js +111 -29
- package/package.json +1 -1
- package/prebuilds/android-arm/bare-url.bare +0 -0
- package/prebuilds/android-arm64/bare-url.bare +0 -0
- package/prebuilds/android-ia32/bare-url.bare +0 -0
- package/prebuilds/android-x64/bare-url.bare +0 -0
- package/prebuilds/darwin-arm64/bare-url.bare +0 -0
- package/prebuilds/darwin-x64/bare-url.bare +0 -0
- package/prebuilds/ios-arm64/bare-url.bare +0 -0
- package/prebuilds/ios-arm64-simulator/bare-url.bare +0 -0
- package/prebuilds/ios-x64-simulator/bare-url.bare +0 -0
- package/prebuilds/linux-arm64/bare-url.bare +0 -0
- package/prebuilds/linux-x64/bare-url.bare +0 -0
- package/prebuilds/win32-arm64/bare-url.bare +0 -0
- package/prebuilds/win32-x64/bare-url.bare +0 -0
package/index.js
CHANGED
|
@@ -7,6 +7,67 @@ const kind = Symbol.for('bare.url.kind')
|
|
|
7
7
|
|
|
8
8
|
const isWindows = Bare.platform === 'win32'
|
|
9
9
|
|
|
10
|
+
// Scratch buffer that the binding writes the parsed component offsets into. A
|
|
11
|
+
// single shared buffer is reused across every parse.
|
|
12
|
+
//
|
|
13
|
+
// The offsets are copied out into fields on the URL immediately after a
|
|
14
|
+
// successful parse, so nothing observes the buffer across calls.
|
|
15
|
+
const components = new Uint32Array(8)
|
|
16
|
+
|
|
17
|
+
// The value used for a component that is not present in the URL.
|
|
18
|
+
const unset = 0xffffffff
|
|
19
|
+
|
|
20
|
+
// The schemes the parser treats specially. A URL cannot be switched between a
|
|
21
|
+
// special and a non-special scheme, and a backslash only terminates a host for
|
|
22
|
+
// the former.
|
|
23
|
+
const special = new Set(['ftp', 'file', 'http', 'https', 'ws', 'wss'])
|
|
24
|
+
|
|
25
|
+
// https://url.spec.whatwg.org/#scheme-start-state
|
|
26
|
+
const scheme = /^[a-z][a-z0-9+\-.]*$/
|
|
27
|
+
|
|
28
|
+
// ASCII tab and newline are removed from input before it is parsed rather than
|
|
29
|
+
// percent-encoded like the other C0 controls.
|
|
30
|
+
const whitespaceAll = /[\t\n\r]/g
|
|
31
|
+
|
|
32
|
+
// The characters that terminate a host, and so bound the value the host and
|
|
33
|
+
// hostname setters accept.
|
|
34
|
+
const hostEnd = /[/\\?#]/
|
|
35
|
+
const hostEndOpaque = /[/?#]/
|
|
36
|
+
|
|
37
|
+
// The delimiters that would let a setter's value escape the component it is
|
|
38
|
+
// spliced into. Everything else is left to the reparse, which applies the full
|
|
39
|
+
// percent-encode set for the component. As elsewhere in this package, each set
|
|
40
|
+
// needs two patterns because test() advances a global pattern's lastIndex.
|
|
41
|
+
//
|
|
42
|
+
// Credentials are not run through the parser and so keep their tabs and
|
|
43
|
+
// newlines, percent-encoded, rather than having them stripped.
|
|
44
|
+
const userinfoDelimiter = /[\t\n\r/\\?#@:]/
|
|
45
|
+
const userinfoDelimiterAll = /[\t\n\r/\\?#@:]/g
|
|
46
|
+
|
|
47
|
+
const pathDelimiter = /[?#]/
|
|
48
|
+
const pathDelimiterAll = /[?#]/g
|
|
49
|
+
|
|
50
|
+
// A leading or trailing run of C0 control or space in a value that ends up at
|
|
51
|
+
// either end of the href. The parser strips those, but only when parsing a URL
|
|
52
|
+
// as a whole, so a setter has to encode its own.
|
|
53
|
+
const edges = /^[\u0000-\u0020]+|[\u0000-\u0020]+$/g
|
|
54
|
+
|
|
55
|
+
const escapes = {
|
|
56
|
+
'\t': '%09',
|
|
57
|
+
'\n': '%0A',
|
|
58
|
+
'\r': '%0D',
|
|
59
|
+
'/': '%2F',
|
|
60
|
+
'\\': '%5C',
|
|
61
|
+
':': '%3A',
|
|
62
|
+
'?': '%3F',
|
|
63
|
+
'@': '%40',
|
|
64
|
+
'#': '%23'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The characters that pathToFileURL() has to percent-encode itself. A backslash
|
|
68
|
+
// is a path separator on Windows and so is left alone there.
|
|
69
|
+
const reserved = isWindows ? /[%#?\n\r\t]/ : /[%#?\n\r\t\\]/
|
|
70
|
+
|
|
10
71
|
class URL {
|
|
11
72
|
static get [kind]() {
|
|
12
73
|
return 0 // Compatibility version
|
|
@@ -19,11 +80,17 @@ class URL {
|
|
|
19
80
|
|
|
20
81
|
if (base !== undefined) base = String(base)
|
|
21
82
|
|
|
22
|
-
this.
|
|
83
|
+
this._href = undefined
|
|
84
|
+
this._schemeEnd = 0
|
|
85
|
+
this._usernameEnd = 0
|
|
86
|
+
this._hostStart = 0
|
|
87
|
+
this._hostEnd = 0
|
|
88
|
+
this._pathStart = 0
|
|
89
|
+
this._queryStart = 0
|
|
90
|
+
this._fragmentStart = 0
|
|
91
|
+
this._params = null
|
|
23
92
|
|
|
24
93
|
this._parse(input, base, opts.throw !== false)
|
|
25
|
-
|
|
26
|
-
if (this._href) this._params = new URLSearchParams(this.search, this)
|
|
27
94
|
}
|
|
28
95
|
|
|
29
96
|
get [kind]() {
|
|
@@ -37,25 +104,47 @@ class URL {
|
|
|
37
104
|
}
|
|
38
105
|
|
|
39
106
|
set href(value) {
|
|
40
|
-
|
|
107
|
+
// Unlike every other setter, the href setter reports a parse failure rather
|
|
108
|
+
// than leaving the URL untouched.
|
|
109
|
+
this._parse(String(value), null, true)
|
|
41
110
|
|
|
42
|
-
this._params._parse(this.search)
|
|
111
|
+
if (this._params) this._params._parse(this.search)
|
|
43
112
|
}
|
|
44
113
|
|
|
45
114
|
// https://url.spec.whatwg.org/#dom-url-protocol
|
|
46
115
|
|
|
47
116
|
get protocol() {
|
|
48
|
-
return this._slice(0, this.
|
|
117
|
+
return this._slice(0, this._schemeEnd) + ':'
|
|
49
118
|
}
|
|
50
119
|
|
|
51
120
|
set protocol(value) {
|
|
52
|
-
|
|
121
|
+
value = strip(String(value))
|
|
122
|
+
|
|
123
|
+
const end = value.indexOf(':')
|
|
124
|
+
|
|
125
|
+
if (end !== -1) value = value.slice(0, end)
|
|
126
|
+
|
|
127
|
+
value = value.toLowerCase()
|
|
128
|
+
|
|
129
|
+
if (!scheme.test(value)) return
|
|
130
|
+
|
|
131
|
+
const current = this._slice(0, this._schemeEnd)
|
|
132
|
+
|
|
133
|
+
if (special.has(current) !== special.has(value)) return
|
|
134
|
+
|
|
135
|
+
if (value === 'file' && (this.username || this.password || this.port)) {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (current === 'file' && this._hostStart === this._hostEnd) return
|
|
140
|
+
|
|
141
|
+
this._update(this._replace(value, 0, this._schemeEnd))
|
|
53
142
|
}
|
|
54
143
|
|
|
55
144
|
// https://url.spec.whatwg.org/#dom-url-username
|
|
56
145
|
|
|
57
146
|
get username() {
|
|
58
|
-
return this._slice(this.
|
|
147
|
+
return this._slice(this._schemeEnd + 3 /* :// */, this._usernameEnd)
|
|
59
148
|
}
|
|
60
149
|
|
|
61
150
|
set username(value) {
|
|
@@ -63,15 +152,17 @@ class URL {
|
|
|
63
152
|
return
|
|
64
153
|
}
|
|
65
154
|
|
|
66
|
-
|
|
155
|
+
value = encodeUserinfo(String(value))
|
|
67
156
|
|
|
68
|
-
|
|
157
|
+
if (!hasCredentials(this)) value += '@'
|
|
158
|
+
|
|
159
|
+
this._update(this._replace(value, this._schemeEnd + 3 /* :// */, this._usernameEnd))
|
|
69
160
|
}
|
|
70
161
|
|
|
71
162
|
// https://url.spec.whatwg.org/#dom-url-password
|
|
72
163
|
|
|
73
164
|
get password() {
|
|
74
|
-
return this._href.slice(this.
|
|
165
|
+
return this._href.slice(this._usernameEnd + 1 /* : */, this._hostStart - 1 /* @ */)
|
|
75
166
|
}
|
|
76
167
|
|
|
77
168
|
set password(value) {
|
|
@@ -79,26 +170,22 @@ class URL {
|
|
|
79
170
|
return
|
|
80
171
|
}
|
|
81
172
|
|
|
82
|
-
|
|
83
|
-
let end = this._components[2] - 1 /* @ */
|
|
173
|
+
value = ':' + encodeUserinfo(String(value))
|
|
84
174
|
|
|
85
|
-
|
|
86
|
-
value = ':' + value
|
|
87
|
-
start--
|
|
88
|
-
}
|
|
175
|
+
let end = this._hostStart - 1 /* @ */
|
|
89
176
|
|
|
90
|
-
if (this
|
|
177
|
+
if (!hasCredentials(this)) {
|
|
91
178
|
value += '@'
|
|
92
|
-
end
|
|
179
|
+
end = this._usernameEnd
|
|
93
180
|
}
|
|
94
181
|
|
|
95
|
-
this._update(this._replace(value,
|
|
182
|
+
this._update(this._replace(value, this._usernameEnd, end))
|
|
96
183
|
}
|
|
97
184
|
|
|
98
185
|
// https://url.spec.whatwg.org/#dom-url-host
|
|
99
186
|
|
|
100
187
|
get host() {
|
|
101
|
-
return this._slice(this.
|
|
188
|
+
return this._slice(this._hostStart, this._pathStart)
|
|
102
189
|
}
|
|
103
190
|
|
|
104
191
|
set host(value) {
|
|
@@ -106,15 +193,44 @@ class URL {
|
|
|
106
193
|
return
|
|
107
194
|
}
|
|
108
195
|
|
|
109
|
-
this.
|
|
110
|
-
|
|
111
|
-
)
|
|
196
|
+
const protocol = this._slice(0, this._schemeEnd)
|
|
197
|
+
|
|
198
|
+
value = truncateHost(protocol, String(value))
|
|
199
|
+
|
|
200
|
+
// An `@` would make the reparse read the value as credentials rather than
|
|
201
|
+
// as a host, so it is rejected outright.
|
|
202
|
+
if (value.includes('@')) return
|
|
203
|
+
|
|
204
|
+
const separator = portSeparator(value)
|
|
205
|
+
|
|
206
|
+
let end = this._hostEnd
|
|
207
|
+
|
|
208
|
+
// A port in the value is parsed separately so that an invalid one leaves
|
|
209
|
+
// the existing port in place rather than rejecting the host along with it.
|
|
210
|
+
if (separator !== -1) {
|
|
211
|
+
// A file URL cannot have a port, so a value carrying one is rejected
|
|
212
|
+
// rather than split.
|
|
213
|
+
if (protocol === 'file') return
|
|
214
|
+
|
|
215
|
+
const port = parsePort(value.slice(separator + 1))
|
|
216
|
+
|
|
217
|
+
value = value.slice(0, separator)
|
|
218
|
+
|
|
219
|
+
if (port !== null) {
|
|
220
|
+
value += port
|
|
221
|
+
end = this._pathStart
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (value === '' && cannotHaveEmptyHost(protocol)) return
|
|
226
|
+
|
|
227
|
+
this._update(this._replace(value, this._hostStart, end))
|
|
112
228
|
}
|
|
113
229
|
|
|
114
230
|
// https://url.spec.whatwg.org/#dom-url-hostname
|
|
115
231
|
|
|
116
232
|
get hostname() {
|
|
117
|
-
return this._slice(this.
|
|
233
|
+
return this._slice(this._hostStart, this._hostEnd)
|
|
118
234
|
}
|
|
119
235
|
|
|
120
236
|
set hostname(value) {
|
|
@@ -122,13 +238,24 @@ class URL {
|
|
|
122
238
|
return
|
|
123
239
|
}
|
|
124
240
|
|
|
125
|
-
this.
|
|
241
|
+
const protocol = this._slice(0, this._schemeEnd)
|
|
242
|
+
|
|
243
|
+
value = truncateHost(protocol, String(value))
|
|
244
|
+
|
|
245
|
+
// A port cannot be set through this setter, and a value that carries one is
|
|
246
|
+
// rejected outright rather than truncated. An `@` would make the reparse
|
|
247
|
+
// read the value as credentials rather than as a host.
|
|
248
|
+
if (value.includes('@') || portSeparator(value) !== -1) return
|
|
249
|
+
|
|
250
|
+
if (value === '' && cannotHaveEmptyHost(protocol)) return
|
|
251
|
+
|
|
252
|
+
this._update(this._replace(value, this._hostStart, this._hostEnd))
|
|
126
253
|
}
|
|
127
254
|
|
|
128
255
|
// https://url.spec.whatwg.org/#dom-url-port
|
|
129
256
|
|
|
130
257
|
get port() {
|
|
131
|
-
return this._slice(this.
|
|
258
|
+
return this._slice(this._hostEnd + 1 /* : */, this._pathStart)
|
|
132
259
|
}
|
|
133
260
|
|
|
134
261
|
set port(value) {
|
|
@@ -136,20 +263,21 @@ class URL {
|
|
|
136
263
|
return
|
|
137
264
|
}
|
|
138
265
|
|
|
139
|
-
|
|
266
|
+
value = strip(String(value))
|
|
267
|
+
|
|
268
|
+
if (value !== '') {
|
|
269
|
+
value = parsePort(value)
|
|
140
270
|
|
|
141
|
-
|
|
142
|
-
value = ':' + value
|
|
143
|
-
start--
|
|
271
|
+
if (value === null) return
|
|
144
272
|
}
|
|
145
273
|
|
|
146
|
-
this._update(this._replace(value,
|
|
274
|
+
this._update(this._replace(value, this._hostEnd, this._pathStart))
|
|
147
275
|
}
|
|
148
276
|
|
|
149
277
|
// https://url.spec.whatwg.org/#dom-url-pathname
|
|
150
278
|
|
|
151
279
|
get pathname() {
|
|
152
|
-
return this._slice(this.
|
|
280
|
+
return this._slice(this._pathStart, this._queryStart - 1 /* ? */)
|
|
153
281
|
}
|
|
154
282
|
|
|
155
283
|
set pathname(value) {
|
|
@@ -157,45 +285,65 @@ class URL {
|
|
|
157
285
|
return
|
|
158
286
|
}
|
|
159
287
|
|
|
160
|
-
|
|
288
|
+
value = encodePath(encodeEdges(String(value)))
|
|
289
|
+
|
|
290
|
+
// An empty path is left alone, as only a special scheme is required to have
|
|
291
|
+
// one and the reparse inserts it there.
|
|
292
|
+
if (value !== '' && value[0] !== '/' && value[0] !== '\\') {
|
|
161
293
|
value = '/' + value
|
|
162
294
|
}
|
|
163
295
|
|
|
164
|
-
this._update(this._replace(value, this.
|
|
296
|
+
this._update(this._replace(value, this._pathStart, this._queryStart - 1 /* ? */))
|
|
165
297
|
}
|
|
166
298
|
|
|
167
299
|
// https://url.spec.whatwg.org/#dom-url-search
|
|
168
300
|
|
|
169
301
|
get search() {
|
|
170
|
-
return this._slice(this.
|
|
302
|
+
return this._slice(this._queryStart - 1 /* ? */, this._fragmentStart - 1 /* # */)
|
|
171
303
|
}
|
|
172
304
|
|
|
173
305
|
set search(value) {
|
|
174
|
-
|
|
306
|
+
value = String(value)
|
|
307
|
+
|
|
308
|
+
if (value !== '') {
|
|
309
|
+
if (value[0] === '?') value = value.slice(1)
|
|
310
|
+
|
|
311
|
+
value = '?' + encodeQuery(encodeEdges(value))
|
|
312
|
+
}
|
|
175
313
|
|
|
176
314
|
this._update(
|
|
177
|
-
this._replace(value, this.
|
|
315
|
+
this._replace(value, this._queryStart - 1 /* ? */, this._fragmentStart - 1 /* # */)
|
|
178
316
|
)
|
|
179
|
-
|
|
180
|
-
this._params._parse(this.search)
|
|
181
317
|
}
|
|
182
318
|
|
|
183
319
|
// https://url.spec.whatwg.org/#dom-url-searchparams
|
|
184
320
|
|
|
185
321
|
get searchParams() {
|
|
322
|
+
if (this._params === null) {
|
|
323
|
+
this._params = new URLSearchParams(this.search, this)
|
|
324
|
+
}
|
|
325
|
+
|
|
186
326
|
return this._params
|
|
187
327
|
}
|
|
188
328
|
|
|
189
329
|
// https://url.spec.whatwg.org/#dom-url-hash
|
|
190
330
|
|
|
191
331
|
get hash() {
|
|
192
|
-
return this._slice(this.
|
|
332
|
+
return this._slice(this._fragmentStart - 1 /* # */)
|
|
193
333
|
}
|
|
194
334
|
|
|
195
335
|
set hash(value) {
|
|
196
|
-
|
|
336
|
+
value = String(value)
|
|
337
|
+
|
|
338
|
+
// The fragment runs to the end of the URL, so nothing in it can escape into
|
|
339
|
+
// another component and no delimiter needs encoding here.
|
|
340
|
+
if (value !== '') {
|
|
341
|
+
if (value[0] === '#') value = value.slice(1)
|
|
342
|
+
|
|
343
|
+
value = '#' + encodeEdges(value)
|
|
344
|
+
}
|
|
197
345
|
|
|
198
|
-
this._update(this._replace(value, this.
|
|
346
|
+
this._update(this._replace(value, this._fragmentStart - 1 /* # */))
|
|
199
347
|
}
|
|
200
348
|
|
|
201
349
|
toString() {
|
|
@@ -233,18 +381,32 @@ class URL {
|
|
|
233
381
|
}
|
|
234
382
|
|
|
235
383
|
_parse(input, base, shouldThrow) {
|
|
384
|
+
let href
|
|
385
|
+
|
|
236
386
|
try {
|
|
237
|
-
|
|
238
|
-
String(input),
|
|
239
|
-
base ? String(base) : null,
|
|
240
|
-
this._components,
|
|
241
|
-
shouldThrow
|
|
242
|
-
)
|
|
387
|
+
href = binding.parse(input, base || null, components, shouldThrow)
|
|
243
388
|
} catch (err) {
|
|
244
|
-
if (err instanceof TypeError) throw err
|
|
389
|
+
if (err instanceof TypeError || err.code !== undefined) throw err
|
|
245
390
|
|
|
246
391
|
throw errors.INVALID_URL(`Invalid URL '${input}'`, input)
|
|
247
392
|
}
|
|
393
|
+
|
|
394
|
+
if (href === undefined) return
|
|
395
|
+
|
|
396
|
+
this._href = href
|
|
397
|
+
this._schemeEnd = components[0]
|
|
398
|
+
this._usernameEnd = components[1]
|
|
399
|
+
this._hostStart = components[2]
|
|
400
|
+
this._hostEnd = components[3]
|
|
401
|
+
this._pathStart = components[5]
|
|
402
|
+
|
|
403
|
+
const queryStart = components[6]
|
|
404
|
+
const fragmentStart = components[7]
|
|
405
|
+
|
|
406
|
+
const end = href.length + 1
|
|
407
|
+
|
|
408
|
+
this._queryStart = queryStart === unset ? end : queryStart
|
|
409
|
+
this._fragmentStart = fragmentStart === unset ? end : fragmentStart
|
|
248
410
|
}
|
|
249
411
|
|
|
250
412
|
_update(input) {
|
|
@@ -252,7 +414,11 @@ class URL {
|
|
|
252
414
|
this._parse(input, null, true)
|
|
253
415
|
} catch (err) {
|
|
254
416
|
if (err instanceof TypeError) throw err
|
|
417
|
+
|
|
418
|
+
return
|
|
255
419
|
}
|
|
420
|
+
|
|
421
|
+
if (this._params) this._params._parse(this.search)
|
|
256
422
|
}
|
|
257
423
|
}
|
|
258
424
|
|
|
@@ -268,6 +434,93 @@ function cannotHaveCredentialsOrPort(url) {
|
|
|
268
434
|
return url.hostname === '' || url.protocol === 'file:'
|
|
269
435
|
}
|
|
270
436
|
|
|
437
|
+
// Whether the URL carries a userinfo section, and so an `@` before its host.
|
|
438
|
+
function hasCredentials(url) {
|
|
439
|
+
return url._hostStart !== url._usernameEnd
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Almost no input contains any of these, so it is worth ruling all three out
|
|
443
|
+
// before rewriting anything.
|
|
444
|
+
function strip(value) {
|
|
445
|
+
if (value.indexOf('\t') === -1 && value.indexOf('\n') === -1 && value.indexOf('\r') === -1) {
|
|
446
|
+
return value
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return value.replace(whitespaceAll, '')
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// https://url.spec.whatwg.org/#host-state
|
|
453
|
+
//
|
|
454
|
+
// Host parsing stops at the first character that starts another component, so
|
|
455
|
+
// anything from there on is dropped rather than spliced into the host.
|
|
456
|
+
function truncateHost(protocol, value) {
|
|
457
|
+
const end = value.search(special.has(protocol) ? hostEnd : hostEndOpaque)
|
|
458
|
+
|
|
459
|
+
return end === -1 ? value : value.slice(0, end)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Only a special scheme other than file has to have a host.
|
|
463
|
+
function cannotHaveEmptyHost(protocol) {
|
|
464
|
+
return protocol !== 'file' && special.has(protocol)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// The index of the colon that separates a host from its port, disregarding the
|
|
468
|
+
// colons of an IPv6 address, or -1 if the host carries no port.
|
|
469
|
+
function portSeparator(host) {
|
|
470
|
+
return host.indexOf(':', host[0] === '[' ? host.indexOf(']') : 0)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// https://url.spec.whatwg.org/#port-state
|
|
474
|
+
//
|
|
475
|
+
// Parsing stops at the first character that is not a digit, so a value with no
|
|
476
|
+
// leading digits, or one that overflows, leaves the port as it was.
|
|
477
|
+
function parsePort(value) {
|
|
478
|
+
value = /^\d*/.exec(value)[0]
|
|
479
|
+
|
|
480
|
+
if (value === '' || Number(value) > 65535) return null
|
|
481
|
+
|
|
482
|
+
return ':' + value
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function encodeUserinfo(value) {
|
|
486
|
+
if (!userinfoDelimiter.test(value)) return value
|
|
487
|
+
|
|
488
|
+
return value.replace(userinfoDelimiterAll, (match) => escapes[match])
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function encodePath(value) {
|
|
492
|
+
if (!pathDelimiter.test(value)) return value
|
|
493
|
+
|
|
494
|
+
return value.replace(pathDelimiterAll, (match) => escapes[match])
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function encodeQuery(value) {
|
|
498
|
+
if (value.indexOf('#') === -1) return value
|
|
499
|
+
|
|
500
|
+
return value.replaceAll('#', '%23')
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Percent-encodes a leading or trailing run of C0 control or space, which the
|
|
504
|
+
// parser would otherwise strip from a value that lands at either end of the
|
|
505
|
+
// href. Every component that can end a URL encodes them anyway, so this only
|
|
506
|
+
// brings the encoding forward.
|
|
507
|
+
function encodeEdges(value) {
|
|
508
|
+
const len = value.length
|
|
509
|
+
|
|
510
|
+
if (len === 0) return value
|
|
511
|
+
if (value.charCodeAt(0) > 0x20 && value.charCodeAt(len - 1) > 0x20) return value
|
|
512
|
+
|
|
513
|
+
return value.replace(edges, (match) => {
|
|
514
|
+
let encoded = ''
|
|
515
|
+
|
|
516
|
+
for (let i = 0, n = match.length; i < n; i++) {
|
|
517
|
+
encoded += '%' + match.charCodeAt(i).toString(16).padStart(2, '0').toUpperCase()
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return encoded
|
|
521
|
+
})
|
|
522
|
+
}
|
|
523
|
+
|
|
271
524
|
exports.URL = URL
|
|
272
525
|
exports.URLSearchParams = URLSearchParams
|
|
273
526
|
|
|
@@ -301,27 +554,35 @@ exports.fileURLToPath = function fileURLToPath(url) {
|
|
|
301
554
|
throw errors.INVALID_URL_SCHEME('The URL must use the file: protocol')
|
|
302
555
|
}
|
|
303
556
|
|
|
304
|
-
if (isWindows) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
} else {
|
|
311
|
-
if (url.hostname) {
|
|
312
|
-
throw errors.INVALID_FILE_URL_HOST("The file: URL host must be 'localhost' or empty")
|
|
313
|
-
}
|
|
557
|
+
if (!isWindows && url.hostname) {
|
|
558
|
+
throw errors.INVALID_FILE_URL_HOST("The file: URL host must be 'localhost' or empty")
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const encoded = url.pathname
|
|
314
562
|
|
|
315
|
-
|
|
563
|
+
// Every check below looks for a percent encoded sequence, as does the decoding
|
|
564
|
+
// that follows, so a path without any can skip all of them.
|
|
565
|
+
const hasEncoded = encoded.includes('%')
|
|
566
|
+
|
|
567
|
+
if (hasEncoded) {
|
|
568
|
+
if (isWindows) {
|
|
569
|
+
if (/%2f|%5c/i.test(encoded)) {
|
|
570
|
+
throw errors.INVALID_FILE_URL_PATH(
|
|
571
|
+
'The file: URL path must not include encoded \\ or / characters'
|
|
572
|
+
)
|
|
573
|
+
}
|
|
574
|
+
} else if (/%2f/i.test(encoded)) {
|
|
316
575
|
throw errors.INVALID_FILE_URL_PATH('The file: URL path must not include encoded / characters')
|
|
317
576
|
}
|
|
318
|
-
}
|
|
319
577
|
|
|
320
|
-
|
|
321
|
-
|
|
578
|
+
if (/%00/i.test(encoded)) {
|
|
579
|
+
throw errors.INVALID_FILE_URL_PATH(
|
|
580
|
+
'The file: URL path must not include encoded NUL characters'
|
|
581
|
+
)
|
|
582
|
+
}
|
|
322
583
|
}
|
|
323
584
|
|
|
324
|
-
const pathname = path.normalize(decodeURIComponent(
|
|
585
|
+
const pathname = path.normalize(hasEncoded ? decodeURIComponent(encoded) : encoded)
|
|
325
586
|
|
|
326
587
|
if (isWindows) {
|
|
327
588
|
if (url.hostname) return '\\\\' + url.hostname + pathname
|
|
@@ -347,16 +608,20 @@ exports.pathToFileURL = function pathToFileURL(pathname) {
|
|
|
347
608
|
resolved += '\\'
|
|
348
609
|
}
|
|
349
610
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
611
|
+
// Paths hardly ever contain any of these, so one pass to rule them out is
|
|
612
|
+
// cheaper than the six or seven replacements it stands in for.
|
|
613
|
+
if (reserved.test(resolved)) {
|
|
614
|
+
resolved = resolved
|
|
615
|
+
.replaceAll('%', '%25') // Must be first
|
|
616
|
+
.replaceAll('#', '%23')
|
|
617
|
+
.replaceAll('?', '%3f')
|
|
618
|
+
.replaceAll('\n', '%0a')
|
|
619
|
+
.replaceAll('\r', '%0d')
|
|
620
|
+
.replaceAll('\t', '%09')
|
|
357
621
|
|
|
358
|
-
|
|
359
|
-
|
|
622
|
+
if (!isWindows) {
|
|
623
|
+
resolved = resolved.replaceAll('\\', '%5c')
|
|
624
|
+
}
|
|
360
625
|
}
|
|
361
626
|
|
|
362
627
|
return new URL('file:' + resolved)
|
|
@@ -1,22 +1,65 @@
|
|
|
1
1
|
interface URLSearchParams extends Iterable<[name: string, value: string]> {
|
|
2
|
+
/** The total number of search parameters. */
|
|
2
3
|
readonly size: number
|
|
3
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Append a new `name`/`value` pair.
|
|
7
|
+
* @param name - The parameter name.
|
|
8
|
+
* @param value - The parameter value.
|
|
9
|
+
*/
|
|
4
10
|
append(name: string, value: string): void
|
|
11
|
+
/**
|
|
12
|
+
* Remove all pairs with `name`. If `value` is provided, only pairs with both the matching `name`
|
|
13
|
+
* and `value` are removed.
|
|
14
|
+
* @param name - The parameter name to remove.
|
|
15
|
+
* @param value - If provided, only pairs also matching this value are removed.
|
|
16
|
+
*/
|
|
5
17
|
delete(name: string, value?: string): void
|
|
18
|
+
/**
|
|
19
|
+
* Return the first value for `name`, or `null` if not present.
|
|
20
|
+
* @param name - The parameter name to look up.
|
|
21
|
+
*/
|
|
6
22
|
get(name: string): string | undefined
|
|
23
|
+
/**
|
|
24
|
+
* Return all values for `name` as an array.
|
|
25
|
+
* @param name - The parameter name to look up.
|
|
26
|
+
*/
|
|
7
27
|
getAll(name: string): string[]
|
|
28
|
+
/**
|
|
29
|
+
* Return `true` if a pair with `name` exists. If `value` is provided, the pair must also match
|
|
30
|
+
* `value`.
|
|
31
|
+
* @param name - The parameter name to check.
|
|
32
|
+
* @param value - If provided, the pair must also match this value.
|
|
33
|
+
*/
|
|
8
34
|
has(name: string, value?: string): boolean
|
|
35
|
+
/**
|
|
36
|
+
* Set the value for `name`, replacing any existing pairs with that name.
|
|
37
|
+
* @param name - The parameter name.
|
|
38
|
+
* @param value - The value to set.
|
|
39
|
+
*/
|
|
9
40
|
set(name: string, value: string): void
|
|
10
41
|
|
|
42
|
+
/** Returns the serialized string form. */
|
|
11
43
|
toString(): string
|
|
44
|
+
/** Returns the serialized string form. Suitable for JSON serialization. */
|
|
12
45
|
toJSON(): string
|
|
13
46
|
}
|
|
14
47
|
|
|
15
48
|
declare class URLSearchParams {
|
|
49
|
+
/**
|
|
50
|
+
* Create a new `URLSearchParams` instance. `init` may be a query string, an iterable of `[name,
|
|
51
|
+
* value]` pairs, or an object of key-value pairs.
|
|
52
|
+
* @param init - A query string, an iterable of `[name, value]` pairs, or an object of key-value
|
|
53
|
+
* pairs to initialize the params from.
|
|
54
|
+
*/
|
|
16
55
|
constructor(init: string | Record<string, string> | Iterable<[string, string]>)
|
|
17
56
|
}
|
|
18
57
|
|
|
19
58
|
declare namespace URLSearchParams {
|
|
59
|
+
/**
|
|
60
|
+
* Return `true` if `value` is a `URLSearchParams` instance.
|
|
61
|
+
* @param value - The value to test.
|
|
62
|
+
*/
|
|
20
63
|
export function isURLSearchParams(value: unknown): value is URLSearchParams
|
|
21
64
|
}
|
|
22
65
|
|