• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

loot / loot-condition-interpreter / 14679793593

26 Apr 2025 09:10AM UTC coverage: 91.6% (+0.02%) from 91.583%
14679793593

push

github

Ortham
Deny a lot of extra lints and fix their errors

337 of 376 new or added lines in 10 files covered. (89.63%)

111 existing lines in 7 files now uncovered.

4907 of 5357 relevant lines covered (91.6%)

15.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.93
/src/function/parse.rs
1
use std::path::{Component, Path, PathBuf};
2
use std::str;
3

4
use nom::branch::alt;
5
use nom::bytes::complete::{is_not, tag};
6
use nom::character::complete::digit1;
7
use nom::character::complete::hex_digit1;
8
use nom::combinator::{map, map_parser, value};
9
use nom::sequence::delimited;
10
use nom::{Err, IResult, Parser};
11
use regex::{Regex, RegexBuilder};
12

13
use super::{ComparisonOperator, Function};
14
use crate::error::ParsingErrorKind;
15
use crate::{map_err, whitespace, ParsingError, ParsingResult};
16

17
impl ComparisonOperator {
18
    pub fn parse(input: &str) -> IResult<&str, ComparisonOperator> {
18✔
19
        alt((
18✔
20
            value(ComparisonOperator::Equal, tag("==")),
18✔
21
            value(ComparisonOperator::NotEqual, tag("!=")),
18✔
22
            value(ComparisonOperator::LessThanOrEqual, tag("<=")),
18✔
23
            value(ComparisonOperator::GreaterThanOrEqual, tag(">=")),
18✔
24
            value(ComparisonOperator::LessThan, tag("<")),
18✔
25
            value(ComparisonOperator::GreaterThan, tag(">")),
18✔
26
        ))
18✔
27
        .parse(input)
18✔
28
    }
18✔
29
}
30

31
const INVALID_PATH_CHARS: &str = "\":*?<>|";
32
const INVALID_NON_REGEX_PATH_CHARS: &str = "\":*?<>|\\"; // \ is treated as invalid to distinguish regex strings.
33
const INVALID_REGEX_PATH_CHARS: &str = "\"<>";
34

35
fn is_in_game_path(path: &Path) -> bool {
76✔
36
    let mut previous_component = Component::CurDir;
76✔
37
    for component in path.components() {
89✔
38
        match (component, previous_component) {
89✔
39
            (Component::Prefix(_) | Component::RootDir, _)
40
            | (Component::ParentDir, Component::ParentDir) => return false,
12✔
41
            (Component::CurDir, _) => {}
5✔
42
            _ => previous_component = component,
72✔
43
        }
44
    }
45

46
    true
64✔
47
}
76✔
48

49
fn build_regex(input: &str) -> Result<(&'static str, Regex), regex::Error> {
16✔
50
    RegexBuilder::new(input)
16✔
51
        .case_insensitive(true)
16✔
52
        .build()
16✔
53
        .map(|r| ("", r))
16✔
54
}
16✔
55

56
fn parse_regex(input: &str) -> ParsingResult<Regex> {
3✔
57
    build_regex(input).map_err(|e| Err::Failure(ParsingErrorKind::from(e).at(input)))
3✔
58
}
3✔
59

60
fn parse_anchored_regex(input: &str) -> ParsingResult<Regex> {
13✔
61
    build_regex(&format!("^{input}$"))
13✔
62
        .map_err(|e| Err::Failure(ParsingErrorKind::from(e).at(input)))
13✔
63
}
13✔
64

65
fn not_in_game_directory(input: &str, path: PathBuf) -> Err<ParsingError<&str>> {
12✔
66
    Err::Failure(ParsingErrorKind::PathIsNotInGameDirectory(path).at(input))
12✔
67
}
12✔
68

69
fn parse_path(input: &str) -> IResult<&str, PathBuf> {
32✔
70
    map(
32✔
71
        delimited(tag("\""), is_not(INVALID_PATH_CHARS), tag("\"")),
32✔
72
        PathBuf::from,
32✔
73
    )
32✔
74
    .parse(input)
32✔
75
}
32✔
76

77
fn parse_size(input: &str) -> ParsingResult<u64> {
2✔
78
    str::parse(input)
2✔
79
        .map(|c| ("", c))
2✔
80
        .map_err(|e| Err::Failure(ParsingErrorKind::from(e).at(input)))
2✔
81
}
2✔
82

