use std::collections::HashMap;
use std::io::{Read, Seek, SeekFrom, Cursor};
use std::fs::File;
use std::path::Path;
use byteorder::{LittleEndian, ReadBytesExt};
use crate::error::DxfError;
use crate::notification::{NotificationCollection, NotificationType};
use crate::io::dwg::dwg_version::DwgVersion;
use crate::io::dwg::dwg21_metadata::Dwg21CompressedMetadata;
use crate::io::dwg::reed_solomon::reed_solomon_decode;
use crate::io::dwg::decompressor_ac18::decompress_ac18;
use crate::io::dwg::decompressor_ac21::decompress_ac21;
use crate::io::dwg::checksum::{apply_mask, apply_magic_sequence};
const AC21_FILE_HEADER_SIZE: u64 = 0x480;
#[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,
}
impl Default for DwgReadOptions {
fn default() -> Self {
Self { failsafe: false }
}
}
impl DwgReadOptions {
pub fn failsafe() -> Self {
Self { failsafe: true }
}
}
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";
fn find_acds_end(buf: &[u8], from: usize, to: usize) -> Option<(usize, usize)> {
let hay = &buf[..to.min(buf.len())];
let asm = find_subsequence(hay, ASM_END_MARKER, from).map(|e| (e, ASM_END_MARKER.len()));
let acis = find_subsequence(hay, ACIS_END_MARKER, from).map(|e| (e, ACIS_END_MARKER.len()));
match (asm, acis) {
(Some(a), Some(b)) => Some(if a.0 <= b.0 { a } else { b }),
(a, None) => a,
(None, b) => b,
}
}
fn extract_acds_record_blobs(buf: &[u8]) -> 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 first_table = usize::MAX;
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; }
first_table = first_table.min(seg);
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 base = seg + 48 + recs.len() * 20; 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 {
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 {
orphan_handles.push(handle); continue;
};
let start = region_start + mp;
let end_bound = (region_end + ASM_END_MARKER.len()).min(seg_end);
if let Some((end, marker_len)) = find_acds_end(buf, start, end_bound) {
out.push((handle, buf[start..end + marker_len].to_vec()));
}
}
}
if !orphan_handles.is_empty() && first_table != usize::MAX {
let mut pool: Vec<Vec<u8>> = Vec::new();
let mut pos = 0;
while let Some(start) = find_acds_magic(buf, pos) {
if start >= first_table {
break;
}
match find_acds_end(buf, start, first_table) {
Some((end, marker_len)) => {
pool.push(buf[start..end + marker_len].to_vec());
pos = end + marker_len;
}
None => break,
}
}
if pool.len() == orphan_handles.len() {
out.extend(orphan_handles.into_iter().zip(pool));
}
}
out
}
fn extract_acds_sab_blobs(buf: &[u8]) -> Vec<Vec<u8>> {
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(buf[start..stop].to_vec());
pos = stop;
}
None => break,
}
}
blobs
}
fn acds_fill(acis: &mut crate::entities::solid3d::AcisData, blob: Vec<u8>) {
acis.sab_data = 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 let Some(p) = s.acis_data.placement_origin() {
s.point_of_reference = p;
}
true
}
EntityType::Region(r) => {
acds_fill(&mut r.acis_data, blob);
if let Some(p) = r.acis_data.placement_origin() {
r.point_of_reference = p;
}
true
}
EntityType::Body(b) => {
acds_fill(&mut b.acis_data, blob);
if let Some(p) = 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()
}
pub struct DwgReader<R: Read + Seek> {
stream: R,
pub options: DwgReadOptions,
pub notifications: NotificationCollection,
}
impl DwgReader<File> {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, DxfError> {
let file = File::open(path)?;
Ok(Self {
stream: file,
options: DwgReadOptions::default(),
notifications: NotificationCollection::new(),
})
}
pub fn from_file_with_options<P: AsRef<Path>>(path: P, options: DwgReadOptions) -> Result<Self, DxfError> {
let file = File::open(path)?;
Ok(Self {
stream: file,
options,
notifications: NotificationCollection::new(),
})
}
}
impl<R: Read + Seek> DwgReader<R> {
pub fn from_stream(stream: R) -> Self {
Self {
stream,
options: DwgReadOptions::default(),
notifications: NotificationCollection::new(),
}
}
pub fn from_stream_with_options(stream: R, options: DwgReadOptions) -> Self {
Self {
stream,
options,
notifications: NotificationCollection::new(),
}
}
pub fn read(&mut self) -> std::result::Result<crate::document::CadDocument, DxfError> {
let failsafe = self.options.failsafe;
let info = match self.read_file_header() {
Ok(info) => info,
Err(e) if failsafe => {
self.notifications.notify(
NotificationType::Error,
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));
return Ok(doc);
}
Err(e) => return Err(e),
};
let dxf_version = crate::types::DxfVersion::parse(&info.version_string)
.unwrap_or(crate::types::DxfVersion::Unknown);
let mut document = crate::document::CadDocument::with_version(dxf_version);
document.maintenance_version = info.acad_maintenance_version;
document.dwg_source_version = Some(dxf_version);
if let Ok(classes_buf) = self.get_section_buffer("AcDb:Classes", &info) {
match crate::io::dwg::dwg_stream_readers::classes_reader::read_classes(
&classes_buf,
dxf_version,
info.acad_maintenance_version,
) {
Ok(classes) => document.classes = classes,
Err(e) => self.notifications.notify(
NotificationType::Warning,
format!("Failed to read classes: {}", e),
),
}
}
if let Ok(header_buf) = self.get_section_buffer("AcDb:Header", &info) {
match crate::io::dwg::dwg_stream_readers::header_reader::read_header(
&header_buf,
dxf_version,
info.acad_maintenance_version,
) {
Ok(header_vars) => document.header = header_vars,
Err(e) => self.notifications.notify(
NotificationType::Warning,
format!("Failed to read header: {}", e),
),
}
}
let handle_map = if let Ok(handle_buf) = self.get_section_buffer("AcDb:Handles", &info) {
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) => {
self.notifications.notify(
NotificationType::Warning,
format!("Failed to read handles: {}", e),
);
std::collections::HashMap::new()
}
}
} else {
std::collections::HashMap::new()
};
if !handle_map.is_empty() {
if let Ok(objects_buf) = self.get_section_buffer("AcDb:AcDbObjects", &info) {
match crate::io::dwg::dwg_stream_readers::object_reader::DwgObjectReader::new(
objects_buf,
dxf_version,
handle_map,
) {
Ok(obj_reader) => {
let mut builder = crate::io::dwg::dwg_document_builder::DwgDocumentBuilder::new(obj_reader);
builder.set_failsafe(failsafe);
let build_notifications = builder.build(&mut document);
self.notifications.extend(build_notifications);
},
Err(e) => self.notifications.notify(
NotificationType::Warning,
format!("Failed to init object reader: {}", e),
),
}
}
}
if let Ok(acds_buf) = self.get_section_buffer("AcDb:AcDsPrototype_1b", &info) {
let record_blobs = extract_acds_record_blobs(&acds_buf);
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)
}
};
if attached > 0 {
self.notifications.notify(
NotificationType::Warning,
format!("AcDs: attached {} SAB blob(s) to modeler entities", attached),
);
}
}
document.notifications.extend(std::mem::take(&mut self.notifications));
Ok(document)
}
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::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;
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 == 2 {
handles_seeker = seeker;
}
if number == 5 {
aux_header_end = seeker + size;
}
}
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;
}
let objects_size = handles_seeker - aux_header_end;
if objects_size > 0 {
info.section_locators.insert(
names::ACDB_OBJECTS.to_string(),
(aux_header_end, objects_size),
);
info.objects_base_offset = aux_header_end;
}
self.notifications.notify(
NotificationType::Warning,
format!(
"AC15 file header: {} locator records, objects at offset {}, size {}",
record_count, aux_header_end, 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 mut compressed = vec![0u8; comp_size as usize];
self.stream.read_exact(&mut compressed)?;
let decompressed = if compression == 2 {
decompress_ac18(&compressed, decomp_size as usize)
} 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 mut compressed = vec![0u8; comp_size as usize];
self.stream.read_exact(&mut compressed)?;
let decompressed = if compression == 2 {
decompress_ac18(&compressed, decomp_size as usize)
} 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> {
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
))
})?;
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 v = compressed_size.wrapping_add(7);
let v1 = v & 0xFFFF_FFF8;
let total_size = v1.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={}, v1={})",
total_size, compressed_size, correction_factor, v1
)));
}
let factor = (total_size + block_size - 1) / block_size;
let read_length = factor * 255;
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);
}
let mut compressed_data = vec![0u8; total_size];
reed_solomon_decode(&encoded_buffer, &mut compressed_data, factor, block_size);
if compressed_size != uncompressed_size {
let src_padded_size = compressed_data.len() + 64;
let mut padded_source = vec![0u8; src_padded_size];
padded_source[..compressed_data.len()].copy_from_slice(&compressed_data);
let dst_padded_size = uncompressed_size as usize + 64;
let mut decompressed_data = vec![0u8; dst_padded_size];
decompress_ac21(
&padded_source,
0,
compressed_size as u32,
&mut decompressed_data,
);
decompressed_data.truncate(uncompressed_size as usize);
Ok(decompressed_data)
} else {
compressed_data.truncate(uncompressed_size as usize);
Ok(compressed_data)
}
}
pub fn get_section_buffer(
&mut self,
section_name: &str,
info: &DwgFileHeaderInfo,
) -> Result<Vec<u8>, DxfError> {
let failsafe = self.options.failsafe;
if !info.section_locators.is_empty() {
if let Some(&(offset, size)) = info.section_locators.get(section_name) {
if size <= 0 {
return Err(DxfError::Parse(
format!("Section '{}' has zero size", section_name)
));
}
self.stream.seek(SeekFrom::Start(offset as u64))?;
let mut buf = vec![0u8; size as usize];
self.stream.read_exact(&mut buf)?;
return Ok(buf);
} else {
return Err(DxfError::Parse(
format!("Section '{}' not found in AC15 locator records", section_name)
));
}
}
if info.is_ac18_format {
return self.get_section_buffer_ac18(section_name, info);
}
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 = section.compressed_size as usize;
let mut result = Vec::with_capacity(total_size);
let encoding = section.encoding;
let block_size: usize = 251;
let mut skipped_pages = 0u32;
for page in §ion.pages {
if let Some(&(page_offset, _page_size)) = info.page_records.get(&(page.page_number as i32)) {
let page_result = if encoding == 1 {
let read_size = page.decompressed_size as usize;
(|| -> Result<Vec<u8>, DxfError> {
self.stream.seek(SeekFrom::Start(AC21_FILE_HEADER_SIZE + page_offset as u64))?;
let mut buf = vec![0u8; read_size];
self.stream.read_exact(&mut buf)?;
Ok(buf)
})()
} else {
self.get_page_buffer_at(
page_offset as u64,
page.compressed_size,
page.decompressed_size,
1, block_size,
)
};
match page_result {
Ok(page_data) => result.extend_from_slice(&page_data),
Err(e) if failsafe => {
skipped_pages += 1;
self.notifications.notify(
NotificationType::Error,
format!(
"Failsafe: skipped corrupt page {} in section '{}': {}",
page.page_number, section_name, e
),
);
let fill_size = page.decompressed_size as usize;
result.extend(std::iter::repeat(0u8).take(fill_size));
}
Err(e) => return Err(e),
}
} else if failsafe {
skipped_pages += 1;
self.notifications.notify(
NotificationType::Error,
format!(
"Failsafe: page {} not found in page map for section '{}'",
page.page_number, section_name
),
);
let fill_size = page.decompressed_size as usize;
result.extend(std::iter::repeat(0u8).take(fill_size));
} else {
return Err(DxfError::Parse(
format!("Page {} not found in page map", page.page_number)
));
}
}
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);
Ok(result)
}
fn get_section_buffer_ac18(
&mut self,
section_name: &str,
info: &DwgFileHeaderInfo,
) -> Result<Vec<u8>, DxfError> {
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 = section.compressed_size as usize;
let is_compressed = section.encoding == 2;
let max_page_size = section.decompressed_size as usize;
let mut result = vec![0u8; total_size];
for page in §ion.pages {
let page_number = page.page_number as i32;
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)
))?;
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 {
self.notifications.notify(
NotificationType::Warning,
format!(
"AC18: Invalid compressed size {} for page {} in section '{}'",
data_compressed_size, page_number, section_name
),
);
continue;
}
let mut compressed = vec![0u8; data_compressed_size as usize];
self.stream.read_exact(&mut compressed)?;
let decompressed = if is_compressed {
decompress_ac18(&compressed, max_page_size)
} else {
compressed
};
let dst_start = data_offset as usize;
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]);
}
}
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 section_name_tests {
use super::section_name_from_field;
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]), "");
}
}