From f686f6c3f65894a1edfa3f8dcb60633f61a6b34a Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 3 Nov 2026 06:33:31 -0800 Subject: [PATCH] PDF: allow opening to be canceled during parsing --- crates/pdf/README.md | 19 +- crates/pdf/src/read.rs | 67 +++++++ crates/pdf/tests/cancellation.rs | 109 +++++++++++ scripts/test-pdf-backends.sh | 1 + vendor/README.craft-reader.md | 12 ++ vendor/hayro-syntax/SOURCE.json | 44 +++-- vendor/hayro-syntax/src/byte_reader.rs | 44 ++++- vendor/hayro-syntax/src/cancel.rs | 176 ++++++++++++++++++ .../hayro-syntax/src/decode_limits_tests.rs | 29 +++ vendor/hayro-syntax/src/filter/ascii_85.rs | 6 + vendor/hayro-syntax/src/filter/ascii_hex.rs | 17 +- .../src/filter/cancellation_tests.rs | 75 ++++++++ vendor/hayro-syntax/src/filter/lzw_flate.rs | 137 +++++++++++--- vendor/hayro-syntax/src/filter/mod.rs | 16 +- vendor/hayro-syntax/src/filter/run_length.rs | 7 + vendor/hayro-syntax/src/lib.rs | 2 + vendor/hayro-syntax/src/object/dict.rs | 2 + vendor/hayro-syntax/src/object/name.rs | 18 +- vendor/hayro-syntax/src/object/stream.rs | 8 +- vendor/hayro-syntax/src/object/string.rs | 14 +- vendor/hayro-syntax/src/page.rs | 33 +++- vendor/hayro-syntax/src/pdf.rs | 46 ++++- vendor/hayro-syntax/src/reader.rs | 42 ++++- vendor/hayro-syntax/src/xref.rs | 154 ++++++++++++--- 24 files changed, 982 insertions(+), 96 deletions(-) create mode 100644 crates/pdf/tests/cancellation.rs create mode 100644 vendor/hayro-syntax/src/cancel.rs create mode 100644 vendor/hayro-syntax/src/filter/cancellation_tests.rs diff --git a/crates/pdf/README.md b/crates/pdf/README.md index 4a73fdc6..816eba77 100644 --- a/crates/pdf/README.md +++ b/crates/pdf/README.md @@ -24,6 +24,18 @@ including during initial xref reads, object streams, repair and fallback. Known- pixels and streaming page content have separate backend budgets. These controls do not bound total parser memory or execution time. +`open_with_options_and_cancel` and `open_slice_with_options_and_cancel` additionally accept +`&syntax::Cancellation`. It owns a `Send + Sync + 'static` callback, which runs on the opening +thread. Capture shared state such as an `Arc` to request cancellation from a worker's +caller. Each open latches a true result and returns `CancellableOpenError::Cancelled`; other +failures wrap the existing `OpenError`. Existing opening APIs and their error types are unchanged. +The returned PDF does not retain the callback or poll it during later reads. + +Opening checks cancellation during xref searches and recovery, byte and object scans, ordinary +stream decoding and predictors, and initial page-tree traversal. The borrowed-input variant also +checks while copying input. Cryptographic and image-codec calls run to completion between checkpoints. +These are opening checkpoints; interpretation and rasterization have their own cancellation policy. + For a bare Flate page, `Page::page_stream` materializes content under the ordinary limit, while `Page::content_reader` decodes incrementally under the separate streaming budget. Interpretation and rasterization use that incremental path. Lowering the ordinary limit @@ -51,7 +63,7 @@ that all objects survived. Some tolerant whole-stream decoders can return an inc prefix without a failure status. Use fresh caches for checks, while retaining these limitations. Native unwinding panics become errors; discard partial devices, buffers, parsers and caches after backend failure. WASM aborts and process-wide allocation failure cannot be caught. -Cancellation is polled between operators, so a decoder may finish before noticing it. +During interpretation, cancellation is polled between operators, so a decoder may finish before noticing it. ## Backend provenance and consumer pins @@ -88,8 +100,9 @@ stay outside this repository; dependency assets and licences are in the root att The Shared PDF workflow runs native and WebAssembly facade contracts, an independent `read-only-contract` consumer, and the transferred backend tests. The independent consumer keeps interpreter-bearing dev-dependencies out of the shipping `read` feature graph. -Run `bash scripts/test-pdf-backends.sh` from the repository root to test all four vendored -backends, the syntax doctest and the syntax-only configuration. The script uses temporary +Run `bash scripts/test-pdf-backends.sh` from the repository root to test the vendored +backends, the syntax doctest, syntax-only configuration and alternate `unsafe` codec implementation. +The script uses temporary source copies and the complete local patch set, preserving the vendored lockfiles. Run `cargo test --manifest-path crates/pdf/read-only-contract/Cargo.toml --locked` for the read-only consumer, then repeat with `--target wasm32-unknown-unknown` and the test runner. diff --git a/crates/pdf/src/read.rs b/crates/pdf/src/read.rs index 509f96bd..cde442db 100644 --- a/crates/pdf/src/read.rs +++ b/crates/pdf/src/read.rs @@ -20,6 +20,73 @@ pub enum OpenError { BackendPanic, } +/// Failure of an open that accepts cooperative cancellation. +#[derive(Debug, thiserror::Error)] +pub enum CancellableOpenError { + #[error("PDF opening canceled")] + Cancelled, + #[error(transparent)] + Open(#[from] OpenError), +} + +/// Open shared input with cooperative cancellation and explicit caller limits. +/// The callback runs on this thread. Once it requests cancellation this operation returns +/// [`CancellableOpenError::Cancelled`]; a loaded PDF does not retain its callback. +pub fn open_with_options_and_cancel( + bytes: Arc>, + password: Option<&str>, + options: &OpenOptions, + cancel: &syntax::Cancellation, +) -> Result { + guard_cancelled_open(|| read_with_cancel(bytes, password, options, cancel)) +} + +fn guard_cancelled_open(read: impl FnOnce() -> Result) -> Result { + catch_unwind(AssertUnwindSafe(read)).map_err(|_| CancellableOpenError::Open(OpenError::BackendPanic))? +} + +fn read_with_cancel( + bytes: Arc>, + password: Option<&str>, + options: &OpenOptions, + cancel: &syntax::Cancellation, +) -> Result { + if cancel.requested() { + return Err(CancellableOpenError::Cancelled); + } + check_input_size_with(bytes.len(), options)?; + let result = syntax::Pdf::new_with_password_limits_and_cancel(bytes, password.unwrap_or(""), options.decode_limits, cancel); + result.map_err(|e| match e { + syntax::CancellableLoadError::Cancelled => CancellableOpenError::Cancelled, + syntax::CancellableLoadError::Load(e) => CancellableOpenError::Open(OpenError::Load(e)), + }) +} + +/// Copy borrowed input in bounded portions and open it with cooperative cancellation. +/// The input bound and allocation error retain [`open_slice_with_options`]'s policy. +pub fn open_slice_with_options_and_cancel( + bytes: &[u8], + password: Option<&str>, + options: &OpenOptions, + cancel: &syntax::Cancellation, +) -> Result { + guard_cancelled_open(|| { + if cancel.requested() { + return Err(CancellableOpenError::Cancelled); + } + check_input_size_with(bytes.len(), options)?; + let mut owned = Vec::new(); + owned.try_reserve_exact(bytes.len()).map_err(|_| OpenError::Allocation)?; + for chunk in bytes.chunks(64 * 1024) { + if cancel.requested() { + return Err(CancellableOpenError::Cancelled); + } + owned.extend_from_slice(chunk); + } + read_with_cancel(Arc::new(owned), password, options, cancel) + }) +} + /// Check the input bound before an application allocates or starts another PDF reader. pub fn check_input_size(size: usize) -> Result<(), OpenError> { check_input_size_with(size, &OpenOptions::default()) diff --git a/crates/pdf/tests/cancellation.rs b/crates/pdf/tests/cancellation.rs new file mode 100644 index 00000000..737284f6 --- /dev/null +++ b/crates/pdf/tests/cancellation.rs @@ -0,0 +1,109 @@ +#![cfg(feature = "read")] +#![allow(clippy::unwrap_used)] + +use craft_pdf::{CancellableOpenError, OpenError, OpenOptions, open, open_slice_with_options_and_cancel, open_with_options_and_cancel, syntax}; +use std::sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, +}; + +fn fixture() -> Vec { + b"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 40 20] >> endobj\ntrailer << /Root 1 0 R >>\n%%EOF".to_vec() +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn cancellation_takes_precedence_for_an_initial_request() { + let cancel = syntax::Cancellation::new(|| true); + let options = OpenOptions::default(); + assert!(matches!(open_with_options_and_cancel(Arc::new(vec![]), None, &options, &cancel), Err(CancellableOpenError::Cancelled))); + assert!(matches!(open_slice_with_options_and_cancel(b"", None, &options, &cancel), Err(CancellableOpenError::Cancelled))); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn cancellation_interrupts_a_reverse_xref_search_and_stays_latched() { + let mut bytes = fixture(); + bytes.extend(std::iter::repeat_n(b' ', 4 << 20)); + let polls = Arc::new(AtomicUsize::new(0)); + let counter = polls.clone(); + let cancel = syntax::Cancellation::new(move || counter.fetch_add(1, Ordering::Relaxed) + 1 == 10); + let result = open_with_options_and_cancel(Arc::new(bytes), None, &OpenOptions::default(), &cancel); + assert!(matches!(result, Err(CancellableOpenError::Cancelled))); + assert_eq!(polls.load(Ordering::Relaxed), 10); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn cancellation_interrupts_borrowed_input_copying() { + let bytes = vec![b' '; 4 << 20]; + let polls = Arc::new(AtomicUsize::new(0)); + let counter = polls.clone(); + let cancel = syntax::Cancellation::new(move || counter.fetch_add(1, Ordering::Relaxed) + 1 == 10); + let result = open_slice_with_options_and_cancel(&bytes, None, &OpenOptions::default(), &cancel); + assert!(matches!(result, Err(CancellableOpenError::Cancelled))); + assert_eq!(polls.load(Ordering::Relaxed), 10); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn a_successful_open_releases_callback_captures_and_does_not_poll_later() { + let polls = Arc::new(AtomicUsize::new(0)); + let counter = polls.clone(); + let marker = Arc::new(()); + let weak = Arc::downgrade(&marker); + let cancel = syntax::Cancellation::new(move || { + let _keep = ▮ + counter.fetch_add(1, Ordering::Relaxed); + false + }); + let bytes = fixture(); + let normal = open(Arc::new(bytes.clone()), None).unwrap(); + let pdf = open_with_options_and_cancel(Arc::new(bytes), None, &OpenOptions::default(), &cancel).unwrap(); + let before = polls.load(Ordering::Relaxed); + drop(cancel); + assert!(weak.upgrade().is_none(), "the returned PDF must not retain the callback"); + assert_eq!(pdf.pages().len(), normal.pages().len()); + assert!(pdf.xref().get::>(syntax::object::ObjectIdentifier::new(1, 0)).is_some()); + assert_eq!(polls.load(Ordering::Relaxed), before); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn an_uncanceled_open_preserves_bounds_and_loading_errors() { + let cancel = syntax::Cancellation::new(|| false); + assert!(matches!( + open_with_options_and_cancel(Arc::new(b"not a PDF".to_vec()), None, &OpenOptions::default(), &cancel), + Err(CancellableOpenError::Open(OpenError::Load(syntax::LoadPdfError::Invalid))) + )); + let options = OpenOptions { max_input_bytes: 1, ..OpenOptions::default() }; + assert!(matches!( + open_slice_with_options_and_cancel(&fixture(), None, &options, &cancel), + Err(CancellableOpenError::Open(OpenError::InputLimit { limit: 1 })) + )); +} + +#[cfg(not(target_arch = "wasm32"))] +#[test] +#[allow(clippy::panic)] +fn callback_panics_become_errors_before_parsing_and_during_copying() { + let bytes = vec![b' '; 4 << 20]; + for borrowed in [false, true] { + for at in [1, 2, 10] { + let polls = Arc::new(AtomicUsize::new(0)); + let counter = polls.clone(); + let cancel = syntax::Cancellation::new(move || { + if counter.fetch_add(1, Ordering::Relaxed) + 1 == at { + panic!("synthetic callback panic"); + } + false + }); + let result = if borrowed { + open_slice_with_options_and_cancel(&bytes, None, &OpenOptions::default(), &cancel) + } else { + open_with_options_and_cancel(Arc::new(bytes.clone()), None, &OpenOptions::default(), &cancel) + }; + assert!(matches!(result, Err(CancellableOpenError::Open(OpenError::BackendPanic)))); + } + } +} diff --git a/scripts/test-pdf-backends.sh b/scripts/test-pdf-backends.sh index cc31a7fa..4b8d6d3c 100644 --- a/scripts/test-pdf-backends.sh +++ b/scripts/test-pdf-backends.sh @@ -14,5 +14,6 @@ done for backend in hayro-syntax hayro-interpret hayro hayro-jbig2 hayro-cmap hayro-postscript; do cargo test --manifest-path "$test_root/$backend/Cargo.toml" --lib "${patches[@]}" done +cargo test --manifest-path "$test_root/hayro-syntax/Cargo.toml" --lib --features unsafe "${patches[@]}" cargo test --manifest-path "$test_root/hayro-syntax/Cargo.toml" --doc "${patches[@]}" cargo test --manifest-path "$test_root/hayro-syntax/Cargo.toml" --lib --no-default-features --features std "${patches[@]}" diff --git a/vendor/README.craft-reader.md b/vendor/README.craft-reader.md index 6967ee6f..6f77d9f2 100644 --- a/vendor/README.craft-reader.md +++ b/vendor/README.craft-reader.md @@ -342,6 +342,18 @@ fonts without Encoding) and #1499 (byte-aligned packed TIFF predictor rows). Synthetic tests cover 1/2/4/8-bit RGB rows and rendering, plus embedded TrueType text with absent or partial ToUnicode maps and explicit encoding precedence. +## Cooperative opening cancellation + +The syntax reader also supports cooperative cancellation scoped to one open operation through +`Cancellation` and `Pdf::new_with_password_limits_and_cancel`. Checks cover xref searches and +recovery, object/byte scans, ordinary ASCIIHex/ASCII85/RunLength/LZW/Flate decoding and predictors, +and initial page-tree construction. A cancellation request stays latched and prevents repair or +partial success. The operation releases callback captures when it returns, including unwinding; +deferred reads on a loaded PDF do not poll the completed operation. Existing loading errors and +constructors, decode limits, and object sizes are preserved. Cryptographic and image-codec calls +run to completion between checkpoints. Synthetic API and backend tests cover interruption inside +the supported stages. + ## Craft patches for the flattener Local changes for craft-prepress's PDF-in adapter (`craft_prepress::flatten::pdf`), which reads diff --git a/vendor/hayro-syntax/SOURCE.json b/vendor/hayro-syntax/SOURCE.json index f2577bd3..09f09689 100644 --- a/vendor/hayro-syntax/SOURCE.json +++ b/vendor/hayro-syntax/SOURCE.json @@ -56,7 +56,7 @@ "path": "src/byte_reader.rs", "source_sha256": "aa8495ca920d5d1f27db8ec98557fd1dc6376a3b6823c1cf8bbd6ed9b9b0c8cf", "source_git_blob": "78b282c8f3df43ee43faee63c944b922539b2ec8", - "sha256": "296cee7a0095b399685128f4ee5d854156597c745847e04db12e398f2afb24cc" + "sha256": "41a68fcfb6e71d74257aa2f1821213cc1ea0beff1ea7031dff6b917e98386ceb" }, { "path": "src/content/gen_ops.py", @@ -134,13 +134,13 @@ "path": "src/filter/ascii_85.rs", "source_sha256": "550cd5b3ceb7ee9342959ac9cc80c35b176aed3c7f061789e379beb8b5873f48", "source_git_blob": "943bfeb742ac8cdc8ba64fcc538a044cb812577e", - "sha256": "054c13b992fd2c4a1a2d9881198eadb82d8c8454666d136b3c2dfaea60f7308a" + "sha256": "6951513c7c99d1290376e843603478e093191548539b09ef0c512dbf5c9a0626" }, { "path": "src/filter/ascii_hex.rs", "source_sha256": "3943384166df1f81f76c8f64a28766131dc947311d672d01ffa5fef05b0a30c8", "source_git_blob": "edcd82d5bf335b6a266ff7d6871fb8eb4f446bbe", - "sha256": "3943384166df1f81f76c8f64a28766131dc947311d672d01ffa5fef05b0a30c8" + "sha256": "d4ee540a6e830902e49026a5d0283eababe1c0abd76b673700a6a4102845ccdf" }, { "path": "src/filter/ccitt.rs", @@ -170,25 +170,25 @@ "path": "src/filter/lzw_flate.rs", "source_sha256": "db51a57effe90ad9db2d314e16081ce90e3ef99e5a5f46fcc4bdbdacfd13a528", "source_git_blob": "f1ab1aae73e4c84ec0993a6eb96594018ca7cc91", - "sha256": "00d478a55b0c123d903768186592d138d563543abf46e5c786c67b9fffd69637" + "sha256": "14da3cf51cadba54fa3c1e654436c4fd982fb278538d553179ca257810a3d2b9" }, { "path": "src/filter/mod.rs", "source_sha256": "0af7145ab16b15b7a60c4b57efecb9bd5fd0cfdc9bb5f9388abb695386e63e49", "source_git_blob": "06bda29487d62c25dc39c1ee109091cb17e3867f", - "sha256": "727fd21fe968d0ce9de54ea0c50658af43658289f9b38b27fbc74596e48d0d4b" + "sha256": "3719baece25774c09263307b823ef36e834ea095e0d1728cfc664a64123fc340" }, { "path": "src/filter/run_length.rs", "source_sha256": "a60985c746312cd50a030b910db99cee25895baffb1737efe6a9dd9fff8fb4c6", "source_git_blob": "2bef8e706afa99fd74c1c2ebbcbe2006a24467f5", - "sha256": "a60985c746312cd50a030b910db99cee25895baffb1737efe6a9dd9fff8fb4c6" + "sha256": "fbdd667982b81a537db6711f5c5c66cb0c8b291c3d5b9028dffda833d3664536" }, { "path": "src/lib.rs", "source_sha256": "0439293354fe274dd0a956f7d8840f512b18b5a84cabcea63452dfddb6629c7e", "source_git_blob": "b676523158fc0a0cb6ed947308c249859bc802dc", - "sha256": "bc8374d895a0d21bf1e13a2123c470557624151573531518933b6368efa7ade6" + "sha256": "0dd0794f805d0813957ae194b4bcff3b5c077828e85bda606f04c54df3b75a22" }, { "path": "src/log.rs", @@ -230,7 +230,7 @@ "path": "src/object/dict.rs", "source_sha256": "bcfefabe931614b79ea54940d02fce534eb2363f1b75a03d6428fd7a04c57981", "source_git_blob": "1e016b567e8ec17801fa8282c47f653092839cb5", - "sha256": "b5699e0774bbb4dfd1ad8c6fa713274c854f10d8afcc8cbe5544bdda79b59bde" + "sha256": "0ec184d3d5368ad67cac0af4c2f98914cc02d2ee7e169276e9ed929ffdb287f2" }, { "path": "src/object/indirect.rs", @@ -248,7 +248,7 @@ "path": "src/object/name.rs", "source_sha256": "c3fb0ec7aa3a1652ba2557b25fd059f3e5253b4534625888f342169967848a6c", "source_git_blob": "892d4d4f6d4c9806defe2c03ab4b1ae5565155b9", - "sha256": "c3fb0ec7aa3a1652ba2557b25fd059f3e5253b4534625888f342169967848a6c" + "sha256": "7ec134325c470344eb4637a3c6f856f92886c6346434e47e3b22449ace5df1dc" }, { "path": "src/object/null.rs", @@ -278,13 +278,13 @@ "path": "src/object/stream.rs", "source_sha256": "f5229eb2f52c9918830027c93112e042a9e97d34b5fec72f97dcc7de8c5410ac", "source_git_blob": "a80f0730cba317ed3771cee61d6a7ab18de296d7", - "sha256": "02151a961322606ed3de7fa5000faf7463fed72b5c59c596954ff00a2267799a" + "sha256": "dac5203740e5e7ca53ef66aea1f8177f491f851517e02081a7292819f47e62fe" }, { "path": "src/object/string.rs", "source_sha256": "cc0b8c7de9b665fe00b59608c97e3c10c8ec4e9ca8d5497789d2edcda7d6d5e9", "source_git_blob": "c168d59351810dcd6f9a0c6c64da164403114023", - "sha256": "cc0b8c7de9b665fe00b59608c97e3c10c8ec4e9ca8d5497789d2edcda7d6d5e9" + "sha256": "03ab01c59fc89ff92429be8de69165a483d122a5ae4af931fe10eaedbb8c23b9" }, { "path": "src/object/tuple.rs", @@ -296,19 +296,19 @@ "path": "src/page.rs", "source_sha256": "17be32b2eaa03a3367f1f20040556f7ee5b207588cf44a1af3e5253386fb46f9", "source_git_blob": "d7f0ec953722a938508f1f31f0adaecc0a181d20", - "sha256": "e39c9888ff5d1a6e6565d1e9fc810b2e4b285c9a014e5b887080a8839c7d5490" + "sha256": "b860e2a1d24eff3e3dab57f195a7c99c108f107eb3387ce917d47a9190a8053c" }, { "path": "src/pdf.rs", "source_sha256": "b6938cd27757a20dca83b49db46d73ab32a0ca2a474a0c4517d1d82b71a1be1c", "source_git_blob": "c196cb14acc1bfd4dd572d7fedd8345b40e44873", - "sha256": "c2efc38c9afa0eab8ef90ad0aaa393a4910555ca463b616ba86db767a385d7d7" + "sha256": "e3c2b63122a7e6a6266aed7b3c58c7983e95595797dc1ab95500717ffff28dc9" }, { "path": "src/reader.rs", "source_sha256": "f2f10a5cddecd70dae36304f432d052f4d7fdf5113dc4473a06de502375c4f2e", "source_git_blob": "818f767f7b982bbe759f9c81dc91f0ddce335d25", - "sha256": "126aa382d6c3e0f53cc9f4d8de34a984134612f7c93c5d14bb622c9d8b460f26" + "sha256": "a5ff6b8a90d3db6672b886558b1a1c13730eabbff4105233bcac637ac5324bfe" }, { "path": "src/sync.rs", @@ -338,13 +338,25 @@ "path": "src/xref.rs", "source_sha256": "51ffeeb7d4ac035bae2cd5ba9288c180088ae06cdcefe2b29dfeda560a793aa0", "source_git_blob": "20e33a410489d42e438a3f2b87d70c7bb8d66eae", - "sha256": "95261b80fd7bdc946de625325c6c6fc9fd7c0c5b3273ad62b8666dff215e5bd9" + "sha256": "f47507871327f04cc1fa30ed731dacd093144de7049342f8d11c72b6d0eeed70" }, { "path": "src/decode_limits_tests.rs", "source_sha256": null, "source_git_blob": null, - "sha256": "758bee2a3fb1c0ac59a4f9747f153d8d2ae7d44f5cdcea13d8a084b04b368283" + "sha256": "0c486b89709267199242934da6568ac20aa3eb5f3269bd227bace3940aecb09e" + }, + { + "path": "src/cancel.rs", + "source_sha256": null, + "source_git_blob": null, + "sha256": "88738ad2ebd3bf0ef1b9610f21c54480d576147aac23f088d89b6f80b5b4d8fb" + }, + { + "path": "src/filter/cancellation_tests.rs", + "source_sha256": null, + "source_git_blob": null, + "sha256": "d0d4054aa80ed9a732778f6a391ef12d327a3ce34de859704940445b8e10906e" } ], "compatibility_reference": { diff --git a/vendor/hayro-syntax/src/byte_reader.rs b/vendor/hayro-syntax/src/byte_reader.rs index e85d7c6e..4f722bb5 100644 --- a/vendor/hayro-syntax/src/byte_reader.rs +++ b/vendor/hayro-syntax/src/byte_reader.rs @@ -19,6 +19,8 @@ pub struct Reader<'a> { // PdfCraft patch: bound direct-object nesting while skipping arrays and dictionaries // and while parsing dictionaries (`Dict::skip`, `Array::skip`, `parse_dict_with`). skip_depth: usize, + control: Option, + next_check: Cell, } impl<'a> Reader<'a> { @@ -31,7 +33,26 @@ impl<'a> Reader<'a> { /// Create a new reader at the given offset. #[inline] pub fn new_with(data: &'a [u8], offset: usize) -> Self { - Self { data, offset, past_end: Cell::new(false), skip_depth: 0 } + Self { data, offset, past_end: Cell::new(false), skip_depth: 0, control: None, next_check: Cell::new(0) } + } + + pub(crate) fn set_control(&mut self, control: Option) { + if self.control.is_none() { + self.control = control; + } + } + + pub(crate) fn control(&self) -> Option<&crate::cancel::Control> { + self.control.as_ref() + } + + fn checkpoint(&self) -> Option<()> { + let next = self.next_check.get(); + if self.control.is_some() && (self.offset >= next || self.offset < next.saturating_sub(64 * 1024)) { + crate::cancel::check(self.control.as_ref())?; + self.next_check.set(self.offset.saturating_add(64 * 1024)); + } + Some(()) } /// PdfCraft patch: whether a read needed bytes past the end of the data since the flag was @@ -62,6 +83,9 @@ impl<'a> Reader<'a> { /// Returns `true` if the reader has reached the end of the data. #[inline] pub fn at_end(&self) -> bool { + if self.checkpoint().is_none() { + return true; + } let at_end = self.offset >= self.data.len(); if at_end { // PdfCraft patch: a parser that stops at the end decides from it (see `past_end`). @@ -148,6 +172,7 @@ impl<'a> Reader<'a> { /// Peeks the specified number of bytes. #[inline] pub fn peek_bytes(&self, len: usize) -> Option<&'a [u8]> { + self.checkpoint()?; let bytes = self .offset .checked_add(len) @@ -162,6 +187,7 @@ impl<'a> Reader<'a> { /// Peeks a single byte. #[inline] pub fn peek_byte(&self) -> Option { + self.checkpoint()?; let byte = self.data.get(self.offset).copied(); if byte.is_none() { self.past_end.set(true); @@ -281,6 +307,22 @@ mod tests { use alloc::format; use alloc::string::String; + #[test] + fn a_rewound_reader_keeps_checking_cancellation() { + let data = alloc::vec![0; 4 * 64 * 1024]; + let (control, polls) = crate::cancel::stop_at(3); + let mut reader = Reader::new(&data); + reader.set_control(Some(control)); + reader.jump(2 * 64 * 1024); + assert_eq!(reader.read_byte(), Some(0)); + reader.jump(0); + assert_eq!(reader.read_byte(), Some(0)); + reader.jump(64 * 1024); + assert!(reader.read_byte().is_none()); + assert_eq!(polls.load(core::sync::atomic::Ordering::Relaxed), 3); + assert_eq!(reader.offset(), 64 * 1024); + } + #[test] fn peek_bytes_rejects_overflowing_len() { let reader = Reader::new(b"abc"); diff --git a/vendor/hayro-syntax/src/cancel.rs b/vendor/hayro-syntax/src/cancel.rs new file mode 100644 index 00000000..79b7bec1 --- /dev/null +++ b/vendor/hayro-syntax/src/cancel.rs @@ -0,0 +1,176 @@ +//! Cancellation scoped to one PDF open, with no thread-local state. + +use crate::sync::{Arc, Mutex, MutexExt}; +use core::fmt; + +/// An owned cancellation callback, called synchronously on the opening thread. +/// +/// Capture shared state, such as an `Arc`, to request cancellation from another +/// thread. A request is latched separately for each open. Parsed PDFs do not retain or call +/// the callback after that open returns. +#[derive(Clone)] +pub struct Cancellation(Arc bool + Send + Sync>); + +impl Cancellation { + /// Own a callback that returns true when the current operation should stop. + pub fn new(check: impl Fn() -> bool + Send + Sync + 'static) -> Self { + Self(Arc::new(check)) + } + + /// Poll the callback. An opening operation latches a true result in its own control. + pub fn requested(&self) -> bool { + (self.0)() + } +} + +impl fmt::Debug for Cancellation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Cancellation").finish_non_exhaustive() + } +} + +#[derive(Debug)] +struct State { + callback: Option, + stopped: bool, +} + +#[derive(Clone, Debug)] +pub(crate) struct Control(Arc); + +#[derive(Debug)] +struct Inner { + state: Mutex, + #[cfg(feature = "std")] + active: core::sync::atomic::AtomicBool, + #[cfg(not(feature = "std"))] + active: core::cell::Cell, +} + +impl Control { + pub(crate) fn new(callback: &Cancellation) -> Self { + Self(Arc::new(Inner { + state: Mutex::new(State { callback: Some(callback.clone()), stopped: false }), + #[cfg(feature = "std")] + active: core::sync::atomic::AtomicBool::new(true), + #[cfg(not(feature = "std"))] + active: core::cell::Cell::new(true), + })) + } + + pub(crate) fn active(&self) -> bool { + #[cfg(feature = "std")] + { + self.0.active.load(core::sync::atomic::Ordering::Relaxed) + } + #[cfg(not(feature = "std"))] + { + self.0.active.get() + } + } + + pub(crate) fn check(&self) -> Option<()> { + if !self.active() { + return Some(()); + } + // Never invoke caller code under the cancellation-state lock. + let callback = { + let state = self.0.state.get(); + if state.stopped { + return None; + } + state.callback.clone() + }; + if callback.is_some_and(|c| c.requested()) { + self.0.state.get().stopped = true; + None + } else { + Some(()) + } + } + + fn finish(&self) { + let callback = self.0.state.get().callback.take(); + #[cfg(feature = "std")] + self.0.active.store(false, core::sync::atomic::Ordering::Relaxed); + #[cfg(not(feature = "std"))] + self.0.active.set(false); + drop(callback); + } +} + +pub(crate) fn check(control: Option<&Control>) -> Option<()> { + control.map_or(Some(()), Control::check) +} + +pub(crate) struct Checkpoints<'a> { + control: Option<&'a Control>, + next: usize, +} + +impl<'a> Checkpoints<'a> { + pub(crate) fn new(control: Option<&'a Control>) -> Self { + Self { control, next: 0 } + } + + pub(crate) fn at(&mut self, work: usize) -> Option<()> { + if self.control.is_some() && work >= self.next { + check(self.control)?; + self.next = work.saturating_add(64 * 1024); + } + Some(()) + } +} + +/// Remove callback captures on success, failure and unwinding. The xref may keep an inert +/// control after a successful open, so deferred reads do not poll a completed operation. +pub(crate) struct Scope(pub(crate) Control); + +impl Drop for Scope { + fn drop(&mut self) { + self.0.finish(); + } +} + +#[cfg(feature = "unsafe")] +pub(crate) struct Input<'a, R> { + inner: R, + control: Option<&'a Control>, + left: usize, +} + +#[cfg(feature = "unsafe")] +impl<'a, R> Input<'a, R> { + pub(crate) fn new(inner: R, control: Option<&'a Control>) -> Self { + Self { inner, control, left: 0 } + } +} + +#[cfg(feature = "unsafe")] +impl std::io::Read for Input<'_, R> { + fn read(&mut self, bytes: &mut [u8]) -> std::io::Result { + if self.control.is_none() { + return self.inner.read(bytes); + } + if bytes.is_empty() { + return Ok(0); + } + if self.left == 0 { + check(self.control).ok_or_else(|| std::io::Error::other("PDF opening canceled"))?; + self.left = 64 * 1024; + } + let len = self.left.min(bytes.len()); + let n = self.inner.read(&mut bytes[..len])?; + self.left -= n; + Ok(n) + } +} + +#[cfg(test)] +pub(crate) fn stop_at(n: usize) -> (Control, alloc::sync::Arc) { + use core::sync::atomic::{AtomicUsize, Ordering}; + let polls = alloc::sync::Arc::new(AtomicUsize::new(0)); + let counter = polls.clone(); + let cancel = Cancellation::new(move || counter.fetch_add(1, Ordering::Relaxed) + 1 == n); + (Control::new(&cancel), polls) +} diff --git a/vendor/hayro-syntax/src/decode_limits_tests.rs b/vendor/hayro-syntax/src/decode_limits_tests.rs index 4f3af09e..9f95fb72 100644 --- a/vendor/hayro-syntax/src/decode_limits_tests.rs +++ b/vendor/hayro-syntax/src/decode_limits_tests.rs @@ -141,6 +141,35 @@ fn ordinary_stream_expansion_is_document_local_and_keeps_caller_budget() { assert_eq!(low.xref().clone().decode_limits(), low.decode_limits()); } +#[test] +fn uncanceled_open_preserves_stream_limits_and_repair_results() { + let cancel = crate::Cancellation::new(|| false); + for input in [ordinary_stream_pdf(), object_stream_pdf(false).0, object_stream_pdf(true).0] { + for cap in [0, 32, 512, MAX_DECODED_STREAM] { + let limits = DecodeLimits::default().with_max_decoded_stream(cap); + let normal = Pdf::new_with_limits(input.clone(), limits); + let controlled = Pdf::new_with_password_limits_and_cancel(input.clone(), "", limits, &cancel); + match (normal, controlled) { + (Ok(normal), Ok(controlled)) => { + assert_eq!(controlled.decode_limits(), normal.decode_limits()); + assert_eq!(controlled.pages().len(), normal.pages().len()); + for object in [40, 41] { + assert_eq!(marker(&controlled, object), marker(&normal, object)); + } + let stream = |pdf: &Pdf| { + pdf.xref().get::>(ObjectIdentifier::new(4, 0)).map(|s| { + s.decoded().map(|bytes| bytes.into_owned()).map_err(|e| format!("{e:?}")) + }) + }; + assert_eq!(stream(&controlled), stream(&normal)); + } + (Err(expected), Err(crate::CancellableLoadError::Load(actual))) => assert_eq!(actual, expected), + _ => panic!("an uncanceled constructor changed the loading result"), + } + } + } +} + #[test] fn final_known_size_images_keep_their_distinct_pixel_budget() { let limits = DecodeLimits::default().with_max_decoded_stream(32); diff --git a/vendor/hayro-syntax/src/filter/ascii_85.rs b/vendor/hayro-syntax/src/filter/ascii_85.rs index b7199365..f3ce4866 100644 --- a/vendor/hayro-syntax/src/filter/ascii_85.rs +++ b/vendor/hayro-syntax/src/filter/ascii_85.rs @@ -7,9 +7,14 @@ use alloc::vec::Vec; /// PdfCraft patch: `limit` is the most bytes produced (see `filter::decode_limit`); each `z` costs /// one input byte and yields four, so a Flate stream of them would outgrow the budget. pub(crate) fn decode(data: &[u8], limit: usize) -> Option> { + decode_controlled(data, limit, None) +} + +pub(crate) fn decode_controlled(data: &[u8], limit: usize, control: Option<&crate::cancel::Control>) -> Option> { const POW_85: [u32; 5] = [52200625, 614125, 7225, 85, 1]; let mut reader = Reader::new(data); + reader.set_control(control.cloned()); let mut read_byte = || -> Option { loop { @@ -69,6 +74,7 @@ pub(crate) fn decode(data: &[u8], limit: usize) -> Option> { } let Some(b) = read_byte() else { + crate::cancel::check(control)?; // Be lenient and accept what we have (see PDFBOX-5910). flush_group(&mut group, &mut decoded)?; decoded.truncate(limit); diff --git a/vendor/hayro-syntax/src/filter/ascii_hex.rs b/vendor/hayro-syntax/src/filter/ascii_hex.rs index edcd82d5..c95ec303 100644 --- a/vendor/hayro-syntax/src/filter/ascii_hex.rs +++ b/vendor/hayro-syntax/src/filter/ascii_hex.rs @@ -9,11 +9,17 @@ pub(crate) fn decode(data: &[u8]) -> Option> { } pub(crate) fn decode_into(data: &[u8]) -> Option { + decode_into_controlled(data, None) +} + +pub(crate) fn decode_into_controlled(data: &[u8], control: Option<&crate::cancel::Control>) -> Option { + let mut checks = crate::cancel::Checkpoints::new(control); // Find end (at `>` or end of data) and check for whitespace. let mut has_whitespace = false; let mut end = data.len(); for (idx, byte) in data.iter().enumerate() { + checks.at(idx)?; has_whitespace |= is_white_space_character(*byte); if *byte == b'>' { @@ -29,7 +35,9 @@ pub(crate) fn decode_into(data: &[u8]) -> Option { // Fast path, don't need to worry about white spaces. let (chunks, remainder) = data.as_chunks::<2>(); - for &[hi, lo] in chunks { + let mut checks = crate::cancel::Checkpoints::new(control); + for (i, &[hi, lo]) in chunks.iter().enumerate() { + checks.at(i * 2)?; decoded.push(decode_hex_digit(hi)? << 4 | decode_hex_digit(lo)?); } @@ -38,10 +46,12 @@ pub(crate) fn decode_into(data: &[u8]) -> Option { } } else { // Slow path, need to strip white spaces. - let mut iter = data.iter().copied(); + let mut iter = data.iter().copied().enumerate(); + let mut checks = crate::cancel::Checkpoints::new(control); let mut read_byte = || -> Option { loop { - let b = iter.next()?; + let (i, b) = iter.next()?; + checks.at(i)?; if !is_white_space_character(b) { return Some(b); } @@ -63,6 +73,7 @@ pub(crate) fn decode_into(data: &[u8]) -> Option { } } + crate::cancel::check(control)?; Some(decoded) } diff --git a/vendor/hayro-syntax/src/filter/cancellation_tests.rs b/vendor/hayro-syntax/src/filter/cancellation_tests.rs new file mode 100644 index 00000000..d072df34 --- /dev/null +++ b/vendor/hayro-syntax/src/filter/cancellation_tests.rs @@ -0,0 +1,75 @@ +use super::*; +use alloc::{vec, vec::Vec}; +use core::sync::atomic::Ordering; + +#[test] +fn a_stream_without_decode_params_keeps_its_open_control() { + use crate::reader::{Reader, ReaderContext, ReaderExt}; + use core::sync::atomic::{AtomicBool, AtomicUsize}; + let armed = alloc::sync::Arc::new(AtomicBool::new(false)); + let polls = alloc::sync::Arc::new(AtomicUsize::new(0)); + let flag = armed.clone(); + let counter = polls.clone(); + let cancel = crate::Cancellation::new(move || flag.load(Ordering::Relaxed) && counter.fetch_add(1, Ordering::Relaxed) + 1 == 4); + let control = crate::cancel::Control::new(&cancel); + let ctx = ReaderContext::dummy_with_control(crate::DecodeLimits::default(), Some(control)); + let mut raw = [129, 0].repeat(30_000); + raw.push(128); + let mut bytes = alloc::format!("<< /Length {} /Filter /RunLengthDecode >> stream\n", raw.len()).into_bytes(); + bytes.extend(raw); + bytes.extend_from_slice(b"\nendstream"); + let stream = Reader::new(&bytes).read_with_context::>(&ctx).unwrap(); + armed.store(true, Ordering::Relaxed); + assert!(stream.decoded().is_err()); + assert_eq!(polls.load(Ordering::Relaxed), 4); +} + +#[test] +fn ascii_decoders_stop_during_long_scans() { + let mut ascii85 = vec![b' '; 2 << 20]; + ascii85.extend_from_slice(b"z~>"); + assert_eq!(ascii_85::decode(&ascii85, usize::MAX), Some(vec![0; 4])); + let (control, polls) = crate::cancel::stop_at(3); + assert!(ascii_85::decode_controlled(&ascii85, usize::MAX, Some(&control)).is_none()); + assert_eq!(polls.load(Ordering::Relaxed), 3); + + let asciihex = vec![b'0'; 2 << 20]; + let (control, polls) = crate::cancel::stop_at(3); + assert!(ascii_hex::decode_into_controlled::>(&asciihex, Some(&control)).is_none()); + assert_eq!(polls.load(Ordering::Relaxed), 3); +} + +#[test] +fn run_length_stops_before_finishing_its_expansion() { + let mut input = [129, 0].repeat(30_000); + input.push(128); + assert_eq!(run_length::decode(&input, usize::MAX).map(|v| v.len()), Some(128 * 30_000)); + let (control, polls) = crate::cancel::stop_at(4); + assert!(run_length::decode_controlled(&input, usize::MAX, Some(&control)).is_none()); + assert_eq!(polls.load(Ordering::Relaxed), 4); +} + +#[test] +fn flate_stops_inside_stream_decoding() { + // Valid zlib data containing forty stored blocks of zeroes, including its Adler32. + let mut input = vec![0x78, 0x01]; + for i in 0..40 { + input.extend_from_slice(&[u8::from(i == 39), 0xff, 0xff, 0, 0]); + input.extend(core::iter::repeat_n(0, 65535)); + } + let length = 40 * 65535; + input.extend_from_slice(&(((length % 65521) as u32) << 16 | 1).to_be_bytes()); + assert_eq!(lzw_flate::flate::decode(&input, &Dict::empty(), usize::MAX).map(|v| v.len()), Some(length)); + let (control, polls) = crate::cancel::stop_at(3); + assert!(lzw_flate::flate::decode_controlled(&input, &Dict::empty(), usize::MAX, Some(&control)).is_none()); + assert_eq!(polls.load(Ordering::Relaxed), 3); +} + +#[test] +fn lzw_without_output_still_observes_cancellation() { + // Eight consecutive nine-bit clear-table codes, repeated without producing a pixel. + let input = [0x80, 0x40, 0x20, 0x10, 8, 4, 2, 1, 0].repeat(65536); + let (control, polls) = crate::cancel::stop_at(4); + assert!(lzw_flate::lzw::decode_controlled(&input, &Dict::empty(), usize::MAX, Some(&control)).is_none()); + assert_eq!(polls.load(Ordering::Relaxed), 4); +} diff --git a/vendor/hayro-syntax/src/filter/lzw_flate.rs b/vendor/hayro-syntax/src/filter/lzw_flate.rs index 9642a749..134a139e 100644 --- a/vendor/hayro-syntax/src/filter/lzw_flate.rs +++ b/vendor/hayro-syntax/src/filter/lzw_flate.rs @@ -20,14 +20,20 @@ pub(crate) mod flate { /// PdfCraft patch: `limit` is the most bytes inflated (see `filter::decode_limit`). #[cfg(feature = "unsafe")] pub(crate) fn decode(data: &[u8], params: &Dict<'_>, limit: usize) -> Option> { + decode_controlled(data, params, limit, None) + } + + #[cfg(feature = "unsafe")] + pub(crate) fn decode_controlled(data: &[u8], params: &Dict<'_>, limit: usize, control: Option<&crate::cancel::Control>) -> Option> { use flate2::read::{DeflateDecoder, ZlibDecoder}; use std::io::Read; // Read in chunks and never past `limit`, so the buffer doesn't grow past it either. - fn read_up_to(mut decoder: impl Read, limit: usize) -> Option> { + fn read_up_to(mut decoder: impl Read, limit: usize, control: Option<&crate::cancel::Control>) -> Option> { let mut result = Vec::new(); let mut chunk = vec![0; 64 * 1024]; while result.len() < limit { + crate::cancel::check(control)?; let n = decoder.read(&mut chunk).ok()?; if n == 0 { break; @@ -37,32 +43,39 @@ pub(crate) mod flate { } Some(result) } - let zlib_stream = |data: &[u8]| read_up_to(ZlibDecoder::new(data), limit); - let deflate_stream = |data: &[u8]| read_up_to(DeflateDecoder::new(data), limit); + let zlib_stream = |data: &[u8]| read_up_to(ZlibDecoder::new(crate::cancel::Input::new(data, control)), limit, control); + let deflate_stream = |data: &[u8]| read_up_to(DeflateDecoder::new(crate::cancel::Input::new(data, control)), limit, control); let decoded = zlib_stream(data) .or_else(|| deflate_stream(data)) .or_else(|| { warn!("flate stream is broken, decoding with fallback"); - fallback::decode(data, limit) + fallback::decode(data, limit, control) })?; // PdfCraft patch: output cut at the limit is reported, never dropped silently. if decoded.len() >= limit { warn!("flate stream stopped at its decode limit of {limit} bytes"); } - let params = PredictorParams::from_params(params); + let mut params = PredictorParams::from_params(params); + params.control = control.cloned(); apply_predictor(decoded, ¶ms) } #[cfg(not(feature = "unsafe"))] pub(crate) fn decode(data: &[u8], params: &Dict<'_>, limit: usize) -> Option> { - let decoded = fallback::decode(data, limit)?; + decode_controlled(data, params, limit, None) + } + + #[cfg(not(feature = "unsafe"))] + pub(crate) fn decode_controlled(data: &[u8], params: &Dict<'_>, limit: usize, control: Option<&crate::cancel::Control>) -> Option> { + let decoded = fallback::decode(data, limit, control)?; // PdfCraft patch: output cut at the limit is reported, never dropped silently. if decoded.len() >= limit { warn!("flate stream stopped at its decode limit of {limit} bytes"); } - let params = PredictorParams::from_params(params); + let mut params = PredictorParams::from_params(params); + params.control = control.cloned(); apply_predictor(decoded, ¶ms) } @@ -72,15 +85,16 @@ pub(crate) mod flate { use alloc::vec; use alloc::vec::Vec; - pub(crate) fn decode(data: &[u8], limit: usize) -> Option> { - flate_decode(data, limit) + pub(crate) fn decode(data: &[u8], limit: usize, control: Option<&crate::cancel::Control>) -> Option> { + flate_decode(data, limit, control) } - fn flate_decode(data: &[u8], limit: usize) -> Option> { + fn flate_decode(data: &[u8], limit: usize, control: Option<&crate::cancel::Control>) -> Option> { let mut state = FlateState::new(data); let mut output = Vec::new(); - state.decode_into(data, &mut output, limit); + state.decode_into_controlled(data, &mut output, limit, control); + crate::cancel::check(control)?; Some(output) } @@ -133,6 +147,10 @@ pub(crate) mod flate { /// Appends decoded bytes to `out` until it holds `until` bytes or the stream ends. /// `data` is the whole encoded stream, the same on every call. pub(crate) fn decode_into(&mut self, data: &[u8], out: &mut Vec, until: usize) { + self.decode_into_controlled(data, out, until, None); + } + + pub(crate) fn decode_into_controlled(&mut self, data: &[u8], out: &mut Vec, until: usize, control: Option<&crate::cancel::Control>) { // The working buffer is the history followed by the bytes decoded now. Only the // latter go to `out`, which the caller may have drained since. let base = self.history.len(); @@ -149,10 +167,21 @@ pub(crate) mod flate { failed: self.failed, limit: base.saturating_add(until.saturating_sub(out.len())), block: core::mem::take(&mut self.block), + control, }; stream.run(); - out.extend_from_slice(stream.output.get(base..).unwrap_or(&[])); + let bytes = stream.output.get(base..).unwrap_or(&[]); + if control.is_some() { + for chunk in bytes.chunks(64 * 1024) { + if crate::cancel::check(control).is_none() { + break; + } + out.extend_from_slice(chunk); + } + } else { + out.extend_from_slice(bytes); + } self.total = self.total.saturating_add(stream.output.len() - base); let keep = stream.output.len().min(WINDOW); stream.output.drain(..stream.output.len() - keep); @@ -171,6 +200,7 @@ pub(crate) mod flate { /// PdfCraft patch: the working state of one [`FlateState::decode_into`] call. struct FlateStream<'a> { data: &'a [u8], + control: Option<&'a crate::cancel::Control>, pos: usize, code_buf: u32, code_size: u8, @@ -210,6 +240,11 @@ pub(crate) mod flate { /// Decodes until `output` holds `limit` bytes or the stream ends. fn run(&mut self) { loop { + if crate::cancel::check(self.control).is_none() { + self.eof = true; + self.failed = true; + return; + } if self.eof || self.output.len() >= self.limit { return; } @@ -444,6 +479,7 @@ pub(crate) mod flate { /// Decodes the codes of a Huffman-coded block until its end, the limit or the end of /// the data. fn read_codes(&mut self) { + let mut checks = crate::cancel::Checkpoints::new(self.control); let Block::Coded { lit: lit_code_table, dist: dist_code_table, @@ -455,6 +491,11 @@ pub(crate) mod flate { }; loop { + if checks.at(self.output.len()).is_none() { + self.eof = true; + self.failed = true; + return; + } if let Some((valid, distance, left)) = copy { let left = self.copy_back(valid, distance, left); if left > 0 { @@ -813,9 +854,14 @@ pub(crate) mod lzw { /// `filter::decode_limit`); with a full table every 12-bit code can repeat an entry of /// thousands of bytes. pub(crate) fn decode(data: &[u8], params: &Dict<'_>, limit: usize) -> Option> { - let params = PredictorParams::from_params(params); + decode_controlled(data, params, limit, None) + } - let decoded = decode_impl(data, params.early_change, limit)?; + pub(crate) fn decode_controlled(data: &[u8], params: &Dict<'_>, limit: usize, control: Option<&crate::cancel::Control>) -> Option> { + let mut params = PredictorParams::from_params(params); + params.control = control.cloned(); + + let decoded = decode_impl(data, params.early_change, limit, control)?; // PdfCraft patch: output cut at the limit is reported, never dropped silently. if decoded.len() >= limit { warn!("LZW stream stopped at its decode limit of {limit} bytes"); @@ -829,14 +875,21 @@ pub(crate) mod lzw { const MAX_ENTRIES: usize = 4096; const INITIAL_SIZE: u16 = 258; - fn decode_impl(data: &[u8], early_change: bool, limit: usize) -> Option> { + fn decode_impl(data: &[u8], early_change: bool, limit: usize, control: Option<&crate::cancel::Control>) -> Option> { let mut table = Table::new(early_change); let mut bit_size = table.code_length(); let mut reader = BitReader::new(data); let mut decoded = vec![]; let mut prev = None; + let mut checks = crate::cancel::Checkpoints::new(control); + let mut output_checks = crate::cancel::Checkpoints::new(control); + let mut codes = 0; loop { + // Count codes as well as output so repeated clear-table codes remain interruptible. + checks.at(codes)?; + output_checks.at(decoded.len())?; + codes += 1; if decoded.len() >= limit { return Some(decoded); } @@ -965,6 +1018,7 @@ struct PredictorParams { bits_per_component: u8, columns: usize, early_change: bool, + control: Option, } impl PredictorParams { @@ -985,6 +1039,7 @@ impl Default for PredictorParams { bits_per_component: 8, columns: 1, early_change: true, + control: None, } } } @@ -997,11 +1052,13 @@ impl PredictorParams { bits_per_component: dict.get(BITS_PER_COMPONENT).unwrap_or(8), columns: dict.get(COLUMNS).unwrap_or(1), early_change: dict.get::(EARLY_CHANGE).map(|e| e != 0).unwrap_or(true), + control: None, } } } fn apply_predictor(data: Vec, params: &PredictorParams) -> Option> { + crate::cancel::check(params.control.as_ref())?; match params.predictor { 1 => Some(data), i => { @@ -1048,8 +1105,10 @@ fn apply_predictor(data: Vec, params: &PredictorParams) -> Option> { let zero_col = BitChunk::new(0, chunk_len); let mut out = vec![0; num_rows * row_len]; let mut writer = BitWriter::new(&mut out, bit_size)?; + let run = PredictorRun { chunk_len, bit_size, control: params.control.as_ref() }; for in_row in data.chunks_exact(total_row_len) { + crate::cancel::check(params.control.as_ref())?; if is_png_predictor { let predictor = in_row[0]; let in_data = &in_row[1..]; @@ -1062,8 +1121,7 @@ fn apply_predictor(data: Vec, params: &PredictorParams) -> Option> { zero_col.clone(), in_data_chunks, &mut writer, - chunk_len, - bit_size, + run, )?, 2 => apply::( prev_row, @@ -1071,8 +1129,7 @@ fn apply_predictor(data: Vec, params: &PredictorParams) -> Option> { zero_col.clone(), in_data_chunks, &mut writer, - chunk_len, - bit_size, + run, )?, 3 => apply::( prev_row, @@ -1080,8 +1137,7 @@ fn apply_predictor(data: Vec, params: &PredictorParams) -> Option> { zero_col.clone(), in_data_chunks, &mut writer, - chunk_len, - bit_size, + run, )?, 4 => apply::( prev_row, @@ -1089,14 +1145,15 @@ fn apply_predictor(data: Vec, params: &PredictorParams) -> Option> { zero_col.clone(), in_data_chunks, &mut writer, - chunk_len, - bit_size, + run, )?, _ => { // Just copy the data. let mut reader = BitReader::new(in_data); + let mut checks = crate::cancel::Checkpoints::new(params.control.as_ref()); while let Some(data) = reader.read(bit_size) { + checks.at(writer.cur_pos())?; writer.write(data); } } @@ -1108,9 +1165,8 @@ fn apply_predictor(data: Vec, params: &PredictorParams) -> Option> { zero_col.clone(), BitChunks::new(in_row, bit_size, chunk_len)?, &mut writer, - chunk_len, - bit_size, - ); + run, + )?; } else { warn!("unknown predictor {i}"); @@ -1130,16 +1186,25 @@ fn apply_predictor(data: Vec, params: &PredictorParams) -> Option> { } } +#[derive(Clone, Copy)] +struct PredictorRun<'a> { + chunk_len: usize, + bit_size: u8, + control: Option<&'a crate::cancel::Control>, +} + fn apply<'a, T: Predictor>( prev_row: BitChunks<'a>, mut prev_col: BitChunk, mut top_left: BitChunk, cur_row: BitChunks<'a>, writer: &mut BitWriter<'a>, - chunk_len: usize, - bit_size: u8, + run: PredictorRun<'_>, ) -> Option<()> { + let PredictorRun { chunk_len, bit_size, control } = run; + let mut checks = crate::cancel::Checkpoints::new(control); for (cur_row, prev_row) in cur_row.zip(prev_row) { + checks.at(writer.cur_pos())?; let old_pos = writer.cur_pos(); for (((cur_row, prev_row), prev_col), top_left) in cur_row @@ -1262,6 +1327,21 @@ mod tests { ] } + #[test] + fn a_wide_predictor_row_can_be_canceled_partway_through() { + let (control, polls) = crate::cancel::stop_at(4); + let params = PredictorParams { + predictor: 12, + columns: (2 << 20) - 1, + control: Some(control), + ..PredictorParams::default() + }; + let mut input = vec![0; 2 << 20]; + input[0] = 2; + assert!(apply_predictor(input, ¶ms).is_none()); + assert_eq!(polls.load(core::sync::atomic::Ordering::Relaxed), 4); + } + fn predictor_test(predictor: u8, input: &[u8]) { let params = PredictorParams { predictor, @@ -1269,6 +1349,7 @@ mod tests { bits_per_component: 8, columns: 3, early_change: false, + control: None, }; let expected = predictor_expected(); diff --git a/vendor/hayro-syntax/src/filter/mod.rs b/vendor/hayro-syntax/src/filter/mod.rs index 9da38e20..271322af 100644 --- a/vendor/hayro-syntax/src/filter/mod.rs +++ b/vendor/hayro-syntax/src/filter/mod.rs @@ -13,6 +13,9 @@ mod jpx; mod lzw_flate; mod run_length; +#[cfg(test)] +mod cancellation_tests; + // PdfCraft patch: the incremental Flate decoder that `page::ContentReader` drives. pub(crate) use lzw_flate::flate::{FlateState, is_plain as flate_is_plain}; @@ -90,21 +93,23 @@ impl Filter { params: &Dict<'_>, #[cfg_attr(not(feature = "images"), allow(unused))] image_params: &ImageDecodeParams, limit: usize, + control: Option<&crate::cancel::Control>, ) -> Result, DecodeFailure> { + crate::cancel::check(control).ok_or(DecodeFailure::StreamDecode)?; let res = match self { - Self::AsciiHexDecode => ascii_hex::decode(data) + Self::AsciiHexDecode => (if control.is_some() { ascii_hex::decode_into_controlled(data, control) } else { ascii_hex::decode(data) }) .map(FilterResult::from_data) .ok_or(DecodeFailure::StreamDecode), - Self::Ascii85Decode => ascii_85::decode(data, limit) + Self::Ascii85Decode => (if control.is_some() { ascii_85::decode_controlled(data, limit, control) } else { ascii_85::decode(data, limit) }) .map(FilterResult::from_data) .ok_or(DecodeFailure::StreamDecode), - Self::RunLengthDecode => run_length::decode(data, limit) + Self::RunLengthDecode => (if control.is_some() { run_length::decode_controlled(data, limit, control) } else { run_length::decode(data, limit) }) .map(FilterResult::from_data) .ok_or(DecodeFailure::StreamDecode), - Self::LzwDecode => lzw_flate::lzw::decode(data, params, limit) + Self::LzwDecode => (if control.is_some() { lzw_flate::lzw::decode_controlled(data, params, limit, control) } else { lzw_flate::lzw::decode(data, params, limit) }) .map(FilterResult::from_data) .ok_or(DecodeFailure::StreamDecode), - Self::FlateDecode => lzw_flate::flate::decode(data, params, limit) + Self::FlateDecode => (if control.is_some() { lzw_flate::flate::decode_controlled(data, params, limit, control) } else { lzw_flate::flate::decode(data, params, limit) }) .map(FilterResult::from_data) .ok_or(DecodeFailure::StreamDecode), #[cfg(feature = "images")] @@ -123,6 +128,7 @@ impl Filter { _ => Err(DecodeFailure::StreamDecode), }; + crate::cancel::check(control).ok_or(DecodeFailure::StreamDecode)?; if res.is_err() { warn!("failed to apply filter {}", self.debug_name()); } diff --git a/vendor/hayro-syntax/src/filter/run_length.rs b/vendor/hayro-syntax/src/filter/run_length.rs index 2bef8e70..f7a6f814 100644 --- a/vendor/hayro-syntax/src/filter/run_length.rs +++ b/vendor/hayro-syntax/src/filter/run_length.rs @@ -5,10 +5,17 @@ use alloc::vec::Vec; /// PdfCraft patch: `limit` is the most bytes produced (see `filter::decode_limit`); a run of 128 /// copies costs two input bytes, so a few megabytes could ask for gigabytes. pub(crate) fn decode(data: &[u8], limit: usize) -> Option> { + decode_controlled(data, limit, None) +} + +pub(crate) fn decode_controlled(data: &[u8], limit: usize, control: Option<&crate::cancel::Control>) -> Option> { let mut reader = Reader::new(data); + reader.set_control(control.cloned()); let mut decoded = vec![]; + let mut checks = crate::cancel::Checkpoints::new(control); loop { + checks.at(decoded.len())?; let left = limit.saturating_sub(decoded.len()); if left == 0 { break; diff --git a/vendor/hayro-syntax/src/lib.rs b/vendor/hayro-syntax/src/lib.rs index 99c203c4..e1740f29 100644 --- a/vendor/hayro-syntax/src/lib.rs +++ b/vendor/hayro-syntax/src/lib.rs @@ -76,6 +76,8 @@ pub(crate) mod math; pub(crate) mod sync; mod data; +mod cancel; +pub use cancel::Cancellation; pub(crate) mod filter; pub(crate) mod pdf; pub(crate) mod trivia; diff --git a/vendor/hayro-syntax/src/object/dict.rs b/vendor/hayro-syntax/src/object/dict.rs index 8366d43a..a5d84ae2 100644 --- a/vendor/hayro-syntax/src/object/dict.rs +++ b/vendor/hayro-syntax/src/object/dict.rs @@ -371,6 +371,8 @@ where { // An inline image dictionary has no enclosing << >> pair; count only actual // containers. Its array/dictionary operands still enter the same depth guard. + crate::cancel::check(ctx.control())?; + r.set_control(ctx.control().cloned()); let container = start_tag.is_some(); if container { r.enter_container()?; diff --git a/vendor/hayro-syntax/src/object/name.rs b/vendor/hayro-syntax/src/object/name.rs index 892d4d4f..dc267878 100644 --- a/vendor/hayro-syntax/src/object/name.rs +++ b/vendor/hayro-syntax/src/object/name.rs @@ -101,8 +101,13 @@ impl<'a> Name<'a> { /// Create a new name from bytes that may contain escape sequences. #[inline] pub fn new_escaped(data: &'a [u8]) -> Option { + Self::new_escaped_controlled(data, None) + } + + fn new_escaped_controlled(data: &'a [u8], control: Option<&crate::cancel::Control>) -> Option { let mut result = SmallVec::new(); let mut r = Reader::new(data); + r.set_control(control.cloned()); while let Some(b) = r.read_byte() { if b == b'#' { @@ -113,6 +118,7 @@ impl<'a> Name<'a> { } } + crate::cancel::check(control)?; Some(Self(NameInner::Owned(result))) } @@ -149,7 +155,17 @@ impl<'a> Readable<'a> for Name<'a> { // Exclude leading solidus. let data = r.range(start + 1..end)?; - Self::new(data) + if let Some(control) = r.control() { + for chunk in data.chunks(64 * 1024) { + control.check()?; + if chunk.contains(&b'#') { + return Self::new_escaped_controlled(data, Some(control)); + } + } + Some(Self::new_unescaped(data)) + } else { + Self::new(data) + } } } diff --git a/vendor/hayro-syntax/src/object/stream.rs b/vendor/hayro-syntax/src/object/stream.rs index 5818fcbf..c17a6eae 100644 --- a/vendor/hayro-syntax/src/object/stream.rs +++ b/vendor/hayro-syntax/src/object/stream.rs @@ -214,6 +214,7 @@ impl<'a> Stream<'a> { image_params: &ImageDecodeParams, max: usize, ) -> Result, DecodeFailure> { + crate::cancel::check(self.dict.ctx().control()).ok_or(DecodeFailure::StreamDecode)?; self.decode_raw_within(self.raw_data(), image_params, max) } @@ -233,7 +234,10 @@ impl<'a> Stream<'a> { image_params: &ImageDecodeParams, max: usize, ) -> Result, DecodeFailure> { + let control = self.dict.ctx().control(); + crate::cancel::check(control).ok_or(DecodeFailure::StreamDecode)?; let filters_and_params = self.filters_and_params(); + crate::cancel::check(control).ok_or(DecodeFailure::StreamDecode)?; let count = filters_and_params.filters.len(); let mut current: Option> = None; @@ -246,10 +250,11 @@ impl<'a> Stream<'a> { { // PdfCraft patch: see `filter::decode_limit`. let limit = crate::filter::decode_limit(image_params, i + 1 == count, self.dict.ctx().decode_limits()).min(max); - let new = filter.apply(current.as_ref().map(|c| c.data.as_ref()).unwrap_or(&data), params, image_params, limit)?; + let new = filter.apply(current.as_ref().map(|c| c.data.as_ref()).unwrap_or(&data), params, image_params, limit, self.dict.ctx().control())?; current = Some(new); } + crate::cancel::check(control).ok_or(DecodeFailure::StreamDecode)?; Ok(current.unwrap_or(FilterResult { data, image_data: None, @@ -285,6 +290,7 @@ impl<'a> Readable<'a> for Stream<'a> { let offset = r.offset(); parse_proper(r, &dict) .or_else(|| { + crate::cancel::check(ctx.control())?; warn!("failed to parse stream, trying to parse it manually"); r.jump(offset); diff --git a/vendor/hayro-syntax/src/object/string.rs b/vendor/hayro-syntax/src/object/string.rs index c168d593..80100675 100644 --- a/vendor/hayro-syntax/src/object/string.rs +++ b/vendor/hayro-syntax/src/object/string.rs @@ -150,7 +150,7 @@ fn read_hex(r: &mut Reader<'_>) -> Option> { // Exclude outer brackets. let raw = r.range(start + 1..end - 1)?; - let decoded = ascii_hex::decode_into(raw)?; + let decoded = ascii_hex::decode_into_controlled(raw, r.control())?; Some(decoded) } @@ -183,11 +183,21 @@ fn read_literal<'a>(r: &mut Reader<'a>) -> Option> { // Exclude outer parentheses. let data = r.range(start + 1..end - 1)?; - if !data.iter().any(|b| matches!(b, b'\\' | b'\n' | b'\r')) { + let mut special = false; + for chunk in data.chunks(64 * 1024) { + crate::cancel::check(r.control())?; + if chunk.iter().any(|b| matches!(b, b'\\' | b'\n' | b'\r')) { + special = true; + break; + } + } + if !special { return Some(StringInner::Borrowed(data)); } + let control = r.control().cloned(); let mut r = Reader::new(data); + r.set_control(control); let mut result = SmallVec::new(); while let Some(byte) = r.read_byte() { diff --git a/vendor/hayro-syntax/src/page.rs b/vendor/hayro-syntax/src/page.rs index 62e8c457..58ccbbbe 100644 --- a/vendor/hayro-syntax/src/page.rs +++ b/vendor/hayro-syntax/src/page.rs @@ -66,9 +66,11 @@ impl<'a> Pages<'a> { /// Of course this could result in the order of pages being messed up, but /// this is still better than nothing. pub(crate) fn new_brute_force(ctx: &ReaderContext<'a>, xref: &'a XRef) -> Option { + crate::cancel::check(ctx.control())?; let mut pages = vec![]; for object in xref.objects() { + crate::cancel::check(ctx.control())?; if let Some(dict) = object.into_dict() && let Some(page) = Page::new( &dict, @@ -81,6 +83,7 @@ impl<'a> Pages<'a> { } } + crate::cancel::check(ctx.control())?; if pages.is_empty() { return None; } @@ -113,6 +116,7 @@ const MAX_PAGE_TREE_WORK: usize = 100_000; enum PageTreeError { Malformed, Limit, + Cancelled, } struct PageTreeFrame<'a> { @@ -142,6 +146,7 @@ fn resolve_pages<'a>( reader_ctx: &ReaderContext<'a>, work_limit: usize, ) -> Result<(), PageTreeError> { + crate::cancel::check(reader_ctx.control()).ok_or(PageTreeError::Cancelled)?; let mut remaining = work_limit.checked_sub(1).ok_or(PageTreeError::Limit)?; let root = PageTreeFrame::new(pages_dict, PagesContext::new(), Resources::new(Dict::empty(), None, reader_ctx)).ok_or(PageTreeError::Malformed)?; @@ -154,6 +159,7 @@ fn resolve_pages<'a>( stack.push(root); while let Some(frame) = stack.last_mut() { + crate::cancel::check(reader_ctx.control()).ok_or(PageTreeError::Cancelled)?; let Some(kid) = frame.kids.next() else { stack.pop(); continue; @@ -196,6 +202,7 @@ fn resolve_pages<'a>( } } + crate::cancel::check(reader_ctx.control()).ok_or(PageTreeError::Cancelled)?; Ok(()) } @@ -882,7 +889,7 @@ pub(crate) mod cached { Err(PageTreeError::Malformed) => Pages::new_brute_force(&ctx, xref_reference)?, // A limit is a failed load, never permission to bypass the bound // or return a partially walked page tree through repair. - Err(PageTreeError::Limit) => return None, + Err(PageTreeError::Limit | PageTreeError::Cancelled) => return None, }, None => Pages::new_brute_force(&ctx, xref_reference)?, }; @@ -925,6 +932,30 @@ mod page_tree_tests { cached::CachedPages::new_with_limit(cached::Arc::new(xref), limit) } + #[test] + fn cancellation_interrupts_initial_page_tree_construction() { + use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + let armed = alloc::sync::Arc::new(AtomicBool::new(false)); + let polls = alloc::sync::Arc::new(AtomicUsize::new(0)); + let flag = armed.clone(); + let counter = polls.clone(); + let cancellation = crate::Cancellation::new(move || { + flag.load(Ordering::Relaxed) && counter.fetch_add(1, Ordering::Relaxed) + 1 == 100 + }); + let control = crate::cancel::Control::new(&cancellation); + let kids = (3..1003).map(|i| format!("{i} 0 R ")).collect::(); + let mut objects = vec![ + String::from("<< /Type /Catalog /Pages 2 0 R >>"), + format!("<< /Type /Pages /Kids [{kids}] /Count 1000 >>"), + ]; + objects.extend((0..1000).map(|_| String::from("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] >>"))); + let xref = crate::xref::root_xref_controlled(pdf_bytes(&objects).into(), b"", DecodeLimits::default(), Some(control)).unwrap(); + armed.store(true, Ordering::Relaxed); + assert!(cached::CachedPages::new(cached::Arc::new(xref)).is_none()); + assert_eq!(polls.load(Ordering::Relaxed), 100); + assert_eq!(cached_with_limit(&objects, 100_000).unwrap().get().len(), 1000); + } + fn assert_deep_tree() { let depth = 1024; let font_id = depth + 3; diff --git a/vendor/hayro-syntax/src/pdf.rs b/vendor/hayro-syntax/src/pdf.rs index 46600e52..a13e3f66 100644 --- a/vendor/hayro-syntax/src/pdf.rs +++ b/vendor/hayro-syntax/src/pdf.rs @@ -6,7 +6,7 @@ use crate::page::Pages; use crate::page::cached::CachedPages; use crate::reader::Reader; use crate::sync::Arc; -use crate::xref::{XRef, XRefError, fallback, root_xref}; +use crate::xref::{XRef, XRefError}; pub use crate::crypto::DecryptionError; use crate::metadata::Metadata; @@ -65,6 +65,15 @@ pub enum LoadPdfError { Invalid, } +/// Failure of a cancellable open. Existing loading errors retain their original type. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CancellableLoadError { + /// The caller requested cancellation while opening the document. + Cancelled, + /// The document could not be loaded. + Load(LoadPdfError), +} + #[allow(clippy::len_without_is_empty)] impl Pdf { /// Try to read the given PDF file. @@ -89,18 +98,47 @@ impl Pdf { /// Try to read a PDF with a password and document-local stream expansion limits. /// The same immutable limits apply during initial xref parsing, repair and later reads. pub fn new_with_password_and_limits(data: impl Into, password: &str, limits: DecodeLimits) -> Result { - let data = data.into(); + Self::load(data.into(), password, limits, None) + } + + /// Open with cooperative cancellation, retaining the existing password and decode policies. + /// The callback is owned by `cancel`, runs on this thread, and is not retained by a loaded + /// PDF. A true result stays latched for this open and never starts a repair pass. + pub fn new_with_password_limits_and_cancel( + data: impl Into, + password: &str, + limits: DecodeLimits, + cancel: &crate::Cancellation, + ) -> Result { + let control = crate::cancel::Control::new(cancel); + let _scope = crate::cancel::Scope(control.clone()); + if control.check().is_none() { + return Err(CancellableLoadError::Cancelled); + } + let result = Self::load(data.into(), password, limits, Some(control.clone())); + if control.check().is_none() { + Err(CancellableLoadError::Cancelled) + } else { + result.map_err(CancellableLoadError::Load) + } + } + + fn load(data: PdfData, password: &str, limits: DecodeLimits, control: Option) -> Result { let password = password.as_bytes(); let version = find_version(data.as_ref()).unwrap_or(PdfVersion::Pdf10); - let xref = match root_xref(data.clone(), password, limits) { + let xref = match crate::xref::root_xref_controlled(data.clone(), password, limits, control.clone()) { Ok(x) => x, Err(e) => match e { - XRefError::Unknown => fallback(data.clone(), password, limits).ok_or(LoadPdfError::Invalid)?, + XRefError::Unknown => { + crate::cancel::check(control.as_ref()).ok_or(LoadPdfError::Invalid)?; + crate::xref::fallback_controlled(data.clone(), password, limits, control.clone()).ok_or(LoadPdfError::Invalid)? + } XRefError::Encryption(e) => return Err(LoadPdfError::Decryption(e)), }, }; let xref = Arc::new(xref); + crate::cancel::check(control.as_ref()).ok_or(LoadPdfError::Invalid)?; let pages = CachedPages::new(xref.clone()).ok_or(LoadPdfError::Invalid)?; Ok(Self { xref, header_version: version, pages, data }) diff --git a/vendor/hayro-syntax/src/reader.rs b/vendor/hayro-syntax/src/reader.rs index 1b9babfc..65b64765 100644 --- a/vendor/hayro-syntax/src/reader.rs +++ b/vendor/hayro-syntax/src/reader.rs @@ -31,6 +31,8 @@ impl<'a> ReaderExt<'a> for Reader<'a> { // that it's not an object reference. #[inline] fn read>(&mut self, ctx: &ReaderContext<'a>) -> Option { + crate::cancel::check(ctx.control())?; + self.set_control(ctx.control().cloned()); let old_offset = self.offset; T::read(self, ctx).or_else(|| { @@ -126,6 +128,14 @@ struct ReaderContextData<'a> { enum ReaderContextInner<'a> { Shared(Arc>), Dummy { in_content_stream: bool, limits: crate::DecodeLimits }, + ControlledDummy(Arc), +} + +#[derive(Clone, Debug)] +struct ControlledDummy { + in_content_stream: bool, + limits: crate::DecodeLimits, + control: crate::cancel::Control, } /// Context for reading PDF objects. @@ -151,10 +161,26 @@ impl<'a> ReaderContext<'a> { Self(ReaderContextInner::Dummy { in_content_stream: false, limits }) } + pub(crate) fn dummy_with_control(limits: crate::DecodeLimits, control: Option) -> Self { + match control { + Some(control) => Self(ReaderContextInner::ControlledDummy(Arc::new(ControlledDummy { in_content_stream: false, limits, control }))), + None => Self::dummy_with_limits(limits), + } + } + + pub(crate) fn control(&self) -> Option<&crate::cancel::Control> { + match &self.0 { + ReaderContextInner::Shared(inner) => inner.xref.control(), + ReaderContextInner::Dummy { .. } => None, + ReaderContextInner::ControlledDummy(inner) => Some(&inner.control).filter(|c| c.active()), + } + } + pub(crate) fn decode_limits(&self) -> crate::DecodeLimits { match &self.0 { ReaderContextInner::Shared(inner) => inner.xref.decode_limits(), ReaderContextInner::Dummy { limits, .. } => *limits, + ReaderContextInner::ControlledDummy(inner) => inner.limits, } } @@ -166,7 +192,7 @@ impl<'a> ReaderContext<'a> { pub(crate) fn xref(&self) -> &'a XRef { match &self.0 { ReaderContextInner::Shared(inner) => inner.xref, - ReaderContextInner::Dummy { .. } => XRef::dummy(), + ReaderContextInner::Dummy { .. } | ReaderContextInner::ControlledDummy(_) => XRef::dummy(), } } @@ -175,6 +201,7 @@ impl<'a> ReaderContext<'a> { match &self.0 { ReaderContextInner::Shared(inner) => inner.in_content_stream, ReaderContextInner::Dummy { in_content_stream, .. } => *in_content_stream, + ReaderContextInner::ControlledDummy(inner) => inner.in_content_stream, } } @@ -182,7 +209,7 @@ impl<'a> ReaderContext<'a> { pub(crate) fn in_object_stream(&self) -> bool { match &self.0 { ReaderContextInner::Shared(inner) => inner.in_object_stream, - ReaderContextInner::Dummy { .. } => false, + ReaderContextInner::Dummy { .. } | ReaderContextInner::ControlledDummy(_) => false, } } @@ -190,7 +217,7 @@ impl<'a> ReaderContext<'a> { pub(crate) fn obj_number(&self) -> Option { match &self.0 { ReaderContextInner::Shared(inner) => inner.obj_number, - ReaderContextInner::Dummy { .. } => None, + ReaderContextInner::Dummy { .. } | ReaderContextInner::ControlledDummy(_) => None, } } @@ -198,7 +225,7 @@ impl<'a> ReaderContext<'a> { pub(crate) fn set_obj_number(&mut self, id: ObjectIdentifier) { match &mut self.0 { ReaderContextInner::Shared(inner) => Arc::make_mut(inner).obj_number = Some(id), - ReaderContextInner::Dummy { .. } => {} + ReaderContextInner::Dummy { .. } | ReaderContextInner::ControlledDummy(_) => {} } } @@ -207,6 +234,7 @@ impl<'a> ReaderContext<'a> { match &mut self.0 { ReaderContextInner::Shared(inner) => Arc::make_mut(inner).in_content_stream = val, ReaderContextInner::Dummy { in_content_stream, .. } => *in_content_stream = val, + ReaderContextInner::ControlledDummy(inner) => Arc::make_mut(inner).in_content_stream = val, } } @@ -214,7 +242,7 @@ impl<'a> ReaderContext<'a> { pub(crate) fn set_in_object_stream(&mut self, val: bool) { match &mut self.0 { ReaderContextInner::Shared(inner) => Arc::make_mut(inner).in_object_stream = val, - ReaderContextInner::Dummy { .. } => {} + ReaderContextInner::Dummy { .. } | ReaderContextInner::ControlledDummy(_) => {} } } @@ -222,7 +250,7 @@ impl<'a> ReaderContext<'a> { pub(crate) fn parent_chain_contains(&self, id: &ObjectIdentifier) -> bool { match &self.0 { ReaderContextInner::Shared(inner) => inner.parent_chain.contains(id), - ReaderContextInner::Dummy { .. } => false, + ReaderContextInner::Dummy { .. } | ReaderContextInner::ControlledDummy(_) => false, } } @@ -230,7 +258,7 @@ impl<'a> ReaderContext<'a> { pub(crate) fn parent_chain_push(&mut self, id: ObjectIdentifier) { match &mut self.0 { ReaderContextInner::Shared(inner) => Arc::make_mut(inner).parent_chain.push(id), - ReaderContextInner::Dummy { .. } => {} + ReaderContextInner::Dummy { .. } | ReaderContextInner::ControlledDummy(_) => {} } } } diff --git a/vendor/hayro-syntax/src/xref.rs b/vendor/hayro-syntax/src/xref.rs index 6476df79..03573a39 100644 --- a/vendor/hayro-syntax/src/xref.rs +++ b/vendor/hayro-syntax/src/xref.rs @@ -38,23 +38,29 @@ pub(crate) enum XRefError { } /// Parse the "root" xref from the PDF. +#[cfg(test)] pub(crate) fn root_xref(data: PdfData, password: &[u8], limits: DecodeLimits) -> Result { - let mut xref_map = FxHashMap::default(); - let xref_pos = find_last_xref_pos(data.as_ref()).ok_or(XRefError::Unknown)?; - let trailer = populate_xref_impl(data.as_ref(), xref_pos, &mut xref_map, limits).ok_or(XRefError::Unknown)?; + root_xref_controlled(data, password, limits, None) +} - XRef::new(data.clone(), xref_map, XRefInput::TrailerDictData(trailer), false, password, limits) +pub(crate) fn root_xref_controlled(data: PdfData, password: &[u8], limits: DecodeLimits, control: Option) -> Result { + let mut xref_map = FxHashMap::default(); + let xref_pos = find_last_xref_pos_controlled(data.as_ref(), control.as_ref()).ok_or(XRefError::Unknown)?; + let trailer = populate_xref_controlled(data.as_ref(), xref_pos, &mut xref_map, limits, control.clone()).ok_or(XRefError::Unknown)?; + + XRef::new(data.clone(), xref_map, XRefInput::TrailerDictData(trailer), false, password, limits, control) } /// Try to manually parse the PDF to build an xref table and trailer dictionary. -pub(crate) fn fallback(data: PdfData, password: &[u8], limits: DecodeLimits) -> Option { +pub(crate) fn fallback_controlled(data: PdfData, password: &[u8], limits: DecodeLimits, control: Option) -> Option { + crate::cancel::check(control.as_ref())?; warn!("xref table was invalid, trying to manually build xref table"); - let (xref_map, xref_input) = fallback_xref_map(&data, password, limits); + let (xref_map, xref_input) = fallback_xref_map(&data, password, limits, control.clone()); if let Some(xref_input) = xref_input { warn!("rebuild xref table with {} entries", xref_map.len()); - XRef::new(data.clone(), xref_map, xref_input, true, password, limits).ok() + XRef::new(data.clone(), xref_map, xref_input, true, password, limits, control).ok() } else { warn!("couldn't find trailer dictionary, failed to rebuild xref table"); @@ -62,8 +68,8 @@ pub(crate) fn fallback(data: PdfData, password: &[u8], limits: DecodeLimits) -> } } -fn fallback_xref_map<'a>(data: &'a PdfData, password: &[u8], limits: DecodeLimits) -> (XrefMap, Option>) { - fallback_xref_map_inner(data, ReaderContext::dummy_with_limits(limits), true, password) +fn fallback_xref_map<'a>(data: &'a PdfData, password: &[u8], limits: DecodeLimits, control: Option) -> (XrefMap, Option>) { + fallback_xref_map_inner(data, ReaderContext::dummy_with_control(limits, control), true, password) } fn fallback_xref_map_inner<'a>( @@ -77,10 +83,14 @@ fn fallback_xref_map_inner<'a>( let mut root_ref = None; let mut r = Reader::new(data.as_ref()); + r.set_control(dummy_ctx.control().cloned()); let mut last_obj_num = None; loop { + if crate::cancel::check(dummy_ctx.control()).is_none() { + return (XrefMap::default(), None); + } let cur_pos = r.offset(); let mut old_r = r.clone(); @@ -240,6 +250,7 @@ fn fallback_xref_map_inner<'a>( true, password, dummy_ctx.decode_limits(), + dummy_ctx.control().cloned(), ) { let ctx = ReaderContext::new(&xref, false); let (patched_map, _) = fallback_xref_map_inner(data, ctx, false, password); @@ -281,7 +292,8 @@ impl XRef { Some(f(&mut cache)) } - fn new(data: PdfData, xref_map: XrefMap, input: XRefInput<'_>, repaired: bool, password: &[u8], limits: DecodeLimits) -> Result { + fn new(data: PdfData, xref_map: XrefMap, input: XRefInput<'_>, repaired: bool, password: &[u8], limits: DecodeLimits, control: Option) -> Result { + crate::cancel::check(control.as_ref()).ok_or(XRefError::Unknown)?; // This is a bit hacky, but the problem is we can't read the resolved trailer dictionary // before we actually created the xref struct. So we first create it using dummy data // and then populate the data. @@ -296,6 +308,7 @@ impl XRef { trailer_data, password: password.to_vec(), limits, + control, dict_cache: Arc::new(Mutex::new(DictCache::default())), }))); @@ -395,6 +408,13 @@ impl XRef { } } + pub(crate) fn control(&self) -> Option<&crate::cancel::Control> { + match &self.0 { + Inner::Dummy => None, + Inner::Some(repr) => repr.control.as_ref().filter(|c| c.active()), + } + } + pub(crate) fn len(&self) -> usize { match &self.0 { Inner::Dummy => 0, @@ -434,10 +454,12 @@ impl XRef { Inner::Dummy => unimplemented!(), Inner::Some(r) => { let locked = r.map.get(); + let control = self.control(); let mut elements = locked .xref_map .iter() - .map(|(id, e)| { + .map_while(|(id, e)| { + crate::cancel::check(control)?; let offset = match e { EntryType::Normal(o) => (*o, 0), EntryType::ObjStream(id, index) => { @@ -451,18 +473,30 @@ impl XRef { } }; - (*id, offset) + Some((*id, offset)) }) .collect::>(); // Try to yield in the order the objects appeared in the // PDF. - elements.sort_by(|e1, e2| e1.1.cmp(&e2.1)); + if let Some(control) = control { + let mut sorted = alloc::collections::BTreeMap::new(); + for (order, (id, offset)) in elements.drain(..).enumerate() { + if control.check().is_none() { + break; + } + sorted.insert((offset, order), (id, offset)); + } + elements.extend(sorted.into_values().map_while(|entry| control.check().map(|_| entry))); + } else { + elements.sort_by(|e1, e2| e1.1.cmp(&e2.1)); + } let mut iter = elements.into_iter(); iter::from_fn(move || { for next in iter.by_ref() { + crate::cancel::check(control)?; if let Some(obj) = self.get_with(next.0, &ReaderContext::new(self, false)) { return Some(obj); } else { @@ -492,7 +526,10 @@ impl XRef { // Set first so an unwinding fallback cannot trigger a reparse storm. // Assignment below publishes a complete replacement atomically. locked.repaired = true; - let (xref_map, _) = fallback_xref_map(r.data.get(), &r.password, r.limits); + if crate::cancel::check(self.control()).is_none() { + return; + } + let (xref_map, _) = fallback_xref_map(r.data.get(), &r.password, r.limits, self.control().cloned()); locked.xref_map = xref_map; } @@ -543,6 +580,7 @@ impl XRef { where T: ObjectLike<'a>, { + crate::cancel::check(ctx.control())?; let Inner::Some(repr) = &self.0 else { return None; }; @@ -646,10 +684,30 @@ pub(crate) enum XRefInput<'a> { RootRef(ObjectIdentifier), } +#[cfg(test)] pub(crate) fn find_last_xref_pos(data: &[u8]) -> Option { + find_last_xref_pos_controlled(data, None) +} + +fn find_last_xref_pos_controlled(data: &[u8], control: Option<&crate::cancel::Control>) -> Option { let needle = b"startxref"; - let pos = findr_needle(data, needle)?; + let pos = if control.is_some() { + let mut found = None; + for i in (0..data.len().div_ceil(64 * 1024)).rev() { + crate::cancel::check(control)?; + let start = i * 64 * 1024; + let end = (start + 64 * 1024 + needle.len() - 1).min(data.len()); + if let Some(at) = findr_needle(data.get(start..end)?, needle) { + found = Some(start + at); + break; + } + } + found? + } else { + findr_needle(data, needle)? + }; let mut finder = Reader::new(data); + finder.set_control(control.cloned()); finder.jump(pos); finder.forward_tag(needle)?; finder.skip_white_spaces_and_comments(); @@ -696,6 +754,17 @@ fn merge_entries(map: &mut XrefMap, own: XrefMap) -> Option<()> { Some(()) } +fn merge_entries_controlled(map: &mut XrefMap, own: XrefMap, control: Option<&crate::cancel::Control>) -> Option<()> { + if control.is_none() { + return merge_entries(map, own); + } + for (id, entry) in own { + crate::cancel::check(control)?; + insert_entry(map, id, entry)?; + } + Some(()) +} + /// Representation of a proper xref table. #[derive(Debug)] struct MapRepr { @@ -730,6 +799,7 @@ struct SomeRepr { has_ocgs: bool, password: Vec, limits: DecodeLimits, + control: Option, trailer_data: TrailerData, } @@ -779,9 +849,14 @@ impl XRefEntry { } } +#[cfg(test)] fn populate_xref_impl<'a>(data: &'a [u8], pos: usize, xref_map: &mut XrefMap, limits: DecodeLimits) -> Option<&'a [u8]> { + populate_xref_controlled(data, pos, xref_map, limits, None) +} + +fn populate_xref_controlled<'a>(data: &'a [u8], pos: usize, xref_map: &mut XrefMap, limits: DecodeLimits, control: Option) -> Option<&'a [u8]> { let mut visited = BTreeSet::new(); - populate_xref_impl_inner(data, pos, xref_map, &mut visited, limits) + populate_xref_impl_inner(data, pos, xref_map, &mut visited, limits, control) } /// The offset of the previous xref section (`Prev`). @@ -801,7 +876,7 @@ fn prev_offset(data: &[u8], trailer: &Dict<'_>, own: &XrefMap) -> Option let mut reader = Reader::new(data); reader.jump(*at); reader - .read_with_context::>(&ReaderContext::dummy()) + .read_with_context::>(&ReaderContext::dummy_with_control(trailer.ctx().decode_limits(), trailer.ctx().control().cloned())) .filter(|o| o.id() == &id) .map(|o| o.get()) .filter(|p| *p >= 0) @@ -821,7 +896,9 @@ fn populate_xref_impl_inner<'a>( xref_map: &mut XrefMap, visited: &mut BTreeSet, limits: DecodeLimits, + control: Option, ) -> Option<&'a [u8]> { + crate::cancel::check(control.as_ref())?; if !visited.insert(pos) { warn!("circular xref PREV chain detected at offset {}", pos); @@ -838,6 +915,7 @@ fn populate_xref_impl_inner<'a>( } let mut reader = Reader::new(data); + reader.set_control(control); reader.jump(pos); // In case the position points to before the object number of a xref stream. reader.skip_white_spaces_and_comments(); @@ -877,7 +955,8 @@ fn populate_from_xref_table<'a>( ) -> Option<&'a [u8]> { let trailer = { let mut reader = reader.clone(); - read_xref_table_trailer(&mut reader, &ReaderContext::dummy_with_limits(limits))? + let ctx = ReaderContext::dummy_with_control(limits, reader.control().cloned()); + read_xref_table_trailer(&mut reader, &ctx)? }; reader.skip_white_spaces(); @@ -913,16 +992,16 @@ fn populate_from_xref_table<'a>( if let Some(prev) = prev_offset(data, &trailer, &own) { // First insert the entries from any previous xref tables. - populate_xref_impl_inner(data, prev, insert_map, visited, limits)?; + populate_xref_impl_inner(data, prev, insert_map, visited, limits, reader.control().cloned())?; } // In hybrid files, entries in `XRefStm` should have higher priority, therefore we insert them // after looking at `PREV`. if let Some(xref_stm) = trailer.get::(XREF_STM) { - populate_xref_impl_inner(data, xref_stm as usize, insert_map, visited, limits)?; + populate_xref_impl_inner(data, xref_stm as usize, insert_map, visited, limits, reader.control().cloned())?; } - merge_entries(insert_map, own)?; + merge_entries_controlled(insert_map, own, reader.control())?; Some(trailer.data()) } @@ -934,7 +1013,8 @@ fn populate_from_xref_stream<'a>( visited: &mut BTreeSet, limits: DecodeLimits, ) -> Option<&'a [u8]> { - let stream = reader.read_with_context::>>(&ReaderContext::dummy_with_limits(limits))?.get(); + let ctx = ReaderContext::dummy_with_control(limits, reader.control().cloned()); + let stream = reader.read_with_context::>>(&ctx)?.get(); let size = stream.dict().get::(SIZE)?; @@ -951,6 +1031,7 @@ fn populate_from_xref_stream<'a>( // PdfCraft patch: (PdfCraft #2400) this section's entries are read first, for `prev_offset`. let mut own = XrefMap::default(); let mut xref_reader = Reader::new(xref_data.as_ref()); + xref_reader.set_control(reader.control().cloned()); if let Some(arr) = stream.dict().get::>(INDEX) { let iter = arr.iter::<(u32, u32)>(); @@ -972,10 +1053,10 @@ fn populate_from_xref_stream<'a>( if let Some(prev) = prev_offset(data, stream.dict(), &own) { // First insert the entries from any previous xref tables. - let _ = populate_xref_impl_inner(data, prev, insert_map, visited, limits)?; + let _ = populate_xref_impl_inner(data, prev, insert_map, visited, limits, reader.control().cloned())?; } - merge_entries(insert_map, own)?; + merge_entries_controlled(insert_map, own, reader.control())?; Some(stream.dict().data()) } @@ -1160,10 +1241,12 @@ impl<'a> ObjectStream<'a> { let first_offset = inner.dict().get::(FIRST)?; let mut r = Reader::new(data); + r.set_control(inner.dict().ctx().control().cloned()); let mut offsets = vec![]; for _ in 0..num_objects { + crate::cancel::check(inner.dict().ctx().control())?; r.skip_white_spaces_and_comments(); // Skip object number let obj_num = r.read_without_context::()?; @@ -1181,6 +1264,7 @@ impl<'a> ObjectStream<'a> { { let offset = self.offsets.get(index as usize)?.1; let mut r = Reader::new(self.data); + r.set_control(self.ctx.control().cloned()); r.jump(offset); r.skip_white_spaces_and_comments(); @@ -1221,6 +1305,28 @@ fn parse_metadata(info_dict: &Dict<'_>) -> Metadata { mod tests { use super::*; + #[test] + fn cancellation_stops_before_a_whole_xref_subsection_is_read() { + let data = vec![1; 512 * 1024]; + let (control, polls) = crate::cancel::stop_at(3); + let mut reader = Reader::new(&data); + reader.set_control(Some(control)); + let mut map = XrefMap::default(); + assert!(xref_stream_subsection(&mut reader, 0, data.len() as u32, 1, 0, 0, &mut map).is_none()); + assert!(reader.offset() > 0 && reader.offset() < data.len()); + assert!(!map.is_empty() && map.len() < data.len()); + assert_eq!(polls.load(core::sync::atomic::Ordering::Relaxed), 3); + } + + #[test] + fn cancellation_interrupts_the_recovery_scanner() { + let data: PdfData = vec![b'a'; 2 << 20].into(); + let (control, polls) = crate::cancel::stop_at(10); + assert!(fallback_controlled(data, b"", DecodeLimits::default(), Some(control.clone())).is_none()); + assert!(control.check().is_none()); + assert_eq!(polls.load(core::sync::atomic::Ordering::Relaxed), 10); + } + #[test] fn xref_stream_entries_need_backing_bytes() { let mut map = XrefMap::default();