1use crate::{
9 LoadError, build_document, capture_dependency_closure, extract_source_skeleton,
10 has_extension_object, project_extension_facts, project_resource_facts, resolve_buffers,
11 source_facts_builder, topology, validate_animations, validate_document, validate_glb_framing,
12};
13use animsmith_core::{
14 Document, LoadedSource, RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS, RawMeshPrimitiveRowV1,
15 RawMeshPrimitiveRowsV1, RawNodeMeshAttachmentRowV1, RawNodeMeshAttachmentRowsV1,
16 RawPrimitiveTopologyV1, RawSceneAttachmentCoverageV1, RawSceneAttachmentInventoryV1,
17 RawSceneRootRowV1, RawSceneRootRowsV1, RawSourceSkeletonEvidenceV1, SourceFactsViewV1,
18 SourceSetCoverageStateV1, SourceSkeletonCoverage,
19};
20use serde::Serialize;
21use serde_json::{Map, Value};
22use std::collections::{BTreeMap, BTreeSet};
23use std::path::Path;
24
25const GLB_MAGIC: &[u8; 4] = b"glTF";
26const GLB_JSON_CHUNK: u32 = 0x4e4f_534a;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
30#[serde(rename_all = "snake_case")]
31pub enum GltfContainerKind {
32 Gltf,
34 Glb,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
40#[serde(rename_all = "snake_case")]
41pub enum GltfBufferSourceKind {
42 BinaryChunk,
44 DataUri,
46 External,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
52pub struct GltfBufferCapability {
53 pub buffer_index: usize,
55 pub source_kind: GltfBufferSourceKind,
57 pub declared_byte_length: u64,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
63#[serde(rename_all = "snake_case")]
64pub enum GltfNodeRestKind {
65 Trs,
67 Matrix,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
73pub struct GltfNodeCapability {
74 pub node_index: usize,
76 pub rest_kind: GltfNodeRestKind,
78 pub mesh_index: Option<usize>,
80 pub skin_index: Option<usize>,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
86pub struct GltfAnimationChannelCapability {
87 pub animation_index: usize,
89 pub channel_index: usize,
91 pub target_node_index: usize,
93 pub target_path: String,
95 pub interpolation: String,
97 pub input_accessor_index: usize,
99 pub output_accessor_index: usize,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
105pub struct GltfAttributeCapability {
106 pub semantic: String,
108 pub accessor_index: usize,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
114pub struct GltfPrimitiveCapability {
115 pub mesh_index: usize,
117 pub primitive_index: usize,
119 pub mode: u64,
121 pub attributes: Vec<GltfAttributeCapability>,
123 pub morph_target_count: usize,
125 pub morph_position_accessors: Vec<usize>,
127 #[serde(skip)]
129 pub unsupported_morph_locations: Vec<String>,
130}
131
132#[derive(Debug, Default)]
134struct RawSceneAttachmentParts {
135 scenes: Vec<RawSceneRootRowV1>,
136 scene_overflow: bool,
137 attachments: Vec<RawNodeMeshAttachmentRowV1>,
138 attachment_overflow: bool,
139 primitives: Vec<RawMeshPrimitiveRowV1>,
140 primitive_overflow: bool,
141 #[cfg(test)]
142 primitive_decode_attempts: usize,
143}
144
145impl RawSceneAttachmentParts {
146 fn retain_scene(&mut self, scene_index: u64, nodes: &[Value]) {
147 if self.scene_overflow {
148 return;
149 }
150 let Some(cost) = nodes.len().checked_add(1) else {
151 self.scene_overflow = true;
152 return;
153 };
154 if self
155 .retained_rows()
156 .checked_add(cost)
157 .is_none_or(|total| total > RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS)
158 {
159 self.scene_overflow = true;
160 return;
161 }
162 let roots = nodes
163 .iter()
164 .filter_map(|node| as_index(Some(node)).map(|index| index as u64))
165 .collect();
166 self.scenes.push(RawSceneRootRowV1::new(scene_index, roots));
167 }
168
169 fn retain_attachment(&mut self, row: RawNodeMeshAttachmentRowV1) {
170 if self.attachment_overflow {
171 return;
172 }
173 if self.retained_rows() < RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS {
174 self.attachments.push(row);
175 } else {
176 self.attachment_overflow = true;
177 }
178 }
179
180 fn retain_primitive(&mut self, row: RawMeshPrimitiveRowV1) {
181 #[cfg(test)]
182 {
183 self.primitive_decode_attempts = self.primitive_decode_attempts.saturating_add(1);
184 }
185 if self.primitive_overflow {
186 return;
187 }
188 if self.retained_rows() < RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS {
189 self.primitives.push(row);
190 } else {
191 self.primitive_overflow = true;
192 }
193 }
194
195 fn retained_rows(&self) -> usize {
196 self.scenes
197 .iter()
198 .fold(0usize, |total, row| {
199 total
200 .saturating_add(1)
201 .saturating_add(row.root_node_indices().len())
202 })
203 .saturating_add(self.attachments.len())
204 .saturating_add(self.primitives.len())
205 }
206
207 fn inventory(self, source: &LoadedSource) -> Result<RawSceneAttachmentInventoryV1, LoadError> {
208 let facts = source.source_facts();
209 let skeleton = facts.source_skeleton();
210 let skeleton_coverage = match skeleton.coverage {
211 SourceSkeletonCoverage::Complete => RawSceneAttachmentCoverageV1::Complete,
212 SourceSkeletonCoverage::Unavailable => RawSceneAttachmentCoverageV1::Unavailable,
213 };
214 RawSceneAttachmentInventoryV1::new(
215 facts.primary_identity().clone(),
216 RawSourceSkeletonEvidenceV1::new(
217 skeleton_coverage,
218 skeleton.nodes.len() as u64,
219 skeleton.skins.len() as u64,
220 ),
221 RawSceneRootRowsV1::new(coverage(self.scene_overflow), self.scenes),
222 RawNodeMeshAttachmentRowsV1::new(coverage(self.attachment_overflow), self.attachments),
223 RawMeshPrimitiveRowsV1::new(coverage(self.primitive_overflow), self.primitives),
224 )
225 .map_err(|error| LoadError::Malformed(format!("raw scene/attachment inventory: {error}")))
226 }
227}
228
229fn coverage(overflow: bool) -> RawSceneAttachmentCoverageV1 {
230 if overflow {
231 RawSceneAttachmentCoverageV1::PrefixOverflow
232 } else {
233 RawSceneAttachmentCoverageV1::Complete
234 }
235}
236
237#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
239pub struct GltfInstancingCapability {
240 pub node_index: usize,
242 pub attributes: Vec<GltfAttributeCapability>,
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
248pub struct GltfAccessorCapability {
249 pub accessor_index: usize,
251 pub buffer_view_index: Option<usize>,
253 pub byte_offset: u64,
255 pub component_type: u64,
257 pub accessor_type: String,
259 pub count: u64,
261 pub normalized: bool,
263 pub sparse: bool,
265}
266
267#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
269pub struct GltfBufferViewCapability {
270 pub buffer_view_index: usize,
272 pub buffer_index: usize,
274 pub byte_offset: u64,
276 pub byte_length: u64,
278 pub byte_stride: Option<u64>,
280}
281
282#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
284pub struct GltfSkinCapability {
285 pub skin_index: usize,
287 pub joint_count: usize,
289 pub inverse_bind_accessor_index: Option<usize>,
291 pub inverse_bind_count: Option<u64>,
293}
294
295#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
297pub struct GltfCapabilityManifest {
298 pub container: GltfContainerKind,
300 pub buffers: Vec<GltfBufferCapability>,
302 pub buffer_views: Vec<GltfBufferViewCapability>,
304 pub accessors: Vec<GltfAccessorCapability>,
306 pub nodes: Vec<GltfNodeCapability>,
308 pub animation_channels: Vec<GltfAnimationChannelCapability>,
310 pub primitives: Vec<GltfPrimitiveCapability>,
312 pub morph_weight_locations: Vec<String>,
314 pub instancing: Vec<GltfInstancingCapability>,
316 pub skins: Vec<GltfSkinCapability>,
318 pub camera_count: usize,
320 pub extensions: Vec<String>,
322 pub extension_locations: Vec<String>,
324 pub external_resource_locations: Vec<String>,
326 pub extras_locations: Vec<String>,
328 pub unknown_member_locations: Vec<String>,
330}
331
332#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
334#[non_exhaustive]
335#[serde(rename_all = "snake_case")]
336pub enum GltfCapabilityViolationKind {
337 ExternalResource,
339 MorphTarget,
341 MorphWeights,
343 Camera,
345 Light,
347 Instancing,
349 ExtensionDeclaration,
351 ExtensionPayload,
353 Extras,
355 UnknownJsonMember,
357 NonTrianglePrimitive,
359 UnsupportedVertexAttribute,
361 SecondarySkinInfluences,
363 MissingInverseBinds,
365 EmptyInverseBindAccessor,
367 InverseBindCountMismatch,
369 UnreadableInverseBinds,
371 UnsafeAccessorLayout,
373 ConflictingAccessorUse,
375 OverlappingAccessorRanges,
379 ConflictingNodeTransform,
382 NonAffineNodeMatrix,
385 AnimatedMatrixNode,
389 ImagePayloadOverlap,
391}
392
393#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
395pub struct GltfCapabilityViolation {
396 pub location: String,
398 pub kind: GltfCapabilityViolationKind,
400}
401
402#[derive(Debug)]
407pub struct GltfScaleSource {
408 loaded_source: LoadedSource,
409 #[cfg(test)]
410 document_override: Option<Document>,
411 manifest: GltfCapabilityManifest,
412 source_bytes: Vec<u8>,
413 raw_json: Value,
414 resolved_buffers: Vec<Vec<u8>>,
415 clip_track_projection_required: bool,
416}
417
418impl GltfScaleSource {
419 pub fn document(&self) -> &Document {
421 #[cfg(test)]
422 if let Some(document) = self.document_override.as_ref() {
423 return document;
424 }
425 self.loaded_source.document()
426 }
427
428 pub fn source_facts(&self) -> SourceFactsViewV1<'_> {
430 self.loaded_source.source_facts()
431 }
432
433 pub fn manifest(&self) -> &GltfCapabilityManifest {
435 &self.manifest
436 }
437
438 pub fn source_bytes(&self) -> &[u8] {
440 &self.source_bytes
441 }
442
443 pub fn raw_json(&self) -> &Value {
445 &self.raw_json
446 }
447
448 pub fn resolved_buffers(&self) -> &[Vec<u8>] {
450 &self.resolved_buffers
451 }
452
453 pub const fn requires_clip_track_projection(&self) -> bool {
459 self.clip_track_projection_required
460 }
461}
462
463#[derive(Debug, thiserror::Error)]
465#[non_exhaustive]
466pub enum GltfScalePreflightError {
467 #[error(transparent)]
469 Load(#[from] LoadError),
470 #[error("glTF scale preflight rejected {count} unsupported source domain(s)")]
472 Unsupported {
473 manifest: Box<GltfCapabilityManifest>,
475 violations: Vec<GltfCapabilityViolation>,
477 count: usize,
479 },
480}
481
482pub fn preflight_scale_source(path: &Path) -> Result<GltfScaleSource, GltfScalePreflightError> {
490 let bytes = std::fs::read(path).map_err(|source| LoadError::Io {
491 path: path.display().to_string(),
492 source,
493 })?;
494 preflight_scale_source_bytes(path, &bytes)
495}
496
497pub fn preflight_scale_source_bytes(
508 path: &Path,
509 bytes: &[u8],
510) -> Result<GltfScaleSource, GltfScalePreflightError> {
511 capture_scale_source(path, bytes, GatePolicy::Enforce)
512}
513
514pub fn preflight_clip_track_source(
536 path: &Path,
537) -> Result<GltfScaleSource, GltfScalePreflightError> {
538 let bytes = std::fs::read(path).map_err(|source| LoadError::Io {
539 path: path.display().to_string(),
540 source,
541 })?;
542 preflight_clip_track_source_bytes(path, &bytes)
543}
544
545pub fn preflight_clip_track_source_bytes(
554 path: &Path,
555 bytes: &[u8],
556) -> Result<GltfScaleSource, GltfScalePreflightError> {
557 capture_source(path, bytes, CapturePolicy::ClipTracks)
558}
559
560#[derive(Debug, Clone, Copy, PartialEq, Eq)]
562enum GatePolicy {
563 Enforce,
566 #[cfg(test)]
579 Bypass,
580}
581
582fn capture_scale_source(
584 path: &Path,
585 bytes: &[u8],
586 policy: GatePolicy,
587) -> Result<GltfScaleSource, GltfScalePreflightError> {
588 capture_source(path, bytes, CapturePolicy::Scale(policy))
589}
590
591#[derive(Debug, Clone, Copy, PartialEq, Eq)]
593enum CapturePolicy {
594 Scale(GatePolicy),
596 ClipTracks,
598}
599
600fn capture_source(
603 path: &Path,
604 bytes: &[u8],
605 policy: CapturePolicy,
606) -> Result<GltfScaleSource, GltfScalePreflightError> {
607 validate_glb_framing(bytes)?;
608 let (container, json_bytes) = raw_json_bytes(bytes)?;
609 let raw_json: Value = serde_json::from_slice(json_bytes)
610 .map_err(|error| LoadError::Malformed(format!("invalid top-level JSON: {error}")))?;
611 if !raw_json.is_object() {
612 return Err(LoadError::Malformed("top-level glTF JSON is not an object".into()).into());
613 }
614 let gltf = gltf::Gltf::from_slice_without_validation(bytes).map_err(LoadError::Gltf)?;
615
616 let mut violations = Vec::new();
617 let (manifest, _raw_scene_attachment) = inventory(&raw_json, container, &mut violations);
618 let accessor_uses = inspect_accessor_uses(&raw_json, &mut violations);
619 match validate_document(&gltf.document) {
620 Ok(()) => {}
621 Err(error) => return Err(LoadError::Gltf(error).into()),
622 }
623 validate_animations(&gltf.document)?;
624 let topology = topology(&gltf.document)?;
625
626 let can_resolve_buffers = !manifest
627 .buffers
628 .iter()
629 .any(|buffer| buffer.source_kind == GltfBufferSourceKind::External);
630 let resolved_buffers = if can_resolve_buffers {
631 resolve_buffers(&gltf, path.parent())?
632 } else {
633 Vec::new()
634 };
635 if can_resolve_buffers {
636 inspect_accessor_layouts(
637 &raw_json,
638 &resolved_buffers,
639 &accessor_uses,
640 &mut violations,
641 );
642 }
643 let (clip_track_projection_required, refuse) = match policy {
644 CapturePolicy::Scale(policy) => {
645 violations.sort();
646 violations.dedup();
647 (
648 false,
649 match policy {
650 GatePolicy::Enforce => !violations.is_empty(),
651 #[cfg(test)]
652 GatePolicy::Bypass => false,
653 },
654 )
655 }
656 CapturePolicy::ClipTracks => {
657 let animation_accessors = animation_accessor_indices(&raw_json);
658 let projection_required = violations
659 .iter()
660 .any(|violation| clip_track_projects_away(violation, &animation_accessors));
661 violations
662 .retain(|violation| !clip_track_projects_away(violation, &animation_accessors));
663 violations.sort();
664 violations.dedup();
665 (projection_required, !violations.is_empty())
666 }
667 };
668 if refuse {
669 let count = violations.len();
670 return Err(GltfScalePreflightError::Unsupported {
671 manifest: Box::new(manifest),
672 violations,
673 count,
674 });
675 }
676
677 if !clip_track_projection_required {
682 let loaded_source = crate::load_source_bytes(path, bytes)?;
683 if matches!(policy, CapturePolicy::ClipTracks)
684 && !clip_track_source_facts_complete(loaded_source.source_facts())
685 {
686 return Err(LoadError::Malformed(
687 "clip-track raw source facts coverage is incomplete".into(),
688 )
689 .into());
690 }
691 return Ok(captured_scale_source(
692 loaded_source,
693 bytes,
694 manifest,
695 raw_json,
696 resolved_buffers,
697 false,
698 ));
699 }
700
701 let mut facts = source_facts_builder(bytes).map_err(LoadError::from)?;
705 project_extension_facts(&gltf.document, &mut facts);
706 project_resource_facts(&gltf.document, &mut facts);
707 let has_unmodeled_extension_domain = has_extension_object(bytes)
708 || gltf.document.extensions_used().next().is_some()
709 || gltf.document.extensions_required().next().is_some();
710 let (dependency_closure, _) = capture_dependency_closure(
711 &facts,
712 None,
713 has_unmodeled_extension_domain,
714 &mut crate::read_external_file,
715 )?;
716 let source_skeleton = extract_source_skeleton(&gltf.document, &resolved_buffers, &topology);
717 let mut document = build_document(&gltf, &resolved_buffers, path, &topology, &mut facts)?;
718 document.assets.source_skeleton = source_skeleton;
719 let loaded_source = facts
720 .finish_with_dependency_closure(document, dependency_closure)
721 .map_err(LoadError::from)?;
722 if !clip_track_source_facts_complete(loaded_source.source_facts()) {
723 return Err(LoadError::Malformed(
724 "clip-track raw source facts coverage is incomplete".into(),
725 )
726 .into());
727 }
728
729 Ok(captured_scale_source(
730 loaded_source,
731 bytes,
732 manifest,
733 raw_json,
734 resolved_buffers,
735 clip_track_projection_required,
736 ))
737}
738
739fn captured_scale_source(
741 loaded_source: LoadedSource,
742 source_bytes: &[u8],
743 manifest: GltfCapabilityManifest,
744 raw_json: Value,
745 resolved_buffers: Vec<Vec<u8>>,
746 clip_track_projection_required: bool,
747) -> GltfScaleSource {
748 GltfScaleSource {
749 loaded_source,
750 #[cfg(test)]
751 document_override: None,
752 manifest,
753 source_bytes: source_bytes.to_vec(),
754 raw_json,
755 resolved_buffers,
756 clip_track_projection_required,
757 }
758}
759
760fn clip_track_projects_away(
763 violation: &GltfCapabilityViolation,
764 animation_accessors: &BTreeSet<usize>,
765) -> bool {
766 use GltfCapabilityViolationKind as Kind;
767 match violation.kind {
768 Kind::MorphTarget
769 | Kind::MorphWeights
770 | Kind::NonTrianglePrimitive
771 | Kind::UnsupportedVertexAttribute
772 | Kind::SecondarySkinInfluences
773 | Kind::MissingInverseBinds
774 | Kind::EmptyInverseBindAccessor
775 | Kind::InverseBindCountMismatch
776 | Kind::UnreadableInverseBinds => true,
777 Kind::UnsafeAccessorLayout
778 | Kind::ConflictingAccessorUse
779 | Kind::OverlappingAccessorRanges => accessor_index_at(&violation.location)
780 .is_some_and(|accessor| !animation_accessors.contains(&accessor)),
781 Kind::ImagePayloadOverlap => true,
786 Kind::ExternalResource
787 | Kind::Camera
788 | Kind::Light
789 | Kind::Instancing
790 | Kind::ExtensionDeclaration
791 | Kind::ExtensionPayload
792 | Kind::Extras
793 | Kind::UnknownJsonMember
794 | Kind::ConflictingNodeTransform
795 | Kind::NonAffineNodeMatrix
796 | Kind::AnimatedMatrixNode => false,
797 }
798}
799
800fn animation_accessor_indices(root: &Value) -> BTreeSet<usize> {
802 let Some(animations) = root.get("animations").and_then(Value::as_array) else {
803 return BTreeSet::new();
804 };
805 animations
806 .iter()
807 .flat_map(|animation| {
808 animation
809 .get("samplers")
810 .and_then(Value::as_array)
811 .into_iter()
812 .flatten()
813 })
814 .flat_map(|sampler| {
815 [
816 as_index(sampler.get("input")),
817 as_index(sampler.get("output")),
818 ]
819 })
820 .flatten()
821 .collect()
822}
823
824fn accessor_index_at(location: &str) -> Option<usize> {
826 location
827 .strip_prefix("/accessors/")?
828 .split('/')
829 .next()?
830 .parse()
831 .ok()
832}
833
834fn clip_track_source_facts_complete(source: SourceFactsViewV1<'_>) -> bool {
837 [
838 source.clips().coverage().state(),
839 source.constructs().coverage().state(),
840 source.resources().coverage().state(),
841 ]
842 .into_iter()
843 .all(|state| state == SourceSetCoverageStateV1::Complete)
844}
845
846#[cfg(test)]
857pub(crate) fn scale_source_past_the_gate(
858 path: &Path,
859 bytes: &[u8],
860) -> Result<GltfScaleSource, GltfScalePreflightError> {
861 capture_scale_source(path, bytes, GatePolicy::Bypass)
862}
863
864#[cfg(test)]
882pub(crate) fn scale_source_with_document(
883 mut source: GltfScaleSource,
884 document: Document,
885) -> GltfScaleSource {
886 source.document_override = Some(document);
887 source
888}
889
890pub(crate) fn raw_json_bytes(bytes: &[u8]) -> Result<(GltfContainerKind, &[u8]), LoadError> {
895 if !bytes.starts_with(GLB_MAGIC) {
896 return Ok((GltfContainerKind::Gltf, bytes));
897 }
898 let chunk_length = bytes
899 .get(12..16)
900 .and_then(|slice| slice.try_into().ok())
901 .map(u32::from_le_bytes)
902 .ok_or_else(|| LoadError::Buffer("malformed GLB JSON chunk header".into()))?
903 as usize;
904 let chunk_type = bytes
905 .get(16..20)
906 .and_then(|slice| slice.try_into().ok())
907 .map(u32::from_le_bytes)
908 .ok_or_else(|| LoadError::Buffer("malformed GLB JSON chunk header".into()))?;
909 if chunk_type != GLB_JSON_CHUNK {
910 return Err(LoadError::Buffer(
911 "GLB first chunk is not a JSON chunk".into(),
912 ));
913 }
914 let end = 20usize
915 .checked_add(chunk_length)
916 .ok_or_else(|| LoadError::Buffer("GLB JSON chunk range overflow".into()))?;
917 let json = bytes
918 .get(20..end)
919 .ok_or_else(|| LoadError::Buffer("malformed GLB JSON chunk length".into()))?;
920 Ok((GltfContainerKind::Glb, json))
921}
922
923pub(crate) fn raw_scene_attachment_inventory_from_bytes(
930 bytes: &[u8],
931 source: &LoadedSource,
932) -> Result<RawSceneAttachmentInventoryV1, LoadError> {
933 let (container, json_bytes) = raw_json_bytes(bytes)?;
934 let root: Value = serde_json::from_slice(json_bytes)
935 .map_err(|error| LoadError::Malformed(format!("invalid top-level JSON: {error}")))?;
936 let mut ignored_violations = Vec::new();
937 let (_, parts) = inventory(&root, container, &mut ignored_violations);
938 parts.inventory(source)
939}
940
941fn violation(
942 violations: &mut Vec<GltfCapabilityViolation>,
943 kind: GltfCapabilityViolationKind,
944 location: impl Into<String>,
945) {
946 violations.push(GltfCapabilityViolation {
947 kind,
948 location: location.into(),
949 });
950}
951
952fn as_index(value: Option<&Value>) -> Option<usize> {
953 value?.as_u64()?.try_into().ok()
954}
955
956fn inventory(
957 root: &Value,
958 container: GltfContainerKind,
959 violations: &mut Vec<GltfCapabilityViolation>,
960) -> (GltfCapabilityManifest, RawSceneAttachmentParts) {
961 let Some(object) = root.as_object() else {
962 return (
963 GltfCapabilityManifest {
964 container,
965 buffers: Vec::new(),
966 buffer_views: Vec::new(),
967 accessors: Vec::new(),
968 nodes: Vec::new(),
969 animation_channels: Vec::new(),
970 primitives: Vec::new(),
971 morph_weight_locations: Vec::new(),
972 instancing: Vec::new(),
973 skins: Vec::new(),
974 camera_count: 0,
975 extensions: Vec::new(),
976 extension_locations: Vec::new(),
977 external_resource_locations: Vec::new(),
978 extras_locations: Vec::new(),
979 unknown_member_locations: Vec::new(),
980 },
981 RawSceneAttachmentParts::default(),
982 );
983 };
984 let mut manifest = GltfCapabilityManifest {
985 container,
986 buffers: Vec::new(),
987 buffer_views: Vec::new(),
988 accessors: Vec::new(),
989 nodes: Vec::new(),
990 animation_channels: Vec::new(),
991 primitives: Vec::new(),
992 morph_weight_locations: Vec::new(),
993 instancing: Vec::new(),
994 skins: Vec::new(),
995 camera_count: object
996 .get("cameras")
997 .and_then(Value::as_array)
998 .map_or(0, Vec::len),
999 extensions: Vec::new(),
1000 extension_locations: Vec::new(),
1001 external_resource_locations: Vec::new(),
1002 extras_locations: Vec::new(),
1003 unknown_member_locations: Vec::new(),
1004 };
1005
1006 inspect_schema_members(root, "", &mut manifest, violations);
1007 inventory_extensions(object, &mut manifest, violations);
1008 inventory_buffers(object, container, &mut manifest, violations);
1009 inventory_buffer_views_and_accessors(object, &mut manifest);
1010 let mut raw_scene_attachment = RawSceneAttachmentParts::default();
1011 inventory_scenes(object, &mut raw_scene_attachment);
1012 inventory_nodes(object, &mut manifest, violations, &mut raw_scene_attachment);
1013 inventory_animations(object, &mut manifest, violations);
1014 inventory_meshes(object, &mut manifest, violations, &mut raw_scene_attachment);
1015 inventory_skins(object, &mut manifest, violations);
1016
1017 if manifest.camera_count > 0 {
1018 violation(violations, GltfCapabilityViolationKind::Camera, "/cameras");
1019 }
1020 manifest.extensions.sort();
1021 manifest.extensions.dedup();
1022 manifest.extension_locations.sort();
1023 manifest.extension_locations.dedup();
1024 manifest.external_resource_locations.sort();
1025 manifest.external_resource_locations.dedup();
1026 manifest.extras_locations.sort();
1027 manifest.extras_locations.dedup();
1028 manifest.unknown_member_locations.sort();
1029 manifest.unknown_member_locations.dedup();
1030 manifest.morph_weight_locations.sort();
1031 manifest.morph_weight_locations.dedup();
1032 (manifest, raw_scene_attachment)
1033}
1034
1035fn inventory_scenes(root: &Map<String, Value>, raw: &mut RawSceneAttachmentParts) {
1036 let Some(scenes) = root.get("scenes").and_then(Value::as_array) else {
1037 return;
1038 };
1039 for (scene_index, scene) in scenes.iter().enumerate() {
1040 if raw.scene_overflow {
1041 break;
1042 }
1043 let Some(scene) = scene.as_object() else {
1044 continue;
1045 };
1046 let roots = scene
1047 .get("nodes")
1048 .and_then(Value::as_array)
1049 .map(Vec::as_slice)
1050 .unwrap_or_default();
1051 raw.retain_scene(scene_index as u64, roots);
1052 }
1053}
1054
1055fn inventory_extensions(
1056 root: &Map<String, Value>,
1057 manifest: &mut GltfCapabilityManifest,
1058 violations: &mut Vec<GltfCapabilityViolation>,
1059) {
1060 for key in ["extensionsUsed", "extensionsRequired"] {
1061 let Some(values) = root.get(key).and_then(Value::as_array) else {
1062 continue;
1063 };
1064 for (index, value) in values.iter().enumerate() {
1065 let Some(name) = value.as_str() else { continue };
1066 manifest.extensions.push(name.to_owned());
1067 let kind = match name {
1068 "KHR_lights_punctual" => GltfCapabilityViolationKind::Light,
1069 "EXT_mesh_gpu_instancing" => GltfCapabilityViolationKind::Instancing,
1070 _ => GltfCapabilityViolationKind::ExtensionDeclaration,
1071 };
1072 violation(violations, kind, format!("/{key}/{index}"));
1073 }
1074 }
1075}
1076
1077fn inventory_buffers(
1078 root: &Map<String, Value>,
1079 container: GltfContainerKind,
1080 manifest: &mut GltfCapabilityManifest,
1081 violations: &mut Vec<GltfCapabilityViolation>,
1082) {
1083 let Some(buffers) = root.get("buffers").and_then(Value::as_array) else {
1084 return;
1085 };
1086 for (buffer_index, buffer) in buffers.iter().enumerate() {
1087 let Some(buffer) = buffer.as_object() else {
1088 continue;
1089 };
1090 let uri = buffer.get("uri").and_then(Value::as_str);
1091 let source_kind = match uri {
1092 Some(uri) if uri.starts_with("data:") => GltfBufferSourceKind::DataUri,
1093 Some(_) => GltfBufferSourceKind::External,
1094 None if container == GltfContainerKind::Glb => GltfBufferSourceKind::BinaryChunk,
1095 None => GltfBufferSourceKind::External,
1096 };
1097 if source_kind == GltfBufferSourceKind::External {
1098 manifest
1099 .external_resource_locations
1100 .push(format!("/buffers/{buffer_index}/uri"));
1101 violation(
1102 violations,
1103 GltfCapabilityViolationKind::ExternalResource,
1104 format!("/buffers/{buffer_index}/uri"),
1105 );
1106 }
1107 manifest.buffers.push(GltfBufferCapability {
1108 buffer_index,
1109 source_kind,
1110 declared_byte_length: buffer
1111 .get("byteLength")
1112 .and_then(Value::as_u64)
1113 .unwrap_or(0),
1114 });
1115 }
1116 if let Some(images) = root.get("images").and_then(Value::as_array) {
1117 for (image_index, image) in images.iter().enumerate() {
1118 if image
1119 .get("uri")
1120 .and_then(Value::as_str)
1121 .is_some_and(|uri| !uri.starts_with("data:"))
1122 {
1123 manifest
1124 .external_resource_locations
1125 .push(format!("/images/{image_index}/uri"));
1126 violation(
1127 violations,
1128 GltfCapabilityViolationKind::ExternalResource,
1129 format!("/images/{image_index}/uri"),
1130 );
1131 }
1132 }
1133 }
1134}
1135
1136fn inventory_buffer_views_and_accessors(
1137 root: &Map<String, Value>,
1138 manifest: &mut GltfCapabilityManifest,
1139) {
1140 if let Some(buffer_views) = root.get("bufferViews").and_then(Value::as_array) {
1141 for (buffer_view_index, view) in buffer_views.iter().enumerate() {
1142 let Some(view) = view.as_object() else {
1143 continue;
1144 };
1145 manifest.buffer_views.push(GltfBufferViewCapability {
1146 buffer_view_index,
1147 buffer_index: as_index(view.get("buffer")).unwrap_or(usize::MAX),
1148 byte_offset: view.get("byteOffset").and_then(Value::as_u64).unwrap_or(0),
1149 byte_length: view.get("byteLength").and_then(Value::as_u64).unwrap_or(0),
1150 byte_stride: view.get("byteStride").and_then(Value::as_u64),
1151 });
1152 }
1153 }
1154 if let Some(accessors) = root.get("accessors").and_then(Value::as_array) {
1155 for (accessor_index, accessor) in accessors.iter().enumerate() {
1156 let Some(accessor) = accessor.as_object() else {
1157 continue;
1158 };
1159 manifest.accessors.push(GltfAccessorCapability {
1160 accessor_index,
1161 buffer_view_index: as_index(accessor.get("bufferView")),
1162 byte_offset: accessor
1163 .get("byteOffset")
1164 .and_then(Value::as_u64)
1165 .unwrap_or(0),
1166 component_type: accessor
1167 .get("componentType")
1168 .and_then(Value::as_u64)
1169 .unwrap_or(0),
1170 accessor_type: accessor
1171 .get("type")
1172 .and_then(Value::as_str)
1173 .unwrap_or_default()
1174 .to_owned(),
1175 count: accessor.get("count").and_then(Value::as_u64).unwrap_or(0),
1176 normalized: accessor
1177 .get("normalized")
1178 .and_then(Value::as_bool)
1179 .unwrap_or(false),
1180 sparse: accessor.contains_key("sparse"),
1181 });
1182 }
1183 }
1184}
1185
1186pub(crate) const AFFINE_LAST_ROW: [(usize, f64); 4] = [(3, 0.0), (7, 0.0), (11, 0.0), (15, 1.0)];
1194
1195#[derive(Debug, Clone, Copy, PartialEq)]
1197pub(crate) enum NodeTransformFault {
1198 TrsBesideMatrix {
1200 node_index: usize,
1202 member: &'static str,
1204 },
1205 ProjectiveMatrixEntry {
1207 node_index: usize,
1209 component: usize,
1211 value: f64,
1213 expected: f64,
1215 },
1216 UnreadableMatrixEntry {
1219 node_index: usize,
1221 component: usize,
1223 },
1224}
1225
1226impl NodeTransformFault {
1227 pub(crate) fn location(self) -> String {
1229 match self {
1230 Self::TrsBesideMatrix { node_index, member } => format!("/nodes/{node_index}/{member}"),
1231 Self::ProjectiveMatrixEntry {
1232 node_index,
1233 component,
1234 ..
1235 }
1236 | Self::UnreadableMatrixEntry {
1237 node_index,
1238 component,
1239 } => format!("/nodes/{node_index}/matrix/{component}"),
1240 }
1241 }
1242
1243 fn kind(self) -> GltfCapabilityViolationKind {
1245 match self {
1246 Self::TrsBesideMatrix { .. } => GltfCapabilityViolationKind::ConflictingNodeTransform,
1247 Self::ProjectiveMatrixEntry { .. } | Self::UnreadableMatrixEntry { .. } => {
1250 GltfCapabilityViolationKind::NonAffineNodeMatrix
1251 }
1252 }
1253 }
1254}
1255
1256pub(crate) fn declared<'a>(object: &'a Value, member: &str) -> Option<&'a Value> {
1275 object.get(member).filter(|value| !value.is_null())
1276}
1277
1278pub(crate) fn node_transform_faults(nodes: &[Value]) -> Vec<NodeTransformFault> {
1301 let mut faults = Vec::new();
1302 for (node_index, node) in nodes.iter().enumerate() {
1303 let Some(matrix) = declared(node, "matrix") else {
1304 continue;
1305 };
1306 for member in ["translation", "rotation", "scale"] {
1307 if declared(node, member).is_some() {
1308 faults.push(NodeTransformFault::TrsBesideMatrix { node_index, member });
1309 }
1310 }
1311 let Some(values) = matrix.as_array().filter(|values| values.len() == 16) else {
1312 continue;
1313 };
1314 for (component, expected) in AFFINE_LAST_ROW {
1315 match values[component].as_f64() {
1316 None => faults.push(NodeTransformFault::UnreadableMatrixEntry {
1317 node_index,
1318 component,
1319 }),
1320 Some(value) if value != expected => {
1321 faults.push(NodeTransformFault::ProjectiveMatrixEntry {
1322 node_index,
1323 component,
1324 value,
1325 expected,
1326 });
1327 }
1328 Some(_) => {}
1329 }
1330 }
1331 }
1332 faults
1333}
1334
1335fn inventory_nodes(
1336 root: &Map<String, Value>,
1337 manifest: &mut GltfCapabilityManifest,
1338 violations: &mut Vec<GltfCapabilityViolation>,
1339 raw: &mut RawSceneAttachmentParts,
1340) {
1341 let Some(nodes) = root.get("nodes").and_then(Value::as_array) else {
1342 return;
1343 };
1344 for fault in node_transform_faults(nodes) {
1345 violation(violations, fault.kind(), fault.location());
1346 }
1347 for (node_index, node) in nodes.iter().enumerate() {
1348 if !node.is_object() {
1349 continue;
1350 }
1351 if node.get("weights").is_some() {
1357 manifest
1358 .morph_weight_locations
1359 .push(format!("/nodes/{node_index}/weights"));
1360 }
1361 if node.get("camera").is_some() {
1362 violation(
1363 violations,
1364 GltfCapabilityViolationKind::Camera,
1365 format!("/nodes/{node_index}/camera"),
1366 );
1367 }
1368 if let Some(attributes) = node
1369 .get("extensions")
1370 .and_then(|extensions| extensions.get("EXT_mesh_gpu_instancing"))
1371 .and_then(|extension| extension.get("attributes"))
1372 .and_then(Value::as_object)
1373 {
1374 let mut attributes = attributes
1375 .iter()
1376 .map(|(semantic, accessor)| GltfAttributeCapability {
1377 semantic: semantic.clone(),
1378 accessor_index: as_index(Some(accessor)).unwrap_or(usize::MAX),
1379 })
1380 .collect::<Vec<_>>();
1381 attributes.sort_by(|left, right| left.semantic.cmp(&right.semantic));
1382 manifest.instancing.push(GltfInstancingCapability {
1383 node_index,
1384 attributes,
1385 });
1386 }
1387 let mesh_index = as_index(node.get("mesh"));
1388 manifest.nodes.push(GltfNodeCapability {
1389 node_index,
1390 rest_kind: if declared(node, "matrix").is_some() {
1393 GltfNodeRestKind::Matrix
1394 } else {
1395 GltfNodeRestKind::Trs
1396 },
1397 mesh_index,
1398 skin_index: as_index(node.get("skin")),
1399 });
1400 if !raw.attachment_overflow
1401 && let Some(mesh_index) = mesh_index
1402 {
1403 raw.retain_attachment(RawNodeMeshAttachmentRowV1::new(
1404 node_index as u64,
1405 mesh_index as u64,
1406 ));
1407 }
1408 }
1409}
1410
1411fn inventory_animations(
1412 root: &Map<String, Value>,
1413 manifest: &mut GltfCapabilityManifest,
1414 violations: &mut Vec<GltfCapabilityViolation>,
1415) {
1416 let Some(animations) = root.get("animations").and_then(Value::as_array) else {
1417 return;
1418 };
1419 for (animation_index, animation) in animations.iter().enumerate() {
1420 let Some(animation) = animation.as_object() else {
1421 continue;
1422 };
1423 let samplers = animation
1424 .get("samplers")
1425 .and_then(Value::as_array)
1426 .map(Vec::as_slice)
1427 .unwrap_or_default();
1428 let channels = animation
1429 .get("channels")
1430 .and_then(Value::as_array)
1431 .map(Vec::as_slice)
1432 .unwrap_or_default();
1433 for (channel_index, channel) in channels.iter().enumerate() {
1434 let Some(channel) = channel.as_object() else {
1435 continue;
1436 };
1437 let sampler_index = as_index(channel.get("sampler")).unwrap_or(usize::MAX);
1438 let Some(sampler) = samplers.get(sampler_index).and_then(Value::as_object) else {
1439 continue;
1440 };
1441 let Some(target) = channel.get("target").and_then(Value::as_object) else {
1442 continue;
1443 };
1444 let target_path = target
1445 .get("path")
1446 .and_then(Value::as_str)
1447 .unwrap_or_default()
1448 .to_owned();
1449 if target_path == "weights" {
1450 manifest.morph_weight_locations.push(format!(
1451 "/animations/{animation_index}/channels/{channel_index}/target/path"
1452 ));
1453 }
1454 let target_node_index = as_index(target.get("node")).unwrap_or(usize::MAX);
1455 if manifest
1456 .nodes
1457 .get(target_node_index)
1458 .is_some_and(|node| node.rest_kind == GltfNodeRestKind::Matrix)
1459 {
1460 violation(
1461 violations,
1462 GltfCapabilityViolationKind::AnimatedMatrixNode,
1463 format!("/animations/{animation_index}/channels/{channel_index}/target"),
1464 );
1465 }
1466 manifest
1467 .animation_channels
1468 .push(GltfAnimationChannelCapability {
1469 animation_index,
1470 channel_index,
1471 target_node_index,
1472 target_path,
1473 interpolation: sampler
1474 .get("interpolation")
1475 .and_then(Value::as_str)
1476 .unwrap_or("LINEAR")
1477 .to_owned(),
1478 input_accessor_index: as_index(sampler.get("input")).unwrap_or(usize::MAX),
1479 output_accessor_index: as_index(sampler.get("output")).unwrap_or(usize::MAX),
1480 });
1481 }
1482 }
1483}
1484
1485fn inventory_meshes(
1486 root: &Map<String, Value>,
1487 manifest: &mut GltfCapabilityManifest,
1488 violations: &mut Vec<GltfCapabilityViolation>,
1489 raw: &mut RawSceneAttachmentParts,
1490) {
1491 let Some(meshes) = root.get("meshes").and_then(Value::as_array) else {
1492 return;
1493 };
1494 for (mesh_index, mesh) in meshes.iter().enumerate() {
1495 let Some(mesh) = mesh.as_object() else {
1496 continue;
1497 };
1498 if mesh.contains_key("weights") {
1499 manifest
1500 .morph_weight_locations
1501 .push(format!("/meshes/{mesh_index}/weights"));
1502 }
1503 let primitives = mesh
1504 .get("primitives")
1505 .and_then(Value::as_array)
1506 .map(Vec::as_slice)
1507 .unwrap_or_default();
1508 for (primitive_index, primitive) in primitives.iter().enumerate() {
1509 let Some(primitive) = primitive.as_object() else {
1510 continue;
1511 };
1512 let mode = primitive.get("mode").and_then(Value::as_u64).unwrap_or(4);
1513 if mode != 4 {
1514 violation(
1515 violations,
1516 GltfCapabilityViolationKind::NonTrianglePrimitive,
1517 format!("/meshes/{mesh_index}/primitives/{primitive_index}/mode"),
1518 );
1519 }
1520 let mut attributes = primitive
1521 .get("attributes")
1522 .and_then(Value::as_object)
1523 .map(|attributes| {
1524 attributes
1525 .iter()
1526 .map(|(semantic, accessor)| GltfAttributeCapability {
1527 semantic: semantic.clone(),
1528 accessor_index: as_index(Some(accessor)).unwrap_or(usize::MAX),
1529 })
1530 .collect::<Vec<_>>()
1531 })
1532 .unwrap_or_default();
1533 attributes.sort_by(|left, right| left.semantic.cmp(&right.semantic));
1534 for attribute in &attributes {
1535 let semantic = &attribute.semantic;
1536 let semantic_pointer = json_pointer_token(semantic);
1537 let location = format!(
1538 "/meshes/{mesh_index}/primitives/{primitive_index}/attributes/{semantic_pointer}"
1539 );
1540 if is_secondary_influence(semantic) {
1541 violation(
1542 violations,
1543 GltfCapabilityViolationKind::SecondarySkinInfluences,
1544 location,
1545 );
1546 } else if !matches!(
1547 semantic.as_str(),
1548 "POSITION" | "NORMAL" | "TEXCOORD_0" | "JOINTS_0" | "WEIGHTS_0"
1549 ) {
1550 violation(
1551 violations,
1552 GltfCapabilityViolationKind::UnsupportedVertexAttribute,
1553 location,
1554 );
1555 }
1556 }
1557 let morph_target_count = primitive
1558 .get("targets")
1559 .and_then(Value::as_array)
1560 .map_or(0, Vec::len);
1561 let mut morph_position_accessors = Vec::new();
1562 let mut unsupported_morph_locations = Vec::new();
1563 for (target_index, target) in primitive
1564 .get("targets")
1565 .and_then(Value::as_array)
1566 .into_iter()
1567 .flatten()
1568 .enumerate()
1569 {
1570 let Some(target) = target.as_object() else {
1571 continue;
1572 };
1573 for (semantic, accessor) in target {
1574 let location = format!(
1575 "/meshes/{mesh_index}/primitives/{primitive_index}/targets/{target_index}/{}",
1576 json_pointer_token(semantic)
1577 );
1578 if semantic == "POSITION" {
1579 if let Some(accessor_index) = as_index(Some(accessor)) {
1580 morph_position_accessors.push(accessor_index);
1581 }
1582 } else {
1583 violation(
1584 violations,
1585 GltfCapabilityViolationKind::MorphTarget,
1586 location.clone(),
1587 );
1588 unsupported_morph_locations.push(location);
1589 }
1590 }
1591 }
1592 manifest.primitives.push(GltfPrimitiveCapability {
1593 mesh_index,
1594 primitive_index,
1595 mode,
1596 attributes,
1597 morph_target_count,
1598 morph_position_accessors,
1599 unsupported_morph_locations,
1600 });
1601 if !raw.primitive_overflow {
1602 raw.retain_primitive(RawMeshPrimitiveRowV1::new(
1603 mesh_index as u64,
1604 primitive_index as u64,
1605 RawPrimitiveTopologyV1::from_gltf_mode(mode),
1606 as_index(primitive.get("indices")).map(|index| index as u64),
1607 ));
1608 }
1609 }
1610 }
1611}
1612
1613fn is_secondary_influence(semantic: &str) -> bool {
1614 semantic
1615 .strip_prefix("JOINTS_")
1616 .or_else(|| semantic.strip_prefix("WEIGHTS_"))
1617 .and_then(|index| index.parse::<u32>().ok())
1618 .is_some_and(|index| index >= 1)
1619}
1620
1621fn inventory_skins(
1622 root: &Map<String, Value>,
1623 manifest: &mut GltfCapabilityManifest,
1624 violations: &mut Vec<GltfCapabilityViolation>,
1625) {
1626 let accessors = root
1627 .get("accessors")
1628 .and_then(Value::as_array)
1629 .map(Vec::as_slice)
1630 .unwrap_or_default();
1631 let Some(skins) = root.get("skins").and_then(Value::as_array) else {
1632 return;
1633 };
1634 for (skin_index, skin) in skins.iter().enumerate() {
1635 let Some(skin) = skin.as_object() else {
1636 continue;
1637 };
1638 let joint_count = skin
1639 .get("joints")
1640 .and_then(Value::as_array)
1641 .map_or(0, Vec::len);
1642 let inverse_bind_accessor_index = as_index(skin.get("inverseBindMatrices"));
1643 let inverse_bind_count = inverse_bind_accessor_index
1644 .and_then(|index| accessors.get(index))
1645 .and_then(|accessor| accessor.get("count"))
1646 .and_then(Value::as_u64);
1647 let inverse_bind_readable = inverse_bind_accessor_index
1648 .and_then(|index| accessors.get(index))
1649 .and_then(Value::as_object)
1650 .is_some_and(|accessor| {
1651 accessor.get("bufferView").and_then(Value::as_u64).is_some()
1652 && accessor.get("componentType").and_then(Value::as_u64) == Some(5126)
1653 && accessor.get("type").and_then(Value::as_str) == Some("MAT4")
1654 && !accessor.contains_key("sparse")
1655 });
1656 match (inverse_bind_accessor_index, inverse_bind_count) {
1657 (None, _) => violation(
1658 violations,
1659 GltfCapabilityViolationKind::MissingInverseBinds,
1660 format!("/skins/{skin_index}/inverseBindMatrices"),
1661 ),
1662 (Some(_), Some(0)) => violation(
1663 violations,
1664 GltfCapabilityViolationKind::EmptyInverseBindAccessor,
1665 format!("/skins/{skin_index}/inverseBindMatrices"),
1666 ),
1667 (Some(_), Some(count)) if count != joint_count as u64 => violation(
1668 violations,
1669 GltfCapabilityViolationKind::InverseBindCountMismatch,
1670 format!("/skins/{skin_index}/inverseBindMatrices"),
1671 ),
1672 (Some(_), _) if !inverse_bind_readable => violation(
1673 violations,
1674 GltfCapabilityViolationKind::UnreadableInverseBinds,
1675 format!("/skins/{skin_index}/inverseBindMatrices"),
1676 ),
1677 _ => {}
1678 }
1679 manifest.skins.push(GltfSkinCapability {
1680 skin_index,
1681 joint_count,
1682 inverse_bind_accessor_index,
1683 inverse_bind_count,
1684 });
1685 }
1686}
1687
1688#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1689enum AccessorUse {
1690 ScaleBearing,
1691 Dimensionless,
1692}
1693
1694#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1696enum RangeOwner {
1697 Accessor(usize),
1699 SparseIndices(usize),
1701 SparseValues(usize),
1703 ImagePayload(usize),
1705}
1706
1707impl RangeOwner {
1708 fn location(self) -> String {
1710 match self {
1711 Self::Accessor(index) => format!("/accessors/{index}"),
1712 Self::SparseIndices(index) => {
1713 format!("/accessors/{index}/sparse/indices/bufferView")
1714 }
1715 Self::SparseValues(index) => {
1716 format!("/accessors/{index}/sparse/values/bufferView")
1717 }
1718 Self::ImagePayload(index) => format!("/images/{index}/bufferView"),
1719 }
1720 }
1721
1722 fn overlap_kind(self) -> GltfCapabilityViolationKind {
1725 match self {
1726 Self::Accessor(_) | Self::SparseIndices(_) | Self::SparseValues(_) => {
1727 GltfCapabilityViolationKind::OverlappingAccessorRanges
1728 }
1729 Self::ImagePayload(_) => GltfCapabilityViolationKind::ImagePayloadOverlap,
1730 }
1731 }
1732}
1733
1734type OwnedRange = (usize, usize, usize, RangeOwner, bool);
1736
1737fn inspect_accessor_layouts(
1738 root: &Value,
1739 buffers: &[Vec<u8>],
1740 uses: &BTreeMap<usize, BTreeSet<AccessorUse>>,
1741 violations: &mut Vec<GltfCapabilityViolation>,
1742) {
1743 let Some(root) = root.as_object() else { return };
1744 let mut ranges: Vec<OwnedRange> = Vec::new();
1745 let accessors = root
1746 .get("accessors")
1747 .and_then(Value::as_array)
1748 .map(Vec::as_slice)
1749 .unwrap_or_default();
1750 for accessor_index in 0..accessors.len() {
1751 let accessor_uses = uses.get(&accessor_index);
1752 let scale_bearing =
1753 accessor_uses.is_some_and(|uses| uses.contains(&AccessorUse::ScaleBearing));
1754 let accessor_ranges = if scale_bearing {
1755 dense_f32_accessor_range(root, buffers, accessor_index).map(|range| {
1756 vec![(
1757 range.0,
1758 range.1,
1759 range.2,
1760 RangeOwner::Accessor(accessor_index),
1761 true,
1762 )]
1763 })
1764 } else if accessor_uses.is_some() {
1765 accessor_range(root, buffers, accessor_index).map(|range| {
1766 vec![(
1767 range.buffer,
1768 range.start,
1769 range.end,
1770 RangeOwner::Accessor(accessor_index),
1771 false,
1772 )]
1773 })
1774 } else {
1775 preserved_accessor_ranges(root, buffers, accessor_index)
1776 };
1777 match accessor_ranges {
1778 Some(accessor_ranges) => ranges.extend(accessor_ranges),
1779 None => violation(
1780 violations,
1781 GltfCapabilityViolationKind::UnsafeAccessorLayout,
1782 format!("/accessors/{accessor_index}"),
1783 ),
1784 }
1785 }
1786 ranges.extend(image_payload_ranges(root));
1787 ranges.sort_unstable();
1788
1789 let mut overlapping = BTreeSet::new();
1790 let mut prior_scale: Option<(usize, usize, RangeOwner)> = None;
1791 for &(buffer, start, end, owner, scale_bearing) in &ranges {
1792 if let Some((left_buffer, left_end, left_owner)) = prior_scale
1793 && left_buffer == buffer
1794 && start < left_end
1795 {
1796 overlapping.insert(left_owner);
1797 overlapping.insert(owner);
1798 }
1799 if scale_bearing
1800 && prior_scale
1801 .is_none_or(|(left_buffer, left_end, _)| left_buffer != buffer || end > left_end)
1802 {
1803 prior_scale = Some((buffer, end, owner));
1804 }
1805 }
1806 let mut later_scale: Option<(usize, usize, RangeOwner)> = None;
1807 for &(buffer, start, end, owner, scale_bearing) in ranges.iter().rev() {
1808 if let Some((right_buffer, right_start, right_owner)) = later_scale
1809 && right_buffer == buffer
1810 && right_start < end
1811 {
1812 overlapping.insert(owner);
1813 overlapping.insert(right_owner);
1814 }
1815 if scale_bearing
1816 && later_scale.is_none_or(|(right_buffer, right_start, _)| {
1817 right_buffer != buffer || start < right_start
1818 })
1819 {
1820 later_scale = Some((buffer, start, owner));
1821 }
1822 }
1823 for owner in overlapping {
1824 violation(violations, owner.overlap_kind(), owner.location());
1825 }
1826}
1827
1828fn preserved_accessor_ranges(
1837 root: &Map<String, Value>,
1838 buffers: &[Vec<u8>],
1839 accessor_index: usize,
1840) -> Option<Vec<OwnedRange>> {
1841 let accessor = root
1842 .get("accessors")?
1843 .as_array()?
1844 .get(accessor_index)?
1845 .as_object()?;
1846 let count: usize = accessor.get("count")?.as_u64()?.try_into().ok()?;
1847 if count == 0 {
1848 return Some(Vec::new());
1849 }
1850 let Some(sparse) = accessor.get("sparse") else {
1851 let range = accessor_range(root, buffers, accessor_index)?;
1852 return Some(vec![(
1853 range.buffer,
1854 range.start,
1855 range.end,
1856 RangeOwner::Accessor(accessor_index),
1857 false,
1858 )]);
1859 };
1860
1861 let mut ranges = Vec::with_capacity(3);
1862 if accessor.get("bufferView").is_some() {
1863 let range = dense_accessor_range(root, buffers, accessor_index)?;
1864 ranges.push((
1865 range.buffer,
1866 range.start,
1867 range.end,
1868 RangeOwner::Accessor(accessor_index),
1869 false,
1870 ));
1871 }
1872
1873 let sparse = sparse.as_object()?;
1874 let sparse_count: usize = sparse.get("count")?.as_u64()?.try_into().ok()?;
1875 if sparse_count == 0 {
1876 return Some(ranges);
1877 }
1878 let indices = sparse.get("indices")?.as_object()?;
1879 let index_size = match indices.get("componentType")?.as_u64()? {
1880 5121 => 1,
1881 5123 => 2,
1882 5125 => 4,
1883 _ => return None,
1884 };
1885 let indices_range = packed_view_range(
1886 root,
1887 buffers,
1888 as_index(indices.get("bufferView"))?,
1889 indices
1890 .get("byteOffset")
1891 .and_then(Value::as_u64)
1892 .unwrap_or(0),
1893 sparse_count,
1894 index_size,
1895 index_size,
1896 )?;
1897 ranges.push((
1898 indices_range.0,
1899 indices_range.1,
1900 indices_range.2,
1901 RangeOwner::SparseIndices(accessor_index),
1902 false,
1903 ));
1904
1905 let values = sparse.get("values")?.as_object()?;
1906 let component_size = component_size(accessor.get("componentType")?.as_u64()?)?;
1907 let element_layout = accessor_element_layout(accessor.get("type")?.as_str()?, component_size)?;
1908 let values_range = packed_view_range(
1909 root,
1910 buffers,
1911 as_index(values.get("bufferView"))?,
1912 values
1913 .get("byteOffset")
1914 .and_then(Value::as_u64)
1915 .unwrap_or(0),
1916 sparse_count,
1917 element_layout.stride,
1918 element_layout.terminal_size,
1919 )?;
1920 ranges.push((
1921 values_range.0,
1922 values_range.1,
1923 values_range.2,
1924 RangeOwner::SparseValues(accessor_index),
1925 false,
1926 ));
1927 Some(ranges)
1928}
1929
1930fn packed_view_range(
1932 root: &Map<String, Value>,
1933 buffers: &[Vec<u8>],
1934 view_index: usize,
1935 relative_offset: u64,
1936 count: usize,
1937 element_stride: usize,
1938 terminal_size: usize,
1939) -> Option<(usize, usize, usize)> {
1940 let view = root
1941 .get("bufferViews")?
1942 .as_array()?
1943 .get(view_index)?
1944 .as_object()?;
1945 let buffer_index = as_index(view.get("buffer"))?;
1946 let buffer = buffers.get(buffer_index)?;
1947 let view_offset: usize = view
1948 .get("byteOffset")
1949 .and_then(Value::as_u64)
1950 .unwrap_or(0)
1951 .try_into()
1952 .ok()?;
1953 let view_length: usize = view.get("byteLength")?.as_u64()?.try_into().ok()?;
1954 if view_offset.checked_add(view_length)? > buffer.len() {
1955 return None;
1956 }
1957 let relative_offset: usize = relative_offset.try_into().ok()?;
1958 let relative_end = relative_offset
1959 .checked_add(count.checked_sub(1)?.checked_mul(element_stride)?)?
1960 .checked_add(terminal_size)?;
1961 if relative_end > view_length {
1962 return None;
1963 }
1964 let start = view_offset.checked_add(relative_offset)?;
1965 let end = view_offset.checked_add(relative_end)?;
1966 Some((buffer_index, start, end))
1967}
1968
1969fn image_payload_ranges(root: &Map<String, Value>) -> Vec<OwnedRange> {
1996 let Some(images) = root.get("images").and_then(Value::as_array) else {
1997 return Vec::new();
1998 };
1999 let buffer_views = root
2000 .get("bufferViews")
2001 .and_then(Value::as_array)
2002 .map(Vec::as_slice)
2003 .unwrap_or_default();
2004 let mut out = Vec::new();
2005 for (image_index, image) in images.iter().enumerate() {
2006 let Some(view_index) = as_index(image.get("bufferView")) else {
2007 continue;
2008 };
2009 let Some(view) = buffer_views.get(view_index).and_then(Value::as_object) else {
2012 continue;
2013 };
2014 let Some(buffer) = as_index(view.get("buffer")) else {
2015 continue;
2016 };
2017 let start = clamped_usize(view.get("byteOffset").and_then(Value::as_u64).unwrap_or(0));
2018 let end = start.saturating_add(clamped_usize(
2019 view.get("byteLength").and_then(Value::as_u64).unwrap_or(0),
2020 ));
2021 if start < end {
2026 out.push((
2027 buffer,
2028 start,
2029 end,
2030 RangeOwner::ImagePayload(image_index),
2031 false,
2032 ));
2033 }
2034 }
2035 out
2036}
2037
2038fn clamped_usize(value: u64) -> usize {
2039 usize::try_from(value).unwrap_or(usize::MAX)
2040}
2041
2042fn inspect_accessor_uses(
2043 root: &Value,
2044 violations: &mut Vec<GltfCapabilityViolation>,
2045) -> BTreeMap<usize, BTreeSet<AccessorUse>> {
2046 let Some(root) = root.as_object() else {
2047 return BTreeMap::new();
2048 };
2049 let uses = collect_accessor_uses(root);
2050 for (accessor_index, accessor_uses) in &uses {
2051 if accessor_uses.len() > 1 {
2052 violation(
2053 violations,
2054 GltfCapabilityViolationKind::ConflictingAccessorUse,
2055 format!("/accessors/{accessor_index}"),
2056 );
2057 }
2058 }
2059 uses
2060}
2061
2062fn collect_accessor_uses(root: &Map<String, Value>) -> BTreeMap<usize, BTreeSet<AccessorUse>> {
2063 let mut uses: BTreeMap<usize, BTreeSet<AccessorUse>> = BTreeMap::new();
2064 let mut add = |index: Option<usize>, kind| {
2065 if let Some(index) = index {
2066 uses.entry(index).or_default().insert(kind);
2067 }
2068 };
2069 if let Some(meshes) = root.get("meshes").and_then(Value::as_array) {
2070 for mesh in meshes {
2071 let Some(primitives) = mesh.get("primitives").and_then(Value::as_array) else {
2072 continue;
2073 };
2074 for primitive in primitives {
2075 if let Some(attributes) = primitive.get("attributes").and_then(Value::as_object) {
2076 for (semantic, index) in attributes {
2077 add(
2078 as_index(Some(index)),
2079 if semantic == "POSITION" {
2080 AccessorUse::ScaleBearing
2081 } else {
2082 AccessorUse::Dimensionless
2083 },
2084 );
2085 }
2086 }
2087 add(
2088 as_index(primitive.get("indices")),
2089 AccessorUse::Dimensionless,
2090 );
2091 if let Some(targets) = primitive.get("targets").and_then(Value::as_array) {
2092 for target in targets {
2093 if let Some(target) = target.as_object() {
2094 for (semantic, index) in target {
2095 add(
2096 as_index(Some(index)),
2097 if semantic == "POSITION" {
2098 AccessorUse::ScaleBearing
2099 } else {
2100 AccessorUse::Dimensionless
2101 },
2102 );
2103 }
2104 }
2105 }
2106 }
2107 }
2108 }
2109 }
2110 if let Some(skins) = root.get("skins").and_then(Value::as_array) {
2111 for skin in skins {
2112 add(
2113 as_index(skin.get("inverseBindMatrices")),
2114 AccessorUse::ScaleBearing,
2115 );
2116 }
2117 }
2118 if let Some(animations) = root.get("animations").and_then(Value::as_array) {
2119 for animation in animations {
2120 let samplers = animation
2121 .get("samplers")
2122 .and_then(Value::as_array)
2123 .map(Vec::as_slice)
2124 .unwrap_or_default();
2125 let channels = animation
2126 .get("channels")
2127 .and_then(Value::as_array)
2128 .map(Vec::as_slice)
2129 .unwrap_or_default();
2130 let referenced: BTreeSet<usize> = channels
2131 .iter()
2132 .filter_map(|channel| as_index(channel.get("sampler")))
2133 .collect();
2134 for (sampler_index, sampler) in samplers.iter().enumerate() {
2135 add(as_index(sampler.get("input")), AccessorUse::Dimensionless);
2136 if !referenced.contains(&sampler_index) {
2137 add(as_index(sampler.get("output")), AccessorUse::Dimensionless);
2138 }
2139 }
2140 for channel in channels {
2141 let sampler_index = as_index(channel.get("sampler")).unwrap_or(usize::MAX);
2142 let Some(sampler) = samplers.get(sampler_index) else {
2143 continue;
2144 };
2145 let path = channel
2146 .get("target")
2147 .and_then(|target| target.get("path"))
2148 .and_then(Value::as_str);
2149 add(
2150 as_index(sampler.get("output")),
2151 if path == Some("translation") {
2152 AccessorUse::ScaleBearing
2153 } else {
2154 AccessorUse::Dimensionless
2155 },
2156 );
2157 }
2158 }
2159 }
2160 uses
2161}
2162
2163pub(crate) fn dense_f32_accessor_range(
2171 root: &Map<String, Value>,
2172 buffers: &[Vec<u8>],
2173 accessor_index: usize,
2174) -> Option<(usize, usize, usize)> {
2175 let accessors = root.get("accessors")?.as_array()?;
2176 let accessor = accessors.get(accessor_index)?.as_object()?;
2177 if accessor.get("componentType")?.as_u64()? != 5126
2178 || accessor.get("normalized").and_then(Value::as_bool) == Some(true)
2179 || accessor.contains_key("sparse")
2180 {
2181 return None;
2182 }
2183 let range = accessor_range(root, buffers, accessor_index)?;
2184 if range.stride != range.element_stride || !range.start.is_multiple_of(4) {
2185 return None;
2186 }
2187 Some((range.buffer, range.start, range.end))
2188}
2189
2190pub(crate) fn resolved_accessor_range(
2199 root: &Map<String, Value>,
2200 buffers: &[Vec<u8>],
2201 accessor_index: usize,
2202) -> Option<(usize, usize, usize)> {
2203 accessor_range(root, buffers, accessor_index)
2204 .map(|range| (range.buffer, range.start, range.end))
2205}
2206
2207#[derive(Debug, Clone, Copy)]
2208struct AccessorRange {
2209 buffer: usize,
2210 start: usize,
2211 end: usize,
2212 stride: usize,
2213 element_stride: usize,
2214}
2215
2216fn accessor_range(
2217 root: &Map<String, Value>,
2218 buffers: &[Vec<u8>],
2219 accessor_index: usize,
2220) -> Option<AccessorRange> {
2221 let accessor = root
2222 .get("accessors")?
2223 .as_array()?
2224 .get(accessor_index)?
2225 .as_object()?;
2226 if accessor.contains_key("sparse") {
2227 return None;
2228 }
2229 dense_accessor_range(root, buffers, accessor_index)
2230}
2231
2232fn dense_accessor_range(
2235 root: &Map<String, Value>,
2236 buffers: &[Vec<u8>],
2237 accessor_index: usize,
2238) -> Option<AccessorRange> {
2239 let accessors = root.get("accessors")?.as_array()?;
2240 let buffer_views = root.get("bufferViews")?.as_array()?;
2241 let accessor = accessors.get(accessor_index)?.as_object()?;
2242 let component_size = component_size(accessor.get("componentType")?.as_u64()?)?;
2243 let element_layout = accessor_element_layout(accessor.get("type")?.as_str()?, component_size)?;
2244 let count: usize = accessor.get("count")?.as_u64()?.try_into().ok()?;
2245 if count == 0 {
2246 return None;
2247 }
2248 let view_index = as_index(accessor.get("bufferView"))?;
2249 let view = buffer_views.get(view_index)?.as_object()?;
2250 let buffer_index = as_index(view.get("buffer"))?;
2251 let buffer = buffers.get(buffer_index)?;
2252 let view_offset: usize = view
2253 .get("byteOffset")
2254 .and_then(Value::as_u64)
2255 .unwrap_or(0)
2256 .try_into()
2257 .ok()?;
2258 let view_length: usize = view.get("byteLength")?.as_u64()?.try_into().ok()?;
2259 if view_offset.checked_add(view_length)? > buffer.len() {
2260 return None;
2261 }
2262 let accessor_offset: usize = accessor
2263 .get("byteOffset")
2264 .and_then(Value::as_u64)
2265 .unwrap_or(0)
2266 .try_into()
2267 .ok()?;
2268 let stride: usize = view
2269 .get("byteStride")
2270 .and_then(Value::as_u64)
2271 .unwrap_or(element_layout.stride as u64)
2272 .try_into()
2273 .ok()?;
2274 if stride < element_layout.stride {
2275 return None;
2276 }
2277 let relative_end = accessor_offset
2278 .checked_add(count.checked_sub(1)?.checked_mul(stride)?)?
2279 .checked_add(element_layout.terminal_size)?;
2280 if relative_end > view_length {
2281 return None;
2282 }
2283 let start = view_offset.checked_add(accessor_offset)?;
2284 let end = view_offset.checked_add(relative_end)?;
2285 (end <= buffer.len()).then_some(AccessorRange {
2286 buffer: buffer_index,
2287 start,
2288 end,
2289 stride,
2290 element_stride: element_layout.stride,
2291 })
2292}
2293
2294fn component_size(component_type: u64) -> Option<usize> {
2295 match component_type {
2296 5120 | 5121 => Some(1),
2297 5122 | 5123 => Some(2),
2298 5125 | 5126 => Some(4),
2299 _ => None,
2300 }
2301}
2302
2303#[derive(Debug, Clone, Copy)]
2311struct AccessorElementLayout {
2312 stride: usize,
2313 terminal_size: usize,
2314}
2315
2316fn accessor_element_layout(
2317 accessor_type: &str,
2318 component_size: usize,
2319) -> Option<AccessorElementLayout> {
2320 let (columns, rows, matrix) = match accessor_type {
2321 "SCALAR" => (1usize, 1usize, false),
2322 "VEC2" => (1, 2, false),
2323 "VEC3" => (1, 3, false),
2324 "VEC4" => (1, 4, false),
2325 "MAT2" => (2, 2, true),
2326 "MAT3" => (3, 3, true),
2327 "MAT4" => (4, 4, true),
2328 _ => return None,
2329 };
2330 let column_size = rows.checked_mul(component_size)?;
2331 let stored_column_size = if matrix {
2332 column_size.checked_add(3)? & !3
2333 } else {
2334 column_size
2335 };
2336 let stride = columns.checked_mul(stored_column_size)?;
2337 let terminal_size = columns
2338 .checked_sub(1)?
2339 .checked_mul(stored_column_size)?
2340 .checked_add(column_size)?;
2341 Some(AccessorElementLayout {
2342 stride,
2343 terminal_size,
2344 })
2345}
2346
2347fn inspect_schema_members(
2348 value: &Value,
2349 pointer: &str,
2350 manifest: &mut GltfCapabilityManifest,
2351 violations: &mut Vec<GltfCapabilityViolation>,
2352) {
2353 match value {
2354 Value::Object(object) => {
2355 if object.get("extras").is_some_and(|value| !value.is_null()) {
2356 let location = format!("{pointer}/extras");
2357 manifest.extras_locations.push(location.clone());
2358 violation(violations, GltfCapabilityViolationKind::Extras, location);
2359 }
2360 if let Some(extensions) = object.get("extensions").and_then(Value::as_object) {
2361 for name in extensions.keys() {
2362 let location = json_pointer_child(&format!("{pointer}/extensions"), name);
2363 manifest.extensions.push(name.clone());
2364 manifest.extension_locations.push(location.clone());
2365 violation(
2366 violations,
2367 match name.as_str() {
2368 "KHR_lights_punctual" => GltfCapabilityViolationKind::Light,
2369 "EXT_mesh_gpu_instancing" => GltfCapabilityViolationKind::Instancing,
2370 _ => GltfCapabilityViolationKind::ExtensionPayload,
2371 },
2372 location,
2373 );
2374 }
2375 }
2376 if let Some(allowed) = allowed_members(pointer) {
2377 for key in object.keys() {
2378 if !allowed.contains(&key.as_str()) {
2379 let location = json_pointer_child(pointer, key);
2380 manifest.unknown_member_locations.push(location.clone());
2381 violation(
2382 violations,
2383 GltfCapabilityViolationKind::UnknownJsonMember,
2384 location,
2385 );
2386 }
2387 }
2388 }
2389 for (key, child) in object {
2390 if key == "extras" || key == "extensions" {
2391 continue;
2392 }
2393 inspect_schema_members(
2394 child,
2395 &json_pointer_child(pointer, key),
2396 manifest,
2397 violations,
2398 );
2399 }
2400 }
2401 Value::Array(values) => {
2402 for (index, child) in values.iter().enumerate() {
2403 inspect_schema_members(child, &format!("{pointer}/{index}"), manifest, violations);
2404 }
2405 }
2406 _ => {}
2407 }
2408}
2409
2410fn json_pointer_child(pointer: &str, token: &str) -> String {
2411 format!("{pointer}/{}", json_pointer_token(token))
2412}
2413
2414fn json_pointer_token(token: &str) -> String {
2415 token.replace('~', "~0").replace('/', "~1")
2416}
2417
2418fn allowed_members(pointer: &str) -> Option<&'static [&'static str]> {
2419 const ROOT: &[&str] = &[
2420 "accessors",
2421 "animations",
2422 "asset",
2423 "buffers",
2424 "bufferViews",
2425 "cameras",
2426 "extensions",
2427 "extensionsRequired",
2428 "extensionsUsed",
2429 "extras",
2430 "images",
2431 "materials",
2432 "meshes",
2433 "nodes",
2434 "samplers",
2435 "scene",
2436 "scenes",
2437 "skins",
2438 "textures",
2439 ];
2440 const ASSET: &[&str] = &[
2441 "copyright",
2442 "extensions",
2443 "extras",
2444 "generator",
2445 "minVersion",
2446 "version",
2447 ];
2448 const ACCESSOR: &[&str] = &[
2449 "bufferView",
2450 "byteOffset",
2451 "componentType",
2452 "count",
2453 "extensions",
2454 "extras",
2455 "max",
2456 "min",
2457 "name",
2458 "normalized",
2459 "sparse",
2460 "type",
2461 ];
2462 const BUFFER: &[&str] = &["byteLength", "extensions", "extras", "name", "uri"];
2463 const VIEW: &[&str] = &[
2464 "buffer",
2465 "byteLength",
2466 "byteOffset",
2467 "byteStride",
2468 "extensions",
2469 "extras",
2470 "name",
2471 "target",
2472 ];
2473 const NODE: &[&str] = &[
2474 "camera",
2475 "children",
2476 "extensions",
2477 "extras",
2478 "matrix",
2479 "mesh",
2480 "name",
2481 "rotation",
2482 "scale",
2483 "skin",
2484 "translation",
2485 "weights",
2486 ];
2487 const MESH: &[&str] = &["extensions", "extras", "name", "primitives", "weights"];
2488 const PRIMITIVE: &[&str] = &[
2489 "attributes",
2490 "extensions",
2491 "extras",
2492 "indices",
2493 "material",
2494 "mode",
2495 "targets",
2496 ];
2497 const ANIMATION: &[&str] = &["channels", "extensions", "extras", "name", "samplers"];
2498 const CHANNEL: &[&str] = &["extensions", "extras", "sampler", "target"];
2499 const TARGET: &[&str] = &["extensions", "extras", "node", "path"];
2500 const ANIM_SAMPLER: &[&str] = &["extensions", "extras", "input", "interpolation", "output"];
2501 const SKIN: &[&str] = &[
2502 "extensions",
2503 "extras",
2504 "inverseBindMatrices",
2505 "joints",
2506 "name",
2507 "skeleton",
2508 ];
2509 const SCENE: &[&str] = &["extensions", "extras", "name", "nodes"];
2510 const IMAGE: &[&str] = &[
2511 "bufferView",
2512 "extensions",
2513 "extras",
2514 "mimeType",
2515 "name",
2516 "uri",
2517 ];
2518 const TEXTURE: &[&str] = &["extensions", "extras", "name", "sampler", "source"];
2519 const SAMPLER: &[&str] = &[
2520 "extensions",
2521 "extras",
2522 "magFilter",
2523 "minFilter",
2524 "name",
2525 "wrapS",
2526 "wrapT",
2527 ];
2528 const CAMERA: &[&str] = &[
2529 "extensions",
2530 "extras",
2531 "name",
2532 "orthographic",
2533 "perspective",
2534 "type",
2535 ];
2536 const MATERIAL: &[&str] = &[
2537 "alphaCutoff",
2538 "alphaMode",
2539 "doubleSided",
2540 "emissiveFactor",
2541 "emissiveTexture",
2542 "extensions",
2543 "extras",
2544 "name",
2545 "normalTexture",
2546 "occlusionTexture",
2547 "pbrMetallicRoughness",
2548 ];
2549 const PBR: &[&str] = &[
2550 "baseColorFactor",
2551 "baseColorTexture",
2552 "extensions",
2553 "extras",
2554 "metallicFactor",
2555 "metallicRoughnessTexture",
2556 "roughnessFactor",
2557 ];
2558 const TEXTURE_INFO: &[&str] = &["extensions", "extras", "index", "texCoord"];
2559 const NORMAL_TEXTURE_INFO: &[&str] = &["extensions", "extras", "index", "scale", "texCoord"];
2560 const OCCLUSION_TEXTURE_INFO: &[&str] =
2561 &["extensions", "extras", "index", "strength", "texCoord"];
2562 const PERSPECTIVE: &[&str] = &[
2563 "aspectRatio",
2564 "extensions",
2565 "extras",
2566 "yfov",
2567 "zfar",
2568 "znear",
2569 ];
2570 const ORTHOGRAPHIC: &[&str] = &["extensions", "extras", "xmag", "ymag", "zfar", "znear"];
2571 const SPARSE: &[&str] = &["count", "extensions", "extras", "indices", "values"];
2572 const SPARSE_INDICES: &[&str] = &[
2573 "bufferView",
2574 "byteOffset",
2575 "componentType",
2576 "extensions",
2577 "extras",
2578 ];
2579 const SPARSE_VALUES: &[&str] = &["bufferView", "byteOffset", "extensions", "extras"];
2580 if pointer.is_empty() {
2581 Some(ROOT)
2582 } else if pointer == "/asset" {
2583 Some(ASSET)
2584 } else if indexed_member(pointer, "/accessors/") {
2585 Some(ACCESSOR)
2586 } else if indexed_member(pointer, "/buffers/") {
2587 Some(BUFFER)
2588 } else if indexed_member(pointer, "/bufferViews/") {
2589 Some(VIEW)
2590 } else if indexed_member(pointer, "/nodes/") {
2591 Some(NODE)
2592 } else if indexed_member(pointer, "/meshes/") {
2593 Some(MESH)
2594 } else if indexed_nested_member(pointer, "/meshes/", "/primitives/") {
2595 Some(PRIMITIVE)
2596 } else if indexed_member(pointer, "/animations/") {
2597 Some(ANIMATION)
2598 } else if indexed_nested_member(pointer, "/animations/", "/channels/") {
2599 Some(CHANNEL)
2600 } else if pointer.contains("/animations/") && pointer.ends_with("/target") {
2601 Some(TARGET)
2602 } else if indexed_nested_member(pointer, "/animations/", "/samplers/") {
2603 Some(ANIM_SAMPLER)
2604 } else if indexed_member(pointer, "/skins/") {
2605 Some(SKIN)
2606 } else if indexed_member(pointer, "/scenes/") {
2607 Some(SCENE)
2608 } else if indexed_member(pointer, "/images/") {
2609 Some(IMAGE)
2610 } else if indexed_member(pointer, "/textures/") {
2611 Some(TEXTURE)
2612 } else if indexed_member(pointer, "/samplers/") {
2613 Some(SAMPLER)
2614 } else if indexed_member(pointer, "/cameras/") {
2615 Some(CAMERA)
2616 } else if indexed_member(pointer, "/materials/") {
2617 Some(MATERIAL)
2618 } else if pointer.contains("/materials/") && pointer.ends_with("/pbrMetallicRoughness") {
2619 Some(PBR)
2620 } else if pointer.contains("/materials/")
2621 && (pointer.ends_with("/baseColorTexture")
2622 || pointer.ends_with("/metallicRoughnessTexture")
2623 || pointer.ends_with("/emissiveTexture"))
2624 {
2625 Some(TEXTURE_INFO)
2626 } else if pointer.contains("/materials/") && pointer.ends_with("/normalTexture") {
2627 Some(NORMAL_TEXTURE_INFO)
2628 } else if pointer.contains("/materials/") && pointer.ends_with("/occlusionTexture") {
2629 Some(OCCLUSION_TEXTURE_INFO)
2630 } else if pointer.contains("/cameras/") && pointer.ends_with("/perspective") {
2631 Some(PERSPECTIVE)
2632 } else if pointer.contains("/cameras/") && pointer.ends_with("/orthographic") {
2633 Some(ORTHOGRAPHIC)
2634 } else if pointer.contains("/accessors/") && pointer.ends_with("/sparse") {
2635 Some(SPARSE)
2636 } else if pointer.contains("/accessors/") && pointer.ends_with("/sparse/indices") {
2637 Some(SPARSE_INDICES)
2638 } else if pointer.contains("/accessors/") && pointer.ends_with("/sparse/values") {
2639 Some(SPARSE_VALUES)
2640 } else {
2641 None
2642 }
2643}
2644
2645fn indexed_member(pointer: &str, prefix: &str) -> bool {
2646 pointer
2647 .strip_prefix(prefix)
2648 .is_some_and(|suffix| !suffix.is_empty() && !suffix.contains('/'))
2649}
2650
2651fn indexed_nested_member(pointer: &str, prefix: &str, nested: &str) -> bool {
2652 let Some(suffix) = pointer.strip_prefix(prefix) else {
2653 return false;
2654 };
2655 let Some((outer, inner)) = suffix.split_once(nested) else {
2656 return false;
2657 };
2658 !outer.is_empty() && !outer.contains('/') && !inner.is_empty() && !inner.contains('/')
2659}
2660
2661#[cfg(test)]
2662mod tests {
2663 use super::*;
2664 use serde_json::json;
2665
2666 #[test]
2667 fn accessor_use_inventory_claims_orphan_sampler_fields_without_self_conflicting_channels() {
2668 let root = json!({
2669 "animations": [{
2670 "samplers": [
2671 { "input": 1, "output": 2 },
2672 { "input": 3, "output": 4 }
2673 ],
2674 "channels": [{
2675 "sampler": 0,
2676 "target": { "node": 0, "path": "translation" }
2677 }]
2678 }]
2679 });
2680 let uses = collect_accessor_uses(root.as_object().expect("root"));
2681 assert_eq!(uses[&1], BTreeSet::from([AccessorUse::Dimensionless]));
2682 assert_eq!(uses[&2], BTreeSet::from([AccessorUse::ScaleBearing]));
2683 assert_eq!(uses[&3], BTreeSet::from([AccessorUse::Dimensionless]));
2684 assert_eq!(uses[&4], BTreeSet::from([AccessorUse::Dimensionless]));
2685 }
2686
2687 #[test]
2688 fn raw_scene_attachment_scanner_keeps_non_triangles_shared_and_unreferenced_meshes_separate() {
2689 let root = json!({
2690 "scenes": [{ "nodes": [1, 0] }],
2691 "nodes": [{ "mesh": 0 }, { "mesh": 0 }],
2692 "meshes": [
2693 { "primitives": [
2694 { "mode": 0, "indices": 7 },
2695 { "mode": 1 }
2696 ] },
2697 { "primitives": [{ "mode": 4, "indices": 8 }] },
2698 { "primitives": [{ "mode": 3, "indices": 9 }] }
2699 ]
2700 });
2701 let mut violations = Vec::new();
2702 let (_, parts) = inventory(&root, GltfContainerKind::Gltf, &mut violations);
2703
2704 assert_eq!(parts.scenes.len(), 1);
2705 assert_eq!(parts.scenes[0].root_node_indices(), &[1, 0]);
2706 assert_eq!(parts.attachments.len(), 2);
2707 assert_eq!(parts.attachments[0].source_mesh_index(), 0);
2708 assert_eq!(parts.attachments[1].source_mesh_index(), 0);
2709 assert_eq!(parts.primitives.len(), 4);
2710 assert_eq!(
2711 parts.primitives[0].topology(),
2712 RawPrimitiveTopologyV1::Points
2713 );
2714 assert_eq!(
2715 parts.primitives[1].topology(),
2716 RawPrimitiveTopologyV1::Lines
2717 );
2718 assert_eq!(parts.primitives[3].source_mesh_index(), 2);
2719 assert_eq!(
2720 parts.primitives[3].topology(),
2721 RawPrimitiveTopologyV1::LineStrip
2722 );
2723 assert_eq!(parts.primitives[3].indices_accessor_index(), Some(9));
2724 }
2725
2726 #[test]
2727 fn raw_scene_attachment_scanner_marks_empty_domains_complete() {
2728 let root = json!({ "asset": { "version": "2.0" } });
2729 let (_, parts) = inventory(&root, GltfContainerKind::Gltf, &mut Vec::new());
2730 assert!(parts.scenes.is_empty());
2731 assert!(parts.attachments.is_empty());
2732 assert!(parts.primitives.is_empty());
2733 assert_eq!(
2734 coverage(parts.scene_overflow),
2735 RawSceneAttachmentCoverageV1::Complete
2736 );
2737 assert_eq!(
2738 coverage(parts.attachment_overflow),
2739 RawSceneAttachmentCoverageV1::Complete
2740 );
2741 assert_eq!(
2742 coverage(parts.primitive_overflow),
2743 RawSceneAttachmentCoverageV1::Complete
2744 );
2745 }
2746
2747 #[test]
2748 fn normal_load_attaches_raw_empty_mesh_evidence_without_inferring_from_assets() {
2749 let bytes = br#"{
2750 "asset": { "version": "2.0" },
2751 "scenes": [{ "nodes": [0] }],
2752 "nodes": [{ "mesh": 0 }, { "mesh": 0 }],
2753 "meshes": [{ "primitives": [] }]
2754 }"#;
2755 let source = crate::load_source_bytes(Path::new("empty-mesh.gltf"), bytes).unwrap();
2756 let inventory = source.raw_scene_attachment_inventory().unwrap();
2757 assert_eq!(
2758 inventory.primary_input(),
2759 source.source_facts().primary_identity()
2760 );
2761 assert_eq!(inventory.scenes().rows()[0].root_node_indices(), &[0]);
2762 assert_eq!(inventory.node_mesh_attachments().rows().len(), 2);
2763 assert!(inventory.mesh_primitives().rows().is_empty());
2764 assert!(inventory.mesh_primitives().coverage().proves_absence());
2765 }
2766
2767 #[test]
2768 fn raw_scene_attachment_scanner_retains_a_bounded_primitive_prefix() {
2769 let primitives = (0..=RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS)
2770 .map(|_| json!({ "mode": 4 }))
2771 .collect::<Vec<_>>();
2772 let root = json!({ "meshes": [{ "primitives": primitives }] });
2773 let (_, parts) = inventory(&root, GltfContainerKind::Gltf, &mut Vec::new());
2774 assert_eq!(
2775 parts.primitives.len(),
2776 RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS
2777 );
2778 assert!(parts.primitive_overflow);
2779 assert_eq!(
2780 parts.primitive_decode_attempts,
2781 RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS + 1,
2782 "the raw sidecar stops decoding after its prefix plus one overflow witness"
2783 );
2784 assert_eq!(
2785 coverage(parts.primitive_overflow),
2786 RawSceneAttachmentCoverageV1::PrefixOverflow
2787 );
2788 }
2789
2790 #[test]
2791 fn raw_scene_attachment_scanner_never_retains_scenes_after_prefix_overflow() {
2792 let oversized_roots = (0..RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS)
2793 .map(|index| json!(index))
2794 .collect::<Vec<_>>();
2795 let root = json!({
2796 "scenes": [
2797 { "nodes": oversized_roots },
2798 { "nodes": [] }
2799 ]
2800 });
2801 let (_, parts) = inventory(&root, GltfContainerKind::Gltf, &mut Vec::new());
2802 assert!(parts.scenes.is_empty());
2803 assert!(parts.scene_overflow);
2804 }
2805}