nu_data/config/
local_config.rs

1use nu_errors::ShellError;
2use std::path::{Path, PathBuf};
3
4static LOCAL_CFG_FILE_NAME: &str = ".nu-env";
5
6pub struct LocalConfigDiff {
7    pub cfgs_to_load: Vec<PathBuf>,
8    pub cfgs_to_unload: Vec<PathBuf>,
9}
10
11/// Finds all local configs between `from` up to `to`.
12/// Every config seen while going up the filesystem (e.G. from `/foo` to `/foo/bar`) is returned
13/// as a config to load
14/// Every config seen while going down the filesystem (e.G. from `/foo/bar` to `/foo/bar`) is
15/// returned as a config to unload
16/// If both paths are unrelated to each other, (e.G. windows paths as: `C:/foo` and `D:/bar`)
17/// this function first walks `from` completely down the filesystem and then it walks up until `to`.
18///
19/// Both paths are required to be absolute.
20impl LocalConfigDiff {
21    pub fn between(from: PathBuf, to: PathBuf) -> (LocalConfigDiff, Vec<ShellError>) {
22        let common_prefix = common_path::common_path(&from, &to);
23        let (cfgs_to_unload, err_down) = walk_down(&from, &common_prefix);
24        let (cfgs_to_load, err_up) = walk_up(&common_prefix, &to);
25
26        (
27            LocalConfigDiff {
28                cfgs_to_load,
29                cfgs_to_unload,
30            },
31            err_down.into_iter().chain(err_up).collect(),
32        )
33    }
34}
35
36///Walks from the first parameter down the filesystem to the second parameter. Marking all
37///configs found in directories on the way as to remove.
38///If to is None, this method walks from the first parameter down to the beginning of the
39///filesystem
40///Returns tuple of (configs to remove, errors from io).
41fn walk_down(
42    from_inclusive: &Path,
43    to_exclusive: &Option<PathBuf>,
44) -> (Vec<PathBuf>, Vec<ShellError>) {
45    let mut all_err = vec![];
46    let mut all_cfgs_to_unload = vec![];
47    for dir in from_inclusive.ancestors().take_while(|cur_path| {
48        if let Some(until_path) = to_exclusive {
49            //Stop before `to_exclusive`
50            *cur_path != until_path
51        } else {
52            //No end, walk all the way down
53            true
54        }
55    }) {
56        match local_cfg_should_be_unloaded(dir.to_path_buf()) {
57            Ok(Some(cfg)) => all_cfgs_to_unload.push(cfg),
58            Err(e) => all_err.push(e),
59            _ => {}
60        }
61    }
62
63    (all_cfgs_to_unload, all_err)
64}
65
66///Walks from the first parameter up the filesystem to the second parameter, returns all configs
67///found in directories on the way to load.
68///Returns combined errors from checking directories on the way
69///If from is None, this method walks from the beginning of the second parameter up to the
70///second parameter
71fn walk_up(
72    from_exclusive: &Option<PathBuf>,
73    to_inclusive: &Path,
74) -> (Vec<PathBuf>, Vec<ShellError>) {
75    let mut all_err = vec![];
76    let mut all_cfgs_to_load = vec![];
77
78    //skip all paths until (inclusive) from (or 0 if from is None)
79    let skip_ahead = from_exclusive
80        .as_ref()
81        .map(|p| p.ancestors().count())
82        .unwrap_or(0);
83    //We have to traverse ancestors in reverse order (apply lower directories first)
84    //ancestors() does not yield iter with .rev() method. So we store all ancestors
85    //and then iterate over the vec
86    let dirs: Vec<_> = to_inclusive.ancestors().map(Path::to_path_buf).collect();
87    for dir in dirs.iter().rev().skip(skip_ahead) {
88        match loadable_cfg_exists_in_dir(dir.clone()) {
89            Ok(Some(cfg)) => all_cfgs_to_load.push(cfg),
90            Err(e) => all_err.push(e),
91            _ => {}
92        }
93    }
94
95    (all_cfgs_to_load, all_err)
96}
97
98fn is_existent_local_cfg(cfg_file_path: &Path) -> Result<bool, ShellError> {
99    if !cfg_file_path.exists() || cfg_file_path.parent() == super::default_path()?.parent() {
100        //Don't treat global cfg as local one
101        Ok(false)
102    } else {
103        Ok(true)
104    }
105}
106
107fn is_trusted_local_cfg_content(cfg_file_path: &Path, content: &[u8]) -> Result<bool, ShellError> {
108    //This checks whether user used `autoenv trust` to mark this cfg as secure
109    if !super::is_file_trusted(cfg_file_path, content)? {
110        //Notify user about present config, but not trusted
111        Err(ShellError::untagged_runtime_error(
112                format!("{:?} is untrusted. Run 'autoenv trust {:?}' to trust it.\nThis needs to be done after each change to the file.",
113                    cfg_file_path, cfg_file_path.parent().unwrap_or_else(|| Path::new("")))))
114    } else {
115        Ok(true)
116    }
117}
118
119fn local_cfg_should_be_unloaded<P: AsRef<Path>>(cfg_dir: P) -> Result<Option<PathBuf>, ShellError> {
120    let mut cfg = cfg_dir.as_ref().to_path_buf();
121    cfg.push(LOCAL_CFG_FILE_NAME);
122    if is_existent_local_cfg(&cfg)? {
123        //No need to compute whether content is good. If it is not loaded before, unloading does
124        //nothing
125        Ok(Some(cfg))
126    } else {
127        Ok(None)
128    }
129}
130
131/// Checks whether a local_cfg exists in cfg_dir and returns:
132/// Ok(Some(cfg_path)) if cfg exists and is good to load
133/// Ok(None) if no cfg exists
134/// Err(error) if cfg exits, but is not good to load
135pub fn loadable_cfg_exists_in_dir(mut cfg_dir: PathBuf) -> Result<Option<PathBuf>, ShellError> {
136    cfg_dir.push(LOCAL_CFG_FILE_NAME);
137    let cfg_path = cfg_dir;
138
139    if !is_existent_local_cfg(&cfg_path)? {
140        return Ok(None);
141    }
142
143    let content = std::fs::read(&cfg_path)?;
144
145    if !is_trusted_local_cfg_content(&cfg_path, &content)? {
146        return Ok(None);
147    }
148
149    Ok(Some(cfg_path))
150}