diff --git a/benches/parsing.rs b/benches/parsing.rs index f63f785..70d9683 100644 --- a/benches/parsing.rs +++ b/benches/parsing.rs @@ -5,7 +5,7 @@ extern crate esplugin; use std::path::Path; use criterion::Criterion; -use esplugin::{GameId, Plugin}; +use esplugin::{GameId, ParseMode, Plugin}; // HearthFires.esm is a 3.8 MB file, so it's got plenty of content without being // large enough to slow down benchmarking much. @@ -19,22 +19,30 @@ fn criterion_benchmark(c: &mut Criterion) { let mut plugin = Plugin::new(GAME_ID, Path::new(PLUGIN_TO_PARSE)); b.iter(|| { - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); }); }); - c.bench_function("Plugin.parse_file() full", |b| { + c.bench_function("Plugin.parse_file() record ids", |b| { let mut plugin = Plugin::new(GAME_ID, Path::new(PLUGIN_TO_PARSE)); b.iter(|| { - assert!(plugin.parse_file(false).is_ok()); + assert!(plugin.parse_file(ParseMode::RecordIds).is_ok()); + }); + }); + + c.bench_function("Plugin.parse_file() full with records", |b| { + let mut plugin = Plugin::new(GAME_ID, Path::new(PLUGIN_TO_PARSE)); + + b.iter(|| { + assert!(plugin.parse_file(ParseMode::All).is_ok()); }); }); c.bench_function("Plugin.overlaps_with()", |b| { let mut plugin = Plugin::new(GAME_ID, Path::new(PLUGIN_TO_PARSE)); - assert!(plugin.parse_file(false).is_ok()); + assert!(plugin.parse_file(ParseMode::RecordIds).is_ok()); b.iter(|| { assert!(plugin.overlaps_with(&plugin)); @@ -44,7 +52,7 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("Plugin.count_override_records()", |b| { let mut plugin = Plugin::new(GAME_ID, Path::new(PLUGIN_TO_PARSE)); - assert!(plugin.parse_file(false).is_ok()); + assert!(plugin.parse_file(ParseMode::RecordIds).is_ok()); b.iter(|| { assert_eq!(plugin.count_override_records(), 1272); diff --git a/ffi/src/plugin.rs b/ffi/src/plugin.rs index 69cde94..e2e02e6 100644 --- a/ffi/src/plugin.rs +++ b/ffi/src/plugin.rs @@ -4,7 +4,7 @@ use std::{f32, mem, panic, ptr}; use libc::{c_char, c_float, size_t}; use constants::*; -use esplugin::Plugin; +use esplugin::{ParseMode, Plugin}; use helpers::*; #[no_mangle] @@ -45,8 +45,13 @@ pub unsafe extern "C" fn esp_plugin_parse(plugin_ptr: *mut Plugin, load_header_o if plugin_ptr.is_null() { ESP_ERROR_NULL_POINTER } else { + let mode = if load_header_only { + ParseMode::HeaderOnly + } else { + ParseMode::RecordIds + }; let plugin = &mut *plugin_ptr; - match plugin.parse_file(load_header_only) { + match plugin.parse_file(mode) { Ok(_) => ESP_OK, Err(_) => ESP_ERROR_PARSE_ERROR, } @@ -177,7 +182,12 @@ pub unsafe extern "C" fn esp_plugin_is_valid( Err(x) => return x, }; - *is_valid = Plugin::is_valid(mapped_game_id, rust_path, load_header_only); + let mode = if load_header_only { + ParseMode::HeaderOnly + } else { + ParseMode::RecordIds + }; + *is_valid = Plugin::is_valid(mapped_game_id, rust_path, mode); ESP_OK } diff --git a/src/lib.rs b/src/lib.rs index 20679f8..fcf63e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,9 @@ use std::convert::TryInto; pub use crate::error::Error; pub use crate::game_id::GameId; -pub use crate::plugin::Plugin; +pub use crate::plugin::{ParseMode, Plugin, PluginEntry}; +pub use crate::record::{Record, RecordHeader}; +pub use crate::subrecord::Subrecord; mod error; mod form_id; diff --git a/src/plugin.rs b/src/plugin.rs index 82be8ff..c93e3a6 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -33,12 +33,25 @@ use crate::group::Group; use crate::record::Record; use crate::record_id::{NamespacedId, RecordId}; +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum ParseMode { + HeaderOnly, + RecordIds, + All, +} + #[derive(Copy, Clone, PartialEq, Eq)] enum FileExtension { ESM, ESL, } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum PluginEntry { + Record(Record), + Group(Group), +} + impl PartialEq<&std::borrow::Cow<'_, str>> for FileExtension { fn eq(&self, other: &&std::borrow::Cow<'_, str>) -> bool { const ESM: &str = "esm"; @@ -92,6 +105,7 @@ impl Default for RecordIds { struct PluginData { header_record: Record, record_ids: RecordIds, + entries: Vec, } #[derive(Clone, PartialEq, Eq, Debug, Hash)] @@ -110,11 +124,11 @@ impl Plugin { } } - pub fn parse(&mut self, input: &[u8], load_header_only: bool) -> Result<(), Error> { + pub fn parse(&mut self, input: &[u8], mode: ParseMode) -> Result<(), Error> { match self.filename() { None => Err(Error::NoFilename), Some(filename) => { - self.data = parse_plugin(input, self.game_id, &filename, load_header_only)?.1; + self.data = parse_plugin(input, self.game_id, &filename, mode)?.1; Ok(()) } @@ -124,7 +138,7 @@ impl Plugin { fn read( &mut self, mut reader: R, - load_header_only: bool, + mode: ParseMode, expected_header_type: &'static [u8], ) -> Result<(), Error> { match self.filename() { @@ -134,7 +148,7 @@ impl Plugin { &mut reader, self.game_id, &filename, - load_header_only, + mode, expected_header_type, )?; @@ -143,21 +157,21 @@ impl Plugin { } } - pub fn parse_open_file(&mut self, file: File, load_header_only: bool) -> Result<(), Error> { + pub fn parse_open_file(&mut self, file: File, mode: ParseMode) -> Result<(), Error> { let mut reader = BufReader::new(&file); - if load_header_only { + if mode == ParseMode::HeaderOnly { let content = Record::read_and_validate(&mut reader, self.game_id, self.header_type())?; - self.parse(&content, load_header_only) + self.parse(&content, mode) } else { - self.read(reader, load_header_only, self.header_type()) + self.read(reader, mode, self.header_type()) } } - pub fn parse_file(&mut self, load_header_only: bool) -> Result<(), Error> { + pub fn parse_file(&mut self, mode: ParseMode) -> Result<(), Error> { let file = File::open(&self.path)?; - self.parse_open_file(file, load_header_only) + self.parse_open_file(file, mode) } pub fn game_id(&self) -> &GameId { @@ -226,10 +240,10 @@ impl Plugin { } } - pub fn is_valid(game_id: GameId, filepath: &Path, load_header_only: bool) -> bool { + pub fn is_valid(game_id: GameId, filepath: &Path, mode: ParseMode) -> bool { let mut plugin = Plugin::new(game_id, &filepath.to_path_buf()); - plugin.parse_file(load_header_only).is_ok() + plugin.parse_file(mode).is_ok() } pub fn description(&self) -> Result, Error> { @@ -365,6 +379,14 @@ impl Plugin { fn is_light_flag_set(&self) -> bool { self.data.header_record.header().flags() & 0x200 != 0 } + + pub fn get_header_record(&self) -> &Record { + &self.data.header_record + } + + pub fn get_entries(&self) -> &Vec { + &self.data.entries + } } fn sorted_slices_intersect(left: &[T], right: &[T]) -> bool { @@ -467,6 +489,19 @@ fn parse_morrowind_record_ids<'a>(input: &'a [u8]) -> IResult<&'a [u8], RecordId Ok((remaining_input, record_ids.into())) } +fn parse_morrowind_records<'a>(input: &'a [u8]) -> IResult<&'a [u8], Vec> { + let mut records = Vec::new(); + let mut remaining_input = input; + + while !remaining_input.is_empty() { + let (input, record) = Record::parse(remaining_input, GameId::Morrowind, false)?; + remaining_input = input; + records.push(PluginEntry::Record(record)); + } + + Ok((remaining_input, records)) +} + fn read_morrowind_record_ids(reader: &mut R) -> Result { let mut record_ids = Vec::new(); let mut header_buf = [0; 16]; // Morrowind record headers are 16 bytes long. @@ -485,6 +520,36 @@ fn read_morrowind_record_ids(reader: &mut R) -> Result(reader: &mut R) -> Result, Error> { + let mut records = Vec::new(); + + while !reader.fill_buf()?.is_empty() { + let record = Record::read(reader, GameId::Morrowind, false)?; + + records.push(PluginEntry::Record(record)); + } + + Ok(records) +} + +fn parse_record_ids_from_entries(game_id: GameId, entries: &[PluginEntry]) -> RecordIds { + let mut record_ids = Vec::new(); + for entry in entries { + match entry { + PluginEntry::Record(record) => { + let record_id = record.parse_record_id_from_self(game_id); + + if let Some(RecordId::NamespacedId(record_id)) = record_id { + record_ids.push(record_id); + } + } + _ => unimplemented!(), + } + } + record_ids.sort(); + record_ids.into() +} + fn parse_record_ids<'a>( input: &'a [u8], game_id: GameId, @@ -507,6 +572,14 @@ fn parse_record_ids<'a>( } } +fn parse_entries<'a>(input: &'a [u8], game_id: GameId) -> IResult<&'a [u8], Vec> { + if game_id == GameId::Morrowind { + parse_morrowind_records(input) + } else { + unimplemented!() + } +} + fn read_record_ids( reader: &mut R, game_id: GameId, @@ -525,31 +598,51 @@ fn read_record_ids( } } +fn read_entries( + reader: &mut R, + game_id: GameId, +) -> Result, Error> { + if game_id == GameId::Morrowind { + read_morrowind_records(reader) + } else { + unimplemented!() + } +} + fn parse_plugin<'a>( input: &'a [u8], game_id: GameId, filename: &str, - load_header_only: bool, + mode: ParseMode, ) -> IResult<&'a [u8], PluginData> { let (input1, header_record) = Record::parse(input, game_id, false)?; - if load_header_only { + if mode == ParseMode::HeaderOnly { return Ok(( input1, PluginData { header_record, record_ids: RecordIds::None, + entries: vec![], }, )); } - let (input2, record_ids) = parse_record_ids(input1, game_id, &header_record, filename)?; + let (input2, entries, record_ids) = if game_id == GameId::Morrowind && mode == ParseMode::All { + let (input2, entries) = parse_entries(input1, game_id)?; + let record_ids = parse_record_ids_from_entries(game_id, &entries); + (input2, entries, record_ids) + } else { + let (input2, record_ids) = parse_record_ids(input1, game_id, &header_record, filename)?; + (input2, vec![], record_ids) + }; Ok(( input2, PluginData { header_record, record_ids, + entries, }, )) } @@ -558,7 +651,7 @@ fn read_plugin( reader: &mut R, game_id: GameId, filename: &str, - load_header_only: bool, + mode: ParseMode, expected_header_type: &'static [u8], ) -> Result { let header_record = Record::read(reader, game_id, false)?; @@ -570,18 +663,27 @@ fn read_plugin( )); } - if load_header_only { + if mode == ParseMode::HeaderOnly { return Ok(PluginData { header_record, record_ids: RecordIds::None, + entries: vec![], }); } - let record_ids = read_record_ids(reader, game_id, &header_record, filename)?; + let (entries, record_ids) = if game_id == GameId::Morrowind && mode == ParseMode::All { + let entries = read_entries(reader, game_id)?; + let record_ids = parse_record_ids_from_entries(game_id, &entries); + (entries, record_ids) + } else { + let record_ids = read_record_ids(reader, game_id, &header_record, filename)?; + (vec![], record_ids) + }; Ok(PluginData { header_record, record_ids, + entries, }) } @@ -602,7 +704,22 @@ mod tests { Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"), ); - assert!(plugin.parse_file(false).is_ok()); + assert!(plugin.parse_file(ParseMode::RecordIds).is_ok()); + + match plugin.data.record_ids { + RecordIds::NamespacedIds(ids) => assert_eq!(10, ids.len()), + _ => panic!("Expected namespaced record IDs"), + } + } + + #[test] + fn parse_file_should_succeed_mode_all() { + let mut plugin = Plugin::new( + GameId::Morrowind, + Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"), + ); + + assert!(plugin.parse_file(ParseMode::All).is_ok()); match plugin.data.record_ids { RecordIds::NamespacedIds(ids) => assert_eq!(10, ids.len()), @@ -617,7 +734,7 @@ mod tests { Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"), ); - assert!(plugin.parse_file(false).is_ok()); + assert!(plugin.parse_file(ParseMode::RecordIds).is_ok()); match plugin.data.record_ids { RecordIds::NamespacedIds(ids) => { @@ -628,6 +745,45 @@ mod tests { } } + #[test] + fn plugin_parse_should_read_a_unique_id_for_each_record_mode_all() { + let mut plugin = Plugin::new( + GameId::Morrowind, + Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"), + ); + + assert!(plugin.parse_file(ParseMode::All).is_ok()); + + match plugin.data.record_ids { + RecordIds::NamespacedIds(ids) => { + let set: HashSet = HashSet::from_iter(ids.iter().cloned()); + assert_eq!(set.len(), ids.len()); + } + _ => panic!("Expected namespaced record IDs"), + } + } + + #[test] + fn plugin_parse_should_read_all_records_mode_all() { + let mut plugin = Plugin::new( + GameId::Morrowind, + Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"), + ); + + assert!(plugin.parse_file(ParseMode::All).is_ok()); + assert_eq!(10, plugin.get_entries().len()); + + for entry in plugin.get_entries() { + match entry { + PluginEntry::Record(record) => { + assert_eq!(record.header_type(), *b"GMST"); + assert_eq!(record.subrecords().len(), 2); + } + PluginEntry::Group(_) => panic!("Unexpected group in morrowind record"), + } + } + } + #[test] fn parse_file_header_only_should_not_store_record_ids() { let mut plugin = Plugin::new( @@ -635,7 +791,7 @@ mod tests { Path::new("testing-plugins/Skyrim/Data/Blank.esm"), ); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); assert_eq!(RecordIds::None, plugin.data.record_ids); } @@ -657,7 +813,7 @@ mod tests { ), ); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); assert!(plugin.is_master_file()); } @@ -688,7 +844,7 @@ mod tests { ), ); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); assert!(!plugin.is_master_file()); } @@ -709,7 +865,7 @@ mod tests { Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"), ); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); let expected_description = format!("{:\0<218}{:\0<38}\n\0\0", "v5.0", "\r"); assert_eq!(expected_description, plugin.description().unwrap().unwrap()); @@ -722,7 +878,7 @@ mod tests { Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"), ); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); assert_eq!(1.2, plugin.header_version().unwrap()); } @@ -735,7 +891,7 @@ mod tests { ); assert!(plugin.record_and_group_count().is_none()); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); assert_eq!(10, plugin.record_and_group_count().unwrap()); } @@ -747,8 +903,25 @@ mod tests { ); assert!(plugin.record_and_group_count().is_none()); - assert!(plugin.parse_file(false).is_ok()); + assert!(plugin.parse_file(ParseMode::RecordIds).is_ok()); + assert_eq!(10, plugin.record_and_group_count().unwrap()); + match plugin.data.record_ids { + RecordIds::NamespacedIds(ids) => assert_eq!(10, ids.len()), + _ => panic!("Expected namespaced record IDs"), + } + } + + #[test] + fn record_and_group_count_should_match_record_ids_and_entries_length_mode_all() { + let mut plugin = Plugin::new( + GameId::Morrowind, + Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"), + ); + + assert!(plugin.record_and_group_count().is_none()); + assert!(plugin.parse_file(ParseMode::All).is_ok()); assert_eq!(10, plugin.record_and_group_count().unwrap()); + assert_eq!(10, plugin.get_entries().len()); match plugin.data.record_ids { RecordIds::NamespacedIds(ids) => assert_eq!(10, ids.len()), _ => panic!("Expected namespaced record IDs"), @@ -762,7 +935,19 @@ mod tests { Path::new("testing-plugins/Morrowind/Data Files/Blank - Master Dependent.esm"), ); - assert!(plugin.parse_file(false).is_ok()); + assert!(plugin.parse_file(ParseMode::RecordIds).is_ok()); + assert_eq!(0, plugin.count_override_records()); + } + + #[test] + fn count_override_records_should_return_0_even_when_override_records_are_present_mode_all() + { + let mut plugin = Plugin::new( + GameId::Morrowind, + Path::new("testing-plugins/Morrowind/Data Files/Blank - Master Dependent.esm"), + ); + + assert!(plugin.parse_file(ParseMode::All).is_ok()); assert_eq!(0, plugin.count_override_records()); } @@ -777,8 +962,45 @@ mod tests { Path::new("testing-plugins/Morrowind/Data Files/Blank - Different.esm"), ); - assert!(plugin1.parse_file(false).is_ok()); - assert!(plugin2.parse_file(false).is_ok()); + assert!(plugin1.parse_file(ParseMode::RecordIds).is_ok()); + assert!(plugin2.parse_file(ParseMode::RecordIds).is_ok()); + + assert!(plugin1.overlaps_with(&plugin1)); + assert!(!plugin1.overlaps_with(&plugin2)); + } + + #[test] + fn overlaps_with_should_detect_when_two_plugins_have_a_record_with_the_same_id_mode_all() { + let mut plugin1 = Plugin::new( + GameId::Morrowind, + Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"), + ); + let mut plugin2 = Plugin::new( + GameId::Morrowind, + Path::new("testing-plugins/Morrowind/Data Files/Blank - Different.esm"), + ); + + assert!(plugin1.parse_file(ParseMode::All).is_ok()); + assert!(plugin2.parse_file(ParseMode::All).is_ok()); + + assert!(plugin1.overlaps_with(&plugin1)); + assert!(!plugin1.overlaps_with(&plugin2)); + } + + #[test] + fn overlaps_with_should_detect_when_two_plugins_have_a_record_with_the_same_id_mode_mixed() + { + let mut plugin1 = Plugin::new( + GameId::Morrowind, + Path::new("testing-plugins/Morrowind/Data Files/Blank.esm"), + ); + let mut plugin2 = Plugin::new( + GameId::Morrowind, + Path::new("testing-plugins/Morrowind/Data Files/Blank - Different.esm"), + ); + + assert!(plugin1.parse_file(ParseMode::RecordIds).is_ok()); + assert!(plugin2.parse_file(ParseMode::All).is_ok()); assert!(plugin1.overlaps_with(&plugin1)); assert!(!plugin1.overlaps_with(&plugin2)); @@ -795,8 +1017,8 @@ mod tests { Path::new("testing-plugins/Morrowind/Data Files/Blank - Master Dependent.esm"), ); - assert!(plugin1.parse_file(false).is_ok()); - assert!(plugin2.parse_file(false).is_ok()); + assert!(plugin1.parse_file(ParseMode::RecordIds).is_ok()); + assert!(plugin2.parse_file(ParseMode::RecordIds).is_ok()); assert_eq!(4, plugin1.overlap_size(&[&plugin2, &plugin2])); } @@ -816,9 +1038,9 @@ mod tests { Path::new("testing-plugins/Morrowind/Data Files/Blank - Master Dependent.esm"), ); - assert!(plugin1.parse_file(false).is_ok()); - assert!(plugin2.parse_file(false).is_ok()); - assert!(plugin3.parse_file(false).is_ok()); + assert!(plugin1.parse_file(ParseMode::RecordIds).is_ok()); + assert!(plugin2.parse_file(ParseMode::RecordIds).is_ok()); + assert!(plugin3.parse_file(ParseMode::RecordIds).is_ok()); assert_eq!(4, plugin1.overlap_size(&[&plugin2, &plugin3])); } @@ -836,11 +1058,11 @@ mod tests { assert_eq!(0, plugin1.overlap_size(&[&plugin2])); - assert!(plugin1.parse_file(false).is_ok()); + assert!(plugin1.parse_file(ParseMode::RecordIds).is_ok()); assert_eq!(0, plugin1.overlap_size(&[&plugin2])); - assert!(plugin2.parse_file(false).is_ok()); + assert!(plugin2.parse_file(ParseMode::RecordIds).is_ok()); assert_ne!(0, plugin1.overlap_size(&[&plugin2])); } @@ -856,8 +1078,8 @@ mod tests { Path::new("testing-plugins/Morrowind/Data Files/Blank.esp"), ); - assert!(plugin1.parse_file(false).is_ok()); - assert!(plugin2.parse_file(false).is_ok()); + assert!(plugin1.parse_file(ParseMode::RecordIds).is_ok()); + assert!(plugin2.parse_file(ParseMode::RecordIds).is_ok()); assert!(!plugin1.overlaps_with(&plugin2)); assert_eq!(0, plugin1.overlap_size(&[&plugin2])); @@ -869,7 +1091,7 @@ mod tests { GameId::Morrowind, Path::new("testing-plugins/Morrowind/Data Files/Blank - Master Dependent.esm"), ); - assert!(plugin.parse_file(false).is_ok()); + assert!(plugin.parse_file(ParseMode::RecordIds).is_ok()); assert!(!plugin.is_valid_as_light_plugin()); } } @@ -894,7 +1116,7 @@ mod tests { Path::new("testing-plugins/Oblivion/Data/Blank.esm"), ); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); assert_eq!(0.8, plugin.header_version().unwrap()); } @@ -905,7 +1127,7 @@ mod tests { GameId::Oblivion, Path::new("testing-plugins/Oblivion/Data/Blank - Master Dependent.esm"), ); - assert!(plugin.parse_file(false).is_ok()); + assert!(plugin.parse_file(ParseMode::RecordIds).is_ok()); assert!(!plugin.is_valid_as_light_plugin()); } } @@ -920,7 +1142,7 @@ mod tests { Path::new("testing-plugins/Skyrim/Data/Blank.esm"), ); - assert!(plugin.parse_file(false).is_ok()); + assert!(plugin.parse_file(ParseMode::RecordIds).is_ok()); match plugin.data.record_ids { RecordIds::FormIds(ids) => assert_eq!(10, ids.len()), @@ -935,7 +1157,7 @@ mod tests { Path::new("testing-plugins/Skyrim/Data/Blank.esm"), ); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); assert_eq!(RecordIds::None, plugin.data.record_ids); } @@ -954,7 +1176,7 @@ mod tests { Path::new("testing-plugins/Skyrim/Data/Blank.esm"), ); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); assert!(plugin.is_master_file()); } @@ -965,7 +1187,7 @@ mod tests { Path::new("testing-plugins/Skyrim/Data/Blank.esp"), ); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); assert!(!plugin.is_master_file()); } @@ -986,7 +1208,7 @@ mod tests { Path::new("testing-plugins/Skyrim/Data/Blank.esm"), ); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); assert_eq!("v5.0", plugin.description().unwrap().unwrap()); let mut plugin = Plugin::new( @@ -994,7 +1216,7 @@ mod tests { Path::new("testing-plugins/Skyrim/Data/Blank.esp"), ); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); assert_eq!("€ƒŠ", plugin.description().unwrap().unwrap()); let mut plugin = Plugin::new( @@ -1005,7 +1227,7 @@ mod tests { ), ); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); assert_eq!("", plugin.description().unwrap().unwrap()); } @@ -1016,7 +1238,7 @@ mod tests { Path::new("testing-plugins/Skyrim/Data/Blank.esm"), ); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); assert_eq!(0.94, plugin.header_version().unwrap()); } @@ -1029,7 +1251,7 @@ mod tests { ); assert!(plugin.record_and_group_count().is_none()); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); assert_eq!(15, plugin.record_and_group_count().unwrap()); } @@ -1040,7 +1262,7 @@ mod tests { Path::new("testing-plugins/Skyrim/Data/Blank - Different Master Dependent.esp"), ); - assert!(plugin.parse_file(false).is_ok()); + assert!(plugin.parse_file(ParseMode::RecordIds).is_ok()); assert_eq!(2, plugin.count_override_records()); } @@ -1055,8 +1277,8 @@ mod tests { Path::new("testing-plugins/Skyrim/Data/Blank - Different.esm"), ); - assert!(plugin1.parse_file(false).is_ok()); - assert!(plugin2.parse_file(false).is_ok()); + assert!(plugin1.parse_file(ParseMode::RecordIds).is_ok()); + assert!(plugin2.parse_file(ParseMode::RecordIds).is_ok()); assert!(plugin1.overlaps_with(&plugin1)); assert!(!plugin1.overlaps_with(&plugin2)); @@ -1073,8 +1295,8 @@ mod tests { Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esm"), ); - assert!(plugin1.parse_file(false).is_ok()); - assert!(plugin2.parse_file(false).is_ok()); + assert!(plugin1.parse_file(ParseMode::RecordIds).is_ok()); + assert!(plugin2.parse_file(ParseMode::RecordIds).is_ok()); assert_eq!(4, plugin1.overlap_size(&[&plugin2, &plugin2])); } @@ -1094,9 +1316,9 @@ mod tests { Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esp"), ); - assert!(plugin1.parse_file(false).is_ok()); - assert!(plugin2.parse_file(false).is_ok()); - assert!(plugin3.parse_file(false).is_ok()); + assert!(plugin1.parse_file(ParseMode::RecordIds).is_ok()); + assert!(plugin2.parse_file(ParseMode::RecordIds).is_ok()); + assert!(plugin3.parse_file(ParseMode::RecordIds).is_ok()); assert_eq!(2, plugin1.overlap_size(&[&plugin2, &plugin3])); } @@ -1114,11 +1336,11 @@ mod tests { assert_eq!(0, plugin1.overlap_size(&[&plugin2])); - assert!(plugin1.parse_file(false).is_ok()); + assert!(plugin1.parse_file(ParseMode::RecordIds).is_ok()); assert_eq!(0, plugin1.overlap_size(&[&plugin2])); - assert!(plugin2.parse_file(false).is_ok()); + assert!(plugin2.parse_file(ParseMode::RecordIds).is_ok()); assert_ne!(0, plugin1.overlap_size(&[&plugin2])); } @@ -1134,8 +1356,8 @@ mod tests { Path::new("testing-plugins/Skyrim/Data/Blank.esp"), ); - assert!(plugin1.parse_file(false).is_ok()); - assert!(plugin2.parse_file(false).is_ok()); + assert!(plugin1.parse_file(ParseMode::RecordIds).is_ok()); + assert!(plugin2.parse_file(ParseMode::RecordIds).is_ok()); assert!(!plugin1.overlaps_with(&plugin2)); assert_eq!(0, plugin1.overlap_size(&[&plugin2])); @@ -1147,7 +1369,7 @@ mod tests { GameId::Skyrim, Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esm"), ); - assert!(plugin.parse_file(false).is_ok()); + assert!(plugin.parse_file(ParseMode::RecordIds).is_ok()); assert!(!plugin.is_valid_as_light_plugin()); } } @@ -1179,14 +1401,14 @@ mod tests { GameId::SkyrimSE, Path::new("testing-plugins/Skyrim/Data/Blank.esp"), ); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); assert!(!plugin.is_master_file()); let mut plugin = Plugin::new( GameId::SkyrimSE, Path::new("testing-plugins/Skyrim/Data/Blank.esm.esp"), ); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); assert!(plugin.is_master_file()); } @@ -1219,7 +1441,7 @@ mod tests { GameId::SkyrimSE, Path::new("testing-plugins/SkyrimSE/Data/Blank.esl.esp"), ); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); assert!(plugin.is_light_plugin()); assert!(!plugin.is_master_file()); } @@ -1237,7 +1459,7 @@ mod tests { GameId::SkyrimSE, Path::new("testing-plugins/SkyrimSE/Data/Blank.esl.esm"), ); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); assert!(plugin.is_light_plugin()); assert!(plugin.is_master_file()); } @@ -1249,7 +1471,7 @@ mod tests { Path::new("testing-plugins/SkyrimSE/Data/Blank.esm"), ); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); assert_eq!(0.94, plugin.header_version().unwrap()); } @@ -1261,7 +1483,7 @@ mod tests { GameId::SkyrimSE, Path::new("testing-plugins/SkyrimSE/Data/Blank - Master Dependent.esm"), ); - assert!(plugin.parse_file(false).is_ok()); + assert!(plugin.parse_file(ParseMode::RecordIds).is_ok()); assert!(plugin.is_valid_as_light_plugin()); } @@ -1280,7 +1502,7 @@ mod tests { bytes[0x7A] = 0xFF; bytes[0x7B] = 0x07; - assert!(plugin.parse(&bytes, false).is_ok()); + assert!(plugin.parse(&bytes, ParseMode::RecordIds).is_ok()); assert!(plugin.is_valid_as_light_plugin()); } @@ -1299,7 +1521,7 @@ mod tests { bytes[0x386] = 0xFF; bytes[0x387] = 0x07; - assert!(plugin.parse(&bytes, false).is_ok()); + assert!(plugin.parse(&bytes, ParseMode::RecordIds).is_ok()); assert!(!plugin.is_valid_as_light_plugin()); } @@ -1318,7 +1540,7 @@ mod tests { bytes[0x386] = 0x00; bytes[0x387] = 0x10; - assert!(plugin.parse(&bytes, false).is_ok()); + assert!(plugin.parse(&bytes, ParseMode::RecordIds).is_ok()); assert!(!plugin.is_valid_as_light_plugin()); } @@ -1343,7 +1565,7 @@ mod tests { GameId::Fallout3, Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esm"), ); - assert!(plugin.parse_file(false).is_ok()); + assert!(plugin.parse_file(ParseMode::RecordIds).is_ok()); assert!(!plugin.is_valid_as_light_plugin()); } } @@ -1367,7 +1589,7 @@ mod tests { GameId::FalloutNV, Path::new("testing-plugins/Skyrim/Data/Blank - Master Dependent.esm"), ); - assert!(plugin.parse_file(false).is_ok()); + assert!(plugin.parse_file(ParseMode::RecordIds).is_ok()); assert!(!plugin.is_valid_as_light_plugin()); } } @@ -1397,14 +1619,14 @@ mod tests { GameId::Fallout4, Path::new("testing-plugins/Skyrim/Data/Blank.esp"), ); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); assert!(!plugin.is_master_file()); let mut plugin = Plugin::new( GameId::Fallout4, Path::new("testing-plugins/Skyrim/Data/Blank.esm.esp"), ); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); assert!(plugin.is_master_file()); } @@ -1431,7 +1653,7 @@ mod tests { GameId::Fallout4, Path::new("testing-plugins/SkyrimSE/Data/Blank - Master Dependent.esm"), ); - assert!(plugin.parse_file(false).is_ok()); + assert!(plugin.parse_file(ParseMode::RecordIds).is_ok()); assert!(plugin.is_valid_as_light_plugin()); } @@ -1448,14 +1670,14 @@ mod tests { fn parse_file_should_error_if_plugin_does_not_exist() { let mut plugin = Plugin::new(GameId::Skyrim, Path::new("Blank.esm")); - assert!(plugin.parse_file(false).is_err()); + assert!(plugin.parse_file(ParseMode::RecordIds).is_err()); } #[test] fn parse_file_should_error_if_plugin_is_not_valid() { let mut plugin = Plugin::new(GameId::Skyrim, Path::new("README.md")); - let result = plugin.parse_file(false); + let result = plugin.parse_file(ParseMode::RecordIds); assert!(result.is_err()); assert_eq!( "failed to fill whole buffer", @@ -1472,7 +1694,7 @@ mod tests { Path::new("testing-plugins/Skyrim/Data/Invalid.esm"), ); - let result = plugin.parse_file(true); + let result = plugin.parse_file(ParseMode::HeaderOnly); assert!(result.is_err()); assert_eq!("An error was encountered while parsing the plugin content [00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00]: Expected record type [54, 45, 53, 34]", result.unwrap_err().to_string()); } @@ -1486,7 +1708,7 @@ mod tests { Path::new("testing-plugins/Skyrim/Data/Invalid.esm"), ); - let result = plugin.parse_file(false); + let result = plugin.parse_file(ParseMode::RecordIds); assert!(result.is_err()); assert_eq!("An error was encountered while parsing the plugin content [00, 00, 00, 00]: Expected record type [54, 45, 53, 34]", result.unwrap_err().to_string()); } @@ -1496,7 +1718,7 @@ mod tests { let is_valid = Plugin::is_valid( GameId::Skyrim, Path::new("testing-plugins/Skyrim/Data/Blank.esm"), - true, + ParseMode::HeaderOnly, ); assert!(is_valid); @@ -1504,7 +1726,11 @@ mod tests { #[test] fn is_valid_should_return_false_for_an_invalid_plugin() { - let is_valid = Plugin::is_valid(GameId::Skyrim, Path::new("README.md"), true); + let is_valid = Plugin::is_valid( + GameId::Skyrim, + Path::new("README.md"), + ParseMode::HeaderOnly, + ); assert!(!is_valid); } @@ -1542,7 +1768,7 @@ mod tests { Path::new("testing-plugins/Skyrim/Data/Blank.esm"), ); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); assert_eq!(0, plugin.masters().unwrap().len()); } @@ -1556,7 +1782,7 @@ mod tests { ), ); - assert!(plugin.parse_file(true).is_ok()); + assert!(plugin.parse_file(ParseMode::HeaderOnly).is_ok()); let masters = plugin.masters().unwrap(); assert_eq!(1, masters.len()); @@ -1577,7 +1803,7 @@ mod tests { data[0x14] = 8; data[0x15] = 0; - assert!(plugin.parse(&data, true).is_ok()); + assert!(plugin.parse(&data, ParseMode::HeaderOnly).is_ok()); let result = plugin.description(); assert!(result.is_err()); @@ -1598,7 +1824,7 @@ mod tests { data[0x14] = 3; data[0x15] = 0; - assert!(plugin.parse(&data, true).is_ok()); + assert!(plugin.parse(&data, ParseMode::HeaderOnly).is_ok()); assert!(plugin.header_version().is_none()); } @@ -1614,7 +1840,7 @@ mod tests { data[0x04] = 0x30; data[0x14] = 0x28; - assert!(plugin.parse(&data, true).is_ok()); + assert!(plugin.parse(&data, ParseMode::HeaderOnly).is_ok()); assert!(plugin.record_and_group_count().is_none()); } diff --git a/src/record.rs b/src/record.rs index 16ad225..fca5f9b 100644 --- a/src/record.rs +++ b/src/record.rs @@ -140,7 +140,7 @@ impl Record { let bytes_read = header_length_read as u32 + header.size_of_subrecords; - let (_, record_id) = parse_morrowind_record_id(&subrecords_data, &header)?; + let (_, record_id) = parse_morrowind_record_id_from_input(&subrecords_data, &header)?; Ok((bytes_read, record_id)) } else { // Seeking discards the current buffer, so only do so if the data @@ -165,7 +165,7 @@ impl Record { let (remaining_input, subrecords_data) = take(header.size_of_subrecords)(remaining_input)?; - let (_, record_id) = parse_morrowind_record_id(subrecords_data, &header)?; + let (_, record_id) = parse_morrowind_record_id_from_input(subrecords_data, &header)?; Ok((remaining_input, record_id)) } else { let mut parser = tuple(( @@ -189,6 +189,29 @@ impl Record { } } + pub fn parse_record_id_from_self(&self, game_id: GameId) -> Option { + if game_id == GameId::Morrowind { + let types = record_id_subrecord_types(self.header.record_type); + if !types.is_empty() { + let subrecordrefs: Vec = self + .subrecords + .iter() + .filter(|x| types.contains(&x.subrecord_type())) + .map(|x| SubrecordRef::from_subrecord(x)) + .collect::>(); + let data = record_id_subrecord_mapper(self.header.record_type, &subrecordrefs); + + data.map(|data| { + RecordId::NamespacedId(NamespacedId::new(self.header.record_type, data)) + }) + } else { + None + } + } else { + unimplemented!() + } + } + pub fn header(&self) -> &RecordHeader { &self.header } @@ -202,7 +225,7 @@ impl Record { } } -fn parse_morrowind_record_id<'a>( +fn parse_morrowind_record_id_from_input<'a>( subrecords_data: &'a [u8], header: &RecordHeader, ) -> IResult<&'a [u8], Option> { @@ -460,6 +483,28 @@ mod tests { assert!(record.header.form_id.is_none()); } + #[test] + fn parse_record_id_from_self_should_return_none_for_tes3_header() { + let data = + &include_bytes!("../testing-plugins/Morrowind/Data Files/Blank.esm")[..0x144]; + + let record = Record::parse(data, GameId::Morrowind, false).unwrap().1; + let form_id = record.parse_record_id_from_self(GameId::Morrowind); + + assert!(form_id.is_none()); + } + + #[test] + fn parse_record_id_from_self_should_return_some_namespaced_id_for_gmst() { + let data = + &include_bytes!("../testing-plugins/Morrowind/Data Files/Blank.esm")[0x144..0x16F]; + + let record = Record::parse(data, GameId::Morrowind, false).unwrap().1; + let form_id = record.parse_record_id_from_self(GameId::Morrowind); + + assert!(form_id.unwrap().namespaced_id().is_some()); + } + #[test] fn parse_record_id_should_return_none_for_tes3_header() { let data = diff --git a/src/subrecord.rs b/src/subrecord.rs index 579a6fc..900feec 100644 --- a/src/subrecord.rs +++ b/src/subrecord.rs @@ -118,6 +118,13 @@ impl<'a> SubrecordRef<'a> { )) } + pub fn from_subrecord(subrecord: &'a Subrecord) -> SubrecordRef<'a> { + SubrecordRef { + subrecord_type: *subrecord.subrecord_type(), + data: &subrecord.data, + } + } + pub fn subrecord_type(&'a self) -> &SubrecordType { &self.subrecord_type }