#![forbid(unsafe_code)]
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Warning {
pub category: WarningCategory,
pub page: Option<usize>,
pub message: String,
pub spec_section: Option<&'static str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WarningCategory {
SpecViolation,
ToUnicodeMissing,
XrefRecovery,
OperatorCapExceeded,
Type3Font,
EofPremature,
Encryption,
Font,
Layout,
}
impl WarningCategory {
pub fn as_str(&self) -> &'static str {
match self {
Self::SpecViolation => "spec_violation",
Self::ToUnicodeMissing => "to_unicode_missing",
Self::XrefRecovery => "xref_recovery",
Self::OperatorCapExceeded => "operator_cap_exceeded",
Self::Type3Font => "type3_font",
Self::EofPremature => "eof_premature",
Self::Encryption => "encryption",
Self::Font => "font",
Self::Layout => "layout",
}
}
}
#[derive(Debug, Default)]
pub struct WarningSink {
warnings: Mutex<Vec<Warning>>,
}
static GLOBAL_WARNING_SINK: Mutex<Vec<Warning>> = Mutex::new(Vec::new());
pub fn push_global_warning(warning: Warning) {
if let Ok(mut v) = GLOBAL_WARNING_SINK.lock() {
v.push(warning);
}
}
pub fn drain_global_warnings() -> Vec<Warning> {
GLOBAL_WARNING_SINK
.lock()
.map(|mut v| std::mem::take(&mut *v))
.unwrap_or_default()
}
pub fn snapshot_global_warnings() -> Vec<Warning> {
GLOBAL_WARNING_SINK
.lock()
.map(|v| v.clone())
.unwrap_or_default()
}
impl WarningSink {
pub fn new() -> Self {
Self::default()
}
pub fn push(&self, warning: Warning) {
if let Ok(mut v) = self.warnings.lock() {
v.push(warning);
}
}
pub fn snapshot(&self) -> Vec<Warning> {
self.warnings.lock().map(|v| v.clone()).unwrap_or_default()
}
pub fn len(&self) -> usize {
self.warnings.lock().map(|v| v.len()).unwrap_or(0)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn clear(&self) {
if let Ok(mut v) = self.warnings.lock() {
v.clear();
}
}
pub fn extend(&self, warnings: impl IntoIterator<Item = Warning>) {
if let Ok(mut v) = self.warnings.lock() {
v.extend(warnings);
}
}
pub fn take(&self) -> Vec<Warning> {
if let Ok(mut v) = self.warnings.lock() {
std::mem::take(&mut *v)
} else {
Vec::new()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sink_starts_empty() {
let sink = WarningSink::new();
assert!(sink.is_empty());
assert_eq!(sink.len(), 0);
assert_eq!(sink.snapshot().len(), 0);
}
#[test]
fn push_and_snapshot() {
let sink = WarningSink::new();
sink.push(Warning {
category: WarningCategory::ToUnicodeMissing,
page: Some(0),
message: "Type0 font 'X' has no ToUnicode entry!".into(),
spec_section: Some("9.10.2"),
});
assert_eq!(sink.len(), 1);
let snap = sink.snapshot();
assert_eq!(snap[0].category, WarningCategory::ToUnicodeMissing);
assert_eq!(snap[0].page, Some(0));
assert!(snap[0].message.contains("ToUnicode"));
}
#[test]
fn category_as_str_stable() {
assert_eq!(WarningCategory::SpecViolation.as_str(), "spec_violation");
assert_eq!(WarningCategory::ToUnicodeMissing.as_str(), "to_unicode_missing");
assert_eq!(WarningCategory::OperatorCapExceeded.as_str(), "operator_cap_exceeded");
}
#[test]
fn clear_resets() {
let sink = WarningSink::new();
sink.push(Warning {
category: WarningCategory::SpecViolation,
page: None,
message: "x".into(),
spec_section: None,
});
assert_eq!(sink.len(), 1);
sink.clear();
assert!(sink.is_empty());
}
#[test]
fn warning_serializes_to_json() {
let w = Warning {
category: WarningCategory::SpecViolation,
page: Some(0),
message: "No newline after stream keyword".into(),
spec_section: Some("7.3.8.1"),
};
let json = serde_json::to_string(&w).unwrap();
assert!(json.contains("\"category\":\"spec_violation\""));
assert!(json.contains("\"page\":0"));
assert!(json.contains("\"spec_section\":\"7.3.8.1\""));
}
#[test]
fn sink_thread_safe() {
use std::sync::Arc;
use std::thread;
let sink = Arc::new(WarningSink::new());
let mut handles = Vec::new();
for i in 0..10 {
let s = sink.clone();
handles.push(thread::spawn(move || {
s.push(Warning {
category: WarningCategory::Font,
page: Some(i),
message: format!("font warning {}", i),
spec_section: None,
});
}));
}
for h in handles {
h.join().unwrap();
}
assert_eq!(sink.len(), 10);
}
}