use std::path::Path;
use rayon::prelude::*;
use crate::{PdfOpsError, Result};
const BYTES_PER_NESTING_LEVEL: usize = if cfg!(debug_assertions) { 2560 } else { 512 };
#[cfg(target_pointer_width = "64")]
const MAX_NESTING_DEPTH: usize = 262_144;
#[cfg(not(target_pointer_width = "64"))]
const MAX_NESTING_DEPTH: usize = 4_096;
pub(crate) const HAYRO_STACK_SIZE: usize = 2 * MAX_NESTING_DEPTH * BYTES_PER_NESTING_LEVEL;
pub(crate) fn with_deep_stack<T, F>(f: F) -> Result<T>
where
T: Send,
F: FnOnce() -> Result<T> + Send,
{
std::thread::scope(|scope| {
let handle = std::thread::Builder::new()
.name("pdq-hayro".to_string())
.stack_size(HAYRO_STACK_SIZE)
.spawn_scoped(scope, f)?;
handle
.join()
.unwrap_or_else(|payload| std::panic::resume_unwind(payload))
})
}
pub(crate) fn guard_nesting_depth(data: &[u8], input: &Path) -> Result<()> {
if count_open_digraphs(data) <= MAX_NESTING_DEPTH {
return Ok(());
}
let mut depth: usize = 0;
let mut consumed = 0usize;
for at in memchr::memchr2_iter(b'<', b'>', data) {
if at < consumed {
continue;
}
let byte = data[at];
if data.get(at + 1) != Some(&byte) {
continue;
}
consumed = at + 2;
if byte == b'<' {
depth += 1;
if depth > MAX_NESTING_DEPTH {
return Err(PdfOpsError::Unsupported(format!(
"object nesting depth exceeds {MAX_NESTING_DEPTH} in {}",
input.display()
)));
}
} else {
depth = depth.saturating_sub(1);
}
}
Ok(())
}
fn count_open_digraphs(data: &[u8]) -> usize {
const PARALLEL_MIN_LEN: usize = 4 << 20;
if data.len() < PARALLEL_MIN_LEN {
return count_open_digraphs_in(data, 0, data.len());
}
let chunk = data.len().div_ceil(rayon::current_num_threads().max(1));
(0..data.len().div_ceil(chunk))
.into_par_iter()
.map(|index| {
let start = index * chunk;
count_open_digraphs_in(data, start, (start + chunk).min(data.len()))
})
.sum()
}
fn count_open_digraphs_in(data: &[u8], start: usize, end: usize) -> usize {
memchr::memchr_iter(b'<', &data[start..end])
.filter(|offset| data.get(start + offset + 1) == Some(&b'<'))
.count()
}
#[cfg(test)]
mod tests {
use super::*;
fn depth_error(data: &[u8]) -> Option<String> {
guard_nesting_depth(data, Path::new("x.pdf"))
.err()
.map(|err| err.to_string())
}
#[test]
fn ordinary_nesting_is_accepted() {
let data = b"trailer\n<</Size 4/Root<</Type/Catalog/Pages<</Count 1>>>>>>\n";
assert!(depth_error(data).is_none());
}
#[test]
fn deep_nesting_is_rejected_before_parsing() {
let mut data = b"trailer\n".to_vec();
data.extend(std::iter::repeat_n(b'<', 2 * (MAX_NESTING_DEPTH + 1)));
data.extend_from_slice(b"/Size 4");
data.extend(std::iter::repeat_n(b'>', 2 * (MAX_NESTING_DEPTH + 1)));
let message = depth_error(&data).expect("deep nesting must be rejected");
assert!(message.contains("nesting depth"), "{message}");
}
#[test]
fn closed_nesting_is_accepted_and_a_later_deep_run_is_not() {
let mut data = Vec::new();
for _ in 0..4 {
data.extend(std::iter::repeat_n(b'<', 2 * (MAX_NESTING_DEPTH / 2)));
data.extend(std::iter::repeat_n(b'>', 2 * (MAX_NESTING_DEPTH / 2)));
}
assert!(depth_error(&data).is_none());
data.extend(std::iter::repeat_n(b'<', 2 * (MAX_NESTING_DEPTH + 1)));
assert!(depth_error(&data).is_some());
}
#[test]
fn unbalanced_close_tokens_clamp_at_zero() {
let mut data = std::iter::repeat_n(b'>', 4096).collect::<Vec<u8>>();
data.extend(std::iter::repeat_n(b'<', 2 * (MAX_NESTING_DEPTH + 1)));
assert!(depth_error(&data).is_some());
}
#[test]
fn chunked_and_sequential_digraph_counts_agree() {
let mut data = Vec::with_capacity(8 << 20);
let pattern: &[u8] = b"<< /A 1 <<>> <<< >>> x < > <<";
while data.len() < (8 << 20) {
data.extend_from_slice(pattern);
data.push(b'<');
}
let sequential = count_open_digraphs_in(&data, 0, data.len());
assert_eq!(count_open_digraphs(&data), sequential);
let window = &data[..1024];
for split in 1..window.len() {
let halves = count_open_digraphs_in(window, 0, split)
+ count_open_digraphs_in(window, split, window.len());
assert_eq!(
halves,
count_open_digraphs_in(window, 0, window.len()),
"split at {split}"
);
}
}
#[test]
fn digraph_count_bounds_the_exact_scan() {
for run in 1..64usize {
let data = vec![b'<'; run];
let digraphs = count_open_digraphs(&data);
assert!(
digraphs >= run / 2,
"run of {run}: {digraphs} digraphs under-counts {} tokens",
run / 2
);
}
}
#[test]
fn deep_stack_propagates_results_and_panics() {
assert_eq!(with_deep_stack(|| Ok(7)).unwrap(), 7);
let panicked =
std::panic::catch_unwind(|| with_deep_stack(|| -> Result<()> { panic!("boom") }));
assert!(panicked.is_err(), "a panic must not be laundered into Err");
}
}