Skip to main content

devset_core/
resolve.rs

1//! Resolving a target's layers into one [`Resolved`] profile: fetched, composed, answered.
2
3use 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/// Which layers [`resolve`] may move off their locked commit.
26#[derive(Clone, Copy, Debug)]
27pub enum Refresh<'a> {
28    /// None: every layer at its locked commit.
29    None,
30    /// Every layer, to what its ref names now.
31    All,
32    /// The layer whose profile has this name.
33    Layer(&'a str),
34}
35
36/// Whether a layer's content is what the lock recorded.
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
38#[serde(rename_all = "lowercase")]
39pub enum Applied {
40    /// It is.
41    Current,
42    /// The layer moved, or its local directory changed, since it was applied.
43    Changed,
44    /// The layer has never been applied.
45    Never,
46}
47
48/// A profile resolved from its source at one revision.
49#[derive(Debug)]
50pub struct Layer {
51    /// Where it came from.
52    source: Source,
53    /// Its `[profile]` table.
54    meta: Meta,
55    /// Its `[merge]` table.
56    merge: MergeSpec,
57    /// Its variables.
58    vars: BTreeMap<VarName, VarSpec>,
59    /// The commit, for a versioned source.
60    rev: Option<Oid>,
61    /// Digest of manifest and payload together.
62    digest: Digest,
63    /// The digest the lock recorded for it.
64    locked: Option<Digest>,
65    /// Its files.
66    files: BTreeMap<RelPath, FileSpec>,
67    /// Their bytes.
68    payload: Tree,
69    /// The profile that requires this one, for a layer not configured by the target.
70    required_by: Option<String>,
71}
72
73impl Layer {
74    /// Where it came from.
75    #[must_use]
76    pub const fn source(&self) -> &Source {
77        &self.source
78    }
79
80    /// Its `[profile]` table.
81    #[must_use]
82    pub const fn meta(&self) -> &Meta {
83        &self.meta
84    }
85
86    /// Its `[merge]` table.
87    #[must_use]
88    pub const fn merge(&self) -> &MergeSpec {
89        &self.merge
90    }
91
92    /// The commit, for a versioned source.
93    #[must_use]
94    pub const fn rev(&self) -> Option<&Oid> {
95        self.rev.as_ref()
96    }
97
98    /// Digest of manifest and payload together.
99    #[must_use]
100    pub const fn digest(&self) -> Digest {
101        self.digest
102    }
103
104    /// The profile that requires this layer; `None` for a layer the target configures.
105    #[must_use]
106    pub fn required_by(&self) -> Option<&str> {
107        self.required_by.as_deref()
108    }
109
110    /// Whether this content is what the lock recorded.
111    #[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    /// Its variables.
121    pub(crate) fn vars(&self) -> impl Iterator<Item = (&VarName, &VarSpec)> {
122        self.vars.iter()
123    }
124
125    /// Where `file` in its profile is, as users know it.
126    pub(crate) fn label(&self, file: &str) -> String {
127        label(&self.source, file)
128    }
129
130    /// Its files' bytes.
131    pub(crate) const fn payload(&self) -> &Tree {
132        &self.payload
133    }
134
135    /// Replaces its files' bytes, templates rendered.
136    pub(crate) fn set_payload(&mut self, payload: Tree) {
137        self.payload = payload;
138    }
139
140    /// Whether any of its files is a template.
141    pub(crate) fn has_templates(&self) -> bool {
142        self.files.values().any(|spec| spec.template)
143    }
144
145    /// Whether `path` is a template.
146    pub(crate) fn is_template(&self, path: &RelPath) -> bool {
147        self.files.get(path).is_some_and(|spec| spec.template)
148    }
149
150    /// Reads and checks the profile at `source`, at `pin` if given.
151    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/// What a layer provides at one slot, overrides applied.
191#[derive(Clone, Debug)]
192pub(crate) struct Provided {
193    /// Index into the layers.
194    pub layer: usize,
195    /// How it is managed.
196    pub policy: Policy,
197    /// How a merge of its file is checked.
198    pub validate: Format,
199    /// Whether the file is written executable.
200    pub executable: bool,
201    /// Its part of the file; `None` when it provides the whole file.
202    pub portion: Option<Portion>,
203}
204
205/// One layer's part of a file.
206#[derive(Clone, Debug)]
207pub(crate) struct Portion {
208    /// How much of the file it is.
209    pub scope: Scope,
210    /// How it is found, read and written.
211    pub shape: Shape,
212    /// What the profile puts there, templates rendered, as [`Shape::read`] reads it.
213    pub content: Vec<u8>,
214    /// For keys, the payload's leaves as it writes them, which new keys follow.
215    pub payload: Written,
216    /// The payload file itself, templates rendered.
217    pub text: Vec<u8>,
218}
219
220impl Portion {
221    /// `file` with the part holding `content` over `keys`, as [`Shape::splice`] writes it.
222    ///
223    /// A file with nothing in it yet takes the profile's keys as the payload writes them, its
224    /// comments and layout included.
225    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
236/// Where `source`'s manifest is, as a path users recognise.
237fn 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/// A target's layers, composed: each file has one provider, or one per part, and settings are
247/// decided.
248#[derive(Debug)]
249pub struct Resolved {
250    /// In `config.toml` order.
251    layers: Vec<Layer>,
252    /// Each slot's provider.
253    providers: BTreeMap<Slot, Provided>,
254    /// The settings in force.
255    settings: Settings,
256    /// Drivers the layers suggest.
257    suggestions: Vec<Suggestion>,
258    /// The answer to every declared variable.
259    answers: BTreeMap<VarName, String>,
260}
261
262impl Resolved {
263    /// The layers, in `config.toml` order.
264    #[must_use]
265    pub fn layers(&self) -> &[Layer] {
266        &self.layers
267    }
268
269    /// The settings in force.
270    #[must_use]
271    pub const fn settings(&self) -> &Settings {
272        &self.settings
273    }
274
275    /// Merge drivers the layers suggest that the target has not configured.
276    #[must_use]
277    pub fn suggestions(&self) -> &[Suggestion] {
278        &self.suggestions
279    }
280
281    /// The answer to every declared variable.
282    #[must_use]
283    pub const fn answers(&self) -> &BTreeMap<VarName, String> {
284        &self.answers
285    }
286
287    /// The name of the layer that provides `entry`.
288    #[must_use]
289    pub fn provider(&self, entry: &Entry) -> Option<&str> {
290        Some(&self.layers.get(entry.want?.layer)?.meta.name)
291    }
292
293    /// Every slot, its provider, and what it provides: a whole file's bytes, or a part's content.
294    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    /// What the profile provides for `entry`: a whole file's bytes, or a part's content.
301    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    /// What `provided` gives `slot`.
311    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    /// `entry`'s part as the profile provides it; `None` for a whole file.
319    pub(crate) fn portion(&self, entry: &Entry) -> Option<&Portion> {
320        self.providers.get(&entry.slot())?.portion.as_ref()
321    }
322}
323
324/// Resolves `target`'s layers into one profile, the target's overrides and settings on top.
325///
326/// Each layer is read at its locked commit unless `refresh` moves it; then the layers are
327/// composed, their variables answered, and their templates rendered.
328///
329/// # Errors
330/// - [`Error::Source`](crate::Error::Source), a source cannot be read.
331/// - [`Error::Profile`](crate::Error::Profile), a source is not a valid profile, or the layers
332///   collide on a path or a setting the target does not decide.
333/// - [`Error::Target`](crate::Error::Target), an override or `refresh` names nothing.
334/// - [`VarError::Unanswered`](crate::VarError::Unanswered), a variable needs an answer; answer it
335///   with [`Target::answer`] and resolve again.
336pub 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
348/// How deep requirements may nest; a longer chain is a mistake, not a design.
349const DEPTH: usize = 16;
350
351/// Every layer of `target`, each after the profiles it requires, at its locked commit unless
352/// `refresh` moves it.
353fn 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
397/// Layers being expanded: each configured layer, after what it requires.
398struct Expansion<'a> {
399    /// Whose layers.
400    target: &'a Target,
401    /// Where git sources are fetched.
402    cache: &'a Cache,
403    /// Expanded so far, in order.
404    layers: Vec<Layer>,
405    /// The sources being expanded, outermost first; one met again is a cycle.
406    chain: Vec<Source>,
407}
408
409impl Expansion<'_> {
410    /// The layer at `source`: at `pin` if given, else at its locked commit, or at what its ref
411    /// names now when it `moves`.
412    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    /// Adds `layer`, after everything it requires; what it requires by git `moves` with it.
421    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
457/// Each of `sources`, as users know it.
458fn names(sources: &[Source]) -> Vec<String> {
459    sources.iter().map(ToString::to_string).collect()
460}
461
462/// The source of the profile at `relative` beside `requirer`, and the commit it is pinned to: the
463/// requirer's own, for a git source.
464fn 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
487/// `path` with `.` dropped and each `..` taking back the directory before it, by name alone;
488/// `None` for an absolute path that climbs above its root.
489fn 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
511/// The one layer that provides each file, or each layer's part of it, with `overrides` applied.
512///
513/// A target's `from` narrows a file to one layer, whatever the scopes; otherwise every layer's
514/// part is taken, so long as all own it in one scope, and one layer at most owns it whole.
515fn 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
579/// Checks that the layers in `candidates` may share `path`: one of them provides it, or all own
580/// parts of it in one scope.
581///
582/// # Errors
583/// - [`ProfileError::Collision`], several own it whole.
584/// - [`ProfileError::Scopes`], they own it in different scopes.
585fn 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
604/// Reads each part's content out of its layer's rendered payload, and checks that no two layers
605/// own overlapping keys of one file.
606fn 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}