diff --git a/apps/pdfcraft-cli/src/main.rs b/apps/pdfcraft-cli/src/main.rs index 4add709ec5..a5a9a3ef44 100644 --- a/apps/pdfcraft-cli/src/main.rs +++ b/apps/pdfcraft-cli/src/main.rs @@ -338,11 +338,20 @@ fn text(args: &[String]) -> Result<(), CliError> { // 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}"))?; + let info = inspect(bytes.clone(), password).map_err(|e| format!("text: {e}"))?; if !info.pages.is_empty() { return Err("text: the document could not be parsed".into()); } } + // Printing the text is copying it, so ask what `text_extract` asks (#1645): without the + // permissions password, a document that doesn't allow copying gives no text. + let mut session = pdfcraft_engine::Session::new(); + if let Some(doc) = session.open(path, None, bytes, password).ok().and_then(|id| session.get(id)) { + // The engine's hint is about editing in the app; on the command line the way in is + // `--password`. + doc.check_copying() + .map_err(|_| "text: the document's permissions don't allow copying its text; pass the owner password with --password to extract it")?; + } let pages: Vec = match selected_page { Some(page) => vec![page], None => (0..r.page_count()).collect(), diff --git a/apps/pdfcraft-cli/tests/text.rs b/apps/pdfcraft-cli/tests/text.rs index 5ac2e07c7d..3d39a9e284 100644 --- a/apps/pdfcraft-cli/tests/text.rs +++ b/apps/pdfcraft-cli/tests/text.rs @@ -96,6 +96,41 @@ fn text_of_a_protected_document_with_the_password_succeeds() { assert!(stdout.contains("Page 1") && stdout.contains("Page 3"), "{stdout}"); } +/// `three.pdf` encrypted so that the open password `openme` doesn't allow copying, and the +/// permissions password `boss` lifts that. +fn copy_protected_fixture() -> std::path::PathBuf { + let src = tmp("to-protect-no-copy.pdf"); + std::fs::write(&src, fixture(3)).unwrap(); + let out_path = tmp("protected-no-copy.pdf"); + let script = tmp("protect-no-copy.json"); + let steps = serde_json::json!([ + { "tool": "doc_open", "args": { "path": src } }, + { "tool": "doc_protect", "args": { "doc": 1, "open_password": "openme", "permissions_password": "boss", "copy": false } }, + { "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 +} + +/// `text` copies text out, so it asks what `text_extract` asks (#1645): the open password alone +/// gives no text when copying isn't allowed, the permissions password does. +#[test] +fn text_respects_a_document_that_does_not_allow_copying() { + let path = copy_protected_fixture(); + let out = Command::new(BIN).args(["text", path.to_str().unwrap(), "--password", "openme"]).output().unwrap(); + assert!(!out.status.success(), "copying isn't allowed: {}", String::from_utf8_lossy(&out.stdout)); + assert!(out.stdout.is_empty(), "{}", String::from_utf8_lossy(&out.stdout)); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(stderr.contains("don't allow copying"), "the reason must be named: {stderr}"); + assert!(stderr.contains("--password"), "and the way in: {stderr}"); + let out = Command::new(BIN).args(["text", path.to_str().unwrap(), "--password", "boss"]).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}"); +} + mod page_argument_tests { use super::*; use std::path::PathBuf;