1use std::collections::HashSet;
21use std::ffi::OsStr;
22use std::fs::File;
23use std::io::{BufRead, BufReader, Seek};
24use std::ops::RangeInclusive;
25use std::path::{Path, PathBuf};
26
27use encoding_rs::WINDOWS_1252;
28
29use crate::error::{Error, ParsingErrorKind};
30use crate::game_id::GameId;
31use crate::group::Group;
32use crate::record::{Record, MAX_RECORD_HEADER_LENGTH};
33use crate::record_id::{NamespacedId, ObjectIndexMask, RecordId, ResolvedRecordId, SourcePlugin};
34
35#[derive(Copy, Clone, PartialEq, Eq)]
36enum FileExtension {
37 Esm,
38 Esl,
39 Ghost,
40 Unrecognised,
41}
42
43impl From<&OsStr> for FileExtension {
44 fn from(value: &OsStr) -> Self {
45 if value.eq_ignore_ascii_case("esm") {
46 FileExtension::Esm
47 } else if value.eq_ignore_ascii_case("esl") {
48 FileExtension::Esl
49 } else if value.eq_ignore_ascii_case("ghost") {
50 FileExtension::Ghost
51 } else {
52 FileExtension::Unrecognised
53 }
54 }
55}
56
57#[derive(Clone, Default, PartialEq, Eq, Debug, Hash)]
58enum RecordIds {
59 #[default]
60 None,
61 FormIds(Vec<u32>),
62 NamespacedIds(Vec<NamespacedId>),
63 Resolved(Vec<ResolvedRecordId>),
64}
65
66impl From<Vec<NamespacedId>> for RecordIds {
67 fn from(record_ids: Vec<NamespacedId>) -> RecordIds {
68 RecordIds::NamespacedIds(record_ids)
69 }
70}
71
72impl From<Vec<u32>> for RecordIds {
73 fn from(form_ids: Vec<u32>) -> RecordIds {
74 RecordIds::FormIds(form_ids)
75 }
76}
77
78#[derive(Clone, PartialEq, Eq, Debug, Hash, Default)]
79struct PluginData {
80 header_record: Record,
81 record_ids: RecordIds,
82}
83
84#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
85enum PluginScale {
86 Full,
87 Medium,
88 Small,
89}
90
91#[derive(Clone, PartialEq, Eq, Debug, Hash)]
92pub struct Plugin {
93 game_id: GameId,
94 path: PathBuf,
95 data: PluginData,
96}
97
98#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
99pub struct ParseOptions {
100 header_only: bool,
101}
102
103impl ParseOptions {
104 pub fn header_only() -> Self {
105 Self { header_only: true }
106 }
107
108 pub fn whole_plugin() -> Self {
109 Self { header_only: false }
110 }
111}
112
113impl Plugin {
114 pub fn new(game_id: GameId, filepath: &Path) -> Plugin {
115 Plugin {
116 game_id,
117 path: filepath.to_path_buf(),
118 data: PluginData::default(),
119 }
120 }
121
122 pub fn parse_reader<R: std::io::Read + std::io::Seek>(
123 &mut self,
124 reader: R,
125 options: ParseOptions,
126 ) -> Result<(), Error> {
127 let mut reader = BufReader::new(reader);
128
129 self.data = read_plugin(&mut reader, self.game_id, options, self.header_type())?;
130
131 if self.game_id != GameId::Morrowind && self.game_id != GameId::Starfield {
132 self.resolve_record_ids(&[])?;
133 }
134
135 Ok(())
136 }
137
138 pub fn parse_file(&mut self, options: ParseOptions) -> Result<(), Error> {
139 let file = File::open(&self.path)?;
140
141 self.parse_reader(file, options)
142 }
143
144 pub fn resolve_record_ids(&mut self, plugins_metadata: &[PluginMetadata]) -> Result<(), Error> {
146 match &self.data.record_ids {
147 RecordIds::FormIds(form_ids) => {
148 let filename = self
149 .filename()
150 .ok_or_else(|| Error::NoFilename(self.path.clone()))?;
151 let parent_metadata = PluginMetadata {
152 filename,
153 scale: self.scale(),
154 record_ids: Box::new([]),
155 };
156 let masters = self.masters()?;
157
158 let form_ids = resolve_form_ids(
159 self.game_id,
160 form_ids,
161 &parent_metadata,
162 &masters,
163 plugins_metadata,
164 )?;
165
166 self.data.record_ids = RecordIds::Resolved(form_ids);
167 }
168 RecordIds::NamespacedIds(namespaced_ids) => {
169 let masters = self.masters()?;
170
171 let record_ids =
172 resolve_namespaced_ids(namespaced_ids, &masters, plugins_metadata)?;
173
174 self.data.record_ids = RecordIds::Resolved(record_ids);
175 }
176 RecordIds::None | RecordIds::Resolved(_) => {
177 }
179 }
180
181 Ok(())
182 }
183
184 pub fn game_id(&self) -> GameId {
185 self.game_id
186 }
187
188 pub fn path(&self) -> &Path {
189 &self.path
190 }
191
192 pub fn filename(&self) -> Option<String> {
193 self.path
194 .file_name()
195 .and_then(std::ffi::OsStr::to_str)
196 .map(std::string::ToString::to_string)
197 }
198
199 pub fn masters(&self) -> Result<Vec<String>, Error> {
200 masters(&self.data.header_record)
201 }
202
203 fn file_extension(&self) -> FileExtension {
204 if let Some(p) = self.path.extension() {
205 match FileExtension::from(p) {
206 FileExtension::Ghost => self
207 .path
208 .file_stem()
209 .map(Path::new)
210 .and_then(Path::extension)
211 .map_or(FileExtension::Unrecognised, FileExtension::from),
212 e => e,
213 }
214 } else {
215 FileExtension::Unrecognised
216 }
217 }
218
219 pub fn is_master_file(&self) -> bool {
220 match self.game_id {
221 GameId::Fallout4 | GameId::SkyrimSE | GameId::Starfield => {
222 self.is_master_flag_set()
225 || matches!(
226 self.file_extension(),
227 FileExtension::Esm | FileExtension::Esl
228 )
229 }
230 _ => self.is_master_flag_set(),
231 }
232 }
233
234 fn scale(&self) -> PluginScale {
235 if self.is_light_plugin() {
236 PluginScale::Small
237 } else if self.is_medium_flag_set() {
238 PluginScale::Medium
239 } else {
240 PluginScale::Full
241 }
242 }
243
244 pub fn is_light_plugin(&self) -> bool {
245 if self.game_id.supports_light_plugins() {
246 if self.game_id == GameId::Starfield {
247 self.is_light_flag_set()
250 || (!self.is_update_flag_set() && self.file_extension() == FileExtension::Esl)
251 } else {
252 self.is_light_flag_set() || self.file_extension() == FileExtension::Esl
253 }
254 } else {
255 false
256 }
257 }
258
259 pub fn is_medium_plugin(&self) -> bool {
260 self.is_medium_flag_set() && !self.is_light_plugin()
262 }
263
264 pub fn is_update_plugin(&self) -> bool {
265 self.is_update_flag_set()
268 && !self.is_light_flag_set()
269 && !self.is_medium_flag_set()
270 && self.masters().map(|m| !m.is_empty()).unwrap_or(false)
271 }
272
273 pub fn is_blueprint_plugin(&self) -> bool {
274 match self.game_id {
275 GameId::Starfield => self.data.header_record.header().flags() & 0x800 != 0,
276 _ => false,
277 }
278 }
279
280 pub fn is_valid(game_id: GameId, filepath: &Path, options: ParseOptions) -> bool {
281 let mut plugin = Plugin::new(game_id, filepath);
282
283 plugin.parse_file(options).is_ok()
284 }
285
286 pub fn description(&self) -> Result<Option<String>, Error> {
287 let (target_subrecord_type, description_offset) = match self.game_id {
288 GameId::Morrowind => (b"HEDR", 40),
289 _ => (b"SNAM", 0),
290 };
291
292 for subrecord in self.data.header_record.subrecords() {
293 if subrecord.subrecord_type() == target_subrecord_type {
294 let data = subrecord
295 .data()
296 .get(description_offset..)
297 .map(until_first_null)
298 .ok_or_else(|| {
299 Error::ParsingError(
300 subrecord.data().into(),
301 ParsingErrorKind::SubrecordDataTooShort(description_offset),
302 )
303 })?;
304
305 return WINDOWS_1252
306 .decode_without_bom_handling_and_without_replacement(data)
307 .map(|s| Some(s.to_string()))
308 .ok_or(Error::DecodeError(data.into()));
309 }
310 }
311
312 Ok(None)
313 }
314
315 pub fn header_version(&self) -> Option<f32> {
316 self.data
317 .header_record
318 .subrecords()
319 .iter()
320 .find(|s| s.subrecord_type() == b"HEDR")
321 .and_then(|s| crate::le_slice_to_f32(s.data()).ok())
322 }
323
324 pub fn record_and_group_count(&self) -> Option<u32> {
325 let count_offset = match self.game_id {
326 GameId::Morrowind => 296,
327 _ => 4,
328 };
329
330 self.data
331 .header_record
332 .subrecords()
333 .iter()
334 .find(|s| s.subrecord_type() == b"HEDR")
335 .and_then(|s| s.data().get(count_offset..))
336 .and_then(|d| crate::le_slice_to_u32(d).ok())
337 }
338
339 pub fn count_override_records(&self) -> Result<usize, Error> {
341 match &self.data.record_ids {
342 RecordIds::None => Ok(0),
343 RecordIds::FormIds(_) | RecordIds::NamespacedIds(_) => {
344 Err(Error::UnresolvedRecordIds(self.path.clone()))
345 }
346 RecordIds::Resolved(form_ids) => {
347 let count = form_ids.iter().filter(|f| f.is_overridden_record()).count();
348 Ok(count)
349 }
350 }
351 }
352
353 pub fn overlaps_with(&self, other: &Self) -> Result<bool, Error> {
354 use RecordIds::{FormIds, NamespacedIds, Resolved};
355 match (&self.data.record_ids, &other.data.record_ids) {
356 (FormIds(_), _) => Err(Error::UnresolvedRecordIds(self.path.clone())),
357 (_, FormIds(_)) => Err(Error::UnresolvedRecordIds(other.path.clone())),
358 (Resolved(left), Resolved(right)) => Ok(sorted_slices_intersect(left, right)),
359 (NamespacedIds(left), NamespacedIds(right)) => Ok(sorted_slices_intersect(left, right)),
360 _ => Ok(false),
361 }
362 }
363
364 pub fn overlap_size(&self, others: &[&Self]) -> Result<usize, Error> {
368 use RecordIds::{FormIds, NamespacedIds, None, Resolved};
369
370 match &self.data.record_ids {
371 FormIds(_) => Err(Error::UnresolvedRecordIds(self.path.clone())),
372 Resolved(ids) => {
373 let mut count = 0;
374 for id in ids {
375 for other in others {
376 match &other.data.record_ids {
377 FormIds(_) => {
378 return Err(Error::UnresolvedRecordIds(other.path.clone()))
379 }
380 Resolved(master_ids) if master_ids.binary_search(id).is_ok() => {
381 count += 1;
382 break;
383 }
384 _ => {
385 }
387 }
388 }
389 }
390
391 Ok(count)
392 }
393 NamespacedIds(ids) => {
394 let count = ids
395 .iter()
396 .filter(|id| {
397 others.iter().any(|other| match &other.data.record_ids {
398 NamespacedIds(master_ids) => master_ids.binary_search(id).is_ok(),
399 _ => false,
400 })
401 })
402 .count();
403 Ok(count)
404 }
405 None => Ok(0),
406 }
407 }
408
409 pub fn is_valid_as_light_plugin(&self) -> Result<bool, Error> {
410 if self.game_id.supports_light_plugins() {
411 match &self.data.record_ids {
412 RecordIds::None => Ok(true),
413 RecordIds::FormIds(_) => Err(Error::UnresolvedRecordIds(self.path.clone())),
414 RecordIds::Resolved(form_ids) => {
415 let valid_range = self.valid_light_form_id_range();
416
417 let is_valid = form_ids
418 .iter()
419 .filter(|f| !f.is_overridden_record())
420 .all(|f| f.is_object_index_in(&valid_range));
421
422 Ok(is_valid)
423 }
424 RecordIds::NamespacedIds(_) => Ok(false),
425 }
426 } else {
427 Ok(false)
428 }
429 }
430
431 pub fn is_valid_as_medium_plugin(&self) -> Result<bool, Error> {
432 if self.game_id.supports_medium_plugins() {
433 match &self.data.record_ids {
434 RecordIds::None => Ok(true),
435 RecordIds::FormIds(_) => Err(Error::UnresolvedRecordIds(self.path.clone())),
436 RecordIds::Resolved(form_ids) => {
437 let valid_range = self.valid_medium_form_id_range();
438
439 let is_valid = form_ids
440 .iter()
441 .filter(|f| !f.is_overridden_record())
442 .all(|f| f.is_object_index_in(&valid_range));
443
444 Ok(is_valid)
445 }
446 RecordIds::NamespacedIds(_) => Ok(false), }
448 } else {
449 Ok(false)
450 }
451 }
452
453 pub fn is_valid_as_update_plugin(&self) -> Result<bool, Error> {
454 if self.game_id == GameId::Starfield {
455 match &self.data.record_ids {
460 RecordIds::None => Ok(true),
461 RecordIds::FormIds(_) => Err(Error::UnresolvedRecordIds(self.path.clone())),
462 RecordIds::Resolved(form_ids) => {
463 Ok(form_ids.iter().all(ResolvedRecordId::is_overridden_record))
464 }
465 RecordIds::NamespacedIds(_) => Ok(false), }
467 } else {
468 Ok(false)
469 }
470 }
471
472 fn header_type(&self) -> &'static [u8] {
473 match self.game_id {
474 GameId::Morrowind => b"TES3",
475 _ => b"TES4",
476 }
477 }
478
479 fn is_master_flag_set(&self) -> bool {
480 match self.game_id {
481 GameId::Morrowind => self
482 .data
483 .header_record
484 .subrecords()
485 .iter()
486 .find(|s| s.subrecord_type() == b"HEDR")
487 .and_then(|s| s.data().get(4))
488 .is_some_and(|b| b & 0x1 != 0),
489 _ => self.data.header_record.header().flags() & 0x1 != 0,
490 }
491 }
492
493 fn is_light_flag_set(&self) -> bool {
494 let flag = match self.game_id {
495 GameId::Starfield => 0x100,
496 GameId::SkyrimSE | GameId::Fallout4 => 0x200,
497 _ => return false,
498 };
499
500 self.data.header_record.header().flags() & flag != 0
501 }
502
503 fn is_medium_flag_set(&self) -> bool {
504 let flag = match self.game_id {
505 GameId::Starfield => 0x400,
506 _ => return false,
507 };
508
509 self.data.header_record.header().flags() & flag != 0
510 }
511
512 fn is_update_flag_set(&self) -> bool {
513 match self.game_id {
514 GameId::Starfield => self.data.header_record.header().flags() & 0x200 != 0,
515 _ => false,
516 }
517 }
518
519 fn valid_light_form_id_range(&self) -> RangeInclusive<u32> {
520 match self.game_id {
521 GameId::SkyrimSE => match self.header_version() {
522 Some(v) if v < 1.71 => 0x800..=0xFFF,
523 Some(_) => 0..=0xFFF,
524 None => 0..=0,
525 },
526 GameId::Fallout4 => match self.header_version() {
527 Some(v) if v < 1.0 => 0x800..=0xFFF,
528 Some(_) => 0x001..=0xFFF,
529 None => 0..=0,
530 },
531 GameId::Starfield => 0..=0xFFF,
532 _ => 0..=0,
533 }
534 }
535
536 fn valid_medium_form_id_range(&self) -> RangeInclusive<u32> {
537 match self.game_id {
538 GameId::Starfield => 0..=0xFFFF,
539 _ => 0..=0,
540 }
541 }
542}
543
544#[derive(Clone, Debug, PartialEq, Eq)]
545pub struct PluginMetadata {
546 filename: String,
547 scale: PluginScale,
548 record_ids: Box<[NamespacedId]>,
549}
550
551pub fn plugins_metadata(plugins: &[&Plugin]) -> Result<Vec<PluginMetadata>, Error> {
553 let mut vec = Vec::new();
554
555 for plugin in plugins {
556 let filename = plugin
557 .filename()
558 .ok_or_else(|| Error::NoFilename(plugin.path().to_path_buf()))?;
559
560 let record_ids = if plugin.game_id == GameId::Morrowind {
561 match &plugin.data.record_ids {
562 RecordIds::NamespacedIds(ids) => ids.clone(),
563 _ => Vec::new(), }
565 } else {
566 Vec::new()
567 };
568
569 let metadata = PluginMetadata {
570 filename,
571 scale: plugin.scale(),
572 record_ids: record_ids.into_boxed_slice(),
573 };
574
575 vec.push(metadata);
576 }
577
578 Ok(vec)
579}
580
581fn sorted_slices_intersect<T: PartialOrd>(left: &[T], right: &[T]) -> bool {
582 let mut left_iter = left.iter();
583 let mut right_iter = right.iter();
584
585 let mut left_element = left_iter.next();
586 let mut right_element = right_iter.next();
587
588 while let (Some(left_value), Some(right_value)) = (left_element, right_element) {
589 if left_value < right_value {
590 left_element = left_iter.next();
591 } else if left_value > right_value {
592 right_element = right_iter.next();
593 } else {
594 return true;
595 }
596 }
597
598 false
599}
600
601fn resolve_form_ids(
602 game_id: GameId,
603 form_ids: &[u32],
604 plugin_metadata: &PluginMetadata,
605 masters: &[String],
606 other_plugins_metadata: &[PluginMetadata],
607) -> Result<Vec<ResolvedRecordId>, Error> {
608 let hashed_parent = hashed_parent(game_id, plugin_metadata);
609 let hashed_masters = match game_id {
610 GameId::Starfield => hashed_masters_for_starfield(masters, other_plugins_metadata)?,
611 _ => hashed_masters(masters),
612 };
613
614 let mut form_ids: Vec<_> = form_ids
615 .iter()
616 .map(|form_id| ResolvedRecordId::from_form_id(hashed_parent, &hashed_masters, *form_id))
617 .collect();
618
619 form_ids.sort();
620
621 Ok(form_ids)
622}
623
624fn resolve_namespaced_ids(
625 namespaced_ids: &[NamespacedId],
626 masters: &[String],
627 other_plugins_metadata: &[PluginMetadata],
628) -> Result<Vec<ResolvedRecordId>, Error> {
629 let mut record_ids: HashSet<NamespacedId> = HashSet::new();
630
631 for master in masters {
632 let master_record_ids = other_plugins_metadata
633 .iter()
634 .find(|m| unicase::eq(&m.filename, master))
635 .map(|m| &m.record_ids)
636 .ok_or_else(|| Error::PluginMetadataNotFound(master.clone()))?;
637
638 record_ids.extend(master_record_ids.iter().cloned());
639 }
640
641 let mut resolved_ids: Vec<_> = namespaced_ids
642 .iter()
643 .map(|id| ResolvedRecordId::from_namespaced_id(id, &record_ids))
644 .collect();
645
646 resolved_ids.sort();
647
648 Ok(resolved_ids)
649}
650
651fn hashed_parent(game_id: GameId, parent_metadata: &PluginMetadata) -> SourcePlugin {
652 match game_id {
653 GameId::Starfield => {
654 let object_index_mask = match parent_metadata.scale {
656 PluginScale::Full => ObjectIndexMask::Full,
657 PluginScale::Medium => ObjectIndexMask::Medium,
658 PluginScale::Small => ObjectIndexMask::Small,
659 };
660 SourcePlugin::parent(&parent_metadata.filename, object_index_mask)
661 }
662 _ => SourcePlugin::parent(&parent_metadata.filename, ObjectIndexMask::Full),
664 }
665}
666
667fn hashed_masters(masters: &[String]) -> Vec<SourcePlugin> {
668 masters
669 .iter()
670 .enumerate()
671 .filter_map(|(i, m)| {
672 let i = u8::try_from(i).ok()?;
674 let mod_index_mask = u32::from(i) << 24u8;
675 Some(SourcePlugin::master(
676 m,
677 mod_index_mask,
678 ObjectIndexMask::Full,
679 ))
680 })
681 .collect()
682}
683
684fn hashed_masters_for_starfield(
686 masters: &[String],
687 masters_metadata: &[PluginMetadata],
688) -> Result<Vec<SourcePlugin>, Error> {
689 let mut hashed_masters = Vec::new();
690 let mut full_mask = 0;
691 let mut medium_mask = 0xFD00_0000;
692 let mut small_mask = 0xFE00_0000;
693
694 for master in masters {
695 let master_scale = masters_metadata
696 .iter()
697 .find(|m| unicase::eq(&m.filename, master))
698 .map(|m| m.scale)
699 .ok_or_else(|| Error::PluginMetadataNotFound(master.clone()))?;
700
701 match master_scale {
702 PluginScale::Full => {
703 hashed_masters.push(SourcePlugin::master(
704 master,
705 full_mask,
706 ObjectIndexMask::Full,
707 ));
708
709 full_mask += 0x0100_0000;
710 }
711 PluginScale::Medium => {
712 hashed_masters.push(SourcePlugin::master(
713 master,
714 medium_mask,
715 ObjectIndexMask::Medium,
716 ));
717
718 medium_mask += 0x0001_0000;
719 }
720 PluginScale::Small => {
721 hashed_masters.push(SourcePlugin::master(
722 master,
723 small_mask,
724 ObjectIndexMask::Small,
725 ));
726
727 small_mask += 0x0000_1000;
728 }
729 }
730 }
731
732 Ok(hashed_masters)
733}
734
735fn masters(header_record: &Record) -> Result<Vec<String>, Error> {
736 header_record
737 .subrecords()
738 .iter()
739 .filter(|s| s.subrecord_type() == b"MAST")
740 .map(|s| until_first_null(s.data()))
741 .map(|d| {
742 WINDOWS_1252
743 .decode_without_bom_handling_and_without_replacement(d)
744 .map(|s| s.to_string())
745 .ok_or(Error::DecodeError(d.into()))
746 })
747 .collect()
748}
749
750fn read_form_ids<R: BufRead + Seek>(reader: &mut R, game_id: GameId) -> Result<Vec<u32>, Error> {
751 let mut form_ids = Vec::new();
752 let mut header_buf = [0; MAX_RECORD_HEADER_LENGTH];
753
754 while !reader.fill_buf()?.is_empty() {
755 Group::read_form_ids(reader, game_id, &mut form_ids, &mut header_buf)?;
756 }
757
758 Ok(form_ids)
759}
760
761fn read_morrowind_record_ids<R: BufRead + Seek>(reader: &mut R) -> Result<RecordIds, Error> {
762 let mut record_ids = Vec::new();
763 let mut header_buf = [0; 16]; while !reader.fill_buf()?.is_empty() {
766 let (_, record_id) =
767 Record::read_record_id(reader, GameId::Morrowind, &mut header_buf, false)?;
768
769 if let Some(RecordId::NamespacedId(record_id)) = record_id {
770 record_ids.push(record_id);
771 }
772 }
773
774 record_ids.sort();
775
776 Ok(record_ids.into())
777}
778
779fn read_record_ids<R: BufRead + Seek>(reader: &mut R, game_id: GameId) -> Result<RecordIds, Error> {
780 if game_id == GameId::Morrowind {
781 read_morrowind_record_ids(reader)
782 } else {
783 read_form_ids(reader, game_id).map(Into::into)
784 }
785}
786
787fn read_plugin<R: BufRead + Seek>(
788 reader: &mut R,
789 game_id: GameId,
790 options: ParseOptions,
791 expected_header_type: &'static [u8],
792) -> Result<PluginData, Error> {
793 let header_record = Record::read(reader, game_id, expected_header_type)?;
794
795 if options.header_only {
796 return Ok(PluginData {
797 header_record,
798 record_ids: RecordIds::None,
799 });
800 }
801
802 let record_ids = read_record_ids(reader, game_id)?;
803
804 Ok(PluginData {
805 header_record,
806 record_ids,
807 })
808}
809
810fn until_first_null(bytes: &[u8]) -> &[u8] {
813 if let Some(i) = memchr::memchr(0, bytes) {
814 bytes.split_at(i).0
815 } else {
816 bytes
817 }
818}
819
820#[cfg(test)]
821mod 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() {
833 let mut plugin = Plugin::new(
834 GameId::Morrowind,
835 Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
836 );
837
838 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
839
840 match plugin.data.record_ids {
841 RecordIds::NamespacedIds(ids) => assert_eq!(10, ids.len()),
842 _ => panic!("Expected namespaced record IDs"),
843 }
844 }
845
846 #[test]
847 fn plugin_parse_file_should_read_a_unique_id_for_each_record() {
848 let mut plugin = Plugin::new(
849 GameId::Morrowind,
850 Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
851 );
852
853 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
854
855 match plugin.data.record_ids {
856 RecordIds::NamespacedIds(ids) => {
857 let set: HashSet<NamespacedId> = ids.iter().cloned().collect();
858 assert_eq!(set.len(), ids.len());
859 }
860 _ => panic!("Expected namespaced record IDs"),
861 }
862 }
863
864 #[test]
865 fn parse_file_header_only_should_not_store_record_ids() {
866 let mut plugin = Plugin::new(
867 GameId::Morrowind,
868 Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
869 );
870
871 let result = plugin.parse_file(ParseOptions::header_only());
872
873 assert!(result.is_ok());
874
875 assert_eq!(RecordIds::None, plugin.data.record_ids);
876 }
877
878 #[test]
879 fn game_id_should_return_the_plugins_associated_game_id() {
880 let plugin = Plugin::new(GameId::Morrowind, Path::new("Data/Blank.esm"));
881
882 assert_eq!(GameId::Morrowind, plugin.game_id());
883 }
884
885 #[test]
886 fn is_master_file_should_be_true_for_plugin_with_master_flag_set() {
887 let mut plugin = Plugin::new(
888 GameId::Morrowind,
889 Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
890 );
891
892 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
893 assert!(plugin.is_master_file());
894 }
895
896 #[test]
897 fn is_master_file_should_be_false_for_plugin_without_master_flag_set() {
898 let mut plugin = Plugin::new(
899 GameId::Morrowind,
900 Path::new("testing-plugins/Morrowind/Data Files/Blank.esp"),
901 );
902
903 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
904 assert!(!plugin.is_master_file());
905 }
906
907 #[test]
908 fn is_master_file_should_ignore_file_extension() {
909 let tmp_dir = tempdir().unwrap();
910
911 let esm = tmp_dir.path().join("Blank.esm");
912 copy(
913 Path::new("testing-plugins/Morrowind/Data Files/Blank.esp"),
914 &esm,
915 )
916 .unwrap();
917
918 let mut plugin = Plugin::new(GameId::Morrowind, &esm);
919
920 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
921 assert!(!plugin.is_master_file());
922 }
923
924 #[test]
925 fn is_light_plugin_should_be_false() {
926 let plugin = Plugin::new(GameId::Morrowind, Path::new("Blank.esp"));
927 assert!(!plugin.is_light_plugin());
928 let plugin = Plugin::new(GameId::Morrowind, Path::new("Blank.esm"));
929 assert!(!plugin.is_light_plugin());
930 let plugin = Plugin::new(GameId::Morrowind, Path::new("Blank.esl"));
931 assert!(!plugin.is_light_plugin());
932 }
933
934 #[test]
935 fn is_medium_plugin_should_be_false() {
936 let plugin = Plugin::new(GameId::Morrowind, Path::new("Blank.esp"));
937 assert!(!plugin.is_medium_plugin());
938 let plugin = Plugin::new(GameId::Morrowind, Path::new("Blank.esm"));
939 assert!(!plugin.is_medium_plugin());
940 let plugin = Plugin::new(GameId::Morrowind, Path::new("Blank.esl"));
941 assert!(!plugin.is_medium_plugin());
942 }
943
944 #[test]
945 fn description_should_trim_nulls_in_plugin_header_hedr_subrecord_content() {
946 let mut plugin = Plugin::new(
947 GameId::Morrowind,
948 Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
949 );
950
951 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
952
953 assert_eq!("v5.0", plugin.description().unwrap().unwrap());
954 }
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() {
959 let mut plugin = Plugin::new(
960 GameId::Morrowind,
961 Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
962 );
963
964 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
965
966 assert_eq!(1.2, plugin.header_version().unwrap());
967 }
968
969 #[test]
970 fn record_and_group_count_should_read_correct_offset() {
971 let mut plugin = Plugin::new(
972 GameId::Morrowind,
973 Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
974 );
975
976 assert!(plugin.record_and_group_count().is_none());
977 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
978 assert_eq!(10, plugin.record_and_group_count().unwrap());
979 }
980
981 #[test]
982 fn record_and_group_count_should_match_record_ids_length() {
983 let mut plugin = Plugin::new(
984 GameId::Morrowind,
985 Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
986 );
987
988 assert!(plugin.record_and_group_count().is_none());
989 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
990 assert_eq!(10, plugin.record_and_group_count().unwrap());
991 match plugin.data.record_ids {
992 RecordIds::NamespacedIds(ids) => assert_eq!(10, ids.len()),
993 _ => panic!("Expected namespaced record IDs"),
994 }
995 }
996
997 #[test]
998 fn count_override_records_should_error_if_record_ids_are_not_yet_resolved() {
999 let mut plugin = Plugin::new(
1000 GameId::Morrowind,
1001 Path::new("testing-plugins/Morrowind/Data Files/Blank - Master Dependent.esm"),
1002 );
1003
1004 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1005
1006 match plugin.count_override_records().unwrap_err() {
1007 Error::UnresolvedRecordIds(path) => assert_eq!(plugin.path, path),
1008 _ => panic!("Expected unresolved record IDs error"),
1009 }
1010 }
1011
1012 #[test]
1013 fn count_override_records_should_count_how_many_records_are_also_present_in_masters() {
1014 let mut plugin = Plugin::new(
1015 GameId::Morrowind,
1016 Path::new("testing-plugins/Morrowind/Data Files/Blank - Master Dependent.esm"),
1017 );
1018 let mut master = Plugin::new(
1019 GameId::Morrowind,
1020 Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
1021 );
1022
1023 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1024 assert!(master.parse_file(ParseOptions::whole_plugin()).is_ok());
1025
1026 let plugins_metadata = plugins_metadata(&[&master]).unwrap();
1027
1028 plugin.resolve_record_ids(&plugins_metadata).unwrap();
1029
1030 assert_eq!(4, plugin.count_override_records().unwrap());
1031 }
1032
1033 #[test]
1034 fn overlaps_with_should_detect_when_two_plugins_have_a_record_with_the_same_id() {
1035 let mut plugin1 = Plugin::new(
1036 GameId::Morrowind,
1037 Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
1038 );
1039 let mut plugin2 = Plugin::new(
1040 GameId::Morrowind,
1041 Path::new("testing-plugins/Morrowind/Data Files/Blank - Different.esm"),
1042 );
1043
1044 assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1045 assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1046
1047 assert!(plugin1.overlaps_with(&plugin1).unwrap());
1048 assert!(!plugin1.overlaps_with(&plugin2).unwrap());
1049 }
1050
1051 #[test]
1052 fn overlap_size_should_only_count_each_record_once() {
1053 let mut plugin1 = Plugin::new(
1054 GameId::Morrowind,
1055 Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
1056 );
1057 let mut plugin2 = Plugin::new(
1058 GameId::Morrowind,
1059 Path::new("testing-plugins/Morrowind/Data Files/Blank - Master Dependent.esm"),
1060 );
1061
1062 assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1063 assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1064
1065 assert_eq!(4, plugin1.overlap_size(&[&plugin2, &plugin2]).unwrap());
1066 }
1067
1068 #[test]
1069 fn overlap_size_should_check_against_all_given_plugins() {
1070 let mut plugin1 = Plugin::new(
1071 GameId::Morrowind,
1072 Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
1073 );
1074 let mut plugin2 = Plugin::new(
1075 GameId::Morrowind,
1076 Path::new("testing-plugins/Morrowind/Data Files/Blank.esp"),
1077 );
1078 let mut plugin3 = Plugin::new(
1079 GameId::Morrowind,
1080 Path::new("testing-plugins/Morrowind/Data Files/Blank - Master Dependent.esm"),
1081 );
1082
1083 assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1084 assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1085 assert!(plugin3.parse_file(ParseOptions::whole_plugin()).is_ok());
1086
1087 assert_eq!(4, plugin1.overlap_size(&[&plugin2, &plugin3]).unwrap());
1088 }
1089
1090 #[test]
1091 fn overlap_size_should_return_0_if_plugins_have_not_been_parsed() {
1092 let mut plugin1 = Plugin::new(
1093 GameId::Morrowind,
1094 Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
1095 );
1096 let mut plugin2 = Plugin::new(
1097 GameId::Morrowind,
1098 Path::new("testing-plugins/Morrowind/Data Files/Blank - Master Dependent.esm"),
1099 );
1100
1101 assert_eq!(0, plugin1.overlap_size(&[&plugin2]).unwrap());
1102
1103 assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1104
1105 assert_eq!(0, plugin1.overlap_size(&[&plugin2]).unwrap());
1106
1107 assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1108
1109 assert_ne!(0, plugin1.overlap_size(&[&plugin2]).unwrap());
1110 }
1111
1112 #[test]
1113 fn overlap_size_should_return_0_when_there_is_no_overlap() {
1114 let mut plugin1 = Plugin::new(
1115 GameId::Morrowind,
1116 Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
1117 );
1118 let mut plugin2 = Plugin::new(
1119 GameId::Morrowind,
1120 Path::new("testing-plugins/Morrowind/Data Files/Blank.esp"),
1121 );
1122
1123 assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1124 assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1125
1126 assert!(!plugin1.overlaps_with(&plugin2).unwrap());
1127 assert_eq!(0, plugin1.overlap_size(&[&plugin2]).unwrap());
1128 }
1129
1130 #[test]
1131 fn valid_light_form_id_range_should_be_empty() {
1132 let mut plugin = Plugin::new(
1133 GameId::Morrowind,
1134 Path::new("testing-plugins/Morrowind/Data Files/Blank - Master Dependent.esm"),
1135 );
1136 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1137
1138 let range = plugin.valid_light_form_id_range();
1139 assert_eq!(&0, range.start());
1140 assert_eq!(&0, range.end());
1141 }
1142
1143 #[test]
1144 fn is_valid_as_light_plugin_should_always_be_false() {
1145 let mut plugin = Plugin::new(
1146 GameId::Morrowind,
1147 Path::new("testing-plugins/Morrowind/Data Files/Blank - Master Dependent.esm"),
1148 );
1149 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1150 assert!(!plugin.is_valid_as_light_plugin().unwrap());
1151 }
1152 }
1153
1154 mod oblivion {
1155 use super::super::*;
1156
1157 #[test]
1158 fn is_light_plugin_should_be_false() {
1159 let plugin = Plugin::new(GameId::Oblivion, Path::new("Blank.esp"));
1160 assert!(!plugin.is_light_plugin());
1161 let plugin = Plugin::new(GameId::Oblivion, Path::new("Blank.esm"));
1162 assert!(!plugin.is_light_plugin());
1163 let plugin = Plugin::new(GameId::Oblivion, Path::new("Blank.esl"));
1164 assert!(!plugin.is_light_plugin());
1165 }
1166
1167 #[test]
1168 fn is_medium_plugin_should_be_false() {
1169 let plugin = Plugin::new(GameId::Oblivion, Path::new("Blank.esp"));
1170 assert!(!plugin.is_medium_plugin());
1171 let plugin = Plugin::new(GameId::Oblivion, Path::new("Blank.esm"));
1172 assert!(!plugin.is_medium_plugin());
1173 let plugin = Plugin::new(GameId::Oblivion, Path::new("Blank.esl"));
1174 assert!(!plugin.is_medium_plugin());
1175 }
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() {
1180 let mut plugin = Plugin::new(
1181 GameId::Oblivion,
1182 Path::new("testing-plugins/Oblivion/Data/Blank.esm"),
1183 );
1184
1185 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1186
1187 assert_eq!(0.8, plugin.header_version().unwrap());
1188 }
1189
1190 #[test]
1191 fn valid_light_form_id_range_should_be_empty() {
1192 let mut plugin = Plugin::new(
1193 GameId::Oblivion,
1194 Path::new("testing-plugins/Oblivion/Data/Blank - Master Dependent.esm"),
1195 );
1196 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1197
1198 let range = plugin.valid_light_form_id_range();
1199 assert_eq!(&0, range.start());
1200 assert_eq!(&0, range.end());
1201 }
1202
1203 #[test]
1204 fn is_valid_as_light_plugin_should_always_be_false() {
1205 let mut plugin = Plugin::new(
1206 GameId::Oblivion,
1207 Path::new("testing-plugins/Oblivion/Data/Blank - Master Dependent.esm"),
1208 );
1209 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1210 assert!(!plugin.is_valid_as_light_plugin().unwrap());
1211 }
1212 }
1213
1214 mod skyrim {
1215 use super::*;
1216
1217 #[test]
1218 fn parse_file_should_succeed() {
1219 let mut plugin = Plugin::new(
1220 GameId::Skyrim,
1221 Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1222 );
1223
1224 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1225
1226 match plugin.data.record_ids {
1227 RecordIds::Resolved(ids) => assert_eq!(10, ids.len()),
1228 _ => panic!("Expected resolved FormIDs"),
1229 }
1230 }
1231
1232 #[test]
1233 fn parse_file_header_only_should_not_store_form_ids() {
1234 let mut plugin = Plugin::new(
1235 GameId::Skyrim,
1236 Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1237 );
1238
1239 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1240
1241 assert_eq!(RecordIds::None, plugin.data.record_ids);
1242 }
1243
1244 #[test]
1245 fn game_id_should_return_the_plugins_associated_game_id() {
1246 let plugin = Plugin::new(GameId::Skyrim, Path::new("Data/Blank.esm"));
1247
1248 assert_eq!(GameId::Skyrim, plugin.game_id());
1249 }
1250
1251 #[test]
1252 fn is_master_file_should_be_true_for_master_file() {
1253 let mut plugin = Plugin::new(
1254 GameId::Skyrim,
1255 Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1256 );
1257
1258 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1259 assert!(plugin.is_master_file());
1260 }
1261
1262 #[test]
1263 fn is_master_file_should_be_false_for_non_master_file() {
1264 let mut plugin = Plugin::new(
1265 GameId::Skyrim,
1266 Path::new("testing-plugins/Skyrim/Data/Blank.esp"),
1267 );
1268
1269 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1270 assert!(!plugin.is_master_file());
1271 }
1272
1273 #[test]
1274 fn is_light_plugin_should_be_false() {
1275 let plugin = Plugin::new(GameId::Skyrim, Path::new("Blank.esp"));
1276 assert!(!plugin.is_light_plugin());
1277 let plugin = Plugin::new(GameId::Skyrim, Path::new("Blank.esm"));
1278 assert!(!plugin.is_light_plugin());
1279 let plugin = Plugin::new(GameId::Skyrim, Path::new("Blank.esl"));
1280 assert!(!plugin.is_light_plugin());
1281 }
1282
1283 #[test]
1284 fn is_medium_plugin_should_be_false() {
1285 let plugin = Plugin::new(GameId::Skyrim, Path::new("Blank.esp"));
1286 assert!(!plugin.is_medium_plugin());
1287 let plugin = Plugin::new(GameId::Skyrim, Path::new("Blank.esm"));
1288 assert!(!plugin.is_medium_plugin());
1289 let plugin = Plugin::new(GameId::Skyrim, Path::new("Blank.esl"));
1290 assert!(!plugin.is_medium_plugin());
1291 }
1292
1293 #[test]
1294 fn description_should_return_plugin_description_field_content() {
1295 let mut plugin = Plugin::new(
1296 GameId::Skyrim,
1297 Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1298 );
1299
1300 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1301 assert_eq!("v5.0", plugin.description().unwrap().unwrap());
1302
1303 let mut plugin = Plugin::new(
1304 GameId::Skyrim,
1305 Path::new("testing-plugins/Skyrim/Data/Blank.esp"),
1306 );
1307
1308 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1309 assert_eq!(
1310 "\u{20ac}\u{192}\u{160}",
1311 plugin.description().unwrap().unwrap()
1312 );
1313
1314 let mut plugin = Plugin::new(
1315 GameId::Skyrim,
1316 Path::new(
1317 "testing-plugins/Skyrim/Data/Blank - \
1318 Master Dependent.esm",
1319 ),
1320 );
1321
1322 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1323 assert_eq!("", plugin.description().unwrap().unwrap());
1324 }
1325
1326 #[test]
1327 fn description_should_trim_nulls_in_plugin_description_field_content() {
1328 let mut plugin = Plugin::new(
1329 GameId::Skyrim,
1330 Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1331 );
1332
1333 let mut bytes = read(plugin.path()).unwrap();
1334
1335 assert_eq!(0x2E, bytes[0x39]);
1336 bytes[0x39] = 0;
1337
1338 assert!(plugin
1339 .parse_reader(Cursor::new(bytes), ParseOptions::whole_plugin())
1340 .is_ok());
1341
1342 assert_eq!("v5", plugin.description().unwrap().unwrap());
1343 }
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() {
1348 let mut plugin = Plugin::new(
1349 GameId::Skyrim,
1350 Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1351 );
1352
1353 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1354
1355 assert_eq!(0.94, plugin.header_version().unwrap());
1356 }
1357
1358 #[test]
1359 fn record_and_group_count_should_be_non_zero_for_a_plugin_with_records() {
1360 let mut plugin = Plugin::new(
1361 GameId::Skyrim,
1362 Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1363 );
1364
1365 assert!(plugin.record_and_group_count().is_none());
1366 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1367 assert_eq!(15, plugin.record_and_group_count().unwrap());
1368 }
1369
1370 #[test]
1371 fn count_override_records_should_count_how_many_records_come_from_masters() {
1372 let mut plugin = Plugin::new(
1373 GameId::Skyrim,
1374 Path::new("testing-plugins/Skyrim/Data/Blank - Different Master Dependent.esp"),
1375 );
1376
1377 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1378 assert_eq!(2, plugin.count_override_records().unwrap());
1379 }
1380
1381 #[test]
1382 fn overlaps_with_should_detect_when_two_plugins_have_a_record_from_the_same_master() {
1383 let mut plugin1 = Plugin::new(
1384 GameId::Skyrim,
1385 Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1386 );
1387 let mut plugin2 = Plugin::new(
1388 GameId::Skyrim,
1389 Path::new("testing-plugins/Skyrim/Data/Blank - Different.esm"),
1390 );
1391
1392 assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1393 assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1394
1395 assert!(plugin1.overlaps_with(&plugin1).unwrap());
1396 assert!(!plugin1.overlaps_with(&plugin2).unwrap());
1397 }
1398
1399 #[test]
1400 fn overlap_size_should_only_count_each_record_once() {
1401 let mut plugin1 = Plugin::new(
1402 GameId::Skyrim,
1403 Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1404 );
1405 let mut plugin2 = Plugin::new(
1406 GameId::Skyrim,
1407 Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esm"),
1408 );
1409
1410 assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1411 assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1412
1413 assert_eq!(4, plugin1.overlap_size(&[&plugin2, &plugin2]).unwrap());
1414 }
1415
1416 #[test]
1417 fn overlap_size_should_check_against_all_given_plugins() {
1418 let mut plugin1 = Plugin::new(
1419 GameId::Skyrim,
1420 Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1421 );
1422 let mut plugin2 = Plugin::new(
1423 GameId::Skyrim,
1424 Path::new("testing-plugins/Skyrim/Data/Blank.esp"),
1425 );
1426 let mut plugin3 = Plugin::new(
1427 GameId::Skyrim,
1428 Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esp"),
1429 );
1430
1431 assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1432 assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1433 assert!(plugin3.parse_file(ParseOptions::whole_plugin()).is_ok());
1434
1435 assert_eq!(2, plugin1.overlap_size(&[&plugin2, &plugin3]).unwrap());
1436 }
1437
1438 #[test]
1439 fn overlap_size_should_return_0_if_plugins_have_not_been_parsed() {
1440 let mut plugin1 = Plugin::new(
1441 GameId::Skyrim,
1442 Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1443 );
1444 let mut plugin2 = Plugin::new(
1445 GameId::Skyrim,
1446 Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esm"),
1447 );
1448
1449 assert_eq!(0, plugin1.overlap_size(&[&plugin2]).unwrap());
1450
1451 assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1452
1453 assert_eq!(0, plugin1.overlap_size(&[&plugin2]).unwrap());
1454
1455 assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1456
1457 assert_ne!(0, plugin1.overlap_size(&[&plugin2]).unwrap());
1458 }
1459
1460 #[test]
1461 fn overlap_size_should_return_0_when_there_is_no_overlap() {
1462 let mut plugin1 = Plugin::new(
1463 GameId::Skyrim,
1464 Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1465 );
1466 let mut plugin2 = Plugin::new(
1467 GameId::Skyrim,
1468 Path::new("testing-plugins/Skyrim/Data/Blank.esp"),
1469 );
1470
1471 assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
1472 assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
1473
1474 assert!(!plugin1.overlaps_with(&plugin2).unwrap());
1475 assert_eq!(0, plugin1.overlap_size(&[&plugin2]).unwrap());
1476 }
1477
1478 #[test]
1479 fn valid_light_form_id_range_should_be_empty() {
1480 let mut plugin = Plugin::new(
1481 GameId::Skyrim,
1482 Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esm"),
1483 );
1484 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1485
1486 let range = plugin.valid_light_form_id_range();
1487 assert_eq!(&0, range.start());
1488 assert_eq!(&0, range.end());
1489 }
1490
1491 #[test]
1492 fn is_valid_as_light_plugin_should_always_be_false() {
1493 let mut plugin = Plugin::new(
1494 GameId::Skyrim,
1495 Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esm"),
1496 );
1497 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1498 assert!(!plugin.is_valid_as_light_plugin().unwrap());
1499 }
1500 }
1501
1502 mod skyrimse {
1503 use super::*;
1504
1505 #[test]
1506 fn is_master_file_should_use_file_extension_and_flag() {
1507 let tmp_dir = tempdir().unwrap();
1508
1509 let master_flagged_esp = tmp_dir.path().join("Blank.esp");
1510 copy(
1511 Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1512 &master_flagged_esp,
1513 )
1514 .unwrap();
1515
1516 let plugin = Plugin::new(GameId::SkyrimSE, Path::new("Blank.esp"));
1517 assert!(!plugin.is_master_file());
1518
1519 let plugin = Plugin::new(GameId::SkyrimSE, Path::new("Blank.esm"));
1520 assert!(plugin.is_master_file());
1521
1522 let plugin = Plugin::new(GameId::SkyrimSE, Path::new("Blank.esl"));
1523 assert!(plugin.is_master_file());
1524
1525 let mut plugin = Plugin::new(
1526 GameId::SkyrimSE,
1527 Path::new("testing-plugins/Skyrim/Data/Blank.esp"),
1528 );
1529 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1530 assert!(!plugin.is_master_file());
1531
1532 let mut plugin = Plugin::new(GameId::SkyrimSE, &master_flagged_esp);
1533 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1534 assert!(plugin.is_master_file());
1535 }
1536
1537 #[test]
1538 fn is_light_plugin_should_be_true_for_plugins_with_an_esl_file_extension() {
1539 let plugin = Plugin::new(GameId::SkyrimSE, Path::new("Blank.esp"));
1540 assert!(!plugin.is_light_plugin());
1541 let plugin = Plugin::new(GameId::SkyrimSE, Path::new("Blank.esm"));
1542 assert!(!plugin.is_light_plugin());
1543 let plugin = Plugin::new(GameId::SkyrimSE, Path::new("Blank.esl"));
1544 assert!(plugin.is_light_plugin());
1545 }
1546
1547 #[test]
1548 fn is_light_plugin_should_be_true_for_a_ghosted_esl_file() {
1549 let plugin = Plugin::new(GameId::SkyrimSE, Path::new("Blank.esl.ghost"));
1550 assert!(plugin.is_light_plugin());
1551 }
1552
1553 #[test]
1554 fn is_light_plugin_should_be_true_for_an_esp_file_with_the_light_flag_set() {
1555 let tmp_dir = tempdir().unwrap();
1556
1557 let light_flagged_esp = tmp_dir.path().join("Blank.esp");
1558 copy(
1559 Path::new("testing-plugins/SkyrimSE/Data/Blank.esl"),
1560 &light_flagged_esp,
1561 )
1562 .unwrap();
1563
1564 let mut plugin = Plugin::new(GameId::SkyrimSE, &light_flagged_esp);
1565 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1566 assert!(plugin.is_light_plugin());
1567 assert!(!plugin.is_master_file());
1568 }
1569
1570 #[test]
1571 fn is_light_plugin_should_be_true_for_an_esm_file_with_the_light_flag_set() {
1572 let tmp_dir = tempdir().unwrap();
1573
1574 let light_flagged_esm = tmp_dir.path().join("Blank.esm");
1575 copy(
1576 Path::new("testing-plugins/SkyrimSE/Data/Blank.esl"),
1577 &light_flagged_esm,
1578 )
1579 .unwrap();
1580
1581 let mut plugin = Plugin::new(GameId::SkyrimSE, &light_flagged_esm);
1582 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1583 assert!(plugin.is_light_plugin());
1584 assert!(plugin.is_master_file());
1585 }
1586
1587 #[test]
1588 fn is_medium_plugin_should_be_false() {
1589 let plugin = Plugin::new(GameId::SkyrimSE, Path::new("Blank.esp"));
1590 assert!(!plugin.is_medium_plugin());
1591 let plugin = Plugin::new(GameId::SkyrimSE, Path::new("Blank.esm"));
1592 assert!(!plugin.is_medium_plugin());
1593 let plugin = Plugin::new(GameId::SkyrimSE, Path::new("Blank.esl"));
1594 assert!(!plugin.is_medium_plugin());
1595 }
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() {
1600 let mut plugin = Plugin::new(
1601 GameId::SkyrimSE,
1602 Path::new("testing-plugins/SkyrimSE/Data/Blank.esm"),
1603 );
1604
1605 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1606
1607 assert_eq!(0.94, plugin.header_version().unwrap());
1608 }
1609
1610 #[test]
1611 fn valid_light_form_id_range_should_be_0x800_to_0xfff_if_hedr_version_is_less_than_1_71() {
1612 let mut plugin = Plugin::new(
1613 GameId::SkyrimSE,
1614 Path::new("testing-plugins/SkyrimSE/Data/Blank - Master Dependent.esm"),
1615 );
1616 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1617
1618 let range = plugin.valid_light_form_id_range();
1619 assert_eq!(&0x800, range.start());
1620 assert_eq!(&0xFFF, range.end());
1621 }
1622
1623 #[test]
1624 fn valid_light_form_id_range_should_be_0_to_0xfff_if_hedr_version_is_1_71_or_greater() {
1625 let mut plugin = Plugin::new(
1626 GameId::SkyrimSE,
1627 Path::new("testing-plugins/SkyrimSE/Data/Blank - Master Dependent.esm"),
1628 );
1629 let mut bytes = read(plugin.path()).unwrap();
1630
1631 assert_eq!(0xD7, bytes[0x1E]);
1632 assert_eq!(0xA3, bytes[0x1F]);
1633 assert_eq!(0x70, bytes[0x20]);
1634 assert_eq!(0x3F, bytes[0x21]);
1635 bytes[0x1E] = 0x48;
1636 bytes[0x1F] = 0xE1;
1637 bytes[0x20] = 0xDA;
1638 bytes[0x21] = 0x3F;
1639
1640 assert!(plugin
1641 .parse_reader(Cursor::new(bytes), ParseOptions::whole_plugin())
1642 .is_ok());
1643
1644 let range = plugin.valid_light_form_id_range();
1645 assert_eq!(&0, range.start());
1646 assert_eq!(&0xFFF, range.end());
1647 }
1648
1649 #[test]
1650 fn is_valid_as_light_plugin_should_be_true_if_the_plugin_has_no_form_ids_outside_the_valid_range(
1651 ) {
1652 let mut plugin = Plugin::new(
1653 GameId::SkyrimSE,
1654 Path::new("testing-plugins/SkyrimSE/Data/Blank - Master Dependent.esm"),
1655 );
1656 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1657
1658 assert!(plugin.is_valid_as_light_plugin().unwrap());
1659 }
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(
1663 ) {
1664 let mut plugin = Plugin::new(
1665 GameId::SkyrimSE,
1666 Path::new("testing-plugins/SkyrimSE/Data/Blank - Master Dependent.esm"),
1667 );
1668 let mut bytes = read(plugin.path()).unwrap();
1669
1670 assert_eq!(0xF0, bytes[0x7A]);
1671 assert_eq!(0x0C, bytes[0x7B]);
1672 bytes[0x7A] = 0xFF;
1673 bytes[0x7B] = 0x07;
1674
1675 assert!(plugin
1676 .parse_reader(Cursor::new(bytes), ParseOptions::whole_plugin())
1677 .is_ok());
1678
1679 assert!(plugin.is_valid_as_light_plugin().unwrap());
1680 }
1681
1682 #[test]
1683 fn is_valid_as_light_plugin_should_be_false_if_the_plugin_has_a_new_form_id_greater_than_0xfff(
1684 ) {
1685 let mut plugin = Plugin::new(
1686 GameId::SkyrimSE,
1687 Path::new("testing-plugins/SkyrimSE/Data/Blank - Master Dependent.esm"),
1688 );
1689 let mut bytes = read(plugin.path()).unwrap();
1690
1691 assert_eq!(0xEB, bytes[0x386]);
1692 assert_eq!(0x0C, bytes[0x387]);
1693 bytes[0x386] = 0x00;
1694 bytes[0x387] = 0x10;
1695
1696 assert!(plugin
1697 .parse_reader(Cursor::new(bytes), ParseOptions::whole_plugin())
1698 .is_ok());
1699
1700 assert!(!plugin.is_valid_as_light_plugin().unwrap());
1701 }
1702 }
1703
1704 mod fallout3 {
1705 use super::super::*;
1706
1707 #[test]
1708 fn is_light_plugin_should_be_false() {
1709 let plugin = Plugin::new(GameId::Fallout3, Path::new("Blank.esp"));
1710 assert!(!plugin.is_light_plugin());
1711 let plugin = Plugin::new(GameId::Fallout3, Path::new("Blank.esm"));
1712 assert!(!plugin.is_light_plugin());
1713 let plugin = Plugin::new(GameId::Fallout3, Path::new("Blank.esl"));
1714 assert!(!plugin.is_light_plugin());
1715 }
1716
1717 #[test]
1718 fn is_medium_plugin_should_be_false() {
1719 let plugin = Plugin::new(GameId::Fallout3, Path::new("Blank.esp"));
1720 assert!(!plugin.is_medium_plugin());
1721 let plugin = Plugin::new(GameId::Fallout3, Path::new("Blank.esm"));
1722 assert!(!plugin.is_medium_plugin());
1723 let plugin = Plugin::new(GameId::Fallout3, Path::new("Blank.esl"));
1724 assert!(!plugin.is_medium_plugin());
1725 }
1726
1727 #[test]
1728 fn valid_light_form_id_range_should_be_empty() {
1729 let mut plugin = Plugin::new(
1730 GameId::Fallout3,
1731 Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esm"),
1732 );
1733 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1734
1735 let range = plugin.valid_light_form_id_range();
1736 assert_eq!(&0, range.start());
1737 assert_eq!(&0, range.end());
1738 }
1739
1740 #[test]
1741 fn is_valid_as_light_plugin_should_always_be_false() {
1742 let mut plugin = Plugin::new(
1743 GameId::Fallout3,
1744 Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esm"),
1745 );
1746 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1747 assert!(!plugin.is_valid_as_light_plugin().unwrap());
1748 }
1749 }
1750
1751 mod falloutnv {
1752 use super::super::*;
1753
1754 #[test]
1755 fn is_light_plugin_should_be_false() {
1756 let plugin = Plugin::new(GameId::FalloutNV, Path::new("Blank.esp"));
1757 assert!(!plugin.is_light_plugin());
1758 let plugin = Plugin::new(GameId::FalloutNV, Path::new("Blank.esm"));
1759 assert!(!plugin.is_light_plugin());
1760 let plugin = Plugin::new(GameId::FalloutNV, Path::new("Blank.esl"));
1761 assert!(!plugin.is_light_plugin());
1762 }
1763
1764 #[test]
1765 fn is_medium_plugin_should_be_false() {
1766 let plugin = Plugin::new(GameId::FalloutNV, Path::new("Blank.esp"));
1767 assert!(!plugin.is_medium_plugin());
1768 let plugin = Plugin::new(GameId::FalloutNV, Path::new("Blank.esm"));
1769 assert!(!plugin.is_medium_plugin());
1770 let plugin = Plugin::new(GameId::FalloutNV, Path::new("Blank.esl"));
1771 assert!(!plugin.is_medium_plugin());
1772 }
1773
1774 #[test]
1775 fn valid_light_form_id_range_should_be_empty() {
1776 let mut plugin = Plugin::new(
1777 GameId::Fallout3,
1778 Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esm"),
1779 );
1780 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1781
1782 let range = plugin.valid_light_form_id_range();
1783 assert_eq!(&0, range.start());
1784 assert_eq!(&0, range.end());
1785 }
1786
1787 #[test]
1788 fn is_valid_as_light_plugin_should_always_be_false() {
1789 let mut plugin = Plugin::new(
1790 GameId::FalloutNV,
1791 Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esm"),
1792 );
1793 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1794 assert!(!plugin.is_valid_as_light_plugin().unwrap());
1795 }
1796 }
1797
1798 mod fallout4 {
1799 use super::*;
1800
1801 #[test]
1802 fn is_master_file_should_use_file_extension_and_flag() {
1803 let tmp_dir = tempdir().unwrap();
1804
1805 let master_flagged_esp = tmp_dir.path().join("Blank.esp");
1806 copy(
1807 Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
1808 &master_flagged_esp,
1809 )
1810 .unwrap();
1811
1812 let plugin = Plugin::new(GameId::Fallout4, Path::new("Blank.esp"));
1813 assert!(!plugin.is_master_file());
1814
1815 let plugin = Plugin::new(GameId::Fallout4, Path::new("Blank.esm"));
1816 assert!(plugin.is_master_file());
1817
1818 let plugin = Plugin::new(GameId::Fallout4, Path::new("Blank.esl"));
1819 assert!(plugin.is_master_file());
1820
1821 let mut plugin = Plugin::new(
1822 GameId::Fallout4,
1823 Path::new("testing-plugins/Skyrim/Data/Blank.esp"),
1824 );
1825 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1826 assert!(!plugin.is_master_file());
1827
1828 let mut plugin = Plugin::new(GameId::Fallout4, &master_flagged_esp);
1829 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1830 assert!(plugin.is_master_file());
1831 }
1832
1833 #[test]
1834 fn is_light_plugin_should_be_true_for_plugins_with_an_esl_file_extension() {
1835 let plugin = Plugin::new(GameId::Fallout4, Path::new("Blank.esp"));
1836 assert!(!plugin.is_light_plugin());
1837 let plugin = Plugin::new(GameId::Fallout4, Path::new("Blank.esm"));
1838 assert!(!plugin.is_light_plugin());
1839 let plugin = Plugin::new(GameId::Fallout4, Path::new("Blank.esl"));
1840 assert!(plugin.is_light_plugin());
1841 }
1842
1843 #[test]
1844 fn is_light_plugin_should_be_true_for_a_ghosted_esl_file() {
1845 let plugin = Plugin::new(GameId::Fallout4, Path::new("Blank.esl.ghost"));
1846 assert!(plugin.is_light_plugin());
1847 }
1848
1849 #[test]
1850 fn is_medium_plugin_should_be_false() {
1851 let plugin = Plugin::new(GameId::Fallout4, Path::new("Blank.esp"));
1852 assert!(!plugin.is_medium_plugin());
1853 let plugin = Plugin::new(GameId::Fallout4, Path::new("Blank.esm"));
1854 assert!(!plugin.is_medium_plugin());
1855 let plugin = Plugin::new(GameId::Fallout4, Path::new("Blank.esl"));
1856 assert!(!plugin.is_medium_plugin());
1857 }
1858
1859 #[test]
1860 fn valid_light_form_id_range_should_be_1_to_0xfff_if_hedr_version_is_less_than_1() {
1861 let mut plugin = Plugin::new(
1862 GameId::Fallout4,
1863 Path::new("testing-plugins/SkyrimSE/Data/Blank - Master Dependent.esm"),
1864 );
1865 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1866
1867 let range = plugin.valid_light_form_id_range();
1868 assert_eq!(&0x800, range.start());
1869 assert_eq!(&0xFFF, range.end());
1870 }
1871
1872 #[test]
1873 fn valid_light_form_id_range_should_be_1_to_0xfff_if_hedr_version_is_1_or_greater() {
1874 let mut plugin = Plugin::new(
1875 GameId::Fallout4,
1876 Path::new("testing-plugins/SkyrimSE/Data/Blank - Master Dependent.esm"),
1877 );
1878 let mut bytes = read(plugin.path()).unwrap();
1879
1880 assert_eq!(0xD7, bytes[0x1E]);
1881 assert_eq!(0xA3, bytes[0x1F]);
1882 assert_eq!(0x70, bytes[0x20]);
1883 assert_eq!(0x3F, bytes[0x21]);
1884 bytes[0x1E] = 0;
1885 bytes[0x1F] = 0;
1886 bytes[0x20] = 0x80;
1887 bytes[0x21] = 0x3F;
1888
1889 assert!(plugin
1890 .parse_reader(Cursor::new(bytes), ParseOptions::whole_plugin())
1891 .is_ok());
1892
1893 let range = plugin.valid_light_form_id_range();
1894 assert_eq!(&1, range.start());
1895 assert_eq!(&0xFFF, range.end());
1896 }
1897
1898 #[test]
1899 fn is_valid_as_light_plugin_should_be_true_if_the_plugin_has_no_form_ids_outside_the_valid_range(
1900 ) {
1901 let mut plugin = Plugin::new(
1902 GameId::Fallout4,
1903 Path::new("testing-plugins/SkyrimSE/Data/Blank - Master Dependent.esm"),
1904 );
1905 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1906
1907 assert!(plugin.is_valid_as_light_plugin().unwrap());
1908 }
1909 }
1910
1911 mod starfield {
1912 use super::*;
1913
1914 #[test]
1915 fn parse_file_should_succeed() {
1916 let mut plugin = Plugin::new(
1917 GameId::Starfield,
1918 Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
1919 );
1920
1921 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1922
1923 match plugin.data.record_ids {
1924 RecordIds::FormIds(ids) => assert_eq!(10, ids.len()),
1925 _ => panic!("Expected raw FormIDs"),
1926 }
1927 }
1928
1929 #[test]
1930 fn resolve_record_ids_should_resolve_unresolved_form_ids() {
1931 let mut plugin = Plugin::new(
1932 GameId::Starfield,
1933 Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
1934 );
1935
1936 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1937
1938 assert!(plugin.resolve_record_ids(&[]).is_ok());
1939
1940 match plugin.data.record_ids {
1941 RecordIds::Resolved(ids) => assert_eq!(10, ids.len()),
1942 _ => panic!("Expected resolved FormIDs"),
1943 }
1944 }
1945
1946 #[test]
1947 fn resolve_record_ids_should_do_nothing_if_form_ids_are_already_resolved() {
1948 let mut plugin = Plugin::new(
1949 GameId::Starfield,
1950 Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
1951 );
1952
1953 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
1954
1955 assert!(plugin.resolve_record_ids(&[]).is_ok());
1956
1957 let vec_ptr = match &plugin.data.record_ids {
1958 RecordIds::Resolved(ids) => ids.as_ptr(),
1959 _ => panic!("Expected resolved FormIDs"),
1960 };
1961
1962 assert!(plugin.resolve_record_ids(&[]).is_ok());
1963
1964 let vec_ptr_2 = match &plugin.data.record_ids {
1965 RecordIds::Resolved(ids) => ids.as_ptr(),
1966 _ => panic!("Expected resolved FormIDs"),
1967 };
1968
1969 assert_eq!(vec_ptr, vec_ptr_2);
1970 }
1971
1972 #[test]
1973 fn scale_should_return_full_for_a_full_plugin() {
1974 let mut plugin = Plugin::new(
1975 GameId::Starfield,
1976 Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
1977 );
1978 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1979
1980 assert_eq!(PluginScale::Full, plugin.scale());
1981 }
1982
1983 #[test]
1984 fn scale_should_return_medium_for_a_medium_plugin() {
1985 let mut plugin = Plugin::new(
1986 GameId::Starfield,
1987 Path::new("testing-plugins/Starfield/Data/Blank.medium.esm"),
1988 );
1989 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
1990
1991 assert_eq!(PluginScale::Medium, plugin.scale());
1992 }
1993
1994 #[test]
1995 fn scale_should_return_small_for_a_small_plugin() {
1996 let mut plugin = Plugin::new(
1997 GameId::Starfield,
1998 Path::new("testing-plugins/Starfield/Data/Blank.small.esm"),
1999 );
2000 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
2001
2002 assert_eq!(PluginScale::Small, plugin.scale());
2003 }
2004
2005 #[test]
2006 fn is_master_file_should_use_file_extension_and_flag() {
2007 let tmp_dir = tempdir().unwrap();
2008
2009 let master_flagged_esp = tmp_dir.path().join("Blank.esp");
2010 copy(
2011 Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
2012 &master_flagged_esp,
2013 )
2014 .unwrap();
2015
2016 let plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esp"));
2017 assert!(!plugin.is_master_file());
2018
2019 let plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esm"));
2020 assert!(plugin.is_master_file());
2021
2022 let plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esl"));
2023 assert!(plugin.is_master_file());
2024
2025 let mut plugin = Plugin::new(
2026 GameId::Starfield,
2027 Path::new("testing-plugins/Starfield/Data/Blank.esp"),
2028 );
2029 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
2030 assert!(!plugin.is_master_file());
2031
2032 let mut plugin = Plugin::new(GameId::Starfield, &master_flagged_esp);
2033 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
2034 assert!(plugin.is_master_file());
2035 }
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(
2039 ) {
2040 let plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esp"));
2042 assert!(!plugin.is_light_plugin());
2043 let plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esm"));
2044 assert!(!plugin.is_light_plugin());
2045 let plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esl"));
2046 assert!(plugin.is_light_plugin());
2047 }
2048
2049 #[test]
2050 fn is_light_plugin_should_be_false_for_a_plugin_with_the_update_flag_set_and_an_esl_extension(
2051 ) {
2052 let tmp_dir = tempdir().unwrap();
2053 let esl_path = tmp_dir.path().join("Blank.esl");
2054 copy(
2055 Path::new("testing-plugins/Starfield/Data/Blank - Override.esp"),
2056 &esl_path,
2057 )
2058 .unwrap();
2059
2060 let mut plugin = Plugin::new(GameId::Starfield, &esl_path);
2061 plugin.parse_file(ParseOptions::header_only()).unwrap();
2062
2063 assert!(!plugin.is_light_plugin());
2064 }
2065
2066 #[test]
2067 fn is_light_plugin_should_be_true_for_a_ghosted_esl_file() {
2068 let plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esl.ghost"));
2069 assert!(plugin.is_light_plugin());
2070 }
2071
2072 #[test]
2073 fn is_light_plugin_should_be_true_for_a_plugin_with_the_light_flag_set() {
2074 let mut plugin = Plugin::new(
2075 GameId::Starfield,
2076 Path::new("testing-plugins/Starfield/Data/Blank.small.esm"),
2077 );
2078 plugin.parse_file(ParseOptions::header_only()).unwrap();
2079
2080 assert!(plugin.is_light_plugin());
2081 }
2082
2083 #[test]
2084 fn is_medium_plugin_should_be_false_for_a_plugin_without_the_medium_flag_set() {
2085 let plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esp"));
2086 assert!(!plugin.is_medium_plugin());
2087 let plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esm"));
2088 assert!(!plugin.is_medium_plugin());
2089 let plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esl"));
2090 assert!(!plugin.is_medium_plugin());
2091 }
2092
2093 #[test]
2094 fn is_medium_plugin_should_be_true_for_a_plugin_with_the_medium_flag_set() {
2095 let mut plugin = Plugin::new(
2096 GameId::Starfield,
2097 Path::new("testing-plugins/Starfield/Data/Blank.medium.esm"),
2098 );
2099 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
2100 assert!(plugin.is_medium_plugin());
2101 }
2102
2103 #[test]
2104 fn is_medium_plugin_should_be_false_for_a_plugin_with_the_medium_and_light_flags_set() {
2105 let mut plugin = Plugin::new(
2106 GameId::Starfield,
2107 Path::new("testing-plugins/Starfield/Data/Blank.small.esm"),
2108 );
2109 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
2110 assert!(!plugin.is_medium_plugin());
2111 }
2112
2113 #[test]
2114 fn is_medium_plugin_should_be_false_for_an_esl_plugin_with_the_medium_flag_set() {
2115 let tmp_dir = tempdir().unwrap();
2116 let path = tmp_dir.path().join("Blank.esl");
2117 copy(
2118 Path::new("testing-plugins/Starfield/Data/Blank.medium.esm"),
2119 &path,
2120 )
2121 .unwrap();
2122
2123 let mut plugin = Plugin::new(GameId::Starfield, &path);
2124 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
2125 assert!(!plugin.is_medium_plugin());
2126 }
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(
2130 ) {
2131 let mut plugin = Plugin::new(
2132 GameId::Starfield,
2133 Path::new("testing-plugins/Starfield/Data/Blank - Override.full.esm"),
2134 );
2135 plugin.parse_file(ParseOptions::header_only()).unwrap();
2136
2137 assert!(plugin.is_update_plugin());
2138 }
2139
2140 #[test]
2141 fn is_update_plugin_should_be_false_for_a_plugin_with_the_update_flag_set_and_no_masters() {
2142 let mut plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esm"));
2143 let file_data = &[
2144 0x54, 0x45, 0x53, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
2145 0x00, 0x00, 0xB2, 0x2E, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00,
2146 ];
2147 plugin.data.header_record =
2148 Record::read(&mut Cursor::new(file_data), GameId::Starfield, b"TES4").unwrap();
2149
2150 assert!(!plugin.is_update_plugin());
2151 }
2152
2153 #[test]
2154 fn is_update_plugin_should_be_false_for_a_plugin_with_the_update_and_light_flags_set() {
2155 let mut plugin = Plugin::new(
2156 GameId::Starfield,
2157 Path::new("testing-plugins/Starfield/Data/Blank - Override.small.esm"),
2158 );
2159 plugin.parse_file(ParseOptions::header_only()).unwrap();
2160
2161 assert!(!plugin.is_update_plugin());
2162 }
2163
2164 #[test]
2165 fn is_update_plugin_should_be_false_for_a_plugin_with_the_update_and_medium_flags_set() {
2166 let mut plugin = Plugin::new(
2167 GameId::Starfield,
2168 Path::new("testing-plugins/Starfield/Data/Blank - Override.medium.esm"),
2169 );
2170 plugin.parse_file(ParseOptions::header_only()).unwrap();
2171
2172 assert!(!plugin.is_update_plugin());
2173 }
2174
2175 #[test]
2176 fn is_blueprint_plugin_should_be_false_for_a_plugin_without_the_blueprint_flag_set() {
2177 let mut plugin = Plugin::new(
2178 GameId::Starfield,
2179 Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
2180 );
2181 plugin.parse_file(ParseOptions::header_only()).unwrap();
2182
2183 assert!(!plugin.is_blueprint_plugin());
2184 }
2185
2186 #[test]
2187 fn is_blueprint_plugin_should_be_false_for_a_plugin_with_the_blueprint_flag_set() {
2188 let mut plugin = Plugin::new(
2189 GameId::Starfield,
2190 Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
2191 );
2192
2193 let mut bytes = read(plugin.path()).unwrap();
2194
2195 assert_eq!(0, bytes[0x09]);
2196 bytes[0x09] = 8;
2197
2198 assert!(plugin
2199 .parse_reader(Cursor::new(bytes), ParseOptions::header_only())
2200 .is_ok());
2201
2202 assert!(plugin.is_blueprint_plugin());
2203 }
2204
2205 #[test]
2206 fn count_override_records_should_error_if_form_ids_are_unresolved() {
2207 let mut plugin = Plugin::new(
2208 GameId::Starfield,
2209 Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
2210 );
2211
2212 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
2213
2214 match plugin.count_override_records().unwrap_err() {
2215 Error::UnresolvedRecordIds(path) => assert_eq!(plugin.path, path),
2216 _ => panic!("Expected unresolved FormIDs error"),
2217 }
2218 }
2219
2220 #[test]
2221 fn count_override_records_should_succeed_if_form_ids_are_resolved() {
2222 let mut plugin = Plugin::new(
2223 GameId::Starfield,
2224 Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
2225 );
2226
2227 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
2228
2229 assert!(plugin.resolve_record_ids(&[]).is_ok());
2230
2231 assert_eq!(0, plugin.count_override_records().unwrap());
2232 }
2233
2234 #[test]
2235 fn overlaps_with_should_error_if_form_ids_in_self_are_unresolved() {
2236 let mut plugin1 = Plugin::new(
2237 GameId::Starfield,
2238 Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
2239 );
2240 let mut plugin2 = Plugin::new(
2241 GameId::Starfield,
2242 Path::new("testing-plugins/Starfield/Data/Blank - Override.esp"),
2243 );
2244 let plugin1_metadata = PluginMetadata {
2245 filename: plugin1.filename().unwrap(),
2246 scale: plugin1.scale(),
2247 record_ids: Box::new([]),
2248 };
2249
2250 assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
2251 assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
2252 assert!(plugin2.resolve_record_ids(&[plugin1_metadata]).is_ok());
2253
2254 match plugin1.overlaps_with(&plugin2).unwrap_err() {
2255 Error::UnresolvedRecordIds(path) => assert_eq!(plugin1.path, path),
2256 _ => panic!("Expected unresolved FormIDs error"),
2257 }
2258 }
2259
2260 #[test]
2261 fn overlaps_with_should_error_if_form_ids_in_other_are_unresolved() {
2262 let mut plugin1 = Plugin::new(
2263 GameId::Starfield,
2264 Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
2265 );
2266 let mut plugin2 = Plugin::new(
2267 GameId::Starfield,
2268 Path::new("testing-plugins/Starfield/Data/Blank - Override.esp"),
2269 );
2270
2271 assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
2272 assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
2273 assert!(plugin1.resolve_record_ids(&[]).is_ok());
2274
2275 match plugin1.overlaps_with(&plugin2).unwrap_err() {
2276 Error::UnresolvedRecordIds(path) => assert_eq!(plugin2.path, path),
2277 _ => panic!("Expected unresolved FormIDs error"),
2278 }
2279 }
2280
2281 #[test]
2282 fn overlaps_with_should_succeed_if_form_ids_are_resolved() {
2283 let mut plugin1 = Plugin::new(
2284 GameId::Starfield,
2285 Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
2286 );
2287 let mut plugin2 = Plugin::new(
2288 GameId::Starfield,
2289 Path::new("testing-plugins/Starfield/Data/Blank - Override.esp"),
2290 );
2291 let plugin1_metadata = PluginMetadata {
2292 filename: plugin1.filename().unwrap(),
2293 scale: plugin1.scale(),
2294 record_ids: Box::new([]),
2295 };
2296
2297 assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
2298 assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
2299 assert!(plugin1.resolve_record_ids(&[]).is_ok());
2300 assert!(plugin2.resolve_record_ids(&[plugin1_metadata]).is_ok());
2301
2302 assert!(plugin1.overlaps_with(&plugin2).unwrap());
2303 }
2304
2305 #[test]
2306 fn overlap_size_should_error_if_form_ids_in_self_are_unresolved() {
2307 let mut plugin1 = Plugin::new(
2308 GameId::Starfield,
2309 Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
2310 );
2311 let mut plugin2 = Plugin::new(
2312 GameId::Starfield,
2313 Path::new("testing-plugins/Starfield/Data/Blank - Override.esp"),
2314 );
2315 let plugin1_metadata = PluginMetadata {
2316 filename: plugin1.filename().unwrap(),
2317 scale: plugin1.scale(),
2318 record_ids: Box::new([]),
2319 };
2320
2321 assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
2322 assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
2323 assert!(plugin2.resolve_record_ids(&[plugin1_metadata]).is_ok());
2324
2325 match plugin1.overlap_size(&[&plugin2]).unwrap_err() {
2326 Error::UnresolvedRecordIds(path) => assert_eq!(plugin1.path, path),
2327 _ => panic!("Expected unresolved FormIDs error"),
2328 }
2329 }
2330
2331 #[test]
2332 fn overlap_size_should_error_if_form_ids_in_other_are_unresolved() {
2333 let mut plugin1 = Plugin::new(
2334 GameId::Starfield,
2335 Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
2336 );
2337 let mut plugin2 = Plugin::new(
2338 GameId::Starfield,
2339 Path::new("testing-plugins/Starfield/Data/Blank - Override.esp"),
2340 );
2341
2342 assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
2343 assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
2344 assert!(plugin1.resolve_record_ids(&[]).is_ok());
2345
2346 match plugin1.overlap_size(&[&plugin2]).unwrap_err() {
2347 Error::UnresolvedRecordIds(path) => assert_eq!(plugin2.path, path),
2348 _ => panic!("Expected unresolved FormIDs error"),
2349 }
2350 }
2351
2352 #[test]
2353 fn overlap_size_should_succeed_if_form_ids_are_resolved() {
2354 let mut plugin1 = Plugin::new(
2355 GameId::Starfield,
2356 Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
2357 );
2358 let mut plugin2 = Plugin::new(
2359 GameId::Starfield,
2360 Path::new("testing-plugins/Starfield/Data/Blank - Override.esp"),
2361 );
2362 let plugin1_metadata = PluginMetadata {
2363 filename: plugin1.filename().unwrap(),
2364 scale: plugin1.scale(),
2365 record_ids: Box::new([]),
2366 };
2367
2368 assert!(plugin1.parse_file(ParseOptions::whole_plugin()).is_ok());
2369 assert!(plugin2.parse_file(ParseOptions::whole_plugin()).is_ok());
2370 assert!(plugin1.resolve_record_ids(&[]).is_ok());
2371 assert!(plugin2.resolve_record_ids(&[plugin1_metadata]).is_ok());
2372
2373 assert_eq!(1, plugin1.overlap_size(&[&plugin2]).unwrap());
2374 }
2375
2376 #[test]
2377 fn valid_light_form_id_range_should_be_0_to_0xfff() {
2378 let plugin = Plugin::new(GameId::Starfield, Path::new("Blank.esp"));
2379
2380 let range = plugin.valid_light_form_id_range();
2381 assert_eq!(&0, range.start());
2382 assert_eq!(&0xFFF, range.end());
2383 }
2384
2385 #[test]
2386 fn valid_medium_form_id_range_should_be_0_to_0xffff() {
2387 let mut plugin = Plugin::new(
2388 GameId::Starfield,
2389 Path::new("testing-plugins/Starfield/Data/Blank.small.esm"),
2390 );
2391 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
2392
2393 let range = plugin.valid_medium_form_id_range();
2394 assert_eq!(&0, range.start());
2395 assert_eq!(&0xFFFF, range.end());
2396 }
2397
2398 #[test]
2399 fn is_valid_as_light_plugin_should_be_false_if_form_ids_are_unresolved() {
2400 let mut plugin = Plugin::new(
2401 GameId::Starfield,
2402 Path::new("testing-plugins/Starfield/Data/Blank.small.esm"),
2403 );
2404 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
2405
2406 match plugin.is_valid_as_light_plugin().unwrap_err() {
2407 Error::UnresolvedRecordIds(path) => assert_eq!(plugin.path, path),
2408 _ => panic!("Expected unresolved FormIDs error"),
2409 }
2410 }
2411
2412 #[test]
2413 fn is_valid_as_light_plugin_should_be_true_if_the_plugin_has_no_form_ids_outside_the_valid_range(
2414 ) {
2415 let mut plugin = Plugin::new(
2416 GameId::Starfield,
2417 Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
2418 );
2419 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
2420 assert!(plugin.resolve_record_ids(&[]).is_ok());
2421
2422 assert!(plugin.is_valid_as_light_plugin().unwrap());
2423 }
2424
2425 #[test]
2426 fn is_valid_as_medium_plugin_should_be_false_if_form_ids_are_unresolved() {
2427 let mut plugin = Plugin::new(
2428 GameId::Starfield,
2429 Path::new("testing-plugins/Starfield/Data/Blank.medium.esm"),
2430 );
2431 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
2432
2433 match plugin.is_valid_as_medium_plugin().unwrap_err() {
2434 Error::UnresolvedRecordIds(path) => assert_eq!(plugin.path, path),
2435 _ => panic!("Expected unresolved FormIDs error"),
2436 }
2437 }
2438
2439 #[test]
2440 fn is_valid_as_medium_plugin_should_be_true_if_the_plugin_has_no_form_ids_outside_the_valid_range(
2441 ) {
2442 let mut plugin = Plugin::new(
2443 GameId::Starfield,
2444 Path::new("testing-plugins/Starfield/Data/Blank.medium.esm"),
2445 );
2446 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
2447 assert!(plugin.resolve_record_ids(&[]).is_ok());
2448
2449 assert!(plugin.is_valid_as_medium_plugin().unwrap());
2450 }
2451
2452 #[test]
2453 fn is_valid_as_update_plugin_should_be_false_if_form_ids_are_unresolved() {
2454 let mut plugin = Plugin::new(
2455 GameId::Starfield,
2456 Path::new("testing-plugins/Starfield/Data/Blank - Override.esp"),
2457 );
2458 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
2459
2460 match plugin.is_valid_as_update_plugin().unwrap_err() {
2461 Error::UnresolvedRecordIds(path) => assert_eq!(plugin.path, path),
2462 _ => panic!("Expected unresolved FormIDs error"),
2463 }
2464 }
2465
2466 #[test]
2467 fn is_valid_as_update_plugin_should_be_true_if_the_plugin_has_no_new_records() {
2468 let master_metadata = PluginMetadata {
2469 filename: "Blank.full.esm".to_owned(),
2470 scale: PluginScale::Full,
2471 record_ids: Box::new([]),
2472 };
2473 let mut plugin = Plugin::new(
2474 GameId::Starfield,
2475 Path::new("testing-plugins/Starfield/Data/Blank - Override.esp"),
2476 );
2477 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
2478 assert!(plugin.resolve_record_ids(&[master_metadata]).is_ok());
2479
2480 assert!(plugin.is_valid_as_update_plugin().unwrap());
2481 }
2482
2483 #[test]
2484 fn is_valid_as_update_plugin_should_be_false_if_the_plugin_has_new_records() {
2485 let master_metadata = PluginMetadata {
2486 filename: "Blank.full.esm".to_owned(),
2487 scale: PluginScale::Full,
2488 record_ids: Box::new([]),
2489 };
2490 let mut plugin = Plugin::new(
2491 GameId::Starfield,
2492 Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
2493 );
2494 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_ok());
2495 assert!(plugin.resolve_record_ids(&[master_metadata]).is_ok());
2496
2497 assert!(!plugin.is_valid_as_update_plugin().unwrap());
2498 }
2499
2500 #[test]
2501 fn plugins_metadata_should_return_plugin_names_and_scales() {
2502 let mut plugin1 = Plugin::new(
2503 GameId::Starfield,
2504 Path::new("testing-plugins/Starfield/Data/Blank.full.esm"),
2505 );
2506 let mut plugin2 = Plugin::new(
2507 GameId::Starfield,
2508 Path::new("testing-plugins/Starfield/Data/Blank.medium.esm"),
2509 );
2510 let mut plugin3 = Plugin::new(
2511 GameId::Starfield,
2512 Path::new("testing-plugins/Starfield/Data/Blank.small.esm"),
2513 );
2514 assert!(plugin1.parse_file(ParseOptions::header_only()).is_ok());
2515 assert!(plugin2.parse_file(ParseOptions::header_only()).is_ok());
2516 assert!(plugin3.parse_file(ParseOptions::header_only()).is_ok());
2517
2518 let metadata = plugins_metadata(&[&plugin1, &plugin2, &plugin3]).unwrap();
2519
2520 assert_eq!(
2521 vec![
2522 PluginMetadata {
2523 filename: "Blank.full.esm".to_owned(),
2524 scale: PluginScale::Full,
2525 record_ids: Box::new([]),
2526 },
2527 PluginMetadata {
2528 filename: "Blank.medium.esm".to_owned(),
2529 scale: PluginScale::Medium,
2530 record_ids: Box::new([]),
2531 },
2532 PluginMetadata {
2533 filename: "Blank.small.esm".to_owned(),
2534 scale: PluginScale::Small,
2535 record_ids: Box::new([]),
2536 },
2537 ],
2538 metadata
2539 );
2540 }
2541
2542 #[test]
2543 fn hashed_parent_should_use_full_object_index_mask_for_games_other_than_starfield() {
2544 let metadata = PluginMetadata {
2545 filename: "a".to_owned(),
2546 scale: PluginScale::Full,
2547 record_ids: Box::new([]),
2548 };
2549
2550 let plugin = hashed_parent(GameId::SkyrimSE, &metadata);
2551
2552 assert_eq!(u32::from(ObjectIndexMask::Full), plugin.mod_index_mask);
2553 assert_eq!(u32::from(ObjectIndexMask::Full), plugin.object_index_mask);
2554
2555 let metadata = PluginMetadata {
2556 filename: "a".to_owned(),
2557 scale: PluginScale::Medium,
2558 record_ids: Box::new([]),
2559 };
2560
2561 let plugin = hashed_parent(GameId::SkyrimSE, &metadata);
2562
2563 assert_eq!(u32::from(ObjectIndexMask::Full), plugin.mod_index_mask);
2564 assert_eq!(u32::from(ObjectIndexMask::Full), plugin.object_index_mask);
2565
2566 let metadata = PluginMetadata {
2567 filename: "a".to_owned(),
2568 scale: PluginScale::Small,
2569 record_ids: Box::new([]),
2570 };
2571
2572 let plugin = hashed_parent(GameId::SkyrimSE, &metadata);
2573
2574 assert_eq!(u32::from(ObjectIndexMask::Full), plugin.mod_index_mask);
2575 assert_eq!(u32::from(ObjectIndexMask::Full), plugin.object_index_mask);
2576 }
2577
2578 #[test]
2579 fn hashed_parent_should_use_object_index_mask_matching_the_plugin_scale_for_starfield() {
2580 let metadata = PluginMetadata {
2581 filename: "a".to_owned(),
2582 scale: PluginScale::Full,
2583 record_ids: Box::new([]),
2584 };
2585
2586 let plugin = hashed_parent(GameId::Starfield, &metadata);
2587
2588 assert_eq!(u32::from(ObjectIndexMask::Full), plugin.mod_index_mask);
2589 assert_eq!(u32::from(ObjectIndexMask::Full), plugin.object_index_mask);
2590
2591 let metadata = PluginMetadata {
2592 filename: "a".to_owned(),
2593 scale: PluginScale::Medium,
2594 record_ids: Box::new([]),
2595 };
2596
2597 let plugin = hashed_parent(GameId::Starfield, &metadata);
2598
2599 assert_eq!(u32::from(ObjectIndexMask::Medium), plugin.mod_index_mask);
2600 assert_eq!(u32::from(ObjectIndexMask::Medium), plugin.object_index_mask);
2601
2602 let metadata = PluginMetadata {
2603 filename: "a".to_owned(),
2604 scale: PluginScale::Small,
2605 record_ids: Box::new([]),
2606 };
2607
2608 let plugin = hashed_parent(GameId::Starfield, &metadata);
2609
2610 assert_eq!(u32::from(ObjectIndexMask::Small), plugin.mod_index_mask);
2611 assert_eq!(u32::from(ObjectIndexMask::Small), plugin.object_index_mask);
2612 }
2613
2614 #[test]
2615 fn hashed_masters_should_use_vec_index_as_mod_index() {
2616 let masters = &["a".to_owned(), "b".to_owned(), "c".to_owned()];
2617 let hashed_masters = hashed_masters(masters);
2618
2619 assert_eq!(
2620 vec![
2621 SourcePlugin::master("a", 0, ObjectIndexMask::Full),
2622 SourcePlugin::master("b", 0x0100_0000, ObjectIndexMask::Full),
2623 SourcePlugin::master("c", 0x0200_0000, ObjectIndexMask::Full),
2624 ],
2625 hashed_masters
2626 );
2627 }
2628
2629 #[test]
2630 fn hashed_masters_for_starfield_should_error_if_it_cannot_find_a_masters_metadata() {
2631 let masters = &["a".to_owned(), "b".to_owned(), "c".to_owned()];
2632 let metadata = &[
2633 PluginMetadata {
2634 filename: masters[0].clone(),
2635 scale: PluginScale::Full,
2636 record_ids: Box::new([]),
2637 },
2638 PluginMetadata {
2639 filename: masters[1].clone(),
2640 scale: PluginScale::Full,
2641 record_ids: Box::new([]),
2642 },
2643 ];
2644
2645 match hashed_masters_for_starfield(masters, metadata).unwrap_err() {
2646 Error::PluginMetadataNotFound(master) => assert_eq!(masters[2], master),
2647 _ => panic!("Expected plugin metadata not found error"),
2648 }
2649 }
2650
2651 #[test]
2652 fn hashed_masters_for_starfield_should_match_names_to_metadata_case_insensitively() {
2653 let masters = &["a".to_owned()];
2654 let metadata = &[PluginMetadata {
2655 filename: "A".to_owned(),
2656 scale: PluginScale::Full,
2657 record_ids: Box::new([]),
2658 }];
2659
2660 let hashed_masters = hashed_masters_for_starfield(masters, metadata).unwrap();
2661
2662 assert_eq!(
2663 vec![SourcePlugin::master(&masters[0], 0, ObjectIndexMask::Full),],
2664 hashed_masters
2665 );
2666 }
2667
2668 #[test]
2669 fn hashed_masters_for_starfield_should_count_mod_indexes_separately_for_different_plugin_scales(
2670 ) {
2671 let masters: Vec<_> = (0u8..7u8).map(|i| i.to_string()).collect();
2672 let metadata = &[
2673 PluginMetadata {
2674 filename: masters[0].clone(),
2675 scale: PluginScale::Full,
2676 record_ids: Box::new([]),
2677 },
2678 PluginMetadata {
2679 filename: masters[1].clone(),
2680 scale: PluginScale::Medium,
2681 record_ids: Box::new([]),
2682 },
2683 PluginMetadata {
2684 filename: masters[2].clone(),
2685 scale: PluginScale::Small,
2686 record_ids: Box::new([]),
2687 },
2688 PluginMetadata {
2689 filename: masters[3].clone(),
2690 scale: PluginScale::Medium,
2691 record_ids: Box::new([]),
2692 },
2693 PluginMetadata {
2694 filename: masters[4].clone(),
2695 scale: PluginScale::Full,
2696 record_ids: Box::new([]),
2697 },
2698 PluginMetadata {
2699 filename: masters[5].clone(),
2700 scale: PluginScale::Small,
2701 record_ids: Box::new([]),
2702 },
2703 PluginMetadata {
2704 filename: masters[6].clone(),
2705 scale: PluginScale::Small,
2706 record_ids: Box::new([]),
2707 },
2708 ];
2709
2710 let hashed_masters = hashed_masters_for_starfield(&masters, metadata).unwrap();
2711
2712 assert_eq!(
2713 vec![
2714 SourcePlugin::master(&masters[0], 0, ObjectIndexMask::Full),
2715 SourcePlugin::master(&masters[1], 0xFD00_0000, ObjectIndexMask::Medium),
2716 SourcePlugin::master(&masters[2], 0xFE00_0000, ObjectIndexMask::Small),
2717 SourcePlugin::master(&masters[3], 0xFD01_0000, ObjectIndexMask::Medium),
2718 SourcePlugin::master(&masters[4], 0x0100_0000, ObjectIndexMask::Full),
2719 SourcePlugin::master(&masters[5], 0xFE00_1000, ObjectIndexMask::Small),
2720 SourcePlugin::master(&masters[6], 0xFE00_2000, ObjectIndexMask::Small),
2721 ],
2722 hashed_masters
2723 );
2724 }
2725 }
2726
2727 fn write_invalid_plugin(path: &Path) {
2728 use std::io::Write;
2729 let mut file = File::create(path).unwrap();
2730 let bytes = [0; MAX_RECORD_HEADER_LENGTH];
2731 file.write_all(&bytes).unwrap();
2732 }
2733
2734 #[test]
2735 fn parse_file_should_error_if_plugin_does_not_exist() {
2736 let mut plugin = Plugin::new(GameId::Skyrim, Path::new("Blank.esm"));
2737
2738 assert!(plugin.parse_file(ParseOptions::whole_plugin()).is_err());
2739 }
2740
2741 #[test]
2742 fn parse_file_should_error_if_plugin_is_not_valid() {
2743 let mut plugin = Plugin::new(
2744 GameId::Oblivion,
2745 Path::new("testing-plugins/Oblivion/Data/Blank.bsa"),
2746 );
2747
2748 let result = plugin.parse_file(ParseOptions::whole_plugin());
2749 assert!(result.is_err());
2750 assert_eq!(
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\"",
2752 result.unwrap_err().to_string()
2753 );
2754 }
2755
2756 #[test]
2757 fn parse_file_header_only_should_fail_for_a_non_plugin_file() {
2758 let tmp_dir = tempdir().unwrap();
2759
2760 let path = tmp_dir.path().join("Invalid.esm");
2761 write_invalid_plugin(&path);
2762
2763 let mut plugin = Plugin::new(GameId::Skyrim, &path);
2764
2765 let result = plugin.parse_file(ParseOptions::header_only());
2766 assert!(result.is_err());
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());
2768 }
2769
2770 #[test]
2771 fn parse_file_should_fail_for_a_non_plugin_file() {
2772 let tmp_dir = tempdir().unwrap();
2773
2774 let path = tmp_dir.path().join("Invalid.esm");
2775 write_invalid_plugin(&path);
2776
2777 let mut plugin = Plugin::new(GameId::Skyrim, &path);
2778
2779 let result = plugin.parse_file(ParseOptions::whole_plugin());
2780 assert!(result.is_err());
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());
2782 }
2783
2784 #[test]
2785 fn is_valid_should_return_true_for_a_valid_plugin() {
2786 let is_valid = Plugin::is_valid(
2787 GameId::Skyrim,
2788 Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
2789 ParseOptions::header_only(),
2790 );
2791
2792 assert!(is_valid);
2793 }
2794
2795 #[test]
2796 fn is_valid_should_return_false_for_an_invalid_plugin() {
2797 let is_valid = Plugin::is_valid(
2798 GameId::Skyrim,
2799 Path::new("testing-plugins/Oblivion/Data/Blank.bsa"),
2800 ParseOptions::header_only(),
2801 );
2802
2803 assert!(!is_valid);
2804 }
2805
2806 #[test]
2807 fn path_should_return_the_full_plugin_path() {
2808 let path = Path::new("Data/Blank.esm");
2809 let plugin = Plugin::new(GameId::Skyrim, path);
2810
2811 assert_eq!(path, plugin.path());
2812 }
2813
2814 #[test]
2815 fn filename_should_return_filename_in_given_path() {
2816 let plugin = Plugin::new(GameId::Skyrim, Path::new("Blank.esm"));
2817
2818 assert_eq!("Blank.esm", plugin.filename().unwrap());
2819
2820 let plugin = Plugin::new(GameId::Skyrim, Path::new("Blank.esp"));
2821
2822 assert_eq!("Blank.esp", plugin.filename().unwrap());
2823 }
2824
2825 #[test]
2826 fn filename_should_not_trim_dot_ghost_extension() {
2827 let plugin = Plugin::new(GameId::Skyrim, Path::new("Blank.esp.ghost"));
2828
2829 assert_eq!("Blank.esp.ghost", plugin.filename().unwrap());
2830 }
2831
2832 #[test]
2833 fn masters_should_be_empty_for_a_plugin_with_no_masters() {
2834 let mut plugin = Plugin::new(
2835 GameId::Skyrim,
2836 Path::new("testing-plugins/Skyrim/Data/Blank.esm"),
2837 );
2838
2839 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
2840 assert_eq!(0, plugin.masters().unwrap().len());
2841 }
2842
2843 #[test]
2844 fn masters_should_not_be_empty_for_a_plugin_with_one_or_more_masters() {
2845 let mut plugin = Plugin::new(
2846 GameId::Skyrim,
2847 Path::new(
2848 "testing-plugins/Skyrim/Data/Blank - \
2849 Master Dependent.esm",
2850 ),
2851 );
2852
2853 assert!(plugin.parse_file(ParseOptions::header_only()).is_ok());
2854
2855 let masters = plugin.masters().unwrap();
2856 assert_eq!(1, masters.len());
2857 assert_eq!("Blank.esm", masters[0]);
2858 }
2859
2860 #[test]
2861 fn masters_should_only_read_up_to_the_first_null_for_each_master() {
2862 let mut plugin = Plugin::new(
2863 GameId::Skyrim,
2864 Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esm"),
2865 );
2866
2867 let mut bytes = read(plugin.path()).unwrap();
2868
2869 assert_eq!(0x2E, bytes[0x43]);
2870 bytes[0x43] = 0;
2871
2872 assert!(plugin
2873 .parse_reader(Cursor::new(bytes), ParseOptions::whole_plugin())
2874 .is_ok());
2875
2876 assert_eq!("Blank", plugin.masters().unwrap()[0]);
2877 }
2878
2879 #[test]
2880 fn description_should_error_for_a_plugin_header_subrecord_that_is_too_small() {
2881 let mut plugin = Plugin::new(
2882 GameId::Morrowind,
2883 Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
2884 );
2885
2886 let mut data =
2887 include_bytes!("../testing-plugins/Morrowind/Data Files/Blank.esm")[..0x20].to_vec();
2888 data[0x04] = 16;
2889 data[0x05] = 0;
2890 data[0x14] = 8;
2891 data[0x15] = 0;
2892
2893 assert!(plugin
2894 .parse_reader(Cursor::new(data), ParseOptions::header_only())
2895 .is_ok());
2896
2897 let result = plugin.description();
2898 assert!(result.is_err());
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());
2900 }
2901
2902 #[test]
2903 fn header_version_should_be_none_for_a_plugin_header_hedr_subrecord_that_is_too_small() {
2904 let mut plugin = Plugin::new(
2905 GameId::Morrowind,
2906 Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
2907 );
2908
2909 let mut data =
2910 include_bytes!("../testing-plugins/Morrowind/Data Files/Blank.esm")[..0x1B].to_vec();
2911 data[0x04] = 11;
2912 data[0x05] = 0;
2913 data[0x14] = 3;
2914 data[0x15] = 0;
2915
2916 assert!(plugin
2917 .parse_reader(Cursor::new(data), ParseOptions::header_only())
2918 .is_ok());
2919 assert!(plugin.header_version().is_none());
2920 }
2921
2922 #[test]
2923 fn record_and_group_count_should_be_none_for_a_plugin_hedr_subrecord_that_is_too_small() {
2924 let mut plugin = Plugin::new(
2925 GameId::Morrowind,
2926 Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"),
2927 );
2928
2929 let mut data =
2930 include_bytes!("../testing-plugins/Morrowind/Data Files/Blank.esm")[..0x140].to_vec();
2931 data[0x04] = 0x30;
2932 data[0x14] = 0x28;
2933
2934 assert!(plugin
2935 .parse_reader(Cursor::new(data), ParseOptions::header_only())
2936 .is_ok());
2937 assert!(plugin.record_and_group_count().is_none());
2938 }
2939
2940 #[test]
2941 fn resolve_form_ids_should_use_plugin_names_case_insensitively() {
2942 let raw_form_ids = vec![0x0000_0001, 0x0100_0002];
2943
2944 let masters = vec!["t\u{e9}st.esm".to_owned()];
2945 let form_ids = resolve_form_ids(
2946 GameId::SkyrimSE,
2947 &raw_form_ids,
2948 &PluginMetadata {
2949 filename: "Bl\u{e0}\u{f1}k.esp".to_owned(),
2950 scale: PluginScale::Full,
2951 record_ids: Box::new([]),
2952 },
2953 &masters,
2954 &[],
2955 )
2956 .unwrap();
2957
2958 let other_masters = vec!["T\u{c9}ST.ESM".to_owned()];
2959 let other_form_ids = resolve_form_ids(
2960 GameId::SkyrimSE,
2961 &raw_form_ids,
2962 &PluginMetadata {
2963 filename: "BL\u{c0}\u{d1}K.ESP".to_owned(),
2964 scale: PluginScale::Full,
2965 record_ids: Box::new([]),
2966 },
2967 &other_masters,
2968 &[],
2969 )
2970 .unwrap();
2971
2972 assert_eq!(form_ids, other_form_ids);
2973 }
2974}