use alloc::collections::{BTreeMap, BTreeSet};
use core::{mem, slice};
use semver::Version;
use serde::Serialize;
use crate::collection::{Index, label};
use crate::digest::Digest;
use crate::errors::{ProfileError, Result, TargetError};
use crate::format::Format;
use crate::gate::{self, Pattern, Reason};
use crate::graph::{Configured, Graph, Load, Node};
pub use crate::graph::{Enabler, Warning};
pub use crate::merge::Driver;
use crate::name::{FeatureName, ProfileName, ScaffoldName, SourceName};
use crate::part::{Key, Scope, Shape, Slot, Written, display, overlap};
use crate::path::RelPath;
use crate::profile::{
FileSpec, MANIFEST, Manifest, MergeSpec, Meta, PAYLOAD, Policy, ScaffoldSpec,
};
pub use crate::settings::{Settings, Suggestion};
use crate::source::{Cache, Oid, Reader, Source};
use crate::survey::Entry;
use crate::target::{Override, Target, from_toml};
use crate::tree::Tree;
pub use crate::vars::Question;
use crate::vars::{self, Context, VarName, VarSpec};
#[derive(Clone, Copy, Debug)]
pub enum Refresh<'a> {
None,
All,
Only(&'a str),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Applied {
Current,
Changed,
Never,
}
#[derive(Debug)]
pub struct Layer {
source: Source,
source_name: Option<SourceName>,
dir: String,
meta: Meta,
merge: MergeSpec,
vars: BTreeMap<VarName, VarSpec>,
declared: Vec<FeatureName>,
features: BTreeMap<FeatureName, BTreeSet<Enabler>>,
required_by: Vec<ProfileName>,
configured: bool,
rev: Option<Oid>,
manifest: Digest,
digest: Digest,
locked: Option<Digest>,
specs: Specs,
scaffolds: BTreeMap<ScaffoldName, Vec<Pattern>>,
files: BTreeMap<RelPath, LayerFile>,
written: Tree,
payload: Tree,
starters: Tree,
}
pub(crate) type Specs = (BTreeMap<RelPath, FileSpec>, BTreeMap<ScaffoldName, ScaffoldSpec>);
#[derive(Clone, Debug)]
pub struct LayerFile {
pub written: RelPath,
pub spec: FileSpec,
pub(crate) exists: Vec<Pattern>,
pub gated: Option<Reason>,
}
impl LayerFile {
pub fn exists(&self) -> impl Iterator<Item = &str> {
self.exists.iter().map(Pattern::as_str)
}
}
impl Layer {
#[must_use]
pub const fn source(&self) -> &Source {
&self.source
}
#[must_use]
pub const fn source_name(&self) -> Option<&SourceName> {
self.source_name.as_ref()
}
#[must_use]
pub const fn name(&self) -> &ProfileName {
&self.meta.name
}
#[must_use]
pub fn qualified(&self) -> String {
let name = &self.meta.name;
self.source_name
.as_ref()
.map_or_else(|| name.to_string(), |source| format!("{source}/{name}"))
}
#[must_use]
pub const fn meta(&self) -> &Meta {
&self.meta
}
#[must_use]
pub const fn merge(&self) -> &MergeSpec {
&self.merge
}
#[must_use]
pub const fn rev(&self) -> Option<&Oid> {
self.rev.as_ref()
}
#[must_use]
pub const fn digest(&self) -> Digest {
self.digest
}
#[must_use]
pub fn declared(&self) -> &[FeatureName] {
&self.declared
}
#[must_use]
pub const fn features(&self) -> &BTreeMap<FeatureName, BTreeSet<Enabler>> {
&self.features
}
#[must_use]
pub fn required_by(&self) -> &[ProfileName] {
&self.required_by
}
#[must_use]
pub const fn configured(&self) -> bool {
self.configured
}
#[must_use]
pub const fn files(&self) -> &BTreeMap<RelPath, LayerFile> {
&self.files
}
pub fn scaffolds(&self) -> impl Iterator<Item = (&ScaffoldName, impl Iterator<Item = &str>)> {
self.scaffolds.iter().map(|(name, unless)| (name, unless.iter().map(Pattern::as_str)))
}
#[must_use]
pub fn applied(&self) -> Applied {
match self.locked {
None => Applied::Never,
Some(locked) if locked == self.digest => Applied::Current,
Some(_) => Applied::Changed,
}
}
pub(crate) fn vars(&self) -> impl Iterator<Item = (&VarName, &VarSpec)> {
self.vars.iter()
}
pub(crate) const fn sentinels(&self) -> &BTreeMap<ScaffoldName, Vec<Pattern>> {
&self.scaffolds
}
pub(crate) fn label(&self, file: &str) -> String {
if self.dir.is_empty() {
label(&self.source, file)
} else {
label(&self.source, &format!("{}/{file}", self.dir))
}
}
pub(crate) fn take_specs(&mut self) -> Specs {
mem::take(&mut self.specs)
}
pub(crate) fn set_files(
&mut self, files: BTreeMap<RelPath, LayerFile>,
scaffolds: BTreeMap<ScaffoldName, Vec<Pattern>>,
) {
self.files = files;
self.scaffolds = scaffolds;
}
pub(crate) const fn files_mut(&mut self) -> &mut BTreeMap<RelPath, LayerFile> {
&mut self.files
}
pub(crate) const fn written(&self) -> &Tree {
&self.written
}
pub(crate) fn set_payload(&mut self, payload: Tree, starters: Tree) {
let mut hasher = blake3::Hasher::new();
hasher.update(self.manifest.as_bytes());
hasher.update(payload.digest().as_bytes());
hasher.update(starters.digest().as_bytes());
for feature in self.features.keys() {
hasher.update(feature.as_str().as_bytes());
hasher.update(&[0]);
}
self.digest = Digest::new(hasher.finalize());
self.payload = payload;
self.starters = starters;
self.written = Tree::default();
}
}
#[derive(Clone, Debug)]
pub(crate) struct Provided {
pub layer: usize,
pub policy: Policy,
pub validate: Format,
pub executable: bool,
pub portion: Option<Portion>,
}
#[derive(Clone, Debug)]
pub(crate) struct Portion {
pub scope: Scope,
pub shape: Shape,
pub content: Vec<u8>,
pub payload: Written,
pub text: Vec<u8>,
pub starts: bool,
pub starter: Option<Vec<u8>>,
}
impl Portion {
pub(crate) fn splice(
&self, file: &[u8], content: &[u8], keys: &BTreeSet<Key>, label: &str,
) -> Result<Vec<u8>> {
let keyed = matches!(self.shape, Shape::Keys(_));
if keyed && content == self.content && file.trim_ascii().is_empty() {
return Ok(self.text.clone());
}
self.shape.splice(file, content, keys, &self.payload, label)
}
}
#[derive(Debug)]
pub struct Resolved {
layers: Vec<Layer>,
providers: BTreeMap<Slot, Provided>,
settings: Settings,
suggestions: Vec<Suggestion>,
answers: BTreeMap<VarName, String>,
warnings: Vec<Warning>,
}
impl Resolved {
#[must_use]
pub fn layers(&self) -> &[Layer] {
&self.layers
}
#[must_use]
pub fn layer(&self, name: &str) -> Option<&Layer> {
self.layers.iter().find(|layer| layer.meta.name == *name)
}
#[must_use]
pub const fn settings(&self) -> &Settings {
&self.settings
}
#[must_use]
pub fn suggestions(&self) -> &[Suggestion] {
&self.suggestions
}
#[must_use]
pub const fn answers(&self) -> &BTreeMap<VarName, String> {
&self.answers
}
#[must_use]
pub fn warnings(&self) -> &[Warning] {
&self.warnings
}
#[must_use]
pub fn provider(&self, entry: &Entry) -> Option<&ProfileName> {
Some(&self.layers.get(entry.want?.layer)?.meta.name)
}
#[must_use]
pub fn file(&self, entry: &Entry) -> Option<&LayerFile> {
self.layers.get(entry.want?.layer)?.files.get(&entry.path)
}
pub(crate) fn provided(&self) -> impl Iterator<Item = (&Slot, &Provided, &[u8])> {
self.providers
.iter()
.filter_map(|(slot, provided)| Some((slot, provided, self.bytes(slot, provided)?)))
}
pub(crate) fn payload(&self, entry: &Entry) -> Result<&[u8]> {
let slot = entry.slot();
let bytes = self.providers.get(&slot).and_then(|provided| self.bytes(&slot, provided));
bytes.ok_or_else(|| {
TargetError::NotManaged { path: entry.path.to_string(), managed: Vec::new() }.into()
})
}
fn bytes<'a>(&'a self, slot: &Slot, provided: &'a Provided) -> Option<&'a [u8]> {
match &provided.portion {
Some(portion) => Some(&portion.content),
None => self.layers.get(provided.layer)?.payload.get(&slot.path),
}
}
pub(crate) fn portion(&self, entry: &Entry) -> Option<&Portion> {
self.providers.get(&entry.slot())?.portion.as_ref()
}
}
pub fn resolve(target: &Target, cache: &Cache, refresh: Refresh<'_>) -> Result<Resolved> {
let moves = Moves::of(target, cache, refresh)?;
let (mut layers, warnings) = layers(target, cache, &moves)?;
let answers = vars::answers(&layers, target)?;
let context = Context::of(target, &layers);
vars::render_paths(&mut layers, &answers, &context)?;
gate::statics(&mut layers, &answers);
let config = target.config();
let mut providers = providers(&layers, &config.files)?;
vars::render(&mut layers, &answers, &context)?;
portions(&layers, &mut providers)?;
let settings = Settings::compose(&layers, &config.merge)?;
let suggestions = Settings::suggestions(&layers, &config.merge);
Ok(Resolved { layers, providers, settings, suggestions, answers, warnings })
}
enum Moves {
None,
All,
Only(Vec<Source>),
}
impl Moves {
fn of(target: &Target, cache: &Cache, refresh: Refresh<'_>) -> Result<Self> {
let name = match refresh {
Refresh::None => return Ok(Self::None),
Refresh::All => return Ok(Self::All),
Refresh::Only(name) => name,
};
let config = target.config();
if let Some(source) = config.sources.iter().find(|(named, _)| named.as_str() == name) {
return Ok(Self::Only(vec![source.1.clone()]));
}
let configured = config.layers.iter().find(|layer| layer.profile.profile == *name);
if let Some(source) = configured.and_then(|layer| config.sources.get(&layer.profile.source))
{
return Ok(Self::Only(vec![source.clone()]));
}
let (layers, _) = layers(target, cache, &Self::None)?;
if let Some(layer) = layers.iter().find(|layer| layer.meta.name == *name) {
return Ok(Self::Only(vec![layer.source.clone()]));
}
let layers = layers.iter().map(|layer| layer.meta.name.to_string());
let names = layers.chain(config.sources.keys().map(ToString::to_string)).collect();
Err(TargetError::NoSuchLayer { name: name.to_owned(), names }.into())
}
fn moves(&self, source: &Source) -> bool {
match self {
Self::None => false,
Self::All => true,
Self::Only(sources) => sources.contains(source),
}
}
}
fn layers(target: &Target, cache: &Cache, moves: &Moves) -> Result<(Vec<Layer>, Vec<Warning>)> {
let config = target.config();
let configured = config
.layers
.iter()
.map(|layer| {
let source = config.sources.get(&layer.profile.source).ok_or_else(|| {
let sources = config.sources.keys().cloned().collect();
let name = layer.profile.source.clone();
let naming = config.layers.iter().filter(|l| l.profile.source == name);
let layers = naming.map(|l| l.profile.clone()).collect();
TargetError::NoSuchSource { name, sources, layers }
})?;
Ok(Configured {
source,
name: &layer.profile.profile,
features: &layer.features,
defaults: layer.default_features,
})
})
.collect::<Result<Vec<_>>>()?;
let mut loader = Loader { target, cache, moves, opened: Vec::new() };
let Graph { nodes, warnings } = Graph::build(&configured, &mut loader)?;
let layers = nodes.into_iter().map(|node| loader.layer(node)).collect::<Result<_>>()?;
Ok((layers, warnings))
}
struct Loader<'a> {
target: &'a Target,
cache: &'a Cache,
moves: &'a Moves,
opened: Vec<Opened>,
}
struct Opened {
source: Source,
reader: Reader,
index: Index,
}
pub(crate) struct Loaded {
opened: usize,
dir: String,
manifest: Manifest,
digest: Digest,
}
impl Load for Loader<'_> {
type Profile = Loaded;
fn load(&mut self, source: &Source, name: &ProfileName) -> Result<Loaded> {
let opened = self.open(source)?;
let Some(Opened { reader, index, .. }) = self.opened.get(opened) else {
return Err(ProfileError::NoProfiles { location: source.to_string() }.into());
};
let dir = index.find(name, source)?.to_owned();
let path = if dir.is_empty() {
RelPath::new(MANIFEST)?
} else {
RelPath::new(&format!("{dir}/{MANIFEST}"))?
};
let read = reader.read("", slice::from_ref(&path))?;
let raw = read
.get(&path)
.ok_or_else(|| ProfileError::NoProfiles { location: source.to_string() })?;
let file = label(source, path.as_str());
let manifest: Manifest = from_toml(raw, &file)?;
if let Some(required) = &manifest.profile.devset
&& !Version::parse(crate::VERSION).is_ok_and(|version| required.matches(&version))
{
let (profile, requires) = (manifest.profile.name, required.to_string());
return Err(ProfileError::Incompatible { profile, requires }.into());
}
manifest.check(&file)?;
Ok(Loaded { opened, dir, manifest, digest: Digest::of(raw) })
}
fn manifest(profile: &Loaded) -> &Manifest {
&profile.manifest
}
}
impl Loader<'_> {
fn open(&mut self, source: &Source) -> Result<usize> {
if let Some(at) = self.opened.iter().position(|opened| opened.source == *source) {
return Ok(at);
}
if let Some(twin) = self.opened.iter().find(|opened| opened.source.twin(source)) {
let (first, second) = (twin.source.to_string(), source.to_string());
return Err(ProfileError::Diverged { first, second }.into());
}
let pin = if self.moves.moves(source) { None } else { self.target.pinned(source) };
let reader = source.open(self.target.root(), pin, self.cache)?;
let index = Index::read(&reader, source)?;
if index.names().next().is_none() {
return Err(ProfileError::NoProfiles { location: source.to_string() }.into());
}
self.opened.push(Opened { source: source.clone(), reader, index });
Ok(self.opened.len().saturating_sub(1))
}
fn layer(&self, node: Node<Loaded>) -> Result<Layer> {
let Node { profile, name, source, features, required_by, configured, .. } = node;
let Loaded { opened, dir, manifest, digest } = profile;
let Some(Opened { reader, .. }) = self.opened.get(opened) else {
return Err(ProfileError::NoProfiles { location: source.to_string() }.into());
};
let paths: Vec<RelPath> = manifest.payload_paths().into_iter().collect();
let payload_dir =
if dir.is_empty() { PAYLOAD.to_owned() } else { format!("{dir}/{PAYLOAD}") };
let written = reader.read(&payload_dir, &paths)?;
if let Some(missing) = paths.into_iter().find(|path| written.get(path).is_none()) {
return Err(ProfileError::MissingPayload { profile: name, path: missing }.into());
}
let config = self.target.config();
let source_name = config
.sources
.iter()
.find(|(_, named)| **named == source)
.map(|(name, _)| name.clone());
let locked = self.target.locked(&source, &name);
let Manifest { profile: meta, merge, vars, features: declared, files, scaffolds, .. } =
manifest;
Ok(Layer {
rev: reader.rev().cloned(),
source,
source_name,
dir,
meta,
merge,
vars,
declared: declared.declared.into_keys().collect(),
features,
required_by: required_by.into_iter().collect(),
configured,
manifest: digest,
digest,
locked,
specs: (files, scaffolds),
scaffolds: BTreeMap::new(),
files: BTreeMap::new(),
written,
payload: Tree::default(),
starters: Tree::default(),
})
}
}
fn providers(
layers: &[Layer], overrides: &BTreeMap<RelPath, Override>,
) -> Result<BTreeMap<Slot, Provided>> {
let mut owners: BTreeMap<&RelPath, Vec<usize>> = BTreeMap::new();
for (index, layer) in layers.iter().enumerate() {
for (path, file) in &layer.files {
if file.gated.is_none() {
owners.entry(path).or_default().push(index);
}
}
}
let listed = |path: &RelPath| layers.iter().any(|layer| layer.files.contains_key(path));
if let Some(path) = overrides.keys().find(|path| !listed(path)) {
let mut provided: Vec<RelPath> =
layers.iter().flat_map(|layer| layer.files.keys().cloned()).collect();
provided.sort();
provided.dedup();
return Err(TargetError::StaleOverride { path: path.clone(), provided }.into());
}
let mut providers = BTreeMap::new();
let mut folds = BTreeMap::<String, &RelPath>::new();
for (path, candidates) in owners {
providers.extend(provide(layers, path, candidates, overrides.get(path))?);
if let Some(other) = folds.insert(path.fold(), path)
&& other != path
{
return Err(
ProfileError::FoldCollision { first: other.clone(), second: path.clone() }.into()
);
}
}
Ok(providers)
}
fn provide(
layers: &[Layer], path: &RelPath, mut candidates: Vec<usize>, over: Option<&Override>,
) -> Result<Vec<(Slot, Provided)>> {
let name = |index: usize| layers.get(index).map(|layer| layer.meta.name.clone());
let spec = |index: usize| {
layers.get(index).and_then(|layer| layer.files.get(path)).map(|file| &file.spec)
};
let policy = |index: usize| {
over.and_then(|o| o.policy)
.unwrap_or_else(|| spec(index).map_or_else(Policy::default, FileSpec::policy))
};
let starts = |index: usize| {
spec(index).is_some_and(|spec| match spec.scope {
Scope::File => policy(index) == Policy::Once,
Scope::Keys | Scope::Block => spec.starter.is_some(),
})
};
let mut starter = None;
if let Some(from) = over.and_then(|over| over.from.as_ref()) {
let listing: Vec<usize> = (0..layers.len())
.filter(|&i| layers.get(i).is_some_and(|l| l.files.contains_key(path)))
.collect();
if !listing.iter().any(|&i| name(i).as_ref() == Some(from)) {
let layers = listing.iter().filter_map(|&i| name(i)).collect();
return Err(ProfileError::NotProvided {
path: path.clone(),
from: from.clone(),
layers,
}
.into());
}
if let Some(picked) = candidates.iter().copied().find(|&i| name(i).as_ref() == Some(from)) {
if starts(picked) {
let part = |i: usize| spec(i).is_some_and(|spec| spec.scope != Scope::File);
candidates.retain(|&i| i == picked || part(i));
starter = Some(picked);
} else {
candidates = vec![picked];
}
}
}
let starts = |index: usize| starter.map_or_else(|| starts(index), |picked| picked == index);
let claims: Vec<Claim> = candidates
.iter()
.filter_map(|&i| {
Some(Claim {
layer: name(i)?,
scope: spec(i)?.scope,
policy: policy(i),
starts: starts(i),
})
})
.collect();
share(path, &claims)?;
let mut provided = Vec::with_capacity(candidates.len());
for index in candidates {
let (Some(spec), Some(owner)) = (spec(index), name(index)) else { continue };
let validate = over.and_then(|o| o.validate).or(spec.validate);
let format = validate.unwrap_or_else(|| Format::of(path));
let shape = Shape::of(spec.scope, path, format, spec.comment, owner.as_str())?;
let portion = shape.map(|shape| Portion {
scope: spec.scope,
shape,
content: Vec::new(),
payload: Written::default(),
text: Vec::new(),
starts: starts(index),
starter: None,
});
let slot = Slot { path: path.clone(), part: portion.as_ref().map(|_| owner.to_string()) };
let policy = policy(index);
provided.push((
slot,
Provided {
layer: index,
policy,
validate: format,
executable: spec.executable,
portion,
},
));
}
Ok(provided)
}
struct Claim {
layer: ProfileName,
scope: Scope,
policy: Policy,
starts: bool,
}
fn share(path: &RelPath, claims: &[Claim]) -> Result<()> {
if claims.len() <= 1 {
return Ok(());
}
let named = |keep: &dyn Fn(&Claim) -> bool| -> Vec<ProfileName> {
claims.iter().filter(|claim| keep(claim)).map(|claim| claim.layer.clone()).collect()
};
let wholes = named(&|claim| claim.scope == Scope::File);
if wholes.len() > 1 {
return Err(ProfileError::Collision { path: path.clone(), layers: wholes }.into());
}
let mut parts =
claims.iter().filter(|claim| claim.scope != Scope::File).map(|claim| claim.scope);
let one_scope = parts.next().is_none_or(|first| parts.all(|scope| scope == first));
let owned =
claims.iter().any(|claim| claim.scope == Scope::File && claim.policy != Policy::Once);
if !one_scope || owned {
let layers = claims.iter().map(|claim| (claim.layer.clone(), claim.scope)).collect();
return Err(ProfileError::Scopes { path: path.clone(), layers }.into());
}
let starters = named(&|claim| claim.starts);
if starters.len() > 1 {
return Err(ProfileError::Starters { path: path.clone(), layers: starters }.into());
}
Ok(())
}
fn portions(layers: &[Layer], providers: &mut BTreeMap<Slot, Provided>) -> Result<()> {
let mut owned: Vec<(&RelPath, usize, Key)> = Vec::new();
for (slot, provided) in providers.iter_mut() {
let Some(portion) = &mut provided.portion else {
continue;
};
let Some(layer) = layers.get(provided.layer) else {
continue;
};
let bytes = layer.payload.get(&slot.path).ok_or_else(|| ProfileError::MissingPayload {
profile: layer.meta.name.clone(),
path: slot.path.clone(),
})?;
let written =
layer.files.get(&slot.path).map_or(slot.path.as_str(), |file| file.written.as_str());
let label = layer.label(&format!("{PAYLOAD}/{written}"));
(portion.content, portion.payload) = portion.shape.read(bytes, &label)?;
portion.text = bytes.to_vec();
portion.starter =
layer.starters.get(&slot.path).filter(|_| portion.starts).map(<[u8]>::to_vec);
let keys = portion.shape.keys(&portion.content);
owned.extend(keys.into_iter().map(|key| (&slot.path, provided.layer, key)));
}
owned.sort_by_key(|&(path, layer, _)| (path, layer));
let name = |index: usize| layers.get(index).map(|l| l.meta.name.clone());
for (at, (path, layer, key)) in owned.iter().enumerate() {
let earlier = owned.get(..at).unwrap_or_default().iter().rev();
let clash = earlier
.take_while(|(other, ..)| other == path)
.find(|(_, other, theirs)| other != layer && overlap(key, theirs));
if let Some((_, other, theirs)) = clash
&& let (Some(first), Some(second)) = (name(*other), name(*layer))
{
let key = display(if key.len() <= theirs.len() { key } else { theirs });
return Err(ProfileError::Overlap { path: (*path).clone(), key, first, second }.into());
}
}
Ok(())
}