fix(store): no root keeps the \\?\ prefix; the Windows root test compares canonical homes (P11c)
check / check (push) Canceled after 0s
portable-macos / portable (macos-latest) (push) Canceled after 0s
portable-windows / portable (windows-latest) (push) Canceled after 0s
two-user / two-user (push) Canceled after 0s

main's canonical default made Root::setup take std::fs::canonicalize(home),
which on Windows is \\?\C:\...  with the long name. root.rs now uses
root::canonical (canonicalize, then the prefix off) at every site, so no
root, record or message keeps the prefix; Unix is unchanged (the identity),
and macOS still agrees on /tmp and /private/tmp. windows_root.rs compares
against default_root(canonical(home)), so TEMP's 8.3 short name is not in
the expectation, and asserts the root has no prefix.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
swaitsandClaude Fable 5.1 committed 2026-10-30 16:18:04 -06:00
1 parent 4a1bd05b7c
commit f2d1d4e892
2 files changed
+20 -10

No files matched your search

+15 -9
View File
@@ -290,7 +290,7 @@ pub fn inside_a_root(path: &Path) -> bool {
let Some(existing) = path.ancestors().find(|dir| dir.exists()) else { let Some(existing) = path.ancestors().find(|dir| dir.exists()) else {
return false; 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()) 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"\\?\", "") 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<PathBuf> {
std::fs::canonicalize(path).map(|real| plain_path(&real))
}
/// [`without_extended_prefix`] for a path (the same path on Unix). /// [`without_extended_prefix`] for a path (the same path on Unix).
pub fn plain_path(path: &Path) -> PathBuf { pub fn plain_path(path: &Path) -> PathBuf {
match path.to_str() { match path.to_str() {
@@ -388,7 +395,7 @@ pub fn short_link_for(dir: &Path) -> std::io::Result<PathBuf> {
} }
Err(e) => return Err(e), 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]); let link = base.join(&hex::encode(Sha256::digest(dir.as_os_str().as_encoded_bytes()))[..16]);
match std::fs::symlink_metadata(&link) { match std::fs::symlink_metadata(&link) {
Ok(meta) => { Ok(meta) => {
@@ -533,8 +540,7 @@ impl Root {
// resolved that way too: on macOS `/tmp` is `/private/tmp`, and a // resolved that way too: on macOS `/tmp` is `/private/tmp`, and a
// root chosen at the default must read as the default (so the // root chosen at the default must read as the default (so the
// record goes), not as somewhere else. // record goes), not as somewhere else.
let default = let default = default_root(&canonical(home).unwrap_or_else(|_| home.to_path_buf()));
default_root(&std::fs::canonicalize(home).unwrap_or_else(|_| home.to_path_buf()));
let recorded = recorded_root(home)?; let recorded = recorded_root(home)?;
let current = recorded.clone().unwrap_or_else(|| default.clone()); let current = recorded.clone().unwrap_or_else(|| default.clone());
let path = match (chosen, &recorded, xdg_data_home) { 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 /// The source of an adoption, made absolute: books outside the root
/// (and not holding it) with a journal. /// (and not holding it) with a journal.
fn adoptable(&self, source: &Path) -> Result<PathBuf, RootError> { fn adoptable(&self, source: &Path) -> Result<PathBuf, RootError> {
let source = std::fs::canonicalize(source) let source = canonical(source)
.map_err(|e| RootError::Adopt(format!("can't read the books given: {}", e.kind())))?; .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) { if source.starts_with(&root) || root.starts_with(&source) {
return Err(RootError::Adopt(format!( return Err(RootError::Adopt(format!(
"those books are inside {THE_ROOT} (or hold it): adopt books from outside it" "those books are inside {THE_ROOT} (or hold it): adopt books from outside it"
@@ -1275,7 +1281,7 @@ impl Root {
confirm: bool, confirm: bool,
) -> Result<PutAside, RootError> { ) -> Result<PutAside, RootError> {
(|| { (|| {
let root = std::fs::canonicalize(&self.path)?; let root = canonical(&self.path)?;
let book = self.book_dir(id); let book = self.book_dir(id);
if !book.join(JOURNAL_FILE).is_file() { if !book.join(JOURNAL_FILE).is_file() {
return Err(RootError::Adopt(format!( return Err(RootError::Adopt(format!(
@@ -1298,7 +1304,7 @@ impl Root {
let (Some(parent), Some(name)) = (given.parent(), given.file_name()) else { let (Some(parent), Some(name)) = (given.parent(), given.file_name()) else {
return Err(RootError::Adopt(format!("{shown} can't be put aside"))); 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())))? .map_err(|e| RootError::Adopt(format!("{shown}: {}", e.kind())))?
.join(name); .join(name);
let meta = std::fs::symlink_metadata(&path) let meta = std::fs::symlink_metadata(&path)
@@ -1932,7 +1938,7 @@ fn resolve_new(path: &Path) -> Result<PathBuf, RootError> {
_ => break, _ => break,
} }
} }
let mut out = std::fs::canonicalize(existing)?; let mut out = canonical(existing)?;
for n in rest.into_iter().rev() { for n in rest.into_iter().rev() {
out.push(n); out.push(n);
} }
+5 -1
View File
@@ -35,7 +35,11 @@ fn the_default_root_sits_under_local_app_data() {
fn setup_makes_a_private_root_and_its_folders() { fn setup_makes_a_private_root_and_its_folders() {
let home = tmp("setup"); let home = tmp("setup");
let (root, report) = Root::setup(home.path(), None, None).unwrap(); 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!(report.created && !report.tightened && !report.recorded);
assert!(winsec::is_private(root.path()).unwrap()); assert!(winsec::is_private(root.path()).unwrap());
for sub in ["books", "run", "legacy"] { for sub in ["books", "run", "legacy"] {