From 8a19d5284b6fc70d9e78e09a18c0cfcf2a4c11cb Mon Sep 17 00:00:00 2001 From: mataha Date: Wed, 16 Aug 2023 15:41:38 +0200 Subject: [PATCH] Normalize drive letters when resolving paths on Windows When it comes to resolving paths on Windows, even though the underlying API expects drive letter prefixes to be uppercase, some sources (e.g. environment variables like `=C:`) won't normalize components, instead returning the value as-is. While this wouldn't be a problem normally as NTFS is case-insensitive on Windows, this introduces duplicates in the database when adding new entries via `zoxide add`: ```batchfile prompt > zoxide query --list D:\ d:\ D:\coding d:\coding D:\coding\.cloned d:\coding\.cloned ``` This is a cherry-pick from #567; see also rust-lang/rust-analyzer#14683. Signed-off-by: mataha --- CHANGELOG.md | 1 + src/util.rs | 55 ++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5316048..4cfa465a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - zsh: better cd completions. - elvish: `z -` now work as expected. - Lazily delete excluded directories from the database. +- Normalize drive letters when resolving paths on Windows. ## [0.9.4] - 2024-02-21 diff --git a/src/util.rs b/src/util.rs index 1f8fc95f..cf8aee89 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,7 +1,7 @@ use std::ffi::OsStr; use std::fs::{self, File, OpenOptions}; use std::io::{self, Read, Write}; -use std::path::{Component, Path, PathBuf}; +use std::path::{Component, Path, PathBuf, Prefix}; use std::process::{Child, Command, Stdio}; use std::time::SystemTime; use std::{env, mem}; @@ -263,6 +263,37 @@ pub fn path_to_str(path: &impl AsRef) -> Result<&str> { path.to_str().with_context(|| format!("invalid unicode in path: {}", path.display())) } +fn patch_path(path: PathBuf) -> PathBuf { + if cfg!(windows) { + fn patch_drive(drive_letter: u8) -> char { + drive_letter.to_ascii_uppercase() as char + } + + let mut components = path.components(); + match components.next() { + Some(Component::Prefix(prefix)) => { + let prefix = match prefix.kind() { + Prefix::Disk(drive_letter) => { + format!(r"{}:", patch_drive(drive_letter)) + } + Prefix::VerbatimDisk(drive_letter) => { + format!(r"\\?\{}:", patch_drive(drive_letter)) + } + _ => return path, + }; + + let mut path = PathBuf::default(); + path.push(prefix); + path.extend(components); + path + } + _ => path, + } + } else { + path + } +} + /// Returns the absolute version of a path. Like /// [`std::path::Path::canonicalize`], but doesn't resolve symlinks. pub fn resolve_path(path: impl AsRef) -> Result { @@ -274,8 +305,6 @@ pub fn resolve_path(path: impl AsRef) -> Result { // initialize root if cfg!(windows) { - use std::path::Prefix; - fn get_drive_letter(path: impl AsRef) -> Option { let path = path.as_ref(); let mut components = path.components(); @@ -292,17 +321,17 @@ pub fn resolve_path(path: impl AsRef) -> Result { } fn get_drive_path(drive_letter: u8) -> PathBuf { - format!(r"{}:\", drive_letter as char).into() + format!(r"{}:\", drive_letter.to_ascii_uppercase() as char).into() } fn get_drive_relative(drive_letter: u8) -> Result { let path = current_dir()?; if Some(drive_letter) == get_drive_letter(&path) { - return Ok(path); + return Ok(patch_path(path)); } if let Some(path) = env::var_os(format!("={}:", drive_letter as char)) { - return Ok(path.into()); + return Ok(patch_path(path.into())); } let path = get_drive_path(drive_letter); @@ -312,23 +341,25 @@ pub fn resolve_path(path: impl AsRef) -> Result { match components.peek() { Some(Component::Prefix(prefix)) => match prefix.kind() { Prefix::Disk(drive_letter) => { - let disk = components.next().unwrap(); + components.next(); if components.peek() == Some(&Component::RootDir) { - let root = components.next().unwrap(); - stack.push(disk); - stack.push(root); + components.next(); + base_path = get_drive_path(drive_letter); } else { base_path = get_drive_relative(drive_letter)?; - stack.extend(base_path.components()); } + + stack.extend(base_path.components()); } Prefix::VerbatimDisk(drive_letter) => { components.next(); if components.peek() == Some(&Component::RootDir) { components.next(); + base_path = get_drive_path(drive_letter); + } else { + bail!("illegal path: {}", path.display()); } - base_path = get_drive_path(drive_letter); stack.extend(base_path.components()); } _ => bail!("invalid path: {}", path.display()),