diff --git a/src/app/command_driver/clipboard.rs b/src/app/command_driver/clipboard.rs index 4afccaa48..9c1ffc150 100644 --- a/src/app/command_driver/clipboard.rs +++ b/src/app/command_driver/clipboard.rs @@ -707,6 +707,11 @@ fn add_entity_clone_doc_inner( } entity.common_mut().handle = Handle::NULL; entity.common_mut().owner_handle = owner; + // An extension dictionary is exclusively owned; the clone must not claim + // the source's (cross-drawing it would dangle into the source document), + // or file validation reports "Extension dictionary Cannot access". + // Paste reattaches a recreated subtree afterwards (finalize_paste). + entity.common_mut().xdictionary_handle = None; } /// Document-level fresh `*D` block from `subs`: the `&mut CadDocument` @@ -1469,3 +1474,86 @@ mod headless_reuse_proof_tests { ); } } + +#[cfg(test)] +mod define_transformed_block_doc_xdict_tests { + use super::define_transformed_block_doc; + + /// No extension dictionary may be referenced by more than one entity. + fn assert_no_shared_xdicts(doc: &codec::CadDocument) { + use std::collections::HashMap; + let mut owners: HashMap = HashMap::new(); + for entity in doc.entities() { + if let Some(xd) = entity.common().xdictionary_handle { + if !xd.is_null() { + *owners.entry(xd).or_default() += 1; + } + } + } + let shared: Vec<_> = owners + .iter() + .filter(|(_, &n)| n > 1) + .map(|(h, n)| format!("{h:?} x{n}")) + .collect(); + assert!( + shared.is_empty(), + "shared extension dictionaries: {}", + shared.join(", ") + ); + } + + /// Pasted/derived sub-entities must not share the source's extension + /// dictionary. `define_transformed_block_doc` is the verbatim + /// document-level port of `Scene::define_transformed_block`, and its + /// clone helper likewise preserves `xdictionary_handle`; across drawings + /// that handle would dangle into the source document. + #[test] + fn cloned_subs_do_not_share_source_xdict() { + let mut doc = codec::CadDocument::new(); + + let dict_handle = doc.allocate_handle(); + let mut dict = codec::objects::Dictionary::new(); + dict.handle = dict_handle; + doc.objects + .insert(dict_handle, codec::objects::ObjectType::Dictionary(dict)); + + let mut line = codec::entities::Line::new(); + line.common.xdictionary_handle = Some(dict_handle); + let src_handle = doc.add_entity(codec::EntityType::Line(line)).unwrap(); + + let subs = vec![doc.get_entity(src_handle).unwrap().clone()]; + let name = define_transformed_block_doc( + &mut doc, + &subs, + &crate::command::EntityTransform::Translate(glam::DVec3::ZERO), + ) + .expect("clone block must be created"); + assert!( + name.starts_with("*D"), + "anonymous block expected, got {name}" + ); + + assert_no_shared_xdicts(&doc); + // Source keeps its dictionary. + match doc.get_entity(src_handle) { + Some(e) => assert_eq!( + e.common().xdictionary_handle, + Some(dict_handle), + "the source must keep its own dictionary" + ), + None => panic!("source entity missing"), + } + // Clones carry none. + let owned = doc.block_records.get(&name).unwrap().entity_handles.clone(); + assert!(!owned.is_empty(), "clone block should own sub-entities"); + for h in &owned { + match doc.get_entity(*h) { + Some(e) => assert!( + e.common().xdictionary_handle.is_none(), + "cloned child {h:?} must not carry an extension dictionary" + ), + None => panic!("cloned child {h:?} missing"), + } + } + } +} diff --git a/src/modules/draw/modify/explode.rs b/src/modules/draw/modify/explode.rs index 4340bd3ca..cd2e4dfa7 100644 --- a/src/modules/draw/modify/explode.rs +++ b/src/modules/draw/modify/explode.rs @@ -869,6 +869,10 @@ fn dim_common( ) -> codec::entities::EntityCommon { let mut c = base.clone(); c.handle = Handle::NULL; + // A baked child must not claim its dimension's extension dictionary: + // xdict ownership is exclusive, and a second claimant fails file + // validation with "Extension dictionary Cannot access". + c.xdictionary_handle = None; if clr != 0 && clr != 256 { c.color = codec::types::Color::from_index(clr); c.color_name = None; @@ -1415,6 +1419,9 @@ fn explode_dimension(dim: &Dimension, doc: &CadDocument) -> Vec { tent.common_mut().color_name = tc.color_name; tent.common_mut().color_book_handle = tc.color_book_handle; tent.common_mut().line_weight = tc.line_weight; + // The live-render text entity carries the dimension's dictionary; + // the baked copy must not share it (see above). + tent.common_mut().xdictionary_handle = None; result.push(tent); } @@ -2124,4 +2131,94 @@ mod tests { failures.join("\n") ); } + + /// No extension dictionary may be referenced by more than one entity. + /// Shared xdict ownership fails file validation as + /// "Extension dictionary Cannot access". + fn assert_no_shared_xdicts(doc: &CadDocument) { + use std::collections::HashMap; + let mut owners: HashMap = HashMap::new(); + for entity in doc.entities() { + if let Some(xd) = entity.common().xdictionary_handle { + if !xd.is_null() { + *owners.entry(xd).or_default() += 1; + } + } + } + let shared: Vec<_> = owners + .iter() + .filter(|(_, &n)| n > 1) + .map(|(h, n)| format!("{h:?} x{n}")) + .collect(); + assert!( + shared.is_empty(), + "shared extension dictionaries: {}", + shared.join(", ") + ); + } + + /// Baked dimension children must not share the dimension's extension + /// dictionary. `dim_common` used to clone the dimension's full common + /// block (including `xdictionary_handle`) onto every baked child, so an + /// R2000 save of a drawing with N such children failed audit + /// with exactly N "Cannot access" errors. + #[test] + fn baked_children_do_not_share_dimension_xdict() { + let mut doc = CadDocument::new(); + + // Made-up dictionary object for the dimension to own. + let dict_handle = doc.allocate_handle(); + let mut dict = codec::objects::Dictionary::new(); + dict.handle = dict_handle; + doc.objects + .insert(dict_handle, codec::objects::ObjectType::Dictionary(dict)); + + // Blockless dimension carrying that dictionary, as a file-read + // dimension with an extension dictionary would. + let mut d = DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0)); + d.definition_point = Vector3::new(0.0, 5.0, 0.0); + d.base.text_middle_point = Vector3::new(5.0, 5.0, 0.0); + d.base.common.xdictionary_handle = Some(dict_handle); + let dim_handle = doc + .add_entity(EntityType::Dimension(Dimension::Linear(d))) + .unwrap(); + + bake_dimension_blocks(&mut doc); + + let block_name = match doc.get_entity(dim_handle) { + Some(EntityType::Dimension(d)) => d.base().block_name.clone(), + _ => panic!("dimension missing"), + }; + let owned: Vec = doc + .block_records + .get(&block_name) + .unwrap() + .entity_handles + .clone(); + assert!( + !owned.is_empty(), + "baked block should own sub-entities for the invariant to mean anything" + ); + // (a) no shared dictionary anywhere in the document. + assert_no_shared_xdicts(&doc); + // (b) the dimension keeps its dictionary. + match doc.get_entity(dim_handle) { + Some(EntityType::Dimension(d)) => assert_eq!( + d.base().common.xdictionary_handle, + Some(dict_handle), + "the dimension must keep its own dictionary" + ), + _ => panic!("dimension missing"), + } + // (c) the baked children carry none. + for h in &owned { + match doc.get_entity(*h) { + Some(e) => assert!( + e.common().xdictionary_handle.is_none(), + "baked child {h:?} must not carry an extension dictionary" + ), + None => panic!("baked child {h:?} missing"), + } + } + } } diff --git a/src/scene/modify.rs b/src/scene/modify.rs index 63ebedc75..aeb936aa7 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -671,6 +671,10 @@ impl Scene { Self::reset_clone_subhandles(&mut self.document, &mut sub); sub.common_mut().handle = Handle::NULL; sub.common_mut().owner_handle = br_handle; + // An extension dictionary is exclusively owned; the clone must + // not claim the source's, or file validation reports + // "Extension dictionary Cannot access". + sub.common_mut().xdictionary_handle = None; let _ = self.document.add_entity(sub); } Some(new_name) @@ -2207,3 +2211,106 @@ pub(crate) fn remap_copied_reactors( .collect(); } } + +#[cfg(test)] +mod define_transformed_block_xdict_tests { + use super::*; + + /// No extension dictionary may be referenced by more than one entity. + fn assert_no_shared_xdicts(doc: &codec::CadDocument) { + use std::collections::HashMap; + let mut owners: HashMap = HashMap::new(); + for entity in doc.entities() { + if let Some(xd) = entity.common().xdictionary_handle { + if !xd.is_null() { + *owners.entry(xd).or_default() += 1; + } + } + } + let shared: Vec<_> = owners + .iter() + .filter(|(_, &n)| n > 1) + .map(|(h, n)| format!("{h:?} x{n}")) + .collect(); + assert!( + shared.is_empty(), + "shared extension dictionaries: {}", + shared.join(", ") + ); + } + + /// Cloned sub-entities must not share the source's extension dictionary. + /// `define_transformed_block` clones each sub wholesale; without clearing + /// `xdictionary_handle` the clone claims the source's dictionary and + /// file validation reports "Extension dictionary Cannot access". + #[test] + fn cloned_subs_do_not_share_source_xdict() { + let mut scene = Scene::new(); + + let dict_handle = scene.document.allocate_handle(); + let mut dict = codec::objects::Dictionary::new(); + dict.handle = dict_handle; + scene + .document + .objects + .insert(dict_handle, codec::objects::ObjectType::Dictionary(dict)); + + let mut br = codec::tables::BlockRecord::new("SRC"); + br.handle = scene.document.allocate_handle(); + scene.document.block_records.add(br).unwrap(); + let mut line = codec::entities::Line::new(); + line.common.xdictionary_handle = Some(dict_handle); + let src_handle = scene + .document + .add_entity(codec::EntityType::Line(line)) + .unwrap(); + scene + .document + .block_records + .get_mut("SRC") + .unwrap() + .entity_handles + .push(src_handle); + + let subs = vec![scene.document.get_entity(src_handle).unwrap().clone()]; + let name = scene + .define_transformed_block( + &subs, + &crate::command::EntityTransform::Translate(glam::DVec3::ZERO), + ) + .expect("clone block must be created"); + assert!( + name.starts_with("*D"), + "anonymous block expected, got {name}" + ); + + assert_no_shared_xdicts(&scene.document); + // Source keeps its dictionary. + match scene.document.get_entity(src_handle) { + Some(e) => assert_eq!( + e.common().xdictionary_handle, + Some(dict_handle), + "the source must keep its own dictionary" + ), + None => panic!("source entity missing"), + } + // Clones carry none. + let owned = scene + .document + .block_records + .get(&name) + .unwrap() + .entity_handles + .clone(); + assert!(!owned.is_empty(), "clone block should own sub-entities"); + for h in &owned { + match scene.document.get_entity(*h) { + Some(e) => assert!( + e.common().xdictionary_handle.is_none(), + "cloned child {h:?} must not carry an extension dictionary" + ), + None => panic!("cloned child {h:?} missing"), + } + } + } +}