fix(bundles): harden the install pipeline for cross-platform correctness

Windows review findings on the bundle provenance code:

- clones now pin core.autocrlf=false and core.eol=lf so recorded sha256
  values reflect repository bytes, not the machine's git config (autocrlf
  on Windows previously made every text file a false conflict on update),
  plus core.longpaths=true for deep bundle trees
- is_safe_relative_path additionally rejects NTFS alternate data stream
  colons, reserved device names (con, nul, COM1..), and trailing dots or
  spaces; such names never come from a valid checkout and previously
  desynced or failed on Windows
- file ownership dedupe compares paths case-insensitively on Windows and
  macOS where case variants denote one physical file (uninstalling one
  bundle could previously delete another bundle's file)
- a failed git clone no longer leaks its partial tree in the temp dir,
  and temp cleanup failures are logged instead of swallowed
- recording a bundle file outside the config dir (asset dir override)
  now warns instead of silently producing an undeletable record
This commit is contained in:
2026-08-25 11:37:41 -06:00
parent b67f1ef854
commit e55120dac6
2 changed files with 191 additions and 23 deletions
+66 -2
View File
@@ -442,7 +442,9 @@ impl BundleStore {
self.ensure_bundle_exists(bundle)?;
for (name, record) in self.bundles.iter_mut() {
if name != bundle {
record.files.retain(|owned| owned.path != file.path);
record
.files
.retain(|owned| !same_installed_path(&owned.path, &file.path));
}
}
let record = self
@@ -450,7 +452,9 @@ impl BundleStore {
.get_mut(bundle)
.expect("bundle existence checked above");
record.files.retain(|owned| owned.path != file.path);
record
.files
.retain(|owned| !same_installed_path(&owned.path, &file.path));
record.files.push(file);
self.save()
@@ -577,6 +581,17 @@ pub(crate) struct BundleListRow {
pub(crate) drift: DriftSummary,
}
/// NTFS and default APFS resolve file names case-insensitively, so records
/// differing only in case denote the same physical file there. Linux keeps
/// exact matching because case variants are genuinely distinct files.
fn same_installed_path(a: &str, b: &str) -> bool {
if cfg!(any(windows, target_os = "macos")) {
a.eq_ignore_ascii_case(b)
} else {
a == b
}
}
/// An unreadable file counts as locally modified: it exists but its integrity
/// cannot be verified.
pub(crate) fn bundle_list_rows(store: &BundleStore, config_dir: &Path) -> Vec<BundleListRow> {
@@ -1489,4 +1504,53 @@ mod tests {
assert!(rows.is_empty());
}
#[test]
fn same_installed_path_matches_filesystem_case_semantics() {
assert!(same_installed_path("macros/a.yaml", "macros/a.yaml"));
assert!(!same_installed_path("macros/a.yaml", "macros/b.yaml"));
assert_eq!(
same_installed_path("macros/Foo.yaml", "macros/foo.yaml"),
cfg!(any(windows, target_os = "macos"))
);
}
#[test]
fn record_file_transfers_case_variant_ownership_on_case_insensitive_hosts() {
let dir = TempStoreDir::new("bundles-case-variant");
let mut store = dir.store();
store
.upsert_bundle("alpha", metadata("https://x/a", "aaa"))
.unwrap();
store
.upsert_bundle("beta", metadata("https://x/b", "bbb"))
.unwrap();
store
.record_file("alpha", file_record("macros/Shared.yaml", "one"))
.unwrap();
store
.record_file("beta", file_record("macros/shared.yaml", "two"))
.unwrap();
let alpha_still_owns = store
.get("alpha")
.unwrap()
.files
.iter()
.any(|f| f.path == "macros/Shared.yaml");
assert_eq!(
alpha_still_owns,
!cfg!(any(windows, target_os = "macos")),
"case-variant paths are one physical file on case-insensitive hosts"
);
assert!(
store
.get("beta")
.unwrap()
.files
.iter()
.any(|f| f.path == "macros/shared.yaml")
);
}
}