PDF: allow opening to be canceled during parsing

This commit is contained in:
dotCooCoo committed 2026-11-03 06:33:31 -08:00
1 parent 55332dd301
commit f686f6c3f6
24 files changed
+982 -96

No files matched your search

+16 -3
View File
@@ -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.
+67
View File
@@ -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())
+109
View File
@@ -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 = &marker;
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))));
}
}
}
+1
View File
@@ -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[@]}"
+12
View File
@@ -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
+28 -16
View File
@@ -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
View File
@@ -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");
+176
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+75
View File
@@ -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
View File
@@ -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, &params)
}
#[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, &params)
}
@@ -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, &params).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
View File
@@ -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());
}
+7
View File
@@ -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;
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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() {
+32 -1
View File
@@ -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;
+42 -4
View File
@@ -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 })
+35 -7
View File
@@ -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(_) => {}
}
}
}
+130 -24
View File
@@ -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();