use std::path::{Path, PathBuf};
use super::paths::normalize_for_policy;
use crate::orchestration::CapabilityPolicy;
pub(super) fn normalized_read_only_roots(policy: &CapabilityPolicy) -> Vec<PathBuf> {
let mut roots: Vec<PathBuf> = Vec::new();
for root in &policy.read_only_roots {
let root = normalize_for_policy(&super::resolve_policy_path(root));
if !roots.contains(&root) {
roots.push(root);
}
}
for dir in super::git_scope_extension_for_roots(&super::base_workspace_roots(policy)).read_only
{
if !roots.iter().any(|existing| existing == &dir) {
roots.push(dir);
}
}
roots
}
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
pub(crate) fn sandbox_user_home_dir() -> Option<PathBuf> {
crate::user_dirs::home_dir().filter(|path| path.is_absolute())
}
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
pub(crate) fn developer_toolchain_read_roots_for_home(home: &Path) -> Vec<PathBuf> {
let mut roots: Vec<_> = [
".asdf",
".bun",
".cargo",
".fnm",
".juliaup",
".local/bin",
".local/share/mise",
".local/share/pnpm",
".local/share/uv",
".nvm",
".pyenv",
".rbenv",
".rustup",
".sdkman",
".swiftly",
".volta",
"Library/pnpm",
"go",
]
.into_iter()
.map(|entry| normalize_for_policy(&home.join(entry)))
.collect();
#[cfg(target_os = "windows")]
roots.extend(
[
"AppData/Local/Programs/Python",
"AppData/Local/uv",
"AppData/Roaming/uv",
"scoop",
]
.into_iter()
.map(|entry| normalize_for_policy(&home.join(entry))),
);
roots.sort_unstable();
roots.dedup();
roots
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(crate) fn developer_toolchain_cache_write_roots_for_home(home: &Path) -> Vec<PathBuf> {
let mut roots: Vec<_> = [
".gradle", ".m2", ".konan", "Library/Caches/CocoaPods", "Library/Developer/Xcode/DerivedData", "Library/Caches/go-build", ".cache/go-build", "go/pkg/mod", "Library/Caches/harn", ".cache/harn", "Library/Application Support/go", ".cargo/registry", ".cargo/git", ".cargo/.package-cache", ]
.into_iter()
.map(|entry| normalize_for_policy(&home.join(entry)))
.collect();
roots.sort_unstable();
roots.dedup();
roots
}
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
pub(crate) fn package_manager_config_read_roots_for_home(home: &Path) -> Vec<PathBuf> {
let mut roots: Vec<_> = [
".npmrc",
".gitconfig",
".netrc",
".yarnrc.yml",
".config/git",
".config/pip",
".config/go",
".config/uv",
".config/pnpm",
".config/yarn",
".config/composer",
".config/rustfmt",
".config/cmake",
".config/gem",
".config/mise",
".config/ruff",
".config/black",
".config/sbt",
".config/coursier/mirror.properties",
".config/swiftpm/configuration",
".npm",
".cache/pip",
".cache/uv",
".cache/node/corepack",
".cache/yarn",
".cache/composer",
".cache/mise",
".cache/black",
".cache/sbt",
".cache/coursier",
".cache/JNA",
".cache/clang",
".cache/org.swift.swiftpm",
".cache/org.swift.foundation.URLCache",
".pip",
".pypirc",
".composer",
"Library/Preferences/pnpm",
".cargo/config",
".cargo/config.toml",
".cargo/credentials",
".cargo/credentials.toml",
]
.into_iter()
.map(|entry| normalize_for_policy(&home.join(entry)))
.collect();
roots.sort_unstable();
roots.dedup();
roots
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(super) mod path_grants;
#[cfg(all(test, any(target_os = "linux", target_os = "macos")))]
mod tests {
use super::*;
#[test]
fn package_manager_roots_do_not_admit_unknown_xdg_siblings() {
let home = tempfile::tempdir().unwrap();
let roots = package_manager_config_read_roots_for_home(home.path());
for relative in [".config/unlisted/token", ".cache/unlisted/token"] {
let target = normalize_for_policy(&home.path().join(relative));
assert!(
!roots.iter().any(|root| target.starts_with(root)),
"unknown XDG sibling is covered by a preset: {roots:?}"
);
}
for relative in [".config/git", ".config/rustfmt", ".cache/pip"] {
assert!(roots.contains(&normalize_for_policy(&home.path().join(relative))));
}
}
#[test]
fn read_only_xdg_roots_do_not_cancel_writable_toolchain_caches() {
let home = tempfile::tempdir().unwrap();
let reads = package_manager_config_read_roots_for_home(home.path());
let cache = normalize_for_policy(&home.path().join(".cache"));
let mut checked = 0;
for writable in developer_toolchain_cache_write_roots_for_home(home.path()) {
if writable.starts_with(&cache) {
checked += 1;
assert!(
!reads.iter().any(|read| writable.starts_with(read)),
"read-only {reads:?} would cancel writes to {writable:?} on macOS"
);
}
}
assert!(checked >= 2, "must check the Go and Harn writable caches");
}
}