extern crate dirs; use std::path::{Path, PathBuf}; use std::{fs}; use crate::matcher::Match; use std::fs::{File, create_dir_all}; use std::io::Read; use serde::{Serialize, Deserialize}; use crate::event::KeyModifier; use std::collections::HashSet; use log::{error}; use std::fmt; use std::error::Error; pub(crate) mod runtime; // TODO: add documentation link const DEFAULT_CONFIG_FILE_CONTENT : &str = include_str!("../res/config.yaml"); const DEFAULT_CONFIG_FILE_NAME : &str = "default.yaml"; // Default values for primitives fn default_name() -> String{ "default".to_owned() } fn default_filter_title() -> String{ "".to_owned() } fn default_filter_class() -> String{ "".to_owned() } fn default_filter_exec() -> String{ "".to_owned() } fn default_disabled() -> bool{ false } fn default_log_level() -> i32 { 0 } fn default_config_caching_interval() -> i32 { 800 } fn default_toggle_interval() -> u32 { 230 } fn default_backspace_limit() -> i32 { 3 } fn default_matches() -> Vec { Vec::new() } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Configs { #[serde(default = "default_name")] pub name: String, #[serde(default = "default_filter_title")] pub filter_title: String, #[serde(default = "default_filter_class")] pub filter_class: String, #[serde(default = "default_filter_exec")] pub filter_exec: String, #[serde(default = "default_disabled")] pub disabled: bool, #[serde(default = "default_log_level")] pub log_level: i32, #[serde(default = "default_config_caching_interval")] pub config_caching_interval: i32, #[serde(default)] pub toggle_key: KeyModifier, #[serde(default = "default_toggle_interval")] pub toggle_interval: u32, #[serde(default = "default_backspace_limit")] pub backspace_limit: i32, #[serde(default)] pub backend: BackendType, #[serde(default = "default_matches")] pub matches: Vec } // Macro used to validate config fields #[macro_export] macro_rules! validate_field { ($result:expr, $field:expr, $def_value:expr) => { if $field != $def_value { let mut field_name = stringify!($field); if field_name.starts_with("self.") { field_name = &field_name[5..]; // Remove the 'self.' prefix } error!("Validation error, parameter '{}' is reserved and can be only used in the default.yaml config file", field_name); $result = false; } }; } impl Configs { /* * Validate the Config instance. * It makes sure that app-specific config instances do not define * attributes reserved to the default config. */ fn validate_specific_config(&self) -> bool { let mut result = true; validate_field!(result, self.config_caching_interval, default_config_caching_interval()); validate_field!(result, self.log_level, default_log_level()); validate_field!(result, self.toggle_key, KeyModifier::default()); validate_field!(result, self.toggle_interval, default_toggle_interval()); validate_field!(result, self.backspace_limit, default_backspace_limit()); result } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum BackendType { Inject, Clipboard } impl Default for BackendType { fn default() -> Self { BackendType::Inject } } impl Configs { fn load_config(path: &Path) -> Result { let file_res = File::open(path); if let Ok(mut file) = file_res { let mut contents = String::new(); let res = file.read_to_string(&mut contents); if let Err(_) = res { return Err(ConfigLoadError::UnableToReadFile) } let config_res = serde_yaml::from_str(&contents); match config_res { Ok(config) => Ok(config), Err(e) => { Err(ConfigLoadError::InvalidYAML(path.to_owned(), e.to_string())) } } }else{ Err(ConfigLoadError::FileNotFound) } } } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ConfigSet { pub default: Configs, pub specific: Vec, } impl ConfigSet { pub fn load(dir_path: &Path) -> Result { if !dir_path.is_dir() { return Err(ConfigLoadError::InvalidConfigDirectory) } let default_file = dir_path.join(DEFAULT_CONFIG_FILE_NAME); let default = Configs::load_config(default_file.as_path())?; let mut specific = Vec::new(); // Used to make sure no duplicates are present let mut name_set = HashSet::new(); let dir_entry = fs::read_dir(dir_path); if dir_entry.is_err() { return Err(ConfigLoadError::UnableToReadFile) } let dir_entry = dir_entry.unwrap(); for entry in dir_entry { let entry = entry; if let Ok(entry) = entry { let path = entry.path(); // Skip the default one, already loaded if path.file_name().unwrap_or("".as_ref()) == "default.yaml" { continue; } let config = Configs::load_config(path.as_path())?; if !config.validate_specific_config() { return Err(ConfigLoadError::InvalidParameter(path.to_owned())) } if config.name == "default" { return Err(ConfigLoadError::MissingName(path.to_owned())); } if name_set.contains(&config.name) { return Err(ConfigLoadError::NameDuplicate(path.to_owned())); } // TODO: check if it contains at least a filter, and warn the user about the problem name_set.insert(config.name.clone()); specific.push(config); } } Ok(ConfigSet { default, specific }) } pub fn load_default() -> Result { let res = dirs::home_dir(); if let Some(home_dir) = res { let espanso_dir = home_dir.join(".espanso"); // Create the espanso dir if id doesn't exist let res = create_dir_all(espanso_dir.as_path()); if let Ok(_) = res { let default_file = espanso_dir.join(DEFAULT_CONFIG_FILE_NAME); // If config file does not exist, create one from template if !default_file.exists() { let result = fs::write(&default_file, DEFAULT_CONFIG_FILE_CONTENT); if result.is_err() { return Err(ConfigLoadError::UnableToCreateDefaultConfig) } } return ConfigSet::load(espanso_dir.as_path()) } } return Err(ConfigLoadError::UnableToCreateDefaultConfig) } } pub trait ConfigManager<'a> { fn active_config(&'a self) -> &'a Configs; fn default_config(&'a self) -> &'a Configs; fn matches(&'a self) -> &'a Vec; } // Error handling #[derive(Debug, PartialEq)] pub enum ConfigLoadError { FileNotFound, UnableToReadFile, InvalidYAML(PathBuf, String), InvalidConfigDirectory, InvalidParameter(PathBuf), MissingName(PathBuf), NameDuplicate(PathBuf), UnableToCreateDefaultConfig, } impl fmt::Display for ConfigLoadError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { ConfigLoadError::FileNotFound => write!(f, "File not found"), ConfigLoadError::UnableToReadFile => write!(f, "Unable to read config file"), ConfigLoadError::InvalidYAML(path, e) => write!(f, "Error parsing YAML file '{}', invalid syntax: {}", path.to_str().unwrap_or_default(), e), ConfigLoadError::InvalidConfigDirectory => write!(f, "Invalid config directory"), ConfigLoadError::InvalidParameter(path) => write!(f, "Invalid parameter in '{}', use of reserved parameters in app-specific configs is not permitted", path.to_str().unwrap_or_default()), ConfigLoadError::MissingName(path) => write!(f, "The 'name' field is required in app-specific configurations, but it's missing in '{}'", path.to_str().unwrap_or_default()), ConfigLoadError::NameDuplicate(path) => write!(f, "Found duplicate 'name' in '{}', please use different names", path.to_str().unwrap_or_default()), ConfigLoadError::UnableToCreateDefaultConfig => write!(f, "Could not generate default config file"), } } } impl Error for ConfigLoadError { fn description(&self) -> &str { match self { ConfigLoadError::FileNotFound => "File not found", ConfigLoadError::UnableToReadFile => "Unable to read config file", ConfigLoadError::InvalidYAML(_, _) => "Error parsing YAML file, invalid syntax", ConfigLoadError::InvalidConfigDirectory => "Invalid config directory", ConfigLoadError::InvalidParameter(_) => "Invalid parameter, use of reserved parameters in app-specific configs is not permitted", ConfigLoadError::MissingName(_) => "The 'name' field is required in app-specific configurations, but it's missing", ConfigLoadError::NameDuplicate(_) => "Found duplicate 'name' in some configurations, please use different names", ConfigLoadError::UnableToCreateDefaultConfig => "Could not generate default config file", } } } #[cfg(test)] mod tests { use super::*; use std::io::Write; use tempfile::{NamedTempFile, TempDir}; use std::any::Any; const TEST_WORKING_CONFIG_FILE : &str = include_str!("../res/test/working_config.yaml"); const TEST_CONFIG_FILE_WITH_BAD_YAML : &str = include_str!("../res/test/config_with_bad_yaml.yaml"); // Test Configs fn create_tmp_file(string: &str) -> NamedTempFile { let file = NamedTempFile::new().unwrap(); file.as_file().write_all(string.as_bytes()); file } fn variant_eq(a: &T, b: &T) -> bool { std::mem::discriminant(a) == std::mem::discriminant(b) } #[test] fn test_config_file_not_found() { let config = Configs::load_config(Path::new("invalid/path")); assert_eq!(config.is_err(), true); assert_eq!(config.unwrap_err(), ConfigLoadError::FileNotFound); } #[test] fn test_config_file_with_bad_yaml_syntax() { let broken_config_file = create_tmp_file(TEST_CONFIG_FILE_WITH_BAD_YAML); let config = Configs::load_config(broken_config_file.path()); match config { Ok(_) => {assert!(false)}, Err(e) => { match e { ConfigLoadError::InvalidYAML(p, _) => assert_eq!(p, broken_config_file.path().to_owned()), _ => assert!(false), } assert!(true); }, } } #[test] fn test_validate_field_macro() { let mut result = true; validate_field!(result, 3, 3); assert_eq!(result, true); validate_field!(result, 10, 3); assert_eq!(result, false); validate_field!(result, 3, 3); assert_eq!(result, false); } #[test] fn test_specific_config_does_not_have_reserved_fields() { let working_config_file = create_tmp_file(r###" backend: Clipboard "###); let config = Configs::load_config(working_config_file.path()); assert_eq!(config.unwrap().validate_specific_config(), true); } #[test] fn test_specific_config_has_reserved_fields_config_caching_interval() { let working_config_file = create_tmp_file(r###" # This should not happen in an app-specific config config_caching_interval: 100 "###); let config = Configs::load_config(working_config_file.path()); assert_eq!(config.unwrap().validate_specific_config(), false); } #[test] fn test_specific_config_has_reserved_fields_toggle_key() { let working_config_file = create_tmp_file(r###" # This should not happen in an app-specific config toggle_key: CTRL "###); let config = Configs::load_config(working_config_file.path()); assert_eq!(config.unwrap().validate_specific_config(), false); } #[test] fn test_specific_config_has_reserved_fields_toggle_interval() { let working_config_file = create_tmp_file(r###" # This should not happen in an app-specific config toggle_interval: 1000 "###); let config = Configs::load_config(working_config_file.path()); assert_eq!(config.unwrap().validate_specific_config(), false); } #[test] fn test_specific_config_has_reserved_fields_backspace_limit() { let working_config_file = create_tmp_file(r###" # This should not happen in an app-specific config backspace_limit: 10 "###); let config = Configs::load_config(working_config_file.path()); assert_eq!(config.unwrap().validate_specific_config(), false); } #[test] fn test_config_loaded_correctly() { let working_config_file = create_tmp_file(TEST_WORKING_CONFIG_FILE); let config = Configs::load_config(working_config_file.path()); assert_eq!(config.is_ok(), true); } // Test ConfigSet #[test] fn test_config_set_default_content_should_work_correctly() { let tmp_dir = TempDir::new().expect("unable to create temp directory"); let default_path = tmp_dir.path().join(DEFAULT_CONFIG_FILE_NAME); fs::write(default_path, DEFAULT_CONFIG_FILE_CONTENT); let config_set = ConfigSet::load(tmp_dir.path()); assert!(config_set.is_ok()); } #[test] fn test_config_set_load_fail_bad_directory() { let config_set = ConfigSet::load(Path::new("invalid/path")); assert_eq!(config_set.is_err(), true); assert_eq!(config_set.unwrap_err(), ConfigLoadError::InvalidConfigDirectory); } #[test] fn test_config_set_missing_default_file() { let tmp_dir = TempDir::new().expect("unable to create temp directory"); let config_set = ConfigSet::load(tmp_dir.path()); assert_eq!(config_set.is_err(), true); assert_eq!(config_set.unwrap_err(), ConfigLoadError::FileNotFound); } #[test] fn test_config_set_invalid_yaml_syntax() { let tmp_dir = TempDir::new().expect("unable to create temp directory"); let default_path = tmp_dir.path().join(DEFAULT_CONFIG_FILE_NAME); let default_path_copy = default_path.clone(); fs::write(default_path, TEST_CONFIG_FILE_WITH_BAD_YAML); let config_set = ConfigSet::load(tmp_dir.path()); match config_set { Ok(_) => {assert!(false)}, Err(e) => { match e { ConfigLoadError::InvalidYAML(p, _) => assert_eq!(p, default_path_copy), _ => assert!(false), } assert!(true); }, } } #[test] fn test_config_set_specific_file_with_reserved_fields() { let tmp_dir = TempDir::new().expect("unable to create temp directory"); let default_path = tmp_dir.path().join(DEFAULT_CONFIG_FILE_NAME); fs::write(default_path, DEFAULT_CONFIG_FILE_CONTENT); let specific_path = tmp_dir.path().join("specific.yaml"); let specific_path_copy = specific_path.clone(); fs::write(specific_path, r###" config_caching_interval: 10000 "###); let config_set = ConfigSet::load(tmp_dir.path()); assert!(config_set.is_err()); assert_eq!(config_set.unwrap_err(), ConfigLoadError::InvalidParameter(specific_path_copy)) } #[test] fn test_config_set_specific_file_missing_name() { let tmp_dir = TempDir::new().expect("unable to create temp directory"); let default_path = tmp_dir.path().join(DEFAULT_CONFIG_FILE_NAME); fs::write(default_path, DEFAULT_CONFIG_FILE_CONTENT); let specific_path = tmp_dir.path().join("specific.yaml"); let specific_path_copy = specific_path.clone(); fs::write(specific_path, r###" backend: Clipboard "###); let config_set = ConfigSet::load(tmp_dir.path()); assert!(config_set.is_err()); assert_eq!(config_set.unwrap_err(), ConfigLoadError::MissingName(specific_path_copy)) } pub fn create_temp_espanso_directory() -> TempDir { let tmp_dir = TempDir::new().expect("unable to create temp directory"); let default_path = tmp_dir.path().join(DEFAULT_CONFIG_FILE_NAME); fs::write(default_path, DEFAULT_CONFIG_FILE_CONTENT); tmp_dir } pub fn create_temp_file_in_dir(tmp_dir: &TempDir, name: &str, content: &str) -> PathBuf { let specific_path = tmp_dir.path().join(name); let specific_path_copy = specific_path.clone(); fs::write(specific_path, content); specific_path_copy } #[test] fn test_config_set_specific_file_duplicate_name() { let tmp_dir = create_temp_espanso_directory(); let specific_path = create_temp_file_in_dir(&tmp_dir, "specific.yaml", r###" name: specific1 "###); let specific_path2 = create_temp_file_in_dir(&tmp_dir, "specific2.yaml", r###" name: specific1 "###); let config_set = ConfigSet::load(tmp_dir.path()); assert!(config_set.is_err()); assert!(variant_eq(&config_set.unwrap_err(), &ConfigLoadError::NameDuplicate(PathBuf::new()))) } }