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

Ortham / esplugin / 14681681270

26 Apr 2025 01:25PM UTC coverage: 85.112% (-0.7%) from 85.785%
14681681270

push

github

Ortham
Deny a lot of extra lints and fix their errors

227 of 313 new or added lines in 12 files covered. (72.52%)

8 existing lines in 4 files now uncovered.

3470 of 4077 relevant lines covered (85.11%)

34.08 hits per line

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

98.32
/src/plugin.rs
1
/*
2
 * This file is part of esplugin
3
 *
4
 * Copyright (C) 2017 Oliver Hamlet
5
 *
6
 * esplugin is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * esplugin is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with esplugin. If not, see <http://www.gnu.org/licenses/>.
18
 */
19

20
use std::collections::HashSet;
21
use std::ffi::OsStr;
22
use std::fs::File;
23
use std::io::{BufRead, BufReader, Seek};
24
use std::ops::RangeInclusive;
25
use std::path::{Path, PathBuf};
26

27
use encoding_rs::WINDOWS_1252;
28

29
use crate::error::{Error, ParsingErrorKind};
30
use crate::game_id::GameId;
31
use crate::group::Group;
32
use crate::record::{Record, MAX_RECORD_HEADER_LENGTH};
33
use crate::record_id::{NamespacedId, ObjectIndexMask, RecordId, ResolvedRecordId, SourcePlugin};
34

35
#[derive(Copy, Clone, PartialEq, Eq)]
36
enum FileExtension {
37
    Esm,
38
    Esl,
39
    Ghost,
40
    Unrecognised,
41
}
42

43
impl From<&OsStr> for FileExtension {
44
    fn from(value: &OsStr) -> Self {
57✔
45
        if value.eq_ignore_ascii_case("esm") {
57✔
46
            FileExtension::Esm
34✔
47
        } else if value.eq_ignore_ascii_case("esl") {
23✔
48
            FileExtension::Esl
10✔
49
        } else if value.eq_ignore_ascii_case("ghost") {
13✔
50
            FileExtension::Ghost
3✔
51
        } else {
52
            FileExtension::Unrecognised
10✔
53
        }
54
    }
57✔
55
}
56

57
#[derive(Clone, Default, PartialEq, Eq, Debug, Hash)]
58
enum RecordIds {
59
    #[default]
60
    None,
61
    FormIds(Vec<u32>),
62
    NamespacedIds(Vec<NamespacedId>),
63
    Resolved(Vec<ResolvedRecordId>),
64
}
65

66
impl From<Vec<NamespacedId>> for RecordIds {
67
    fn from(record_ids: Vec<NamespacedId>) -> RecordIds {
19✔
68
        RecordIds::NamespacedIds(record_ids)
19✔
69
    }
19✔
70
}
71

72
impl From<Vec<u32>> for RecordIds {
73
    fn from(form_ids: Vec<u32>) -> RecordIds {
56✔
74
        RecordIds::FormIds(form_ids)
56✔
75
    }
56✔
76
}
77

78
#[derive(Clone, PartialEq, Eq, Debug, Hash, Default)]
79
struct PluginData {
80
    header_record: Record,
81
    record_ids: RecordIds,
82
}
83

84
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
85
enum PluginScale {
86
    Full,
87
    Medium,
88
    Small,
89
}
90

91
#[derive(Clone, PartialEq, Eq, Debug, Hash)]
92
pub struct Plugin {
93
    game_id: GameId,
94
    path: PathBuf,
95
    data: PluginData,
96
}
97

98
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
99
pub struct ParseOptions {
100
    header_only: bool,
101
}
102

103
impl ParseOptions {
104
    pub fn header_only() -> Self {
49✔
105
        Self { header_only: true }
49✔
106
    }
49✔
107

108
    pub fn whole_plugin() -> Self {
78✔
109
        Self { header_only: false }
78✔
110
    }
78✔
111
}
112

113
impl Plugin {
114
    pub fn new(game_id: GameId, filepath: &Path) -> Plugin {
195✔
115
        Plugin {
195✔
116
            game_id,
195✔
117
            path: filepath.to_path_buf(),
195✔
118
            data: PluginData::default(),
195✔
119
        }
195✔
120
    }
195✔
121

122
    pub fn parse_reader<R: std::io::Read + std::io::Seek>(
126✔
123
        &mut self,
126✔
124
        reader: R,
126✔
125
        options: ParseOptions,
126✔
126
    ) -> Result<(), Error> {
126✔
127
        let mut reader = BufReader::new(reader);
126✔
128

129
        self.data = read_plugin(&mut reader, self.game_id, options, self.header_type())?;
126✔
130

131
        if self.game_id != GameId::Morrowind && self.game_id != GameId::Starfield {
122✔
132
            self.resolve_record_ids(&[])?;
50✔
133
        }
72✔
134

135
        Ok(())
122✔
136
    }
126✔
137

138
    pub fn parse_file(&mut self, options: ParseOptions) -> Result<(), Error> {
117✔
139
        let file = File::open(&self.path)?;
117✔
140

141
        self.parse_reader(file, options)
116✔
142
    }
117✔
143

144
    /// plugins_metadata can be empty for all games other than Starfield, and for Starfield plugins with no masters.
145
    pub fn resolve_record_ids(&mut self, plugins_metadata: &[PluginMetadata]) -> Result<(), Error> {
67✔
146
        match &self.data.record_ids {
67✔
147
            RecordIds::FormIds(form_ids) => {
46✔
148
                let filename = self
46✔
149
                    .filename()
46✔
150
                    .ok_or_else(|| Error::NoFilename(self.path.clone()))?;
46✔
151
                let parent_metadata = PluginMetadata {
46✔
152
                    filename,
46✔
153
                    scale: self.scale(),
46✔
154
                    record_ids: Box::new([]),
46✔
155
                };
46✔
156
                let masters = self.masters()?;
46✔
157

158
                let form_ids = resolve_form_ids(
46✔
159
                    self.game_id,
46✔
160
                    form_ids,
46✔
161
                    &parent_metadata,
46✔
162
                    &masters,
46✔
163
                    plugins_metadata,
46✔
164
                )?;
46✔
165

166
                self.data.record_ids = RecordIds::Resolved(form_ids);
46✔
167
            }
168
            RecordIds::NamespacedIds(namespaced_ids) => {
1✔
169
                let masters = self.masters()?;
1✔
170

171
                let record_ids =
1✔
172
                    resolve_namespaced_ids(namespaced_ids, &masters, plugins_metadata)?;
1✔
173

174
                self.data.record_ids = RecordIds::Resolved(record_ids);
1✔
175
            }
176
            RecordIds::None | RecordIds::Resolved(_) => {
20✔
177
                // Do nothing.
20✔
178
            }
20✔
179
        }
180

181
        Ok(())
67✔
182
    }
67✔
183

184
    pub fn game_id(&self) -> GameId {
2✔
185
        self.game_id
2✔
186
    }
2✔
187

188
    pub fn path(&self) -> &Path {
8✔
189
        &self.path
8✔
190
    }
8✔
191

192
    pub fn filename(&self) -> Option<String> {
57✔
193
        self.path
57✔
194
            .file_name()
57✔
195
            .and_then(std::ffi::OsStr::to_str)
57✔
196
            .map(std::string::ToString::to_string)
57✔
197
    }
57✔
198

199
    pub fn masters(&self) -> Result<Vec<String>, Error> {
52✔
200
        masters(&self.data.header_record)
52✔
201
    }
52✔
202

203
    fn file_extension(&self) -> FileExtension {
54✔
204
        if let Some(p) = self.path.extension() {
54✔
205
            match FileExtension::from(p) {
54✔
206
                FileExtension::Ghost => self
3✔
207
                    .path
3✔
208
                    .file_stem()
3✔
209
                    .map(Path::new)
3✔
210
                    .and_then(Path::extension)
3✔
211
                    .map_or(FileExtension::Unrecognised, FileExtension::from),
3✔
212
                e => e,
51✔
213
            }
214
        } else {
215
            FileExtension::Unrecognised
×
216
        }
217
    }
54✔
218

219
    pub fn is_master_file(&self) -> bool {
22✔
220
        match self.game_id {
22✔
221
            GameId::Fallout4 | GameId::SkyrimSE | GameId::Starfield => {
222
                // The .esl extension implies the master flag, but the light and
223
                // medium flags do not.
224
                self.is_master_flag_set()
17✔
225
                    || matches!(
7✔
226
                        self.file_extension(),
14✔
227
                        FileExtension::Esm | FileExtension::Esl
228
                    )
229
            }
230
            _ => self.is_master_flag_set(),
5✔
231
        }
232
    }
22✔
233

234
    fn scale(&self) -> PluginScale {
57✔
235
        if self.is_light_plugin() {
57✔
236
            PluginScale::Small
2✔
237
        } else if self.is_medium_flag_set() {
55✔
238
            PluginScale::Medium
3✔
239
        } else {
240
            PluginScale::Full
52✔
241
        }
242
    }
57✔
243

244
    pub fn is_light_plugin(&self) -> bool {
91✔
245
        if self.game_id.supports_light_plugins() {
91✔
246
            if self.game_id == GameId::Starfield {
52✔
247
                // If the inject flag is set, it prevents the .esl extension from
248
                // causing the light flag to be forcibly set on load.
249
                self.is_light_flag_set()
34✔
250
                    || (!self.is_update_flag_set() && self.file_extension() == FileExtension::Esl)
30✔
251
            } else {
252
                self.is_light_flag_set() || self.file_extension() == FileExtension::Esl
18✔
253
            }
254
        } else {
255
            false
39✔
256
        }
257
    }
91✔
258

259
    pub fn is_medium_plugin(&self) -> bool {
27✔
260
        // If the medium flag is set in a light plugin then the medium flag is ignored.
27✔
261
        self.is_medium_flag_set() && !self.is_light_plugin()
27✔
262
    }
27✔
263

264
    pub fn is_update_plugin(&self) -> bool {
4✔
265
        // The update flag is unset by the game if the plugin has no masters or
4✔
266
        // if the plugin's light or medium flags are set.
4✔
267
        self.is_update_flag_set()
4✔
268
            && !self.is_light_flag_set()
4✔
269
            && !self.is_medium_flag_set()
3✔
270
            && self.masters().map(|m| !m.is_empty()).unwrap_or(false)
2✔
271
    }
4✔
272

273
    pub fn is_blueprint_plugin(&self) -> bool {
2✔
274
        match self.game_id {
2✔
275
            GameId::Starfield => self.data.header_record.header().flags() & 0x800 != 0,
2✔
276
            _ => false,
×
277
        }
278
    }
2✔
279

280
    pub fn is_valid(game_id: GameId, filepath: &Path, options: ParseOptions) -> bool {
2✔
281
        let mut plugin = Plugin::new(game_id, filepath);
2✔
282

2✔
283
        plugin.parse_file(options).is_ok()
2✔
284
    }
2✔
285

286
    pub fn description(&self) -> Result<Option<String>, Error> {
6✔
287
        let (target_subrecord_type, description_offset) = match self.game_id {
6✔
288
            GameId::Morrowind => (b"HEDR", 40),
2✔
289
            _ => (b"SNAM", 0),
4✔
290
        };
291

292
        for subrecord in self.data.header_record.subrecords() {
14✔
293
            if subrecord.subrecord_type() == target_subrecord_type {
14✔
294
                let data = subrecord
6✔
295
                    .data()
6✔
296
                    .get(description_offset..)
6✔
297
                    .map(until_first_null)
6✔
298
                    .ok_or_else(|| {
6✔
299
                        Error::ParsingError(
1✔
300
                            subrecord.data().into(),
1✔
301
                            ParsingErrorKind::SubrecordDataTooShort(description_offset),
1✔
302
                        )
1✔
303
                    })?;
6✔
304

305
                return WINDOWS_1252
5✔
306
                    .decode_without_bom_handling_and_without_replacement(data)
5✔
307
                    .map(|s| Some(s.to_string()))
5✔
308
                    .ok_or(Error::DecodeError(data.into()));
5✔
309
            }
8✔
310
        }
311

312
        Ok(None)
×
313
    }
6✔
314

315
    pub fn header_version(&self) -> Option<f32> {
13✔
316
        self.data
13✔
317
            .header_record
13✔
318
            .subrecords()
13✔
319
            .iter()
13✔
320
            .find(|s| s.subrecord_type() == b"HEDR")
13✔
321
            .and_then(|s| crate::le_slice_to_f32(s.data()).ok())
13✔
322
    }
13✔
323

324
    pub fn record_and_group_count(&self) -> Option<u32> {
7✔
325
        let count_offset = match self.game_id {
7✔
326
            GameId::Morrowind => 296,
5✔
327
            _ => 4,
2✔
328
        };
329

330
        self.data
7✔
331
            .header_record
7✔
332
            .subrecords()
7✔
333
            .iter()
7✔
334
            .find(|s| s.subrecord_type() == b"HEDR")
7✔
335
            .and_then(|s| s.data().get(count_offset..))
7✔
336
            .and_then(|d| crate::le_slice_to_u32(d).ok())
7✔
337
    }
7✔
338

339
    /// This needs records to be resolved first if run for Morrowind or Starfield.
340
    pub fn count_override_records(&self) -> Result<usize, Error> {
5✔
341
        match &self.data.record_ids {
5✔
342
            RecordIds::None => Ok(0),
×
343
            RecordIds::FormIds(_) | RecordIds::NamespacedIds(_) => {
344
                Err(Error::UnresolvedRecordIds(self.path.clone()))
2✔
345
            }
346
            RecordIds::Resolved(form_ids) => {
3✔
347
                let count = form_ids.iter().filter(|f| f.is_overridden_record()).count();
21✔
348
                Ok(count)
3✔
349
            }
350
        }
351
    }
5✔
352

353
    pub fn overlaps_with(&self, other: &Self) -> Result<bool, Error> {
9✔
354
        use RecordIds::{FormIds, NamespacedIds, Resolved};
355
        match (&self.data.record_ids, &other.data.record_ids) {
9✔
356
            (FormIds(_), _) => Err(Error::UnresolvedRecordIds(self.path.clone())),
1✔
357
            (_, FormIds(_)) => Err(Error::UnresolvedRecordIds(other.path.clone())),
1✔
358
            (Resolved(left), Resolved(right)) => Ok(sorted_slices_intersect(left, right)),
4✔
359
            (NamespacedIds(left), NamespacedIds(right)) => Ok(sorted_slices_intersect(left, right)),
3✔
360
            _ => Ok(false),
×
361
        }
362
    }
9✔
363

364
    /// Count the number of records that appear in this plugin and one or more
365
    /// the others passed. If more than one other contains the same record, it
366
    /// is only counted once.
367
    pub fn overlap_size(&self, others: &[&Self]) -> Result<usize, Error> {
15✔
368
        use RecordIds::{FormIds, NamespacedIds, None, Resolved};
369

370
        match &self.data.record_ids {
15✔
371
            FormIds(_) => Err(Error::UnresolvedRecordIds(self.path.clone())),
1✔
372
            Resolved(ids) => {
7✔
373
                let mut count = 0;
7✔
374
                for id in ids {
67✔
375
                    for other in others {
126✔
376
                        match &other.data.record_ids {
66✔
377
                            FormIds(_) => {
378
                                return Err(Error::UnresolvedRecordIds(other.path.clone()))
1✔
379
                            }
380
                            Resolved(master_ids) if master_ids.binary_search(id).is_ok() => {
66✔
381
                                count += 1;
11✔
382
                                break;
11✔
383
                            }
384
                            _ => {
65✔
385
                                // Do nothing.
65✔
386
                            }
65✔
387
                        }
388
                    }
389
                }
390

391
                Ok(count)
6✔
392
            }
393
            NamespacedIds(ids) => {
5✔
394
                let count = ids
5✔
395
                    .iter()
5✔
396
                    .filter(|id| {
50✔
397
                        others.iter().any(|other| match &other.data.record_ids {
66✔
398
                            NamespacedIds(master_ids) => master_ids.binary_search(id).is_ok(),
56✔
399
                            _ => false,
10✔
400
                        })
66✔
401
                    })
50✔
402
                    .count();
5✔
403
                Ok(count)
5✔
404
            }
405
            None => Ok(0),
2✔
406
        }
407
    }
15✔
408

409
    pub fn is_valid_as_light_plugin(&self) -> Result<bool, Error> {
11✔
410
        if self.game_id.supports_light_plugins() {
11✔
411
            match &self.data.record_ids {
6✔
412
                RecordIds::None => Ok(true),
×
413
                RecordIds::FormIds(_) => Err(Error::UnresolvedRecordIds(self.path.clone())),
1✔
414
                RecordIds::Resolved(form_ids) => {
5✔
415
                    let valid_range = self.valid_light_form_id_range();
5✔
416

5✔
417
                    let is_valid = form_ids
5✔
418
                        .iter()
5✔
419
                        .filter(|f| !f.is_overridden_record())
42✔
420
                        .all(|f| f.is_object_index_in(&valid_range));
26✔
421

5✔
422
                    Ok(is_valid)
5✔
423
                }
424
                RecordIds::NamespacedIds(_) => Ok(false),
×
425
            }
426
        } else {
427
            Ok(false)
5✔
428
        }
429
    }
11✔
430

431
    pub fn is_valid_as_medium_plugin(&self) -> Result<bool, Error> {
2✔
432
        if self.game_id.supports_medium_plugins() {
2✔
433
            match &self.data.record_ids {
2✔
434
                RecordIds::None => Ok(true),
×
435
                RecordIds::FormIds(_) => Err(Error::UnresolvedRecordIds(self.path.clone())),
1✔
436
                RecordIds::Resolved(form_ids) => {
1✔
437
                    let valid_range = self.valid_medium_form_id_range();
1✔
438

1✔
439
                    let is_valid = form_ids
1✔
440
                        .iter()
1✔
441
                        .filter(|f| !f.is_overridden_record())
10✔
442
                        .all(|f| f.is_object_index_in(&valid_range));
10✔
443

1✔
444
                    Ok(is_valid)
1✔
445
                }
446
                RecordIds::NamespacedIds(_) => Ok(false), // this should never happen.
×
447
            }
448
        } else {
449
            Ok(false)
×
450
        }
451
    }
2✔
452

453
    pub fn is_valid_as_update_plugin(&self) -> Result<bool, Error> {
3✔
454
        if self.game_id == GameId::Starfield {
3✔
455
            // If an update plugin has a record that does not override an existing record, that
456
            // record is placed into the mod index of the plugin's first master, which risks
457
            // overwriting an unrelated record with the same object index, so treat that case as
458
            // invalid.
459
            match &self.data.record_ids {
3✔
460
                RecordIds::None => Ok(true),
×
461
                RecordIds::FormIds(_) => Err(Error::UnresolvedRecordIds(self.path.clone())),
1✔
462
                RecordIds::Resolved(form_ids) => {
2✔
463
                    Ok(form_ids.iter().all(ResolvedRecordId::is_overridden_record))
2✔
464
                }
465
                RecordIds::NamespacedIds(_) => Ok(false), // this should never happen.
×
466
            }
467
        } else {
468
            Ok(false)
×
469
        }
470
    }
3✔
471

472
    fn header_type(&self) -> &'static [u8] {
126✔
473
        match self.game_id {
126✔
474
            GameId::Morrowind => b"TES3",
29✔
475
            _ => b"TES4",
97✔
476
        }
477
    }
