1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum GltfContainerFormat {
22 Gltf,
24 Glb,
26}
27
28#[derive(Clone, Copy, Debug)]
30pub struct GltfContainer<'a> {
31 pub format: GltfContainerFormat,
33 pub json: &'a [u8],
35 pub bin: Option<&'a [u8]>,
37}
38
39#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
41pub enum OutputFormat {
42 #[default]
44 SameAsInput,
45 GltfEmbeddedBuffers,
47 Glb,
49}
50
51#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
53pub struct ResourceLimits {
54 pub max_resource_bytes: Option<usize>,
56 pub max_total_buffer_bytes: Option<usize>,
58 pub max_image_pixels: Option<u64>,
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub struct GltfBufferReference<'a> {
65 pub uri: Option<&'a str>,
67 pub byte_length: usize,
69}
70
71pub trait ResourceResolver {
73 fn resolve(&self, uri: &str) -> Result<Vec<u8>>;
75
76 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
87pub 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
111pub enum ExternalFilePolicy {
112 #[default]
114 Deny,
115 Allow,
117 ConfineToBase,
119}
120
121#[derive(Clone, Debug)]
123pub struct FileResourceResolver {
124 base: PathBuf,
125 policy: ExternalFilePolicy,
126}
127
128impl FileResourceResolver {
129 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
197pub 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 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
289pub 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#[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
520pub 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#[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#[cfg(any(feature = "gltf-writer", test))]
575pub fn build_glb_container(document: &Value, bin: &[u8]) -> Result<Vec<u8>> {
576 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}