#3692: empty polygon groups in metafiles reserve nothing

Finding 33. geom::subpaths reserved pts + counts ops up front, so a
poly-polygon record with a long list of zero counts reserved room for ops
it never emits, and the canvas charged only ops.len() against MAX_BYTES.
subpaths now sizes the vector exactly, and Canvas::push charges what each
item holds (capacity) for paths, bitmaps and text.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Martin Ball
2026-10-26 00:44:51 +00:00
co-authored by Claude Opus 5.5
parent 2a647661fb
commit c98efffc06
3 changed files with 32 additions and 4 deletions
+6 -3
View File
@@ -284,9 +284,12 @@ impl Canvas {
fn push(&mut self, raw: Raw) {
let cost = match &raw {
Raw::Path { ops, .. } => ops.len() * std::mem::size_of::<Op>(),
Raw::Bitmap { img, .. } => img.px.len(),
Raw::Text(t) => t.text.len() + t.font.len() + t.dx.len() * std::mem::size_of::<f64>(),
// What is held, capacity and all.
Raw::Path { ops, .. } => ops.capacity().saturating_mul(std::mem::size_of::<Op>()),
Raw::Bitmap { img, .. } => img.px.capacity(),
Raw::Text(t) => {
t.text.capacity().saturating_add(t.font.capacity()).saturating_add(t.dx.capacity().saturating_mul(std::mem::size_of::<f64>()))
}
};
if self.items.len() >= MAX_ITEMS || self.used.saturating_add(cost) > MAX_BYTES {
self.full = true;
+11 -1
View File
@@ -99,7 +99,17 @@ fn arc(cx: f64, cy: f64, rx: f64, ry: f64, a0: f64, sweep: f64) -> Vec<Op> {
/// Consecutive subpaths: `counts[i]` points each, taken in order from `pts`. Counts that run past the
/// point list are cut short rather than failing the whole record.
pub(crate) fn subpaths(pts: &[P], counts: &[usize], close: bool) -> Vec<Op> {
let mut ops = Vec::with_capacity(pts.len() + counts.len());
// The exact size first: empty groups add no ops, so a long count list of zeros (file data)
// mustn't reserve room the canvas budget never sees.
let (mut n, mut at) = (0usize, 0usize);
for &c in counts {
let end = at.saturating_add(c).min(pts.len());
if end > at {
n = n.saturating_add(end - at + usize::from(close));
}
at = end;
}
let mut ops = Vec::with_capacity(n);
let mut at = 0usize;
for &c in counts {
let end = at.saturating_add(c).min(pts.len());
+15
View File
@@ -1053,3 +1053,18 @@ fn plain_wmf_target_comes_from_a_window_that_was_read() {
let (x0, y0, x1, y1) = rect_extent(&p.items[0]);
assert!(close(x0, 0.0) && close(y0, 0.0) && close(x1, 200.0) && close(y1, 100.0), "{x0} {y0} {x1} {y1}");
}
/// A poly-polygon's group counts are file data: a long list of empty groups adds no ops, so it
/// mustn't reserve room for them either (the canvas charges what a path holds, capacity included).
#[test]
fn empty_poly_groups_reserve_nothing() {
let pts = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)];
let mut counts = vec![0usize; 100_000];
counts.push(3);
let ops = crate::geom::subpaths(&pts, &counts, true);
assert_eq!(ops.len(), 4, "M L L Z");
assert!(ops.capacity() <= 8, "capacity {}", ops.capacity());
// Counts past the points are cut short, and the size still matches.
let ops = crate::geom::subpaths(&pts, &[2, 5, 4], false);
assert_eq!((ops.len(), ops.capacity()), (3, 3));
}