diff --git a/src/function/eval.rs b/src/function/eval.rs index e65f5e7..a5e12e1 100644 --- a/src/function/eval.rs +++ b/src/function/eval.rs @@ -7,7 +7,8 @@ use std::path::{Path, PathBuf}; use crc::{crc32, Hasher32}; use regex::Regex; -use super::Function; +use super::{ComparisonOperator, Function}; +use version::Version; use Error; use State; @@ -148,9 +149,50 @@ fn evaluate_checksum(state: &State, file_path: &Path, crc: u32) -> Result Option { + path.file_name() + .and_then(OsStr::to_str) + .map(str::to_lowercase) +} + +fn get_version(state: &State, file_path: &Path) -> Result { + if let Some(key) = lowercase_filename(file_path) { + if let Some(version) = state.plugin_versions.get(&key) { + return Ok(Version::from(version.as_str())); + } + } + + Version::read_file_version(file_path) +} + +fn evaluate_version( + state: &State, + file_path: &Path, + given_version: &str, + comparator: ComparisonOperator, +) -> Result { + let file_path = resolve_path(state, file_path); + if !file_path.exists() { + return Ok(comparator == ComparisonOperator::NotEqual + || comparator == ComparisonOperator::LessThan + || comparator == ComparisonOperator::LessThanOrEqual); + } + + let given_version = Version::from(given_version); + let actual_version = get_version(state, &file_path)?; + + match comparator { + ComparisonOperator::Equal => Ok(actual_version == given_version), + ComparisonOperator::NotEqual => Ok(actual_version != given_version), + ComparisonOperator::LessThan => Ok(actual_version < given_version), + ComparisonOperator::GreaterThan => Ok(actual_version > given_version), + ComparisonOperator::LessThanOrEqual => Ok(actual_version <= given_version), + ComparisonOperator::GreaterThanOrEqual => Ok(actual_version >= given_version), + } +} + impl Function { pub fn eval(&self, state: &State) -> Result { - // TODO: Handle all variants. match *self { Function::FilePath(ref f) => evaluate_file_path(state, f), Function::FileRegex(ref p, ref r) => evaluate_file_regex(state, p, r), @@ -159,7 +201,7 @@ impl Function { Function::Many(ref p, ref r) => evaluate_many(state, p, r), Function::ManyActive(ref r) => evaluate_many_active(state, r), Function::Checksum(ref path, ref crc) => evaluate_checksum(state, path, *crc), - _ => Ok(false), + Function::Version(ref p, ref v, ref c) => evaluate_version(state, p, v, *c), } } } @@ -181,17 +223,25 @@ mod tests { } fn state_with_active_plugins>(data_path: T, active_plugins: &[&str]) -> State { - state_with_loot_path_and_active_plugins(data_path, "", active_plugins) + state_with_data(data_path, "", active_plugins, &[]) } fn state_with_loot_path>(data_path: T, loot_path: &str) -> State { - state_with_loot_path_and_active_plugins(data_path, loot_path, &[]) + state_with_data(data_path, loot_path, &[], &[]) } - fn state_with_loot_path_and_active_plugins>( + fn state_with_versions>( + data_path: T, + plugin_versions: &[(&str, &str)], + ) -> State { + state_with_data(data_path, "", &[], plugin_versions) + } + + fn state_with_data>( data_path: T, loot_path: &str, active_plugins: &[&str], + plugin_versions: &[(&str, &str)], ) -> State { let data_path = data_path.into(); if !data_path.exists() { @@ -207,6 +257,10 @@ mod tests { .map(|s| s.to_lowercase()) .collect(), crc_cache: RwLock::default(), + plugin_versions: plugin_versions + .iter() + .map(|(p, v)| (p.to_lowercase(), v.to_string())) + .collect(), } } @@ -518,4 +572,235 @@ mod tests { assert!(function.eval(&state).unwrap()); } + + #[test] + fn function_version_eval_should_be_true_if_the_path_does_not_exist_and_comparator_is_ne() { + let function = + Function::Version("missing".into(), "1.0".into(), ComparisonOperator::NotEqual); + let state = state("."); + + assert!(function.eval(&state).unwrap()); + } + + #[test] + fn function_version_eval_should_be_true_if_the_path_does_not_exist_and_comparator_is_lt() { + let function = + Function::Version("missing".into(), "1.0".into(), ComparisonOperator::LessThan); + let state = state("."); + + assert!(function.eval(&state).unwrap()); + } + + #[test] + fn function_version_eval_should_be_true_if_the_path_does_not_exist_and_comparator_is_lteq() { + let function = Function::Version( + "missing".into(), + "1.0".into(), + ComparisonOperator::LessThanOrEqual, + ); + let state = state("."); + + assert!(function.eval(&state).unwrap()); + } + + #[test] + fn function_version_eval_should_be_false_if_the_path_does_not_exist_and_comparator_is_eq() { + let function = Function::Version("missing".into(), "1.0".into(), ComparisonOperator::Equal); + let state = state("."); + + assert!(!function.eval(&state).unwrap()); + } + + #[test] + fn function_version_eval_should_be_false_if_the_path_does_not_exist_and_comparator_is_gt() { + let function = Function::Version( + "missing".into(), + "1.0".into(), + ComparisonOperator::GreaterThan, + ); + let state = state("."); + + assert!(!function.eval(&state).unwrap()); + } + + #[test] + fn function_version_eval_should_be_false_if_the_path_does_not_exist_and_comparator_is_gteq() { + let function = Function::Version( + "missing".into(), + "1.0".into(), + ComparisonOperator::GreaterThanOrEqual, + ); + let state = state("."); + + assert!(!function.eval(&state).unwrap()); + } + + #[test] + fn function_version_eval_should_be_false_if_versions_are_not_equal_and_comparator_is_eq() { + let function = Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::Equal); + let state = state_with_versions("./testing-plugins/Oblivion/Data", &[("Blank.esm", "1")]); + + assert!(!function.eval(&state).unwrap()); + } + + #[test] + fn function_version_eval_should_be_true_if_versions_are_equal_and_comparator_is_eq() { + let function = Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::Equal); + let state = state_with_versions("./testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]); + + assert!(function.eval(&state).unwrap()); + } + + #[test] + fn function_version_eval_should_be_false_if_versions_are_equal_and_comparator_is_ne() { + let function = + Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::NotEqual); + let state = state_with_versions("./testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]); + + assert!(!function.eval(&state).unwrap()); + } + + #[test] + fn function_version_eval_should_be_true_if_versions_are_not_equal_and_comparator_is_ne() { + let function = + Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::NotEqual); + let state = state_with_versions("./testing-plugins/Oblivion/Data", &[("Blank.esm", "1")]); + + assert!(function.eval(&state).unwrap()); + } + + #[test] + fn function_version_eval_should_be_false_if_actual_version_is_eq_and_comparator_is_lt() { + let function = + Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::LessThan); + let state = state_with_versions("./testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]); + + assert!(!function.eval(&state).unwrap()); + } + + #[test] + fn function_version_eval_should_be_false_if_actual_version_is_gt_and_comparator_is_lt() { + let function = + Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::LessThan); + let state = state_with_versions("./testing-plugins/Oblivion/Data", &[("Blank.esm", "6")]); + + assert!(!function.eval(&state).unwrap()); + } + + #[test] + fn function_version_eval_should_be_true_if_actual_version_is_lt_and_comparator_is_lt() { + let function = + Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::NotEqual); + let state = state_with_versions("./testing-plugins/Oblivion/Data", &[("Blank.esm", "1")]); + + assert!(function.eval(&state).unwrap()); + } + + #[test] + fn function_version_eval_should_be_false_if_actual_version_is_eq_and_comparator_is_gt() { + let function = Function::Version( + "Blank.esm".into(), + "5".into(), + ComparisonOperator::GreaterThan, + ); + let state = state_with_versions("./testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]); + + assert!(!function.eval(&state).unwrap()); + } + + #[test] + fn function_version_eval_should_be_false_if_actual_version_is_lt_and_comparator_is_gt() { + let function = Function::Version( + "Blank.esm".into(), + "5".into(), + ComparisonOperator::GreaterThan, + ); + let state = state_with_versions("./testing-plugins/Oblivion/Data", &[("Blank.esm", "4")]); + + assert!(!function.eval(&state).unwrap()); + } + + #[test] + fn function_version_eval_should_be_true_if_actual_version_is_gt_and_comparator_is_gt() { + let function = Function::Version( + "Blank.esm".into(), + "5".into(), + ComparisonOperator::GreaterThan, + ); + let state = state_with_versions("./testing-plugins/Oblivion/Data", &[("Blank.esm", "6")]); + + assert!(function.eval(&state).unwrap()); + } + + #[test] + fn function_version_eval_should_be_false_if_actual_version_is_gt_and_comparator_is_lteq() { + let function = Function::Version( + "Blank.esm".into(), + "5".into(), + ComparisonOperator::LessThanOrEqual, + ); + let state = state_with_versions("./testing-plugins/Oblivion/Data", &[("Blank.esm", "6")]); + + assert!(!function.eval(&state).unwrap()); + } + + #[test] + fn function_version_eval_should_be_true_if_actual_version_is_eq_and_comparator_is_lteq() { + let function = Function::Version( + "Blank.esm".into(), + "5".into(), + ComparisonOperator::LessThanOrEqual, + ); + let state = state_with_versions("./testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]); + + assert!(function.eval(&state).unwrap()); + } + + #[test] + fn function_version_eval_should_be_true_if_actual_version_is_lt_and_comparator_is_lteq() { + let function = Function::Version( + "Blank.esm".into(), + "5".into(), + ComparisonOperator::LessThanOrEqual, + ); + let state = state_with_versions("./testing-plugins/Oblivion/Data", &[("Blank.esm", "4")]); + + assert!(function.eval(&state).unwrap()); + } + + #[test] + fn function_version_eval_should_be_false_if_actual_version_is_lt_and_comparator_is_gteq() { + let function = Function::Version( + "Blank.esm".into(), + "5".into(), + ComparisonOperator::GreaterThanOrEqual, + ); + let state = state_with_versions("./testing-plugins/Oblivion/Data", &[("Blank.esm", "4")]); + + assert!(!function.eval(&state).unwrap()); + } + + #[test] + fn function_version_eval_should_be_true_if_actual_version_is_eq_and_comparator_is_gteq() { + let function = Function::Version( + "Blank.esm".into(), + "5".into(), + ComparisonOperator::GreaterThanOrEqual, + ); + let state = state_with_versions("./testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]); + + assert!(function.eval(&state).unwrap()); + } + + #[test] + fn function_version_eval_should_be_true_if_actual_version_is_gt_and_comparator_is_gteq() { + let function = Function::Version( + "Blank.esm".into(), + "5".into(), + ComparisonOperator::GreaterThanOrEqual, + ); + let state = state_with_versions("./testing-plugins/Oblivion/Data", &[("Blank.esm", "6")]); + + assert!(function.eval(&state).unwrap()); + } } diff --git a/src/function/mod.rs b/src/function/mod.rs index 3e7414b..32ab9c8 100644 --- a/src/function/mod.rs +++ b/src/function/mod.rs @@ -5,7 +5,7 @@ use regex::Regex; pub mod eval; pub mod parse; -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ComparisonOperator { Equal, NotEqual, diff --git a/src/lib.rs b/src/lib.rs index 9137ed3..47e187a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,9 @@ extern crate regex; #[cfg(test)] extern crate tempfile; +mod function; +mod version; + use std::collections::{HashMap, HashSet}; use std::ffi::OsStr; use std::io; @@ -15,7 +18,6 @@ use std::sync::RwLock; use nom::{Err, IResult}; -mod function; use function::Function; #[derive(Debug)] @@ -80,6 +82,7 @@ pub struct State { loot_path: PathBuf, active_plugins: HashSet, // Lowercased plugin filenames. crc_cache: RwLock>, // Lowercased paths. + plugin_versions: HashMap, // Lowercased plugin filenames and their versions as found in description fields. } // Compound conditions joined by 'or' @@ -182,6 +185,7 @@ mod tests { loot_path: PathBuf::new(), active_plugins: HashSet::new(), crc_cache: RwLock::default(), + plugin_versions: HashMap::default(), } } diff --git a/src/version.rs b/src/version.rs new file mode 100644 index 0000000..5adfd63 --- /dev/null +++ b/src/version.rs @@ -0,0 +1,40 @@ +use std::cmp::Ordering; +use std::path::Path; + +use Error; + +#[derive(Debug)] +pub struct Version { + string: String, +} + +impl Version { + pub fn read_file_version(file_path: &Path) -> Result { + // TODO: Actually read the file's File Version field. + Ok(Version { + string: format!("{}", file_path.display()), + }) + } +} + +impl<'a> From<&'a str> for Version { + fn from(string: &'a str) -> Self { + Version { + string: string.to_string(), + } + } +} + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Version) -> Option { + // TODO: Compare with same behaviour as pseudosem. + self.string.partial_cmp(&other.string) + } +} + +impl PartialEq for Version { + fn eq(&self, other: &Version) -> bool { + // TODO: Compare with same behaviour as pseudosem. + self.string == other.string + } +}