1use std::collections::{BTreeMap, BTreeSet};
6
7use crate::jigsaw::anchor::{Anchored, anchor};
8use crate::jigsaw::check::{check_host, check_otp};
9use crate::jigsaw::findings::{Checked, Finding, Stop, Warning, Withheld, WithheldSymbol};
10use crate::jigsaw::loaded::LoadedPlan;
11use crate::jigsaw::plugin_set::PluginSet;
12use crate::jigsaw::references::{ReferenceSet, ReferenceSite, UncheckedSite};
13use crate::jigsaw::report::{
14 FindingSpan, HostIdx, JigsawReport, OtpIdx, RangedFinding, RangedWarning, RangedWithheld,
15 ReportParts, Tallies, TargetAxes,
16};
17use crate::jigsaw::surface::{AppPresence, Blocker, ReleaseSurface};
18use crate::model::names::ApplicationName;
19use crate::model::non_empty::NonEmpty;
20
21#[derive(Debug, Clone, Copy)]
22enum Target {
23 Host(HostIdx),
24 Otp(OtpIdx),
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
28enum Axis {
29 Host,
30 Otp,
31 Everywhere,
32}
33
34impl Target {
35 fn axis(self) -> Axis {
36 match self {
37 Self::Host(_) => Axis::Host,
38 Self::Otp(_) => Axis::Otp,
39 }
40 }
41}
42
43#[derive(Default)]
44struct Hit {
45 hosts: BTreeSet<HostIdx>,
46 otp: BTreeSet<OtpIdx>,
47 sites: BTreeSet<ReferenceSite>,
48}
49
50impl Hit {
51 fn add(&mut self, target: Target, sites: &BTreeSet<ReferenceSite>) {
52 match target {
53 Target::Host(host) => self.hosts.insert(host),
54 Target::Otp(otp) => self.otp.insert(otp),
55 };
56 self.sites.extend(sites.iter().cloned());
57 }
58
59 fn span(self, axis: Axis) -> (FindingSpan, BTreeSet<ReferenceSite>) {
60 let span = match axis {
61 Axis::Host => FindingSpan::Host(self.hosts),
62 Axis::Otp => FindingSpan::Otp(self.otp),
63 Axis::Everywhere => FindingSpan::Everywhere,
64 };
65 (span, self.sites)
66 }
67}
68
69#[derive(Default)]
71struct Accumulator {
72 findings: BTreeMap<(Axis, Finding), Hit>,
73 warnings: BTreeMap<Warning, BTreeSet<HostIdx>>,
74 withheld: BTreeMap<(Axis, WithheldSymbol), Hit>,
75 stopped: BTreeMap<Stop, BTreeSet<(Axis, usize)>>,
76}
77
78impl Accumulator {
79 fn record(
80 &mut self,
81 target: Target,
82 anchor_index: usize,
83 sites: &BTreeSet<ReferenceSite>,
84 checked: Checked,
85 ) {
86 for finding in checked.findings {
87 self.findings
88 .entry((target.axis(), finding))
89 .or_default()
90 .add(target, sites);
91 }
92 if let Target::Host(host) = target {
93 for warning in checked.warnings {
94 self.warnings.entry(warning).or_default().insert(host);
95 }
96 }
97 for withheld in checked.withheld {
98 match withheld {
99 Withheld::Symbol(symbol) => self
100 .withheld
101 .entry((target.axis(), symbol))
102 .or_default()
103 .add(target, sites),
104 Withheld::Stopped(stop) => {
105 self.stopped
106 .entry(stop)
107 .or_default()
108 .insert((target.axis(), anchor_index));
109 }
110 }
111 }
112 }
113}
114
115pub fn evaluate<E>(
123 loaded: &LoadedPlan,
124 plugins: &PluginSet,
125 collect: impl FnOnce(&PluginSet, &ReleaseSurface<'_>) -> Result<ReferenceSet, E>,
126) -> Result<JigsawReport, E> {
127 let surfaces = loaded.surfaces();
128 let references = collect(plugins, surfaces.build())?;
129 let anchored = anchor(plugins, &references, surfaces.build(), surfaces.otp());
130 let axes = TargetAxes::of(loaded.plan());
131 let mut acc = Accumulator::default();
132
133 for (host, release) in axes.host_indices().zip(surfaces.releases()) {
134 for (index, anchor) in anchored.host.iter().enumerate() {
135 let checked = check_host(&anchor.kind, release);
136 acc.record(Target::Host(host), index, &anchor.sites, checked);
137 }
138 for (app, blocker) in release.shipped().blockers() {
139 let warning = match blocker {
140 Blocker::Manifest(reason) => Warning::ManifestUnreadable {
141 app: app.clone(),
142 reason: reason.clone(),
143 },
144 Blocker::Untracked => Warning::AppNotTracked { app: app.clone() },
145 };
146 acc.warnings.entry(warning).or_default().insert(host);
147 }
148 for app in plugins.apps().keys() {
149 if release.shipped().lookup(app) == AppPresence::Provided {
150 acc.warnings
151 .entry(Warning::BundledAppShippedByHost { app: app.clone() })
152 .or_default()
153 .insert(host);
154 }
155 }
156 }
157 for (otp, surface) in axes.otp_indices().zip(surfaces.otp()) {
158 for (index, anchor) in anchored.otp.iter().enumerate() {
159 let checked = check_otp(&anchor.kind, surface);
160 acc.record(Target::Otp(otp), index, &anchor.sites, checked);
161 }
162 }
163 for unprovided in &anchored.unprovided {
164 let finding = Finding::AppMissing {
165 app: unprovided.kind.app.clone(),
166 required_by: unprovided.kind.required_by.clone(),
167 };
168 acc.findings
169 .entry((Axis::Everywhere, finding))
170 .or_default()
171 .sites
172 .extend(unprovided.sites.iter().cloned());
173 }
174
175 Ok(assemble(loaded, plugins, &references, &anchored, axes, acc))
176}
177
178fn assemble(
179 loaded: &LoadedPlan,
180 plugins: &PluginSet,
181 references: &ReferenceSet,
182 anchored: &Anchored,
183 targets: TargetAxes,
184 acc: Accumulator,
185) -> JigsawReport {
186 let findings = acc
187 .findings
188 .into_iter()
189 .filter_map(|((axis, finding), hit)| {
190 let (span, sites) = hit.span(axis);
191 let apps: BTreeSet<ApplicationName> =
192 sites.iter().map(|site| site.file.app.clone()).collect();
193 Some(RangedFinding {
194 finding,
195 apps: NonEmpty::try_from(apps.into_iter().collect::<Vec<_>>()).ok()?,
196 sites: NonEmpty::try_from(sites.into_iter().collect::<Vec<_>>()).ok()?,
197 span,
198 })
199 })
200 .collect();
201 let warnings = acc
202 .warnings
203 .into_iter()
204 .map(|(warning, hosts)| RangedWarning {
205 warning,
206 span: FindingSpan::Host(hosts),
207 })
208 .collect();
209 let not_checked = references
210 .not_checked()
211 .iter()
212 .chain(&anchored.not_checked)
213 .map(|(&reason, &n)| (reason, n));
214 let unchecked: Vec<UncheckedSite> = references
215 .unchecked()
216 .iter()
217 .chain(&anchored.unchecked)
218 .cloned()
219 .collect::<BTreeSet<_>>()
220 .into_iter()
221 .collect();
222 let stopped = acc
223 .stopped
224 .into_iter()
225 .map(|(stop, anchors)| (stop, anchors.len()));
226 let withheld: Vec<RangedWithheld> = acc
227 .withheld
228 .into_iter()
229 .filter_map(|((axis, symbol), hit)| {
230 let (span, sites) = hit.span(axis);
231 Some(RangedWithheld {
232 symbol,
233 sites: NonEmpty::try_from(sites.into_iter().collect::<Vec<_>>()).ok()?,
234 span,
235 })
236 })
237 .collect();
238 let plan = loaded.plan();
239 JigsawReport::from_parts(ReportParts {
240 host: plan.host().clone(),
241 series: plan.series(),
242 tag_source: plan.tag_source(),
243 targets,
244 apps: plugins
245 .apps()
246 .iter()
247 .map(|(name, app)| (name.clone(), app.role()))
248 .collect(),
249 findings,
250 warnings,
251 tallies: Tallies::new(
252 anchored.checked(),
253 not_checked,
254 &unchecked,
255 stopped,
256 &withheld,
257 ),
258 withheld,
259 unchecked,
260 host_provided: plugins.host_provided().clone(),
261 store_digest: loaded.store_digest().clone(),
262 })
263}