1use crate::{
7 DependencyClosureError, DependencyClosureV1, Document, InputIdentity, SourceSkeletonAssets,
8};
9use serde::Serialize;
10use std::fmt;
11
12pub const RAW_SOURCE_FACTS_V1_ID: &str = "urn:animsmith:raw-source-facts:1";
14pub const RAW_SOURCE_V1_MAX_OBSERVATIONS: usize = 65_536;
16pub const RAW_SOURCE_V1_MAX_CLIPS: usize = 4_096;
18pub const RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES: usize = 4_096;
20pub const RAW_SOURCE_V1_MAX_TEXT_BYTES: usize = 4_096;
22pub const RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES: usize = 8 * 1024 * 1024;
24pub const RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH: usize = 128;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
29pub enum SourceFormatV1 {
30 GltfJson,
32 Glb,
34 Fbx,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
40pub enum SourceUnavailableReasonV1 {
41 Malformed,
43 Discarded,
45 NormalizedAway,
47 BakedAway,
49 LoaderUnsupported,
51 ProjectionBudgetExceeded,
53 ParserUnavailable,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum SourceSetCoverageStateV1 {
60 Complete,
62 Partial,
64 Unavailable,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub struct SourceSetCoverageV1 {
71 state: SourceSetCoverageStateV1,
72 reason: Option<SourceUnavailableReasonV1>,
73}
74
75impl SourceSetCoverageV1 {
76 pub const fn complete() -> Self {
78 Self {
79 state: SourceSetCoverageStateV1::Complete,
80 reason: None,
81 }
82 }
83
84 pub const fn partial(reason: SourceUnavailableReasonV1) -> Self {
86 Self {
87 state: SourceSetCoverageStateV1::Partial,
88 reason: Some(reason),
89 }
90 }
91
92 pub const fn unavailable(reason: SourceUnavailableReasonV1) -> Self {
94 Self {
95 state: SourceSetCoverageStateV1::Unavailable,
96 reason: Some(reason),
97 }
98 }
99
100 pub const fn state(self) -> SourceSetCoverageStateV1 {
102 self.state
103 }
104
105 pub const fn reason(self) -> Option<SourceUnavailableReasonV1> {
107 self.reason
108 }
109
110 pub const fn proves_absence(self) -> bool {
112 matches!(self.state, SourceSetCoverageStateV1::Complete)
113 }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
118pub enum SourceLoaderDispositionV1 {
119 Preserved,
121 Normalized,
123 Baked,
125 Discarded,
127 Unsupported,
129 Unknown,
131 NotApplicable,
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
137pub enum SourceProvenanceKindV1 {
138 FormatDefined,
140 SourceDeclared,
142 ParserProjected,
144 DerivedFromSource,
146}
147
148#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
150pub struct SourceTextV1(String);
151
152impl SourceTextV1 {
153 pub fn new(value: impl AsRef<str>) -> Result<Self, SourceFactsError> {
159 let value = value.as_ref();
160 if value.len() > RAW_SOURCE_V1_MAX_TEXT_BYTES {
161 return Err(SourceFactsError::TextTooLong {
162 bytes: value.len(),
163 limit: RAW_SOURCE_V1_MAX_TEXT_BYTES,
164 });
165 }
166 Ok(Self(value.to_owned()))
167 }
168
169 pub fn as_str(&self) -> &str {
171 &self.0
172 }
173
174 fn retained_bytes(&self) -> usize {
175 self.0.len()
176 }
177}
178
179impl fmt::Debug for SourceTextV1 {
180 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
181 formatter
182 .debug_tuple("SourceTextV1")
183 .field(&self.0)
184 .finish()
185 }
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
190pub struct SourceLogicalLocatorV1 {
191 text: SourceTextV1,
192}
193
194impl SourceLogicalLocatorV1 {
195 pub fn gltf_json_pointer(value: impl AsRef<str>) -> Result<Self, SourceFactsError> {
202 let value = value.as_ref();
203 let mut segments = value
204 .strip_prefix('/')
205 .into_iter()
206 .flat_map(|value| value.split('/'));
207 let valid_root = matches!(
208 segments.next(),
209 Some("animations" | "buffers" | "images" | "extensionsUsed" | "extensionsRequired")
210 );
211 if !valid_root || !segments.all(valid_logical_segment) {
212 return Err(SourceFactsError::InvalidLogicalLocator);
213 }
214 Ok(Self {
215 text: SourceTextV1::new(value)?,
216 })
217 }
218
219 pub fn fbx_parser_path(value: impl AsRef<str>) -> Result<Self, SourceFactsError> {
227 let value = value.as_ref();
228 let Some(path) = value.strip_prefix("fbx:") else {
229 return Err(SourceFactsError::InvalidLogicalLocator);
230 };
231 if path.is_empty()
232 || path
233 .split('/')
234 .any(|segment| !valid_logical_segment(segment))
235 {
236 return Err(SourceFactsError::InvalidLogicalLocator);
237 }
238 Ok(Self {
239 text: SourceTextV1::new(value)?,
240 })
241 }
242
243 pub fn as_str(&self) -> &str {
245 self.text.as_str()
246 }
247
248 fn retained_bytes(&self) -> usize {
249 self.text.retained_bytes()
250 }
251}
252
253fn valid_logical_segment(segment: &str) -> bool {
254 !segment.is_empty()
255 && !matches!(segment, "." | "..")
256 && segment.bytes().all(|value| {
257 value.is_ascii_alphanumeric() || matches!(value, b'_' | b'-' | b'.' | b'*')
258 })
259}
260
261#[derive(Clone, PartialEq, Eq)]
263pub struct SourceProvenanceV1 {
264 kind: SourceProvenanceKindV1,
265 locator: Option<SourceLogicalLocatorV1>,
266}
267
268impl fmt::Debug for SourceProvenanceV1 {
269 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
270 formatter
271 .debug_struct("SourceProvenanceV1")
272 .field("kind", &self.kind)
273 .field("locator_retained", &self.locator.is_some())
274 .finish()
275 }
276}
277
278impl SourceProvenanceV1 {
279 pub const fn format_defined() -> Self {
281 Self {
282 kind: SourceProvenanceKindV1::FormatDefined,
283 locator: None,
284 }
285 }
286
287 pub fn source_declared(locator: SourceLogicalLocatorV1) -> Self {
289 Self {
290 kind: SourceProvenanceKindV1::SourceDeclared,
291 locator: Some(locator),
292 }
293 }
294
295 pub fn parser_projected(locator: SourceLogicalLocatorV1) -> Self {
297 Self {
298 kind: SourceProvenanceKindV1::ParserProjected,
299 locator: Some(locator),
300 }
301 }
302
303 pub fn derived_from_source(locator: SourceLogicalLocatorV1) -> Self {
305 Self {
306 kind: SourceProvenanceKindV1::DerivedFromSource,
307 locator: Some(locator),
308 }
309 }
310
311 pub const fn kind(&self) -> SourceProvenanceKindV1 {
313 self.kind
314 }
315
316 pub fn locator(&self) -> Option<&SourceLogicalLocatorV1> {
318 self.locator.as_ref()
319 }
320
321 fn retained_bytes(&self) -> usize {
322 self.locator
323 .as_ref()
324 .map_or(0, SourceLogicalLocatorV1::retained_bytes)
325 }
326}
327
328#[derive(Debug, Clone, PartialEq)]
330pub enum SourceObservationStateV1<T> {
331 Observed(T),
333 ProvenAbsent,
335 Unavailable(SourceUnavailableReasonV1),
337}
338
339#[derive(Debug, Clone, PartialEq)]
341pub struct SourceObservationV1<T> {
342 state: SourceObservationStateV1<T>,
343 disposition: SourceLoaderDispositionV1,
344 provenance: Option<SourceProvenanceV1>,
345}
346
347impl<T> SourceObservationV1<T> {
348 pub fn observed(
350 value: T,
351 provenance: SourceProvenanceV1,
352 disposition: SourceLoaderDispositionV1,
353 ) -> Self {
354 Self {
355 state: SourceObservationStateV1::Observed(value),
356 disposition,
357 provenance: Some(provenance),
358 }
359 }
360
361 pub fn proven_absent(provenance: SourceProvenanceV1) -> Self {
363 Self {
364 state: SourceObservationStateV1::ProvenAbsent,
365 disposition: SourceLoaderDispositionV1::NotApplicable,
366 provenance: Some(provenance),
367 }
368 }
369
370 pub fn unavailable(
372 reason: SourceUnavailableReasonV1,
373 provenance: Option<SourceProvenanceV1>,
374 disposition: SourceLoaderDispositionV1,
375 ) -> Self {
376 Self {
377 state: SourceObservationStateV1::Unavailable(reason),
378 disposition,
379 provenance,
380 }
381 }
382
383 pub const fn state(&self) -> &SourceObservationStateV1<T> {
385 &self.state
386 }
387
388 pub const fn disposition(&self) -> SourceLoaderDispositionV1 {
390 self.disposition
391 }
392
393 pub fn provenance(&self) -> Option<&SourceProvenanceV1> {
395 self.provenance.as_ref()
396 }
397
398 fn retained_bytes(&self) -> usize {
399 self.provenance
400 .as_ref()
401 .map_or(0, SourceProvenanceV1::retained_bytes)
402 }
403}
404
405impl SourceObservationV1<SourceTextV1> {
406 fn retained_text_bytes(&self) -> usize {
407 let value_bytes = match &self.state {
408 SourceObservationStateV1::Observed(value) => value.retained_bytes(),
409 SourceObservationStateV1::ProvenAbsent | SourceObservationStateV1::Unavailable(_) => 0,
410 };
411 value_bytes.saturating_add(self.retained_bytes())
412 }
413}
414
415#[derive(Debug, Clone, PartialEq)]
417pub struct SourceFactSetV1<T> {
418 coverage: SourceSetCoverageV1,
419 rows: Vec<T>,
420}
421
422impl<T> SourceFactSetV1<T> {
423 pub fn complete(rows: Vec<T>) -> Self {
425 Self {
426 coverage: SourceSetCoverageV1::complete(),
427 rows,
428 }
429 }
430
431 pub fn partial(rows: Vec<T>, reason: SourceUnavailableReasonV1) -> Self {
433 Self {
434 coverage: SourceSetCoverageV1::partial(reason),
435 rows,
436 }
437 }
438
439 pub fn unavailable(reason: SourceUnavailableReasonV1) -> Self {
441 Self {
442 coverage: SourceSetCoverageV1::unavailable(reason),
443 rows: Vec::new(),
444 }
445 }
446
447 pub const fn coverage(&self) -> SourceSetCoverageV1 {
449 self.coverage
450 }
451
452 pub fn rows(&self) -> &[T] {
454 &self.rows
455 }
456
457 pub fn proves_absence(&self) -> bool {
459 self.rows.is_empty() && self.coverage.proves_absence()
460 }
461
462 fn mark_partial(&mut self, reason: SourceUnavailableReasonV1) {
463 self.coverage = SourceSetCoverageV1::partial(reason);
464 }
465}
466
467#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
469pub enum SourceAxisV1 {
470 PositiveX,
472 NegativeX,
474 PositiveY,
476 NegativeY,
478 PositiveZ,
480 NegativeZ,
482}
483
484impl SourceAxisV1 {
485 const fn unsigned(self) -> u8 {
486 match self {
487 Self::PositiveX | Self::NegativeX => 0,
488 Self::PositiveY | Self::NegativeY => 1,
489 Self::PositiveZ | Self::NegativeZ => 2,
490 }
491 }
492
493 const fn vector(self) -> [i8; 3] {
494 match self {
495 Self::PositiveX => [1, 0, 0],
496 Self::NegativeX => [-1, 0, 0],
497 Self::PositiveY => [0, 1, 0],
498 Self::NegativeY => [0, -1, 0],
499 Self::PositiveZ => [0, 0, 1],
500 Self::NegativeZ => [0, 0, -1],
501 }
502 }
503}
504
505#[derive(Debug, Clone, Copy, PartialEq, Eq)]
507pub enum SourceHandednessV1 {
508 Right,
510 Left,
512}
513
514#[derive(Debug, Clone, Copy, PartialEq, Eq)]
516pub struct SourceCoordinateBasisV1 {
517 right: SourceAxisV1,
518 up: SourceAxisV1,
519 forward: SourceAxisV1,
520}
521
522impl SourceCoordinateBasisV1 {
523 pub fn new(
530 right: SourceAxisV1,
531 up: SourceAxisV1,
532 forward: SourceAxisV1,
533 ) -> Result<Self, SourceFactsError> {
534 if right.unsigned() == up.unsigned()
535 || right.unsigned() == forward.unsigned()
536 || up.unsigned() == forward.unsigned()
537 {
538 return Err(SourceFactsError::DuplicateBasisAxis);
539 }
540 Ok(Self { right, up, forward })
541 }
542
543 pub const fn right(self) -> SourceAxisV1 {
545 self.right
546 }
547
548 pub const fn up(self) -> SourceAxisV1 {
550 self.up
551 }
552
553 pub const fn forward(self) -> SourceAxisV1 {
555 self.forward
556 }
557
558 pub fn handedness(self) -> SourceHandednessV1 {
560 let [rx, ry, rz] = self.right.vector();
561 let [ux, uy, uz] = self.up.vector();
562 let [fx, fy, fz] = self.forward.vector();
563 let determinant = i16::from(rx)
564 * (i16::from(uy) * i16::from(fz) - i16::from(uz) * i16::from(fy))
565 - i16::from(ry) * (i16::from(ux) * i16::from(fz) - i16::from(uz) * i16::from(fx))
566 + i16::from(rz) * (i16::from(ux) * i16::from(fy) - i16::from(uy) * i16::from(fx));
567 if determinant > 0 {
568 SourceHandednessV1::Right
569 } else {
570 SourceHandednessV1::Left
571 }
572 }
573}
574
575#[derive(Debug, Clone, Copy, PartialEq)]
577pub struct SourceLinearUnitV1(f64);
578
579impl SourceLinearUnitV1 {
580 pub fn new(meters_per_source_unit: f64) -> Result<Self, SourceFactsError> {
586 if !meters_per_source_unit.is_finite() || meters_per_source_unit <= 0.0 {
587 return Err(SourceFactsError::InvalidLinearUnit);
588 }
589 Ok(Self(meters_per_source_unit))
590 }
591
592 pub const fn meters_per_source_unit(self) -> f64 {
594 self.0
595 }
596}
597
598#[derive(Debug, Clone, Copy, PartialEq)]
600pub struct SourceFramesPerSecondV1(f64);
601
602impl SourceFramesPerSecondV1 {
603 pub fn new(value: f64) -> Result<Self, SourceFactsError> {
609 if !value.is_finite() || value <= 0.0 {
610 return Err(SourceFactsError::InvalidFramesPerSecond);
611 }
612 Ok(Self(value))
613 }
614
615 pub const fn get(self) -> f64 {
617 self.0
618 }
619}
620
621#[derive(Debug, Clone, Copy, PartialEq)]
623pub struct SourceTimeRangeV1 {
624 begin_s: f64,
625 end_s: f64,
626}
627
628impl SourceTimeRangeV1 {
629 pub fn new(begin_s: f64, end_s: f64) -> Result<Self, SourceFactsError> {
635 if !begin_s.is_finite() || !end_s.is_finite() || begin_s > end_s {
636 return Err(SourceFactsError::InvalidTimeRange);
637 }
638 Ok(Self { begin_s, end_s })
639 }
640
641 pub const fn begin_s(self) -> f64 {
643 self.begin_s
644 }
645
646 pub const fn end_s(self) -> f64 {
648 self.end_s
649 }
650}
651
652#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
654pub enum SourceChannelPropertyV1 {
655 Translation,
657 Rotation,
659 Scale,
661 Weights,
663 Other,
665}
666
667#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
669pub enum SourceInterpolationV1 {
670 Step,
672 Linear,
674 CubicSpline,
676 Other,
678}
679
680#[derive(Debug, Clone, Copy, PartialEq, Eq)]
682pub struct SourceComponentMaskV1 {
683 x: bool,
684 y: bool,
685 z: bool,
686}
687
688impl SourceComponentMaskV1 {
689 pub const fn new(x: bool, y: bool, z: bool) -> Self {
691 Self { x, y, z }
692 }
693
694 pub const fn x(self) -> bool {
696 self.x
697 }
698
699 pub const fn y(self) -> bool {
701 self.y
702 }
703
704 pub const fn z(self) -> bool {
706 self.z
707 }
708}
709
710#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
712pub enum SourceTargetKindV1 {
713 Node,
715 Element,
717 Other,
719}
720
721#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
723pub struct SourceTargetV1 {
724 kind: SourceTargetKindV1,
725 index: u64,
726}
727
728impl SourceTargetV1 {
729 pub const fn new(kind: SourceTargetKindV1, index: u64) -> Self {
731 Self { kind, index }
732 }
733
734 pub const fn kind(self) -> SourceTargetKindV1 {
736 self.kind
737 }
738
739 pub const fn index(self) -> u64 {
741 self.index
742 }
743}
744
745#[derive(Debug, Clone, PartialEq)]
747pub struct SourceChannelFactV1 {
748 source_channel_index: usize,
749 source_layer_index: Option<usize>,
750 target: SourceTargetV1,
751 property: SourceChannelPropertyV1,
752 property_name: Option<SourceTextV1>,
753 components: SourceComponentMaskV1,
754 interpolation: SourceObservationV1<SourceInterpolationV1>,
755 input_accessor_index: Option<usize>,
756 output_accessor_index: Option<usize>,
757 disposition: SourceLoaderDispositionV1,
758 provenance: SourceProvenanceV1,
759}
760
761impl SourceChannelFactV1 {
762 pub fn new(
764 source_channel_index: usize,
765 target: SourceTargetV1,
766 property: SourceChannelPropertyV1,
767 components: SourceComponentMaskV1,
768 interpolation: SourceObservationV1<SourceInterpolationV1>,
769 disposition: SourceLoaderDispositionV1,
770 provenance: SourceProvenanceV1,
771 ) -> Self {
772 Self {
773 source_channel_index,
774 source_layer_index: None,
775 target,
776 property,
777 property_name: None,
778 components,
779 interpolation,
780 input_accessor_index: None,
781 output_accessor_index: None,
782 disposition,
783 provenance,
784 }
785 }
786
787 pub fn with_source_layer_index(mut self, index: usize) -> Self {
789 self.source_layer_index = Some(index);
790 self
791 }
792
793 pub fn with_property_name(mut self, name: SourceTextV1) -> Self {
795 self.property_name = Some(name);
796 self
797 }
798
799 pub fn with_accessors(mut self, input: usize, output: usize) -> Self {
801 self.input_accessor_index = Some(input);
802 self.output_accessor_index = Some(output);
803 self
804 }
805
806 pub const fn source_channel_index(&self) -> usize {
808 self.source_channel_index
809 }
810
811 pub const fn source_layer_index(&self) -> Option<usize> {
813 self.source_layer_index
814 }
815
816 pub const fn target(&self) -> SourceTargetV1 {
818 self.target
819 }
820
821 pub const fn property(&self) -> SourceChannelPropertyV1 {
823 self.property
824 }
825
826 pub fn property_name(&self) -> Option<&SourceTextV1> {
828 self.property_name.as_ref()
829 }
830
831 pub const fn components(&self) -> SourceComponentMaskV1 {
833 self.components
834 }
835
836 pub const fn interpolation(&self) -> &SourceObservationV1<SourceInterpolationV1> {
838 &self.interpolation
839 }
840
841 pub const fn input_accessor_index(&self) -> Option<usize> {
843 self.input_accessor_index
844 }
845
846 pub const fn output_accessor_index(&self) -> Option<usize> {
848 self.output_accessor_index
849 }
850
851 pub const fn disposition(&self) -> SourceLoaderDispositionV1 {
853 self.disposition
854 }
855
856 pub const fn provenance(&self) -> &SourceProvenanceV1 {
858 &self.provenance
859 }
860
861 fn retained_bytes(&self) -> usize {
862 self.property_name
863 .as_ref()
864 .map_or(0, SourceTextV1::retained_bytes)
865 .saturating_add(self.interpolation.retained_bytes())
866 .saturating_add(self.provenance.retained_bytes())
867 }
868}
869
870#[derive(Debug, Clone, PartialEq)]
872pub struct SourceClipFactV1 {
873 source_clip_index: usize,
874 source_name: SourceObservationV1<SourceTextV1>,
875 normalized_clip_index: SourceObservationV1<usize>,
876 source_range: SourceObservationV1<SourceTimeRangeV1>,
877 sampler_range: SourceObservationV1<SourceTimeRangeV1>,
878 channels: SourceFactSetV1<SourceChannelFactV1>,
879}
880
881impl SourceClipFactV1 {
882 pub fn new(
884 source_clip_index: usize,
885 source_name: SourceObservationV1<SourceTextV1>,
886 normalized_clip_index: SourceObservationV1<usize>,
887 source_range: SourceObservationV1<SourceTimeRangeV1>,
888 sampler_range: SourceObservationV1<SourceTimeRangeV1>,
889 channels: SourceFactSetV1<SourceChannelFactV1>,
890 ) -> Self {
891 Self {
892 source_clip_index,
893 source_name,
894 normalized_clip_index,
895 source_range,
896 sampler_range,
897 channels,
898 }
899 }
900
901 pub const fn source_clip_index(&self) -> usize {
903 self.source_clip_index
904 }
905
906 pub const fn source_name(&self) -> &SourceObservationV1<SourceTextV1> {
908 &self.source_name
909 }
910
911 pub const fn normalized_clip_index(&self) -> &SourceObservationV1<usize> {
913 &self.normalized_clip_index
914 }
915
916 pub const fn source_range(&self) -> &SourceObservationV1<SourceTimeRangeV1> {
918 &self.source_range
919 }
920
921 pub const fn sampler_range(&self) -> &SourceObservationV1<SourceTimeRangeV1> {
923 &self.sampler_range
924 }
925
926 pub const fn channels(&self) -> &SourceFactSetV1<SourceChannelFactV1> {
928 &self.channels
929 }
930
931 fn retained_row_count(&self) -> usize {
932 1usize.saturating_add(self.channels.rows.len())
933 }
934
935 fn retained_bytes(&self) -> usize {
936 self.retained_non_channel_bytes().saturating_add(
937 self.channels
938 .rows
939 .iter()
940 .map(SourceChannelFactV1::retained_bytes)
941 .fold(0usize, usize::saturating_add),
942 )
943 }
944
945 fn retained_non_channel_bytes(&self) -> usize {
946 self.source_name
947 .retained_text_bytes()
948 .saturating_add(self.normalized_clip_index.retained_bytes())
949 .saturating_add(self.source_range.retained_bytes())
950 .saturating_add(self.sampler_range.retained_bytes())
951 }
952
953 fn truncate_channels(&mut self, retained: usize) {
954 if self.channels.rows.len() > retained {
955 self.channels.rows.truncate(retained);
956 self.channels
957 .mark_partial(SourceUnavailableReasonV1::ProjectionBudgetExceeded);
958 }
959 }
960
961 fn truncate_channels_to_text(&mut self, available_bytes: usize) -> bool {
962 let fixed_bytes = self.retained_non_channel_bytes();
963 if fixed_bytes > available_bytes {
964 return false;
965 }
966 let mut retained_bytes = fixed_bytes;
967 let retained_channels = self
968 .channels
969 .rows
970 .iter()
971 .take_while(|channel| {
972 let next = retained_bytes.saturating_add(channel.retained_bytes());
973 if next > available_bytes {
974 false
975 } else {
976 retained_bytes = next;
977 true
978 }
979 })
980 .count();
981 self.truncate_channels(retained_channels);
982 true
983 }
984}
985
986#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
988pub enum SourceConstructKindV1 {
989 Extension,
991 CustomProperty,
993 UnknownElement,
995}
996
997#[derive(Debug, Clone, PartialEq, Eq)]
999pub struct SourceConstructFactV1 {
1000 source_order_index: usize,
1001 kind: SourceConstructKindV1,
1002 name: SourceTextV1,
1003 required: bool,
1004 count: u64,
1005 disposition: SourceLoaderDispositionV1,
1006 provenance: SourceProvenanceV1,
1007}
1008
1009impl SourceConstructFactV1 {
1010 pub fn new(
1017 source_order_index: usize,
1018 kind: SourceConstructKindV1,
1019 name: SourceTextV1,
1020 required: bool,
1021 count: u64,
1022 disposition: SourceLoaderDispositionV1,
1023 provenance: SourceProvenanceV1,
1024 ) -> Result<Self, SourceFactsError> {
1025 if count == 0 {
1026 return Err(SourceFactsError::ZeroConstructCount);
1027 }
1028 Ok(Self {
1029 source_order_index,
1030 kind,
1031 name,
1032 required,
1033 count,
1034 disposition,
1035 provenance,
1036 })
1037 }
1038
1039 pub const fn source_order_index(&self) -> usize {
1041 self.source_order_index
1042 }
1043
1044 pub const fn kind(&self) -> SourceConstructKindV1 {
1046 self.kind
1047 }
1048
1049 pub const fn name(&self) -> &SourceTextV1 {
1051 &self.name
1052 }
1053
1054 pub const fn required(&self) -> bool {
1056 self.required
1057 }
1058
1059 pub const fn count(&self) -> u64 {
1061 self.count
1062 }
1063
1064 pub const fn disposition(&self) -> SourceLoaderDispositionV1 {
1066 self.disposition
1067 }
1068
1069 pub const fn provenance(&self) -> &SourceProvenanceV1 {
1071 &self.provenance
1072 }
1073
1074 fn retained_bytes(&self) -> usize {
1075 self.name
1076 .retained_bytes()
1077 .saturating_add(self.provenance.retained_bytes())
1078 }
1079}
1080
1081#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1083#[serde(rename_all = "snake_case")]
1084pub enum SourceResourceKindV1 {
1085 Buffer,
1087 Image,
1089 Texture,
1091 Video,
1093 Cache,
1095}
1096
1097#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
1099pub enum SourceResourceLocatorV1 {
1100 Embedded,
1102 DataUri,
1104 Relative(SourceRelativeLocatorV1),
1106 Absolute,
1108 Escaping,
1110 Remote,
1112 Malformed,
1114 Oversized,
1116 Missing,
1118}
1119
1120impl SourceResourceLocatorV1 {
1121 pub fn classify(value: &str) -> Self {
1127 if let Some(classification) = redacted_resource_locator(value) {
1128 return classification;
1129 }
1130 SourceTextV1::new(value).map_or(Self::Oversized, |value| {
1131 Self::Relative(SourceRelativeLocatorV1(value))
1132 })
1133 }
1134
1135 pub fn retained_relative_bytes(value: &str) -> usize {
1140 if redacted_resource_locator(value).is_none() {
1141 value.len()
1142 } else {
1143 0
1144 }
1145 }
1146
1147 fn retained_bytes(&self) -> usize {
1148 match self {
1149 Self::Relative(value) => value.0.retained_bytes(),
1150 _ => 0,
1151 }
1152 }
1153}
1154
1155fn redacted_resource_locator(value: &str) -> Option<SourceResourceLocatorV1> {
1156 if value
1157 .get(..5)
1158 .is_some_and(|prefix| prefix.eq_ignore_ascii_case("data:"))
1159 {
1160 return Some(SourceResourceLocatorV1::DataUri);
1161 }
1162 if value.len() > RAW_SOURCE_V1_MAX_TEXT_BYTES {
1163 return Some(SourceResourceLocatorV1::Oversized);
1164 }
1165 if value.is_empty() || value.chars().any(char::is_control) || malformed_percent_escape(value) {
1166 return Some(SourceResourceLocatorV1::Malformed);
1167 }
1168 if value.starts_with(['/', '\\'])
1169 || value.as_bytes().get(1).is_some_and(|value| *value == b':')
1170 || value
1171 .get(..5)
1172 .is_some_and(|prefix| prefix.eq_ignore_ascii_case("file:"))
1173 {
1174 return Some(SourceResourceLocatorV1::Absolute);
1175 }
1176 if has_uri_scheme(value) {
1177 return Some(SourceResourceLocatorV1::Remote);
1178 }
1179 let mut escaped = false;
1180 let mut malformed = false;
1181 for component in value.split(['/', '\\']) {
1182 escaped |= component == ".." || is_encoded_dot_segment(component);
1183 malformed |= component.is_empty() || component == ".";
1184 }
1185 if escaped || contains_encoded_path_escape(value) {
1186 return Some(SourceResourceLocatorV1::Escaping);
1187 }
1188 if malformed {
1189 return Some(SourceResourceLocatorV1::Malformed);
1190 }
1191 None
1192}
1193
1194#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
1196pub struct SourceRelativeLocatorV1(SourceTextV1);
1197
1198impl SourceRelativeLocatorV1 {
1199 pub fn as_str(&self) -> &str {
1201 self.0.as_str()
1202 }
1203}
1204
1205fn malformed_percent_escape(value: &str) -> bool {
1206 let bytes = value.as_bytes();
1207 let mut index = 0;
1208 while index < bytes.len() {
1209 if bytes[index] != b'%' {
1210 index += 1;
1211 continue;
1212 }
1213 if bytes.get(index + 1).and_then(|value| hex(*value)).is_none()
1214 || bytes.get(index + 2).and_then(|value| hex(*value)).is_none()
1215 {
1216 return true;
1217 }
1218 index += 3;
1219 }
1220 false
1221}
1222
1223fn contains_encoded_path_escape(value: &str) -> bool {
1224 let lower = value.to_ascii_lowercase();
1225 lower.contains("%2f") || lower.contains("%5c") || lower.contains("%00")
1226}
1227
1228fn is_encoded_dot_segment(value: &str) -> bool {
1229 let bytes = value.as_bytes();
1230 let mut index = 0;
1231 let mut dots = 0;
1232 let mut encoded = false;
1233 while index < bytes.len() {
1234 if bytes[index] == b'.' {
1235 dots += 1;
1236 index += 1;
1237 } else if bytes.get(index..index + 3).is_some_and(|escape| {
1238 escape[0] == b'%' && escape[1] == b'2' && matches!(escape[2], b'e' | b'E')
1239 }) {
1240 dots += 1;
1241 encoded = true;
1242 index += 3;
1243 } else {
1244 return false;
1245 }
1246 if dots > 2 {
1247 return false;
1248 }
1249 }
1250 encoded && matches!(dots, 1 | 2)
1251}
1252
1253fn hex(value: u8) -> Option<u8> {
1254 match value {
1255 b'0'..=b'9' => Some(value - b'0'),
1256 b'a'..=b'f' => Some(value - b'a' + 10),
1257 b'A'..=b'F' => Some(value - b'A' + 10),
1258 _ => None,
1259 }
1260}
1261
1262fn has_uri_scheme(value: &str) -> bool {
1263 let Some((scheme, _)) = value.split_once(':') else {
1264 return false;
1265 };
1266 !scheme.is_empty()
1267 && scheme.as_bytes()[0].is_ascii_alphabetic()
1268 && scheme
1269 .bytes()
1270 .all(|value| value.is_ascii_alphanumeric() || matches!(value, b'+' | b'-' | b'.'))
1271}
1272
1273#[derive(Debug, Clone, PartialEq, Eq)]
1275pub struct SourceResourceReferenceV1 {
1276 source_order_index: usize,
1277 kind: SourceResourceKindV1,
1278 source_index: u64,
1279 locator: SourceResourceLocatorV1,
1280 disposition: SourceLoaderDispositionV1,
1281 provenance: SourceProvenanceV1,
1282}
1283
1284impl SourceResourceReferenceV1 {
1285 pub fn new(
1287 source_order_index: usize,
1288 kind: SourceResourceKindV1,
1289 source_index: u64,
1290 locator: SourceResourceLocatorV1,
1291 disposition: SourceLoaderDispositionV1,
1292 provenance: SourceProvenanceV1,
1293 ) -> Self {
1294 Self {
1295 source_order_index,
1296 kind,
1297 source_index,
1298 locator,
1299 disposition,
1300 provenance,
1301 }
1302 }
1303
1304 pub const fn source_order_index(&self) -> usize {
1306 self.source_order_index
1307 }
1308
1309 pub const fn kind(&self) -> SourceResourceKindV1 {
1311 self.kind
1312 }
1313
1314 pub const fn source_index(&self) -> u64 {
1316 self.source_index
1317 }
1318
1319 pub const fn locator(&self) -> &SourceResourceLocatorV1 {
1321 &self.locator
1322 }
1323
1324 pub const fn disposition(&self) -> SourceLoaderDispositionV1 {
1326 self.disposition
1327 }
1328
1329 pub const fn provenance(&self) -> &SourceProvenanceV1 {
1331 &self.provenance
1332 }
1333
1334 fn retained_bytes(&self) -> usize {
1335 self.locator
1336 .retained_bytes()
1337 .saturating_add(self.provenance.retained_bytes())
1338 }
1339}
1340
1341#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1343pub struct SourceProjectionWorkV1 {
1344 inspected_rows: usize,
1345 retained_rows: usize,
1346 retained_text_bytes: usize,
1347 max_traversal_depth: usize,
1348}
1349
1350impl SourceProjectionWorkV1 {
1351 pub const fn inspected_rows(self) -> usize {
1353 self.inspected_rows
1354 }
1355
1356 pub const fn retained_rows(self) -> usize {
1358 self.retained_rows
1359 }
1360
1361 pub const fn retained_text_bytes(self) -> usize {
1363 self.retained_text_bytes
1364 }
1365
1366 pub const fn max_traversal_depth(self) -> usize {
1368 self.max_traversal_depth
1369 }
1370}
1371
1372#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1374pub enum SourceFactDomainV1 {
1375 Clips,
1377 Constructs,
1379 Resources,
1381}
1382
1383impl SourceFactDomainV1 {
1384 const fn index(self) -> usize {
1385 match self {
1386 Self::Clips => 0,
1387 Self::Constructs => 1,
1388 Self::Resources => 2,
1389 }
1390 }
1391}
1392
1393#[derive(Debug, Clone, PartialEq)]
1395pub struct RawSourceFactsV1 {
1396 format: SourceFormatV1,
1397 primary_identity: InputIdentity,
1398 linear_unit: SourceObservationV1<SourceLinearUnitV1>,
1399 coordinate_basis: SourceObservationV1<SourceCoordinateBasisV1>,
1400 frames_per_second: SourceObservationV1<SourceFramesPerSecondV1>,
1401 clips: SourceFactSetV1<SourceClipFactV1>,
1402 constructs: SourceFactSetV1<SourceConstructFactV1>,
1403 resources: SourceFactSetV1<SourceResourceReferenceV1>,
1404 work: SourceProjectionWorkV1,
1405}
1406
1407pub struct RawSourceFactsBuilderV1 {
1409 facts: RawSourceFactsV1,
1410 stopped: [bool; 3],
1411}
1412
1413fn unavailable_observation<T>() -> SourceObservationV1<T> {
1414 SourceObservationV1::unavailable(
1415 SourceUnavailableReasonV1::ParserUnavailable,
1416 None,
1417 SourceLoaderDispositionV1::Unknown,
1418 )
1419}
1420
1421fn replace_scalar_observation<T>(
1422 work: &mut SourceProjectionWorkV1,
1423 slot: &mut SourceObservationV1<T>,
1424 value: SourceObservationV1<T>,
1425) -> bool {
1426 let previous_bytes = slot.retained_bytes();
1427 let value_bytes = value.retained_bytes();
1428 let baseline = work.retained_text_bytes.saturating_sub(previous_bytes);
1429 if baseline
1430 .checked_add(value_bytes)
1431 .is_some_and(|total| total <= RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES)
1432 {
1433 work.retained_text_bytes = baseline.saturating_add(value_bytes);
1434 *slot = value;
1435 true
1436 } else {
1437 work.retained_text_bytes = baseline;
1438 *slot = SourceObservationV1::unavailable(
1439 SourceUnavailableReasonV1::ProjectionBudgetExceeded,
1440 None,
1441 SourceLoaderDispositionV1::Unknown,
1442 );
1443 false
1444 }
1445}
1446
1447impl RawSourceFactsBuilderV1 {
1448 pub fn new(format: SourceFormatV1, primary_identity: InputIdentity) -> Self {
1450 Self {
1451 facts: RawSourceFactsV1 {
1452 format,
1453 primary_identity,
1454 linear_unit: unavailable_observation(),
1455 coordinate_basis: unavailable_observation(),
1456 frames_per_second: unavailable_observation(),
1457 clips: SourceFactSetV1::unavailable(SourceUnavailableReasonV1::ParserUnavailable),
1458 constructs: SourceFactSetV1::unavailable(
1459 SourceUnavailableReasonV1::ParserUnavailable,
1460 ),
1461 resources: SourceFactSetV1::unavailable(
1462 SourceUnavailableReasonV1::ParserUnavailable,
1463 ),
1464 work: SourceProjectionWorkV1::default(),
1465 },
1466 stopped: [false; 3],
1467 }
1468 }
1469
1470 pub fn set_linear_unit(&mut self, value: SourceObservationV1<SourceLinearUnitV1>) -> bool {
1475 replace_scalar_observation(&mut self.facts.work, &mut self.facts.linear_unit, value)
1476 }
1477
1478 pub fn set_coordinate_basis(
1480 &mut self,
1481 value: SourceObservationV1<SourceCoordinateBasisV1>,
1482 ) -> bool {
1483 replace_scalar_observation(
1484 &mut self.facts.work,
1485 &mut self.facts.coordinate_basis,
1486 value,
1487 )
1488 }
1489
1490 pub fn set_frames_per_second(
1492 &mut self,
1493 value: SourceObservationV1<SourceFramesPerSecondV1>,
1494 ) -> bool {
1495 replace_scalar_observation(
1496 &mut self.facts.work,
1497 &mut self.facts.frames_per_second,
1498 value,
1499 )
1500 }
1501
1502 pub const fn remaining_observation_rows(&self) -> usize {
1506 RAW_SOURCE_V1_MAX_OBSERVATIONS.saturating_sub(self.facts.work.retained_rows)
1507 }
1508
1509 pub fn remaining_clip_rows(&self) -> usize {
1511 RAW_SOURCE_V1_MAX_CLIPS.saturating_sub(self.facts.clips.rows.len())
1512 }
1513
1514 pub fn remaining_resource_rows(&self) -> usize {
1516 RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES.saturating_sub(self.facts.resources.rows.len())
1517 }
1518
1519 pub const fn resource_coverage(&self) -> SourceSetCoverageV1 {
1521 self.facts.resources.coverage
1522 }
1523
1524 pub fn resource_rows(&self) -> &[SourceResourceReferenceV1] {
1526 &self.facts.resources.rows
1527 }
1528
1529 pub const fn primary_identity(&self) -> &InputIdentity {
1531 &self.facts.primary_identity
1532 }
1533
1534 pub const fn remaining_text_bytes(&self) -> usize {
1538 RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES.saturating_sub(self.facts.work.retained_text_bytes)
1539 }
1540
1541 pub fn mark_unavailable(
1543 &mut self,
1544 domain: SourceFactDomainV1,
1545 reason: SourceUnavailableReasonV1,
1546 ) {
1547 let (retained_rows, retained_bytes) = match domain {
1548 SourceFactDomainV1::Clips => self
1549 .facts
1550 .clips
1551 .rows
1552 .iter()
1553 .map(|clip| (clip.retained_row_count(), clip.retained_bytes()))
1554 .fold(
1555 (0usize, 0usize),
1556 |(rows, bytes), (clip_rows, clip_bytes)| {
1557 (
1558 rows.saturating_add(clip_rows),
1559 bytes.saturating_add(clip_bytes),
1560 )
1561 },
1562 ),
1563 SourceFactDomainV1::Constructs => (
1564 self.facts.constructs.rows.len(),
1565 self.facts
1566 .constructs
1567 .rows
1568 .iter()
1569 .map(SourceConstructFactV1::retained_bytes)
1570 .fold(0usize, usize::saturating_add),
1571 ),
1572 SourceFactDomainV1::Resources => (
1573 self.facts.resources.rows.len(),
1574 self.facts
1575 .resources
1576 .rows
1577 .iter()
1578 .map(SourceResourceReferenceV1::retained_bytes)
1579 .fold(0usize, usize::saturating_add),
1580 ),
1581 };
1582 self.facts.work.retained_rows = self.facts.work.retained_rows.saturating_sub(retained_rows);
1583 self.facts.work.retained_text_bytes = self
1584 .facts
1585 .work
1586 .retained_text_bytes
1587 .saturating_sub(retained_bytes);
1588 self.stopped[domain.index()] = true;
1589 match domain {
1590 SourceFactDomainV1::Clips => self.facts.clips = SourceFactSetV1::unavailable(reason),
1591 SourceFactDomainV1::Constructs => {
1592 self.facts.constructs = SourceFactSetV1::unavailable(reason)
1593 }
1594 SourceFactDomainV1::Resources => {
1595 self.facts.resources = SourceFactSetV1::unavailable(reason)
1596 }
1597 }
1598 }
1599
1600 pub fn mark_partial(&mut self, domain: SourceFactDomainV1, reason: SourceUnavailableReasonV1) {
1602 if !self.stopped[domain.index()] {
1603 *self.set_for_domain_mut(domain) = SourceSetCoverageV1::partial(reason);
1604 }
1605 }
1606
1607 pub fn mark_complete(&mut self, domain: SourceFactDomainV1) {
1612 if !self.stopped[domain.index()]
1613 && matches!(
1614 self.set_for_domain_mut(domain).state(),
1615 SourceSetCoverageStateV1::Unavailable
1616 )
1617 {
1618 *self.set_for_domain_mut(domain) = SourceSetCoverageV1::complete();
1619 }
1620 }
1621
1622 pub fn mark_budget_exceeded(&mut self, domain: SourceFactDomainV1) {
1627 if self.stopped[domain.index()] {
1628 return;
1629 }
1630 self.facts.work.inspected_rows = self.facts.work.inspected_rows.saturating_add(1);
1631 self.stop_for_budget(domain);
1632 }
1633
1634 pub fn observe_traversal_depth(&mut self, domain: SourceFactDomainV1, depth: usize) -> bool {
1638 if self.stopped[domain.index()] {
1639 return false;
1640 }
1641 self.facts.work.max_traversal_depth = self
1642 .facts
1643 .work
1644 .max_traversal_depth
1645 .max(depth.min(RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH + 1));
1646 if depth > RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH {
1647 self.stop_for_budget(domain);
1648 return false;
1649 }
1650 true
1651 }
1652
1653 pub fn push_clip(&mut self, mut clip: SourceClipFactV1) -> bool {
1658 if self.stopped[SourceFactDomainV1::Clips.index()] {
1659 return false;
1660 }
1661 if self.facts.clips.rows.len() >= RAW_SOURCE_V1_MAX_CLIPS {
1662 self.mark_budget_exceeded(SourceFactDomainV1::Clips);
1663 return false;
1664 }
1665 let remaining_rows =
1666 RAW_SOURCE_V1_MAX_OBSERVATIONS.saturating_sub(self.facts.work.retained_rows);
1667 if remaining_rows == 0 {
1668 self.mark_budget_exceeded(SourceFactDomainV1::Clips);
1669 return false;
1670 }
1671 let original_rows = clip.retained_row_count();
1672 let supplied_budget_prefix = matches!(
1673 clip.channels.coverage(),
1674 SourceSetCoverageV1 {
1675 state: SourceSetCoverageStateV1::Partial,
1676 reason: Some(SourceUnavailableReasonV1::ProjectionBudgetExceeded),
1677 }
1678 );
1679 let mut builder_truncated = false;
1680 if clip.retained_row_count() > remaining_rows {
1681 clip.truncate_channels(remaining_rows - 1);
1682 builder_truncated = true;
1683 }
1684 if !clip.truncate_channels_to_text(self.remaining_text_bytes()) {
1685 self.mark_budget_exceeded(SourceFactDomainV1::Clips);
1686 return false;
1687 }
1688 builder_truncated |= clip.retained_row_count() < original_rows;
1689 let retained_rows = clip.retained_row_count();
1690 let inspected_rows = if builder_truncated || supplied_budget_prefix {
1691 retained_rows.saturating_add(1)
1692 } else {
1693 retained_rows
1694 };
1695 self.facts.work.inspected_rows = self
1696 .facts
1697 .work
1698 .inspected_rows
1699 .saturating_add(inspected_rows);
1700 if builder_truncated || supplied_budget_prefix {
1701 self.stop_for_budget(SourceFactDomainV1::Clips);
1702 }
1703 self.retain_work(clip.retained_row_count(), clip.retained_bytes());
1704 self.facts.clips.rows.push(clip);
1705 true
1706 }
1707
1708 pub fn push_construct(&mut self, row: SourceConstructFactV1) -> bool {
1710 if self.stopped[SourceFactDomainV1::Constructs.index()] {
1711 return false;
1712 }
1713 if !self.can_retain_row(row.retained_bytes()) {
1714 self.mark_budget_exceeded(SourceFactDomainV1::Constructs);
1715 return false;
1716 }
1717 self.facts.work.inspected_rows = self.facts.work.inspected_rows.saturating_add(1);
1718 self.retain_work(1, row.retained_bytes());
1719 self.facts.constructs.rows.push(row);
1720 true
1721 }
1722
1723 pub fn push_resource(&mut self, row: SourceResourceReferenceV1) -> bool {
1725 if self.stopped[SourceFactDomainV1::Resources.index()] {
1726 return false;
1727 }
1728 if self.facts.resources.rows.len() >= RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES
1729 || !self.can_retain_row(row.retained_bytes())
1730 {
1731 self.mark_budget_exceeded(SourceFactDomainV1::Resources);
1732 return false;
1733 }
1734 self.facts.work.inspected_rows = self.facts.work.inspected_rows.saturating_add(1);
1735 self.retain_work(1, row.retained_bytes());
1736 self.facts.resources.rows.push(row);
1737 true
1738 }
1739
1740 pub fn finish(mut self, document: Document) -> Result<LoadedSource, SourceFactsError> {
1747 self.qualify_unfinished_positive_rows();
1748 let closure = DependencyClosureV1::capture_unavailable(
1749 self.facts.primary_identity.clone(),
1750 self.facts.resources.coverage,
1751 );
1752 self.finish_with_dependency_closure(document, closure)
1753 }
1754
1755 pub fn finish_with_dependency_closure(
1766 mut self,
1767 document: Document,
1768 dependency_closure: DependencyClosureV1,
1769 ) -> Result<LoadedSource, SourceFactsError> {
1770 self.qualify_unfinished_positive_rows();
1771 validate_clip_rows(&self.facts.clips, document.clips.len())?;
1772 validate_ordered_rows(&self.facts.constructs, &self.facts.resources)?;
1773 dependency_closure.validate_against(
1774 self.facts.format,
1775 &self.facts.primary_identity,
1776 &self.facts.resources,
1777 )?;
1778 Ok(LoadedSource {
1779 document,
1780 facts: self.facts,
1781 dependency_closure,
1782 })
1783 }
1784
1785 fn can_retain_text(&self, bytes: usize) -> bool {
1786 self.facts
1787 .work
1788 .retained_text_bytes
1789 .checked_add(bytes)
1790 .is_some_and(|total| total <= RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES)
1791 }
1792
1793 fn can_retain_row(&self, bytes: usize) -> bool {
1794 self.facts.work.retained_rows < RAW_SOURCE_V1_MAX_OBSERVATIONS
1795 && self.can_retain_text(bytes)
1796 }
1797
1798 fn retain_work(&mut self, rows: usize, bytes: usize) {
1799 self.facts.work.retained_rows = self.facts.work.retained_rows.saturating_add(rows);
1800 self.facts.work.retained_text_bytes =
1801 self.facts.work.retained_text_bytes.saturating_add(bytes);
1802 }
1803
1804 fn set_for_domain_mut(&mut self, domain: SourceFactDomainV1) -> &mut SourceSetCoverageV1 {
1805 match domain {
1806 SourceFactDomainV1::Clips => &mut self.facts.clips.coverage,
1807 SourceFactDomainV1::Constructs => &mut self.facts.constructs.coverage,
1808 SourceFactDomainV1::Resources => &mut self.facts.resources.coverage,
1809 }
1810 }
1811
1812 fn stop_for_budget(&mut self, domain: SourceFactDomainV1) {
1813 *self.set_for_domain_mut(domain) =
1814 SourceSetCoverageV1::partial(SourceUnavailableReasonV1::ProjectionBudgetExceeded);
1815 self.stopped[domain.index()] = true;
1816 }
1817
1818 fn qualify_unfinished_positive_rows(&mut self) {
1819 for domain in [
1820 SourceFactDomainV1::Clips,
1821 SourceFactDomainV1::Constructs,
1822 SourceFactDomainV1::Resources,
1823 ] {
1824 let has_rows = match domain {
1825 SourceFactDomainV1::Clips => !self.facts.clips.rows.is_empty(),
1826 SourceFactDomainV1::Constructs => !self.facts.constructs.rows.is_empty(),
1827 SourceFactDomainV1::Resources => !self.facts.resources.rows.is_empty(),
1828 };
1829 if has_rows
1830 && matches!(
1831 self.set_for_domain_mut(domain).state(),
1832 SourceSetCoverageStateV1::Unavailable
1833 )
1834 {
1835 *self.set_for_domain_mut(domain) =
1836 SourceSetCoverageV1::partial(SourceUnavailableReasonV1::ParserUnavailable);
1837 }
1838 }
1839 }
1840}
1841
1842fn validate_clip_rows(
1843 clips: &SourceFactSetV1<SourceClipFactV1>,
1844 normalized_clip_count: usize,
1845) -> Result<(), SourceFactsError> {
1846 for (expected_clip_index, clip) in clips.rows.iter().enumerate() {
1847 if clip.source_clip_index != expected_clip_index {
1848 return Err(SourceFactsError::NonCanonicalClipIndex {
1849 expected: expected_clip_index,
1850 actual: clip.source_clip_index,
1851 });
1852 }
1853 if let SourceObservationStateV1::Observed(index) = clip.normalized_clip_index.state()
1854 && *index >= normalized_clip_count
1855 {
1856 return Err(SourceFactsError::NormalizedClipIndexOutOfRange {
1857 index: *index,
1858 clip_count: normalized_clip_count,
1859 });
1860 }
1861 for (expected_channel_index, channel) in clip.channels.rows.iter().enumerate() {
1862 if channel.source_channel_index != expected_channel_index {
1863 return Err(SourceFactsError::NonCanonicalChannelIndex {
1864 source_clip_index: clip.source_clip_index,
1865 expected: expected_channel_index,
1866 actual: channel.source_channel_index,
1867 });
1868 }
1869 }
1870 }
1871 Ok(())
1872}
1873
1874fn validate_ordered_rows(
1875 constructs: &SourceFactSetV1<SourceConstructFactV1>,
1876 resources: &SourceFactSetV1<SourceResourceReferenceV1>,
1877) -> Result<(), SourceFactsError> {
1878 for (expected, row) in constructs.rows.iter().enumerate() {
1879 if row.source_order_index != expected {
1880 return Err(SourceFactsError::NonCanonicalConstructOrder {
1881 expected,
1882 actual: row.source_order_index,
1883 });
1884 }
1885 }
1886 for (expected, row) in resources.rows.iter().enumerate() {
1887 if row.source_order_index != expected {
1888 return Err(SourceFactsError::NonCanonicalResourceOrder {
1889 expected,
1890 actual: row.source_order_index,
1891 });
1892 }
1893 }
1894 Ok(())
1895}
1896
1897pub struct LoadedSource {
1902 document: Document,
1903 facts: RawSourceFactsV1,
1904 dependency_closure: DependencyClosureV1,
1905}
1906
1907impl fmt::Debug for LoadedSource {
1908 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1909 formatter
1910 .debug_struct("LoadedSource")
1911 .field("format", &self.facts.format)
1912 .field("primary_identity", &self.facts.primary_identity)
1913 .field("dependency_closure", &self.dependency_closure)
1914 .field("work", &self.facts.work)
1915 .finish_non_exhaustive()
1916 }
1917}
1918
1919impl LoadedSource {
1920 pub const fn document(&self) -> &Document {
1922 &self.document
1923 }
1924
1925 pub fn source_facts(&self) -> SourceFactsViewV1<'_> {
1927 SourceFactsViewV1 {
1928 facts: &self.facts,
1929 source_skeleton: &self.document.assets.source_skeleton,
1930 }
1931 }
1932
1933 pub const fn dependency_closure(&self) -> &DependencyClosureV1 {
1935 &self.dependency_closure
1936 }
1937
1938 pub fn into_document(self) -> Document {
1940 self.document
1941 }
1942}
1943
1944#[derive(Debug, Clone, Copy)]
1946pub struct SourceFactsViewV1<'a> {
1947 facts: &'a RawSourceFactsV1,
1948 source_skeleton: &'a SourceSkeletonAssets,
1949}
1950
1951impl<'a> SourceFactsViewV1<'a> {
1952 pub const fn contract_id(self) -> &'static str {
1954 RAW_SOURCE_FACTS_V1_ID
1955 }
1956
1957 pub const fn format(self) -> SourceFormatV1 {
1959 self.facts.format
1960 }
1961
1962 pub const fn primary_identity(self) -> &'a InputIdentity {
1964 &self.facts.primary_identity
1965 }
1966
1967 pub const fn linear_unit(self) -> &'a SourceObservationV1<SourceLinearUnitV1> {
1969 &self.facts.linear_unit
1970 }
1971
1972 pub const fn coordinate_basis(self) -> &'a SourceObservationV1<SourceCoordinateBasisV1> {
1974 &self.facts.coordinate_basis
1975 }
1976
1977 pub const fn frames_per_second(self) -> &'a SourceObservationV1<SourceFramesPerSecondV1> {
1979 &self.facts.frames_per_second
1980 }
1981
1982 pub const fn clips(self) -> &'a SourceFactSetV1<SourceClipFactV1> {
1984 &self.facts.clips
1985 }
1986
1987 pub const fn constructs(self) -> &'a SourceFactSetV1<SourceConstructFactV1> {
1989 &self.facts.constructs
1990 }
1991
1992 pub const fn resources(self) -> &'a SourceFactSetV1<SourceResourceReferenceV1> {
1994 &self.facts.resources
1995 }
1996
1997 pub const fn source_skeleton(self) -> &'a SourceSkeletonAssets {
1999 self.source_skeleton
2000 }
2001
2002 pub const fn work(self) -> SourceProjectionWorkV1 {
2004 self.facts.work
2005 }
2006}
2007
2008#[derive(Debug, thiserror::Error, PartialEq, Eq)]
2010#[non_exhaustive]
2011pub enum SourceFactsError {
2012 #[error("source text is {bytes} bytes, exceeding the V1 limit of {limit}")]
2014 TextTooLong {
2015 bytes: usize,
2017 limit: usize,
2019 },
2020 #[error("source logical locator is invalid or unsafe")]
2022 InvalidLogicalLocator,
2023 #[error("source coordinate basis must use each unsigned axis exactly once")]
2025 DuplicateBasisAxis,
2026 #[error("metres per source unit must be finite and positive")]
2028 InvalidLinearUnit,
2029 #[error("source frames per second must be finite and positive")]
2031 InvalidFramesPerSecond,
2032 #[error("source time range endpoints must be finite with begin <= end")]
2034 InvalidTimeRange,
2035 #[error("source construct occurrence count must be positive")]
2037 ZeroConstructCount,
2038 #[error("source clip index {actual} is not the expected prefix index {expected}")]
2040 NonCanonicalClipIndex {
2041 expected: usize,
2043 actual: usize,
2045 },
2046 #[error(
2048 "source channel index {actual} is not expected prefix index {expected} in clip {source_clip_index}"
2049 )]
2050 NonCanonicalChannelIndex {
2051 source_clip_index: usize,
2053 expected: usize,
2055 actual: usize,
2057 },
2058 #[error("source construct order {actual} is not expected prefix index {expected}")]
2060 NonCanonicalConstructOrder {
2061 expected: usize,
2063 actual: usize,
2065 },
2066 #[error("source resource order {actual} is not expected prefix index {expected}")]
2068 NonCanonicalResourceOrder {
2069 expected: usize,
2071 actual: usize,
2073 },
2074 #[error("normalized clip index {index} is outside document clip count {clip_count}")]
2076 NormalizedClipIndexOutOfRange {
2077 index: usize,
2079 clip_count: usize,
2081 },
2082 #[error(transparent)]
2084 DependencyClosure(#[from] DependencyClosureError),
2085}
2086
2087#[cfg(test)]
2088mod tests {
2089 use super::*;
2090
2091 fn format_provenance() -> SourceProvenanceV1 {
2092 SourceProvenanceV1::format_defined()
2093 }
2094
2095 fn unavailable<T>() -> SourceObservationV1<T> {
2096 SourceObservationV1::unavailable(
2097 SourceUnavailableReasonV1::ParserUnavailable,
2098 None,
2099 SourceLoaderDispositionV1::Unknown,
2100 )
2101 }
2102
2103 fn clip(index: usize) -> SourceClipFactV1 {
2104 SourceClipFactV1::new(
2105 index,
2106 SourceObservationV1::proven_absent(format_provenance()),
2107 unavailable(),
2108 SourceObservationV1::proven_absent(format_provenance()),
2109 SourceObservationV1::proven_absent(format_provenance()),
2110 SourceFactSetV1::complete(Vec::new()),
2111 )
2112 }
2113
2114 fn construct(index: usize, name: String) -> SourceConstructFactV1 {
2115 SourceConstructFactV1::new(
2116 index,
2117 SourceConstructKindV1::Extension,
2118 SourceTextV1::new(name).expect("bounded name"),
2119 false,
2120 1,
2121 SourceLoaderDispositionV1::Unsupported,
2122 format_provenance(),
2123 )
2124 .expect("positive construct count")
2125 }
2126
2127 fn resource(index: usize) -> SourceResourceReferenceV1 {
2128 SourceResourceReferenceV1::new(
2129 index,
2130 SourceResourceKindV1::Image,
2131 index as u64,
2132 SourceResourceLocatorV1::Embedded,
2133 SourceLoaderDispositionV1::Preserved,
2134 format_provenance(),
2135 )
2136 }
2137
2138 fn named_channel(index: usize, name: &str) -> SourceChannelFactV1 {
2139 SourceChannelFactV1::new(
2140 index,
2141 SourceTargetV1::new(SourceTargetKindV1::Node, 0),
2142 SourceChannelPropertyV1::Other,
2143 SourceComponentMaskV1::new(true, false, false),
2144 SourceObservationV1::proven_absent(format_provenance()),
2145 SourceLoaderDispositionV1::Unsupported,
2146 format_provenance(),
2147 )
2148 .with_property_name(SourceTextV1::new(name).expect("bounded property name"))
2149 }
2150
2151 #[test]
2152 fn scalar_value_types_reject_non_finite_and_contradictory_values() {
2153 for value in [0.0, -1.0, f64::NAN, f64::INFINITY] {
2154 assert_eq!(
2155 SourceLinearUnitV1::new(value),
2156 Err(SourceFactsError::InvalidLinearUnit)
2157 );
2158 assert_eq!(
2159 SourceFramesPerSecondV1::new(value),
2160 Err(SourceFactsError::InvalidFramesPerSecond)
2161 );
2162 }
2163 assert_eq!(
2164 SourceTimeRangeV1::new(2.0, 1.0),
2165 Err(SourceFactsError::InvalidTimeRange)
2166 );
2167 assert_eq!(
2168 SourceTimeRangeV1::new(f64::NAN, 1.0),
2169 Err(SourceFactsError::InvalidTimeRange)
2170 );
2171 assert_eq!(
2172 SourceCoordinateBasisV1::new(
2173 SourceAxisV1::PositiveX,
2174 SourceAxisV1::NegativeX,
2175 SourceAxisV1::PositiveZ,
2176 ),
2177 Err(SourceFactsError::DuplicateBasisAxis)
2178 );
2179
2180 let range = SourceTimeRangeV1::new(1.0, 1.0).expect("zero duration is evidence");
2181 assert_eq!(range.begin_s(), range.end_s());
2182 let basis = SourceCoordinateBasisV1::new(
2183 SourceAxisV1::PositiveX,
2184 SourceAxisV1::PositiveY,
2185 SourceAxisV1::PositiveZ,
2186 )
2187 .expect("orthogonal basis");
2188 assert_eq!(basis.handedness(), SourceHandednessV1::Right);
2189 assert_eq!(
2190 SourceConstructFactV1::new(
2191 0,
2192 SourceConstructKindV1::Extension,
2193 SourceTextV1::new("EXT_zero").expect("bounded name"),
2194 false,
2195 0,
2196 SourceLoaderDispositionV1::Unsupported,
2197 format_provenance(),
2198 ),
2199 Err(SourceFactsError::ZeroConstructCount)
2200 );
2201 }
2202
2203 #[test]
2204 fn partial_empty_sets_never_prove_absence() {
2205 let complete = SourceFactSetV1::<SourceConstructFactV1>::complete(Vec::new());
2206 let partial = SourceFactSetV1::<SourceConstructFactV1>::partial(
2207 Vec::new(),
2208 SourceUnavailableReasonV1::ProjectionBudgetExceeded,
2209 );
2210 let unavailable = SourceFactSetV1::<SourceConstructFactV1>::unavailable(
2211 SourceUnavailableReasonV1::LoaderUnsupported,
2212 );
2213 assert!(complete.proves_absence());
2214 assert!(!partial.proves_absence());
2215 assert!(!unavailable.proves_absence());
2216 }
2217
2218 #[test]
2219 fn builder_requires_explicit_complete_coverage_to_prove_absence() {
2220 let builder = RawSourceFactsBuilderV1::new(
2221 SourceFormatV1::GltfJson,
2222 InputIdentity::from_bytes(b"untouched"),
2223 );
2224 let loaded = builder
2225 .finish(Document::default())
2226 .expect("unavailable defaults");
2227 let facts = loaded.source_facts();
2228 assert!(!facts.clips().proves_absence());
2229 assert!(!facts.constructs().proves_absence());
2230 assert!(!facts.resources().proves_absence());
2231 assert_eq!(
2232 loaded.dependency_closure().coverage().reasons(),
2233 &[
2234 crate::DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable,
2235 crate::DependencyClosureCoverageReasonV1::CaptureUnavailable,
2236 ]
2237 );
2238
2239 let mut builder = RawSourceFactsBuilderV1::new(
2240 SourceFormatV1::GltfJson,
2241 InputIdentity::from_bytes(b"exhaustive"),
2242 );
2243 builder.mark_complete(SourceFactDomainV1::Clips);
2244 builder.mark_complete(SourceFactDomainV1::Constructs);
2245 builder.mark_complete(SourceFactDomainV1::Resources);
2246 let loaded = builder
2247 .finish(Document::default())
2248 .expect("complete empty domains");
2249 let facts = loaded.source_facts();
2250 assert!(facts.clips().proves_absence());
2251 assert!(facts.constructs().proves_absence());
2252 assert!(facts.resources().proves_absence());
2253 assert_eq!(
2254 loaded.dependency_closure().coverage().reasons(),
2255 &[crate::DependencyClosureCoverageReasonV1::CaptureUnavailable]
2256 );
2257 }
2258
2259 #[test]
2260 fn loaded_source_accepts_a_complete_format_bound_dependency_closure() {
2261 let primary = InputIdentity::from_bytes(b"gltf");
2262 let mut facts = RawSourceFactsBuilderV1::new(SourceFormatV1::GltfJson, primary.clone());
2263 assert!(facts.push_resource(SourceResourceReferenceV1::new(
2264 0,
2265 SourceResourceKindV1::Buffer,
2266 0,
2267 SourceResourceLocatorV1::classify("buffers/a%20b.bin"),
2268 SourceLoaderDispositionV1::Preserved,
2269 format_provenance(),
2270 )));
2271 facts.mark_complete(SourceFactDomainV1::Resources);
2272
2273 let key = crate::DependencyResourceKeyV1::from_source_str(
2274 "buffers/a b.bin",
2275 crate::ResourceKeySyntaxV1::GltfUri,
2276 )
2277 .unwrap();
2278 let mut closure = crate::DependencyClosureBuilderV1::new(
2279 primary.clone(),
2280 facts.resource_coverage(),
2281 facts.resource_rows().len(),
2282 );
2283 assert!(closure.begin_reference(17, 2));
2284 assert_eq!(closure.prepare_external_key(&key).unwrap(), Some(true));
2285 closure.record_external_open_attempt(&key).unwrap();
2286 assert!(
2287 closure
2288 .push_captured_external(
2289 0,
2290 SourceResourceKindV1::Buffer,
2291 0,
2292 key,
2293 InputIdentity::from_bytes(b"buffer"),
2294 )
2295 .unwrap()
2296 );
2297 let loaded = facts
2298 .finish_with_dependency_closure(Document::default(), closure.finish().unwrap())
2299 .unwrap();
2300 assert!(loaded.dependency_closure().coverage().is_complete());
2301 assert!(loaded.dependency_closure().identity().is_some());
2302 let document = loaded.into_document();
2303 assert!(document.clips.is_empty());
2304 }
2305
2306 #[test]
2307 fn nested_partial_channels_do_not_weaken_complete_clip_identity_coverage() {
2308 let mut builder = RawSourceFactsBuilderV1::new(
2309 SourceFormatV1::Fbx,
2310 InputIdentity::from_bytes(b"nested-partial"),
2311 );
2312 assert!(builder.push_clip(SourceClipFactV1::new(
2313 0,
2314 SourceObservationV1::proven_absent(format_provenance()),
2315 unavailable(),
2316 SourceObservationV1::proven_absent(format_provenance()),
2317 SourceObservationV1::proven_absent(format_provenance()),
2318 SourceFactSetV1::partial(
2319 vec![named_channel(0, "property")],
2320 SourceUnavailableReasonV1::ParserUnavailable,
2321 ),
2322 )));
2323 builder.mark_complete(SourceFactDomainV1::Clips);
2324 let loaded = builder
2325 .finish(Document::default())
2326 .expect("independent coverage");
2327 let facts = loaded.source_facts();
2328 assert_eq!(facts.clips().coverage(), SourceSetCoverageV1::complete());
2329 assert_eq!(
2330 facts.clips().rows()[0].channels().coverage(),
2331 SourceSetCoverageV1::partial(SourceUnavailableReasonV1::ParserUnavailable)
2332 );
2333 }
2334
2335 #[test]
2336 fn resource_locator_classification_redacts_unsafe_spelling() {
2337 let secret = "/home/example/private.bin";
2338 for classified in [
2339 SourceResourceLocatorV1::classify(secret),
2340 SourceResourceLocatorV1::classify("https://example.invalid/a.bin"),
2341 SourceResourceLocatorV1::classify("../escape.bin"),
2342 SourceResourceLocatorV1::classify("a/%2e%2e/escape.bin"),
2343 SourceResourceLocatorV1::classify("a/%2f/escape.bin"),
2344 SourceResourceLocatorV1::classify("bad%q0.bin"),
2345 ] {
2346 assert!(!format!("{classified:?}").contains(secret));
2347 assert!(!matches!(classified, SourceResourceLocatorV1::Relative(_)));
2348 }
2349 let control_bearing = "textures/TOP_SECRET\nname.png";
2350 let classified = SourceResourceLocatorV1::classify(control_bearing);
2351 assert_eq!(classified, SourceResourceLocatorV1::Malformed);
2352 assert!(!format!("{classified:?}").contains("TOP_SECRET"));
2353 assert_eq!(
2354 SourceResourceLocatorV1::retained_relative_bytes(control_bearing),
2355 0
2356 );
2357 let SourceResourceLocatorV1::Relative(relative) =
2358 SourceResourceLocatorV1::classify("textures/normal.png")
2359 else {
2360 panic!("safe relative declaration retained");
2361 };
2362 assert_eq!(relative.as_str(), "textures/normal.png");
2363 assert_eq!(
2364 SourceResourceLocatorV1::retained_relative_bytes("textures/normal.png"),
2365 "textures/normal.png".len()
2366 );
2367 assert_eq!(
2368 SourceResourceLocatorV1::retained_relative_bytes("../private.bin"),
2369 0
2370 );
2371 assert_eq!(
2372 SourceResourceLocatorV1::classify("data:image/png;base64,private"),
2373 SourceResourceLocatorV1::DataUri
2374 );
2375 assert_eq!(
2376 SourceResourceLocatorV1::classify("DATA:image/png;base64,private"),
2377 SourceResourceLocatorV1::DataUri
2378 );
2379 let oversized_data_uri = format!(
2380 "data:application/octet-stream;base64,{}",
2381 "A".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES)
2382 );
2383 assert_eq!(
2384 SourceResourceLocatorV1::classify(&oversized_data_uri),
2385 SourceResourceLocatorV1::DataUri
2386 );
2387 assert_eq!(
2388 SourceResourceLocatorV1::retained_relative_bytes(&oversized_data_uri),
2389 0
2390 );
2391 assert_eq!(
2392 SourceResourceLocatorV1::classify(r"C:\private\texture.png"),
2393 SourceResourceLocatorV1::Absolute
2394 );
2395 assert_eq!(
2396 SourceResourceLocatorV1::classify("file:///private/texture.png"),
2397 SourceResourceLocatorV1::Absolute
2398 );
2399 }
2400
2401 #[test]
2402 fn provenance_uses_only_validated_source_logical_locators() {
2403 for value in [
2404 "/home/private/input.glb",
2405 "/../animations",
2406 "https://host/path",
2407 ] {
2408 assert_eq!(
2409 SourceLogicalLocatorV1::gltf_json_pointer(value),
2410 Err(SourceFactsError::InvalidLogicalLocator)
2411 );
2412 }
2413 for value in [
2414 "/home/private/input.fbx",
2415 "fbx:/home/private",
2416 "fbx:../private",
2417 ] {
2418 assert_eq!(
2419 SourceLogicalLocatorV1::fbx_parser_path(value),
2420 Err(SourceFactsError::InvalidLogicalLocator)
2421 );
2422 }
2423
2424 let pointer = SourceLogicalLocatorV1::gltf_json_pointer("/animations/0/channels/1")
2425 .expect("generated glTF pointer");
2426 let provenance = SourceProvenanceV1::source_declared(pointer);
2427 assert_eq!(provenance.kind(), SourceProvenanceKindV1::SourceDeclared);
2428 assert_eq!(
2429 provenance.locator().map(SourceLogicalLocatorV1::as_str),
2430 Some("/animations/0/channels/1")
2431 );
2432 assert!(!format!("{provenance:?}").contains("animations"));
2433
2434 let path = SourceLogicalLocatorV1::fbx_parser_path("fbx:scene.settings.axes")
2435 .expect("generated FBX parser path");
2436 assert_eq!(
2437 SourceProvenanceV1::parser_projected(path).kind(),
2438 SourceProvenanceKindV1::ParserProjected
2439 );
2440 assert!(SourceProvenanceV1::format_defined().locator().is_none());
2441 }
2442
2443 #[test]
2444 fn loaded_source_binds_exact_identity_and_canonical_source_skeleton() {
2445 let bytes = b"same bytes parsed by the loader";
2446 let identity = InputIdentity::from_bytes(bytes);
2447 let mut document = Document::default();
2448 document.source.path = Some("/home/example/private/input.glb".into());
2449 let mut builder = RawSourceFactsBuilderV1::new(SourceFormatV1::Glb, identity.clone());
2450 builder.set_linear_unit(SourceObservationV1::observed(
2451 SourceLinearUnitV1::new(1.0).expect("metres"),
2452 format_provenance(),
2453 SourceLoaderDispositionV1::Preserved,
2454 ));
2455 let loaded = builder.finish(document).expect("facts bind");
2456 let source_skeleton_ptr = &loaded.document().assets.source_skeleton as *const _;
2457 let facts = loaded.source_facts();
2458 assert_eq!(facts.contract_id(), RAW_SOURCE_FACTS_V1_ID);
2459 assert_eq!(facts.format(), SourceFormatV1::Glb);
2460 assert_eq!(facts.primary_identity(), &identity);
2461 assert_eq!(facts.primary_identity().bytes(), bytes.len() as u64);
2462 assert!(std::ptr::eq(
2463 facts.source_skeleton() as *const _,
2464 source_skeleton_ptr
2465 ));
2466 assert_eq!(loaded.dependency_closure().primary_input(), &identity);
2467 assert!(matches!(
2468 loaded.dependency_closure().coverage(),
2469 crate::DependencyClosureCoverageV1::Unavailable { .. }
2470 ));
2471 assert!(loaded.dependency_closure().identity().is_none());
2472 assert!(!format!("{loaded:?}").contains("/home/example/private"));
2473 let document = loaded.into_document();
2474 assert!(document.clips.is_empty());
2475 }
2476
2477 #[test]
2478 fn clip_limit_retains_n_then_marks_n_plus_one_partial() {
2479 let mut builder =
2480 RawSourceFactsBuilderV1::new(SourceFormatV1::Fbx, InputIdentity::from_bytes(b"fbx"));
2481 for index in 0..RAW_SOURCE_V1_MAX_CLIPS {
2482 assert!(builder.push_clip(clip(index)));
2483 }
2484 assert!(!builder.push_clip(clip(RAW_SOURCE_V1_MAX_CLIPS)));
2485 assert!(!builder.push_clip(clip(RAW_SOURCE_V1_MAX_CLIPS + 1)));
2486 let loaded = builder.finish(Document::default()).expect("bounded facts");
2487 let facts = loaded.source_facts();
2488 assert_eq!(facts.clips().rows().len(), RAW_SOURCE_V1_MAX_CLIPS);
2489 assert_eq!(
2490 facts.clips().coverage(),
2491 SourceSetCoverageV1::partial(SourceUnavailableReasonV1::ProjectionBudgetExceeded)
2492 );
2493 assert_eq!(facts.work().inspected_rows(), RAW_SOURCE_V1_MAX_CLIPS + 1);
2494 }
2495
2496 #[test]
2497 fn resource_limit_retains_n_then_marks_n_plus_one_partial() {
2498 let mut builder = RawSourceFactsBuilderV1::new(
2499 SourceFormatV1::GltfJson,
2500 InputIdentity::from_bytes(b"gltf"),
2501 );
2502 for index in 0..RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES {
2503 assert!(builder.push_resource(resource(index)));
2504 }
2505 assert!(!builder.push_resource(resource(RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES)));
2506 assert!(!builder.push_resource(resource(RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES + 1)));
2507 let loaded = builder.finish(Document::default()).expect("bounded facts");
2508 let facts = loaded.source_facts();
2509 assert_eq!(
2510 facts.resources().rows().len(),
2511 RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES
2512 );
2513 assert_eq!(
2514 facts.resources().coverage().state(),
2515 SourceSetCoverageStateV1::Partial
2516 );
2517 assert_eq!(
2518 facts.work().inspected_rows(),
2519 RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES + 1
2520 );
2521 }
2522
2523 #[test]
2524 fn observed_clip_names_count_toward_the_aggregate_text_limit() {
2525 let rows_at_limit = RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES / RAW_SOURCE_V1_MAX_TEXT_BYTES;
2526 let mut named_clips = RawSourceFactsBuilderV1::new(
2527 SourceFormatV1::GltfJson,
2528 InputIdentity::from_bytes(b"named-clips"),
2529 );
2530 for index in 0..rows_at_limit {
2531 let source_name = SourceObservationV1::observed(
2532 SourceTextV1::new("n".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES)).expect("bounded name"),
2533 format_provenance(),
2534 SourceLoaderDispositionV1::Preserved,
2535 );
2536 assert!(named_clips.push_clip(SourceClipFactV1::new(
2537 index,
2538 source_name,
2539 SourceObservationV1::proven_absent(format_provenance()),
2540 SourceObservationV1::proven_absent(format_provenance()),
2541 SourceObservationV1::proven_absent(format_provenance()),
2542 SourceFactSetV1::complete(Vec::new()),
2543 )));
2544 }
2545 let overflow_name = SourceObservationV1::observed(
2546 SourceTextV1::new("n".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES)).expect("bounded name"),
2547 format_provenance(),
2548 SourceLoaderDispositionV1::Preserved,
2549 );
2550 assert!(!named_clips.push_clip(SourceClipFactV1::new(
2551 rows_at_limit,
2552 overflow_name,
2553 SourceObservationV1::proven_absent(format_provenance()),
2554 SourceObservationV1::proven_absent(format_provenance()),
2555 SourceObservationV1::proven_absent(format_provenance()),
2556 SourceFactSetV1::complete(Vec::new()),
2557 )));
2558 let loaded = named_clips
2559 .finish(Document::default())
2560 .expect("bounded named clips");
2561 assert_eq!(
2562 loaded.source_facts().work().retained_text_bytes(),
2563 RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES
2564 );
2565 assert_eq!(
2566 loaded.source_facts().clips().coverage().state(),
2567 SourceSetCoverageStateV1::Partial
2568 );
2569 }
2570
2571 #[test]
2572 fn aggregate_text_limit_retains_nested_channel_prefix() {
2573 let mut builder = RawSourceFactsBuilderV1::new(
2574 SourceFormatV1::GltfJson,
2575 InputIdentity::from_bytes(b"channel-prefix"),
2576 );
2577 let full_rows = RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES / RAW_SOURCE_V1_MAX_TEXT_BYTES;
2578 for _ in 0..full_rows - 1 {
2579 assert!(builder.push_construct(construct(
2580 builder.facts.constructs.rows.len(),
2581 "x".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES)
2582 )));
2583 }
2584 assert!(builder.push_construct(construct(
2585 builder.facts.constructs.rows.len(),
2586 "x".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES - 12)
2587 )));
2588 assert_eq!(builder.remaining_text_bytes(), 12);
2589
2590 let channels = SourceFactSetV1::complete(vec![
2591 named_channel(0, "aaaa"),
2592 named_channel(1, "bbbb"),
2593 named_channel(2, "cccc"),
2594 ]);
2595 assert!(builder.push_clip(SourceClipFactV1::new(
2596 0,
2597 SourceObservationV1::observed(
2598 SourceTextV1::new("name").expect("bounded name"),
2599 format_provenance(),
2600 SourceLoaderDispositionV1::Preserved,
2601 ),
2602 SourceObservationV1::proven_absent(format_provenance()),
2603 SourceObservationV1::proven_absent(format_provenance()),
2604 SourceObservationV1::proven_absent(format_provenance()),
2605 channels,
2606 )));
2607
2608 let loaded = builder.finish(Document::default()).expect("bounded prefix");
2609 let facts = loaded.source_facts();
2610 assert_eq!(
2611 facts.work().retained_text_bytes(),
2612 RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES
2613 );
2614 assert_eq!(facts.clips().rows().len(), 1);
2615 assert_eq!(facts.clips().rows()[0].channels().rows().len(), 2);
2616 assert_eq!(
2617 facts.clips().rows()[0].channels().coverage().state(),
2618 SourceSetCoverageStateV1::Partial
2619 );
2620 assert_eq!(
2621 facts.clips().coverage().state(),
2622 SourceSetCoverageStateV1::Partial
2623 );
2624 }
2625
2626 #[test]
2627 fn total_observation_limit_retains_exact_prefix_and_work_count() {
2628 let mut builder = RawSourceFactsBuilderV1::new(
2629 SourceFormatV1::GltfJson,
2630 InputIdentity::from_bytes(b"gltf"),
2631 );
2632 for index in 0..=RAW_SOURCE_V1_MAX_OBSERVATIONS {
2633 let retained = builder.push_construct(construct(index, format!("e{index}")));
2634 assert_eq!(retained, index < RAW_SOURCE_V1_MAX_OBSERVATIONS);
2635 }
2636 assert!(!builder.push_construct(construct(
2637 RAW_SOURCE_V1_MAX_OBSERVATIONS + 1,
2638 "must-not-resume".to_string()
2639 )));
2640 let loaded = builder.finish(Document::default()).expect("bounded facts");
2641 let facts = loaded.source_facts();
2642 assert_eq!(
2643 facts.constructs().rows().len(),
2644 RAW_SOURCE_V1_MAX_OBSERVATIONS
2645 );
2646 assert_eq!(
2647 facts.constructs().coverage(),
2648 SourceSetCoverageV1::partial(SourceUnavailableReasonV1::ProjectionBudgetExceeded)
2649 );
2650 assert_eq!(
2651 facts.work().inspected_rows(),
2652 RAW_SOURCE_V1_MAX_OBSERVATIONS + 1
2653 );
2654 assert_eq!(facts.work().retained_rows(), RAW_SOURCE_V1_MAX_OBSERVATIONS);
2655 }
2656
2657 #[test]
2658 fn unavailable_domain_discards_prefix_and_updates_retained_work() {
2659 let mut builder = RawSourceFactsBuilderV1::new(
2660 SourceFormatV1::GltfJson,
2661 InputIdentity::from_bytes(b"discarded-prefix"),
2662 );
2663 assert!(builder.push_construct(construct(0, "retained-name".to_string())));
2664 assert_eq!(
2665 builder.remaining_observation_rows(),
2666 RAW_SOURCE_V1_MAX_OBSERVATIONS - 1
2667 );
2668 assert_eq!(
2669 builder.remaining_text_bytes(),
2670 RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES - 13
2671 );
2672
2673 builder.mark_unavailable(
2674 SourceFactDomainV1::Constructs,
2675 SourceUnavailableReasonV1::ParserUnavailable,
2676 );
2677 builder.mark_partial(
2678 SourceFactDomainV1::Constructs,
2679 SourceUnavailableReasonV1::ProjectionBudgetExceeded,
2680 );
2681 let loaded = builder
2682 .finish(Document::default())
2683 .expect("unavailable set remains valid");
2684 let facts = loaded.source_facts();
2685 assert!(facts.constructs().rows().is_empty());
2686 assert_eq!(
2687 facts.constructs().coverage(),
2688 SourceSetCoverageV1::unavailable(SourceUnavailableReasonV1::ParserUnavailable)
2689 );
2690 assert_eq!(facts.work().retained_rows(), 0);
2691 assert_eq!(facts.work().retained_text_bytes(), 0);
2692 assert_eq!(facts.work().inspected_rows(), 1);
2693 }
2694
2695 #[test]
2696 fn preallocation_budget_stop_counts_terminal_row() {
2697 let mut builder = RawSourceFactsBuilderV1::new(
2698 SourceFormatV1::GltfJson,
2699 InputIdentity::from_bytes(b"preallocation-stop"),
2700 );
2701 assert!(builder.push_construct(construct(0, "first".to_string())));
2702 builder.mark_budget_exceeded(SourceFactDomainV1::Constructs);
2703 assert!(!builder.push_construct(construct(1, "must-not-resume".to_string())));
2704 let loaded = builder.finish(Document::default()).expect("partial prefix");
2705 let facts = loaded.source_facts();
2706 assert_eq!(facts.work().inspected_rows(), 2);
2707 assert_eq!(facts.work().retained_rows(), 1);
2708 assert_eq!(facts.constructs().rows()[0].name().as_str(), "first");
2709 assert_eq!(
2710 facts.constructs().coverage(),
2711 SourceSetCoverageV1::partial(SourceUnavailableReasonV1::ProjectionBudgetExceeded)
2712 );
2713 }
2714
2715 #[test]
2716 fn text_and_traversal_limits_are_exact_and_coverage_qualified() {
2717 assert!(SourceTextV1::new("x".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES)).is_ok());
2718 assert_eq!(
2719 SourceTextV1::new("x".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES + 1)),
2720 Err(SourceFactsError::TextTooLong {
2721 bytes: RAW_SOURCE_V1_MAX_TEXT_BYTES + 1,
2722 limit: RAW_SOURCE_V1_MAX_TEXT_BYTES,
2723 })
2724 );
2725
2726 let mut builder = RawSourceFactsBuilderV1::new(
2727 SourceFormatV1::GltfJson,
2728 InputIdentity::from_bytes(b"gltf"),
2729 );
2730 let rows_at_limit = RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES / RAW_SOURCE_V1_MAX_TEXT_BYTES;
2731 for index in 0..rows_at_limit {
2732 assert!(
2733 builder.push_construct(construct(index, "x".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES)))
2734 );
2735 }
2736 assert!(!builder.push_construct(construct(
2737 rows_at_limit,
2738 "x".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES)
2739 )));
2740 assert!(!builder.push_construct(construct(rows_at_limit + 1, "short".to_string())));
2741 assert!(
2742 !builder.set_linear_unit(SourceObservationV1::observed(
2743 SourceLinearUnitV1::new(1.0).expect("metres"),
2744 SourceProvenanceV1::source_declared(
2745 SourceLogicalLocatorV1::gltf_json_pointer("/animations")
2746 .expect("generated logical locator"),
2747 ),
2748 SourceLoaderDispositionV1::Preserved,
2749 ))
2750 );
2751 assert!(builder.observe_traversal_depth(
2752 SourceFactDomainV1::Resources,
2753 RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH
2754 ));
2755 assert!(!builder.observe_traversal_depth(
2756 SourceFactDomainV1::Resources,
2757 RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH + 1
2758 ));
2759 assert!(!builder.push_resource(resource(0)));
2760 let loaded = builder.finish(Document::default()).expect("bounded facts");
2761 let facts = loaded.source_facts();
2762 assert_eq!(
2763 facts.work().retained_text_bytes(),
2764 RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES
2765 );
2766 assert!(matches!(
2767 facts.linear_unit().state(),
2768 SourceObservationStateV1::Unavailable(
2769 SourceUnavailableReasonV1::ProjectionBudgetExceeded
2770 )
2771 ));
2772 assert_eq!(
2773 facts.work().max_traversal_depth(),
2774 RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH + 1
2775 );
2776 assert_eq!(
2777 facts.resources().coverage().state(),
2778 SourceSetCoverageStateV1::Partial
2779 );
2780 }
2781
2782 #[test]
2783 fn finish_rejects_stale_normalized_clip_mappings_and_noncanonical_order() {
2784 let observed_index = |index| {
2785 SourceObservationV1::observed(
2786 index,
2787 format_provenance(),
2788 SourceLoaderDispositionV1::Preserved,
2789 )
2790 };
2791 let make = |source_index, normalized_index| {
2792 SourceClipFactV1::new(
2793 source_index,
2794 SourceObservationV1::proven_absent(format_provenance()),
2795 observed_index(normalized_index),
2796 SourceObservationV1::proven_absent(format_provenance()),
2797 SourceObservationV1::proven_absent(format_provenance()),
2798 SourceFactSetV1::complete(Vec::new()),
2799 )
2800 };
2801
2802 let mut stale =
2803 RawSourceFactsBuilderV1::new(SourceFormatV1::Fbx, InputIdentity::from_bytes(b"fbx"));
2804 assert!(stale.push_clip(make(0, 0)));
2805 assert!(matches!(
2806 stale.finish(Document::default()),
2807 Err(SourceFactsError::NormalizedClipIndexOutOfRange { .. })
2808 ));
2809
2810 let mut unordered =
2811 RawSourceFactsBuilderV1::new(SourceFormatV1::Fbx, InputIdentity::from_bytes(b"fbx"));
2812 assert!(unordered.push_clip(clip(1)));
2813 assert!(unordered.push_clip(clip(0)));
2814 assert!(matches!(
2815 unordered.finish(Document::default()),
2816 Err(SourceFactsError::NonCanonicalClipIndex { .. })
2817 ));
2818
2819 let mut channel_gap =
2820 RawSourceFactsBuilderV1::new(SourceFormatV1::Fbx, InputIdentity::from_bytes(b"fbx"));
2821 assert!(channel_gap.push_clip(SourceClipFactV1::new(
2822 0,
2823 SourceObservationV1::proven_absent(format_provenance()),
2824 unavailable(),
2825 SourceObservationV1::proven_absent(format_provenance()),
2826 SourceObservationV1::proven_absent(format_provenance()),
2827 SourceFactSetV1::partial(
2828 vec![named_channel(1, "gap")],
2829 SourceUnavailableReasonV1::ParserUnavailable,
2830 ),
2831 )));
2832 assert!(matches!(
2833 channel_gap.finish(Document::default()),
2834 Err(SourceFactsError::NonCanonicalChannelIndex { .. })
2835 ));
2836
2837 let mut construct_gap = RawSourceFactsBuilderV1::new(
2838 SourceFormatV1::GltfJson,
2839 InputIdentity::from_bytes(b"gltf"),
2840 );
2841 assert!(construct_gap.push_construct(construct(1, "gap".to_string())));
2842 assert!(matches!(
2843 construct_gap.finish(Document::default()),
2844 Err(SourceFactsError::NonCanonicalConstructOrder { .. })
2845 ));
2846
2847 let mut resource_gap = RawSourceFactsBuilderV1::new(
2848 SourceFormatV1::GltfJson,
2849 InputIdentity::from_bytes(b"gltf"),
2850 );
2851 assert!(resource_gap.push_resource(resource(1)));
2852 assert!(matches!(
2853 resource_gap.finish(Document::default()),
2854 Err(SourceFactsError::NonCanonicalResourceOrder { .. })
2855 ));
2856 }
2857}