Skip to main content

animsmith_core/
raw_gltf_addressability.rs

1//! Immutable, bounded raw glTF scene, node, skin, attachment, and path evidence.
2//!
3//! The normalized [`crate::Document`] is intentionally not authority for these
4//! source-array identities. Format loaders construct this sidecar during the
5//! same load and bind it to both the exact primary bytes and the complete
6//! bounded [`crate::DependencyClosureV1`] record they observed.
7
8use 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
17/// Immutable raw glTF addressability inventory contract identity.
18pub const RAW_GLTF_ADDRESSABILITY_INVENTORY_V1_ID: &str =
19    "urn:animsmith:raw-gltf-addressability-inventory:1";
20/// Maximum retained rows in each independent addressability domain.
21pub const RAW_GLTF_ADDRESSABILITY_V1_MAX_ROWS_PER_DOMAIN: usize = 4_096;
22/// Maximum aggregate structural index references retained by one inventory.
23pub const RAW_GLTF_ADDRESSABILITY_V1_MAX_STRUCTURAL_REFERENCES: usize = 65_536;
24/// Maximum UTF-8 bytes in one retained source name.
25pub const RAW_GLTF_ADDRESSABILITY_V1_MAX_NAME_BYTES: usize = 1_024;
26/// Maximum node-index segments in one retained scene path candidate.
27pub const RAW_GLTF_ADDRESSABILITY_V1_MAX_PATH_SEGMENTS: usize = 256;
28/// Maximum UTF-8 bytes in one slash-delimited authored-or-fallback path.
29pub const RAW_GLTF_ADDRESSABILITY_V1_MAX_PATH_BYTES: usize = 4_096;
30/// Maximum aggregate UTF-8 bytes retained in source names.
31pub const RAW_GLTF_ADDRESSABILITY_V1_MAX_TEXT_BYTES: usize = 1024 * 1024;
32/// Maximum serialized inventory bytes accepted by [`RawGltfAddressabilityInventoryV1::read_from`].
33pub 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/// Terminal reason for incomplete raw glTF projection coverage.
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
128#[serde(rename_all = "snake_case")]
129pub enum RawGltfAddressabilityCoverageReasonV1 {
130    /// A V1 row, reference, name, or aggregate text ceiling was exceeded.
131    ProjectionBudgetExceeded,
132    /// The loader could not observe this domain through its parser.
133    ParserUnavailable,
134}
135
136/// Independent exhaustive/prefix/unavailable state for one row domain.
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
138#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
139pub enum RawGltfAddressabilityCoverageV1 {
140    /// Every source row is retained; empty proves absence.
141    Complete,
142    /// The retained rows are a canonical source-order prefix only.
143    Partial {
144        /// Why projection stopped after the retained prefix.
145        reason: RawGltfAddressabilityCoverageReasonV1,
146    },
147    /// No positive source rows are authoritative for this domain.
148    Unavailable {
149        /// Why the domain could not be projected.
150        reason: RawGltfAddressabilityCoverageReasonV1,
151    },
152}
153
154impl RawGltfAddressabilityCoverageV1 {
155    /// Canonical projection-budget partial state.
156    pub const fn budget_exceeded() -> Self {
157        Self::Partial {
158            reason: RawGltfAddressabilityCoverageReasonV1::ProjectionBudgetExceeded,
159        }
160    }
161
162    /// Whether an empty row set proves source absence.
163    pub const fn proves_absence(self) -> bool {
164        matches!(self, Self::Complete)
165    }
166
167    /// Whether the domain is exhaustive.
168    pub const fn is_complete(self) -> bool {
169        matches!(self, Self::Complete)
170    }
171}
172
173/// Exact observation of the optional top-level glTF `scene` member.
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
175#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
176pub enum RawGltfDefaultSceneObservationV1 {
177    /// The member was absent. No scene is selected by default.
178    Absent,
179    /// The member selected one existing source scene index.
180    Selected {
181        /// Exact source scene-array index.
182        source_scene_index: u64,
183    },
184    /// The parser could not observe the member.
185    Unavailable {
186        /// Why no exact observation is present.
187        reason: RawGltfAddressabilityCoverageReasonV1,
188    },
189}
190
191impl RawGltfDefaultSceneObservationV1 {
192    /// Selected source scene index, if one was explicitly observed.
193    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/// Exact source observation of a skin's optional inverse-bind accessor.
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
203#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
204pub enum RawGltfInverseBindMatricesObservationV1 {
205    /// No accessor was declared; glTF's identity fallback applies.
206    Absent,
207    /// An accessor was explicitly declared.
208    Declared {
209        /// Exact source accessor-array index.
210        source_accessor_index: u64,
211    },
212}
213
214/// One source scene and its declared roots in authored order.
215#[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    /// Construct one source scene row.
227    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    /// Exact source scene-array index.
235    pub const fn source_scene_index(&self) -> u64 {
236        self.source_scene_index
237    }
238    /// Optional authored scene name.
239    pub fn name(&self) -> Option<&str> {
240        self.name.as_deref()
241    }
242    /// Root node identities in authored order.
243    pub fn root_node_indices(&self) -> &[u64] {
244        &self.root_node_indices
245    }
246}
247
248/// One source node with exact parent and authored child order.
249#[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    /// Construct one source node row.
262    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    /// Exact source node-array index.
276    pub const fn source_node_index(&self) -> u64 {
277        self.source_node_index
278    }
279    /// Optional authored node name.
280    pub fn name(&self) -> Option<&str> {
281        self.name.as_deref()
282    }
283    /// Exact source parent, absent for a forest root.
284    pub const fn parent_node_index(&self) -> Option<u64> {
285        self.parent_node_index
286    }
287    /// Child node identities in authored order.
288    pub fn child_node_indices(&self) -> &[u64] {
289        &self.child_node_indices
290    }
291}
292
293/// One source skin with exact joints and optional authored skeleton root.
294#[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    /// Construct one source skin row.
308    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    /// Exact source skin-array index.
324    pub const fn source_skin_index(&self) -> u64 {
325        self.source_skin_index
326    }
327    /// Optional authored skin name.
328    pub fn name(&self) -> Option<&str> {
329        self.name.as_deref()
330    }
331    /// Joint node identities in authored order.
332    pub fn joint_node_indices(&self) -> &[u64] {
333        &self.joint_node_indices
334    }
335    /// Explicit source `skin.skeleton`, without any inferred-root claim.
336    pub const fn skeleton_root_node_index(&self) -> Option<u64> {
337        self.skeleton_root_node_index
338    }
339    /// Exact optional inverse-bind accessor observation.
340    pub const fn inverse_bind_matrices(&self) -> RawGltfInverseBindMatricesObservationV1 {
341        self.inverse_bind_matrices
342    }
343}
344
345/// One source node-to-skin reference, in source node order.
346#[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    /// Construct one source node-to-skin reference.
355    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    /// Exact source node-array index.
362    pub const fn source_node_index(&self) -> u64 {
363        self.source_node_index
364    }
365    /// Exact referenced source skin-array index.
366    pub const fn source_skin_index(&self) -> u64 {
367        self.source_skin_index
368    }
369}
370
371/// One scene-root-to-node candidate path in deterministic scene DFS order.
372#[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    /// Construct one exact source-index path candidate.
383    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    /// Canonical candidate ordinal across all scenes.
395    pub const fn source_path_candidate_index(&self) -> u64 {
396        self.source_path_candidate_index
397    }
398    /// Exact source scene-array index.
399    pub const fn source_scene_index(&self) -> u64 {
400        self.source_scene_index
401    }
402    /// Root-to-node source identities, inclusive and in traversal order.
403    pub fn source_node_indices(&self) -> &[u64] {
404        &self.source_node_indices
405    }
406    /// Target node at the end of this candidate path.
407    pub fn target_node_index(&self) -> Option<u64> {
408        self.source_node_indices.last().copied()
409    }
410}
411
412/// Public validated constructor input for one raw glTF inventory.
413#[derive(Debug, Clone, PartialEq, Eq)]
414pub struct RawGltfAddressabilityInventoryInputV1 {
415    /// Exact observation of the optional top-level default-scene selector.
416    pub default_scene: RawGltfDefaultSceneObservationV1,
417    /// Independent source scene coverage.
418    pub scene_coverage: RawGltfAddressabilityCoverageV1,
419    /// Canonical source scene prefix.
420    pub scenes: Vec<RawGltfSceneRowV1>,
421    /// Independent source node coverage.
422    pub node_coverage: RawGltfAddressabilityCoverageV1,
423    /// Canonical source node prefix.
424    pub nodes: Vec<RawGltfNodeRowV1>,
425    /// Independent source skin coverage.
426    pub skin_coverage: RawGltfAddressabilityCoverageV1,
427    /// Canonical source skin prefix.
428    pub skins: Vec<RawGltfSkinRowV1>,
429    /// Independent node-to-skin attachment coverage.
430    pub attachment_coverage: RawGltfAddressabilityCoverageV1,
431    /// Canonical source-node-order attachment prefix.
432    pub attachments: Vec<RawGltfSkinAttachmentRowV1>,
433    /// Independent all-scene path-candidate coverage.
434    pub path_candidate_coverage: RawGltfAddressabilityCoverageV1,
435    /// Canonical scene/root/child traversal prefix.
436    pub path_candidates: Vec<RawGltfScenePathCandidateRowV1>,
437}
438
439/// Strict, canonical raw glTF addressability sidecar.
440#[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    /// Construct and validate one exact same-load inventory.
523    ///
524    /// # Errors
525    ///
526    /// Returns [`RawGltfAddressabilityInventoryErrorV1`] for mismatched
527    /// provenance, noncanonical prefixes, contradictory coverage, or a V1
528    /// collection, structural-reference, or text bound violation.
529    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    /// Read one strict inventory through the immutable 256 MiB byte cap.
557    ///
558    /// # Errors
559    ///
560    /// Returns a typed I/O, N+1 size, JSON-shape, or semantic contract error.
561    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    /// Semantic inventory identifier.
582    pub const fn contract_id(&self) -> &'static str {
583        self.schema
584    }
585    /// Canonical identity over exact primary, closure, coverage, and rows.
586    pub const fn identity(&self) -> &InputIdentity {
587        &self.identity
588    }
589    /// Exact primary input identity.
590    pub const fn primary_input(&self) -> &InputIdentity {
591        &self.primary_input
592    }
593    /// Exact dependency-closure V1 record from the same loader invocation.
594    pub const fn dependency_closure(&self) -> &DependencyClosureV1 {
595        &self.dependency_closure
596    }
597    /// Optional top-level default-scene observation.
598    pub const fn default_scene(&self) -> RawGltfDefaultSceneObservationV1 {
599        self.default_scene
600    }
601    /// Source scene coverage.
602    pub const fn scene_coverage(&self) -> RawGltfAddressabilityCoverageV1 {
603        self.scene_coverage
604    }
605    /// Canonical source scene prefix.
606    pub fn scenes(&self) -> &[RawGltfSceneRowV1] {
607        &self.scenes
608    }
609    /// Source node coverage.
610    pub const fn node_coverage(&self) -> RawGltfAddressabilityCoverageV1 {
611        self.node_coverage
612    }
613    /// Canonical source node prefix.
614    pub fn nodes(&self) -> &[RawGltfNodeRowV1] {
615        &self.nodes
616    }
617    /// Source skin coverage.
618    pub const fn skin_coverage(&self) -> RawGltfAddressabilityCoverageV1 {
619        self.skin_coverage
620    }
621    /// Canonical source skin prefix.
622    pub fn skins(&self) -> &[RawGltfSkinRowV1] {
623        &self.skins
624    }
625    /// Source node-to-skin attachment coverage.
626    pub const fn attachment_coverage(&self) -> RawGltfAddressabilityCoverageV1 {
627        self.attachment_coverage
628    }
629    /// Canonical source-node-order attachment prefix.
630    pub fn attachments(&self) -> &[RawGltfSkinAttachmentRowV1] {
631        &self.attachments
632    }
633    /// All-scene path-candidate coverage.
634    pub const fn path_candidate_coverage(&self) -> RawGltfAddressabilityCoverageV1 {
635        self.path_candidate_coverage
636    }
637    /// Canonical scene/root/child traversal prefix.
638    pub fn path_candidates(&self) -> &[RawGltfScenePathCandidateRowV1] {
639        &self.path_candidates
640    }
641
642    /// Validate this inventory without changing its identity.
643    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/// Invalid raw glTF addressability inventory.
1013#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1014#[non_exhaustive]
1015pub enum RawGltfAddressabilityInventoryErrorV1 {
1016    /// The semantic schema is not immutable V1.
1017    #[error("invalid raw glTF addressability inventory schema")]
1018    InvalidSchema,
1019    /// The embedded dependency closure identifies different primary bytes.
1020    #[error("raw glTF addressability dependency closure does not match primary input")]
1021    DependencyClosureMismatch,
1022    /// One independent domain exceeded its row ceiling.
1023    #[error("raw glTF addressability {domain} exceeded its row bound")]
1024    TooManyRows {
1025        /// Affected domain.
1026        domain: &'static str,
1027    },
1028    /// An unavailable domain retained positive-presence rows.
1029    #[error("raw glTF addressability unavailable {domain} retained rows")]
1030    UnavailableHasRows {
1031        /// Affected domain.
1032        domain: &'static str,
1033    },
1034    /// A row set is not a canonical source-order prefix.
1035    #[error("raw glTF addressability {domain} rows are not canonical")]
1036    NonCanonicalRows {
1037        /// Affected domain.
1038        domain: &'static str,
1039    },
1040    /// A retained name exceeded the per-name ceiling.
1041    #[error("raw glTF addressability name exceeded its UTF-8 byte bound")]
1042    NameTooLong,
1043    /// Aggregate retained text exceeded the V1 ceiling.
1044    #[error("raw glTF addressability retained too much text")]
1045    TooMuchText,
1046    /// One retained path exceeded the per-candidate segment ceiling.
1047    #[error("raw glTF addressability scene path exceeded its segment bound")]
1048    TooManyPathSegments,
1049    /// One observable authored-or-fallback path exceeded the UTF-8 byte ceiling.
1050    #[error("raw glTF addressability projected scene path exceeded its UTF-8 byte bound")]
1051    ProjectedPathTooLong,
1052    /// Aggregate structural references exceeded the V1 ceiling.
1053    #[error("raw glTF addressability retained too many structural references")]
1054    TooManyStructuralReferences,
1055    /// A reference is outside a completely observed source domain.
1056    #[error("raw glTF addressability reference is outside a complete source domain")]
1057    ReferenceOutOfRange,
1058    /// Complete parent/child observations disagree.
1059    #[error("raw glTF addressability node hierarchy is contradictory")]
1060    InvalidHierarchy,
1061    /// A retained candidate is not a scene-root-to-node path.
1062    #[error("raw glTF addressability scene path candidate is invalid")]
1063    InvalidPathCandidate,
1064    /// The deterministic identity encoding failed.
1065    #[error("raw glTF addressability identity encoding failed")]
1066    IdentityEncoding,
1067}
1068
1069/// Bounded raw glTF inventory reader failure.
1070#[derive(Debug, thiserror::Error)]
1071#[non_exhaustive]
1072pub enum RawGltfAddressabilityInventoryReadErrorV1 {
1073    /// Reading the bounded input failed.
1074    #[error("failed to read raw glTF addressability inventory: {source}")]
1075    Io {
1076        /// Underlying reader error.
1077        source: std::io::Error,
1078    },
1079    /// The serialized input exceeded the immutable reader cap.
1080    #[error("raw glTF addressability inventory exceeds byte limit {limit}")]
1081    InventoryTooLarge {
1082        /// Immutable byte ceiling.
1083        limit: u64,
1084    },
1085    /// JSON shape or semantic validation failed.
1086    #[error("invalid raw glTF addressability inventory: {source}")]
1087    InvalidJson {
1088        /// Strict JSON decoder diagnostic.
1089        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}