diff --git a/src/archive/ba2.rs b/src/archive/ba2.rs index 044fe35f..1e898d8d 100644 --- a/src/archive/ba2.rs +++ b/src/archive/ba2.rs @@ -1,11 +1,6 @@ -use std::{ - collections::{BTreeMap, BTreeSet}, - hash::{DefaultHasher, Hash, Hasher}, - io::{BufRead, Seek}, - path::Path, -}; +use std::io::{BufRead, Seek}; -use crate::{escape_ascii, logging}; +use crate::archive::{ArchiveAssets, normalise_path}; use super::error::ArchiveParsingError; @@ -62,19 +57,17 @@ impl TryFrom<[u8; HEADER_SIZE - TYPE_ID.len()]> for Header { pub(super) fn read_assets( mut reader: T, - archive_path: &Path, -) -> Result>, ArchiveParsingError> { +) -> Result { let mut header_buffer = [0; HEADER_SIZE - TYPE_ID.len()]; reader.read_exact(&mut header_buffer)?; let header = Header::try_from(header_buffer)?; - let mut assets = BTreeMap::new(); + let mut assets = ArchiveAssets::new(); reader.seek(std::io::SeekFrom::Start(header.file_paths_offset))?; - let mut collision_count: usize = 0; for _ in 0..header.file_count { let mut length_buf = [0; 2]; reader.read_exact(&mut length_buf)?; @@ -85,75 +78,41 @@ pub(super) fn read_assets( normalise_path(&mut file_path_bytes); - let file_path_bytes = trim_slashes(&file_path_bytes); + trim_slashes(&mut file_path_bytes); - let (folder_hash, file_hash) = rsplit_on(file_path_bytes, b'\\').map_or_else( - || (0, hash(&file_path_bytes)), - |(folder_path, file_path)| (hash(&folder_path), hash(&file_path)), - ); + let (folder_path, file_name) = rsplit_on(file_path_bytes, b'\\'); - let file_hashes: &mut BTreeSet = assets.entry(folder_hash).or_default(); - - if !file_hashes.insert(file_hash) { - collision_count += 1; - } - } - - if collision_count > 0 { - logging::debug!( - "Encountered {} hash collisions for asset file paths while reading \"{}\"", - collision_count, - escape_ascii(archive_path) - ); + assets.entry(folder_path).or_default().insert(file_name); } Ok(assets) } -fn normalise_path(path_bytes: &mut [u8]) { - for byte in path_bytes { - // Ignore any non-ASCII characters. - if *byte > 127 { - continue; - } +fn trim_slashes(path_bytes: &mut Vec) { + let predicate = |c: &u8| *c != b'\\'; - *byte = match byte { - b'/' => b'\\', - _ => byte.to_ascii_lowercase(), - } - } -} - -fn trim_slashes(mut path_bytes: &[u8]) -> &[u8] { - while let [first, rest @ ..] = path_bytes { - if *first == b'\\' { - path_bytes = rest; - } else { - break; - } + match path_bytes.iter().rposition(predicate) { + Some(i) => path_bytes.truncate(i + 1), + None => path_bytes.clear(), } - while let [rest @ .., last] = path_bytes { - if *last == b'\\' { - path_bytes = rest; - } else { - break; - } - } + let mut trimmed = path_bytes + .iter() + .position(predicate) + .map(|i| path_bytes.split_off(i)) + .unwrap_or_default(); - path_bytes + std::mem::swap(path_bytes, &mut trimmed); } -fn rsplit_on(slice: &[u8], needle: u8) -> Option<(&[u8], &[u8])> { - let mut iter = slice.rsplitn(2, |b| *b == needle); - let second = iter.next()?; - let first = iter.next()?; +fn rsplit_on(mut path_bytes: Vec, needle: u8) -> (Box<[u8]>, Box<[u8]>) { + let second = path_bytes + .iter() + .rposition(|b| *b == needle) + .map(|i| path_bytes.split_off(i + 1)) + .unwrap_or_default(); - Some((first, second)) -} + path_bytes.truncate(path_bytes.len().saturating_sub(1)); -fn hash(value: &T) -> u64 { - let mut hasher = DefaultHasher::new(); - value.hash(&mut hasher); - hasher.finish() + (path_bytes.into_boxed_slice(), second.into_boxed_slice()) } diff --git a/src/archive/bsa.rs b/src/archive/bsa.rs index 639a7abe..e4300593 100644 --- a/src/archive/bsa.rs +++ b/src/archive/bsa.rs @@ -1,10 +1,6 @@ -use std::{ - collections::{BTreeMap, BTreeSet, btree_map::Entry}, - io::BufRead, - path::Path, -}; +use std::io::BufRead; -use crate::{escape_ascii, logging}; +use crate::archive::{ArchiveAssets, normalise_path}; use super::error::ArchiveParsingError; @@ -12,6 +8,10 @@ pub(super) const TYPE_ID: [u8; 4] = *b"BSA\0"; const HEADER_SIZE: usize = 36; const FILE_RECORD_SIZE: usize = 16; +const ARCHIVE_FLAG_HAS_DIRECTORY_NAMES: u32 = 0x1; +const ARCHIVE_FLAG_HAS_FILE_NAMES: u32 = 0x2; +const ARCHIVE_FLAG_BIG_ENDIAN: u32 = 0x40; + #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] struct Header { type_id: [u8; 4], @@ -79,16 +79,11 @@ impl TryFrom<[u8; HEADER_SIZE - TYPE_ID.len()]> for Header { )); } - if (header.archive_flags & 0x40) != 0 { - return Err(ArchiveParsingError::UsesBigEndianNumbers); - } - Ok(header) } } struct FolderRecord { - name_hash: u64, file_count: u32, file_records_offset: u32, } @@ -101,10 +96,9 @@ mod v103 { pub(super) fn read_folder_record(value: &[u8; FOLDER_RECORD_SIZE]) -> FolderRecord { // LIMITATION: There's no syntax to infallibly split an array into sub-arrays. - let [name_hash @ .., c0, c1, c2, c3, o0, o1, o2, o3] = *value; + let [_name_hash @ .., c0, c1, c2, c3, o0, o1, o2, o3] = *value; FolderRecord { - name_hash: u64::from_le_bytes(name_hash), file_count: u32::from_le_bytes([c0, c1, c2, c3]), file_records_offset: u32::from_le_bytes([o0, o1, o2, o3]), } @@ -120,7 +114,7 @@ mod v105 { // LIMITATION: There's no syntax to infallibly split an array into sub-arrays. #[rustfmt::skip] let [ - name_hash @ .., + _name_hash @ .., c0, c1, c2, c3, // file_count _, _, _, _, o0, o1, o2, o3, // file_records_offset @@ -128,17 +122,13 @@ mod v105 { ] = *value; FolderRecord { - name_hash: u64::from_le_bytes(name_hash), file_count: u32::from_le_bytes([c0, c1, c2, c3]), file_records_offset: u32::from_le_bytes([o0, o1, o2, o3]), } } } -pub(super) fn read_assets( - mut reader: T, - archive_path: &Path, -) -> Result>, ArchiveParsingError> { +pub(super) fn read_assets(mut reader: T) -> Result { let mut header_buffer = [0; HEADER_SIZE - TYPE_ID.len()]; reader.read_exact(&mut header_buffer)?; @@ -148,13 +138,11 @@ pub(super) fn read_assets( match header.version { 103 | 104 => read_assets_with_header::( reader, - archive_path, &header, v103::read_folder_record, ), 105 => read_assets_with_header::( reader, - archive_path, &header, v105::read_folder_record, ), @@ -166,10 +154,21 @@ pub(super) fn read_assets( fn read_assets_with_header( mut reader: T, - archive_path: &Path, header: &Header, read_folder_record: impl Fn(&[u8; U]) -> FolderRecord, -) -> Result>, ArchiveParsingError> { +) -> Result { + if (header.archive_flags & ARCHIVE_FLAG_BIG_ENDIAN) != 0 { + return Err(ArchiveParsingError::UsesBigEndianNumbers); + } + + if (header.archive_flags & ARCHIVE_FLAG_HAS_DIRECTORY_NAMES) == 0 { + return Err(ArchiveParsingError::DirectoryNamesNotIncluded); + } + + if (header.archive_flags & ARCHIVE_FLAG_HAS_FILE_NAMES) == 0 { + return Err(ArchiveParsingError::FileNamesNotIncluded); + } + let mut folders_buffer: Vec = vec![0; U * to_usize(header.folder_count)]; reader.read_exact(folders_buffer.as_mut_slice())?; @@ -182,80 +181,102 @@ fn read_assets_with_header( reader.read_exact(file_records_buffer.as_mut_slice())?; + let mut file_names_buffer: Vec = vec![0; to_usize(header.total_file_names_length)]; + + reader.read_exact(file_names_buffer.as_mut_slice())?; + let folder_record_offset_baseline = HEADER_SIZE + folders_buffer.len() + to_usize(header.total_file_names_length); - let mut folder_collision_count: usize = 0; - let mut asset_collision_count: usize = 0; - let mut assets = BTreeMap::new(); + // Store folder names and their file counts. + let mut folders: Vec<(Box<[u8]>, u32)> = Vec::with_capacity(to_usize(header.folder_count)); for chunk in folders_buffer.as_chunks::().0 { let folder_record = read_folder_record(chunk); - let entry = assets.entry(folder_record.name_hash); - if let Entry::Occupied(_) = entry { - folder_collision_count += 1; - } + let folder_name_length_offset = + to_usize(folder_record.file_records_offset) - folder_record_offset_baseline; - let file_records_offset = if (header.archive_flags & 0x1) == 0 { - to_usize(folder_record.file_records_offset) - folder_record_offset_baseline - } else { - let folder_name_length_offset = - to_usize(folder_record.file_records_offset) - folder_record_offset_baseline; + let folder_name = read_folder_name(&file_records_buffer, folder_name_length_offset)?; - if let Some(folder_name_length) = file_records_buffer.get(folder_name_length_offset) { - folder_name_length_offset + 1 + usize::from(*folder_name_length) - } else { - return Err(ArchiveParsingError::InvalidFolderNameLengthOffset( - folder_name_length_offset, - )); - } - }; - - let Some(file_records_buffer) = file_records_buffer.get(file_records_offset..) else { - return Err(ArchiveParsingError::InvalidFileRecordsOffset( - file_records_offset, - )); - }; - - let file_hashes: &mut BTreeSet = entry.or_default(); - - for file_chunk in file_records_buffer - .as_chunks::() - .0 - .iter() - .take(to_usize(folder_record.file_count)) - { - let file_hash = file_record_hash(file_chunk); - - if !file_hashes.insert(file_hash) { - asset_collision_count += 1; - } - } + folders.push((folder_name, folder_record.file_count)); } - if folder_collision_count > 0 { - logging::debug!( - "Encountered {} hash collisions for asset folders while reading \"{}\"", - folder_collision_count, - escape_ascii(archive_path) - ); - } + // Repeat each folder name by the number of files in that folder. + let folder_names_iter = folders + .into_iter() + .flat_map(|(f, c)| std::iter::repeat_n(f, to_usize(c))); - if asset_collision_count > 0 { - logging::debug!( - "Encountered {} hash collisions for asset file paths while reading \"{}\"", - asset_collision_count, - escape_ascii(archive_path) - ); + // File names are separated by a null byte, and are listed in the order that + // their file records appear, which is the order in which their parent + // folder records appear. + let file_names_iter = file_names_buffer.split(|b| *b == 0); + + let mut assets = ArchiveAssets::new(); + + for (folder_name, file_name) in folder_names_iter.zip(file_names_iter) { + let file_name = trim_and_normalise(file_name); + assets.entry(folder_name).or_default().insert(file_name); } Ok(assets) } -fn file_record_hash(file_record: &[u8; FILE_RECORD_SIZE]) -> u64 { - // LIMITATION: There's no syntax to infallibly split an array into sub-arrays. - let [hash_bytes @ .., _, _, _, _, _, _, _, _] = file_record; - u64::from_le_bytes(*hash_bytes) +fn read_folder_name( + file_records_buffer: &[u8], + folder_name_length_offset: usize, +) -> Result, ArchiveParsingError> { + if let Some(folder_name_length) = file_records_buffer.get(folder_name_length_offset) { + let folder_name_start = folder_name_length_offset + 1; + let folder_name_range = + folder_name_start..folder_name_start + usize::from(*folder_name_length); + + file_records_buffer + .get(folder_name_range.clone()) + .map(trim_and_normalise) + .ok_or(ArchiveParsingError::InvalidFolderNameRange( + folder_name_range, + )) + } else { + Err(ArchiveParsingError::InvalidFolderNameLengthOffset( + folder_name_length_offset, + )) + } +} + +fn trim_and_normalise(path_bytes: &[u8]) -> Box<[u8]> { + let mut trimmed: Box<[u8]> = trim_slashes(trim_null_terminator(path_bytes)).into(); + + normalise_path(&mut trimmed); + + trimmed +} + +fn trim_null_terminator(path_bytes: &[u8]) -> &[u8] { + if let [rest @ .., b'\0'] = path_bytes { + rest + } else { + path_bytes + } +} + +fn trim_slashes(mut path_bytes: &[u8]) -> &[u8] { + while let [first, rest @ ..] = path_bytes { + if *first == b'\\' || *first == b'/' { + path_bytes = rest; + } else { + break; + } + } + + while let [rest @ .., last] = path_bytes { + if *last == b'\\' || *last == b'/' { + path_bytes = rest; + } else { + break; + } + } + + path_bytes } #[expect( diff --git a/src/archive/error.rs b/src/archive/error.rs index d6c9725a..692cf3df 100644 --- a/src/archive/error.rs +++ b/src/archive/error.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{ops::Range, path::PathBuf}; use crate::escape_ascii; @@ -45,8 +45,10 @@ pub(crate) enum ArchiveParsingError { UnsupportedArchiveTypeId([u8; 4]), InvalidRecordsOffset(u32), InvalidFolderNameLengthOffset(usize), - InvalidFileRecordsOffset(usize), + InvalidFolderNameRange(Range), UsesBigEndianNumbers, + DirectoryNamesNotIncluded, + FileNamesNotIncluded, } impl std::fmt::Display for ArchiveParsingError { @@ -64,10 +66,24 @@ impl std::fmt::Display for ArchiveParsingError { Self::InvalidFolderNameLengthOffset(o) => { write!(f, "invalid folder name length offset {o}") } - Self::InvalidFileRecordsOffset(o) => write!(f, "invalid file records offset {o}"), + Self::InvalidFolderNameRange(r) => { + write!(f, "invalid folder name range {:#x}..{:#x}", r.start, r.end) + } Self::UsesBigEndianNumbers => { write!(f, "archive uses big-endian numbers, which is unsupported") } + Self::DirectoryNamesNotIncluded => { + write!( + f, + "archive does not include directory names, which is unsupported" + ) + } + Self::FileNamesNotIncluded => { + write!( + f, + "archive does not include file names, which is unsupported" + ) + } } } } diff --git a/src/archive/mod.rs b/src/archive/mod.rs index b8badc71..5890d773 100644 --- a/src/archive/mod.rs +++ b/src/archive/mod.rs @@ -9,10 +9,9 @@ use std::collections::{BTreeMap, BTreeSet}; pub(crate) use find::find_associated_archives; pub(crate) use parse::assets_in_archives; -pub(crate) fn do_assets_overlap( - assets: &BTreeMap>, - other_assets: &BTreeMap>, -) -> bool { +pub(crate) type ArchiveAssets = BTreeMap, BTreeSet>>; + +pub(crate) fn do_assets_overlap(assets: &ArchiveAssets, other_assets: &ArchiveAssets) -> bool { let mut assets_iter = assets.iter(); let mut other_assets_iter = other_assets.iter(); @@ -36,6 +35,20 @@ pub(crate) fn do_assets_overlap( false } +fn normalise_path(path_bytes: &mut [u8]) { + for byte in path_bytes { + // Ignore any non-ASCII characters. + if *byte > 127 { + continue; + } + + *byte = match byte { + b'/' => b'\\', + _ => byte.to_ascii_lowercase(), + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -61,7 +74,7 @@ mod tests { let path = PathBuf::from("./testing-plugins/Skyrim/Data/Blank.bsa"); let assets2 = assets_in_archives(&[path]); - assert_eq!(assets1.get(&0), assets2.get(&0x2E01_002E)); + assert_eq!(assets1.get("".as_bytes()), assets2.get("\x2E".as_bytes())); assert!(!do_assets_overlap(&assets1, &assets2)); } diff --git a/src/archive/parse.rs b/src/archive/parse.rs index 383f01f4..1a7eb694 100644 --- a/src/archive/parse.rs +++ b/src/archive/parse.rs @@ -1,5 +1,4 @@ use std::{ - collections::{BTreeMap, BTreeSet}, fs::File, io::{BufReader, Read}, path::{Path, PathBuf}, @@ -7,14 +6,15 @@ use std::{ use super::error::{ArchiveParsingError, ArchivePathParsingError}; use crate::{ + archive::ArchiveAssets, escape_ascii, logging::{self, format_details}, }; use super::{ba2, bsa}; -pub(crate) fn assets_in_archives(archive_paths: &[PathBuf]) -> BTreeMap> { - let mut archive_assets: BTreeMap> = BTreeMap::new(); +pub(crate) fn assets_in_archives(archive_paths: &[PathBuf]) -> ArchiveAssets { + let mut archive_assets: ArchiveAssets = ArchiveAssets::new(); for archive_path in archive_paths { logging::trace!( @@ -51,9 +51,7 @@ pub(crate) fn assets_in_archives(archive_paths: &[PathBuf]) -> BTreeMap Result>, ArchivePathParsingError> { +fn get_assets_in_archive(archive_path: &Path) -> Result { let file = File::open(archive_path) .map_err(|e| ArchivePathParsingError::from_io_error(archive_path.into(), e))?; let mut reader = BufReader::new(file); @@ -64,9 +62,9 @@ fn get_assets_in_archive( .map_err(|e| ArchivePathParsingError::from_io_error(archive_path.into(), e))?; match type_id { - bsa::TYPE_ID => bsa::read_assets(reader, archive_path) + bsa::TYPE_ID => bsa::read_assets(reader) .map_err(|e| ArchivePathParsingError::new(archive_path.into(), e)), - ba2::TYPE_ID => ba2::read_assets(reader, archive_path) + ba2::TYPE_ID => ba2::read_assets(reader) .map_err(|e| ArchivePathParsingError::new(archive_path.into(), e)), _ => Err(ArchivePathParsingError::new( archive_path.into(), @@ -80,22 +78,13 @@ mod tests { use super::*; mod get_assets_in_archive { - use std::{ - hash::{DefaultHasher, Hash, Hasher}, - io::SeekFrom, - }; + use std::{collections::BTreeSet, io::SeekFrom}; use array_parameterized_test::{parameterized_test, test_parameter}; use tempfile::tempdir; use super::*; - fn hash(value: T) -> u64 { - let mut hasher = DefaultHasher::new(); - value.hash(&mut hasher); - hasher.finish() - } - #[test] fn should_error_if_file_cannot_be_opened() { let path = Path::new("./invalid.bsa"); @@ -109,14 +98,14 @@ mod tests { let files_count: usize = assets.values().map(BTreeSet::len).sum(); - let expected_key = 0; + let expected_key = "".as_bytes(); assert_eq!(1, assets.len()); assert_eq!(1, files_count); - assert_eq!(expected_key, *assets.first_key_value().unwrap().0); - assert_eq!(1, assets[&expected_key].len()); + assert_eq!(expected_key, assets.first_key_value().unwrap().0.as_ref()); + assert_eq!(1, assets[expected_key].len()); assert_eq!( - 0x4670_B683_6C07_7365, - *assets[&expected_key].first().unwrap() + "license".as_bytes(), + assets[expected_key].first().unwrap().as_ref() ); } @@ -127,14 +116,14 @@ mod tests { let files_count: usize = assets.values().map(BTreeSet::len).sum(); - let expected_key = 0x2E01_002E; + let expected_key = "\x2E".as_bytes(); assert_eq!(1, assets.len()); assert_eq!(1, files_count); - assert_eq!(expected_key, *assets.first_key_value().unwrap().0); - assert_eq!(1, assets[&expected_key].len()); + assert_eq!(expected_key, assets.first_key_value().unwrap().0.as_ref()); + assert_eq!(1, assets[expected_key].len()); assert_eq!( - 0x4670_B683_6C07_7365, - *assets[&expected_key].first().unwrap() + "license".as_bytes(), + assets[expected_key].first().unwrap().as_ref() ); } @@ -145,14 +134,14 @@ mod tests { let files_count: usize = assets.values().map(BTreeSet::len).sum(); - let expected_key = 0xB681_02C9_6417_6E73; + let expected_key = "dev\\git\\testing-plugins".as_bytes(); assert_eq!(1, assets.len()); assert_eq!(1, files_count); - assert_eq!(expected_key, *assets.first_key_value().unwrap().0); - assert_eq!(1, assets[&expected_key].len()); + assert_eq!(expected_key, assets.first_key_value().unwrap().0.as_ref()); + assert_eq!(1, assets[expected_key].len()); assert_eq!( - 0x4670_B683_6C07_7365, - *assets[&expected_key].first().unwrap() + "license".as_bytes(), + assets[expected_key].first().unwrap().as_ref() ); } @@ -163,16 +152,16 @@ mod tests { let files_count: usize = assets.values().map(BTreeSet::len).sum(); - let expected_key = hash("dev\\git\\testing-plugins".as_bytes()); - let expected_file_hash = hash("license.txt".as_bytes()); + let expected_key = "dev\\git\\testing-plugins".as_bytes(); + let expected_file_hash = "license.txt".as_bytes(); assert_eq!(1, assets.len()); assert_eq!(1, files_count); let (key, value) = assets.first_key_value().unwrap(); - assert_eq!(expected_key, *key); + assert_eq!(expected_key, key.as_ref()); assert_eq!(1, value.len()); - assert_eq!(expected_file_hash, *value.first().unwrap()); + assert_eq!(expected_file_hash, value.first().unwrap().as_ref()); } #[test] @@ -182,16 +171,16 @@ mod tests { let files_count: usize = assets.values().map(BTreeSet::len).sum(); - let expected_key = hash("dev\\git\\testing-plugins".as_bytes()); - let expected_file_hash = hash("blank.dds".as_bytes()); + let expected_key = "dev\\git\\testing-plugins".as_bytes(); + let expected_file_hash = "blank.dds".as_bytes(); assert_eq!(1, assets.len()); assert_eq!(1, files_count); let (key, value) = assets.first_key_value().unwrap(); - assert_eq!(expected_key, *key); + assert_eq!(expected_key, key.as_ref()); assert_eq!(1, value.len()); - assert_eq!(expected_file_hash, *value.first().unwrap()); + assert_eq!(expected_file_hash, value.first().unwrap().as_ref()); } #[test_parameter] @@ -218,6 +207,8 @@ mod tests { } mod assets_in_archives { + use std::collections::BTreeSet; + use super::*; #[test] @@ -235,9 +226,9 @@ mod tests { assert_eq!(1, files_count); let (key, value) = assets.first_key_value().unwrap(); - assert_eq!(0x2E01_002E, *key); + assert_eq!("\x2E".as_bytes(), key.as_ref()); assert_eq!(1, value.len()); - assert_eq!(0x4670_B683_6C07_7365, *value.first().unwrap()); + assert_eq!("license".as_bytes(), value.first().unwrap().as_ref()); } #[test] @@ -255,17 +246,17 @@ mod tests { assert_eq!(3, assets.len()); assert_eq!(3, files_count); - let value = &assets[&0]; + let value = &assets["".as_bytes()]; assert_eq!(1, value.len()); - assert_eq!(0x4670_B683_6C07_7365, *value.first().unwrap()); + assert_eq!("license".as_bytes(), value.first().unwrap().as_ref()); - let value = &assets[&0x2E01_002E]; + let value = &assets["\x2E".as_bytes()]; assert_eq!(1, value.len()); - assert_eq!(0x4670_B683_6C07_7365, *value.first().unwrap()); + assert_eq!("license".as_bytes(), value.first().unwrap().as_ref()); - let value = &assets[&0xB681_02C9_6417_6E73]; + let value = &assets["dev\\git\\testing-plugins".as_bytes()]; assert_eq!(1, value.len()); - assert_eq!(0x4670_B683_6C07_7365, *value.first().unwrap()); + assert_eq!("license".as_bytes(), value.first().unwrap().as_ref()); } } } diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs index b034a0a7..1350a1eb 100644 --- a/src/plugin/mod.rs +++ b/src/plugin/mod.rs @@ -1,7 +1,6 @@ pub(crate) mod error; use std::{ - collections::{BTreeMap, BTreeSet}, fs::File, hash::Hasher, io::{BufRead, BufReader}, @@ -14,7 +13,7 @@ use regress::Regex; use crate::{ GameType, - archive::{assets_in_archives, do_assets_overlap, find_associated_archives}, + archive::{ArchiveAssets, assets_in_archives, do_assets_overlap, find_associated_archives}, case_insensitive_regex, escape_ascii, game::GameCache, logging, @@ -50,7 +49,7 @@ pub struct Plugin { version: Option, tags: Box<[String]>, archive_paths: Box<[PathBuf]>, - archive_assets: BTreeMap>, + archive_assets: ArchiveAssets, } impl Plugin { @@ -72,7 +71,7 @@ impl Plugin { let mut version = None; let mut tags = Box::default(); let mut archive_paths = Box::default(); - let mut archive_assets = BTreeMap::new(); + let mut archive_assets = ArchiveAssets::new(); let plugin = if game_type != GameType::OpenMW || !has_ascii_extension(plugin_path, "omwscripts") { let mut plugin = esplugin::Plugin::new(game_type.into(), plugin_path);