diff --git a/book/src/configuration.md b/book/src/configuration.md index a43ede76a..9c61ad8d3 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -30,8 +30,9 @@ You can use a custom configuration file by specifying it with the `-c` or Additionally, you can reload the configuration file by sending the USR1 signal to the Helix process on Unix operating systems, such as by using the command `pkill -USR1 hx`. -Finally, you can have a `config.toml` local to a project by putting it under a `.helix` directory in your repository. +Finally, you can have a `config.toml` local to a project by putting it under a `.helix` directory in your repository and adding `workspace-config = true` to the top of your configuration directory `config.toml`. Its settings will be merged with the configuration directory `config.toml` and the built-in configuration. +Enabling workspace configs is a potential security risk. A project from an untrusted sources may e.g. contain a `.helix/config.toml` that overrides the `editor.shell` parameter to execute malicious code on your machine. ## Editor diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index bcba8d8e1..34b571d61 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -11,14 +11,16 @@ use toml::de::Error as TomlError; #[derive(Debug, Clone, PartialEq)] pub struct Config { + pub workspace_config: bool, pub theme: Option, pub keys: HashMap, pub editor: helix_view::editor::Config, } #[derive(Debug, Clone, PartialEq, Deserialize)] -#[serde(deny_unknown_fields)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct ConfigRaw { + pub workspace_config: Option, pub theme: Option, pub keys: Option>, pub editor: Option, @@ -27,6 +29,7 @@ pub struct ConfigRaw { impl Default for Config { fn default() -> Config { Config { + workspace_config: false, theme: None, keys: keymap::default(), editor: helix_view::editor::Config::default(), @@ -57,72 +60,45 @@ impl Display for ConfigLoadError { impl Config { pub fn load( - global: Result, - local: Result, + mut global: ConfigRaw, mut workspace: Option, ) -> Result { - let global_config: Result = - global.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig)); - let local_config: Result = - local.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig)); - let res = match (global_config, local_config) { - (Ok(global), Ok(local)) => { - let mut keys = keymap::default(); - if let Some(global_keys) = global.keys { - merge_keys(&mut keys, global_keys) - } - if let Some(local_keys) = local.keys { - merge_keys(&mut keys, local_keys) - } - - let editor = match (global.editor, local.editor) { - (None, None) => helix_view::editor::Config::default(), - (None, Some(val)) | (Some(val), None) => { - val.try_into().map_err(ConfigLoadError::BadConfig)? - } - (Some(global), Some(local)) => merge_toml_values(global, local, 3) - .try_into() - .map_err(ConfigLoadError::BadConfig)?, - }; - - Config { - theme: local.theme.or(global.theme), - keys, - editor, - } - } - // if any configs are invalid return that first - (_, Err(ConfigLoadError::BadConfig(err))) - | (Err(ConfigLoadError::BadConfig(err)), _) => { - return Err(ConfigLoadError::BadConfig(err)) - } - (Ok(config), Err(_)) | (Err(_), Ok(config)) => { - let mut keys = keymap::default(); - if let Some(keymap) = config.keys { - merge_keys(&mut keys, keymap); - } - Config { - theme: config.theme, - keys, - editor: config.editor.map_or_else( - || Ok(helix_view::editor::Config::default()), - |val| val.try_into().map_err(ConfigLoadError::BadConfig), - )?, - } - } - - // these are just two io errors return the one for the global config - (Err(err), Err(_)) => return Err(err), + // Create merged keymap + let mut keys = keymap::default(); + [Some(&mut global), workspace.as_mut()].into_iter() + .flatten().filter_map(|c| c.keys.take()) + .for_each(|k| merge_keys(&mut keys, k)); + + // Create config + let config = Config { + workspace_config: global.workspace_config.unwrap_or(false), + theme: workspace.as_mut().and_then(|c| c.theme.take()).or(global.theme), + keys, + editor: match (global.editor, workspace.and_then(|c| c.editor)) { + (None, None) => Ok(helix_view::editor::Config::default()), + (None, Some(editor)) | (Some(editor), None) => editor.try_into(), + (Some(glob), Some(work)) => merge_toml_values(glob, work, 3).try_into(), + }.map_err(ConfigLoadError::BadConfig)?, }; - Ok(res) + Ok(config) } pub fn load_default() -> Result { - let global_config = - fs::read_to_string(helix_loader::config_file()).map_err(ConfigLoadError::Error); - let local_config = fs::read_to_string(helix_loader::workspace_config_file()) - .map_err(ConfigLoadError::Error); - Config::load(global_config, local_config) + // Load and parse global config returning all errors + let global: ConfigRaw = fs::read_to_string(helix_loader::config_file()) + .map_err(ConfigLoadError::Error) + .and_then(|c| toml::from_str(&c) + .map_err(ConfigLoadError::BadConfig))?; + + // Load and parse workspace config if enabled ignoring IO errors + let workspace: Option = global.workspace_config.unwrap_or(false) + .then(|| helix_loader::workspace_config_file()) + .and_then(|f| fs::read_to_string(f).ok()) + .map(|c| toml::from_str(&c).map_err(ConfigLoadError::BadConfig)) + .transpose()?; + + // Create merged config + Config::load(global, workspace) } } @@ -131,8 +107,8 @@ mod tests { use super::*; impl Config { - fn load_test(config: &str) -> Config { - Config::load(Ok(config.to_owned()), Err(ConfigLoadError::default())).unwrap() + fn load_test(file: &str) -> Config { + Config::load(toml::from_str(file).unwrap(), None).unwrap() } }