use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Severity {
Info,
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Category {
FontIntegrity,
UnicodeHygiene,
HiddenContent,
Structural,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Verdict {
Confirmed,
Refuted,
Unconfirmed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Finding {
pub rule: String,
pub severity: Severity,
pub category: Category,
pub message: String,
pub location: String,
pub confidence: f32,
}
impl Finding {
pub fn new(
rule: &str,
severity: Severity,
category: Category,
location: impl Into<String>,
message: impl Into<String>,
confidence: f32,
) -> Self {
Finding {
rule: rule.to_string(),
severity,
category,
location: location.into(),
message: message.into(),
confidence,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Report {
pub file: String,
pub format: String,
pub fonts_examined: usize,
pub findings: Vec<Finding>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub phrases: Vec<PhraseDiff>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verdict: Option<Verdict>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PhraseDiff {
pub extracted: String,
pub presumed: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ocr: Option<String>,
}
impl Report {
pub fn new(file: &str, format: &str) -> Self {
Report {
file: file.to_string(),
format: format.to_string(),
fonts_examined: 0,
findings: Vec::new(),
phrases: Vec::new(),
verdict: None,
}
}
pub fn push(&mut self, f: Finding) {
self.findings.push(f);
}
pub fn max_severity(&self) -> Option<Severity> {
self.findings.iter().map(|f| f.severity).max()
}
}
pub struct OutlineScan {
pub findings: Vec<Finding>,
pub substitutions: Vec<(char, char)>,
}
pub fn clean_snippet(text: &str, max_chars: usize) -> String {
let collapsed: String = text.split_whitespace().collect::<Vec<_>>().join(" ");
if collapsed.chars().count() > max_chars {
let mut s: String = collapsed.chars().take(max_chars).collect();
s.push('…');
s
} else {
collapsed
}
}
pub fn apply_substitutions(text: &str, subs: &[(char, char)]) -> String {
use std::collections::HashMap;
let map: HashMap<char, char> = subs.iter().copied().collect();
text.chars()
.map(|c| {
let lower = c.to_lowercase().next().unwrap_or(c);
match map.get(&lower) {
Some(&corr) if c.is_uppercase() => corr.to_uppercase().next().unwrap_or(corr),
Some(&corr) => corr,
None => c,
}
})
.collect()
}
pub fn split_sentences(text: &str) -> Vec<String> {
let mut out = Vec::new();
let mut cur = String::new();
for c in text.chars() {
cur.push(c);
if matches!(c, '.' | '!' | '?' | ';' | '\n' | '\r') {
let s = clean_snippet(&cur, 400);
if !s.is_empty() {
out.push(s);
}
cur.clear();
}
}
let s = clean_snippet(&cur, 400);
if !s.is_empty() {
out.push(s);
}
out
}
pub fn phrase_diffs(extracted_text: &str, subs: &[(char, char)]) -> Vec<PhraseDiff> {
split_sentences(extracted_text)
.into_iter()
.filter_map(|s| {
let presumed = apply_substitutions(&s, subs);
if presumed != s {
Some(PhraseDiff { extracted: s, presumed, ocr: None })
} else {
None
}
})
.take(100)
.collect()
}