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/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 e3ad041..992223d 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,35 @@ 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); + } + // SAFETY: trimmed spaces and dots only + #[cfg(not(windows))] + unsafe { + let trimmed = + std::ffi::OsStr::from_encoded_bytes_unchecked(&bytes[..end]); + ret.push(trimmed); + } + } + } else { + ret.push(c); + } } } } @@ -299,19 +341,18 @@ 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() { - path_str = rest.to_string().into(); - } + 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() { - path_str = rest.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(); } for component in path_str.split('\\') { @@ -738,6 +779,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 +940,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)),