Skip to content

Commit 6053cf3

Browse files
authored
fix(normalize_path): remove current dir components, ensure backslashes on windows, and ensure no trailing slash (#15)
1 parent e8094f1 commit 6053cf3

File tree

4 files changed

+89
-17
lines changed

4 files changed

+89
-17
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ divan = "0.1.21"
2626
members = ["."]
2727

2828
[workspace.dependencies]
29-
sys_traits = "0.1.10"
29+
sys_traits = "0.1.17"
3030

3131
[[bench]]
3232
name = "bench"

benches/bench.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,18 @@ fn main() {
88
divan::main();
99
}
1010

11-
#[divan::bench(sample_size = 512000)]
11+
#[divan::bench(sample_size = 51200)]
1212
fn bench_normalize_path_changed(bencher: divan::Bencher) {
1313
let path = PathBuf::from("/testing/../this/./out/testing/../test");
1414
bencher.bench(|| normalize_path(Cow::Borrowed(&path)))
1515
}
1616

17-
#[divan::bench(sample_size = 512000)]
17+
#[divan::bench(sample_size = 51200)]
1818
fn bench_normalize_path_no_change(bencher: divan::Bencher) {
19-
let path = PathBuf::from("/testing/this/out/testing/test");
19+
let path = if cfg!(windows) {
20+
PathBuf::from("C:\\testing\\this\\out\\testing\\test")
21+
} else {
22+
PathBuf::from("/testing/this/out/testing/test")
23+
};
2024
bencher.bench(|| normalize_path(Cow::Borrowed(&path)))
2125
}

src/lib.rs

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -146,16 +146,67 @@ fn url_to_file_path_wasm(url: &Url) -> Result<PathBuf, ()> {
146146
#[inline]
147147
pub fn normalize_path(path: Cow<Path>) -> Cow<Path> {
148148
fn should_normalize(path: &Path) -> bool {
149+
if path_has_trailing_separator(path) {
150+
return true;
151+
}
152+
149153
for component in path.components() {
150154
match component {
151-
Component::Prefix(..) | Component::CurDir | Component::ParentDir => {
152-
return true
155+
Component::CurDir | Component::ParentDir => {
156+
return true;
153157
}
154-
Component::RootDir | Component::Normal(_) => {
158+
Component::Prefix(..) | Component::RootDir | Component::Normal(_) => {
155159
// ok
156160
}
157161
}
158162
}
163+
164+
path_has_cur_dir_separator(path)
165+
}
166+
167+
fn path_has_trailing_separator(path: &Path) -> bool {
168+
#[cfg(unix)]
169+
let raw = std::os::unix::ffi::OsStrExt::as_bytes(path.as_os_str());
170+
#[cfg(windows)]
171+
let raw = path.as_os_str().as_encoded_bytes();
172+
#[cfg(target_arch = "wasm32")]
173+
let raw = path.to_string_lossy();
174+
#[cfg(target_arch = "wasm32")]
175+
let raw = raw.as_bytes();
176+
177+
if sys_traits::impls::is_windows() {
178+
raw.contains(&b'/') || raw.ends_with(b"\\")
179+
} else {
180+
raw.ends_with(b"/")
181+
}
182+
}
183+
184+
// Rust normalizes away `Component::CurDir` most of the time
185+
// so we need to explicitly check for it in the bytes
186+
fn path_has_cur_dir_separator(path: &Path) -> bool {
187+
#[cfg(unix)]
188+
let raw = std::os::unix::ffi::OsStrExt::as_bytes(path.as_os_str());
189+
#[cfg(windows)]
190+
let raw = path.as_os_str().as_encoded_bytes();
191+
#[cfg(target_arch = "wasm32")]
192+
let raw = path.to_string_lossy();
193+
#[cfg(target_arch = "wasm32")]
194+
let raw = raw.as_bytes();
195+
196+
if sys_traits::impls::is_windows() {
197+
for window in raw.windows(3) {
198+
if matches!(window, [b'\\', b'.', b'\\']) {
199+
return true;
200+
}
201+
}
202+
} else {
203+
for window in raw.windows(3) {
204+
if matches!(window, [b'/', b'.', b'/']) {
205+
return true;
206+
}
207+
}
208+
}
209+
159210
false
160211
}
161212

@@ -603,22 +654,39 @@ mod tests {
603654
}
604655
}
605656

657+
#[test]
658+
fn test_normalize_path_basic() {
659+
let run_test = run_normalize_path_test;
660+
run_test("a/../b", "b");
661+
run_test("a/./b/", &PathBuf::from("a").join("b").to_string_lossy());
662+
run_test(
663+
"a/./b/../c",
664+
&PathBuf::from("a").join("c").to_string_lossy(),
665+
);
666+
}
667+
606668
#[cfg(windows)]
607669
#[test]
608-
fn test_normalize_path() {
609-
use super::*;
670+
fn test_normalize_path_win() {
671+
let run_test = run_normalize_path_test;
610672

611673
run_test("C:\\test\\file.txt", "C:\\test\\file.txt");
612674
run_test("C:\\test\\./file.txt", "C:\\test\\file.txt");
613675
run_test("C:\\test\\../other/file.txt", "C:\\other\\file.txt");
614676
run_test("C:\\test\\../other\\file.txt", "C:\\other\\file.txt");
677+
run_test(
678+
"C:\\test\\removes_trailing_slash\\",
679+
"C:\\test\\removes_trailing_slash",
680+
);
681+
run_test("C:\\a\\.\\b\\..\\c", "C:\\a\\c");
682+
}
615683

616-
fn run_test(input: &str, expected: &str) {
617-
assert_eq!(
618-
normalize_path(Cow::Owned(PathBuf::from(input))),
619-
PathBuf::from(expected)
620-
);
621-
}
684+
#[track_caller]
685+
fn run_normalize_path_test(input: &str, expected: &str) {
686+
assert_eq!(
687+
normalize_path(Cow::Owned(PathBuf::from(input))).to_string_lossy(),
688+
expected
689+
);
622690
}
623691

624692
#[test]

0 commit comments

Comments
 (0)