Skip to content

Commit a2ddae3

Browse files
authored
fix: Prevent URLSearchParams.toString() from returning unencoded params when not modified (#292)
* fix: prevent URLSearchParams.toString() from returning unencoded params when not modified * refactor: address pr review feedback
1 parent 19f6f35 commit a2ddae3

File tree

4 files changed

+40
-17
lines changed

4 files changed

+40
-17
lines changed

crates/rust-url/src/lib.rs

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#![allow(clippy::missing_safety_doc)]
22

33
/// Wrapper for the Url crate and a URLSearchParams implementation that enables use from C++.
4+
use std::cell::RefCell;
45
use std::marker::PhantomData;
56
use std::slice;
67
use url::{form_urlencoded, quirks, Url};
@@ -14,12 +15,22 @@ impl JSUrl {
1415
fn update_params(&self) {
1516
if let Some(params) = unsafe { self.params.as_mut() } {
1617
params.list = self.url.query_pairs().into_owned().collect();
18+
if let UrlOrString::Url {
19+
ref serialized_query_cache,
20+
..
21+
} = params.url_or_str
22+
{
23+
*serialized_query_cache.borrow_mut() = None;
24+
}
1725
}
1826
}
1927
}
2028

2129
enum UrlOrString {
22-
Url(*mut JSUrl),
30+
Url {
31+
url: *mut JSUrl,
32+
serialized_query_cache: RefCell<Option<String>>,
33+
},
2334
Str(String),
2435
}
2536

@@ -36,7 +47,10 @@ impl JSUrlSearchParams {
3647
/// This is used in `params_to_string` to hand out a stable reference.
3748
fn update_url_or_str(&mut self) {
3849
match self.url_or_str {
39-
UrlOrString::Url(url) => {
50+
UrlOrString::Url {
51+
url,
52+
ref serialized_query_cache,
53+
} => {
4054
let url = unsafe { url.as_mut().unwrap() };
4155
if self.list.is_empty() {
4256
url.url.set_query(None);
@@ -45,6 +59,7 @@ impl JSUrlSearchParams {
4559
pairs.clear();
4660
pairs.extend_pairs(self.list.iter());
4761
}
62+
*serialized_query_cache.borrow_mut() = None;
4863
}
4964
UrlOrString::Str(_) => {
5065
let str = if self.list.is_empty() {
@@ -222,7 +237,10 @@ pub unsafe extern "C" fn url_search_params(url: *mut JSUrl) -> *mut JSUrlSearchP
222237
if url.params.is_null() {
223238
url.params = Box::into_raw(Box::new(JSUrlSearchParams {
224239
list: url.url.query_pairs().into_owned().collect(),
225-
url_or_str: UrlOrString::Url(url),
240+
url_or_str: UrlOrString::Url {
241+
url,
242+
serialized_query_cache: RefCell::new(None),
243+
},
226244
}));
227245
}
228246
url.params
@@ -401,12 +419,17 @@ pub extern "C" fn params_sort(params: &mut JSUrlSearchParams) {
401419
#[no_mangle]
402420
pub extern "C" fn params_to_string(params: &JSUrlSearchParams) -> SpecSlice<'_> {
403421
match &params.url_or_str {
404-
UrlOrString::Url(url) => {
405-
let url = unsafe { url.as_mut().unwrap() };
406-
match url.url.query() {
407-
Some(query) => query.into(),
408-
None => "".into(),
409-
}
422+
UrlOrString::Url {
423+
serialized_query_cache,
424+
..
425+
} => {
426+
let mut cache = serialized_query_cache.borrow_mut();
427+
let query = cache.get_or_insert_with(|| {
428+
form_urlencoded::Serializer::new(String::new())
429+
.extend_pairs(&params.list)
430+
.finish()
431+
});
432+
SpecSlice::new(query.as_ptr(), query.len())
410433
}
411434
UrlOrString::Str(str) => str.as_str().into(),
412435
}

tests/wpt-harness/expectations/url/url-constructor.any.js.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1461,7 +1461,7 @@
14611461
"status": "PASS"
14621462
},
14631463
"Parsing: <??a=b&c=d> against <http://example.org/foo/bar>": {
1464-
"status": "FAIL"
1464+
"status": "PASS"
14651465
},
14661466
"Parsing: <http:> against <http://example.org/foo/bar>": {
14671467
"status": "PASS"
@@ -1473,19 +1473,19 @@
14731473
"status": "PASS"
14741474
},
14751475
"Parsing: <http://foo.bar/baz?qux#foo\bbar> without base": {
1476-
"status": "FAIL"
1476+
"status": "PASS"
14771477
},
14781478
"Parsing: <http://foo.bar/baz?qux#foo\"bar> without base": {
1479-
"status": "FAIL"
1479+
"status": "PASS"
14801480
},
14811481
"Parsing: <http://foo.bar/baz?qux#foo<bar> without base": {
1482-
"status": "FAIL"
1482+
"status": "PASS"
14831483
},
14841484
"Parsing: <http://foo.bar/baz?qux#foo>bar> without base": {
1485-
"status": "FAIL"
1485+
"status": "PASS"
14861486
},
14871487
"Parsing: <http://foo.bar/baz?qux#foo`bar> without base": {
1488-
"status": "FAIL"
1488+
"status": "PASS"
14891489
},
14901490
"Parsing: <http://1.2.3.4/> against <http://other.com/>": {
14911491
"status": "PASS"

tests/wpt-harness/expectations/url/url-searchparams.any.js.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@
99
"status": "PASS"
1010
},
1111
"URL.searchParams and URL.search setters, update propagation": {
12-
"status": "FAIL"
12+
"status": "PASS"
1313
}
1414
}

tests/wpt-harness/expectations/url/urlsearchparams-stringifier.any.js.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"status": "PASS"
3737
},
3838
"URLSearchParams connected to URL": {
39-
"status": "FAIL"
39+
"status": "PASS"
4040
},
4141
"URLSearchParams must not do newline normalization": {
4242
"status": "PASS"

0 commit comments

Comments
 (0)