1use super::*;
2
3#[derive(Clone, Debug, PartialEq)]
5pub struct PackagedArtifactProductionProfileSummary {
6 pub profile_id: &'static str,
8 pub label: &'static str,
10 pub artifact_version: u16,
12 pub time_range: TimeRange,
14 pub source_provenance: String,
16 pub body_coverage: PackagedBodyCoverageSummary,
18 pub artifact_profile: ArtifactProfile,
20 pub speed_policy: pleiades_compression::SpeedPolicy,
22 pub generation_policy: PackagedArtifactGenerationPolicy,
24 pub request_policy: PackagedRequestPolicySummary,
26 pub lookup_epoch_policy: PackagedLookupEpochPolicy,
28 pub frame_treatment: PackagedFrameTreatmentSummary,
30 pub storage_summary: PackagedArtifactStorageSummary,
32 pub target_thresholds: PackagedArtifactTargetThresholdSummary,
34}
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
38pub enum PackagedArtifactProductionProfileSummaryValidationError {
39 FieldOutOfSync { field: &'static str },
41}
42
43impl fmt::Display for PackagedArtifactProductionProfileSummaryValidationError {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 match self {
46 Self::FieldOutOfSync { field } => write!(
47 f,
48 "the packaged artifact production profile summary field `{field}` is out of sync with the current posture"
49 ),
50 }
51 }
52}
53
54impl std::error::Error for PackagedArtifactProductionProfileSummaryValidationError {}
55
56impl PackagedArtifactProductionProfileSummary {
57 pub fn summary_line(&self) -> String {
59 format!(
60 "Packaged artifact production profile draft: profile id={}; label={}; version={}; time range={}; source provenance={}; body coverage={}; artifact profile={}; output support={}; speed policy={}; generation policy={}; segment strategy={}; request policy={}; lookup epoch policy={}; frame treatment={}; storage/reconstruction={}; {}",
61 self.profile_id,
62 self.label,
63 self.artifact_version,
64 self.time_range,
65 self.source_provenance,
66 self.body_coverage,
67 self.artifact_profile,
68 self.artifact_profile.output_support_entries_summary_line(),
69 self.speed_policy,
70 self.generation_policy,
71 self.generation_policy.segment_strategy(),
72 self.request_policy,
73 self.lookup_epoch_policy.summary_line(),
74 self.frame_treatment,
75 self.storage_summary,
76 self.target_thresholds,
77 )
78 }
79
80 pub fn validate(&self) -> Result<(), PackagedArtifactProductionProfileSummaryValidationError> {
82 if self.profile_id != ARTIFACT_PROFILE_ID {
83 return Err(
84 PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
85 field: "profile_id",
86 },
87 );
88 }
89 if self.label != ARTIFACT_LABEL {
90 return Err(
91 PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
92 field: "label",
93 },
94 );
95 }
96 if self.artifact_version != packaged_artifact().header.version {
97 return Err(
98 PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
99 field: "artifact_version",
100 },
101 );
102 }
103 if self.time_range != artifact_time_range(packaged_artifact()) {
104 return Err(
105 PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
106 field: "time_range",
107 },
108 );
109 }
110 if self.source_provenance != production_generation_source_summary_for_report() {
111 return Err(
112 PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
113 field: "source_provenance",
114 },
115 );
116 }
117 self.body_coverage.validate().map_err(|_| {
118 PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
119 field: "body_coverage",
120 }
121 })?;
122 if self.artifact_profile != packaged_artifact_profile_summary_details().profile {
123 return Err(
124 PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
125 field: "artifact_profile",
126 },
127 );
128 }
129 if self.speed_policy != self.artifact_profile.speed_policy {
130 return Err(
131 PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
132 field: "speed_policy",
133 },
134 );
135 }
136 self.generation_policy.validate().map_err(|_| {
137 PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
138 field: "generation_policy",
139 }
140 })?;
141 self.request_policy.validate().map_err(|_| {
142 PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
143 field: "request_policy",
144 }
145 })?;
146 if self.lookup_epoch_policy != packaged_lookup_epoch_policy_summary_details().policy {
147 return Err(
148 PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
149 field: "lookup_epoch_policy",
150 },
151 );
152 }
153 self.frame_treatment.validate().map_err(|_| {
154 PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
155 field: "frame_treatment",
156 }
157 })?;
158 self.storage_summary.validate().map_err(|_| {
159 PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
160 field: "storage_summary",
161 }
162 })?;
163 self.target_thresholds.validate().map_err(|_| {
164 PackagedArtifactProductionProfileSummaryValidationError::FieldOutOfSync {
165 field: "target_thresholds",
166 }
167 })?;
168
169 Ok(())
170 }
171
172 pub fn validated_summary_line(
174 &self,
175 ) -> Result<String, PackagedArtifactProductionProfileSummaryValidationError> {
176 self.validate()?;
177 Ok(self.summary_line())
178 }
179}
180
181impl fmt::Display for PackagedArtifactProductionProfileSummary {
182 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183 f.write_str(&self.summary_line())
184 }
185}
186
187pub fn packaged_artifact_production_profile_summary_details(
189) -> PackagedArtifactProductionProfileSummary {
190 let artifact = packaged_artifact();
191 let profile_summary = packaged_artifact_profile_summary_details();
192 let speed_policy = profile_summary.profile.speed_policy;
193 let summary = PackagedArtifactProductionProfileSummary {
194 profile_id: ARTIFACT_PROFILE_ID,
195 label: ARTIFACT_LABEL,
196 artifact_version: artifact.header.version,
197 time_range: artifact_time_range(artifact),
198 source_provenance: production_generation_source_summary_for_report(),
199 body_coverage: packaged_body_coverage_summary_details(),
200 artifact_profile: profile_summary.profile,
201 speed_policy,
202 generation_policy: PackagedArtifactGenerationPolicy::AdjacentSameBodyQuadraticWindows,
203 request_policy: packaged_request_policy_summary_details(),
204 lookup_epoch_policy: packaged_lookup_epoch_policy_summary_details().policy,
205 frame_treatment: packaged_frame_treatment_summary_details(),
206 storage_summary: packaged_artifact_storage_summary_details(),
207 target_thresholds: packaged_artifact_target_threshold_summary_details(),
208 };
209 debug_assert!(summary.validate().is_ok());
210 summary
211}
212
213pub fn packaged_artifact_production_profile_summary_for_report() -> String {
215 static SUMMARY: OnceLock<String> = OnceLock::new();
216 SUMMARY
217 .get_or_init(|| {
218 let summary = packaged_artifact_production_profile_summary_details();
219 match summary.validated_summary_line() {
220 Ok(line) => line,
221 Err(error) => {
222 format!("Packaged artifact production profile draft: unavailable ({error})")
223 }
224 }
225 })
226 .clone()
227}
228
229pub fn packaged_artifact_production_profile_summary() -> &'static str {
231 static SUMMARY: OnceLock<String> = OnceLock::new();
232 SUMMARY
233 .get_or_init(|| {
234 let summary = packaged_artifact_production_profile_summary_details();
235 match summary.validated_summary_line() {
236 Ok(rendered) => rendered,
237 Err(error) => {
238 format!("Packaged artifact production profile draft: unavailable ({error})")
239 }
240 }
241 })
242 .as_str()
243}
244
245#[derive(Clone, Debug, PartialEq)]
247pub struct PackagedArtifactGeneratorParameters {
248 pub profile_id: &'static str,
250 pub label: &'static str,
252 pub artifact_version: u16,
254 pub time_range: TimeRange,
256 pub source_provenance: String,
258 pub checksum: u64,
260 pub artifact_size_bytes: usize,
262 pub body_coverage: PackagedBodyCoverageSummary,
264 pub residual_body_coverage: ArtifactResidualBodyCoverageSummary,
266 pub artifact_profile: ArtifactProfile,
268 pub speed_policy: pleiades_compression::SpeedPolicy,
270 pub generation_policy: PackagedArtifactGenerationPolicy,
272 pub request_policy: PackagedRequestPolicySummary,
274 pub lookup_epoch_policy: PackagedLookupEpochPolicy,
276 pub frame_treatment: PackagedFrameTreatmentSummary,
278 pub storage_summary: PackagedArtifactStorageSummary,
280 pub target_thresholds: PackagedArtifactTargetThresholdSummary,
282}
283
284impl PackagedArtifactGeneratorParameters {
285 pub fn summary_line(&self) -> String {
287 format!(
288 "Packaged artifact generator parameters: profile id={}; label={}; version={}; time range={}; source provenance={}; checksum=0x{:016x}; artifact size={} bytes; body coverage={}; residual bodies={}; artifact profile={}; output support={}; speed policy={}; generation policy={}; segment strategy={}; request policy={}; lookup epoch policy={}; frame treatment={}; storage/reconstruction={}; {}",
289 self.profile_id,
290 self.label,
291 self.artifact_version,
292 self.time_range,
293 self.source_provenance,
294 self.checksum,
295 self.artifact_size_bytes,
296 self.body_coverage,
297 self.residual_body_coverage.summary_line_with_body_count(),
298 self.artifact_profile,
299 self.artifact_profile.output_support_entries_summary_line(),
300 self.speed_policy,
301 self.generation_policy,
302 self.generation_policy.segment_strategy(),
303 self.request_policy,
304 self.lookup_epoch_policy.summary_line(),
305 self.frame_treatment,
306 self.storage_summary,
307 self.target_thresholds,
308 )
309 }
310
311 pub fn validate(&self) -> Result<(), pleiades_compression::CompressionError> {
313 let current = packaged_artifact_production_profile_summary_details();
314 let artifact = packaged_artifact();
315
316 if self.profile_id != current.profile_id {
317 return Err(pleiades_compression::CompressionError::new(
318 pleiades_compression::CompressionErrorKind::InvalidFormat,
319 "packaged artifact generator parameters profile id does not match the current production profile",
320 ));
321 }
322 if self.label != current.label {
323 return Err(pleiades_compression::CompressionError::new(
324 pleiades_compression::CompressionErrorKind::InvalidFormat,
325 "packaged artifact generator parameters label does not match the current production profile",
326 ));
327 }
328 if self.artifact_version != current.artifact_version {
329 return Err(pleiades_compression::CompressionError::new(
330 pleiades_compression::CompressionErrorKind::InvalidFormat,
331 "packaged artifact generator parameters version does not match the current production profile",
332 ));
333 }
334 if self.time_range != current.time_range {
335 return Err(pleiades_compression::CompressionError::new(
336 pleiades_compression::CompressionErrorKind::InvalidFormat,
337 "packaged artifact generator parameters time range does not match the current production profile",
338 ));
339 }
340 if self.source_provenance != current.source_provenance {
341 return Err(pleiades_compression::CompressionError::new(
342 pleiades_compression::CompressionErrorKind::InvalidFormat,
343 "packaged artifact generator parameters source provenance does not match the current production profile",
344 ));
345 }
346 if self.checksum != artifact.checksum {
347 return Err(pleiades_compression::CompressionError::new(
348 pleiades_compression::CompressionErrorKind::InvalidFormat,
349 "packaged artifact generator parameters checksum does not match the current packaged artifact",
350 ));
351 }
352 let expected_artifact_size_bytes = packaged_artifact_encoded_bytes(artifact);
353 if self.artifact_size_bytes != expected_artifact_size_bytes {
354 return Err(pleiades_compression::CompressionError::new(
355 pleiades_compression::CompressionErrorKind::InvalidFormat,
356 format!(
357 "packaged artifact generator parameters artifact size {} bytes does not match the current packaged artifact size {} bytes",
358 self.artifact_size_bytes,
359 expected_artifact_size_bytes
360 ),
361 ));
362 }
363 if self.body_coverage != current.body_coverage {
364 return Err(pleiades_compression::CompressionError::new(
365 pleiades_compression::CompressionErrorKind::InvalidFormat,
366 "packaged artifact generator parameters body coverage does not match the current production profile",
367 ));
368 }
369 if self.residual_body_coverage != artifact.residual_body_coverage_summary() {
370 return Err(pleiades_compression::CompressionError::new(
371 pleiades_compression::CompressionErrorKind::InvalidFormat,
372 "packaged artifact generator parameters residual body coverage does not match the current packaged artifact",
373 ));
374 }
375 if self.artifact_profile != current.artifact_profile {
376 return Err(pleiades_compression::CompressionError::new(
377 pleiades_compression::CompressionErrorKind::InvalidFormat,
378 "packaged artifact generator parameters artifact profile does not match the current production profile",
379 ));
380 }
381 if self.speed_policy != current.speed_policy {
382 return Err(pleiades_compression::CompressionError::new(
383 pleiades_compression::CompressionErrorKind::InvalidFormat,
384 "packaged artifact generator parameters speed policy does not match the current production profile",
385 ));
386 }
387 if self.generation_policy != current.generation_policy {
388 return Err(pleiades_compression::CompressionError::new(
389 pleiades_compression::CompressionErrorKind::InvalidFormat,
390 "packaged artifact generator parameters generation policy does not match the current production profile",
391 ));
392 }
393 if self.request_policy != current.request_policy {
394 return Err(pleiades_compression::CompressionError::new(
395 pleiades_compression::CompressionErrorKind::InvalidFormat,
396 "packaged artifact generator parameters request policy does not match the current production profile",
397 ));
398 }
399 if self.lookup_epoch_policy != current.lookup_epoch_policy {
400 return Err(pleiades_compression::CompressionError::new(
401 pleiades_compression::CompressionErrorKind::InvalidFormat,
402 "packaged artifact generator parameters lookup epoch policy does not match the current production profile",
403 ));
404 }
405 if self.frame_treatment != current.frame_treatment {
406 return Err(pleiades_compression::CompressionError::new(
407 pleiades_compression::CompressionErrorKind::InvalidFormat,
408 "packaged artifact generator parameters frame-treatment policy does not match the current production profile",
409 ));
410 }
411 if self.storage_summary != current.storage_summary {
412 return Err(pleiades_compression::CompressionError::new(
413 pleiades_compression::CompressionErrorKind::InvalidFormat,
414 "packaged artifact generator parameters storage summary does not match the current production profile",
415 ));
416 }
417 self.target_thresholds
418 .state
419 .validate_production_ready()
420 .map_err(|_| {
421 pleiades_compression::CompressionError::new(
422 pleiades_compression::CompressionErrorKind::InvalidFormat,
423 "packaged artifact generator parameters target thresholds do not match the current production profile",
424 )
425 })?;
426 if self.target_thresholds != current.target_thresholds {
427 return Err(pleiades_compression::CompressionError::new(
428 pleiades_compression::CompressionErrorKind::InvalidFormat,
429 "packaged artifact generator parameters target thresholds do not match the current production profile",
430 ));
431 }
432
433 Ok(())
434 }
435
436 pub fn validated_summary_line(&self) -> Result<String, pleiades_compression::CompressionError> {
438 self.validate()?;
439 Ok(self.summary_line())
440 }
441}
442
443impl fmt::Display for PackagedArtifactGeneratorParameters {
444 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
445 f.write_str(&self.summary_line())
446 }
447}
448
449pub(crate) fn fnv1a64(bytes: &[u8]) -> u64 {
450 const OFFSET_BASIS: u64 = 0xcbf29ce484222325;
451 const PRIME: u64 = 0x100000001b3;
452
453 let mut hash = OFFSET_BASIS;
454 for byte in bytes {
455 hash ^= u64::from(*byte);
456 hash = hash.wrapping_mul(PRIME);
457 }
458 hash
459}
460
461fn packaged_artifact_generation_manifest_checksum(
462 parameters: &PackagedArtifactGeneratorParameters,
463 regeneration: &PackagedArtifactRegenerationSummary,
464) -> u64 {
465 let mut bytes = Vec::new();
466 bytes.extend_from_slice(parameters.summary_line().as_bytes());
467 bytes.push(b'\n');
468 bytes.extend_from_slice(regeneration.summary_line().as_bytes());
469 fnv1a64(&bytes)
470}
471
472#[derive(Clone, Debug, PartialEq)]
474pub struct PackagedArtifactGenerationManifest {
475 pub parameters: PackagedArtifactGeneratorParameters,
477 pub regeneration: PackagedArtifactRegenerationSummary,
479 pub manifest_checksum: u64,
481}
482
483impl PackagedArtifactGenerationManifest {
484 pub fn summary_line(&self) -> String {
486 format!(
487 "Packaged artifact generation manifest: manifest checksum=0x{:016x}; {}; regeneration={}",
488 self.manifest_checksum, self.parameters, self.regeneration,
489 )
490 }
491
492 pub fn validate(&self) -> Result<(), pleiades_compression::CompressionError> {
494 self.parameters.validate()?;
495 self.regeneration.validate()?;
496
497 let expected_checksum =
498 packaged_artifact_generation_manifest_checksum(&self.parameters, &self.regeneration);
499 if self.manifest_checksum != expected_checksum {
500 return Err(pleiades_compression::CompressionError::new(
501 pleiades_compression::CompressionErrorKind::InvalidFormat,
502 format!(
503 "packaged artifact generation manifest checksum 0x{:016x} does not match the current packaged-artifact manifest checksum 0x{:016x}",
504 self.manifest_checksum,
505 expected_checksum
506 ),
507 ));
508 }
509
510 Ok(())
511 }
512
513 pub fn validated_summary_line(&self) -> Result<String, pleiades_compression::CompressionError> {
515 self.validate()?;
516 Ok(self.summary_line())
517 }
518}
519
520impl fmt::Display for PackagedArtifactGenerationManifest {
521 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
522 f.write_str(&self.summary_line())
523 }
524}
525
526pub fn packaged_artifact_generator_parameters_details() -> PackagedArtifactGeneratorParameters {
528 let summary = packaged_artifact_production_profile_summary_details();
529 let regeneration = packaged_artifact_regeneration_summary_details();
530 let parameters = PackagedArtifactGeneratorParameters {
531 profile_id: summary.profile_id,
532 label: summary.label,
533 artifact_version: summary.artifact_version,
534 time_range: summary.time_range,
535 source_provenance: summary.source_provenance,
536 checksum: regeneration.checksum,
537 artifact_size_bytes: regeneration.artifact_size_bytes,
538 body_coverage: summary.body_coverage,
539 residual_body_coverage: regeneration.residual_body_coverage_summary(),
540 artifact_profile: summary.artifact_profile,
541 speed_policy: summary.speed_policy,
542 generation_policy: summary.generation_policy,
543 request_policy: summary.request_policy,
544 lookup_epoch_policy: summary.lookup_epoch_policy,
545 frame_treatment: summary.frame_treatment,
546 storage_summary: summary.storage_summary,
547 target_thresholds: summary.target_thresholds,
548 };
549 debug_assert!(parameters.validate().is_ok());
550 parameters
551}
552
553pub fn packaged_artifact_generation_manifest_details() -> PackagedArtifactGenerationManifest {
555 let parameters = packaged_artifact_generator_parameters_details();
556 let regeneration = packaged_artifact_regeneration_summary_details();
557 let manifest = PackagedArtifactGenerationManifest {
558 manifest_checksum: packaged_artifact_generation_manifest_checksum(
559 ¶meters,
560 ®eneration,
561 ),
562 parameters,
563 regeneration,
564 };
565 debug_assert!(manifest.validate().is_ok());
566 manifest
567}
568
569pub fn packaged_artifact_generation_manifest_for_report() -> String {
571 static SUMMARY: OnceLock<String> = OnceLock::new();
572 SUMMARY
573 .get_or_init(|| {
574 let manifest = packaged_artifact_generation_manifest_details();
575 match manifest.validated_summary_line() {
576 Ok(line) => line,
577 Err(error) => {
578 format!("Packaged artifact generation manifest: unavailable ({error})")
579 }
580 }
581 })
582 .clone()
583}
584
585pub fn packaged_artifact_generation_manifest_checksum_for_report() -> String {
587 static SUMMARY: OnceLock<String> = OnceLock::new();
588 SUMMARY
589 .get_or_init(|| {
590 let manifest = packaged_artifact_generation_manifest_details();
591 match manifest.validate() {
592 Ok(()) => format!(
593 "Packaged artifact generation manifest checksum: 0x{:016x}",
594 manifest.manifest_checksum
595 ),
596 Err(error) => {
597 format!("Packaged artifact generation manifest checksum: unavailable ({error})")
598 }
599 }
600 })
601 .clone()
602}
603
604pub fn packaged_artifact_generation_manifest() -> &'static str {
606 static SUMMARY: OnceLock<String> = OnceLock::new();
607 SUMMARY
608 .get_or_init(|| {
609 let manifest = packaged_artifact_generation_manifest_details();
610 match manifest.validated_summary_line() {
611 Ok(rendered) => rendered,
612 Err(error) => {
613 format!("Packaged artifact generation manifest: unavailable ({error})")
614 }
615 }
616 })
617 .as_str()
618}
619
620#[derive(Clone, Debug, PartialEq, Eq)]
621pub struct PackagedArtifactProfileSummary {
622 pub body_count: usize,
624 pub bodies: Vec<CelestialBody>,
626 pub endian_policy: EndianPolicy,
628 pub profile: ArtifactProfile,
630}
631
632impl PackagedArtifactProfileSummary {
633 pub fn profile_coverage_summary(&self) -> ArtifactProfileCoverageSummary {
635 ArtifactProfileCoverageSummary::new(self.profile.clone(), self.bodies.clone())
636 }
637
638 pub fn validate(&self) -> Result<(), pleiades_compression::CompressionError> {
641 let artifact = packaged_artifact();
642 if self.endian_policy != artifact.header.endian_policy {
643 return Err(pleiades_compression::CompressionError::new(
644 pleiades_compression::CompressionErrorKind::InvalidFormat,
645 "packaged artifact profile byte-order policy does not match the checked-in packaged artifact header",
646 ));
647 }
648 if self.profile != artifact.profile_coverage_summary().profile {
649 return Err(pleiades_compression::CompressionError::new(
650 pleiades_compression::CompressionErrorKind::InvalidFormat,
651 "packaged artifact profile metadata does not match the checked-in packaged artifact profile",
652 ));
653 }
654
655 if self.body_count != self.bodies.len() {
656 return Err(pleiades_compression::CompressionError::new(
657 pleiades_compression::CompressionErrorKind::InvalidFormat,
658 "packaged artifact profile body count does not match bundled body list",
659 ));
660 }
661
662 if self.bodies.is_empty() {
663 let coverage = self.profile_coverage_summary();
664 coverage.validate()?;
665 return Ok(());
666 }
667
668 if self
669 .bodies
670 .iter()
671 .enumerate()
672 .any(|(index, body)| self.bodies[..index].contains(body))
673 {
674 let coverage = self.profile_coverage_summary();
675 coverage.validate()?;
676 return Ok(());
677 }
678
679 if self.bodies.as_slice() != packaged_bodies() {
680 return Err(pleiades_compression::CompressionError::new(
681 pleiades_compression::CompressionErrorKind::InvalidFormat,
682 format!(
683 "packaged artifact profile bundled body list does not match the checked-in packaged body set: expected [{}]; got [{}]",
684 join_display(packaged_bodies()),
685 join_display(&self.bodies)
686 ),
687 ));
688 }
689
690 let coverage = self.profile_coverage_summary();
691 coverage.validate()?;
692
693 Ok(())
694 }
695
696 pub fn validated_summary_line(&self) -> Result<String, pleiades_compression::CompressionError> {
698 self.validate()?;
699 Ok(self.summary_line())
700 }
701
702 pub fn validated_summary_line_with_bodies(
704 &self,
705 ) -> Result<String, pleiades_compression::CompressionError> {
706 self.validate()?;
707 Ok(self.summary_line_with_bodies())
708 }
709
710 pub fn validated_summary_line_with_output_support(
712 &self,
713 ) -> Result<String, pleiades_compression::CompressionError> {
714 self.validate()?;
715 Ok(format!(
716 "{}; output support: {}",
717 self.summary_line_with_bodies(),
718 self.profile.validated_output_support_summary_line()?
719 ))
720 }
721
722 pub fn summary_line(&self) -> String {
724 let coverage = self.profile_coverage_summary();
725 format!(
726 "byte order: {}; {}",
727 self.endian_policy,
728 coverage.summary_line()
729 )
730 }
731
732 pub fn output_support_summary_line(&self) -> String {
734 self.profile.output_support_summary_line()
735 }
736
737 pub fn summary_line_with_bodies(&self) -> String {
739 let coverage = self.profile_coverage_summary();
740 format!(
741 "byte order: {}; {}",
742 self.endian_policy,
743 coverage.summary_line_with_bodies(),
744 )
745 }
746
747 pub fn summary_line_with_output_support(&self) -> String {
750 format!(
751 "{}; output support: {}",
752 self.summary_line_with_bodies(),
753 self.output_support_summary_line()
754 )
755 }
756}
757
758impl fmt::Display for PackagedArtifactProfileSummary {
759 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
760 f.write_str(&self.summary_line())
761 }
762}
763
764pub fn packaged_artifact_profile_summary_details() -> PackagedArtifactProfileSummary {
766 let artifact = packaged_artifact();
767 let coverage = artifact.profile_coverage_summary();
768 let summary = PackagedArtifactProfileSummary {
769 body_count: coverage.body_count,
770 bodies: coverage.bodies,
771 endian_policy: artifact.header.endian_policy,
772 profile: coverage.profile,
773 };
774 debug_assert!(summary.validate().is_ok());
775 summary
776}
777
778pub fn packaged_artifact_profile_coverage_summary_details() -> ArtifactProfileCoverageSummary {
780 packaged_artifact_profile_summary_details().profile_coverage_summary()
781}
782
783pub fn packaged_artifact_profile_coverage_summary_for_report() -> String {
785 static SUMMARY: OnceLock<String> = OnceLock::new();
786 SUMMARY
787 .get_or_init(|| {
788 let summary = packaged_artifact_profile_summary_details();
789 match summary.validate() {
790 Ok(()) => match summary
791 .profile_coverage_summary()
792 .validated_summary_line_with_bodies()
793 {
794 Ok(line) => line,
795 Err(error) => format!("Artifact profile coverage: unavailable ({error})"),
796 },
797 Err(error) => format!("Artifact profile coverage: unavailable ({error})"),
798 }
799 })
800 .clone()
801}
802
803pub fn packaged_artifact_profile_summary() -> String {
808 render_packaged_artifact_profile_summary(&packaged_artifact_profile_summary_details(), false)
809}
810
811pub fn packaged_artifact_profile_summary_with_body_coverage() -> String {
816 render_packaged_artifact_profile_summary(&packaged_artifact_profile_summary_details(), true)
817}
818
819pub fn packaged_artifact_profile_summary_with_output_support() -> String {
824 let summary = packaged_artifact_profile_summary_details();
825 match summary.validated_summary_line_with_output_support() {
826 Ok(line) => line,
827 Err(error) => {
828 format!("Packaged artifact profile with output support: unavailable ({error})")
829 }
830 }
831}
832
833pub fn packaged_artifact_profile_summary_with_output_support_for_report() -> String {
835 packaged_artifact_profile_summary_with_output_support()
836}
837
838#[derive(Clone, Debug, PartialEq, Eq)]
840pub struct PackagedArtifactOutputSupportSummary {
841 pub profile: ArtifactProfile,
843}
844
845fn validate_packaged_artifact_output_support_profile(
846 profile: &ArtifactProfile,
847) -> Result<(), pleiades_compression::CompressionError> {
848 let expected_states = [
849 (
850 ArtifactOutput::EclipticCoordinates,
851 ArtifactOutputSupport::Derived,
852 ),
853 (
854 ArtifactOutput::EquatorialCoordinates,
855 ArtifactOutputSupport::Derived,
856 ),
857 (
858 ArtifactOutput::ApparentCorrections,
859 ArtifactOutputSupport::Unsupported,
860 ),
861 (
862 ArtifactOutput::TopocentricCoordinates,
863 ArtifactOutputSupport::Unsupported,
864 ),
865 (
866 ArtifactOutput::SiderealCoordinates,
867 ArtifactOutputSupport::Unsupported,
868 ),
869 (ArtifactOutput::Motion, ArtifactOutputSupport::Derived),
870 ];
871
872 for (output, expected_support) in expected_states {
873 let actual_support = profile.output_support(output);
874 if actual_support != expected_support {
875 return Err(pleiades_compression::CompressionError::new(
876 pleiades_compression::CompressionErrorKind::InvalidFormat,
877 format!(
878 "packaged artifact output support summary is out of sync with the bundled artifact profile field `output_support[{output}]`: expected {expected_support}, found {actual_support}"
879 ),
880 ));
881 }
882 }
883
884 Ok(())
885}
886
887impl PackagedArtifactOutputSupportSummary {
888 pub fn validate(&self) -> Result<(), pleiades_compression::CompressionError> {
892 self.profile.validate()?;
893 validate_packaged_artifact_output_support_profile(&self.profile)
894 }
895
896 pub fn validated_summary_line(&self) -> Result<String, pleiades_compression::CompressionError> {
898 self.validate()?;
899 Ok(self.summary_line())
900 }
901
902 pub fn summary_line(&self) -> String {
904 self.profile.output_support_summary_line()
905 }
906}
907
908impl fmt::Display for PackagedArtifactOutputSupportSummary {
909 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
910 f.write_str(&self.summary_line())
911 }
912}
913
914pub fn packaged_artifact_output_support_summary_details() -> PackagedArtifactOutputSupportSummary {
916 let summary = PackagedArtifactOutputSupportSummary {
917 profile: packaged_artifact_profile_summary_details().profile,
918 };
919 debug_assert!(summary.validate().is_ok());
920 summary
921}
922
923pub fn packaged_artifact_output_support_summary_for_report() -> String {
925 static SUMMARY: OnceLock<String> = OnceLock::new();
926 SUMMARY
927 .get_or_init(|| {
928 let summary = packaged_artifact_output_support_summary_details();
929 match summary.validated_summary_line() {
930 Ok(rendered) => rendered,
931 Err(error) => format!("unavailable ({error})"),
932 }
933 })
934 .clone()
935}
936
937#[derive(Clone, Copy, Debug, PartialEq, Eq)]
939pub struct PackagedArtifactSpeedPolicySummary {
940 pub policy: SpeedPolicy,
942}
943
944#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
946pub enum PackagedArtifactSpeedPolicySummaryValidationError {
947 FieldOutOfSync { field: &'static str },
949}
950
951impl fmt::Display for PackagedArtifactSpeedPolicySummaryValidationError {
952 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
953 match self {
954 Self::FieldOutOfSync { field } => write!(
955 f,
956 "the packaged artifact speed-policy summary field `{field}` is out of sync with the current posture"
957 ),
958 }
959 }
960}
961
962impl std::error::Error for PackagedArtifactSpeedPolicySummaryValidationError {}
963
964impl PackagedArtifactSpeedPolicySummary {
965 pub fn summary_line(self) -> String {
967 format!(
968 "{}; motion output support={}",
969 self.policy,
970 self.policy.motion_output_support()
971 )
972 }
973
974 pub fn validate(&self) -> Result<(), PackagedArtifactSpeedPolicySummaryValidationError> {
976 let current_policy = packaged_artifact_profile_summary_details()
977 .profile
978 .speed_policy;
979 if self.policy != current_policy {
980 return Err(
981 PackagedArtifactSpeedPolicySummaryValidationError::FieldOutOfSync {
982 field: "policy",
983 },
984 );
985 }
986 Ok(())
987 }
988
989 pub fn validated_summary_line(
991 &self,
992 ) -> Result<String, PackagedArtifactSpeedPolicySummaryValidationError> {
993 self.validate()?;
994 Ok(self.summary_line())
995 }
996}
997
998impl fmt::Display for PackagedArtifactSpeedPolicySummary {
999 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1000 f.write_str(&self.summary_line())
1001 }
1002}
1003
1004const PACKAGED_ARTIFACT_SPEED_POLICY_SUMMARY: PackagedArtifactSpeedPolicySummary =
1005 PackagedArtifactSpeedPolicySummary {
1006 policy: SpeedPolicy::FittedDerivative,
1007 };
1008
1009pub fn packaged_artifact_speed_policy_summary_details() -> PackagedArtifactSpeedPolicySummary {
1011 let summary = PACKAGED_ARTIFACT_SPEED_POLICY_SUMMARY;
1012 debug_assert!(summary.validate().is_ok());
1013 summary
1014}
1015
1016pub fn packaged_artifact_speed_policy_summary_for_report() -> String {
1018 static SUMMARY: OnceLock<String> = OnceLock::new();
1019 SUMMARY
1020 .get_or_init(|| {
1021 let summary = packaged_artifact_speed_policy_summary_details();
1022 match summary.validate() {
1023 Ok(()) => summary.summary_line(),
1024 Err(error) => format!("unavailable ({error})"),
1025 }
1026 })
1027 .clone()
1028}
1029
1030pub(crate) fn render_packaged_artifact_profile_summary(
1031 summary: &PackagedArtifactProfileSummary,
1032 with_bodies: bool,
1033) -> String {
1034 if with_bodies {
1035 match summary.validated_summary_line_with_bodies() {
1036 Ok(line) => line,
1037 Err(error) => {
1038 format!("Packaged artifact profile with bundled bodies: unavailable ({error})")
1039 }
1040 }
1041 } else {
1042 match summary.validated_summary_line() {
1043 Ok(line) => line,
1044 Err(error) => format!("Packaged artifact profile: unavailable ({error})"),
1045 }
1046 }
1047}