{"record":{"id":"a0117444eb7ace2f","repo":"jdx/mise","slug":"brew-cask-generic-artifact-target-must-stay","errorCode":null,"errorMessage":"brew-cask: generic artifact target '{}' must stay below {}","messagePattern":"brew-cask: generic artifact target '(.+?)' must stay below (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask.rs","lineNumber":2375,"sourceCode":"        run_installer_artifact(stage, installer, targets.copied_files())?;\n        completed(index)?;\n    }\n    durabilize_staged_symlink_targets(stage, temporary_caskroom, targets)\n}\n\nfn generic_artifact_target_path(target: &str) -> Result<PathBuf> {\n    let prefix = prefix::prefix();\n    let expanded = target.replace(\"$HOMEBREW_PREFIX\", &prefix.to_string_lossy());\n    let target = PathBuf::from(expanded);\n    if !target.is_absolute()\n        || !target.starts_with(&prefix)\n        || target.strip_prefix(&prefix)?.components().next().is_none()\n        || target\n            .components()\n            .any(|component| matches!(component, Component::ParentDir))\n        || !path_starts_with_resolved_root(&target, &prefix)\n    {\n        bail!(\n            \"brew-cask: generic artifact target '{}' must stay below {}\",\n            target.display(),\n            prefix.display()\n        );\n    }\n    Ok(target)\n}\n\nfn generic_artifact_targets(artifacts: &CaskArtifacts) -> Result<Vec<PathBuf>> {\n    artifacts\n        .generic\n        .iter()\n        .map(|artifact| generic_artifact_target_path(&artifact.target))\n        .collect()\n}\n\nfn previous_generic_targets(cask: &Cask) -> Result<Vec<CaskTargetRecord>> {\n    let Some(version) = installed_version(&cask.token) else {","sourceCodeStart":2357,"sourceCodeEnd":2393,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/system/packages/brew/cask.rs#L2357-L2393","documentation":"generic_artifact_target_path expands $HOMEBREW_PREFIX in the cask's target string and validates the result: it must be absolute, lexically under the prefix with at least one component beneath it, free of '..' components, and its symlink-resolved root must still be under the prefix. Any violation refuses with the offending target shown.","triggerScenarios":"A target string that is relative (\"Library/Foo\"), absolute outside the prefix (\"/Library/Foo\"), contains '..', or where the prefix itself is a symlink so the resolved root no longer matches (path_starts_with_resolved_root fails).","commonSituations":"Casks hardcoding absolute macOS paths; HOMEBREW_PREFIX pointed at a symlinked directory; target templates missing the $HOMEBREW_PREFIX variable.","solutions":["Write the target as an absolute path beginning with $HOMEBREW_PREFIX (\"$HOMEBREW_PREFIX/Library/Foo\")","Remove '..' components from the target string","Make the prefix a real directory or ensure its symlinks resolve consistently so the resolved-root check passes"],"exampleFix":"# before\ntarget: \"Library/Application Support/Foo\"\n\n# after\ntarget: \"$HOMEBREW_PREFIX/Library/Application Support/Foo\"","handlingStrategy":"validation","validationCode":"use std::path::{Path, Component};\n\nfn target_string_is_valid(target: &str, prefix: &Path) -> bool {\n    let expanded = target.replace(\"$HOMEBREW_PREFIX\", &prefix.to_string_lossy());\n    let t = Path::new(&expanded);\n    t.is_absolute()\n        && t.starts_with(prefix)\n        && t.strip_prefix(prefix).is_ok_and(|r| r.components().next().is_some())\n        && !t.components().any(|c| matches!(c, Component::ParentDir))\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always write generic artifact targets as \"$HOMEBREW_PREFIX/...\" absolute paths","Keep '..' out of target strings","Ensure the prefix resolves consistently (no retargeted symlinks) across operations"],"tags":["brew-cask","generic-artifact","path-validation","homebrew-prefix"],"backgroundTag":"path-traversal-guard","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}