diff options
Diffstat (limited to 'crates/msh-core/src')
| -rw-r--r-- | crates/msh-core/src/error.rs | 75 | ||||
| -rw-r--r-- | crates/msh-core/src/lib.rs | 434 | ||||
| -rw-r--r-- | crates/msh-core/src/tests.rs | 438 |
3 files changed, 0 insertions, 947 deletions
diff --git a/crates/msh-core/src/error.rs b/crates/msh-core/src/error.rs deleted file mode 100644 index d46c7b1..0000000 --- a/crates/msh-core/src/error.rs +++ /dev/null @@ -1,75 +0,0 @@ -use core::fmt; - -#[derive(Debug)] -#[non_exhaustive] -pub enum Error { - Nres(nres::error::Error), - MissingResource { - kind: u32, - label: &'static str, - }, - InvalidResourceSize { - label: &'static str, - size: usize, - stride: usize, - }, - InvalidRes2Size { - size: usize, - }, - UnsupportedNodeStride { - stride: usize, - }, - IndexOutOfBounds { - label: &'static str, - index: usize, - limit: usize, - }, - IntegerOverflow, -} - -impl From<nres::error::Error> for Error { - fn from(value: nres::error::Error) -> Self { - Self::Nres(value) - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Nres(err) => write!(f, "{err}"), - Self::MissingResource { kind, label } => { - write!(f, "missing required resource type={kind} ({label})") - } - Self::InvalidResourceSize { - label, - size, - stride, - } => { - write!( - f, - "invalid {label} size={size}, expected multiple of stride={stride}" - ) - } - Self::InvalidRes2Size { size } => { - write!(f, "invalid Res2 size={size}, expected >= 140") - } - Self::UnsupportedNodeStride { stride } => { - write!( - f, - "unsupported Res1 node stride={stride}, expected 38 or 24" - ) - } - Self::IndexOutOfBounds { - label, - index, - limit, - } => write!( - f, - "{label} index out of bounds: index={index}, limit={limit}" - ), - Self::IntegerOverflow => write!(f, "integer overflow"), - } - } -} - -impl std::error::Error for Error {} diff --git a/crates/msh-core/src/lib.rs b/crates/msh-core/src/lib.rs deleted file mode 100644 index bc51357..0000000 --- a/crates/msh-core/src/lib.rs +++ /dev/null @@ -1,434 +0,0 @@ -pub mod error; - -use crate::error::Error; -use encoding_rs::WINDOWS_1251; -use std::sync::Arc; - -pub type Result<T> = core::result::Result<T, Error>; - -pub const RES1_NODE_TABLE: u32 = 1; -pub const RES2_SLOTS: u32 = 2; -pub const RES3_POSITIONS: u32 = 3; -pub const RES4_NORMALS: u32 = 4; -pub const RES5_UV0: u32 = 5; -pub const RES6_INDICES: u32 = 6; -pub const RES10_NAMES: u32 = 10; -pub const RES13_BATCHES: u32 = 13; - -#[derive(Clone, Debug)] -pub struct Slot { - pub tri_start: u16, - pub tri_count: u16, - pub batch_start: u16, - pub batch_count: u16, - pub aabb_min: [f32; 3], - pub aabb_max: [f32; 3], - pub sphere_center: [f32; 3], - pub sphere_radius: f32, - pub opaque: [u32; 5], -} - -#[derive(Clone, Debug)] -pub struct Batch { - pub batch_flags: u16, - pub material_index: u16, - pub opaque4: u16, - pub opaque6: u16, - pub index_count: u16, - pub index_start: u32, - pub opaque14: u16, - pub base_vertex: u32, -} - -#[derive(Clone, Debug)] -pub struct Model { - pub node_stride: usize, - pub node_count: usize, - pub nodes_raw: Vec<u8>, - pub slots: Vec<Slot>, - pub positions: Vec<[f32; 3]>, - pub normals: Option<Vec<[i8; 4]>>, - pub uv0: Option<Vec<[i16; 2]>>, - pub indices: Vec<u16>, - pub batches: Vec<Batch>, - pub node_names: Option<Vec<Option<String>>>, -} - -impl Model { - pub fn slot_index(&self, node_index: usize, lod: usize, group: usize) -> Option<usize> { - if node_index >= self.node_count || lod >= 3 || group >= 5 { - return None; - } - if self.node_stride != 38 { - return None; - } - let node_off = node_index.checked_mul(self.node_stride)?; - let matrix_off = node_off.checked_add(8)?; - let word_off = matrix_off.checked_add((lod * 5 + group) * 2)?; - let raw = read_u16(&self.nodes_raw, word_off).ok()?; - if raw == u16::MAX { - return None; - } - let idx = usize::from(raw); - if idx >= self.slots.len() { - return None; - } - Some(idx) - } -} - -pub fn parse_model_payload(payload: &[u8]) -> Result<Model> { - let archive = nres::Archive::open_bytes( - Arc::from(payload.to_vec().into_boxed_slice()), - nres::OpenOptions::default(), - )?; - - let res1 = read_required(&archive, RES1_NODE_TABLE, "Res1")?; - let res2 = read_required(&archive, RES2_SLOTS, "Res2")?; - let res3 = read_required(&archive, RES3_POSITIONS, "Res3")?; - let res6 = read_required(&archive, RES6_INDICES, "Res6")?; - let res13 = read_required(&archive, RES13_BATCHES, "Res13")?; - - let res4 = read_optional(&archive, RES4_NORMALS)?; - let res5 = read_optional(&archive, RES5_UV0)?; - let res10 = read_optional(&archive, RES10_NAMES)?; - - let node_stride = usize::try_from(res1.meta.attr3).map_err(|_| Error::IntegerOverflow)?; - if node_stride != 38 && node_stride != 24 { - return Err(Error::UnsupportedNodeStride { - stride: node_stride, - }); - } - if res1.bytes.len() % node_stride != 0 { - return Err(Error::InvalidResourceSize { - label: "Res1", - size: res1.bytes.len(), - stride: node_stride, - }); - } - let node_count = res1.bytes.len() / node_stride; - - if res2.bytes.len() < 0x8C { - return Err(Error::InvalidRes2Size { - size: res2.bytes.len(), - }); - } - let slot_blob = res2 - .bytes - .len() - .checked_sub(0x8C) - .ok_or(Error::IntegerOverflow)?; - if slot_blob % 68 != 0 { - return Err(Error::InvalidResourceSize { - label: "Res2.slots", - size: slot_blob, - stride: 68, - }); - } - let slot_count = slot_blob / 68; - let mut slots = Vec::with_capacity(slot_count); - for i in 0..slot_count { - let off = 0x8Cusize - .checked_add(i.checked_mul(68).ok_or(Error::IntegerOverflow)?) - .ok_or(Error::IntegerOverflow)?; - slots.push(Slot { - tri_start: read_u16(&res2.bytes, off)?, - tri_count: read_u16(&res2.bytes, off + 2)?, - batch_start: read_u16(&res2.bytes, off + 4)?, - batch_count: read_u16(&res2.bytes, off + 6)?, - aabb_min: [ - read_f32(&res2.bytes, off + 8)?, - read_f32(&res2.bytes, off + 12)?, - read_f32(&res2.bytes, off + 16)?, - ], - aabb_max: [ - read_f32(&res2.bytes, off + 20)?, - read_f32(&res2.bytes, off + 24)?, - read_f32(&res2.bytes, off + 28)?, - ], - sphere_center: [ - read_f32(&res2.bytes, off + 32)?, - read_f32(&res2.bytes, off + 36)?, - read_f32(&res2.bytes, off + 40)?, - ], - sphere_radius: read_f32(&res2.bytes, off + 44)?, - opaque: [ - read_u32(&res2.bytes, off + 48)?, - read_u32(&res2.bytes, off + 52)?, - read_u32(&res2.bytes, off + 56)?, - read_u32(&res2.bytes, off + 60)?, - read_u32(&res2.bytes, off + 64)?, - ], - }); - } - - let positions = parse_positions(&res3.bytes)?; - let indices = parse_u16_array(&res6.bytes, "Res6")?; - let batches = parse_batches(&res13.bytes)?; - validate_slot_batch_ranges(&slots, batches.len())?; - validate_batch_index_ranges(&batches, indices.len())?; - - let normals = match res4 { - Some(raw) => Some(parse_i8x4_array(&raw.bytes, "Res4")?), - None => None, - }; - let uv0 = match res5 { - Some(raw) => Some(parse_i16x2_array(&raw.bytes, "Res5")?), - None => None, - }; - let node_names = match res10 { - Some(raw) => Some(parse_res10_names(&raw.bytes, node_count)?), - None => None, - }; - - Ok(Model { - node_stride, - node_count, - nodes_raw: res1.bytes, - slots, - positions, - normals, - uv0, - indices, - batches, - node_names, - }) -} - -fn validate_slot_batch_ranges(slots: &[Slot], batch_count: usize) -> Result<()> { - for slot in slots { - let start = usize::from(slot.batch_start); - let end = start - .checked_add(usize::from(slot.batch_count)) - .ok_or(Error::IntegerOverflow)?; - if end > batch_count { - return Err(Error::IndexOutOfBounds { - label: "Res2.batch_range", - index: end, - limit: batch_count, - }); - } - } - Ok(()) -} - -fn validate_batch_index_ranges(batches: &[Batch], index_count: usize) -> Result<()> { - for batch in batches { - let start = usize::try_from(batch.index_start).map_err(|_| Error::IntegerOverflow)?; - let end = start - .checked_add(usize::from(batch.index_count)) - .ok_or(Error::IntegerOverflow)?; - if end > index_count { - return Err(Error::IndexOutOfBounds { - label: "Res13.index_range", - index: end, - limit: index_count, - }); - } - } - Ok(()) -} - -fn parse_positions(data: &[u8]) -> Result<Vec<[f32; 3]>> { - if !data.len().is_multiple_of(12) { - return Err(Error::InvalidResourceSize { - label: "Res3", - size: data.len(), - stride: 12, - }); - } - let count = data.len() / 12; - let mut out = Vec::with_capacity(count); - for i in 0..count { - let off = i * 12; - out.push([ - read_f32(data, off)?, - read_f32(data, off + 4)?, - read_f32(data, off + 8)?, - ]); - } - Ok(out) -} - -fn parse_batches(data: &[u8]) -> Result<Vec<Batch>> { - if !data.len().is_multiple_of(20) { - return Err(Error::InvalidResourceSize { - label: "Res13", - size: data.len(), - stride: 20, - }); - } - let count = data.len() / 20; - let mut out = Vec::with_capacity(count); - for i in 0..count { - let off = i * 20; - out.push(Batch { - batch_flags: read_u16(data, off)?, - material_index: read_u16(data, off + 2)?, - opaque4: read_u16(data, off + 4)?, - opaque6: read_u16(data, off + 6)?, - index_count: read_u16(data, off + 8)?, - index_start: read_u32(data, off + 10)?, - opaque14: read_u16(data, off + 14)?, - base_vertex: read_u32(data, off + 16)?, - }); - } - Ok(out) -} - -fn parse_u16_array(data: &[u8], label: &'static str) -> Result<Vec<u16>> { - if !data.len().is_multiple_of(2) { - return Err(Error::InvalidResourceSize { - label, - size: data.len(), - stride: 2, - }); - } - let mut out = Vec::with_capacity(data.len() / 2); - for i in (0..data.len()).step_by(2) { - out.push(read_u16(data, i)?); - } - Ok(out) -} - -fn parse_i8x4_array(data: &[u8], label: &'static str) -> Result<Vec<[i8; 4]>> { - if !data.len().is_multiple_of(4) { - return Err(Error::InvalidResourceSize { - label, - size: data.len(), - stride: 4, - }); - } - let mut out = Vec::with_capacity(data.len() / 4); - for i in (0..data.len()).step_by(4) { - out.push([ - read_i8(data, i)?, - read_i8(data, i + 1)?, - read_i8(data, i + 2)?, - read_i8(data, i + 3)?, - ]); - } - Ok(out) -} - -fn parse_i16x2_array(data: &[u8], label: &'static str) -> Result<Vec<[i16; 2]>> { - if !data.len().is_multiple_of(4) { - return Err(Error::InvalidResourceSize { - label, - size: data.len(), - stride: 4, - }); - } - let mut out = Vec::with_capacity(data.len() / 4); - for i in (0..data.len()).step_by(4) { - out.push([read_i16(data, i)?, read_i16(data, i + 2)?]); - } - Ok(out) -} - -fn parse_res10_names(data: &[u8], node_count: usize) -> Result<Vec<Option<String>>> { - let mut out = Vec::with_capacity(node_count); - let mut off = 0usize; - for _ in 0..node_count { - let len = usize::try_from(read_u32(data, off)?).map_err(|_| Error::IntegerOverflow)?; - off = off.checked_add(4).ok_or(Error::IntegerOverflow)?; - if len == 0 { - out.push(None); - continue; - } - let need = len.checked_add(1).ok_or(Error::IntegerOverflow)?; - let end = off.checked_add(need).ok_or(Error::IntegerOverflow)?; - let slice = data.get(off..end).ok_or(Error::InvalidResourceSize { - label: "Res10", - size: data.len(), - stride: 1, - })?; - let text = if slice.last().copied() == Some(0) { - &slice[..slice.len().saturating_sub(1)] - } else { - slice - }; - let decoded = decode_cp1251(text); - out.push(Some(decoded)); - off = end; - } - Ok(out) -} - -fn decode_cp1251(bytes: &[u8]) -> String { - let (decoded, _, _) = WINDOWS_1251.decode(bytes); - decoded.into_owned() -} - -struct RawResource { - meta: nres::EntryMeta, - bytes: Vec<u8>, -} - -fn read_required(archive: &nres::Archive, kind: u32, label: &'static str) -> Result<RawResource> { - let id = archive - .entries() - .find(|entry| entry.meta.kind == kind) - .map(|entry| entry.id) - .ok_or(Error::MissingResource { kind, label })?; - let entry = archive.get(id).ok_or(Error::IndexOutOfBounds { - label, - index: usize::try_from(id.0).map_err(|_| Error::IntegerOverflow)?, - limit: archive.entry_count(), - })?; - let data = archive.read(id)?.into_owned(); - Ok(RawResource { - meta: entry.meta.clone(), - bytes: data, - }) -} - -fn read_optional(archive: &nres::Archive, kind: u32) -> Result<Option<RawResource>> { - let Some(id) = archive - .entries() - .find(|entry| entry.meta.kind == kind) - .map(|entry| entry.id) - else { - return Ok(None); - }; - let entry = archive.get(id).ok_or(Error::IndexOutOfBounds { - label: "optional", - index: usize::try_from(id.0).map_err(|_| Error::IntegerOverflow)?, - limit: archive.entry_count(), - })?; - let data = archive.read(id)?.into_owned(); - Ok(Some(RawResource { - meta: entry.meta.clone(), - bytes: data, - })) -} - -fn read_u16(data: &[u8], offset: usize) -> Result<u16> { - let bytes = data.get(offset..offset + 2).ok_or(Error::IntegerOverflow)?; - let arr: [u8; 2] = bytes.try_into().map_err(|_| Error::IntegerOverflow)?; - Ok(u16::from_le_bytes(arr)) -} - -fn read_i16(data: &[u8], offset: usize) -> Result<i16> { - let bytes = data.get(offset..offset + 2).ok_or(Error::IntegerOverflow)?; - let arr: [u8; 2] = bytes.try_into().map_err(|_| Error::IntegerOverflow)?; - Ok(i16::from_le_bytes(arr)) -} - -fn read_i8(data: &[u8], offset: usize) -> Result<i8> { - let byte = data.get(offset).copied().ok_or(Error::IntegerOverflow)?; - Ok(i8::from_le_bytes([byte])) -} - -fn read_u32(data: &[u8], offset: usize) -> Result<u32> { - let bytes = data.get(offset..offset + 4).ok_or(Error::IntegerOverflow)?; - let arr: [u8; 4] = bytes.try_into().map_err(|_| Error::IntegerOverflow)?; - Ok(u32::from_le_bytes(arr)) -} - -fn read_f32(data: &[u8], offset: usize) -> Result<f32> { - Ok(f32::from_bits(read_u32(data, offset)?)) -} - -#[cfg(test)] -mod tests; diff --git a/crates/msh-core/src/tests.rs b/crates/msh-core/src/tests.rs deleted file mode 100644 index 90a7fdc..0000000 --- a/crates/msh-core/src/tests.rs +++ /dev/null @@ -1,438 +0,0 @@ -use super::*; -use common::collect_files_recursive; -use nres::Archive; -use proptest::prelude::*; -use std::fs; -use std::path::{Path, PathBuf}; - -fn nres_test_files() -> Vec<PathBuf> { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("testdata"); - let mut files = Vec::new(); - collect_files_recursive(&root, &mut files); - files.sort(); - files - .into_iter() - .filter(|path| { - fs::read(path) - .map(|bytes| bytes.get(0..4) == Some(b"NRes")) - .unwrap_or(false) - }) - .collect() -} - -fn is_msh_name(name: &str) -> bool { - name.to_ascii_lowercase().ends_with(".msh") -} - -#[derive(Clone)] -struct SyntheticEntry { - kind: u32, - name: String, - attr1: u32, - attr2: u32, - attr3: u32, - data: Vec<u8>, -} - -fn build_nested_nres(entries: &[SyntheticEntry]) -> Vec<u8> { - let mut payload = Vec::new(); - payload.extend_from_slice(b"NRes"); - payload.extend_from_slice(&0x100u32.to_le_bytes()); - payload.extend_from_slice( - &u32::try_from(entries.len()) - .expect("entry count overflow in test") - .to_le_bytes(), - ); - payload.extend_from_slice(&0u32.to_le_bytes()); // total_size placeholder - - let mut resource_offsets = Vec::with_capacity(entries.len()); - for entry in entries { - resource_offsets.push(u32::try_from(payload.len()).expect("offset overflow in test")); - payload.extend_from_slice(&entry.data); - while !payload.len().is_multiple_of(8) { - payload.push(0); - } - } - - for (index, entry) in entries.iter().enumerate() { - payload.extend_from_slice(&entry.kind.to_le_bytes()); - payload.extend_from_slice(&entry.attr1.to_le_bytes()); - payload.extend_from_slice(&entry.attr2.to_le_bytes()); - payload.extend_from_slice( - &u32::try_from(entry.data.len()) - .expect("size overflow in test") - .to_le_bytes(), - ); - payload.extend_from_slice(&entry.attr3.to_le_bytes()); - - let mut name_raw = [0u8; 36]; - let name_bytes = entry.name.as_bytes(); - assert!(name_bytes.len() <= 35, "name too long for synthetic test"); - name_raw[..name_bytes.len()].copy_from_slice(name_bytes); - payload.extend_from_slice(&name_raw); - - payload.extend_from_slice(&resource_offsets[index].to_le_bytes()); - payload.extend_from_slice(&(index as u32).to_le_bytes()); - } - - let total_size = u32::try_from(payload.len()).expect("size overflow in test"); - payload[12..16].copy_from_slice(&total_size.to_le_bytes()); - payload -} - -fn synthetic_entry(kind: u32, name: &str, attr3: u32, data: Vec<u8>) -> SyntheticEntry { - SyntheticEntry { - kind, - name: name.to_string(), - attr1: 1, - attr2: 0, - attr3, - data, - } -} - -fn res1_stride38_nodes(node_count: usize, node0_slot00: Option<u16>) -> Vec<u8> { - let mut out = vec![0u8; node_count.saturating_mul(38)]; - for node in 0..node_count { - let node_off = node * 38; - for i in 0..15 { - let off = node_off + 8 + i * 2; - out[off..off + 2].copy_from_slice(&u16::MAX.to_le_bytes()); - } - } - if let Some(slot) = node0_slot00 { - out[8..10].copy_from_slice(&slot.to_le_bytes()); - } - out -} - -fn res1_stride24_nodes(node_count: usize) -> Vec<u8> { - vec![0u8; node_count.saturating_mul(24)] -} - -fn res2_single_slot(batch_start: u16, batch_count: u16) -> Vec<u8> { - let mut res2 = vec![0u8; 0x8C + 68]; - res2[0x8C..0x8C + 2].copy_from_slice(&0u16.to_le_bytes()); // tri_start - res2[0x8C + 2..0x8C + 4].copy_from_slice(&0u16.to_le_bytes()); // tri_count - res2[0x8C + 4..0x8C + 6].copy_from_slice(&batch_start.to_le_bytes()); // batch_start - res2[0x8C + 6..0x8C + 8].copy_from_slice(&batch_count.to_le_bytes()); // batch_count - res2 -} - -fn res3_triangle_positions() -> Vec<u8> { - [0f32, 0f32, 0f32, 1f32, 0f32, 0f32, 0f32, 1f32, 0f32] - .iter() - .flat_map(|v| v.to_le_bytes()) - .collect() -} - -fn res4_normals() -> Vec<u8> { - vec![127u8, 0u8, 128u8, 0u8] -} - -fn res5_uv0() -> Vec<u8> { - [1024i16, -1024i16] - .iter() - .flat_map(|v| v.to_le_bytes()) - .collect() -} - -fn res6_triangle_indices() -> Vec<u8> { - [0u16, 1u16, 2u16] - .iter() - .flat_map(|v| v.to_le_bytes()) - .collect() -} - -fn res13_single_batch(index_start: u32, index_count: u16) -> Vec<u8> { - let mut batch = vec![0u8; 20]; - batch[0..2].copy_from_slice(&0u16.to_le_bytes()); - batch[2..4].copy_from_slice(&0u16.to_le_bytes()); - batch[8..10].copy_from_slice(&index_count.to_le_bytes()); - batch[10..14].copy_from_slice(&index_start.to_le_bytes()); - batch[16..20].copy_from_slice(&0u32.to_le_bytes()); - batch -} - -fn res10_names_raw(names: &[Option<&[u8]>]) -> Vec<u8> { - let mut out = Vec::new(); - for name in names { - match name { - Some(name) => { - out.extend_from_slice( - &u32::try_from(name.len()) - .expect("name size overflow in test") - .to_le_bytes(), - ); - out.extend_from_slice(name); - out.push(0); - } - None => out.extend_from_slice(&0u32.to_le_bytes()), - } - } - out -} - -fn res10_names(names: &[Option<&str>]) -> Vec<u8> { - let raw: Vec<Option<&[u8]>> = names.iter().map(|name| name.map(str::as_bytes)).collect(); - res10_names_raw(&raw) -} - -fn base_synthetic_entries() -> Vec<SyntheticEntry> { - vec![ - synthetic_entry(RES1_NODE_TABLE, "Res1", 38, res1_stride38_nodes(1, Some(0))), - synthetic_entry(RES2_SLOTS, "Res2", 68, res2_single_slot(0, 1)), - synthetic_entry(RES3_POSITIONS, "Res3", 12, res3_triangle_positions()), - synthetic_entry(RES6_INDICES, "Res6", 2, res6_triangle_indices()), - synthetic_entry(RES13_BATCHES, "Res13", 20, res13_single_batch(0, 3)), - ] -} - -#[test] -fn parse_all_game_msh_models() { - let archives = nres_test_files(); - if archives.is_empty() { - eprintln!("skipping parse_all_game_msh_models: no NRes files in testdata"); - return; - } - - let mut model_count = 0usize; - let mut renderable_count = 0usize; - let mut legacy_stride24_count = 0usize; - - for archive_path in archives { - let archive = Archive::open_path(&archive_path) - .unwrap_or_else(|err| panic!("failed to open {}: {err}", archive_path.display())); - - for entry in archive.entries() { - if !is_msh_name(&entry.meta.name) { - continue; - } - model_count += 1; - let payload = archive.read(entry.id).unwrap_or_else(|err| { - panic!( - "failed to read model '{}' in {}: {err}", - entry.meta.name, - archive_path.display() - ) - }); - let model = parse_model_payload(payload.as_slice()).unwrap_or_else(|err| { - panic!( - "failed to parse model '{}' in {}: {err}", - entry.meta.name, - archive_path.display() - ) - }); - - if model.node_stride == 24 { - legacy_stride24_count += 1; - } - - for node_index in 0..model.node_count { - for lod in 0..3 { - for group in 0..5 { - if let Some(slot_idx) = model.slot_index(node_index, lod, group) { - assert!( - slot_idx < model.slots.len(), - "slot index out of bounds in '{}' ({})", - entry.meta.name, - archive_path.display() - ); - } - } - } - } - - let mut has_renderable_batch = false; - for node_index in 0..model.node_count { - let Some(slot_idx) = model.slot_index(node_index, 0, 0) else { - continue; - }; - let slot = &model.slots[slot_idx]; - let batch_end = - usize::from(slot.batch_start).saturating_add(usize::from(slot.batch_count)); - if batch_end > model.batches.len() { - continue; - } - for batch in &model.batches[usize::from(slot.batch_start)..batch_end] { - let index_start = usize::try_from(batch.index_start).unwrap_or(usize::MAX); - let index_count = usize::from(batch.index_count); - let end = index_start.saturating_add(index_count); - if end <= model.indices.len() && index_count >= 3 { - has_renderable_batch = true; - break; - } - } - if has_renderable_batch { - break; - } - } - if has_renderable_batch { - renderable_count += 1; - } - } - } - - assert!(model_count > 0, "no .msh entries found"); - assert!( - renderable_count > 0, - "no renderable models (lod0/group0) were detected" - ); - assert!( - legacy_stride24_count <= model_count, - "internal test accounting error" - ); -} - -#[test] -fn parse_minimal_synthetic_model() { - let payload = build_nested_nres(&base_synthetic_entries()); - let model = parse_model_payload(&payload).expect("failed to parse synthetic model"); - assert_eq!(model.node_count, 1); - assert_eq!(model.positions.len(), 3); - assert_eq!(model.indices.len(), 3); - assert_eq!(model.batches.len(), 1); - assert_eq!(model.slot_index(0, 0, 0), Some(0)); -} - -#[test] -fn parse_synthetic_stride24_variant() { - let mut entries = base_synthetic_entries(); - entries[0] = synthetic_entry(RES1_NODE_TABLE, "Res1", 24, res1_stride24_nodes(1)); - let payload = build_nested_nres(&entries); - - let model = parse_model_payload(&payload).expect("failed to parse stride24 model"); - assert_eq!(model.node_stride, 24); - assert_eq!(model.node_count, 1); - assert_eq!(model.slot_index(0, 0, 0), None); -} - -#[test] -fn parse_synthetic_model_with_optional_res4_res5_res10() { - let mut entries = base_synthetic_entries(); - entries.push(synthetic_entry(RES4_NORMALS, "Res4", 4, res4_normals())); - entries.push(synthetic_entry(RES5_UV0, "Res5", 4, res5_uv0())); - entries.push(synthetic_entry( - RES10_NAMES, - "Res10", - 1, - res10_names(&[Some("Hull"), None]), - )); - entries[0] = synthetic_entry(RES1_NODE_TABLE, "Res1", 38, res1_stride38_nodes(2, Some(0))); - let payload = build_nested_nres(&entries); - - let model = parse_model_payload(&payload).expect("failed to parse model with optional data"); - assert_eq!(model.node_count, 2); - assert_eq!(model.normals.as_ref().map(Vec::len), Some(1)); - assert_eq!(model.uv0.as_ref().map(Vec::len), Some(1)); - assert_eq!(model.node_names, Some(vec![Some("Hull".to_string()), None])); -} - -#[test] -fn parse_res10_names_decodes_cp1251() { - let mut entries = base_synthetic_entries(); - entries[0] = synthetic_entry(RES1_NODE_TABLE, "Res1", 38, res1_stride38_nodes(1, Some(0))); - entries.push(synthetic_entry( - RES10_NAMES, - "Res10", - 1, - res10_names_raw(&[Some(&[0xC0])]), - )); - let payload = build_nested_nres(&entries); - - let model = parse_model_payload(&payload).expect("failed to parse model with cp1251 name"); - assert_eq!(model.node_names, Some(vec![Some("А".to_string())])); -} - -#[test] -fn parse_fails_when_required_resource_missing() { - let mut entries = base_synthetic_entries(); - entries.retain(|entry| entry.kind != RES13_BATCHES); - let payload = build_nested_nres(&entries); - - assert!(matches!( - parse_model_payload(&payload), - Err(Error::MissingResource { - kind: RES13_BATCHES, - label: "Res13" - }) - )); -} - -#[test] -fn parse_fails_for_invalid_res2_size() { - let mut entries = base_synthetic_entries(); - entries[1] = synthetic_entry(RES2_SLOTS, "Res2", 68, vec![0u8; 0x8B]); - let payload = build_nested_nres(&entries); - - assert!(matches!( - parse_model_payload(&payload), - Err(Error::InvalidRes2Size { .. }) - )); -} - -#[test] -fn parse_fails_for_unsupported_node_stride() { - let mut entries = base_synthetic_entries(); - entries[0] = synthetic_entry(RES1_NODE_TABLE, "Res1", 30, vec![0u8; 30]); - let payload = build_nested_nres(&entries); - - assert!(matches!( - parse_model_payload(&payload), - Err(Error::UnsupportedNodeStride { stride: 30 }) - )); -} - -#[test] -fn parse_fails_for_invalid_optional_resource_size() { - let mut entries = base_synthetic_entries(); - entries.push(synthetic_entry(RES4_NORMALS, "Res4", 4, vec![1, 2, 3])); - let payload = build_nested_nres(&entries); - - assert!(matches!( - parse_model_payload(&payload), - Err(Error::InvalidResourceSize { label: "Res4", .. }) - )); -} - -#[test] -fn parse_fails_for_slot_batch_range_out_of_bounds() { - let mut entries = base_synthetic_entries(); - entries[1] = synthetic_entry(RES2_SLOTS, "Res2", 68, res2_single_slot(0, 2)); - let payload = build_nested_nres(&entries); - - assert!(matches!( - parse_model_payload(&payload), - Err(Error::IndexOutOfBounds { - label: "Res2.batch_range", - .. - }) - )); -} - -#[test] -fn parse_fails_for_batch_index_range_out_of_bounds() { - let mut entries = base_synthetic_entries(); - entries[4] = synthetic_entry(RES13_BATCHES, "Res13", 20, res13_single_batch(1, 3)); - let payload = build_nested_nres(&entries); - - assert!(matches!( - parse_model_payload(&payload), - Err(Error::IndexOutOfBounds { - label: "Res13.index_range", - .. - }) - )); -} - -proptest! { - #![proptest_config(ProptestConfig::with_cases(64))] - - #[test] - fn parse_model_payload_never_panics_on_random_bytes(data in proptest::collection::vec(any::<u8>(), 0..8192)) { - let _ = parse_model_payload(&data); - } -} |
