1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
use nu_errors::ShellError;
use std::path::{Path, PathBuf};

static LOCAL_CFG_FILE_NAME: &str = ".nu-env";

pub struct LocalConfigDiff {
    pub cfgs_to_load: Vec<PathBuf>,
    pub cfgs_to_unload: Vec<PathBuf>,
}

/// Finds all local configs between `from` up to `to`.
/// Every config seen while going up the filesystem (e.G. from `/foo` to `/foo/bar`) is returned
/// as a config to load
/// Every config seen while going down the filesystem (e.G. from `/foo/bar` to `/foo/bar`) is
/// returned as a config to unload
/// If both paths are unrelated to each other, (e.G. windows paths as: `C:/foo` and `D:/bar`)
/// this function first walks `from` completely down the filesystem and then it walks up until `to`.
///
/// Both paths are required to be absolute.
impl LocalConfigDiff {
    pub fn between(from: PathBuf, to: PathBuf) -> (LocalConfigDiff, Vec<ShellError>) {
        let common_prefix = common_path::common_path(&from, &to);
        let (cfgs_to_unload, err_down) = walk_down(&from, &common_prefix);
        let (cfgs_to_load, err_up) = walk_up(&common_prefix, &to);

        (
            LocalConfigDiff {
                cfgs_to_load,
                cfgs_to_unload,
            },
            err_down.into_iter().chain(err_up).collect(),
        )
    }
}

///Walks from the first parameter down the filesystem to the second parameter. Marking all
///configs found in directories on the way as to remove.
///If to is None, this method walks from the first parameter down to the beginning of the
///filesystem
///Returns tuple of (configs to remove, errors from io).
fn walk_down(
    from_inclusive: &Path,
    to_exclusive: &Option<PathBuf>,
) -> (Vec<PathBuf>, Vec<ShellError>) {
    let mut all_err = vec![];
    let mut all_cfgs_to_unload = vec![];
    for dir in from_inclusive.ancestors().take_while(|cur_path| {
        if let Some(until_path) = to_exclusive {
            //Stop before `to_exclusive`
            *cur_path != until_path
        } else {
            //No end, walk all the way down
            true
        }
    }) {
        match local_cfg_should_be_unloaded(dir.to_path_buf()) {
            Ok(Some(cfg)) => all_cfgs_to_unload.push(cfg),
            Err(e) => all_err.push(e),
            _ => {}
        }
    }

    (all_cfgs_to_unload, all_err)
}

///Walks from the first parameter up the filesystem to the second parameter, returns all configs
///found in directories on the way to load.
///Returns combined errors from checking directories on the way
///If from is None, this method walks from the beginning of the second parameter up to the
///second parameter
fn walk_up(
    from_exclusive: &Option<PathBuf>,
    to_inclusive: &Path,
) -> (Vec<PathBuf>, Vec<ShellError>) {
    let mut all_err = vec![];
    let mut all_cfgs_to_load = vec![];

    //skip all paths until (inclusive) from (or 0 if from is None)
    let skip_ahead = from_exclusive
        .as_ref()
        .map(|p| p.ancestors().count())
        .unwrap_or(0);
    //We have to traverse ancestors in reverse order (apply lower directories first)
    //ancestors() does not yield iter with .rev() method. So we store all ancestors
    //and then iterate over the vec
    let dirs: Vec<_> = to_inclusive.ancestors().map(Path::to_path_buf).collect();
    for dir in dirs.iter().rev().skip(skip_ahead) {
        match loadable_cfg_exists_in_dir(dir.clone()) {
            Ok(Some(cfg)) => all_cfgs_to_load.push(cfg),
            Err(e) => all_err.push(e),
            _ => {}
        }
    }

    (all_cfgs_to_load, all_err)
}

fn is_existent_local_cfg(cfg_file_path: &Path) -> Result<bool, ShellError> {
    if !cfg_file_path.exists() || cfg_file_path.parent() == super::default_path()?.parent() {
        //Don't treat global cfg as local one
        Ok(false)
    } else {
        Ok(true)
    }
}

fn is_trusted_local_cfg_content(cfg_file_path: &Path, content: &[u8]) -> Result<bool, ShellError> {
    //This checks whether user used `autoenv trust` to mark this cfg as secure
    if !super::is_file_trusted(cfg_file_path, content)? {
        //Notify user about present config, but not trusted
        Err(ShellError::untagged_runtime_error(
                format!("{:?} is untrusted. Run 'autoenv trust {:?}' to trust it.\nThis needs to be done after each change to the file.",
                    cfg_file_path, cfg_file_path.parent().unwrap_or_else(|| Path::new("")))))
    } else {
        Ok(true)
    }
}

fn local_cfg_should_be_unloaded<P: AsRef<Path>>(cfg_dir: P) -> Result<Option<PathBuf>, ShellError> {
    let mut cfg = cfg_dir.as_ref().to_path_buf();
    cfg.push(LOCAL_CFG_FILE_NAME);
    if is_existent_local_cfg(&cfg)? {
        //No need to compute whether content is good. If it is not loaded before, unloading does
        //nothing
        Ok(Some(cfg))
    } else {
        Ok(None)
    }
}

/// Checks whether a local_cfg exists in cfg_dir and returns:
/// Ok(Some(cfg_path)) if cfg exists and is good to load
/// Ok(None) if no cfg exists
/// Err(error) if cfg exits, but is not good to load
pub fn loadable_cfg_exists_in_dir(mut cfg_dir: PathBuf) -> Result<Option<PathBuf>, ShellError> {
    cfg_dir.push(LOCAL_CFG_FILE_NAME);
    let cfg_path = cfg_dir;

    if !is_existent_local_cfg(&cfg_path)? {
        return Ok(None);
    }

    let content = std::fs::read(&cfg_path)?;

    if !is_trusted_local_cfg_content(&cfg_path, &content)? {
        return Ok(None);
    }

    Ok(Some(cfg_path))
}