Skip to main content

gpui_kit/media/
gltf.rs

1//! A bounded reader for a stated subset of glTF 2.0.
2//!
3//! A 3D document is the widest input this library takes: it is a container, a
4//! JSON document, an index space, and a byte buffer, and every one of those is
5//! a place where a file can ask a reader to allocate more than the reader has
6//! or to read outside what it was given. So this reader is written the other
7//! way round from a loader: it states what it accepts, refuses everything
8//! else, and checks every bound *before* allocating for it.
9//!
10//! # What it accepts
11//!
12//! - Both containers: a `.gltf` JSON document, and a `.glb` binary container
13//!   whose header declares version 2 and whose chunk lengths stay inside the
14//!   bytes handed in.
15//! - Buffers that are inside the file: the GLB binary chunk, and `data:` URIs
16//!   with a `;base64,` payload. **Any other URI is refused**, because
17//!   resolving one is I/O and this crate performs none — the same rule that
18//!   makes `Markdown` name an image rather than fetch it.
19//! - Triangle primitives with a `POSITION` accessor of `VEC3` `FLOAT`, with
20//!   or without indices; indices may be unsigned byte, short, or int.
21//! - Node hierarchy with either a 4×4 `matrix` or `translation`/`rotation`/
22//!   `scale`, to a bounded depth.
23//!
24//! # What it refuses, rather than approximates
25//!
26//! Materials, textures, cameras, lights, animation, skins, morph targets,
27//! sparse accessors, non-triangle primitives, and the `KHR_draco` family are
28//! not read. Where ignoring one would change what the reader draws it is a
29//! [`ModelDefect`]; where it only changes how the geometry is *shaded* it is
30//! ignored, and [`ModelViewer`](crate::media::ModelViewer) draws untextured
31//! geometry rather than pretending to a material it did not read.
32//!
33//! # Fail closed
34//!
35//! [`ModelBounds`] caps bytes, nodes, hierarchy depth, primitives, vertices,
36//! and triangles. Every cap is checked while reading, so a document that
37//! declares ten million vertices is refused at the accessor that says so
38//! rather than after the allocation. A refusal names the limit, what the file
39//! asked for, and what was allowed, so the caller can raise a bound on
40//! purpose instead of guessing.
41
42use gpui::SharedString;
43use serde::Deserialize;
44
45/// A magic number, a version, and the two chunk types of the GLB container.
46const GLB_MAGIC: &[u8; 4] = b"glTF";
47const GLB_HEADER: usize = 12;
48const GLB_CHUNK_HEADER: usize = 8;
49const GLB_CHUNK_JSON: u32 = 0x4E4F_534A;
50const GLB_CHUNK_BIN: u32 = 0x004E_4942;
51
52/// The glTF component types this reader knows.
53const COMPONENT_UNSIGNED_BYTE: u32 = 5121;
54const COMPONENT_UNSIGNED_SHORT: u32 = 5123;
55const COMPONENT_UNSIGNED_INT: u32 = 5125;
56const COMPONENT_FLOAT: u32 = 5126;
57
58/// The only primitive mode that is a surface. 4 is `TRIANGLES`.
59const MODE_TRIANGLES: u32 = 4;
60
61/// Which cap a document ran into.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum ModelLimit {
64    Bytes,
65    Nodes,
66    Depth,
67    Primitives,
68    Vertices,
69    Triangles,
70}
71
72impl ModelLimit {
73    /// The name a refusal publishes and a caller matches on.
74    pub fn name(self) -> &'static str {
75        match self {
76            Self::Bytes => "bytes",
77            Self::Nodes => "nodes",
78            Self::Depth => "depth",
79            Self::Primitives => "primitives",
80            Self::Vertices => "vertices",
81            Self::Triangles => "triangles",
82        }
83    }
84}
85
86/// Why a document is not one this reader will draw.
87///
88/// Each variant is a code rather than a sentence, because the sentence a
89/// reader sees is the host's to write and this crate holds no English outside
90/// [`crate::strings`].
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum ModelDefect {
93    /// The bytes are not JSON, or not a JSON object.
94    NotJson,
95    /// The GLB header, its declared length, or a chunk length is wrong.
96    BadContainer,
97    /// There is no `asset.version`, so this is not a glTF document.
98    NotGltf,
99    /// The document declares a glTF major version other than 2.
100    UnsupportedVersion,
101    /// A buffer points somewhere this crate would have to fetch.
102    ExternalResource,
103    /// A `data:` URI that is not base64, or base64 that will not decode.
104    UnsupportedEncoding,
105    /// A sparse accessor, which substitutes values this reader does not read.
106    SparseAccessor,
107    /// A primitive that is not a triangle list.
108    UnsupportedPrimitive,
109    /// An accessor component type this reader does not read.
110    UnsupportedComponentType,
111    /// An accessor whose element type is not the one the attribute requires.
112    UnsupportedAccessorType,
113    /// A primitive with no `POSITION`, which has no geometry to draw.
114    MissingPositions,
115    /// An index into accessors, buffer views, buffers, meshes, or nodes that
116    /// the document does not contain.
117    DanglingIndex,
118    /// An accessor that reads past the end of the bytes it was given.
119    TruncatedBuffer,
120    /// An index that names a vertex the primitive does not have.
121    IndexOutOfRange,
122    /// A document that parsed and contains no triangle to draw.
123    EmptyScene,
124}
125
126impl ModelDefect {
127    /// The code a refusal publishes.
128    pub fn name(self) -> &'static str {
129        match self {
130            Self::NotJson => "not-json",
131            Self::BadContainer => "bad-container",
132            Self::NotGltf => "not-gltf",
133            Self::UnsupportedVersion => "unsupported-version",
134            Self::ExternalResource => "external-resource",
135            Self::UnsupportedEncoding => "unsupported-encoding",
136            Self::SparseAccessor => "sparse-accessor",
137            Self::UnsupportedPrimitive => "unsupported-primitive",
138            Self::UnsupportedComponentType => "unsupported-component-type",
139            Self::UnsupportedAccessorType => "unsupported-accessor-type",
140            Self::MissingPositions => "missing-positions",
141            Self::DanglingIndex => "dangling-index",
142            Self::TruncatedBuffer => "truncated-buffer",
143            Self::IndexOutOfRange => "index-out-of-range",
144            Self::EmptyScene => "empty-scene",
145        }
146    }
147}
148
149/// Why a document was not read.
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum ModelError {
152    /// The document asked for more than the caller allowed.
153    TooLarge {
154        limit: ModelLimit,
155        /// What the document asked for.
156        found: usize,
157        /// What the caller allowed.
158        allowed: usize,
159    },
160    /// The document is outside the subset this reader accepts.
161    Rejected(ModelDefect),
162}
163
164impl ModelError {
165    /// The name a semantic node publishes for the refusal itself.
166    pub fn name(self) -> &'static str {
167        match self {
168            Self::TooLarge { .. } => "too-large",
169            Self::Rejected(_) => "rejected",
170        }
171    }
172
173    /// The code inside the refusal: a limit name or a defect code.
174    pub fn code(self) -> &'static str {
175        match self {
176            Self::TooLarge { limit, .. } => limit.name(),
177            Self::Rejected(defect) => defect.name(),
178        }
179    }
180}
181
182/// How much of a document this reader will take.
183///
184/// The defaults are chosen for what the viewer can draw at an interactive
185/// frame rate on a laptop rather than for what a format allows: a model that
186/// would take a second to paint is refused with a number the caller can raise
187/// on purpose.
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub struct ModelBounds {
190    pub max_bytes: usize,
191    pub max_nodes: usize,
192    pub max_depth: usize,
193    pub max_primitives: usize,
194    pub max_vertices: usize,
195    pub max_triangles: usize,
196}
197
198impl Default for ModelBounds {
199    fn default() -> Self {
200        Self {
201            max_bytes: 8 * 1024 * 1024,
202            max_nodes: 1024,
203            max_depth: 16,
204            max_primitives: 128,
205            max_vertices: 65_536,
206            max_triangles: 24_576,
207        }
208    }
209}
210
211impl ModelBounds {
212    pub fn max_bytes(mut self, bytes: usize) -> Self {
213        self.max_bytes = bytes;
214        self
215    }
216
217    pub fn max_nodes(mut self, nodes: usize) -> Self {
218        self.max_nodes = nodes;
219        self
220    }
221
222    pub fn max_depth(mut self, depth: usize) -> Self {
223        self.max_depth = depth;
224        self
225    }
226
227    pub fn max_primitives(mut self, primitives: usize) -> Self {
228        self.max_primitives = primitives;
229        self
230    }
231
232    pub fn max_vertices(mut self, vertices: usize) -> Self {
233        self.max_vertices = vertices;
234        self
235    }
236
237    pub fn max_triangles(mut self, triangles: usize) -> Self {
238        self.max_triangles = triangles;
239        self
240    }
241
242    fn check(self, limit: ModelLimit, found: usize) -> Result<(), ModelError> {
243        let allowed = match limit {
244            ModelLimit::Bytes => self.max_bytes,
245            ModelLimit::Nodes => self.max_nodes,
246            ModelLimit::Depth => self.max_depth,
247            ModelLimit::Primitives => self.max_primitives,
248            ModelLimit::Vertices => self.max_vertices,
249            ModelLimit::Triangles => self.max_triangles,
250        };
251        if found > allowed {
252            return Err(ModelError::TooLarge {
253                limit,
254                found,
255                allowed,
256            });
257        }
258        Ok(())
259    }
260}
261
262/// An axis-aligned box around everything the reader accepted.
263#[derive(Debug, Clone, Copy, PartialEq)]
264pub struct ModelAabb {
265    pub min: [f32; 3],
266    pub max: [f32; 3],
267}
268
269impl ModelAabb {
270    pub fn centre(self) -> [f32; 3] {
271        [
272            (self.min[0] + self.max[0]) / 2.0,
273            (self.min[1] + self.max[1]) / 2.0,
274            (self.min[2] + self.max[2]) / 2.0,
275        ]
276    }
277
278    /// The radius of the sphere around the box, which is what a camera fits
279    /// against so that orbiting does not change the size of the model.
280    pub fn radius(self) -> f32 {
281        let half = [
282            (self.max[0] - self.min[0]) / 2.0,
283            (self.max[1] - self.min[1]) / 2.0,
284            (self.max[2] - self.min[2]) / 2.0,
285        ];
286        (half[0] * half[0] + half[1] * half[1] + half[2] * half[2]).sqrt()
287    }
288}
289
290/// One primitive's triangles, already in the document's world space.
291#[derive(Debug, Clone, PartialEq)]
292pub struct ModelMesh {
293    name: SharedString,
294    positions: Vec<[f32; 3]>,
295    indices: Vec<u32>,
296}
297
298impl ModelMesh {
299    pub fn name(&self) -> &SharedString {
300        &self.name
301    }
302
303    pub fn positions(&self) -> &[[f32; 3]] {
304        &self.positions
305    }
306
307    /// Three indices per triangle, into [`positions`](Self::positions).
308    pub fn indices(&self) -> &[u32] {
309        &self.indices
310    }
311
312    pub fn triangle_count(&self) -> usize {
313        self.indices.len() / 3
314    }
315}
316
317/// Everything the reader accepted out of one document.
318#[derive(Debug, Clone, PartialEq)]
319pub struct ModelScene {
320    meshes: Vec<ModelMesh>,
321    vertices: usize,
322    triangles: usize,
323    aabb: ModelAabb,
324}
325
326impl ModelScene {
327    /// Reads a glTF 2.0 document, refusing anything outside the accepted
328    /// subset and anything past `bounds`.
329    pub fn parse(bytes: &[u8], bounds: ModelBounds) -> Result<Self, ModelError> {
330        bounds.check(ModelLimit::Bytes, bytes.len())?;
331        let (json, binary) = split_container(bytes)?;
332        let document: Document =
333            serde_json::from_slice(json).map_err(|_| ModelError::Rejected(ModelDefect::NotJson))?;
334        read(&document, binary, bounds)
335    }
336
337    pub fn meshes(&self) -> &[ModelMesh] {
338        &self.meshes
339    }
340
341    pub fn mesh_count(&self) -> usize {
342        self.meshes.len()
343    }
344
345    pub fn vertex_count(&self) -> usize {
346        self.vertices
347    }
348
349    pub fn triangle_count(&self) -> usize {
350        self.triangles
351    }
352
353    pub fn aabb(&self) -> ModelAabb {
354        self.aabb
355    }
356}
357
358// ---------------------------------------------------------------------------
359// The container
360// ---------------------------------------------------------------------------
361
362/// Splits the bytes into the JSON document and the binary chunk behind it.
363///
364/// A GLB whose declared total length, chunk length, or padding runs past what
365/// was handed in is refused rather than clamped: a length that disagrees with
366/// the file is exactly the shape of a document written to be read wrongly.
367fn split_container(bytes: &[u8]) -> Result<(&[u8], &[u8]), ModelError> {
368    if bytes.len() < 4 || &bytes[..4] != GLB_MAGIC {
369        return Ok((bytes, &[]));
370    }
371    let bad = ModelError::Rejected(ModelDefect::BadContainer);
372    if bytes.len() < GLB_HEADER {
373        return Err(bad);
374    }
375    if read_u32(bytes, 4).ok_or(bad)? != 2 {
376        return Err(ModelError::Rejected(ModelDefect::UnsupportedVersion));
377    }
378    let declared = read_u32(bytes, 8).ok_or(bad)? as usize;
379    if declared > bytes.len() || declared < GLB_HEADER {
380        return Err(bad);
381    }
382
383    let mut json: Option<&[u8]> = None;
384    let mut binary: &[u8] = &[];
385    let mut at = GLB_HEADER;
386    while at + GLB_CHUNK_HEADER <= declared {
387        let length = read_u32(bytes, at).ok_or(bad)? as usize;
388        let kind = read_u32(bytes, at + 4).ok_or(bad)?;
389        let start = at + GLB_CHUNK_HEADER;
390        let end = start.checked_add(length).ok_or(bad)?;
391        if end > declared {
392            return Err(bad);
393        }
394        match kind {
395            GLB_CHUNK_JSON if json.is_none() => json = Some(&bytes[start..end]),
396            GLB_CHUNK_BIN if binary.is_empty() => binary = &bytes[start..end],
397            // An unknown chunk type is skipped by the specification, which is
398            // safe here because its length has already been bounded.
399            _ => {}
400        }
401        // Chunks are padded to four bytes.
402        at = end + (4 - end % 4) % 4;
403    }
404    Ok((json.ok_or(bad)?, binary))
405}
406
407fn read_u32(bytes: &[u8], at: usize) -> Option<u32> {
408    let slice = bytes.get(at..at + 4)?;
409    Some(u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]))
410}
411
412// ---------------------------------------------------------------------------
413// The document
414// ---------------------------------------------------------------------------
415
416#[derive(Debug, Deserialize)]
417#[serde(rename_all = "camelCase")]
418struct Document {
419    #[serde(default)]
420    asset: Asset,
421    #[serde(default)]
422    scene: Option<usize>,
423    #[serde(default)]
424    scenes: Vec<SceneNode>,
425    #[serde(default)]
426    nodes: Vec<Node>,
427    #[serde(default)]
428    meshes: Vec<Mesh>,
429    #[serde(default)]
430    accessors: Vec<Accessor>,
431    #[serde(default)]
432    buffer_views: Vec<BufferView>,
433    #[serde(default)]
434    buffers: Vec<Buffer>,
435}
436
437#[derive(Debug, Default, Deserialize)]
438struct Asset {
439    #[serde(default)]
440    version: Option<String>,
441}
442
443#[derive(Debug, Deserialize)]
444struct SceneNode {
445    #[serde(default)]
446    nodes: Vec<usize>,
447}
448
449#[derive(Debug, Deserialize)]
450struct Node {
451    #[serde(default)]
452    name: Option<String>,
453    #[serde(default)]
454    mesh: Option<usize>,
455    #[serde(default)]
456    children: Vec<usize>,
457    #[serde(default)]
458    matrix: Option<[f32; 16]>,
459    #[serde(default)]
460    translation: Option<[f32; 3]>,
461    #[serde(default)]
462    rotation: Option<[f32; 4]>,
463    #[serde(default)]
464    scale: Option<[f32; 3]>,
465}
466
467#[derive(Debug, Deserialize)]
468struct Mesh {
469    #[serde(default)]
470    name: Option<String>,
471    #[serde(default)]
472    primitives: Vec<Primitive>,
473}
474
475#[derive(Debug, Deserialize)]
476struct Primitive {
477    #[serde(default)]
478    attributes: Attributes,
479    #[serde(default)]
480    indices: Option<usize>,
481    #[serde(default)]
482    mode: Option<u32>,
483}
484
485#[derive(Debug, Default, Deserialize)]
486struct Attributes {
487    #[serde(rename = "POSITION", default)]
488    position: Option<usize>,
489}
490
491#[derive(Debug, Deserialize)]
492#[serde(rename_all = "camelCase")]
493struct Accessor {
494    #[serde(default)]
495    buffer_view: Option<usize>,
496    #[serde(default)]
497    byte_offset: usize,
498    component_type: u32,
499    count: usize,
500    #[serde(rename = "type")]
501    kind: String,
502    #[serde(default)]
503    sparse: Option<serde_json::Value>,
504}
505
506#[derive(Debug, Deserialize)]
507#[serde(rename_all = "camelCase")]
508struct BufferView {
509    buffer: usize,
510    #[serde(default)]
511    byte_offset: usize,
512    byte_length: usize,
513    #[serde(default)]
514    byte_stride: Option<usize>,
515}
516
517#[derive(Debug, Deserialize)]
518struct Buffer {
519    #[serde(default)]
520    uri: Option<String>,
521}
522
523// ---------------------------------------------------------------------------
524// Reading
525// ---------------------------------------------------------------------------
526
527/// A 4×4 transform in glTF's column-major order.
528type Mat4 = [f32; 16];
529
530const IDENTITY: Mat4 = [
531    1.0, 0.0, 0.0, 0.0, //
532    0.0, 1.0, 0.0, 0.0, //
533    0.0, 0.0, 1.0, 0.0, //
534    0.0, 0.0, 0.0, 1.0,
535];
536
537fn multiply(a: &Mat4, b: &Mat4) -> Mat4 {
538    let mut out = [0.0; 16];
539    for column in 0..4 {
540        for row in 0..4 {
541            let mut sum = 0.0;
542            for step in 0..4 {
543                sum += a[step * 4 + row] * b[column * 4 + step];
544            }
545            out[column * 4 + row] = sum;
546        }
547    }
548    out
549}
550
551fn transform(matrix: &Mat4, point: [f32; 3]) -> [f32; 3] {
552    let [x, y, z] = point;
553    [
554        matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12],
555        matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13],
556        matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14],
557    ]
558}
559
560/// The node's own transform: an explicit matrix, or the composed T·R·S.
561fn local(node: &Node) -> Mat4 {
562    if let Some(matrix) = node.matrix {
563        return matrix;
564    }
565    let [tx, ty, tz] = node.translation.unwrap_or([0.0; 3]);
566    let [sx, sy, sz] = node.scale.unwrap_or([1.0; 3]);
567    let [x, y, z, w] = node.rotation.unwrap_or([0.0, 0.0, 0.0, 1.0]);
568    // The quaternion is normalized here rather than trusted: an unnormalized
569    // one in the file would otherwise scale the model as well as turn it.
570    let length = (x * x + y * y + z * z + w * w).sqrt();
571    let (x, y, z, w) = if length > f32::EPSILON {
572        (x / length, y / length, z / length, w / length)
573    } else {
574        (0.0, 0.0, 0.0, 1.0)
575    };
576    let (xx, yy, zz) = (x * x, y * y, z * z);
577    let (xy, xz, yz) = (x * y, x * z, y * z);
578    let (wx, wy, wz) = (w * x, w * y, w * z);
579    [
580        (1.0 - 2.0 * (yy + zz)) * sx,
581        (2.0 * (xy + wz)) * sx,
582        (2.0 * (xz - wy)) * sx,
583        0.0,
584        (2.0 * (xy - wz)) * sy,
585        (1.0 - 2.0 * (xx + zz)) * sy,
586        (2.0 * (yz + wx)) * sy,
587        0.0,
588        (2.0 * (xz + wy)) * sz,
589        (2.0 * (yz - wx)) * sz,
590        (1.0 - 2.0 * (xx + yy)) * sz,
591        0.0,
592        tx,
593        ty,
594        tz,
595        1.0,
596    ]
597}
598
599/// What one traversal accumulates, so every cap is checked as it grows.
600struct Reader<'a> {
601    document: &'a Document,
602    buffers: Vec<Vec<u8>>,
603    binary: &'a [u8],
604    bounds: ModelBounds,
605    meshes: Vec<ModelMesh>,
606    vertices: usize,
607    triangles: usize,
608    visited: usize,
609    min: [f32; 3],
610    max: [f32; 3],
611}
612
613fn read(document: &Document, binary: &[u8], bounds: ModelBounds) -> Result<ModelScene, ModelError> {
614    let version = document
615        .asset
616        .version
617        .as_deref()
618        .ok_or(ModelError::Rejected(ModelDefect::NotGltf))?;
619    if !version.starts_with('2') {
620        return Err(ModelError::Rejected(ModelDefect::UnsupportedVersion));
621    }
622    bounds.check(ModelLimit::Nodes, document.nodes.len())?;
623
624    let mut buffers = Vec::with_capacity(document.buffers.len());
625    for buffer in &document.buffers {
626        buffers.push(resolve(buffer, binary, bounds)?);
627    }
628
629    let mut reader = Reader {
630        document,
631        buffers,
632        binary,
633        bounds,
634        meshes: Vec::new(),
635        vertices: 0,
636        triangles: 0,
637        visited: 0,
638        min: [f32::INFINITY; 3],
639        max: [f32::NEG_INFINITY; 3],
640    };
641
642    let roots: Vec<usize> = match document.scenes.get(document.scene.unwrap_or(0)) {
643        Some(scene) => scene.nodes.clone(),
644        // A document with no scene list still has nodes, and drawing all of
645        // them is what every reader does with one.
646        None => (0..document.nodes.len()).collect(),
647    };
648    for root in roots {
649        reader.walk(root, &IDENTITY, 0)?;
650    }
651
652    if reader.triangles == 0 {
653        return Err(ModelError::Rejected(ModelDefect::EmptyScene));
654    }
655    Ok(ModelScene {
656        meshes: reader.meshes,
657        vertices: reader.vertices,
658        triangles: reader.triangles,
659        aabb: ModelAabb {
660            min: reader.min,
661            max: reader.max,
662        },
663    })
664}
665
666/// The bytes behind one buffer, or a refusal to go and get them.
667fn resolve(buffer: &Buffer, binary: &[u8], bounds: ModelBounds) -> Result<Vec<u8>, ModelError> {
668    let Some(uri) = buffer.uri.as_deref() else {
669        return Ok(binary.to_vec());
670    };
671    let Some(rest) = uri.strip_prefix("data:") else {
672        return Err(ModelError::Rejected(ModelDefect::ExternalResource));
673    };
674    let Some((_, payload)) = rest.split_once(";base64,") else {
675        return Err(ModelError::Rejected(ModelDefect::UnsupportedEncoding));
676    };
677    // The decoded length is known from the encoded one, so an oversize buffer
678    // is refused before it is decoded rather than after.
679    bounds.check(ModelLimit::Bytes, payload.len() / 4 * 3)?;
680    base64(payload).ok_or(ModelError::Rejected(ModelDefect::UnsupportedEncoding))
681}
682
683/// Standard base64 with optional padding, and nothing else.
684fn base64(payload: &str) -> Option<Vec<u8>> {
685    fn sextet(byte: u8) -> Option<u32> {
686        match byte {
687            b'A'..=b'Z' => Some(u32::from(byte - b'A')),
688            b'a'..=b'z' => Some(u32::from(byte - b'a') + 26),
689            b'0'..=b'9' => Some(u32::from(byte - b'0') + 52),
690            b'+' => Some(62),
691            b'/' => Some(63),
692            _ => None,
693        }
694    }
695    let payload = payload.trim_end_matches('=');
696    let mut out = Vec::with_capacity(payload.len() / 4 * 3);
697    let mut accumulator = 0_u32;
698    let mut bits = 0_u32;
699    for byte in payload.bytes() {
700        if byte.is_ascii_whitespace() {
701            continue;
702        }
703        accumulator = (accumulator << 6) | sextet(byte)?;
704        bits += 6;
705        if bits >= 8 {
706            bits -= 8;
707            out.push(((accumulator >> bits) & 0xFF) as u8);
708        }
709    }
710    // Any bits left over must be the zero padding of the final group; a
711    // non-zero remainder means the payload was truncated mid-byte.
712    if accumulator & ((1 << bits) - 1) != 0 {
713        return None;
714    }
715    Some(out)
716}
717
718impl Reader<'_> {
719    fn walk(&mut self, index: usize, parent: &Mat4, depth: usize) -> Result<(), ModelError> {
720        self.bounds.check(ModelLimit::Depth, depth)?;
721        self.visited += 1;
722        // A file may name the same node from two parents, and a malformed one
723        // may name a node that reaches itself. The visit budget is what stops
724        // either from becoming an unbounded walk.
725        self.bounds.check(ModelLimit::Nodes, self.visited)?;
726
727        let node = self
728            .document
729            .nodes
730            .get(index)
731            .ok_or(ModelError::Rejected(ModelDefect::DanglingIndex))?;
732        let world = multiply(parent, &local(node));
733
734        if let Some(mesh) = node.mesh {
735            let mesh = self
736                .document
737                .meshes
738                .get(mesh)
739                .ok_or(ModelError::Rejected(ModelDefect::DanglingIndex))?;
740            let name = mesh
741                .name
742                .clone()
743                .or_else(|| node.name.clone())
744                .unwrap_or_else(|| format!("mesh-{index}"));
745            for primitive in &mesh.primitives {
746                self.primitive(primitive, &world, &name)?;
747            }
748        }
749
750        for child in &node.children {
751            self.walk(*child, &world, depth + 1)?;
752        }
753        Ok(())
754    }
755
756    fn primitive(
757        &mut self,
758        primitive: &Primitive,
759        world: &Mat4,
760        name: &str,
761    ) -> Result<(), ModelError> {
762        if primitive.mode.unwrap_or(MODE_TRIANGLES) != MODE_TRIANGLES {
763            return Err(ModelError::Rejected(ModelDefect::UnsupportedPrimitive));
764        }
765        let accessor = primitive
766            .attributes
767            .position
768            .ok_or(ModelError::Rejected(ModelDefect::MissingPositions))?;
769        self.bounds
770            .check(ModelLimit::Primitives, self.meshes.len() + 1)?;
771
772        let positions = self.positions(accessor, world)?;
773        let indices = match primitive.indices {
774            Some(accessor) => self.indices(accessor, positions.len())?,
775            // An unindexed primitive is three consecutive vertices per face.
776            None => (0..positions.len() as u32).collect(),
777        };
778        if indices.len() < 3 {
779            return Ok(());
780        }
781        let indices: Vec<u32> = indices[..indices.len() - indices.len() % 3].to_vec();
782
783        self.triangles += indices.len() / 3;
784        self.bounds.check(ModelLimit::Triangles, self.triangles)?;
785        self.meshes.push(ModelMesh {
786            name: SharedString::from(name.to_owned()),
787            positions,
788            indices,
789        });
790        Ok(())
791    }
792
793    /// The bytes one accessor addresses, with every bound checked first.
794    fn view(&self, accessor: &Accessor, size: usize) -> Result<(&[u8], usize), ModelError> {
795        if accessor.sparse.is_some() {
796            return Err(ModelError::Rejected(ModelDefect::SparseAccessor));
797        }
798        let truncated = ModelError::Rejected(ModelDefect::TruncatedBuffer);
799        let dangling = ModelError::Rejected(ModelDefect::DanglingIndex);
800        let view = accessor
801            .buffer_view
802            .and_then(|index| self.document.buffer_views.get(index))
803            .ok_or(dangling)?;
804        let buffer = self
805            .buffers
806            .get(view.buffer)
807            .map(Vec::as_slice)
808            .or(if view.buffer == 0 {
809                Some(self.binary)
810            } else {
811                None
812            })
813            .ok_or(dangling)?;
814
815        let start = view
816            .byte_offset
817            .checked_add(accessor.byte_offset)
818            .ok_or(truncated)?;
819        let stride = view.byte_stride.unwrap_or(size).max(size);
820        // The last element only needs its own size, not a whole stride, which
821        // is what a tightly packed final element in an interleaved view is.
822        let span = stride
823            .checked_mul(accessor.count.saturating_sub(1))
824            .and_then(|span| span.checked_add(size))
825            .ok_or(truncated)?;
826        let end = start.checked_add(span).ok_or(truncated)?;
827        if end > buffer.len() || view.byte_offset + view.byte_length > buffer.len() {
828            return Err(truncated);
829        }
830        Ok((&buffer[start..end], stride))
831    }
832
833    fn positions(&mut self, index: usize, world: &Mat4) -> Result<Vec<[f32; 3]>, ModelError> {
834        let accessor = self
835            .document
836            .accessors
837            .get(index)
838            .ok_or(ModelError::Rejected(ModelDefect::DanglingIndex))?;
839        if accessor.kind != "VEC3" {
840            return Err(ModelError::Rejected(ModelDefect::UnsupportedAccessorType));
841        }
842        if accessor.component_type != COMPONENT_FLOAT {
843            return Err(ModelError::Rejected(ModelDefect::UnsupportedComponentType));
844        }
845        self.vertices += accessor.count;
846        self.bounds.check(ModelLimit::Vertices, self.vertices)?;
847
848        let (bytes, stride) = self.view(accessor, 12)?;
849        let mut positions = Vec::with_capacity(accessor.count);
850        for element in 0..accessor.count {
851            let at = element * stride;
852            let float = |offset: usize| {
853                let slice = &bytes[at + offset..at + offset + 4];
854                f32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]])
855            };
856            positions.push(transform(world, [float(0), float(4), float(8)]));
857        }
858        for point in &positions {
859            for (axis, value) in point.iter().enumerate() {
860                self.min[axis] = self.min[axis].min(*value);
861                self.max[axis] = self.max[axis].max(*value);
862            }
863        }
864        Ok(positions)
865    }
866
867    fn indices(&self, index: usize, vertices: usize) -> Result<Vec<u32>, ModelError> {
868        let accessor = self
869            .document
870            .accessors
871            .get(index)
872            .ok_or(ModelError::Rejected(ModelDefect::DanglingIndex))?;
873        if accessor.kind != "SCALAR" {
874            return Err(ModelError::Rejected(ModelDefect::UnsupportedAccessorType));
875        }
876        let size = match accessor.component_type {
877            COMPONENT_UNSIGNED_BYTE => 1,
878            COMPONENT_UNSIGNED_SHORT => 2,
879            COMPONENT_UNSIGNED_INT => 4,
880            _ => return Err(ModelError::Rejected(ModelDefect::UnsupportedComponentType)),
881        };
882        self.bounds
883            .check(ModelLimit::Triangles, self.triangles + accessor.count / 3)?;
884
885        let (bytes, stride) = self.view(accessor, size)?;
886        let mut indices = Vec::with_capacity(accessor.count);
887        for element in 0..accessor.count {
888            let at = element * stride;
889            let value = match size {
890                1 => u32::from(bytes[at]),
891                2 => u32::from(u16::from_le_bytes([bytes[at], bytes[at + 1]])),
892                _ => u32::from_le_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]]),
893            };
894            if value as usize >= vertices {
895                return Err(ModelError::Rejected(ModelDefect::IndexOutOfRange));
896            }
897            indices.push(value);
898        }
899        Ok(indices)
900    }
901}
902
903#[cfg(test)]
904mod tests {
905    use super::*;
906
907    /// One triangle, its positions in a base64 `data:` buffer.
908    ///
909    /// Written out rather than loaded, so the reader's tests need no file and
910    /// no fixture directory.
911    fn triangle() -> String {
912        let positions: [f32; 9] = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
913        let mut bytes = Vec::new();
914        for value in positions {
915            bytes.extend_from_slice(&value.to_le_bytes());
916        }
917        format!(
918            r#"{{
919              "asset": {{"version": "2.0"}},
920              "scene": 0,
921              "scenes": [{{"nodes": [0]}}],
922              "nodes": [{{"mesh": 0}}],
923              "meshes": [{{"name": "tri", "primitives": [{{"attributes": {{"POSITION": 0}}}}]}}],
924              "accessors": [
925                {{"bufferView": 0, "componentType": 5126, "count": 3, "type": "VEC3"}}
926              ],
927              "bufferViews": [{{"buffer": 0, "byteOffset": 0, "byteLength": {length}}}],
928              "buffers": [{{"uri": "data:application/octet-stream;base64,{payload}"}}]
929            }}"#,
930            length = bytes.len(),
931            payload = encode(&bytes),
932        )
933    }
934
935    /// The encoder the fixtures are written with. The reader has only a
936    /// decoder, so this is a test's own tool rather than public surface.
937    fn encode(bytes: &[u8]) -> String {
938        const ALPHABET: &[u8; 64] =
939            b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
940        let mut out = String::new();
941        for chunk in bytes.chunks(3) {
942            let mut block = [0_u8; 3];
943            block[..chunk.len()].copy_from_slice(chunk);
944            let packed =
945                (u32::from(block[0]) << 16) | (u32::from(block[1]) << 8) | u32::from(block[2]);
946            for step in 0..4 {
947                if step <= chunk.len() {
948                    let sextet = ((packed >> (18 - step * 6)) & 0x3F) as usize;
949                    out.push(char::from(ALPHABET[sextet]));
950                } else {
951                    out.push('=');
952                }
953            }
954        }
955        out
956    }
957
958    #[test]
959    fn a_triangle_reads_as_one_mesh_of_three_vertices() {
960        let scene = ModelScene::parse(triangle().as_bytes(), ModelBounds::default())
961            .expect("a triangle is inside the subset");
962        assert_eq!(scene.mesh_count(), 1);
963        assert_eq!(scene.vertex_count(), 3);
964        assert_eq!(scene.triangle_count(), 1);
965        assert_eq!(scene.meshes()[0].name().as_ref(), "tri");
966        assert_eq!(scene.aabb().min, [0.0, 0.0, 0.0]);
967        assert_eq!(scene.aabb().max, [1.0, 1.0, 0.0]);
968    }
969
970    #[test]
971    fn a_base64_round_trip_holds_and_a_truncated_payload_does_not() {
972        let bytes: Vec<u8> = (0..32).collect();
973        assert_eq!(base64(&encode(&bytes)), Some(bytes));
974        assert_eq!(
975            base64("!!!!"),
976            None,
977            "a byte outside the alphabet is not data"
978        );
979    }
980
981    #[test]
982    fn a_document_larger_than_the_caller_allowed_is_refused_by_its_size() {
983        let bounds = ModelBounds::default().max_bytes(16);
984        assert_eq!(
985            ModelScene::parse(triangle().as_bytes(), bounds),
986            Err(ModelError::TooLarge {
987                limit: ModelLimit::Bytes,
988                found: triangle().len(),
989                allowed: 16,
990            })
991        );
992    }
993
994    #[test]
995    fn a_model_past_a_geometry_cap_is_refused_before_it_is_built() {
996        let bounds = ModelBounds::default().max_vertices(2);
997        assert_eq!(
998            ModelScene::parse(triangle().as_bytes(), bounds),
999            Err(ModelError::TooLarge {
1000                limit: ModelLimit::Vertices,
1001                found: 3,
1002                allowed: 2,
1003            })
1004        );
1005        let bounds = ModelBounds::default().max_triangles(0);
1006        assert!(matches!(
1007            ModelScene::parse(triangle().as_bytes(), bounds),
1008            Err(ModelError::TooLarge {
1009                limit: ModelLimit::Triangles,
1010                ..
1011            })
1012        ));
1013    }
1014
1015    #[test]
1016    fn a_buffer_this_crate_would_have_to_fetch_is_refused() {
1017        let document = triangle().replace(
1018            "data:application/octet-stream;base64,",
1019            "https://example.invalid/scene.bin?",
1020        );
1021        assert_eq!(
1022            ModelScene::parse(document.as_bytes(), ModelBounds::default()),
1023            Err(ModelError::Rejected(ModelDefect::ExternalResource)),
1024            "resolving a URI is I/O, and this crate performs none"
1025        );
1026    }
1027
1028    #[test]
1029    fn a_document_outside_the_subset_names_what_it_asked_for() {
1030        let lines = triangle().replace(r#""POSITION": 0}}"#, r#""POSITION": 0}, "mode": 1}"#);
1031        assert_eq!(
1032            ModelScene::parse(lines.as_bytes(), ModelBounds::default()),
1033            Err(ModelError::Rejected(ModelDefect::UnsupportedPrimitive))
1034        );
1035
1036        let sparse = triangle().replace(
1037            r#""type": "VEC3"}"#,
1038            r#""type": "VEC3", "sparse": {"count": 1}}"#,
1039        );
1040        assert_eq!(
1041            ModelScene::parse(sparse.as_bytes(), ModelBounds::default()),
1042            Err(ModelError::Rejected(ModelDefect::SparseAccessor))
1043        );
1044
1045        let doubles = triangle().replace(r#""componentType": 5126"#, r#""componentType": 5130"#);
1046        assert_eq!(
1047            ModelScene::parse(doubles.as_bytes(), ModelBounds::default()),
1048            Err(ModelError::Rejected(ModelDefect::UnsupportedComponentType))
1049        );
1050    }
1051
1052    #[test]
1053    fn bytes_that_are_not_a_document_are_refused_as_such() {
1054        assert_eq!(
1055            ModelScene::parse(b"not a model", ModelBounds::default()),
1056            Err(ModelError::Rejected(ModelDefect::NotJson))
1057        );
1058        assert_eq!(
1059            ModelScene::parse(br#"{"nodes": []}"#, ModelBounds::default()),
1060            Err(ModelError::Rejected(ModelDefect::NotGltf))
1061        );
1062        assert_eq!(
1063            ModelScene::parse(br#"{"asset": {"version": "1.0"}}"#, ModelBounds::default()),
1064            Err(ModelError::Rejected(ModelDefect::UnsupportedVersion))
1065        );
1066    }
1067
1068    #[test]
1069    fn an_accessor_that_reads_past_its_buffer_is_refused() {
1070        let overrun = triangle().replace(
1071            r#""count": 3, "type": "VEC3""#,
1072            r#""count": 9, "type": "VEC3""#,
1073        );
1074        assert_eq!(
1075            ModelScene::parse(overrun.as_bytes(), ModelBounds::default()),
1076            Err(ModelError::Rejected(ModelDefect::TruncatedBuffer))
1077        );
1078    }
1079
1080    #[test]
1081    fn a_glb_container_carries_the_same_document() {
1082        let json = triangle();
1083        let mut padded = json.into_bytes();
1084        while !padded.len().is_multiple_of(4) {
1085            padded.push(b' ');
1086        }
1087        let mut glb = Vec::new();
1088        glb.extend_from_slice(GLB_MAGIC);
1089        glb.extend_from_slice(&2_u32.to_le_bytes());
1090        glb.extend_from_slice(
1091            &((GLB_HEADER + GLB_CHUNK_HEADER + padded.len()) as u32).to_le_bytes(),
1092        );
1093        glb.extend_from_slice(&(padded.len() as u32).to_le_bytes());
1094        glb.extend_from_slice(&GLB_CHUNK_JSON.to_le_bytes());
1095        glb.extend_from_slice(&padded);
1096
1097        let scene =
1098            ModelScene::parse(&glb, ModelBounds::default()).expect("a GLB is inside the subset");
1099        assert_eq!(scene.triangle_count(), 1);
1100
1101        // A declared length past what was handed in is the shape of a file
1102        // written to be read wrongly, so it is refused rather than clamped.
1103        let mut lying = glb.clone();
1104        lying[8..12].copy_from_slice(&u32::MAX.to_le_bytes());
1105        assert_eq!(
1106            ModelScene::parse(&lying, ModelBounds::default()),
1107            Err(ModelError::Rejected(ModelDefect::BadContainer))
1108        );
1109    }
1110
1111    #[test]
1112    fn a_node_that_reaches_itself_stops_at_the_visit_budget() {
1113        let looping = r#"{
1114          "asset": {"version": "2.0"},
1115          "scenes": [{"nodes": [0]}],
1116          "nodes": [{"children": [1]}, {"children": [0]}]
1117        }"#;
1118        assert!(matches!(
1119            ModelScene::parse(looping.as_bytes(), ModelBounds::default()),
1120            Err(ModelError::TooLarge {
1121                limit: ModelLimit::Depth | ModelLimit::Nodes,
1122                ..
1123            })
1124        ));
1125    }
1126
1127    #[test]
1128    fn a_transform_places_the_geometry_the_node_moved() {
1129        let moved = triangle().replace(
1130            r#"{"mesh": 0}"#,
1131            r#"{"mesh": 0, "translation": [10.0, 0.0, 0.0]}"#,
1132        );
1133        let scene = ModelScene::parse(moved.as_bytes(), ModelBounds::default()).expect("read");
1134        assert_eq!(scene.aabb().min, [10.0, 0.0, 0.0]);
1135        assert_eq!(scene.aabb().max, [11.0, 1.0, 0.0]);
1136    }
1137
1138    #[test]
1139    fn a_bounding_box_answers_a_centre_and_a_radius() {
1140        let aabb = ModelAabb {
1141            min: [-1.0, -1.0, -1.0],
1142            max: [1.0, 1.0, 1.0],
1143        };
1144        assert_eq!(aabb.centre(), [0.0, 0.0, 0.0]);
1145        assert!((aabb.radius() - 3.0_f32.sqrt()).abs() < 0.001);
1146    }
1147}