Skip to content

Commit 8a19d52

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 8a19d52

File tree

2 files changed

+44
-12
lines changed

2 files changed

+44
-12
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

+43-12
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::ffi::OsStr;
22
use std::fs::{self, File, OpenOptions};
33
use std::io::{self, Read, Write};
4-
use std::path::{Component, Path, PathBuf};
4+
use std::path::{Component, Path, PathBuf, Prefix};
55
use std::process::{Child, Command, Stdio};
66
use std::time::SystemTime;
77
use std::{env, mem};
@@ -263,6 +263,37 @@ pub fn path_to_str(path: &impl AsRef<Path>) -> Result<&str> {
263263
path.to_str().with_context(|| format!("invalid unicode in path: {}", path.display()))
264264
}
265265

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

275306
// initialize root
276307
if cfg!(windows) {
277-
use std::path::Prefix;
278-
279308
fn get_drive_letter(path: impl AsRef<Path>) -> Option<u8> {
280309
let path = path.as_ref();
281310
let mut components = path.components();
@@ -292,17 +321,17 @@ pub fn resolve_path(path: impl AsRef<Path>) -> Result<PathBuf> {
292321
}
293322

294323
fn get_drive_path(drive_letter: u8) -> PathBuf {
295-
format!(r"{}:\", drive_letter as char).into()
324+
format!(r"{}:\", drive_letter.to_ascii_uppercase() as char).into()
296325
}
297326

298327
fn get_drive_relative(drive_letter: u8) -> Result<PathBuf> {
299328
let path = current_dir()?;
300329
if Some(drive_letter) == get_drive_letter(&path) {
301-
return Ok(path);
330+
return Ok(patch_path(path));
302331
}
303332

304333
if let Some(path) = env::var_os(format!("={}:", drive_letter as char)) {
305-
return Ok(path.into());
334+
return Ok(patch_path(path.into()));
306335
}
307336

308337
let path = get_drive_path(drive_letter);
@@ -312,23 +341,25 @@ pub fn resolve_path(path: impl AsRef<Path>) -> Result<PathBuf> {
312341
match components.peek() {
313342
Some(Component::Prefix(prefix)) => match prefix.kind() {
314343
Prefix::Disk(drive_letter) => {
315-
let disk = components.next().unwrap();
344+
components.next();
316345
if components.peek() == Some(&Component::RootDir) {
317-
let root = components.next().unwrap();
318-
stack.push(disk);
319-
stack.push(root);
346+
components.next();
347+
base_path = get_drive_path(drive_letter);
320348
} else {
321349
base_path = get_drive_relative(drive_letter)?;
322-
stack.extend(base_path.components());
323350
}
351+
352+
stack.extend(base_path.components());
324353
}
325354
Prefix::VerbatimDisk(drive_letter) => {
326355
components.next();
327356
if components.peek() == Some(&Component::RootDir) {
328357
components.next();
358+
base_path = get_drive_path(drive_letter);
359+
} else {
360+
bail!("illegal path: {}", path.display());
329361
}
330362

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

0 commit comments

Comments
 (0)