From d3ba97a2360ab40cb622c0c054022f148643bfdf Mon Sep 17 00:00:00 2001 From: konstin Date: Mon, 24 Aug 2026 15:53:14 +0200 Subject: [PATCH 1/2] Only pop drive letters at the start of a path Disclosure: I found this with automated scanning for crashes in uv. It's one line fix (plus comments and tests), so I'm putting it up anyway. Currently, in `file:///a/c:` the `c:` is erroneously considered a windows drive letter that we have to retain, so `file:///a/c:` remains instead of becoming `file:///a/`, subsequently failing the debug assertion at https://github.com/servo/rust-url/blob/00a6ce58d02f4e0d43c5ca0702c0bedb8b1ebf3a/url/src/parser.rs#L1301. The additional check ensure that only `file:///c:` can be a path. --- url/src/parser.rs | 5 +++++ url/tests/unit.rs | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/url/src/parser.rs b/url/src/parser.rs index 6ccfa9b46..244c9e4d0 100644 --- a/url/src/parser.rs +++ b/url/src/parser.rs @@ -1391,17 +1391,22 @@ impl Parser<'_> { /// https://url.spec.whatwg.org/#pop-a-urls-path fn pop_path(&mut self, scheme_type: SchemeType, path_start: usize) { + std::dbg!(&self.serialization); if self.serialization.len() > path_start { let slash_position = self.serialization[path_start..].rfind('/').unwrap(); // + 1 since rfind returns the position before the slash. let segment_start = path_start + slash_position + 1; // Don’t pop a Windows drive letter if !(scheme_type.is_file() + // It's only a drive letter if it's at the beginning of a path, + // e.g. in `/a/c:` the `c:` isn't one because it's not at the start of a path. + && segment_start == path_start + 1 && is_normalized_windows_drive_letter(&self.serialization[segment_start..])) { self.serialization.truncate(segment_start); } } + std::dbg!(&self.serialization); } pub fn parse_cannot_be_a_base_path<'i>(&mut self, mut input: Input<'i>) -> Input<'i> { diff --git a/url/tests/unit.rs b/url/tests/unit.rs index faeb7c5f4..8702302ca 100644 --- a/url/tests/unit.rs +++ b/url/tests/unit.rs @@ -1351,6 +1351,25 @@ fn test_file_with_drive_and_path() { assert_eq!(url2.to_string(), "file:///p:/a"); } +#[test] +fn test_file_base_drive_shaped_segment() { + let testcases = [ + ("file:a/c:", "..", "file:///"), + ( + "file:a/c:", + "../example-1.0-py3-none-any.whl", + "file:///example-1.0-py3-none-any.whl", + ), + ("file:///w:/c:", "file:..", "file:///w:/"), + ("file:///c:/..", "", "file:///c:/"), + ]; + + for (base, reference, expected) in testcases { + let base = Url::parse(base).unwrap(); + assert_eq!(base.join(reference).unwrap().as_str(), expected); + } +} + #[cfg(feature = "std")] #[test] fn issue_864() { From 8ca0c53bf42e254f8560b0ecef3c570f1eb3cda6 Mon Sep 17 00:00:00 2001 From: konstin Date: Mon, 24 Aug 2026 16:30:23 +0200 Subject: [PATCH 2/2] Remove dbg! --- url/src/parser.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/url/src/parser.rs b/url/src/parser.rs index 244c9e4d0..589b597d0 100644 --- a/url/src/parser.rs +++ b/url/src/parser.rs @@ -1391,7 +1391,6 @@ impl Parser<'_> { /// https://url.spec.whatwg.org/#pop-a-urls-path fn pop_path(&mut self, scheme_type: SchemeType, path_start: usize) { - std::dbg!(&self.serialization); if self.serialization.len() > path_start { let slash_position = self.serialization[path_start..].rfind('/').unwrap(); // + 1 since rfind returns the position before the slash. @@ -1406,7 +1405,6 @@ impl Parser<'_> { self.serialization.truncate(segment_start); } } - std::dbg!(&self.serialization); } pub fn parse_cannot_be_a_base_path<'i>(&mut self, mut input: Input<'i>) -> Input<'i> {