mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-11 04:52:13 +00:00
PDF: allow opening to be canceled during parsing
This commit is contained in:
1 parent
55332dd301
commit
f686f6c3f6
24 files changed
+982
-96
No files matched your search
+16
-3
@@ -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<AtomicBool>` 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.
|
||||
|
||||
@@ -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<Vec<u8>>,
|
||||
password: Option<&str>,
|
||||
options: &OpenOptions,
|
||||
cancel: &syntax::Cancellation,
|
||||
) -> Result<syntax::Pdf, CancellableOpenError> {
|
||||
guard_cancelled_open(|| read_with_cancel(bytes, password, options, cancel))
|
||||
}
|
||||
|
||||
fn guard_cancelled_open(read: impl FnOnce() -> Result<syntax::Pdf, CancellableOpenError>) -> Result<syntax::Pdf, CancellableOpenError> {
|
||||
catch_unwind(AssertUnwindSafe(read)).map_err(|_| CancellableOpenError::Open(OpenError::BackendPanic))?
|
||||
}
|
||||
|
||||
fn read_with_cancel(
|
||||
bytes: Arc<Vec<u8>>,
|
||||
password: Option<&str>,
|
||||
options: &OpenOptions,
|
||||
cancel: &syntax::Cancellation,
|
||||
) -> Result<syntax::Pdf, CancellableOpenError> {
|
||||
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<syntax::Pdf, CancellableOpenError> {
|
||||
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())
|
||||
|
||||
@@ -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<u8> {
|
||||
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::Dict<'_>>(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))));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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[@]}"
|
||||
Vendored
+12
@@ -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
|
||||
|
||||
Vendored
+28
-16
@@ -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": {
|
||||
|
||||
+43
-1
@@ -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<crate::cancel::Control>,
|
||||
next_check: Cell<usize>,
|
||||
}
|
||||
|
||||
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<crate::cancel::Control>) {
|
||||
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<u8> {
|
||||
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");
|
||||
|
||||
Vendored
+176
@@ -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<AtomicBool>`, 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<dyn Fn() -> 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<Cancellation>,
|
||||
stopped: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Control(Arc<Inner>);
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Inner {
|
||||
state: Mutex<State>,
|
||||
#[cfg(feature = "std")]
|
||||
active: core::sync::atomic::AtomicBool,
|
||||
#[cfg(not(feature = "std"))]
|
||||
active: core::cell::Cell<bool>,
|
||||
}
|
||||
|
||||
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<R: std::io::Read> std::io::Read for Input<'_, R> {
|
||||
fn read(&mut self, bytes: &mut [u8]) -> std::io::Result<usize> {
|
||||
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<core::sync::atomic::AtomicUsize>) {
|
||||
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)
|
||||
}
|
||||
+29
@@ -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::<Stream<'_>>(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);
|
||||
|
||||
+6
@@ -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<Vec<u8>> {
|
||||
decode_controlled(data, limit, None)
|
||||
}
|
||||
|
||||
pub(crate) fn decode_controlled(data: &[u8], limit: usize, control: Option<&crate::cancel::Control>) -> Option<Vec<u8>> {
|
||||
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<u8> {
|
||||
loop {
|
||||
@@ -69,6 +74,7 @@ pub(crate) fn decode(data: &[u8], limit: usize) -> Option<Vec<u8>> {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
+14
-3
@@ -9,11 +9,17 @@ pub(crate) fn decode(data: &[u8]) -> Option<Vec<u8>> {
|
||||
}
|
||||
|
||||
pub(crate) fn decode_into<B: ByteBuf>(data: &[u8]) -> Option<B> {
|
||||
decode_into_controlled(data, None)
|
||||
}
|
||||
|
||||
pub(crate) fn decode_into_controlled<B: ByteBuf>(data: &[u8], control: Option<&crate::cancel::Control>) -> Option<B> {
|
||||
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<B: ByteBuf>(data: &[u8]) -> Option<B> {
|
||||
// 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<B: ByteBuf>(data: &[u8]) -> Option<B> {
|
||||
}
|
||||
} 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<u8> {
|
||||
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<B: ByteBuf>(data: &[u8]) -> Option<B> {
|
||||
}
|
||||
}
|
||||
|
||||
crate::cancel::check(control)?;
|
||||
Some(decoded)
|
||||
}
|
||||
|
||||
|
||||
@@ -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::<crate::object::Stream<'_>>(&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::<Vec<u8>>(&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);
|
||||
}
|
||||
+109
-28
@@ -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<Vec<u8>> {
|
||||
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<Vec<u8>> {
|
||||
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<Vec<u8>> {
|
||||
fn read_up_to(mut decoder: impl Read, limit: usize, control: Option<&crate::cancel::Control>) -> Option<Vec<u8>> {
|
||||
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<Vec<u8>> {
|
||||
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<Vec<u8>> {
|
||||
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<Vec<u8>> {
|
||||
flate_decode(data, limit)
|
||||
pub(crate) fn decode(data: &[u8], limit: usize, control: Option<&crate::cancel::Control>) -> Option<Vec<u8>> {
|
||||
flate_decode(data, limit, control)
|
||||
}
|
||||
|
||||
fn flate_decode(data: &[u8], limit: usize) -> Option<Vec<u8>> {
|
||||
fn flate_decode(data: &[u8], limit: usize, control: Option<&crate::cancel::Control>) -> Option<Vec<u8>> {
|
||||
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<u8>, until: usize) {
|
||||
self.decode_into_controlled(data, out, until, None);
|
||||
}
|
||||
|
||||
pub(crate) fn decode_into_controlled(&mut self, data: &[u8], out: &mut Vec<u8>, 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<Vec<u8>> {
|
||||
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<Vec<u8>> {
|
||||
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<Vec<u8>> {
|
||||
fn decode_impl(data: &[u8], early_change: bool, limit: usize, control: Option<&crate::cancel::Control>) -> Option<Vec<u8>> {
|
||||
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<crate::cancel::Control>,
|
||||
}
|
||||
|
||||
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::<u8>(EARLY_CHANGE).map(|e| e != 0).unwrap_or(true),
|
||||
control: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_predictor(data: Vec<u8>, params: &PredictorParams) -> Option<Vec<u8>> {
|
||||
crate::cancel::check(params.control.as_ref())?;
|
||||
match params.predictor {
|
||||
1 => Some(data),
|
||||
i => {
|
||||
@@ -1048,8 +1105,10 @@ fn apply_predictor(data: Vec<u8>, params: &PredictorParams) -> Option<Vec<u8>> {
|
||||
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<u8>, params: &PredictorParams) -> Option<Vec<u8>> {
|
||||
zero_col.clone(),
|
||||
in_data_chunks,
|
||||
&mut writer,
|
||||
chunk_len,
|
||||
bit_size,
|
||||
run,
|
||||
)?,
|
||||
2 => apply::<Up>(
|
||||
prev_row,
|
||||
@@ -1071,8 +1129,7 @@ fn apply_predictor(data: Vec<u8>, params: &PredictorParams) -> Option<Vec<u8>> {
|
||||
zero_col.clone(),
|
||||
in_data_chunks,
|
||||
&mut writer,
|
||||
chunk_len,
|
||||
bit_size,
|
||||
run,
|
||||
)?,
|
||||
3 => apply::<Avg>(
|
||||
prev_row,
|
||||
@@ -1080,8 +1137,7 @@ fn apply_predictor(data: Vec<u8>, params: &PredictorParams) -> Option<Vec<u8>> {
|
||||
zero_col.clone(),
|
||||
in_data_chunks,
|
||||
&mut writer,
|
||||
chunk_len,
|
||||
bit_size,
|
||||
run,
|
||||
)?,
|
||||
4 => apply::<Paeth>(
|
||||
prev_row,
|
||||
@@ -1089,14 +1145,15 @@ fn apply_predictor(data: Vec<u8>, params: &PredictorParams) -> Option<Vec<u8>> {
|
||||
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<u8>, params: &PredictorParams) -> Option<Vec<u8>> {
|
||||
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<u8>, params: &PredictorParams) -> Option<Vec<u8>> {
|
||||
}
|
||||
}
|
||||
|
||||
#[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();
|
||||
|
||||
+11
-5
@@ -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<FilterResult<'static>, 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());
|
||||
}
|
||||
|
||||
@@ -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<Vec<u8>> {
|
||||
decode_controlled(data, limit, None)
|
||||
}
|
||||
|
||||
pub(crate) fn decode_controlled(data: &[u8], limit: usize, control: Option<&crate::cancel::Control>) -> Option<Vec<u8>> {
|
||||
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;
|
||||
|
||||
Vendored
+2
@@ -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;
|
||||
|
||||
+2
@@ -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()?;
|
||||
|
||||
+17
-1
@@ -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> {
|
||||
Self::new_escaped_controlled(data, None)
|
||||
}
|
||||
|
||||
fn new_escaped_controlled(data: &'a [u8], control: Option<&crate::cancel::Control>) -> Option<Self> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-1
@@ -214,6 +214,7 @@ impl<'a> Stream<'a> {
|
||||
image_params: &ImageDecodeParams,
|
||||
max: usize,
|
||||
) -> Result<FilterResult<'a>, 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<FilterResult<'a>, 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<FilterResult<'a>> = 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);
|
||||
|
||||
+12
-2
@@ -150,7 +150,7 @@ fn read_hex(r: &mut Reader<'_>) -> Option<SmallVec<[u8; 23]>> {
|
||||
|
||||
// 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<StringInner<'a>> {
|
||||
// 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() {
|
||||
|
||||
Vendored
+32
-1
@@ -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<Self> {
|
||||
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::<String>();
|
||||
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;
|
||||
|
||||
Vendored
+42
-4
@@ -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<PdfData>, password: &str, limits: DecodeLimits) -> Result<Self, LoadPdfError> {
|
||||
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<PdfData>,
|
||||
password: &str,
|
||||
limits: DecodeLimits,
|
||||
cancel: &crate::Cancellation,
|
||||
) -> Result<Self, CancellableLoadError> {
|
||||
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<crate::cancel::Control>) -> Result<Self, LoadPdfError> {
|
||||
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 })
|
||||
|
||||
Vendored
+35
-7
@@ -31,6 +31,8 @@ impl<'a> ReaderExt<'a> for Reader<'a> {
|
||||
// that it's not an object reference.
|
||||
#[inline]
|
||||
fn read<T: Readable<'a>>(&mut self, ctx: &ReaderContext<'a>) -> Option<T> {
|
||||
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<ReaderContextData<'a>>),
|
||||
Dummy { in_content_stream: bool, limits: crate::DecodeLimits },
|
||||
ControlledDummy(Arc<ControlledDummy>),
|
||||
}
|
||||
|
||||
#[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<crate::cancel::Control>) -> 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<ObjectIdentifier> {
|
||||
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(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+130
-24
@@ -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<XRef, XRefError> {
|
||||
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<crate::cancel::Control>) -> Result<XRef, XRefError> {
|
||||
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<XRef> {
|
||||
pub(crate) fn fallback_controlled(data: PdfData, password: &[u8], limits: DecodeLimits, control: Option<crate::cancel::Control>) -> Option<XRef> {
|
||||
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<XRefInput<'a>>) {
|
||||
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<crate::cancel::Control>) -> (XrefMap, Option<XRefInput<'a>>) {
|
||||
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<Self, XRefError> {
|
||||
fn new(data: PdfData, xref_map: XrefMap, input: XRefInput<'_>, repaired: bool, password: &[u8], limits: DecodeLimits, control: Option<crate::cancel::Control>) -> Result<Self, XRefError> {
|
||||
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::<Vec<_>>();
|
||||
|
||||
// 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<usize> {
|
||||
find_last_xref_pos_controlled(data, None)
|
||||
}
|
||||
|
||||
fn find_last_xref_pos_controlled(data: &[u8], control: Option<&crate::cancel::Control>) -> Option<usize> {
|
||||
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<u8>,
|
||||
limits: DecodeLimits,
|
||||
control: Option<crate::cancel::Control>,
|
||||
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<crate::cancel::Control>) -> 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<usize>
|
||||
let mut reader = Reader::new(data);
|
||||
reader.jump(*at);
|
||||
reader
|
||||
.read_with_context::<IndirectObject<i32>>(&ReaderContext::dummy())
|
||||
.read_with_context::<IndirectObject<i32>>(&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<usize>,
|
||||
limits: DecodeLimits,
|
||||
control: Option<crate::cancel::Control>,
|
||||
) -> 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::<i32>(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<usize>,
|
||||
limits: DecodeLimits,
|
||||
) -> Option<&'a [u8]> {
|
||||
let stream = reader.read_with_context::<IndirectObject<Stream<'_>>>(&ReaderContext::dummy_with_limits(limits))?.get();
|
||||
let ctx = ReaderContext::dummy_with_control(limits, reader.control().cloned());
|
||||
let stream = reader.read_with_context::<IndirectObject<Stream<'_>>>(&ctx)?.get();
|
||||
|
||||
let size = stream.dict().get::<u32>(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::<Array<'_>>(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::<usize>(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::<u32>()?;
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user