use crate::Result;
use serde::{Deserialize, Serialize};
pub const SCHEMA_SCAN_LIMIT: usize = 64 * 1024;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SchemaTable {
pub classes: Vec<ClassEntry>,
pub cpp_types: Vec<String>,
pub skipped_records: usize,
#[serde(default)]
pub scanned_bytes: usize,
#[serde(default)]
pub total_bytes: usize,
#[serde(default)]
pub scan_truncated: bool,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct SchemaDiagnostics {
pub class_count: usize,
pub parsed_field_count: usize,
pub declared_field_count_sum: u64,
pub field_count_mismatches: usize,
pub tagged_class_count: usize,
pub parent_only_class_count: usize,
pub ancestor_tag_count: usize,
pub skipped_records: usize,
pub cpp_type_count: usize,
}
impl SchemaTable {
pub fn diagnostics(&self) -> SchemaDiagnostics {
let mut d = SchemaDiagnostics {
class_count: self.classes.len(),
skipped_records: self.skipped_records,
cpp_type_count: self.cpp_types.len(),
..SchemaDiagnostics::default()
};
for c in &self.classes {
d.parsed_field_count += c.fields.len();
if let Some(declared) = c.declared_field_count {
d.declared_field_count_sum += declared as u64;
if (declared as usize) != c.fields.len() {
d.field_count_mismatches += 1;
}
}
if c.tag.is_some() {
d.tagged_class_count += 1;
}
if c.was_parent_only {
d.parent_only_class_count += 1;
}
if c.ancestor_tag.is_some() {
d.ancestor_tag_count += 1;
}
}
d
}
pub fn tagged_ancestor(&self, class_name: &str) -> Option<(&str, u16)> {
let mut seen: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
let mut current = class_name;
loop {
if !seen.insert(current) {
return None;
}
let entry = self.classes.iter().find(|c| c.name == current)?;
if let Some(t) = entry.tag {
return Some((entry.name.as_str(), t));
}
current = entry.parent.as_deref()?;
}
}
pub fn tagged_ancestor_map(&self) -> std::collections::BTreeMap<String, (String, u16)> {
let mut out = std::collections::BTreeMap::new();
for c in &self.classes {
if let Some((anc, tag)) = self.tagged_ancestor(&c.name) {
out.insert(c.name.clone(), (anc.to_string(), tag));
}
}
out
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClassEntry {
pub name: String,
pub offset: usize,
pub fields: Vec<FieldEntry>,
pub tag: Option<u16>,
pub parent: Option<String>,
pub declared_field_count: Option<u32>,
#[serde(default)]
pub was_parent_only: bool,
#[serde(default)]
pub ancestor_tag: Option<u16>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldEntry {
pub name: String,
pub cpp_type: Option<String>,
pub field_type: Option<FieldType>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum FieldType {
Primitive { kind: u8, size: u8 },
String,
Guid,
ElementId,
ElementIdRef {
referenced_tag: u16,
sub: u16,
},
Pointer { kind: u8 },
Vector {
kind: u8,
body: Vec<u8>,
},
Container {
kind: u8,
cpp_signature: Option<String>,
body: Vec<u8>,
},
Unknown { bytes: Vec<u8> },
}
impl FieldType {
pub fn decode(bytes: &[u8]) -> Self {
if bytes.is_empty() {
return FieldType::Unknown { bytes: Vec::new() };
}
let sub = if bytes.len() >= 3 {
u16::from_le_bytes([bytes[1], bytes[2]])
} else if bytes.len() == 2 {
bytes[1] as u16
} else {
0
};
fn scalar_size(kind: u8) -> Option<u8> {
match kind {
0x01 => Some(1), 0x02 => Some(2), 0x03 => Some(4), 0x04 => Some(4), 0x05 => Some(4), 0x06 => Some(4), 0x07 => Some(8), 0x0b => Some(8), _ => None,
}
}
let is_vector_capable = matches!(
bytes[0],
0x01 | 0x02 | 0x04 | 0x05 | 0x06 | 0x07 | 0x0b | 0x0d
);
let body_after_header: Vec<u8> = bytes.get(4..).unwrap_or(&[]).to_vec();
match bytes[0] {
k if scalar_size(k).is_some() && sub == 0x0000 => FieldType::Primitive {
kind: k,
size: scalar_size(k).unwrap(),
},
k if is_vector_capable && sub == 0x0010 => FieldType::Vector {
kind: k,
body: body_after_header.clone(),
},
k if is_vector_capable && sub == 0x0050 => FieldType::Container {
kind: k,
cpp_signature: None,
body: Vec::new(),
},
0x08 if sub == 0x6000 || sub == 0x0060 => FieldType::String,
0x09 if sub == 0x0000 => FieldType::Guid,
0x0e => match sub {
0x0000 if bytes.len() >= 8 => {
let ref_tag = u16::from_le_bytes([bytes[4], bytes[5]]);
let sub2 = u16::from_le_bytes([bytes[6], bytes[7]]);
if ref_tag == 0x0014 && sub2 == 0x0000 {
FieldType::ElementId
} else {
FieldType::ElementIdRef {
referenced_tag: ref_tag,
sub: sub2,
}
}
}
0x0000 if bytes.len() >= 6 && bytes[4] == 0x14 && bytes[5] == 0x00 => {
FieldType::ElementId
}
0x0001..=0x0003 if bytes.len() >= 4 => FieldType::Pointer { kind: bytes[1] },
0x0010 | 0x0011 => FieldType::Vector {
kind: 0x0e,
body: body_after_header.clone(),
},
0x0050 | 0x0051 => extract_container(0x0e, &body_after_header),
_ => FieldType::Unknown {
bytes: bytes.to_vec(),
},
},
_ => FieldType::Unknown {
bytes: bytes.to_vec(),
},
}
}
pub fn encode(&self) -> Vec<u8> {
match self {
FieldType::Primitive { kind, .. } => vec![*kind, 0x00, 0x00, 0x00],
FieldType::String => vec![0x08, 0x00, 0x60, 0x00],
FieldType::Guid => vec![0x09, 0x00, 0x00, 0x00],
FieldType::ElementId => {
vec![0x0e, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00]
}
FieldType::ElementIdRef {
referenced_tag,
sub,
} => {
let tag_bytes = referenced_tag.to_le_bytes();
let sub_bytes = sub.to_le_bytes();
vec![
0x0e,
0x00,
0x00,
0x00,
tag_bytes[0],
tag_bytes[1],
sub_bytes[0],
sub_bytes[1],
]
}
FieldType::Pointer { kind } => vec![0x0e, *kind, 0x00, 0x00],
FieldType::Vector { kind, body } => {
let mut out = vec![*kind, 0x10, 0x00, 0x00];
out.extend_from_slice(body);
out
}
FieldType::Container { kind, body, .. } => {
let mut out = vec![*kind, 0x50, 0x00, 0x00];
out.extend_from_slice(body);
out
}
FieldType::Unknown { bytes } => bytes.clone(),
}
}
}
fn extract_container(kind: u8, body: &[u8]) -> FieldType {
let mut cpp_signature = None;
let mut k = 0;
while k + 2 < body.len() {
let slen = u16::from_le_bytes([body[k], body[k + 1]]) as usize;
if (3..=120).contains(&slen) && k + 2 + slen <= body.len() {
let sig = &body[k + 2..k + 2 + slen];
if sig.iter().all(|b| b.is_ascii_graphic() || *b == b' ')
&& sig.iter().any(|b| *b == b':' || *b == b'<')
{
cpp_signature = Some(std::str::from_utf8(sig).unwrap_or("").to_string());
break;
}
}
k += 1;
}
FieldType::Container {
kind,
cpp_signature,
body: body.to_vec(),
}
}
pub fn parse_schema(decompressed: &[u8]) -> Result<SchemaTable> {
parse_schema_with_scan_limit(decompressed, usize::MAX)
}
pub fn parse_schema_with_scan_limit(decompressed: &[u8], scan_limit: usize) -> Result<SchemaTable> {
let mut classes = Vec::new();
let mut cpp_types = std::collections::BTreeSet::new();
let mut skipped = 0usize;
let scan_truncated = decompressed.len() > scan_limit;
let data = if scan_truncated {
&decompressed[..scan_limit]
} else {
decompressed
};
let mut i = 0;
while i + 2 < data.len() {
let len = u16::from_le_bytes([data[i], data[i + 1]]) as usize;
if !(3..=60).contains(&len) {
i += 1;
continue;
}
let str_start = i + 2;
if str_start + len > data.len() {
i += 1;
continue;
}
let name_bytes = &data[str_start..str_start + len];
if !looks_like_class_name(name_bytes) {
i += 1;
continue;
}
let class_name = std::str::from_utf8(name_bytes).unwrap().to_string();
let class_offset = i;
let mut cursor = str_start + len;
let mut tag: Option<u16> = None;
let mut parent: Option<String> = None;
let mut declared_field_count: Option<u32> = None;
let mut ancestor_tag: Option<u16> = None;
if cursor + 2 <= data.len() {
let raw_tag = u16::from_le_bytes([data[cursor], data[cursor + 1]]);
if raw_tag & 0x8000 != 0 {
tag = Some(raw_tag & 0x7fff);
cursor += 2;
if cursor + 4 <= data.len() {
let pad = u16::from_le_bytes([data[cursor], data[cursor + 1]]);
let plen = u16::from_le_bytes([data[cursor + 2], data[cursor + 3]]) as usize;
if pad == 0 && (3..=40).contains(&plen) && cursor + 4 + plen <= data.len() {
let p = &data[cursor + 4..cursor + 4 + plen];
if looks_like_class_name(p) {
let preamble_at = cursor + 4 + plen;
if preamble_at + 10 <= data.len() {
let flag =
u16::from_le_bytes([data[preamble_at], data[preamble_at + 1]]);
let fc = u32::from_le_bytes([
data[preamble_at + 2],
data[preamble_at + 3],
data[preamble_at + 4],
data[preamble_at + 5],
]);
let fc2 = u32::from_le_bytes([
data[preamble_at + 6],
data[preamble_at + 7],
data[preamble_at + 8],
data[preamble_at + 9],
]);
if flag & 0x8000 == 0 && fc == fc2 && fc <= 200 {
parent = Some(std::str::from_utf8(p).unwrap().to_string());
declared_field_count = Some(fc);
if flag != 0 {
ancestor_tag = Some(flag);
}
cursor = preamble_at + 10;
}
}
}
}
}
}
}
let mut fields = Vec::new();
let (next_class_offset, found_fields) = scan_fields_until_next_class_bounded(
data,
cursor,
&mut cpp_types,
declared_field_count,
);
fields.extend(found_fields);
cursor = next_class_offset;
if class_name.is_empty() {
skipped += 1;
} else {
classes.push(ClassEntry {
name: class_name,
offset: class_offset,
fields,
tag,
parent,
declared_field_count,
was_parent_only: false,
ancestor_tag,
});
}
i = cursor.max(i + 1);
}
let declared_names: std::collections::BTreeSet<String> =
classes.iter().map(|c| c.name.clone()).collect();
let parent_names: std::collections::BTreeSet<String> =
classes.iter().filter_map(|c| c.parent.clone()).collect();
for parent_name in parent_names.difference(&declared_names) {
classes.push(ClassEntry {
name: parent_name.clone(),
offset: 0,
fields: Vec::new(),
tag: None,
parent: None,
declared_field_count: None,
was_parent_only: true,
ancestor_tag: None,
});
}
Ok(SchemaTable {
classes,
cpp_types: cpp_types.into_iter().collect(),
skipped_records: skipped,
scanned_bytes: data.len(),
total_bytes: decompressed.len(),
scan_truncated,
})
}
fn looks_like_class_name(bytes: &[u8]) -> bool {
if bytes.is_empty() {
return false;
}
let first = bytes[0];
if !first.is_ascii_uppercase() {
return false;
}
bytes[1..]
.iter()
.all(|c| c.is_ascii_alphanumeric() || *c == b'_')
}
fn looks_like_field_name(bytes: &[u8]) -> bool {
if bytes.is_empty() {
return false;
}
let first = bytes[0];
if !(first.is_ascii_alphanumeric() || first == b'_') {
return false;
}
bytes
.iter()
.all(|c| c.is_ascii_alphanumeric() || *c == b'_')
}
fn looks_like_cpp_type(bytes: &[u8]) -> bool {
if bytes.is_empty() {
return false;
}
let s = match std::str::from_utf8(bytes) {
Ok(v) => v,
Err(_) => return false,
};
s.chars().all(|c| {
c.is_ascii_alphanumeric()
|| matches!(
c,
':' | '<' | '>' | ',' | ' ' | '_' | '*' | '&' | '[' | ']' | '(' | ')'
)
}) && (s.chars().any(|c| c.is_ascii_uppercase())
|| s.contains("std::")
|| s.contains("int")
|| s.contains("double")
|| s.contains("long"))
}
#[allow(dead_code)]
fn scan_fields_until_next_class(
data: &[u8],
start: usize,
cpp_types: &mut std::collections::BTreeSet<String>,
) -> (usize, Vec<FieldEntry>) {
scan_fields_until_next_class_bounded(data, start, cpp_types, None)
}
fn scan_fields_until_next_class_bounded(
data: &[u8],
start: usize,
cpp_types: &mut std::collections::BTreeSet<String>,
max_fields: Option<u32>,
) -> (usize, Vec<FieldEntry>) {
let mut fields = Vec::new();
let mut i = start;
let hard_stop = (start + 4096).min(data.len());
while i + 4 < hard_stop {
if let Some(max) = max_fields {
if fields.len() as u32 >= max {
return (i, fields);
}
}
let u16_len = u16::from_le_bytes([data[i], data[i + 1]]) as usize;
if (4..=60).contains(&u16_len) && i + 2 + u16_len <= hard_stop {
let slice = &data[i + 2..i + 2 + u16_len];
if looks_like_class_name(slice) {
return (i, fields);
}
}
let u32_len = u32::from_le_bytes([data[i], data[i + 1], data[i + 2], data[i + 3]]) as usize;
if (2..=60).contains(&u32_len) && i + 4 + u32_len <= hard_stop {
let slice = &data[i + 4..i + 4 + u32_len];
if looks_like_field_name(slice) {
let field_name = std::str::from_utf8(slice).unwrap().to_string();
let post_name = i + 4 + u32_len;
let mut cpp_type = None;
let consumed = post_name;
let mut type_consumed_bytes = 0usize;
for (prefix_len, is_u32) in [(4usize, true), (2usize, false)] {
if consumed + prefix_len >= hard_stop {
continue;
}
let type_len = if is_u32 {
u32::from_le_bytes([
data[consumed],
data[consumed + 1],
data[consumed + 2],
data[consumed + 3],
]) as usize
} else {
u16::from_le_bytes([data[consumed], data[consumed + 1]]) as usize
};
if (3..=120).contains(&type_len)
&& consumed + prefix_len + type_len <= hard_stop
{
let type_slice =
&data[consumed + prefix_len..consumed + prefix_len + type_len];
if looks_like_cpp_type(type_slice) {
let ts = std::str::from_utf8(type_slice)
.unwrap_or_default()
.trim()
.to_string();
cpp_types.insert(ts.clone());
cpp_type = Some(ts);
type_consumed_bytes = prefix_len + type_len;
break;
}
}
}
let enc_end = (post_name + 32).min(hard_stop);
let field_type = if enc_end > post_name {
Some(FieldType::decode(&data[post_name..enc_end]))
} else {
None
};
if let Some(FieldType::Container {
cpp_signature: Some(sig),
..
}) = &field_type
{
cpp_types.insert(sig.clone());
if cpp_type.is_none() {
cpp_type = Some(sig.clone());
}
}
fields.push(FieldEntry {
name: field_name,
cpp_type,
field_type,
});
i = if type_consumed_bytes > 0 {
consumed + type_consumed_bytes
} else {
post_name
};
continue;
}
}
i += 1;
}
(i, fields)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SchemaClass {
pub tag: u16,
pub name: String,
pub base: Option<u16>,
pub version: u32,
pub field_count: u32,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SchemaClasses {
pub classes: Vec<SchemaClass>,
pub parsed_bytes: usize,
pub stopped: Option<String>,
}
impl SchemaClasses {
pub fn by_tag(&self, tag: u16) -> Option<&SchemaClass> {
let first = self.classes.first()?.tag;
let class = self.classes.get(usize::from(tag.checked_sub(first)?))?;
(class.tag == tag).then_some(class)
}
}
pub fn schema_classes(data: &[u8]) -> SchemaClasses {
let mut reader = GrammarReader {
data,
next_tag: FIRST_SCHEMA_TAG,
classes: Vec::new(),
};
let mut at = 0;
let stopped = loop {
if at + 2 > data.len() {
break None;
}
if reader.u16(at) != Some(0) {
break Some(format!("record separator is not 0 at {at:#x}"));
}
at += 2;
if at >= data.len() || data[at..].iter().all(|&b| b == 0) {
at = data.len();
break None;
}
let (tag, count) = (reader.next_tag, reader.classes.len());
match reader.class(at, 0) {
Ok((_, next)) => at = next,
Err(reason) => {
reader.classes.truncate(count);
reader.next_tag = tag;
break Some(reason);
}
}
};
reader.classes.sort_by_key(|class| class.tag);
SchemaClasses {
classes: reader.classes,
parsed_bytes: at,
stopped,
}
}
const FIRST_SCHEMA_TAG: u16 = 12;
const MAX_SCHEMA_NESTING: usize = 64;
struct GrammarReader<'a> {
data: &'a [u8],
next_tag: u16,
classes: Vec<SchemaClass>,
}
impl GrammarReader<'_> {
fn u16(&self, at: usize) -> Option<u16> {
let bytes = self.data.get(at..at.checked_add(2)?)?;
Some(u16::from_le_bytes([bytes[0], bytes[1]]))
}
fn u32(&self, at: usize) -> Option<u32> {
let bytes = self.data.get(at..at.checked_add(4)?)?;
Some(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
fn need<T>(value: Option<T>, what: &str, at: usize) -> std::result::Result<T, String> {
value.ok_or_else(|| format!("{what} past the end at {at:#x}"))
}
fn name(&self, at: usize, width: usize) -> std::result::Result<(String, usize), String> {
let len = if width == 2 {
usize::from(Self::need(self.u16(at), "name length", at)?)
} else {
Self::need(self.u32(at), "name length", at)? as usize
};
let start = at + width;
let raw = (1..=250)
.contains(&len)
.then(|| self.data.get(start..start + len))
.flatten()
.ok_or_else(|| format!("bad name length {len} at {at:#x}"))?;
if !raw.iter().all(|b| (0x20..0x7f).contains(b)) {
return Err(format!("name is not ASCII at {at:#x}"));
}
Ok((String::from_utf8_lossy(raw).into_owned(), start + len))
}
fn inline_class(
&mut self,
at: usize,
reference: u16,
depth: usize,
) -> std::result::Result<(u16, usize), String> {
if self.u16(at) != Some(0) {
return Err(format!("inline pad is not 0 at {at:#x}"));
}
let (tag, next) = self.class(at + 2, depth + 1)?;
if tag != reference & 0x7fff {
return Err(format!(
"inline tag {tag} does not match reference {} at {at:#x}",
reference & 0x7fff
));
}
Ok((tag, next))
}
fn class_ref(&mut self, at: usize, depth: usize) -> std::result::Result<(bool, usize), String> {
let reference = Self::need(self.u16(at), "class reference", at)?;
if reference & 0x8000 == 0 {
return Ok((true, at + 2));
}
let (_, next) = self.inline_class(at + 2, reference, depth)?;
Ok((true, next))
}
fn field_type(
&mut self,
at: usize,
depth: usize,
) -> std::result::Result<(bool, usize), String> {
if depth > MAX_SCHEMA_NESTING {
return Err(format!("types nest too deep at {at:#x}"));
}
let word = Self::need(self.u32(at), "type word", at)?;
let mut at = at + 4;
let (kind, modifier) = ((word & 0xff) as u8, ((word >> 8) & 0xff) as u8);
if !matches!(modifier & 0xf0, 0x00 | 0x10 | 0x50 | 0x60) {
return Err(format!("unknown modifier {modifier:#04x} at {:#x}", at - 4));
}
if modifier & 0xf0 == 0x10 {
let count = Self::need(self.u32(at), "array count", at)?;
if count > 65_536 {
return Err(format!("array count {count} at {at:#x}"));
}
at += 4;
}
if kind == 0x0d {
if self.data.get(at..at + 5) != Some(&[1, 0, 0, 0, 0x20][..]) {
return Err(format!("array filler at {at:#x}"));
}
let (inner_ref, next) = self.field_type(at + 5, depth + 1)?;
at = next;
if inner_ref {
return self.class_ref(at, depth);
}
return Ok((false, at));
}
if kind == 0x0e && modifier & 0x0f == 0 {
return self.class_ref(at, depth);
}
Ok((false, at))
}
fn class(&mut self, at: usize, depth: usize) -> std::result::Result<(u16, usize), String> {
if depth > MAX_SCHEMA_NESTING {
return Err(format!("classes nest too deep at {at:#x}"));
}
let (name, mut p) = self.name(at, 2)?;
let tag = self.next_tag;
self.next_tag = tag
.checked_add(1)
.filter(|next| *next < 0x8000)
.ok_or_else(|| format!("too many classes at {at:#x}"))?;
let reference = Self::need(self.u16(p), "base reference", p)?;
p += 2;
let base = if reference & 0x8000 != 0 {
let (base, next) = self.inline_class(p, reference, depth)?;
p = next;
Some(base)
} else {
(reference != 0).then_some(reference)
};
let version = Self::need(self.u32(p), "version", p)?;
let field_count = Self::need(self.u32(p + 4), "field count", p + 4)?;
p += 8;
if field_count > 2_000 {
return Err(format!("field count {field_count} at {:#x}", p - 4));
}
for _ in 0..field_count {
let (_, next) = self.name(p, 4)?;
let (_, next) = self.field_type(next, depth)?;
p = next;
}
let guids = Self::need(self.u32(p), "GUID count", p)? as usize;
p += 4;
p = guids
.checked_mul(16)
.and_then(|len| p.checked_add(len))
.filter(|end| *end <= self.data.len() && guids <= 60_000)
.ok_or_else(|| format!("GUID count {guids} at {:#x}", p - 4))?;
self.classes.push(SchemaClass {
tag,
name,
base,
version,
field_count,
});
Ok((tag, p))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn class_name_heuristic() {
assert!(looks_like_class_name(b"ADocument"));
assert!(looks_like_class_name(b"A3PartyObject"));
assert!(looks_like_class_name(b"APIEventHandlerStatus"));
assert!(!looks_like_class_name(b"lowercaseStart"));
assert!(!looks_like_class_name(b""));
assert!(!looks_like_class_name(b"Has-Dash"));
}
#[test]
fn field_name_heuristic() {
assert!(looks_like_field_name(b"m_id"));
assert!(looks_like_field_name(b"m_id64"));
assert!(looks_like_field_name(b"first"));
assert!(looks_like_field_name(b"second"));
assert!(!looks_like_field_name(b"Has Space"));
}
#[test]
fn cpp_type_heuristic() {
assert!(looks_like_cpp_type(b"std::pair< ElementId, double >"));
assert!(looks_like_cpp_type(b"ElementId"));
assert!(looks_like_cpp_type(b"int"));
assert!(!looks_like_cpp_type(b"m_id")); }
fn mk_class(name: &str, tag: Option<u16>, parent: Option<&str>) -> ClassEntry {
ClassEntry {
name: name.into(),
offset: 0,
fields: Vec::new(),
tag,
parent: parent.map(|s| s.into()),
declared_field_count: None,
was_parent_only: false,
ancestor_tag: None,
}
}
#[test]
fn tagged_ancestor_resolves_direct_tag() {
let schema = SchemaTable {
classes: vec![mk_class("ArcWall", Some(0x0191), Some("Wall"))],
cpp_types: vec![],
skipped_records: 0,
..Default::default()
};
let (anc, tag) = schema.tagged_ancestor("ArcWall").unwrap();
assert_eq!(anc, "ArcWall");
assert_eq!(tag, 0x0191);
}
#[test]
fn tagged_ancestor_walks_parent_chain() {
let schema = SchemaTable {
classes: vec![
mk_class("Wall", None, Some("HostObject")),
mk_class("HostObject", None, Some("HostObjAttr")),
mk_class("HostObjAttr", Some(0x006b), None),
],
cpp_types: vec![],
skipped_records: 0,
..Default::default()
};
let (anc, tag) = schema.tagged_ancestor("Wall").unwrap();
assert_eq!(anc, "HostObjAttr");
assert_eq!(tag, 0x006b);
}
#[test]
fn tagged_ancestor_returns_none_when_no_tag_in_chain() {
let schema = SchemaTable {
classes: vec![
mk_class("Abstract", None, Some("Root")),
mk_class("Root", None, None),
],
cpp_types: vec![],
skipped_records: 0,
..Default::default()
};
assert!(schema.tagged_ancestor("Abstract").is_none());
}
#[test]
fn tagged_ancestor_guards_against_cycles() {
let schema = SchemaTable {
classes: vec![
mk_class("A", None, Some("B")),
mk_class("B", None, Some("A")),
],
cpp_types: vec![],
skipped_records: 0,
..Default::default()
};
assert!(schema.tagged_ancestor("A").is_none());
}
#[test]
fn tagged_ancestor_unknown_class_is_none() {
let schema = SchemaTable {
classes: vec![mk_class("Known", Some(0x0001), None)],
cpp_types: vec![],
skipped_records: 0,
..Default::default()
};
assert!(schema.tagged_ancestor("Nonexistent").is_none());
}
#[test]
fn tagged_ancestor_map_covers_all_resolvable_classes() {
let schema = SchemaTable {
classes: vec![
mk_class("Wall", None, Some("HostObjAttr")),
mk_class("Floor", None, Some("HostObjAttr")),
mk_class("HostObjAttr", Some(0x006b), None),
mk_class("Unreachable", None, Some("MissingParent")),
],
cpp_types: vec![],
skipped_records: 0,
..Default::default()
};
let m = schema.tagged_ancestor_map();
assert_eq!(m.get("Wall").unwrap(), &("HostObjAttr".to_string(), 0x006b));
assert_eq!(
m.get("Floor").unwrap(),
&("HostObjAttr".to_string(), 0x006b)
);
assert_eq!(
m.get("HostObjAttr").unwrap(),
&("HostObjAttr".to_string(), 0x006b)
);
assert!(!m.contains_key("Unreachable"));
assert_eq!(m.len(), 3);
}
fn tiny_schema_prefix() -> Vec<u8> {
let mut buf = Vec::<u8>::new();
buf.extend_from_slice(&[0x0d, 0x00]); buf.extend_from_slice(b"ACDPtrWrapper");
buf.extend_from_slice(&[0x00, 0x00]); buf.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); buf.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); buf.extend_from_slice(&[0x06, 0x00, 0x00, 0x00]); buf.extend_from_slice(b"m_pACD");
buf
}
#[test]
fn scan_truncation_flag_unset_for_small_buffer() {
let buf = tiny_schema_prefix();
assert!(buf.len() < SCHEMA_SCAN_LIMIT);
let schema = parse_schema(&buf).unwrap();
assert!(!schema.scan_truncated);
assert_eq!(schema.scanned_bytes, buf.len());
assert_eq!(schema.total_bytes, buf.len());
}
#[test]
fn scan_truncation_flag_set_past_limit_without_changing_parse() {
let prefix = tiny_schema_prefix();
let baseline = parse_schema(&prefix).unwrap();
let mut buf = prefix.clone();
buf.resize(SCHEMA_SCAN_LIMIT + 4096, 0);
let schema = parse_schema_with_scan_limit(&buf, SCHEMA_SCAN_LIMIT).unwrap();
assert!(schema.scan_truncated);
assert_eq!(schema.scanned_bytes, SCHEMA_SCAN_LIMIT);
assert_eq!(schema.total_bytes, buf.len());
assert_eq!(
schema.classes.iter().map(|c| &c.name).collect::<Vec<_>>(),
baseline.classes.iter().map(|c| &c.name).collect::<Vec<_>>()
);
assert_eq!(schema.cpp_types, baseline.cpp_types);
}
#[test]
fn parses_sample_schema_snippet() {
let mut buf = Vec::<u8>::new();
buf.extend_from_slice(&[0x0d, 0x00]); buf.extend_from_slice(b"ACDPtrWrapper"); buf.extend_from_slice(&[0x00, 0x00]); buf.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); buf.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); buf.extend_from_slice(&[0x06, 0x00, 0x00, 0x00]); buf.extend_from_slice(b"m_pACD");
let schema = parse_schema(&buf).unwrap();
assert!(
schema.classes.iter().any(|c| c.name == "ACDPtrWrapper"),
"expected class ACDPtrWrapper, got {:?}",
schema.classes.iter().map(|c| &c.name).collect::<Vec<_>>()
);
let class = schema
.classes
.iter()
.find(|c| c.name == "ACDPtrWrapper")
.unwrap();
assert!(
class.fields.iter().any(|f| f.name == "m_pACD"),
"expected field m_pACD, got {:?}",
class.fields.iter().map(|f| &f.name).collect::<Vec<_>>()
);
}
#[test]
fn decodes_field_type_element_id() {
let bytes = [0x0e, 0x00, 0x00, 0x00, 0x14, 0x00];
assert_eq!(FieldType::decode(&bytes), FieldType::ElementId);
}
#[test]
fn decodes_field_type_pointer() {
let bytes = [0x0e, 0x02, 0x00, 0x00];
let ft = FieldType::decode(&bytes);
assert!(matches!(ft, FieldType::Pointer { kind: 0x02 }));
}
#[test]
fn decodes_field_type_primitive_u32_legacy() {
let bytes = [0x04, 0x00, 0x00, 0x00];
let ft = FieldType::decode(&bytes);
assert!(matches!(
ft,
FieldType::Primitive {
kind: 0x04,
size: 4
}
));
}
#[test]
fn decodes_field_type_primitive_bool() {
let bytes = [0x01, 0x00, 0x00, 0x00];
let ft = FieldType::decode(&bytes);
assert!(matches!(
ft,
FieldType::Primitive {
kind: 0x01,
size: 1
}
));
}
#[test]
fn decodes_field_type_primitive_f64() {
let bytes = [0x07, 0x00, 0x00, 0x00];
let ft = FieldType::decode(&bytes);
assert!(matches!(
ft,
FieldType::Primitive {
kind: 0x07,
size: 8
}
));
}
#[test]
fn decodes_field_type_string() {
let bytes = [0x08, 0x00, 0x60, 0x00];
let ft = FieldType::decode(&bytes);
assert!(matches!(ft, FieldType::String));
}
#[test]
fn decodes_field_type_guid() {
let bytes = [0x09, 0x00, 0x00, 0x00];
let ft = FieldType::decode(&bytes);
assert!(matches!(ft, FieldType::Guid));
}
#[test]
fn decodes_field_type_u64() {
let bytes = [0x0b, 0x00, 0x00, 0x00];
let ft = FieldType::decode(&bytes);
assert!(matches!(
ft,
FieldType::Primitive {
kind: 0x0b,
size: 8
}
));
}
#[test]
fn decodes_field_type_container_with_cpp_signature() {
let sig = b"std::pair< int, X >";
let mut bytes = vec![0x0e, 0x50, 0x00, 0x00];
bytes.extend_from_slice(&0x0000814au32.to_le_bytes());
bytes.extend_from_slice(&(sig.len() as u16).to_le_bytes());
bytes.extend_from_slice(sig);
let ft = FieldType::decode(&bytes);
match ft {
FieldType::Container {
kind,
cpp_signature,
..
} => {
assert_eq!(kind, 0x0e);
assert_eq!(cpp_signature.as_deref(), Some("std::pair< int, X >"));
}
other => panic!("expected Container, got {other:?}"),
}
}
#[test]
fn decodes_field_type_string_alt_encoding() {
let bytes = [0x08, 0x60, 0x00, 0x00];
assert!(matches!(FieldType::decode(&bytes), FieldType::String));
}
#[test]
fn decodes_field_type_primitive_deprecated_0x03() {
let bytes = [0x03, 0x00, 0x00, 0x00];
assert!(matches!(
FieldType::decode(&bytes),
FieldType::Primitive {
kind: 0x03,
size: 4,
}
));
}
#[test]
fn decodes_field_type_scalar_vector_f32() {
let bytes = [0x06, 0x10, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00];
let ft = FieldType::decode(&bytes);
match ft {
FieldType::Vector { kind, body } => {
assert_eq!(kind, 0x06);
assert_eq!(body, [0x03, 0x00, 0x00, 0x00]);
}
other => panic!("expected Vector<f32>, got {other:?}"),
}
}
#[test]
fn decodes_field_type_scalar_vector_i32() {
let bytes = [0x04, 0x10, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00];
let ft = FieldType::decode(&bytes);
assert!(matches!(&ft, FieldType::Vector { kind: 0x04, .. }));
}
#[test]
fn decodes_field_type_scalar_vector_bool() {
let bytes = [0x01, 0x10, 0x00, 0x00];
let ft = FieldType::decode(&bytes);
assert!(matches!(&ft, FieldType::Vector { kind: 0x01, .. }));
}
#[test]
fn decodes_field_type_point_vector_0x0d() {
let bytes = [0x0d, 0x10, 0x00, 0x00];
let ft = FieldType::decode(&bytes);
assert!(matches!(&ft, FieldType::Vector { kind: 0x0d, .. }));
}
#[test]
fn decodes_field_type_point_container_0x0d() {
let bytes = [0x0d, 0x50, 0x00, 0x00];
let ft = FieldType::decode(&bytes);
assert!(matches!(&ft, FieldType::Container { kind: 0x0d, .. }));
}
#[test]
fn decodes_field_type_scalar_container_u32() {
let bytes = [0x04, 0x50, 0x00, 0x00];
let ft = FieldType::decode(&bytes);
assert!(matches!(&ft, FieldType::Container { kind: 0x04, .. }));
}
#[test]
fn decodes_field_type_scalar_container_f64() {
let bytes = [0x07, 0x50, 0x00, 0x00];
let ft = FieldType::decode(&bytes);
assert!(matches!(&ft, FieldType::Container { kind: 0x07, .. }));
}
#[test]
fn decodes_field_type_element_id_ref() {
let bytes = [0x0e, 0x00, 0x00, 0x00, 0x58, 0x00, 0x09, 0x00];
let ft = FieldType::decode(&bytes);
match ft {
FieldType::ElementIdRef {
referenced_tag,
sub,
} => {
assert_eq!(referenced_tag, 0x0058);
assert_eq!(sub, 0x0009);
}
other => panic!("expected ElementIdRef, got {other:?}"),
}
}
#[test]
fn decodes_field_type_element_id_tag_0x14_8byte_still_canonical() {
let bytes = [0x0e, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00];
assert_eq!(FieldType::decode(&bytes), FieldType::ElementId);
}
#[test]
fn decodes_field_type_truncated_ref_container() {
let bytes = [0x0e, 0x50];
let ft = FieldType::decode(&bytes);
assert!(matches!(&ft, FieldType::Container { kind: 0x0e, .. }));
}
#[test]
fn decodes_field_type_truncated_point_vector() {
let bytes = [0x0d, 0x10];
let ft = FieldType::decode(&bytes);
assert!(matches!(&ft, FieldType::Vector { kind: 0x0d, .. }));
}
#[test]
fn decode_is_safe_on_short_inputs() {
for len in 0..=3 {
let input: Vec<u8> = (0..len).map(|_| 0xff).collect();
let _ = FieldType::decode(&input); }
}
#[test]
fn parses_tagged_class_with_parent() {
let mut buf = Vec::<u8>::new();
buf.extend_from_slice(&[0x0b, 0x00]);
buf.extend_from_slice(b"HostObjAttr");
buf.extend_from_slice(&[0x6b, 0x80]); buf.extend_from_slice(&[0x00, 0x00]); buf.extend_from_slice(&[0x06, 0x00]); buf.extend_from_slice(b"Symbol"); buf.extend_from_slice(&[0x25, 0x00]); buf.extend_from_slice(&[0x03, 0x00, 0x00, 0x00]); buf.extend_from_slice(&[0x03, 0x00, 0x00, 0x00]); buf.extend_from_slice(&[0x0c, 0x00, 0x00, 0x00]); buf.extend_from_slice(b"m_symbolInfo"); buf.extend_from_slice(&[0x0e, 0x02, 0x00, 0x00]); buf.resize(512, 0);
let schema = parse_schema(&buf).unwrap();
let class = schema
.classes
.iter()
.find(|c| c.name == "HostObjAttr")
.expect("HostObjAttr class not parsed");
assert_eq!(class.tag, Some(0x006b));
assert_eq!(class.parent.as_deref(), Some("Symbol"));
assert_eq!(class.declared_field_count, Some(3));
}
#[test]
fn schema_diagnostics_on_synthetic_table() {
let table = SchemaTable {
classes: vec![
ClassEntry {
name: "Wall".into(),
offset: 0,
fields: vec![
FieldEntry {
name: "m_id".into(),
cpp_type: None,
field_type: None,
},
FieldEntry {
name: "m_name".into(),
cpp_type: None,
field_type: None,
},
],
tag: Some(0x01),
parent: None,
declared_field_count: Some(2),
was_parent_only: false,
ancestor_tag: Some(0x42),
},
ClassEntry {
name: "Door".into(),
offset: 128,
fields: vec![FieldEntry {
name: "m_id".into(),
cpp_type: None,
field_type: None,
}],
tag: Some(0x02),
parent: Some("FamilyInstance".into()),
declared_field_count: Some(3),
was_parent_only: false,
ancestor_tag: None,
},
ClassEntry {
name: "FamilyInstance".into(),
offset: 256,
fields: vec![],
tag: None,
parent: None,
declared_field_count: None,
was_parent_only: true,
ancestor_tag: None,
},
],
cpp_types: vec!["ElementId".into(), "double".into()],
skipped_records: 7,
..Default::default()
};
let d = table.diagnostics();
assert_eq!(d.class_count, 3);
assert_eq!(d.parsed_field_count, 3);
assert_eq!(d.declared_field_count_sum, 5);
assert_eq!(d.field_count_mismatches, 1); assert_eq!(d.tagged_class_count, 2);
assert_eq!(d.parent_only_class_count, 1);
assert_eq!(d.ancestor_tag_count, 1);
assert_eq!(d.skipped_records, 7);
assert_eq!(d.cpp_type_count, 2);
}
#[test]
fn schema_diagnostics_empty_table_is_zero() {
let table = SchemaTable {
classes: vec![],
cpp_types: vec![],
skipped_records: 0,
..Default::default()
};
let d = table.diagnostics();
assert_eq!(d.class_count, 0);
assert_eq!(d.parsed_field_count, 0);
assert_eq!(d.declared_field_count_sum, 0);
assert_eq!(d.field_count_mismatches, 0);
assert_eq!(d.tagged_class_count, 0);
}
#[test]
fn encode_primitive_round_trips() {
for kind in [0x01_u8, 0x02, 0x04, 0x05, 0x06, 0x07, 0x0b] {
let original = FieldType::decode(&[kind, 0x00, 0x00, 0x00]);
let encoded = original.encode();
let decoded = FieldType::decode(&encoded);
assert_eq!(original, decoded, "primitive kind 0x{kind:02x} round-trip");
}
}
#[test]
fn encode_string_emits_canonical_form() {
let original = FieldType::String;
let encoded = original.encode();
assert_eq!(encoded, vec![0x08, 0x00, 0x60, 0x00]);
assert_eq!(FieldType::decode(&encoded), FieldType::String);
}
#[test]
fn encode_string_alt_form_collapses_on_round_trip() {
let alt = FieldType::decode(&[0x08, 0x60, 0x00, 0x00]);
assert_eq!(alt, FieldType::String);
let canonical = alt.encode();
assert_eq!(canonical, vec![0x08, 0x00, 0x60, 0x00]);
}
#[test]
fn encode_guid_round_trips() {
let g = FieldType::Guid;
assert_eq!(g.encode(), vec![0x09, 0x00, 0x00, 0x00]);
assert_eq!(FieldType::decode(&g.encode()), FieldType::Guid);
}
#[test]
fn encode_element_id_round_trips() {
let id = FieldType::ElementId;
let bytes = id.encode();
assert_eq!(bytes.len(), 8);
assert_eq!(FieldType::decode(&bytes), FieldType::ElementId);
}
#[test]
fn encode_element_id_ref_round_trips() {
let refed = FieldType::ElementIdRef {
referenced_tag: 0x0150,
sub: 0x0009,
};
let bytes = refed.encode();
let decoded = FieldType::decode(&bytes);
assert_eq!(decoded, refed);
}
#[test]
fn encode_pointer_round_trips() {
for kind in [0x01_u8, 0x02, 0x03] {
let p = FieldType::Pointer { kind };
let bytes = p.encode();
assert_eq!(FieldType::decode(&bytes), p);
}
}
#[test]
fn encode_vector_round_trips_for_scalar_bases() {
for kind in [0x01_u8, 0x02, 0x05, 0x06, 0x07, 0x0b] {
let body = vec![0xAA, 0xBB, 0xCC];
let v = FieldType::Vector {
kind,
body: body.clone(),
};
let bytes = v.encode();
assert_eq!(FieldType::decode(&bytes), v, "vector kind 0x{kind:02x}");
}
}
#[test]
fn encode_vector_reference_round_trips() {
let body = vec![0x14, 0x00, 0x00, 0x00];
let v = FieldType::Vector {
kind: 0x0e,
body: body.clone(),
};
let bytes = v.encode();
assert_eq!(FieldType::decode(&bytes), v);
}
#[test]
fn encode_container_round_trips_with_cpp_signature() {
let sig = b"std::map< unsigned, double >";
let mut body = Vec::new();
body.extend_from_slice(&(sig.len() as u16).to_le_bytes());
body.extend_from_slice(sig);
body.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); let c = FieldType::Container {
kind: 0x0e,
cpp_signature: Some("std::map< unsigned, double >".to_string()),
body: body.clone(),
};
let bytes = c.encode();
let decoded = FieldType::decode(&bytes);
assert_eq!(decoded, c);
}
#[test]
fn encode_unknown_emits_captured_bytes() {
let raw = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x42];
let u = FieldType::Unknown { bytes: raw.clone() };
assert_eq!(u.encode(), raw);
}
}