use crate::formats::ClassEntry;
use crate::walker::{
Completeness, DecodedElement, InstanceField, decode_instance, encode_instance,
};
#[derive(Debug, Clone)]
pub struct RoundTripReport {
pub original_len: usize,
pub encoded_len: usize,
pub completeness: Completeness,
pub byte_exact: bool,
pub first_diff_at: Option<usize>,
pub first_diff_field: Option<usize>,
}
impl RoundTripReport {
pub fn is_byte_exact(&self) -> bool {
self.byte_exact && self.original_len == self.encoded_len
}
pub fn is_typed_clean(&self) -> bool {
self.completeness.raw_bytes_fallback == 0 && self.completeness.total > 0
}
}
pub fn verify_instance_round_trip(
bytes: &[u8],
offset: usize,
class: &ClassEntry,
) -> RoundTripReport {
let decoded = decode_instance(bytes, offset, class);
let consumed = &bytes[decoded.byte_range.clone()];
let encoded = encode_instance(&decoded, class);
let byte_exact = encoded.as_slice() == consumed;
let (first_diff_at, first_diff_field) = if byte_exact {
(None, None)
} else {
let diff = consumed
.iter()
.zip(encoded.iter())
.position(|(a, b)| a != b)
.or(Some(consumed.len().min(encoded.len())));
(diff, locate_diff_field(&decoded, class, diff))
};
RoundTripReport {
original_len: consumed.len(),
encoded_len: encoded.len(),
completeness: completeness_of(&decoded),
byte_exact,
first_diff_at,
first_diff_field,
}
}
fn completeness_of(d: &DecodedElement) -> Completeness {
let mut c = Completeness {
total: d.fields.len(),
..Completeness::default()
};
for (_, value) in &d.fields {
match value {
InstanceField::Bytes(b) => {
c.raw_bytes_fallback += 1;
if !b.is_empty() {
}
}
_ => {
c.typed += 1;
if !matches!(value, InstanceField::Bytes(b) if b.is_empty()) {
c.typed_and_non_empty += 1;
}
}
}
}
c
}
fn locate_diff_field(
decoded: &DecodedElement,
class: &ClassEntry,
target: Option<usize>,
) -> Option<usize> {
let target = target?;
let mut cursor = 0;
for (idx, schema_field) in class.fields.iter().enumerate() {
let Some((_, value)) = decoded.fields.get(idx) else {
break;
};
if let Some(ft) = schema_field.field_type.as_ref() {
let mut chunk = Vec::new();
crate::walker::write_field_by_type(value, ft, &mut chunk);
if cursor + chunk.len() > target {
return Some(idx);
}
cursor += chunk.len();
} else if let InstanceField::Bytes(raw) = value {
if cursor + raw.len() > target {
return Some(idx);
}
cursor += raw.len();
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::formats::{FieldEntry, FieldType};
fn mk_class(fields: Vec<(&str, FieldType)>) -> ClassEntry {
ClassEntry {
name: "TestClass".into(),
offset: 0,
fields: fields
.into_iter()
.map(|(name, ft)| FieldEntry {
name: name.to_string(),
cpp_type: None,
field_type: Some(ft),
})
.collect(),
tag: None,
parent: None,
declared_field_count: None,
was_parent_only: false,
ancestor_tag: None,
}
}
#[test]
fn round_trip_primitives_is_byte_exact() {
let class = mk_class(vec![
(
"id",
FieldType::Primitive {
kind: 0x05,
size: 4,
},
),
(
"value",
FieldType::Primitive {
kind: 0x07,
size: 8,
},
),
]);
let mut bytes = Vec::new();
bytes.extend_from_slice(&0x42_u32.to_le_bytes());
bytes.extend_from_slice(&2.5_f64.to_le_bytes());
let report = verify_instance_round_trip(&bytes, 0, &class);
assert!(report.is_byte_exact());
assert!(report.is_typed_clean());
assert_eq!(report.original_len, 12);
assert_eq!(report.encoded_len, 12);
assert_eq!(report.completeness.total, 2);
assert_eq!(report.completeness.typed, 2);
}
#[test]
fn round_trip_string_is_byte_exact() {
let class = mk_class(vec![("name", FieldType::String)]);
let text = "hello";
let utf16: Vec<u16> = text.encode_utf16().collect();
let mut bytes = Vec::new();
bytes.extend_from_slice(&(utf16.len() as u32).to_le_bytes());
for c in &utf16 {
bytes.extend_from_slice(&c.to_le_bytes());
}
let report = verify_instance_round_trip(&bytes, 0, &class);
assert!(report.is_byte_exact());
}
#[test]
fn round_trip_element_id_is_byte_exact() {
let class = mk_class(vec![("m_id", FieldType::ElementId)]);
let mut bytes = Vec::new();
bytes.extend_from_slice(&0x14_u32.to_le_bytes());
bytes.extend_from_slice(&0x1337_u32.to_le_bytes());
let report = verify_instance_round_trip(&bytes, 0, &class);
assert!(report.is_byte_exact());
}
#[test]
fn round_trip_reports_first_diff_on_mismatch() {
let class = mk_class(vec![(
"id",
FieldType::Primitive {
kind: 0x05,
size: 4,
},
)]);
let bytes = 0x2a_u32.to_le_bytes();
let report = verify_instance_round_trip(&bytes, 0, &class);
assert!(report.is_byte_exact());
assert_eq!(report.first_diff_at, None);
assert_eq!(report.first_diff_field, None);
}
#[test]
fn round_trip_typed_clean_requires_typed_fields() {
let class = ClassEntry {
name: "TestClass".into(),
offset: 0,
fields: vec![FieldEntry {
name: "weird".into(),
cpp_type: None,
field_type: Some(FieldType::Unknown { bytes: vec![0xAA] }),
}],
tag: None,
parent: None,
declared_field_count: None,
was_parent_only: false,
ancestor_tag: None,
};
let bytes = vec![0xAA, 0xBB, 0xCC, 0xDD];
let report = verify_instance_round_trip(&bytes, 0, &class);
assert!(!report.is_typed_clean());
assert!(report.is_byte_exact());
}
#[test]
fn round_trip_empty_instance_is_empty() {
let class = mk_class(Vec::new());
let report = verify_instance_round_trip(&[], 0, &class);
assert_eq!(report.original_len, 0);
assert_eq!(report.encoded_len, 0);
assert!(!report.is_typed_clean());
assert!(report.is_byte_exact());
}
#[test]
fn round_trip_multi_field_heterogeneous_is_byte_exact() {
let class = mk_class(vec![
(
"flag",
FieldType::Primitive {
kind: 0x01,
size: 1,
},
),
(
"count",
FieldType::Primitive {
kind: 0x05,
size: 4,
},
),
("name", FieldType::String),
("ref", FieldType::ElementId),
]);
let mut bytes = Vec::new();
bytes.push(1_u8); bytes.extend_from_slice(&42_u32.to_le_bytes()); let utf16: Vec<u16> = "ab".encode_utf16().collect();
bytes.extend_from_slice(&(utf16.len() as u32).to_le_bytes());
for c in &utf16 {
bytes.extend_from_slice(&c.to_le_bytes());
}
bytes.extend_from_slice(&0x14_u32.to_le_bytes());
bytes.extend_from_slice(&0x99_u32.to_le_bytes());
let report = verify_instance_round_trip(&bytes, 0, &class);
assert!(report.is_byte_exact(), "{report:?}");
assert!(report.is_typed_clean());
assert_eq!(report.completeness.total, 4);
assert_eq!(report.completeness.typed, 4);
}
#[test]
fn round_trip_vector_of_doubles_is_byte_exact() {
let class = mk_class(vec![(
"values",
FieldType::Vector {
kind: 0x07,
body: Vec::new(),
},
)]);
let values = [1.5_f64, -2.5, 42.0];
let mut bytes = Vec::new();
bytes.extend_from_slice(&(values.len() as u32).to_le_bytes());
for v in &values {
bytes.extend_from_slice(&v.to_le_bytes());
}
let report = verify_instance_round_trip(&bytes, 0, &class);
assert!(report.is_byte_exact());
}
#[test]
fn round_trip_with_offset_decodes_suffix() {
let class = mk_class(vec![(
"id",
FieldType::Primitive {
kind: 0x05,
size: 4,
},
)]);
let mut bytes = vec![0xFF, 0xFF, 0xFF]; bytes.extend_from_slice(&0x2a_u32.to_le_bytes());
let report = verify_instance_round_trip(&bytes, 3, &class);
assert!(report.is_byte_exact());
assert_eq!(report.original_len, 4);
assert_eq!(report.encoded_len, 4);
}
}