1use crate::bounded_deserialize::{CappedSequence, deserialize_capped_sequence};
8use crate::{
9 InputIdentity, SourceFactSetV1, SourceFormatV1, SourceRelativeLocatorV1, SourceResourceKindV1,
10 SourceResourceLocatorV1, SourceResourceReferenceV1, SourceSetCoverageStateV1,
11 SourceSetCoverageV1,
12};
13use serde::de::Error as _;
14use serde::{Deserialize, Deserializer, Serialize};
15use std::collections::{BTreeMap, BTreeSet};
16
17pub const DEPENDENCY_CLOSURE_V1_ID: &str = "urn:animsmith:dependency-closure:1";
19pub const DEPENDENCY_CLOSURE_BUDGET_V1_ID: &str = "urn:animsmith:dependency-closure-budget:1";
21pub const DEPENDENCY_CLOSURE_V1_MAX_REFERENCES: usize = 4_096;
23pub const DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES: usize = 1_024;
25pub const DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES: usize = 4_096;
27pub const DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS: usize = 128;
29pub const DEPENDENCY_CLOSURE_V1_MAX_NORMALIZATION_BYTES: usize = 8 * 1024 * 1024;
31pub const DEPENDENCY_CLOSURE_V1_MAX_RESOURCE_BYTES: u64 = 64 * 1024 * 1024;
33pub const DEPENDENCY_CLOSURE_V1_MAX_TOTAL_RESOURCE_BYTES: u64 = 256 * 1024 * 1024;
35pub const DEPENDENCY_CLOSURE_V1_MAX_DEDUP_PROBES: usize = 4_096;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
40pub struct ResourceClosureBudgetV1 {
41 schema: &'static str,
42 max_references: usize,
43 max_external_resources: usize,
44 max_key_bytes: usize,
45 max_path_components: usize,
46 max_normalization_bytes: usize,
47 max_resource_bytes: u64,
48 max_total_resource_bytes: u64,
49 max_dedup_probes: usize,
50}
51
52impl<'de> Deserialize<'de> for ResourceClosureBudgetV1 {
53 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
54 where
55 D: Deserializer<'de>,
56 {
57 #[derive(Deserialize)]
58 #[serde(deny_unknown_fields)]
59 struct WireBudget {
60 schema: String,
61 max_references: usize,
62 max_external_resources: usize,
63 max_key_bytes: usize,
64 max_path_components: usize,
65 max_normalization_bytes: usize,
66 max_resource_bytes: u64,
67 max_total_resource_bytes: u64,
68 max_dedup_probes: usize,
69 }
70 let wire = WireBudget::deserialize(deserializer)?;
71 let expected = Self::VALUE;
72 if wire.schema != expected.schema
73 || wire.max_references != expected.max_references
74 || wire.max_external_resources != expected.max_external_resources
75 || wire.max_key_bytes != expected.max_key_bytes
76 || wire.max_path_components != expected.max_path_components
77 || wire.max_normalization_bytes != expected.max_normalization_bytes
78 || wire.max_resource_bytes != expected.max_resource_bytes
79 || wire.max_total_resource_bytes != expected.max_total_resource_bytes
80 || wire.max_dedup_probes != expected.max_dedup_probes
81 {
82 return Err(D::Error::custom(
83 "dependency-closure budget must equal immutable V1",
84 ));
85 }
86 Ok(expected)
87 }
88}
89
90impl ResourceClosureBudgetV1 {
91 pub const VALUE: Self = Self {
93 schema: DEPENDENCY_CLOSURE_BUDGET_V1_ID,
94 max_references: DEPENDENCY_CLOSURE_V1_MAX_REFERENCES,
95 max_external_resources: DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES,
96 max_key_bytes: DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES,
97 max_path_components: DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS,
98 max_normalization_bytes: DEPENDENCY_CLOSURE_V1_MAX_NORMALIZATION_BYTES,
99 max_resource_bytes: DEPENDENCY_CLOSURE_V1_MAX_RESOURCE_BYTES,
100 max_total_resource_bytes: DEPENDENCY_CLOSURE_V1_MAX_TOTAL_RESOURCE_BYTES,
101 max_dedup_probes: DEPENDENCY_CLOSURE_V1_MAX_DEDUP_PROBES,
102 };
103
104 pub const fn contract_id(self) -> &'static str {
106 DEPENDENCY_CLOSURE_BUDGET_V1_ID
107 }
108
109 pub const fn max_references(self) -> usize {
111 self.max_references
112 }
113
114 pub const fn max_external_resources(self) -> usize {
116 self.max_external_resources
117 }
118
119 pub const fn max_key_bytes(self) -> usize {
121 self.max_key_bytes
122 }
123
124 pub const fn max_path_components(self) -> usize {
126 self.max_path_components
127 }
128
129 pub const fn max_normalization_bytes(self) -> usize {
131 self.max_normalization_bytes
132 }
133
134 pub const fn max_resource_bytes(self) -> u64 {
136 self.max_resource_bytes
137 }
138
139 pub const fn max_total_resource_bytes(self) -> u64 {
141 self.max_total_resource_bytes
142 }
143
144 pub const fn max_dedup_probes(self) -> usize {
146 self.max_dedup_probes
147 }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
152#[serde(rename_all = "snake_case")]
153pub enum DependencyResourcePurposeV1 {
154 LoaderEssential,
156 Nonessential,
158 TargetOnly,
160}
161
162impl DependencyResourcePurposeV1 {
163 const fn from_kind(kind: SourceResourceKindV1) -> Self {
164 match kind {
165 SourceResourceKindV1::Buffer => Self::LoaderEssential,
166 SourceResourceKindV1::Image | SourceResourceKindV1::Texture => Self::Nonessential,
167 SourceResourceKindV1::Video | SourceResourceKindV1::Cache => Self::TargetOnly,
168 }
169 }
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub enum ResourceKeySyntaxV1 {
175 GltfUri,
177 ParserRelativePath,
179}
180
181#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
183#[serde(transparent)]
184pub struct DependencyResourceKeyV1(String);
185
186impl DependencyResourceKeyV1 {
187 pub fn from_relative(
194 locator: &SourceRelativeLocatorV1,
195 syntax: ResourceKeySyntaxV1,
196 ) -> Result<Self, DependencyClosureError> {
197 Self::from_source_str(locator.as_str(), syntax)
198 }
199
200 pub fn from_source_str(
211 raw: &str,
212 syntax: ResourceKeySyntaxV1,
213 ) -> Result<Self, DependencyClosureError> {
214 if raw.len() > DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES {
215 return Err(DependencyClosureError::ResourceKeyTooLong {
216 bytes: raw.len(),
217 limit: DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES,
218 });
219 }
220 if raw.contains('\\') || raw.chars().any(char::is_control) {
221 return Err(DependencyClosureError::InvalidResourceKey);
222 }
223 let normalized = match syntax {
224 ResourceKeySyntaxV1::GltfUri => decode_percent_utf8(raw)?,
225 ResourceKeySyntaxV1::ParserRelativePath => raw.to_owned(),
226 };
227 validate_normalized_key(&normalized)?;
228 Ok(Self(normalized))
229 }
230
231 pub fn as_str(&self) -> &str {
233 &self.0
234 }
235
236 pub fn source_component_count(locator: &SourceRelativeLocatorV1) -> usize {
238 locator.as_str().split('/').count()
239 }
240}
241
242impl<'de> Deserialize<'de> for DependencyResourceKeyV1 {
243 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
244 where
245 D: Deserializer<'de>,
246 {
247 Self::from_source_str(
248 &String::deserialize(deserializer)?,
249 ResourceKeySyntaxV1::ParserRelativePath,
250 )
251 .map_err(D::Error::custom)
252 }
253}
254
255fn decode_percent_utf8(raw: &str) -> Result<String, DependencyClosureError> {
256 let bytes = raw.as_bytes();
257 let mut decoded = Vec::with_capacity(bytes.len());
258 let mut index = 0usize;
259 while index < bytes.len() {
260 if bytes[index] != b'%' {
261 decoded.push(bytes[index]);
262 index += 1;
263 continue;
264 }
265 let high = bytes
266 .get(index + 1)
267 .copied()
268 .and_then(hex)
269 .ok_or(DependencyClosureError::InvalidResourceKey)?;
270 let low = bytes
271 .get(index + 2)
272 .copied()
273 .and_then(hex)
274 .ok_or(DependencyClosureError::InvalidResourceKey)?;
275 let value = (high << 4) | low;
276 if matches!(value, b'/' | b'\\' | 0) {
277 return Err(DependencyClosureError::InvalidResourceKey);
278 }
279 decoded.push(value);
280 index += 3;
281 }
282 String::from_utf8(decoded).map_err(|_| DependencyClosureError::InvalidResourceKey)
283}
284
285fn hex(value: u8) -> Option<u8> {
286 match value {
287 b'0'..=b'9' => Some(value - b'0'),
288 b'a'..=b'f' => Some(value - b'a' + 10),
289 b'A'..=b'F' => Some(value - b'A' + 10),
290 _ => None,
291 }
292}
293
294fn validate_normalized_key(value: &str) -> Result<(), DependencyClosureError> {
295 if value.is_empty()
296 || value.len() > DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES
297 || value.starts_with('/')
298 || value.contains('\\')
299 || value.contains([':', '?', '#'])
300 || value.chars().any(char::is_control)
301 || has_uri_scheme(value)
302 {
303 return Err(DependencyClosureError::InvalidResourceKey);
304 }
305 let mut components = 0usize;
306 for component in value.split('/') {
307 components = components.saturating_add(1);
308 if component.is_empty() || matches!(component, "." | "..") {
309 return Err(DependencyClosureError::InvalidResourceKey);
310 }
311 }
312 if components > DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS {
313 return Err(DependencyClosureError::TooManyPathComponents {
314 components,
315 limit: DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS,
316 });
317 }
318 Ok(())
319}
320
321fn has_uri_scheme(value: &str) -> bool {
322 let Some((scheme, _)) = value.split_once(':') else {
323 return false;
324 };
325 !scheme.is_empty()
326 && scheme.as_bytes()[0].is_ascii_alphabetic()
327 && scheme
328 .bytes()
329 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.'))
330}
331
332#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
334#[serde(rename_all = "snake_case")]
335pub enum DependencyResourceRefusalReasonV1 {
336 Absolute,
338 Escaping,
340 Remote,
342 Malformed,
344 Oversized,
346 Symlink,
348}
349
350#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
352#[serde(rename_all = "snake_case")]
353pub enum DependencyResourceUnavailableReasonV1 {
354 ResourceRootUnavailable,
356 Missing,
358 Unreadable,
360 ResourceBudgetExceeded,
362}
363
364#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
366#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
367pub enum DependencyReferenceTargetV1 {
368 Primary,
370 External {
372 key: DependencyResourceKeyV1,
374 },
375 Refused {
377 #[serde(skip_serializing_if = "Option::is_none")]
379 key: Option<DependencyResourceKeyV1>,
380 reason: DependencyResourceRefusalReasonV1,
382 },
383 Unavailable {
385 #[serde(skip_serializing_if = "Option::is_none")]
387 key: Option<DependencyResourceKeyV1>,
388 reason: DependencyResourceUnavailableReasonV1,
390 },
391}
392
393#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
395pub struct DependencyClosureReferenceV1 {
396 source_order_index: usize,
397 kind: SourceResourceKindV1,
398 purpose: DependencyResourcePurposeV1,
399 source_index: u64,
400 target: DependencyReferenceTargetV1,
401}
402
403impl<'de> Deserialize<'de> for DependencyClosureReferenceV1 {
404 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
405 where
406 D: Deserializer<'de>,
407 {
408 #[derive(Debug, Clone, Copy, Deserialize)]
409 #[serde(rename_all = "snake_case")]
410 enum WireResourceKind {
411 Buffer,
412 Image,
413 Texture,
414 Video,
415 Cache,
416 }
417 impl From<WireResourceKind> for SourceResourceKindV1 {
418 fn from(value: WireResourceKind) -> Self {
419 match value {
420 WireResourceKind::Buffer => Self::Buffer,
421 WireResourceKind::Image => Self::Image,
422 WireResourceKind::Texture => Self::Texture,
423 WireResourceKind::Video => Self::Video,
424 WireResourceKind::Cache => Self::Cache,
425 }
426 }
427 }
428 #[derive(Deserialize)]
429 #[serde(deny_unknown_fields)]
430 struct WireReference {
431 source_order_index: usize,
432 kind: WireResourceKind,
433 purpose: DependencyResourcePurposeV1,
434 source_index: u64,
435 target: DependencyReferenceTargetV1,
436 }
437 let wire = WireReference::deserialize(deserializer)?;
438 let kind = SourceResourceKindV1::from(wire.kind);
439 let reference = Self::new(
440 wire.source_order_index,
441 kind,
442 wire.source_index,
443 wire.target,
444 );
445 if reference.purpose != wire.purpose {
446 return Err(D::Error::custom(
447 "dependency reference purpose must be derived from kind",
448 ));
449 }
450 Ok(reference)
451 }
452}
453
454impl DependencyClosureReferenceV1 {
455 fn new(
456 source_order_index: usize,
457 kind: SourceResourceKindV1,
458 source_index: u64,
459 target: DependencyReferenceTargetV1,
460 ) -> Self {
461 Self {
462 source_order_index,
463 kind,
464 purpose: DependencyResourcePurposeV1::from_kind(kind),
465 source_index,
466 target,
467 }
468 }
469
470 pub const fn source_order_index(&self) -> usize {
472 self.source_order_index
473 }
474
475 pub const fn kind(&self) -> SourceResourceKindV1 {
477 self.kind
478 }
479
480 pub const fn purpose(&self) -> DependencyResourcePurposeV1 {
482 self.purpose
483 }
484
485 pub const fn source_index(&self) -> u64 {
487 self.source_index
488 }
489
490 pub const fn target(&self) -> &DependencyReferenceTargetV1 {
492 &self.target
493 }
494}
495
496#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
498#[serde(deny_unknown_fields)]
499pub struct ExternalResourceIdentityV1 {
500 key: DependencyResourceKeyV1,
501 identity: InputIdentity,
502}
503
504impl ExternalResourceIdentityV1 {
505 pub const fn key(&self) -> &DependencyResourceKeyV1 {
507 &self.key
508 }
509
510 pub const fn identity(&self) -> &InputIdentity {
512 &self.identity
513 }
514}
515
516#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
518#[serde(rename_all = "snake_case")]
519pub enum DependencyClosureCoverageReasonV1 {
520 SourceDeclarationsPartial,
522 SourceDeclarationsUnavailable,
524 CaptureUnavailable,
526 RefusedResource,
528 UnavailableResource,
530 ResourceBudgetExceeded,
532 UnmodeledResourceDomain,
534}
535
536#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
538#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
539pub enum DependencyClosureCoverageV1 {
540 Complete,
542 Partial {
544 reasons: Vec<DependencyClosureCoverageReasonV1>,
546 },
547 Unavailable {
549 reasons: Vec<DependencyClosureCoverageReasonV1>,
551 },
552}
553
554const DEPENDENCY_CLOSURE_COVERAGE_REASON_VARIANTS: usize = 7;
555
556#[derive(Deserialize)]
557#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
558enum DependencyClosureCoverageWireV1 {
559 Complete,
560 Partial {
561 #[serde(deserialize_with = "deserialize_closure_coverage_reasons")]
562 reasons: CappedSequence<DependencyClosureCoverageReasonV1>,
563 },
564 Unavailable {
565 #[serde(deserialize_with = "deserialize_closure_coverage_reasons")]
566 reasons: CappedSequence<DependencyClosureCoverageReasonV1>,
567 },
568}
569
570impl DependencyClosureCoverageWireV1 {
571 fn into_value(self) -> (DependencyClosureCoverageV1, bool) {
572 match self {
573 Self::Complete => (DependencyClosureCoverageV1::Complete, false),
574 Self::Partial { reasons } => (
575 DependencyClosureCoverageV1::Partial {
576 reasons: reasons.values,
577 },
578 reasons.overflowed,
579 ),
580 Self::Unavailable { reasons } => (
581 DependencyClosureCoverageV1::Unavailable {
582 reasons: reasons.values,
583 },
584 reasons.overflowed,
585 ),
586 }
587 }
588}
589
590impl<'de> Deserialize<'de> for DependencyClosureCoverageV1 {
591 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
592 where
593 D: Deserializer<'de>,
594 {
595 let (coverage, overflowed) =
596 DependencyClosureCoverageWireV1::deserialize(deserializer)?.into_value();
597 if overflowed {
598 return Err(D::Error::custom(
599 "dependency coverage reasons must be strictly ordered",
600 ));
601 }
602 Ok(coverage)
603 }
604}
605
606impl DependencyClosureCoverageV1 {
607 pub fn reasons(&self) -> &[DependencyClosureCoverageReasonV1] {
609 match self {
610 Self::Complete => &[],
611 Self::Partial { reasons } | Self::Unavailable { reasons } => reasons,
612 }
613 }
614
615 pub const fn is_complete(&self) -> bool {
617 matches!(self, Self::Complete)
618 }
619}
620
621#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
623#[serde(transparent)]
624pub struct DependencyClosureIdentityV1(InputIdentity);
625
626impl DependencyClosureIdentityV1 {
627 pub const fn input_identity(&self) -> &InputIdentity {
629 &self.0
630 }
631}
632
633#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
635#[serde(deny_unknown_fields)]
636pub struct DependencyClosureWorkV1 {
637 inspected_references: usize,
638 retained_references: usize,
639 normalization_bytes_inspected: usize,
640 path_components_inspected: usize,
641 dedup_probes: usize,
642 external_open_attempts: usize,
643 distinct_external_keys: usize,
644 captured_external_resources: usize,
645 external_bytes_read_hashed: u64,
646}
647
648impl DependencyClosureWorkV1 {
649 pub const fn inspected_references(self) -> usize {
651 self.inspected_references
652 }
653
654 pub const fn retained_references(self) -> usize {
656 self.retained_references
657 }
658
659 pub const fn normalization_bytes_inspected(self) -> usize {
661 self.normalization_bytes_inspected
662 }
663
664 pub const fn path_components_inspected(self) -> usize {
666 self.path_components_inspected
667 }
668
669 pub const fn dedup_probes(self) -> usize {
671 self.dedup_probes
672 }
673
674 pub const fn external_open_attempts(self) -> usize {
676 self.external_open_attempts
677 }
678
679 pub const fn distinct_external_keys(self) -> usize {
681 self.distinct_external_keys
682 }
683
684 pub const fn captured_external_resources(self) -> usize {
686 self.captured_external_resources
687 }
688
689 pub const fn external_bytes_read_hashed(self) -> u64 {
691 self.external_bytes_read_hashed
692 }
693}
694
695#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
697pub struct DependencyClosureV1 {
698 schema: &'static str,
699 budget: ResourceClosureBudgetV1,
700 primary_input: InputIdentity,
701 coverage: DependencyClosureCoverageV1,
702 #[serde(skip_serializing_if = "Option::is_none")]
703 identity: Option<DependencyClosureIdentityV1>,
704 references: Vec<DependencyClosureReferenceV1>,
705 external_resources: Vec<ExternalResourceIdentityV1>,
706 work: DependencyClosureWorkV1,
707}
708
709#[derive(Deserialize)]
710#[serde(deny_unknown_fields)]
711struct DependencyClosureWireV1 {
712 schema: String,
713 budget: ResourceClosureBudgetV1,
714 primary_input: InputIdentity,
715 coverage: DependencyClosureCoverageWireV1,
716 #[serde(default, deserialize_with = "deserialize_optional_non_null")]
717 identity: OptionalNonNull<DependencyClosureIdentityV1>,
718 #[serde(deserialize_with = "deserialize_closure_references")]
719 references: CappedSequence<DependencyClosureReferenceV1>,
720 #[serde(deserialize_with = "deserialize_closure_external_resources")]
721 external_resources: CappedSequence<ExternalResourceIdentityV1>,
722 work: DependencyClosureWorkV1,
723}
724
725fn deserialize_closure_references<'de, D>(
726 deserializer: D,
727) -> Result<CappedSequence<DependencyClosureReferenceV1>, D::Error>
728where
729 D: Deserializer<'de>,
730{
731 deserialize_capped_sequence(deserializer, DEPENDENCY_CLOSURE_V1_MAX_REFERENCES)
732}
733
734fn deserialize_closure_external_resources<'de, D>(
735 deserializer: D,
736) -> Result<CappedSequence<ExternalResourceIdentityV1>, D::Error>
737where
738 D: Deserializer<'de>,
739{
740 deserialize_capped_sequence(deserializer, DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES)
741}
742
743fn deserialize_closure_coverage_reasons<'de, D>(
744 deserializer: D,
745) -> Result<CappedSequence<DependencyClosureCoverageReasonV1>, D::Error>
746where
747 D: Deserializer<'de>,
748{
749 deserialize_capped_sequence(deserializer, DEPENDENCY_CLOSURE_COVERAGE_REASON_VARIANTS)
750}
751
752#[derive(Debug, Default)]
753enum OptionalNonNull<T> {
754 #[default]
755 Missing,
756 Present(T),
757}
758
759fn deserialize_optional_non_null<'de, D, T>(deserializer: D) -> Result<OptionalNonNull<T>, D::Error>
760where
761 D: Deserializer<'de>,
762 T: Deserialize<'de>,
763{
764 T::deserialize(deserializer).map(OptionalNonNull::Present)
765}
766
767#[derive(Debug)]
768pub(crate) enum DependencyClosureDecodeError {
769 Shape(serde_json::Error),
770 Semantic(String),
771}
772
773pub(crate) fn decode_dependency_closure_v1(
774 raw: &str,
775) -> Result<DependencyClosureV1, DependencyClosureDecodeError> {
776 let wire = serde_json::from_str(raw).map_err(DependencyClosureDecodeError::Shape)?;
777 DependencyClosureV1::from_wire(wire).map_err(DependencyClosureDecodeError::Semantic)
778}
779
780impl<'de> Deserialize<'de> for DependencyClosureV1 {
781 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
782 where
783 D: Deserializer<'de>,
784 {
785 Self::from_wire(DependencyClosureWireV1::deserialize(deserializer)?)
786 .map_err(D::Error::custom)
787 }
788}
789
790impl DependencyClosureV1 {
791 fn from_wire(wire: DependencyClosureWireV1) -> Result<Self, String> {
792 if wire.schema != DEPENDENCY_CLOSURE_V1_ID {
793 return Err(format!(
794 "dependency closure schema must be {DEPENDENCY_CLOSURE_V1_ID:?}"
795 ));
796 }
797 if wire.budget != ResourceClosureBudgetV1::VALUE {
798 return Err("dependency closure budget is not immutable V1".to_owned());
799 }
800 if wire.references.overflowed {
801 return Err("dependency closure has too many references".to_owned());
802 }
803 if wire.external_resources.overflowed {
804 return Err("dependency closure has too many external resources".to_owned());
805 }
806 let (coverage, coverage_reasons_overflowed) = wire.coverage.into_value();
807 let closure = Self {
808 schema: DEPENDENCY_CLOSURE_V1_ID,
809 budget: wire.budget,
810 primary_input: wire.primary_input,
811 coverage,
812 identity: match wire.identity {
813 OptionalNonNull::Missing => None,
814 OptionalNonNull::Present(identity) => Some(identity),
815 },
816 references: wire.references.values,
817 external_resources: wire.external_resources.values,
818 work: wire.work,
819 };
820 closure.validate_wire(coverage_reasons_overflowed)?;
821 Ok(closure)
822 }
823
824 pub fn unavailable(primary_input: InputIdentity) -> Self {
826 Self {
827 schema: DEPENDENCY_CLOSURE_V1_ID,
828 budget: ResourceClosureBudgetV1::VALUE,
829 primary_input,
830 coverage: DependencyClosureCoverageV1::Unavailable {
831 reasons: vec![DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable],
832 },
833 identity: None,
834 references: Vec::new(),
835 external_resources: Vec::new(),
836 work: DependencyClosureWorkV1::default(),
837 }
838 }
839
840 pub(crate) fn capture_unavailable(
841 primary_input: InputIdentity,
842 source_coverage: SourceSetCoverageV1,
843 ) -> Self {
844 let mut reasons = Vec::with_capacity(2);
845 match source_coverage.state() {
846 SourceSetCoverageStateV1::Complete => {}
847 SourceSetCoverageStateV1::Partial => {
848 reasons.push(DependencyClosureCoverageReasonV1::SourceDeclarationsPartial);
849 }
850 SourceSetCoverageStateV1::Unavailable => {
851 reasons.push(DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable);
852 }
853 }
854 reasons.push(DependencyClosureCoverageReasonV1::CaptureUnavailable);
855 reasons.sort_unstable();
856 Self {
857 schema: DEPENDENCY_CLOSURE_V1_ID,
858 budget: ResourceClosureBudgetV1::VALUE,
859 primary_input,
860 coverage: DependencyClosureCoverageV1::Unavailable { reasons },
861 identity: None,
862 references: Vec::new(),
863 external_resources: Vec::new(),
864 work: DependencyClosureWorkV1::default(),
865 }
866 }
867
868 pub const fn contract_id(&self) -> &'static str {
870 DEPENDENCY_CLOSURE_V1_ID
871 }
872
873 pub const fn budget(&self) -> ResourceClosureBudgetV1 {
875 self.budget
876 }
877
878 pub const fn primary_input(&self) -> &InputIdentity {
880 &self.primary_input
881 }
882
883 pub const fn coverage(&self) -> &DependencyClosureCoverageV1 {
885 &self.coverage
886 }
887
888 pub const fn identity(&self) -> Option<&DependencyClosureIdentityV1> {
890 self.identity.as_ref()
891 }
892
893 pub fn references(&self) -> &[DependencyClosureReferenceV1] {
895 &self.references
896 }
897
898 pub fn external_resources(&self) -> &[ExternalResourceIdentityV1] {
900 &self.external_resources
901 }
902
903 pub const fn work(&self) -> DependencyClosureWorkV1 {
905 self.work
906 }
907
908 pub fn record_identity(&self) -> InputIdentity {
915 canonical_record_identity(self)
916 }
917
918 fn validate_wire(&self, coverage_reasons_overflowed: bool) -> Result<(), String> {
919 if self.budget != ResourceClosureBudgetV1::VALUE {
920 return Err("dependency closure budget is not immutable V1".to_owned());
921 }
922 if self.references.len() > DEPENDENCY_CLOSURE_V1_MAX_REFERENCES {
923 return Err("dependency closure has too many references".to_owned());
924 }
925 if self.external_resources.len() > DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES {
926 return Err("dependency closure has too many external resources".to_owned());
927 }
928 for (expected, reference) in self.references.iter().enumerate() {
929 if reference.source_order_index != expected {
930 return Err(format!(
931 "dependency reference order {} is not expected prefix index {expected}",
932 reference.source_order_index
933 ));
934 }
935 if reference.purpose != DependencyResourcePurposeV1::from_kind(reference.kind) {
936 return Err("dependency reference purpose disagrees with kind".to_owned());
937 }
938 match &reference.target {
939 DependencyReferenceTargetV1::Refused {
940 key: Some(_),
941 reason: DependencyResourceRefusalReasonV1::Symlink,
942 }
943 | DependencyReferenceTargetV1::Refused {
944 key: None,
945 reason:
946 DependencyResourceRefusalReasonV1::Absolute
947 | DependencyResourceRefusalReasonV1::Escaping
948 | DependencyResourceRefusalReasonV1::Remote
949 | DependencyResourceRefusalReasonV1::Malformed
950 | DependencyResourceRefusalReasonV1::Oversized,
951 } => {}
952 DependencyReferenceTargetV1::Refused { .. } => {
953 return Err(
954 "dependency refused target has an invalid key/reason pair".to_owned()
955 );
956 }
957 _ => {}
958 }
959 }
960 if self
961 .external_resources
962 .windows(2)
963 .any(|rows| rows[0].key >= rows[1].key)
964 {
965 return Err("dependency external resources must be strictly key ordered".to_owned());
966 }
967 let reasons = self.coverage.reasons();
968 if coverage_reasons_overflowed || reasons.windows(2).any(|rows| rows[0] >= rows[1]) {
969 return Err("dependency coverage reasons must be strictly ordered".to_owned());
970 }
971 match &self.coverage {
972 DependencyClosureCoverageV1::Complete if !reasons.is_empty() => {
973 return Err("complete dependency closure cannot have reasons".to_owned());
974 }
975 DependencyClosureCoverageV1::Partial { .. }
976 | DependencyClosureCoverageV1::Unavailable { .. }
977 if reasons.is_empty() =>
978 {
979 return Err("incomplete dependency closure requires reasons".to_owned());
980 }
981 _ => {}
982 }
983 if self.coverage.is_complete()
984 && self.references.iter().any(|reference| {
985 !matches!(
986 reference.target,
987 DependencyReferenceTargetV1::Primary
988 | DependencyReferenceTargetV1::External { .. }
989 )
990 })
991 {
992 return Err("complete dependency closure has an incomplete target".to_owned());
993 }
994 for reference in &self.references {
995 if let DependencyReferenceTargetV1::External { key } = &reference.target
996 && self
997 .external_resources
998 .binary_search_by(|resource| resource.key.cmp(key))
999 .is_err()
1000 {
1001 return Err("dependency reference names an absent external row".to_owned());
1002 }
1003 }
1004 for resource in &self.external_resources {
1005 if !self.references.iter().any(|reference| {
1006 matches!(
1007 &reference.target,
1008 DependencyReferenceTargetV1::External { key } if key == &resource.key
1009 )
1010 }) {
1011 return Err("dependency external row is not referenced".to_owned());
1012 }
1013 }
1014 let has_refused = self.references.iter().any(|reference| {
1015 matches!(
1016 reference.target,
1017 DependencyReferenceTargetV1::Refused { .. }
1018 )
1019 });
1020 let has_unavailable = self.references.iter().any(|reference| {
1021 matches!(
1022 reference.target,
1023 DependencyReferenceTargetV1::Unavailable { .. }
1024 )
1025 });
1026 if has_refused != reasons.contains(&DependencyClosureCoverageReasonV1::RefusedResource)
1027 || has_unavailable
1028 != reasons.contains(&DependencyClosureCoverageReasonV1::UnavailableResource)
1029 {
1030 return Err(
1031 "dependency closure target states disagree with coverage reasons".to_owned(),
1032 );
1033 }
1034 if reasons.contains(&DependencyClosureCoverageReasonV1::CaptureUnavailable)
1035 && (!matches!(
1036 self.coverage,
1037 DependencyClosureCoverageV1::Unavailable { .. }
1038 ) || !self.references.is_empty()
1039 || !self.external_resources.is_empty())
1040 {
1041 return Err(
1042 "capture-unavailable dependency closure must retain no closure rows".to_owned(),
1043 );
1044 }
1045 if self.coverage.is_complete() != self.identity.is_some() {
1046 return Err(
1047 "dependency closure identity must be present exactly for complete coverage"
1048 .to_owned(),
1049 );
1050 }
1051 if let Some(identity) = &self.identity
1052 && identity
1053 != &canonical_identity(
1054 &self.primary_input,
1055 &self.references,
1056 &self.external_resources,
1057 )
1058 {
1059 return Err("dependency closure identity does not match its preimage".to_owned());
1060 }
1061 let work = self.work;
1062 let retained_or_terminal = self.references.len()..=self.references.len().saturating_add(1);
1063 let mut target_keys = BTreeSet::new();
1064 for reference in &self.references {
1065 match &reference.target {
1066 DependencyReferenceTargetV1::External { key }
1067 | DependencyReferenceTargetV1::Refused { key: Some(key), .. }
1068 | DependencyReferenceTargetV1::Unavailable { key: Some(key), .. } => {
1069 target_keys.insert(key);
1070 }
1071 DependencyReferenceTargetV1::Primary
1072 | DependencyReferenceTargetV1::Refused { key: None, .. }
1073 | DependencyReferenceTargetV1::Unavailable { key: None, .. } => {}
1074 }
1075 }
1076 let captured_bytes = self
1077 .external_resources
1078 .iter()
1079 .try_fold(0_u64, |total, row| total.checked_add(row.identity.bytes()));
1080 if work.inspected_references > DEPENDENCY_CLOSURE_V1_MAX_REFERENCES + 1
1081 || !retained_or_terminal.contains(&work.inspected_references)
1082 || (work.inspected_references != self.references.len()
1083 && !reasons.contains(&DependencyClosureCoverageReasonV1::ResourceBudgetExceeded))
1084 || work.retained_references != self.references.len()
1085 || work.normalization_bytes_inspected
1086 > DEPENDENCY_CLOSURE_V1_MAX_NORMALIZATION_BYTES + 1
1087 || work.dedup_probes > DEPENDENCY_CLOSURE_V1_MAX_DEDUP_PROBES + 1
1088 || work.dedup_probes != work.inspected_references
1089 || work.path_components_inspected
1090 > DEPENDENCY_CLOSURE_V1_MAX_REFERENCES
1091 .saturating_mul(DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS)
1092 .saturating_add(DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS + 1)
1093 || work.external_open_attempts > DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES
1094 || work.distinct_external_keys != target_keys.len()
1095 || work.external_open_attempts < self.external_resources.len()
1096 || work.external_open_attempts > work.distinct_external_keys
1097 || work.captured_external_resources != self.external_resources.len()
1098 || captured_bytes.is_none()
1099 || work.external_bytes_read_hashed < captured_bytes.unwrap_or(u64::MAX)
1100 || (work.external_bytes_read_hashed != captured_bytes.unwrap_or(u64::MAX)
1101 && !reasons.contains(&DependencyClosureCoverageReasonV1::ResourceBudgetExceeded))
1102 || work.external_bytes_read_hashed
1103 > DEPENDENCY_CLOSURE_V1_MAX_TOTAL_RESOURCE_BYTES.saturating_add(1)
1104 {
1105 return Err("dependency closure work counters are invalid".to_owned());
1106 }
1107 Ok(())
1108 }
1109
1110 pub(crate) fn validate_against(
1111 &self,
1112 format: SourceFormatV1,
1113 primary: &InputIdentity,
1114 resources: &SourceFactSetV1<SourceResourceReferenceV1>,
1115 ) -> Result<(), DependencyClosureError> {
1116 if &self.primary_input != primary {
1117 return Err(DependencyClosureError::PrimaryIdentityMismatch);
1118 }
1119 if self.references.len() > resources.rows().len() {
1120 return Err(DependencyClosureError::ResourceReferenceCountMismatch {
1121 facts: resources.rows().len(),
1122 closure: self.references.len(),
1123 });
1124 }
1125 for (closure, source) in self.references.iter().zip(resources.rows()) {
1126 if closure.source_order_index != source.source_order_index()
1127 || closure.kind != source.kind()
1128 || closure.purpose != DependencyResourcePurposeV1::from_kind(source.kind())
1129 || closure.source_index != source.source_index()
1130 {
1131 return Err(DependencyClosureError::ResourceReferenceMismatch {
1132 source_order_index: closure.source_order_index,
1133 });
1134 }
1135 validate_target_against_locator(
1136 format,
1137 closure.source_order_index,
1138 &closure.target,
1139 source.locator(),
1140 )?;
1141 }
1142 if self.coverage.is_complete()
1143 && (!matches!(
1144 resources.coverage().state(),
1145 SourceSetCoverageStateV1::Complete
1146 ) || self.references.len() != resources.rows().len())
1147 {
1148 return Err(DependencyClosureError::CompleteCoverageMismatch);
1149 }
1150 if matches!(
1151 resources.coverage().state(),
1152 SourceSetCoverageStateV1::Unavailable
1153 ) && (!self.references.is_empty()
1154 || !matches!(
1155 self.coverage,
1156 DependencyClosureCoverageV1::Unavailable { .. }
1157 ))
1158 {
1159 return Err(DependencyClosureError::UnavailableCoverageMismatch);
1160 }
1161 let reasons = self.coverage.reasons();
1162 let source_reason_matches = match resources.coverage().state() {
1163 SourceSetCoverageStateV1::Complete => {
1164 !reasons.contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsPartial)
1165 && !reasons
1166 .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable)
1167 }
1168 SourceSetCoverageStateV1::Partial => {
1169 reasons.contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsPartial)
1170 && !reasons
1171 .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable)
1172 }
1173 SourceSetCoverageStateV1::Unavailable => {
1174 reasons.contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable)
1175 && !reasons
1176 .contains(&DependencyClosureCoverageReasonV1::SourceDeclarationsPartial)
1177 }
1178 };
1179 let capture_reason_matches = !reasons
1180 .contains(&DependencyClosureCoverageReasonV1::CaptureUnavailable)
1181 || (matches!(
1182 self.coverage,
1183 DependencyClosureCoverageV1::Unavailable { .. }
1184 ) && self.references.is_empty()
1185 && self.external_resources.is_empty()
1186 && self.identity.is_none());
1187 if !source_reason_matches || !capture_reason_matches {
1188 return Err(DependencyClosureError::CoverageReasonMismatch);
1189 }
1190 if self.coverage.is_complete() != self.identity.is_some() {
1191 return Err(DependencyClosureError::ClosureIdentityCoverageMismatch);
1192 }
1193 Ok(())
1194 }
1195}
1196
1197fn validate_target_against_locator(
1198 format: SourceFormatV1,
1199 source_order_index: usize,
1200 target: &DependencyReferenceTargetV1,
1201 locator: &SourceResourceLocatorV1,
1202) -> Result<(), DependencyClosureError> {
1203 let matches = match locator {
1204 SourceResourceLocatorV1::Relative(locator) => {
1205 let syntax = match format {
1206 SourceFormatV1::GltfJson | SourceFormatV1::Glb => ResourceKeySyntaxV1::GltfUri,
1207 SourceFormatV1::Fbx => ResourceKeySyntaxV1::ParserRelativePath,
1208 };
1209 match DependencyResourceKeyV1::from_relative(locator, syntax) {
1210 Ok(expected) => match target {
1211 DependencyReferenceTargetV1::External { key }
1212 | DependencyReferenceTargetV1::Unavailable { key: Some(key), .. } => {
1213 if key != &expected {
1214 return Err(DependencyClosureError::ResourceKeyMismatch {
1215 source_order_index,
1216 });
1217 }
1218 true
1219 }
1220 DependencyReferenceTargetV1::Refused {
1221 key: Some(key),
1222 reason: DependencyResourceRefusalReasonV1::Symlink,
1223 } => {
1224 if key != &expected {
1225 return Err(DependencyClosureError::ResourceKeyMismatch {
1226 source_order_index,
1227 });
1228 }
1229 true
1230 }
1231 _ => false,
1232 },
1233 Err(
1234 DependencyClosureError::ResourceKeyTooLong { .. }
1235 | DependencyClosureError::TooManyPathComponents { .. },
1236 ) => matches!(
1237 target,
1238 DependencyReferenceTargetV1::Refused {
1239 key: None,
1240 reason: DependencyResourceRefusalReasonV1::Oversized,
1241 }
1242 ),
1243 Err(DependencyClosureError::InvalidResourceKey) => matches!(
1244 target,
1245 DependencyReferenceTargetV1::Refused {
1246 key: None,
1247 reason: DependencyResourceRefusalReasonV1::Malformed,
1248 }
1249 ),
1250 Err(_) => false,
1251 }
1252 }
1253 _ => matches!(
1254 (target, locator),
1255 (
1256 DependencyReferenceTargetV1::Primary,
1257 SourceResourceLocatorV1::Embedded | SourceResourceLocatorV1::DataUri
1258 ) | (
1259 DependencyReferenceTargetV1::Unavailable {
1260 key: None,
1261 reason: DependencyResourceUnavailableReasonV1::Missing,
1262 },
1263 SourceResourceLocatorV1::Missing
1264 ) | (
1265 DependencyReferenceTargetV1::Refused {
1266 key: None,
1267 reason: DependencyResourceRefusalReasonV1::Absolute
1268 },
1269 SourceResourceLocatorV1::Absolute
1270 ) | (
1271 DependencyReferenceTargetV1::Refused {
1272 key: None,
1273 reason: DependencyResourceRefusalReasonV1::Escaping
1274 },
1275 SourceResourceLocatorV1::Escaping
1276 ) | (
1277 DependencyReferenceTargetV1::Refused {
1278 key: None,
1279 reason: DependencyResourceRefusalReasonV1::Remote
1280 },
1281 SourceResourceLocatorV1::Remote
1282 ) | (
1283 DependencyReferenceTargetV1::Refused {
1284 key: None,
1285 reason: DependencyResourceRefusalReasonV1::Malformed
1286 },
1287 SourceResourceLocatorV1::Malformed
1288 ) | (
1289 DependencyReferenceTargetV1::Refused {
1290 key: None,
1291 reason: DependencyResourceRefusalReasonV1::Oversized
1292 },
1293 SourceResourceLocatorV1::Oversized
1294 )
1295 ),
1296 };
1297 if matches {
1298 Ok(())
1299 } else {
1300 Err(DependencyClosureError::ResourceReferenceMismatch { source_order_index })
1301 }
1302}
1303
1304#[derive(Debug, Clone, PartialEq, Eq)]
1305struct PendingReferenceV1 {
1306 external_key: Option<DependencyResourceKeyV1>,
1307}
1308
1309#[derive(Debug, Clone, PartialEq, Eq)]
1310enum CachedExternalOutcomeV1 {
1311 Captured(InputIdentity),
1312 Refused(DependencyResourceRefusalReasonV1),
1313 Unavailable(DependencyResourceUnavailableReasonV1),
1314}
1315
1316pub struct DependencyClosureBuilderV1 {
1318 primary_input: InputIdentity,
1319 source_coverage: SourceSetCoverageV1,
1320 expected_references: usize,
1321 references: Vec<DependencyClosureReferenceV1>,
1322 external_resources: BTreeMap<DependencyResourceKeyV1, InputIdentity>,
1323 external_keys: BTreeSet<DependencyResourceKeyV1>,
1324 external_outcomes: BTreeMap<DependencyResourceKeyV1, CachedExternalOutcomeV1>,
1325 opened_external_keys: BTreeSet<DependencyResourceKeyV1>,
1326 reasons: BTreeSet<DependencyClosureCoverageReasonV1>,
1327 work: DependencyClosureWorkV1,
1328 pending_reference: Option<PendingReferenceV1>,
1329 stopped: bool,
1330 unmodeled_domain: bool,
1331}
1332
1333impl DependencyClosureBuilderV1 {
1334 pub fn new(
1336 primary_input: InputIdentity,
1337 source_coverage: SourceSetCoverageV1,
1338 expected_references: usize,
1339 ) -> Self {
1340 let mut reasons = BTreeSet::new();
1341 match source_coverage.state() {
1342 SourceSetCoverageStateV1::Complete => {}
1343 SourceSetCoverageStateV1::Partial => {
1344 reasons.insert(DependencyClosureCoverageReasonV1::SourceDeclarationsPartial);
1345 }
1346 SourceSetCoverageStateV1::Unavailable => {
1347 reasons.insert(DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable);
1348 }
1349 }
1350 Self {
1351 primary_input,
1352 source_coverage,
1353 expected_references,
1354 references: Vec::with_capacity(
1355 expected_references.min(DEPENDENCY_CLOSURE_V1_MAX_REFERENCES),
1356 ),
1357 external_resources: BTreeMap::new(),
1358 external_keys: BTreeSet::new(),
1359 external_outcomes: BTreeMap::new(),
1360 opened_external_keys: BTreeSet::new(),
1361 reasons,
1362 work: DependencyClosureWorkV1::default(),
1363 pending_reference: None,
1364 stopped: false,
1365 unmodeled_domain: false,
1366 }
1367 }
1368
1369 pub const fn primary_input(&self) -> &InputIdentity {
1371 &self.primary_input
1372 }
1373
1374 pub const fn max_resource_bytes(&self) -> u64 {
1376 DEPENDENCY_CLOSURE_V1_MAX_RESOURCE_BYTES
1377 }
1378
1379 pub const fn remaining_external_bytes(&self) -> u64 {
1381 DEPENDENCY_CLOSURE_V1_MAX_TOTAL_RESOURCE_BYTES
1382 .saturating_sub(self.work.external_bytes_read_hashed)
1383 }
1384
1385 pub fn external_identity(&self, key: &DependencyResourceKeyV1) -> Option<&InputIdentity> {
1387 self.external_resources.get(key)
1388 }
1389
1390 pub fn begin_reference(&mut self, locator_bytes: usize, path_components: usize) -> bool {
1394 if self.stopped || self.pending_reference.is_some() {
1395 return false;
1396 }
1397 self.work.inspected_references = bounded_add(
1398 self.work.inspected_references,
1399 1,
1400 DEPENDENCY_CLOSURE_V1_MAX_REFERENCES,
1401 );
1402 self.work.normalization_bytes_inspected = bounded_add(
1403 self.work.normalization_bytes_inspected,
1404 locator_bytes,
1405 DEPENDENCY_CLOSURE_V1_MAX_NORMALIZATION_BYTES,
1406 );
1407 self.work.path_components_inspected = self
1408 .work
1409 .path_components_inspected
1410 .saturating_add(path_components.min(DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS + 1));
1411 self.work.dedup_probes = bounded_add(
1412 self.work.dedup_probes,
1413 1,
1414 DEPENDENCY_CLOSURE_V1_MAX_DEDUP_PROBES,
1415 );
1416 if self.references.len() >= DEPENDENCY_CLOSURE_V1_MAX_REFERENCES
1417 || locator_bytes > DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES
1418 || path_components > DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS
1419 || self.work.normalization_bytes_inspected
1420 > DEPENDENCY_CLOSURE_V1_MAX_NORMALIZATION_BYTES
1421 || self.work.dedup_probes > DEPENDENCY_CLOSURE_V1_MAX_DEDUP_PROBES
1422 {
1423 self.stop_for_budget();
1424 return false;
1425 }
1426 self.pending_reference = Some(PendingReferenceV1 { external_key: None });
1427 true
1428 }
1429
1430 pub fn prepare_external_key(
1435 &mut self,
1436 key: &DependencyResourceKeyV1,
1437 ) -> Result<Option<bool>, DependencyClosureError> {
1438 let pending = self
1439 .pending_reference
1440 .as_mut()
1441 .ok_or(DependencyClosureError::ReferenceNotStarted)?;
1442 if pending.external_key.is_some() {
1443 return Err(DependencyClosureError::ExternalKeyAlreadyPrepared);
1444 }
1445 if self.external_keys.contains(key) {
1446 if !self.external_outcomes.contains_key(key) {
1447 return Err(DependencyClosureError::ExternalOutcomeMissing);
1448 }
1449 pending.external_key = Some(key.clone());
1450 return Ok(Some(false));
1451 }
1452 if self.external_keys.len() >= DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES {
1453 self.pending_reference = None;
1454 self.stop_for_budget();
1455 return Ok(None);
1456 }
1457 self.external_keys.insert(key.clone());
1458 pending.external_key = Some(key.clone());
1459 self.work.distinct_external_keys = self.work.distinct_external_keys.saturating_add(1);
1460 Ok(Some(true))
1461 }
1462
1463 pub fn record_external_open_attempt(
1470 &mut self,
1471 key: &DependencyResourceKeyV1,
1472 ) -> Result<(), DependencyClosureError> {
1473 self.require_pending_key(key)?;
1474 if self.external_outcomes.contains_key(key) {
1475 return Err(DependencyClosureError::ExternalOutcomeMismatch);
1476 }
1477 if !self.opened_external_keys.insert(key.clone()) {
1478 return Err(DependencyClosureError::DuplicateExternalOpen);
1479 }
1480 self.work.external_open_attempts = self.work.external_open_attempts.saturating_add(1);
1481 Ok(())
1482 }
1483
1484 pub fn push_primary(
1486 &mut self,
1487 source_order_index: usize,
1488 kind: SourceResourceKindV1,
1489 source_index: u64,
1490 ) -> Result<(), DependencyClosureError> {
1491 self.require_reference_order(source_order_index)?;
1492 self.require_no_pending_key()?;
1493 self.push_reference(DependencyClosureReferenceV1::new(
1494 source_order_index,
1495 kind,
1496 source_index,
1497 DependencyReferenceTargetV1::Primary,
1498 ))
1499 }
1500
1501 pub fn push_refused(
1503 &mut self,
1504 source_order_index: usize,
1505 kind: SourceResourceKindV1,
1506 source_index: u64,
1507 reason: DependencyResourceRefusalReasonV1,
1508 ) -> Result<(), DependencyClosureError> {
1509 self.require_reference_order(source_order_index)?;
1510 let prepared_key = self.pending_external_key()?.cloned();
1511 let key = match prepared_key {
1512 Some(key) if reason == DependencyResourceRefusalReasonV1::Symlink => {
1513 match self.external_outcomes.get(&key) {
1514 Some(CachedExternalOutcomeV1::Refused(cached)) if *cached == reason => {}
1515 Some(_) => return Err(DependencyClosureError::ExternalOutcomeMismatch),
1516 None => {
1517 self.external_outcomes
1518 .insert(key.clone(), CachedExternalOutcomeV1::Refused(reason));
1519 }
1520 }
1521 Some(key)
1522 }
1523 Some(_) => return Err(DependencyClosureError::ExternalOutcomeMismatch),
1524 None if reason == DependencyResourceRefusalReasonV1::Symlink => {
1525 return Err(DependencyClosureError::ExternalKeyNotPrepared);
1526 }
1527 None => None,
1528 };
1529 self.reasons
1530 .insert(DependencyClosureCoverageReasonV1::RefusedResource);
1531 self.push_reference(DependencyClosureReferenceV1::new(
1532 source_order_index,
1533 kind,
1534 source_index,
1535 DependencyReferenceTargetV1::Refused { key, reason },
1536 ))
1537 }
1538
1539 pub fn push_unavailable(
1541 &mut self,
1542 source_order_index: usize,
1543 kind: SourceResourceKindV1,
1544 source_index: u64,
1545 key: Option<DependencyResourceKeyV1>,
1546 reason: DependencyResourceUnavailableReasonV1,
1547 ) -> Result<(), DependencyClosureError> {
1548 self.require_reference_order(source_order_index)?;
1549 match &key {
1550 Some(key) => {
1551 self.require_pending_key(key)?;
1552 match self.external_outcomes.get(key) {
1553 Some(CachedExternalOutcomeV1::Unavailable(cached)) if *cached == reason => {}
1554 Some(_) => return Err(DependencyClosureError::ExternalOutcomeMismatch),
1555 None => {
1556 self.external_outcomes
1557 .insert(key.clone(), CachedExternalOutcomeV1::Unavailable(reason));
1558 }
1559 }
1560 }
1561 None => self.require_no_pending_key()?,
1562 }
1563 self.reasons
1564 .insert(DependencyClosureCoverageReasonV1::UnavailableResource);
1565 self.push_reference(DependencyClosureReferenceV1::new(
1566 source_order_index,
1567 kind,
1568 source_index,
1569 DependencyReferenceTargetV1::Unavailable { key, reason },
1570 ))
1571 }
1572
1573 pub fn push_external_alias(
1575 &mut self,
1576 source_order_index: usize,
1577 kind: SourceResourceKindV1,
1578 source_index: u64,
1579 key: DependencyResourceKeyV1,
1580 ) -> Result<(), DependencyClosureError> {
1581 self.require_reference_order(source_order_index)?;
1582 self.require_pending_key(&key)?;
1583 match self.external_outcomes.get(&key) {
1584 Some(CachedExternalOutcomeV1::Captured(identity))
1585 if self.external_resources.get(&key) == Some(identity) => {}
1586 Some(_) => return Err(DependencyClosureError::ExternalOutcomeMismatch),
1587 None => return Err(DependencyClosureError::ExternalIdentityMissing),
1588 }
1589 self.push_external_reference(source_order_index, kind, source_index, key)
1590 }
1591
1592 pub fn push_captured_external(
1597 &mut self,
1598 source_order_index: usize,
1599 kind: SourceResourceKindV1,
1600 source_index: u64,
1601 key: DependencyResourceKeyV1,
1602 identity: InputIdentity,
1603 ) -> Result<bool, DependencyClosureError> {
1604 self.require_reference_order(source_order_index)?;
1605 self.require_pending_key(&key)?;
1606 if self.external_outcomes.contains_key(&key) || self.external_resources.contains_key(&key) {
1607 return Err(DependencyClosureError::ExternalOutcomeMismatch);
1608 }
1609 if !self.opened_external_keys.contains(&key) {
1610 return Err(DependencyClosureError::ExternalKeyNotOpened);
1611 }
1612 let bytes = identity.bytes();
1613 let next_total = self.work.external_bytes_read_hashed.checked_add(bytes);
1614 if bytes > DEPENDENCY_CLOSURE_V1_MAX_RESOURCE_BYTES
1615 || next_total.is_none_or(|total| total > DEPENDENCY_CLOSURE_V1_MAX_TOTAL_RESOURCE_BYTES)
1616 {
1617 let bounded_observed = bytes.min(DEPENDENCY_CLOSURE_V1_MAX_RESOURCE_BYTES + 1);
1618 self.work.external_bytes_read_hashed = self
1619 .work
1620 .external_bytes_read_hashed
1621 .saturating_add(bounded_observed)
1622 .min(DEPENDENCY_CLOSURE_V1_MAX_TOTAL_RESOURCE_BYTES + 1);
1623 self.push_unavailable(
1624 source_order_index,
1625 kind,
1626 source_index,
1627 Some(key),
1628 DependencyResourceUnavailableReasonV1::ResourceBudgetExceeded,
1629 )?;
1630 self.stop_for_budget();
1631 return Ok(false);
1632 }
1633 self.work.external_bytes_read_hashed = next_total.unwrap_or(u64::MAX);
1634 self.work.captured_external_resources =
1635 self.work.captured_external_resources.saturating_add(1);
1636 self.external_resources
1637 .insert(key.clone(), identity.clone());
1638 self.external_outcomes
1639 .insert(key.clone(), CachedExternalOutcomeV1::Captured(identity));
1640 self.push_external_reference(source_order_index, kind, source_index, key)?;
1641 Ok(true)
1642 }
1643
1644 pub fn mark_unmodeled_resource_domain(&mut self) {
1646 self.unmodeled_domain = true;
1647 self.reasons
1648 .insert(DependencyClosureCoverageReasonV1::UnmodeledResourceDomain);
1649 }
1650
1651 pub fn finish(self) -> Result<DependencyClosureV1, DependencyClosureError> {
1658 if self.pending_reference.is_some() {
1659 return Err(DependencyClosureError::UnfinishedReference);
1660 }
1661 if self.references.len() != self.expected_references && !self.stopped {
1662 return Err(DependencyClosureError::ReferenceCountMismatch {
1663 expected: self.expected_references,
1664 actual: self.references.len(),
1665 });
1666 }
1667 if self.references.len() > self.expected_references {
1668 return Err(DependencyClosureError::ReferenceCountMismatch {
1669 expected: self.expected_references,
1670 actual: self.references.len(),
1671 });
1672 }
1673 let coverage = match self.source_coverage.state() {
1674 SourceSetCoverageStateV1::Unavailable => DependencyClosureCoverageV1::Unavailable {
1675 reasons: self.reasons.into_iter().collect(),
1676 },
1677 SourceSetCoverageStateV1::Complete
1678 if self.reasons.is_empty()
1679 && !self.unmodeled_domain
1680 && self.references.len() == self.expected_references
1681 && self.references.iter().all(|reference| {
1682 matches!(
1683 reference.target,
1684 DependencyReferenceTargetV1::Primary
1685 | DependencyReferenceTargetV1::External { .. }
1686 )
1687 }) =>
1688 {
1689 DependencyClosureCoverageV1::Complete
1690 }
1691 _ => DependencyClosureCoverageV1::Partial {
1692 reasons: self.reasons.into_iter().collect(),
1693 },
1694 };
1695 let external_resources = self
1696 .external_resources
1697 .into_iter()
1698 .map(|(key, identity)| ExternalResourceIdentityV1 { key, identity })
1699 .collect::<Vec<_>>();
1700 let identity = coverage.is_complete().then(|| {
1701 canonical_identity(&self.primary_input, &self.references, &external_resources)
1702 });
1703 Ok(DependencyClosureV1 {
1704 schema: DEPENDENCY_CLOSURE_V1_ID,
1705 budget: ResourceClosureBudgetV1::VALUE,
1706 primary_input: self.primary_input,
1707 coverage,
1708 identity,
1709 references: self.references,
1710 external_resources,
1711 work: self.work,
1712 })
1713 }
1714
1715 fn push_external_reference(
1716 &mut self,
1717 source_order_index: usize,
1718 kind: SourceResourceKindV1,
1719 source_index: u64,
1720 key: DependencyResourceKeyV1,
1721 ) -> Result<(), DependencyClosureError> {
1722 self.push_reference(DependencyClosureReferenceV1::new(
1723 source_order_index,
1724 kind,
1725 source_index,
1726 DependencyReferenceTargetV1::External { key },
1727 ))
1728 }
1729
1730 fn push_reference(
1731 &mut self,
1732 reference: DependencyClosureReferenceV1,
1733 ) -> Result<(), DependencyClosureError> {
1734 if self.pending_reference.is_none() {
1735 return Err(DependencyClosureError::ReferenceNotStarted);
1736 }
1737 let expected = self.references.len();
1738 if reference.source_order_index != expected {
1739 return Err(DependencyClosureError::NonCanonicalReferenceOrder {
1740 expected,
1741 actual: reference.source_order_index,
1742 });
1743 }
1744 self.pending_reference = None;
1745 self.work.retained_references = self.work.retained_references.saturating_add(1);
1746 self.references.push(reference);
1747 Ok(())
1748 }
1749
1750 fn stop_for_budget(&mut self) {
1751 self.reasons
1752 .insert(DependencyClosureCoverageReasonV1::ResourceBudgetExceeded);
1753 self.stopped = true;
1754 }
1755
1756 fn pending_external_key(
1757 &self,
1758 ) -> Result<Option<&DependencyResourceKeyV1>, DependencyClosureError> {
1759 self.pending_reference
1760 .as_ref()
1761 .map(|pending| pending.external_key.as_ref())
1762 .ok_or(DependencyClosureError::ReferenceNotStarted)
1763 }
1764
1765 fn require_no_pending_key(&self) -> Result<(), DependencyClosureError> {
1766 match self.pending_external_key()? {
1767 None => Ok(()),
1768 Some(_) => Err(DependencyClosureError::ExternalOutcomeMismatch),
1769 }
1770 }
1771
1772 fn require_pending_key(
1773 &self,
1774 key: &DependencyResourceKeyV1,
1775 ) -> Result<(), DependencyClosureError> {
1776 match self.pending_external_key()? {
1777 Some(pending) if pending == key => Ok(()),
1778 Some(_) => Err(DependencyClosureError::ExternalKeyMismatch),
1779 None => Err(DependencyClosureError::ExternalKeyNotPrepared),
1780 }
1781 }
1782
1783 fn require_reference_order(
1784 &self,
1785 source_order_index: usize,
1786 ) -> Result<(), DependencyClosureError> {
1787 let expected = self.references.len();
1788 if source_order_index == expected {
1789 Ok(())
1790 } else {
1791 Err(DependencyClosureError::NonCanonicalReferenceOrder {
1792 expected,
1793 actual: source_order_index,
1794 })
1795 }
1796 }
1797}
1798
1799fn bounded_add(current: usize, observed: usize, limit: usize) -> usize {
1800 current
1801 .saturating_add(observed)
1802 .min(limit.saturating_add(1))
1803}
1804
1805fn canonical_identity(
1806 primary: &InputIdentity,
1807 references: &[DependencyClosureReferenceV1],
1808 resources: &[ExternalResourceIdentityV1],
1809) -> DependencyClosureIdentityV1 {
1810 let mut bytes = Vec::new();
1811 encode_text(&mut bytes, DEPENDENCY_CLOSURE_V1_ID);
1812 encode_text(&mut bytes, DEPENDENCY_CLOSURE_BUDGET_V1_ID);
1813 encode_u64(&mut bytes, DEPENDENCY_CLOSURE_V1_MAX_REFERENCES as u64);
1814 encode_u64(
1815 &mut bytes,
1816 DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES as u64,
1817 );
1818 encode_u64(&mut bytes, DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES as u64);
1819 encode_u64(&mut bytes, DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS as u64);
1820 encode_u64(
1821 &mut bytes,
1822 DEPENDENCY_CLOSURE_V1_MAX_NORMALIZATION_BYTES as u64,
1823 );
1824 encode_u64(&mut bytes, DEPENDENCY_CLOSURE_V1_MAX_RESOURCE_BYTES);
1825 encode_u64(&mut bytes, DEPENDENCY_CLOSURE_V1_MAX_TOTAL_RESOURCE_BYTES);
1826 encode_u64(&mut bytes, DEPENDENCY_CLOSURE_V1_MAX_DEDUP_PROBES as u64);
1827 bytes.push(0); encode_identity(&mut bytes, primary);
1829 encode_references(&mut bytes, references, |bytes, target| match target {
1830 DependencyReferenceTargetV1::Primary => bytes.push(0),
1831 DependencyReferenceTargetV1::External { key } => {
1832 bytes.push(1);
1833 encode_text(bytes, key.as_str());
1834 }
1835 DependencyReferenceTargetV1::Refused { .. }
1836 | DependencyReferenceTargetV1::Unavailable { .. } => {
1837 unreachable!("only complete reference targets enter closure identity")
1838 }
1839 });
1840 encode_external_resources(&mut bytes, resources);
1841 DependencyClosureIdentityV1(InputIdentity::from_bytes(&bytes))
1842}
1843
1844fn canonical_record_identity(closure: &DependencyClosureV1) -> InputIdentity {
1845 let mut bytes = Vec::new();
1846 encode_text(&mut bytes, "animsmith-dependency-closure-record-v1");
1847 encode_text(&mut bytes, closure.schema);
1848 encode_text(&mut bytes, closure.budget.schema);
1849 encode_u64(&mut bytes, closure.budget.max_references as u64);
1850 encode_u64(&mut bytes, closure.budget.max_external_resources as u64);
1851 encode_u64(&mut bytes, closure.budget.max_key_bytes as u64);
1852 encode_u64(&mut bytes, closure.budget.max_path_components as u64);
1853 encode_u64(&mut bytes, closure.budget.max_normalization_bytes as u64);
1854 encode_u64(&mut bytes, closure.budget.max_resource_bytes);
1855 encode_u64(&mut bytes, closure.budget.max_total_resource_bytes);
1856 encode_u64(&mut bytes, closure.budget.max_dedup_probes as u64);
1857 encode_identity(&mut bytes, &closure.primary_input);
1858 match &closure.coverage {
1859 DependencyClosureCoverageV1::Complete => bytes.push(0),
1860 DependencyClosureCoverageV1::Partial { reasons } => {
1861 bytes.push(1);
1862 encode_u64(&mut bytes, reasons.len() as u64);
1863 for reason in reasons {
1864 bytes.push(coverage_reason_tag(*reason));
1865 }
1866 }
1867 DependencyClosureCoverageV1::Unavailable { reasons } => {
1868 bytes.push(2);
1869 encode_u64(&mut bytes, reasons.len() as u64);
1870 for reason in reasons {
1871 bytes.push(coverage_reason_tag(*reason));
1872 }
1873 }
1874 }
1875 match &closure.identity {
1876 Some(identity) => {
1877 bytes.push(1);
1878 encode_identity(&mut bytes, identity.input_identity());
1879 }
1880 None => bytes.push(0),
1881 }
1882 encode_references(
1883 &mut bytes,
1884 &closure.references,
1885 |bytes, target| match target {
1886 DependencyReferenceTargetV1::Primary => bytes.push(0),
1887 DependencyReferenceTargetV1::External { key } => {
1888 bytes.push(1);
1889 encode_text(bytes, key.as_str());
1890 }
1891 DependencyReferenceTargetV1::Refused { key, reason } => {
1892 bytes.push(2);
1893 encode_optional_key(bytes, key.as_ref());
1894 bytes.push(refusal_reason_tag(*reason));
1895 }
1896 DependencyReferenceTargetV1::Unavailable { key, reason } => {
1897 bytes.push(3);
1898 encode_optional_key(bytes, key.as_ref());
1899 bytes.push(unavailable_reason_tag(*reason));
1900 }
1901 },
1902 );
1903 encode_external_resources(&mut bytes, &closure.external_resources);
1904 let work = closure.work;
1905 encode_u64(&mut bytes, work.inspected_references as u64);
1906 encode_u64(&mut bytes, work.retained_references as u64);
1907 encode_u64(&mut bytes, work.normalization_bytes_inspected as u64);
1908 encode_u64(&mut bytes, work.path_components_inspected as u64);
1909 encode_u64(&mut bytes, work.dedup_probes as u64);
1910 encode_u64(&mut bytes, work.external_open_attempts as u64);
1911 encode_u64(&mut bytes, work.distinct_external_keys as u64);
1912 encode_u64(&mut bytes, work.captured_external_resources as u64);
1913 encode_u64(&mut bytes, work.external_bytes_read_hashed);
1914 InputIdentity::from_bytes(&bytes)
1915}
1916
1917fn encode_references(
1918 bytes: &mut Vec<u8>,
1919 references: &[DependencyClosureReferenceV1],
1920 mut encode_target: impl FnMut(&mut Vec<u8>, &DependencyReferenceTargetV1),
1921) {
1922 encode_u64(bytes, references.len() as u64);
1923 for reference in references {
1924 encode_u64(bytes, reference.source_order_index as u64);
1925 bytes.push(resource_kind_tag(reference.kind));
1926 bytes.push(resource_purpose_tag(reference.purpose));
1927 encode_u64(bytes, reference.source_index);
1928 encode_target(bytes, &reference.target);
1929 }
1930}
1931
1932fn encode_external_resources(bytes: &mut Vec<u8>, resources: &[ExternalResourceIdentityV1]) {
1933 encode_u64(bytes, resources.len() as u64);
1934 for resource in resources {
1935 encode_text(bytes, resource.key.as_str());
1936 encode_identity(bytes, &resource.identity);
1937 }
1938}
1939
1940fn encode_optional_key(bytes: &mut Vec<u8>, key: Option<&DependencyResourceKeyV1>) {
1941 match key {
1942 Some(key) => {
1943 bytes.push(1);
1944 encode_text(bytes, key.as_str());
1945 }
1946 None => bytes.push(0),
1947 }
1948}
1949
1950fn coverage_reason_tag(reason: DependencyClosureCoverageReasonV1) -> u8 {
1951 match reason {
1952 DependencyClosureCoverageReasonV1::SourceDeclarationsPartial => 0,
1953 DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable => 1,
1954 DependencyClosureCoverageReasonV1::CaptureUnavailable => 2,
1955 DependencyClosureCoverageReasonV1::RefusedResource => 3,
1956 DependencyClosureCoverageReasonV1::UnavailableResource => 4,
1957 DependencyClosureCoverageReasonV1::ResourceBudgetExceeded => 5,
1958 DependencyClosureCoverageReasonV1::UnmodeledResourceDomain => 6,
1959 }
1960}
1961
1962fn refusal_reason_tag(reason: DependencyResourceRefusalReasonV1) -> u8 {
1963 match reason {
1964 DependencyResourceRefusalReasonV1::Absolute => 0,
1965 DependencyResourceRefusalReasonV1::Escaping => 1,
1966 DependencyResourceRefusalReasonV1::Remote => 2,
1967 DependencyResourceRefusalReasonV1::Malformed => 3,
1968 DependencyResourceRefusalReasonV1::Oversized => 4,
1969 DependencyResourceRefusalReasonV1::Symlink => 5,
1970 }
1971}
1972
1973fn unavailable_reason_tag(reason: DependencyResourceUnavailableReasonV1) -> u8 {
1974 match reason {
1975 DependencyResourceUnavailableReasonV1::ResourceRootUnavailable => 0,
1976 DependencyResourceUnavailableReasonV1::Missing => 1,
1977 DependencyResourceUnavailableReasonV1::Unreadable => 2,
1978 DependencyResourceUnavailableReasonV1::ResourceBudgetExceeded => 3,
1979 }
1980}
1981
1982fn encode_identity(bytes: &mut Vec<u8>, identity: &InputIdentity) {
1983 encode_text(bytes, identity.sha256());
1984 encode_u64(bytes, identity.bytes());
1985}
1986
1987fn encode_text(bytes: &mut Vec<u8>, value: &str) {
1988 encode_u64(bytes, value.len() as u64);
1989 bytes.extend_from_slice(value.as_bytes());
1990}
1991
1992fn encode_u64(bytes: &mut Vec<u8>, value: u64) {
1993 bytes.extend_from_slice(&value.to_le_bytes());
1994}
1995
1996fn resource_kind_tag(kind: SourceResourceKindV1) -> u8 {
1997 match kind {
1998 SourceResourceKindV1::Buffer => 0,
1999 SourceResourceKindV1::Image => 1,
2000 SourceResourceKindV1::Texture => 2,
2001 SourceResourceKindV1::Video => 3,
2002 SourceResourceKindV1::Cache => 4,
2003 }
2004}
2005
2006fn resource_purpose_tag(purpose: DependencyResourcePurposeV1) -> u8 {
2007 match purpose {
2008 DependencyResourcePurposeV1::LoaderEssential => 0,
2009 DependencyResourcePurposeV1::Nonessential => 1,
2010 DependencyResourcePurposeV1::TargetOnly => 2,
2011 }
2012}
2013
2014#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2016#[non_exhaustive]
2017pub enum DependencyClosureError {
2018 #[error("dependency resource key is {bytes} bytes, exceeding the V1 limit of {limit}")]
2020 ResourceKeyTooLong {
2021 bytes: usize,
2023 limit: usize,
2025 },
2026 #[error("dependency resource key is invalid or unsafe")]
2028 InvalidResourceKey,
2029 #[error(
2031 "dependency resource key has {components} components, exceeding the V1 limit of {limit}"
2032 )]
2033 TooManyPathComponents {
2034 components: usize,
2036 limit: usize,
2038 },
2039 #[error("dependency reference outcome was supplied without begin_reference")]
2041 ReferenceNotStarted,
2042 #[error("dependency reference already has a prepared external key")]
2044 ExternalKeyAlreadyPrepared,
2045 #[error("dependency reference was begun but no outcome was supplied")]
2047 UnfinishedReference,
2048 #[error("dependency reference order {actual} is not expected prefix index {expected}")]
2050 NonCanonicalReferenceOrder {
2051 expected: usize,
2053 actual: usize,
2055 },
2056 #[error("dependency alias references an external key without an identity")]
2058 ExternalIdentityMissing,
2059 #[error("dependency external key was not prepared for rooted capture")]
2061 ExternalKeyNotPrepared,
2062 #[error("dependency external key does not match the prepared reference key")]
2064 ExternalKeyMismatch,
2065 #[error("dependency external key has no cached outcome")]
2067 ExternalOutcomeMissing,
2068 #[error("dependency external outcome contradicts the cached key outcome")]
2070 ExternalOutcomeMismatch,
2071 #[error("dependency external key was not opened before capture")]
2073 ExternalKeyNotOpened,
2074 #[error("dependency external key was opened more than once")]
2076 DuplicateExternalOpen,
2077 #[error("dependency closure retained {actual} references but expected {expected}")]
2079 ReferenceCountMismatch {
2080 expected: usize,
2082 actual: usize,
2084 },
2085 #[error("dependency closure primary identity does not match raw source facts")]
2087 PrimaryIdentityMismatch,
2088 #[error("dependency closure has {closure} references but raw facts retain {facts}")]
2090 ResourceReferenceCountMismatch {
2091 facts: usize,
2093 closure: usize,
2095 },
2096 #[error("dependency reference {source_order_index} does not match raw source facts")]
2098 ResourceReferenceMismatch {
2099 source_order_index: usize,
2101 },
2102 #[error("dependency reference {source_order_index} key does not match raw source facts")]
2104 ResourceKeyMismatch {
2105 source_order_index: usize,
2107 },
2108 #[error("complete dependency closure does not match complete raw resource coverage")]
2110 CompleteCoverageMismatch,
2111 #[error("unavailable raw resource coverage requires unavailable empty dependency closure")]
2113 UnavailableCoverageMismatch,
2114 #[error("dependency closure coverage reasons do not match raw resource coverage")]
2116 CoverageReasonMismatch,
2117 #[error("dependency closure identity must be present exactly for complete coverage")]
2119 ClosureIdentityCoverageMismatch,
2120}
2121
2122#[cfg(test)]
2123mod tests {
2124 use super::*;
2125 use crate::{
2126 SourceFactSetV1, SourceLoaderDispositionV1, SourceProvenanceV1, SourceResourceLocatorV1,
2127 };
2128
2129 fn source_resource(
2130 order: usize,
2131 kind: SourceResourceKindV1,
2132 source_index: u64,
2133 locator: SourceResourceLocatorV1,
2134 ) -> SourceResourceReferenceV1 {
2135 SourceResourceReferenceV1::new(
2136 order,
2137 kind,
2138 source_index,
2139 locator,
2140 SourceLoaderDispositionV1::Preserved,
2141 SourceProvenanceV1::format_defined(),
2142 )
2143 }
2144
2145 fn relative(value: &str) -> SourceRelativeLocatorV1 {
2146 let SourceResourceLocatorV1::Relative(value) = SourceResourceLocatorV1::classify(value)
2147 else {
2148 panic!("fixture must be safe relative")
2149 };
2150 value
2151 }
2152
2153 #[test]
2154 fn gltf_percent_aliases_normalize_but_fbx_percent_is_literal() {
2155 let escaped = relative("textures/a%20b.png");
2156 let plain = relative("textures/a b.png");
2157 assert_eq!(
2158 DependencyResourceKeyV1::from_relative(&escaped, ResourceKeySyntaxV1::GltfUri).unwrap(),
2159 DependencyResourceKeyV1::from_relative(&plain, ResourceKeySyntaxV1::GltfUri).unwrap()
2160 );
2161 assert_ne!(
2162 DependencyResourceKeyV1::from_relative(
2163 &escaped,
2164 ResourceKeySyntaxV1::ParserRelativePath
2165 )
2166 .unwrap(),
2167 DependencyResourceKeyV1::from_relative(&plain, ResourceKeySyntaxV1::ParserRelativePath)
2168 .unwrap()
2169 );
2170 }
2171
2172 #[test]
2173 fn precomputed_digest_constructor_preserves_the_canonical_identity_shape() {
2174 let identity = InputIdentity::from_sha256_digest([0xab; 32], 17);
2175 assert_eq!(identity.sha256(), "ab".repeat(32));
2176 assert_eq!(identity.bytes(), 17);
2177 }
2178
2179 #[test]
2180 fn normalized_keys_reject_encoded_escapes_controls_and_component_n_plus_one() {
2181 for value in [
2182 "",
2183 "/absolute.bin",
2184 "C:/drive.bin",
2185 "file:secret.bin",
2186 "https://example.invalid/a.bin",
2187 "a\\b.bin",
2188 "a/./b.bin",
2189 "a/../b.bin",
2190 "a?query.bin",
2191 "a#fragment.bin",
2192 "a\ncontrol.bin",
2193 "a/%2f/b",
2194 "a/%5c/b",
2195 "a/%00/b",
2196 "a/%ff/b",
2197 "a/%zz/b",
2198 ] {
2199 assert!(
2200 matches!(
2201 DependencyResourceKeyV1::from_source_str(value, ResourceKeySyntaxV1::GltfUri),
2202 Err(DependencyClosureError::InvalidResourceKey)
2203 ),
2204 "unexpected safe key: {value:?}"
2205 );
2206 }
2207 let at_limit = (0..DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS)
2208 .map(|_| "a")
2209 .collect::<Vec<_>>()
2210 .join("/");
2211 assert!(
2212 DependencyResourceKeyV1::from_relative(
2213 &relative(&at_limit),
2214 ResourceKeySyntaxV1::ParserRelativePath
2215 )
2216 .is_ok()
2217 );
2218 let over = format!("{at_limit}/a");
2219 assert!(matches!(
2220 DependencyResourceKeyV1::from_relative(
2221 &relative(&over),
2222 ResourceKeySyntaxV1::ParserRelativePath
2223 ),
2224 Err(DependencyClosureError::TooManyPathComponents { .. })
2225 ));
2226 }
2227
2228 #[test]
2229 fn complete_closure_deduplicates_aliases_and_changes_with_external_identity() {
2230 let primary = InputIdentity::from_bytes(b"primary");
2231 let key = DependencyResourceKeyV1::from_relative(
2232 &relative("a.bin"),
2233 ResourceKeySyntaxV1::GltfUri,
2234 )
2235 .unwrap();
2236 let mut builder =
2237 DependencyClosureBuilderV1::new(primary.clone(), SourceSetCoverageV1::complete(), 2);
2238 assert!(builder.begin_reference(5, 1));
2239 assert_eq!(builder.prepare_external_key(&key).unwrap(), Some(true));
2240 builder.record_external_open_attempt(&key).unwrap();
2241 assert!(
2242 builder
2243 .push_captured_external(
2244 0,
2245 SourceResourceKindV1::Buffer,
2246 0,
2247 key.clone(),
2248 InputIdentity::from_bytes(b"one"),
2249 )
2250 .unwrap()
2251 );
2252 assert!(builder.begin_reference(7, 1));
2253 assert_eq!(builder.prepare_external_key(&key).unwrap(), Some(false));
2254 builder
2255 .push_external_alias(1, SourceResourceKindV1::Image, 0, key.clone())
2256 .unwrap();
2257 let first = builder.finish().unwrap();
2258 assert!(first.coverage().is_complete());
2259 let record_identity = first.record_identity();
2260 assert_eq!(
2261 record_identity.sha256(),
2262 "dd3e91c7a0e1ea436c0ad538fae3de1e5d085b0b165d8727ebc1db61b8b2bbb5"
2263 );
2264 assert_eq!(record_identity.bytes(), 608);
2265 let identity = first.identity().expect("complete closure identity");
2266 assert_eq!(
2267 identity.input_identity().sha256(),
2268 "43fccaa09b2616c57863a1186b88d8d674cb404b4e99c35b18a239a4d4b782ad"
2269 );
2270 assert_eq!(identity.input_identity().bytes(), 409);
2271 assert_eq!(first.external_resources().len(), 1);
2272 assert_eq!(first.work().external_open_attempts(), 1);
2273 let wire = serde_json::to_value(&first).unwrap();
2274 assert_eq!(wire["schema"], DEPENDENCY_CLOSURE_V1_ID);
2275 assert_eq!(wire["budget"]["schema"], DEPENDENCY_CLOSURE_BUDGET_V1_ID);
2276 assert_eq!(
2277 wire["budget"]["max_external_resources"],
2278 DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES
2279 );
2280 assert_eq!(wire["coverage"]["state"], "complete");
2281 assert_eq!(
2282 wire["identity"]["sha256"],
2283 "43fccaa09b2616c57863a1186b88d8d674cb404b4e99c35b18a239a4d4b782ad"
2284 );
2285
2286 let round_trip: DependencyClosureV1 =
2287 serde_json::from_value(wire.clone()).expect("builder work reads back");
2288 assert_eq!(round_trip, first);
2289 for (field, value) in [
2290 ("inspected_references", serde_json::json!(0)),
2291 ("dedup_probes", serde_json::json!(0)),
2292 ("distinct_external_keys", serde_json::json!(0)),
2293 ("external_open_attempts", serde_json::json!(0)),
2294 ("external_bytes_read_hashed", serde_json::json!(0)),
2295 ] {
2296 let mut impossible = wire.clone();
2297 impossible["work"][field] = value;
2298 let error = serde_json::from_value::<DependencyClosureV1>(impossible)
2299 .expect_err("impossible complete work must fail strict readback");
2300 assert!(
2301 error
2302 .to_string()
2303 .contains("dependency closure work counters are invalid"),
2304 "field {field} produced {error}"
2305 );
2306 }
2307 let mut impossible_terminal = wire.clone();
2308 impossible_terminal["work"]["inspected_references"] = serde_json::json!(3);
2309 impossible_terminal["work"]["dedup_probes"] = serde_json::json!(3);
2310 let error = serde_json::from_value::<DependencyClosureV1>(impossible_terminal)
2311 .expect_err("complete work cannot claim a terminal N+1 inspection");
2312 assert!(
2313 error
2314 .to_string()
2315 .contains("dependency closure work counters are invalid"),
2316 "unexpected terminal-work error: {error}"
2317 );
2318
2319 let mut changed =
2320 DependencyClosureBuilderV1::new(primary, SourceSetCoverageV1::complete(), 2);
2321 assert!(changed.begin_reference(5, 1));
2322 let changed_key = DependencyResourceKeyV1::from_relative(
2323 &relative("a.bin"),
2324 ResourceKeySyntaxV1::GltfUri,
2325 )
2326 .unwrap();
2327 assert_eq!(
2328 changed.prepare_external_key(&changed_key).unwrap(),
2329 Some(true)
2330 );
2331 changed.record_external_open_attempt(&changed_key).unwrap();
2332 assert!(
2333 changed
2334 .push_captured_external(
2335 0,
2336 SourceResourceKindV1::Buffer,
2337 0,
2338 changed_key,
2339 InputIdentity::from_bytes(b"two"),
2340 )
2341 .unwrap()
2342 );
2343 assert!(changed.begin_reference(7, 1));
2344 assert_eq!(changed.prepare_external_key(&key).unwrap(), Some(false));
2345 changed
2346 .push_external_alias(1, SourceResourceKindV1::Image, 0, key)
2347 .unwrap();
2348 let changed = changed.finish().unwrap();
2349 assert_eq!(first.references().len(), changed.references().len());
2350 assert_eq!(first.references()[0].kind(), changed.references()[0].kind());
2351 assert_eq!(first.references()[1].kind(), changed.references()[1].kind());
2352 assert_ne!(first.identity(), changed.identity());
2353 assert_ne!(first.record_identity(), changed.record_identity());
2354 }
2355
2356 #[test]
2357 fn partial_and_unavailable_closures_never_claim_identity() {
2358 let primary = InputIdentity::from_bytes(b"primary");
2359 let mut partial =
2360 DependencyClosureBuilderV1::new(primary.clone(), SourceSetCoverageV1::complete(), 1);
2361 assert!(partial.begin_reference(0, 0));
2362 partial
2363 .push_refused(
2364 0,
2365 SourceResourceKindV1::Image,
2366 0,
2367 DependencyResourceRefusalReasonV1::Remote,
2368 )
2369 .unwrap();
2370 let partial = partial.finish().unwrap();
2371 assert!(matches!(
2372 partial.coverage(),
2373 DependencyClosureCoverageV1::Partial { .. }
2374 ));
2375 assert!(partial.identity().is_none());
2376 let mut explicit_null_identity = serde_json::to_value(&partial).unwrap();
2377 explicit_null_identity["identity"] = serde_json::Value::Null;
2378 let error = serde_json::from_value::<DependencyClosureV1>(explicit_null_identity)
2379 .expect_err("incomplete closure identity must be absent, not null");
2380 assert!(error.to_string().contains("invalid type: null"), "{error}");
2381 let mut impossible_terminal = serde_json::to_value(&partial).unwrap();
2382 impossible_terminal["work"]["inspected_references"] = serde_json::json!(2);
2383 impossible_terminal["work"]["dedup_probes"] = serde_json::json!(2);
2384 let error = serde_json::from_value::<DependencyClosureV1>(impossible_terminal)
2385 .expect_err("non-budget partial work cannot claim N+1 inspection");
2386 assert!(
2387 error
2388 .to_string()
2389 .contains("dependency closure work counters are invalid"),
2390 "unexpected partial terminal-work error: {error}"
2391 );
2392
2393 let unavailable = DependencyClosureV1::unavailable(primary);
2394 assert!(matches!(
2395 unavailable.coverage(),
2396 DependencyClosureCoverageV1::Unavailable { .. }
2397 ));
2398 assert!(unavailable.identity().is_none());
2399 }
2400
2401 #[test]
2402 fn reference_limit_stops_at_n_plus_one_and_never_resumes() {
2403 let primary = InputIdentity::from_bytes(b"primary");
2404 let mut builder = DependencyClosureBuilderV1::new(
2405 primary,
2406 SourceSetCoverageV1::complete(),
2407 DEPENDENCY_CLOSURE_V1_MAX_REFERENCES + 2,
2408 );
2409 for index in 0..DEPENDENCY_CLOSURE_V1_MAX_REFERENCES {
2410 assert!(builder.begin_reference(0, 0));
2411 builder
2412 .push_primary(index, SourceResourceKindV1::Image, index as u64)
2413 .unwrap();
2414 }
2415 assert!(!builder.begin_reference(0, 0));
2416 assert!(!builder.begin_reference(0, 0));
2417 let closure = builder.finish().unwrap();
2418 assert_eq!(
2419 closure.references().len(),
2420 DEPENDENCY_CLOSURE_V1_MAX_REFERENCES
2421 );
2422 assert_eq!(
2423 closure.work().inspected_references(),
2424 DEPENDENCY_CLOSURE_V1_MAX_REFERENCES + 1
2425 );
2426 assert_eq!(
2427 closure.work().dedup_probes(),
2428 DEPENDENCY_CLOSURE_V1_MAX_DEDUP_PROBES + 1
2429 );
2430 assert!(matches!(
2431 closure.coverage(),
2432 DependencyClosureCoverageV1::Partial { reasons }
2433 if reasons.contains(&DependencyClosureCoverageReasonV1::ResourceBudgetExceeded)
2434 ));
2435 let wire = serde_json::to_value(&closure).unwrap();
2436 let round_trip: DependencyClosureV1 =
2437 serde_json::from_value(wire).expect("budget N+1 work reads back");
2438 assert_eq!(round_trip, closure);
2439 }
2440
2441 #[test]
2442 fn closure_binding_checks_primary_and_exact_resource_prefix() {
2443 let primary = InputIdentity::from_bytes(b"primary");
2444 let source_rows = SourceFactSetV1::complete(vec![source_resource(
2445 0,
2446 SourceResourceKindV1::Image,
2447 7,
2448 SourceResourceLocatorV1::Embedded,
2449 )]);
2450 let mut builder = DependencyClosureBuilderV1::new(
2451 primary.clone(),
2452 source_rows.coverage(),
2453 source_rows.rows().len(),
2454 );
2455 assert!(builder.begin_reference(0, 0));
2456 builder
2457 .push_primary(0, SourceResourceKindV1::Image, 7)
2458 .unwrap();
2459 let closure = builder.finish().unwrap();
2460 closure
2461 .validate_against(SourceFormatV1::Glb, &primary, &source_rows)
2462 .unwrap();
2463 assert_eq!(
2464 closure.validate_against(
2465 SourceFormatV1::Glb,
2466 &InputIdentity::from_bytes(b"other"),
2467 &source_rows,
2468 ),
2469 Err(DependencyClosureError::PrimaryIdentityMismatch)
2470 );
2471
2472 let mut wrong_target = closure.clone();
2473 wrong_target.references[0].target = DependencyReferenceTargetV1::Refused {
2474 key: None,
2475 reason: DependencyResourceRefusalReasonV1::Remote,
2476 };
2477 assert_eq!(
2478 wrong_target.validate_against(SourceFormatV1::Glb, &primary, &source_rows),
2479 Err(DependencyClosureError::ResourceReferenceMismatch {
2480 source_order_index: 0
2481 })
2482 );
2483 }
2484
2485 #[test]
2486 fn key_and_normalization_byte_limits_are_exact_and_terminal() {
2487 let at_key_limit = "a".repeat(DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES);
2488 assert!(
2489 DependencyResourceKeyV1::from_source_str(
2490 &at_key_limit,
2491 ResourceKeySyntaxV1::ParserRelativePath
2492 )
2493 .is_ok()
2494 );
2495 let over_key_limit = "a".repeat(DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES + 1);
2496 let error = DependencyResourceKeyV1::from_source_str(
2497 &over_key_limit,
2498 ResourceKeySyntaxV1::ParserRelativePath,
2499 )
2500 .unwrap_err();
2501 assert_eq!(
2502 error,
2503 DependencyClosureError::ResourceKeyTooLong {
2504 bytes: DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES + 1,
2505 limit: DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES,
2506 }
2507 );
2508
2509 let rows =
2510 DEPENDENCY_CLOSURE_V1_MAX_NORMALIZATION_BYTES / DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES;
2511 let mut builder = DependencyClosureBuilderV1::new(
2512 InputIdentity::from_bytes(b"primary"),
2513 SourceSetCoverageV1::complete(),
2514 rows + 1,
2515 );
2516 for index in 0..rows {
2517 assert!(builder.begin_reference(DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES, 1));
2518 builder
2519 .push_primary(index, SourceResourceKindV1::Image, index as u64)
2520 .unwrap();
2521 }
2522 assert!(!builder.begin_reference(1, 1));
2523 assert!(!builder.begin_reference(0, 0));
2524 let closure = builder.finish().unwrap();
2525 assert_eq!(
2526 closure.work().normalization_bytes_inspected(),
2527 DEPENDENCY_CLOSURE_V1_MAX_NORMALIZATION_BYTES + 1
2528 );
2529 assert_eq!(closure.work().inspected_references(), rows + 1);
2530 assert_eq!(closure.work().path_components_inspected(), rows + 1);
2531 assert_eq!(closure.work().dedup_probes(), rows + 1);
2532 }
2533
2534 #[test]
2535 fn distinct_external_key_limit_stops_before_n_plus_one_open() {
2536 let primary = InputIdentity::from_bytes(b"primary");
2537 let mut builder = DependencyClosureBuilderV1::new(
2538 primary,
2539 SourceSetCoverageV1::complete(),
2540 DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES + 1,
2541 );
2542 for index in 0..DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES {
2543 assert!(builder.begin_reference(8, 1));
2544 let key = DependencyResourceKeyV1::from_source_str(
2545 &format!("r{index}.bin"),
2546 ResourceKeySyntaxV1::ParserRelativePath,
2547 )
2548 .unwrap();
2549 assert_eq!(builder.prepare_external_key(&key).unwrap(), Some(true));
2550 builder.record_external_open_attempt(&key).unwrap();
2551 assert!(
2552 builder
2553 .push_captured_external(
2554 index,
2555 SourceResourceKindV1::Buffer,
2556 index as u64,
2557 key,
2558 InputIdentity::from_bytes(&[]),
2559 )
2560 .unwrap()
2561 );
2562 }
2563 assert!(builder.begin_reference(8, 1));
2564 let overflow = DependencyResourceKeyV1::from_source_str(
2565 "overflow.bin",
2566 ResourceKeySyntaxV1::ParserRelativePath,
2567 )
2568 .unwrap();
2569 assert_eq!(builder.prepare_external_key(&overflow).unwrap(), None);
2570 assert_eq!(
2571 builder.record_external_open_attempt(&overflow),
2572 Err(DependencyClosureError::ReferenceNotStarted)
2573 );
2574 let closure = builder.finish().unwrap();
2575 assert_eq!(
2576 closure.work().distinct_external_keys(),
2577 DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES
2578 );
2579 assert_eq!(
2580 closure.work().inspected_references(),
2581 DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES + 1
2582 );
2583 assert_eq!(
2584 closure.work().dedup_probes(),
2585 DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES + 1
2586 );
2587 assert_eq!(
2588 closure.work().external_open_attempts(),
2589 DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES
2590 );
2591 assert_eq!(
2592 closure.external_resources().len(),
2593 DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES
2594 );
2595 }
2596
2597 #[test]
2598 fn captured_external_identity_requires_the_recorded_same_capture_open() {
2599 let mut builder = DependencyClosureBuilderV1::new(
2600 InputIdentity::from_bytes(b"primary"),
2601 SourceSetCoverageV1::complete(),
2602 1,
2603 );
2604 assert!(builder.begin_reference(5, 1));
2605 let key = DependencyResourceKeyV1::from_source_str(
2606 "a.bin",
2607 ResourceKeySyntaxV1::ParserRelativePath,
2608 )
2609 .unwrap();
2610 assert_eq!(builder.prepare_external_key(&key).unwrap(), Some(true));
2611 assert_eq!(
2612 builder.push_captured_external(
2613 0,
2614 SourceResourceKindV1::Buffer,
2615 0,
2616 key.clone(),
2617 InputIdentity::from_bytes(b"bytes"),
2618 ),
2619 Err(DependencyClosureError::ExternalKeyNotOpened)
2620 );
2621 builder.record_external_open_attempt(&key).unwrap();
2622 assert!(
2623 builder
2624 .push_captured_external(
2625 0,
2626 SourceResourceKindV1::Buffer,
2627 0,
2628 key,
2629 InputIdentity::from_bytes(b"bytes"),
2630 )
2631 .unwrap()
2632 );
2633 }
2634
2635 #[test]
2636 fn external_byte_limits_are_exact_without_allocating_fixture_payloads() {
2637 let identity = |tag: u8, bytes| InputIdentity::from_sha256_digest([tag; 32], bytes);
2638 let key = |index| {
2639 DependencyResourceKeyV1::from_source_str(
2640 &format!("r{index}.bin"),
2641 ResourceKeySyntaxV1::ParserRelativePath,
2642 )
2643 .unwrap()
2644 };
2645
2646 let mut exact = DependencyClosureBuilderV1::new(
2647 InputIdentity::from_bytes(b"primary"),
2648 SourceSetCoverageV1::complete(),
2649 4,
2650 );
2651 for index in 0..4 {
2652 assert!(exact.begin_reference(8, 1));
2653 let key = key(index);
2654 assert_eq!(exact.prepare_external_key(&key).unwrap(), Some(true));
2655 exact.record_external_open_attempt(&key).unwrap();
2656 assert!(
2657 exact
2658 .push_captured_external(
2659 index,
2660 SourceResourceKindV1::Buffer,
2661 index as u64,
2662 key,
2663 identity(index as u8, DEPENDENCY_CLOSURE_V1_MAX_RESOURCE_BYTES),
2664 )
2665 .unwrap()
2666 );
2667 }
2668 let exact = exact.finish().unwrap();
2669 assert!(exact.coverage().is_complete());
2670 assert_eq!(
2671 exact.work().external_bytes_read_hashed(),
2672 DEPENDENCY_CLOSURE_V1_MAX_TOTAL_RESOURCE_BYTES
2673 );
2674
2675 let mut per_resource_over = DependencyClosureBuilderV1::new(
2676 InputIdentity::from_bytes(b"primary"),
2677 SourceSetCoverageV1::complete(),
2678 1,
2679 );
2680 assert!(per_resource_over.begin_reference(8, 1));
2681 let over_key = key(9);
2682 assert_eq!(
2683 per_resource_over.prepare_external_key(&over_key).unwrap(),
2684 Some(true)
2685 );
2686 per_resource_over
2687 .record_external_open_attempt(&over_key)
2688 .unwrap();
2689 assert!(
2690 !per_resource_over
2691 .push_captured_external(
2692 0,
2693 SourceResourceKindV1::Buffer,
2694 0,
2695 over_key,
2696 identity(9, DEPENDENCY_CLOSURE_V1_MAX_RESOURCE_BYTES + 1),
2697 )
2698 .unwrap()
2699 );
2700 let per_resource_over = per_resource_over.finish().unwrap();
2701 assert_eq!(per_resource_over.references().len(), 1);
2702 assert!(matches!(
2703 per_resource_over.references()[0].target(),
2704 DependencyReferenceTargetV1::Unavailable {
2705 reason: DependencyResourceUnavailableReasonV1::ResourceBudgetExceeded,
2706 ..
2707 }
2708 ));
2709 assert_eq!(
2710 per_resource_over.work().external_bytes_read_hashed(),
2711 DEPENDENCY_CLOSURE_V1_MAX_RESOURCE_BYTES + 1
2712 );
2713 assert!(matches!(
2714 per_resource_over.coverage(),
2715 DependencyClosureCoverageV1::Partial { reasons }
2716 if reasons.contains(&DependencyClosureCoverageReasonV1::ResourceBudgetExceeded)
2717 && reasons.contains(&DependencyClosureCoverageReasonV1::UnavailableResource)
2718 ));
2719
2720 let mut aggregate_over = DependencyClosureBuilderV1::new(
2721 InputIdentity::from_bytes(b"primary"),
2722 SourceSetCoverageV1::complete(),
2723 5,
2724 );
2725 for index in 0..4 {
2726 assert!(aggregate_over.begin_reference(8, 1));
2727 let key = key(index);
2728 assert_eq!(
2729 aggregate_over.prepare_external_key(&key).unwrap(),
2730 Some(true)
2731 );
2732 aggregate_over.record_external_open_attempt(&key).unwrap();
2733 assert!(
2734 aggregate_over
2735 .push_captured_external(
2736 index,
2737 SourceResourceKindV1::Buffer,
2738 index as u64,
2739 key,
2740 identity(index as u8, DEPENDENCY_CLOSURE_V1_MAX_RESOURCE_BYTES),
2741 )
2742 .unwrap()
2743 );
2744 }
2745 assert!(aggregate_over.begin_reference(8, 1));
2746 let fifth = key(5);
2747 assert_eq!(
2748 aggregate_over.prepare_external_key(&fifth).unwrap(),
2749 Some(true)
2750 );
2751 aggregate_over.record_external_open_attempt(&fifth).unwrap();
2752 assert!(
2753 !aggregate_over
2754 .push_captured_external(4, SourceResourceKindV1::Buffer, 4, fifth, identity(5, 1),)
2755 .unwrap()
2756 );
2757 let aggregate_over = aggregate_over.finish().unwrap();
2758 assert_eq!(aggregate_over.references().len(), 5);
2759 assert_eq!(aggregate_over.external_resources().len(), 4);
2760 assert_eq!(aggregate_over.work().inspected_references(), 5);
2761 assert_eq!(
2762 aggregate_over.work().external_bytes_read_hashed(),
2763 DEPENDENCY_CLOSURE_V1_MAX_TOTAL_RESOURCE_BYTES + 1
2764 );
2765 assert!(matches!(
2766 aggregate_over.coverage(),
2767 DependencyClosureCoverageV1::Partial { reasons }
2768 if reasons.contains(&DependencyClosureCoverageReasonV1::ResourceBudgetExceeded)
2769 ));
2770 }
2771
2772 #[test]
2773 fn source_order_is_identity_bearing_while_external_rows_remain_key_sorted() {
2774 fn closure(order: [(&str, u64); 2]) -> DependencyClosureV1 {
2775 let mut builder = DependencyClosureBuilderV1::new(
2776 InputIdentity::from_bytes(b"primary"),
2777 SourceSetCoverageV1::complete(),
2778 2,
2779 );
2780 for (source_order, (name, source_index)) in order.into_iter().enumerate() {
2781 assert!(builder.begin_reference(name.len(), 1));
2782 let key = DependencyResourceKeyV1::from_source_str(
2783 name,
2784 ResourceKeySyntaxV1::ParserRelativePath,
2785 )
2786 .unwrap();
2787 assert_eq!(builder.prepare_external_key(&key).unwrap(), Some(true));
2788 builder.record_external_open_attempt(&key).unwrap();
2789 assert!(
2790 builder
2791 .push_captured_external(
2792 source_order,
2793 SourceResourceKindV1::Image,
2794 source_index,
2795 key,
2796 InputIdentity::from_bytes(name.as_bytes()),
2797 )
2798 .unwrap()
2799 );
2800 }
2801 builder.finish().unwrap()
2802 }
2803
2804 let first = closure([("z.png", 0), ("a.png", 1)]);
2805 let second = closure([("a.png", 1), ("z.png", 0)]);
2806 assert_eq!(
2807 first
2808 .external_resources()
2809 .iter()
2810 .map(|row| row.key().as_str())
2811 .collect::<Vec<_>>(),
2812 vec!["a.png", "z.png"]
2813 );
2814 assert_ne!(first.identity(), second.identity());
2815 }
2816
2817 #[test]
2818 fn refused_and_unavailable_serialization_never_has_an_unsafe_spelling() {
2819 let mut builder = DependencyClosureBuilderV1::new(
2820 InputIdentity::from_bytes(b"primary"),
2821 SourceSetCoverageV1::complete(),
2822 2,
2823 );
2824 assert!(builder.begin_reference(0, 0));
2825 builder
2826 .push_refused(
2827 0,
2828 SourceResourceKindV1::Image,
2829 0,
2830 DependencyResourceRefusalReasonV1::Absolute,
2831 )
2832 .unwrap();
2833 assert!(builder.begin_reference(8, 1));
2834 let key = DependencyResourceKeyV1::from_source_str(
2835 "safe.png",
2836 ResourceKeySyntaxV1::ParserRelativePath,
2837 )
2838 .unwrap();
2839 assert_eq!(builder.prepare_external_key(&key).unwrap(), Some(true));
2840 builder
2841 .push_unavailable(
2842 1,
2843 SourceResourceKindV1::Image,
2844 1,
2845 Some(key),
2846 DependencyResourceUnavailableReasonV1::ResourceRootUnavailable,
2847 )
2848 .unwrap();
2849 let closure = builder.finish().unwrap();
2850 let json = serde_json::to_string(&closure).unwrap();
2851 let debug = format!("{closure:?}");
2852 for rendered in [json, debug] {
2853 assert!(!rendered.contains("/home/private/secret.png"));
2854 assert!(rendered.contains("safe.png"));
2855 }
2856 }
2857
2858 #[test]
2859 fn resource_purpose_is_authoritatively_derived_and_serialized() {
2860 let cases = [
2861 (
2862 SourceResourceKindV1::Buffer,
2863 DependencyResourcePurposeV1::LoaderEssential,
2864 "loader_essential",
2865 ),
2866 (
2867 SourceResourceKindV1::Image,
2868 DependencyResourcePurposeV1::Nonessential,
2869 "nonessential",
2870 ),
2871 (
2872 SourceResourceKindV1::Texture,
2873 DependencyResourcePurposeV1::Nonessential,
2874 "nonessential",
2875 ),
2876 (
2877 SourceResourceKindV1::Video,
2878 DependencyResourcePurposeV1::TargetOnly,
2879 "target_only",
2880 ),
2881 (
2882 SourceResourceKindV1::Cache,
2883 DependencyResourcePurposeV1::TargetOnly,
2884 "target_only",
2885 ),
2886 ];
2887 let mut builder = DependencyClosureBuilderV1::new(
2888 InputIdentity::from_bytes(b"primary"),
2889 SourceSetCoverageV1::complete(),
2890 cases.len(),
2891 );
2892 for (index, (kind, _, _)) in cases.iter().copied().enumerate() {
2893 assert!(builder.begin_reference(0, 0));
2894 builder.push_primary(index, kind, index as u64).unwrap();
2895 }
2896 let closure = builder.finish().unwrap();
2897 let wire = serde_json::to_value(&closure).unwrap();
2898 for (index, (_, purpose, spelling)) in cases.iter().copied().enumerate() {
2899 assert_eq!(closure.references()[index].purpose(), purpose);
2900 assert_eq!(wire["references"][index]["purpose"], spelling);
2901 }
2902
2903 let mut mutated = closure.references.clone();
2904 mutated[0].purpose = DependencyResourcePurposeV1::TargetOnly;
2905 assert_ne!(
2906 canonical_identity(
2907 closure.primary_input(),
2908 &mutated,
2909 closure.external_resources(),
2910 ),
2911 *closure.identity().unwrap()
2912 );
2913 }
2914
2915 #[test]
2916 fn raw_relative_key_binding_uses_the_source_format_normalization() {
2917 let primary = InputIdentity::from_bytes(b"primary");
2918 let raw_locator = SourceResourceLocatorV1::classify("textures/a%20b.png");
2919 let source_rows = SourceFactSetV1::complete(vec![source_resource(
2920 0,
2921 SourceResourceKindV1::Image,
2922 0,
2923 raw_locator,
2924 )]);
2925 let literal_key = DependencyResourceKeyV1::from_source_str(
2926 "textures/a%20b.png",
2927 ResourceKeySyntaxV1::ParserRelativePath,
2928 )
2929 .unwrap();
2930 let mut builder = DependencyClosureBuilderV1::new(
2931 primary.clone(),
2932 source_rows.coverage(),
2933 source_rows.rows().len(),
2934 );
2935 assert!(builder.begin_reference(20, 2));
2936 assert_eq!(
2937 builder.prepare_external_key(&literal_key).unwrap(),
2938 Some(true)
2939 );
2940 builder.record_external_open_attempt(&literal_key).unwrap();
2941 assert!(
2942 builder
2943 .push_captured_external(
2944 0,
2945 SourceResourceKindV1::Image,
2946 0,
2947 literal_key,
2948 InputIdentity::from_bytes(b"image"),
2949 )
2950 .unwrap()
2951 );
2952 let closure = builder.finish().unwrap();
2953 closure
2954 .validate_against(SourceFormatV1::Fbx, &primary, &source_rows)
2955 .unwrap();
2956 assert_eq!(
2957 closure.validate_against(SourceFormatV1::GltfJson, &primary, &source_rows),
2958 Err(DependencyClosureError::ResourceKeyMismatch {
2959 source_order_index: 0,
2960 })
2961 );
2962
2963 let wrong_rows = SourceFactSetV1::complete(vec![source_resource(
2964 0,
2965 SourceResourceKindV1::Image,
2966 0,
2967 SourceResourceLocatorV1::classify("textures/b%20b.png"),
2968 )]);
2969 assert_eq!(
2970 closure.validate_against(SourceFormatV1::Fbx, &primary, &wrong_rows),
2971 Err(DependencyClosureError::ResourceKeyMismatch {
2972 source_order_index: 0,
2973 })
2974 );
2975 }
2976
2977 #[test]
2978 fn binding_rejects_a_wrong_missing_reason_and_wrong_source_coverage_reason() {
2979 let primary = InputIdentity::from_bytes(b"primary");
2980 let missing_rows = SourceFactSetV1::complete(vec![source_resource(
2981 0,
2982 SourceResourceKindV1::Image,
2983 0,
2984 SourceResourceLocatorV1::Missing,
2985 )]);
2986 let mut builder = DependencyClosureBuilderV1::new(
2987 primary.clone(),
2988 missing_rows.coverage(),
2989 missing_rows.rows().len(),
2990 );
2991 assert!(builder.begin_reference(0, 0));
2992 builder
2993 .push_unavailable(
2994 0,
2995 SourceResourceKindV1::Image,
2996 0,
2997 None,
2998 DependencyResourceUnavailableReasonV1::Missing,
2999 )
3000 .unwrap();
3001 let closure = builder.finish().unwrap();
3002 closure
3003 .validate_against(SourceFormatV1::GltfJson, &primary, &missing_rows)
3004 .unwrap();
3005 let mut wrong_reason = closure.clone();
3006 wrong_reason.references[0].target = DependencyReferenceTargetV1::Unavailable {
3007 key: None,
3008 reason: DependencyResourceUnavailableReasonV1::Unreadable,
3009 };
3010 assert_eq!(
3011 wrong_reason.validate_against(SourceFormatV1::GltfJson, &primary, &missing_rows),
3012 Err(DependencyClosureError::ResourceReferenceMismatch {
3013 source_order_index: 0,
3014 })
3015 );
3016
3017 let complete_rows = SourceFactSetV1::<SourceResourceReferenceV1>::complete(Vec::new());
3018 let mut wrong_coverage = DependencyClosureV1::capture_unavailable(
3019 primary.clone(),
3020 SourceSetCoverageV1::complete(),
3021 );
3022 wrong_coverage.coverage = DependencyClosureCoverageV1::Unavailable {
3023 reasons: vec![
3024 DependencyClosureCoverageReasonV1::SourceDeclarationsUnavailable,
3025 DependencyClosureCoverageReasonV1::CaptureUnavailable,
3026 ],
3027 };
3028 assert_eq!(
3029 wrong_coverage.validate_against(SourceFormatV1::GltfJson, &primary, &complete_rows,),
3030 Err(DependencyClosureError::CoverageReasonMismatch)
3031 );
3032 }
3033
3034 #[test]
3035 fn safe_symlink_refusal_retains_and_validates_only_the_normalized_key() {
3036 let primary = InputIdentity::from_bytes(b"primary");
3037 let source_rows = SourceFactSetV1::complete(vec![
3038 source_resource(
3039 0,
3040 SourceResourceKindV1::Image,
3041 0,
3042 SourceResourceLocatorV1::classify("textures/a%20b.png"),
3043 ),
3044 source_resource(
3045 1,
3046 SourceResourceKindV1::Image,
3047 1,
3048 SourceResourceLocatorV1::classify("textures/a b.png"),
3049 ),
3050 ]);
3051 let key = DependencyResourceKeyV1::from_source_str(
3052 "textures/a b.png",
3053 ResourceKeySyntaxV1::GltfUri,
3054 )
3055 .unwrap();
3056 let mut builder = DependencyClosureBuilderV1::new(
3057 primary.clone(),
3058 source_rows.coverage(),
3059 source_rows.rows().len(),
3060 );
3061 for index in 0..2 {
3062 assert!(builder.begin_reference(20, 2));
3063 assert_eq!(
3064 builder.prepare_external_key(&key).unwrap(),
3065 Some(index == 0)
3066 );
3067 builder
3068 .push_refused(
3069 index,
3070 SourceResourceKindV1::Image,
3071 index as u64,
3072 DependencyResourceRefusalReasonV1::Symlink,
3073 )
3074 .unwrap();
3075 }
3076 let closure = builder.finish().unwrap();
3077 closure
3078 .validate_against(SourceFormatV1::GltfJson, &primary, &source_rows)
3079 .unwrap();
3080 assert_eq!(closure.work().external_open_attempts(), 0);
3081 let wire = serde_json::to_value(&closure).unwrap();
3082 assert_eq!(wire["references"][0]["target"]["key"], "textures/a b.png");
3083 assert_eq!(wire["references"][1]["target"]["key"], "textures/a b.png");
3084 }
3085
3086 #[test]
3087 fn builder_rejects_multiple_keys_and_cached_outcome_contradictions() {
3088 let primary = InputIdentity::from_bytes(b"primary");
3089 let first = DependencyResourceKeyV1::from_source_str(
3090 "a.bin",
3091 ResourceKeySyntaxV1::ParserRelativePath,
3092 )
3093 .unwrap();
3094 let second = DependencyResourceKeyV1::from_source_str(
3095 "b.bin",
3096 ResourceKeySyntaxV1::ParserRelativePath,
3097 )
3098 .unwrap();
3099 let mut unavailable =
3100 DependencyClosureBuilderV1::new(primary.clone(), SourceSetCoverageV1::complete(), 2);
3101 assert!(unavailable.begin_reference(5, 1));
3102 assert_eq!(
3103 unavailable.prepare_external_key(&first).unwrap(),
3104 Some(true)
3105 );
3106 assert_eq!(
3107 unavailable.prepare_external_key(&second),
3108 Err(DependencyClosureError::ExternalKeyAlreadyPrepared)
3109 );
3110 assert_eq!(
3111 unavailable.record_external_open_attempt(&second),
3112 Err(DependencyClosureError::ExternalKeyMismatch)
3113 );
3114 assert_eq!(
3115 unavailable.push_unavailable(
3116 0,
3117 SourceResourceKindV1::Image,
3118 0,
3119 Some(second),
3120 DependencyResourceUnavailableReasonV1::Missing,
3121 ),
3122 Err(DependencyClosureError::ExternalKeyMismatch)
3123 );
3124 unavailable
3125 .push_unavailable(
3126 0,
3127 SourceResourceKindV1::Image,
3128 0,
3129 Some(first.clone()),
3130 DependencyResourceUnavailableReasonV1::Missing,
3131 )
3132 .unwrap();
3133 assert!(unavailable.begin_reference(5, 1));
3134 assert_eq!(
3135 unavailable.prepare_external_key(&first).unwrap(),
3136 Some(false)
3137 );
3138 assert_eq!(
3139 unavailable.push_unavailable(
3140 1,
3141 SourceResourceKindV1::Image,
3142 1,
3143 Some(first.clone()),
3144 DependencyResourceUnavailableReasonV1::Unreadable,
3145 ),
3146 Err(DependencyClosureError::ExternalOutcomeMismatch)
3147 );
3148 unavailable
3149 .push_unavailable(
3150 1,
3151 SourceResourceKindV1::Image,
3152 1,
3153 Some(first.clone()),
3154 DependencyResourceUnavailableReasonV1::Missing,
3155 )
3156 .unwrap();
3157 unavailable.finish().unwrap();
3158
3159 let mut captured =
3160 DependencyClosureBuilderV1::new(primary, SourceSetCoverageV1::complete(), 2);
3161 assert!(captured.begin_reference(5, 1));
3162 assert_eq!(captured.prepare_external_key(&first).unwrap(), Some(true));
3163 captured.record_external_open_attempt(&first).unwrap();
3164 assert!(
3165 captured
3166 .push_captured_external(
3167 0,
3168 SourceResourceKindV1::Buffer,
3169 0,
3170 first.clone(),
3171 InputIdentity::from_bytes(b"one"),
3172 )
3173 .unwrap()
3174 );
3175 assert!(captured.begin_reference(5, 1));
3176 assert_eq!(captured.prepare_external_key(&first).unwrap(), Some(false));
3177 assert_eq!(
3178 captured.push_captured_external(
3179 1,
3180 SourceResourceKindV1::Buffer,
3181 1,
3182 first.clone(),
3183 InputIdentity::from_bytes(b"two"),
3184 ),
3185 Err(DependencyClosureError::ExternalOutcomeMismatch)
3186 );
3187 assert_eq!(
3188 captured.push_unavailable(
3189 1,
3190 SourceResourceKindV1::Buffer,
3191 1,
3192 Some(first.clone()),
3193 DependencyResourceUnavailableReasonV1::Missing,
3194 ),
3195 Err(DependencyClosureError::ExternalOutcomeMismatch)
3196 );
3197 captured
3198 .push_external_alias(1, SourceResourceKindV1::Buffer, 1, first)
3199 .unwrap();
3200 captured.finish().unwrap();
3201 }
3202
3203 #[test]
3204 fn finish_requires_all_expected_rows_without_a_real_terminal_stop() {
3205 let builder = DependencyClosureBuilderV1::new(
3206 InputIdentity::from_bytes(b"primary"),
3207 SourceSetCoverageV1::complete(),
3208 1,
3209 );
3210 assert_eq!(
3211 builder.finish(),
3212 Err(DependencyClosureError::ReferenceCountMismatch {
3213 expected: 1,
3214 actual: 0,
3215 })
3216 );
3217 }
3218
3219 #[test]
3220 fn terminal_work_counters_retain_each_n_plus_one_witness() {
3221 let mut path = DependencyClosureBuilderV1::new(
3222 InputIdentity::from_bytes(b"primary"),
3223 SourceSetCoverageV1::complete(),
3224 1,
3225 );
3226 assert!(!path.begin_reference(
3227 DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES,
3228 DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS + 1,
3229 ));
3230 assert!(!path.begin_reference(0, 0));
3231 let path = path.finish().unwrap();
3232 assert_eq!(path.work().inspected_references(), 1);
3233 assert_eq!(
3234 path.work().normalization_bytes_inspected(),
3235 DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES
3236 );
3237 assert_eq!(
3238 path.work().path_components_inspected(),
3239 DEPENDENCY_CLOSURE_V1_MAX_PATH_COMPONENTS + 1
3240 );
3241 assert_eq!(path.work().dedup_probes(), 1);
3242
3243 let mut locator = DependencyClosureBuilderV1::new(
3244 InputIdentity::from_bytes(b"primary"),
3245 SourceSetCoverageV1::complete(),
3246 1,
3247 );
3248 assert!(!locator.begin_reference(DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES + 1, 1));
3249 let locator = locator.finish().unwrap();
3250 assert_eq!(
3251 locator.work().normalization_bytes_inspected(),
3252 DEPENDENCY_CLOSURE_V1_MAX_KEY_BYTES + 1
3253 );
3254 assert_eq!(locator.work().path_components_inspected(), 1);
3255 assert_eq!(locator.work().dedup_probes(), 1);
3256 }
3257
3258 #[test]
3259 fn closure_wire_sequences_reject_n_plus_one_before_decoding_the_sentinel() {
3260 let primary = InputIdentity::from_bytes(b"primary");
3261 let mut builder =
3262 DependencyClosureBuilderV1::new(primary, SourceSetCoverageV1::complete(), 1);
3263 assert!(builder.begin_reference(0, 0));
3264 builder
3265 .push_primary(0, SourceResourceKindV1::Buffer, 0)
3266 .unwrap();
3267 let closure = builder.finish().unwrap();
3268 let base = serde_json::to_value(&closure).unwrap();
3269
3270 let reference = base["references"][0].clone();
3271 let mut references = Vec::with_capacity(DEPENDENCY_CLOSURE_V1_MAX_REFERENCES + 1);
3272 for source_order_index in 0..DEPENDENCY_CLOSURE_V1_MAX_REFERENCES {
3273 let mut row = reference.clone();
3274 row["source_order_index"] = serde_json::json!(source_order_index);
3275 references.push(row);
3276 }
3277 let mut exact = base.clone();
3278 exact["references"] = references.clone().into();
3279 let exact_error = decode_dependency_closure_v1(&serde_json::to_string(&exact).unwrap())
3280 .expect_err("later closure semantics reject the synthetic exact-N prefix");
3281 assert!(!matches!(
3282 exact_error,
3283 DependencyClosureDecodeError::Semantic(ref reason)
3284 if reason == "dependency closure has too many references"
3285 ));
3286 references.push(serde_json::Value::Null);
3287 let mut over = base.clone();
3288 over["references"] = references.into();
3289 assert!(matches!(
3290 decode_dependency_closure_v1(&serde_json::to_string(&over).unwrap()),
3291 Err(DependencyClosureDecodeError::Semantic(reason))
3292 if reason == "dependency closure has too many references"
3293 ));
3294
3295 let external = serde_json::json!({
3296 "key": "a.bin",
3297 "identity": {"sha256": "00".repeat(32), "bytes": 0}
3298 });
3299 let mut external_resources = vec![external; DEPENDENCY_CLOSURE_V1_MAX_EXTERNAL_RESOURCES];
3300 let mut exact = base.clone();
3301 exact["external_resources"] = external_resources.clone().into();
3302 let exact_error = decode_dependency_closure_v1(&serde_json::to_string(&exact).unwrap())
3303 .expect_err("later closure semantics reject duplicate exact-N resources");
3304 assert!(!matches!(
3305 exact_error,
3306 DependencyClosureDecodeError::Semantic(ref reason)
3307 if reason == "dependency closure has too many external resources"
3308 ));
3309 external_resources.push(serde_json::Value::Null);
3310 let mut over = base.clone();
3311 over["external_resources"] = external_resources.into();
3312 assert!(matches!(
3313 decode_dependency_closure_v1(&serde_json::to_string(&over).unwrap()),
3314 Err(DependencyClosureDecodeError::Semantic(reason))
3315 if reason == "dependency closure has too many external resources"
3316 ));
3317
3318 let mut reasons = vec![
3319 serde_json::json!("source_declarations_partial"),
3320 serde_json::json!("source_declarations_unavailable"),
3321 serde_json::json!("capture_unavailable"),
3322 serde_json::json!("refused_resource"),
3323 serde_json::json!("unavailable_resource"),
3324 serde_json::json!("resource_budget_exceeded"),
3325 serde_json::json!("unmodeled_resource_domain"),
3326 ];
3327 reasons.push(serde_json::Value::Null);
3328 let mut over = base;
3329 over["coverage"] = serde_json::json!({"state": "partial", "reasons": reasons});
3330 assert!(matches!(
3331 decode_dependency_closure_v1(&serde_json::to_string(&over).unwrap()),
3332 Err(DependencyClosureDecodeError::Semantic(reason))
3333 if reason == "dependency coverage reasons must be strictly ordered"
3334 ));
3335 }
3336
3337 #[test]
3338 fn unmodeled_domain_prevents_complete_identity() {
3339 let mut builder = DependencyClosureBuilderV1::new(
3340 InputIdentity::from_bytes(b"primary"),
3341 SourceSetCoverageV1::complete(),
3342 0,
3343 );
3344 builder.mark_unmodeled_resource_domain();
3345 let closure = builder.finish().unwrap();
3346 assert!(matches!(
3347 closure.coverage(),
3348 DependencyClosureCoverageV1::Partial { reasons }
3349 if reasons == &[DependencyClosureCoverageReasonV1::UnmodeledResourceDomain]
3350 ));
3351 assert!(closure.identity().is_none());
3352 }
3353
3354 #[test]
3355 fn equal_content_at_distinct_keys_remains_two_resources() {
3356 let mut builder = DependencyClosureBuilderV1::new(
3357 InputIdentity::from_bytes(b"primary"),
3358 SourceSetCoverageV1::complete(),
3359 2,
3360 );
3361 for (index, name) in ["a.bin", "b.bin"].into_iter().enumerate() {
3362 let key = DependencyResourceKeyV1::from_source_str(
3363 name,
3364 ResourceKeySyntaxV1::ParserRelativePath,
3365 )
3366 .unwrap();
3367 assert!(builder.begin_reference(name.len(), 1));
3368 assert_eq!(builder.prepare_external_key(&key).unwrap(), Some(true));
3369 builder.record_external_open_attempt(&key).unwrap();
3370 assert!(
3371 builder
3372 .push_captured_external(
3373 index,
3374 SourceResourceKindV1::Buffer,
3375 index as u64,
3376 key,
3377 InputIdentity::from_bytes(b"same"),
3378 )
3379 .unwrap()
3380 );
3381 }
3382 let closure = builder.finish().unwrap();
3383 assert!(closure.coverage().is_complete());
3384 assert_eq!(closure.external_resources().len(), 2);
3385 assert_eq!(closure.work().external_open_attempts(), 2);
3386 assert_eq!(
3387 closure
3388 .external_resources()
3389 .iter()
3390 .map(|resource| resource.identity())
3391 .collect::<Vec<_>>(),
3392 vec![
3393 &InputIdentity::from_bytes(b"same"),
3394 &InputIdentity::from_bytes(b"same"),
3395 ]
3396 );
3397 }
3398}