Skip to content

Commit fd279ca

Browse files
committed
fix: ensure consistent normalization of UNC paths with trailing separators and \\
1 parent 7c1aec1 commit fd279ca

File tree

4 files changed

+70
-19
lines changed

4 files changed

+70
-19
lines changed

src/impl_sugar_path.rs

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ type StrVec<'a> = SmallVec<[&'a str; 8]>;
1717
impl SugarPath for Path {
1818
fn normalize(&self) -> PathBuf {
1919
let peekable = self.components().peekable();
20-
let mut components = to_normalized_components(peekable);
20+
let components = to_normalized_components(peekable);
2121

22-
normalize_inner(&mut components)
22+
normalize_inner(&components)
2323
}
2424

2525
fn absolutize(&self) -> PathBuf {
@@ -51,8 +51,8 @@ impl SugarPath for Path {
5151
// a UNC path at this points, because UNC paths are always absolute.
5252
let mut components: ComponentVec = components.collect();
5353
components.insert(1, Component::RootDir);
54-
let mut components = to_normalized_components(components.into_iter().peekable());
55-
normalize_inner(&mut components)
54+
let components = to_normalized_components(components.into_iter().peekable());
55+
normalize_inner(&components)
5656
} else {
5757
base.to_mut().push(self);
5858
base.normalize()
@@ -167,19 +167,77 @@ impl SugarPath for Path {
167167
}
168168

169169
#[inline]
170-
fn normalize_inner(components: &mut ComponentVec) -> PathBuf {
170+
fn normalize_inner(components: &ComponentVec) -> PathBuf {
171171
if components.is_empty() {
172172
return PathBuf::from(".");
173173
}
174174

175-
if cfg!(target_family = "windows")
176-
&& components.len() == 1
177-
&& matches!(components[0], Component::Prefix(_))
178-
{
179-
components.push(Component::CurDir)
175+
let sep = std::path::MAIN_SEPARATOR_STR;
176+
let mut result = std::ffi::OsString::with_capacity(
177+
components.iter().map(|c| c.as_os_str().len()).sum::<usize>() + components.len(),
178+
);
179+
let mut need_sep = false;
180+
181+
for c in components.iter() {
182+
match c {
183+
#[cfg(target_family = "windows")]
184+
Component::Prefix(p) => {
185+
if let std::path::Prefix::UNC(server, share) = p.kind() {
186+
// Reconstruct UNC prefix from structured data to ensure
187+
// correct backslash separators regardless of input format.
188+
result.push("\\\\");
189+
result.push(server);
190+
result.push("\\");
191+
result.push(share);
192+
} else {
193+
result.push(p.as_os_str());
194+
}
195+
need_sep = false;
196+
}
197+
#[cfg(not(target_family = "windows"))]
198+
Component::Prefix(_) => unreachable!(),
199+
Component::RootDir => {
200+
result.push(sep);
201+
need_sep = false;
202+
}
203+
Component::Normal(s) => {
204+
if need_sep {
205+
result.push(sep);
206+
}
207+
result.push(s);
208+
need_sep = true;
209+
}
210+
Component::ParentDir => {
211+
if need_sep {
212+
result.push(sep);
213+
}
214+
result.push("..");
215+
need_sep = true;
216+
}
217+
Component::CurDir => {
218+
if need_sep {
219+
result.push(sep);
220+
}
221+
result.push(".");
222+
need_sep = true;
223+
}
224+
}
225+
}
226+
227+
// Prefix-only: append trailing separator or CurDir.
228+
// e.g. \\server\share → \\server\share\, C: → C:.
229+
#[cfg(target_family = "windows")]
230+
if components.len() == 1 {
231+
if let Some(Component::Prefix(p)) = components.first() {
232+
if matches!(p.kind(), std::path::Prefix::UNC(_, _)) {
233+
result.push("\\");
234+
} else {
235+
result.push(".");
236+
}
237+
}
180238
}
181239

182-
components.iter().collect()
240+
PathBuf::from(result)
183241
}
184242

185243
impl<T: Deref<Target = str>> SugarPath for T {

tests/absolutize_with.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,6 @@ fn windows_absolutize_with_unc_paths() {
9090
"..\\other".absolutize_with("\\\\server\\share\\folder"),
9191
"\\\\server\\share\\other"
9292
);
93-
// TODO: should be "\\\\other\\share" — normalize() on a UNC root produces
94-
// [Prefix, RootDir] which reconstructs with a trailing `\`.
9593
assert_eq_str!("\\\\other\\share".absolutize_with("\\\\server\\share"), "\\\\other\\share\\");
9694
}
9795

tests/normalize.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,7 @@ fn windows() {
4040
assert_eq_str!(p!("/foo/../../../bar").normalize(), "\\bar");
4141
assert_eq_str!(p!("a//b//../b").normalize(), "a\\b");
4242
assert_eq_str!(p!("a//b//./c").normalize(), "a\\b\\c");
43-
// TODO: should be "\\\\server\\share\\dir\\file.ext" — the UNC prefix preserves
44-
// original forward slashes from the input instead of normalizing to backslashes.
45-
assert_eq_str!(p!("//server/share/dir/file.ext").normalize(), "//server/share\\dir\\file.ext");
43+
assert_eq_str!(p!("//server/share/dir/file.ext").normalize(), "\\\\server\\share\\dir\\file.ext");
4644
assert_eq_str!(p!("/foo/../../../bar").normalize(), "\\bar");
4745
assert_eq_str!(p!("/a/b/c/../../../x/y/z").normalize(), "\\x\\y\\z");
4846
assert_eq_str!(p!("C:").normalize(), "C:.");

tests/relative.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,6 @@ fn windows_unc() {
7373
let cases = [
7474
("\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz"),
7575
("\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz"),
76-
// TODO: should be "\\\\foo\\baz" — relative() for cross-root UNC paths
77-
// returns the absolute target via normalize(), which produces [Prefix, RootDir]
78-
// and reconstructs with a trailing `\`.
7976
("\\\\foo\\baz-quux", "\\\\foo\\baz", "\\\\foo\\baz\\"),
8077
("\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux"),
8178
("\\\\foo\\bar\\baz", "\\\\foo\\bar", ".."),

0 commit comments

Comments
 (0)