//! Original-byte MD3 parsing for the Rust rewrite. No renderer or unsafe code. #![forbid(unsafe_code)] use std::fmt; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Error(pub &'static str); impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.0) } } impl std::error::Error for Error {} type Result = std::result::Result; fn range(offset: usize, count: usize, stride: usize, end: usize, min: usize) -> Result<()> { let size = count .checked_mul(stride) .ok_or(Error("region size overflow"))?; if (count > 0 && offset < min) || offset.checked_add(size).is_none_or(|v| v > end) { return Err(Error("region outside MD3 bounds")); } Ok(()) } fn int(data: &[u8], offset: usize) -> Result { let raw = data .get(offset..offset.checked_add(4).ok_or(Error("offset overflow"))?) .ok_or(Error("truncated integer"))?; Ok(i32::from_le_bytes( raw.try_into().map_err(|_| Error("integer width"))?, )) } fn count(data: &[u8], offset: usize) -> Result { usize::try_from(int(data, offset)?).map_err(|_| Error("negative MD3 count/offset")) } fn float(data: &[u8], offset: usize) -> Result { let v = f32::from_bits(int(data, offset)? as u32); if !v.is_finite() { return Err(Error("nonfinite MD3 geometry")); } Ok(v) } fn text(data: &[u8], offset: usize, width: usize) -> Result { let raw = data .get(offset..offset.checked_add(width).ok_or(Error("offset overflow"))?) .ok_or(Error("truncated name"))?; let end = raw.iter().position(|&x| x == 0).unwrap_or(raw.len()); Ok(String::from_utf8_lossy(&raw[..end]).into_owned()) } #[derive(Debug, Clone)] pub struct Surface { pub offset: usize, pub name: String, pub frames: usize, pub shaders: usize, pub vertices: usize, pub triangles: usize, pub triangles_offset: usize, pub shaders_offset: usize, pub uv_offset: usize, pub vertices_offset: usize, pub end: usize, } #[derive(Debug, Clone, PartialEq)] pub struct Vertex { pub packed: [i16; 3], pub normal_word: u16, } impl Vertex { pub fn position(&self) -> [f32; 3] { self.packed.map(|v| f32::from(v) / 64.0) } } #[derive(Debug, Clone, PartialEq)] pub struct Tag { pub name: String, pub origin: [f32; 3], pub axis: [[f32; 3]; 3], } #[derive(Debug, Clone, PartialEq)] pub struct Frame { pub mins: [f32; 3], pub maxs: [f32; 3], pub origin: [f32; 3], pub radius: f32, pub name: String, } pub struct Md3<'a> { data: &'a [u8], pub name: String, pub frames: usize, pub tags: usize, frames_offset: usize, tags_offset: usize, surfaces: Vec, } impl<'a> Md3<'a> { pub fn parse(data: &'a [u8]) -> Result { if data.len() < 108 || &data[..4] != b"IDP3" || int(data, 4)? != 15 { return Err(Error("unsupported MD3 header")); } let frames = count(data, 76)?; let tags = count(data, 80)?; let surface_count = count(data, 84)?; let _skins = count(data, 88)?; let frames_offset = count(data, 92)?; let tags_offset = count(data, 96)?; let mut offset = count(data, 100)?; let end = count(data, 104)?; if end != data.len() { return Err(Error("MD3 declared end differs from file size")); } range(frames_offset, frames, 56, end, 108)?; range( tags_offset, frames .checked_mul(tags) .ok_or(Error("tag count overflow"))?, 112, end, 108, )?; range(offset, surface_count, 108, end, 108)?; let mut surfaces = Vec::with_capacity(surface_count); for _ in 0..surface_count { range(offset, 1, 108, end, 108)?; if &data[offset..offset + 4] != b"IDP3" { return Err(Error("invalid surface signature")); } let s = Surface { offset, name: text(data, offset + 4, 64)?, frames: count(data, offset + 72)?, shaders: count(data, offset + 76)?, vertices: count(data, offset + 80)?, triangles: count(data, offset + 84)?, triangles_offset: count(data, offset + 88)?, shaders_offset: count(data, offset + 92)?, uv_offset: count(data, offset + 96)?, vertices_offset: count(data, offset + 100)?, end: count(data, offset + 104)?, }; if s.frames != frames || s.end < 108 || s.end > end - offset { return Err(Error("invalid surface frames/end")); } range(s.triangles_offset, s.triangles, 12, s.end, 108)?; range(s.shaders_offset, s.shaders, 68, s.end, 108)?; range(s.uv_offset, s.vertices, 8, s.end, 108)?; range( s.vertices_offset, frames .checked_mul(s.vertices) .ok_or(Error("vertex count overflow"))?, 8, s.end, 108, )?; for triangle in 0..s.triangles { let p = offset + s.triangles_offset + triangle * 12; for j in 0..3 { if count(data, p + 4 * j)? >= s.vertices { return Err(Error("triangle outside vertex range")); } } } offset += s.end; surfaces.push(s); } if surface_count > 0 && offset != end { return Err(Error("surface chain differs from EOF")); } let model = Self { data, name: text(data, 8, 64)?, frames, tags, frames_offset, tags_offset, surfaces, }; for index in 0..frames { model.frame(index)?; for tag in 0..tags { model.tag(index, tag)?; } } for (index, surface) in model.surfaces.iter().enumerate() { for vertex in 0..surface.vertices { model.uv(index, vertex)?; } } Ok(model) } pub fn surfaces(&self) -> &[Surface] { &self.surfaces } fn surface(&self, index: usize) -> Result<&Surface> { self.surfaces .get(index) .ok_or(Error("surface index outside bounds")) } pub fn triangle(&self, surface: usize, index: usize) -> Result<[usize; 3]> { let s = self.surface(surface)?; range(index, 1, 1, s.triangles, 0)?; let p = s.offset + s.triangles_offset + 12 * index; Ok([ count(self.data, p)?, count(self.data, p + 4)?, count(self.data, p + 8)?, ]) } pub fn vertex(&self, surface: usize, frame: usize, index: usize) -> Result { let s = self.surface(surface)?; range(frame, 1, 1, s.frames, 0)?; range(index, 1, 1, s.vertices, 0)?; let p = s.offset + s.vertices_offset + (frame * s.vertices + index) * 8; let mut packed = [0; 3]; for (j, value) in packed.iter_mut().enumerate() { *value = i16::from_le_bytes( self.data[p + 2 * j..p + 2 * j + 2] .try_into() .map_err(|_| Error("vertex width"))?, ); } Ok(Vertex { packed, normal_word: u16::from_le_bytes( self.data[p + 6..p + 8] .try_into() .map_err(|_| Error("normal width"))?, ), }) } pub fn uv(&self, surface: usize, index: usize) -> Result<[f32; 2]> { let s = self.surface(surface)?; range(index, 1, 1, s.vertices, 0)?; let p = s.offset + s.uv_offset + index * 8; Ok([float(self.data, p)?, float(self.data, p + 4)?]) } pub fn shader(&self, surface: usize, index: usize) -> Result<(String, i32)> { let s = self.surface(surface)?; range(index, 1, 1, s.shaders, 0)?; let p = s.offset + s.shaders_offset + index * 68; Ok((text(self.data, p, 64)?, int(self.data, p + 64)?)) } pub fn frame(&self, index: usize) -> Result { range(index, 1, 1, self.frames, 0)?; let p = self.frames_offset + index * 56; Ok(Frame { mins: [ float(self.data, p)?, float(self.data, p + 4)?, float(self.data, p + 8)?, ], maxs: [ float(self.data, p + 12)?, float(self.data, p + 16)?, float(self.data, p + 20)?, ], origin: [ float(self.data, p + 24)?, float(self.data, p + 28)?, float(self.data, p + 32)?, ], radius: float(self.data, p + 36)?, name: text(self.data, p + 40, 16)?, }) } pub fn tag(&self, frame: usize, index: usize) -> Result { range(frame, 1, 1, self.frames, 0)?; range(index, 1, 1, self.tags, 0)?; let p = self.tags_offset + (frame * self.tags + index) * 112; Ok(Tag { name: text(self.data, p, 64)?, origin: [ float(self.data, p + 64)?, float(self.data, p + 68)?, float(self.data, p + 72)?, ], axis: [ [ float(self.data, p + 76)?, float(self.data, p + 80)?, float(self.data, p + 84)?, ], [ float(self.data, p + 88)?, float(self.data, p + 92)?, float(self.data, p + 96)?, ], [ float(self.data, p + 100)?, float(self.data, p + 104)?, float(self.data, p + 108)?, ], ], }) } } #[cfg(test)] mod tests { use super::*; fn put(d: &mut [u8], p: usize, v: i32) { d[p..p + 4].copy_from_slice(&v.to_le_bytes()); } fn fixture() -> Vec { let mut d = vec![0; 276]; d[..4].copy_from_slice(b"IDP3"); put(&mut d, 4, 15); for (p, v) in [ (76, 1), (84, 1), (92, 108), (96, 164), (100, 164), (104, 276), ] { put(&mut d, p, v); } d[164..168].copy_from_slice(b"IDP3"); put(&mut d, 236, 1); for (p, v) in [(252, 108), (256, 108), (260, 108), (264, 108), (268, 112)] { put(&mut d, p, v); } d } #[test] fn empty_surface_is_bounded() { let d = fixture(); let m = Md3::parse(&d).unwrap(); assert_eq!(m.surfaces().len(), 1); assert!(m.vertex(0, 0, 0).is_err()); } #[test] fn truncation_rejects() { let d = fixture(); assert!(Md3::parse(&d[..275]).is_err()); } #[test] fn signature_and_version_reject() { for p in [0, 4] { let mut d = fixture(); d[p] = 0; assert!(Md3::parse(&d).is_err()); } } #[test] fn negative_counts_reject() { let mut d = fixture(); put(&mut d, 76, -1); assert!(Md3::parse(&d).is_err()); } #[test] fn outside_regions_reject() { let mut d = fixture(); put(&mut d, 260, 10000); assert!(Md3::parse(&d).is_err()); } #[test] fn frame_accessor_bounds_reject() { let d = fixture(); let m = Md3::parse(&d).unwrap(); assert!(m.frame(1).is_err()); assert!(m.tag(0, 0).is_err()); } #[test] fn nonfinite_frame_rejects() { let mut d = fixture(); put(&mut d, 108, f32::NAN.to_bits() as i32); assert!(Md3::parse(&d).is_err()); } #[test] fn region_arithmetic_overflow_rejects() { assert!(range(usize::MAX, 1, 4, usize::MAX, 0).is_err()); assert!(range(0, usize::MAX, 4, usize::MAX, 0).is_err()); } #[test] fn triangle_index_outside_vertex_count_rejects() { let mut d = fixture(); d.resize(300, 0); for (p, v) in [ (104, 300), (244, 1), (248, 1), (252, 108), (260, 120), (264, 128), (268, 136), (280, 1), ] { put(&mut d, p, v); } assert!(Md3::parse(&d).is_err()); } #[test] fn signed_coordinate_extrema_and_normal_word_preserved() { let v = Vertex { packed: [i16::MIN, i16::MAX, -1], normal_word: 0xabcd, }; assert_eq!(v.position(), [-512.0, 511.984375, -0.015625]); assert_eq!(v.normal_word, 0xabcd); } }