{"record":{"id":"bc6ff41f56672156","repo":"jdx/mise","slug":"brew-cask-generic-artifact-backup-changed-directo","errorCode":null,"errorMessage":"brew-cask: generic artifact backup changed directories","messagePattern":"brew-cask: generic artifact backup changed directories","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask.rs","lineNumber":2614,"sourceCode":"fn validate_trusted_operation_parent(\n    parent: &TrustedOperationParent,\n    expected_parent: &Path,\n) -> Result<()> {\n    let actual_parent = std::fs::canonicalize(parent.path()?)?;\n    if actual_parent != expected_parent {\n        bail!(\n            \"brew-cask: refusing operation through a changed generic artifact parent: {}\",\n            expected_parent.display()\n        );\n    }\n    Ok(())\n}\n\nfn rename_trusted_generic_target(from: &Path, to: &Path, expected_parent: &Path) -> Result<()> {\n    #[cfg(unix)]\n    {\n        if from.parent() != to.parent() {\n            bail!(\"brew-cask: generic artifact backup changed directories\");\n        }\n        let parent = open_trusted_operation_parent(from, true, false)?;\n        validate_trusted_operation_parent(&parent, expected_parent)?;\n        let from_name = from\n            .file_name()\n            .ok_or_else(|| eyre!(\"brew-cask: generic artifact source has no filename\"))?;\n        let to_name = to\n            .file_name()\n            .ok_or_else(|| eyre!(\"brew-cask: generic artifact target has no filename\"))?;\n        nix::fcntl::renameat(&parent.fd, from_name, &parent.fd, to_name)?;\n        Ok(())\n    }\n    #[cfg(not(unix))]\n    {\n        let _ = expected_parent;\n        file::rename(from, to)\n    }\n}","sourceCodeStart":2596,"sourceCodeEnd":2632,"githubUrl":"https://github.com/jdx/mise/blob/6f52dcdf99e282ef7a7db68c81301fa4618d0f79/src/system/packages/brew/cask.rs#L2596-L2632","documentation":"rename_trusted_generic_target atomically renames a generic artifact target to its backup name using renameat against a single trusted parent directory fd. It first requires from.parent() == to.parent() — the backup must live in the same directory as the target so the rename never crosses directories. Because the backup is normally derived via target.with_extension(\"mise-old-<hash>\") (which preserves the parent), a mismatch is an internal-invariant breach: the two paths were built from different bases.","triggerScenarios":"Programmatic callers passing a backup path located in a different directory; path normalization that changes the computed parent (trailing separators, surviving '..' components); not a configuration-level error for end users.","commonSituations":"Forks or custom tooling that construct backup paths themselves; essentially unseen in normal CLI-driven installs.","solutions":["If calling this from code, derive the backup with target.with_extension(...) so it shares the target's parent","Re-run once to rule out a transient filesystem quirk","If it persists, capture it with MISE_DEBUG=1 (or RUST_BACKTRACE=1) and file an issue — the invariant should hold for every validated target"],"exampleFix":"// before (caller code)\nlet backup = backups_dir.join(target.file_name().unwrap());\nrename_trusted_generic_target(&target, &backup, &parent)?;\n\n// after\nlet backup = target.with_extension(format!(\"mise-old-{}\", hash));\nrename_trusted_generic_target(&target, &backup, &parent)?;","handlingStrategy":"validation","validationCode":"// If you call rename_trusted_generic_target yourself, keep both paths in one dir.\nlet backup = target.with_extension(format!(\"mise-old-{}\", short_hash(&target)));\nassert_eq!(target.parent(), backup.parent());","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Derive backup paths from the target via with_extension so the parent is shared","Never pass hand-built absolute backup paths into the trusted rename API"],"tags":["brew-cask","generic-artifact","rename","invariant"],"backgroundTag":"cross-directory-rename","analyzedSha":"6f52dcdf99e282ef7a7db68c81301fa4618d0f79","analyzedAt":"2026-08-22T10:14:23.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}