mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-28 04:17:06 -06:00
#3692: a row's gridAfter is bounded like the DOCX reader's
Audit finding 35: Table::cols() added a deserialized row's grid_after (u32) unchecked, and layout_table sized its column vectors from it, so a small native JSON document could ask for a 16 GB grid (and overflow the sum on 32-bit wasm). Row::grid_after() now clamps to MAX_COLS as first_grid() does and the importer does; cols() sums with saturating adds; layout reads the bounded value. Test: tests_budget::a_huge_grid_after_is_bounded_like_the_docx_readers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d81b43b336
commit
4963e30eed
@@ -48,6 +48,12 @@ impl Row {
|
||||
pub fn first_grid(&self) -> usize {
|
||||
(self.props.grid_before as usize).min(MAX_COLS)
|
||||
}
|
||||
|
||||
/// Grid columns `w:gridAfter` leaves empty after the row's last cell, bounded as the DOCX
|
||||
/// reader bounds them (a native document can say anything).
|
||||
pub fn grid_after(&self) -> usize {
|
||||
(self.props.grid_after as usize).min(MAX_COLS)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
|
||||
@@ -97,7 +103,8 @@ impl Table {
|
||||
}
|
||||
|
||||
pub fn cols(&self) -> usize {
|
||||
let row_cols = |r: &Row| r.first_grid() + r.cells.iter().map(Cell::span).sum::<usize>() + r.props.grid_after as usize;
|
||||
let row_cols =
|
||||
|r: &Row| r.first_grid().saturating_add(r.cells.iter().map(Cell::span).fold(0, usize::saturating_add)).saturating_add(r.grid_after());
|
||||
self.grid.len().max(self.rows.iter().map(row_cols).max().unwrap_or(0))
|
||||
}
|
||||
|
||||
|
||||
@@ -4967,6 +4967,8 @@ mod tests_autospace;
|
||||
#[cfg(test)]
|
||||
mod tests_bidi;
|
||||
#[cfg(test)]
|
||||
mod tests_budget;
|
||||
#[cfg(test)]
|
||||
mod tests_complex;
|
||||
#[cfg(test)]
|
||||
mod tests_grid;
|
||||
|
||||
@@ -537,7 +537,7 @@ pub fn layout_table(ctx: &mut Ctx, story: StoryRef, t: &Table, path: &[u32], ava
|
||||
let row_borders = row.props.exceptions.as_ref().and_then(|e| e.borders).unwrap_or_default();
|
||||
// A row that skips grid columns (`w:gridBefore`/`w:gridAfter`) has its edge at its own
|
||||
// first and last cell.
|
||||
let (first_g, last_g) = (g == row.first_grid(), g + span >= ncols.saturating_sub(row.props.grid_after as usize));
|
||||
let (first_g, last_g) = (g == row.first_grid(), g + span >= ncols.saturating_sub(row.grid_after()));
|
||||
let rb = look.map(|l| region_borders(&l.borders, (ri, nrows), (first_g, last_g))).unwrap_or_default();
|
||||
let mut borders = Borders {
|
||||
top: edge(cb.top.or(if ri == 0 { row_borders.top } else { row_borders.between }).or(rb.top), ri == 0, tb.top, tb.between),
|
||||
@@ -1170,7 +1170,7 @@ fn column_content(ctx: &mut Ctx, t: &Table, style: Option<&TableStyleProps>, dep
|
||||
/// columns, width in points). Column widths make room for them like for an empty cell.
|
||||
fn row_gaps(row: &Row, end: usize) -> impl Iterator<Item = (usize, usize, f32)> {
|
||||
let p = &row.props;
|
||||
[(0, row.first_grid(), p.w_before), (end, p.grid_after as usize, p.w_after)]
|
||||
[(0, row.first_grid(), p.w_before), (end, row.grid_after(), p.w_after)]
|
||||
.into_iter()
|
||||
.filter(|&(_, n, _)| n > 0)
|
||||
.filter_map(|(g, n, w)| w.filter(|w| w.is_finite() && *w > 0.0).map(|w| (g, n, w.min(1584.0))))
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
//! Hostile documents whose layout work or memory would otherwise grow far past their size: each
|
||||
//! is bounded by a cap, and real documents stay well under it.
|
||||
|
||||
use super::*;
|
||||
use wordcraft_doc::Block;
|
||||
use wordcraft_doc::table::{MAX_COLS, Table};
|
||||
|
||||
fn lay(doc: &Document) -> DocLayout {
|
||||
let mut c = LayoutCache::new();
|
||||
layout(doc, &mut c, &LayoutOptions::default())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_huge_grid_after_is_bounded_like_the_docx_readers() {
|
||||
// A native document can say a row leaves four billion columns empty after its cells.
|
||||
let mut t = Table::new(1, 2, 468.0);
|
||||
t.rows[0].props.grid_after = u32::MAX;
|
||||
t.grid.clear();
|
||||
assert!(t.cols() <= 2 + MAX_COLS, "{} columns", t.cols());
|
||||
let mut d = Document::from_text("");
|
||||
d.insert_block(StoryRef::Body, &Path::top(1), Block::Table(t)).unwrap();
|
||||
let l = lay(&d);
|
||||
assert!(l.pages[0].items.iter().any(|i| matches!(i, Placed::Cell { row: 0, cell: 1, .. })));
|
||||
}
|
||||
Reference in New Issue
Block a user