mirror of
https://github.com/loot/loot-condition-interpreter.git
synced 2026-07-27 14:16:09 -07:00
Implement file path function evaluation
This commit is contained in:
@@ -2,3 +2,4 @@
|
||||
/target/
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
/testing-plugins
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user