From 30e3ed9c863fcbe80b834f4bd9ce05120501b09e Mon Sep 17 00:00:00 2001 From: Oliver Hamlet Date: Sat, 15 Feb 2025 18:28:39 +0000 Subject: [PATCH] Add file_size() function It evaluates to false if the file doesn't exist. --- src/function/eval.rs | 80 +++++++++++++++++++++++++++++++++++++++++++ src/function/mod.rs | 74 +++++++++++++++++++++++++++++++++++++-- src/function/parse.rs | 50 +++++++++++++++++++++++++++ 3 files changed, 202 insertions(+), 2 deletions(-) diff --git a/src/function/eval.rs b/src/function/eval.rs index 6472303..5495337 100644 --- a/src/function/eval.rs +++ b/src/function/eval.rs @@ -64,6 +64,12 @@ fn evaluate_file_regex(state: &State, parent_path: &Path, regex: &Regex) -> Resu ) } +fn evaluate_file_size(state: &State, path: &Path, size: u64) -> Result { + std::fs::metadata(resolve_path(state, path)) + .map(|m| m.len() == size) + .or(Ok(false)) +} + fn evaluate_readable(state: &State, path: &Path) -> Result { if path.is_dir() { Ok(read_dir(resolve_path(state, path)).is_ok()) @@ -283,6 +289,7 @@ impl Function { let result = match self { Function::FilePath(f) => evaluate_file_path(state, f), Function::FileRegex(p, r) => evaluate_file_regex(state, p, r), + Function::FileSize(p, s) => evaluate_file_size(state, p, *s), Function::Readable(p) => evaluate_readable(state, p), Function::IsExecutable(p) => evaluate_is_executable(state, p), Function::ActivePath(p) => evaluate_active_path(state, p), @@ -502,6 +509,79 @@ mod tests { assert!(function.eval(&state).unwrap()); } + #[test] + fn function_file_size_eval_should_return_false_if_file_does_not_exist() { + let function = Function::FileSize("missing.esp".into(), 55); + let state = state_with_data( + "./src", + vec!["./tests/testing-plugins/Oblivion/Data"], + &[], + &[], + ); + + assert!(!function.eval(&state).unwrap()); + } + + #[test] + fn function_file_size_eval_should_return_false_if_file_size_is_different() { + let function = Function::FileSize("Blank.esp".into(), 10); + let state = state_with_data( + "./src", + vec!["./tests/testing-plugins/Oblivion/Data"], + &[], + &[], + ); + + assert!(!function.eval(&state).unwrap()); + } + + #[test] + fn function_file_size_eval_should_return_true_if_file_size_is_equal() { + let function = Function::FileSize("Blank.esp".into(), 55); + let state = state_with_data( + "./src", + vec!["./tests/testing-plugins/Oblivion/Data"], + &[], + &[], + ); + + assert!(function.eval(&state).unwrap()); + } + + #[test] + fn function_file_size_eval_should_return_true_if_given_a_plugin_that_is_ghosted() { + let tmp_dir = tempdir().unwrap(); + let data_path = tmp_dir.path().join("Data"); + let state = state(data_path); + + copy( + Path::new("tests/testing-plugins/Oblivion/Data/Blank.esp"), + state.data_path.join("Blank.esp.ghost"), + ) + .unwrap(); + + let function = Function::FileSize(PathBuf::from("Blank.esp"), 55); + + assert!(function.eval(&state).unwrap()); + } + + #[test] + fn function_file_size_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("tests/testing-plugins/Oblivion/Data/Blank.bsa"), + state.data_path.join("Blank.bsa.ghost"), + ) + .unwrap(); + + let function = Function::FileSize(PathBuf::from("Blank.bsa"), 736); + + assert!(!function.eval(&state).unwrap()); + } + #[test] fn function_readable_eval_should_be_true_for_a_file_that_can_be_opened_as_read_only() { let function = Function::Readable(PathBuf::from("Cargo.toml")); diff --git a/src/function/mod.rs b/src/function/mod.rs index 8ccf81a..4081f69 100644 --- a/src/function/mod.rs +++ b/src/function/mod.rs @@ -39,6 +39,7 @@ impl fmt::Display for ComparisonOperator { pub enum Function { FilePath(PathBuf), FileRegex(PathBuf, Regex), + FileSize(PathBuf, u64), Readable(PathBuf), IsExecutable(PathBuf), ActivePath(PathBuf), @@ -57,6 +58,7 @@ impl fmt::Display for Function { match self { FilePath(p) => write!(f, "file(\"{}\")", p.display()), FileRegex(p, r) => write!(f, "file(\"{}/{}\")", p.display(), r), + FileSize(p, s) => write!(f, "file_size(\"{}\", {})", p.display(), s), Readable(p) => write!(f, "readable(\"{}\")", p.display()), IsExecutable(p) => write!(f, "is_executable(\"{}\")", p.display()), ActivePath(p) => write!(f, "active(\"{}\")", p.display()), @@ -81,6 +83,9 @@ impl PartialEq for Function { (FileRegex(p1, r1), FileRegex(p2, r2)) => { eq(r1.as_str(), r2.as_str()) && eq(&p1.to_string_lossy(), &p2.to_string_lossy()) } + (FileSize(p1, s1), FileSize(p2, s2)) => { + s1 == s2 && eq(&p1.to_string_lossy(), &p2.to_string_lossy()) + } (Readable(p1), Readable(p2)) => eq(&p1.to_string_lossy(), &p2.to_string_lossy()), (IsExecutable(p1), IsExecutable(p2)) => { eq(&p1.to_string_lossy(), &p2.to_string_lossy()) @@ -119,6 +124,10 @@ impl Hash for Function { p.to_string_lossy().to_lowercase().hash(state); r.as_str().to_lowercase().hash(state); } + FileSize(p, s) => { + p.to_string_lossy().to_lowercase().hash(state); + s.hash(state); + } Readable(p) => { p.to_string_lossy().to_lowercase().hash(state); } @@ -186,6 +195,16 @@ mod tests { assert_eq!("file(\"subdir/Blank.*\")", &format!("{}", function)); } + #[test] + fn function_fmt_for_file_size_should_format_correctly() { + let function = Function::FileSize("subdir/Blank.esm".into(), 12345678); + + assert_eq!( + "file_size(\"subdir/Blank.esm\", 12345678)", + &format!("{}", function) + ); + } + #[test] fn function_fmt_for_readable_should_format_correctly() { let function = Function::Readable("subdir/Blank.esm".into()); @@ -326,6 +345,31 @@ mod tests { ); } + #[test] + fn function_eq_for_file_size_should_check_pathbuf_and_size() { + assert_eq!( + Function::FileSize("subdir".into(), 1), + Function::FileSize("subdir".into(), 1) + ); + + assert_ne!( + Function::FileSize("subdir".into(), 1), + Function::FileSize("other".into(), 1) + ); + assert_ne!( + Function::FileSize("subdir".into(), 1), + Function::FileSize("subdir".into(), 2) + ); + } + + #[test] + fn function_eq_for_file_size_should_be_case_insensitive_on_pathbuf() { + assert_eq!( + Function::FileSize("subdir".into(), 1), + Function::FileSize("Subdir".into(), 1) + ); + } + #[test] fn function_eq_for_readable_should_check_pathbuf() { assert_eq!( @@ -512,8 +556,8 @@ mod tests { #[test] fn function_eq_for_many_should_be_case_insensitive_on_pathbuf_and_regex() { assert_eq!( - Function::FileRegex("subdir".into(), regex("blank.*")), - Function::FileRegex("Subdir".into(), regex("Blank.*")) + Function::Many("subdir".into(), regex("blank.*")), + Function::Many("Subdir".into(), regex("Blank.*")) ); } @@ -700,6 +744,32 @@ mod tests { assert_eq!(hash(function1), hash(function2)); } + #[test] + fn function_hash_file_size_should_hash_pathbuf_and_size() { + let function1 = Function::FileSize("subdir".into(), 1); + let function2 = Function::FileSize("subdir".into(), 1); + + assert_eq!(hash(function1), hash(function2)); + + let function1 = Function::FileSize("subdir".into(), 1); + let function2 = Function::FileSize("other".into(), 1); + + assert_ne!(hash(function1), hash(function2)); + + let function1 = Function::FileSize("subdir".into(), 1); + let function2 = Function::FileSize("subdir".into(), 2); + + assert_ne!(hash(function1), hash(function2)); + } + + #[test] + fn function_hash_file_size_should_be_case_insensitive() { + let function1 = Function::FileSize("Subdir".into(), 1); + let function2 = Function::FileSize("subdir".into(), 1); + + assert_eq!(hash(function1), hash(function2)); + } + #[test] fn function_hash_readable_should_hash_pathbuf() { let function1 = Function::Readable("Blank.esm".into()); diff --git a/src/function/parse.rs b/src/function/parse.rs index 2c50715..4fb4f4b 100644 --- a/src/function/parse.rs +++ b/src/function/parse.rs @@ -3,6 +3,7 @@ use std::str; use nom::branch::alt; use nom::bytes::complete::{is_not, tag}; +use nom::character::complete::digit1; use nom::character::complete::hex_digit1; use nom::combinator::{map, map_parser, value}; use nom::sequence::delimited; @@ -65,6 +66,28 @@ fn parse_path(input: &str) -> IResult<&str, PathBuf> { .parse(input) } +fn parse_size(input: &str) -> ParsingResult { + str::parse(input) + .map(|c| ("", c)) + .map_err(|e| Err::Failure(ParsingErrorKind::from(e).at(input))) +} + +fn parse_file_size_args(input: &str) -> ParsingResult<(PathBuf, u64)> { + let mut parser = ( + map_err(parse_path), + map_err(whitespace(tag(","))), + map_parser(digit1, parse_size), + ); + + let (remaining_input, (path, _, size)) = parser.parse(input)?; + + if is_in_game_path(&path) { + Ok((remaining_input, (path, size))) + } else { + Err(not_in_game_directory(input, path)) + } +} + fn parse_version_args(input: &str) -> ParsingResult<(PathBuf, String, ComparisonOperator)> { let version_parser = map( delimited(tag("\""), is_not("\""), tag("\"")), @@ -173,6 +196,14 @@ impl Function { ), |(path, regex)| Function::FileRegex(path, regex), ), + map( + delimited( + map_err(tag("file_size(")), + parse_file_size_args, + map_err(tag(")")), + ), + |(path, size)| Function::FileSize(path, size), + ), map( delimited( map_err(tag("readable(\"")), @@ -332,6 +363,25 @@ mod tests { assert!(Function::parse("file(\"../../Cargo.*\")").is_err()); } + #[test] + fn function_parse_should_parse_a_file_size_function() { + let output = Function::parse("file_size(\"Cargo.toml\", 1234)").unwrap(); + + assert!(output.0.is_empty()); + match output.1 { + Function::FileSize(f, s) => { + assert_eq!(Path::new("Cargo.toml"), f); + assert_eq!(1234, s); + } + _ => panic!("Expected a file size function"), + } + } + + #[test] + fn function_parse_should_error_if_the_file_size_is_outside_the_game_directory() { + assert!(Function::parse("file_size(\"../../Cargo.toml\", 1234)").is_err()); + } + #[test] fn function_parse_should_parse_a_readable_function() { let output = Function::parse("readable(\"Cargo.toml\")").unwrap();