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::part::Scope;
12use crate::part::{Key, Shape, Slot, decode};
13use crate::path::RelPath;
14use crate::plan::Mode;
15use crate::profile::Policy;
16use crate::resolve::Resolved;
17use crate::target::{Record, Target, read_optional};
18
19#[derive(Clone, Debug)]
22pub struct Entry {
23 pub path: RelPath,
25 pub part: Option<Part>,
27 pub want: Option<Want>,
29 pub record: Option<Fingerprint>,
31 pub recorded: Option<Policy>,
33 pub found: Option<Fingerprint>,
35 pub conflict: bool,
37 pub(crate) keys: BTreeSet<Key>,
39}
40
41#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
43pub struct Part {
44 pub owner: String,
46 pub scope: Scope,
48}
49
50#[derive(Clone, Copy, Debug)]
52pub struct Want {
53 pub layer: usize,
55 pub policy: Policy,
57 pub validate: Format,
59 pub executable: bool,
61 pub fingerprint: Fingerprint,
63}
64
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67pub enum Drift {
68 Unchanged,
70 Cosmetic,
72 Edited,
74 Missing,
76}
77
78impl Entry {
79 #[must_use]
81 pub fn scope(&self) -> Scope {
82 self.part.as_ref().map_or(Scope::File, |part| part.scope)
83 }
84
85 pub(crate) fn slot(&self) -> Slot {
87 Slot { path: self.path.clone(), part: self.part.as_ref().map(|part| part.owner.clone()) }
88 }
89
90 #[must_use]
92 pub fn drift(&self) -> Option<Drift> {
93 let record = self.record?;
94 Some(match self.found {
95 None => Drift::Missing,
96 Some(found) if found.exact == record.exact => Drift::Unchanged,
97 Some(found) if found.same_content(&record) => Drift::Cosmetic,
98 Some(_) => Drift::Edited,
99 })
100 }
101}
102
103impl fmt::Display for Entry {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 match &self.part {
107 Some(Part { owner, scope }) => write!(f, "{} [{scope}: {owner}]", self.path),
108 None => write!(f, "{}", self.path),
109 }
110 }
111}
112
113#[derive(Debug)]
115pub struct Survey {
116 pub(crate) resolved: Resolved,
118 pub(crate) entries: Vec<Entry>,
120}
121
122impl Survey {
123 #[must_use]
125 pub const fn resolved(&self) -> &Resolved {
126 &self.resolved
127 }
128
129 #[must_use]
131 pub fn entries(&self) -> &[Entry] {
132 &self.entries
133 }
134
135 #[must_use]
137 pub fn unfinished(&self) -> bool {
138 self.entries.iter().any(|entry| entry.conflict)
139 }
140
141 pub fn wanted(&self, entry: &Entry, target: &Target) -> Result<Option<Vec<u8>>> {
148 if entry.want.is_none() {
149 return Ok(None);
150 }
151 let payload = self.resolved.payload(entry)?;
152 let Some(portion) = self.resolved.portion(entry) else {
153 return Ok(Some(payload.to_vec()));
154 };
155 let file = read(target, &entry.path)?.unwrap_or_default();
156 Ok(Some(portion.splice(&file, payload, &entry.keys, entry.path.as_str())?))
157 }
158
159 #[must_use]
161 pub fn drifted(&self) -> bool {
162 self.entries.iter().any(|entry| entry.action(Mode::Force).writes())
163 }
164}
165
166pub fn survey(resolved: Resolved, target: &Target) -> Result<Survey> {
176 let mut wants: BTreeMap<&Slot, (Scope, Want, &[u8])> = resolved
177 .provided()
178 .map(|(slot, provided, bytes)| {
179 let want = Want {
180 layer: provided.layer,
181 policy: provided.policy,
182 validate: provided.validate,
183 executable: provided.executable,
184 fingerprint: Fingerprint::of(bytes),
185 };
186 let scope = provided.portion.as_ref().map_or(Scope::File, |portion| portion.scope);
187 (slot, (scope, want, bytes))
188 })
189 .collect();
190 let mut entries = Vec::new();
191 for (slot, record) in target.records() {
192 let wanted = wants.remove(&slot);
193 let now = wanted.map_or(record.scope, |(now, ..)| now);
194 let record = (now == record.scope).then_some(record);
196 entries.push(entry(slot, now, wanted.map(|(_, want, bytes)| (want, bytes)), record));
197 }
198 for (slot, (scope, want, bytes)) in wants {
199 entries.push(entry(slot.clone(), scope, Some((want, bytes)), None));
200 }
201 entries.sort_unstable_by(|a, b| (&a.path, &a.part).cmp(&(&b.path, &b.part)));
202 let mut file: Option<(RelPath, Option<Vec<u8>>)> = None;
203 for entry in &mut entries {
204 if file.as_ref().is_none_or(|(path, _)| *path != entry.path) {
205 file = Some((entry.path.clone(), read(target, &entry.path)?));
206 }
207 let Some((_, bytes)) = &file else { continue };
208 entry.conflict = target.sidecar(&entry.path).is_file();
209 let Some(bytes) = bytes else { continue };
210 let Some(part) = &entry.part else {
211 entry.found = Some(Fingerprint::of(bytes));
212 continue;
213 };
214 let shape = match resolved.portion(entry) {
215 Some(portion) => Cow::Borrowed(&portion.shape),
216 None => match released(target, entry) {
217 Some(shape) => Cow::Owned(shape),
218 None => continue,
219 },
220 };
221 if part.scope == Scope::Keys
222 && let Some(record) = &entry.record
223 {
224 let base = target.base(record)?.unwrap_or_default();
225 entry.keys.extend(shape.keys(&base));
226 }
227 let view = shape.view(bytes, &entry.keys, entry.path.as_str())?;
228 entry.found = view.as_deref().map(Fingerprint::of);
229 }
230 Ok(Survey { resolved, entries })
231}
232
233pub(crate) fn released(target: &Target, entry: &Entry) -> Option<Shape> {
237 let part = entry.part.as_ref()?;
238 let format = target.config().files.get(&entry.path).and_then(|over| over.validate);
239 let format = format.unwrap_or_else(|| Format::of(&entry.path));
240 Shape::of(part.scope, &entry.path, format, None, &part.owner).ok().flatten()
241}
242
243fn entry(slot: Slot, scope: Scope, want: Option<(Want, &[u8])>, record: Option<Record>) -> Entry {
245 let Slot { path, part } = slot;
246 let part = part.map(|owner| Part { owner, scope });
247 let keys = match (&part, want) {
248 (Some(Part { scope: Scope::Keys, .. }), Some((_, content))) => {
249 decode(content).map(|leaves| leaves.into_keys().collect())
250 },
251 _ => None,
252 };
253 Entry {
254 path,
255 part,
256 want: want.map(|(want, _)| want),
257 record: record.map(|record| record.fingerprint),
258 recorded: record.map(|record| record.policy),
259 found: None,
260 conflict: false,
261 keys: keys.unwrap_or_default(),
262 }
263}
264
265fn read(target: &Target, path: &RelPath) -> Result<Option<Vec<u8>>> {
267 let file = path.under(target.root());
268 match fs_err::symlink_metadata(&file) {
269 Ok(meta) if meta.is_file() => read_optional(&file),
270 Ok(meta) => {
271 let kind = if meta.is_symlink() {
272 "a symlink"
273 } else if meta.is_dir() {
274 "a directory"
275 } else {
276 "a special file"
277 };
278 Err(TargetError::NotAFile { path: path.clone(), kind }.into())
279 },
280 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
281 Err(e) => Err(e.into()),
282 }
283}