Some checks failed
API and SemVer surface / api-surface (push) Failing after 1m34s
Native code generation / deterministic (push) Has been cancelled
Concurrency and resource soak audit / soak (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
performance evidence / audit (push) Has been cancelled
First release candidate / non-fuzz-release-gate (push) Has been cancelled
Release platform and feature matrix / audit (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
Native release artifact audit / audit (push) Has been cancelled
Imaging and meshing gate / native (push) Failing after 53s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 59s
Skia feature / linux (push) Has been cancelled
724 lines
25 KiB
Rust
724 lines
25 KiB
Rust
//! Validated reader for Linden Lab's legacy `.llm` avatar mesh format.
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::path::Path;
|
|
|
|
use libremetaverse_types::compat::{Object, SortedList};
|
|
use libremetaverse_types::{Error, Vector2, Vector3};
|
|
|
|
use crate::rendering::{
|
|
LindenMeshFace, LindenMeshMorph, LindenMeshMorphVertex, LindenMeshSkinWeightElement,
|
|
LindenMeshVertex, LindenMeshVertexRemap, LindenSkeleton,
|
|
};
|
|
|
|
const MESH_HEADER: &str = "Linden Binary Mesh 1.0";
|
|
const MORPH_FOOTER: &str = "End Morphs";
|
|
const MAX_MESH_BYTES: usize = 64 * 1024 * 1024;
|
|
const MAX_MORPHS: usize = 16_384;
|
|
const MORPH_VERTEX_BYTES: usize = 48;
|
|
|
|
/// Complete native state for a full Linden avatar mesh.
|
|
#[derive(Clone, Debug)]
|
|
pub struct LindenMesh {
|
|
pub min_pixel_width: f32,
|
|
pub morphs: Vec<LindenMeshMorph>,
|
|
pub skin_weights: Vec<LindenMeshSkinWeightElement>,
|
|
name: String,
|
|
header: String,
|
|
has_weights: bool,
|
|
has_detail_tex_coords: bool,
|
|
position: Vector3,
|
|
rotation_angles: Vector3,
|
|
scale: Vector3,
|
|
num_vertices: u16,
|
|
vertices: Vec<LindenMeshVertex>,
|
|
num_faces: u16,
|
|
faces: Vec<LindenMeshFace>,
|
|
num_skin_joints: u16,
|
|
skin_joints: Vec<String>,
|
|
num_remaps: i32,
|
|
vertex_remaps: Vec<LindenMeshVertexRemap>,
|
|
lod_meshes: SortedList<i32, Object>,
|
|
skeleton: Option<LindenSkeleton>,
|
|
}
|
|
|
|
/// Complete native state for a reference-only Linden LOD mesh.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct ReferenceMesh {
|
|
pub min_pixel_width: f32,
|
|
pub header: String,
|
|
pub has_weights: bool,
|
|
pub has_detail_tex_coords: bool,
|
|
pub position: Vector3,
|
|
pub rotation_angles: Vector3,
|
|
pub rotation_order: u8,
|
|
pub scale: Vector3,
|
|
pub num_faces: u16,
|
|
pub faces: Vec<LindenMeshFace>,
|
|
}
|
|
|
|
impl LindenMesh {
|
|
pub(crate) fn native_new(
|
|
name: String,
|
|
skeleton: Option<LindenSkeleton>,
|
|
) -> Result<Self, Error> {
|
|
Ok(Self {
|
|
min_pixel_width: 0.0,
|
|
morphs: Vec::new(),
|
|
skin_weights: Vec::new(),
|
|
name,
|
|
header: String::new(),
|
|
has_weights: false,
|
|
has_detail_tex_coords: false,
|
|
position: Vector3::zero(),
|
|
rotation_angles: Vector3::zero(),
|
|
scale: Vector3::zero(),
|
|
num_vertices: 0,
|
|
vertices: Vec::new(),
|
|
num_faces: 0,
|
|
faces: Vec::new(),
|
|
num_skin_joints: 0,
|
|
skin_joints: Vec::new(),
|
|
num_remaps: 0,
|
|
vertex_remaps: Vec::new(),
|
|
lod_meshes: SortedList(BTreeMap::new()),
|
|
skeleton: Some(skeleton.unwrap_or(LindenSkeleton::load_with_method()?)),
|
|
})
|
|
}
|
|
|
|
pub(crate) fn native_load_mesh(&mut self, filename: &str) -> Result<(), Error> {
|
|
let bytes = read_mesh_file(filename)?;
|
|
let parsed = ParsedFullMesh::parse(&bytes)?;
|
|
|
|
self.header = parsed.header;
|
|
self.has_weights = parsed.has_weights;
|
|
self.has_detail_tex_coords = parsed.has_detail_tex_coords;
|
|
self.position = parsed.position;
|
|
self.rotation_angles = parsed.rotation_angles;
|
|
self.scale = parsed.scale;
|
|
self.num_vertices = parsed.num_vertices;
|
|
self.vertices = parsed.vertices;
|
|
self.num_faces = parsed.num_faces;
|
|
self.faces = parsed.faces;
|
|
self.num_skin_joints = parsed.num_skin_joints;
|
|
self.skin_joints = parsed.skin_joints;
|
|
self.morphs = parsed.morphs;
|
|
self.num_remaps = parsed.num_remaps;
|
|
self.vertex_remaps = parsed.vertex_remaps;
|
|
self.expand_skin_weights()?;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn native_load_lod_mesh(
|
|
&mut self,
|
|
level: i32,
|
|
filename: &str,
|
|
) -> Result<Object, Error> {
|
|
let object = if is_eye_lod(filename) {
|
|
let mut mesh = Self::native_new(String::new(), None)?;
|
|
mesh.native_load_mesh(filename)?;
|
|
Object::opaque(mesh)
|
|
} else {
|
|
let mut mesh = ReferenceMesh::native_new()?;
|
|
mesh.native_load_mesh(filename)?;
|
|
Object::opaque(mesh)
|
|
};
|
|
self.lod_meshes.0.insert(level, object.clone());
|
|
Ok(object)
|
|
}
|
|
|
|
pub(crate) fn native_load_reference_mesh(
|
|
&mut self,
|
|
lod_level: i32,
|
|
filename: &str,
|
|
) -> Result<ReferenceMesh, Error> {
|
|
if is_eye_lod(filename) {
|
|
return Err(Error::Argument);
|
|
}
|
|
let mut mesh = ReferenceMesh::native_new()?;
|
|
mesh.native_load_mesh(filename)?;
|
|
self.lod_meshes
|
|
.0
|
|
.insert(lod_level, Object::opaque(mesh.clone()));
|
|
Ok(mesh)
|
|
}
|
|
|
|
fn expand_skin_weights(&mut self) -> Result<(), Error> {
|
|
self.skin_weights.clear();
|
|
let Some(skeleton) = &self.skeleton else {
|
|
return Ok(());
|
|
};
|
|
let mut joints =
|
|
skeleton.build_expanded_joint_list(Box::new(self.skin_joints.clone().into_iter()))?;
|
|
if joints.is_empty() {
|
|
match self.name.as_str() {
|
|
"eyeBallLeftMesh" => joints.extend(["mEyeLeft".to_owned(), "mSkull".to_owned()]),
|
|
"eyeBallRightMesh" => {
|
|
joints.extend(["mEyeRight".to_owned(), "mSkull".to_owned()]);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
if joints.is_empty() {
|
|
return Ok(());
|
|
}
|
|
self.skin_weights.reserve(self.vertices.len());
|
|
for vertex in &self.vertices {
|
|
let raw_index = vertex.weight.floor() as i32;
|
|
let bone_index = raw_index - 1;
|
|
let blend = (vertex.weight - raw_index as f32).clamp(0.0, 1.0);
|
|
let valid_pair = usize::try_from(bone_index)
|
|
.ok()
|
|
.filter(|index| index.saturating_add(1) < joints.len());
|
|
let weight = if let Some(index) = valid_pair {
|
|
LindenMeshSkinWeightElement {
|
|
bone1: joints[index].clone(),
|
|
bone2: joints[index + 1].clone(),
|
|
weight1: 1.0 - blend,
|
|
weight2: blend,
|
|
}
|
|
} else {
|
|
let nearest = bone_index.clamp(0, joints.len().saturating_sub(1) as i32) as usize;
|
|
LindenMeshSkinWeightElement {
|
|
bone1: joints[nearest].clone(),
|
|
bone2: "mPelvis".to_owned(),
|
|
weight1: 1.0,
|
|
weight2: 0.0,
|
|
}
|
|
};
|
|
self.skin_weights.push(weight);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn native_faces(&self) -> Vec<LindenMeshFace> {
|
|
self.faces.clone()
|
|
}
|
|
pub(crate) fn native_set_faces(&mut self, value: Vec<LindenMeshFace>) {
|
|
self.num_faces = value.len().try_into().unwrap_or(u16::MAX);
|
|
self.faces = value;
|
|
}
|
|
pub(crate) fn native_has_detail_tex_coords(&self) -> bool {
|
|
self.has_detail_tex_coords
|
|
}
|
|
pub(crate) fn native_set_has_detail_tex_coords(&mut self, value: bool) {
|
|
self.has_detail_tex_coords = value;
|
|
}
|
|
pub(crate) fn native_has_weights(&self) -> bool {
|
|
self.has_weights
|
|
}
|
|
pub(crate) fn native_set_has_weights(&mut self, value: bool) {
|
|
self.has_weights = value;
|
|
}
|
|
pub(crate) fn native_header(&self) -> String {
|
|
self.header.clone()
|
|
}
|
|
pub(crate) fn native_set_header(&mut self, value: String) {
|
|
self.header = value;
|
|
}
|
|
pub(crate) fn native_lod_meshes(&self) -> SortedList<i32, Object> {
|
|
self.lod_meshes.clone()
|
|
}
|
|
pub(crate) fn native_set_lod_meshes(&mut self, value: SortedList<i32, Object>) {
|
|
self.lod_meshes = value;
|
|
}
|
|
pub(crate) fn native_name(&self) -> String {
|
|
self.name.clone()
|
|
}
|
|
pub(crate) fn native_set_name(&mut self, value: String) {
|
|
self.name = value;
|
|
}
|
|
pub(crate) fn native_num_faces(&self) -> u16 {
|
|
self.num_faces
|
|
}
|
|
pub(crate) fn native_set_num_faces(&mut self, value: u16) {
|
|
self.num_faces = value;
|
|
}
|
|
pub(crate) fn native_num_remaps(&self) -> i32 {
|
|
self.num_remaps
|
|
}
|
|
pub(crate) fn native_set_num_remaps(&mut self, value: i32) {
|
|
self.num_remaps = value;
|
|
}
|
|
pub(crate) fn native_num_skin_joints(&self) -> u16 {
|
|
self.num_skin_joints
|
|
}
|
|
pub(crate) fn native_set_num_skin_joints(&mut self, value: u16) {
|
|
self.num_skin_joints = value;
|
|
}
|
|
pub(crate) fn native_num_vertices(&self) -> u16 {
|
|
self.num_vertices
|
|
}
|
|
pub(crate) fn native_set_num_vertices(&mut self, value: u16) {
|
|
self.num_vertices = value;
|
|
}
|
|
pub(crate) fn native_position(&self) -> Vector3 {
|
|
self.position
|
|
}
|
|
pub(crate) fn native_set_position(&mut self, value: Vector3) {
|
|
self.position = value;
|
|
}
|
|
pub(crate) fn native_rotation_angles(&self) -> Vector3 {
|
|
self.rotation_angles
|
|
}
|
|
pub(crate) fn native_set_rotation_angles(&mut self, value: Vector3) {
|
|
self.rotation_angles = value;
|
|
}
|
|
pub(crate) fn native_scale(&self) -> Vector3 {
|
|
self.scale
|
|
}
|
|
pub(crate) fn native_set_scale(&mut self, value: Vector3) {
|
|
self.scale = value;
|
|
}
|
|
pub(crate) fn native_skeleton(&self) -> Option<LindenSkeleton> {
|
|
self.skeleton.clone()
|
|
}
|
|
pub(crate) fn native_set_skeleton(&mut self, value: Option<LindenSkeleton>) {
|
|
self.skeleton = value;
|
|
}
|
|
pub(crate) fn native_skin_joints(&self) -> Vec<String> {
|
|
self.skin_joints.clone()
|
|
}
|
|
pub(crate) fn native_set_skin_joints(&mut self, value: Vec<String>) {
|
|
self.num_skin_joints = value.len().try_into().unwrap_or(u16::MAX);
|
|
self.skin_joints = value;
|
|
}
|
|
pub(crate) fn native_vertex_remaps(&self) -> Vec<LindenMeshVertexRemap> {
|
|
self.vertex_remaps.clone()
|
|
}
|
|
pub(crate) fn native_set_vertex_remaps(&mut self, value: Vec<LindenMeshVertexRemap>) {
|
|
self.num_remaps = value.len().try_into().unwrap_or(i32::MAX);
|
|
self.vertex_remaps = value;
|
|
}
|
|
pub(crate) fn native_vertices(&self) -> Vec<LindenMeshVertex> {
|
|
self.vertices.clone()
|
|
}
|
|
pub(crate) fn native_set_vertices(&mut self, value: Vec<LindenMeshVertex>) {
|
|
self.num_vertices = value.len().try_into().unwrap_or(u16::MAX);
|
|
self.vertices = value;
|
|
}
|
|
}
|
|
|
|
impl ReferenceMesh {
|
|
pub(crate) fn native_new() -> Result<Self, Error> {
|
|
Ok(Self {
|
|
min_pixel_width: 0.0,
|
|
header: String::new(),
|
|
has_weights: false,
|
|
has_detail_tex_coords: false,
|
|
position: Vector3::zero(),
|
|
rotation_angles: Vector3::zero(),
|
|
rotation_order: 0,
|
|
scale: Vector3::zero(),
|
|
num_faces: 0,
|
|
faces: Vec::new(),
|
|
})
|
|
}
|
|
|
|
pub(crate) fn native_load_mesh(&mut self, filename: &str) -> Result<(), Error> {
|
|
let bytes = read_mesh_file(filename)?;
|
|
let mut reader = Reader::new(&bytes);
|
|
self.header = reader.fixed_string(24, "reference mesh header")?;
|
|
validate_header(&self.header)?;
|
|
self.has_weights = reader.byte("reference weights flag")? != 0;
|
|
self.has_detail_tex_coords = reader.byte("reference detail flag")? != 0;
|
|
self.position = reader.vector3("reference position")?;
|
|
self.rotation_angles = reader.vector3("reference rotation")?;
|
|
self.rotation_order = reader.byte("reference rotation order")?;
|
|
self.scale = reader.vector3("reference scale")?;
|
|
self.num_faces = reader.u16("reference face count")?;
|
|
self.faces = read_faces(&mut reader, self.num_faces)?;
|
|
reader.finish("reference mesh trailing bytes")?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct ParsedFullMesh {
|
|
header: String,
|
|
has_weights: bool,
|
|
has_detail_tex_coords: bool,
|
|
position: Vector3,
|
|
rotation_angles: Vector3,
|
|
scale: Vector3,
|
|
num_vertices: u16,
|
|
vertices: Vec<LindenMeshVertex>,
|
|
num_faces: u16,
|
|
faces: Vec<LindenMeshFace>,
|
|
num_skin_joints: u16,
|
|
skin_joints: Vec<String>,
|
|
morphs: Vec<LindenMeshMorph>,
|
|
num_remaps: i32,
|
|
vertex_remaps: Vec<LindenMeshVertexRemap>,
|
|
}
|
|
|
|
impl ParsedFullMesh {
|
|
fn parse(bytes: &[u8]) -> Result<Self, Error> {
|
|
let mut reader = Reader::new(bytes);
|
|
let header = reader.fixed_string(24, "mesh header")?;
|
|
validate_header(&header)?;
|
|
let has_weights = reader.byte("weights flag")? != 0;
|
|
let has_detail_tex_coords = reader.byte("detail flag")? != 0;
|
|
let position = reader.vector3("position")?;
|
|
let rotation_angles = reader.vector3("rotation")?;
|
|
let _rotation_order = reader.byte("rotation order")?;
|
|
let scale = reader.vector3("scale")?;
|
|
let num_vertices = reader.u16("vertex count")?;
|
|
let count = usize::from(num_vertices);
|
|
let mut vertices = vec![empty_vertex(); count];
|
|
for vertex in &mut vertices {
|
|
vertex.coord = reader.vector3("vertex coordinates")?;
|
|
}
|
|
for vertex in &mut vertices {
|
|
vertex.normal = reader.vector3("vertex normals")?;
|
|
}
|
|
for vertex in &mut vertices {
|
|
vertex.bi_normal = reader.vector3("vertex binormals")?;
|
|
}
|
|
for vertex in &mut vertices {
|
|
vertex.tex_coord = reader.vector2("vertex texture coordinates")?;
|
|
}
|
|
if has_detail_tex_coords {
|
|
for vertex in &mut vertices {
|
|
vertex.detail_tex_coord = reader.vector2("vertex detail texture coordinates")?;
|
|
}
|
|
}
|
|
if has_weights {
|
|
for vertex in &mut vertices {
|
|
vertex.weight = reader.f32("vertex weight")?;
|
|
if !vertex.weight.is_finite() {
|
|
return Err(parse_error(reader.position(), "non-finite vertex weight"));
|
|
}
|
|
}
|
|
}
|
|
let num_faces = reader.u16("face count")?;
|
|
let faces = read_faces(&mut reader, num_faces)?;
|
|
let (num_skin_joints, skin_joints) = if has_weights {
|
|
let count = reader.u16("skin joint count")?;
|
|
let mut joints = Vec::with_capacity(usize::from(count));
|
|
for _ in 0..count {
|
|
joints.push(reader.fixed_string(64, "skin joint name")?);
|
|
}
|
|
(count, joints)
|
|
} else {
|
|
(0, Vec::new())
|
|
};
|
|
let mut morphs = Vec::new();
|
|
loop {
|
|
let name = reader.fixed_string(64, "morph name")?;
|
|
if name == MORPH_FOOTER {
|
|
break;
|
|
}
|
|
if morphs.len() >= MAX_MORPHS {
|
|
return Err(parse_error(reader.position(), "morph count exceeds limit"));
|
|
}
|
|
let signed_count = reader.i32("morph vertex count")?;
|
|
let morph_count = usize::try_from(signed_count)
|
|
.map_err(|_| parse_error(reader.position(), "negative morph vertex count"))?;
|
|
reader.require(
|
|
morph_count
|
|
.checked_mul(MORPH_VERTEX_BYTES)
|
|
.ok_or_else(|| parse_error(reader.position(), "morph size overflow"))?,
|
|
"morph vertices",
|
|
)?;
|
|
let mut morph_vertices = Vec::with_capacity(morph_count);
|
|
for _ in 0..morph_count {
|
|
morph_vertices.push(LindenMeshMorphVertex {
|
|
vertex_index: reader.u32("morph vertex index")?,
|
|
coord: reader.vector3("morph coordinates")?,
|
|
normal: reader.vector3("morph normal")?,
|
|
bi_normal: reader.vector3("morph binormal")?,
|
|
tex_coord: reader.vector2("morph texture coordinates")?,
|
|
});
|
|
}
|
|
morphs.push(LindenMeshMorph {
|
|
name,
|
|
num_vertices: signed_count,
|
|
vertices: morph_vertices,
|
|
});
|
|
}
|
|
let (num_remaps, vertex_remaps) = if reader.remaining() == 0 {
|
|
(0, Vec::new())
|
|
} else {
|
|
let signed_count = reader.i32("vertex remap count")?;
|
|
let count = usize::try_from(signed_count)
|
|
.map_err(|_| parse_error(reader.position(), "negative vertex remap count"))?;
|
|
reader.require(
|
|
count
|
|
.checked_mul(8)
|
|
.ok_or_else(|| parse_error(reader.position(), "vertex remap size overflow"))?,
|
|
"vertex remaps",
|
|
)?;
|
|
let mut remaps = Vec::with_capacity(count);
|
|
for _ in 0..count {
|
|
remaps.push(LindenMeshVertexRemap {
|
|
remap_source: reader.i32("remap source")?,
|
|
remap_destination: reader.i32("remap destination")?,
|
|
});
|
|
}
|
|
(signed_count, remaps)
|
|
};
|
|
reader.finish("mesh trailing bytes")?;
|
|
Ok(Self {
|
|
header,
|
|
has_weights,
|
|
has_detail_tex_coords,
|
|
position,
|
|
rotation_angles,
|
|
scale,
|
|
num_vertices,
|
|
vertices,
|
|
num_faces,
|
|
faces,
|
|
num_skin_joints,
|
|
skin_joints,
|
|
morphs,
|
|
num_remaps,
|
|
vertex_remaps,
|
|
})
|
|
}
|
|
}
|
|
|
|
fn empty_vertex() -> LindenMeshVertex {
|
|
LindenMeshVertex {
|
|
bi_normal: Vector3::zero(),
|
|
coord: Vector3::zero(),
|
|
detail_tex_coord: Vector2::zero(),
|
|
normal: Vector3::zero(),
|
|
tex_coord: Vector2::zero(),
|
|
weight: 0.0,
|
|
}
|
|
}
|
|
|
|
fn read_faces(reader: &mut Reader<'_>, count: u16) -> Result<Vec<LindenMeshFace>, Error> {
|
|
reader.require(usize::from(count) * 6, "face indices")?;
|
|
let mut faces = Vec::with_capacity(usize::from(count));
|
|
for _ in 0..count {
|
|
faces.push(LindenMeshFace {
|
|
indices: vec![
|
|
reader.i16("face index")?,
|
|
reader.i16("face index")?,
|
|
reader.i16("face index")?,
|
|
],
|
|
});
|
|
}
|
|
Ok(faces)
|
|
}
|
|
|
|
fn read_mesh_file(filename: &str) -> Result<Vec<u8>, Error> {
|
|
let metadata = std::fs::metadata(filename).map_err(|_| Error::Argument)?;
|
|
let length = usize::try_from(metadata.len()).map_err(|_| Error::Argument)?;
|
|
if length > MAX_MESH_BYTES {
|
|
return Err(Error::Argument);
|
|
}
|
|
let bytes = std::fs::read(filename).map_err(|_| Error::Argument)?;
|
|
if bytes.len() > MAX_MESH_BYTES {
|
|
return Err(Error::Argument);
|
|
}
|
|
Ok(bytes)
|
|
}
|
|
|
|
fn is_eye_lod(filename: &str) -> bool {
|
|
Path::new(filename)
|
|
.file_name()
|
|
.and_then(|name| name.to_str())
|
|
== Some("avatar_eye_1.llm")
|
|
}
|
|
|
|
fn validate_header(header: &str) -> Result<(), Error> {
|
|
if header == MESH_HEADER {
|
|
Ok(())
|
|
} else {
|
|
Err(parse_error(0, "unrecognized Linden mesh header"))
|
|
}
|
|
}
|
|
|
|
pub(crate) fn trim_at_nul(value: &str) -> String {
|
|
value
|
|
.split_once('\0')
|
|
.map_or(value, |(prefix, _)| prefix)
|
|
.to_owned()
|
|
}
|
|
|
|
struct Reader<'a> {
|
|
bytes: &'a [u8],
|
|
position: usize,
|
|
}
|
|
|
|
impl<'a> Reader<'a> {
|
|
fn new(bytes: &'a [u8]) -> Self {
|
|
Self { bytes, position: 0 }
|
|
}
|
|
fn position(&self) -> usize {
|
|
self.position
|
|
}
|
|
fn remaining(&self) -> usize {
|
|
self.bytes.len().saturating_sub(self.position)
|
|
}
|
|
fn require(&self, count: usize, context: &'static str) -> Result<(), Error> {
|
|
if count <= self.remaining() {
|
|
Ok(())
|
|
} else {
|
|
Err(parse_error(self.position, context))
|
|
}
|
|
}
|
|
fn take(&mut self, count: usize, context: &'static str) -> Result<&'a [u8], Error> {
|
|
self.require(count, context)?;
|
|
let start = self.position;
|
|
self.position += count;
|
|
Ok(&self.bytes[start..self.position])
|
|
}
|
|
fn byte(&mut self, context: &'static str) -> Result<u8, Error> {
|
|
Ok(self.take(1, context)?[0])
|
|
}
|
|
fn i16(&mut self, context: &'static str) -> Result<i16, Error> {
|
|
let bytes: [u8; 2] = self
|
|
.take(2, context)?
|
|
.try_into()
|
|
.map_err(|_| parse_error(self.position, context))?;
|
|
Ok(i16::from_le_bytes(bytes))
|
|
}
|
|
fn u16(&mut self, context: &'static str) -> Result<u16, Error> {
|
|
let bytes: [u8; 2] = self
|
|
.take(2, context)?
|
|
.try_into()
|
|
.map_err(|_| parse_error(self.position, context))?;
|
|
Ok(u16::from_le_bytes(bytes))
|
|
}
|
|
fn i32(&mut self, context: &'static str) -> Result<i32, Error> {
|
|
let bytes: [u8; 4] = self
|
|
.take(4, context)?
|
|
.try_into()
|
|
.map_err(|_| parse_error(self.position, context))?;
|
|
Ok(i32::from_le_bytes(bytes))
|
|
}
|
|
fn u32(&mut self, context: &'static str) -> Result<u32, Error> {
|
|
let bytes: [u8; 4] = self
|
|
.take(4, context)?
|
|
.try_into()
|
|
.map_err(|_| parse_error(self.position, context))?;
|
|
Ok(u32::from_le_bytes(bytes))
|
|
}
|
|
fn f32(&mut self, context: &'static str) -> Result<f32, Error> {
|
|
let bytes: [u8; 4] = self
|
|
.take(4, context)?
|
|
.try_into()
|
|
.map_err(|_| parse_error(self.position, context))?;
|
|
let value = f32::from_le_bytes(bytes);
|
|
if value.is_finite() {
|
|
Ok(value)
|
|
} else {
|
|
Err(parse_error(self.position, context))
|
|
}
|
|
}
|
|
fn vector2(&mut self, context: &'static str) -> Result<Vector2, Error> {
|
|
Ok(Vector2 {
|
|
x: self.f32(context)?,
|
|
y: self.f32(context)?,
|
|
})
|
|
}
|
|
fn vector3(&mut self, context: &'static str) -> Result<Vector3, Error> {
|
|
Ok(Vector3 {
|
|
x: self.f32(context)?,
|
|
y: self.f32(context)?,
|
|
z: self.f32(context)?,
|
|
})
|
|
}
|
|
fn fixed_string(&mut self, count: usize, context: &'static str) -> Result<String, Error> {
|
|
Ok(trim_at_nul(&String::from_utf8_lossy(
|
|
self.take(count, context)?,
|
|
)))
|
|
}
|
|
fn finish(&self, context: &'static str) -> Result<(), Error> {
|
|
if self.remaining() == 0 {
|
|
Ok(())
|
|
} else {
|
|
Err(parse_error(self.position, context))
|
|
}
|
|
}
|
|
}
|
|
|
|
const fn parse_error(position: usize, context: &'static str) -> Error {
|
|
Error::Parse { position, context }
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn base_header(weights: bool, detail: bool) -> Vec<u8> {
|
|
let mut bytes = [0_u8; 24].to_vec();
|
|
bytes[..MESH_HEADER.len()].copy_from_slice(MESH_HEADER.as_bytes());
|
|
bytes.push(u8::from(weights));
|
|
bytes.push(u8::from(detail));
|
|
for value in [1.0_f32, 2.0, 3.0, 0.1, 0.2, 0.3] {
|
|
bytes.extend(value.to_le_bytes());
|
|
}
|
|
bytes.push(7);
|
|
for value in [4.0_f32, 5.0, 6.0] {
|
|
bytes.extend(value.to_le_bytes());
|
|
}
|
|
bytes
|
|
}
|
|
|
|
#[test]
|
|
fn parses_full_mesh_arrays_morphs_and_remaps() {
|
|
let mut bytes = base_header(false, true);
|
|
bytes.extend(1_u16.to_le_bytes());
|
|
for value in [
|
|
1.0_f32, 2.0, 3.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.25, 0.75, 0.5, 0.5,
|
|
] {
|
|
bytes.extend(value.to_le_bytes());
|
|
}
|
|
bytes.extend(1_u16.to_le_bytes());
|
|
for index in [0_i16, 0, 0] {
|
|
bytes.extend(index.to_le_bytes());
|
|
}
|
|
let mut morph_name = [0_u8; 64];
|
|
morph_name[..5].copy_from_slice(b"smile");
|
|
bytes.extend(morph_name);
|
|
bytes.extend(1_i32.to_le_bytes());
|
|
bytes.extend(0_u32.to_le_bytes());
|
|
for value in [0.1_f32; 11] {
|
|
bytes.extend(value.to_le_bytes());
|
|
}
|
|
let mut footer = [0_u8; 64];
|
|
footer[..MORPH_FOOTER.len()].copy_from_slice(MORPH_FOOTER.as_bytes());
|
|
bytes.extend(footer);
|
|
bytes.extend(1_i32.to_le_bytes());
|
|
bytes.extend(3_i32.to_le_bytes());
|
|
bytes.extend(4_i32.to_le_bytes());
|
|
let mesh = ParsedFullMesh::parse(&bytes).unwrap();
|
|
assert_eq!(mesh.vertices.len(), 1);
|
|
assert_eq!(mesh.faces[0].indices, vec![0, 0, 0]);
|
|
assert_eq!(mesh.morphs[0].name, "smile");
|
|
assert_eq!(mesh.vertex_remaps[0].remap_destination, 4);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_bad_header_truncation_negative_counts_and_trailing_data() {
|
|
assert!(ParsedFullMesh::parse(b"bad").is_err());
|
|
let mut negative = base_header(false, false);
|
|
negative.extend(0_u16.to_le_bytes());
|
|
negative.extend(0_u16.to_le_bytes());
|
|
let mut name = [0_u8; 64];
|
|
name[0] = b'x';
|
|
negative.extend(name);
|
|
negative.extend((-1_i32).to_le_bytes());
|
|
assert!(ParsedFullMesh::parse(&negative).is_err());
|
|
let mut valid = base_header(false, false);
|
|
valid.extend(0_u16.to_le_bytes());
|
|
valid.extend(0_u16.to_le_bytes());
|
|
let mut footer = [0_u8; 64];
|
|
footer[..MORPH_FOOTER.len()].copy_from_slice(MORPH_FOOTER.as_bytes());
|
|
valid.extend(footer);
|
|
valid.push(1);
|
|
assert!(ParsedFullMesh::parse(&valid).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn trim_stops_at_first_nul() {
|
|
assert_eq!(trim_at_nul("abc\0def\0"), "abc");
|
|
}
|
|
}
|