Skip to content

Commit 174ba55

Browse files
committed
Add description_contains() condition function
It evaluates to true if the plugin description contains text that matches the given regex.
1 parent f84083d commit 174ba55

File tree

3 files changed

+228
-12
lines changed

3 files changed

+228
-12
lines changed

src/function/eval.rs

+72-6
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,11 @@ fn evaluate_active_regex(state: &State, regex: &Regex) -> Result<bool, Error> {
106106
Ok(state.active_plugins.iter().any(|p| regex.is_match(p)))
107107
}
108108

109-
fn evaluate_is_master(state: &State, file_path: &Path) -> Result<bool, Error> {
109+
fn parse_plugin(state: &State, file_path: &Path) -> Option<esplugin::Plugin> {
110110
use esplugin::GameId;
111111

112112
let game_id = match state.game_type {
113-
GameType::OpenMW => return Ok(false),
113+
GameType::OpenMW => return None,
114114
GameType::Morrowind => GameId::Morrowind,
115115
GameType::Oblivion => GameId::Oblivion,
116116
GameType::Skyrim => GameId::Skyrim,
@@ -125,10 +125,17 @@ fn evaluate_is_master(state: &State, file_path: &Path) -> Result<bool, Error> {
125125

126126
let mut plugin = esplugin::Plugin::new(game_id, &path);
127127

128-
plugin
129-
.parse_file(ParseOptions::header_only())
130-
.map(|_| plugin.is_master_file())
131-
.or(Ok(false))
128+
if plugin.parse_file(ParseOptions::header_only()).is_ok() {
129+
Some(plugin)
130+
} else {
131+
None
132+
}
133+
}
134+
135+
fn evaluate_is_master(state: &State, file_path: &Path) -> Result<bool, Error> {
136+
Ok(parse_plugin(state, file_path)
137+
.map(|plugin| plugin.is_master_file())
138+
.unwrap_or(false))
132139
}
133140

134141
fn evaluate_many_active(state: &State, regex: &Regex) -> Result<bool, Error> {
@@ -281,6 +288,17 @@ fn evaluate_filename_version(
281288
evaluate_dir_entries(state, parent_path, evaluator)
282289
}
283290

291+
fn evaluate_description_contains(
292+
state: &State,
293+
file_path: &Path,
294+
regex: &Regex,
295+
) -> Result<bool, Error> {
296+
Ok(parse_plugin(state, file_path)
297+
.and_then(|plugin| plugin.description().unwrap_or(None))
298+
.map(|description| regex.is_match(&description))
299+
.unwrap_or(false))
300+
}
301+
284302
impl Function {
285303
pub fn eval(&self, state: &State) -> Result<bool, Error> {
286304
if self.is_slow() {
@@ -308,6 +326,7 @@ impl Function {
308326
evaluate_version(state, p, v, *c, |_, p| get_product_version(p))
309327
}
310328
Function::FilenameVersion(p, r, v, c) => evaluate_filename_version(state, p, r, v, *c),
329+
Function::DescriptionContains(p, r) => evaluate_description_contains(state, p, r),
311330
};
312331

313332
if self.is_slow() {
@@ -1580,4 +1599,51 @@ mod tests {
15801599

15811600
assert!(function.eval(&state).unwrap());
15821601
}
1602+
1603+
#[test]
1604+
fn function_description_contains_eval_should_return_false_if_the_file_does_not_exist() {
1605+
let state = state_with_versions("tests/testing-plugins/Oblivion/Data", &[]);
1606+
1607+
let function = Function::DescriptionContains("missing.esp".into(), regex("€ƒ."));
1608+
1609+
assert!(!function.eval(&state).unwrap());
1610+
}
1611+
1612+
#[test]
1613+
fn function_description_contains_eval_should_return_false_if_the_file_is_not_a_plugin() {
1614+
let state = state_with_versions("tests/testing-plugins/Oblivion/Data", &[]);
1615+
1616+
let function = Function::DescriptionContains("Blank.bsa".into(), regex("€ƒ."));
1617+
1618+
assert!(!function.eval(&state).unwrap());
1619+
}
1620+
1621+
#[test]
1622+
fn function_description_contains_eval_should_return_false_if_the_plugin_has_no_description() {
1623+
let state = state_with_versions("tests/testing-plugins/Oblivion/Data", &[]);
1624+
1625+
let function = Function::DescriptionContains("Blank - Different.esm".into(), regex("€ƒ."));
1626+
1627+
assert!(!function.eval(&state).unwrap());
1628+
}
1629+
1630+
#[test]
1631+
fn function_description_contains_eval_should_return_false_if_the_plugin_description_does_not_match(
1632+
) {
1633+
let state = state_with_versions("tests/testing-plugins/Oblivion/Data", &[]);
1634+
1635+
let function = Function::DescriptionContains("Blank.esm".into(), regex("€ƒ."));
1636+
1637+
assert!(!function.eval(&state).unwrap());
1638+
}
1639+
1640+
#[test]
1641+
fn function_description_contains_eval_should_return_true_if_the_plugin_description_contains_a_match(
1642+
) {
1643+
let state = state_with_versions("tests/testing-plugins/Oblivion/Data", &[]);
1644+
1645+
let function = Function::DescriptionContains("Blank.esp".into(), regex("ƒ"));
1646+
1647+
assert!(function.eval(&state).unwrap());
1648+
}
15831649
}

src/function/mod.rs

+89
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ pub enum Function {
5151
Version(PathBuf, String, ComparisonOperator),
5252
ProductVersion(PathBuf, String, ComparisonOperator),
5353
FilenameVersion(PathBuf, Regex, String, ComparisonOperator),
54+
DescriptionContains(PathBuf, Regex),
5455
}
5556

5657
impl fmt::Display for Function {
@@ -82,6 +83,9 @@ impl fmt::Display for Function {
8283
c
8384
)
8485
}
86+
DescriptionContains(p, r) => {
87+
write!(f, "description_contains(\"{}\", \"{}\")", p.display(), r)
88+
}
8589
}
8690
}
8791
}
@@ -123,6 +127,9 @@ impl PartialEq for Function {
123127
&& eq(r1.as_str(), r2.as_str())
124128
&& eq(&p1.to_string_lossy(), &p2.to_string_lossy())
125129
}
130+
(DescriptionContains(p1, r1), DescriptionContains(p2, r2)) => {
131+
eq(r1.as_str(), r2.as_str()) && eq(&p1.to_string_lossy(), &p2.to_string_lossy())
132+
}
126133
_ => false,
127134
}
128135
}
@@ -187,6 +194,10 @@ impl Hash for Function {
187194
v.to_lowercase().hash(state);
188195
c.hash(state);
189196
}
197+
DescriptionContains(p, r) => {
198+
p.to_string_lossy().to_lowercase().hash(state);
199+
r.as_str().to_lowercase().hash(state);
200+
}
190201
}
191202

