feat: Refactor code structure and enhance functionality across multiple crates

This commit is contained in:
2026-02-19 10:09:18 +00:00
parent efab61a45c
commit bb827c3928
15 changed files with 232 additions and 120 deletions

View File

@@ -1,4 +1,6 @@
use std::fs;
use std::io; use std::io;
use std::path::{Path, PathBuf};
/// Resource payload that can be either borrowed from mapped bytes or owned. /// Resource payload that can be either borrowed from mapped bytes or owned.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -42,3 +44,18 @@ impl OutputBuffer for Vec<u8> {
Ok(()) Ok(())
} }
} }
/// Recursively collects all files under `root`.
pub fn collect_files_recursive(root: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = fs::read_dir(root) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_files_recursive(&path, out);
} else if path.is_file() {
out.push(path);
}
}
}

View File

@@ -4,4 +4,9 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
encoding_rs = "0.8"
nres = { path = "../nres" } nres = { path = "../nres" }
[dev-dependencies]
common = { path = "../common" }
proptest = "1"

View File

@@ -1,6 +1,7 @@
pub mod error; pub mod error;
use crate::error::Error; use crate::error::Error;
use encoding_rs::WINDOWS_1251;
use std::sync::Arc; use std::sync::Arc;
pub type Result<T> = core::result::Result<T, Error>; pub type Result<T> = core::result::Result<T, Error>;
@@ -347,13 +348,18 @@ fn parse_res10_names(data: &[u8], node_count: usize) -> Result<Vec<Option<String
} else { } else {
slice slice
}; };
let decoded = String::from_utf8_lossy(text).to_string(); let decoded = decode_cp1251(text);
out.push(Some(decoded)); out.push(Some(decoded));
off = end; off = end;
} }
Ok(out) Ok(out)
} }
fn decode_cp1251(bytes: &[u8]) -> String {
let (decoded, _, _) = WINDOWS_1251.decode(bytes);
decoded.into_owned()
}
struct RawResource { struct RawResource {
meta: nres::EntryMeta, meta: nres::EntryMeta,
bytes: Vec<u8>, bytes: Vec<u8>,

View File

@@ -1,22 +1,10 @@
use super::*; use super::*;
use common::collect_files_recursive;
use nres::Archive; use nres::Archive;
use proptest::prelude::*;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
fn collect_files_recursive(root: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = fs::read_dir(root) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_files_recursive(&path, out);
} else if path.is_file() {
out.push(path);
}
}
}
fn nres_test_files() -> Vec<PathBuf> { fn nres_test_files() -> Vec<PathBuf> {
let root = Path::new(env!("CARGO_MANIFEST_DIR")) let root = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..") .join("..")
@@ -169,18 +157,17 @@ fn res13_single_batch(index_start: u32, index_count: u16) -> Vec<u8> {
batch batch
} }
fn res10_names(names: &[Option<&str>]) -> Vec<u8> { fn res10_names_raw(names: &[Option<&[u8]>]) -> Vec<u8> {
let mut out = Vec::new(); let mut out = Vec::new();
for name in names { for name in names {
match name { match name {
Some(name) => { Some(name) => {
let bytes = name.as_bytes();
out.extend_from_slice( out.extend_from_slice(
&u32::try_from(bytes.len()) &u32::try_from(name.len())
.expect("name size overflow in test") .expect("name size overflow in test")
.to_le_bytes(), .to_le_bytes(),
); );
out.extend_from_slice(bytes); out.extend_from_slice(name);
out.push(0); out.push(0);
} }
None => out.extend_from_slice(&0u32.to_le_bytes()), None => out.extend_from_slice(&0u32.to_le_bytes()),
@@ -189,6 +176,11 @@ fn res10_names(names: &[Option<&str>]) -> Vec<u8> {
out 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> { fn base_synthetic_entries() -> Vec<SyntheticEntry> {
vec![ vec![
synthetic_entry(RES1_NODE_TABLE, "Res1", 38, res1_stride38_nodes(1, Some(0))), synthetic_entry(RES1_NODE_TABLE, "Res1", 38, res1_stride38_nodes(1, Some(0))),
@@ -339,6 +331,22 @@ fn parse_synthetic_model_with_optional_res4_res5_res10() {
assert_eq!(model.node_names, Some(vec![Some("Hull".to_string()), None])); 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] #[test]
fn parse_fails_when_required_resource_missing() { fn parse_fails_when_required_resource_missing() {
let mut entries = base_synthetic_entries(); let mut entries = base_synthetic_entries();
@@ -419,3 +427,12 @@ fn parse_fails_for_batch_index_range_out_of_bounds() {
}) })
)); ));
} }
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);
}
}

View File

@@ -1,4 +1,5 @@
use super::*; use super::*;
use common::collect_files_recursive;
use std::any::Any; use std::any::Any;
use std::fs; use std::fs;
use std::panic::{catch_unwind, AssertUnwindSafe}; use std::panic::{catch_unwind, AssertUnwindSafe};
@@ -13,20 +14,6 @@ struct SyntheticEntry<'a> {
data: &'a [u8], data: &'a [u8],
} }
fn collect_files_recursive(root: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = fs::read_dir(root) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_files_recursive(&path, out);
} else if path.is_file() {
out.push(path);
}
}
}
fn nres_test_files() -> Vec<PathBuf> { fn nres_test_files() -> Vec<PathBuf> {
let root = Path::new(env!("CARGO_MANIFEST_DIR")) let root = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..") .join("..")

View File

@@ -7,4 +7,5 @@ edition = "2021"
msh-core = { path = "../msh-core" } msh-core = { path = "../msh-core" }
[dev-dependencies] [dev-dependencies]
common = { path = "../common" }
nres = { path = "../nres" } nres = { path = "../nres" }

View File

@@ -1,4 +1,5 @@
use msh_core::Model; use msh_core::Model;
use std::collections::HashMap;
pub const DEFAULT_UV_SCALE: f32 = 1024.0; pub const DEFAULT_UV_SCALE: f32 = 1024.0;
@@ -11,21 +12,24 @@ pub struct RenderVertex {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct RenderMesh { pub struct RenderMesh {
pub vertices: Vec<RenderVertex>, pub vertices: Vec<RenderVertex>,
pub indices: Vec<u16>,
pub batch_count: usize, pub batch_count: usize,
pub index_overflow: bool,
} }
impl RenderMesh { impl RenderMesh {
pub fn triangle_count(&self) -> usize { pub fn triangle_count(&self) -> usize {
self.vertices.len() / 3 self.indices.len() / 3
} }
} }
/// Builds an expanded triangle list for a specific LOD/group pair. /// Builds an indexed triangle mesh for a specific LOD/group pair.
///
/// The output is suitable for simple `glDrawArrays(GL_TRIANGLES, ...)` paths.
pub fn build_render_mesh(model: &Model, lod: usize, group: usize) -> RenderMesh { pub fn build_render_mesh(model: &Model, lod: usize, group: usize) -> RenderMesh {
let mut vertices = Vec::new(); let mut vertices = Vec::new();
let mut indices = Vec::new();
let mut index_remap: HashMap<usize, u16> = HashMap::new();
let mut batch_count = 0usize; let mut batch_count = 0usize;
let mut index_overflow = false;
let uv0 = model.uv0.as_ref(); let uv0 = model.uv0.as_ref();
for node_index in 0..model.node_count { for node_index in 0..model.node_count {
@@ -49,13 +53,26 @@ pub fn build_render_mesh(model: &Model, lod: usize, group: usize) -> RenderMesh
continue; continue;
} }
let batch_out_start = indices.len();
let mut batch_valid = true;
for &idx in &model.indices[index_start..index_end] { for &idx in &model.indices[index_start..index_end] {
let final_idx_u64 = u64::from(batch.base_vertex).saturating_add(u64::from(idx)); let final_idx_u64 = u64::from(batch.base_vertex).saturating_add(u64::from(idx));
let Ok(final_idx) = usize::try_from(final_idx_u64) else { let Ok(final_idx) = usize::try_from(final_idx_u64) else {
continue; batch_valid = false;
break;
}; };
let Some(pos) = model.positions.get(final_idx) else { let Some(pos) = model.positions.get(final_idx) else {
continue; batch_valid = false;
break;
};
let local_index = if let Some(&mapped) = index_remap.get(&final_idx) {
mapped
} else {
let Ok(mapped) = u16::try_from(vertices.len()) else {
index_overflow = true;
batch_valid = false;
break;
}; };
let uv = uv0 let uv = uv0
.and_then(|uvs| uvs.get(final_idx)) .and_then(|uvs| uvs.get(final_idx))
@@ -71,14 +88,27 @@ pub fn build_render_mesh(model: &Model, lod: usize, group: usize) -> RenderMesh
position: *pos, position: *pos,
uv0: uv, uv0: uv,
}); });
index_remap.insert(final_idx, mapped);
mapped
};
indices.push(local_index);
} }
if !batch_valid {
indices.truncate(batch_out_start);
continue;
}
batch_count += 1; batch_count += 1;
} }
} }
RenderMesh { RenderMesh {
vertices, vertices,
indices,
batch_count, batch_count,
index_overflow,
} }
} }

