Add lexicographical evaluation of version conditions

Versions aren't yet read from executables, and evaluation isn't
pseudosem-compatible.
This commit is contained in:
Oliver Hamlet
2018-10-05 17:12:38 +01:00
parent 3acf2ffea1
commit 8cb69dfa86
4 changed files with 337 additions and 8 deletions
+291 -6
View File
@@ -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<bool,
Ok(calculated_crc == crc)
}
fn lowercase_filename(path: &Path) -> Option<String> {
path.file_name()
.and_then(OsStr::to_str)
.map(str::to_lowercase)
}
fn get_version(state: &State, file_path: &Path) -> Result<Version, Error> {
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<bool, Error> {
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<bool, Error> {
// 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<T: Into<PathBuf>>(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<T: Into<PathBuf>>(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<T: Into<PathBuf>>(
fn state_with_versions<T: Into<PathBuf>>(
data_path: T,
plugin_versions: &[(&str, &str)],
) -> State {
state_with_data(data_path, "", &[], plugin_versions)
}
fn state_with_data<T: Into<PathBuf>>(
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());
}
}
+1 -1
View File
@@ -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,
+5 -1
View File
@@ -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<String>, // Lowercased plugin filenames.
crc_cache: RwLock<HashMap<String, u32>>, // Lowercased paths.
plugin_versions: HashMap<String, String>, // 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(),
}
}
+40
View File
@@ -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<Self, Error> {
// 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<Ordering> {
// 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
}
}