126✔
478

479
    fn is_master_flag_set(&self) -> bool {
22✔
480
        match self.game_id {
22✔
481
            GameId::Morrowind => self
3✔
482
                .data
3✔
483
                .header_record
3✔
484
                .subrecords()
3✔
485
                .iter()
3✔
486
                .find(|s| s.subrecord_type() == b"HEDR")
3✔
487
                .and_then(|s| s.data().get(4))
3✔
488
                .is_some_and(|b| b & 0x1 != 0),
3✔
489
            _ => self.data.header_record.header().flags() & 0x1 != 0,
19✔
490
        }
491
    }
22✔
492

493
    fn is_light_flag_set(&self) -> bool {
56✔
494
        let flag = match self.game_id {
56✔
495
            GameId::Starfield => 0x100,
38✔
496
            GameId::SkyrimSE | GameId::Fallout4 => 0x200,
18✔
497
            _ => return false,
×
498
        };
499

500
        self.data.header_record.header().flags() & flag != 0
56✔
501
    }
56✔
502

503
    fn is_medium_flag_set(&self) -> bool {
85✔
504
        let flag = match self.game_id {
85✔
505
            GameId::Starfield => 0x400,
32✔
506
            _ => return false,
53✔
507
        };
508

509
        self.data.header_record.header().flags() & flag != 0
32✔
510
    }
85✔
511

512
    fn is_update_flag_set(&self) -> bool {
34✔
513
        match self.game_id {
34✔
514
            GameId::Starfield => self.data.header_record.header().flags() & 0x200 != 0,
34✔
515
            _ => false,
×
516
        }
517
    }
34✔
518

519
    fn valid_light_form_id_range(&self) -> RangeInclusive<u32> {
15✔
520
        match self.game_id {
15✔
521
            GameId::SkyrimSE => match self.header_version() {
5✔
522
                Some(v) if v < 1.71 => 0x800..=0xFFF,
5✔
523
                Some(_) => 0..=0xFFF,
1✔
524
                None => 0..=0,
×
525
            },
526
            GameId::Fallout4 => match self.header_version() {
3✔
527
                Some(v) if v < 1.0 => 0x800..=0xFFF,
3✔
528
                Some(_) => 0x001..=0xFFF,
1✔
529
                None => 0..=0,
×
530
            },
531
            GameId::Starfield => 0..=0xFFF,
2✔
532
            _ => 0..=0,
5✔
533
        }
534
    }
15✔
535

536
    fn valid_medium_form_id_range(&self) -> RangeInclusive<u32> {
2✔
537
        match self.game_id {
2✔
538
            GameId::Starfield => 0..=0xFFFF,
2✔
539
            _ => 0..=0,
×
540
        }
541
    }
2✔
542
}
543

544
#[derive(Clone, Debug, PartialEq, Eq)]
545
pub struct PluginMetadata {
546
    filename: String,
547
    scale: PluginScale,
548
    record_ids: Box<[NamespacedId]>,
549
}
550

551
// Get PluginMetadata objects for a collection of loaded plugins.
552
pub fn plugins_metadata(plugins: &[&Plugin]) -> Result<Vec<PluginMetadata>, Error> {
2✔
553
    let mut vec = Vec::new();
2✔
554

555
    for plugin in plugins {
6✔
556
        let filename = plugin
4✔
557
            .filename()
4✔
558
            .ok_or_else(|| Error::NoFilename(plugin.path().to_path_buf()))?;
4✔
559

560
        let record_ids = if plugin.game_id == GameId::Morrowind {
4✔
561
            match &plugin.data.record_ids {
1✔
562
                RecordIds::NamespacedIds(ids) => ids.clone(),
1✔
563
                _ => Vec::new(), // This should never happen.
×
564
            }
565
        } else {
566
            Vec::new()
3✔
567
        };
568

569
        let metadata = PluginMetadata {
4✔
570
            filename,
4✔
571
            scale: plugin.scale(),
4✔
572
            record_ids: record_ids.into_boxed_slice(),
4✔
573
        };
4✔
574

4✔
575
        vec.push(metadata);
4✔
576
    }
577

578
    Ok(vec)
2✔
579
}
2✔
580

581
fn sorted_slices_intersect<T: PartialOrd>(left: &[T], right: &[T]) -> bool {
7✔
582
    let mut left_iter = left.iter();
7✔
583
    let mut right_iter = right.iter();
7✔
584

7✔
585
    let mut left_element = left_iter.next();
7✔
586
    let mut right_element = right_iter.next();
7✔
587

588
    while let (Some(left_value), Some(right_value)) = (left_element, right_element) {
46✔
589
        if left_value < right_value {
42✔
590
            left_element = left_iter.next();
19✔
591
        } else if left_value > right_value {
23✔
592
            right_element = right_iter.next();
20✔
593
        } else {
20✔
594
            return true;
3✔
595
        }
596
    }
597

598
    false
4✔
599
}
7✔
600

601
fn resolve_form_ids(
48✔
602
    game_id: GameId,
48✔
603
    form_ids: &[u32],
48✔
604
    plugin_metadata: &PluginMetadata,
48✔
605
    masters: &[String],
48✔
606
    other_plugins_metadata: &[PluginMetadata],
48✔
607
) -> Result<Vec<ResolvedRecordId>, Error> {
48✔
608
    let hashed_parent = hashed_parent(game_id, plugin_metadata);
48✔
609
    let hashed_masters = match game_id {
48✔
610
        GameId::Starfield => hashed_masters_for_starfield(masters, other_plugins_metadata)?,
15✔
611
        _ => hashed_masters(masters),
33✔
612
    };
613

614
    let mut form_ids: Vec<_> = form_ids
48✔
615
        .iter()
48✔
616
        .map(|form_id| ResolvedRecordId::from_form_id(hashed_parent, &hashed_masters, *form_id))
359✔
617
        .collect();
48✔
618

48✔
619
    form_ids.sort();
48✔
620

48✔
621
    Ok(form_ids)
48✔
622
}
48✔
623

624
fn resolve_namespaced_ids(
1✔
625
    namespaced_ids: &[NamespacedId],
1✔
626
    masters: &[String],
1✔
627
    other_plugins_metadata: &[PluginMetadata],
1✔
628
) -> Result<Vec<ResolvedRecordId>, Error> {
1✔
629
    let mut record_ids: HashSet<NamespacedId> = HashSet::new();
1✔
630

631
    for master in masters {
2✔
632
        let master_record_ids = other_plugins_metadata
1✔
633
            .iter()
1✔
634
            .find(|m| unicase::eq(&m.filename, master))
1✔
635
            .map(|m| &m.record_ids)
1✔
636
            .ok_or_else(|| Error::PluginMetadataNotFound(master.clone()))?;
1✔
637

638
        record_ids.extend(master_record_ids.iter().cloned());
1✔
639
    }
640

641
    let mut resolved_ids: Vec<_> = namespaced_ids
1✔
642
        .iter()
1✔
643
        .map(|id| ResolvedRecordId::from_namespaced_id(id, &record_ids))
8✔
644
        .collect();
1✔
645

1✔
646
    resolved_ids.sort();
1✔
647

1✔
648
    Ok(resolved_ids)
1✔
649
}
1✔
650

651
fn hashed_parent(game_id: GameId, parent_metadata: &PluginMetadata) -> SourcePlugin {
54✔
652
    match game_id {
54✔
653
        GameId::Starfield => {
654
            // The Creation Kit can create plugins that contain new records that use mod indexes that don't match the plugin's scale (full/medium/small), e.g. a medium plugin might have new records with FormIDs that don't start with 0xFD. However, at runtime the mod index part is replaced with the mod index of the plugin, according to the plugin's scale, so the plugin's scale is what matters when resolving FormIDs for comparison between plugins.
655
            let object_index_mask = match parent_metadata.scale {
18✔
656
                PluginScale::Full => ObjectIndexMask::Full,
15✔
657
                PluginScale::Medium => ObjectIndexMask::Medium,
2✔
658
                PluginScale::Small => ObjectIndexMask::Small,
1✔
659
            };
660
            SourcePlugin::parent(&parent_metadata.filename, object_index_mask)
18✔
661
        }
662
        // The full object index mask is used for all plugin scales in other games.
663
        _ => SourcePlugin::parent(&parent_metadata.filename, ObjectIndexMask::Full),
36✔
664
    }
665
}
54✔
666

667
fn hashed_masters(masters: &[String]) -> Vec<SourcePlugin> {
34✔
668
    masters
34✔
669
        .iter()
34✔
670
        .enumerate()
34✔
671
        .filter_map(|(i, m)| {
34✔
672
            // If the index is somehow > 256 then this isn't a valid master so skip it.
673
            let i = u8::try_from(i).ok()?;
26✔
674
            let mod_index_mask = u32::from(i) << 24u8;
26✔
675
            Some(SourcePlugin::master(
26✔
676
                m,
26✔
677
                mod_index_mask,
26✔
678
                ObjectIndexMask::Full,
26✔
679
            ))
26✔
680
        })
34✔
681
        .collect()
34✔
682
}
34✔
683

684
// Get HashedMaster objects for the current plugin.
685
fn hashed_masters_for_starfield(
18✔
686
    masters: &[String],
18✔
687
    masters_metadata: &[PluginMetadata],
18✔
688
) -> Result<Vec<SourcePlugin>, Error> {
18✔
689
    let mut hashed_masters = Vec::new();
18✔
690
    let mut full_mask = 0;
18✔
691
    let mut medium_mask = 0xFD00_0000;
18✔
692
    let mut small_mask = 0xFE00_0000;
18✔
693

694
    for master in masters {
33✔
695
        let master_scale = masters_metadata
16✔
696
            .iter()
16✔
697
            .find(|m| unicase::eq(&m.filename, master))
39✔
698
            .map(|m| m.scale)
16✔
699
            .ok_or_else(|| Error::PluginMetadataNotFound(master.clone()))?;
16✔
700

701
        match master_scale {
15✔
702
            PluginScale::Full => {
10✔
703
                hashed_masters.push(SourcePlugin::master(
10✔
704
                    master,
10✔
705
                    full_mask,
10✔
706
                    ObjectIndexMask::Full,
10✔
707
                ));
10✔
708

10✔
709
                full_mask += 0x0100_0000;
10✔
710
            }
10✔
711
            PluginScale::Medium => {
2✔
712
                hashed_masters.push(SourcePlugin::master(
2✔
713
                    master,
2✔
714
                    medium_mask,
2✔
715
                    ObjectIndexMask::Medium,
2✔
716
                ));
2✔
717

2✔
718
                medium_mask += 0x0001_0000;
2✔
719
            }
2✔
720
            PluginScale::Small => {
3✔
721
                hashed_masters.push(SourcePlugin::master(
3✔
722
                    master,
3✔
723
                    small_mask,
3✔
724
                    ObjectIndexMask::Small,
3✔
725
                ));
3✔
726

3✔
727
                small_mask += 0x0000_1000;
3✔
728
            }
3✔
729
        }
730
    }
731

732
    Ok(hashed_masters)
17✔
733
}
18✔
734

735
fn masters(header_record: &Record) -> Result<Vec<String>, Error> {
52✔
736
    header_record
52✔
737
        .subrecords()
52✔
738
        .iter()
52✔
739
        .filter(|s| s.subrecord_type() == b"MAST")
234✔
740
        .map(|s| until_first_null(s.data()))
52✔
741
        .map(|d| {
52✔
742
            WINDOWS_1252
30✔
743
                .decode_without_bom_handling_and_without_replacement(d)
30✔
744
                .map(|s| s.to_string())
30✔
745
                .ok_or(Error::DecodeError(d.into()))
30✔
746
        })
52✔
747
        .collect()
52✔
748
}
52✔
749

750
fn read_form_ids<R: BufRead + Seek>(reader: &mut R, game_id: GameId) -> Result<Vec<u32>, Error> {
56✔
751
    let mut form_ids = Vec::new();
56✔
752
    let mut header_buf = [0; MAX_RECORD_HEADER_LENGTH];
56✔
753

754
    while !reader.fill_buf()?.is_empty() {
136✔
755
        Group::read_form_ids(reader, game_id, &mut form_ids, &mut header_buf)?;
80✔
756
    }
757

758
    Ok(form_ids)
56✔
759
}
56✔
760

761
fn read_morrowind_record_ids<R: BufRead + Seek>(reader: &mut R) -> Result<RecordIds, Error> {
19✔
762
    let mut record_ids = Vec::new();
19✔
763
    let mut header_buf = [0; 16]; // Morrowind record headers are 16 bytes long.
19✔
764

765
    while !reader.fill_buf()?.is_empty() {
177✔
766
        let (_, record_id) =
158✔
767
            Record::read_record_id(reader, GameId::Morrowind, &mut header_buf, false)?;
158✔
768

769
        if let Some(RecordId::NamespacedId(record_id)) = record_id {
158✔
770
            record_ids.push(record_id);
158✔
771
        }
158✔
772
    }
773

774
    record_ids.sort();
19✔
775

19✔
776
    Ok(record_ids.into())
19✔
777
}
19✔
778

779
fn read_record_ids<R: BufRead + Seek>(reader: &mut R, game_id: GameId) -> Result<RecordIds, Error> {
75✔
780
    if game_id == GameId::Morrowind {
75✔
781
        read_morrowind_record_ids(reader)
19✔
782
    } else {
783
        read_form_ids(reader, game_id).map(Into::into)
56✔
784
    }
785
}
75✔
786

787
fn read_plugin<R: BufRead + Seek>(
126✔
788
    reader: &mut R,
126✔
789
    game_id: GameId,
126✔
790
    options: ParseOptions,
126✔
791
    expected_header_type: &'static [u8],
126✔
792
) -> Result<PluginData, Error> {
126✔
793
    let header_record = Record::read(reader, game_id, expected_header_type)?;
126✔
794

795
    if options.header_only {
122✔
796
        return Ok(PluginData {
47✔
797
            header_record,
47✔
798
            record_ids: RecordIds::None,
47✔
799
        });
47✔
800
    }
75✔
801

802
    let record_ids = read_record_ids(reader, game_id)?;
75✔
803

804
    Ok(PluginData {
75✔
805
        header_record,
75✔
806
        record_ids,
75✔
807
    })
75✔
808
}
126✔
809

