Files
MetaCrate/crates/libremetaverse/src/linden_mesh.rs
Chili Palmer c9a1170a27
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
Complete first release candidate audit (#107)
2026-08-12 14:44:28 +00:00

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");
}
}