diff options
Diffstat (limited to 'crates/texm/src')
| -rw-r--r-- | crates/texm/src/error.rs | 86 | ||||
| -rw-r--r-- | crates/texm/src/lib.rs | 417 | ||||
| -rw-r--r-- | crates/texm/src/tests.rs | 330 |
3 files changed, 0 insertions, 833 deletions
diff --git a/crates/texm/src/error.rs b/crates/texm/src/error.rs deleted file mode 100644 index 90d618d..0000000 --- a/crates/texm/src/error.rs +++ /dev/null @@ -1,86 +0,0 @@ -use core::fmt; - -#[derive(Debug)] -#[non_exhaustive] -pub enum Error { - HeaderTooSmall { - size: usize, - }, - InvalidMagic { - got: u32, - }, - InvalidDimensions { - width: u32, - height: u32, - }, - InvalidMipCount { - mip_count: u32, - }, - UnknownFormat { - format: u32, - }, - IntegerOverflow, - CoreDataOutOfBounds { - expected_end: usize, - actual_size: usize, - }, - MipIndexOutOfRange { - requested: usize, - mip_count: usize, - }, - MipDataOutOfBounds { - offset: usize, - size: usize, - payload_size: usize, - }, - InvalidPageMagic, - InvalidPageSize { - expected: usize, - actual: usize, - }, -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::HeaderTooSmall { size } => { - write!(f, "Texm payload too small for header: {size}") - } - Self::InvalidMagic { got } => write!(f, "invalid Texm magic: 0x{got:08X}"), - Self::InvalidDimensions { width, height } => { - write!(f, "invalid Texm dimensions: {width}x{height}") - } - Self::InvalidMipCount { mip_count } => write!(f, "invalid Texm mip_count={mip_count}"), - Self::UnknownFormat { format } => write!(f, "unknown Texm format={format}"), - Self::IntegerOverflow => write!(f, "integer overflow"), - Self::CoreDataOutOfBounds { - expected_end, - actual_size, - } => write!( - f, - "Texm core data out of bounds: expected_end={expected_end}, actual_size={actual_size}" - ), - Self::MipIndexOutOfRange { - requested, - mip_count, - } => write!( - f, - "Texm mip index out of range: requested={requested}, mip_count={mip_count}" - ), - Self::MipDataOutOfBounds { - offset, - size, - payload_size, - } => write!( - f, - "Texm mip data out of bounds: offset={offset}, size={size}, payload_size={payload_size}" - ), - Self::InvalidPageMagic => write!(f, "Texm tail exists but Page magic is missing"), - Self::InvalidPageSize { expected, actual } => { - write!(f, "invalid Page chunk size: expected={expected}, actual={actual}") - } - } - } -} - -impl std::error::Error for Error {} diff --git a/crates/texm/src/lib.rs b/crates/texm/src/lib.rs deleted file mode 100644 index 7a166f3..0000000 --- a/crates/texm/src/lib.rs +++ /dev/null @@ -1,417 +0,0 @@ -pub mod error; - -use crate::error::Error; - -pub type Result<T> = core::result::Result<T, Error>; - -pub const TEXM_MAGIC: u32 = 0x6D78_6554; -pub const PAGE_MAGIC: u32 = 0x6567_6150; - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum PixelFormat { - Indexed8, - Rgb565, - Rgb556, - Argb4444, - LuminanceAlpha88, - Rgb888, - Argb8888, -} - -impl PixelFormat { - pub fn from_raw(raw: u32) -> Option<Self> { - match raw { - 0 => Some(Self::Indexed8), - 565 => Some(Self::Rgb565), - 556 => Some(Self::Rgb556), - 4444 => Some(Self::Argb4444), - 88 => Some(Self::LuminanceAlpha88), - 888 => Some(Self::Rgb888), - 8888 => Some(Self::Argb8888), - _ => None, - } - } - - pub fn bytes_per_pixel(self) -> usize { - match self { - Self::Indexed8 => 1, - Self::Rgb565 | Self::Rgb556 | Self::Argb4444 | Self::LuminanceAlpha88 => 2, - // Parkan stores format 888 as 32-bit RGBX in texture payloads. - Self::Rgb888 | Self::Argb8888 => 4, - } - } -} - -#[derive(Clone, Debug)] -pub struct Header { - pub width: u32, - pub height: u32, - pub mip_count: u32, - pub flags4: u32, - pub flags5: u32, - pub unk6: u32, - pub format_raw: u32, - pub format: PixelFormat, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct MipLevel { - pub width: u32, - pub height: u32, - pub offset: usize, - pub size: usize, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct PageRect { - pub x: i16, - pub w: i16, - pub y: i16, - pub h: i16, -} - -#[derive(Clone, Debug)] -pub struct Texture { - pub header: Header, - pub palette: Option<[u8; 1024]>, - pub mip_levels: Vec<MipLevel>, - pub page_rects: Vec<PageRect>, -} - -impl Texture { - pub fn core_size(&self) -> usize { - let mut size = 32usize; - if self.palette.is_some() { - size += 1024; - } - for level in &self.mip_levels { - size += level.size; - } - size - } -} - -#[derive(Clone, Debug)] -pub struct DecodedMip { - pub width: u32, - pub height: u32, - pub rgba8: Vec<u8>, -} - -pub fn parse_texm(payload: &[u8]) -> Result<Texture> { - if payload.len() < 32 { - return Err(Error::HeaderTooSmall { - size: payload.len(), - }); - } - - let magic = read_u32(payload, 0)?; - if magic != TEXM_MAGIC { - return Err(Error::InvalidMagic { got: magic }); - } - - let width = read_u32(payload, 4)?; - let height = read_u32(payload, 8)?; - let mip_count = read_u32(payload, 12)?; - let flags4 = read_u32(payload, 16)?; - let flags5 = read_u32(payload, 20)?; - let unk6 = read_u32(payload, 24)?; - let format_raw = read_u32(payload, 28)?; - - if width == 0 || height == 0 { - return Err(Error::InvalidDimensions { width, height }); - } - if mip_count == 0 { - return Err(Error::InvalidMipCount { mip_count }); - } - - let format = - PixelFormat::from_raw(format_raw).ok_or(Error::UnknownFormat { format: format_raw })?; - let bytes_per_pixel = format.bytes_per_pixel(); - - let mut offset = 32usize; - let palette = if format == PixelFormat::Indexed8 { - let end = offset.checked_add(1024).ok_or(Error::IntegerOverflow)?; - if end > payload.len() { - return Err(Error::CoreDataOutOfBounds { - expected_end: end, - actual_size: payload.len(), - }); - } - let mut pal = [0u8; 1024]; - pal.copy_from_slice(&payload[offset..end]); - offset = end; - Some(pal) - } else { - None - }; - - let mut mip_levels = - Vec::with_capacity(usize::try_from(mip_count).map_err(|_| Error::IntegerOverflow)?); - let mut w = width; - let mut h = height; - for _ in 0..mip_count { - let pixel_count_u64 = u64::from(w) - .checked_mul(u64::from(h)) - .ok_or(Error::IntegerOverflow)?; - let level_size_u64 = pixel_count_u64 - .checked_mul(u64::try_from(bytes_per_pixel).map_err(|_| Error::IntegerOverflow)?) - .ok_or(Error::IntegerOverflow)?; - let level_size = usize::try_from(level_size_u64).map_err(|_| Error::IntegerOverflow)?; - let level_offset = offset; - offset = offset - .checked_add(level_size) - .ok_or(Error::IntegerOverflow)?; - if offset > payload.len() { - return Err(Error::CoreDataOutOfBounds { - expected_end: offset, - actual_size: payload.len(), - }); - } - mip_levels.push(MipLevel { - width: w, - height: h, - offset: level_offset, - size: level_size, - }); - w = (w >> 1).max(1); - h = (h >> 1).max(1); - } - - let page_rects = parse_page_tail(payload, offset)?; - - Ok(Texture { - header: Header { - width, - height, - mip_count, - flags4, - flags5, - unk6, - format_raw, - format, - }, - palette, - mip_levels, - page_rects, - }) -} - -pub fn decode_mip_rgba8(texture: &Texture, payload: &[u8], mip_index: usize) -> Result<DecodedMip> { - let Some(level) = texture.mip_levels.get(mip_index).copied() else { - return Err(Error::MipIndexOutOfRange { - requested: mip_index, - mip_count: texture.mip_levels.len(), - }); - }; - - let end = level - .offset - .checked_add(level.size) - .ok_or(Error::IntegerOverflow)?; - let Some(level_data) = payload.get(level.offset..end) else { - return Err(Error::MipDataOutOfBounds { - offset: level.offset, - size: level.size, - payload_size: payload.len(), - }); - }; - - let pixel_count = usize::try_from(level.width) - .ok() - .and_then(|w| { - usize::try_from(level.height) - .ok() - .map(|h| w.saturating_mul(h)) - }) - .ok_or(Error::IntegerOverflow)?; - let mut rgba = vec![0u8; pixel_count.saturating_mul(4)]; - - match texture.header.format { - PixelFormat::Indexed8 => { - let palette = texture.palette.as_ref().ok_or(Error::IntegerOverflow)?; - for (i, &index) in level_data.iter().enumerate() { - if i >= pixel_count { - break; - } - let poff = usize::from(index).saturating_mul(4); - // Keep this form to accept the last palette item (index 255). - if poff + 4 > palette.len() { - continue; - } - let out = i.saturating_mul(4); - rgba[out] = palette[poff]; - rgba[out + 1] = palette[poff + 1]; - rgba[out + 2] = palette[poff + 2]; - rgba[out + 3] = palette[poff + 3]; - } - } - PixelFormat::Rgb565 => { - decode_words(level_data, pixel_count, &mut rgba, decode_rgb565); - } - PixelFormat::Rgb556 => { - decode_words(level_data, pixel_count, &mut rgba, decode_rgb556); - } - PixelFormat::Argb4444 => { - decode_words(level_data, pixel_count, &mut rgba, decode_argb4444); - } - PixelFormat::LuminanceAlpha88 => { - decode_words(level_data, pixel_count, &mut rgba, decode_luminance_alpha88); - } - PixelFormat::Rgb888 => { - decode_dwords(level_data, pixel_count, &mut rgba, decode_rgb888x); - } - PixelFormat::Argb8888 => { - decode_dwords(level_data, pixel_count, &mut rgba, decode_argb8888); - } - } - - Ok(DecodedMip { - width: level.width, - height: level.height, - rgba8: rgba, - }) -} - -fn parse_page_tail(payload: &[u8], core_end: usize) -> Result<Vec<PageRect>> { - if core_end == payload.len() { - return Ok(Vec::new()); - } - if payload.len().saturating_sub(core_end) < 8 { - return Err(Error::InvalidPageSize { - expected: 8, - actual: payload.len().saturating_sub(core_end), - }); - } - let magic = read_u32(payload, core_end)?; - if magic != PAGE_MAGIC { - return Err(Error::InvalidPageMagic); - } - let rect_count = read_u32(payload, core_end + 4)?; - let rect_count_usize = usize::try_from(rect_count).map_err(|_| Error::IntegerOverflow)?; - let expected_size = 8usize - .checked_add( - rect_count_usize - .checked_mul(8) - .ok_or(Error::IntegerOverflow)?, - ) - .ok_or(Error::IntegerOverflow)?; - let actual = payload.len().saturating_sub(core_end); - if expected_size != actual { - return Err(Error::InvalidPageSize { - expected: expected_size, - actual, - }); - } - - let mut rects = Vec::with_capacity(rect_count_usize); - for i in 0..rect_count_usize { - let off = core_end - .checked_add(8) - .and_then(|v| v.checked_add(i * 8)) - .ok_or(Error::IntegerOverflow)?; - rects.push(PageRect { - x: read_i16(payload, off)?, - w: read_i16(payload, off + 2)?, - y: read_i16(payload, off + 4)?, - h: read_i16(payload, off + 6)?, - }); - } - Ok(rects) -} - -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_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 decode_words(data: &[u8], pixel_count: usize, rgba: &mut [u8], decode: fn(u16) -> [u8; 4]) { - for i in 0..pixel_count { - let off = i.saturating_mul(2); - let Some(bytes) = data.get(off..off + 2) else { - break; - }; - let word = u16::from_le_bytes([bytes[0], bytes[1]]); - let px = decode(word); - let out = i.saturating_mul(4); - rgba[out..out + 4].copy_from_slice(&px); - } -} - -fn decode_dwords(data: &[u8], pixel_count: usize, rgba: &mut [u8], decode: fn(u32) -> [u8; 4]) { - for i in 0..pixel_count { - let off = i.saturating_mul(4); - let Some(bytes) = data.get(off..off + 4) else { - break; - }; - let dword = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); - let px = decode(dword); - let out = i.saturating_mul(4); - rgba[out..out + 4].copy_from_slice(&px); - } -} - -fn expand5(v: u16) -> u8 { - ((u32::from(v) * 255 + 15) / 31) as u8 -} - -fn expand6(v: u16) -> u8 { - ((u32::from(v) * 255 + 31) / 63) as u8 -} - -fn expand4(v: u16) -> u8 { - (u32::from(v) * 17) as u8 -} - -fn decode_rgb565(word: u16) -> [u8; 4] { - let r = expand5((word >> 11) & 0x1F); - let g = expand6((word >> 5) & 0x3F); - let b = expand5(word & 0x1F); - [r, g, b, 255] -} - -fn decode_rgb556(word: u16) -> [u8; 4] { - let r = expand5((word >> 11) & 0x1F); - let g = expand5((word >> 6) & 0x1F); - let b = expand6(word & 0x3F); - [r, g, b, 255] -} - -fn decode_argb4444(word: u16) -> [u8; 4] { - let a = expand4((word >> 12) & 0x0F); - let r = expand4((word >> 8) & 0x0F); - let g = expand4((word >> 4) & 0x0F); - let b = expand4(word & 0x0F); - [r, g, b, a] -} - -fn decode_luminance_alpha88(word: u16) -> [u8; 4] { - let l = ((word >> 8) & 0xFF) as u8; - let a = (word & 0xFF) as u8; - [l, l, l, a] -} - -fn decode_rgb888x(dword: u32) -> [u8; 4] { - let r = (dword & 0xFF) as u8; - let g = ((dword >> 8) & 0xFF) as u8; - let b = ((dword >> 16) & 0xFF) as u8; - [r, g, b, 255] -} - -fn decode_argb8888(dword: u32) -> [u8; 4] { - let a = (dword & 0xFF) as u8; - let r = ((dword >> 8) & 0xFF) as u8; - let g = ((dword >> 16) & 0xFF) as u8; - let b = ((dword >> 24) & 0xFF) as u8; - [r, g, b, a] -} - -#[cfg(test)] -mod tests; diff --git a/crates/texm/src/tests.rs b/crates/texm/src/tests.rs deleted file mode 100644 index 49a7100..0000000 --- a/crates/texm/src/tests.rs +++ /dev/null @@ -1,330 +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 build_texm_payload( - width: u32, - height: u32, - format_raw: u32, - flags5: u32, - palette: Option<[u8; 1024]>, - mip_levels: &[&[u8]], -) -> Vec<u8> { - let mut payload = Vec::new(); - payload.extend_from_slice(&TEXM_MAGIC.to_le_bytes()); - payload.extend_from_slice(&width.to_le_bytes()); - payload.extend_from_slice(&height.to_le_bytes()); - payload.extend_from_slice( - &u32::try_from(mip_levels.len()) - .expect("mip level count overflow in test") - .to_le_bytes(), - ); - payload.extend_from_slice(&0u32.to_le_bytes()); // flags4 - payload.extend_from_slice(&flags5.to_le_bytes()); - payload.extend_from_slice(&0u32.to_le_bytes()); // unk6 - payload.extend_from_slice(&format_raw.to_le_bytes()); - if let Some(palette) = palette { - payload.extend_from_slice(&palette); - } - for level in mip_levels { - payload.extend_from_slice(level); - } - payload -} - -#[test] -fn texm_parse_all_game_textures() { - let archives = nres_test_files(); - if archives.is_empty() { - eprintln!("skipping texm_parse_all_game_textures: no NRes files in testdata"); - return; - } - - let mut texm_total = 0usize; - let mut texm_with_page = 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 entry.meta.kind != TEXM_MAGIC { - continue; - } - texm_total += 1; - let payload = archive.read(entry.id).unwrap_or_else(|err| { - panic!( - "failed to read Texm entry '{}' in {}: {err}", - entry.meta.name, - archive_path.display() - ) - }); - let texture = parse_texm(payload.as_slice()).unwrap_or_else(|err| { - panic!( - "failed to parse Texm '{}' in {}: {err}", - entry.meta.name, - archive_path.display() - ) - }); - if !texture.page_rects.is_empty() { - texm_with_page += 1; - } - - assert!( - texture.core_size() <= payload.as_slice().len(), - "core size must be within payload for '{}' in {}", - entry.meta.name, - archive_path.display() - ); - assert_eq!( - usize::try_from(texture.header.mip_count).ok(), - Some(texture.mip_levels.len()), - "mip count mismatch for '{}' in {}", - entry.meta.name, - archive_path.display() - ); - } - } - - assert!(texm_total > 0, "no Texm textures found"); - assert!( - texm_with_page > 0, - "expected at least one Texm texture with Page chunk" - ); -} - -#[test] -fn texm_parse_minimal_argb8888_no_page() { - let payload = build_texm_payload(1, 1, 8888, 0, None, &[&[1, 2, 3, 4]]); - - let parsed = parse_texm(&payload).expect("failed to parse minimal texm"); - assert_eq!(parsed.header.width, 1); - assert_eq!(parsed.header.height, 1); - assert_eq!(parsed.mip_levels.len(), 1); - assert!(parsed.page_rects.is_empty()); -} - -#[test] -fn texm_decode_minimal_argb8888_no_page() { - let payload = build_texm_payload(1, 1, 8888, 0, None, &[&[0x40, 0x11, 0x22, 0x33]]); - let parsed = parse_texm(&payload).expect("failed to parse minimal texm"); - let decoded = decode_mip_rgba8(&parsed, &payload, 0).expect("failed to decode mip"); - assert_eq!(decoded.width, 1); - assert_eq!(decoded.height, 1); - assert_eq!(decoded.rgba8, vec![0x11, 0x22, 0x33, 0x40]); -} - -#[test] -fn texm_decode_rgb565() { - let word = 0xFFE0u16; // r=31 g=63 b=0 - let payload = build_texm_payload(1, 1, 565, 0, None, &[&word.to_le_bytes()]); - let parsed = parse_texm(&payload).expect("failed to parse rgb565 texm"); - let decoded = decode_mip_rgba8(&parsed, &payload, 0).expect("failed to decode rgb565 texm"); - assert_eq!(decoded.rgba8, vec![255, 255, 0, 255]); -} - -#[test] -fn texm_decode_rgb556() { - let word = 0xF800u16; // r=31 g=0 b=0 - let payload = build_texm_payload(1, 1, 556, 0, None, &[&word.to_le_bytes()]); - let parsed = parse_texm(&payload).expect("failed to parse rgb556 texm"); - let decoded = decode_mip_rgba8(&parsed, &payload, 0).expect("failed to decode rgb556 texm"); - assert_eq!(decoded.rgba8, vec![255, 0, 0, 255]); -} - -#[test] -fn texm_decode_argb4444() { - let word = 0xF12Eu16; // a=F r=1 g=2 b=E - let payload = build_texm_payload(1, 1, 4444, 0, None, &[&word.to_le_bytes()]); - let parsed = parse_texm(&payload).expect("failed to parse argb4444 texm"); - let decoded = decode_mip_rgba8(&parsed, &payload, 0).expect("failed to decode argb4444 texm"); - assert_eq!(decoded.rgba8, vec![17, 34, 238, 255]); -} - -#[test] -fn texm_decode_luminance_alpha88() { - let word = 0x7F40u16; // luminance=0x7F alpha=0x40 - let payload = build_texm_payload(1, 1, 88, 0, None, &[&word.to_le_bytes()]); - let parsed = parse_texm(&payload).expect("failed to parse la88 texm"); - let decoded = decode_mip_rgba8(&parsed, &payload, 0).expect("failed to decode la88 texm"); - assert_eq!(decoded.rgba8, vec![0x7F, 0x7F, 0x7F, 0x40]); -} - -#[test] -fn texm_decode_rgb888x() { - let payload = build_texm_payload(1, 1, 888, 0, None, &[&[0x11, 0x22, 0x33, 0x99]]); - let parsed = parse_texm(&payload).expect("failed to parse rgb888 texm"); - let decoded = decode_mip_rgba8(&parsed, &payload, 0).expect("failed to decode rgb888 texm"); - assert_eq!(decoded.rgba8, vec![0x11, 0x22, 0x33, 255]); -} - -#[test] -fn texm_parse_indexed_with_page_chunk() { - let mut palette = [0u8; 1024]; - palette[4..8].copy_from_slice(&[10, 20, 30, 255]); - let mut payload = build_texm_payload(2, 2, 0, 0, Some(palette), &[&[1, 1, 1, 1]]); - payload.extend_from_slice(&PAGE_MAGIC.to_le_bytes()); - payload.extend_from_slice(&1u32.to_le_bytes()); // rect_count - payload.extend_from_slice(&0i16.to_le_bytes()); // x - payload.extend_from_slice(&2i16.to_le_bytes()); // w - payload.extend_from_slice(&0i16.to_le_bytes()); // y - payload.extend_from_slice(&2i16.to_le_bytes()); // h - - let parsed = parse_texm(&payload).expect("failed to parse indexed texm"); - assert!(parsed.palette.is_some()); - assert_eq!(parsed.page_rects.len(), 1); - assert_eq!( - parsed.page_rects[0], - PageRect { - x: 0, - w: 2, - y: 0, - h: 2 - } - ); -} - -#[test] -fn texm_decode_indexed_with_palette_last_entry() { - let mut palette = [0u8; 1024]; - palette[4..8].copy_from_slice(&[10, 20, 30, 255]); // index 1 - palette[8..12].copy_from_slice(&[40, 50, 60, 200]); // index 2 - palette[1020..1024].copy_from_slice(&[1, 2, 3, 4]); // index 255 (last) - let payload = build_texm_payload(3, 1, 0, 0, Some(palette), &[&[1u8, 2u8, 255u8]]); - - let parsed = parse_texm(&payload).expect("failed to parse indexed texm"); - let decoded = decode_mip_rgba8(&parsed, &payload, 0).expect("failed to decode indexed texm"); - assert_eq!(decoded.width, 3); - assert_eq!(decoded.height, 1); - assert_eq!( - decoded.rgba8, - vec![10, 20, 30, 255, 40, 50, 60, 200, 1, 2, 3, 4] - ); -} - -#[test] -fn texm_parse_multi_mip_offsets() { - let mip0 = [0x10u8; 32]; // 4*2*4 - let mip1 = [0x20u8; 8]; // 2*1*4 - let mip2 = [0x30u8; 4]; // 1*1*4 - let payload = build_texm_payload(4, 2, 8888, 0, None, &[&mip0, &mip1, &mip2]); - - let parsed = parse_texm(&payload).expect("failed to parse multi-mip texm"); - assert_eq!(parsed.header.mip_count, 3); - assert_eq!(parsed.mip_levels.len(), 3); - assert_eq!( - parsed.mip_levels, - vec![ - MipLevel { - width: 4, - height: 2, - offset: 32, - size: 32 - }, - MipLevel { - width: 2, - height: 1, - offset: 64, - size: 8 - }, - MipLevel { - width: 1, - height: 1, - offset: 72, - size: 4 - }, - ] - ); -} - -#[test] -fn texm_preserves_flags5_for_mip_skip_metadata() { - let payload = build_texm_payload(1, 1, 8888, 0x0000_00A5, None, &[&[0, 0, 0, 0]]); - let parsed = parse_texm(&payload).expect("failed to parse texm"); - assert_eq!(parsed.header.flags5, 0x0000_00A5); -} - -#[test] -fn texm_errors_for_invalid_header_values() { - let mut bad_magic = build_texm_payload(1, 1, 8888, 0, None, &[&[0, 0, 0, 0]]); - bad_magic[0..4].copy_from_slice(&0u32.to_le_bytes()); - assert!(matches!( - parse_texm(&bad_magic), - Err(Error::InvalidMagic { .. }) - )); - - let zero_dims = build_texm_payload(0, 1, 8888, 0, None, &[&[]]); - assert!(matches!( - parse_texm(&zero_dims), - Err(Error::InvalidDimensions { .. }) - )); - - let mut bad_mips = build_texm_payload(1, 1, 8888, 0, None, &[&[0, 0, 0, 0]]); - bad_mips[12..16].copy_from_slice(&0u32.to_le_bytes()); - assert!(matches!( - parse_texm(&bad_mips), - Err(Error::InvalidMipCount { .. }) - )); - - let bad_format = build_texm_payload(1, 1, 12345, 0, None, &[&[0, 0, 0, 0]]); - assert!(matches!( - parse_texm(&bad_format), - Err(Error::UnknownFormat { .. }) - )); -} - -#[test] -fn texm_errors_for_page_chunk_and_mip_bounds() { - let mut bad_page = build_texm_payload(1, 1, 8888, 0, None, &[&[0, 0, 0, 0]]); - bad_page.extend_from_slice(b"X"); - assert!(matches!( - parse_texm(&bad_page), - Err(Error::InvalidPageSize { .. }) - )); - - let payload = build_texm_payload(1, 1, 8888, 0, None, &[&[1, 2, 3, 4]]); - let parsed = parse_texm(&payload).expect("failed to parse valid texm"); - assert!(matches!( - decode_mip_rgba8(&parsed, &payload, 7), - Err(Error::MipIndexOutOfRange { .. }) - )); - - let truncated = &payload[..payload.len() - 1]; - assert!(matches!( - decode_mip_rgba8(&parsed, truncated, 0), - Err(Error::MipDataOutOfBounds { .. }) - )); -} - -proptest! { - #![proptest_config(ProptestConfig::with_cases(64))] - - #[test] - fn parse_texm_is_panic_free_on_random_bytes(payload in proptest::collection::vec(any::<u8>(), 0..4096)) { - if let Ok(texture) = parse_texm(&payload) { - for mip_index in 0..texture.mip_levels.len() { - let _ = decode_mip_rgba8(&texture, &payload, mip_index); - } - } - } -} |