83
fn parse_file_size_args(input: &str) -> ParsingResult<(PathBuf, u64)> {
2✔
84
    let mut parser = (
2✔
85
        map_err(parse_path),
2✔
86
        map_err(whitespace(tag(","))),
2✔
87
        map_parser(digit1, parse_size),
2✔
88
    );
2✔
89

90
    let (remaining_input, (path, _, size)) = parser.parse(input)?;
2✔
91

92
    if is_in_game_path(&path) {
2✔
93
        Ok((remaining_input, (path, size)))
1✔
94
    } else {
95
        Err(not_in_game_directory(input, path))
1✔
96
    }
97
}
2✔
98

99
fn parse_version(input: &str) -> IResult<&str, String> {
18✔
100
    map(
18✔
101
        delimited(tag("\""), is_not("\""), tag("\"")),
18✔
102
        |version: &str| version.to_owned(),
18✔
103
    )
18✔
104
    .parse(input)
18✔
105
}
18✔
106

107
fn parse_version_args(input: &str) -> ParsingResult<(PathBuf, String, ComparisonOperator)> {
16✔
108
    let parser = (
16✔
109
        parse_path,
16✔
110
        whitespace(tag(",")),
16✔
111
        parse_version,
16✔
112
        whitespace(tag(",")),
16✔
113
        ComparisonOperator::parse,
16✔
114
    );
16✔
115

116
    let (remaining_input, (path, _, version, _, comparator)) = map_err(parser).parse(input)?;
16✔
117

118
    if is_in_game_path(&path) {
16✔
119
        Ok((remaining_input, (path, version, comparator)))
14✔
120
    } else {
121
        Err(not_in_game_directory(input, path))
2✔
122
    }
123
}
16✔
124

125
fn parse_filename_version_args(
2✔
126
    input: &str,
2✔
127
) -> ParsingResult<(PathBuf, Regex, String, ComparisonOperator)> {
2✔
128
    let mut parser = (
2✔
129
        delimited(map_err(tag("\"")), parse_regex_path, map_err(tag("\""))),
2✔
130
        map_err(whitespace(tag(","))),
2✔
131
        map_err(parse_version),
2✔
132
        map_err(whitespace(tag(","))),
2✔
133
        map_err(ComparisonOperator::parse),
2✔
134
    );
2✔
135

136
    let (remaining_input, ((path, regex), _, version, _, comparator)) = parser.parse(input)?;
2✔
137

138
    if regex.captures_len() != 2 {
2✔
139
        return Err(Err::Failure(
1✔
140
            ParsingErrorKind::InvalidRegexUnknown.at(input),
1✔
141
        ));
1✔
142
    }
1✔
143

1✔
144
    Ok((remaining_input, (path, regex, version, comparator)))
1✔
145
}
2✔
146

147
fn parse_description_contains_args(input: &str) -> ParsingResult<(PathBuf, Regex)> {
1✔
148
    let mut parser = (
1✔
149
        map_err(parse_path),
1✔
150
        map_err(whitespace(tag(","))),
1✔
151
        delimited(
1✔
152
            map_err(tag("\"")),
1✔
153
            map_parser(is_not("\""), parse_regex),
1✔
154
            map_err(tag("\"")),
1✔
155
        ),
1✔
156
    );
1✔
157

158
    let (remaining_input, (path, _, regex)) = parser.parse(input)?;
1✔
159

160
    Ok((remaining_input, (path, regex)))
1✔
161
}
1✔
162

163
fn parse_crc(input: &str) -> ParsingResult<u32> {
13✔
164
    u32::from_str_radix(input, 16)
13✔
165
        .map(|c| ("", c))
13✔
166
        .map_err(|e| Err::Failure(ParsingErrorKind::from(e).at(input)))
13✔
167
}
13✔
168

169
fn parse_checksum_args(input: &str) -> ParsingResult<(PathBuf, u32)> {
13✔
170
    let mut parser = (
13✔
171
        map_err(parse_path),
13✔
172
        map_err(whitespace(tag(","))),
13✔
173
        map_parser(hex_digit1, parse_crc),
13✔
174
    );
13✔
175

176
    let (remaining_input, (path, _, crc)) = parser.parse(input)?;
13✔
177

178
    if is_in_game_path(&path) {
12✔
179
        Ok((remaining_input, (path, crc)))
11✔
180
    } else {
181
        Err(not_in_game_directory(input, path))
1✔
182
    }
183
}
13✔
184

