From 249e5dbbf42a25bdb7d6eff39cb1409ca9de4f96 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Wed, 29 Oct 2025 13:03:59 -0400 Subject: [PATCH 1/6] fix(windows): normalize_path - strip trailing spaces and dots --- Cargo.toml | 2 +- rust-toolchain.toml | 2 +- src/lib.rs | 48 ++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cf2812c..8393fd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "deno_path_util" description = "Path utilities used in Deno" version = "0.6.3" -edition = "2021" +edition = "2024" authors = ["the Deno authors"] license = "MIT" repository = "https://github.com/denoland/deno_path_util" diff --git a/rust-toolchain.toml b/rust-toolchain.toml index d092944..43e5784 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.86.0" +channel = "1.90.0" components = ["rustfmt", "clippy"] diff --git a/src/lib.rs b/src/lib.rs index e3ad041..4f1d277 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ use std::path::Component; use std::path::Path; use std::path::PathBuf; use sys_traits::SystemRandom; +use sys_traits::impls::is_windows; use thiserror::Error; use url::Url; @@ -150,14 +151,27 @@ pub fn normalize_path(path: Cow) -> Cow { return true; } + let mut last_part = None; for component in path.components() { match component { Component::CurDir | Component::ParentDir => { return true; } - Component::Prefix(..) | Component::RootDir | Component::Normal(_) => { + Component::Prefix(..) | Component::RootDir => { // ok } + Component::Normal(component) => { + last_part = Some(component); + } + } + } + + if is_windows() + && let Some(last_part) = last_part + { + let bytes = last_part.as_encoded_bytes(); + if bytes.ends_with(b".") || bytes.ends_with(b" ") { + return true; } } @@ -235,7 +249,33 @@ pub fn normalize_path(path: Cow) -> Cow { ret.pop(); } Component::Normal(c) => { - ret.push(c); + if is_windows() { + let bytes = c.as_encoded_bytes(); + // Strip trailing dots and spaces on Windows + let mut end = bytes.len(); + while end > 0 && (bytes[end - 1] == b'.' || bytes[end - 1] == b' ') { + end -= 1; + } + if end == bytes.len() { + ret.push(c); + } else if end > 0 { + #[cfg(windows)] + { + use std::os::windows::ffi::{OsStrExt, OsStringExt}; + let wide: Vec = c.encode_wide().collect(); + let trimmed = std::ffi::OsString::from_wide(&wide[..end]); + ret.push(trimmed); + } + #[cfg(not(windows))] + { + let trimmed = + std::ffi::OsStr::from_encoded_bytes_unchecked(&bytes[..end]); + ret.push(trimmed); + } + } + } else { + ret.push(c); + } } } } @@ -738,6 +778,8 @@ mod tests { ); run_test("C:\\a\\.\\b\\..\\c", "C:\\a\\c"); run_test("C:\\test\\.", "C:\\test"); + run_test("C:\\test\\test...", "C:\\test\\test"); + run_test("C:\\test\\test ", "C:\\test\\test"); } #[track_caller] @@ -897,8 +939,8 @@ mod tests { #[test] fn test_resolve_url_or_path_deprecated_error() { - use url::ParseError::*; use ResolveUrlOrPathError::*; + use url::ParseError::*; let mut tests = vec![ ("https://eggplant:b/c", UrlParse(InvalidPort)), From b284d47f08a08cfed367903169c50bae955f0f34 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Wed, 29 Oct 2025 13:04:26 -0400 Subject: [PATCH 2/6] format --- src/fs.rs | 4 ++-- src/lib.rs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/fs.rs b/src/fs.rs index 2a23307..edcca89 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -174,14 +174,14 @@ mod test { use std::path::Path; use std::path::PathBuf; - use sys_traits::impls::InMemorySys; - use sys_traits::impls::RealSys; use sys_traits::EnvCurrentDir; use sys_traits::EnvSetCurrentDir; use sys_traits::FsCanonicalize; use sys_traits::FsCreateDirAll; use sys_traits::FsRead; use sys_traits::FsSymlinkDir; + use sys_traits::impls::InMemorySys; + use sys_traits::impls::RealSys; use super::atomic_write_file_with_retries; use super::canonicalize_path_maybe_not_exists; diff --git a/src/lib.rs b/src/lib.rs index 4f1d277..c5fc81e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -253,7 +253,8 @@ pub fn normalize_path(path: Cow) -> Cow { let bytes = c.as_encoded_bytes(); // Strip trailing dots and spaces on Windows let mut end = bytes.len(); - while end > 0 && (bytes[end - 1] == b'.' || bytes[end - 1] == b' ') { + while end > 0 && (bytes[end - 1] == b'.' || bytes[end - 1] == b' ') + { end -= 1; } if end == bytes.len() { From fb1698c0d0e0577bd6bc23e55567115a8cb94cb4 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Wed, 29 Oct 2025 13:06:24 -0400 Subject: [PATCH 3/6] maybe fix linux ci --- src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index c5fc81e..4f2e160 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -267,8 +267,9 @@ pub fn normalize_path(path: Cow) -> Cow { let trimmed = std::ffi::OsString::from_wide(&wide[..end]); ret.push(trimmed); } + /// SAFETY: trimmed spaces and dots only #[cfg(not(windows))] - { + unsafe { let trimmed = std::ffi::OsStr::from_encoded_bytes_unchecked(&bytes[..end]); ret.push(trimmed); From 6dc578bcc92ab5f1d58171778845b5a95b736e24 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Wed, 29 Oct 2025 13:07:20 -0400 Subject: [PATCH 4/6] maybe fix ci --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 4f2e160..974dc56 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -267,7 +267,7 @@ pub fn normalize_path(path: Cow) -> Cow { let trimmed = std::ffi::OsString::from_wide(&wide[..end]); ret.push(trimmed); } - /// SAFETY: trimmed spaces and dots only + // SAFETY: trimmed spaces and dots only #[cfg(not(windows))] unsafe { let trimmed = From 4127c7a4a58a4e380c43f9a7e8e36cb3715d01d7 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Wed, 29 Oct 2025 13:09:47 -0400 Subject: [PATCH 5/6] clippy --- src/lib.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 974dc56..a23d0c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -341,20 +341,17 @@ fn url_from_file_path_wasm(path: &Path) -> Result { if path_str.contains('\\') { let mut url = Url::parse("file://").unwrap(); if let Some(next) = path_str.strip_prefix(r#"\\?\UNC\"#) { - if let Some((host, rest)) = next.split_once('\\') { - if url.set_host(Some(host)).is_ok() { + if let Some((host, rest)) = next.split_once('\\') + && url.set_host(Some(host)).is_ok() { path_str = rest.to_string().into(); } - } } else if let Some(next) = path_str.strip_prefix(r#"\\?\"#) { path_str = next.to_string().into(); - } else if let Some(next) = path_str.strip_prefix(r#"\\"#) { - if let Some((host, rest)) = next.split_once('\\') { - if url.set_host(Some(host)).is_ok() { + } else if let Some(next) = path_str.strip_prefix(r#"\\"#) + && let Some((host, rest)) = next.split_once('\\') + && url.set_host(Some(host)).is_ok() { path_str = rest.to_string().into(); } - } - } for component in path_str.split('\\') { url.path_segments_mut().unwrap().push(component); From 04dfe62e3961b48b741016f89cd264c5fed5a010 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Wed, 29 Oct 2025 13:13:37 -0400 Subject: [PATCH 6/6] sigh --- src/lib.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a23d0c3..992223d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -342,16 +342,18 @@ fn url_from_file_path_wasm(path: &Path) -> Result { let mut url = Url::parse("file://").unwrap(); if let Some(next) = path_str.strip_prefix(r#"\\?\UNC\"#) { if let Some((host, rest)) = next.split_once('\\') - && url.set_host(Some(host)).is_ok() { - path_str = rest.to_string().into(); - } + && url.set_host(Some(host)).is_ok() + { + path_str = rest.to_string().into(); + } } else if let Some(next) = path_str.strip_prefix(r#"\\?\"#) { path_str = next.to_string().into(); } else if let Some(next) = path_str.strip_prefix(r#"\\"#) && let Some((host, rest)) = next.split_once('\\') - && url.set_host(Some(host)).is_ok() { - path_str = rest.to_string().into(); - } + && url.set_host(Some(host)).is_ok() + { + path_str = rest.to_string().into(); + } for component in path_str.split('\\') { url.path_segments_mut().unwrap().push(component);