Skip to main content

draco_io/
gltf_reader.rs

1//! glTF/GLB reader with full scene graph and mesh decoding support.
2//!
3//! This module provides support for reading glTF 2.0 files. It supports:
4//! - Draco-compressed primitives via `KHR_draco_mesh_compression`
5//! - Standard (non-Draco) primitives with accessor-based geometry
6//! - Full scene graph parsing (scenes, nodes, transforms, hierarchy)
7//! - Both `.gltf` (JSON + separate `.bin`) and `.glb` (binary container) formats
8//!
9//! # Example
10//!
11//! ```no_run
12//! use draco_io::gltf_reader::GltfReader;
13//! use draco_io::SceneReader;
14//!
15//! let mut reader = GltfReader::open("model.glb")?;
16//!
17//! // Read all meshes (Draco and non-Draco)
18//! let meshes = reader.decode_all_meshes()?;
19//!
20//! // Or read the full scene graph with transforms
21//! let scene = reader.read_scene()?;
22//! for node in &scene.root_nodes {
23//!     println!(
24//!         "Node: {:?}, mesh instances: {}",
25//!         node.name,
26//!         node.mesh_instances.len()
27//!     );
28//! }
29//! # Ok::<(), draco_io::GltfError>(())
30//! ```
31
32use std::collections::{BTreeMap, BTreeSet};
33use std::fs;
34use std::io;
35use std::path::Path;
36
37use draco_core::decoder_buffer::DecoderBuffer;
38use draco_core::draco_types::DataType;
39use draco_core::geometry_attribute::PointAttribute;
40use draco_core::mesh::Mesh;
41use draco_core::mesh_decoder::MeshDecoder;
42#[cfg(feature = "point_cloud_decode")]
43use draco_core::point_cloud::PointCloud;
44#[cfg(feature = "point_cloud_decode")]
45use draco_core::point_cloud_decoder::PointCloudDecoder;
46use serde::{Deserialize, Deserializer};
47
48use crate::gltf_container::{
49    parse_gltf_container, resolve_gltf_buffers, resolve_resource_uri, ExternalFilePolicy,
50    FileResourceResolver, GltfBufferReference, GltfContainerFormat, ResourceLimits,
51    ResourceResolver,
52};
53use crate::traits::ReadFromBytes;
54
55// The error type, the reader-agnostic geometry decoder, and the glTF numeric
56// constants live in `gltf_geometry` so they are available with only the writer
57// feature (the compressor reuses them without linking this reader).
58use crate::gltf_geometry::{
59    add_named_attribute, component_type_for_data_type, decode_geometry,
60    gltf_type_for_num_components, supported_semantic_spec, validate_semantic_accessor,
61    AccessorSource, DecodedAccessor, GltfError, Result, GLTF_COMPONENT_BYTE, GLTF_COMPONENT_FLOAT,
62    GLTF_COMPONENT_SHORT, GLTF_COMPONENT_UNSIGNED_BYTE, GLTF_COMPONENT_UNSIGNED_INT,
63    GLTF_COMPONENT_UNSIGNED_SHORT, GLTF_MODE_TRIANGLES,
64};
65use crate::gltf_khr_draco::{
66    validate_khr_draco_contract, validate_khr_draco_document, KhrDracoExtensionContract,
67    KhrDracoPrimitiveContract, KHR_DRACO_MESH_COMPRESSION,
68};
69
70// ============================================================================
71// glTF JSON Schema (full scene graph support)
72// ============================================================================
73
74#[allow(dead_code)]
75#[derive(Debug, Deserialize)]
76#[serde(rename_all = "camelCase")]
77struct GltfRoot {
78    asset: Asset,
79    #[serde(default)]
80    accessors: Vec<Accessor>,
81    #[serde(default)]
82    buffer_views: Vec<BufferView>,
83    #[serde(default)]
84    buffers: Vec<Buffer>,
85    #[serde(default)]
86    images: Vec<Image>,
87    #[serde(default)]
88    meshes: Vec<GltfMesh>,
89    #[serde(default)]
90    nodes: Vec<GltfNode>,
91    #[serde(default)]
92    scenes: Vec<GltfScene>,
93    #[serde(default)]
94    skins: Vec<Skin>,
95    #[serde(default)]
96    animations: Vec<Animation>,
97    /// Default scene index (if present).
98    #[serde(default, deserialize_with = "deserialize_present_option")]
99    scene: Option<usize>,
100    #[serde(default)]
101    extensions_used: Vec<String>,
102    #[serde(default)]
103    extensions_required: Vec<String>,
104}
105
106#[allow(dead_code)]
107#[derive(Debug, Deserialize)]
108#[serde(rename_all = "camelCase")]
109struct Asset {
110    version: String,
111    min_version: Option<String>,
112}
113
114fn deserialize_present_option<'de, D, T>(
115    deserializer: D,
116) -> std::result::Result<Option<T>, D::Error>
117where
118    D: Deserializer<'de>,
119    T: Deserialize<'de>,
120{
121    T::deserialize(deserializer).map(Some)
122}
123
124#[allow(dead_code)]
125#[derive(Debug, Deserialize)]
126#[serde(rename_all = "camelCase")]
127struct Skin {
128    #[serde(default, deserialize_with = "deserialize_present_option")]
129    inverse_bind_matrices: Option<usize>,
130    #[serde(default, deserialize_with = "deserialize_present_option")]
131    skeleton: Option<usize>,
132    joints: Vec<usize>,
133}
134
135#[allow(dead_code)]
136#[derive(Debug, Deserialize)]
137struct Animation {
138    channels: Vec<AnimationChannel>,
139    samplers: Vec<AnimationSampler>,
140}
141
142#[allow(dead_code)]
143#[derive(Debug, Deserialize)]
144struct AnimationChannel {
145    sampler: usize,
146    target: AnimationTarget,
147}
148
149#[allow(dead_code)]
150#[derive(Debug, Deserialize)]
151struct AnimationTarget {
152    #[serde(default, deserialize_with = "deserialize_present_option")]
153    node: Option<usize>,
154    path: String,
155}
156
157#[allow(dead_code)]
158#[derive(Debug, Deserialize)]
159struct AnimationSampler {
160    input: usize,
161    output: usize,
162    #[serde(default)]
163    interpolation: Option<String>,
164}
165
166/// A glTF scene containing root node indices.
167#[allow(dead_code)]
168#[derive(Debug, Deserialize)]
169#[serde(rename_all = "camelCase")]
170struct GltfScene {
171    name: Option<String>,
172    #[serde(default)]
173    nodes: Vec<usize>,
174}
175
176/// A glTF node in the scene graph.
177#[allow(dead_code)]
178#[derive(Debug, Deserialize)]
179#[serde(rename_all = "camelCase")]
180struct GltfNode {
181    name: Option<String>,
182    /// Index into meshes array.
183    #[serde(default, deserialize_with = "deserialize_present_option")]
184    mesh: Option<usize>,
185    /// Child node indices.
186    #[serde(default)]
187    children: Vec<usize>,
188    /// 4x4 transformation matrix (column-major).
189    matrix: Option<[f32; 16]>,
190    /// Translation (T in TRS).
191    translation: Option<[f32; 3]>,
192    /// Rotation quaternion [x, y, z, w] (R in TRS).
193    rotation: Option<[f32; 4]>,
194    /// Scale (S in TRS).
195    scale: Option<[f32; 3]>,
196    #[serde(default, deserialize_with = "deserialize_present_option")]
197    skin: Option<usize>,
198}
199
200#[allow(dead_code)]
201#[derive(Debug, Deserialize)]
202#[serde(rename_all = "camelCase")]
203struct Accessor {
204    #[serde(default, deserialize_with = "deserialize_present_option")]
205    buffer_view: Option<usize>,
206    #[serde(default, deserialize_with = "deserialize_present_option")]
207    byte_offset: Option<usize>,
208    component_type: u32,
209    #[serde(default)]
210    normalized: bool,
211    count: usize,
212    #[serde(rename = "type")]
213    accessor_type: String,
214    #[serde(default)]
215    min: Vec<f64>,
216    #[serde(default)]
217    max: Vec<f64>,
218    #[serde(default, deserialize_with = "deserialize_present_option")]
219    sparse: Option<SparseAccessor>,
220}
221
222#[allow(dead_code)]
223#[derive(Debug, Deserialize)]
224#[serde(rename_all = "camelCase")]
225struct SparseAccessor {
226    count: usize,
227    indices: SparseIndices,
228    values: SparseValues,
229}
230
231#[allow(dead_code)]
232#[derive(Debug, Deserialize)]
233#[serde(rename_all = "camelCase")]
234struct SparseIndices {
235    buffer_view: usize,
236    #[serde(default, deserialize_with = "deserialize_present_option")]
237    byte_offset: Option<usize>,
238    component_type: u32,
239}
240
241#[allow(dead_code)]
242#[derive(Debug, Deserialize)]
243#[serde(rename_all = "camelCase")]
244struct SparseValues {
245    buffer_view: usize,
246    #[serde(default, deserialize_with = "deserialize_present_option")]
247    byte_offset: Option<usize>,
248}
249
250#[allow(dead_code)]
251#[derive(Debug, Deserialize)]
252#[serde(rename_all = "camelCase")]
253struct BufferView {
254    buffer: usize,
255    byte_offset: Option<usize>,
256    byte_length: usize,
257    byte_stride: Option<usize>,
258    target: Option<u32>,
259}
260
261#[allow(dead_code)]
262#[derive(Debug, Deserialize)]
263#[serde(rename_all = "camelCase")]
264struct Buffer {
265    byte_length: usize,
266    #[serde(default, deserialize_with = "deserialize_present_option")]
267    uri: Option<String>,
268}
269
270#[allow(dead_code)]
271#[derive(Debug, Deserialize)]
272#[serde(rename_all = "camelCase")]
273struct Image {
274    #[serde(default, deserialize_with = "deserialize_present_option")]
275    uri: Option<String>,
276    #[serde(default, deserialize_with = "deserialize_present_option")]
277    buffer_view: Option<usize>,
278    #[serde(default, deserialize_with = "deserialize_present_option")]
279    mime_type: Option<String>,
280}
281
282#[derive(Debug, Deserialize)]
283#[serde(rename_all = "camelCase")]
284struct GltfMesh {
285    name: Option<String>,
286    primitives: Vec<Primitive>,
287}
288
289#[allow(dead_code)]
290#[derive(Debug, Deserialize)]
291#[serde(rename_all = "camelCase")]
292struct Primitive {
293    #[serde(default)]
294    attributes: BTreeMap<String, usize>,
295    #[serde(default, deserialize_with = "deserialize_present_option")]
296    indices: Option<usize>,
297    #[serde(default, deserialize_with = "deserialize_present_option")]
298    mode: Option<u32>,
299    #[serde(default, deserialize_with = "deserialize_present_option")]
300    material: Option<usize>,
301    #[serde(default)]
302    targets: Vec<BTreeMap<String, usize>>,
303    #[serde(default, deserialize_with = "deserialize_present_option")]
304    extensions: Option<PrimitiveExtensions>,
305}
306
307#[derive(Debug, Deserialize)]
308#[serde(rename_all = "camelCase")]
309struct PrimitiveExtensions {
310    #[serde(
311        rename = "KHR_draco_mesh_compression",
312        default,
313        deserialize_with = "deserialize_present_option"
314    )]
315    khr_draco_mesh_compression: Option<DracoExtension>,
316}
317
318#[derive(Debug, Deserialize)]
319#[serde(rename_all = "camelCase", deny_unknown_fields)]
320struct DracoExtension {
321    buffer_view: u32,
322    #[serde(default)]
323    attributes: BTreeMap<String, u32>,
324}
325
326// ============================================================================
327// GltfReader
328// ============================================================================
329
330/// A reader for glTF/GLB files with Draco mesh decompression support.
331pub struct GltfReader {
332    root: GltfRoot,
333    buffers: Vec<Vec<u8>>,
334}
335
336/// Lightweight scene metadata exposed without reparsing the JSON document.
337#[derive(Clone, Debug, Default, PartialEq)]
338pub struct GltfDocumentMetadata {
339    /// Mesh names repeated once per primitive, in mesh-major order.
340    pub primitive_names: Vec<Option<String>>,
341    pub nodes: Vec<GltfNodeMetadata>,
342    pub scenes: Vec<GltfSceneMetadata>,
343    pub default_scene: Option<usize>,
344    pub uses_draco: bool,
345    /// Non-data companion URIs referenced by buffers or images, deduplicated.
346    pub external_resource_uris: Vec<String>,
347}
348
349/// Node fields used by lightweight front ends.
350#[derive(Clone, Debug, Default, PartialEq)]
351pub struct GltfNodeMetadata {
352    pub name: Option<String>,
353    pub mesh: Option<usize>,
354    pub translation: Option<[f32; 3]>,
355    pub rotation: Option<[f32; 4]>,
356    pub scale: Option<[f32; 3]>,
357    pub children: Vec<usize>,
358}
359
360/// Scene name and root-node indices.
361#[derive(Clone, Debug, Default, PartialEq, Eq)]
362pub struct GltfSceneMetadata {
363    pub name: Option<String>,
364    pub nodes: Vec<usize>,
365}
366
367// `DecodedAccessor` and the `AccessorSource` trait now live in `gltf_geometry`;
368// `GltfAccessorReader` is this crate's implementation over a parsed `GltfRoot`.
369struct GltfAccessorReader<'a> {
370    accessors: &'a [Accessor],
371    buffer_views: &'a [BufferView],
372    buffers: &'a [Vec<u8>],
373}
374
375impl AccessorSource for GltfAccessorReader<'_> {
376    fn read_attribute(
377        &self,
378        accessor: usize,
379        expected_types: &[&str],
380        allowed_component_types: &[u32],
381    ) -> Result<DecodedAccessor> {
382        GltfAccessorReader::read_attribute(self, accessor, expected_types, allowed_component_types)
383    }
384
385    fn read_indices(&self, accessor: usize) -> Result<Vec<u32>> {
386        GltfAccessorReader::read_indices(self, accessor)
387    }
388}
389
390impl<'a> GltfAccessorReader<'a> {
391    fn new(root: &'a GltfRoot, buffers: &'a [Vec<u8>]) -> Self {
392        Self {
393            accessors: &root.accessors,
394            buffer_views: &root.buffer_views,
395            buffers,
396        }
397    }
398
399    fn read_attribute(
400        &self,
401        accessor_idx: usize,
402        expected_types: &[&str],
403        allowed_component_types: &[u32],
404    ) -> Result<DecodedAccessor> {
405        let accessor = self.accessor(accessor_idx)?;
406
407        if !expected_types
408            .iter()
409            .any(|expected| accessor.accessor_type == *expected)
410        {
411            return Err(GltfError::InvalidGltf(format!(
412                "Expected one of {:?} accessor, got {}",
413                expected_types, accessor.accessor_type
414            )));
415        }
416
417        if !allowed_component_types.contains(&accessor.component_type) {
418            return Err(GltfError::Unsupported(format!(
419                "Unsupported {} component type: {}",
420                accessor.accessor_type, accessor.component_type
421            )));
422        }
423
424        let num_components = accessor_num_components(&accessor.accessor_type)?;
425        let data_type = data_type_for_component_type(accessor.component_type)?;
426        let component_size = data_type.byte_length();
427        let row_size = (num_components as usize)
428            .checked_mul(component_size)
429            .ok_or_else(|| GltfError::InvalidGltf("Accessor row size overflow".into()))?;
430        let layout = self.accessor_layout(accessor, row_size, component_size, true, "Accessor")?;
431
432        let byte_len = accessor
433            .count
434            .checked_mul(row_size)
435            .ok_or_else(|| GltfError::InvalidGltf("Accessor byte size overflow".into()))?;
436        let mut bytes = Vec::new();
437        bytes
438            .try_reserve_exact(byte_len)
439            .map_err(|_| GltfError::ResourceLimitExceeded("Accessor allocation failed".into()))?;
440        for i in 0..accessor.count {
441            let relative = i
442                .checked_mul(layout.stride)
443                .ok_or_else(|| GltfError::InvalidGltf("Accessor range overflow".into()))?;
444            let offset = layout
445                .start
446                .checked_add(relative)
447                .ok_or_else(|| GltfError::InvalidGltf("Accessor range overflow".into()))?;
448            let end = offset
449                .checked_add(row_size)
450                .filter(|end| *end <= layout.view_end)
451                .ok_or_else(|| {
452                    GltfError::InvalidGltf(format!(
453                        "{} accessor out of bounds",
454                        accessor.accessor_type
455                    ))
456                })?;
457            if end > layout.buffer.len() {
458                return Err(GltfError::InvalidGltf(format!(
459                    "{} accessor out of bounds",
460                    accessor.accessor_type
461                )));
462            }
463            bytes.extend_from_slice(&layout.buffer[offset..end]);
464        }
465
466        DecodedAccessor::new(
467            accessor.count,
468            num_components,
469            data_type,
470            accessor.normalized,
471            bytes,
472        )
473    }
474
475    fn read_indices(&self, accessor_idx: usize) -> Result<Vec<u32>> {
476        let accessor = self.accessor(accessor_idx)?;
477
478        if accessor.accessor_type != "SCALAR" {
479            return Err(GltfError::InvalidGltf(format!(
480                "Expected SCALAR accessor for indices, got {}",
481                accessor.accessor_type
482            )));
483        }
484        if accessor.normalized {
485            return Err(GltfError::InvalidGltf(
486                "Index accessor must not be normalized".into(),
487            ));
488        }
489
490        let component_size = match accessor.component_type {
491            GLTF_COMPONENT_UNSIGNED_BYTE => 1,
492            GLTF_COMPONENT_UNSIGNED_SHORT => 2,
493            GLTF_COMPONENT_UNSIGNED_INT => 4,
494            _ => {
495                return Err(GltfError::Unsupported(format!(
496                    "Unsupported index component type: {}",
497                    accessor.component_type
498                )));
499            }
500        };
501        let layout = self.accessor_layout(
502            accessor,
503            component_size,
504            component_size,
505            false,
506            "Index accessor",
507        )?;
508        let mut result = Vec::new();
509        result.try_reserve_exact(accessor.count).map_err(|_| {
510            GltfError::ResourceLimitExceeded("Index accessor allocation failed".into())
511        })?;
512
513        for index in 0..accessor.count {
514            let bytes = layout.element(index, component_size, "Index accessor")?;
515            let value = match accessor.component_type {
516                GLTF_COMPONENT_UNSIGNED_BYTE => bytes[0] as u32,
517                GLTF_COMPONENT_UNSIGNED_SHORT => u16::from_le_bytes([bytes[0], bytes[1]]) as u32,
518                GLTF_COMPONENT_UNSIGNED_INT => {
519                    u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
520                }
521                component_type => {
522                    return Err(GltfError::Unsupported(format!(
523                        "Unsupported index component type: {component_type}"
524                    )));
525                }
526            };
527            result.push(value);
528        }
529
530        Ok(result)
531    }
532
533    fn accessor(&self, accessor_idx: usize) -> Result<&Accessor> {
534        self.accessors.get(accessor_idx).ok_or_else(|| {
535            GltfError::InvalidGltf(format!("Invalid accessor index: {}", accessor_idx))
536        })
537    }
538
539    fn accessor_layout(
540        &self,
541        accessor: &Accessor,
542        element_size: usize,
543        component_size: usize,
544        vertex_attribute: bool,
545        label: &str,
546    ) -> Result<AccessorLayout<'a>> {
547        if accessor.sparse.is_some() {
548            return Err(GltfError::Unsupported(
549                "Sparse accessors are not supported".into(),
550            ));
551        }
552        if accessor.count == 0 {
553            return Err(GltfError::InvalidGltf(format!(
554                "{} count must be greater than zero",
555                label
556            )));
557        }
558
559        let buffer_view_idx = accessor
560            .buffer_view
561            .ok_or_else(|| GltfError::InvalidGltf(format!("{} has no bufferView", label)))?;
562
563        let buffer_view = self.buffer_views.get(buffer_view_idx).ok_or_else(|| {
564            GltfError::InvalidGltf(format!("Invalid bufferView index: {}", buffer_view_idx))
565        })?;
566
567        let buffer = self.buffers.get(buffer_view.buffer).ok_or_else(|| {
568            GltfError::InvalidGltf(format!("Invalid buffer index: {}", buffer_view.buffer))
569        })?;
570
571        let view_offset = buffer_view.byte_offset.unwrap_or(0);
572        let accessor_offset = accessor.byte_offset.unwrap_or(0);
573        if !accessor_offset.is_multiple_of(component_size) {
574            return Err(GltfError::InvalidGltf(format!(
575                "{} byteOffset is not aligned to component size {}",
576                label, component_size
577            )));
578        }
579
580        let start = view_offset
581            .checked_add(accessor_offset)
582            .ok_or_else(|| GltfError::InvalidGltf("Accessor start overflow".into()))?;
583        if start % component_size != 0 {
584            return Err(GltfError::InvalidGltf(format!(
585                "{} absolute byte offset is not aligned to component size {}",
586                label, component_size
587            )));
588        }
589        if !vertex_attribute && buffer_view.byte_stride.is_some() {
590            return Err(GltfError::InvalidGltf(format!(
591                "{} bufferView must not define byteStride",
592                label
593            )));
594        }
595
596        let stride = buffer_view.byte_stride.unwrap_or(element_size);
597
598        if stride < element_size {
599            return Err(GltfError::InvalidGltf(format!(
600                "{} byteStride {} is smaller than element size {}",
601                label, stride, element_size
602            )));
603        }
604        if stride % component_size != 0 {
605            return Err(GltfError::InvalidGltf(format!(
606                "{} byteStride {} is not aligned to component size {}",
607                label, stride, component_size
608            )));
609        }
610        if let Some(byte_stride) = buffer_view.byte_stride {
611            if !(4..=252).contains(&byte_stride) {
612                return Err(GltfError::InvalidGltf(format!(
613                    "{} byteStride {} is outside glTF range 4..=252",
614                    label, byte_stride
615                )));
616            }
617            if vertex_attribute && byte_stride % 4 != 0 {
618                return Err(GltfError::InvalidGltf(format!(
619                    "{} byteStride {} is not 4-byte aligned",
620                    label, byte_stride
621                )));
622            }
623        }
624
625        let view_end = view_offset
626            .checked_add(buffer_view.byte_length)
627            .ok_or_else(|| GltfError::InvalidGltf("Buffer view range overflow".into()))?;
628        if start > view_end {
629            return Err(GltfError::InvalidGltf(format!(
630                "{} starts past bufferView end",
631                label
632            )));
633        }
634        let byte_len = stride
635            .checked_mul(accessor.count - 1)
636            .and_then(|prefix| prefix.checked_add(element_size))
637            .ok_or_else(|| GltfError::InvalidGltf("Accessor byte range overflow".into()))?;
638        let accessor_end = start
639            .checked_add(byte_len)
640            .ok_or_else(|| GltfError::InvalidGltf("Accessor byte range overflow".into()))?;
641        if accessor_end > view_end {
642            return Err(GltfError::InvalidGltf(format!(
643                "{} accessor does not fit its bufferView",
644                label
645            )));
646        }
647        if view_end > buffer.len() {
648            return Err(GltfError::InvalidGltf(
649                "Buffer view extends past buffer end".into(),
650            ));
651        }
652
653        Ok(AccessorLayout {
654            buffer,
655            start,
656            stride,
657            view_end,
658        })
659    }
660}
661
662struct AccessorLayout<'a> {
663    buffer: &'a [u8],
664    start: usize,
665    stride: usize,
666    view_end: usize,
667}
668
669impl<'a> AccessorLayout<'a> {
670    fn element(&self, index: usize, size: usize, label: &str) -> Result<&'a [u8]> {
671        let relative = index
672            .checked_mul(self.stride)
673            .ok_or_else(|| GltfError::InvalidGltf(format!("{label} range overflow")))?;
674        let start = self
675            .start
676            .checked_add(relative)
677            .ok_or_else(|| GltfError::InvalidGltf(format!("{label} range overflow")))?;
678        let end = start
679            .checked_add(size)
680            .filter(|end| *end <= self.view_end)
681            .ok_or_else(|| GltfError::InvalidGltf(format!("{label} out of bounds")))?;
682        self.buffer
683            .get(start..end)
684            .ok_or_else(|| GltfError::InvalidGltf(format!("{label} out of bounds")))
685    }
686}
687
688/// Information about a Draco-compressed primitive within a glTF mesh.
689#[derive(Debug, Clone)]
690pub struct DracoPrimitiveInfo {
691    /// Index of the mesh in the glTF file.
692    pub mesh_index: usize,
693    /// Name of the mesh (if available).
694    pub mesh_name: Option<String>,
695    /// Index of the primitive within the mesh.
696    pub primitive_index: usize,
697    /// Buffer view index containing the Draco data.
698    pub buffer_view: usize,
699    /// Attribute mappings from glTF semantic to Draco attribute ID.
700    pub attributes: BTreeMap<String, u32>,
701}
702
703impl GltfReader {
704    /// Open a glTF or GLB file.
705    ///
706    /// The file type is detected automatically based on the magic bytes.
707    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
708        let path = path.as_ref();
709        let data = fs::read(path)?;
710        let base = path.parent().unwrap_or_else(|| Path::new("."));
711        let resolver = FileResourceResolver::new(base, ExternalFilePolicy::Allow);
712        Self::from_bytes_with_resolver(&data, &resolver, &ResourceLimits::default())
713    }
714
715    /// Parse from GLB binary data.
716    pub fn from_glb(data: &[u8]) -> Result<Self> {
717        let container = parse_gltf_container(data)?;
718        if container.format != GltfContainerFormat::Glb {
719            return Err(GltfError::InvalidGlb("input is not a GLB container".into()));
720        }
721        Self::from_bytes(data)
722    }
723
724    /// Parse from glTF JSON or GLB binary data.
725    ///
726    /// The payload type is detected automatically from the GLB magic bytes.
727    /// For glTF JSON with external buffers, use [`Self::from_bytes_with_base_path`].
728    pub fn from_bytes(data: &[u8]) -> Result<Self> {
729        Self::from_bytes_impl(data, None, &ResourceLimits::default(), false)
730    }
731
732    /// Parse from glTF JSON or GLB binary data with an optional base path for external buffers.
733    pub fn from_bytes_with_base_path(data: &[u8], base_path: Option<&Path>) -> Result<Self> {
734        if let Some(base) = base_path {
735            let resolver = FileResourceResolver::new(base, ExternalFilePolicy::Allow);
736            Self::from_bytes_with_resolver(data, &resolver, &ResourceLimits::default())
737        } else {
738            Self::from_bytes_impl(data, None, &ResourceLimits::default(), false)
739        }
740    }
741
742    /// Parse with a caller-provided external resource resolver and quotas.
743    pub fn from_bytes_with_resolver(
744        data: &[u8],
745        resolver: &dyn ResourceResolver,
746        limits: &ResourceLimits,
747    ) -> Result<Self> {
748        Self::from_bytes_impl(data, Some(resolver), limits, false)
749    }
750
751    /// Parse from glTF JSON data with optional base path for external buffers.
752    pub fn from_gltf(json_data: &[u8], base_path: Option<&Path>) -> Result<Self> {
753        let container = parse_gltf_container(json_data)?;
754        if container.format != GltfContainerFormat::Gltf {
755            return Err(GltfError::InvalidGltf("input is a GLB container".into()));
756        }
757        Self::from_bytes_with_base_path(json_data, base_path)
758    }
759
760    /// Parse glTF/GLB bytes, decoding geometry even when the asset uses scene
761    /// features this crate does not model (skins, animations, morph targets).
762    ///
763    /// Unlike [`Self::from_bytes`], which rejects such assets, this reader
764    /// ignores those features and decodes only geometry. Use it to read meshes
765    /// out of skinned or animated assets, including the output of
766    /// [`crate::compress_gltf_bytes`] for those assets. Per-primitive decoding
767    /// still fails for unsupported attribute layouts.
768    pub fn from_bytes_lenient(data: &[u8]) -> Result<Self> {
769        Self::from_bytes_impl(data, None, &ResourceLimits::default(), true)
770    }
771
772    /// Like [`Self::from_bytes_lenient`], with a base path for external buffers.
773    pub fn from_bytes_lenient_with_base_path(
774        data: &[u8],
775        base_path: Option<&Path>,
776    ) -> Result<Self> {
777        if let Some(base) = base_path {
778            let resolver = FileResourceResolver::new(base, ExternalFilePolicy::Allow);
779            Self::from_bytes_lenient_with_resolver(data, &resolver, &ResourceLimits::default())
780        } else {
781            Self::from_bytes_impl(data, None, &ResourceLimits::default(), true)
782        }
783    }
784
785    /// Lenient geometry parse with a caller-provided resolver and quotas.
786    pub fn from_bytes_lenient_with_resolver(
787        data: &[u8],
788        resolver: &dyn ResourceResolver,
789        limits: &ResourceLimits,
790    ) -> Result<Self> {
791        Self::from_bytes_impl(data, Some(resolver), limits, true)
792    }
793
794    fn from_bytes_impl(
795        data: &[u8],
796        resolver: Option<&dyn ResourceResolver>,
797        limits: &ResourceLimits,
798        lenient: bool,
799    ) -> Result<Self> {
800        let container = parse_gltf_container(data)?;
801        let root: GltfRoot = serde_json::from_slice(container.json)?;
802        validate_typed_khr_draco_document(&root)?;
803        validate_root_metadata(&root)?;
804        if !lenient {
805            reject_unsupported_features(&root)?;
806        }
807        let buffers = load_buffers(
808            &root,
809            container.format == GltfContainerFormat::Glb,
810            container.bin,
811            resolver,
812            limits,
813        )?;
814        validate_images(&root, &buffers, resolver, limits)?;
815        Ok(Self { root, buffers })
816    }
817
818    /// Builds a lenient reader from an already-parsed glTF document (`doc`) and
819    /// its resolved buffer bytes.
820    ///
821    /// This lets a caller that already holds a parsed scene and its buffers
822    /// (for example a `gltf-rs` document and the bytes it resolved) decode
823    /// geometry through this reader without serializing back to glTF/GLB bytes
824    /// and re-resolving the buffers. The same lenient policy as
825    /// [`Self::from_bytes_lenient`] applies: skins, animations, and morph
826    /// targets are ignored (not rejected), and per-primitive decoding still
827    /// fails for unsupported attribute layouts.
828    ///
829    /// `buffers` must already be resolved and indexed by glTF buffer index; no
830    /// URI or BIN-chunk resolution is performed here.
831    pub fn from_value(doc: &serde_json::Value, buffers: Vec<Vec<u8>>) -> Result<Self> {
832        validate_khr_draco_document(doc)?;
833        let root: GltfRoot = serde_json::from_value(doc.clone())?;
834        validate_root_metadata(&root)?;
835        Ok(Self { root, buffers })
836    }
837
838    /// Resolved buffer bytes, indexed by glTF buffer index. Used by the
839    /// byte-API compressor, which also requires the writer feature.
840    #[cfg(feature = "gltf-writer")]
841    pub(crate) fn buffers(&self) -> &[Vec<u8>] {
842        &self.buffers
843    }
844
845    /// Decode a single non-Draco primitive, returning the mesh and the
846    /// `(glTF semantic, Draco unique id)` mapping for its attributes.
847    ///
848    /// Used by the compressor to build the `KHR_draco_mesh_compression`
849    /// attributes map with the original glTF semantic names (including
850    /// `TANGENT`, `JOINTS_n`, `WEIGHTS_n`, extra `TEXCOORD_n`/`COLOR_n`, and
851    /// custom `_*` attributes), which the Draco attribute model alone cannot
852    /// preserve. Errors if the primitive is already Draco-compressed.
853    ///
854    /// This is the geometry-decode callback expected by
855    /// [`crate::compress_gltf_value`], so a caller holding a parsed scene can
856    /// drive the compressor: build a reader with [`Self::from_value`] and pass
857    /// `|mesh, prim| reader.decode_primitive_with_semantics(mesh, prim)`.
858    pub fn decode_primitive_with_semantics(
859        &self,
860        mesh_idx: usize,
861        prim_idx: usize,
862    ) -> Result<(Mesh, Vec<(String, u32)>)> {
863        let gltf_mesh = self.root.meshes.get(mesh_idx).ok_or_else(|| {
864            GltfError::InvalidGltf(format!("Mesh index {} out of range", mesh_idx))
865        })?;
866        let primitive = gltf_mesh.primitives.get(prim_idx).ok_or_else(|| {
867            GltfError::InvalidGltf(format!(
868                "Primitive index {}:{} out of range",
869                mesh_idx, prim_idx
870            ))
871        })?;
872        if primitive
873            .extensions
874            .as_ref()
875            .and_then(|ext| ext.khr_draco_mesh_compression.as_ref())
876            .is_some()
877        {
878            return Err(GltfError::Unsupported(
879                "primitive is already Draco-compressed".into(),
880            ));
881        }
882        self.decode_standard_primitive(mesh_idx, prim_idx, primitive)
883    }
884
885    /// Check if the glTF file uses Draco compression.
886    pub fn has_draco_extension(&self) -> bool {
887        self.root
888            .extensions_used
889            .iter()
890            .any(|ext| ext == KHR_DRACO_MESH_COMPRESSION)
891    }
892
893    /// Return lightweight metadata from the already-parsed document.
894    pub fn document_metadata(&self) -> GltfDocumentMetadata {
895        let external_resource_uris = self
896            .root
897            .buffers
898            .iter()
899            .filter_map(|buffer| buffer.uri.as_deref())
900            .chain(
901                self.root
902                    .images
903                    .iter()
904                    .filter_map(|image| image.uri.as_deref()),
905            )
906            .filter(|uri| !uri.starts_with("data:"))
907            .map(str::to_owned)
908            .collect::<BTreeSet<_>>()
909            .into_iter()
910            .collect();
911        GltfDocumentMetadata {
912            primitive_names: self
913                .root
914                .meshes
915                .iter()
916                .flat_map(|mesh| std::iter::repeat_n(mesh.name.clone(), mesh.primitives.len()))
917                .collect(),
918            nodes: self
919                .root
920                .nodes
921                .iter()
922                .map(|node| GltfNodeMetadata {
923                    name: node.name.clone(),
924                    mesh: node.mesh,
925                    translation: node.translation,
926                    rotation: node.rotation,
927                    scale: node.scale,
928                    children: node.children.clone(),
929                })
930                .collect(),
931            scenes: self
932                .root
933                .scenes
934                .iter()
935                .map(|scene| GltfSceneMetadata {
936                    name: scene.name.clone(),
937                    nodes: scene.nodes.clone(),
938                })
939                .collect(),
940            default_scene: self.root.scene,
941            uses_draco: self.has_draco_extension(),
942            external_resource_uris,
943        }
944    }
945
946    /// Get information about all Draco-compressed primitives.
947    pub fn draco_primitives(&self) -> Vec<DracoPrimitiveInfo> {
948        let mut result = Vec::new();
949
950        for (mesh_idx, mesh) in self.root.meshes.iter().enumerate() {
951            for (prim_idx, primitive) in mesh.primitives.iter().enumerate() {
952                if let Some(ext) = &primitive.extensions {
953                    if let Some(draco) = &ext.khr_draco_mesh_compression {
954                        result.push(DracoPrimitiveInfo {
955                            mesh_index: mesh_idx,
956                            mesh_name: mesh.name.clone(),
957                            primitive_index: prim_idx,
958                            buffer_view: draco.buffer_view as usize,
959                            attributes: draco.attributes.clone(),
960                        });
961                    }
962                }
963            }
964        }
965
966        result
967    }
968
969    /// Get the raw Draco-compressed data for a primitive.
970    pub fn get_draco_data(&self, info: &DracoPrimitiveInfo) -> Result<&[u8]> {
971        let buffer_view = self
972            .root
973            .buffer_views
974            .get(info.buffer_view)
975            .ok_or_else(|| {
976                GltfError::InvalidGltf(format!("Invalid buffer view index: {}", info.buffer_view))
977            })?;
978
979        let buffer = self.buffers.get(buffer_view.buffer).ok_or_else(|| {
980            GltfError::InvalidGltf(format!("Invalid buffer index: {}", buffer_view.buffer))
981        })?;
982
983        let offset = buffer_view.byte_offset.unwrap_or(0);
984        let end = offset
985            .checked_add(buffer_view.byte_length)
986            .ok_or_else(|| GltfError::InvalidGltf("Buffer view range overflow".into()))?;
987
988        if end > buffer.len() {
989            return Err(GltfError::InvalidGltf(
990                "Buffer view extends past buffer end".into(),
991            ));
992        }
993
994        Ok(&buffer[offset..end])
995    }
996
997    /// Decode a Draco-compressed primitive as a Mesh.
998    pub fn decode_draco_mesh(&self, info: &DracoPrimitiveInfo) -> Result<Mesh> {
999        let data = self.get_draco_data(info)?;
1000        let mut decoder_buffer = DecoderBuffer::new(data);
1001        let mut mesh = Mesh::new();
1002        let mut decoder = MeshDecoder::new();
1003
1004        decoder
1005            .decode(&mut decoder_buffer, &mut mesh)
1006            .map_err(GltfError::DracoDecode)?;
1007
1008        let primitive = self.primitive_for_draco_info(info)?;
1009        self.validate_draco_primitive_metadata(info, primitive, &mesh)?;
1010        self.add_draco_side_attributes(&mut mesh, primitive, info)?;
1011
1012        Ok(mesh)
1013    }
1014
1015    /// Decode a Draco-compressed primitive as a PointCloud.
1016    #[cfg(feature = "point_cloud_decode")]
1017    pub fn decode_draco_point_cloud(&self, info: &DracoPrimitiveInfo) -> Result<PointCloud> {
1018        let data = self.get_draco_data(info)?;
1019        let mut decoder_buffer = DecoderBuffer::new(data);
1020        let mut point_cloud = PointCloud::new();
1021        let mut decoder = PointCloudDecoder::new();
1022
1023        decoder
1024            .decode(&mut decoder_buffer, &mut point_cloud)
1025            .map_err(GltfError::DracoDecode)?;
1026
1027        Ok(point_cloud)
1028    }
1029
1030    /// Decode all Draco-compressed primitives as meshes.
1031    pub fn decode_all_draco_meshes(&self) -> Result<Vec<(DracoPrimitiveInfo, Mesh)>> {
1032        let primitives = self.draco_primitives();
1033        let mut result = Vec::with_capacity(primitives.len());
1034
1035        for info in primitives {
1036            let mesh = self.decode_draco_mesh(&info)?;
1037            result.push((info, mesh));
1038        }
1039
1040        Ok(result)
1041    }
1042
1043    // ========================================================================
1044    // Non-Draco Mesh Decoding
1045    // ========================================================================
1046
1047    /// Decode a non-Draco primitive from accessors/bufferViews.
1048    ///
1049    /// Returns the decoded mesh plus the `(glTF semantic, Draco unique id)`
1050    /// mapping for each attribute, in attribute add order. The unique id equals
1051    /// the attribute index, which is what the `KHR_draco_mesh_compression`
1052    /// attributes map references.
1053    fn decode_standard_primitive(
1054        &self,
1055        _mesh_idx: usize,
1056        _prim_idx: usize,
1057        primitive: &Primitive,
1058    ) -> Result<(Mesh, Vec<(String, u32)>)> {
1059        let mode = primitive.mode.unwrap_or(GLTF_MODE_TRIANGLES);
1060        let attributes: Vec<(String, usize)> = primitive
1061            .attributes
1062            .iter()
1063            .map(|(semantic, accessor)| (semantic.clone(), *accessor))
1064            .collect();
1065        decode_geometry(
1066            &self.accessor_reader(),
1067            mode,
1068            &attributes,
1069            primitive.indices,
1070        )
1071    }
1072
1073    fn accessor_reader(&self) -> GltfAccessorReader<'_> {
1074        GltfAccessorReader::new(&self.root, &self.buffers)
1075    }
1076
1077    fn decode_primitive_mesh(
1078        &self,
1079        mesh_idx: usize,
1080        gltf_mesh: &GltfMesh,
1081        prim_idx: usize,
1082        primitive: &Primitive,
1083    ) -> Result<Mesh> {
1084        if let Some(draco) = primitive
1085            .extensions
1086            .as_ref()
1087            .and_then(|ext| ext.khr_draco_mesh_compression.as_ref())
1088        {
1089            let info = DracoPrimitiveInfo {
1090                mesh_index: mesh_idx,
1091                mesh_name: gltf_mesh.name.clone(),
1092                primitive_index: prim_idx,
1093                buffer_view: draco.buffer_view as usize,
1094                attributes: draco.attributes.clone(),
1095            };
1096            self.decode_draco_mesh(&info)
1097        } else {
1098            self.decode_standard_primitive(mesh_idx, prim_idx, primitive)
1099                .map(|(mesh, _)| mesh)
1100        }
1101    }
1102
1103    fn primitive_for_draco_info(&self, info: &DracoPrimitiveInfo) -> Result<&Primitive> {
1104        let mesh = self.root.meshes.get(info.mesh_index).ok_or_else(|| {
1105            GltfError::InvalidGltf(format!("Invalid mesh index: {}", info.mesh_index))
1106        })?;
1107        let primitive = mesh.primitives.get(info.primitive_index).ok_or_else(|| {
1108            GltfError::InvalidGltf(format!(
1109                "Invalid primitive index {} for mesh {}",
1110                info.primitive_index, info.mesh_index
1111            ))
1112        })?;
1113        let draco = primitive
1114            .extensions
1115            .as_ref()
1116            .and_then(|ext| ext.khr_draco_mesh_compression.as_ref())
1117            .ok_or_else(|| {
1118                GltfError::InvalidGltf(format!(
1119                    "Primitive {}:{} does not use {}",
1120                    info.mesh_index, info.primitive_index, KHR_DRACO_MESH_COMPRESSION
1121                ))
1122            })?;
1123        if draco.buffer_view as usize != info.buffer_view || draco.attributes != info.attributes {
1124            return Err(GltfError::InvalidGltf(
1125                "Draco primitive info does not match source primitive".into(),
1126            ));
1127        }
1128        Ok(primitive)
1129    }
1130
1131    fn validate_draco_primitive_metadata(
1132        &self,
1133        info: &DracoPrimitiveInfo,
1134        primitive: &Primitive,
1135        mesh: &Mesh,
1136    ) -> Result<()> {
1137        let mode = primitive.mode.unwrap_or(GLTF_MODE_TRIANGLES);
1138        if mode != GLTF_MODE_TRIANGLES && mode != 5 {
1139            return Err(GltfError::Unsupported(format!(
1140                "{} supports only TRIANGLES=4 or TRIANGLE_STRIP=5, got mode {}",
1141                KHR_DRACO_MESH_COMPRESSION, mode
1142            )));
1143        }
1144
1145        for (semantic, &draco_attribute_id) in &info.attributes {
1146            let Some(accessor_idx) = primitive.attributes.get(semantic) else {
1147                return Err(GltfError::InvalidGltf(format!(
1148                    "{} attribute {} is not present in primitive.attributes",
1149                    KHR_DRACO_MESH_COMPRESSION, semantic
1150                )));
1151            };
1152            let attribute_spec = supported_semantic_spec(semantic)?;
1153            let attribute = mesh
1154                .attribute_by_unique_id(draco_attribute_id)
1155                .ok_or_else(|| {
1156                    GltfError::InvalidGltf(format!(
1157                        "Draco unique attribute id {} for {} is absent",
1158                        draco_attribute_id, semantic
1159                    ))
1160                })?;
1161            if attribute.attribute_type() != attribute_spec.attribute_type {
1162                return Err(GltfError::InvalidGltf(format!(
1163                    "Draco attribute {} has type {:?}, expected {:?}",
1164                    semantic,
1165                    attribute.attribute_type(),
1166                    attribute_spec.attribute_type
1167                )));
1168            }
1169            self.validate_accessor_matches_attribute(*accessor_idx, semantic, attribute)?;
1170        }
1171
1172        if let Some(indices_accessor_idx) = primitive.indices {
1173            let accessor = self
1174                .root
1175                .accessors
1176                .get(indices_accessor_idx)
1177                .ok_or_else(|| {
1178                    GltfError::InvalidGltf(format!(
1179                        "Invalid indices accessor index: {}",
1180                        indices_accessor_idx
1181                    ))
1182                })?;
1183            if accessor.sparse.is_some() {
1184                return Err(GltfError::Unsupported(
1185                    "Sparse index accessors are not supported".into(),
1186                ));
1187            }
1188            if accessor.accessor_type != "SCALAR" {
1189                return Err(GltfError::InvalidGltf(format!(
1190                    "Expected SCALAR accessor for Draco indices, got {}",
1191                    accessor.accessor_type
1192                )));
1193            }
1194            if accessor.normalized {
1195                return Err(GltfError::InvalidGltf(
1196                    "Draco indices accessor must not be normalized".into(),
1197                ));
1198            }
1199            if ![
1200                GLTF_COMPONENT_UNSIGNED_BYTE,
1201                GLTF_COMPONENT_UNSIGNED_SHORT,
1202                GLTF_COMPONENT_UNSIGNED_INT,
1203            ]
1204            .contains(&accessor.component_type)
1205            {
1206                return Err(GltfError::Unsupported(format!(
1207                    "Unsupported Draco index accessor component type: {}",
1208                    accessor.component_type
1209                )));
1210            }
1211            let expected_count = if mode == GLTF_MODE_TRIANGLES {
1212                mesh.num_faces().checked_mul(3)
1213            } else {
1214                mesh.num_faces().checked_add(2)
1215            }
1216            .ok_or_else(|| GltfError::InvalidGltf("decoded index count overflow".into()))?;
1217            if accessor.count != expected_count {
1218                return Err(GltfError::InvalidGltf(format!(
1219                    "Draco indices accessor count {} does not match decoded index count {}",
1220                    accessor.count, expected_count
1221                )));
1222            }
1223        }
1224
1225        Ok(())
1226    }
1227
1228    fn validate_accessor_matches_attribute(
1229        &self,
1230        accessor_idx: usize,
1231        semantic: &str,
1232        attribute: &PointAttribute,
1233    ) -> Result<()> {
1234        let accessor = self.root.accessors.get(accessor_idx).ok_or_else(|| {
1235            GltfError::InvalidGltf(format!("Invalid accessor index: {}", accessor_idx))
1236        })?;
1237        validate_semantic_accessor(
1238            semantic,
1239            &accessor.accessor_type,
1240            accessor.component_type,
1241            accessor.normalized,
1242        )?;
1243        if accessor.sparse.is_some() {
1244            return Err(GltfError::Unsupported(format!(
1245                "Sparse accessor for {} is not supported",
1246                semantic
1247            )));
1248        }
1249        let expected_accessor_type = gltf_type_for_num_components(attribute.num_components())?;
1250        if accessor.accessor_type != expected_accessor_type {
1251            return Err(GltfError::InvalidGltf(format!(
1252                "{} accessor type {} does not match decoded attribute type {}",
1253                semantic, accessor.accessor_type, expected_accessor_type
1254            )));
1255        }
1256        let expected_component_type = component_type_for_data_type(attribute.data_type())?;
1257        let attribute_spec = supported_semantic_spec(semantic)?;
1258        if !attribute_spec
1259            .allowed_component_types
1260            .contains(&expected_component_type)
1261        {
1262            return Err(GltfError::Unsupported(format!(
1263                "{} decoded component type {} is not supported by draco-io glTF",
1264                semantic, expected_component_type
1265            )));
1266        }
1267        if accessor.component_type != expected_component_type {
1268            return Err(GltfError::InvalidGltf(format!(
1269                "{} accessor componentType {} does not match decoded componentType {}",
1270                semantic, accessor.component_type, expected_component_type
1271            )));
1272        }
1273        if accessor.normalized != attribute.normalized() {
1274            return Err(GltfError::InvalidGltf(format!(
1275                "{} accessor normalized={} does not match decoded normalized={}",
1276                semantic,
1277                accessor.normalized,
1278                attribute.normalized()
1279            )));
1280        }
1281        if accessor.count != attribute.size() {
1282            return Err(GltfError::InvalidGltf(format!(
1283                "{} accessor count {} does not match decoded attribute count {}",
1284                semantic,
1285                accessor.count,
1286                attribute.size()
1287            )));
1288        }
1289        Ok(())
1290    }
1291
1292    fn add_draco_side_attributes(
1293        &self,
1294        mesh: &mut Mesh,
1295        primitive: &Primitive,
1296        info: &DracoPrimitiveInfo,
1297    ) -> Result<()> {
1298        let accessor_reader = self.accessor_reader();
1299        let mut attributes: Vec<_> = primitive.attributes.iter().collect();
1300        attributes.sort_by_key(|(left, _)| *left);
1301
1302        for (semantic, accessor_idx) in attributes {
1303            if info.attributes.contains_key(semantic) {
1304                continue;
1305            }
1306            add_named_attribute(mesh, &accessor_reader, semantic, *accessor_idx, None)?;
1307        }
1308        Ok(())
1309    }
1310
1311    /// Get the number of meshes in the glTF file.
1312    pub fn num_meshes(&self) -> usize {
1313        self.root.meshes.len()
1314    }
1315
1316    /// Get the number of buffers in the glTF file.
1317    pub fn num_buffers(&self) -> usize {
1318        self.buffers.len()
1319    }
1320
1321    /// Get the extensions used by this glTF file.
1322    pub fn extensions_used(&self) -> &[String] {
1323        &self.root.extensions_used
1324    }
1325
1326    /// Get the extensions required by this glTF file.
1327    pub fn extensions_required(&self) -> &[String] {
1328        &self.root.extensions_required
1329    }
1330}
1331
1332// ============================================================================
1333// Helper Functions
1334// ============================================================================
1335
1336fn validate_typed_khr_draco_document(root: &GltfRoot) -> Result<()> {
1337    let used = root
1338        .extensions_used
1339        .iter()
1340        .any(|extension| extension == KHR_DRACO_MESH_COMPRESSION);
1341    let required = root
1342        .extensions_required
1343        .iter()
1344        .any(|extension| extension == KHR_DRACO_MESH_COMPRESSION);
1345    if required && !used {
1346        return Err(GltfError::InvalidGltf(format!(
1347            "{KHR_DRACO_MESH_COMPRESSION} is required but is not listed in extensionsUsed"
1348        )));
1349    }
1350
1351    for mesh in &root.meshes {
1352        for primitive in &mesh.primitives {
1353            let Some(extension) = primitive
1354                .extensions
1355                .as_ref()
1356                .and_then(|extensions| extensions.khr_draco_mesh_compression.as_ref())
1357            else {
1358                continue;
1359            };
1360            validate_khr_draco_contract(
1361                KhrDracoPrimitiveContract {
1362                    extension: KhrDracoExtensionContract {
1363                        buffer_view: extension.buffer_view as usize,
1364                        attributes: &extension.attributes,
1365                    },
1366                    primitive_attributes: primitive
1367                        .attributes
1368                        .iter()
1369                        .map(|(semantic, accessor)| (semantic.as_str(), *accessor)),
1370                    indices: primitive.indices,
1371                    mode: primitive.mode.unwrap_or(GLTF_MODE_TRIANGLES),
1372                    extension_used: used,
1373                    extension_required: required,
1374                    buffer_view_count: root.buffer_views.len(),
1375                },
1376                |accessor| {
1377                    root.accessors
1378                        .get(accessor)
1379                        .map(|accessor| accessor.buffer_view.is_some() || accessor.sparse.is_some())
1380                },
1381            )?;
1382        }
1383    }
1384    Ok(())
1385}
1386
1387fn validate_root_metadata(root: &GltfRoot) -> Result<()> {
1388    if root.asset.version != "2.0" {
1389        return Err(GltfError::Unsupported(format!(
1390            "Unsupported glTF asset version: {}",
1391            root.asset.version
1392        )));
1393    }
1394    if let Some(min_version) = &root.asset.min_version {
1395        if min_version != "2.0" {
1396            return Err(GltfError::Unsupported(format!(
1397                "Unsupported glTF minimum version: {}",
1398                min_version
1399            )));
1400        }
1401    }
1402
1403    // glTF validity: every required extension must also be listed as used.
1404    // Whether an unknown required extension is *acceptable* is a scope decision
1405    // left to reject_unsupported_features (strict readers only); the lenient
1406    // document-preserving path tolerates them since it preserves, not
1407    // interprets, the rest of the document.
1408    for required in &root.extensions_required {
1409        if !root.extensions_used.iter().any(|used| used == required) {
1410            return Err(GltfError::InvalidGltf(format!(
1411                "Required extension {} is not listed in extensionsUsed",
1412                required
1413            )));
1414        }
1415    }
1416
1417    let mut has_draco_primitive = false;
1418    for mesh in &root.meshes {
1419        for primitive in &mesh.primitives {
1420            if primitive
1421                .extensions
1422                .as_ref()
1423                .and_then(|ext| ext.khr_draco_mesh_compression.as_ref())
1424                .is_some()
1425            {
1426                has_draco_primitive = true;
1427            }
1428        }
1429    }
1430    if has_draco_primitive
1431        && !root
1432            .extensions_used
1433            .iter()
1434            .any(|used| used == KHR_DRACO_MESH_COMPRESSION)
1435    {
1436        return Err(GltfError::InvalidGltf(format!(
1437            "Primitive uses {} but extensionsUsed does not list it",
1438            KHR_DRACO_MESH_COMPRESSION
1439        )));
1440    }
1441
1442    validate_document_references(root)?;
1443
1444    Ok(())
1445}
1446
1447fn validate_reference(index: usize, count: usize, label: &str) -> Result<()> {
1448    if index >= count {
1449        return Err(GltfError::InvalidGltf(format!(
1450            "{label} {index} is out of range for {count} entries"
1451        )));
1452    }
1453    Ok(())
1454}
1455
1456fn validate_document_references(root: &GltfRoot) -> Result<()> {
1457    if let Some(scene) = root.scene {
1458        validate_reference(scene, root.scenes.len(), "default scene")?;
1459    }
1460    for (scene_index, scene) in root.scenes.iter().enumerate() {
1461        for &node in &scene.nodes {
1462            validate_reference(node, root.nodes.len(), &format!("scene {scene_index} node"))?;
1463        }
1464    }
1465
1466    for (node_index, node) in root.nodes.iter().enumerate() {
1467        if let Some(mesh) = node.mesh {
1468            validate_reference(mesh, root.meshes.len(), &format!("node {node_index} mesh"))?;
1469        }
1470        if let Some(skin) = node.skin {
1471            validate_reference(skin, root.skins.len(), &format!("node {node_index} skin"))?;
1472        }
1473        for &child in &node.children {
1474            validate_reference(child, root.nodes.len(), &format!("node {node_index} child"))?;
1475            if child == node_index {
1476                return Err(GltfError::InvalidGltf(format!(
1477                    "node {node_index} cannot be its own child"
1478                )));
1479            }
1480        }
1481    }
1482
1483    for (mesh_index, mesh) in root.meshes.iter().enumerate() {
1484        for (primitive_index, primitive) in mesh.primitives.iter().enumerate() {
1485            for (semantic, &accessor) in &primitive.attributes {
1486                validate_reference(
1487                    accessor,
1488                    root.accessors.len(),
1489                    &format!("primitive {mesh_index}:{primitive_index} {semantic} accessor"),
1490                )?;
1491            }
1492            if let Some(indices) = primitive.indices {
1493                validate_reference(
1494                    indices,
1495                    root.accessors.len(),
1496                    &format!("primitive {mesh_index}:{primitive_index} indices accessor"),
1497                )?;
1498            }
1499            let vertex_count = primitive
1500                .attributes
1501                .values()
1502                .next()
1503                .and_then(|accessor| root.accessors.get(*accessor))
1504                .map(|accessor| accessor.count);
1505            for (target_index, target) in primitive.targets.iter().enumerate() {
1506                for (semantic, &accessor_index) in target {
1507                    validate_reference(
1508                        accessor_index,
1509                        root.accessors.len(),
1510                        &format!(
1511                            "primitive {mesh_index}:{primitive_index} morph target {target_index} {semantic} accessor"
1512                        ),
1513                    )?;
1514                    let accessor = &root.accessors[accessor_index];
1515                    if vertex_count.is_some_and(|count| accessor.count != count) {
1516                        return Err(GltfError::InvalidGltf(format!(
1517                            "primitive {mesh_index}:{primitive_index} morph target accessor count {} does not match vertex count {}",
1518                            accessor.count,
1519                            vertex_count.unwrap_or_default()
1520                        )));
1521                    }
1522                    if !matches!(semantic.as_str(), "POSITION" | "NORMAL" | "TANGENT")
1523                        || accessor.accessor_type != "VEC3"
1524                        || accessor.component_type != GLTF_COMPONENT_FLOAT
1525                        || accessor.normalized
1526                    {
1527                        return Err(GltfError::InvalidGltf(format!(
1528                            "primitive {mesh_index}:{primitive_index} morph target {semantic} accessor has an invalid contract"
1529                        )));
1530                    }
1531                }
1532            }
1533        }
1534    }
1535
1536    for (skin_index, skin) in root.skins.iter().enumerate() {
1537        if skin.joints.is_empty() {
1538            return Err(GltfError::InvalidGltf(format!(
1539                "skin {skin_index} has no joints"
1540            )));
1541        }
1542        for &joint in &skin.joints {
1543            validate_reference(joint, root.nodes.len(), &format!("skin {skin_index} joint"))?;
1544        }
1545        if let Some(skeleton) = skin.skeleton {
1546            validate_reference(
1547                skeleton,
1548                root.nodes.len(),
1549                &format!("skin {skin_index} skeleton"),
1550            )?;
1551        }
1552        if let Some(accessor_index) = skin.inverse_bind_matrices {
1553            validate_reference(
1554                accessor_index,
1555                root.accessors.len(),
1556                &format!("skin {skin_index} inverseBindMatrices accessor"),
1557            )?;
1558            let accessor = &root.accessors[accessor_index];
1559            if accessor.accessor_type != "MAT4"
1560                || accessor.component_type != GLTF_COMPONENT_FLOAT
1561                || accessor.normalized
1562                || accessor.count < skin.joints.len()
1563            {
1564                return Err(GltfError::InvalidGltf(format!(
1565                    "skin {skin_index} inverseBindMatrices accessor has an invalid contract"
1566                )));
1567            }
1568        }
1569    }
1570
1571    for (animation_index, animation) in root.animations.iter().enumerate() {
1572        if animation.channels.is_empty() || animation.samplers.is_empty() {
1573            return Err(GltfError::InvalidGltf(format!(
1574                "animation {animation_index} must contain channels and samplers"
1575            )));
1576        }
1577        for (sampler_index, sampler) in animation.samplers.iter().enumerate() {
1578            validate_reference(
1579                sampler.input,
1580                root.accessors.len(),
1581                &format!("animation {animation_index} sampler {sampler_index} input accessor"),
1582            )?;
1583            validate_reference(
1584                sampler.output,
1585                root.accessors.len(),
1586                &format!("animation {animation_index} sampler {sampler_index} output accessor"),
1587            )?;
1588            let input = &root.accessors[sampler.input];
1589            if input.accessor_type != "SCALAR"
1590                || input.component_type != GLTF_COMPONENT_FLOAT
1591                || input.normalized
1592            {
1593                return Err(GltfError::InvalidGltf(format!(
1594                    "animation {animation_index} sampler {sampler_index} input accessor has an invalid contract"
1595                )));
1596            }
1597            if sampler
1598                .interpolation
1599                .as_deref()
1600                .is_some_and(|interpolation| {
1601                    !matches!(interpolation, "LINEAR" | "STEP" | "CUBICSPLINE")
1602                })
1603            {
1604                return Err(GltfError::InvalidGltf(format!(
1605                    "animation {animation_index} sampler {sampler_index} has invalid interpolation"
1606                )));
1607            }
1608        }
1609        for (channel_index, channel) in animation.channels.iter().enumerate() {
1610            validate_reference(
1611                channel.sampler,
1612                animation.samplers.len(),
1613                &format!("animation {animation_index} channel {channel_index} sampler"),
1614            )?;
1615            if channel.target.path.is_empty() {
1616                return Err(GltfError::InvalidGltf(format!(
1617                    "animation {animation_index} channel {channel_index} target path is empty"
1618                )));
1619            }
1620            if let Some(node) = channel.target.node {
1621                validate_reference(
1622                    node,
1623                    root.nodes.len(),
1624                    &format!("animation {animation_index} channel {channel_index} target node"),
1625                )?;
1626            }
1627        }
1628    }
1629    Ok(())
1630}
1631
1632/// Rejects glTF features outside this crate's geometry-decoding scope.
1633///
1634/// Used by the strict readers ([`GltfReader::from_bytes`] and friends). The
1635/// document-preserving compressor uses a lenient path instead: it never
1636/// interprets these features, it just carries them through untouched, so it
1637/// does not reject them here.
1638fn reject_unsupported_features(root: &GltfRoot) -> Result<()> {
1639    // The strict reader cannot faithfully load an asset that *requires* an
1640    // extension it does not implement. (KHR_draco_mesh_compression is the only
1641    // one this crate honors.) The lenient/compressor path skips this check.
1642    for required in &root.extensions_required {
1643        if required != KHR_DRACO_MESH_COMPRESSION {
1644            return Err(GltfError::Unsupported(format!(
1645                "Unsupported required extension: {}",
1646                required
1647            )));
1648        }
1649    }
1650
1651    for (mesh_idx, mesh) in root.meshes.iter().enumerate() {
1652        for (prim_idx, primitive) in mesh.primitives.iter().enumerate() {
1653            if !primitive.targets.is_empty() {
1654                return Err(GltfError::Unsupported(format!(
1655                    "Morph targets are not supported on primitive {}:{}",
1656                    mesh_idx, prim_idx
1657                )));
1658            }
1659        }
1660    }
1661
1662    if !root.skins.is_empty() {
1663        return Err(GltfError::Unsupported("Skins are not supported".into()));
1664    }
1665    if root.nodes.iter().any(|node| node.skin.is_some()) {
1666        return Err(GltfError::Unsupported(
1667            "Skinned nodes are not supported".into(),
1668        ));
1669    }
1670    if !root.animations.is_empty() {
1671        return Err(GltfError::Unsupported(
1672            "Animations are not supported".into(),
1673        ));
1674    }
1675
1676    Ok(())
1677}
1678
1679fn load_buffers(
1680    root: &GltfRoot,
1681    is_glb: bool,
1682    glb_bin_chunk: Option<&[u8]>,
1683    resolver: Option<&dyn ResourceResolver>,
1684    limits: &ResourceLimits,
1685) -> Result<Vec<Vec<u8>>> {
1686    let mut references = Vec::new();
1687    references
1688        .try_reserve_exact(root.buffers.len())
1689        .map_err(|_| GltfError::ResourceLimitExceeded("buffer table allocation failed".into()))?;
1690    for buffer in &root.buffers {
1691        references.push(GltfBufferReference {
1692            uri: buffer.uri.as_deref(),
1693            byte_length: buffer.byte_length,
1694        });
1695    }
1696    let format = if is_glb {
1697        GltfContainerFormat::Glb
1698    } else {
1699        GltfContainerFormat::Gltf
1700    };
1701    resolve_gltf_buffers(&references, format, glb_bin_chunk, resolver, limits)
1702}
1703
1704fn validate_images(
1705    root: &GltfRoot,
1706    buffers: &[Vec<u8>],
1707    resolver: Option<&dyn ResourceResolver>,
1708    limits: &ResourceLimits,
1709) -> Result<()> {
1710    for (index, image) in root.images.iter().enumerate() {
1711        match (image.uri.as_deref(), image.buffer_view) {
1712            (Some(_), Some(_)) => {
1713                return Err(GltfError::InvalidGltf(format!(
1714                    "Image {index} defines both uri and bufferView"
1715                )));
1716            }
1717            (None, None) => {
1718                return Err(GltfError::InvalidGltf(format!(
1719                    "Image {index} defines neither uri nor bufferView"
1720                )));
1721            }
1722            (Some(uri), None) => {
1723                // Resolve even though the geometry reader does not decode image
1724                // pixels: missing companion files and byte quotas must fail at
1725                // import time rather than producing a false-success document.
1726                let _ = resolve_resource_uri(uri, resolver, limits.max_resource_bytes)?;
1727            }
1728            (None, Some(view_index)) => {
1729                if image.mime_type.is_none() {
1730                    return Err(GltfError::InvalidGltf(format!(
1731                        "Buffer-view image {index} has no mimeType"
1732                    )));
1733                }
1734                let view = root.buffer_views.get(view_index).ok_or_else(|| {
1735                    GltfError::InvalidGltf(format!(
1736                        "Image {index} references invalid bufferView {view_index}"
1737                    ))
1738                })?;
1739                let buffer = buffers.get(view.buffer).ok_or_else(|| {
1740                    GltfError::InvalidGltf(format!(
1741                        "Image {index} bufferView references invalid buffer {}",
1742                        view.buffer
1743                    ))
1744                })?;
1745                let start = view.byte_offset.unwrap_or(0);
1746                let end = start.checked_add(view.byte_length).ok_or_else(|| {
1747                    GltfError::InvalidGltf(format!("Image {index} byte range overflow"))
1748                })?;
1749                if end > buffer.len() {
1750                    return Err(GltfError::InvalidGltf(format!(
1751                        "Image {index} extends past its buffer"
1752                    )));
1753                }
1754            }
1755        }
1756    }
1757    Ok(())
1758}
1759
1760fn accessor_num_components(accessor_type: &str) -> Result<u8> {
1761    match accessor_type {
1762        "SCALAR" => Ok(1),
1763        "VEC2" => Ok(2),
1764        "VEC3" => Ok(3),
1765        "VEC4" => Ok(4),
1766        _ => Err(GltfError::Unsupported(format!(
1767            "Unsupported accessor type: {}",
1768            accessor_type
1769        ))),
1770    }
1771}
1772
1773fn data_type_for_component_type(component_type: u32) -> Result<DataType> {
1774    match component_type {
1775        GLTF_COMPONENT_BYTE => Ok(DataType::Int8),
1776        GLTF_COMPONENT_UNSIGNED_BYTE => Ok(DataType::Uint8),
1777        GLTF_COMPONENT_SHORT => Ok(DataType::Int16),
1778        GLTF_COMPONENT_UNSIGNED_SHORT => Ok(DataType::Uint16),
1779        GLTF_COMPONENT_UNSIGNED_INT => Ok(DataType::Uint32),
1780        GLTF_COMPONENT_FLOAT => Ok(DataType::Float32),
1781        _ => Err(GltfError::Unsupported(format!(
1782            "Unsupported component type: {}",
1783            component_type
1784        ))),
1785    }
1786}
1787
1788// Implement the Reader trait for glTF/GLB files. Decodes all primitives
1789// (Draco-compressed and standard) and returns them as meshes.
1790impl crate::traits::Reader for GltfReader {
1791    fn open<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
1792        GltfReader::open(path).map_err(|e| std::io::Error::other(e.to_string()))
1793    }
1794
1795    fn read_meshes(&mut self) -> std::io::Result<Vec<draco_core::mesh::Mesh>> {
1796        self.decode_all_meshes()
1797            .map_err(|e| std::io::Error::other(e.to_string()))
1798    }
1799}
1800
1801impl ReadFromBytes for GltfReader {
1802    fn from_bytes(bytes: &[u8]) -> io::Result<Self> {
1803        GltfReader::from_bytes(bytes).map_err(|e| io::Error::other(e.to_string()))
1804    }
1805}
1806
1807impl GltfReader {
1808    /// Decode all primitives (both Draco and standard) as meshes.
1809    pub fn decode_all_meshes(&self) -> Result<Vec<Mesh>> {
1810        let mut result = Vec::new();
1811
1812        for (mesh_idx, gltf_mesh) in self.root.meshes.iter().enumerate() {
1813            for (prim_idx, primitive) in gltf_mesh.primitives.iter().enumerate() {
1814                let mesh = self.decode_primitive_mesh(mesh_idx, gltf_mesh, prim_idx, primitive)?;
1815                result.push(mesh);
1816            }
1817        }
1818
1819        Ok(result)
1820    }
1821
1822    /// Compute a node's local transform as a row-major 4x4 matrix.
1823    fn compute_node_transform(node: &GltfNode) -> Option<crate::scene::Transform> {
1824        if let Some(m) = &node.matrix {
1825            // glTF stores column-major; convert to row-major
1826            Some(crate::scene::Transform {
1827                matrix: [
1828                    [m[0], m[4], m[8], m[12]],
1829                    [m[1], m[5], m[9], m[13]],
1830                    [m[2], m[6], m[10], m[14]],
1831                    [m[3], m[7], m[11], m[15]],
1832                ],
1833            })
1834        } else if node.translation.is_some() || node.rotation.is_some() || node.scale.is_some() {
1835            // Compose T * R * S
1836            let t = node.translation.unwrap_or([0.0, 0.0, 0.0]);
1837            let r = node.rotation.unwrap_or([0.0, 0.0, 0.0, 1.0]); // [x, y, z, w]
1838            let s = node.scale.unwrap_or([1.0, 1.0, 1.0]);
1839
1840            // Quaternion to rotation matrix (row-major)
1841            let (qx, qy, qz, qw) = (r[0], r[1], r[2], r[3]);
1842            let xx = qx * qx;
1843            let yy = qy * qy;
1844            let zz = qz * qz;
1845            let xy = qx * qy;
1846            let xz = qx * qz;
1847            let yz = qy * qz;
1848            let wx = qw * qx;
1849            let wy = qw * qy;
1850            let wz = qw * qz;
1851
1852            // Rotation matrix (row-major)
1853            let rot = [
1854                [1.0 - 2.0 * (yy + zz), 2.0 * (xy - wz), 2.0 * (xz + wy)],
1855                [2.0 * (xy + wz), 1.0 - 2.0 * (xx + zz), 2.0 * (yz - wx)],
1856                [2.0 * (xz - wy), 2.0 * (yz + wx), 1.0 - 2.0 * (xx + yy)],
1857            ];
1858
1859            // Compose T * R * S into 4x4 row-major
1860            Some(crate::scene::Transform {
1861                matrix: [
1862                    [rot[0][0] * s[0], rot[0][1] * s[1], rot[0][2] * s[2], t[0]],
1863                    [rot[1][0] * s[0], rot[1][1] * s[1], rot[1][2] * s[2], t[1]],
1864                    [rot[2][0] * s[0], rot[2][1] * s[1], rot[2][2] * s[2], t[2]],
1865                    [0.0, 0.0, 0.0, 1.0],
1866                ],
1867            })
1868        } else {
1869            None
1870        }
1871    }
1872
1873    /// Recursively build a SceneNode from a glTF node index.
1874    fn build_scene_node(
1875        &self,
1876        node_idx: usize,
1877        visited: &mut Vec<bool>,
1878    ) -> Result<crate::scene::SceneNode> {
1879        if node_idx >= self.root.nodes.len() {
1880            return Err(GltfError::InvalidGltf(format!(
1881                "Invalid node index: {}",
1882                node_idx
1883            )));
1884        }
1885
1886        // Cycle detection
1887        if visited[node_idx] {
1888            return Err(GltfError::InvalidGltf(format!(
1889                "Cycle detected at node {}",
1890                node_idx
1891            )));
1892        }
1893        visited[node_idx] = true;
1894
1895        let gltf_node = &self.root.nodes[node_idx];
1896
1897        let mut scene_node = crate::scene::SceneNode::new(gltf_node.name.clone());
1898        scene_node.transform = Self::compute_node_transform(gltf_node);
1899
1900        // Attach meshes if this node references a mesh
1901        if let Some(mesh_idx) = gltf_node.mesh {
1902            if let Some(gltf_mesh) = self.root.meshes.get(mesh_idx) {
1903                for (prim_idx, primitive) in gltf_mesh.primitives.iter().enumerate() {
1904                    let mesh =
1905                        self.decode_primitive_mesh(mesh_idx, gltf_mesh, prim_idx, primitive)?;
1906
1907                    let mesh_instance_name = if gltf_mesh.primitives.len() > 1 {
1908                        gltf_mesh
1909                            .name
1910                            .as_ref()
1911                            .map(|n| format!("{}_{}", n, prim_idx))
1912                    } else {
1913                        gltf_mesh.name.clone()
1914                    };
1915
1916                    scene_node.mesh_instances.push(crate::scene::MeshInstance {
1917                        name: mesh_instance_name,
1918                        mesh,
1919                        transform: None, // Primitive-level transform is identity
1920                    });
1921                }
1922            }
1923        }
1924
1925        // Recursively build children
1926        for &child_idx in &gltf_node.children {
1927            let child_node = self.build_scene_node(child_idx, visited)?;
1928            scene_node.children.push(child_node);
1929        }
1930
1931        Ok(scene_node)
1932    }
1933
1934    fn root_node_indices_without_scenes(&self) -> Vec<usize> {
1935        let mut is_child = vec![false; self.root.nodes.len()];
1936        for node in &self.root.nodes {
1937            for &child_idx in &node.children {
1938                if child_idx < is_child.len() {
1939                    is_child[child_idx] = true;
1940                }
1941            }
1942        }
1943
1944        (0..self.root.nodes.len())
1945            .filter(|&i| !is_child[i])
1946            .collect()
1947    }
1948
1949    fn build_scene_from_roots(
1950        &self,
1951        name: Option<String>,
1952        root_node_indices: &[usize],
1953    ) -> Result<crate::scene::Scene> {
1954        let mut visited = vec![false; self.root.nodes.len()];
1955        let mut root_nodes = Vec::with_capacity(root_node_indices.len());
1956        for &node_idx in root_node_indices {
1957            root_nodes.push(self.build_scene_node(node_idx, &mut visited)?);
1958        }
1959
1960        Ok(crate::scene::Scene { name, root_nodes })
1961    }
1962
1963    fn read_scene_result(&self) -> Result<crate::scene::Scene> {
1964        let scene_idx = self.root.scene.or({
1965            if self.root.scenes.is_empty() {
1966                None
1967            } else {
1968                Some(0)
1969            }
1970        });
1971
1972        if let Some(idx) = scene_idx {
1973            let gltf_scene =
1974                self.root.scenes.get(idx).ok_or_else(|| {
1975                    GltfError::InvalidGltf(format!("Invalid scene index: {}", idx))
1976                })?;
1977            self.build_scene_from_roots(gltf_scene.name.clone(), &gltf_scene.nodes)
1978        } else {
1979            let roots = self.root_node_indices_without_scenes();
1980            self.build_scene_from_roots(None, &roots)
1981        }
1982    }
1983
1984    fn read_scenes_result(&self) -> Result<Vec<crate::scene::Scene>> {
1985        if self.root.scenes.is_empty() {
1986            let roots = self.root_node_indices_without_scenes();
1987            return self
1988                .build_scene_from_roots(None, &roots)
1989                .map(|scene| vec![scene]);
1990        }
1991
1992        self.root
1993            .scenes
1994            .iter()
1995            .map(|scene| self.build_scene_from_roots(scene.name.clone(), &scene.nodes))
1996            .collect()
1997    }
1998}
1999
2000impl crate::scene::SceneReader for GltfReader {
2001    fn read_scene(&mut self) -> std::io::Result<crate::scene::Scene> {
2002        self.read_scene_result()
2003            .map_err(|e| std::io::Error::other(e.to_string()))
2004    }
2005
2006    fn read_scenes(&mut self) -> std::io::Result<Vec<crate::scene::Scene>> {
2007        self.read_scenes_result()
2008            .map_err(|e| std::io::Error::other(e.to_string()))
2009    }
2010}
2011// ============================================================================
2012// Tests
2013// ============================================================================
2014
2015#[cfg(test)]
2016mod tests {
2017    use super::*;
2018    use draco_core::draco_types::DataType;
2019    use draco_core::geometry_attribute::GeometryAttributeType;
2020    #[cfg(feature = "gltf-writer")]
2021    use draco_core::geometry_attribute::PointAttribute;
2022    use draco_core::mesh::Mesh;
2023    use serde_json::Value;
2024    use tempfile::tempdir;
2025
2026    fn build_glb(json: &str) -> Vec<u8> {
2027        let document: serde_json::Value = serde_json::from_str(json).unwrap();
2028        crate::gltf_container::build_glb_container(&document, &[]).unwrap()
2029    }
2030
2031    fn triangle_positions() -> Vec<u8> {
2032        [
2033            0.0f32, 0.0, 0.0, //
2034            1.0, 0.0, 0.0, //
2035            0.0, 1.0, 0.0,
2036        ]
2037        .into_iter()
2038        .flat_map(f32::to_le_bytes)
2039        .collect()
2040    }
2041
2042    #[cfg(feature = "gltf-writer")]
2043    fn triangle_mesh() -> Mesh {
2044        let mut mesh = Mesh::new();
2045        mesh.set_num_points(3);
2046        mesh.add_face([
2047            draco_core::geometry_indices::PointIndex(0),
2048            draco_core::geometry_indices::PointIndex(1),
2049            draco_core::geometry_indices::PointIndex(2),
2050        ]);
2051
2052        let mut positions = PointAttribute::new();
2053        positions.init(
2054            GeometryAttributeType::Position,
2055            3,
2056            DataType::Float32,
2057            false,
2058            3,
2059        );
2060        positions.buffer_mut().write(0, &triangle_positions());
2061        mesh.add_attribute(positions);
2062        mesh
2063    }
2064
2065    #[cfg(feature = "gltf-writer")]
2066    fn writer_gltf_json_value() -> serde_json::Value {
2067        let mut writer = crate::gltf_writer::GltfWriter::new();
2068        writer
2069            .add_draco_mesh(&triangle_mesh(), Some("triangle"), None)
2070            .unwrap();
2071        serde_json::from_str(&writer.to_gltf_embedded().unwrap()).unwrap()
2072    }
2073
2074    fn read_attribute_bytes(mesh: &Mesh, attribute_type: GeometryAttributeType) -> Vec<u8> {
2075        mesh.named_attribute(attribute_type)
2076            .expect("missing attribute")
2077            .buffer()
2078            .data()
2079            .to_vec()
2080    }
2081
2082    #[test]
2083    fn test_minimal_gltf_json() {
2084        let json = r#"{
2085            "asset": {"version": "2.0"},
2086            "meshes": [],
2087            "buffers": [],
2088            "bufferViews": [],
2089            "accessors": []
2090        }"#;
2091
2092        let root: GltfRoot = serde_json::from_str(json).unwrap();
2093        assert!(root.meshes.is_empty());
2094        assert!(root.buffers.is_empty());
2095    }
2096
2097    #[test]
2098    fn test_read_scenes_returns_all_gltf_scenes() {
2099        use crate::scene::SceneReader;
2100
2101        let json = r#"{
2102            "asset": {"version": "2.0"},
2103            "scene": 1,
2104            "scenes": [
2105                {"name": "Preview", "nodes": [0]},
2106                {"name": "Full", "nodes": [1, 2]}
2107            ],
2108            "nodes": [
2109                {"name": "PreviewRoot"},
2110                {"name": "FullRootA"},
2111                {"name": "FullRootB"}
2112            ]
2113        }"#;
2114
2115        let mut reader = GltfReader::from_gltf(json.as_bytes(), None).unwrap();
2116        let default_scene = reader.read_scene().unwrap();
2117        assert_eq!(default_scene.name, Some("Full".to_string()));
2118        assert_eq!(default_scene.root_nodes.len(), 2);
2119        assert_eq!(
2120            default_scene.root_nodes[0].name,
2121            Some("FullRootA".to_string())
2122        );
2123
2124        let scenes = reader.read_scenes().unwrap();
2125        assert_eq!(scenes.len(), 2);
2126        assert_eq!(scenes[0].name, Some("Preview".to_string()));
2127        assert_eq!(scenes[0].root_nodes.len(), 1);
2128        assert_eq!(
2129            scenes[0].root_nodes[0].name,
2130            Some("PreviewRoot".to_string())
2131        );
2132        assert_eq!(scenes[1].name, Some("Full".to_string()));
2133        assert_eq!(scenes[1].root_nodes.len(), 2);
2134    }
2135
2136    #[test]
2137    fn test_gltf_with_draco_extension() {
2138        let json = r#"{
2139            "asset": {"version": "2.0"},
2140            "extensionsUsed": ["KHR_draco_mesh_compression"],
2141            "extensionsRequired": ["KHR_draco_mesh_compression"],
2142            "meshes": [{
2143                "name": "TestMesh",
2144                "primitives": [{
2145                    "attributes": {"POSITION": 0},
2146                    "extensions": {
2147                        "KHR_draco_mesh_compression": {
2148                            "bufferView": 0,
2149                            "attributes": {"POSITION": 0}
2150                        }
2151                    }
2152                }]
2153            }],
2154            "buffers": [{"byteLength": 3, "uri": "data:application/octet-stream;base64,AAAA"}],
2155            "bufferViews": [{"buffer": 0, "byteLength": 3}],
2156            "accessors": [{"componentType": 5126, "count": 1, "type": "VEC3"}]
2157        }"#;
2158
2159        let reader = GltfReader::from_gltf(json.as_bytes(), None).unwrap();
2160        assert!(reader.has_draco_extension());
2161        assert_eq!(reader.num_meshes(), 1);
2162
2163        let primitives = reader.draco_primitives();
2164        assert_eq!(primitives.len(), 1);
2165        assert_eq!(primitives[0].mesh_name, Some("TestMesh".to_string()));
2166        assert_eq!(primitives[0].buffer_view, 0);
2167    }
2168
2169    #[test]
2170    fn typed_khr_parser_rejects_malformed_schema_and_missing_side_fallback() {
2171        let base = serde_json::json!({
2172            "asset": {"version": "2.0"},
2173            "extensionsUsed": [KHR_DRACO_MESH_COMPRESSION],
2174            "extensionsRequired": [KHR_DRACO_MESH_COMPRESSION],
2175            "meshes": [{"primitives": [{
2176                "attributes": {"POSITION": 0},
2177                "extensions": {KHR_DRACO_MESH_COMPRESSION: {
2178                    "bufferView": 0,
2179                    "attributes": {"POSITION": 10}
2180                }}
2181            }]}],
2182            "buffers": [{"byteLength": 4, "uri": "data:;base64,AAAAAA=="}],
2183            "bufferViews": [{"buffer": 0, "byteLength": 4}],
2184            "accessors": [{"componentType": 5126, "count": 1, "type": "VEC3"}]
2185        });
2186
2187        let mut malformed = base.clone();
2188        malformed["meshes"][0]["primitives"][0]["extensions"][KHR_DRACO_MESH_COMPRESSION]
2189            ["unexpected"] = serde_json::json!(true);
2190        assert!(GltfReader::from_gltf(&serde_json::to_vec(&malformed).unwrap(), None).is_err());
2191
2192        let mut too_large = base.clone();
2193        too_large["meshes"][0]["primitives"][0]["extensions"][KHR_DRACO_MESH_COMPRESSION]
2194            ["attributes"]["POSITION"] = serde_json::Value::from(u64::from(u32::MAX) + 1);
2195        assert!(GltfReader::from_gltf(&serde_json::to_vec(&too_large).unwrap(), None).is_err());
2196
2197        let mut empty = base.clone();
2198        empty["meshes"][0]["primitives"][0]["extensions"][KHR_DRACO_MESH_COMPRESSION]
2199            ["attributes"] = serde_json::json!({});
2200        assert!(GltfReader::from_gltf(&serde_json::to_vec(&empty).unwrap(), None).is_err());
2201
2202        let mut side_attribute = base;
2203        side_attribute["meshes"][0]["primitives"][0]["attributes"]["NORMAL"] =
2204            serde_json::Value::from(1);
2205        side_attribute["accessors"]
2206            .as_array_mut()
2207            .unwrap()
2208            .push(serde_json::json!({
2209                "componentType": 5126,
2210                "count": 1,
2211                "type": "VEC3"
2212            }));
2213        assert!(
2214            GltfReader::from_gltf(&serde_json::to_vec(&side_attribute).unwrap(), None).is_err()
2215        );
2216    }
2217
2218    #[test]
2219    fn test_rejects_unknown_required_extension() {
2220        let json = r#"{
2221            "asset": {"version": "2.0"},
2222            "extensionsUsed": ["EXT_required"],
2223            "extensionsRequired": ["EXT_required"]
2224        }"#;
2225
2226        assert!(matches!(
2227            GltfReader::from_gltf(json.as_bytes(), None),
2228            Err(GltfError::Unsupported(_))
2229        ));
2230    }
2231
2232    #[test]
2233    fn test_rejects_draco_primitive_missing_extensions_used() {
2234        let json = r#"{
2235            "asset": {"version": "2.0"},
2236            "meshes": [{
2237                "primitives": [{
2238                    "attributes": {"POSITION": 0},
2239                    "extensions": {
2240                        "KHR_draco_mesh_compression": {
2241                            "bufferView": 0,
2242                            "attributes": {"POSITION": 0}
2243                        }
2244                    }
2245                }]
2246            }],
2247            "buffers": [{"byteLength": 3, "uri": "data:application/octet-stream;base64,AAAA"}],
2248            "bufferViews": [{"buffer": 0, "byteLength": 3}],
2249            "accessors": []
2250        }"#;
2251
2252        assert!(matches!(
2253            GltfReader::from_gltf(json.as_bytes(), None),
2254            Err(GltfError::InvalidGltf(_))
2255        ));
2256    }
2257
2258    #[test]
2259    fn test_glb_open_loads_relative_external_buffer() {
2260        let dir = tempdir().unwrap();
2261        let bin_path = dir.path().join("mesh.bin");
2262        std::fs::write(&bin_path, triangle_positions()).unwrap();
2263
2264        let json = r#"{
2265            "asset": {"version": "2.0"},
2266            "buffers": [{"byteLength": 36, "uri": "mesh.bin"}],
2267            "bufferViews": [{"buffer": 0, "byteOffset": 0, "byteLength": 36}],
2268            "accessors": [{
2269                "bufferView": 0,
2270                "componentType": 5126,
2271                "count": 3,
2272                "type": "VEC3"
2273            }],
2274            "meshes": [{
2275                "primitives": [{
2276                    "attributes": {"POSITION": 0},
2277                    "mode": 4
2278                }]
2279            }]
2280        }"#;
2281        let glb = build_glb(json);
2282        let glb_path = dir.path().join("external.glb");
2283        std::fs::write(&glb_path, &glb).unwrap();
2284
2285        let reader = GltfReader::open(&glb_path).unwrap();
2286        let meshes = reader.decode_all_meshes().unwrap();
2287
2288        assert_eq!(meshes.len(), 1);
2289        assert_eq!(meshes[0].num_points(), 3);
2290        assert_eq!(meshes[0].num_faces(), 1);
2291        assert_eq!(
2292            read_attribute_bytes(&meshes[0], GeometryAttributeType::Position),
2293            triangle_positions()
2294        );
2295    }
2296
2297    #[test]
2298    fn test_from_glb_rejects_external_buffer_without_base_path() {
2299        let json = r#"{
2300            "asset": {"version": "2.0"},
2301            "buffers": [{"byteLength": 36, "uri": "mesh.bin"}],
2302            "bufferViews": [{"buffer": 0, "byteOffset": 0, "byteLength": 36}],
2303            "accessors": [],
2304            "meshes": []
2305        }"#;
2306
2307        let err = match GltfReader::from_glb(&build_glb(json)) {
2308            Ok(_) => panic!("external buffer unexpectedly loaded without a base path"),
2309            Err(err) => err,
2310        };
2311        assert!(matches!(err, GltfError::ExternalResourceDenied(_)));
2312    }
2313
2314    #[test]
2315    fn external_images_are_resolved_and_reported_without_reparsing() {
2316        let json = br#"{
2317            "asset": {"version": "2.0"},
2318            "images": [{"uri": "textures%2Falbedo.png"}]
2319        }"#;
2320        assert!(matches!(
2321            GltfReader::from_gltf(json, None),
2322            Err(GltfError::ExternalResourceDenied(uri)) if uri == "textures%2Falbedo.png"
2323        ));
2324
2325        let resolver = |uri: &str| -> Result<Vec<u8>> {
2326            if uri == "textures%2Falbedo.png" {
2327                Ok(vec![1, 2, 3])
2328            } else {
2329                Err(GltfError::ExternalResourceDenied(uri.to_owned()))
2330            }
2331        };
2332        let reader =
2333            GltfReader::from_bytes_with_resolver(json, &resolver, &ResourceLimits::default())
2334                .unwrap();
2335        assert_eq!(
2336            reader.document_metadata().external_resource_uris,
2337            ["textures%2Falbedo.png"]
2338        );
2339
2340        let limits = ResourceLimits {
2341            max_resource_bytes: Some(2),
2342            ..ResourceLimits::default()
2343        };
2344        assert!(matches!(
2345            GltfReader::from_bytes_with_resolver(json, &resolver, &limits),
2346            Err(GltfError::ResourceLimitExceeded(_))
2347        ));
2348    }
2349
2350    #[test]
2351    fn test_texcoord_unsigned_short_normalized_vec2() {
2352        let mut bytes = triangle_positions();
2353        let texcoords = [0u16, 0, 65535, 0, 0, 65535];
2354        bytes.extend(texcoords.into_iter().flat_map(u16::to_le_bytes));
2355        let data_uri = format!(
2356            "data:application/octet-stream;base64,{}",
2357            base64_for_test(&bytes)
2358        );
2359        let json = format!(
2360            r#"{{
2361                "asset": {{"version": "2.0"}},
2362                "buffers": [{{"byteLength": {}, "uri": "{}"}}],
2363                "bufferViews": [
2364                    {{"buffer": 0, "byteOffset": 0, "byteLength": 36}},
2365                    {{"buffer": 0, "byteOffset": 36, "byteLength": 12}}
2366                ],
2367                "accessors": [
2368                    {{"bufferView": 0, "componentType": 5126, "count": 3, "type": "VEC3"}},
2369                    {{
2370                        "bufferView": 1,
2371                        "componentType": 5123,
2372                        "normalized": true,
2373                        "count": 3,
2374                        "type": "VEC2"
2375                    }}
2376                ],
2377                "meshes": [{{
2378                    "primitives": [{{
2379                        "attributes": {{"POSITION": 0, "TEXCOORD_0": 1}},
2380                        "mode": 4
2381                    }}]
2382                }}]
2383            }}"#,
2384            bytes.len(),
2385            data_uri
2386        );
2387
2388        let mesh = GltfReader::from_gltf(json.as_bytes(), None)
2389            .unwrap()
2390            .decode_all_meshes()
2391            .unwrap()
2392            .remove(0);
2393        let texcoord = mesh
2394            .named_attribute(GeometryAttributeType::TexCoord)
2395            .expect("missing texcoord");
2396
2397        assert_eq!(texcoord.data_type(), DataType::Uint16);
2398        assert!(texcoord.normalized());
2399        assert_eq!(texcoord.num_components(), 2);
2400        assert_eq!(texcoord.buffer().data(), &bytes[36..48]);
2401    }
2402
2403    #[test]
2404    fn test_color_unsigned_byte_normalized_vec3() {
2405        let mut bytes = triangle_positions();
2406        let colors = [255u8, 0, 0, 0, 255, 0, 0, 0, 255];
2407        bytes.extend(colors);
2408        let data_uri = format!(
2409            "data:application/octet-stream;base64,{}",
2410            base64_for_test(&bytes)
2411        );
2412        let json = format!(
2413            r#"{{
2414                "asset": {{"version": "2.0"}},
2415                "buffers": [{{"byteLength": {}, "uri": "{}"}}],
2416                "bufferViews": [
2417                    {{"buffer": 0, "byteOffset": 0, "byteLength": 36}},
2418                    {{"buffer": 0, "byteOffset": 36, "byteLength": 9}}
2419                ],
2420                "accessors": [
2421                    {{"bufferView": 0, "componentType": 5126, "count": 3, "type": "VEC3"}},
2422                    {{
2423                        "bufferView": 1,
2424                        "componentType": 5121,
2425                        "normalized": true,
2426                        "count": 3,
2427                        "type": "VEC3"
2428                    }}
2429                ],
2430                "meshes": [{{
2431                    "primitives": [{{
2432                        "attributes": {{"POSITION": 0, "COLOR_0": 1}},
2433                        "mode": 4
2434                    }}]
2435                }}]
2436            }}"#,
2437            bytes.len(),
2438            data_uri
2439        );
2440
2441        let mesh = GltfReader::from_gltf(json.as_bytes(), None)
2442            .unwrap()
2443            .decode_all_meshes()
2444            .unwrap()
2445            .remove(0);
2446        let color = mesh
2447            .named_attribute(GeometryAttributeType::Color)
2448            .expect("missing color");
2449
2450        assert_eq!(color.data_type(), DataType::Uint8);
2451        assert!(color.normalized());
2452        assert_eq!(color.num_components(), 3);
2453        assert_eq!(color.buffer().data(), &bytes[36..45]);
2454    }
2455
2456    #[test]
2457    fn test_points_primitive_decodes_without_faces() {
2458        let indices = [2u16, 0];
2459        let mut bytes = triangle_positions();
2460        bytes.extend(indices.into_iter().flat_map(u16::to_le_bytes));
2461        let data_uri = format!(
2462            "data:application/octet-stream;base64,{}",
2463            base64_for_test(&bytes)
2464        );
2465        let json = format!(
2466            r#"{{
2467                "asset": {{"version": "2.0"}},
2468                "buffers": [{{"byteLength": {}, "uri": "{}"}}],
2469                "bufferViews": [
2470                    {{"buffer": 0, "byteOffset": 0, "byteLength": 36}},
2471                    {{"buffer": 0, "byteOffset": 36, "byteLength": 4}}
2472                ],
2473                "accessors": [
2474                    {{"bufferView": 0, "componentType": 5126, "count": 3, "type": "VEC3"}},
2475                    {{"bufferView": 1, "componentType": 5123, "count": 2, "type": "SCALAR"}}
2476                ],
2477                "meshes": [{{
2478                    "primitives": [{{
2479                        "attributes": {{"POSITION": 0}},
2480                        "indices": 1,
2481                        "mode": 0
2482                    }}]
2483                }}]
2484            }}"#,
2485            bytes.len(),
2486            data_uri
2487        );
2488
2489        let mesh = GltfReader::from_gltf(json.as_bytes(), None)
2490            .unwrap()
2491            .decode_all_meshes()
2492            .unwrap()
2493            .remove(0);
2494
2495        assert_eq!(mesh.num_points(), 2);
2496        assert_eq!(mesh.num_faces(), 0);
2497        let positions = read_attribute_bytes(&mesh, GeometryAttributeType::Position);
2498        assert_eq!(&positions[0..12], &triangle_positions()[24..36]);
2499        assert_eq!(&positions[12..24], &triangle_positions()[0..12]);
2500    }
2501
2502    #[cfg(feature = "gltf-writer")]
2503    #[test]
2504    fn test_writer_glb_roundtrips_through_reader() {
2505        let dir = tempdir().unwrap();
2506        let path = dir.path().join("roundtrip.glb");
2507
2508        let mut writer = crate::gltf_writer::GltfWriter::new();
2509        writer
2510            .add_draco_mesh(&triangle_mesh(), Some("triangle"), None)
2511            .unwrap();
2512        writer.write_glb(&path).unwrap();
2513
2514        let reader = GltfReader::open(&path).unwrap();
2515        let primitives = reader.draco_primitives();
2516        assert_eq!(primitives.len(), 1);
2517        assert_eq!(primitives[0].attributes.get("POSITION"), Some(&0));
2518
2519        let decoded = reader.decode_all_meshes().unwrap().remove(0);
2520        let position = decoded
2521            .named_attribute(GeometryAttributeType::Position)
2522            .expect("missing position");
2523        assert_eq!(position.data_type(), DataType::Float32);
2524        assert_eq!(position.num_components(), 3);
2525        assert_eq!(decoded.num_faces(), 1);
2526    }
2527
2528    #[cfg(feature = "gltf-writer")]
2529    #[test]
2530    fn test_draco_decode_rejects_extension_attribute_not_in_primitive_attributes() {
2531        let mut value = writer_gltf_json_value();
2532        value["meshes"][0]["primitives"][0]["extensions"]["KHR_draco_mesh_compression"]
2533            ["attributes"]["TEXCOORD_0"] = serde_json::json!(0);
2534        let json = serde_json::to_string(&value).unwrap();
2535
2536        let err = match GltfReader::from_gltf(json.as_bytes(), None) {
2537            Err(error) => error,
2538            Ok(_) => panic!("malformed extension unexpectedly parsed"),
2539        };
2540        assert!(matches!(err, GltfError::InvalidGltf(_)));
2541    }
2542
2543    #[cfg(feature = "gltf-writer")]
2544    #[test]
2545    fn test_draco_decode_rejects_accessor_metadata_mismatch_and_sparse() {
2546        let mut count_mismatch = writer_gltf_json_value();
2547        let pos_accessor = count_mismatch["meshes"][0]["primitives"][0]["attributes"]["POSITION"]
2548            .as_u64()
2549            .unwrap() as usize;
2550        count_mismatch["accessors"][pos_accessor]["count"] = serde_json::json!(99);
2551        let json = serde_json::to_string(&count_mismatch).unwrap();
2552        let err = GltfReader::from_gltf(json.as_bytes(), None)
2553            .unwrap()
2554            .decode_all_meshes()
2555            .unwrap_err();
2556        assert!(matches!(err, GltfError::InvalidGltf(_)));
2557
2558        let mut sparse = writer_gltf_json_value();
2559        let pos_accessor = sparse["meshes"][0]["primitives"][0]["attributes"]["POSITION"]
2560            .as_u64()
2561            .unwrap() as usize;
2562        sparse["accessors"][pos_accessor]["sparse"] = serde_json::json!({
2563            "count": 1,
2564            "indices": {"bufferView": 0, "componentType": 5123},
2565            "values": {"bufferView": 0}
2566        });
2567        let json = serde_json::to_string(&sparse).unwrap();
2568        let err = GltfReader::from_gltf(json.as_bytes(), None)
2569            .unwrap()
2570            .decode_all_meshes()
2571            .unwrap_err();
2572        assert!(matches!(err, GltfError::Unsupported(_)));
2573    }
2574
2575    #[test]
2576    fn test_standard_triangle_rejects_out_of_bounds_indices() {
2577        let indices = [0u16, 1, 3];
2578        let mut bytes = triangle_positions();
2579        bytes.extend(indices.into_iter().flat_map(u16::to_le_bytes));
2580        let data_uri = format!(
2581            "data:application/octet-stream;base64,{}",
2582            base64_for_test(&bytes)
2583        );
2584        let json = format!(
2585            r#"{{
2586                "asset": {{"version": "2.0"}},
2587                "buffers": [{{"byteLength": {}, "uri": "{}"}}],
2588                "bufferViews": [
2589                    {{"buffer": 0, "byteOffset": 0, "byteLength": 36}},
2590                    {{"buffer": 0, "byteOffset": 36, "byteLength": 6}}
2591                ],
2592                "accessors": [
2593                    {{"bufferView": 0, "componentType": 5126, "count": 3, "type": "VEC3"}},
2594                    {{"bufferView": 1, "componentType": 5123, "count": 3, "type": "SCALAR"}}
2595                ],
2596                "meshes": [{{
2597                    "primitives": [{{
2598                        "attributes": {{"POSITION": 0}},
2599                        "indices": 1,
2600                        "mode": 4
2601                    }}]
2602                }}]
2603            }}"#,
2604            bytes.len(),
2605            data_uri
2606        );
2607
2608        let err = GltfReader::from_gltf(json.as_bytes(), None)
2609            .unwrap()
2610            .decode_all_meshes()
2611            .unwrap_err();
2612        assert!(matches!(err, GltfError::InvalidGltf(_)));
2613    }
2614
2615    #[test]
2616    fn semantic_and_index_normalized_contracts_are_strict() {
2617        fn document_with_attribute(
2618            semantic: &str,
2619            accessor_type: &str,
2620            component_type: u32,
2621            normalized: bool,
2622        ) -> Vec<u8> {
2623            let components = match accessor_type {
2624                "VEC3" => 3,
2625                "VEC4" => 4,
2626                _ => 1,
2627            };
2628            let component_size = if component_type == GLTF_COMPONENT_FLOAT {
2629                4
2630            } else {
2631                1
2632            };
2633            let mut bytes = triangle_positions();
2634            let extra_offset = bytes.len();
2635            bytes.resize(extra_offset + 3 * components * component_size, 0);
2636            let document = serde_json::json!({
2637                "asset": {"version": "2.0"},
2638                "buffers": [{
2639                    "byteLength": bytes.len(),
2640                    "uri": format!("data:application/octet-stream;base64,{}", base64_for_test(&bytes))
2641                }],
2642                "bufferViews": [
2643                    {"buffer": 0, "byteOffset": 0, "byteLength": 36},
2644                    {"buffer": 0, "byteOffset": extra_offset, "byteLength": bytes.len() - extra_offset}
2645                ],
2646                "accessors": [
2647                    {"bufferView": 0, "componentType": 5126, "count": 3, "type": "VEC3"},
2648                    {"bufferView": 1, "componentType": component_type, "normalized": normalized,
2649                     "count": 3, "type": accessor_type}
2650                ],
2651                "meshes": [{"primitives": [{
2652                    "attributes": {"POSITION": 0, (semantic): 1}, "mode": 4
2653                }]}]
2654            });
2655            serde_json::to_vec(&document).unwrap()
2656        }
2657
2658        for document in [
2659            document_with_attribute("TANGENT", "VEC3", 5126, false),
2660            document_with_attribute("JOINTS_0", "VEC4", 5126, false),
2661            document_with_attribute("WEIGHTS_0", "VEC4", 5121, false),
2662        ] {
2663            let error = GltfReader::from_bytes_lenient(&document)
2664                .unwrap()
2665                .decode_all_meshes()
2666                .unwrap_err();
2667            assert!(matches!(
2668                error,
2669                GltfError::InvalidGltf(_) | GltfError::Unsupported(_)
2670            ));
2671        }
2672
2673        let mut bytes = triangle_positions();
2674        let indices_offset = bytes.len();
2675        bytes.extend([0u16, 1, 2].into_iter().flat_map(u16::to_le_bytes));
2676        let document = serde_json::json!({
2677            "asset": {"version": "2.0"},
2678            "buffers": [{
2679                "byteLength": bytes.len(),
2680                "uri": format!("data:application/octet-stream;base64,{}", base64_for_test(&bytes))
2681            }],
2682            "bufferViews": [
2683                {"buffer": 0, "byteOffset": 0, "byteLength": 36},
2684                {"buffer": 0, "byteOffset": indices_offset, "byteLength": 6}
2685            ],
2686            "accessors": [
2687                {"bufferView": 0, "componentType": 5126, "count": 3, "type": "VEC3"},
2688                {"bufferView": 1, "componentType": 5123, "normalized": true,
2689                 "count": 3, "type": "SCALAR"}
2690            ],
2691            "meshes": [{"primitives": [{
2692                "attributes": {"POSITION": 0}, "indices": 1, "mode": 4
2693            }]}]
2694        });
2695        let error = GltfReader::from_bytes_lenient(&serde_json::to_vec(&document).unwrap())
2696            .unwrap()
2697            .decode_all_meshes()
2698            .unwrap_err();
2699        assert!(matches!(error, GltfError::InvalidGltf(_)));
2700    }
2701
2702    #[test]
2703    fn lenient_reader_validates_scene_animation_and_skin_references() {
2704        let valid = serde_json::json!({
2705            "asset": {"version": "2.0"},
2706            "scene": 0,
2707            "scenes": [{"nodes": [0]}],
2708            "nodes": [{"mesh": 0, "skin": 0, "children": [1]}, {}],
2709            "meshes": [{"primitives": [{"attributes": {"POSITION": 0}, "mode": 4}]}],
2710            "accessors": [
2711                {"componentType": 5126, "count": 3, "type": "VEC3"},
2712                {"componentType": 5126, "count": 1, "type": "MAT4"},
2713                {"componentType": 5126, "count": 1, "type": "SCALAR"},
2714                {"componentType": 5126, "count": 1, "type": "VEC3"}
2715            ],
2716            "skins": [{"inverseBindMatrices": 1, "skeleton": 0, "joints": [0]}],
2717            "animations": [{
2718                "samplers": [{"input": 2, "output": 3}],
2719                "channels": [{"sampler": 0, "target": {"node": 0, "path": "translation"}}]
2720            }]
2721        });
2722        let valid_bytes = serde_json::to_vec(&valid).unwrap();
2723        GltfReader::from_bytes_lenient(&valid_bytes).unwrap();
2724        assert!(matches!(
2725            GltfReader::from_bytes(&valid_bytes),
2726            Err(GltfError::Unsupported(_))
2727        ));
2728
2729        for path in [
2730            "/scene",
2731            "/scenes/0/nodes/0",
2732            "/nodes/0/mesh",
2733            "/nodes/0/children/0",
2734            "/nodes/0/skin",
2735            "/skins/0/joints/0",
2736            "/skins/0/inverseBindMatrices",
2737            "/animations/0/samplers/0/input",
2738            "/animations/0/samplers/0/output",
2739            "/animations/0/channels/0/sampler",
2740            "/animations/0/channels/0/target/node",
2741        ] {
2742            let mut invalid = valid.clone();
2743            *invalid.pointer_mut(path).unwrap() = Value::from(99);
2744            let invalid_bytes = serde_json::to_vec(&invalid).unwrap();
2745            let error = match GltfReader::from_bytes_lenient(&invalid_bytes) {
2746                Ok(_) => panic!("invalid reference at {path} unexpectedly parsed"),
2747                Err(error) => error,
2748            };
2749            assert!(
2750                matches!(error, GltfError::InvalidGltf(_)),
2751                "path {path}: {error}"
2752            );
2753        }
2754    }
2755
2756    #[cfg(feature = "legacy-bitstream-decode")]
2757    #[test]
2758    fn test_draco_legacy_bitstream_data_uri() {
2759        let draco_bytes =
2760            include_bytes!("../../../testdata/legacy_draco/cube_att.mesh_seq.1.1.0.drc");
2761        let data_uri = format!(
2762            "data:application/octet-stream;base64,{}",
2763            base64_for_test(draco_bytes)
2764        );
2765        let json = format!(
2766            r#"{{
2767                "asset": {{"version": "2.0"}},
2768                "extensionsUsed": ["KHR_draco_mesh_compression"],
2769                "buffers": [{{"byteLength": {}, "uri": "{}"}}],
2770                "bufferViews": [{{"buffer": 0, "byteOffset": 0, "byteLength": {}}}],
2771                "accessors": [
2772                    {{"componentType": 5126, "count": 24, "type": "VEC3"}}
2773                ],
2774                "meshes": [{{
2775                    "primitives": [{{
2776                        "attributes": {{"POSITION": 0}},
2777                        "mode": 4,
2778                        "extensions": {{
2779                            "KHR_draco_mesh_compression": {{
2780                                "bufferView": 0,
2781                                "attributes": {{"POSITION": 0}}
2782                            }}
2783                        }}
2784                    }}]
2785                }}]
2786            }}"#,
2787            draco_bytes.len(),
2788            data_uri,
2789            draco_bytes.len()
2790        );
2791
2792        let mesh = GltfReader::from_gltf(json.as_bytes(), None)
2793            .unwrap()
2794            .decode_all_draco_meshes()
2795            .unwrap()
2796            .remove(0)
2797            .1;
2798
2799        assert_eq!(mesh.num_faces(), 12);
2800        assert_eq!(mesh.num_points(), 24);
2801        assert_eq!(
2802            mesh.named_attribute(GeometryAttributeType::Position)
2803                .expect("missing position")
2804                .size(),
2805            24
2806        );
2807    }
2808
2809    fn base64_for_test(bytes: &[u8]) -> String {
2810        const TABLE: &[u8; 64] =
2811            b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2812        let mut out = String::new();
2813
2814        for chunk in bytes.chunks(3) {
2815            let b0 = chunk[0];
2816            let b1 = *chunk.get(1).unwrap_or(&0);
2817            let b2 = *chunk.get(2).unwrap_or(&0);
2818            let n = ((b0 as u32) << 16) | ((b1 as u32) << 8) | b2 as u32;
2819
2820            out.push(TABLE[((n >> 18) & 0x3f) as usize] as char);
2821            out.push(TABLE[((n >> 12) & 0x3f) as usize] as char);
2822            if chunk.len() > 1 {
2823                out.push(TABLE[((n >> 6) & 0x3f) as usize] as char);
2824            } else {
2825                out.push('=');
2826            }
2827            if chunk.len() > 2 {
2828                out.push(TABLE[(n & 0x3f) as usize] as char);
2829            } else {
2830                out.push('=');
2831            }
2832        }
2833
2834        out
2835    }
2836}