aboutsummaryrefslogtreecommitdiff
path: root/crates/texm
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-22 12:12:27 +0300
committerValentin Popov <valentin@popov.link>2026-06-22 12:13:32 +0300
commitd0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448 (patch)
treea0bd35c3940be62a5b5de1acc2366af377ffd181 /crates/texm
parent7416fdc7e9a48837fff5056e6dc8d0774e90964b (diff)
downloadfparkan-d0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448.tar.xz
fparkan-d0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448.zip
feat: implement FParkan architecture foundation
Add the modular fparkan workspace, domain crates, adapters, apps, xtask policy/CI, acceptance evidence, and licensed corpus gates for the macOS-focused roadmap foundation.
Diffstat (limited to 'crates/texm')
-rw-r--r--crates/texm/Cargo.toml9
-rw-r--r--crates/texm/README.md15
-rw-r--r--crates/texm/src/error.rs86
-rw-r--r--crates/texm/src/lib.rs417
-rw-r--r--crates/texm/src/tests.rs330
5 files changed, 0 insertions, 857 deletions
diff --git a/crates/texm/Cargo.toml b/crates/texm/Cargo.toml
deleted file mode 100644
index f9c49b6..0000000
--- a/crates/texm/Cargo.toml
+++ /dev/null
@@ -1,9 +0,0 @@
-[package]
-name = "texm"
-version = "0.1.0"
-edition = "2021"
-
-[dev-dependencies]
-common = { path = "../common" }
-nres = { path = "../nres" }
-proptest = "1"
diff --git a/crates/texm/README.md b/crates/texm/README.md
deleted file mode 100644
index 370ac54..0000000
--- a/crates/texm/README.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# texm
-
-Парсер формата текстур `Texm`.
-
-Покрывает:
-
-- header (`width/height/mipCount/flags/format`);
-- core size расчёт;
-- optional `Page` chunk;
-- строгую валидацию layout.
-
-Тесты:
-
-- прогон по реальным `Texm` из `testdata`;
-- синтетические edge-cases (indexed + page, minimal rgba).
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);
- }
- }
- }
-}