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/
**/*.rs.bk
Cargo.lock
/testing-plugins
+3
View File
@@ -6,3 +6,6 @@ authors = ["Oliver Hamlet <oliver.hamlet@gmail.com>"]
[dependencies]
nom = "4.0.0"
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
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 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 {
pub fn eval(&self) -> Result<bool, Error> {
pub fn eval(&self, state: &State) -> Result<bool, Error> {
// 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<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]
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]
+3 -5
View File
@@ -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"),
}
}
+83 -18
View File
@@ -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<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'
#[derive(Debug)]
pub struct Expression(Vec<CompoundCondition>);
impl Expression {
pub fn eval(&self) -> Result<bool, Error> {
pub fn eval(&self, state: &State) -> Result<bool, Error> {
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<Condition>);
impl CompoundCondition {
fn eval(&self) -> Result<bool, Error> {
fn eval(&self, state: &State) -> Result<bool, Error> {
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<bool, Error> {
fn eval(&self, state: &State) -> Result<bool, Error> {
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<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]
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());
}
}