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