backhopper_core/snapshot/
freshness.rs1use std::fmt;
6
7use crate::config::Project;
8use crate::digest::DigestWriter;
9use crate::model::names::{ExtractionSettingsDigest, ExtractorVersion};
10use crate::model::snapshot::{FORMAT_VERSION, SnapshotHeader};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum StaleReason {
14 Unreadable,
15 FormatVersion { found: u32 },
16 ExtractorVersion { found: String },
17 ExtractionSettings,
18 HostFacts,
19 MissingApp,
20}
21
22impl fmt::Display for StaleReason {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 match self {
25 Self::Unreadable => f.write_str("the file cannot be read"),
26 Self::FormatVersion { found } => {
27 write!(f, "format-version {found}, expected {FORMAT_VERSION}")
28 }
29 Self::ExtractorVersion { found } if found.is_empty() => {
30 f.write_str("no extractor version recorded")
31 }
32 Self::ExtractorVersion { found } => write!(f, "extractor version {found}"),
33 Self::ExtractionSettings => {
34 f.write_str("made with other extraction settings than the configured ones")
35 }
36 Self::HostFacts => f.write_str("made with other host release facts"),
37 Self::MissingApp => f.write_str("does not hold an app the plan needs"),
38 }
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct ExpectedExtraction {
44 pub extractor: ExtractorVersion,
45 pub settings: ExtractionSettingsDigest,
46}
47
48#[must_use]
50pub fn extraction_settings_digest(project: &Project) -> ExtractionSettingsDigest {
51 let mut writer = DigestWriter::new("extraction settings");
52 writer
53 .text(&project.layout.to_string())
54 .text(&project.manifest.to_string());
55 feed_set(&mut writer, project.scan_paths.iter().map(String::as_str));
56 feed_set(&mut writer, project.app_roots.iter().map(String::as_str));
57 feed_set(
58 &mut writer,
59 project.include_apps.iter().map(|app| app.as_str()),
60 );
61 feed_set(
62 &mut writer,
63 project.exclude_apps.iter().map(|app| app.as_str()),
64 );
65 feed_set(
66 &mut writer,
67 project.excluded_subdirs.iter().map(String::as_str),
68 );
69 feed_set(
70 &mut writer,
71 project.public_modules.iter().map(|module| module.as_str()),
72 );
73 feed_set(
74 &mut writer,
75 project
76 .internal_modules
77 .iter()
78 .map(|module| module.as_str()),
79 );
80 ExtractionSettingsDigest::from_digest(writer.finish())
81}
82
83fn feed_set<'a>(writer: &mut DigestWriter, values: impl Iterator<Item = &'a str>) {
84 let mut values: Vec<&str> = values.collect();
85 values.sort_unstable();
86 values.dedup();
87 writer.count(values.len());
88 for value in values {
89 writer.text(value);
90 }
91}
92
93#[must_use]
94pub fn extraction_freshness(
95 header: &SnapshotHeader,
96 expected: &ExpectedExtraction,
97) -> Option<StaleReason> {
98 if header.extractor_version != expected.extractor.as_str() {
99 return Some(StaleReason::ExtractorVersion {
100 found: header.extractor_version.clone(),
101 });
102 }
103 if header.format_version != FORMAT_VERSION {
104 return Some(StaleReason::FormatVersion {
105 found: header.format_version,
106 });
107 }
108 if header.extraction_settings.as_ref() != Some(&expected.settings) {
109 return Some(StaleReason::ExtractionSettings);
110 }
111 None
112}