810
/// Return the slice up to and not including the first null byte. If there is no
811
/// null byte, return the whole string.
812
fn until_first_null(bytes: &[u8]) -> &[u8] {
35✔
813
    if let Some(i) = memchr::memchr(0, bytes) {
35✔
814
        bytes.split_at(i).0
35✔
815
    } else {
816
        bytes
×
817
    }
818
}
35✔
819

820
#[cfg(test)]
821
mod tests {
822
    use std::fs::{copy, read};
823
    use std::io::Cursor;
824
    use tempfile::tempdir;
825

826
    use super::*;
827

828
    mod morrowind {
829
        use super::*;
830

831
        #[test]
832
        fn parse_file_should_succeed() {
1✔
833
            let mut plugin = Plugin::new(
1✔
834
                GameId::Morrowind,
1✔
835
                Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
1✔
836
            );
1✔
837

1✔
838
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
839

840
            match plugin.data.record_ids {
1✔
841
                RecordIds::NamespacedIds(ids) => assert_eq!(10, ids.len()),
1✔
842
                _ => panic!("Expected namespaced record IDs"),
×
843
            }
844
        }
1✔
845

846
        #[test]
847
        fn plugin_parse_file_should_read_a_unique_id_for_each_record() {
1✔
848
            let mut plugin = Plugin::new(
1✔
849
                GameId::Morrowind,
1✔
850
                Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
1✔
851
            );
1✔
852

1✔
853
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
854

855
            match plugin.data.record_ids {
1✔
856
                RecordIds::NamespacedIds(ids) => {
1✔
857
                    let set: HashSet<NamespacedId> = ids.iter().cloned().collect();
1✔
858
                    assert_eq!(set.len(), ids.len());
1✔
859
                }
860
                _ => panic!("Expected namespaced record IDs"),
×
861
            }
862
        }
1✔
863

864
        #[test]
865
        fn parse_file_header_only_should_not_store_record_ids() {
1✔
866
            let mut plugin = Plugin::new(
1✔
867
                GameId::Morrowind,
1✔
868
                Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
1✔
869
            );
1✔
870

1✔
871
            let result = plugin.parse_file(ParseOptions::header_only());
1✔
872

1✔
873
            assert!(result.is_ok());
1✔
874

875
            assert_eq!(RecordIds::None, plugin.data.record_ids);
1✔
876
        }
1✔
877

878
        #[test]
879
        fn game_id_should_return_the_plugins_associated_game_id() {
1✔
880
            let plugin = Plugin::new(GameId::Morrowind, Path::new("Data/Blank.esm"));
1✔
881

1✔
882
            assert_eq!(GameId::Morrowind, plugin.game_id());
1✔
883
        }
1✔
884

885
        #[test]
886
        fn is_master_file_should_be_true_for_plugin_with_master_flag_set() {
1✔
887
            let mut plugin = Plugin::new(
1✔
888
                GameId::Morrowind,
1✔
889
                Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
1✔
890
            );
1✔
891

1✔
892
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
893
            assert!(plugin.is_master_file());
1✔
894
        }
1✔
895

896
        #[test]
897
        fn is_master_file_should_be_false_for_plugin_without_master_flag_set() {
1✔
898
            let mut plugin = Plugin::new(
1✔
899
                GameId::Morrowind,
1✔
900
                Path::new("testing-plugins/Morrowind/Data Files/Blank.esp"),
1✔
901
            );
1✔
902

1✔
903
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
904
            assert!(!plugin.is_master_file());
1✔
905
        }
1✔
906

907
        #[test]
908
        fn is_master_file_should_ignore_file_extension() {
1✔
909
            let tmp_dir = tempdir().unwrap();
1✔
910

1✔
911
            let esm = tmp_dir.path().join("Blank.esm");
1✔
912
            copy(
1✔
913
                Path::new("testing-plugins/Morrowind/Data Files/Blank.esp"),
1✔
914
                &esm,
1✔
915
            )
1✔
916
            .unwrap();
1✔
917

1✔
918
            let mut plugin = Plugin::new(GameId::Morrowind, &esm);
1✔
919

1✔
920
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
921
            assert!(!plugin.is_master_file());
1✔
922
        }
1✔
923

924
        #[test]
925
        fn is_light_plugin_should_be_false() {
1✔
926
            let plugin = Plugin::new(GameId::Morrowind, Path::new("Blank.esp"));
1✔
927
            assert!(!plugin.is_light_plugin());
1✔
928
            let plugin = Plugin::new(GameId::Morrowind, Path::new("Blank.esm"));
1✔
929
            assert!(!plugin.is_light_plugin());
1✔
930
            let plugin = Plugin::new(GameId::Morrowind, Path::new("Blank.esl"));
1✔
931
            assert!(!plugin.is_light_plugin());
1✔
932
        }
1✔
933

934
        #[test]
935
        fn is_medium_plugin_should_be_false() {
1✔
936
            let plugin = Plugin::new(GameId::Morrowind, Path::new("Blank.esp"));
1✔
937
            assert!(!plugin.is_medium_plugin());
1✔
938
            let plugin = Plugin::new(GameId::Morrowind, Path::new("Blank.esm"));
1✔
939
            assert!(!plugin.is_medium_plugin());
1✔
940
            let plugin = Plugin::new(GameId::Morrowind, Path::new("Blank.esl"));
1✔
941
            assert!(!plugin.is_medium_plugin());
1✔
942
        }
1✔
943

944
        #[test]
945
        fn description_should_trim_nulls_in_plugin_header_hedr_subrecord_content() {
1✔
946
            let mut plugin = Plugin::new(
1✔
947
                GameId::Morrowind,
1✔
948
                Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
1✔
949
            );
1✔
950

1✔
951
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
952

953
            assert_eq!("v5.0", plugin.description().unwrap().unwrap());
1✔
954
        }
1✔
955

956
        #[expect(clippy::float_cmp, reason = "float values should be exactly equal")]
957
        #[test]
958
        fn header_version_should_return_plugin_header_hedr_subrecord_field() {
1✔
959
            let mut plugin = Plugin::new(
1✔
960
                GameId::Morrowind,
1✔
961
                Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
1✔
962
            );
1✔
963

1✔
964
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
965

966
            assert_eq!(1.2, plugin.header_version().unwrap());
1✔
967
        }
1✔
968

969
        #[test]
970
        fn record_and_group_count_should_read_correct_offset() {
1✔
971
            let mut plugin = Plugin::new(
1✔
972
                GameId::Morrowind,
1✔
973
                Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
1✔
974
            );
1✔
975

1✔
976
            assert!(plugin.record_and_group_count().is_none());
1✔
977
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
978
            assert_eq!(10, plugin.record_and_group_count().unwrap());
1✔
979
        }
1✔
980

981
        #[test]
982
        fn record_and_group_count_should_match_record_ids_length() {
1✔
983
            let mut plugin = Plugin::new(
1✔
984
                GameId::Morrowind,
1✔
985
                Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
1✔
986
            );
1✔
987

1✔
988
            assert!(plugin.record_and_group_count().is_none());
1✔
989
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
990
            assert_eq!(10, plugin.record_and_group_count().unwrap());
1✔
991
            match plugin.data.record_ids {
1✔
992
                RecordIds::NamespacedIds(ids) => assert_eq!(10, ids.len()),
1✔
993
                _ => panic!("Expected namespaced record IDs"),
×
994
            }
995
        }
1✔
996

997
        #[test]
998
        fn count_override_records_should_error_if_record_ids_are_not_yet_resolved() {
1✔
999
            let mut plugin = Plugin::new(
1✔
1000
                GameId::Morrowind,
1✔
1001
                Path::new("testing-plugins/Morrowind/Data Files/Blank - Master Dependent.esm"),
1✔
1002
            );
1✔
1003

1✔
1004
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1005

1006
            match plugin.count_override_records().unwrap_err() {
1✔
1007
                Error::UnresolvedRecordIds(path) => assert_eq!(plugin.path, path),
1✔
1008
                _ => panic!("Expected unresolved record IDs error"),
×
1009
            }
1010
        }
1✔
1011

1012
        #[test]
1013
        fn count_override_records_should_count_how_many_records_are_also_present_in_masters() {
1✔
1014
            let mut plugin = Plugin::new(
1✔
1015
                GameId::Morrowind,
1✔
1016
                Path::new("testing-plugins/Morrowind/Data Files/Blank - Master Dependent.esm"),
1✔
1017
            );
1✔
1018
            let mut master = Plugin::new(
1✔
1019
                GameId::Morrowind,
1✔
1020
                Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
1✔
1021
            );
1✔
1022

1✔
1023
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1024
            assert!(master.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1025

1026
            let plugins_metadata = plugins_metadata(&[&master]).unwrap();
1✔
1027

1✔
1028
            plugin.resolve_record_ids(&plugins_metadata).unwrap();
1✔
1029

1✔
1030
            assert_eq!(4, plugin.count_override_records().unwrap());
1✔
1031
        }
1✔
1032

1033
        #[test]
1034
        fn overlaps_with_should_detect_when_two_plugins_have_a_record_with_the_same_id() {
1✔
1035
            let mut plugin1 = Plugin::new(
1✔
1036
                GameId::Morrowind,
1✔
1037
                Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
1✔
1038
            );
1✔
1039
            let mut plugin2 = Plugin::new(
1✔
1040
                GameId::Morrowind,
1✔
1041
                Path::new("testing-plugins/Morrowind/Data Files/Blank - Different.esm"),
1✔
1042
            );
1✔
1043

1✔
1044
            assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1045
            assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1046

1047
            assert!(plugin1.overlaps_with(&plugin1).unwrap());
1✔
1048
            assert!(!plugin1.overlaps_with(&plugin2).unwrap());
1✔
1049
        }
1✔
1050

1051
        #[test]
1052
        fn overlap_size_should_only_count_each_record_once() {
1✔
1053
            let mut plugin1 = Plugin::new(
1✔
1054
                GameId::Morrowind,
1✔
1055
                Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
1✔
1056
            );
1✔
1057
            let mut plugin2 = Plugin::new(
1✔
1058
                GameId::Morrowind,
1✔
1059
                Path::new("testing-plugins/Morrowind/Data Files/Blank - Master Dependent.esm"),
1✔
1060
            );
1✔
1061

1✔
1062
            assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1063
            assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1064

1065
            assert_eq!(4, plugin1.overlap_size(&[&plugin2, &plugin2]).unwrap());
1✔
1066
        }
1✔
1067

1068
        #[test]
1069
        fn overlap_size_should_check_against_all_given_plugins() {
1✔
1070
            let mut plugin1 = Plugin::new(
1✔
1071
                GameId::Morrowind,
1✔
1072
                Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
1✔
1073
            );
1✔
1074
            let mut plugin2 = Plugin::new(
1✔
1075
                GameId::Morrowind,
1✔
1076
                Path::new("testing-plugins/Morrowind/Data Files/Blank.esp"),
1✔
1077
            );
1✔
1078
            let mut plugin3 = Plugin::new(
1✔
1079
                GameId::Morrowind,
1✔
1080
                Path::new("testing-plugins/Morrowind/Data Files/Blank - Master Dependent.esm"),
1✔
1081
            );
1✔
1082

1✔
1083
            assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1084
            assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1085
            assert!(plugin3.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1086

1087
            assert_eq!(4, plugin1.overlap_size(&[&plugin2, &plugin3]).unwrap());
1✔
1088
        }
1✔
1089

1090
        #[test]
1091
        fn overlap_size_should_return_0_if_plugins_have_not_been_parsed() {
1✔
1092
            let mut plugin1 = Plugin::new(
1✔
1093
                GameId::Morrowind,
1✔
1094
                Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
1✔
1095
            );
1✔
1096
            let mut plugin2 = Plugin::new(
1✔
1097
                GameId::Morrowind,
1✔
1098
                Path::new("testing-plugins/Morrowind/Data Files/Blank - Master Dependent.esm"),
1✔
1099
            );
1✔
1100

1✔
1101
            assert_eq!(0, plugin1.overlap_size(&[&plugin2]).unwrap());
1✔
1102

1103
            assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1104

1105
            assert_eq!(0, plugin1.overlap_size(&[&plugin2]).unwrap());
1✔
1106

1107
            assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1108

1109
            assert_ne!(0, plugin1.overlap_size(&[&plugin2]).unwrap());
1✔
1110
        }
1✔
1111

1112
        #[test]
1113
        fn overlap_size_should_return_0_when_there_is_no_overlap() {
1✔
1114
            let mut plugin1 = Plugin::new(
1✔
1115
                GameId::Morrowind,
1✔
1116
                Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
1✔
1117
            );
1✔
1118
            let mut plugin2 = Plugin::new(
1✔
1119
                GameId::Morrowind,
1✔
1120
                Path::new("testing-plugins/Morrowind/Data Files/Blank.esp"),
1✔
1121
            );
1✔
1122

1✔
1123
            assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1124
            assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1125

1126
            assert!(!plugin1.overlaps_with(&plugin2).unwrap());
1✔
1127
            assert_eq!(0, plugin1.overlap_size(&[&plugin2]).unwrap());
1✔
1128
        }
1✔
1129

1130
        #[test]
1131
        fn valid_light_form_id_range_should_be_empty() {
1✔
1132
            let mut plugin = Plugin::new(
1✔
1133
                GameId::Morrowind,
1✔
1134
                Path::new("testing-plugins/Morrowind/Data Files/Blank - Master Dependent.esm"),
1✔
1135
            );
1✔
1136
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1137

1138
            let range = plugin.valid_light_form_id_range();
1✔
1139
            assert_eq!(&0, range.start());
1✔
1140
            assert_eq!(&0, range.end());
1✔
1141
        }
1✔
1142

1143
        #[test]
1144
        fn is_valid_as_light_plugin_should_always_be_false() {
1✔
1145
            let mut plugin = Plugin::new(
1✔
1146
                GameId::Morrowind,
1✔
1147
                Path::new("testing-plugins/Morrowind/Data Files/Blank - Master Dependent.esm"),
1✔
1148
            );
1✔
1149
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1150
            assert!(!plugin.is_valid_as_light_plugin().unwrap());
1✔
1151
        }
1✔
1152
    }
1153

1154
    mod oblivion {
1155
        use super::super::*;
1156

1157
        #[test]
1158
        fn is_light_plugin_should_be_false() {
1✔
1159
            let plugin = Plugin::new(GameId::Oblivion, Path::new("Blank.esp"));
1✔
1160
            assert!(!plugin.is_light_plugin());
1✔
1161
            let plugin = Plugin::new(GameId::Oblivion, Path::new("Blank.esm"));
1✔
1162
            assert!(!plugin.is_light_plugin());
1✔
1163
            let plugin = Plugin::new(GameId::Oblivion, Path::new("Blank.esl"));
1✔
1164
            assert!(!plugin.is_light_plugin());
1✔
1165
        }
1✔
1166

1167
        #[test]
1168
        fn is_medium_plugin_should_be_false() {
1✔
1169
            let plugin = Plugin::new(GameId::Oblivion, Path::new("Blank.esp"));
1✔
1170
            assert!(!plugin.is_medium_plugin());
1✔
1171
            let plugin = Plugin::new(GameId::Oblivion, Path::new("Blank.esm"));
1✔
1172
            assert!(!plugin.is_medium_plugin());
1✔
1173
            let plugin = Plugin::new(GameId::Oblivion, Path::new("Blank.esl"));
1✔
1174
            assert!(!plugin.is_medium_plugin());
1✔
1175
        }
1✔
1176

1177
        #[expect(clippy::float_cmp, reason = "float values should be exactly equal")]
1178
        #[test]
1179
        fn header_version_should_return_plugin_header_hedr_subrecord_field() {
1✔
1180
            let mut plugin = Plugin::new(
1✔
1181
                GameId::Oblivion,
1✔
1182
                Path::new("testing-plugins/Oblivion/Data/Blank.esm"),
1✔
1183
            );
1✔
1184

1✔
1185
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
1186

1187
            assert_eq!(0.8, plugin.header_version().unwrap());
1✔
1188
        }
1✔
1189

1190
        #[test]
1191
        fn valid_light_form_id_range_should_be_empty() {
1✔
1192
            let mut plugin = Plugin::new(
1✔
1193
                GameId::Oblivion,
1✔
1194
                Path::new("testing-plugins/Oblivion/Data/Blank - Master Dependent.esm"),
1✔
1195
            );
1✔
1196
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1197

1198
            let range = plugin.valid_light_form_id_range();
1✔
1199
            assert_eq!(&0, range.start());
1✔
1200
            assert_eq!(&0, range.end());
1✔
1201
        }
1✔
1202

1203
        #[test]
1204
        fn is_valid_as_light_plugin_should_always_be_false() {
1✔
1205
            let mut plugin = Plugin::new(
1✔
1206
                GameId::Oblivion,
1✔
1207
                Path::new("testing-plugins/Oblivion/Data/Blank - Master Dependent.esm"),
1✔
1208
            );
1✔
1209
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1210
            assert!(!plugin.is_valid_as_light_plugin().unwrap());
1✔
1211
        }
1✔
1212
    }
1213

1214
    mod skyrim {
1215
        use super::*;
1216

1217
        #[test]
1218
        fn parse_file_should_succeed() {
1✔
1219
            let mut plugin = Plugin::new(
1✔
1220
                GameId::Skyrim,
1✔
1221
                Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1✔
1222
            );
1✔
1223

1✔
1224
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1225

1226
            match plugin.data.record_ids {
1✔
1227
                RecordIds::Resolved(ids) => assert_eq!(10, ids.len()),
1✔
1228
                _ => panic!("Expected resolved FormIDs"),
×
1229
            }
1230
        }
1✔
1231

1232
        #[test]
1233
        fn parse_file_header_only_should_not_store_form_ids() {
1✔
1234
            let mut plugin = Plugin::new(
1✔
1235
                GameId::Skyrim,
1✔
1236
                Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1✔
1237
            );
1✔
1238

1✔
1239
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
1240

1241
            assert_eq!(RecordIds::None, plugin.data.record_ids);
1✔
1242
        }
1✔
1243

1244
        #[test]
1245
        fn game_id_should_return_the_plugins_associated_game_id() {
1✔
1246
            let plugin = Plugin::new(GameId::Skyrim, Path::new("Data/Blank.esm"));
1✔
1247

1✔
1248
            assert_eq!(GameId::Skyrim, plugin.game_id());
1✔
1249
        }
1✔
1250

1251
        #[test]
1252
        fn is_master_file_should_be_true_for_master_file() {
1✔
1253
            let mut plugin = Plugin::new(
1✔
1254
                GameId::Skyrim,
1✔
1255
                Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1✔
1256
            );
1✔
1257

1✔
1258
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
1259
            assert!(plugin.is_master_file());
1✔
1260
        }
1✔
1261

1262
        #[test]
1263
        fn is_master_file_should_be_false_for_non_master_file() {
1✔
1264
            let mut plugin = Plugin::new(
1✔
1265
                GameId::Skyrim,
1✔
1266
                Path::new("testing-plugins/Skyrim/Data/Blank.esp"),
1✔
1267
            );
1✔
1268

1✔
1269
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
1270
            assert!(!plugin.is_master_file());
1✔
1271
        }
1✔
1272

1273
        #[test]
1274
        fn is_light_plugin_should_be_false() {
1✔
1275
            let plugin = Plugin::new(GameId::Skyrim, Path::new("Blank.esp"));
1✔
1276
            assert!(!plugin.is_light_plugin());
1✔
1277
            let plugin = Plugin::new(GameId::Skyrim, Path::new("Blank.esm"));
1✔
1278
            assert!(!plugin.is_light_plugin());
1✔
1279
            let plugin = Plugin::new(GameId::Skyrim, Path::new("Blank.esl"));
1✔
1280
            assert!(!plugin.is_light_plugin());
1✔
1281
        }
1✔
1282

1283
        #[test]
1284
        fn is_medium_plugin_should_be_false() {
1✔
1285
            let plugin = Plugin::new(GameId::Skyrim, Path::new("Blank.esp"));
1✔
1286
            assert!(!plugin.is_medium_plugin());
1✔
1287
            let plugin = Plugin::new(GameId::Skyrim, Path::new("Blank.esm"));
1✔
1288
            assert!(!plugin.is_medium_plugin());
1✔
1289
            let plugin = Plugin::new(GameId::Skyrim, Path::new("Blank.esl"));
1✔
1290
            assert!(!plugin.is_medium_plugin());
1✔
1291
        }
1✔
1292

1293
        #[test]
1294
        fn description_should_return_plugin_description_field_content() {
1✔
1295
            let mut plugin = Plugin::new(
1✔
1296
                GameId::Skyrim,
1✔
1297
                Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1✔
1298
            );
1✔
1299

1✔
1300
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
1301
            assert_eq!("v5.0", plugin.description().unwrap().unwrap());
1✔
1302

1303
            let mut plugin = Plugin::new(
1✔
1304
                GameId::Skyrim,
1✔
1305
                Path::new("testing-plugins/Skyrim/Data/Blank.esp"),
1✔
1306
            );
1✔
1307

1✔
1308
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
1309
            assert_eq!(
1✔
1310
                "\u{20ac}\u{192}\u{160}",
1✔
1311
                plugin.description().unwrap().unwrap()
1✔
1312
            );
1✔
1313

1314
            let mut plugin = Plugin::new(
1✔
1315
                GameId::Skyrim,
1✔
1316
                Path::new(
1✔
1317
                    "testing-plugins/Skyrim/Data/Blank - \
1✔
1318
                      Master Dependent.esm",
1✔
1319
                ),
1✔
1320
            );
1✔
1321

1✔
1322
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
1323
            assert_eq!("", plugin.description().unwrap().unwrap());
1✔
1324
        }
1✔
1325

1326
        #[test]
1327
        fn description_should_trim_nulls_in_plugin_description_field_content() {
1✔
1328
            let mut plugin = Plugin::new(
1✔
1329
                GameId::Skyrim,
1✔
1330
                Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1✔
1331
            );
1✔
1332

1✔
1333
            let mut bytes = read(plugin.path()).unwrap();
1✔
1334

1✔
1335
            assert_eq!(0x2E, bytes[0x39]);
1✔
1336
            bytes[0x39] = 0;
1✔
1337

1✔
1338
            assert!(plugin
1✔
1339
                .parse_reader(Cursor::new(bytes), ParseOptions::whole_plugin())
1✔
1340
                .is_ok());
1✔
1341

1342
            assert_eq!("v5", plugin.description().unwrap().unwrap());
1✔
1343
        }
1✔
1344

1345
        #[expect(clippy::float_cmp, reason = "float values should be exactly equal")]
1346
        #[test]
1347
        fn header_version_should_return_plugin_header_hedr_subrecord_field() {
1✔
1348
            let mut plugin = Plugin::new(
1✔
1349
                GameId::Skyrim,
1✔
1350
                Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1✔
1351
            );
1✔
1352

1✔
1353
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
1354

1355
            assert_eq!(0.94, plugin.header_version().unwrap());
1✔
1356
        }
1✔
1357

1358
        #[test]
1359
        fn record_and_group_count_should_be_non_zero_for_a_plugin_with_records() {
1✔
1360
            let mut plugin = Plugin::new(
1✔
1361
                GameId::Skyrim,
1✔
1362
                Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1✔
1363
            );
1✔
1364

1✔
1365
            assert!(plugin.record_and_group_count().is_none());
1✔
1366
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
1367
            assert_eq!(15, plugin.record_and_group_count().unwrap());
1✔
1368
        }
1✔
1369

1370
        #[test]
1371
        fn count_override_records_should_count_how_many_records_come_from_masters() {
1✔
1372
            let mut plugin = Plugin::new(
1✔
1373
                GameId::Skyrim,
1✔
1374
                Path::new("testing-plugins/Skyrim/Data/Blank - Different Master Dependent.esp"),
1✔
1375
            );
1✔
1376

1✔
1377
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1378
            assert_eq!(2, plugin.count_override_records().unwrap());
1✔
1379
        }
1✔
1380

1381
        #[test]
1382
        fn overlaps_with_should_detect_when_two_plugins_have_a_record_from_the_same_master() {
1✔
1383
            let mut plugin1 = Plugin::new(
1✔
1384
                GameId::Skyrim,
1✔
1385
                Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1✔
1386
            );
1✔
1387
            let mut plugin2 = Plugin::new(
1✔
1388
                GameId::Skyrim,
1✔
1389
                Path::new("testing-plugins/Skyrim/Data/Blank - Different.esm"),
1✔
1390
            );
1✔
1391

1✔
1392
            assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1393
            assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1394

1395
            assert!(plugin1.overlaps_with(&plugin1).unwrap());
1✔
1396
            assert!(!plugin1.overlaps_with(&plugin2).unwrap());
1✔
1397
        }
1✔
1398

1399
        #[test]
1400
        fn overlap_size_should_only_count_each_record_once() {
1✔
1401
            let mut plugin1 = Plugin::new(
1✔
1402
                GameId::Skyrim,
1✔
1403
                Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1✔
1404
            );
1✔
1405
            let mut plugin2 = Plugin::new(
1✔
1406
                GameId::Skyrim,
1✔
1407
                Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esm"),
1✔
1408
            );
1✔
1409

1✔
1410
            assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1411
            assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1412

1413
            assert_eq!(4, plugin1.overlap_size(&[&plugin2, &plugin2]).unwrap());
1✔
1414
        }
1✔
1415

1416
        #[test]
1417
        fn overlap_size_should_check_against_all_given_plugins() {
1✔
1418
            let mut plugin1 = Plugin::new(
1✔
1419
                GameId::Skyrim,
1✔
1420
                Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1✔
1421
            );
1✔
1422
            let mut plugin2 = Plugin::new(
1✔
1423
                GameId::Skyrim,
1✔
1424
                Path::new("testing-plugins/Skyrim/Data/Blank.esp"),
1✔
1425
            );
1✔
1426
            let mut plugin3 = Plugin::new(
1✔
1427
                GameId::Skyrim,
1✔
1428
                Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esp"),
1✔
1429
            );
1✔
1430

1✔
1431
            assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1432
            assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1433
            assert!(plugin3.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1434

1435
            assert_eq!(2, plugin1.overlap_size(&[&plugin2, &plugin3]).unwrap());
1✔
1436
        }
1✔
1437

1438
        #[test]
1439
        fn overlap_size_should_return_0_if_plugins_have_not_been_parsed() {
1✔
1440
            let mut plugin1 = Plugin::new(
1✔
1441
                GameId::Skyrim,
1✔
1442
                Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1✔
1443
            );
1✔
1444
            let mut plugin2 = Plugin::new(
1✔
1445
                GameId::Skyrim,
1✔
1446
                Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esm"),
1✔
1447
            );
1✔
1448

1✔
1449
            assert_eq!(0, plugin1.overlap_size(&[&plugin2]).unwrap());
1✔
1450

1451
            assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1452

1453
            assert_eq!(0, plugin1.overlap_size(&[&plugin2]).unwrap());
1✔
1454

1455
            assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1456

1457
            assert_ne!(0, plugin1.overlap_size(&[&plugin2]).unwrap());
1✔
1458
        }
1✔
1459

1460
        #[test]
1461
        fn overlap_size_should_return_0_when_there_is_no_overlap() {
1✔
1462
            let mut plugin1 = Plugin::new(
1✔
1463
                GameId::Skyrim,
1✔
1464
                Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1✔
1465
            );
1✔
1466
            let mut plugin2 = Plugin::new(
1✔
1467
                GameId::Skyrim,
1✔
1468
                Path::new("testing-plugins/Skyrim/Data/Blank.esp"),
1✔
1469
            );
1✔
1470

1✔
1471
            assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1472
            assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1473

1474
            assert!(!plugin1.overlaps_with(&plugin2).unwrap());
1✔
1475
            assert_eq!(0, plugin1.overlap_size(&[&plugin2]).unwrap());
1✔
1476
        }
1✔
1477

1478
        #[test]
1479
        fn valid_light_form_id_range_should_be_empty() {
1✔
1480
            let mut plugin = Plugin::new(
1✔
1481
                GameId::Skyrim,
1✔
1482
                Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esm"),
1✔
1483
            );
1✔
1484
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1485

1486
            let range = plugin.valid_light_form_id_range();
1✔
1487
            assert_eq!(&0, range.start());
1✔
1488
            assert_eq!(&0, range.end());
1✔
1489
        }
1✔
1490

1491
        #[test]
1492
        fn is_valid_as_light_plugin_should_always_be_false() {
1✔
1493
            let mut plugin = Plugin::new(
1✔
1494
                GameId::Skyrim,
1✔
1495
                Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esm"),
1✔
1496
            );
1✔
1497
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1498
            assert!(!plugin.is_valid_as_light_plugin().unwrap());
1✔
1499
        }
1✔
1500
    }
1501

1502
    mod skyrimse {
1503
        use super::*;
1504

1505
        #[test]
1506
        fn is_master_file_should_use_file_extension_and_flag() {
1✔
1507
            let tmp_dir = tempdir().unwrap();
1✔
1508

1✔
1509
            let master_flagged_esp = tmp_dir.path().join("Blank.esp");
1✔
1510
            copy(
1✔
1511
                Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1✔
1512
                &master_flagged_esp,
1✔
1513
            )
1✔
1514
            .unwrap();
1✔
1515

1✔
1516
            let plugin = Plugin::new(GameId::SkyrimSE, Path::new("Blank.esp"));
1✔
1517
            assert!(!plugin.is_master_file());
1✔
1518

1519
            let plugin = Plugin::new(GameId::SkyrimSE, Path::new("Blank.esm"));
1✔
1520
            assert!(plugin.is_master_file());
1✔
1521

1522
            let plugin = Plugin::new(GameId::SkyrimSE, Path::new("Blank.esl"));
1✔
1523
            assert!(plugin.is_master_file());
1✔
1524

1525
            let mut plugin = Plugin::new(
1✔
1526
                GameId::SkyrimSE,
1✔
1527
                Path::new("testing-plugins/Skyrim/Data/Blank.esp"),
1✔
1528
            );
1✔
1529
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
1530
            assert!(!plugin.is_master_file());
1✔
1531

1532
            let mut plugin = Plugin::new(GameId::SkyrimSE, &master_flagged_esp);
1✔
1533
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
1534
            assert!(plugin.is_master_file());
1✔
1535
        }
1✔
1536

1537
        #[test]
1538
        fn is_light_plugin_should_be_true_for_plugins_with_an_esl_file_extension() {
1✔
1539
            let plugin = Plugin::new(GameId::SkyrimSE, Path::new("Blank.esp"));
1✔
1540
            assert!(!plugin.is_light_plugin());
1✔
1541
            let plugin = Plugin::new(GameId::SkyrimSE, Path::new("Blank.esm"));
1✔
1542
            assert!(!plugin.is_light_plugin());
1✔
1543
            let plugin = Plugin::new(GameId::SkyrimSE, Path::new("Blank.esl"));
1✔
1544
            assert!(plugin.is_light_plugin());
1✔
1545
        }
1✔
1546

1547
        #[test]
1548
        fn is_light_plugin_should_be_true_for_a_ghosted_esl_file() {
1✔
1549
            let plugin = Plugin::new(GameId::SkyrimSE, Path::new("Blank.esl.ghost"));
1✔
1550
            assert!(plugin.is_light_plugin());
1✔
1551
        }
1✔
1552

1553
        #[test]
1554
        fn is_light_plugin_should_be_true_for_an_esp_file_with_the_light_flag_set() {
1✔
1555
            let tmp_dir = tempdir().unwrap();
1✔
1556

1✔
1557
            let light_flagged_esp = tmp_dir.path().join("Blank.esp");
1✔
1558
            copy(
1✔
1559
                Path::new("testing-plugins/SkyrimSE/Data/Blank.esl"),
1✔
1560
                &light_flagged_esp,
1✔
1561
            )
1✔
1562
            .unwrap();
1✔
1563

1✔
1564
            let mut plugin = Plugin::new(GameId::SkyrimSE, &light_flagged_esp);
1✔
1565
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
1566
            assert!(plugin.is_light_plugin());
1✔
1567
            assert!(!plugin.is_master_file());
1✔
1568
        }
1✔
1569

1570
        #[test]
1571
        fn is_light_plugin_should_be_true_for_an_esm_file_with_the_light_flag_set() {
1✔
1572
            let tmp_dir = tempdir().unwrap();
1✔
1573

1✔
1574
            let light_flagged_esm = tmp_dir.path().join("Blank.esm");
1✔
1575
            copy(
1✔
1576
                Path::new("testing-plugins/SkyrimSE/Data/Blank.esl"),
1✔
1577
                &light_flagged_esm,
1✔
1578
            )
1✔
1579
            .unwrap();
1✔
1580

1✔
1581
            let mut plugin = Plugin::new(GameId::SkyrimSE, &light_flagged_esm);
1✔
1582
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
1583
            assert!(plugin.is_light_plugin());
1✔
1584
            assert!(plugin.is_master_file());
1✔
1585
        }
1✔
1586

1587
        #[test]
1588
        fn is_medium_plugin_should_be_false() {
1✔
1589
            let plugin = Plugin::new(GameId::SkyrimSE, Path::new("Blank.esp"));
1✔
1590
            assert!(!plugin.is_medium_plugin());
1✔
1591
            let plugin = Plugin::new(GameId::SkyrimSE, Path::new("Blank.esm"));
1✔
1592
            assert!(!plugin.is_medium_plugin());
1✔
1593
            let plugin = Plugin::new(GameId::SkyrimSE, Path::new("Blank.esl"));
1✔
1594
            assert!(!plugin.is_medium_plugin());
1✔
1595
        }
1✔
1596

1597
        #[expect(clippy::float_cmp, reason = "float values should be exactly equal")]
1598
        #[test]
1599
        fn header_version_should_return_plugin_header_hedr_subrecord_field() {
1✔
1600
            let mut plugin = Plugin::new(
1✔
1601
                GameId::SkyrimSE,
1✔
1602
                Path::new("testing-plugins/SkyrimSE/Data/Blank.esm"),
1✔
1603
            );
1✔
1604

1✔
1605
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
1606

1607
            assert_eq!(0.94, plugin.header_version().unwrap());
1✔
1608
        }
1✔
1609

1610
        #[test]
1611
        fn valid_light_form_id_range_should_be_0x800_to_0xfff_if_hedr_version_is_less_than_1_71() {
1✔
1612
            let mut plugin = Plugin::new(
1✔
1613
                GameId::SkyrimSE,
1✔
1614
                Path::new("testing-plugins/SkyrimSE/Data/Blank - Master Dependent.esm"),
1✔
1615
            );
1✔
1616
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1617

1618
            let range = plugin.valid_light_form_id_range();
1✔
1619
            assert_eq!(&0x800, range.start());
1✔
1620
            assert_eq!(&0xFFF, range.end());
1✔
1621
        }
1✔
1622

1623
        #[test]
1624
        fn valid_light_form_id_range_should_be_0_to_0xfff_if_hedr_version_is_1_71_or_greater() {
1✔
1625
            let mut plugin = Plugin::new(
1✔
1626
                GameId::SkyrimSE,
1✔
1627
                Path::new("testing-plugins/SkyrimSE/Data/Blank - Master Dependent.esm"),
1✔
1628
            );
1✔
1629
            let mut bytes = read(plugin.path()).unwrap();
1✔
1630

1✔
1631
            assert_eq!(0xD7, bytes[0x1E]);
1✔
1632
            assert_eq!(0xA3, bytes[0x1F]);
1✔
1633
            assert_eq!(0x70, bytes[0x20]);
1✔
1634
            assert_eq!(0x3F, bytes[0x21]);
1✔
1635
            bytes[0x1E] = 0x48;
1✔
1636
            bytes[0x1F] = 0xE1;
1✔
1637
            bytes[0x20] = 0xDA;
1✔
1638
            bytes[0x21] = 0x3F;
1✔
1639

1✔
1640
            assert!(plugin
1✔
1641
                .parse_reader(Cursor::new(bytes), ParseOptions::whole_plugin())
1✔
1642
                .is_ok());
1✔
1643

1644
            let range = plugin.valid_light_form_id_range();
1✔
1645
            assert_eq!(&0, range.start());
1✔
1646
            assert_eq!(&0xFFF, range.end());
1✔
1647
        }
1✔
1648

1649
        #[test]
1650
        fn is_valid_as_light_plugin_should_be_true_if_the_plugin_has_no_form_ids_outside_the_valid_range(
1✔
1651
        ) {
1✔
1652
            let mut plugin = Plugin::new(
1✔
1653
                GameId::SkyrimSE,
1✔
1654
                Path::new("testing-plugins/SkyrimSE/Data/Blank - Master Dependent.esm"),
1✔
1655
            );
1✔
1656
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1657

1658
            assert!(plugin.is_valid_as_light_plugin().unwrap());
1✔
1659
        }
1✔
1660

1661
        #[test]
1662
        fn is_valid_as_light_plugin_should_be_true_if_the_plugin_has_an_override_form_id_outside_the_valid_range(
1✔
1663
        ) {
1✔
1664
            let mut plugin = Plugin::new(
1✔
1665
                GameId::SkyrimSE,
1✔
1666
                Path::new("testing-plugins/SkyrimSE/Data/Blank - Master Dependent.esm"),
1✔
1667
            );
1✔
1668
            let mut bytes = read(plugin.path()).unwrap();
1✔
1669

1✔
1670
            assert_eq!(0xF0, bytes[0x7A]);
1✔
1671
            assert_eq!(0x0C, bytes[0x7B]);
1✔
1672
            bytes[0x7A] = 0xFF;
1✔
1673
            bytes[0x7B] = 0x07;
1✔
1674

1✔
1675
            assert!(plugin
1✔
1676
                .parse_reader(Cursor::new(bytes), ParseOptions::whole_plugin())
1✔
1677
                .is_ok());
1✔
1678

1679
            assert!(plugin.is_valid_as_light_plugin().unwrap());
1✔
1680
        }
1✔
1681

1682
        #[test]
1683
        fn is_valid_as_light_plugin_should_be_false_if_the_plugin_has_a_new_form_id_greater_than_0xfff(
1✔
1684
        ) {
1✔
1685
            let mut plugin = Plugin::new(
1✔
1686
                GameId::SkyrimSE,
1✔
1687
                Path::new("testing-plugins/SkyrimSE/Data/Blank - Master Dependent.esm"),
1✔
1688
            );
1✔
1689
            let mut bytes = read(plugin.path()).unwrap();
1✔
1690

1✔
1691
            assert_eq!(0xEB, bytes[0x386]);
1✔
1692
            assert_eq!(0x0C, bytes[0x387]);
1✔
1693
            bytes[0x386] = 0x00;
1✔
1694
            bytes[0x387] = 0x10;
1✔
1695

1✔
1696
            assert!(plugin
1✔
1697
                .parse_reader(Cursor::new(bytes), ParseOptions::whole_plugin())
1✔
1698
                .is_ok());
1✔
1699

1700
            assert!(!plugin.is_valid_as_light_plugin().unwrap());
1✔
1701
        }
1✔
1702
    }
1703

1704
    mod fallout3 {
1705
        use super::super::*;
1706

1707
        #[test]
1708
        fn is_light_plugin_should_be_false() {
1✔
1709
            let plugin = Plugin::new(GameId::Fallout3, Path::new("Blank.esp"));
1✔
1710
            assert!(!plugin.is_light_plugin());
1✔
1711
            let plugin = Plugin::new(GameId::Fallout3, Path::new("Blank.esm"));
1✔
1712
            assert!(!plugin.is_light_plugin());
1✔
1713
            let plugin = Plugin::new(GameId::Fallout3, Path::new("Blank.esl"));
1✔
1714
            assert!(!plugin.is_light_plugin());
1✔
1715
        }
1✔
1716

1717
        #[test]
1718
        fn is_medium_plugin_should_be_false() {
1✔
1719
            let plugin = Plugin::new(GameId::Fallout3, Path::new("Blank.esp"));
1✔
1720
            assert!(!plugin.is_medium_plugin());
1✔
1721
            let plugin = Plugin::new(GameId::Fallout3, Path::new("Blank.esm"));
1✔
1722
            assert!(!plugin.is_medium_plugin());
1✔
1723
            let plugin = Plugin::new(GameId::Fallout3, Path::new("Blank.esl"));
1✔
1724
            assert!(!plugin.is_medium_plugin());
1✔
1725
        }
1✔
1726

1727
        #[test]
1728
        fn valid_light_form_id_range_should_be_empty() {
1✔
1729
            let mut plugin = Plugin::new(
1✔
1730
                GameId::Fallout3,
1✔
1731
                Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esm"),
1✔
1732
            );
1✔
1733
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1734

1735
            let range = plugin.valid_light_form_id_range();
1✔
1736
            assert_eq!(&0, range.start());
1✔
1737
            assert_eq!(&0, range.end());
1✔
1738
        }
1✔
1739

1740
        #[test]
1741
        fn is_valid_as_light_plugin_should_always_be_false() {
1✔
1742
            let mut plugin = Plugin::new(
1✔
1743
                GameId::Fallout3,
1✔
1744
                Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esm"),
1✔
1745
            );
1✔
1746
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1747
            assert!(!plugin.is_valid_as_light_plugin().unwrap());
1✔
1748
        }
1✔
1749
    }
1750

1751
    mod falloutnv {
1752
        use super::super::*;
1753

1754
        #[test]
1755
        fn is_light_plugin_should_be_false() {
1✔
1756
            let plugin = Plugin::new(GameId::FalloutNV, Path::new("Blank.esp"));
1✔
1757
            assert!(!plugin.is_light_plugin());
1✔
1758
            let plugin = Plugin::new(GameId::FalloutNV, Path::new("Blank.esm"));
1✔
1759
            assert!(!plugin.is_light_plugin());
1✔
1760
            let plugin = Plugin::new(GameId::FalloutNV, Path::new("Blank.esl"));
1✔
1761
            assert!(!plugin.is_light_plugin());
1✔
1762
        }
1✔
1763

1764
        #[test]
1765
        fn is_medium_plugin_should_be_false() {
1✔
1766
            let plugin = Plugin::new(GameId::FalloutNV, Path::new("Blank.esp"));
1✔
1767
            assert!(!plugin.is_medium_plugin());
1✔
1768
            let plugin = Plugin::new(GameId::FalloutNV, Path::new("Blank.esm"));
1✔
1769
            assert!(!plugin.is_medium_plugin());
1✔
1770
            let plugin = Plugin::new(GameId::FalloutNV, Path::new("Blank.esl"));
1✔
1771
            assert!(!plugin.is_medium_plugin());
1✔
1772
        }
1✔
1773

1774
        #[test]
1775
        fn valid_light_form_id_range_should_be_empty() {
1✔
1776
            let mut plugin = Plugin::new(
1✔
1777
                GameId::Fallout3,
1✔
1778
                Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esm"),
1✔
1779
            );
1✔
1780
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1781

1782
            let range = plugin.valid_light_form_id_range();
1✔
1783
            assert_eq!(&0, range.start());
1✔
1784
            assert_eq!(&0, range.end());
1✔
1785
        }
1✔
1786

1787
        #[test]
1788
        fn is_valid_as_light_plugin_should_always_be_false() {
1✔
1789
            let mut plugin = Plugin::new(
1✔
1790
                GameId::FalloutNV,
1✔
1791
                Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esm"),
1✔
1792
            );
1✔
1793
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1794
            assert!(!plugin.is_valid_as_light_plugin().unwrap());
1✔
1795
        }
1✔
1796
    }
1797

1798
    mod fallout4 {
1799
        use super::*;
1800

1801
        #[test]
1802
        fn is_master_file_should_use_file_extension_and_flag() {
1✔
1803
            let tmp_dir = tempdir().unwrap();
1✔
1804

1✔
1805
            let master_flagged_esp = tmp_dir.path().join("Blank.esp");
1✔
1806
            copy(
1✔
1807
                Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1✔
1808
                &master_flagged_esp,
1✔
1809
            )
1✔
1810
            .unwrap();
1✔
1811

1✔
1812
            let plugin = Plugin::new(GameId::Fallout4, Path::new("Blank.esp"));
1✔
1813
            assert!(!plugin.is_master_file());
1✔
1814

1815
            let plugin = Plugin::new(GameId::Fallout4, Path::new("Blank.esm"));
1✔
1816
            assert!(plugin.is_master_file());
1✔
1817

1818
            let plugin = Plugin::new(GameId::Fallout4, Path::new("Blank.esl"));
1✔
1819
            assert!(plugin.is_master_file());
1✔
1820

1821
            let mut plugin = Plugin::new(
1✔
1822
                GameId::Fallout4,
1✔
1823
                Path::new("testing-plugins/Skyrim/Data/Blank.esp"),
1✔
1824
            );
1✔
1825
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
1826
            assert!(!plugin.is_master_file());
1✔
1827

1828
            let mut plugin = Plugin::new(GameId::Fallout4, &master_flagged_esp);
1✔
1829
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
1830
            assert!(plugin.is_master_file());
1✔
1831
        }
1✔
1832

1833
        #[test]
1834
        fn is_light_plugin_should_be_true_for_plugins_with_an_esl_file_extension() {
1✔
1835
            let plugin = Plugin::new(GameId::Fallout4, Path::new("Blank.esp"));
1✔
1836
            assert!(!plugin.is_light_plugin());
1✔
1837
            let plugin = Plugin::new(GameId::Fallout4, Path::new("Blank.esm"));
1✔
1838
            assert!(!plugin.is_light_plugin());
1✔
1839
            let plugin = Plugin::new(GameId::Fallout4, Path::new("Blank.esl"));
1✔
1840
            assert!(plugin.is_light_plugin());
1✔
1841
        }
1✔
1842

1843
        #[test]
1844
        fn is_light_plugin_should_be_true_for_a_ghosted_esl_file() {
1✔
1845
            let plugin = Plugin::new(GameId::Fallout4, Path::new("Blank.esl.ghost"));
1✔
1846
            assert!(plugin.is_light_plugin());
1✔
1847
        }
1✔
1848

1849
        #[test]
1850
        fn is_medium_plugin_should_be_false() {
1✔
1851
            let plugin = Plugin::new(GameId::Fallout4, Path::new("Blank.esp"));
1✔
1852
            assert!(!plugin.is_medium_plugin());
1✔
1853
            let plugin = Plugin::new(GameId::Fallout4, Path::new("Blank.esm"));
1✔
1854
            assert!(!plugin.is_medium_plugin());
1✔
1855
            let plugin = Plugin::new(GameId::Fallout4, Path::new("Blank.esl"));
1✔
1856
            assert!(!plugin.is_medium_plugin());
1✔
1857
        }
1✔
1858

1859
        #[test]
1860
        fn valid_light_form_id_range_should_be_1_to_0xfff_if_hedr_version_is_less_than_1() {
1✔
1861
            let mut plugin = Plugin::new(
1✔
1862
                GameId::Fallout4,
1✔
1863
                Path::new("testing-plugins/SkyrimSE/Data/Blank - Master Dependent.esm"),
1✔
1864
            );
1✔
1865
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1866

1867
            let range = plugin.valid_light_form_id_range();
1✔
1868
            assert_eq!(&0x800, range.start());
1✔
1869
            assert_eq!(&0xFFF, range.end());
1✔
1870
        }
1✔
1871

1872
        #[test]
1873
        fn valid_light_form_id_range_should_be_1_to_0xfff_if_hedr_version_is_1_or_greater() {
1✔
1874
            let mut plugin = Plugin::new(
1✔
1875
                GameId::Fallout4,
1✔
1876
                Path::new("testing-plugins/SkyrimSE/Data/Blank - Master Dependent.esm"),
1✔
1877
            );
1✔
1878
            let mut bytes = read(plugin.path()).unwrap();
1✔
1879

1✔
1880
            assert_eq!(0xD7, bytes[0x1E]);
1✔
1881
            assert_eq!(0xA3, bytes[0x1F]);
1✔
1882
            assert_eq!(0x70, bytes[0x20]);
1✔
1883
            assert_eq!(0x3F, bytes[0x21]);
1✔
1884
            bytes[0x1E] = 0;
1✔
1885
            bytes[0x1F] = 0;
1✔
1886
            bytes[0x20] = 0x80;
1✔
1887
            bytes[0x21] = 0x3F;
1✔
1888

1✔
1889
            assert!(plugin
1✔
1890
                .parse_reader(Cursor::new(bytes), ParseOptions::whole_plugin())
1✔
1891
                .is_ok());
1✔
1892

1893
            let range = plugin.valid_light_form_id_range();
1✔
1894
            assert_eq!(&1, range.start());
1✔
1895
            assert_eq!(&0xFFF, range.end());
1✔
1896
        }
1✔
1897

1898
        #[test]
1899
        fn is_valid_as_light_plugin_should_be_true_if_the_plugin_has_no_form_ids_outside_the_valid_range(
1✔
1900
        ) {
1✔
1901
            let mut plugin = Plugin::new(
1✔
1902
                GameId::Fallout4,
1✔
1903
                Path::new("testing-plugins/SkyrimSE/Data/Blank - Master Dependent.esm"),
1✔
1904
            );
1✔
1905
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1906

1907
            assert!(plugin.is_valid_as_light_plugin().unwrap());
1✔
1908
        }
1✔
1909
    }
1910

1911
    mod starfield {
1912
        use super::*;
1913

1914
        #[test]
1915
        fn parse_file_should_succeed() {
1✔
1916
            let mut plugin = Plugin::new(
1✔
1917
                GameId::Starfield,
1✔
1918
                Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
1✔
1919
            );
1✔
1920

1✔
1921
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1922

1923
            match plugin.data.record_ids {
1✔
1924
                RecordIds::FormIds(ids) => assert_eq!(10, ids.len()),
1✔
1925
                _ => panic!("Expected raw FormIDs"),
×
1926
            }
1927
        }
1✔
1928

1929
        #[test]
1930
        fn resolve_record_ids_should_resolve_unresolved_form_ids() {
1✔
1931
            let mut plugin = Plugin::new(
1✔
1932
                GameId::Starfield,
1✔
1933
                Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
1✔
1934
            );
1✔
1935

1✔
1936
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1937

1938
            assert!(plugin.resolve_record_ids(&[]).is_ok());
1✔
1939

1940
            match plugin.data.record_ids {
1✔
1941
                RecordIds::Resolved(ids) => assert_eq!(10, ids.len()),
1✔
1942
                _ => panic!("Expected resolved FormIDs"),
×
1943
            }
1944
        }
1✔
1945

1946
        #[test]
1947
        fn resolve_record_ids_should_do_nothing_if_form_ids_are_already_resolved() {
1✔
1948
            let mut plugin = Plugin::new(
1✔
1949
                GameId::Starfield,
1✔
1950
                Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
1✔
1951
            );
1✔
1952

1✔
1953
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
1954

1955
            assert!(plugin.resolve_record_ids(&[]).is_ok());
1✔
1956

1957
            let vec_ptr = match &plugin.data.record_ids {
1✔
1958
                RecordIds::Resolved(ids) => ids.as_ptr(),
1✔
UNCOV
1959
                _ => panic!("Expected resolved FormIDs"),
×
1960
            };
1961

1962
            assert!(plugin.resolve_record_ids(&[]).is_ok());
1✔
1963

1964
            let vec_ptr_2 = match &plugin.data.record_ids {
1✔
1965
                RecordIds::Resolved(ids) => ids.as_ptr(),
1✔
UNCOV
1966
                _ => panic!("Expected resolved FormIDs"),
×
1967
            };
1968

1969
            assert_eq!(vec_ptr, vec_ptr_2);
1✔
1970
        }
1✔
1971

1972
        #[test]
1973
        fn scale_should_return_full_for_a_full_plugin() {
1✔
1974
            let mut plugin = Plugin::new(
1✔
1975
                GameId::Starfield,
1✔
1976
                Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
1✔
1977
            );
1✔
1978
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
1979

1980
            assert_eq!(PluginScale::Full, plugin.scale());
1✔
1981
        }
1✔
1982

1983
        #[test]
1984
        fn scale_should_return_medium_for_a_medium_plugin() {
1✔
1985
            let mut plugin = Plugin::new(
1✔
1986
                GameId::Starfield,
1✔
1987
                Path::new("testing-plugins/Starfield/Data/Blank.medium.esm"),
1✔
1988
            );
1✔
1989
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
1990

1991
            assert_eq!(PluginScale::Medium, plugin.scale());
1✔
1992
        }
1✔
1993

1994
        #[test]
1995
        fn scale_should_return_small_for_a_small_plugin() {
1✔
1996
            let mut plugin = Plugin::new(
1✔
1997
                GameId::Starfield,
1✔
1998
                Path::new("testing-plugins/Starfield/Data/Blank.small.esm"),
1✔
1999
            );
1✔
2000
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
2001

2002
            assert_eq!(PluginScale::Small, plugin.scale());
1✔
2003
        }
1✔
2004

2005
        #[test]
2006
        fn is_master_file_should_use_file_extension_and_flag() {
1✔
2007
            let tmp_dir = tempdir().unwrap();
1✔
2008

1✔
2009
            let master_flagged_esp = tmp_dir.path().join("Blank.esp");
1✔
2010
            copy(
1✔
2011
                Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
1✔
2012
                &master_flagged_esp,
1✔
2013
            )
1✔
2014
            .unwrap();
1✔
2015

1✔
2016
            let plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esp"));
1✔
2017
            assert!(!plugin.is_master_file());
1✔
2018

2019
            let plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esm"));
1✔
2020
            assert!(plugin.is_master_file());
1✔
2021

2022
            let plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esl"));
1✔
2023
            assert!(plugin.is_master_file());
1✔
2024

2025
            let mut plugin = Plugin::new(
1✔
2026
                GameId::Starfield,
1✔
2027
                Path::new("testing-plugins/Starfield/Data/Blank.esp"),
1✔
2028
            );
1✔
2029
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
2030
            assert!(!plugin.is_master_file());
1✔
2031

2032
            let mut plugin = Plugin::new(GameId::Starfield, &master_flagged_esp);
1✔
2033
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
2034
            assert!(plugin.is_master_file());
1✔
2035
        }
1✔
2036

2037
        #[test]
2038
        fn is_light_plugin_should_be_true_for_plugins_with_an_esl_file_extension_if_the_update_flag_is_not_set(
1✔
2039
        ) {
1✔
2040
            // The flag won't be set because no data is loaded.
1✔
2041
            let plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esp"));
1✔
2042
            assert!(!plugin.is_light_plugin());
1✔
2043
            let plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esm"));
1✔
2044
            assert!(!plugin.is_light_plugin());
1✔
2045
            let plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esl"));
1✔
2046
            assert!(plugin.is_light_plugin());
1✔
2047
        }
1✔
2048

2049
        #[test]
2050
        fn is_light_plugin_should_be_false_for_a_plugin_with_the_update_flag_set_and_an_esl_extension(
1✔
2051
        ) {
1✔
2052
            let tmp_dir = tempdir().unwrap();
1✔
2053
            let esl_path = tmp_dir.path().join("Blank.esl");
1✔
2054
            copy(
1✔
2055
                Path::new("testing-plugins/Starfield/Data/Blank - Override.esp"),
1✔
2056
                &esl_path,
1✔
2057
            )
1✔
2058
            .unwrap();
1✔
2059

1✔
2060
            let mut plugin = Plugin::new(GameId::Starfield, &esl_path);
1✔
2061
            plugin.parse_file(ParseOptions::header_only()).unwrap();
1✔
2062

1✔
2063
            assert!(!plugin.is_light_plugin());
1✔
2064
        }
1✔
2065

2066
        #[test]
2067
        fn is_light_plugin_should_be_true_for_a_ghosted_esl_file() {
1✔
2068
            let plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esl.ghost"));
1✔
2069
            assert!(plugin.is_light_plugin());
1✔
2070
        }
1✔
2071

2072
        #[test]
2073
        fn is_light_plugin_should_be_true_for_a_plugin_with_the_light_flag_set() {
1✔
2074
            let mut plugin = Plugin::new(
1✔
2075
                GameId::Starfield,
1✔
2076
                Path::new("testing-plugins/Starfield/Data/Blank.small.esm"),
1✔
2077
            );
1✔
2078
            plugin.parse_file(ParseOptions::header_only()).unwrap();
1✔
2079

1✔
2080
            assert!(plugin.is_light_plugin());
1✔
2081
        }
1✔
2082

2083
        #[test]
2084
        fn is_medium_plugin_should_be_false_for_a_plugin_without_the_medium_flag_set() {
1✔
2085
            let plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esp"));
1✔
2086
            assert!(!plugin.is_medium_plugin());
1✔
2087
            let plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esm"));
1✔
2088
            assert!(!plugin.is_medium_plugin());
1✔
2089
            let plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esl"));
1✔
2090
            assert!(!plugin.is_medium_plugin());
1✔
2091
        }
1✔
2092

2093
        #[test]
2094
        fn is_medium_plugin_should_be_true_for_a_plugin_with_the_medium_flag_set() {
1✔
2095
            let mut plugin = Plugin::new(
1✔
2096
                GameId::Starfield,
1✔
2097
                Path::new("testing-plugins/Starfield/Data/Blank.medium.esm"),
1✔
2098
            );
1✔
2099
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
2100
            assert!(plugin.is_medium_plugin());
1✔
2101
        }
1✔
2102

2103
        #[test]
2104
        fn is_medium_plugin_should_be_false_for_a_plugin_with_the_medium_and_light_flags_set() {
1✔
2105
            let mut plugin = Plugin::new(
1✔
2106
                GameId::Starfield,
1✔
2107
                Path::new("testing-plugins/Starfield/Data/Blank.small.esm"),
1✔
2108
            );
1✔
2109
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
2110
            assert!(!plugin.is_medium_plugin());
1✔
2111
        }
1✔
2112

2113
        #[test]
2114
        fn is_medium_plugin_should_be_false_for_an_esl_plugin_with_the_medium_flag_set() {
1✔
2115
            let tmp_dir = tempdir().unwrap();
1✔
2116
            let path = tmp_dir.path().join("Blank.esl");
1✔
2117
            copy(
1✔
2118
                Path::new("testing-plugins/Starfield/Data/Blank.medium.esm"),
1✔
2119
                &path,
1✔
2120
            )
1✔
2121
            .unwrap();
1✔
2122

1✔
2123
            let mut plugin = Plugin::new(GameId::Starfield, &path);
1✔
2124
            assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
2125
            assert!(!plugin.is_medium_plugin());
1✔
2126
        }
1✔
2127

2128
        #[test]
2129
        fn is_update_plugin_should_be_true_for_a_plugin_with_the_update_flag_set_and_at_least_one_master_and_no_light_flag(
1✔
2130
        ) {
1✔
2131
            let mut plugin = Plugin::new(
1✔
2132
                GameId::Starfield,
1✔
2133
                Path::new("testing-plugins/Starfield/Data/Blank - Override.full.esm"),
1✔
2134
            );
1✔
2135
            plugin.parse_file(ParseOptions::header_only()).unwrap();
1✔
2136

1✔
2137
            assert!(plugin.is_update_plugin());
1✔
2138
        }
1✔
2139

2140
        #[test]
2141
        fn is_update_plugin_should_be_false_for_a_plugin_with_the_update_flag_set_and_no_masters() {
1✔
2142
            let mut plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esm"));
1✔
2143
            let file_data = &[
1✔
2144
                0x54, 0x45, 0x53, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
1✔
2145
                0x00, 0x00, 0xB2, 0x2E, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00,
1✔
2146
            ];
1✔
2147
            plugin.data.header_record =
1✔
2148
                Record::read(&mut Cursor::new(file_data), GameId::Starfield, b"TES4").unwrap();
1✔
2149

1✔
2150
            assert!(!plugin.is_update_plugin());
1✔
2151
        }
1✔
2152

2153
        #[test]
2154
        fn is_update_plugin_should_be_false_for_a_plugin_with_the_update_and_light_flags_set() {
1✔
2155
            let mut plugin = Plugin::new(
1✔
2156
                GameId::Starfield,
1✔
2157
                Path::new("testing-plugins/Starfield/Data/Blank - Override.small.esm"),
1✔
2158
            );
1✔
2159
            plugin.parse_file(ParseOptions::header_only()).unwrap();
1✔
2160

1✔
2161
            assert!(!plugin.is_update_plugin());
1✔
2162
        }
1✔
2163

2164
        #[test]
2165
        fn is_update_plugin_should_be_false_for_a_plugin_with_the_update_and_medium_flags_set() {
1✔
2166
            let mut plugin = Plugin::new(
1✔
2167
                GameId::Starfield,
1✔
2168
                Path::new("testing-plugins/Starfield/Data/Blank - Override.medium.esm"),
1✔
2169
            );
1✔
2170
            plugin.parse_file(ParseOptions::header_only()).unwrap();
1✔
2171

1✔
2172
            assert!(!plugin.is_update_plugin());
1✔
2173
        }
1✔
2174

2175
        #[test]
2176
        fn is_blueprint_plugin_should_be_false_for_a_plugin_without_the_blueprint_flag_set() {
1✔
2177
            let mut plugin = Plugin::new(
1✔
2178
                GameId::Starfield,
1✔
2179
                Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
1✔
2180
            );
1✔
2181
            plugin.parse_file(ParseOptions::header_only()).unwrap();
1✔
2182

1✔
2183
            assert!(!plugin.is_blueprint_plugin());
1✔
2184
        }
1✔
2185

2186
        #[test]
2187
        fn is_blueprint_plugin_should_be_false_for_a_plugin_with_the_blueprint_flag_set() {
1✔
2188
            let mut plugin = Plugin::new(
1✔
2189
                GameId::Starfield,
1✔
2190
                Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
1✔
2191
            );
1✔
2192

1✔
2193
            let mut bytes = read(plugin.path()).unwrap();
1✔
2194

1✔
2195
            assert_eq!(0, bytes[0x09]);
1✔
2196
            bytes[0x09] = 8;
1✔
2197

1✔
2198
            assert!(plugin
1✔
2199
                .parse_reader(Cursor::new(bytes), ParseOptions::header_only())
1✔
2200
                .is_ok());
1✔
2201

2202
            assert!(plugin.is_blueprint_plugin());
1✔
2203
        }
1✔
2204

2205
        #[test]
2206
        fn count_override_records_should_error_if_form_ids_are_unresolved() {
1✔
2207
            let mut plugin = Plugin::new(
1✔
2208
                GameId::Starfield,
1✔
2209
                Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
1✔
2210
            );
1✔
2211

1✔
2212
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
2213

2214
            match plugin.count_override_records().unwrap_err() {
1✔
2215
                Error::UnresolvedRecordIds(path) => assert_eq!(plugin.path, path),
1✔
2216
                _ => panic!("Expected unresolved FormIDs error"),
×
2217
            }
2218
        }
1✔
2219

2220
        #[test]
2221
        fn count_override_records_should_succeed_if_form_ids_are_resolved() {
1✔
2222
            let mut plugin = Plugin::new(
1✔
2223
                GameId::Starfield,
1✔
2224
                Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
1✔
2225
            );
1✔
2226

1✔
2227
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
2228

2229
            assert!(plugin.resolve_record_ids(&[]).is_ok());
1✔
2230

2231
            assert_eq!(0, plugin.count_override_records().unwrap());
1✔
2232
        }
1✔
2233

2234
        #[test]
2235
        fn overlaps_with_should_error_if_form_ids_in_self_are_unresolved() {
1✔
2236
            let mut plugin1 = Plugin::new(
1✔
2237
                GameId::Starfield,
1✔
2238
                Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
1✔
2239
            );
1✔
2240
            let mut plugin2 = Plugin::new(
1✔
2241
                GameId::Starfield,
1✔
2242
                Path::new("testing-plugins/Starfield/Data/Blank - Override.esp"),
1✔
2243
            );
1✔
2244
            let plugin1_metadata = PluginMetadata {
1✔
2245
                filename: plugin1.filename().unwrap(),
1✔
2246
                scale: plugin1.scale(),
1✔
2247
                record_ids: Box::new([]),
1✔
2248
            };
1✔
2249

1✔
2250
            assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
2251
            assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
2252
            assert!(plugin2.resolve_record_ids(&[plugin1_metadata]).is_ok());
1✔
2253

2254
            match plugin1.overlaps_with(&plugin2).unwrap_err() {
1✔
2255
                Error::UnresolvedRecordIds(path) => assert_eq!(plugin1.path, path),
1✔
2256
                _ => panic!("Expected unresolved FormIDs error"),
×
2257
            }
2258
        }
1✔
2259

2260
        #[test]
2261
        fn overlaps_with_should_error_if_form_ids_in_other_are_unresolved() {
1✔
2262
            let mut plugin1 = Plugin::new(
1✔
2263
                GameId::Starfield,
1✔
2264
                Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
1✔
2265
            );
1✔
2266
            let mut plugin2 = Plugin::new(
1✔
2267
                GameId::Starfield,
1✔
2268
                Path::new("testing-plugins/Starfield/Data/Blank - Override.esp"),
1✔
2269
            );
1✔
2270

1✔
2271
            assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
2272
            assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
2273
            assert!(plugin1.resolve_record_ids(&[]).is_ok());
1✔
2274

2275
            match plugin1.overlaps_with(&plugin2).unwrap_err() {
1✔
2276
                Error::UnresolvedRecordIds(path) => assert_eq!(plugin2.path, path),
1✔
2277
                _ => panic!("Expected unresolved FormIDs error"),
×
2278
            }
2279
        }
1✔
2280

2281
        #[test]
2282
        fn overlaps_with_should_succeed_if_form_ids_are_resolved() {
1✔
2283
            let mut plugin1 = Plugin::new(
1✔
2284
                GameId::Starfield,
1✔
2285
                Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
1✔
2286
            );
1✔
2287
            let mut plugin2 = Plugin::new(
1✔
2288
                GameId::Starfield,
1✔
2289
                Path::new("testing-plugins/Starfield/Data/Blank - Override.esp"),
1✔
2290
            );
1✔
2291
            let plugin1_metadata = PluginMetadata {
1✔
2292
                filename: plugin1.filename().unwrap(),
1✔
2293
                scale: plugin1.scale(),
1✔
2294
                record_ids: Box::new([]),
1✔
2295
            };
1✔
2296

1✔
2297
            assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
2298
            assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
2299
            assert!(plugin1.resolve_record_ids(&[]).is_ok());
1✔
2300
            assert!(plugin2.resolve_record_ids(&[plugin1_metadata]).is_ok());
1✔
2301

2302
            assert!(plugin1.overlaps_with(&plugin2).unwrap());
1✔
2303
        }
1✔
2304

2305
        #[test]
2306
        fn overlap_size_should_error_if_form_ids_in_self_are_unresolved() {
1✔
2307
            let mut plugin1 = Plugin::new(
1✔
2308
                GameId::Starfield,
1✔
2309
                Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
1✔
2310
            );
1✔
2311
            let mut plugin2 = Plugin::new(
1✔
2312
                GameId::Starfield,
1✔
2313
                Path::new("testing-plugins/Starfield/Data/Blank - Override.esp"),
1✔
2314
            );
1✔
2315
            let plugin1_metadata = PluginMetadata {
1✔
2316
                filename: plugin1.filename().unwrap(),
1✔
2317
                scale: plugin1.scale(),
1✔
2318
                record_ids: Box::new([]),
1✔
2319
            };
1✔
2320

1✔
2321
            assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
2322
            assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
2323
            assert!(plugin2.resolve_record_ids(&[plugin1_metadata]).is_ok());
1✔
2324

2325
            match plugin1.overlap_size(&[&plugin2]).unwrap_err() {
1✔
2326
                Error::UnresolvedRecordIds(path) => assert_eq!(plugin1.path, path),
1✔
2327
                _ => panic!("Expected unresolved FormIDs error"),
×
2328
            }
2329
        }
1✔
2330

2331
        #[test]
2332
        fn overlap_size_should_error_if_form_ids_in_other_are_unresolved() {
1✔
2333
            let mut plugin1 = Plugin::new(
1✔
2334
                GameId::Starfield,
1✔
2335
                Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
1✔
2336
            );
1✔
2337
            let mut plugin2 = Plugin::new(
1✔
2338
                GameId::Starfield,
1✔
2339
                Path::new("testing-plugins/Starfield/Data/Blank - Override.esp"),
1✔
2340
            );
1✔
2341

1✔
2342
            assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
2343
            assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
2344
            assert!(plugin1.resolve_record_ids(&[]).is_ok());
1✔
2345

2346
            match plugin1.overlap_size(&[&plugin2]).unwrap_err() {
1✔
2347
                Error::UnresolvedRecordIds(path) => assert_eq!(plugin2.path, path),
1✔
2348
                _ => panic!("Expected unresolved FormIDs error"),
×
2349
            }
2350
        }
1✔
2351

2352
        #[test]
2353
        fn overlap_size_should_succeed_if_form_ids_are_resolved() {
1✔
2354
            let mut plugin1 = Plugin::new(
1✔
2355
                GameId::Starfield,
1✔
2356
                Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
1✔
2357
            );
1✔
2358
            let mut plugin2 = Plugin::new(
1✔
2359
                GameId::Starfield,
1✔
2360
                Path::new("testing-plugins/Starfield/Data/Blank - Override.esp"),
1✔
2361
            );
1✔
2362
            let plugin1_metadata = PluginMetadata {
1✔
2363
                filename: plugin1.filename().unwrap(),
1✔
2364
                scale: plugin1.scale(),
1✔
2365
                record_ids: Box::new([]),
1✔
2366
            };
1✔
2367

1✔
2368
            assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
2369
            assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
2370
            assert!(plugin1.resolve_record_ids(&[]).is_ok());
1✔
2371
            assert!(plugin2.resolve_record_ids(&[plugin1_metadata]).is_ok());
1✔
2372

2373
            assert_eq!(1, plugin1.overlap_size(&[&plugin2]).unwrap());
1✔
2374
        }
1✔
2375

2376
        #[test]
2377
        fn valid_light_form_id_range_should_be_0_to_0xfff() {
1✔
2378
            let plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esp"));
1✔
2379

1✔
2380
            let range = plugin.valid_light_form_id_range();
1✔
2381
            assert_eq!(&0, range.start());
1✔
2382
            assert_eq!(&0xFFF, range.end());
1✔
2383
        }
1✔
2384

2385
        #[test]
2386
        fn valid_medium_form_id_range_should_be_0_to_0xffff() {
1✔
2387
            let mut plugin = Plugin::new(
1✔
2388
                GameId::Starfield,
1✔
2389
                Path::new("testing-plugins/Starfield/Data/Blank.small.esm"),
1✔
2390
            );
1✔
2391
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
2392

2393
            let range = plugin.valid_medium_form_id_range();
1✔
2394
            assert_eq!(&0, range.start());
1✔
2395
            assert_eq!(&0xFFFF, range.end());
1✔
2396
        }
1✔
2397

2398
        #[test]
2399
        fn is_valid_as_light_plugin_should_be_false_if_form_ids_are_unresolved() {
1✔
2400
            let mut plugin = Plugin::new(
1✔
2401
                GameId::Starfield,
1✔
2402
                Path::new("testing-plugins/Starfield/Data/Blank.small.esm"),
1✔
2403
            );
1✔
2404
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
2405

2406
            match plugin.is_valid_as_light_plugin().unwrap_err() {
1✔
2407
                Error::UnresolvedRecordIds(path) => assert_eq!(plugin.path, path),
1✔
2408
                _ => panic!("Expected unresolved FormIDs error"),
×
2409
            }
2410
        }
1✔
2411

2412
        #[test]
2413
        fn is_valid_as_light_plugin_should_be_true_if_the_plugin_has_no_form_ids_outside_the_valid_range(
1✔
2414
        ) {
1✔
2415
            let mut plugin = Plugin::new(
1✔
2416
                GameId::Starfield,
1✔
2417
                Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
1✔
2418
            );
1✔
2419
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
2420
            assert!(plugin.resolve_record_ids(&[]).is_ok());
1✔
2421

2422
            assert!(plugin.is_valid_as_light_plugin().unwrap());
1✔
2423
        }
1✔
2424

2425
        #[test]
2426
        fn is_valid_as_medium_plugin_should_be_false_if_form_ids_are_unresolved() {
1✔
2427
            let mut plugin = Plugin::new(
1✔
2428
                GameId::Starfield,
1✔
2429
                Path::new("testing-plugins/Starfield/Data/Blank.medium.esm"),
1✔
2430
            );
1✔
2431
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
2432

2433
            match plugin.is_valid_as_medium_plugin().unwrap_err() {
1✔
2434
                Error::UnresolvedRecordIds(path) => assert_eq!(plugin.path, path),
1✔
2435
                _ => panic!("Expected unresolved FormIDs error"),
×
2436
            }
2437
        }
1✔
2438

2439
        #[test]
2440
        fn is_valid_as_medium_plugin_should_be_true_if_the_plugin_has_no_form_ids_outside_the_valid_range(
1✔
2441
        ) {
1✔
2442
            let mut plugin = Plugin::new(
1✔
2443
                GameId::Starfield,
1✔
2444
                Path::new("testing-plugins/Starfield/Data/Blank.medium.esm"),
1✔
2445
            );
1✔
2446
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
2447
            assert!(plugin.resolve_record_ids(&[]).is_ok());
1✔
2448

2449
            assert!(plugin.is_valid_as_medium_plugin().unwrap());
1✔
2450
        }
1✔
2451

2452
        #[test]
2453
        fn is_valid_as_update_plugin_should_be_false_if_form_ids_are_unresolved() {
1✔
2454
            let mut plugin = Plugin::new(
1✔
2455
                GameId::Starfield,
1✔
2456
                Path::new("testing-plugins/Starfield/Data/Blank - Override.esp"),
1✔
2457
            );
1✔
2458
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
2459

2460
            match plugin.is_valid_as_update_plugin().unwrap_err() {
1✔
2461
                Error::UnresolvedRecordIds(path) => assert_eq!(plugin.path, path),
1✔
2462
                _ => panic!("Expected unresolved FormIDs error"),
×
2463
            }
2464
        }
1✔
2465

2466
        #[test]
2467
        fn is_valid_as_update_plugin_should_be_true_if_the_plugin_has_no_new_records() {
1✔
2468
            let master_metadata = PluginMetadata {
1✔
2469
                filename: "Blank.full.esm".to_owned(),
1✔
2470
                scale: PluginScale::Full,
1✔
2471
                record_ids: Box::new([]),
1✔
2472
            };
1✔
2473
            let mut plugin = Plugin::new(
1✔
2474
                GameId::Starfield,
1✔
2475
                Path::new("testing-plugins/Starfield/Data/Blank - Override.esp"),
1✔
2476
            );
1✔
2477
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
2478
            assert!(plugin.resolve_record_ids(&[master_metadata]).is_ok());
1✔
2479

2480
            assert!(plugin.is_valid_as_update_plugin().unwrap());
1✔
2481
        }
1✔
2482

2483
        #[test]
2484
        fn is_valid_as_update_plugin_should_be_false_if_the_plugin_has_new_records() {
1✔
2485
            let master_metadata = PluginMetadata {
1✔
2486
                filename: "Blank.full.esm".to_owned(),
1✔
2487
                scale: PluginScale::Full,
1✔
2488
                record_ids: Box::new([]),
1✔
2489
            };
1✔
2490
            let mut plugin = Plugin::new(
1✔
2491
                GameId::Starfield,
1✔
2492
                Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
1✔
2493
            );
1✔
2494
            assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1✔
2495
            assert!(plugin.resolve_record_ids(&[master_metadata]).is_ok());
1✔
2496

2497
            assert!(!plugin.is_valid_as_update_plugin().unwrap());
1✔
2498
        }
1✔
2499

2500
        #[test]
2501
        fn plugins_metadata_should_return_plugin_names_and_scales() {
1✔
2502
            let mut plugin1 = Plugin::new(
1✔
2503
                GameId::Starfield,
1✔
2504
                Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
1✔
2505
            );
1✔
2506
            let mut plugin2 = Plugin::new(
1✔
2507
                GameId::Starfield,
1✔
2508
                Path::new("testing-plugins/Starfield/Data/Blank.medium.esm"),
1✔
2509
            );
1✔
2510
            let mut plugin3 = Plugin::new(
1✔
2511
                GameId::Starfield,
1✔
2512
                Path::new("testing-plugins/Starfield/Data/Blank.small.esm"),
1✔
2513
            );
1✔
2514
            assert!(plugin1.parse_file(ParseOptions::header_only()).is_ok());
1✔
2515
            assert!(plugin2.parse_file(ParseOptions::header_only()).is_ok());
1✔
2516
            assert!(plugin3.parse_file(ParseOptions::header_only()).is_ok());
1✔
2517

2518
            let metadata = plugins_metadata(&[&plugin1, &plugin2, &plugin3]).unwrap();
1✔
2519

1✔
2520
            assert_eq!(
1✔
2521
                vec![
1✔
2522
                    PluginMetadata {
1✔
2523
                        filename: "Blank.full.esm".to_owned(),
1✔
2524
                        scale: PluginScale::Full,
1✔
2525
                        record_ids: Box::new([]),
1✔
2526
                    },
1✔
2527
                    PluginMetadata {
1✔
2528
                        filename: "Blank.medium.esm".to_owned(),
1✔
2529
                        scale: PluginScale::Medium,
1✔
2530
                        record_ids: Box::new([]),
1✔
2531
                    },
1✔
2532
                    PluginMetadata {
1✔
2533
                        filename: "Blank.small.esm".to_owned(),
1✔
2534
                        scale: PluginScale::Small,
1✔
2535
                        record_ids: Box::new([]),
1✔
2536
                    },
1✔
2537
                ],
1✔
2538
                metadata
1✔
2539
            );
1✔
2540
        }
1✔
2541

2542
        #[test]
2543
        fn hashed_parent_should_use_full_object_index_mask_for_games_other_than_starfield() {
1✔
2544
            let metadata = PluginMetadata {
1✔
2545
                filename: "a".to_owned(),
1✔
2546
                scale: PluginScale::Full,
1✔
2547
                record_ids: Box::new([]),
1✔
2548
            };
1✔
2549

1✔
2550
            let plugin = hashed_parent(GameId::SkyrimSE, &metadata);
1✔
2551

1✔
2552
            assert_eq!(u32::from(ObjectIndexMask::Full), plugin.mod_index_mask);
1✔
2553
            assert_eq!(u32::from(ObjectIndexMask::Full), plugin.object_index_mask);
1✔
2554

2555
            let metadata = PluginMetadata {
1✔
2556
                filename: "a".to_owned(),
1✔
2557
                scale: PluginScale::Medium,
1✔
2558
                record_ids: Box::new([]),
1✔
2559
            };
1✔
2560

1✔
2561
            let plugin = hashed_parent(GameId::SkyrimSE, &metadata);
1✔
2562

1✔
2563
            assert_eq!(u32::from(ObjectIndexMask::Full), plugin.mod_index_mask);
1✔
2564
            assert_eq!(u32::from(ObjectIndexMask::Full), plugin.object_index_mask);
1✔
2565

2566
            let metadata = PluginMetadata {
1✔
2567
                filename: "a".to_owned(),
1✔
2568
                scale: PluginScale::Small,
1✔
2569
                record_ids: Box::new([]),
1✔
2570
            };
1✔
2571

1✔
2572
            let plugin = hashed_parent(GameId::SkyrimSE, &metadata);
1✔
2573

1✔
2574
            assert_eq!(u32::from(ObjectIndexMask::Full), plugin.mod_index_mask);
1✔
2575
            assert_eq!(u32::from(ObjectIndexMask::Full), plugin.object_index_mask);
1✔
2576
        }
1✔
2577

2578
        #[test]
2579
        fn hashed_parent_should_use_object_index_mask_matching_the_plugin_scale_for_starfield() {
1✔
2580
            let metadata = PluginMetadata {
1✔
2581
                filename: "a".to_owned(),
1✔
2582
                scale: PluginScale::Full,
1✔
2583
                record_ids: Box::new([]),
1✔
2584
            };
1✔
2585

1✔
2586
            let plugin = hashed_parent(GameId::Starfield, &metadata);
1✔
2587

1✔
2588
            assert_eq!(u32::from(ObjectIndexMask::Full), plugin.mod_index_mask);
1✔
2589
            assert_eq!(u32::from(ObjectIndexMask::Full), plugin.object_index_mask);
1✔
2590

2591
            let metadata = PluginMetadata {
1✔
2592
                filename: "a".to_owned(),
1✔
2593
                scale: PluginScale::Medium,
1✔
2594
                record_ids: Box::new([]),
1✔
2595
            };
1✔
2596

1✔
2597
            let plugin = hashed_parent(GameId::Starfield, &metadata);
1✔
2598

1✔
2599
            assert_eq!(u32::from(ObjectIndexMask::Medium), plugin.mod_index_mask);
1✔
2600
            assert_eq!(u32::from(ObjectIndexMask::Medium), plugin.object_index_mask);
1✔
2601

2602
            let metadata = PluginMetadata {
1✔
2603
                filename: "a".to_owned(),
1✔
2604
                scale: PluginScale::Small,
1✔
2605
                record_ids: Box::new([]),
1✔
2606
            };
1✔
2607

1✔
2608
            let plugin = hashed_parent(GameId::Starfield, &metadata);
1✔
2609

1✔
2610
            assert_eq!(u32::from(ObjectIndexMask::Small), plugin.mod_index_mask);
1✔
2611
            assert_eq!(u32::from(ObjectIndexMask::Small), plugin.object_index_mask);
1✔
2612
        }
1✔
2613

2614
        #[test]
2615
        fn hashed_masters_should_use_vec_index_as_mod_index() {
1✔
2616
            let masters = &["a".to_owned(), "b".to_owned(), "c".to_owned()];
1✔
2617
            let hashed_masters = hashed_masters(masters);
1✔
2618

1✔
2619
            assert_eq!(
1✔
2620
                vec![
1✔
2621
                    SourcePlugin::master("a", 0, ObjectIndexMask::Full),
1✔
2622
                    SourcePlugin::master("b", 0x0100_0000, ObjectIndexMask::Full),
1✔
2623
                    SourcePlugin::master("c", 0x0200_0000, ObjectIndexMask::Full),
1✔
2624
                ],
1✔
2625
                hashed_masters
1✔
2626
            );
1✔
2627
        }
1✔
2628

2629
        #[test]
2630
        fn hashed_masters_for_starfield_should_error_if_it_cannot_find_a_masters_metadata() {
1✔
2631
            let masters = &["a".to_owned(), "b".to_owned(), "c".to_owned()];
1✔
2632
            let metadata = &[
1✔
2633
                PluginMetadata {
1✔
2634
                    filename: masters[0].clone(),
1✔
2635
                    scale: PluginScale::Full,
1✔
2636
                    record_ids: Box::new([]),
1✔
2637
                },
1✔
2638
                PluginMetadata {
1✔
2639
                    filename: masters[1].clone(),
1✔
2640
                    scale: PluginScale::Full,
1✔
2641
                    record_ids: Box::new([]),
1✔
2642
                },
1✔
2643
            ];
1✔
2644

1✔
2645
            match hashed_masters_for_starfield(masters, metadata).unwrap_err() {
1✔
2646
                Error::PluginMetadataNotFound(master) => assert_eq!(masters[2], master),
1✔
2647
                _ => panic!("Expected plugin metadata not found error"),
×
2648
            }
2649
        }
1✔
2650

2651
        #[test]
2652
        fn hashed_masters_for_starfield_should_match_names_to_metadata_case_insensitively() {
1✔
2653
            let masters = &["a".to_owned()];
1✔
2654
            let metadata = &[PluginMetadata {
1✔
2655
                filename: "A".to_owned(),
1✔
2656
                scale: PluginScale::Full,
1✔
2657
                record_ids: Box::new([]),
1✔
2658
            }];
1✔
2659

1✔
2660
            let hashed_masters = hashed_masters_for_starfield(masters, metadata).unwrap();
1✔
2661

1✔
2662
            assert_eq!(
1✔
2663
                vec![SourcePlugin::master(&masters[0], 0, ObjectIndexMask::Full),],
1✔
2664
                hashed_masters
1✔
2665
            );
1✔
2666
        }
1✔
2667

2668
        #[test]
2669
        fn hashed_masters_for_starfield_should_count_mod_indexes_separately_for_different_plugin_scales(
1✔
2670
        ) {
1✔
2671
            let masters: Vec<_> = (0u8..7u8).map(|i| i.to_string()).collect();
7✔
2672
            let metadata = &[
1✔
2673
                PluginMetadata {
1✔
2674
                    filename: masters[0].clone(),
1✔
2675
                    scale: PluginScale::Full,
1✔
2676
                    record_ids: Box::new([]),
1✔
2677
                },
1✔
2678
                PluginMetadata {
1✔
2679
                    filename: masters[1].clone(),
1✔
2680
                    scale: PluginScale::Medium,
1✔
2681
                    record_ids: Box::new([]),
1✔
2682
                },
1✔
2683
                PluginMetadata {
1✔
2684
                    filename: masters[2].clone(),
1✔
2685
                    scale: PluginScale::Small,
1✔
2686
                    record_ids: Box::new([]),
1✔
2687
                },
1✔
2688
                PluginMetadata {
1✔
2689
                    filename: masters[3].clone(),
1✔
2690
                    scale: PluginScale::Medium,
1✔
2691
                    record_ids: Box::new([]),
1✔
2692
                },
1✔
2693
                PluginMetadata {
1✔
2694
                    filename: masters[4].clone(),
1✔
2695
                    scale: PluginScale::Full,
1✔
2696
                    record_ids: Box::new([]),
1✔
2697
                },
1✔
2698
                PluginMetadata {
1✔
2699
                    filename: masters[5].clone(),
1✔
2700
                    scale: PluginScale::Small,
1✔
2701
                    record_ids: Box::new([]),
1✔
2702
                },
1✔
2703
                PluginMetadata {
1✔
2704
                    filename: masters[6].clone(),
1✔
2705
                    scale: PluginScale::Small,
1✔
2706
                    record_ids: Box::new([]),
1✔
2707
                },
1✔
2708
            ];
1✔
2709

1✔
2710
            let hashed_masters = hashed_masters_for_starfield(&masters, metadata).unwrap();
1✔
2711

1✔
2712
            assert_eq!(
1✔
2713
                vec![
1✔
2714
                    SourcePlugin::master(&masters[0], 0, ObjectIndexMask::Full),
1✔
2715
                    SourcePlugin::master(&masters[1], 0xFD00_0000, ObjectIndexMask::Medium),
1✔
2716
                    SourcePlugin::master(&masters[2], 0xFE00_0000, ObjectIndexMask::Small),
1✔
2717
                    SourcePlugin::master(&masters[3], 0xFD01_0000, ObjectIndexMask::Medium),
1✔
2718
                    SourcePlugin::master(&masters[4], 0x0100_0000, ObjectIndexMask::Full),
1✔
2719
                    SourcePlugin::master(&masters[5], 0xFE00_1000, ObjectIndexMask::Small),
1✔
2720
                    SourcePlugin::master(&masters[6], 0xFE00_2000, ObjectIndexMask::Small),
1✔
2721
                ],
1✔
2722
                hashed_masters
1✔
2723
            );
1✔
2724
        }
1✔
2725
    }
2726

2727
    fn write_invalid_plugin(path: &Path) {
2✔
2728
        use std::io::Write;
2729
        let mut file = File::create(path).unwrap();
2✔
2730
        let bytes = [0; MAX_RECORD_HEADER_LENGTH];
2✔
2731
        file.write_all(&bytes).unwrap();
2✔
2732
    }
2✔
2733

2734
    #[test]
2735
    fn parse_file_should_error_if_plugin_does_not_exist() {
1✔
2736
        let mut plugin = Plugin::new(GameId::Skyrim, Path::new("Blank.esm"));
1✔
2737

1✔
2738
        assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_err());
1✔
2739
    }
1✔
2740

2741
    #[test]
2742
    fn parse_file_should_error_if_plugin_is_not_valid() {
1✔
2743
        let mut plugin = Plugin::new(
1✔
2744
            GameId::Oblivion,
1✔
2745
            Path::new("testing-plugins/Oblivion/Data/Blank.bsa"),
1✔
2746
        );
1✔
2747

1✔
2748
        let result = plugin.parse_file(ParseOptions::whole_plugin());
1✔
2749
        assert!(result.is_err());
1✔
2750
        assert_eq!(
1✔
2751
             "An error was encountered while parsing the plugin content \"BSA\\x00g\\x00\\x00\\x00$\\x00\\x00\\x00\\x07\\x07\\x00\\x00\": Expected record type \"TES4\"",
1✔
2752
             result.unwrap_err().to_string()
1✔
2753
         );
1✔
2754
    }
1✔
2755

2756
    #[test]
2757
    fn parse_file_header_only_should_fail_for_a_non_plugin_file() {
1✔
2758
        let tmp_dir = tempdir().unwrap();
1✔
2759

1✔
2760
        let path = tmp_dir.path().join("Invalid.esm");
1✔
2761
        write_invalid_plugin(&path);
1✔
2762

1✔
2763
        let mut plugin = Plugin::new(GameId::Skyrim, &path);
1✔
2764

1✔
2765
        let result = plugin.parse_file(ParseOptions::header_only());
1✔
2766
        assert!(result.is_err());
1✔
2767
        assert_eq!("An error was encountered while parsing the plugin content \"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\": Expected record type \"TES4\"", result.unwrap_err().to_string());
1✔
2768
    }
1✔
2769

2770
    #[test]
2771
    fn parse_file_should_fail_for_a_non_plugin_file() {
1✔
2772
        let tmp_dir = tempdir().unwrap();
1✔
2773

1✔
2774
        let path = tmp_dir.path().join("Invalid.esm");
1✔
2775
        write_invalid_plugin(&path);
1✔
2776

1✔
2777
        let mut plugin = Plugin::new(GameId::Skyrim, &path);
1✔
2778

1✔
2779
        let result = plugin.parse_file(ParseOptions::whole_plugin());
1✔
2780
        assert!(result.is_err());
1✔
2781
        assert_eq!("An error was encountered while parsing the plugin content \"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\": Expected record type \"TES4\"", result.unwrap_err().to_string());
1✔
2782
    }
1✔
2783

2784
    #[test]
2785
    fn is_valid_should_return_true_for_a_valid_plugin() {
1✔
2786
        let is_valid = Plugin::is_valid(
1✔
2787
            GameId::Skyrim,
1✔
2788
            Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1✔
2789
            ParseOptions::header_only(),
1✔
2790
        );
1✔
2791

1✔
2792
        assert!(is_valid);
1✔
2793
    }
1✔
2794

2795
    #[test]
2796
    fn is_valid_should_return_false_for_an_invalid_plugin() {
1✔
2797
        let is_valid = Plugin::is_valid(
1✔
2798
            GameId::Skyrim,
1✔
2799
            Path::new("testing-plugins/Oblivion/Data/Blank.bsa"),
1✔
2800
            ParseOptions::header_only(),
1✔
2801
        );
1✔
2802

1✔
2803
        assert!(!is_valid);
1✔
2804
    }
1✔
2805

2806
    #[test]
2807
    fn path_should_return_the_full_plugin_path() {
1✔
2808
        let path = Path::new("Data/Blank.esm");
1✔
2809
        let plugin = Plugin::new(GameId::Skyrim, path);
1✔
2810

1✔
2811
        assert_eq!(path, plugin.path());
1✔
2812
    }
1✔
2813

2814
    #[test]
2815
    fn filename_should_return_filename_in_given_path() {
1✔
2816
        let plugin = Plugin::new(GameId::Skyrim, Path::new("Blank.esm"));
1✔
2817

1✔
2818
        assert_eq!("Blank.esm", plugin.filename().unwrap());
1✔
2819

2820
        let plugin = Plugin::new(GameId::Skyrim, Path::new("Blank.esp"));
1✔
2821

1✔
2822
        assert_eq!("Blank.esp", plugin.filename().unwrap());
1✔
2823
    }
1✔
2824

2825
    #[test]
2826
    fn filename_should_not_trim_dot_ghost_extension() {
1✔
2827
        let plugin = Plugin::new(GameId::Skyrim, Path::new("Blank.esp.ghost"));
1✔
2828

1✔
2829
        assert_eq!("Blank.esp.ghost", plugin.filename().unwrap());
1✔
2830
    }
1✔
2831

2832
    #[test]
2833
    fn masters_should_be_empty_for_a_plugin_with_no_masters() {
1✔
2834
        let mut plugin = Plugin::new(
1✔
2835
            GameId::Skyrim,
1✔
2836
            Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1✔
2837
        );
1✔
2838

1✔
2839
        assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
2840
        assert_eq!(0, plugin.masters().unwrap().len());
1✔
2841
    }
1✔
2842

2843
    #[test]
2844
    fn masters_should_not_be_empty_for_a_plugin_with_one_or_more_masters() {
1✔
2845
        let mut plugin = Plugin::new(
1✔
2846
            GameId::Skyrim,
1✔
2847
            Path::new(
1✔
2848
                "testing-plugins/Skyrim/Data/Blank - \
1✔
2849
                  Master Dependent.esm",
1✔
2850
            ),
1✔
2851
        );
1✔
2852

1✔
2853
        assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1✔
2854

2855
        let masters = plugin.masters().unwrap();
1✔
2856
        assert_eq!(1, masters.len());
1✔
2857
        assert_eq!("Blank.esm", masters[0]);
1✔
2858
    }
1✔
2859

2860
    #[test]
2861
    fn masters_should_only_read_up_to_the_first_null_for_each_master() {
1✔
2862
        let mut plugin = Plugin::new(
1✔
2863
            GameId::Skyrim,
1✔
2864
            Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esm"),
1✔
2865
        );
1✔
2866

1✔
2867
        let mut bytes = read(plugin.path()).unwrap();
1✔
2868

1✔
2869
        assert_eq!(0x2E, bytes[0x43]);
1✔
2870
        bytes[0x43] = 0;
1✔
2871

1✔
2872
        assert!(plugin
1✔
2873
            .parse_reader(Cursor::new(bytes), ParseOptions::whole_plugin())
1✔
2874
            .is_ok());
1✔
2875

2876
        assert_eq!("Blank", plugin.masters().unwrap()[0]);
1✔
2877
    }
1✔
2878

2879
    #[test]
2880
    fn description_should_error_for_a_plugin_header_subrecord_that_is_too_small() {
1✔
2881
        let mut plugin = Plugin::new(
1✔
2882
            GameId::Morrowind,
1✔
2883
            Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
1✔
2884
        );
1✔
2885

1✔
2886
        let mut data =
1✔
2887
            include_bytes!("../testing-plugins/Morrowind/Data Files/Blank.esm")[..0x20].to_vec();
1✔
2888
        data[0x04] = 16;
1✔
2889
        data[0x05] = 0;
1✔
2890
        data[0x14] = 8;
1✔
2891
        data[0x15] = 0;
1✔
2892

1✔
2893
        assert!(plugin
1✔
2894
            .parse_reader(Cursor::new(data), ParseOptions::header_only())
1✔
2895
            .is_ok());
1✔
2896

2897
        let result = plugin.description();
1✔
2898
        assert!(result.is_err());
1✔
2899
        assert_eq!("An error was encountered while parsing the plugin content \"\\x9a\\x99\\x99?\\x01\\x00\\x00\\x00\": Subrecord data field too short, expected at least 40 bytes", result.unwrap_err().to_string());
1✔
2900
    }
1✔
2901

2902
    #[test]
2903
    fn header_version_should_be_none_for_a_plugin_header_hedr_subrecord_that_is_too_small() {
1✔
2904
        let mut plugin = Plugin::new(
1✔
2905
            GameId::Morrowind,
1✔
2906
            Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
1✔
2907
        );
1✔
2908

1✔
2909
        let mut data =
1✔
2910
            include_bytes!("../testing-plugins/Morrowind/Data Files/Blank.esm")[..0x1B].to_vec();
1✔
2911
        data[0x04] = 11;
1✔
2912
        data[0x05] = 0;
1✔
2913
        data[0x14] = 3;
1✔
2914
        data[0x15] = 0;
1✔
2915

1✔
2916
        assert!(plugin
1✔
2917
            .parse_reader(Cursor::new(data), ParseOptions::header_only())
1✔
2918
            .is_ok());
1✔
2919
        assert!(plugin.header_version().is_none());
1✔
2920
    }
1✔
2921

2922
    #[test]
2923
    fn record_and_group_count_should_be_none_for_a_plugin_hedr_subrecord_that_is_too_small() {
1✔
2924
        let mut plugin = Plugin::new(
1✔
2925
            GameId::Morrowind,
1✔
2926
            Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
1✔
2927
        );
1✔
2928

1✔
2929
        let mut data =
1✔
2930
            include_bytes!("../testing-plugins/Morrowind/Data Files/Blank.esm")[..0x140].to_vec();
1✔
2931
        data[0x04] = 0x30;
1✔
2932
        data[0x14] = 0x28;
1✔
2933

1✔
2934
        assert!(plugin
1✔
2935
            .parse_reader(Cursor::new(data), ParseOptions::header_only())
1✔
2936
            .is_ok());
1✔
2937
        assert!(plugin.record_and_group_count().is_none());
1✔
2938
    }
1✔
2939

2940
    #[test]
2941
    fn resolve_form_ids_should_use_plugin_names_case_insensitively() {
1✔
2942
        let raw_form_ids = vec![0x0000_0001, 0x0100_0002];
1✔
2943

1✔
2944
        let masters = vec!["t\u{e9}st.esm".to_owned()];
1✔
2945
        let form_ids = resolve_form_ids(
1✔
2946
            GameId::SkyrimSE,
1✔
2947
            &raw_form_ids,
1✔
2948
            &PluginMetadata {
1✔
2949
                filename: "Bl\u{e0}\u{f1}k.esp".to_owned(),
1✔
2950
                scale: PluginScale::Full,
1✔
2951
                record_ids: Box::new([]),
1✔
2952
            },
1✔
2953
            &masters,
1✔
2954
            &[],
1✔
2955
        )
1✔
2956
        .unwrap();
1✔
2957

1✔
2958
        let other_masters = vec!["T\u{c9}ST.ESM".to_owned()];
1✔
2959
        let other_form_ids = resolve_form_ids(
1✔
2960
            GameId::SkyrimSE,
1✔
2961
            &raw_form_ids,
1✔
2962
            &PluginMetadata {
1✔
2963
                filename: "BL\u{c0}\u{d1}K.ESP".to_owned(),
1✔
2964
                scale: PluginScale::Full,
1✔
2965
                record_ids: Box::new([]),
1✔
2966
            },
1✔
2967
            &other_masters,
1✔
2968
            &[],
1✔
2969
        )
1✔
2970
        .unwrap();
1✔
2971

1✔
2972
        assert_eq!(form_ids, other_form_ids);
1✔
2973
    }
1✔
2974
}
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