diff --git a/apps/pdfcraft-cli/src/main.rs b/apps/pdfcraft-cli/src/main.rs index 8e783a1a..bb1e66c6 100644 --- a/apps/pdfcraft-cli/src/main.rs +++ b/apps/pdfcraft-cli/src/main.rs @@ -174,7 +174,18 @@ fn info(args: &[String]) -> Result<(), CliError> { fn text(args: &[String]) -> Result<(), CliError> { let path = *positional(args).first().ok_or("text: missing file")?; - let mut r = PageRenderer::new(read(path)?, RenderConfig { password: flag(args, "--password").map(Arc::from), ..Default::default() }); + let password = flag(args, "--password"); + let bytes = read(path)?; + let mut r = PageRenderer::new(bytes.clone(), RenderConfig { password: password.map(Arc::from), ..Default::default() }); + // A document the renderer could not open (wrong or missing password, unparseable file) has + // no pages, and "extract every page" of nothing would print nothing and exit 0 (#132). Ask + // `inspect` why instead, so a protected file fails the same way `info` does. + if r.page_count() == 0 { + let info = inspect(bytes, password).map_err(|e| format!("text: {e}"))?; + if !info.pages.is_empty() { + return Err("text: the document could not be parsed".into()); + } + } let pages: Vec = match flag(args, "--page") { Some(p) => vec![p.parse::().map_err(|_| "bad --page")?.saturating_sub(1)], None => (0..r.page_count()).collect(), diff --git a/apps/pdfcraft-cli/tests/text.rs b/apps/pdfcraft-cli/tests/text.rs index 3e0d6c85..88fd749c 100644 --- a/apps/pdfcraft-cli/tests/text.rs +++ b/apps/pdfcraft-cli/tests/text.rs @@ -1,4 +1,5 @@ -//! `text` must fail loudly when a page it was asked for cannot be read (#131). +//! `text` must fail loudly when a page it was asked for cannot be read (#131), and when the +//! document itself cannot be opened, e.g. a password-protected file without `--password` (#132). use std::process::Command; @@ -57,3 +58,39 @@ fn text_of_a_readable_document_succeeds() { let stdout = String::from_utf8_lossy(&out.stdout); assert!(stdout.contains("Page 1") && stdout.contains("Page 3"), "{stdout}"); } + +/// `three.pdf` encrypted with the open password `openme`, via `run --script`. +fn protected_fixture() -> std::path::PathBuf { + let src = tmp("to-protect.pdf"); + std::fs::write(&src, fixture(3)).unwrap(); + let out_path = tmp("protected.pdf"); + let script = tmp("protect.json"); + let steps = serde_json::json!([ + { "tool": "doc_open", "args": { "path": src } }, + { "tool": "doc_protect", "args": { "doc": 1, "open_password": "openme" } }, + { "tool": "doc_save", "args": { "doc": 1, "path": out_path } }, + ]); + std::fs::write(&script, steps.to_string()).unwrap(); + let out = Command::new(BIN).args(["run", "--script", script.to_str().unwrap()]).output().unwrap(); + assert!(out.status.success(), "protecting the fixture failed: {}", String::from_utf8_lossy(&out.stderr)); + out_path +} + +#[test] +fn text_of_a_protected_document_without_the_password_fails() { + let path = protected_fixture(); + let out = Command::new(BIN).args(["text", path.to_str().unwrap()]).output().unwrap(); + assert!(!out.status.success(), "a document that cannot be opened must not exit 0"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(stderr.contains("password"), "the reason must be named: {stderr}"); + assert!(out.stdout.is_empty(), "{}", String::from_utf8_lossy(&out.stdout)); +} + +#[test] +fn text_of_a_protected_document_with_the_password_succeeds() { + let path = protected_fixture(); + let out = Command::new(BIN).args(["text", path.to_str().unwrap(), "--password", "openme"]).output().unwrap(); + assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr)); + let stdout = String::from_utf8_lossy(&out.stdout); + assert!(stdout.contains("Page 1") && stdout.contains("Page 3"), "{stdout}"); +}