use anyhow::{Result, bail};
use crate::policy::match_tree::{CompiledPolicy, Node};
pub trait EnvResolver {
fn resolve(&self, name: &str) -> Result<String>;
}
pub struct StdEnvResolver;
pub const UNAVAILABLE_SESSION_PATH: &str = "/dev/null/.clash-no-session";
const SESSION_VAR_DEFAULTS: &[(&str, &str)] = &[("TRANSCRIPT_DIR", UNAVAILABLE_SESSION_PATH)];
impl EnvResolver for StdEnvResolver {
fn resolve(&self, name: &str) -> Result<String> {
match std::env::var(name) {
Ok(val) => Ok(val),
Err(_) => {
for &(var, default) in SESSION_VAR_DEFAULTS {
if name == var {
return Ok(default.to_string());
}
}
anyhow::bail!("environment variable not set: {name}")
}
}
}
}
pub fn compile_to_tree(source: &str) -> Result<CompiledPolicy> {
compile_policy(source)
}
pub fn compile_multi_level_to_tree(
levels: &[(crate::settings::PolicyLevel, &str, &str)],
) -> Result<CompiledPolicy> {
if levels.is_empty() {
bail!("no policy levels to compile");
}
if levels.len() == 1 {
return compile_policy_with_source(levels[0].1, levels[0].2);
}
let mut sorted: Vec<(crate::settings::PolicyLevel, &str, &str)> = levels.to_vec();
sorted.sort_by(|a, b| b.0.cmp(&a.0));
let first: CompiledPolicy = serde_json::from_str(sorted[0].1)
.map_err(|e| anyhow::anyhow!("{} policy: invalid JSON: {}", sorted[0].0.name(), e))?;
let mut merged = CompiledPolicy {
sandboxes: first.sandboxes,
default_sandbox: first.default_sandbox,
tree: first.tree,
default_effect: first.default_effect,
};
let first_source = sorted[0].2;
for node in &mut merged.tree {
node.stamp_source(first_source);
}
for (level, src, path) in &sorted[1..] {
let mut policy: CompiledPolicy = serde_json::from_str(src)
.map_err(|e| anyhow::anyhow!("{} policy: invalid JSON: {}", level.name(), e))?;
for node in &mut policy.tree {
node.stamp_source(path);
}
merged.tree.extend(policy.tree);
for (k, v) in policy.sandboxes {
merged.sandboxes.entry(k).or_insert(v);
}
}
let errors = merged.validate();
if !errors.is_empty() {
bail!("match tree validation errors: {}", errors.join("; "));
}
merged.tree = Node::compact(merged.tree);
Ok(merged)
}
fn compile_policy(source: &str) -> Result<CompiledPolicy> {
compile_policy_with_source(source, "")
}
fn compile_policy_with_source(source: &str, source_path: &str) -> Result<CompiledPolicy> {
let mut policy: CompiledPolicy = serde_json::from_str(source)
.map_err(|e| anyhow::anyhow!("invalid match tree policy JSON: {e}"))?;
if !source_path.is_empty() {
for node in &mut policy.tree {
node.stamp_source(source_path);
}
}
let errors = policy.validate();
if !errors.is_empty() {
bail!("match tree validation errors: {}", errors.join("; "));
}
policy.tree = Node::compact(policy.tree);
Ok(policy)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compile_basic_policy() {
let source = r#"{
"schema_version": 5,
"default_effect": "deny",
"sandboxes": {},
"tree": [{
"condition": {
"observe": "tool_name",
"pattern": {"literal": {"literal": "Bash"}},
"children": [{"decision": {"allow": null}}]
}
}]
}"#;
let policy = compile_to_tree(source).unwrap();
assert_eq!(policy.tree.len(), 1);
assert_eq!(policy.default_effect, crate::policy::Effect::Deny);
}
#[test]
fn compile_with_internals() {
let source = r#"{
"schema_version": 5,
"default_effect": "deny",
"sandboxes": {},
"tree": [{
"condition": {
"observe": "tool_name",
"pattern": {"literal": {"literal": "Bash"}},
"children": [{"decision": {"allow": null}}]
}
}]
}"#;
let policy = compile_policy(source).unwrap();
assert_eq!(policy.tree.len(), 1);
}
#[test]
fn compile_valid_sandbox_reference() {
let source = r#"{
"schema_version": 5,
"default_effect": "deny",
"sandboxes": {
"dev": {
"default": ["read", "execute"],
"network": "deny"
}
},
"tree": [{
"condition": {
"observe": "tool_name",
"pattern": {"literal": {"literal": "Bash"}},
"children": [{"decision": {"allow": "dev"}}]
}
}]
}"#;
let policy = compile_to_tree(source);
assert!(policy.is_ok(), "valid sandbox reference should compile");
let policy = policy.unwrap();
assert!(policy.sandboxes.contains_key("dev"));
}
#[test]
fn compile_undefined_sandbox_reference_fails() {
let source = r#"{
"schema_version": 5,
"default_effect": "deny",
"sandboxes": {},
"tree": [{
"condition": {
"observe": "tool_name",
"pattern": {"literal": {"literal": "Bash"}},
"children": [{"decision": {"allow": "nonexistent"}}]
}
}]
}"#;
let result = compile_to_tree(source);
assert!(result.is_err(), "undefined sandbox reference should fail");
let err = result.unwrap_err().to_string();
assert!(
err.contains("nonexistent"),
"error should mention the undefined sandbox name, got: {err}"
);
}
}