use std::path::{Path, PathBuf};
use std::process::Command;
use pdfluent::{OpenOptions, PdfDocument};
fn mini(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/corpus-mini")
.join(name)
}
fn qpdf_ok() -> bool {
Command::new("qpdf")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn qpdf_encrypt(src: &Path, dst: &Path, user: &str, owner: &str, bits: u32, aes: bool) -> bool {
let mut cmd = Command::new("qpdf");
cmd.arg("--encrypt")
.arg(format!("--user-password={user}"))
.arg(format!("--owner-password={owner}"))
.arg(format!("--bits={bits}"));
if bits == 128 {
cmd.arg(if aes { "--use-aes=y" } else { "--use-aes=n" });
}
cmd.arg("--").arg(src).arg(dst);
let code = cmd
.output()
.ok()
.and_then(|o| o.status.code())
.unwrap_or(-1);
if code != 0 && code != 3 {
return false;
}
Command::new("qpdf")
.arg("--is-encrypted")
.arg(dst)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[derive(Debug)]
struct Outcome {
handler: &'static str,
#[allow(dead_code)]
correct_opens: bool,
wrong_is_err: bool,
missing_is_err: bool,
}
fn open_pw(path: &Path, pw: Option<&str>) -> Result<PdfDocument, pdfluent::Error> {
match pw {
Some(p) => PdfDocument::open_with(path, OpenOptions::new().with_password(p)),
None => PdfDocument::open(path),
}
}
#[test]
fn qr3_decryption_handler_matrix() {
if !qpdf_ok() {
eprintln!("qpdf unavailable — skipping QR-3 decryption matrix");
return;
}
let src = mini("multi-page.pdf");
if !src.exists() {
return;
}
let tmp = std::env::temp_dir().join("pdfluent_qr3");
std::fs::create_dir_all(&tmp).expect("mk tmp");
const USER: &str = "user-pw";
const OWNER: &str = "owner-pw";
let handlers: &[(&str, u32, bool)] = &[
("rc4-128", 128, false),
("aes-128", 128, true),
("aes-256", 256, true),
];
let mut outcomes = Vec::new();
for (name, bits, aes) in handlers {
let dst = tmp.join(format!("enc_{name}.pdf"));
if !qpdf_encrypt(&src, &dst, USER, OWNER, *bits, *aes) {
eprintln!("qpdf could not produce {name} — skipping that handler");
continue;
}
let correct = std::panic::catch_unwind(|| open_pw(&dst, Some(USER)))
.expect("open(correct) must not panic");
let wrong = std::panic::catch_unwind(|| open_pw(&dst, Some("definitely-wrong")))
.expect("open(wrong) must not panic");
let missing =
std::panic::catch_unwind(|| open_pw(&dst, None)).expect("open(missing) must not panic");
if let (Ok(c), Ok(w)) = (&correct, &wrong) {
let ct = c.extract_text().unwrap_or_default();
let wt = w.extract_text().unwrap_or_default();
assert!(
ct.is_empty() || ct != wt,
"{name}: wrong password produced the same plaintext as correct — decryption bypass"
);
}
outcomes.push(Outcome {
handler: name,
correct_opens: correct.is_ok(),
wrong_is_err: wrong.is_err(),
missing_is_err: missing.is_err(),
});
}
for o in &outcomes {
eprintln!("QR-3 {o:?}");
assert!(
o.wrong_is_err,
"{}: wrong password must return a typed error, not Ok",
o.handler
);
assert!(
o.missing_is_err,
"{}: missing password must return a typed error, not Ok",
o.handler
);
}
assert!(
!outcomes.is_empty(),
"no encryption handlers could be generated by qpdf"
);
}