pdfluent 1.0.0-beta.17.3

Pure-Rust PDF SDK with PDF/A, digital signatures, WASM, and experimental (feature-gated) XFA support.
Documentation
//! GA Quality/Reliability milestone (SDK_GA_QUALITY_RELIABILITY_100) tests.
//!
//! Closes:
//! - QR-1: no public facade entrypoint panics on hostile/corpus input;
//!   construction on garbage returns a typed error (no silent success).
//! - QR-8: `PdfDocument` is `Send + Sync` (static assertion) and is safe to
//!   build + read from many threads concurrently.
//!
//! Pure std + the public `pdfluent` facade; no new dependency.

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"),
    }
}

/// Every read-only public method, exercised under a panic guard. If the
/// document opened, none of these may panic regardless of input shape.
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());
    // annotations on the first page and a wildly out-of-range page index.
    let _ = no_panic(&format!("{tag}: annotations(0)"), || doc.annotations(0));
    let _ = no_panic(&format!("{tag}: annotations(MAX)"), || {
        doc.annotations(usize::MAX)
    });
    // render of an out-of-range page must error, never panic.
    let _ = no_panic(&format!("{tag}: render oob"), || {
        doc.render_page(n.saturating_add(9999), 72, ImageFormat::Png)
    });
}

// --------------------------------------------------------------------------
// QR-1 — no-panic on hostile synthetic input + typed error (no silent success)
// --------------------------------------------------------------------------

#[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 {
            // If a degenerate input happens to parse, the doc must still be
            // safe to interrogate without panicking.
            Ok(doc) => exercise_readonly(&doc, tag),
            // Otherwise it must be a *typed* error — never a panic, never a
            // silent success.
            Err(_e) => { /* typed pdfluent::Error — acceptable */ }
        }
    }
}

#[test]
fn qr1_corpus_open_paths_never_panic() {
    // Every in-repo fixture must be safe to open + interrogate (or fail with
    // a typed error), 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; // fixture optional
    };
    // Opening an encrypted document without a password must yield a typed
    // outcome (Ok if the SDK can read structure, or a typed Error) — never a
    // panic and never a silent partial-success that looks complete.
    let res = no_panic("open encrypted", || PdfDocument::from_bytes(&bytes));
    // Ok or typed Err both acceptable; only requirement is no panic.
    if let Ok(doc) = res {
        let _ = no_panic("encrypted text", || doc.text());
    }
}

// --------------------------------------------------------------------------
// QR-8 — Send + Sync + concurrent build/read
// --------------------------------------------------------------------------

fn _assert_send<T: Send>() {}
fn _assert_sync<T: Sync>() {}

#[test]
fn qr8_pdfdocument_is_send_and_sync() {
    // Compile-time contract matching the documented "Send + Sync" guarantee.
    _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);
    // Baseline single-threaded result.
    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"
        );
    }
}