//! DWF (6.x, a zip after a "(DWF V06.00)" tag, sheets as W2D streams) and //! DWFx (an XPS package, sheets as fixed pages) underlays: sheet names, and //! each sheet's plotted geometry mapped back to the model units it was //! plotted from, which is how an underlay places it. use std::collections::HashMap; use std::io::{Cursor, Read}; use super::model::{arc_cubics, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath, Text, CAP_PER_EM}; /// The package's files, with `\` separators turned into `/`. fn unzip(bytes: &[u8]) -> Option>> { // A DWF 6 package is a zip after a short tag; the archive reader finds // the offset itself. let mut archive = zip::ZipArchive::new(Cursor::new(bytes)).ok()?; let mut files = HashMap::new(); // Total inflated size a package may reach: far above any real sheet // set, and a crafted archive cannot exhaust memory. let mut budget: u64 = 1 << 30; for i in 0..archive.len() { let file = archive.by_index(i).ok()?; let name = file.name().replace('\\', "/").trim_start_matches('/').to_string(); let mut data = Vec::new(); file.take(budget + 1).read_to_end(&mut data).ok()?; budget = budget.checked_sub(data.len() as u64)?; files.insert(name, data); } Some(files) } /// One plotted sheet of the package. struct SheetEntry { name: String, order: i64, /// The sheet's graphics: a W2D stream or an XPS fixed page. graphics: String, /// Paper millimetres per graphics unit and the paper offset (mm). paper: ([f64; 2], [f64; 2]), paper_height_mm: f64, /// DWFx: the W2X resource holding the model units transform. w2x: Option, } fn attr<'a>(node: roxmltree::Node<'a, '_>, name: &str) -> Option<&'a str> { node.attributes().find(|a| a.name() == name).map(|a| a.value()) } fn numbers(text: &str) -> Vec { text.split(|c: char| c == ',' || c.is_whitespace()) .filter(|s| !s.is_empty()) .filter_map(|s| s.parse().ok()) .collect() } /// Ceiling on element nesting depth before roxmltree sees the text. Its /// tokenizer recurses once per level (`parse_element` ↔ `parse_content`, /// present in 0.20 and 0.21), so a 100 000-deep fpage overflows the stack /// *inside* `Document::parse`. Real pages and descriptors nest single /// digits; 256 keeps the frames well under the thread stack budget. const MAX_XML_DEPTH: usize = 256; /// True when no element nests deeper than `max`. A linear byte scan that /// skips comments, CDATA, PIs, doctype subsets and quoted attribute values /// so their `<`/`>` never count. Defensive only: malformed markup may /// overcount and reject the file, but must never undercount. fn xml_depth_within(text: &str, max: usize) -> bool { let b = text.as_bytes(); let mut i = 0usize; let mut depth = 0usize; let mut tag = false; // inside `< … >` let mut closing = false; // the current tag is ` { if b[i..].starts_with(b"-->") { mark = 0; i += 3; } else { i += 1; } continue; } 2 => { if b[i..].starts_with(b"]]>") { mark = 0; i += 3; } else { i += 1; } continue; } 3 => { if b[i..].starts_with(b"?>") { mark = 0; i += 2; } else { i += 1; } continue; } 4 => { match b[i] { b'[' => brackets += 1, b']' => brackets = brackets.saturating_sub(1), b'>' if brackets == 0 => mark = 0, _ => {} } i += 1; continue; } _ => {} } let c = b[i]; if quote != 0 { if c == quote { quote = 0; } i += 1; continue; } if tag { match c { b'"' | b'\'' => quote = c, b'>' => { tag = false; if closing { depth = depth.saturating_sub(1); } else { // Self-closing (``, ``) adds no depth. let mut j = i; while j > 0 && b[j - 1].is_ascii_whitespace() { j -= 1; } if !(j > 0 && b[j - 1] == b'/') { depth += 1; if depth > max { return false; } } } } _ => {} } i += 1; continue; } if c != b'<' { i += 1; continue; } let rest = &b[i..]; if rest.starts_with(b" ]]>"/> "#); } s.push_str(&"".repeat(1000)); assert!(super::xml_depth_within(&s, super::MAX_XML_DEPTH)); // Self-closing tags add no depth; only real opens do. assert!(super::xml_depth_within(&"".repeat(5000), super::MAX_XML_DEPTH)); assert!(super::xml_depth_within( &format!("", "".repeat(1000)), super::MAX_XML_DEPTH )); } /// The w2x units resource goes through the same guarded parse; a deep /// one is skipped while the page itself still renders. #[test] fn deep_w2x_is_skipped_without_hurting_the_page() { let mut deep = String::from(""); for _ in 0..100_000 { deep.push_str(""); } for _ in 0..100_000 { deep.push_str(""); } deep.push_str(""); let xml = r##" "##; let (paths, _view, _texts) = xps::read( xml.as_bytes(), Some(deep.as_bytes()), &entry(), &|_| None, ) .expect("page parses"); assert_eq!(paths.len(), 1); } /// A hostile descriptor.xml is dropped instead of crashing `sheets`. #[test] fn deep_descriptor_produces_no_sheets() { let mut deep = String::from(""); for _ in 0..100_000 { deep.push_str(""); } for _ in 0..100_000 { deep.push_str(""); } deep.push_str(""); let mut files = std::collections::HashMap::new(); files.insert( "3D/descriptor.xml".to_string(), deep.into_bytes(), ); assert!(super::sheets(&files).is_empty()); } }