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

loot / loot-condition-interpreter / 19803524316

30 Nov 2025 07:07PM UTC coverage: 89.921%. Remained the same
19803524316

push

github

Ortham
Remove pelite dependency

The advantages of using object instead of pelite are:

- It's 1 dependency instead of 8
- It's 57474 unaudited lines of code instead of 240715 (though if I swap out winapi for windows-sys that drops to 59392, since windows-sys is also used by other dependencies)
- object seems to be better maintained - it's had a few releases this year, while pelite hasn't had any in 3 years, and my PR for replacing winapi hasn't had any response in the month and a half it's been open
- object is more popular, with 238 dependents vs 33, and though it's about half a year newer it's got about 600x more downloads

I'm not sure how much heavy lifting object is doing for me - it might not be too much effort to do everything I need without it.

6 of 6 new or added lines in 1 file covered. (100.0%)

13 existing lines in 2 files now uncovered.

4345 of 4832 relevant lines covered (89.92%)

29.7 hits per line

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

99.84
/src/function/version.rs
1
mod pe;
2

3
use std::cmp::Ordering;
4
use std::path::Path;
5

6
use crate::error::Error;
7
use pe::{read_file_version, read_product_version, read_version_info_data};
8

9
#[derive(Clone, Debug)]
10
enum ReleaseId {
11
    Numeric(u32),
12
    NonNumeric(String),
13
}
14

15
impl<'a> From<&'a str> for ReleaseId {
16
    fn from(string: &'a str) -> Self {
1,572✔
17
        string.trim().parse().map_or_else(
1,572✔
18
            |_| ReleaseId::NonNumeric(string.to_lowercase()),
52✔
19
            ReleaseId::Numeric,
20
        )
21
    }
1,572✔
22
}
23

24
fn are_numeric_values_equal(n: u32, s: &str) -> bool {
12✔
25
    // The values can only be equal if the trimmed string can be wholly
26
    // converted to the same u32 value.
27
    match s.trim().parse() {
12✔
28
        Ok(n2) => n == n2,
8✔
29
        Err(_) => false,
4✔
30
    }
31
}
12✔
32

33
impl PartialEq for ReleaseId {
34
    fn eq(&self, other: &Self) -> bool {
418✔
35
        match (self, other) {
418✔
36
            (Self::Numeric(n1), Self::Numeric(n2)) => n1 == n2,
392✔
37
            (Self::NonNumeric(s1), Self::NonNumeric(s2)) => s1 == s2,
14✔
38
            (Self::Numeric(n), Self::NonNumeric(s)) | (Self::NonNumeric(s), Self::Numeric(n)) => {
6✔
39
                are_numeric_values_equal(*n, s)
12✔
40
            }
41
        }
42
    }
418✔
43
}
44

45
// This is like u32::from_str_radix(), but stops instead of erroring when it
46
// encounters a non-digit character. It also doesn't support signs.
47
fn u32_from_str(id: &str) -> (Option<u32>, usize) {
24✔
48
    // Conversion can fail even with only ASCII digits because of overflow, so
49
    // take that into account.
50
    if let Some((digits, remainder)) = id.split_once(|c: char| !c.is_ascii_digit()) {
56✔
51
        if digits.is_empty() {
20✔
52
            (None, id.len())
4✔
53
        } else {
54
            (digits.trim().parse().ok(), remainder.len() + 1)
16✔
55
        }
56
    } else {
57
        (id.trim().parse().ok(), 0)
4✔
58
    }
59
}
24✔
60

61
fn compare_heterogeneous_ids(lhs_number: u32, rhs_string: &str) -> Option<Ordering> {
24✔
62
    match u32_from_str(rhs_string) {
24✔
63
        (Some(rhs_number), remaining_slice_length) => {
20✔
64
            match lhs_number.partial_cmp(&rhs_number) {
20✔
65
                // If not all bytes were digits, treat the non-numeric ID as
66
                // greater.
67
                Some(Ordering::Equal) if remaining_slice_length > 0 => Some(Ordering::Less),
12✔
68
                order => order,
12✔
69
            }
70
        }
71
        // If there are no digits to compare, numeric values are
72
        // always less than non-numeric values.
73
        (None, _) => Some(Ordering::Less),
4✔
74
    }
75
}
24✔
76

77
impl PartialOrd for ReleaseId {
78
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
438✔
79
        match (self, other) {
438✔
80
            (Self::Numeric(n1), Self::Numeric(n2)) => n1.partial_cmp(n2),
396✔
81
            (Self::NonNumeric(s1), Self::NonNumeric(s2)) => s1.partial_cmp(s2),
18✔
82
            (Self::Numeric(n), Self::NonNumeric(s)) => compare_heterogeneous_ids(*n, s),
12✔
83
            (Self::NonNumeric(s), Self::Numeric(n)) => {
12✔
84
                compare_heterogeneous_ids(*n, s).map(Ordering::reverse)
12✔
85
            }
86
        }
87
    }
438✔
88
}
89

90
#[derive(Clone, Debug, PartialEq, PartialOrd)]
91
enum PreReleaseId {
92
    Numeric(u32),
93
    NonNumeric(String),
94
}
95

96
impl<'a> From<&'a str> for PreReleaseId {
97
    fn from(string: &'a str) -> Self {
160✔
98
        string.trim().parse().map_or_else(
160✔
99
            |_| PreReleaseId::NonNumeric(string.to_lowercase()),
72✔
100
            PreReleaseId::Numeric,
101
        )
102
    }
160✔
103
}
104

105
#[derive(Debug)]
106
pub(super) struct Version {
107
    release_ids: Vec<ReleaseId>,
108
    pre_release_ids: Vec<PreReleaseId>,
109
}
110

