#![forbid(unsafe_code)]
use std::cell::RefCell;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug, Default)]
pub struct RecoveryCounts {
pub missing_font_decodes: AtomicU64,
pub missing_font_bytes: AtomicU64,
pub xref_misses: AtomicU64,
pub file_scan_recoveries: AtomicU64,
pub objects_treated_as_null: AtomicU64,
pub unsupported_cff_versions: AtomicU64,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct RecoverySnapshot {
pub missing_font_decodes: u64,
pub missing_font_bytes: u64,
pub xref_misses: u64,
pub file_scan_recoveries: u64,
pub objects_treated_as_null: u64,
pub unsupported_cff_versions: u64,
}
impl RecoverySnapshot {
pub fn is_empty(&self) -> bool {
*self == Self::default()
}
}
impl RecoveryCounts {
pub(crate) fn snapshot(&self) -> RecoverySnapshot {
RecoverySnapshot {
missing_font_decodes: self.missing_font_decodes.load(Ordering::Relaxed),
missing_font_bytes: self.missing_font_bytes.load(Ordering::Relaxed),
xref_misses: self.xref_misses.load(Ordering::Relaxed),
file_scan_recoveries: self.file_scan_recoveries.load(Ordering::Relaxed),
objects_treated_as_null: self.objects_treated_as_null.load(Ordering::Relaxed),
unsupported_cff_versions: self.unsupported_cff_versions.load(Ordering::Relaxed),
}
}
}
thread_local! {
static ACTIVE: RefCell<Option<Arc<RecoveryCounts>>> = const { RefCell::new(None) };
}
#[must_use = "the scope is only active while the guard is alive"]
pub(crate) struct RecoveryScope {
previous: Option<Arc<RecoveryCounts>>,
}
impl RecoveryScope {
pub(crate) fn enter(counts: &Arc<RecoveryCounts>) -> Self {
let previous = ACTIVE.with(|slot| slot.borrow_mut().replace(Arc::clone(counts)));
Self { previous }
}
}
impl Drop for RecoveryScope {
fn drop(&mut self) {
ACTIVE.with(|slot| *slot.borrow_mut() = self.previous.take());
}
}
pub(crate) fn record(bump: impl FnOnce(&RecoveryCounts)) {
ACTIVE.with(|slot| {
if let Some(counts) = slot.borrow().as_ref() {
bump(counts);
}
});
}
pub(crate) fn report(snapshot: &RecoverySnapshot) {
if snapshot.is_empty() {
return;
}
tracing::debug!(
target: "xberg_native_pdf::recovery",
missing_font_decodes = snapshot.missing_font_decodes,
missing_font_bytes = snapshot.missing_font_bytes,
xref_misses = snapshot.xref_misses,
file_scan_recoveries = snapshot.file_scan_recoveries,
objects_treated_as_null = snapshot.objects_treated_as_null,
unsupported_cff_versions = snapshot.unsupported_cff_versions,
"recovered from malformed PDF input"
);
}
#[cfg(test)]
mod tests {
use super::*;
fn counts() -> Arc<RecoveryCounts> {
Arc::new(RecoveryCounts::default())
}
#[test]
fn record_outside_any_scope_is_a_noop_rather_than_a_panic() {
record(|c| {
c.xref_misses.fetch_add(1, Ordering::Relaxed);
});
}
#[test]
fn record_inside_a_scope_charges_that_documents_counters() {
let document = counts();
{
let _scope = RecoveryScope::enter(&document);
record(|c| {
c.missing_font_decodes.fetch_add(1, Ordering::Relaxed);
});
record(|c| {
c.missing_font_bytes.fetch_add(7, Ordering::Relaxed);
});
}
let snapshot = document.snapshot();
assert_eq!(snapshot.missing_font_decodes, 1);
assert_eq!(snapshot.missing_font_bytes, 7);
}
#[test]
fn record_after_the_scope_ends_charges_nothing() {
let document = counts();
drop(RecoveryScope::enter(&document));
record(|c| {
c.xref_misses.fetch_add(1, Ordering::Relaxed);
});
assert_eq!(document.snapshot().xref_misses, 0);
}
#[test]
fn a_nested_scope_restores_the_outer_document_rather_than_detaching_it() {
let outer = counts();
let inner = counts();
let _outer_scope = RecoveryScope::enter(&outer);
{
let _inner_scope = RecoveryScope::enter(&inner);
record(|c| {
c.xref_misses.fetch_add(1, Ordering::Relaxed);
});
}
record(|c| {
c.xref_misses.fetch_add(1, Ordering::Relaxed);
});
assert_eq!(inner.snapshot().xref_misses, 1, "inner scope took its own recovery");
assert_eq!(
outer.snapshot().xref_misses,
1,
"outer scope resumed after the inner one ended"
);
}
#[test]
fn a_clean_document_snapshot_reports_nothing() {
assert!(counts().snapshot().is_empty());
}
#[test]
fn decoding_without_a_font_charges_the_document_instead_of_warning() {
let document = counts();
let bytes = b"hello";
{
let _scope = RecoveryScope::enter(&document);
let decoded = crate::fonts::unicode_decode::decode_text_to_unicode(
bytes,
None,
crate::fonts::unicode_decode::DecodePolicy::default(),
None,
);
assert_eq!(decoded, "hello", "Latin-1 fallback still decodes the bytes");
}
let snapshot = document.snapshot();
assert_eq!(snapshot.missing_font_decodes, 1);
assert_eq!(snapshot.missing_font_bytes, bytes.len() as u64);
}
}