use crate::Result;
use std::time::{Duration, Instant};
const MAX_DEPTH: usize = 64;
const MAX_CHUNKS: usize = 1_000_000;
pub(super) struct Chunk<'a> {
pub id: u16,
pub data: &'a [u8],
pub children: Option<Vec<Chunk<'a>>>,
}
impl<'a> Chunk<'a> {
pub fn child(&self, id: u16) -> Result<Option<&Chunk<'a>>> {
let mut matches = self.list()?.iter().filter(|chunk| chunk.id == id);
let first = matches.next();
if matches.next().is_some() {
return Err(format!("duplicate 3ds Max chunk {id:04x}").into());
}
Ok(first)
}
pub fn required(&self, id: u16) -> Result<&Chunk<'a>> {
self.child(id)?
.ok_or_else(|| format!("missing 3ds Max chunk {id:04x}").into())
}
pub fn list(&self) -> Result<&[Chunk<'a>]> {
self.children
.as_deref()
.ok_or_else(|| "3ds Max expected a container chunk".into())
}
}
pub(super) struct Budget {
started: Instant,
chunks: usize,
}
impl Budget {
pub fn new() -> Self {
Self {
started: Instant::now(),
chunks: 0,
}
}
pub fn check(&self) -> Result<()> {
if self.started.elapsed() > Duration::from_secs(30) {
return Err("3ds Max geometry import exceeded its 30 second limit".into());
}
Ok(())
}
pub fn parse<'a>(&mut self, bytes: &'a [u8], depth: usize) -> Result<Vec<Chunk<'a>>> {
self.check()?;
if depth > MAX_DEPTH {
return Err("3ds Max chunk nesting exceeds its limit".into());
}
let mut chunks = Vec::new();
let mut offset = 0;
while offset < bytes.len() {
self.check()?;
self.chunks += 1;
if self.chunks > MAX_CHUNKS {
return Err("3ds Max chunk count exceeds its limit".into());
}
let id = u16_at(bytes, offset)?;
let short_size = u32_at(bytes, offset + 2)?;
let (header, size, container) = if short_size == 0 {
let size = u64::from_le_bytes(slice(bytes, offset + 6, 8)?.try_into()?);
(
14,
usize::try_from(size & 0x7fff_ffff_ffff_ffff)?,
size >> 63 != 0,
)
} else {
(
6,
usize::try_from(short_size & 0x7fff_ffff)?,
short_size >> 31 != 0,
)
};
if size < header {
return Err("invalid 3ds Max chunk extent".into());
}
let data = slice(bytes, offset + header, size - header)?;
let children = if container {
Some(self.parse(data, depth + 1)?)
} else {
None
};
chunks.push(Chunk { id, data, children });
offset = offset
.checked_add(size)
.ok_or("3ds Max chunk offset overflow")?;
}
Ok(chunks)
}
}
pub(super) fn slice(data: &[u8], offset: usize, size: usize) -> Result<&[u8]> {
data.get(offset..offset.checked_add(size).ok_or("3ds Max offset overflow")?)
.ok_or_else(|| "truncated 3ds Max native data".into())
}
pub(super) fn u16_at(data: &[u8], offset: usize) -> Result<u16> {
Ok(u16::from_le_bytes(slice(data, offset, 2)?.try_into()?))
}
pub(super) fn u32_at(data: &[u8], offset: usize) -> Result<u32> {
Ok(u32::from_le_bytes(slice(data, offset, 4)?.try_into()?))
}
pub(super) fn float_at(data: &[u8], offset: usize) -> Result<f64> {
let value = f64::from(f32::from_le_bytes(slice(data, offset, 4)?.try_into()?));
if !value.is_finite() {
return Err("non-finite 3ds Max geometry or transform value".into());
}
Ok(value)
}