use std::path::{Path, PathBuf};
use harn_vm::orchestration::{CapabilityPolicy, ProcessSandboxPolicy, SandboxProfile};
pub(crate) fn policy(case_root: &Path) -> CapabilityPolicy {
case_policy(case_root)
}
pub(crate) fn normalize_owned_root(case_root: &Path) -> std::io::Result<PathBuf> {
case_root.canonicalize()
}
fn case_policy(case_root: &Path) -> CapabilityPolicy {
let mut process_roots = runner_roots();
process_roots.push(root_string(case_root));
process_roots.sort();
process_roots.dedup();
CapabilityPolicy {
workspace_roots: vec![root_string(case_root)],
read_only_roots: filesystem_roots(),
process_sandbox: ProcessSandboxPolicy {
read_roots: process_roots.clone(),
write_roots: process_roots,
..ProcessSandboxPolicy::default()
},
sandbox_profile: SandboxProfile::WorkspacePaths,
..CapabilityPolicy::default()
}
}
fn runner_roots() -> Vec<String> {
std::env::current_dir()
.map(|dir| vec![root_string(&dir)])
.unwrap_or_default()
}
fn filesystem_roots() -> Vec<String> {
let anchors = [
std::env::current_dir().ok(),
Some(std::env::temp_dir()),
std::env::current_exe().ok(),
];
let mut roots: Vec<String> = anchors
.into_iter()
.flatten()
.filter_map(|path| path.ancestors().last().map(Path::to_path_buf))
.map(|root| root.display().to_string())
.collect();
roots.sort();
roots.dedup();
roots
}
fn root_string(path: &Path) -> String {
path.display().to_string()
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
#[test]
fn the_case_owns_its_root_but_not_its_fixture_directory() {
let policy = case_policy(Path::new("/tmp/harn-conformance-case-x"));
assert!(policy
.workspace_roots
.iter()
.any(|root| root == "/tmp/harn-conformance-case-x"));
assert!(
!policy
.workspace_roots
.iter()
.any(|root| root.contains("conformance/tests")),
"a fixture directory is not the case's to write: {:?}",
policy.workspace_roots
);
}
#[test]
fn a_checkout_below_the_host_temp_root_is_not_writable() {
let host_temp = tempfile::tempdir().expect("host temp root");
let case_root = host_temp.path().join("case-root");
let checkout = host_temp.path().join("runner-checkout");
let policy = case_policy(&case_root);
assert!(
policy
.workspace_roots
.iter()
.all(|root| !checkout.starts_with(Path::new(root))),
"an ambient temp root must not grant an unrelated checkout: {:?}",
policy.workspace_roots
);
}
#[test]
fn owned_root_is_normalized_before_it_becomes_authority() {
let host_temp = tempfile::tempdir().expect("host temp root");
let case_root = host_temp.path().join("case-root");
let alias_parent = host_temp.path().join("alias-parent");
std::fs::create_dir_all(&case_root).expect("case root");
std::fs::create_dir_all(&alias_parent).expect("alias parent");
let aliased = alias_parent.join("..").join("case-root");
assert_eq!(
normalize_owned_root(&aliased).expect("normalize owned root"),
case_root.canonicalize().expect("canonical case root")
);
}
#[test]
fn reads_are_not_confined() {
let policy = case_policy(Path::new("/state/.harn"));
assert!(
!policy.read_only_roots.is_empty(),
"a case must still be able to read the repository and the toolchain"
);
for root in &policy.read_only_roots {
assert_eq!(
Path::new(root).ancestors().last().map(Path::to_path_buf),
Some(PathBuf::from(root)),
"read roots are filesystem roots, not curated directories"
);
}
}
#[test]
fn subprocesses_may_still_start_from_the_runners_directory() {
let case_root = Path::new("/case-root");
let policy = case_policy(case_root);
let cwd = root_string(&std::env::current_dir().unwrap());
assert!(
policy.process_sandbox.read_roots.contains(&cwd)
&& policy.process_sandbox.write_roots.contains(&cwd),
"cases shell out from the runner's directory: {:?}",
policy.process_sandbox
);
let case_root = root_string(case_root);
assert!(
policy.process_sandbox.read_roots.contains(&case_root)
&& policy.process_sandbox.write_roots.contains(&case_root),
"cases shell out from their owned scratch root: {:?}",
policy.process_sandbox
);
assert!(
!policy.workspace_roots.contains(&cwd),
"the runner's directory is launchable, not writable: {:?}",
policy.workspace_roots
);
}
#[test]
fn the_profile_confines_writes_but_not_subprocesses() {
let profile = case_policy(Path::new("/state/.harn")).sandbox_profile;
assert!(
profile.enforces_path_scope(),
"a stray write is the failure this module exists to catch"
);
assert!(
!profile.confines_processes(),
"cases legitimately shell out; OS confinement would deny them \
without closing the write hole"
);
}
}