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