mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Audit: PDF import and export bounds
This commit is contained in:
committed by
bflatastic
parent
9c8afb09ba
commit
4b8e69981e
@@ -113,6 +113,13 @@ impl SecuritySettings {
|
||||
standard.label()
|
||||
)));
|
||||
}
|
||||
// Revision 2 has one printing bit: it can't say "low resolution only".
|
||||
if revision(c) == 2 && self.printing == Printing::Low && !self.permissions_password.is_empty() {
|
||||
return Err(PdfError::BadSetting(format!(
|
||||
"{} can't limit printing to low resolution: choose PDF 1.4 or later, or allow high-resolution printing",
|
||||
c.label()
|
||||
)));
|
||||
}
|
||||
if self.open_password == self.permissions_password {
|
||||
return Err(PdfError::BadSetting("the open and permissions passwords must differ".into()));
|
||||
}
|
||||
@@ -146,6 +153,9 @@ pub(crate) fn warnings(set: &PdfSettings) -> Vec<String> {
|
||||
if s.protected() && s.plaintext_metadata && revision(set.compatibility) <= 3 {
|
||||
out.push(format!("{} encryption covers the metadata too: plaintext metadata needs PDF 1.5 or later", set.compatibility.label()));
|
||||
}
|
||||
if s.protected() && set.compatibility == Compatibility::Pdf13 {
|
||||
out.push(format!("{} encrypts with a 40-bit key, which can be searched in minutes: choose PDF 1.6 or later for stronger protection", set.compatibility.label()));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
|
||||
@@ -47,11 +47,15 @@ pub fn export_with_report(doc: &Document, opts: &PdfOptions) -> Result<ExportRep
|
||||
/// holds. The writer reports such an image (a PNG, GIF or WebP kept as it is, whose header reads)
|
||||
/// only when it finishes the file. Each repeated run leaves out at least one more image.
|
||||
pub(crate) fn leaving_out_undecodable<T>(mut write: impl FnMut(&mut HashSet<String>) -> Result<T, PdfError>) -> Result<T, PdfError> {
|
||||
// The most runs repeated: each is a whole export, and a document of many images that don't
|
||||
// decode would make them quadratic. Past it the writer's error is the result.
|
||||
const MAX_RETRIES: usize = 16;
|
||||
let mut left_out = HashSet::new();
|
||||
let mut retries = 0;
|
||||
loop {
|
||||
let known = left_out.len();
|
||||
match write(&mut left_out) {
|
||||
Err(_) if left_out.len() > known => {}
|
||||
Err(_) if left_out.len() > known && retries < MAX_RETRIES => retries += 1,
|
||||
done => return done,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,6 +483,10 @@ fn read_with_reader_options(bytes: &[u8], opts: &ImportOptions, reader: &craft_p
|
||||
let list = b.missing_fonts.join(", ");
|
||||
b.warn(&format!("fonts that aren't available show in the fallback font until they are: {list}"));
|
||||
}
|
||||
// Before anything walks the tree by recursion (`fix_next_id` does): nested clips can be deeper
|
||||
// than the stack goes.
|
||||
let mut depth_notes = vec![];
|
||||
doc.limit_depth_noting(&mut depth_notes);
|
||||
doc.fix_next_id();
|
||||
doc.reserve_ids(b.next);
|
||||
|
||||
@@ -494,7 +498,7 @@ fn read_with_reader_options(bytes: &[u8], opts: &ImportOptions, reader: &craft_p
|
||||
}
|
||||
}
|
||||
}
|
||||
doc.limit_depth_noting(&mut warnings);
|
||||
warnings.extend(depth_notes);
|
||||
let user_unit = picked.first().and_then(|&n| pages.get(n)).map_or(1.0, |page| f64::from(page.user_unit()));
|
||||
Ok(ImportReport { document: doc, warnings, native: crate::editing::editing_in(&pdf), only_text, user_unit })
|
||||
}
|
||||
@@ -982,12 +986,16 @@ fn stroke_layer(paint: Paint, opacity: f32, p: &StrokeProps, scale: f64) -> Stro
|
||||
kurbo::Join::Bevel => LineJoin::Bevel,
|
||||
};
|
||||
st.miter_limit = p.miter_limit as f64;
|
||||
let dash = Dash { pattern: p.dash_array.iter().map(|v| *v as f64 * scale).collect(), offset: p.dash_offset as f64 * scale, align_corners: false };
|
||||
// (Every stroke keeps its own copy: a dash array has a few entries, and one with thousands is cut.)
|
||||
let dash = Dash { pattern: p.dash_array.iter().take(MAX_DASH_ENTRIES).map(|v| *v as f64 * scale).collect(), offset: p.dash_offset as f64 * scale, align_corners: false };
|
||||
// An invalid dash array (a negative value, or all zeros) strokes solid.
|
||||
st.dash = dash.is_dashed().then_some(dash);
|
||||
st
|
||||
}
|
||||
|
||||
/// The most entries of a dash array kept (an even count keeps it a pattern of dashes and gaps).
|
||||
const MAX_DASH_ENTRIES: usize = 64;
|
||||
|
||||
pub(crate) fn round3(v: f32) -> f32 {
|
||||
(v * 1000.0).round() / 1000.0
|
||||
}
|
||||
|
||||
@@ -362,7 +362,8 @@ impl<'p> Colors<'p> {
|
||||
let alt = alt_model(&it.next::<Object<'_>>()?);
|
||||
let tint = Function::new(&it.next::<Object<'_>>()?)?;
|
||||
let key = debug_text(&tint)?;
|
||||
if names.is_empty() || self.spaces.iter().any(|s| s.key == key && s.device_n == device_n && s.names == names) {
|
||||
// (PDF's limit is 32 colorants; each gets a vector of that length below.)
|
||||
if names.is_empty() || names.len() > MAX_COLORANTS || self.spaces.iter().any(|s| s.key == key && s.device_n == device_n && s.names == names) {
|
||||
return None;
|
||||
}
|
||||
let colorants = names
|
||||
@@ -521,3 +522,6 @@ fn alt_color(alt: Alt, v: &[f32]) -> Option<Color> {
|
||||
Alt::Lab => Color::lab(round3(c(0)?.clamp(0.0, 100.0)), round3(c(1)?.clamp(-128.0, 127.0)), round3(c(2)?.clamp(-128.0, 127.0))),
|
||||
})
|
||||
}
|
||||
|
||||
/// The most colorants of a DeviceN space read.
|
||||
const MAX_COLORANTS: usize = 64;
|
||||
|
||||
@@ -28,6 +28,9 @@ use crate::import_color::ink_names;
|
||||
|
||||
/// The interpreter's limit on nested form XObjects.
|
||||
const MAX_DEPTH: u32 = 50;
|
||||
|
||||
/// The most graphics states saved at once (PDF's own limit for nesting is 28; files nest far deeper).
|
||||
const MAX_STATES: usize = 65_536;
|
||||
/// The most content operators walked per page (a guard against forms drawn within each other
|
||||
/// many times over): the art past it can't be told apart by group. A drawing of a million paths
|
||||
/// takes a few million; the walk reads about 15 million a second, twice as fast as the
|
||||
@@ -464,6 +467,8 @@ struct Walker<'w, 'p> {
|
||||
/// The saved graphics states' overprint parameters and inks, the current one last, pushed and
|
||||
/// popped as the interpreter does its states.
|
||||
states: Vec<State>,
|
||||
/// The `q`s past [`MAX_STATES`] whose `Q` has not come.
|
||||
unkept: usize,
|
||||
/// The text shown since the last painted path.
|
||||
text: TextOverprint,
|
||||
/// The inks of the text shown since the last painted path: of its fills, of its strokes.
|
||||
@@ -513,12 +518,20 @@ impl Walker<'_, '_> {
|
||||
}
|
||||
|
||||
fn save(&mut self) {
|
||||
// Past this many nested states the new ones are not kept (a stream of `q` is cheap to write
|
||||
// and holds a state each); their `Q`s are counted off first.
|
||||
if self.states.len() >= MAX_STATES {
|
||||
self.unkept = self.unkept.saturating_add(1);
|
||||
return;
|
||||
}
|
||||
self.states.push(self.state());
|
||||
}
|
||||
|
||||
/// The interpreter never pops its first state.
|
||||
fn restore(&mut self) {
|
||||
if self.states.len() > 1 {
|
||||
if self.unkept > 0 {
|
||||
self.unkept -= 1;
|
||||
} else if self.states.len() > 1 {
|
||||
self.states.pop();
|
||||
}
|
||||
}
|
||||
@@ -1028,6 +1041,7 @@ fn walk_page<'w, 'p>(page: &Page<'p>, ocgs: &'w mut Ocgs, all_on: bool, fonts: &
|
||||
out: Scan::default(),
|
||||
hidden_form: false,
|
||||
states: vec![State::default()],
|
||||
unkept: 0,
|
||||
text: TextOverprint::None,
|
||||
text_inks: [RunInk::None; 2],
|
||||
ink_index: HashMap::new(),
|
||||
|
||||
@@ -166,6 +166,9 @@ pub(crate) struct TextLine {
|
||||
/// Runs of glyphs drawn alike: their look, how far the file raised them off the line's
|
||||
/// baseline (points, up; text rise, `Ts`), their text.
|
||||
runs: Vec<(Look, f64, String)>,
|
||||
/// The bytes of text the horizontal glyphs added to `runs` so far (counted as they come: a
|
||||
/// sum over the runs for each glyph is quadratic when every glyph is a run).
|
||||
text_bytes: usize,
|
||||
/// Where the file put each character of the runs' text, in order: the first character of each
|
||||
/// glyph has its glyph's [`Slot`]; a space read from a gap and the other characters of a
|
||||
/// glyph of several have none.
|
||||
@@ -227,6 +230,7 @@ impl TextLine {
|
||||
last_size: at.size,
|
||||
baseline: vec![],
|
||||
runs: vec![],
|
||||
text_bytes: 0,
|
||||
slots: vec![],
|
||||
upright: None,
|
||||
column: false,
|
||||
@@ -292,7 +296,7 @@ impl TextLine {
|
||||
if self.column || self.upright.is_some() {
|
||||
return false;
|
||||
}
|
||||
let bytes: usize = self.runs.iter().map(|(_, _, t)| t.len()).sum();
|
||||
let bytes = self.text_bytes;
|
||||
let glyph = self.baseline.len();
|
||||
let curved = self.curved();
|
||||
let mut shift = 0.0;
|
||||
@@ -350,6 +354,7 @@ impl TextLine {
|
||||
} else {
|
||||
self.runs.push((look.clone(), 0.0, text.to_string()));
|
||||
}
|
||||
self.text_bytes = self.text_bytes.saturating_add(text.len());
|
||||
let one = text.chars().count() == 1;
|
||||
self.slots.push(Some(Slot { glyph, advance: one.then_some(advance) }));
|
||||
self.slots.extend(text.chars().skip(1).map(|_| None));
|
||||
|
||||
@@ -221,6 +221,10 @@ impl<'a> Job<'a> {
|
||||
if printed.is_empty() {
|
||||
return Err(PdfError::BadSetting("output.inks: no ink is set to print".into()));
|
||||
}
|
||||
// Counted before the sheets are made: layouts times inks can be far more than the limit.
|
||||
if layouts.len().saturating_mul(printed.len()).saturating_mul(set.copies as usize) > MAX_PAGES {
|
||||
return Err(PdfError::BadSetting(format!("the job would print more than {MAX_PAGES} pages")));
|
||||
}
|
||||
let sheets: Vec<(usize, Option<usize>)> = (0..layouts.len()).flat_map(|l| printed.iter().map(move |i| (l, *i))).collect();
|
||||
if sheets.len().saturating_mul(set.copies as usize) > MAX_PAGES {
|
||||
return Err(PdfError::BadSetting(format!("the job would print more than {MAX_PAGES} pages")));
|
||||
|
||||
Reference in New Issue
Block a user