2018-10-03 19:21:45 +01:00
|
|
|
extern crate crc;
|
2018-02-26 23:10:53 +00:00
|
|
|
#[macro_use]
|
|
|
|
|
extern crate nom;
|
|
|
|
|
extern crate regex;
|
|
|
|
|
|
2018-10-01 20:44:48 +01:00
|
|
|
#[cfg(test)]
|
|
|
|
|
extern crate tempfile;
|
|
|
|
|
|
2018-10-03 19:21:45 +01:00
|
|
|
use std::collections::{HashMap, HashSet};
|
2018-10-01 20:44:48 +01:00
|
|
|
use std::io;
|
2018-09-29 13:01:59 +01:00
|
|
|
use std::path::PathBuf;
|
2018-09-29 10:52:38 +01:00
|
|
|
use std::str;
|
2018-10-03 19:21:45 +01:00
|
|
|
use std::sync::RwLock;
|
2018-02-26 23:10:53 +00:00
|
|
|
|
2018-10-01 20:44:48 +01:00
|
|
|
use nom::{Err, IResult};
|
|
|
|
|
|
2018-09-29 13:01:59 +01:00
|
|
|
mod function;
|
|
|
|
|
use function::Function;
|
|
|
|
|
|
2018-02-26 23:10:53 +00:00
|
|
|
#[derive(Debug)]
|
2018-09-29 13:01:59 +01:00
|
|
|
pub enum Error {
|
2018-02-26 23:10:53 +00:00
|
|
|
ParsingIncomplete,
|
|
|
|
|
ParsingError,
|
|
|
|
|
InvalidPath(PathBuf),
|
|
|
|
|
InvalidRegex(String),
|
2018-10-01 20:44:48 +01:00
|
|
|
IoError(io::Error),
|
2018-02-26 23:10:53 +00:00
|
|
|
}
|
|
|
|
|
|
2018-09-29 13:16:58 +01:00
|
|
|
impl<I> From<Err<I>> for Error {
|
|
|
|
|
fn from(error: Err<I>) -> Self {
|
2018-02-26 23:10:53 +00:00
|
|
|
match error {
|
2018-09-29 13:16:58 +01:00
|
|
|
Err::Incomplete(_) => Error::ParsingIncomplete,
|
|
|
|
|
_ => Error::ParsingError,
|
2018-02-26 23:10:53 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-01 20:44:48 +01:00
|
|
|
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,
|
2018-10-03 19:21:45 +01:00
|
|
|
loot_path: PathBuf,
|
2018-10-03 18:31:26 +01:00
|
|
|
active_plugins: HashSet<String>, // Lowercased plugin filenames.
|
2018-10-03 19:21:45 +01:00
|
|
|
crc_cache: RwLock<HashMap<String, u32>>, // Lowercased paths.
|
2018-10-01 20:44:48 +01:00
|
|
|
}
|
|
|
|
|
|
2018-02-26 23:10:53 +00:00
|
|
|
// Compound conditions joined by 'or'
|
2018-09-29 10:52:38 +01:00
|
|
|
#[derive(Debug)]
|
2018-09-29 13:01:59 +01:00
|
|
|
pub struct Expression(Vec<CompoundCondition>);
|
2018-02-26 23:10:53 +00:00
|
|
|
|
|
|
|
|
impl Expression {
|
2018-10-01 20:44:48 +01:00
|
|
|
pub fn eval(&self, state: &State) -> Result<bool, Error> {
|
2018-02-26 23:10:53 +00:00
|
|
|
for compound_condition in &self.0 {
|
2018-10-01 20:44:48 +01:00
|
|
|
if compound_condition.eval(state)? {
|
2018-02-26 23:10:53 +00:00
|
|
|
return Ok(true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(false)
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-29 13:01:59 +01:00
|
|
|
pub fn parse(input: &str) -> IResult<&str, Expression> {
|
2018-09-29 10:52:38 +01:00
|
|
|
do_parse!(
|
|
|
|
|
input,
|
|
|
|
|
compound_conditions:
|
|
|
|
|
separated_list_complete!(ws!(tag!("or")), CompoundCondition::parse)
|
|
|
|
|
>> (Expression(compound_conditions))
|
|
|
|
|
)
|
2018-02-26 23:10:53 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Conditions joined by 'and'
|
2018-09-29 10:52:38 +01:00
|
|
|
#[derive(Debug)]
|
2018-02-26 23:10:53 +00:00
|
|
|
struct CompoundCondition(Vec<Condition>);
|
|
|
|
|
|
|
|
|
|
impl CompoundCondition {
|
2018-10-01 20:44:48 +01:00
|
|
|
fn eval(&self, state: &State) -> Result<bool, Error> {
|
2018-02-26 23:10:53 +00:00
|
|
|
for condition in &self.0 {
|
2018-10-01 20:44:48 +01:00
|
|
|
if !condition.eval(state)? {
|
2018-02-26 23:10:53 +00:00
|
|
|
return Ok(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse(input: &str) -> IResult<&str, CompoundCondition> {
|
2018-09-29 10:52:38 +01:00
|
|
|
do_parse!(
|
|
|
|
|
input,
|
|
|
|
|
conditions: separated_list_complete!(ws!(tag!("and")), Condition::parse)
|
|
|
|
|
>> (CompoundCondition(conditions))
|
|
|
|
|
)
|
2018-02-26 23:10:53 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-29 10:52:38 +01:00
|
|
|
#[derive(Debug)]
|
2018-02-26 23:10:53 +00:00
|
|
|
enum Condition {
|
|
|
|
|
Function(Function),
|
2018-09-29 10:52:38 +01:00
|
|
|
InvertedFunction(Function),
|
2018-02-26 23:10:53 +00:00
|
|
|
Expression(Expression),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Condition {
|
2018-10-01 20:44:48 +01:00
|
|
|
fn eval(&self, state: &State) -> Result<bool, Error> {
|
2018-02-26 23:10:53 +00:00
|
|
|
match *self {
|
2018-10-01 20:44:48 +01:00
|
|
|
Condition::Function(ref f) => f.eval(state),
|
|
|
|
|
Condition::InvertedFunction(ref f) => f.eval(state).map(|r| !r),
|
|
|
|
|
Condition::Expression(ref e) => e.eval(state),
|
2018-02-26 23:10:53 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse(input: &str) -> IResult<&str, Condition> {
|
2018-09-29 10:52:38 +01:00
|
|
|
do_parse!(
|
|
|
|
|
input,
|
|
|
|
|
condition:
|
|
|
|
|
alt!(
|
|
|
|
|
call!(Function::parse) => {
|
|
|
|
|
|f| Condition::Function(f)
|
|
|
|
|
} |
|
|
|
|
|
preceded!(ws!(tag!("not")), call!(Function::parse)) => {
|
|
|
|
|
|f| Condition::InvertedFunction(f)
|
|
|
|
|
} |
|
|
|
|
|
delimited!(tag!("("), call!(Expression::parse), tag!(")")) => {
|
|
|
|
|
|e| Condition::Expression(e)
|
|
|
|
|
}
|
|
|
|
|
) >> (condition)
|
|
|
|
|
)
|
2018-02-26 23:10:53 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
2018-10-01 20:44:48 +01:00
|
|
|
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,
|
2018-10-03 19:21:45 +01:00
|
|
|
loot_path: PathBuf::new(),
|
2018-10-03 18:31:26 +01:00
|
|
|
active_plugins: HashSet::new(),
|
2018-10-03 19:21:45 +01:00
|
|
|
crc_cache: RwLock::default(),
|
2018-10-01 20:44:48 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-29 10:52:38 +01:00
|
|
|
#[test]
|
|
|
|
|
fn expression_parse_should_handle_a_single_compound_condition() {
|
2018-09-29 13:50:42 +01:00
|
|
|
let result = Expression::parse("file(\"Cargo.toml\")").unwrap().1;
|
2018-09-29 10:52:38 +01:00
|
|
|
|
|
|
|
|
match result.0.as_slice() {
|
|
|
|
|
[CompoundCondition(_)] => {}
|
|
|
|
|
_ => panic!("Expected an expression with one compound condition"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn expression_parse_should_handle_multiple_compound_conditions() {
|
|
|
|
|
let result = Expression::parse("file(\"Cargo.toml\") or file(\"Cargo.toml\")")
|
2018-09-29 13:50:42 +01:00
|
|
|
.unwrap()
|
|
|
|
|
.1;
|
2018-09-29 10:52:38 +01:00
|
|
|
|
|
|
|
|
match result.0.as_slice() {
|
|
|
|
|
[CompoundCondition(_), CompoundCondition(_)] => {}
|
|
|
|
|
v => panic!(
|
|
|
|
|
"Expected an expression with two compound conditions, got {:?}",
|
|
|
|
|
v
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn compound_condition_parse_should_handle_a_single_condition() {
|
2018-09-29 13:50:42 +01:00
|
|
|
let result = CompoundCondition::parse("file(\"Cargo.toml\")").unwrap().1;
|
2018-09-29 10:52:38 +01:00
|
|
|
|
|
|
|
|
match result.0.as_slice() {
|
|
|
|
|
[Condition::Function(Function::FilePath(f))] => {
|
|
|
|
|
assert_eq!(&PathBuf::from("Cargo.toml"), f)
|
|
|
|
|
}
|
|
|
|
|
v => panic!(
|
|
|
|
|
"Expected an expression with two compound conditions, got {:?}",
|
|
|
|
|
v
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn compound_condition_parse_should_handle_multiple_conditions() {
|
|
|
|
|
let result = CompoundCondition::parse("file(\"Cargo.toml\") and file(\"README.md\")")
|
2018-09-29 13:50:42 +01:00
|
|
|
.unwrap()
|
|
|
|
|
.1;
|
2018-09-29 10:52:38 +01:00
|
|
|
|
|
|
|
|
match result.0.as_slice() {
|
|
|
|
|
[Condition::Function(Function::FilePath(f1)), Condition::Function(Function::FilePath(f2))] =>
|
|
|
|
|
{
|
|
|
|
|
assert_eq!(&PathBuf::from("Cargo.toml"), f1);
|
|
|
|
|
assert_eq!(&PathBuf::from("README.md"), f2);
|
|
|
|
|
}
|
|
|
|
|
v => panic!(
|
|
|
|
|
"Expected an expression with two compound conditions, got {:?}",
|
|
|
|
|
v
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn condition_parse_should_handle_a_function() {
|
2018-09-29 13:50:42 +01:00
|
|
|
let result = Condition::parse("file(\"Cargo.toml\")").unwrap().1;
|
2018-09-29 10:52:38 +01:00
|
|
|
|
|
|
|
|
match result {
|
|
|
|
|
Condition::Function(Function::FilePath(f)) => {
|
|
|
|
|
assert_eq!(PathBuf::from("Cargo.toml"), f)
|
|
|
|
|
}
|
|
|
|
|
v => panic!(
|
|
|
|
|
"Expected an expression with two compound conditions, got {:?}",
|
|
|
|
|
v
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn condition_parse_should_handle_a_inverted_function() {
|
2018-09-29 13:50:42 +01:00
|
|
|
let result = Condition::parse("not file(\"Cargo.toml\")").unwrap().1;
|
2018-09-29 10:52:38 +01:00
|
|
|
|
|
|
|
|
match result {
|
|
|
|
|
Condition::InvertedFunction(Function::FilePath(f)) => {
|
|
|
|
|
assert_eq!(PathBuf::from("Cargo.toml"), f)
|
|
|
|
|
}
|
|
|
|
|
v => panic!(
|
|
|
|
|
"Expected an expression with two compound conditions, got {:?}",
|
|
|
|
|
v
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn condition_parse_should_handle_an_expression_in_parentheses() {
|
2018-09-29 13:50:42 +01:00
|
|
|
let result = Condition::parse("(not file(\"Cargo.toml\"))").unwrap().1;
|
2018-09-29 10:52:38 +01:00
|
|
|
|
|
|
|
|
match result {
|
|
|
|
|
Condition::Expression(_) => {}
|
|
|
|
|
v => panic!(
|
|
|
|
|
"Expected an expression with two compound conditions, got {:?}",
|
|
|
|
|
v
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-26 23:10:53 +00:00
|
|
|
#[test]
|
|
|
|
|
fn condition_eval_should_return_function_eval_for_a_function_condition() {
|
2018-10-01 20:44:48 +01:00
|
|
|
let state = state(".");
|
|
|
|
|
|
2018-02-26 23:10:53 +00:00
|
|
|
let condition = Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml")));
|
|
|
|
|
|
2018-10-01 20:44:48 +01:00
|
|
|
assert!(condition.eval(&state).unwrap());
|
2018-02-26 23:10:53 +00:00
|
|
|
|
|
|
|
|
let condition = Condition::Function(Function::FilePath(PathBuf::from("missing")));
|
|
|
|
|
|
2018-10-01 20:44:48 +01:00
|
|
|
assert!(!condition.eval(&state).unwrap());
|
2018-02-26 23:10:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn condition_eval_should_return_expression_eval_for_an_expression_condition() {
|
2018-10-01 20:44:48 +01:00
|
|
|
let state = state(".");
|
|
|
|
|
|
2018-09-29 10:52:38 +01:00
|
|
|
let condition = Condition::Expression(Expression(vec![CompoundCondition(vec![
|
|
|
|
|
Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))),
|
|
|
|
|
])]));
|
2018-02-26 23:10:53 +00:00
|
|
|
|
2018-10-01 20:44:48 +01:00
|
|
|
assert!(condition.eval(&state).unwrap());
|
2018-02-26 23:10:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn condition_eval_should_return_inverse_of_function_eval_for_a_not_function_condition() {
|
2018-10-01 20:44:48 +01:00
|
|
|
let state = state(".");
|
|
|
|
|
|
2018-09-29 10:52:38 +01:00
|
|
|
let condition =
|
|
|
|
|
Condition::InvertedFunction(Function::FilePath(PathBuf::from("Cargo.toml")));
|
2018-02-26 23:10:53 +00:00
|
|
|
|
2018-10-01 20:44:48 +01:00
|
|
|
assert!(!condition.eval(&state).unwrap());
|
2018-02-26 23:10:53 +00:00
|
|
|
|
2018-09-29 10:52:38 +01:00
|
|
|
let condition = Condition::InvertedFunction(Function::FilePath(PathBuf::from("missing")));
|
2018-02-26 23:10:53 +00:00
|
|
|
|
2018-10-01 20:44:48 +01:00
|
|
|
assert!(condition.eval(&state).unwrap());
|
2018-02-26 23:10:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn compound_condition_eval_should_be_true_if_all_conditions_are_true() {
|
2018-10-01 20:44:48 +01:00
|
|
|
let state = state(".");
|
|
|
|
|
|
2018-02-26 23:10:53 +00:00
|
|
|
let compound_condition = CompoundCondition(vec![
|
|
|
|
|
Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))),
|
|
|
|
|
Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))),
|
|
|
|
|
]);
|
|
|
|
|
|
2018-10-01 20:44:48 +01:00
|
|
|
assert!(compound_condition.eval(&state).unwrap());
|
2018-02-26 23:10:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn compound_condition_eval_should_be_false_if_any_condition_is_false() {
|
2018-10-01 20:44:48 +01:00
|
|
|
let state = state(".");
|
|
|
|
|
|
2018-02-26 23:10:53 +00:00
|
|
|
let compound_condition = CompoundCondition(vec![
|
|
|
|
|
Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))),
|
|
|
|
|
Condition::Function(Function::FilePath(PathBuf::from("missing"))),
|
|
|
|
|
]);
|
|
|
|
|
|
2018-10-01 20:44:48 +01:00
|
|
|
assert!(!compound_condition.eval(&state).unwrap());
|
2018-02-26 23:10:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn expression_eval_should_be_true_if_any_compound_condition_is_true() {
|
2018-10-01 20:44:48 +01:00
|
|
|
let state = state(".");
|
|
|
|
|
|
2018-02-26 23:10:53 +00:00
|
|
|
let expression = Expression(vec![
|
2018-09-29 10:52:38 +01:00
|
|
|
CompoundCondition(vec![Condition::Function(Function::FilePath(
|
|
|
|
|
PathBuf::from("Cargo.toml"),
|
|
|
|
|
))]),
|
|
|
|
|
CompoundCondition(vec![Condition::Function(Function::FilePath(
|
|
|
|
|
PathBuf::from("missing"),
|
|
|
|
|
))]),
|
2018-02-26 23:10:53 +00:00
|
|
|
]);
|
2018-10-01 20:44:48 +01:00
|
|
|
assert!(expression.eval(&state).unwrap());
|
2018-02-26 23:10:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn expression_eval_should_be_false_if_all_compound_conditions_are_false() {
|
2018-10-01 20:44:48 +01:00
|
|
|
let state = state(".");
|
|
|
|
|
|
2018-02-26 23:10:53 +00:00
|
|
|
let expression = Expression(vec![
|
2018-09-29 10:52:38 +01:00
|
|
|
CompoundCondition(vec![Condition::Function(Function::FilePath(
|
|
|
|
|
PathBuf::from("missing"),
|
|
|
|
|
))]),
|
|
|
|
|
CompoundCondition(vec![Condition::Function(Function::FilePath(
|
|
|
|
|
PathBuf::from("missing"),
|
|
|
|
|
))]),
|
2018-02-26 23:10:53 +00:00
|
|
|
]);
|
2018-10-01 20:44:48 +01:00
|
|
|
assert!(!expression.eval(&state).unwrap());
|
2018-02-26 23:10:53 +00:00
|
|
|
}
|
|
|
|
|
}
|