Merge pull request #2917 from mkzung/fix/dwg-application-object-proxies

DWG: another application's objects are kept as proxy objects of their class
This commit is contained in:
FredApps
2026-11-08 14:52:50 +01:00
committed by GitHub
+331
View File
@@ -107,6 +107,9 @@ mod native {
}
let mut notes = read_notes(&outcome.stats);
let mut doc = outcome.document;
notes.extend(
guarded(AssertUnwindSafe(|| application_objects(&mut doc))).map_err(|_| "the DWG application object conversion failed".to_string())?,
);
let orphans = guarded(AssertUnwindSafe(|| post_process(&mut doc))).map_err(|_| "the DWG post-processing failed on this file".to_string())?;
if orphans > 0 {
let (s, were) = if orphans == 1 { ("", "was") } else { ("s", "were") };
@@ -390,6 +393,214 @@ mod native {
std::panic::catch_unwind(f).map_err(|_| ())
}
/// The version a proxy record of a DWG of `version` names its format by (Open Design
/// Specification for .dwg, proxy objects): 19 for R13, 21 for R14, 23 for 2000, 25 for 2004,
/// 27 for 2007, 29 for 2010, 31 for 2013 and 33 for 2018; `None` for any other version.
fn proxy_dwg_version(version: acadrust::DxfVersion) -> Option<i32> {
Some(match version.version_code() {
1012 => 19,
1014 => 21,
1015 => 23,
1018 => 25,
1021 => 27,
1024 => 29,
1027 => 31,
1032 => 33,
_ => return None,
})
}
/// Objects of another application's class that acadrust keeps only as their DWG record
/// (`ObjectType::Unknown`, which has no DXF form): its DXF writer left them out, and the
/// references to them went with them (a dimension's extension dictionary entry naming its
/// application data, say), with no note. Each is carried as a proxy object of its class instead
/// (DXF Reference, ACAD_PROXY_OBJECT), so the DXF keeps it and a DWG written from that DXF has it
/// again: its common object data (owner, reactors, extension dictionary) and the streams after
/// it (data bits, R2007+ strings, object references), as the Open Design Specification for .dwg
/// splits an object record. Objects of the drawing database's own classes are not turned into
/// another application's proxies. What is still without a DXF form is named in the notes, so a
/// save over the original is not mistaken for a complete one.
fn application_objects(doc: &mut acadrust::CadDocument) -> Vec<String> {
use acadrust::objects::{ObjectType, ProxyObject};
let classes: Vec<acadrust::classes::DxfClass> = doc.classes.iter().cloned().collect();
let source = doc.dwg_source_version.unwrap_or(doc.version);
if let Some(dwg_version) = proxy_dwg_version(source) {
let maintenance = i32::from(doc.maintenance_version);
for object in doc.objects.values_mut() {
let ObjectType::Unknown { handle, raw_dwg_data: Some(data), raw_dwg_handle_bits, raw_dwg_version: Some(version), type_name, .. } =
&*object
else {
continue;
};
if !application_class(&classes, type_name) {
continue;
}
let raw = acadrust::entities::RawRecord { data: data.clone(), handle_bits: *raw_dwg_handle_bits, version: *version };
let handle = *handle;
// A record that doesn't split stays as it was, and is named below.
let Ok(Ok(record)) = std::panic::catch_unwind(AssertUnwindSafe(|| object_record(&raw, handle.value()))) else { continue };
let Some(class) = classes.iter().find(|c| c.class_number == record.kind && !c.is_an_entity) else { continue };
*object = ObjectType::ProxyObject(ProxyObject {
handle,
owner: record.owner,
reactors: record.reactors,
xdictionary_handle: record.xdictionary,
proxy_id: 499,
class_id: i32::from(class.class_number),
dwg_version,
maintenance_version: maintenance,
version: (maintenance << 16) | dwg_version,
payload: record.payload,
text_payload: record.text_payload,
object_ids: record.object_ids,
..Default::default()
});
}
}
let mut left: std::collections::BTreeMap<&str, usize> = std::collections::BTreeMap::new();
for object in doc.objects.values() {
if let ObjectType::Unknown { type_name, raw_dxf_codes: None, .. } = object {
let n = left.entry(type_name.as_str()).or_insert(0);
*n = n.saturating_add(1);
}
}
if left.is_empty() {
return Vec::new();
}
let total = left.values().fold(0usize, |a, n| a.saturating_add(*n));
let mut list: Vec<String> = left.iter().take(NOTE_TYPES).map(|(t, n)| format!("{t:.60} × {n}")).collect();
if left.len() > NOTE_TYPES {
list.push(format!("{} more types", left.len() - NOTE_TYPES));
}
vec![format!(
"DWG: {total} objects of types CADCraft doesn't keep were left out ({}); saving over the original file would lose them",
list.join(", ")
)]
}
/// Whether `name` is an object class of another application, which only that application
/// rebuilds: not one of the drawing database's own classes (`AcDb…`/`ACDB…`/`ACAD_…`), whose
/// leftovers the database regenerates itself.
fn application_class(classes: &[acadrust::classes::DxfClass], name: &str) -> bool {
classes.iter().any(|c| {
!c.is_an_entity
&& c.dxf_name.eq_ignore_ascii_case(name)
&& !c.cpp_class_name.starts_with("AcDb")
&& !["ACDB", "ACAD_", "ACAD"].iter().any(|p| c.dxf_name.to_ascii_uppercase().starts_with(p))
})
}
/// An application object acadrust kept only as its record: the parts a proxy object carries.
struct ObjectRecord {
kind: i16,
owner: acadrust::types::Handle,
reactors: Vec<acadrust::types::Handle>,
xdictionary: Option<acadrust::types::Handle>,
payload: acadrust::objects::ProxyPayload,
text_payload: acadrust::objects::ProxyPayload,
object_ids: Vec<acadrust::objects::ProxyObjectReference>,
}
/// Split an object record into its common object data and the uninterpreted streams after it
/// (data bits, R2007+ strings and object references), as an application object's proxy holds
/// them (Open Design Specification for .dwg, common object data and proxy objects). The record
/// is hostile: sizes are checked against it, and acadrust's checked stream reader does the rest.
fn object_record(raw: &acadrust::entities::RawRecord, handle: u64) -> Result<ObjectRecord, String> {
use acadrust::io::dwg::{
DwgVersion, dwg_reference_type::DwgReferenceType, dwg_stream_readers::object_reader::DwgObjectReader,
dwg_stream_writers::bit_writer::DwgBitWriter,
};
use acadrust::objects::{ProxyObjectReference, ProxyPayload, ProxyReferenceKind};
use acadrust::types::{DxfVersion, Handle};
if raw.data.len() > 32 << 20 {
return Err("object record exceeds the transport limit".into());
}
let handle_bits = usize::try_from(raw.handle_bits).map_err(|_| "object has a negative handle stream size")?;
let total_bits = raw.data.len().checked_mul(8).ok_or("object record is too large")?;
if handle_bits > total_bits {
return Err("object handle stream exceeds its record".into());
}
let version = DwgVersion::from_dxf_version(raw.version).map_err(|e| e.to_string())?;
let mut frame = DwgBitWriter::new(version, raw.version);
frame.write_modular_short(raw.data.len());
if raw.version >= DxfVersion::AC1024 {
frame.write_modular_char(handle_bits);
}
frame.write_bytes(&raw.data);
let reader = DwgObjectReader::new(frame.into_bytes(), raw.version, std::collections::HashMap::new()).map_err(|e| e.to_string())?;
let (kind, mut streams) = reader.read_record_at(0).map_err(|e| e.to_string())?;
if kind < 500 {
return Err("object has no application class".into());
}
let common = reader.read_common_non_entity_data(&mut streams, kind);
streams.check_error().map_err(|e| e.to_string())?;
if common.common.handle != handle {
return Err("object record names another handle".into());
}
let read_bits = |count: usize, bit: &mut dyn FnMut() -> bool| -> Result<ProxyPayload, String> {
if count > total_bits {
return Err("object stream exceeds its record".into());
}
let n = u32::try_from(count).map_err(|_| "object stream is too large")?;
let mut bytes = vec![0u8; count.div_ceil(8)];
for index in 0..count {
if bit()
&& let Some(b) = bytes.get_mut(index / 8)
{
*b |= 0x80 >> (index % 8);
}
}
Ok(ProxyPayload::from_bits(&bytes, n))
};
let main = usize::try_from(streams.main_remaining_bits()).map_err(|_| "object body has an invalid size")?;
let payload = read_bits(main, &mut || streams.read_bit())?;
let text_payload = if raw.version >= DxfVersion::AC1021 {
let bits = usize::try_from(streams.text_remaining_bits()).map_err(|_| "object has a negative string stream size")?;
read_bits(bits, &mut || streams.read_text_bit())?
} else {
ProxyPayload::default()
};
// Every reference to the end of the handle stream, null ones too: an object keeps a null pointer in its
// field's place, and the references after it are the next fields' (as acadrust reads a proxy's own). R2010+
// records give the stream's exact length; the bits of its last byte past it are padding.
let mut object_ids = Vec::new();
let mut left = if raw.version >= DxfVersion::AC1024 {
streams.get_handle_bits().max(0).min(streams.handle_remaining_bits())
} else {
streams.handle_remaining_bits()
};
while left >= 8 {
if object_ids.len() >= 100_000 {
return Err("object has too many references".into());
}
let before = streams.handle_remaining_bits();
let (target, reference) = streams.read_handle_reference(handle);
let used = before.saturating_sub(streams.handle_remaining_bits());
if used <= 0 || used > left {
break;
}
left -= used;
let kind = match reference {
DwgReferenceType::SoftOwnership => ProxyReferenceKind::SoftOwnership,
DwgReferenceType::HardOwnership => ProxyReferenceKind::HardOwnership,
DwgReferenceType::SoftPointer => ProxyReferenceKind::SoftPointer,
DwgReferenceType::HardPointer => ProxyReferenceKind::HardPointer,
DwgReferenceType::Undefined => ProxyReferenceKind::Undefined,
};
object_ids.push(ProxyObjectReference { handle: Handle::from(target), kind });
}
streams.check_error().map_err(|e| e.to_string())?;
Ok(ObjectRecord {
kind,
owner: Handle::from(common.owner_handle),
reactors: common.reactors.into_iter().map(Handle::from).collect(),
xdictionary: common.xdictionary_handle.map(Handle::from),
payload,
text_payload,
object_ids,
})
}
/// [`table_merges`] visiting at most `budget` cells in all; a range that would pass it is
/// left unmerged. Returns the cells visited.
fn table_merges_within(doc: &mut acadrust::CadDocument, budget: usize) -> usize {
@@ -1159,6 +1370,126 @@ mod native {
assert_eq!(guarded(|| 3), Ok(3));
}
}
#[cfg(test)]
mod application_object_tests {
use super::*;
use acadrust::CadDocument;
use acadrust::classes::DxfClass;
use acadrust::objects::{ObjectType, ProxyObjectReference, ProxyPayload, ProxyReferenceKind, RegisteredClassObject};
use acadrust::types::{DxfVersion, Handle};
/// An object of another application's class that acadrust keeps only as its record (a
/// vertical product's dimension data, say) is carried as that class's proxy object, with its
/// data bits, references, owner and extension dictionary, through DXF and back to DWG, twice.
#[test]
fn undecoded_application_object_is_kept_as_its_proxy() {
for version in [DxfVersion::AC1027, DxfVersion::AC1032] {
let mut source = CadDocument::new();
source.version = version;
source.classes.add_or_update(DxfClass::new("TESTCONTAINER", "xyzDbTestContainer"));
let handle = Handle::from(0x2Bu64);
let target = Handle::from(0x2Cu64);
let payload = ProxyPayload::from_bits(&[0x5A, 0x3C, 0xC0], 18);
// A null pointer between two references: it keeps its place, the one after it is not lost.
let references = vec![
ProxyObjectReference { handle: target, kind: ProxyReferenceKind::HardPointer },
ProxyObjectReference { handle: Handle::from(0u64), kind: ProxyReferenceKind::SoftPointer },
ProxyObjectReference { handle: Handle::from(0x0Cu64), kind: ProxyReferenceKind::HardPointer },
];
source.objects.insert(
handle,
ObjectType::RegisteredClass(RegisteredClassObject {
handle,
owner: Handle::from(0x0Cu64),
dxf_name: "TESTCONTAINER".into(),
cpp_class_name: "xyzDbTestContainer".into(),
payload: payload.clone(),
object_ids: references.clone(),
..Default::default()
}),
);
let mut dwg = acadrust::DwgWriter::write_to_vec(&source).unwrap();
// acadrust has no model of it: read back, it is only its record.
let decoded = acadrust::DwgReader::from_stream(Cursor::new(dwg.clone())).read().unwrap();
assert!(matches!(decoded.objects.get(&handle), Some(ObjectType::Unknown { .. })), "{:?}", decoded.objects.get(&handle));
for generation in 0..2 {
let (dxf, notes) = dwg_to_dxf_noted(&dwg, Limits::DEFAULT).unwrap();
assert!(notes.is_empty(), "{version:?} {generation}: {notes:?}");
// The DXF has the object, by its handle, as a proxy record.
let text = String::from_utf8_lossy(&dxf);
let lines: Vec<&str> = text.lines().map(str::trim).collect();
let record = lines.windows(4).any(|w| w == ["0", "ACAD_PROXY_OBJECT", "5", "2B"]);
assert!(record, "{version:?} {generation}: no proxy record for the object");
dwg = dxf_to_dwg_as(&dxf, "AC1032").unwrap();
let doc = acadrust::DwgReader::from_stream(Cursor::new(dwg.clone())).read().unwrap();
let Some(ObjectType::ProxyObject(proxy)) = doc.objects.get(&handle) else { panic!("{version:?} {generation}: proxy object") };
assert_eq!(proxy.payload.bit_count, payload.bit_count);
assert_eq!(proxy.payload.data(), payload.data());
assert_eq!(proxy.object_ids, references);
let class = doc.classes.iter().find(|c| i32::from(c.class_number) == proxy.class_id).unwrap();
assert_eq!(class.dxf_name, "TESTCONTAINER");
assert!(!class.is_an_entity);
}
}
}
/// The database's own classes are not turned into another application's proxies; what is
/// left without a DXF form is said, not dropped silently.
#[test]
fn database_objects_without_a_form_are_reported() {
let mut doc = CadDocument::new();
doc.classes.add_or_update(DxfClass::new("BLOCKPROPERTIESTABLE", "AcDbBlockPropertiesTable"));
let handle = Handle::from(0x40u64);
doc.objects.insert(
handle,
ObjectType::Unknown {
type_name: "BLOCKPROPERTIESTABLE".into(),
handle,
owner: Handle::from(0x0Cu64),
raw_dxf_codes: None,
raw_dwg_data: Some(vec![0; 8]),
raw_dwg_handle_bits: 0,
raw_dwg_version: Some(DxfVersion::AC1032),
},
);
let notes = application_objects(&mut doc);
assert!(matches!(doc.objects.get(&handle), Some(ObjectType::Unknown { .. })));
assert_eq!(
notes,
[
"DWG: 1 objects of types CADCraft doesn't keep were left out (BLOCKPROPERTIESTABLE × 1); saving over the original file would lose them"
]
);
}
/// A record that doesn't split stays as it was and is named, never guessed at.
#[test]
fn invalid_object_records_are_rejected() {
for handle_bits in [-1, 1000] {
let raw = acadrust::entities::RawRecord { data: vec![0; 8], handle_bits, version: DxfVersion::AC1032 };
assert!(object_record(&raw, 42).is_err());
}
let mut doc = CadDocument::new();
doc.classes.add_or_update(DxfClass::new("TESTCONTAINER", "xyzDbTestContainer"));
let handle = Handle::from(0x41u64);
doc.objects.insert(
handle,
ObjectType::Unknown {
type_name: "TESTCONTAINER".into(),
handle,
owner: Handle::from(0x0Cu64),
raw_dxf_codes: None,
raw_dwg_data: Some(vec![0xFF; 4]),
raw_dwg_handle_bits: 1000,
raw_dwg_version: Some(DxfVersion::AC1032),
},
);
let notes = application_objects(&mut doc);
assert!(matches!(doc.objects.get(&handle), Some(ObjectType::Unknown { .. })));
assert!(notes.iter().any(|n| n.contains("(TESTCONTAINER × 1)")), "{notes:?}");
}
}
}
#[cfg(not(no_dwg_bridge))]