From ee5ba87c2f5a06c327b75e4bd26282c946808017 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/cmd/add.rs | 9 +++++---- src/util.rs | 51 ++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 47 insertions(+), 14 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/cmd/add.rs b/src/cmd/add.rs index 945bbe5d..4490ca9c 100644 --- a/src/cmd/add.rs +++ b/src/cmd/add.rs @@ -19,10 +19,11 @@ impl Run for Add { let mut db = Database::open()?; for path in &self.paths { - let path = - if config::resolve_symlinks() { util::canonicalize } else { util::resolve_path }( - path, - )?; + let path = util::patch_path(if config::resolve_symlinks() { + util::canonicalize + } else { + util::resolve_path + }(path)?); let path = util::path_to_str(&path)?; // Ignore path if it contains unsupported characters, or if it's in the exclude diff --git a/src/util.rs b/src/util.rs index 1f8fc95f..85c7cd84 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())) } +pub 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,7 +321,7 @@ 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 { @@ -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()),