//! DGN V8 design files: an OLE compound file whose models (Dgn-Md/#n) //! keep their graphic elements in a compressed stream (Dgn^G/$1), and whose //! non-model stream (Dgn^Nm/$1) holds the colour table, the levels and the //! shared cell definitions. Element fields are little-endian; coordinates //! are UORs. use std::collections::HashMap; use std::io::Read; use super::model::{ arc_cubics, dgn_color_table, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath, Text, DGN_DEFAULT_COLORS, }; // ── Compound file ─────────────────────────────────────────────────────────── struct Cfb<'a> { b: &'a [u8], sector: usize, fat: Vec, mini_fat: Vec, mini: Vec, cutoff: u32, entries: Vec, } struct Entry { name: String, kind: u8, left: u32, right: u32, child: u32, start: u32, size: u32, } const END: u32 = 0xFFFF_FFFA; fn u32_at(b: &[u8], at: usize) -> Option { Some(u32::from_le_bytes(b.get(at..at + 4)?.try_into().ok()?)) } impl<'a> Cfb<'a> { fn open(b: &'a [u8]) -> Option { // The 512-byte header, with the only two sector sizes the format // has (512 and 4096 bytes); anything else is not a compound file. if b.len() < 512 || !b.starts_with(&[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]) { return None; } let sector_shift = u16::from_le_bytes([b[30], b[31]]); if sector_shift != 9 && sector_shift != 12 { return None; } let sector = 1usize << sector_shift; let mini_sector_shift = u16::from_le_bytes([b[32], b[33]]); if mini_sector_shift != 6 { return None; } let mut difat: Vec = (0..109) .filter_map(|i| u32_at(b, 76 + i * 4)) .filter(|&v| v < END) .collect(); // The DIFAT chain cannot hold more sectors than the file does, so a // looping chain stops there. let (mut next, mut count) = ( u32_at(b, 68)?, u32_at(b, 72)?.min((b.len() / sector) as u32), ); while count > 0 && next < END { let at = (next as usize + 1) * sector; for i in 0..sector / 4 - 1 { if let Some(v) = u32_at(b, at + i * 4).filter(|&v| v < END) { difat.push(v); } } next = u32_at(b, at + sector - 4)?; count -= 1; } let mut fat = Vec::new(); for s in difat { let at = (s as usize + 1) * sector; for i in 0..sector / 4 { fat.push(u32_at(b, at + i * 4).unwrap_or(END)); } } let mut cfb = Cfb { b, sector, fat, mini_fat: Vec::new(), mini: Vec::new(), cutoff: u32_at(b, 56)?, entries: Vec::new(), }; let dir = cfb.chain(u32_at(b, 48)?, None); for e in dir.chunks_exact(128) { let len = u16::from_le_bytes([e[64], e[65]]) as usize; let name16: Vec = e[..len.saturating_sub(2).min(64)] .chunks_exact(2) .map(|c| u16::from_le_bytes([c[0], c[1]])) .collect(); cfb.entries.push(Entry { name: String::from_utf16_lossy(&name16), kind: e[66], left: u32_at(e, 68)?, right: u32_at(e, 72)?, child: u32_at(e, 76)?, start: u32_at(e, 116)?, size: u32_at(e, 120)?, }); } let root = cfb.entries.first()?; let (root_start, root_size) = (root.start, root.size); cfb.mini = cfb.chain(root_start, Some(root_size as usize)); let mini_fat = cfb.chain(u32_at(b, 60)?, None); cfb.mini_fat = mini_fat .chunks_exact(4) .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]])) .collect(); Some(cfb) } fn chain(&self, mut s: u32, size: Option) -> Vec { // A FAT entry can point at itself (`fat[s] = s`): the old fixed // 1_000_000-iteration guard still let every call accumulate // `1e6 × sector` bytes (4.1 GB at sector_shift = 12) — directory, // mini stream and miniFAT chains are alive at once (~12 GB peak → // allocation-failure abort). A chain can only read in-file sectors, // and after that many steps it must have repeated one; the walk is // deterministic, so a repeat means a cycle that yields nothing new. // Bounding by the file's own sector count never truncates a // legitimate chain (each sector read at most once). let max_sectors = self.b.len() / self.sector; let mut out = Vec::new(); let mut guard = 0; while s < END && guard < max_sectors { let at = (s as usize + 1) * self.sector; let Some(block) = self.b.get(at..at + self.sector) else { break; }; out.extend_from_slice(block); s = self.fat.get(s as usize).copied().unwrap_or(END); guard += 1; } if let Some(n) = size { out.truncate(n); } out } fn stream(&self, e: &Entry) -> Vec { if e.size >= self.cutoff { return self.chain(e.start, Some(e.size as usize)); } let mut out = Vec::new(); let mut s = e.start; let mut guard = 0; // Same cycle argument as `chain`, over the mini sectors: a loop in // the miniFAT used to spin the full 1_000_000-iteration budget // (64 MB per call) before `truncate` shortened the length. let max_sectors = self.mini.len() / 64; while s < END && guard < max_sectors { let at = s as usize * 64; let Some(block) = self.mini.get(at..at + 64) else { break; }; out.extend_from_slice(block); s = self.mini_fat.get(s as usize).copied().unwrap_or(END); guard += 1; } out.truncate(e.size as usize); out } /// Every stream with its full path ("Dgn-Md/#000000/Dgn^G/$1"). fn streams(&self) -> Vec<(String, &Entry)> { let mut out = Vec::new(); fn walk<'b>( c: &'b Cfb, idx: u32, path: &str, out: &mut Vec<(String, &'b Entry)>, depth: usize, ) { let Some(e) = c.entries.get(idx as usize) else { return; }; if depth > 64 { return; } walk(c, e.left, path, out, depth + 1); let p = if path.is_empty() { e.name.clone() } else { format!("{path}/{}", e.name) }; if e.kind == 2 { out.push((p.clone(), e)); } else if e.kind == 1 { walk(c, e.child, &p, out, depth + 1); } walk(c, e.right, path, out, depth + 1); } if let Some(root) = self.entries.first() { walk(self, root.child, "", &mut out, 0); } out } } /// 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; 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) .read_to_end(&mut out) .is_ok() && !out.is_empty() { return out; } } } data.to_vec() } // ── Elements ──────────────────────────────────────────────────────────────── /// Element records of an element stream (after its 4-byte count). fn elements(d: &[u8]) -> Vec<&[u8]> { let mut out = Vec::new(); let mut at = 4; while at + 8 <= d.len() { let Some(words) = u32_at(d, at + 4) else { break; }; let len = 4 + words as usize * 2; if len < 8 { break; } // The stream can end inside its last element (its trailing words are // not stored); what is there is still read. let e = &d[at..(at + len).min(d.len())]; if !is_deleted(e) { out.push(e); } at += len; } out } fn kind(e: &[u8]) -> u8 { e[0] } fn flags(e: &[u8]) -> u16 { u16::from_le_bytes([e[2], e[3]]) } /// A deleted element (flag 0x80 of its second byte), kept in the stream but /// no longer part of the file. fn is_deleted(e: &[u8]) -> bool { e[1] & 0x80 != 0 } /// A component of a complex element, cell or shared cell definition. fn is_component(e: &[u8]) -> bool { flags(e) & 0x4000 != 0 } fn f64_at(e: &[u8], at: usize) -> Option { Some(f64::from_le_bytes(e.get(at..at + 8)?.try_into().ok()?)) } /// A string stored after the "ff fe 01 00" marker, when the element has one. fn marked_string(e: &[u8], from: usize) -> Option { let i = e .get(from..)? .windows(4) .position(|w| w == [0xff, 0xfe, 0x01, 0x00])? + from + 4; let end = e[i..] .iter() .position(|&c| c == 0 || c < 0x20) .map(|n| i + n) .unwrap_or(e.len()); Some(String::from_utf8_lossy(&e[i..end]).to_string()) } struct Styles { colors: Vec<[u8; 3]>, /// Level id → colour index. levels: HashMap, /// Levels the underlay turns off: their elements are left out. hidden: std::collections::HashSet, /// True colours, in the order colour words number them (1 first). extended: Vec<[u8; 3]>, } /// The true colours of the file: a zlib-packed UTF-16 record in the /// non-model attributes stream reading /// `…`. fn extended_colors(cfb: &Cfb) -> Vec<[u8; 3]> { let Some(attrs) = cfb .streams() .iter() .find(|(p, _)| p == "Dgn^NmA/$1") .map(|(_, e)| inflate(&cfb.stream(e))) else { return Vec::new(); }; for at in 0..attrs.len().saturating_sub(2) { // A zlib header: deflate, and a check value divisible by 31. let (a, b) = (attrs[at], attrs[at + 1]); if a & 0x0f != 8 || ((a as u16) << 8 | b as u16) % 31 != 0 { continue; } // The record is a short XML list; the cap keeps a stream that // inflates without end (or a crafted one) from exhausting memory. const RECORD_LIMIT: u64 = 4 << 20; let mut out = Vec::new(); if flate2::read::ZlibDecoder::new(&attrs[at..]) .take(RECORD_LIMIT) .read_to_end(&mut out) .is_err() || out.len() < 2 { continue; } let units: Vec = out .chunks_exact(2) .map(|c| u16::from_le_bytes([c[0], c[1]])) .collect(); let text = String::from_utf16_lossy(&units); if !text.contains(" = rest .split(')') .next()? .split(',') .filter_map(|n| n.trim().parse().ok()) .collect(); (v.len() == 3).then(|| [v[0], v[1], v[2]]) }) .collect(); } Vec::new() } impl Styles { fn color(&self, e: &[u8]) -> [u8; 3] { // The colour word: an index into the colour table, 0xFFFFFFFF for // the level's colour, or true colour n (from 1) × 256 plus the index // of its nearest table colour, drawn in its own RGB when the file // lists it. let explicit = u32_at(e, 52).unwrap_or(u32::MAX); let index = if explicit == u32::MAX { self.levels .get(&u32_at(e, 12).unwrap_or(0)) .copied() .unwrap_or(0) } else { if let Some(rgb) = (explicit >> 8) .checked_sub(1) .and_then(|n| self.extended.get(n as usize)) { return *rgb; } explicit & 0xff }; self.colors .get(index as usize) .copied() .unwrap_or([255, 255, 255]) } } /// Plan geometry in UORs, and the shared cells it places. #[derive(Default, Clone)] struct Raw { paths: Vec, texts: Vec, instances: Vec, } #[derive(Clone)] struct Instance { name: String, /// The definition's placement, with every enclosing one applied. xf: Xf, } /// A placement in UORs: x' = r[0]·(x, y, z, 1), y' = r[1]·…, z' = r[2]·…. type Xf = [[f64; 4]; 3]; const IDENTITY: Xf = [ [1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], ]; fn xf_vector(m: &Xf, v: [f64; 3]) -> [f64; 3] { m.map(|r| r[0] * v[0] + r[1] * v[1] + r[2] * v[2]) } fn xf_point(m: &Xf, p: [f64; 3]) -> [f64; 3] { m.map(|r| r[0] * p[0] + r[1] * p[1] + r[2] * p[2] + r[3]) } fn xf_compose(outer: &Xf, inner: &Xf) -> Xf { let mut out = [[0.0; 4]; 3]; for (i, row) in out.iter_mut().enumerate() { for (j, v) in row.iter_mut().enumerate() { *v = (0..3).map(|k| outer[i][k] * inner[k][j]).sum::() + if j == 3 { outer[i][3] } else { 0.0 }; } } out } /// A 3D element (flag 0x0800 of its properties word): three coordinates a /// point, orientations as quaternions. Its plan view is drawn. fn is_3d(e: &[u8]) -> bool { u32_at(e, 40).is_some_and(|v| v & 0x0800 != 0) } /// The in-plane x and y axes of a 3D element's orientation quaternion w, x, /// y, z stored at `at` (the rows of its rotation matrix). fn quat_axes(e: &[u8], at: usize) -> Option<([f64; 3], [f64; 3])> { let (w, x, y, z) = ( f64_at(e, at)?, f64_at(e, at + 8)?, f64_at(e, at + 16)?, f64_at(e, at + 24)?, ); Some(( [ 1.0 - 2.0 * (y * y + z * z), 2.0 * (x * y - w * z), 2.0 * (x * z + w * y), ], [ 2.0 * (x * y + w * z), 1.0 - 2.0 * (x * x + z * z), 2.0 * (y * z - w * x), ], )) } /// The in-plane axes of a 2D element turned by `rotation` (radians). fn turned_axes(rotation: f64) -> ([f64; 3], [f64; 3]) { let (c, s) = (rotation.cos(), rotation.sin()); ([c, s, 0.0], [-s, c, 0.0]) } /// An elliptical arc on axes `u` and `v` (plan projections) as cubics. fn projected_arc( c: [f64; 2], a: f64, b: f64, u: [f64; 2], v: [f64; 2], start: f64, sweep: f64, ) -> Vec { let map = |p: [f64; 2]| { [ c[0] + a * p[0] * u[0] + b * p[1] * v[0], c[1] + a * p[0] * u[1] + b * p[1] * v[1], ] }; arc_cubics([0.0, 0.0], 1.0, 1.0, 0.0, start, sweep) .into_iter() .map(|[p0, p1, p2, p3]| Segment::Cubic(map(p0), map(p1), map(p2), map(p3))) .collect() } /// Reads a run of elements placed through `xf` into plan geometry. fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw { let mut raw = Raw::default(); // A complex shape or chain collects its components into one path. let mut complex: Option<( usize, PathBuilder, Option<([u8; 3], f64)>, Option<[u8; 3]>, bool, )> = None; fn push( raw: &mut Raw, complex: &mut Option<( usize, PathBuilder, Option<([u8; 3], f64)>, Option<[u8; 3]>, bool, )>, path: Path, ) { if let Some((left, pb, _, _, _)) = complex.as_mut() { for sp in &path.subpaths { for seg in &sp.segments { let (a, b) = match seg { Segment::Line(a, b) | Segment::Cubic(a, _, _, b) => (*a, *b), }; if pb.current() != Some(a) { if pb.current().is_none() { pb.move_to(a) } else { pb.line_to(a) } } match seg { Segment::Line(..) => pb.line_to(b), Segment::Cubic(_, c1, c2, _) => pb.cubic_to(*c1, *c2, b), } } } *left = left.saturating_sub(1); if *left == 0 { let (_, mut pb, stroke, fill, closed) = complex.take().unwrap(); if closed { pb.close(); } raw.paths.push(Path { subpaths: pb.finish(), stroke, fill, }); } } else { raw.paths.push(path); } } let plan = |p: [f64; 3]| { let q = xf_point(xf, p); [q[0], q[1]] }; let axis = |v: [f64; 3]| { let q = xf_vector(xf, v); [q[0], q[1]] }; for e in run { if e.len() < 104 || styles.hidden.contains(&u32_at(e, 12).unwrap_or(0)) { continue; } let color = styles.color(e); let stroke = Some((color, -1.0)); let d3 = is_3d(e); // A point: x, y (and z in a 3D element). let p3 = |at: usize| -> Option<[f64; 3]> { Some([ f64_at(e, at)?, f64_at(e, at + 8)?, if d3 { f64_at(e, at + 16)? } else { 0.0 }, ]) }; match kind(e) { 12 | 14 => { let n = u32_at(e, 104).unwrap_or(0) as usize; if n > 0 { let fill = (kind(e) == 14 && e.windows(4).any(|w| w == [0x07, 0x10, 0x41, 0x00])) .then_some(color); complex = Some((n, PathBuilder::default(), stroke, fill, kind(e) == 14)); } } 3 => { if let (Some(a), Some(b)) = (p3(104), p3(if d3 { 128 } else { 120 })) { push( &mut raw, &mut complex, Path { subpaths: vec![SubPath { segments: vec![Segment::Line(plan(a), plan(b))], closed: false, }], stroke, fill: None, }, ); } } 4 | 6 | 11 => { let n = u32_at(e, 104).unwrap_or(0) as usize; let stride = if d3 { 24 } else { 16 }; let pts: Vec<[f64; 2]> = (0..n) .filter_map(|k| p3(112 + k * stride)) .map(plan) .collect(); if pts.len() >= 2 { let mut pb = PathBuilder::default(); pb.move_to(pts[0]); for p in &pts[1..] { pb.line_to(*p); } if kind(e) == 6 { pb.close(); } push( &mut raw, &mut complex, Path { subpaths: pb.finish(), stroke, fill: None, }, ); } } 15 | 16 => { // Radii, orientation and centre. 3D: ellipse a 104, b 112, // quaternion 120, centre 152; arc start 104, sweep 112, a 120, // b 128, quaternion 136, centre 168. 2D: the orientation is an // angle, ellipse centre 128, arc centre 144. let arc = match (kind(e), d3) { (15, true) => (|| { Some(( f64_at(e, 104)?, f64_at(e, 112)?, quat_axes(e, 120)?, p3(152)?, 0.0, 0.0, )) })(), (_, true) => (|| { Some(( f64_at(e, 120)?, f64_at(e, 128)?, quat_axes(e, 136)?, p3(168)?, f64_at(e, 104)?, f64_at(e, 112)?, )) })(), (15, false) => (|| { Some(( f64_at(e, 104)?, f64_at(e, 112)?, turned_axes(f64_at(e, 120)?), p3(128)?, 0.0, 0.0, )) })(), _ => (|| { Some(( f64_at(e, 120)?, f64_at(e, 128)?, turned_axes(f64_at(e, 136)?), p3(144)?, f64_at(e, 104)?, f64_at(e, 112)?, )) })(), }; let Some((a, b, (u, v), c, start, sweep)) = arc else { continue; }; let sweep = if sweep == 0.0 { std::f64::consts::TAU } else { sweep }; let closed = sweep.abs() >= std::f64::consts::TAU - 1e-9; push( &mut raw, &mut complex, Path { subpaths: vec![SubPath { segments: projected_arc(plan(c), a, b, axis(u), axis(v), start, sweep), closed, }], stroke, fill: None, }, ); } 17 => { // 3D: width 112, height 120, quaternion 144, origin 176, the // characters after the string marker. 2D: angle 144, origin // 152, a byte count (with its 4-byte marker) at 110 and the // characters from 174. let placed = if d3 { (|| { Some(( f64_at(e, 112)?, f64_at(e, 120)?, quat_axes(e, 144)?, p3(176)?, marked_string(e, 200)?, )) })() } else { (|| { let count = u16::from_le_bytes(e.get(110..112)?.try_into().ok()?) as usize; let chars = e.get(174..170 + count.max(4))?; let text = String::from_utf8_lossy(chars) .trim_end_matches(char::from(0)) .to_string(); Some(( f64_at(e, 112)?, f64_at(e, 120)?, turned_axes(f64_at(e, 144)?), p3(152)?, text, )) })() }; let Some((width, height, (u, v), origin, text)) = placed else { continue; }; // The text's axes as the plan shows them: its baseline sets the // angle, and each axis's plan length scales its size. let (pu, pv) = (axis(u), axis(v)); let (su, sv) = (pu[0].hypot(pu[1]), pv[0].hypot(pv[1])); if su < 1e-9 || sv < 1e-9 { continue; } let (height, width) = (height * 6.0 / 1000.0 * sv, width * 6.0 / 1000.0 * su); raw.texts.push(Text { text, origin: plan(origin), height, width_factor: if height > 0.0 { width / height } else { 1.0 }, rotation: pu[1].atan2(pu[0]), color, font: "txt".to_string(), }); } 35 => { // Shared cell instance: a 3×3 matrix by rows from 160, the // origin at 232 (z at 248 in 3D), the definition's name after // the string marker. let m: Option> = (0..9).map(|k| f64_at(e, 160 + k * 8)).collect(); let (Some(m), Some(o)) = (m, p3(232)) else { continue; }; let Some(name) = marked_string(e, 248) else { continue; }; let local: Xf = [ [m[0], m[1], m[2], o[0]], [m[3], m[4], m[5], o[1]], [m[6], m[7], m[8], o[2]], ]; raw.instances.push(Instance { name: name.to_ascii_uppercase(), xf: xf_compose(xf, &local), }); } _ => {} } } // A complex element cut short keeps the components it has. if let Some((_, mut pb, stroke, fill, closed)) = complex.take() { if closed { pb.close(); } raw.paths.push(Path { subpaths: pb.finish(), stroke, fill, }); } raw } type Affine = [f64; 6]; fn apply(m: &Affine, p: [f64; 2]) -> [f64; 2] { [ m[0] * p[0] + m[1] * p[1] + m[4], m[2] * p[0] + m[3] * p[1] + m[5], ] } /// Places plan geometry through the sheet mapping `m`, and the shared cells /// it instances (each definition read through its own placement). fn place( raw: &Raw, m: &Affine, defs: &HashMap>, styles: &Styles, depth: usize, out: &mut Sheet, ) { let map = |p: [f64; 2]| apply(m, p); for path in &raw.paths { out.paths.push(Path { subpaths: path .subpaths .iter() .map(|sp| SubPath { segments: sp .segments .iter() .map(|s| match s { Segment::Line(a, b) => Segment::Line(map(*a), map(*b)), Segment::Cubic(a, b, c, d) => { Segment::Cubic(map(*a), map(*b), map(*c), map(*d)) } }) .collect(), closed: sp.closed, }) .collect(), stroke: path.stroke, fill: path.fill, }); } let scale = (m[0] * m[3] - m[1] * m[2]).abs().sqrt(); let turn = m[2].atan2(m[0]); for t in &raw.texts { out.texts.push(Text { origin: map(t.origin), height: t.height * scale, rotation: t.rotation + turn, ..t.clone() }); } if depth >= 8 { return; } for inst in &raw.instances { let Some(def) = defs.get(&inst.name) else { continue; }; place( &read_run(def, styles, &inst.xf), m, defs, styles, depth + 1, out, ); } } /// Units of a model header: UORs per master and per sub unit. fn units(header: &[u8]) -> (f64, f64) { // Unit definitions are numerator/denominator pairs per metre: master // (4180/4188), sub (4196/4204), storage (4332/4340), and the UORs per // storage unit (4324). let get = |at: usize| f64_at(header, at).filter(|v| v.is_finite() && *v > 0.0); let (mn, md, sn, sd) = (get(4180), get(4188), get(4196), get(4204)); let (uor, stn, std) = (get(4324), get(4332), get(4340)); let uor_per_metre = match (uor, stn, std) { (Some(u), Some(n), Some(d)) => u * n / d, _ => 1_000_000.0, }; let master = match (mn, md) { (Some(n), Some(d)) => uor_per_metre * d / n, _ => uor_per_metre, }; let sub = match (sn, sd) { (Some(n), Some(d)) => uor_per_metre * d / n, _ => master / 1000.0, }; (master, sub) } /// Models of the file: (name, graphic stream, header stream). fn models(cfb: &Cfb) -> Vec<(String, Vec, Vec)> { let streams = cfb.streams(); let mut dirs: Vec = streams .iter() .filter_map(|(p, _)| p.strip_suffix("/Dgn^G/$1").map(|d| d.to_string())) .collect(); dirs.sort(); // Each stream may inflate to `STREAM_LIMIT` (512 MB) and every model // dir inflates its own — entries can all point at the same payload, so // N dirs × 512 MB was reachable from one small stream. The budget // 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 mut total = 0usize; let mut out = Vec::new(); for dir in dirs { if total >= total_limit { break; } let get = |suffix: &str| { streams .iter() .find(|(p, _)| *p == format!("{dir}/{suffix}")) .map(|(_, e)| inflate(&cfb.stream(e))) .unwrap_or_default() }; let graphics = get("Dgn^G/$1"); let header = get("Dgn~Mh"); total = total .saturating_add(graphics.len()) .saturating_add(header.len()); let name = marked_string(&header, 0).unwrap_or_else(|| "Default".to_string()); out.push((name, graphics, header)); } out } pub fn model_names(bytes: &[u8]) -> Option> { let cfb = Cfb::open(bytes)?; let names: Vec = models(&cfb).into_iter().map(|m| m.0).collect(); (!names.is_empty()).then_some(names) } /// The level table (the table entries after a header of table 1): id, /// name and colour index. The default level (64) is listed as "0", as the /// reference names it. fn level_table(nm: &[&[u8]]) -> Vec<(u32, String, u32)> { let mut out = Vec::new(); let mut in_levels = false; for e in nm { match kind(e) { 96 => in_levels = u32_at(e, 12) == Some(1), 95 if in_levels && e.len() >= 80 => { let (Some(id), Some(color)) = (u32_at(e, 32), u32_at(e, 72)) else { continue; }; let mut name = marked_string(e, 32).unwrap_or_default(); if id == 64 && name.eq_ignore_ascii_case("Default") { name = "0".to_string(); } out.push((id, name, color)); } _ => {} } } out } fn non_model(cfb: &Cfb) -> Vec { cfb.streams() .iter() .find(|(p, _)| p == "Dgn^Nm/$1") .map(|(_, e)| inflate(&cfb.stream(e))) .unwrap_or_default() } /// Level names of the file, for the underlay layers list. pub fn layer_names(bytes: &[u8]) -> Option> { let cfb = Cfb::open(bytes)?; let nm = non_model(&cfb); Some( level_table(&elements(&nm)) .into_iter() .map(|l| l.1) .filter(|n| !n.is_empty()) .collect(), ) } pub fn model(bytes: &[u8], name: &str, hidden: &[String]) -> Option { let cfb = Cfb::open(bytes)?; let all = models(&cfb); let (_, graphics, header) = all .iter() .find(|m| m.0.eq_ignore_ascii_case(name)) .or_else(|| all.first())?; let non_model = non_model(&cfb); let nm = elements(&non_model); // Colours 0-254 from byte 37, the background (255) opening the table. let color_table = nm .iter() .find(|e| kind(e) == 5) .and_then(|e| dgn_color_table(e, 37)); let table_colors = color_table.is_some(); let colors = color_table.unwrap_or_else(|| DGN_DEFAULT_COLORS.to_vec()); let table = level_table(&nm); let levels = table.iter().map(|l| (l.0, l.2)).collect(); let hidden = table .iter() .filter(|l| hidden.iter().any(|h| h.eq_ignore_ascii_case(&l.1))) .map(|l| l.0) .collect(); let styles = Styles { colors, levels, hidden, extended: extended_colors(&cfb), }; // Shared cell definitions: a type-34 element and its components. let mut defs: HashMap> = HashMap::new(); let mut i = 0; while i < nm.len() { if kind(nm[i]) == 34 && !is_component(nm[i]) { let name = marked_string(nm[i], 248) .unwrap_or_default() .to_ascii_uppercase(); let mut j = i + 1; while j < nm.len() && is_component(nm[j]) { j += 1; } defs.insert(name, nm[i + 1..j].to_vec()); i = j; } else { i += 1; } } let graphics = elements(graphics); let raw = read_run(&graphics, &styles, &IDENTITY); let (per_unit, sub_uor) = units(header); let origin = global_origin(header); let m: Affine = [ 1.0 / per_unit, 0.0, 0.0, 1.0 / per_unit, -origin[0] / per_unit, -origin[1] / per_unit, ]; let mut sheet = Sheet { sub_per_master: per_unit / sub_uor, table_colors, ..Default::default() }; place(&raw, &m, &defs, &styles, 0, &mut sheet); // The model's extent is its elements' stored ranges (text by its full // box), as the reference sizes a model. sheet.rect = match stored_range(&graphics) { Some([x0, y0, x1, y1]) => { let (lo, hi) = (apply(&m, [x0, y0]), apply(&m, [x1, y1])); [lo[0], lo[1], hi[0], hi[1]] } None => paths_bounds(&sheet.paths)?, }; Some(sheet) } /// The model's global origin (UORs; the design plane point drawn at the /// model's 0,0). fn global_origin(header: &[u8]) -> [f64; 2] { let get = |at: usize| f64_at(header, at).filter(|v| v.is_finite()).unwrap_or(0.0); [get(4212), get(4220)] } /// The union of the top-level elements' ranges (UORs): each graphic element /// stores its low corner at 56 and its size at 80, as 64-bit integers. fn stored_range(graphics: &[&[u8]]) -> Option<[f64; 4]> { let i64_at = |e: &[u8], at: usize| Some(i64::from_le_bytes(e.get(at..at + 8)?.try_into().ok()?) as f64); let mut r = [ f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY, ]; for e in graphics .iter() .filter(|e| e.len() >= 104 && !is_component(e)) { let (Some(x), Some(y), Some(w), Some(h)) = (i64_at(e, 56), i64_at(e, 64), i64_at(e, 80), i64_at(e, 88)) else { continue; }; r = [r[0].min(x), r[1].min(y), r[2].max(x + w), r[3].max(y + h)]; } (r[0] <= r[2]).then_some(r) } #[cfg(test)] mod cfb_memory_tests { use super::*; const ENDOFCHAIN: u32 = 0xFFFF_FFFE; const NONE: u32 = 0xFFFF_FFFF; /// Directory entries start at the first sector — `(0 + 1) * sector`, /// so the header occupies a full sector, not just its first 512 bytes. fn put_entry( b: &mut [u8], base: usize, idx: usize, name: &str, kind: u8, left: u32, right: u32, child: u32, start: u32, size: u32, ) { let e = base + idx * 128; let utf16: Vec = name.encode_utf16().chain(std::iter::once(0)).collect(); for (i, ch) in utf16.iter().enumerate() { b[e + i * 2..e + i * 2 + 2].copy_from_slice(&ch.to_le_bytes()); } b[e + 64..e + 66].copy_from_slice(&((utf16.len() * 2) as u16).to_le_bytes()); b[e + 66] = kind; b[e + 68..e + 72].copy_from_slice(&left.to_le_bytes()); b[e + 72..e + 76].copy_from_slice(&right.to_le_bytes()); b[e + 76..e + 80].copy_from_slice(&child.to_le_bytes()); b[e + 116..e + 120].copy_from_slice(&start.to_le_bytes()); b[e + 120..e + 124].copy_from_slice(&size.to_le_bytes()); } fn header(b: &mut [u8], sector_shift: u16, dir_start: u32, minifat_start: u32, difat0: u32) { b[0..8].copy_from_slice(&[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]); b[30..32].copy_from_slice(§or_shift.to_le_bytes()); b[32..34].copy_from_slice(&6u16.to_le_bytes()); // mini sector shift b[48..52].copy_from_slice(&dir_start.to_le_bytes()); b[56..60].copy_from_slice(&4096u32.to_le_bytes()); // mini cutoff b[60..64].copy_from_slice(&minifat_start.to_le_bytes()); b[68..72].copy_from_slice(&ENDOFCHAIN.to_le_bytes()); // DIFAT chain: none b[72..76].copy_from_slice(&0u32.to_le_bytes()); // DIFAT chain count: 0 b[76..80].copy_from_slice(&difat0.to_le_bytes()); // header DIFAT slot 0 for slot in 1..109 { b[76 + slot * 4..80 + slot * 4].copy_from_slice(&NONE.to_le_bytes()); } } /// ~1.5 KB CFB (sector_shift = 9): directory and miniFAT both start at /// sector 0, whose FAT entry (sector 1, all zeros) points at itself — /// `chain()` loops its full 1_000_000-iteration budget → 512 MB per /// call, three calls alive at once. fn hostile_self_fat_cfb() -> Vec { let mut b = vec![0u8; 3 * 512]; header(&mut b, 9, 0, 0, 1); // Sector 0 = directory: entry 0 is the root; its start field also // points at sector 0 (self-loop), size = u32::MAX so the mini chain // is never truncated down to a small length. put_entry(&mut b, 512, 0, "Root", 1, NONE, NONE, NONE, 0, u32::MAX); // Sector 1 = FAT, already zero: fat[0] = 0 (self-referential). b } /// Write the model storages' left/right sibling links as a balanced /// binary tree over entry indices `2, 4, …, 2·n` (walk depth O(log n)). fn link_models(b: &mut [u8], base: usize, model_count: usize) { fn build(b: &mut [u8], base: usize, lo: usize, hi: usize) -> u32 { if lo > hi { return NONE; } let mid = (lo + hi) / 2; let left = if mid > lo { build(b, base, lo, mid - 1) } else { NONE }; let right = if mid < hi { build(b, base, mid + 1, hi) } else { NONE }; let e = base + (2 + mid * 2) * 128; b[e + 68..e + 72].copy_from_slice(&left.to_le_bytes()); b[e + 72..e + 76].copy_from_slice(&right.to_le_bytes()); (2 + mid * 2) as u32 } build(b, base, 0, model_count - 1); } /// 300 model directories all sharing one 4 MB stream: `models()` /// retains every model simultaneously with no total budget → 1.2 GB /// from a 4 MB file (inflate() falls back to a raw copy of the zeros). fn multi_model_cfb() -> Vec { let sector = 4096usize; let model_count = 300usize; let entry_count = 2 + model_count * 2; let dir_sectors = (entry_count * 128 + sector - 1) / sector; let payload_len = 4 << 20; let payload_sectors = payload_len / sector; // Sectors: 0..dir_sectors directory; then 2 FAT; then payload. let fat_start = dir_sectors; let payload_start = fat_start + 2; let mut b = vec![0u8; (payload_start + payload_sectors + 1) * sector]; header(&mut b, 12, 0, ENDOFCHAIN, fat_start as u32); b[80..84].copy_from_slice(&((fat_start + 1) as u32).to_le_bytes()); // DIFAT slot 1 // Directory: root → storage → model storages, sibling links as a // balanced tree (real CFB directories are red-black; a linear chain // would hit streams()' existing depth-64 guard). Each storage has // one "Dgn^G/$1" stream pointing at the shared payload. let root_model = (model_count - 1) / 2; put_entry(&mut b, sector, 0, "Root", 1, NONE, NONE, 1, ENDOFCHAIN, 0); put_entry( &mut b, sector, 1, "Dgn-Md", 1, NONE, NONE, (2 + root_model * 2) as u32, ENDOFCHAIN, 0, ); for i in 0..model_count { let model = 2 + i * 2; let stream = model + 1; put_entry( &mut b, sector, model, &format!("Model{i}"), 1, NONE, NONE, stream as u32, ENDOFCHAIN, 0, ); put_entry( &mut b, sector, stream, "Dgn^G/$1", 2, NONE, NONE, NONE, payload_start as u32, payload_len as u32, ); } link_models(&mut b, sector, model_count); // Directory chain across its sectors. let fat_at = (fat_start + 1) * sector; for i in 0..dir_sectors { let at = fat_at + i * 4; let next = if i + 1 < dir_sectors { (i + 1) as u32 } else { ENDOFCHAIN }; b[at..at + 4].copy_from_slice(&next.to_le_bytes()); } // Both FAT sectors are not part of any chain; payload chain follows. for i in 0..2 { let at = fat_at + (fat_start + i) * 4; b[at..at + 4].copy_from_slice(&ENDOFCHAIN.to_le_bytes()); } for i in 0..payload_sectors { let at = fat_at + (payload_start + i) * 4; let next = if i + 1 < payload_sectors { (payload_start + i + 1) as u32 } else { ENDOFCHAIN }; b[at..at + 4].copy_from_slice(&next.to_le_bytes()); } b } /// A self-referential FAT must not let `chain()` (directory, mini /// stream, miniFAT) grow past the file it was read from: the old fixed /// 1_000_000-iteration guard built 512 MB per call from 1.5 KB. #[test] fn a_self_referential_fat_builds_no_more_bytes_than_the_file() { let bytes = hostile_self_fat_cfb(); let cfb = Cfb::open(&bytes).expect("hostile CFB parses"); assert!( cfb.entries.len() * 128 <= bytes.len(), "directory grew to {} entries from a {} byte file", cfb.entries.len(), bytes.len() ); assert!( cfb.mini.len() <= bytes.len(), "mini stream grew to {} bytes from a {} byte file", cfb.mini.len(), bytes.len() ); assert!( cfb.mini_fat.len() * 4 <= bytes.len(), "miniFAT grew to {} bytes from a {} byte file", cfb.mini_fat.len() * 4, bytes.len() ); } /// Hundreds of model dirs sharing one stream: `models()` inflates all /// of them at once. The total must stay within the file-scaled budget, /// while the first models still load. #[test] fn models_keep_their_total_inflated_size_within_budget() { let bytes = multi_model_cfb(); let cfb = Cfb::open(&bytes).expect("multi-model CFB parses"); let out = models(&cfb); let total: usize = out.iter().map(|(_, g, h)| g.len() + h.len()).sum(); let budget = (bytes.len() * 64).max(256 << 20); assert!( total <= budget + (512 << 20), "models inflated to {} bytes from a {} byte file (budget {budget})", total, bytes.len() ); assert!(!out.is_empty(), "the first models still load"); assert!(out.len() < 300, "models past the budget are dropped"); assert!(model_names(&bytes).is_some()); } }