Skip to content

Commit 7f7462d

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 ajeetdsouza#567; see also rust-lang/rust-analyzer#14683. Signed-off-by: mataha <[email protected]>
1 parent 2856310 commit 7f7462d

File tree

3 files changed

+53
-16
lines changed

3 files changed

+53
-16
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ All notable changes to this project will be documented in this file.
77
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
88
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
99

10+
## Unreleased
11+
12+
### Fixed
13+
14+
- Normalize drive letters when resolving paths on Windows.
15+
1016
## [0.9.2] - 2023-08-04
1117

1218
### Added

src/shell.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ mod tests {
196196
}
197197

198198
#[apply(opts)]
199-
fn posix_shellcheck_(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) {
199+
fn posix_shellcheck(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) {
200200
let opts = Opts { cmd, hook, echo, resolve_symlinks };
201201
let source = Posix(&opts).render().unwrap();
202202

src/util.rs

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -291,44 +291,75 @@ pub fn resolve_path(path: impl AsRef<Path>) -> Result<PathBuf> {
291291
}
292292
}
293293

294-
fn get_drive_path(drive_letter: u8) -> PathBuf {
295-
format!(r"{}:\", drive_letter as char).into()
294+
fn get_drive_prefix_path(drive_letter: u8) -> PathBuf {
295+
format!(r"{}:\", patch_drive_letter(drive_letter)).into()
296296
}
297297

298-
fn get_drive_relative(drive_letter: u8) -> Result<PathBuf> {
298+
fn get_drive_relative_path(drive_letter: u8) -> Result<PathBuf> {
299299
let path = current_dir()?;
300300
if Some(drive_letter) == get_drive_letter(&path) {
301-
return Ok(path);
301+
return Ok(patch_drive_prefix(path));
302302
}
303303

304-
if let Some(path) = env::var_os(format!("={}:", drive_letter as char)) {
305-
return Ok(path.into());
304+
if let Some(path) = env::var_os(format!("={}:", patch_drive_letter(drive_letter))) {
305+
return Ok(patch_drive_prefix(path.into()));
306306
}
307307

308-
let path = get_drive_path(drive_letter);
308+
let path = get_drive_prefix_path(drive_letter);
309309
Ok(path)
310310
}
311311

312+
fn patch_drive_letter(drive_letter: u8) -> char {
313+
drive_letter.to_ascii_uppercase() as char
314+
}
315+
316+
// https://github.com/rust-lang/rust-analyzer/pull/14689
317+
fn patch_drive_prefix(path: PathBuf) -> PathBuf {
318+
let mut components = path.components();
319+
320+
match components.next() {
321+
Some(Component::Prefix(prefix)) => {
322+
let prefix = match prefix.kind() {
323+
Prefix::Disk(drive_letter) => {
324+
format!(r"{}:", patch_drive_letter(drive_letter))
325+
}
326+
Prefix::VerbatimDisk(drive_letter) => {
327+
format!(r"\\?\{}:", patch_drive_letter(drive_letter))
328+
}
329+
_ => return path,
330+
};
331+
332+
let mut path = PathBuf::default();
333+
path.push(prefix);
334+
path.extend(components);
335+
path
336+
}
337+
_ => path,
338+
}
339+
}
340+
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_prefix_path(drive_letter);
320348
} else {
321-
base_path = get_drive_relative(drive_letter)?;
322-
stack.extend(base_path.components());
349+
base_path = get_drive_relative_path(drive_letter)?;
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_prefix_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()),
@@ -340,7 +371,7 @@ pub fn resolve_path(path: impl AsRef<Path>) -> Result<PathBuf> {
340371
let drive_letter = get_drive_letter(&current_dir).with_context(|| {
341372
format!("could not get drive letter: {}", current_dir.display())
342373
})?;
343-
base_path = get_drive_path(drive_letter);
374+
base_path = get_drive_prefix_path(drive_letter);
344375
stack.extend(base_path.components());
345376
}
346377
_ => {

0 commit comments

Comments
 (0)