diff --git a/Cargo.lock b/Cargo.lock index cbf6c20951..b2fc520da2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -322,7 +322,16 @@ dependencies = [ "block-padding", "byte-tools", "byteorder", - "generic-array", + "generic-array 0.12.3", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array 0.14.2", ] [[package]] @@ -634,6 +643,12 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" +[[package]] +name = "cpuid-bool" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d375c433320f6c5057ae04a04376eef4d04ce2801448cf8863a78da99107be4" + [[package]] name = "crc32fast" version = "1.2.0" @@ -866,7 +881,16 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" dependencies = [ - "generic-array", + "generic-array 0.12.3", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.2", ] [[package]] @@ -1418,6 +1442,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "generic-array" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac746a5f3bbfdadd6106868134545e684693d54d9d44f6e9588a7d54af0bf980" +dependencies = [ + "typenum", + "version_check 0.9.2", +] + [[package]] name = "gethostname" version = "0.2.1" @@ -2517,6 +2551,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "serde_yaml", + "sha2", "shellexpand", "starship", "strip-ansi-escapes", @@ -2969,6 +3004,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "open" version = "1.4.0" @@ -3892,10 +3933,23 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" dependencies = [ - "block-buffer", - "digest", + "block-buffer 0.7.3", + "digest 0.8.1", "fake-simd", - "opaque-debug", + "opaque-debug 0.2.3", +] + +[[package]] +name = "sha2" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2933378ddfeda7ea26f48c555bdad8bb446bf8a3d17832dc83e380d444cfb8c1" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpuid-bool", + "digest 0.9.0", + "opaque-debug 0.3.0", ] [[package]] diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index de1625e660..2d27f6c95a 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -75,6 +75,7 @@ serde_ini = "0.2.0" serde_json = "1.0.55" serde_urlencoded = "0.6.1" serde_yaml = "0.8" +sha2 = "0.9.1" shellexpand = "2.0.0" strip-ansi-escapes = "0.1.0" tempfile = "3.1.0" diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index 00a4013ff0..4b2e236a3e 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -352,6 +352,9 @@ pub fn create_default_context( whole_stream_command(Headers), // Data processing whole_stream_command(Histogram), + whole_stream_command(Autoenv), + whole_stream_command(AutoenvTrust), + whole_stream_command(AutoenvUnTrust), whole_stream_command(Math), whole_stream_command(MathAverage), whole_stream_command(MathMedian), diff --git a/crates/nu-cli/src/commands.rs b/crates/nu-cli/src/commands.rs index d106f23171..56fc47d540 100644 --- a/crates/nu-cli/src/commands.rs +++ b/crates/nu-cli/src/commands.rs @@ -8,6 +8,9 @@ pub(crate) mod alias; pub(crate) mod ansi; pub(crate) mod append; pub(crate) mod args; +pub(crate) mod autoenv; +pub(crate) mod autoenv_trust; +pub(crate) mod autoenv_untrust; pub(crate) mod autoview; pub(crate) mod build_string; pub(crate) mod cal; @@ -142,6 +145,9 @@ pub(crate) use command::{ pub(crate) use alias::Alias; pub(crate) use ansi::Ansi; pub(crate) use append::Append; +pub(crate) use autoenv::Autoenv; +pub(crate) use autoenv_trust::AutoenvTrust; +pub(crate) use autoenv_untrust::AutoenvUnTrust; pub(crate) use build_string::BuildString; pub(crate) use cal::Cal; pub(crate) use calc::Calc; diff --git a/crates/nu-cli/src/commands/autoenv.rs b/crates/nu-cli/src/commands/autoenv.rs new file mode 100644 index 0000000000..c397fd0ee7 --- /dev/null +++ b/crates/nu-cli/src/commands/autoenv.rs @@ -0,0 +1,82 @@ +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{ReturnSuccess, Signature, UntaggedValue}; +use serde::Deserialize; +use serde::Serialize; +use sha2::{Digest, Sha256}; +use std::io::Read; +use std::path::PathBuf; +pub struct Autoenv; + +#[derive(Deserialize, Serialize, Debug, Default)] +pub struct Trusted { + pub files: IndexMap>, +} +impl Trusted { + pub fn new() -> Self { + Trusted { + files: IndexMap::new(), + } + } +} +pub fn file_is_trusted(nu_env_file: &PathBuf, content: &[u8]) -> Result { + let contentdigest = Sha256::digest(&content).as_slice().to_vec(); + let nufile = nu_env_file.to_str().unwrap_or(""); + + let trusted = read_trusted()?; + Ok(trusted.files.get(nufile) == Some(&contentdigest)) +} + +pub fn read_trusted() -> Result { + let config_path = config::default_path_for(&Some(PathBuf::from("nu-env.toml")))?; + + let mut file = std::fs::OpenOptions::new() + .read(true) + .create(true) + .write(true) + .open(config_path) + .or_else(|_| { + Err(ShellError::untagged_runtime_error( + "Couldn't open nu-env.toml", + )) + })?; + let mut doc = String::new(); + file.read_to_string(&mut doc)?; + + let allowed = toml::de::from_str(doc.as_str()).unwrap_or_else(|_| Trusted::new()); + Ok(allowed) +} + +#[async_trait] +impl WholeStreamCommand for Autoenv { + fn name(&self) -> &str { + "autoenv" + } + fn usage(&self) -> &str { + // "Mark a .nu-env file in a directory as trusted. Needs to be re-run after each change to the file or its filepath." + "Manage directory specific environments" + } + fn signature(&self) -> Signature { + Signature::build("autoenv") + } + async fn run( + &self, + _args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + let registry = registry.clone(); + Ok(OutputStream::one(ReturnSuccess::value( + UntaggedValue::string(crate::commands::help::get_help(&Autoenv, ®istry)) + .into_value(Tag::unknown()), + ))) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Allow .nu-env file in current directory", + example: "autoenv trust", + result: None, + }] + } +} diff --git a/crates/nu-cli/src/commands/autoenv_trust.rs b/crates/nu-cli/src/commands/autoenv_trust.rs new file mode 100644 index 0000000000..d7276aba58 --- /dev/null +++ b/crates/nu-cli/src/commands/autoenv_trust.rs @@ -0,0 +1,74 @@ +use super::autoenv::read_trusted; +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::SyntaxShape; +use nu_protocol::{Primitive, ReturnSuccess, Signature, UntaggedValue, Value}; +use sha2::{Digest, Sha256}; +use std::{fs, path::PathBuf}; +pub struct AutoenvTrust; + +#[async_trait] +impl WholeStreamCommand for AutoenvTrust { + fn name(&self) -> &str { + "autoenv trust" + } + + fn signature(&self) -> Signature { + Signature::build("autoenv trust").optional("dir", SyntaxShape::String, "Directory to allow") + } + + fn usage(&self) -> &str { + "Trust a .nu-env file in the current or given directory" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + let tag = args.call_info.name_tag.clone(); + + let file_to_trust = match args.call_info.evaluate(registry).await?.args.nth(0) { + Some(Value { + value: UntaggedValue::Primitive(Primitive::String(ref path)), + tag: _, + }) => { + let mut dir = fs::canonicalize(path)?; + dir.push(".nu-env"); + dir + } + _ => { + let mut dir = std::env::current_dir()?; + dir.push(".nu-env"); + dir + } + }; + + let content = std::fs::read(&file_to_trust)?; + + let filename = file_to_trust.to_string_lossy().to_string(); + let mut allowed = read_trusted()?; + allowed + .files + .insert(filename, Sha256::digest(&content).as_slice().to_vec()); + + let config_path = config::default_path_for(&Some(PathBuf::from("nu-env.toml")))?; + let tomlstr = toml::to_string(&allowed).or_else(|_| { + Err(ShellError::untagged_runtime_error( + "Couldn't serialize allowed dirs to nu-env.toml", + )) + })?; + fs::write(config_path, tomlstr).expect("Couldn't write to toml file"); + + Ok(OutputStream::one(ReturnSuccess::value( + UntaggedValue::string(".nu-env trusted!").into_value(tag), + ))) + } + fn is_binary(&self) -> bool { + false + } + fn examples(&self) -> Vec { + Vec::new() + } +} diff --git a/crates/nu-cli/src/commands/autoenv_untrust.rs b/crates/nu-cli/src/commands/autoenv_untrust.rs new file mode 100644 index 0000000000..7dec2d67dc --- /dev/null +++ b/crates/nu-cli/src/commands/autoenv_untrust.rs @@ -0,0 +1,98 @@ +use super::autoenv::Trusted; +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::SyntaxShape; +use nu_protocol::{Primitive, ReturnSuccess, Signature, UntaggedValue, Value}; +use std::io::Read; +use std::{fs, path::PathBuf}; +pub struct AutoenvUnTrust; + +#[async_trait] +impl WholeStreamCommand for AutoenvUnTrust { + fn name(&self) -> &str { + "autoenv untrust" + } + + fn signature(&self) -> Signature { + Signature::build("autoenv untrust").optional( + "dir", + SyntaxShape::String, + "Directory to disallow", + ) + } + + fn usage(&self) -> &str { + "Untrust a .nu-env file in the current or given directory" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + let tag = args.call_info.name_tag.clone(); + let file_to_untrust = match args.call_info.evaluate(registry).await?.args.nth(0) { + Some(Value { + value: UntaggedValue::Primitive(Primitive::String(ref path)), + tag: _, + }) => { + let mut dir = fs::canonicalize(path)?; + dir.push(".nu-env"); + dir + } + _ => { + let mut dir = std::env::current_dir()?; + dir.push(".nu-env"); + dir + } + }; + + let config_path = config::default_path_for(&Some(PathBuf::from("nu-env.toml")))?; + + let mut file = match std::fs::OpenOptions::new() + .read(true) + .create(true) + .write(true) + .open(config_path.clone()) + { + Ok(p) => p, + Err(_) => { + return Err(ShellError::untagged_runtime_error( + "Couldn't open nu-env.toml", + )); + } + }; + + let mut doc = String::new(); + file.read_to_string(&mut doc)?; + + let mut allowed: Trusted = toml::from_str(doc.as_str()).unwrap_or_else(|_| Trusted::new()); + + let file_to_untrust = file_to_untrust.to_string_lossy().to_string(); + + if allowed.files.remove(&file_to_untrust).is_none() { + return + Err(ShellError::untagged_runtime_error( + "No .nu-env file to untrust in the given directory. Is it missing, or already untrusted?", + )); + } + + let tomlstr = toml::to_string(&allowed).or_else(|_| { + Err(ShellError::untagged_runtime_error( + "Couldn't serialize allowed dirs to nu-env.toml", + )) + })?; + fs::write(config_path, tomlstr).expect("Couldn't write to toml file"); + + Ok(OutputStream::one(ReturnSuccess::value( + UntaggedValue::string(".nu-env untrusted!").into_value(tag), + ))) + } + fn is_binary(&self) -> bool { + false + } + fn examples(&self) -> Vec { + Vec::new() + } +} diff --git a/crates/nu-cli/src/commands/classified/internal.rs b/crates/nu-cli/src/commands/classified/internal.rs index c76d3fd7af..4fdf56aa70 100644 --- a/crates/nu-cli/src/commands/classified/internal.rs +++ b/crates/nu-cli/src/commands/classified/internal.rs @@ -28,6 +28,10 @@ pub(crate) async fn run_internal_command( let objects: InputStream = trace_stream!(target: "nu::trace_stream::internal", "input" = input); let internal_command = context.expect_command(&command.name); + if command.name == "autoenv untrust" { + context.user_recently_used_autoenv_untrust = true; + } + let result = { context .run_command( diff --git a/crates/nu-cli/src/commands/run_external.rs b/crates/nu-cli/src/commands/run_external.rs index e5326228bb..135e9a1ebe 100644 --- a/crates/nu-cli/src/commands/run_external.rs +++ b/crates/nu-cli/src/commands/run_external.rs @@ -75,6 +75,7 @@ impl WholeStreamCommand for RunExternalCommand { Context { registry: registry.clone(), host: args.host.clone(), + user_recently_used_autoenv_untrust: false, shell_manager: args.shell_manager.clone(), ctrl_c: args.ctrl_c.clone(), current_errors: Arc::new(Mutex::new(vec![])), @@ -88,6 +89,7 @@ impl WholeStreamCommand for RunExternalCommand { { Context { registry: registry.clone(), + user_recently_used_autoenv_untrust: false, host: args.host.clone(), shell_manager: args.shell_manager.clone(), ctrl_c: args.ctrl_c.clone(), diff --git a/crates/nu-cli/src/context.rs b/crates/nu-cli/src/context.rs index a643ae32c4..2c2c95b75e 100644 --- a/crates/nu-cli/src/context.rs +++ b/crates/nu-cli/src/context.rs @@ -76,6 +76,7 @@ pub struct Context { pub current_errors: Arc>>, pub ctrl_c: Arc, pub raw_input: String, + pub user_recently_used_autoenv_untrust: bool, pub(crate) shell_manager: ShellManager, #[cfg(windows)] @@ -96,6 +97,7 @@ impl Context { current_errors: raw_args.current_errors.clone(), ctrl_c: raw_args.ctrl_c.clone(), shell_manager: raw_args.shell_manager.clone(), + user_recently_used_autoenv_untrust: false, windows_drives_previous_cwd: Arc::new(Mutex::new(std::collections::HashMap::new())), raw_input: String::default(), } @@ -108,6 +110,7 @@ impl Context { current_errors: raw_args.current_errors.clone(), ctrl_c: raw_args.ctrl_c.clone(), shell_manager: raw_args.shell_manager.clone(), + user_recently_used_autoenv_untrust: false, raw_input: String::default(), } } @@ -122,6 +125,7 @@ impl Context { current_errors: args.current_errors.clone(), ctrl_c: args.ctrl_c.clone(), shell_manager: args.shell_manager.clone(), + user_recently_used_autoenv_untrust: false, windows_drives_previous_cwd: Arc::new(Mutex::new(std::collections::HashMap::new())), raw_input: String::default(), } @@ -133,6 +137,7 @@ impl Context { host: args.host.clone(), current_errors: args.current_errors.clone(), ctrl_c: args.ctrl_c.clone(), + user_recently_used_autoenv_untrust: false, shell_manager: args.shell_manager.clone(), raw_input: String::default(), } @@ -151,6 +156,7 @@ impl Context { ))), current_errors: Arc::new(Mutex::new(vec![])), ctrl_c: Arc::new(AtomicBool::new(false)), + user_recently_used_autoenv_untrust: false, shell_manager: ShellManager::basic(registry)?, windows_drives_previous_cwd: Arc::new(Mutex::new(std::collections::HashMap::new())), raw_input: String::default(), @@ -166,6 +172,7 @@ impl Context { ))), current_errors: Arc::new(Mutex::new(vec![])), ctrl_c: Arc::new(AtomicBool::new(false)), + user_recently_used_autoenv_untrust: false, shell_manager: ShellManager::basic(registry)?, raw_input: String::default(), }) diff --git a/crates/nu-cli/src/data/config/conf.rs b/crates/nu-cli/src/data/config/conf.rs index 1f53f0c7a2..1805769339 100644 --- a/crates/nu-cli/src/data/config/conf.rs +++ b/crates/nu-cli/src/data/config/conf.rs @@ -4,7 +4,6 @@ use std::fmt::Debug; pub trait Conf: Debug + Send { fn env(&self) -> Option; fn path(&self) -> Option; - fn nu_env_dirs(&self) -> Option; fn reload(&self); } @@ -13,10 +12,6 @@ impl Conf for Box { (**self).env() } - fn nu_env_dirs(&self) -> Option { - (**self).nu_env_dirs() - } - fn path(&self) -> Option { (**self).path() } diff --git a/crates/nu-cli/src/data/config/nuconfig.rs b/crates/nu-cli/src/data/config/nuconfig.rs index a313fc020f..88c7d424fd 100644 --- a/crates/nu-cli/src/data/config/nuconfig.rs +++ b/crates/nu-cli/src/data/config/nuconfig.rs @@ -20,10 +20,6 @@ impl Conf for NuConfig { self.path() } - fn nu_env_dirs(&self) -> Option { - self.nu_env_dirs() - } - fn reload(&self) { let mut vars = self.vars.lock(); @@ -56,14 +52,6 @@ impl NuConfig { None } - pub fn nu_env_dirs(&self) -> Option { - let vars = self.vars.lock(); - if let Some(dirs) = vars.get("nu_env_dirs") { - return Some(dirs.clone()); - } - None - } - pub fn path(&self) -> Option { let vars = self.vars.lock(); diff --git a/crates/nu-cli/src/data/config/tests.rs b/crates/nu-cli/src/data/config/tests.rs index ea8712fe0f..ecbe0cb7f7 100644 --- a/crates/nu-cli/src/data/config/tests.rs +++ b/crates/nu-cli/src/data/config/tests.rs @@ -16,10 +16,6 @@ impl Conf for FakeConfig { self.config.env() } - fn nu_env_dirs(&self) -> Option { - None - } - fn path(&self) -> Option { self.config.path() } diff --git a/crates/nu-cli/src/env/directory_specific_environment.rs b/crates/nu-cli/src/env/directory_specific_environment.rs index 3ffa405cf3..1f0f3662c5 100644 --- a/crates/nu-cli/src/env/directory_specific_environment.rs +++ b/crates/nu-cli/src/env/directory_specific_environment.rs @@ -1,192 +1,238 @@ -use indexmap::IndexMap; -use nu_protocol::{Primitive, UntaggedValue, Value}; -use std::io::{Error, ErrorKind, Result}; -use std::{ffi::OsString, fmt::Debug, path::PathBuf}; +use crate::commands; +use commands::autoenv; +use indexmap::{IndexMap, IndexSet}; +use nu_errors::ShellError; +use serde::Deserialize; +use std::cmp::Ordering::Less; +use std::env::*; +use std::process::Command; +use std::{ + ffi::OsString, + fmt::Debug, + path::{Path, PathBuf}, +}; + +type EnvKey = String; +type EnvVal = OsString; #[derive(Debug, Default)] pub struct DirectorySpecificEnvironment { - allowed_directories: Vec, + pub last_seen_directory: PathBuf, + //If an environment var has been added from a .nu in a directory, we track it here so we can remove it when the user leaves the directory. + //If setting the var overwrote some value, we save the old value in an option so we can restore it later. + added_env_vars: IndexMap>>, + exitscripts: IndexMap>, +} - //Directory -> Env key. If an environment var has been added from a .nu in a directory, we track it here so we can remove it when the user leaves the directory. - added_env_vars: IndexMap>, - - //Directory -> (env_key, value). If a .nu overwrites some existing environment variables, they are added here so that they can be restored later. - overwritten_env_values: IndexMap>, +#[derive(Deserialize, Debug, Default)] +pub struct NuEnvDoc { + pub env: Option>, + pub scriptvars: Option>, + pub scripts: Option>>, + pub entryscripts: Option>, + pub exitscripts: Option>, } impl DirectorySpecificEnvironment { - pub fn new(allowed_directories: Option) -> DirectorySpecificEnvironment { - let mut allowed_directories = if let Some(Value { - value: UntaggedValue::Table(ref wrapped_directories), - .. - }) = allowed_directories - { - wrapped_directories - .iter() - .filter_map(|dirval| { - if let Value { - value: UntaggedValue::Primitive(Primitive::String(ref dir)), - .. - } = dirval - { - return Some(PathBuf::from(&dir)); - } - None - }) - .collect() + pub fn new() -> DirectorySpecificEnvironment { + let root_dir = if cfg!(target_os = "windows") { + PathBuf::from("c:\\") } else { - vec![] + PathBuf::from("/") }; - allowed_directories.sort(); - DirectorySpecificEnvironment { - allowed_directories, + last_seen_directory: root_dir, added_env_vars: IndexMap::new(), - overwritten_env_values: IndexMap::new(), + exitscripts: IndexMap::new(), } } - //If we are no longer in a directory, we restore the values it overwrote. - pub fn overwritten_values_to_restore(&mut self) -> Result> { - let current_dir = std::env::current_dir()?; + fn toml_if_directory_is_trusted( + &mut self, + nu_env_file: &PathBuf, + ) -> Result { + let content = std::fs::read(&nu_env_file)?; - let mut keyvals_to_restore = IndexMap::new(); - let mut new_overwritten = IndexMap::new(); + if autoenv::file_is_trusted(&nu_env_file, &content)? { + let mut doc: NuEnvDoc = toml::de::from_slice(&content) + .or_else(|e| Err(ShellError::untagged_runtime_error(format!("{:?}", e))))?; - for (directory, keyvals) in &self.overwritten_env_values { - let mut working_dir = Some(current_dir.as_path()); - - let mut re_add_keyvals = true; - while let Some(wdir) = working_dir { - if wdir == directory.as_path() { - re_add_keyvals = false; - new_overwritten.insert(directory.clone(), keyvals.clone()); - break; - } else { - working_dir = working_dir //Keep going up in the directory structure with .parent() - .ok_or_else(|| { - Error::new(ErrorKind::NotFound, "Root directory has no parent") - })? - .parent(); - } - } - if re_add_keyvals { - for (k, v) in keyvals { - keyvals_to_restore.insert( - k.clone(), - v.to_str() - .ok_or_else(|| { - Error::new( - ErrorKind::Other, - format!("{:?} is not valid unicode", v), - ) - })? - .to_string(), - ); - } - } - } - - self.overwritten_env_values = new_overwritten; - Ok(keyvals_to_restore) - } - - pub fn env_vars_to_add(&mut self) -> Result> { - let current_dir = std::env::current_dir()?; - - let mut vars_to_add = IndexMap::new(); - for dir in &self.allowed_directories { - let mut working_dir = Some(current_dir.as_path()); - - //Start in the current directory, then traverse towards the root with working_dir to see if we are in a subdirectory of a valid directory. - while let Some(wdir) = working_dir { - if wdir == dir.as_path() { - let toml_doc = std::fs::read_to_string(wdir.join(".nu-env").as_path())? - .parse::()?; - - let vars_in_current_file = toml_doc - .get("env") - .ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::InvalidData, - "No [env] section in .nu-env", - ) - })? - .as_table() - .ok_or_else(|| { - Error::new(ErrorKind::InvalidData, "Malformed [env] section in .nu-env") - })?; - - let mut keys_in_current_nufile = vec![]; - for (k, v) in vars_in_current_file { - vars_to_add.insert( - k.clone(), - v.as_str() - .ok_or_else(|| { - Error::new( - ErrorKind::InvalidData, - format!("Could not read environment variable: {}", v), - ) - })? - .to_string(), - ); //This is used to add variables to the environment - keys_in_current_nufile.push(k.clone()); //this is used to keep track of which directory added which variables + if let Some(scripts) = doc.scripts.as_ref() { + for (k, v) in scripts { + if k == "entryscripts" { + doc.entryscripts = Some(v.clone()); + } else if k == "exitscripts" { + doc.exitscripts = Some(v.clone()); } - - //If we are about to overwrite any environment variables, we save them first so they can be restored later. - self.overwritten_env_values.insert( - wdir.to_path_buf(), - keys_in_current_nufile - .iter() - .filter_map(|key| { - if let Some(val) = std::env::var_os(key) { - return Some((key.clone(), val)); - } - None - }) - .collect(), - ); - - self.added_env_vars - .insert(wdir.to_path_buf(), keys_in_current_nufile); - break; - } else { - working_dir = working_dir //Keep going up in the directory structure with .parent() - .ok_or_else(|| { - Error::new(ErrorKind::NotFound, "Root directory has no parent") - })? - .parent(); } } + return Ok(doc); + } + 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.", nu_env_file, nu_env_file.parent().unwrap_or_else(|| &Path::new(""))))) + } + + pub fn env_vars_to_add(&mut self) -> Result, ShellError> { + let mut dir = current_dir()?; + let mut vars_to_add: IndexMap = IndexMap::new(); + + //If we are in the last seen directory, do nothing + //If we are in a parent directory to last_seen_directory, just return without applying .nu-env in the parent directory - they were already applied earlier. + //parent.cmp(child) = Less + let mut popped = true; + while self.last_seen_directory.cmp(&dir) == Less && popped { + let nu_env_file = dir.join(".nu-env"); + if nu_env_file.exists() { + let nu_env_doc = self.toml_if_directory_is_trusted(&nu_env_file)?; + + //add regular variables from the [env section] + if let Some(env) = nu_env_doc.env { + for (env_key, env_val) in env { + self.add_key_if_appropriate(&mut vars_to_add, &dir, &env_key, &env_val); + } + } + + //Add variables that need to evaluate scripts to run, from [scriptvars] section + if let Some(scriptvars) = nu_env_doc.scriptvars { + for (env_key, dir_val_script) in scriptvars { + let command = if cfg!(target_os = "windows") { + Command::new("cmd") + .args(&["/C", dir_val_script.as_str()]) + .output()? + } else { + Command::new("sh").arg("-c").arg(&dir_val_script).output()? + }; + if command.stdout.is_empty() { + return Err(ShellError::untagged_runtime_error(format!( + "{:?} in {:?} did not return any output", + dir_val_script, dir + ))); + } + let response = + std::str::from_utf8(&command.stdout[..command.stdout.len() - 1]) + .or_else(|e| { + Err(ShellError::untagged_runtime_error(format!( + "Couldn't parse stdout from command {:?}: {:?}", + command, e + ))) + })?; + self.add_key_if_appropriate( + &mut vars_to_add, + &dir, + &env_key, + &response.to_string(), + ); + } + } + + if let Some(entryscripts) = nu_env_doc.entryscripts { + for script in entryscripts { + if cfg!(target_os = "windows") { + Command::new("cmd") + .args(&["/C", script.as_str()]) + .output()?; + } else { + Command::new("sh").arg("-c").arg(script).output()?; + } + } + } + + if let Some(exitscripts) = nu_env_doc.exitscripts { + self.exitscripts.insert(dir.clone(), exitscripts); + } + } + popped = dir.pop(); } Ok(vars_to_add) } - //If the user has left directories which added env vars through .nu, we clear those vars - pub fn env_vars_to_delete(&mut self) -> Result> { - let current_dir = std::env::current_dir()?; + pub fn add_key_if_appropriate( + &mut self, + vars_to_add: &mut IndexMap, + dir: &PathBuf, + env_key: &str, + env_val: &str, + ) { + //This condition is to make sure variables in parent directories don't overwrite variables set by subdirectories. + if !vars_to_add.contains_key(env_key) { + vars_to_add.insert(env_key.to_string(), OsString::from(env_val)); + self.added_env_vars + .entry(dir.clone()) + .or_insert(IndexMap::new()) + .insert(env_key.to_string(), var_os(env_key)); + } + } - //Gather up all environment variables that should be deleted. - //If we are not in a directory or one of its subdirectories, mark the env_vals it maps to for removal. - let vars_to_delete = self.added_env_vars.iter().fold( - Vec::new(), - |mut vars_to_delete, (directory, env_vars)| { - let mut working_dir = Some(current_dir.as_path()); + pub fn cleanup_after_dir_exit( + &mut self, + ) -> Result>, ShellError> { + let current_dir = current_dir()?; + let mut vars_to_cleanup = IndexMap::new(); - while let Some(wdir) = working_dir { - if wdir == directory { - return vars_to_delete; + //If we are in the same directory as last_seen, or a subdirectory to it, do nothing + //If we are in a subdirectory to last seen, do nothing + //If we are in a parent directory to last seen, exit .nu-envs from last seen to parent and restore old vals + let mut dir = self.last_seen_directory.clone(); + + let mut popped = true; + while current_dir.cmp(&dir) == Less && popped { + if let Some(vars_added_by_this_directory) = self.added_env_vars.get(&dir) { + for (k, v) in vars_added_by_this_directory { + vars_to_cleanup.insert(k.clone(), v.clone()); + } + self.added_env_vars.remove(&dir); + } + + if let Some(scripts) = self.exitscripts.get(&dir) { + for script in scripts { + if cfg!(target_os = "windows") { + Command::new("cmd") + .args(&["/C", script.as_str()]) + .output()?; } else { - working_dir = working_dir.expect("Root directory has no parent").parent(); + Command::new("sh").arg("-c").arg(script).output()?; } } - //only delete vars from directories we are not in - vars_to_delete.extend(env_vars.clone()); - vars_to_delete - }, - ); + } + popped = dir.pop(); + } + Ok(vars_to_cleanup) + } - Ok(vars_to_delete) + // If the user recently ran autoenv untrust on a file, we clear the environment variables it set and make sure to not run any possible exitscripts. + pub fn clear_recently_untrusted_file(&mut self) -> Result<(), ShellError> { + // Figure out which file was untrusted + // Remove all vars set by it + let current_trusted_files: IndexSet = autoenv::read_trusted()? + .files + .iter() + .map(|(k, _)| PathBuf::from(k)) + .collect(); + + // We figure out which file(s) the user untrusted by taking the set difference of current trusted files in .config/nu/nu-env.toml and the files tracked by self.added_env_vars + // If a file is in self.added_env_vars but not in nu-env.toml, it was just untrusted. + let untrusted_files: IndexSet = self + .added_env_vars + .iter() + .filter_map(|(path, _)| { + if !current_trusted_files.contains(path) { + return Some(path.clone()); + } + None + }) + .collect(); + + for path in untrusted_files { + if let Some(added_keys) = self.added_env_vars.get(&path) { + for (key, _) in added_keys { + remove_var(key); + } + } + self.exitscripts.remove(&path); + self.added_env_vars.remove(&path); + } + + Ok(()) } } diff --git a/crates/nu-cli/src/env/environment.rs b/crates/nu-cli/src/env/environment.rs index abb276d985..00c07932ab 100644 --- a/crates/nu-cli/src/env/environment.rs +++ b/crates/nu-cli/src/env/environment.rs @@ -1,15 +1,18 @@ use crate::data::config::Conf; use crate::env::directory_specific_environment::*; use indexmap::{indexmap, IndexSet}; +use nu_errors::ShellError; use nu_protocol::{UntaggedValue, Value}; +use std::env::*; use std::ffi::OsString; + use std::fmt::Debug; pub trait Env: Debug + Send { fn env(&self) -> Option; fn path(&self) -> Option; - fn add_env(&mut self, key: &str, value: &str, overwrite_existing: bool); + fn add_env(&mut self, key: &str, value: &str); fn add_path(&mut self, new_path: OsString); } @@ -22,8 +25,8 @@ impl Env for Box { (**self).path() } - fn add_env(&mut self, key: &str, value: &str, overwrite_existing: bool) { - (**self).add_env(key, value, overwrite_existing); + fn add_env(&mut self, key: &str, value: &str) { + (**self).add_env(key, value); } fn add_path(&mut self, new_path: OsString) { @@ -35,7 +38,7 @@ impl Env for Box { pub struct Environment { environment_vars: Option, path_vars: Option, - pub direnv: DirectorySpecificEnvironment, + pub autoenv: DirectorySpecificEnvironment, } impl Environment { @@ -43,49 +46,41 @@ impl Environment { Environment { environment_vars: None, path_vars: None, - direnv: DirectorySpecificEnvironment::new(None), + autoenv: DirectorySpecificEnvironment::new(), } } pub fn from_config(configuration: &T) -> Environment { let env = configuration.env(); let path = configuration.path(); - Environment { environment_vars: env, path_vars: path, - direnv: DirectorySpecificEnvironment::new(configuration.nu_env_dirs()), + autoenv: DirectorySpecificEnvironment::new(), } } - pub fn maintain_directory_environment(&mut self) -> std::io::Result<()> { - self.direnv.env_vars_to_delete()?.iter().for_each(|k| { - self.remove_env(&k); - }); - self.direnv.env_vars_to_add()?.iter().for_each(|(k, v)| { - self.add_env(&k, &v, true); - }); + pub fn autoenv(&mut self, reload_trusted: bool) -> Result<(), ShellError> { + for (k, v) in self.autoenv.env_vars_to_add()? { + set_var(&k, OsString::from(v.to_string_lossy().to_string())); + } - self.direnv - .overwritten_values_to_restore()? - .iter() - .for_each(|(k, v)| { - self.add_env(&k, &v, true); - }); + for (k, v) in self.autoenv.cleanup_after_dir_exit()? { + if let Some(v) = v { + set_var(k, v); + } else { + remove_var(k); + } + } + if reload_trusted { + self.autoenv.clear_recently_untrusted_file()?; + } + + self.autoenv.last_seen_directory = current_dir()?; Ok(()) } - fn remove_env(&mut self, key: &str) { - if let Some(Value { - value: UntaggedValue::Row(ref mut envs), - .. - }) = self.environment_vars - { - envs.entries.remove(key); - }; - } - pub fn morph(&mut self, configuration: &T) { self.environment_vars = configuration.env(); self.path_vars = configuration.path(); @@ -109,7 +104,7 @@ impl Env for Environment { None } - fn add_env(&mut self, key: &str, value: &str, overwrite_existing: bool) { + fn add_env(&mut self, key: &str, value: &str) { let value = UntaggedValue::string(value); let new_envs = { @@ -120,7 +115,7 @@ impl Env for Environment { { let mut new_envs = envs.clone(); - if !new_envs.contains_key(key) || overwrite_existing { + if !new_envs.contains_key(key) { new_envs.insert_data_at_key(key, value.into_value(tag.clone())); } @@ -146,7 +141,7 @@ impl Env for Environment { { let mut new_paths = current_paths.clone(); - let new_path_candidates = std::env::split_paths(&paths).map(|path| { + let new_path_candidates = split_paths(&paths).map(|path| { UntaggedValue::string(path.to_string_lossy()).into_value(tag.clone()) }); @@ -238,7 +233,7 @@ mod tests { let fake_config = FakeConfig::new(&file); let mut actual = Environment::from_config(&fake_config); - actual.add_env("USER", "NUNO", false); + actual.add_env("USER", "NUNO"); assert_eq!( actual.env(), @@ -271,7 +266,7 @@ mod tests { let fake_config = FakeConfig::new(&file); let mut actual = Environment::from_config(&fake_config); - actual.add_env("SHELL", "/usr/bin/sh", false); + actual.add_env("SHELL", "/usr/bin/sh"); assert_eq!( actual.env(), diff --git a/crates/nu-cli/src/env/environment_syncer.rs b/crates/nu-cli/src/env/environment_syncer.rs index fbcd72e6a6..fc761e130a 100644 --- a/crates/nu-cli/src/env/environment_syncer.rs +++ b/crates/nu-cli/src/env/environment_syncer.rs @@ -1,6 +1,8 @@ use crate::context::Context; use crate::data::config::{Conf, NuConfig}; + use crate::env::environment::{Env, Environment}; +use nu_source::Text; use parking_lot::Mutex; use std::sync::Arc; @@ -44,14 +46,16 @@ impl EnvironmentSyncer { pub fn sync_env_vars(&mut self, ctx: &mut Context) { let mut environment = self.env.lock(); + if let Err(e) = environment.autoenv(ctx.user_recently_used_autoenv_untrust) { + crate::cli::print_err(e, &Text::from("")); + } + ctx.user_recently_used_autoenv_untrust = false; if environment.env().is_some() { for (name, value) in ctx.with_host(|host| host.vars()) { if name != "path" && name != "PATH" { // account for new env vars present in the current session // that aren't loaded from config. - environment.add_env(&name, &value, false); - - environment.maintain_directory_environment().ok(); + environment.add_env(&name, &value); // clear the env var from the session // we are about to replace them @@ -126,6 +130,7 @@ mod tests { use crate::context::Context; use crate::data::config::tests::FakeConfig; use crate::env::environment::Env; + use indexmap::IndexMap; use nu_errors::ShellError; use nu_test_support::fs::Stub::FileWithContent; use nu_test_support::playground::Playground; @@ -139,13 +144,12 @@ mod tests { let mut ctx = Context::basic()?; ctx.host = Arc::new(Mutex::new(Box::new(crate::env::host::FakeHost::new()))); - let expected = vec![ - ( - "SHELL".to_string(), - "/usr/bin/you_already_made_the_nu_choice".to_string(), - ), - ("USER".to_string(), "NUNO".to_string()), - ]; + let mut expected = IndexMap::new(); + expected.insert( + "SHELL".to_string(), + "/usr/bin/you_already_made_the_nu_choice".to_string(), + ); + expected.insert("USER".to_string(), "NUNO".to_string()); Playground::setup("syncs_env_test_1", |dirs, sandbox| { sandbox.with_files(vec![FileWithContent( @@ -203,33 +207,34 @@ mod tests { .into_string() .expect("Couldn't convert to string."); - let actual = vec![ - ("SHELL".to_string(), var_shell), - ("USER".to_string(), var_user), - ]; + let mut found = IndexMap::new(); + found.insert("SHELL".to_string(), var_shell); + found.insert("USER".to_string(), var_user); - assert_eq!(actual, expected); + for k in found.keys() { + assert!(expected.contains_key(k)); + } }); // Now confirm in-memory environment variables synced appropriately // including the newer one accounted for. let environment = actual.env.lock(); - let vars = environment + let mut vars = IndexMap::new(); + environment .env() .expect("No variables in the environment.") .row_entries() - .map(|(name, value)| { - ( + .for_each(|(name, value)| { + vars.insert( name.to_string(), value.as_string().expect("Couldn't convert to string"), - ) - }) - .collect::>(); - - assert_eq!(vars, expected); + ); + }); + for k in expected.keys() { + assert!(vars.contains_key(k)); + } }); - Ok(()) } @@ -238,10 +243,11 @@ mod tests { let mut ctx = Context::basic()?; ctx.host = Arc::new(Mutex::new(Box::new(crate::env::host::FakeHost::new()))); - let expected = vec![( + let mut expected = IndexMap::new(); + expected.insert( "SHELL".to_string(), "/usr/bin/you_already_made_the_nu_choice".to_string(), - )]; + ); Playground::setup("syncs_env_test_2", |dirs, sandbox| { sandbox.with_files(vec![FileWithContent( @@ -278,26 +284,30 @@ mod tests { .into_string() .expect("Couldn't convert to string."); - let actual = vec![("SHELL".to_string(), var_shell)]; + let mut found = IndexMap::new(); + found.insert("SHELL".to_string(), var_shell); - assert_eq!(actual, expected); + for k in found.keys() { + assert!(expected.contains_key(k)); + } }); let environment = actual.env.lock(); - let vars = environment + let mut vars = IndexMap::new(); + environment .env() .expect("No variables in the environment.") .row_entries() - .map(|(name, value)| { - ( + .for_each(|(name, value)| { + vars.insert( name.to_string(), - value.as_string().expect("Couldn't convert to string"), - ) - }) - .collect::>(); - - assert_eq!(vars, expected); + value.as_string().expect("couldn't convert to string"), + ); + }); + for k in expected.keys() { + assert!(vars.contains_key(k)); + } }); Ok(()) diff --git a/crates/nu-cli/tests/commands/autoenv.rs b/crates/nu-cli/tests/commands/autoenv.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/crates/nu-cli/tests/commands/autoenv.rs @@ -0,0 +1 @@ + diff --git a/crates/nu-cli/tests/commands/autoenv_trust.rs b/crates/nu-cli/tests/commands/autoenv_trust.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/crates/nu-cli/tests/commands/autoenv_trust.rs @@ -0,0 +1 @@ + diff --git a/crates/nu-cli/tests/commands/autoenv_untrust.rs b/crates/nu-cli/tests/commands/autoenv_untrust.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/crates/nu-cli/tests/commands/autoenv_untrust.rs @@ -0,0 +1 @@ + diff --git a/crates/nu-cli/tests/commands/mod.rs b/crates/nu-cli/tests/commands/mod.rs index 6fc9cde723..1161d9fcd7 100644 --- a/crates/nu-cli/tests/commands/mod.rs +++ b/crates/nu-cli/tests/commands/mod.rs @@ -1,5 +1,8 @@ mod alias; mod append; +mod autoenv; +mod autoenv_trust; +mod autoenv_untrust; mod cal; mod calc; mod cd;