use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::io::{Cursor, Read, Seek, SeekFrom};
use std::path::Path;
use byteorder::{LittleEndian, ReadBytesExt};
use crate::error::DxfError;
use crate::io::dwg::checksum::{apply_magic_sequence, apply_mask};
use crate::io::dwg::decompressor_ac18::decompress_ac18;
use crate::io::dwg::decompressor_ac21::decompress_ac21;
use crate::io::dwg::dwg21_metadata::Dwg21CompressedMetadata;
use crate::io::dwg::dwg_version::DwgVersion;
use crate::io::dwg::parallel::{map_ordered, worker_count};
use crate::io::dwg::reed_solomon::reed_solomon_decode;
use crate::io::dwg::size_limits::{
size_from_file, MAX_DATA_PAGE_SIZE, MAX_SECTION_SIZE, MAX_SYSTEM_PAGE_SIZE,
};
use crate::io::read::{push_read_diagnostic, ReadDiagnostic, ReadStage, SourceFormat};
use crate::notification::{NotificationCollection, NotificationType};
fn report_read_error(
notifications: &mut NotificationCollection,
diagnostics: &mut Vec<ReadDiagnostic>,
code: &str,
stage: ReadStage,
section: Option<&str>,
message: String,
) {
notifications.notify(NotificationType::Error, message.clone());
let mut diagnostic = ReadDiagnostic::new(code, stage, message);
diagnostic.section = section.map(str::to_owned);
push_read_diagnostic(diagnostics, diagnostic);
}
fn parse_template(data: &[u8], version: crate::types::DxfVersion) -> Result<i16, DxfError> {
let length_bytes = data
.get(..2)
.ok_or_else(|| DxfError::InvalidFormat("Truncated Template length".into()))?;
let description_len = i16::from_le_bytes(length_bytes.try_into().unwrap());
if description_len < 0 {
return Err(DxfError::InvalidFormat(
"Negative Template description length".into(),
));
}
let character_width = if version >= crate::types::DxfVersion::AC1021 {
2
} else {
1
};
let description_bytes = (description_len as usize)
.checked_mul(character_width)
.ok_or_else(|| DxfError::InvalidFormat("Invalid Template length".into()))?;
let measurement_offset = 2usize
.checked_add(description_bytes)
.ok_or_else(|| DxfError::InvalidFormat("Invalid Template length".into()))?;
let measurement_bytes = data
.get(measurement_offset..measurement_offset + 2)
.ok_or_else(|| DxfError::InvalidFormat("Truncated Template payload".into()))?;
let measurement = i16::from_le_bytes(measurement_bytes.try_into().unwrap());
if !matches!(measurement, 0 | 1) {
return Err(DxfError::InvalidFormat(format!(
"Invalid MEASUREMENT value: {measurement}"
)));
}
Ok(measurement)
}
#[cfg(test)]
mod template_tests {
use super::parse_template;
use crate::types::DxfVersion;
#[test]
fn parses_nonempty_description_and_rejects_malformed_payloads() {
assert_eq!(
parse_template(&[3, 0, b'a', b'b', b'c', 1, 0], DxfVersion::AC1018).unwrap(),
1
);
assert_eq!(
parse_template(&[1, 0, 0, 0, 1, 0], DxfVersion::AC1032).unwrap(),
1
);
assert!(parse_template(&[], DxfVersion::AC1032).is_err());
assert!(parse_template(&[0xFF, 0xFF, 0, 0], DxfVersion::AC1032).is_err());
assert!(parse_template(&[3, 0, b'a', b'b', b'c'], DxfVersion::AC1018).is_err());
assert!(parse_template(&[0, 0, 2, 0], DxfVersion::AC1032).is_err());
}
}
const AC21_FILE_HEADER_SIZE: u64 = 0x480;
fn ac21_page_layout(
compressed_size: u64,
correction_factor: u64,
block_size: usize,
) -> Result<(usize, usize, usize), DxfError> {
let aligned = compressed_size.wrapping_add(7) & 0xFFFF_FFF8;
let total_size = aligned.wrapping_mul(correction_factor) as usize;
if total_size == 0 || total_size > 100_000_000 {
return Err(DxfError::InvalidFormat(format!(
"Invalid page buffer size: {} (compressed={}, factor={}, aligned={})",
total_size, compressed_size, correction_factor, aligned
)));
}
let factor = (total_size + block_size - 1) / block_size;
Ok((total_size, factor, factor * 255))
}
fn decode_ac21_page(
encoded: &[u8],
total_size: usize,
factor: usize,
block_size: usize,
compressed_size: u64,
uncompressed_size: u64,
) -> Vec<u8> {
let mut compressed_data = vec![0u8; total_size];
reed_solomon_decode(encoded, &mut compressed_data, factor, block_size);
if compressed_size == uncompressed_size {
compressed_data.truncate(uncompressed_size as usize);
return compressed_data;
}
let mut padded_source = vec![0u8; compressed_data.len() + 64];
padded_source[..compressed_data.len()].copy_from_slice(&compressed_data);
let mut decompressed_data = vec![0u8; uncompressed_size as usize + 64];
decompress_ac21(
&padded_source,
0,
compressed_size as u32,
&mut decompressed_data,
);
decompressed_data.truncate(uncompressed_size as usize);
decompressed_data
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DwgFileHeaderInfo {
pub version_string: String,
pub version: DwgVersion,
pub acad_maintenance_version: u8,
pub preview_address: i32,
pub dwg_version: u8,
pub app_release_version: u8,
pub code_page: u16,
pub security_type: i32,
pub summary_info_addr: i32,
pub vba_project_addr: i32,
pub ac21_metadata: Option<Dwg21CompressedMetadata>,
pub ac21_header_crc: Option<i64>,
pub ac21_unknown_key: Option<i64>,
pub ac21_compressed_data_crc: Option<i64>,
pub page_records: HashMap<i32, (i64, i64)>,
pub section_descriptors: Vec<DwgSectionInfo>,
pub section_locators: HashMap<String, (i64, i64)>,
pub objects_base_offset: i64,
pub is_ac18_format: bool,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DwgSectionInfo {
pub name: String,
pub compressed_size: u64,
pub decompressed_size: u64,
pub encrypted: u64,
pub hash_code: u64,
pub encoding: u64,
pub page_count: u64,
pub pages: Vec<DwgPageCrcInfo>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DwgPageCrcInfo {
pub page_number: i64,
pub offset: u64,
pub size: i64,
pub decompressed_size: u64,
pub compressed_size: u64,
pub checksum: u64,
pub crc: u64,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DwgReadOptions {
pub failsafe: bool,
#[cfg_attr(feature = "serde", serde(default))]
pub max_mesh_faces: Option<usize>,
#[cfg_attr(feature = "serde", serde(default))]
pub max_mesh_face_indices: Option<usize>,
}
impl Default for DwgReadOptions {
fn default() -> Self {
Self {
failsafe: false,
max_mesh_faces: None,
max_mesh_face_indices: None,
}
}
}
impl DwgReadOptions {
pub fn failsafe() -> Self {
Self {
failsafe: true,
..Self::default()
}
}
pub fn with_mesh_limits(mut self, max_faces: usize, max_face_indices: usize) -> Self {
self.max_mesh_faces = Some(max_faces);
self.max_mesh_face_indices = Some(max_face_indices);
self
}
pub fn with_unlimited_mesh(self) -> Self {
self.with_mesh_limits(usize::MAX, usize::MAX)
}
pub fn mesh_budget(&self) -> crate::io::dwg::mesh_budget::MeshFaceBudget {
crate::io::dwg::mesh_budget::MeshFaceBudget::from_system()
.with_overrides(self.max_mesh_faces, self.max_mesh_face_indices)
}
}
fn find_subsequence(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
if needle.is_empty() || haystack.len() < needle.len() {
return None;
}
(from..=haystack.len() - needle.len()).find(|&i| &haystack[i..i + needle.len()] == needle)
}
fn find_acds_magic(buf: &[u8], from: usize) -> Option<usize> {
const ACIS_MAGIC: &[u8] = b"ACIS BinaryFile";
const ASM_MAGIC: &[u8] = b"ASM BinaryFile";
let mut i = from;
while i < buf.len() {
if buf[i] == b'A' {
let rest = &buf[i..];
if rest.starts_with(ACIS_MAGIC) || rest.starts_with(ASM_MAGIC) {
return Some(i);
}
}
i += 1;
}
None
}
const ASM_END_MARKER: &[u8] = b"\x0E\x03End\x0E\x02of\x0E\x03ASM\x0D\x04data";
const ACIS_END_MARKER: &[u8] = b"End-of-ACIS-data";
const ACIS_TAGGED_END_MARKER: &[u8] = b"\x0e\x03End\x0e\x02of\x0e\x04ACIS\x0d\x04data";
fn find_acds_end(buf: &[u8], from: usize, to: usize) -> Option<(usize, usize)> {
let hay = &buf[..to.min(buf.len())];
[ASM_END_MARKER, ACIS_END_MARKER, ACIS_TAGGED_END_MARKER]
.into_iter()
.filter_map(|marker| find_subsequence(hay, marker, from).map(|e| (e, marker.len())))
.min_by_key(|(offset, _)| *offset)
}
fn extract_acds_record_blobs(buf: &[u8], modeler_handles: &HashSet<u64>) -> Vec<(u64, Vec<u8>)> {
let rd = |p: usize| -> Option<u32> {
buf.get(p..p + 4)
.map(|b| u32::from_le_bytes(b.try_into().unwrap()))
};
let marker = [0xACu8, 0xD5, 0x5F, 0x64, 0x61, 0x74, 0x61, 0x5F]; let mut out: Vec<(u64, Vec<u8>)> = Vec::new();
let mut orphan_handles: Vec<u64> = Vec::new();
let mut claimed_starts: HashSet<usize> = HashSet::new();
let mut scan = 0;
while let Some(i) = buf[scan..].windows(8).position(|w| w == marker) {
let seg = scan + i;
scan = seg + 8;
if rd(seg + 48) != Some(0x14) {
continue; }
let seg_size = buf
.get(seg + 16..seg + 24)
.map_or(0, |b| u64::from_le_bytes(b.try_into().unwrap()) as usize);
let seg_end = seg.saturating_add(seg_size).min(buf.len());
let mut recs: Vec<(u64, usize)> = Vec::new();
let mut p = seg + 48;
while rd(p) == Some(0x14) {
let (Some(handle), Some(off)) = (rd(p + 8), rd(p + 16)) else {
break;
};
recs.push((handle as u64, off as usize));
p += 20;
}
if recs.len() < 2 {
continue;
}
let table_end = seg + 48 + recs.len() * 20;
let aligned = rd(seg + 36)
.and_then(|units| seg.checked_add(units as usize * 16))
.filter(|base| *base >= table_end && *base < seg_end);
let base = aligned.unwrap_or(table_end);
for k in 0..recs.len() {
let (handle, off) = recs[k];
let region_start = base + off;
let region_end = recs.get(k + 1).map_or(seg_end, |&(_, o)| base + o);
if region_start >= seg_end || region_end <= region_start {
if modeler_handles.contains(&handle) {
orphan_handles.push(handle); }
continue;
}
let region = &buf[region_start..region_end.min(seg_end)];
let Some(mp) = region
.windows(14)
.position(|w| w == b"ASM BinaryFile")
.or_else(|| region.windows(15).position(|w| w == b"ACIS BinaryFile"))
else {
if modeler_handles.contains(&handle) {
orphan_handles.push(handle); }
continue;
};
let start = region_start + mp;
let end_bound = (region_end + ACIS_TAGGED_END_MARKER.len()).min(seg_end);
if let Some((end, marker_len)) = find_acds_end(buf, start, end_bound) {
claimed_starts.insert(start);
out.push((handle, buf[start..end + marker_len].to_vec()));
}
}
}
if !orphan_handles.is_empty() {
let pool: Vec<Vec<u8>> = extract_acds_sab_blob_ranges(buf)
.into_iter()
.filter(|(start, _)| !claimed_starts.contains(start))
.map(|(start, stop)| buf[start..stop].to_vec())
.collect();
if pool.len() == orphan_handles.len() {
out.extend(orphan_handles.into_iter().zip(pool));
}
}
out
}
fn extract_acds_sab_blob_ranges(buf: &[u8]) -> Vec<(usize, usize)> {
let mut blobs = Vec::new();
let mut pos = 0usize;
while let Some(start) = find_acds_magic(buf, pos) {
let bound = find_acds_magic(buf, start + 1).unwrap_or(buf.len());
match find_acds_end(buf, start, bound) {
Some((end, marker_len)) => {
let stop = end + marker_len;
blobs.push((start, stop));
pos = stop;
}
None => break,
}
}
blobs
}
fn extract_acds_sab_blobs(buf: &[u8]) -> Vec<Vec<u8>> {
extract_acds_sab_blob_ranges(buf)
.into_iter()
.map(|(start, stop)| buf[start..stop].to_vec())
.collect()
}
fn strip_acds_blob01_frames(blob: Vec<u8>) -> Vec<u8> {
const PREFIX: &[u8] = b"\xAC\xD5blob01";
const FRAME_LEN: usize = 80;
const PADDING: [u8; 8] = [0x55; 8];
let mut clean = Vec::with_capacity(blob.len());
let mut cursor = 0usize;
let mut scan = 0usize;
while let Some(relative) = blob[scan..].windows(PREFIX.len()).position(|w| w == PREFIX) {
let start = scan + relative;
let valid = start + FRAME_LEN <= blob.len()
&& blob.get(start + 12..start + 16) == Some(&1u32.to_le_bytes())
&& blob.get(start + 16..start + 24) == Some(&48u64.to_le_bytes())
&& blob.get(start + 40..start + 48) == Some(&PADDING);
if valid {
clean.extend_from_slice(&blob[cursor..start]);
cursor = start + FRAME_LEN;
scan = cursor;
} else {
scan = start + 1;
}
}
if cursor == 0 {
return blob;
}
clean.extend_from_slice(&blob[cursor..]);
clean
}
fn acds_fill(acis: &mut crate::entities::solid3d::AcisData, blob: Vec<u8>) {
acis.sab_data = strip_acds_blob01_frames(blob);
acis.sat_data = String::new();
acis.is_binary = true;
acis.version = crate::entities::solid3d::AcisVersion::Version2;
}
fn acds_apply(entity: &mut crate::entities::EntityType, blob: Vec<u8>) -> bool {
use crate::entities::EntityType;
match entity {
EntityType::Solid3D(s) => {
acds_fill(&mut s.acis_data, blob);
if s.point_of_reference == crate::types::Vector3::ZERO {
if let Some(p) = s
.acis_data
.geometry_centre()
.or_else(|| s.acis_data.placement_origin())
{
s.point_of_reference = p;
}
}
true
}
EntityType::Region(r) => {
acds_fill(&mut r.acis_data, blob);
if r.point_of_reference == crate::types::Vector3::ZERO {
if let Some(p) = r
.acis_data
.geometry_centre()
.or_else(|| r.acis_data.placement_origin())
{
r.point_of_reference = p;
}
}
true
}
EntityType::Body(b) => {
acds_fill(&mut b.acis_data, blob);
if b.point_of_reference == crate::types::Vector3::ZERO {
if let Some(p) = b
.acis_data
.geometry_centre()
.or_else(|| b.acis_data.placement_origin())
{
b.point_of_reference = p;
}
}
true
}
EntityType::Surface(s) => {
acds_fill(&mut s.acis_data, blob);
true
}
_ => false,
}
}
fn attach_acds_record_blobs(
document: &mut crate::document::CadDocument,
record_blobs: Vec<(u64, Vec<u8>)>,
) -> usize {
document.acis_sab_handles.clear();
let mut attached = 0usize;
for (h, blob) in record_blobs {
if let Some(entity) = document.get_entity_mut(crate::Handle::new(h)) {
if acds_apply(entity, blob) {
attached += 1;
}
}
}
attached
}
fn attach_acds_sab_blobs(
document: &mut crate::document::CadDocument,
blobs: Vec<Vec<u8>>,
) -> usize {
use crate::entities::EntityType;
let ordered = std::mem::take(&mut document.acis_sab_handles);
if !ordered.is_empty() {
let mut attached = 0usize;
for (handle, blob) in ordered.into_iter().zip(blobs.into_iter()) {
if let Some(entity) = document.get_entity_mut(handle) {
if acds_apply(entity, blob) {
attached += 1;
}
}
}
return attached;
}
let mut it = blobs.into_iter();
let mut attached = 0usize;
for entity in document.entities_mut() {
if matches!(
entity,
EntityType::Solid3D(_)
| EntityType::Region(_)
| EntityType::Body(_)
| EntityType::Surface(_)
) {
let Some(blob) = it.next() else { break };
if acds_apply(entity, blob) {
attached += 1;
}
continue;
}
}
attached
}
fn section_name_from_field(name_buf: &[u8; 64]) -> String {
let end = name_buf
.iter()
.position(|&b| b == 0)
.unwrap_or(name_buf.len());
String::from_utf8_lossy(&name_buf[..end]).into_owned()
}
fn read_t16(
cur: &mut &[u8],
utf16: bool,
encoding: crate::io::dxf::code_page::LegacyEncoding,
) -> String {
if cur.len() < 2 {
*cur = &[];
return String::new();
}
let count = u16::from_le_bytes([cur[0], cur[1]]) as usize;
*cur = &cur[2..];
if utf16 {
let bytes = count.saturating_mul(2).min(cur.len());
let units: Vec<u16> = cur[..bytes]
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
*cur = &cur[bytes..];
String::from_utf16_lossy(&units)
.trim_end_matches('\0')
.to_string()
} else {
let bytes = count.min(cur.len());
let (s, _) = encoding.decode_without_bom_handling(&cur[..bytes]);
*cur = &cur[bytes..];
crate::io::dxf::code_page::decode_legacy_escapes(s.trim_end_matches('\0'))
}
}
fn decode_field_xdata(document: &mut crate::document::CadDocument) {
let wide = document.version >= crate::types::DxfVersion::AC1021;
let fields: Vec<_> = document
.eed_by_handle
.keys()
.copied()
.filter(|h| {
matches!(
document.objects.get(h),
Some(crate::objects::ObjectType::Field(_))
)
})
.collect();
for h in fields {
let Some(blocks) = document.eed_by_handle.remove(&h) else {
continue;
};
let mut kept = Vec::new();
let mut xdata = crate::xdata::ExtendedData::new();
for (app, bytes) in blocks {
let name = document
.app_ids
.iter()
.find(|a| a.handle.value() == app)
.map(|a| a.name.clone());
match (
name,
crate::io::dwg::eed_codec::decode_values(&bytes, wide, |_| None),
) {
(Some(name), Some(values)) => {
let mut rec = crate::xdata::ExtendedDataRecord::new(name);
rec.values = values;
xdata.add_record(rec);
}
_ => kept.push((app, bytes)),
}
}
if !kept.is_empty() {
document.eed_by_handle.insert(h, kept);
}
if let Some(crate::objects::ObjectType::Field(f)) = document.objects.get_mut(&h) {
f.xdata = xdata;
}
}
}
fn decode_block_units_xdata(document: &mut crate::document::CadDocument) {
if document.version >= crate::types::DxfVersion::AC1021 {
return;
}
let Some(acad) = document.app_ids.get("ACAD").map(|app| app.handle.value()) else {
return;
};
for record in document.block_records.iter_mut() {
let Some(blocks) = document.eed_by_handle.get_mut(&record.handle) else {
continue;
};
blocks.retain(|(app, bytes)| {
let units = (*app == acad)
.then(|| crate::io::dwg::eed_codec::decode_values(bytes, false, |_| None))
.flatten()
.and_then(|values| crate::tables::block_record::design_center_units(&values));
match units {
Some(units) => {
if record.units == 0 {
record.units = units;
}
false
}
None => true,
}
});
if blocks.is_empty() {
document.eed_by_handle.remove(&record.handle);
}
}
}
fn parse_summary_info(
buf: &[u8],
utf16: bool,
encoding: crate::io::dxf::code_page::LegacyEncoding,
) -> crate::document::SummaryInfo {
let mut cur: &[u8] = buf;
let mut si = crate::document::SummaryInfo {
title: read_t16(&mut cur, utf16, encoding),
subject: read_t16(&mut cur, utf16, encoding),
author: read_t16(&mut cur, utf16, encoding),
keywords: read_t16(&mut cur, utf16, encoding),
comments: read_t16(&mut cur, utf16, encoding),
last_saved_by: read_t16(&mut cur, utf16, encoding),
revision_number: read_t16(&mut cur, utf16, encoding),
hyperlink_base: read_t16(&mut cur, utf16, encoding),
custom_properties: Vec::new(),
};
if cur.len() >= 24 {
cur = &cur[24..];
}
if cur.len() >= 2 {
let n = u16::from_le_bytes([cur[0], cur[1]]) as usize;
cur = &cur[2..];
for _ in 0..n.min(256) {
let tag = read_t16(&mut cur, utf16, encoding);
let val = read_t16(&mut cur, utf16, encoding);
if tag.is_empty() && val.is_empty() {
break;
}
si.custom_properties.push((tag, val));
}
}
si
}
pub struct DwgReader<R: Read + Seek> {
stream: R,
pub options: DwgReadOptions,
pub notifications: NotificationCollection,
source_path: Option<String>,
progress: Option<std::sync::Arc<dyn Fn(u16) + Send + Sync>>,
legacy_font_resolver: Option<std::sync::Arc<crate::io::dwg::r12::ImportFontResolver>>,
r12_code_page: Option<String>,
pending_diagnostics: Vec<ReadDiagnostic>,
}
impl DwgReader<File> {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, DxfError> {
Self::from_file_with_options(path, DwgReadOptions::default())
}
pub fn from_file_with_options<P: AsRef<Path>>(
path: P,
options: DwgReadOptions,
) -> Result<Self, DxfError> {
let path = path.as_ref();
let file = File::open(path)?;
Ok(Self {
stream: file,
options,
notifications: NotificationCollection::new(),
source_path: Some(path.to_string_lossy().into_owned()),
progress: None,
legacy_font_resolver: None,
r12_code_page: None,
pending_diagnostics: Vec::new(),
})
}
}
#[cfg(not(target_arch = "wasm32"))]
impl DwgReader<Cursor<memmap2::Mmap>> {
pub fn from_mmap<P: AsRef<Path>>(path: P) -> Result<Self, DxfError> {
let path = path.as_ref();
let file = File::open(path)?;
let map = unsafe { memmap2::MmapOptions::new().map(&file)? };
Ok(Self {
stream: Cursor::new(map),
options: DwgReadOptions::default(),
notifications: NotificationCollection::new(),
source_path: Some(path.to_string_lossy().into_owned()),
progress: None,
legacy_font_resolver: None,
r12_code_page: None,
pending_diagnostics: Vec::new(),
})
}
}
impl<R: Read + Seek> DwgReader<R> {
pub fn with_r12_code_page(mut self, code_page: impl Into<String>) -> Result<Self, DxfError> {
let code_page = code_page.into();
if crate::io::dxf::code_page::try_encoding_from_code_page(&code_page).is_none() {
return Err(DxfError::InvalidFormat(format!(
"Unsupported AC1009 code page override: {code_page}"
)));
}
self.r12_code_page = Some(code_page);
Ok(self)
}
pub fn with_legacy_font_resolver<F>(mut self, resolver: F) -> Self
where
F: Fn(&str) -> String + Send + Sync + 'static,
{
self.legacy_font_resolver = Some(std::sync::Arc::new(resolver));
self
}
pub fn from_stream(stream: R) -> Self {
Self {
stream,
options: DwgReadOptions::default(),
notifications: NotificationCollection::new(),
source_path: None,
progress: None,
legacy_font_resolver: None,
r12_code_page: None,
pending_diagnostics: Vec::new(),
}
}
pub fn from_stream_with_options(stream: R, options: DwgReadOptions) -> Self {
Self {
stream,
options,
notifications: NotificationCollection::new(),
source_path: None,
progress: None,
legacy_font_resolver: None,
r12_code_page: None,
pending_diagnostics: Vec::new(),
}
}
pub fn set_progress_callback(&mut self, progress: std::sync::Arc<dyn Fn(u16) + Send + Sync>) {
self.progress = Some(progress);
}
#[inline]
fn report_progress(&self, value: u16) {
if let Some(progress) = &self.progress {
progress(value.min(1000));
}
}
pub fn read(&mut self) -> std::result::Result<crate::document::CadDocument, DxfError> {
self.read_with_stats().map(|outcome| outcome.document)
}
fn report_section_diagnostic(
&mut self,
code: &str,
section: &str,
source_offset: Option<u64>,
message: impl Into<String>,
) {
let mut diagnostic = ReadDiagnostic::new(code, ReadStage::Section, message);
diagnostic.section = Some(section.to_owned());
diagnostic.source_offset = source_offset;
diagnostic.source_offset_basis = Some("file-byte".to_owned());
push_read_diagnostic(&mut self.pending_diagnostics, diagnostic);
}
pub fn recover(&mut self) -> std::result::Result<crate::io::recover::RecoverOutcome, DxfError> {
self.options.failsafe = true;
crate::io::recover::RecoverOutcome::from_read_outcome(self.read_with_stats()?)
}
pub fn read_visiting<F>(
&mut self,
mut visit: F,
) -> std::result::Result<crate::document::CadDocument, DxfError>
where
F: FnMut(
&crate::document::CadDocument,
crate::entities::EntityType,
) -> Option<crate::entities::EntityType>,
{
self.read_with_optional_visitor(Some(&mut visit))
.map(|outcome| outcome.document)
}
pub fn read_with_stats(
&mut self,
) -> std::result::Result<crate::io::read::ReadOutcome, DxfError> {
self.read_with_optional_visitor(
None::<
&mut dyn FnMut(
&crate::document::CadDocument,
crate::entities::EntityType,
) -> Option<crate::entities::EntityType>,
>,
)
}
fn read_with_optional_visitor(
&mut self,
visit: Option<
&mut dyn FnMut(
&crate::document::CadDocument,
crate::entities::EntityType,
) -> Option<crate::entities::EntityType>,
>,
) -> std::result::Result<crate::io::read::ReadOutcome, DxfError> {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.read_drawing(visit))) {
Ok(result) => result,
Err(payload) => Err(DxfError::Parse(format!(
"DWG reader failed: {}",
crate::io::dwg::unwind_message(payload.as_ref())
))),
}
}
fn read_drawing(
&mut self,
visit: Option<
&mut dyn FnMut(
&crate::document::CadDocument,
crate::entities::EntityType,
) -> Option<crate::entities::EntityType>,
>,
) -> std::result::Result<crate::io::read::ReadOutcome, DxfError> {
let failsafe = self.options.failsafe;
let perf = std::env::var_os("PERF").is_some();
let total_started = web_time::Instant::now();
let mut diagnostics = Vec::new();
self.report_progress(0);
self.stream.seek(SeekFrom::Start(0))?;
let mut magic = [0u8; 6];
self.stream.read_exact(&mut magic)?;
self.stream.seek(SeekFrom::Start(0))?;
if magic == *b"AC1009" || magic == *b"AD1009" {
let mut bytes = Vec::new();
self.stream.read_to_end(&mut bytes)?;
let mut document = match crate::io::dwg::r12::read_document(
&bytes,
self.legacy_font_resolver.as_deref(),
self.r12_code_page.as_deref(),
) {
Ok(document) => document,
Err(error) if failsafe => {
report_read_error(
&mut self.notifications,
&mut diagnostics,
"legacy-read-failed",
ReadStage::RecordStream,
None,
format!("Failsafe: legacy AC1009 read failed: {}", error),
);
let mut document = crate::document::CadDocument::default();
document
.notifications
.extend(std::mem::take(&mut self.notifications));
let stats = crate::io::read::ReadStats::from_document(
&document,
SourceFormat::Dwg,
1,
0,
0,
1,
true,
true,
false,
diagnostics,
);
return Ok(crate::io::read::ReadOutcome::new(document, stats));
}
Err(error) => return Err(error),
};
if let Some(visit) = visit {
let handles: Vec<_> = document
.entities()
.map(|entity| entity.common().handle)
.collect();
for handle in handles {
let Some(entity) = document.get_entity(handle).cloned() else {
continue;
};
let replacement = visit(&document, entity);
document.remove_entity(handle);
if let Some(entity) = replacement {
document.add_entity(entity)?;
}
}
}
self.report_progress(1000);
crate::document::sync_dimension_arrow_header(&mut document);
let stats = crate::io::read::ReadStats::from_document(
&document,
SourceFormat::Dwg,
1,
document.entities().count(),
document.entities().count(),
0,
true,
false,
true,
diagnostics,
);
return Ok(crate::io::read::ReadOutcome::new(document, stats));
}
let stage_started = web_time::Instant::now();
let info = match self.read_file_header() {
Ok(info) => info,
Err(e) if failsafe => {
report_read_error(
&mut self.notifications,
&mut diagnostics,
"file-header-read-failed",
ReadStage::FileHeader,
None,
format!(
"Failsafe: file header read failed, returning partial document: {}",
e
),
);
let mut doc = crate::document::CadDocument::default();
doc.notifications
.extend(std::mem::take(&mut self.notifications));
let stats = crate::io::read::ReadStats::from_document(
&doc,
SourceFormat::Dwg,
0,
0,
0,
0,
false,
true,
false,
diagnostics,
);
return Ok(crate::io::read::ReadOutcome::new(doc, stats));
}
Err(e) => return Err(e),
};
if perf {
eprintln!(
"[perf] dwg-read header={:.1}ms sections={} pages={}",
stage_started.elapsed().as_secs_f64() * 1000.0,
info.section_descriptors.len(),
info.page_records.len(),
);
}
let dxf_version = crate::types::DxfVersion::parse(&info.version_string)
.unwrap_or(crate::types::DxfVersion::Unknown);
self.report_progress(20);
let mut document = crate::document::CadDocument::with_version(dxf_version);
document.maintenance_version = info.acad_maintenance_version;
document.dwg_source_version = Some(dxf_version);
match self.get_section_buffer("AcDb:Classes", &info) {
Ok(classes_buf) => {
match crate::io::dwg::dwg_stream_readers::classes_reader::read_classes_with_encoding(
&classes_buf,
dxf_version,
info.acad_maintenance_version,
crate::io::dxf::code_page::encoding_from_dwg_code_page(info.code_page),
) {
Ok(classes) => document.classes = classes,
Err(e) => report_read_error(
&mut self.notifications,
&mut diagnostics,
"classes-decode-failed",
ReadStage::Classes,
Some("AcDb:Classes"),
format!("Failed to read classes: {}", e),
),
}
}
Err(e) => report_read_error(
&mut self.notifications,
&mut diagnostics,
"classes-extract-failed",
ReadStage::Classes,
Some("AcDb:Classes"),
format!("Failed to extract classes section: {}", e),
),
}
self.report_progress(30);
match self.get_section_buffer("AcDb:Header", &info) {
Ok(header_buf) => {
match crate::io::dwg::dwg_stream_readers::header_reader::read_header_with_encoding(
&header_buf,
dxf_version,
info.acad_maintenance_version,
crate::io::dxf::code_page::encoding_from_dwg_code_page(info.code_page),
) {
Ok(header_vars) => document.header = header_vars,
Err(e) if !failsafe => return Err(e),
Err(e) => report_read_error(
&mut self.notifications,
&mut diagnostics,
"header-decode-failed",
ReadStage::Header,
Some("AcDb:Header"),
format!("Failed to read header: {}", e),
),
}
}
Err(e) => report_read_error(
&mut self.notifications,
&mut diagnostics,
"header-extract-failed",
ReadStage::Header,
Some("AcDb:Header"),
format!("Failed to extract header section: {}", e),
),
}
document.header.code_page =
crate::io::dxf::code_page::dwg_code_page_name(info.code_page).to_string();
if let Ok(template_buf) = self.get_section_buffer("AcDb:Template", &info) {
match parse_template(&template_buf, dxf_version) {
Ok(measurement) => document.header.measurement = measurement,
Err(error) if failsafe => report_read_error(
&mut self.notifications,
&mut diagnostics,
"template-decode-failed",
ReadStage::Header,
Some("AcDb:Template"),
error.to_string(),
),
Err(error) => return Err(error),
}
}
self.report_progress(40);
let mut handles_section_read = false;
let handle_map = match self.get_section_buffer("AcDb:Handles", &info) {
Ok(handle_buf) => {
handles_section_read = true;
match crate::io::dwg::dwg_stream_readers::handle_reader::read_handles(&handle_buf) {
Ok(mut hm) => {
if info.objects_base_offset != 0 {
let base = info.objects_base_offset;
for offset in hm.values_mut() {
*offset -= base;
}
}
hm
}
Err(e) => {
report_read_error(
&mut self.notifications,
&mut diagnostics,
"handles-decode-failed",
ReadStage::Handles,
Some("AcDb:Handles"),
format!("Failed to read handles: {}", e),
);
std::collections::HashMap::new()
}
}
}
Err(e) => {
report_read_error(
&mut self.notifications,
&mut diagnostics,
"handles-extract-failed",
ReadStage::Handles,
Some("AcDb:Handles"),
format!("Failed to extract handles section: {}", e),
);
std::collections::HashMap::new()
}
};
self.report_progress(50);
let handle_count = handle_map.len();
let mut objects_section_read = false;
let mut record_stream_read = false;
let mut decoded_source_records = 0usize;
let mut skipped_source_records = 0usize;
let objects_started = web_time::Instant::now();
if !handle_map.is_empty() {
match self.get_section_buffer("AcDb:AcDbObjects", &info) {
Ok(objects_buf) => {
objects_section_read = true;
match crate::io::dwg::dwg_stream_readers::object_reader::DwgObjectReader::with_encoding(
objects_buf,
dxf_version,
handle_map,
crate::io::dxf::code_page::encoding_from_dwg_code_page(info.code_page),
) {
Ok(mut obj_reader) => {
record_stream_read = true;
obj_reader.set_mesh_budget(self.options.mesh_budget());
let mut builder = crate::io::dwg::dwg_document_builder::DwgDocumentBuilder::new(obj_reader);
builder.set_failsafe(failsafe);
if let Some(progress) = &self.progress {
builder.set_progress_callback(progress.clone());
}
let build_outcome = match visit {
Some(visit) => builder.build_with_visitor_stats(&mut document, visit),
None => builder.build_with_stats(&mut document),
};
decoded_source_records = build_outcome.decoded_records;
skipped_source_records = build_outcome.skipped_records;
if !failsafe && skipped_source_records != 0 {
return Err(DxfError::Parse(build_outcome.diagnostics.iter()
.find(|diagnostic| diagnostic.code != "auxiliary-object-preserved")
.map(|diagnostic| diagnostic.message.clone())
.unwrap_or_else(|| "Invalid DWG object records".to_string())));
}
diagnostics.extend(build_outcome.diagnostics);
self.notifications.extend(build_outcome.notifications);
},
Err(e) => report_read_error(
&mut self.notifications,
&mut diagnostics,
"objects-init-failed",
ReadStage::Objects,
Some("AcDb:AcDbObjects"),
format!("Failed to init object reader: {}", e),
),
}
}
Err(e) => report_read_error(
&mut self.notifications,
&mut diagnostics,
"objects-extract-failed",
ReadStage::Objects,
Some("AcDb:AcDbObjects"),
format!("Failed to extract objects section: {}", e),
),
}
}
self.report_progress(930);
if perf {
eprintln!(
"[perf] dwg-read objects={:.1}ms handles={} entities={} objects={}",
objects_started.elapsed().as_secs_f64() * 1000.0,
handle_count,
document.entities().count(),
document.objects.len(),
);
}
if let Ok(acds_buf) = self.get_section_buffer("AcDb:AcDsPrototype_1b", &info) {
let modeler_handles: HashSet<u64> = document
.entities()
.filter(|entity| {
matches!(
entity,
crate::entities::EntityType::Solid3D(_)
| crate::entities::EntityType::Region(_)
| crate::entities::EntityType::Body(_)
| crate::entities::EntityType::Surface(_)
) && entity.common().has_ds_data
})
.map(|entity| entity.common().handle.value())
.collect();
let record_blobs = extract_acds_record_blobs(&acds_buf, &modeler_handles);
let attached = if !record_blobs.is_empty() {
attach_acds_record_blobs(&mut document, record_blobs)
} else {
let blobs = extract_acds_sab_blobs(&acds_buf);
if blobs.is_empty() {
0
} else {
attach_acds_sab_blobs(&mut document, blobs)
}
};
let fingerprint = super::sab_fingerprint(document.entities().filter_map(|entity| {
let acis = match entity {
crate::entities::EntityType::Solid3D(entity) => &entity.acis_data,
crate::entities::EntityType::Region(entity) => &entity.acis_data,
crate::entities::EntityType::Body(entity) => &entity.acis_data,
crate::entities::EntityType::Surface(entity) => &entity.acis_data,
_ => return None,
};
(!acis.sab_data.is_empty())
.then_some((entity.common().handle, acis.sab_data.as_slice()))
}));
document.raw_acds_data = Some(std::sync::Arc::new(acds_buf));
document.raw_acds_fingerprint = fingerprint;
if attached > 0 {
self.notifications.notify(
NotificationType::Warning,
format!(
"AcDs: attached {} SAB blob(s) to modeler entities",
attached
),
);
}
}
self.report_progress(970);
if info.preview_address > 0 {
let base = info.preview_address as u64;
if self.stream.seek(SeekFrom::Start(base)).is_ok() {
let mut head = [0u8; 20];
if self.stream.read_exact(&mut head).is_ok() {
if let Some(overall) = crate::io::dwg::preview::overall_size(&head) {
if overall > 0 && overall < 64 * 1024 * 1024 {
let total = crate::io::dwg::preview::container_len(overall);
let mut buf = vec![0u8; total];
if self.stream.seek(SeekFrom::Start(base)).is_ok()
&& self.stream.read_exact(&mut buf).is_ok()
{
document.preview =
crate::io::dwg::preview::parse_preview(&buf, base);
}
}
}
}
}
}
document.source_path = self.source_path.clone();
decode_field_xdata(&mut document);
decode_block_units_xdata(&mut document);
crate::objects::restore_visual_style_roundtrip(&mut document);
crate::objects::restore_table_style_roundtrip(&mut document);
if let Ok(buf) = self.get_section_buffer("AcDb:SummaryInfo", &info) {
let utf16 = crate::io::dwg::dwg_version::DwgVersion::from_dxf_version(dxf_version)
.map(|v| v.r2007_plus())
.unwrap_or(true);
let encoding = crate::io::dxf::code_page::encoding_from_dwg_code_page(info.code_page);
document.summary_info = parse_summary_info(&buf, utf16, encoding);
}
let pre_r2004 = crate::io::dwg::dwg_version::DwgVersion::from_dxf_version(dxf_version)
.map(|v| !v.r2004_plus())
.unwrap_or(false);
if pre_r2004 {
recover_roundtrip_gradients(&mut document);
recover_mtext_bg_roundtrip(&mut document);
}
if info.preview_address > 0 {
let base = info.preview_address as u64;
if self.stream.seek(SeekFrom::Start(base)).is_ok() {
let mut head = [0u8; 20];
if self.stream.read_exact(&mut head).is_ok() {
if let Some(overall) = crate::io::dwg::preview::overall_size(&head) {
if overall > 0 && overall < 64 * 1024 * 1024 {
let total = crate::io::dwg::preview::container_len(overall);
let mut buf = vec![0u8; total];
if self.stream.seek(SeekFrom::Start(base)).is_ok()
&& self.stream.read_exact(&mut buf).is_ok()
{
document.preview =
crate::io::dwg::preview::parse_preview(&buf, base);
}
}
}
}
}
}
document.source_path = self.source_path.clone();
decode_field_xdata(&mut document);
decode_block_units_xdata(&mut document);
crate::objects::restore_visual_style_roundtrip(&mut document);
crate::objects::restore_table_style_roundtrip(&mut document);
if let Ok(buf) = self.get_section_buffer("AcDb:SummaryInfo", &info) {
let utf16 = crate::io::dwg::dwg_version::DwgVersion::from_dxf_version(dxf_version)
.map(|v| v.r2007_plus())
.unwrap_or(true);
let encoding = crate::io::dxf::code_page::encoding_from_dwg_code_page(info.code_page);
document.summary_info = parse_summary_info(&buf, utf16, encoding);
}
document
.notifications
.extend(std::mem::take(&mut self.notifications));
document.restore_constraint_vertex_ids();
if perf {
eprintln!(
"[perf] dwg-read total={:.1}ms entities={} objects={} classes={}",
total_started.elapsed().as_secs_f64() * 1000.0,
document.entities().count(),
document.objects.len(),
document.classes.iter().count(),
);
}
self.report_progress(1000);
crate::document::sync_dimension_arrow_header(&mut document);
diagnostics.extend(std::mem::take(&mut self.pending_diagnostics));
let source_sections =
usize::from(handles_section_read).saturating_add(usize::from(objects_section_read));
let stats = crate::io::read::ReadStats::from_document(
&document,
SourceFormat::Dwg,
source_sections,
handle_count,
decoded_source_records,
skipped_source_records,
record_stream_read,
failsafe,
true,
diagnostics,
);
Ok(crate::io::read::ReadOutcome::new(document, stats))
}
pub fn read_file_header(&mut self) -> Result<DwgFileHeaderInfo, DxfError> {
self.stream.seek(SeekFrom::Start(0))?;
let mut version_buf = [0u8; 6];
self.stream.read_exact(&mut version_buf)?;
let version_string = String::from_utf8_lossy(&version_buf).to_string();
let version = DwgVersion::from_version_string(&version_string)
.ok_or_else(|| DxfError::UnsupportedVersion(version_string.clone()))?;
self.notifications.notify(
NotificationType::Warning,
format!(
"Reading DWG file version: {} ({:?})",
version_string, version
),
);
let mut info = DwgFileHeaderInfo {
version_string,
version,
acad_maintenance_version: 0,
preview_address: 0,
dwg_version: 0,
app_release_version: 0,
code_page: 0,
security_type: 0,
summary_info_addr: 0,
vba_project_addr: 0,
ac21_metadata: None,
ac21_header_crc: None,
ac21_unknown_key: None,
ac21_compressed_data_crc: None,
page_records: HashMap::new(),
section_descriptors: Vec::new(),
section_locators: HashMap::new(),
objects_base_offset: 0,
is_ac18_format: false,
};
match version {
DwgVersion::AC9 => {
self.stream.seek(SeekFrom::Start(0))?;
let mut bytes = Vec::new();
self.stream.read_to_end(&mut bytes)?;
let r12 = crate::io::dwg::r12::header::read_header(&bytes)?;
info.acad_maintenance_version = r12.maintenance_version;
info.section_locators = r12
.tables
.iter()
.enumerate()
.map(|(index, table)| {
(
crate::io::dwg::r12::header::TABLE_NAMES[index].to_owned(),
(
table.address as i64,
(table.record_size as u32 * table.count as u32) as i64,
),
)
})
.collect();
info.objects_base_offset = r12.layout.entities_start as i64;
}
DwgVersion::AC21 => {
self.read_file_metadata(&mut info)?;
self.read_file_header_ac21(&mut info)?;
}
DwgVersion::AC18 | DwgVersion::AC24 => {
self.read_file_metadata(&mut info)?;
self.read_file_header_ac18(&mut info)?;
}
_ => {
self.read_file_header_ac15(&mut info)?;
}
}
Ok(info)
}
fn read_file_metadata(&mut self, info: &mut DwgFileHeaderInfo) -> Result<(), DxfError> {
let mut skip = [0u8; 5];
self.stream.read_exact(&mut skip)?;
info.acad_maintenance_version = self.stream.read_u8()?;
self.stream.read_exact(&mut [0u8; 1])?;
info.preview_address = self.stream.read_i32::<LittleEndian>()?;
info.dwg_version = self.stream.read_u8()?;
info.app_release_version = self.stream.read_u8()?;
info.code_page = self.stream.read_u16::<LittleEndian>()?;
self.stream.read_exact(&mut [0u8; 3])?;
info.security_type = self.stream.read_i32::<LittleEndian>()?;
self.stream.read_i32::<LittleEndian>()?;
info.summary_info_addr = self.stream.read_i32::<LittleEndian>()?;
info.vba_project_addr = self.stream.read_i32::<LittleEndian>()?;
self.stream.read_i32::<LittleEndian>()?;
self.stream.read_i32::<LittleEndian>()?;
let mut pad = [0u8; 80];
self.stream.read_exact(&mut pad)?;
Ok(())
}
fn read_file_header_ac15(&mut self, info: &mut DwgFileHeaderInfo) -> Result<(), DxfError> {
use crate::io::dwg::file_headers::section_definition::names;
self.stream.seek(SeekFrom::Start(6))?;
let mut pad = [0u8; 5];
self.stream.read_exact(&mut pad)?;
info.acad_maintenance_version = self.stream.read_u8()?;
let _unknown = self.stream.read_u8()?;
info.preview_address = self.stream.read_i32::<LittleEndian>()?;
let _magic1 = self.stream.read_u8()?;
let _magic2 = self.stream.read_u8()?;
info.code_page = self.stream.read_u16::<LittleEndian>()?;
let record_count = self.stream.read_i32::<LittleEndian>()?;
let section_name_for = |n: u8| -> &str {
match n {
0 => names::HEADER,
1 => names::CLASSES,
2 => names::HANDLES,
3 => names::OBJ_FREE_SPACE,
4 => names::TEMPLATE,
5 => names::AUX_HEADER,
_ => "Unknown",
}
};
let mut handles_seeker: i64 = 0;
let mut aux_header_end: i64 = 0;
let mut classes_end: i64 = 0;
for _ in 0..record_count.min(6) {
let number = self.stream.read_u8()?;
let seeker = self.stream.read_i32::<LittleEndian>()? as i64;
let size = self.stream.read_i32::<LittleEndian>()? as i64;
let name = section_name_for(number);
info.section_locators
.insert(name.to_string(), (seeker, size));
if number == 1 {
classes_end = seeker + size;
}
if number == 2 {
handles_seeker = seeker;
}
if number == 5 {
aux_header_end = seeker + size;
}
}
let objects_start: i64;
let objects_size: i64;
if classes_end > 0 && handles_seeker > classes_end {
objects_start = classes_end;
objects_size = handles_seeker - classes_end;
} else {
if aux_header_end == 0 {
let file_header_size: i64 = 0x61;
let mut offset = file_header_size;
for § in &[
names::HEADER,
names::CLASSES,
names::OBJ_FREE_SPACE,
names::TEMPLATE,
names::AUX_HEADER,
] {
if let Some(&(_, size)) = info.section_locators.get(sect) {
offset += size;
}
}
aux_header_end = offset;
}
objects_start = aux_header_end;
objects_size = handles_seeker - aux_header_end;
}
if objects_size > 0 {
info.section_locators.insert(
names::ACDB_OBJECTS.to_string(),
(objects_start, objects_size),
);
info.objects_base_offset = objects_start;
}
self.notifications.notify(
NotificationType::Warning,
format!(
"AC15 file header: {} locator records, objects at offset {}, size {}",
record_count, objects_start, objects_size
),
);
Ok(())
}
fn read_file_header_ac18(&mut self, info: &mut DwgFileHeaderInfo) -> Result<(), DxfError> {
self.stream.seek(SeekFrom::Start(0x80))?;
let mut inner = [0u8; 0x6C];
self.stream.read_exact(&mut inner)?;
apply_magic_sequence(&mut inner);
if &inner[..12] != b"AcFssFcAJMB\0" {
return Err(DxfError::InvalidFormat(
"Invalid AC18 inner file header identifier".into(),
));
}
let mut cursor = Cursor::new(&inner[..]);
cursor.set_position(0x28);
let _last_page_id = cursor.read_i32::<LittleEndian>()?;
let _last_section_addr = cursor.read_u64::<LittleEndian>()?;
let _second_header_addr = cursor.read_u64::<LittleEndian>()?;
let _gap_amount = cursor.read_u32::<LittleEndian>()?;
let _section_amount = cursor.read_u32::<LittleEndian>()?;
cursor.set_position(0x50);
let _section_page_map_id = cursor.read_u32::<LittleEndian>()?;
let page_map_address_stored = cursor.read_u64::<LittleEndian>()?;
let section_map_id = cursor.read_u32::<LittleEndian>()?;
let page_map_address = page_map_address_stored + 0x100;
info.is_ac18_format = true;
self.notifications.notify(
NotificationType::Warning,
format!(
"AC18 inner header: page_map_address={:#X}, section_map_id={}",
page_map_address, section_map_id
),
);
self.read_page_map_ac18(info, page_map_address)?;
self.read_section_map_ac18(info, section_map_id as i32)?;
Ok(())
}
fn read_page_map_ac18(
&mut self,
info: &mut DwgFileHeaderInfo,
page_map_address: u64,
) -> Result<(), DxfError> {
self.stream.seek(SeekFrom::Start(page_map_address))?;
let _section_type = self.stream.read_i32::<LittleEndian>()?;
let decomp_size = self.stream.read_i32::<LittleEndian>()?;
let comp_size = self.stream.read_i32::<LittleEndian>()?;
let compression = self.stream.read_i32::<LittleEndian>()?;
let _checksum = self.stream.read_u32::<LittleEndian>()?;
if comp_size <= 0 || comp_size > 10_000_000 {
return Err(DxfError::InvalidFormat(format!(
"Invalid AC18 page map compressed size: {}",
comp_size
)));
}
let decomp_size = size_from_file(
"AC18 page map decompressed size",
decomp_size,
1..=MAX_SYSTEM_PAGE_SIZE,
)?;
let mut compressed = vec![0u8; comp_size as usize];
self.stream.read_exact(&mut compressed)?;
let decompressed = if compression == 2 {
decompress_ac18(&compressed, decomp_size)
} else {
compressed
};
let mut cursor = Cursor::new(&decompressed);
let mut file_offset: i64 = 0x100;
while (cursor.position() as usize) + 8 <= decompressed.len() {
let page_number = cursor.read_i32::<LittleEndian>()?;
let page_size = cursor.read_i32::<LittleEndian>()?;
if page_size <= 0 {
continue;
}
if page_number > 0 {
info.page_records
.insert(page_number, (file_offset, page_size as i64));
}
if page_size > 0 {
file_offset += page_size as i64;
}
}
self.notifications.notify(
NotificationType::Warning,
format!(
"AC18: Read {} page records from page map",
info.page_records.len()
),
);
Ok(())
}
fn read_section_map_ac18(
&mut self,
info: &mut DwgFileHeaderInfo,
section_map_id: i32,
) -> Result<(), DxfError> {
let &(page_offset, _) = info.page_records.get(§ion_map_id).ok_or_else(|| {
DxfError::InvalidFormat(format!(
"AC18 section map page {} not found in page records",
section_map_id
))
})?;
self.stream.seek(SeekFrom::Start(page_offset as u64))?;
let _section_type = self.stream.read_i32::<LittleEndian>()?;
let decomp_size = self.stream.read_i32::<LittleEndian>()?;
let comp_size = self.stream.read_i32::<LittleEndian>()?;
let compression = self.stream.read_i32::<LittleEndian>()?;
let _checksum = self.stream.read_u32::<LittleEndian>()?;
if comp_size <= 0 || comp_size > 10_000_000 {
return Err(DxfError::InvalidFormat(format!(
"Invalid AC18 section map compressed size: {}",
comp_size
)));
}
let decomp_size = size_from_file(
"AC18 section map decompressed size",
decomp_size,
1..=MAX_SYSTEM_PAGE_SIZE,
)?;
let mut compressed = vec![0u8; comp_size as usize];
self.stream.read_exact(&mut compressed)?;
let decompressed = if compression == 2 {
decompress_ac18(&compressed, decomp_size)
} else {
compressed
};
let mut cursor = Cursor::new(&decompressed);
let num_descriptions = cursor.read_i32::<LittleEndian>()?;
let _marker = cursor.read_i32::<LittleEndian>()?; let _max_decomp = cursor.read_i32::<LittleEndian>()?; let _unknown = cursor.read_i32::<LittleEndian>()?; let _num_desc2 = cursor.read_i32::<LittleEndian>()?;
for _ in 0..num_descriptions {
let data_size = cursor.read_u64::<LittleEndian>()?;
let page_count = cursor.read_i32::<LittleEndian>()?;
let max_decomp_page_size = cursor.read_i32::<LittleEndian>()?;
let _unknown = cursor.read_i32::<LittleEndian>()?;
let compressed_code = cursor.read_i32::<LittleEndian>()?;
let _section_id = cursor.read_i32::<LittleEndian>()?;
let encrypted = cursor.read_i32::<LittleEndian>()?;
let mut name_buf = [0u8; 64];
cursor.read_exact(&mut name_buf)?;
let name = section_name_from_field(&name_buf);
let mut pages = Vec::new();
for _ in 0..page_count {
let page_number = cursor.read_i32::<LittleEndian>()?;
let page_compressed_size = cursor.read_i32::<LittleEndian>()?;
let page_offset_in_section = cursor.read_u64::<LittleEndian>()?;
pages.push(DwgPageCrcInfo {
page_number: page_number as i64,
offset: page_offset_in_section,
size: 0,
decompressed_size: max_decomp_page_size as u64,
compressed_size: page_compressed_size as u64,
checksum: 0,
crc: 0,
});
}
if !name.is_empty() {
info.section_descriptors.push(DwgSectionInfo {
name: name.clone(),
compressed_size: data_size,
decompressed_size: max_decomp_page_size as u64,
encrypted: encrypted as u64,
hash_code: 0,
encoding: compressed_code as u64,
page_count: page_count as u64,
pages,
});
}
}
self.notifications.notify(
NotificationType::Warning,
format!(
"AC18: Read {} section descriptors from section map",
info.section_descriptors.len()
),
);
Ok(())
}
fn read_file_header_ac21(&mut self, info: &mut DwgFileHeaderInfo) -> Result<(), DxfError> {
let mut compressed_data = [0u8; 0x400];
self.stream.read_exact(&mut compressed_data)?;
let mut decoded_data = vec![0u8; 3 * 239]; reed_solomon_decode(&compressed_data, &mut decoded_data, 3, 239);
let mut cursor = Cursor::new(&decoded_data);
let crc = cursor.read_i64::<LittleEndian>()?;
let unknown_key = cursor.read_i64::<LittleEndian>()?;
let compressed_data_crc = cursor.read_i64::<LittleEndian>()?;
let compr_len = cursor.read_i32::<LittleEndian>()?;
let _length2 = cursor.read_i32::<LittleEndian>()?;
info.ac21_header_crc = Some(crc);
info.ac21_unknown_key = Some(unknown_key);
info.ac21_compressed_data_crc = Some(compressed_data_crc);
self.notifications.notify(
NotificationType::Warning,
format!(
"AC1021 header: CRC={:#018X}, UnknownKey={:#018X}, CompressedDataCRC={:#018X}, ComprLen={}",
crc as u64, unknown_key as u64, compressed_data_crc as u64, compr_len
),
);
let mut metadata_buffer = vec![0u8; 0x110];
if compr_len < 0 {
let raw_len = (-compr_len) as usize;
let src_start = 32; let copy_len = raw_len
.min(0x110)
.min(decoded_data.len().saturating_sub(src_start));
metadata_buffer[..copy_len]
.copy_from_slice(&decoded_data[src_start..src_start + copy_len]);
} else {
decompress_ac21(&decoded_data, 32, compr_len as u32, &mut metadata_buffer);
}
let metadata = Dwg21CompressedMetadata::from_bytes(&metadata_buffer)?;
self.notifications.notify(
NotificationType::Warning,
format!(
"AC1021 Header CRC-64 extracted: {:#018X}",
metadata.header_crc64
),
);
self.notifications.notify(
NotificationType::Warning,
format!(
"AC1021 CRC Seeds: CrcSeed={:#018X}, CrcSeedEncoded={:#018X}, RandomSeed={:#018X}",
metadata.crc_seed, metadata.crc_seed_encoded, metadata.random_seed
),
);
self.notifications.notify(
NotificationType::Warning,
format!(
"AC1021 Pages Map CRC: compressed={:#018X}, uncompressed={:#018X}, seed={:#018X}",
metadata.pages_map_crc_compressed,
metadata.pages_map_crc_uncompressed,
metadata.pages_map_crc_seed
),
);
self.notifications.notify(
NotificationType::Warning,
format!(
"AC1021 Sections Map CRC: compressed={:#018X}, uncompressed={:#018X}, seed={:#018X}",
metadata.sections_map_crc_compressed,
metadata.sections_map_crc_uncompressed,
metadata.sections_map_crc_seed
),
);
self.read_page_map_ac21(info, &metadata)?;
self.read_section_map_ac21(info, &metadata)?;
info.ac21_metadata = Some(metadata.clone());
Ok(())
}
fn read_page_map_ac21(
&mut self,
info: &mut DwgFileHeaderInfo,
metadata: &Dwg21CompressedMetadata,
) -> Result<(), DxfError> {
size_from_file(
"AC21 page map compressed size",
metadata.pages_map_size_compressed,
1..=MAX_SYSTEM_PAGE_SIZE,
)?;
size_from_file(
"AC21 page map decompressed size",
metadata.pages_map_size_uncompressed,
1..=MAX_SYSTEM_PAGE_SIZE,
)?;
let page_buffer = self.get_page_buffer(
metadata.pages_map_offset,
metadata.pages_map_size_compressed,
metadata.pages_map_size_uncompressed,
metadata.pages_map_correction_factor,
0xEF,
)?;
let mut cursor = Cursor::new(&page_buffer);
let mut offset: i64 = 0;
while (cursor.position() as usize) < page_buffer.len() {
let size = cursor.read_i64::<LittleEndian>()?;
let id = cursor.read_i64::<LittleEndian>()?;
if size == 0 && id == 0 {
break;
}
let ind = id.unsigned_abs();
info.page_records.insert(ind as i32, (offset, size));
offset += size;
}
self.notifications.notify(
NotificationType::Warning,
format!(
"AC1021: Read {} page records from page map",
info.page_records.len()
),
);
Ok(())
}
fn read_section_map_ac21(
&mut self,
info: &mut DwgFileHeaderInfo,
metadata: &Dwg21CompressedMetadata,
) -> Result<(), DxfError> {
let sections_map_id = metadata.sections_map_id as i32;
let seeker = info
.page_records
.get(§ions_map_id)
.map(|&(offset, _)| offset)
.ok_or_else(|| {
DxfError::InvalidFormat(format!(
"Section map page ID {} not found in page records",
sections_map_id
))
})?;
size_from_file(
"AC21 section map compressed size",
metadata.sections_map_size_compressed,
1..=MAX_SYSTEM_PAGE_SIZE,
)?;
size_from_file(
"AC21 section map decompressed size",
metadata.sections_map_size_uncompressed,
1..=MAX_SYSTEM_PAGE_SIZE,
)?;
let section_buffer = self.get_page_buffer_at(
seeker as u64,
metadata.sections_map_size_compressed,
metadata.sections_map_size_uncompressed,
metadata.sections_map_correction_factor,
239,
)?;
let mut cursor = Cursor::new(§ion_buffer);
while (cursor.position() as usize) < section_buffer.len() {
if section_buffer.len() - (cursor.position() as usize) < 64 {
break;
}
let compressed_size = cursor.read_u64::<LittleEndian>()?;
let decompressed_size = cursor.read_u64::<LittleEndian>()?;
let encrypted = cursor.read_u64::<LittleEndian>()?;
let hash_code = cursor.read_u64::<LittleEndian>()?;
let section_name_length = cursor.read_i64::<LittleEndian>()?;
let _unknown = cursor.read_u64::<LittleEndian>()?;
let encoding = cursor.read_u64::<LittleEndian>()?;
let page_count = cursor.read_u64::<LittleEndian>()?;
let name = if section_name_length > 0 {
let byte_len = section_name_length as usize;
if cursor.position() as usize + byte_len > section_buffer.len() {
break;
}
let mut name_bytes = vec![0u8; byte_len];
cursor.read_exact(&mut name_bytes)?;
let words: Vec<u16> = name_bytes
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
String::from_utf16_lossy(&words)
.trim_end_matches('\0')
.to_string()
} else {
String::new()
};
let mut pages = Vec::new();
for _ in 0..page_count {
if section_buffer.len() - (cursor.position() as usize) < 56 {
break;
}
let page_offset = cursor.read_u64::<LittleEndian>()?;
let page_size = cursor.read_i64::<LittleEndian>()?;
let page_number = cursor.read_i64::<LittleEndian>()?;
let page_decompressed_size = cursor.read_u64::<LittleEndian>()?;
let page_compressed_size = cursor.read_u64::<LittleEndian>()?;
let page_checksum = cursor.read_u64::<LittleEndian>()?;
let page_crc = cursor.read_u64::<LittleEndian>()?;
self.notifications.notify(
NotificationType::Warning,
format!(
" Section '{}' page {}: CRC={:#018X}, Checksum={:#018X}, CompSize={}, DecompSize={}",
name, page_number, page_crc, page_checksum,
page_compressed_size, page_decompressed_size
),
);
pages.push(DwgPageCrcInfo {
page_number,
offset: page_offset,
size: page_size,
decompressed_size: page_decompressed_size,
compressed_size: page_compressed_size,
checksum: page_checksum,
crc: page_crc,
});
}
if section_name_length > 0 {
info.section_descriptors.push(DwgSectionInfo {
name: name.clone(),
compressed_size,
decompressed_size,
encrypted,
hash_code,
encoding,
page_count,
pages,
});
}
}
self.notifications.notify(
NotificationType::Warning,
format!(
"AC1021: Read {} section descriptors from section map",
info.section_descriptors.len()
),
);
Ok(())
}
fn get_page_buffer(
&mut self,
page_offset: u64,
compressed_size: u64,
uncompressed_size: u64,
correction_factor: u64,
block_size: usize,
) -> Result<Vec<u8>, DxfError> {
self.get_page_buffer_at(
page_offset,
compressed_size,
uncompressed_size,
correction_factor,
block_size,
)
}
fn get_page_buffer_at(
&mut self,
page_offset: u64,
compressed_size: u64,
uncompressed_size: u64,
correction_factor: u64,
block_size: usize,
) -> Result<Vec<u8>, DxfError> {
let (total_size, factor, read_length) =
ac21_page_layout(compressed_size, correction_factor, block_size)?;
self.stream
.seek(SeekFrom::Start(AC21_FILE_HEADER_SIZE + page_offset))?;
let mut encoded_buffer = vec![0u8; read_length];
let bytes_read = self.stream.read(&mut encoded_buffer)?;
if bytes_read < read_length {
encoded_buffer[bytes_read..].fill(0);
}
Ok(decode_ac21_page(
&encoded_buffer,
total_size,
factor,
block_size,
compressed_size,
uncompressed_size,
))
}
pub fn get_section_buffer(
&mut self,
section_name: &str,
info: &DwgFileHeaderInfo,
) -> Result<Vec<u8>, DxfError> {
let failsafe = self.options.failsafe;
let perf = std::env::var_os("PERF").is_some();
let started = web_time::Instant::now();
if !info.section_locators.is_empty() {
if let Some(&(offset, size)) = info.section_locators.get(section_name) {
if offset < 0 || size <= 0 {
return Err(DxfError::Parse(format!(
"Section '{}' has invalid locator offset/size ({offset}, {size})",
section_name
)));
}
let requested_size = usize::try_from(size).map_err(|_| {
DxfError::Parse(format!(
"Section '{}' size {} does not fit in memory",
section_name, size
))
})?;
let file_len = self.stream.seek(SeekFrom::End(0))?;
let available = file_len.saturating_sub(offset as u64);
if !failsafe && requested_size as u64 > available {
return Err(DxfError::Parse(format!(
"Section '{}' size {} runs past the end of the file",
section_name, requested_size
)));
}
self.stream.seek(SeekFrom::Start(offset as u64))?;
let mut buf = vec![0u8; requested_size.min(available as usize)];
if failsafe {
let mut filled = 0usize;
let mut read_error = None;
while filled < buf.len() {
match self.stream.read(&mut buf[filled..]) {
Ok(0) => break,
Ok(count) => filled += count,
Err(error) => {
read_error = Some(error);
break;
}
}
}
if filled != requested_size || read_error.is_some() {
let detail =
read_error
.map(|error| error.to_string())
.unwrap_or_else(|| {
format!(
"unexpected end of file after {} of {} bytes",
filled, requested_size
)
});
buf.truncate(filled);
self.notifications.notify(
NotificationType::Warning,
format!(
"Failsafe: truncated AC15 section '{}' at byte {}: {}",
section_name,
offset.saturating_add(filled as i64),
detail
),
);
self.report_section_diagnostic(
"section-read-truncated",
section_name,
Some(offset.saturating_add(filled as i64) as u64),
format!(
"AC15 section read stopped after {} of {} bytes: {}",
filled, requested_size, detail
),
);
}
} else {
self.stream.read_exact(&mut buf)?;
}
if perf {
eprintln!(
"[perf] dwg-section name={} format=ac15 time={:.1}ms bytes={}",
section_name,
started.elapsed().as_secs_f64() * 1000.0,
buf.len(),
);
}
return Ok(buf);
} else {
return Err(DxfError::Parse(format!(
"Section '{}' not found in AC15 locator records",
section_name
)));
}
}
if info.is_ac18_format {
let result = self.get_section_buffer_ac18(section_name, info);
if perf {
eprintln!(
"[perf] dwg-section name={} format=ac18 time={:.1}ms bytes={}",
section_name,
started.elapsed().as_secs_f64() * 1000.0,
result.as_ref().map_or(0, Vec::len),
);
}
return result;
}
let section = info
.section_descriptors
.iter()
.find(|s| s.name == section_name)
.ok_or_else(|| {
DxfError::Parse(format!("Section '{}' not found in file", section_name))
})?;
let total_size = size_from_file(
format_args!("AC21 section '{}' size", section_name),
section.compressed_size,
0..=MAX_SECTION_SIZE,
)?;
let mut page_total = 0u64;
for page in §ion.pages {
size_from_file(
format_args!(
"AC21 section '{}' page {} decompressed size",
section_name, page.page_number
),
page.decompressed_size,
0..=MAX_DATA_PAGE_SIZE,
)?;
size_from_file(
format_args!(
"AC21 section '{}' page {} compressed size",
section_name, page.page_number
),
page.compressed_size,
0..=MAX_DATA_PAGE_SIZE,
)?;
page_total = page_total.saturating_add(page.decompressed_size);
}
let page_total = size_from_file(
format_args!("AC21 section '{}' page total", section_name),
page_total,
0..=MAX_SECTION_SIZE,
)?;
let mut result = Vec::with_capacity(total_size.min(page_total));
let encoding = section.encoding;
let block_size: usize = 251;
enum PagePayload {
Ready(Vec<u8>),
Encoded {
bytes: Vec<u8>,
total_size: usize,
factor: usize,
compressed_size: u64,
uncompressed_size: u64,
},
Failed(String),
}
struct PreparedPage {
number: i64,
fill_size: usize,
payload: PagePayload,
}
let batch_pages = worker_count().saturating_mul(2);
let mut skipped_pages = 0u32;
for page_batch in section.pages.chunks(batch_pages) {
let mut prepared = Vec::with_capacity(page_batch.len());
for page in page_batch {
let fill_size = page.decompressed_size as usize;
let payload = match info.page_records.get(&(page.page_number as i32)) {
Some(&(page_offset, _)) if encoding == 1 => {
let read = (|| -> Result<Vec<u8>, DxfError> {
self.stream.seek(SeekFrom::Start(
AC21_FILE_HEADER_SIZE + page_offset as u64,
))?;
let mut bytes = vec![0u8; fill_size];
self.stream.read_exact(&mut bytes)?;
Ok(bytes)
})();
match read {
Ok(bytes) => PagePayload::Ready(bytes),
Err(error) if failsafe => PagePayload::Failed(error.to_string()),
Err(error) => return Err(error),
}
}
Some(&(page_offset, _)) => {
match ac21_page_layout(page.compressed_size, 1, block_size) {
Ok((total_size, factor, read_length)) => {
let read = (|| -> Result<Vec<u8>, DxfError> {
self.stream.seek(SeekFrom::Start(
AC21_FILE_HEADER_SIZE + page_offset as u64,
))?;
let mut bytes = vec![0u8; read_length];
let read = self.stream.read(&mut bytes)?;
bytes[read..].fill(0);
Ok(bytes)
})();
match read {
Ok(bytes) => PagePayload::Encoded {
bytes,
total_size,
factor,
compressed_size: page.compressed_size,
uncompressed_size: page.decompressed_size,
},
Err(error) if failsafe => {
PagePayload::Failed(error.to_string())
}
Err(error) => return Err(error),
}
}
Err(error) if failsafe => PagePayload::Failed(error.to_string()),
Err(error) => return Err(error),
}
}
None if failsafe => PagePayload::Failed(format!(
"page {} not found in page map",
page.page_number
)),
None => {
return Err(DxfError::Parse(format!(
"Page {} not found in page map",
page.page_number
)))
}
};
prepared.push(PreparedPage {
number: page.page_number,
fill_size,
payload,
});
}
let decoded = map_ordered(prepared, |page| {
let data = match page.payload {
PagePayload::Ready(bytes) => Ok(bytes),
PagePayload::Encoded {
bytes,
total_size,
factor,
compressed_size,
uncompressed_size,
} => Ok(decode_ac21_page(
&bytes,
total_size,
factor,
block_size,
compressed_size,
uncompressed_size,
)),
PagePayload::Failed(error) => Err(error),
};
(page.number, page.fill_size, data)
});
for (page_number, fill_size, page_result) in decoded {
match page_result {
Ok(page_data) => result.extend_from_slice(&page_data),
Err(error) => {
skipped_pages += 1;
self.notifications.notify(
NotificationType::Error,
format!(
"Failsafe: skipped corrupt page {} in section '{}': {}",
page_number, section_name, error
),
);
result.resize(result.len() + fill_size, 0);
}
}
}
}
if skipped_pages > 0 {
self.notifications.notify(
NotificationType::Warning,
format!(
"Failsafe: {} of {} pages skipped in section '{}'",
skipped_pages,
section.pages.len(),
section_name
),
);
}
result.truncate(total_size);
if perf {
eprintln!(
"[perf] dwg-section name={} format=ac21 time={:.1}ms bytes={} pages={}",
section_name,
started.elapsed().as_secs_f64() * 1000.0,
result.len(),
section.pages.len(),
);
}
Ok(result)
}
fn get_section_buffer_ac18(
&mut self,
section_name: &str,
info: &DwgFileHeaderInfo,
) -> Result<Vec<u8>, DxfError> {
let failsafe = self.options.failsafe;
let section = info
.section_descriptors
.iter()
.find(|s| s.name == section_name)
.ok_or_else(|| {
DxfError::Parse(format!("Section '{}' not found in AC18 file", section_name))
})?;
let total_size = size_from_file(
format_args!("AC18 section '{}' size", section_name),
section.compressed_size,
0..=MAX_SECTION_SIZE,
)?;
let is_compressed = section.encoding == 2;
let max_page_size = size_from_file(
format_args!("AC18 section '{}' page size", section_name),
section.decompressed_size,
0..=MAX_DATA_PAGE_SIZE,
)?;
let page_count = section.pages.len();
let page_capacity = page_count.saturating_mul(max_page_size);
if total_size > page_capacity {
let hard_limit = page_capacity.saturating_mul(2);
if page_capacity == 0 || total_size > hard_limit {
return Err(DxfError::Parse(format!(
"AC18 section '{}' size {} exceeds its {} page(s) of {} bytes",
section_name, total_size, page_count, max_page_size
)));
}
self.notifications.notify(
NotificationType::Warning,
format!(
"AC18 section '{}' declared size {} is slightly larger than its {} page(s) of {} bytes; retaining the bounded section",
section_name, total_size, page_count, max_page_size
),
);
}
let mut result = vec![0u8; total_size];
let batch_pages = worker_count().saturating_mul(2);
let mut skipped_pages = 0usize;
for page_batch in section.pages.chunks(batch_pages) {
let mut prepared = Vec::with_capacity(page_batch.len());
for page in page_batch {
let page_number = page.page_number as i32;
let page_result = (|| -> Result<(usize, Vec<u8>), DxfError> {
let &(page_file_offset, _page_total_size) =
info.page_records.get(&page_number).ok_or_else(|| {
DxfError::Parse(format!(
"AC18 page {} not found in page records for section '{}'",
page_number, section_name
))
})?;
if page_file_offset < 0 {
return Err(DxfError::Parse(format!(
"AC18 page {} has a negative file offset {}",
page_number, page_file_offset
)));
}
self.stream.seek(SeekFrom::Start(page_file_offset as u64))?;
let mut header = [0u8; 32];
self.stream.read_exact(&mut header)?;
apply_mask(&mut header, page_file_offset as u64);
let mut hcursor = Cursor::new(&header[..]);
let _section_type = hcursor.read_i32::<LittleEndian>()?;
let _section_id = hcursor.read_i32::<LittleEndian>()?;
let data_compressed_size = hcursor.read_i32::<LittleEndian>()?;
let _page_size = hcursor.read_i32::<LittleEndian>()?;
let data_offset = hcursor.read_i64::<LittleEndian>()?;
if data_compressed_size <= 0 || data_compressed_size > 10_000_000 {
return Err(DxfError::Parse(format!(
"invalid compressed size {}",
data_compressed_size
)));
}
if data_offset < 0 {
return Err(DxfError::Parse(format!(
"negative destination offset {}",
data_offset
)));
}
let mut compressed = vec![0u8; data_compressed_size as usize];
self.stream.read_exact(&mut compressed)?;
Ok((data_offset as usize, compressed))
})();
match page_result {
Ok(page) => prepared.push(page),
Err(error) if failsafe => {
skipped_pages += 1;
let page_offset = info
.page_records
.get(&page_number)
.and_then(|(offset, _)| (*offset >= 0).then_some(*offset as u64));
self.notifications.notify(
NotificationType::Warning,
format!(
"Failsafe: skipped corrupt AC18 page {} in section '{}': {}",
page_number, section_name, error
),
);
self.report_section_diagnostic(
"section-page-skipped",
section_name,
page_offset,
format!(
"AC18 page {} skipped while reading section '{}': {}",
page_number, section_name, error
),
);
}
Err(error) => return Err(error),
}
}
let decoded = map_ordered(prepared, |(dst_start, compressed)| {
let decompressed = if is_compressed {
decompress_ac18(&compressed, max_page_size)
} else {
compressed
};
(dst_start, decompressed)
});
for (dst_start, decompressed) in decoded {
if dst_start < total_size {
let copy_len = decompressed.len().min(total_size - dst_start);
result[dst_start..dst_start + copy_len]
.copy_from_slice(&decompressed[..copy_len]);
}
}
}
if skipped_pages > 0 {
self.notifications.notify(
NotificationType::Warning,
format!(
"Failsafe: {} of {} pages skipped in AC18 section '{}'",
skipped_pages,
section.pages.len(),
section_name
),
);
}
Ok(result)
}
pub fn find_section<'a>(info: &'a DwgFileHeaderInfo, name: &str) -> Option<&'a DwgSectionInfo> {
info.section_descriptors.iter().find(|s| s.name == name)
}
pub fn extract_all_crcs(&mut self) -> Result<CrcExtractionReport, DxfError> {
let info = self.read_file_header()?;
let mut report = CrcExtractionReport {
version: info.version_string.clone(),
header_crc64: None,
header_crc: info.ac21_header_crc,
compressed_data_crc: info.ac21_compressed_data_crc,
pages_map_crc_compressed: None,
pages_map_crc_uncompressed: None,
pages_map_crc_seed: None,
sections_map_crc_compressed: None,
sections_map_crc_uncompressed: None,
sections_map_crc_seed: None,
crc_seed: None,
crc_seed_encoded: None,
random_seed: None,
page_crcs: Vec::new(),
notifications: std::mem::take(&mut self.notifications),
};
if let Some(ref metadata) = info.ac21_metadata {
report.header_crc64 = Some(metadata.header_crc64);
report.pages_map_crc_compressed = Some(metadata.pages_map_crc_compressed);
report.pages_map_crc_uncompressed = Some(metadata.pages_map_crc_uncompressed);
report.pages_map_crc_seed = Some(metadata.pages_map_crc_seed);
report.sections_map_crc_compressed = Some(metadata.sections_map_crc_compressed);
report.sections_map_crc_uncompressed = Some(metadata.sections_map_crc_uncompressed);
report.sections_map_crc_seed = Some(metadata.sections_map_crc_seed);
report.crc_seed = Some(metadata.crc_seed);
report.crc_seed_encoded = Some(metadata.crc_seed_encoded);
report.random_seed = Some(metadata.random_seed);
}
for section in &info.section_descriptors {
for page in §ion.pages {
report.page_crcs.push(PageCrcEntry {
section_name: section.name.clone(),
page_number: page.page_number,
crc: page.crc,
checksum: page.checksum,
compressed_size: page.compressed_size,
decompressed_size: page.decompressed_size,
});
}
}
Ok(report)
}
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CrcExtractionReport {
pub version: String,
pub header_crc64: Option<u64>,
pub header_crc: Option<i64>,
pub compressed_data_crc: Option<i64>,
pub pages_map_crc_compressed: Option<u64>,
pub pages_map_crc_uncompressed: Option<u64>,
pub pages_map_crc_seed: Option<u64>,
pub sections_map_crc_compressed: Option<u64>,
pub sections_map_crc_uncompressed: Option<u64>,
pub sections_map_crc_seed: Option<u64>,
pub crc_seed: Option<u64>,
pub crc_seed_encoded: Option<u64>,
pub random_seed: Option<u64>,
pub page_crcs: Vec<PageCrcEntry>,
pub notifications: NotificationCollection,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PageCrcEntry {
pub section_name: String,
pub page_number: i64,
pub crc: u64,
pub checksum: u64,
pub compressed_size: u64,
pub decompressed_size: u64,
}
impl std::fmt::Display for CrcExtractionReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "=== DWG CRC Extraction Report ===")?;
writeln!(f, "Version: {}", self.version)?;
writeln!(f)?;
if let Some(crc64) = self.header_crc64 {
writeln!(f, "--- Header CRC-64 ---")?;
writeln!(f, " Header CRC-64: {:#018X}", crc64)?;
}
if let Some(crc) = self.header_crc {
writeln!(f, " Header CRC (RS decoded): {:#018X}", crc as u64)?;
}
if let Some(crc) = self.compressed_data_crc {
writeln!(f, " Compressed Data CRC: {:#018X}", crc as u64)?;
}
if self.crc_seed.is_some() {
writeln!(f)?;
writeln!(f, "--- CRC Seeds ---")?;
if let Some(v) = self.crc_seed {
writeln!(f, " CRC Seed: {:#018X}", v)?;
}
if let Some(v) = self.crc_seed_encoded {
writeln!(f, " CRC Seed Encoded: {:#018X}", v)?;
}
if let Some(v) = self.random_seed {
writeln!(f, " Random Seed: {:#018X}", v)?;
}
}
if self.pages_map_crc_compressed.is_some() {
writeln!(f)?;
writeln!(f, "--- Pages Map CRC ---")?;
if let Some(v) = self.pages_map_crc_compressed {
writeln!(f, " Compressed: {:#018X}", v)?;
}
if let Some(v) = self.pages_map_crc_uncompressed {
writeln!(f, " Uncompressed: {:#018X}", v)?;
}
if let Some(v) = self.pages_map_crc_seed {
writeln!(f, " Seed: {:#018X}", v)?;
}
}
if self.sections_map_crc_compressed.is_some() {
writeln!(f)?;
writeln!(f, "--- Sections Map CRC ---")?;
if let Some(v) = self.sections_map_crc_compressed {
writeln!(f, " Compressed: {:#018X}", v)?;
}
if let Some(v) = self.sections_map_crc_uncompressed {
writeln!(f, " Uncompressed: {:#018X}", v)?;
}
if let Some(v) = self.sections_map_crc_seed {
writeln!(f, " Seed: {:#018X}", v)?;
}
}
if !self.page_crcs.is_empty() {
writeln!(f)?;
writeln!(f, "--- Per-Page CRC Values ---")?;
for entry in &self.page_crcs {
writeln!(
f,
" {} [page {}]: CRC={:#018X}, Checksum={:#018X} (comp={}, decomp={})",
entry.section_name,
entry.page_number,
entry.crc,
entry.checksum,
entry.compressed_size,
entry.decompressed_size
)?;
}
}
Ok(())
}
}
#[cfg(test)]
mod dwg_reader_tests {
use super::{extract_acds_record_blobs, section_name_from_field, ACIS_END_MARKER};
use std::collections::HashSet;
fn field(prefix: &[u8]) -> [u8; 64] {
let mut b = [0u8; 64];
b[..prefix.len()].copy_from_slice(prefix);
b
}
#[test]
fn clean_zero_padded_name() {
assert_eq!(
section_name_from_field(&field(b"AcDb:Handles")),
"AcDb:Handles"
);
}
#[test]
fn stops_at_first_null_ignoring_trailing_garbage() {
let mut b = field(b"AcDb:Handles");
b[13] = b't';
b[14] = 0x01;
b[20] = b'X';
assert_eq!(section_name_from_field(&b), "AcDb:Handles");
}
#[test]
fn empty_when_first_byte_null() {
assert_eq!(section_name_from_field(&[0u8; 64]), "");
}
#[test]
fn acds_record_table_recovers_a_trailing_external_blob_segment() {
const HEADER_SIZE: usize = 48;
const RECORD_SIZE: usize = 20;
const RECORDS: usize = 3;
fn blob(label: u8) -> Vec<u8> {
let mut value = b"ASM BinaryFile".to_vec();
value.push(label);
value.extend_from_slice(ACIS_END_MARKER);
value
}
let external = blob(3);
let physical = [(0x91u32, blob(1)), (0x92, blob(2))];
let mut offsets = vec![(0x89u32, 0usize)];
let mut data = vec![0u8; 44];
for (handle, value) in &physical {
offsets.push((*handle, data.len()));
data.extend_from_slice(value);
}
let base = HEADER_SIZE + RECORD_SIZE * RECORDS;
let segment_size = base + data.len();
let external_segment = segment_size;
let external_header_size = 64;
let mut buffer = vec![0u8; segment_size + external_header_size + external.len()];
buffer[..8].copy_from_slice(b"\xAC\xD5_data_");
buffer[16..24].copy_from_slice(&(segment_size as u64).to_le_bytes());
buffer[external_segment..external_segment + 8].copy_from_slice(b"\xAC\xD5_data_");
buffer[external_segment + 16..external_segment + 24]
.copy_from_slice(&((external_header_size + external.len()) as u64).to_le_bytes());
for (index, handle) in [0x89u32, 0x91, 0x92].into_iter().enumerate() {
let entry = HEADER_SIZE + index * RECORD_SIZE;
let offset = offsets
.iter()
.find(|(candidate, _)| *candidate == handle)
.unwrap()
.1;
buffer[entry..entry + 4].copy_from_slice(&0x14u32.to_le_bytes());
buffer[entry + 8..entry + 12].copy_from_slice(&handle.to_le_bytes());
buffer[entry + 16..entry + 20].copy_from_slice(&(offset as u32).to_le_bytes());
}
buffer[base..segment_size].copy_from_slice(&data);
buffer[external_segment + external_header_size..].copy_from_slice(&external);
let handles = HashSet::from([0x89u64, 0x91, 0x92]);
let extracted = extract_acds_record_blobs(&buffer, &handles);
assert_eq!(extracted.len(), RECORDS);
for (handle, expected) in physical.into_iter().chain([(0x89, external)]) {
assert_eq!(
extracted
.iter()
.find(|(candidate, _)| *candidate == handle as u64)
.unwrap()
.1,
expected,
);
}
}
}
pub(crate) fn recover_roundtrip_gradients(document: &mut crate::document::CadDocument) {
use crate::entities::EntityType;
use crate::objects::ObjectType;
use crate::types::{Color, Handle};
const GRADIENT_NAMES: &[&str] = &[
"SPHERICAL",
"HEMISPHERICAL",
"CURVED",
"CYLINDER",
"INVSPHERICAL",
"INVHEMISPHERICAL",
"INVCURVED",
"INVCYLINDER",
"LINEAR",
];
let mut recovered: Vec<(Handle, u8, u8, String)> = Vec::new();
for e in document.entities() {
let EntityType::Hatch(h) = e else { continue };
if h.gradient_color.enabled || !h.is_solid {
continue;
}
let eed = &h.common.extended_data;
let aci = |app: &str| -> Option<u8> {
eed.records()
.iter()
.find(|r| r.application_name.eq_ignore_ascii_case(app))
.and_then(|r| {
r.values.iter().find_map(|v| match v {
crate::xdata::XDataValue::Integer16(n) => Some(*n as u8),
_ => None,
})
})
};
let (Some(c1), Some(c2)) = (aci("GradientColor1ACI"), aci("GradientColor2ACI")) else {
continue;
};
let mut name = String::new();
let mut has_roundtrip = false;
if let Some(xd) = h.common.xdictionary_handle {
if let Some(ObjectType::Dictionary(d)) = document.objects.get(&xd) {
if let Some((_, xrec_h)) = d.entries.iter().find(|(k, _)| {
k.len() >= 19 && k[..19].eq_ignore_ascii_case("ACAD_XREC_ROUNDTRIP")
}) {
has_roundtrip = true;
if let Some(ObjectType::XRecord(xr)) = document.objects.get(xrec_h) {
let ascii: String = xr
.raw_data
.iter()
.map(|&b| {
if (32..127).contains(&b) {
b as char
} else {
' '
}
})
.collect();
let up = ascii.to_ascii_uppercase();
for cand in GRADIENT_NAMES {
if up.contains(cand) {
name = (*cand).to_string();
break;
}
}
if name.is_empty() && xr.raw_data.len() >= 4 {
const BY_INDEX: &[&str] = &[
"LINEAR",
"CYLINDER",
"INVCYLINDER",
"SPHERICAL",
"HEMISPHERICAL",
"CURVED",
"INVSPHERICAL",
"INVHEMISPHERICAL",
"INVCURVED",
];
let v = u32::from_le_bytes([
xr.raw_data[0],
xr.raw_data[1],
xr.raw_data[2],
xr.raw_data[3],
]);
if (123..123 + BY_INDEX.len() as u32).contains(&v) {
name = BY_INDEX[(v - 123) as usize].to_string();
}
}
}
}
}
}
if !has_roundtrip {
continue;
}
if name.is_empty() {
name = "LINEAR".to_string();
}
recovered.push((e.common().handle, c1, c2, name));
}
for (handle, c1, c2, name) in recovered {
if let Some(EntityType::Hatch(h)) = document.get_entity_mut(handle) {
h.gradient_color.enabled = true;
h.gradient_color.is_single_color = c1 == c2;
h.gradient_color.colors = vec![
crate::entities::hatch::GradientColorEntry {
value: 0.0,
color: Color::from_index(c1 as i16),
color_name: None,
book_name: None,
},
crate::entities::hatch::GradientColorEntry {
value: 1.0,
color: Color::from_index(c2 as i16),
color_name: None,
book_name: None,
},
];
h.gradient_color.name = name;
}
}
}
pub(crate) fn recover_mtext_bg_roundtrip(document: &mut crate::document::CadDocument) {
use crate::entities::EntityType;
use crate::types::{Color, Handle};
use crate::xdata::XDataValue;
fn color_from_method(v: i32) -> Color {
let u = v as u32;
match (u >> 24) & 0xFF {
0xC0 => Color::ByLayer,
0xC1 => Color::ByBlock,
0xC3 => Color::from_index((u & 0xFF) as i16),
_ => Color::Rgb {
r: ((u >> 16) & 0xFF) as u8,
g: ((u >> 8) & 0xFF) as u8,
b: (u & 0xFF) as u8,
},
}
}
let mut recovered: Vec<(Handle, i32, f64, Color, i32)> = Vec::new();
for e in document.entities() {
let EntityType::MText(m) = e else { continue };
if m.background_fill_flags != 0 {
continue; }
let Some(rec) = m
.common
.extended_data
.records()
.iter()
.find(|r| r.application_name.eq_ignore_ascii_case("ACAD"))
else {
continue;
};
let vals = &rec.values;
let Some(begin) = vals
.iter()
.position(|v| matches!(v, XDataValue::String(s) if s == "ACAD_MTEXT_BBRT"))
else {
continue;
};
let (mut flags, mut scale, mut color, mut transp) = (0i32, 1.0f64, Color::ByLayer, 0i32);
let mut i = begin + 1;
while i + 1 < vals.len() {
if matches!(&vals[i], XDataValue::String(s) if s == "ACAD_MTEXT_BERT") {
break;
}
if let XDataValue::Integer16(code) = vals[i] {
match (code, &vals[i + 1]) {
(91, XDataValue::Integer32(v)) => flags = *v,
(46, XDataValue::Real(v)) => scale = *v,
(64, XDataValue::Integer32(v)) => color = color_from_method(*v),
(442, XDataValue::Integer32(v)) => transp = *v,
_ => {}
}
i += 2;
} else {
i += 1;
}
}
if flags != 0 {
recovered.push((m.common.handle, flags, scale, color, transp));
}
}
for (h, flags, scale, color, transp) in recovered {
if let Some(EntityType::MText(m)) = document.get_entity_mut(h) {
m.background_fill_flags = flags;
m.background_scale = scale;
m.background_color = color;
m.background_transparency = transp;
}
}
}
#[cfg(test)]
mod section_recovery_tests {
use super::*;
fn ac15_info(offset: i64, size: i64) -> DwgFileHeaderInfo {
DwgFileHeaderInfo {
version_string: "AC1015".to_owned(),
version: DwgVersion::AC15,
acad_maintenance_version: 0,
preview_address: 0,
dwg_version: 0,
app_release_version: 0,
code_page: 0,
security_type: 0,
summary_info_addr: 0,
vba_project_addr: 0,
ac21_metadata: None,
ac21_header_crc: None,
ac21_unknown_key: None,
ac21_compressed_data_crc: None,
page_records: HashMap::new(),
section_descriptors: Vec::new(),
section_locators: HashMap::from([("AcDb:Header".to_owned(), (offset, size))]),
objects_base_offset: 0,
is_ac18_format: false,
}
}
fn ac18_info(page_number: i64) -> DwgFileHeaderInfo {
DwgFileHeaderInfo {
version_string: "AC1018".to_owned(),
version: DwgVersion::AC18,
acad_maintenance_version: 0,
preview_address: 0,
dwg_version: 0,
app_release_version: 0,
code_page: 0,
security_type: 0,
summary_info_addr: 0,
vba_project_addr: 0,
ac21_metadata: None,
ac21_header_crc: None,
ac21_unknown_key: None,
ac21_compressed_data_crc: None,
page_records: HashMap::new(),
section_descriptors: vec![DwgSectionInfo {
name: "AcDb:Header".to_owned(),
compressed_size: 8,
decompressed_size: 8,
encrypted: 0,
hash_code: 0,
encoding: 0,
page_count: 1,
pages: vec![DwgPageCrcInfo {
page_number,
offset: 0,
size: 0,
decompressed_size: 8,
compressed_size: 0,
checksum: 0,
crc: 0,
}],
}],
section_locators: HashMap::new(),
objects_base_offset: 0,
is_ac18_format: true,
}
}
#[test]
fn ac15_failsafe_keeps_readable_prefix_after_truncated_section() {
let stream = Cursor::new(vec![0, 1, 2, 3, 4]);
let mut reader = DwgReader::from_stream_with_options(stream, DwgReadOptions::failsafe());
let bytes = reader
.get_section_buffer("AcDb:Header", &ac15_info(2, 10))
.unwrap();
assert_eq!(bytes, vec![2, 3, 4]);
assert_eq!(reader.pending_diagnostics.len(), 1);
assert_eq!(reader.pending_diagnostics[0].code, "section-read-truncated");
}
#[test]
fn ac15_strict_mode_still_rejects_truncated_section() {
let stream = Cursor::new(vec![0, 1, 2, 3, 4]);
let mut reader = DwgReader::from_stream(stream);
assert!(reader
.get_section_buffer("AcDb:Header", &ac15_info(2, 10))
.is_err());
}
#[test]
fn ac18_failsafe_skips_missing_page_and_preserves_section_offsets() {
let mut reader = DwgReader::from_stream_with_options(
Cursor::new(Vec::new()),
DwgReadOptions::failsafe(),
);
let bytes = reader
.get_section_buffer("AcDb:Header", &ac18_info(7))
.unwrap();
assert_eq!(bytes, vec![0; 8]);
assert_eq!(reader.pending_diagnostics.len(), 1);
assert_eq!(reader.pending_diagnostics[0].code, "section-page-skipped");
}
#[test]
fn ac18_strict_mode_still_rejects_missing_page() {
let mut reader = DwgReader::from_stream(Cursor::new(Vec::new()));
assert!(reader
.get_section_buffer("AcDb:Header", &ac18_info(7))
.is_err());
}
}