1use alloc::borrow::Cow;
4use alloc::collections::{BTreeMap, BTreeSet};
5use core::fmt;
6use std::io;
7
8pub use crate::digest::{Digest, Fingerprint};
9use crate::errors::{Result, TargetError};
10use crate::format::Format;
11pub use crate::gate::Reason;
12use crate::gate::{self, Settled};
13use crate::name::ScaffoldId;
14pub use crate::part::Scope;
15use crate::part::{Key, Shape, Slot, decode};
16use crate::path::RelPath;
17use crate::plan::Mode;
18use crate::profile::Policy;
19use crate::resolve::{Layer, Resolved};
20use crate::target::{Record, Target, read_optional};
21pub use crate::target::{Scaffold, Scaffolded};
22
23#[derive(Clone, Debug)]
26pub struct Entry {
27 pub path: RelPath,
29 pub part: Option<Part>,
31 pub want: Option<Want>,
33 pub record: Option<Fingerprint>,
35 pub recorded: Option<Policy>,
37 pub found: Option<Fingerprint>,
39 pub conflict: bool,
41 pub gate: Option<Reason>,
43 pub(crate) present: bool,
45 pub(crate) keys: BTreeSet<Key>,
47}
48
49#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
51pub struct Part {
52 pub owner: String,
54 pub scope: Scope,
56}
57
58#[derive(Clone, Copy, Debug)]
60pub struct Want {
61 pub layer: usize,
63 pub policy: Policy,
65 pub validate: Format,
67 pub executable: bool,
69 pub fingerprint: Fingerprint,
71}
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub enum Drift {
76 Unchanged,
78 Cosmetic,
80 Edited,
82 Missing,
84}
85
86impl Entry {
87 #[must_use]
89 pub fn scope(&self) -> Scope {
90 self.part.as_ref().map_or(Scope::File, |part| part.scope)
91 }
92
93 pub(crate) fn slot(&self) -> Slot {
95 Slot { path: self.path.clone(), part: self.part.as_ref().map(|part| part.owner.clone()) }
96 }
97
98 #[must_use]
100 pub fn drift(&self) -> Option<Drift> {
101 let record = self.record?;
102 Some(match self.found {
103 None => Drift::Missing,
104 Some(found) if found.exact == record.exact => Drift::Unchanged,
105 Some(found) if found.same_content(&record) => Drift::Cosmetic,
106 Some(_) => Drift::Edited,
107 })
108 }
109}
110
111impl fmt::Display for Entry {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 match &self.part {
115 Some(Part { owner, scope }) => write!(f, "{} [{scope}: {owner}]", self.path),
116 None => write!(f, "{}", self.path),
117 }
118 }
119}
120
121#[derive(Debug)]
123pub struct Survey {
124 pub(crate) resolved: Resolved,
126 pub(crate) entries: Vec<Entry>,
128 pub(crate) scaffolds: BTreeMap<ScaffoldId, Scaffold>,
130 pub(crate) gated: BTreeMap<Slot, Reason>,
132 pub(crate) exists: BTreeMap<Slot, Vec<(String, bool)>>,
134}
135
136impl Survey {
137 #[must_use]
139 pub const fn resolved(&self) -> &Resolved {
140 &self.resolved
141 }
142
143 #[must_use]
145 pub fn entries(&self) -> &[Entry] {
146 &self.entries
147 }
148
149 #[must_use]
151 pub const fn scaffolds(&self) -> &BTreeMap<ScaffoldId, Scaffold> {
152 &self.scaffolds
153 }
154
155 #[must_use]
158 pub fn exists(&self, path: &RelPath, part: Option<&str>) -> Option<&[(String, bool)]> {
159 let slot = Slot { path: path.clone(), part: part.map(str::to_owned) };
160 self.exists.get(&slot).map(Vec::as_slice)
161 }
162
163 #[must_use]
165 pub fn gated(&self, path: &RelPath, part: Option<&str>) -> Option<&Reason> {
166 self.gated.get(&Slot { path: path.clone(), part: part.map(str::to_owned) })
167 }
168
169 #[must_use]
171 pub fn unfinished(&self) -> bool {
172 self.entries.iter().any(|entry| entry.conflict)
173 }
174
175 pub fn wanted(&self, entry: &Entry, target: &Target) -> Result<Option<Vec<u8>>> {
182 if entry.want.is_none() {
183 return Ok(None);
184 }
185 let payload = self.resolved.payload(entry)?;
186 let Some(portion) = self.resolved.portion(entry) else {
187 return Ok(Some(payload.to_vec()));
188 };
189 let file = read(target, &entry.path)?.unwrap_or_default();
190 Ok(Some(portion.splice(&file, payload, &entry.keys, entry.path.as_str())?))
191 }
192
193 #[must_use]
195 pub fn drifted(&self) -> bool {
196 self.entries.iter().any(|entry| entry.action(Mode::Force).writes())
197 }
198}
199
200pub fn survey(resolved: Resolved, target: &Target) -> Result<Survey> {
212 let mut wants: BTreeMap<&Slot, (Scope, Want, &[u8])> = resolved
213 .provided()
214 .map(|(slot, provided, bytes)| {
215 let want = Want {
216 layer: provided.layer,
217 policy: provided.policy,
218 validate: provided.validate,
219 executable: provided.executable,
220 fingerprint: Fingerprint::of(bytes),
221 };
222 let scope = provided.portion.as_ref().map_or(Scope::File, |portion| portion.scope);
223 (slot, (scope, want, bytes))
224 })
225 .collect();
226 let mut entries = Vec::new();
227 for (slot, record) in target.records() {
228 let wanted = wants.remove(&slot);
229 let now = wanted.map_or(record.scope, |(now, ..)| now);
230 let record = (now == record.scope).then_some(record);
232 entries.push(entry(slot, now, wanted.map(|(_, want, bytes)| (want, bytes)), record));
233 }
234 for (slot, (scope, want, bytes)) in wants {
235 entries.push(entry(slot.clone(), scope, Some((want, bytes)), None));
236 }
237 entries.sort_unstable_by(|a, b| (&a.path, &a.part).cmp(&(&b.path, &b.part)));
238 let mut file: Option<(RelPath, Option<Vec<u8>>)> = None;
239 for entry in &mut entries {
240 if file.as_ref().is_none_or(|(path, _)| *path != entry.path) {
241 file = Some((entry.path.clone(), read(target, &entry.path)?));
242 }
243 let Some((_, bytes)) = &file else { continue };
244 entry.conflict = target.sidecar(&entry.path).is_file();
245 let Some(bytes) = bytes else { continue };
246 entry.present = true;
247 let Some(part) = &entry.part else {
248 entry.found = Some(Fingerprint::of(bytes));
249 continue;
250 };
251 let shape = match resolved.portion(entry) {
252 Some(portion) => Cow::Borrowed(&portion.shape),
253 None => match released(&resolved, target, entry) {
254 Some(shape) => Cow::Owned(shape),
255 None => continue,
256 },
257 };
258 if part.scope == Scope::Keys
259 && let Some(record) = &entry.record
260 {
261 let base = target.base(record)?.unwrap_or_default();
262 entry.keys.extend(shape.keys(&base));
263 }
264 let view = shape.view(bytes, &entry.keys, entry.path.as_str())?;
265 let placed = part.scope == Scope::Block
268 && entry.record.is_none()
269 && view.as_deref().is_some_and(|body| body.trim_ascii().is_empty());
270 entry.found = if placed { None } else { view.as_deref().map(Fingerprint::of) };
271 }
272 starters(&mut entries);
273 let Settled { scaffolds, gated, exists } = gate::settle(&mut entries, &resolved, target)?;
274 for entry in entries.iter_mut().filter(|entry| entry.want.is_none() && entry.gate.is_none()) {
275 entry.gate = gated_by(&resolved, entry);
276 }
277 Ok(Survey { resolved, entries, scaffolds, gated, exists })
278}
279
280fn starters(entries: &mut [Entry]) {
283 let parted: BTreeSet<RelPath> = entries
284 .iter()
285 .filter(|entry| entry.part.is_some() && entry.want.is_some())
286 .map(|entry| entry.path.clone())
287 .collect();
288 for entry in entries.iter_mut() {
289 let starter = entry.part.is_none()
290 && entry.want.is_some_and(|want| want.policy == Policy::Once)
291 && parted.contains(&entry.path);
292 if starter && entry.found.is_some() && entry.record.is_some() {
293 entry.found = entry.record;
294 }
295 }
296}
297
298fn gated_by(resolved: &Resolved, entry: &Entry) -> Option<Reason> {
300 let listed = |layer: &Layer| layer.files().get(&entry.path)?.gated.clone();
301 match &entry.part {
302 Some(part) => listed(resolved.layer(&part.owner)?),
303 None => resolved.layers().iter().find_map(listed),
304 }
305}
306
307pub(crate) fn released(resolved: &Resolved, target: &Target, entry: &Entry) -> Option<Shape> {
312 let part = entry.part.as_ref()?;
313 let listed = resolved.layer(&part.owner).and_then(|layer| layer.files().get(&entry.path));
314 let spec = listed.map(|file| &file.spec);
315 let over = target.config().files.get(&entry.path).and_then(|over| over.validate);
316 let format = over
317 .or_else(|| spec.and_then(|spec| spec.validate))
318 .unwrap_or_else(|| Format::of(&entry.path));
319 let comment = spec.and_then(|spec| spec.comment);
320 Shape::of(part.scope, &entry.path, format, comment, &part.owner).ok().flatten()
321}
322
323fn entry(slot: Slot, scope: Scope, want: Option<(Want, &[u8])>, record: Option<Record>) -> Entry {
325 let Slot { path, part } = slot;
326 let part = part.map(|owner| Part { owner, scope });
327 let keys = match (&part, want) {
328 (Some(Part { scope: Scope::Keys, .. }), Some((_, content))) => {
329 decode(content).map(|leaves| leaves.into_keys().collect())
330 },
331 _ => None,
332 };
333 Entry {
334 path,
335 part,
336 want: want.map(|(want, _)| want),
337 record: record.map(|record| record.fingerprint),
338 recorded: record.map(|record| record.policy),
339 found: None,
340 conflict: false,
341 gate: None,
342 present: false,
343 keys: keys.unwrap_or_default(),
344 }
345}
346
347fn read(target: &Target, path: &RelPath) -> Result<Option<Vec<u8>>> {
349 let file = path.under(target.root());
350 match fs_err::symlink_metadata(&file) {
351 Ok(meta) if meta.is_file() => read_optional(&file),
352 Ok(meta) => {
353 let kind = if meta.is_symlink() {
354 "a symlink"
355 } else if meta.is_dir() {
356 "a directory"
357 } else {
358 "a special file"
359 };
360 Err(TargetError::NotAFile { path: path.clone(), kind }.into())
361 },
362 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
363 Err(e) => Err(e.into()),
364 }
365}