use std::collections::BTreeSet;
use regex::Regex;
use tracing::warn;
use crate::policy::sandbox_types::{Cap, RuleEffect, SandboxPolicy};
use super::formatter::BlockedPath;
use super::{FS_ERROR_PATTERNS, MAX_REPORTED_PATHS, is_noise_path, suggest_parent_directory};
pub(crate) fn contains_fs_error(text: &str) -> bool {
let lower = text.to_lowercase();
FS_ERROR_PATTERNS
.iter()
.any(|pattern| lower.contains(pattern))
}
pub(crate) fn extract_paths_from_errors(text: &str) -> Vec<String> {
let patterns = [
r"(?:open|stat|read|write|mkdir|access|unlink|rename|chmod|chown|lstat|readlink|creat|opendir)\s+(/[^\s:]+):\s*(?i:operation not permitted|permission denied)",
r"(?i:operation not permitted|permission denied):\s*'(/[^']+)'",
r"'(/[^']+)':\s*(?i:operation not permitted|permission denied)",
r"(?i:EACCES|EPERM):\s*(?:permission denied|operation not permitted),?\s*\w+\s*'([^']+)'",
r"(/(?:[^\s:])+):\s*(?:Permission denied|Operation not permitted)",
];
let mut paths = Vec::new();
let mut seen = BTreeSet::new();
for pattern in &patterns {
let re = match Regex::new(pattern) {
Ok(re) => re,
Err(e) => {
warn!(pattern = pattern, error = %e, "Failed to compile path extraction regex");
continue;
}
};
for cap in re.captures_iter(text) {
if let Some(m) = cap.get(1) {
let path = m.as_str().to_string();
if path.starts_with('/') && seen.insert(path.clone()) {
paths.push(path);
}
}
}
}
paths
}
pub(crate) fn extract_blocked_paths(
text: &str,
sandbox: &SandboxPolicy,
cwd: &str,
) -> Vec<BlockedPath> {
let paths = extract_paths_from_errors(text);
let mut blocked = Vec::new();
let mut seen_dirs = BTreeSet::new();
for path in paths {
if is_noise_path(&path) || !is_likely_sandbox_violation(&path, sandbox, cwd) {
continue;
}
let dir = suggest_parent_directory(&path);
if seen_dirs.insert(dir.clone()) {
blocked.push(BlockedPath {
current_caps: sandbox.effective_caps(&path, cwd),
path,
suggested_dir: dir,
});
}
if blocked.len() >= MAX_REPORTED_PATHS {
break;
}
}
blocked
}
pub(crate) fn is_likely_sandbox_violation(path: &str, sandbox: &SandboxPolicy, cwd: &str) -> bool {
let caps = sandbox.effective_caps(path, cwd);
let missing_write_or_create = !caps.contains(Cap::WRITE) || !caps.contains(Cap::CREATE);
let under_explicit_allow = sandbox.rules.iter().any(|rule| {
if rule.effect != RuleEffect::Allow {
return false;
}
let resolved = SandboxPolicy::resolve_path(&rule.path, cwd);
path.starts_with(&resolved)
});
missing_write_or_create && !under_explicit_allow
}