1#![warn(missing_docs)]
74
75mod capability;
76pub mod fix;
77mod scale;
78pub mod write;
79
80pub use capability::{
81 GltfAccessorCapability, GltfAnimationChannelCapability, GltfAttributeCapability,
82 GltfBufferCapability, GltfBufferSourceKind, GltfBufferViewCapability, GltfCapabilityManifest,
83 GltfCapabilityViolation, GltfCapabilityViolationKind, GltfContainerKind,
84 GltfInstancingCapability, GltfNodeCapability, GltfNodeRestKind, GltfPrimitiveCapability,
85 GltfScalePreflightError, GltfScaleSource, GltfSkinCapability, preflight_scale_source,
86 preflight_scale_source_bytes,
87};
88pub use scale::{
89 GltfRawJsonDifference, GltfRawJsonDifferenceKind, GltfRawJsonDifferenceSummary,
90 GltfScaleArtifact, GltfScaleArtifactProof, GltfScaleRewriteError, capability_facts,
91 capability_facts_for_source, operation_capability_facts, operation_capability_facts_for_source,
92 prove_rewritten_artifact, prove_rewritten_rest_bind, rewrite_linear_units, rewrite_rest_bind,
93 rewrite_scale_plan,
94};
95
96use animsmith_core::model::{
97 AdditionalInfluenceSet, Bone, Clip, DecodedImageColorType, Document, ImageContainerFormat,
98 ImageSourceKind, ImageUnavailableReason, Interpolation, MaterialAsset, MaterialResourceAssets,
99 MaterialResourceCoverage, MaterialTextureSlot, MeshAsset, MeshInstance, NormalTextureAsset,
100 OcclusionTextureAsset, Primitive, Property, SceneAsset, SceneAssets, Skeleton,
101 SourceImageAsset, SourceImageInspection, SourceInfo, SourceInverseBindAccessor,
102 SourceInverseBindAccessorStatus, SourceMaterialAsset, SourceMaterialTextureBinding,
103 SourceNodeAsset, SourceNodeLocalRest, SourceSkeletonAssets, SourceSkeletonCoverage,
104 SourceSkinAsset, SourceSkinAttachment, SourceTextureAsset, TextureAsset, Track, TrackValues,
105 Transform,
106};
107use animsmith_core::{
108 DependencyClosureBuilderV1, DependencyClosureError, DependencyClosureV1,
109 DependencyResourceKeyV1, DependencyResourceRefusalReasonV1,
110 DependencyResourceUnavailableReasonV1, InputIdentity, LoadedSource,
111 RAW_SOURCE_V1_MAX_TEXT_BYTES, RawSourceFactsBuilderV1, ResourceKeySyntaxV1, SourceAxisV1,
112 SourceChannelFactV1, SourceChannelPropertyV1, SourceClipFactV1, SourceComponentMaskV1,
113 SourceConstructFactV1, SourceConstructKindV1, SourceCoordinateBasisV1, SourceFactDomainV1,
114 SourceFactSetV1, SourceFactsError, SourceFormatV1, SourceInterpolationV1, SourceLinearUnitV1,
115 SourceLoaderDispositionV1, SourceLogicalLocatorV1, SourceObservationV1, SourceProvenanceKindV1,
116 SourceProvenanceV1, SourceResourceKindV1, SourceResourceLocatorV1, SourceResourceReferenceV1,
117 SourceTargetKindV1, SourceTargetV1, SourceTextV1, SourceTimeRangeV1, SourceUnavailableReasonV1,
118};
119use base64::Engine as _;
120use glam::{Mat4, Quat, Vec3};
121use gltf::accessor::{DataType as ComponentType, Dimensions as AccessorType};
122use image::{ColorType, ImageError, ImageFormat, ImageReader, Limits};
123use std::collections::{BTreeMap, BTreeSet};
124use std::io::{Cursor, Read};
125use std::path::{Path, PathBuf};
126
127#[derive(Debug, thiserror::Error)]
133#[non_exhaustive]
134pub enum LoadError {
135 #[error("failed to read {path}: {source}")]
137 Io {
138 path: String,
140 source: std::io::Error,
142 },
143 #[error("external resource load failed: {0}")]
145 ExternalResource(ExternalResourceFailure),
146 #[error("glTF parse error: {0}")]
148 Gltf(#[from] gltf::Error),
149 #[error("invalid raw-source facts: {0}")]
151 SourceFacts(#[from] SourceFactsError),
152 #[error("buffer resolution failed: {0}")]
154 Buffer(String),
155 #[error("malformed animation data: {0}")]
157 Malformed(String),
158 #[error("malformed node graph: {0}")]
160 Topology(String),
161 #[error(
169 "mesh {mesh} primitive {primitive} {attribute}: accessor {accessor} is {found}, but the loader reads {expected}"
170 )]
171 PrimitiveEncoding {
172 mesh: usize,
174 primitive: usize,
176 attribute: String,
178 accessor: usize,
180 found: String,
182 expected: String,
186 },
187 #[error("mesh {mesh} primitive {primitive} {attribute}: accessor {accessor} {problem}")]
198 PrimitiveAccessorLayout {
199 mesh: usize,
201 primitive: usize,
203 attribute: String,
205 accessor: usize,
207 problem: String,
211 },
212 #[error("clip '{clip}' node {node} sampler {slot}: accessor {accessor} {problem}")]
221 AnimationAccessorLayout {
222 clip: String,
225 node: usize,
227 slot: &'static str,
229 accessor: usize,
231 problem: String,
235 },
236 #[error(
242 "animation {animation} sampler {sampler} {slot} for node {node} {property}: accessor {accessor} is {found}, but the loader reads {expected}"
243 )]
244 AnimationEncoding {
245 animation: usize,
247 sampler: usize,
249 slot: &'static str,
251 node: usize,
253 property: &'static str,
255 accessor: usize,
257 found: String,
259 expected: String,
261 },
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
266pub enum ExternalResourceFailure {
267 #[error("external resource requires an explicit trusted root")]
269 ResourceRootRequired,
270 #[error("unsafe external buffer resource")]
272 Refused,
273 #[error("external buffer resource exceeds capture limits")]
275 CaptureLimitExceeded,
276 #[error("external buffer resource is unavailable")]
278 Unavailable,
279}
280
281#[derive(Debug, thiserror::Error)]
287#[non_exhaustive]
288pub enum FixError {
289 #[error(transparent)]
291 Load(#[from] LoadError),
292 #[error(transparent)]
294 Write(#[from] WriteError),
295}
296
297#[derive(Debug, thiserror::Error)]
299#[non_exhaustive]
300pub enum WriteError {
301 #[error("failed to write {path}: {source}")]
303 Io {
304 path: String,
306 source: std::io::Error,
308 },
309 #[error("failed to serialize glTF JSON: {0}")]
311 Serialize(#[from] serde_json::Error),
312 #[error(
314 "GLB too large: {field} is {bytes} bytes, exceeding the 4 GiB limit of a GLB u32 length field"
315 )]
316 TooLarge {
317 field: &'static str,
319 bytes: usize,
321 },
322}
323
324pub(crate) fn safe_external_buffer_path(uri: &str) -> Result<PathBuf, LoadError> {
330 DependencyResourceKeyV1::from_source_str(uri, ResourceKeySyntaxV1::GltfUri)
331 .map(|key| PathBuf::from(key.as_str()))
332 .map_err(|_| unsafe_external_uri())
333}
334
335fn unsafe_external_uri() -> LoadError {
336 LoadError::Buffer("unsafe external buffer URI: expected a relative child path".to_owned())
337}
338
339pub(crate) fn validate_glb_framing(bytes: &[u8]) -> Result<(), LoadError> {
348 const GLB_MAGIC: &[u8; 4] = b"glTF";
349 const GLB_HEADER_LEN: usize = 12;
350 if !bytes.starts_with(GLB_MAGIC) {
351 return Ok(());
352 }
353 if bytes.len() < GLB_HEADER_LEN {
354 return Err(LoadError::Buffer(
355 "truncated GLB: file ends before the 12-byte header".into(),
356 ));
357 }
358 let declared =
359 u32::from_le_bytes(bytes[8..12].try_into().expect("slice has four bytes")) as usize;
360 if declared < GLB_HEADER_LEN || declared > bytes.len() {
361 return Err(LoadError::Buffer(format!(
362 "GLB header declares {declared} bytes but the file is {}",
363 bytes.len()
364 )));
365 }
366 Ok(())
367}
368
369fn has_extension_object(primary_bytes: &[u8]) -> bool {
377 let Some(json) = source_json_payload(primary_bytes) else {
378 return true;
379 };
380 json_has_object_key(json, b"extensions")
381}
382
383fn source_json_payload(primary_bytes: &[u8]) -> Option<&[u8]> {
384 if !primary_bytes.starts_with(b"glTF") {
385 return Some(primary_bytes);
386 }
387 const GLB_JSON_OFFSET: usize = 20;
388 let length = u32::from_le_bytes(primary_bytes.get(12..16)?.try_into().ok()?) as usize;
389 primary_bytes.get(GLB_JSON_OFFSET..GLB_JSON_OFFSET.checked_add(length)?)
390}
391
392fn json_has_object_key(json: &[u8], target: &[u8]) -> bool {
393 let mut cursor = 0;
394 while cursor < json.len() {
395 if json[cursor] != b'"' {
396 cursor += 1;
397 continue;
398 }
399 cursor += 1;
400 let mut target_index = 0;
401 let mut candidate = true;
402 loop {
403 let Some(&byte) = json.get(cursor) else {
404 return true;
405 };
406 if byte == b'"' {
407 cursor += 1;
408 break;
409 }
410 let decoded = if byte == b'\\' {
411 cursor += 1;
412 let Some(&escape) = json.get(cursor) else {
413 return true;
414 };
415 match escape {
416 b'u' => {
417 let Some(hex) = json.get(cursor + 1..cursor + 5) else {
418 return true;
419 };
420 cursor += 5;
421 decode_json_hex_quad(hex).and_then(|value| u8::try_from(value).ok())
422 }
423 b'"' | b'\\' | b'/' => {
424 cursor += 1;
425 Some(escape)
426 }
427 b'b' | b'f' | b'n' | b'r' | b't' => {
428 cursor += 1;
429 None
430 }
431 _ => return true,
432 }
433 } else {
434 cursor += 1;
435 Some(byte)
436 };
437 if candidate {
438 match decoded {
439 Some(decoded) if target.get(target_index) == Some(&decoded) => {
440 target_index += 1;
441 }
442 _ => candidate = false,
443 }
444 }
445 }
446 let mut delimiter = cursor;
447 while matches!(json.get(delimiter), Some(b' ' | b'\t' | b'\r' | b'\n')) {
448 delimiter += 1;
449 }
450 if candidate && target_index == target.len() && json.get(delimiter).copied() == Some(b':') {
451 return true;
452 }
453 }
454 false
455}
456
457fn decode_json_hex_quad(hex: &[u8]) -> Option<u16> {
458 hex.iter().try_fold(0_u16, |value, byte| {
459 let digit = match byte {
460 b'0'..=b'9' => u16::from(*byte - b'0'),
461 b'a'..=b'f' => u16::from(*byte - b'a' + 10),
462 b'A'..=b'F' => u16::from(*byte - b'A' + 10),
463 _ => return None,
464 };
465 Some((value << 4) | digit)
466 })
467}
468
469pub(crate) fn validate_animation_channels(root: &gltf::json::Root) -> Result<(), LoadError> {
484 use gltf::json::validation::Checked;
485 let node_count = root.nodes.len();
486 for (ai, anim) in root.animations.iter().enumerate() {
487 for (ci, channel) in anim.channels.iter().enumerate() {
488 if matches!(channel.target.path, Checked::Invalid) {
489 return Err(LoadError::Malformed(format!(
490 "animation {ai} channel {ci}: unknown target path"
491 )));
492 }
493 if channel.target.node.value() >= node_count {
494 return Err(LoadError::Malformed(format!(
495 "animation {ai} channel {ci}: target node index {} out of range ({node_count} nodes)",
496 channel.target.node.value()
497 )));
498 }
499 }
500 }
501 Ok(())
502}
503
504pub(crate) fn validate_animations(doc: &gltf::Document) -> Result<(), LoadError> {
508 validate_animation_channels(doc.as_json())?;
509 validate_animation_accessor_encodings(doc)
510}
511
512pub(crate) fn validate_document(document: &gltf::Document) -> Result<(), gltf::Error> {
517 use gltf::json::validation::{Error, Validate};
518
519 let root = document.as_json();
520 let mut errors = Vec::new();
521 root.validate(root, gltf::json::Path::new, &mut |path, error| {
522 errors.push((path(), error));
523 });
524 if errors.iter().all(|(_, error)| *error == Error::Unsupported) {
525 Ok(())
526 } else {
527 Err(gltf::Error::Validation(errors))
528 }
529}
530
531struct ReaderEncoding {
534 accessor_type: AccessorType,
536 component_types: &'static [ComponentType],
538 normalized_integers: bool,
544}
545
546const POSITION_ENCODING: ReaderEncoding = ReaderEncoding {
551 accessor_type: AccessorType::Vec3,
552 component_types: &[ComponentType::F32],
553 normalized_integers: false,
554};
555const NORMAL_ENCODING: ReaderEncoding = ReaderEncoding {
558 accessor_type: AccessorType::Vec3,
559 component_types: &[ComponentType::F32],
560 normalized_integers: false,
561};
562const TEX_COORD_ENCODING: ReaderEncoding = ReaderEncoding {
569 accessor_type: AccessorType::Vec2,
570 component_types: &[ComponentType::U8, ComponentType::U16, ComponentType::F32],
571 normalized_integers: true,
572};
573const JOINTS_ENCODING: ReaderEncoding = ReaderEncoding {
576 accessor_type: AccessorType::Vec4,
577 component_types: &[ComponentType::U8, ComponentType::U16],
578 normalized_integers: false,
579};
580const WEIGHTS_ENCODING: ReaderEncoding = ReaderEncoding {
584 accessor_type: AccessorType::Vec4,
585 component_types: &[ComponentType::U8, ComponentType::U16, ComponentType::F32],
586 normalized_integers: true,
587};
588const INDEX_ENCODING: ReaderEncoding = ReaderEncoding {
590 accessor_type: AccessorType::Scalar,
591 component_types: &[ComponentType::U8, ComponentType::U16, ComponentType::U32],
592 normalized_integers: false,
593};
594const INVERSE_BIND_ENCODING: ReaderEncoding = ReaderEncoding {
597 accessor_type: AccessorType::Mat4,
598 component_types: &[ComponentType::F32],
599 normalized_integers: false,
600};
601const ANIMATION_INPUT_ENCODING: ReaderEncoding = ReaderEncoding {
603 accessor_type: AccessorType::Scalar,
604 component_types: &[ComponentType::F32],
605 normalized_integers: false,
606};
607const ANIMATION_VEC3_OUTPUT_ENCODING: ReaderEncoding = ReaderEncoding {
609 accessor_type: AccessorType::Vec3,
610 component_types: &[ComponentType::F32],
611 normalized_integers: false,
612};
613const ANIMATION_ROTATION_OUTPUT_ENCODING: ReaderEncoding = ReaderEncoding {
616 accessor_type: AccessorType::Vec4,
617 component_types: &[
618 ComponentType::I8,
619 ComponentType::U8,
620 ComponentType::I16,
621 ComponentType::U16,
622 ComponentType::F32,
623 ],
624 normalized_integers: false,
625};
626const ANIMATION_WEIGHT_OUTPUT_ENCODING: ReaderEncoding = ReaderEncoding {
632 accessor_type: AccessorType::Scalar,
633 component_types: &[
634 ComponentType::I8,
635 ComponentType::U8,
636 ComponentType::I16,
637 ComponentType::U16,
638 ComponentType::F32,
639 ],
640 normalized_integers: false,
641};
642
643fn validate_animation_accessor_encodings(doc: &gltf::Document) -> Result<(), LoadError> {
646 for animation in doc.animations() {
647 for channel in animation.channels() {
648 let sampler = channel.sampler();
649 let target = channel.target();
650 let node = target.node().index();
651 let property = target.property();
652 check_animation_accessor_encoding(
653 animation.index(),
654 sampler.index(),
655 node,
656 animation_property_name(property),
657 "input",
658 &sampler.input(),
659 &ANIMATION_INPUT_ENCODING,
660 )?;
661 let output_encoding = match property {
662 gltf::animation::Property::Translation | gltf::animation::Property::Scale => {
663 &ANIMATION_VEC3_OUTPUT_ENCODING
664 }
665 gltf::animation::Property::Rotation => &ANIMATION_ROTATION_OUTPUT_ENCODING,
666 gltf::animation::Property::MorphTargetWeights => &ANIMATION_WEIGHT_OUTPUT_ENCODING,
667 };
668 check_animation_accessor_encoding(
669 animation.index(),
670 sampler.index(),
671 node,
672 animation_property_name(property),
673 "output",
674 &sampler.output(),
675 output_encoding,
676 )?;
677 }
678 }
679 Ok(())
680}
681
682fn check_animation_accessor_encoding(
683 animation: usize,
684 sampler: usize,
685 node: usize,
686 property: &'static str,
687 slot: &'static str,
688 accessor: &gltf::Accessor<'_>,
689 required: &ReaderEncoding,
690) -> Result<(), LoadError> {
691 if encoding_matches(accessor, required) {
692 return Ok(());
693 }
694 Err(LoadError::AnimationEncoding {
695 animation,
696 sampler,
697 slot,
698 node,
699 property,
700 accessor: accessor.index(),
701 found: format!(
702 "{} of {}",
703 accessor_type_name(accessor.dimensions()),
704 component_type_name(accessor.data_type())
705 ),
706 expected: describe_encoding(required),
707 })
708}
709
710fn animation_property_name(property: gltf::animation::Property) -> &'static str {
711 match property {
712 gltf::animation::Property::Translation => "translation",
713 gltf::animation::Property::Rotation => "rotation",
714 gltf::animation::Property::Scale => "scale",
715 gltf::animation::Property::MorphTargetWeights => "weights",
716 }
717}
718
719fn required_attribute_encoding(semantic: &gltf::Semantic) -> Option<&'static ReaderEncoding> {
737 match semantic {
738 gltf::Semantic::Positions => Some(&POSITION_ENCODING),
739 gltf::Semantic::Normals => Some(&NORMAL_ENCODING),
740 gltf::Semantic::TexCoords(0) => Some(&TEX_COORD_ENCODING),
743 gltf::Semantic::Joints(0) => Some(&JOINTS_ENCODING),
744 gltf::Semantic::Weights(0) => Some(&WEIGHTS_ENCODING),
745 gltf::Semantic::Tangents
750 | gltf::Semantic::Colors(_)
751 | gltf::Semantic::TexCoords(_)
752 | gltf::Semantic::Joints(_)
753 | gltf::Semantic::Weights(_) => None,
754 }
755}
756
757fn validate_primitive_accessors(
822 doc: &gltf::Document,
823 buffers: &[Vec<u8>],
824) -> Result<(), LoadError> {
825 for mesh in doc.meshes() {
826 for primitive in mesh.primitives() {
827 if primitive.mode() != gltf::mesh::Mode::Triangles {
828 continue;
829 }
830 for (semantic, accessor) in primitive.attributes() {
831 let Some(required) = required_attribute_encoding(&semantic) else {
832 continue;
833 };
834 check_primitive_accessor(
835 &mesh,
836 &primitive,
837 &semantic.to_string(),
838 &accessor,
839 required,
840 buffers,
841 )?;
842 }
843 if let Some(accessor) = primitive.indices() {
844 check_primitive_accessor(
845 &mesh,
846 &primitive,
847 "indices",
848 &accessor,
849 &INDEX_ENCODING,
850 buffers,
851 )?;
852 }
853 }
854 }
855 Ok(())
856}
857
858fn check_primitive_accessor(
861 mesh: &gltf::Mesh<'_>,
862 primitive: &gltf::Primitive<'_>,
863 attribute: &str,
864 accessor: &gltf::Accessor<'_>,
865 required: &ReaderEncoding,
866 buffers: &[Vec<u8>],
867) -> Result<(), LoadError> {
868 if !encoding_matches(accessor, required) {
869 return Err(LoadError::PrimitiveEncoding {
870 mesh: mesh.index(),
871 primitive: primitive.index(),
872 attribute: attribute.to_owned(),
873 accessor: accessor.index(),
874 found: format!(
875 "{} of {}",
876 accessor_type_name(accessor.dimensions()),
877 component_type_name(accessor.data_type())
878 ),
879 expected: describe_encoding(required),
880 });
881 }
882 if let Some(problem) = unreadable_primitive_layout(accessor, buffers) {
883 return Err(LoadError::PrimitiveAccessorLayout {
884 mesh: mesh.index(),
885 primitive: primitive.index(),
886 attribute: attribute.to_owned(),
887 accessor: accessor.index(),
888 problem,
889 });
890 }
891 Ok(())
892}
893
894fn unreadable_primitive_layout(
897 accessor: &gltf::Accessor<'_>,
898 buffers: &[Vec<u8>],
899) -> Option<String> {
900 unreadable_layout(accessor).or_else(|| {
901 if let Some(view) = accessor.view()
902 && let Some(problem) = loaded_buffer_shortfall("elements", &view, buffers)
903 {
904 return Some(problem);
905 }
906 let sparse = accessor.sparse()?;
907 loaded_buffer_shortfall("sparse indices", &sparse.indices().view(), buffers)
908 .or_else(|| loaded_buffer_shortfall("sparse values", &sparse.values().view(), buffers))
909 })
910}
911
912fn loaded_buffer_shortfall(
917 subject: &str,
918 view: &gltf::buffer::View<'_>,
919 buffers: &[Vec<u8>],
920) -> Option<String> {
921 let view_end = view_end(view)?;
922 let buffer_index = view.buffer().index();
923 let loaded_length = buffers.get(buffer_index).map_or(0, Vec::len);
924 (view_end > loaded_length).then(|| {
925 format!(
926 "reads its {subject} from buffer view {}, whose byte extent ends at {view_end} \
927 beyond loaded buffer {buffer_index}'s {loaded_length} bytes",
928 view.index()
929 )
930 })
931}
932
933fn check_sampler_accessor(
949 clip: &str,
950 node: usize,
951 slot: &'static str,
952 accessor: &gltf::Accessor<'_>,
953) -> Result<(), LoadError> {
954 match unreadable_layout(accessor) {
955 Some(problem) => Err(LoadError::AnimationAccessorLayout {
956 clip: clip.to_owned(),
957 node,
958 slot,
959 accessor: accessor.index(),
960 problem,
961 }),
962 None => Ok(()),
963 }
964}
965
966fn unreadable_layout(accessor: &gltf::Accessor<'_>) -> Option<String> {
1020 let size = accessor.size();
1021 if let Some(view) = accessor.view()
1022 && let Some(problem) =
1023 unwalkable("elements", &view, accessor.offset(), accessor.count(), size)
1024 {
1025 return Some(problem);
1026 }
1027 let sparse = accessor.sparse()?;
1028 if sparse.count() == 0 {
1029 return Some("declares a sparse block of count 0, which its reader cannot walk".to_owned());
1030 }
1031 let indices = sparse.indices();
1032 let values = sparse.values();
1033 unwalkable(
1037 "sparse indices",
1038 &indices.view(),
1039 indices.offset(),
1040 sparse.count(),
1041 indices.index_type().size(),
1042 )
1043 .or_else(|| {
1044 unwalkable(
1045 "sparse values",
1046 &values.view(),
1047 values.offset(),
1048 sparse.count(),
1049 size,
1050 )
1051 })
1052}
1053
1054fn view_end(view: &gltf::buffer::View<'_>) -> Option<usize> {
1070 view.offset().checked_add(view.length())
1071}
1072
1073fn unwalkable(
1076 subject: &str,
1077 view: &gltf::buffer::View<'_>,
1078 offset: usize,
1079 count: usize,
1080 size: usize,
1081) -> Option<String> {
1082 let Some(view_end) = view_end(view) else {
1085 return Some(format!(
1086 "reads its {subject} from buffer view {}, whose byteOffset {} plus byteLength {} \
1087 is a byte extent that overflows",
1088 view.index(),
1089 view.offset(),
1090 view.length()
1091 ));
1092 };
1093 if view_end > view.buffer().length() {
1094 return Some(format!(
1095 "reads its {subject} from buffer view {}, whose byte extent ends at {view_end} \
1096 beyond buffer {}'s byteLength {}",
1097 view.index(),
1098 view.buffer().index(),
1099 view.buffer().length()
1100 ));
1101 }
1102 let stride = view.stride().unwrap_or(size);
1103 if stride < size {
1104 return Some(format!(
1105 "reads its {subject} from buffer view {} at byteStride {stride}, \
1106 shorter than the {size}-byte element it strides over",
1107 view.index()
1108 ));
1109 }
1110 let required_end = count
1111 .checked_sub(1)
1112 .and_then(|last| stride.checked_mul(last))
1113 .and_then(|span| span.checked_add(offset))
1114 .and_then(|end| end.checked_add(size));
1115 if count == 0 {
1119 return None;
1120 }
1121 let Some(required_end) = required_end else {
1122 return Some(format!(
1123 "walks {count} {subject} of {size} bytes at byteStride {stride} \
1124 from byteOffset {offset}, a byte extent that overflows"
1125 ));
1126 };
1127 (required_end > view.length()).then(|| {
1128 format!(
1129 "walks {count} {subject} of {size} bytes at byteStride {stride} from byteOffset \
1130 {offset}, requiring byte extent {required_end} beyond buffer view {}'s byteLength {}",
1131 view.index(),
1132 view.length()
1133 )
1134 })
1135}
1136
1137fn inverse_bind_is_readable(accessor: &gltf::Accessor<'_>) -> bool {
1149 encoding_matches(accessor, &INVERSE_BIND_ENCODING) && unreadable_layout(accessor).is_none()
1150}
1151
1152fn encoding_matches(accessor: &gltf::Accessor<'_>, required: &ReaderEncoding) -> bool {
1153 accessor.dimensions() == required.accessor_type
1154 && required.component_types.contains(&accessor.data_type())
1155 && (!required.normalized_integers
1156 || accessor.data_type() == ComponentType::F32
1157 || accessor.normalized())
1158}
1159
1160fn describe_encoding(required: &ReaderEncoding) -> String {
1163 let names: Vec<String> = required
1164 .component_types
1165 .iter()
1166 .copied()
1167 .map(|component| {
1168 let name = component_type_name(component);
1169 if required.normalized_integers && component != ComponentType::F32 {
1170 format!("normalized {name}")
1171 } else {
1172 name.to_owned()
1173 }
1174 })
1175 .collect();
1176 let components = match names.as_slice() {
1177 [] => String::new(),
1178 [only] => only.clone(),
1179 [first, last] => format!("{first} or {last}"),
1180 [rest @ .., last] => format!("{}, or {last}", rest.join(", ")),
1181 };
1182 format!(
1183 "{} of {components}",
1184 accessor_type_name(required.accessor_type)
1185 )
1186}
1187
1188fn accessor_type_name(accessor_type: AccessorType) -> &'static str {
1189 match accessor_type {
1190 AccessorType::Scalar => "SCALAR",
1191 AccessorType::Vec2 => "VEC2",
1192 AccessorType::Vec3 => "VEC3",
1193 AccessorType::Vec4 => "VEC4",
1194 AccessorType::Mat2 => "MAT2",
1195 AccessorType::Mat3 => "MAT3",
1196 AccessorType::Mat4 => "MAT4",
1197 }
1198}
1199
1200fn component_type_name(component_type: ComponentType) -> &'static str {
1201 match component_type {
1202 ComponentType::I8 => "BYTE",
1203 ComponentType::U8 => "UNSIGNED_BYTE",
1204 ComponentType::I16 => "SHORT",
1205 ComponentType::U16 => "UNSIGNED_SHORT",
1206 ComponentType::U32 => "UNSIGNED_INT",
1207 ComponentType::F32 => "FLOAT",
1208 }
1209}
1210
1211fn validate_track_lengths(
1217 clip: &str,
1218 node: usize,
1219 interpolation: Interpolation,
1220 times: &[f32],
1221 values: &TrackValues,
1222) -> Result<(), LoadError> {
1223 if times.is_empty() {
1224 return Err(LoadError::Malformed(format!(
1225 "clip '{clip}' node {node}: animation channel with zero keyframes"
1226 )));
1227 }
1228 let per_key = match interpolation {
1229 Interpolation::CubicSpline => 3,
1230 _ => 1,
1231 };
1232 let expected = times.len() * per_key;
1233 let actual = match values {
1234 TrackValues::Vec3s(v) => v.len(),
1235 TrackValues::Quats(v) => v.len(),
1236 };
1237 if actual != expected {
1238 return Err(LoadError::Malformed(format!(
1239 "clip '{clip}' node {node}: {} keyframe times but {actual} output values (expected {expected})",
1240 times.len()
1241 )));
1242 }
1243 Ok(())
1244}
1245
1246pub fn load(path: &Path) -> Result<Document, LoadError> {
1262 load_source(path).map(LoadedSource::into_document)
1263}
1264
1265pub fn load_source(path: &Path) -> Result<LoadedSource, LoadError> {
1278 let bytes = std::fs::read(path).map_err(|source| LoadError::Io {
1279 path: path.display().to_string(),
1280 source,
1281 })?;
1282 let root = path.parent().unwrap_or_else(|| Path::new("."));
1283 load_source_bytes_with_resource_root(path, &bytes, root)
1284}
1285
1286pub fn load_bytes(path: &Path, bytes: &[u8]) -> Result<Document, LoadError> {
1301 load_source_bytes(path, bytes).map(LoadedSource::into_document)
1302}
1303
1304pub fn load_bytes_with_resource_root(
1315 path: &Path,
1316 bytes: &[u8],
1317 resource_root: &Path,
1318) -> Result<Document, LoadError> {
1319 load_source_bytes_with_resource_root(path, bytes, resource_root)
1320 .map(LoadedSource::into_document)
1321}
1322
1323pub fn load_source_bytes(path: &Path, bytes: &[u8]) -> Result<LoadedSource, LoadError> {
1337 load_source_bytes_inner(path, bytes, None)
1338}
1339
1340pub fn load_source_bytes_with_resource_root(
1354 path: &Path,
1355 bytes: &[u8],
1356 resource_root: &Path,
1357) -> Result<LoadedSource, LoadError> {
1358 load_source_bytes_inner(path, bytes, Some(resource_root))
1359}
1360
1361fn load_source_bytes_inner(
1362 path: &Path,
1363 bytes: &[u8],
1364 resource_root: Option<&Path>,
1365) -> Result<LoadedSource, LoadError> {
1366 load_source_bytes_inner_with_reader(path, bytes, resource_root, read_external_file)
1367}
1368
1369fn load_source_bytes_inner_with_reader<F>(
1370 path: &Path,
1371 bytes: &[u8],
1372 resource_root: Option<&Path>,
1373 mut read_external: F,
1374) -> Result<LoadedSource, LoadError>
1375where
1376 F: FnMut(&Path, u64) -> CapturedResource,
1377{
1378 validate_glb_framing(bytes)?;
1386 let gltf = gltf::Gltf::from_slice(bytes)?;
1391 validate_animations(&gltf.document)?;
1392 let mut facts = source_facts_builder(bytes)?;
1393 project_extension_facts(&gltf.document, &mut facts);
1394 project_resource_facts(&gltf.document, &mut facts);
1395 let has_unmodeled_extension_domain = has_extension_object(bytes)
1396 || gltf.document.extensions_used().next().is_some()
1397 || gltf.document.extensions_required().next().is_some();
1398 let (dependency_closure, mut resources) = capture_dependency_closure(
1399 &facts,
1400 resource_root,
1401 has_unmodeled_extension_domain,
1402 &mut read_external,
1403 )?;
1404 let buffers = resolve_captured_buffers(&gltf, &mut resources)?;
1405 validate_primitive_accessors(&gltf.document, &buffers)?;
1406 let topo = topology(&gltf.document)?;
1410 let source_skeleton = extract_source_skeleton(&gltf.document, &buffers, &topo);
1411 let mut doc = build_document(&gltf, &buffers, path, &topo, &mut facts)?;
1412 doc.assets = extract_assets(&gltf.document, &buffers, &mut resources, &topo.bone_of_node);
1413 doc.assets.scenes = extract_scenes(&gltf.document, &topo.bone_of_node);
1414 doc.assets.default_scene = gltf.document.default_scene().map(|scene| scene.index());
1415 doc.assets.source_skeleton = source_skeleton;
1416 facts
1417 .finish_with_dependency_closure(doc, dependency_closure)
1418 .map_err(LoadError::from)
1419}
1420
1421fn source_facts_builder(primary_bytes: &[u8]) -> Result<RawSourceFactsBuilderV1, SourceFactsError> {
1422 let format = if primary_bytes.starts_with(b"glTF") {
1423 SourceFormatV1::Glb
1424 } else {
1425 SourceFormatV1::GltfJson
1426 };
1427 let mut facts = RawSourceFactsBuilderV1::new(format, InputIdentity::from_bytes(primary_bytes));
1428 facts.set_linear_unit(SourceObservationV1::observed(
1429 SourceLinearUnitV1::new(1.0)?,
1430 SourceProvenanceV1::format_defined(),
1431 SourceLoaderDispositionV1::Preserved,
1432 ));
1433 facts.set_coordinate_basis(SourceObservationV1::observed(
1434 SourceCoordinateBasisV1::new(
1435 SourceAxisV1::PositiveX,
1436 SourceAxisV1::PositiveY,
1437 SourceAxisV1::PositiveZ,
1438 )?,
1439 SourceProvenanceV1::format_defined(),
1440 SourceLoaderDispositionV1::Preserved,
1441 ));
1442 facts.set_frames_per_second(SourceObservationV1::proven_absent(
1443 SourceProvenanceV1::format_defined(),
1444 ));
1445
1446 Ok(facts)
1447}
1448
1449fn project_extension_facts(document: &gltf::Document, facts: &mut RawSourceFactsBuilderV1) {
1450 let mut source_order_index = 0;
1451 for name in document.extensions_used() {
1452 if !project_extension_declaration(name, false, "/extensionsUsed", source_order_index, facts)
1453 {
1454 return;
1455 }
1456 source_order_index += 1;
1457 }
1458 for name in document.extensions_required() {
1459 if !project_extension_declaration(
1460 name,
1461 true,
1462 "/extensionsRequired",
1463 source_order_index,
1464 facts,
1465 ) {
1466 return;
1467 }
1468 source_order_index += 1;
1469 }
1470 facts.mark_complete(SourceFactDomainV1::Constructs);
1471}
1472
1473fn project_extension_declaration(
1474 name: &str,
1475 required: bool,
1476 provenance: &'static str,
1477 source_order_index: usize,
1478 facts: &mut RawSourceFactsBuilderV1,
1479) -> bool {
1480 if facts.remaining_observation_rows() == 0 {
1481 facts.mark_budget_exceeded(SourceFactDomainV1::Constructs);
1482 return false;
1483 }
1484 if name.len().saturating_add(provenance.len()) > facts.remaining_text_bytes()
1485 || name.len() > RAW_SOURCE_V1_MAX_TEXT_BYTES
1486 {
1487 facts.mark_budget_exceeded(SourceFactDomainV1::Constructs);
1488 return false;
1489 }
1490 let row = SourceConstructFactV1::new(
1491 source_order_index,
1492 SourceConstructKindV1::Extension,
1493 SourceTextV1::new(name).expect("extension name was checked against the text bound"),
1494 required,
1495 1,
1496 SourceLoaderDispositionV1::Unsupported,
1497 located_provenance(
1498 SourceProvenanceKindV1::SourceDeclared,
1499 provenance.to_owned(),
1500 ),
1501 )
1502 .expect("an extension declaration has a positive count");
1503 facts.push_construct(row)
1504}
1505
1506fn project_resource_facts(document: &gltf::Document, facts: &mut RawSourceFactsBuilderV1) {
1507 let mut source_order_index = 0;
1508 for buffer in document.buffers() {
1509 if facts.remaining_resource_rows() == 0 || facts.remaining_observation_rows() == 0 {
1510 facts.mark_budget_exceeded(SourceFactDomainV1::Resources);
1511 return;
1512 }
1513 let (possible_locator_bytes, pointer_len) = match buffer.source() {
1514 gltf::buffer::Source::Bin => (0, "/buffers/".len() + decimal_len(buffer.index())),
1515 gltf::buffer::Source::Uri(uri) => (
1516 SourceResourceLocatorV1::retained_relative_bytes(uri),
1517 "/buffers/".len() + decimal_len(buffer.index()) + "/uri".len(),
1518 ),
1519 };
1520 if possible_locator_bytes.saturating_add(pointer_len) > facts.remaining_text_bytes() {
1521 facts.mark_budget_exceeded(SourceFactDomainV1::Resources);
1522 return;
1523 }
1524 let (locator, pointer) = match buffer.source() {
1525 gltf::buffer::Source::Bin => (
1526 SourceResourceLocatorV1::Embedded,
1527 format!("/buffers/{}", buffer.index()),
1528 ),
1529 gltf::buffer::Source::Uri(uri) => (
1530 SourceResourceLocatorV1::classify(uri),
1531 format!("/buffers/{}/uri", buffer.index()),
1532 ),
1533 };
1534 if !facts.push_resource(SourceResourceReferenceV1::new(
1535 source_order_index,
1536 SourceResourceKindV1::Buffer,
1537 buffer.index() as u64,
1538 locator,
1539 SourceLoaderDispositionV1::Preserved,
1540 located_provenance(SourceProvenanceKindV1::SourceDeclared, pointer),
1541 )) {
1542 return;
1543 }
1544 source_order_index += 1;
1545 }
1546 for image in document.images() {
1547 if facts.remaining_resource_rows() == 0 || facts.remaining_observation_rows() == 0 {
1548 facts.mark_budget_exceeded(SourceFactDomainV1::Resources);
1549 return;
1550 }
1551 let (possible_locator_bytes, pointer_len) = match image.source() {
1552 gltf::image::Source::View { .. } => (0, "/images/".len() + decimal_len(image.index())),
1553 gltf::image::Source::Uri { uri, .. } => (
1554 SourceResourceLocatorV1::retained_relative_bytes(uri),
1555 "/images/".len() + decimal_len(image.index()) + "/uri".len(),
1556 ),
1557 };
1558 if possible_locator_bytes.saturating_add(pointer_len) > facts.remaining_text_bytes() {
1559 facts.mark_budget_exceeded(SourceFactDomainV1::Resources);
1560 return;
1561 }
1562 let (locator, pointer) = match image.source() {
1563 gltf::image::Source::View { .. } => (
1564 SourceResourceLocatorV1::Embedded,
1565 format!("/images/{}", image.index()),
1566 ),
1567 gltf::image::Source::Uri { uri, .. } => (
1568 SourceResourceLocatorV1::classify(uri),
1569 format!("/images/{}/uri", image.index()),
1570 ),
1571 };
1572 if !facts.push_resource(SourceResourceReferenceV1::new(
1573 source_order_index,
1574 SourceResourceKindV1::Image,
1575 image.index() as u64,
1576 locator,
1577 SourceLoaderDispositionV1::Preserved,
1578 located_provenance(SourceProvenanceKindV1::SourceDeclared, pointer),
1579 )) {
1580 return;
1581 }
1582 source_order_index += 1;
1583 }
1584 facts.mark_complete(SourceFactDomainV1::Resources);
1585}
1586
1587fn located_provenance(kind: SourceProvenanceKindV1, locator: String) -> SourceProvenanceV1 {
1588 let locator = SourceLogicalLocatorV1::gltf_json_pointer(locator)
1589 .expect("generated glTF locator is valid and bounded");
1590 match kind {
1591 SourceProvenanceKindV1::SourceDeclared => SourceProvenanceV1::source_declared(locator),
1592 SourceProvenanceKindV1::ParserProjected => SourceProvenanceV1::parser_projected(locator),
1593 SourceProvenanceKindV1::DerivedFromSource => {
1594 SourceProvenanceV1::derived_from_source(locator)
1595 }
1596 SourceProvenanceKindV1::FormatDefined => {
1597 unreachable!("located glTF provenance is never format-defined")
1598 }
1599 }
1600}
1601
1602pub(crate) fn resolve_buffers(
1603 gltf: &gltf::Gltf,
1604 base: Option<&Path>,
1605) -> Result<Vec<Vec<u8>>, LoadError> {
1606 let mut buffers = Vec::new();
1607 for buffer in gltf.buffers() {
1608 let data = match buffer.source() {
1609 gltf::buffer::Source::Bin => gltf
1610 .blob
1611 .clone()
1612 .ok_or_else(|| LoadError::Buffer("GLB has no BIN chunk".into()))?,
1613 gltf::buffer::Source::Uri(uri) => {
1614 if let Some(encoded) = uri.strip_prefix("data:") {
1615 let payload =
1616 encoded
1617 .split_once("base64,")
1618 .map(|(_, p)| p)
1619 .ok_or_else(|| {
1620 LoadError::Buffer("unsupported data URI in buffer".to_owned())
1621 })?;
1622 base64::engine::general_purpose::STANDARD
1623 .decode(payload)
1624 .map_err(|e| LoadError::Buffer(format!("bad base64 data URI: {e}")))?
1625 } else {
1626 let root = base.ok_or_else(resource_root_required)?;
1627 let path = root.join(safe_external_buffer_path(uri)?);
1628 std::fs::read(&path).map_err(|source| LoadError::Io {
1629 path: "<external glTF buffer>".to_owned(),
1633 source,
1634 })?
1635 }
1636 }
1637 };
1638 buffers.push(data);
1639 }
1640 Ok(buffers)
1641}
1642
1643const MAX_EXTERNAL_MATERIALIZED_BYTES: u64 = 256 * 1024 * 1024;
1646
1647#[derive(Clone, Copy)]
1648enum CapturedResourceFailure {
1649 Refused(DependencyResourceRefusalReasonV1),
1650 Unavailable(DependencyResourceUnavailableReasonV1),
1651}
1652
1653enum CapturedResource {
1654 Bytes(Vec<u8>),
1655 Failure(CapturedResourceFailure),
1656}
1657
1658#[derive(Clone)]
1659enum CapturedReference {
1660 Primary,
1661 External(DependencyResourceKeyV1),
1662 Failure(CapturedResourceFailure),
1663}
1664
1665struct ResourceCaptureSession {
1671 root: TrustedResourceRoot,
1672 resources: BTreeMap<DependencyResourceKeyV1, CapturedResource>,
1673 references: BTreeMap<(SourceResourceKindV1, u64), CapturedReference>,
1674 materialized_external_bytes: u64,
1675 materialized_external_limit: u64,
1676}
1677
1678enum TrustedResourceRoot {
1679 Absent,
1680 Available(PathBuf),
1681 Failure(CapturedResourceFailure),
1682}
1683
1684impl ResourceCaptureSession {
1685 fn new(root: Option<&Path>) -> Self {
1686 Self {
1687 root: trusted_resource_root(root),
1688 resources: BTreeMap::new(),
1689 references: BTreeMap::new(),
1690 materialized_external_bytes: 0,
1691 materialized_external_limit: MAX_EXTERNAL_MATERIALIZED_BYTES,
1692 }
1693 }
1694
1695 fn insert_reference(
1696 &mut self,
1697 kind: SourceResourceKindV1,
1698 source_index: u64,
1699 reference: CapturedReference,
1700 ) {
1701 self.references.insert((kind, source_index), reference);
1702 }
1703
1704 fn reference(&self, kind: SourceResourceKindV1, source_index: u64) -> CapturedReference {
1705 self.references
1706 .get(&(kind, source_index))
1707 .cloned()
1708 .unwrap_or(CapturedReference::Failure(
1709 CapturedResourceFailure::Unavailable(
1710 DependencyResourceUnavailableReasonV1::ResourceBudgetExceeded,
1711 ),
1712 ))
1713 }
1714
1715 fn preflight_external(
1718 &self,
1719 key: &DependencyResourceKeyV1,
1720 ) -> Result<PathBuf, CapturedResourceFailure> {
1721 let root = match &self.root {
1722 TrustedResourceRoot::Available(root) => root,
1723 TrustedResourceRoot::Absent => {
1724 return Err(CapturedResourceFailure::Unavailable(
1725 DependencyResourceUnavailableReasonV1::ResourceRootUnavailable,
1726 ));
1727 }
1728 TrustedResourceRoot::Failure(failure) => return Err(*failure),
1729 };
1730 let mut path = root.clone();
1731 let mut components = key.as_str().split('/').peekable();
1732 while let Some(component) = components.next() {
1733 let is_final = components.peek().is_none();
1734 path.push(component);
1735 match std::fs::symlink_metadata(&path) {
1736 Ok(metadata) if metadata.file_type().is_symlink() => {
1737 return Err(CapturedResourceFailure::Refused(
1738 DependencyResourceRefusalReasonV1::Symlink,
1739 ));
1740 }
1741 Ok(metadata)
1742 if (!is_final && metadata.is_dir()) || (is_final && metadata.is_file()) => {}
1743 Ok(_) => {
1744 return Err(CapturedResourceFailure::Unavailable(
1745 DependencyResourceUnavailableReasonV1::Unreadable,
1746 ));
1747 }
1748 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
1749 return Err(CapturedResourceFailure::Unavailable(
1750 DependencyResourceUnavailableReasonV1::Missing,
1751 ));
1752 }
1753 Err(_) => {
1754 return Err(CapturedResourceFailure::Unavailable(
1755 DependencyResourceUnavailableReasonV1::Unreadable,
1756 ));
1757 }
1758 }
1759 }
1760 Ok(path)
1761 }
1762
1763 fn materialize_external(
1764 &mut self,
1765 kind: SourceResourceKindV1,
1766 source_index: u64,
1767 ) -> Result<Vec<u8>, CapturedResourceFailure> {
1768 let CapturedReference::External(key) = self.reference(kind, source_index) else {
1769 return Err(reference_failure(self.reference(kind, source_index)));
1770 };
1771 let length = match self.resources.get(&key) {
1772 Some(CapturedResource::Bytes(bytes)) => bytes.len() as u64,
1773 Some(CapturedResource::Failure(failure)) => return Err(*failure),
1774 None => {
1775 return Err(CapturedResourceFailure::Unavailable(
1776 DependencyResourceUnavailableReasonV1::ResourceBudgetExceeded,
1777 ));
1778 }
1779 };
1780 let Some(next) = self.materialized_external_bytes.checked_add(length) else {
1781 return Err(CapturedResourceFailure::Unavailable(
1782 DependencyResourceUnavailableReasonV1::ResourceBudgetExceeded,
1783 ));
1784 };
1785 if next > self.materialized_external_limit {
1786 return Err(CapturedResourceFailure::Unavailable(
1787 DependencyResourceUnavailableReasonV1::ResourceBudgetExceeded,
1788 ));
1789 }
1790 let bytes = match self.resources.get(&key) {
1791 Some(CapturedResource::Bytes(bytes)) => bytes.clone(),
1792 _ => unreachable!("captured resource state changed without mutation"),
1793 };
1794 self.materialized_external_bytes = next;
1795 Ok(bytes)
1796 }
1797
1798 fn external_image_payload(
1799 &self,
1800 image_index: usize,
1801 ) -> (Option<&[u8]>, ImageUnavailableReason) {
1802 let CapturedReference::External(key) =
1803 self.reference(SourceResourceKindV1::Image, image_index as u64)
1804 else {
1805 return (None, ImageUnavailableReason::SourceUnavailable);
1806 };
1807 match self.resources.get(&key) {
1808 Some(CapturedResource::Bytes(bytes)) => (
1809 Some(bytes.as_slice()),
1810 ImageUnavailableReason::SourceUnavailable,
1811 ),
1812 _ => (None, ImageUnavailableReason::SourceUnavailable),
1813 }
1814 }
1815
1816 fn external_image_is_available(&self, image_index: usize) -> bool {
1817 self.external_image_payload(image_index).0.is_some()
1818 }
1819
1820 fn clone_image_for_material(
1821 &mut self,
1822 image_index: usize,
1823 texture: &TextureAsset,
1824 ) -> Option<TextureAsset> {
1825 if matches!(
1826 self.reference(SourceResourceKindV1::Image, image_index as u64),
1827 CapturedReference::External(_)
1828 ) {
1829 let length = texture.bytes.len() as u64;
1830 let next = self.materialized_external_bytes.checked_add(length)?;
1831 if next > self.materialized_external_limit {
1832 return None;
1833 }
1834 self.materialized_external_bytes = next;
1835 }
1836 Some(texture.clone())
1837 }
1838}
1839
1840fn read_external_file(path: &Path, limit: u64) -> CapturedResource {
1843 let file = match std::fs::File::open(path) {
1844 Ok(file) => file,
1845 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
1846 return CapturedResource::Failure(CapturedResourceFailure::Unavailable(
1847 DependencyResourceUnavailableReasonV1::Missing,
1848 ));
1849 }
1850 Err(_) => {
1851 return CapturedResource::Failure(CapturedResourceFailure::Unavailable(
1852 DependencyResourceUnavailableReasonV1::Unreadable,
1853 ));
1854 }
1855 };
1856 let max_read = limit.saturating_add(1);
1857 let mut bytes = Vec::new();
1858 let read = file.take(max_read).read_to_end(&mut bytes);
1859 if read.is_err() {
1860 return CapturedResource::Failure(CapturedResourceFailure::Unavailable(
1861 DependencyResourceUnavailableReasonV1::Unreadable,
1862 ));
1863 }
1864 CapturedResource::Bytes(bytes)
1865}
1866
1867fn trusted_resource_root(root: Option<&Path>) -> TrustedResourceRoot {
1874 let Some(root) = root else {
1875 return TrustedResourceRoot::Absent;
1876 };
1877 let root = if root.is_absolute() {
1878 root.to_path_buf()
1879 } else {
1880 match std::env::current_dir() {
1881 Ok(current) => current.join(root),
1882 Err(_) => {
1883 return TrustedResourceRoot::Failure(CapturedResourceFailure::Unavailable(
1884 DependencyResourceUnavailableReasonV1::ResourceRootUnavailable,
1885 ));
1886 }
1887 }
1888 };
1889 match std::fs::symlink_metadata(&root) {
1890 Ok(metadata) if metadata.is_dir() => TrustedResourceRoot::Available(root),
1891 Ok(metadata) if metadata.file_type().is_symlink() => TrustedResourceRoot::Failure(
1892 CapturedResourceFailure::Refused(DependencyResourceRefusalReasonV1::Symlink),
1893 ),
1894 Ok(_) | Err(_) => TrustedResourceRoot::Failure(CapturedResourceFailure::Unavailable(
1895 DependencyResourceUnavailableReasonV1::ResourceRootUnavailable,
1896 )),
1897 }
1898}
1899
1900fn reference_failure(reference: CapturedReference) -> CapturedResourceFailure {
1901 match reference {
1902 CapturedReference::Failure(failure) => failure,
1903 CapturedReference::Primary | CapturedReference::External(_) => {
1904 CapturedResourceFailure::Unavailable(
1905 DependencyResourceUnavailableReasonV1::ResourceBudgetExceeded,
1906 )
1907 }
1908 }
1909}
1910
1911fn capture_dependency_closure<F>(
1912 facts: &RawSourceFactsBuilderV1,
1913 root: Option<&Path>,
1914 has_unmodeled_resource_domain: bool,
1915 read_external: &mut F,
1916) -> Result<(DependencyClosureV1, ResourceCaptureSession), LoadError>
1917where
1918 F: FnMut(&Path, u64) -> CapturedResource,
1919{
1920 let mut closure = DependencyClosureBuilderV1::new(
1921 facts.primary_identity().clone(),
1922 facts.resource_coverage(),
1923 facts.resource_rows().len(),
1924 );
1925 if has_unmodeled_resource_domain {
1926 closure.mark_unmodeled_resource_domain();
1927 }
1928 let mut session = ResourceCaptureSession::new(root);
1929 for row in facts.resource_rows() {
1930 let (locator_bytes, components) = match row.locator() {
1931 SourceResourceLocatorV1::Relative(locator) => (
1932 locator.as_str().len(),
1933 DependencyResourceKeyV1::source_component_count(locator),
1934 ),
1935 _ => (0, 0),
1936 };
1937 if !closure.begin_reference(locator_bytes, components) {
1938 break;
1939 }
1940 let kind = row.kind();
1941 let source_index = row.source_index();
1942 let source_order_index = row.source_order_index();
1943 match row.locator() {
1944 SourceResourceLocatorV1::Embedded | SourceResourceLocatorV1::DataUri => {
1945 closure
1946 .push_primary(source_order_index, kind, source_index)
1947 .map_err(SourceFactsError::from)?;
1948 session.insert_reference(kind, source_index, CapturedReference::Primary);
1949 }
1950 SourceResourceLocatorV1::Absolute => {
1951 record_refused(
1952 &mut closure,
1953 &mut session,
1954 source_order_index,
1955 kind,
1956 source_index,
1957 DependencyResourceRefusalReasonV1::Absolute,
1958 )?;
1959 }
1960 SourceResourceLocatorV1::Escaping => {
1961 record_refused(
1962 &mut closure,
1963 &mut session,
1964 source_order_index,
1965 kind,
1966 source_index,
1967 DependencyResourceRefusalReasonV1::Escaping,
1968 )?;
1969 }
1970 SourceResourceLocatorV1::Remote => {
1971 record_refused(
1972 &mut closure,
1973 &mut session,
1974 source_order_index,
1975 kind,
1976 source_index,
1977 DependencyResourceRefusalReasonV1::Remote,
1978 )?;
1979 }
1980 SourceResourceLocatorV1::Malformed => {
1981 record_refused(
1982 &mut closure,
1983 &mut session,
1984 source_order_index,
1985 kind,
1986 source_index,
1987 DependencyResourceRefusalReasonV1::Malformed,
1988 )?;
1989 }
1990 SourceResourceLocatorV1::Oversized => {
1991 record_refused(
1992 &mut closure,
1993 &mut session,
1994 source_order_index,
1995 kind,
1996 source_index,
1997 DependencyResourceRefusalReasonV1::Oversized,
1998 )?;
1999 }
2000 SourceResourceLocatorV1::Missing => {
2001 closure
2002 .push_unavailable(
2003 source_order_index,
2004 kind,
2005 source_index,
2006 None,
2007 DependencyResourceUnavailableReasonV1::Missing,
2008 )
2009 .map_err(SourceFactsError::from)?;
2010 session.insert_reference(
2011 kind,
2012 source_index,
2013 CapturedReference::Failure(CapturedResourceFailure::Unavailable(
2014 DependencyResourceUnavailableReasonV1::Missing,
2015 )),
2016 );
2017 }
2018 SourceResourceLocatorV1::Relative(locator) => {
2019 if root.is_none() {
2022 return Err(resource_root_required());
2023 }
2024 let key = match DependencyResourceKeyV1::from_relative(
2025 locator,
2026 ResourceKeySyntaxV1::GltfUri,
2027 ) {
2028 Ok(key) => key,
2029 Err(error) => {
2030 let reason = match error {
2031 DependencyClosureError::ResourceKeyTooLong { .. }
2032 | DependencyClosureError::TooManyPathComponents { .. } => {
2033 DependencyResourceRefusalReasonV1::Oversized
2034 }
2035 _ => DependencyResourceRefusalReasonV1::Malformed,
2036 };
2037 record_refused(
2038 &mut closure,
2039 &mut session,
2040 source_order_index,
2041 kind,
2042 source_index,
2043 reason,
2044 )?;
2045 continue;
2046 }
2047 };
2048 match closure
2049 .prepare_external_key(&key)
2050 .map_err(SourceFactsError::from)?
2051 {
2052 None => break,
2053 Some(false) => {
2054 let reference = session
2055 .resources
2056 .get(&key)
2057 .map(|resource| match resource {
2058 CapturedResource::Bytes(_) => {
2059 CapturedReference::External(key.clone())
2060 }
2061 CapturedResource::Failure(failure) => {
2062 CapturedReference::Failure(*failure)
2063 }
2064 })
2065 .unwrap_or(CapturedReference::Failure(
2066 CapturedResourceFailure::Unavailable(
2067 DependencyResourceUnavailableReasonV1::ResourceBudgetExceeded,
2068 ),
2069 ));
2070 record_cached_reference(
2071 &mut closure,
2072 &mut session,
2073 source_order_index,
2074 kind,
2075 source_index,
2076 key,
2077 reference,
2078 )?;
2079 }
2080 Some(true) => {
2081 let limit = closure
2082 .max_resource_bytes()
2083 .min(closure.remaining_external_bytes());
2084 let resource = match session.preflight_external(&key) {
2085 Ok(path) => {
2086 closure
2089 .record_external_open_attempt(&key)
2090 .map_err(SourceFactsError::from)?;
2091 read_external(&path, limit)
2092 }
2093 Err(failure) => CapturedResource::Failure(failure),
2094 };
2095 let reference = match &resource {
2096 CapturedResource::Bytes(bytes) => {
2097 let identity = InputIdentity::from_bytes(bytes);
2098 let captured = closure
2099 .push_captured_external(
2100 source_order_index,
2101 kind,
2102 source_index,
2103 key.clone(),
2104 identity,
2105 )
2106 .map_err(SourceFactsError::from)?;
2107 if captured {
2108 CapturedReference::External(key.clone())
2109 } else {
2110 CapturedReference::Failure(
2111 CapturedResourceFailure::Unavailable(
2112 DependencyResourceUnavailableReasonV1::ResourceBudgetExceeded,
2113 ),
2114 )
2115 }
2116 }
2117 CapturedResource::Failure(failure) => {
2118 record_failure(
2119 &mut closure,
2120 source_order_index,
2121 kind,
2122 source_index,
2123 Some(key.clone()),
2124 *failure,
2125 )?;
2126 CapturedReference::Failure(*failure)
2127 }
2128 };
2129 let resource = match reference {
2130 CapturedReference::Failure(failure)
2131 if matches!(&resource, CapturedResource::Bytes(_)) =>
2132 {
2133 CapturedResource::Failure(failure)
2134 }
2135 _ => resource,
2136 };
2137 session.resources.insert(key, resource);
2138 session.insert_reference(kind, source_index, reference);
2139 }
2140 }
2141 }
2142 }
2143 }
2144 let closure = closure.finish().map_err(SourceFactsError::from)?;
2145 Ok((closure, session))
2146}
2147
2148fn record_refused(
2149 closure: &mut DependencyClosureBuilderV1,
2150 session: &mut ResourceCaptureSession,
2151 source_order_index: usize,
2152 kind: SourceResourceKindV1,
2153 source_index: u64,
2154 reason: DependencyResourceRefusalReasonV1,
2155) -> Result<(), LoadError> {
2156 closure
2157 .push_refused(source_order_index, kind, source_index, reason)
2158 .map_err(SourceFactsError::from)?;
2159 session.insert_reference(
2160 kind,
2161 source_index,
2162 CapturedReference::Failure(CapturedResourceFailure::Refused(reason)),
2163 );
2164 Ok(())
2165}
2166
2167fn record_failure(
2168 closure: &mut DependencyClosureBuilderV1,
2169 source_order_index: usize,
2170 kind: SourceResourceKindV1,
2171 source_index: u64,
2172 key: Option<DependencyResourceKeyV1>,
2173 failure: CapturedResourceFailure,
2174) -> Result<(), LoadError> {
2175 match failure {
2176 CapturedResourceFailure::Refused(reason) => closure
2177 .push_refused(source_order_index, kind, source_index, reason)
2178 .map_err(SourceFactsError::from)?,
2179 CapturedResourceFailure::Unavailable(reason) => closure
2180 .push_unavailable(source_order_index, kind, source_index, key, reason)
2181 .map_err(SourceFactsError::from)?,
2182 }
2183 Ok(())
2184}
2185
2186fn record_cached_reference(
2187 closure: &mut DependencyClosureBuilderV1,
2188 session: &mut ResourceCaptureSession,
2189 source_order_index: usize,
2190 kind: SourceResourceKindV1,
2191 source_index: u64,
2192 key: DependencyResourceKeyV1,
2193 reference: CapturedReference,
2194) -> Result<(), LoadError> {
2195 match &reference {
2196 CapturedReference::External(_) => closure
2197 .push_external_alias(source_order_index, kind, source_index, key)
2198 .map_err(SourceFactsError::from)?,
2199 CapturedReference::Failure(failure) => record_failure(
2200 closure,
2201 source_order_index,
2202 kind,
2203 source_index,
2204 Some(key),
2205 *failure,
2206 )?,
2207 CapturedReference::Primary => unreachable!("external aliases never map to primary"),
2208 }
2209 session.insert_reference(kind, source_index, reference);
2210 Ok(())
2211}
2212
2213fn resolve_captured_buffers(
2214 gltf: &gltf::Gltf,
2215 resources: &mut ResourceCaptureSession,
2216) -> Result<Vec<Vec<u8>>, LoadError> {
2217 let mut buffers = Vec::new();
2218 for buffer in gltf.buffers() {
2219 let data = match buffer.source() {
2220 gltf::buffer::Source::Bin => gltf
2221 .blob
2222 .clone()
2223 .ok_or_else(|| LoadError::Buffer("GLB has no BIN chunk".into()))?,
2224 gltf::buffer::Source::Uri(uri) if uri.starts_with("data:") => {
2225 let payload = uri
2226 .strip_prefix("data:")
2227 .and_then(|encoded| encoded.split_once("base64,").map(|(_, payload)| payload))
2228 .ok_or_else(|| {
2229 LoadError::Buffer("unsupported data URI in buffer".to_owned())
2230 })?;
2231 base64::engine::general_purpose::STANDARD
2232 .decode(payload)
2233 .map_err(|_| LoadError::Buffer("invalid data URI in buffer".to_owned()))?
2234 }
2235 gltf::buffer::Source::Uri(_) => resources
2236 .materialize_external(SourceResourceKindV1::Buffer, buffer.index() as u64)
2237 .map_err(buffer_capture_error)?,
2238 };
2239 buffers.push(data);
2240 }
2241 Ok(buffers)
2242}
2243
2244fn buffer_capture_error(failure: CapturedResourceFailure) -> LoadError {
2245 match failure {
2246 CapturedResourceFailure::Refused(_) => {
2247 LoadError::ExternalResource(ExternalResourceFailure::Refused)
2248 }
2249 CapturedResourceFailure::Unavailable(
2250 DependencyResourceUnavailableReasonV1::ResourceRootUnavailable,
2251 ) => resource_root_required(),
2252 CapturedResourceFailure::Unavailable(
2253 DependencyResourceUnavailableReasonV1::ResourceBudgetExceeded,
2254 ) => LoadError::ExternalResource(ExternalResourceFailure::CaptureLimitExceeded),
2255 CapturedResourceFailure::Unavailable(_) => {
2256 LoadError::ExternalResource(ExternalResourceFailure::Unavailable)
2257 }
2258 }
2259}
2260
2261fn resource_root_required() -> LoadError {
2262 LoadError::ExternalResource(ExternalResourceFailure::ResourceRootRequired)
2263}
2264
2265struct PendingGltfClipFacts {
2266 animation_index: usize,
2267 source_name: SourceObservationV1<SourceTextV1>,
2268 normalized_clip_provenance: SourceProvenanceV1,
2269 range_provenance: SourceProvenanceV1,
2270 channels: Vec<SourceChannelFactV1>,
2271 channel_limit: usize,
2272 remaining_text: usize,
2273 minimum: f64,
2274 maximum: f64,
2275 saw_input: bool,
2276 sampler_inputs_available: bool,
2277 sampler_inputs_finite: bool,
2278 truncated: bool,
2279}
2280
2281impl PendingGltfClipFacts {
2282 fn begin(
2283 animation: &gltf::Animation<'_>,
2284 builder: &mut RawSourceFactsBuilderV1,
2285 ) -> Option<Self> {
2286 if builder.remaining_clip_rows() == 0 || builder.remaining_observation_rows() == 0 {
2287 builder.mark_budget_exceeded(SourceFactDomainV1::Clips);
2288 return None;
2289 }
2290 let animation_index = animation.index();
2291 let prefix_len = "/animations/".len() + decimal_len(animation_index);
2292 let name_locator_len = prefix_len + "/name".len();
2293 let normalized_locator_len = prefix_len;
2294 let range_locator_len = prefix_len + "/samplers/*/input".len();
2295 let retained_name_len = animation.name().map_or(0, |name| {
2296 if name.len() <= RAW_SOURCE_V1_MAX_TEXT_BYTES {
2297 name.len()
2298 } else {
2299 0
2300 }
2301 });
2302 let fixed_text_len = name_locator_len
2303 .saturating_add(retained_name_len)
2304 .saturating_add(normalized_locator_len)
2305 .saturating_add(range_locator_len);
2306 if fixed_text_len > builder.remaining_text_bytes() {
2307 builder.mark_budget_exceeded(SourceFactDomainV1::Clips);
2308 return None;
2309 }
2310
2311 let name_locator = format!("/animations/{animation_index}/name");
2312 let name_provenance =
2313 located_provenance(SourceProvenanceKindV1::SourceDeclared, name_locator);
2314 let source_name = match animation.name() {
2315 Some(name) if name.len() <= RAW_SOURCE_V1_MAX_TEXT_BYTES => {
2316 SourceObservationV1::observed(
2317 SourceTextV1::new(name).expect("source name length checked before cloning"),
2318 name_provenance,
2319 SourceLoaderDispositionV1::Preserved,
2320 )
2321 }
2322 Some(_) => SourceObservationV1::unavailable(
2323 SourceUnavailableReasonV1::ProjectionBudgetExceeded,
2324 Some(name_provenance),
2325 SourceLoaderDispositionV1::Preserved,
2326 ),
2327 None => SourceObservationV1::proven_absent(name_provenance),
2328 };
2329
2330 Some(Self {
2331 animation_index,
2332 source_name,
2333 normalized_clip_provenance: located_provenance(
2334 SourceProvenanceKindV1::ParserProjected,
2335 format!("/animations/{animation_index}"),
2336 ),
2337 range_provenance: located_provenance(
2338 SourceProvenanceKindV1::DerivedFromSource,
2339 format!("/animations/{animation_index}/samplers/*/input"),
2340 ),
2341 channels: Vec::new(),
2342 channel_limit: builder.remaining_observation_rows().saturating_sub(1),
2343 remaining_text: builder.remaining_text_bytes() - fixed_text_len,
2344 minimum: f64::INFINITY,
2345 maximum: f64::NEG_INFINITY,
2346 saw_input: false,
2347 sampler_inputs_available: true,
2348 sampler_inputs_finite: true,
2349 truncated: false,
2350 })
2351 }
2352
2353 fn record_channel(&mut self, channel: &gltf::animation::Channel<'_>, times: Option<&[f32]>) {
2354 if self.channels.len() >= self.channel_limit {
2355 self.truncated = true;
2356 return;
2357 }
2358 let sampler = channel.sampler();
2359 let channel_index = channel.index();
2360 let interpolation_locator_len = "/animations/".len()
2361 + decimal_len(self.animation_index)
2362 + "/samplers/".len()
2363 + decimal_len(sampler.index())
2364 + "/interpolation".len();
2365 let channel_locator_len = "/animations/".len()
2366 + decimal_len(self.animation_index)
2367 + "/channels/".len()
2368 + decimal_len(channel_index);
2369 let row_text_len = interpolation_locator_len.saturating_add(channel_locator_len);
2370 if row_text_len > self.remaining_text {
2371 self.truncated = true;
2372 return;
2373 }
2374
2375 let (property, components, disposition) = match channel.target().property() {
2376 gltf::animation::Property::Translation => (
2377 SourceChannelPropertyV1::Translation,
2378 SourceComponentMaskV1::new(true, true, true),
2379 SourceLoaderDispositionV1::Preserved,
2380 ),
2381 gltf::animation::Property::Rotation => (
2382 SourceChannelPropertyV1::Rotation,
2383 SourceComponentMaskV1::new(true, true, true),
2384 SourceLoaderDispositionV1::Preserved,
2385 ),
2386 gltf::animation::Property::Scale => (
2387 SourceChannelPropertyV1::Scale,
2388 SourceComponentMaskV1::new(true, true, true),
2389 SourceLoaderDispositionV1::Preserved,
2390 ),
2391 gltf::animation::Property::MorphTargetWeights => (
2392 SourceChannelPropertyV1::Weights,
2393 SourceComponentMaskV1::new(false, false, false),
2394 SourceLoaderDispositionV1::Discarded,
2395 ),
2396 };
2397 let interpolation = match sampler.interpolation() {
2398 gltf::animation::Interpolation::Linear => SourceInterpolationV1::Linear,
2399 gltf::animation::Interpolation::Step => SourceInterpolationV1::Step,
2400 gltf::animation::Interpolation::CubicSpline => SourceInterpolationV1::CubicSpline,
2401 };
2402 let interpolation_provenance = located_provenance(
2403 SourceProvenanceKindV1::ParserProjected,
2407 format!(
2408 "/animations/{}/samplers/{}/interpolation",
2409 self.animation_index,
2410 sampler.index()
2411 ),
2412 );
2413 let channel_provenance = located_provenance(
2414 SourceProvenanceKindV1::SourceDeclared,
2415 format!(
2416 "/animations/{}/channels/{channel_index}",
2417 self.animation_index
2418 ),
2419 );
2420 self.channels.push(
2421 SourceChannelFactV1::new(
2422 channel_index,
2423 SourceTargetV1::new(
2424 SourceTargetKindV1::Node,
2425 channel.target().node().index() as u64,
2426 ),
2427 property,
2428 components,
2429 SourceObservationV1::observed(interpolation, interpolation_provenance, disposition),
2430 disposition,
2431 channel_provenance,
2432 )
2433 .with_accessors(sampler.input().index(), sampler.output().index()),
2434 );
2435 self.remaining_text -= row_text_len;
2436
2437 match times {
2438 Some(times) => {
2439 for &time in times {
2440 self.saw_input = true;
2441 if time.is_finite() {
2442 self.minimum = self.minimum.min(f64::from(time));
2443 self.maximum = self.maximum.max(f64::from(time));
2444 } else {
2445 self.sampler_inputs_finite = false;
2446 }
2447 }
2448 }
2449 None => self.sampler_inputs_available = false,
2450 }
2451 }
2452
2453 fn finish(self) -> Result<SourceClipFactV1, SourceFactsError> {
2454 let sampler_range = if self.truncated {
2455 SourceObservationV1::unavailable(
2456 SourceUnavailableReasonV1::ProjectionBudgetExceeded,
2457 Some(self.range_provenance),
2458 SourceLoaderDispositionV1::Preserved,
2459 )
2460 } else if !self.sampler_inputs_available {
2461 SourceObservationV1::unavailable(
2462 SourceUnavailableReasonV1::ParserUnavailable,
2463 Some(self.range_provenance),
2464 SourceLoaderDispositionV1::Unknown,
2465 )
2466 } else if !self.sampler_inputs_finite {
2467 SourceObservationV1::unavailable(
2468 SourceUnavailableReasonV1::Malformed,
2469 Some(self.range_provenance),
2470 SourceLoaderDispositionV1::Preserved,
2471 )
2472 } else if self.saw_input {
2473 SourceObservationV1::observed(
2474 SourceTimeRangeV1::new(self.minimum, self.maximum)?,
2475 self.range_provenance,
2476 SourceLoaderDispositionV1::Preserved,
2477 )
2478 } else {
2479 SourceObservationV1::proven_absent(self.range_provenance)
2480 };
2481 let channels = if self.truncated {
2482 SourceFactSetV1::partial(
2483 self.channels,
2484 SourceUnavailableReasonV1::ProjectionBudgetExceeded,
2485 )
2486 } else {
2487 SourceFactSetV1::complete(self.channels)
2488 };
2489 Ok(SourceClipFactV1::new(
2490 self.animation_index,
2491 self.source_name,
2492 SourceObservationV1::observed(
2493 self.animation_index,
2494 self.normalized_clip_provenance,
2495 SourceLoaderDispositionV1::Preserved,
2496 ),
2497 SourceObservationV1::proven_absent(SourceProvenanceV1::format_defined()),
2498 sampler_range,
2499 channels,
2500 ))
2501 }
2502}
2503
2504fn decimal_len(mut value: usize) -> usize {
2505 let mut len = 1;
2506 while value >= 10 {
2507 value /= 10;
2508 len += 1;
2509 }
2510 len
2511}
2512
2513fn build_document(
2514 gltf: &gltf::Gltf,
2515 buffers: &[Vec<u8>],
2516 path: &Path,
2517 topo: &Topology,
2518 source_facts: &mut RawSourceFactsBuilderV1,
2519) -> Result<Document, LoadError> {
2520 let doc = &gltf.document;
2521
2522 let nodes: Vec<gltf::Node> = doc.nodes().collect();
2523 let Topology {
2524 order,
2525 parent,
2526 bone_of_node,
2527 } = topo;
2528
2529 let mut bones: Vec<Bone> = Vec::with_capacity(nodes.len());
2530 for &node_index in order {
2531 let node = &nodes[node_index];
2532 let (t, r, s) = node.transform().decomposed();
2533 bones.push(Bone {
2534 name: node
2535 .name()
2536 .map(str::to_owned)
2537 .unwrap_or_else(|| format!("node{node_index}")),
2538 parent: parent[node_index].and_then(|p| bone_of_node[p]),
2539 rest: Transform {
2540 translation: Vec3::from_array(t),
2541 rotation: Quat::from_array(r),
2542 scale: Vec3::from_array(s),
2543 },
2544 inverse_bind: None,
2545 });
2546 }
2547
2548 for skin in doc.skins() {
2552 if skin
2557 .inverse_bind_matrices()
2558 .is_none_or(|accessor| accessor.count() == 0 || !inverse_bind_is_readable(&accessor))
2559 {
2560 continue;
2561 }
2562 let reader = skin.reader(|buffer| buffers.get(buffer.index()).map(Vec::as_slice));
2563 if let Some(ibms) = reader.read_inverse_bind_matrices() {
2564 for (joint, ibm) in skin.joints().zip(ibms) {
2565 if let Some(bone_id) = bone_of_node[joint.index()] {
2566 bones[bone_id].inverse_bind = Some(Mat4::from_cols_array_2d(&ibm));
2567 }
2568 }
2569 }
2570 }
2571
2572 let mut clips = Vec::new();
2574 let mut name_uses: BTreeMap<String, usize> = BTreeMap::new();
2575 let mut facts_complete = true;
2576 for animation in doc.animations() {
2577 let mut pending_facts = if facts_complete {
2578 PendingGltfClipFacts::begin(&animation, source_facts)
2579 } else {
2580 None
2581 };
2582 if pending_facts.is_none() {
2583 facts_complete = false;
2584 }
2585 let base_name = animation
2586 .name()
2587 .map(str::to_owned)
2588 .unwrap_or_else(|| format!("animation{}", animation.index()));
2589 let uses = name_uses.entry(base_name.clone()).or_insert(0);
2590 let name = if *uses == 0 {
2591 base_name.clone()
2592 } else {
2593 format!("{base_name}#{uses}")
2594 };
2595 *uses += 1;
2596
2597 let mut tracks = Vec::new();
2598 let mut duration = 0.0f64;
2599 for channel in animation.channels() {
2600 let Some(bone) = bone_of_node[channel.target().node().index()] else {
2601 continue;
2602 };
2603 let sampler = channel.sampler();
2612 let node = channel.target().node().index();
2613 if sampler.input().count() == 0 || sampler.output().count() == 0 {
2614 return Err(LoadError::Malformed(format!(
2615 "clip '{name}' node {node}: animation channel with zero keyframes"
2616 )));
2617 }
2618 check_sampler_accessor(&name, node, "input", &sampler.input())?;
2621 check_sampler_accessor(&name, node, "output", &sampler.output())?;
2622 let reader = channel.reader(|buffer| buffers.get(buffer.index()).map(Vec::as_slice));
2623 let times = reader.read_inputs().map(|it| it.collect::<Vec<f32>>());
2624 if let Some(pending) = pending_facts.as_mut()
2625 && !pending.truncated
2626 {
2627 pending.record_channel(&channel, times.as_deref());
2628 }
2629 let Some(times) = times else {
2630 continue;
2631 };
2632 let (property, values) = match reader.read_outputs() {
2633 Some(gltf::animation::util::ReadOutputs::Translations(it)) => (
2634 Property::Translation,
2635 TrackValues::Vec3s(it.map(Vec3::from_array).collect()),
2636 ),
2637 Some(gltf::animation::util::ReadOutputs::Rotations(r)) => (
2638 Property::Rotation,
2639 TrackValues::Quats(r.into_f32().map(Quat::from_array).collect()),
2640 ),
2641 Some(gltf::animation::util::ReadOutputs::Scales(it)) => (
2642 Property::Scale,
2643 TrackValues::Vec3s(it.map(Vec3::from_array).collect()),
2644 ),
2645 Some(gltf::animation::util::ReadOutputs::MorphTargetWeights(_)) | None => continue,
2648 };
2649 let interpolation = match channel.sampler().interpolation() {
2650 gltf::animation::Interpolation::Linear => Interpolation::Linear,
2651 gltf::animation::Interpolation::Step => Interpolation::Step,
2652 gltf::animation::Interpolation::CubicSpline => Interpolation::CubicSpline,
2653 };
2654 validate_track_lengths(&name, node, interpolation, ×, &values)?;
2655 duration = times
2656 .iter()
2657 .copied()
2658 .filter(|time| time.is_finite())
2659 .map(f64::from)
2660 .fold(duration, f64::max);
2661 tracks.push(Track {
2662 bone,
2663 property,
2664 interpolation,
2665 times,
2666 values,
2667 });
2668 }
2669 clips.push(Clip {
2670 name,
2671 duration_s: duration,
2672 tracks,
2673 });
2674 if let Some(pending) = pending_facts {
2675 let truncated = pending.truncated;
2676 if !source_facts.push_clip(pending.finish()?) || truncated {
2677 facts_complete = false;
2678 }
2679 }
2680 }
2681 if facts_complete {
2682 source_facts.mark_complete(SourceFactDomainV1::Clips);
2683 }
2684
2685 Ok(Document {
2686 skeleton: Skeleton { bones },
2687 clips,
2688 assets: SceneAssets::default(),
2691 source: SourceInfo {
2692 path: Some(path.display().to_string()),
2693 format: Some("gltf".into()),
2694 },
2695 })
2696}
2697
2698struct Topology {
2702 order: Vec<usize>,
2705 parent: Vec<Option<usize>>,
2708 bone_of_node: Vec<Option<usize>>,
2712}
2713
2714fn topology(doc: &gltf::Document) -> Result<Topology, LoadError> {
2743 let node_count = doc.nodes().count();
2744 let mut parent_refs: Vec<u32> = vec![0; node_count];
2752 for node in doc.nodes() {
2753 for child in node.children() {
2754 let refs = &mut parent_refs[child.index()];
2755 *refs = refs.saturating_add(1);
2756 }
2757 }
2758 if let Some(dup) = parent_refs.iter().position(|&refs| refs > 1) {
2759 return Err(LoadError::Topology(format!(
2760 "node {dup} is a child of {} nodes; glTF requires a forest (one parent per node)",
2761 parent_refs[dup]
2762 )));
2763 }
2764
2765 let nodes: Vec<gltf::Node> = doc.nodes().collect();
2766 let mut order: Vec<usize> = Vec::with_capacity(node_count);
2767 let mut parent: Vec<Option<usize>> = vec![None; node_count];
2768 let mut stack: Vec<usize> = doc
2769 .nodes()
2770 .filter(|n| parent_refs[n.index()] == 0)
2771 .map(|n| n.index())
2772 .collect();
2773 stack.reverse(); let mut visited: Vec<bool> = vec![false; node_count];
2784 while let Some(i) = stack.pop() {
2785 if visited[i] {
2786 continue;
2787 }
2788 visited[i] = true;
2789 order.push(i);
2790 let children: Vec<usize> = nodes[i].children().map(|c| c.index()).collect();
2791 for &c in children.iter().rev() {
2792 parent[c] = Some(i);
2793 stack.push(c);
2794 }
2795 }
2796
2797 if order.len() != node_count {
2803 let orphan = (0..node_count).find(|&n| !visited[n]).unwrap();
2804 return Err(LoadError::Topology(format!(
2805 "node {orphan} is unreachable from any root; the node graph contains a cycle"
2806 )));
2807 }
2808
2809 let mut bone_of_node: Vec<Option<usize>> = vec![None; node_count];
2810 for (bone_id, &node_index) in order.iter().enumerate() {
2811 bone_of_node[node_index] = Some(bone_id);
2812 }
2813 Ok(Topology {
2814 order,
2815 parent,
2816 bone_of_node,
2817 })
2818}
2819
2820fn extract_source_skeleton(
2829 doc: &gltf::Document,
2830 buffers: &[Vec<u8>],
2831 topo: &Topology,
2832) -> SourceSkeletonAssets {
2833 let mut scene_root_indices = vec![Vec::new(); doc.nodes().count()];
2834 for scene in doc.scenes() {
2835 for root in scene.nodes() {
2836 if let Some(indices) = scene_root_indices.get_mut(root.index()) {
2837 indices.push(scene.index());
2838 }
2839 }
2840 }
2841 for indices in &mut scene_root_indices {
2842 indices.sort_unstable();
2843 indices.dedup();
2844 }
2845 let mut attachments = vec![Vec::new(); doc.skins().count()];
2846 for node in doc.nodes() {
2847 let Some(skin) = node.skin() else {
2848 continue;
2849 };
2850 let Some(for_skin) = attachments.get_mut(skin.index()) else {
2851 return SourceSkeletonAssets::default();
2852 };
2853 for_skin.push(SourceSkinAttachment {
2854 source_node_index: node.index(),
2855 source_mesh_index: node.mesh().map(|mesh| mesh.index()),
2856 });
2857 }
2858
2859 let mut nodes = Vec::with_capacity(doc.nodes().count());
2860 for node in doc.nodes() {
2861 let local_rest = match node.transform() {
2862 gltf::scene::Transform::Decomposed {
2863 translation,
2864 rotation,
2865 scale,
2866 } => SourceNodeLocalRest::Trs {
2867 translation: Vec3::from_array(translation),
2868 rotation: Quat::from_array(rotation),
2869 scale: Vec3::from_array(scale),
2870 },
2871 gltf::scene::Transform::Matrix { matrix } => {
2872 SourceNodeLocalRest::Matrix(Mat4::from_cols_array_2d(&matrix))
2873 }
2874 };
2875 let mut source_node = SourceNodeAsset::new(node.index(), local_rest);
2876 source_node.name = node.name().map(str::to_owned);
2877 source_node.parent_source_node_index = topo.parent[node.index()];
2878 source_node.scene_root_indices = std::mem::take(&mut scene_root_indices[node.index()]);
2879 source_node.bone = topo.bone_of_node[node.index()];
2880 nodes.push(source_node);
2881 }
2882
2883 let mut skins = Vec::with_capacity(doc.skins().count());
2884 for skin in doc.skins() {
2885 let joints = skin.joints().map(|joint| joint.index()).collect::<Vec<_>>();
2886 let skeleton_root = skin.skeleton().map(|node| node.index());
2887 let inverse_bind_accessor = match skin.inverse_bind_matrices() {
2888 None => SourceInverseBindAccessor::default(),
2889 Some(accessor) if accessor.count() == 0 => SourceInverseBindAccessor {
2890 status: SourceInverseBindAccessorStatus::EmptyAccessor,
2891 declared_count: Some(0),
2892 matrices: Vec::new(),
2893 },
2894 Some(accessor) if !inverse_bind_is_readable(&accessor) => SourceInverseBindAccessor {
2898 status: SourceInverseBindAccessorStatus::Unreadable,
2899 declared_count: Some(accessor.count()),
2900 matrices: Vec::new(),
2901 },
2902 Some(accessor) => {
2903 let declared_count = accessor.count();
2904 let reader = skin.reader(|buffer| buffers.get(buffer.index()).map(Vec::as_slice));
2905 match reader.read_inverse_bind_matrices() {
2906 Some(matrices) => {
2907 let matrices = matrices
2908 .map(|matrix| Mat4::from_cols_array_2d(&matrix))
2909 .collect::<Vec<_>>();
2910 SourceInverseBindAccessor {
2911 status: if matrices.len() >= joints.len() {
2912 SourceInverseBindAccessorStatus::Available
2913 } else {
2914 SourceInverseBindAccessorStatus::CountMismatch
2915 },
2916 declared_count: Some(declared_count),
2917 matrices,
2918 }
2919 }
2920 None => SourceInverseBindAccessor {
2921 status: SourceInverseBindAccessorStatus::Unreadable,
2922 declared_count: Some(declared_count),
2923 matrices: Vec::new(),
2924 },
2925 }
2926 }
2927 };
2928 skins.push(SourceSkinAsset {
2929 source_skin_index: skin.index(),
2930 name: skin.name().map(str::to_owned),
2931 skeleton_root_source_node_index: skeleton_root,
2932 joint_source_node_indices: joints,
2933 inverse_bind_accessor,
2934 attachments: std::mem::take(&mut attachments[skin.index()]),
2935 });
2936 }
2937
2938 SourceSkeletonAssets {
2939 coverage: SourceSkeletonCoverage::Complete,
2940 nodes,
2941 skins,
2942 }
2943}
2944
2945fn extract_assets(
2955 doc: &gltf::Document,
2956 buffers: &[Vec<u8>],
2957 resources: &mut ResourceCaptureSession,
2958 bone_of_node: &[Option<usize>],
2959) -> SceneAssets {
2960 let mut assets = SceneAssets::default();
2961
2962 let source_images = extract_source_images(doc, buffers, resources);
2963 let (raw_images, source_image_records): (Vec<_>, Vec<_>) = source_images
2964 .into_iter()
2965 .map(|image| (image.texture, image.record))
2966 .unzip();
2967 assets.material_resources = MaterialResourceAssets {
2968 coverage: MaterialResourceCoverage::Complete,
2969 materials: Vec::new(),
2970 textures: extract_source_textures(doc),
2971 images: source_image_records,
2972 };
2973
2974 for material in doc.materials() {
2979 let Some(material_index) = material.index() else {
2980 continue;
2981 };
2982 let pbr = material.pbr_metallic_roughness();
2983 assets
2984 .material_resources
2985 .materials
2986 .push(SourceMaterialAsset {
2987 material_index,
2988 name: material.name().map(str::to_owned),
2989 texture_bindings: source_material_texture_bindings(&material),
2990 });
2991 let base_color_texture = pbr.base_color_texture().and_then(|info| {
2992 material_texture(&raw_images, info.texture().source().index(), resources)
2993 });
2994 let normal_texture = material.normal_texture().and_then(|info| {
2995 material_texture(&raw_images, info.texture().source().index(), resources).map(
2996 |texture| NormalTextureAsset {
2997 texture,
2998 scale: info.scale(),
2999 },
3000 )
3001 });
3002 let metallic_roughness_texture = pbr.metallic_roughness_texture().and_then(|info| {
3003 material_texture(&raw_images, info.texture().source().index(), resources)
3004 });
3005 let occlusion_texture = material.occlusion_texture().and_then(|info| {
3006 material_texture(&raw_images, info.texture().source().index(), resources).map(
3007 |texture| OcclusionTextureAsset {
3008 texture,
3009 strength: info.strength(),
3010 },
3011 )
3012 });
3013 assets.materials.push(MaterialAsset {
3014 name: material.name().unwrap_or("material").to_string(),
3015 base_color: pbr.base_color_factor(),
3016 metallic: pbr.metallic_factor(),
3017 roughness: pbr.roughness_factor(),
3018 base_color_texture,
3019 normal_texture,
3020 metallic_roughness_texture,
3021 occlusion_texture,
3022 });
3023 }
3024
3025 let mut core_mesh_of_source = vec![None; doc.meshes().count()];
3029 for mesh in doc.meshes() {
3030 let mut primitives = Vec::new();
3031 for prim in mesh.primitives() {
3032 if prim.mode() != gltf::mesh::Mode::Triangles {
3039 continue;
3040 }
3041 let reader = prim.reader(|b| buffers.get(b.index()).map(Vec::as_slice));
3049 let has = |sem: gltf::Semantic| prim.get(&sem).is_some_and(|a| a.count() > 0);
3055 if !has(gltf::Semantic::Positions) {
3056 continue;
3057 }
3058 let positions: Vec<Vec3> = reader
3059 .read_positions()
3060 .map(|it| it.map(Vec3::from_array).collect())
3061 .unwrap_or_default();
3062 let normals = if has(gltf::Semantic::Normals) {
3063 reader
3064 .read_normals()
3065 .map(|it| it.map(Vec3::from_array).collect())
3066 .unwrap_or_default()
3067 } else {
3068 Vec::new()
3069 };
3070 let uvs = if has(gltf::Semantic::TexCoords(0)) {
3071 reader
3072 .read_tex_coords(0)
3073 .map(|tc| tc.into_f32().collect())
3074 .unwrap_or_default()
3075 } else {
3076 Vec::new()
3077 };
3078 let (joints, weights) =
3080 if has(gltf::Semantic::Joints(0)) && has(gltf::Semantic::Weights(0)) {
3081 match (reader.read_joints(0), reader.read_weights(0)) {
3082 (Some(j), Some(w)) => (j.into_u16().collect(), w.into_f32().collect()),
3083 _ => (Vec::new(), Vec::new()),
3084 }
3085 } else {
3086 (Vec::new(), Vec::new())
3087 };
3088 let mut additional_influence_sets: BTreeMap<u32, AdditionalInfluenceSet> =
3094 BTreeMap::new();
3095 for (semantic, accessor) in prim.attributes() {
3096 if accessor.count() == 0 {
3097 continue;
3098 }
3099 match semantic {
3100 gltf::Semantic::Joints(set) if set >= 1 => {
3101 additional_influence_sets
3102 .entry(set)
3103 .and_modify(|entry| entry.joints_present = true)
3104 .or_insert(AdditionalInfluenceSet {
3105 set_index: set,
3106 joints_present: true,
3107 weights_present: false,
3108 });
3109 }
3110 gltf::Semantic::Weights(set) if set >= 1 => {
3111 additional_influence_sets
3112 .entry(set)
3113 .and_modify(|entry| entry.weights_present = true)
3114 .or_insert(AdditionalInfluenceSet {
3115 set_index: set,
3116 joints_present: false,
3117 weights_present: true,
3118 });
3119 }
3120 _ => {}
3121 }
3122 }
3123 let indices = if prim.indices().is_some_and(|a| a.count() > 0) {
3124 reader
3125 .read_indices()
3126 .map(|it| it.into_u32().collect())
3127 .unwrap_or_default()
3128 } else {
3129 Vec::new()
3130 };
3131 primitives.push(Primitive {
3132 material: prim.material().index(),
3133 indices,
3134 positions,
3135 normals,
3136 uvs,
3137 joints,
3138 weights,
3139 additional_influence_sets: additional_influence_sets.into_values().collect(),
3140 });
3141 }
3142 if primitives.is_empty() {
3143 continue;
3144 }
3145 let core_mesh = assets.meshes.len();
3146 core_mesh_of_source[mesh.index()] = Some(core_mesh);
3147 assets.meshes.push(MeshAsset {
3148 name: mesh.name().unwrap_or("mesh").to_string(),
3149 source_mesh_index: mesh.index(),
3150 primitives,
3151 });
3152 }
3153
3154 for node in doc.nodes() {
3155 let Some(source_mesh) = node.mesh() else {
3156 continue;
3157 };
3158 let Some(mesh) = core_mesh_of_source[source_mesh.index()] else {
3159 continue;
3160 };
3161 let skin = node.skin();
3162 let skin_joints = skin
3163 .as_ref()
3164 .map(|skin| {
3165 skin.joints()
3166 .map(|joint| bone_of_node[joint.index()].unwrap_or(0))
3167 .collect()
3168 })
3169 .unwrap_or_default();
3170 let skin_ibms = skin
3171 .as_ref()
3172 .filter(|skin| {
3173 skin.inverse_bind_matrices().is_some_and(|accessor| {
3174 accessor.count() > 0 && inverse_bind_is_readable(&accessor)
3175 })
3176 })
3177 .map(|skin| {
3178 let reader = skin.reader(|buffer| buffers.get(buffer.index()).map(Vec::as_slice));
3179 reader
3180 .read_inverse_bind_matrices()
3181 .map(|matrices| {
3182 matrices
3183 .map(|matrix| Mat4::from_cols_array_2d(&matrix))
3184 .collect()
3185 })
3186 .unwrap_or_default()
3187 })
3188 .unwrap_or_default();
3189 assets.instances.push(MeshInstance {
3190 source_node_index: node.index(),
3191 node: bone_of_node[node.index()].unwrap_or(0),
3192 mesh,
3193 skin_joints,
3194 skin_ibms,
3195 });
3196 }
3197
3198 assets
3199}
3200
3201fn material_texture(
3202 raw_images: &[Option<TextureAsset>],
3203 image_index: usize,
3204 resources: &mut ResourceCaptureSession,
3205) -> Option<TextureAsset> {
3206 let texture = raw_images.get(image_index)?.as_ref()?;
3207 resources.clone_image_for_material(image_index, texture)
3208}
3209
3210fn extract_scenes(doc: &gltf::Document, bone_of_node: &[Option<usize>]) -> Vec<SceneAsset> {
3214 doc.scenes()
3215 .map(|scene| SceneAsset {
3216 source_scene_index: scene.index(),
3217 name: scene.name().map(str::to_owned),
3218 roots: scene
3219 .nodes()
3220 .filter_map(|node| bone_of_node[node.index()])
3221 .collect(),
3222 })
3223 .collect()
3224}
3225
3226const MAX_IMAGE_ENCODED_BYTES: usize = 64 * 1024 * 1024;
3228const MAX_IMAGE_DECODE_ALLOC_BYTES: u64 = 192 * 1024 * 1024;
3230
3231struct LoadedSourceImage {
3235 record: SourceImageAsset,
3236 texture: Option<TextureAsset>,
3237}
3238
3239fn extract_source_images(
3243 doc: &gltf::Document,
3244 buffers: &[Vec<u8>],
3245 resources: &mut ResourceCaptureSession,
3246) -> Vec<LoadedSourceImage> {
3247 let writer_images = writer_image_indices(doc);
3248 doc.images()
3249 .map(|image| {
3250 let image_index = image.index();
3251 let retain_raw = writer_images.contains(&image_index);
3252 let name = image.name().map(str::to_owned);
3253 let (source_kind, declared_mime_type, raw, unavailable_reason, inspected) =
3254 match image.source() {
3255 gltf::image::Source::View { view, mime_type } => {
3256 let bytes = buffers.get(view.buffer().index()).and_then(|buffer| {
3257 view_end(&view).and_then(|end| buffer.get(view.offset()..end))
3261 });
3262 let (raw, reason) = match bytes {
3263 Some(bytes) if !retain_raw && bytes.len() > MAX_IMAGE_ENCODED_BYTES => {
3264 (None, ImageUnavailableReason::ResourceLimit)
3265 }
3266 Some(bytes) => (
3267 Some(TextureAsset {
3268 bytes: bytes.to_vec(),
3269 mime: mime_type.to_string(),
3270 }),
3271 ImageUnavailableReason::SourceUnavailable,
3272 ),
3273 None => (None, ImageUnavailableReason::SourceUnavailable),
3274 };
3275 (
3276 ImageSourceKind::Embedded,
3277 Some(mime_type.to_string()),
3278 raw,
3279 reason,
3280 None,
3281 )
3282 }
3283 gltf::image::Source::Uri { uri, mime_type } => {
3284 if let Some(encoded) = uri.strip_prefix("data:") {
3285 let (mime_from_uri, raw, reason) =
3286 read_data_uri_image(encoded, mime_type, retain_raw);
3287 (
3288 ImageSourceKind::DataUri,
3289 mime_type.map(str::to_owned).or(mime_from_uri),
3290 raw,
3291 reason,
3292 None,
3293 )
3294 } else {
3295 let (detected_container, inspection) = {
3296 let (bytes, reason) = resources.external_image_payload(image_index);
3297 inspect_source_image(bytes, reason)
3298 };
3299 let raw = retain_raw.then(|| {
3300 resources
3301 .materialize_external(
3302 SourceResourceKindV1::Image,
3303 image_index as u64,
3304 )
3305 .ok()
3306 .map(|bytes| TextureAsset {
3307 bytes,
3308 mime: mime_type.unwrap_or_default().to_owned(),
3309 })
3310 });
3311 let raw = raw.flatten();
3312 let materialization_limited = retain_raw
3313 && raw.is_none()
3314 && resources.external_image_is_available(image_index);
3315 (
3316 ImageSourceKind::External,
3317 mime_type.map(str::to_owned),
3318 raw,
3319 if materialization_limited {
3320 ImageUnavailableReason::ResourceLimit
3321 } else {
3322 ImageUnavailableReason::SourceUnavailable
3323 },
3324 Some(if materialization_limited {
3325 (
3326 None,
3327 SourceImageInspection::Unavailable {
3328 reason: ImageUnavailableReason::ResourceLimit,
3329 },
3330 )
3331 } else {
3332 (detected_container, inspection)
3333 }),
3334 )
3335 }
3336 }
3337 };
3338 let (detected_container, inspection) = inspected.unwrap_or_else(|| {
3339 inspect_source_image(
3340 raw.as_ref().map(|texture| texture.bytes.as_slice()),
3341 unavailable_reason,
3342 )
3343 });
3344 LoadedSourceImage {
3345 record: SourceImageAsset {
3346 image_index,
3347 name,
3348 source_kind,
3349 declared_mime_type,
3350 detected_container,
3351 inspection,
3352 },
3353 texture: if retain_raw { raw } else { None },
3354 }
3355 })
3356 .collect()
3357}
3358
3359fn writer_image_indices(doc: &gltf::Document) -> BTreeSet<usize> {
3363 let mut images = BTreeSet::new();
3364 for material in doc.materials() {
3365 let pbr = material.pbr_metallic_roughness();
3366 for texture in [
3367 pbr.base_color_texture().map(|info| info.texture()),
3368 material.normal_texture().map(|info| info.texture()),
3369 pbr.metallic_roughness_texture().map(|info| info.texture()),
3370 material.occlusion_texture().map(|info| info.texture()),
3371 ]
3372 .into_iter()
3373 .flatten()
3374 {
3375 images.insert(texture.source().index());
3376 }
3377 }
3378 images
3379}
3380
3381fn read_data_uri_image(
3385 encoded: &str,
3386 mime_type: Option<&str>,
3387 retain_raw: bool,
3388) -> (Option<String>, Option<TextureAsset>, ImageUnavailableReason) {
3389 let Some((metadata, payload)) = encoded.split_once(',') else {
3390 return (None, None, ImageUnavailableReason::InvalidDataUri);
3391 };
3392 if !metadata.ends_with(";base64") {
3393 return (None, None, ImageUnavailableReason::InvalidDataUri);
3394 }
3395 let mime_from_uri = metadata
3396 .strip_suffix(";base64")
3397 .filter(|mime| !mime.is_empty())
3398 .map(str::to_owned);
3399 if !retain_raw && estimated_base64_decoded_len(payload.len()) > MAX_IMAGE_ENCODED_BYTES {
3400 return (mime_from_uri, None, ImageUnavailableReason::ResourceLimit);
3401 }
3402 let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(payload) else {
3403 return (mime_from_uri, None, ImageUnavailableReason::InvalidDataUri);
3404 };
3405 if !retain_raw && bytes.len() > MAX_IMAGE_ENCODED_BYTES {
3406 return (mime_from_uri, None, ImageUnavailableReason::ResourceLimit);
3407 }
3408 let mime = mime_type
3409 .map(str::to_owned)
3410 .or_else(|| mime_from_uri.clone())
3411 .unwrap_or_default();
3412 (
3413 mime_from_uri,
3414 Some(TextureAsset { bytes, mime }),
3415 ImageUnavailableReason::InvalidDataUri,
3416 )
3417}
3418
3419fn estimated_base64_decoded_len(encoded_len: usize) -> usize {
3420 encoded_len.saturating_add(3) / 4 * 3
3421}
3422
3423fn source_material_texture_bindings(
3426 material: &gltf::Material<'_>,
3427) -> Vec<SourceMaterialTextureBinding> {
3428 let pbr = material.pbr_metallic_roughness();
3429 let mut texture_bindings = Vec::with_capacity(5);
3430 let mut push = |slot, texture: Option<gltf::Texture>| {
3431 if let Some(texture) = texture {
3432 texture_bindings.push(SourceMaterialTextureBinding {
3433 slot,
3434 texture_index: texture.index(),
3435 });
3436 }
3437 };
3438 push(
3439 MaterialTextureSlot::BaseColor,
3440 pbr.base_color_texture().map(|info| info.texture()),
3441 );
3442 push(
3443 MaterialTextureSlot::Normal,
3444 material.normal_texture().map(|info| info.texture()),
3445 );
3446 push(
3447 MaterialTextureSlot::MetallicRoughness,
3448 pbr.metallic_roughness_texture().map(|info| info.texture()),
3449 );
3450 push(
3451 MaterialTextureSlot::Occlusion,
3452 material.occlusion_texture().map(|info| info.texture()),
3453 );
3454 push(
3455 MaterialTextureSlot::Emissive,
3456 material.emissive_texture().map(|info| info.texture()),
3457 );
3458 texture_bindings
3459}
3460
3461fn extract_source_textures(doc: &gltf::Document) -> Vec<SourceTextureAsset> {
3464 doc.textures()
3465 .map(|texture| SourceTextureAsset {
3466 texture_index: texture.index(),
3467 name: texture.name().map(str::to_owned),
3468 image_index: texture.source().index(),
3469 })
3470 .collect()
3471}
3472
3473fn inspect_source_image(
3477 bytes: Option<&[u8]>,
3478 unavailable_reason: ImageUnavailableReason,
3479) -> (Option<ImageContainerFormat>, SourceImageInspection) {
3480 let Some(bytes) = bytes else {
3481 return (
3482 None,
3483 SourceImageInspection::Unavailable {
3484 reason: unavailable_reason,
3485 },
3486 );
3487 };
3488 if bytes.len() > MAX_IMAGE_ENCODED_BYTES {
3489 return (
3490 detect_container(bytes),
3491 SourceImageInspection::Unavailable {
3492 reason: ImageUnavailableReason::ResourceLimit,
3493 },
3494 );
3495 }
3496 let Some((format, detected_container)) = image_format(bytes) else {
3497 return (
3498 None,
3499 SourceImageInspection::Unavailable {
3500 reason: ImageUnavailableReason::UnsupportedContainer,
3501 },
3502 );
3503 };
3504 let mut reader = ImageReader::new(Cursor::new(bytes));
3505 reader.set_format(format);
3506 let mut limits = Limits::default();
3507 limits.max_alloc = Some(MAX_IMAGE_DECODE_ALLOC_BYTES);
3508 reader.limits(limits);
3509 match reader.decode() {
3510 Ok(decoded) => {
3511 let color_type = match decoded.color() {
3512 ColorType::L8 => Some(DecodedImageColorType::L8),
3513 ColorType::La8 => Some(DecodedImageColorType::La8),
3514 ColorType::Rgb8 => Some(DecodedImageColorType::Rgb8),
3515 ColorType::Rgba8 => Some(DecodedImageColorType::Rgba8),
3516 ColorType::L16 => Some(DecodedImageColorType::L16),
3517 ColorType::La16 => Some(DecodedImageColorType::La16),
3518 ColorType::Rgb16 => Some(DecodedImageColorType::Rgb16),
3519 ColorType::Rgba16 => Some(DecodedImageColorType::Rgba16),
3520 _ => None,
3521 };
3522 let (width, height) = (decoded.width(), decoded.height());
3523 match color_type {
3524 Some(color_type) => (
3525 Some(detected_container),
3526 SourceImageInspection::Available {
3527 width,
3528 height,
3529 channel_count: decoded.color().channel_count(),
3530 color_type,
3531 },
3532 ),
3533 None => (
3534 Some(detected_container),
3535 SourceImageInspection::Unavailable {
3536 reason: ImageUnavailableReason::DecodeFailed,
3537 },
3538 ),
3539 }
3540 }
3541 Err(ImageError::Limits(_)) => (
3542 Some(detected_container),
3543 SourceImageInspection::Unavailable {
3544 reason: ImageUnavailableReason::ResourceLimit,
3545 },
3546 ),
3547 Err(_) => (
3548 Some(detected_container),
3549 SourceImageInspection::Unavailable {
3550 reason: ImageUnavailableReason::DecodeFailed,
3551 },
3552 ),
3553 }
3554}
3555
3556fn image_format(bytes: &[u8]) -> Option<(ImageFormat, ImageContainerFormat)> {
3558 if bytes.starts_with(b"\x89PNG\r\n\x1a\n") {
3559 Some((ImageFormat::Png, ImageContainerFormat::Png))
3560 } else if bytes.starts_with(&[0xff, 0xd8, 0xff]) {
3561 Some((ImageFormat::Jpeg, ImageContainerFormat::Jpeg))
3562 } else {
3563 None
3564 }
3565}
3566
3567fn detect_container(bytes: &[u8]) -> Option<ImageContainerFormat> {
3569 image_format(bytes).map(|(_, container)| container)
3570}
3571
3572#[cfg(test)]
3573mod dependency_capture_tests {
3574 use super::*;
3575
3576 fn key() -> DependencyResourceKeyV1 {
3577 DependencyResourceKeyV1::from_source_str("shared.bin", ResourceKeySyntaxV1::GltfUri)
3578 .expect("safe test key")
3579 }
3580
3581 fn session_with_aliases(limit: u64) -> ResourceCaptureSession {
3582 let key = key();
3583 let mut session = ResourceCaptureSession::new(None);
3584 session.materialized_external_limit = limit;
3585 session
3586 .resources
3587 .insert(key.clone(), CapturedResource::Bytes(vec![1, 2]));
3588 for (kind, index) in [
3589 (SourceResourceKindV1::Buffer, 0),
3590 (SourceResourceKindV1::Buffer, 1),
3591 (SourceResourceKindV1::Image, 0),
3592 (SourceResourceKindV1::Image, 1),
3593 ] {
3594 session.insert_reference(kind, index, CapturedReference::External(key.clone()));
3595 }
3596 session
3597 }
3598
3599 #[test]
3600 fn recording_reader_binds_one_capture_to_the_digest_and_document() {
3601 let dir = tempfile::tempdir().expect("temp dir");
3602 let external_path = dir.path().join("shared.bin");
3603 std::fs::write(&external_path, [0_u8; 36]).expect("decoy external bytes");
3604
3605 let mut captured = Vec::new();
3606 for value in [0.0_f32, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 3.0, 0.0] {
3607 captured.extend_from_slice(&value.to_le_bytes());
3608 }
3609 let primary = serde_json::to_vec(&serde_json::json!({
3610 "asset": { "version": "2.0" },
3611 "buffers": [
3612 { "uri": "shared.bin", "byteLength": captured.len() },
3613 { "uri": "shared.bin", "byteLength": captured.len() }
3614 ],
3615 "bufferViews": [{ "buffer": 0, "byteOffset": 0, "byteLength": captured.len() }],
3616 "accessors": [{
3617 "bufferView": 0,
3618 "componentType": 5126,
3619 "count": 3,
3620 "type": "VEC3",
3621 "min": [0.0, 0.0, 0.0],
3622 "max": [2.0, 3.0, 0.0]
3623 }],
3624 "meshes": [{ "primitives": [{ "attributes": { "POSITION": 0 } }] }],
3625 "nodes": [{ "mesh": 0 }],
3626 "scenes": [{ "nodes": [0] }],
3627 "scene": 0
3628 }))
3629 .expect("analytic glTF JSON");
3630 let mut opens = 0;
3631 let loaded = load_source_bytes_inner_with_reader(
3632 &dir.path().join("recorded.gltf"),
3633 &primary,
3634 Some(dir.path()),
3635 |path, limit| {
3636 opens += 1;
3637 assert_eq!(path, external_path);
3638 assert!(limit >= captured.len() as u64);
3639 CapturedResource::Bytes(captured.clone())
3640 },
3641 )
3642 .expect("recorded external capture loads");
3643
3644 assert_eq!(opens, 1, "two aliases cause one resolver open");
3645 let closure = loaded.dependency_closure();
3646 assert!(closure.coverage().is_complete());
3647 assert_eq!(closure.references().len(), 2);
3648 assert_eq!(closure.external_resources().len(), 1);
3649 assert_eq!(
3650 closure.external_resources()[0].identity(),
3651 &InputIdentity::from_bytes(&captured),
3652 "the closure hashes the resolver-returned capture"
3653 );
3654 assert_eq!(
3655 loaded.document().assets.meshes[0].primitives[0].positions,
3656 [
3657 Vec3::new(0.0, 0.0, 0.0),
3658 Vec3::new(2.0, 0.0, 0.0),
3659 Vec3::new(0.0, 3.0, 0.0),
3660 ],
3661 "the Document consumes the recorded capture, not the on-disk decoy"
3662 );
3663 }
3664
3665 #[test]
3666 fn raw_extension_key_scan_handles_nesting_escaping_and_non_key_text() {
3667 assert!(json_has_object_key(
3668 br#"{"meshes":[{"primitives":[{"extensio\u006es":{"X":{}}}]}]}"#,
3669 b"extensions"
3670 ));
3671 assert!(!json_has_object_key(
3672 br#"{"extras":{"label":"extensions","note":"\"extensions\":"}}"#,
3673 b"extensions"
3674 ));
3675 }
3676
3677 #[test]
3678 fn materialization_cap_refuses_essential_buffer_alias_without_leaking_a_path() {
3679 let gltf = gltf::Gltf::from_slice(
3680 br#"{
3681 "asset":{"version":"2.0"},
3682 "buffers":[
3683 {"uri":"shared.bin","byteLength":2},
3684 {"uri":"shared.bin","byteLength":2}
3685 ]
3686 }"#,
3687 )
3688 .expect("test glTF");
3689 let mut session = session_with_aliases(3);
3690 let error = resolve_captured_buffers(&gltf, &mut session)
3691 .expect_err("second essential clone exceeds the internal cap");
3692 assert!(
3693 error
3694 .to_string()
3695 .contains("external buffer resource exceeds capture limits"),
3696 "{error}"
3697 );
3698 }
3699
3700 #[test]
3701 fn materialization_cap_omits_optional_image_alias_while_its_capture_stays_reusable() {
3702 let gltf = gltf::Gltf::from_slice(
3703 br#"{
3704 "asset":{"version":"2.0"},
3705 "images":[
3706 {"uri":"shared.bin"},
3707 {"uri":"shared.bin"}
3708 ],
3709 "textures":[{"source":0},{"source":1}],
3710 "materials":[
3711 {"pbrMetallicRoughness":{"baseColorTexture":{"index":0}}},
3712 {"pbrMetallicRoughness":{"baseColorTexture":{"index":1}}}
3713 ]
3714 }"#,
3715 )
3716 .expect("test glTF");
3717 let mut session = session_with_aliases(3);
3718 let images = extract_source_images(&gltf.document, &[], &mut session);
3719 assert!(images[0].texture.is_some());
3720 assert!(images[1].texture.is_none());
3721 assert!(matches!(
3722 images[1].record.inspection,
3723 SourceImageInspection::Unavailable {
3724 reason: ImageUnavailableReason::ResourceLimit
3725 }
3726 ));
3727 assert!(session.external_image_is_available(1));
3728 }
3729}