commit b3435122dd00b1936d720181146fd559a2023d3a Author: Oliver Hamlet Date: Mon Feb 26 23:10:53 2018 +0000 Initial commit of experimental LOOT condition parser Some FilePath functionality is missing, not all tests pass, and parsing is incorrect for all structs. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..143b1ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ + +/target/ +**/*.rs.bk +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..aac1766 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "loot-condition-interpreter" +version = "0.1.0" +authors = ["Oliver Hamlet "] + +[dependencies] +nom = "3.2.1" +regex = "0.2" diff --git a/README.md b/README.md new file mode 100644 index 0000000..fcc5ed8 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +loot-condition-interpreter +========================== + +An experimental library for parsing and evaluating LOOT's metadata condition +strings. Written in Rust. Very incomplete. + +Goals: + +- Explore parsing conditions into an intermediate representation, then + evaluating from that representation. LOOT's current parser evaluates + conditions while parsing them, tightly coupling the two operations. +- Explore more granular result caching. LOOT's current caching maps each + condition string to its result, which can't handle case insensitivity (some + parts of the condition string are case sensitive) or re-use results in + compound conditions. +- Explore round trip serialisation from the intermediate representation. +- Evaluate the cost/benefit of integrating into LOOT, replacing the existing + code. + +Currently only condition parsing is complete. Evaluation is partially done, the +rest hasn't yet been started. diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..705f55f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,248 @@ +#[macro_use] +extern crate nom; +extern crate regex; + +use regex::Regex; + +use std::str; +use std::path::PathBuf; +use nom::{IError, IResult}; + +#[derive(Debug)] +enum Error { + ParsingIncomplete, + ParsingError, + InvalidPath(PathBuf), + InvalidRegex(String), +} + +impl From for Error { + fn from(error: IError) -> Self { + match error { + IError::Error(_) => Error::ParsingError, + _ => Error::ParsingIncomplete, + } + } +} + +enum ComparisonOperator { + Equal, + NotEqual, + LessThan, + GreaterThan, + LessThanOrEqual, + GreaterThanOrEqual, +} + +enum Function { + FilePath(PathBuf), + FileRegex(Regex), + ActivePath(PathBuf), + ActiveRegex(Regex), + Many(Regex), + ManyActive(Regex), + Checksum(PathBuf, u32), + Version(PathBuf, String, ComparisonOperator), +} + +impl Function { + fn eval(&self) -> Result { + // TODO: Handle all variants. + // TODO: Paths may not lead outside game directory. + match *self { + Function::FilePath(ref f) => Ok(f.exists()), + _ => Ok(false), + } + } + + fn parse(input: &str) -> IResult<&str, Function> { + // TODO: Handle all variants. + // TODO: Paths may not contain :*?"<>| + do_parse!( + input, + tag!("file(\"") >> path: is_not!("\"") >> tag!("\")") + >> (Function::FilePath(PathBuf::from(path))) + ) + } +} + +// Compound conditions joined by 'or' +struct Expression(Vec); + +impl Expression { + fn eval(&self) -> Result { + for compound_condition in &self.0 { + if compound_condition.eval()? { + return Ok(true); + } + } + Ok(false) + } + + fn parse(input: &str) -> IResult<&str, Expression> { + IResult::Done(input, Expression(vec![])) + } +} + +// Conditions joined by 'and' +struct CompoundCondition(Vec); + +impl CompoundCondition { + fn eval(&self) -> Result { + for condition in &self.0 { + if !condition.eval()? { + return Ok(false); + } + } + Ok(true) + } + + fn parse(input: &str) -> IResult<&str, CompoundCondition> { + IResult::Done(input, CompoundCondition(vec![])) + } +} + +enum Condition { + Function(Function), + NotFunction(Function), + Expression(Expression), +} + +impl Condition { + fn eval(&self) -> Result { + match *self { + Condition::Function(ref f) => f.eval(), + Condition::NotFunction(ref f) => f.eval().map(|r| !r), + Condition::Expression(ref e) => e.eval(), + } + } + + fn parse(input: &str) -> IResult<&str, Condition> { + let (input1, function) = try_parse!(input, Function::parse); + IResult::Done(input1, Condition::Function(function)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn function_parse_should_parse_a_file_function() { + let result = Function::parse("file(\"Cargo.toml\")").to_result().unwrap(); + + match result { + Function::FilePath(f) => assert_eq!(PathBuf::from("Cargo.toml"), f), + _ => panic!("Expected a file function"), + } + } + + #[test] + fn function_file_path_eval_should_return_true_if_the_file_exists_relative_to_the_data_path() { + let function = Function::FilePath(PathBuf::from("Cargo.toml")); + + assert!(function.eval().unwrap()); + + unimplemented!("not yet any way to actually specify the data path"); + } + + #[test] + fn function_file_path_eval_should_return_true_if_given_a_plugin_that_is_ghosted() { + let function = Function::FilePath(PathBuf::from("test.esp")); + + assert!(function.eval().unwrap()); + + unimplemented!("need to add tempdir and create a test.esp.ghost"); + } + + #[test] + fn function_file_path_eval_should_error_if_the_path_is_outside_game_directory() { + unimplemented!("to do"); + } + + #[test] + fn function_file_path_eval_should_return_false_if_the_file_does_not_exist() { + let function = Function::FilePath(PathBuf::from("missing")); + + assert!(!function.eval().unwrap()); + } + + #[test] + fn condition_eval_should_return_function_eval_for_a_function_condition() { + let condition = Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))); + + assert!(condition.eval().unwrap()); + + let condition = Condition::Function(Function::FilePath(PathBuf::from("missing"))); + + assert!(!condition.eval().unwrap()); + } + + #[test] + fn condition_eval_should_return_expression_eval_for_an_expression_condition() { + let condition = Condition::Expression(Expression(vec![ + CompoundCondition(vec![ + Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))), + ]), + ])); + + assert!(condition.eval().unwrap()); + } + + #[test] + fn condition_eval_should_return_inverse_of_function_eval_for_a_not_function_condition() { + let condition = Condition::NotFunction(Function::FilePath(PathBuf::from("Cargo.toml"))); + + assert!(!condition.eval().unwrap()); + + let condition = Condition::NotFunction(Function::FilePath(PathBuf::from("missing"))); + + assert!(condition.eval().unwrap()); + } + + #[test] + fn compound_condition_eval_should_be_true_if_all_conditions_are_true() { + let compound_condition = CompoundCondition(vec![ + Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))), + Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))), + ]); + + assert!(compound_condition.eval().unwrap()); + } + + #[test] + fn compound_condition_eval_should_be_false_if_any_condition_is_false() { + let compound_condition = CompoundCondition(vec![ + Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))), + Condition::Function(Function::FilePath(PathBuf::from("missing"))), + ]); + + assert!(!compound_condition.eval().unwrap()); + } + + #[test] + fn expression_eval_should_be_true_if_any_compound_condition_is_true() { + let expression = Expression(vec![ + CompoundCondition(vec![ + Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))), + ]), + CompoundCondition(vec![ + Condition::Function(Function::FilePath(PathBuf::from("missing"))), + ]), + ]); + assert!(expression.eval().unwrap()); + } + + #[test] + fn expression_eval_should_be_false_if_all_compound_conditions_are_false() { + let expression = Expression(vec![ + CompoundCondition(vec![ + Condition::Function(Function::FilePath(PathBuf::from("missing"))), + ]), + CompoundCondition(vec![ + Condition::Function(Function::FilePath(PathBuf::from("missing"))), + ]), + ]); + assert!(!expression.eval().unwrap()); + } +}