Add description_contains() condition function

It evaluates to true if the plugin description contains text that matches the given regex.
This commit is contained in:
Oliver Hamlet
2025-02-15 22:38:18 +00:00
parent f84083da52
commit 174ba55c95
3 changed files with 228 additions and 12 deletions
+72 -6
View File
@@ -106,11 +106,11 @@ fn evaluate_active_regex(state: &State, regex: &Regex) -> Result<bool, Error> {
Ok(state.active_plugins.iter().any(|p| regex.is_match(p)))
}
fn evaluate_is_master(state: &State, file_path: &Path) -> Result<bool, Error> {
fn parse_plugin(state: &State, file_path: &Path) -> Option<esplugin::Plugin> {
use esplugin::GameId;
let game_id = match state.game_type {
GameType::OpenMW => return Ok(false),
GameType::OpenMW => return None,
GameType::Morrowind => GameId::Morrowind,
GameType::Oblivion => GameId::Oblivion,
GameType::Skyrim => GameId::Skyrim,
@@ -125,10 +125,17 @@ fn evaluate_is_master(state: &State, file_path: &Path) -> Result<bool, Error> {
let mut plugin = esplugin::Plugin::new(game_id, &path);
plugin
.parse_file(ParseOptions::header_only())
.map(|_| plugin.is_master_file())
.or(Ok(false))
if plugin.parse_file(ParseOptions::header_only()).is_ok() {
Some(plugin)
} else {
None
}
}
fn evaluate_is_master(state: &State, file_path: &Path) -> Result<bool, Error> {
Ok(parse_plugin(state, file_path)
.map(|plugin| plugin.is_master_file())
.unwrap_or(false))
}
fn evaluate_many_active(state: &State, regex: &Regex) -> Result<bool, Error> {
@@ -281,6 +288,17 @@ fn evaluate_filename_version(
evaluate_dir_entries(state, parent_path, evaluator)
}
fn evaluate_description_contains(
state: &State,
file_path: &Path,
regex: &Regex,
) -> Result<bool, Error> {
Ok(parse_plugin(state, file_path)
.and_then(|plugin| plugin.description().unwrap_or(None))
.map(|description| regex.is_match(&description))
.unwrap_or(false))
}
impl Function {
pub fn eval(&self, state: &State) -> Result<bool, Error> {
if self.is_slow() {
@@ -308,6 +326,7 @@ impl Function {
evaluate_version(state, p, v, *c, |_, p| get_product_version(p))
}
Function::FilenameVersion(p, r, v, c) => evaluate_filename_version(state, p, r, v, *c),
Function::DescriptionContains(p, r) => evaluate_description_contains(state, p, r),
};
if self.is_slow() {
@@ -1580,4 +1599,51 @@ mod tests {
assert!(function.eval(&state).unwrap());
}
#[test]
fn function_description_contains_eval_should_return_false_if_the_file_does_not_exist() {
let state = state_with_versions("tests/testing-plugins/Oblivion/Data", &[]);
let function = Function::DescriptionContains("missing.esp".into(), regex("€ƒ."));
assert!(!function.eval(&state).unwrap());
}
#[test]
fn function_description_contains_eval_should_return_false_if_the_file_is_not_a_plugin() {
let state = state_with_versions("tests/testing-plugins/Oblivion/Data", &[]);
let function = Function::DescriptionContains("Blank.bsa".into(), regex("€ƒ."));
assert!(!function.eval(&state).unwrap());
}
#[test]
fn function_description_contains_eval_should_return_false_if_the_plugin_has_no_description() {
let state = state_with_versions("tests/testing-plugins/Oblivion/Data", &[]);
let function = Function::DescriptionContains("Blank - Different.esm".into(), regex("€ƒ."));
assert!(!function.eval(&state).unwrap());
}
#[test]
fn function_description_contains_eval_should_return_false_if_the_plugin_description_does_not_match(
) {
let state = state_with_versions("tests/testing-plugins/Oblivion/Data", &[]);
let function = Function::DescriptionContains("Blank.esm".into(), regex("€ƒ."));
assert!(!function.eval(&state).unwrap());
}
#[test]
fn function_description_contains_eval_should_return_true_if_the_plugin_description_contains_a_match(
) {
let state = state_with_versions("tests/testing-plugins/Oblivion/Data", &[]);
let function = Function::DescriptionContains("Blank.esp".into(), regex("ƒ"));
assert!(function.eval(&state).unwrap());
}
}
+89
View File
@@ -51,6 +51,7 @@ pub enum Function {
Version(PathBuf, String, ComparisonOperator),
ProductVersion(PathBuf, String, ComparisonOperator),
FilenameVersion(PathBuf, Regex, String, ComparisonOperator),
DescriptionContains(PathBuf, Regex),
}
impl fmt::Display for Function {
@@ -82,6 +83,9 @@ impl fmt::Display for Function {
c
)
}
DescriptionContains(p, r) => {
write!(f, "description_contains(\"{}\", \"{}\")", p.display(), r)
}
}
}
}
@@ -123,6 +127,9 @@ impl PartialEq for Function {
&& eq(r1.as_str(), r2.as_str())
&& eq(&p1.to_string_lossy(), &p2.to_string_lossy())
}
(DescriptionContains(p1, r1), DescriptionContains(p2, r2)) => {
eq(r1.as_str(), r2.as_str()) && eq(&p1.to_string_lossy(), &p2.to_string_lossy())
}
_ => false,
}
}
@@ -187,6 +194,10 @@ impl Hash for Function {
v.to_lowercase().hash(state);
c.hash(state);
}
DescriptionContains(p, r) => {
p.to_string_lossy().to_lowercase().hash(state);
r.as_str().to_lowercase().hash(state);
}
}
discriminant(self).hash(state);
@@ -332,6 +343,16 @@ mod tests {
&format!("{}", function)
);
}
#[test]
fn function_fmt_for_description_contains_should_format_correctly() {
let function = Function::DescriptionContains("Blank.esp".into(), regex("€ƒ."));
assert_eq!(
"description_contains(\"Blank.esp\", \"€ƒ.\")",
&format!("{}", function)
);
}
}
mod eq {
@@ -815,6 +836,40 @@ mod tests {
)
);
}
#[test]
fn function_eq_for_description_contains_should_check_pathbuf_and_regex() {
assert_eq!(
Function::DescriptionContains("Blank.esp".into(), regex("€ƒ.")),
Function::DescriptionContains("Blank.esp".into(), regex("€ƒ."))
);
assert_ne!(
Function::DescriptionContains("Blank.esp".into(), regex("€ƒ.")),
Function::DescriptionContains("Blank.esp".into(), regex(".*"))
);
assert_ne!(
Function::DescriptionContains("Blank.esp".into(), regex("€ƒ.")),
Function::DescriptionContains("other".into(), regex("€ƒ."))
);
}
#[test]
fn function_eq_for_description_contains_should_be_case_insensitive_on_pathbuf_and_regex() {
assert_eq!(
Function::DescriptionContains("Blank.esp".into(), regex("€ƒ.")),
Function::DescriptionContains("blank.esp".into(), regex("€Ƒ."))
);
}
#[test]
fn function_eq_description_contains_should_not_be_equal_to_file_regex_with_same_pathbuf_and_regex(
) {
assert_ne!(
Function::DescriptionContains("Blank.esp".into(), regex("€ƒ.")),
Function::FileRegex("Blank.esp".into(), regex("€ƒ."))
);
}
}
mod hash {
@@ -1346,5 +1401,39 @@ mod tests {
assert_eq!(hash(function1), hash(function2));
}
#[test]
fn function_hash_description_contains_should_hash_pathbuf_and_regex() {
let function1 = Function::DescriptionContains("Blank.esp".into(), regex("€ƒ."));
let function2 = Function::DescriptionContains("Blank.esp".into(), regex("€ƒ."));
assert_eq!(hash(function1), hash(function2));
let function1 = Function::DescriptionContains("Blank.esp".into(), regex("€ƒ."));
let function2 = Function::DescriptionContains("other".into(), regex("€ƒ."));
assert_ne!(hash(function1), hash(function2));
let function1 = Function::DescriptionContains("Blank.esp".into(), regex("€ƒ."));
let function2 = Function::DescriptionContains("Blank.esp".into(), regex(".*"));
assert_ne!(hash(function1), hash(function2));
}
#[test]
fn function_hash_description_contains_should_be_case_insensitive() {
let function1 = Function::DescriptionContains("blank.esp".into(), regex("€Ƒ."));
let function2 = Function::DescriptionContains("Blank.esp".into(), regex("€ƒ."));
assert_eq!(hash(function1), hash(function2));
}
#[test]
fn function_hash_file_regex_and_description_contains_should_not_have_equal_hashes() {
let function1 = Function::FileRegex("Blank.esp".into(), regex("€ƒ."));
let function2 = Function::DescriptionContains("Blank.esp".into(), regex("€ƒ."));
assert_ne!(hash(function1), hash(function2));
}
}
}
+67 -6
View File
@@ -46,11 +46,20 @@ fn is_in_game_path(path: &Path) -> bool {
true
}
fn parse_regex(input: &str) -> ParsingResult<Regex> {
RegexBuilder::new(&format!("^{}$", input))
fn build_regex(input: &str) -> Result<(&'static str, Regex), regex::Error> {
RegexBuilder::new(input)
.case_insensitive(true)
.build()
.map(|r| ("", r))
}
fn parse_regex(input: &str) -> ParsingResult<Regex> {
build_regex(input).map_err(|e| Err::Failure(ParsingErrorKind::from(e).at(input)))
}
fn parse_anchored_regex(input: &str) -> ParsingResult<Regex> {
build_regex(&format!("^{}$", input))
.map_err(|e| Err::Failure(ParsingErrorKind::from(e).at(input)))
}
@@ -136,6 +145,22 @@ fn parse_filename_version_args(
Ok((remaining_input, (path, regex, version, comparator)))
}
fn parse_description_contains_args(input: &str) -> ParsingResult<(PathBuf, Regex)> {
let mut parser = (
map_err(parse_path),
map_err(whitespace(tag(","))),
delimited(
map_err(tag("\"")),
map_parser(is_not("\""), parse_regex),
map_err(tag("\"")),
),
);
let (remaining_input, (path, _, regex)) = parser.parse(input)?;
Ok((remaining_input, (path, regex)))
}
fn parse_crc(input: &str) -> ParsingResult<u32> {
u32::from_str_radix(input, 16)
.map(|c| ("", c))
@@ -193,13 +218,13 @@ fn parse_regex_path(input: &str) -> ParsingResult<(PathBuf, Regex)> {
return Err(not_in_game_directory(input, parent_path));
}
let regex = parse_regex(regex_slice)?.1;
let regex = parse_anchored_regex(regex_slice)?.1;
Ok((remaining_input, (parent_path, regex)))
}
fn parse_regex_filename(input: &str) -> ParsingResult<Regex> {
map_parser(is_not(INVALID_REGEX_PATH_CHARS), parse_regex).parse(input)
map_parser(is_not(INVALID_REGEX_PATH_CHARS), parse_anchored_regex).parse(input)
}
impl Function {
@@ -319,6 +344,14 @@ impl Function {
),
|(path, crc)| Function::Checksum(path, crc),
),
map(
delimited(
map_err(tag("description_contains(")),
parse_description_contains_args,
map_err(tag(")")),
),
|(path, regex)| Function::DescriptionContains(path, regex),
),
))
.parse(input)
}
@@ -338,8 +371,22 @@ mod tests {
}
#[test]
fn parse_regex_should_produce_a_regex_that_does_not_partially_match() {
let (_, regex) = parse_regex("cargo.").unwrap();
fn parse_regex_should_produce_a_regex_that_does_partially_match() {
let (_, regex) = parse_regex("argo.").unwrap();
assert!(regex.is_match("Cargo.toml"));
}
#[test]
fn parse_anchored_regex_should_produce_case_insensitive_regex() {
let (_, regex) = parse_anchored_regex("cargo.*").unwrap();
assert!(regex.is_match("Cargo.toml"));
}
#[test]
fn parse_anchored_regex_should_produce_a_regex_that_does_not_partially_match() {
let (_, regex) = parse_anchored_regex("cargo.").unwrap();
assert!(!regex.is_match("Cargo.toml"));
}
@@ -725,4 +772,18 @@ mod tests {
Function::parse("filename_version(\"subdir/Cargo .+.toml\", \"1.2\", ==)").is_err()
);
}
#[test]
fn function_parse_should_parse_a_description_contains_function() {
let output = Function::parse("description_contains(\"Blank.esp\", \"€ƒ.\")").unwrap();
assert!(output.0.is_empty());
match output.1 {
Function::DescriptionContains(p, r) => {
assert_eq!(PathBuf::from("Blank.esp"), p);
assert_eq!(Regex::new("€ƒ.").unwrap().as_str(), r.as_str());
}
_ => panic!("Expected a description_contains function"),
}
}
}