1use crate::{
9 LoadError, load_source_bytes, resolve_buffers, topology, validate_animations,
10 validate_document, validate_glb_framing,
11};
12use animsmith_core::{Document, LoadedSource, SourceFactsViewV1};
13use serde::Serialize;
14use serde_json::{Map, Value};
15use std::collections::{BTreeMap, BTreeSet};
16use std::path::Path;
17
18const GLB_MAGIC: &[u8; 4] = b"glTF";
19const GLB_JSON_CHUNK: u32 = 0x4e4f_534a;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
23#[serde(rename_all = "snake_case")]
24pub enum GltfContainerKind {
25 Gltf,
27 Glb,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
33#[serde(rename_all = "snake_case")]
34pub enum GltfBufferSourceKind {
35 BinaryChunk,
37 DataUri,
39 External,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
45pub struct GltfBufferCapability {
46 pub buffer_index: usize,
48 pub source_kind: GltfBufferSourceKind,
50 pub declared_byte_length: u64,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
56#[serde(rename_all = "snake_case")]
57pub enum GltfNodeRestKind {
58 Trs,
60 Matrix,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
66pub struct GltfNodeCapability {
67 pub node_index: usize,
69 pub rest_kind: GltfNodeRestKind,
71 pub mesh_index: Option<usize>,
73 pub skin_index: Option<usize>,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
79pub struct GltfAnimationChannelCapability {
80 pub animation_index: usize,
82 pub channel_index: usize,
84 pub target_node_index: usize,
86 pub target_path: String,
88 pub interpolation: String,
90 pub input_accessor_index: usize,
92 pub output_accessor_index: usize,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
98pub struct GltfAttributeCapability {
99 pub semantic: String,
101 pub accessor_index: usize,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
107pub struct GltfPrimitiveCapability {
108 pub mesh_index: usize,
110 pub primitive_index: usize,
112 pub mode: u64,
114 pub attributes: Vec<GltfAttributeCapability>,
116 pub morph_target_count: usize,
118 pub morph_position_accessors: Vec<usize>,
120 #[serde(skip)]
122 pub unsupported_morph_locations: Vec<String>,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
127pub struct GltfInstancingCapability {
128 pub node_index: usize,
130 pub attributes: Vec<GltfAttributeCapability>,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
136pub struct GltfAccessorCapability {
137 pub accessor_index: usize,
139 pub buffer_view_index: Option<usize>,
141 pub byte_offset: u64,
143 pub component_type: u64,
145 pub accessor_type: String,
147 pub count: u64,
149 pub normalized: bool,
151 pub sparse: bool,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
157pub struct GltfBufferViewCapability {
158 pub buffer_view_index: usize,
160 pub buffer_index: usize,
162 pub byte_offset: u64,
164 pub byte_length: u64,
166 pub byte_stride: Option<u64>,
168}
169
170#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
172pub struct GltfSkinCapability {
173 pub skin_index: usize,
175 pub joint_count: usize,
177 pub inverse_bind_accessor_index: Option<usize>,
179 pub inverse_bind_count: Option<u64>,
181}
182
183#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
185pub struct GltfCapabilityManifest {
186 pub container: GltfContainerKind,
188 pub buffers: Vec<GltfBufferCapability>,
190 pub buffer_views: Vec<GltfBufferViewCapability>,
192 pub accessors: Vec<GltfAccessorCapability>,
194 pub nodes: Vec<GltfNodeCapability>,
196 pub animation_channels: Vec<GltfAnimationChannelCapability>,
198 pub primitives: Vec<GltfPrimitiveCapability>,
200 pub morph_weight_locations: Vec<String>,
202 pub instancing: Vec<GltfInstancingCapability>,
204 pub skins: Vec<GltfSkinCapability>,
206 pub camera_count: usize,
208 pub extensions: Vec<String>,
210 pub extension_locations: Vec<String>,
212 pub external_resource_locations: Vec<String>,
214 pub extras_locations: Vec<String>,
216 pub unknown_member_locations: Vec<String>,
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
222#[non_exhaustive]
223#[serde(rename_all = "snake_case")]
224pub enum GltfCapabilityViolationKind {
225 ExternalResource,
227 MorphTarget,
229 MorphWeights,
231 Camera,
233 Light,
235 Instancing,
237 ExtensionDeclaration,
239 ExtensionPayload,
241 Extras,
243 UnknownJsonMember,
245 NonTrianglePrimitive,
247 UnsupportedVertexAttribute,
249 SecondarySkinInfluences,
251 MissingInverseBinds,
253 EmptyInverseBindAccessor,
255 InverseBindCountMismatch,
257 UnreadableInverseBinds,
259 UnsafeAccessorLayout,
261 ConflictingAccessorUse,
263 OverlappingAccessorRanges,
267 ConflictingNodeTransform,
270 NonAffineNodeMatrix,
273 AnimatedMatrixNode,
277 ImagePayloadOverlap,
279}
280
281#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
283pub struct GltfCapabilityViolation {
284 pub location: String,
286 pub kind: GltfCapabilityViolationKind,
288}
289
290#[derive(Debug)]
295pub struct GltfScaleSource {
296 loaded_source: LoadedSource,
297 #[cfg(test)]
298 document_override: Option<Document>,
299 manifest: GltfCapabilityManifest,
300 source_bytes: Vec<u8>,
301 raw_json: Value,
302 resolved_buffers: Vec<Vec<u8>>,
303}
304
305impl GltfScaleSource {
306 pub fn document(&self) -> &Document {
308 #[cfg(test)]
309 if let Some(document) = self.document_override.as_ref() {
310 return document;
311 }
312 self.loaded_source.document()
313 }
314
315 pub fn source_facts(&self) -> SourceFactsViewV1<'_> {
317 self.loaded_source.source_facts()
318 }
319
320 pub fn manifest(&self) -> &GltfCapabilityManifest {
322 &self.manifest
323 }
324
325 pub fn source_bytes(&self) -> &[u8] {
327 &self.source_bytes
328 }
329
330 pub fn raw_json(&self) -> &Value {
332 &self.raw_json
333 }
334
335 pub fn resolved_buffers(&self) -> &[Vec<u8>] {
337 &self.resolved_buffers
338 }
339}
340
341#[derive(Debug, thiserror::Error)]
343#[non_exhaustive]
344pub enum GltfScalePreflightError {
345 #[error(transparent)]
347 Load(#[from] LoadError),
348 #[error("glTF scale preflight rejected {count} unsupported source domain(s)")]
350 Unsupported {
351 manifest: Box<GltfCapabilityManifest>,
353 violations: Vec<GltfCapabilityViolation>,
355 count: usize,
357 },
358}
359
360pub fn preflight_scale_source(path: &Path) -> Result<GltfScaleSource, GltfScalePreflightError> {
368 let bytes = std::fs::read(path).map_err(|source| LoadError::Io {
369 path: path.display().to_string(),
370 source,
371 })?;
372 preflight_scale_source_bytes(path, &bytes)
373}
374
375pub fn preflight_scale_source_bytes(
386 path: &Path,
387 bytes: &[u8],
388) -> Result<GltfScaleSource, GltfScalePreflightError> {
389 capture_scale_source(path, bytes, GatePolicy::Enforce)
390}
391
392#[derive(Debug, Clone, Copy, PartialEq, Eq)]
394enum GatePolicy {
395 Enforce,
398 #[cfg(test)]
411 Bypass,
412}
413
414fn capture_scale_source(
416 path: &Path,
417 bytes: &[u8],
418 policy: GatePolicy,
419) -> Result<GltfScaleSource, GltfScalePreflightError> {
420 validate_glb_framing(bytes)?;
421 let (container, json_bytes) = raw_json_bytes(bytes)?;
422 let raw_json: Value = serde_json::from_slice(json_bytes)
423 .map_err(|error| LoadError::Malformed(format!("invalid top-level JSON: {error}")))?;
424 if !raw_json.is_object() {
425 return Err(LoadError::Malformed("top-level glTF JSON is not an object".into()).into());
426 }
427 let gltf = gltf::Gltf::from_slice_without_validation(bytes).map_err(LoadError::Gltf)?;
428
429 let mut violations = Vec::new();
430 let manifest = inventory(&raw_json, container, &mut violations);
431 let accessor_uses = inspect_accessor_uses(&raw_json, &mut violations);
432 match validate_document(&gltf.document) {
433 Ok(()) => {}
434 Err(error) => return Err(LoadError::Gltf(error).into()),
435 }
436 validate_animations(&gltf.document)?;
437 topology(&gltf.document)?;
438
439 let can_resolve_buffers = !manifest
440 .buffers
441 .iter()
442 .any(|buffer| buffer.source_kind == GltfBufferSourceKind::External);
443 let resolved_buffers = if can_resolve_buffers {
444 resolve_buffers(&gltf, path.parent())?
445 } else {
446 Vec::new()
447 };
448 if can_resolve_buffers {
449 inspect_accessor_layouts(
450 &raw_json,
451 &resolved_buffers,
452 &accessor_uses,
453 &mut violations,
454 );
455 }
456 violations.sort();
457 violations.dedup();
458 let refuse = match policy {
459 GatePolicy::Enforce => !violations.is_empty(),
460 #[cfg(test)]
461 GatePolicy::Bypass => false,
462 };
463 if refuse {
464 let count = violations.len();
465 return Err(GltfScalePreflightError::Unsupported {
466 manifest: Box::new(manifest),
467 violations,
468 count,
469 });
470 }
471
472 let loaded_source = load_source_bytes(path, bytes)?;
473 Ok(GltfScaleSource {
474 loaded_source,
475 #[cfg(test)]
476 document_override: None,
477 manifest,
478 source_bytes: bytes.to_vec(),
479 raw_json,
480 resolved_buffers,
481 })
482}
483
484#[cfg(test)]
495pub(crate) fn scale_source_past_the_gate(
496 path: &Path,
497 bytes: &[u8],
498) -> Result<GltfScaleSource, GltfScalePreflightError> {
499 capture_scale_source(path, bytes, GatePolicy::Bypass)
500}
501
502#[cfg(test)]
520pub(crate) fn scale_source_with_document(
521 mut source: GltfScaleSource,
522 document: Document,
523) -> GltfScaleSource {
524 source.document_override = Some(document);
525 source
526}
527
528pub(crate) fn raw_json_bytes(bytes: &[u8]) -> Result<(GltfContainerKind, &[u8]), LoadError> {
533 if !bytes.starts_with(GLB_MAGIC) {
534 return Ok((GltfContainerKind::Gltf, bytes));
535 }
536 let chunk_length = bytes
537 .get(12..16)
538 .and_then(|slice| slice.try_into().ok())
539 .map(u32::from_le_bytes)
540 .ok_or_else(|| LoadError::Buffer("malformed GLB JSON chunk header".into()))?
541 as usize;
542 let chunk_type = bytes
543 .get(16..20)
544 .and_then(|slice| slice.try_into().ok())
545 .map(u32::from_le_bytes)
546 .ok_or_else(|| LoadError::Buffer("malformed GLB JSON chunk header".into()))?;
547 if chunk_type != GLB_JSON_CHUNK {
548 return Err(LoadError::Buffer(
549 "GLB first chunk is not a JSON chunk".into(),
550 ));
551 }
552 let end = 20usize
553 .checked_add(chunk_length)
554 .ok_or_else(|| LoadError::Buffer("GLB JSON chunk range overflow".into()))?;
555 let json = bytes
556 .get(20..end)
557 .ok_or_else(|| LoadError::Buffer("malformed GLB JSON chunk length".into()))?;
558 Ok((GltfContainerKind::Glb, json))
559}
560
561fn violation(
562 violations: &mut Vec<GltfCapabilityViolation>,
563 kind: GltfCapabilityViolationKind,
564 location: impl Into<String>,
565) {
566 violations.push(GltfCapabilityViolation {
567 kind,
568 location: location.into(),
569 });
570}
571
572fn as_index(value: Option<&Value>) -> Option<usize> {
573 value?.as_u64()?.try_into().ok()
574}
575
576fn inventory(
577 root: &Value,
578 container: GltfContainerKind,
579 violations: &mut Vec<GltfCapabilityViolation>,
580) -> GltfCapabilityManifest {
581 let Some(object) = root.as_object() else {
582 return GltfCapabilityManifest {
583 container,
584 buffers: Vec::new(),
585 buffer_views: Vec::new(),
586 accessors: Vec::new(),
587 nodes: Vec::new(),
588 animation_channels: Vec::new(),
589 primitives: Vec::new(),
590 morph_weight_locations: Vec::new(),
591 instancing: Vec::new(),
592 skins: Vec::new(),
593 camera_count: 0,
594 extensions: Vec::new(),
595 extension_locations: Vec::new(),
596 external_resource_locations: Vec::new(),
597 extras_locations: Vec::new(),
598 unknown_member_locations: Vec::new(),
599 };
600 };
601 let mut manifest = GltfCapabilityManifest {
602 container,
603 buffers: Vec::new(),
604 buffer_views: Vec::new(),
605 accessors: Vec::new(),
606 nodes: Vec::new(),
607 animation_channels: Vec::new(),
608 primitives: Vec::new(),
609 morph_weight_locations: Vec::new(),
610 instancing: Vec::new(),
611 skins: Vec::new(),
612 camera_count: object
613 .get("cameras")
614 .and_then(Value::as_array)
615 .map_or(0, Vec::len),
616 extensions: Vec::new(),
617 extension_locations: Vec::new(),
618 external_resource_locations: Vec::new(),
619 extras_locations: Vec::new(),
620 unknown_member_locations: Vec::new(),
621 };
622
623 inspect_schema_members(root, "", &mut manifest, violations);
624 inventory_extensions(object, &mut manifest, violations);
625 inventory_buffers(object, container, &mut manifest, violations);
626 inventory_buffer_views_and_accessors(object, &mut manifest);
627 inventory_nodes(object, &mut manifest, violations);
628 inventory_animations(object, &mut manifest, violations);
629 inventory_meshes(object, &mut manifest, violations);
630 inventory_skins(object, &mut manifest, violations);
631
632 if manifest.camera_count > 0 {
633 violation(violations, GltfCapabilityViolationKind::Camera, "/cameras");
634 }
635 manifest.extensions.sort();
636 manifest.extensions.dedup();
637 manifest.extension_locations.sort();
638 manifest.extension_locations.dedup();
639 manifest.external_resource_locations.sort();
640 manifest.external_resource_locations.dedup();
641 manifest.extras_locations.sort();
642 manifest.extras_locations.dedup();
643 manifest.unknown_member_locations.sort();
644 manifest.unknown_member_locations.dedup();
645 manifest.morph_weight_locations.sort();
646 manifest.morph_weight_locations.dedup();
647 manifest
648}
649
650fn inventory_extensions(
651 root: &Map<String, Value>,
652 manifest: &mut GltfCapabilityManifest,
653 violations: &mut Vec<GltfCapabilityViolation>,
654) {
655 for key in ["extensionsUsed", "extensionsRequired"] {
656 let Some(values) = root.get(key).and_then(Value::as_array) else {
657 continue;
658 };
659 for (index, value) in values.iter().enumerate() {
660 let Some(name) = value.as_str() else { continue };
661 manifest.extensions.push(name.to_owned());
662 let kind = match name {
663 "KHR_lights_punctual" => GltfCapabilityViolationKind::Light,
664 "EXT_mesh_gpu_instancing" => GltfCapabilityViolationKind::Instancing,
665 _ => GltfCapabilityViolationKind::ExtensionDeclaration,
666 };
667 violation(violations, kind, format!("/{key}/{index}"));
668 }
669 }
670}
671
672fn inventory_buffers(
673 root: &Map<String, Value>,
674 container: GltfContainerKind,
675 manifest: &mut GltfCapabilityManifest,
676 violations: &mut Vec<GltfCapabilityViolation>,
677) {
678 let Some(buffers) = root.get("buffers").and_then(Value::as_array) else {
679 return;
680 };
681 for (buffer_index, buffer) in buffers.iter().enumerate() {
682 let Some(buffer) = buffer.as_object() else {
683 continue;
684 };
685 let uri = buffer.get("uri").and_then(Value::as_str);
686 let source_kind = match uri {
687 Some(uri) if uri.starts_with("data:") => GltfBufferSourceKind::DataUri,
688 Some(_) => GltfBufferSourceKind::External,
689 None if container == GltfContainerKind::Glb => GltfBufferSourceKind::BinaryChunk,
690 None => GltfBufferSourceKind::External,
691 };
692 if source_kind == GltfBufferSourceKind::External {
693 manifest
694 .external_resource_locations
695 .push(format!("/buffers/{buffer_index}/uri"));
696 violation(
697 violations,
698 GltfCapabilityViolationKind::ExternalResource,
699 format!("/buffers/{buffer_index}/uri"),
700 );
701 }
702 manifest.buffers.push(GltfBufferCapability {
703 buffer_index,
704 source_kind,
705 declared_byte_length: buffer
706 .get("byteLength")
707 .and_then(Value::as_u64)
708 .unwrap_or(0),
709 });
710 }
711 if let Some(images) = root.get("images").and_then(Value::as_array) {
712 for (image_index, image) in images.iter().enumerate() {
713 if image
714 .get("uri")
715 .and_then(Value::as_str)
716 .is_some_and(|uri| !uri.starts_with("data:"))
717 {
718 manifest
719 .external_resource_locations
720 .push(format!("/images/{image_index}/uri"));
721 violation(
722 violations,
723 GltfCapabilityViolationKind::ExternalResource,
724 format!("/images/{image_index}/uri"),
725 );
726 }
727 }
728 }
729}
730
731fn inventory_buffer_views_and_accessors(
732 root: &Map<String, Value>,
733 manifest: &mut GltfCapabilityManifest,
734) {
735 if let Some(buffer_views) = root.get("bufferViews").and_then(Value::as_array) {
736 for (buffer_view_index, view) in buffer_views.iter().enumerate() {
737 let Some(view) = view.as_object() else {
738 continue;
739 };
740 manifest.buffer_views.push(GltfBufferViewCapability {
741 buffer_view_index,
742 buffer_index: as_index(view.get("buffer")).unwrap_or(usize::MAX),
743 byte_offset: view.get("byteOffset").and_then(Value::as_u64).unwrap_or(0),
744 byte_length: view.get("byteLength").and_then(Value::as_u64).unwrap_or(0),
745 byte_stride: view.get("byteStride").and_then(Value::as_u64),
746 });
747 }
748 }
749 if let Some(accessors) = root.get("accessors").and_then(Value::as_array) {
750 for (accessor_index, accessor) in accessors.iter().enumerate() {
751 let Some(accessor) = accessor.as_object() else {
752 continue;
753 };
754 manifest.accessors.push(GltfAccessorCapability {
755 accessor_index,
756 buffer_view_index: as_index(accessor.get("bufferView")),
757 byte_offset: accessor
758 .get("byteOffset")
759 .and_then(Value::as_u64)
760 .unwrap_or(0),
761 component_type: accessor
762 .get("componentType")
763 .and_then(Value::as_u64)
764 .unwrap_or(0),
765 accessor_type: accessor
766 .get("type")
767 .and_then(Value::as_str)
768 .unwrap_or_default()
769 .to_owned(),
770 count: accessor.get("count").and_then(Value::as_u64).unwrap_or(0),
771 normalized: accessor
772 .get("normalized")
773 .and_then(Value::as_bool)
774 .unwrap_or(false),
775 sparse: accessor.contains_key("sparse"),
776 });
777 }
778 }
779}
780
781pub(crate) const AFFINE_LAST_ROW: [(usize, f64); 4] = [(3, 0.0), (7, 0.0), (11, 0.0), (15, 1.0)];
789
790#[derive(Debug, Clone, Copy, PartialEq)]
792pub(crate) enum NodeTransformFault {
793 TrsBesideMatrix {
795 node_index: usize,
797 member: &'static str,
799 },
800 ProjectiveMatrixEntry {
802 node_index: usize,
804 component: usize,
806 value: f64,
808 expected: f64,
810 },
811 UnreadableMatrixEntry {
814 node_index: usize,
816 component: usize,
818 },
819}
820
821impl NodeTransformFault {
822 pub(crate) fn location(self) -> String {
824 match self {
825 Self::TrsBesideMatrix { node_index, member } => format!("/nodes/{node_index}/{member}"),
826 Self::ProjectiveMatrixEntry {
827 node_index,
828 component,
829 ..
830 }
831 | Self::UnreadableMatrixEntry {
832 node_index,
833 component,
834 } => format!("/nodes/{node_index}/matrix/{component}"),
835 }
836 }
837
838 fn kind(self) -> GltfCapabilityViolationKind {
840 match self {
841 Self::TrsBesideMatrix { .. } => GltfCapabilityViolationKind::ConflictingNodeTransform,
842 Self::ProjectiveMatrixEntry { .. } | Self::UnreadableMatrixEntry { .. } => {
845 GltfCapabilityViolationKind::NonAffineNodeMatrix
846 }
847 }
848 }
849}
850
851pub(crate) fn declared<'a>(object: &'a Value, member: &str) -> Option<&'a Value> {
870 object.get(member).filter(|value| !value.is_null())
871}
872
873pub(crate) fn node_transform_faults(nodes: &[Value]) -> Vec<NodeTransformFault> {
896 let mut faults = Vec::new();
897 for (node_index, node) in nodes.iter().enumerate() {
898 let Some(matrix) = declared(node, "matrix") else {
899 continue;
900 };
901 for member in ["translation", "rotation", "scale"] {
902 if declared(node, member).is_some() {
903 faults.push(NodeTransformFault::TrsBesideMatrix { node_index, member });
904 }
905 }
906 let Some(values) = matrix.as_array().filter(|values| values.len() == 16) else {
907 continue;
908 };
909 for (component, expected) in AFFINE_LAST_ROW {
910 match values[component].as_f64() {
911 None => faults.push(NodeTransformFault::UnreadableMatrixEntry {
912 node_index,
913 component,
914 }),
915 Some(value) if value != expected => {
916 faults.push(NodeTransformFault::ProjectiveMatrixEntry {
917 node_index,
918 component,
919 value,
920 expected,
921 });
922 }
923 Some(_) => {}
924 }
925 }
926 }
927 faults
928}
929
930fn inventory_nodes(
931 root: &Map<String, Value>,
932 manifest: &mut GltfCapabilityManifest,
933 violations: &mut Vec<GltfCapabilityViolation>,
934) {
935 let Some(nodes) = root.get("nodes").and_then(Value::as_array) else {
936 return;
937 };
938 for fault in node_transform_faults(nodes) {
939 violation(violations, fault.kind(), fault.location());
940 }
941 for (node_index, node) in nodes.iter().enumerate() {
942 if !node.is_object() {
943 continue;
944 }
945 if node.get("weights").is_some() {
951 manifest
952 .morph_weight_locations
953 .push(format!("/nodes/{node_index}/weights"));
954 }
955 if node.get("camera").is_some() {
956 violation(
957 violations,
958 GltfCapabilityViolationKind::Camera,
959 format!("/nodes/{node_index}/camera"),
960 );
961 }
962 if let Some(attributes) = node
963 .get("extensions")
964 .and_then(|extensions| extensions.get("EXT_mesh_gpu_instancing"))
965 .and_then(|extension| extension.get("attributes"))
966 .and_then(Value::as_object)
967 {
968 let mut attributes = attributes
969 .iter()
970 .map(|(semantic, accessor)| GltfAttributeCapability {
971 semantic: semantic.clone(),
972 accessor_index: as_index(Some(accessor)).unwrap_or(usize::MAX),
973 })
974 .collect::<Vec<_>>();
975 attributes.sort_by(|left, right| left.semantic.cmp(&right.semantic));
976 manifest.instancing.push(GltfInstancingCapability {
977 node_index,
978 attributes,
979 });
980 }
981 manifest.nodes.push(GltfNodeCapability {
982 node_index,
983 rest_kind: if declared(node, "matrix").is_some() {
986 GltfNodeRestKind::Matrix
987 } else {
988 GltfNodeRestKind::Trs
989 },
990 mesh_index: as_index(node.get("mesh")),
991 skin_index: as_index(node.get("skin")),
992 });
993 }
994}
995
996fn inventory_animations(
997 root: &Map<String, Value>,
998 manifest: &mut GltfCapabilityManifest,
999 violations: &mut Vec<GltfCapabilityViolation>,
1000) {
1001 let Some(animations) = root.get("animations").and_then(Value::as_array) else {
1002 return;
1003 };
1004 for (animation_index, animation) in animations.iter().enumerate() {
1005 let Some(animation) = animation.as_object() else {
1006 continue;
1007 };
1008 let samplers = animation
1009 .get("samplers")
1010 .and_then(Value::as_array)
1011 .map(Vec::as_slice)
1012 .unwrap_or_default();
1013 let channels = animation
1014 .get("channels")
1015 .and_then(Value::as_array)
1016 .map(Vec::as_slice)
1017 .unwrap_or_default();
1018 for (channel_index, channel) in channels.iter().enumerate() {
1019 let Some(channel) = channel.as_object() else {
1020 continue;
1021 };
1022 let sampler_index = as_index(channel.get("sampler")).unwrap_or(usize::MAX);
1023 let Some(sampler) = samplers.get(sampler_index).and_then(Value::as_object) else {
1024 continue;
1025 };
1026 let Some(target) = channel.get("target").and_then(Value::as_object) else {
1027 continue;
1028 };
1029 let target_path = target
1030 .get("path")
1031 .and_then(Value::as_str)
1032 .unwrap_or_default()
1033 .to_owned();
1034 if target_path == "weights" {
1035 manifest.morph_weight_locations.push(format!(
1036 "/animations/{animation_index}/channels/{channel_index}/target/path"
1037 ));
1038 }
1039 let target_node_index = as_index(target.get("node")).unwrap_or(usize::MAX);
1040 if manifest
1041 .nodes
1042 .get(target_node_index)
1043 .is_some_and(|node| node.rest_kind == GltfNodeRestKind::Matrix)
1044 {
1045 violation(
1046 violations,
1047 GltfCapabilityViolationKind::AnimatedMatrixNode,
1048 format!("/animations/{animation_index}/channels/{channel_index}/target"),
1049 );
1050 }
1051 manifest
1052 .animation_channels
1053 .push(GltfAnimationChannelCapability {
1054 animation_index,
1055 channel_index,
1056 target_node_index,
1057 target_path,
1058 interpolation: sampler
1059 .get("interpolation")
1060 .and_then(Value::as_str)
1061 .unwrap_or("LINEAR")
1062 .to_owned(),
1063 input_accessor_index: as_index(sampler.get("input")).unwrap_or(usize::MAX),
1064 output_accessor_index: as_index(sampler.get("output")).unwrap_or(usize::MAX),
1065 });
1066 }
1067 }
1068}
1069
1070fn inventory_meshes(
1071 root: &Map<String, Value>,
1072 manifest: &mut GltfCapabilityManifest,
1073 violations: &mut Vec<GltfCapabilityViolation>,
1074) {
1075 let Some(meshes) = root.get("meshes").and_then(Value::as_array) else {
1076 return;
1077 };
1078 for (mesh_index, mesh) in meshes.iter().enumerate() {
1079 let Some(mesh) = mesh.as_object() else {
1080 continue;
1081 };
1082 if mesh.contains_key("weights") {
1083 manifest
1084 .morph_weight_locations
1085 .push(format!("/meshes/{mesh_index}/weights"));
1086 }
1087 let primitives = mesh
1088 .get("primitives")
1089 .and_then(Value::as_array)
1090 .map(Vec::as_slice)
1091 .unwrap_or_default();
1092 for (primitive_index, primitive) in primitives.iter().enumerate() {
1093 let Some(primitive) = primitive.as_object() else {
1094 continue;
1095 };
1096 let mode = primitive.get("mode").and_then(Value::as_u64).unwrap_or(4);
1097 if mode != 4 {
1098 violation(
1099 violations,
1100 GltfCapabilityViolationKind::NonTrianglePrimitive,
1101 format!("/meshes/{mesh_index}/primitives/{primitive_index}/mode"),
1102 );
1103 }
1104 let mut attributes = primitive
1105 .get("attributes")
1106 .and_then(Value::as_object)
1107 .map(|attributes| {
1108 attributes
1109 .iter()
1110 .map(|(semantic, accessor)| GltfAttributeCapability {
1111 semantic: semantic.clone(),
1112 accessor_index: as_index(Some(accessor)).unwrap_or(usize::MAX),
1113 })
1114 .collect::<Vec<_>>()
1115 })
1116 .unwrap_or_default();
1117 attributes.sort_by(|left, right| left.semantic.cmp(&right.semantic));
1118 for attribute in &attributes {
1119 let semantic = &attribute.semantic;
1120 let semantic_pointer = json_pointer_token(semantic);
1121 let location = format!(
1122 "/meshes/{mesh_index}/primitives/{primitive_index}/attributes/{semantic_pointer}"
1123 );
1124 if is_secondary_influence(semantic) {
1125 violation(
1126 violations,
1127 GltfCapabilityViolationKind::SecondarySkinInfluences,
1128 location,
1129 );
1130 } else if !matches!(
1131 semantic.as_str(),
1132 "POSITION" | "NORMAL" | "TEXCOORD_0" | "JOINTS_0" | "WEIGHTS_0"
1133 ) {
1134 violation(
1135 violations,
1136 GltfCapabilityViolationKind::UnsupportedVertexAttribute,
1137 location,
1138 );
1139 }
1140 }
1141 let morph_target_count = primitive
1142 .get("targets")
1143 .and_then(Value::as_array)
1144 .map_or(0, Vec::len);
1145 let mut morph_position_accessors = Vec::new();
1146 let mut unsupported_morph_locations = Vec::new();
1147 for (target_index, target) in primitive
1148 .get("targets")
1149 .and_then(Value::as_array)
1150 .into_iter()
1151 .flatten()
1152 .enumerate()
1153 {
1154 let Some(target) = target.as_object() else {
1155 continue;
1156 };
1157 for (semantic, accessor) in target {
1158 let location = format!(
1159 "/meshes/{mesh_index}/primitives/{primitive_index}/targets/{target_index}/{}",
1160 json_pointer_token(semantic)
1161 );
1162 if semantic == "POSITION" {
1163 if let Some(accessor_index) = as_index(Some(accessor)) {
1164 morph_position_accessors.push(accessor_index);
1165 }
1166 } else {
1167 violation(
1168 violations,
1169 GltfCapabilityViolationKind::MorphTarget,
1170 location.clone(),
1171 );
1172 unsupported_morph_locations.push(location);
1173 }
1174 }
1175 }
1176 manifest.primitives.push(GltfPrimitiveCapability {
1177 mesh_index,
1178 primitive_index,
1179 mode,
1180 attributes,
1181 morph_target_count,
1182 morph_position_accessors,
1183 unsupported_morph_locations,
1184 });
1185 }
1186 }
1187}
1188
1189fn is_secondary_influence(semantic: &str) -> bool {
1190 semantic
1191 .strip_prefix("JOINTS_")
1192 .or_else(|| semantic.strip_prefix("WEIGHTS_"))
1193 .and_then(|index| index.parse::<u32>().ok())
1194 .is_some_and(|index| index >= 1)
1195}
1196
1197fn inventory_skins(
1198 root: &Map<String, Value>,
1199 manifest: &mut GltfCapabilityManifest,
1200 violations: &mut Vec<GltfCapabilityViolation>,
1201) {
1202 let accessors = root
1203 .get("accessors")
1204 .and_then(Value::as_array)
1205 .map(Vec::as_slice)
1206 .unwrap_or_default();
1207 let Some(skins) = root.get("skins").and_then(Value::as_array) else {
1208 return;
1209 };
1210 for (skin_index, skin) in skins.iter().enumerate() {
1211 let Some(skin) = skin.as_object() else {
1212 continue;
1213 };
1214 let joint_count = skin
1215 .get("joints")
1216 .and_then(Value::as_array)
1217 .map_or(0, Vec::len);
1218 let inverse_bind_accessor_index = as_index(skin.get("inverseBindMatrices"));
1219 let inverse_bind_count = inverse_bind_accessor_index
1220 .and_then(|index| accessors.get(index))
1221 .and_then(|accessor| accessor.get("count"))
1222 .and_then(Value::as_u64);
1223 let inverse_bind_readable = inverse_bind_accessor_index
1224 .and_then(|index| accessors.get(index))
1225 .and_then(Value::as_object)
1226 .is_some_and(|accessor| {
1227 accessor.get("bufferView").and_then(Value::as_u64).is_some()
1228 && accessor.get("componentType").and_then(Value::as_u64) == Some(5126)
1229 && accessor.get("type").and_then(Value::as_str) == Some("MAT4")
1230 && !accessor.contains_key("sparse")
1231 });
1232 match (inverse_bind_accessor_index, inverse_bind_count) {
1233 (None, _) => violation(
1234 violations,
1235 GltfCapabilityViolationKind::MissingInverseBinds,
1236 format!("/skins/{skin_index}/inverseBindMatrices"),
1237 ),
1238 (Some(_), Some(0)) => violation(
1239 violations,
1240 GltfCapabilityViolationKind::EmptyInverseBindAccessor,
1241 format!("/skins/{skin_index}/inverseBindMatrices"),
1242 ),
1243 (Some(_), Some(count)) if count != joint_count as u64 => violation(
1244 violations,
1245 GltfCapabilityViolationKind::InverseBindCountMismatch,
1246 format!("/skins/{skin_index}/inverseBindMatrices"),
1247 ),
1248 (Some(_), _) if !inverse_bind_readable => violation(
1249 violations,
1250 GltfCapabilityViolationKind::UnreadableInverseBinds,
1251 format!("/skins/{skin_index}/inverseBindMatrices"),
1252 ),
1253 _ => {}
1254 }
1255 manifest.skins.push(GltfSkinCapability {
1256 skin_index,
1257 joint_count,
1258 inverse_bind_accessor_index,
1259 inverse_bind_count,
1260 });
1261 }
1262}
1263
1264#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1265enum AccessorUse {
1266 ScaleBearing,
1267 Dimensionless,
1268}
1269
1270#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1272enum RangeOwner {
1273 Accessor(usize),
1275 SparseIndices(usize),
1277 SparseValues(usize),
1279 ImagePayload(usize),
1281}
1282
1283impl RangeOwner {
1284 fn location(self) -> String {
1286 match self {
1287 Self::Accessor(index) => format!("/accessors/{index}"),
1288 Self::SparseIndices(index) => {
1289 format!("/accessors/{index}/sparse/indices/bufferView")
1290 }
1291 Self::SparseValues(index) => {
1292 format!("/accessors/{index}/sparse/values/bufferView")
1293 }
1294 Self::ImagePayload(index) => format!("/images/{index}/bufferView"),
1295 }
1296 }
1297
1298 fn overlap_kind(self) -> GltfCapabilityViolationKind {
1301 match self {
1302 Self::Accessor(_) | Self::SparseIndices(_) | Self::SparseValues(_) => {
1303 GltfCapabilityViolationKind::OverlappingAccessorRanges
1304 }
1305 Self::ImagePayload(_) => GltfCapabilityViolationKind::ImagePayloadOverlap,
1306 }
1307 }
1308}
1309
1310type OwnedRange = (usize, usize, usize, RangeOwner, bool);
1312
1313fn inspect_accessor_layouts(
1314 root: &Value,
1315 buffers: &[Vec<u8>],
1316 uses: &BTreeMap<usize, BTreeSet<AccessorUse>>,
1317 violations: &mut Vec<GltfCapabilityViolation>,
1318) {
1319 let Some(root) = root.as_object() else { return };
1320 let mut ranges: Vec<OwnedRange> = Vec::new();
1321 let accessors = root
1322 .get("accessors")
1323 .and_then(Value::as_array)
1324 .map(Vec::as_slice)
1325 .unwrap_or_default();
1326 for accessor_index in 0..accessors.len() {
1327 let accessor_uses = uses.get(&accessor_index);
1328 let scale_bearing =
1329 accessor_uses.is_some_and(|uses| uses.contains(&AccessorUse::ScaleBearing));
1330 let accessor_ranges = if scale_bearing {
1331 dense_f32_accessor_range(root, buffers, accessor_index).map(|range| {
1332 vec![(
1333 range.0,
1334 range.1,
1335 range.2,
1336 RangeOwner::Accessor(accessor_index),
1337 true,
1338 )]
1339 })
1340 } else if accessor_uses.is_some() {
1341 accessor_range(root, buffers, accessor_index).map(|range| {
1342 vec![(
1343 range.buffer,
1344 range.start,
1345 range.end,
1346 RangeOwner::Accessor(accessor_index),
1347 false,
1348 )]
1349 })
1350 } else {
1351 preserved_accessor_ranges(root, buffers, accessor_index)
1352 };
1353 match accessor_ranges {
1354 Some(accessor_ranges) => ranges.extend(accessor_ranges),
1355 None => violation(
1356 violations,
1357 GltfCapabilityViolationKind::UnsafeAccessorLayout,
1358 format!("/accessors/{accessor_index}"),
1359 ),
1360 }
1361 }
1362 ranges.extend(image_payload_ranges(root));
1363 ranges.sort_unstable();
1364
1365 let mut overlapping = BTreeSet::new();
1366 let mut prior_scale: Option<(usize, usize, RangeOwner)> = None;
1367 for &(buffer, start, end, owner, scale_bearing) in &ranges {
1368 if let Some((left_buffer, left_end, left_owner)) = prior_scale
1369 && left_buffer == buffer
1370 && start < left_end
1371 {
1372 overlapping.insert(left_owner);
1373 overlapping.insert(owner);
1374 }
1375 if scale_bearing
1376 && prior_scale
1377 .is_none_or(|(left_buffer, left_end, _)| left_buffer != buffer || end > left_end)
1378 {
1379 prior_scale = Some((buffer, end, owner));
1380 }
1381 }
1382 let mut later_scale: Option<(usize, usize, RangeOwner)> = None;
1383 for &(buffer, start, end, owner, scale_bearing) in ranges.iter().rev() {
1384 if let Some((right_buffer, right_start, right_owner)) = later_scale
1385 && right_buffer == buffer
1386 && right_start < end
1387 {
1388 overlapping.insert(owner);
1389 overlapping.insert(right_owner);
1390 }
1391 if scale_bearing
1392 && later_scale.is_none_or(|(right_buffer, right_start, _)| {
1393 right_buffer != buffer || start < right_start
1394 })
1395 {
1396 later_scale = Some((buffer, start, owner));
1397 }
1398 }
1399 for owner in overlapping {
1400 violation(violations, owner.overlap_kind(), owner.location());
1401 }
1402}
1403
1404fn preserved_accessor_ranges(
1413 root: &Map<String, Value>,
1414 buffers: &[Vec<u8>],
1415 accessor_index: usize,
1416) -> Option<Vec<OwnedRange>> {
1417 let accessor = root
1418 .get("accessors")?
1419 .as_array()?
1420 .get(accessor_index)?
1421 .as_object()?;
1422 let count: usize = accessor.get("count")?.as_u64()?.try_into().ok()?;
1423 if count == 0 {
1424 return Some(Vec::new());
1425 }
1426 let Some(sparse) = accessor.get("sparse") else {
1427 let range = accessor_range(root, buffers, accessor_index)?;
1428 return Some(vec![(
1429 range.buffer,
1430 range.start,
1431 range.end,
1432 RangeOwner::Accessor(accessor_index),
1433 false,
1434 )]);
1435 };
1436
1437 let mut ranges = Vec::with_capacity(3);
1438 if accessor.get("bufferView").is_some() {
1439 let range = dense_accessor_range(root, buffers, accessor_index)?;
1440 ranges.push((
1441 range.buffer,
1442 range.start,
1443 range.end,
1444 RangeOwner::Accessor(accessor_index),
1445 false,
1446 ));
1447 }
1448
1449 let sparse = sparse.as_object()?;
1450 let sparse_count: usize = sparse.get("count")?.as_u64()?.try_into().ok()?;
1451 if sparse_count == 0 {
1452 return Some(ranges);
1453 }
1454 let indices = sparse.get("indices")?.as_object()?;
1455 let index_size = match indices.get("componentType")?.as_u64()? {
1456 5121 => 1,
1457 5123 => 2,
1458 5125 => 4,
1459 _ => return None,
1460 };
1461 let indices_range = packed_view_range(
1462 root,
1463 buffers,
1464 as_index(indices.get("bufferView"))?,
1465 indices
1466 .get("byteOffset")
1467 .and_then(Value::as_u64)
1468 .unwrap_or(0),
1469 sparse_count,
1470 index_size,
1471 index_size,
1472 )?;
1473 ranges.push((
1474 indices_range.0,
1475 indices_range.1,
1476 indices_range.2,
1477 RangeOwner::SparseIndices(accessor_index),
1478 false,
1479 ));
1480
1481 let values = sparse.get("values")?.as_object()?;
1482 let component_size = component_size(accessor.get("componentType")?.as_u64()?)?;
1483 let element_layout = accessor_element_layout(accessor.get("type")?.as_str()?, component_size)?;
1484 let values_range = packed_view_range(
1485 root,
1486 buffers,
1487 as_index(values.get("bufferView"))?,
1488 values
1489 .get("byteOffset")
1490 .and_then(Value::as_u64)
1491 .unwrap_or(0),
1492 sparse_count,
1493 element_layout.stride,
1494 element_layout.terminal_size,
1495 )?;
1496 ranges.push((
1497 values_range.0,
1498 values_range.1,
1499 values_range.2,
1500 RangeOwner::SparseValues(accessor_index),
1501 false,
1502 ));
1503 Some(ranges)
1504}
1505
1506fn packed_view_range(
1508 root: &Map<String, Value>,
1509 buffers: &[Vec<u8>],
1510 view_index: usize,
1511 relative_offset: u64,
1512 count: usize,
1513 element_stride: usize,
1514 terminal_size: usize,
1515) -> Option<(usize, usize, usize)> {
1516 let view = root
1517 .get("bufferViews")?
1518 .as_array()?
1519 .get(view_index)?
1520 .as_object()?;
1521 let buffer_index = as_index(view.get("buffer"))?;
1522 let buffer = buffers.get(buffer_index)?;
1523 let view_offset: usize = view
1524 .get("byteOffset")
1525 .and_then(Value::as_u64)
1526 .unwrap_or(0)
1527 .try_into()
1528 .ok()?;
1529 let view_length: usize = view.get("byteLength")?.as_u64()?.try_into().ok()?;
1530 if view_offset.checked_add(view_length)? > buffer.len() {
1531 return None;
1532 }
1533 let relative_offset: usize = relative_offset.try_into().ok()?;
1534 let relative_end = relative_offset
1535 .checked_add(count.checked_sub(1)?.checked_mul(element_stride)?)?
1536 .checked_add(terminal_size)?;
1537 if relative_end > view_length {
1538 return None;
1539 }
1540 let start = view_offset.checked_add(relative_offset)?;
1541 let end = view_offset.checked_add(relative_end)?;
1542 Some((buffer_index, start, end))
1543}
1544
1545fn image_payload_ranges(root: &Map<String, Value>) -> Vec<OwnedRange> {
1572 let Some(images) = root.get("images").and_then(Value::as_array) else {
1573 return Vec::new();
1574 };
1575 let buffer_views = root
1576 .get("bufferViews")
1577 .and_then(Value::as_array)
1578 .map(Vec::as_slice)
1579 .unwrap_or_default();
1580 let mut out = Vec::new();
1581 for (image_index, image) in images.iter().enumerate() {
1582 let Some(view_index) = as_index(image.get("bufferView")) else {
1583 continue;
1584 };
1585 let Some(view) = buffer_views.get(view_index).and_then(Value::as_object) else {
1588 continue;
1589 };
1590 let Some(buffer) = as_index(view.get("buffer")) else {
1591 continue;
1592 };
1593 let start = clamped_usize(view.get("byteOffset").and_then(Value::as_u64).unwrap_or(0));
1594 let end = start.saturating_add(clamped_usize(
1595 view.get("byteLength").and_then(Value::as_u64).unwrap_or(0),
1596 ));
1597 if start < end {
1602 out.push((
1603 buffer,
1604 start,
1605 end,
1606 RangeOwner::ImagePayload(image_index),
1607 false,
1608 ));
1609 }
1610 }
1611 out
1612}
1613
1614fn clamped_usize(value: u64) -> usize {
1615 usize::try_from(value).unwrap_or(usize::MAX)
1616}
1617
1618fn inspect_accessor_uses(
1619 root: &Value,
1620 violations: &mut Vec<GltfCapabilityViolation>,
1621) -> BTreeMap<usize, BTreeSet<AccessorUse>> {
1622 let Some(root) = root.as_object() else {
1623 return BTreeMap::new();
1624 };
1625 let uses = collect_accessor_uses(root);
1626 for (accessor_index, accessor_uses) in &uses {
1627 if accessor_uses.len() > 1 {
1628 violation(
1629 violations,
1630 GltfCapabilityViolationKind::ConflictingAccessorUse,
1631 format!("/accessors/{accessor_index}"),
1632 );
1633 }
1634 }
1635 uses
1636}
1637
1638fn collect_accessor_uses(root: &Map<String, Value>) -> BTreeMap<usize, BTreeSet<AccessorUse>> {
1639 let mut uses: BTreeMap<usize, BTreeSet<AccessorUse>> = BTreeMap::new();
1640 let mut add = |index: Option<usize>, kind| {
1641 if let Some(index) = index {
1642 uses.entry(index).or_default().insert(kind);
1643 }
1644 };
1645 if let Some(meshes) = root.get("meshes").and_then(Value::as_array) {
1646 for mesh in meshes {
1647 let Some(primitives) = mesh.get("primitives").and_then(Value::as_array) else {
1648 continue;
1649 };
1650 for primitive in primitives {
1651 if let Some(attributes) = primitive.get("attributes").and_then(Value::as_object) {
1652 for (semantic, index) in attributes {
1653 add(
1654 as_index(Some(index)),
1655 if semantic == "POSITION" {
1656 AccessorUse::ScaleBearing
1657 } else {
1658 AccessorUse::Dimensionless
1659 },
1660 );
1661 }
1662 }
1663 add(
1664 as_index(primitive.get("indices")),
1665 AccessorUse::Dimensionless,
1666 );
1667 if let Some(targets) = primitive.get("targets").and_then(Value::as_array) {
1668 for target in targets {
1669 if let Some(target) = target.as_object() {
1670 for (semantic, index) in target {
1671 add(
1672 as_index(Some(index)),
1673 if semantic == "POSITION" {
1674 AccessorUse::ScaleBearing
1675 } else {
1676 AccessorUse::Dimensionless
1677 },
1678 );
1679 }
1680 }
1681 }
1682 }
1683 }
1684 }
1685 }
1686 if let Some(skins) = root.get("skins").and_then(Value::as_array) {
1687 for skin in skins {
1688 add(
1689 as_index(skin.get("inverseBindMatrices")),
1690 AccessorUse::ScaleBearing,
1691 );
1692 }
1693 }
1694 if let Some(animations) = root.get("animations").and_then(Value::as_array) {
1695 for animation in animations {
1696 let samplers = animation
1697 .get("samplers")
1698 .and_then(Value::as_array)
1699 .map(Vec::as_slice)
1700 .unwrap_or_default();
1701 let channels = animation
1702 .get("channels")
1703 .and_then(Value::as_array)
1704 .map(Vec::as_slice)
1705 .unwrap_or_default();
1706 let referenced: BTreeSet<usize> = channels
1707 .iter()
1708 .filter_map(|channel| as_index(channel.get("sampler")))
1709 .collect();
1710 for (sampler_index, sampler) in samplers.iter().enumerate() {
1711 add(as_index(sampler.get("input")), AccessorUse::Dimensionless);
1712 if !referenced.contains(&sampler_index) {
1713 add(as_index(sampler.get("output")), AccessorUse::Dimensionless);
1714 }
1715 }
1716 for channel in channels {
1717 let sampler_index = as_index(channel.get("sampler")).unwrap_or(usize::MAX);
1718 let Some(sampler) = samplers.get(sampler_index) else {
1719 continue;
1720 };
1721 let path = channel
1722 .get("target")
1723 .and_then(|target| target.get("path"))
1724 .and_then(Value::as_str);
1725 add(
1726 as_index(sampler.get("output")),
1727 if path == Some("translation") {
1728 AccessorUse::ScaleBearing
1729 } else {
1730 AccessorUse::Dimensionless
1731 },
1732 );
1733 }
1734 }
1735 }
1736 uses
1737}
1738
1739pub(crate) fn dense_f32_accessor_range(
1747 root: &Map<String, Value>,
1748 buffers: &[Vec<u8>],
1749 accessor_index: usize,
1750) -> Option<(usize, usize, usize)> {
1751 let accessors = root.get("accessors")?.as_array()?;
1752 let accessor = accessors.get(accessor_index)?.as_object()?;
1753 if accessor.get("componentType")?.as_u64()? != 5126
1754 || accessor.get("normalized").and_then(Value::as_bool) == Some(true)
1755 || accessor.contains_key("sparse")
1756 {
1757 return None;
1758 }
1759 let range = accessor_range(root, buffers, accessor_index)?;
1760 if range.stride != range.element_stride || !range.start.is_multiple_of(4) {
1761 return None;
1762 }
1763 Some((range.buffer, range.start, range.end))
1764}
1765
1766pub(crate) fn resolved_accessor_range(
1775 root: &Map<String, Value>,
1776 buffers: &[Vec<u8>],
1777 accessor_index: usize,
1778) -> Option<(usize, usize, usize)> {
1779 accessor_range(root, buffers, accessor_index)
1780 .map(|range| (range.buffer, range.start, range.end))
1781}
1782
1783#[derive(Debug, Clone, Copy)]
1784struct AccessorRange {
1785 buffer: usize,
1786 start: usize,
1787 end: usize,
1788 stride: usize,
1789 element_stride: usize,
1790}
1791
1792fn accessor_range(
1793 root: &Map<String, Value>,
1794 buffers: &[Vec<u8>],
1795 accessor_index: usize,
1796) -> Option<AccessorRange> {
1797 let accessor = root
1798 .get("accessors")?
1799 .as_array()?
1800 .get(accessor_index)?
1801 .as_object()?;
1802 if accessor.contains_key("sparse") {
1803 return None;
1804 }
1805 dense_accessor_range(root, buffers, accessor_index)
1806}
1807
1808fn dense_accessor_range(
1811 root: &Map<String, Value>,
1812 buffers: &[Vec<u8>],
1813 accessor_index: usize,
1814) -> Option<AccessorRange> {
1815 let accessors = root.get("accessors")?.as_array()?;
1816 let buffer_views = root.get("bufferViews")?.as_array()?;
1817 let accessor = accessors.get(accessor_index)?.as_object()?;
1818 let component_size = component_size(accessor.get("componentType")?.as_u64()?)?;
1819 let element_layout = accessor_element_layout(accessor.get("type")?.as_str()?, component_size)?;
1820 let count: usize = accessor.get("count")?.as_u64()?.try_into().ok()?;
1821 if count == 0 {
1822 return None;
1823 }
1824 let view_index = as_index(accessor.get("bufferView"))?;
1825 let view = buffer_views.get(view_index)?.as_object()?;
1826 let buffer_index = as_index(view.get("buffer"))?;
1827 let buffer = buffers.get(buffer_index)?;
1828 let view_offset: usize = view
1829 .get("byteOffset")
1830 .and_then(Value::as_u64)
1831 .unwrap_or(0)
1832 .try_into()
1833 .ok()?;
1834 let view_length: usize = view.get("byteLength")?.as_u64()?.try_into().ok()?;
1835 if view_offset.checked_add(view_length)? > buffer.len() {
1836 return None;
1837 }
1838 let accessor_offset: usize = accessor
1839 .get("byteOffset")
1840 .and_then(Value::as_u64)
1841 .unwrap_or(0)
1842 .try_into()
1843 .ok()?;
1844 let stride: usize = view
1845 .get("byteStride")
1846 .and_then(Value::as_u64)
1847 .unwrap_or(element_layout.stride as u64)
1848 .try_into()
1849 .ok()?;
1850 if stride < element_layout.stride {
1851 return None;
1852 }
1853 let relative_end = accessor_offset
1854 .checked_add(count.checked_sub(1)?.checked_mul(stride)?)?
1855 .checked_add(element_layout.terminal_size)?;
1856 if relative_end > view_length {
1857 return None;
1858 }
1859 let start = view_offset.checked_add(accessor_offset)?;
1860 let end = view_offset.checked_add(relative_end)?;
1861 (end <= buffer.len()).then_some(AccessorRange {
1862 buffer: buffer_index,
1863 start,
1864 end,
1865 stride,
1866 element_stride: element_layout.stride,
1867 })
1868}
1869
1870fn component_size(component_type: u64) -> Option<usize> {
1871 match component_type {
1872 5120 | 5121 => Some(1),
1873 5122 | 5123 => Some(2),
1874 5125 | 5126 => Some(4),
1875 _ => None,
1876 }
1877}
1878
1879#[derive(Debug, Clone, Copy)]
1887struct AccessorElementLayout {
1888 stride: usize,
1889 terminal_size: usize,
1890}
1891
1892fn accessor_element_layout(
1893 accessor_type: &str,
1894 component_size: usize,
1895) -> Option<AccessorElementLayout> {
1896 let (columns, rows, matrix) = match accessor_type {
1897 "SCALAR" => (1usize, 1usize, false),
1898 "VEC2" => (1, 2, false),
1899 "VEC3" => (1, 3, false),
1900 "VEC4" => (1, 4, false),
1901 "MAT2" => (2, 2, true),
1902 "MAT3" => (3, 3, true),
1903 "MAT4" => (4, 4, true),
1904 _ => return None,
1905 };
1906 let column_size = rows.checked_mul(component_size)?;
1907 let stored_column_size = if matrix {
1908 column_size.checked_add(3)? & !3
1909 } else {
1910 column_size
1911 };
1912 let stride = columns.checked_mul(stored_column_size)?;
1913 let terminal_size = columns
1914 .checked_sub(1)?
1915 .checked_mul(stored_column_size)?
1916 .checked_add(column_size)?;
1917 Some(AccessorElementLayout {
1918 stride,
1919 terminal_size,
1920 })
1921}
1922
1923fn inspect_schema_members(
1924 value: &Value,
1925 pointer: &str,
1926 manifest: &mut GltfCapabilityManifest,
1927 violations: &mut Vec<GltfCapabilityViolation>,
1928) {
1929 match value {
1930 Value::Object(object) => {
1931 if object.get("extras").is_some_and(|value| !value.is_null()) {
1932 let location = format!("{pointer}/extras");
1933 manifest.extras_locations.push(location.clone());
1934 violation(violations, GltfCapabilityViolationKind::Extras, location);
1935 }
1936 if let Some(extensions) = object.get("extensions").and_then(Value::as_object) {
1937 for name in extensions.keys() {
1938 let location = json_pointer_child(&format!("{pointer}/extensions"), name);
1939 manifest.extensions.push(name.clone());
1940 manifest.extension_locations.push(location.clone());
1941 violation(
1942 violations,
1943 match name.as_str() {
1944 "KHR_lights_punctual" => GltfCapabilityViolationKind::Light,
1945 "EXT_mesh_gpu_instancing" => GltfCapabilityViolationKind::Instancing,
1946 _ => GltfCapabilityViolationKind::ExtensionPayload,
1947 },
1948 location,
1949 );
1950 }
1951 }
1952 if let Some(allowed) = allowed_members(pointer) {
1953 for key in object.keys() {
1954 if !allowed.contains(&key.as_str()) {
1955 let location = json_pointer_child(pointer, key);
1956 manifest.unknown_member_locations.push(location.clone());
1957 violation(
1958 violations,
1959 GltfCapabilityViolationKind::UnknownJsonMember,
1960 location,
1961 );
1962 }
1963 }
1964 }
1965 for (key, child) in object {
1966 if key == "extras" || key == "extensions" {
1967 continue;
1968 }
1969 inspect_schema_members(
1970 child,
1971 &json_pointer_child(pointer, key),
1972 manifest,
1973 violations,
1974 );
1975 }
1976 }
1977 Value::Array(values) => {
1978 for (index, child) in values.iter().enumerate() {
1979 inspect_schema_members(child, &format!("{pointer}/{index}"), manifest, violations);
1980 }
1981 }
1982 _ => {}
1983 }
1984}
1985
1986fn json_pointer_child(pointer: &str, token: &str) -> String {
1987 format!("{pointer}/{}", json_pointer_token(token))
1988}
1989
1990fn json_pointer_token(token: &str) -> String {
1991 token.replace('~', "~0").replace('/', "~1")
1992}
1993
1994fn allowed_members(pointer: &str) -> Option<&'static [&'static str]> {
1995 const ROOT: &[&str] = &[
1996 "accessors",
1997 "animations",
1998 "asset",
1999 "buffers",
2000 "bufferViews",
2001 "cameras",
2002 "extensions",
2003 "extensionsRequired",
2004 "extensionsUsed",
2005 "extras",
2006 "images",
2007 "materials",
2008 "meshes",
2009 "nodes",
2010 "samplers",
2011 "scene",
2012 "scenes",
2013 "skins",
2014 "textures",
2015 ];
2016 const ASSET: &[&str] = &[
2017 "copyright",
2018 "extensions",
2019 "extras",
2020 "generator",
2021 "minVersion",
2022 "version",
2023 ];
2024 const ACCESSOR: &[&str] = &[
2025 "bufferView",
2026 "byteOffset",
2027 "componentType",
2028 "count",
2029 "extensions",
2030 "extras",
2031 "max",
2032 "min",
2033 "name",
2034 "normalized",
2035 "sparse",
2036 "type",
2037 ];
2038 const BUFFER: &[&str] = &["byteLength", "extensions", "extras", "name", "uri"];
2039 const VIEW: &[&str] = &[
2040 "buffer",
2041 "byteLength",
2042 "byteOffset",
2043 "byteStride",
2044 "extensions",
2045 "extras",
2046 "name",
2047 "target",
2048 ];
2049 const NODE: &[&str] = &[
2050 "camera",
2051 "children",
2052 "extensions",
2053 "extras",
2054 "matrix",
2055 "mesh",
2056 "name",
2057 "rotation",
2058 "scale",
2059 "skin",
2060 "translation",
2061 "weights",
2062 ];
2063 const MESH: &[&str] = &["extensions", "extras", "name", "primitives", "weights"];
2064 const PRIMITIVE: &[&str] = &[
2065 "attributes",
2066 "extensions",
2067 "extras",
2068 "indices",
2069 "material",
2070 "mode",
2071 "targets",
2072 ];
2073 const ANIMATION: &[&str] = &["channels", "extensions", "extras", "name", "samplers"];
2074 const CHANNEL: &[&str] = &["extensions", "extras", "sampler", "target"];
2075 const TARGET: &[&str] = &["extensions", "extras", "node", "path"];
2076 const ANIM_SAMPLER: &[&str] = &["extensions", "extras", "input", "interpolation", "output"];
2077 const SKIN: &[&str] = &[
2078 "extensions",
2079 "extras",
2080 "inverseBindMatrices",
2081 "joints",
2082 "name",
2083 "skeleton",
2084 ];
2085 const SCENE: &[&str] = &["extensions", "extras", "name", "nodes"];
2086 const IMAGE: &[&str] = &[
2087 "bufferView",
2088 "extensions",
2089 "extras",
2090 "mimeType",
2091 "name",
2092 "uri",
2093 ];
2094 const TEXTURE: &[&str] = &["extensions", "extras", "name", "sampler", "source"];
2095 const SAMPLER: &[&str] = &[
2096 "extensions",
2097 "extras",
2098 "magFilter",
2099 "minFilter",
2100 "name",
2101 "wrapS",
2102 "wrapT",
2103 ];
2104 const CAMERA: &[&str] = &[
2105 "extensions",
2106 "extras",
2107 "name",
2108 "orthographic",
2109 "perspective",
2110 "type",
2111 ];
2112 const MATERIAL: &[&str] = &[
2113 "alphaCutoff",
2114 "alphaMode",
2115 "doubleSided",
2116 "emissiveFactor",
2117 "emissiveTexture",
2118 "extensions",
2119 "extras",
2120 "name",
2121 "normalTexture",
2122 "occlusionTexture",
2123 "pbrMetallicRoughness",
2124 ];
2125 const PBR: &[&str] = &[
2126 "baseColorFactor",
2127 "baseColorTexture",
2128 "extensions",
2129 "extras",
2130 "metallicFactor",
2131 "metallicRoughnessTexture",
2132 "roughnessFactor",
2133 ];
2134 const TEXTURE_INFO: &[&str] = &["extensions", "extras", "index", "texCoord"];
2135 const NORMAL_TEXTURE_INFO: &[&str] = &["extensions", "extras", "index", "scale", "texCoord"];
2136 const OCCLUSION_TEXTURE_INFO: &[&str] =
2137 &["extensions", "extras", "index", "strength", "texCoord"];
2138 const PERSPECTIVE: &[&str] = &[
2139 "aspectRatio",
2140 "extensions",
2141 "extras",
2142 "yfov",
2143 "zfar",
2144 "znear",
2145 ];
2146 const ORTHOGRAPHIC: &[&str] = &["extensions", "extras", "xmag", "ymag", "zfar", "znear"];
2147 const SPARSE: &[&str] = &["count", "extensions", "extras", "indices", "values"];
2148 const SPARSE_INDICES: &[&str] = &[
2149 "bufferView",
2150 "byteOffset",
2151 "componentType",
2152 "extensions",
2153 "extras",
2154 ];
2155 const SPARSE_VALUES: &[&str] = &["bufferView", "byteOffset", "extensions", "extras"];
2156 if pointer.is_empty() {
2157 Some(ROOT)
2158 } else if pointer == "/asset" {
2159 Some(ASSET)
2160 } else if indexed_member(pointer, "/accessors/") {
2161 Some(ACCESSOR)
2162 } else if indexed_member(pointer, "/buffers/") {
2163 Some(BUFFER)
2164 } else if indexed_member(pointer, "/bufferViews/") {
2165 Some(VIEW)
2166 } else if indexed_member(pointer, "/nodes/") {
2167 Some(NODE)
2168 } else if indexed_member(pointer, "/meshes/") {
2169 Some(MESH)
2170 } else if indexed_nested_member(pointer, "/meshes/", "/primitives/") {
2171 Some(PRIMITIVE)
2172 } else if indexed_member(pointer, "/animations/") {
2173 Some(ANIMATION)
2174 } else if indexed_nested_member(pointer, "/animations/", "/channels/") {
2175 Some(CHANNEL)
2176 } else if pointer.contains("/animations/") && pointer.ends_with("/target") {
2177 Some(TARGET)
2178 } else if indexed_nested_member(pointer, "/animations/", "/samplers/") {
2179 Some(ANIM_SAMPLER)
2180 } else if indexed_member(pointer, "/skins/") {
2181 Some(SKIN)
2182 } else if indexed_member(pointer, "/scenes/") {
2183 Some(SCENE)
2184 } else if indexed_member(pointer, "/images/") {
2185 Some(IMAGE)
2186 } else if indexed_member(pointer, "/textures/") {
2187 Some(TEXTURE)
2188 } else if indexed_member(pointer, "/samplers/") {
2189 Some(SAMPLER)
2190 } else if indexed_member(pointer, "/cameras/") {
2191 Some(CAMERA)
2192 } else if indexed_member(pointer, "/materials/") {
2193 Some(MATERIAL)
2194 } else if pointer.contains("/materials/") && pointer.ends_with("/pbrMetallicRoughness") {
2195 Some(PBR)
2196 } else if pointer.contains("/materials/")
2197 && (pointer.ends_with("/baseColorTexture")
2198 || pointer.ends_with("/metallicRoughnessTexture")
2199 || pointer.ends_with("/emissiveTexture"))
2200 {
2201 Some(TEXTURE_INFO)
2202 } else if pointer.contains("/materials/") && pointer.ends_with("/normalTexture") {
2203 Some(NORMAL_TEXTURE_INFO)
2204 } else if pointer.contains("/materials/") && pointer.ends_with("/occlusionTexture") {
2205 Some(OCCLUSION_TEXTURE_INFO)
2206 } else if pointer.contains("/cameras/") && pointer.ends_with("/perspective") {
2207 Some(PERSPECTIVE)
2208 } else if pointer.contains("/cameras/") && pointer.ends_with("/orthographic") {
2209 Some(ORTHOGRAPHIC)
2210 } else if pointer.contains("/accessors/") && pointer.ends_with("/sparse") {
2211 Some(SPARSE)
2212 } else if pointer.contains("/accessors/") && pointer.ends_with("/sparse/indices") {
2213 Some(SPARSE_INDICES)
2214 } else if pointer.contains("/accessors/") && pointer.ends_with("/sparse/values") {
2215 Some(SPARSE_VALUES)
2216 } else {
2217 None
2218 }
2219}
2220
2221fn indexed_member(pointer: &str, prefix: &str) -> bool {
2222 pointer
2223 .strip_prefix(prefix)
2224 .is_some_and(|suffix| !suffix.is_empty() && !suffix.contains('/'))
2225}
2226
2227fn indexed_nested_member(pointer: &str, prefix: &str, nested: &str) -> bool {
2228 let Some(suffix) = pointer.strip_prefix(prefix) else {
2229 return false;
2230 };
2231 let Some((outer, inner)) = suffix.split_once(nested) else {
2232 return false;
2233 };
2234 !outer.is_empty() && !outer.contains('/') && !inner.is_empty() && !inner.contains('/')
2235}
2236
2237#[cfg(test)]
2238mod tests {
2239 use super::*;
2240 use serde_json::json;
2241
2242 #[test]
2243 fn accessor_use_inventory_claims_orphan_sampler_fields_without_self_conflicting_channels() {
2244 let root = json!({
2245 "animations": [{
2246 "samplers": [
2247 { "input": 1, "output": 2 },
2248 { "input": 3, "output": 4 }
2249 ],
2250 "channels": [{
2251 "sampler": 0,
2252 "target": { "node": 0, "path": "translation" }
2253 }]
2254 }]
2255 });
2256 let uses = collect_accessor_uses(root.as_object().expect("root"));
2257 assert_eq!(uses[&1], BTreeSet::from([AccessorUse::Dimensionless]));
2258 assert_eq!(uses[&2], BTreeSet::from([AccessorUse::ScaleBearing]));
2259 assert_eq!(uses[&3], BTreeSet::from([AccessorUse::Dimensionless]));
2260 assert_eq!(uses[&4], BTreeSet::from([AccessorUse::Dimensionless]));
2261 }
2262}