1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum GltfContainerFormat {
18 Gltf,
20 GlbV2,
22 GlbV3,
24}
25
26pub 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 pub const fn is_glb(self) -> bool {
141 matches!(self, Self::GlbV2 | Self::GlbV3)
142 }
143}
144
145#[derive(Clone, Copy, Debug)]
147pub struct GltfContainer<'a> {
148 pub format: GltfContainerFormat,
150 pub json: &'a [u8],
152 pub bin: Option<&'a [u8]>,
154}
155
156#[derive(Clone, Copy, Debug, PartialEq, Eq)]
158pub struct GlbChunkDescriptor {
159 pub offset: u64,
161 pub length: u64,
163 pub kind: u32,
165 pub encoding: u32,
167}
168
169#[derive(Clone, Debug, PartialEq, Eq)]
171pub struct GlbLayout {
172 pub format: GltfContainerFormat,
174 pub length: u64,
176 pub chunks: Vec<GlbChunkDescriptor>,
178}
179
180pub struct GlbRangeReader<R> {
187 input: R,
188 layout: GlbLayout,
189}
190
191impl<R: Read + Seek> GlbRangeReader<R> {
192 pub fn open(mut input: R) -> Result<Self> {
194 let layout = inspect_glb(&mut input)?;
195 Ok(Self { input, layout })
196 }
197
198 pub fn layout(&self) -> &GlbLayout {
200 &self.layout
201 }
202
203 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 pub fn into_inner(self) -> R {
230 self.input
231 }
232}
233
234pub 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
314pub struct ResourceLimits {
315 pub max_resource_bytes: Option<usize>,
317 pub max_total_buffer_bytes: Option<usize>,
319 pub max_image_pixels: Option<u64>,
321 pub max_external_asset_depth: Option<usize>,
326}
327
328#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
330pub struct GltfBufferReference<'a> {
331 pub uri: Option<&'a str>,
333 pub byte_length: usize,
335 pub meshopt_fallback: bool,
338}
339
340pub trait ResourceResolver {
342 fn resolve(&self, uri: &str) -> Result<Vec<u8>>;
344
345 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
356pub 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
380pub enum ExternalFilePolicy {
381 #[default]
383 Deny,
384 Allow,
386 ConfineToBase,
388}
389
390#[derive(Clone, Debug)]
392pub struct FileResourceResolver {
393 base: PathBuf,
394 policy: ExternalFilePolicy,
395}
396
397impl FileResourceResolver {
398 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
466pub 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 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
561fn 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
648pub 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
732pub 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 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
903pub 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}