185
fn parse_non_regex_path(input: &str) -> ParsingResult<PathBuf> {
37✔
186
    let (remaining_input, path) = map(is_not(INVALID_NON_REGEX_PATH_CHARS), |path: &str| {
37✔
187
        PathBuf::from(path)
37✔
188
    })
37✔
189
    .parse(input)?;
37✔
190

191
    if is_in_game_path(&path) {
37✔
192
        Ok((remaining_input, path))
29✔
193
    } else {
194
        Err(not_in_game_directory(input, path))
8✔
195
    }
196
}
37✔
197

198
/// Parse a string that is a path where the last component is a regex string
199
/// that may contain characters that are invalid in paths but valid in regex.
200
fn parse_regex_path(input: &str) -> ParsingResult<(PathBuf, Regex)> {
12✔
201
    let (remaining_input, string) = is_not(INVALID_REGEX_PATH_CHARS)(input)?;
12✔
202

203
    if string.ends_with('/') {
12✔
204
        return Err(Err::Failure(
3✔
205
            ParsingErrorKind::PathEndsInADirectorySeparator(string.into()).at(input),
3✔
206
        ));
3✔
207
    }
9✔
208

9✔
209
    let (parent_path_slice, regex_slice) = string.rsplit_once('/').unwrap_or((".", string));
9✔
210

9✔
211
    let parent_path = PathBuf::from(parent_path_slice);
9✔
212

9✔
213
    if !is_in_game_path(&parent_path) {
9✔
UNCOV
214
        return Err(not_in_game_directory(input, parent_path));
×
215
    }
9✔
216

217
    let regex = parse_anchored_regex(regex_slice)?.1;
9✔
218

219
    Ok((remaining_input, (parent_path, regex)))
8✔
220
}
12✔
221

222
fn parse_regex_filename(input: &str) -> ParsingResult<Regex> {
2✔
223
    map_parser(is_not(INVALID_REGEX_PATH_CHARS), parse_anchored_regex).parse(input)
2✔
224
}
2✔
225

226
impl Function {
227
    #[expect(clippy::too_many_lines)]
228
    pub fn parse(input: &str) -> ParsingResult<Function> {
91✔
229
        alt((
91✔
230
            map(
91✔
231
                delimited(
91✔
232
                    map_err(tag("file(\"")),
91✔
233
                    parse_non_regex_path,
91✔
234
                    map_err(tag("\")")),
91✔
235
                ),
91✔
236
                Function::FilePath,
91✔
237
            ),
91✔
238
            map(
91✔
239
                delimited(
91✔
240
                    map_err(tag("file(\"")),
91✔
241
                    parse_regex_path,
91✔
242
                    map_err(tag("\")")),
91✔
243
                ),
91✔
244
                |(path, regex)| Function::FileRegex(path, regex),
91✔
245
            ),
91✔
246
            map(
91✔
247
                delimited(
91✔
248
                    map_err(tag("file_size(")),
91✔
249
                    parse_file_size_args,
91✔
250
                    map_err(tag(")")),
91✔
251
                ),
91✔
252
                |(path, size)| Function::FileSize(path, size),
91✔
253
            ),
91✔
254
            map(
91✔
255
                delimited(
91✔
256
                    map_err(tag("readable(\"")),
91✔
257
                    parse_non_regex_path,
91✔
258
                    map_err(tag("\")")),
91✔
259
                ),
91✔
260
                Function::Readable,
91✔
261
            ),
91✔
262
            map(
91✔
263
                delimited(
91✔
264
                    map_err(tag("is_executable(\"")),
91✔
265
                    parse_non_regex_path,
91✔
266
                    map_err(tag("\")")),
91✔
267
                ),
91✔
268
                Function::IsExecutable,
91✔
269
            ),
91✔
270
            map(
91✔
271
                delimited(
91✔
272
                    map_err(tag("active(\"")),
91✔
273
                    parse_non_regex_path,
91✔
274
                    map_err(tag("\")")),
91✔
275
                ),
91✔
276
                Function::ActivePath,
91✔
277
            ),
91✔
278
            map(
91✔
279
                delimited(
91✔
280
                    map_err(tag("active(\"")),
91✔
281
                    parse_regex_filename,
91✔
282
                    map_err(tag("\")")),
91✔
283
                ),
91✔
284
                Function::ActiveRegex,
91✔
285
            ),
91✔
286
            map(
91✔
287
                delimited(
91✔
288
                    map_err(tag("is_master(\"")),
91✔
289
                    parse_non_regex_path,
91✔
290
                    map_err(tag("\")")),
91✔
291
                ),
91✔
292
                Function::IsMaster,
91✔
293
            ),
91✔
294
            map(
91✔
295
                delimited(
91✔
296
                    map_err(tag("many(\"")),
91✔
297
                    parse_regex_path,
91✔
298
                    map_err(tag("\")")),
91✔
299
                ),
91✔
300
                |(path, regex)| Function::Many(path, regex),
91✔
301
            ),
91✔
302
            map(
91✔
303
                delimited(
91✔
304
                    map_err(tag("many_active(\"")),
91✔
305
                    parse_regex_filename,
91✔
306
                    map_err(tag("\")")),
91✔
307
                ),
91✔
308
                Function::ManyActive,
91✔
309
            ),
91✔
310
            map(
91✔
311
                delimited(
91✔
312
                    map_err(tag("version(")),
91✔
313
                    parse_version_args,
91✔
314
                    map_err(tag(")")),
91✔
315
                ),
91✔
316
                |(path, version, comparator)| Function::Version(path, version, comparator),
91✔
317
            ),
91✔
318
            map(
91✔
319
                delimited(
91✔
320
                    map_err(tag("product_version(")),
91✔
321
                    parse_version_args,
91✔
322
                    map_err(tag(")")),
91✔
323
                ),
91✔
324
                |(path, version, comparator)| Function::ProductVersion(path, version, comparator),
91✔
325
            ),
91✔
326
            map(
91✔
327
                delimited(
91✔
328
                    map_err(tag("filename_version(")),
91✔
329
                    parse_filename_version_args,
91✔
330
                    map_err(tag(")")),
91✔
331
                ),
91✔
332
                |(path, regex, version, comparator)| {
91✔
333
                    Function::FilenameVersion(path, regex, version, comparator)
1✔
334
                },
91✔
335
            ),
91✔
336
            map(
91✔
337
                delimited(
91✔
338
                    map_err(tag("checksum(")),
91✔
339
                    parse_checksum_args,
91✔
340
                    map_err(tag(")")),
91✔
341
                ),
91✔
342
                |(path, crc)| Function::Checksum(path, crc),
91✔
343
            ),
91✔
344
            map(
91✔
345
                delimited(
91✔
346
                    map_err(tag("description_contains(")),
91✔
347
                    parse_description_contains_args,
91✔
348
                    map_err(tag(")")),
91✔
349
                ),
91✔
350
                |(path, regex)| Function::DescriptionContains(path, regex),
91✔
351
            ),
91✔
352
        ))
91✔
353
        .parse(input)
91✔
354
    }
91✔
355
}
356

