1use crate::bounded_deserialize::{
9 CappedSequence, deserialize_capped_option_string, deserialize_capped_sequence,
10};
11use crate::{DependencyClosureV1, InputIdentity};
12use serde::de::Error as _;
13use serde::{Deserialize, Deserializer, Serialize};
14use std::collections::BTreeSet;
15use std::io::Read;
16
17pub const RAW_GLTF_ADDRESSABILITY_INVENTORY_V1_ID: &str =
19 "urn:animsmith:raw-gltf-addressability-inventory:1";
20pub const RAW_GLTF_ADDRESSABILITY_V1_MAX_ROWS_PER_DOMAIN: usize = 4_096;
22pub const RAW_GLTF_ADDRESSABILITY_V1_MAX_STRUCTURAL_REFERENCES: usize = 65_536;
24pub const RAW_GLTF_ADDRESSABILITY_V1_MAX_NAME_BYTES: usize = 1_024;
26pub const RAW_GLTF_ADDRESSABILITY_V1_MAX_PATH_SEGMENTS: usize = 256;
28pub const RAW_GLTF_ADDRESSABILITY_V1_MAX_PATH_BYTES: usize = 4_096;
30pub const RAW_GLTF_ADDRESSABILITY_V1_MAX_TEXT_BYTES: usize = 1024 * 1024;
32pub const RAW_GLTF_ADDRESSABILITY_V1_MAX_READER_BYTES: u64 = 256 * 1024 * 1024;
34
35fn deserialize_name<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
36where
37 D: Deserializer<'de>,
38{
39 deserialize_capped_option_string(deserializer, RAW_GLTF_ADDRESSABILITY_V1_MAX_NAME_BYTES)
40}
41
42fn deserialize_rows<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
43where
44 D: Deserializer<'de>,
45 T: Deserialize<'de>,
46{
47 let rows: CappedSequence<T> =
48 deserialize_capped_sequence(deserializer, RAW_GLTF_ADDRESSABILITY_V1_MAX_ROWS_PER_DOMAIN)?;
49 if rows.overflowed {
50 return Err(D::Error::custom(
51 "raw glTF addressability domain exceeded its row bound",
52 ));
53 }
54 Ok(rows.values)
55}
56
57fn deserialize_scene_rows<'de, D>(deserializer: D) -> Result<Vec<RawGltfSceneRowV1>, D::Error>
58where
59 D: Deserializer<'de>,
60{
61 deserialize_rows(deserializer)
62}
63
64fn deserialize_node_rows<'de, D>(deserializer: D) -> Result<Vec<RawGltfNodeRowV1>, D::Error>
65where
66 D: Deserializer<'de>,
67{
68 deserialize_rows(deserializer)
69}
70
71fn deserialize_skin_rows<'de, D>(deserializer: D) -> Result<Vec<RawGltfSkinRowV1>, D::Error>
72where
73 D: Deserializer<'de>,
74{
75 deserialize_rows(deserializer)
76}
77
78fn deserialize_attachment_rows<'de, D>(
79 deserializer: D,
80) -> Result<Vec<RawGltfSkinAttachmentRowV1>, D::Error>
81where
82 D: Deserializer<'de>,
83{
84 deserialize_rows(deserializer)
85}
86
87fn deserialize_path_rows<'de, D>(
88 deserializer: D,
89) -> Result<Vec<RawGltfScenePathCandidateRowV1>, D::Error>
90where
91 D: Deserializer<'de>,
92{
93 deserialize_rows(deserializer)
94}
95
96fn deserialize_structural_references<'de, D>(deserializer: D) -> Result<Vec<u64>, D::Error>
97where
98 D: Deserializer<'de>,
99{
100 let references: CappedSequence<u64> = deserialize_capped_sequence(
101 deserializer,
102 RAW_GLTF_ADDRESSABILITY_V1_MAX_STRUCTURAL_REFERENCES,
103 )?;
104 if references.overflowed {
105 return Err(D::Error::custom(
106 "raw glTF addressability row exceeded its structural-reference bound",
107 ));
108 }
109 Ok(references.values)
110}
111
112fn deserialize_path_segments<'de, D>(deserializer: D) -> Result<Vec<u64>, D::Error>
113where
114 D: Deserializer<'de>,
115{
116 let segments: CappedSequence<u64> =
117 deserialize_capped_sequence(deserializer, RAW_GLTF_ADDRESSABILITY_V1_MAX_PATH_SEGMENTS)?;
118 if segments.overflowed {
119 return Err(D::Error::custom(
120 "raw glTF addressability path exceeded its segment bound",
121 ));
122 }
123 Ok(segments.values)
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
128#[serde(rename_all = "snake_case")]
129pub enum RawGltfAddressabilityCoverageReasonV1 {
130 ProjectionBudgetExceeded,
132 ParserUnavailable,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
138#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
139pub enum RawGltfAddressabilityCoverageV1 {
140 Complete,
142 Partial {
144 reason: RawGltfAddressabilityCoverageReasonV1,
146 },
147 Unavailable {
149 reason: RawGltfAddressabilityCoverageReasonV1,
151 },
152}
153
154impl RawGltfAddressabilityCoverageV1 {
155 pub const fn budget_exceeded() -> Self {
157 Self::Partial {
158 reason: RawGltfAddressabilityCoverageReasonV1::ProjectionBudgetExceeded,
159 }
160 }
161
162 pub const fn proves_absence(self) -> bool {
164 matches!(self, Self::Complete)
165 }
166
167 pub const fn is_complete(self) -> bool {
169 matches!(self, Self::Complete)
170 }
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
175#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
176pub enum RawGltfDefaultSceneObservationV1 {
177 Absent,
179 Selected {
181 source_scene_index: u64,
183 },
184 Unavailable {
186 reason: RawGltfAddressabilityCoverageReasonV1,
188 },
189}
190
191impl RawGltfDefaultSceneObservationV1 {
192 pub const fn selected_scene_index(self) -> Option<u64> {
194 match self {
195 Self::Selected { source_scene_index } => Some(source_scene_index),
196 Self::Absent | Self::Unavailable { .. } => None,
197 }
198 }
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
203#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
204pub enum RawGltfInverseBindMatricesObservationV1 {
205 Absent,
207 Declared {
209 source_accessor_index: u64,
211 },
212}
213
214#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
216#[serde(deny_unknown_fields)]
217pub struct RawGltfSceneRowV1 {
218 source_scene_index: u64,
219 #[serde(deserialize_with = "deserialize_name")]
220 name: Option<String>,
221 #[serde(deserialize_with = "deserialize_structural_references")]
222 root_node_indices: Vec<u64>,
223}
224
225impl RawGltfSceneRowV1 {
226 pub fn new(source_scene_index: u64, name: Option<String>, root_node_indices: Vec<u64>) -> Self {
228 Self {
229 source_scene_index,
230 name,
231 root_node_indices,
232 }
233 }
234 pub const fn source_scene_index(&self) -> u64 {
236 self.source_scene_index
237 }
238 pub fn name(&self) -> Option<&str> {
240 self.name.as_deref()
241 }
242 pub fn root_node_indices(&self) -> &[u64] {
244 &self.root_node_indices
245 }
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
250#[serde(deny_unknown_fields)]
251pub struct RawGltfNodeRowV1 {
252 source_node_index: u64,
253 #[serde(deserialize_with = "deserialize_name")]
254 name: Option<String>,
255 parent_node_index: Option<u64>,
256 #[serde(deserialize_with = "deserialize_structural_references")]
257 child_node_indices: Vec<u64>,
258}
259
260impl RawGltfNodeRowV1 {
261 pub fn new(
263 source_node_index: u64,
264 name: Option<String>,
265 parent_node_index: Option<u64>,
266 child_node_indices: Vec<u64>,
267 ) -> Self {
268 Self {
269 source_node_index,
270 name,
271 parent_node_index,
272 child_node_indices,
273 }
274 }
275 pub const fn source_node_index(&self) -> u64 {
277 self.source_node_index
278 }
279 pub fn name(&self) -> Option<&str> {
281 self.name.as_deref()
282 }
283 pub const fn parent_node_index(&self) -> Option<u64> {
285 self.parent_node_index
286 }
287 pub fn child_node_indices(&self) -> &[u64] {
289 &self.child_node_indices
290 }
291}
292
293#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
295#[serde(deny_unknown_fields)]
296pub struct RawGltfSkinRowV1 {
297 source_skin_index: u64,
298 #[serde(deserialize_with = "deserialize_name")]
299 name: Option<String>,
300 #[serde(deserialize_with = "deserialize_structural_references")]
301 joint_node_indices: Vec<u64>,
302 skeleton_root_node_index: Option<u64>,
303 inverse_bind_matrices: RawGltfInverseBindMatricesObservationV1,
304}
305
306impl RawGltfSkinRowV1 {
307 pub fn new(
309 source_skin_index: u64,
310 name: Option<String>,
311 joint_node_indices: Vec<u64>,
312 skeleton_root_node_index: Option<u64>,
313 inverse_bind_matrices: RawGltfInverseBindMatricesObservationV1,
314 ) -> Self {
315 Self {
316 source_skin_index,
317 name,
318 joint_node_indices,
319 skeleton_root_node_index,
320 inverse_bind_matrices,
321 }
322 }
323 pub const fn source_skin_index(&self) -> u64 {
325 self.source_skin_index
326 }
327 pub fn name(&self) -> Option<&str> {
329 self.name.as_deref()
330 }
331 pub fn joint_node_indices(&self) -> &[u64] {
333 &self.joint_node_indices
334 }
335 pub const fn skeleton_root_node_index(&self) -> Option<u64> {
337 self.skeleton_root_node_index
338 }
339 pub const fn inverse_bind_matrices(&self) -> RawGltfInverseBindMatricesObservationV1 {
341 self.inverse_bind_matrices
342 }
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
347#[serde(deny_unknown_fields)]
348pub struct RawGltfSkinAttachmentRowV1 {
349 source_node_index: u64,
350 source_skin_index: u64,
351}
352
353impl RawGltfSkinAttachmentRowV1 {
354 pub const fn new(source_node_index: u64, source_skin_index: u64) -> Self {
356 Self {
357 source_node_index,
358 source_skin_index,
359 }
360 }
361 pub const fn source_node_index(&self) -> u64 {
363 self.source_node_index
364 }
365 pub const fn source_skin_index(&self) -> u64 {
367 self.source_skin_index
368 }
369}
370
371#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
373#[serde(deny_unknown_fields)]
374pub struct RawGltfScenePathCandidateRowV1 {
375 source_path_candidate_index: u64,
376 source_scene_index: u64,
377 #[serde(deserialize_with = "deserialize_path_segments")]
378 source_node_indices: Vec<u64>,
379}
380
381impl RawGltfScenePathCandidateRowV1 {
382 pub fn new(
384 source_path_candidate_index: u64,
385 source_scene_index: u64,
386 source_node_indices: Vec<u64>,
387 ) -> Self {
388 Self {
389 source_path_candidate_index,
390 source_scene_index,
391 source_node_indices,
392 }
393 }
394 pub const fn source_path_candidate_index(&self) -> u64 {
396 self.source_path_candidate_index
397 }
398 pub const fn source_scene_index(&self) -> u64 {
400 self.source_scene_index
401 }
402 pub fn source_node_indices(&self) -> &[u64] {
404 &self.source_node_indices
405 }
406 pub fn target_node_index(&self) -> Option<u64> {
408 self.source_node_indices.last().copied()
409 }
410}
411
412#[derive(Debug, Clone, PartialEq, Eq)]
414pub struct RawGltfAddressabilityInventoryInputV1 {
415 pub default_scene: RawGltfDefaultSceneObservationV1,
417 pub scene_coverage: RawGltfAddressabilityCoverageV1,
419 pub scenes: Vec<RawGltfSceneRowV1>,
421 pub node_coverage: RawGltfAddressabilityCoverageV1,
423 pub nodes: Vec<RawGltfNodeRowV1>,
425 pub skin_coverage: RawGltfAddressabilityCoverageV1,
427 pub skins: Vec<RawGltfSkinRowV1>,
429 pub attachment_coverage: RawGltfAddressabilityCoverageV1,
431 pub attachments: Vec<RawGltfSkinAttachmentRowV1>,
433 pub path_candidate_coverage: RawGltfAddressabilityCoverageV1,
435 pub path_candidates: Vec<RawGltfScenePathCandidateRowV1>,
437}
438
439#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
441#[serde(deny_unknown_fields)]
442pub struct RawGltfAddressabilityInventoryV1 {
443 schema: &'static str,
444 identity: InputIdentity,
445 primary_input: InputIdentity,
446 dependency_closure: DependencyClosureV1,
447 default_scene: RawGltfDefaultSceneObservationV1,
448 scene_coverage: RawGltfAddressabilityCoverageV1,
449 scenes: Vec<RawGltfSceneRowV1>,
450 node_coverage: RawGltfAddressabilityCoverageV1,
451 nodes: Vec<RawGltfNodeRowV1>,
452 skin_coverage: RawGltfAddressabilityCoverageV1,
453 skins: Vec<RawGltfSkinRowV1>,
454 attachment_coverage: RawGltfAddressabilityCoverageV1,
455 attachments: Vec<RawGltfSkinAttachmentRowV1>,
456 path_candidate_coverage: RawGltfAddressabilityCoverageV1,
457 path_candidates: Vec<RawGltfScenePathCandidateRowV1>,
458}
459
460#[derive(Deserialize)]
461#[serde(deny_unknown_fields)]
462struct RawGltfAddressabilityInventoryWireV1 {
463 schema: String,
464 identity: InputIdentity,
465 primary_input: InputIdentity,
466 dependency_closure: DependencyClosureV1,
467 default_scene: RawGltfDefaultSceneObservationV1,
468 scene_coverage: RawGltfAddressabilityCoverageV1,
469 #[serde(deserialize_with = "deserialize_scene_rows")]
470 scenes: Vec<RawGltfSceneRowV1>,
471 node_coverage: RawGltfAddressabilityCoverageV1,
472 #[serde(deserialize_with = "deserialize_node_rows")]
473 nodes: Vec<RawGltfNodeRowV1>,
474 skin_coverage: RawGltfAddressabilityCoverageV1,
475 #[serde(deserialize_with = "deserialize_skin_rows")]
476 skins: Vec<RawGltfSkinRowV1>,
477 attachment_coverage: RawGltfAddressabilityCoverageV1,
478 #[serde(deserialize_with = "deserialize_attachment_rows")]
479 attachments: Vec<RawGltfSkinAttachmentRowV1>,
480 path_candidate_coverage: RawGltfAddressabilityCoverageV1,
481 #[serde(deserialize_with = "deserialize_path_rows")]
482 path_candidates: Vec<RawGltfScenePathCandidateRowV1>,
483}
484
485impl<'de> Deserialize<'de> for RawGltfAddressabilityInventoryV1 {
486 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
487 where
488 D: Deserializer<'de>,
489 {
490 let wire = RawGltfAddressabilityInventoryWireV1::deserialize(deserializer)?;
491 if wire.schema != RAW_GLTF_ADDRESSABILITY_INVENTORY_V1_ID {
492 return Err(D::Error::custom("invalid raw glTF addressability schema"));
493 }
494 let value = Self {
495 schema: RAW_GLTF_ADDRESSABILITY_INVENTORY_V1_ID,
496 identity: wire.identity,
497 primary_input: wire.primary_input,
498 dependency_closure: wire.dependency_closure,
499 default_scene: wire.default_scene,
500 scene_coverage: wire.scene_coverage,
501 scenes: wire.scenes,
502 node_coverage: wire.node_coverage,
503 nodes: wire.nodes,
504 skin_coverage: wire.skin_coverage,
505 skins: wire.skins,
506 attachment_coverage: wire.attachment_coverage,
507 attachments: wire.attachments,
508 path_candidate_coverage: wire.path_candidate_coverage,
509 path_candidates: wire.path_candidates,
510 };
511 value.validate().map_err(D::Error::custom)?;
512 if value.identity != value.canonical_identity().map_err(D::Error::custom)? {
513 return Err(D::Error::custom(
514 "raw glTF addressability identity does not match its contents",
515 ));
516 }
517 Ok(value)
518 }
519}
520
521impl RawGltfAddressabilityInventoryV1 {
522 pub fn new(
530 primary_input: InputIdentity,
531 dependency_closure: DependencyClosureV1,
532 input: RawGltfAddressabilityInventoryInputV1,
533 ) -> Result<Self, RawGltfAddressabilityInventoryErrorV1> {
534 let mut value = Self {
535 schema: RAW_GLTF_ADDRESSABILITY_INVENTORY_V1_ID,
536 identity: InputIdentity::from_bytes(&[]),
537 primary_input,
538 dependency_closure,
539 default_scene: input.default_scene,
540 scene_coverage: input.scene_coverage,
541 scenes: input.scenes,
542 node_coverage: input.node_coverage,
543 nodes: input.nodes,
544 skin_coverage: input.skin_coverage,
545 skins: input.skins,
546 attachment_coverage: input.attachment_coverage,
547 attachments: input.attachments,
548 path_candidate_coverage: input.path_candidate_coverage,
549 path_candidates: input.path_candidates,
550 };
551 value.validate()?;
552 value.identity = value.canonical_identity()?;
553 Ok(value)
554 }
555
556 pub fn read_from(reader: impl Read) -> Result<Self, RawGltfAddressabilityInventoryReadErrorV1> {
562 Self::read_from_with_limit(reader, RAW_GLTF_ADDRESSABILITY_V1_MAX_READER_BYTES)
563 }
564
565 fn read_from_with_limit(
566 reader: impl Read,
567 limit: u64,
568 ) -> Result<Self, RawGltfAddressabilityInventoryReadErrorV1> {
569 let mut bounded = reader.take(limit + 1);
570 let mut bytes = Vec::new();
571 bounded
572 .read_to_end(&mut bytes)
573 .map_err(|source| RawGltfAddressabilityInventoryReadErrorV1::Io { source })?;
574 if bytes.len() as u64 > limit {
575 return Err(RawGltfAddressabilityInventoryReadErrorV1::InventoryTooLarge { limit });
576 }
577 serde_json::from_slice(&bytes)
578 .map_err(|source| RawGltfAddressabilityInventoryReadErrorV1::InvalidJson { source })
579 }
580
581 pub const fn contract_id(&self) -> &'static str {
583 self.schema
584 }
585 pub const fn identity(&self) -> &InputIdentity {
587 &self.identity
588 }
589 pub const fn primary_input(&self) -> &InputIdentity {
591 &self.primary_input
592 }
593 pub const fn dependency_closure(&self) -> &DependencyClosureV1 {
595 &self.dependency_closure
596 }
597 pub const fn default_scene(&self) -> RawGltfDefaultSceneObservationV1 {
599 self.default_scene
600 }
601 pub const fn scene_coverage(&self) -> RawGltfAddressabilityCoverageV1 {
603 self.scene_coverage
604 }
605 pub fn scenes(&self) -> &[RawGltfSceneRowV1] {
607 &self.scenes
608 }
609 pub const fn node_coverage(&self) -> RawGltfAddressabilityCoverageV1 {
611 self.node_coverage
612 }
613 pub fn nodes(&self) -> &[RawGltfNodeRowV1] {
615 &self.nodes
616 }
617 pub const fn skin_coverage(&self) -> RawGltfAddressabilityCoverageV1 {
619 self.skin_coverage
620 }
621 pub fn skins(&self) -> &[RawGltfSkinRowV1] {
623 &self.skins
624 }
625 pub const fn attachment_coverage(&self) -> RawGltfAddressabilityCoverageV1 {
627 self.attachment_coverage
628 }
629 pub fn attachments(&self) -> &[RawGltfSkinAttachmentRowV1] {
631 &self.attachments
632 }
633 pub const fn path_candidate_coverage(&self) -> RawGltfAddressabilityCoverageV1 {
635 self.path_candidate_coverage
636 }
637 pub fn path_candidates(&self) -> &[RawGltfScenePathCandidateRowV1] {
639 &self.path_candidates
640 }
641
642 pub fn validate(&self) -> Result<(), RawGltfAddressabilityInventoryErrorV1> {
644 if self.schema != RAW_GLTF_ADDRESSABILITY_INVENTORY_V1_ID {
645 return Err(RawGltfAddressabilityInventoryErrorV1::InvalidSchema);
646 }
647 if self.dependency_closure.primary_input() != &self.primary_input {
648 return Err(RawGltfAddressabilityInventoryErrorV1::DependencyClosureMismatch);
649 }
650 validate_domain("scenes", self.scene_coverage, self.scenes.len())?;
651 validate_domain("nodes", self.node_coverage, self.nodes.len())?;
652 validate_domain("skins", self.skin_coverage, self.skins.len())?;
653 validate_domain(
654 "attachments",
655 self.attachment_coverage,
656 self.attachments.len(),
657 )?;
658 validate_domain(
659 "path_candidates",
660 self.path_candidate_coverage,
661 self.path_candidates.len(),
662 )?;
663
664 for (expected, row) in self.scenes.iter().enumerate() {
665 if row.source_scene_index != expected as u64 {
666 return Err(RawGltfAddressabilityInventoryErrorV1::NonCanonicalRows {
667 domain: "scenes",
668 });
669 }
670 }
671 for (expected, row) in self.nodes.iter().enumerate() {
672 if row.source_node_index != expected as u64 {
673 return Err(RawGltfAddressabilityInventoryErrorV1::NonCanonicalRows {
674 domain: "nodes",
675 });
676 }
677 }
678 for (expected, row) in self.skins.iter().enumerate() {
679 if row.source_skin_index != expected as u64 {
680 return Err(RawGltfAddressabilityInventoryErrorV1::NonCanonicalRows {
681 domain: "skins",
682 });
683 }
684 }
685 if self
686 .attachments
687 .windows(2)
688 .any(|rows| rows[0].source_node_index >= rows[1].source_node_index)
689 {
690 return Err(RawGltfAddressabilityInventoryErrorV1::NonCanonicalRows {
691 domain: "attachments",
692 });
693 }
694 for (expected, row) in self.path_candidates.iter().enumerate() {
695 if row.source_path_candidate_index != expected as u64
696 || row.source_node_indices.is_empty()
697 {
698 return Err(RawGltfAddressabilityInventoryErrorV1::NonCanonicalRows {
699 domain: "path_candidates",
700 });
701 }
702 if row.source_node_indices.len() > RAW_GLTF_ADDRESSABILITY_V1_MAX_PATH_SEGMENTS {
703 return Err(RawGltfAddressabilityInventoryErrorV1::TooManyPathSegments);
704 }
705 }
706
707 let mut text_bytes = 0usize;
708 for name in self
709 .scenes
710 .iter()
711 .filter_map(|row| row.name.as_deref())
712 .chain(self.nodes.iter().filter_map(|row| row.name.as_deref()))
713 .chain(self.skins.iter().filter_map(|row| row.name.as_deref()))
714 {
715 if name.len() > RAW_GLTF_ADDRESSABILITY_V1_MAX_NAME_BYTES {
716 return Err(RawGltfAddressabilityInventoryErrorV1::NameTooLong);
717 }
718 text_bytes = text_bytes
719 .checked_add(name.len())
720 .ok_or(RawGltfAddressabilityInventoryErrorV1::TooMuchText)?;
721 }
722 if text_bytes > RAW_GLTF_ADDRESSABILITY_V1_MAX_TEXT_BYTES {
723 return Err(RawGltfAddressabilityInventoryErrorV1::TooMuchText);
724 }
725 for path in &self.path_candidates {
726 let mut projected_bytes = 0usize;
727 let mut observable = true;
728 for (position, &node_index) in path.source_node_indices.iter().enumerate() {
729 let Some(node) = usize::try_from(node_index)
730 .ok()
731 .and_then(|index| self.nodes.get(index))
732 else {
733 observable = false;
734 break;
735 };
736 if node.source_node_index != node_index {
737 observable = false;
738 break;
739 }
740 let segment_bytes = node
741 .name
742 .as_ref()
743 .map_or_else(|| format!("GltfNode{node_index}").len(), String::len);
744 projected_bytes = projected_bytes
745 .saturating_add(usize::from(position > 0))
746 .saturating_add(segment_bytes);
747 }
748 if observable && projected_bytes > RAW_GLTF_ADDRESSABILITY_V1_MAX_PATH_BYTES {
749 return Err(RawGltfAddressabilityInventoryErrorV1::ProjectedPathTooLong);
750 }
751 }
752
753 let mut references = usize::from(matches!(
754 self.default_scene,
755 RawGltfDefaultSceneObservationV1::Selected { .. }
756 ));
757 for row in &self.scenes {
758 references = add_references(references, row.root_node_indices.len())?;
759 }
760 for row in &self.nodes {
761 references = add_references(
762 references,
763 row.child_node_indices.len() + usize::from(row.parent_node_index.is_some()),
764 )?;
765 }
766 for row in &self.skins {
767 references = add_references(
768 references,
769 row.joint_node_indices.len()
770 + usize::from(row.skeleton_root_node_index.is_some())
771 + usize::from(matches!(
772 row.inverse_bind_matrices,
773 RawGltfInverseBindMatricesObservationV1::Declared { .. }
774 )),
775 )?;
776 }
777 references = add_references(references, self.attachments.len().saturating_mul(2))?;
778 for row in &self.path_candidates {
779 references = add_references(references, 1 + row.source_node_indices.len())?;
780 }
781 if references > RAW_GLTF_ADDRESSABILITY_V1_MAX_STRUCTURAL_REFERENCES {
782 return Err(RawGltfAddressabilityInventoryErrorV1::TooManyStructuralReferences);
783 }
784
785 self.validate_references()?;
786 Ok(())
787 }
788
789 fn validate_references(&self) -> Result<(), RawGltfAddressabilityInventoryErrorV1> {
790 if self.scene_coverage.is_complete()
791 && let RawGltfDefaultSceneObservationV1::Selected { source_scene_index } =
792 self.default_scene
793 && source_scene_index >= self.scenes.len() as u64
794 {
795 return Err(RawGltfAddressabilityInventoryErrorV1::ReferenceOutOfRange);
796 }
797 if self.node_coverage.is_complete() {
798 let node_count = self.nodes.len() as u64;
799 for scene in &self.scenes {
800 if scene
801 .root_node_indices
802 .iter()
803 .any(|&node| node >= node_count)
804 {
805 return Err(RawGltfAddressabilityInventoryErrorV1::ReferenceOutOfRange);
806 }
807 }
808 for node in &self.nodes {
809 let mut unique_children = BTreeSet::new();
810 if node
811 .parent_node_index
812 .is_some_and(|parent| parent >= node_count)
813 || node
814 .child_node_indices
815 .iter()
816 .any(|&child| child >= node_count || !unique_children.insert(child))
817 {
818 return Err(RawGltfAddressabilityInventoryErrorV1::ReferenceOutOfRange);
819 }
820 for &child in &node.child_node_indices {
821 if self.nodes[child as usize].parent_node_index != Some(node.source_node_index)
822 {
823 return Err(RawGltfAddressabilityInventoryErrorV1::InvalidHierarchy);
824 }
825 }
826 if let Some(parent) = node.parent_node_index
827 && !self.nodes[parent as usize]
828 .child_node_indices
829 .contains(&node.source_node_index)
830 {
831 return Err(RawGltfAddressabilityInventoryErrorV1::InvalidHierarchy);
832 }
833 }
834 for node in &self.nodes {
835 let mut current = Some(node.source_node_index);
836 for _ in 0..self.nodes.len() {
837 let Some(index) = current else {
838 break;
839 };
840 current = self.nodes[index as usize].parent_node_index;
841 }
842 if current.is_some() {
843 return Err(RawGltfAddressabilityInventoryErrorV1::InvalidHierarchy);
844 }
845 }
846 for skin in &self.skins {
847 if skin
848 .joint_node_indices
849 .iter()
850 .any(|&node| node >= node_count)
851 || skin
852 .skeleton_root_node_index
853 .is_some_and(|node| node >= node_count)
854 {
855 return Err(RawGltfAddressabilityInventoryErrorV1::ReferenceOutOfRange);
856 }
857 }
858 }
859 if self.node_coverage.is_complete() && self.skin_coverage.is_complete() {
860 for attachment in &self.attachments {
861 if attachment.source_node_index >= self.nodes.len() as u64
862 || attachment.source_skin_index >= self.skins.len() as u64
863 {
864 return Err(RawGltfAddressabilityInventoryErrorV1::ReferenceOutOfRange);
865 }
866 }
867 }
868 for path in &self.path_candidates {
869 if self.scene_coverage.is_complete()
870 && path.source_scene_index >= self.scenes.len() as u64
871 {
872 return Err(RawGltfAddressabilityInventoryErrorV1::ReferenceOutOfRange);
873 }
874 if self.node_coverage.is_complete()
875 && path
876 .source_node_indices
877 .iter()
878 .any(|&node| node >= self.nodes.len() as u64)
879 {
880 return Err(RawGltfAddressabilityInventoryErrorV1::ReferenceOutOfRange);
881 }
882 if self.scene_coverage.is_complete() && self.node_coverage.is_complete() {
883 let scene = &self.scenes[path.source_scene_index as usize];
884 if !scene
885 .root_node_indices
886 .contains(&path.source_node_indices[0])
887 || path.source_node_indices.windows(2).any(|pair| {
888 !self.nodes[pair[0] as usize]
889 .child_node_indices
890 .contains(&pair[1])
891 })
892 {
893 return Err(RawGltfAddressabilityInventoryErrorV1::InvalidPathCandidate);
894 }
895 }
896 }
897 if self.scene_coverage.is_complete() && self.node_coverage.is_complete() {
898 self.validate_canonical_path_prefix()?;
899 }
900 Ok(())
901 }
902
903 fn validate_canonical_path_prefix(&self) -> Result<(), RawGltfAddressabilityInventoryErrorV1> {
904 let mut expected_index = 0usize;
905 'scenes: for scene in &self.scenes {
906 let mut stack = scene
907 .root_node_indices
908 .iter()
909 .rev()
910 .map(|&root| vec![root])
911 .collect::<Vec<_>>();
912 while let Some(path) = stack.pop() {
913 if expected_index == self.path_candidates.len() {
914 if self.path_candidate_coverage.is_complete() {
915 return Err(RawGltfAddressabilityInventoryErrorV1::NonCanonicalRows {
916 domain: "path_candidates",
917 });
918 }
919 break 'scenes;
920 }
921 let expected = &self.path_candidates[expected_index];
922 if expected.source_scene_index != scene.source_scene_index
923 || expected.source_node_indices != path
924 {
925 return Err(RawGltfAddressabilityInventoryErrorV1::NonCanonicalRows {
926 domain: "path_candidates",
927 });
928 }
929 expected_index += 1;
930 let target = *path.last().expect("canonical paths are nonempty");
931 for &child in self.nodes[target as usize].child_node_indices.iter().rev() {
932 let mut child_path = path.clone();
933 child_path.push(child);
934 stack.push(child_path);
935 }
936 }
937 }
938 if expected_index != self.path_candidates.len() {
939 return Err(RawGltfAddressabilityInventoryErrorV1::NonCanonicalRows {
940 domain: "path_candidates",
941 });
942 }
943 Ok(())
944 }
945
946 fn canonical_identity(&self) -> Result<InputIdentity, RawGltfAddressabilityInventoryErrorV1> {
947 #[derive(Serialize)]
948 struct IdentityFields<'a> {
949 schema: &'static str,
950 primary_input: &'a InputIdentity,
951 dependency_closure: &'a DependencyClosureV1,
952 default_scene: RawGltfDefaultSceneObservationV1,
953 scene_coverage: RawGltfAddressabilityCoverageV1,
954 scenes: &'a [RawGltfSceneRowV1],
955 node_coverage: RawGltfAddressabilityCoverageV1,
956 nodes: &'a [RawGltfNodeRowV1],
957 skin_coverage: RawGltfAddressabilityCoverageV1,
958 skins: &'a [RawGltfSkinRowV1],
959 attachment_coverage: RawGltfAddressabilityCoverageV1,
960 attachments: &'a [RawGltfSkinAttachmentRowV1],
961 path_candidate_coverage: RawGltfAddressabilityCoverageV1,
962 path_candidates: &'a [RawGltfScenePathCandidateRowV1],
963 }
964 let bytes = serde_json::to_vec(&IdentityFields {
965 schema: RAW_GLTF_ADDRESSABILITY_INVENTORY_V1_ID,
966 primary_input: &self.primary_input,
967 dependency_closure: &self.dependency_closure,
968 default_scene: self.default_scene,
969 scene_coverage: self.scene_coverage,
970 scenes: &self.scenes,
971 node_coverage: self.node_coverage,
972 nodes: &self.nodes,
973 skin_coverage: self.skin_coverage,
974 skins: &self.skins,
975 attachment_coverage: self.attachment_coverage,
976 attachments: &self.attachments,
977 path_candidate_coverage: self.path_candidate_coverage,
978 path_candidates: &self.path_candidates,
979 })
980 .map_err(|_| RawGltfAddressabilityInventoryErrorV1::IdentityEncoding)?;
981 Ok(InputIdentity::from_bytes(&bytes))
982 }
983}
984
985fn validate_domain(
986 domain: &'static str,
987 coverage: RawGltfAddressabilityCoverageV1,
988 rows: usize,
989) -> Result<(), RawGltfAddressabilityInventoryErrorV1> {
990 if rows > RAW_GLTF_ADDRESSABILITY_V1_MAX_ROWS_PER_DOMAIN {
991 return Err(RawGltfAddressabilityInventoryErrorV1::TooManyRows { domain });
992 }
993 if matches!(
994 coverage,
995 RawGltfAddressabilityCoverageV1::Unavailable { .. }
996 ) && rows != 0
997 {
998 return Err(RawGltfAddressabilityInventoryErrorV1::UnavailableHasRows { domain });
999 }
1000 Ok(())
1001}
1002
1003fn add_references(
1004 current: usize,
1005 additional: usize,
1006) -> Result<usize, RawGltfAddressabilityInventoryErrorV1> {
1007 current
1008 .checked_add(additional)
1009 .ok_or(RawGltfAddressabilityInventoryErrorV1::TooManyStructuralReferences)
1010}
1011
1012#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1014#[non_exhaustive]
1015pub enum RawGltfAddressabilityInventoryErrorV1 {
1016 #[error("invalid raw glTF addressability inventory schema")]
1018 InvalidSchema,
1019 #[error("raw glTF addressability dependency closure does not match primary input")]
1021 DependencyClosureMismatch,
1022 #[error("raw glTF addressability {domain} exceeded its row bound")]
1024 TooManyRows {
1025 domain: &'static str,
1027 },
1028 #[error("raw glTF addressability unavailable {domain} retained rows")]
1030 UnavailableHasRows {
1031 domain: &'static str,
1033 },
1034 #[error("raw glTF addressability {domain} rows are not canonical")]
1036 NonCanonicalRows {
1037 domain: &'static str,
1039 },
1040 #[error("raw glTF addressability name exceeded its UTF-8 byte bound")]
1042 NameTooLong,
1043 #[error("raw glTF addressability retained too much text")]
1045 TooMuchText,
1046 #[error("raw glTF addressability scene path exceeded its segment bound")]
1048 TooManyPathSegments,
1049 #[error("raw glTF addressability projected scene path exceeded its UTF-8 byte bound")]
1051 ProjectedPathTooLong,
1052 #[error("raw glTF addressability retained too many structural references")]
1054 TooManyStructuralReferences,
1055 #[error("raw glTF addressability reference is outside a complete source domain")]
1057 ReferenceOutOfRange,
1058 #[error("raw glTF addressability node hierarchy is contradictory")]
1060 InvalidHierarchy,
1061 #[error("raw glTF addressability scene path candidate is invalid")]
1063 InvalidPathCandidate,
1064 #[error("raw glTF addressability identity encoding failed")]
1066 IdentityEncoding,
1067}
1068
1069#[derive(Debug, thiserror::Error)]
1071#[non_exhaustive]
1072pub enum RawGltfAddressabilityInventoryReadErrorV1 {
1073 #[error("failed to read raw glTF addressability inventory: {source}")]
1075 Io {
1076 source: std::io::Error,
1078 },
1079 #[error("raw glTF addressability inventory exceeds byte limit {limit}")]
1081 InventoryTooLarge {
1082 limit: u64,
1084 },
1085 #[error("invalid raw glTF addressability inventory: {source}")]
1087 InvalidJson {
1088 source: serde_json::Error,
1090 },
1091}
1092
1093#[cfg(test)]
1094mod tests {
1095 use super::*;
1096
1097 fn empty() -> RawGltfAddressabilityInventoryV1 {
1098 let primary = InputIdentity::from_bytes(b"gltf");
1099 RawGltfAddressabilityInventoryV1::new(
1100 primary.clone(),
1101 DependencyClosureV1::unavailable(primary),
1102 RawGltfAddressabilityInventoryInputV1 {
1103 default_scene: RawGltfDefaultSceneObservationV1::Absent,
1104 scene_coverage: RawGltfAddressabilityCoverageV1::Complete,
1105 scenes: Vec::new(),
1106 node_coverage: RawGltfAddressabilityCoverageV1::Complete,
1107 nodes: Vec::new(),
1108 skin_coverage: RawGltfAddressabilityCoverageV1::Complete,
1109 skins: Vec::new(),
1110 attachment_coverage: RawGltfAddressabilityCoverageV1::Complete,
1111 attachments: Vec::new(),
1112 path_candidate_coverage: RawGltfAddressabilityCoverageV1::Complete,
1113 path_candidates: Vec::new(),
1114 },
1115 )
1116 .unwrap()
1117 }
1118
1119 #[test]
1120 fn strict_round_trip_rejects_schema_identity_and_unknown_field_mutations() {
1121 let inventory = empty();
1122 let encoded = serde_json::to_vec(&inventory).unwrap();
1123 assert_eq!(
1124 RawGltfAddressabilityInventoryV1::read_from(encoded.as_slice()).unwrap(),
1125 inventory
1126 );
1127 let mut value = serde_json::to_value(&inventory).unwrap();
1128 value["schema"] = serde_json::json!("urn:animsmith:raw-gltf-addressability-inventory:2");
1129 assert!(serde_json::from_value::<RawGltfAddressabilityInventoryV1>(value).is_err());
1130 let mut value = serde_json::to_value(&inventory).unwrap();
1131 value["identity"]["sha256"] = serde_json::json!("0".repeat(64));
1132 assert!(serde_json::from_value::<RawGltfAddressabilityInventoryV1>(value).is_err());
1133 let mut value = serde_json::to_value(&inventory).unwrap();
1134 value["extra"] = serde_json::json!(true);
1135 assert!(serde_json::from_value::<RawGltfAddressabilityInventoryV1>(value).is_err());
1136 }
1137
1138 #[test]
1139 fn row_name_and_structural_reference_bounds_accept_n_and_reject_n_plus_one() {
1140 let primary = InputIdentity::from_bytes(b"bounded");
1141 let closure = DependencyClosureV1::unavailable(primary.clone());
1142 let rows = (0..RAW_GLTF_ADDRESSABILITY_V1_MAX_ROWS_PER_DOMAIN)
1143 .map(|index| RawGltfSceneRowV1::new(index as u64, None, Vec::new()))
1144 .collect::<Vec<_>>();
1145 let inventory = RawGltfAddressabilityInventoryV1::new(
1146 primary.clone(),
1147 closure.clone(),
1148 RawGltfAddressabilityInventoryInputV1 {
1149 default_scene: RawGltfDefaultSceneObservationV1::Absent,
1150 scene_coverage: RawGltfAddressabilityCoverageV1::Complete,
1151 scenes: rows,
1152 node_coverage: RawGltfAddressabilityCoverageV1::Complete,
1153 nodes: Vec::new(),
1154 skin_coverage: RawGltfAddressabilityCoverageV1::Complete,
1155 skins: Vec::new(),
1156 attachment_coverage: RawGltfAddressabilityCoverageV1::Complete,
1157 attachments: Vec::new(),
1158 path_candidate_coverage: RawGltfAddressabilityCoverageV1::Complete,
1159 path_candidates: Vec::new(),
1160 },
1161 )
1162 .unwrap();
1163 let mut value = serde_json::to_value(&inventory).unwrap();
1164 value["scenes"]
1165 .as_array_mut()
1166 .unwrap()
1167 .push(serde_json::json!({
1168 "source_scene_index": RAW_GLTF_ADDRESSABILITY_V1_MAX_ROWS_PER_DOMAIN,
1169 "name": null,
1170 "root_node_indices": []
1171 }));
1172 assert!(serde_json::from_value::<RawGltfAddressabilityInventoryV1>(value).is_err());
1173
1174 let exact_name = "x".repeat(RAW_GLTF_ADDRESSABILITY_V1_MAX_NAME_BYTES);
1175 let row = RawGltfSceneRowV1::new(0, Some(exact_name), Vec::new());
1176 assert!(
1177 RawGltfAddressabilityInventoryV1::new(
1178 primary.clone(),
1179 closure.clone(),
1180 RawGltfAddressabilityInventoryInputV1 {
1181 default_scene: RawGltfDefaultSceneObservationV1::Absent,
1182 scene_coverage: RawGltfAddressabilityCoverageV1::Complete,
1183 scenes: vec![row],
1184 node_coverage: RawGltfAddressabilityCoverageV1::Complete,
1185 nodes: Vec::new(),
1186 skin_coverage: RawGltfAddressabilityCoverageV1::Complete,
1187 skins: Vec::new(),
1188 attachment_coverage: RawGltfAddressabilityCoverageV1::Complete,
1189 attachments: Vec::new(),
1190 path_candidate_coverage: RawGltfAddressabilityCoverageV1::Complete,
1191 path_candidates: Vec::new(),
1192 },
1193 )
1194 .is_ok()
1195 );
1196 let mut value = serde_json::to_value(empty()).unwrap();
1197 value["scenes"] = serde_json::json!([{
1198 "source_scene_index": 0,
1199 "name": "x".repeat(RAW_GLTF_ADDRESSABILITY_V1_MAX_NAME_BYTES + 1),
1200 "root_node_indices": []
1201 }]);
1202 value["scene_coverage"] = serde_json::json!({"state":"complete"});
1203 assert!(serde_json::from_value::<RawGltfAddressabilityInventoryV1>(value).is_err());
1204
1205 let bounded_input = |roots| RawGltfAddressabilityInventoryInputV1 {
1206 default_scene: RawGltfDefaultSceneObservationV1::Absent,
1207 scene_coverage: RawGltfAddressabilityCoverageV1::budget_exceeded(),
1208 scenes: vec![RawGltfSceneRowV1::new(0, None, roots)],
1209 node_coverage: RawGltfAddressabilityCoverageV1::Unavailable {
1210 reason: RawGltfAddressabilityCoverageReasonV1::ParserUnavailable,
1211 },
1212 nodes: Vec::new(),
1213 skin_coverage: RawGltfAddressabilityCoverageV1::Complete,
1214 skins: Vec::new(),
1215 attachment_coverage: RawGltfAddressabilityCoverageV1::Complete,
1216 attachments: Vec::new(),
1217 path_candidate_coverage: RawGltfAddressabilityCoverageV1::Unavailable {
1218 reason: RawGltfAddressabilityCoverageReasonV1::ParserUnavailable,
1219 },
1220 path_candidates: Vec::new(),
1221 };
1222 assert!(
1223 RawGltfAddressabilityInventoryV1::new(
1224 primary.clone(),
1225 closure.clone(),
1226 bounded_input(vec![
1227 0;
1228 RAW_GLTF_ADDRESSABILITY_V1_MAX_STRUCTURAL_REFERENCES
1229 ]),
1230 )
1231 .is_ok()
1232 );
1233 assert_eq!(
1234 RawGltfAddressabilityInventoryV1::new(
1235 primary.clone(),
1236 closure.clone(),
1237 bounded_input(vec![
1238 0;
1239 RAW_GLTF_ADDRESSABILITY_V1_MAX_STRUCTURAL_REFERENCES + 1
1240 ]),
1241 )
1242 .unwrap_err(),
1243 RawGltfAddressabilityInventoryErrorV1::TooManyStructuralReferences
1244 );
1245
1246 let exact_text_rows = (0..1024)
1247 .map(|index| {
1248 RawGltfSceneRowV1::new(
1249 index,
1250 Some("x".repeat(RAW_GLTF_ADDRESSABILITY_V1_MAX_NAME_BYTES)),
1251 Vec::new(),
1252 )
1253 })
1254 .collect::<Vec<_>>();
1255 let mut over_text_rows = exact_text_rows.clone();
1256 over_text_rows.push(RawGltfSceneRowV1::new(1024, Some("x".into()), Vec::new()));
1257 let text_input = |scenes| RawGltfAddressabilityInventoryInputV1 {
1258 default_scene: RawGltfDefaultSceneObservationV1::Absent,
1259 scene_coverage: RawGltfAddressabilityCoverageV1::budget_exceeded(),
1260 scenes,
1261 node_coverage: RawGltfAddressabilityCoverageV1::Complete,
1262 nodes: Vec::new(),
1263 skin_coverage: RawGltfAddressabilityCoverageV1::Complete,
1264 skins: Vec::new(),
1265 attachment_coverage: RawGltfAddressabilityCoverageV1::Complete,
1266 attachments: Vec::new(),
1267 path_candidate_coverage: RawGltfAddressabilityCoverageV1::Complete,
1268 path_candidates: Vec::new(),
1269 };
1270 assert!(
1271 RawGltfAddressabilityInventoryV1::new(
1272 primary.clone(),
1273 closure.clone(),
1274 text_input(exact_text_rows),
1275 )
1276 .is_ok()
1277 );
1278 assert_eq!(
1279 RawGltfAddressabilityInventoryV1::new(primary, closure, text_input(over_text_rows),)
1280 .unwrap_err(),
1281 RawGltfAddressabilityInventoryErrorV1::TooMuchText
1282 );
1283 }
1284
1285 #[test]
1286 fn path_segment_and_projected_byte_bounds_accept_n_and_reject_n_plus_one() {
1287 let primary = InputIdentity::from_bytes(b"path-bounds");
1288 let closure = DependencyClosureV1::unavailable(primary.clone());
1289 let partial = RawGltfAddressabilityCoverageV1::budget_exceeded();
1290 let path_input = |segments| RawGltfAddressabilityInventoryInputV1 {
1291 default_scene: RawGltfDefaultSceneObservationV1::Absent,
1292 scene_coverage: RawGltfAddressabilityCoverageV1::Unavailable {
1293 reason: RawGltfAddressabilityCoverageReasonV1::ParserUnavailable,
1294 },
1295 scenes: Vec::new(),
1296 node_coverage: RawGltfAddressabilityCoverageV1::Unavailable {
1297 reason: RawGltfAddressabilityCoverageReasonV1::ParserUnavailable,
1298 },
1299 nodes: Vec::new(),
1300 skin_coverage: RawGltfAddressabilityCoverageV1::Complete,
1301 skins: Vec::new(),
1302 attachment_coverage: RawGltfAddressabilityCoverageV1::Complete,
1303 attachments: Vec::new(),
1304 path_candidate_coverage: partial,
1305 path_candidates: vec![RawGltfScenePathCandidateRowV1::new(0, 0, segments)],
1306 };
1307 let exact_segments = (0..RAW_GLTF_ADDRESSABILITY_V1_MAX_PATH_SEGMENTS as u64).collect();
1308 let exact = RawGltfAddressabilityInventoryV1::new(
1309 primary.clone(),
1310 closure.clone(),
1311 path_input(exact_segments),
1312 )
1313 .expect("the exact path-segment ceiling is valid");
1314 let mut serialized = serde_json::to_value(&exact).unwrap();
1315 serialized["path_candidates"][0]["source_node_indices"]
1316 .as_array_mut()
1317 .unwrap()
1318 .push(serde_json::json!(
1319 RAW_GLTF_ADDRESSABILITY_V1_MAX_PATH_SEGMENTS
1320 ));
1321 assert!(
1322 serde_json::from_value::<RawGltfAddressabilityInventoryV1>(serialized).is_err(),
1323 "strict readback must reject the 257th segment"
1324 );
1325 assert_eq!(
1326 RawGltfAddressabilityInventoryV1::new(
1327 primary.clone(),
1328 closure.clone(),
1329 path_input((0..=RAW_GLTF_ADDRESSABILITY_V1_MAX_PATH_SEGMENTS as u64).collect()),
1330 )
1331 .unwrap_err(),
1332 RawGltfAddressabilityInventoryErrorV1::TooManyPathSegments
1333 );
1334
1335 let chain_input = |names: Vec<String>| {
1336 let count = names.len();
1337 RawGltfAddressabilityInventoryInputV1 {
1338 default_scene: RawGltfDefaultSceneObservationV1::Selected {
1339 source_scene_index: 0,
1340 },
1341 scene_coverage: RawGltfAddressabilityCoverageV1::Complete,
1342 scenes: vec![RawGltfSceneRowV1::new(0, None, vec![0])],
1343 node_coverage: RawGltfAddressabilityCoverageV1::Complete,
1344 nodes: names
1345 .into_iter()
1346 .enumerate()
1347 .map(|(index, name)| {
1348 RawGltfNodeRowV1::new(
1349 index as u64,
1350 Some(name),
1351 index.checked_sub(1).map(|parent| parent as u64),
1352 (index + 1 < count)
1353 .then_some(vec![(index + 1) as u64])
1354 .unwrap_or_default(),
1355 )
1356 })
1357 .collect(),
1358 skin_coverage: RawGltfAddressabilityCoverageV1::Complete,
1359 skins: Vec::new(),
1360 attachment_coverage: RawGltfAddressabilityCoverageV1::Complete,
1361 attachments: Vec::new(),
1362 path_candidate_coverage: RawGltfAddressabilityCoverageV1::Complete,
1363 path_candidates: (0..count)
1364 .map(|index| {
1365 RawGltfScenePathCandidateRowV1::new(
1366 index as u64,
1367 0,
1368 (0..=index as u64).collect(),
1369 )
1370 })
1371 .collect(),
1372 }
1373 };
1374 let exact_names = vec![
1375 "a".repeat(1_023),
1376 "b".repeat(1_023),
1377 "c".repeat(1_023),
1378 "d".repeat(1_024),
1379 ];
1380 assert!(
1381 RawGltfAddressabilityInventoryV1::new(
1382 primary.clone(),
1383 closure.clone(),
1384 chain_input(exact_names.clone()),
1385 )
1386 .is_ok(),
1387 "4,096 projected bytes are valid"
1388 );
1389 let mut over_names = exact_names;
1390 over_names.push(String::new());
1391 assert_eq!(
1392 RawGltfAddressabilityInventoryV1::new(primary, closure, chain_input(over_names),)
1393 .unwrap_err(),
1394 RawGltfAddressabilityInventoryErrorV1::ProjectedPathTooLong
1395 );
1396 }
1397
1398 #[test]
1399 fn every_row_domain_accepts_n_and_strict_readback_rejects_n_plus_one() {
1400 fn build(input: RawGltfAddressabilityInventoryInputV1) -> RawGltfAddressabilityInventoryV1 {
1401 let primary = InputIdentity::from_bytes(b"all-row-domains");
1402 RawGltfAddressabilityInventoryV1::new(
1403 primary.clone(),
1404 DependencyClosureV1::unavailable(primary),
1405 input,
1406 )
1407 .expect("the exact row ceiling is valid")
1408 }
1409
1410 fn assert_appended_row_is_rejected(
1411 inventory: &RawGltfAddressabilityInventoryV1,
1412 field: &str,
1413 row: serde_json::Value,
1414 ) {
1415 let mut value = serde_json::to_value(inventory).unwrap();
1416 value[field].as_array_mut().unwrap().push(row);
1417 assert!(
1418 serde_json::from_value::<RawGltfAddressabilityInventoryV1>(value).is_err(),
1419 "{field} must reject N+1 before accepting an over-limit contract"
1420 );
1421 }
1422
1423 let unavailable = RawGltfAddressabilityCoverageV1::Unavailable {
1424 reason: RawGltfAddressabilityCoverageReasonV1::ParserUnavailable,
1425 };
1426 let partial = RawGltfAddressabilityCoverageV1::budget_exceeded();
1427 let limit = RAW_GLTF_ADDRESSABILITY_V1_MAX_ROWS_PER_DOMAIN;
1428
1429 let scenes = build(RawGltfAddressabilityInventoryInputV1 {
1430 default_scene: RawGltfDefaultSceneObservationV1::Absent,
1431 scene_coverage: RawGltfAddressabilityCoverageV1::Complete,
1432 scenes: (0..limit)
1433 .map(|index| RawGltfSceneRowV1::new(index as u64, None, Vec::new()))
1434 .collect(),
1435 node_coverage: unavailable,
1436 nodes: Vec::new(),
1437 skin_coverage: unavailable,
1438 skins: Vec::new(),
1439 attachment_coverage: unavailable,
1440 attachments: Vec::new(),
1441 path_candidate_coverage: unavailable,
1442 path_candidates: Vec::new(),
1443 });
1444 assert_appended_row_is_rejected(
1445 &scenes,
1446 "scenes",
1447 serde_json::to_value(RawGltfSceneRowV1::new(limit as u64, None, Vec::new())).unwrap(),
1448 );
1449
1450 let nodes = build(RawGltfAddressabilityInventoryInputV1 {
1451 default_scene: RawGltfDefaultSceneObservationV1::Absent,
1452 scene_coverage: unavailable,
1453 scenes: Vec::new(),
1454 node_coverage: RawGltfAddressabilityCoverageV1::Complete,
1455 nodes: (0..limit)
1456 .map(|index| RawGltfNodeRowV1::new(index as u64, None, None, Vec::new()))
1457 .collect(),
1458 skin_coverage: unavailable,
1459 skins: Vec::new(),
1460 attachment_coverage: unavailable,
1461 attachments: Vec::new(),
1462 path_candidate_coverage: unavailable,
1463 path_candidates: Vec::new(),
1464 });
1465 assert_appended_row_is_rejected(
1466 &nodes,
1467 "nodes",
1468 serde_json::to_value(RawGltfNodeRowV1::new(limit as u64, None, None, Vec::new()))
1469 .unwrap(),
1470 );
1471
1472 let skins = build(RawGltfAddressabilityInventoryInputV1 {
1473 default_scene: RawGltfDefaultSceneObservationV1::Absent,
1474 scene_coverage: unavailable,
1475 scenes: Vec::new(),
1476 node_coverage: unavailable,
1477 nodes: Vec::new(),
1478 skin_coverage: RawGltfAddressabilityCoverageV1::Complete,
1479 skins: (0..limit)
1480 .map(|index| {
1481 RawGltfSkinRowV1::new(
1482 index as u64,
1483 None,
1484 Vec::new(),
1485 None,
1486 RawGltfInverseBindMatricesObservationV1::Absent,
1487 )
1488 })
1489 .collect(),
1490 attachment_coverage: unavailable,
1491 attachments: Vec::new(),
1492 path_candidate_coverage: unavailable,
1493 path_candidates: Vec::new(),
1494 });
1495 assert_appended_row_is_rejected(
1496 &skins,
1497 "skins",
1498 serde_json::to_value(RawGltfSkinRowV1::new(
1499 limit as u64,
1500 None,
1501 Vec::new(),
1502 None,
1503 RawGltfInverseBindMatricesObservationV1::Absent,
1504 ))
1505 .unwrap(),
1506 );
1507
1508 let attachments = build(RawGltfAddressabilityInventoryInputV1 {
1509 default_scene: RawGltfDefaultSceneObservationV1::Absent,
1510 scene_coverage: unavailable,
1511 scenes: Vec::new(),
1512 node_coverage: unavailable,
1513 nodes: Vec::new(),
1514 skin_coverage: unavailable,
1515 skins: Vec::new(),
1516 attachment_coverage: partial,
1517 attachments: (0..limit)
1518 .map(|index| RawGltfSkinAttachmentRowV1::new(index as u64, index as u64))
1519 .collect(),
1520 path_candidate_coverage: unavailable,
1521 path_candidates: Vec::new(),
1522 });
1523 assert_appended_row_is_rejected(
1524 &attachments,
1525 "attachments",
1526 serde_json::to_value(RawGltfSkinAttachmentRowV1::new(limit as u64, limit as u64))
1527 .unwrap(),
1528 );
1529
1530 let paths = build(RawGltfAddressabilityInventoryInputV1 {
1531 default_scene: RawGltfDefaultSceneObservationV1::Absent,
1532 scene_coverage: unavailable,
1533 scenes: Vec::new(),
1534 node_coverage: unavailable,
1535 nodes: Vec::new(),
1536 skin_coverage: unavailable,
1537 skins: Vec::new(),
1538 attachment_coverage: unavailable,
1539 attachments: Vec::new(),
1540 path_candidate_coverage: partial,
1541 path_candidates: (0..limit)
1542 .map(|index| {
1543 RawGltfScenePathCandidateRowV1::new(index as u64, 0, vec![index as u64])
1544 })
1545 .collect(),
1546 });
1547 assert_appended_row_is_rejected(
1548 &paths,
1549 "path_candidates",
1550 serde_json::to_value(RawGltfScenePathCandidateRowV1::new(
1551 limit as u64,
1552 0,
1553 vec![limit as u64],
1554 ))
1555 .unwrap(),
1556 );
1557 }
1558
1559 #[test]
1560 fn source_and_closure_mutations_are_rejected() {
1561 let inventory = empty();
1562 let mut value = serde_json::to_value(&inventory).unwrap();
1563 value["primary_input"]["bytes"] = serde_json::json!(999);
1564 assert!(serde_json::from_value::<RawGltfAddressabilityInventoryV1>(value).is_err());
1565 let mut value = serde_json::to_value(&inventory).unwrap();
1566 value["dependency_closure"]["primary_input"]["bytes"] = serde_json::json!(999);
1567 assert!(serde_json::from_value::<RawGltfAddressabilityInventoryV1>(value).is_err());
1568 }
1569
1570 #[test]
1571 fn bounded_reader_accepts_n_and_rejects_n_plus_one_before_json_decode() {
1572 let encoded = serde_json::to_vec(&empty()).unwrap();
1573 assert!(
1574 RawGltfAddressabilityInventoryV1::read_from_with_limit(
1575 encoded.as_slice(),
1576 encoded.len() as u64,
1577 )
1578 .is_ok()
1579 );
1580 assert!(matches!(
1581 RawGltfAddressabilityInventoryV1::read_from_with_limit(
1582 encoded.as_slice(),
1583 encoded.len() as u64 - 1,
1584 ),
1585 Err(RawGltfAddressabilityInventoryReadErrorV1::InventoryTooLarge { .. })
1586 ));
1587 }
1588}