diff --git a/.gitignore b/.gitignore index 143b1ca..d222fd8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /target/ **/*.rs.bk Cargo.lock +/testing-plugins diff --git a/Cargo.toml b/Cargo.toml index a238cc4..3e90c8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,6 @@ authors = ["Oliver Hamlet "] [dependencies] nom = "4.0.0" regex = "1.0.5" + +[dev-dependencies] +tempfile = "3.0.0" diff --git a/README.md b/README.md index fcc5ed8..72f6318 100644 --- a/README.md +++ b/README.md @@ -19,3 +19,6 @@ Goals: Currently only condition parsing is complete. Evaluation is partially done, the rest hasn't yet been started. + +The tests need the [testing-plugins](https://github.com/WrinklyNinja/testing-plugins) +directory to be present in the repo root. diff --git a/src/function/eval.rs b/src/function/eval.rs index 077e075..a32fa3d 100644 --- a/src/function/eval.rs +++ b/src/function/eval.rs @@ -1,12 +1,77 @@ -use ::Error; +use std::ffi::OsStr; +use std::path::{Component, Path, PathBuf}; + use super::Function; +use Error; +use State; + +fn has_plugin_file_extension(path: &Path, state: &State) -> bool { + match path.extension().and_then(OsStr::to_str) { + Some("esp") | Some("esm") => true, + Some("esl") if state.game_type.supports_light_plugins() => true, + Some("ghost") => path + .file_stem() + .map(|s| has_plugin_file_extension(Path::new(s), state)) + .unwrap_or(false), + _ => false, + } +} + +fn add_extension(path: &Path, extension: &str) -> PathBuf { + match path.extension() { + Some(e) => { + let mut new_extension = e.to_os_string(); + new_extension.push(format!(".{}", extension)); + path.with_extension(&new_extension) + } + None => path.with_extension(extension), + } +} + +fn equals(path: &Path, test: &str) -> bool { + path.to_str().map(|s| s == test).unwrap_or(false) +} + +fn is_in_game_path(path: &Path) -> bool { + let mut previous_component = Component::CurDir; + for component in path.components() { + match (component, previous_component) { + (Component::Prefix(_), _) => return false, + (Component::RootDir, _) => return false, + (Component::ParentDir, Component::ParentDir) => return false, + (Component::CurDir, _) => continue, + _ => previous_component = component, + } + } + + true +} + +fn evaluate_file_path(state: &State, file_path: &Path) -> Result { + if equals(file_path, "LOOT") { + return Ok(true); + } + + if !is_in_game_path(file_path) { + return Err(Error::InvalidPath(file_path.to_path_buf())); + } + + let path = state.data_path.join(file_path); + let exists = path.exists(); + + if !exists && has_plugin_file_extension(&path, state) { + Ok(add_extension(&path, "ghost").exists()) + } else { + Ok(exists) + } +} impl Function { - pub fn eval(&self) -> Result { + pub fn eval(&self, state: &State) -> Result { // TODO: Handle all variants. // TODO: Paths may not lead outside game directory. match *self { - Function::FilePath(ref f) => Ok(f.exists()), + Function::FilePath(ref f) => evaluate_file_path(state, f), _ => Ok(false), } } @@ -14,49 +79,103 @@ impl Function { #[cfg(test)] mod tests { - use function::Function; + use super::*; - use std::path::PathBuf; + use std::fs::{copy, create_dir}; + + use tempfile::tempdir; + + use GameType; + + fn state>(data_path: T) -> State { + let data_path = data_path.into(); + if !data_path.exists() { + create_dir(&data_path).unwrap(); + } + + State { + game_type: GameType::tes4, + data_path: data_path, + } + } #[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")); + let state = state("."); - assert!(function.eval().unwrap()); - - unimplemented!("not yet any way to actually specify the data path"); + assert!(function.eval(&state).unwrap()); } #[test] fn function_file_path_eval_should_return_true_if_given_a_plugin_that_is_ghosted() { - let function = Function::FilePath(PathBuf::from("test.esp")); + let tmp_dir = tempdir().unwrap(); + let data_path = tmp_dir.path().join("Data"); + let state = state(data_path); - assert!(function.eval().unwrap()); + copy( + Path::new("testing-plugins/Oblivion/Data/Blank.esp"), + &state.data_path.join("Blank.esp.ghost"), + ).unwrap(); - unimplemented!("need to add tempdir and create a test.esp.ghost"); + let function = Function::FilePath(PathBuf::from("Blank.esp")); + + assert!(function.eval(&state).unwrap()); } #[test] #[allow(non_snake_case)] fn function_file_path_eval_should_be_true_if_given_LOOT() { - unimplemented!(); + let function = Function::FilePath(PathBuf::from("LOOT")); + let state = state("."); + + assert!(function.eval(&state).unwrap()); } #[test] fn function_file_path_eval_should_not_check_for_ghosted_non_plugin_file() { - unimplemented!(); + let tmp_dir = tempdir().unwrap(); + let data_path = tmp_dir.path().join("Data"); + let state = state(data_path); + + copy( + Path::new("Cargo.toml"), + &state.data_path.join("Cargo.toml.ghost"), + ).unwrap(); + + let function = Function::FilePath(PathBuf::from("Cargo.toml")); + + assert!(!function.eval(&state).unwrap()); + } + + #[test] + fn function_file_path_eval_should_not_error_if_the_path_is_in_the_game_directory() { + let tmp_dir = tempdir().unwrap(); + let data_path = tmp_dir.path().join("Data"); + let state = state(data_path); + + let function = Function::FilePath(PathBuf::from("../Cargo.toml")); + + assert!(function.eval(&state).is_ok()); } #[test] fn function_file_path_eval_should_error_if_the_path_is_outside_game_directory() { - unimplemented!("to do"); + let tmp_dir = tempdir().unwrap(); + let data_path = tmp_dir.path().join("Data"); + let state = state(data_path); + + let function = Function::FilePath(PathBuf::from("../../Cargo.toml")); + + assert!(function.eval(&state).is_err()); } #[test] fn function_file_path_eval_should_return_false_if_the_file_does_not_exist() { let function = Function::FilePath(PathBuf::from("missing")); + let state = state("."); - assert!(!function.eval().unwrap()); + assert!(!function.eval(&state).unwrap()); } #[test] diff --git a/src/function/parse.rs b/src/function/parse.rs index a4aeb4a..fa034dc 100644 --- a/src/function/parse.rs +++ b/src/function/parse.rs @@ -47,7 +47,7 @@ fn parse_version_args(input: &str) -> IResult<&str, (PathBuf, &str, ComparisonOp >> tag!("\"") >> ws!(tag!(",")) >> operator: call!(ComparisonOperator::parse) - >> ((PathBuf::from(path), version, operator)) + >> (PathBuf::from(path), version, operator) ) } @@ -65,7 +65,7 @@ fn parse_checksum_args(input: &str) -> IResult<&str, (PathBuf, u32)> { >> tag!("\"") >> ws!(tag!(",")) >> crc: flat_map!(call!(hex_digit), parse_crc) - >> ((PathBuf::from(path), crc)) + >> (PathBuf::from(path), crc) ) } @@ -161,9 +161,7 @@ mod tests { let result = Function::parse("many(\"Cargo.*\")").unwrap().1; match result { - Function::Many(r) => { - assert_eq!(Regex::new("Cargo.*").unwrap().as_str(), r.as_str()) - }, + Function::Many(r) => assert_eq!(Regex::new("Cargo.*").unwrap().as_str(), r.as_str()), _ => panic!("Expected a many function"), } } diff --git a/src/lib.rs b/src/lib.rs index 6499d64..77c5548 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,10 +2,15 @@ extern crate nom; extern crate regex; -use nom::{Err, IResult}; +#[cfg(test)] +extern crate tempfile; + +use std::io; use std::path::PathBuf; use std::str; +use nom::{Err, IResult}; + mod function; use function::Function; @@ -15,6 +20,7 @@ pub enum Error { ParsingError, InvalidPath(PathBuf), InvalidRegex(String), + IoError(io::Error), } impl From> for Error { @@ -26,14 +32,45 @@ impl From> for Error { } } +impl From for Error { + fn from(error: io::Error) -> Self { + Error::IoError(error) + } +} + +pub enum GameType { + tes4, + tes5, + tes5se, + tes5vr, + fo3, + fonv, + fo4, + fo4vr, +} + +impl GameType { + fn supports_light_plugins(&self) -> bool { + match self { + GameType::tes5se | GameType::tes5vr | GameType::fo4 | GameType::fo4vr => true, + _ => false, + } + } +} + +pub struct State { + game_type: GameType, + data_path: PathBuf, +} + // Compound conditions joined by 'or' #[derive(Debug)] pub struct Expression(Vec); impl Expression { - pub fn eval(&self) -> Result { + pub fn eval(&self, state: &State) -> Result { for compound_condition in &self.0 { - if compound_condition.eval()? { + if compound_condition.eval(state)? { return Ok(true); } } @@ -55,9 +92,9 @@ impl Expression { struct CompoundCondition(Vec); impl CompoundCondition { - fn eval(&self) -> Result { + fn eval(&self, state: &State) -> Result { for condition in &self.0 { - if !condition.eval()? { + if !condition.eval(state)? { return Ok(false); } } @@ -81,11 +118,11 @@ enum Condition { } impl Condition { - fn eval(&self) -> Result { + fn eval(&self, state: &State) -> Result { match *self { - Condition::Function(ref f) => f.eval(), - Condition::InvertedFunction(ref f) => f.eval().map(|r| !r), - Condition::Expression(ref e) => e.eval(), + Condition::Function(ref f) => f.eval(state), + Condition::InvertedFunction(ref f) => f.eval(state).map(|r| !r), + Condition::Expression(ref e) => e.eval(state), } } @@ -112,6 +149,20 @@ impl Condition { mod tests { use super::*; + use std::fs::create_dir; + + fn state>(data_path: T) -> State { + let data_path = data_path.into(); + if !data_path.exists() { + create_dir(&data_path).unwrap(); + } + + State { + game_type: GameType::tes4, + data_path: data_path, + } + } + #[test] fn expression_parse_should_handle_a_single_compound_condition() { let result = Expression::parse("file(\"Cargo.toml\")").unwrap().1; @@ -216,58 +267,70 @@ mod tests { #[test] fn condition_eval_should_return_function_eval_for_a_function_condition() { + let state = state("."); + let condition = Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))); - assert!(condition.eval().unwrap()); + assert!(condition.eval(&state).unwrap()); let condition = Condition::Function(Function::FilePath(PathBuf::from("missing"))); - assert!(!condition.eval().unwrap()); + assert!(!condition.eval(&state).unwrap()); } #[test] fn condition_eval_should_return_expression_eval_for_an_expression_condition() { + let state = state("."); + let condition = Condition::Expression(Expression(vec![CompoundCondition(vec![ Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))), ])])); - assert!(condition.eval().unwrap()); + assert!(condition.eval(&state).unwrap()); } #[test] fn condition_eval_should_return_inverse_of_function_eval_for_a_not_function_condition() { + let state = state("."); + let condition = Condition::InvertedFunction(Function::FilePath(PathBuf::from("Cargo.toml"))); - assert!(!condition.eval().unwrap()); + assert!(!condition.eval(&state).unwrap()); let condition = Condition::InvertedFunction(Function::FilePath(PathBuf::from("missing"))); - assert!(condition.eval().unwrap()); + assert!(condition.eval(&state).unwrap()); } #[test] fn compound_condition_eval_should_be_true_if_all_conditions_are_true() { + let state = state("."); + 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()); + assert!(compound_condition.eval(&state).unwrap()); } #[test] fn compound_condition_eval_should_be_false_if_any_condition_is_false() { + let state = state("."); + 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()); + assert!(!compound_condition.eval(&state).unwrap()); } #[test] fn expression_eval_should_be_true_if_any_compound_condition_is_true() { + let state = state("."); + let expression = Expression(vec![ CompoundCondition(vec![Condition::Function(Function::FilePath( PathBuf::from("Cargo.toml"), @@ -276,11 +339,13 @@ mod tests { PathBuf::from("missing"), ))]), ]); - assert!(expression.eval().unwrap()); + assert!(expression.eval(&state).unwrap()); } #[test] fn expression_eval_should_be_false_if_all_compound_conditions_are_false() { + let state = state("."); + let expression = Expression(vec![ CompoundCondition(vec![Condition::Function(Function::FilePath( PathBuf::from("missing"), @@ -289,6 +354,6 @@ mod tests { PathBuf::from("missing"), ))]), ]); - assert!(!expression.eval().unwrap()); + assert!(!expression.eval(&state).unwrap()); } }