1use crate::{
7 DependencyClosureError, DependencyClosureV1, Document, ExactSourceTimingContractError,
8 ExactSourceTimingV1, InputIdentity, RawGltfAddressabilityInventoryV1,
9 RawSceneAttachmentCoverageV1, RawSceneAttachmentInventoryV1, RawSourceSkeletonEvidenceV1,
10 RawTransformPathInventoryV1, SourceSkeletonAssets, SourceSkeletonCoverage,
11};
12use serde::Serialize;
13use std::fmt;
14
15pub const RAW_SOURCE_FACTS_V1_ID: &str = "urn:animsmith:raw-source-facts:1";
17pub const RAW_SOURCE_V1_MAX_OBSERVATIONS: usize = 65_536;
19pub const RAW_SOURCE_V1_MAX_CLIPS: usize = 4_096;
21pub const RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES: usize = 4_096;
23pub const RAW_SOURCE_V1_MAX_TEXT_BYTES: usize = 4_096;
25pub const RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES: usize = 8 * 1024 * 1024;
27pub const RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH: usize = 128;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
32pub enum SourceFormatV1 {
33 GltfJson,
35 Glb,
37 Fbx,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
43pub enum SourceUnavailableReasonV1 {
44 Malformed,
46 Discarded,
48 NormalizedAway,
50 BakedAway,
52 LoaderUnsupported,
54 ProjectionBudgetExceeded,
56 ParserUnavailable,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum SourceSetCoverageStateV1 {
63 Complete,
65 Partial,
67 Unavailable,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub struct SourceSetCoverageV1 {
74 state: SourceSetCoverageStateV1,
75 reason: Option<SourceUnavailableReasonV1>,
76}
77
78impl SourceSetCoverageV1 {
79 pub const fn complete() -> Self {
81 Self {
82 state: SourceSetCoverageStateV1::Complete,
83 reason: None,
84 }
85 }
86
87 pub const fn partial(reason: SourceUnavailableReasonV1) -> Self {
89 Self {
90 state: SourceSetCoverageStateV1::Partial,
91 reason: Some(reason),
92 }
93 }
94
95 pub const fn unavailable(reason: SourceUnavailableReasonV1) -> Self {
97 Self {
98 state: SourceSetCoverageStateV1::Unavailable,
99 reason: Some(reason),
100 }
101 }
102
103 pub const fn state(self) -> SourceSetCoverageStateV1 {
105 self.state
106 }
107
108 pub const fn reason(self) -> Option<SourceUnavailableReasonV1> {
110 self.reason
111 }
112
113 pub const fn proves_absence(self) -> bool {
115 matches!(self.state, SourceSetCoverageStateV1::Complete)
116 }
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
121pub enum SourceLoaderDispositionV1 {
122 Preserved,
124 Normalized,
126 Baked,
128 Discarded,
130 Unsupported,
132 Unknown,
134 NotApplicable,
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
140pub enum SourceProvenanceKindV1 {
141 FormatDefined,
143 SourceDeclared,
145 ParserProjected,
147 DerivedFromSource,
149}
150
151#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
153pub struct SourceTextV1(String);
154
155impl SourceTextV1 {
156 pub fn new(value: impl AsRef<str>) -> Result<Self, SourceFactsError> {
162 let value = value.as_ref();
163 if value.len() > RAW_SOURCE_V1_MAX_TEXT_BYTES {
164 return Err(SourceFactsError::TextTooLong {
165 bytes: value.len(),
166 limit: RAW_SOURCE_V1_MAX_TEXT_BYTES,
167 });
168 }
169 Ok(Self(value.to_owned()))
170 }
171
172 pub fn as_str(&self) -> &str {
174 &self.0
175 }
176
177 fn retained_bytes(&self) -> usize {
178 self.0.len()
179 }
180}
181
182impl fmt::Debug for SourceTextV1 {
183 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
184 formatter
185 .debug_tuple("SourceTextV1")
186 .field(&self.0)
187 .finish()
188 }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
193pub struct SourceLogicalLocatorV1 {
194 text: SourceTextV1,
195}
196
197impl SourceLogicalLocatorV1 {
198 pub fn gltf_json_pointer(value: impl AsRef<str>) -> Result<Self, SourceFactsError> {
205 let value = value.as_ref();
206 let mut segments = value
207 .strip_prefix('/')
208 .into_iter()
209 .flat_map(|value| value.split('/'));
210 let valid_root = matches!(
211 segments.next(),
212 Some("animations" | "buffers" | "images" | "extensionsUsed" | "extensionsRequired")
213 );
214 if !valid_root || !segments.all(valid_logical_segment) {
215 return Err(SourceFactsError::InvalidLogicalLocator);
216 }
217 Ok(Self {
218 text: SourceTextV1::new(value)?,
219 })
220 }
221
222 pub fn fbx_parser_path(value: impl AsRef<str>) -> Result<Self, SourceFactsError> {
230 let value = value.as_ref();
231 let Some(path) = value.strip_prefix("fbx:") else {
232 return Err(SourceFactsError::InvalidLogicalLocator);
233 };
234 if path.is_empty()
235 || path
236 .split('/')
237 .any(|segment| !valid_logical_segment(segment))
238 {
239 return Err(SourceFactsError::InvalidLogicalLocator);
240 }
241 Ok(Self {
242 text: SourceTextV1::new(value)?,
243 })
244 }
245
246 pub fn as_str(&self) -> &str {
248 self.text.as_str()
249 }
250
251 fn retained_bytes(&self) -> usize {
252 self.text.retained_bytes()
253 }
254}
255
256fn valid_logical_segment(segment: &str) -> bool {
257 !segment.is_empty()
258 && !matches!(segment, "." | "..")
259 && segment.bytes().all(|value| {
260 value.is_ascii_alphanumeric() || matches!(value, b'_' | b'-' | b'.' | b'*')
261 })
262}
263
264#[derive(Clone, PartialEq, Eq)]
266pub struct SourceProvenanceV1 {
267 kind: SourceProvenanceKindV1,
268 locator: Option<SourceLogicalLocatorV1>,
269}
270
271impl fmt::Debug for SourceProvenanceV1 {
272 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
273 formatter
274 .debug_struct("SourceProvenanceV1")
275 .field("kind", &self.kind)
276 .field("locator_retained", &self.locator.is_some())
277 .finish()
278 }
279}
280
281impl SourceProvenanceV1 {
282 pub const fn format_defined() -> Self {
284 Self {
285 kind: SourceProvenanceKindV1::FormatDefined,
286 locator: None,
287 }
288 }
289
290 pub fn source_declared(locator: SourceLogicalLocatorV1) -> Self {
292 Self {
293 kind: SourceProvenanceKindV1::SourceDeclared,
294 locator: Some(locator),
295 }
296 }
297
298 pub fn parser_projected(locator: SourceLogicalLocatorV1) -> Self {
300 Self {
301 kind: SourceProvenanceKindV1::ParserProjected,
302 locator: Some(locator),
303 }
304 }
305
306 pub fn derived_from_source(locator: SourceLogicalLocatorV1) -> Self {
308 Self {
309 kind: SourceProvenanceKindV1::DerivedFromSource,
310 locator: Some(locator),
311 }
312 }
313
314 pub const fn kind(&self) -> SourceProvenanceKindV1 {
316 self.kind
317 }
318
319 pub fn locator(&self) -> Option<&SourceLogicalLocatorV1> {
321 self.locator.as_ref()
322 }
323
324 fn retained_bytes(&self) -> usize {
325 self.locator
326 .as_ref()
327 .map_or(0, SourceLogicalLocatorV1::retained_bytes)
328 }
329}
330
331#[derive(Debug, Clone, PartialEq)]
333pub enum SourceObservationStateV1<T> {
334 Observed(T),
336 ProvenAbsent,
338 Unavailable(SourceUnavailableReasonV1),
340}
341
342#[derive(Debug, Clone, PartialEq)]
344pub struct SourceObservationV1<T> {
345 state: SourceObservationStateV1<T>,
346 disposition: SourceLoaderDispositionV1,
347 provenance: Option<SourceProvenanceV1>,
348}
349
350impl<T> SourceObservationV1<T> {
351 pub fn observed(
353 value: T,
354 provenance: SourceProvenanceV1,
355 disposition: SourceLoaderDispositionV1,
356 ) -> Self {
357 Self {
358 state: SourceObservationStateV1::Observed(value),
359 disposition,
360 provenance: Some(provenance),
361 }
362 }
363
364 pub fn proven_absent(provenance: SourceProvenanceV1) -> Self {
366 Self {
367 state: SourceObservationStateV1::ProvenAbsent,
368 disposition: SourceLoaderDispositionV1::NotApplicable,
369 provenance: Some(provenance),
370 }
371 }
372
373 pub fn unavailable(
375 reason: SourceUnavailableReasonV1,
376 provenance: Option<SourceProvenanceV1>,
377 disposition: SourceLoaderDispositionV1,
378 ) -> Self {
379 Self {
380 state: SourceObservationStateV1::Unavailable(reason),
381 disposition,
382 provenance,
383 }
384 }
385
386 pub const fn state(&self) -> &SourceObservationStateV1<T> {
388 &self.state
389 }
390
391 pub const fn disposition(&self) -> SourceLoaderDispositionV1 {
393 self.disposition
394 }
395
396 pub fn provenance(&self) -> Option<&SourceProvenanceV1> {
398 self.provenance.as_ref()
399 }
400
401 fn retained_bytes(&self) -> usize {
402 self.provenance
403 .as_ref()
404 .map_or(0, SourceProvenanceV1::retained_bytes)
405 }
406}
407
408impl SourceObservationV1<SourceTextV1> {
409 fn retained_text_bytes(&self) -> usize {
410 let value_bytes = match &self.state {
411 SourceObservationStateV1::Observed(value) => value.retained_bytes(),
412 SourceObservationStateV1::ProvenAbsent | SourceObservationStateV1::Unavailable(_) => 0,
413 };
414 value_bytes.saturating_add(self.retained_bytes())
415 }
416}
417
418#[derive(Debug, Clone, PartialEq)]
420pub struct SourceFactSetV1<T> {
421 coverage: SourceSetCoverageV1,
422 rows: Vec<T>,
423}
424
425impl<T> SourceFactSetV1<T> {
426 pub fn complete(rows: Vec<T>) -> Self {
428 Self {
429 coverage: SourceSetCoverageV1::complete(),
430 rows,
431 }
432 }
433
434 pub fn partial(rows: Vec<T>, reason: SourceUnavailableReasonV1) -> Self {
436 Self {
437 coverage: SourceSetCoverageV1::partial(reason),
438 rows,
439 }
440 }
441
442 pub fn unavailable(reason: SourceUnavailableReasonV1) -> Self {
444 Self {
445 coverage: SourceSetCoverageV1::unavailable(reason),
446 rows: Vec::new(),
447 }
448 }
449
450 pub const fn coverage(&self) -> SourceSetCoverageV1 {
452 self.coverage
453 }
454
455 pub fn rows(&self) -> &[T] {
457 &self.rows
458 }
459
460 pub fn proves_absence(&self) -> bool {
462 self.rows.is_empty() && self.coverage.proves_absence()
463 }
464
465 fn mark_partial(&mut self, reason: SourceUnavailableReasonV1) {
466 self.coverage = SourceSetCoverageV1::partial(reason);
467 }
468}
469
470#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
472pub enum SourceAxisV1 {
473 PositiveX,
475 NegativeX,
477 PositiveY,
479 NegativeY,
481 PositiveZ,
483 NegativeZ,
485}
486
487impl SourceAxisV1 {
488 const fn unsigned(self) -> u8 {
489 match self {
490 Self::PositiveX | Self::NegativeX => 0,
491 Self::PositiveY | Self::NegativeY => 1,
492 Self::PositiveZ | Self::NegativeZ => 2,
493 }
494 }
495
496 const fn vector(self) -> [i8; 3] {
497 match self {
498 Self::PositiveX => [1, 0, 0],
499 Self::NegativeX => [-1, 0, 0],
500 Self::PositiveY => [0, 1, 0],
501 Self::NegativeY => [0, -1, 0],
502 Self::PositiveZ => [0, 0, 1],
503 Self::NegativeZ => [0, 0, -1],
504 }
505 }
506}
507
508#[derive(Debug, Clone, Copy, PartialEq, Eq)]
510pub enum SourceHandednessV1 {
511 Right,
513 Left,
515}
516
517#[derive(Debug, Clone, Copy, PartialEq, Eq)]
519pub struct SourceCoordinateBasisV1 {
520 right: SourceAxisV1,
521 up: SourceAxisV1,
522 forward: SourceAxisV1,
523}
524
525impl SourceCoordinateBasisV1 {
526 pub fn new(
533 right: SourceAxisV1,
534 up: SourceAxisV1,
535 forward: SourceAxisV1,
536 ) -> Result<Self, SourceFactsError> {
537 if right.unsigned() == up.unsigned()
538 || right.unsigned() == forward.unsigned()
539 || up.unsigned() == forward.unsigned()
540 {
541 return Err(SourceFactsError::DuplicateBasisAxis);
542 }
543 Ok(Self { right, up, forward })
544 }
545
546 pub const fn right(self) -> SourceAxisV1 {
548 self.right
549 }
550
551 pub const fn up(self) -> SourceAxisV1 {
553 self.up
554 }
555
556 pub const fn forward(self) -> SourceAxisV1 {
558 self.forward
559 }
560
561 pub fn handedness(self) -> SourceHandednessV1 {
563 let [rx, ry, rz] = self.right.vector();
564 let [ux, uy, uz] = self.up.vector();
565 let [fx, fy, fz] = self.forward.vector();
566 let determinant = i16::from(rx)
567 * (i16::from(uy) * i16::from(fz) - i16::from(uz) * i16::from(fy))
568 - i16::from(ry) * (i16::from(ux) * i16::from(fz) - i16::from(uz) * i16::from(fx))
569 + i16::from(rz) * (i16::from(ux) * i16::from(fy) - i16::from(uy) * i16::from(fx));
570 if determinant > 0 {
571 SourceHandednessV1::Right
572 } else {
573 SourceHandednessV1::Left
574 }
575 }
576}
577
578#[derive(Debug, Clone, Copy, PartialEq)]
580pub struct SourceLinearUnitV1(f64);
581
582impl SourceLinearUnitV1 {
583 pub fn new(meters_per_source_unit: f64) -> Result<Self, SourceFactsError> {
589 if !meters_per_source_unit.is_finite() || meters_per_source_unit <= 0.0 {
590 return Err(SourceFactsError::InvalidLinearUnit);
591 }
592 Ok(Self(meters_per_source_unit))
593 }
594
595 pub const fn meters_per_source_unit(self) -> f64 {
597 self.0
598 }
599}
600
601#[derive(Debug, Clone, Copy, PartialEq)]
603pub struct SourceFramesPerSecondV1(f64);
604
605impl SourceFramesPerSecondV1 {
606 pub fn new(value: f64) -> Result<Self, SourceFactsError> {
612 if !value.is_finite() || value <= 0.0 {
613 return Err(SourceFactsError::InvalidFramesPerSecond);
614 }
615 Ok(Self(value))
616 }
617
618 pub const fn get(self) -> f64 {
620 self.0
621 }
622}
623
624#[derive(Debug, Clone, Copy, PartialEq)]
626pub struct SourceTimeRangeV1 {
627 begin_s: f64,
628 end_s: f64,
629}
630
631impl SourceTimeRangeV1 {
632 pub fn new(begin_s: f64, end_s: f64) -> Result<Self, SourceFactsError> {
638 if !begin_s.is_finite() || !end_s.is_finite() || begin_s > end_s {
639 return Err(SourceFactsError::InvalidTimeRange);
640 }
641 Ok(Self { begin_s, end_s })
642 }
643
644 pub const fn begin_s(self) -> f64 {
646 self.begin_s
647 }
648
649 pub const fn end_s(self) -> f64 {
651 self.end_s
652 }
653}
654
655#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
657pub enum SourceChannelPropertyV1 {
658 Translation,
660 Rotation,
662 Scale,
664 Weights,
666 Other,
668}
669
670#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
672pub enum SourceInterpolationV1 {
673 Step,
675 Linear,
677 CubicSpline,
679 Other,
681}
682
683#[derive(Debug, Clone, Copy, PartialEq, Eq)]
685pub struct SourceComponentMaskV1 {
686 x: bool,
687 y: bool,
688 z: bool,
689}
690
691impl SourceComponentMaskV1 {
692 pub const fn new(x: bool, y: bool, z: bool) -> Self {
694 Self { x, y, z }
695 }
696
697 pub const fn x(self) -> bool {
699 self.x
700 }
701
702 pub const fn y(self) -> bool {
704 self.y
705 }
706
707 pub const fn z(self) -> bool {
709 self.z
710 }
711}
712
713#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
715pub enum SourceTargetKindV1 {
716 Node,
718 Element,
720 Other,
722}
723
724#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
726pub struct SourceTargetV1 {
727 kind: SourceTargetKindV1,
728 index: u64,
729}
730
731impl SourceTargetV1 {
732 pub const fn new(kind: SourceTargetKindV1, index: u64) -> Self {
734 Self { kind, index }
735 }
736
737 pub const fn kind(self) -> SourceTargetKindV1 {
739 self.kind
740 }
741
742 pub const fn index(self) -> u64 {
744 self.index
745 }
746}
747
748#[derive(Debug, Clone, PartialEq)]
750pub struct SourceChannelFactV1 {
751 source_channel_index: usize,
752 source_layer_index: Option<usize>,
753 target: SourceTargetV1,
754 property: SourceChannelPropertyV1,
755 property_name: Option<SourceTextV1>,
756 components: SourceComponentMaskV1,
757 interpolation: SourceObservationV1<SourceInterpolationV1>,
758 input_accessor_index: Option<usize>,
759 output_accessor_index: Option<usize>,
760 disposition: SourceLoaderDispositionV1,
761 provenance: SourceProvenanceV1,
762}
763
764impl SourceChannelFactV1 {
765 pub fn new(
767 source_channel_index: usize,
768 target: SourceTargetV1,
769 property: SourceChannelPropertyV1,
770 components: SourceComponentMaskV1,
771 interpolation: SourceObservationV1<SourceInterpolationV1>,
772 disposition: SourceLoaderDispositionV1,
773 provenance: SourceProvenanceV1,
774 ) -> Self {
775 Self {
776 source_channel_index,
777 source_layer_index: None,
778 target,
779 property,
780 property_name: None,
781 components,
782 interpolation,
783 input_accessor_index: None,
784 output_accessor_index: None,
785 disposition,
786 provenance,
787 }
788 }
789
790 pub fn with_source_layer_index(mut self, index: usize) -> Self {
792 self.source_layer_index = Some(index);
793 self
794 }
795
796 pub fn with_property_name(mut self, name: SourceTextV1) -> Self {
798 self.property_name = Some(name);
799 self
800 }
801
802 pub fn with_accessors(mut self, input: usize, output: usize) -> Self {
804 self.input_accessor_index = Some(input);
805 self.output_accessor_index = Some(output);
806 self
807 }
808
809 pub const fn source_channel_index(&self) -> usize {
811 self.source_channel_index
812 }
813
814 pub const fn source_layer_index(&self) -> Option<usize> {
816 self.source_layer_index
817 }
818
819 pub const fn target(&self) -> SourceTargetV1 {
821 self.target
822 }
823
824 pub const fn property(&self) -> SourceChannelPropertyV1 {
826 self.property
827 }
828
829 pub fn property_name(&self) -> Option<&SourceTextV1> {
831 self.property_name.as_ref()
832 }
833
834 pub const fn components(&self) -> SourceComponentMaskV1 {
836 self.components
837 }
838
839 pub const fn interpolation(&self) -> &SourceObservationV1<SourceInterpolationV1> {
841 &self.interpolation
842 }
843
844 pub const fn input_accessor_index(&self) -> Option<usize> {
846 self.input_accessor_index
847 }
848
849 pub const fn output_accessor_index(&self) -> Option<usize> {
851 self.output_accessor_index
852 }
853
854 pub const fn disposition(&self) -> SourceLoaderDispositionV1 {
856 self.disposition
857 }
858
859 pub const fn provenance(&self) -> &SourceProvenanceV1 {
861 &self.provenance
862 }
863
864 fn retained_bytes(&self) -> usize {
865 self.property_name
866 .as_ref()
867 .map_or(0, SourceTextV1::retained_bytes)
868 .saturating_add(self.interpolation.retained_bytes())
869 .saturating_add(self.provenance.retained_bytes())
870 }
871}
872
873#[derive(Debug, Clone, PartialEq)]
875pub struct SourceClipFactV1 {
876 source_clip_index: usize,
877 source_name: SourceObservationV1<SourceTextV1>,
878 normalized_clip_index: SourceObservationV1<usize>,
879 source_range: SourceObservationV1<SourceTimeRangeV1>,
880 sampler_range: SourceObservationV1<SourceTimeRangeV1>,
881 channels: SourceFactSetV1<SourceChannelFactV1>,
882}
883
884impl SourceClipFactV1 {
885 pub fn new(
887 source_clip_index: usize,
888 source_name: SourceObservationV1<SourceTextV1>,
889 normalized_clip_index: SourceObservationV1<usize>,
890 source_range: SourceObservationV1<SourceTimeRangeV1>,
891 sampler_range: SourceObservationV1<SourceTimeRangeV1>,
892 channels: SourceFactSetV1<SourceChannelFactV1>,
893 ) -> Self {
894 Self {
895 source_clip_index,
896 source_name,
897 normalized_clip_index,
898 source_range,
899 sampler_range,
900 channels,
901 }
902 }
903
904 pub const fn source_clip_index(&self) -> usize {
906 self.source_clip_index
907 }
908
909 pub const fn source_name(&self) -> &SourceObservationV1<SourceTextV1> {
911 &self.source_name
912 }
913
914 pub const fn normalized_clip_index(&self) -> &SourceObservationV1<usize> {
916 &self.normalized_clip_index
917 }
918
919 pub const fn source_range(&self) -> &SourceObservationV1<SourceTimeRangeV1> {
921 &self.source_range
922 }
923
924 pub const fn sampler_range(&self) -> &SourceObservationV1<SourceTimeRangeV1> {
926 &self.sampler_range
927 }
928
929 pub const fn channels(&self) -> &SourceFactSetV1<SourceChannelFactV1> {
931 &self.channels
932 }
933
934 fn retained_row_count(&self) -> usize {
935 1usize.saturating_add(self.channels.rows.len())
936 }
937
938 fn retained_bytes(&self) -> usize {
939 self.retained_non_channel_bytes().saturating_add(
940 self.channels
941 .rows
942 .iter()
943 .map(SourceChannelFactV1::retained_bytes)
944 .fold(0usize, usize::saturating_add),
945 )
946 }
947
948 fn retained_non_channel_bytes(&self) -> usize {
949 self.source_name
950 .retained_text_bytes()
951 .saturating_add(self.normalized_clip_index.retained_bytes())
952 .saturating_add(self.source_range.retained_bytes())
953 .saturating_add(self.sampler_range.retained_bytes())
954 }
955
956 fn truncate_channels(&mut self, retained: usize) {
957 if self.channels.rows.len() > retained {
958 self.channels.rows.truncate(retained);
959 self.channels
960 .mark_partial(SourceUnavailableReasonV1::ProjectionBudgetExceeded);
961 }
962 }
963
964 fn truncate_channels_to_text(&mut self, available_bytes: usize) -> bool {
965 let fixed_bytes = self.retained_non_channel_bytes();
966 if fixed_bytes > available_bytes {
967 return false;
968 }
969 let mut retained_bytes = fixed_bytes;
970 let retained_channels = self
971 .channels
972 .rows
973 .iter()
974 .take_while(|channel| {
975 let next = retained_bytes.saturating_add(channel.retained_bytes());
976 if next > available_bytes {
977 false
978 } else {
979 retained_bytes = next;
980 true
981 }
982 })
983 .count();
984 self.truncate_channels(retained_channels);
985 true
986 }
987}
988
989#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
991pub enum SourceConstructKindV1 {
992 Extension,
994 CustomProperty,
996 UnknownElement,
998}
999
1000#[derive(Debug, Clone, PartialEq, Eq)]
1002pub struct SourceConstructFactV1 {
1003 source_order_index: usize,
1004 kind: SourceConstructKindV1,
1005 name: SourceTextV1,
1006 required: bool,
1007 count: u64,
1008 disposition: SourceLoaderDispositionV1,
1009 provenance: SourceProvenanceV1,
1010}
1011
1012impl SourceConstructFactV1 {
1013 pub fn new(
1020 source_order_index: usize,
1021 kind: SourceConstructKindV1,
1022 name: SourceTextV1,
1023 required: bool,
1024 count: u64,
1025 disposition: SourceLoaderDispositionV1,
1026 provenance: SourceProvenanceV1,
1027 ) -> Result<Self, SourceFactsError> {
1028 if count == 0 {
1029 return Err(SourceFactsError::ZeroConstructCount);
1030 }
1031 Ok(Self {
1032 source_order_index,
1033 kind,
1034 name,
1035 required,
1036 count,
1037 disposition,
1038 provenance,
1039 })
1040 }
1041
1042 pub const fn source_order_index(&self) -> usize {
1044 self.source_order_index
1045 }
1046
1047 pub const fn kind(&self) -> SourceConstructKindV1 {
1049 self.kind
1050 }
1051
1052 pub const fn name(&self) -> &SourceTextV1 {
1054 &self.name
1055 }
1056
1057 pub const fn required(&self) -> bool {
1059 self.required
1060 }
1061
1062 pub const fn count(&self) -> u64 {
1064 self.count
1065 }
1066
1067 pub const fn disposition(&self) -> SourceLoaderDispositionV1 {
1069 self.disposition
1070 }
1071
1072 pub const fn provenance(&self) -> &SourceProvenanceV1 {
1074 &self.provenance
1075 }
1076
1077 fn retained_bytes(&self) -> usize {
1078 self.name
1079 .retained_bytes()
1080 .saturating_add(self.provenance.retained_bytes())
1081 }
1082}
1083
1084#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1086#[serde(rename_all = "snake_case")]
1087pub enum SourceResourceKindV1 {
1088 Buffer,
1090 Image,
1092 Texture,
1094 Video,
1096 Cache,
1098}
1099
1100#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
1102pub enum SourceResourceLocatorV1 {
1103 Embedded,
1105 DataUri,
1107 Relative(SourceRelativeLocatorV1),
1109 Absolute,
1111 Escaping,
1113 Remote,
1115 Malformed,
1117 Oversized,
1119 Missing,
1121}
1122
1123impl SourceResourceLocatorV1 {
1124 pub fn classify(value: &str) -> Self {
1130 if let Some(classification) = redacted_resource_locator(value) {
1131 return classification;
1132 }
1133 SourceTextV1::new(value).map_or(Self::Oversized, |value| {
1134 Self::Relative(SourceRelativeLocatorV1(value))
1135 })
1136 }
1137
1138 pub fn retained_relative_bytes(value: &str) -> usize {
1143 if redacted_resource_locator(value).is_none() {
1144 value.len()
1145 } else {
1146 0
1147 }
1148 }
1149
1150 fn retained_bytes(&self) -> usize {
1151 match self {
1152 Self::Relative(value) => value.0.retained_bytes(),
1153 _ => 0,
1154 }
1155 }
1156}
1157
1158fn redacted_resource_locator(value: &str) -> Option<SourceResourceLocatorV1> {
1159 if value
1160 .get(..5)
1161 .is_some_and(|prefix| prefix.eq_ignore_ascii_case("data:"))
1162 {
1163 return Some(SourceResourceLocatorV1::DataUri);
1164 }
1165 if value.len() > RAW_SOURCE_V1_MAX_TEXT_BYTES {
1166 return Some(SourceResourceLocatorV1::Oversized);
1167 }
1168 if value.is_empty() || value.chars().any(char::is_control) || malformed_percent_escape(value) {
1169 return Some(SourceResourceLocatorV1::Malformed);
1170 }
1171 if value.starts_with(['/', '\\'])
1172 || value.as_bytes().get(1).is_some_and(|value| *value == b':')
1173 || value
1174 .get(..5)
1175 .is_some_and(|prefix| prefix.eq_ignore_ascii_case("file:"))
1176 {
1177 return Some(SourceResourceLocatorV1::Absolute);
1178 }
1179 if has_uri_scheme(value) {
1180 return Some(SourceResourceLocatorV1::Remote);
1181 }
1182 let mut escaped = false;
1183 let mut malformed = false;
1184 for component in value.split(['/', '\\']) {
1185 escaped |= component == ".." || is_encoded_dot_segment(component);
1186 malformed |= component.is_empty() || component == ".";
1187 }
1188 if escaped || contains_encoded_path_escape(value) {
1189 return Some(SourceResourceLocatorV1::Escaping);
1190 }
1191 if malformed {
1192 return Some(SourceResourceLocatorV1::Malformed);
1193 }
1194 None
1195}
1196
1197#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
1199pub struct SourceRelativeLocatorV1(SourceTextV1);
1200
1201impl SourceRelativeLocatorV1 {
1202 pub fn as_str(&self) -> &str {
1204 self.0.as_str()
1205 }
1206}
1207
1208fn malformed_percent_escape(value: &str) -> bool {
1209 let bytes = value.as_bytes();
1210 let mut index = 0;
1211 while index < bytes.len() {
1212 if bytes[index] != b'%' {
1213 index += 1;
1214 continue;
1215 }
1216 if bytes.get(index + 1).and_then(|value| hex(*value)).is_none()
1217 || bytes.get(index + 2).and_then(|value| hex(*value)).is_none()
1218 {
1219 return true;
1220 }
1221 index += 3;
1222 }
1223 false
1224}
1225
1226fn contains_encoded_path_escape(value: &str) -> bool {
1227 let lower = value.to_ascii_lowercase();
1228 lower.contains("%2f") || lower.contains("%5c") || lower.contains("%00")
1229}
1230
1231fn is_encoded_dot_segment(value: &str) -> bool {
1232 let bytes = value.as_bytes();
1233 let mut index = 0;
1234 let mut dots = 0;
1235 let mut encoded = false;
1236 while index < bytes.len() {
1237 if bytes[index] == b'.' {
1238 dots += 1;
1239 index += 1;
1240 } else if bytes.get(index..index + 3).is_some_and(|escape| {
1241 escape[0] == b'%' && escape[1] == b'2' && matches!(escape[2], b'e' | b'E')
1242 }) {
1243 dots += 1;
1244 encoded = true;
1245 index += 3;
1246 } else {
1247 return false;
1248 }
1249 if dots > 2 {
1250 return false;
1251 }
1252 }
1253 encoded && matches!(dots, 1 | 2)
1254}
1255
1256fn hex(value: u8) -> Option<u8> {
1257 match value {
1258 b'0'..=b'9' => Some(value - b'0'),
1259 b'a'..=b'f' => Some(value - b'a' + 10),
1260 b'A'..=b'F' => Some(value - b'A' + 10),
1261 _ => None,
1262 }
1263}
1264
1265fn has_uri_scheme(value: &str) -> bool {
1266 let Some((scheme, _)) = value.split_once(':') else {
1267 return false;
1268 };
1269 !scheme.is_empty()
1270 && scheme.as_bytes()[0].is_ascii_alphabetic()
1271 && scheme
1272 .bytes()
1273 .all(|value| value.is_ascii_alphanumeric() || matches!(value, b'+' | b'-' | b'.'))
1274}
1275
1276#[derive(Debug, Clone, PartialEq, Eq)]
1278pub struct SourceResourceReferenceV1 {
1279 source_order_index: usize,
1280 kind: SourceResourceKindV1,
1281 source_index: u64,
1282 locator: SourceResourceLocatorV1,
1283 disposition: SourceLoaderDispositionV1,
1284 provenance: SourceProvenanceV1,
1285}
1286
1287impl SourceResourceReferenceV1 {
1288 pub fn new(
1290 source_order_index: usize,
1291 kind: SourceResourceKindV1,
1292 source_index: u64,
1293 locator: SourceResourceLocatorV1,
1294 disposition: SourceLoaderDispositionV1,
1295 provenance: SourceProvenanceV1,
1296 ) -> Self {
1297 Self {
1298 source_order_index,
1299 kind,
1300 source_index,
1301 locator,
1302 disposition,
1303 provenance,
1304 }
1305 }
1306
1307 pub const fn source_order_index(&self) -> usize {
1309 self.source_order_index
1310 }
1311
1312 pub const fn kind(&self) -> SourceResourceKindV1 {
1314 self.kind
1315 }
1316
1317 pub const fn source_index(&self) -> u64 {
1319 self.source_index
1320 }
1321
1322 pub const fn locator(&self) -> &SourceResourceLocatorV1 {
1324 &self.locator
1325 }
1326
1327 pub const fn disposition(&self) -> SourceLoaderDispositionV1 {
1329 self.disposition
1330 }
1331
1332 pub const fn provenance(&self) -> &SourceProvenanceV1 {
1334 &self.provenance
1335 }
1336
1337 fn retained_bytes(&self) -> usize {
1338 self.locator
1339 .retained_bytes()
1340 .saturating_add(self.provenance.retained_bytes())
1341 }
1342}
1343
1344#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1346pub struct SourceProjectionWorkV1 {
1347 inspected_rows: usize,
1348 retained_rows: usize,
1349 retained_text_bytes: usize,
1350 max_traversal_depth: usize,
1351}
1352
1353impl SourceProjectionWorkV1 {
1354 pub const fn inspected_rows(self) -> usize {
1356 self.inspected_rows
1357 }
1358
1359 pub const fn retained_rows(self) -> usize {
1361 self.retained_rows
1362 }
1363
1364 pub const fn retained_text_bytes(self) -> usize {
1366 self.retained_text_bytes
1367 }
1368
1369 pub const fn max_traversal_depth(self) -> usize {
1371 self.max_traversal_depth
1372 }
1373}
1374
1375#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1377pub enum SourceFactDomainV1 {
1378 Clips,
1380 Constructs,
1382 Resources,
1384}
1385
1386impl SourceFactDomainV1 {
1387 const fn index(self) -> usize {
1388 match self {
1389 Self::Clips => 0,
1390 Self::Constructs => 1,
1391 Self::Resources => 2,
1392 }
1393 }
1394}
1395
1396#[derive(Debug, Clone, PartialEq)]
1398pub struct RawSourceFactsV1 {
1399 format: SourceFormatV1,
1400 primary_identity: InputIdentity,
1401 linear_unit: SourceObservationV1<SourceLinearUnitV1>,
1402 coordinate_basis: SourceObservationV1<SourceCoordinateBasisV1>,
1403 frames_per_second: SourceObservationV1<SourceFramesPerSecondV1>,
1404 clips: SourceFactSetV1<SourceClipFactV1>,
1405 constructs: SourceFactSetV1<SourceConstructFactV1>,
1406 resources: SourceFactSetV1<SourceResourceReferenceV1>,
1407 work: SourceProjectionWorkV1,
1408}
1409
1410pub struct RawSourceFactsBuilderV1 {
1412 facts: RawSourceFactsV1,
1413 stopped: [bool; 3],
1414}
1415
1416fn unavailable_observation<T>() -> SourceObservationV1<T> {
1417 SourceObservationV1::unavailable(
1418 SourceUnavailableReasonV1::ParserUnavailable,
1419 None,
1420 SourceLoaderDispositionV1::Unknown,
1421 )
1422}
1423
1424fn replace_scalar_observation<T>(
1425 work: &mut SourceProjectionWorkV1,
1426 slot: &mut SourceObservationV1<T>,
1427 value: SourceObservationV1<T>,
1428) -> bool {
1429 let previous_bytes = slot.retained_bytes();
1430 let value_bytes = value.retained_bytes();
1431 let baseline = work.retained_text_bytes.saturating_sub(previous_bytes);
1432 if baseline
1433 .checked_add(value_bytes)
1434 .is_some_and(|total| total <= RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES)
1435 {
1436 work.retained_text_bytes = baseline.saturating_add(value_bytes);
1437 *slot = value;
1438 true
1439 } else {
1440 work.retained_text_bytes = baseline;
1441 *slot = SourceObservationV1::unavailable(
1442 SourceUnavailableReasonV1::ProjectionBudgetExceeded,
1443 None,
1444 SourceLoaderDispositionV1::Unknown,
1445 );
1446 false
1447 }
1448}
1449
1450impl RawSourceFactsBuilderV1 {
1451 pub fn new(format: SourceFormatV1, primary_identity: InputIdentity) -> Self {
1453 Self {
1454 facts: RawSourceFactsV1 {
1455 format,
1456 primary_identity,
1457 linear_unit: unavailable_observation(),
1458 coordinate_basis: unavailable_observation(),
1459 frames_per_second: unavailable_observation(),
1460 clips: SourceFactSetV1::unavailable(SourceUnavailableReasonV1::ParserUnavailable),
1461 constructs: SourceFactSetV1::unavailable(
1462 SourceUnavailableReasonV1::ParserUnavailable,
1463 ),
1464 resources: SourceFactSetV1::unavailable(
1465 SourceUnavailableReasonV1::ParserUnavailable,
1466 ),
1467 work: SourceProjectionWorkV1::default(),
1468 },
1469 stopped: [false; 3],
1470 }
1471 }
1472
1473 pub fn set_linear_unit(&mut self, value: SourceObservationV1<SourceLinearUnitV1>) -> bool {
1478 replace_scalar_observation(&mut self.facts.work, &mut self.facts.linear_unit, value)
1479 }
1480
1481 pub fn set_coordinate_basis(
1483 &mut self,
1484 value: SourceObservationV1<SourceCoordinateBasisV1>,
1485 ) -> bool {
1486 replace_scalar_observation(
1487 &mut self.facts.work,
1488 &mut self.facts.coordinate_basis,
1489 value,
1490 )
1491 }
1492
1493 pub fn set_frames_per_second(
1495 &mut self,
1496 value: SourceObservationV1<SourceFramesPerSecondV1>,
1497 ) -> bool {
1498 replace_scalar_observation(
1499 &mut self.facts.work,
1500 &mut self.facts.frames_per_second,
1501 value,
1502 )
1503 }
1504
1505 pub const fn remaining_observation_rows(&self) -> usize {
1509 RAW_SOURCE_V1_MAX_OBSERVATIONS.saturating_sub(self.facts.work.retained_rows)
1510 }
1511
1512 pub fn remaining_clip_rows(&self) -> usize {
1514 RAW_SOURCE_V1_MAX_CLIPS.saturating_sub(self.facts.clips.rows.len())
1515 }
1516
1517 pub fn remaining_resource_rows(&self) -> usize {
1519 RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES.saturating_sub(self.facts.resources.rows.len())
1520 }
1521
1522 pub const fn resource_coverage(&self) -> SourceSetCoverageV1 {
1524 self.facts.resources.coverage
1525 }
1526
1527 pub fn resource_rows(&self) -> &[SourceResourceReferenceV1] {
1529 &self.facts.resources.rows
1530 }
1531
1532 pub const fn primary_identity(&self) -> &InputIdentity {
1534 &self.facts.primary_identity
1535 }
1536
1537 pub const fn remaining_text_bytes(&self) -> usize {
1541 RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES.saturating_sub(self.facts.work.retained_text_bytes)
1542 }
1543
1544 pub fn mark_unavailable(
1546 &mut self,
1547 domain: SourceFactDomainV1,
1548 reason: SourceUnavailableReasonV1,
1549 ) {
1550 let (retained_rows, retained_bytes) = match domain {
1551 SourceFactDomainV1::Clips => self
1552 .facts
1553 .clips
1554 .rows
1555 .iter()
1556 .map(|clip| (clip.retained_row_count(), clip.retained_bytes()))
1557 .fold(
1558 (0usize, 0usize),
1559 |(rows, bytes), (clip_rows, clip_bytes)| {
1560 (
1561 rows.saturating_add(clip_rows),
1562 bytes.saturating_add(clip_bytes),
1563 )
1564 },
1565 ),
1566 SourceFactDomainV1::Constructs => (
1567 self.facts.constructs.rows.len(),
1568 self.facts
1569 .constructs
1570 .rows
1571 .iter()
1572 .map(SourceConstructFactV1::retained_bytes)
1573 .fold(0usize, usize::saturating_add),
1574 ),
1575 SourceFactDomainV1::Resources => (
1576 self.facts.resources.rows.len(),
1577 self.facts
1578 .resources
1579 .rows
1580 .iter()
1581 .map(SourceResourceReferenceV1::retained_bytes)
1582 .fold(0usize, usize::saturating_add),
1583 ),
1584 };
1585 self.facts.work.retained_rows = self.facts.work.retained_rows.saturating_sub(retained_rows);
1586 self.facts.work.retained_text_bytes = self
1587 .facts
1588 .work
1589 .retained_text_bytes
1590 .saturating_sub(retained_bytes);
1591 self.stopped[domain.index()] = true;
1592 match domain {
1593 SourceFactDomainV1::Clips => self.facts.clips = SourceFactSetV1::unavailable(reason),
1594 SourceFactDomainV1::Constructs => {
1595 self.facts.constructs = SourceFactSetV1::unavailable(reason)
1596 }
1597 SourceFactDomainV1::Resources => {
1598 self.facts.resources = SourceFactSetV1::unavailable(reason)
1599 }
1600 }
1601 }
1602
1603 pub fn mark_partial(&mut self, domain: SourceFactDomainV1, reason: SourceUnavailableReasonV1) {
1605 if !self.stopped[domain.index()] {
1606 *self.set_for_domain_mut(domain) = SourceSetCoverageV1::partial(reason);
1607 }
1608 }
1609
1610 pub fn mark_complete(&mut self, domain: SourceFactDomainV1) {
1615 if !self.stopped[domain.index()]
1616 && matches!(
1617 self.set_for_domain_mut(domain).state(),
1618 SourceSetCoverageStateV1::Unavailable
1619 )
1620 {
1621 *self.set_for_domain_mut(domain) = SourceSetCoverageV1::complete();
1622 }
1623 }
1624
1625 pub fn mark_budget_exceeded(&mut self, domain: SourceFactDomainV1) {
1630 if self.stopped[domain.index()] {
1631 return;
1632 }
1633 self.facts.work.inspected_rows = self.facts.work.inspected_rows.saturating_add(1);
1634 self.stop_for_budget(domain);
1635 }
1636
1637 pub fn observe_traversal_depth(&mut self, domain: SourceFactDomainV1, depth: usize) -> bool {
1641 if self.stopped[domain.index()] {
1642 return false;
1643 }
1644 self.facts.work.max_traversal_depth = self
1645 .facts
1646 .work
1647 .max_traversal_depth
1648 .max(depth.min(RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH + 1));
1649 if depth > RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH {
1650 self.stop_for_budget(domain);
1651 return false;
1652 }
1653 true
1654 }
1655
1656 pub fn push_clip(&mut self, mut clip: SourceClipFactV1) -> bool {
1661 if self.stopped[SourceFactDomainV1::Clips.index()] {
1662 return false;
1663 }
1664 if self.facts.clips.rows.len() >= RAW_SOURCE_V1_MAX_CLIPS {
1665 self.mark_budget_exceeded(SourceFactDomainV1::Clips);
1666 return false;
1667 }
1668 let remaining_rows =
1669 RAW_SOURCE_V1_MAX_OBSERVATIONS.saturating_sub(self.facts.work.retained_rows);
1670 if remaining_rows == 0 {
1671 self.mark_budget_exceeded(SourceFactDomainV1::Clips);
1672 return false;
1673 }
1674 let original_rows = clip.retained_row_count();
1675 let supplied_budget_prefix = matches!(
1676 clip.channels.coverage(),
1677 SourceSetCoverageV1 {
1678 state: SourceSetCoverageStateV1::Partial,
1679 reason: Some(SourceUnavailableReasonV1::ProjectionBudgetExceeded),
1680 }
1681 );
1682 let mut builder_truncated = false;
1683 if clip.retained_row_count() > remaining_rows {
1684 clip.truncate_channels(remaining_rows - 1);
1685 builder_truncated = true;
1686 }
1687 if !clip.truncate_channels_to_text(self.remaining_text_bytes()) {
1688 self.mark_budget_exceeded(SourceFactDomainV1::Clips);
1689 return false;
1690 }
1691 builder_truncated |= clip.retained_row_count() < original_rows;
1692 let retained_rows = clip.retained_row_count();
1693 let inspected_rows = if builder_truncated || supplied_budget_prefix {
1694 retained_rows.saturating_add(1)
1695 } else {
1696 retained_rows
1697 };
1698 self.facts.work.inspected_rows = self
1699 .facts
1700 .work
1701 .inspected_rows
1702 .saturating_add(inspected_rows);
1703 if builder_truncated || supplied_budget_prefix {
1704 self.stop_for_budget(SourceFactDomainV1::Clips);
1705 }
1706 self.retain_work(clip.retained_row_count(), clip.retained_bytes());
1707 self.facts.clips.rows.push(clip);
1708 true
1709 }
1710
1711 pub fn push_construct(&mut self, row: SourceConstructFactV1) -> bool {
1713 if self.stopped[SourceFactDomainV1::Constructs.index()] {
1714 return false;
1715 }
1716 if !self.can_retain_row(row.retained_bytes()) {
1717 self.mark_budget_exceeded(SourceFactDomainV1::Constructs);
1718 return false;
1719 }
1720 self.facts.work.inspected_rows = self.facts.work.inspected_rows.saturating_add(1);
1721 self.retain_work(1, row.retained_bytes());
1722 self.facts.constructs.rows.push(row);
1723 true
1724 }
1725
1726 pub fn push_resource(&mut self, row: SourceResourceReferenceV1) -> bool {
1728 if self.stopped[SourceFactDomainV1::Resources.index()] {
1729 return false;
1730 }
1731 if self.facts.resources.rows.len() >= RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES
1732 || !self.can_retain_row(row.retained_bytes())
1733 {
1734 self.mark_budget_exceeded(SourceFactDomainV1::Resources);
1735 return false;
1736 }
1737 self.facts.work.inspected_rows = self.facts.work.inspected_rows.saturating_add(1);
1738 self.retain_work(1, row.retained_bytes());
1739 self.facts.resources.rows.push(row);
1740 true
1741 }
1742
1743 pub fn finish(mut self, document: Document) -> Result<LoadedSource, SourceFactsError> {
1750 self.qualify_unfinished_positive_rows();
1751 let closure = DependencyClosureV1::capture_unavailable(
1752 self.facts.primary_identity.clone(),
1753 self.facts.resources.coverage,
1754 );
1755 self.finish_with_dependency_closure(document, closure)
1756 }
1757
1758 pub fn finish_with_dependency_closure(
1769 mut self,
1770 document: Document,
1771 dependency_closure: DependencyClosureV1,
1772 ) -> Result<LoadedSource, SourceFactsError> {
1773 self.qualify_unfinished_positive_rows();
1774 validate_clip_rows(&self.facts.clips, document.clips.len())?;
1775 validate_ordered_rows(&self.facts.constructs, &self.facts.resources)?;
1776 dependency_closure.validate_against(
1777 self.facts.format,
1778 &self.facts.primary_identity,
1779 &self.facts.resources,
1780 )?;
1781 Ok(LoadedSource {
1782 document,
1783 facts: self.facts,
1784 dependency_closure,
1785 exact_source_timing: None,
1786 raw_gltf_addressability_inventory: None,
1787 raw_scene_attachment_inventory: None,
1788 raw_transform_path_inventory: None,
1789 })
1790 }
1791
1792 fn can_retain_text(&self, bytes: usize) -> bool {
1793 self.facts
1794 .work
1795 .retained_text_bytes
1796 .checked_add(bytes)
1797 .is_some_and(|total| total <= RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES)
1798 }
1799
1800 fn can_retain_row(&self, bytes: usize) -> bool {
1801 self.facts.work.retained_rows < RAW_SOURCE_V1_MAX_OBSERVATIONS
1802 && self.can_retain_text(bytes)
1803 }
1804
1805 fn retain_work(&mut self, rows: usize, bytes: usize) {
1806 self.facts.work.retained_rows = self.facts.work.retained_rows.saturating_add(rows);
1807 self.facts.work.retained_text_bytes =
1808 self.facts.work.retained_text_bytes.saturating_add(bytes);
1809 }
1810
1811 fn set_for_domain_mut(&mut self, domain: SourceFactDomainV1) -> &mut SourceSetCoverageV1 {
1812 match domain {
1813 SourceFactDomainV1::Clips => &mut self.facts.clips.coverage,
1814 SourceFactDomainV1::Constructs => &mut self.facts.constructs.coverage,
1815 SourceFactDomainV1::Resources => &mut self.facts.resources.coverage,
1816 }
1817 }
1818
1819 fn stop_for_budget(&mut self, domain: SourceFactDomainV1) {
1820 *self.set_for_domain_mut(domain) =
1821 SourceSetCoverageV1::partial(SourceUnavailableReasonV1::ProjectionBudgetExceeded);
1822 self.stopped[domain.index()] = true;
1823 }
1824
1825 fn qualify_unfinished_positive_rows(&mut self) {
1826 for domain in [
1827 SourceFactDomainV1::Clips,
1828 SourceFactDomainV1::Constructs,
1829 SourceFactDomainV1::Resources,
1830 ] {
1831 let has_rows = match domain {
1832 SourceFactDomainV1::Clips => !self.facts.clips.rows.is_empty(),
1833 SourceFactDomainV1::Constructs => !self.facts.constructs.rows.is_empty(),
1834 SourceFactDomainV1::Resources => !self.facts.resources.rows.is_empty(),
1835 };
1836 if has_rows
1837 && matches!(
1838 self.set_for_domain_mut(domain).state(),
1839 SourceSetCoverageStateV1::Unavailable
1840 )
1841 {
1842 *self.set_for_domain_mut(domain) =
1843 SourceSetCoverageV1::partial(SourceUnavailableReasonV1::ParserUnavailable);
1844 }
1845 }
1846 }
1847}
1848
1849fn validate_clip_rows(
1850 clips: &SourceFactSetV1<SourceClipFactV1>,
1851 normalized_clip_count: usize,
1852) -> Result<(), SourceFactsError> {
1853 for (expected_clip_index, clip) in clips.rows.iter().enumerate() {
1854 if clip.source_clip_index != expected_clip_index {
1855 return Err(SourceFactsError::NonCanonicalClipIndex {
1856 expected: expected_clip_index,
1857 actual: clip.source_clip_index,
1858 });
1859 }
1860 if let SourceObservationStateV1::Observed(index) = clip.normalized_clip_index.state()
1861 && *index >= normalized_clip_count
1862 {
1863 return Err(SourceFactsError::NormalizedClipIndexOutOfRange {
1864 index: *index,
1865 clip_count: normalized_clip_count,
1866 });
1867 }
1868 for (expected_channel_index, channel) in clip.channels.rows.iter().enumerate() {
1869 if channel.source_channel_index != expected_channel_index {
1870 return Err(SourceFactsError::NonCanonicalChannelIndex {
1871 source_clip_index: clip.source_clip_index,
1872 expected: expected_channel_index,
1873 actual: channel.source_channel_index,
1874 });
1875 }
1876 }
1877 }
1878 Ok(())
1879}
1880
1881fn validate_ordered_rows(
1882 constructs: &SourceFactSetV1<SourceConstructFactV1>,
1883 resources: &SourceFactSetV1<SourceResourceReferenceV1>,
1884) -> Result<(), SourceFactsError> {
1885 for (expected, row) in constructs.rows.iter().enumerate() {
1886 if row.source_order_index != expected {
1887 return Err(SourceFactsError::NonCanonicalConstructOrder {
1888 expected,
1889 actual: row.source_order_index,
1890 });
1891 }
1892 }
1893 for (expected, row) in resources.rows.iter().enumerate() {
1894 if row.source_order_index != expected {
1895 return Err(SourceFactsError::NonCanonicalResourceOrder {
1896 expected,
1897 actual: row.source_order_index,
1898 });
1899 }
1900 }
1901 Ok(())
1902}
1903
1904pub struct LoadedSource {
1909 document: Document,
1910 facts: RawSourceFactsV1,
1911 dependency_closure: DependencyClosureV1,
1912 exact_source_timing: Option<ExactSourceTimingV1>,
1913 raw_gltf_addressability_inventory: Option<RawGltfAddressabilityInventoryV1>,
1914 raw_scene_attachment_inventory: Option<RawSceneAttachmentInventoryV1>,
1915 raw_transform_path_inventory: Option<RawTransformPathInventoryV1>,
1916}
1917
1918impl fmt::Debug for LoadedSource {
1919 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1920 formatter
1921 .debug_struct("LoadedSource")
1922 .field("format", &self.facts.format)
1923 .field("primary_identity", &self.facts.primary_identity)
1924 .field("dependency_closure", &self.dependency_closure)
1925 .field("exact_source_timing", &self.exact_source_timing)
1926 .field(
1927 "raw_gltf_addressability_inventory",
1928 &self.raw_gltf_addressability_inventory,
1929 )
1930 .field(
1931 "raw_scene_attachment_inventory",
1932 &self.raw_scene_attachment_inventory,
1933 )
1934 .field(
1935 "raw_transform_path_inventory",
1936 &self.raw_transform_path_inventory,
1937 )
1938 .field("work", &self.facts.work)
1939 .finish_non_exhaustive()
1940 }
1941}
1942
1943impl LoadedSource {
1944 pub const fn document(&self) -> &Document {
1946 &self.document
1947 }
1948
1949 pub fn source_facts(&self) -> SourceFactsViewV1<'_> {
1951 SourceFactsViewV1 {
1952 facts: &self.facts,
1953 source_skeleton: &self.document.assets.source_skeleton,
1954 }
1955 }
1956
1957 pub const fn dependency_closure(&self) -> &DependencyClosureV1 {
1959 &self.dependency_closure
1960 }
1961
1962 pub fn with_exact_source_timing(
1969 mut self,
1970 timing: ExactSourceTimingV1,
1971 ) -> Result<Self, ExactSourceTimingContractError> {
1972 if timing.clips().len() != self.facts.clips.rows.len() {
1973 return Err(ExactSourceTimingContractError::ClipCountMismatch {
1974 exact: timing.clips().len(),
1975 source_count: self.facts.clips.rows.len(),
1976 });
1977 }
1978 if timing.clip_coverage() != self.facts.clips.coverage {
1979 return Err(ExactSourceTimingContractError::ClipCoverageMismatch);
1980 }
1981 self.exact_source_timing = Some(timing);
1982 Ok(self)
1983 }
1984
1985 pub const fn exact_source_timing(&self) -> Option<&ExactSourceTimingV1> {
1989 self.exact_source_timing.as_ref()
1990 }
1991
1992 pub fn with_raw_gltf_addressability_inventory(
2000 mut self,
2001 inventory: RawGltfAddressabilityInventoryV1,
2002 ) -> Result<Self, RawGltfAddressabilityBindingErrorV1> {
2003 inventory
2004 .validate()
2005 .map_err(|_| RawGltfAddressabilityBindingErrorV1::InvalidInventory)?;
2006 if !matches!(
2007 self.facts.format,
2008 SourceFormatV1::GltfJson | SourceFormatV1::Glb
2009 ) {
2010 return Err(RawGltfAddressabilityBindingErrorV1::UnsupportedSourceFormat);
2011 }
2012 if inventory.primary_input() != &self.facts.primary_identity {
2013 return Err(RawGltfAddressabilityBindingErrorV1::PrimaryIdentityMismatch);
2014 }
2015 if inventory.dependency_closure() != &self.dependency_closure {
2016 return Err(RawGltfAddressabilityBindingErrorV1::DependencyClosureMismatch);
2017 }
2018 self.raw_gltf_addressability_inventory = Some(inventory);
2019 Ok(self)
2020 }
2021
2022 pub const fn raw_gltf_addressability_inventory(
2024 &self,
2025 ) -> Option<&RawGltfAddressabilityInventoryV1> {
2026 self.raw_gltf_addressability_inventory.as_ref()
2027 }
2028
2029 pub fn with_raw_scene_attachment_inventory(
2037 mut self,
2038 inventory: RawSceneAttachmentInventoryV1,
2039 ) -> Result<Self, RawSceneAttachmentBindingError> {
2040 if !matches!(
2041 self.facts.format,
2042 SourceFormatV1::GltfJson | SourceFormatV1::Glb
2043 ) {
2044 return Err(RawSceneAttachmentBindingError::UnsupportedSourceFormat);
2045 }
2046 if inventory.primary_input() != &self.facts.primary_identity {
2047 return Err(RawSceneAttachmentBindingError::PrimaryIdentityMismatch);
2048 }
2049 if inventory.source_skeleton()
2050 != &source_skeleton_evidence(&self.document.assets.source_skeleton)
2051 {
2052 return Err(RawSceneAttachmentBindingError::SourceSkeletonMismatch);
2053 }
2054 self.raw_scene_attachment_inventory = Some(inventory);
2055 Ok(self)
2056 }
2057
2058 pub const fn raw_scene_attachment_inventory(&self) -> Option<&RawSceneAttachmentInventoryV1> {
2063 self.raw_scene_attachment_inventory.as_ref()
2064 }
2065
2066 pub fn with_raw_transform_path_inventory(
2074 mut self,
2075 inventory: RawTransformPathInventoryV1,
2076 ) -> Result<Self, RawTransformPathBindingError> {
2077 inventory
2078 .validate()
2079 .map_err(|_| RawTransformPathBindingError::InvalidInventory)?;
2080 if self.facts.format != SourceFormatV1::Fbx
2081 || inventory.source_format() != SourceFormatV1::Fbx
2082 {
2083 return Err(RawTransformPathBindingError::UnsupportedSourceFormat);
2084 }
2085 if inventory.primary_input() != &self.facts.primary_identity {
2086 return Err(RawTransformPathBindingError::PrimaryIdentityMismatch);
2087 }
2088 if inventory.projected_bone_count() != self.document.skeleton.bones.len() as u64 {
2089 return Err(RawTransformPathBindingError::ProjectedBoneCountMismatch);
2090 }
2091 self.raw_transform_path_inventory = Some(inventory);
2092 Ok(self)
2093 }
2094
2095 pub const fn raw_transform_path_inventory(&self) -> Option<&RawTransformPathInventoryV1> {
2097 self.raw_transform_path_inventory.as_ref()
2098 }
2099
2100 pub fn into_document(self) -> Document {
2102 self.document
2103 }
2104}
2105
2106fn source_skeleton_evidence(source: &SourceSkeletonAssets) -> RawSourceSkeletonEvidenceV1 {
2107 RawSourceSkeletonEvidenceV1::new(
2108 match source.coverage {
2109 SourceSkeletonCoverage::Complete => RawSceneAttachmentCoverageV1::Complete,
2110 SourceSkeletonCoverage::Unavailable => RawSceneAttachmentCoverageV1::Unavailable,
2111 },
2112 source.nodes.len() as u64,
2113 source.skins.len() as u64,
2114 )
2115}
2116
2117#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
2119#[non_exhaustive]
2120pub enum RawSceneAttachmentBindingError {
2121 #[error("raw scene/attachment inventory requires a glTF or GLB source")]
2123 UnsupportedSourceFormat,
2124 #[error("raw scene/attachment inventory primary input does not match the loaded source")]
2126 PrimaryIdentityMismatch,
2127 #[error(
2129 "raw scene/attachment inventory source-skeleton evidence does not match the loaded source"
2130 )]
2131 SourceSkeletonMismatch,
2132}
2133
2134#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
2136#[non_exhaustive]
2137pub enum RawGltfAddressabilityBindingErrorV1 {
2138 #[error("raw glTF addressability inventory requires a glTF or GLB source")]
2140 UnsupportedSourceFormat,
2141 #[error("raw glTF addressability inventory is invalid")]
2143 InvalidInventory,
2144 #[error("raw glTF addressability inventory primary input does not match loaded source")]
2146 PrimaryIdentityMismatch,
2147 #[error("raw glTF addressability inventory dependency closure does not match loaded source")]
2149 DependencyClosureMismatch,
2150}
2151
2152#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
2154#[non_exhaustive]
2155pub enum RawTransformPathBindingError {
2156 #[error("raw transform-path inventory requires an FBX source")]
2158 UnsupportedSourceFormat,
2159 #[error("raw transform-path inventory is invalid")]
2161 InvalidInventory,
2162 #[error("raw transform-path inventory primary input does not match the loaded source")]
2164 PrimaryIdentityMismatch,
2165 #[error("raw transform-path inventory projected bone count does not match the document")]
2167 ProjectedBoneCountMismatch,
2168}
2169
2170#[derive(Debug, Clone, Copy)]
2172pub struct SourceFactsViewV1<'a> {
2173 facts: &'a RawSourceFactsV1,
2174 source_skeleton: &'a SourceSkeletonAssets,
2175}
2176
2177impl<'a> SourceFactsViewV1<'a> {
2178 pub const fn contract_id(self) -> &'static str {
2180 RAW_SOURCE_FACTS_V1_ID
2181 }
2182
2183 pub const fn format(self) -> SourceFormatV1 {
2185 self.facts.format
2186 }
2187
2188 pub const fn primary_identity(self) -> &'a InputIdentity {
2190 &self.facts.primary_identity
2191 }
2192
2193 pub const fn linear_unit(self) -> &'a SourceObservationV1<SourceLinearUnitV1> {
2195 &self.facts.linear_unit
2196 }
2197
2198 pub const fn coordinate_basis(self) -> &'a SourceObservationV1<SourceCoordinateBasisV1> {
2200 &self.facts.coordinate_basis
2201 }
2202
2203 pub const fn frames_per_second(self) -> &'a SourceObservationV1<SourceFramesPerSecondV1> {
2205 &self.facts.frames_per_second
2206 }
2207
2208 pub const fn clips(self) -> &'a SourceFactSetV1<SourceClipFactV1> {
2210 &self.facts.clips
2211 }
2212
2213 pub const fn constructs(self) -> &'a SourceFactSetV1<SourceConstructFactV1> {
2215 &self.facts.constructs
2216 }
2217
2218 pub const fn resources(self) -> &'a SourceFactSetV1<SourceResourceReferenceV1> {
2220 &self.facts.resources
2221 }
2222
2223 pub const fn source_skeleton(self) -> &'a SourceSkeletonAssets {
2225 self.source_skeleton
2226 }
2227
2228 pub const fn work(self) -> SourceProjectionWorkV1 {
2230 self.facts.work
2231 }
2232}
2233
2234#[derive(Debug, thiserror::Error, PartialEq, Eq)]
2236#[non_exhaustive]
2237pub enum SourceFactsError {
2238 #[error("source text is {bytes} bytes, exceeding the V1 limit of {limit}")]
2240 TextTooLong {
2241 bytes: usize,
2243 limit: usize,
2245 },
2246 #[error("source logical locator is invalid or unsafe")]
2248 InvalidLogicalLocator,
2249 #[error("source coordinate basis must use each unsigned axis exactly once")]
2251 DuplicateBasisAxis,
2252 #[error("metres per source unit must be finite and positive")]
2254 InvalidLinearUnit,
2255 #[error("source frames per second must be finite and positive")]
2257 InvalidFramesPerSecond,
2258 #[error("source time range endpoints must be finite with begin <= end")]
2260 InvalidTimeRange,
2261 #[error("source construct occurrence count must be positive")]
2263 ZeroConstructCount,
2264 #[error("source clip index {actual} is not the expected prefix index {expected}")]
2266 NonCanonicalClipIndex {
2267 expected: usize,
2269 actual: usize,
2271 },
2272 #[error(
2274 "source channel index {actual} is not expected prefix index {expected} in clip {source_clip_index}"
2275 )]
2276 NonCanonicalChannelIndex {
2277 source_clip_index: usize,
2279 expected: usize,
2281 actual: usize,
2283 },
2284 #[error("source construct order {actual} is not expected prefix index {expected}")]
2286 NonCanonicalConstructOrder {
2287 expected: usize,
2289 actual: usize,
2291 },
2292 #[error("source resource order {actual} is not expected prefix index {expected}")]
2294 NonCanonicalResourceOrder {
2295 expected: usize,
2297 actual: usize,
2299 },
2300 #[error("normalized clip index {index} is outside document clip count {clip_count}")]
2302 NormalizedClipIndexOutOfRange {
2303 index: usize,
2305 clip_count: usize,
2307 },
2308 #[error(transparent)]
2310 DependencyClosure(#[from] DependencyClosureError),
2311}
2312
2313#[cfg(test)]
2314mod tests {
2315 use super::*;
2316
2317 fn format_provenance() -> SourceProvenanceV1 {
2318 SourceProvenanceV1::format_defined()
2319 }
2320
2321 fn unavailable<T>() -> SourceObservationV1<T> {
2322 SourceObservationV1::unavailable(
2323 SourceUnavailableReasonV1::ParserUnavailable,
2324 None,
2325 SourceLoaderDispositionV1::Unknown,
2326 )
2327 }
2328
2329 fn exact_observed<T>(value: T) -> crate::ExactSourceTimingObservationV1<T> {
2330 crate::ExactSourceTimingObservationV1::observed(
2331 value,
2332 format_provenance(),
2333 SourceLoaderDispositionV1::Preserved,
2334 )
2335 }
2336
2337 fn clip(index: usize) -> SourceClipFactV1 {
2338 SourceClipFactV1::new(
2339 index,
2340 SourceObservationV1::proven_absent(format_provenance()),
2341 unavailable(),
2342 SourceObservationV1::proven_absent(format_provenance()),
2343 SourceObservationV1::proven_absent(format_provenance()),
2344 SourceFactSetV1::complete(Vec::new()),
2345 )
2346 }
2347
2348 fn construct(index: usize, name: String) -> SourceConstructFactV1 {
2349 SourceConstructFactV1::new(
2350 index,
2351 SourceConstructKindV1::Extension,
2352 SourceTextV1::new(name).expect("bounded name"),
2353 false,
2354 1,
2355 SourceLoaderDispositionV1::Unsupported,
2356 format_provenance(),
2357 )
2358 .expect("positive construct count")
2359 }
2360
2361 fn resource(index: usize) -> SourceResourceReferenceV1 {
2362 SourceResourceReferenceV1::new(
2363 index,
2364 SourceResourceKindV1::Image,
2365 index as u64,
2366 SourceResourceLocatorV1::Embedded,
2367 SourceLoaderDispositionV1::Preserved,
2368 format_provenance(),
2369 )
2370 }
2371
2372 fn named_channel(index: usize, name: &str) -> SourceChannelFactV1 {
2373 SourceChannelFactV1::new(
2374 index,
2375 SourceTargetV1::new(SourceTargetKindV1::Node, 0),
2376 SourceChannelPropertyV1::Other,
2377 SourceComponentMaskV1::new(true, false, false),
2378 SourceObservationV1::proven_absent(format_provenance()),
2379 SourceLoaderDispositionV1::Unsupported,
2380 format_provenance(),
2381 )
2382 .with_property_name(SourceTextV1::new(name).expect("bounded property name"))
2383 }
2384
2385 #[test]
2386 fn scalar_value_types_reject_non_finite_and_contradictory_values() {
2387 for value in [0.0, -1.0, f64::NAN, f64::INFINITY] {
2388 assert_eq!(
2389 SourceLinearUnitV1::new(value),
2390 Err(SourceFactsError::InvalidLinearUnit)
2391 );
2392 assert_eq!(
2393 SourceFramesPerSecondV1::new(value),
2394 Err(SourceFactsError::InvalidFramesPerSecond)
2395 );
2396 }
2397 assert_eq!(
2398 SourceTimeRangeV1::new(2.0, 1.0),
2399 Err(SourceFactsError::InvalidTimeRange)
2400 );
2401 assert_eq!(
2402 SourceTimeRangeV1::new(f64::NAN, 1.0),
2403 Err(SourceFactsError::InvalidTimeRange)
2404 );
2405 assert_eq!(
2406 SourceCoordinateBasisV1::new(
2407 SourceAxisV1::PositiveX,
2408 SourceAxisV1::NegativeX,
2409 SourceAxisV1::PositiveZ,
2410 ),
2411 Err(SourceFactsError::DuplicateBasisAxis)
2412 );
2413
2414 let range = SourceTimeRangeV1::new(1.0, 1.0).expect("zero duration is evidence");
2415 assert_eq!(range.begin_s(), range.end_s());
2416 let basis = SourceCoordinateBasisV1::new(
2417 SourceAxisV1::PositiveX,
2418 SourceAxisV1::PositiveY,
2419 SourceAxisV1::PositiveZ,
2420 )
2421 .expect("orthogonal basis");
2422 assert_eq!(basis.handedness(), SourceHandednessV1::Right);
2423 assert_eq!(
2424 SourceConstructFactV1::new(
2425 0,
2426 SourceConstructKindV1::Extension,
2427 SourceTextV1::new("EXT_zero").expect("bounded name"),
2428 false,
2429 0,
2430 SourceLoaderDispositionV1::Unsupported,
2431 format_provenance(),
2432 ),
2433 Err(SourceFactsError::ZeroConstructCount)
2434 );
2435 }
2436
2437 #[test]
2438 fn partial_empty_sets_never_prove_absence() {
2439 let complete = SourceFactSetV1::<SourceConstructFactV1>::complete(Vec::new());
2440 let partial = SourceFactSetV1::<SourceConstructFactV1>::partial(
2441 Vec::new(),
2442 SourceUnavailableReasonV1::ProjectionBudgetExceeded,
2443 );
2444 let unavailable = SourceFactSetV1::<SourceConstructFactV1>::unavailable(
2445 SourceUnavailableReasonV1::LoaderUnsupported,
2446 );
2447 assert!(complete.proves_absence());
2448 assert!(!partial.proves_absence());
2449 assert!(!unavailable.proves_absence());
2450 }
2451
2452 #[test]
2453 fn builder_requires_explicit_complete_coverage_to_prove_absence() {
2454 let builder = RawSourceFactsBuilderV1::new(
2455 SourceFormatV1::GltfJson,
2456 InputIdentity::from_bytes(b"untouched"),
2457 );
2458 let loaded = builder
2459 .finish(Document::default())
2460 .expect("unavailable defaults");
2461 let facts = loaded.source_facts();
2462 assert!(!facts.clips().proves_absence());
2463 assert!(!facts.constructs().proves_absence());
2464 assert!(!facts.resources().proves_absence());
2465 assert_eq!(
2466 loaded.dependency_closure().coverage().reasons(),
2467 &[
2468 crate::DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable,
2469 crate::DependencyClosureCoverageReasonV1::CaptureUnavailable,
2470 ]
2471 );
2472
2473 let mut builder = RawSourceFactsBuilderV1::new(
2474 SourceFormatV1::GltfJson,
2475 InputIdentity::from_bytes(b"exhaustive"),
2476 );
2477 builder.mark_complete(SourceFactDomainV1::Clips);
2478 builder.mark_complete(SourceFactDomainV1::Constructs);
2479 builder.mark_complete(SourceFactDomainV1::Resources);
2480 let loaded = builder
2481 .finish(Document::default())
2482 .expect("complete empty domains");
2483 let facts = loaded.source_facts();
2484 assert!(facts.clips().proves_absence());
2485 assert!(facts.constructs().proves_absence());
2486 assert!(facts.resources().proves_absence());
2487 assert_eq!(
2488 loaded.dependency_closure().coverage().reasons(),
2489 &[crate::DependencyClosureCoverageReasonV1::CaptureUnavailable]
2490 );
2491 }
2492
2493 #[test]
2494 fn loaded_source_binds_raw_gltf_addressability_to_exact_primary_and_closure() {
2495 fn inventory(
2496 primary: InputIdentity,
2497 closure: DependencyClosureV1,
2498 ) -> RawGltfAddressabilityInventoryV1 {
2499 RawGltfAddressabilityInventoryV1::new(
2500 primary,
2501 closure,
2502 crate::RawGltfAddressabilityInventoryInputV1 {
2503 default_scene: crate::RawGltfDefaultSceneObservationV1::Absent,
2504 scene_coverage: crate::RawGltfAddressabilityCoverageV1::Complete,
2505 scenes: Vec::new(),
2506 node_coverage: crate::RawGltfAddressabilityCoverageV1::Complete,
2507 nodes: Vec::new(),
2508 skin_coverage: crate::RawGltfAddressabilityCoverageV1::Complete,
2509 skins: Vec::new(),
2510 attachment_coverage: crate::RawGltfAddressabilityCoverageV1::Complete,
2511 attachments: Vec::new(),
2512 path_candidate_coverage: crate::RawGltfAddressabilityCoverageV1::Complete,
2513 path_candidates: Vec::new(),
2514 },
2515 )
2516 .unwrap()
2517 }
2518
2519 let primary = InputIdentity::from_bytes(b"gltf-addressability");
2520 let source = RawSourceFactsBuilderV1::new(SourceFormatV1::GltfJson, primary.clone())
2521 .finish(Document::default())
2522 .unwrap();
2523 let exact = inventory(primary.clone(), source.dependency_closure().clone());
2524 let source = source
2525 .with_raw_gltf_addressability_inventory(exact)
2526 .expect("exact sidecar binds");
2527 assert!(source.raw_gltf_addressability_inventory().is_some());
2528
2529 let wrong_primary = InputIdentity::from_bytes(b"other");
2530 let wrong = inventory(
2531 wrong_primary.clone(),
2532 DependencyClosureV1::unavailable(wrong_primary),
2533 );
2534 assert_eq!(
2535 RawSourceFactsBuilderV1::new(SourceFormatV1::GltfJson, primary.clone())
2536 .finish(Document::default())
2537 .unwrap()
2538 .with_raw_gltf_addressability_inventory(wrong)
2539 .unwrap_err(),
2540 RawGltfAddressabilityBindingErrorV1::PrimaryIdentityMismatch
2541 );
2542
2543 let wrong_closure = DependencyClosureV1::unavailable(primary.clone());
2544 let wrong = inventory(primary.clone(), wrong_closure);
2545 assert_eq!(
2546 RawSourceFactsBuilderV1::new(SourceFormatV1::GltfJson, primary)
2547 .finish(Document::default())
2548 .unwrap()
2549 .with_raw_gltf_addressability_inventory(wrong)
2550 .unwrap_err(),
2551 RawGltfAddressabilityBindingErrorV1::DependencyClosureMismatch
2552 );
2553 }
2554
2555 #[test]
2556 fn loaded_source_accepts_a_complete_format_bound_dependency_closure() {
2557 let primary = InputIdentity::from_bytes(b"gltf");
2558 let mut facts = RawSourceFactsBuilderV1::new(SourceFormatV1::GltfJson, primary.clone());
2559 assert!(facts.push_resource(SourceResourceReferenceV1::new(
2560 0,
2561 SourceResourceKindV1::Buffer,
2562 0,
2563 SourceResourceLocatorV1::classify("buffers/a%20b.bin"),
2564 SourceLoaderDispositionV1::Preserved,
2565 format_provenance(),
2566 )));
2567 facts.mark_complete(SourceFactDomainV1::Resources);
2568
2569 let key = crate::DependencyResourceKeyV1::from_source_str(
2570 "buffers/a b.bin",
2571 crate::ResourceKeySyntaxV1::GltfUri,
2572 )
2573 .unwrap();
2574 let mut closure = crate::DependencyClosureBuilderV1::new(
2575 primary.clone(),
2576 facts.resource_coverage(),
2577 facts.resource_rows().len(),
2578 );
2579 assert!(closure.begin_reference(17, 2));
2580 assert_eq!(closure.prepare_external_key(&key).unwrap(), Some(true));
2581 closure.record_external_open_attempt(&key).unwrap();
2582 assert!(
2583 closure
2584 .push_captured_external(
2585 0,
2586 SourceResourceKindV1::Buffer,
2587 0,
2588 key,
2589 InputIdentity::from_bytes(b"buffer"),
2590 )
2591 .unwrap()
2592 );
2593 let loaded = facts
2594 .finish_with_dependency_closure(Document::default(), closure.finish().unwrap())
2595 .unwrap();
2596 assert!(loaded.dependency_closure().coverage().is_complete());
2597 assert!(loaded.dependency_closure().identity().is_some());
2598 let document = loaded.into_document();
2599 assert!(document.clips.is_empty());
2600 }
2601
2602 #[test]
2603 fn nested_partial_channels_do_not_weaken_complete_clip_identity_coverage() {
2604 let mut builder = RawSourceFactsBuilderV1::new(
2605 SourceFormatV1::Fbx,
2606 InputIdentity::from_bytes(b"nested-partial"),
2607 );
2608 assert!(builder.push_clip(SourceClipFactV1::new(
2609 0,
2610 SourceObservationV1::proven_absent(format_provenance()),
2611 unavailable(),
2612 SourceObservationV1::proven_absent(format_provenance()),
2613 SourceObservationV1::proven_absent(format_provenance()),
2614 SourceFactSetV1::partial(
2615 vec![named_channel(0, "property")],
2616 SourceUnavailableReasonV1::ParserUnavailable,
2617 ),
2618 )));
2619 builder.mark_complete(SourceFactDomainV1::Clips);
2620 let loaded = builder
2621 .finish(Document::default())
2622 .expect("independent coverage");
2623 let facts = loaded.source_facts();
2624 assert_eq!(facts.clips().coverage(), SourceSetCoverageV1::complete());
2625 assert_eq!(
2626 facts.clips().rows()[0].channels().coverage(),
2627 SourceSetCoverageV1::partial(SourceUnavailableReasonV1::ParserUnavailable)
2628 );
2629 }
2630
2631 #[test]
2632 fn resource_locator_classification_redacts_unsafe_spelling() {
2633 let secret = "/home/example/private.bin";
2634 for classified in [
2635 SourceResourceLocatorV1::classify(secret),
2636 SourceResourceLocatorV1::classify("https://example.invalid/a.bin"),
2637 SourceResourceLocatorV1::classify("../escape.bin"),
2638 SourceResourceLocatorV1::classify("a/%2e%2e/escape.bin"),
2639 SourceResourceLocatorV1::classify("a/%2f/escape.bin"),
2640 SourceResourceLocatorV1::classify("bad%q0.bin"),
2641 ] {
2642 assert!(!format!("{classified:?}").contains(secret));
2643 assert!(!matches!(classified, SourceResourceLocatorV1::Relative(_)));
2644 }
2645 let control_bearing = "textures/TOP_SECRET\nname.png";
2646 let classified = SourceResourceLocatorV1::classify(control_bearing);
2647 assert_eq!(classified, SourceResourceLocatorV1::Malformed);
2648 assert!(!format!("{classified:?}").contains("TOP_SECRET"));
2649 assert_eq!(
2650 SourceResourceLocatorV1::retained_relative_bytes(control_bearing),
2651 0
2652 );
2653 let SourceResourceLocatorV1::Relative(relative) =
2654 SourceResourceLocatorV1::classify("textures/normal.png")
2655 else {
2656 panic!("safe relative declaration retained");
2657 };
2658 assert_eq!(relative.as_str(), "textures/normal.png");
2659 assert_eq!(
2660 SourceResourceLocatorV1::retained_relative_bytes("textures/normal.png"),
2661 "textures/normal.png".len()
2662 );
2663 assert_eq!(
2664 SourceResourceLocatorV1::retained_relative_bytes("../private.bin"),
2665 0
2666 );
2667 assert_eq!(
2668 SourceResourceLocatorV1::classify("data:image/png;base64,private"),
2669 SourceResourceLocatorV1::DataUri
2670 );
2671 assert_eq!(
2672 SourceResourceLocatorV1::classify("DATA:image/png;base64,private"),
2673 SourceResourceLocatorV1::DataUri
2674 );
2675 let oversized_data_uri = format!(
2676 "data:application/octet-stream;base64,{}",
2677 "A".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES)
2678 );
2679 assert_eq!(
2680 SourceResourceLocatorV1::classify(&oversized_data_uri),
2681 SourceResourceLocatorV1::DataUri
2682 );
2683 assert_eq!(
2684 SourceResourceLocatorV1::retained_relative_bytes(&oversized_data_uri),
2685 0
2686 );
2687 assert_eq!(
2688 SourceResourceLocatorV1::classify(r"C:\private\texture.png"),
2689 SourceResourceLocatorV1::Absolute
2690 );
2691 assert_eq!(
2692 SourceResourceLocatorV1::classify("file:///private/texture.png"),
2693 SourceResourceLocatorV1::Absolute
2694 );
2695 }
2696
2697 #[test]
2698 fn provenance_uses_only_validated_source_logical_locators() {
2699 for value in [
2700 "/home/private/input.glb",
2701 "/../animations",
2702 "https://host/path",
2703 ] {
2704 assert_eq!(
2705 SourceLogicalLocatorV1::gltf_json_pointer(value),
2706 Err(SourceFactsError::InvalidLogicalLocator)
2707 );
2708 }
2709 for value in [
2710 "/home/private/input.fbx",
2711 "fbx:/home/private",
2712 "fbx:../private",
2713 ] {
2714 assert_eq!(
2715 SourceLogicalLocatorV1::fbx_parser_path(value),
2716 Err(SourceFactsError::InvalidLogicalLocator)
2717 );
2718 }
2719
2720 let pointer = SourceLogicalLocatorV1::gltf_json_pointer("/animations/0/channels/1")
2721 .expect("generated glTF pointer");
2722 let provenance = SourceProvenanceV1::source_declared(pointer);
2723 assert_eq!(provenance.kind(), SourceProvenanceKindV1::SourceDeclared);
2724 assert_eq!(
2725 provenance.locator().map(SourceLogicalLocatorV1::as_str),
2726 Some("/animations/0/channels/1")
2727 );
2728 assert!(!format!("{provenance:?}").contains("animations"));
2729
2730 let path = SourceLogicalLocatorV1::fbx_parser_path("fbx:scene.settings.axes")
2731 .expect("generated FBX parser path");
2732 assert_eq!(
2733 SourceProvenanceV1::parser_projected(path).kind(),
2734 SourceProvenanceKindV1::ParserProjected
2735 );
2736 assert!(SourceProvenanceV1::format_defined().locator().is_none());
2737 }
2738
2739 #[test]
2740 fn loaded_source_binds_exact_identity_and_canonical_source_skeleton() {
2741 let bytes = b"same bytes parsed by the loader";
2742 let identity = InputIdentity::from_bytes(bytes);
2743 let mut document = Document::default();
2744 document.source.path = Some("/home/example/private/input.glb".into());
2745 let mut builder = RawSourceFactsBuilderV1::new(SourceFormatV1::Glb, identity.clone());
2746 builder.set_linear_unit(SourceObservationV1::observed(
2747 SourceLinearUnitV1::new(1.0).expect("metres"),
2748 format_provenance(),
2749 SourceLoaderDispositionV1::Preserved,
2750 ));
2751 let loaded = builder.finish(document).expect("facts bind");
2752 let source_skeleton_ptr = &loaded.document().assets.source_skeleton as *const _;
2753 let facts = loaded.source_facts();
2754 assert_eq!(facts.contract_id(), RAW_SOURCE_FACTS_V1_ID);
2755 assert_eq!(facts.format(), SourceFormatV1::Glb);
2756 assert_eq!(facts.primary_identity(), &identity);
2757 assert_eq!(facts.primary_identity().bytes(), bytes.len() as u64);
2758 assert!(loaded.exact_source_timing().is_none());
2759 assert!(std::ptr::eq(
2760 facts.source_skeleton() as *const _,
2761 source_skeleton_ptr
2762 ));
2763 assert_eq!(loaded.dependency_closure().primary_input(), &identity);
2764 assert!(matches!(
2765 loaded.dependency_closure().coverage(),
2766 crate::DependencyClosureCoverageV1::Unavailable { .. }
2767 ));
2768 assert!(loaded.dependency_closure().identity().is_none());
2769 assert!(!format!("{loaded:?}").contains("/home/example/private"));
2770 let document = loaded.into_document();
2771 assert!(document.clips.is_empty());
2772 }
2773
2774 #[test]
2775 fn exact_source_timing_attachment_is_format_neutral_and_rejects_mismatched_clip_domains() {
2776 let loaded = || {
2777 let mut builder = RawSourceFactsBuilderV1::new(
2778 SourceFormatV1::Glb,
2779 InputIdentity::from_bytes(b"generic-exact-source-timing"),
2780 );
2781 assert!(builder.push_clip(clip(0)));
2782 builder.mark_complete(SourceFactDomainV1::Clips);
2783 builder.finish(Document::default()).expect("facts bind")
2784 };
2785
2786 let exact = |coverage, clips: Vec<crate::ExactSourceClipTimingV1>| {
2787 ExactSourceTimingV1::new(
2788 exact_observed(crate::ExactSourceTimeBasisV1::new(1_000).unwrap()),
2789 exact_observed(crate::SourceTimelineModeV1::Fps24),
2790 exact_observed(crate::SourceTimelineModeV1::Fps24),
2791 crate::ExactSourceTimingObservationV1::proven_absent(format_provenance()),
2792 exact_observed(crate::ExactSourceFramePeriodV1::new(1).unwrap()),
2793 crate::ExactSourceTimingObservationV1::proven_absent(format_provenance()),
2794 exact_observed(crate::SourceTimeDisplayProtocolV1::Default),
2795 coverage,
2796 clips,
2797 )
2798 .unwrap()
2799 };
2800 let one_clip = || {
2801 vec![crate::ExactSourceClipTimingV1::new(
2802 0,
2803 exact_observed(
2804 crate::ExactSourceClipTimeRangeV1::new(
2805 crate::ExactSourceRangeSelectionV1::Primary,
2806 0,
2807 1,
2808 )
2809 .unwrap(),
2810 ),
2811 )]
2812 };
2813
2814 let attached = loaded()
2815 .with_exact_source_timing(exact(SourceSetCoverageV1::complete(), one_clip()))
2816 .expect("generic exact timing attaches to a GLB source");
2817 assert!(attached.exact_source_timing().is_some());
2818
2819 assert!(matches!(
2820 loaded().with_exact_source_timing(exact(SourceSetCoverageV1::complete(), Vec::new())),
2821 Err(ExactSourceTimingContractError::ClipCountMismatch {
2822 exact: 0,
2823 source_count: 1,
2824 })
2825 ));
2826 assert!(matches!(
2827 loaded().with_exact_source_timing(exact(
2828 SourceSetCoverageV1::partial(SourceUnavailableReasonV1::ParserUnavailable),
2829 one_clip(),
2830 )),
2831 Err(ExactSourceTimingContractError::ClipCoverageMismatch)
2832 ));
2833 }
2834
2835 #[test]
2836 fn clip_limit_retains_n_then_marks_n_plus_one_partial() {
2837 let mut builder =
2838 RawSourceFactsBuilderV1::new(SourceFormatV1::Fbx, InputIdentity::from_bytes(b"fbx"));
2839 for index in 0..RAW_SOURCE_V1_MAX_CLIPS {
2840 assert!(builder.push_clip(clip(index)));
2841 }
2842 assert!(!builder.push_clip(clip(RAW_SOURCE_V1_MAX_CLIPS)));
2843 assert!(!builder.push_clip(clip(RAW_SOURCE_V1_MAX_CLIPS + 1)));
2844 let loaded = builder.finish(Document::default()).expect("bounded facts");
2845 let facts = loaded.source_facts();
2846 assert_eq!(facts.clips().rows().len(), RAW_SOURCE_V1_MAX_CLIPS);
2847 assert_eq!(
2848 facts.clips().coverage(),
2849 SourceSetCoverageV1::partial(SourceUnavailableReasonV1::ProjectionBudgetExceeded)
2850 );
2851 assert_eq!(facts.work().inspected_rows(), RAW_SOURCE_V1_MAX_CLIPS + 1);
2852 }
2853
2854 #[test]
2855 fn resource_limit_retains_n_then_marks_n_plus_one_partial() {
2856 let mut builder = RawSourceFactsBuilderV1::new(
2857 SourceFormatV1::GltfJson,
2858 InputIdentity::from_bytes(b"gltf"),
2859 );
2860 for index in 0..RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES {
2861 assert!(builder.push_resource(resource(index)));
2862 }
2863 assert!(!builder.push_resource(resource(RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES)));
2864 assert!(!builder.push_resource(resource(RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES + 1)));
2865 let loaded = builder.finish(Document::default()).expect("bounded facts");
2866 let facts = loaded.source_facts();
2867 assert_eq!(
2868 facts.resources().rows().len(),
2869 RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES
2870 );
2871 assert_eq!(
2872 facts.resources().coverage().state(),
2873 SourceSetCoverageStateV1::Partial
2874 );
2875 assert_eq!(
2876 facts.work().inspected_rows(),
2877 RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES + 1
2878 );
2879 }
2880
2881 #[test]
2882 fn observed_clip_names_count_toward_the_aggregate_text_limit() {
2883 let rows_at_limit = RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES / RAW_SOURCE_V1_MAX_TEXT_BYTES;
2884 let mut named_clips = RawSourceFactsBuilderV1::new(
2885 SourceFormatV1::GltfJson,
2886 InputIdentity::from_bytes(b"named-clips"),
2887 );
2888 for index in 0..rows_at_limit {
2889 let source_name = SourceObservationV1::observed(
2890 SourceTextV1::new("n".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES)).expect("bounded name"),
2891 format_provenance(),
2892 SourceLoaderDispositionV1::Preserved,
2893 );
2894 assert!(named_clips.push_clip(SourceClipFactV1::new(
2895 index,
2896 source_name,
2897 SourceObservationV1::proven_absent(format_provenance()),
2898 SourceObservationV1::proven_absent(format_provenance()),
2899 SourceObservationV1::proven_absent(format_provenance()),
2900 SourceFactSetV1::complete(Vec::new()),
2901 )));
2902 }
2903 let overflow_name = SourceObservationV1::observed(
2904 SourceTextV1::new("n".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES)).expect("bounded name"),
2905 format_provenance(),
2906 SourceLoaderDispositionV1::Preserved,
2907 );
2908 assert!(!named_clips.push_clip(SourceClipFactV1::new(
2909 rows_at_limit,
2910 overflow_name,
2911 SourceObservationV1::proven_absent(format_provenance()),
2912 SourceObservationV1::proven_absent(format_provenance()),
2913 SourceObservationV1::proven_absent(format_provenance()),
2914 SourceFactSetV1::complete(Vec::new()),
2915 )));
2916 let loaded = named_clips
2917 .finish(Document::default())
2918 .expect("bounded named clips");
2919 assert_eq!(
2920 loaded.source_facts().work().retained_text_bytes(),
2921 RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES
2922 );
2923 assert_eq!(
2924 loaded.source_facts().clips().coverage().state(),
2925 SourceSetCoverageStateV1::Partial
2926 );
2927 }
2928
2929 #[test]
2930 fn aggregate_text_limit_retains_nested_channel_prefix() {
2931 let mut builder = RawSourceFactsBuilderV1::new(
2932 SourceFormatV1::GltfJson,
2933 InputIdentity::from_bytes(b"channel-prefix"),
2934 );
2935 let full_rows = RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES / RAW_SOURCE_V1_MAX_TEXT_BYTES;
2936 for _ in 0..full_rows - 1 {
2937 assert!(builder.push_construct(construct(
2938 builder.facts.constructs.rows.len(),
2939 "x".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES)
2940 )));
2941 }
2942 assert!(builder.push_construct(construct(
2943 builder.facts.constructs.rows.len(),
2944 "x".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES - 12)
2945 )));
2946 assert_eq!(builder.remaining_text_bytes(), 12);
2947
2948 let channels = SourceFactSetV1::complete(vec![
2949 named_channel(0, "aaaa"),
2950 named_channel(1, "bbbb"),
2951 named_channel(2, "cccc"),
2952 ]);
2953 assert!(builder.push_clip(SourceClipFactV1::new(
2954 0,
2955 SourceObservationV1::observed(
2956 SourceTextV1::new("name").expect("bounded name"),
2957 format_provenance(),
2958 SourceLoaderDispositionV1::Preserved,
2959 ),
2960 SourceObservationV1::proven_absent(format_provenance()),
2961 SourceObservationV1::proven_absent(format_provenance()),
2962 SourceObservationV1::proven_absent(format_provenance()),
2963 channels,
2964 )));
2965
2966 let loaded = builder.finish(Document::default()).expect("bounded prefix");
2967 let facts = loaded.source_facts();
2968 assert_eq!(
2969 facts.work().retained_text_bytes(),
2970 RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES
2971 );
2972 assert_eq!(facts.clips().rows().len(), 1);
2973 assert_eq!(facts.clips().rows()[0].channels().rows().len(), 2);
2974 assert_eq!(
2975 facts.clips().rows()[0].channels().coverage().state(),
2976 SourceSetCoverageStateV1::Partial
2977 );
2978 assert_eq!(
2979 facts.clips().coverage().state(),
2980 SourceSetCoverageStateV1::Partial
2981 );
2982 }
2983
2984 #[test]
2985 fn total_observation_limit_retains_exact_prefix_and_work_count() {
2986 let mut builder = RawSourceFactsBuilderV1::new(
2987 SourceFormatV1::GltfJson,
2988 InputIdentity::from_bytes(b"gltf"),
2989 );
2990 for index in 0..=RAW_SOURCE_V1_MAX_OBSERVATIONS {
2991 let retained = builder.push_construct(construct(index, format!("e{index}")));
2992 assert_eq!(retained, index < RAW_SOURCE_V1_MAX_OBSERVATIONS);
2993 }
2994 assert!(!builder.push_construct(construct(
2995 RAW_SOURCE_V1_MAX_OBSERVATIONS + 1,
2996 "must-not-resume".to_string()
2997 )));
2998 let loaded = builder.finish(Document::default()).expect("bounded facts");
2999 let facts = loaded.source_facts();
3000 assert_eq!(
3001 facts.constructs().rows().len(),
3002 RAW_SOURCE_V1_MAX_OBSERVATIONS
3003 );
3004 assert_eq!(
3005 facts.constructs().coverage(),
3006 SourceSetCoverageV1::partial(SourceUnavailableReasonV1::ProjectionBudgetExceeded)
3007 );
3008 assert_eq!(
3009 facts.work().inspected_rows(),
3010 RAW_SOURCE_V1_MAX_OBSERVATIONS + 1
3011 );
3012 assert_eq!(facts.work().retained_rows(), RAW_SOURCE_V1_MAX_OBSERVATIONS);
3013 }
3014
3015 #[test]
3016 fn unavailable_domain_discards_prefix_and_updates_retained_work() {
3017 let mut builder = RawSourceFactsBuilderV1::new(
3018 SourceFormatV1::GltfJson,
3019 InputIdentity::from_bytes(b"discarded-prefix"),
3020 );
3021 assert!(builder.push_construct(construct(0, "retained-name".to_string())));
3022 assert_eq!(
3023 builder.remaining_observation_rows(),
3024 RAW_SOURCE_V1_MAX_OBSERVATIONS - 1
3025 );
3026 assert_eq!(
3027 builder.remaining_text_bytes(),
3028 RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES - 13
3029 );
3030
3031 builder.mark_unavailable(
3032 SourceFactDomainV1::Constructs,
3033 SourceUnavailableReasonV1::ParserUnavailable,
3034 );
3035 builder.mark_partial(
3036 SourceFactDomainV1::Constructs,
3037 SourceUnavailableReasonV1::ProjectionBudgetExceeded,
3038 );
3039 let loaded = builder
3040 .finish(Document::default())
3041 .expect("unavailable set remains valid");
3042 let facts = loaded.source_facts();
3043 assert!(facts.constructs().rows().is_empty());
3044 assert_eq!(
3045 facts.constructs().coverage(),
3046 SourceSetCoverageV1::unavailable(SourceUnavailableReasonV1::ParserUnavailable)
3047 );
3048 assert_eq!(facts.work().retained_rows(), 0);
3049 assert_eq!(facts.work().retained_text_bytes(), 0);
3050 assert_eq!(facts.work().inspected_rows(), 1);
3051 }
3052
3053 #[test]
3054 fn preallocation_budget_stop_counts_terminal_row() {
3055 let mut builder = RawSourceFactsBuilderV1::new(
3056 SourceFormatV1::GltfJson,
3057 InputIdentity::from_bytes(b"preallocation-stop"),
3058 );
3059 assert!(builder.push_construct(construct(0, "first".to_string())));
3060 builder.mark_budget_exceeded(SourceFactDomainV1::Constructs);
3061 assert!(!builder.push_construct(construct(1, "must-not-resume".to_string())));
3062 let loaded = builder.finish(Document::default()).expect("partial prefix");
3063 let facts = loaded.source_facts();
3064 assert_eq!(facts.work().inspected_rows(), 2);
3065 assert_eq!(facts.work().retained_rows(), 1);
3066 assert_eq!(facts.constructs().rows()[0].name().as_str(), "first");
3067 assert_eq!(
3068 facts.constructs().coverage(),
3069 SourceSetCoverageV1::partial(SourceUnavailableReasonV1::ProjectionBudgetExceeded)
3070 );
3071 }
3072
3073 #[test]
3074 fn text_and_traversal_limits_are_exact_and_coverage_qualified() {
3075 assert!(SourceTextV1::new("x".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES)).is_ok());
3076 assert_eq!(
3077 SourceTextV1::new("x".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES + 1)),
3078 Err(SourceFactsError::TextTooLong {
3079 bytes: RAW_SOURCE_V1_MAX_TEXT_BYTES + 1,
3080 limit: RAW_SOURCE_V1_MAX_TEXT_BYTES,
3081 })
3082 );
3083
3084 let mut builder = RawSourceFactsBuilderV1::new(
3085 SourceFormatV1::GltfJson,
3086 InputIdentity::from_bytes(b"gltf"),
3087 );
3088 let rows_at_limit = RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES / RAW_SOURCE_V1_MAX_TEXT_BYTES;
3089 for index in 0..rows_at_limit {
3090 assert!(
3091 builder.push_construct(construct(index, "x".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES)))
3092 );
3093 }
3094 assert!(!builder.push_construct(construct(
3095 rows_at_limit,
3096 "x".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES)
3097 )));
3098 assert!(!builder.push_construct(construct(rows_at_limit + 1, "short".to_string())));
3099 assert!(
3100 !builder.set_linear_unit(SourceObservationV1::observed(
3101 SourceLinearUnitV1::new(1.0).expect("metres"),
3102 SourceProvenanceV1::source_declared(
3103 SourceLogicalLocatorV1::gltf_json_pointer("/animations")
3104 .expect("generated logical locator"),
3105 ),
3106 SourceLoaderDispositionV1::Preserved,
3107 ))
3108 );
3109 assert!(builder.observe_traversal_depth(
3110 SourceFactDomainV1::Resources,
3111 RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH
3112 ));
3113 assert!(!builder.observe_traversal_depth(
3114 SourceFactDomainV1::Resources,
3115 RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH + 1
3116 ));
3117 assert!(!builder.push_resource(resource(0)));
3118 let loaded = builder.finish(Document::default()).expect("bounded facts");
3119 let facts = loaded.source_facts();
3120 assert_eq!(
3121 facts.work().retained_text_bytes(),
3122 RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES
3123 );
3124 assert!(matches!(
3125 facts.linear_unit().state(),
3126 SourceObservationStateV1::Unavailable(
3127 SourceUnavailableReasonV1::ProjectionBudgetExceeded
3128 )
3129 ));
3130 assert_eq!(
3131 facts.work().max_traversal_depth(),
3132 RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH + 1
3133 );
3134 assert_eq!(
3135 facts.resources().coverage().state(),
3136 SourceSetCoverageStateV1::Partial
3137 );
3138 }
3139
3140 #[test]
3141 fn finish_rejects_stale_normalized_clip_mappings_and_noncanonical_order() {
3142 let observed_index = |index| {
3143 SourceObservationV1::observed(
3144 index,
3145 format_provenance(),
3146 SourceLoaderDispositionV1::Preserved,
3147 )
3148 };
3149 let make = |source_index, normalized_index| {
3150 SourceClipFactV1::new(
3151 source_index,
3152 SourceObservationV1::proven_absent(format_provenance()),
3153 observed_index(normalized_index),
3154 SourceObservationV1::proven_absent(format_provenance()),
3155 SourceObservationV1::proven_absent(format_provenance()),
3156 SourceFactSetV1::complete(Vec::new()),
3157 )
3158 };
3159
3160 let mut stale =
3161 RawSourceFactsBuilderV1::new(SourceFormatV1::Fbx, InputIdentity::from_bytes(b"fbx"));
3162 assert!(stale.push_clip(make(0, 0)));
3163 assert!(matches!(
3164 stale.finish(Document::default()),
3165 Err(SourceFactsError::NormalizedClipIndexOutOfRange { .. })
3166 ));
3167
3168 let mut unordered =
3169 RawSourceFactsBuilderV1::new(SourceFormatV1::Fbx, InputIdentity::from_bytes(b"fbx"));
3170 assert!(unordered.push_clip(clip(1)));
3171 assert!(unordered.push_clip(clip(0)));
3172 assert!(matches!(
3173 unordered.finish(Document::default()),
3174 Err(SourceFactsError::NonCanonicalClipIndex { .. })
3175 ));
3176
3177 let mut channel_gap =
3178 RawSourceFactsBuilderV1::new(SourceFormatV1::Fbx, InputIdentity::from_bytes(b"fbx"));
3179 assert!(channel_gap.push_clip(SourceClipFactV1::new(
3180 0,
3181 SourceObservationV1::proven_absent(format_provenance()),
3182 unavailable(),
3183 SourceObservationV1::proven_absent(format_provenance()),
3184 SourceObservationV1::proven_absent(format_provenance()),
3185 SourceFactSetV1::partial(
3186 vec![named_channel(1, "gap")],
3187 SourceUnavailableReasonV1::ParserUnavailable,
3188 ),
3189 )));
3190 assert!(matches!(
3191 channel_gap.finish(Document::default()),
3192 Err(SourceFactsError::NonCanonicalChannelIndex { .. })
3193 ));
3194
3195 let mut construct_gap = RawSourceFactsBuilderV1::new(
3196 SourceFormatV1::GltfJson,
3197 InputIdentity::from_bytes(b"gltf"),
3198 );
3199 assert!(construct_gap.push_construct(construct(1, "gap".to_string())));
3200 assert!(matches!(
3201 construct_gap.finish(Document::default()),
3202 Err(SourceFactsError::NonCanonicalConstructOrder { .. })
3203 ));
3204
3205 let mut resource_gap = RawSourceFactsBuilderV1::new(
3206 SourceFormatV1::GltfJson,
3207 InputIdentity::from_bytes(b"gltf"),
3208 );
3209 assert!(resource_gap.push_resource(resource(1)));
3210 assert!(matches!(
3211 resource_gap.finish(Document::default()),
3212 Err(SourceFactsError::NonCanonicalResourceOrder { .. })
3213 ));
3214 }
3215}