security(doc): compute auto-slice coverage per band, not per cell

auto_slices tested every grid cell against the full slice-rectangle list
(covered(x, y) = taken.iter().any(...)). With slice edges forming the
grid, that is cubic in the slice count, so a crafted document's slice
arrangement could stall ordinary canvas drawing on the UI thread.

For each row band, mark covered x-cells in a single pass over the rects
that span the band, then scan the precomputed cells. This drops the inner
work from O(rects) per cell to O(1), while producing the same tiling.

Finding: 9fbea43b-3096-4cdc-b005-86d44a8fe57e (CWE-400)
This commit is contained in:
Dylan M. Taylor
2026-10-22 07:11:06 -04:00
parent 9e8a3eecf0
commit 58cd22d4ff
+71 -6
View File
@@ -202,24 +202,42 @@ pub fn auto_slices(canvas: Rect, taken: &[Rect]) -> Vec<Rect> {
xs.dedup();
ys.sort_unstable();
ys.dedup();
let covered = |x0: i32, y0: i32| taken.iter().any(|r| x0 >= r.x0 && x0 < r.x1 && y0 >= r.y0 && y0 < r.y1);
// Horizontal runs per row band.
// Clip the taken rectangles to the canvas once, so band coverage only considers rects that
// can actually cover a cell.
let clipped: Vec<Rect> = taken.iter().map(|r| r.intersect(&canvas)).filter(|r| !r.is_empty()).collect();
// Map an x edge to its cell index in `xs` (xs is sorted/deduped).
let x_cell = |x: i32| xs.partition_point(|&e| e <= x).saturating_sub(1);
// Horizontal runs per row band. For each band, mark covered x-cells in a single pass over
// the rects intersecting that band, instead of scanning every rect for every cell (which
// was cubic in the slice count and let a crafted document stall canvas drawing).
let ncells = xs.len().saturating_sub(1);
let mut runs: Vec<Rect> = Vec::new();
for yw in ys.windows(2) {
let (band_y0, band_y1) = (yw[0], yw[1]);
let mut covered_cell = vec![false; ncells];
for r in &clipped {
if r.y0 <= band_y0 && band_y1 <= r.y1 {
// Rect spans this whole band vertically; mark the x-cells it covers.
let (c0, c1) = (x_cell(r.x0), x_cell(r.x1));
for c in covered_cell.iter_mut().take(c1).skip(c0) {
*c = true;
}
}
}
let mut start: Option<i32> = None;
for xw in xs.windows(2) {
let free = !covered(xw[0], yw[0]);
for (ci, xw) in xs.windows(2).enumerate() {
let free = !covered_cell.get(ci).copied().unwrap_or(false);
match (free, start) {
(true, None) => start = Some(xw[0]),
(false, Some(s)) => {
runs.push(Rect::new(s, yw[0], xw[0], yw[1]));
runs.push(Rect::new(s, band_y0, xw[0], band_y1));
start = None;
}
_ => {}
}
}
if let Some(s) = start {
runs.push(Rect::new(s, yw[0], *xs.last().unwrap_or(&canvas.x1), yw[1]));
runs.push(Rect::new(s, band_y0, *xs.last().unwrap_or(&canvas.x1), band_y1));
}
}
// Merge vertically adjacent runs with the same columns.
@@ -299,6 +317,53 @@ mod tests {
assert_eq!(auto_slices(canvas, &[]), vec![canvas]);
}
#[test]
fn auto_slices_cover_exactly_with_many_overlapping_slices() {
// Several overlapping slices form a non-trivial grid. The per-band coverage pass must
// still tile the complement exactly: auto area + taken-union area == canvas, with no
// auto slice overlapping a taken rect or another auto slice.
let canvas = Rect::new(0, 0, 200, 150);
let taken = [Rect::new(10, 10, 90, 70), Rect::new(50, 40, 160, 120), Rect::new(120, 0, 200, 50), Rect::new(0, 100, 60, 150)];
let auto = auto_slices(canvas, &taken);
// No auto slice intersects any taken rect.
for a in &auto {
for t in &taken {
assert!(a.intersect(t).is_empty(), "{a:?} overlaps taken {t:?}");
}
}
// Auto slices do not overlap each other.
for (i, a) in auto.iter().enumerate() {
for b in &auto[i + 1..] {
assert!(a.intersect(b).is_empty(), "{a:?} overlaps {b:?}");
}
}
// Auto area equals the canvas minus the union of taken rects (computed on the same grid).
let union_cells: i64 = {
// Sum cell areas covered by at least one taken rect, over the fine grid.
let mut xs = vec![canvas.x0, canvas.x1];
let mut ys = vec![canvas.y0, canvas.y1];
for t in &taken {
xs.extend([t.x0, t.x1]);
ys.extend([t.y0, t.y1]);
}
xs.sort_unstable();
xs.dedup();
ys.sort_unstable();
ys.dedup();
let mut sum = 0i64;
for yw in ys.windows(2) {
for xw in xs.windows(2) {
let covered = taken.iter().any(|r| xw[0] >= r.x0 && xw[0] < r.x1 && yw[0] >= r.y0 && yw[0] < r.y1);
if covered {
sum += i64::from(xw[1] - xw[0]) * i64::from(yw[1] - yw[0]);
}
}
}
sum
};
assert_eq!(area(&auto) + union_cells, i64::from(canvas.width()) * i64::from(canvas.height()));
}
#[test]
fn resolve_numbers_top_to_bottom() {
let mut d = Document::new("site.psd", Size::new(100, 80), ColorMode::Rgb, SampleType::U8);