use crate::{
DependencyClosureError, DependencyClosureV1, Document, InputIdentity, SourceSkeletonAssets,
};
use serde::Serialize;
use std::fmt;
pub const RAW_SOURCE_FACTS_V1_ID: &str = "urn:animsmith:raw-source-facts:1";
pub const RAW_SOURCE_V1_MAX_OBSERVATIONS: usize = 65_536;
pub const RAW_SOURCE_V1_MAX_CLIPS: usize = 4_096;
pub const RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES: usize = 4_096;
pub const RAW_SOURCE_V1_MAX_TEXT_BYTES: usize = 4_096;
pub const RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES: usize = 8 * 1024 * 1024;
pub const RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH: usize = 128;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SourceFormatV1 {
GltfJson,
Glb,
Fbx,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SourceUnavailableReasonV1 {
Malformed,
Discarded,
NormalizedAway,
BakedAway,
LoaderUnsupported,
ProjectionBudgetExceeded,
ParserUnavailable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceSetCoverageStateV1 {
Complete,
Partial,
Unavailable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SourceSetCoverageV1 {
state: SourceSetCoverageStateV1,
reason: Option<SourceUnavailableReasonV1>,
}
impl SourceSetCoverageV1 {
pub const fn complete() -> Self {
Self {
state: SourceSetCoverageStateV1::Complete,
reason: None,
}
}
pub const fn partial(reason: SourceUnavailableReasonV1) -> Self {
Self {
state: SourceSetCoverageStateV1::Partial,
reason: Some(reason),
}
}
pub const fn unavailable(reason: SourceUnavailableReasonV1) -> Self {
Self {
state: SourceSetCoverageStateV1::Unavailable,
reason: Some(reason),
}
}
pub const fn state(self) -> SourceSetCoverageStateV1 {
self.state
}
pub const fn reason(self) -> Option<SourceUnavailableReasonV1> {
self.reason
}
pub const fn proves_absence(self) -> bool {
matches!(self.state, SourceSetCoverageStateV1::Complete)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SourceLoaderDispositionV1 {
Preserved,
Normalized,
Baked,
Discarded,
Unsupported,
Unknown,
NotApplicable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SourceProvenanceKindV1 {
FormatDefined,
SourceDeclared,
ParserProjected,
DerivedFromSource,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SourceTextV1(String);
impl SourceTextV1 {
pub fn new(value: impl AsRef<str>) -> Result<Self, SourceFactsError> {
let value = value.as_ref();
if value.len() > RAW_SOURCE_V1_MAX_TEXT_BYTES {
return Err(SourceFactsError::TextTooLong {
bytes: value.len(),
limit: RAW_SOURCE_V1_MAX_TEXT_BYTES,
});
}
Ok(Self(value.to_owned()))
}
pub fn as_str(&self) -> &str {
&self.0
}
fn retained_bytes(&self) -> usize {
self.0.len()
}
}
impl fmt::Debug for SourceTextV1 {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_tuple("SourceTextV1")
.field(&self.0)
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct SourceLogicalLocatorV1 {
text: SourceTextV1,
}
impl SourceLogicalLocatorV1 {
pub fn gltf_json_pointer(value: impl AsRef<str>) -> Result<Self, SourceFactsError> {
let value = value.as_ref();
let mut segments = value
.strip_prefix('/')
.into_iter()
.flat_map(|value| value.split('/'));
let valid_root = matches!(
segments.next(),
Some("animations" | "buffers" | "images" | "extensionsUsed" | "extensionsRequired")
);
if !valid_root || !segments.all(valid_logical_segment) {
return Err(SourceFactsError::InvalidLogicalLocator);
}
Ok(Self {
text: SourceTextV1::new(value)?,
})
}
pub fn fbx_parser_path(value: impl AsRef<str>) -> Result<Self, SourceFactsError> {
let value = value.as_ref();
let Some(path) = value.strip_prefix("fbx:") else {
return Err(SourceFactsError::InvalidLogicalLocator);
};
if path.is_empty()
|| path
.split('/')
.any(|segment| !valid_logical_segment(segment))
{
return Err(SourceFactsError::InvalidLogicalLocator);
}
Ok(Self {
text: SourceTextV1::new(value)?,
})
}
pub fn as_str(&self) -> &str {
self.text.as_str()
}
fn retained_bytes(&self) -> usize {
self.text.retained_bytes()
}
}
fn valid_logical_segment(segment: &str) -> bool {
!segment.is_empty()
&& !matches!(segment, "." | "..")
&& segment.bytes().all(|value| {
value.is_ascii_alphanumeric() || matches!(value, b'_' | b'-' | b'.' | b'*')
})
}
#[derive(Clone, PartialEq, Eq)]
pub struct SourceProvenanceV1 {
kind: SourceProvenanceKindV1,
locator: Option<SourceLogicalLocatorV1>,
}
impl fmt::Debug for SourceProvenanceV1 {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SourceProvenanceV1")
.field("kind", &self.kind)
.field("locator_retained", &self.locator.is_some())
.finish()
}
}
impl SourceProvenanceV1 {
pub const fn format_defined() -> Self {
Self {
kind: SourceProvenanceKindV1::FormatDefined,
locator: None,
}
}
pub fn source_declared(locator: SourceLogicalLocatorV1) -> Self {
Self {
kind: SourceProvenanceKindV1::SourceDeclared,
locator: Some(locator),
}
}
pub fn parser_projected(locator: SourceLogicalLocatorV1) -> Self {
Self {
kind: SourceProvenanceKindV1::ParserProjected,
locator: Some(locator),
}
}
pub fn derived_from_source(locator: SourceLogicalLocatorV1) -> Self {
Self {
kind: SourceProvenanceKindV1::DerivedFromSource,
locator: Some(locator),
}
}
pub const fn kind(&self) -> SourceProvenanceKindV1 {
self.kind
}
pub fn locator(&self) -> Option<&SourceLogicalLocatorV1> {
self.locator.as_ref()
}
fn retained_bytes(&self) -> usize {
self.locator
.as_ref()
.map_or(0, SourceLogicalLocatorV1::retained_bytes)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum SourceObservationStateV1<T> {
Observed(T),
ProvenAbsent,
Unavailable(SourceUnavailableReasonV1),
}
#[derive(Debug, Clone, PartialEq)]
pub struct SourceObservationV1<T> {
state: SourceObservationStateV1<T>,
disposition: SourceLoaderDispositionV1,
provenance: Option<SourceProvenanceV1>,
}
impl<T> SourceObservationV1<T> {
pub fn observed(
value: T,
provenance: SourceProvenanceV1,
disposition: SourceLoaderDispositionV1,
) -> Self {
Self {
state: SourceObservationStateV1::Observed(value),
disposition,
provenance: Some(provenance),
}
}
pub fn proven_absent(provenance: SourceProvenanceV1) -> Self {
Self {
state: SourceObservationStateV1::ProvenAbsent,
disposition: SourceLoaderDispositionV1::NotApplicable,
provenance: Some(provenance),
}
}
pub fn unavailable(
reason: SourceUnavailableReasonV1,
provenance: Option<SourceProvenanceV1>,
disposition: SourceLoaderDispositionV1,
) -> Self {
Self {
state: SourceObservationStateV1::Unavailable(reason),
disposition,
provenance,
}
}
pub const fn state(&self) -> &SourceObservationStateV1<T> {
&self.state
}
pub const fn disposition(&self) -> SourceLoaderDispositionV1 {
self.disposition
}
pub fn provenance(&self) -> Option<&SourceProvenanceV1> {
self.provenance.as_ref()
}
fn retained_bytes(&self) -> usize {
self.provenance
.as_ref()
.map_or(0, SourceProvenanceV1::retained_bytes)
}
}
impl SourceObservationV1<SourceTextV1> {
fn retained_text_bytes(&self) -> usize {
let value_bytes = match &self.state {
SourceObservationStateV1::Observed(value) => value.retained_bytes(),
SourceObservationStateV1::ProvenAbsent | SourceObservationStateV1::Unavailable(_) => 0,
};
value_bytes.saturating_add(self.retained_bytes())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SourceFactSetV1<T> {
coverage: SourceSetCoverageV1,
rows: Vec<T>,
}
impl<T> SourceFactSetV1<T> {
pub fn complete(rows: Vec<T>) -> Self {
Self {
coverage: SourceSetCoverageV1::complete(),
rows,
}
}
pub fn partial(rows: Vec<T>, reason: SourceUnavailableReasonV1) -> Self {
Self {
coverage: SourceSetCoverageV1::partial(reason),
rows,
}
}
pub fn unavailable(reason: SourceUnavailableReasonV1) -> Self {
Self {
coverage: SourceSetCoverageV1::unavailable(reason),
rows: Vec::new(),
}
}
pub const fn coverage(&self) -> SourceSetCoverageV1 {
self.coverage
}
pub fn rows(&self) -> &[T] {
&self.rows
}
pub fn proves_absence(&self) -> bool {
self.rows.is_empty() && self.coverage.proves_absence()
}
fn mark_partial(&mut self, reason: SourceUnavailableReasonV1) {
self.coverage = SourceSetCoverageV1::partial(reason);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SourceAxisV1 {
PositiveX,
NegativeX,
PositiveY,
NegativeY,
PositiveZ,
NegativeZ,
}
impl SourceAxisV1 {
const fn unsigned(self) -> u8 {
match self {
Self::PositiveX | Self::NegativeX => 0,
Self::PositiveY | Self::NegativeY => 1,
Self::PositiveZ | Self::NegativeZ => 2,
}
}
const fn vector(self) -> [i8; 3] {
match self {
Self::PositiveX => [1, 0, 0],
Self::NegativeX => [-1, 0, 0],
Self::PositiveY => [0, 1, 0],
Self::NegativeY => [0, -1, 0],
Self::PositiveZ => [0, 0, 1],
Self::NegativeZ => [0, 0, -1],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceHandednessV1 {
Right,
Left,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SourceCoordinateBasisV1 {
right: SourceAxisV1,
up: SourceAxisV1,
forward: SourceAxisV1,
}
impl SourceCoordinateBasisV1 {
pub fn new(
right: SourceAxisV1,
up: SourceAxisV1,
forward: SourceAxisV1,
) -> Result<Self, SourceFactsError> {
if right.unsigned() == up.unsigned()
|| right.unsigned() == forward.unsigned()
|| up.unsigned() == forward.unsigned()
{
return Err(SourceFactsError::DuplicateBasisAxis);
}
Ok(Self { right, up, forward })
}
pub const fn right(self) -> SourceAxisV1 {
self.right
}
pub const fn up(self) -> SourceAxisV1 {
self.up
}
pub const fn forward(self) -> SourceAxisV1 {
self.forward
}
pub fn handedness(self) -> SourceHandednessV1 {
let [rx, ry, rz] = self.right.vector();
let [ux, uy, uz] = self.up.vector();
let [fx, fy, fz] = self.forward.vector();
let determinant = i16::from(rx)
* (i16::from(uy) * i16::from(fz) - i16::from(uz) * i16::from(fy))
- i16::from(ry) * (i16::from(ux) * i16::from(fz) - i16::from(uz) * i16::from(fx))
+ i16::from(rz) * (i16::from(ux) * i16::from(fy) - i16::from(uy) * i16::from(fx));
if determinant > 0 {
SourceHandednessV1::Right
} else {
SourceHandednessV1::Left
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SourceLinearUnitV1(f64);
impl SourceLinearUnitV1 {
pub fn new(meters_per_source_unit: f64) -> Result<Self, SourceFactsError> {
if !meters_per_source_unit.is_finite() || meters_per_source_unit <= 0.0 {
return Err(SourceFactsError::InvalidLinearUnit);
}
Ok(Self(meters_per_source_unit))
}
pub const fn meters_per_source_unit(self) -> f64 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SourceFramesPerSecondV1(f64);
impl SourceFramesPerSecondV1 {
pub fn new(value: f64) -> Result<Self, SourceFactsError> {
if !value.is_finite() || value <= 0.0 {
return Err(SourceFactsError::InvalidFramesPerSecond);
}
Ok(Self(value))
}
pub const fn get(self) -> f64 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SourceTimeRangeV1 {
begin_s: f64,
end_s: f64,
}
impl SourceTimeRangeV1 {
pub fn new(begin_s: f64, end_s: f64) -> Result<Self, SourceFactsError> {
if !begin_s.is_finite() || !end_s.is_finite() || begin_s > end_s {
return Err(SourceFactsError::InvalidTimeRange);
}
Ok(Self { begin_s, end_s })
}
pub const fn begin_s(self) -> f64 {
self.begin_s
}
pub const fn end_s(self) -> f64 {
self.end_s
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SourceChannelPropertyV1 {
Translation,
Rotation,
Scale,
Weights,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SourceInterpolationV1 {
Step,
Linear,
CubicSpline,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SourceComponentMaskV1 {
x: bool,
y: bool,
z: bool,
}
impl SourceComponentMaskV1 {
pub const fn new(x: bool, y: bool, z: bool) -> Self {
Self { x, y, z }
}
pub const fn x(self) -> bool {
self.x
}
pub const fn y(self) -> bool {
self.y
}
pub const fn z(self) -> bool {
self.z
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SourceTargetKindV1 {
Node,
Element,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct SourceTargetV1 {
kind: SourceTargetKindV1,
index: u64,
}
impl SourceTargetV1 {
pub const fn new(kind: SourceTargetKindV1, index: u64) -> Self {
Self { kind, index }
}
pub const fn kind(self) -> SourceTargetKindV1 {
self.kind
}
pub const fn index(self) -> u64 {
self.index
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SourceChannelFactV1 {
source_channel_index: usize,
source_layer_index: Option<usize>,
target: SourceTargetV1,
property: SourceChannelPropertyV1,
property_name: Option<SourceTextV1>,
components: SourceComponentMaskV1,
interpolation: SourceObservationV1<SourceInterpolationV1>,
input_accessor_index: Option<usize>,
output_accessor_index: Option<usize>,
disposition: SourceLoaderDispositionV1,
provenance: SourceProvenanceV1,
}
impl SourceChannelFactV1 {
pub fn new(
source_channel_index: usize,
target: SourceTargetV1,
property: SourceChannelPropertyV1,
components: SourceComponentMaskV1,
interpolation: SourceObservationV1<SourceInterpolationV1>,
disposition: SourceLoaderDispositionV1,
provenance: SourceProvenanceV1,
) -> Self {
Self {
source_channel_index,
source_layer_index: None,
target,
property,
property_name: None,
components,
interpolation,
input_accessor_index: None,
output_accessor_index: None,
disposition,
provenance,
}
}
pub fn with_source_layer_index(mut self, index: usize) -> Self {
self.source_layer_index = Some(index);
self
}
pub fn with_property_name(mut self, name: SourceTextV1) -> Self {
self.property_name = Some(name);
self
}
pub fn with_accessors(mut self, input: usize, output: usize) -> Self {
self.input_accessor_index = Some(input);
self.output_accessor_index = Some(output);
self
}
pub const fn source_channel_index(&self) -> usize {
self.source_channel_index
}
pub const fn source_layer_index(&self) -> Option<usize> {
self.source_layer_index
}
pub const fn target(&self) -> SourceTargetV1 {
self.target
}
pub const fn property(&self) -> SourceChannelPropertyV1 {
self.property
}
pub fn property_name(&self) -> Option<&SourceTextV1> {
self.property_name.as_ref()
}
pub const fn components(&self) -> SourceComponentMaskV1 {
self.components
}
pub const fn interpolation(&self) -> &SourceObservationV1<SourceInterpolationV1> {
&self.interpolation
}
pub const fn input_accessor_index(&self) -> Option<usize> {
self.input_accessor_index
}
pub const fn output_accessor_index(&self) -> Option<usize> {
self.output_accessor_index
}
pub const fn disposition(&self) -> SourceLoaderDispositionV1 {
self.disposition
}
pub const fn provenance(&self) -> &SourceProvenanceV1 {
&self.provenance
}
fn retained_bytes(&self) -> usize {
self.property_name
.as_ref()
.map_or(0, SourceTextV1::retained_bytes)
.saturating_add(self.interpolation.retained_bytes())
.saturating_add(self.provenance.retained_bytes())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SourceClipFactV1 {
source_clip_index: usize,
source_name: SourceObservationV1<SourceTextV1>,
normalized_clip_index: SourceObservationV1<usize>,
source_range: SourceObservationV1<SourceTimeRangeV1>,
sampler_range: SourceObservationV1<SourceTimeRangeV1>,
channels: SourceFactSetV1<SourceChannelFactV1>,
}
impl SourceClipFactV1 {
pub fn new(
source_clip_index: usize,
source_name: SourceObservationV1<SourceTextV1>,
normalized_clip_index: SourceObservationV1<usize>,
source_range: SourceObservationV1<SourceTimeRangeV1>,
sampler_range: SourceObservationV1<SourceTimeRangeV1>,
channels: SourceFactSetV1<SourceChannelFactV1>,
) -> Self {
Self {
source_clip_index,
source_name,
normalized_clip_index,
source_range,
sampler_range,
channels,
}
}
pub const fn source_clip_index(&self) -> usize {
self.source_clip_index
}
pub const fn source_name(&self) -> &SourceObservationV1<SourceTextV1> {
&self.source_name
}
pub const fn normalized_clip_index(&self) -> &SourceObservationV1<usize> {
&self.normalized_clip_index
}
pub const fn source_range(&self) -> &SourceObservationV1<SourceTimeRangeV1> {
&self.source_range
}
pub const fn sampler_range(&self) -> &SourceObservationV1<SourceTimeRangeV1> {
&self.sampler_range
}
pub const fn channels(&self) -> &SourceFactSetV1<SourceChannelFactV1> {
&self.channels
}
fn retained_row_count(&self) -> usize {
1usize.saturating_add(self.channels.rows.len())
}
fn retained_bytes(&self) -> usize {
self.retained_non_channel_bytes().saturating_add(
self.channels
.rows
.iter()
.map(SourceChannelFactV1::retained_bytes)
.fold(0usize, usize::saturating_add),
)
}
fn retained_non_channel_bytes(&self) -> usize {
self.source_name
.retained_text_bytes()
.saturating_add(self.normalized_clip_index.retained_bytes())
.saturating_add(self.source_range.retained_bytes())
.saturating_add(self.sampler_range.retained_bytes())
}
fn truncate_channels(&mut self, retained: usize) {
if self.channels.rows.len() > retained {
self.channels.rows.truncate(retained);
self.channels
.mark_partial(SourceUnavailableReasonV1::ProjectionBudgetExceeded);
}
}
fn truncate_channels_to_text(&mut self, available_bytes: usize) -> bool {
let fixed_bytes = self.retained_non_channel_bytes();
if fixed_bytes > available_bytes {
return false;
}
let mut retained_bytes = fixed_bytes;
let retained_channels = self
.channels
.rows
.iter()
.take_while(|channel| {
let next = retained_bytes.saturating_add(channel.retained_bytes());
if next > available_bytes {
false
} else {
retained_bytes = next;
true
}
})
.count();
self.truncate_channels(retained_channels);
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SourceConstructKindV1 {
Extension,
CustomProperty,
UnknownElement,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceConstructFactV1 {
source_order_index: usize,
kind: SourceConstructKindV1,
name: SourceTextV1,
required: bool,
count: u64,
disposition: SourceLoaderDispositionV1,
provenance: SourceProvenanceV1,
}
impl SourceConstructFactV1 {
pub fn new(
source_order_index: usize,
kind: SourceConstructKindV1,
name: SourceTextV1,
required: bool,
count: u64,
disposition: SourceLoaderDispositionV1,
provenance: SourceProvenanceV1,
) -> Result<Self, SourceFactsError> {
if count == 0 {
return Err(SourceFactsError::ZeroConstructCount);
}
Ok(Self {
source_order_index,
kind,
name,
required,
count,
disposition,
provenance,
})
}
pub const fn source_order_index(&self) -> usize {
self.source_order_index
}
pub const fn kind(&self) -> SourceConstructKindV1 {
self.kind
}
pub const fn name(&self) -> &SourceTextV1 {
&self.name
}
pub const fn required(&self) -> bool {
self.required
}
pub const fn count(&self) -> u64 {
self.count
}
pub const fn disposition(&self) -> SourceLoaderDispositionV1 {
self.disposition
}
pub const fn provenance(&self) -> &SourceProvenanceV1 {
&self.provenance
}
fn retained_bytes(&self) -> usize {
self.name
.retained_bytes()
.saturating_add(self.provenance.retained_bytes())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SourceResourceKindV1 {
Buffer,
Image,
Texture,
Video,
Cache,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum SourceResourceLocatorV1 {
Embedded,
DataUri,
Relative(SourceRelativeLocatorV1),
Absolute,
Escaping,
Remote,
Malformed,
Oversized,
Missing,
}
impl SourceResourceLocatorV1 {
pub fn classify(value: &str) -> Self {
if let Some(classification) = redacted_resource_locator(value) {
return classification;
}
SourceTextV1::new(value).map_or(Self::Oversized, |value| {
Self::Relative(SourceRelativeLocatorV1(value))
})
}
pub fn retained_relative_bytes(value: &str) -> usize {
if redacted_resource_locator(value).is_none() {
value.len()
} else {
0
}
}
fn retained_bytes(&self) -> usize {
match self {
Self::Relative(value) => value.0.retained_bytes(),
_ => 0,
}
}
}
fn redacted_resource_locator(value: &str) -> Option<SourceResourceLocatorV1> {
if value
.get(..5)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("data:"))
{
return Some(SourceResourceLocatorV1::DataUri);
}
if value.len() > RAW_SOURCE_V1_MAX_TEXT_BYTES {
return Some(SourceResourceLocatorV1::Oversized);
}
if value.is_empty() || value.chars().any(char::is_control) || malformed_percent_escape(value) {
return Some(SourceResourceLocatorV1::Malformed);
}
if value.starts_with(['/', '\\'])
|| value.as_bytes().get(1).is_some_and(|value| *value == b':')
|| value
.get(..5)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("file:"))
{
return Some(SourceResourceLocatorV1::Absolute);
}
if has_uri_scheme(value) {
return Some(SourceResourceLocatorV1::Remote);
}
let mut escaped = false;
let mut malformed = false;
for component in value.split(['/', '\\']) {
escaped |= component == ".." || is_encoded_dot_segment(component);
malformed |= component.is_empty() || component == ".";
}
if escaped || contains_encoded_path_escape(value) {
return Some(SourceResourceLocatorV1::Escaping);
}
if malformed {
return Some(SourceResourceLocatorV1::Malformed);
}
None
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct SourceRelativeLocatorV1(SourceTextV1);
impl SourceRelativeLocatorV1 {
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
fn malformed_percent_escape(value: &str) -> bool {
let bytes = value.as_bytes();
let mut index = 0;
while index < bytes.len() {
if bytes[index] != b'%' {
index += 1;
continue;
}
if bytes.get(index + 1).and_then(|value| hex(*value)).is_none()
|| bytes.get(index + 2).and_then(|value| hex(*value)).is_none()
{
return true;
}
index += 3;
}
false
}
fn contains_encoded_path_escape(value: &str) -> bool {
let lower = value.to_ascii_lowercase();
lower.contains("%2f") || lower.contains("%5c") || lower.contains("%00")
}
fn is_encoded_dot_segment(value: &str) -> bool {
let bytes = value.as_bytes();
let mut index = 0;
let mut dots = 0;
let mut encoded = false;
while index < bytes.len() {
if bytes[index] == b'.' {
dots += 1;
index += 1;
} else if bytes.get(index..index + 3).is_some_and(|escape| {
escape[0] == b'%' && escape[1] == b'2' && matches!(escape[2], b'e' | b'E')
}) {
dots += 1;
encoded = true;
index += 3;
} else {
return false;
}
if dots > 2 {
return false;
}
}
encoded && matches!(dots, 1 | 2)
}
fn hex(value: u8) -> Option<u8> {
match value {
b'0'..=b'9' => Some(value - b'0'),
b'a'..=b'f' => Some(value - b'a' + 10),
b'A'..=b'F' => Some(value - b'A' + 10),
_ => None,
}
}
fn has_uri_scheme(value: &str) -> bool {
let Some((scheme, _)) = value.split_once(':') else {
return false;
};
!scheme.is_empty()
&& scheme.as_bytes()[0].is_ascii_alphabetic()
&& scheme
.bytes()
.all(|value| value.is_ascii_alphanumeric() || matches!(value, b'+' | b'-' | b'.'))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceResourceReferenceV1 {
source_order_index: usize,
kind: SourceResourceKindV1,
source_index: u64,
locator: SourceResourceLocatorV1,
disposition: SourceLoaderDispositionV1,
provenance: SourceProvenanceV1,
}
impl SourceResourceReferenceV1 {
pub fn new(
source_order_index: usize,
kind: SourceResourceKindV1,
source_index: u64,
locator: SourceResourceLocatorV1,
disposition: SourceLoaderDispositionV1,
provenance: SourceProvenanceV1,
) -> Self {
Self {
source_order_index,
kind,
source_index,
locator,
disposition,
provenance,
}
}
pub const fn source_order_index(&self) -> usize {
self.source_order_index
}
pub const fn kind(&self) -> SourceResourceKindV1 {
self.kind
}
pub const fn source_index(&self) -> u64 {
self.source_index
}
pub const fn locator(&self) -> &SourceResourceLocatorV1 {
&self.locator
}
pub const fn disposition(&self) -> SourceLoaderDispositionV1 {
self.disposition
}
pub const fn provenance(&self) -> &SourceProvenanceV1 {
&self.provenance
}
fn retained_bytes(&self) -> usize {
self.locator
.retained_bytes()
.saturating_add(self.provenance.retained_bytes())
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct SourceProjectionWorkV1 {
inspected_rows: usize,
retained_rows: usize,
retained_text_bytes: usize,
max_traversal_depth: usize,
}
impl SourceProjectionWorkV1 {
pub const fn inspected_rows(self) -> usize {
self.inspected_rows
}
pub const fn retained_rows(self) -> usize {
self.retained_rows
}
pub const fn retained_text_bytes(self) -> usize {
self.retained_text_bytes
}
pub const fn max_traversal_depth(self) -> usize {
self.max_traversal_depth
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceFactDomainV1 {
Clips,
Constructs,
Resources,
}
impl SourceFactDomainV1 {
const fn index(self) -> usize {
match self {
Self::Clips => 0,
Self::Constructs => 1,
Self::Resources => 2,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RawSourceFactsV1 {
format: SourceFormatV1,
primary_identity: InputIdentity,
linear_unit: SourceObservationV1<SourceLinearUnitV1>,
coordinate_basis: SourceObservationV1<SourceCoordinateBasisV1>,
frames_per_second: SourceObservationV1<SourceFramesPerSecondV1>,
clips: SourceFactSetV1<SourceClipFactV1>,
constructs: SourceFactSetV1<SourceConstructFactV1>,
resources: SourceFactSetV1<SourceResourceReferenceV1>,
work: SourceProjectionWorkV1,
}
pub struct RawSourceFactsBuilderV1 {
facts: RawSourceFactsV1,
stopped: [bool; 3],
}
fn unavailable_observation<T>() -> SourceObservationV1<T> {
SourceObservationV1::unavailable(
SourceUnavailableReasonV1::ParserUnavailable,
None,
SourceLoaderDispositionV1::Unknown,
)
}
fn replace_scalar_observation<T>(
work: &mut SourceProjectionWorkV1,
slot: &mut SourceObservationV1<T>,
value: SourceObservationV1<T>,
) -> bool {
let previous_bytes = slot.retained_bytes();
let value_bytes = value.retained_bytes();
let baseline = work.retained_text_bytes.saturating_sub(previous_bytes);
if baseline
.checked_add(value_bytes)
.is_some_and(|total| total <= RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES)
{
work.retained_text_bytes = baseline.saturating_add(value_bytes);
*slot = value;
true
} else {
work.retained_text_bytes = baseline;
*slot = SourceObservationV1::unavailable(
SourceUnavailableReasonV1::ProjectionBudgetExceeded,
None,
SourceLoaderDispositionV1::Unknown,
);
false
}
}
impl RawSourceFactsBuilderV1 {
pub fn new(format: SourceFormatV1, primary_identity: InputIdentity) -> Self {
Self {
facts: RawSourceFactsV1 {
format,
primary_identity,
linear_unit: unavailable_observation(),
coordinate_basis: unavailable_observation(),
frames_per_second: unavailable_observation(),
clips: SourceFactSetV1::unavailable(SourceUnavailableReasonV1::ParserUnavailable),
constructs: SourceFactSetV1::unavailable(
SourceUnavailableReasonV1::ParserUnavailable,
),
resources: SourceFactSetV1::unavailable(
SourceUnavailableReasonV1::ParserUnavailable,
),
work: SourceProjectionWorkV1::default(),
},
stopped: [false; 3],
}
}
pub fn set_linear_unit(&mut self, value: SourceObservationV1<SourceLinearUnitV1>) -> bool {
replace_scalar_observation(&mut self.facts.work, &mut self.facts.linear_unit, value)
}
pub fn set_coordinate_basis(
&mut self,
value: SourceObservationV1<SourceCoordinateBasisV1>,
) -> bool {
replace_scalar_observation(
&mut self.facts.work,
&mut self.facts.coordinate_basis,
value,
)
}
pub fn set_frames_per_second(
&mut self,
value: SourceObservationV1<SourceFramesPerSecondV1>,
) -> bool {
replace_scalar_observation(
&mut self.facts.work,
&mut self.facts.frames_per_second,
value,
)
}
pub const fn remaining_observation_rows(&self) -> usize {
RAW_SOURCE_V1_MAX_OBSERVATIONS.saturating_sub(self.facts.work.retained_rows)
}
pub fn remaining_clip_rows(&self) -> usize {
RAW_SOURCE_V1_MAX_CLIPS.saturating_sub(self.facts.clips.rows.len())
}
pub fn remaining_resource_rows(&self) -> usize {
RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES.saturating_sub(self.facts.resources.rows.len())
}
pub const fn resource_coverage(&self) -> SourceSetCoverageV1 {
self.facts.resources.coverage
}
pub fn resource_rows(&self) -> &[SourceResourceReferenceV1] {
&self.facts.resources.rows
}
pub const fn primary_identity(&self) -> &InputIdentity {
&self.facts.primary_identity
}
pub const fn remaining_text_bytes(&self) -> usize {
RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES.saturating_sub(self.facts.work.retained_text_bytes)
}
pub fn mark_unavailable(
&mut self,
domain: SourceFactDomainV1,
reason: SourceUnavailableReasonV1,
) {
let (retained_rows, retained_bytes) = match domain {
SourceFactDomainV1::Clips => self
.facts
.clips
.rows
.iter()
.map(|clip| (clip.retained_row_count(), clip.retained_bytes()))
.fold(
(0usize, 0usize),
|(rows, bytes), (clip_rows, clip_bytes)| {
(
rows.saturating_add(clip_rows),
bytes.saturating_add(clip_bytes),
)
},
),
SourceFactDomainV1::Constructs => (
self.facts.constructs.rows.len(),
self.facts
.constructs
.rows
.iter()
.map(SourceConstructFactV1::retained_bytes)
.fold(0usize, usize::saturating_add),
),
SourceFactDomainV1::Resources => (
self.facts.resources.rows.len(),
self.facts
.resources
.rows
.iter()
.map(SourceResourceReferenceV1::retained_bytes)
.fold(0usize, usize::saturating_add),
),
};
self.facts.work.retained_rows = self.facts.work.retained_rows.saturating_sub(retained_rows);
self.facts.work.retained_text_bytes = self
.facts
.work
.retained_text_bytes
.saturating_sub(retained_bytes);
self.stopped[domain.index()] = true;
match domain {
SourceFactDomainV1::Clips => self.facts.clips = SourceFactSetV1::unavailable(reason),
SourceFactDomainV1::Constructs => {
self.facts.constructs = SourceFactSetV1::unavailable(reason)
}
SourceFactDomainV1::Resources => {
self.facts.resources = SourceFactSetV1::unavailable(reason)
}
}
}
pub fn mark_partial(&mut self, domain: SourceFactDomainV1, reason: SourceUnavailableReasonV1) {
if !self.stopped[domain.index()] {
*self.set_for_domain_mut(domain) = SourceSetCoverageV1::partial(reason);
}
}
pub fn mark_complete(&mut self, domain: SourceFactDomainV1) {
if !self.stopped[domain.index()]
&& matches!(
self.set_for_domain_mut(domain).state(),
SourceSetCoverageStateV1::Unavailable
)
{
*self.set_for_domain_mut(domain) = SourceSetCoverageV1::complete();
}
}
pub fn mark_budget_exceeded(&mut self, domain: SourceFactDomainV1) {
if self.stopped[domain.index()] {
return;
}
self.facts.work.inspected_rows = self.facts.work.inspected_rows.saturating_add(1);
self.stop_for_budget(domain);
}
pub fn observe_traversal_depth(&mut self, domain: SourceFactDomainV1, depth: usize) -> bool {
if self.stopped[domain.index()] {
return false;
}
self.facts.work.max_traversal_depth = self
.facts
.work
.max_traversal_depth
.max(depth.min(RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH + 1));
if depth > RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH {
self.stop_for_budget(domain);
return false;
}
true
}
pub fn push_clip(&mut self, mut clip: SourceClipFactV1) -> bool {
if self.stopped[SourceFactDomainV1::Clips.index()] {
return false;
}
if self.facts.clips.rows.len() >= RAW_SOURCE_V1_MAX_CLIPS {
self.mark_budget_exceeded(SourceFactDomainV1::Clips);
return false;
}
let remaining_rows =
RAW_SOURCE_V1_MAX_OBSERVATIONS.saturating_sub(self.facts.work.retained_rows);
if remaining_rows == 0 {
self.mark_budget_exceeded(SourceFactDomainV1::Clips);
return false;
}
let original_rows = clip.retained_row_count();
let supplied_budget_prefix = matches!(
clip.channels.coverage(),
SourceSetCoverageV1 {
state: SourceSetCoverageStateV1::Partial,
reason: Some(SourceUnavailableReasonV1::ProjectionBudgetExceeded),
}
);
let mut builder_truncated = false;
if clip.retained_row_count() > remaining_rows {
clip.truncate_channels(remaining_rows - 1);
builder_truncated = true;
}
if !clip.truncate_channels_to_text(self.remaining_text_bytes()) {
self.mark_budget_exceeded(SourceFactDomainV1::Clips);
return false;
}
builder_truncated |= clip.retained_row_count() < original_rows;
let retained_rows = clip.retained_row_count();
let inspected_rows = if builder_truncated || supplied_budget_prefix {
retained_rows.saturating_add(1)
} else {
retained_rows
};
self.facts.work.inspected_rows = self
.facts
.work
.inspected_rows
.saturating_add(inspected_rows);
if builder_truncated || supplied_budget_prefix {
self.stop_for_budget(SourceFactDomainV1::Clips);
}
self.retain_work(clip.retained_row_count(), clip.retained_bytes());
self.facts.clips.rows.push(clip);
true
}
pub fn push_construct(&mut self, row: SourceConstructFactV1) -> bool {
if self.stopped[SourceFactDomainV1::Constructs.index()] {
return false;
}
if !self.can_retain_row(row.retained_bytes()) {
self.mark_budget_exceeded(SourceFactDomainV1::Constructs);
return false;
}
self.facts.work.inspected_rows = self.facts.work.inspected_rows.saturating_add(1);
self.retain_work(1, row.retained_bytes());
self.facts.constructs.rows.push(row);
true
}
pub fn push_resource(&mut self, row: SourceResourceReferenceV1) -> bool {
if self.stopped[SourceFactDomainV1::Resources.index()] {
return false;
}
if self.facts.resources.rows.len() >= RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES
|| !self.can_retain_row(row.retained_bytes())
{
self.mark_budget_exceeded(SourceFactDomainV1::Resources);
return false;
}
self.facts.work.inspected_rows = self.facts.work.inspected_rows.saturating_add(1);
self.retain_work(1, row.retained_bytes());
self.facts.resources.rows.push(row);
true
}
pub fn finish(mut self, document: Document) -> Result<LoadedSource, SourceFactsError> {
self.qualify_unfinished_positive_rows();
let closure = DependencyClosureV1::capture_unavailable(
self.facts.primary_identity.clone(),
self.facts.resources.coverage,
);
self.finish_with_dependency_closure(document, closure)
}
pub fn finish_with_dependency_closure(
mut self,
document: Document,
dependency_closure: DependencyClosureV1,
) -> Result<LoadedSource, SourceFactsError> {
self.qualify_unfinished_positive_rows();
validate_clip_rows(&self.facts.clips, document.clips.len())?;
validate_ordered_rows(&self.facts.constructs, &self.facts.resources)?;
dependency_closure.validate_against(
self.facts.format,
&self.facts.primary_identity,
&self.facts.resources,
)?;
Ok(LoadedSource {
document,
facts: self.facts,
dependency_closure,
})
}
fn can_retain_text(&self, bytes: usize) -> bool {
self.facts
.work
.retained_text_bytes
.checked_add(bytes)
.is_some_and(|total| total <= RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES)
}
fn can_retain_row(&self, bytes: usize) -> bool {
self.facts.work.retained_rows < RAW_SOURCE_V1_MAX_OBSERVATIONS
&& self.can_retain_text(bytes)
}
fn retain_work(&mut self, rows: usize, bytes: usize) {
self.facts.work.retained_rows = self.facts.work.retained_rows.saturating_add(rows);
self.facts.work.retained_text_bytes =
self.facts.work.retained_text_bytes.saturating_add(bytes);
}
fn set_for_domain_mut(&mut self, domain: SourceFactDomainV1) -> &mut SourceSetCoverageV1 {
match domain {
SourceFactDomainV1::Clips => &mut self.facts.clips.coverage,
SourceFactDomainV1::Constructs => &mut self.facts.constructs.coverage,
SourceFactDomainV1::Resources => &mut self.facts.resources.coverage,
}
}
fn stop_for_budget(&mut self, domain: SourceFactDomainV1) {
*self.set_for_domain_mut(domain) =
SourceSetCoverageV1::partial(SourceUnavailableReasonV1::ProjectionBudgetExceeded);
self.stopped[domain.index()] = true;
}
fn qualify_unfinished_positive_rows(&mut self) {
for domain in [
SourceFactDomainV1::Clips,
SourceFactDomainV1::Constructs,
SourceFactDomainV1::Resources,
] {
let has_rows = match domain {
SourceFactDomainV1::Clips => !self.facts.clips.rows.is_empty(),
SourceFactDomainV1::Constructs => !self.facts.constructs.rows.is_empty(),
SourceFactDomainV1::Resources => !self.facts.resources.rows.is_empty(),
};
if has_rows
&& matches!(
self.set_for_domain_mut(domain).state(),
SourceSetCoverageStateV1::Unavailable
)
{
*self.set_for_domain_mut(domain) =
SourceSetCoverageV1::partial(SourceUnavailableReasonV1::ParserUnavailable);
}
}
}
}
fn validate_clip_rows(
clips: &SourceFactSetV1<SourceClipFactV1>,
normalized_clip_count: usize,
) -> Result<(), SourceFactsError> {
for (expected_clip_index, clip) in clips.rows.iter().enumerate() {
if clip.source_clip_index != expected_clip_index {
return Err(SourceFactsError::NonCanonicalClipIndex {
expected: expected_clip_index,
actual: clip.source_clip_index,
});
}
if let SourceObservationStateV1::Observed(index) = clip.normalized_clip_index.state()
&& *index >= normalized_clip_count
{
return Err(SourceFactsError::NormalizedClipIndexOutOfRange {
index: *index,
clip_count: normalized_clip_count,
});
}
for (expected_channel_index, channel) in clip.channels.rows.iter().enumerate() {
if channel.source_channel_index != expected_channel_index {
return Err(SourceFactsError::NonCanonicalChannelIndex {
source_clip_index: clip.source_clip_index,
expected: expected_channel_index,
actual: channel.source_channel_index,
});
}
}
}
Ok(())
}
fn validate_ordered_rows(
constructs: &SourceFactSetV1<SourceConstructFactV1>,
resources: &SourceFactSetV1<SourceResourceReferenceV1>,
) -> Result<(), SourceFactsError> {
for (expected, row) in constructs.rows.iter().enumerate() {
if row.source_order_index != expected {
return Err(SourceFactsError::NonCanonicalConstructOrder {
expected,
actual: row.source_order_index,
});
}
}
for (expected, row) in resources.rows.iter().enumerate() {
if row.source_order_index != expected {
return Err(SourceFactsError::NonCanonicalResourceOrder {
expected,
actual: row.source_order_index,
});
}
}
Ok(())
}
pub struct LoadedSource {
document: Document,
facts: RawSourceFactsV1,
dependency_closure: DependencyClosureV1,
}
impl fmt::Debug for LoadedSource {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("LoadedSource")
.field("format", &self.facts.format)
.field("primary_identity", &self.facts.primary_identity)
.field("dependency_closure", &self.dependency_closure)
.field("work", &self.facts.work)
.finish_non_exhaustive()
}
}
impl LoadedSource {
pub const fn document(&self) -> &Document {
&self.document
}
pub fn source_facts(&self) -> SourceFactsViewV1<'_> {
SourceFactsViewV1 {
facts: &self.facts,
source_skeleton: &self.document.assets.source_skeleton,
}
}
pub const fn dependency_closure(&self) -> &DependencyClosureV1 {
&self.dependency_closure
}
pub fn into_document(self) -> Document {
self.document
}
}
#[derive(Debug, Clone, Copy)]
pub struct SourceFactsViewV1<'a> {
facts: &'a RawSourceFactsV1,
source_skeleton: &'a SourceSkeletonAssets,
}
impl<'a> SourceFactsViewV1<'a> {
pub const fn contract_id(self) -> &'static str {
RAW_SOURCE_FACTS_V1_ID
}
pub const fn format(self) -> SourceFormatV1 {
self.facts.format
}
pub const fn primary_identity(self) -> &'a InputIdentity {
&self.facts.primary_identity
}
pub const fn linear_unit(self) -> &'a SourceObservationV1<SourceLinearUnitV1> {
&self.facts.linear_unit
}
pub const fn coordinate_basis(self) -> &'a SourceObservationV1<SourceCoordinateBasisV1> {
&self.facts.coordinate_basis
}
pub const fn frames_per_second(self) -> &'a SourceObservationV1<SourceFramesPerSecondV1> {
&self.facts.frames_per_second
}
pub const fn clips(self) -> &'a SourceFactSetV1<SourceClipFactV1> {
&self.facts.clips
}
pub const fn constructs(self) -> &'a SourceFactSetV1<SourceConstructFactV1> {
&self.facts.constructs
}
pub const fn resources(self) -> &'a SourceFactSetV1<SourceResourceReferenceV1> {
&self.facts.resources
}
pub const fn source_skeleton(self) -> &'a SourceSkeletonAssets {
self.source_skeleton
}
pub const fn work(self) -> SourceProjectionWorkV1 {
self.facts.work
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum SourceFactsError {
#[error("source text is {bytes} bytes, exceeding the V1 limit of {limit}")]
TextTooLong {
bytes: usize,
limit: usize,
},
#[error("source logical locator is invalid or unsafe")]
InvalidLogicalLocator,
#[error("source coordinate basis must use each unsigned axis exactly once")]
DuplicateBasisAxis,
#[error("metres per source unit must be finite and positive")]
InvalidLinearUnit,
#[error("source frames per second must be finite and positive")]
InvalidFramesPerSecond,
#[error("source time range endpoints must be finite with begin <= end")]
InvalidTimeRange,
#[error("source construct occurrence count must be positive")]
ZeroConstructCount,
#[error("source clip index {actual} is not the expected prefix index {expected}")]
NonCanonicalClipIndex {
expected: usize,
actual: usize,
},
#[error(
"source channel index {actual} is not expected prefix index {expected} in clip {source_clip_index}"
)]
NonCanonicalChannelIndex {
source_clip_index: usize,
expected: usize,
actual: usize,
},
#[error("source construct order {actual} is not expected prefix index {expected}")]
NonCanonicalConstructOrder {
expected: usize,
actual: usize,
},
#[error("source resource order {actual} is not expected prefix index {expected}")]
NonCanonicalResourceOrder {
expected: usize,
actual: usize,
},
#[error("normalized clip index {index} is outside document clip count {clip_count}")]
NormalizedClipIndexOutOfRange {
index: usize,
clip_count: usize,
},
#[error(transparent)]
DependencyClosure(#[from] DependencyClosureError),
}
#[cfg(test)]
mod tests {
use super::*;
fn format_provenance() -> SourceProvenanceV1 {
SourceProvenanceV1::format_defined()
}
fn unavailable<T>() -> SourceObservationV1<T> {
SourceObservationV1::unavailable(
SourceUnavailableReasonV1::ParserUnavailable,
None,
SourceLoaderDispositionV1::Unknown,
)
}
fn clip(index: usize) -> SourceClipFactV1 {
SourceClipFactV1::new(
index,
SourceObservationV1::proven_absent(format_provenance()),
unavailable(),
SourceObservationV1::proven_absent(format_provenance()),
SourceObservationV1::proven_absent(format_provenance()),
SourceFactSetV1::complete(Vec::new()),
)
}
fn construct(index: usize, name: String) -> SourceConstructFactV1 {
SourceConstructFactV1::new(
index,
SourceConstructKindV1::Extension,
SourceTextV1::new(name).expect("bounded name"),
false,
1,
SourceLoaderDispositionV1::Unsupported,
format_provenance(),
)
.expect("positive construct count")
}
fn resource(index: usize) -> SourceResourceReferenceV1 {
SourceResourceReferenceV1::new(
index,
SourceResourceKindV1::Image,
index as u64,
SourceResourceLocatorV1::Embedded,
SourceLoaderDispositionV1::Preserved,
format_provenance(),
)
}
fn named_channel(index: usize, name: &str) -> SourceChannelFactV1 {
SourceChannelFactV1::new(
index,
SourceTargetV1::new(SourceTargetKindV1::Node, 0),
SourceChannelPropertyV1::Other,
SourceComponentMaskV1::new(true, false, false),
SourceObservationV1::proven_absent(format_provenance()),
SourceLoaderDispositionV1::Unsupported,
format_provenance(),
)
.with_property_name(SourceTextV1::new(name).expect("bounded property name"))
}
#[test]
fn scalar_value_types_reject_non_finite_and_contradictory_values() {
for value in [0.0, -1.0, f64::NAN, f64::INFINITY] {
assert_eq!(
SourceLinearUnitV1::new(value),
Err(SourceFactsError::InvalidLinearUnit)
);
assert_eq!(
SourceFramesPerSecondV1::new(value),
Err(SourceFactsError::InvalidFramesPerSecond)
);
}
assert_eq!(
SourceTimeRangeV1::new(2.0, 1.0),
Err(SourceFactsError::InvalidTimeRange)
);
assert_eq!(
SourceTimeRangeV1::new(f64::NAN, 1.0),
Err(SourceFactsError::InvalidTimeRange)
);
assert_eq!(
SourceCoordinateBasisV1::new(
SourceAxisV1::PositiveX,
SourceAxisV1::NegativeX,
SourceAxisV1::PositiveZ,
),
Err(SourceFactsError::DuplicateBasisAxis)
);
let range = SourceTimeRangeV1::new(1.0, 1.0).expect("zero duration is evidence");
assert_eq!(range.begin_s(), range.end_s());
let basis = SourceCoordinateBasisV1::new(
SourceAxisV1::PositiveX,
SourceAxisV1::PositiveY,
SourceAxisV1::PositiveZ,
)
.expect("orthogonal basis");
assert_eq!(basis.handedness(), SourceHandednessV1::Right);
assert_eq!(
SourceConstructFactV1::new(
0,
SourceConstructKindV1::Extension,
SourceTextV1::new("EXT_zero").expect("bounded name"),
false,
0,
SourceLoaderDispositionV1::Unsupported,
format_provenance(),
),
Err(SourceFactsError::ZeroConstructCount)
);
}
#[test]
fn partial_empty_sets_never_prove_absence() {
let complete = SourceFactSetV1::<SourceConstructFactV1>::complete(Vec::new());
let partial = SourceFactSetV1::<SourceConstructFactV1>::partial(
Vec::new(),
SourceUnavailableReasonV1::ProjectionBudgetExceeded,
);
let unavailable = SourceFactSetV1::<SourceConstructFactV1>::unavailable(
SourceUnavailableReasonV1::LoaderUnsupported,
);
assert!(complete.proves_absence());
assert!(!partial.proves_absence());
assert!(!unavailable.proves_absence());
}
#[test]
fn builder_requires_explicit_complete_coverage_to_prove_absence() {
let builder = RawSourceFactsBuilderV1::new(
SourceFormatV1::GltfJson,
InputIdentity::from_bytes(b"untouched"),
);
let loaded = builder
.finish(Document::default())
.expect("unavailable defaults");
let facts = loaded.source_facts();
assert!(!facts.clips().proves_absence());
assert!(!facts.constructs().proves_absence());
assert!(!facts.resources().proves_absence());
assert_eq!(
loaded.dependency_closure().coverage().reasons(),
&[
crate::DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable,
crate::DependencyClosureCoverageReasonV1::CaptureUnavailable,
]
);
let mut builder = RawSourceFactsBuilderV1::new(
SourceFormatV1::GltfJson,
InputIdentity::from_bytes(b"exhaustive"),
);
builder.mark_complete(SourceFactDomainV1::Clips);
builder.mark_complete(SourceFactDomainV1::Constructs);
builder.mark_complete(SourceFactDomainV1::Resources);
let loaded = builder
.finish(Document::default())
.expect("complete empty domains");
let facts = loaded.source_facts();
assert!(facts.clips().proves_absence());
assert!(facts.constructs().proves_absence());
assert!(facts.resources().proves_absence());
assert_eq!(
loaded.dependency_closure().coverage().reasons(),
&[crate::DependencyClosureCoverageReasonV1::CaptureUnavailable]
);
}
#[test]
fn loaded_source_accepts_a_complete_format_bound_dependency_closure() {
let primary = InputIdentity::from_bytes(b"gltf");
let mut facts = RawSourceFactsBuilderV1::new(SourceFormatV1::GltfJson, primary.clone());
assert!(facts.push_resource(SourceResourceReferenceV1::new(
0,
SourceResourceKindV1::Buffer,
0,
SourceResourceLocatorV1::classify("buffers/a%20b.bin"),
SourceLoaderDispositionV1::Preserved,
format_provenance(),
)));
facts.mark_complete(SourceFactDomainV1::Resources);
let key = crate::DependencyResourceKeyV1::from_source_str(
"buffers/a b.bin",
crate::ResourceKeySyntaxV1::GltfUri,
)
.unwrap();
let mut closure = crate::DependencyClosureBuilderV1::new(
primary.clone(),
facts.resource_coverage(),
facts.resource_rows().len(),
);
assert!(closure.begin_reference(17, 2));
assert_eq!(closure.prepare_external_key(&key).unwrap(), Some(true));
closure.record_external_open_attempt(&key).unwrap();
assert!(
closure
.push_captured_external(
0,
SourceResourceKindV1::Buffer,
0,
key,
InputIdentity::from_bytes(b"buffer"),
)
.unwrap()
);
let loaded = facts
.finish_with_dependency_closure(Document::default(), closure.finish().unwrap())
.unwrap();
assert!(loaded.dependency_closure().coverage().is_complete());
assert!(loaded.dependency_closure().identity().is_some());
let document = loaded.into_document();
assert!(document.clips.is_empty());
}
#[test]
fn nested_partial_channels_do_not_weaken_complete_clip_identity_coverage() {
let mut builder = RawSourceFactsBuilderV1::new(
SourceFormatV1::Fbx,
InputIdentity::from_bytes(b"nested-partial"),
);
assert!(builder.push_clip(SourceClipFactV1::new(
0,
SourceObservationV1::proven_absent(format_provenance()),
unavailable(),
SourceObservationV1::proven_absent(format_provenance()),
SourceObservationV1::proven_absent(format_provenance()),
SourceFactSetV1::partial(
vec![named_channel(0, "property")],
SourceUnavailableReasonV1::ParserUnavailable,
),
)));
builder.mark_complete(SourceFactDomainV1::Clips);
let loaded = builder
.finish(Document::default())
.expect("independent coverage");
let facts = loaded.source_facts();
assert_eq!(facts.clips().coverage(), SourceSetCoverageV1::complete());
assert_eq!(
facts.clips().rows()[0].channels().coverage(),
SourceSetCoverageV1::partial(SourceUnavailableReasonV1::ParserUnavailable)
);
}
#[test]
fn resource_locator_classification_redacts_unsafe_spelling() {
let secret = "/home/example/private.bin";
for classified in [
SourceResourceLocatorV1::classify(secret),
SourceResourceLocatorV1::classify("https://example.invalid/a.bin"),
SourceResourceLocatorV1::classify("../escape.bin"),
SourceResourceLocatorV1::classify("a/%2e%2e/escape.bin"),
SourceResourceLocatorV1::classify("a/%2f/escape.bin"),
SourceResourceLocatorV1::classify("bad%q0.bin"),
] {
assert!(!format!("{classified:?}").contains(secret));
assert!(!matches!(classified, SourceResourceLocatorV1::Relative(_)));
}
let control_bearing = "textures/TOP_SECRET\nname.png";
let classified = SourceResourceLocatorV1::classify(control_bearing);
assert_eq!(classified, SourceResourceLocatorV1::Malformed);
assert!(!format!("{classified:?}").contains("TOP_SECRET"));
assert_eq!(
SourceResourceLocatorV1::retained_relative_bytes(control_bearing),
0
);
let SourceResourceLocatorV1::Relative(relative) =
SourceResourceLocatorV1::classify("textures/normal.png")
else {
panic!("safe relative declaration retained");
};
assert_eq!(relative.as_str(), "textures/normal.png");
assert_eq!(
SourceResourceLocatorV1::retained_relative_bytes("textures/normal.png"),
"textures/normal.png".len()
);
assert_eq!(
SourceResourceLocatorV1::retained_relative_bytes("../private.bin"),
0
);
assert_eq!(
SourceResourceLocatorV1::classify("data:image/png;base64,private"),
SourceResourceLocatorV1::DataUri
);
assert_eq!(
SourceResourceLocatorV1::classify("DATA:image/png;base64,private"),
SourceResourceLocatorV1::DataUri
);
let oversized_data_uri = format!(
"data:application/octet-stream;base64,{}",
"A".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES)
);
assert_eq!(
SourceResourceLocatorV1::classify(&oversized_data_uri),
SourceResourceLocatorV1::DataUri
);
assert_eq!(
SourceResourceLocatorV1::retained_relative_bytes(&oversized_data_uri),
0
);
assert_eq!(
SourceResourceLocatorV1::classify(r"C:\private\texture.png"),
SourceResourceLocatorV1::Absolute
);
assert_eq!(
SourceResourceLocatorV1::classify("file:///private/texture.png"),
SourceResourceLocatorV1::Absolute
);
}
#[test]
fn provenance_uses_only_validated_source_logical_locators() {
for value in [
"/home/private/input.glb",
"/../animations",
"https://host/path",
] {
assert_eq!(
SourceLogicalLocatorV1::gltf_json_pointer(value),
Err(SourceFactsError::InvalidLogicalLocator)
);
}
for value in [
"/home/private/input.fbx",
"fbx:/home/private",
"fbx:../private",
] {
assert_eq!(
SourceLogicalLocatorV1::fbx_parser_path(value),
Err(SourceFactsError::InvalidLogicalLocator)
);
}
let pointer = SourceLogicalLocatorV1::gltf_json_pointer("/animations/0/channels/1")
.expect("generated glTF pointer");
let provenance = SourceProvenanceV1::source_declared(pointer);
assert_eq!(provenance.kind(), SourceProvenanceKindV1::SourceDeclared);
assert_eq!(
provenance.locator().map(SourceLogicalLocatorV1::as_str),
Some("/animations/0/channels/1")
);
assert!(!format!("{provenance:?}").contains("animations"));
let path = SourceLogicalLocatorV1::fbx_parser_path("fbx:scene.settings.axes")
.expect("generated FBX parser path");
assert_eq!(
SourceProvenanceV1::parser_projected(path).kind(),
SourceProvenanceKindV1::ParserProjected
);
assert!(SourceProvenanceV1::format_defined().locator().is_none());
}
#[test]
fn loaded_source_binds_exact_identity_and_canonical_source_skeleton() {
let bytes = b"same bytes parsed by the loader";
let identity = InputIdentity::from_bytes(bytes);
let mut document = Document::default();
document.source.path = Some("/home/example/private/input.glb".into());
let mut builder = RawSourceFactsBuilderV1::new(SourceFormatV1::Glb, identity.clone());
builder.set_linear_unit(SourceObservationV1::observed(
SourceLinearUnitV1::new(1.0).expect("metres"),
format_provenance(),
SourceLoaderDispositionV1::Preserved,
));
let loaded = builder.finish(document).expect("facts bind");
let source_skeleton_ptr = &loaded.document().assets.source_skeleton as *const _;
let facts = loaded.source_facts();
assert_eq!(facts.contract_id(), RAW_SOURCE_FACTS_V1_ID);
assert_eq!(facts.format(), SourceFormatV1::Glb);
assert_eq!(facts.primary_identity(), &identity);
assert_eq!(facts.primary_identity().bytes(), bytes.len() as u64);
assert!(std::ptr::eq(
facts.source_skeleton() as *const _,
source_skeleton_ptr
));
assert_eq!(loaded.dependency_closure().primary_input(), &identity);
assert!(matches!(
loaded.dependency_closure().coverage(),
crate::DependencyClosureCoverageV1::Unavailable { .. }
));
assert!(loaded.dependency_closure().identity().is_none());
assert!(!format!("{loaded:?}").contains("/home/example/private"));
let document = loaded.into_document();
assert!(document.clips.is_empty());
}
#[test]
fn clip_limit_retains_n_then_marks_n_plus_one_partial() {
let mut builder =
RawSourceFactsBuilderV1::new(SourceFormatV1::Fbx, InputIdentity::from_bytes(b"fbx"));
for index in 0..RAW_SOURCE_V1_MAX_CLIPS {
assert!(builder.push_clip(clip(index)));
}
assert!(!builder.push_clip(clip(RAW_SOURCE_V1_MAX_CLIPS)));
assert!(!builder.push_clip(clip(RAW_SOURCE_V1_MAX_CLIPS + 1)));
let loaded = builder.finish(Document::default()).expect("bounded facts");
let facts = loaded.source_facts();
assert_eq!(facts.clips().rows().len(), RAW_SOURCE_V1_MAX_CLIPS);
assert_eq!(
facts.clips().coverage(),
SourceSetCoverageV1::partial(SourceUnavailableReasonV1::ProjectionBudgetExceeded)
);
assert_eq!(facts.work().inspected_rows(), RAW_SOURCE_V1_MAX_CLIPS + 1);
}
#[test]
fn resource_limit_retains_n_then_marks_n_plus_one_partial() {
let mut builder = RawSourceFactsBuilderV1::new(
SourceFormatV1::GltfJson,
InputIdentity::from_bytes(b"gltf"),
);
for index in 0..RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES {
assert!(builder.push_resource(resource(index)));
}
assert!(!builder.push_resource(resource(RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES)));
assert!(!builder.push_resource(resource(RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES + 1)));
let loaded = builder.finish(Document::default()).expect("bounded facts");
let facts = loaded.source_facts();
assert_eq!(
facts.resources().rows().len(),
RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES
);
assert_eq!(
facts.resources().coverage().state(),
SourceSetCoverageStateV1::Partial
);
assert_eq!(
facts.work().inspected_rows(),
RAW_SOURCE_V1_MAX_RESOURCE_REFERENCES + 1
);
}
#[test]
fn observed_clip_names_count_toward_the_aggregate_text_limit() {
let rows_at_limit = RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES / RAW_SOURCE_V1_MAX_TEXT_BYTES;
let mut named_clips = RawSourceFactsBuilderV1::new(
SourceFormatV1::GltfJson,
InputIdentity::from_bytes(b"named-clips"),
);
for index in 0..rows_at_limit {
let source_name = SourceObservationV1::observed(
SourceTextV1::new("n".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES)).expect("bounded name"),
format_provenance(),
SourceLoaderDispositionV1::Preserved,
);
assert!(named_clips.push_clip(SourceClipFactV1::new(
index,
source_name,
SourceObservationV1::proven_absent(format_provenance()),
SourceObservationV1::proven_absent(format_provenance()),
SourceObservationV1::proven_absent(format_provenance()),
SourceFactSetV1::complete(Vec::new()),
)));
}
let overflow_name = SourceObservationV1::observed(
SourceTextV1::new("n".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES)).expect("bounded name"),
format_provenance(),
SourceLoaderDispositionV1::Preserved,
);
assert!(!named_clips.push_clip(SourceClipFactV1::new(
rows_at_limit,
overflow_name,
SourceObservationV1::proven_absent(format_provenance()),
SourceObservationV1::proven_absent(format_provenance()),
SourceObservationV1::proven_absent(format_provenance()),
SourceFactSetV1::complete(Vec::new()),
)));
let loaded = named_clips
.finish(Document::default())
.expect("bounded named clips");
assert_eq!(
loaded.source_facts().work().retained_text_bytes(),
RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES
);
assert_eq!(
loaded.source_facts().clips().coverage().state(),
SourceSetCoverageStateV1::Partial
);
}
#[test]
fn aggregate_text_limit_retains_nested_channel_prefix() {
let mut builder = RawSourceFactsBuilderV1::new(
SourceFormatV1::GltfJson,
InputIdentity::from_bytes(b"channel-prefix"),
);
let full_rows = RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES / RAW_SOURCE_V1_MAX_TEXT_BYTES;
for _ in 0..full_rows - 1 {
assert!(builder.push_construct(construct(
builder.facts.constructs.rows.len(),
"x".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES)
)));
}
assert!(builder.push_construct(construct(
builder.facts.constructs.rows.len(),
"x".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES - 12)
)));
assert_eq!(builder.remaining_text_bytes(), 12);
let channels = SourceFactSetV1::complete(vec![
named_channel(0, "aaaa"),
named_channel(1, "bbbb"),
named_channel(2, "cccc"),
]);
assert!(builder.push_clip(SourceClipFactV1::new(
0,
SourceObservationV1::observed(
SourceTextV1::new("name").expect("bounded name"),
format_provenance(),
SourceLoaderDispositionV1::Preserved,
),
SourceObservationV1::proven_absent(format_provenance()),
SourceObservationV1::proven_absent(format_provenance()),
SourceObservationV1::proven_absent(format_provenance()),
channels,
)));
let loaded = builder.finish(Document::default()).expect("bounded prefix");
let facts = loaded.source_facts();
assert_eq!(
facts.work().retained_text_bytes(),
RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES
);
assert_eq!(facts.clips().rows().len(), 1);
assert_eq!(facts.clips().rows()[0].channels().rows().len(), 2);
assert_eq!(
facts.clips().rows()[0].channels().coverage().state(),
SourceSetCoverageStateV1::Partial
);
assert_eq!(
facts.clips().coverage().state(),
SourceSetCoverageStateV1::Partial
);
}
#[test]
fn total_observation_limit_retains_exact_prefix_and_work_count() {
let mut builder = RawSourceFactsBuilderV1::new(
SourceFormatV1::GltfJson,
InputIdentity::from_bytes(b"gltf"),
);
for index in 0..=RAW_SOURCE_V1_MAX_OBSERVATIONS {
let retained = builder.push_construct(construct(index, format!("e{index}")));
assert_eq!(retained, index < RAW_SOURCE_V1_MAX_OBSERVATIONS);
}
assert!(!builder.push_construct(construct(
RAW_SOURCE_V1_MAX_OBSERVATIONS + 1,
"must-not-resume".to_string()
)));
let loaded = builder.finish(Document::default()).expect("bounded facts");
let facts = loaded.source_facts();
assert_eq!(
facts.constructs().rows().len(),
RAW_SOURCE_V1_MAX_OBSERVATIONS
);
assert_eq!(
facts.constructs().coverage(),
SourceSetCoverageV1::partial(SourceUnavailableReasonV1::ProjectionBudgetExceeded)
);
assert_eq!(
facts.work().inspected_rows(),
RAW_SOURCE_V1_MAX_OBSERVATIONS + 1
);
assert_eq!(facts.work().retained_rows(), RAW_SOURCE_V1_MAX_OBSERVATIONS);
}
#[test]
fn unavailable_domain_discards_prefix_and_updates_retained_work() {
let mut builder = RawSourceFactsBuilderV1::new(
SourceFormatV1::GltfJson,
InputIdentity::from_bytes(b"discarded-prefix"),
);
assert!(builder.push_construct(construct(0, "retained-name".to_string())));
assert_eq!(
builder.remaining_observation_rows(),
RAW_SOURCE_V1_MAX_OBSERVATIONS - 1
);
assert_eq!(
builder.remaining_text_bytes(),
RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES - 13
);
builder.mark_unavailable(
SourceFactDomainV1::Constructs,
SourceUnavailableReasonV1::ParserUnavailable,
);
builder.mark_partial(
SourceFactDomainV1::Constructs,
SourceUnavailableReasonV1::ProjectionBudgetExceeded,
);
let loaded = builder
.finish(Document::default())
.expect("unavailable set remains valid");
let facts = loaded.source_facts();
assert!(facts.constructs().rows().is_empty());
assert_eq!(
facts.constructs().coverage(),
SourceSetCoverageV1::unavailable(SourceUnavailableReasonV1::ParserUnavailable)
);
assert_eq!(facts.work().retained_rows(), 0);
assert_eq!(facts.work().retained_text_bytes(), 0);
assert_eq!(facts.work().inspected_rows(), 1);
}
#[test]
fn preallocation_budget_stop_counts_terminal_row() {
let mut builder = RawSourceFactsBuilderV1::new(
SourceFormatV1::GltfJson,
InputIdentity::from_bytes(b"preallocation-stop"),
);
assert!(builder.push_construct(construct(0, "first".to_string())));
builder.mark_budget_exceeded(SourceFactDomainV1::Constructs);
assert!(!builder.push_construct(construct(1, "must-not-resume".to_string())));
let loaded = builder.finish(Document::default()).expect("partial prefix");
let facts = loaded.source_facts();
assert_eq!(facts.work().inspected_rows(), 2);
assert_eq!(facts.work().retained_rows(), 1);
assert_eq!(facts.constructs().rows()[0].name().as_str(), "first");
assert_eq!(
facts.constructs().coverage(),
SourceSetCoverageV1::partial(SourceUnavailableReasonV1::ProjectionBudgetExceeded)
);
}
#[test]
fn text_and_traversal_limits_are_exact_and_coverage_qualified() {
assert!(SourceTextV1::new("x".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES)).is_ok());
assert_eq!(
SourceTextV1::new("x".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES + 1)),
Err(SourceFactsError::TextTooLong {
bytes: RAW_SOURCE_V1_MAX_TEXT_BYTES + 1,
limit: RAW_SOURCE_V1_MAX_TEXT_BYTES,
})
);
let mut builder = RawSourceFactsBuilderV1::new(
SourceFormatV1::GltfJson,
InputIdentity::from_bytes(b"gltf"),
);
let rows_at_limit = RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES / RAW_SOURCE_V1_MAX_TEXT_BYTES;
for index in 0..rows_at_limit {
assert!(
builder.push_construct(construct(index, "x".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES)))
);
}
assert!(!builder.push_construct(construct(
rows_at_limit,
"x".repeat(RAW_SOURCE_V1_MAX_TEXT_BYTES)
)));
assert!(!builder.push_construct(construct(rows_at_limit + 1, "short".to_string())));
assert!(
!builder.set_linear_unit(SourceObservationV1::observed(
SourceLinearUnitV1::new(1.0).expect("metres"),
SourceProvenanceV1::source_declared(
SourceLogicalLocatorV1::gltf_json_pointer("/animations")
.expect("generated logical locator"),
),
SourceLoaderDispositionV1::Preserved,
))
);
assert!(builder.observe_traversal_depth(
SourceFactDomainV1::Resources,
RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH
));
assert!(!builder.observe_traversal_depth(
SourceFactDomainV1::Resources,
RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH + 1
));
assert!(!builder.push_resource(resource(0)));
let loaded = builder.finish(Document::default()).expect("bounded facts");
let facts = loaded.source_facts();
assert_eq!(
facts.work().retained_text_bytes(),
RAW_SOURCE_V1_MAX_TOTAL_TEXT_BYTES
);
assert!(matches!(
facts.linear_unit().state(),
SourceObservationStateV1::Unavailable(
SourceUnavailableReasonV1::ProjectionBudgetExceeded
)
));
assert_eq!(
facts.work().max_traversal_depth(),
RAW_SOURCE_V1_MAX_TRAVERSAL_DEPTH + 1
);
assert_eq!(
facts.resources().coverage().state(),
SourceSetCoverageStateV1::Partial
);
}
#[test]
fn finish_rejects_stale_normalized_clip_mappings_and_noncanonical_order() {
let observed_index = |index| {
SourceObservationV1::observed(
index,
format_provenance(),
SourceLoaderDispositionV1::Preserved,
)
};
let make = |source_index, normalized_index| {
SourceClipFactV1::new(
source_index,
SourceObservationV1::proven_absent(format_provenance()),
observed_index(normalized_index),
SourceObservationV1::proven_absent(format_provenance()),
SourceObservationV1::proven_absent(format_provenance()),
SourceFactSetV1::complete(Vec::new()),
)
};
let mut stale =
RawSourceFactsBuilderV1::new(SourceFormatV1::Fbx, InputIdentity::from_bytes(b"fbx"));
assert!(stale.push_clip(make(0, 0)));
assert!(matches!(
stale.finish(Document::default()),
Err(SourceFactsError::NormalizedClipIndexOutOfRange { .. })
));
let mut unordered =
RawSourceFactsBuilderV1::new(SourceFormatV1::Fbx, InputIdentity::from_bytes(b"fbx"));
assert!(unordered.push_clip(clip(1)));
assert!(unordered.push_clip(clip(0)));
assert!(matches!(
unordered.finish(Document::default()),
Err(SourceFactsError::NonCanonicalClipIndex { .. })
));
let mut channel_gap =
RawSourceFactsBuilderV1::new(SourceFormatV1::Fbx, InputIdentity::from_bytes(b"fbx"));
assert!(channel_gap.push_clip(SourceClipFactV1::new(
0,
SourceObservationV1::proven_absent(format_provenance()),
unavailable(),
SourceObservationV1::proven_absent(format_provenance()),
SourceObservationV1::proven_absent(format_provenance()),
SourceFactSetV1::partial(
vec![named_channel(1, "gap")],
SourceUnavailableReasonV1::ParserUnavailable,
),
)));
assert!(matches!(
channel_gap.finish(Document::default()),
Err(SourceFactsError::NonCanonicalChannelIndex { .. })
));
let mut construct_gap = RawSourceFactsBuilderV1::new(
SourceFormatV1::GltfJson,
InputIdentity::from_bytes(b"gltf"),
);
assert!(construct_gap.push_construct(construct(1, "gap".to_string())));
assert!(matches!(
construct_gap.finish(Document::default()),
Err(SourceFactsError::NonCanonicalConstructOrder { .. })
));
let mut resource_gap = RawSourceFactsBuilderV1::new(
SourceFormatV1::GltfJson,
InputIdentity::from_bytes(b"gltf"),
);
assert!(resource_gap.push_resource(resource(1)));
assert!(matches!(
resource_gap.finish(Document::default()),
Err(SourceFactsError::NonCanonicalResourceOrder { .. })
));
}
}