357
#[cfg(test)]
358
mod tests {
359
    use super::*;
360

361
    #[test]
362
    fn parse_regex_should_produce_case_insensitive_regex() {
1✔
363
        let (_, regex) = parse_regex("cargo.*").unwrap();
1✔
364

1✔
365
        assert!(regex.is_match("Cargo.toml"));
1✔
366
    }
1✔
367

368
    #[test]
369
    fn parse_regex_should_produce_a_regex_that_does_partially_match() {
1✔
370
        let (_, regex) = parse_regex("argo.").unwrap();
1✔
371

1✔
372
        assert!(regex.is_match("Cargo.toml"));
1✔
373
    }
1✔
374

375
    #[test]
376
    fn parse_anchored_regex_should_produce_case_insensitive_regex() {
1✔
377
        let (_, regex) = parse_anchored_regex("cargo.*").unwrap();
1✔
378

1✔
379
        assert!(regex.is_match("Cargo.toml"));
1✔
380
    }
1✔
381

382
    #[test]
383
    fn parse_anchored_regex_should_produce_a_regex_that_does_not_partially_match() {
1✔
384
        let (_, regex) = parse_anchored_regex("cargo.").unwrap();
1✔
385

1✔
386
        assert!(!regex.is_match("Cargo.toml"));
1✔
387
    }
1✔
388

389
    #[test]
390
    fn function_parse_should_parse_a_file_path_function() {
1✔
391
        let output = Function::parse("file(\"Cargo.toml\")").unwrap();
1✔
392

1✔
393
        assert!(output.0.is_empty());
1✔
394
        match output.1 {
1✔
395
            Function::FilePath(f) => assert_eq!(Path::new("Cargo.toml"), f),
1✔
UNCOV
396
            _ => panic!("Expected a file path function"),
×
397
        }
398
    }
1✔
399

400
    #[test]
401
    fn function_parse_should_error_if_the_file_path_is_outside_the_game_directory() {
1✔
402
        assert!(Function::parse("file(\"../../Cargo.toml\")").is_err());
1✔
403
    }
1✔
404

405
    #[test]
406
    fn function_parse_should_parse_a_file_regex_function_with_no_parent_path() {
1✔
407
        let output = Function::parse("file(\"Cargo.*\")").unwrap();
1✔
408

1✔
409
        assert!(output.0.is_empty());
1✔
410
        match output.1 {
1✔
411
            Function::FileRegex(p, r) => {
1✔
412
                assert_eq!(PathBuf::from("."), p);
1✔
413
                assert_eq!(Regex::new("^Cargo.*$").unwrap().as_str(), r.as_str());
1✔
414
            }
UNCOV
415
            _ => panic!("Expected a file regex function"),
×
416
        }
417
    }
1✔
418

419
    #[test]
420
    fn function_parse_should_parse_a_file_regex_function_with_a_parent_path() {
1✔
421
        let output = Function::parse("file(\"subdir/Cargo.*\")").unwrap();
1✔
422

1✔
423
        assert!(output.0.is_empty());
1✔
424
        match output.1 {
1✔
425
            Function::FileRegex(p, r) => {
1✔
426
                assert_eq!(PathBuf::from("subdir"), p);
1✔
427
                assert_eq!(Regex::new("^Cargo.*$").unwrap().as_str(), r.as_str());
1✔
428
            }
UNCOV
429
            _ => panic!("Expected a file regex function"),
×
430
        }
431
    }
1✔
432

433
    #[test]
434
    fn function_parse_should_error_if_given_a_file_regex_function_ending_in_a_forward_slash() {
1✔
435
        assert!(Function::parse("file(\"sub\\dir/\")").is_err());
1✔
436
    }
1✔
437

438
    #[test]
439
    fn function_parse_should_error_if_the_file_regex_parent_path_is_outside_the_game_directory() {
1✔
440
        assert!(Function::parse("file(\"../../Cargo.*\")").is_err());
1✔
441
    }
1✔
442

443
    #[test]
444
    fn function_parse_should_parse_a_file_size_function() {
1✔
445
        let output = Function::parse("file_size(\"Cargo.toml\", 1234)").unwrap();
1✔
446

1✔
447
        assert!(output.0.is_empty());
1✔
448
        match output.1 {
1✔
449
            Function::FileSize(f, s) => {
1✔
450
                assert_eq!(Path::new("Cargo.toml"), f);
1✔
451
                assert_eq!(1234, s);
1✔
452
            }
UNCOV
453
            _ => panic!("Expected a file size function"),
×
454
        }
455
    }
1✔
456

457
    #[test]
458
    fn function_parse_should_error_if_the_file_size_is_outside_the_game_directory() {
1✔
459
        assert!(Function::parse("file_size(\"../../Cargo.toml\", 1234)").is_err());
1✔
460
    }
1✔
461

462
    #[test]
463
    fn function_parse_should_parse_a_readable_function() {
1✔
464
        let output = Function::parse("readable(\"Cargo.toml\")").unwrap();
1✔
465

1✔
466
        assert!(output.0.is_empty());
1✔
467
        match output.1 {
1✔
468
            Function::Readable(f) => assert_eq!(Path::new("Cargo.toml"), f),
1✔
UNCOV
469
            _ => panic!("Expected a readable function"),
×
470
        }
471
    }
1✔
472

473
    #[test]
474
    fn function_parse_should_error_if_the_readable_path_is_outside_the_game_directory() {
1✔
475
        assert!(Function::parse("readable(\"../../Cargo.toml\")").is_err());
1✔
476
    }
1✔
477

478
    #[test]
479
    fn function_parse_should_parse_an_is_executable_function() {
1✔
480
        let output = Function::parse("is_executable(\"Cargo.toml\")").unwrap();
1✔
481

1✔
482
        assert!(output.0.is_empty());
1✔
483
        match output.1 {
1✔
484
            Function::IsExecutable(f) => assert_eq!(Path::new("Cargo.toml"), f),
1✔
UNCOV
485
            _ => panic!("Expected an is_executable function"),
×
486
        }
487
    }
1✔
488

489
    #[test]
490
    fn function_parse_should_error_if_the_is_executable_path_is_outside_the_game_directory() {
1✔
491
        assert!(Function::parse("is_executable(\"../../Cargo.toml\")").is_err());
1✔
492
    }
1✔
493

494
    #[test]
495
    fn function_parse_should_parse_an_active_path_function() {
1✔
496
        let output = Function::parse("active(\"Cargo.toml\")").unwrap();
1✔
497

1✔
498
        assert!(output.0.is_empty());
1✔
499
        match output.1 {
1✔
500
            Function::ActivePath(f) => assert_eq!(Path::new("Cargo.toml"), f),
1✔
UNCOV
501
            _ => panic!("Expected an active path function"),
×
502
        }
503
    }
1✔
504

505
    #[test]
506
    fn function_parse_should_error_if_the_active_path_is_outside_the_game_directory() {
1✔
507
        // Trying to check if a path that isn't a plugin in the data folder is
1✔
508
        // active is pointless, but it's not worth having a more specific check.
1✔
509
        assert!(Function::parse("active(\"../../Cargo.toml\")").is_err());
1✔
510
    }
1✔
511

512
    #[test]
513
    fn function_parse_should_parse_an_active_regex_function() {
1✔
514
        let output = Function::parse("active(\"Cargo.*\")").unwrap();
1✔
515

1✔
516
        assert!(output.0.is_empty());
1✔
517
        match output.1 {
1✔
518
            Function::ActiveRegex(r) => {
1✔
519
                assert_eq!(Regex::new("^Cargo.*$").unwrap().as_str(), r.as_str());
1✔
520
            }
UNCOV
521
            _ => panic!("Expected an active regex function"),
×
522
        }
523
    }
1✔
524

525
    #[test]
526
    fn function_parse_should_parse_an_is_master_function() {
1✔
527
        let output = Function::parse("is_master(\"Blank.esm\")").unwrap();
1✔
528

1✔
529
        assert!(output.0.is_empty());
1✔
530
        match output.1 {
1✔
531
            Function::IsMaster(f) => assert_eq!(Path::new("Blank.esm"), f),
1✔
UNCOV
532
            _ => panic!("Expected an is master function"),
×
533
        }
534
    }
1✔
535

536
    #[test]
537
    fn function_parse_should_error_if_the_is_master_path_is_outside_the_game_directory() {
1✔
538
        // Trying to check if a path that isn't a plugin in the data folder is
1✔
539
        // active is pointless, but it's not worth having a more specific check.
1✔
540
        assert!(Function::parse("is_master(\"../../Blank.esm\")").is_err());
1✔
541
    }
1✔
542

543
    #[test]
544
    fn function_parse_should_parse_a_many_function_with_no_parent_path() {
1✔
545
        let output = Function::parse("many(\"Cargo.*\")").unwrap();
1✔
546

1✔
547
        assert!(output.0.is_empty());
1✔
548
        match output.1 {
1✔
549
            Function::Many(p, r) => {
1✔
550
                assert_eq!(PathBuf::from("."), p);
1✔
551
                assert_eq!(Regex::new("^Cargo.*$").unwrap().as_str(), r.as_str());
1✔
552
            }
UNCOV
553
            _ => panic!("Expected a many function"),
×
554
        }
555
    }
1✔
556

557
    #[test]
558
    fn function_parse_should_parse_a_many_function_with_a_parent_path() {
1✔
559
        let output = Function::parse("many(\"subdir/Cargo.*\")").unwrap();
1✔
560

1✔
561
        assert!(output.0.is_empty());
1✔
562
        match output.1 {
1✔
563
            Function::Many(p, r) => {
1✔
564
                assert_eq!(PathBuf::from("subdir"), p);
1✔
565
                assert_eq!(Regex::new("^Cargo.*$").unwrap().as_str(), r.as_str());
1✔
566
            }
UNCOV
567
            _ => panic!("Expected a many function"),
×
568
        }
569
    }
1✔
570

571
    #[test]
572
    fn function_parse_should_error_if_given_a_many_function_ending_in_a_forward_slash() {
1✔
573
        assert!(Function::parse("many(\"subdir/\")").is_err());
1✔
574
    }
1✔
575

576
    #[test]
577
    fn function_parse_should_error_if_the_many_parent_path_is_outside_the_game_directory() {
1✔
578
        assert!(Function::parse("file(\"../../Cargo.*\")").is_err());
1✔
579
    }
1✔
580

581
    #[test]
582
    fn function_parse_should_parse_a_many_active_function() {
1✔
583
        let output = Function::parse("many_active(\"Cargo.*\")").unwrap();
1✔
584

1✔
585
        assert!(output.0.is_empty());
1✔
586
        match output.1 {
1✔
587
            Function::ManyActive(r) => {
1✔
588
                assert_eq!(Regex::new("^Cargo.*$").unwrap().as_str(), r.as_str());
1✔
589
            }
UNCOV
590
            _ => panic!("Expected a many active function"),
×
591
        }
592
    }
1✔
593

594
    #[test]
595
    fn function_parse_should_parse_a_checksum_function() {
1✔
596
        let output = Function::parse("checksum(\"Cargo.toml\", DEADBEEF)").unwrap();
1✔
597

1✔
598
        assert!(output.0.is_empty());
1✔
599
        match output.1 {
1✔
600
            Function::Checksum(path, crc) => {
1✔
601
                assert_eq!(Path::new("Cargo.toml"), path);
1✔
602
                assert_eq!(0xDEAD_BEEF, crc);
1✔
603
            }
UNCOV
604
            _ => panic!("Expected a checksum function"),
×
605
        }
606
    }
1✔
607

608
    #[test]
609
    fn function_parse_should_error_if_the_checksum_path_is_outside_the_game_directory() {
1✔
610
        assert!(Function::parse("checksum(\"../../Cargo.toml\", DEADBEEF)").is_err());
1✔
611
    }
1✔
612

613
    #[test]
614
    fn function_parse_should_parse_a_version_equals_function() {
1✔
615
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", ==)").unwrap();
1✔
616

1✔
617
        assert!(output.0.is_empty());
1✔
618
        match output.1 {
1✔
619
            Function::Version(path, version, comparator) => {
1✔
620
                assert_eq!(Path::new("Cargo.toml"), path);
1✔
621
                assert_eq!("1.2", version);
1✔
622
                assert_eq!(ComparisonOperator::Equal, comparator);
1✔
623
            }
UNCOV
624
            _ => panic!("Expected a version function"),
×
625
        }
626
    }