111
impl Version {
112
    pub(super) fn read_file_version(file_path: &Path) -> Result<Option<Self>, Error> {
12✔
113
        if let Some(data) = read_version_info_data(file_path)? {
12✔
114
            read_file_version(&data)
6✔
115
                .map_err(|e| Error::PeParsingError(file_path.to_path_buf(), e.into()))
6✔
116
        } else {
117
            Ok(None)
2✔
118
        }
119
    }
12✔
120

121
    pub(super) fn read_product_version(file_path: &Path) -> Result<Option<Self>, Error> {
18✔
122
        if let Some(data) = read_version_info_data(file_path)? {
18✔
123
            read_product_version(&data)
10✔
124
                .map_err(|e| Error::PeParsingError(file_path.to_path_buf(), e.into()))
10✔
125
        } else {
126
            Ok(None)
2✔
127
        }
128
    }
18✔
129

130
    pub(super) fn is_readable(file_path: &Path) -> bool {
10✔
131
        read_version_info_data(file_path).is_ok()
10✔
132
    }
10✔
133
}
134

135
fn is_separator(c: char) -> bool {
3,294✔
136
    c == '-' || c == ' ' || c == ':' || c == '_'
3,294✔
137
}
3,294✔
138

139
fn is_pre_release_separator(c: char) -> bool {
440✔
140
    c == '.' || is_separator(c)
440✔
141
}
440✔
142

143
fn split_version_string(string: &str) -> (&str, &str) {
568✔
144
    // Special case for strings of the form "0, 1, 2, 3", which are used in
145
    // OBSE and SKSE, and which should be interpreted as "0.1.2.3".
146
    if let Ok(regex) = regex::Regex::new("\\d+, \\d+, \\d+, \\d+") {
568✔
147
        if regex.is_match(string) {
568✔
148
            return (string, "");
2✔
149
        }
566✔
UNCOV
150
    }
×
151

152
    string.split_once(is_separator).unwrap_or((string, ""))
566✔
153
}
568✔
154

155
impl<T: AsRef<str>> From<T> for Version {
156
    fn from(string: T) -> Self {
568✔
157
        let (release, pre_release) = split_version_string(trim_metadata(string.as_ref()));
568✔
158

159
        Version {
568✔
160
            release_ids: release.split(['.', ',']).map(ReleaseId::from).collect(),
568✔
161
            pre_release_ids: pre_release
568✔
162
                .split_terminator(is_pre_release_separator)
568✔
163
                .map(PreReleaseId::from)
568✔
164
                .collect(),
568✔
165
        }
568✔
166
    }
568✔
167
}
168

169
fn trim_metadata(version: &str) -> &str {
568✔
170
    if version.is_empty() {
568✔
171
        "0"
16✔
172
    } else if let Some((prefix, _)) = version.split_once('+') {
552✔
173
        prefix
20✔
174
    } else {
175
        version
532✔
176
    }
177
}
568✔
178

179
impl PartialOrd for Version {
180
    fn partial_cmp(&self, other: &Version) -> Option<Ordering> {
156✔
181
        let (self_release_ids, other_release_ids) =
156✔
182
            pad_release_ids(&self.release_ids, &other.release_ids);
156✔
183

184
        match self_release_ids.partial_cmp(&other_release_ids) {
156✔
185
            Some(Ordering::Equal) | None => {
186
                match (
70✔
187
                    self.pre_release_ids.is_empty(),
70✔
188
                    other.pre_release_ids.is_empty(),
70✔
189
                ) {
70✔
190
                    (true, false) => Some(Ordering::Greater),
2✔
191
                    (false, true) => Some(Ordering::Less),
2✔
192
                    _ => self.pre_release_ids.partial_cmp(&other.pre_release_ids),
66✔
193
                }
194
            }
195
            r => r,
86✔
196
        }
197
    }
156✔
198
}
199

200
impl PartialEq for Version {
201
    fn eq(&self, other: &Version) -> bool {
118✔
202
        let (self_release_ids, other_release_ids) =
118✔
203
            pad_release_ids(&self.release_ids, &other.release_ids);
118✔
204

205
        self_release_ids == other_release_ids && self.pre_release_ids == other.pre_release_ids
118✔
206
    }
118✔
207
}
208

209
fn pad_release_ids(ids1: &[ReleaseId], ids2: &[ReleaseId]) -> (Vec<ReleaseId>, Vec<ReleaseId>) {
274✔
210
    let mut ids1 = ids1.to_vec();
274✔
211
    let mut ids2 = ids2.to_vec();
274✔
212

213
    match ids1.len().cmp(&ids2.len()) {
274✔
214
        Ordering::Less => ids1.resize(ids2.len(), ReleaseId::Numeric(0)),
12✔
215
        Ordering::Greater => ids2.resize(ids1.len(), ReleaseId::Numeric(0)),
12✔
216
        Ordering::Equal => {}
250✔
217
    }
218

219
    (ids1, ids2)
274✔
220
}
274✔
221

