fix(crypt, engine): refuse page extraction when Protect allows any change except extracting pages (#5081)

## What this changes

Adds `Permissions::extract_pages()`: `assemble()` **and** copying (bit 5), as #5081 asks, since extracted pages carry their text and graphics into a file without the source's protection. It is used for Extract, Split, Split by size and every Combine/Insert/Replace/paste source check (`sources::source_document`). Changes within the document (rotate, delete, move, insert) keep `assemble()`.

Since this PR was opened, #6309 made `assemble()` revision-aware on `main`: bit 11 alone at revision 3 and later, bit 4 at revision 2. That already refuses "Any except extracting pages" at revision 3+. This PR builds on it:

- At revision 2, `assemble()` reads bit 4, which "Any except extracting pages" sets, so such a file could still be extracted and split into unprotected copies. Requiring copying closes that.
- New `Needs::PageExtraction` greys out Extract and Split up front. It reuses the existing security-refusal string, so no catalogue changes.
- `Document::allows_page_extraction()`, and an `extract_pages` key in the automation security JSON.
- `page_extract` with `separate: true` extracts and writes one page at a time (`extract_separate`), so it no longer buffers every page. The first page is extracted before `out_dir` is created, so a refused extraction leaves no empty directory. If a later page fails, the files this call wrote are removed, and `out_dir` too if this call created it (review on this PR).
- Insert Pages checks a PDF through the same path as the insert itself: new `Session::insert_source(dest, name, bytes)`, which the UI's insert now calls (review on this PR).
- The `organize.combine` parity notes now say "page extraction".

Fixes #5081

## How you verified it

`cargo fmt --all` is clean. Regression tests (adopted from Harrison-Booth's review patch, plus CodeRabbit's follow-ups):

- `crates/crypt/src/tests.rs::page_extraction_needs_assembly_and_copying`: unit test for `extract_pages()`.
- `crates/engine/src/tests.rs::extraction_permissions_cover_revision_three_to_six_and_leave_no_partial_output`: AnyExceptExtract on RC4-128, AES-128 and AES-256 refuses Extract, Split, Split by size and Combine/Insert/Replace sources without the permissions password, leaves source and destination untouched, and the owner can do everything. The authorized combine output has 2 pages, and a differently encrypted or unprotected destination is refused.
- `crates/engine/src/tests.rs::page_extraction_needs_the_copy_permission_on_every_revision`: a file that withholds only copying (R2 RC4-40 and AES-256) refuses extraction; the owner password lifts it.
- `crates/engine/src/tests.rs::pages_under_the_same_protection_are_inserted_without_page_extraction`: a separately built file under matching protection is inserted, and the result stays restricted.
- `crates/automation/tests/it/automation.rs::refused_extraction_writes_no_split_or_page_output`, and the `extract_pages` checks in `protecting_through_tools`, which now cover three presets, including a user (not the owner) for whom extraction is allowed.

**Only the crypt unit test has been run locally, and it passes.** The engine and automation tests timed out while compiling on this machine and have not been run, nor seen failing without the fix. The one `pdfcraft-ui-egui` call-site change has not been compiled. CI is the first place these build.

- [x] `cargo fmt --check`
- [ ] clippy with `-D warnings` on the crates I changed (not re-run after the rebase)
- [ ] tests pass on the crates I changed (crypt only)

## Anything a reviewer should look at first

- **Rebased onto #6309.** The tests no longer expect "Any except extracting pages" to allow assembly at revision 3+. The same-protection paste tests now use the "Inserting, deleting and rotating pages" preset without copying.
- **Copy (bit 5) is now required**, as the issue suggests. `crates/ui-egui/tests/it/editing.rs::protected_files_that_can_be_combined_are_flagged` used a copy-withheld file as "protected but combinable"; it now uses a print-withheld file, since a no-copy file is no longer a Combine source without the permissions password.
- **Same-document paste**: Paste re-opens the document's own bytes as an Insert source, which the new check would refuse. `sources::open_source_into` accepts a source that withholds extraction but allows assembly only when the destination will be saved under exactly the same protection (file key and `/Encrypt` values), so cut/copy and paste within one protected document still works. Pasting into an unprotected or differently protected document is refused. One limit: after changing security in the same session without saving, a cut can't be pasted back until you save.
- **Not in this PR:** CodeRabbit's `disabled_reason` suggestion for read-only documents predates this change and covers other commands too, so it belongs in a separate UI change.


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

- **New Features**
  - Page extraction and splitting now follow a dedicated permission check, distinct from page assembly.
  - Pages can be inserted from certain protected PDFs when their protection settings match the destination document.

- **Bug Fixes**
  - Separate-page extraction now cleans up files created during a failed operation and avoids creating an output directory when the first page cannot be extracted.
  - Security details now report whether page extraction is permitted.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Michael Higgins authored and GitHub committed 2026-11-11 09:06:48 +01:00
1 parent c04925c580
commit 4efcfa6ea3
16 files changed
+387 -33

No files matched your search

+1 -1
View File
@@ -3047,7 +3047,7 @@ status = "shipped"
commands = ["page.combine"]
tools = ["doc_combine"]
evidence = ["crates/organize/src/tests.rs::combine_concatenates_with_a_bookmark_per_file", "crates/ui-egui/tests/it/editing.rs::combining_files_opens_a_new_unsaved_tab", "crates/ui-egui/tests/it/ui.rs::files_to_combine_from_the_operating_system", "apps/pdfcraft/src/single_instance.rs::a_second_launch_hands_its_files_to_the_first", "crates/ui-egui/tests/it/editing.rs::combine_files_opens_in_its_own_tab", "crates/ui-egui/tests/it/editing.rs::combine_lists_size_and_warns_before_combining", "crates/ui-egui/tests/it/editing.rs::a_password_protected_file_is_unlocked_with_its_password_and_combined", "crates/ui-egui/tests/it/editing.rs::a_folder_adds_its_pdfs_images_and_text_in_order_and_optionally_its_subfolders", "crates/ui-egui/tests/it/editing.rs::images_and_text_files_join_the_combine_list_as_pdfs", "crates/ui-egui/tests/it/editing.rs::files_that_cannot_be_read_are_listed_and_left_out_once_the_user_agrees", "crates/ui-egui/tests/it/ui.rs::pdfs_dropped_on_the_combine_tab_join_its_list", "crates/ui-egui/tests/it/ui.rs::files_dropped_on_the_combine_tab_join_the_end_of_its_list_in_the_order_dropped", "crates/ui-egui/tests/it/ui.rs::a_file_combine_does_not_take_is_left_out_of_a_drop_and_named", "crates/ui-egui/tests/it/ui.rs::a_pdf_dropped_on_a_document_opens_while_the_combine_tab_waits_behind_it", "crates/automation/tests/it/automation.rs::combine_extract_and_split", "crates/automation/tests/it/automation.rs::combine_opens_protected_files_with_their_passwords", "crates/automation/tests/it/automation.rs::combine_takes_images_and_text_files", "crates/engine/src/tests.rs::files_for_combine_are_made_into_pdfs_and_pdfs_pass_through", "crates/organize/src/tests.rs::combine_keeps_pdfx_when_every_source_agrees", "crates/organize/src/tests.rs::combine_claims_no_pdfx_when_the_sources_disagree"]
notes = "Combine files is its own tab: a table of the files (name, pages, size, modified, warnings) with column sorting, resizing and moving; files, folders (with subfolders) and open documents can be added, or dropped on the tab (desktop and web): a drop joins the end of the list in the order dropped, as one undo step, not at the place under the pointer; files of a kind Combine does not take are left out and named. Dropped elsewhere, files open as before. File Explorer's \"Combine files in PdfCraft\" and `pdfcraft --combine files…` add files from outside, to the running PdfCraft's tab (#585). Dropping adds no engine behaviour, so it has no tool of its own (doc_combine takes the files). Images (PNG, JPEG, TIFF, GIF, BMP, JPEG 2000) and .txt files become PDFs as they are added (#569; also in doc_combine). A file that can't be read stays in the list with why, and Combine asks before leaving it out; Office documents, HTML and other formats are not converted yet. Problems show before Combine (password, security settings that withhold page assembly, bad page ranges, duplicates); protected files are unlocked with their open or permissions password (also doc_combine passwords). The result is never encrypted, and is not yet offered for protection in the same step. A PDF/X result (output intent, GTS_PDFXVersion, Trapped, XMP pdfxid) only when every source declares the same version and output intent (#263). When they differ the result claims none, and nothing reports that yet; Acrobat behaviour not observed."
notes = "Combine files is its own tab: a table of the files (name, pages, size, modified, warnings) with column sorting, resizing and moving; files, folders (with subfolders) and open documents can be added, or dropped on the tab (desktop and web): a drop joins the end of the list in the order dropped, as one undo step, not at the place under the pointer; files of a kind Combine does not take are left out and named. Dropped elsewhere, files open as before. File Explorer's \"Combine files in PdfCraft\" and `pdfcraft --combine files…` add files from outside, to the running PdfCraft's tab (#585). Dropping adds no engine behaviour, so it has no tool of its own (doc_combine takes the files). Images (PNG, JPEG, TIFF, GIF, BMP, JPEG 2000) and .txt files become PDFs as they are added (#569; also in doc_combine). A file that can't be read stays in the list with why, and Combine asks before leaving it out; Office documents, HTML and other formats are not converted yet. Problems show before Combine (password, security settings that withhold page extraction, bad page ranges, duplicates); protected files are unlocked with their open or permissions password (also doc_combine passwords). The result is never encrypted, and is not yet offered for protection in the same step. A PDF/X result (output intent, GTS_PDFXVersion, Trapped, XMP pdfxid) only when every source declares the same version and output intent (#263). When they differ the result claims none, and nothing reports that yet; Acrobat behaviour not observed."
[[feature]]
id = "organize.combine-bookmarks"