CLI: text fails when the document cannot be opened (#132) (#233)

`PageRenderer::new` keeps a failed open as `None`, so `page_count()` is 0 and
`text` without `--page` iterated nothing, printed nothing and exited 0 on a
password-protected file. When the renderer has no pages, ask `inspect` why
and fail with its message (the same wording `info` gives); a document that
inspects fine but still renders no pages is reported as unparseable. A
genuinely empty document still succeeds.

Fixes #132

Co-authored-by: wstock <1394687+wstock@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
wstock 2026-10-08 09:25:52 +01:00 committed by GitHub
parent 11ffedcc98
commit 1e54e709b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 50 additions and 2 deletions

View File

@ -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<usize> = match flag(args, "--page") {
Some(p) => vec![p.parse::<usize>().map_err(|_| "bad --page")?.saturating_sub(1)],
None => (0..r.page_count()).collect(),

View File

@ -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}");
}