View File

@@ -1,23 +1,10 @@
use super::*; use super::*;
use common::collect_files_recursive;
use msh_core::parse_model_payload; use msh_core::parse_model_payload;
use nres::Archive; use nres::Archive;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
fn collect_files_recursive(root: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = fs::read_dir(root) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_files_recursive(&path, out);
} else if path.is_file() {
out.push(path);
}
}
}
fn nres_test_files() -> Vec<PathBuf> { fn nres_test_files() -> Vec<PathBuf> {
let root = Path::new(env!("CARGO_MANIFEST_DIR")) let root = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..") .join("..")
@@ -71,12 +58,20 @@ fn build_render_mesh_for_real_models() {
) )
}); });
let mesh = build_render_mesh(&model, 0, 0); let mesh = build_render_mesh(&model, 0, 0);
if !mesh.vertices.is_empty() { if !mesh.indices.is_empty() {
meshes_non_empty += 1; meshes_non_empty += 1;
} }
if compute_bounds_for_mesh(&mesh.vertices).is_some() { if compute_bounds_for_mesh(&mesh.vertices).is_some() {
bounds_non_empty += 1; bounds_non_empty += 1;
} }
for &index in &mesh.indices {
assert!(
usize::from(index) < mesh.vertices.len(),
"index out of bounds for '{}' in {}",
entry.meta.name,
archive_path.display()
);
}
for vertex in &mesh.vertices { for vertex in &mesh.vertices {
assert!( assert!(
vertex.uv0[0].is_finite() && vertex.uv0[1].is_finite(), vertex.uv0[0].is_finite() && vertex.uv0[1].is_finite(),
@@ -189,6 +184,7 @@ fn build_render_mesh_handles_empty_slot_model() {
let mesh = build_render_mesh(&model, 0, 0); let mesh = build_render_mesh(&model, 0, 0);
assert!(mesh.vertices.is_empty()); assert!(mesh.vertices.is_empty());
assert!(mesh.indices.is_empty());
assert_eq!(mesh.batch_count, 0); assert_eq!(mesh.batch_count, 0);
assert_eq!(mesh.triangle_count(), 0); assert_eq!(mesh.triangle_count(), 0);
} }
@@ -225,9 +221,36 @@ fn build_render_mesh_supports_multi_node_and_uv_scaling() {
let mesh = build_render_mesh(&model, 0, 0); let mesh = build_render_mesh(&model, 0, 0);
assert_eq!(mesh.batch_count, 2); assert_eq!(mesh.batch_count, 2);
assert_eq!(mesh.vertices.len(), 6); assert_eq!(mesh.vertices.len(), 6);
assert_eq!(mesh.indices, vec![0, 1, 2, 3, 4, 5]);
assert_eq!(mesh.triangle_count(), 2); assert_eq!(mesh.triangle_count(), 2);
assert_eq!(mesh.vertices[0].uv0, [1.0, -1.0]); assert_eq!(mesh.vertices[0].uv0, [1.0, -1.0]);
assert_eq!(mesh.vertices[1].uv0, [0.5, 0.25]); assert_eq!(mesh.vertices[1].uv0, [0.5, 0.25]);
assert_eq!(mesh.vertices[2].uv0, [0.0, 0.0]); assert_eq!(mesh.vertices[2].uv0, [0.0, 0.0]);
assert_eq!(mesh.vertices[3].uv0, [1.0, 1.0]); assert_eq!(mesh.vertices[3].uv0, [1.0, 1.0]);
} }
#[test]
fn build_render_mesh_deduplicates_shared_vertices() {
let model = msh_core::Model {
node_stride: 38,
node_count: 1,
nodes_raw: nodes_with_slot_refs(&[Some(0)]),
slots: vec![slot(0, 1)],
positions: vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[1.0, 1.0, 0.0],
],
normals: None,
uv0: None,
indices: vec![0, 1, 2, 2, 1, 3],
batches: vec![batch(0, 6, 0)],
node_names: None,
};
let mesh = build_render_mesh(&model, 0, 0);
assert_eq!(mesh.vertices.len(), 4);
assert_eq!(mesh.indices, vec![0, 1, 2, 2, 1, 3]);
assert_eq!(mesh.triangle_count(), 2);
}

View File

@@ -17,6 +17,9 @@ sdl2 = { version = "0.37", optional = true, default-features = false, features =
glow = { version = "0.16", optional = true } glow = { version = "0.16", optional = true }
image = { version = "0.25", optional = true, default-features = false, features = ["png"] } image = { version = "0.25", optional = true, default-features = false, features = ["png"] }
[dev-dependencies]
common = { path = "../common" }
[[bin]] [[bin]]
name = "parkan-render-demo" name = "parkan-render-demo"
path = "src/main.rs" path = "src/main.rs"

View File

@@ -435,23 +435,10 @@ fn decode_texture_entry(archive: &Archive, entry: EntryRef<'_>) -> Result<Loaded
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use common::collect_files_recursive;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
fn collect_files_recursive(root: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = fs::read_dir(root) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_files_recursive(&path, out);
} else if path.is_file() {
out.push(path);
}
}
}
fn archive_with_msh() -> Option<PathBuf> { fn archive_with_msh() -> Option<PathBuf> {
let root = Path::new(env!("CARGO_MANIFEST_DIR")) let root = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..") .join("..")

View File

@@ -221,12 +221,17 @@ fn run(args: Args) -> Result<(), String> {
) )
})?; })?;
let mesh = build_render_mesh(&loaded_model.model, args.lod, args.group); let mesh = build_render_mesh(&loaded_model.model, args.lod, args.group);
if mesh.vertices.is_empty() { if mesh.indices.is_empty() {
return Err(format!( return Err(format!(
"model has no renderable triangles for lod={} group={}", "model has no renderable triangles for lod={} group={}",
args.lod, args.group args.lod, args.group
)); ));
} }
if mesh.index_overflow {
eprintln!(
"warning: mesh exceeds u16 index space and may be partially rendered on GLES2 targets"
);
}
let Some((bounds_min, bounds_max)) = compute_bounds_for_mesh(&mesh.vertices) else { let Some((bounds_min, bounds_max)) = compute_bounds_for_mesh(&mesh.vertices) else {
return Err(String::from("failed to compute mesh bounds")); return Err(String::from("failed to compute mesh bounds"));
}; };
@@ -304,6 +309,7 @@ fn run(args: Args) -> Result<(), String> {
vertex_data.push(vertex.uv0[1]); vertex_data.push(vertex.uv0[1]);
} }
let vertex_bytes = f32_slice_to_ne_bytes(&vertex_data); let vertex_bytes = f32_slice_to_ne_bytes(&vertex_data);
let index_bytes = u16_slice_to_ne_bytes(&mesh.indices);
let gl = unsafe { let gl = unsafe {
glow::Context::from_loader_function(|name| video.gl_get_proc_address(name) as *const _) glow::Context::from_loader_function(|name| video.gl_get_proc_address(name) as *const _)
@@ -319,9 +325,13 @@ fn run(args: Args) -> Result<(), String> {
.ok_or_else(|| String::from("shader attribute a_uv is missing"))?; .ok_or_else(|| String::from("shader attribute a_uv is missing"))?;
let vbo = unsafe { gl.create_buffer().map_err(|e| e.to_string())? }; let vbo = unsafe { gl.create_buffer().map_err(|e| e.to_string())? };
let ebo = unsafe { gl.create_buffer().map_err(|e| e.to_string())? };
unsafe { unsafe {
gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo)); gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo));
gl.buffer_data_u8_slice(glow::ARRAY_BUFFER, &vertex_bytes, glow::STATIC_DRAW); gl.buffer_data_u8_slice(glow::ARRAY_BUFFER, &vertex_bytes, glow::STATIC_DRAW);
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(ebo));
gl.buffer_data_u8_slice(glow::ELEMENT_ARRAY_BUFFER, &index_bytes, glow::STATIC_DRAW);
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, None);
gl.bind_buffer(glow::ARRAY_BUFFER, None); gl.bind_buffer(glow::ARRAY_BUFFER, None);
} }
@@ -341,8 +351,9 @@ fn run(args: Args) -> Result<(), String> {
a_pos, a_pos,
a_uv, a_uv,
vbo, vbo,
ebo,
gpu_texture.as_ref(), gpu_texture.as_ref(),
mesh.vertices.len(), mesh.indices.len(),
&args, &args,
center, center,
camera_distance, camera_distance,
@@ -360,8 +371,9 @@ fn run(args: Args) -> Result<(), String> {
a_pos, a_pos,
a_uv, a_uv,
vbo, vbo,
ebo,
gpu_texture.as_ref(), gpu_texture.as_ref(),
mesh.vertices.len(), mesh.indices.len(),
&args, &args,
center, center,
camera_distance, camera_distance,
@@ -372,6 +384,7 @@ fn run(args: Args) -> Result<(), String> {
if let Some(texture) = gpu_texture { if let Some(texture) = gpu_texture {
gl.delete_texture(texture.handle); gl.delete_texture(texture.handle);
} }
gl.delete_buffer(ebo);
gl.delete_buffer(vbo); gl.delete_buffer(vbo);
gl.delete_program(program); gl.delete_program(program);
} }
@@ -452,8 +465,9 @@ fn run_capture(
a_pos: u32, a_pos: u32,
a_uv: u32, a_uv: u32,
vbo: glow::NativeBuffer, vbo: glow::NativeBuffer,
ebo: glow::NativeBuffer,
texture: Option<&GpuTexture>, texture: Option<&GpuTexture>,
vertex_count: usize, index_count: usize,
args: &Args, args: &Args,
center: [f32; 3], center: [f32; 3],
camera_distance: f32, camera_distance: f32,
@@ -478,8 +492,9 @@ fn run_capture(
a_pos, a_pos,
a_uv, a_uv,
vbo, vbo,
ebo,
texture, texture,
vertex_count, index_count,
args.width, args.width,
args.height, args.height,
&mvp, &mvp,
@@ -504,8 +519,9 @@ fn run_interactive(
a_pos: u32, a_pos: u32,
a_uv: u32, a_uv: u32,
vbo: glow::NativeBuffer, vbo: glow::NativeBuffer,
ebo: glow::NativeBuffer,
texture: Option<&GpuTexture>, texture: Option<&GpuTexture>,
vertex_count: usize, index_count: usize,
args: &Args, args: &Args,
center: [f32; 3], center: [f32; 3],
camera_distance: f32, camera_distance: f32,
@@ -543,8 +559,9 @@ fn run_interactive(
a_pos, a_pos,
a_uv, a_uv,
vbo, vbo,
ebo,
texture, texture,
vertex_count, index_count,
w, w,
h, h,
&mvp, &mvp,
@@ -584,8 +601,9 @@ unsafe fn draw_frame(
a_pos: u32, a_pos: u32,
a_uv: u32, a_uv: u32,
vbo: glow::NativeBuffer, vbo: glow::NativeBuffer,
ebo: glow::NativeBuffer,
texture: Option<&GpuTexture>, texture: Option<&GpuTexture>,
vertex_count: usize, index_count: usize,
width: u32, width: u32,
height: u32, height: u32,
mvp: &[f32; 16], mvp: &[f32; 16],
@@ -614,17 +632,20 @@ unsafe fn draw_frame(
} }
gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo)); gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo));
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(ebo));
gl.enable_vertex_attrib_array(a_pos); gl.enable_vertex_attrib_array(a_pos);
gl.vertex_attrib_pointer_f32(a_pos, 3, glow::FLOAT, false, 20, 0); gl.vertex_attrib_pointer_f32(a_pos, 3, glow::FLOAT, false, 20, 0);
gl.enable_vertex_attrib_array(a_uv); gl.enable_vertex_attrib_array(a_uv);
gl.vertex_attrib_pointer_f32(a_uv, 2, glow::FLOAT, false, 20, 12); gl.vertex_attrib_pointer_f32(a_uv, 2, glow::FLOAT, false, 20, 12);
gl.draw_arrays( gl.draw_elements(
glow::TRIANGLES, glow::TRIANGLES,
index_count.min(i32::MAX as usize) as i32,
glow::UNSIGNED_SHORT,
0, 0,
vertex_count.min(i32::MAX as usize) as i32,
); );
gl.disable_vertex_attrib_array(a_uv); gl.disable_vertex_attrib_array(a_uv);
gl.disable_vertex_attrib_array(a_pos); gl.disable_vertex_attrib_array(a_pos);
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, None);
gl.bind_buffer(glow::ARRAY_BUFFER, None); gl.bind_buffer(glow::ARRAY_BUFFER, None);
gl.bind_texture(glow::TEXTURE_2D, None); gl.bind_texture(glow::TEXTURE_2D, None);
gl.use_program(None); gl.use_program(None);
@@ -758,6 +779,14 @@ fn f32_slice_to_ne_bytes(slice: &[f32]) -> Vec<u8> {
out out
} }
fn u16_slice_to_ne_bytes(slice: &[u16]) -> Vec<u8> {
let mut out = Vec::with_capacity(slice.len().saturating_mul(std::mem::size_of::<u16>()));
for &value in slice {
out.extend_from_slice(&value.to_ne_bytes());
}
out
}
fn mat4_identity() -> [f32; 16] { fn mat4_identity() -> [f32; 16] {
[ [
1.0, 0.0, 0.0, 0.0, // 1.0, 0.0, 0.0, 0.0, //

View File

@@ -6,3 +6,6 @@ edition = "2021"
[dependencies] [dependencies]
common = { path = "../common" } common = { path = "../common" }
flate2 = { version = "1", default-features = false, features = ["rust_backend"] } flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
[dev-dependencies]
proptest = "1"

View File

@@ -1,14 +1,17 @@
use super::*; use super::*;
use crate::compress::lzh::{LZH_MAX_FREQ, LZH_N_CHAR, LZH_R, LZH_T}; use crate::compress::lzh::{LZH_MAX_FREQ, LZH_N_CHAR, LZH_R, LZH_T};
use crate::compress::xor::xor_stream; use crate::compress::xor::xor_stream;
use common::collect_files_recursive;
use flate2::write::DeflateEncoder; use flate2::write::DeflateEncoder;
use flate2::write::ZlibEncoder; use flate2::write::ZlibEncoder;
use flate2::Compression; use flate2::Compression;
use proptest::prelude::*;
use std::any::Any; use std::any::Any;
use std::fs; use std::fs;
use std::io::Write as _; use std::io::Write as _;
use std::panic::{catch_unwind, AssertUnwindSafe}; use std::panic::{catch_unwind, AssertUnwindSafe};
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct SyntheticRsliEntry { struct SyntheticRsliEntry {
@@ -37,20 +40,6 @@ impl Default for RsliBuildOptions {
} }
} }
fn collect_files_recursive(root: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = fs::read_dir(root) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_files_recursive(&path, out);
} else if path.is_file() {
out.push(path);
}
}
}
fn rsli_test_files() -> Vec<PathBuf> { fn rsli_test_files() -> Vec<PathBuf> {
let root = Path::new(env!("CARGO_MANIFEST_DIR")) let root = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..") .join("..")
@@ -1335,3 +1324,15 @@ fn rsli_validation_error_cases() {
} }
let _ = fs::remove_file(&path); let _ = fs::remove_file(&path);
} }
proptest! {
#![proptest_config(ProptestConfig::with_cases(64))]
#[test]
fn parse_library_is_panic_free_on_random_bytes(data in proptest::collection::vec(any::<u8>(), 0..4096)) {
let _ = crate::parse::parse_library(
Arc::from(data.into_boxed_slice()),
OpenOptions::default(),
);
}
}

View File

@@ -4,4 +4,6 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dev-dependencies] [dev-dependencies]
common = { path = "../common" }
nres = { path = "../nres" } nres = { path = "../nres" }
proptest = "1"

View File

@@ -1,22 +1,10 @@
use super::*; use super::*;
use common::collect_files_recursive;
use nres::Archive; use nres::Archive;
use proptest::prelude::*;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
fn collect_files_recursive(root: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = fs::read_dir(root) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_files_recursive(&path, out);
} else if path.is_file() {
out.push(path);
}
}
}
fn nres_test_files() -> Vec<PathBuf> { fn nres_test_files() -> Vec<PathBuf> {
let root = Path::new(env!("CARGO_MANIFEST_DIR")) let root = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..") .join("..")
@@ -327,3 +315,16 @@ fn texm_errors_for_page_chunk_and_mip_bounds() {
Err(Error::MipDataOutOfBounds { .. }) 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);
}
}
}
}