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

loot / loot-condition-interpreter / 19904365303

03 Dec 2025 06:11PM UTC coverage: 90.219% (+0.3%) from 89.921%
19904365303

push

github

Ortham
Remove pelite dependency

The advantages of using the built-in implementation instead of pelite are:

- It's much faster on average: for Starfield.exe (~ 100 MB) it is 2.8x
  faster and 4% slower reading file and product versions respectively,
  and for sfse_1_15_222.dll it is 3.35x faster and < 1% slower
  respectively.
- It reduces the transitive dependency count by 8
- It uses ~ 700 lines of first-party code that only depends on the
  standard library, instead of 240715 lines of unaudited third-party code
- pelite hasn't had a release in 3 years, and my PR for replacing winapi
  hasn't had any response in the month and a half it's been open, so the
  built-in implementation is probably less of a maintenance risk.

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

59 existing lines in 2 files now uncovered.

4492 of 4979 relevant lines covered (90.22%)

31.57 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_pe_version, read_product_version};
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
        read_pe_version(file_path, read_file_version)
12✔
114
    }
12✔
115

116
    pub(super) fn read_product_version(file_path: &Path) -> Result<Option<Self>, Error> {
18✔
117
        read_pe_version(file_path, read_product_version)
18✔
118
    }
18✔
119

120
    pub(super) fn is_readable(file_path: &Path) -> bool {
10✔
121
        read_pe_version(file_path, |_| Ok(None)).is_ok()
10✔
122
    }
10✔
123
}
124

125
fn is_separator(c: char) -> bool {
3,294✔
126
    c == '-' || c == ' ' || c == ':' || c == '_'
3,294✔
127
}
3,294✔
128

129
fn is_pre_release_separator(c: char) -> bool {
440✔
130
    c == '.' || is_separator(c)
440✔
131
}
440✔
132

133
fn split_version_string(string: &str) -> (&str, &str) {
568✔
134
    // Special case for strings of the form "0, 1, 2, 3", which are used in
135
    // OBSE and SKSE, and which should be interpreted as "0.1.2.3".
136
    if let Ok(regex) = regex::Regex::new("\\d+, \\d+, \\d+, \\d+") {
568✔
137
        if regex.is_match(string) {
568✔
138
            return (string, "");
2✔
139
        }
566✔
UNCOV
140
    }
×
141

142
    string.split_once(is_separator).unwrap_or((string, ""))
566✔
143
}
568✔
144

145
impl<T: AsRef<str>> From<T> for Version {
146
    fn from(string: T) -> Self {
568✔
147
        let (release, pre_release) = split_version_string(trim_metadata(string.as_ref()));
568✔
148

149
        Version {
568✔
150
            release_ids: release.split(['.', ',']).map(ReleaseId::from).collect(),
568✔
151
            pre_release_ids: pre_release
568✔
152
                .split_terminator(is_pre_release_separator)
568✔
153
                .map(PreReleaseId::from)
568✔
154
                .collect(),
568✔
155
        }
568✔
156
    }
568✔
157
}
158

159
fn trim_metadata(version: &str) -> &str {
568✔
160
    if version.is_empty() {
568✔
161
        "0"
16✔
162
    } else if let Some((prefix, _)) = version.split_once('+') {
552✔
163
        prefix
20✔
164
    } else {
165
        version
532✔
166
    }
167
}
568✔
168

169
impl PartialOrd for Version {
170
    fn partial_cmp(&self, other: &Version) -> Option<Ordering> {
156✔
171
        let (self_release_ids, other_release_ids) =
156✔
172
            pad_release_ids(&self.release_ids, &other.release_ids);
156✔
173

174
        match self_release_ids.partial_cmp(&other_release_ids) {
156✔
175
            Some(Ordering::Equal) | None => {
176
                match (
70✔
177
                    self.pre_release_ids.is_empty(),
70✔
178
                    other.pre_release_ids.is_empty(),
70✔
179
                ) {
70✔
180
                    (true, false) => Some(Ordering::Greater),
2✔
181
                    (false, true) => Some(Ordering::Less),
2✔
182
                    _ => self.pre_release_ids.partial_cmp(&other.pre_release_ids),
66✔
183
                }
184
            }
185
            r => r,
86✔
186
        }
187
    }
156✔
188
}
189

190
impl PartialEq for Version {
191
    fn eq(&self, other: &Version) -> bool {
118✔
192
        let (self_release_ids, other_release_ids) =
118✔
193
            pad_release_ids(&self.release_ids, &other.release_ids);
118✔
194

195
        self_release_ids == other_release_ids && self.pre_release_ids == other.pre_release_ids
118✔
196
    }
118✔
197
}
198

199
fn pad_release_ids(ids1: &[ReleaseId], ids2: &[ReleaseId]) -> (Vec<ReleaseId>, Vec<ReleaseId>) {
274✔
200
    let mut ids1 = ids1.to_vec();
274✔
201
    let mut ids2 = ids2.to_vec();
274✔
202

203
    match ids1.len().cmp(&ids2.len()) {
274✔
204
        Ordering::Less => ids1.resize(ids2.len(), ReleaseId::Numeric(0)),
12✔
205
        Ordering::Greater => ids2.resize(ids1.len(), ReleaseId::Numeric(0)),
12✔
206
        Ordering::Equal => {}
250✔
207
    }
208

209
    (ids1, ids2)
274✔
210
}
274✔
211

