diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 09d33df8..b2db0a2c 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -380,6 +380,7 @@ fn timeline_edits_reresolve_references() { run(&mut s, "UndoCommand", json!({})); run(&mut s, "UndoCommand", json!({})); assert!(rel(volume(&mut s), 60.0 * 30.0 * 25.0 - f4 - PI * 9.0 * 25.0) < 1e-3); +} /// A 40 × 30 × 20 box exported to a STEP file in a fresh temp directory. fn step_box_file(tag: &str) -> std::path::PathBuf { diff --git a/crates/io/src/lib.rs b/crates/io/src/lib.rs index e92ac7b4..2c13bd9c 100644 --- a/crates/io/src/lib.rs +++ b/crates/io/src/lib.rs @@ -61,8 +61,10 @@ pub fn step_import_feature(bytes: &[u8], file: &str) -> Result { .filter(|s| !s.trim().is_empty()) .unwrap_or_else(|| "Import".into()); let generic = |n: &str| n.trim().is_empty() || n.starts_with('(') || n.eq_ignore_ascii_case("import") || n.eq_ignore_ascii_case("unnamed"); - let name = match imp.tree.as_slice() { - [root] if !generic(&root.name) => root.name.clone(), + // The root product's name; else the only body's name; else the file name. + let name = match (imp.tree.as_slice(), imp.bodies.as_slice()) { + ([root], _) if !generic(&root.name) => root.name.clone(), + (_, [only]) if !generic(&only.name) => only.name.clone(), _ => stem, }; let file_name = std::path::Path::new(file).file_name().map(|s| s.to_string_lossy().to_string()).unwrap_or_else(|| file.to_string()); diff --git a/crates/kernel/src/step_in/mod.rs b/crates/kernel/src/step_in/mod.rs index c6a37272..9dc34846 100644 --- a/crates/kernel/src/step_in/mod.rs +++ b/crates/kernel/src/step_in/mod.rs @@ -26,6 +26,9 @@ use p21::{Entity, Exchange, Param}; const MAX_ASSEMBLY_DEPTH: usize = 32; /// Most body instances in one import. const MAX_BODIES: usize = 10_000; +/// Most product occurrences and mapped representations visited (a shared sub-assembly used +/// twice at every level would otherwise grow exponentially). +const MAX_VISITS: usize = 100_000; /// A body read from a STEP file. #[derive(Clone, Debug)] @@ -228,6 +231,8 @@ struct Reader<'a> { used_items: HashSet, /// Items already instanced for the product being read. node_items: HashSet, + /// Occurrences and mapped representations visited so far. + visits: usize, bodies: Vec, } @@ -417,6 +422,11 @@ impl<'a> Reader<'a> { self.warn("mapped items nested too deeply"); return; } + self.visits += 1; + if self.visits > MAX_VISITS { + self.warn(format!("more than {MAX_VISITS} component instances: the rest are left out")); + return; + } let u = self.units_of(rep); let items = self.reps.get(&rep).map(|r| r.0.clone()).unwrap_or_default(); for item in items { @@ -542,6 +552,11 @@ impl<'a> Reader<'a> { self.warn("assembly nested too deeply or recursive: some components were left out"); return node; } + self.visits += 1; + if self.visits > MAX_VISITS { + self.warn(format!("more than {MAX_VISITS} component instances: the rest are left out")); + return node; + } stack.push(pd); let mut path = path.to_vec(); path.push(name.clone()); @@ -609,6 +624,7 @@ pub fn step_import(text: &str) -> Result { solids: HashMap::new(), used_items: HashSet::new(), node_items: HashSet::new(), + visits: 0, bodies: Vec::new(), }; rd.index(); @@ -642,6 +658,7 @@ pub fn step_import(text: &str) -> Result { if !loose.is_empty() { let mut bodies = Vec::new(); rd.node_items = rd.used_items.clone(); + rd.visits = 0; for r in loose { rd.rep_bodies(r, &identity, &[], "Import", &mut bodies, 0); } diff --git a/crates/kernel/src/step_in/tests.rs b/crates/kernel/src/step_in/tests.rs index 9387104d..fdf4b739 100644 --- a/crates/kernel/src/step_in/tests.rs +++ b/crates/kernel/src/step_in/tests.rs @@ -283,3 +283,26 @@ fn assembly_cycles_are_bounded() { let imp = r.unwrap(); assert_eq!(imp.bodies.len(), 1); } + +#[test] +fn exponential_assemblies_are_bounded() { + // P0 uses P1 twice, P1 uses P2 twice, … 30 levels: 2^30 occurrences if followed blindly. + let mut t = String::from(HEAD); + t += "#8=PRODUCT_DEFINITION_CONTEXT('part definition',#9,'design');\n#9=APPLICATION_CONTEXT('');\n"; + t += &cube_entities(); + t += &context(20, "(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.))"); + let levels = 30u64; + for l in 0..=levels { + t += &product(1000 + l * 10, &format!("P{l}")); + if l < levels { + for k in 0..2 { + t += &format!("#{}=NEXT_ASSEMBLY_USAGE_OCCURRENCE('','','',#{},#{},$);\n", 2000 + l * 10 + k, 1000 + l * 10, 1000 + (l + 1) * 10); + } + } + } + t += &format!("#60=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#101),#20);\n#61=SHAPE_DEFINITION_REPRESENTATION(#{},#60);\n", 1000 + levels * 10 + 4); + t += TAIL; + let imp = step_import(&t).unwrap(); + assert!(!imp.bodies.is_empty() && imp.bodies.len() <= MAX_BODIES); + assert!(imp.warnings.iter().any(|w| w.contains("left out")), "{:?}", imp.warnings); +}