lib/resolver/
mod.rs

1use crate::{
2    common::{util, AbsolutePath, Item},
3    config::Config,
4    verbose_println,
5};
6use derive_more::From;
7use failure::Fail;
8use std::{
9    collections::HashSet,
10    ffi::OsString,
11    io, iter,
12    path::{Path, PathBuf},
13};
14use walkdir::WalkDir;
15
16/// Appends a "." to the start of `path`
17fn make_hidden(path: &Path) -> PathBuf {
18    let path_str = OsString::from(path.as_os_str());
19    let hidden_path = {
20        let mut hidden_path = OsString::from(".");
21        hidden_path.push(path_str);
22
23        hidden_path
24    };
25
26    PathBuf::from(hidden_path)
27}
28
29/// Returns every non-hidden non-excluded file in `dir` (recursively, ignoring
30/// directories).
31fn link_dir_contents(
32    dir: &AbsolutePath,
33    excludes: &HashSet<&AbsolutePath>,
34) -> Result<Vec<Item>, Error> {
35    let mut res = vec![];
36    for entry in WalkDir::new(dir)
37        .into_iter()
38        .filter_entry(|entry| !util::is_hidden(entry.file_name()))
39    {
40        let entry = entry?;
41
42        let path = AbsolutePath::from(entry.path());
43
44        if excludes.contains(&path) {
45            verbose_println!("Excluded {}", path);
46        }
47
48        if !util::is_hidden(entry.file_name())
49            && entry.file_type().is_file()
50            && !excludes.contains(&path)
51        {
52            let dest = {
53                let dest_tail = match dir.parent() {
54                    None => path.as_path(),
55                    Some(parent) => path
56                        .strip_prefix(parent)
57                        .expect("dir must be a prefix of entry"),
58                };
59
60                AbsolutePath::from(util::home_dir().join(make_hidden(dest_tail)))
61            };
62            let source = path;
63
64            res.push(Item::new(source, dest));
65        }
66    }
67
68    Ok(res)
69}
70
71/// Finds the items under `path` which are to be symlinked, according to all the
72/// options specified, and place then in `res`
73fn find_items(
74    root: AbsolutePath,
75    is_prefixed: &impl Fn(&Path) -> bool,
76    active_prefixed_dirs: &HashSet<&Path>,
77    excludes: &HashSet<&AbsolutePath>,
78    res: &mut Vec<Item>,
79) -> Result<(), Error> {
80    for entry in root.read_dir()? {
81        let entry = entry?;
82        let path = AbsolutePath::from(entry.path());
83
84        let entry_name = entry.file_name();
85        let entry_name = Path::new(&entry_name);
86
87        let excluded = excludes.contains(&path);
88        if util::is_hidden(entry_name.as_os_str()) || excluded {
89            if excluded {
90                verbose_println!("Excluded {}", path);
91            }
92            continue;
93        }
94
95        if is_prefixed(&entry_name) {
96            if active_prefixed_dirs.contains(entry_name) {
97                find_items(path, is_prefixed, active_prefixed_dirs, excludes, res)?;
98            }
99        } else {
100            let contents = link_dir_contents(&AbsolutePath::from(entry.path()), excludes)?;
101            res.extend(contents);
102        }
103    }
104
105    Ok(())
106}
107
108pub fn get(config: &Config) -> Result<Vec<Item>, Error> {
109    let hostname_prefix = "host-";
110    let tag_prefix = "tag-";
111    let platform_prefix = "platform-";
112    let prefixes = [hostname_prefix, tag_prefix, platform_prefix];
113
114    // Checks if a path is prefixed by any element of `prefixes`
115    // If the path cannot be read as a String, assume it isn't.
116    let is_prefixed = |filename: &Path| -> bool {
117        for prefix in &prefixes {
118            match filename.to_str() {
119                Some(s) if s.starts_with(prefix) => return true,
120                _ => continue,
121            }
122        }
123
124        false
125    };
126
127    let hostname_dir = PathBuf::from([hostname_prefix, config.hostname()].concat());
128
129    let platform_dirs: Vec<PathBuf> = config
130        .platform()
131        .strs()
132        .iter()
133        .map(|platform| PathBuf::from([platform_prefix, platform].concat()))
134        .collect();
135
136    let tag_dirs: Vec<PathBuf> = config
137        .tags()
138        .iter()
139        .map(|tag| PathBuf::from([tag_prefix, tag].concat()))
140        .collect();
141
142    let active_prefixed_dirs: HashSet<&Path> = iter::once(&hostname_dir)
143        .chain(tag_dirs.iter())
144        .chain(platform_dirs.iter())
145        .map(|p| p.as_path())
146        .collect();
147
148    let excludes = config.excludes().iter().collect();
149
150    let mut res = vec![];
151
152    find_items(
153        config.dotfiles_path().clone(),
154        &is_prefixed,
155        &active_prefixed_dirs,
156        &excludes,
157        &mut res,
158    )?;
159
160    // Check for duplicate destinations
161    let mut seen = HashSet::new();
162    for item in &res {
163        let dest = item.dest();
164        if seen.contains(dest) {
165            return Err(DuplicateFiles { dest: dest.clone() });
166        } else {
167            seen.insert(dest);
168        }
169    }
170
171    Ok(res)
172}
173
174#[derive(Debug, From, Fail)]
175pub enum Error {
176    /// Indicates when there are multiple active sources pointing to the same
177    /// destination.
178    #[fail(display = "multiple source files for destination {}", dest)]
179    DuplicateFiles { dest: AbsolutePath },
180
181    #[fail(display = "error reading from dotfiles directory ({})", _0)]
182    IoError(#[fail(cause)] io::Error),
183
184    #[fail(display = "error reading from dotfiles directory ({})", _0)]
185    WalkdirError(#[fail(cause)] walkdir::Error),
186}
187use Error::*;