use crate::{core::context, core::error::TookaError, rules::contradiction, rules::rule::Rule};
use serde::{Deserialize, Serialize};
use std::{
fs,
io::Read,
path::{Path, PathBuf},
};
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct RulesFile {
pub rules: Vec<Rule>,
}
impl RulesFile {
pub fn load() -> Result<Self, TookaError> {
log::debug!("Loading rules from file");
let path = Self::rules_file_path()?;
if !path.exists() {
log::warn!(
"Rules file does not exist: {}, creating new one",
path.display()
);
let empty = Self::default();
Self::write_to_file(&path, &empty)?;
return Ok(empty);
}
if !path.is_file() {
return Err(TookaError::ConfigError(format!(
"Rules file is not a regular file: {}",
path.display()
)));
}
let content = fs::read_to_string(&path)?;
let rules: Self = serde_yaml::from_str(&content)?;
log::debug!("Successfully loaded {} rules", rules.rules.len());
Ok(rules)
}
pub fn save(&self) -> Result<(), TookaError> {
log::debug!("Saving rules to file");
let path = Self::rules_file_path()?;
Self::write_to_file(&path, self)?;
log::debug!("Saved {} rules to {}", self.rules.len(), path.display());
Ok(())
}
pub fn add_rule_from_file(
&mut self,
file_path: &str,
overwrite: bool,
) -> Result<(), TookaError> {
log::debug!("Adding rule(s) from file: {file_path}");
let mut content = String::new();
fs::File::open(file_path)?.read_to_string(&mut content)?;
if content.trim_start().starts_with("rules:") {
self.add_multiple_rules(&content, overwrite)
} else {
self.add_single_rule(&content, overwrite)
}
}
fn add_single_rule(&mut self, yaml: &str, overwrite: bool) -> Result<(), TookaError> {
let rule: Rule = serde_yaml::from_str(yaml)?;
log::debug!("Parsed new rule: {rule:?}");
rule.validate(true)?;
let self_conflicts = contradiction::check_self_contradiction(&rule);
for conflict in self_conflicts {
if conflict.level == contradiction::ConflictLevel::SelfContradiction {
return Err(TookaError::InvalidRule(format!(
"Rule '{}' has a self-contradiction: {}",
rule.id, conflict.message
)));
}
}
let rule_conflicts = contradiction::check_rule_conflicts(&rule, &self.rules);
if let Some(pos) = self.rules.iter().position(|r| r.id == rule.id) {
if overwrite {
self.rules[pos] = rule;
self.save()?;
Self::log_conflicts(&rule_conflicts);
return Ok(());
}
return Err(TookaError::InvalidRule(format!(
"Rule ID '{}' already exists",
rule.id
)));
}
self.rules.push(rule);
self.save()?;
Self::log_conflicts(&rule_conflicts);
Ok(())
}
fn add_multiple_rules(&mut self, yaml: &str, overwrite: bool) -> Result<(), TookaError> {
let parsed: RulesFile = serde_yaml::from_str(yaml)?;
for rule in parsed.rules {
log::debug!("Parsed rule: {rule:?}");
rule.validate(true)?;
let self_conflicts = contradiction::check_self_contradiction(&rule);
for conflict in self_conflicts {
if conflict.level == contradiction::ConflictLevel::SelfContradiction {
return Err(TookaError::InvalidRule(format!(
"Rule '{}' has a self-contradiction: {}",
rule.id, conflict.message
)));
}
}
let rule_conflicts = contradiction::check_rule_conflicts(&rule, &self.rules);
if let Some(pos) = self.rules.iter().position(|r| r.id == rule.id) {
if overwrite {
self.rules[pos] = rule;
Self::log_conflicts(&rule_conflicts);
} else {
return Err(TookaError::InvalidRule(format!(
"Rule ID '{}' already exists",
rule.id
)));
}
} else {
self.rules.push(rule);
Self::log_conflicts(&rule_conflicts);
}
}
self.save()?;
Ok(())
}
pub fn remove_rule(&mut self, rule_id: &str) -> Result<(), TookaError> {
log::debug!("Removing rule with id: {rule_id}");
if let Some(pos) = self.rules.iter().position(|r| r.id == rule_id) {
self.rules.remove(pos);
self.save()?;
log::debug!("Successfully removed rule with id: {rule_id}");
Ok(())
} else {
Err(TookaError::RuleNotFound(format!(
"Rule with id '{rule_id}' not found"
)))
}
}
pub fn find_rule(&self, rule_id: &str) -> Option<Rule> {
log::debug!("Finding rule with id: {rule_id}");
self.rules.iter().find(|r| r.id == rule_id).cloned()
}
pub fn export_rule(&self, rule_id: &str, out_path: Option<&str>) -> Result<(), TookaError> {
log::debug!(
"Exporting rule with id: {} to {}",
rule_id,
out_path.unwrap_or("stdout")
);
if let Some(rule) = self.rules.iter().find(|r| r.id == rule_id) {
let content = serde_yaml::to_string(rule)?;
if let Some(path) = out_path {
fs::write(path, content)?;
log::debug!("Exported rule {rule_id} to {path}");
} else {
println!("{content}");
log::debug!("Exported rule {rule_id} to stdout");
}
Ok(())
} else {
Err(TookaError::RuleNotFound(format!(
"Rule with id '{rule_id}' not found"
)))
}
}
pub fn list_rules(&self) -> Vec<Rule> {
log::debug!("Listing all rules");
self.rules.clone()
}
pub fn toggle_rule(&mut self, rule_id: &str) -> Result<(), TookaError> {
log::debug!("Toggling rule with id: {rule_id}");
if let Some(rule) = self.rules.iter_mut().find(|r| r.id == rule_id) {
rule.enabled = !rule.enabled;
self.save()?;
log::debug!("Successfully toggled rule with id: {rule_id}");
Ok(())
} else {
Err(TookaError::RuleNotFound(format!(
"Rule with id '{rule_id}' not found"
)))
}
}
pub fn optimized_with_filter(self, rule_filter: Option<&[String]>) -> Result<Self, TookaError> {
let filtered_rules = if let Some(rule_ids) = rule_filter {
let mut filtered = Vec::with_capacity(rule_ids.len());
for rule_id in rule_ids {
if let Some(rule) = self.rules.iter().find(|r| &r.id == rule_id) {
filtered.push(rule.clone());
} else {
return Err(TookaError::RuleNotFound(format!(
"Rule with id '{rule_id}' not found"
)));
}
}
filtered
} else {
self.rules
};
let enabled_rules: Vec<Rule> = filtered_rules
.into_iter()
.filter(|rule| rule.enabled)
.collect();
if enabled_rules.is_empty() {
return Err(TookaError::RuleNotFound(
"No enabled rules found to apply.".to_string(),
));
}
let mut indexed_rules: Vec<(usize, Rule)> = enabled_rules.into_iter().enumerate().collect();
indexed_rules.sort_by(|a, b| b.1.priority.cmp(&a.1.priority).then(a.0.cmp(&b.0)));
Ok(Self {
rules: indexed_rules.into_iter().map(|(_, rule)| rule).collect(),
})
}
fn rules_file_path() -> Result<PathBuf, TookaError> {
let config = context::get_locked_config()
.map_err(|e| TookaError::ConfigError(format!("Failed to get config: {e}")))?;
Ok(Path::new(&config.rules_file).to_path_buf())
}
fn write_to_file(path: &Path, rules: &Self) -> Result<(), TookaError> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let file = fs::File::create(path)?;
serde_yaml::to_writer(file, rules)?;
Ok(())
}
fn log_conflicts(conflicts: &[contradiction::Conflict]) {
for conflict in conflicts {
match conflict.level {
contradiction::ConflictLevel::SelfContradiction => {
log::error!(
"Self-contradiction in rule '{}': {}",
conflict.rule_id,
conflict.message
);
}
contradiction::ConflictLevel::PotentialConflict => {
log::warn!(
"Potential conflict in rule '{}': {}",
conflict.rule_id,
conflict.message
);
}
contradiction::ConflictLevel::Overlap => {
log::info!(
"Rule overlap detected for '{}': {}",
conflict.rule_id,
conflict.message
);
}
}
}
}
}