1✔
627

628
    #[test]
629
    fn function_parse_should_parse_a_version_not_equals_function() {
1✔
630
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", !=)").unwrap();
1✔
631

1✔
632
        assert!(output.0.is_empty());
1✔
633
        match output.1 {
1✔
634
            Function::Version(path, version, comparator) => {
1✔
635
                assert_eq!(Path::new("Cargo.toml"), path);
1✔
636
                assert_eq!("1.2", version);
1✔
637
                assert_eq!(ComparisonOperator::NotEqual, comparator);
1✔
638
            }
UNCOV
639
            _ => panic!("Expected a version function"),
×
640
        }
641
    }
1✔
642

643
    #[test]
644
    fn function_parse_should_parse_a_version_less_than_function() {
1✔
645
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", <)").unwrap();
1✔
646

1✔
647
        assert!(output.0.is_empty());
1✔
648
        match output.1 {
1✔
649
            Function::Version(path, version, comparator) => {
1✔
650
                assert_eq!(Path::new("Cargo.toml"), path);
1✔
651
                assert_eq!("1.2", version);
1✔
652
                assert_eq!(ComparisonOperator::LessThan, comparator);
1✔
653
            }
UNCOV
654
            _ => panic!("Expected a version function"),
×
655
        }
656
    }
1✔
657

