#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LeniencySeverity {
Info,
Warning,
Critical,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LeniencyEvent {
pub code: &'static str,
pub severity: LeniencySeverity,
pub message: &'static str,
}
impl LeniencyEvent {
const fn new(code: &'static str, severity: LeniencySeverity, message: &'static str) -> Self {
Self {
code,
severity,
message,
}
}
}
pub const FLATE_BROKEN_FALLBACK: LeniencyEvent = LeniencyEvent::new(
"FLATE_BROKEN_FALLBACK",
LeniencySeverity::Warning,
"A flate stream was broken; the pure-Rust fallback decoder was used. \
Output may be truncated or differ from the original.",
);
pub const FLATE_BAD_BLOCK: LeniencyEvent = LeniencyEvent::new(
"FLATE_BAD_BLOCK",
LeniencySeverity::Warning,
"A bad block header was encountered in a flate stream; the block was skipped.",
);
pub const LZW_PREMATURE_EOF: LeniencyEvent = LeniencyEvent::new(
"LZW_PREMATURE_EOF",
LeniencySeverity::Warning,
"Premature EOF in an LZW stream; the end-of-data code was absent.",
);
pub const LZW_INVALID_CODE: LeniencyEvent = LeniencyEvent::new(
"LZW_INVALID_CODE",
LeniencySeverity::Warning,
"An invalid LZW code was encountered; the stream may be truncated.",
);
pub const ASCII85_LENIENT_PARTIAL: LeniencyEvent = LeniencyEvent::new(
"ASCII85_LENIENT_PARTIAL",
LeniencySeverity::Info,
"An ASCII-85 stream contained a 1-character terminal group; accepted leniently \
(common in scanned PDFs). No bytes produced for that group.",
);
pub const CCITT_PARTIAL_DECODE: LeniencyEvent = LeniencyEvent::new(
"CCITT_PARTIAL_DECODE",
LeniencySeverity::Warning,
"The CCITT filter encountered an error after decoding at least one row; \
partial output returned.",
);
pub const STREAM_PARSE_FALLBACK: LeniencyEvent = LeniencyEvent::new(
"STREAM_PARSE_FALLBACK",
LeniencySeverity::Warning,
"Normal stream parsing failed; a manual fallback parser was used instead.",
);
pub const INDIRECT_CYCLE: LeniencyEvent = LeniencyEvent::new(
"INDIRECT_CYCLE",
LeniencySeverity::Warning,
"A cycle was detected in indirect object references; resolution was stopped.",
);
pub const INDIRECT_DEPTH_EXCEEDED: LeniencyEvent = LeniencyEvent::new(
"INDIRECT_DEPTH_EXCEEDED",
LeniencySeverity::Warning,
"Indirect object resolution depth exceeded 512; resolution was stopped.",
);
#[cfg(feature = "std")]
use std::cell::RefCell;
#[cfg(feature = "std")]
thread_local! {
static COLLECTOR: RefCell<Option<Vec<LeniencyEvent>>> = const { RefCell::new(None) };
}
#[cfg(feature = "std")]
pub fn activate() {
COLLECTOR.with(|c| *c.borrow_mut() = Some(Vec::new()));
}
#[cfg(feature = "std")]
pub fn drain() -> alloc::vec::Vec<LeniencyEvent> {
COLLECTOR.with(|c| c.borrow_mut().take().unwrap_or_default())
}
#[cfg(feature = "std")]
pub(crate) fn emit(event: LeniencyEvent) {
COLLECTOR.with(|c| {
if let Some(vec) = c.borrow_mut().as_mut() {
if !vec.iter().any(|e| e.code == event.code) {
vec.push(event);
}
}
});
}
#[cfg(not(feature = "std"))]
pub(crate) fn emit(_event: LeniencyEvent) {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn activate_emit_drain_roundtrip() {
activate();
emit(FLATE_BROKEN_FALLBACK);
let events = drain();
assert_eq!(events.len(), 1);
assert_eq!(events[0].code, "FLATE_BROKEN_FALLBACK");
}
#[test]
fn per_code_deduplication() {
activate();
emit(LZW_PREMATURE_EOF);
emit(LZW_PREMATURE_EOF);
emit(LZW_PREMATURE_EOF);
let events = drain();
assert_eq!(events.len(), 1, "same code must appear at most once");
}
#[test]
fn multiple_distinct_codes_all_collected() {
activate();
emit(FLATE_BROKEN_FALLBACK);
emit(FLATE_BAD_BLOCK);
emit(LZW_PREMATURE_EOF);
let events = drain();
assert_eq!(events.len(), 3);
let codes: Vec<&str> = events.iter().map(|e| e.code).collect();
assert!(codes.contains(&"FLATE_BROKEN_FALLBACK"));
assert!(codes.contains(&"FLATE_BAD_BLOCK"));
assert!(codes.contains(&"LZW_PREMATURE_EOF"));
}
#[test]
fn emit_without_activate_is_noop() {
let _ = drain();
emit(INDIRECT_CYCLE);
let events = drain();
assert!(events.is_empty());
}
#[test]
fn drain_without_activate_returns_empty() {
let _ = drain(); let events = drain();
assert!(events.is_empty());
}
#[test]
fn activate_resets_prior_events() {
activate();
emit(FLATE_BROKEN_FALLBACK);
activate();
emit(INDIRECT_CYCLE);
let events = drain();
assert_eq!(events.len(), 1);
assert_eq!(events[0].code, "INDIRECT_CYCLE");
}
#[test]
fn all_known_constants_have_unique_codes() {
let all = [
FLATE_BROKEN_FALLBACK.code,
FLATE_BAD_BLOCK.code,
LZW_PREMATURE_EOF.code,
LZW_INVALID_CODE.code,
ASCII85_LENIENT_PARTIAL.code,
CCITT_PARTIAL_DECODE.code,
STREAM_PARSE_FALLBACK.code,
INDIRECT_CYCLE.code,
INDIRECT_DEPTH_EXCEEDED.code,
];
let unique: std::collections::HashSet<&str> = all.iter().copied().collect();
assert_eq!(unique.len(), all.len(), "duplicate event code found");
}
}