Skip to content

Commit 9b18df9

Browse files
thisalihassanaduh95
authored andcommitted
url: implement parse method for safer URL parsing
Implement the static parse method as per the WHATWG URL specification. Unlike the URL constructor, URL.parse does not throw on invalid input, instead returning null. This behavior allows safer parsing of URLs without the need for try-catch blocks around constructor calls. The implementation follows the steps outlined in the WHATWG URL standard, ensuring compatibility and consistency with web platform URL parsing APIs. Fixes: #52208 Refs: whatwg/url#825 PR-URL: #52280 Reviewed-By: Yagiz Nizipli <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Daniel Lemire <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]>
1 parent fdcde84 commit 9b18df9

File tree

6 files changed

+268
-6
lines changed

6 files changed

+268
-6
lines changed

lib/internal/url.js

+22-2
Original file line numberDiff line numberDiff line change
@@ -769,6 +769,14 @@ function isURL(self) {
769769
return Boolean(self?.href && self.protocol && self.auth === undefined && self.path === undefined);
770770
}
771771

772+
/**
773+
* A unique symbol used as a private identifier to safely invoke the URL constructor
774+
* with a special parsing behavior. When passed as the third argument to the URL
775+
* constructor, it signals that the constructor should not throw an exception
776+
* for invalid URL inputs.
777+
*/
778+
const kParseURLSymbol = Symbol('kParseURL');
779+
772780
class URL {
773781
#context = new URLContext();
774782
#searchParams;
@@ -787,7 +795,7 @@ class URL {
787795
};
788796
}
789797

