Skip to content

Commit 34f1b22

Browse files
committed
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 <[email protected]>
1 parent dbe6f18 commit 34f1b22

File tree

2 files changed

+45
-11
lines changed

2 files changed

+45
-11
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
- zsh: better cd completions.
1919
- elvish: `z -` now work as expected.
2020
- Lazily delete excluded directories from the database.
21+
- Normalize drive letters when resolving paths on Windows.
2122

2223
## [0.9.4] - 2024-02-21
2324

src/util.rs

+44-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use std::ffi::OsStr;
22
use std::fs::{self, File, OpenOptions};
33
use std::io::{self, Read, Write};
4+
#[cfg(windows)]
5+
use std::path::Prefix;
46
use std::path::{Component, Path, PathBuf};
57
use std::process::{Child, Command, Stdio};
68
use std::time::SystemTime;
@@ -263,6 +265,37 @@ pub fn path_to_str(path: &impl AsRef<Path>) -> Result<&str> {
263265
path.to_str().with_context(|| format!("invalid unicode in path: {}", path.display()))
264266
}
265267

268+
fn patch_path(path: PathBuf) -> PathBuf {
269+
if cfg!(windows) {
270+
fn patch_drive(drive_letter: u8) -> char {
271+
drive_letter.to_ascii_uppercase() as char
272+
}
273+
274+
let mut components = path.components();
275+
match components.next() {
276+
Some(Component::Prefix(prefix)) => {
277+
let prefix = match prefix.kind() {
278+
Prefix::Disk(drive_letter) => {
279+
format!(r"{}:", patch_drive(drive_letter))
280+
}
281+
Prefix::VerbatimDisk(drive_letter) => {
282+
format!(r"\\?\{}:", patch_drive(drive_letter))
283+
}
284+
_ => return path,
285+
};
286+
287+
let mut path = PathBuf::default();
288+
path.push(prefix);
289+
path.extend(components);
290+
path
291+
}
292+
_ => path,
293+
}
294+
} else {
295+
path
296+
}
297+
}
298+
266299
/// Returns the absolute version of a path. Like
267300
/// [`std::path::Path::canonicalize`], but doesn't resolve symlinks.
268301
pub fn resolve_path(path: impl AsRef<Path>) -> Result<PathBuf> {
@@ -274,8 +307,6 @@ pub fn resolve_path(path: impl AsRef<Path>) -> Result<PathBuf> {
274307

275308
// initialize root
276309
if cfg!(windows) {
277-
use std::path::Prefix;
278-
279310
fn get_drive_letter(path: impl AsRef<Path>) -> Option<u8> {
280311
let path = path.as_ref();
281312
let mut components = path.components();
@@ -292,17 +323,17 @@ pub fn resolve_path(path: impl AsRef<Path>) -> Result<PathBuf> {
292323
}
293324

294325
fn get_drive_path(drive_letter: u8) -> PathBuf {
295-
format!(r"{}:\", drive_letter as char).into()
326+
format!(r"{}:\", drive_letter.to_ascii_uppercase() as char).into()
296327
}
297328

298329
fn get_drive_relative(drive_letter: u8) -> Result<PathBuf> {
299330
let path = current_dir()?;
300331
if Some(drive_letter) == get_drive_letter(&path) {
301-
return Ok(path);
332+
return Ok(patch_path(path));
302333
}
303334

304335
if let Some(path) = env::var_os(format!("={}:", drive_letter as char)) {
305-
return Ok(path.into());
336+
return Ok(patch_path(path.into()));
306337
}
307338

308339
let path = get_drive_path(drive_letter);
@@ -312,23 +343,25 @@ pub fn resolve_path(path: impl AsRef<Path>) -> Result<PathBuf> {
312343
match components.peek() {
313344
Some(Component::Prefix(prefix)) => match prefix.kind() {
314345
Prefix::Disk(drive_letter) => {
315-
let disk = components.next().unwrap();
346+
components.next();
316347
if components.peek() == Some(&Component::RootDir) {
317-
let root = components.next().unwrap();
318-
stack.push(disk);
319-
stack.push(root);
348+
components.next();
349+
base_path = get_drive_path(drive_letter);
320350
} else {
321351
base_path = get_drive_relative(drive_letter)?;
322-
stack.extend(base_path.components());
323352
}
353+
354+
stack.extend(base_path.components());
324355
}
325356
Prefix::VerbatimDisk(drive_letter) => {
326357
components.next();
327358
if components.peek() == Some(&Component::RootDir) {
328359
components.next();
360+
base_path = get_drive_path(drive_letter);
361+
} else {
362+
bail!("illegal path: {}", path.display());
329363
}
330364

331-
base_path = get_drive_path(drive_letter);
332365
stack.extend(base_path.components());
333366
}
334367
_ => bail!("invalid path: {}", path.display()),

0 commit comments

Comments
 (0)