658
    #[test]
659
    fn function_parse_should_parse_a_version_greater_than_function() {
1✔
660
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", >)").unwrap();
1✔
661

1✔
662
        assert!(output.0.is_empty());
1✔
663
        match output.1 {
1✔
664
            Function::Version(path, version, comparator) => {
1✔
665
                assert_eq!(Path::new("Cargo.toml"), path);
1✔
666
                assert_eq!("1.2", version);
1✔
667
                assert_eq!(ComparisonOperator::GreaterThan, comparator);
1✔
668
            }
UNCOV
669
            _ => panic!("Expected a version function"),
×
670
        }
671
    }
1✔
672

673
    #[test]
674
    fn function_parse_should_parse_a_version_less_than_or_equal_to_function() {
1✔
675
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", <=)").unwrap();
1✔
676

1✔
677
        assert!(output.0.is_empty());
1✔
678
        match output.1 {
1✔
679
            Function::Version(path, version, comparator) => {
1✔
680
                assert_eq!(Path::new("Cargo.toml"), path);
1✔
681
                assert_eq!("1.2", version);
1✔
682
                assert_eq!(ComparisonOperator::LessThanOrEqual, comparator);
1✔
683
            }
UNCOV
684
            _ => panic!("Expected a version function"),
×
685
        }