222
#[cfg(test)]
223
mod tests {
224
    fn is_cmp_eq(lhs: &super::Version, rhs: &super::Version) -> bool {
30✔
225
        lhs.partial_cmp(rhs).unwrap().is_eq()
30✔
226
    }
30✔
227

228
    mod release_ids {
229
        use super::super::*;
230

231
        #[test]
232
        fn eq_should_compare_equality_of_u32_values() {
2✔
233
            assert_eq!(ReleaseId::Numeric(1), ReleaseId::Numeric(1));
2✔
234
            assert_ne!(ReleaseId::Numeric(1), ReleaseId::Numeric(0));
2✔
235
        }
2✔
236

237
        #[test]
238
        fn eq_should_compare_equality_of_string_values() {
2✔
239
            assert_eq!(
2✔
240
                ReleaseId::NonNumeric("abcd".into()),
2✔
241
                ReleaseId::NonNumeric("abcd".into())
2✔
242
            );
243
            assert_ne!(
2✔
244
                ReleaseId::NonNumeric("abcd".into()),
2✔
245
                ReleaseId::NonNumeric("abce".into())
2✔
246
            );
247
        }
2✔
248

249
        #[test]
250
        fn eq_should_convert_string_values_to_u32_before_comparing_against_a_u32_value() {
2✔
251
            assert_eq!(ReleaseId::Numeric(123), ReleaseId::NonNumeric("123".into()));
2✔
252
            assert_eq!(
2✔
253
                ReleaseId::Numeric(123),
254
                ReleaseId::NonNumeric(" 123 ".into())
2✔
255
            );
256

257
            assert_ne!(
2✔
258
                ReleaseId::Numeric(123),
259
                ReleaseId::NonNumeric("1two3".into())
2✔
260
            );
261

262
            assert_eq!(ReleaseId::NonNumeric("123".into()), ReleaseId::Numeric(123));
2✔
263
            assert_eq!(
2✔
264
                ReleaseId::NonNumeric(" 123 ".into()),
2✔
265
                ReleaseId::Numeric(123)
266
            );
267

268
            assert_ne!(
2✔
269
                ReleaseId::NonNumeric("1two3".into()),
2✔
270
                ReleaseId::Numeric(123)
271
            );
272
        }
2✔
273

274
        #[test]
275
        fn cmp_should_compare_u32_values() {
2✔
276
            let cmp = ReleaseId::Numeric(1).partial_cmp(&ReleaseId::Numeric(1));
2✔
277
            assert_eq!(Some(Ordering::Equal), cmp);
2✔
278

279
            let cmp = ReleaseId::Numeric(1).partial_cmp(&ReleaseId::Numeric(2));
2✔
280
            assert_eq!(Some(Ordering::Less), cmp);
2✔
281

282
            let cmp = ReleaseId::Numeric(2).partial_cmp(&ReleaseId::Numeric(1));
2✔
283
            assert_eq!(Some(Ordering::Greater), cmp);
2✔
284
        }
2✔
285

286
        #[test]
287
        fn cmp_should_compare_string_values() {
2✔
288
            let cmp = ReleaseId::NonNumeric("alpha".into())
2✔
289
                .partial_cmp(&ReleaseId::NonNumeric("alpha".into()));
2✔
290
            assert_eq!(Some(Ordering::Equal), cmp);
2✔
291

292
            let cmp = ReleaseId::NonNumeric("alpha".into())
2✔
293
                .partial_cmp(&ReleaseId::NonNumeric("beta".into()));
2✔
294
            assert_eq!(Some(Ordering::Less), cmp);
2✔
295

296
            let cmp = ReleaseId::NonNumeric("beta".into())
2✔
297
                .partial_cmp(&ReleaseId::NonNumeric("alpha".into()));
2✔
298
            assert_eq!(Some(Ordering::Greater), cmp);
2✔
299
        }
2✔
300

301
        #[test]
302
        fn cmp_should_treat_strings_with_no_leading_digits_as_always_greater_than_u32s() {
2✔
303
            let cmp = ReleaseId::Numeric(123).partial_cmp(&ReleaseId::NonNumeric("one23".into()));
2✔
304
            assert_eq!(Some(Ordering::Less), cmp);
2✔
305

306
            let cmp = ReleaseId::NonNumeric("one23".into()).partial_cmp(&ReleaseId::Numeric(123));
2✔
307
            assert_eq!(Some(Ordering::Greater), cmp);
2✔
308
        }
2✔
309

310
        #[test]
311
        fn cmp_should_compare_leading_digits_in_strings_against_u32s_and_use_the_result_if_it_is_not_equal(
2✔
312
        ) {
2✔
313
            let cmp = ReleaseId::Numeric(86).partial_cmp(&ReleaseId::NonNumeric("78b".into()));
2✔
314
            assert_eq!(Some(Ordering::Greater), cmp);
2✔
315

316
            let cmp = ReleaseId::NonNumeric("78b".into()).partial_cmp(&ReleaseId::Numeric(86));
2✔
317
            assert_eq!(Some(Ordering::Less), cmp);
2✔
318
        }
2✔
319

320
        #[test]
321
        fn cmp_should_compare_leading_digits_in_strings_against_u32s_and_use_the_result_if_it_is_equal_and_there_are_no_non_digit_characters(
2✔
322
        ) {
2✔
323
            let cmp = ReleaseId::Numeric(86).partial_cmp(&ReleaseId::NonNumeric("86".into()));
2✔
324
            assert_eq!(Some(Ordering::Equal), cmp);
2✔
325

326
            let cmp = ReleaseId::NonNumeric("86".into()).partial_cmp(&ReleaseId::Numeric(86));
2✔
327
            assert_eq!(Some(Ordering::Equal), cmp);
2✔
328
        }
2✔
329

330
        #[test]
331
        fn cmp_should_compare_leading_digits_in_strings_against_u32s_and_treat_the_u32_as_less_if_the_result_is_equal_and_there_are_non_digit_characters(
2✔
332
        ) {
2✔
333
            let cmp = ReleaseId::Numeric(86).partial_cmp(&ReleaseId::NonNumeric("86b".into()));
2✔
334
            assert_eq!(Some(Ordering::Less), cmp);
2✔
335

336
            let cmp = ReleaseId::NonNumeric("86b".into()).partial_cmp(&ReleaseId::Numeric(86));
2✔
337
            assert_eq!(Some(Ordering::Greater), cmp);
2✔
338
        }
2✔
339
    }
340

341
    mod constructors {
342
        use super::super::*;
343

344
        #[test]
345
        fn version_read_file_version_should_read_the_file_version_field_of_a_32_bit_executable() {
2✔
346
            let version = Version::read_file_version(Path::new("tests/libloot_win32/loot.dll"))
2✔
347
                .unwrap()
2✔
348
                .unwrap();
2✔
349

350
            assert_eq!(
2✔
351
                version.release_ids,
352
                vec![
2✔
353
                    ReleaseId::Numeric(0),
2✔
354
                    ReleaseId::Numeric(18),
2✔
355
                    ReleaseId::Numeric(2),
2✔
356
                    ReleaseId::Numeric(0),
2✔
357
                ]
358
            );
359
            assert!(version.pre_release_ids.is_empty());
2✔
360
        }
2✔
361

362
        #[test]
363
        fn version_read_file_version_should_read_the_file_version_field_of_a_64_bit_executable() {
2✔
364
            let version = Version::read_file_version(Path::new("tests/libloot_win64/loot.dll"))
2✔
365
                .unwrap()
2✔
366
                .unwrap();
2✔
367

368
            assert_eq!(
2✔
369
                version.release_ids,
370
                vec![
2✔
371
                    ReleaseId::Numeric(0),
2✔
372
                    ReleaseId::Numeric(18),
2✔
373
                    ReleaseId::Numeric(2),
2✔
374
                    ReleaseId::Numeric(0),
2✔
375
                ]
376
            );
377
            assert!(version.pre_release_ids.is_empty());
2✔
378
        }
2✔
379

380
        #[test]
381
        fn version_read_file_version_should_error_with_path_if_path_does_not_exist() {
2✔
382
            let error = Version::read_file_version(Path::new("missing")).unwrap_err();
2✔
383

384
            assert!(error
2✔
385
                .to_string()
2✔
386
                .starts_with("An error was encountered while accessing the path \"missing\":"));
2✔
387
        }
2✔
388

389
        #[test]
390
        fn version_read_file_version_should_error_with_path_if_the_file_is_not_an_executable() {
2✔
391
            let error = Version::read_file_version(Path::new("Cargo.toml")).unwrap_err();
2✔
392

393
            assert_eq!("An error was encountered while reading the version fields of \"Cargo.toml\": Unknown file magic", error.to_string());
2✔
394
        }
2✔
395

396
        #[test]
397
        fn version_read_file_version_should_return_none_if_there_is_no_version_info() {
2✔
398
            let version =
2✔
399
                Version::read_file_version(Path::new("tests/loot_api_python/loot_api.pyd"))
2✔
400
                    .unwrap();
2✔
401

402
            assert!(version.is_none());
2✔
403
        }
2✔
404

405
        #[test]
406
        fn version_read_product_version_should_read_the_file_version_field_of_a_32_bit_executable()
2✔
407
        {
408
            let version = Version::read_product_version(Path::new("tests/libloot_win32/loot.dll"))
2✔
409
                .unwrap()
2✔
410
                .unwrap();
2✔
411

412
            assert_eq!(
2✔
413
                version.release_ids,
414
                vec![
2✔
415
                    ReleaseId::Numeric(0),
2✔
416
                    ReleaseId::Numeric(18),
2✔
417
                    ReleaseId::Numeric(2)
2✔
418
                ]
419
            );
420
            assert!(version.pre_release_ids.is_empty());
2✔
421
        }
2✔
422

423
        #[test]
424
        fn version_read_product_version_should_read_the_file_version_field_of_a_64_bit_executable()
2✔
425
        {
426
            let version = Version::read_product_version(Path::new("tests/libloot_win64/loot.dll"))
2✔
427
                .unwrap()
2✔
428
                .unwrap();
2✔
429

430
            assert_eq!(
2✔
431
                version.release_ids,
432
                vec![
2✔
433
                    ReleaseId::Numeric(0),
2✔
434
                    ReleaseId::Numeric(18),
2✔
435
                    ReleaseId::Numeric(2),
2✔
436
                ]
437
            );
438
            assert!(version.pre_release_ids.is_empty());
2✔
439
        }
2✔
440

441
        #[test]
442
        fn version_read_product_version_should_find_non_us_english_version_strings() {
2✔
443
            let tmp_dir = tempfile::tempdir().unwrap();
2✔
444
            let dll_path = tmp_dir.path().join("loot.ru.dll");
2✔
445

446
            let mut dll_bytes = std::fs::read("tests/libloot_win32/loot.dll").unwrap();
2✔
447

448
            // Set the version info block's language code to 1049 (Russian).
449
            dll_bytes[0x0053_204A] = b'1'; // This changes VersionInfo.strings.Language.lang_id
2✔
450
            dll_bytes[0x0053_216C] = 0x19; // This changes VersionInfo.langs.Language.lang_id
2✔
451

452
            std::fs::write(&dll_path, dll_bytes).unwrap();
2✔
453

454
            let version = Version::read_product_version(&dll_path).unwrap().unwrap();
2✔
455

456
            assert_eq!(
2✔
457
                version.release_ids,
458
                vec![
2✔
459
                    ReleaseId::Numeric(0),
2✔
460
                    ReleaseId::Numeric(18),
2✔
461
                    ReleaseId::Numeric(2)
2✔
462
                ]
463
            );
464
            assert!(version.pre_release_ids.is_empty());
2✔
465
        }
2✔
466

467
        #[test]
468
        fn version_read_product_version_should_error_with_path_if_path_does_not_exist() {
2✔
469
            let error = Version::read_product_version(Path::new("missing")).unwrap_err();
2✔
470

471
            assert!(error
2✔
472
                .to_string()
2✔
473
                .starts_with("An error was encountered while accessing the path \"missing\":"));
2✔
474
        }
2✔
475

476
        #[test]
477
        fn version_read_product_version_should_error_with_path_if_the_file_is_not_an_executable() {
2✔
478
            let error = Version::read_product_version(Path::new("Cargo.toml")).unwrap_err();
2✔
479

480
            assert_eq!("An error was encountered while reading the version fields of \"Cargo.toml\": Unknown file magic", error.to_string());
2✔
481
        }
2✔
482

483
        #[test]
484
        fn version_read_product_version_should_return_none_if_there_is_no_version_info() {
2✔
485
            let version =
2✔
486
                Version::read_product_version(Path::new("tests/loot_api_python/loot_api.pyd"))
2✔
487
                    .unwrap();
2✔
488

489
            assert!(version.is_none());
2✔
490
        }
2✔
491
    }
492

493
    mod empty {
494
        use super::super::*;
495
        #[test]
496
        fn version_eq_an_empty_string_should_equal_an_empty_string() {
2✔
497
            assert_eq!(Version::from(""), Version::from(""));
2✔
498
        }
2✔
499

500
        #[test]
501
        fn version_eq_an_empty_string_should_equal_a_version_of_zero() {
2✔
502
            assert_eq!(Version::from(""), Version::from("0"));
2✔
503
            assert_eq!(Version::from("0"), Version::from(""));
2✔
504
        }
2✔
505

506
        #[test]
507
        fn version_eq_an_empty_string_should_not_equal_a_non_zero_version() {
2✔
508
            assert_ne!(Version::from(""), Version::from("5"));
2✔
509
            assert_ne!(Version::from("5"), Version::from(""));
2✔
510
        }
2✔
511

512
        #[test]
513
        fn version_partial_cmp_an_empty_string_should_be_less_than_a_non_zero_version() {
2✔
514
            assert!(Version::from("") < Version::from("1"));
2✔
515
            assert!(Version::from("1") > Version::from(""));
2✔
516
        }
2✔
517
    }
518

519
    mod numeric {
520
        use super::super::*;
521

522
        #[test]
523
        fn version_eq_a_non_empty_string_should_equal_itself() {
2✔
524
            assert_eq!(Version::from("5"), Version::from("5"));
2✔
525
        }
2✔
526

527
        #[test]
528
        fn version_eq_single_digit_versions_should_compare_digits() {
2✔
529
            assert_eq!(Version::from("5"), Version::from("5"));
2✔
530

531
            assert_ne!(Version::from("4"), Version::from("5"));
2✔
532
            assert_ne!(Version::from("5"), Version::from("4"));
2✔
533
        }
2✔
534

535
        #[test]
536
        fn version_partial_cmp_single_digit_versions_should_compare_digits() {
2✔
537
            assert!(Version::from("4") < Version::from("5"));
2✔
538
            assert!(Version::from("5") > Version::from("4"));
2✔
539
        }
2✔
540

541
        #[test]
542
        fn version_eq_numeric_versions_should_compare_numbers() {
2✔
543
            assert_ne!(Version::from("5"), Version::from("10"));
2✔
544
            assert_ne!(Version::from("10"), Version::from("5"));
2✔
545
        }
2✔
546

547
        #[test]
548
        fn version_partial_cmp_numeric_versions_should_compare_numbers() {
2✔
549
            assert!(Version::from("5") < Version::from("10"));
2✔
550
            assert!(Version::from("10") > Version::from("5"));
2✔
551
        }
2✔
552
    }
553

554
    mod semver {
555
        use super::super::*;
556
        use super::is_cmp_eq;
557

558
        #[test]
559
        fn version_eq_should_compare_patch_numbers() {
2✔
560
            assert_eq!(Version::from("0.0.5"), Version::from("0.0.5"));
2✔
561

562
            assert_ne!(Version::from("0.0.5"), Version::from("0.0.10"));
2✔
563
            assert_ne!(Version::from("0.0.10"), Version::from("0.0.5"));
2✔
564
        }
2✔
565

566
        #[test]
567
        fn version_partial_cmp_should_compare_patch_numbers() {
2✔
568
            assert!(Version::from("0.0.5") < Version::from("0.0.10"));
2✔
569
            assert!(Version::from("0.0.10") > Version::from("0.0.5"));
2✔
570
        }
2✔
571

572
        #[test]
573
        fn version_eq_should_compare_minor_numbers() {
2✔
574
            assert_eq!(Version::from("0.5.0"), Version::from("0.5.0"));
2✔
575

576
            assert_ne!(Version::from("0.5.0"), Version::from("0.10.0"));
2✔
577
            assert_ne!(Version::from("0.10.0"), Version::from("0.5.0"));
2✔
578
        }
2✔
579

580
        #[test]
581
        fn version_partial_cmp_should_compare_minor_numbers() {
2✔
582
            assert!(Version::from("0.5.0") < Version::from("0.10.0"));
2✔
583
            assert!(Version::from("0.10.0") > Version::from("0.5.0"));
2✔
584
        }
2✔
585

586
        #[test]
587
        fn version_partial_cmp_minor_numbers_should_take_precedence_over_patch_numbers() {
2✔
588
            assert!(Version::from("0.5.10") < Version::from("0.10.5"));
2✔
589
            assert!(Version::from("0.10.5") > Version::from("0.5.10"));
2✔
590
        }
2✔
591

592
        #[test]
593
        fn version_eq_should_compare_major_numbers() {
2✔
594
            assert_eq!(Version::from("5.0.0"), Version::from("5.0.0"));
2✔
595

596
            assert_ne!(Version::from("5.0.0"), Version::from("10.0.0"));
2✔
597
            assert_ne!(Version::from("10.0.0"), Version::from("5.0.0"));
2✔
598
        }
2✔
599

600
        #[test]
601
        fn version_partial_cmp_should_compare_major_numbers() {
2✔
602
            assert!(Version::from("5.0.0") < Version::from("10.0.0"));
2✔
603
            assert!(Version::from("10.0.0") > Version::from("5.0.0"));
2✔
604
        }
2✔
605

606
        #[test]
607
        fn version_partial_cmp_major_numbers_should_take_precedence_over_minor_numbers() {
2✔
608
            assert!(Version::from("5.10.0") < Version::from("10.5.0"));
2✔
609
            assert!(Version::from("10.5.0") > Version::from("5.10.0"));
2✔
610
        }
2✔
611

612
        #[test]
613
        fn version_partial_cmp_major_numbers_should_take_precedence_over_patch_numbers() {
2✔
614
            assert!(Version::from("5.0.10") < Version::from("10.0.5"));
2✔
615
            assert!(Version::from("10.0.5") > Version::from("5.0.10"));
2✔
616
        }
2✔
617

618
        #[test]
619
        fn version_eq_should_consider_versions_that_differ_by_the_presence_of_a_pre_release_id_to_be_not_equal(
2✔
620
        ) {
2✔
621
            assert_ne!(Version::from("1.0.0"), Version::from("1.0.0-alpha"));
2✔
622
        }
2✔
623

624
        #[test]
625
        fn version_partial_cmp_should_treat_the_absence_of_a_pre_release_id_as_greater_than_its_presence(
2✔
626
        ) {
2✔
627
            assert!(Version::from("1.0.0-alpha") < Version::from("1.0.0"));
2✔
628
            assert!(Version::from("1.0.0") > Version::from("1.0.0-alpha"));
2✔
629
        }
2✔
630

631
        #[test]
632
        fn version_eq_should_compare_pre_release_identifiers() {
2✔
633
            assert_eq!(
2✔
634
                Version::from("0.0.5-5.alpha"),
2✔
635
                Version::from("0.0.5-5.alpha")
2✔
636
            );
637

638
            assert_ne!(
2✔
639
                Version::from("0.0.5-5.alpha"),
2✔
640
                Version::from("0.0.5-10.beta")
2✔
641
            );
642
            assert_ne!(
2✔
643
                Version::from("0.0.5-10.beta"),
2✔
644
                Version::from("0.0.5-5.alpha")
2✔
645
            );
646
        }
2✔
647

648
        #[test]
649
        fn version_partial_cmp_should_compare_numeric_pre_release_ids_numerically() {
2✔
650
            assert!(Version::from("0.0.5-5") < Version::from("0.0.5-10"));
2✔
651
            assert!(Version::from("0.0.5-10") > Version::from("0.0.5-5"));
2✔
652
        }
2✔
653

654
        #[test]
655
        fn version_partial_cmp_should_compare_non_numeric_pre_release_ids_lexically() {
2✔
656
            assert!(Version::from("0.0.5-a") < Version::from("0.0.5-b"));
2✔
657
            assert!(Version::from("0.0.5-b") > Version::from("0.0.5-a"));
2✔
658
        }
2✔
659

660
        #[test]
661
        fn version_partial_cmp_numeric_pre_release_ids_should_be_less_than_than_non_numeric_ids() {
2✔
662
            assert!(Version::from("0.0.5-9") < Version::from("0.0.5-a"));
2✔
663
            assert!(Version::from("0.0.5-a") > Version::from("0.0.5-9"));
2✔
664

665
            assert!(Version::from("0.0.5-86") < Version::from("0.0.5-78b"));
2✔
666
            assert!(Version::from("0.0.5-78b") > Version::from("0.0.5-86"));
2✔
667
        }
2✔
668

669
        #[test]
670
        fn version_partial_cmp_earlier_pre_release_ids_should_take_precedence_over_later_ids() {
2✔
671
            assert!(Version::from("0.0.5-5.10") < Version::from("0.0.5-10.5"));
2✔
672
            assert!(Version::from("0.0.5-10.5") > Version::from("0.0.5-5.10"));
2✔
673
        }
2✔
674

675
        #[test]
676
        fn version_partial_cmp_a_version_with_more_pre_release_ids_is_greater() {
2✔
677
            assert!(Version::from("0.0.5-5") < Version::from("0.0.5-5.0"));
2✔
678
            assert!(Version::from("0.0.5-5.0") > Version::from("0.0.5-5"));
2✔
679
        }
2✔
680

681
        #[test]
682
        fn version_partial_cmp_release_ids_should_take_precedence_over_pre_release_ids() {
2✔
683
            assert!(Version::from("0.0.5-10") < Version::from("0.0.10-5"));
2✔
684
            assert!(Version::from("0.0.10-5") > Version::from("0.0.5-10"));
2✔
685
        }
2✔
686

687
        #[test]
688
        fn version_eq_should_ignore_metadata() {
2✔
689
            assert_eq!(Version::from("0.0.1+alpha"), Version::from("0.0.1+beta"));
2✔
690
        }
2✔
691

692
        #[test]
693
        fn version_partial_cmp_should_ignore_metadata() {
2✔
694
            assert!(is_cmp_eq(
2✔
695
                &Version::from("0.0.1+alpha"),
2✔
696
                &Version::from("0.0.1+1")
2✔
697
            ));
698
            assert!(is_cmp_eq(
2✔
699
                &Version::from("0.0.1+1"),
2✔
700
                &Version::from("0.0.1+alpha")
2✔
701
            ));
702

703
            assert!(is_cmp_eq(
2✔
704
                &Version::from("0.0.1+2"),
2✔
705
                &Version::from("0.0.1+1")
2✔
706
            ));
707
            assert!(is_cmp_eq(
2✔
708
                &Version::from("0.0.1+1"),
2✔
709
                &Version::from("0.0.1+2")
2✔
710
            ));
711
        }
2✔
712
    }
713

714
    mod extensions {
715
        use super::super::*;
716
        use super::is_cmp_eq;
717

718
        #[test]
719
        fn version_from_should_parse_comma_separated_versions() {
2✔
720
            // OBSE and SKSE use version string fields of the form "0, 2, 0, 12".
721
            let version = Version::from("0, 2, 0, 12");
2✔
722

723
            assert_eq!(
2✔
724
                version.release_ids,
725
                vec![
2✔
726
                    ReleaseId::Numeric(0),
2✔
727
                    ReleaseId::Numeric(2),
2✔
728
                    ReleaseId::Numeric(0),
2✔
729
                    ReleaseId::Numeric(12),
2✔
730
                ]
731
            );
732
            assert!(version.pre_release_ids.is_empty());
2✔
733
        }
2✔
734

735
        #[test]
736
        fn version_eq_should_ignore_leading_zeroes_in_major_version_numbers() {
2✔
737
            assert_eq!(Version::from("05.0.0"), Version::from("5.0.0"));
2✔
738
            assert_eq!(Version::from("5.0.0"), Version::from("05.0.0"));
2✔
739
        }
2✔
740

741
        #[test]
742
        fn version_partial_cmp_should_ignore_leading_zeroes_in_major_version_numbers() {
2✔
743
            assert!(is_cmp_eq(&Version::from("05.0.0"), &Version::from("5.0.0")));
2✔
744
            assert!(is_cmp_eq(&Version::from("5.0.0"), &Version::from("05.0.0")));
2✔
745
        }
2✔
746

747
        #[test]
748
        fn version_eq_should_ignore_leading_zeroes_in_minor_version_numbers() {
2✔
749
            assert_eq!(Version::from("0.05.0"), Version::from("0.5.0"));
2✔
750
            assert_eq!(Version::from("0.5.0"), Version::from("0.05.0"));
2✔
751
        }
2✔
752

753
        #[test]
754
        fn version_partial_cmp_should_ignore_leading_zeroes_in_minor_version_numbers() {
2✔
755
            assert!(is_cmp_eq(&Version::from("0.05.0"), &Version::from("0.5.0")));
2✔
756
            assert!(is_cmp_eq(&Version::from("0.5.0"), &Version::from("0.05.0")));
2✔
757
        }
2✔
758

759
        #[test]
760
        fn version_eq_should_ignore_leading_zeroes_in_patch_version_numbers() {
2✔
761
            assert_eq!(Version::from("0.0.05"), Version::from("0.0.5"));
2✔
762
            assert_eq!(Version::from("0.0.5"), Version::from("0.0.05"));
2✔
763
        }
2✔
764

765
        #[test]
766
        fn version_partial_cmp_should_ignore_leading_zeroes_in_patch_version_numbers() {
2✔
767
            assert!(is_cmp_eq(&Version::from("0.0.05"), &Version::from("0.0.5")));
2✔
768
            assert!(is_cmp_eq(&Version::from("0.0.5"), &Version::from("0.0.05")));
2✔
769
        }
2✔
770

771
        #[test]
772
        fn version_eq_should_ignore_leading_zeroes_in_numeric_pre_release_ids() {
2✔
773
            assert_eq!(Version::from("0.0.5-05"), Version::from("0.0.5-5"));
2✔
774
            assert_eq!(Version::from("0.0.5-5"), Version::from("0.0.5-05"));
2✔
775
        }
2✔
776

777
        #[test]
778
        fn version_partial_cmp_should_ignore_leading_zeroes_in_numeric_pre_release_ids() {
2✔
779
            assert!(is_cmp_eq(
2✔
780
                &Version::from("0.0.5-05"),
2✔
781
                &Version::from("0.0.5-5")
2✔
782
            ));
783
            assert!(is_cmp_eq(
2✔
784
                &Version::from("0.0.5-5"),
2✔
785
                &Version::from("0.0.5-05")
2✔
786
            ));
787
        }
2✔
788

789
        #[test]
790
        fn version_eq_should_compare_an_equal_but_arbitrary_number_of_version_numbers() {
2✔
791
            assert_eq!(Version::from("1.0.0.1.0.0"), Version::from("1.0.0.1.0.0"));
2✔
792

793
            assert_ne!(Version::from("1.0.0.0.0.0"), Version::from("1.0.0.0.0.1"));
2✔
794
            assert_ne!(Version::from("1.0.0.0.0.1"), Version::from("1.0.0.0.0.0"));
2✔
795
        }
2✔
796

797
        #[test]
798
        fn version_partial_cmp_should_compare_an_equal_but_arbitrary_number_of_version_numbers() {
2✔
799
            assert!(is_cmp_eq(
2✔
800
                &Version::from("1.0.0.1.0.0"),
2✔
801
                &Version::from("1.0.0.1.0.0")
2✔
802
            ));
803

804
            assert!(Version::from("1.0.0.0.0.0") < Version::from("1.0.0.0.0.1"));
2✔
805
            assert!(Version::from("1.0.0.0.0.1") > Version::from("1.0.0.0.0.0"));
2✔
806
        }
2✔
807

808
        #[test]
809
        fn version_eq_non_numeric_release_ids_should_be_compared_lexically() {
2✔
810
            assert_eq!(Version::from("1.0.0a"), Version::from("1.0.0a"));
2✔
811

812
            assert_ne!(Version::from("1.0.0a"), Version::from("1.0.0b"));
2✔
813
            assert_ne!(Version::from("1.0.0b"), Version::from("1.0.0a"));
2✔
814
        }
2✔
815

816
        #[test]
817
        fn version_partial_cmp_non_numeric_release_ids_should_be_compared_lexically() {
2✔
818
            assert!(Version::from("1.0.0a") < Version::from("1.0.0b"));
2✔
819
            assert!(Version::from("1.0.0b") > Version::from("1.0.0a"));
2✔
820
        }
2✔
821

822
        #[test]
823
        fn version_partial_cmp_numeric_and_non_numeric_release_ids_should_be_compared_by_leading_numeric_values_first(
2✔
824
        ) {
2✔
825
            assert!(Version::from("0.78b") < Version::from("0.86"));
2✔
826
            assert!(Version::from("0.86") > Version::from("0.78b"));
2✔
827
        }
2✔
828

829
        #[test]
830
        fn version_partial_cmp_non_numeric_release_ids_should_be_greater_than_release_ids() {
2✔
831
            assert!(Version::from("1.0.0") < Version::from("1.0.0a"));
2✔
832
            assert!(Version::from("1.0.0a") > Version::from("1.0.0"));
2✔
833
        }
2✔
834

835
        #[test]
836
        fn version_partial_cmp_any_release_id_may_be_non_numeric() {
2✔
837
            assert!(Version::from("1.0.0alpha.2") < Version::from("1.0.0beta.2"));
2✔
838
            assert!(Version::from("1.0.0beta.2") > Version::from("1.0.0alpha.2"));
2✔
839
        }
2✔
840

841
        #[test]
842
        fn version_eq_should_compare_release_ids_case_insensitively() {
2✔
843
            assert_eq!(Version::from("1.0.0A"), Version::from("1.0.0a"));
2✔
844
            assert_eq!(Version::from("1.0.0a"), Version::from("1.0.0A"));
2✔
845
        }
2✔
846

847
        #[test]
848
        fn version_partial_cmp_should_compare_release_ids_case_insensitively() {
2✔
849
            assert!(Version::from("1.0.0a") < Version::from("1.0.0B"));
2✔
850
            assert!(Version::from("1.0.0B") > Version::from("1.0.0a"));
2✔
851
        }
2✔
852

853
        #[test]
854
        fn version_eq_should_compare_pre_release_ids_case_insensitively() {
2✔
855
            assert_eq!(Version::from("1.0.0-Alpha"), Version::from("1.0.0-alpha"));
2✔
856
            assert_eq!(Version::from("1.0.0-alpha"), Version::from("1.0.0-Alpha"));
2✔
857
        }
2✔
858

859
        #[test]
860
        fn version_partial_cmp_should_compare_pre_release_ids_case_insensitively() {
2✔
861
            assert!(Version::from("1.0.0-alpha") < Version::from("1.0.0-Beta"));
2✔
862
            assert!(Version::from("1.0.0-Beta") > Version::from("1.0.0-alpha"));
2✔
863
        }
2✔
864

865
        #[test]
866
        fn version_eq_should_pad_release_id_vecs_to_equal_length_with_zeroes() {
2✔
867
            assert_eq!(Version::from("1-beta"), Version::from("1.0.0-beta"));
2✔
868
            assert_eq!(Version::from("1.0.0-beta"), Version::from("1-beta"));
2✔
869

870
            assert_eq!(Version::from("0.0.0.1"), Version::from("0.0.0.1.0.0"));
2✔
871
            assert_eq!(Version::from("0.0.0.1.0.0"), Version::from("0.0.0.1"));
2✔
872

873
            assert_ne!(Version::from("1.0.0.0"), Version::from("1.0.0.0.0.1"));
2✔
874
            assert_ne!(Version::from("1.0.0.0.0.1"), Version::from("1.0.0.0"));
2✔
875
        }
2✔
876

877
        #[test]
878
        fn version_partial_cmp_should_pad_release_id_vecs_to_equal_length_with_zeroes() {
2✔
879
            assert!(Version::from("1.0.0.0.0.0") < Version::from("1.0.0.1"));
2✔
880
            assert!(Version::from("1.0.0.1") > Version::from("1.0.0.0.0.0"));
2✔
881

882
            assert!(Version::from("1.0.0.0") < Version::from("1.0.0.0.0.1"));
2✔
883
            assert!(Version::from("1.0.0.0.0.1") > Version::from("1.0.0.0"));
2✔
884

885
            assert!(is_cmp_eq(
2✔
886
                &Version::from("1.0.0.0.0.0"),
2✔
887
                &Version::from("1.0.0.0")
2✔
888
            ));
889
            assert!(is_cmp_eq(
2✔
890
                &Version::from("1.0.0.0"),
2✔
891
                &Version::from("1.0.0.0.0.0")
2✔
892
            ));
893
        }
2✔
894

895
        #[test]
896
        fn version_from_should_treat_space_as_separator_between_release_and_pre_release_ids() {
2✔
897
            let version = Version::from("1.0.0 alpha");
2✔
898
            assert_eq!(
2✔
899
                version.release_ids,
900
                vec![
2✔
901
                    ReleaseId::Numeric(1),
2✔
902
                    ReleaseId::Numeric(0),
2✔
903
                    ReleaseId::Numeric(0)
2✔
904
                ]
905
            );
906
            assert_eq!(
2✔
907
                version.pre_release_ids,
908
                vec![PreReleaseId::NonNumeric("alpha".into())]
2✔
909
            );
910
        }
2✔
911

912
        #[test]
913
        fn version_from_should_treat_colon_as_separator_between_release_and_pre_release_ids() {
2✔
914
            let version = Version::from("1.0.0:alpha");
2✔
915
            assert_eq!(
2✔
916
                version.release_ids,
917
                vec![
2✔
918
                    ReleaseId::Numeric(1),
2✔
919
                    ReleaseId::Numeric(0),
2✔
920
                    ReleaseId::Numeric(0)
2✔
921
                ]
922
            );
923
            assert_eq!(
2✔
924
                version.pre_release_ids,
925
                vec![PreReleaseId::NonNumeric("alpha".into())]
2✔
926
            );
927
        }
2✔
928

929
        #[test]
930
        fn version_from_should_treat_underscore_as_separator_between_release_and_pre_release_ids() {
2✔
931
            let version = Version::from("1.0.0_alpha");
2✔
932
            assert_eq!(
2✔
933
                version.release_ids,
934
                vec![
2✔
935
                    ReleaseId::Numeric(1),
2✔
936
                    ReleaseId::Numeric(0),
2✔
937
                    ReleaseId::Numeric(0)
2✔
938
                ]
939
            );
940
            assert_eq!(
2✔
941
                version.pre_release_ids,
942
                vec![PreReleaseId::NonNumeric("alpha".into())]
2✔
943
            );
944
        }
2✔
945

946
        #[test]
947
        fn version_from_should_treat_space_as_separator_between_pre_release_ids() {
2✔
948
            let version = Version::from("1.0.0-alpha 1");
2✔
949
            assert_eq!(
2✔
950
                version.release_ids,
951
                vec![
2✔
952
                    ReleaseId::Numeric(1),
2✔
953
                    ReleaseId::Numeric(0),
2✔
954
                    ReleaseId::Numeric(0)
2✔
955
                ]
956
            );
957
            assert_eq!(
2✔
958
                version.pre_release_ids,
959
                vec![
2✔
960
                    PreReleaseId::NonNumeric("alpha".into()),
2✔
961
                    PreReleaseId::Numeric(1)
2✔
962
                ]
963
            );
964
        }
2✔
965

966
        #[test]
967
        fn version_from_should_treat_colon_as_separator_between_pre_release_ids() {
2✔
968
            let version = Version::from("1.0.0-alpha:1");
2✔
969
            assert_eq!(
2✔
970
                version.release_ids,
971
                vec![
2✔
972
                    ReleaseId::Numeric(1),
2✔
973
                    ReleaseId::Numeric(0),
2✔
974
                    ReleaseId::Numeric(0)
2✔
975
                ]
976
            );
977
            assert_eq!(
2✔
978
                version.pre_release_ids,
979
                vec![
2✔
980
                    PreReleaseId::NonNumeric("alpha".into()),
2✔
981
                    PreReleaseId::Numeric(1)
2✔
982
                ]
983
            );
984
        }
2✔
985

986
        #[test]
987
        fn version_from_should_treat_underscore_as_separator_between_pre_release_ids() {
2✔
988
            let version = Version::from("1.0.0-alpha_1");
2✔
989
            assert_eq!(
2✔
990
                version.release_ids,
991
                vec![
2✔
992
                    ReleaseId::Numeric(1),
2✔
993
                    ReleaseId::Numeric(0),
2✔
994
                    ReleaseId::Numeric(0)
2✔
995
                ]
996
            );
997
            assert_eq!(
2✔
998
                version.pre_release_ids,
999
                vec![
2✔
1000
                    PreReleaseId::NonNumeric("alpha".into()),
2✔
1001
                    PreReleaseId::Numeric(1)
2✔
1002
                ]
1003
            );
1004
        }
2✔
1005

1006
        #[test]
1007
        fn version_from_should_treat_dash_as_separator_between_pre_release_ids() {
2✔
1008
            let version = Version::from("1.0.0-alpha-1");
2✔
1009
            assert_eq!(
2✔
1010
                version.release_ids,
1011
                vec![
2✔
1012
                    ReleaseId::Numeric(1),
2✔
1013
                    ReleaseId::Numeric(0),
2✔
1014
                    ReleaseId::Numeric(0)
2✔
1015
                ]
1016
            );
1017
            assert_eq!(
2✔
1018
                version.pre_release_ids,
1019
                vec![
2✔
1020
                    PreReleaseId::NonNumeric("alpha".into()),
2✔
1021
                    PreReleaseId::Numeric(1)
2✔
1022
                ]
1023
            );
1024
        }
2✔
1025
    }
1026
}
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