feat: Refactor code structure and enhance functionality across multiple crates
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Resource payload that can be either borrowed from mapped bytes or owned.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -42,3 +44,18 @@ impl OutputBuffer for Vec<u8> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,9 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
encoding_rs = "0.8"
|
||||
nres = { path = "../nres" }
|
||||
|
||||
[dev-dependencies]
|
||||
common = { path = "../common" }
|
||||
proptest = "1"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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>;
|
||||
@@ -347,13 +348,18 @@ fn parse_res10_names(data: &[u8], node_count: usize) -> Result<Vec<Option<String
|
||||
} else {
|
||||
slice
|
||||
};
|
||||
let decoded = String::from_utf8_lossy(text).to_string();
|
||||
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>,
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
use super::*;
|
||||
use common::collect_files_recursive;
|
||||
use nres::Archive;
|
||||
use proptest::prelude::*;
|
||||
use std::fs;
|
||||
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> {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
@@ -169,18 +157,17 @@ fn res13_single_batch(index_start: u32, index_count: u16) -> Vec<u8> {
|
||||
batch
|
||||
}
|
||||
|
||||
fn res10_names(names: &[Option<&str>]) -> Vec<u8> {
|
||||
fn res10_names_raw(names: &[Option<&[u8]>]) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
for name in names {
|
||||
match name {
|
||||
Some(name) => {
|
||||
let bytes = name.as_bytes();
|
||||
out.extend_from_slice(
|
||||
&u32::try_from(bytes.len())
|
||||
&u32::try_from(name.len())
|
||||
.expect("name size overflow in test")
|
||||
.to_le_bytes(),
|
||||
);
|
||||
out.extend_from_slice(bytes);
|
||||
out.extend_from_slice(name);
|
||||
out.push(0);
|
||||
}
|
||||
None => out.extend_from_slice(&0u32.to_le_bytes()),
|
||||
@@ -189,6 +176,11 @@ fn res10_names(names: &[Option<&str>]) -> Vec<u8> {
|
||||
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))),
|
||||
@@ -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]));
|
||||
}
|
||||
|
||||
#[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();
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use common::collect_files_recursive;
|
||||
use std::any::Any;
|
||||
use std::fs;
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
@@ -13,20 +14,6 @@ struct SyntheticEntry<'a> {
|
||||
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> {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
|
||||
@@ -7,4 +7,5 @@ edition = "2021"
|
||||
msh-core = { path = "../msh-core" }
|
||||
|
||||
[dev-dependencies]
|
||||
common = { path = "../common" }
|
||||
nres = { path = "../nres" }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use msh_core::Model;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub const DEFAULT_UV_SCALE: f32 = 1024.0;
|
||||
|
||||
@@ -11,21 +12,24 @@ pub struct RenderVertex {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RenderMesh {
|
||||
pub vertices: Vec<RenderVertex>,
|
||||
pub indices: Vec<u16>,
|
||||
pub batch_count: usize,
|
||||
pub index_overflow: bool,
|
||||
}
|
||||
|
||||
impl RenderMesh {
|
||||
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.
|
||||
///
|
||||
/// The output is suitable for simple `glDrawArrays(GL_TRIANGLES, ...)` paths.
|
||||
/// Builds an indexed triangle mesh for a specific LOD/group pair.
|
||||
pub fn build_render_mesh(model: &Model, lod: usize, group: usize) -> RenderMesh {
|
||||
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 index_overflow = false;
|
||||
let uv0 = model.uv0.as_ref();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
let batch_out_start = indices.len();
|
||||
let mut batch_valid = true;
|
||||
for &idx in &model.indices[index_start..index_end] {
|
||||
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 {
|
||||
continue;
|
||||
batch_valid = false;
|
||||
break;
|
||||
};
|
||||
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
|
||||
.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,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
RenderMesh {
|
||||
vertices,
|
||||
indices,
|
||||
batch_count,
|
||||
index_overflow,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,10 @@
|
||||
use super::*;
|
||||
use common::collect_files_recursive;
|
||||
use msh_core::parse_model_payload;
|
||||
use nres::Archive;
|
||||
use std::fs;
|
||||
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> {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
@@ -71,12 +58,20 @@ fn build_render_mesh_for_real_models() {
|
||||
)
|
||||
});
|
||||
let mesh = build_render_mesh(&model, 0, 0);
|
||||
if !mesh.vertices.is_empty() {
|
||||
if !mesh.indices.is_empty() {
|
||||
meshes_non_empty += 1;
|
||||
}
|
||||
if compute_bounds_for_mesh(&mesh.vertices).is_some() {
|
||||
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 {
|
||||
assert!(
|
||||
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);
|
||||
assert!(mesh.vertices.is_empty());
|
||||
assert!(mesh.indices.is_empty());
|
||||
assert_eq!(mesh.batch_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);
|
||||
assert_eq!(mesh.batch_count, 2);
|
||||
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.vertices[0].uv0, [1.0, -1.0]);
|
||||
assert_eq!(mesh.vertices[1].uv0, [0.5, 0.25]);
|
||||
assert_eq!(mesh.vertices[2].uv0, [0.0, 0.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);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ sdl2 = { version = "0.37", optional = true, default-features = false, features =
|
||||
glow = { version = "0.16", optional = true }
|
||||
image = { version = "0.25", optional = true, default-features = false, features = ["png"] }
|
||||
|
||||
[dev-dependencies]
|
||||
common = { path = "../common" }
|
||||
|
||||
[[bin]]
|
||||
name = "parkan-render-demo"
|
||||
path = "src/main.rs"
|
||||
|
||||
@@ -435,23 +435,10 @@ fn decode_texture_entry(archive: &Archive, entry: EntryRef<'_>) -> Result<Loaded
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use common::collect_files_recursive;
|
||||
use std::fs;
|
||||
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> {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
|
||||
@@ -221,12 +221,17 @@ fn run(args: Args) -> Result<(), String> {
|
||||
)
|
||||
})?;
|
||||
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!(
|
||||
"model has no renderable triangles for lod={} 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 {
|
||||
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]);
|
||||
}
|
||||
let vertex_bytes = f32_slice_to_ne_bytes(&vertex_data);
|
||||
let index_bytes = u16_slice_to_ne_bytes(&mesh.indices);
|
||||
|
||||
let gl = unsafe {
|
||||
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"))?;
|
||||
|
||||
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 {
|
||||
gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo));
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -341,8 +351,9 @@ fn run(args: Args) -> Result<(), String> {
|
||||
a_pos,
|
||||
a_uv,
|
||||
vbo,
|
||||
ebo,
|
||||
gpu_texture.as_ref(),
|
||||
mesh.vertices.len(),
|
||||
mesh.indices.len(),
|
||||
&args,
|
||||
center,
|
||||
camera_distance,
|
||||
@@ -360,8 +371,9 @@ fn run(args: Args) -> Result<(), String> {
|
||||
a_pos,
|
||||
a_uv,
|
||||
vbo,
|
||||
ebo,
|
||||
gpu_texture.as_ref(),
|
||||
mesh.vertices.len(),
|
||||
mesh.indices.len(),
|
||||
&args,
|
||||
center,
|
||||
camera_distance,
|
||||
@@ -372,6 +384,7 @@ fn run(args: Args) -> Result<(), String> {
|
||||
if let Some(texture) = gpu_texture {
|
||||
gl.delete_texture(texture.handle);
|
||||
}
|
||||
gl.delete_buffer(ebo);
|
||||
gl.delete_buffer(vbo);
|
||||
gl.delete_program(program);
|
||||
}
|
||||
@@ -452,8 +465,9 @@ fn run_capture(
|
||||
a_pos: u32,
|
||||
a_uv: u32,
|
||||
vbo: glow::NativeBuffer,
|
||||
ebo: glow::NativeBuffer,
|
||||
texture: Option<&GpuTexture>,
|
||||
vertex_count: usize,
|
||||
index_count: usize,
|
||||
args: &Args,
|
||||
center: [f32; 3],
|
||||
camera_distance: f32,
|
||||
@@ -478,8 +492,9 @@ fn run_capture(
|
||||
a_pos,
|
||||
a_uv,
|
||||
vbo,
|
||||
ebo,
|
||||
texture,
|
||||
vertex_count,
|
||||
index_count,
|
||||
args.width,
|
||||
args.height,
|
||||
&mvp,
|
||||
@@ -504,8 +519,9 @@ fn run_interactive(
|
||||
a_pos: u32,
|
||||
a_uv: u32,
|
||||
vbo: glow::NativeBuffer,
|
||||
ebo: glow::NativeBuffer,
|
||||
texture: Option<&GpuTexture>,
|
||||
vertex_count: usize,
|
||||
index_count: usize,
|
||||
args: &Args,
|
||||
center: [f32; 3],
|
||||
camera_distance: f32,
|
||||
@@ -543,8 +559,9 @@ fn run_interactive(
|
||||
a_pos,
|
||||
a_uv,
|
||||
vbo,
|
||||
ebo,
|
||||
texture,
|
||||
vertex_count,
|
||||
index_count,
|
||||
w,
|
||||
h,
|
||||
&mvp,
|
||||
@@ -584,8 +601,9 @@ unsafe fn draw_frame(
|
||||
a_pos: u32,
|
||||
a_uv: u32,
|
||||
vbo: glow::NativeBuffer,
|
||||
ebo: glow::NativeBuffer,
|
||||
texture: Option<&GpuTexture>,
|
||||
vertex_count: usize,
|
||||
index_count: usize,
|
||||
width: u32,
|
||||
height: u32,
|
||||
mvp: &[f32; 16],
|
||||
@@ -614,17 +632,20 @@ unsafe fn draw_frame(
|
||||
}
|
||||
|
||||
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.vertex_attrib_pointer_f32(a_pos, 3, glow::FLOAT, false, 20, 0);
|
||||
gl.enable_vertex_attrib_array(a_uv);
|
||||
gl.vertex_attrib_pointer_f32(a_uv, 2, glow::FLOAT, false, 20, 12);
|
||||
gl.draw_arrays(
|
||||
gl.draw_elements(
|
||||
glow::TRIANGLES,
|
||||
index_count.min(i32::MAX as usize) as i32,
|
||||
glow::UNSIGNED_SHORT,
|
||||
0,
|
||||
vertex_count.min(i32::MAX as usize) as i32,
|
||||
);
|
||||
gl.disable_vertex_attrib_array(a_uv);
|
||||
gl.disable_vertex_attrib_array(a_pos);
|
||||
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, None);
|
||||
gl.bind_buffer(glow::ARRAY_BUFFER, None);
|
||||
gl.bind_texture(glow::TEXTURE_2D, None);
|
||||
gl.use_program(None);
|
||||
@@ -758,6 +779,14 @@ fn f32_slice_to_ne_bytes(slice: &[f32]) -> Vec<u8> {
|
||||
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] {
|
||||
[
|
||||
1.0, 0.0, 0.0, 0.0, //
|
||||
|
||||
@@ -6,3 +6,6 @@ edition = "2021"
|
||||
[dependencies]
|
||||
common = { path = "../common" }
|
||||
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = "1"
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
use super::*;
|
||||
use crate::compress::lzh::{LZH_MAX_FREQ, LZH_N_CHAR, LZH_R, LZH_T};
|
||||
use crate::compress::xor::xor_stream;
|
||||
use common::collect_files_recursive;
|
||||
use flate2::write::DeflateEncoder;
|
||||
use flate2::write::ZlibEncoder;
|
||||
use flate2::Compression;
|
||||
use proptest::prelude::*;
|
||||
use std::any::Any;
|
||||
use std::fs;
|
||||
use std::io::Write as _;
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
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> {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
@@ -1335,3 +1324,15 @@ fn rsli_validation_error_cases() {
|
||||
}
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,6 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dev-dependencies]
|
||||
common = { path = "../common" }
|
||||
nres = { path = "../nres" }
|
||||
proptest = "1"
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
use super::*;
|
||||
use common::collect_files_recursive;
|
||||
use nres::Archive;
|
||||
use proptest::prelude::*;
|
||||
use std::fs;
|
||||
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> {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
@@ -327,3 +315,16 @@ fn texm_errors_for_page_chunk_and_mip_bounds() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user