mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 22:28:09 +00:00
Merge pull request #5491 from storytold/fix/sec-affinity-shared-budget
Affinity import: one shared work and name budget across repeated links
This commit is contained in:
@@ -29,6 +29,14 @@ const MAX_DOCUMENT_CELLS: usize = 1 << 18;
|
||||
/// Points of all strokes' pressure curves together (a stroke style may be shared by many layers).
|
||||
const MAX_DOCUMENT_PRESSURE_POINTS: usize = 1 << 20;
|
||||
|
||||
/// Child references inspected, page rectangles produced, and outline segments retained, in all
|
||||
/// (shared objects are visited once per link, so each visit is charged). Real documents stay
|
||||
/// within a few times [`MAX_NODES`]; this leaves ~30x headroom over that.
|
||||
const MAX_DOCUMENT_WORK: usize = 1 << 24;
|
||||
|
||||
/// Owned layer-name characters kept in all (each visit of a shared object keeps its own copy).
|
||||
const MAX_DOCUMENT_NAME_CHARS: usize = 16 << 20;
|
||||
|
||||
/// What a whole read may still use. Shared objects can be linked from many layers, and embedded
|
||||
/// documents are read with what their embedding document left, so linking or embedding the
|
||||
/// same content many times can't multiply the work.
|
||||
@@ -44,6 +52,10 @@ pub(crate) struct Budget {
|
||||
pub(crate) cells: usize,
|
||||
/// Points of pressure curves ([`MAX_DOCUMENT_PRESSURE_POINTS`] in all).
|
||||
pub(crate) pressure: usize,
|
||||
/// Child references, page rectangles and outline segments ([`MAX_DOCUMENT_WORK`] in all).
|
||||
pub(crate) work: usize,
|
||||
/// Layer-name characters ([`MAX_DOCUMENT_NAME_CHARS`] in all).
|
||||
pub(crate) name_chars: usize,
|
||||
}
|
||||
|
||||
impl Budget {
|
||||
@@ -53,7 +65,15 @@ impl Budget {
|
||||
chars: MAX_DOCUMENT_CHARS,
|
||||
cells: MAX_DOCUMENT_CELLS,
|
||||
pressure: MAX_DOCUMENT_PRESSURE_POINTS,
|
||||
work: MAX_DOCUMENT_WORK,
|
||||
name_chars: MAX_DOCUMENT_NAME_CHARS,
|
||||
};
|
||||
|
||||
/// Take `n` units of work, or fail the read.
|
||||
pub(crate) fn spend(&mut self, n: usize) -> Result<(), Error> {
|
||||
self.work = self.work.checked_sub(n).ok_or(Error::Limit("too much shared geometry or page data"))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a document: archive, `doc.dat` and the model.
|
||||
@@ -192,6 +212,7 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
|
||||
return Ok(None);
|
||||
}
|
||||
let pages = pages(s, spread);
|
||||
self.budget.spend(pages.len() + 1)?;
|
||||
let default_size = s.floats::<2>(doc, b"DfSz").filter(|[w, h]| *w > 0.0 && *h > 0.0).map(|[w, h]| Rect { x0: 0.0, y0: 0.0, x1: w, y1: h });
|
||||
let bounds = s.floats::<4>(spread, b"SprB").map(rect).or_else(|| pages.iter().copied().reduce(union)).or(default_size).unwrap_or(Rect {
|
||||
x0: 0.0,
|
||||
@@ -214,7 +235,10 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
|
||||
let s = self.s;
|
||||
let mut out = Vec::new();
|
||||
let mut mask = None;
|
||||
for c in s.objs(parent, b"Chld") {
|
||||
let kids = s.objs(parent, b"Chld");
|
||||
// Invisible masks and skipped children cost no node, so the visit is charged here.
|
||||
self.budget.spend(kids.len())?;
|
||||
for c in kids {
|
||||
if s.is(c, b"MRst") {
|
||||
// A mask layer inside a group or layer masks that container.
|
||||
if s.bool(c, b"Visi") == Some(false) {
|
||||
@@ -254,7 +278,8 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
|
||||
let world = local.then(parent);
|
||||
let class = s.class(id).unwrap_or(Tag(0));
|
||||
// A layer name is short; a long one is cut, as every visit of a shared object keeps a copy.
|
||||
let name: String = s.str(id, b"Desc").unwrap_or_default().chars().take(MAX_NAME_CHARS).collect();
|
||||
let name: String = s.str(id, b"Desc").unwrap_or_default().chars().take(MAX_NAME_CHARS.min(self.budget.name_chars)).collect();
|
||||
self.budget.name_chars -= name.chars().count();
|
||||
let visible = s.bool(id, b"Visi").unwrap_or(true);
|
||||
let locked = s.bool(id, b"Edtb").is_some_and(|e| !e);
|
||||
let opacity = s.f64(id, b"Opac").map_or(1.0, |o| o.clamp(0.0, 1.0));
|
||||
@@ -312,6 +337,7 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
|
||||
self.warn(format!("a {} that could not be read", describe(class)));
|
||||
return Ok(Kind::Unsupported);
|
||||
};
|
||||
self.budget.spend(local.subpaths.iter().map(|sp| sp.segments.len() + 1).sum())?;
|
||||
let (fills, strokes) = paint::node_paint(self, id, world);
|
||||
Kind::Shape { path: local.transformed(world), fills, strokes, even_odd }
|
||||
}
|
||||
@@ -645,3 +671,15 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
|
||||
Kind::Artboard { rect, path: path.transformed(world), even_odd, background, strokes }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod budget_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn shared_work_is_bounded() {
|
||||
let mut b = Budget::FULL;
|
||||
assert!(b.spend(MAX_DOCUMENT_WORK).is_ok());
|
||||
assert_eq!(b.spend(1), Err(Error::Limit("too much shared geometry or page data")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,12 +238,13 @@ pub(crate) fn descriptor(r: &mut Reader, id: ObjId, local: Affine) -> Option<Pai
|
||||
};
|
||||
let grad = s.obj(fill, b"Grad")?;
|
||||
let cols = s.objs(grad, b"Cols");
|
||||
let posn: Vec<Vec<f64>> = match s.field(grad, b"Posn") {
|
||||
Some(Value::Array(a)) => a.iter().filter_map(floats).collect(),
|
||||
_ => Vec::new(),
|
||||
// Convert only the positions that pair with a colour, as shared attributes repeat this per run.
|
||||
let posn: &[Value] = match s.field(grad, b"Posn") {
|
||||
Some(Value::Array(a)) => a,
|
||||
_ => &[],
|
||||
};
|
||||
let mut stops = Vec::new();
|
||||
for (c, p) in cols.iter().zip(&posn).take(MAX_STOPS) {
|
||||
for (c, p) in cols.iter().zip(posn.iter().filter_map(floats)).take(MAX_STOPS) {
|
||||
let color = color(r, *c)?;
|
||||
let offset = p.first().copied().filter(|v| v.is_finite()).unwrap_or(0.0).clamp(0.0, 1.0);
|
||||
let bias = p.get(1).copied().filter(|v| v.is_finite()).unwrap_or(0.5).clamp(0.0, 1.0);
|
||||
|
||||
@@ -195,6 +195,11 @@ fn decode_within(r: &mut Reader, bitmap: ObjId, crop: (u32, u32, u32, u32), worl
|
||||
if let Some(name) = s.entry(bitmap, b"Bckg") {
|
||||
let width = s.int(bitmap, b"BmpW").and_then(|v| u32::try_from(v).ok()).unwrap_or(0);
|
||||
let height = s.int(bitmap, b"BmpH").and_then(|v| u32::try_from(v).ok()).unwrap_or(0);
|
||||
// The whole source is decoded for every layer that reads it, so charge it all, not only the crop.
|
||||
let Some(left) = r.budget.pixels.checked_sub(u64::from(width) * u64::from(height)) else {
|
||||
return Err(Error::Limit("embedded image pixels"));
|
||||
};
|
||||
r.budget.pixels = left;
|
||||
match original(r, name).and_then(|bytes| source_pixels(&bytes, width, height)) {
|
||||
Ok(pixels) => Some((width as usize, pixels)),
|
||||
Err(Error::Limit(e)) => return Err(Error::Limit(e)),
|
||||
@@ -318,8 +323,10 @@ fn grid(s: &stream::Stream, bitmap: ObjId, c: usize, bps: usize) -> (usize, usiz
|
||||
let height = s.int(bitmap, b"BmpH").and_then(|v| usize::try_from(v).ok()).unwrap_or(0);
|
||||
let tw = s.int(bitmap, &tag(b"TWi", c)).and_then(|v| usize::try_from(v).ok()).unwrap_or_else(|| width.saturating_mul(bps).div_ceil(TILE));
|
||||
let th = s.int(bitmap, &tag(b"THi", c)).and_then(|v| usize::try_from(v).ok()).unwrap_or_else(|| height.div_ceil(TILE));
|
||||
// A grid larger than the bitmap has tiles no crop can reach: don't convert or scan them on every reference.
|
||||
let tiles = width.saturating_mul(bps).div_ceil(TILE).saturating_mul(height.div_ceil(TILE));
|
||||
let states: Vec<u8> = match s.field(bitmap, &tag(b"Sta", c)) {
|
||||
Some(Value::Array(a)) => a.iter().map(|v| if let Value::UInt(u) = v { u8::try_from(*u).unwrap_or(255) } else { 255 }).collect(),
|
||||
Some(Value::Array(a)) => a.iter().take(tiles).map(|v| if let Value::UInt(u) = v { u8::try_from(*u).unwrap_or(255) } else { 255 }).collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
(tw, th, states)
|
||||
|
||||
@@ -51,6 +51,9 @@ const MAX_TABLE_LINES: usize = 4096;
|
||||
/// Cells a table may have (rows × columns): each becomes a text object with its edge lines.
|
||||
const MAX_TABLE_CELLS: usize = 1 << 16;
|
||||
|
||||
/// Work charged for each copy of a run's formatting a table cell keeps.
|
||||
const TABLE_RUN_COST: usize = 16;
|
||||
|
||||
/// A table node (`TxtT`): the grid, the cells' text and the lines on the cell edges.
|
||||
pub(crate) fn table(r: &mut Reader, id: ObjId, world: Affine) -> Option<Table> {
|
||||
let s = r.s;
|
||||
@@ -108,9 +111,10 @@ pub(crate) fn table(r: &mut Reader, id: ObjId, world: Affine) -> Option<Table> {
|
||||
let mut parts = run.text.split(CELL_BREAK).peekable();
|
||||
while let Some(part) = parts.next() {
|
||||
if !part.is_empty() {
|
||||
// Each cell keeps its own copy of the run's formatting, so each copy is charged.
|
||||
match target.and_then(|i| cells.get_mut(i)) {
|
||||
Some(cell) => cell.runs.push(TextRun { text: part.to_string(), ..run.clone() }),
|
||||
None => extra = true,
|
||||
Some(cell) if r.budget.spend(TABLE_RUN_COST).is_ok() => cell.runs.push(TextRun { text: part.to_string(), ..run.clone() }),
|
||||
_ => extra = true,
|
||||
}
|
||||
}
|
||||
if parts.peek().is_some() {
|
||||
@@ -157,6 +161,13 @@ pub(crate) fn story_runs(r: &mut Reader, story: ObjId, world: Affine) -> (Vec<Te
|
||||
let mut placeholders = false;
|
||||
let mut attribute_budget = Some(MAX_ATTRIBUTE_BYTES);
|
||||
for block in s.objs(story, b"Blok") {
|
||||
// Shared blocks and run lists are visited once per link, so every visit is charged, empty or not.
|
||||
let paragraph_runs = s.obj(block, b"PAtt").map(|p| s.objs(p, b"Runs")).unwrap_or_default();
|
||||
let attribute_runs = s.obj(block, b"GAtt").map(|g| s.objs(g, b"Runs")).unwrap_or_default();
|
||||
if r.budget.spend(1 + paragraph_runs.len() + attribute_runs.len()).is_err() {
|
||||
r.warn("text beyond the work one document may hold (truncated)");
|
||||
break;
|
||||
}
|
||||
let Some(glyphs) = s.obj(block, b"Glyp") else {
|
||||
r.warn("a text block without readable characters (left out)");
|
||||
continue;
|
||||
@@ -209,7 +220,7 @@ pub(crate) fn story_runs(r: &mut Reader, story: ObjId, world: Affine) -> (Vec<Te
|
||||
// Paragraph formats by end index; a run without an index covers the rest of the block.
|
||||
let mut paragraphs: Vec<(usize, Paragraph)> = Vec::new();
|
||||
let mut open: Option<Paragraph> = None;
|
||||
for run in s.obj(block, b"PAtt").map(|p| s.objs(p, b"Runs")).unwrap_or_default() {
|
||||
for run in paragraph_runs {
|
||||
let Some(attrs) = s.obj(run, b"Item") else { continue };
|
||||
let a = paragraph(r, attrs);
|
||||
first_align.get_or_insert(a.align);
|
||||
@@ -238,7 +249,7 @@ pub(crate) fn story_runs(r: &mut Reader, story: ObjId, world: Affine) -> (Vec<Te
|
||||
}
|
||||
};
|
||||
let mut start = 0usize;
|
||||
for run in s.obj(block, b"GAtt").map(|g| s.objs(g, b"Runs")).unwrap_or_default() {
|
||||
for run in attribute_runs {
|
||||
let Some(index) = s.int(run, b"Indx").and_then(|v| usize::try_from(v).ok()) else {
|
||||
r.warn("invalid text attribute range (bounded; unassigned characters use defaults)");
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user