use std::fs::File;
use std::io::{self, BufReader, Cursor, Read, Seek, SeekFrom};
use std::path::Path;
pub use crate::fbx_node::{FbxNode, FbxProperty};
use crate::fbx_options::{FbxByteOrder, FbxReadOptions};
use crate::fbx_scene::{push_warning, FbxWarning, FbxWarningCode};
use crate::traits::ReadFromBytes;
use draco_core::mesh::Mesh;
const FBX_MAGIC: &[u8; 21] = b"Kaydara FBX Binary \0";
const FBX_MAGIC_TAIL: u8 = 0x1A;
const FBX_HEADER_LEN: u64 = 27;
const FBX_FOOTER_ID: [u8; 16] = [
0xFA, 0xBC, 0xAB, 0x09, 0xD0, 0xC8, 0xD4, 0x66, 0xB1, 0x76, 0xFB, 0x83, 0x1C, 0xF7, 0x26, 0x7E,
];
const FBX_FOOTER_MAGIC: [u8; 16] = [
0xF8, 0x5A, 0x8C, 0x6A, 0xDE, 0xF5, 0xD9, 0x7E, 0xEC, 0xE9, 0x0C, 0xE3, 0x75, 0x8F, 0x29, 0x0B,
];
const FBX_MIN_VERSION: u32 = 6000;
const FBX_MAX_VERSION: u32 = 8000;
pub struct FbxReader<R: Read + Seek = BufReader<File>> {
reader: R,
version: u32,
byte_order: FbxByteOrder,
options: FbxReadOptions,
file_len: u64,
budget: DecodeBudget,
warnings: Vec<FbxWarning>,
ascii_nodes: Option<Vec<FbxNode>>,
}
#[derive(Debug, Clone, Copy, Default)]
struct DecodeBudget {
nodes: u64,
array_raw_bytes: u64,
}
pub type FbxMemoryReader = FbxReader<Cursor<Vec<u8>>>;
impl FbxReader<BufReader<File>> {
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
Self::open_with_options(path, FbxReadOptions::default())
}
pub fn open_with_options<P: AsRef<Path>>(path: P, options: FbxReadOptions) -> io::Result<Self> {
let file = File::open(path)?;
let reader = BufReader::new(file);
Self::new_with_options(reader, options)
}
}
impl FbxReader<Cursor<Vec<u8>>> {
pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> io::Result<Self> {
Self::new(Cursor::new(bytes.into()))
}
pub fn from_bytes_with_options(
bytes: impl Into<Vec<u8>>,
options: FbxReadOptions,
) -> io::Result<Self> {
Self::new_with_options(Cursor::new(bytes.into()), options)
}
pub fn read_from_bytes(bytes: &[u8]) -> io::Result<Vec<Mesh>> {
let mut reader = Self::from_bytes(bytes.to_vec())?;
reader.read_meshes()
}
}
impl ReadFromBytes for FbxReader<Cursor<Vec<u8>>> {
fn from_bytes(bytes: &[u8]) -> io::Result<Self> {
Self::from_bytes(bytes.to_vec())
}
}
impl<R: Read + Seek> FbxReader<R> {
pub fn new(reader: R) -> io::Result<Self> {
Self::new_with_options(reader, FbxReadOptions::default())
}
pub fn new_with_options(mut reader: R, options: FbxReadOptions) -> io::Result<Self> {
let file_len = reader.seek(SeekFrom::End(0))?;
reader.rewind()?;
if file_len > options.limits.max_file_bytes {
return Err(io::Error::new(
io::ErrorKind::OutOfMemory,
format!(
"FBX: input is {file_len} bytes, over the {} byte limit",
options.limits.max_file_bytes
),
));
}
if file_len < FBX_HEADER_LEN {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("FBX: input is {file_len} bytes, shorter than the 27-byte header"),
));
}
let mut header = [0u8; FBX_HEADER_LEN as usize];
reader.read_exact(&mut header)?;
if crate::fbx_ascii::is_ascii_fbx(&header) {
reader.rewind()?;
let mut text = Vec::with_capacity(file_len as usize);
reader.read_to_end(&mut text)?;
let nodes = crate::fbx_ascii::parse_ascii_nodes(&text, &options)?;
return Ok(Self {
reader,
version: 7000,
byte_order: FbxByteOrder::Little,
options,
file_len,
budget: DecodeBudget::default(),
warnings: Vec::new(),
ascii_nodes: Some(nodes),
});
}
if &header[..21] != FBX_MAGIC {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Not a valid binary FBX file",
));
}
if header[21] != FBX_MAGIC_TAIL {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"FBX: fixed magic ends with {:#04x}, expected {FBX_MAGIC_TAIL:#04x}",
header[21]
),
));
}
let marker = header[22];
if options.strict && marker > 1 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("FBX: endian marker {marker:#04x} is neither 0 nor 1"),
));
}
let byte_order = if marker == 0 {
FbxByteOrder::Little
} else {
FbxByteOrder::Big
};
let version = byte_order.u32([header[23], header[24], header[25], header[26]]);
if !(FBX_MIN_VERSION..=FBX_MAX_VERSION).contains(&version) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"FBX: version {version} is outside the supported \
{FBX_MIN_VERSION}..={FBX_MAX_VERSION} range \
(pre-6000 files use a layout this reader does not implement)"
),
));
}
Ok(Self {
reader,
version,
ascii_nodes: None,
byte_order,
options,
file_len,
budget: DecodeBudget::default(),
warnings: Vec::new(),
})
}
fn note_deviation(
&mut self,
code: FbxWarningCode,
message: String,
subject: Option<&str>,
) -> io::Result<()> {
if self.options.strict {
return Err(io::Error::new(io::ErrorKind::InvalidData, message));
}
push_warning(&mut self.warnings, code, message, subject);
Ok(())
}
pub fn warnings(&self) -> &[FbxWarning] {
&self.warnings
}
pub(crate) fn extend_warnings(&mut self, warnings: impl IntoIterator<Item = FbxWarning>) {
self.warnings.extend(warnings);
}
fn check_available(&mut self, requested: u64, what: &str) -> io::Result<()> {
let position = self.reader.stream_position()?;
let remaining = self.file_len.saturating_sub(position);
if requested > remaining {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"FBX: {what} at offset {position} claims {requested} bytes, \
but only {remaining} remain in the file"
),
));
}
Ok(())
}
fn limit_exceeded(what: &str, value: u64, limit: u64) -> io::Error {
io::Error::new(
io::ErrorKind::OutOfMemory,
format!("FBX: {what} is {value}, over the {limit} limit"),
)
}
pub fn version(&self) -> u32 {
self.version
}
pub fn byte_order(&self) -> FbxByteOrder {
self.byte_order
}
pub fn options(&self) -> &FbxReadOptions {
&self.options
}
fn is_64bit(&self) -> bool {
self.version >= 7500
}
fn read_node(&mut self, depth: u32) -> io::Result<Option<FbxNode>> {
if depth > self.options.limits.max_depth {
return Err(Self::limit_exceeded(
"node nesting depth",
depth.into(),
self.options.limits.max_depth.into(),
));
}
let order = self.byte_order;
let (end_offset, num_properties, property_list_len, name_len) = if self.is_64bit() {
let mut buf = [0u8; 25];
self.reader.read_exact(&mut buf)?;
let end_offset = order.u64([
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
]);
let num_properties = order.u64([
buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15],
]);
let property_list_len = order.u64([
buf[16], buf[17], buf[18], buf[19], buf[20], buf[21], buf[22], buf[23],
]);
let name_len = buf[24];
(
end_offset,
num_properties as u32,
property_list_len,
name_len,
)
} else {
let mut buf = [0u8; 13];
self.reader.read_exact(&mut buf)?;
let end_offset = order.u32([buf[0], buf[1], buf[2], buf[3]]) as u64;
let num_properties = order.u32([buf[4], buf[5], buf[6], buf[7]]);
let property_list_len = order.u32([buf[8], buf[9], buf[10], buf[11]]) as u64;
let name_len = buf[12];
(end_offset, num_properties, property_list_len, name_len)
};
if end_offset == 0 && name_len == 0 {
if num_properties != 0 || property_list_len != 0 {
self.note_deviation(
FbxWarningCode::MalformedNullRecord,
format!(
"FBX: null record has non-zero property fields \
(count {num_properties}, list length {property_list_len})"
),
None,
)?;
}
return Ok(None);
}
let declared_end = (end_offset != 0).then_some(end_offset);
if declared_end.is_none() {
self.note_deviation(
FbxWarningCode::MissingNodeEndOffset,
"FBX: a named node declares no end offset; reading it without children".to_string(),
None,
)?;
}
let record_end = self.reader.stream_position()?;
if let Some(end) = declared_end {
if end < record_end {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"FBX: node record at {record_end} claims it ends at {end}, \
before its own header"
),
));
}
if end > self.file_len {
if self.options.strict {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"FBX: node record claims it ends at {end}, past the {} byte file",
self.file_len
),
));
}
return Ok(None);
}
}
self.budget.nodes += 1;
if self.budget.nodes > self.options.limits.max_nodes {
return Err(Self::limit_exceeded(
"node count",
self.budget.nodes,
self.options.limits.max_nodes,
));
}
let mut name_bytes = vec![0u8; name_len as usize];
self.reader.read_exact(&mut name_bytes)?;
let name = String::from_utf8_lossy(&name_bytes).to_string();
if u64::from(num_properties) > self.options.limits.max_properties_per_node {
return Err(Self::limit_exceeded(
"property count on one node",
num_properties.into(),
self.options.limits.max_properties_per_node,
));
}
let properties_start = self.reader.stream_position()?;
let properties_end = properties_start
.checked_add(property_list_len)
.filter(|end| *end <= declared_end.unwrap_or(self.file_len) && *end <= self.file_len)
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"FBX: node '{name}' declares a {property_list_len} byte property list \
at offset {properties_start}, which runs past its own end"
),
)
})?;
let mut properties = Vec::with_capacity(num_properties as usize);
for _ in 0..num_properties {
properties.push(self.read_property()?);
}
let properties_read_to = self.reader.stream_position()?;
if properties_read_to != properties_end {
self.note_deviation(
FbxWarningCode::PropertyListLengthMismatch,
format!(
"FBX: node '{name}' property list ended at {properties_read_to}, \
but its header declared {properties_end}"
),
Some(&name),
)?;
self.reader.seek(SeekFrom::Start(properties_end))?;
}
let mut children = Vec::new();
if let Some(end) = declared_end {
let current_pos = self.reader.stream_position()?;
if current_pos < end {
while let Some(child) = self.read_node(depth + 1)? {
children.push(child);
}
}
self.reader.seek(SeekFrom::Start(end))?;
}
Ok(Some(FbxNode {
name,
properties,
children,
}))
}
fn read_property(&mut self) -> io::Result<FbxProperty> {
let order = self.byte_order;
let mut type_code = [0u8; 1];
self.reader.read_exact(&mut type_code)?;
match type_code[0] {
b'B' | b'C' => {
let mut v = [0u8; 1];
self.reader.read_exact(&mut v)?;
Ok(FbxProperty::Bool(v[0] != 0))
}
b'Z' => {
let mut v = [0u8; 1];
self.reader.read_exact(&mut v)?;
Ok(FbxProperty::U8(v[0]))
}
b'Y' => {
let mut v = [0u8; 2];
self.reader.read_exact(&mut v)?;
Ok(FbxProperty::I16(order.i16(v)))
}
b'I' => {
let mut v = [0u8; 4];
self.reader.read_exact(&mut v)?;
Ok(FbxProperty::I32(order.i32(v)))
}
b'L' => {
let mut v = [0u8; 8];
self.reader.read_exact(&mut v)?;
Ok(FbxProperty::I64(order.i64(v)))
}
b'F' => {
let mut v = [0u8; 4];
self.reader.read_exact(&mut v)?;
Ok(FbxProperty::F32(order.f32(v)))
}
b'D' => {
let mut v = [0u8; 8];
self.reader.read_exact(&mut v)?;
Ok(FbxProperty::F64(order.f64(v)))
}
b'S' | b'R' => {
let mut len_bytes = [0u8; 4];
self.reader.read_exact(&mut len_bytes)?;
let len = u64::from(order.u32(len_bytes));
let is_string = type_code[0] == b'S';
let (limit, what) = if is_string {
(self.options.limits.max_string_bytes, "string property")
} else {
(self.options.limits.max_blob_bytes, "raw property")
};
if len > limit {
return Err(Self::limit_exceeded(what, len, limit));
}
self.check_available(len, what)?;
let mut data = vec![0u8; len as usize];
self.reader.read_exact(&mut data)?;
if is_string {
Ok(FbxProperty::String(
String::from_utf8_lossy(&data).to_string(),
))
} else {
Ok(FbxProperty::Raw(data))
}
}
b'b' | b'c' => Ok(FbxProperty::BoolArray(self.read_array_bool()?)),
b'i' => Ok(FbxProperty::I32Array(self.read_array_i32()?)),
b'l' => Ok(FbxProperty::I64Array(self.read_array_i64()?)),
b'f' => Ok(FbxProperty::F32Array(self.read_array_f32()?)),
b'd' => Ok(FbxProperty::F64Array(self.read_array_f64()?)),
_ => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Unknown property type: {}", type_code[0] as char),
)),
}
}
fn read_array_header(&mut self) -> io::Result<(u32, u32, u32)> {
let order = self.byte_order;
let mut buf = [0u8; 12];
self.reader.read_exact(&mut buf)?;
let array_len = order.u32([buf[0], buf[1], buf[2], buf[3]]);
let encoding = order.u32([buf[4], buf[5], buf[6], buf[7]]);
let compressed_len = order.u32([buf[8], buf[9], buf[10], buf[11]]);
Ok((array_len, encoding, compressed_len))
}
fn read_array_data(
&mut self,
len: u32,
encoding: u32,
compressed_len: u32,
element_size: usize,
) -> io::Result<Vec<u8>> {
let limits = self.options.limits;
let element_count = u64::from(len);
if element_count > limits.max_array_elements {
return Err(Self::limit_exceeded(
"array element count",
element_count,
limits.max_array_elements,
));
}
let uncompressed_size =
element_count
.checked_mul(element_size as u64)
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("FBX: array of {element_count} x {element_size} bytes overflows"),
)
})?;
if uncompressed_size > limits.max_array_raw_bytes {
return Err(Self::limit_exceeded(
"array size",
uncompressed_size,
limits.max_array_raw_bytes,
));
}
self.budget.array_raw_bytes = self
.budget
.array_raw_bytes
.saturating_add(uncompressed_size);
if self.budget.array_raw_bytes > limits.max_total_array_raw_bytes {
return Err(Self::limit_exceeded(
"total decoded array bytes",
self.budget.array_raw_bytes,
limits.max_total_array_raw_bytes,
));
}
let uncompressed_size = usize::try_from(uncompressed_size).map_err(|_| {
io::Error::new(
io::ErrorKind::OutOfMemory,
format!("FBX: array of {uncompressed_size} bytes does not fit in memory"),
)
})?;
let order = self.byte_order;
let mut data = self.read_array_payload(encoding, compressed_len, uncompressed_size)?;
order.swap_elements_in_place(&mut data, element_size);
Ok(data)
}
fn read_array_payload(
&mut self,
encoding: u32,
compressed_len: u32,
uncompressed_size: usize,
) -> io::Result<Vec<u8>> {
if encoding == 0 {
if compressed_len != 0 && compressed_len as usize != uncompressed_size {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"FBX: uncompressed array stores {compressed_len} bytes \
but its elements span {uncompressed_size}"
),
));
}
self.check_available(uncompressed_size as u64, "uncompressed array")?;
let mut data = vec![0u8; uncompressed_size];
self.reader.read_exact(&mut data)?;
Ok(data)
} else if encoding == 1 {
self.check_available(compressed_len.into(), "compressed array")?;
let mut compressed = vec![0u8; compressed_len as usize];
self.reader.read_exact(&mut compressed)?;
#[cfg(feature = "compression")]
{
use miniz_oxide::inflate::decompress_to_vec_zlib_with_limit;
let data = decompress_to_vec_zlib_with_limit(&compressed, uncompressed_size)
.map_err(|error| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("FBX: array decompression failed ({:?})", error.status),
)
})?;
if data.len() != uncompressed_size {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"FBX: compressed array decoded to {} bytes, expected {uncompressed_size}",
data.len()
),
));
}
Ok(data)
}
#[cfg(not(feature = "compression"))]
{
Err(io::Error::new(
io::ErrorKind::Unsupported,
"FBX array compression not supported (enable 'compression' feature)",
))
}
} else {
Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Unknown array encoding: {}", encoding),
))
}
}
fn read_array_bool(&mut self) -> io::Result<Vec<bool>> {
let (len, encoding, compressed_len) = self.read_array_header()?;
let data = self.read_array_data(len, encoding, compressed_len, 1)?;
Ok(data.into_iter().map(|b| b != 0).collect())
}
fn read_array_i32(&mut self) -> io::Result<Vec<i32>> {
let (len, encoding, compressed_len) = self.read_array_header()?;
let data = self.read_array_data(len, encoding, compressed_len, 4)?;
Ok(data
.chunks_exact(4)
.map(|c| i32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect())
}
fn read_array_i64(&mut self) -> io::Result<Vec<i64>> {
let (len, encoding, compressed_len) = self.read_array_header()?;
let data = self.read_array_data(len, encoding, compressed_len, 8)?;
Ok(data
.chunks_exact(8)
.map(|c| i64::from_le_bytes([c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]]))
.collect())
}
fn read_array_f32(&mut self) -> io::Result<Vec<f32>> {
let (len, encoding, compressed_len) = self.read_array_header()?;
let data = self.read_array_data(len, encoding, compressed_len, 4)?;
Ok(data
.chunks_exact(4)
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect())
}
fn read_array_f64(&mut self) -> io::Result<Vec<f64>> {
let (len, encoding, compressed_len) = self.read_array_header()?;
let data = self.read_array_data(len, encoding, compressed_len, 8)?;
Ok(data
.chunks_exact(8)
.map(|c| f64::from_le_bytes([c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]]))
.collect())
}
pub fn read_nodes(&mut self) -> io::Result<Vec<FbxNode>> {
if let Some(nodes) = &self.ascii_nodes {
return Ok(nodes.clone());
}
self.reader.seek(SeekFrom::Start(FBX_HEADER_LEN))?;
self.budget = DecodeBudget::default();
let mut nodes = Vec::new();
while let Some(node) = self.read_node(0)? {
nodes.push(node);
}
if self.options.strict {
self.validate_footer()?;
}
Ok(nodes)
}
fn validate_footer(&mut self) -> io::Result<()> {
let start = self.reader.stream_position()?;
let mut footer = Vec::new();
self.reader.read_to_end(&mut footer)?;
if footer.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("FBX: no footer after the root terminator at offset {start}"),
));
}
if !footer.starts_with(&FBX_FOOTER_ID) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("FBX: footer at offset {start} does not begin with the footer id"),
));
}
if !footer.ends_with(&FBX_FOOTER_MAGIC) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"FBX: file does not end with the footer magic".to_string(),
));
}
let Some((version_start, version_end)) = footer
.len()
.checked_sub(FBX_FOOTER_MAGIC.len())
.and_then(|end| end.checked_sub(120))
.and_then(|end| Some((end.checked_sub(4)?, end)))
else {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"FBX: footer is only {} bytes, too short to hold a version",
footer.len()
),
));
};
let repeated = self.byte_order.u32([
footer[version_start],
footer[version_start + 1],
footer[version_start + 2],
footer[version_start + 3],
]);
if repeated != self.version {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"FBX: footer repeats version {repeated}, but the header declared {}",
self.version
),
));
}
if footer[version_end..footer.len() - FBX_FOOTER_MAGIC.len()]
.iter()
.any(|byte| *byte != 0)
{
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"FBX: footer padding after the version is not zeroed".to_string(),
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fbx_options::FbxDecodeLimits;
use std::io::Cursor;
#[test]
fn test_fbx_magic() {
let mut data = Vec::new();
data.extend_from_slice(FBX_MAGIC);
data.extend_from_slice(&[0x1A, 0x00]); data.extend_from_slice(&7300u32.to_le_bytes()); data.extend_from_slice(&[0u8; 13]);
let cursor = Cursor::new(data);
let reader = FbxReader::new(cursor).unwrap();
assert_eq!(reader.version(), 7300);
}
#[test]
fn test_invalid_magic() {
let data = b"Not an FBX file at all";
let cursor = Cursor::new(data.to_vec());
assert!(FbxReader::new(cursor).is_err());
}
#[test]
fn memory_reader_reads_an_empty_scene() {
let mut data = Vec::new();
data.extend_from_slice(FBX_MAGIC);
data.extend_from_slice(&[0x1A, 0x00]);
data.extend_from_slice(&7300u32.to_le_bytes());
data.extend_from_slice(&[0u8; 13]);
let mut reader = FbxReader::new(Cursor::new(data)).unwrap();
let scene = reader.read_scene().unwrap();
assert!(scene.root_nodes.is_empty());
}
fn header_with(marker: u8, version: u32, body: &[u8]) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(FBX_MAGIC);
data.push(FBX_MAGIC_TAIL);
data.push(marker);
if marker == 0 {
data.extend_from_slice(&version.to_le_bytes());
} else {
data.extend_from_slice(&version.to_be_bytes());
}
data.extend_from_slice(body);
data
}
#[test]
fn endian_marker_selects_big_endian_and_its_version() {
let data = header_with(1, 7500, &[0u8; 25]);
let reader = FbxReader::new(Cursor::new(data)).unwrap();
assert_eq!(reader.byte_order(), FbxByteOrder::Big);
assert_eq!(reader.version(), 7500);
}
#[test]
fn strict_mode_rejects_an_undocumented_endian_marker() {
let data = header_with(2, 7500, &[0u8; 25]);
assert!(FbxReader::new(Cursor::new(data.clone())).is_ok());
assert!(
FbxReader::new_with_options(Cursor::new(data), FbxReadOptions::strict()).is_err(),
"strict mode should reject a marker that is neither 0 nor 1"
);
}
#[test]
fn truncated_magic_tail_is_rejected() {
let mut data = Vec::new();
data.extend_from_slice(FBX_MAGIC);
data.push(0x00); data.push(0x00);
data.extend_from_slice(&7500u32.to_le_bytes());
data.extend_from_slice(&[0u8; 25]);
assert!(FbxReader::new(Cursor::new(data)).is_err());
}
#[test]
fn pre_6000_versions_are_rejected_rather_than_read_as_empty() {
let data = header_with(0, 3000, &[0u8; 13]);
let error = FbxReader::new(Cursor::new(data))
.err()
.expect("expected rejection");
assert_eq!(error.kind(), io::ErrorKind::InvalidData);
assert!(error.to_string().contains("3000"), "{error}");
}
#[test]
fn a_backwards_end_offset_is_rejected_instead_of_looping() {
let mut body = Vec::new();
body.extend_from_slice(&1u32.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); body.push(0); let data = header_with(0, 7400, &body);
let mut reader = FbxReader::new(Cursor::new(data)).unwrap();
let error = reader.read_nodes().unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn an_oversized_array_is_refused_without_allocating() {
let mut body = Vec::new();
let node_start = 27u32;
let node_len = 13 + 1 + 1 + 12;
body.extend_from_slice(&(node_start + node_len).to_le_bytes());
body.extend_from_slice(&1u32.to_le_bytes()); body.extend_from_slice(&13u32.to_le_bytes()); body.push(1); body.push(b'X');
body.push(b'd'); body.extend_from_slice(&u32::MAX.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); let data = header_with(0, 7400, &body);
let mut reader = FbxReader::new(Cursor::new(data)).unwrap();
let error = reader.read_nodes().unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::OutOfMemory);
}
#[test]
fn reading_the_same_document_twice_succeeds() {
let data = header_with(0, 7400, &[0u8; 13]);
let mut reader = FbxReader::new(Cursor::new(data)).unwrap();
assert!(reader.read_nodes().is_ok());
assert!(reader.read_nodes().is_ok());
}
#[test]
fn a_short_footer_is_refused_instead_of_panicking() {
let mut data = Vec::new();
data.extend_from_slice(FBX_MAGIC);
data.push(FBX_MAGIC_TAIL);
data.push(0);
data.extend_from_slice(&7400u32.to_le_bytes());
data.extend_from_slice(&[0u8; 13]); data.extend_from_slice(&FBX_FOOTER_ID);
data.extend_from_slice(&FBX_FOOTER_MAGIC);
let mut reader =
FbxReader::new_with_options(Cursor::new(data), FbxReadOptions::strict()).unwrap();
let error = reader.read_nodes().unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::InvalidData);
assert!(error.to_string().contains("too short"), "{error}");
}
#[test]
fn repeated_deviations_collapse_into_one_counted_warning() {
let mut warnings = Vec::new();
for _ in 0..5 {
push_warning(
&mut warnings,
FbxWarningCode::PropertyListLengthMismatch,
"mismatch".to_string(),
Some("Geometry"),
);
}
push_warning(
&mut warnings,
FbxWarningCode::PropertyListLengthMismatch,
"mismatch".to_string(),
Some("Model"),
);
assert_eq!(warnings.len(), 2, "distinct subjects stay distinct");
assert_eq!(warnings[0].count, 5);
assert_eq!(warnings[0].to_string(), "mismatch (x5)");
assert_eq!(warnings[1].count, 1);
assert_eq!(warnings[1].to_string(), "mismatch");
}
#[test]
fn a_tolerated_deviation_is_reported_and_strict_mode_rejects_it() {
let mut data = Vec::new();
data.extend_from_slice(FBX_MAGIC);
data.push(FBX_MAGIC_TAIL);
data.push(0);
data.extend_from_slice(&7400u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&3u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes()); data.push(0);
let mut reader = FbxReader::new(Cursor::new(data.clone())).unwrap();
let scene = reader.read_scene().unwrap();
assert_eq!(scene.warnings.len(), 1);
assert_eq!(scene.warnings[0].code, FbxWarningCode::MalformedNullRecord);
assert!(!scene.warnings[0].code.is_data_loss());
assert_eq!(scene.warnings[0].code.as_str(), "malformed-null-record");
let strict = FbxReader::new_with_options(Cursor::new(data), FbxReadOptions::strict())
.unwrap()
.read_scene();
assert!(strict.is_err(), "strict mode should reject the deviation");
}
#[test]
fn a_file_over_the_size_limit_is_refused() {
let data = header_with(0, 7400, &[0u8; 13]);
let options = FbxReadOptions::default()
.with_limits(FbxDecodeLimits::default().with_max_file_bytes(8));
let error = FbxReader::new_with_options(Cursor::new(data), options)
.err()
.expect("expected rejection");
assert_eq!(error.kind(), io::ErrorKind::OutOfMemory);
}
}