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

loot / loot-condition-interpreter / 13353805198

16 Feb 2025 09:13AM UTC coverage: 91.413% (+1.6%) from 89.787%
13353805198

push

github

Ortham
Update versions and changelog for v5.1.0

4929 of 5392 relevant lines covered (91.41%)

15.81 hits per line

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

95.59
/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> {
17✔
19
        alt((
17✔
20
            value(ComparisonOperator::Equal, tag("==")),
17✔
21
            value(ComparisonOperator::NotEqual, tag("!=")),
17✔
22
            value(ComparisonOperator::LessThanOrEqual, tag("<=")),
17✔
23
            value(ComparisonOperator::GreaterThanOrEqual, tag(">=")),
17✔
24
            value(ComparisonOperator::LessThan, tag("<")),
17✔
25
            value(ComparisonOperator::GreaterThan, tag(">")),
17✔
26
        ))
17✔
27
        .parse(input)
17✔
28
    }
17✔
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 {
73✔
36
    let mut previous_component = Component::CurDir;
73✔
37
    for component in path.components() {
86✔
38
        match (component, previous_component) {
86✔
39
            (Component::Prefix(_), _) => return false,
×
40
            (Component::RootDir, _) => return false,
×
41
            (Component::ParentDir, Component::ParentDir) => return false,
12✔
42
            (Component::CurDir, _) => continue,
5✔
43
            _ => previous_component = component,
69✔
44
        }
45
    }
46

47
    true
61✔
48
}
73✔
49

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

192
    if is_in_game_path(&path) {
35✔
193
        Ok((remaining_input, path))
27✔
194
    } else {
195
        Err(not_in_game_directory(input, path))
8✔
196
    }
197
}
35✔
198

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

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

9✔
210
    let (parent_path_slice, regex_slice) = string
9✔
211
        .rfind('/')
9✔
212
        .map(|i| (&string[..i], &string[i + 1..]))
9✔
213
        .unwrap_or_else(|| (".", string));
9✔
214

9✔
215
    let parent_path = PathBuf::from(parent_path_slice);
9✔
216

9✔
217
    if !is_in_game_path(&parent_path) {
9✔
218
        return Err(not_in_game_directory(input, parent_path));
×
219
    }
9✔
220

221
    let regex = parse_anchored_regex(regex_slice)?.1;
9✔
222

223
    Ok((remaining_input, (parent_path, regex)))
8✔
224
}
12✔
225

226
fn parse_regex_filename(input: &str) -> ParsingResult<Regex> {
2✔
227
    map_parser(is_not(INVALID_REGEX_PATH_CHARS), parse_anchored_regex).parse(input)
2✔
228
}
2✔
229

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

