repograph_core/path.rs
1//! Path canonicalization that yields shell-usable paths on every platform.
2//!
3//! Every registered repo path flows through [`canonicalize`] before it is
4//! stored in the registry, inspected for status, or emitted by `switch`. The
5//! result must therefore be a path the user's shell can `cd` into directly.
6
7use std::path::{Path, PathBuf};
8
9/// Canonicalize `path`, stripping the Windows `\\?\` verbatim prefix whenever
10/// the simplified form is unambiguous. On Unix this is a plain canonicalize.
11///
12/// `std::fs::canonicalize` (and `fs_err`'s wrapper) return extended-length
13/// `\\?\C:\…` paths on Windows. Those break every consumer that matters here:
14/// `cd '\\?\C:\…'` fails in `cmd.exe`, the prefix surfaces verbatim in
15/// `list` / `status` / `doctor` output, and the leading `\\?\` is not what a
16/// user ever typed. [`dunce::simplified`] drops the prefix when the path stays
17/// valid without it (the common case for repo paths) and preserves it
18/// untouched when it is genuinely required — paths beyond `MAX_PATH`, or
19/// components illegal in a normal Win32 path.
20///
21/// # Errors
22///
23/// Propagates the [`std::io::Error`] from the underlying canonicalize — most
24/// commonly [`std::io::ErrorKind::NotFound`] when `path` does not exist —
25/// carrying `fs_err`'s path context in the message.
26pub fn canonicalize(path: &Path) -> std::io::Result<PathBuf> {
27 let canonical = fs_err::canonicalize(path)?;
28 Ok(dunce::simplified(&canonical).to_path_buf())
29}
30
31#[cfg(test)]
32mod tests {
33 #![allow(clippy::unwrap_used)]
34 use super::*;
35 use tempfile::TempDir;
36
37 #[test]
38 fn canonicalizes_existing_dir_to_absolute() {
39 let tmp = TempDir::new().unwrap();
40 let resolved = canonicalize(tmp.path()).unwrap();
41 assert!(resolved.is_absolute());
42 }
43
44 #[test]
45 #[cfg(windows)]
46 fn strips_verbatim_prefix_on_windows() {
47 let tmp = TempDir::new().unwrap();
48 let resolved = canonicalize(tmp.path()).unwrap();
49 let as_str = resolved.to_string_lossy();
50 assert!(
51 !as_str.starts_with(r"\\?\"),
52 "verbatim prefix must be stripped so the path is shell-usable, got: {as_str}"
53 );
54 }
55
56 #[test]
57 fn missing_path_is_not_found() {
58 let tmp = TempDir::new().unwrap();
59 let err = canonicalize(&tmp.path().join("does-not-exist")).unwrap_err();
60 assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
61 }
62}