use std::fmt;
use std::time::Duration;
use crate::PageIndex;
use crate::deadline::{Deadline, Operation};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Limits {
pub max_object_nesting: u32,
pub max_array_len: usize,
pub max_xref_size: u32,
pub max_object_number: u32,
pub header_scan: u64,
pub startxref_scan: u64,
pub max_word_len: usize,
pub max_page_tree_depth: u32,
pub max_page_count: u32,
pub max_decoded_stream_len: usize,
pub max_cmap_ranges: usize,
pub max_name_tree_depth: u32,
pub max_script_loop_iterations: u64,
pub max_script_recursion: usize,
pub max_script_stack: usize,
pub max_calculate_depth: u32,
pub max_render_pixels: Option<u64>,
pub deadline: Option<Deadline>,
}
impl Limits {
pub fn check_deadline(&self, during: Operation) -> Result<(), LimitExceeded> {
match &self.deadline {
Some(deadline) => deadline.check(during),
None => Ok(()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum LimitExceeded {
RenderPixels {
width: u32,
height: u32,
allowed: u64,
},
Time {
budget: Duration,
during: Operation,
page: Option<PageIndex>,
},
Stopped {
during: Operation,
page: Option<PageIndex>,
},
}
impl LimitExceeded {
#[must_use]
pub fn on_page(self, page: PageIndex) -> LimitExceeded {
match self {
LimitExceeded::Time { budget, during, .. } => LimitExceeded::Time {
budget,
during,
page: Some(page),
},
LimitExceeded::Stopped { during, .. } => LimitExceeded::Stopped {
during,
page: Some(page),
},
other => other,
}
}
}
fn budget_text(budget: Duration) -> String {
if budget.subsec_nanos() == 0 {
format!("{} s", budget.as_secs())
} else {
format!("{} ms", budget.as_millis())
}
}
fn megapixels(px: u64) -> String {
let whole = px / 1_000_000;
let tenths = (px % 1_000_000) / 100_000;
if tenths == 0 {
whole.to_string()
} else {
format!("{whole}.{tenths}")
}
}
impl fmt::Display for LimitExceeded {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LimitExceeded::RenderPixels {
width,
height,
allowed,
} => write!(
f,
"render of {width} x {height} px ({} megapixels) is above the cap of {} \
megapixels; render at a smaller scale or raise `Limits::max_render_pixels`",
megapixels(u64::from(*width) * u64::from(*height)),
megapixels(*allowed),
),
LimitExceeded::Time {
budget,
during,
page,
} => write!(
f,
"time limit of {} exceeded while {}; allow more time or do less",
budget_text(*budget),
during.describe(*page),
),
LimitExceeded::Stopped { during, page } => {
write!(f, "stopped by the caller while {}", during.describe(*page))
}
}
}
}
impl std::error::Error for LimitExceeded {}
impl Default for Limits {
fn default() -> Self {
Self {
max_object_nesting: 64,
max_array_len: usize::MAX,
max_xref_size: 25_165_825,
max_object_number: 25_165_824,
header_scan: 1024,
startxref_scan: 4096,
max_word_len: 256,
max_page_tree_depth: 1024,
max_page_count: 0x000F_FFFF,
max_decoded_stream_len: 1024 * 1024 * 1024,
max_cmap_ranges: 65_536,
max_name_tree_depth: 32,
max_script_loop_iterations: 10_000_000,
max_script_recursion: 512,
max_script_stack: 10_240,
max_calculate_depth: 1,
max_render_pixels: None,
deadline: None,
}
}
}
#[cfg(test)]
mod tests {
use super::{Deadline, LimitExceeded, Limits, Operation, PageIndex};
use std::time::Duration;
#[test]
fn pdfium_equivalent_defaults() {
let l = Limits::default();
assert_eq!(l.max_object_nesting, 64);
assert_eq!(l.max_xref_size, 25_165_825);
assert_eq!(l.max_object_number, 25_165_824);
assert_eq!(l.max_xref_size, l.max_object_number + 1);
assert_eq!(l.header_scan, 1024);
assert_eq!(l.startxref_scan, 4096);
assert_eq!(l.max_word_len, 256);
assert_eq!(l.max_page_tree_depth, 1024);
assert_eq!(l.max_page_count, 1_048_575);
assert_eq!(l.max_decoded_stream_len, 1024 * 1024 * 1024);
assert_eq!(l.max_cmap_ranges, 65_536);
assert_eq!(l.max_name_tree_depth, 32);
assert_eq!(l.max_array_len, usize::MAX);
assert_eq!(l.max_render_pixels, None, "the host's ceilings are off");
assert_eq!(l.deadline, None);
}
#[test]
fn no_deadline_is_never_exceeded_and_a_spent_one_always_is() {
assert_eq!(Limits::default().check_deadline(Operation::Open), Ok(()));
let spent = Limits {
deadline: Some(Deadline::after(Duration::ZERO)),
..Limits::default()
};
assert!(matches!(
spent.check_deadline(Operation::Extract),
Err(LimitExceeded::Time {
during: Operation::Extract,
page: None,
..
})
));
}
#[test]
fn the_time_message_names_the_budget_the_work_and_the_page() {
let e = LimitExceeded::Time {
budget: Duration::from_secs(5),
during: Operation::Render,
page: Some(PageIndex::new(3)),
};
assert_eq!(
e.to_string(),
"time limit of 5 s exceeded while rendering page 3; allow more time or do less"
);
let e = LimitExceeded::Time {
budget: Duration::from_millis(250),
during: Operation::Open,
page: None,
};
assert_eq!(
e.to_string(),
"time limit of 250 ms exceeded while opening the document; allow more time or do less"
);
let e = LimitExceeded::Time {
budget: Duration::from_secs(1),
during: Operation::Interpret,
page: None,
};
assert!(e.to_string().contains("while interpreting page;"));
let e = LimitExceeded::Stopped {
during: Operation::PageLoad,
page: Some(PageIndex::new(7)),
};
assert_eq!(e.to_string(), "stopped by the caller while loading page 7");
}
#[test]
fn a_raised_flag_is_a_stop_and_the_page_is_added_by_the_caller() {
let stop = Deadline::manual();
let limits = Limits {
deadline: Some(stop.clone()),
..Limits::default()
};
assert_eq!(limits.check_deadline(Operation::Render), Ok(()));
stop.stop();
assert_eq!(
limits
.check_deadline(Operation::Render)
.map_err(|e| e.on_page(PageIndex::new(1))),
Err(LimitExceeded::Stopped {
during: Operation::Render,
page: Some(PageIndex::new(1)),
})
);
}
#[test]
fn the_pixel_message_names_the_size_the_cap_and_the_remedy() {
let e = LimitExceeded::RenderPixels {
width: 20_000,
height: 20_000,
allowed: 100_000_000,
};
assert_eq!(
e.to_string(),
"render of 20000 x 20000 px (400 megapixels) is above the cap of 100 megapixels; \
render at a smaller scale or raise `Limits::max_render_pixels`"
);
let e = LimitExceeded::RenderPixels {
width: 1_234,
height: 1_000,
allowed: 1_050_000,
};
assert!(e.to_string().contains("(1.2 megapixels)"));
assert!(e.to_string().contains("cap of 1 megapixels"));
}
#[test]
fn struct_update_syntax_keeps_the_rest() {
let l = Limits {
max_array_len: 8,
..Limits::default()
};
assert_eq!(l.max_array_len, 8);
assert_eq!(l.max_object_nesting, 64);
}
}