360
#[cfg(test)]
361
mod tests {
362
    use super::*;
363

364
    use std::path::Path;
365

366
    #[test]
367
    fn parse_regex_should_produce_case_insensitive_regex() {
1✔
368
        let (_, regex) = parse_regex("cargo.*").unwrap();
1✔
369

1✔
370
        assert!(regex.is_match("Cargo.toml"));
1✔
371
    }
1✔
372

373
    #[test]
374
    fn parse_regex_should_produce_a_regex_that_does_partially_match() {
1✔
375
        let (_, regex) = parse_regex("argo.").unwrap();
1✔
376

1✔
377
        assert!(regex.is_match("Cargo.toml"));
1✔
378
    }
1✔
379

380
    #[test]
381
    fn parse_anchored_regex_should_produce_case_insensitive_regex() {
1✔
382
        let (_, regex) = parse_anchored_regex("cargo.*").unwrap();
1✔
383

1✔
384
        assert!(regex.is_match("Cargo.toml"));
1✔
385
    }
1✔
386

387
    #[test]
388
    fn parse_anchored_regex_should_produce_a_regex_that_does_not_partially_match() {
1✔
389
        let (_, regex) = parse_anchored_regex("cargo.").unwrap();
1✔
390

1✔
391
        assert!(!regex.is_match("Cargo.toml"));
1✔
392
    }
1✔
393

394
    #[test]
395
    fn function_parse_should_parse_a_file_path_function() {
1✔
396
        let output = Function::parse("file(\"Cargo.toml\")").unwrap();
1✔
397

1✔
398
        assert!(output.0.is_empty());
1✔
399
        match output.1 {
1✔
400
            Function::FilePath(f) => assert_eq!(Path::new("Cargo.toml"), f),
1✔
401
            _ => panic!("Expected a file path function"),
×
402
        }
403
    }
1✔
404

405
    #[test]
406
    fn function_parse_should_error_if_the_file_path_is_outside_the_game_directory() {
1✔
407
        assert!(Function::parse("file(\"../../Cargo.toml\")").is_err());
1✔
408
    }
1✔
409

410
    #[test]
411
    fn function_parse_should_parse_a_file_regex_function_with_no_parent_path() {
1✔
412
        let output = Function::parse("file(\"Cargo.*\")").unwrap();
1✔
413

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

424
    #[test]
425
    fn function_parse_should_parse_a_file_regex_function_with_a_parent_path() {
1✔
426
        let output = Function::parse("file(\"subdir/Cargo.*\")").unwrap();
1✔
427

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

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

443
    #[test]
444
    fn function_parse_should_error_if_the_file_regex_parent_path_is_outside_the_game_directory() {
1✔
445
        assert!(Function::parse("file(\"../../Cargo.*\")").is_err());
1✔
446
    }
1✔
447

448
    #[test]
449
    fn function_parse_should_parse_a_file_size_function() {
1✔
450
        let output = Function::parse("file_size(\"Cargo.toml\", 1234)").unwrap();
1✔
451

1✔
452
        assert!(output.0.is_empty());
1✔
453
        match output.1 {
1✔
454
            Function::FileSize(f, s) => {
1✔
455
                assert_eq!(Path::new("Cargo.toml"), f);
1✔
456
                assert_eq!(1234, s);
1✔
457
            }
458
            _ => panic!("Expected a file size function"),
×
459
        }
460
    }
1✔
461

462
    #[test]
463
    fn function_parse_should_error_if_the_file_size_is_outside_the_game_directory() {
1✔
464
        assert!(Function::parse("file_size(\"../../Cargo.toml\", 1234)").is_err());
1✔
465
    }
1✔
466

467
    #[test]
468
    fn function_parse_should_parse_a_readable_function() {
1✔
469
        let output = Function::parse("readable(\"Cargo.toml\")").unwrap();
1✔
470

1✔
471
        assert!(output.0.is_empty());
1✔
472
        match output.1 {
1✔
473
            Function::Readable(f) => assert_eq!(Path::new("Cargo.toml"), f),
1✔
474
            _ => panic!("Expected a readable function"),
×
475
        }
476
    }
1✔
477

478
    #[test]
479
    fn function_parse_should_error_if_the_readable_path_is_outside_the_game_directory() {
1✔
480
        assert!(Function::parse("readable(\"../../Cargo.toml\")").is_err());
1✔
481
    }
1✔
482

483
    #[test]
484
    fn function_parse_should_parse_an_is_executable_function() {
1✔
485
        let output = Function::parse("is_executable(\"Cargo.toml\")").unwrap();
1✔
486

1✔
487
        assert!(output.0.is_empty());
1✔
488
        match output.1 {
1✔
489
            Function::IsExecutable(f) => assert_eq!(Path::new("Cargo.toml"), f),
1✔
490
            _ => panic!("Expected an is_executable function"),
×
491
        }
492
    }
1✔
493

494
    #[test]
495
    fn function_parse_should_error_if_the_is_executable_path_is_outside_the_game_directory() {
1✔
496
        assert!(Function::parse("is_executable(\"../../Cargo.toml\")").is_err());
1✔
497
    }
1✔
498

499
    #[test]
500
    fn function_parse_should_parse_an_active_path_function() {
1✔
501
        let output = Function::parse("active(\"Cargo.toml\")").unwrap();
1✔
502

1✔
503
        assert!(output.0.is_empty());
1✔
504
        match output.1 {
1✔
505
            Function::ActivePath(f) => assert_eq!(Path::new("Cargo.toml"), f),
1✔
506
            _ => panic!("Expected an active path function"),
×
507
        }
508
    }
1✔
509

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

517
    #[test]
518
    fn function_parse_should_parse_an_active_regex_function() {
1✔
519
        let output = Function::parse("active(\"Cargo.*\")").unwrap();
1✔
520

1✔
521
        assert!(output.0.is_empty());
1✔
522
        match output.1 {
1✔
523
            Function::ActiveRegex(r) => {
1✔
524
                assert_eq!(Regex::new("^Cargo.*$").unwrap().as_str(), r.as_str())
1✔
525
            }
526
            _ => panic!("Expected an active regex function"),
×
527
        }
528
    }
1✔
529

530
    #[test]
531
    fn function_parse_should_parse_an_is_master_function() {
1✔
532
        let output = Function::parse("is_master(\"Blank.esm\")").unwrap();
1✔
533

1✔
534
        assert!(output.0.is_empty());
1✔
535
        match output.1 {
1✔
536
            Function::IsMaster(f) => assert_eq!(Path::new("Blank.esm"), f),
1✔
537
            _ => panic!("Expected an is master function"),
×
538
        }
539
    }
1✔
540

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

548
    #[test]
549
    fn function_parse_should_parse_a_many_function_with_no_parent_path() {
1✔
550
        let output = Function::parse("many(\"Cargo.*\")").unwrap();
1✔
551

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

562
    #[test]
563
    fn function_parse_should_parse_a_many_function_with_a_parent_path() {
1✔
564
        let output = Function::parse("many(\"subdir/Cargo.*\")").unwrap();
1✔
565

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

576
    #[test]
577
    fn function_parse_should_error_if_given_a_many_function_ending_in_a_forward_slash() {
1✔
578
        assert!(Function::parse("many(\"subdir/\")").is_err());
1✔
579
    }
1✔
580

581
    #[test]
582
    fn function_parse_should_error_if_the_many_parent_path_is_outside_the_game_directory() {
1✔
583
        assert!(Function::parse("file(\"../../Cargo.*\")").is_err());
1✔
584
    }
1✔
585

586
    #[test]
587
    fn function_parse_should_parse_a_many_active_function() {
1✔
588
        let output = Function::parse("many_active(\"Cargo.*\")").unwrap();
1✔
589

1✔
590
        assert!(output.0.is_empty());
1✔
591
        match output.1 {
1✔
592
            Function::ManyActive(r) => {
1✔
593
                assert_eq!(Regex::new("^Cargo.*$").unwrap().as_str(), r.as_str())
1✔
594
            }
595
            _ => panic!("Expected a many active function"),
×
596
        }
597
    }
1✔
598

599
    #[test]
600
    fn function_parse_should_parse_a_checksum_function() {
1✔
601
        let output = Function::parse("checksum(\"Cargo.toml\", DEADBEEF)").unwrap();
1✔
602

1✔
603
        assert!(output.0.is_empty());
1✔
604
        match output.1 {
1✔
605
            Function::Checksum(path, crc) => {
1✔
606
                assert_eq!(Path::new("Cargo.toml"), path);
1✔
607
                assert_eq!(0xDEADBEEF, crc);
1✔
608
            }
609
            _ => panic!("Expected a checksum function"),
×
610
        }
611
    }
1✔
612

613
    #[test]
614
    fn function_parse_should_error_if_the_checksum_path_is_outside_the_game_directory() {
1✔
615
        assert!(Function::parse("checksum(\"../../Cargo.toml\", DEADBEEF)").is_err());
1✔
616
    }
1✔
617

618
    #[test]
619
    fn function_parse_should_parse_a_version_equals_function() {
1✔
620
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", ==)").unwrap();
1✔
621

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

633
    #[test]
634
    fn function_parse_should_parse_a_version_not_equals_function() {
1✔
635
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", !=)").unwrap();
1✔
636

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

648
    #[test]
649
    fn function_parse_should_parse_a_version_less_than_function() {
1✔
650
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", <)").unwrap();
1✔
651

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

663
    #[test]
664
    fn function_parse_should_parse_a_version_greater_than_function() {
1✔
665
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", >)").unwrap();
1✔
666

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

678
    #[test]
679
    fn function_parse_should_parse_a_version_less_than_or_equal_to_function() {
1✔
680
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", <=)").unwrap();
1✔
681

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

693
    #[test]
694
    fn function_parse_should_parse_a_version_greater_than_or_equal_to_function() {
1✔
695
        let output = Function::parse("version(\"Cargo.toml\", \"1.2\", >=)").unwrap();
1✔
696

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

708
    #[test]
709
    fn function_parse_should_parse_a_version_with_a_path_containing_backslashes() {
1✔
710
        let output = Function::parse("version(\"..\\Cargo.toml\", \"1.2\", ==)").unwrap();
1✔
711

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

723
    #[test]
724
    fn function_parse_should_error_if_the_version_path_is_outside_the_game_directory() {
1✔
725
        assert!(Function::parse("version(\"../../Cargo.toml\", \"1.2\", ==)").is_err());
1✔
726
    }
1✔
727

728
    #[test]
729
    fn function_parse_should_parse_a_product_version_equals_function() {
1✔
730
        let output = Function::parse("product_version(\"Cargo.toml\", \"1.2\", ==)").unwrap();
1✔
731

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

743
    #[test]
744
    fn function_parse_should_error_if_the_product_version_path_is_outside_the_game_directory() {
1✔
745
        assert!(Function::parse("product_version(\"../../Cargo.toml\", \"1.2\", ==)").is_err());
1✔
746
    }
1✔
747

748
    #[test]
749
    fn function_parse_should_parse_a_filename_version_equals_function() {
1✔
750
        let output =
1✔
751
            Function::parse("filename_version(\"subdir/Cargo (.+).toml\", \"1.2\", ==)").unwrap();
1✔
752

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

768
    #[test]
769
    fn function_parse_should_error_if_the_filename_version_regex_does_not_contain_an_explicit_capture_group(
1✔
770
    ) {
1✔
771
        assert!(
1✔
772
            Function::parse("filename_version(\"subdir/Cargo .+.toml\", \"1.2\", ==)").is_err()
1✔
773
        );
1✔
774
    }
1✔
775

776
    #[test]
777
    fn function_parse_should_parse_a_description_contains_function() {
1✔
778
        let output = Function::parse("description_contains(\"Blank.esp\", \"€ƒ.\")").unwrap();
1✔
779

1✔
780
        assert!(output.0.is_empty());
1✔
781
        match output.1 {
1✔
782
            Function::DescriptionContains(p, r) => {
1✔
783
                assert_eq!(PathBuf::from("Blank.esp"), p);
1✔
784
                assert_eq!(Regex::new("€ƒ.").unwrap().as_str(), r.as_str());
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