mirror of
https://github.com/loot/loot-condition-interpreter.git
synced 2026-07-27 14:16:09 -07:00
Add evaluation of checksum() conditions
This commit is contained in:
@@ -4,6 +4,7 @@ version = "0.1.0"
|
||||
authors = ["Oliver Hamlet <oliver.hamlet@gmail.com>"]
|
||||
|
||||
[dependencies]
|
||||
crc = "1.0.0"
|
||||
nom = "4.0.0"
|
||||
regex = "1.0.5"
|
||||
|
||||
|
||||
+188
-15
@@ -1,7 +1,10 @@
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::read_dir;
|
||||
use std::fs::{read_dir, File};
|
||||
use std::hash::Hasher;
|
||||
use std::io::{BufReader, Read};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crc::{crc32, Hasher32};
|
||||
use regex::Regex;
|
||||
|
||||
use super::Function;
|
||||
@@ -35,21 +38,24 @@ fn equals(path: &Path, test: &str) -> bool {
|
||||
path.to_str().map(|s| s == test).unwrap_or(false)
|
||||
}
|
||||
|
||||
fn evaluate_file_path(state: &State, file_path: &Path) -> Result<bool, Error> {
|
||||
if equals(file_path, "LOOT") {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
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())
|
||||
fn resolve_path(state: &State, path: &Path) -> PathBuf {
|
||||
if equals(path, "LOOT") {
|
||||
state.loot_path.clone()
|
||||
} else {
|
||||
Ok(exists)
|
||||
let path = state.data_path.join(path);
|
||||
|
||||
if !path.exists() && has_plugin_file_extension(&path, state) {
|
||||
add_extension(&path, "ghost")
|
||||
} else {
|
||||
path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_file_path(state: &State, file_path: &Path) -> Result<bool, Error> {
|
||||
Ok(resolve_path(state, file_path).exists())
|
||||
}
|
||||
|
||||
fn is_match(regex: &Regex, file_name: &OsStr) -> bool {
|
||||
file_name
|
||||
.to_str()
|
||||
@@ -118,10 +124,45 @@ fn evaluate_many_active(state: &State, regex: &Regex) -> Result<bool, Error> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn lowercase(path: &Path) -> Option<String> {
|
||||
path.to_str().map(str::to_lowercase)
|
||||
}
|
||||
|
||||
fn evaluate_checksum(state: &State, file_path: &Path, crc: u32) -> Result<bool, Error> {
|
||||
if let Ok(reader) = state.crc_cache.read() {
|
||||
if let Some(key) = lowercase(file_path) {
|
||||
if let Some(cached_crc) = reader.get(&key) {
|
||||
return Ok(*cached_crc == crc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let path = resolve_path(state, file_path);
|
||||
|
||||
if !path.exists() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let reader = BufReader::new(File::open(path)?);
|
||||
let mut digest = crc32::Digest::new(crc32::IEEE);
|
||||
|
||||
for byte in reader.bytes() {
|
||||
digest.write_u8(byte?);
|
||||
}
|
||||
|
||||
let calculated_crc = digest.sum32();
|
||||
if let Ok(mut writer) = state.crc_cache.write() {
|
||||
if let Some(key) = lowercase(file_path) {
|
||||
writer.insert(key, calculated_crc);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(calculated_crc == crc)
|
||||
}
|
||||
|
||||
impl Function {
|
||||
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) => evaluate_file_path(state, f),
|
||||
Function::FileRegex(ref p, ref r) => evaluate_file_regex(state, p, r),
|
||||
@@ -129,6 +170,7 @@ impl Function {
|
||||
Function::ActiveRegex(ref r) => evaluate_active_regex(state, r),
|
||||
Function::Many(ref p, ref r) => evaluate_many(state, p, r),
|
||||
Function::ManyActive(ref r) => evaluate_many_active(state, r),
|
||||
Function::Checksum(ref path, ref crc) => evaluate_checksum(state, path, *crc),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
@@ -139,6 +181,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::fs::{copy, create_dir};
|
||||
use std::sync::RwLock;
|
||||
|
||||
use regex::RegexBuilder;
|
||||
use tempfile::tempdir;
|
||||
@@ -150,6 +193,18 @@ mod tests {
|
||||
}
|
||||
|
||||
fn state_with_active_plugins<T: Into<PathBuf>>(data_path: T, active_plugins: &[&str]) -> State {
|
||||
state_with_loot_path_and_active_plugins(data_path, "", active_plugins)
|
||||
}
|
||||
|
||||
fn state_with_loot_path<T: Into<PathBuf>>(data_path: T, loot_path: &str) -> State {
|
||||
state_with_loot_path_and_active_plugins(data_path, loot_path, &[])
|
||||
}
|
||||
|
||||
fn state_with_loot_path_and_active_plugins<T: Into<PathBuf>>(
|
||||
data_path: T,
|
||||
loot_path: &str,
|
||||
active_plugins: &[&str],
|
||||
) -> State {
|
||||
let data_path = data_path.into();
|
||||
if !data_path.exists() {
|
||||
create_dir(&data_path).unwrap();
|
||||
@@ -158,10 +213,12 @@ mod tests {
|
||||
State {
|
||||
game_type: GameType::tes4,
|
||||
data_path: data_path,
|
||||
loot_path: loot_path.into(),
|
||||
active_plugins: active_plugins
|
||||
.into_iter()
|
||||
.map(|s| s.to_lowercase())
|
||||
.collect(),
|
||||
crc_cache: RwLock::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,13 +255,22 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
#[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_and_loot_path_exists() {
|
||||
let function = Function::FilePath(PathBuf::from("LOOT"));
|
||||
let state = state(".");
|
||||
let state = state_with_loot_path(".", "Cargo.toml");
|
||||
|
||||
assert!(function.eval(&state).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(non_snake_case)]
|
||||
fn function_file_path_eval_should_be_false_if_given_LOOT_and_loot_path_does_not_exist() {
|
||||
let function = Function::FilePath(PathBuf::from("LOOT"));
|
||||
let state = state_with_loot_path(".", "missing");
|
||||
|
||||
assert!(!function.eval(&state).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_file_path_eval_should_not_check_for_ghosted_non_plugin_file() {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
@@ -357,4 +423,111 @@ mod tests {
|
||||
|
||||
assert!(!function.eval(&state).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_checksum_eval_should_be_false_if_the_file_does_not_exist() {
|
||||
let function = Function::Checksum(PathBuf::from("missing"), 0x374E2A6F);
|
||||
let state = state(".");
|
||||
|
||||
assert!(!function.eval(&state).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_checksum_eval_should_be_false_if_the_file_checksum_does_not_equal_the_given_checksum(
|
||||
) {
|
||||
let function = Function::Checksum(
|
||||
PathBuf::from("testing-plugins/Oblivion/Data/Blank.esm"),
|
||||
0xDEADBEEF,
|
||||
);
|
||||
let state = state(".");
|
||||
|
||||
assert!(!function.eval(&state).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_checksum_eval_should_be_true_if_the_file_checksum_equals_the_given_checksum() {
|
||||
let function = Function::Checksum(
|
||||
PathBuf::from("testing-plugins/Oblivion/Data/Blank.esm"),
|
||||
0x374E2A6F,
|
||||
);
|
||||
let state = state(".");
|
||||
|
||||
assert!(function.eval(&state).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_checksum_eval_should_support_checking_the_crc_of_a_ghosted_plugin() {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let data_path = tmp_dir.path().join("Data");
|
||||
let state = state(data_path);
|
||||
|
||||
copy(
|
||||
Path::new("testing-plugins/Oblivion/Data/Blank.esm"),
|
||||
&state.data_path.join("Blank.esm.ghost"),
|
||||
).unwrap();
|
||||
|
||||
let function = Function::Checksum(PathBuf::from("Blank.esm"), 0x374E2A6F);
|
||||
|
||||
assert!(function.eval(&state).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_checksum_eval_should_not_check_for_ghosted_non_plugin_file() {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let data_path = tmp_dir.path().join("Data");
|
||||
let state = state(data_path);
|
||||
|
||||
copy(
|
||||
Path::new("testing-plugins/Oblivion/Data/Blank.bsa"),
|
||||
&state.data_path.join("Blank.bsa.ghost"),
|
||||
).unwrap();
|
||||
|
||||
let function = Function::Checksum(PathBuf::from("Blank.bsa"), 0x22AB79D9);
|
||||
|
||||
assert!(!function.eval(&state).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(non_snake_case)]
|
||||
fn function_checksum_eval_should_be_true_if_given_LOOT_crc_matches() {
|
||||
let function = Function::Checksum(PathBuf::from("LOOT"), 0x374E2A6F);
|
||||
let state = state_with_loot_path(".", "testing-plugins/Oblivion/Data/Blank.esm");
|
||||
|
||||
assert!(function.eval(&state).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(non_snake_case)]
|
||||
fn function_checksum_eval_should_be_false_if_given_LOOT_crc_does_not_match() {
|
||||
let function = Function::Checksum(PathBuf::from("LOOT"), 0xDEADBEEF);
|
||||
let state = state_with_loot_path(".", "testing-plugins/Oblivion/Data/Blank.esm");
|
||||
|
||||
assert!(!function.eval(&state).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_checksum_eval_should_cache_and_use_cached_crcs() {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let data_path = tmp_dir.path().join("Data");
|
||||
let state = state(data_path);
|
||||
|
||||
copy(
|
||||
Path::new("testing-plugins/Oblivion/Data/Blank.esm"),
|
||||
&state.data_path.join("Blank.esm"),
|
||||
).unwrap();
|
||||
|
||||
let function = Function::Checksum(PathBuf::from("Blank.esm"), 0x374E2A6F);
|
||||
|
||||
assert!(function.eval(&state).unwrap());
|
||||
|
||||
// Change the CRC of the file to test that the cached value is used.
|
||||
copy(
|
||||
Path::new("testing-plugins/Oblivion/Data/Blank.bsa"),
|
||||
&state.data_path.join("Blank.esm"),
|
||||
).unwrap();
|
||||
|
||||
let function = Function::Checksum(PathBuf::from("Blank.esm"), 0x374E2A6F);
|
||||
|
||||
assert!(function.eval(&state).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
+7
-1
@@ -1,3 +1,4 @@
|
||||
extern crate crc;
|
||||
#[macro_use]
|
||||
extern crate nom;
|
||||
extern crate regex;
|
||||
@@ -5,10 +6,11 @@ extern crate regex;
|
||||
#[cfg(test)]
|
||||
extern crate tempfile;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::str;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use nom::{Err, IResult};
|
||||
|
||||
@@ -62,7 +64,9 @@ impl GameType {
|
||||
pub struct State {
|
||||
game_type: GameType,
|
||||
data_path: PathBuf,
|
||||
loot_path: PathBuf,
|
||||
active_plugins: HashSet<String>, // Lowercased plugin filenames.
|
||||
crc_cache: RwLock<HashMap<String, u32>>, // Lowercased paths.
|
||||
}
|
||||
|
||||
// Compound conditions joined by 'or'
|
||||
@@ -162,7 +166,9 @@ mod tests {
|
||||
State {
|
||||
game_type: GameType::tes4,
|
||||
data_path: data_path,
|
||||
loot_path: PathBuf::new(),
|
||||
active_plugins: HashSet::new(),
|
||||
crc_cache: RwLock::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user