nmarshall's picture
assets: rust md3 core
12313a3
Raw History Blame Contribute Delete
13.1 kB
//! 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<T> = std::result::Result<T, Error>;
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<i32> {
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> {
usize::try_from(int(data, offset)?).map_err(|_| Error("negative MD3 count/offset"))
}
fn float(data: &[u8], offset: usize) -> Result<f32> {
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<String> {
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<Surface>,
}
impl<'a> Md3<'a> {
pub fn parse(data: &'a [u8]) -> Result<Self> {
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<Vertex> {
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<Frame> {
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<Tag> {
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<u8> {
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);
}
}