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;
5#[cfg(feature = "gltf-writer")]
6use std::io::{self, Write};
7use std::path::{Path, PathBuf};
8
9#[cfg(any(feature = "gltf-writer", test))]
10use serde_json::Value;
11
12use crate::gltf_geometry::{GltfError, Result};
13
14const GLB_MAGIC: u32 = 0x4654_6c67;
15const GLB_VERSION: u32 = 2;
16const GLB_CHUNK_JSON: u32 = 0x4e4f_534a;
17const GLB_CHUNK_BIN: u32 = 0x004e_4942;
18
19/// Container used by an input glTF document.
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum GltfContainerFormat {
22    /// JSON glTF document.
23    Gltf,
24    /// Binary GLB 2.0 document.
25    Glb,
26}
27
28/// Borrowed, strictly parsed glTF container.
29#[derive(Clone, Copy, Debug)]
30pub struct GltfContainer<'a> {
31    /// Input container kind.
32    pub format: GltfContainerFormat,
33    /// JSON document bytes (including legal GLB JSON padding).
34    pub json: &'a [u8],
35    /// Optional GLB BIN chunk.
36    pub bin: Option<&'a [u8]>,
37}
38
39/// Output selection shared by the native and WASM glTF APIs.
40#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
41pub enum OutputFormat {
42    /// Preserve the input container kind.
43    #[default]
44    SameAsInput,
45    /// Emit JSON glTF with its buffer embedded as a data URI.
46    GltfEmbeddedBuffers,
47    /// Emit a binary GLB 2.0 container.
48    Glb,
49}
50
51/// Optional resource quotas. `None` means unlimited.
52#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
53pub struct ResourceLimits {
54    /// Maximum decoded size of any one resource.
55    pub max_resource_bytes: Option<usize>,
56    /// Maximum decoded size of all glTF buffers combined.
57    pub max_total_buffer_bytes: Option<usize>,
58    /// Maximum decoded image pixel count. Image decoders enforce this limit.
59    pub max_image_pixels: Option<u64>,
60}
61
62/// One glTF `buffers[]` declaration, independent of a JSON front end.
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub struct GltfBufferReference<'a> {
65    /// Optional data or companion-resource URI.
66    pub uri: Option<&'a str>,
67    /// Declared `byteLength` of the logical buffer, excluding GLB padding.
68    pub byte_length: usize,
69}
70
71/// Synchronous resolver for non-data resource URIs.
72pub trait ResourceResolver {
73    /// Resolve `uri` to its exact bytes.
74    fn resolve(&self, uri: &str) -> Result<Vec<u8>>;
75
76    /// Resolve with a decoded-byte limit when the implementation can preflight
77    /// it. The default preserves compatibility for custom resolvers and checks
78    /// their returned bytes; filesystem resolvers override this to check file
79    /// metadata before allocating.
80    fn resolve_with_limit(&self, uri: &str, max_bytes: Option<usize>) -> Result<Vec<u8>> {
81        let data = self.resolve(uri)?;
82        check_limit(data.len(), max_bytes, uri)?;
83        Ok(data)
84    }
85}
86
87/// Resolve a data or companion-resource URI with an optional byte quota.
88pub fn resolve_resource_uri(
89    uri: &str,
90    resolver: Option<&dyn ResourceResolver>,
91    max_bytes: Option<usize>,
92) -> Result<Vec<u8>> {
93    if uri.starts_with("data:") {
94        return decode_data_uri(uri, max_bytes);
95    }
96    let resolver = resolver.ok_or_else(|| GltfError::ExternalResourceDenied(uri.to_owned()))?;
97    resolver.resolve_with_limit(uri, max_bytes)
98}
99
100impl<F> ResourceResolver for F
101where
102    F: Fn(&str) -> Result<Vec<u8>>,
103{
104    fn resolve(&self, uri: &str) -> Result<Vec<u8>> {
105        self(uri)
106    }
107}
108
109/// Policy used by [`FileResourceResolver`].
110#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
111pub enum ExternalFilePolicy {
112    /// Reject every external file URI.
113    #[default]
114    Deny,
115    /// Allow paths outside the base directory.
116    Allow,
117    /// Require resolved paths to remain below the base directory.
118    ConfineToBase,
119}
120
121/// Native filesystem resolver used by convenience file-loading APIs.
122#[derive(Clone, Debug)]
123pub struct FileResourceResolver {
124    base: PathBuf,
125    policy: ExternalFilePolicy,
126}
127
128impl FileResourceResolver {
129    /// Create a resolver rooted at `base`.
130    pub fn new(base: impl Into<PathBuf>, policy: ExternalFilePolicy) -> Self {
131        Self {
132            base: base.into(),
133            policy,
134        }
135    }
136}
137
138impl ResourceResolver for FileResourceResolver {
139    fn resolve(&self, uri: &str) -> Result<Vec<u8>> {
140        self.resolve_with_limit(uri, None)
141    }
142
143    fn resolve_with_limit(&self, uri: &str, max_bytes: Option<usize>) -> Result<Vec<u8>> {
144        if self.policy == ExternalFilePolicy::Deny {
145            return Err(GltfError::ExternalResourceDenied(uri.to_owned()));
146        }
147        if uri.contains("://") || uri.starts_with("data:") {
148            return Err(GltfError::Unsupported(format!(
149                "unsupported external resource URI: {uri}"
150            )));
151        }
152
153        let decoded = percent_decode(uri)?;
154        let decoded = std::str::from_utf8(&decoded).map_err(|_| {
155            GltfError::InvalidGltf("external resource path is not valid UTF-8".into())
156        })?;
157        let candidate = self.base.join(Path::new(decoded));
158        if self.policy == ExternalFilePolicy::ConfineToBase {
159            let base = self.base.canonicalize()?;
160            let path = candidate.canonicalize()?;
161            if !path.starts_with(&base) {
162                return Err(GltfError::ExternalResourceDenied(uri.to_owned()));
163            }
164            return read_file_fallibly(&path, max_bytes);
165        }
166        read_file_fallibly(&candidate, max_bytes)
167    }
168}
169
170fn read_file_fallibly(path: &Path, max_bytes: Option<usize>) -> Result<Vec<u8>> {
171    let length_u64 = fs::metadata(path)?.len();
172    let length = usize::try_from(length_u64).map_err(|_| {
173        GltfError::ResourceLimitExceeded(format!(
174            "{} is too large for this platform",
175            path.display()
176        ))
177    })?;
178    check_limit(length, max_bytes, &path.display().to_string())?;
179
180    let mut data = Vec::new();
181    data.try_reserve_exact(length).map_err(|_| {
182        GltfError::ResourceLimitExceeded(format!("{} allocation failed", path.display()))
183    })?;
184    data.resize(length, 0);
185    let mut file = File::open(path)?;
186    file.read_exact(&mut data)?;
187    let mut extra = [0u8; 1];
188    if file.read(&mut extra)? != 0 {
189        return Err(GltfError::InvalidGltf(format!(
190            "resource {} grew while it was read",
191            path.display()
192        )));
193    }
194    Ok(data)
195}
196
197/// Strictly parse JSON glTF or a GLB 2.0 container.
198pub fn parse_gltf_container(data: &[u8]) -> Result<GltfContainer<'_>> {
199    if data.len() < 4 || read_u32(data, 0)? != GLB_MAGIC {
200        return Ok(GltfContainer {
201            format: GltfContainerFormat::Gltf,
202            json: data,
203            bin: None,
204        });
205    }
206    if data.len() < 12 {
207        return Err(GltfError::InvalidGlb(
208            "file is too small for a GLB header".into(),
209        ));
210    }
211    if read_u32(data, 4)? != GLB_VERSION {
212        return Err(GltfError::InvalidGlb(format!(
213            "unsupported GLB version {}",
214            read_u32(data, 4)?
215        )));
216    }
217    let declared = usize::try_from(read_u32(data, 8)?)
218        .map_err(|_| GltfError::InvalidGlb("GLB length cannot fit usize".into()))?;
219    if declared != data.len() {
220        return Err(GltfError::InvalidGlb(
221            "GLB header length does not match file length".into(),
222        ));
223    }
224
225    let mut offset = 12usize;
226    let mut chunk_index = 0usize;
227    let mut json = None;
228    let mut bin = None;
229    while offset < declared {
230        let header_end = offset
231            .checked_add(8)
232            .filter(|end| *end <= declared)
233            .ok_or_else(|| GltfError::InvalidGlb("partial GLB chunk header".into()))?;
234        let length = usize::try_from(read_u32(data, offset)?)
235            .map_err(|_| GltfError::InvalidGlb("chunk length cannot fit usize".into()))?;
236        let kind = read_u32(data, offset + 4)?;
237        if !length.is_multiple_of(4) {
238            return Err(GltfError::InvalidGlb(
239                "GLB chunk length is not 4-byte aligned".into(),
240            ));
241        }
242        let end = header_end
243            .checked_add(length)
244            .filter(|end| *end <= declared)
245            .ok_or_else(|| GltfError::InvalidGlb("GLB chunk extends past file end".into()))?;
246        let bytes = &data[header_end..end];
247        match kind {
248            GLB_CHUNK_JSON => {
249                if chunk_index != 0 || json.replace(bytes).is_some() {
250                    return Err(GltfError::InvalidGlb(
251                        "JSON must be the first and only JSON chunk".into(),
252                    ));
253                }
254                // JSON permits trailing whitespace, which is indistinguishable
255                // from the space bytes used to pad a GLB JSON chunk. Keep the
256                // full chunk for the JSON parser and reject only an empty chunk
257                // here; malformed trailing bytes are rejected during parsing.
258                if !bytes
259                    .iter()
260                    .any(|byte| !matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
261                {
262                    return Err(GltfError::InvalidGlb("JSON chunk is empty".into()));
263                }
264            }
265            GLB_CHUNK_BIN => {
266                if chunk_index != 1 || bin.replace(bytes).is_some() {
267                    return Err(GltfError::InvalidGlb(
268                        "BIN must be the second and only BIN chunk".into(),
269                    ));
270                }
271            }
272            _ => {
273                if chunk_index == 0 {
274                    return Err(GltfError::InvalidGlb("JSON chunk must be first".into()));
275                }
276            }
277        }
278        offset = end;
279        chunk_index += 1;
280    }
281
282    Ok(GltfContainer {
283        format: GltfContainerFormat::Glb,
284        json: json.ok_or_else(|| GltfError::InvalidGlb("GLB has no JSON chunk".into()))?,
285        bin,
286    })
287}
288
289/// Resolve every declared glTF buffer through the shared resource policy.
290///
291/// Returned buffers have exactly their declared `byteLength`; legal GLB BIN
292/// padding is validated and removed. Resource and aggregate quotas are checked
293/// before buffers are exposed to accessor readers.
294pub fn resolve_gltf_buffers(
295    references: &[GltfBufferReference<'_>],
296    format: GltfContainerFormat,
297    glb_bin: Option<&[u8]>,
298    resolver: Option<&dyn ResourceResolver>,
299    limits: &ResourceLimits,
300) -> Result<Vec<Vec<u8>>> {
301    if format == GltfContainerFormat::Gltf && glb_bin.is_some() {
302        return Err(GltfError::InvalidGltf(
303            "JSON glTF input cannot have a GLB BIN chunk".into(),
304        ));
305    }
306    if references.is_empty() && glb_bin.is_some() {
307        return Err(GltfError::InvalidGlb(
308            "GLB has a BIN chunk but declares no buffer".into(),
309        ));
310    }
311    if glb_bin.is_some()
312        && references
313            .first()
314            .is_some_and(|buffer| buffer.uri.is_some())
315    {
316        return Err(GltfError::InvalidGlb(
317            "GLB BIN chunk requires buffer 0 without a URI".into(),
318        ));
319    }
320
321    let mut buffers = Vec::new();
322    buffers
323        .try_reserve_exact(references.len())
324        .map_err(|_| GltfError::ResourceLimitExceeded("buffer table allocation failed".into()))?;
325    let mut total = 0usize;
326    for (index, reference) in references.iter().enumerate() {
327        let remaining_total = limits
328            .max_total_buffer_bytes
329            .map(|limit| {
330                limit.checked_sub(total).ok_or_else(|| {
331                    GltfError::ResourceLimitExceeded(
332                        "glTF buffers exceed the configured total".into(),
333                    )
334                })
335            })
336            .transpose()?;
337        if remaining_total.is_some_and(|remaining| reference.byte_length > remaining) {
338            return Err(GltfError::ResourceLimitExceeded(format!(
339                "buffer {index} byteLength {} exceeds the remaining total quota",
340                reference.byte_length
341            )));
342        }
343        let effective_limit = match (limits.max_resource_bytes, remaining_total) {
344            (Some(resource), Some(total)) => Some(resource.min(total)),
345            (Some(resource), None) => Some(resource),
346            (None, Some(total)) => Some(total),
347            (None, None) => None,
348        };
349        let effective_limits = ResourceLimits {
350            max_resource_bytes: effective_limit,
351            ..*limits
352        };
353        let data = resolve_gltf_buffer(
354            index,
355            *reference,
356            format,
357            glb_bin,
358            resolver,
359            &effective_limits,
360        )?;
361        total = total
362            .checked_add(data.len())
363            .ok_or_else(|| GltfError::ResourceLimitExceeded("total buffer size overflow".into()))?;
364        check_limit(total, limits.max_total_buffer_bytes, "glTF buffers total")?;
365        buffers.push(data);
366    }
367    Ok(buffers)
368}
369
370fn resolve_gltf_buffer(
371    index: usize,
372    reference: GltfBufferReference<'_>,
373    format: GltfContainerFormat,
374    glb_bin: Option<&[u8]>,
375    resolver: Option<&dyn ResourceResolver>,
376    limits: &ResourceLimits,
377) -> Result<Vec<u8>> {
378    if let Some(uri) = reference.uri {
379        let mut data = resolve_resource_uri(uri, resolver, limits.max_resource_bytes)?;
380        validate_declared_buffer_length(index, reference.byte_length, data.len(), false)?;
381        data.truncate(reference.byte_length);
382        return Ok(data);
383    }
384
385    if format != GltfContainerFormat::Glb {
386        return Err(GltfError::InvalidGltf(format!(
387            "Buffer {index} has no URI in JSON glTF"
388        )));
389    }
390    if index != 0 {
391        return Err(GltfError::InvalidGlb(format!(
392            "Buffer {index} has no URI and is not buffer 0"
393        )));
394    }
395    let bin = glb_bin.ok_or_else(|| {
396        GltfError::InvalidGlb("Buffer 0 has no URI but GLB has no BIN chunk".into())
397    })?;
398    check_limit(bin.len(), limits.max_resource_bytes, "GLB BIN chunk")?;
399    validate_declared_buffer_length(index, reference.byte_length, bin.len(), true)?;
400    if bin[reference.byte_length..]
401        .iter()
402        .any(|&padding| padding != 0)
403    {
404        return Err(GltfError::InvalidGlb(
405            "GLB BIN padding must contain only zero bytes".into(),
406        ));
407    }
408    copy_prefix(bin, reference.byte_length, "GLB BIN chunk")
409}
410
411fn validate_declared_buffer_length(
412    index: usize,
413    declared: usize,
414    actual: usize,
415    glb_bin: bool,
416) -> Result<()> {
417    if actual < declared {
418        return Err(GltfError::InvalidGltf(format!(
419            "Buffer {index} byteLength {declared} exceeds resource length {actual}"
420        )));
421    }
422    if glb_bin {
423        let padded_limit = declared
424            .checked_add(3)
425            .ok_or_else(|| GltfError::InvalidGlb("buffer byteLength overflow".into()))?;
426        if actual > padded_limit {
427            return Err(GltfError::InvalidGlb(format!(
428                "GLB BIN chunk length {actual} is more than 3 bytes larger than buffer[0].byteLength {declared}"
429            )));
430        }
431    }
432    Ok(())
433}
434
435fn copy_prefix(data: &[u8], length: usize, label: &str) -> Result<Vec<u8>> {
436    let prefix = data
437        .get(..length)
438        .ok_or_else(|| GltfError::InvalidGltf(format!("{label} is truncated")))?;
439    let mut output = Vec::new();
440    output
441        .try_reserve_exact(length)
442        .map_err(|_| GltfError::ResourceLimitExceeded(format!("{label} allocation failed")))?;
443    output.extend_from_slice(prefix);
444    Ok(output)
445}
446
447/// Serialize a document and one consolidated binary buffer.
448#[cfg(feature = "gltf-writer")]
449pub fn serialize_gltf_document(
450    document: &Value,
451    bin: &[u8],
452    input_format: GltfContainerFormat,
453    output_format: OutputFormat,
454) -> Result<Vec<u8>> {
455    let format = match output_format {
456        OutputFormat::SameAsInput => input_format,
457        OutputFormat::GltfEmbeddedBuffers => GltfContainerFormat::Gltf,
458        OutputFormat::Glb => GltfContainerFormat::Glb,
459    };
460    let mut document = document.clone();
461    normalize_single_buffer(&mut document, bin, format)?;
462    match format {
463        GltfContainerFormat::Gltf => serialize_json(&document),
464        GltfContainerFormat::Glb => build_glb_container(&document, bin),
465    }
466}
467
468#[cfg(feature = "gltf-writer")]
469fn normalize_single_buffer(
470    document: &mut Value,
471    bin: &[u8],
472    format: GltfContainerFormat,
473) -> Result<()> {
474    let root = document
475        .as_object_mut()
476        .ok_or_else(|| GltfError::InvalidGltf("glTF root is not an object".into()))?;
477    if bin.is_empty() {
478        let has_views = root
479            .get("bufferViews")
480            .and_then(Value::as_array)
481            .is_some_and(|views| !views.is_empty());
482        if has_views {
483            return Err(GltfError::InvalidGltf(
484                "empty consolidated buffer cannot back bufferViews".into(),
485            ));
486        }
487        root.remove("buffers");
488        return Ok(());
489    }
490
491    let buffers = root
492        .entry("buffers")
493        .or_insert_with(|| Value::Array(vec![Value::Object(Default::default())]))
494        .as_array_mut()
495        .ok_or_else(|| GltfError::InvalidGltf("buffers is not an array".into()))?;
496    if buffers.len() != 1 {
497        return Err(GltfError::InvalidGltf(format!(
498            "consolidated serializer requires exactly one buffer, got {}",
499            buffers.len()
500        )));
501    }
502    let buffer = buffers[0]
503        .as_object_mut()
504        .ok_or_else(|| GltfError::InvalidGltf("buffer 0 is not an object".into()))?;
505    buffer.insert("byteLength".into(), Value::from(bin.len() as u64));
506    match format {
507        GltfContainerFormat::Gltf => {
508            buffer.insert(
509                "uri".into(),
510                Value::String(encode_data_uri("application/octet-stream", bin)?),
511            );
512        }
513        GltfContainerFormat::Glb => {
514            buffer.remove("uri");
515        }
516    }
517    Ok(())
518}
519
520/// Decode a `data:` URI with an optional decoded-byte quota.
521pub fn decode_data_uri(uri: &str, max_bytes: Option<usize>) -> Result<Vec<u8>> {
522    let body = uri
523        .strip_prefix("data:")
524        .ok_or_else(|| GltfError::InvalidGltf("URI is not a data URI".into()))?;
525    let comma = body
526        .find(',')
527        .ok_or_else(|| GltfError::InvalidGltf("data URI has no comma".into()))?;
528    let metadata = &body[..comma];
529    let payload = &body[comma + 1..];
530    let is_base64 = metadata
531        .split(';')
532        .skip(1)
533        .any(|part| part.eq_ignore_ascii_case("base64"));
534    let decoded = if is_base64 {
535        decode_base64(payload, max_bytes)?
536    } else {
537        percent_decode_with_limit(payload, max_bytes, "data URI")?
538    };
539    Ok(decoded)
540}
541
542/// Encode bytes as a base64 `data:` URI.
543#[cfg(feature = "gltf-writer")]
544pub fn encode_data_uri(media_type: &str, data: &[u8]) -> Result<String> {
545    let encoded_len = data
546        .len()
547        .checked_add(2)
548        .and_then(|length| length.checked_div(3))
549        .and_then(|length| length.checked_mul(4))
550        .ok_or_else(|| GltfError::ResourceLimitExceeded("base64 size overflow".into()))?;
551    let prefix_len = "data:;base64,"
552        .len()
553        .checked_add(media_type.len())
554        .ok_or_else(|| GltfError::ResourceLimitExceeded("data URI size overflow".into()))?;
555    let capacity = prefix_len
556        .checked_add(encoded_len)
557        .ok_or_else(|| GltfError::ResourceLimitExceeded("data URI size overflow".into()))?;
558    let mut output = String::new();
559    output
560        .try_reserve_exact(capacity)
561        .map_err(|_| GltfError::ResourceLimitExceeded("data URI allocation failed".into()))?;
562    output.push_str("data:");
563    output.push_str(media_type);
564    output.push_str(";base64,");
565    encode_base64_into(data, &mut output);
566    Ok(output)
567}
568
569/// Build a strict GLB 2.0 container without rewriting the JSON document.
570///
571/// Most callers should use [`serialize_gltf_document`], which also normalizes
572/// the consolidated buffer declaration. This lower-level helper is useful for
573/// fixtures whose JSON intentionally references companion resources.
574#[cfg(any(feature = "gltf-writer", test))]
575pub fn build_glb_container(document: &Value, bin: &[u8]) -> Result<Vec<u8>> {
576    // Reject an impossible BIN chunk before attempting to copy it. The padded
577    // chunk length and the complete GLB both have to fit GLB's u32 fields.
578    let bin_padding = (4 - bin.len() % 4) % 4;
579    let padded_bin_len = bin
580        .len()
581        .checked_add(bin_padding)
582        .ok_or_else(|| GltfError::InvalidGlb("BIN chunk size overflow".into()))?;
583    u32::try_from(padded_bin_len)
584        .map_err(|_| GltfError::InvalidGlb("BIN chunk exceeds the 32-bit limit".into()))?;
585
586    let mut json = serialize_json(document)?;
587    pad_to_four(&mut json, b' ')?;
588    let mut bin_copy = Vec::new();
589    bin_copy
590        .try_reserve_exact(bin.len())
591        .map_err(|_| GltfError::ResourceLimitExceeded("BIN chunk allocation failed".into()))?;
592    bin_copy.extend_from_slice(bin);
593    let mut bin = bin_copy;
594    pad_to_four(&mut bin, 0)?;
595
596    let mut total = 12usize
597        .checked_add(8)
598        .and_then(|value| value.checked_add(json.len()))
599        .ok_or_else(|| GltfError::InvalidGlb("GLB size overflow".into()))?;
600    if !bin.is_empty() {
601        total = total
602            .checked_add(8)
603            .and_then(|value| value.checked_add(bin.len()))
604            .ok_or_else(|| GltfError::InvalidGlb("GLB size overflow".into()))?;
605    }
606    let total_u32 = u32::try_from(total)
607        .map_err(|_| GltfError::InvalidGlb("GLB exceeds the 32-bit length limit".into()))?;
608    let json_len = u32::try_from(json.len())
609        .map_err(|_| GltfError::InvalidGlb("JSON chunk exceeds the 32-bit limit".into()))?;
610    let bin_len = u32::try_from(bin.len())
611        .map_err(|_| GltfError::InvalidGlb("BIN chunk exceeds the 32-bit limit".into()))?;
612
613    let mut output = Vec::new();
614    output
615        .try_reserve_exact(total)
616        .map_err(|_| GltfError::ResourceLimitExceeded("GLB allocation failed".into()))?;
617    output.extend_from_slice(&GLB_MAGIC.to_le_bytes());
618    output.extend_from_slice(&GLB_VERSION.to_le_bytes());
619    output.extend_from_slice(&total_u32.to_le_bytes());
620    output.extend_from_slice(&json_len.to_le_bytes());
621    output.extend_from_slice(&GLB_CHUNK_JSON.to_le_bytes());
622    output.extend_from_slice(&json);
623    if !bin.is_empty() {
624        output.extend_from_slice(&bin_len.to_le_bytes());
625        output.extend_from_slice(&GLB_CHUNK_BIN.to_le_bytes());
626        output.extend_from_slice(&bin);
627    }
628    Ok(output)
629}
630
631#[cfg(any(feature = "gltf-writer", test))]
632fn pad_to_four(bytes: &mut Vec<u8>, padding: u8) -> Result<()> {
633    let padding_len = (4 - bytes.len() % 4) % 4;
634    let padded_len = bytes
635        .len()
636        .checked_add(padding_len)
637        .ok_or_else(|| GltfError::ResourceLimitExceeded("padding size overflow".into()))?;
638    bytes
639        .try_reserve(padding_len)
640        .map_err(|_| GltfError::ResourceLimitExceeded("padding allocation failed".into()))?;
641    bytes.resize(padded_len, padding);
642    Ok(())
643}
644
645fn read_u32(data: &[u8], offset: usize) -> Result<u32> {
646    let end = offset
647        .checked_add(4)
648        .filter(|end| *end <= data.len())
649        .ok_or_else(|| GltfError::InvalidGlb("truncated u32".into()))?;
650    let mut bytes = [0u8; 4];
651    bytes.copy_from_slice(&data[offset..end]);
652    Ok(u32::from_le_bytes(bytes))
653}
654
655#[cfg(feature = "gltf-writer")]
656struct FallibleJsonBuffer {
657    bytes: Vec<u8>,
658    allocation_failed: bool,
659}
660
661#[cfg(feature = "gltf-writer")]
662impl Write for FallibleJsonBuffer {
663    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
664        if self.bytes.try_reserve(bytes.len()).is_err() {
665            self.allocation_failed = true;
666            return Err(io::Error::other("JSON allocation failed"));
667        }
668        self.bytes.extend_from_slice(bytes);
669        Ok(bytes.len())
670    }
671
672    fn flush(&mut self) -> io::Result<()> {
673        Ok(())
674    }
675}
676
677#[cfg(feature = "gltf-writer")]
678fn serialize_json(document: &Value) -> Result<Vec<u8>> {
679    let mut output = FallibleJsonBuffer {
680        bytes: Vec::new(),
681        allocation_failed: false,
682    };
683    if let Err(error) = serde_json::to_writer(&mut output, document) {
684        if output.allocation_failed {
685            return Err(GltfError::ResourceLimitExceeded(
686                "JSON allocation failed".into(),
687            ));
688        }
689        return Err(GltfError::Json(error));
690    }
691    Ok(output.bytes)
692}
693
694#[cfg(all(test, not(feature = "gltf-writer")))]
695fn serialize_json(document: &Value) -> Result<Vec<u8>> {
696    Ok(serde_json::to_vec(document)?)
697}
698
699fn check_limit(length: usize, limit: Option<usize>, resource: &str) -> Result<()> {
700    if limit.is_some_and(|limit| length > limit) {
701        return Err(GltfError::ResourceLimitExceeded(format!(
702            "{resource} is {length} bytes"
703        )));
704    }
705    Ok(())
706}
707
708fn decode_base64(input: &str, limit: Option<usize>) -> Result<Vec<u8>> {
709    if input.bytes().any(|byte| byte.is_ascii_whitespace()) {
710        return Err(GltfError::InvalidGltf(
711            "base64 data must not contain whitespace".into(),
712        ));
713    }
714    if !input.len().is_multiple_of(4) {
715        return Err(GltfError::InvalidGltf(
716            "base64 length must be divisible by four".into(),
717        ));
718    }
719    let padding = input
720        .as_bytes()
721        .iter()
722        .rev()
723        .take_while(|b| **b == b'=')
724        .count();
725    if padding > 2 || input.as_bytes()[..input.len().saturating_sub(padding)].contains(&b'=') {
726        return Err(GltfError::InvalidGltf("invalid base64 padding".into()));
727    }
728    let decoded_len = input
729        .len()
730        .checked_div(4)
731        .and_then(|length| length.checked_mul(3))
732        .and_then(|length| length.checked_sub(padding))
733        .ok_or_else(|| GltfError::ResourceLimitExceeded("base64 size overflow".into()))?;
734    check_limit(decoded_len, limit, "data URI")?;
735    let mut output = Vec::new();
736    output
737        .try_reserve_exact(decoded_len)
738        .map_err(|_| GltfError::ResourceLimitExceeded("base64 allocation failed".into()))?;
739    for chunk in input.as_bytes().chunks_exact(4) {
740        let a = base64_value(chunk[0])? as u32;
741        let b = base64_value(chunk[1])? as u32;
742        let c = if chunk[2] == b'=' {
743            0
744        } else {
745            base64_value(chunk[2])? as u32
746        };
747        let d = if chunk[3] == b'=' {
748            0
749        } else {
750            base64_value(chunk[3])? as u32
751        };
752        if (chunk[2] == b'=' && b & 0x0f != 0) || (chunk[3] == b'=' && c & 0x03 != 0) {
753            return Err(GltfError::InvalidGltf(
754                "base64 has non-zero padding bits".into(),
755            ));
756        }
757        let value = (a << 18) | (b << 12) | (c << 6) | d;
758        output.push((value >> 16) as u8);
759        if chunk[2] != b'=' {
760            output.push((value >> 8) as u8);
761        }
762        if chunk[3] != b'=' {
763            output.push(value as u8);
764        }
765    }
766    debug_assert_eq!(output.len(), decoded_len);
767    Ok(output)
768}
769
770fn base64_value(byte: u8) -> Result<u8> {
771    match byte {
772        b'A'..=b'Z' => Ok(byte - b'A'),
773        b'a'..=b'z' => Ok(byte - b'a' + 26),
774        b'0'..=b'9' => Ok(byte - b'0' + 52),
775        b'+' => Ok(62),
776        b'/' => Ok(63),
777        _ => Err(GltfError::InvalidGltf("invalid base64 character".into())),
778    }
779}
780
781#[cfg(feature = "gltf-writer")]
782fn encode_base64_into(data: &[u8], output: &mut String) {
783    const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
784    for chunk in data.chunks(3) {
785        let a = chunk[0] as u32;
786        let b = chunk.get(1).copied().unwrap_or(0) as u32;
787        let c = chunk.get(2).copied().unwrap_or(0) as u32;
788        let value = (a << 16) | (b << 8) | c;
789        output.push(TABLE[((value >> 18) & 63) as usize] as char);
790        output.push(TABLE[((value >> 12) & 63) as usize] as char);
791        output.push(if chunk.len() > 1 {
792            TABLE[((value >> 6) & 63) as usize] as char
793        } else {
794            '='
795        });
796        output.push(if chunk.len() > 2 {
797            TABLE[(value & 63) as usize] as char
798        } else {
799            '='
800        });
801    }
802}
803
804fn percent_decode(input: &str) -> Result<Vec<u8>> {
805    percent_decode_with_limit(input, None, "percent-encoded URI")
806}
807
808fn percent_decode_with_limit(input: &str, limit: Option<usize>, label: &str) -> Result<Vec<u8>> {
809    let bytes = input.as_bytes();
810    let mut decoded_len = 0usize;
811    let mut index = 0usize;
812    while index < bytes.len() {
813        if bytes[index] == b'%' {
814            let end = index
815                .checked_add(3)
816                .filter(|end| *end <= bytes.len())
817                .ok_or_else(|| GltfError::InvalidGltf("truncated percent escape".into()))?;
818            let _ = hex(bytes[index + 1])?;
819            let _ = hex(bytes[index + 2])?;
820            index = end;
821        } else {
822            index += 1;
823        }
824        decoded_len = decoded_len.checked_add(1).ok_or_else(|| {
825            GltfError::ResourceLimitExceeded("percent-decoded size overflow".into())
826        })?;
827    }
828    check_limit(decoded_len, limit, label)?;
829
830    let mut output = Vec::new();
831    output
832        .try_reserve_exact(decoded_len)
833        .map_err(|_| GltfError::ResourceLimitExceeded("percent decode allocation failed".into()))?;
834    let mut index = 0usize;
835    while index < bytes.len() {
836        if bytes[index] == b'%' {
837            let end = index
838                .checked_add(3)
839                .filter(|end| *end <= bytes.len())
840                .ok_or_else(|| GltfError::InvalidGltf("truncated percent escape".into()))?;
841            let high = hex(bytes[index + 1])?;
842            let low = hex(bytes[index + 2])?;
843            output.push((high << 4) | low);
844            index = end;
845        } else {
846            output.push(bytes[index]);
847            index += 1;
848        }
849    }
850    Ok(output)
851}
852
853fn hex(byte: u8) -> Result<u8> {
854    match byte {
855        b'0'..=b'9' => Ok(byte - b'0'),
856        b'a'..=b'f' => Ok(byte - b'a' + 10),
857        b'A'..=b'F' => Ok(byte - b'A' + 10),
858        _ => Err(GltfError::InvalidGltf("invalid percent escape".into())),
859    }
860}
861
862#[cfg(test)]
863mod tests {
864    use std::cell::Cell;
865
866    use super::*;
867
868    fn raw_glb(chunks: &[(u32, &[u8])]) -> Vec<u8> {
869        let total = 12
870            + chunks
871                .iter()
872                .map(|(_, bytes)| 8 + bytes.len())
873                .sum::<usize>();
874        let mut output = Vec::with_capacity(total);
875        output.extend_from_slice(&GLB_MAGIC.to_le_bytes());
876        output.extend_from_slice(&GLB_VERSION.to_le_bytes());
877        output.extend_from_slice(&(total as u32).to_le_bytes());
878        for (kind, bytes) in chunks {
879            output.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
880            output.extend_from_slice(&kind.to_le_bytes());
881            output.extend_from_slice(bytes);
882        }
883        output
884    }
885
886    #[test]
887    fn strict_data_uri_rejects_malformed_input() {
888        assert_eq!(decode_data_uri("data:,a%20b", None).unwrap(), b"a b");
889        assert_eq!(decode_data_uri("data:;base64,YQ==", None).unwrap(), b"a");
890        assert!(decode_data_uri("data:;base64,YQ", None).is_err());
891        assert!(decode_data_uri("data:;base64,YR==", None).is_err());
892        assert!(decode_data_uri("data:;base64,YWF=", None).is_err());
893        assert!(decode_data_uri("data:,a%2", None).is_err());
894        assert!(decode_data_uri("data:;base64,YQ==", Some(0)).is_err());
895        assert!(decode_data_uri("data:,abcd", Some(2)).is_err());
896    }
897
898    #[test]
899    fn buffer_total_quota_is_forwarded_before_resolution() {
900        struct LimitAwareResolver(Cell<Option<usize>>);
901
902        impl ResourceResolver for LimitAwareResolver {
903            fn resolve(&self, _: &str) -> Result<Vec<u8>> {
904                panic!("resolve_with_limit must be used")
905            }
906
907            fn resolve_with_limit(&self, _: &str, max_bytes: Option<usize>) -> Result<Vec<u8>> {
908                self.0.set(max_bytes);
909                Ok(vec![1, 2])
910            }
911        }
912
913        let resolver = LimitAwareResolver(Cell::new(None));
914        let buffers = resolve_gltf_buffers(
915            &[GltfBufferReference {
916                uri: Some("mesh.bin"),
917                byte_length: 2,
918            }],
919            GltfContainerFormat::Gltf,
920            None,
921            Some(&resolver),
922            &ResourceLimits {
923                max_total_buffer_bytes: Some(3),
924                ..ResourceLimits::default()
925            },
926        )
927        .unwrap();
928        assert_eq!(buffers, [vec![1, 2]]);
929        assert_eq!(resolver.0.get(), Some(3));
930
931        assert!(resolve_gltf_buffers(
932            &[GltfBufferReference {
933                uri: Some("data:,abcd"),
934                byte_length: 4,
935            }],
936            GltfContainerFormat::Gltf,
937            None,
938            None,
939            &ResourceLimits {
940                max_total_buffer_bytes: Some(2),
941                ..ResourceLimits::default()
942            },
943        )
944        .is_err());
945    }
946
947    #[test]
948    fn glb_builder_and_parser_are_strict() {
949        let document = serde_json::json!({
950            "asset": {"version": "2.0"},
951            "buffers": [{"byteLength": 3}]
952        });
953        let bytes = build_glb_container(&document, &[1, 2, 3]).unwrap();
954        let parsed = parse_gltf_container(&bytes).unwrap();
955        assert_eq!(parsed.format, GltfContainerFormat::Glb);
956        assert_eq!(&parsed.bin.unwrap()[..3], &[1, 2, 3]);
957        let parsed_json: Value = serde_json::from_slice(parsed.json).unwrap();
958        assert_eq!(parsed_json["buffers"][0]["byteLength"], 3);
959        assert!(parsed_json["buffers"][0].get("uri").is_none());
960
961        let mut trailing = bytes;
962        trailing.push(0);
963        assert!(parse_gltf_container(&trailing).is_err());
964
965        let json = b"{\"asset\":{\"version\":\"2.0\"}} ";
966        let bin = [0u8; 4];
967        let bin_first = raw_glb(&[(GLB_CHUNK_BIN, &bin), (GLB_CHUNK_JSON, json)]);
968        assert!(parse_gltf_container(&bin_first).is_err());
969        let duplicate_bin = raw_glb(&[
970            (GLB_CHUNK_JSON, json),
971            (GLB_CHUNK_BIN, &bin),
972            (GLB_CHUNK_BIN, &bin),
973        ]);
974        assert!(parse_gltf_container(&duplicate_bin).is_err());
975        let unaligned = raw_glb(&[(GLB_CHUNK_JSON, b"{}")]);
976        assert!(parse_gltf_container(&unaligned).is_err());
977        let trailing_json_whitespace = raw_glb(&[(GLB_CHUNK_JSON, b"{}\t\t")]);
978        let parsed = parse_gltf_container(&trailing_json_whitespace).unwrap();
979        let parsed_json: Value = serde_json::from_slice(parsed.json).unwrap();
980        assert_eq!(parsed_json, serde_json::json!({}));
981
982        let invalid_bin_padding = [1u8, 0xff, 0, 0];
983        assert!(resolve_gltf_buffers(
984            &[GltfBufferReference {
985                uri: None,
986                byte_length: 1,
987            }],
988            GltfContainerFormat::Glb,
989            Some(&invalid_bin_padding),
990            None,
991            &ResourceLimits::default(),
992        )
993        .is_err());
994
995        let mut wrong_length = build_glb_container(&document, &[]).unwrap();
996        let declared = (wrong_length.len() as u32 - 1).to_le_bytes();
997        wrong_length[8..12].copy_from_slice(&declared);
998        assert!(parse_gltf_container(&wrong_length).is_err());
999    }
1000
1001    #[test]
1002    #[cfg(feature = "gltf-writer")]
1003    fn embedded_serializer_sets_uri_and_exact_length() {
1004        let document = serde_json::json!({
1005            "asset": {"version": "2.0"},
1006            "buffers": [{"byteLength": 1, "uri": "old.bin"}]
1007        });
1008        let bytes = serialize_gltf_document(
1009            &document,
1010            &[1, 2, 3],
1011            GltfContainerFormat::Glb,
1012            OutputFormat::GltfEmbeddedBuffers,
1013        )
1014        .unwrap();
1015        let output: Value = serde_json::from_slice(&bytes).unwrap();
1016        assert_eq!(output["buffers"][0]["byteLength"], 3);
1017        assert_eq!(
1018            output["buffers"][0]["uri"],
1019            "data:application/octet-stream;base64,AQID"
1020        );
1021    }
1022}