1use alloc::collections::{BTreeMap, BTreeSet};
19use core::cell::OnceCell;
20use core::fmt;
21
22use camino::Utf8Path;
23use globset::{GlobBuilder, GlobMatcher};
24use ignore::WalkBuilder;
25
26use crate::errors::{Result, TargetError};
27use crate::graph::Enabler;
28use crate::name::{FeatureName, ProfileName, ScaffoldId};
29use crate::part::Slot;
30use crate::path::RelPath;
31use crate::plan::{Action, Mode};
32use crate::profile::When;
33use crate::resolve::{Layer, Resolved};
34use crate::survey::Entry;
35use crate::target::{DIR, Scaffold, Scaffolded, Target};
36use crate::vars::VarName;
37
38#[derive(Clone, Debug)]
40pub(crate) enum Pattern {
41 Path(RelPath),
43 Glob {
45 written: String,
47 glob: GlobMatcher,
49 },
50}
51
52impl Pattern {
53 pub(crate) fn parse(written: &str) -> Result<Self, String> {
58 if !written.contains(['*', '?', '[', '{']) {
59 return RelPath::new(written).map(Self::Path).map_err(|e| e.to_string());
60 }
61 let glob = GlobBuilder::new(written)
62 .literal_separator(true)
63 .build()
64 .map_err(|e| e.kind().to_string())?;
65 Ok(Self::Glob { written: written.to_owned(), glob: glob.compile_matcher() })
66 }
67
68 pub(crate) fn as_str(&self) -> &str {
70 match self {
71 Self::Path(path) => path.as_str(),
72 Self::Glob { written, .. } => written,
73 }
74 }
75}
76
77impl fmt::Display for Pattern {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 f.write_str(self.as_str())
80 }
81}
82
83#[derive(Clone, Debug, PartialEq, Eq)]
85pub enum Reason {
86 Feature(FeatureName),
88 Profile(ProfileName),
90 Var {
92 name: VarName,
94 answer: String,
96 },
97 Found(ScaffoldId),
99 Moved {
101 scaffold: ScaffoldId,
103 path: RelPath,
105 },
106 Absent(String),
108}
109
110impl fmt::Display for Reason {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 match self {
113 Self::Feature(feature) => write!(f, "feature {feature} is off"),
114 Self::Profile(profile) => write!(f, "profile {profile} is not active"),
115 Self::Var { name, answer } => write!(f, "{name} is {answer:?}"),
116 Self::Found(scaffold) => write!(f, "scaffold {scaffold} found the target's own"),
117 Self::Moved { scaffold, path } => write!(f, "scaffold {scaffold} wrote it at {path}"),
118 Self::Absent(pattern) => write!(f, "{pattern} will not exist"),
119 }
120 }
121}
122
123pub(crate) fn check(
126 when: &When, features: &BTreeMap<FeatureName, BTreeSet<Enabler>>,
127 active: &BTreeSet<&ProfileName>, answers: &BTreeMap<VarName, String>,
128) -> Option<Reason> {
129 if let Some(feature) = when.features.iter().find(|f| !features.contains_key(*f)) {
130 return Some(Reason::Feature(feature.clone()));
131 }
132 if let Some(profile) = when.profiles.iter().find(|p| !active.contains(p)) {
133 return Some(Reason::Profile(profile.clone()));
134 }
135 when.vars.iter().find_map(|(name, values)| {
136 let answer = answers.get(name).map_or("", String::as_str);
137 (!values.iter().any(|value| value == answer))
138 .then(|| Reason::Var { name: name.clone(), answer: answer.to_owned() })
139 })
140}
141
142pub(crate) fn statics(layers: &mut [Layer], answers: &BTreeMap<VarName, String>) {
144 let active: BTreeSet<ProfileName> = layers.iter().map(|layer| layer.name().clone()).collect();
145 let active: BTreeSet<&ProfileName> = active.iter().collect();
146 for layer in layers {
147 let features = layer.features().clone();
148 for file in layer.files_mut().values_mut() {
149 file.gated = check(&file.spec.when, &features, &active, answers);
150 }
151 }
152}
153
154#[derive(Debug, Default)]
157pub(crate) struct Settled {
158 pub scaffolds: BTreeMap<ScaffoldId, Scaffold>,
160 pub gated: BTreeMap<Slot, Reason>,
162 pub exists: BTreeMap<Slot, Vec<(String, bool)>>,
164}
165
166pub(crate) fn settle(
172 entries: &mut Vec<Entry>, resolved: &Resolved, target: &Target,
173) -> Result<Settled> {
174 let disk = Disk::new(target.root());
175 let mut scaffolds = decide(resolved, target, &disk)?;
176 let mut off: Vec<Option<Reason>> = vec![None; entries.len()];
177 let mut gated: Vec<(usize, &[Pattern])> = Vec::new();
178 let mut scaffolded: Vec<(usize, ScaffoldId, &RelPath)> = Vec::new();
180 for (at, entry) in entries.iter_mut().enumerate() {
181 let (Some(file), Some(profile)) = (resolved.file(entry), resolved.provider(entry)) else {
182 continue;
183 };
184 if let Some(group) = &file.spec.scaffold {
185 let id = ScaffoldId { profile: profile.clone(), group: group.clone() };
186 let reason = match scaffolds.get(&id) {
187 Some(Scaffold { decision: Scaffolded::Found, .. }) => {
188 Some(Reason::Found(id.clone()))
189 },
190 Some(Scaffold { files, .. }) => files
191 .get(&file.written)
192 .filter(|path| **path != entry.path)
193 .map(|path| Reason::Moved { scaffold: id.clone(), path: path.clone() }),
194 None => None,
195 };
196 if let Some(reason) = reason {
197 if let Some(slot) = off.get_mut(at) {
198 *slot = Some(reason);
199 }
200 continue;
201 }
202 if target.rescaffolds().contains(&id) {
203 entry.record = None;
204 entry.recorded = None;
205 }
206 scaffolded.push((at, id, &file.written));
207 }
208 if !file.exists.is_empty() {
209 gated.push((at, &file.exists));
210 }
211 }
212 let evidence = Evidence::new(entries, &disk);
213 let mut on: Vec<bool> = off.iter().map(Option::is_none).collect();
214 for &(at, _) in &gated {
215 if let Some(slot) = on.get_mut(at) {
216 *slot = false;
217 }
218 }
219 loop {
220 let turned: Vec<usize> = gated
221 .iter()
222 .filter(|&&(at, _)| !on.get(at).copied().unwrap_or(true))
223 .filter(|&&(at, patterns)| {
224 patterns.iter().all(|pattern| evidence.holds(pattern, &on, at))
225 })
226 .map(|&(at, _)| at)
227 .collect();
228 if turned.is_empty() {
229 break;
230 }
231 for at in turned {
232 if let Some(slot) = on.get_mut(at) {
233 *slot = true;
234 }
235 }
236 }
237 let mut exists = BTreeMap::new();
238 for &(at, patterns) in &gated {
239 let outcomes: Vec<(String, bool)> = patterns
240 .iter()
241 .map(|pattern| (pattern.to_string(), evidence.holds(pattern, &on, at)))
242 .collect();
243 let absent = outcomes.iter().find(|(_, holds)| !holds).map(|(pattern, _)| pattern.clone());
244 if let (Some(slot), Some(absent), false) =
245 (off.get_mut(at), absent, on.get(at).copied().unwrap_or(true))
246 {
247 *slot = Some(Reason::Absent(absent));
248 }
249 if let Some(entry) = entries.get(at) {
250 exists.insert(entry.slot(), outcomes);
251 }
252 }
253 for (at, id, written) in scaffolded {
254 let (Some(true), Some(entry), Some(scaffold)) =
255 (on.get(at).copied(), entries.get(at), scaffolds.get_mut(&id))
256 else {
257 continue;
258 };
259 scaffold.files.insert(written.clone(), entry.path.clone());
260 }
261 let mut settled = Settled { scaffolds, gated: BTreeMap::new(), exists };
262 let mut kept = Vec::with_capacity(entries.len());
263 for (entry, reason) in entries.drain(..).zip(off) {
264 let Some(reason) = reason else {
265 kept.push(entry);
266 continue;
267 };
268 settled.gated.insert(entry.slot(), reason.clone());
269 if entry.record.is_some() {
270 kept.push(Entry { want: None, gate: Some(reason), ..entry });
271 }
272 }
273 *entries = kept;
274 Ok(settled)
275}
276
277fn decide(
281 resolved: &Resolved, target: &Target, disk: &Disk<'_>,
282) -> Result<BTreeMap<ScaffoldId, Scaffold>> {
283 let mut decided = BTreeMap::new();
284 let mut known = Vec::new();
285 for layer in resolved.layers() {
286 for (group, unless) in layer.sentinels() {
287 let id = ScaffoldId { profile: layer.name().clone(), group: group.clone() };
288 known.push(id.to_string());
289 let applies = layer
290 .files()
291 .values()
292 .any(|file| file.gated.is_none() && file.spec.scaffold.as_ref() == Some(group));
293 let rescaffold = target.rescaffolds().contains(&id);
294 let decision = if rescaffold {
295 Scaffold { decision: Scaffolded::Written, files: BTreeMap::new() }
296 } else if let Some(recorded) = target.scaffolds().get(&id) {
297 recorded.clone()
298 } else if !applies {
299 continue;
300 } else {
301 let found = unless.iter().any(|pattern| disk.has(pattern));
302 let decision = if found { Scaffolded::Found } else { Scaffolded::Written };
303 Scaffold { decision, files: BTreeMap::new() }
304 };
305 decided.insert(id, decision);
306 }
307 }
308 if let Some(unknown) = target.rescaffolds().iter().find(|id| !decided.contains_key(*id)) {
309 return Err(
310 TargetError::NoSuchScaffold { name: unknown.to_string(), scaffolds: known }.into()
311 );
312 }
313 Ok(decided)
314}
315
316struct Disk<'a> {
318 root: &'a Utf8Path,
320 files: OnceCell<Vec<RelPath>>,
322}
323
324impl<'a> Disk<'a> {
325 const fn new(root: &'a Utf8Path) -> Self {
327 Self { root, files: OnceCell::new() }
328 }
329
330 fn exists(&self, path: &RelPath) -> bool {
332 fs_err::symlink_metadata(path.under(self.root)).is_ok()
333 }
334
335 fn has(&self, pattern: &Pattern) -> bool {
337 match pattern {
338 Pattern::Path(path) => self.exists(path),
339 Pattern::Glob { glob, .. } => {
340 self.files().iter().any(|file| glob.is_match(file.as_str()))
341 },
342 }
343 }
344
345 fn files(&self) -> &[RelPath] {
347 self.files.get_or_init(|| {
348 WalkBuilder::new(self.root)
349 .hidden(false)
350 .require_git(false)
351 .git_global(false)
352 .filter_entry(|entry| !matches!(entry.file_name().to_str(), Some(".git" | DIR)))
353 .build()
354 .flatten()
355 .filter(|entry| entry.file_type().is_some_and(|kind| !kind.is_dir()))
356 .filter_map(|entry| {
357 let inside = entry.path().strip_prefix(self.root).ok()?;
358 let inside = Utf8Path::from_path(inside)?;
359 let joined: Vec<&str> = inside.components().map(|part| part.as_str()).collect();
360 RelPath::new(&joined.join("/")).ok()
361 })
362 .collect()
363 })
364 }
365}
366
367struct Evidence<'a> {
369 entries: &'a [Entry],
371 by_path: BTreeMap<&'a RelPath, Vec<usize>>,
373 disk: &'a Disk<'a>,
375}
376
377impl<'a> Evidence<'a> {
378 fn new(entries: &'a [Entry], disk: &'a Disk<'a>) -> Self {
380 let mut by_path: BTreeMap<&RelPath, Vec<usize>> = BTreeMap::new();
381 for (at, entry) in entries.iter().enumerate() {
382 by_path.entry(&entry.path).or_default().push(at);
383 }
384 Self { entries, by_path, disk }
385 }
386
387 fn holds(&self, pattern: &Pattern, on: &[bool], own: usize) -> bool {
390 match pattern {
391 Pattern::Path(path) => self.stays(path, on, own),
392 Pattern::Glob { glob, .. } => {
393 let managed = self.by_path.keys().copied();
394 let found =
395 self.disk.files().iter().filter(|file| !self.by_path.contains_key(file));
396 managed
397 .chain(found)
398 .any(|path| glob.is_match(path.as_str()) && self.stays(path, on, own))
399 },
400 }
401 }
402
403 fn stays(&self, path: &RelPath, on: &[bool], own: usize) -> bool {
406 let at: Vec<usize> = self
407 .by_path
408 .get(path)
409 .map_or_default(|at| at.iter().copied().filter(|&at| at != own).collect());
410 if at.is_empty() {
411 let under = format!("{path}/");
412 let written =
413 self.by_path.keys().filter(|managed| managed.as_str().starts_with(&under));
414 return self.disk.exists(path) || written.into_iter().any(|p| self.stays(p, on, own));
415 }
416 at.iter().any(|&at| {
417 let Some(entry) = self.entries.get(at) else { return false };
418 let applies = on.get(at).copied().unwrap_or(false);
419 let action = if applies {
420 entry.action(Mode::Apply)
421 } else {
422 Entry { want: None, ..entry.clone() }.action(Mode::Apply)
423 };
424 match action {
425 Action::Write | Action::Merge | Action::Record => true,
426 Action::Remove => entry.part.is_some() && entry.present,
428 Action::Keep | Action::Conflict | Action::Untrack => entry.present,
429 }
430 })
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use super::Pattern;
437
438 #[test]
439 fn patterns() {
440 assert!(matches!(Pattern::parse("docs/book.toml"), Ok(Pattern::Path(_))), "a path");
441 assert!(matches!(Pattern::parse("**/*.proto"), Ok(Pattern::Glob { .. })), "a glob");
442 let Ok(Pattern::Glob { glob, .. }) = Pattern::parse("src/*.rs") else { panic!("a glob") };
443 assert!(
444 glob.is_match("src/lib.rs") && !glob.is_match("src/a/lib.rs"),
445 "`*` stays in a component"
446 );
447 for bad in ["", "../x", "/abs", "a/[b"] {
448 assert!(Pattern::parse(bad).is_err(), "{bad:?} is neither");
449 }
450 }
451}