use super::entities::{EntityRecord, PositionedRecord};
use crate::error::{DxfError, Result};
#[derive(Clone, Debug, Default)]
pub(crate) struct SectionRecords {
pub base_address: u32,
pub size: u32,
pub records: Vec<PositionedRecord>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Owner {
Entities,
Blocks,
}
#[derive(Clone, Copy)]
struct Cursor {
section: usize,
index: usize,
}
struct Sequence {
address: u32,
child_kind: &'static str,
}
fn invalid(message: impl Into<String>) -> DxfError {
DxfError::InvalidFormat(message.into())
}
struct Walker {
sections: [SectionRecords; 3],
visited: [Vec<Option<Owner>>; 3],
entities: Vec<EntityRecord>,
blocks: Vec<EntityRecord>,
}
impl Walker {
fn sequence_owner_matches(&self, raw: u32, address: u32) -> bool {
if raw == address {
return true;
}
self.sections.iter().enumerate().any(|(section, records)| {
let Some(offset) = address.checked_sub(records.base_address) else {
return false;
};
if offset > records.size {
return false;
}
let flag = match section {
0 => 0,
1 => 0x4000_0000,
2 => 0x8000_0000,
_ => return false,
};
raw == flag | offset
})
}
fn jump_target(&self, raw: u32, owner: Owner) -> Result<Option<Cursor>> {
let mut offset = raw & 0x00ff_ffff;
let section = match raw >> 24 {
0 if self.sections[0].base_address <= raw
&& raw
<= self.sections[0]
.base_address
.saturating_add(self.sections[0].size) =>
{
offset = raw - self.sections[0].base_address;
0
}
0 if self.sections[1].size != 0
&& self.sections[1].base_address <= raw
&& raw
<= self.sections[1]
.base_address
.saturating_add(self.sections[1].size) =>
{
offset = raw - self.sections[1].base_address;
1
}
0 => 0,
0x40 => 1,
0x80 => 2,
flag => {
return Err(invalid(format!(
"Invalid AC1009 JUMP section flag {flag:#x}"
)))
}
};
if matches!((owner, section), (Owner::Entities, 1) | (Owner::Blocks, 0)) {
return Err(invalid("AC1009 JUMP crosses entity/block ownership"));
}
let target = &self.sections[section];
if offset == target.size {
return Ok(None);
}
if offset > target.size {
return Err(invalid(format!(
"AC1009 JUMP target {raw:#x} exceeds section bounds"
)));
}
let index = target
.records
.binary_search_by_key(&offset, |record| record.offset)
.map_err(|_| {
invalid(format!(
"AC1009 JUMP target {raw:#x} is not a record boundary"
))
})?;
Ok(Some(Cursor { section, index }))
}
fn next(&self, cursor: Cursor) -> Option<Cursor> {
(cursor.index + 1 < self.sections[cursor.section].records.len()).then_some(Cursor {
section: cursor.section,
index: cursor.index + 1,
})
}
fn walk(&mut self, start: Cursor, owner: Owner) -> Result<()> {
let mut cursor = Some(start);
let mut sequence: Option<Sequence> = None;
let mut in_block = false;
while let Some(current) = cursor {
let record = &self.sections[current.section].records[current.index];
if let Some(previous) = self.visited[current.section][current.index] {
let message = if previous != owner {
"duplicate entity/block ownership"
} else {
"cycle or duplicate record ownership"
};
return Err(invalid(format!(
"AC1009 JUMP {message} at {:#x}",
record.address
)));
}
self.visited[current.section][current.index] = Some(owner);
if let Some(target) = record.jump_address {
cursor = self.jump_target(target, owner)?;
continue;
}
if let Some(entity) = &record.record {
let kind = entity.kind.as_str();
match kind {
"BLOCK" => {
if owner != Owner::Blocks || in_block || sequence.is_some() {
return Err(invalid("Invalid AC1009 BLOCK ownership"));
}
in_block = true;
}
"ENDBLK" => {
if !in_block || sequence.is_some() {
return Err(invalid("Invalid AC1009 ENDBLK ownership"));
}
in_block = false;
}
"VERTEX" | "ATTRIB" => {
if sequence.as_ref().map(|s| s.child_kind) != Some(kind) {
return Err(invalid(format!(
"Orphan AC1009 {kind} at {:#x}",
record.address
)));
}
}
"SEQEND" => {
let Some(open) = sequence.take() else {
return Err(invalid(format!(
"Orphan AC1009 SEQEND at {:#x}",
record.address
)));
};
if !record
.sequence_owner
.is_some_and(|owner| self.sequence_owner_matches(owner, open.address))
{
return Err(invalid(format!(
"AC1009 SEQEND at {:#x} names owner {:?}, expected {:#x}",
record.address, record.sequence_owner, open.address
)));
}
}
_ => {
if sequence.is_some() {
return Err(invalid(format!(
"AC1009 compound entity interrupted by {kind} at {:#x}",
record.address
)));
}
if owner == Owner::Blocks && !in_block {
return Err(invalid(
"AC1009 block entity appears outside BLOCK/ENDBLK",
));
}
if kind == "POLYLINE" || kind == "INSERT" && entity.integer(66, 0) != 0 {
sequence = Some(Sequence {
address: record.address,
child_kind: if kind == "POLYLINE" {
"VERTEX"
} else {
"ATTRIB"
},
});
}
}
}
let output = match owner {
Owner::Entities => &mut self.entities,
Owner::Blocks => &mut self.blocks,
};
output.push(entity.clone());
}
cursor = self.next(current);
}
if sequence.is_some() {
return Err(invalid("AC1009 compound entity has no SEQEND"));
}
if in_block {
return Err(invalid("AC1009 BLOCK has no ENDBLK"));
}
Ok(())
}
}
pub(crate) fn reassemble_sections(
entities: SectionRecords,
blocks: SectionRecords,
extras: SectionRecords,
) -> Result<(Vec<EntityRecord>, Vec<EntityRecord>)> {
let sections = [entities, blocks, extras];
for section in §ions {
let mut expected = 0;
for record in §ion.records {
if record.offset != expected
|| record.address
!= section
.base_address
.checked_add(expected)
.ok_or_else(|| invalid("AC1009 record address overflow"))?
{
return Err(invalid("AC1009 positioned records are not contiguous"));
}
expected = expected
.checked_add(u32::from(record.size))
.ok_or_else(|| invalid("AC1009 section size overflow"))?;
}
if expected != section.size {
return Err(invalid("AC1009 positioned records do not cover section"));
}
}
let visited = std::array::from_fn(|index| vec![None; sections[index].records.len()]);
let mut walker = Walker {
sections,
visited,
entities: Vec::new(),
blocks: Vec::new(),
};
for section in 0..3 {
for index in 0..walker.sections[section].records.len() {
if walker.visited[section][index].is_some() {
continue;
}
let owner = if section == 1 {
Owner::Blocks
} else {
Owner::Entities
};
walker.walk(Cursor { section, index }, owner)?;
}
}
Ok((walker.entities, walker.blocks))
}
#[cfg(test)]
#[path = "traversal_tests.rs"]
mod tests;