use crate::diff::entry::{DiffEntry, LARGE_FILE_THRESHOLD, fmt_size};
use crate::config::definition::{AuditEntry, AuditStrategy};
#[derive(Debug, Clone)]
pub enum AuditWarning {
LargeFileStrategy {
path: String,
strategy: &'static str,
size_bytes: u64,
},
NoStrategyOnModified { path: String },
NoApprover { path: String },
}
impl AuditWarning {
pub fn message(&self) -> String {
match self {
AuditWarning::LargeFileStrategy { path, strategy, size_bytes } =>
format!(
"{path}: {strategy} strategy applied to a large file ({}). \
Consider using Checksum instead.",
fmt_size(*size_bytes)
),
AuditWarning::NoStrategyOnModified { path } =>
format!("{path}: Modified file uses `None` strategy — content not verified."),
AuditWarning::NoApprover { path } =>
format!("{path}: Entry has no `approved_by` field — approver unknown."),
}
}
pub fn kind(&self) -> &'static str {
match self {
AuditWarning::LargeFileStrategy { .. } => "large-file",
AuditWarning::NoStrategyOnModified { .. } => "no-strategy",
AuditWarning::NoApprover { .. } => "no-approver",
}
}
}
pub fn collect(diff: &DiffEntry, entry: &AuditEntry) -> Vec<AuditWarning> {
let mut warns = Vec::new();
let size = diff.after_size.or(diff.before_size).unwrap_or(0);
if size > LARGE_FILE_THRESHOLD {
match &entry.strategy {
AuditStrategy::Exact { .. } | AuditStrategy::LineMatch { .. } => {
warns.push(AuditWarning::LargeFileStrategy {
path: diff.path.clone(),
strategy: entry.strategy.label(),
size_bytes: size,
});
}
_ => {}
}
}
if diff.diff_type == crate::diff::entry::DiffType::Modified {
if let AuditStrategy::None = entry.strategy {
warns.push(AuditWarning::NoStrategyOnModified { path: diff.path.clone() });
}
}
if entry.approved_by.is_none() && !entry.reason.trim().is_empty() {
warns.push(AuditWarning::NoApprover { path: diff.path.clone() });
}
warns
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diff::entry::{DiffEntry, DiffType};
use crate::config::definition::{AuditEntry, AuditStrategy, LineRule, LineAction};
fn make_diff(path: &str, size: u64, diff_type: DiffType) -> DiffEntry {
DiffEntry {
path: path.to_string(), diff_type, is_dir: false,
before_text: None, after_text: None,
is_binary: false,
before_size: Some(size), after_size: Some(size),
before_sha256: None, after_sha256: None,
stats: None, error_detail: None,
}
}
fn make_entry(strategy: AuditStrategy) -> AuditEntry {
AuditEntry {
path: "f.txt".to_string(),
diff_type: DiffType::Modified,
reason: "test".to_string(),
strategy,
enabled: true,
ticket: None,
approved_by: None,
approved_at: None,
expires_at: None,
note: None,
created_at: None,
updated_at: None,
}
}
#[test]
fn large_file_with_linematch_warns() {
let diff = make_diff("big.txt", 2 * 1024 * 1024, DiffType::Modified);
let entry = make_entry(AuditStrategy::LineMatch {
rules: vec![LineRule { action: LineAction::Added, line: "x".into() }],
});
let warns = collect(&diff, &entry);
assert!(warns.iter().any(|w| matches!(w, AuditWarning::LargeFileStrategy { .. })),
"large LineMatch should warn");
}
#[test]
fn large_file_with_checksum_no_warn() {
let diff = make_diff("big.bin", 2 * 1024 * 1024, DiffType::Modified);
let entry = make_entry(AuditStrategy::Checksum { expected_sha256: "a".repeat(64) });
let warns = collect(&diff, &entry);
assert!(!warns.iter().any(|w| matches!(w, AuditWarning::LargeFileStrategy { .. })),
"Checksum on large file should not warn");
}
#[test]
fn none_strategy_on_modified_warns() {
let diff = make_diff("cfg.toml", 100, DiffType::Modified);
let entry = make_entry(AuditStrategy::None);
let warns = collect(&diff, &entry);
assert!(warns.iter().any(|w| matches!(w, AuditWarning::NoStrategyOnModified { .. })));
}
#[test]
fn none_strategy_on_added_no_warn() {
let diff = make_diff("new.txt", 100, DiffType::Added);
let mut entry = make_entry(AuditStrategy::None);
entry.diff_type = DiffType::Added;
let warns = collect(&diff, &entry);
assert!(!warns.iter().any(|w| matches!(w, AuditWarning::NoStrategyOnModified { .. })));
}
#[test]
fn no_approver_warns() {
let diff = make_diff("f.txt", 100, DiffType::Modified);
let entry = make_entry(AuditStrategy::None);
let warns = collect(&diff, &entry);
assert!(warns.iter().any(|w| matches!(w, AuditWarning::NoApprover { .. })));
}
#[test]
fn with_approver_no_warn() {
let diff = make_diff("f.txt", 100, DiffType::Modified);
let mut entry = make_entry(AuditStrategy::None);
entry.approved_by = Some("alice".into());
let warns = collect(&diff, &entry);
assert!(!warns.iter().any(|w| matches!(w, AuditWarning::NoApprover { .. })));
}
}