use alloc::borrow::Cow;
use alloc::collections::{BTreeMap, BTreeSet};
use core::fmt;
use std::io;
pub use crate::digest::{Digest, Fingerprint};
use crate::errors::{Result, TargetError};
use crate::format::Format;
pub use crate::gate::Reason;
use crate::gate::{self, Settled};
use crate::name::ScaffoldId;
pub use crate::part::Scope;
use crate::part::{Key, Shape, Slot, decode};
use crate::path::RelPath;
use crate::plan::Mode;
use crate::profile::Policy;
use crate::resolve::{Layer, Resolved};
use crate::target::{Record, Target, read_optional};
pub use crate::target::{Scaffold, Scaffolded};
#[derive(Clone, Debug)]
pub struct Entry {
pub path: RelPath,
pub part: Option<Part>,
pub want: Option<Want>,
pub record: Option<Fingerprint>,
pub recorded: Option<Policy>,
pub found: Option<Fingerprint>,
pub conflict: bool,
pub gate: Option<Reason>,
pub(crate) present: bool,
pub(crate) keys: BTreeSet<Key>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Part {
pub owner: String,
pub scope: Scope,
}
#[derive(Clone, Copy, Debug)]
pub struct Want {
pub layer: usize,
pub policy: Policy,
pub validate: Format,
pub executable: bool,
pub fingerprint: Fingerprint,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Drift {
Unchanged,
Cosmetic,
Edited,
Missing,
}
impl Entry {
#[must_use]
pub fn scope(&self) -> Scope {
self.part.as_ref().map_or(Scope::File, |part| part.scope)
}
pub(crate) fn slot(&self) -> Slot {
Slot { path: self.path.clone(), part: self.part.as_ref().map(|part| part.owner.clone()) }
}
#[must_use]
pub fn drift(&self) -> Option<Drift> {
let record = self.record?;
Some(match self.found {
None => Drift::Missing,
Some(found) if found.exact == record.exact => Drift::Unchanged,
Some(found) if found.same_content(&record) => Drift::Cosmetic,
Some(_) => Drift::Edited,
})
}
}
impl fmt::Display for Entry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.part {
Some(Part { owner, scope }) => write!(f, "{} [{scope}: {owner}]", self.path),
None => write!(f, "{}", self.path),
}
}
}
#[derive(Debug)]
pub struct Survey {
pub(crate) resolved: Resolved,
pub(crate) entries: Vec<Entry>,
pub(crate) scaffolds: BTreeMap<ScaffoldId, Scaffold>,
pub(crate) gated: BTreeMap<Slot, Reason>,
pub(crate) exists: BTreeMap<Slot, Vec<(String, bool)>>,
}
impl Survey {
#[must_use]
pub const fn resolved(&self) -> &Resolved {
&self.resolved
}
#[must_use]
pub fn entries(&self) -> &[Entry] {
&self.entries
}
#[must_use]
pub const fn scaffolds(&self) -> &BTreeMap<ScaffoldId, Scaffold> {
&self.scaffolds
}
#[must_use]
pub fn exists(&self, path: &RelPath, part: Option<&str>) -> Option<&[(String, bool)]> {
let slot = Slot { path: path.clone(), part: part.map(str::to_owned) };
self.exists.get(&slot).map(Vec::as_slice)
}
#[must_use]
pub fn gated(&self, path: &RelPath, part: Option<&str>) -> Option<&Reason> {
self.gated.get(&Slot { path: path.clone(), part: part.map(str::to_owned) })
}
#[must_use]
pub fn unfinished(&self) -> bool {
self.entries.iter().any(|entry| entry.conflict)
}
pub fn wanted(&self, entry: &Entry, target: &Target) -> Result<Option<Vec<u8>>> {
if entry.want.is_none() {
return Ok(None);
}
let payload = self.resolved.payload(entry)?;
let Some(portion) = self.resolved.portion(entry) else {
return Ok(Some(payload.to_vec()));
};
let file = read(target, &entry.path)?.unwrap_or_default();
Ok(Some(portion.splice(&file, payload, &entry.keys, entry.path.as_str())?))
}
#[must_use]
pub fn drifted(&self) -> bool {
self.entries.iter().any(|entry| entry.action(Mode::Force).writes())
}
}
pub fn survey(resolved: Resolved, target: &Target) -> Result<Survey> {
let mut wants: BTreeMap<&Slot, (Scope, Want, &[u8])> = resolved
.provided()
.map(|(slot, provided, bytes)| {
let want = Want {
layer: provided.layer,
policy: provided.policy,
validate: provided.validate,
executable: provided.executable,
fingerprint: Fingerprint::of(bytes),
};
let scope = provided.portion.as_ref().map_or(Scope::File, |portion| portion.scope);
(slot, (scope, want, bytes))
})
.collect();
let mut entries = Vec::new();
for (slot, record) in target.records() {
let wanted = wants.remove(&slot);
let now = wanted.map_or(record.scope, |(now, ..)| now);
let record = (now == record.scope).then_some(record);
entries.push(entry(slot, now, wanted.map(|(_, want, bytes)| (want, bytes)), record));
}
for (slot, (scope, want, bytes)) in wants {
entries.push(entry(slot.clone(), scope, Some((want, bytes)), None));
}
entries.sort_unstable_by(|a, b| (&a.path, &a.part).cmp(&(&b.path, &b.part)));
let mut file: Option<(RelPath, Option<Vec<u8>>)> = None;
for entry in &mut entries {
if file.as_ref().is_none_or(|(path, _)| *path != entry.path) {
file = Some((entry.path.clone(), read(target, &entry.path)?));
}
let Some((_, bytes)) = &file else { continue };
entry.conflict = target.sidecar(&entry.path).is_file();
let Some(bytes) = bytes else { continue };
entry.present = true;
let Some(part) = &entry.part else {
entry.found = Some(Fingerprint::of(bytes));
continue;
};
let shape = match resolved.portion(entry) {
Some(portion) => Cow::Borrowed(&portion.shape),
None => match released(&resolved, target, entry) {
Some(shape) => Cow::Owned(shape),
None => continue,
},
};
if part.scope == Scope::Keys
&& let Some(record) = &entry.record
{
let base = target.base(record)?.unwrap_or_default();
entry.keys.extend(shape.keys(&base));
}
let view = shape.view(bytes, &entry.keys, entry.path.as_str())?;
let placed = part.scope == Scope::Block
&& entry.record.is_none()
&& view.as_deref().is_some_and(|body| body.trim_ascii().is_empty());
entry.found = if placed { None } else { view.as_deref().map(Fingerprint::of) };
}
starters(&mut entries);
let Settled { scaffolds, gated, exists } = gate::settle(&mut entries, &resolved, target)?;
for entry in entries.iter_mut().filter(|entry| entry.want.is_none() && entry.gate.is_none()) {
entry.gate = gated_by(&resolved, entry);
}
Ok(Survey { resolved, entries, scaffolds, gated, exists })
}
fn starters(entries: &mut [Entry]) {
let parted: BTreeSet<RelPath> = entries
.iter()
.filter(|entry| entry.part.is_some() && entry.want.is_some())
.map(|entry| entry.path.clone())
.collect();
for entry in entries.iter_mut() {
let starter = entry.part.is_none()
&& entry.want.is_some_and(|want| want.policy == Policy::Once)
&& parted.contains(&entry.path);
if starter && entry.found.is_some() && entry.record.is_some() {
entry.found = entry.record;
}
}
}
fn gated_by(resolved: &Resolved, entry: &Entry) -> Option<Reason> {
let listed = |layer: &Layer| layer.files().get(&entry.path)?.gated.clone();
match &entry.part {
Some(part) => listed(resolved.layer(&part.owner)?),
None => resolved.layers().iter().find_map(listed),
}
}
pub(crate) fn released(resolved: &Resolved, target: &Target, entry: &Entry) -> Option<Shape> {
let part = entry.part.as_ref()?;
let listed = resolved.layer(&part.owner).and_then(|layer| layer.files().get(&entry.path));
let spec = listed.map(|file| &file.spec);
let over = target.config().files.get(&entry.path).and_then(|over| over.validate);
let format = over
.or_else(|| spec.and_then(|spec| spec.validate))
.unwrap_or_else(|| Format::of(&entry.path));
let comment = spec.and_then(|spec| spec.comment);
Shape::of(part.scope, &entry.path, format, comment, &part.owner).ok().flatten()
}
fn entry(slot: Slot, scope: Scope, want: Option<(Want, &[u8])>, record: Option<Record>) -> Entry {
let Slot { path, part } = slot;
let part = part.map(|owner| Part { owner, scope });
let keys = match (&part, want) {
(Some(Part { scope: Scope::Keys, .. }), Some((_, content))) => {
decode(content).map(|leaves| leaves.into_keys().collect())
},
_ => None,
};
Entry {
path,
part,
want: want.map(|(want, _)| want),
record: record.map(|record| record.fingerprint),
recorded: record.map(|record| record.policy),
found: None,
conflict: false,
gate: None,
present: false,
keys: keys.unwrap_or_default(),
}
}
fn read(target: &Target, path: &RelPath) -> Result<Option<Vec<u8>>> {
let file = path.under(target.root());
match fs_err::symlink_metadata(&file) {
Ok(meta) if meta.is_file() => read_optional(&file),
Ok(meta) => {
let kind = if meta.is_symlink() {
"a symlink"
} else if meta.is_dir() {
"a directory"
} else {
"a special file"
};
Err(TargetError::NotAFile { path: path.clone(), kind }.into())
},
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e.into()),
}
}