Skip to main content

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}