diff --git a/src/scene/model/underlay_vector/dgn7.rs b/src/scene/model/underlay_vector/dgn7.rs index 1a626eec7..f9be987e6 100644 --- a/src/scene/model/underlay_vector/dgn7.rs +++ b/src/scene/model/underlay_vector/dgn7.rs @@ -172,11 +172,14 @@ struct Instance { origin: [f64; 2], } +/// How deep cells inside cells are read. +const MAX_CELL_DEPTH: usize = 16; + /// Reads a run of elements into raw geometry: plain elements, complex /// chains and shapes (joined), B-splines, text, cells and shared cell /// instances. In a 3D file (`d3`) points have a z and planar elements their /// plane; the drawn geometry is their plan. -fn read_run(run: &[&[u8]], colors: &[[u8; 3]], d3: bool) -> Raw { +fn read_run(run: &[&[u8]], colors: &[[u8; 3]], d3: bool, depth: usize) -> Raw { let mut raw = Raw::default(); // An open B-spline curve: order, pole count, colour, closed, and the // header as the element it imports as. @@ -280,14 +283,21 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]], d3: bool) -> Raw { let plan = |p: [f64; 3]| [p[0], p[1]]; let step = if d3 { 12 } else { 8 }; match kind { - // A cell: its components follow as complex elements; they are - // already placed, so the cell keeps them as they are. + // A cell: its components follow as complex elements, as many as + // its total length (36, the words after its first 38 bytes) + // covers; they are already placed, so the cell keeps them as + // they are. Nesting past `MAX_CELL_DEPTH` is left out. 2 => { + let mut left = (38 + uint16(e, 36).unwrap_or(0) as usize * 2).saturating_sub(e.len()); let start = index; - while index < run.len() && run[index][0] & 0x80 != 0 { + while left > 0 && index < run.len() && run[index][0] & 0x80 != 0 { + left = left.saturating_sub(run[index].len()); index += 1; } - let inner = read_run(&run[start..index], colors, d3); + if depth >= MAX_CELL_DEPTH { + continue; + } + let inner = read_run(&run[start..index], colors, d3, depth + 1); raw.paths.extend(inner.paths); raw.texts.extend(inner.texts); raw.instances.extend(inner.instances); @@ -836,14 +846,14 @@ pub fn model(bytes: &[u8], hidden: &[String]) -> Option { while j < all.len() && all[j][0] & 0x80 != 0 { j += 1; } - defs.insert(name, read_run(&all[i + 1..j], &colors, is_3d(bytes))); + defs.insert(name, read_run(&all[i + 1..j], &colors, is_3d(bytes), 0)); i = j; continue; } top.push(e); i += 1; } - let raw = read_run(&top, &colors, is_3d(bytes)); + let raw = read_run(&top, &colors, is_3d(bytes), 0); let per_unit = u.uor_per_sub * u.sub_per_master; let m: Affine = [ 1.0 / per_unit, @@ -910,7 +920,7 @@ pub fn import(bytes: &[u8]) -> Option { while j < all.len() && all[j][0] & 0x80 != 0 { j += 1; } - let def = read_run(&all[i + 1..j], &colors, is_3d(bytes)).elements; + let def = read_run(&all[i + 1..j], &colors, is_3d(bytes), 0).elements; cells.push((name, scaled_elements(&def, s, [0.0, 0.0]))); i = j; continue; @@ -918,7 +928,7 @@ pub fn import(bytes: &[u8]) -> Option { top.push(e); i += 1; } - let raw = read_run(&top, &colors, is_3d(bytes)); + let raw = read_run(&top, &colors, is_3d(bytes), 0); let levels = (0..64u32) .map(|l| { let name = names.get(&(l as u8)).cloned().unwrap_or_else(|| l.to_string()); diff --git a/src/scene/model/underlay_vector/dgn8.rs b/src/scene/model/underlay_vector/dgn8.rs index 29ecd4de8..ca2f5f63b 100644 --- a/src/scene/model/underlay_vector/dgn8.rs +++ b/src/scene/model/underlay_vector/dgn8.rs @@ -209,16 +209,22 @@ impl<'a> Cfb<'a> { } } +/// Far above any real element stream; stops a crafted one from inflating +/// until memory runs out. +const STREAM_LIMIT: u64 = 512 << 20; + /// A DGN stream: raw, or zlib data after a 16-byte header. fn inflate(data: &[u8]) -> Vec { - // Far above any real element stream; stops a crafted one from - // inflating until memory runs out. - const STREAM_LIMIT: u64 = 512 << 20; + inflate_at_most(data, STREAM_LIMIT) +} + +/// [`inflate`] stopping after `limit` bytes. +fn inflate_at_most(data: &[u8], limit: u64) -> Vec { for skip in [16usize, 0] { if let Some(z) = data.get(skip..) { let mut out = Vec::new(); if flate2::read::ZlibDecoder::new(z) - .take(STREAM_LIMIT) + .take(limit) .read_to_end(&mut out) .is_ok() && !out.is_empty() @@ -997,7 +1003,10 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw { 27 => { // B-spline curve header: order and closure in the word at // 108, pole count 112; its poles follow (type 21). - let flags = u16::from_le_bytes([e[108], e[109]]); + let Some(&[lo, hi]) = e.get(108..110) else { + continue; + }; + let flags = u16::from_le_bytes([lo, hi]); spline = Some(( ((flags & 0x0f) + 2) as usize, u32_at(e, 112).unwrap_or(0) as usize, @@ -1149,6 +1158,11 @@ fn units(header: &[u8]) -> (f64, f64) { (master, sub) } +/// How many bytes the streams of one file may inflate to in all. +fn inflate_budget(cfb: &Cfb) -> usize { + cfb.b.len().saturating_mul(64).max(256 << 20) +} + /// Models of the file: (name, graphic stream, header stream). fn models(cfb: &Cfb) -> Vec<(String, Vec, Vec)> { let streams = cfb.streams(); @@ -1163,7 +1177,7 @@ fn models(cfb: &Cfb) -> Vec<(String, Vec, Vec)> { // scales with the file (×64 covers any real compression ratio, so // legitimate files never trip it) with a 256 MiB floor for small ones; // models past the budget are dropped, the earlier ones still load. - let total_limit = cfb.b.len().saturating_mul(64).max(256 << 20); + let total_limit = inflate_budget(cfb); let mut total = 0usize; let mut out = Vec::new(); for dir in dirs { @@ -1400,24 +1414,28 @@ pub fn design_model_names(bytes: &[u8]) -> Option> { (!names.is_empty()).then_some(names) } -/// The control stream (Dgn^C) of each model, in the order `models` lists -/// them. -fn control_streams(cfb: &Cfb) -> Vec> { +/// The control stream (Dgn^C) of model `k` in the order `models` lists +/// them, inflated within what is left of the file's budget (`used` bytes +/// are taken by the models already read). +fn control_stream(cfb: &Cfb, k: usize, used: usize) -> Vec { let streams = cfb.streams(); - let mut dirs: Vec = streams + let mut dirs: Vec<&str> = streams .iter() - .filter_map(|(p, _)| p.strip_suffix("/Dgn^G/$1").map(|d| d.to_string())) + .filter_map(|(p, _)| p.strip_suffix("/Dgn^G/$1")) .collect(); dirs.sort(); - dirs.iter() - .map(|dir| { - streams - .iter() - .find(|(p, _)| *p == format!("{dir}/Dgn^C/$1")) - .map(|(_, e)| inflate(&cfb.stream(e))) - .unwrap_or_default() - }) - .collect() + let Some(dir) = dirs.get(k) else { + return Vec::new(); + }; + let left = inflate_budget(cfb).saturating_sub(used) as u64; + if left == 0 { + return Vec::new(); + } + streams + .iter() + .find(|(p, _)| *p == format!("{dir}/Dgn^C/$1")) + .map(|(_, e)| inflate_at_most(&cfb.stream(e), left.min(STREAM_LIMIT))) + .unwrap_or_default() } /// Every string of an element stored after the "ff fe 01 00" marker, with @@ -1476,7 +1494,6 @@ fn references(control: &[u8]) -> Vec { pub fn import(bytes: &[u8], name: &str) -> Option { let cfb = Cfb::open(bytes)?; let all = models(&cfb); - let controls = control_streams(&cfb); let designs: Vec = (0..all.len()).filter(|k| is_design(&all[*k].2)).collect(); let k = designs .iter() @@ -1536,10 +1553,8 @@ pub fn import(bytes: &[u8], name: &str) -> Option { (Some(n), Some(d)) if n > 0.0 && d > 0.0 => unit_name(d / n), _ => String::new(), }; - let references = controls - .get(k) - .map(|c| references(c)) - .unwrap_or_default() + let used = all.iter().map(|m| m.1.len() + m.2.len()).sum(); + let references = references(&control_stream(&cfb, k, used)) .into_iter() .map(|r| DgnReference { origin: [