use cadmpeg_ir::le::u32_at as u32_le;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{catalog, value_block};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ObjectGraph {
pub pos: usize,
pub total_len: usize,
pub catalog_pos: Option<usize>,
pub records: Vec<ObjectRecord>,
}
impl ObjectGraph {
#[must_use]
#[cfg(test)]
pub fn record(&self, ordinal: u32) -> Option<&ObjectRecord> {
let index = usize::try_from(ordinal.checked_sub(1)?).ok()?;
self.records.get(index)
}
#[cfg(test)]
pub fn children(&self, owner_ordinal: u32) -> impl Iterator<Item = &ObjectRecord> {
self.records
.iter()
.filter(move |record| record.owner_ref == Some(owner_ordinal))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ObjectRecord {
pub index: usize,
pub pos: usize,
pub total_len: usize,
pub lead: u8,
pub head: Vec<HeadToken>,
pub owner_ref: Option<u32>,
pub class_ref: Option<u32>,
pub class_name: Option<String>,
pub storage_ref: Option<u32>,
pub payload: ObjectPayload,
pub subtype: PayloadSubtype,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum HeadToken {
Lead(u8),
Separator,
Reference(u32),
Literal(u8),
NullHandle,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ObjectPayload {
pub size: usize,
pub fields: Vec<PayloadField>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum ListItem {
Reference(u32),
Atom(u32),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum PayloadField {
Atom {
value: u32,
offset: usize,
},
Reference {
value: u32,
offset: usize,
},
Scalar {
tag: u8,
value: u32,
offset: usize,
},
Blob {
declared_len: usize,
#[serde(with = "cadmpeg_ir::bytes")]
#[schemars(with = "String")]
bytes: Vec<u8>,
offset: usize,
},
BulkTable {
count: u32,
table_count: u32,
offset: usize,
},
List {
declared_count: u32,
items: Vec<ListItem>,
offset: usize,
},
Sentinel {
offset: usize,
},
Terminator,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum PayloadSubtype {
BulkTable,
TripletChain,
ListAggregator,
Blob,
AtomVector,
Empty,
Mixed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum AliasLead {
SurfaceSupportStorage,
E5LinkedSurfaceStorage,
NonSurfaceAlias,
Unclassified(u32),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SurfaceAlias {
pub pos: usize,
pub lead: AliasLead,
pub lead_raw: u32,
pub tag: u32,
pub tag_raw: u32,
pub flag: u8,
pub f1: [u8; 3],
pub entity_record_ordinal: u8,
pub f2: u32,
pub f3: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg(test)]
pub struct Marker7cd9 {
pub pos: usize,
pub context: Vec<u8>,
pub next_delta: Option<usize>,
}
#[must_use]
#[cfg(test)]
pub fn markers_7cd9(data: &[u8], context_len: usize) -> Vec<Marker7cd9> {
let positions: Vec<usize> = data
.windows(2)
.enumerate()
.filter_map(|(pos, bytes)| (bytes == [0x7c, 0xd9]).then_some(pos))
.collect();
positions
.iter()
.enumerate()
.map(|(index, &pos)| Marker7cd9 {
pos,
context: data[pos..pos.saturating_add(context_len).min(data.len())].to_vec(),
next_delta: positions.get(index + 1).map(|next| next - pos),
})
.collect()
}
#[must_use]
pub fn surface_aliases(data: &[u8]) -> Vec<SurfaceAlias> {
const MARKER: [u8; 4] = [0x01, 0x00, 0x04, 0x00];
data.windows(MARKER.len())
.enumerate()
.filter(|(_, bytes)| *bytes == MARKER)
.filter_map(|(pos, _)| {
let tag_raw = u32_le(data, pos + 4)?;
let tag = tag_raw & 0x00ff_ffff;
if pos + 20 > data.len() {
return None;
}
let lead_raw = u32_le(data, pos.checked_sub(4)?)?;
let lead = if lead_raw & 0xff == 1 {
AliasLead::SurfaceSupportStorage
} else if lead_raw == 0x8e {
AliasLead::E5LinkedSurfaceStorage
} else if lead_raw == 0 {
AliasLead::NonSurfaceAlias
} else {
AliasLead::Unclassified(lead_raw)
};
let f1 = [data[pos + 9], data[pos + 10], data[pos + 11]];
Some(SurfaceAlias {
pos,
lead,
lead_raw,
tag,
tag_raw,
flag: data[pos + 8],
f1,
entity_record_ordinal: f1[2],
f2: u32_le(data, pos + 12)?,
f3: u32_le(data, pos + 16)?,
})
})
.collect()
}
#[must_use]
#[cfg(test)]
pub fn parse(data: &[u8]) -> Option<ObjectGraph> {
parse_all(data)
.into_iter()
.max_by_key(|graph| graph.records.len())
}
#[must_use]
pub fn parse_all(data: &[u8]) -> Vec<ObjectGraph> {
let catalogs = catalog::parse(data);
let value_blocks = value_block::parse(data);
let candidates = data
.windows(2)
.enumerate()
.filter(|(_, marker)| *marker == [0x7c, 0x08])
.filter_map(|(pos, _)| parse_candidate(data, pos))
.collect::<Vec<_>>();
let mut roots = Vec::<ObjectGraph>::new();
for graph in candidates {
let graph_end = graph.pos + graph.total_len;
if roots
.iter()
.any(|outer| outer.pos < graph.pos && outer.pos + outer.total_len >= graph_end)
{
continue;
}
roots.push(graph);
}
roots
.into_iter()
.map(|mut graph| {
bind_catalog(&mut graph, &catalogs, &value_blocks);
graph
})
.collect()
}
fn bind_catalog(
graph: &mut ObjectGraph,
catalogs: &[catalog::Catalog],
value_blocks: &[value_block::ValueBlock],
) {
let Some(graph_end) = graph.pos.checked_add(graph.total_len) else {
return;
};
let schema = catalogs
.iter()
.find(|schema| schema.pos == graph_end)
.or_else(|| {
value_blocks
.iter()
.find(|block| block.pos == graph_end)
.and_then(|block| block.pos.checked_add(block.total_len))
.and_then(|value_end| catalogs.iter().find(|schema| schema.pos == value_end))
});
let Some(schema) = schema else {
return;
};
graph.catalog_pos = Some(schema.pos);
for record in &mut graph.records {
record.class_name = record
.class_ref
.and_then(|ordinal| schema.entries.get(ordinal as usize))
.map(|entry| entry.value.clone());
}
}
fn parse_candidate(data: &[u8], pos: usize) -> Option<ObjectGraph> {
let total_len = usize::try_from(u32_le(data, pos + 2)?).ok()?;
let end = pos.checked_add(total_len)?;
if total_len < 15 || end > data.len() {
return None;
}
let mut at = pos + 6;
let mut records = Vec::new();
while at + 6 <= end && data.get(at..at + 2) == Some(&[0x7c, 0x09]) {
let record_len = usize::try_from(u32_le(data, at + 2)?).ok()?;
let record_end = at.checked_add(record_len)?;
if record_len < 6 || record_end > end {
return None;
}
let head_start = at + 6;
let mut children = data[head_start..record_end]
.windows(2)
.enumerate()
.filter_map(|(relative, marker)| {
if marker != [0x7c, 0x0a] {
return None;
}
let child = head_start + relative;
let child_len = usize::try_from(u32_le(data, child + 2)?).ok()?;
(child_len >= 6 && child.checked_add(child_len) == Some(record_end))
.then_some((child, child_len))
});
let (child, _) = children.next()?;
if children.next().is_some() {
return None;
}
let lead = *data.get(head_start..child)?.first()?;
let head = decode_head(&data[head_start..child]);
let roles = if matches!(head.get(1), Some(HeadToken::Separator)) {
&head[2..]
} else {
let native_role_count = match lead {
0x02 => 1,
0x12 => 2,
0x52 => 3,
_ => 0,
};
if head.len() == native_role_count + 1 {
&head[1..]
} else {
&[]
}
};
let owner_ref = match roles.first() {
Some(HeadToken::Reference(value)) => Some(*value),
_ => None,
};
let class_ref = owner_ref.and_then(|_| match roles.get(1) {
Some(HeadToken::Reference(value)) => Some(*value),
_ => None,
});
let storage_ref = class_ref.and_then(|_| match roles.get(2) {
Some(HeadToken::Reference(value)) => Some(*value),
_ => None,
});
let payload = decode_payload(&data[child + 6..record_end])?;
let subtype = classify(&payload.fields);
records.push(ObjectRecord {
index: records.len(),
pos: at,
total_len: record_len,
lead,
head,
owner_ref,
class_ref,
class_name: None,
storage_ref,
payload,
subtype,
});
at = record_end;
}
(!records.is_empty() && at == end).then_some(ObjectGraph {
pos,
total_len,
catalog_pos: None,
records,
})
}
fn decode_head(bytes: &[u8]) -> Vec<HeadToken> {
let Some(&lead) = bytes.first() else {
return Vec::new();
};
let mut tokens = vec![HeadToken::Lead(lead)];
let mut at = 1;
while at < bytes.len() {
let byte = bytes[at];
if byte == 0x01 {
tokens.push(HeadToken::Separator);
at += 1;
} else if bytes.get(at..at + 4) == Some(&[0xff; 4]) {
tokens.push(HeadToken::NullHandle);
at += 4;
} else if (0xd1..=0xe4).contains(&byte) && at + 1 < bytes.len() {
tokens.push(HeadToken::Reference(
u32::from(byte - 0xd1) * 256 + u32::from(bytes[at + 1]) + 1,
));
at += 2;
} else if (0x80..=0xd0).contains(&byte) {
tokens.push(HeadToken::Reference(u32::from(byte - 0x80)));
at += 1;
} else {
tokens.push(HeadToken::Literal(byte));
at += 1;
}
}
tokens
}
fn atom(bytes: &[u8], at: usize) -> Option<(u32, usize)> {
let byte = *bytes.get(at)?;
match byte {
0x80..=0xd0 => Some((u32::from(byte - 0x80), 1)),
0x51..=0x7f => Some((u32::from(byte), 1)),
0xd1..=0xe4 => Some((
u32::from(byte - 0xd1) * 256 + u32::from(*bytes.get(at + 1)?) + 1,
2,
)),
_ => Some((u32::from(byte), 1)),
}
}
fn decode_payload(bytes: &[u8]) -> Option<ObjectPayload> {
let mut fields = Vec::new();
let mut at = 0;
while at < bytes.len() {
let offset = at;
match bytes[at] {
0xfe => {
fields.push(PayloadField::Terminator);
at += 1;
break;
}
0xe5 if at + 5 <= bytes.len() => {
let declared_len = usize::try_from(u32_le(bytes, at + 1).unwrap_or(0)).unwrap_or(0);
let start = at + 5;
let end = start.saturating_add(declared_len).min(bytes.len());
fields.push(PayloadField::Blob {
declared_len,
bytes: bytes[start..end].to_vec(),
offset,
});
at = end;
}
0x3c => {
let Some((count, advance)) = atom(bytes, at + 1) else {
fields.push(PayloadField::Atom {
value: 0x3c,
offset,
});
at += 1;
continue;
};
let table_at = at + 1 + advance;
let table_count = u32_le(bytes, table_at).unwrap_or(u32::MAX);
if usize::try_from(table_count)
.ok()
.is_some_and(|count| count <= bytes.len())
{
fields.push(PayloadField::BulkTable {
count,
table_count,
offset,
});
at = table_at + 4;
} else {
fields.push(PayloadField::Atom {
value: 0x3c,
offset,
});
at += 1;
}
}
0x3b => {
if bytes.get(at + 1) == Some(&0xfe) {
fields.push(PayloadField::Atom {
value: 0x3b,
offset,
});
at += 1;
continue;
}
let Some((declared_count, advance)) = atom(bytes, at + 1) else {
fields.push(PayloadField::Atom {
value: 0x3b,
offset,
});
at += 1;
continue;
};
at += 1 + advance;
let mut items = Vec::new();
for _ in 0..declared_count {
if at >= bytes.len() || bytes[at] == 0xfe {
break;
}
let tagged_reference = bytes[at] == 0x81;
let tagged_atom = bytes[at] == 0x80;
let value_at = at + usize::from(tagged_reference || tagged_atom);
if (tagged_reference || tagged_atom)
&& (value_at >= bytes.len() || bytes[value_at] == 0xfe)
{
at = value_at;
break;
}
let Some((value, consumed)) = atom(bytes, value_at) else {
break;
};
items.push(if tagged_reference {
ListItem::Reference(value)
} else {
ListItem::Atom(value)
});
at = value_at + consumed;
}
fields.push(PayloadField::List {
declared_count,
items,
offset,
});
}
0x80 | 0x32 if at + 5 <= bytes.len() => {
let tag = bytes[at];
fields.push(if tag == 0x80 {
PayloadField::Atom {
value: u32_le(bytes, at + 1).expect("checked escaped atom extent"),
offset,
}
} else {
PayloadField::Reference {
value: u32_le(bytes, at + 1).expect("checked scalar extent"),
offset,
}
});
at += 5;
}
0x81 | 0x3a | 0x39 | 0x7a => {
let tag = bytes[at];
if bytes.get(at + 1) == Some(&0xfe) {
fields.push(PayloadField::Atom {
value: u32::from(tag),
offset,
});
at += 1;
continue;
}
let Some((value, consumed)) = atom(bytes, at + 1) else {
fields.push(PayloadField::Atom {
value: u32::from(tag),
offset,
});
at += 1;
continue;
};
fields.push(match tag {
0x81 => PayloadField::Reference { value, offset },
_ => PayloadField::Scalar { tag, value, offset },
});
at += 1 + consumed;
}
0x0d => {
fields.push(PayloadField::Sentinel { offset });
at += 1;
}
_ => {
let (value, consumed) = atom(bytes, at).unwrap_or((u32::from(bytes[at]), 1));
fields.push(PayloadField::Atom { value, offset });
at += consumed;
}
}
}
(at == bytes.len() && matches!(fields.last(), Some(PayloadField::Terminator))).then_some(
ObjectPayload {
size: bytes.len(),
fields,
},
)
}
fn classify(fields: &[PayloadField]) -> PayloadSubtype {
if fields
.iter()
.any(|field| matches!(field, PayloadField::BulkTable { .. }))
{
return PayloadSubtype::BulkTable;
}
let triplets = fields
.windows(3)
.filter(|window| {
matches!(window[0], PayloadField::Scalar { .. })
&& matches!(window[1], PayloadField::Atom { .. })
&& matches!(window[2], PayloadField::Atom { .. })
})
.count();
if triplets >= 2 {
return PayloadSubtype::TripletChain;
}
if fields.iter().any(
|field| matches!(field, PayloadField::List { declared_count, .. } if *declared_count >= 3),
) {
return PayloadSubtype::ListAggregator;
}
if fields
.iter()
.any(|field| matches!(field, PayloadField::Blob { .. }))
{
return PayloadSubtype::Blob;
}
let atom_count = fields
.iter()
.filter(|field| matches!(field, PayloadField::Atom { .. }))
.count();
let list_count = fields
.iter()
.filter(|field| matches!(field, PayloadField::List { .. }))
.count();
if atom_count >= 2 && triplets == 0 && list_count == 0 {
return PayloadSubtype::AtomVector;
}
if fields.is_empty() || matches!(fields, [PayloadField::Terminator]) {
PayloadSubtype::Empty
} else {
PayloadSubtype::Mixed
}
}