bare-url 2.5.0 → 2.5.2

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/binding.c CHANGED
@@ -8,8 +8,91 @@
8
8
  #include <utf.h>
9
9
  #include <utf/string.h>
10
10
 
11
+ // Maximum length, in bytes, of a UTF-8 string that is read into a stack buffer.
12
+ // Longer strings fall back to a heap allocation. This covers the vast majority
13
+ // of URLs without touching the heap.
14
+ #define BARE_URL_STACK_STRING_MAX 1024
15
+
16
+ // The UTF-8 encoding of a JavaScript string, together with whatever backs it.
17
+ typedef struct {
18
+ const utf8_t *data;
19
+ size_t len;
20
+
21
+ js_string_view_t *view;
22
+ utf8_t *heap;
23
+ } bare_url_string_t;
24
+
25
+ // Exposes `value` as UTF-8. Strings that are stored as ASCII, which is nearly
26
+ // all of them, are borrowed directly from the engine without being copied.
27
+ // Anything else is transcoded into `stack`, or into a freshly allocated heap
28
+ // buffer when it does not fit. The result must be released with
29
+ // bare_url__free_string().
30
+ static inline void
31
+ bare_url__read_string(js_env_t *env, js_value_t *value, utf8_t *stack, size_t stack_len, bare_url_string_t *result) {
32
+ int err;
33
+
34
+ js_string_encoding_t encoding;
35
+ const void *data;
36
+ size_t len;
37
+
38
+ err = js_get_string_view(env, value, &encoding, &data, &len, &result->view);
39
+ assert(err == 0);
40
+
41
+ result->heap = NULL;
42
+
43
+ size_t utf8_len;
44
+
45
+ if (encoding == js_utf8) {
46
+ result->data = (const utf8_t *) data;
47
+ result->len = len;
48
+
49
+ return;
50
+ }
51
+
52
+ if (encoding == js_latin1) {
53
+ utf8_len = utf8_length_from_latin1((const latin1_t *) data, len);
54
+
55
+ // A Latin-1 string that is no longer as UTF-8 contains only ASCII, whose
56
+ // Latin-1 and UTF-8 encodings are identical.
57
+ if (utf8_len == len) {
58
+ result->data = (const utf8_t *) data;
59
+ result->len = len;
60
+
61
+ return;
62
+ }
63
+ } else {
64
+ assert(encoding == js_utf16le);
65
+
66
+ utf8_len = utf8_length_from_utf16le((const utf16_t *) data, len);
67
+ }
68
+
69
+ utf8_t *buffer = utf8_len <= stack_len ? stack : (result->heap = malloc(utf8_len));
70
+
71
+ if (encoding == js_latin1) {
72
+ latin1_convert_to_utf8((const latin1_t *) data, len, buffer);
73
+ } else {
74
+ utf16le_convert_to_utf8((const utf16_t *) data, len, buffer);
75
+ }
76
+
77
+ result->data = buffer;
78
+ result->len = utf8_len;
79
+ }
80
+
81
+ // Releases a string read with bare_url__read_string(), freeing its buffer only
82
+ // when it was heap allocated rather than borrowed or written to the caller's
83
+ // stack buffer.
84
+ static inline void
85
+ bare_url__free_string(js_env_t *env, bare_url_string_t *string) {
86
+ int err;
87
+
88
+ free(string->heap);
89
+
90
+ err = js_release_string_view(env, string->view);
91
+ assert(err == 0);
92
+ }
93
+
11
94
  static js_value_t *