686
    }
1✔
687

688
    #[test]
689
    fn function_parse_should_parse_a_version_greater_than_or_equal_to_function() {
1✔
690
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", >=)").unwrap();
1✔
691

1✔
692
        assert!(output.0.is_empty());
1✔
693
        match output.1 {
1✔
694
            Function::Version(path, version, comparator) => {
1✔
695
                assert_eq!(Path::new("Cargo.toml"), path);
1✔
696
                assert_eq!("1.2", version);
1✔
697
                assert_eq!(ComparisonOperator::GreaterThanOrEqual, comparator);
1✔
698
            }
UNCOV
699
            _ => panic!("Expected a version function"),
×
700
        }
701
    }
1✔
702

703
    #[test]
704
    fn function_parse_should_parse_a_version_with_a_path_containing_backslashes() {
1✔
705
        let output = Function::parse("version(\"..\\Cargo.toml\", \"1.2\", ==)").unwrap();
1✔
706

1✔
707
        assert!(output.0.is_empty());
1✔
708
        match output.1 {
1✔
709
            Function::Version(path, version, comparator) => {
1✔
710
                assert_eq!(Path::new("..\\Cargo.toml"), path);
1✔
711
                assert_eq!("1.2", version);
1✔
712
                assert_eq!(ComparisonOperator::Equal, comparator);
1✔
713
            }
UNCOV
714
            _ => panic!("Expected a version function"),
×
715
        }
716
    }
