diff --git a/crates/ledger-store/src/root.rs b/crates/ledger-store/src/root.rs index 7d9f151..bb394fd 100644 --- a/crates/ledger-store/src/root.rs +++ b/crates/ledger-store/src/root.rs @@ -290,7 +290,7 @@ pub fn inside_a_root(path: &Path) -> bool { let Some(existing) = path.ancestors().find(|dir| dir.exists()) else { return false; }; - let real = std::fs::canonicalize(existing).unwrap_or_else(|_| existing.to_path_buf()); + let real = canonical(existing).unwrap_or_else(|_| existing.to_path_buf()); real.ancestors().any(|dir| dir.join(MARKER_FILE).is_file()) } @@ -323,6 +323,13 @@ pub fn without_extended_prefix(text: &str) -> String { text.replace(r"\\?\UNC\", r"\\").replace(r"\\?\", "") } +/// `std::fs::canonicalize` with Windows' extended-length prefix taken off +/// (`\\?\C:\dir` is `C:\dir`): no root, record, message or git argument +/// ever keeps the prefix. +pub fn canonical(path: &Path) -> std::io::Result { + std::fs::canonicalize(path).map(|real| plain_path(&real)) +} + /// [`without_extended_prefix`] for a path (the same path on Unix). pub fn plain_path(path: &Path) -> PathBuf { match path.to_str() { @@ -388,7 +395,7 @@ pub fn short_link_for(dir: &Path) -> std::io::Result { } Err(e) => return Err(e), } - let dir = std::fs::canonicalize(dir)?; + let dir = canonical(dir)?; let link = base.join(&hex::encode(Sha256::digest(dir.as_os_str().as_encoded_bytes()))[..16]); match std::fs::symlink_metadata(&link) { Ok(meta) => { @@ -533,8 +540,7 @@ impl Root { // resolved that way too: on macOS `/tmp` is `/private/tmp`, and a // root chosen at the default must read as the default (so the // record goes), not as somewhere else. - let default = - default_root(&std::fs::canonicalize(home).unwrap_or_else(|_| home.to_path_buf())); + let default = default_root(&canonical(home).unwrap_or_else(|_| home.to_path_buf())); let recorded = recorded_root(home)?; let current = recorded.clone().unwrap_or_else(|| default.clone()); let path = match (chosen, &recorded, xdg_data_home) { @@ -1209,9 +1215,9 @@ impl Root { /// The source of an adoption, made absolute: books outside the root /// (and not holding it) with a journal. fn adoptable(&self, source: &Path) -> Result { - let source = std::fs::canonicalize(source) + let source = canonical(source) .map_err(|e| RootError::Adopt(format!("can't read the books given: {}", e.kind())))?; - let root = std::fs::canonicalize(&self.path)?; + let root = canonical(&self.path)?; if source.starts_with(&root) || root.starts_with(&source) { return Err(RootError::Adopt(format!( "those books are inside {THE_ROOT} (or hold it): adopt books from outside it" @@ -1275,7 +1281,7 @@ impl Root { confirm: bool, ) -> Result { (|| { - let root = std::fs::canonicalize(&self.path)?; + let root = canonical(&self.path)?; let book = self.book_dir(id); if !book.join(JOURNAL_FILE).is_file() { return Err(RootError::Adopt(format!( @@ -1298,7 +1304,7 @@ impl Root { let (Some(parent), Some(name)) = (given.parent(), given.file_name()) else { return Err(RootError::Adopt(format!("{shown} can't be put aside"))); }; - let path = std::fs::canonicalize(parent) + let path = canonical(parent) .map_err(|e| RootError::Adopt(format!("{shown}: {}", e.kind())))? .join(name); let meta = std::fs::symlink_metadata(&path) @@ -1932,7 +1938,7 @@ fn resolve_new(path: &Path) -> Result { _ => break, } } - let mut out = std::fs::canonicalize(existing)?; + let mut out = canonical(existing)?; for n in rest.into_iter().rev() { out.push(n); } diff --git a/crates/ledger-store/tests/windows_root.rs b/crates/ledger-store/tests/windows_root.rs index d9b016d..629ba23 100644 --- a/crates/ledger-store/tests/windows_root.rs +++ b/crates/ledger-store/tests/windows_root.rs @@ -35,7 +35,11 @@ fn the_default_root_sits_under_local_app_data() { fn setup_makes_a_private_root_and_its_folders() { let home = tmp("setup"); let (root, report) = Root::setup(home.path(), None, None).unwrap(); - assert_eq!(root.path(), default_root(home.path())); + // The default root of the home as the system names it (its long name, + // no `\\?\` prefix; TEMP may hold an 8.3 short name). + let real_home = ledger_store::root::canonical(home.path()).unwrap(); + assert_eq!(root.path(), default_root(&real_home)); + assert!(!root.path().to_string_lossy().starts_with(r"\\?\")); assert!(report.created && !report.tightened && !report.recorded); assert!(winsec::is_private(root.path()).unwrap()); for sub in ["books", "run", "legacy"] {