use cadmpeg_core::le::u32_at;
use cadmpeg_core::le::u64_at as read_le_u64;
#[derive(Debug, Clone, PartialEq)]
pub struct AsmHeader {
pub width: u8,
pub save_format_version: Option<u32>,
pub record_count: Option<u32>,
pub entity_count: Option<u64>,
pub flags: Option<u64>,
pub product_family: Option<String>,
pub product_version: Option<String>,
pub save_date: Option<String>,
pub scale: Option<f64>,
pub linear: Option<f64>,
pub angular: Option<f64>,
}
const MAGIC_PREFIX: &[u8] = b"ASM BinaryFile";
pub const HISTORY_PARTITION_FLAG: u64 = 1;
pub const FORMAT_REVISION_FLAGS: u64 = 0xfe;
const FORMAT_REVISION_SHIFT: u32 = 1;
impl AsmHeader {
pub fn save_format_major(&self) -> Option<u32> {
self.save_format_version.map(|version| version / 100)
}
pub fn save_format_minor(&self) -> Option<u32> {
self.save_format_version.map(|version| version % 100)
}
pub fn has_history_partition(&self) -> bool {
self.flags
.is_some_and(|flags| flags & HISTORY_PARTITION_FLAG != 0)
}
pub fn format_revision(&self) -> Option<u32> {
self.flags
.map(|flags| ((flags & FORMAT_REVISION_FLAGS) >> FORMAT_REVISION_SHIFT) as u32)
}
pub fn unassigned_flags(&self) -> Option<u64> {
self.flags
.map(|flags| flags & !(HISTORY_PARTITION_FLAG | FORMAT_REVISION_FLAGS))
}
}
const BF8_STRING_REGION_START: usize = 47;
const BF4_STRING_REGION_START: usize = 31;
pub fn has_asm_magic(bytes: &[u8]) -> bool {
bytes.len() >= 16 && bytes.starts_with(MAGIC_PREFIX) && matches!(bytes[14], b'4' | b'8')
}
pub fn stream_ref_width(bytes: &[u8]) -> usize {
parse(bytes).map_or(8, |header| usize::from(header.width))
}
fn string_region_start(bytes: &[u8]) -> Option<usize> {
match bytes[14] {
b'8' => Some(BF8_STRING_REGION_START),
b'4' => Some(BF4_STRING_REGION_START),
_ => None,
}
}
pub fn parse(bytes: &[u8]) -> Option<AsmHeader> {
if !has_asm_magic(bytes) {
return None;
}
let width = bytes[14] - b'0';
let mut header = AsmHeader {
width,
save_format_version: None,
record_count: None,
entity_count: None,
flags: None,
product_family: None,
product_version: None,
save_date: None,
scale: None,
linear: None,
angular: None,
};
match width {
8 => {
header.save_format_version = u32_at(bytes, 15);
header.entity_count = read_le_u64(bytes, 31);
header.flags = read_le_u64(bytes, 39);
}
4 => {
header.save_format_version = u32_at(bytes, 15);
header.record_count = u32_at(bytes, 19);
header.entity_count = u32_at(bytes, 23).map(u64::from);
header.flags = u32_at(bytes, 27).map(u64::from);
}
_ => return Some(header),
}
let Some(start) = string_region_start(bytes) else {
return Some(header);
};
let (strings, doubles, _) = read_string_region(bytes, start);
let mut it = strings.into_iter();
header.product_family = it.next();
header.product_version = it.next();
header.save_date = it.next();
let mut dit = doubles.into_iter();
header.scale = dit.next();
header.linear = dit.next();
header.angular = dit.next();
Some(header)
}
pub fn record_stream_start(bytes: &[u8]) -> Option<usize> {
if !has_asm_magic(bytes) {
return None;
}
let start = string_region_start(bytes)?;
let (strings, doubles, cur) = read_string_region(bytes, start);
(strings.len() == 3 && doubles.len() == 3).then_some(cur)
}
fn read_string_region(bytes: &[u8], start: usize) -> (Vec<String>, Vec<f64>, usize) {
let mut cur = start;
let mut strings = Vec::new();
while strings.len() < 3 {
match read_u8_string(bytes, cur) {
Some((s, next)) => {
strings.push(s);
cur = next;
}
None => break,
}
}
let mut doubles = Vec::new();
while doubles.len() < 3 {
match read_tagged_f64(bytes, cur) {
Some((v, next)) => {
doubles.push(v);
cur = next;
}
None => break,
}
}
(strings, doubles, cur)
}
const HISTORY_PREAMBLE_RECORD: &str = "Begin-of-ASM-History-Data";
pub fn solved_record_limit(bytes: &[u8]) -> Option<usize> {
let header = parse(bytes)?;
if !header.has_history_partition() {
return None;
}
let start = record_stream_start(bytes)?;
let records = crate::sab::frame(bytes, start, bytes.len(), usize::from(header.width)).ok()?;
if let Some(preamble) = records
.iter()
.find(|record| record.name == HISTORY_PREAMBLE_RECORD)
{
return Some(preamble.offset);
}
let mut next = match records.last() {
Some(record) => record.offset.checked_add(record.len)?,
None => start,
};
while bytes.get(next) == Some(&0x11) {
next += 1;
}
exact_identifier_at(bytes, next, "delta_state").then_some(next)
}
fn exact_identifier_at(bytes: &[u8], at: usize, expected: &str) -> bool {
let Some((&0x0d, rest)) = bytes.get(at..).and_then(|tail| tail.split_first()) else {
return false;
};
let Some((&length, payload)) = rest.split_first() else {
return false;
};
usize::from(length) == expected.len()
&& payload.get(..usize::from(length)) == Some(expected.as_bytes())
}
fn read_u8_string(bytes: &[u8], at: usize) -> Option<(String, usize)> {
if *bytes.get(at)? != 0x07 {
return None;
}
let len = *bytes.get(at + 1)? as usize;
let start = at + 2;
let slice = bytes.get(start..start + len)?;
let s = std::str::from_utf8(slice).ok()?.to_string();
Some((s, start + len))
}
fn read_tagged_f64(bytes: &[u8], at: usize) -> Option<(f64, usize)> {
if *bytes.get(at)? != 0x06 {
return None;
}
let slice = bytes.get(at + 1..at + 9)?;
Some((
f64::from_le_bytes(
slice
.try_into()
.expect("invariant: bytes.get(at+1..at+9) is an 8-byte slice"),
),
at + 9,
))
}