790-
constructor(input, base = undefined) {
798+
constructor(input, base = undefined, parseSymbol = undefined) {
791799
markTransferMode(this, false, false);
792800

793801
if (arguments.length === 0) {
@@ -801,7 +809,19 @@ class URL {
801809
base = `${base}`;
802810
}
803811

804-
this.#updateContext(bindingUrl.parse(input, base));
812+
const raiseException = parseSymbol !== kParseURLSymbol;
813+
const href = bindingUrl.parse(input, base, raiseException);
814+
if (href) {
815+
this.#updateContext(href);
816+
}
817+
}
818+
819+
static parse(input, base = undefined) {
820+
if (arguments.length === 0) {
821+
throw new ERR_MISSING_ARGS('url');
822+
}
823+
const parsedURLObject = new URL(input, base, kParseURLSymbol);
824+
return parsedURLObject.href ? parsedURLObject : null;
805825
}
806826

807827
[inspect.custom](depth, opts) {

src/node_url.cc

+9-2
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,9 @@ void BindingData::Parse(const FunctionCallbackInfo<Value>& args) {
233233
CHECK_GE(args.Length(), 1);
234234
CHECK(args[0]->IsString()); // input
235235
// args[1] // base url
236+
// args[2] // raise Exception
237+
238+
const bool raise_exception = args.Length() > 2 && args[2]->IsTrue();
236239

237240
Realm* realm = Realm::GetCurrent(args);
238241
BindingData* binding_data = realm->GetBindingData<BindingData>();
@@ -245,16 +248,20 @@ void BindingData::Parse(const FunctionCallbackInfo<Value>& args) {
245248
if (args[1]->IsString()) {
246249
base_ = Utf8Value(isolate, args[1]).ToString();
247250
base = ada::parse<ada::url_aggregator>(*base_);
248-
if (!base) {
251+
if (!base && raise_exception) {
249252
return ThrowInvalidURL(realm->env(), input.ToStringView(), base_);
253+
} else if (!base) {
254+
return;
250255
}
251256
base_pointer = &base.value();
252257
}
253258
auto out =
254259
ada::parse<ada::url_aggregator>(input.ToStringView(), base_pointer);
255260

256-
if (!out) {
261+
if (!out && raise_exception) {
257262
return ThrowInvalidURL(realm->env(), input.ToStringView(), base_);
263+
} else if (!out) {
264+
return;
258265
}
259266

260267
binding_data->UpdateComponents(out->get_components(), out->type);

test/fixtures/wpt/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Last update:
2828
- resource-timing: https://github.com/web-platform-tests/wpt/tree/22d38586d0/resource-timing
2929
- resources: https://github.com/web-platform-tests/wpt/tree/1e140d63ec/resources
3030
- streams: https://github.com/web-platform-tests/wpt/tree/3df6d94318/streams
31-
- url: https://github.com/web-platform-tests/wpt/tree/c2d7e70b52/url
31+
- url: https://github.com/web-platform-tests/wpt/tree/0f550ab9f5/url
3232
- user-timing: https://github.com/web-platform-tests/wpt/tree/5ae85bf826/user-timing
3333
- wasm/jsapi: https://github.com/web-platform-tests/wpt/tree/cde25e7e3c/wasm/jsapi
3434
- wasm/webapi: https://github.com/web-platform-tests/wpt/tree/fd1b23eeaa/wasm/webapi

test/fixtures/wpt/url/resources/urltestdata.json

+185
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,36 @@
734734
"search": "",
735735
"hash": ""
736736
},
737+
{
738+
"input": "http://a:b@c\\",
739+
"base": null,
740+
"href": "http://a:b@c/",
741+
"origin": "http://c",
742+
"protocol": "http:",
743+
"username": "a",
744+
"password": "b",
745+
"host": "c",
746+
"hostname": "c",
747+
"port": "",
748+
"pathname": "/",
749+
"search": "",
750+
"hash": ""
751+
},
752+
{
753+
"input": "ws://a@b\\c",
754+
"base": null,
755+
"href": "ws://a@b/c",
756+
"origin": "ws://b",
757+
"protocol": "ws:",
758+
"username": "a",
759+
"password": "",
760+
"host": "b",
761+
"hostname": "b",
762+
"port": "",
763+
"pathname": "/c",
764+
"search": "",
765+
"hash": ""
766+
},
737767
{
738768
"input": "foo:/",
739769
"base": "http://example.org/foo/bar",
@@ -9529,5 +9559,160 @@
95299559
"pathname": "",
95309560
"search": "",
95319561
"hash": ""
9562+
},
9563+
"Scheme relative path starting with multiple slashes",
9564+
{
9565+
"input": "///test",
9566+
"base": "http://example.org/",
9567+
"href": "http://test/",
9568+
"protocol": "http:",
9569+
"username": "",
9570+
"password": "",
9571+
"host": "test",
9572+
"hostname": "test",
9573+
"port": "",
9574+
"pathname": "/",
9575+
"search": "",
9576+
"hash": ""
9577+
},
9578+
{
9579+
"input": "///\\//\\//test",
9580+
"base": "http://example.org/",
9581+
"href": "http://test/",
9582+
"protocol": "http:",
9583+
"username": "",
9584+
"password": "",
9585+
"host": "test",
9586+
"hostname": "test",
9587+
"port": "",
9588+
"pathname": "/",
9589+
"search": "",
9590+
"hash": ""
9591+
},
9592+
{
9593+
"input": "///example.org/path",
9594+
"base": "http://example.org/",
9595+
"href": "http://example.org/path",
9596+
"protocol": "http:",
9597+
"username": "",
9598+
"password": "",
9599+
"host": "example.org",
9600+
"hostname": "example.org",
9601+
"port": "",
9602+
"pathname": "/path",
9603+
"search": "",
9604+
"hash": ""
9605+
},
9606+
{
9607+
"input": "///example.org/../path",
9608+
"base": "http://example.org/",
9609+
"href": "http://example.org/path",
9610+
"protocol": "http:",
9611+
"username": "",
9612+
"password": "",
9613+
"host": "example.org",
9614+
"hostname": "example.org",
9615+
"port": "",
9616+
"pathname": "/path",
9617+
"search": "",
9618+
"hash": ""
9619+
},
9620+
{
9621+
"input": "///example.org/../../",
9622+
"base": "http://example.org/",
9623+
"href": "http://example.org/",
9624+
"protocol": "http:",
9625+
"username": "",
9626+
"password": "",
9627+
"host": "example.org",
9628+
"hostname": "example.org",
9629+
"port": "",
9630+
"pathname": "/",
9631+
"search": "",
9632+
"hash": ""
9633+
},
9634+
{
9635+
"input": "///example.org/../path/../../",
9636+
"base": "http://example.org/",
9637+
"href": "http://example.org/",
9638+
"protocol": "http:",
9639+
"username": "",
9640+
"password": "",
9641+
"host": "example.org",
9642+
"hostname": "example.org",
9643+
"port": "",
9644+
"pathname": "/",
9645+
"search": "",
9646+
"hash": ""
9647+
},
9648+
{
9649+
"input": "///example.org/../path/../../path",
9650+
"base": "http://example.org/",
9651+
"href": "http://example.org/path",
9652+
"protocol": "http:",
9653+
"username": "",
9654+
"password": "",
9655+
"host": "example.org",
9656+
"hostname": "example.org",
9657+
"port": "",
9658+
"pathname": "/path",
9659+
"search": "",
9660+
"hash": ""
9661+
},
9662+
{
9663+
"input": "/\\/\\//example.org/../path",
9664+
"base": "http://example.org/",
9665+
"href": "http://example.org/path",
9666+
"protocol": "http:",
9667+
"username": "",
9668+
"password": "",
9669+
"host": "example.org",
9670+
"hostname": "example.org",
9671+
"port": "",
9672+
"pathname": "/path",
9673+
"search": "",
9674+
"hash": ""
9675+
},
9676+
{
9677+
"input": "///abcdef/../",
9678+
"base": "file:///",
9679+
"href": "file:///",
9680+
"protocol": "file:",
9681+
"username": "",
9682+
"password": "",
9683+
"host": "",
9684+
"hostname": "",
9685+
"port": "",
9686+
"pathname": "/",
9687+
"search": "",
9688+
"hash": ""
9689+
},
9690+
{
9691+
"input": "/\\//\\/a/../",
9692+
"base": "file:///",
9693+
"href": "file://////",
9694+
"protocol": "file:",
9695+
"username": "",
9696+
"password": "",
9697+
"host": "",
9698+
"hostname": "",
9699+
"port": "",
9700+
"pathname": "////",
9701+
"search": "",
9702+
"hash": ""
9703+
},
9704+
{
9705+
"input": "//a/../",
9706+
"base": "file:///",
9707+
"href": "file://a/",
9708+
"protocol": "file:",
9709+
"username": "",
9710+
"password": "",
9711+
"host": "a",
9712+
"hostname": "a",
9713+
"port": "",
9714+
"pathname": "/",
9715+
"search": "",
9716+
"hash": ""
95329717
}
95339718
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// This intentionally does not use resources/urltestdata.json to preserve resources.
2+
[
3+
{
4+
"url": undefined,
5+
"base": undefined,
6+
"expected": false
7+
},
8+
{
9+
"url": "aaa:b",
10+
"base": undefined,
11+
"expected": true
12+
},
13+
{
14+
"url": undefined,
15+
"base": "aaa:b",
16+
"expected": false
17+
},
18+
{
19+
"url": "aaa:/b",
20+
"base": undefined,
21+
"expected": true
22+
},
23+
{
24+
"url": undefined,
25+
"base": "aaa:/b",
26+
"expected": true
27+
},
28+
{
29+
"url": "https://test:test",
30+
"base": undefined,
31+
"expected": false
32+
},
33+
{
34+
"url": "a",
35+
"base": "https://b/",
36+
"expected": true
37+
}
38+
].forEach(({ url, base, expected }) => {
39+
test(() => {
40+
if (expected == false) {
41+
assert_equals(URL.parse(url, base), null);
42+
} else {
43+
assert_equals(URL.parse(url, base).href, new URL(url, base).href);
44+
}
45+
}, `URL.parse(${url}, ${base})`);
46+
});
47+
48+
test(() => {
49+
assert_not_equals(URL.parse("https://example/"), URL.parse("https://example/"));
50+
}, `URL.parse() should return a unique object`);

test/fixtures/wpt/versions.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
"path": "streams"
7373
},
7474
"url": {
75-
"commit": "c2d7e70b52cbd9a5b938aa32f37078d7a71e0b21",
75+
"commit": "0f550ab9f5a07ed293926a306e914866164b346b",
7676
"path": "url"
7777
},
7878
"user-timing": {

0 commit comments

Comments
 (0)