1use std::fs::File;
14use std::io::{self, BufReader, Cursor, Read, Seek, SeekFrom};
15use std::path::Path;
16
17pub use crate::fbx_node::{FbxNode, FbxProperty};
21use crate::fbx_options::{FbxByteOrder, FbxReadOptions};
22use crate::fbx_scene::{push_warning, FbxWarning, FbxWarningCode};
23use crate::traits::ReadFromBytes;
24use draco_core::mesh::Mesh;
25
26const FBX_MAGIC: &[u8; 21] = b"Kaydara FBX Binary \0";
28
29const FBX_MAGIC_TAIL: u8 = 0x1A;
31
32const FBX_HEADER_LEN: u64 = 27;
36
37const FBX_FOOTER_ID: [u8; 16] = [
39 0xFA, 0xBC, 0xAB, 0x09, 0xD0, 0xC8, 0xD4, 0x66, 0xB1, 0x76, 0xFB, 0x83, 0x1C, 0xF7, 0x26, 0x7E,
40];
41
42const FBX_FOOTER_MAGIC: [u8; 16] = [
44 0xF8, 0x5A, 0x8C, 0x6A, 0xDE, 0xF5, 0xD9, 0x7E, 0xEC, 0xE9, 0x0C, 0xE3, 0x75, 0x8F, 0x29, 0x0B,
45];
46
47const FBX_MIN_VERSION: u32 = 6000;
54
55const FBX_MAX_VERSION: u32 = 8000;
58
59pub struct FbxReader<R: Read + Seek = BufReader<File>> {
61 reader: R,
62 version: u32,
63 byte_order: FbxByteOrder,
64 options: FbxReadOptions,
65 file_len: u64,
68 budget: DecodeBudget,
69 warnings: Vec<FbxWarning>,
75 ascii_nodes: Option<Vec<FbxNode>>,
81}
82
83#[derive(Debug, Clone, Copy, Default)]
90struct DecodeBudget {
91 nodes: u64,
92 array_raw_bytes: u64,
93}
94
95pub type FbxMemoryReader = FbxReader<Cursor<Vec<u8>>>;
97
98impl FbxReader<BufReader<File>> {
99 pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
101 Self::open_with_options(path, FbxReadOptions::default())
102 }
103
104 pub fn open_with_options<P: AsRef<Path>>(path: P, options: FbxReadOptions) -> io::Result<Self> {
106 let file = File::open(path)?;
107 let reader = BufReader::new(file);
108 Self::new_with_options(reader, options)
109 }
110}
111
112impl FbxReader<Cursor<Vec<u8>>> {
113 pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> io::Result<Self> {
115 Self::new(Cursor::new(bytes.into()))
116 }
117
118 pub fn from_bytes_with_options(
120 bytes: impl Into<Vec<u8>>,
121 options: FbxReadOptions,
122 ) -> io::Result<Self> {
123 Self::new_with_options(Cursor::new(bytes.into()), options)
124 }
125
126 pub fn read_from_bytes(bytes: &[u8]) -> io::Result<Vec<Mesh>> {
128 let mut reader = Self::from_bytes(bytes.to_vec())?;
129 reader.read_meshes()
130 }
131}
132
133impl ReadFromBytes for FbxReader<Cursor<Vec<u8>>> {
134 fn from_bytes(bytes: &[u8]) -> io::Result<Self> {
135 Self::from_bytes(bytes.to_vec())
136 }
137}
138
139impl<R: Read + Seek> FbxReader<R> {
140 pub fn new(reader: R) -> io::Result<Self> {
142 Self::new_with_options(reader, FbxReadOptions::default())
143 }
144
145 pub fn new_with_options(mut reader: R, options: FbxReadOptions) -> io::Result<Self> {
150 let file_len = reader.seek(SeekFrom::End(0))?;
151 reader.rewind()?;
152
153 if file_len > options.limits.max_file_bytes {
154 return Err(io::Error::new(
155 io::ErrorKind::OutOfMemory,
156 format!(
157 "FBX: input is {file_len} bytes, over the {} byte limit",
158 options.limits.max_file_bytes
159 ),
160 ));
161 }
162 if file_len < FBX_HEADER_LEN {
163 return Err(io::Error::new(
164 io::ErrorKind::InvalidData,
165 format!("FBX: input is {file_len} bytes, shorter than the 27-byte header"),
166 ));
167 }
168
169 let mut header = [0u8; FBX_HEADER_LEN as usize];
171 reader.read_exact(&mut header)?;
172
173 if crate::fbx_ascii::is_ascii_fbx(&header) {
179 reader.rewind()?;
180 let mut text = Vec::with_capacity(file_len as usize);
181 reader.read_to_end(&mut text)?;
182 let nodes = crate::fbx_ascii::parse_ascii_nodes(&text, &options)?;
183 return Ok(Self {
184 reader,
185 version: 7000,
189 byte_order: FbxByteOrder::Little,
190 options,
191 file_len,
192 budget: DecodeBudget::default(),
193 warnings: Vec::new(),
194 ascii_nodes: Some(nodes),
195 });
196 }
197
198 if &header[..21] != FBX_MAGIC {
199 return Err(io::Error::new(
200 io::ErrorKind::InvalidData,
201 "Not a valid binary FBX file",
202 ));
203 }
204 if header[21] != FBX_MAGIC_TAIL {
205 return Err(io::Error::new(
206 io::ErrorKind::InvalidData,
207 format!(
208 "FBX: fixed magic ends with {:#04x}, expected {FBX_MAGIC_TAIL:#04x}",
209 header[21]
210 ),
211 ));
212 }
213
214 let marker = header[22];
217 if options.strict && marker > 1 {
218 return Err(io::Error::new(
219 io::ErrorKind::InvalidData,
220 format!("FBX: endian marker {marker:#04x} is neither 0 nor 1"),
221 ));
222 }
223 let byte_order = if marker == 0 {
224 FbxByteOrder::Little
225 } else {
226 FbxByteOrder::Big
227 };
228
229 let version = byte_order.u32([header[23], header[24], header[25], header[26]]);
230 if !(FBX_MIN_VERSION..=FBX_MAX_VERSION).contains(&version) {
231 return Err(io::Error::new(
232 io::ErrorKind::InvalidData,
233 format!(
234 "FBX: version {version} is outside the supported \
235 {FBX_MIN_VERSION}..={FBX_MAX_VERSION} range \
236 (pre-6000 files use a layout this reader does not implement)"
237 ),
238 ));
239 }
240
241 Ok(Self {
242 reader,
243 version,
244 ascii_nodes: None,
245 byte_order,
246 options,
247 file_len,
248 budget: DecodeBudget::default(),
249 warnings: Vec::new(),
250 })
251 }
252
253 fn note_deviation(
255 &mut self,
256 code: FbxWarningCode,
257 message: String,
258 subject: Option<&str>,
259 ) -> io::Result<()> {
260 if self.options.strict {
261 return Err(io::Error::new(io::ErrorKind::InvalidData, message));
262 }
263 push_warning(&mut self.warnings, code, message, subject);
264 Ok(())
265 }
266
267 pub fn warnings(&self) -> &[FbxWarning] {
269 &self.warnings
270 }
271
272 pub(crate) fn extend_warnings(&mut self, warnings: impl IntoIterator<Item = FbxWarning>) {
278 self.warnings.extend(warnings);
279 }
280
281 fn check_available(&mut self, requested: u64, what: &str) -> io::Result<()> {
286 let position = self.reader.stream_position()?;
287 let remaining = self.file_len.saturating_sub(position);
288 if requested > remaining {
289 return Err(io::Error::new(
290 io::ErrorKind::InvalidData,
291 format!(
292 "FBX: {what} at offset {position} claims {requested} bytes, \
293 but only {remaining} remain in the file"
294 ),
295 ));
296 }
297 Ok(())
298 }
299
300 fn limit_exceeded(what: &str, value: u64, limit: u64) -> io::Error {
301 io::Error::new(
302 io::ErrorKind::OutOfMemory,
303 format!("FBX: {what} is {value}, over the {limit} limit"),
304 )
305 }
306
307 pub fn version(&self) -> u32 {
309 self.version
310 }
311
312 pub fn byte_order(&self) -> FbxByteOrder {
314 self.byte_order
315 }
316
317 pub fn options(&self) -> &FbxReadOptions {
319 &self.options
320 }
321
322 fn is_64bit(&self) -> bool {
324 self.version >= 7500
325 }
326
327 fn read_node(&mut self, depth: u32) -> io::Result<Option<FbxNode>> {
329 if depth > self.options.limits.max_depth {
330 return Err(Self::limit_exceeded(
331 "node nesting depth",
332 depth.into(),
333 self.options.limits.max_depth.into(),
334 ));
335 }
336 let order = self.byte_order;
337 let (end_offset, num_properties, property_list_len, name_len) = if self.is_64bit() {
338 let mut buf = [0u8; 25];
339 self.reader.read_exact(&mut buf)?;
340 let end_offset = order.u64([
341 buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
342 ]);
343 let num_properties = order.u64([
344 buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15],
345 ]);
346 let property_list_len = order.u64([
347 buf[16], buf[17], buf[18], buf[19], buf[20], buf[21], buf[22], buf[23],
348 ]);
349 let name_len = buf[24];
350 (
351 end_offset,
352 num_properties as u32,
353 property_list_len,
354 name_len,
355 )
356 } else {
357 let mut buf = [0u8; 13];
358 self.reader.read_exact(&mut buf)?;
359 let end_offset = order.u32([buf[0], buf[1], buf[2], buf[3]]) as u64;
360 let num_properties = order.u32([buf[4], buf[5], buf[6], buf[7]]);
361 let property_list_len = order.u32([buf[8], buf[9], buf[10], buf[11]]) as u64;
362 let name_len = buf[12];
363 (end_offset, num_properties, property_list_len, name_len)
364 };
365
366 if end_offset == 0 && name_len == 0 {
370 if num_properties != 0 || property_list_len != 0 {
371 self.note_deviation(
372 FbxWarningCode::MalformedNullRecord,
373 format!(
374 "FBX: null record has non-zero property fields \
375 (count {num_properties}, list length {property_list_len})"
376 ),
377 None,
378 )?;
379 }
380 return Ok(None);
381 }
382
383 let declared_end = (end_offset != 0).then_some(end_offset);
388 if declared_end.is_none() {
389 self.note_deviation(
390 FbxWarningCode::MissingNodeEndOffset,
391 "FBX: a named node declares no end offset; reading it without children".to_string(),
392 None,
393 )?;
394 }
395
396 let record_end = self.reader.stream_position()?;
400 if let Some(end) = declared_end {
401 if end < record_end {
402 return Err(io::Error::new(
403 io::ErrorKind::InvalidData,
404 format!(
405 "FBX: node record at {record_end} claims it ends at {end}, \
406 before its own header"
407 ),
408 ));
409 }
410 if end > self.file_len {
411 if self.options.strict {
412 return Err(io::Error::new(
413 io::ErrorKind::InvalidData,
414 format!(
415 "FBX: node record claims it ends at {end}, past the {} byte file",
416 self.file_len
417 ),
418 ));
419 }
420 return Ok(None);
423 }
424 }
425
426 self.budget.nodes += 1;
427 if self.budget.nodes > self.options.limits.max_nodes {
428 return Err(Self::limit_exceeded(
429 "node count",
430 self.budget.nodes,
431 self.options.limits.max_nodes,
432 ));
433 }
434
435 let mut name_bytes = vec![0u8; name_len as usize];
437 self.reader.read_exact(&mut name_bytes)?;
438 let name = String::from_utf8_lossy(&name_bytes).to_string();
439
440 if u64::from(num_properties) > self.options.limits.max_properties_per_node {
443 return Err(Self::limit_exceeded(
444 "property count on one node",
445 num_properties.into(),
446 self.options.limits.max_properties_per_node,
447 ));
448 }
449 let properties_start = self.reader.stream_position()?;
453 let properties_end = properties_start
454 .checked_add(property_list_len)
455 .filter(|end| *end <= declared_end.unwrap_or(self.file_len) && *end <= self.file_len)
456 .ok_or_else(|| {
457 io::Error::new(
458 io::ErrorKind::InvalidData,
459 format!(
460 "FBX: node '{name}' declares a {property_list_len} byte property list \
461 at offset {properties_start}, which runs past its own end"
462 ),
463 )
464 })?;
465
466 let mut properties = Vec::with_capacity(num_properties as usize);
467 for _ in 0..num_properties {
468 properties.push(self.read_property()?);
469 }
470
471 let properties_read_to = self.reader.stream_position()?;
472 if properties_read_to != properties_end {
473 self.note_deviation(
474 FbxWarningCode::PropertyListLengthMismatch,
475 format!(
476 "FBX: node '{name}' property list ended at {properties_read_to}, \
477 but its header declared {properties_end}"
478 ),
479 Some(&name),
480 )?;
481 self.reader.seek(SeekFrom::Start(properties_end))?;
482 }
483
484 let mut children = Vec::new();
488 if let Some(end) = declared_end {
489 let current_pos = self.reader.stream_position()?;
490 if current_pos < end {
491 while let Some(child) = self.read_node(depth + 1)? {
492 children.push(child);
493 }
494 }
495 self.reader.seek(SeekFrom::Start(end))?;
498 }
499
500 Ok(Some(FbxNode {
501 name,
502 properties,
503 children,
504 }))
505 }
506
507 fn read_property(&mut self) -> io::Result<FbxProperty> {
509 let order = self.byte_order;
510 let mut type_code = [0u8; 1];
511 self.reader.read_exact(&mut type_code)?;
512
513 match type_code[0] {
514 b'B' | b'C' => {
516 let mut v = [0u8; 1];
517 self.reader.read_exact(&mut v)?;
518 Ok(FbxProperty::Bool(v[0] != 0))
519 }
520 b'Z' => {
521 let mut v = [0u8; 1];
522 self.reader.read_exact(&mut v)?;
523 Ok(FbxProperty::U8(v[0]))
524 }
525 b'Y' => {
526 let mut v = [0u8; 2];
527 self.reader.read_exact(&mut v)?;
528 Ok(FbxProperty::I16(order.i16(v)))
529 }
530 b'I' => {
531 let mut v = [0u8; 4];
532 self.reader.read_exact(&mut v)?;
533 Ok(FbxProperty::I32(order.i32(v)))
534 }
535 b'L' => {
536 let mut v = [0u8; 8];
537 self.reader.read_exact(&mut v)?;
538 Ok(FbxProperty::I64(order.i64(v)))
539 }
540 b'F' => {
541 let mut v = [0u8; 4];
542 self.reader.read_exact(&mut v)?;
543 Ok(FbxProperty::F32(order.f32(v)))
544 }
545 b'D' => {
546 let mut v = [0u8; 8];
547 self.reader.read_exact(&mut v)?;
548 Ok(FbxProperty::F64(order.f64(v)))
549 }
550 b'S' | b'R' => {
552 let mut len_bytes = [0u8; 4];
553 self.reader.read_exact(&mut len_bytes)?;
554 let len = u64::from(order.u32(len_bytes));
555 let is_string = type_code[0] == b'S';
556 let (limit, what) = if is_string {
557 (self.options.limits.max_string_bytes, "string property")
558 } else {
559 (self.options.limits.max_blob_bytes, "raw property")
561 };
562 if len > limit {
563 return Err(Self::limit_exceeded(what, len, limit));
564 }
565 self.check_available(len, what)?;
566 let mut data = vec![0u8; len as usize];
567 self.reader.read_exact(&mut data)?;
568 if is_string {
569 Ok(FbxProperty::String(
570 String::from_utf8_lossy(&data).to_string(),
571 ))
572 } else {
573 Ok(FbxProperty::Raw(data))
574 }
575 }
576 b'b' | b'c' => Ok(FbxProperty::BoolArray(self.read_array_bool()?)),
579 b'i' => Ok(FbxProperty::I32Array(self.read_array_i32()?)),
580 b'l' => Ok(FbxProperty::I64Array(self.read_array_i64()?)),
581 b'f' => Ok(FbxProperty::F32Array(self.read_array_f32()?)),
582 b'd' => Ok(FbxProperty::F64Array(self.read_array_f64()?)),
583 _ => Err(io::Error::new(
584 io::ErrorKind::InvalidData,
585 format!("Unknown property type: {}", type_code[0] as char),
586 )),
587 }
588 }
589
590 fn read_array_header(&mut self) -> io::Result<(u32, u32, u32)> {
592 let order = self.byte_order;
593 let mut buf = [0u8; 12];
594 self.reader.read_exact(&mut buf)?;
595 let array_len = order.u32([buf[0], buf[1], buf[2], buf[3]]);
596 let encoding = order.u32([buf[4], buf[5], buf[6], buf[7]]);
597 let compressed_len = order.u32([buf[8], buf[9], buf[10], buf[11]]);
598 Ok((array_len, encoding, compressed_len))
599 }
600
601 fn read_array_data(
607 &mut self,
608 len: u32,
609 encoding: u32,
610 compressed_len: u32,
611 element_size: usize,
612 ) -> io::Result<Vec<u8>> {
613 let limits = self.options.limits;
614 let element_count = u64::from(len);
615 if element_count > limits.max_array_elements {
616 return Err(Self::limit_exceeded(
617 "array element count",
618 element_count,
619 limits.max_array_elements,
620 ));
621 }
622 let uncompressed_size =
625 element_count
626 .checked_mul(element_size as u64)
627 .ok_or_else(|| {
628 io::Error::new(
629 io::ErrorKind::InvalidData,
630 format!("FBX: array of {element_count} x {element_size} bytes overflows"),
631 )
632 })?;
633 if uncompressed_size > limits.max_array_raw_bytes {
634 return Err(Self::limit_exceeded(
635 "array size",
636 uncompressed_size,
637 limits.max_array_raw_bytes,
638 ));
639 }
640 self.budget.array_raw_bytes = self
641 .budget
642 .array_raw_bytes
643 .saturating_add(uncompressed_size);
644 if self.budget.array_raw_bytes > limits.max_total_array_raw_bytes {
645 return Err(Self::limit_exceeded(
646 "total decoded array bytes",
647 self.budget.array_raw_bytes,
648 limits.max_total_array_raw_bytes,
649 ));
650 }
651 let uncompressed_size = usize::try_from(uncompressed_size).map_err(|_| {
652 io::Error::new(
653 io::ErrorKind::OutOfMemory,
654 format!("FBX: array of {uncompressed_size} bytes does not fit in memory"),
655 )
656 })?;
657
658 let order = self.byte_order;
659 let mut data = self.read_array_payload(encoding, compressed_len, uncompressed_size)?;
660 order.swap_elements_in_place(&mut data, element_size);
661 Ok(data)
662 }
663
664 fn read_array_payload(
665 &mut self,
666 encoding: u32,
667 compressed_len: u32,
668 uncompressed_size: usize,
669 ) -> io::Result<Vec<u8>> {
670 if encoding == 0 {
671 if compressed_len != 0 && compressed_len as usize != uncompressed_size {
674 return Err(io::Error::new(
675 io::ErrorKind::InvalidData,
676 format!(
677 "FBX: uncompressed array stores {compressed_len} bytes \
678 but its elements span {uncompressed_size}"
679 ),
680 ));
681 }
682 self.check_available(uncompressed_size as u64, "uncompressed array")?;
683 let mut data = vec![0u8; uncompressed_size];
684 self.reader.read_exact(&mut data)?;
685 Ok(data)
686 } else if encoding == 1 {
687 self.check_available(compressed_len.into(), "compressed array")?;
689 let mut compressed = vec![0u8; compressed_len as usize];
690 self.reader.read_exact(&mut compressed)?;
691
692 #[cfg(feature = "compression")]
693 {
694 use miniz_oxide::inflate::decompress_to_vec_zlib_with_limit;
695 let data = decompress_to_vec_zlib_with_limit(&compressed, uncompressed_size)
699 .map_err(|error| {
700 io::Error::new(
704 io::ErrorKind::InvalidData,
705 format!("FBX: array decompression failed ({:?})", error.status),
706 )
707 })?;
708 if data.len() != uncompressed_size {
709 return Err(io::Error::new(
710 io::ErrorKind::InvalidData,
711 format!(
712 "FBX: compressed array decoded to {} bytes, expected {uncompressed_size}",
713 data.len()
714 ),
715 ));
716 }
717 Ok(data)
718 }
719
720 #[cfg(not(feature = "compression"))]
721 {
722 Err(io::Error::new(
723 io::ErrorKind::Unsupported,
724 "FBX array compression not supported (enable 'compression' feature)",
725 ))
726 }
727 } else {
728 Err(io::Error::new(
729 io::ErrorKind::InvalidData,
730 format!("Unknown array encoding: {}", encoding),
731 ))
732 }
733 }
734
735 fn read_array_bool(&mut self) -> io::Result<Vec<bool>> {
737 let (len, encoding, compressed_len) = self.read_array_header()?;
738 let data = self.read_array_data(len, encoding, compressed_len, 1)?;
739 Ok(data.into_iter().map(|b| b != 0).collect())
740 }
741
742 fn read_array_i32(&mut self) -> io::Result<Vec<i32>> {
743 let (len, encoding, compressed_len) = self.read_array_header()?;
744 let data = self.read_array_data(len, encoding, compressed_len, 4)?;
745 Ok(data
746 .chunks_exact(4)
747 .map(|c| i32::from_le_bytes([c[0], c[1], c[2], c[3]]))
748 .collect())
749 }
750
751 fn read_array_i64(&mut self) -> io::Result<Vec<i64>> {
752 let (len, encoding, compressed_len) = self.read_array_header()?;
753 let data = self.read_array_data(len, encoding, compressed_len, 8)?;
754 Ok(data
755 .chunks_exact(8)
756 .map(|c| i64::from_le_bytes([c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]]))
757 .collect())
758 }
759
760 fn read_array_f32(&mut self) -> io::Result<Vec<f32>> {
761 let (len, encoding, compressed_len) = self.read_array_header()?;
762 let data = self.read_array_data(len, encoding, compressed_len, 4)?;
763 Ok(data
764 .chunks_exact(4)
765 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
766 .collect())
767 }
768
769 fn read_array_f64(&mut self) -> io::Result<Vec<f64>> {
770 let (len, encoding, compressed_len) = self.read_array_header()?;
771 let data = self.read_array_data(len, encoding, compressed_len, 8)?;
772 Ok(data
773 .chunks_exact(8)
774 .map(|c| f64::from_le_bytes([c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]]))
775 .collect())
776 }
777
778 pub fn read_nodes(&mut self) -> io::Result<Vec<FbxNode>> {
783 if let Some(nodes) = &self.ascii_nodes {
786 return Ok(nodes.clone());
787 }
788 self.reader.seek(SeekFrom::Start(FBX_HEADER_LEN))?;
790 self.budget = DecodeBudget::default();
791
792 let mut nodes = Vec::new();
793 while let Some(node) = self.read_node(0)? {
794 nodes.push(node);
795 }
796 if self.options.strict {
797 self.validate_footer()?;
798 }
799 Ok(nodes)
800 }
801
802 fn validate_footer(&mut self) -> io::Result<()> {
808 let start = self.reader.stream_position()?;
809 let mut footer = Vec::new();
810 self.reader.read_to_end(&mut footer)?;
811
812 if footer.is_empty() {
813 return Err(io::Error::new(
814 io::ErrorKind::InvalidData,
815 format!("FBX: no footer after the root terminator at offset {start}"),
816 ));
817 }
818 if !footer.starts_with(&FBX_FOOTER_ID) {
819 return Err(io::Error::new(
820 io::ErrorKind::InvalidData,
821 format!("FBX: footer at offset {start} does not begin with the footer id"),
822 ));
823 }
824 if !footer.ends_with(&FBX_FOOTER_MAGIC) {
825 return Err(io::Error::new(
826 io::ErrorKind::InvalidData,
827 "FBX: file does not end with the footer magic".to_string(),
828 ));
829 }
830
831 let Some((version_start, version_end)) = footer
836 .len()
837 .checked_sub(FBX_FOOTER_MAGIC.len())
838 .and_then(|end| end.checked_sub(120))
839 .and_then(|end| Some((end.checked_sub(4)?, end)))
840 else {
841 return Err(io::Error::new(
842 io::ErrorKind::InvalidData,
843 format!(
844 "FBX: footer is only {} bytes, too short to hold a version",
845 footer.len()
846 ),
847 ));
848 };
849 let repeated = self.byte_order.u32([
850 footer[version_start],
851 footer[version_start + 1],
852 footer[version_start + 2],
853 footer[version_start + 3],
854 ]);
855 if repeated != self.version {
856 return Err(io::Error::new(
857 io::ErrorKind::InvalidData,
858 format!(
859 "FBX: footer repeats version {repeated}, but the header declared {}",
860 self.version
861 ),
862 ));
863 }
864 if footer[version_end..footer.len() - FBX_FOOTER_MAGIC.len()]
865 .iter()
866 .any(|byte| *byte != 0)
867 {
868 return Err(io::Error::new(
869 io::ErrorKind::InvalidData,
870 "FBX: footer padding after the version is not zeroed".to_string(),
871 ));
872 }
873 Ok(())
874 }
875}
876
877#[cfg(test)]
878mod tests {
879 use super::*;
880 use crate::fbx_options::FbxDecodeLimits;
881 use std::io::Cursor;
882
883 #[test]
884 fn test_fbx_magic() {
885 let mut data = Vec::new();
886 data.extend_from_slice(FBX_MAGIC);
887 data.extend_from_slice(&[0x1A, 0x00]); data.extend_from_slice(&7300u32.to_le_bytes()); data.extend_from_slice(&[0u8; 13]);
891
892 let cursor = Cursor::new(data);
893 let reader = FbxReader::new(cursor).unwrap();
894 assert_eq!(reader.version(), 7300);
895 }
896
897 #[test]
898 fn test_invalid_magic() {
899 let data = b"Not an FBX file at all";
900 let cursor = Cursor::new(data.to_vec());
901 assert!(FbxReader::new(cursor).is_err());
902 }
903
904 #[test]
905 fn memory_reader_reads_an_empty_scene() {
906 let mut data = Vec::new();
907 data.extend_from_slice(FBX_MAGIC);
908 data.extend_from_slice(&[0x1A, 0x00]);
909 data.extend_from_slice(&7300u32.to_le_bytes());
910 data.extend_from_slice(&[0u8; 13]);
911
912 let mut reader = FbxReader::new(Cursor::new(data)).unwrap();
913 let scene = reader.read_scene().unwrap();
914 assert!(scene.root_nodes.is_empty());
915 }
916
917 fn header_with(marker: u8, version: u32, body: &[u8]) -> Vec<u8> {
919 let mut data = Vec::new();
920 data.extend_from_slice(FBX_MAGIC);
921 data.push(FBX_MAGIC_TAIL);
922 data.push(marker);
923 if marker == 0 {
924 data.extend_from_slice(&version.to_le_bytes());
925 } else {
926 data.extend_from_slice(&version.to_be_bytes());
927 }
928 data.extend_from_slice(body);
929 data
930 }
931
932 #[test]
933 fn endian_marker_selects_big_endian_and_its_version() {
934 let data = header_with(1, 7500, &[0u8; 25]);
935 let reader = FbxReader::new(Cursor::new(data)).unwrap();
936 assert_eq!(reader.byte_order(), FbxByteOrder::Big);
937 assert_eq!(reader.version(), 7500);
938 }
939
940 #[test]
941 fn strict_mode_rejects_an_undocumented_endian_marker() {
942 let data = header_with(2, 7500, &[0u8; 25]);
943 assert!(FbxReader::new(Cursor::new(data.clone())).is_ok());
945 assert!(
946 FbxReader::new_with_options(Cursor::new(data), FbxReadOptions::strict()).is_err(),
947 "strict mode should reject a marker that is neither 0 nor 1"
948 );
949 }
950
951 #[test]
952 fn truncated_magic_tail_is_rejected() {
953 let mut data = Vec::new();
954 data.extend_from_slice(FBX_MAGIC);
955 data.push(0x00); data.push(0x00);
957 data.extend_from_slice(&7500u32.to_le_bytes());
958 data.extend_from_slice(&[0u8; 25]);
959 assert!(FbxReader::new(Cursor::new(data)).is_err());
960 }
961
962 #[test]
963 fn pre_6000_versions_are_rejected_rather_than_read_as_empty() {
964 let data = header_with(0, 3000, &[0u8; 13]);
967 let error = FbxReader::new(Cursor::new(data))
968 .err()
969 .expect("expected rejection");
970 assert_eq!(error.kind(), io::ErrorKind::InvalidData);
971 assert!(error.to_string().contains("3000"), "{error}");
972 }
973
974 #[test]
975 fn a_backwards_end_offset_is_rejected_instead_of_looping() {
976 let mut body = Vec::new();
979 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);
984
985 let mut reader = FbxReader::new(Cursor::new(data)).unwrap();
986 let error = reader.read_nodes().unwrap_err();
987 assert_eq!(error.kind(), io::ErrorKind::InvalidData);
988 }
989
990 #[test]
991 fn an_oversized_array_is_refused_without_allocating() {
992 let mut body = Vec::new();
995 let node_start = 27u32;
996 let node_len = 13 + 1 + 1 + 12;
997 body.extend_from_slice(&(node_start + node_len).to_le_bytes());
998 body.extend_from_slice(&1u32.to_le_bytes()); body.extend_from_slice(&13u32.to_le_bytes()); body.push(1); body.push(b'X');
1002 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);
1007
1008 let mut reader = FbxReader::new(Cursor::new(data)).unwrap();
1009 let error = reader.read_nodes().unwrap_err();
1010 assert_eq!(error.kind(), io::ErrorKind::OutOfMemory);
1011 }
1012
1013 #[test]
1014 fn reading_the_same_document_twice_succeeds() {
1015 let data = header_with(0, 7400, &[0u8; 13]);
1018 let mut reader = FbxReader::new(Cursor::new(data)).unwrap();
1019 assert!(reader.read_nodes().is_ok());
1020 assert!(reader.read_nodes().is_ok());
1021 }
1022
1023 #[test]
1024 fn a_short_footer_is_refused_instead_of_panicking() {
1025 let mut data = Vec::new();
1031 data.extend_from_slice(FBX_MAGIC);
1032 data.push(FBX_MAGIC_TAIL);
1033 data.push(0);
1034 data.extend_from_slice(&7400u32.to_le_bytes());
1035 data.extend_from_slice(&[0u8; 13]); data.extend_from_slice(&FBX_FOOTER_ID);
1037 data.extend_from_slice(&FBX_FOOTER_MAGIC);
1038
1039 let mut reader =
1040 FbxReader::new_with_options(Cursor::new(data), FbxReadOptions::strict()).unwrap();
1041 let error = reader.read_nodes().unwrap_err();
1042 assert_eq!(error.kind(), io::ErrorKind::InvalidData);
1043 assert!(error.to_string().contains("too short"), "{error}");
1044 }
1045
1046 #[test]
1047 fn repeated_deviations_collapse_into_one_counted_warning() {
1048 let mut warnings = Vec::new();
1051 for _ in 0..5 {
1052 push_warning(
1053 &mut warnings,
1054 FbxWarningCode::PropertyListLengthMismatch,
1055 "mismatch".to_string(),
1056 Some("Geometry"),
1057 );
1058 }
1059 push_warning(
1060 &mut warnings,
1061 FbxWarningCode::PropertyListLengthMismatch,
1062 "mismatch".to_string(),
1063 Some("Model"),
1064 );
1065
1066 assert_eq!(warnings.len(), 2, "distinct subjects stay distinct");
1067 assert_eq!(warnings[0].count, 5);
1068 assert_eq!(warnings[0].to_string(), "mismatch (x5)");
1069 assert_eq!(warnings[1].count, 1);
1070 assert_eq!(warnings[1].to_string(), "mismatch");
1071 }
1072
1073 #[test]
1074 fn a_tolerated_deviation_is_reported_and_strict_mode_rejects_it() {
1075 let mut data = Vec::new();
1078 data.extend_from_slice(FBX_MAGIC);
1079 data.push(FBX_MAGIC_TAIL);
1080 data.push(0);
1081 data.extend_from_slice(&7400u32.to_le_bytes());
1082 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();
1088 let scene = reader.read_scene().unwrap();
1089 assert_eq!(scene.warnings.len(), 1);
1090 assert_eq!(scene.warnings[0].code, FbxWarningCode::MalformedNullRecord);
1091 assert!(!scene.warnings[0].code.is_data_loss());
1092 assert_eq!(scene.warnings[0].code.as_str(), "malformed-null-record");
1093
1094 let strict = FbxReader::new_with_options(Cursor::new(data), FbxReadOptions::strict())
1095 .unwrap()
1096 .read_scene();
1097 assert!(strict.is_err(), "strict mode should reject the deviation");
1098 }
1099
1100 #[test]
1101 fn a_file_over_the_size_limit_is_refused() {
1102 let data = header_with(0, 7400, &[0u8; 13]);
1103 let options = FbxReadOptions::default()
1104 .with_limits(FbxDecodeLimits::default().with_max_file_bytes(8));
1105 let error = FbxReader::new_with_options(Cursor::new(data), options)
1106 .err()
1107 .expect("expected rejection");
1108 assert_eq!(error.kind(), io::ErrorKind::OutOfMemory);
1109 }
1110}