From 3e1c2084e5e1ca68251cc05fe98830732481f5e9 Mon Sep 17 00:00:00 2001 From: Oliver Hamlet Date: Wed, 3 Oct 2018 19:21:45 +0100 Subject: [PATCH] Add evaluation of checksum() conditions --- Cargo.toml | 1 + src/function/eval.rs | 203 +++++++++++++++++++++++++++++++++++++++---- src/lib.rs | 8 +- 3 files changed, 196 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3e90c8c..1cd0930 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" authors = ["Oliver Hamlet "] [dependencies] +crc = "1.0.0" nom = "4.0.0" regex = "1.0.5" diff --git a/src/function/eval.rs b/src/function/eval.rs index 8cecf59..22ce64f 100644 --- a/src/function/eval.rs +++ b/src/function/eval.rs @@ -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 { - 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 { + 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 { Ok(false) } +fn lowercase(path: &Path) -> Option { + path.to_str().map(str::to_lowercase) +} + +fn evaluate_checksum(state: &State, file_path: &Path, crc: u32) -> Result { + 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 { // 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>(data_path: T, active_plugins: &[&str]) -> State { + state_with_loot_path_and_active_plugins(data_path, "", active_plugins) + } + + fn state_with_loot_path>(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>( + 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()); + } } diff --git a/src/lib.rs b/src/lib.rs index 8615f62..6955d84 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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, // Lowercased plugin filenames. + crc_cache: RwLock>, // 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(), } }