192203
discriminant(self).hash(state);
@@ -332,6 +343,16 @@ mod tests {
332343
&format!("{}", function)
333344
);
334345
}
346+
347+
#[test]
348+
fn function_fmt_for_description_contains_should_format_correctly() {
349+
let function = Function::DescriptionContains("Blank.esp".into(), regex("€ƒ."));
350+
351+
assert_eq!(
352+
"description_contains(\"Blank.esp\", \"€ƒ.\")",
353+
&format!("{}", function)
354+
);
355+
}
335356
}
336357

337358
mod eq {
@@ -815,6 +836,40 @@ mod tests {
815836
)
816837
);
817838
}
839+
840+
#[test]
841+
fn function_eq_for_description_contains_should_check_pathbuf_and_regex() {
842+
assert_eq!(
843+
Function::DescriptionContains("Blank.esp".into(), regex("€ƒ.")),
844+
Function::DescriptionContains("Blank.esp".into(), regex("€ƒ."))
845+
);
846+
847+
assert_ne!(
848+
Function::DescriptionContains("Blank.esp".into(), regex("€ƒ.")),
849+
Function::DescriptionContains("Blank.esp".into(), regex(".*"))
850+
);
851+
assert_ne!(
852+
Function::DescriptionContains("Blank.esp".into(), regex("€ƒ.")),
853+
Function::DescriptionContains("other".into(), regex("€ƒ."))
854+
);
855+
}
856+
857+
#[test]
858+
fn function_eq_for_description_contains_should_be_case_insensitive_on_pathbuf_and_regex() {
859+
assert_eq!(
860+
Function::DescriptionContains("Blank.esp".into(), regex("€ƒ.")),
861+
Function::DescriptionContains("blank.esp".into(), regex("€Ƒ."))
862+
);
863+
}
864+
865+
#[test]
866+
fn function_eq_description_contains_should_not_be_equal_to_file_regex_with_same_pathbuf_and_regex(
867+
) {
868+
assert_ne!(
869+
Function::DescriptionContains("Blank.esp".into(), regex("€ƒ.")),
870+
Function::FileRegex("Blank.esp".into(), regex("€ƒ."))
871+
);
872+
}
818873
}
819874

