From a038e9a3af4d99ba557478ae7387528129be635c Mon Sep 17 00:00:00 2001 From: Oliver Hamlet Date: Tue, 28 Apr 2026 18:54:08 +0100 Subject: [PATCH] Store asset paths instead of hashes This avoids the issue of hash collisions, within any one archive, a plugin's archives, or between different plugins' archives. That means that sorting has accurate data on what the asset counts loaded by plugins are, and what the asset overlap between plugins, so it can make better decisions when adding overlap edges. It does require that the archives include the folder and file names, which is apparently not strictly required, but it seems that having the names is practically required[1][2], and I verified that the parsing code can handle archives from all of the supported games (apart from Morrowind, which doesn't have its BSAs read by LOOT). That testing covered 321 archives containing 771246 folder records and 3404007 file records, including some mod BSAs/BA2s. If an archive doesn't have the flags set for containing folder and file names, then libloot will log an error and effectively ignore its contents, so the new behaviour means that some archives that were (potentially inaccurately) taken into account before may now be ignored. Falling back to using the hashes for archives that don't have names would be an option, but that complicates comparison against archives that do have names, and I have no evidence that the fallback would be useful in practice. This doubles the size of each map key and set entry (from 8 to 16 bytes), introduces another level of indirection when comparing keys or values, and also means that the name strings need to be stored, further increasing memory usage. I tested the performance impact when sorting Skyrim SE and Starfield load orders, using LOOT v0.29.1 and comparing against using it with libloot v0.24.4: - The Skyrim SE load order had 1630 plugins and 18 BSAs totalling 3.14 GB (not including Skyrim.esm's or its BSAs, which LOOT doesn't fully load). - Loading plugins (which includes parsing BSAs) was ~ 2% (5 ms) faster - Sorting was ~ 1% (21 ms) faster - Memory usage after sorting was 1 MB (< 1%) higher - These differences are probably within margin of error - The Starfield load order had 31 plugins and 72 BA2s totalling 20.2 GB (not including Starfield.esm or its BA2s, which LOOT doesn't fully load). - Loading plugins (which includes parsing BA2s) was ~ 96% (423 ms) faster - Sorting speed was unchanged - Memory usage after sorting was 11 MB (12%) higher It looks like the accuracy improvement is also faster, and the memory usage cost is relatively modest. [1]: https://en.uesp.net/wiki/Oblivion_Mod:BSA_File_Format [2]: https://en.uesp.net/wiki/Skyrim_Mod:Archive_File_Format --- src/archive/ba2.rs | 93 ++++++---------------- src/archive/bsa.rs | 183 ++++++++++++++++++++++++------------------- src/archive/error.rs | 22 +++++- src/archive/mod.rs | 23 ++++-- src/archive/parse.rs | 89 ++++++++++----------- src/plugin/mod.rs | 7 +- 6 files changed, 208 insertions(+), 209 deletions(-) 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);