{"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":2488,"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":2470,"sourceCodeEnd":2506,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/system/packages/brew/cask.rs#L2470-L2506","documentation":"The backup/restore rename of a generic artifact is performed with renameat on a single opened parent directory descriptor, which only works when source and destination share one directory. rename_trusted_generic_target therefore requires from.parent() == to.parent() and refuses when the backup path lives in a different directory.","triggerScenarios":"A caller constructing the backup path under a different directory (e.g. a temp dir) than the live target; any code path where the '.mise-old' backup name is appended to a different base path.","commonSituations":"Custom wrappers moving backups to /tmp; orchestration bugs mixing up source and target parents; almost never caused by end-user configuration.","solutions":["Build the backup path in the same directory as the target: target.with_file_name(format!(\".{}.mise-old\", name))","If calling rename_trusted_generic_target directly, assert from.parent() == to.parent() beforehand","Keep backup/restore inside one directory so the single-parent rename stays valid"],"exampleFix":"// before -- backup created in another directory\nlet backup = std::env::temp_dir().join(target.file_name().unwrap());\n\n// after -- same parent as the target\nlet backup = target.with_file_name(format!(\".{}.mise-old\", target.file_name().unwrap().to_string_lossy()));","handlingStrategy":"validation","validationCode":"if from.parent() != to.parent() {\n    anyhow::bail!(\"backup path must share the target's directory: {} vs {}\",\n        from.display(), to.display());\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always derive backup paths with target.with_file_name(...) so they stay in the same directory","Never relocate backups to a temp dir before the rename completes","Assert from.parent() == to.parent() before calling rename helpers"],"tags":["brew-cask","rename","backup","invariant"],"backgroundTag":"cross-directory-rename","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}