Compare commits

...
3 changed files with 72 additions and 9 deletions
+12
View File
@@ -46,6 +46,12 @@ pub struct Imp<'a> {
pub tag_bytes: usize, pub tag_bytes: usize,
/// The tag budget ran out and the warning was given. /// The tag budget ran out and the warning was given.
pub tags_capped: bool, pub tags_capped: bool,
/// Bytes of preserved theme XML kept by the masters read so far.
pub theme_extra_bytes: usize,
/// Shapes read from SmartArt drawings so far (see `shapes::MAX_SMARTART_SHAPES`).
pub smartart_shapes: usize,
/// Bytes of SmartArt layout, style and colour definitions copied into diagrams so far.
pub diagram_xml_bytes: usize,
/// Whether a part has relationships of its own, by normalized part name: looked up once /// Whether a part has relationships of its own, by normalized part name: looked up once
/// however many embedded objects point at the part. /// however many embedded objects point at the part.
pub has_rels: HashMap<String, bool>, pub has_rels: HashMap<String, bool>,
@@ -449,6 +455,9 @@ pub fn import_theme(bytes: &[u8]) -> Result<(deckcraft_model::theme::Theme, Vec<
tags_by_part: HashMap::new(), tags_by_part: HashMap::new(),
tag_bytes: 0, tag_bytes: 0,
tags_capped: false, tags_capped: false,
theme_extra_bytes: 0,
smartart_shapes: 0,
diagram_xml_bytes: 0,
has_rels: HashMap::new(), has_rels: HashMap::new(),
}; };
let part = Part::new(&pkg, &theme_path, &doc); let part = Part::new(&pkg, &theme_path, &doc);
@@ -491,6 +500,9 @@ pub fn import_checked(bytes: &[u8]) -> Result<(Presentation, Vec<String>), PptxE
tags_by_part: HashMap::new(), tags_by_part: HashMap::new(),
tag_bytes: 0, tag_bytes: 0,
tags_capped: false, tags_capped: false,
theme_extra_bytes: 0,
smartart_shapes: 0,
diagram_xml_bytes: 0,
has_rels: HashMap::new(), has_rels: HashMap::new(),
}; };
// Slide ids first so hyperlinks anywhere can point at slides. // Slide ids first so hyperlinks anywhere can point at slides.
+50 -8
View File
@@ -480,6 +480,14 @@ fn frame(imp: &mut Imp, part: &Part, ctx: &mut IdCtx, e: &El, depth: usize) -> S
return None; return None;
} }
let (kind, target) = (rel.ty.clone(), rel.target.clone()); let (kind, target) = (rel.ty.clone(), rel.target.clone());
// A diagram's definition parts copied here count against the same presentation-wide budget.
if rel.kind.starts_with("diagram") && !imp.media_by_part.contains_key(&crate::opc::norm(&target)) {
let len = pkg.get(&target).map_or(0, |b| b.len());
if len > MAX_DIAGRAM_PART_BYTES || imp.diagram_xml_bytes.saturating_add(len) > MAX_DIAGRAM_XML_BYTES {
return None;
}
imp.diagram_xml_bytes += len;
}
let media = if rel.kind == "diagramData" { diagram_data(imp, &target) } else { imp.media_for(&target) }; let media = if rel.kind == "diagramData" { diagram_data(imp, &target) } else { imp.media_for(&target) };
Some((id, kind, media?)) Some((id, kind, media?))
}) })
@@ -559,9 +567,23 @@ fn diagram_data(imp: &mut Imp, target: &str) -> Option<deckcraft_model::MediaId>
Some(id) Some(id)
} }
/// Shapes all SmartArt drawings of one presentation may add; a frame read after that is left as a
/// placeholder. Each frame that references a drawing reads it again.
const MAX_SMARTART_SHAPES: usize = 250_000;
/// SmartArt definition XML (layout, style, colours) all diagrams of one presentation may copy.
const MAX_DIAGRAM_XML_BYTES: usize = 32 << 20;
/// The largest single diagram definition part copied.
const MAX_DIAGRAM_PART_BYTES: usize = 4 << 20;
fn count_shapes(shapes: &[Shape]) -> usize {
shapes.iter().map(|s| 1 + count_shapes(s.children())).sum()
}
/// SmartArt: read the pre-drawn shapes of its drawing part as a group. /// SmartArt: read the pre-drawn shapes of its drawing part as a group.
fn smartart(imp: &mut Imp, part: &Part, gd: &El, frame: &Shape, depth: usize) -> Option<Shape> { fn smartart(imp: &mut Imp, part: &Part, gd: &El, frame: &Shape, depth: usize) -> Option<Shape> {
if depth > 8 { if depth > 8 || imp.smartart_shapes >= MAX_SMARTART_SHAPES {
return None; return None;
} }
let rel_ids = gd.child("relIds")?; let rel_ids = gd.child("relIds")?;
@@ -573,10 +595,19 @@ fn smartart(imp: &mut Imp, part: &Part, gd: &El, frame: &Shape, depth: usize) ->
let dpart = Part::new(imp.pkg, &drawing_rel, &d); let dpart = Part::new(imp.pkg, &drawing_rel, &d);
let tree = d.root.child("spTree")?; let tree = d.root.child("spTree")?;
let mut sub = IdCtx { fresh: true, ..Default::default() }; let mut sub = IdCtx { fresh: true, ..Default::default() };
let before = imp.smartart_shapes;
let children = sp_tree(imp, &dpart, &mut sub, tree, depth + 1); let children = sp_tree(imp, &dpart, &mut sub, tree, depth + 1);
if children.is_empty() { if children.is_empty() {
return None; return None;
} }
// Counted once here, nested SmartArt included: a drawing that frames reuse counts every time it is read.
let total = before.saturating_add(count_shapes(&children));
if total > MAX_SMARTART_SHAPES {
// Over the presentation's remaining budget: a placeholder, and nothing charged, nested SmartArt included.
imp.smartart_shapes = before;
return None;
}
imp.smartart_shapes = total;
let children = smartart_text_rects(imp, &mut sub, tree, children, depth + 1); let children = smartart_text_rects(imp, &mut sub, tree, children, depth + 1);
let mut g = frame.clone(); let mut g = frame.clone();
let x = frame.xfrm.unwrap_or_default(); let x = frame.xfrm.unwrap_or_default();
@@ -665,10 +696,11 @@ fn text_on_rect(imp: &mut Imp, ctx: &mut IdCtx, s: &mut Shape, t: Xfrm) -> Optio
return None; return None;
} }
} }
if ctx.count >= MAX_SHAPES { if ctx.count >= MAX_SHAPES || imp.smartart_shapes >= MAX_SMARTART_SHAPES {
return None; return None;
} }
ctx.count += 1; ctx.count += 1;
imp.smartart_shapes += 1;
let text = s.text.take(); let text = s.text.take();
// No fill, outline or effects; the style keeps only the text colour and font. // No fill, outline or effects; the style keeps only the text colour and font.
let style = s.style.clone().map(|mut st| { let style = s.style.clone().map(|mut st| {
@@ -704,15 +736,22 @@ fn text_area(geom: &Geom, w: f64, h: f64) -> deckcraft_geom::Rect {
/// The outline of a SmartArt graphic (for the Text Pane), with its layout, quick style and colour /// The outline of a SmartArt graphic (for the Text Pane), with its layout, quick style and colour
/// parts kept to be written back. /// parts kept to be written back.
fn diagram_outline(imp: &Imp, part: &Part, rel_ids: &El, data: &El) -> Option<deckcraft_model::Diagram> { fn diagram_outline(imp: &mut Imp, part: &Part, rel_ids: &El, data: &El) -> Option<deckcraft_model::Diagram> {
let nodes = crate::dgm::outline(data); let nodes = crate::dgm::outline(data);
if nodes.is_empty() { if nodes.is_empty() {
return None; return None;
} }
let (pkg, spent) = (imp.pkg, std::cell::Cell::new(imp.diagram_xml_bytes));
let raw = |attr: &str| -> Option<String> { let raw = |attr: &str| -> Option<String> {
let rel = part.rels.get(rel_ids.attr(attr)?)?; let rel = part.rels.get(rel_ids.attr(attr)?)?;
let bytes = imp.pkg.get(&rel.target)?; let bytes = pkg.get(&rel.target)?;
(bytes.len() <= 4 << 20).then(|| String::from_utf8(bytes.to_vec()).ok()).flatten() // Every diagram copies its definitions, so the copies share one presentation-wide budget.
if bytes.len() > MAX_DIAGRAM_PART_BYTES || spent.get().saturating_add(bytes.len()) > MAX_DIAGRAM_XML_BYTES {
return None;
}
let xml = String::from_utf8(bytes.to_vec()).ok()?;
spent.set(spent.get() + xml.len());
Some(xml)
}; };
let layout_xml = raw("r:lo"); let layout_xml = raw("r:lo");
let (layout_id, cat) = layout_xml let (layout_id, cat) = layout_xml
@@ -733,16 +772,19 @@ fn diagram_outline(imp: &Imp, part: &Part, rel_ids: &El, data: &El) -> Option<de
let pr = data.path(&["ptLst"]).and_then(|l| l.children_named("pt").find(|p| p.attr("type") == Some("doc"))).and_then(|p| p.child("prSet")); let pr = data.path(&["ptLst"]).and_then(|l| l.children_named("pt").find(|p| p.attr("type") == Some("doc"))).and_then(|p| p.child("prSet"));
let style = crate::dgm::style_for(pr.and_then(|p| p.attr("qsTypeId")).unwrap_or("")).to_string(); let style = crate::dgm::style_for(pr.and_then(|p| p.attr("qsTypeId")).unwrap_or("")).to_string();
let colors = crate::dgm::colors_for(pr.and_then(|p| p.attr("csTypeId")).unwrap_or("")); let colors = crate::dgm::colors_for(pr.and_then(|p| p.attr("csTypeId")).unwrap_or(""));
let (style_xml, colors_xml) = (raw("r:qs"), raw("r:cs"));
imp.diagram_xml_bytes = spent.get();
Some(deckcraft_model::Diagram { Some(deckcraft_model::Diagram {
kind: crate::dgm::kind_for(&id, &cat).to_string(), kind: crate::dgm::kind_for(&id, &cat).to_string(),
nodes, nodes,
style, style,
colors, colors,
imported: true, imported: true,
layout_id: id, // Without the layout part the writer generates one for `kind`; empty keeps data's loTypeId in step with it.
layout_id: if layout_xml.is_some() { id } else { String::new() },
layout_xml, layout_xml,
style_xml: raw("r:qs"), style_xml,
colors_xml: raw("r:cs"), colors_xml,
}) })
} }
+10 -1
View File
@@ -20,6 +20,10 @@ fn font_set(e: Option<&El>) -> FontSet {
FontSet { latin: tf("latin"), ea: tf("ea"), cs: tf("cs"), scripts } FontSet { latin: tf("latin"), ea: tf("ea"), cs: tf("cs"), scripts }
} }
/// Bytes of preserved theme XML (object defaults and extra colour schemes) all masters of one
/// presentation may keep.
const MAX_THEME_EXTRA_BYTES: usize = 16 << 20;
pub fn read_theme(imp: &mut Imp, part: &Part, root: &El) -> Theme { pub fn read_theme(imp: &mut Imp, part: &Part, root: &El) -> Theme {
let mut t = Theme { name: root.attr("name").unwrap_or("Theme").to_string(), ..Default::default() }; let mut t = Theme { name: root.attr("name").unwrap_or("Theme").to_string(), ..Default::default() };
let Some(te) = root.child("themeElements") else { return t }; let Some(te) = root.child("themeElements") else { return t };
@@ -89,8 +93,13 @@ pub fn read_theme(imp: &mut Imp, part: &Part, root: &El) -> Theme {
.filter(|e| !super::has_rel_refs(e)) .filter(|e| !super::has_rel_refs(e))
.map(|e| part.keep(e)) .map(|e| part.keep(e))
.collect(); .collect();
if !extra.is_empty() { // Every master that references this theme keeps its own copy of the preserved XML, so the
// copies share one budget for the presentation.
if !extra.is_empty() && imp.theme_extra_bytes.saturating_add(extra.len()) <= MAX_THEME_EXTRA_BYTES {
imp.theme_extra_bytes += extra.len();
t.raw_extra = Some(extra); t.raw_extra = Some(extra);
} else if !extra.is_empty() {
log::warn!("pptx: theme object defaults not kept; the preserved-XML budget is spent");
} }
t t
} }