1✔
717

718
    #[test]
719
    fn function_parse_should_error_if_the_version_path_is_outside_the_game_directory() {
1✔
720
        assert!(Function::parse("version(\"../../Cargo.toml\", \"1.2\", ==)").is_err());
1✔
721
    }
1✔
722

723
    #[test]
724
    fn function_parse_should_parse_a_product_version_equals_function() {
1✔
725
        let output = Function::parse("product_version(\"Cargo.toml\", \"1.2\", ==)").unwrap();
1✔
726

1✔
727
        assert!(output.0.is_empty());
1✔
728
        match output.1 {
1✔
729
            Function::ProductVersion(path, version, comparator) => {
1✔
730
                assert_eq!(Path::new("Cargo.toml"), path);
1✔
731
                assert_eq!("1.2", version);
1✔
732
                assert_eq!(ComparisonOperator::Equal, comparator);
1✔
733
            }
UNCOV
734
            _ => panic!("Expected a product version function"),
×
735
        }
736
    }
1✔
737

738
    #[test]
739
    fn function_parse_should_error_if_the_product_version_path_is_outside_the_game_directory() {
1✔
740
        assert!(Function::parse("product_version(\"../../Cargo.toml\", \"1.2\", ==)").is_err());
1✔
741
    }
1✔
742

743
    #[test]
744
    fn function_parse_should_parse_a_filename_version_equals_function() {
1✔
745
        let output =
1✔
746
            Function::parse("filename_version(\"subdir/Cargo (.+).toml\", \"1.2\", ==)").unwrap();
1✔
747

1✔
748
        assert!(output.0.is_empty());
1✔
749
        match output.1 {
1✔
750
            Function::FilenameVersion(path, regex, version, comparator) => {
1✔
751
                assert_eq!(PathBuf::from("subdir"), path);
1✔
752
                assert_eq!(
1✔
753
                    Regex::new("^Cargo (.+).toml$").unwrap().as_str(),
1✔
754
                    regex.as_str()
1✔
755
                );
1✔
756
                assert_eq!("1.2", version);
1✔
757
                assert_eq!(ComparisonOperator::Equal, comparator);
1✔
758
            }
UNCOV
759
            _ => panic!("Expected a filename version function"),
×
760
        }
761
    }
1✔
762

763
    #[test]
764
    fn function_parse_should_error_if_the_filename_version_regex_does_not_contain_an_explicit_capture_group(
1✔
765
    ) {
1✔
766
        assert!(
1✔
767
            Function::parse("filename_version(\"subdir/Cargo .+.toml\", \"1.2\", ==)").is_err()
1✔
768
        );
1✔
769
    }
1✔
770

771
    #[test]
772
    fn function_parse_should_parse_a_description_contains_function() {
1✔
773
        let lowercase_non_ascii = "\u{20ac}\u{192}.";
1✔
774
        let function = format!("description_contains(\"Blank.esp\", \"{lowercase_non_ascii}\")");
1✔
775
        let output = Function::parse(&function).unwrap();
1✔
776

1✔
777
        assert!(output.0.is_empty());
1✔
778
        match output.1 {
1✔
779
            Function::DescriptionContains(p, r) => {
1✔
780
                assert_eq!(PathBuf::from("Blank.esp"), p);
1✔
781
                assert_eq!(
1✔
782
                    Regex::new(lowercase_non_ascii).unwrap().as_str(),
1✔
783
                    r.as_str()
1✔
784
                );
1✔
785
            }
786
            _ => panic!("Expected a description_contains function"),
×
787
        }
788
    }
1✔
789
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc