mirror of
https://github.com/loot/loot-condition-interpreter.git
synced 2026-07-27 14:16:09 -07:00
Add lexicographical evaluation of version conditions
Versions aren't yet read from executables, and evaluation isn't pseudosem-compatible.
This commit is contained in:
+291
-6
@@ -7,7 +7,8 @@ use std::path::{Path, PathBuf};
|
|||||||
use crc::{crc32, Hasher32};
|
use crc::{crc32, Hasher32};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
use super::Function;
|
use super::{ComparisonOperator, Function};
|
||||||
|
use version::Version;
|
||||||
use Error;
|
use Error;
|
||||||
use State;
|
use State;
|
||||||
|
|
||||||
@@ -148,9 +149,50 @@ fn evaluate_checksum(state: &State, file_path: &Path, crc: u32) -> Result<bool,
|
|||||||
Ok(calculated_crc == crc)
|
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 {
|
impl Function {
|
||||||
pub fn eval(&self, state: &State) -> Result<bool, Error> {
|
pub fn eval(&self, state: &State) -> Result<bool, Error> {
|
||||||
// TODO: Handle all variants.
|
|
||||||
match *self {
|
match *self {
|
||||||
Function::FilePath(ref f) => evaluate_file_path(state, f),
|
Function::FilePath(ref f) => evaluate_file_path(state, f),
|
||||||
Function::FileRegex(ref p, ref r) => evaluate_file_regex(state, p, r),
|
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::Many(ref p, ref r) => evaluate_many(state, p, r),
|
||||||
Function::ManyActive(ref r) => evaluate_many_active(state, r),
|
Function::ManyActive(ref r) => evaluate_many_active(state, r),
|
||||||
Function::Checksum(ref path, ref crc) => evaluate_checksum(state, path, *crc),
|
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 {
|
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 {
|
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,
|
data_path: T,
|
||||||
loot_path: &str,
|
loot_path: &str,
|
||||||
active_plugins: &[&str],
|
active_plugins: &[&str],
|
||||||
|
plugin_versions: &[(&str, &str)],
|
||||||
) -> State {
|
) -> State {
|
||||||
let data_path = data_path.into();
|
let data_path = data_path.into();
|
||||||
if !data_path.exists() {
|
if !data_path.exists() {
|
||||||
@@ -207,6 +257,10 @@ mod tests {
|
|||||||
.map(|s| s.to_lowercase())
|
.map(|s| s.to_lowercase())
|
||||||
.collect(),
|
.collect(),
|
||||||
crc_cache: RwLock::default(),
|
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());
|
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
@@ -5,7 +5,7 @@ use regex::Regex;
|
|||||||
pub mod eval;
|
pub mod eval;
|
||||||
pub mod parse;
|
pub mod parse;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum ComparisonOperator {
|
pub enum ComparisonOperator {
|
||||||
Equal,
|
Equal,
|
||||||
NotEqual,
|
NotEqual,
|
||||||
|
|||||||
+5
-1
@@ -6,6 +6,9 @@ extern crate regex;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
extern crate tempfile;
|
extern crate tempfile;
|
||||||
|
|
||||||
|
mod function;
|
||||||
|
mod version;
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::io;
|
use std::io;
|
||||||
@@ -15,7 +18,6 @@ use std::sync::RwLock;
|
|||||||
|
|
||||||
use nom::{Err, IResult};
|
use nom::{Err, IResult};
|
||||||
|
|
||||||
mod function;
|
|
||||||
use function::Function;
|
use function::Function;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -80,6 +82,7 @@ pub struct State {
|
|||||||
loot_path: PathBuf,
|
loot_path: PathBuf,
|
||||||
active_plugins: HashSet<String>, // Lowercased plugin filenames.
|
active_plugins: HashSet<String>, // Lowercased plugin filenames.
|
||||||
crc_cache: RwLock<HashMap<String, u32>>, // Lowercased paths.
|
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'
|
// Compound conditions joined by 'or'
|
||||||
@@ -182,6 +185,7 @@ mod tests {
|
|||||||
loot_path: PathBuf::new(),
|
loot_path: PathBuf::new(),
|
||||||
active_plugins: HashSet::new(),
|
active_plugins: HashSet::new(),
|
||||||
crc_cache: RwLock::default(),
|
crc_cache: RwLock::default(),
|
||||||
|
plugin_versions: HashMap::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user