Skip to main content

draco_io/
fbx_container.rs

1//! FBX binary container decoder: bytes in, a tree of [`FbxNode`](crate::fbx_container::FbxNode) out.
2//!
3//! This layer knows the container and nothing about what the document means.
4//! It validates the header, walks node records, decodes property records and
5//! their optionally deflated arrays, enforces the [`crate::FbxDecodeLimits`]
6//! budget against untrusted input, and checks the footer.
7//!
8//! Everything above it -- objects, connections, materials, skins, animation --
9//! lives in [`crate::fbx_reader`] and consumes only the node tree. That is not
10//! an aspiration: [`crate::fbx_ascii`] is a second decoder for the text
11//! container, and it feeds the same scene layer without a single change to it.
12
13use std::fs::File;
14use std::io::{self, BufReader, Cursor, Read, Seek, SeekFrom};
15use std::path::Path;
16
17/// The document tree this decoder produces, re-exported so
18/// `draco_io::fbx_container::FbxNode` keeps resolving now that the type is
19/// shared with the writer.
20pub 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
26/// FBX file magic: "Kaydara FBX Binary  \0"
27const FBX_MAGIC: &[u8; 21] = b"Kaydara FBX Binary  \0";
28
29/// Byte 21 of the fixed magic, immediately after the NUL terminator.
30const FBX_MAGIC_TAIL: u8 = 0x1A;
31
32/// `FixedMagic[22]`, `EndianMarker[1]`, `Version[4]`.
33///
34/// The layout is fixed, so this offset does not depend on byte order.
35const FBX_HEADER_LEN: u64 = 27;
36
37/// Marks the start of the binary footer, right after the root terminator.
38const FBX_FOOTER_ID: [u8; 16] = [
39    0xFA, 0xBC, 0xAB, 0x09, 0xD0, 0xC8, 0xD4, 0x66, 0xB1, 0x76, 0xFB, 0x83, 0x1C, 0xF7, 0x26, 0x7E,
40];
41
42/// Closes the file, after the repeated version and 120 bytes of padding.
43const FBX_FOOTER_MAGIC: [u8; 16] = [
44    0xF8, 0x5A, 0x8C, 0x6A, 0xDE, 0xF5, 0xD9, 0x7E, 0xEC, 0xE9, 0x0C, 0xE3, 0x75, 0x8F, 0x29, 0x0B,
45];
46
47/// Oldest supported version.
48///
49/// FBX 3000-era files reuse the same magic but lay out `Objects` differently,
50/// with pre-7000 multi-value arrays. This reader has never understood them: it
51/// used to return an empty scene, which reads as "the file had no meshes"
52/// rather than "this file is not supported". Rejecting them says so outright.
53const FBX_MIN_VERSION: u32 = 6000;
54
55/// Newest supported version. Beyond this the node record layout is unknown, and
56/// guessing a record width from a garbage version corrupts the whole parse.
57const FBX_MAX_VERSION: u32 = 8000;
58
59/// FBX reader for binary FBX files.
60pub struct FbxReader<R: Read + Seek = BufReader<File>> {
61    reader: R,
62    version: u32,
63    byte_order: FbxByteOrder,
64    options: FbxReadOptions,
65    /// Total input length, captured once so record offsets can be bounds
66    /// checked without trusting the file's own claims.
67    file_len: u64,
68    budget: DecodeBudget,
69    /// Non-fatal container-layout notices raised while reading nodes.
70    ///
71    /// Merged into [`crate::FbxScene::warnings`] by `read_scene`. Deviations
72    /// that are tolerated in lenient mode are reported here rather than
73    /// silently accepted.
74    warnings: Vec<FbxWarning>,
75    /// Nodes parsed from an ASCII document, which has no binary layout to walk.
76    ///
77    /// Present only for the ASCII container; `read_nodes` serves these instead
78    /// of decoding records, so every entry point above it is unaware of which
79    /// container it was given.
80    ascii_nodes: Option<Vec<FbxNode>>,
81}
82
83/// Running totals for the limits that apply to a whole document.
84///
85/// Reset at the start of every [`FbxReader::read_nodes`] call: that method is
86/// re-entrant (both `read_scene` and `read_meshes` call it), and carrying
87/// totals across calls would fail the second read of a file the first read
88/// accepted.
89#[derive(Debug, Clone, Copy, Default)]
90struct DecodeBudget {
91    nodes: u64,
92    array_raw_bytes: u64,
93}
94
95/// FBX reader backed by in-memory bytes.
96pub type FbxMemoryReader = FbxReader<Cursor<Vec<u8>>>;
97
98impl FbxReader<BufReader<File>> {
99    /// Open an FBX file from a path, using default read options.
100    pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
101        Self::open_with_options(path, FbxReadOptions::default())
102    }
103
104    /// Open an FBX file from a path with explicit read options.
105    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    /// Create an FBX reader from in-memory bytes, using default read options.
114    pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> io::Result<Self> {
115        Self::new(Cursor::new(bytes.into()))
116    }
117
118    /// Create an FBX reader from in-memory bytes with explicit read options.
119    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    /// Read all meshes directly from in-memory bytes.
127    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    /// Create a new FBX reader from a reader, using default read options.
141    pub fn new(reader: R) -> io::Result<Self> {
142        Self::new_with_options(reader, FbxReadOptions::default())
143    }
144
145    /// Create a new FBX reader from a reader with explicit read options.
146    ///
147    /// The header itself is parsed under `options`, so the options cannot be
148    /// changed after construction.
149    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        // `FixedMagic[22] EndianMarker[1] Version[4]`, read in one go.
170        let mut header = [0u8; FBX_HEADER_LEN as usize];
171        reader.read_exact(&mut header)?;
172
173        // The ASCII container has no header to validate, so it is parsed whole
174        // here and served from `read_nodes` like any other document. Doing it
175        // at construction rather than at each entry point is what lets
176        // `read_scene`, `read_meshes` and the `Reader` traits all accept ASCII
177        // without knowing it exists.
178        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                // The ASCII container records its version inside the node
186                // tree rather than in a header, and `check_version` has
187                // already rejected anything pre-7000.
188                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        // ufbx treats any non-zero marker as big-endian; strict mode accepts
215        // only the two documented values.
216        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    /// Records a tolerated container-layout deviation, or fails in strict mode.
254    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    /// Container-layout notices collected by the most recent read.
268    pub fn warnings(&self) -> &[FbxWarning] {
269        &self.warnings
270    }
271
272    /// Folds notices raised by the scene layer back into this reader.
273    ///
274    /// `read_meshes` collects geometry notices separately because it borrows
275    /// the reader immutably while decoding, then hands them back here so a
276    /// caller sees the same list either entry point produces.
277    pub(crate) fn extend_warnings(&mut self, warnings: impl IntoIterator<Item = FbxWarning>) {
278        self.warnings.extend(warnings);
279    }
280
281    /// Fails when `requested` bytes cannot possibly be in the file.
282    ///
283    /// Checked before every length-prefixed allocation so a hostile header
284    /// cannot make us reserve gigabytes for data that is not there.
285    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    /// Get the FBX file version.
308    pub fn version(&self) -> u32 {
309        self.version
310    }
311
312    /// Byte order selected by this file's header endian marker.
313    pub fn byte_order(&self) -> FbxByteOrder {
314        self.byte_order
315    }
316
317    /// Read options this reader was constructed with.
318    pub fn options(&self) -> &FbxReadOptions {
319        &self.options
320    }
321
322    /// Check if this is FBX 7.5+ (uses 64-bit offsets).
323    fn is_64bit(&self) -> bool {
324        self.version >= 7500
325    }
326
327    /// Read a node record.
328    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        // The terminator is `end_offset == 0 && name_len == 0`, matching ufbx.
367        // A canonical writer zeroes the whole record, so anything left set is
368        // worth reporting even though we still accept it.
369        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        // A *named* record may still declare `end_offset == 0`; Maya emits
384        // these (see `maya_zero_end_*` in the ufbx corpus). ufbx treats the
385        // node as having no children and resumes after its property list, so
386        // there is no end offset to bounds check or seek to.
387        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        // A record must end after it starts and inside the file. Without the
397        // first check a backwards `end_offset` makes the seek below rewind,
398        // and the caller re-reads the same record forever.
399        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                // Some exporters emit a bogus trailing offset. Treat it as the
421                // end of this sibling list rather than failing a good file.
422                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        // `name_len` is a u8, so it needs no limit of its own.
436        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        // Bound the count before reserving for it: `num_properties` comes
441        // straight from the file and feeds `Vec::with_capacity`.
442        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        // `property_list_len` is the authoritative size of the property block.
450        // Honouring it lets a node whose properties decoded wrongly re-sync at
451        // the child records instead of consuming them as property data.
452        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        // Read children. A node without a declared end has none: `ufbx` stops
485        // its child loop immediately for those, and following the property
486        // list with a terminator search would consume the next sibling.
487        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            // Resynchronize on the declared end; children may have stopped
496            // short of it, and trailing slack is legal.
497            self.reader.seek(SeekFrom::Start(end))?;
498        }
499
500        Ok(Some(FbxNode {
501            name,
502            properties,
503            children,
504        }))
505    }
506
507    /// Read a property.
508    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            // Single-byte scalars are never byte-swapped.
515            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            // The length is byte-swapped; the payload bytes never are.
551            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                    // Embedded textures land here through `Video.Content`.
560                    (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` is a bool array and `c` a byte array; both are one byte per
577            // element, so neither is ever byte-swapped.
578            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    /// Read array header and return (length, encoding, compressed_length).
591    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    /// Read array data, decompressing and byte-swapping as needed.
602    ///
603    /// `element_size` drives the big-endian conversion, which happens once in
604    /// bulk here so the little-endian path stays a plain `from_le_bytes`
605    /// decode in the callers.
606    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        // `usize` is 32-bit on wasm32, so this product genuinely overflows
623        // there rather than merely in theory.
624        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            // For a raw array the stored length must equal the element extent
672            // exactly; ufbx enforces the same equality.
673            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            // Deflate/zlib compressed
688            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                // Bounding the output makes a zip bomb an error instead of an
696                // out-of-memory abort; the exact-size check then rejects a
697                // stream that does not describe this array.
698                let data = decompress_to_vec_zlib_with_limit(&compressed, uncompressed_size)
699                    .map_err(|error| {
700                        // Only the status: `DecompressError`'s `Debug` carries
701                        // the whole partial output, which would put megabytes
702                        // of payload into the message.
703                        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    /// Byte-array payload (`b`/`c`); single bytes are never byte-swapped.
736    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    /// Read all top-level nodes.
779    ///
780    /// Safe to call more than once: per-document budgets restart here, so a
781    /// second read of the same file behaves exactly like the first.
782    pub fn read_nodes(&mut self) -> io::Result<Vec<FbxNode>> {
783        // An ASCII document was parsed whole at construction; hand back a copy
784        // so this stays re-entrant like the binary path.
785        if let Some(nodes) = &self.ascii_nodes {
786            return Ok(nodes.clone());
787        }
788        // Seek to start of nodes (after the fixed-size header).
789        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    /// Checks the binary footer that follows the root terminator.
803    ///
804    /// Strict mode only. `ufbx` never looks at the footer, and shipping
805    /// exporters get its padding wrong often enough that rejecting on it by
806    /// default would fail files every other reader accepts.
807    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        // Layout after the id: 4 zero bytes, alignment padding, the version
832        // repeated, 120 zero bytes, then the closing magic. A footer can begin
833        // with the id and end with the magic and still be far too short to
834        // hold the middle, so every step back from the end must be checked.
835        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]); // Unknown bytes
888        data.extend_from_slice(&7300u32.to_le_bytes()); // Version 7.3
889                                                        // Add null record to end nodes
890        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    /// Builds a header plus `body`, using the given endian marker.
918    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        // Lenient follows ufbx: any non-zero marker means big-endian.
944        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); // should be 0x1A
956        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        // These used to yield a scene with no nodes, which reads as "the file
965        // had no meshes" instead of "this layout is unsupported".
966        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        // `end_offset` points back into the header. Before the bounds check
977        // the reader seeked backwards and re-read this record forever.
978        let mut body = Vec::new();
979        body.extend_from_slice(&1u32.to_le_bytes()); // end_offset = 1
980        body.extend_from_slice(&0u32.to_le_bytes()); // num_properties
981        body.extend_from_slice(&0u32.to_le_bytes()); // property_list_len
982        body.push(0); // name_len
983        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        // A 12-byte array header claiming ~4G elements: the old reader
993        // multiplied it out and asked the allocator for 34 GB.
994        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()); // one property
999        body.extend_from_slice(&13u32.to_le_bytes()); // property_list_len
1000        body.push(1); // name_len
1001        body.push(b'X');
1002        body.push(b'd'); // f64 array
1003        body.extend_from_slice(&u32::MAX.to_le_bytes()); // element count
1004        body.extend_from_slice(&0u32.to_le_bytes()); // encoding: raw
1005        body.extend_from_slice(&0u32.to_le_bytes()); // compressed length
1006        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        // `read_nodes` is re-entrant; a per-document budget that accumulated
1016        // across calls would fail the second read of an accepted file.
1017        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        // Found by `cargo fuzz run fbx_read_scene` within minutes of the
1026        // target existing. A footer can begin with the id and end with the
1027        // magic while being far too short to hold the version and padding
1028        // between them; stepping back from the end then wrapped around and
1029        // indexed out of bounds.
1030        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]); // root terminator
1036        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        // A malformed pattern repeated across a large file must not produce
1049        // one warning per node.
1050        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        // A terminator carrying non-zero property fields: accepted with a
1076        // notice, refused outright when strict.
1077        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()); // end_offset
1083        data.extend_from_slice(&3u32.to_le_bytes()); // num_properties, should be 0
1084        data.extend_from_slice(&0u32.to_le_bytes()); // property_list_len
1085        data.push(0); // name_len
1086
1087        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}