Compare commits

..
6 changed files with 22 additions and 51 deletions
+5
View File
@@ -415,6 +415,11 @@ impl CaptureScanner {
}
fn read_parts(src: &(impl ByteSource + ?Sized), parts: &[(u64, u32)]) -> Result<Vec<u8>> {
// Ranges come from a scan of some source; check them against this one before sizing a buffer.
let len = src.len();
if parts.iter().any(|&(o, n)| o.checked_add(u64::from(n)).is_none_or(|end| end > len)) {
return Err(Error::Invalid("packet range lies outside the source".into()));
}
let mut v = Vec::with_capacity(parts.iter().map(|p| p.1 as usize).sum());
for &(o, n) in parts {
let a = v.len();
+10
View File
@@ -274,3 +274,13 @@ fn a_budget_too_small_to_search_is_spent() {
let f = opus_file(6, 312, 0);
assert_eq!(scanner.find(&f, 0, f.len() as u64).unwrap(), None);
}
#[test]
fn read_packet_checks_its_ranges_against_the_source_before_allocating() {
let f = opus_file(10, 312, 500);
let o = open(&f).unwrap();
let s = o.stream_of(Codec::Opus).unwrap();
// The last packet lies past the end of a source cut short: an error, not a read of missing bytes.
let short = &f[..f.len() / 2];
assert!(matches!(o.read_packet(&short, s, 9), Err(Error::Invalid(_))));
}
-4
View File
@@ -38,8 +38,6 @@ pub struct Imp<'a> {
pub vml_previews: HashMap<String, HashMap<String, String>>,
/// 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<String, deckcraft_model::chart::ChartKept>,
/// Add-in tags parts, parsed once each, by normalized part name.
@@ -444,7 +442,6 @@ 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,
@@ -486,7 +483,6 @@ pub fn import_checked(bytes: &[u8]) -> Result<(Presentation, Vec<String>), 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,
-42
View File
@@ -39,24 +39,6 @@ pub(crate) fn rich_text(t: &El) -> Option<String> {
/// 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 {
let series = |s: &deckcraft_model::chart::Series| {
let bars = s.error_bars.as_ref().map_or(0, |e| e.plus.len().saturating_add(e.minus.len()));
s.values.len().saturating_add(s.x_values.len()).saturating_add(s.sizes.len()).saturating_add(bars)
};
let levels: usize = c.category_levels.iter().map(Vec::len).sum();
1usize.saturating_add(c.categories.len()).saturating_add(levels).saturating_add(c.series.iter().map(series).fold(0, usize::saturating_add))
}
/// Bytes of the strings a kept relationship owns.
fn rel_bytes(r: &deckcraft_model::chart::ChartRel) -> usize {
r.id.len() + r.rel_type.len() + r.target.len() + r.part.len()
}
/// Most slots one cache may hold.
const MAX_CACHE_POINTS: usize = 100_000;
@@ -484,13 +466,6 @@ 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::<usize>() + cached.rels.iter().map(rel_bytes).sum::<usize>();
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();
}
@@ -510,13 +485,6 @@ pub(crate) fn capture_kept(imp: &mut super::Imp, chart_part: &str) -> deckcraft_
kept.rels.push(ChartRel { id: r.id, rel_type: r.ty, target: r.target_raw, part: r.target, external: false });
}
}
// The first capture's relationship strings are retained too (the parts were charged as copied).
let rel_total: usize = kept.rels.iter().map(rel_bytes).sum();
if imp.kept_sidecar_bytes.saturating_add(rel_total) > MAX_KEPT_TOTAL_BYTES {
log::warn!("pptx: chart sidecar retained byte budget exceeded for {chart_part}");
return ChartKept::default();
}
imp.kept_sidecar_bytes += rel_total;
imp.kept_charts.insert(key, kept.clone());
kept
}
@@ -622,16 +590,6 @@ fn text_size(e: Option<&El>) -> Option<f64> {
mod tests {
use super::*;
#[test]
fn point_count_charges_every_retained_vector() {
let mut c = Chart { categories: vec!["a".into()], category_levels: vec![vec!["x".into(); 2], vec!["y".into(); 3]], ..Default::default() };
let mut s =
deckcraft_model::chart::Series { values: vec![Some(1.0); 4], x_values: vec![None; 5], sizes: vec![None; 6], ..Default::default() };
s.error_bars = Some(deckcraft_model::chart::ErrorBars { plus: vec![None; 7], minus: vec![None; 8], ..Default::default() });
c.series = vec![s];
assert_eq!(point_count(&c), 1 + 1 + 5 + 4 + 5 + 6 + 15);
}
#[test]
fn sparse_caches_share_one_chart_wide_budget() {
// Every series declares 100000 points but writes one: without a chart-wide budget each
-4
View File
@@ -428,14 +428,10 @@ 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) }
// A chart that would cross the presentation limit is read as the placeholder frame instead.
&& imp.chart_points.saturating_add(super::chart::point_count(&chart)) <= super::chart::MAX_PRESENTATION_CHART_POINTS
{
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;
+7 -1
View File
@@ -18,7 +18,13 @@ fn walk(dir: &Path, c: &mut Counts) {
let p = e.path();
let name = e.file_name();
let name = name.to_string_lossy();
if p.is_dir() {
// `file_type` does not follow links, so a directory link is not walked (and aliases are
// not counted twice).
let Ok(kind) = e.file_type() else { continue };
if kind.is_symlink() {
continue;
}
if kind.is_dir() {
// Nested fuzz workspaces and build output are not part of the crate.
if name != "target" && name != "fuzz" && !name.starts_with('.') {
walk(&p, c);