use std::panic::{catch_unwind, AssertUnwindSafe};
use std::path::PathBuf;
use std::sync::Arc;
use pdfluent::prelude::*;
fn mini(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/corpus-mini")
.join(name)
}
fn read_mini(name: &str) -> Option<Vec<u8>> {
std::fs::read(mini(name)).ok()
}
fn no_panic<T>(what: &str, f: impl FnOnce() -> T) -> T {
match catch_unwind(AssertUnwindSafe(f)) {
Ok(v) => v,
Err(_) => panic!("{what} PANICKED — public entrypoint must never panic"),
}
}
fn exercise_readonly(doc: &PdfDocument, tag: &str) {
let n = no_panic(&format!("{tag}: page_count"), || doc.page_count());
no_panic(&format!("{tag}: version"), || doc.version());
no_panic(&format!("{tag}: metadata"), || doc.metadata());
let _ = no_panic(&format!("{tag}: text"), || doc.text());
let _ = no_panic(&format!("{tag}: extract_text"), || doc.extract_text());
let _ = no_panic(&format!("{tag}: form_fields"), || doc.form_fields());
let _ = no_panic(&format!("{tag}: outlines"), || doc.outlines());
let _ = no_panic(&format!("{tag}: attachments"), || doc.attachments());
let _ = no_panic(&format!("{tag}: to_bytes"), || doc.to_bytes());
let _ = no_panic(&format!("{tag}: split_pages"), || doc.split_pages());
let _ = no_panic(&format!("{tag}: annotations(0)"), || doc.annotations(0));
let _ = no_panic(&format!("{tag}: annotations(MAX)"), || {
doc.annotations(usize::MAX)
});
let _ = no_panic(&format!("{tag}: render oob"), || {
doc.render_page(n.saturating_add(9999), 72, ImageFormat::Png)
});
}
#[test]
fn qr1_hostile_synthetic_input_never_panics_and_is_typed() {
let cases: &[(&str, Vec<u8>)] = &[
("empty", Vec::new()),
("garbage", vec![0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0xFF, 0x42]),
("header_only", b"%PDF-1.7\n".to_vec()),
(
"truncated",
b"%PDF-1.7\n1 0 obj\n<< /Type /Catalog".to_vec(),
),
(
"bogus_xref",
b"%PDF-1.7\nxref\nstartxref\n999999\n%%EOF".to_vec(),
),
("nul_bytes", vec![0u8; 4096]),
];
for (tag, bytes) in cases {
let res = no_panic(&format!("from_bytes({tag})"), || {
PdfDocument::from_bytes(bytes)
});
match res {
Ok(doc) => exercise_readonly(&doc, tag),
Err(_e) => { }
}
}
}
#[test]
fn qr1_corpus_open_paths_never_panic() {
for name in [
"simple.pdf",
"multi-page.pdf",
"acroform.pdf",
"pdfa-2b.pdf",
"scanned.pdf",
"encrypted.pdf",
"malformed.pdf",
"zugferd.pdf",
"signed-rsa.pdf",
] {
let Some(bytes) = read_mini(name) else {
continue;
};
let res = no_panic(&format!("open({name})"), || PdfDocument::from_bytes(&bytes));
if let Ok(doc) = res {
exercise_readonly(&doc, name);
}
}
}
#[test]
fn qr1_encrypted_without_password_is_typed_not_panic() {
let Some(bytes) = read_mini("encrypted.pdf") else {
return; };
let res = no_panic("open encrypted", || PdfDocument::from_bytes(&bytes));
if let Ok(doc) = res {
let _ = no_panic("encrypted text", || doc.text());
}
}
fn _assert_send<T: Send>() {}
fn _assert_sync<T: Sync>() {}
#[test]
fn qr8_pdfdocument_is_send_and_sync() {
_assert_send::<PdfDocument>();
_assert_sync::<PdfDocument>();
}
#[test]
fn qr8_concurrent_open_and_read_is_consistent() {
let Some(bytes) = read_mini("multi-page.pdf") else {
return;
};
let shared = Arc::new(bytes);
let base = PdfDocument::from_bytes(&shared).expect("open baseline");
let base_pages = base.page_count();
let mut handles = Vec::new();
for _ in 0..8 {
let b = Arc::clone(&shared);
handles.push(std::thread::spawn(move || {
let doc = PdfDocument::from_bytes(&b).expect("open in thread");
let pages = doc.page_count();
let _ = doc.text();
pages
}));
}
for h in handles {
let pages = h.join().expect("thread must not panic");
assert_eq!(
pages, base_pages,
"page_count must be deterministic across threads"
);
}
}