1use alloc::collections::{BTreeMap, BTreeSet};
4use core::slice;
5
6use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
7use semver::Version;
8use serde::Serialize;
9
10use crate::digest::Digest;
11use crate::errors::{ProfileError, Result, TargetError};
12use crate::format::Format;
13pub use crate::merge::Driver;
14use crate::part::{Key, Scope, Shape, Slot, Written, display, overlap};
15use crate::path::RelPath;
16use crate::profile::{FileSpec, MANIFEST, Manifest, MergeSpec, Meta, PAYLOAD, Policy, Requirement};
17pub use crate::settings::{Settings, Suggestion};
18use crate::source::{Cache, Oid, Source};
19use crate::survey::Entry;
20use crate::target::{Override, Target, from_toml};
21use crate::tree::Tree;
22pub use crate::vars::Question;
23use crate::vars::{self, VarName, VarSpec};
24
25#[derive(Clone, Copy, Debug)]
27pub enum Refresh<'a> {
28 None,
30 All,
32 Layer(&'a str),
34}
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
38#[serde(rename_all = "lowercase")]
39pub enum Applied {
40 Current,
42 Changed,
44 Never,
46}
47
48#[derive(Debug)]
50pub struct Layer {
51 source: Source,
53 meta: Meta,
55 merge: MergeSpec,
57 vars: BTreeMap<VarName, VarSpec>,
59 rev: Option<Oid>,
61 digest: Digest,
63 locked: Option<Digest>,
65 files: BTreeMap<RelPath, FileSpec>,
67 payload: Tree,
69 required_by: Option<String>,
71}
72
73impl Layer {
74 #[must_use]
76 pub const fn source(&self) -> &Source {
77 &self.source
78 }
79
80 #[must_use]
82 pub const fn meta(&self) -> &Meta {
83 &self.meta
84 }
85
86 #[must_use]
88 pub const fn merge(&self) -> &MergeSpec {
89 &self.merge
90 }
91
92 #[must_use]
94 pub const fn rev(&self) -> Option<&Oid> {
95 self.rev.as_ref()
96 }
97
98 #[must_use]
100 pub const fn digest(&self) -> Digest {
101 self.digest
102 }
103
104 #[must_use]
106 pub fn required_by(&self) -> Option<&str> {
107 self.required_by.as_deref()
108 }
109
110 #[must_use]
112 pub fn applied(&self) -> Applied {
113 match self.locked {
114 None => Applied::Never,
115 Some(locked) if locked == self.digest => Applied::Current,
116 Some(_) => Applied::Changed,
117 }
118 }
119
120 pub(crate) fn vars(&self) -> impl Iterator<Item = (&VarName, &VarSpec)> {
122 self.vars.iter()
123 }
124
125 pub(crate) fn label(&self, file: &str) -> String {
127 label(&self.source, file)
128 }
129
130 pub(crate) const fn payload(&self) -> &Tree {
132 &self.payload
133 }
134
135 pub(crate) fn set_payload(&mut self, payload: Tree) {
137 self.payload = payload;
138 }
139
140 pub(crate) fn has_templates(&self) -> bool {
142 self.files.values().any(|spec| spec.template)
143 }
144
145 pub(crate) fn is_template(&self, path: &RelPath) -> bool {
147 self.files.get(path).is_some_and(|spec| spec.template)
148 }
149
150 fn load(source: &Source, root: &Utf8Path, pin: Option<&Oid>, cache: &Cache) -> Result<Self> {
152 let reader = source.open(root, pin, cache)?;
153 let manifest_path = RelPath::new(MANIFEST)?;
154 let head = reader.read("", slice::from_ref(&manifest_path))?;
155 let Some(raw) = head.get(&manifest_path) else {
156 let profiles = reader.profiles(source);
157 return Err(ProfileError::NotAProfile { location: source.to_string(), profiles }.into());
158 };
159 let manifest: Manifest = from_toml(raw, &label(source, MANIFEST))?;
160 let meta = manifest.profile;
161 if let Some(required) = &meta.devset
162 && !Version::parse(crate::VERSION).is_ok_and(|version| required.matches(&version))
163 {
164 let requires = required.to_string();
165 return Err(ProfileError::Incompatible { profile: meta.name, requires }.into());
166 }
167 let paths: Vec<RelPath> = manifest.files.keys().cloned().collect();
168 let payload = reader.read(PAYLOAD, &paths)?;
169 if let Some(missing) = paths.into_iter().find(|path| payload.get(path).is_none()) {
170 return Err(ProfileError::MissingPayload { profile: meta.name, path: missing }.into());
171 }
172 let mut hasher = blake3::Hasher::new();
173 hasher.update(Digest::of(raw).as_bytes());
174 hasher.update(payload.digest().as_bytes());
175 Ok(Self {
176 source: source.clone(),
177 meta,
178 merge: manifest.merge,
179 vars: manifest.vars,
180 rev: reader.rev().cloned(),
181 digest: Digest::new(hasher.finalize()),
182 locked: None,
183 files: manifest.files,
184 payload,
185 required_by: None,
186 })
187 }
188}
189
190#[derive(Clone, Debug)]
192pub(crate) struct Provided {
193 pub layer: usize,
195 pub policy: Policy,
197 pub validate: Format,
199 pub executable: bool,
201 pub portion: Option<Portion>,
203}
204
205#[derive(Clone, Debug)]
207pub(crate) struct Portion {
208 pub scope: Scope,
210 pub shape: Shape,
212 pub content: Vec<u8>,
214 pub payload: Written,
216 pub text: Vec<u8>,
218}
219
220impl Portion {
221 pub(crate) fn splice(
226 &self, file: &[u8], content: &[u8], keys: &BTreeSet<Key>, label: &str,
227 ) -> Result<Vec<u8>> {
228 let keyed = matches!(self.shape, Shape::Keys(_));
229 if keyed && content == self.content && file.trim_ascii().is_empty() {
230 return Ok(self.text.clone());
231 }
232 self.shape.splice(file, content, keys, &self.payload, label)
233 }
234}
235
236fn label(source: &Source, file: &str) -> String {
238 match source {
239 Source::Dir(dir) => dir.join(file).into_string(),
240 Source::Git { url, path, .. } => path
241 .as_ref()
242 .map_or_else(|| format!("{url}/{file}"), |path| format!("{url}/{path}/{file}")),
243 }
244}
245
246#[derive(Debug)]
249pub struct Resolved {
250 layers: Vec<Layer>,
252 providers: BTreeMap<Slot, Provided>,
254 settings: Settings,
256 suggestions: Vec<Suggestion>,
258 answers: BTreeMap<VarName, String>,
260}
261
262impl Resolved {
263 #[must_use]
265 pub fn layers(&self) -> &[Layer] {
266 &self.layers
267 }
268
269 #[must_use]
271 pub const fn settings(&self) -> &Settings {
272 &self.settings
273 }
274
275 #[must_use]
277 pub fn suggestions(&self) -> &[Suggestion] {
278 &self.suggestions
279 }
280
281 #[must_use]
283 pub const fn answers(&self) -> &BTreeMap<VarName, String> {
284 &self.answers
285 }
286
287 #[must_use]
289 pub fn provider(&self, entry: &Entry) -> Option<&str> {
290 Some(&self.layers.get(entry.want?.layer)?.meta.name)
291 }
292
293 pub(crate) fn provided(&self) -> impl Iterator<Item = (&Slot, &Provided, &[u8])> {
295 self.providers
296 .iter()
297 .filter_map(|(slot, provided)| Some((slot, provided, self.bytes(slot, provided)?)))
298 }
299
300 pub(crate) fn payload(&self, entry: &Entry) -> Result<&[u8]> {
302 let slot = entry.slot();
303 let bytes = self.providers.get(&slot).and_then(|provided| self.bytes(&slot, provided));
304 bytes.ok_or_else(|| {
305 let profile = self.provider(entry).unwrap_or_default().to_owned();
306 ProfileError::MissingPayload { profile, path: entry.path.clone() }.into()
307 })
308 }
309
310 fn bytes<'a>(&'a self, slot: &Slot, provided: &'a Provided) -> Option<&'a [u8]> {
312 match &provided.portion {
313 Some(portion) => Some(&portion.content),
314 None => self.layers.get(provided.layer)?.payload.get(&slot.path),
315 }
316 }
317
318 pub(crate) fn portion(&self, entry: &Entry) -> Option<&Portion> {
320 self.providers.get(&entry.slot())?.portion.as_ref()
321 }
322}
323
324pub fn resolve(target: &Target, cache: &Cache, refresh: Refresh<'_>) -> Result<Resolved> {
337 let config = target.config();
338 let layers = load(target, cache, refresh)?;
339 let mut providers = providers(&layers, &config.files)?;
340 let answers = vars::answers(&layers, target)?;
341 let layers = vars::render(layers, &answers)?;
342 portions(&layers, &mut providers)?;
343 let settings = Settings::compose(&layers, &config.merge)?;
344 let suggestions = Settings::suggestions(&layers, &config.merge);
345 Ok(Resolved { layers, providers, settings, suggestions, answers })
346}
347
348const DEPTH: usize = 16;
350
351fn load(target: &Target, cache: &Cache, refresh: Refresh<'_>) -> Result<Vec<Layer>> {
354 let mut expansion = Expansion { target, cache, layers: Vec::new(), chain: Vec::new() };
355 let mut refreshed = false;
356 for source in &target.config().layers {
357 let moves = matches!(refresh, Refresh::All);
358 let mut layer = expansion.load(source, None, moves)?;
359 let named = matches!(refresh, Refresh::Layer(name) if layer.meta.name == name);
360 if named {
361 refreshed = true;
362 if layer.rev.is_some() {
363 layer = expansion.load(source, None, true)?;
364 }
365 }
366 expansion.expand(layer, moves || named, 0)?;
367 }
368 let layers = expansion.layers;
369 for (index, layer) in layers.iter().enumerate() {
370 let twin = layers
371 .iter()
372 .skip(index.saturating_add(1))
373 .find(|other| other.meta.name == layer.meta.name);
374 if let Some(twin) = twin {
375 let (first, second) = (layer.source.to_string(), twin.source.to_string());
376 return Err(
377 ProfileError::SameName { name: layer.meta.name.clone(), first, second }.into()
378 );
379 }
380 }
381 if let Refresh::Layer(name) = refresh
382 && !refreshed
383 {
384 if let Some(by) = layers.iter().find(|l| l.meta.name == name).and_then(Layer::required_by) {
385 return Err(TargetError::Required { name: name.into(), by: by.to_owned() }.into());
386 }
387 let names = layers
388 .iter()
389 .filter(|layer| layer.required_by.is_none())
390 .map(|layer| layer.meta.name.clone())
391 .collect();
392 return Err(TargetError::NoSuchLayer { name: name.into(), layers: names }.into());
393 }
394 Ok(layers)
395}
396
397struct Expansion<'a> {
399 target: &'a Target,
401 cache: &'a Cache,
403 layers: Vec<Layer>,
405 chain: Vec<Source>,
407}
408
409impl Expansion<'_> {
410 fn load(&self, source: &Source, pin: Option<&Oid>, moves: bool) -> Result<Layer> {
413 let locked = self.target.locked(source);
414 let pin = pin.or_else(|| locked.and_then(|locked| locked.rev.as_ref()).filter(|_| !moves));
415 let mut layer = Layer::load(source, self.target.root(), pin, self.cache)?;
416 layer.locked = locked.map(|locked| locked.digest);
417 Ok(layer)
418 }
419
420 fn expand(&mut self, layer: Layer, moves: bool, depth: usize) -> Result<()> {
422 if self.layers.iter().any(|expanded| expanded.source == layer.source) {
423 return Ok(());
424 }
425 if let Some(twin) = self.layers.iter().find(|expanded| expanded.source.twin(&layer.source))
426 {
427 let (first, second) = (twin.source.to_string(), layer.source.to_string());
428 return Err(ProfileError::Diverged { first, second }.into());
429 }
430 self.chain.push(layer.source.clone());
431 if depth >= DEPTH {
432 return Err(ProfileError::TooDeep { chain: names(&self.chain) }.into());
433 }
434 for requirement in &layer.meta.requires {
435 let (source, pin) = match requirement {
436 Requirement::Sibling(relative) => sibling(&layer, relative)?,
437 Requirement::Git(source) => (source.clone(), None),
438 };
439 if self.chain.contains(&source) {
440 let mut chain = names(&self.chain);
441 chain.push(source.to_string());
442 return Err(ProfileError::Cycle { chain }.into());
443 }
444 if self.layers.iter().any(|expanded| expanded.source == source) {
445 continue;
446 }
447 let mut required = self.load(&source, pin.as_ref(), moves)?;
448 required.required_by = Some(layer.meta.name.clone());
449 self.expand(required, moves, depth.saturating_add(1))?;
450 }
451 self.chain.pop();
452 self.layers.push(layer);
453 Ok(())
454 }
455}
456
457fn names(sources: &[Source]) -> Vec<String> {
459 sources.iter().map(ToString::to_string).collect()
460}
461
462fn sibling(requirer: &Layer, relative: &str) -> Result<(Source, Option<Oid>)> {
465 let escapes =
466 || ProfileError::Escapes { profile: requirer.meta.name.clone(), path: relative.to_owned() };
467 match &requirer.source {
468 Source::Dir(dir) => {
469 Ok((Source::Dir(lexical(&dir.join(relative)).ok_or_else(escapes)?), None))
470 },
471 Source::Git { url, at, path } => {
472 let base = Utf8Path::new(path.as_ref().map_or("", RelPath::as_str));
473 let joined = lexical(&base.join(relative)).ok_or_else(escapes)?;
474 if joined.starts_with("..") {
475 return Err(escapes().into());
476 }
477 let path = match joined.as_str() {
478 "" | "." => None,
479 inside => Some(RelPath::new(inside)?),
480 };
481 let source = Source::Git { url: url.clone(), at: at.clone(), path };
482 Ok((source, requirer.rev.clone()))
483 },
484 }
485}
486
487fn lexical(path: &Utf8Path) -> Option<Utf8PathBuf> {
490 let mut out: Vec<Utf8Component<'_>> = Vec::new();
491 for component in path.components() {
492 match component {
493 Utf8Component::CurDir => {},
494 Utf8Component::ParentDir => match out.last() {
495 Some(Utf8Component::Normal(_)) => {
496 out.pop();
497 },
498 Some(Utf8Component::RootDir | Utf8Component::Prefix(_)) => return None,
499 Some(Utf8Component::ParentDir | Utf8Component::CurDir) | None => {
500 out.push(component);
501 },
502 },
503 Utf8Component::Prefix(_) | Utf8Component::RootDir | Utf8Component::Normal(_) => {
504 out.push(component);
505 },
506 }
507 }
508 Some(out.into_iter().collect())
509}
510
511fn providers(
516 layers: &[Layer], overrides: &BTreeMap<RelPath, Override>,
517) -> Result<BTreeMap<Slot, Provided>> {
518 let mut owners: BTreeMap<&RelPath, Vec<usize>> = BTreeMap::new();
519 for (index, layer) in layers.iter().enumerate() {
520 for path in layer.files.keys() {
521 owners.entry(path).or_default().push(index);
522 }
523 }
524 if let Some(path) = overrides.keys().find(|path| !owners.contains_key(path)) {
525 let provided = owners.keys().map(|&path| path.clone()).collect();
526 return Err(TargetError::StaleOverride { path: path.clone(), provided }.into());
527 }
528 let name =
529 |index: usize| layers.get(index).map_or_else(String::new, |layer| layer.meta.name.clone());
530 let mut providers = BTreeMap::new();
531 let mut folds = BTreeMap::<String, &RelPath>::new();
532 for (path, mut candidates) in owners {
533 let over = overrides.get(path);
534 let spec = |index: usize| layers.get(index).and_then(|layer| layer.files.get(path));
535 let scope = |index: usize| spec(index).map_or(Scope::File, |spec| spec.scope);
536 if let Some(from) = over.and_then(|over| over.from.as_deref()) {
537 let picked =
538 candidates.iter().copied().find(|&i| name(i) == from).ok_or_else(|| {
539 let layers = candidates.iter().map(|&i| name(i)).collect();
540 ProfileError::NotProvided { path: path.clone(), from: from.into(), layers }
541 })?;
542 candidates = vec![picked];
543 }
544 share(path, &candidates, name, scope)?;
545 for index in candidates {
546 let spec = spec(index).cloned().unwrap_or_default();
547 let owner = name(index);
548 let validate = over.and_then(|o| o.validate).or(spec.validate);
549 let format = validate.unwrap_or_else(|| Format::of(path));
550 let shape = Shape::of(spec.scope, path, format, spec.comment, &owner)?;
551 let portion = shape.map(|shape| Portion {
552 scope: spec.scope,
553 shape,
554 content: Vec::new(),
555 payload: Written::default(),
556 text: Vec::new(),
557 });
558 let slot = Slot { path: path.clone(), part: portion.as_ref().map(|_| owner) };
559 let provided = Provided {
560 layer: index,
561 policy: over.and_then(|o| o.policy).or(spec.policy).unwrap_or_default(),
562 validate: format,
563 executable: spec.executable,
564 portion,
565 };
566 providers.insert(slot, provided);
567 }
568 if let Some(other) = folds.insert(path.fold(), path)
569 && other != path
570 {
571 return Err(
572 ProfileError::FoldCollision { first: other.clone(), second: path.clone() }.into()
573 );
574 }
575 }
576 Ok(providers)
577}
578
579fn share(
586 path: &RelPath, candidates: &[usize], name: impl Fn(usize) -> String,
587 scope: impl Fn(usize) -> Scope,
588) -> Result<()> {
589 let scopes: Vec<Scope> = candidates.iter().map(|&i| scope(i)).collect();
590 match scopes.as_slice() {
591 [_] => Ok(()),
592 [first, rest @ ..] if rest.iter().all(|s| s == first) && *first != Scope::File => Ok(()),
593 _ if scopes.iter().all(|&s| s == Scope::File) => {
594 let layers = candidates.iter().map(|&i| name(i)).collect();
595 Err(ProfileError::Collision { path: path.clone(), layers }.into())
596 },
597 _ => {
598 let layers = candidates.iter().map(|&i| (name(i), scope(i))).collect();
599 Err(ProfileError::Scopes { path: path.clone(), layers }.into())
600 },
601 }
602}
603
604fn portions(layers: &[Layer], providers: &mut BTreeMap<Slot, Provided>) -> Result<()> {
607 let mut owned: Vec<(&RelPath, usize, Key)> = Vec::new();
608 for (slot, provided) in providers.iter_mut() {
609 let Some(portion) = &mut provided.portion else {
610 continue;
611 };
612 let Some(layer) = layers.get(provided.layer) else {
613 continue;
614 };
615 let bytes = layer.payload.get(&slot.path).ok_or_else(|| ProfileError::MissingPayload {
616 profile: layer.meta.name.clone(),
617 path: slot.path.clone(),
618 })?;
619 let label = layer.label(&format!("{PAYLOAD}/{}", slot.path));
620 (portion.content, portion.payload) = portion.shape.read(bytes, &label)?;
621 portion.text = bytes.to_vec();
622 let keys = portion.shape.keys(&portion.content);
623 owned.extend(keys.into_iter().map(|key| (&slot.path, provided.layer, key)));
624 }
625 owned.sort_by_key(|&(path, layer, _)| (path, layer));
626 let name = |index: usize| layers.get(index).map_or_else(String::new, |l| l.meta.name.clone());
627 for (at, (path, layer, key)) in owned.iter().enumerate() {
628 let earlier = owned.get(..at).unwrap_or_default().iter().rev();
629 let clash = earlier
630 .take_while(|(other, ..)| other == path)
631 .find(|(_, other, theirs)| other != layer && overlap(key, theirs));
632 if let Some((_, other, theirs)) = clash {
633 let key = display(if key.len() <= theirs.len() { key } else { theirs });
634 let (first, second) = (name(*other), name(*layer));
635 return Err(ProfileError::Overlap { path: (*path).clone(), key, first, second }.into());
636 }
637 }
638 Ok(())
639}