use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use crate::orchestration::CapabilityPolicy;
use super::{
base_workspace_roots, normalize_for_policy, normalized_workspace_roots, path_is_within,
warn_once,
};
pub(crate) const WORKSPACE_TMPDIR_NAME: &str = ".harn-tmp";
pub(crate) const TMPDIR_ENV_KEYS: [&str; 3] = ["TMPDIR", "TMP", "TEMP"];
pub(crate) const WORKSPACE_TOOLCHAIN_CACHE_NAME: &str = ".harn-toolchain-cache";
pub fn is_workspace_runtime_directory(name: &OsStr) -> bool {
name == OsStr::new(WORKSPACE_TMPDIR_NAME) || name == OsStr::new(WORKSPACE_TOOLCHAIN_CACHE_NAME)
}
fn create_self_ignored_dir(
policy: &CapabilityPolicy,
name: &str,
warning_key: &str,
label: &str,
) -> Option<PathBuf> {
if !policy.sandbox_profile.enforces_path_scope() {
return None;
}
let root = normalized_workspace_roots(policy).into_iter().next()?;
let path = root.join(name);
if let Err(error) = initialize_self_ignored_dir(&root, name) {
warn_once(
warning_key,
&format!(
"could not create workspace-local {label} '{}': {error}; \
leaving the child's inherited environment in place",
path.display()
),
);
return None;
}
Some(path)
}
#[cfg(unix)]
fn initialize_self_ignored_dir(root: &Path, name: &str) -> std::io::Result<()> {
let (parent, leaf) = super::ensure_parent_dirs_scoped(&super::ScopedMutationTarget {
root: root.to_path_buf(),
relative: Path::new(name).join(".gitignore"),
})?;
let _ = write_scoped_self_ignore(&parent, &leaf);
Ok(())
}
#[cfg(unix)]
fn write_scoped_self_ignore(parent: &std::os::fd::OwnedFd, name: &str) -> std::io::Result<()> {
use std::io::Write;
use std::os::fd::AsRawFd;
match super::openat_file(
parent.as_raw_fd(),
name,
libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_CLOEXEC | libc::O_NOFOLLOW,
0o600,
) {
Ok(mut file) => file.write_all(crate::runtime_paths::SELF_IGNORE_CONTENTS.as_bytes()),
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => Ok(()),
Err(error) => Err(error),
}
}
#[cfg(not(unix))]
fn initialize_self_ignored_dir(root: &Path, name: &str) -> std::io::Result<()> {
super::create_dir_all_scoped_target(&super::ScopedMutationTarget {
root: root.to_path_buf(),
relative: PathBuf::from(name),
})?;
let _ = crate::runtime_paths::ensure_self_ignored_dir(&root.join(name));
Ok(())
}
pub fn workspace_local_tmpdir(policy: &CapabilityPolicy) -> Option<PathBuf> {
create_self_ignored_dir(
policy,
WORKSPACE_TMPDIR_NAME,
"handler_sandbox_workspace_tmpdir",
"temp dir",
)
}
fn workspace_local_toolchain_cache(policy: &CapabilityPolicy) -> Option<PathBuf> {
create_self_ignored_dir(
policy,
WORKSPACE_TOOLCHAIN_CACHE_NAME,
"handler_sandbox_workspace_toolchain_cache",
"toolchain cache",
)
}
fn inherited_workspace_cache_path(policy: &CapabilityPolicy, key: &str) -> Option<String> {
if !crate::security::environment_policy::TOOLCHAIN_CACHE_ENV_VARS.contains(&key) {
return None;
}
let raw = match crate::stdlib::process::current_session_environment() {
Some(environment) => environment.launcher_value(key)?.to_string(),
None => crate::test_env::env_var_seamed(key)?,
};
let candidate = PathBuf::from(raw.trim());
if !candidate.is_absolute() {
return None;
}
let resolved = normalize_for_policy(&candidate);
base_workspace_roots(policy)
.iter()
.any(|root| path_is_within(&resolved, root))
.then(|| resolved.display().to_string())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ToolchainHomeSource {
Inherited,
DerivedFromHome,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ToolchainHome {
pub path: String,
pub source: ToolchainHomeSource,
}
pub(crate) fn toolchain_home(name: &str, suffix: &str) -> Option<ToolchainHome> {
toolchain_home_from(
std::env::var_os(name).as_deref(),
crate::user_dirs::home_dir(),
suffix,
)
}
pub(crate) fn toolchain_home_from(
explicit: Option<&OsStr>,
home: Option<PathBuf>,
suffix: &str,
) -> Option<ToolchainHome> {
if let Some(explicit) = explicit
.map(PathBuf::from)
.filter(|value| value.is_absolute())
{
return Some(ToolchainHome {
path: explicit.display().to_string(),
source: ToolchainHomeSource::Inherited,
});
}
let home = home.filter(|home| home.is_absolute())?;
Some(ToolchainHome {
path: home.join(suffix).display().to_string(),
source: ToolchainHomeSource::DerivedFromHome,
})
}
fn workspace_toolchain_env_with_package_cache(
policy: &CapabilityPolicy,
package_cache: Option<PathBuf>,
) -> Vec<(String, String)> {
let Some(root) = workspace_local_toolchain_cache(policy) else {
return Vec::new();
};
let path = |key: &str, suffix: &str| {
inherited_workspace_cache_path(policy, key)
.unwrap_or_else(|| root.join(suffix).display().to_string())
};
let mut env = vec![
(
"XDG_CACHE_HOME".to_string(),
root.join("xdg-cache").display().to_string(),
),
("GOCACHE".to_string(), path("GOCACHE", "go-build")),
("GOMODCACHE".to_string(), path("GOMODCACHE", "go-mod")),
("GOPATH".to_string(), path("GOPATH", "go")),
(
"SWIFTPM_MODULECACHE_OVERRIDE".to_string(),
path("SWIFTPM_MODULECACHE_OVERRIDE", "swiftpm/modules"),
),
(
"CLANG_MODULE_CACHE_PATH".to_string(),
path("CLANG_MODULE_CACHE_PATH", "clang-module-cache"),
),
(
"xcrun_db".to_string(),
root.join("xcrun/xcrun_db").display().to_string(),
),
(
"CARGO_TARGET_DIR".to_string(),
path("CARGO_TARGET_DIR", "cargo-target"),
),
("PIP_CACHE_DIR".to_string(), path("PIP_CACHE_DIR", "pip")),
("UV_CACHE_DIR".to_string(), path("UV_CACHE_DIR", "uv")),
(
"NPM_CONFIG_CACHE".to_string(),
path("NPM_CONFIG_CACHE", "npm"),
),
(
"YARN_CACHE_FOLDER".to_string(),
path("YARN_CACHE_FOLDER", "yarn"),
),
("PNPM_HOME".to_string(), path("PNPM_HOME", "pnpm/home")),
(
"COMPOSER_CACHE_DIR".to_string(),
path("COMPOSER_CACHE_DIR", "composer"),
),
("CCACHE_DIR".to_string(), path("CCACHE_DIR", "ccache")),
(
"CCACHE_TEMPDIR".to_string(),
path("CCACHE_TEMPDIR", "ccache-tmp"),
),
];
if let Some(package_cache) = package_cache {
env.push((
"HARN_CACHE_DIR".to_string(),
package_cache.display().to_string(),
));
}
for (name, suffix) in [("CARGO_HOME", ".cargo"), ("RUSTUP_HOME", ".rustup")] {
if let Some(resolved) = toolchain_home(name, suffix) {
env.push((name.to_string(), resolved.path));
}
}
if let Some(home) = crate::user_dirs::home_dir().filter(|home| home.is_absolute()) {
for (key, candidate) in [
("GIT_CONFIG_GLOBAL", home.join(".gitconfig")),
("PIP_CONFIG_FILE", home.join(".config/pip/pip.conf")),
] {
if key == "GIT_CONFIG_GLOBAL" && std::env::var_os(key).is_some() {
continue;
}
if candidate.is_file() {
env.push((key.to_string(), candidate.display().to_string()));
}
}
env.push((
"NPM_CONFIG_USERCONFIG".to_string(),
seed_sanitized_npmrc(&home.join(".npmrc"), &root.join("npm/npmrc"))
.display()
.to_string(),
));
env.push((
"COMPOSER_HOME".to_string(),
seed_composer_home(&home, &root.join("composer/home"))
.display()
.to_string(),
));
}
env
}
fn seed_sanitized_npmrc(source: &Path, stand_in: &Path) -> PathBuf {
let Ok(raw) = std::fs::read_to_string(source) else {
return stand_in.to_path_buf();
};
let sanitized = sanitize_npmrc(&raw);
if std::fs::read_to_string(stand_in).ok().as_deref() != Some(sanitized.as_str()) {
if let Some(parent) = stand_in.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(stand_in, sanitized);
}
stand_in.to_path_buf()
}
fn seed_composer_home(operator_home: &Path, dest: &Path) -> PathBuf {
let _ = std::fs::create_dir_all(dest);
for name in ["config.json", "composer.json"] {
for source in [
operator_home.join(".composer").join(name),
operator_home.join(".config/composer").join(name),
] {
if let Ok(raw) = std::fs::read_to_string(&source) {
let dest_file = dest.join(name);
if std::fs::read_to_string(&dest_file).ok().as_deref() != Some(raw.as_str()) {
let _ = std::fs::write(&dest_file, raw);
}
break;
}
}
}
dest.to_path_buf()
}
fn sanitize_npmrc(raw: &str) -> String {
let mut out = String::with_capacity(raw.len());
for line in raw.lines() {
let trimmed = line.trim_start();
let key = trimmed
.split_once('=')
.map(|(key, _)| key.trim_end())
.unwrap_or(trimmed);
let leaf = key.rsplit(':').next().unwrap_or(key).trim();
let credential = key.starts_with("//") || leaf.starts_with('_');
if !credential {
out.push_str(line);
out.push('\n');
}
}
out
}
fn workspace_toolchain_env(policy: &CapabilityPolicy) -> Vec<(String, String)> {
workspace_toolchain_env_with_package_cache(policy, crate::user_dirs::package_cache_dir())
}
pub(crate) fn inject_workspace_tmpdir(env: &mut Vec<(String, String)>, policy: &CapabilityPolicy) {
let Some(tmpdir) = workspace_local_tmpdir(policy) else {
return;
};
let tmpdir = tmpdir.display().to_string();
for key in TMPDIR_ENV_KEYS {
if !env.iter().any(|(existing, _)| existing == key) {
env.push((key.to_string(), tmpdir.clone()));
}
}
}
pub(crate) fn inject_workspace_process_env(
env: &mut Vec<(String, String)>,
policy: &CapabilityPolicy,
) {
inject_workspace_tmpdir(env, policy);
#[cfg(target_os = "macos")]
super::macos::private_temp::inject_env(env, policy);
for (key, value) in workspace_toolchain_env(policy) {
if !env.iter().any(|(existing, _)| existing == &key) {
env.push((key, value));
}
}
inject_jvm_loopback_env(env, policy);
#[cfg(target_os = "macos")]
super::macos::swiftpm::inject_env(env, policy);
}
const JVM_PREFER_IPV4_STACK: &str = "-Djava.net.preferIPv4Stack=true";
fn inject_jvm_loopback_env(env: &mut Vec<(String, String)>, policy: &CapabilityPolicy) {
if !cfg!(target_os = "macos") || !policy.process_sandbox.allow_tcp_loopback {
return;
}
let existing = env
.iter()
.position(|(key, _)| key == "JAVA_TOOL_OPTIONS")
.map(|index| env[index].1.clone())
.or_else(
|| match crate::stdlib::process::current_session_environment() {
Some(environment) => environment
.launcher_value("JAVA_TOOL_OPTIONS")
.map(str::to_string),
None => crate::test_env::env_var_seamed("JAVA_TOOL_OPTIONS"),
},
)
.unwrap_or_default();
if existing.contains("java.net.preferIPv4Stack")
|| existing.contains("java.net.preferIPv6Addresses")
{
return;
}
let value = if existing.trim().is_empty() {
JVM_PREFER_IPV4_STACK.to_string()
} else {
format!("{} {JVM_PREFER_IPV4_STACK}", existing.trim_end())
};
env.retain(|(key, _)| key != "JAVA_TOOL_OPTIONS");
env.push(("JAVA_TOOL_OPTIONS".to_string(), value));
}
pub fn active_workspace_process_env() -> Vec<(String, String)> {
let Some(policy) = crate::orchestration::current_execution_policy() else {
return Vec::new();
};
let mut env = Vec::new();
inject_workspace_process_env(&mut env, &policy);
env
}
#[cfg(test)]
mod toolchain_home_tests {
use super::{toolchain_home_from, ToolchainHomeSource};
use std::ffi::OsString;
use std::path::PathBuf;
fn absolute(parts: &[&str]) -> PathBuf {
let mut path = if cfg!(windows) {
PathBuf::from("C:\\")
} else {
PathBuf::from("/")
};
for part in parts {
path.push(part);
}
assert!(path.is_absolute(), "the fixture root must be absolute here");
path
}
fn absolute_string(parts: &[&str]) -> String {
absolute(parts).display().to_string()
}
#[test]
fn the_fixture_root_is_absolute_and_a_unix_spelling_is_not_portable() {
assert!(absolute(&["opt", "cargo"]).is_absolute());
assert!(absolute_string(&["users", "real", ".cargo"]).ends_with(".cargo"));
assert_eq!(PathBuf::from("/opt/cargo").is_absolute(), !cfg!(windows));
}
#[test]
fn an_explicit_toolchain_home_is_inherited_not_derived() {
let resolved = toolchain_home_from(
Some(&OsString::from(absolute(&["opt", "cargo"]))),
Some(absolute(&["relocated", "home"])),
".cargo",
)
.expect("a set value always resolves");
assert_eq!(resolved.path, absolute_string(&["opt", "cargo"]));
assert_eq!(resolved.source, ToolchainHomeSource::Inherited);
}
#[test]
fn an_unset_toolchain_home_is_derived_from_the_home_directory() {
let resolved = toolchain_home_from(None, Some(absolute(&["users", "real"])), ".rustup")
.expect("an absolute home always resolves");
assert_eq!(
resolved.path,
absolute_string(&["users", "real", ".rustup"])
);
assert_eq!(resolved.source, ToolchainHomeSource::DerivedFromHome);
}
#[test]
fn a_set_value_is_never_overwritten_by_the_derived_one() {
let derived_would_be = absolute_string(&["relocated", "home", ".rustup"]);
let resolved = toolchain_home_from(
Some(&OsString::from(absolute(&["opt", "rustup"]))),
Some(absolute(&["relocated", "home"])),
".rustup",
)
.expect("a set value always resolves");
assert_ne!(
resolved.path, derived_would_be,
"the derivation overwrote a value the caller set"
);
assert_eq!(resolved.source, ToolchainHomeSource::Inherited);
}
#[test]
fn a_relative_or_empty_setting_reads_as_unset() {
for setting in ["", "relative/cargo"] {
let resolved = toolchain_home_from(
Some(&OsString::from(setting)),
Some(absolute(&["users", "real"])),
".cargo",
)
.expect("the home directory still resolves");
assert_eq!(resolved.path, absolute_string(&["users", "real", ".cargo"]));
assert_eq!(resolved.source, ToolchainHomeSource::DerivedFromHome);
}
}
#[test]
fn no_setting_and_no_absolute_home_resolves_to_nothing() {
assert!(toolchain_home_from(None, None, ".cargo").is_none());
assert!(toolchain_home_from(None, Some(PathBuf::from("relative")), ".cargo").is_none());
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::*;
use crate::orchestration::SandboxProfile;
fn policy(root: &std::path::Path) -> CapabilityPolicy {
CapabilityPolicy {
sandbox_profile: SandboxProfile::Worktree,
workspace_roots: vec![root.display().to_string()],
..CapabilityPolicy::default()
}
}
#[cfg(unix)]
#[test]
fn runtime_ignore_uses_the_opened_directory_after_a_workspace_path_swap() {
let workspace = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
std::fs::write(outside.path().join("marker"), "host-unchanged").unwrap();
let (parent, leaf) =
super::super::ensure_parent_dirs_scoped(&super::super::ScopedMutationTarget {
root: workspace.path().to_path_buf(),
relative: PathBuf::from(".harn-tmp/.gitignore"),
})
.unwrap();
let original = workspace.path().join("original-temp");
std::fs::rename(workspace.path().join(".harn-tmp"), &original).unwrap();
std::os::unix::fs::symlink(outside.path(), workspace.path().join(".harn-tmp")).unwrap();
write_scoped_self_ignore(&parent, &leaf).unwrap();
assert_eq!(
std::fs::read_to_string(original.join(".gitignore")).unwrap(),
crate::runtime_paths::SELF_IGNORE_CONTENTS
);
assert!(!outside.path().join(".gitignore").exists());
assert_eq!(
std::fs::read_to_string(outside.path().join("marker")).unwrap(),
"host-unchanged"
);
}
#[test]
fn process_env_is_workspace_local_and_self_ignored() {
let workspace = tempfile::tempdir().unwrap();
let policy = policy(workspace.path());
let mut env = Vec::new();
inject_workspace_process_env(&mut env, &policy);
let env: BTreeMap<_, _> = env.into_iter().collect();
let workspace = workspace.path().canonicalize().unwrap();
let cache = workspace.join(WORKSPACE_TOOLCHAIN_CACHE_NAME);
let tmp = workspace.join(WORKSPACE_TMPDIR_NAME);
for key in [
"XDG_CACHE_HOME",
"GOCACHE",
"GOMODCACHE",
"GOPATH",
"CARGO_TARGET_DIR",
"PIP_CACHE_DIR",
"UV_CACHE_DIR",
"NPM_CONFIG_CACHE",
"YARN_CACHE_FOLDER",
"PNPM_HOME",
"CCACHE_DIR",
"CCACHE_TEMPDIR",
] {
let value = PathBuf::from(env.get(key).unwrap());
assert!(value.starts_with(&cache), "{key} escaped cache: {value:?}");
}
for key in ["HOME", "PYTHONUSERBASE"] {
assert!(
!env.contains_key(key),
"{key} must NOT be relocated; the child inherits the real one"
);
}
assert!(
!env.contains_key("NPM_CONFIG_STORE_DIR"),
"pnpm storage follows the isolated HOME/XDG roots; a global npm_config store-dir leaks an unsupported option into npm"
);
assert!(PathBuf::from(env.get("CARGO_HOME").unwrap()).is_absolute());
assert!(PathBuf::from(env.get("RUSTUP_HOME").unwrap()).is_absolute());
let harn_cache = PathBuf::from(env.get("HARN_CACHE_DIR").unwrap());
assert!(harn_cache.is_absolute());
assert!(
!harn_cache.starts_with(&cache),
"HARN_CACHE_DIR followed the relocated HOME into the workspace: {harn_cache:?}"
);
assert_eq!(
Some(harn_cache),
crate::user_dirs::package_cache_dir(),
"the child must be handed the same cache root the host resolves"
);
for key in TMPDIR_ENV_KEYS {
assert_eq!(
env.get(key).map(String::as_str),
Some(tmp.to_str().unwrap())
);
}
for root in [cache, tmp] {
let ignore = std::fs::read_to_string(root.join(".gitignore")).unwrap();
assert!(ignore.lines().any(|line| line.trim() == "*"));
}
}
#[test]
fn explicit_values_win_and_unrestricted_is_a_noop() {
let workspace = tempfile::tempdir().unwrap();
let mut env = vec![
("HOME".to_string(), "/caller/home".to_string()),
("GOCACHE".to_string(), "/caller/go".to_string()),
("TMPDIR".to_string(), "/caller/tmp".to_string()),
];
inject_workspace_process_env(&mut env, &policy(workspace.path()));
let env: BTreeMap<_, _> = env.into_iter().collect();
assert_eq!(env.get("HOME").map(String::as_str), Some("/caller/home"));
assert_eq!(env.get("GOCACHE").map(String::as_str), Some("/caller/go"));
assert_eq!(env.get("TMPDIR").map(String::as_str), Some("/caller/tmp"));
let unrestricted = CapabilityPolicy {
sandbox_profile: SandboxProfile::Unrestricted,
workspace_roots: vec![workspace.path().display().to_string()],
..CapabilityPolicy::default()
};
let mut env = Vec::new();
inject_workspace_process_env(&mut env, &unrestricted);
assert!(env.is_empty());
}
#[test]
fn explicit_package_cache_root_is_forwarded_exactly() {
let workspace = tempfile::tempdir().unwrap();
let cache = workspace.path().join("operator-cache");
let env: BTreeMap<_, _> = workspace_toolchain_env_with_package_cache(
&policy(workspace.path()),
Some(cache.clone()),
)
.into_iter()
.collect();
assert_eq!(
env.get("HARN_CACHE_DIR"),
Some(&cache.display().to_string())
);
assert_ne!(
env.get("HARN_CACHE_DIR"),
Some(&cache.join("harn").display().to_string()),
"HARN_CACHE_DIR is already the cache root, not an XDG base"
);
}
#[test]
fn the_npmrc_stand_in_keeps_settings_and_drops_every_credential_form() {
let raw = "registry=https://registry.example.test/\n\
@acme:registry=https://npm.acme.test/\n\
//npm.acme.test/:_authToken=secret-token\n\
//npm.acme.test/:username=alice\n\
//registry.npmjs.org/:_auth=YWxpY2U6aHVudGVyMg==\n\
_authToken=legacy-secret\n\
_password=hunter2\n\
store-dir=/opt/pnpm-store\n\
always-auth=true\n";
let sanitized = sanitize_npmrc(raw);
assert_eq!(
sanitized,
"registry=https://registry.example.test/\n\
@acme:registry=https://npm.acme.test/\n\
store-dir=/opt/pnpm-store\n\
always-auth=true\n"
);
for secret in ["secret-token", "hunter2", "YWxpY2U", "alice"] {
assert!(!sanitized.contains(secret), "{secret} leaked: {sanitized}");
}
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join(".npmrc");
let stand_in = dir.path().join("cache/npm/npmrc");
assert_eq!(seed_sanitized_npmrc(&source, &stand_in), stand_in);
assert!(
!stand_in.exists(),
"no source, no file: npm treats absent as empty"
);
std::fs::write(&source, raw).unwrap();
seed_sanitized_npmrc(&source, &stand_in);
assert_eq!(std::fs::read_to_string(&stand_in).unwrap(), sanitized);
let written = std::fs::metadata(&stand_in).unwrap().modified().unwrap();
seed_sanitized_npmrc(&source, &stand_in);
assert_eq!(
std::fs::metadata(&stand_in).unwrap().modified().unwrap(),
written,
"an unchanged stand-in is not rewritten"
);
}
#[test]
fn a_confined_child_never_reads_npm_config_from_the_denied_home_file() {
let workspace = tempfile::tempdir().unwrap();
let env: BTreeMap<_, _> =
workspace_toolchain_env_with_package_cache(&policy(workspace.path()), None)
.into_iter()
.collect();
let Some(userconfig) = env.get("NPM_CONFIG_USERCONFIG") else {
return; };
let cache = workspace
.path()
.canonicalize()
.unwrap()
.join(WORKSPACE_TOOLCHAIN_CACHE_NAME);
assert!(
PathBuf::from(userconfig).starts_with(&cache),
"userconfig must be the workspace stand-in, not ~/.npmrc: {userconfig}"
);
assert!(
PathBuf::from(env.get("COMPOSER_CACHE_DIR").unwrap()).starts_with(&cache),
"composer cache is a pure cache and relocates"
);
let composer_home = PathBuf::from(env.get("COMPOSER_HOME").unwrap());
assert!(
composer_home.starts_with(&cache),
"COMPOSER_HOME must be the workspace stand-in: {composer_home:?}"
);
assert!(
!composer_home.join("auth.json").exists(),
"the stand-in must never carry auth.json"
);
}
#[test]
fn a_loopback_grant_pins_the_jvm_to_the_ipv4_stack_on_macos() {
let workspace = tempfile::tempdir().unwrap();
let mut policy = policy(workspace.path());
let mut env = Vec::new();
inject_workspace_process_env(&mut env, &policy);
assert!(
!env.iter().any(|(key, _)| key == "JAVA_TOOL_OPTIONS"),
"no loopback grant, no JVM option"
);
policy.process_sandbox.allow_tcp_loopback = true;
let mut env = vec![("JAVA_TOOL_OPTIONS".to_string(), "-Xmx1g".to_string())];
inject_workspace_process_env(&mut env, &policy);
let value = env
.iter()
.find(|(key, _)| key == "JAVA_TOOL_OPTIONS")
.map(|(_, value)| value.clone())
.unwrap();
if cfg!(target_os = "macos") {
assert_eq!(value, format!("-Xmx1g {JVM_PREFER_IPV4_STACK}"));
} else {
assert_eq!(value, "-Xmx1g", "only the macOS seatbelt needs the pin");
}
let mut env = vec![(
"JAVA_TOOL_OPTIONS".to_string(),
"-Djava.net.preferIPv6Addresses=true".to_string(),
)];
inject_workspace_process_env(&mut env, &policy);
assert_eq!(
env.iter()
.filter(|(key, _)| key == "JAVA_TOOL_OPTIONS")
.count(),
1
);
assert_eq!(env[0].1, "-Djava.net.preferIPv6Addresses=true");
}
}