Skip to main content

draco_io/
gltf_container.rs

1//! Shared glTF/GLB container and resource handling.
2
3use std::fs::{self, File};
4use std::io::{Read, Seek, SeekFrom};
5use std::path::{Path, PathBuf};
6
7use crate::gltf_error::{GltfError, Result};
8
9const GLB_MAGIC: u32 = 0x4654_6c67;
10const GLB_VERSION_V2: u32 = 2;
11const GLB_VERSION_V3: u32 = 3;
12const GLB_CHUNK_JSON: u32 = 0x4e4f_534a;
13const GLB_CHUNK_BIN: u32 = 0x004e_4942;
14
15/// Container used by an input glTF document.
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum GltfContainerFormat {
18    /// JSON glTF document.
19    Gltf,
20    /// Binary GLB 2.0 document.
21    GlbV2,
22    /// Draft glTF 2.1 binary container (GLB version 3).
23    GlbV3,
24}
25
26/// Builds a GLB v2 or draft v3 container from serialized JSON and binary data.
27///
28/// ```
29/// # use draco_io::{parse_gltf_container, GltfContainerFormat};
30/// # use draco_io::gltf_container::build_glb_from_json;
31/// let json = br#"{"asset":{"version":"2.0"}}"#;
32/// let glb = build_glb_from_json(json, &[1, 2, 3], GltfContainerFormat::GlbV2)?;
33/// let container = parse_gltf_container(&glb)?;
34/// assert_eq!(container.format, GltfContainerFormat::GlbV2);
35/// assert_eq!(&container.bin.unwrap()[..3], &[1, 2, 3]);
36/// # Ok::<(), draco_io::GltfError>(())
37/// ```
38pub fn build_glb_from_json(
39    json: &[u8],
40    bin: &[u8],
41    format: GltfContainerFormat,
42) -> Result<Vec<u8>> {
43    if !matches!(
44        format,
45        GltfContainerFormat::GlbV2 | GltfContainerFormat::GlbV3
46    ) {
47        return Err(GltfError::InvalidGlb(
48            "GLB output requires a GLB format".into(),
49        ));
50    }
51    let mut json = json.to_vec();
52    while !json.len().is_multiple_of(4) {
53        json.push(b' ');
54    }
55    let mut bin = bin.to_vec();
56    while !bin.len().is_multiple_of(4) {
57        bin.push(0);
58    }
59    let v3 = format == GltfContainerFormat::GlbV3;
60    let header = if v3 { 16 } else { 12 };
61    let chunk_header = if v3 { 16 } else { 8 };
62    let total = header
63        + chunk_header
64        + json.len()
65        + if bin.is_empty() {
66            0
67        } else {
68            chunk_header + bin.len()
69        };
70    let mut out = Vec::with_capacity(total);
71    out.extend_from_slice(&GLB_MAGIC.to_le_bytes());
72    out.extend_from_slice(&(if v3 { 3u32 } else { 2u32 }).to_le_bytes());
73    if v3 {
74        out.extend_from_slice(&(total as u64).to_le_bytes());
75    } else {
76        out.extend_from_slice(
77            &u32::try_from(total)
78                .map_err(|_| GltfError::ResourceLimitExceeded("GLB v2 exceeds u32".into()))?
79                .to_le_bytes(),
80        );
81    }
82    for (kind, bytes) in [(GLB_CHUNK_JSON, &json), (GLB_CHUNK_BIN, &bin)] {
83        if kind == GLB_CHUNK_BIN && bytes.is_empty() {
84            continue;
85        }
86        if v3 {
87            out.extend_from_slice(&(bytes.len() as u64).to_le_bytes());
88            out.extend_from_slice(&kind.to_le_bytes());
89            out.extend_from_slice(&0u32.to_le_bytes());
90        } else {
91            out.extend_from_slice(
92                &u32::try_from(bytes.len())
93                    .map_err(|_| {
94                        GltfError::ResourceLimitExceeded("GLB v2 chunk exceeds u32".into())
95                    })?
96                    .to_le_bytes(),
97            );
98            out.extend_from_slice(&kind.to_le_bytes());
99        }
100        out.extend_from_slice(bytes);
101    }
102    Ok(out)
103}
104
105#[cfg(test)]
106mod glb_tests {
107    use super::*;
108
109    #[test]
110    fn glb_v3_builder_roundtrips_container_layout() {
111        let bytes = build_glb_from_json(
112            br#"{"asset":{"version":"2.1"}}"#,
113            &[1, 2, 3],
114            GltfContainerFormat::GlbV3,
115        )
116        .unwrap();
117        let parsed = parse_gltf_container(&bytes).unwrap();
118        assert_eq!(parsed.format, GltfContainerFormat::GlbV3);
119        assert_eq!(parsed.bin.unwrap()[..3], [1, 2, 3]);
120    }
121
122    #[test]
123    fn range_reader_materializes_only_selected_v3_chunk() {
124        let bytes = build_glb_from_json(
125            br#"{"asset":{"version":"2.1"}}"#,
126            &[1, 2, 3, 4],
127            GltfContainerFormat::GlbV3,
128        )
129        .unwrap();
130        let mut reader = GlbRangeReader::open(std::io::Cursor::new(bytes)).unwrap();
131        assert_eq!(reader.layout().chunks.len(), 2);
132        let bin = reader.layout().chunks[1];
133        assert_eq!(reader.read_chunk(bin, Some(4)).unwrap(), [1, 2, 3, 4]);
134        assert!(reader.read_chunk(bin, Some(3)).is_err());
135    }
136}
137
138impl GltfContainerFormat {
139    /// Whether this is either binary GLB container version.
140    pub const fn is_glb(self) -> bool {
141        matches!(self, Self::GlbV2 | Self::GlbV3)
142    }
143}
144
145/// Borrowed, strictly parsed glTF container.
146#[derive(Clone, Copy, Debug)]
147pub struct GltfContainer<'a> {
148    /// Input container kind.
149    pub format: GltfContainerFormat,
150    /// JSON document bytes (including legal GLB JSON padding).
151    pub json: &'a [u8],
152    /// Optional GLB BIN chunk.
153    pub bin: Option<&'a [u8]>,
154}
155
156/// A chunk address in a seekable GLB input. No chunk bytes are materialized.
157#[derive(Clone, Copy, Debug, PartialEq, Eq)]
158pub struct GlbChunkDescriptor {
159    /// Absolute byte offset of chunk payload.
160    pub offset: u64,
161    /// Payload length in bytes.
162    pub length: u64,
163    /// Four-byte chunk kind.
164    pub kind: u32,
165    /// Reserved GLB v3 encoding field; currently zero.
166    pub encoding: u32,
167}
168
169/// Metadata for a seekable GLB input, suitable for range-based resource loading.
170#[derive(Clone, Debug, PartialEq, Eq)]
171pub struct GlbLayout {
172    /// Container version represented by this layout.
173    pub format: GltfContainerFormat,
174    /// Declared total container length.
175    pub length: u64,
176    /// Ordered chunk descriptors.
177    pub chunks: Vec<GlbChunkDescriptor>,
178}
179
180/// Seekable, range-backed GLB source.
181///
182/// Opening the source reads only headers and chunk descriptors. Callers choose
183/// which chunk to materialize and can enforce an independent quota for each
184/// range; this is the non-slice path for GLB v3 files larger than addressable
185/// memory.
186pub struct GlbRangeReader<R> {
187    input: R,
188    layout: GlbLayout,
189}
190
191impl<R: Read + Seek> GlbRangeReader<R> {
192    /// Inspects a seekable source without materializing chunk payloads.
193    pub fn open(mut input: R) -> Result<Self> {
194        let layout = inspect_glb(&mut input)?;
195        Ok(Self { input, layout })
196    }
197
198    /// Returns the inspected container layout.
199    pub fn layout(&self) -> &GlbLayout {
200        &self.layout
201    }
202
203    /// Materializes one described chunk after checking the caller's limit.
204    pub fn read_chunk(
205        &mut self,
206        descriptor: GlbChunkDescriptor,
207        max_bytes: Option<usize>,
208    ) -> Result<Vec<u8>> {
209        if !self.layout.chunks.contains(&descriptor) {
210            return Err(GltfError::InvalidGlb(
211                "GLB chunk does not belong to this source".into(),
212            ));
213        }
214        let length = usize::try_from(descriptor.length).map_err(|_| {
215            GltfError::ResourceLimitExceeded("GLB chunk exceeds this platform".into())
216        })?;
217        check_limit(length, max_bytes, "GLB chunk")?;
218        let mut bytes = Vec::new();
219        bytes
220            .try_reserve_exact(length)
221            .map_err(|_| GltfError::ResourceLimitExceeded("GLB chunk allocation failed".into()))?;
222        bytes.resize(length, 0);
223        self.input.seek(SeekFrom::Start(descriptor.offset))?;
224        self.input.read_exact(&mut bytes)?;
225        Ok(bytes)
226    }
227
228    /// Returns the underlying seekable source.
229    pub fn into_inner(self) -> R {
230        self.input
231    }
232}
233
234/// Inspects GLB v2/v3 headers from a seekable source without allocating chunks.
235pub fn inspect_glb<R: Read + Seek>(input: &mut R) -> Result<GlbLayout> {
236    input.seek(SeekFrom::Start(0))?;
237    let magic = read_u32_stream(input)?;
238    if magic != GLB_MAGIC {
239        return Err(GltfError::InvalidGlb("input is not a GLB container".into()));
240    }
241    let version = read_u32_stream(input)?;
242    let (format, length, header, chunk_header) = match version {
243        GLB_VERSION_V2 => (
244            GltfContainerFormat::GlbV2,
245            u64::from(read_u32_stream(input)?),
246            12u64,
247            8u64,
248        ),
249        GLB_VERSION_V3 => (
250            GltfContainerFormat::GlbV3,
251            read_u64_stream(input)?,
252            16u64,
253            16u64,
254        ),
255        _ => {
256            return Err(GltfError::InvalidGlb(format!(
257                "unsupported GLB version {version}"
258            )))
259        }
260    };
261    let actual = input.seek(SeekFrom::End(0))?;
262    if actual != length {
263        return Err(GltfError::InvalidGlb(
264            "GLB header length does not match stream length".into(),
265        ));
266    }
267    input.seek(SeekFrom::Start(header))?;
268    let mut chunks = Vec::new();
269    let mut offset = header;
270    while offset < length {
271        if length - offset < chunk_header {
272            return Err(GltfError::InvalidGlb("partial GLB chunk header".into()));
273        }
274        let chunk_length = if format == GltfContainerFormat::GlbV3 {
275            read_u64_stream(input)?
276        } else {
277            u64::from(read_u32_stream(input)?)
278        };
279        let kind = read_u32_stream(input)?;
280        let encoding = if format == GltfContainerFormat::GlbV3 {
281            read_u32_stream(input)?
282        } else {
283            0
284        };
285        if encoding != 0 {
286            return Err(GltfError::InvalidGlb(
287                "GLB v3 chunk encoding is reserved and must be zero".into(),
288            ));
289        }
290        if chunk_length % 4 != 0 || chunk_length > length - offset - chunk_header {
291            return Err(GltfError::InvalidGlb("invalid GLB chunk length".into()));
292        }
293        chunks.push(GlbChunkDescriptor {
294            offset: offset + chunk_header,
295            length: chunk_length,
296            kind,
297            encoding,
298        });
299        offset = offset
300            .checked_add(chunk_header)
301            .and_then(|value| value.checked_add(chunk_length))
302            .ok_or_else(|| GltfError::InvalidGlb("GLB chunk offset overflow".into()))?;
303        input.seek(SeekFrom::Start(offset))?;
304    }
305    Ok(GlbLayout {
306        format,
307        length,
308        chunks,
309    })
310}
311
312/// Optional resource quotas. `None` means unlimited.
313#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
314pub struct ResourceLimits {
315    /// Maximum decoded size of any one resource.
316    pub max_resource_bytes: Option<usize>,
317    /// Maximum decoded size of all glTF buffers combined.
318    pub max_total_buffer_bytes: Option<usize>,
319    /// Maximum decoded image pixel count. Image decoders enforce this limit.
320    pub max_image_pixels: Option<u64>,
321    /// Maximum number of explicit nested glTF assets on one `files` chain.
322    ///
323    /// The document layer owns graph traversal; this quota bounds each
324    /// caller-directed chain without triggering implicit recursion.
325    pub max_external_asset_depth: Option<usize>,
326}
327
328/// One glTF `buffers[]` declaration, independent of a JSON front end.
329#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
330pub struct GltfBufferReference<'a> {
331    /// Optional data or companion-resource URI.
332    pub uri: Option<&'a str>,
333    /// Declared `byteLength` of the logical buffer, excluding GLB padding.
334    pub byte_length: usize,
335    /// `EXT_meshopt_compression` fallback buffer: it carries no stored bytes
336    /// and starts out zeroed, waiting for its buffer views to be decoded.
337    pub meshopt_fallback: bool,
338}
339
340/// Synchronous resolver for non-data resource URIs.
341pub trait ResourceResolver {
342    /// Resolve `uri` to its exact bytes.
343    fn resolve(&self, uri: &str) -> Result<Vec<u8>>;
344
345    /// Resolve with a decoded-byte limit when the implementation can preflight
346    /// it. The default preserves compatibility for custom resolvers and checks
347    /// their returned bytes; filesystem resolvers override this to check file
348    /// metadata before allocating.
349    fn resolve_with_limit(&self, uri: &str, max_bytes: Option<usize>) -> Result<Vec<u8>> {
350        let data = self.resolve(uri)?;
351        check_limit(data.len(), max_bytes, uri)?;
352        Ok(data)
353    }
354}
355
356/// Resolve a data or companion-resource URI with an optional byte quota.
357pub fn resolve_resource_uri(
358    uri: &str,
359    resolver: Option<&dyn ResourceResolver>,
360    max_bytes: Option<usize>,
361) -> Result<Vec<u8>> {
362    if uri.starts_with("data:") {
363        return decode_data_uri(uri, max_bytes);
364    }
365    let resolver = resolver.ok_or_else(|| GltfError::ExternalResourceDenied(uri.to_owned()))?;
366    resolver.resolve_with_limit(uri, max_bytes)
367}
368
369impl<F> ResourceResolver for F
370where
371    F: Fn(&str) -> Result<Vec<u8>>,
372{
373    fn resolve(&self, uri: &str) -> Result<Vec<u8>> {
374        self(uri)
375    }
376}
377
378/// Policy used by [`FileResourceResolver`].
379#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
380pub enum ExternalFilePolicy {
381    /// Reject every external file URI.
382    #[default]
383    Deny,
384    /// Allow paths outside the base directory.
385    Allow,
386    /// Require resolved paths to remain below the base directory.
387    ConfineToBase,
388}
389
390/// Filesystem resolver used by convenience file-loading APIs.
391#[derive(Clone, Debug)]
392pub struct FileResourceResolver {
393    base: PathBuf,
394    policy: ExternalFilePolicy,
395}
396
397impl FileResourceResolver {
398    /// Create a resolver rooted at `base`.
399    pub fn new(base: impl Into<PathBuf>, policy: ExternalFilePolicy) -> Self {
400        Self {
401            base: base.into(),
402            policy,
403        }
404    }
405}
406
407impl ResourceResolver for FileResourceResolver {
408    fn resolve(&self, uri: &str) -> Result<Vec<u8>> {
409        self.resolve_with_limit(uri, None)
410    }
411
412    fn resolve_with_limit(&self, uri: &str, max_bytes: Option<usize>) -> Result<Vec<u8>> {
413        if self.policy == ExternalFilePolicy::Deny {
414            return Err(GltfError::ExternalResourceDenied(uri.to_owned()));
415        }
416        if uri.contains("://") || uri.starts_with("data:") {
417            return Err(GltfError::Unsupported(format!(
418                "unsupported external resource URI: {uri}"
419            )));
420        }
421
422        let decoded = percent_decode(uri)?;
423        let decoded = std::str::from_utf8(&decoded).map_err(|_| {
424            GltfError::InvalidGltf("external resource path is not valid UTF-8".into())
425        })?;
426        let candidate = self.base.join(Path::new(decoded));
427        if self.policy == ExternalFilePolicy::ConfineToBase {
428            let base = self.base.canonicalize()?;
429            let path = candidate.canonicalize()?;
430            if !path.starts_with(&base) {
431                return Err(GltfError::ExternalResourceDenied(uri.to_owned()));
432            }
433            return read_file_fallibly(&path, max_bytes);
434        }
435        read_file_fallibly(&candidate, max_bytes)
436    }
437}
438
439fn read_file_fallibly(path: &Path, max_bytes: Option<usize>) -> Result<Vec<u8>> {
440    let length_u64 = fs::metadata(path)?.len();
441    let length = usize::try_from(length_u64).map_err(|_| {
442        GltfError::ResourceLimitExceeded(format!(
443            "{} is too large for this platform",
444            path.display()
445        ))
446    })?;
447    check_limit(length, max_bytes, &path.display().to_string())?;
448
449    let mut data = Vec::new();
450    data.try_reserve_exact(length).map_err(|_| {
451        GltfError::ResourceLimitExceeded(format!("{} allocation failed", path.display()))
452    })?;
453    data.resize(length, 0);
454    let mut file = File::open(path)?;
455    file.read_exact(&mut data)?;
456    let mut extra = [0u8; 1];
457    if file.read(&mut extra)? != 0 {
458        return Err(GltfError::InvalidGltf(format!(
459            "resource {} grew while it was read",
460            path.display()
461        )));
462    }
463    Ok(data)
464}
465
466/// Strictly parse JSON glTF, GLB 2.0, or the draft GLB 3 container.
467pub fn parse_gltf_container(data: &[u8]) -> Result<GltfContainer<'_>> {
468    if data.len() < 4 || read_u32(data, 0)? != GLB_MAGIC {
469        return Ok(GltfContainer {
470            format: GltfContainerFormat::Gltf,
471            json: data,
472            bin: None,
473        });
474    }
475    if data.len() < 12 {
476        return Err(GltfError::InvalidGlb(
477            "file is too small for a GLB header".into(),
478        ));
479    }
480    let version = read_u32(data, 4)?;
481    if version == GLB_VERSION_V3 {
482        return parse_glb_v3(data);
483    }
484    if version != GLB_VERSION_V2 {
485        return Err(GltfError::InvalidGlb(format!(
486            "unsupported GLB version {version}"
487        )));
488    }
489    let declared = usize::try_from(read_u32(data, 8)?)
490        .map_err(|_| GltfError::InvalidGlb("GLB length cannot fit usize".into()))?;
491    if declared != data.len() {
492        return Err(GltfError::InvalidGlb(
493            "GLB header length does not match file length".into(),
494        ));
495    }
496
497    let mut offset = 12usize;
498    let mut chunk_index = 0usize;
499    let mut json = None;
500    let mut bin = None;
501    while offset < declared {
502        let header_end = offset
503            .checked_add(8)
504            .filter(|end| *end <= declared)
505            .ok_or_else(|| GltfError::InvalidGlb("partial GLB chunk header".into()))?;
506        let length = usize::try_from(read_u32(data, offset)?)
507            .map_err(|_| GltfError::InvalidGlb("chunk length cannot fit usize".into()))?;
508        let kind = read_u32(data, offset + 4)?;
509        if !length.is_multiple_of(4) {
510            return Err(GltfError::InvalidGlb(
511                "GLB chunk length is not 4-byte aligned".into(),
512            ));
513        }
514        let end = header_end
515            .checked_add(length)
516            .filter(|end| *end <= declared)
517            .ok_or_else(|| GltfError::InvalidGlb("GLB chunk extends past file end".into()))?;
518        let bytes = &data[header_end..end];
519        match kind {
520            GLB_CHUNK_JSON => {
521                if chunk_index != 0 || json.replace(bytes).is_some() {
522                    return Err(GltfError::InvalidGlb(
523                        "JSON must be the first and only JSON chunk".into(),
524                    ));
525                }
526                // JSON permits trailing whitespace, which is indistinguishable
527                // from the space bytes used to pad a GLB JSON chunk. Keep the
528                // full chunk for the JSON parser and reject only an empty chunk
529                // here; malformed trailing bytes are rejected during parsing.
530                if !bytes
531                    .iter()
532                    .any(|byte| !matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
533                {
534                    return Err(GltfError::InvalidGlb("JSON chunk is empty".into()));
535                }
536            }
537            GLB_CHUNK_BIN => {
538                if chunk_index != 1 || bin.replace(bytes).is_some() {
539                    return Err(GltfError::InvalidGlb(
540                        "BIN must be the second and only BIN chunk".into(),
541                    ));
542                }
543            }
544            _ => {
545                if chunk_index == 0 {
546                    return Err(GltfError::InvalidGlb("JSON chunk must be first".into()));
547                }
548            }
549        }
550        offset = end;
551        chunk_index += 1;
552    }
553
554    Ok(GltfContainer {
555        format: GltfContainerFormat::GlbV2,
556        json: json.ok_or_else(|| GltfError::InvalidGlb("GLB has no JSON chunk".into()))?,
557        bin,
558    })
559}
560
561/// Parses the current draft GLB v3 wire format. Its header is
562/// `magic:u32, version:u32, length:u64`; each chunk header is
563/// `length:u64, type:u32, encoding:u32`. glTF 2.1 reserves `encoding` and
564/// requires it to be zero.
565fn parse_glb_v3(data: &[u8]) -> Result<GltfContainer<'_>> {
566    if data.len() < 16 {
567        return Err(GltfError::InvalidGlb(
568            "file is too small for a GLB v3 header".into(),
569        ));
570    }
571    let declared = read_u64(data, 8)?;
572    let actual = u64::try_from(data.len())
573        .map_err(|_| GltfError::InvalidGlb("input length cannot fit u64".into()))?;
574    if declared != actual {
575        return Err(GltfError::InvalidGlb(
576            "GLB v3 header length does not match file length".into(),
577        ));
578    }
579
580    let mut offset = 16usize;
581    let mut chunk_index = 0usize;
582    let mut json = None;
583    let mut bin = None;
584    while offset < data.len() {
585        let header_end = offset
586            .checked_add(16)
587            .filter(|end| *end <= data.len())
588            .ok_or_else(|| GltfError::InvalidGlb("partial GLB v3 chunk header".into()))?;
589        let length = usize::try_from(read_u64(data, offset)?).map_err(|_| {
590            GltfError::ResourceLimitExceeded("GLB v3 chunk exceeds platform address space".into())
591        })?;
592        let kind = read_u32(data, offset + 8)?;
593        let encoding = read_u32(data, offset + 12)?;
594        if encoding != 0 {
595            return Err(GltfError::InvalidGlb(
596                "GLB v3 chunk encoding is reserved and must be zero".into(),
597            ));
598        }
599        if !length.is_multiple_of(4) {
600            return Err(GltfError::InvalidGlb(
601                "GLB v3 chunk length is not 4-byte aligned".into(),
602            ));
603        }
604        let end = header_end
605            .checked_add(length)
606            .filter(|end| *end <= data.len())
607            .ok_or_else(|| GltfError::InvalidGlb("GLB v3 chunk extends past file end".into()))?;
608        let bytes = &data[header_end..end];
609        match kind {
610            GLB_CHUNK_JSON => {
611                if chunk_index != 0 || json.replace(bytes).is_some() {
612                    return Err(GltfError::InvalidGlb(
613                        "JSON must be the first and only GLB v3 JSON chunk".into(),
614                    ));
615                }
616                if !bytes
617                    .iter()
618                    .any(|byte| !matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
619                {
620                    return Err(GltfError::InvalidGlb("GLB v3 JSON chunk is empty".into()));
621                }
622            }
623            GLB_CHUNK_BIN if chunk_index == 1 && bin.is_none() => bin = Some(bytes),
624            GLB_CHUNK_BIN => {
625                return Err(GltfError::InvalidGlb(
626                    "BIN must be the second and only GLB v3 BIN chunk".into(),
627                ));
628            }
629            _ if chunk_index == 0 => {
630                return Err(GltfError::InvalidGlb(
631                    "GLB v3 JSON chunk must be first".into(),
632                ));
633            }
634            _ => {}
635        }
636        offset = end;
637        chunk_index = chunk_index
638            .checked_add(1)
639            .ok_or_else(|| GltfError::InvalidGlb("too many GLB v3 chunks".into()))?;
640    }
641    Ok(GltfContainer {
642        format: GltfContainerFormat::GlbV3,
643        json: json.ok_or_else(|| GltfError::InvalidGlb("GLB v3 has no JSON chunk".into()))?,
644        bin,
645    })
646}
647
648/// Parses only the JSON and optional BIN slices from a GLB container.
649///
650/// This keeps strict GLB container checks available to callers that do not need
651/// to construct a document model.
652pub fn parse_glb_json_and_bin(data: &[u8]) -> Result<(&[u8], Option<&[u8]>)> {
653    if data.len() < 4
654        || u32::from_le_bytes(
655            data[0..4]
656                .try_into()
657                .map_err(|_| GltfError::InvalidGlb("short GLB magic".into()))?,
658        ) != GLB_MAGIC
659    {
660        return Err(GltfError::InvalidGlb("input is not a GLB container".into()));
661    }
662    if data.len() < 12 {
663        return Err(GltfError::InvalidGlb("GLB header is truncated".into()));
664    }
665    let version = u32::from_le_bytes(data[4..8].try_into().unwrap());
666    if version != GLB_VERSION_V2 {
667        return Err(GltfError::InvalidGlb("unsupported GLB version".into()));
668    }
669    let declared = usize::try_from(u32::from_le_bytes(data[8..12].try_into().unwrap()))
670        .map_err(|_| GltfError::InvalidGlb("GLB length is too large".into()))?;
671    if declared != data.len() {
672        return Err(GltfError::InvalidGlb(
673            "GLB length does not match input".into(),
674        ));
675    }
676    let mut offset = 12usize;
677    let mut chunks = 0usize;
678    let mut json = None;
679    let mut bin = None;
680    while offset < declared {
681        let header_end = offset
682            .checked_add(8)
683            .filter(|end| *end <= declared)
684            .ok_or_else(|| GltfError::InvalidGlb("GLB chunk header is truncated".into()))?;
685        let length = usize::try_from(u32::from_le_bytes(
686            data[offset..offset + 4].try_into().unwrap(),
687        ))
688        .map_err(|_| GltfError::InvalidGlb("GLB chunk is too large".into()))?;
689        if !length.is_multiple_of(4) {
690            return Err(GltfError::InvalidGlb(
691                "GLB chunk is not 4-byte aligned".into(),
692            ));
693        }
694        let kind = u32::from_le_bytes(data[offset + 4..offset + 8].try_into().unwrap());
695        let end = header_end
696            .checked_add(length)
697            .filter(|end| *end <= declared)
698            .ok_or_else(|| GltfError::InvalidGlb("GLB chunk exceeds input".into()))?;
699        match kind {
700            GLB_CHUNK_JSON if chunks == 0 && json.is_none() => {
701                let bytes = &data[header_end..end];
702                if bytes
703                    .iter()
704                    .all(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
705                {
706                    return Err(GltfError::InvalidGlb("GLB JSON chunk is empty".into()));
707                }
708                json = Some(bytes);
709            }
710            GLB_CHUNK_JSON => {
711                return Err(GltfError::InvalidGlb(
712                    "GLB JSON must be first and unique".into(),
713                ))
714            }
715            GLB_CHUNK_BIN if bin.is_none() => bin = Some(&data[header_end..end]),
716            GLB_CHUNK_BIN => {
717                return Err(GltfError::InvalidGlb(
718                    "GLB contains duplicate BIN chunks".into(),
719                ))
720            }
721            _ => {}
722        }
723        offset = end;
724        chunks = chunks
725            .checked_add(1)
726            .ok_or_else(|| GltfError::InvalidGlb("too many GLB chunks".into()))?;
727    }
728    json.map(|json| (json, bin))
729        .ok_or_else(|| GltfError::InvalidGlb("GLB JSON chunk is missing".into()))
730}
731
732/// Resolves all declared buffer references under the configured quotas.
733pub fn resolve_gltf_buffers(
734    references: &[GltfBufferReference<'_>],
735    format: GltfContainerFormat,
736    glb_bin: Option<&[u8]>,
737    resolver: Option<&dyn ResourceResolver>,
738    limits: &ResourceLimits,
739) -> Result<Vec<Vec<u8>>> {
740    if format == GltfContainerFormat::Gltf && glb_bin.is_some() {
741        return Err(GltfError::InvalidGltf(
742            "JSON glTF input cannot have a GLB BIN chunk".into(),
743        ));
744    }
745    if references.is_empty() && glb_bin.is_some() {
746        return Err(GltfError::InvalidGlb(
747            "GLB has a BIN chunk but declares no buffer".into(),
748        ));
749    }
750    if glb_bin.is_some()
751        && references
752            .first()
753            .is_some_and(|buffer| buffer.uri.is_some())
754    {
755        return Err(GltfError::InvalidGlb(
756            "GLB BIN chunk requires buffer 0 without a URI".into(),
757        ));
758    }
759
760    let mut buffers = Vec::new();
761    buffers
762        .try_reserve_exact(references.len())
763        .map_err(|_| GltfError::ResourceLimitExceeded("buffer table allocation failed".into()))?;
764    let mut total = 0usize;
765    for (index, reference) in references.iter().enumerate() {
766        let remaining_total = limits
767            .max_total_buffer_bytes
768            .map(|limit| {
769                limit.checked_sub(total).ok_or_else(|| {
770                    GltfError::ResourceLimitExceeded(
771                        "glTF buffers exceed the configured total".into(),
772                    )
773                })
774            })
775            .transpose()?;
776        if remaining_total.is_some_and(|remaining| reference.byte_length > remaining) {
777            return Err(GltfError::ResourceLimitExceeded(format!(
778                "buffer {index} byteLength {} exceeds the remaining total quota",
779                reference.byte_length
780            )));
781        }
782        let effective_limit = match (limits.max_resource_bytes, remaining_total) {
783            (Some(resource), Some(total)) => Some(resource.min(total)),
784            (Some(resource), None) => Some(resource),
785            (None, Some(total)) => Some(total),
786            (None, None) => None,
787        };
788        let effective_limits = ResourceLimits {
789            max_resource_bytes: effective_limit,
790            ..*limits
791        };
792        let data = resolve_gltf_buffer(
793            index,
794            *reference,
795            format,
796            glb_bin,
797            resolver,
798            &effective_limits,
799        )?;
800        total = total
801            .checked_add(data.len())
802            .ok_or_else(|| GltfError::ResourceLimitExceeded("total buffer size overflow".into()))?;
803        check_limit(total, limits.max_total_buffer_bytes, "glTF buffers total")?;
804        buffers.push(data);
805    }
806    Ok(buffers)
807}
808
809fn resolve_gltf_buffer(
810    index: usize,
811    reference: GltfBufferReference<'_>,
812    format: GltfContainerFormat,
813    glb_bin: Option<&[u8]>,
814    resolver: Option<&dyn ResourceResolver>,
815    limits: &ResourceLimits,
816) -> Result<Vec<u8>> {
817    // A fallback buffer only carries stored bytes when the extension is
818    // optional; when it is required the buffer starts out zeroed and its views
819    // are filled in by the meshopt decoder.
820    if reference.meshopt_fallback && reference.uri.is_none() && !(index == 0 && glb_bin.is_some()) {
821        check_limit(
822            reference.byte_length,
823            limits.max_resource_bytes,
824            "meshopt fallback buffer",
825        )?;
826        let mut data = Vec::new();
827        data.try_reserve_exact(reference.byte_length).map_err(|_| {
828            GltfError::ResourceLimitExceeded("meshopt fallback buffer allocation failed".into())
829        })?;
830        data.resize(reference.byte_length, 0);
831        return Ok(data);
832    }
833
834    if let Some(uri) = reference.uri {
835        let mut data = resolve_resource_uri(uri, resolver, limits.max_resource_bytes)?;
836        validate_declared_buffer_length(index, reference.byte_length, data.len(), false)?;
837        data.truncate(reference.byte_length);
838        return Ok(data);
839    }
840
841    if !format.is_glb() {
842        return Err(GltfError::InvalidGltf(format!(
843            "Buffer {index} has no URI in JSON glTF"
844        )));
845    }
846    if index != 0 {
847        return Err(GltfError::InvalidGlb(format!(
848            "Buffer {index} has no URI and is not buffer 0"
849        )));
850    }
851    let bin = glb_bin.ok_or_else(|| {
852        GltfError::InvalidGlb("Buffer 0 has no URI but GLB has no BIN chunk".into())
853    })?;
854    check_limit(bin.len(), limits.max_resource_bytes, "GLB BIN chunk")?;
855    validate_declared_buffer_length(index, reference.byte_length, bin.len(), true)?;
856    if bin[reference.byte_length..]
857        .iter()
858        .any(|&padding| padding != 0)
859    {
860        return Err(GltfError::InvalidGlb(
861            "GLB BIN padding must contain only zero bytes".into(),
862        ));
863    }
864    copy_prefix(bin, reference.byte_length, "GLB BIN chunk")
865}
866
867fn validate_declared_buffer_length(
868    index: usize,
869    declared: usize,
870    actual: usize,
871    glb_bin: bool,
872) -> Result<()> {
873    if actual < declared {
874        return Err(GltfError::InvalidGltf(format!(
875            "Buffer {index} byteLength {declared} exceeds resource length {actual}"
876        )));
877    }
878    if glb_bin {
879        let padded_limit = declared
880            .checked_add(3)
881            .ok_or_else(|| GltfError::InvalidGlb("buffer byteLength overflow".into()))?;
882        if actual > padded_limit {
883            return Err(GltfError::InvalidGlb(format!(
884                "GLB BIN chunk length {actual} is more than 3 bytes larger than buffer[0].byteLength {declared}"
885            )));
886        }
887    }
888    Ok(())
889}
890
891fn copy_prefix(data: &[u8], length: usize, label: &str) -> Result<Vec<u8>> {
892    let prefix = data
893        .get(..length)
894        .ok_or_else(|| GltfError::InvalidGltf(format!("{label} is truncated")))?;
895    let mut output = Vec::new();
896    output
897        .try_reserve_exact(length)
898        .map_err(|_| GltfError::ResourceLimitExceeded(format!("{label} allocation failed")))?;
899    output.extend_from_slice(prefix);
900    Ok(output)
901}
902
903/// Decode a `data:` URI with an optional decoded-byte quota.
904///
905/// ```
906/// # use draco_io::decode_data_uri;
907/// assert_eq!(decode_data_uri("data:text/plain;base64,aGk=", None)?, b"hi");
908/// # Ok::<(), draco_io::GltfError>(())
909/// ```
910pub fn decode_data_uri(uri: &str, max_bytes: Option<usize>) -> Result<Vec<u8>> {
911    let body = uri
912        .strip_prefix("data:")
913        .ok_or_else(|| GltfError::InvalidGltf("URI is not a data URI".into()))?;
914    let comma = body
915        .find(',')
916        .ok_or_else(|| GltfError::InvalidGltf("data URI has no comma".into()))?;
917    let metadata = &body[..comma];
918    let payload = &body[comma + 1..];
919    let is_base64 = metadata
920        .split(';')
921        .skip(1)
922        .any(|part| part.eq_ignore_ascii_case("base64"));
923    let decoded = if is_base64 {
924        decode_base64(payload, max_bytes)?
925    } else {
926        percent_decode_with_limit(payload, max_bytes, "data URI")?
927    };
928    Ok(decoded)
929}
930
931fn read_u32(data: &[u8], offset: usize) -> Result<u32> {
932    let end = offset
933        .checked_add(4)
934        .filter(|end| *end <= data.len())
935        .ok_or_else(|| GltfError::InvalidGlb("truncated u32".into()))?;
936    let mut bytes = [0u8; 4];
937    bytes.copy_from_slice(&data[offset..end]);
938    Ok(u32::from_le_bytes(bytes))
939}
940
941fn read_u64(data: &[u8], offset: usize) -> Result<u64> {
942    let end = offset
943        .checked_add(8)
944        .filter(|end| *end <= data.len())
945        .ok_or_else(|| GltfError::InvalidGlb("truncated u64".into()))?;
946    let mut bytes = [0u8; 8];
947    bytes.copy_from_slice(&data[offset..end]);
948    Ok(u64::from_le_bytes(bytes))
949}
950
951fn read_u32_stream<R: Read>(input: &mut R) -> Result<u32> {
952    let mut bytes = [0; 4];
953    input.read_exact(&mut bytes)?;
954    Ok(u32::from_le_bytes(bytes))
955}
956fn read_u64_stream<R: Read>(input: &mut R) -> Result<u64> {
957    let mut bytes = [0; 8];
958    input.read_exact(&mut bytes)?;
959    Ok(u64::from_le_bytes(bytes))
960}
961
962fn check_limit(length: usize, limit: Option<usize>, resource: &str) -> Result<()> {
963    if limit.is_some_and(|limit| length > limit) {
964        return Err(GltfError::ResourceLimitExceeded(format!(
965            "{resource} is {length} bytes"
966        )));
967    }
968    Ok(())
969}
970
971fn decode_base64(input: &str, limit: Option<usize>) -> Result<Vec<u8>> {
972    if input.bytes().any(|byte| byte.is_ascii_whitespace()) {
973        return Err(GltfError::InvalidGltf(
974            "base64 data must not contain whitespace".into(),
975        ));
976    }
977    if !input.len().is_multiple_of(4) {
978        return Err(GltfError::InvalidGltf(
979            "base64 length must be divisible by four".into(),
980        ));
981    }
982    let padding = input
983        .as_bytes()
984        .iter()
985        .rev()
986        .take_while(|b| **b == b'=')
987        .count();
988    if padding > 2 || input.as_bytes()[..input.len().saturating_sub(padding)].contains(&b'=') {
989        return Err(GltfError::InvalidGltf("invalid base64 padding".into()));
990    }
991    let decoded_len = input
992        .len()
993        .checked_div(4)
994        .and_then(|length| length.checked_mul(3))
995        .and_then(|length| length.checked_sub(padding))
996        .ok_or_else(|| GltfError::ResourceLimitExceeded("base64 size overflow".into()))?;
997    check_limit(decoded_len, limit, "data URI")?;
998    let mut output = Vec::new();
999    output
1000        .try_reserve_exact(decoded_len)
1001        .map_err(|_| GltfError::ResourceLimitExceeded("base64 allocation failed".into()))?;
1002    for chunk in input.as_bytes().chunks_exact(4) {
1003        let a = base64_value(chunk[0])? as u32;
1004        let b = base64_value(chunk[1])? as u32;
1005        let c = if chunk[2] == b'=' {
1006            0
1007        } else {
1008            base64_value(chunk[2])? as u32
1009        };
1010        let d = if chunk[3] == b'=' {
1011            0
1012        } else {
1013            base64_value(chunk[3])? as u32
1014        };
1015        if (chunk[2] == b'=' && b & 0x0f != 0) || (chunk[3] == b'=' && c & 0x03 != 0) {
1016            return Err(GltfError::InvalidGltf(
1017                "base64 has non-zero padding bits".into(),
1018            ));
1019        }
1020        let value = (a << 18) | (b << 12) | (c << 6) | d;
1021        output.push((value >> 16) as u8);
1022        if chunk[2] != b'=' {
1023            output.push((value >> 8) as u8);
1024        }
1025        if chunk[3] != b'=' {
1026            output.push(value as u8);
1027        }
1028    }
1029    debug_assert_eq!(output.len(), decoded_len);
1030    Ok(output)
1031}
1032
1033fn base64_value(byte: u8) -> Result<u8> {
1034    match byte {
1035        b'A'..=b'Z' => Ok(byte - b'A'),
1036        b'a'..=b'z' => Ok(byte - b'a' + 26),
1037        b'0'..=b'9' => Ok(byte - b'0' + 52),
1038        b'+' => Ok(62),
1039        b'/' => Ok(63),
1040        _ => Err(GltfError::InvalidGltf("invalid base64 character".into())),
1041    }
1042}
1043
1044fn percent_decode(input: &str) -> Result<Vec<u8>> {
1045    percent_decode_with_limit(input, None, "percent-encoded URI")
1046}
1047
1048fn percent_decode_with_limit(input: &str, limit: Option<usize>, label: &str) -> Result<Vec<u8>> {
1049    let bytes = input.as_bytes();
1050    let mut decoded_len = 0usize;
1051    let mut index = 0usize;
1052    while index < bytes.len() {
1053        if bytes[index] == b'%' {
1054            let end = index
1055                .checked_add(3)
1056                .filter(|end| *end <= bytes.len())
1057                .ok_or_else(|| GltfError::InvalidGltf("truncated percent escape".into()))?;
1058            let _ = hex(bytes[index + 1])?;
1059            let _ = hex(bytes[index + 2])?;
1060            index = end;
1061        } else {
1062            index += 1;
1063        }
1064        decoded_len = decoded_len.checked_add(1).ok_or_else(|| {
1065            GltfError::ResourceLimitExceeded("percent-decoded size overflow".into())
1066        })?;
1067    }
1068    check_limit(decoded_len, limit, label)?;
1069
1070    let mut output = Vec::new();
1071    output
1072        .try_reserve_exact(decoded_len)
1073        .map_err(|_| GltfError::ResourceLimitExceeded("percent decode allocation failed".into()))?;
1074    let mut index = 0usize;
1075    while index < bytes.len() {
1076        if bytes[index] == b'%' {
1077            let end = index
1078                .checked_add(3)
1079                .filter(|end| *end <= bytes.len())
1080                .ok_or_else(|| GltfError::InvalidGltf("truncated percent escape".into()))?;
1081            let high = hex(bytes[index + 1])?;
1082            let low = hex(bytes[index + 2])?;
1083            output.push((high << 4) | low);
1084            index = end;
1085        } else {
1086            output.push(bytes[index]);
1087            index += 1;
1088        }
1089    }
1090    Ok(output)
1091}
1092
1093fn hex(byte: u8) -> Result<u8> {
1094    match byte {
1095        b'0'..=b'9' => Ok(byte - b'0'),
1096        b'a'..=b'f' => Ok(byte - b'a' + 10),
1097        b'A'..=b'F' => Ok(byte - b'A' + 10),
1098        _ => Err(GltfError::InvalidGltf("invalid percent escape".into())),
1099    }
1100}
1101
1102#[cfg(test)]
1103mod tests {
1104    use std::cell::Cell;
1105
1106    use super::*;
1107
1108    fn raw_glb(chunks: &[(u32, &[u8])]) -> Vec<u8> {
1109        let total = 12
1110            + chunks
1111                .iter()
1112                .map(|(_, bytes)| 8 + bytes.len())
1113                .sum::<usize>();
1114        let mut output = Vec::with_capacity(total);
1115        output.extend_from_slice(&GLB_MAGIC.to_le_bytes());
1116        output.extend_from_slice(&GLB_VERSION_V2.to_le_bytes());
1117        output.extend_from_slice(&(total as u32).to_le_bytes());
1118        for (kind, bytes) in chunks {
1119            output.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
1120            output.extend_from_slice(&kind.to_le_bytes());
1121            output.extend_from_slice(bytes);
1122        }
1123        output
1124    }
1125
1126    #[test]
1127    fn strict_data_uri_rejects_malformed_input() {
1128        assert_eq!(decode_data_uri("data:,a%20b", None).unwrap(), b"a b");
1129        assert_eq!(decode_data_uri("data:;base64,YQ==", None).unwrap(), b"a");
1130        assert!(decode_data_uri("data:;base64,YQ", None).is_err());
1131        assert!(decode_data_uri("data:;base64,YR==", None).is_err());
1132        assert!(decode_data_uri("data:;base64,YWF=", None).is_err());
1133        assert!(decode_data_uri("data:,a%2", None).is_err());
1134        assert!(decode_data_uri("data:;base64,YQ==", Some(0)).is_err());
1135        assert!(decode_data_uri("data:,abcd", Some(2)).is_err());
1136    }
1137
1138    #[test]
1139    fn compact_glb_slice_parser_keeps_strict_container_checks() {
1140        let json = b"{}  ";
1141        let bin = [1u8, 2, 3, 4];
1142        let bytes = raw_glb(&[(GLB_CHUNK_JSON, json), (GLB_CHUNK_BIN, &bin)]);
1143        let (parsed_json, parsed_bin) = parse_glb_json_and_bin(&bytes).unwrap();
1144        assert_eq!(parsed_json, json);
1145        assert_eq!(parsed_bin, Some(bin.as_slice()));
1146
1147        let mut malformed = bytes.clone();
1148        malformed[8..12].copy_from_slice(&(bytes.len() as u32 - 4).to_le_bytes());
1149        assert!(parse_glb_json_and_bin(&malformed).is_err());
1150    }
1151
1152    #[test]
1153    fn buffer_total_quota_is_forwarded_before_resolution() {
1154        struct LimitAwareResolver(Cell<Option<usize>>);
1155
1156        impl ResourceResolver for LimitAwareResolver {
1157            fn resolve(&self, _: &str) -> Result<Vec<u8>> {
1158                panic!("resolve_with_limit must be used")
1159            }
1160
1161            fn resolve_with_limit(&self, _: &str, max_bytes: Option<usize>) -> Result<Vec<u8>> {
1162                self.0.set(max_bytes);
1163                Ok(vec![1, 2])
1164            }
1165        }
1166
1167        let resolver = LimitAwareResolver(Cell::new(None));
1168        let buffers = resolve_gltf_buffers(
1169            &[GltfBufferReference {
1170                uri: Some("mesh.bin"),
1171                byte_length: 2,
1172                meshopt_fallback: false,
1173            }],
1174            GltfContainerFormat::Gltf,
1175            None,
1176            Some(&resolver),
1177            &ResourceLimits {
1178                max_total_buffer_bytes: Some(3),
1179                ..ResourceLimits::default()
1180            },
1181        )
1182        .unwrap();
1183        assert_eq!(buffers, [vec![1, 2]]);
1184        assert_eq!(resolver.0.get(), Some(3));
1185
1186        assert!(resolve_gltf_buffers(
1187            &[GltfBufferReference {
1188                uri: Some("data:,abcd"),
1189                byte_length: 4,
1190                meshopt_fallback: false,
1191            }],
1192            GltfContainerFormat::Gltf,
1193            None,
1194            None,
1195            &ResourceLimits {
1196                max_total_buffer_bytes: Some(2),
1197                ..ResourceLimits::default()
1198            },
1199        )
1200        .is_err());
1201    }
1202}