Implement file path function evaluation

This commit is contained in:
Oliver Hamlet
2018-10-03 17:15:34 +01:00
parent 11fef635d3
commit b9448e2072
6 changed files with 227 additions and 38 deletions
+1
View File
@@ -2,3 +2,4 @@
/target/ /target/
**/*.rs.bk **/*.rs.bk
Cargo.lock Cargo.lock
/testing-plugins
+3
View File
@@ -6,3 +6,6 @@ authors = ["Oliver Hamlet <oliver.hamlet@gmail.com>"]
[dependencies] [dependencies]
nom = "4.0.0" nom = "4.0.0"
regex = "1.0.5" regex = "1.0.5"
[dev-dependencies]
tempfile = "3.0.0"
+3
View File
@@ -19,3 +19,6 @@ Goals:
Currently only condition parsing is complete. Evaluation is partially done, the Currently only condition parsing is complete. Evaluation is partially done, the
rest hasn't yet been started. 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.
+134 -15
View File
@@ -1,12 +1,77 @@
use ::Error; use std::ffi::OsStr;
use std::path::{Component, Path, PathBuf};
use super::Function; 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<bool, Error> {
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 { impl Function {
pub fn eval(&self) -> Result<bool, Error> { pub fn eval(&self, state: &State) -> Result<bool, Error> {
// TODO: Handle all variants. // TODO: Handle all variants.
// TODO: Paths may not lead outside game directory. // TODO: Paths may not lead outside game directory.
match *self { match *self {
Function::FilePath(ref f) => Ok(f.exists()), Function::FilePath(ref f) => evaluate_file_path(state, f),
_ => Ok(false), _ => Ok(false),
} }
} }
@@ -14,49 +79,103 @@ impl Function {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use function::Function; use super::*;
use std::path::PathBuf; use std::fs::{copy, create_dir};
use tempfile::tempdir;
use GameType;
fn state<T: Into<PathBuf>>(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] #[test]
fn function_file_path_eval_should_return_true_if_the_file_exists_relative_to_the_data_path() { 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 function = Function::FilePath(PathBuf::from("Cargo.toml"));
let state = state(".");
assert!(function.eval().unwrap()); assert!(function.eval(&state).unwrap());
unimplemented!("not yet any way to actually specify the data path");
} }
#[test] #[test]
fn function_file_path_eval_should_return_true_if_given_a_plugin_that_is_ghosted() { 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] #[test]
#[allow(non_snake_case)] #[allow(non_snake_case)]
fn function_file_path_eval_should_be_true_if_given_LOOT() { 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] #[test]
fn function_file_path_eval_should_not_check_for_ghosted_non_plugin_file() { 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] #[test]
fn function_file_path_eval_should_error_if_the_path_is_outside_game_directory() { 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] #[test]
fn function_file_path_eval_should_return_false_if_the_file_does_not_exist() { fn function_file_path_eval_should_return_false_if_the_file_does_not_exist() {
let function = Function::FilePath(PathBuf::from("missing")); let function = Function::FilePath(PathBuf::from("missing"));
let state = state(".");
assert!(!function.eval().unwrap()); assert!(!function.eval(&state).unwrap());
} }
#[test] #[test]
+3 -5
View File
@@ -47,7 +47,7 @@ fn parse_version_args(input: &str) -> IResult<&str, (PathBuf, &str, ComparisonOp
>> tag!("\"") >> tag!("\"")
>> ws!(tag!(",")) >> ws!(tag!(","))
>> operator: call!(ComparisonOperator::parse) >> 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!("\"") >> tag!("\"")
>> ws!(tag!(",")) >> ws!(tag!(","))
>> crc: flat_map!(call!(hex_digit), parse_crc) >> 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; let result = Function::parse("many(\"Cargo.*\")").unwrap().1;
match result { match result {
Function::Many(r) => { Function::Many(r) => assert_eq!(Regex::new("Cargo.*").unwrap().as_str(), r.as_str()),
assert_eq!(Regex::new("Cargo.*").unwrap().as_str(), r.as_str())
},
_ => panic!("Expected a many function"), _ => panic!("Expected a many function"),
} }
} }
+83 -18
View File
@@ -2,10 +2,15 @@
extern crate nom; extern crate nom;
extern crate regex; extern crate regex;
use nom::{Err, IResult}; #[cfg(test)]
extern crate tempfile;
use std::io;
use std::path::PathBuf; use std::path::PathBuf;
use std::str; use std::str;
use nom::{Err, IResult};
mod function; mod function;
use function::Function; use function::Function;
@@ -15,6 +20,7 @@ pub enum Error {
ParsingError, ParsingError,
InvalidPath(PathBuf), InvalidPath(PathBuf),
InvalidRegex(String), InvalidRegex(String),
IoError(io::Error),
} }
impl<I> From<Err<I>> for Error { impl<I> From<Err<I>> for Error {
@@ -26,14 +32,45 @@ impl<I> From<Err<I>> for Error {
} }
} }
impl From<io::Error> 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' // Compound conditions joined by 'or'
#[derive(Debug)] #[derive(Debug)]
pub struct Expression(Vec<CompoundCondition>); pub struct Expression(Vec<CompoundCondition>);
impl Expression { impl Expression {
pub fn eval(&self) -> Result<bool, Error> { pub fn eval(&self, state: &State) -> Result<bool, Error> {
for compound_condition in &self.0 { for compound_condition in &self.0 {
if compound_condition.eval()? { if compound_condition.eval(state)? {
return Ok(true); return Ok(true);
} }
} }
@@ -55,9 +92,9 @@ impl Expression {
struct CompoundCondition(Vec<Condition>); struct CompoundCondition(Vec<Condition>);
impl CompoundCondition { impl CompoundCondition {
fn eval(&self) -> Result<bool, Error> { fn eval(&self, state: &State) -> Result<bool, Error> {
for condition in &self.0 { for condition in &self.0 {
if !condition.eval()? { if !condition.eval(state)? {
return Ok(false); return Ok(false);
} }
} }
@@ -81,11 +118,11 @@ enum Condition {
} }
impl Condition { impl Condition {
fn eval(&self) -> Result<bool, Error> { fn eval(&self, state: &State) -> Result<bool, Error> {
match *self { match *self {
Condition::Function(ref f) => f.eval(), Condition::Function(ref f) => f.eval(state),
Condition::InvertedFunction(ref f) => f.eval().map(|r| !r), Condition::InvertedFunction(ref f) => f.eval(state).map(|r| !r),
Condition::Expression(ref e) => e.eval(), Condition::Expression(ref e) => e.eval(state),
} }
} }
@@ -112,6 +149,20 @@ impl Condition {
mod tests { mod tests {
use super::*; use super::*;
use std::fs::create_dir;
fn state<T: Into<PathBuf>>(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] #[test]
fn expression_parse_should_handle_a_single_compound_condition() { fn expression_parse_should_handle_a_single_compound_condition() {
let result = Expression::parse("file(\"Cargo.toml\")").unwrap().1; let result = Expression::parse("file(\"Cargo.toml\")").unwrap().1;
@@ -216,58 +267,70 @@ mod tests {
#[test] #[test]
fn condition_eval_should_return_function_eval_for_a_function_condition() { fn condition_eval_should_return_function_eval_for_a_function_condition() {
let state = state(".");
let condition = Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))); 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"))); let condition = Condition::Function(Function::FilePath(PathBuf::from("missing")));
assert!(!condition.eval().unwrap()); assert!(!condition.eval(&state).unwrap());
} }
#[test] #[test]
fn condition_eval_should_return_expression_eval_for_an_expression_condition() { fn condition_eval_should_return_expression_eval_for_an_expression_condition() {
let state = state(".");
let condition = Condition::Expression(Expression(vec![CompoundCondition(vec![ let condition = Condition::Expression(Expression(vec![CompoundCondition(vec![
Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))), Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))),
])])); ])]));
assert!(condition.eval().unwrap()); assert!(condition.eval(&state).unwrap());
} }
#[test] #[test]
fn condition_eval_should_return_inverse_of_function_eval_for_a_not_function_condition() { fn condition_eval_should_return_inverse_of_function_eval_for_a_not_function_condition() {
let state = state(".");
let condition = let condition =
Condition::InvertedFunction(Function::FilePath(PathBuf::from("Cargo.toml"))); 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"))); let condition = Condition::InvertedFunction(Function::FilePath(PathBuf::from("missing")));
assert!(condition.eval().unwrap()); assert!(condition.eval(&state).unwrap());
} }
#[test] #[test]
fn compound_condition_eval_should_be_true_if_all_conditions_are_true() { fn compound_condition_eval_should_be_true_if_all_conditions_are_true() {
let state = state(".");
let compound_condition = CompoundCondition(vec![ let compound_condition = CompoundCondition(vec![
Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))), Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))),
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] #[test]
fn compound_condition_eval_should_be_false_if_any_condition_is_false() { fn compound_condition_eval_should_be_false_if_any_condition_is_false() {
let state = state(".");
let compound_condition = CompoundCondition(vec![ let compound_condition = CompoundCondition(vec![
Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))), Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))),
Condition::Function(Function::FilePath(PathBuf::from("missing"))), Condition::Function(Function::FilePath(PathBuf::from("missing"))),
]); ]);
assert!(!compound_condition.eval().unwrap()); assert!(!compound_condition.eval(&state).unwrap());
} }
#[test] #[test]
fn expression_eval_should_be_true_if_any_compound_condition_is_true() { fn expression_eval_should_be_true_if_any_compound_condition_is_true() {
let state = state(".");
let expression = Expression(vec![ let expression = Expression(vec![
CompoundCondition(vec![Condition::Function(Function::FilePath( CompoundCondition(vec![Condition::Function(Function::FilePath(
PathBuf::from("Cargo.toml"), PathBuf::from("Cargo.toml"),
@@ -276,11 +339,13 @@ mod tests {
PathBuf::from("missing"), PathBuf::from("missing"),
))]), ))]),
]); ]);
assert!(expression.eval().unwrap()); assert!(expression.eval(&state).unwrap());
} }
#[test] #[test]
fn expression_eval_should_be_false_if_all_compound_conditions_are_false() { fn expression_eval_should_be_false_if_all_compound_conditions_are_false() {
let state = state(".");
let expression = Expression(vec![ let expression = Expression(vec![
CompoundCondition(vec![Condition::Function(Function::FilePath( CompoundCondition(vec![Condition::Function(Function::FilePath(
PathBuf::from("missing"), PathBuf::from("missing"),
@@ -289,6 +354,6 @@ mod tests {
PathBuf::from("missing"), PathBuf::from("missing"),
))]), ))]),
]); ]);
assert!(!expression.eval().unwrap()); assert!(!expression.eval(&state).unwrap());
} }
} }