Skip to content

Commit f122364

Browse files
committed
Fix mutating path of URL without authority (idempotency, empty path segments)
1 parent 8ff69ae commit f122364

File tree

4 files changed

+103
-6
lines changed

4 files changed

+103
-6
lines changed

url/src/lib.rs

+11-2
Original file line numberDiff line numberDiff line change
@@ -2129,7 +2129,7 @@ impl Url {
21292129
} else {
21302130
self.host_end
21312131
};
2132-
let suffix = self.slice(old_suffix_pos..).to_owned();
2132+
let mut suffix = self.slice(old_suffix_pos..).to_owned();
21332133
self.serialization.truncate(self.host_start as usize);
21342134
if !self.has_authority() {
21352135
debug_assert!(self.slice(self.scheme_end..self.host_start) == ":");
@@ -2149,7 +2149,16 @@ impl Url {
21492149
write!(&mut self.serialization, ":{}", port).unwrap();
21502150
}
21512151
}
2152-
let new_suffix_pos = to_u32(self.serialization.len()).unwrap();
2152+
let mut new_suffix_pos = to_u32(self.serialization.len()).unwrap();
2153+
2154+
// Remove starting "/." for empty path segment followed by the host
2155+
if suffix.starts_with("/.//") {
2156+
let adjustment: usize = "/.".len();
2157+
suffix.drain(..adjustment);
2158+
// pathname should be "//p" not "p" given that the first segment was empty
2159+
new_suffix_pos -= adjustment as u32;
2160+
}
2161+
21532162
self.serialization.push_str(&suffix);
21542163

21552164
let adjust = |index: &mut u32| {

url/src/path_segments.rs

+40-2
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ impl<'a> PathSegmentsMut<'a> {
239239
I::Item: AsRef<str>,
240240
{
241241
let scheme_type = SchemeType::from(self.url.scheme());
242-
let path_start = self.url.path_start as usize;
242+
let mut path_start = self.url.path_start as usize;
243243
self.url.mutate(|parser| {
244244
parser.context = parser::Context::PathSegmentSetter;
245245
for segment in segments {
@@ -253,7 +253,44 @@ impl<'a> PathSegmentsMut<'a> {
253253
{
254254
parser.serialization.push('/');
255255
}
256-
let mut has_host = true; // FIXME account for this?
256+
257+
let mut path_empty = false;
258+
259+
// Check ':' and then see if the next character is '/'
260+
let mut has_host = if let Some(index) = parser.serialization.find(":") {
261+
if parser.serialization.len() > index + 1
262+
&& parser.serialization.as_bytes().get(index + 1) == Some(&b'/')
263+
{
264+
let rest = &parser.serialization[(index + ":/".len())..];
265+
let host_part = rest.split('/').next().unwrap_or("");
266+
path_empty = rest.is_empty();
267+
!host_part.is_empty() && !host_part.contains('@')
268+
} else {
269+
false
270+
}
271+
} else {
272+
false
273+
};
274+
275+
// For cases where normalization is applied across both the serialization and the path.
276+
// Append "/." immediately after the scheme (up to ":")
277+
// This is done if three conditions are met.
278+
// https://url.spec.whatwg.org/#url-serializing
279+
// 1. The host is null
280+
// 2. The url's path length is greater than 1
281+
// 3. the first segment of the URL's path is an empty string
282+
if !has_host && segment.len() > 1 && path_empty {
283+
if let Some(index) = parser.serialization.find(":") {
284+
if parser.serialization.len() == index + 2
285+
&& parser.serialization.as_bytes().get(index + 1) == Some(&b'/')
286+
{
287+
// Append an extra '/' to ensure that "/./path" becomes "/.//path"
288+
parser.serialization.insert_str(index + ":".len(), "/./");
289+
path_start += "/.".len();
290+
}
291+
}
292+
}
293+
257294
parser.parse_path(
258295
scheme_type,
259296
&mut has_host,
@@ -262,6 +299,7 @@ impl<'a> PathSegmentsMut<'a> {
262299
);
263300
}
264301
});
302+
self.url.path_start = path_start as u32;
265303
self
266304
}
267305
}

url/tests/expected_failures.txt

-2
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@
3636
<file:/.//p>
3737
<http://example.net/path> set hostname to <example.com:8080>
3838
<http://example.net:8080/path> set hostname to <example.com:>
39-
<non-spec:/.//p> set hostname to <h>
40-
<non-spec:/.//p> set hostname to <>
4139
<foo:///some/path> set pathname to <>
4240
<file:///var/log/system.log> set href to <http://0300.168.0xF0>
4341
<file://monkey/> set pathname to <\\\\>

url/tests/unit.rs

+52
Original file line numberDiff line numberDiff line change
@@ -1419,3 +1419,55 @@ fn test_fuzzing_uri_failures() {
14191419
assert_eq!(url.as_str(), "web+demo:////.dummy.path");
14201420
url.check_invariants().unwrap();
14211421
}
1422+
1423+
#[test]
1424+
fn test_can_be_a_base_with_set_path() {
1425+
use url::quirks;
1426+
let mut url = Url::parse("web+demo:/").unwrap();
1427+
assert!(!url.cannot_be_a_base());
1428+
1429+
url.set_path("//not-a-host");
1430+
assert_eq!(url.path(), "//not-a-host");
1431+
1432+
let segments: Vec<_> = url
1433+
.path_segments()
1434+
.expect("should have path segments")
1435+
.collect();
1436+
1437+
assert_eq!(segments, vec!["", "not-a-host"]);
1438+
1439+
url.set_query(Some("query"));
1440+
url.set_fragment(Some("frag"));
1441+
1442+
assert_eq!(url.as_str(), "web+demo:/.//not-a-host?query#frag");
1443+
quirks::set_hostname(&mut url, "test").unwrap();
1444+
assert_eq!(url.as_str(), "web+demo://test//not-a-host?query#frag");
1445+
url.check_invariants().unwrap();
1446+
quirks::set_hostname(&mut url, "").unwrap();
1447+
assert_eq!(url.as_str(), "web+demo:////not-a-host?query#frag");
1448+
url.check_invariants().unwrap();
1449+
}
1450+
1451+
#[test]
1452+
fn test_can_be_a_base_with_path_segments_mut() {
1453+
let mut url = Url::parse("web+demo:/").unwrap();
1454+
assert!(!url.cannot_be_a_base());
1455+
1456+
url.path_segments_mut()
1457+
.expect("should have path segments")
1458+
.push("")
1459+
.push("not-a-host");
1460+
1461+
url.set_query(Some("query"));
1462+
url.set_fragment(Some("frag"));
1463+
1464+
assert_eq!(url.as_str(), "web+demo:/.//not-a-host?query#frag");
1465+
assert_eq!(url.path(), "//not-a-host");
1466+
url.check_invariants().unwrap();
1467+
1468+
let segments: Vec<_> = url
1469+
.path_segments()
1470+
.expect("should have path segments")
1471+
.collect();
1472+
assert_eq!(segments, vec!["", "not-a-host"]);
1473+
}

0 commit comments

Comments
 (0)