use crate::ast::{FieldValue, PgnPacket};
use crate::registry::{ActionRegistry, SafetyRules, WorkflowRegistry};
use crate::resolver::ResolvedRef;
#[derive(Debug, Clone, PartialEq)]
pub enum SafetyRuleId {
Sg1,
Sg2,
Sg3,
Sg4,
Sg5,
Sg6,
Sg7,
Sg8,
Sg9,
}
impl std::fmt::Display for SafetyRuleId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SafetyRuleId::Sg1 => write!(f, "SG-1"),
SafetyRuleId::Sg2 => write!(f, "SG-2"),
SafetyRuleId::Sg3 => write!(f, "SG-3"),
SafetyRuleId::Sg4 => write!(f, "SG-4"),
SafetyRuleId::Sg5 => write!(f, "SG-5"),
SafetyRuleId::Sg6 => write!(f, "SG-6"),
SafetyRuleId::Sg7 => write!(f, "SG-7"),
SafetyRuleId::Sg8 => write!(f, "SG-8"),
SafetyRuleId::Sg9 => write!(f, "SG-9"),
}
}
}
#[derive(Debug, Clone)]
pub struct SafetyResult {
pub allowed: bool,
pub blocked: bool,
pub fired_rules: Vec<SafetyRuleId>,
pub human_required: bool,
pub effective_risk: String,
}
#[derive(Debug, Clone)]
pub struct BlockReason {
pub rule: SafetyRuleId,
pub message: String,
}
pub fn check_safety(
packet: &PgnPacket,
action_registry: &ActionRegistry,
safety_rules: &SafetyRules,
workflows: &WorkflowRegistry,
) -> SafetyResult {
let mut fired_rules = Vec::new();
let mut human_required = false;
let effective_risk = compute_effective_risk(packet, workflows);
if let (Some(FieldValue::List(do_list)), Some(FieldValue::List(denied_list))) =
(packet.fields.get("do"), packet.fields.get("deny"))
{
for action in do_list {
if denied_list.contains(action) {
fired_rules.push(SafetyRuleId::Sg1);
break;
}
}
}
if let Some(FieldValue::List(do_list)) = packet.fields.get("do") {
let human_field = packet.fields.get("human");
let has_human_yes = matches!(human_field, Some(FieldValue::Scalar(s)) if s == "yes");
for action in do_list {
if action_registry.human_gated.contains(action) && !has_human_yes {
fired_rules.push(SafetyRuleId::Sg2);
break;
}
}
}
if effective_risk == "high" || effective_risk == "crit" {
human_required = true;
if let Some(FieldValue::Scalar(human)) = packet.fields.get("human")
&& human == "no"
{
fired_rules.push(SafetyRuleId::Sg3);
}
}
if let Some(FieldValue::List(references)) = packet.fields.get("in") {
for reference in references {
for pattern in &safety_rules.private_paths {
if reference.contains(pattern) || reference_matches_pattern(reference, pattern) {
fired_rules.push(SafetyRuleId::Sg4);
break;
}
}
}
}
if let Some(FieldValue::Scalar(wf_name)) = packet.fields.get("wf")
&& !workflows.workflows.contains_key(wf_name)
{
fired_rules.push(SafetyRuleId::Sg5);
}
if let (Some(FieldValue::Scalar(wf_name)), Some(FieldValue::Scalar(mode))) =
(packet.fields.get("wf"), packet.fields.get("mode"))
&& let Some(workflow) = workflows.workflows.get(wf_name)
&& !workflow.allowed_modes.contains(mode)
{
fired_rules.push(SafetyRuleId::Sg6);
}
let blocked = !fired_rules.is_empty();
let allowed = !blocked;
SafetyResult {
allowed,
blocked,
fired_rules,
human_required,
effective_risk,
}
}
fn compute_effective_risk(packet: &PgnPacket, workflows: &WorkflowRegistry) -> String {
let declared_risk = packet
.fields
.get("risk")
.and_then(|v| match v {
FieldValue::Scalar(s) => Some(s.as_str()),
_ => None,
})
.unwrap_or("med");
let workflow_default = packet
.fields
.get("wf")
.and_then(|v| match v {
FieldValue::Scalar(s) => workflows.workflows.get(s),
_ => None,
})
.map(|w| w.risk_default.as_str())
.unwrap_or("med");
if packet.fields.contains_key("risk") {
declared_risk.to_string()
} else {
workflow_default.to_string()
}
}
fn reference_matches_pattern(reference: &str, pattern: &str) -> bool {
if let Some(suffix) = pattern.strip_prefix("**/") {
reference.contains(suffix)
} else if let Some(prefix) = pattern.strip_suffix(".*") {
reference.starts_with(prefix)
} else if pattern.starts_with('.') {
reference == pattern || reference.starts_with(&format!("{}.", pattern))
} else {
reference == pattern
}
}
pub fn check_resolved_refs_safety(
resolved: &[ResolvedRef],
private_paths: &[String],
) -> Vec<SafetyRuleId> {
let mut fired = Vec::new();
for r in resolved {
let path_str = r.resolved_path.as_ref().map(|p| p.display().to_string());
let check_str = path_str.as_deref().unwrap_or(&r.ref_id);
for pattern in private_paths {
if check_str.contains(pattern) || reference_matches_pattern(check_str, pattern) {
fired.push(SafetyRuleId::Sg4);
break;
}
}
}
fired
}