12
- bare_url_parse (js_env_t *env, js_callback_info_t *info) {
95
+ bare_url_parse(js_env_t *env, js_callback_info_t *info) {
13
96
  int err;
14
97
 
15
98
  size_t argc = 4;
@@ -32,17 +115,14 @@ bare_url_parse (js_env_t *env, js_callback_info_t *info) {
32
115
  url_init(&base);
33
116
 
34
117
  if (has_base) {
35
- size_t len;
36
- err = js_get_value_string_utf8(env, argv[1], NULL, 0, &len);
37
- assert(err == 0);
118
+ utf8_t stack[BARE_URL_STACK_STRING_MAX];
38
119
 
39
- utf8_t *input = malloc(len);
40
- err = js_get_value_string_utf8(env, argv[1], input, len, NULL);
41
- assert(err == 0);
120
+ bare_url_string_t input;
121
+ bare_url__read_string(env, argv[1], stack, sizeof(stack), &input);
42
122
 
43
- err = url_parse(&base, input, len, NULL);
123
+ err = url_parse(&base, input.data, input.len, NULL);
44
124
 
45
- free(input);
125
+ bare_url__free_string(env, &input);
46
126
 
47
127
  if (err < 0) {
48
128
  url_destroy(&base);
@@ -53,22 +133,17 @@ bare_url_parse (js_env_t *env, js_callback_info_t *info) {
53
133
  }
54
134
  }
55
135
 
56
- size_t len;
57
- err = js_get_value_string_utf8(env, argv[0], NULL, 0, &len);
58
- assert(err == 0);
136
+ utf8_t stack[BARE_URL_STACK_STRING_MAX];
59
137
 
60
- utf8_t *input = malloc(len);
61
- err = js_get_value_string_utf8(env, argv[0], input, len, NULL);
62
- assert(err == 0);
63
-
64
- js_value_t *handle;
138
+ bare_url_string_t input;
139
+ bare_url__read_string(env, argv[0], stack, sizeof(stack), &input);
65
140
 
66
141
  url_t url;
67
142
  url_init(&url);
68
143
 
69
- err = url_parse(&url, input, len, has_base ? &base : NULL);
144
+ err = url_parse(&url, input.data, input.len, has_base ? &base : NULL);
70
145
 
71
- free(input);
146
+ bare_url__free_string(env, &input);
72
147
 
73
148
  if (err < 0) {
74
149
  url_destroy(&base);
@@ -80,7 +155,7 @@ bare_url_parse (js_env_t *env, js_callback_info_t *info) {
80
155
  }
81
156
 
82
157
  js_value_t *href;
83
- err = js_create_string_utf8(env, url.href.data, url.href.len, &href);
158
+ err = js_create_string_latin1(env, (const latin1_t *) url.href.data, url.href.len, &href);
84
159
  assert(err == 0);
85
160
 
86
161
  uint32_t *components;
@@ -96,7 +171,7 @@ bare_url_parse (js_env_t *env, js_callback_info_t *info) {
96
171
  }
97
172
 
98
173
  static js_value_t *
99
- bare_url_can_parse (js_env_t *env, js_callback_info_t *info) {
174
+ bare_url_can_parse(js_env_t *env, js_callback_info_t *info) {
100
175
  int err;
101
176
 
102
177
  size_t argc = 2;
@@ -115,17 +190,14 @@ bare_url_can_parse (js_env_t *env, js_callback_info_t *info) {
115
190
  url_init(&base);
116
191
 
117
192
  if (has_base) {
118
- size_t len;
119
- err = js_get_value_string_utf8(env, argv[1], NULL, 0, &len);
120
- assert(err == 0);
193
+ utf8_t stack[BARE_URL_STACK_STRING_MAX];
121
194
 
122
- utf8_t *input = malloc(len);
123
- err = js_get_value_string_utf8(env, argv[1], input, len, NULL);
124
- assert(err == 0);
195
+ bare_url_string_t input;
196
+ bare_url__read_string(env, argv[1], stack, sizeof(stack), &input);
125
197
 
126
- err = url_parse(&base, input, len, NULL);
198
+ err = url_parse(&base, input.data, input.len, NULL);
127
199
 
128
- free(input);
200
+ bare_url__free_string(env, &input);
129
201
 
130
202
  if (err < 0) {
131
203
  url_destroy(&base);
@@ -138,20 +210,17 @@ bare_url_can_parse (js_env_t *env, js_callback_info_t *info) {
138
210
  }
139
211
  }
140
212
 
141
- size_t len;
142
- err = js_get_value_string_utf8(env, argv[0], NULL, 0, &len);
143
- assert(err == 0);
213
+ utf8_t stack[BARE_URL_STACK_STRING_MAX];
144
214
 
145
- utf8_t *input = malloc(len);
146
- err = js_get_value_string_utf8(env, argv[0], input, len, NULL);
147
- assert(err == 0);
215
+ bare_url_string_t input;
216
+ bare_url__read_string(env, argv[0], stack, sizeof(stack), &input);
148
217
 
149
218
  url_t url;
150
219
  url_init(&url);
151
220
 
152
- err = url_parse(&url, input, len, has_base ? &base : NULL);
221
+ err = url_parse(&url, input.data, input.len, has_base ? &base : NULL);
153
222
 
154
- free(input);
223
+ bare_url__free_string(env, &input);
155
224
 
156
225
  url_destroy(&base);
157
226
  url_destroy(&url);
@@ -164,7 +233,7 @@ bare_url_can_parse (js_env_t *env, js_callback_info_t *info) {
164
233
  }
165
234
 
166
235
  static js_value_t *
167
- bare_url_exports (js_env_t *env, js_value_t *exports) {
236
+ bare_url_exports(js_env_t *env, js_value_t *exports) {
168
237
  int err;
169
238
 
170
239
  #define V(name, fn) \
package/index.js CHANGED
@@ -7,6 +7,20 @@ 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 characters that pathToFileURL() has to percent-encode itself. A backslash
21
+ // is a path separator on Windows and so is left alone there.
22
+ const reserved = isWindows ? /[%#?\n\r\t]/ : /[%#?\n\r\t\\]/
23
+
10
24
  class URL {
11
25
  static get [kind]() {
12
26
  return 0 // Compatibility version
@@ -19,11 +33,17 @@ class URL {
19
33
 
20
34
  if (base !== undefined) base = String(base)
21
35
 
22
- this._components = new Uint32Array(8)
36
+ this._href = undefined
37
+ this._schemeEnd = 0
38
+ this._usernameEnd = 0
39
+ this._hostStart = 0
40
+ this._hostEnd = 0
41
+ this._pathStart = 0
42
+ this._queryStart = 0
43
+ this._fragmentStart = 0
44
+ this._params = null
23
45
 
24
46
  this._parse(input, base, opts.throw !== false)
25
-
26
- if (this._href) this._params = new URLSearchParams(this.search, this)
27
47
  }
28
48
 
29
49
  get [kind]() {
@@ -39,23 +59,23 @@ class URL {
39
59
  set href(value) {
40
60
  this._update(value)
41
61
 
42
- this._params._parse(this.search)
62
+ if (this._params) this._params._parse(this.search)
43
63
  }
44
64
 
45
65
  // https://url.spec.whatwg.org/#dom-url-protocol
46
66
 
47
67
  get protocol() {
48
- return this._slice(0, this._components[0]) + ':'
68
+ return this._slice(0, this._schemeEnd) + ':'
49
69
  }
50
70
 
51
71
  set protocol(value) {
52
- this._update(this._replace(value.replace(/:+$/, ''), 0, this._components[0]))
72
+ this._update(this._replace(value.replace(/:+$/, ''), 0, this._schemeEnd))
53
73
  }
54
74
 
55
75
  // https://url.spec.whatwg.org/#dom-url-username
56
76
 
57
77
  get username() {
58
- return this._slice(this._components[0] + 3 /* :// */, this._components[1])
78
+ return this._slice(this._schemeEnd + 3 /* :// */, this._usernameEnd)
59
79
  }
60
80
 
61
81
  set username(value) {
@@ -65,13 +85,13 @@ class URL {
65
85
 
66
86
  if (this.username === '') value += '@'
67
87
 
68
- this._update(this._replace(value, this._components[0] + 3 /* :// */, this._components[1]))
88
+ this._update(this._replace(value, this._schemeEnd + 3 /* :// */, this._usernameEnd))
69
89
  }
70
90
 
71
91
  // https://url.spec.whatwg.org/#dom-url-password
72
92
 
73
93
  get password() {
74
- return this._href.slice(this._components[1] + 1 /* : */, this._components[2] - 1 /* @ */)
94
+ return this._href.slice(this._usernameEnd + 1 /* : */, this._hostStart - 1 /* @ */)
75
95
  }
76
96
 
77
97
  set password(value) {
@@ -79,8 +99,8 @@ class URL {
79
99
  return
80
100
  }
81
101
 
82
- let start = this._components[1] + 1 /* : */
83
- let end = this._components[2] - 1 /* @ */
102
+ let start = this._usernameEnd + 1 /* : */
103
+ let end = this._hostStart - 1 /* @ */
84
104
 
85
105
  if (this.password === '') {
86
106
  value = ':' + value
@@ -98,7 +118,7 @@ class URL {
98
118
  // https://url.spec.whatwg.org/#dom-url-host
99
119
 
100
120
  get host() {
101
- return this._slice(this._components[2], this._components[5])
121
+ return this._slice(this._hostStart, this._pathStart)
102
122
  }
103
123
 
104
124
  set host(value) {
@@ -107,14 +127,14 @@ class URL {
107
127
  }
108
128
 
109
129
  this._update(
110
- this._replace(value, this._components[2], this._components[value.includes(':') ? 5 : 3])
130
+ this._replace(value, this._hostStart, value.includes(':') ? this._pathStart : this._hostEnd)
111
131
  )
112
132
  }
113
133
 
114
134
  // https://url.spec.whatwg.org/#dom-url-hostname
115
135
 
116
136
  get hostname() {
117
- return this._slice(this._components[2], this._components[3])
137
+ return this._slice(this._hostStart, this._hostEnd)
118
138
  }
119
139
 
120
140
  set hostname(value) {
@@ -122,13 +142,13 @@ class URL {
122
142
  return
123
143
  }
124
144
 
125
- this._update(this._replace(value, this._components[2], this._components[3]))
145
+ this._update(this._replace(value, this._hostStart, this._hostEnd))
126
146
  }
127
147
 
128
148
  // https://url.spec.whatwg.org/#dom-url-port
129
149
 
130
150
  get port() {
131
- return this._slice(this._components[3] + 1 /* : */, this._components[5])
151
+ return this._slice(this._hostEnd + 1 /* : */, this._pathStart)
132
152
  }
133
153
 
134
154
  set port(value) {
@@ -136,20 +156,20 @@ class URL {
136
156
  return
137
157
  }
138
158
 
139
- let start = this._components[3] + 1 /* : */
159
+ let start = this._hostEnd + 1 /* : */
140
160
 
141
161
  if (this.port === '') {
142
162
  value = ':' + value
143
163
  start--
144
164
  }
145
165
 
146
- this._update(this._replace(value, start, this._components[5]))
166
+ this._update(this._replace(value, start, this._pathStart))
147
167
  }
148
168
 
149
169
  // https://url.spec.whatwg.org/#dom-url-pathname
150
170
 
151
171
  get pathname() {
152
- return this._slice(this._components[5], this._components[6] - 1 /* ? */)
172
+ return this._slice(this._pathStart, this._queryStart - 1 /* ? */)
153
173
  }
154
174
 
155
175
  set pathname(value) {
@@ -161,41 +181,45 @@ class URL {
161
181
  value = '/' + value
162
182
  }
163
183
 
164
- this._update(this._replace(value, this._components[5], this._components[6] - 1 /* ? */))
184
+ this._update(this._replace(value, this._pathStart, this._queryStart - 1 /* ? */))
165
185
  }
166
186
 
167
187
  // https://url.spec.whatwg.org/#dom-url-search
168
188
 
169
189
  get search() {
170
- return this._slice(this._components[6] - 1 /* ? */, this._components[7] - 1 /* # */)
190
+ return this._slice(this._queryStart - 1 /* ? */, this._fragmentStart - 1 /* # */)
171
191
  }
172
192
 
173
193
  set search(value) {
174
194
  if (value && value[0] !== '?') value = '?' + value
175
195
 
176
196
  this._update(
177
- this._replace(value, this._components[6] - 1 /* ? */, this._components[7] - 1 /* # */)
197
+ this._replace(value, this._queryStart - 1 /* ? */, this._fragmentStart - 1 /* # */)
178
198
  )
179
199
 
180
- this._params._parse(this.search)
200
+ if (this._params) this._params._parse(this.search)
181
201
  }
182
202
 
183
203
  // https://url.spec.whatwg.org/#dom-url-searchparams
184
204
 
185
205
  get searchParams() {
206
+ if (this._params === null) {
207
+ this._params = new URLSearchParams(this.search, this)
208
+ }
209
+
186
210
  return this._params
187
211
  }
188
212
 
189
213
  // https://url.spec.whatwg.org/#dom-url-hash
190
214
 
191
215
  get hash() {
192
- return this._slice(this._components[7] - 1 /* # */)
216
+ return this._slice(this._fragmentStart - 1 /* # */)
193
217
  }
194
218
 
195
219
  set hash(value) {
196
220
  if (value && value[0] !== '#') value = '#' + value
197
221
 
198
- this._update(this._replace(value, this._components[7] - 1 /* # */))
222
+ this._update(this._replace(value, this._fragmentStart - 1 /* # */))
199
223
  }
200
224
 
201
225
  toString() {
@@ -233,18 +257,32 @@ class URL {
233
257
  }
234
258
 
235
259
  _parse(input, base, shouldThrow) {
260
+ let href
261
+
236
262
  try {
237
- this._href = binding.parse(
238
- String(input),
239
- base ? String(base) : null,
240
- this._components,
241
- shouldThrow
242
- )
263
+ href = binding.parse(input, base || null, components, shouldThrow)
243
264
  } catch (err) {
244
265
  if (err instanceof TypeError) throw err
245
266
 
246
267
  throw errors.INVALID_URL(`Invalid URL '${input}'`, input)
247
268
  }
269
+
270
+ if (href === undefined) return
271
+
272
+ this._href = href
273
+ this._schemeEnd = components[0]
274
+ this._usernameEnd = components[1]
275
+ this._hostStart = components[2]
276
+ this._hostEnd = components[3]
277
+ this._pathStart = components[5]
278
+
279
+ const queryStart = components[6]
280
+ const fragmentStart = components[7]
281
+
282
+ const end = href.length + 1
283
+
284
+ this._queryStart = queryStart === unset ? end : queryStart
285
+ this._fragmentStart = fragmentStart === unset ? end : fragmentStart
248
286
  }
249
287
 
250
288
  _update(input) {
@@ -301,27 +339,35 @@ exports.fileURLToPath = function fileURLToPath(url) {
301
339
  throw errors.INVALID_URL_SCHEME('The URL must use the file: protocol')
302
340
  }
303
341
 
304
- if (isWindows) {
305
- if (/%2f|%5c/i.test(url.pathname)) {
306
- throw errors.INVALID_FILE_URL_PATH(
307
- 'The file: URL path must not include encoded \\ or / characters'
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
- }
342
+ if (!isWindows && url.hostname) {
343
+ throw errors.INVALID_FILE_URL_HOST("The file: URL host must be 'localhost' or empty")
344
+ }
345
+
346
+ const encoded = url.pathname
314
347
 
315
- if (/%2f/i.test(url.pathname)) {
348
+ // Every check below looks for a percent encoded sequence, as does the decoding
349
+ // that follows, so a path without any can skip all of them.
350
+ const hasEncoded = encoded.includes('%')
351
+
352
+ if (hasEncoded) {
353
+ if (isWindows) {
354
+ if (/%2f|%5c/i.test(encoded)) {
355
+ throw errors.INVALID_FILE_URL_PATH(
356
+ 'The file: URL path must not include encoded \\ or / characters'
357
+ )
358
+ }
359
+ } else if (/%2f/i.test(encoded)) {
316
360
  throw errors.INVALID_FILE_URL_PATH('The file: URL path must not include encoded / characters')
317
361
  }
318
- }
319
362
 
320
- if (/%00/i.test(url.pathname)) {
321
- throw errors.INVALID_FILE_URL_PATH('The file: URL path must not include encoded NUL characters')
363
+ if (/%00/i.test(encoded)) {
364
+ throw errors.INVALID_FILE_URL_PATH(
365
+ 'The file: URL path must not include encoded NUL characters'
366
+ )
367
+ }
322
368
  }
323
369
 
324
- const pathname = path.normalize(decodeURIComponent(url.pathname))
370
+ const pathname = path.normalize(hasEncoded ? decodeURIComponent(encoded) : encoded)
325
371
 
326
372
  if (isWindows) {
327
373
  if (url.hostname) return '\\\\' + url.hostname + pathname
@@ -347,16 +393,20 @@ exports.pathToFileURL = function pathToFileURL(pathname) {
347
393
  resolved += '\\'
348
394
  }
349
395
 
350
- resolved = resolved
351
- .replaceAll('%', '%25') // Must be first
352
- .replaceAll('#', '%23')
353
- .replaceAll('?', '%3f')
354
- .replaceAll('\n', '%0a')
355
- .replaceAll('\r', '%0d')
356
- .replaceAll('\t', '%09')
396
+ // Paths hardly ever contain any of these, so one pass to rule them out is
397
+ // cheaper than the six or seven replacements it stands in for.
398
+ if (reserved.test(resolved)) {
399
+ resolved = resolved
400
+ .replaceAll('%', '%25') // Must be first
401
+ .replaceAll('#', '%23')
402
+ .replaceAll('?', '%3f')
403
+ .replaceAll('\n', '%0a')
404
+ .replaceAll('\r', '%0d')
405
+ .replaceAll('\t', '%09')
357
406
 
358
- if (!isWindows) {
359
- resolved = resolved.replaceAll('\\', '%5c')
407
+ if (!isWindows) {
408
+ resolved = resolved.replaceAll('\\', '%5c')
409
+ }
360
410
  }
361
411
 
362
412
  return new URL('file:' + resolved)
@@ -9,17 +9,21 @@ class URLSearchParams {
9
9
 
10
10
  // https://url.spec.whatwg.org/#dom-urlsearchparams-urlsearchparams
11
11
  constructor(init, url = null) {
12
- this._params = new Map()
12
+ this._params = null
13
13
 
14
14
  if (url) URLSearchParams._urls.set(this, url)
15
15
 
16
16
  if (typeof init === 'string') {
17
17
  this._parse(init)
18
- } else if (init) {
19
- for (const [name, value] of typeof init[Symbol.iterator] === 'function'
20
- ? init
21
- : Object.entries(init)) {
22
- this.append(name, value)
18
+ } else {
19
+ this._params = new Map()
20
+
21
+ if (init) {
22
+ for (const [name, value] of typeof init[Symbol.iterator] === 'function'
23
+ ? init
24
+ : Object.entries(init)) {
25
+ this.append(name, value)
26
+ }
23
27
  }
24
28
  }
25
29
  }
@@ -30,7 +34,11 @@ class URLSearchParams {
30
34
 
31
35
  // https://url.spec.whatwg.org/#dom-urlsearchparams-size
32
36
  get size() {
33
- return this._params.length
37
+ let size = 0
38
+
39
+ for (const values of this._params.values()) size += values.length
40
+
41
+ return size
34
42
  }
35
43
 
36
44
  // https://url.spec.whatwg.org/#dom-urlsearchparams-append
@@ -141,47 +149,51 @@ class URLSearchParams {
141
149
 
142
150
  // https://url.spec.whatwg.org/#concept-urlencoded-parser
143
151
  _parse(input) {
144
- if (input[0] === '?') input = input.substring(1)
152
+ const params = new Map()
145
153
 
146
- this._params = new Map()
154
+ this._params = params
147
155
 
148
- for (let sequence of input.split('&')) {
149
- if (sequence.length === 0) continue
156
+ const len = input.length
150
157
 
151
- sequence = sequence.replace(/\+/g, ' ')
158
+ let start = input.charCodeAt(0) === 0x3f /* ? */ ? 1 : 0
152
159
 
153
- let i = sequence.indexOf('=')
154
- if (i === -1) i = sequence.length
160
+ while (start < len) {
161
+ let end = input.indexOf('&', start)
162
+ if (end === -1) end = len
155
163
 
156
- const name = decodeURIComponent(sequence.substring(0, i))
157
- const value = decodeURIComponent(sequence.substring(i + 1, sequence.length))
164
+ if (end !== start) {
165
+ let split = input.indexOf('=', start)
166
+ if (split === -1 || split > end) split = end
158
167
 
159
- let list = this._params.get(name)
168
+ const name = decode(input.slice(start, split))
169
+ const value = split === end ? '' : decode(input.slice(split + 1, end))
160
170
 
161
- if (list === undefined) {
162
- list = []
163
- this._params.set(name, list)
171
+ const list = params.get(name)
172
+
173
+ if (list === undefined) params.set(name, [value])
174
+ else list.push(value)
164
175
  }
165
176
 
166
- list.push(value)
177
+ start = end + 1
167
178
  }
168
179
  }
169
180
 
170
181
  // https://url.spec.whatwg.org/#concept-urlencoded-serializer
171
182
  _serialize() {
172
- let output = ''
183
+ let result = ''
184
+ let separator = ''
173
185
 
174
- for (let [name, values] of this._params) {
175
- name = encodeURIComponent(name)
186
+ for (const [name, values] of this._params) {
187
+ // The name is shared by all of its values, so it is only encoded once.
188
+ const encoded = encode(String(name))
176
189
 
177
190
  for (const value of values) {
178
- if (output) output += '&'
179
-
180
- output += name + '=' + encodeURIComponent(value)
191
+ result += separator + encoded + '=' + encode(String(value))
192
+ separator = '&'
181
193
  }
182
194
  }
183
195
 
184
- return output
196
+ return result
185
197
  }
186
198
  }
187
199
 
@@ -192,3 +204,66 @@ exports.isURLSearchParams = function isURLSearchParams(value) {
192
204
 
193
205
  return typeof value === 'object' && value !== null && value[kind] === URLSearchParams[kind]
194
206
  }
207
+
208
+ // https://url.spec.whatwg.org/#concept-urlencoded-byte-serializer
209
+ //
210
+ // Everything except ASCII alphanumerics and `*-._` is percent-encoded, and a
211
+ // space becomes `+` rather than `%20`. Names and values usually consist entirely
212
+ // of characters that are left alone, so a component is tested as a whole before
213
+ // any of it is rewritten.
214
+ const unencoded = /^[\w*.-]*$/
215
+
216
+ // As above, but allowing spaces, which need nothing more than turning into `+`.
217
+ // Spaces are by far the most common reason for a component to need rewriting at
218
+ // all, so recognizing them here spares those components a trip through
219
+ // encodeURIComponent().
220
+ const spaced = /^[\w*. -]*$/
221
+
222
+ // The characters that encodeURIComponent() leaves alone but the urlencoded byte
223
+ // serializer does not. Two patterns are needed because a global regular
224
+ // expression cannot be shared between test() and replace(), as test() advances
225
+ // its lastIndex.
226
+ const extra = /[!'()~]/
227
+ const extraAll = /[!'()~]/g
228
+
229
+ const escapes = {
230
+ '!': '%21',
231
+ "'": '%27',
232
+ '(': '%28',
233
+ ')': '%29',
234
+ '~': '%7E'
235
+ }
236
+
237
+ // A surrogate that is not part of a pair, and so does not encode a scalar value.
238
+ const lone = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/g
239
+
240
+ function encode(component) {
241
+ if (unencoded.test(component)) return component
242
+
243
+ if (spaced.test(component)) return component.replaceAll(' ', '+')
244
+
245
+ let encoded
246
+
247
+ try {
248
+ encoded = encodeURIComponent(component)
249
+ } catch {
250
+ // encodeURIComponent() rejects lone surrogates, whereas the UTF-8 encoder the
251
+ // serializer is defined in terms of replaces them with U+FFFD.
252
+ encoded = encodeURIComponent(component.replace(lone, '\ufffd'))
253
+ }
254
+
255
+ if (encoded.includes('%20')) encoded = encoded.replaceAll('%20', '+')
256
+
257
+ if (extra.test(encoded)) encoded = encoded.replace(extraAll, (match) => escapes[match])
258
+
259
+ return encoded
260
+ }
261
+
262
+ // https://url.spec.whatwg.org/#percent-decode-after-encoding
263
+ function decode(component) {
264
+ if (component.indexOf('+') !== -1) component = component.replaceAll('+', ' ')
265
+
266
+ if (component.indexOf('%') === -1) return component
267
+
268
+ return decodeURIComponent(component)
269
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bare-url",
3
- "version": "2.5.0",
3
+ "version": "2.5.2",
4
4
  "description": "WHATWG URL implementation for JavaScript",
5
5
  "exports": {
6
6
  "./package": "./package.json",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "devDependencies": {
47
47
  "brittle": "^3.3.2",
48
- "cmake-bare": "^1.1.6",
48
+ "cmake-bare": "^1.8.1",
49
49
  "cmake-fetch": "^1.0.0",
50
50
  "lunte": "^1.6.0",
51
51
  "prettier": "^3.3.3",
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file