820875
mod hash {
@@ -1346,5 +1401,39 @@ mod tests {
13461401

13471402
assert_eq!(hash(function1), hash(function2));
13481403
}
1404+
1405+
#[test]
1406+
fn function_hash_description_contains_should_hash_pathbuf_and_regex() {
1407+
let function1 = Function::DescriptionContains("Blank.esp".into(), regex("€ƒ."));
1408+
let function2 = Function::DescriptionContains("Blank.esp".into(), regex("€ƒ."));
1409+
1410+
assert_eq!(hash(function1), hash(function2));
1411+
1412+
let function1 = Function::DescriptionContains("Blank.esp".into(), regex("€ƒ."));
1413+
let function2 = Function::DescriptionContains("other".into(), regex("€ƒ."));
1414+
1415+
assert_ne!(hash(function1), hash(function2));
1416+
1417+
let function1 = Function::DescriptionContains("Blank.esp".into(), regex("€ƒ."));
1418+
let function2 = Function::DescriptionContains("Blank.esp".into(), regex(".*"));
1419+
1420+
assert_ne!(hash(function1), hash(function2));
1421+
}
1422+
1423+
#[test]
1424+
fn function_hash_description_contains_should_be_case_insensitive() {
1425+
let function1 = Function::DescriptionContains("blank.esp".into(), regex("€Ƒ."));
1426+
let function2 = Function::DescriptionContains("Blank.esp".into(), regex("€ƒ."));
1427+
1428+
assert_eq!(hash(function1), hash(function2));
1429+
}
1430+
1431+
#[test]
1432+
fn function_hash_file_regex_and_description_contains_should_not_have_equal_hashes() {
1433+
let function1 = Function::FileRegex("Blank.esp".into(), regex("€ƒ."));
1434+
let function2 = Function::DescriptionContains("Blank.esp".into(), regex("€ƒ."));
1435+
1436+
assert_ne!(hash(function1), hash(function2));
1437+
}
13491438
}
13501439
}

src/function/parse.rs

