Skip to content

Commit e09b622

Browse files
tdragonclaude
andcommitted
fix(config): resolve trust hash collision for same-name directories
PathBuf::with_extension("hash") replaces everything after the last dot in the trust filename. Since hashed filenames contain dots (e.g. "infra-mise.toml-a1b2c3d4"), this causes all configs with the same parent directory leaf name to share a single hash file, silently breaking paranoid mode trust verification and monorepo markers. Replace with_extension() with a helper that appends the extension to the full filename instead of replacing. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent 443e831 commit e09b622

1 file changed

Lines changed: 43 additions & 5 deletions

File tree

src/config/config_file/mod.rs

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ pub fn is_trusted(path: &Path) -> bool {
358358
{
359359
let mut current = parent;
360360
while let Some(dir) = current.parent() {
361-
let monorepo_marker = trust_path(dir).with_extension("monorepo");
361+
let monorepo_marker = with_appended_extension(&trust_path(dir), "monorepo");
362362
if monorepo_marker.exists() {
363363
add_trusted(canonicalized_path.to_path_buf());
364364
return true;
@@ -440,7 +440,7 @@ pub fn trust(path: &Path) -> Result<()> {
440440
file::make_symlink_or_file(path.canonicalize()?.as_path(), &hashed_path)?;
441441
}
442442
if Settings::get().paranoid {
443-
let trust_hash_path = hashed_path.with_extension("hash");
443+
let trust_hash_path = with_appended_extension(&hashed_path, "hash");
444444
let hash = hash::file_hash_sha256(path, None)?;
445445
file::write(trust_hash_path, hash)?;
446446
}
@@ -451,7 +451,7 @@ pub fn trust(path: &Path) -> Result<()> {
451451
pub fn mark_as_monorepo_root(path: &Path) -> Result<()> {
452452
let config_root = config_trust_root(path);
453453
let hashed_path = trust_path(&config_root);
454-
let monorepo_marker = hashed_path.with_extension("monorepo");
454+
let monorepo_marker = with_appended_extension(&hashed_path, "monorepo");
455455
if !monorepo_marker.exists() {
456456
file::create_dir_all(monorepo_marker.parent().unwrap())?;
457457
file::write(&monorepo_marker, "")?;
@@ -463,7 +463,15 @@ pub fn untrust(path: &Path) -> eyre::Result<()> {
463463
rm_ignored(path.to_path_buf())?;
464464
let hashed_path = trust_path(path);
465465
if hashed_path.exists() {
466-
file::remove_file(hashed_path)?;
466+
file::remove_file(&hashed_path)?;
467+
}
468+
let hash_path = with_appended_extension(&hashed_path, "hash");
469+
if hash_path.exists() {
470+
file::remove_file(&hash_path)?;
471+
}
472+
let monorepo_path = with_appended_extension(&hashed_path, "monorepo");
473+
if monorepo_path.exists() {
474+
file::remove_file(&monorepo_path)?;
467475
}
468476
Ok(())
469477
}
@@ -477,6 +485,20 @@ fn ignore_path(path: &Path) -> PathBuf {
477485
dirs::IGNORED_CONFIGS.join(hashed_path_filename(path))
478486
}
479487

488+
/// Appends an extension to a path without replacing existing dots in the filename.
489+
/// Unlike `Path::with_extension`, this preserves the full filename.
490+
/// e.g. "foo-bar.toml-abc123" + "hash" → "foo-bar.toml-abc123.hash"
491+
///
492+
/// NOTE: This changes the filename convention for .hash and .monorepo files.
493+
/// Existing files from prior versions will not be found, requiring a one-time
494+
/// re-trust of previously trusted configs after upgrade.
495+
fn with_appended_extension(path: &Path, ext: &str) -> PathBuf {
496+
let mut os_string = path.as_os_str().to_owned();
497+
os_string.push(".");
498+
os_string.push(ext);
499+
PathBuf::from(os_string)
500+
}
501+
480502
/// creates the filename portion of trust/ignore files, e.g.:
481503
fn hashed_path_filename(path: &Path) -> String {
482504
let canonicalized_path = path.canonicalize().unwrap();
@@ -510,7 +532,7 @@ fn hashed_path_filename(path: &Path) -> String {
510532

511533
fn trust_file_hash(path: &Path) -> eyre::Result<bool> {
512534
let trust_path = trust_path(path);
513-
let trust_hash_path = trust_path.with_extension("hash");
535+
let trust_hash_path = with_appended_extension(&trust_path, "hash");
514536
if !trust_hash_path.exists() {
515537
return Ok(false);
516538
}
@@ -622,4 +644,20 @@ mod tests {
622644
Some(ConfigFileType::IdiomaticVersion(_))
623645
));
624646
}
647+
648+
#[test]
649+
fn test_with_appended_extension() {
650+
let path = Path::new("/tmp/trusted/infra-mise.toml-a1b2c3d4e5f67890");
651+
let result = with_appended_extension(path, "hash");
652+
assert_eq!(
653+
result,
654+
Path::new("/tmp/trusted/infra-mise.toml-a1b2c3d4e5f67890.hash")
655+
);
656+
657+
let result2 = with_appended_extension(path, "monorepo");
658+
assert_eq!(
659+
result2,
660+
Path::new("/tmp/trusted/infra-mise.toml-a1b2c3d4e5f67890.monorepo")
661+
);
662+
}
625663
}

0 commit comments

Comments
 (0)