diff --git a/crates/pptx/src/read.rs b/crates/pptx/src/read.rs index 288e497f8..ad8453538 100644 --- a/crates/pptx/src/read.rs +++ b/crates/pptx/src/read.rs @@ -38,6 +38,8 @@ pub struct Imp<'a> { pub vml_previews: HashMap>, /// Aggregate byte count of retained chart sidecars across the presentation. pub kept_sidecar_bytes: usize, + /// Data slots of the charts decoded so far (see `chart::MAX_PRESENTATION_CHART_POINTS`). + pub chart_points: usize, /// Cached kept sidecars by normalized chart part name. pub kept_charts: HashMap, /// Add-in tags parts, parsed once each, by normalized part name. @@ -442,6 +444,7 @@ pub fn import_theme(bytes: &[u8]) -> Result<(deckcraft_model::theme::Theme, Vec< table_style_ids: Vec::new(), vml_previews: HashMap::new(), kept_sidecar_bytes: 0, + chart_points: 0, kept_charts: HashMap::new(), tags_by_part: HashMap::new(), tag_bytes: 0, @@ -483,6 +486,7 @@ pub fn import_checked(bytes: &[u8]) -> Result<(Presentation, Vec), PptxE table_style_ids: Vec::new(), vml_previews: HashMap::new(), kept_sidecar_bytes: 0, + chart_points: 0, kept_charts: HashMap::new(), tags_by_part: HashMap::new(), tag_bytes: 0, diff --git a/crates/pptx/src/read/chart.rs b/crates/pptx/src/read/chart.rs index 358f88ff5..af54ebd8e 100644 --- a/crates/pptx/src/read/chart.rs +++ b/crates/pptx/src/read/chart.rs @@ -39,6 +39,14 @@ pub(crate) fn rich_text(t: &El) -> Option { /// Most category/value/size slots one chart may decode across all its series and caches, so /// sparse `ptCount`/`idx` declarations can't expand far beyond the XML they came from. pub(crate) const MAX_CHART_POINTS: usize = 1_000_000; +/// Data slots (values and categories) the charts of one presentation may hold in all; a chart past +/// it is read as a placeholder frame. Each frame that references a chart decodes its own copy. +pub(crate) const MAX_PRESENTATION_CHART_POINTS: usize = 4_000_000; + +/// Slots a decoded chart holds, counting the chart itself as at least one. +pub(crate) fn point_count(c: &Chart) -> usize { + 1 + c.categories.len() + c.series.iter().map(|s| s.values.len()).sum::() +} /// Most slots one cache may hold. const MAX_CACHE_POINTS: usize = 100_000; @@ -466,6 +474,13 @@ pub(crate) fn capture_kept(imp: &mut super::Imp, chart_part: &str) -> deckcraft_ let key = crate::opc::norm(chart_part); if let Some(cached) = imp.kept_charts.get(&key) { + // Every frame that shows this chart gets its own copy, so each copy is charged like the first. + let bytes: usize = cached.parts.iter().map(|p| p.bytes.len()).sum(); + if imp.kept_sidecar_bytes.saturating_add(bytes) > MAX_KEPT_TOTAL_BYTES { + log::warn!("pptx: chart sidecar retained byte budget exceeded for {chart_part}"); + return ChartKept::default(); + } + imp.kept_sidecar_bytes += bytes; return cached.clone(); } diff --git a/crates/pptx/src/read/shapes.rs b/crates/pptx/src/read/shapes.rs index b3e3d48e6..96de4dd3c 100644 --- a/crates/pptx/src/read/shapes.rs +++ b/crates/pptx/src/read/shapes.rs @@ -428,10 +428,12 @@ fn frame(imp: &mut Imp, part: &Part, ctx: &mut IdCtx, e: &El, depth: usize) -> S return s; } if let Some(c) = gd.child("chart") + && imp.chart_points < super::chart::MAX_PRESENTATION_CHART_POINTS && let Some(rel) = part.rels.get(c.attr("r:id").unwrap_or("")) && let Some(bytes) = imp.pkg.xml_bytes(&rel.target) && let Some(mut chart) = if uri.ends_with("/chartex") { super::read_chartex_xml(bytes) } else { super::read_chart_xml(bytes) } { + imp.chart_points = imp.chart_points.saturating_add(super::chart::point_count(&chart)); chart.kept = super::chart::capture_kept(imp, &rel.target); s.kind = ShapeKind::Chart(Box::new(chart)); return s;