+67-6
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,20 @@ fn is_in_game_path(path: &Path) -> bool {
4646

4747
true
4848
}
49-
fn parse_regex(input: &str) -> ParsingResult<Regex> {
50-
RegexBuilder::new(&format!("^{}$", input))
49+
50+
fn build_regex(input: &str) -> Result<(&'static str, Regex), regex::Error> {
51+
RegexBuilder::new(input)
5152
.case_insensitive(true)
5253
.build()
5354
.map(|r| ("", r))
55+
}
56+
57+
fn parse_regex(input: &str) -> ParsingResult<Regex> {
58+
build_regex(input).map_err(|e| Err::Failure(ParsingErrorKind::from(e).at(input)))
59+
}
60+
61+
fn parse_anchored_regex(input: &str) -> ParsingResult<Regex> {
62+
build_regex(&format!("^{}$", input))
5463
.map_err(|e| Err::Failure(ParsingErrorKind::from(e).at(input)))
5564
}
5665

@@ -136,6 +145,22 @@ fn parse_filename_version_args(
136145
Ok((remaining_input, (path, regex, version, comparator)))
137146
}
138147

148+
fn parse_description_contains_args(input: &str) -> ParsingResult<(PathBuf, Regex)> {
149+
let mut parser = (
150+
map_err(parse_path),
151+
map_err(whitespace(tag(","))),
152+
delimited(
153+
map_err(tag("\"")),
154+
map_parser(is_not("\""), parse_regex),
155+
map_err(tag("\"")),
156+
),
157+
);
158+
159+
let (remaining_input, (path, _, regex)) = parser.parse(input)?;
160+
161+
Ok((remaining_input, (path, regex)))
162+
}
163+
139164
fn parse_crc(input: &str) -> ParsingResult<u32> {
140165
u32::from_str_radix(input, 16)
141166
.map(|c| ("", c))
@@ -193,13 +218,13 @@ fn parse_regex_path(input: &str) -> ParsingResult<(PathBuf, Regex)> {
193218
return Err(not_in_game_directory(input, parent_path));
194219
}
195220

196-
let regex = parse_regex(regex_slice)?.1;
221+
let regex = parse_anchored_regex(regex_slice)?.1;
197222

198223
Ok((remaining_input, (parent_path, regex)))
199224
}
200225

201226
fn parse_regex_filename(input: &str) -> ParsingResult<Regex> {
202-
map_parser(is_not(INVALID_REGEX_PATH_CHARS), parse_regex).parse(input)
227+
map_parser(is_not(INVALID_REGEX_PATH_CHARS), parse_anchored_regex).parse(input)
203228
}
204229

205230
impl Function {
@@ -319,6 +344,14 @@ impl Function {
319344
),
320345
|(path, crc)| Function::Checksum(path, crc),
321346
),
347+
map(
348+
delimited(
349+
map_err(tag("description_contains(")),
350+
parse_description_contains_args,
351+
map_err(tag(")")),
352+
),
353+
|(path, regex)| Function::DescriptionContains(path, regex),
354+
),
322355
))
323356
.parse(input)
324357
}
@@ -338,8 +371,22 @@ mod tests {
338371
}
339372

340373
#[test]
341-
fn parse_regex_should_produce_a_regex_that_does_not_partially_match() {
342-
let (_, regex) = parse_regex("cargo.").unwrap();
374+
fn parse_regex_should_produce_a_regex_that_does_partially_match() {
375+
let (_, regex) = parse_regex("argo.").unwrap();
376+
377+
assert!(regex.is_match("Cargo.toml"));
378+
}
379+
380+
#[test]
381+
fn parse_anchored_regex_should_produce_case_insensitive_regex() {
382+
let (_, regex) = parse_anchored_regex("cargo.*").unwrap();
383+
384+
assert!(regex.is_match("Cargo.toml"));
385+
}
386+
387+
#[test]
388+
fn parse_anchored_regex_should_produce_a_regex_that_does_not_partially_match() {
389+
let (_, regex) = parse_anchored_regex("cargo.").unwrap();
343390

344391
assert!(!regex.is_match("Cargo.toml"));
345392
}
@@ -725,4 +772,18 @@ mod tests {
725772
Function::parse("filename_version(\"subdir/Cargo .+.toml\", \"1.2\", ==)").is_err()
726773
);
727774
}
775+
776+
#[test]
777+
fn function_parse_should_parse_a_description_contains_function() {
778+
let output = Function::parse("description_contains(\"Blank.esp\", \"€ƒ.\")").unwrap();
779+
780+
assert!(output.0.is_empty());
781+
match output.1 {
782+
Function::DescriptionContains(p, r) => {
783+
assert_eq!(PathBuf::from("Blank.esp"), p);
784+
assert_eq!(Regex::new("€ƒ.").unwrap().as_str(), r.as_str());
785+
}
786+
_ => panic!("Expected a description_contains function"),
787+
}
788+
}
728789
}

0 commit comments

Comments
 (0)