212
#[cfg(test)]
213
mod tests {
214
    fn is_cmp_eq(lhs: &super::Version, rhs: &super::Version) -> bool {
30✔
215
        lhs.partial_cmp(rhs).unwrap().is_eq()
30✔
216
    }
30✔
217

218
    mod release_ids {
219
        use super::super::*;
220

221
        #[test]
222
        fn eq_should_compare_equality_of_u32_values() {
2✔
223
            assert_eq!(ReleaseId::Numeric(1), ReleaseId::Numeric(1));
2✔
224
            assert_ne!(ReleaseId::Numeric(1), ReleaseId::Numeric(0));
2✔
225
        }
2✔
226

227
        #[test]
228
        fn eq_should_compare_equality_of_string_values() {
2✔
229
            assert_eq!(
2✔
230
                ReleaseId::NonNumeric("abcd".into()),
2✔
231
                ReleaseId::NonNumeric("abcd".into())
2✔
232
            );
233
            assert_ne!(
2✔
234
                ReleaseId::NonNumeric("abcd".into()),
2✔
235
                ReleaseId::NonNumeric("abce".into())
2✔
236
            );
237
        }
2✔
238

239
        #[test]
240
        fn eq_should_convert_string_values_to_u32_before_comparing_against_a_u32_value() {
2✔
241
            assert_eq!(ReleaseId::Numeric(123), ReleaseId::NonNumeric("123".into()));
2✔
242
            assert_eq!(
2✔
243
                ReleaseId::Numeric(123),
244
                ReleaseId::NonNumeric(" 123 ".into())
2✔
245
            );
246

247
            assert_ne!(
2✔
248
                ReleaseId::Numeric(123),
249
                ReleaseId::NonNumeric("1two3".into())
2✔
250
            );
251

252
            assert_eq!(ReleaseId::NonNumeric("123".into()), ReleaseId::Numeric(123));
2✔
253
            assert_eq!(
2✔
254
                ReleaseId::NonNumeric(" 123 ".into()),
2✔
255
                ReleaseId::Numeric(123)
256
            );
257

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

264
        #[test]
265
        fn cmp_should_compare_u32_values() {
2✔
266
            let cmp = ReleaseId::Numeric(1).partial_cmp(&ReleaseId::Numeric(1));
2✔
267
            assert_eq!(Some(Ordering::Equal), cmp);
2✔
268

269
            let cmp = ReleaseId::Numeric(1).partial_cmp(&ReleaseId::Numeric(2));
2✔
270
            assert_eq!(Some(Ordering::Less), cmp);
2✔
271

272
            let cmp = ReleaseId::Numeric(2).partial_cmp(&ReleaseId::Numeric(1));
2✔
273
            assert_eq!(Some(Ordering::Greater), cmp);
2✔
274
        }
2✔
275

276
        #[test]
277
        fn cmp_should_compare_string_values() {
2✔
278
            let cmp = ReleaseId::NonNumeric("alpha".into())
2✔
279
                .partial_cmp(&ReleaseId::NonNumeric("alpha".into()));
2✔
280
            assert_eq!(Some(Ordering::Equal), cmp);
2✔
281

282
            let cmp = ReleaseId::NonNumeric("alpha".into())
2✔
283
                .partial_cmp(&ReleaseId::NonNumeric("beta".into()));
2✔
284
            assert_eq!(Some(Ordering::Less), cmp);
2✔
285

286
            let cmp = ReleaseId::NonNumeric("beta".into())
2✔
287
                .partial_cmp(&ReleaseId::NonNumeric("alpha".into()));
2✔
288
            assert_eq!(Some(Ordering::Greater), cmp);
2✔
289
        }
2✔
290

291
        #[test]
292
        fn cmp_should_treat_strings_with_no_leading_digits_as_always_greater_than_u32s() {
2✔
293
            let cmp = ReleaseId::Numeric(123).partial_cmp(&ReleaseId::NonNumeric("one23".into()));
2✔
294
            assert_eq!(Some(Ordering::Less), cmp);
2✔
295

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

300
        #[test]
301
        fn cmp_should_compare_leading_digits_in_strings_against_u32s_and_use_the_result_if_it_is_not_equal(
2✔
302
        ) {
2✔
303
            let cmp = ReleaseId::Numeric(86).partial_cmp(&ReleaseId::NonNumeric("78b".into()));
2✔
304
            assert_eq!(Some(Ordering::Greater), cmp);
2✔
305

306
            let cmp = ReleaseId::NonNumeric("78b".into()).partial_cmp(&ReleaseId::Numeric(86));
2✔
307
            assert_eq!(Some(Ordering::Less), 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_equal_and_there_are_no_non_digit_characters(
2✔
312
        ) {
2✔
313
            let cmp = ReleaseId::Numeric(86).partial_cmp(&ReleaseId::NonNumeric("86".into()));
2✔
314
            assert_eq!(Some(Ordering::Equal), cmp);
2✔
315

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

320
        #[test]
321
        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✔
322
        ) {
2✔
323
            let cmp = ReleaseId::Numeric(86).partial_cmp(&ReleaseId::NonNumeric("86b".into()));
2✔
324
            assert_eq!(Some(Ordering::Less), cmp);
2✔
325

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

331
    mod constructors {
332
        use super::super::*;
333

334
        #[test]
335
        fn version_read_file_version_should_read_the_file_version_field_of_a_32_bit_executable() {
2✔
336
            let version = Version::read_file_version(Path::new("tests/libloot_win32/loot.dll"))
2✔
337
                .unwrap()
2✔
338
                .unwrap();
2✔
339

340
            assert_eq!(
2✔
341
                version.release_ids,
342
                vec![
2✔
343
                    ReleaseId::Numeric(0),
2✔
344
                    ReleaseId::Numeric(18),
2✔
345
                    ReleaseId::Numeric(2),
2✔
346
                    ReleaseId::Numeric(0),
2✔
347
                ]
348
            );
349
            assert!(version.pre_release_ids.is_empty());
2✔
350
        }
2✔
351

352
        #[test]
353
        fn version_read_file_version_should_read_the_file_version_field_of_a_64_bit_executable() {
2✔
354
            let version = Version::read_file_version(Path::new("tests/libloot_win64/loot.dll"))
2✔
355
                .unwrap()
2✔
356
                .unwrap();
2✔
357

358
            assert_eq!(
2✔
359
                version.release_ids,
360
                vec![
2✔
361
                    ReleaseId::Numeric(0),
2✔
362
                    ReleaseId::Numeric(18),
2✔
363
                    ReleaseId::Numeric(2),
2✔
364
                    ReleaseId::Numeric(0),
2✔
365
                ]
366
            );
367
            assert!(version.pre_release_ids.is_empty());
2✔
368
        }
2✔
369

370
        #[test]
371
        fn version_read_file_version_should_error_with_path_if_path_does_not_exist() {
2✔
372
            let error = Version::read_file_version(Path::new("missing")).unwrap_err();
2✔
373

374
            assert!(error
2✔
375
                .to_string()
2✔
376
                .starts_with("An error was encountered while accessing the path \"missing\":"));
2✔
377
        }
2✔
378

379
        #[test]
380
        fn version_read_file_version_should_error_with_path_if_the_file_is_not_an_executable() {
2✔
381
            let error = Version::read_file_version(Path::new("Cargo.toml")).unwrap_err();
2✔
382

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

386
        #[test]
387
        fn version_read_file_version_should_return_none_if_there_is_no_version_info() {
2✔
388
            let version =
2✔
389
                Version::read_file_version(Path::new("tests/loot_api_python/loot_api.pyd"))
2✔
390
                    .unwrap();
2✔
391

392
            assert!(version.is_none());
2✔
393
        }
2✔
394

395
        #[test]
396
        fn version_read_product_version_should_read_the_file_version_field_of_a_32_bit_executable()
2✔
397
        {
398
            let version = Version::read_product_version(Path::new("tests/libloot_win32/loot.dll"))
2✔
399
                .unwrap()
2✔
400
                .unwrap();
2✔
401

402
            assert_eq!(
2✔
403
                version.release_ids,
404
                vec![
2✔
405
                    ReleaseId::Numeric(0),
2✔
406
                    ReleaseId::Numeric(18),
2✔
407
                    ReleaseId::Numeric(2)
2✔
408
                ]
409
            );
410
            assert!(version.pre_release_ids.is_empty());
2✔
411
        }
2✔
412

413
        #[test]
414
        fn version_read_product_version_should_read_the_file_version_field_of_a_64_bit_executable()
2✔
415
        {
416
            let version = Version::read_product_version(Path::new("tests/libloot_win64/loot.dll"))
2✔
417
                .unwrap()
2✔
418
                .unwrap();
2✔
419

420
            assert_eq!(
2✔
421
                version.release_ids,
422
                vec![
2✔
423
                    ReleaseId::Numeric(0),
2✔
424
                    ReleaseId::Numeric(18),
2✔
425
                    ReleaseId::Numeric(2),
2✔
426
                ]
427
            );
428
            assert!(version.pre_release_ids.is_empty());
2✔
429
        }
2✔
430

431
        #[test]
432
        fn version_read_product_version_should_find_non_us_english_version_strings() {
2✔
433
            let tmp_dir = tempfile::tempdir().unwrap();
2✔
434
            let dll_path = tmp_dir.path().join("loot.ru.dll");
2✔
435

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

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

442
            std::fs::write(&dll_path, dll_bytes).unwrap();
2✔
443

444
            let version = Version::read_product_version(&dll_path).unwrap().unwrap();
2✔
445

446
            assert_eq!(
2✔
447
                version.release_ids,
448
                vec![
2✔
449
                    ReleaseId::Numeric(0),
2✔
450
                    ReleaseId::Numeric(18),
2✔
451
                    ReleaseId::Numeric(2)
2✔
452
                ]
453
            );
454
            assert!(version.pre_release_ids.is_empty());
2✔
455
        }
2✔
456

457
        #[test]
458
        fn version_read_product_version_should_error_with_path_if_path_does_not_exist() {
2✔
459
            let error = Version::read_product_version(Path::new("missing")).unwrap_err();
2✔
460

461
            assert!(error
2✔
462
                .to_string()
2✔
463
                .starts_with("An error was encountered while accessing the path \"missing\":"));
2✔
464
        }
2✔
465

466
        #[test]
467
        fn version_read_product_version_should_error_with_path_if_the_file_is_not_an_executable() {
2✔
468
            let error = Version::read_product_version(Path::new("Cargo.toml")).unwrap_err();
2✔
469

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

473
        #[test]
474
        fn version_read_product_version_should_return_none_if_there_is_no_version_info() {
2✔
475
            let version =
2✔
476
                Version::read_product_version(Path::new("tests/loot_api_python/loot_api.pyd"))
2✔
477
                    .unwrap();
2✔
478

479
            assert!(version.is_none());
2✔
480
        }
2✔
481
    }
482

483
    mod empty {
484
        use super::super::*;
485
        #[test]
486
        fn version_eq_an_empty_string_should_equal_an_empty_string() {
2✔
487
            assert_eq!(Version::from(""), Version::from(""));
2✔
488
        }
2✔
489

490
        #[test]
491
        fn version_eq_an_empty_string_should_equal_a_version_of_zero() {
2✔
492
            assert_eq!(Version::from(""), Version::from("0"));
2✔
493
            assert_eq!(Version::from("0"), Version::from(""));
2✔
494
        }
2✔
495

496
        #[test]
497
        fn version_eq_an_empty_string_should_not_equal_a_non_zero_version() {
2✔
498
            assert_ne!(Version::from(""), Version::from("5"));
2✔
499
            assert_ne!(Version::from("5"), Version::from(""));
2✔
500
        }
2✔
501

502
        #[test]
503
        fn version_partial_cmp_an_empty_string_should_be_less_than_a_non_zero_version() {
2✔
504
            assert!(Version::from("") < Version::from("1"));
2✔
505
            assert!(Version::from("1") > Version::from(""));
2✔
506
        }
2✔
507
    }
508

509
    mod numeric {
510
        use super::super::*;
511

512
        #[test]
513
        fn version_eq_a_non_empty_string_should_equal_itself() {
2✔
514
            assert_eq!(Version::from("5"), Version::from("5"));
2✔
515
        }
2✔
516

517
        #[test]
518
        fn version_eq_single_digit_versions_should_compare_digits() {
2✔
519
            assert_eq!(Version::from("5"), Version::from("5"));
2✔
520

521
            assert_ne!(Version::from("4"), Version::from("5"));
2✔
522
            assert_ne!(Version::from("5"), Version::from("4"));
2✔
523
        }
2✔
524

525
        #[test]
526
        fn version_partial_cmp_single_digit_versions_should_compare_digits() {
2✔
527
            assert!(Version::from("4") < Version::from("5"));
2✔
528
            assert!(Version::from("5") > Version::from("4"));
2✔
529
        }
2✔
530

531
        #[test]
532
        fn version_eq_numeric_versions_should_compare_numbers() {
2✔
533
            assert_ne!(Version::from("5"), Version::from("10"));
2✔
534
            assert_ne!(Version::from("10"), Version::from("5"));
2✔
535
        }
2✔
536

537
        #[test]
538
        fn version_partial_cmp_numeric_versions_should_compare_numbers() {
2✔
539
            assert!(Version::from("5") < Version::from("10"));
2✔
540
            assert!(Version::from("10") > Version::from("5"));
2✔
541
        }
2✔
542
    }
543

544
    mod semver {
545
        use super::super::*;
546
        use super::is_cmp_eq;
547

548
        #[test]
549
        fn version_eq_should_compare_patch_numbers() {
2✔
550
            assert_eq!(Version::from("0.0.5"), Version::from("0.0.5"));
2✔
551

552
            assert_ne!(Version::from("0.0.5"), Version::from("0.0.10"));
2✔
553
            assert_ne!(Version::from("0.0.10"), Version::from("0.0.5"));
2✔
554
        }
2✔
555

556
        #[test]
557
        fn version_partial_cmp_should_compare_patch_numbers() {
2✔
558
            assert!(Version::from("0.0.5") < Version::from("0.0.10"));
2✔
559
            assert!(Version::from("0.0.10") > Version::from("0.0.5"));
2✔
560
        }
2✔
561

562
        #[test]
563
        fn version_eq_should_compare_minor_numbers() {
2✔
564
            assert_eq!(Version::from("0.5.0"), Version::from("0.5.0"));
2✔
565

566
            assert_ne!(Version::from("0.5.0"), Version::from("0.10.0"));
2✔
567
            assert_ne!(Version::from("0.10.0"), Version::from("0.5.0"));
2✔
568
        }
2✔
569

570
        #[test]
571
        fn version_partial_cmp_should_compare_minor_numbers() {
2✔
572
            assert!(Version::from("0.5.0") < Version::from("0.10.0"));
2✔
573
            assert!(Version::from("0.10.0") > Version::from("0.5.0"));
2✔
574
        }
2✔
575

576
        #[test]
577
        fn version_partial_cmp_minor_numbers_should_take_precedence_over_patch_numbers() {
2✔
578
            assert!(Version::from("0.5.10") < Version::from("0.10.5"));
2✔
579
            assert!(Version::from("0.10.5") > Version::from("0.5.10"));
2✔
580
        }
2✔
581

582
        #[test]
583
        fn version_eq_should_compare_major_numbers() {
2✔
584
            assert_eq!(Version::from("5.0.0"), Version::from("5.0.0"));
2✔
585

586
            assert_ne!(Version::from("5.0.0"), Version::from("10.0.0"));
2✔
587
            assert_ne!(Version::from("10.0.0"), Version::from("5.0.0"));
2✔
588
        }
2✔
589

590
        #[test]
591
        fn version_partial_cmp_should_compare_major_numbers() {
2✔
592
            assert!(Version::from("5.0.0") < Version::from("10.0.0"));
2✔
593
            assert!(Version::from("10.0.0") > Version::from("5.0.0"));
2✔
594
        }
2✔
595

596
        #[test]
597
        fn version_partial_cmp_major_numbers_should_take_precedence_over_minor_numbers() {
2✔
598
            assert!(Version::from("5.10.0") < Version::from("10.5.0"));
2✔
599
            assert!(Version::from("10.5.0") > Version::from("5.10.0"));
2✔
600
        }
2✔
601

602
        #[test]
603
        fn version_partial_cmp_major_numbers_should_take_precedence_over_patch_numbers() {
2✔
604
            assert!(Version::from("5.0.10") < Version::from("10.0.5"));
2✔
605
            assert!(Version::from("10.0.5") > Version::from("5.0.10"));
2✔
606
        }
2✔
607

608
        #[test]
609
        fn version_eq_should_consider_versions_that_differ_by_the_presence_of_a_pre_release_id_to_be_not_equal(
2✔
610
        ) {
2✔
611
            assert_ne!(Version::from("1.0.0"), Version::from("1.0.0-alpha"));
2✔
612
        }
2✔
613

614
        #[test]
615
        fn version_partial_cmp_should_treat_the_absence_of_a_pre_release_id_as_greater_than_its_presence(
2✔
616
        ) {
2✔
617
            assert!(Version::from("1.0.0-alpha") < Version::from("1.0.0"));
2✔
618
            assert!(Version::from("1.0.0") > Version::from("1.0.0-alpha"));
2✔
619
        }
2✔
620

621
        #[test]
622
        fn version_eq_should_compare_pre_release_identifiers() {
2✔
623
            assert_eq!(
2✔
624
                Version::from("0.0.5-5.alpha"),
2✔
625
                Version::from("0.0.5-5.alpha")
2✔
626
            );
627

628
            assert_ne!(
2✔
629
                Version::from("0.0.5-5.alpha"),
2✔
630
                Version::from("0.0.5-10.beta")
2✔
631
            );
632
            assert_ne!(
2✔
633
                Version::from("0.0.5-10.beta"),
2✔
634
                Version::from("0.0.5-5.alpha")
2✔
635
            );
636
        }
2✔
637

638
        #[test]
639
        fn version_partial_cmp_should_compare_numeric_pre_release_ids_numerically() {
2✔
640
            assert!(Version::from("0.0.5-5") < Version::from("0.0.5-10"));
2✔
641
            assert!(Version::from("0.0.5-10") > Version::from("0.0.5-5"));
2✔
642
        }
2✔
643

644
        #[test]
645
        fn version_partial_cmp_should_compare_non_numeric_pre_release_ids_lexically() {
2✔
646
            assert!(Version::from("0.0.5-a") < Version::from("0.0.5-b"));
2✔
647
            assert!(Version::from("0.0.5-b") > Version::from("0.0.5-a"));
2✔
648
        }
2✔
649

650
        #[test]
651
        fn version_partial_cmp_numeric_pre_release_ids_should_be_less_than_than_non_numeric_ids() {
2✔
652
            assert!(Version::from("0.0.5-9") < Version::from("0.0.5-a"));
2✔
653
            assert!(Version::from("0.0.5-a") > Version::from("0.0.5-9"));
2✔
654

655
            assert!(Version::from("0.0.5-86") < Version::from("0.0.5-78b"));
2✔
656
            assert!(Version::from("0.0.5-78b") > Version::from("0.0.5-86"));
2✔
657
        }
2✔
658

659
        #[test]
660
        fn version_partial_cmp_earlier_pre_release_ids_should_take_precedence_over_later_ids() {
2✔
661
            assert!(Version::from("0.0.5-5.10") < Version::from("0.0.5-10.5"));
2✔
662
            assert!(Version::from("0.0.5-10.5") > Version::from("0.0.5-5.10"));
2✔
663
        }
2✔
664

665
        #[test]
666
        fn version_partial_cmp_a_version_with_more_pre_release_ids_is_greater() {
2✔
667
            assert!(Version::from("0.0.5-5") < Version::from("0.0.5-5.0"));
2✔
668
            assert!(Version::from("0.0.5-5.0") > Version::from("0.0.5-5"));
2✔
669
        }
2✔
670

671
        #[test]
672
        fn version_partial_cmp_release_ids_should_take_precedence_over_pre_release_ids() {
2✔
673
            assert!(Version::from("0.0.5-10") < Version::from("0.0.10-5"));
2✔
674
            assert!(Version::from("0.0.10-5") > Version::from("0.0.5-10"));
2✔
675
        }
2✔
676

677
        #[test]
678
        fn version_eq_should_ignore_metadata() {
2✔
679
            assert_eq!(Version::from("0.0.1+alpha"), Version::from("0.0.1+beta"));
2✔
680
        }
2✔
681

682
        #[test]
683
        fn version_partial_cmp_should_ignore_metadata() {
2✔
684
            assert!(is_cmp_eq(
2✔
685
                &Version::from("0.0.1+alpha"),
2✔
686
                &Version::from("0.0.1+1")
2✔
687
            ));
688
            assert!(is_cmp_eq(
2✔
689
                &Version::from("0.0.1+1"),
2✔
690
                &Version::from("0.0.1+alpha")
2✔
691
            ));
692

693
            assert!(is_cmp_eq(
2✔
694
                &Version::from("0.0.1+2"),
2✔
695
                &Version::from("0.0.1+1")
2✔
696
            ));
697
            assert!(is_cmp_eq(
2✔
698
                &Version::from("0.0.1+1"),
2✔
699
                &Version::from("0.0.1+2")
2✔
700
            ));
701
        }
2✔
702
    }
703

704
    mod extensions {
705
        use super::super::*;
706
        use super::is_cmp_eq;
707

708
        #[test]
709
        fn version_from_should_parse_comma_separated_versions() {
2✔
710
            // OBSE and SKSE use version string fields of the form "0, 2, 0, 12".
711
            let version = Version::from("0, 2, 0, 12");
2✔
712

713
            assert_eq!(
2✔
714
                version.release_ids,
715
                vec![
2✔
716
                    ReleaseId::Numeric(0),
2✔
717
                    ReleaseId::Numeric(2),
2✔
718
                    ReleaseId::Numeric(0),
2✔
719
                    ReleaseId::Numeric(12),
2✔
720
                ]
721
            );
722
            assert!(version.pre_release_ids.is_empty());
2✔
723
        }
2✔
724

725
        #[test]
726
        fn version_eq_should_ignore_leading_zeroes_in_major_version_numbers() {
2✔
727
            assert_eq!(Version::from("05.0.0"), Version::from("5.0.0"));
2✔
728
            assert_eq!(Version::from("5.0.0"), Version::from("05.0.0"));
2✔
729
        }
2✔
730

731
        #[test]
732
        fn version_partial_cmp_should_ignore_leading_zeroes_in_major_version_numbers() {
2✔
733
            assert!(is_cmp_eq(&Version::from("05.0.0"), &Version::from("5.0.0")));
2✔
734
            assert!(is_cmp_eq(&Version::from("5.0.0"), &Version::from("05.0.0")));
2✔
735
        }
2✔
736

737
        #[test]
738
        fn version_eq_should_ignore_leading_zeroes_in_minor_version_numbers() {
2✔
739
            assert_eq!(Version::from("0.05.0"), Version::from("0.5.0"));
2✔
740
            assert_eq!(Version::from("0.5.0"), Version::from("0.05.0"));
2✔
741
        }
2✔
742

743
        #[test]
744
        fn version_partial_cmp_should_ignore_leading_zeroes_in_minor_version_numbers() {
2✔
745
            assert!(is_cmp_eq(&Version::from("0.05.0"), &Version::from("0.5.0")));
2✔
746
            assert!(is_cmp_eq(&Version::from("0.5.0"), &Version::from("0.05.0")));
2✔
747
        }
2✔
748

749
        #[test]
750
        fn version_eq_should_ignore_leading_zeroes_in_patch_version_numbers() {
2✔
751
            assert_eq!(Version::from("0.0.05"), Version::from("0.0.5"));
2✔
752
            assert_eq!(Version::from("0.0.5"), Version::from("0.0.05"));
2✔
753
        }
2✔
754

755
        #[test]
756
        fn version_partial_cmp_should_ignore_leading_zeroes_in_patch_version_numbers() {
2✔
757
            assert!(is_cmp_eq(&Version::from("0.0.05"), &Version::from("0.0.5")));
2✔
758
            assert!(is_cmp_eq(&Version::from("0.0.5"), &Version::from("0.0.05")));
2✔
759
        }
2✔
760

761
        #[test]
762
        fn version_eq_should_ignore_leading_zeroes_in_numeric_pre_release_ids() {
2✔
763
            assert_eq!(Version::from("0.0.5-05"), Version::from("0.0.5-5"));
2✔
764
            assert_eq!(Version::from("0.0.5-5"), Version::from("0.0.5-05"));
2✔
765
        }
2✔
766

767
        #[test]
768
        fn version_partial_cmp_should_ignore_leading_zeroes_in_numeric_pre_release_ids() {
2✔
769
            assert!(is_cmp_eq(
2✔
770
                &Version::from("0.0.5-05"),
2✔
771
                &Version::from("0.0.5-5")
2✔
772
            ));
773
            assert!(is_cmp_eq(
2✔
774
                &Version::from("0.0.5-5"),
2✔
775
                &Version::from("0.0.5-05")
2✔
776
            ));
777
        }
2✔
778

779
        #[test]
780
        fn version_eq_should_compare_an_equal_but_arbitrary_number_of_version_numbers() {
2✔
781
            assert_eq!(Version::from("1.0.0.1.0.0"), Version::from("1.0.0.1.0.0"));
2✔
782

783
            assert_ne!(Version::from("1.0.0.0.0.0"), Version::from("1.0.0.0.0.1"));
2✔
784
            assert_ne!(Version::from("1.0.0.0.0.1"), Version::from("1.0.0.0.0.0"));
2✔
785
        }
2✔
786

787
        #[test]
788
        fn version_partial_cmp_should_compare_an_equal_but_arbitrary_number_of_version_numbers() {
2✔
789
            assert!(is_cmp_eq(
2✔
790
                &Version::from("1.0.0.1.0.0"),
2✔
791
                &Version::from("1.0.0.1.0.0")
2✔
792
            ));
793

794
            assert!(Version::from("1.0.0.0.0.0") < Version::from("1.0.0.0.0.1"));
2✔
795
            assert!(Version::from("1.0.0.0.0.1") > Version::from("1.0.0.0.0.0"));
2✔
796
        }
2✔
797

798
        #[test]
799
        fn version_eq_non_numeric_release_ids_should_be_compared_lexically() {
2✔
800
            assert_eq!(Version::from("1.0.0a"), Version::from("1.0.0a"));
2✔
801

802
            assert_ne!(Version::from("1.0.0a"), Version::from("1.0.0b"));
2✔
803
            assert_ne!(Version::from("1.0.0b"), Version::from("1.0.0a"));
2✔
804
        }
2✔
805

806
        #[test]
807
        fn version_partial_cmp_non_numeric_release_ids_should_be_compared_lexically() {
2✔
808
            assert!(Version::from("1.0.0a") < Version::from("1.0.0b"));
2✔
809
            assert!(Version::from("1.0.0b") > Version::from("1.0.0a"));
2✔
810
        }
2✔
811

812
        #[test]
813
        fn version_partial_cmp_numeric_and_non_numeric_release_ids_should_be_compared_by_leading_numeric_values_first(
2✔
814
        ) {
2✔
815
            assert!(Version::from("0.78b") < Version::from("0.86"));
2✔
816
            assert!(Version::from("0.86") > Version::from("0.78b"));
2✔
817
        }
2✔
818

819
        #[test]
820
        fn version_partial_cmp_non_numeric_release_ids_should_be_greater_than_release_ids() {
2✔
821
            assert!(Version::from("1.0.0") < Version::from("1.0.0a"));
2✔
822
            assert!(Version::from("1.0.0a") > Version::from("1.0.0"));
2✔
823
        }
2✔
824

825
        #[test]
826
        fn version_partial_cmp_any_release_id_may_be_non_numeric() {
2✔
827
            assert!(Version::from("1.0.0alpha.2") < Version::from("1.0.0beta.2"));
2✔
828
            assert!(Version::from("1.0.0beta.2") > Version::from("1.0.0alpha.2"));
2✔
829
        }
2✔
830

831
        #[test]
832
        fn version_eq_should_compare_release_ids_case_insensitively() {
2✔
833
            assert_eq!(Version::from("1.0.0A"), Version::from("1.0.0a"));
2✔
834
            assert_eq!(Version::from("1.0.0a"), Version::from("1.0.0A"));
2✔
835
        }
2✔
836

837
        #[test]
838
        fn version_partial_cmp_should_compare_release_ids_case_insensitively() {
2✔
839
            assert!(Version::from("1.0.0a") < Version::from("1.0.0B"));
2✔
840
            assert!(Version::from("1.0.0B") > Version::from("1.0.0a"));
2✔
841
        }
2✔
842

843
        #[test]
844
        fn version_eq_should_compare_pre_release_ids_case_insensitively() {
2✔
845
            assert_eq!(Version::from("1.0.0-Alpha"), Version::from("1.0.0-alpha"));
2✔
846
            assert_eq!(Version::from("1.0.0-alpha"), Version::from("1.0.0-Alpha"));
2✔
847
        }
2✔
848

849
        #[test]
850
        fn version_partial_cmp_should_compare_pre_release_ids_case_insensitively() {
2✔
851
            assert!(Version::from("1.0.0-alpha") < Version::from("1.0.0-Beta"));
2✔
852
            assert!(Version::from("1.0.0-Beta") > Version::from("1.0.0-alpha"));
2✔
853
        }
2✔
854

855
        #[test]
856
        fn version_eq_should_pad_release_id_vecs_to_equal_length_with_zeroes() {
2✔
857
            assert_eq!(Version::from("1-beta"), Version::from("1.0.0-beta"));
2✔
858
            assert_eq!(Version::from("1.0.0-beta"), Version::from("1-beta"));
2✔
859

860
            assert_eq!(Version::from("0.0.0.1"), Version::from("0.0.0.1.0.0"));
2✔
861
            assert_eq!(Version::from("0.0.0.1.0.0"), Version::from("0.0.0.1"));
2✔
862

863
            assert_ne!(Version::from("1.0.0.0"), Version::from("1.0.0.0.0.1"));
2✔
864
            assert_ne!(Version::from("1.0.0.0.0.1"), Version::from("1.0.0.0"));
2✔
865
        }
2✔
866

867
        #[test]
868
        fn version_partial_cmp_should_pad_release_id_vecs_to_equal_length_with_zeroes() {
2✔
869
            assert!(Version::from("1.0.0.0.0.0") < Version::from("1.0.0.1"));
2✔
870
            assert!(Version::from("1.0.0.1") > Version::from("1.0.0.0.0.0"));
2✔
871

872
            assert!(Version::from("1.0.0.0") < Version::from("1.0.0.0.0.1"));
2✔
873
            assert!(Version::from("1.0.0.0.0.1") > Version::from("1.0.0.0"));
2✔
874

875
            assert!(is_cmp_eq(
2✔
876
                &Version::from("1.0.0.0.0.0"),
2✔
877
                &Version::from("1.0.0.0")
2✔
878
            ));
879
            assert!(is_cmp_eq(
2✔
880
                &Version::from("1.0.0.0"),
2✔
881
                &Version::from("1.0.0.0.0.0")
2✔
882
            ));
883
        }
2✔
884

885
        #[test]
886
        fn version_from_should_treat_space_as_separator_between_release_and_pre_release_ids() {
2✔
887
            let version = Version::from("1.0.0 alpha");
2✔
888
            assert_eq!(
2✔
889
                version.release_ids,
890
                vec![
2✔
891
                    ReleaseId::Numeric(1),
2✔
892
                    ReleaseId::Numeric(0),
2✔
893
                    ReleaseId::Numeric(0)
2✔
894
                ]
895
            );
896
            assert_eq!(
2✔
897
                version.pre_release_ids,
898
                vec![PreReleaseId::NonNumeric("alpha".into())]
2✔
899
            );
900
        }
2✔
901

902
        #[test]
903
        fn version_from_should_treat_colon_as_separator_between_release_and_pre_release_ids() {
2✔
904
            let version = Version::from("1.0.0:alpha");
2✔
905
            assert_eq!(
2✔
906
                version.release_ids,
907
                vec![
2✔
908
                    ReleaseId::Numeric(1),
2✔
909
                    ReleaseId::Numeric(0),
2✔
910
                    ReleaseId::Numeric(0)
2✔
911
                ]
912
            );
913
            assert_eq!(
2✔
914
                version.pre_release_ids,
915
                vec![PreReleaseId::NonNumeric("alpha".into())]
2✔
916
            );
917
        }
2✔
918

919
        #[test]
920
        fn version_from_should_treat_underscore_as_separator_between_release_and_pre_release_ids() {
2✔
921
            let version = Version::from("1.0.0_alpha");
2✔
922
            assert_eq!(
2✔
923
                version.release_ids,
924
                vec![
2✔
925
                    ReleaseId::Numeric(1),
2✔
926
                    ReleaseId::Numeric(0),
2✔
927
                    ReleaseId::Numeric(0)
2✔
928
                ]
929
            );
930
            assert_eq!(
2✔
931
                version.pre_release_ids,
932
                vec![PreReleaseId::NonNumeric("alpha".into())]
2✔
933
            );
934
        }
2✔
935

936
        #[test]
937
        fn version_from_should_treat_space_as_separator_between_pre_release_ids() {
2✔
938
            let version = Version::from("1.0.0-alpha 1");
2✔
939
            assert_eq!(
2✔
940
                version.release_ids,
941
                vec![
2✔
942
                    ReleaseId::Numeric(1),
2✔
943
                    ReleaseId::Numeric(0),
2✔
944
                    ReleaseId::Numeric(0)
2✔
945
                ]
946
            );
947
            assert_eq!(
2✔
948
                version.pre_release_ids,
949
                vec![
2✔
950
                    PreReleaseId::NonNumeric("alpha".into()),
2✔
951
                    PreReleaseId::Numeric(1)
2✔
952
                ]
953
            );
954
        }
2✔
955

956
        #[test]
957
        fn version_from_should_treat_colon_as_separator_between_pre_release_ids() {
2✔
958
            let version = Version::from("1.0.0-alpha:1");
2✔
959
            assert_eq!(
2✔
960
                version.release_ids,
961
                vec![
2✔
962
                    ReleaseId::Numeric(1),
2✔
963
                    ReleaseId::Numeric(0),
2✔
964
                    ReleaseId::Numeric(0)
2✔
965
                ]
966
            );
967
            assert_eq!(
2✔
968
                version.pre_release_ids,
969
                vec![
2✔
970
                    PreReleaseId::NonNumeric("alpha".into()),
2✔
971
                    PreReleaseId::Numeric(1)
2✔
972
                ]
973
            );
974
        }
2✔
975

976
        #[test]
977
        fn version_from_should_treat_underscore_as_separator_between_pre_release_ids() {
2✔
978
            let version = Version::from("1.0.0-alpha_1");
2✔
979
            assert_eq!(
2✔
980
                version.release_ids,
981
                vec![
2✔
982
                    ReleaseId::Numeric(1),
2✔
983
                    ReleaseId::Numeric(0),
2✔
984
                    ReleaseId::Numeric(0)
2✔
985
                ]
986
            );
987
            assert_eq!(
2✔
988
                version.pre_release_ids,
989
                vec![
2✔
990
                    PreReleaseId::NonNumeric("alpha".into()),
2✔
991
                    PreReleaseId::Numeric(1)
2✔
992
                ]
993
            );
994
        }
2✔
995

996
        #[test]
997
        fn version_from_should_treat_dash_as_separator_between_pre_release_ids() {
2✔
998
            let version = Version::from("1.0.0-alpha-1");
2✔
999
            assert_eq!(
2✔
1000
                version.release_ids,
1001
                vec![
2✔
1002
                    ReleaseId::Numeric(1),
2✔
1003
                    ReleaseId::Numeric(0),
2✔
1004
                    ReleaseId::Numeric(0)
2✔
1005
                ]
1006
            );
1007
            assert_eq!(
2✔
1008
                version.pre_release_ids,
1009
                vec![
2✔
1010
                    PreReleaseId::NonNumeric("alpha".into()),
2✔
1011
                    PreReleaseId::Numeric(1)
2✔
1012
                ]
1013
            );
1014
        }
2✔
1015
    }
1016
}
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