use crate::bounded_deserialize::{CappedSequence, deserialize_capped_sequence};
use crate::{
InputIdentity, SourceFactSetV1, SourceFormatV1, SourceRelativeLocatorV1, SourceResourceKindV1,
SourceResourceLocatorV1, SourceResourceReferenceV1, SourceSetCoverageStateV1,
SourceSetCoverageV1,
};
use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::{BTreeMap, BTreeSet};
pub const DEPENDENCY_CLOSURE_V1_ID: &str = "urn:animsmith:dependency-closure:1";
pub const DEPENDENCY_CLOSURE_BUDGET_V1_ID: &str = "urn:animsmith:dependency-closure-budget:1";
pub const DEPENDENCY_CLOSURE_V1_MAX_REFERENCES: usize = 4_096;
pub const DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES: usize = 1_024;
pub const DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES: usize = 4_096;
pub const DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS: usize = 128;
pub const DEPENDENCY_CLOSURE_V1_MAX_NORMALIZATION_BYTES: usize = 8 * 1024 * 1024;
pub const DEPENDENCY_CLOSURE_V1_MAX_RESOURCE_BYTES: u64 = 64 * 1024 * 1024;
pub const DEPENDENCY_CLOSURE_V1_MAX_TOTAL_RESOURCE_BYTES: u64 = 256 * 1024 * 1024;
pub const DEPENDENCY_CLOSURE_V1_MAX_DEDUP_PROBES: usize = 4_096;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct ResourceClosureBudgetV1 {
schema: &'static str,
max_references: usize,
max_external_resources: usize,
max_key_bytes: usize,
max_path_components: usize,
max_normalization_bytes: usize,
max_resource_bytes: u64,
max_total_resource_bytes: u64,
max_dedup_probes: usize,
}
impl<'de> Deserialize<'de> for ResourceClosureBudgetV1 {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct WireBudget {
schema: String,
max_references: usize,
max_external_resources: usize,
max_key_bytes: usize,
max_path_components: usize,
max_normalization_bytes: usize,
max_resource_bytes: u64,
max_total_resource_bytes: u64,
max_dedup_probes: usize,
}
let wire = WireBudget::deserialize(deserializer)?;
let expected = Self::VALUE;
if wire.schema != expected.schema
|| wire.max_references != expected.max_references
|| wire.max_external_resources != expected.max_external_resources
|| wire.max_key_bytes != expected.max_key_bytes
|| wire.max_path_components != expected.max_path_components
|| wire.max_normalization_bytes != expected.max_normalization_bytes
|| wire.max_resource_bytes != expected.max_resource_bytes
|| wire.max_total_resource_bytes != expected.max_total_resource_bytes
|| wire.max_dedup_probes != expected.max_dedup_probes
{
return Err(D::Error::custom(
"dependency-closure budget must equal immutable V1",
));
}
Ok(expected)
}
}
impl ResourceClosureBudgetV1 {
pub const VALUE: Self = Self {
schema: DEPENDENCY_CLOSURE_BUDGET_V1_ID,
max_references: DEPENDENCY_CLOSURE_V1_MAX_REFERENCES,
max_external_resources: DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES,
max_key_bytes: DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES,
max_path_components: DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS,
max_normalization_bytes: DEPENDENCY_CLOSURE_V1_MAX_NORMALIZATION_BYTES,
max_resource_bytes: DEPENDENCY_CLOSURE_V1_MAX_RESOURCE_BYTES,
max_total_resource_bytes: DEPENDENCY_CLOSURE_V1_MAX_TOTAL_RESOURCE_BYTES,
max_dedup_probes: DEPENDENCY_CLOSURE_V1_MAX_DEDUP_PROBES,
};
pub const fn contract_id(self) -> &'static str {
DEPENDENCY_CLOSURE_BUDGET_V1_ID
}
pub const fn max_references(self) -> usize {
self.max_references
}
pub const fn max_external_resources(self) -> usize {
self.max_external_resources
}
pub const fn max_key_bytes(self) -> usize {
self.max_key_bytes
}
pub const fn max_path_components(self) -> usize {
self.max_path_components
}
pub const fn max_normalization_bytes(self) -> usize {
self.max_normalization_bytes
}
pub const fn max_resource_bytes(self) -> u64 {
self.max_resource_bytes
}
pub const fn max_total_resource_bytes(self) -> u64 {
self.max_total_resource_bytes
}
pub const fn max_dedup_probes(self) -> usize {
self.max_dedup_probes
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DependencyResourcePurposeV1 {
LoaderEssential,
Nonessential,
TargetOnly,
}
impl DependencyResourcePurposeV1 {
const fn from_kind(kind: SourceResourceKindV1) -> Self {
match kind {
SourceResourceKindV1::Buffer => Self::LoaderEssential,
SourceResourceKindV1::Image | SourceResourceKindV1::Texture => Self::Nonessential,
SourceResourceKindV1::Video | SourceResourceKindV1::Cache => Self::TargetOnly,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResourceKeySyntaxV1 {
GltfUri,
ParserRelativePath,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(transparent)]
pub struct DependencyResourceKeyV1(String);
impl DependencyResourceKeyV1 {
pub fn from_relative(
locator: &SourceRelativeLocatorV1,
syntax: ResourceKeySyntaxV1,
) -> Result<Self, DependencyClosureError> {
Self::from_source_str(locator.as_str(), syntax)
}
pub fn from_source_str(
raw: &str,
syntax: ResourceKeySyntaxV1,
) -> Result<Self, DependencyClosureError> {
if raw.len() > DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES {
return Err(DependencyClosureError::ResourceKeyTooLong {
bytes: raw.len(),
limit: DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES,
});
}
if raw.contains('\\') || raw.chars().any(char::is_control) {
return Err(DependencyClosureError::InvalidResourceKey);
}
let normalized = match syntax {
ResourceKeySyntaxV1::GltfUri => decode_percent_utf8(raw)?,
ResourceKeySyntaxV1::ParserRelativePath => raw.to_owned(),
};
validate_normalized_key(&normalized)?;
Ok(Self(normalized))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn source_component_count(locator: &SourceRelativeLocatorV1) -> usize {
locator.as_str().split('/').count()
}
}
impl<'de> Deserialize<'de> for DependencyResourceKeyV1 {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Self::from_source_str(
&String::deserialize(deserializer)?,
ResourceKeySyntaxV1::ParserRelativePath,
)
.map_err(D::Error::custom)
}
}
fn decode_percent_utf8(raw: &str) -> Result<String, DependencyClosureError> {
let bytes = raw.as_bytes();
let mut decoded = Vec::with_capacity(bytes.len());
let mut index = 0usize;
while index < bytes.len() {
if bytes[index] != b'%' {
decoded.push(bytes[index]);
index += 1;
continue;
}
let high = bytes
.get(index + 1)
.copied()
.and_then(hex)
.ok_or(DependencyClosureError::InvalidResourceKey)?;
let low = bytes
.get(index + 2)
.copied()
.and_then(hex)
.ok_or(DependencyClosureError::InvalidResourceKey)?;
let value = (high << 4) | low;
if matches!(value, b'/' | b'\\' | 0) {
return Err(DependencyClosureError::InvalidResourceKey);
}
decoded.push(value);
index += 3;
}
String::from_utf8(decoded).map_err(|_| DependencyClosureError::InvalidResourceKey)
}
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 validate_normalized_key(value: &str) -> Result<(), DependencyClosureError> {
if value.is_empty()
|| value.len() > DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES
|| value.starts_with('/')
|| value.contains('\\')
|| value.contains([':', '?', '#'])
|| value.chars().any(char::is_control)
|| has_uri_scheme(value)
{
return Err(DependencyClosureError::InvalidResourceKey);
}
let mut components = 0usize;
for component in value.split('/') {
components = components.saturating_add(1);
if component.is_empty() || matches!(component, "." | "..") {
return Err(DependencyClosureError::InvalidResourceKey);
}
}
if components > DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS {
return Err(DependencyClosureError::TooManyPathComponents {
components,
limit: DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS,
});
}
Ok(())
}
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(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.'))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DependencyResourceRefusalReasonV1 {
Absolute,
Escaping,
Remote,
Malformed,
Oversized,
Symlink,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DependencyResourceUnavailableReasonV1 {
ResourceRootUnavailable,
Missing,
Unreadable,
ResourceBudgetExceeded,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
pub enum DependencyReferenceTargetV1 {
Primary,
External {
key: DependencyResourceKeyV1,
},
Refused {
#[serde(skip_serializing_if = "Option::is_none")]
key: Option<DependencyResourceKeyV1>,
reason: DependencyResourceRefusalReasonV1,
},
Unavailable {
#[serde(skip_serializing_if = "Option::is_none")]
key: Option<DependencyResourceKeyV1>,
reason: DependencyResourceUnavailableReasonV1,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct DependencyClosureReferenceV1 {
source_order_index: usize,
kind: SourceResourceKindV1,
purpose: DependencyResourcePurposeV1,
source_index: u64,
target: DependencyReferenceTargetV1,
}
impl<'de> Deserialize<'de> for DependencyClosureReferenceV1 {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(rename_all = "snake_case")]
enum WireResourceKind {
Buffer,
Image,
Texture,
Video,
Cache,
}
impl From<WireResourceKind> for SourceResourceKindV1 {
fn from(value: WireResourceKind) -> Self {
match value {
WireResourceKind::Buffer => Self::Buffer,
WireResourceKind::Image => Self::Image,
WireResourceKind::Texture => Self::Texture,
WireResourceKind::Video => Self::Video,
WireResourceKind::Cache => Self::Cache,
}
}
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct WireReference {
source_order_index: usize,
kind: WireResourceKind,
purpose: DependencyResourcePurposeV1,
source_index: u64,
target: DependencyReferenceTargetV1,
}
let wire = WireReference::deserialize(deserializer)?;
let kind = SourceResourceKindV1::from(wire.kind);
let reference = Self::new(
wire.source_order_index,
kind,
wire.source_index,
wire.target,
);
if reference.purpose != wire.purpose {
return Err(D::Error::custom(
"dependency reference purpose must be derived from kind",
));
}
Ok(reference)
}
}
impl DependencyClosureReferenceV1 {
fn new(
source_order_index: usize,
kind: SourceResourceKindV1,
source_index: u64,
target: DependencyReferenceTargetV1,
) -> Self {
Self {
source_order_index,
kind,
purpose: DependencyResourcePurposeV1::from_kind(kind),
source_index,
target,
}
}
pub const fn source_order_index(&self) -> usize {
self.source_order_index
}
pub const fn kind(&self) -> SourceResourceKindV1 {
self.kind
}
pub const fn purpose(&self) -> DependencyResourcePurposeV1 {
self.purpose
}
pub const fn source_index(&self) -> u64 {
self.source_index
}
pub const fn target(&self) -> &DependencyReferenceTargetV1 {
&self.target
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExternalResourceIdentityV1 {
key: DependencyResourceKeyV1,
identity: InputIdentity,
}
impl ExternalResourceIdentityV1 {
pub const fn key(&self) -> &DependencyResourceKeyV1 {
&self.key
}
pub const fn identity(&self) -> &InputIdentity {
&self.identity
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DependencyClosureCoverageReasonV1 {
SourceDeclarationsPartial,
SourceDeclarationsUnavailable,
CaptureUnavailable,
RefusedResource,
UnavailableResource,
ResourceBudgetExceeded,
UnmodeledResourceDomain,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
pub enum DependencyClosureCoverageV1 {
Complete,
Partial {
reasons: Vec<DependencyClosureCoverageReasonV1>,
},
Unavailable {
reasons: Vec<DependencyClosureCoverageReasonV1>,
},
}
const DEPENDENCY_CLOSURE_COVERAGE_REASON_VARIANTS: usize = 7;
#[derive(Deserialize)]
#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
enum DependencyClosureCoverageWireV1 {
Complete,
Partial {
#[serde(deserialize_with = "deserialize_closure_coverage_reasons")]
reasons: CappedSequence<DependencyClosureCoverageReasonV1>,
},
Unavailable {
#[serde(deserialize_with = "deserialize_closure_coverage_reasons")]
reasons: CappedSequence<DependencyClosureCoverageReasonV1>,
},
}
impl DependencyClosureCoverageWireV1 {
fn into_value(self) -> (DependencyClosureCoverageV1, bool) {
match self {
Self::Complete => (DependencyClosureCoverageV1::Complete, false),
Self::Partial { reasons } => (
DependencyClosureCoverageV1::Partial {
reasons: reasons.values,
},
reasons.overflowed,
),
Self::Unavailable { reasons } => (
DependencyClosureCoverageV1::Unavailable {
reasons: reasons.values,
},
reasons.overflowed,
),
}
}
}
impl<'de> Deserialize<'de> for DependencyClosureCoverageV1 {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let (coverage, overflowed) =
DependencyClosureCoverageWireV1::deserialize(deserializer)?.into_value();
if overflowed {
return Err(D::Error::custom(
"dependency coverage reasons must be strictly ordered",
));
}
Ok(coverage)
}
}
impl DependencyClosureCoverageV1 {
pub fn reasons(&self) -> &[DependencyClosureCoverageReasonV1] {
match self {
Self::Complete => &[],
Self::Partial { reasons } | Self::Unavailable { reasons } => reasons,
}
}
pub const fn is_complete(&self) -> bool {
matches!(self, Self::Complete)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct DependencyClosureIdentityV1(InputIdentity);
impl DependencyClosureIdentityV1 {
pub const fn input_identity(&self) -> &InputIdentity {
&self.0
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DependencyClosureWorkV1 {
inspected_references: usize,
retained_references: usize,
normalization_bytes_inspected: usize,
path_components_inspected: usize,
dedup_probes: usize,
external_open_attempts: usize,
distinct_external_keys: usize,
captured_external_resources: usize,
external_bytes_read_hashed: u64,
}
impl DependencyClosureWorkV1 {
pub const fn inspected_references(self) -> usize {
self.inspected_references
}
pub const fn retained_references(self) -> usize {
self.retained_references
}
pub const fn normalization_bytes_inspected(self) -> usize {
self.normalization_bytes_inspected
}
pub const fn path_components_inspected(self) -> usize {
self.path_components_inspected
}
pub const fn dedup_probes(self) -> usize {
self.dedup_probes
}
pub const fn external_open_attempts(self) -> usize {
self.external_open_attempts
}
pub const fn distinct_external_keys(self) -> usize {
self.distinct_external_keys
}
pub const fn captured_external_resources(self) -> usize {
self.captured_external_resources
}
pub const fn external_bytes_read_hashed(self) -> u64 {
self.external_bytes_read_hashed
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct DependencyClosureV1 {
schema: &'static str,
budget: ResourceClosureBudgetV1,
primary_input: InputIdentity,
coverage: DependencyClosureCoverageV1,
#[serde(skip_serializing_if = "Option::is_none")]
identity: Option<DependencyClosureIdentityV1>,
references: Vec<DependencyClosureReferenceV1>,
external_resources: Vec<ExternalResourceIdentityV1>,
work: DependencyClosureWorkV1,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct DependencyClosureWireV1 {
schema: String,
budget: ResourceClosureBudgetV1,
primary_input: InputIdentity,
coverage: DependencyClosureCoverageWireV1,
#[serde(default, deserialize_with = "deserialize_optional_non_null")]
identity: OptionalNonNull<DependencyClosureIdentityV1>,
#[serde(deserialize_with = "deserialize_closure_references")]
references: CappedSequence<DependencyClosureReferenceV1>,
#[serde(deserialize_with = "deserialize_closure_external_resources")]
external_resources: CappedSequence<ExternalResourceIdentityV1>,
work: DependencyClosureWorkV1,
}
fn deserialize_closure_references<'de, D>(
deserializer: D,
) -> Result<CappedSequence<DependencyClosureReferenceV1>, D::Error>
where
D: Deserializer<'de>,
{
deserialize_capped_sequence(deserializer, DEPENDENCY_CLOSURE_V1_MAX_REFERENCES)
}
fn deserialize_closure_external_resources<'de, D>(
deserializer: D,
) -> Result<CappedSequence<ExternalResourceIdentityV1>, D::Error>
where
D: Deserializer<'de>,
{
deserialize_capped_sequence(deserializer, DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES)
}
fn deserialize_closure_coverage_reasons<'de, D>(
deserializer: D,
) -> Result<CappedSequence<DependencyClosureCoverageReasonV1>, D::Error>
where
D: Deserializer<'de>,
{
deserialize_capped_sequence(deserializer, DEPENDENCY_CLOSURE_COVERAGE_REASON_VARIANTS)
}
#[derive(Debug, Default)]
enum OptionalNonNull<T> {
#[default]
Missing,
Present(T),
}
fn deserialize_optional_non_null<'de, D, T>(deserializer: D) -> Result<OptionalNonNull<T>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
{
T::deserialize(deserializer).map(OptionalNonNull::Present)
}
#[derive(Debug)]
pub(crate) enum DependencyClosureDecodeError {
Shape(serde_json::Error),
Semantic(String),
}
pub(crate) fn decode_dependency_closure_v1(
raw: &str,
) -> Result<DependencyClosureV1, DependencyClosureDecodeError> {
let wire = serde_json::from_str(raw).map_err(DependencyClosureDecodeError::Shape)?;
DependencyClosureV1::from_wire(wire).map_err(DependencyClosureDecodeError::Semantic)
}
impl<'de> Deserialize<'de> for DependencyClosureV1 {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Self::from_wire(DependencyClosureWireV1::deserialize(deserializer)?)
.map_err(D::Error::custom)
}
}
impl DependencyClosureV1 {
fn from_wire(wire: DependencyClosureWireV1) -> Result<Self, String> {
if wire.schema != DEPENDENCY_CLOSURE_V1_ID {
return Err(format!(
"dependency closure schema must be {DEPENDENCY_CLOSURE_V1_ID:?}"
));
}
if wire.budget != ResourceClosureBudgetV1::VALUE {
return Err("dependency closure budget is not immutable V1".to_owned());
}
if wire.references.overflowed {
return Err("dependency closure has too many references".to_owned());
}
if wire.external_resources.overflowed {
return Err("dependency closure has too many external resources".to_owned());
}
let (coverage, coverage_reasons_overflowed) = wire.coverage.into_value();
let closure = Self {
schema: DEPENDENCY_CLOSURE_V1_ID,
budget: wire.budget,
primary_input: wire.primary_input,
coverage,
identity: match wire.identity {
OptionalNonNull::Missing => None,
OptionalNonNull::Present(identity) => Some(identity),
},
references: wire.references.values,
external_resources: wire.external_resources.values,
work: wire.work,
};
closure.validate_wire(coverage_reasons_overflowed)?;
Ok(closure)
}
pub fn unavailable(primary_input: InputIdentity) -> Self {
Self {
schema: DEPENDENCY_CLOSURE_V1_ID,
budget: ResourceClosureBudgetV1::VALUE,
primary_input,
coverage: DependencyClosureCoverageV1::Unavailable {
reasons: vec![DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable],
},
identity: None,
references: Vec::new(),
external_resources: Vec::new(),
work: DependencyClosureWorkV1::default(),
}
}
pub(crate) fn capture_unavailable(
primary_input: InputIdentity,
source_coverage: SourceSetCoverageV1,
) -> Self {
let mut reasons = Vec::with_capacity(2);
match source_coverage.state() {
SourceSetCoverageStateV1::Complete => {}
SourceSetCoverageStateV1::Partial => {
reasons.push(DependencyClosureCoverageReasonV1::SourceDeclarationsPartial);
}
SourceSetCoverageStateV1::Unavailable => {
reasons.push(DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable);
}
}
reasons.push(DependencyClosureCoverageReasonV1::CaptureUnavailable);
reasons.sort_unstable();
Self {
schema: DEPENDENCY_CLOSURE_V1_ID,
budget: ResourceClosureBudgetV1::VALUE,
primary_input,
coverage: DependencyClosureCoverageV1::Unavailable { reasons },
identity: None,
references: Vec::new(),
external_resources: Vec::new(),
work: DependencyClosureWorkV1::default(),
}
}
pub const fn contract_id(&self) -> &'static str {
DEPENDENCY_CLOSURE_V1_ID
}
pub const fn budget(&self) -> ResourceClosureBudgetV1 {
self.budget
}
pub const fn primary_input(&self) -> &InputIdentity {
&self.primary_input
}
pub const fn coverage(&self) -> &DependencyClosureCoverageV1 {
&self.coverage
}
pub const fn identity(&self) -> Option<&DependencyClosureIdentityV1> {
self.identity.as_ref()
}
pub fn references(&self) -> &[DependencyClosureReferenceV1] {
&self.references
}
pub fn external_resources(&self) -> &[ExternalResourceIdentityV1] {
&self.external_resources
}
pub const fn work(&self) -> DependencyClosureWorkV1 {
self.work
}
pub fn record_identity(&self) -> InputIdentity {
canonical_record_identity(self)
}
fn validate_wire(&self, coverage_reasons_overflowed: bool) -> Result<(), String> {
if self.budget != ResourceClosureBudgetV1::VALUE {
return Err("dependency closure budget is not immutable V1".to_owned());
}
if self.references.len() > DEPENDENCY_CLOSURE_V1_MAX_REFERENCES {
return Err("dependency closure has too many references".to_owned());
}
if self.external_resources.len() > DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES {
return Err("dependency closure has too many external resources".to_owned());
}
for (expected, reference) in self.references.iter().enumerate() {
if reference.source_order_index != expected {
return Err(format!(
"dependency reference order {} is not expected prefix index {expected}",
reference.source_order_index
));
}
if reference.purpose != DependencyResourcePurposeV1::from_kind(reference.kind) {
return Err("dependency reference purpose disagrees with kind".to_owned());
}
match &reference.target {
DependencyReferenceTargetV1::Refused {
key: Some(_),
reason: DependencyResourceRefusalReasonV1::Symlink,
}
| DependencyReferenceTargetV1::Refused {
key: None,
reason:
DependencyResourceRefusalReasonV1::Absolute
| DependencyResourceRefusalReasonV1::Escaping
| DependencyResourceRefusalReasonV1::Remote
| DependencyResourceRefusalReasonV1::Malformed
| DependencyResourceRefusalReasonV1::Oversized,
} => {}
DependencyReferenceTargetV1::Refused { .. } => {
return Err(
"dependency refused target has an invalid key/reason pair".to_owned()
);
}
_ => {}
}
}
if self
.external_resources
.windows(2)
.any(|rows| rows[0].key >= rows[1].key)
{
return Err("dependency external resources must be strictly key ordered".to_owned());
}
let reasons = self.coverage.reasons();
if coverage_reasons_overflowed || reasons.windows(2).any(|rows| rows[0] >= rows[1]) {
return Err("dependency coverage reasons must be strictly ordered".to_owned());
}
match &self.coverage {
DependencyClosureCoverageV1::Complete if !reasons.is_empty() => {
return Err("complete dependency closure cannot have reasons".to_owned());
}
DependencyClosureCoverageV1::Partial { .. }
| DependencyClosureCoverageV1::Unavailable { .. }
if reasons.is_empty() =>
{
return Err("incomplete dependency closure requires reasons".to_owned());
}
_ => {}
}
if self.coverage.is_complete()
&& self.references.iter().any(|reference| {
!matches!(
reference.target,
DependencyReferenceTargetV1::Primary
| DependencyReferenceTargetV1::External { .. }
)
})
{
return Err("complete dependency closure has an incomplete target".to_owned());
}
for reference in &self.references {
if let DependencyReferenceTargetV1::External { key } = &reference.target
&& self
.external_resources
.binary_search_by(|resource| resource.key.cmp(key))
.is_err()
{
return Err("dependency reference names an absent external row".to_owned());
}
}
for resource in &self.external_resources {
if !self.references.iter().any(|reference| {
matches!(
&reference.target,
DependencyReferenceTargetV1::External { key } if key == &resource.key
)
}) {
return Err("dependency external row is not referenced".to_owned());
}
}
let has_refused = self.references.iter().any(|reference| {
matches!(
reference.target,
DependencyReferenceTargetV1::Refused { .. }
)
});
let has_unavailable = self.references.iter().any(|reference| {
matches!(
reference.target,
DependencyReferenceTargetV1::Unavailable { .. }
)
});
if has_refused != reasons.contains(&DependencyClosureCoverageReasonV1::RefusedResource)
|| has_unavailable
!= reasons.contains(&DependencyClosureCoverageReasonV1::UnavailableResource)
{
return Err(
"dependency closure target states disagree with coverage reasons".to_owned(),
);
}
if reasons.contains(&DependencyClosureCoverageReasonV1::CaptureUnavailable)
&& (!matches!(
self.coverage,
DependencyClosureCoverageV1::Unavailable { .. }
) || !self.references.is_empty()
|| !self.external_resources.is_empty())
{
return Err(
"capture-unavailable dependency closure must retain no closure rows".to_owned(),
);
}
if self.coverage.is_complete() != self.identity.is_some() {
return Err(
"dependency closure identity must be present exactly for complete coverage"
.to_owned(),
);
}
if let Some(identity) = &self.identity
&& identity
!= &canonical_identity(
&self.primary_input,
&self.references,
&self.external_resources,
)
{
return Err("dependency closure identity does not match its preimage".to_owned());
}
let work = self.work;
let retained_or_terminal = self.references.len()..=self.references.len().saturating_add(1);
let mut target_keys = BTreeSet::new();
for reference in &self.references {
match &reference.target {
DependencyReferenceTargetV1::External { key }
| DependencyReferenceTargetV1::Refused { key: Some(key), .. }
| DependencyReferenceTargetV1::Unavailable { key: Some(key), .. } => {
target_keys.insert(key);
}
DependencyReferenceTargetV1::Primary
| DependencyReferenceTargetV1::Refused { key: None, .. }
| DependencyReferenceTargetV1::Unavailable { key: None, .. } => {}
}
}
let captured_bytes = self
.external_resources
.iter()
.try_fold(0_u64, |total, row| total.checked_add(row.identity.bytes()));
if work.inspected_references > DEPENDENCY_CLOSURE_V1_MAX_REFERENCES + 1
|| !retained_or_terminal.contains(&work.inspected_references)
|| (work.inspected_references != self.references.len()
&& !reasons.contains(&DependencyClosureCoverageReasonV1::ResourceBudgetExceeded))
|| work.retained_references != self.references.len()
|| work.normalization_bytes_inspected
> DEPENDENCY_CLOSURE_V1_MAX_NORMALIZATION_BYTES + 1
|| work.dedup_probes > DEPENDENCY_CLOSURE_V1_MAX_DEDUP_PROBES + 1
|| work.dedup_probes != work.inspected_references
|| work.path_components_inspected
> DEPENDENCY_CLOSURE_V1_MAX_REFERENCES
.saturating_mul(DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS)
.saturating_add(DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS + 1)
|| work.external_open_attempts > DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES
|| work.distinct_external_keys != target_keys.len()
|| work.external_open_attempts < self.external_resources.len()
|| work.external_open_attempts > work.distinct_external_keys
|| work.captured_external_resources != self.external_resources.len()
|| captured_bytes.is_none()
|| work.external_bytes_read_hashed < captured_bytes.unwrap_or(u64::MAX)
|| (work.external_bytes_read_hashed != captured_bytes.unwrap_or(u64::MAX)
&& !reasons.contains(&DependencyClosureCoverageReasonV1::ResourceBudgetExceeded))
|| work.external_bytes_read_hashed
> DEPENDENCY_CLOSURE_V1_MAX_TOTAL_RESOURCE_BYTES.saturating_add(1)
{
return Err("dependency closure work counters are invalid".to_owned());
}
Ok(())
}
pub(crate) fn validate_against(
&self,
format: SourceFormatV1,
primary: &InputIdentity,
resources: &SourceFactSetV1<SourceResourceReferenceV1>,
) -> Result<(), DependencyClosureError> {
if &self.primary_input != primary {
return Err(DependencyClosureError::PrimaryIdentityMismatch);
}
if self.references.len() > resources.rows().len() {
return Err(DependencyClosureError::ResourceReferenceCountMismatch {
facts: resources.rows().len(),
closure: self.references.len(),
});
}
for (closure, source) in self.references.iter().zip(resources.rows()) {
if closure.source_order_index != source.source_order_index()
|| closure.kind != source.kind()
|| closure.purpose != DependencyResourcePurposeV1::from_kind(source.kind())
|| closure.source_index != source.source_index()
{
return Err(DependencyClosureError::ResourceReferenceMismatch {
source_order_index: closure.source_order_index,
});
}
validate_target_against_locator(
format,
closure.source_order_index,
&closure.target,
source.locator(),
)?;
}
if self.coverage.is_complete()
&& (!matches!(
resources.coverage().state(),
SourceSetCoverageStateV1::Complete
) || self.references.len() != resources.rows().len())
{
return Err(DependencyClosureError::CompleteCoverageMismatch);
}
if matches!(
resources.coverage().state(),
SourceSetCoverageStateV1::Unavailable
) && (!self.references.is_empty()
|| !matches!(
self.coverage,
DependencyClosureCoverageV1::Unavailable { .. }
))
{
return Err(DependencyClosureError::UnavailableCoverageMismatch);
}
let reasons = self.coverage.reasons();
let source_reason_matches = match resources.coverage().state() {
SourceSetCoverageStateV1::Complete => {
!reasons.contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsPartial)
&& !reasons
.contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable)
}
SourceSetCoverageStateV1::Partial => {
reasons.contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsPartial)
&& !reasons
.contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable)
}
SourceSetCoverageStateV1::Unavailable => {
reasons.contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable)
&& !reasons
.contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsPartial)
}
};
let capture_reason_matches = !reasons
.contains(&DependencyClosureCoverageReasonV1::CaptureUnavailable)
|| (matches!(
self.coverage,
DependencyClosureCoverageV1::Unavailable { .. }
) && self.references.is_empty()
&& self.external_resources.is_empty()
&& self.identity.is_none());
if !source_reason_matches || !capture_reason_matches {
return Err(DependencyClosureError::CoverageReasonMismatch);
}
if self.coverage.is_complete() != self.identity.is_some() {
return Err(DependencyClosureError::ClosureIdentityCoverageMismatch);
}
Ok(())
}
}
fn validate_target_against_locator(
format: SourceFormatV1,
source_order_index: usize,
target: &DependencyReferenceTargetV1,
locator: &SourceResourceLocatorV1,
) -> Result<(), DependencyClosureError> {
let matches = match locator {
SourceResourceLocatorV1::Relative(locator) => {
let syntax = match format {
SourceFormatV1::GltfJson | SourceFormatV1::Glb => ResourceKeySyntaxV1::GltfUri,
SourceFormatV1::Fbx => ResourceKeySyntaxV1::ParserRelativePath,
};
match DependencyResourceKeyV1::from_relative(locator, syntax) {
Ok(expected) => match target {
DependencyReferenceTargetV1::External { key }
| DependencyReferenceTargetV1::Unavailable { key: Some(key), .. } => {
if key != &expected {
return Err(DependencyClosureError::ResourceKeyMismatch {
source_order_index,
});
}
true
}
DependencyReferenceTargetV1::Refused {
key: Some(key),
reason: DependencyResourceRefusalReasonV1::Symlink,
} => {
if key != &expected {
return Err(DependencyClosureError::ResourceKeyMismatch {
source_order_index,
});
}
true
}
_ => false,
},
Err(
DependencyClosureError::ResourceKeyTooLong { .. }
| DependencyClosureError::TooManyPathComponents { .. },
) => matches!(
target,
DependencyReferenceTargetV1::Refused {
key: None,
reason: DependencyResourceRefusalReasonV1::Oversized,
}
),
Err(DependencyClosureError::InvalidResourceKey) => matches!(
target,
DependencyReferenceTargetV1::Refused {
key: None,
reason: DependencyResourceRefusalReasonV1::Malformed,
}
),
Err(_) => false,
}
}
_ => matches!(
(target, locator),
(
DependencyReferenceTargetV1::Primary,
SourceResourceLocatorV1::Embedded | SourceResourceLocatorV1::DataUri
) | (
DependencyReferenceTargetV1::Unavailable {
key: None,
reason: DependencyResourceUnavailableReasonV1::Missing,
},
SourceResourceLocatorV1::Missing
) | (
DependencyReferenceTargetV1::Refused {
key: None,
reason: DependencyResourceRefusalReasonV1::Absolute
},
SourceResourceLocatorV1::Absolute
) | (
DependencyReferenceTargetV1::Refused {
key: None,
reason: DependencyResourceRefusalReasonV1::Escaping
},
SourceResourceLocatorV1::Escaping
) | (
DependencyReferenceTargetV1::Refused {
key: None,
reason: DependencyResourceRefusalReasonV1::Remote
},
SourceResourceLocatorV1::Remote
) | (
DependencyReferenceTargetV1::Refused {
key: None,
reason: DependencyResourceRefusalReasonV1::Malformed
},
SourceResourceLocatorV1::Malformed
) | (
DependencyReferenceTargetV1::Refused {
key: None,
reason: DependencyResourceRefusalReasonV1::Oversized
},
SourceResourceLocatorV1::Oversized
)
),
};
if matches {
Ok(())
} else {
Err(DependencyClosureError::ResourceReferenceMismatch { source_order_index })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct PendingReferenceV1 {
external_key: Option<DependencyResourceKeyV1>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum CachedExternalOutcomeV1 {
Captured(InputIdentity),
Refused(DependencyResourceRefusalReasonV1),
Unavailable(DependencyResourceUnavailableReasonV1),
}
pub struct DependencyClosureBuilderV1 {
primary_input: InputIdentity,
source_coverage: SourceSetCoverageV1,
expected_references: usize,
references: Vec<DependencyClosureReferenceV1>,
external_resources: BTreeMap<DependencyResourceKeyV1, InputIdentity>,
external_keys: BTreeSet<DependencyResourceKeyV1>,
external_outcomes: BTreeMap<DependencyResourceKeyV1, CachedExternalOutcomeV1>,
opened_external_keys: BTreeSet<DependencyResourceKeyV1>,
reasons: BTreeSet<DependencyClosureCoverageReasonV1>,
work: DependencyClosureWorkV1,
pending_reference: Option<PendingReferenceV1>,
stopped: bool,
unmodeled_domain: bool,
}
impl DependencyClosureBuilderV1 {
pub fn new(
primary_input: InputIdentity,
source_coverage: SourceSetCoverageV1,
expected_references: usize,
) -> Self {
let mut reasons = BTreeSet::new();
match source_coverage.state() {
SourceSetCoverageStateV1::Complete => {}
SourceSetCoverageStateV1::Partial => {
reasons.insert(DependencyClosureCoverageReasonV1::SourceDeclarationsPartial);
}
SourceSetCoverageStateV1::Unavailable => {
reasons.insert(DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable);
}
}
Self {
primary_input,
source_coverage,
expected_references,
references: Vec::with_capacity(
expected_references.min(DEPENDENCY_CLOSURE_V1_MAX_REFERENCES),
),
external_resources: BTreeMap::new(),
external_keys: BTreeSet::new(),
external_outcomes: BTreeMap::new(),
opened_external_keys: BTreeSet::new(),
reasons,
work: DependencyClosureWorkV1::default(),
pending_reference: None,
stopped: false,
unmodeled_domain: false,
}
}
pub const fn primary_input(&self) -> &InputIdentity {
&self.primary_input
}
pub const fn max_resource_bytes(&self) -> u64 {
DEPENDENCY_CLOSURE_V1_MAX_RESOURCE_BYTES
}
pub const fn remaining_external_bytes(&self) -> u64 {
DEPENDENCY_CLOSURE_V1_MAX_TOTAL_RESOURCE_BYTES
.saturating_sub(self.work.external_bytes_read_hashed)
}
pub fn external_identity(&self, key: &DependencyResourceKeyV1) -> Option<&InputIdentity> {
self.external_resources.get(key)
}
pub fn begin_reference(&mut self, locator_bytes: usize, path_components: usize) -> bool {
if self.stopped || self.pending_reference.is_some() {
return false;
}
self.work.inspected_references = bounded_add(
self.work.inspected_references,
1,
DEPENDENCY_CLOSURE_V1_MAX_REFERENCES,
);
self.work.normalization_bytes_inspected = bounded_add(
self.work.normalization_bytes_inspected,
locator_bytes,
DEPENDENCY_CLOSURE_V1_MAX_NORMALIZATION_BYTES,
);
self.work.path_components_inspected = self
.work
.path_components_inspected
.saturating_add(path_components.min(DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS + 1));
self.work.dedup_probes = bounded_add(
self.work.dedup_probes,
1,
DEPENDENCY_CLOSURE_V1_MAX_DEDUP_PROBES,
);
if self.references.len() >= DEPENDENCY_CLOSURE_V1_MAX_REFERENCES
|| locator_bytes > DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES
|| path_components > DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS
|| self.work.normalization_bytes_inspected
> DEPENDENCY_CLOSURE_V1_MAX_NORMALIZATION_BYTES
|| self.work.dedup_probes > DEPENDENCY_CLOSURE_V1_MAX_DEDUP_PROBES
{
self.stop_for_budget();
return false;
}
self.pending_reference = Some(PendingReferenceV1 { external_key: None });
true
}
pub fn prepare_external_key(
&mut self,
key: &DependencyResourceKeyV1,
) -> Result<Option<bool>, DependencyClosureError> {
let pending = self
.pending_reference
.as_mut()
.ok_or(DependencyClosureError::ReferenceNotStarted)?;
if pending.external_key.is_some() {
return Err(DependencyClosureError::ExternalKeyAlreadyPrepared);
}
if self.external_keys.contains(key) {
if !self.external_outcomes.contains_key(key) {
return Err(DependencyClosureError::ExternalOutcomeMissing);
}
pending.external_key = Some(key.clone());
return Ok(Some(false));
}
if self.external_keys.len() >= DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES {
self.pending_reference = None;
self.stop_for_budget();
return Ok(None);
}
self.external_keys.insert(key.clone());
pending.external_key = Some(key.clone());
self.work.distinct_external_keys = self.work.distinct_external_keys.saturating_add(1);
Ok(Some(true))
}
pub fn record_external_open_attempt(
&mut self,
key: &DependencyResourceKeyV1,
) -> Result<(), DependencyClosureError> {
self.require_pending_key(key)?;
if self.external_outcomes.contains_key(key) {
return Err(DependencyClosureError::ExternalOutcomeMismatch);
}
if !self.opened_external_keys.insert(key.clone()) {
return Err(DependencyClosureError::DuplicateExternalOpen);
}
self.work.external_open_attempts = self.work.external_open_attempts.saturating_add(1);
Ok(())
}
pub fn push_primary(
&mut self,
source_order_index: usize,
kind: SourceResourceKindV1,
source_index: u64,
) -> Result<(), DependencyClosureError> {
self.require_reference_order(source_order_index)?;
self.require_no_pending_key()?;
self.push_reference(DependencyClosureReferenceV1::new(
source_order_index,
kind,
source_index,
DependencyReferenceTargetV1::Primary,
))
}
pub fn push_refused(
&mut self,
source_order_index: usize,
kind: SourceResourceKindV1,
source_index: u64,
reason: DependencyResourceRefusalReasonV1,
) -> Result<(), DependencyClosureError> {
self.require_reference_order(source_order_index)?;
let prepared_key = self.pending_external_key()?.cloned();
let key = match prepared_key {
Some(key) if reason == DependencyResourceRefusalReasonV1::Symlink => {
match self.external_outcomes.get(&key) {
Some(CachedExternalOutcomeV1::Refused(cached)) if *cached == reason => {}
Some(_) => return Err(DependencyClosureError::ExternalOutcomeMismatch),
None => {
self.external_outcomes
.insert(key.clone(), CachedExternalOutcomeV1::Refused(reason));
}
}
Some(key)
}
Some(_) => return Err(DependencyClosureError::ExternalOutcomeMismatch),
None if reason == DependencyResourceRefusalReasonV1::Symlink => {
return Err(DependencyClosureError::ExternalKeyNotPrepared);
}
None => None,
};
self.reasons
.insert(DependencyClosureCoverageReasonV1::RefusedResource);
self.push_reference(DependencyClosureReferenceV1::new(
source_order_index,
kind,
source_index,
DependencyReferenceTargetV1::Refused { key, reason },
))
}
pub fn push_unavailable(
&mut self,
source_order_index: usize,
kind: SourceResourceKindV1,
source_index: u64,
key: Option<DependencyResourceKeyV1>,
reason: DependencyResourceUnavailableReasonV1,
) -> Result<(), DependencyClosureError> {
self.require_reference_order(source_order_index)?;
match &key {
Some(key) => {
self.require_pending_key(key)?;
match self.external_outcomes.get(key) {
Some(CachedExternalOutcomeV1::Unavailable(cached)) if *cached == reason => {}
Some(_) => return Err(DependencyClosureError::ExternalOutcomeMismatch),
None => {
self.external_outcomes
.insert(key.clone(), CachedExternalOutcomeV1::Unavailable(reason));
}
}
}
None => self.require_no_pending_key()?,
}
self.reasons
.insert(DependencyClosureCoverageReasonV1::UnavailableResource);
self.push_reference(DependencyClosureReferenceV1::new(
source_order_index,
kind,
source_index,
DependencyReferenceTargetV1::Unavailable { key, reason },
))
}
pub fn push_external_alias(
&mut self,
source_order_index: usize,
kind: SourceResourceKindV1,
source_index: u64,
key: DependencyResourceKeyV1,
) -> Result<(), DependencyClosureError> {
self.require_reference_order(source_order_index)?;
self.require_pending_key(&key)?;
match self.external_outcomes.get(&key) {
Some(CachedExternalOutcomeV1::Captured(identity))
if self.external_resources.get(&key) == Some(identity) => {}
Some(_) => return Err(DependencyClosureError::ExternalOutcomeMismatch),
None => return Err(DependencyClosureError::ExternalIdentityMissing),
}
self.push_external_reference(source_order_index, kind, source_index, key)
}
pub fn push_captured_external(
&mut self,
source_order_index: usize,
kind: SourceResourceKindV1,
source_index: u64,
key: DependencyResourceKeyV1,
identity: InputIdentity,
) -> Result<bool, DependencyClosureError> {
self.require_reference_order(source_order_index)?;
self.require_pending_key(&key)?;
if self.external_outcomes.contains_key(&key) || self.external_resources.contains_key(&key) {
return Err(DependencyClosureError::ExternalOutcomeMismatch);
}
if !self.opened_external_keys.contains(&key) {
return Err(DependencyClosureError::ExternalKeyNotOpened);
}
let bytes = identity.bytes();
let next_total = self.work.external_bytes_read_hashed.checked_add(bytes);
if bytes > DEPENDENCY_CLOSURE_V1_MAX_RESOURCE_BYTES
|| next_total.is_none_or(|total| total > DEPENDENCY_CLOSURE_V1_MAX_TOTAL_RESOURCE_BYTES)
{
let bounded_observed = bytes.min(DEPENDENCY_CLOSURE_V1_MAX_RESOURCE_BYTES + 1);
self.work.external_bytes_read_hashed = self
.work
.external_bytes_read_hashed
.saturating_add(bounded_observed)
.min(DEPENDENCY_CLOSURE_V1_MAX_TOTAL_RESOURCE_BYTES + 1);
self.push_unavailable(
source_order_index,
kind,
source_index,
Some(key),
DependencyResourceUnavailableReasonV1::ResourceBudgetExceeded,
)?;
self.stop_for_budget();
return Ok(false);
}
self.work.external_bytes_read_hashed = next_total.unwrap_or(u64::MAX);
self.work.captured_external_resources =
self.work.captured_external_resources.saturating_add(1);
self.external_resources
.insert(key.clone(), identity.clone());
self.external_outcomes
.insert(key.clone(), CachedExternalOutcomeV1::Captured(identity));
self.push_external_reference(source_order_index, kind, source_index, key)?;
Ok(true)
}
pub fn mark_unmodeled_resource_domain(&mut self) {
self.unmodeled_domain = true;
self.reasons
.insert(DependencyClosureCoverageReasonV1::UnmodeledResourceDomain);
}
pub fn finish(self) -> Result<DependencyClosureV1, DependencyClosureError> {
if self.pending_reference.is_some() {
return Err(DependencyClosureError::UnfinishedReference);
}
if self.references.len() != self.expected_references && !self.stopped {
return Err(DependencyClosureError::ReferenceCountMismatch {
expected: self.expected_references,
actual: self.references.len(),
});
}
if self.references.len() > self.expected_references {
return Err(DependencyClosureError::ReferenceCountMismatch {
expected: self.expected_references,
actual: self.references.len(),
});
}
let coverage = match self.source_coverage.state() {
SourceSetCoverageStateV1::Unavailable => DependencyClosureCoverageV1::Unavailable {
reasons: self.reasons.into_iter().collect(),
},
SourceSetCoverageStateV1::Complete
if self.reasons.is_empty()
&& !self.unmodeled_domain
&& self.references.len() == self.expected_references
&& self.references.iter().all(|reference| {
matches!(
reference.target,
DependencyReferenceTargetV1::Primary
| DependencyReferenceTargetV1::External { .. }
)
}) =>
{
DependencyClosureCoverageV1::Complete
}
_ => DependencyClosureCoverageV1::Partial {
reasons: self.reasons.into_iter().collect(),
},
};
let external_resources = self
.external_resources
.into_iter()
.map(|(key, identity)| ExternalResourceIdentityV1 { key, identity })
.collect::<Vec<_>>();
let identity = coverage.is_complete().then(|| {
canonical_identity(&self.primary_input, &self.references, &external_resources)
});
Ok(DependencyClosureV1 {
schema: DEPENDENCY_CLOSURE_V1_ID,
budget: ResourceClosureBudgetV1::VALUE,
primary_input: self.primary_input,
coverage,
identity,
references: self.references,
external_resources,
work: self.work,
})
}
fn push_external_reference(
&mut self,
source_order_index: usize,
kind: SourceResourceKindV1,
source_index: u64,
key: DependencyResourceKeyV1,
) -> Result<(), DependencyClosureError> {
self.push_reference(DependencyClosureReferenceV1::new(
source_order_index,
kind,
source_index,
DependencyReferenceTargetV1::External { key },
))
}
fn push_reference(
&mut self,
reference: DependencyClosureReferenceV1,
) -> Result<(), DependencyClosureError> {
if self.pending_reference.is_none() {
return Err(DependencyClosureError::ReferenceNotStarted);
}
let expected = self.references.len();
if reference.source_order_index != expected {
return Err(DependencyClosureError::NonCanonicalReferenceOrder {
expected,
actual: reference.source_order_index,
});
}
self.pending_reference = None;
self.work.retained_references = self.work.retained_references.saturating_add(1);
self.references.push(reference);
Ok(())
}
fn stop_for_budget(&mut self) {
self.reasons
.insert(DependencyClosureCoverageReasonV1::ResourceBudgetExceeded);
self.stopped = true;
}
fn pending_external_key(
&self,
) -> Result<Option<&DependencyResourceKeyV1>, DependencyClosureError> {
self.pending_reference
.as_ref()
.map(|pending| pending.external_key.as_ref())
.ok_or(DependencyClosureError::ReferenceNotStarted)
}
fn require_no_pending_key(&self) -> Result<(), DependencyClosureError> {
match self.pending_external_key()? {
None => Ok(()),
Some(_) => Err(DependencyClosureError::ExternalOutcomeMismatch),
}
}
fn require_pending_key(
&self,
key: &DependencyResourceKeyV1,
) -> Result<(), DependencyClosureError> {
match self.pending_external_key()? {
Some(pending) if pending == key => Ok(()),
Some(_) => Err(DependencyClosureError::ExternalKeyMismatch),
None => Err(DependencyClosureError::ExternalKeyNotPrepared),
}
}
fn require_reference_order(
&self,
source_order_index: usize,
) -> Result<(), DependencyClosureError> {
let expected = self.references.len();
if source_order_index == expected {
Ok(())
} else {
Err(DependencyClosureError::NonCanonicalReferenceOrder {
expected,
actual: source_order_index,
})
}
}
}
fn bounded_add(current: usize, observed: usize, limit: usize) -> usize {
current
.saturating_add(observed)
.min(limit.saturating_add(1))
}
fn canonical_identity(
primary: &InputIdentity,
references: &[DependencyClosureReferenceV1],
resources: &[ExternalResourceIdentityV1],
) -> DependencyClosureIdentityV1 {
let mut bytes = Vec::new();
encode_text(&mut bytes, DEPENDENCY_CLOSURE_V1_ID);
encode_text(&mut bytes, DEPENDENCY_CLOSURE_BUDGET_V1_ID);
encode_u64(&mut bytes, DEPENDENCY_CLOSURE_V1_MAX_REFERENCES as u64);
encode_u64(
&mut bytes,
DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES as u64,
);
encode_u64(&mut bytes, DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES as u64);
encode_u64(&mut bytes, DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS as u64);
encode_u64(
&mut bytes,
DEPENDENCY_CLOSURE_V1_MAX_NORMALIZATION_BYTES as u64,
);
encode_u64(&mut bytes, DEPENDENCY_CLOSURE_V1_MAX_RESOURCE_BYTES);
encode_u64(&mut bytes, DEPENDENCY_CLOSURE_V1_MAX_TOTAL_RESOURCE_BYTES);
encode_u64(&mut bytes, DEPENDENCY_CLOSURE_V1_MAX_DEDUP_PROBES as u64);
bytes.push(0); encode_identity(&mut bytes, primary);
encode_references(&mut bytes, references, |bytes, target| match target {
DependencyReferenceTargetV1::Primary => bytes.push(0),
DependencyReferenceTargetV1::External { key } => {
bytes.push(1);
encode_text(bytes, key.as_str());
}
DependencyReferenceTargetV1::Refused { .. }
| DependencyReferenceTargetV1::Unavailable { .. } => {
unreachable!("only complete reference targets enter closure identity")
}
});
encode_external_resources(&mut bytes, resources);
DependencyClosureIdentityV1(InputIdentity::from_bytes(&bytes))
}
fn canonical_record_identity(closure: &DependencyClosureV1) -> InputIdentity {
let mut bytes = Vec::new();
encode_text(&mut bytes, "animsmith-dependency-closure-record-v1");
encode_text(&mut bytes, closure.schema);
encode_text(&mut bytes, closure.budget.schema);
encode_u64(&mut bytes, closure.budget.max_references as u64);
encode_u64(&mut bytes, closure.budget.max_external_resources as u64);
encode_u64(&mut bytes, closure.budget.max_key_bytes as u64);
encode_u64(&mut bytes, closure.budget.max_path_components as u64);
encode_u64(&mut bytes, closure.budget.max_normalization_bytes as u64);
encode_u64(&mut bytes, closure.budget.max_resource_bytes);
encode_u64(&mut bytes, closure.budget.max_total_resource_bytes);
encode_u64(&mut bytes, closure.budget.max_dedup_probes as u64);
encode_identity(&mut bytes, &closure.primary_input);
match &closure.coverage {
DependencyClosureCoverageV1::Complete => bytes.push(0),
DependencyClosureCoverageV1::Partial { reasons } => {
bytes.push(1);
encode_u64(&mut bytes, reasons.len() as u64);
for reason in reasons {
bytes.push(coverage_reason_tag(*reason));
}
}
DependencyClosureCoverageV1::Unavailable { reasons } => {
bytes.push(2);
encode_u64(&mut bytes, reasons.len() as u64);
for reason in reasons {
bytes.push(coverage_reason_tag(*reason));
}
}
}
match &closure.identity {
Some(identity) => {
bytes.push(1);
encode_identity(&mut bytes, identity.input_identity());
}
None => bytes.push(0),
}
encode_references(
&mut bytes,
&closure.references,
|bytes, target| match target {
DependencyReferenceTargetV1::Primary => bytes.push(0),
DependencyReferenceTargetV1::External { key } => {
bytes.push(1);
encode_text(bytes, key.as_str());
}
DependencyReferenceTargetV1::Refused { key, reason } => {
bytes.push(2);
encode_optional_key(bytes, key.as_ref());
bytes.push(refusal_reason_tag(*reason));
}
DependencyReferenceTargetV1::Unavailable { key, reason } => {
bytes.push(3);
encode_optional_key(bytes, key.as_ref());
bytes.push(unavailable_reason_tag(*reason));
}
},
);
encode_external_resources(&mut bytes, &closure.external_resources);
let work = closure.work;
encode_u64(&mut bytes, work.inspected_references as u64);
encode_u64(&mut bytes, work.retained_references as u64);
encode_u64(&mut bytes, work.normalization_bytes_inspected as u64);
encode_u64(&mut bytes, work.path_components_inspected as u64);
encode_u64(&mut bytes, work.dedup_probes as u64);
encode_u64(&mut bytes, work.external_open_attempts as u64);
encode_u64(&mut bytes, work.distinct_external_keys as u64);
encode_u64(&mut bytes, work.captured_external_resources as u64);
encode_u64(&mut bytes, work.external_bytes_read_hashed);
InputIdentity::from_bytes(&bytes)
}
fn encode_references(
bytes: &mut Vec<u8>,
references: &[DependencyClosureReferenceV1],
mut encode_target: impl FnMut(&mut Vec<u8>, &DependencyReferenceTargetV1),
) {
encode_u64(bytes, references.len() as u64);
for reference in references {
encode_u64(bytes, reference.source_order_index as u64);
bytes.push(resource_kind_tag(reference.kind));
bytes.push(resource_purpose_tag(reference.purpose));
encode_u64(bytes, reference.source_index);
encode_target(bytes, &reference.target);
}
}
fn encode_external_resources(bytes: &mut Vec<u8>, resources: &[ExternalResourceIdentityV1]) {
encode_u64(bytes, resources.len() as u64);
for resource in resources {
encode_text(bytes, resource.key.as_str());
encode_identity(bytes, &resource.identity);
}
}
fn encode_optional_key(bytes: &mut Vec<u8>, key: Option<&DependencyResourceKeyV1>) {
match key {
Some(key) => {
bytes.push(1);
encode_text(bytes, key.as_str());
}
None => bytes.push(0),
}
}
fn coverage_reason_tag(reason: DependencyClosureCoverageReasonV1) -> u8 {
match reason {
DependencyClosureCoverageReasonV1::SourceDeclarationsPartial => 0,
DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable => 1,
DependencyClosureCoverageReasonV1::CaptureUnavailable => 2,
DependencyClosureCoverageReasonV1::RefusedResource => 3,
DependencyClosureCoverageReasonV1::UnavailableResource => 4,
DependencyClosureCoverageReasonV1::ResourceBudgetExceeded => 5,
DependencyClosureCoverageReasonV1::UnmodeledResourceDomain => 6,
}
}
fn refusal_reason_tag(reason: DependencyResourceRefusalReasonV1) -> u8 {
match reason {
DependencyResourceRefusalReasonV1::Absolute => 0,
DependencyResourceRefusalReasonV1::Escaping => 1,
DependencyResourceRefusalReasonV1::Remote => 2,
DependencyResourceRefusalReasonV1::Malformed => 3,
DependencyResourceRefusalReasonV1::Oversized => 4,
DependencyResourceRefusalReasonV1::Symlink => 5,
}
}
fn unavailable_reason_tag(reason: DependencyResourceUnavailableReasonV1) -> u8 {
match reason {
DependencyResourceUnavailableReasonV1::ResourceRootUnavailable => 0,
DependencyResourceUnavailableReasonV1::Missing => 1,
DependencyResourceUnavailableReasonV1::Unreadable => 2,
DependencyResourceUnavailableReasonV1::ResourceBudgetExceeded => 3,
}
}
fn encode_identity(bytes: &mut Vec<u8>, identity: &InputIdentity) {
encode_text(bytes, identity.sha256());
encode_u64(bytes, identity.bytes());
}
fn encode_text(bytes: &mut Vec<u8>, value: &str) {
encode_u64(bytes, value.len() as u64);
bytes.extend_from_slice(value.as_bytes());
}
fn encode_u64(bytes: &mut Vec<u8>, value: u64) {
bytes.extend_from_slice(&value.to_le_bytes());
}
fn resource_kind_tag(kind: SourceResourceKindV1) -> u8 {
match kind {
SourceResourceKindV1::Buffer => 0,
SourceResourceKindV1::Image => 1,
SourceResourceKindV1::Texture => 2,
SourceResourceKindV1::Video => 3,
SourceResourceKindV1::Cache => 4,
}
}
fn resource_purpose_tag(purpose: DependencyResourcePurposeV1) -> u8 {
match purpose {
DependencyResourcePurposeV1::LoaderEssential => 0,
DependencyResourcePurposeV1::Nonessential => 1,
DependencyResourcePurposeV1::TargetOnly => 2,
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum DependencyClosureError {
#[error("dependency resource key is {bytes} bytes, exceeding the V1 limit of {limit}")]
ResourceKeyTooLong {
bytes: usize,
limit: usize,
},
#[error("dependency resource key is invalid or unsafe")]
InvalidResourceKey,
#[error(
"dependency resource key has {components} components, exceeding the V1 limit of {limit}"
)]
TooManyPathComponents {
components: usize,
limit: usize,
},
#[error("dependency reference outcome was supplied without begin_reference")]
ReferenceNotStarted,
#[error("dependency reference already has a prepared external key")]
ExternalKeyAlreadyPrepared,
#[error("dependency reference was begun but no outcome was supplied")]
UnfinishedReference,
#[error("dependency reference order {actual} is not expected prefix index {expected}")]
NonCanonicalReferenceOrder {
expected: usize,
actual: usize,
},
#[error("dependency alias references an external key without an identity")]
ExternalIdentityMissing,
#[error("dependency external key was not prepared for rooted capture")]
ExternalKeyNotPrepared,
#[error("dependency external key does not match the prepared reference key")]
ExternalKeyMismatch,
#[error("dependency external key has no cached outcome")]
ExternalOutcomeMissing,
#[error("dependency external outcome contradicts the cached key outcome")]
ExternalOutcomeMismatch,
#[error("dependency external key was not opened before capture")]
ExternalKeyNotOpened,
#[error("dependency external key was opened more than once")]
DuplicateExternalOpen,
#[error("dependency closure retained {actual} references but expected {expected}")]
ReferenceCountMismatch {
expected: usize,
actual: usize,
},
#[error("dependency closure primary identity does not match raw source facts")]
PrimaryIdentityMismatch,
#[error("dependency closure has {closure} references but raw facts retain {facts}")]
ResourceReferenceCountMismatch {
facts: usize,
closure: usize,
},
#[error("dependency reference {source_order_index} does not match raw source facts")]
ResourceReferenceMismatch {
source_order_index: usize,
},
#[error("dependency reference {source_order_index} key does not match raw source facts")]
ResourceKeyMismatch {
source_order_index: usize,
},
#[error("complete dependency closure does not match complete raw resource coverage")]
CompleteCoverageMismatch,
#[error("unavailable raw resource coverage requires unavailable empty dependency closure")]
UnavailableCoverageMismatch,
#[error("dependency closure coverage reasons do not match raw resource coverage")]
CoverageReasonMismatch,
#[error("dependency closure identity must be present exactly for complete coverage")]
ClosureIdentityCoverageMismatch,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
SourceFactSetV1, SourceLoaderDispositionV1, SourceProvenanceV1, SourceResourceLocatorV1,
};
fn source_resource(
order: usize,
kind: SourceResourceKindV1,
source_index: u64,
locator: SourceResourceLocatorV1,
) -> SourceResourceReferenceV1 {
SourceResourceReferenceV1::new(
order,
kind,
source_index,
locator,
SourceLoaderDispositionV1::Preserved,
SourceProvenanceV1::format_defined(),
)
}
fn relative(value: &str) -> SourceRelativeLocatorV1 {
let SourceResourceLocatorV1::Relative(value) = SourceResourceLocatorV1::classify(value)
else {
panic!("fixture must be safe relative")
};
value
}
#[test]
fn gltf_percent_aliases_normalize_but_fbx_percent_is_literal() {
let escaped = relative("textures/a%20b.png");
let plain = relative("textures/a b.png");
assert_eq!(
DependencyResourceKeyV1::from_relative(&escaped, ResourceKeySyntaxV1::GltfUri).unwrap(),
DependencyResourceKeyV1::from_relative(&plain, ResourceKeySyntaxV1::GltfUri).unwrap()
);
assert_ne!(
DependencyResourceKeyV1::from_relative(
&escaped,
ResourceKeySyntaxV1::ParserRelativePath
)
.unwrap(),
DependencyResourceKeyV1::from_relative(&plain, ResourceKeySyntaxV1::ParserRelativePath)
.unwrap()
);
}
#[test]
fn precomputed_digest_constructor_preserves_the_canonical_identity_shape() {
let identity = InputIdentity::from_sha256_digest([0xab; 32], 17);
assert_eq!(identity.sha256(), "ab".repeat(32));
assert_eq!(identity.bytes(), 17);
}
#[test]
fn normalized_keys_reject_encoded_escapes_controls_and_component_n_plus_one() {
for value in [
"",
"/absolute.bin",
"C:/drive.bin",
"file:secret.bin",
"https://example.invalid/a.bin",
"a\\b.bin",
"a/./b.bin",
"a/../b.bin",
"a?query.bin",
"a#fragment.bin",
"a\ncontrol.bin",
"a/%2f/b",
"a/%5c/b",
"a/%00/b",
"a/%ff/b",
"a/%zz/b",
] {
assert!(
matches!(
DependencyResourceKeyV1::from_source_str(value, ResourceKeySyntaxV1::GltfUri),
Err(DependencyClosureError::InvalidResourceKey)
),
"unexpected safe key: {value:?}"
);
}
let at_limit = (0..DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS)
.map(|_| "a")
.collect::<Vec<_>>()
.join("/");
assert!(
DependencyResourceKeyV1::from_relative(
&relative(&at_limit),
ResourceKeySyntaxV1::ParserRelativePath
)
.is_ok()
);
let over = format!("{at_limit}/a");
assert!(matches!(
DependencyResourceKeyV1::from_relative(
&relative(&over),
ResourceKeySyntaxV1::ParserRelativePath
),
Err(DependencyClosureError::TooManyPathComponents { .. })
));
}
#[test]
fn complete_closure_deduplicates_aliases_and_changes_with_external_identity() {
let primary = InputIdentity::from_bytes(b"primary");
let key = DependencyResourceKeyV1::from_relative(
&relative("a.bin"),
ResourceKeySyntaxV1::GltfUri,
)
.unwrap();
let mut builder =
DependencyClosureBuilderV1::new(primary.clone(), SourceSetCoverageV1::complete(), 2);
assert!(builder.begin_reference(5, 1));
assert_eq!(builder.prepare_external_key(&key).unwrap(), Some(true));
builder.record_external_open_attempt(&key).unwrap();
assert!(
builder
.push_captured_external(
0,
SourceResourceKindV1::Buffer,
0,
key.clone(),
InputIdentity::from_bytes(b"one"),
)
.unwrap()
);
assert!(builder.begin_reference(7, 1));
assert_eq!(builder.prepare_external_key(&key).unwrap(), Some(false));
builder
.push_external_alias(1, SourceResourceKindV1::Image, 0, key.clone())
.unwrap();
let first = builder.finish().unwrap();
assert!(first.coverage().is_complete());
let record_identity = first.record_identity();
assert_eq!(
record_identity.sha256(),
"dd3e91c7a0e1ea436c0ad538fae3de1e5d085b0b165d8727ebc1db61b8b2bbb5"
);
assert_eq!(record_identity.bytes(), 608);
let identity = first.identity().expect("complete closure identity");
assert_eq!(
identity.input_identity().sha256(),
"43fccaa09b2616c57863a1186b88d8d674cb404b4e99c35b18a239a4d4b782ad"
);
assert_eq!(identity.input_identity().bytes(), 409);
assert_eq!(first.external_resources().len(), 1);
assert_eq!(first.work().external_open_attempts(), 1);
let wire = serde_json::to_value(&first).unwrap();
assert_eq!(wire["schema"], DEPENDENCY_CLOSURE_V1_ID);
assert_eq!(wire["budget"]["schema"], DEPENDENCY_CLOSURE_BUDGET_V1_ID);
assert_eq!(
wire["budget"]["max_external_resources"],
DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES
);
assert_eq!(wire["coverage"]["state"], "complete");
assert_eq!(
wire["identity"]["sha256"],
"43fccaa09b2616c57863a1186b88d8d674cb404b4e99c35b18a239a4d4b782ad"
);
let round_trip: DependencyClosureV1 =
serde_json::from_value(wire.clone()).expect("builder work reads back");
assert_eq!(round_trip, first);
for (field, value) in [
("inspected_references", serde_json::json!(0)),
("dedup_probes", serde_json::json!(0)),
("distinct_external_keys", serde_json::json!(0)),
("external_open_attempts", serde_json::json!(0)),
("external_bytes_read_hashed", serde_json::json!(0)),
] {
let mut impossible = wire.clone();
impossible["work"][field] = value;
let error = serde_json::from_value::<DependencyClosureV1>(impossible)
.expect_err("impossible complete work must fail strict readback");
assert!(
error
.to_string()
.contains("dependency closure work counters are invalid"),
"field {field} produced {error}"
);
}
let mut impossible_terminal = wire.clone();
impossible_terminal["work"]["inspected_references"] = serde_json::json!(3);
impossible_terminal["work"]["dedup_probes"] = serde_json::json!(3);
let error = serde_json::from_value::<DependencyClosureV1>(impossible_terminal)
.expect_err("complete work cannot claim a terminal N+1 inspection");
assert!(
error
.to_string()
.contains("dependency closure work counters are invalid"),
"unexpected terminal-work error: {error}"
);
let mut changed =
DependencyClosureBuilderV1::new(primary, SourceSetCoverageV1::complete(), 2);
assert!(changed.begin_reference(5, 1));
let changed_key = DependencyResourceKeyV1::from_relative(
&relative("a.bin"),
ResourceKeySyntaxV1::GltfUri,
)
.unwrap();
assert_eq!(
changed.prepare_external_key(&changed_key).unwrap(),
Some(true)
);
changed.record_external_open_attempt(&changed_key).unwrap();
assert!(
changed
.push_captured_external(
0,
SourceResourceKindV1::Buffer,
0,
changed_key,
InputIdentity::from_bytes(b"two"),
)
.unwrap()
);
assert!(changed.begin_reference(7, 1));
assert_eq!(changed.prepare_external_key(&key).unwrap(), Some(false));
changed
.push_external_alias(1, SourceResourceKindV1::Image, 0, key)
.unwrap();
let changed = changed.finish().unwrap();
assert_eq!(first.references().len(), changed.references().len());
assert_eq!(first.references()[0].kind(), changed.references()[0].kind());
assert_eq!(first.references()[1].kind(), changed.references()[1].kind());
assert_ne!(first.identity(), changed.identity());
assert_ne!(first.record_identity(), changed.record_identity());
}
#[test]
fn partial_and_unavailable_closures_never_claim_identity() {
let primary = InputIdentity::from_bytes(b"primary");
let mut partial =
DependencyClosureBuilderV1::new(primary.clone(), SourceSetCoverageV1::complete(), 1);
assert!(partial.begin_reference(0, 0));
partial
.push_refused(
0,
SourceResourceKindV1::Image,
0,
DependencyResourceRefusalReasonV1::Remote,
)
.unwrap();
let partial = partial.finish().unwrap();
assert!(matches!(
partial.coverage(),
DependencyClosureCoverageV1::Partial { .. }
));
assert!(partial.identity().is_none());
let mut explicit_null_identity = serde_json::to_value(&partial).unwrap();
explicit_null_identity["identity"] = serde_json::Value::Null;
let error = serde_json::from_value::<DependencyClosureV1>(explicit_null_identity)
.expect_err("incomplete closure identity must be absent, not null");
assert!(error.to_string().contains("invalid type: null"), "{error}");
let mut impossible_terminal = serde_json::to_value(&partial).unwrap();
impossible_terminal["work"]["inspected_references"] = serde_json::json!(2);
impossible_terminal["work"]["dedup_probes"] = serde_json::json!(2);
let error = serde_json::from_value::<DependencyClosureV1>(impossible_terminal)
.expect_err("non-budget partial work cannot claim N+1 inspection");
assert!(
error
.to_string()
.contains("dependency closure work counters are invalid"),
"unexpected partial terminal-work error: {error}"
);
let unavailable = DependencyClosureV1::unavailable(primary);
assert!(matches!(
unavailable.coverage(),
DependencyClosureCoverageV1::Unavailable { .. }
));
assert!(unavailable.identity().is_none());
}
#[test]
fn reference_limit_stops_at_n_plus_one_and_never_resumes() {
let primary = InputIdentity::from_bytes(b"primary");
let mut builder = DependencyClosureBuilderV1::new(
primary,
SourceSetCoverageV1::complete(),
DEPENDENCY_CLOSURE_V1_MAX_REFERENCES + 2,
);
for index in 0..DEPENDENCY_CLOSURE_V1_MAX_REFERENCES {
assert!(builder.begin_reference(0, 0));
builder
.push_primary(index, SourceResourceKindV1::Image, index as u64)
.unwrap();
}
assert!(!builder.begin_reference(0, 0));
assert!(!builder.begin_reference(0, 0));
let closure = builder.finish().unwrap();
assert_eq!(
closure.references().len(),
DEPENDENCY_CLOSURE_V1_MAX_REFERENCES
);
assert_eq!(
closure.work().inspected_references(),
DEPENDENCY_CLOSURE_V1_MAX_REFERENCES + 1
);
assert_eq!(
closure.work().dedup_probes(),
DEPENDENCY_CLOSURE_V1_MAX_DEDUP_PROBES + 1
);
assert!(matches!(
closure.coverage(),
DependencyClosureCoverageV1::Partial { reasons }
if reasons.contains(&DependencyClosureCoverageReasonV1::ResourceBudgetExceeded)
));
let wire = serde_json::to_value(&closure).unwrap();
let round_trip: DependencyClosureV1 =
serde_json::from_value(wire).expect("budget N+1 work reads back");
assert_eq!(round_trip, closure);
}
#[test]
fn closure_binding_checks_primary_and_exact_resource_prefix() {
let primary = InputIdentity::from_bytes(b"primary");
let source_rows = SourceFactSetV1::complete(vec![source_resource(
0,
SourceResourceKindV1::Image,
7,
SourceResourceLocatorV1::Embedded,
)]);
let mut builder = DependencyClosureBuilderV1::new(
primary.clone(),
source_rows.coverage(),
source_rows.rows().len(),
);
assert!(builder.begin_reference(0, 0));
builder
.push_primary(0, SourceResourceKindV1::Image, 7)
.unwrap();
let closure = builder.finish().unwrap();
closure
.validate_against(SourceFormatV1::Glb, &primary, &source_rows)
.unwrap();
assert_eq!(
closure.validate_against(
SourceFormatV1::Glb,
&InputIdentity::from_bytes(b"other"),
&source_rows,
),
Err(DependencyClosureError::PrimaryIdentityMismatch)
);
let mut wrong_target = closure.clone();
wrong_target.references[0].target = DependencyReferenceTargetV1::Refused {
key: None,
reason: DependencyResourceRefusalReasonV1::Remote,
};
assert_eq!(
wrong_target.validate_against(SourceFormatV1::Glb, &primary, &source_rows),
Err(DependencyClosureError::ResourceReferenceMismatch {
source_order_index: 0
})
);
}
#[test]
fn key_and_normalization_byte_limits_are_exact_and_terminal() {
let at_key_limit = "a".repeat(DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES);
assert!(
DependencyResourceKeyV1::from_source_str(
&at_key_limit,
ResourceKeySyntaxV1::ParserRelativePath
)
.is_ok()
);
let over_key_limit = "a".repeat(DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES + 1);
let error = DependencyResourceKeyV1::from_source_str(
&over_key_limit,
ResourceKeySyntaxV1::ParserRelativePath,
)
.unwrap_err();
assert_eq!(
error,
DependencyClosureError::ResourceKeyTooLong {
bytes: DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES + 1,
limit: DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES,
}
);
let rows =
DEPENDENCY_CLOSURE_V1_MAX_NORMALIZATION_BYTES / DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES;
let mut builder = DependencyClosureBuilderV1::new(
InputIdentity::from_bytes(b"primary"),
SourceSetCoverageV1::complete(),
rows + 1,
);
for index in 0..rows {
assert!(builder.begin_reference(DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES, 1));
builder
.push_primary(index, SourceResourceKindV1::Image, index as u64)
.unwrap();
}
assert!(!builder.begin_reference(1, 1));
assert!(!builder.begin_reference(0, 0));
let closure = builder.finish().unwrap();
assert_eq!(
closure.work().normalization_bytes_inspected(),
DEPENDENCY_CLOSURE_V1_MAX_NORMALIZATION_BYTES + 1
);
assert_eq!(closure.work().inspected_references(), rows + 1);
assert_eq!(closure.work().path_components_inspected(), rows + 1);
assert_eq!(closure.work().dedup_probes(), rows + 1);
}
#[test]
fn distinct_external_key_limit_stops_before_n_plus_one_open() {
let primary = InputIdentity::from_bytes(b"primary");
let mut builder = DependencyClosureBuilderV1::new(
primary,
SourceSetCoverageV1::complete(),
DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES + 1,
);
for index in 0..DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES {
assert!(builder.begin_reference(8, 1));
let key = DependencyResourceKeyV1::from_source_str(
&format!("r{index}.bin"),
ResourceKeySyntaxV1::ParserRelativePath,
)
.unwrap();
assert_eq!(builder.prepare_external_key(&key).unwrap(), Some(true));
builder.record_external_open_attempt(&key).unwrap();
assert!(
builder
.push_captured_external(
index,
SourceResourceKindV1::Buffer,
index as u64,
key,
InputIdentity::from_bytes(&[]),
)
.unwrap()
);
}
assert!(builder.begin_reference(8, 1));
let overflow = DependencyResourceKeyV1::from_source_str(
"overflow.bin",
ResourceKeySyntaxV1::ParserRelativePath,
)
.unwrap();
assert_eq!(builder.prepare_external_key(&overflow).unwrap(), None);
assert_eq!(
builder.record_external_open_attempt(&overflow),
Err(DependencyClosureError::ReferenceNotStarted)
);
let closure = builder.finish().unwrap();
assert_eq!(
closure.work().distinct_external_keys(),
DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES
);
assert_eq!(
closure.work().inspected_references(),
DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES + 1
);
assert_eq!(
closure.work().dedup_probes(),
DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES + 1
);
assert_eq!(
closure.work().external_open_attempts(),
DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES
);
assert_eq!(
closure.external_resources().len(),
DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES
);
}
#[test]
fn captured_external_identity_requires_the_recorded_same_capture_open() {
let mut builder = DependencyClosureBuilderV1::new(
InputIdentity::from_bytes(b"primary"),
SourceSetCoverageV1::complete(),
1,
);
assert!(builder.begin_reference(5, 1));
let key = DependencyResourceKeyV1::from_source_str(
"a.bin",
ResourceKeySyntaxV1::ParserRelativePath,
)
.unwrap();
assert_eq!(builder.prepare_external_key(&key).unwrap(), Some(true));
assert_eq!(
builder.push_captured_external(
0,
SourceResourceKindV1::Buffer,
0,
key.clone(),
InputIdentity::from_bytes(b"bytes"),
),
Err(DependencyClosureError::ExternalKeyNotOpened)
);
builder.record_external_open_attempt(&key).unwrap();
assert!(
builder
.push_captured_external(
0,
SourceResourceKindV1::Buffer,
0,
key,
InputIdentity::from_bytes(b"bytes"),
)
.unwrap()
);
}
#[test]
fn external_byte_limits_are_exact_without_allocating_fixture_payloads() {
let identity = |tag: u8, bytes| InputIdentity::from_sha256_digest([tag; 32], bytes);
let key = |index| {
DependencyResourceKeyV1::from_source_str(
&format!("r{index}.bin"),
ResourceKeySyntaxV1::ParserRelativePath,
)
.unwrap()
};
let mut exact = DependencyClosureBuilderV1::new(
InputIdentity::from_bytes(b"primary"),
SourceSetCoverageV1::complete(),
4,
);
for index in 0..4 {
assert!(exact.begin_reference(8, 1));
let key = key(index);
assert_eq!(exact.prepare_external_key(&key).unwrap(), Some(true));
exact.record_external_open_attempt(&key).unwrap();
assert!(
exact
.push_captured_external(
index,
SourceResourceKindV1::Buffer,
index as u64,
key,
identity(index as u8, DEPENDENCY_CLOSURE_V1_MAX_RESOURCE_BYTES),
)
.unwrap()
);
}
let exact = exact.finish().unwrap();
assert!(exact.coverage().is_complete());
assert_eq!(
exact.work().external_bytes_read_hashed(),
DEPENDENCY_CLOSURE_V1_MAX_TOTAL_RESOURCE_BYTES
);
let mut per_resource_over = DependencyClosureBuilderV1::new(
InputIdentity::from_bytes(b"primary"),
SourceSetCoverageV1::complete(),
1,
);
assert!(per_resource_over.begin_reference(8, 1));
let over_key = key(9);
assert_eq!(
per_resource_over.prepare_external_key(&over_key).unwrap(),
Some(true)
);
per_resource_over
.record_external_open_attempt(&over_key)
.unwrap();
assert!(
!per_resource_over
.push_captured_external(
0,
SourceResourceKindV1::Buffer,
0,
over_key,
identity(9, DEPENDENCY_CLOSURE_V1_MAX_RESOURCE_BYTES + 1),
)
.unwrap()
);
let per_resource_over = per_resource_over.finish().unwrap();
assert_eq!(per_resource_over.references().len(), 1);
assert!(matches!(
per_resource_over.references()[0].target(),
DependencyReferenceTargetV1::Unavailable {
reason: DependencyResourceUnavailableReasonV1::ResourceBudgetExceeded,
..
}
));
assert_eq!(
per_resource_over.work().external_bytes_read_hashed(),
DEPENDENCY_CLOSURE_V1_MAX_RESOURCE_BYTES + 1
);
assert!(matches!(
per_resource_over.coverage(),
DependencyClosureCoverageV1::Partial { reasons }
if reasons.contains(&DependencyClosureCoverageReasonV1::ResourceBudgetExceeded)
&& reasons.contains(&DependencyClosureCoverageReasonV1::UnavailableResource)
));
let mut aggregate_over = DependencyClosureBuilderV1::new(
InputIdentity::from_bytes(b"primary"),
SourceSetCoverageV1::complete(),
5,
);
for index in 0..4 {
assert!(aggregate_over.begin_reference(8, 1));
let key = key(index);
assert_eq!(
aggregate_over.prepare_external_key(&key).unwrap(),
Some(true)
);
aggregate_over.record_external_open_attempt(&key).unwrap();
assert!(
aggregate_over
.push_captured_external(
index,
SourceResourceKindV1::Buffer,
index as u64,
key,
identity(index as u8, DEPENDENCY_CLOSURE_V1_MAX_RESOURCE_BYTES),
)
.unwrap()
);
}
assert!(aggregate_over.begin_reference(8, 1));
let fifth = key(5);
assert_eq!(
aggregate_over.prepare_external_key(&fifth).unwrap(),
Some(true)
);
aggregate_over.record_external_open_attempt(&fifth).unwrap();
assert!(
!aggregate_over
.push_captured_external(4, SourceResourceKindV1::Buffer, 4, fifth, identity(5, 1),)
.unwrap()
);
let aggregate_over = aggregate_over.finish().unwrap();
assert_eq!(aggregate_over.references().len(), 5);
assert_eq!(aggregate_over.external_resources().len(), 4);
assert_eq!(aggregate_over.work().inspected_references(), 5);
assert_eq!(
aggregate_over.work().external_bytes_read_hashed(),
DEPENDENCY_CLOSURE_V1_MAX_TOTAL_RESOURCE_BYTES + 1
);
assert!(matches!(
aggregate_over.coverage(),
DependencyClosureCoverageV1::Partial { reasons }
if reasons.contains(&DependencyClosureCoverageReasonV1::ResourceBudgetExceeded)
));
}
#[test]
fn source_order_is_identity_bearing_while_external_rows_remain_key_sorted() {
fn closure(order: [(&str, u64); 2]) -> DependencyClosureV1 {
let mut builder = DependencyClosureBuilderV1::new(
InputIdentity::from_bytes(b"primary"),
SourceSetCoverageV1::complete(),
2,
);
for (source_order, (name, source_index)) in order.into_iter().enumerate() {
assert!(builder.begin_reference(name.len(), 1));
let key = DependencyResourceKeyV1::from_source_str(
name,
ResourceKeySyntaxV1::ParserRelativePath,
)
.unwrap();
assert_eq!(builder.prepare_external_key(&key).unwrap(), Some(true));
builder.record_external_open_attempt(&key).unwrap();
assert!(
builder
.push_captured_external(
source_order,
SourceResourceKindV1::Image,
source_index,
key,
InputIdentity::from_bytes(name.as_bytes()),
)
.unwrap()
);
}
builder.finish().unwrap()
}
let first = closure([("z.png", 0), ("a.png", 1)]);
let second = closure([("a.png", 1), ("z.png", 0)]);
assert_eq!(
first
.external_resources()
.iter()
.map(|row| row.key().as_str())
.collect::<Vec<_>>(),
vec!["a.png", "z.png"]
);
assert_ne!(first.identity(), second.identity());
}
#[test]
fn refused_and_unavailable_serialization_never_has_an_unsafe_spelling() {
let mut builder = DependencyClosureBuilderV1::new(
InputIdentity::from_bytes(b"primary"),
SourceSetCoverageV1::complete(),
2,
);
assert!(builder.begin_reference(0, 0));
builder
.push_refused(
0,
SourceResourceKindV1::Image,
0,
DependencyResourceRefusalReasonV1::Absolute,
)
.unwrap();
assert!(builder.begin_reference(8, 1));
let key = DependencyResourceKeyV1::from_source_str(
"safe.png",
ResourceKeySyntaxV1::ParserRelativePath,
)
.unwrap();
assert_eq!(builder.prepare_external_key(&key).unwrap(), Some(true));
builder
.push_unavailable(
1,
SourceResourceKindV1::Image,
1,
Some(key),
DependencyResourceUnavailableReasonV1::ResourceRootUnavailable,
)
.unwrap();
let closure = builder.finish().unwrap();
let json = serde_json::to_string(&closure).unwrap();
let debug = format!("{closure:?}");
for rendered in [json, debug] {
assert!(!rendered.contains("/home/private/secret.png"));
assert!(rendered.contains("safe.png"));
}
}
#[test]
fn resource_purpose_is_authoritatively_derived_and_serialized() {
let cases = [
(
SourceResourceKindV1::Buffer,
DependencyResourcePurposeV1::LoaderEssential,
"loader_essential",
),
(
SourceResourceKindV1::Image,
DependencyResourcePurposeV1::Nonessential,
"nonessential",
),
(
SourceResourceKindV1::Texture,
DependencyResourcePurposeV1::Nonessential,
"nonessential",
),
(
SourceResourceKindV1::Video,
DependencyResourcePurposeV1::TargetOnly,
"target_only",
),
(
SourceResourceKindV1::Cache,
DependencyResourcePurposeV1::TargetOnly,
"target_only",
),
];
let mut builder = DependencyClosureBuilderV1::new(
InputIdentity::from_bytes(b"primary"),
SourceSetCoverageV1::complete(),
cases.len(),
);
for (index, (kind, _, _)) in cases.iter().copied().enumerate() {
assert!(builder.begin_reference(0, 0));
builder.push_primary(index, kind, index as u64).unwrap();
}
let closure = builder.finish().unwrap();
let wire = serde_json::to_value(&closure).unwrap();
for (index, (_, purpose, spelling)) in cases.iter().copied().enumerate() {
assert_eq!(closure.references()[index].purpose(), purpose);
assert_eq!(wire["references"][index]["purpose"], spelling);
}
let mut mutated = closure.references.clone();
mutated[0].purpose = DependencyResourcePurposeV1::TargetOnly;
assert_ne!(
canonical_identity(
closure.primary_input(),
&mutated,
closure.external_resources(),
),
*closure.identity().unwrap()
);
}
#[test]
fn raw_relative_key_binding_uses_the_source_format_normalization() {
let primary = InputIdentity::from_bytes(b"primary");
let raw_locator = SourceResourceLocatorV1::classify("textures/a%20b.png");
let source_rows = SourceFactSetV1::complete(vec![source_resource(
0,
SourceResourceKindV1::Image,
0,
raw_locator,
)]);
let literal_key = DependencyResourceKeyV1::from_source_str(
"textures/a%20b.png",
ResourceKeySyntaxV1::ParserRelativePath,
)
.unwrap();
let mut builder = DependencyClosureBuilderV1::new(
primary.clone(),
source_rows.coverage(),
source_rows.rows().len(),
);
assert!(builder.begin_reference(20, 2));
assert_eq!(
builder.prepare_external_key(&literal_key).unwrap(),
Some(true)
);
builder.record_external_open_attempt(&literal_key).unwrap();
assert!(
builder
.push_captured_external(
0,
SourceResourceKindV1::Image,
0,
literal_key,
InputIdentity::from_bytes(b"image"),
)
.unwrap()
);
let closure = builder.finish().unwrap();
closure
.validate_against(SourceFormatV1::Fbx, &primary, &source_rows)
.unwrap();
assert_eq!(
closure.validate_against(SourceFormatV1::GltfJson, &primary, &source_rows),
Err(DependencyClosureError::ResourceKeyMismatch {
source_order_index: 0,
})
);
let wrong_rows = SourceFactSetV1::complete(vec![source_resource(
0,
SourceResourceKindV1::Image,
0,
SourceResourceLocatorV1::classify("textures/b%20b.png"),
)]);
assert_eq!(
closure.validate_against(SourceFormatV1::Fbx, &primary, &wrong_rows),
Err(DependencyClosureError::ResourceKeyMismatch {
source_order_index: 0,
})
);
}
#[test]
fn binding_rejects_a_wrong_missing_reason_and_wrong_source_coverage_reason() {
let primary = InputIdentity::from_bytes(b"primary");
let missing_rows = SourceFactSetV1::complete(vec![source_resource(
0,
SourceResourceKindV1::Image,
0,
SourceResourceLocatorV1::Missing,
)]);
let mut builder = DependencyClosureBuilderV1::new(
primary.clone(),
missing_rows.coverage(),
missing_rows.rows().len(),
);
assert!(builder.begin_reference(0, 0));
builder
.push_unavailable(
0,
SourceResourceKindV1::Image,
0,
None,
DependencyResourceUnavailableReasonV1::Missing,
)
.unwrap();
let closure = builder.finish().unwrap();
closure
.validate_against(SourceFormatV1::GltfJson, &primary, &missing_rows)
.unwrap();
let mut wrong_reason = closure.clone();
wrong_reason.references[0].target = DependencyReferenceTargetV1::Unavailable {
key: None,
reason: DependencyResourceUnavailableReasonV1::Unreadable,
};
assert_eq!(
wrong_reason.validate_against(SourceFormatV1::GltfJson, &primary, &missing_rows),
Err(DependencyClosureError::ResourceReferenceMismatch {
source_order_index: 0,
})
);
let complete_rows = SourceFactSetV1::<SourceResourceReferenceV1>::complete(Vec::new());
let mut wrong_coverage = DependencyClosureV1::capture_unavailable(
primary.clone(),
SourceSetCoverageV1::complete(),
);
wrong_coverage.coverage = DependencyClosureCoverageV1::Unavailable {
reasons: vec![
DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable,
DependencyClosureCoverageReasonV1::CaptureUnavailable,
],
};
assert_eq!(
wrong_coverage.validate_against(SourceFormatV1::GltfJson, &primary, &complete_rows,),
Err(DependencyClosureError::CoverageReasonMismatch)
);
}
#[test]
fn safe_symlink_refusal_retains_and_validates_only_the_normalized_key() {
let primary = InputIdentity::from_bytes(b"primary");
let source_rows = SourceFactSetV1::complete(vec![
source_resource(
0,
SourceResourceKindV1::Image,
0,
SourceResourceLocatorV1::classify("textures/a%20b.png"),
),
source_resource(
1,
SourceResourceKindV1::Image,
1,
SourceResourceLocatorV1::classify("textures/a b.png"),
),
]);
let key = DependencyResourceKeyV1::from_source_str(
"textures/a b.png",
ResourceKeySyntaxV1::GltfUri,
)
.unwrap();
let mut builder = DependencyClosureBuilderV1::new(
primary.clone(),
source_rows.coverage(),
source_rows.rows().len(),
);
for index in 0..2 {
assert!(builder.begin_reference(20, 2));
assert_eq!(
builder.prepare_external_key(&key).unwrap(),
Some(index == 0)
);
builder
.push_refused(
index,
SourceResourceKindV1::Image,
index as u64,
DependencyResourceRefusalReasonV1::Symlink,
)
.unwrap();
}
let closure = builder.finish().unwrap();
closure
.validate_against(SourceFormatV1::GltfJson, &primary, &source_rows)
.unwrap();
assert_eq!(closure.work().external_open_attempts(), 0);
let wire = serde_json::to_value(&closure).unwrap();
assert_eq!(wire["references"][0]["target"]["key"], "textures/a b.png");
assert_eq!(wire["references"][1]["target"]["key"], "textures/a b.png");
}
#[test]
fn builder_rejects_multiple_keys_and_cached_outcome_contradictions() {
let primary = InputIdentity::from_bytes(b"primary");
let first = DependencyResourceKeyV1::from_source_str(
"a.bin",
ResourceKeySyntaxV1::ParserRelativePath,
)
.unwrap();
let second = DependencyResourceKeyV1::from_source_str(
"b.bin",
ResourceKeySyntaxV1::ParserRelativePath,
)
.unwrap();
let mut unavailable =
DependencyClosureBuilderV1::new(primary.clone(), SourceSetCoverageV1::complete(), 2);
assert!(unavailable.begin_reference(5, 1));
assert_eq!(
unavailable.prepare_external_key(&first).unwrap(),
Some(true)
);
assert_eq!(
unavailable.prepare_external_key(&second),
Err(DependencyClosureError::ExternalKeyAlreadyPrepared)
);
assert_eq!(
unavailable.record_external_open_attempt(&second),
Err(DependencyClosureError::ExternalKeyMismatch)
);
assert_eq!(
unavailable.push_unavailable(
0,
SourceResourceKindV1::Image,
0,
Some(second),
DependencyResourceUnavailableReasonV1::Missing,
),
Err(DependencyClosureError::ExternalKeyMismatch)
);
unavailable
.push_unavailable(
0,
SourceResourceKindV1::Image,
0,
Some(first.clone()),
DependencyResourceUnavailableReasonV1::Missing,
)
.unwrap();
assert!(unavailable.begin_reference(5, 1));
assert_eq!(
unavailable.prepare_external_key(&first).unwrap(),
Some(false)
);
assert_eq!(
unavailable.push_unavailable(
1,
SourceResourceKindV1::Image,
1,
Some(first.clone()),
DependencyResourceUnavailableReasonV1::Unreadable,
),
Err(DependencyClosureError::ExternalOutcomeMismatch)
);
unavailable
.push_unavailable(
1,
SourceResourceKindV1::Image,
1,
Some(first.clone()),
DependencyResourceUnavailableReasonV1::Missing,
)
.unwrap();
unavailable.finish().unwrap();
let mut captured =
DependencyClosureBuilderV1::new(primary, SourceSetCoverageV1::complete(), 2);
assert!(captured.begin_reference(5, 1));
assert_eq!(captured.prepare_external_key(&first).unwrap(), Some(true));
captured.record_external_open_attempt(&first).unwrap();
assert!(
captured
.push_captured_external(
0,
SourceResourceKindV1::Buffer,
0,
first.clone(),
InputIdentity::from_bytes(b"one"),
)
.unwrap()
);
assert!(captured.begin_reference(5, 1));
assert_eq!(captured.prepare_external_key(&first).unwrap(), Some(false));
assert_eq!(
captured.push_captured_external(
1,
SourceResourceKindV1::Buffer,
1,
first.clone(),
InputIdentity::from_bytes(b"two"),
),
Err(DependencyClosureError::ExternalOutcomeMismatch)
);
assert_eq!(
captured.push_unavailable(
1,
SourceResourceKindV1::Buffer,
1,
Some(first.clone()),
DependencyResourceUnavailableReasonV1::Missing,
),
Err(DependencyClosureError::ExternalOutcomeMismatch)
);
captured
.push_external_alias(1, SourceResourceKindV1::Buffer, 1, first)
.unwrap();
captured.finish().unwrap();
}
#[test]
fn finish_requires_all_expected_rows_without_a_real_terminal_stop() {
let builder = DependencyClosureBuilderV1::new(
InputIdentity::from_bytes(b"primary"),
SourceSetCoverageV1::complete(),
1,
);
assert_eq!(
builder.finish(),
Err(DependencyClosureError::ReferenceCountMismatch {
expected: 1,
actual: 0,
})
);
}
#[test]
fn terminal_work_counters_retain_each_n_plus_one_witness() {
let mut path = DependencyClosureBuilderV1::new(
InputIdentity::from_bytes(b"primary"),
SourceSetCoverageV1::complete(),
1,
);
assert!(!path.begin_reference(
DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES,
DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS + 1,
));
assert!(!path.begin_reference(0, 0));
let path = path.finish().unwrap();
assert_eq!(path.work().inspected_references(), 1);
assert_eq!(
path.work().normalization_bytes_inspected(),
DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES
);
assert_eq!(
path.work().path_components_inspected(),
DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS + 1
);
assert_eq!(path.work().dedup_probes(), 1);
let mut locator = DependencyClosureBuilderV1::new(
InputIdentity::from_bytes(b"primary"),
SourceSetCoverageV1::complete(),
1,
);
assert!(!locator.begin_reference(DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES + 1, 1));
let locator = locator.finish().unwrap();
assert_eq!(
locator.work().normalization_bytes_inspected(),
DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES + 1
);
assert_eq!(locator.work().path_components_inspected(), 1);
assert_eq!(locator.work().dedup_probes(), 1);
}
#[test]
fn closure_wire_sequences_reject_n_plus_one_before_decoding_the_sentinel() {
let primary = InputIdentity::from_bytes(b"primary");
let mut builder =
DependencyClosureBuilderV1::new(primary, SourceSetCoverageV1::complete(), 1);
assert!(builder.begin_reference(0, 0));
builder
.push_primary(0, SourceResourceKindV1::Buffer, 0)
.unwrap();
let closure = builder.finish().unwrap();
let base = serde_json::to_value(&closure).unwrap();
let reference = base["references"][0].clone();
let mut references = Vec::with_capacity(DEPENDENCY_CLOSURE_V1_MAX_REFERENCES + 1);
for source_order_index in 0..DEPENDENCY_CLOSURE_V1_MAX_REFERENCES {
let mut row = reference.clone();
row["source_order_index"] = serde_json::json!(source_order_index);
references.push(row);
}
let mut exact = base.clone();
exact["references"] = references.clone().into();
let exact_error = decode_dependency_closure_v1(&serde_json::to_string(&exact).unwrap())
.expect_err("later closure semantics reject the synthetic exact-N prefix");
assert!(!matches!(
exact_error,
DependencyClosureDecodeError::Semantic(ref reason)
if reason == "dependency closure has too many references"
));
references.push(serde_json::Value::Null);
let mut over = base.clone();
over["references"] = references.into();
assert!(matches!(
decode_dependency_closure_v1(&serde_json::to_string(&over).unwrap()),
Err(DependencyClosureDecodeError::Semantic(reason))
if reason == "dependency closure has too many references"
));
let external = serde_json::json!({
"key": "a.bin",
"identity": {"sha256": "00".repeat(32), "bytes": 0}
});
let mut external_resources = vec![external; DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES];
let mut exact = base.clone();
exact["external_resources"] = external_resources.clone().into();
let exact_error = decode_dependency_closure_v1(&serde_json::to_string(&exact).unwrap())
.expect_err("later closure semantics reject duplicate exact-N resources");
assert!(!matches!(
exact_error,
DependencyClosureDecodeError::Semantic(ref reason)
if reason == "dependency closure has too many external resources"
));
external_resources.push(serde_json::Value::Null);
let mut over = base.clone();
over["external_resources"] = external_resources.into();
assert!(matches!(
decode_dependency_closure_v1(&serde_json::to_string(&over).unwrap()),
Err(DependencyClosureDecodeError::Semantic(reason))
if reason == "dependency closure has too many external resources"
));
let mut reasons = vec![
serde_json::json!("source_declarations_partial"),
serde_json::json!("source_declarations_unavailable"),
serde_json::json!("capture_unavailable"),
serde_json::json!("refused_resource"),
serde_json::json!("unavailable_resource"),
serde_json::json!("resource_budget_exceeded"),
serde_json::json!("unmodeled_resource_domain"),
];
reasons.push(serde_json::Value::Null);
let mut over = base;
over["coverage"] = serde_json::json!({"state": "partial", "reasons": reasons});
assert!(matches!(
decode_dependency_closure_v1(&serde_json::to_string(&over).unwrap()),
Err(DependencyClosureDecodeError::Semantic(reason))
if reason == "dependency coverage reasons must be strictly ordered"
));
}
#[test]
fn unmodeled_domain_prevents_complete_identity() {
let mut builder = DependencyClosureBuilderV1::new(
InputIdentity::from_bytes(b"primary"),
SourceSetCoverageV1::complete(),
0,
);
builder.mark_unmodeled_resource_domain();
let closure = builder.finish().unwrap();
assert!(matches!(
closure.coverage(),
DependencyClosureCoverageV1::Partial { reasons }
if reasons == &[DependencyClosureCoverageReasonV1::UnmodeledResourceDomain]
));
assert!(closure.identity().is_none());
}
#[test]
fn equal_content_at_distinct_keys_remains_two_resources() {
let mut builder = DependencyClosureBuilderV1::new(
InputIdentity::from_bytes(b"primary"),
SourceSetCoverageV1::complete(),
2,
);
for (index, name) in ["a.bin", "b.bin"].into_iter().enumerate() {
let key = DependencyResourceKeyV1::from_source_str(
name,
ResourceKeySyntaxV1::ParserRelativePath,
)
.unwrap();
assert!(builder.begin_reference(name.len(), 1));
assert_eq!(builder.prepare_external_key(&key).unwrap(), Some(true));
builder.record_external_open_attempt(&key).unwrap();
assert!(
builder
.push_captured_external(
index,
SourceResourceKindV1::Buffer,
index as u64,
key,
InputIdentity::from_bytes(b"same"),
)
.unwrap()
);
}
let closure = builder.finish().unwrap();
assert!(closure.coverage().is_complete());
assert_eq!(closure.external_resources().len(), 2);
assert_eq!(closure.work().external_open_attempts(), 2);
assert_eq!(
closure
.external_resources()
.iter()
.map(|resource| resource.identity())
.collect::<Vec<_>>(),
vec![
&InputIdentity::from_bytes(b"same"),
&InputIdentity::from_bytes(b"same"),
]
);
}
}