bare-url 2.5.2 → 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 CHANGED
@@ -9,7 +9,7 @@ fetch_package("github:holepunchto/libutf#a1ceca8")
9
9
  fetch_package("github:holepunchto/libpunycode#e91ee34")
10
10
  fetch_package("github:holepunchto/libnormalize#0e81f65")
11
11
  fetch_package("github:holepunchto/libidna#1471406")
12
- fetch_package("github:holepunchto/liburl#fc3abba")
12
+ fetch_package("github:holepunchto/liburl#2efedc5")
13
13
 
14
14
  add_bare_module(bare_url)
15
15
 
package/README.md CHANGED
@@ -26,146 +26,7 @@ require('bare-url/global')
26
26
 
27
27
  ## API
28
28
 
29
- #### `const url = new URL(input[, base])`
30
-
31
- Parse `input` as a URL. If `base` is provided, `input` is resolved relative to `base`. Throws if `input` is not a valid URL.
32
-
33
- #### `url.href`
34
-
35
- The full serialized URL string. Setting this property reparses the URL.
36
-
37
- #### `url.protocol`
38
-
39
- The URL scheme followed by `':'`, e.g. `'https:'`.
40
-
41
- #### `url.username`
42
-
43
- The username portion of the URL, or an empty string.
44
-
45
- #### `url.password`
46
-
47
- The password portion of the URL, or an empty string.
48
-
49
- #### `url.host`
50
-
51
- The hostname and port, e.g. `'example.com:8080'`.
52
-
53
- #### `url.hostname`
54
-
55
- The hostname without the port.
56
-
57
- #### `url.port`
58
-
59
- The port as a string, or an empty string if not present.
60
-
61
- #### `url.pathname`
62
-
63
- The path portion of the URL.
64
-
65
- #### `url.search`
66
-
67
- The query string including the leading `'?'`, or an empty string.
68
-
69
- #### `url.searchParams`
70
-
71
- A `URLSearchParams` object for the query string. Mutations to the params are reflected in the URL.
72
-
73
- #### `url.hash`
74
-
75
- The fragment including the leading `'#'`, or an empty string.
76
-
77
- #### `url.toString()`
78
-
79
- Returns the serialized URL string. Equivalent to `url.href`.
80
-
81
- #### `url.toJSON()`
82
-
83
- Returns the serialized URL string. Suitable for JSON serialization.
84
-
85
- #### `const params = new URLSearchParams([init])`
86
-
87
- Create a new `URLSearchParams` instance. `init` may be a query string, an iterable of `[name, value]` pairs, or an object of key-value pairs.
88
-
89
- #### `params.size`
90
-
91
- The total number of search parameters.
92
-
93
- #### `params.append(name, value)`
94
-
95
- Append a new `name`/`value` pair.
96
-
97
- #### `params.delete(name[, value])`
98
-
99
- Remove all pairs with `name`. If `value` is provided, only pairs with both the matching `name` and `value` are removed.
100
-
101
- #### `params.get(name)`
102
-
103
- Return the first value for `name`, or `null` if not present.
104
-
105
- #### `params.getAll(name)`
106
-
107
- Return all values for `name` as an array.
108
-
109
- #### `params.has(name[, value])`
110
-
111
- Return `true` if a pair with `name` exists. If `value` is provided, the pair must also match `value`.
112
-
113
- #### `params.set(name, value)`
114
-
115
- Set the value for `name`, replacing any existing pairs with that name.
116
-
117
- #### `params.toString()`
118
-
119
- Return the serialized query string without the leading `'?'`.
120
-
121
- #### `params.toJSON()`
122
-
123
- Return the parameters as an array of `[name, value]` pairs.
124
-
125
- #### `URL.isURL(value)`
126
-
127
- Return `true` if `value` is a `URL` instance.
128
-
129
- #### `URLSearchParams.isURLSearchParams(value)`
130
-
131
- Return `true` if `value` is a `URLSearchParams` instance.
132
-
133
- #### `const url = URL.parse(input[, base])`
134
-
135
- Parse `input` as a URL without throwing. Returns a `URL` instance on success, or `null` on failure.
136
-
137
- #### `const valid = URL.canParse(input[, base])`
138
-
139
- Return `true` if `input` can be parsed as a valid URL, optionally relative to `base`.
140
-
141
- #### `const pathname = URL.fileURLToPath(url)`
142
-
143
- Convert a `file:` URL to a platform-specific file path. `url` may be a `URL` instance or a string. Throws if the URL does not use the `file:` protocol or contains invalid path characters.
144
-
145
- #### `const url = URL.pathToFileURL(pathname)`
146
-
147
- Convert a platform-specific file path to a `file:` URL.
148
-
149
- #### `const href = URL.format(parts)`
150
-
151
- Format a URL from individual `parts`:
152
-
153
- ```js
154
- parts = {
155
- protocol,
156
- auth,
157
- host,
158
- hostname,
159
- port,
160
- pathname,
161
- search,
162
- query,
163
- hash,
164
- slashes
165
- }
166
- ```
167
-
168
- All properties are optional. If `host` is provided, `hostname` and `port` are ignored. If `search` is provided, `query` is ignored. Set `slashes` to `true` to include `'//'` after the protocol.
29
+ See the [`bare-url` reference](https://docs.pears.com/reference/bare/modules/bare-url).
169
30
 
170
31
  ## License
171
32
 
package/binding.c CHANGED
@@ -1,18 +1,134 @@
1
1
  #include <assert.h>
2
2
  #include <bare.h>
3
3
  #include <js.h>
4
+ #include <stdbool.h>
4
5
  #include <stddef.h>
5
6
  #include <stdlib.h>
6
7
  #include <string.h>
7
8
  #include <url.h>
8
9
  #include <utf.h>
9
10
  #include <utf/string.h>
11
+ #include <uv.h>
10
12
 
11
13
  // Maximum length, in bytes, of a UTF-8 string that is read into a stack buffer.
12
14
  // Longer strings fall back to a heap allocation. This covers the vast majority
13
15
  // of URLs without touching the heap.
14
16
  #define BARE_URL_STACK_STRING_MAX 1024
15
17
 
18
+ // The number of component offsets the parser writes out.
19
+ #define BARE_URL_COMPONENTS_LEN (sizeof(((url_t *) 0)->components) / sizeof(url_component_t))
20
+
21
+ static bool
22
+ bare_url__check_argc(js_env_t *env, size_t argc, size_t expected) {
23
+ int err;
24
+
25
+ if (argc < expected) {
26
+ err = js_throw_type_errorf(env, NULL, "Expected %zu arguments, got %zu", expected, argc);
27
+ assert(err == 0);
28
+
29
+ return false;
30
+ }
31
+
32
+ return true;
33
+ }
34
+
35
+ static bool
36
+ bare_url__check_string(js_env_t *env, js_value_t *value, const char *message) {
37
+ int err;
38
+
39
+ bool is_string;
40
+ err = js_is_string(env, value, &is_string);
41
+ assert(err == 0);
42
+
43
+ if (!is_string) {
44
+ err = js_throw_type_error(env, NULL, message);
45
+ assert(err == 0);
46
+ }
47
+
48
+ return is_string;
49
+ }
50
+
51
+ static bool
52
+ bare_url__check_base(js_env_t *env, js_value_t *value, bool *has_base) {
53
+ int err;
54
+
55
+ js_value_type_t type;
56
+ err = js_typeof(env, value, &type);
57
+ assert(err == 0);
58
+
59
+ *has_base = type == js_string;
60
+
61
+ if (type == js_string || type == js_null || type == js_undefined) return true;
62
+
63
+ err = js_throw_type_error(env, NULL, "Base must be a string, null, or undefined");
64
+ assert(err == 0);
65
+
66
+ return false;
67
+ }
68
+
69
+ static bool
70
+ bare_url__check_boolean(js_env_t *env, js_value_t *value, const char *message) {
71
+ int err;
72
+
73
+ bool is_boolean;
74
+ err = js_is_boolean(env, value, &is_boolean);
75
+ assert(err == 0);
76
+
77
+ if (!is_boolean) {
78
+ err = js_throw_type_error(env, NULL, message);
79
+ assert(err == 0);
80
+ }
81
+
82
+ return is_boolean;
83
+ }
84
+
85
+ static bool
86
+ bare_url__check_components(js_env_t *env, js_value_t *value, uint32_t **result) {
87
+ int err;
88
+
89
+ bool is_typedarray;
90
+ err = js_is_typedarray(env, value, &is_typedarray);
91
+ assert(err == 0);
92
+
93
+ js_typedarray_type_t type;
94
+ size_t len;
95
+ js_value_t *arraybuffer;
96
+
97
+ if (is_typedarray) {
98
+ err = js_get_typedarray_info(env, value, &type, (void **) result, &len, &arraybuffer, NULL);
99
+ if (err < 0) return false;
100
+ }
101
+
102
+ if (!is_typedarray || type != js_uint32array) {
103
+ err = js_throw_type_error(env, NULL, "Components must be a Uint32Array");
104
+ assert(err == 0);
105
+
106
+ return false;
107
+ }
108
+
109
+ // A detached typed array reports no length of its own, but its data pointer
110
+ // is stale rather than null and so must not be written to.
111
+ bool is_detached;
112
+ err = js_is_detached_arraybuffer(env, arraybuffer, &is_detached);
113
+ assert(err == 0);
114
+
115
+ if (is_detached) {
116
+ err = js_throw_type_error(env, NULL, "Components must not be detached");
117
+ assert(err == 0);
118
+
119
+ return false;
120
+ }
121
+
122
+ if (len < BARE_URL_COMPONENTS_LEN) {
123
+ err = js_throw_range_errorf(env, NULL, "Components must have at least %zu elements, got %zu", (size_t) BARE_URL_COMPONENTS_LEN, len);
124
+ assert(err == 0);
125
+
126
+ return false;
127
+ }
128
+
129
+ return true;
130
+ }
131
+
16
132
  // The UTF-8 encoding of a JavaScript string, together with whatever backs it.
17
133
  typedef struct {
18
134
  const utf8_t *data;
@@ -25,20 +141,23 @@ typedef struct {
25
141
  // Exposes `value` as UTF-8. Strings that are stored as ASCII, which is nearly
26
142
  // all of them, are borrowed directly from the engine without being copied.
27
143
  // 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
144
+ // buffer when it does not fit. A non-zero return leaves an exception pending.
145
+ // The result must be released with bare_url__free_string() either way.
146
+ static inline int
31
147
  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
148
  int err;
33
149
 
150
+ result->data = NULL;
151
+ result->len = 0;
152
+ result->view = NULL;
153
+ result->heap = NULL;
154
+
34
155
  js_string_encoding_t encoding;
35
156
  const void *data;
36
157
  size_t len;
37
158
 
38
159
  err = js_get_string_view(env, value, &encoding, &data, &len, &result->view);
39
- assert(err == 0);
40
-
41
- result->heap = NULL;
160
+ if (err < 0) return err;
42
161
 
43
162
  size_t utf8_len;
44
163
 
@@ -46,7 +165,7 @@ bare_url__read_string(js_env_t *env, js_value_t *value, utf8_t *stack, size_t st
46
165
  result->data = (const utf8_t *) data;
47
166
  result->len = len;
48
167
 
49
- return;
168
+ return 0;
50
169
  }
51
170
 
52
171
  if (encoding == js_latin1) {
@@ -58,7 +177,7 @@ bare_url__read_string(js_env_t *env, js_value_t *value, utf8_t *stack, size_t st
58
177
  result->data = (const utf8_t *) data;
59
178
  result->len = len;
60
179
 
61
- return;
180
+ return 0;
62
181
  }
63
182
  } else {
64
183
  assert(encoding == js_utf16le);
@@ -66,7 +185,20 @@ bare_url__read_string(js_env_t *env, js_value_t *value, utf8_t *stack, size_t st
66
185
  utf8_len = utf8_length_from_utf16le((const utf16_t *) data, len);
67
186
  }
68
187
 
69
- utf8_t *buffer = utf8_len <= stack_len ? stack : (result->heap = malloc(utf8_len));
188
+ utf8_t *buffer;
189
+
190
+ if (utf8_len <= stack_len) {
191
+ buffer = stack;
192
+ } else {
193
+ buffer = result->heap = malloc(utf8_len);
194
+
195
+ if (buffer == NULL) {
196
+ err = js_throw_error(env, uv_err_name(UV_ENOMEM), uv_strerror(UV_ENOMEM));
197
+ assert(err == 0);
198
+
199
+ return -1;
200
+ }
201
+ }
70
202
 
71
203
  if (encoding == js_latin1) {
72
204
  latin1_convert_to_utf8((const latin1_t *) data, len, buffer);
@@ -76,6 +208,8 @@ bare_url__read_string(js_env_t *env, js_value_t *value, utf8_t *stack, size_t st
76
208
 
77
209
  result->data = buffer;
78
210
  result->len = utf8_len;
211
+
212
+ return 0;
79
213
  }
80
214
 
81
215
  // Releases a string read with bare_url__read_string(), freeing its buffer only
@@ -87,6 +221,8 @@ bare_url__free_string(js_env_t *env, bare_url_string_t *string) {
87
221
 
88
222
  free(string->heap);
89
223
 
224
+ if (string->view == NULL) return;
225
+
90
226
  err = js_release_string_view(env, string->view);
91
227
  assert(err == 0);
92
228
  }
@@ -101,15 +237,25 @@ bare_url_parse(js_env_t *env, js_callback_info_t *info) {
101
237
  err = js_get_callback_info(env, info, &argc, argv, NULL, NULL);
102
238
  assert(err == 0);
103
239
 
104
- assert(argc == 4);
240
+ if (!bare_url__check_argc(env, argc, 4)) return NULL;
241
+
242
+ if (!bare_url__check_string(env, argv[0], "Input must be a string")) return NULL;
243
+
244
+ bool has_base;
245
+ if (!bare_url__check_base(env, argv[1], &has_base)) return NULL;
246
+
247
+ uint32_t *components;
248
+ if (!bare_url__check_components(env, argv[2], &components)) return NULL;
249
+
250
+ if (!bare_url__check_boolean(env, argv[3], "Throw must be a boolean")) return NULL;
105
251
 
106
252
  bool should_throw;
107
253
  err = js_get_value_bool(env, argv[3], &should_throw);
108
254
  assert(err == 0);
109
255
 
110
- bool has_base;
111
- err = js_is_string(env, argv[1], &has_base);
112
- assert(err == 0);
256
+ // Set when the input could not be read, which leaves an exception of its own
257
+ // pending and so must not be reported as a parse failure on top.
258
+ bool threw = false;
113
259
 
114
260
  url_t base;
115
261
  url_init(&base);
@@ -118,16 +264,20 @@ bare_url_parse(js_env_t *env, js_callback_info_t *info) {
118
264
  utf8_t stack[BARE_URL_STACK_STRING_MAX];
119
265
 
120
266
  bare_url_string_t input;
121
- bare_url__read_string(env, argv[1], stack, sizeof(stack), &input);
267
+ err = bare_url__read_string(env, argv[1], stack, sizeof(stack), &input);
122
268
 
123
- err = url_parse(&base, input.data, input.len, NULL);
269
+ if (err == 0) err = url_parse(&base, input.data, input.len, NULL);
270
+ else threw = true;
124
271
 
125
272
  bare_url__free_string(env, &input);
126
273
 
127
274
  if (err < 0) {
128
275
  url_destroy(&base);
129
276
 
130
- if (should_throw) js_throw_error(env, NULL, "Invalid base URL");
277
+ if (should_throw && !threw) {
278
+ err = js_throw_error(env, NULL, "Invalid base URL");
279
+ assert(err == 0);
280
+ }
131
281
 
132
282
  return NULL;
133
283
  }
@@ -136,12 +286,13 @@ bare_url_parse(js_env_t *env, js_callback_info_t *info) {
136
286
  utf8_t stack[BARE_URL_STACK_STRING_MAX];
137
287
 
138
288
  bare_url_string_t input;
139
- bare_url__read_string(env, argv[0], stack, sizeof(stack), &input);
289
+ err = bare_url__read_string(env, argv[0], stack, sizeof(stack), &input);
140
290
 
141
291
  url_t url;
142
292
  url_init(&url);
143
293
 
144
- err = url_parse(&url, input.data, input.len, has_base ? &base : NULL);
294
+ if (err == 0) err = url_parse(&url, input.data, input.len, has_base ? &base : NULL);
295
+ else threw = true;
145
296
 
146
297
  bare_url__free_string(env, &input);
147
298
 
@@ -149,24 +300,26 @@ bare_url_parse(js_env_t *env, js_callback_info_t *info) {
149
300
  url_destroy(&base);
150
301
  url_destroy(&url);
151
302
 
152
- if (should_throw) js_throw_error(env, NULL, "Invalid URL");
303
+ if (should_throw && !threw) {
304
+ err = js_throw_error(env, NULL, "Invalid URL");
305
+ assert(err == 0);
306
+ }
153
307
 
154
308
  return NULL;
155
309
  }
156
310
 
157
311
  js_value_t *href;
158
312
  err = js_create_string_latin1(env, (const latin1_t *) url.href.data, url.href.len, &href);
159
- assert(err == 0);
160
-
161
- uint32_t *components;
162
- err = js_get_typedarray_info(env, argv[2], NULL, (void **) &components, NULL, NULL, NULL);
163
- assert(err == 0);
164
313
 
165
- memcpy(components, &url.components, sizeof(url.components));
314
+ // The offsets are only handed over once the href they refer to is, so a
315
+ // failure here leaves the caller's buffer as it was.
316
+ if (err == 0) memcpy(components, &url.components, sizeof(url.components));
166
317
 
167
318
  url_destroy(&base);
168
319
  url_destroy(&url);
169
320
 
321
+ if (err < 0) return NULL;
322
+
170
323
  return href;
171
324
  }
172
325
 
@@ -180,11 +333,14 @@ bare_url_can_parse(js_env_t *env, js_callback_info_t *info) {
180
333
  err = js_get_callback_info(env, info, &argc, argv, NULL, NULL);
181
334
  assert(err == 0);
182
335
 
183
- assert(argc == 2);
336
+ if (!bare_url__check_argc(env, argc, 2)) return NULL;
337
+
338
+ if (!bare_url__check_string(env, argv[0], "Input must be a string")) return NULL;
184
339
 
185
340
  bool has_base;
186
- err = js_is_string(env, argv[1], &has_base);
187
- assert(err == 0);
341
+ if (!bare_url__check_base(env, argv[1], &has_base)) return NULL;
342
+
343
+ bool threw = false;
188
344
 
189
345
  url_t base;
190
346
  url_init(&base);
@@ -193,15 +349,18 @@ bare_url_can_parse(js_env_t *env, js_callback_info_t *info) {
193
349
  utf8_t stack[BARE_URL_STACK_STRING_MAX];
194
350
 
195
351
  bare_url_string_t input;
196
- bare_url__read_string(env, argv[1], stack, sizeof(stack), &input);
352
+ err = bare_url__read_string(env, argv[1], stack, sizeof(stack), &input);
197
353
 
198
- err = url_parse(&base, input.data, input.len, NULL);
354
+ if (err == 0) err = url_parse(&base, input.data, input.len, NULL);
355
+ else threw = true;
199
356
 
200
357
  bare_url__free_string(env, &input);
201
358
 
202
359
  if (err < 0) {
203
360
  url_destroy(&base);
204
361
 
362
+ if (threw) return NULL;
363
+
205
364
  js_value_t *result;
206
365
  err = js_get_boolean(env, false, &result);
207
366
  assert(err == 0);
@@ -213,18 +372,21 @@ bare_url_can_parse(js_env_t *env, js_callback_info_t *info) {
213
372
  utf8_t stack[BARE_URL_STACK_STRING_MAX];
214
373
 
215
374
  bare_url_string_t input;
216
- bare_url__read_string(env, argv[0], stack, sizeof(stack), &input);
375
+ err = bare_url__read_string(env, argv[0], stack, sizeof(stack), &input);
217
376
 
218
377
  url_t url;
219
378
  url_init(&url);
220
379
 
221
- err = url_parse(&url, input.data, input.len, has_base ? &base : NULL);
380
+ if (err == 0) err = url_parse(&url, input.data, input.len, has_base ? &base : NULL);
381
+ else threw = true;
222
382
 
223
383
  bare_url__free_string(env, &input);
224
384
 
225
385
  url_destroy(&base);
226
386
  url_destroy(&url);
227
387
 
388
+ if (threw) return NULL;
389
+
228
390
  js_value_t *result;
229
391
  err = js_get_boolean(env, err == 0, &result);
230
392
  assert(err == 0);
package/global.d.ts CHANGED
@@ -4,7 +4,19 @@ type URLConstructor = typeof url.URL
4
4
  type URLSearchParamsConstructor = typeof url.URLSearchParams
5
5
 
6
6
  declare global {
7
+ /**
8
+ * Parse `input` as a URL. If `base` is provided, `input` is resolved relative to `base`.
9
+ * @param input - The URL string to parse.
10
+ * @param base - A base URL that `input` is resolved relative to, if provided.
11
+ * @throws {INVALID_URL} `input` is not a valid URL.
12
+ */
7
13
  type URL = url.URL
14
+ /**
15
+ * Create a new `URLSearchParams` instance. `init` may be a query string, an iterable of `[name,
16
+ * value]` pairs, or an object of key-value pairs.
17
+ * @param init - A query string, an iterable of `[name, value]` pairs, or an object of key-value
18
+ * pairs to initialize the params from.
19
+ */
8
20
  type URLSearchParams = url.URLSearchParams
9
21
 
10
22
  const URL: URLConstructor
package/index.d.ts CHANGED
@@ -2,37 +2,92 @@ import URLError from './lib/errors'
2
2
  import URLSearchParams from './lib/url-search-params'
3
3
 
4
4
  interface URL {
5
+ /** The full serialized URL string. Setting this property reparses the URL. */
5
6
  href: string
7
+ /** The URL scheme followed by `':'`, for example `'https:'`. */
6
8
  protocol: string
9
+ /** The username portion of the URL, or an empty string. */
7
10
  username: string
11
+ /** The password portion of the URL, or an empty string. */
8
12
  password: string
13
+ /** The hostname and port, for example `'example.com:8080'`. */
9
14
  host: string
15
+ /** The hostname without the port. */
10
16
  hostname: string
17
+ /** The port as a string, or an empty string if not present. */
11
18
  port: string
19
+ /** The path portion of the URL. */
12
20
  pathname: string
21
+ /** The query string including the leading `'?'`, or an empty string. */
13
22
  search: string
23
+ /**
24
+ * A `URLSearchParams` object for the query string. Mutations to the params are reflected in the
25
+ * URL.
26
+ */
14
27
  searchParams: URLSearchParams
28
+ /** The fragment including the leading `'#'`, or an empty string. */
15
29
  hash: string
16
30
 
31
+ /** Returns the serialized string form. */
17
32
  toString(): string
33
+ /** Returns the serialized string form. Suitable for JSON serialization. */
18
34
  toJSON(): string
19
35
  }
20
36
 
21
37
  declare class URL {
38
+ /**
39
+ * Parse `input` as a URL. If `base` is provided, `input` is resolved relative to `base`.
40
+ * @param input - The URL string to parse.
41
+ * @param base - A base URL that `input` is resolved relative to, if provided.
42
+ * @throws {INVALID_URL} `input` is not a valid URL.
43
+ */
22
44
  constructor(input: string | URL, base?: string | URL)
23
45
  }
24
46
 
25
47
  declare namespace URL {
48
+ /**
49
+ * Return `true` if `value` is a `URL` instance.
50
+ * @param value - The value to test.
51
+ */
26
52
  export function isURL(value: unknown): value is URL
27
53
 
54
+ /**
55
+ * Return `true` if `value` is a `URLSearchParams` instance.
56
+ * @param value - The value to test.
57
+ */
28
58
  export function isURLSearchParams(value: unknown): value is URLSearchParams
29
59
 
60
+ /**
61
+ * Parse `input` as a URL without throwing.
62
+ * @param input - The URL string to parse.
63
+ * @param base - A base URL that `input` is resolved relative to, if provided.
64
+ * @returns A `URL` instance if `input` parses successfully, or `null` on failure.
65
+ */
30
66
  export function parse(input: string, base?: string | URL): URL | null
31
67
 
68
+ /**
69
+ * Return `true` if `input` can be parsed as a valid URL, optionally relative to `base`.
70
+ * @param input - The URL string to test.
71
+ * @param base - A base URL that `input` is resolved relative to, if provided.
72
+ */
32
73
  export function canParse(input: string, base?: string | URL): boolean
33
74
 
75
+ /**
76
+ * Convert a `file:` URL to a platform-specific file path. `url` may be a `URL` instance or a
77
+ * string.
78
+ * @param url - The `file:` URL to convert, as a `URL` instance or a string.
79
+ * @throws {INVALID_URL_SCHEME} the URL does not use the `file:` protocol.
80
+ * @throws {INVALID_FILE_URL_HOST} (non-Windows) the URL has a host other than empty or
81
+ * `'localhost'`.
82
+ * @throws {INVALID_FILE_URL_PATH} the URL path contains an encoded path-separator or NUL
83
+ * character, or, on Windows, is not an absolute drive path.
84
+ */
34
85
  export function fileURLToPath(url: URL | string): string
35
86
 
87
+ /**
88
+ * Convert a platform-specific file path to a `file:` URL.
89
+ * @param pathname - The platform-specific file path to convert.
90
+ */
36
91
  export function pathToFileURL(pathname: string): URL
37
92
 
38
93
  export { URL, type URLError, URLError as errors, URLSearchParams }
package/index.js CHANGED
@@ -17,6 +17,53 @@ const components = new Uint32Array(8)
17
17
  // The value used for a component that is not present in the URL.
18
18
  const unset = 0xffffffff
19
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
+
20
67
  // The characters that pathToFileURL() has to percent-encode itself. A backslash
21
68
  // is a path separator on Windows and so is left alone there.
22
69
  const reserved = isWindows ? /[%#?\n\r\t]/ : /[%#?\n\r\t\\]/
@@ -57,7 +104,9 @@ class URL {
57
104
  }
58
105
 
59
106
  set href(value) {
60
- this._update(value)
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)
61
110
 
62
111
  if (this._params) this._params._parse(this.search)
63
112
  }
@@ -69,7 +118,27 @@ class URL {
69
118
  }
70
119
 
71
120
  set protocol(value) {
72
- this._update(this._replace(value.replace(/:+$/, ''), 0, this._schemeEnd))
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))
73
142
  }
74
143
 
75
144
  // https://url.spec.whatwg.org/#dom-url-username
@@ -83,7 +152,9 @@ class URL {
83
152
  return
84
153
  }
85
154
 
86
- if (this.username === '') value += '@'
155
+ value = encodeUserinfo(String(value))
156
+
157
+ if (!hasCredentials(this)) value += '@'
87
158
 
88
159
  this._update(this._replace(value, this._schemeEnd + 3 /* :// */, this._usernameEnd))
89
160
  }
@@ -99,20 +170,16 @@ class URL {
99
170
  return
100
171
  }
101
172
 
102
- let start = this._usernameEnd + 1 /* : */
103
- let end = this._hostStart - 1 /* @ */
173
+ value = ':' + encodeUserinfo(String(value))
104
174
 
105
- if (this.password === '') {
106
- value = ':' + value
107
- start--
108
- }
175
+ let end = this._hostStart - 1 /* @ */
109
176
 
110
- if (this.username === '') {
177
+ if (!hasCredentials(this)) {
111
178
  value += '@'
112
- end++
179
+ end = this._usernameEnd
113
180
  }
114
181
 
115
- this._update(this._replace(value, start, end))
182
+ this._update(this._replace(value, this._usernameEnd, end))
116
183
  }
117
184
 
118
185
  // https://url.spec.whatwg.org/#dom-url-host
@@ -126,9 +193,38 @@ class URL {
126
193
  return
127
194
  }
128
195
 
129
- this._update(
130
- this._replace(value, this._hostStart, value.includes(':') ? this._pathStart : this._hostEnd)
131
- )
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))
132
228
  }
133
229
 
134
230
  // https://url.spec.whatwg.org/#dom-url-hostname
@@ -142,6 +238,17 @@ class URL {
142
238
  return
143
239
  }
144
240
 
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
+
145
252
  this._update(this._replace(value, this._hostStart, this._hostEnd))
146
253
  }
147
254
 
@@ -156,14 +263,15 @@ class URL {
156
263
  return
157
264
  }
158
265
 
159
- let start = this._hostEnd + 1 /* : */
266
+ value = strip(String(value))
160
267
 
161
- if (this.port === '') {
162
- value = ':' + value
163
- start--
268
+ if (value !== '') {
269
+ value = parsePort(value)
270
+
271
+ if (value === null) return
164
272
  }
165
273
 
166
- this._update(this._replace(value, start, this._pathStart))
274
+ this._update(this._replace(value, this._hostEnd, this._pathStart))
167
275
  }
168
276
 
169
277
  // https://url.spec.whatwg.org/#dom-url-pathname
@@ -177,7 +285,11 @@ class URL {
177
285
  return
178
286
  }
179
287
 
180
- if (value[0] !== '/' && value[0] !== '\\') {
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] !== '\\') {
181
293
  value = '/' + value
182
294
  }
183
295
 
@@ -191,13 +303,17 @@ class URL {
191
303
  }
192
304
 
193
305
  set search(value) {
194
- if (value && value[0] !== '?') value = '?' + value
306
+ value = String(value)
307
+
308
+ if (value !== '') {
309
+ if (value[0] === '?') value = value.slice(1)
310
+
311
+ value = '?' + encodeQuery(encodeEdges(value))
312
+ }
195
313
 
196
314
  this._update(
197
315
  this._replace(value, this._queryStart - 1 /* ? */, this._fragmentStart - 1 /* # */)
198
316
  )
199
-
200
- if (this._params) this._params._parse(this.search)
201
317
  }
202
318
 
203
319
  // https://url.spec.whatwg.org/#dom-url-searchparams
@@ -217,7 +333,15 @@ class URL {
217
333
  }
218
334
 
219
335
  set hash(value) {
220
- if (value && value[0] !== '#') value = '#' + value
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
+ }
221
345
 
222
346
  this._update(this._replace(value, this._fragmentStart - 1 /* # */))
223
347
  }
@@ -262,7 +386,7 @@ class URL {
262
386
  try {
263
387
  href = binding.parse(input, base || null, components, shouldThrow)
264
388
  } catch (err) {
265
- if (err instanceof TypeError) throw err
389
+ if (err instanceof TypeError || err.code !== undefined) throw err
266
390
 
267
391
  throw errors.INVALID_URL(`Invalid URL '${input}'`, input)
268
392
  }
@@ -290,7 +414,11 @@ class URL {
290
414
  this._parse(input, null, true)
291
415
  } catch (err) {
292
416
  if (err instanceof TypeError) throw err
417
+
418
+ return
293
419
  }
420
+
421
+ if (this._params) this._params._parse(this.search)
294
422
  }
295
423
  }
296
424
 
@@ -306,6 +434,93 @@ function cannotHaveCredentialsOrPort(url) {
306
434
  return url.hostname === '' || url.protocol === 'file:'
307
435
  }
308
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
+
309
524
  exports.URL = URL
310
525
  exports.URLSearchParams = URLSearchParams
311
526
 
@@ -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
 
@@ -1,8 +1,10 @@
1
1
  const kind = Symbol.for('bare.url.search-params.kind')
2
2
 
3
- class URLSearchParams {
4
- static _urls = new WeakMap()
3
+ // The URL each instance writes back to, if any. Kept module private so that the
4
+ // setter it drives cannot be pointed at an arbitrary object.
5
+ const urls = new WeakMap()
5
6
 
7
+ class URLSearchParams {
6
8
  static get [kind]() {
7
9
  return 0 // Compatibility version
8
10
  }
@@ -11,7 +13,7 @@ class URLSearchParams {
11
13
  constructor(init, url = null) {
12
14
  this._params = null
13
15
 
14
- if (url) URLSearchParams._urls.set(this, url)
16
+ if (url) urls.set(this, url)
15
17
 
16
18
  if (typeof init === 'string') {
17
19
  this._parse(init)
@@ -140,7 +142,7 @@ class URLSearchParams {
140
142
 
141
143
  // https://url.spec.whatwg.org/#concept-urlsearchparams-update
142
144
  _update() {
143
- const url = URLSearchParams._urls.get(this)
145
+ const url = urls.get(this)
144
146
 
145
147
  if (url === undefined) return
146
148
 
@@ -234,8 +236,11 @@ const escapes = {
234
236
  '~': '%7E'
235
237
  }
236
238
 
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
+ // A surrogate pair, or a surrogate that is not part of one and so encodes no
240
+ // scalar value. Pairs are matched first so that only the capture group can hold
241
+ // a lone surrogate; a lookbehind would say this more directly but isn't
242
+ // supported by every engine.
243
+ const lone = /[\ud800-\udbff][\udc00-\udfff]|([\ud800-\udbff]|[\udc00-\udfff])/g
239
244
 
240
245
  function encode(component) {
241
246
  if (unencoded.test(component)) return component
@@ -249,7 +254,9 @@ function encode(component) {
249
254
  } catch {
250
255
  // encodeURIComponent() rejects lone surrogates, whereas the UTF-8 encoder the
251
256
  // serializer is defined in terms of replaces them with U+FFFD.
252
- encoded = encodeURIComponent(component.replace(lone, '\ufffd'))
257
+ encoded = encodeURIComponent(
258
+ component.replace(lone, (match, single) => (single === undefined ? match : '\ufffd'))
259
+ )
253
260
  }
254
261
 
255
262
  if (encoded.includes('%20')) encoded = encoded.replaceAll('%20', '+')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bare-url",
3
- "version": "2.5.2",
3
+ "version": "2.5.3",
4
4
  "description": "WHATWG URL implementation for JavaScript",
5
5
  "exports": {
6
6
  "./package": "./package.json",
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file