diff --git a/README.md b/README.md index 1e0c4f54..5dea8e08 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,7 @@ This is an **incomplete** and **experimental** reimplementation of [libloot](htt Currently complete: -- [x] Public API types and function declarations (excluding errors) -- [ ] Public API error types +- [x] Public API types and function declarations - [x] Public API doc comments - [x] Library versioning - [ ] Setting a logging callback diff --git a/src/archive/ba2.rs b/src/archive/ba2.rs index e7d575f0..908ee553 100644 --- a/src/archive/ba2.rs +++ b/src/archive/ba2.rs @@ -4,7 +4,7 @@ use std::{ io::{BufRead, Seek}, }; -use crate::error::{GeneralError, InvalidArgumentError}; +use super::error::ArchiveParsingError; use super::parse::{to_u32, to_u64}; @@ -23,7 +23,7 @@ struct Header { } impl TryFrom<[u8; HEADER_SIZE - TYPE_ID.len()]> for Header { - type Error = InvalidArgumentError; + type Error = ArchiveParsingError; fn try_from(value: [u8; HEADER_SIZE - TYPE_ID.len()]) -> Result { let header = Self { @@ -37,15 +37,15 @@ impl TryFrom<[u8; HEADER_SIZE - TYPE_ID.len()]> for Header { // The header version is 1, 7 or 8 for Fallout 4 and 2 or 3 for Starfield. if !matches!(header.version, 1 | 2 | 3 | 7 | 8) { - return Err(InvalidArgumentError { - message: "BA2 file header version is invalid".into(), - }); + return Err(ArchiveParsingError::UnsupportedHeaderVersion( + header.version, + )); } if !matches!(header.archive_type, BA2_GENERAL_TYPE | BA2_TEXTURE_TYPE) { - return Err(InvalidArgumentError { - message: "BA2 file header archive type is invalid".into(), - }); + return Err(ArchiveParsingError::UnsupportedHeaderArchiveType( + header.archive_type, + )); } Ok(header) @@ -54,7 +54,7 @@ impl TryFrom<[u8; HEADER_SIZE - TYPE_ID.len()]> for Header { pub(super) fn read_assets( mut reader: T, -) -> Result>, GeneralError> { +) -> Result>, ArchiveParsingError> { let mut header_buffer = [0; HEADER_SIZE - TYPE_ID.len()]; reader.read_exact(&mut header_buffer)?; @@ -84,13 +84,10 @@ pub(super) fn read_assets( let file_hashes: &mut BTreeSet = assets.entry(folder_hash).or_default(); if !file_hashes.insert(file_hash) { - return Err(InvalidArgumentError { - message: format!( - "Unexpected collision for file name hash {:x} in set for folder name hash {:x}", - file_hash, folder_hash - ), - } - .into()); + return Err(ArchiveParsingError::HashCollision { + folder_hash, + file_hash, + }); } } diff --git a/src/archive/bsa.rs b/src/archive/bsa.rs index 70910735..b9f84020 100644 --- a/src/archive/bsa.rs +++ b/src/archive/bsa.rs @@ -3,7 +3,7 @@ use std::{ io::BufRead, }; -use crate::error::{GeneralError, InvalidArgumentError}; +use super::error::ArchiveParsingError; use super::parse::{to_u32, to_u64, to_usize}; @@ -25,7 +25,7 @@ struct Header { } impl TryFrom<[u8; HEADER_SIZE - TYPE_ID.len()]> for Header { - type Error = InvalidArgumentError; + type Error = ArchiveParsingError; fn try_from(value: [u8; HEADER_SIZE - TYPE_ID.len()]) -> Result { let header = Self { @@ -40,19 +40,18 @@ impl TryFrom<[u8; HEADER_SIZE - TYPE_ID.len()]> for Header { content_type_flags: to_u32(&value[28..]), }; - if header.records_offset != 36 { - return Err(InvalidArgumentError { - message: format!( - "BSA file has an invalid records offset value: {}", - header.records_offset - ), - }); + if header.records_offset + != HEADER_SIZE + .try_into() + .expect("header size can fit in a u32") + { + return Err(ArchiveParsingError::InvalidRecordsOffset( + header.records_offset, + )); } if (header.archive_flags & 0x40) != 0 { - return Err(InvalidArgumentError { - message: "BSA file uses big-endian numbers".into(), - }); + return Err(ArchiveParsingError::UsesBigEndianNumbers); } Ok(header) @@ -104,7 +103,7 @@ mod v105 { pub(super) fn read_assets( mut reader: T, -) -> Result>, GeneralError> { +) -> Result>, ArchiveParsingError> { let mut header_buffer = [0; HEADER_SIZE - TYPE_ID.len()]; reader.read_exact(&mut header_buffer)?; @@ -122,10 +121,9 @@ pub(super) fn read_assets( &header, v105::read_folder_record, ), - _ => Err(InvalidArgumentError { - message: format!("BSA file has an unrecognised version: {}", header.version), - } - .into()), + _ => Err(ArchiveParsingError::UnsupportedHeaderVersion( + header.version, + )), } } @@ -133,7 +131,7 @@ fn read_assets_with_header( mut reader: T, header: &Header, read_folder_record: impl Fn(&[u8]) -> FolderRecord, -) -> Result>, GeneralError> { +) -> Result>, ArchiveParsingError> { let mut folders_buffer: Vec = vec![0; U * to_usize(header.folder_count)]; reader.read_exact(folders_buffer.as_mut_slice())?; @@ -155,13 +153,9 @@ fn read_assets_with_header( let entry = assets.entry(folder_record.name_hash); if let Entry::Occupied(_) = entry { - return Err(InvalidArgumentError { - message: format!( - "Unexpected collision for folder name hash {:x}", - folder_record.name_hash - ), - } - .into()); + return Err(ArchiveParsingError::FolderHashCollision( + folder_record.name_hash, + )); } let file_records_offset = if (header.archive_flags & 0x1) == 0 { @@ -173,20 +167,18 @@ fn read_assets_with_header( if let Some(folder_name_length) = file_records_buffer.get(folder_name_length_offset) { folder_name_length_offset + 1 + to_usize(u32::from(*folder_name_length)) } else { - return Err(InvalidArgumentError { - message: "BSA file contains an invalid folder name length offset".into(), - } - .into()); + return Err(ArchiveParsingError::InvalidFolderNameLengthOffset( + folder_name_length_offset, + )); } }; let file_records_buffer = match file_records_buffer.get(file_records_offset..) { Some(s) => s, None => { - return Err(InvalidArgumentError { - message: "BSA file contains an invalid file records offset".into(), - } - .into()); + return Err(ArchiveParsingError::InvalidFileRecordsOffset( + file_records_offset, + )); } }; @@ -199,7 +191,10 @@ fn read_assets_with_header( let file_hash = to_u64(file_chunk); if !file_hashes.insert(file_hash) { - return Err(InvalidArgumentError { message: format!("Unexpected collision for file name hash {:x} in set for folder name hash {:x}", file_hash, folder_record.name_hash)}.into()); + return Err(ArchiveParsingError::HashCollision { + folder_hash: folder_record.name_hash, + file_hash, + }); } } } diff --git a/src/archive/error.rs b/src/archive/error.rs new file mode 100644 index 00000000..8dafe31b --- /dev/null +++ b/src/archive/error.rs @@ -0,0 +1,95 @@ +use std::path::PathBuf; + +#[derive(Debug)] +pub(crate) struct ArchivePathParsingError { + path: PathBuf, + error: ArchiveParsingError, +} + +impl ArchivePathParsingError { + pub(crate) fn new(path: PathBuf, error: ArchiveParsingError) -> Self { + Self { path, error } + } + + pub(crate) fn from_io_error(path: PathBuf, error: std::io::Error) -> Self { + Self { + path, + error: ArchiveParsingError::IoError(error), + } + } +} + +impl std::fmt::Display for ArchivePathParsingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "failed to parse the archive at \"{}\"", + self.path.display() + ) + } +} + +impl std::error::Error for ArchivePathParsingError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.error) + } +} + +#[derive(Debug)] +pub(crate) enum ArchiveParsingError { + IoError(std::io::Error), + UnsupportedHeaderVersion(u32), + UnsupportedHeaderArchiveType([u8; 4]), + UnsupportedArchiveTypeId([u8; 4]), + InvalidRecordsOffset(u32), + InvalidFolderNameLengthOffset(usize), + InvalidFileRecordsOffset(usize), + UsesBigEndianNumbers, + FolderHashCollision(u64), + HashCollision { folder_hash: u64, file_hash: u64 }, +} + +impl std::fmt::Display for ArchiveParsingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::IoError(_) => write!(f, "an I/O error occurred"), + Self::UnsupportedHeaderVersion(v) => write!(f, "unsupported archive version {}", v), + Self::UnsupportedHeaderArchiveType(a) => write!(f, "unsupported archive type {:?}", a), + Self::UnsupportedArchiveTypeId(t) => write!(f, "unsupported archive type ID {:?}", t), + Self::InvalidRecordsOffset(o) => write!(f, "invalid records offset {}", o), + Self::InvalidFolderNameLengthOffset(o) => { + write!(f, "invalid folder name length offset {}", o) + } + Self::InvalidFileRecordsOffset(o) => write!(f, "invalid file records offset {}", o), + Self::UsesBigEndianNumbers => { + write!(f, "archive uses big-endian numbers, which is unsupported") + } + Self::FolderHashCollision(h) => { + write!(f, "unexpected collision for folder name hash {:x}", h) + } + Self::HashCollision { + folder_hash, + file_hash, + } => write!( + f, + "unexpected collision for file name hash {:x} in set for folder name hash {:x}", + file_hash, folder_hash + ), + } + } +} + +impl std::error::Error for ArchiveParsingError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::IoError(e) => Some(e), + _ => None, + } + } +} + +impl From for ArchiveParsingError { + fn from(value: std::io::Error) -> Self { + ArchiveParsingError::IoError(value) + } +} diff --git a/src/archive/mod.rs b/src/archive/mod.rs index cfe6cf56..16582db8 100644 --- a/src/archive/mod.rs +++ b/src/archive/mod.rs @@ -1,5 +1,6 @@ mod ba2; mod bsa; +mod error; mod find; mod parse; diff --git a/src/archive/parse.rs b/src/archive/parse.rs index c0825824..b8e3363c 100644 --- a/src/archive/parse.rs +++ b/src/archive/parse.rs @@ -5,10 +5,8 @@ use std::{ path::{Path, PathBuf}, }; -use crate::{ - error::{GeneralError, InvalidArgumentError}, - plugin::has_ascii_extension, -}; +use super::error::{ArchiveParsingError, ArchivePathParsingError}; +use crate::plugin::has_ascii_extension; use super::{ba2, bsa}; @@ -70,22 +68,25 @@ fn should_warn_on_hash_collisions(archive_path: &Path) -> bool { fn get_assets_in_archive( archive_path: &Path, -) -> Result>, GeneralError> { - let mut reader = BufReader::new(File::open(archive_path)?); +) -> Result>, ArchivePathParsingError> { + let file = File::open(archive_path) + .map_err(|e| ArchivePathParsingError::from_io_error(archive_path.into(), e))?; + let mut reader = BufReader::new(file); let mut type_id: [u8; 4] = [0; 4]; - reader.read_exact(&mut type_id)?; + reader + .read_exact(&mut type_id) + .map_err(|e| ArchivePathParsingError::from_io_error(archive_path.into(), e))?; match type_id { - bsa::TYPE_ID => bsa::read_assets(reader), - ba2::TYPE_ID => ba2::read_assets(reader), - _ => Err(InvalidArgumentError { - message: format!( - "Bethesda archive at \"{}\" has an unrecognised type ID", - archive_path.display() - ), - } - .into()), + bsa::TYPE_ID => bsa::read_assets(reader) + .map_err(|e| ArchivePathParsingError::new(archive_path.into(), e)), + ba2::TYPE_ID => ba2::read_assets(reader) + .map_err(|e| ArchivePathParsingError::new(archive_path.into(), e)), + _ => Err(ArchivePathParsingError::new( + archive_path.into(), + ArchiveParsingError::UnsupportedArchiveTypeId(type_id), + )), } } diff --git a/src/database/conditions.rs b/src/database/conditions.rs new file mode 100644 index 00000000..22640623 --- /dev/null +++ b/src/database/conditions.rs @@ -0,0 +1,108 @@ +use std::str::FromStr; + +use loot_condition_interpreter::Expression; + +use crate::metadata::{File, PluginCleaningData, PluginMetadata}; + +pub fn evaluate_all_conditions( + mut metadata: PluginMetadata, + state: &loot_condition_interpreter::State, +) -> Result, loot_condition_interpreter::Error> { + metadata.set_load_after_files(filter_files_on_conditions( + metadata.load_after_files(), + state, + )?); + + metadata.set_requirements(filter_files_on_conditions(metadata.requirements(), state)?); + + metadata.set_incompatibilities(filter_files_on_conditions( + metadata.incompatibilities(), + state, + )?); + + metadata.set_messages( + metadata + .messages() + .iter() + .filter_map(|m| filter_map_on_condition(m, m.condition(), state)) + .collect::, _>>()?, + ); + + metadata.set_tags( + metadata + .tags() + .iter() + .filter_map(|t| filter_map_on_condition(t, t.condition(), state)) + .collect::, _>>()?, + ); + + if !metadata.is_regex_plugin() { + metadata.set_dirty_info(filter_cleaning_data_on_conditions( + metadata.name(), + metadata.dirty_info(), + state, + )?); + + metadata.set_clean_info(filter_cleaning_data_on_conditions( + metadata.name(), + metadata.clean_info(), + state, + )?); + } + + if metadata.has_name_only() { + Ok(None) + } else { + Ok(Some(metadata)) + } +} + +fn evaluate_condition( + condition: Option<&str>, + state: &loot_condition_interpreter::State, +) -> Result { + if let Some(condition) = condition { + Expression::from_str(condition).and_then(|e| e.eval(state)) + } else { + Ok(true) + } +} + +pub fn filter_map_on_condition( + item: &T, + condition: Option<&str>, + state: &loot_condition_interpreter::State, +) -> Option> { + evaluate_condition(condition, state) + .map(|r| r.then(|| item.clone())) + .transpose() +} + +fn filter_files_on_conditions( + files: &[File], + state: &loot_condition_interpreter::State, +) -> Result, loot_condition_interpreter::Error> { + files + .iter() + .filter_map(|file| filter_map_on_condition(file, file.condition(), state)) + .collect::, _>>() +} + +fn filter_cleaning_data_on_conditions( + plugin_name: &str, + cleaning_info: &[PluginCleaningData], + state: &loot_condition_interpreter::State, +) -> Result, loot_condition_interpreter::Error> { + if plugin_name.is_empty() { + return Ok(Vec::new()); + } + + cleaning_info + .iter() + .filter_map(|i| { + let condition = format!("checksum(\"{}\", {:08X})", plugin_name, i.crc()); + + filter_map_on_condition(i, Some(condition.as_str()), state) + }) + .collect::, _>>() +} diff --git a/src/database/error.rs b/src/database/error.rs new file mode 100644 index 00000000..74d1b83f --- /dev/null +++ b/src/database/error.rs @@ -0,0 +1,57 @@ +use crate::metadata::error::RegexError; + +/// Represents an error that occurred while evaluating a metadata condition. +#[derive(Debug)] +pub struct ConditionEvaluationError(Box); + +impl std::fmt::Display for ConditionEvaluationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "failed to evaluate condition") + } +} + +impl std::error::Error for ConditionEvaluationError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.0) + } +} + +impl From for ConditionEvaluationError { + fn from(value: loot_condition_interpreter::Error) -> Self { + ConditionEvaluationError(Box::new(value)) + } +} + +/// Represents an error that occurred while retrieving metadata for a plugin. +#[derive(Debug)] +pub enum MetadataRetrievalError { + ConditionEvaluationError(ConditionEvaluationError), + RegexError(RegexError), +} + +impl std::fmt::Display for MetadataRetrievalError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "failed to retrieve metadata") + } +} + +impl std::error::Error for MetadataRetrievalError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::ConditionEvaluationError(e) => Some(e), + Self::RegexError(e) => Some(e), + } + } +} + +impl From for MetadataRetrievalError { + fn from(value: loot_condition_interpreter::Error) -> Self { + MetadataRetrievalError::ConditionEvaluationError(value.into()) + } +} + +impl From for MetadataRetrievalError { + fn from(value: RegexError) -> Self { + MetadataRetrievalError::RegexError(value) + } +} diff --git a/src/database.rs b/src/database/mod.rs similarity index 64% rename from src/database.rs rename to src/database/mod.rs index f3c50d21..5ab07747 100644 --- a/src/database.rs +++ b/src/database/mod.rs @@ -1,18 +1,23 @@ -use std::{path::Path, str::FromStr}; +mod conditions; +mod error; -use loot_condition_interpreter::Expression; +use std::{collections::HashMap, path::Path}; + +use conditions::{evaluate_all_conditions, filter_map_on_condition}; use crate::{ - error::{FileAccessError, GeneralError, InvalidArgumentError}, metadata::{ - File, Group, Message, PluginCleaningData, PluginMetadata, + Group, Message, PluginMetadata, + error::{LoadMetadataError, WriteMetadataError, WriteMetadataErrorReason}, metadata_document::MetadataDocument, }, sorting::{ + error::GroupsPathError, groups::{build_groups_graph, find_path}, vertex::Vertex, }, }; +pub use error::{ConditionEvaluationError, MetadataRetrievalError}; /// The interface through which metadata can be accessed. #[derive(Debug)] @@ -38,21 +43,18 @@ impl Database { &mut self.condition_evaluator_state } + pub(crate) fn clear_condition_cache(&mut self) { + if let Err(e) = self.condition_evaluator_state.clear_condition_cache() { + log::error!("The condition cache's lock is poisoned, assigning a new cache"); + *e.into_inner() = HashMap::new(); + } + } + /// Loads the masterlist from the given path. /// /// Replaces any existing data that was previously loaded from a masterlist. - pub fn load_masterlist(&mut self, path: &Path) -> Result<(), GeneralError> { - if path.exists() { - self.masterlist.load(path) - } else { - Err(FileAccessError { - message: format!( - "The given masterlist path does not exist: {}", - path.display() - ), - } - .into()) - } + pub fn load_masterlist(&mut self, path: &Path) -> Result<(), LoadMetadataError> { + self.masterlist.load(path) } /// Loads the masterlist from the given path, using the prelude at the given @@ -63,41 +65,16 @@ impl Database { &mut self, masterlist_path: &Path, prelude_path: &Path, - ) -> Result<(), GeneralError> { - if !masterlist_path.exists() { - Err(FileAccessError { - message: format!( - "The given masterlist path does not exist: {}", - masterlist_path.display() - ), - } - .into()) - } else if !prelude_path.exists() { - Err(FileAccessError { - message: format!( - "The given prelude path does not exist: {}", - prelude_path.display() - ), - } - .into()) - } else { - self.masterlist - .load_with_prelude(masterlist_path, prelude_path) - } + ) -> Result<(), LoadMetadataError> { + self.masterlist + .load_with_prelude(masterlist_path, prelude_path) } /// Loads the userlist from the given path. /// /// Replaces any existing data that was previously loaded from a userlist. - pub fn load_userlist(&mut self, path: &Path) -> Result<(), GeneralError> { - if path.exists() { - self.userlist.load(path) - } else { - Err(FileAccessError { - message: format!("The given userlist path does not exist: {}", path.display()), - } - .into()) - } + pub fn load_userlist(&mut self, path: &Path) -> Result<(), LoadMetadataError> { + self.userlist.load(path) } /// Writes a metadata file containing all loaded user-added metadata. @@ -108,7 +85,7 @@ impl Database { &self, output_path: &Path, overwrite: bool, - ) -> Result<(), GeneralError> { + ) -> Result<(), WriteMetadataError> { validate_write_path(output_path, overwrite)?; self.userlist.save(output_path) @@ -123,13 +100,14 @@ impl Database { &self, output_path: &Path, overwrite: bool, - ) -> Result<(), GeneralError> { + ) -> Result<(), WriteMetadataError> { validate_write_path(output_path, overwrite)?; let mut doc = MetadataDocument::default(); for plugin in self.masterlist.plugins() { - let mut minimal_plugin = PluginMetadata::new(plugin.name())?; + let mut minimal_plugin = PluginMetadata::new(plugin.name()) + .expect("Regex plugin name from existing PluginMetadata object is valid"); minimal_plugin.set_tags(plugin.tags().to_vec()); minimal_plugin.set_dirty_info(plugin.dirty_info().to_vec()); @@ -158,7 +136,11 @@ impl Database { pub fn general_messages( &mut self, evaluate_conditions: bool, - ) -> Result, GeneralError> { + ) -> Result, ConditionEvaluationError> { + if evaluate_conditions { + self.clear_condition_cache(); + } + let messages_iter = self .masterlist .messages() @@ -166,8 +148,6 @@ impl Database { .chain(self.userlist.messages()); if evaluate_conditions { - self.condition_evaluator_state.clear_condition_cache()?; - let messages = messages_iter .filter_map(|m| { filter_map_on_condition(m, m.condition(), &self.condition_evaluator_state) @@ -217,10 +197,12 @@ impl Database { &self, from_group_name: &str, to_group_name: &str, - ) -> Result, GeneralError> { + ) -> Result, GroupsPathError> { let graph = build_groups_graph(self.masterlist.groups(), self.userlist.groups())?; - find_path(&graph, from_group_name, to_group_name) + let path = find_path(&graph, from_group_name, to_group_name)?; + + Ok(path) } /// Get all of a plugin's loaded metadata. @@ -238,7 +220,7 @@ impl Database { plugin_name: &str, include_user_metadata: bool, evaluate_conditions: bool, - ) -> Result, GeneralError> { + ) -> Result, MetadataRetrievalError> { let mut metadata = self.masterlist.find_plugin(plugin_name)?; if include_user_metadata { @@ -270,17 +252,17 @@ impl Database { &self, plugin_name: &str, evaluate_conditions: bool, - ) -> Result, GeneralError> { - let metadata = self.userlist.find_plugin(plugin_name); + ) -> Result, MetadataRetrievalError> { + let metadata = self.userlist.find_plugin(plugin_name)?; if evaluate_conditions { - if let Ok(Some(metadata)) = metadata { + if let Some(metadata) = metadata { return evaluate_all_conditions(metadata, &self.condition_evaluator_state) .map_err(Into::into); } } - metadata + Ok(metadata) } /// Sets a plugin's user metadata, replacing any loaded user metadata for @@ -302,17 +284,17 @@ impl Database { } } -fn validate_write_path(output_path: &Path, overwrite: bool) -> Result<(), GeneralError> { +fn validate_write_path(output_path: &Path, overwrite: bool) -> Result<(), WriteMetadataError> { if !output_path.parent().map(|p| p.exists()).unwrap_or(false) { - Err(InvalidArgumentError { - message: "The output directory does not exist.".into(), - } - .into()) + Err(WriteMetadataError::new( + output_path.into(), + WriteMetadataErrorReason::ParentDirectoryNotFound, + )) } else if !overwrite && output_path.exists() { - Err(FileAccessError { - message: "Output file exists but overwrite is not set to true.".into(), - } - .into()) + Err(WriteMetadataError::new( + output_path.into(), + WriteMetadataErrorReason::PathAlreadyExists, + )) } else { Ok(()) } @@ -350,106 +332,3 @@ fn merge_groups(lhs: &[Group], rhs: &[Group]) -> Vec { groups } - -fn evaluate_all_conditions( - mut metadata: PluginMetadata, - state: &loot_condition_interpreter::State, -) -> Result, loot_condition_interpreter::Error> { - metadata.set_load_after_files(filter_files_on_conditions( - metadata.load_after_files(), - state, - )?); - - metadata.set_requirements(filter_files_on_conditions(metadata.requirements(), state)?); - - metadata.set_incompatibilities(filter_files_on_conditions( - metadata.incompatibilities(), - state, - )?); - - metadata.set_messages( - metadata - .messages() - .iter() - .filter_map(|m| filter_map_on_condition(m, m.condition(), state)) - .collect::, _>>()?, - ); - - metadata.set_tags( - metadata - .tags() - .iter() - .filter_map(|t| filter_map_on_condition(t, t.condition(), state)) - .collect::, _>>()?, - ); - - if !metadata.is_regex_plugin() { - metadata.set_dirty_info(filter_cleaning_data_on_conditions( - metadata.name(), - metadata.dirty_info(), - state, - )?); - - metadata.set_clean_info(filter_cleaning_data_on_conditions( - metadata.name(), - metadata.clean_info(), - state, - )?); - } - - if metadata.has_name_only() { - Ok(None) - } else { - Ok(Some(metadata)) - } -} - -fn evaluate_condition( - condition: Option<&str>, - state: &loot_condition_interpreter::State, -) -> Result { - if let Some(condition) = condition { - Expression::from_str(condition).and_then(|e| e.eval(state)) - } else { - Ok(true) - } -} - -fn filter_map_on_condition( - item: &T, - condition: Option<&str>, - state: &loot_condition_interpreter::State, -) -> Option> { - evaluate_condition(condition, state) - .map(|r| r.then(|| item.clone())) - .transpose() -} - -fn filter_files_on_conditions( - files: &[File], - state: &loot_condition_interpreter::State, -) -> Result, loot_condition_interpreter::Error> { - files - .iter() - .filter_map(|file| filter_map_on_condition(file, file.condition(), state)) - .collect::, _>>() -} - -fn filter_cleaning_data_on_conditions( - plugin_name: &str, - cleaning_info: &[PluginCleaningData], - state: &loot_condition_interpreter::State, -) -> Result, loot_condition_interpreter::Error> { - if plugin_name.is_empty() { - return Ok(Vec::new()); - } - - cleaning_info - .iter() - .filter_map(|i| { - let condition = format!("checksum(\"{}\", {:08X})", plugin_name, i.crc()); - - filter_map_on_condition(i, Some(condition.as_str()), state) - }) - .collect::, _>>() -} diff --git a/src/error.rs b/src/error.rs index 264a6dbd..00d998dd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,353 +1,275 @@ -use std::fmt::Display; +//! Holds all error types aside from those related to LOOT metadata. +use std::path::PathBuf; -use petgraph::graph::NodeIndex; -use saphyr::Marker; +pub use crate::database::{ConditionEvaluationError, MetadataRetrievalError}; +pub use crate::plugin::error::PluginDataError; +use crate::plugin::error::PluginValidationError; +pub use crate::sorting::error::GroupsPathError; -use crate::{ - metadata::{ - MessageContent, - yaml::{YamlObjectType, to_yaml}, - }, - sorting::vertex::Vertex, +use crate::Vertex; +use crate::sorting::error::{ + BuildGroupsGraphError, PluginGraphValidationError, SortingError, display_cycle, }; +/// Represents an error that occurred while trying to create a [Game][crate::Game]. #[derive(Debug)] -pub struct CyclicInteractionError { - pub cycle: Vec, +#[non_exhaustive] +pub enum GameHandleCreationError { + NotADirectory(PathBuf), + LoadOrderError(LoadOrderError), } -impl CyclicInteractionError { - pub fn new(cycle: Vec) -> Self { - Self { cycle } - } -} - -impl Display for CyclicInteractionError { +impl std::fmt::Display for GameHandleCreationError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let cycle: String = self - .cycle - .iter() - .map(|v| { - if let Some(edge_type) = v.out_edge_type() { - format!("{} --[{}]-> ", v.name(), edge_type) - } else { - v.name().to_string() - } - }) - .chain(self.cycle.first().iter().map(|v| v.name().to_string())) - .collect(); - write!(f, "Cyclic interaction detected: {}", cycle) - } -} - -impl std::error::Error for CyclicInteractionError {} - -#[derive(Debug)] -pub struct FileAccessError { - pub message: String, -} - -impl FileAccessError { - pub(crate) fn new(message: String) -> Self { - FileAccessError { message } - } -} - -impl std::fmt::Display for FileAccessError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.message) - } -} - -impl std::error::Error for FileAccessError {} - -#[derive(Debug)] -pub struct UndefinedGroupError { - pub group_name: String, -} - -impl UndefinedGroupError { - pub fn new(group_name: String) -> Self { - Self { group_name } - } -} - -impl Display for UndefinedGroupError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "The group \"{}\" does not exist", self.group_name) - } -} - -impl std::error::Error for UndefinedGroupError {} - -#[derive(Debug)] -pub struct InvalidArgumentError { - pub message: String, -} - -impl std::fmt::Display for InvalidArgumentError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.message) - } -} - -impl std::error::Error for InvalidArgumentError {} - -#[derive(Debug)] -pub(crate) struct YamlMergeKeyError { - value: saphyr::MarkedYaml, -} - -impl YamlMergeKeyError { - pub(crate) fn new(value: saphyr::MarkedYaml) -> Self { - YamlMergeKeyError { value } - } -} - -impl std::fmt::Display for YamlMergeKeyError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut output = String::new(); - - let yaml = to_yaml(&self.value); - - if saphyr::YamlEmitter::new(&mut output).dump(&yaml).is_ok() { - write!( + match self { + Self::NotADirectory(p) => write!( f, - "Invalid YAML merge key value at line {} column {}: {}", - self.value.span.start.line(), - self.value.span.start.col(), - output - ) - } else { - write!( - f, - "Invalid YAML merge key value at line {} column {}: {:?}", - self.value.span.start.line(), - self.value.span.start.col(), - self.value - ) + "the path \"{}\" does not resolve to a directory", + p.display() + ), + Self::LoadOrderError(_) => { + write!(f, "failed to initialise the load order game settings") + } } } } -impl std::error::Error for YamlMergeKeyError {} - -#[derive(Debug)] -pub struct YamlParseError { - marker: saphyr::Marker, - message: String, -} - -impl YamlParseError { - pub fn new(marker: Marker, message: String) -> Self { - YamlParseError { marker, message } - } - - pub fn missing_key(marker: Marker, key: &str, yaml_type: YamlObjectType) -> Self { - YamlParseError::new( - marker, - format!("'{}' key missing from '{}' map object", key, yaml_type), - ) - } -} - -impl std::fmt::Display for YamlParseError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "Encountered a YAML parsing error at line {} column {}: {}", - self.marker.line(), - self.marker.col(), - self.message - ) - } -} - -impl std::error::Error for YamlParseError {} - -#[derive(Debug)] -pub struct InvalidMultilingualMessageContents {} - -impl std::fmt::Display for InvalidMultilingualMessageContents { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "Multilingual messages must contain a content string that uses the {} language code", - MessageContent::DEFAULT_LANGUAGE - ) - } -} - -impl std::error::Error for InvalidMultilingualMessageContents {} - -#[derive(Debug)] -pub struct PoisonedMutexError; - -impl std::fmt::Display for PoisonedMutexError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "A mutex is poisoned",) - } -} - -impl std::error::Error for PoisonedMutexError {} - -#[derive(Debug)] -pub struct PathfindingError { - message: String, -} - -impl PathfindingError { - pub fn new(message: String) -> Self { - Self { message } - } -} - -impl std::fmt::Display for PathfindingError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.message) - } -} - -impl std::error::Error for PathfindingError {} - -#[derive(Debug)] -pub struct SortingLogicError { - message: String, -} - -impl SortingLogicError { - pub fn new(message: String) -> Self { - Self { message } - } -} - -impl std::fmt::Display for SortingLogicError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.message) - } -} - -impl std::error::Error for SortingLogicError {} - -#[derive(Debug)] -pub struct GeneralError(Box); - -impl std::fmt::Display for GeneralError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -impl std::error::Error for GeneralError { +impl std::error::Error for GameHandleCreationError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - Some(self.0.as_ref()) + match self { + Self::NotADirectory(_) => None, + Self::LoadOrderError(e) => Some(e), + } } } -impl From for GeneralError { - fn from(value: std::io::Error) -> Self { - GeneralError(Box::new(value)) - } -} - -impl From for GeneralError { - fn from(value: saphyr::ScanError) -> Self { - GeneralError(Box::new(value)) - } -} - -impl From for GeneralError { - fn from(value: YamlMergeKeyError) -> Self { - GeneralError(Box::new(value)) - } -} - -impl From for GeneralError { - fn from(value: FileAccessError) -> Self { - GeneralError(Box::new(value)) - } -} - -impl From for GeneralError { - fn from(value: YamlParseError) -> Self { - GeneralError(Box::new(value)) - } -} - -impl From for GeneralError { - fn from(value: loot_condition_interpreter::Error) -> Self { - GeneralError(Box::new(value)) - } -} - -impl From for GeneralError { - fn from(value: std::num::TryFromIntError) -> Self { - GeneralError(Box::new(value)) - } -} - -impl From for GeneralError { - fn from(value: InvalidMultilingualMessageContents) -> Self { - GeneralError(Box::new(value)) - } -} - -impl From for GeneralError { - fn from(value: InvalidArgumentError) -> Self { - GeneralError(Box::new(value)) - } -} - -impl From for GeneralError { +impl From for GameHandleCreationError { fn from(value: loadorder::Error) -> Self { - GeneralError(Box::new(value)) + GameHandleCreationError::LoadOrderError(value.into()) } } -impl From> for GeneralError { +/// Represents an error that occurred while trying to interact with the load order. +#[derive(Debug)] +pub struct LoadOrderError(Box); + +impl std::fmt::Display for LoadOrderError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "load order interaction failed") + } +} + +impl std::error::Error for LoadOrderError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.0) + } +} + +impl From for LoadOrderError { + fn from(value: loadorder::Error) -> Self { + LoadOrderError(Box::new(value)) + } +} + +/// Indicates that the Database's RwLock wrapper has been poisoned and as such +/// the Database may be in an invalid state. +#[derive(Clone, Copy, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct DatabaseLockPoisonError; + +impl std::fmt::Display for DatabaseLockPoisonError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "the database's lock has been poisoned") + } +} + +impl std::error::Error for DatabaseLockPoisonError {} + +impl From> for DatabaseLockPoisonError { fn from(_: std::sync::PoisonError) -> Self { - GeneralError(Box::new(PoisonedMutexError)) + DatabaseLockPoisonError } } -impl From for GeneralError { - fn from(value: esplugin::Error) -> Self { - GeneralError(Box::new(value)) +/// Represents an error that occurred while loading plugins. +#[derive(Debug)] +#[non_exhaustive] +pub enum LoadPluginsError { + DatabaseLockPoisoned, + IoError(Box), + PluginValidationError(Box), + PluginDataError(PluginDataError), +} + +impl std::fmt::Display for LoadPluginsError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::DatabaseLockPoisoned => DatabaseLockPoisonError.fmt(f), + Self::IoError(_) => write!(f, "an I/O error occurred"), + Self::PluginValidationError(_) => write!(f, "failed validation of input plugin paths"), + Self::PluginDataError(_) => write!(f, "failed to read loaded plugin data"), + } } } -impl From> for GeneralError { - fn from(value: Box) -> Self { - GeneralError(value) +impl std::error::Error for LoadPluginsError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::DatabaseLockPoisoned => None, + Self::IoError(e) => Some(e), + Self::PluginValidationError(e) => Some(e.as_ref()), + Self::PluginDataError(e) => Some(e), + } } } -impl From for GeneralError { - fn from(value: UndefinedGroupError) -> Self { - GeneralError(Box::new(value)) +impl From> for LoadPluginsError { + fn from(_: std::sync::PoisonError) -> Self { + LoadPluginsError::DatabaseLockPoisoned } } -impl From for GeneralError { - fn from(value: CyclicInteractionError) -> Self { - GeneralError(Box::new(value)) +impl From for LoadPluginsError { + fn from(_: DatabaseLockPoisonError) -> Self { + LoadPluginsError::DatabaseLockPoisoned } } -impl From for GeneralError { - fn from(value: PathfindingError) -> Self { - GeneralError(Box::new(value)) +impl From for LoadPluginsError { + fn from(value: std::io::Error) -> Self { + LoadPluginsError::IoError(Box::new(value)) } } -impl From> for GeneralError { - fn from(_: petgraph::algo::Cycle) -> Self { - GeneralError(Box::new(CyclicInteractionError { cycle: Vec::new() })) +impl From for LoadPluginsError { + fn from(value: PluginValidationError) -> Self { + LoadPluginsError::PluginValidationError(Box::new(value)) } } -impl From for GeneralError { - fn from(value: SortingLogicError) -> Self { - GeneralError(Box::new(value)) +impl From for LoadPluginsError { + fn from(value: PluginDataError) -> Self { + LoadPluginsError::PluginDataError(value) + } +} + +/// Represents an error that occurred while trying to load the current load +/// order state. +#[derive(Debug)] +#[non_exhaustive] +pub enum LoadOrderStateError { + DatabaseLockPoisoned, + LoadOrderError(LoadOrderError), +} + +impl std::fmt::Display for LoadOrderStateError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::DatabaseLockPoisoned => DatabaseLockPoisonError.fmt(f), + Self::LoadOrderError(_) => write!(f, "failed to load the current load order state"), + } + } +} + +impl std::error::Error for LoadOrderStateError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::DatabaseLockPoisoned => None, + Self::LoadOrderError(e) => Some(e), + } + } +} + +impl From> for LoadOrderStateError { + fn from(_: std::sync::PoisonError) -> Self { + LoadOrderStateError::DatabaseLockPoisoned + } +} + +impl From for LoadOrderStateError { + fn from(value: loadorder::Error) -> Self { + LoadOrderStateError::LoadOrderError(value.into()) + } +} + +/// Represents an error that occurred during sorting. +#[derive(Debug)] +#[non_exhaustive] +pub enum SortPluginsError { + DatabaseLockPoisoned, + PluginNotLoaded(String), + MetadataRetrievalError(MetadataRetrievalError), + UndefinedGroup(String), + CycleFound(Vec), + CycleFoundInvolving(String), + PluginDataError(PluginDataError), + PathfindingError(Box), +} + +impl std::fmt::Display for SortPluginsError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::DatabaseLockPoisoned => DatabaseLockPoisonError.fmt(f), + Self::PluginNotLoaded(n) => write!(f, "the plugin \"{}\" has not been loaded", n), + Self::UndefinedGroup(g) => write!(f, "the group \"{}\" does not exist", g), + Self::CycleFound(c) => write!(f, "found a cycle: {}", display_cycle(c)), + Self::CycleFoundInvolving(n) => write!(f, "found a cycle involving \"{}\"", n), + Self::PluginDataError(_) => write!(f, "failed to read loaded plugin data"), + Self::MetadataRetrievalError(_) => write!(f, "failed to retrieve plugin metadata"), + Self::PathfindingError(_) => write!(f, "failed to find a path in the plugins graph"), + } + } +} + +impl std::error::Error for SortPluginsError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::MetadataRetrievalError(e) => Some(e), + Self::PluginDataError(e) => Some(e), + Self::PathfindingError(e) => Some(e.as_ref()), + _ => None, + } + } +} + +impl From> for SortPluginsError { + fn from(_: std::sync::PoisonError) -> Self { + SortPluginsError::DatabaseLockPoisoned + } +} + +impl From for SortPluginsError { + fn from(value: SortingError) -> Self { + match value { + SortingError::ValidationError(e) => match e { + PluginGraphValidationError::CycleFound(c) => Self::CycleFound(c.into_cycle()), + PluginGraphValidationError::PluginDataError(e) => Self::PluginDataError(e), + }, + SortingError::UndefinedGroup(g) => Self::UndefinedGroup(g.into_group_name()), + SortingError::CycleFound(c) => Self::CycleFound(c.into_cycle()), + SortingError::CycleInvolving(n) => Self::CycleFoundInvolving(n), + SortingError::PluginDataError(e) => Self::PluginDataError(e), + SortingError::PathfindingError(e) => Self::PathfindingError(Box::new(e)), + } + } +} + +impl From for SortPluginsError { + fn from(value: BuildGroupsGraphError) -> Self { + match value { + BuildGroupsGraphError::UndefinedGroup(g) => Self::UndefinedGroup(g.into_group_name()), + BuildGroupsGraphError::CycleFound(c) => Self::CycleFound(c.into_cycle()), + } + } +} + +impl From for SortPluginsError { + fn from(value: PluginDataError) -> Self { + SortPluginsError::PluginDataError(value) + } +} + +impl From for SortPluginsError { + fn from(value: MetadataRetrievalError) -> Self { + SortPluginsError::MetadataRetrievalError(value) } } diff --git a/src/game.rs b/src/game.rs index ba513080..9fa2f1ea 100644 --- a/src/game.rs +++ b/src/game.rs @@ -10,12 +10,16 @@ use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use crate::{ database::Database, - error::{GeneralError, InvalidArgumentError}, + error::{ + DatabaseLockPoisonError, GameHandleCreationError, LoadOrderError, LoadOrderStateError, + LoadPluginsError, SortPluginsError, + }, metadata::{ Filename, plugin_metadata::{GHOST_FILE_EXTENSION, iends_with_ascii}, }, - plugin::{LoadScope, Plugin, is_valid_plugin, plugins_metadata}, + plugin::error::{InvalidFilenameReason, PluginValidationError}, + plugin::{LoadScope, Plugin, plugins_metadata, validate_plugin_path_and_header}, sorting::{ groups::build_groups_graph, plugins::{PluginSortingData, sort_plugins}, @@ -144,7 +148,7 @@ impl Game { /// may fail in some situations (e.g. when running libloot natively on Linux /// for a game other than Morrowind or OpenMW). [Game::with_local_path] /// can be used to provide the local path instead. - pub fn new(game_type: GameType, game_path: &Path) -> Result { + pub fn new(game_type: GameType, game_path: &Path) -> Result { log::info!( "Attempting to create a game handle for game type {} with game path {:?}", game_type, @@ -153,13 +157,7 @@ impl Game { let resolved_game_path = resolve_path(game_path); if !resolved_game_path.is_dir() { - return Err(InvalidArgumentError { - message: format!( - "Given game path \"{:?}\" does not resolve to a valid directory.", - game_path - ), - } - .into()); + return Err(GameHandleCreationError::NotADirectory(game_path.into())); } let load_order = @@ -192,7 +190,7 @@ impl Game { game_type: GameType, game_path: &Path, game_local_path: &Path, - ) -> Result { + ) -> Result { log::info!( "Attempting to create a game handle for game type {} with game path {:?} and game local path {:?}", game_type, @@ -202,23 +200,14 @@ impl Game { let resolved_game_path = resolve_path(game_path); if !resolved_game_path.is_dir() { - return Err(InvalidArgumentError { - message: format!( - "Given game path \"{:?}\" does not resolve to a valid directory.", - game_path - ), - } - .into()); + return Err(GameHandleCreationError::NotADirectory(game_path.into())); } let resolved_game_local_path = resolve_path(game_local_path); if resolved_game_local_path.exists() && !resolved_game_local_path.is_dir() { - return Err(InvalidArgumentError { - message: format!( - "Given game local path \"{:?}\" resolves to a path that exists but is not a valid directory.", - game_local_path - ), - }.into()); + return Err(GameHandleCreationError::NotADirectory( + game_local_path.into(), + )); } let load_order = loadorder::GameSettings::with_local_path( @@ -272,21 +261,22 @@ impl Game { pub fn set_additional_data_paths( &mut self, additional_data_paths: &[&Path], - ) -> Result<(), GeneralError> { + ) -> Result<(), DatabaseLockPoisonError> { let paths: Vec<_> = additional_data_paths .iter() .map(|p| p.to_path_buf()) .collect(); let mut database = self.database.write()?; - let state = database.condition_evaluator_state_mut(); - state.clear_condition_cache()?; + database.clear_condition_cache(); self.load_order .game_settings_mut() .set_additional_plugins_directories(paths.clone()); - state.set_additional_data_paths(paths); + database + .condition_evaluator_state_mut() + .set_additional_data_paths(paths); Ok(()) } @@ -306,7 +296,7 @@ impl Game { /// relative to the game's plugins directory, while absolute paths are used /// as given. pub fn is_valid_plugin(&self, plugin_path: &Path) -> bool { - is_valid_plugin(self.game_type, plugin_path) + validate_plugin_path_and_header(self.game_type, plugin_path).is_ok() } /// Fully parses plugins and loads their data. @@ -324,7 +314,7 @@ impl Game { /// /// Loading plugins clears the condition cache in this game's database /// object. - pub fn load_plugins(&mut self, plugin_paths: &[&Path]) -> Result<(), GeneralError> { + pub fn load_plugins(&mut self, plugin_paths: &[&Path]) -> Result<(), LoadPluginsError> { let mut plugins = self.load_plugins_common(plugin_paths, LoadScope::WholePlugin)?; if matches!( @@ -338,7 +328,9 @@ impl Game { } } - self.store_plugins(plugins) + self.store_plugins(plugins)?; + + Ok(()) } /// Parses plugin headers and loads their data. @@ -352,17 +344,19 @@ impl Game { /// /// Loading plugins clears the condition cache in this game's database /// object. - pub fn load_plugin_headers(&mut self, plugin_paths: &[&Path]) -> Result<(), GeneralError> { + pub fn load_plugin_headers(&mut self, plugin_paths: &[&Path]) -> Result<(), LoadPluginsError> { let plugins = self.load_plugins_common(plugin_paths, LoadScope::HeaderOnly)?; - self.store_plugins(plugins) + self.store_plugins(plugins)?; + + Ok(()) } fn load_plugins_common( &mut self, plugin_paths: &[&Path], load_scope: LoadScope, - ) -> Result, GeneralError> { + ) -> Result, LoadPluginsError> { validate_plugin_paths(self.game_type, plugin_paths)?; let data_path = data_path(self.game_type, &self.game_path); @@ -384,14 +378,16 @@ impl Game { Ok(plugins) } - fn store_plugins(&mut self, plugins: Vec) -> Result<(), GeneralError> { + fn store_plugins(&mut self, plugins: Vec) -> Result<(), DatabaseLockPoisonError> { self.cache.insert_plugins(plugins); let mut database = self.database.write()?; update_loaded_plugin_state( database.condition_evaluator_state_mut(), self.cache.plugins(), - ) + ); + + Ok(()) } /// Clears the plugins loaded by previous calls to [Game::load_plugins] or @@ -421,13 +417,12 @@ impl Game { /// The order in which plugins are listed in `plugin_filenames` is used as /// their current load order. All given plugins must have been already been /// loaded using [Game::load_plugins] or [Game::load_plugin_headers]. - pub fn sort_plugins(&self, plugin_names: &[&str]) -> Result, GeneralError> { + pub fn sort_plugins(&self, plugin_names: &[&str]) -> Result, SortPluginsError> { let plugins = plugin_names .iter() .map(|n| { - self.plugin(n).ok_or_else(|| InvalidArgumentError { - message: format!("The plugin \"{}\" has not been loaded.", n), - }) + self.plugin(n) + .ok_or_else(|| SortPluginsError::PluginNotLoaded(n.to_string())) }) .collect::, _>>()?; @@ -439,7 +434,13 @@ impl Game { .map(|(i, p)| { let masterlist_metadata = database.plugin_metadata(p.name(), false, true)?; let user_metadata = database.plugin_user_metadata(p.name(), true)?; - PluginSortingData::new(p, masterlist_metadata.as_ref(), user_metadata.as_ref(), i) + let plugin = PluginSortingData::new( + p, + masterlist_metadata.as_ref(), + user_metadata.as_ref(), + i, + )?; + Ok::<_, SortPluginsError>(plugin) }) .collect::, _>>()?; @@ -476,13 +477,14 @@ impl Game { /// /// Loading the current load order state clears the condition cache in this /// game's database object. - pub fn load_current_load_order_state(&mut self) -> Result<(), GeneralError> { + pub fn load_current_load_order_state(&mut self) -> Result<(), LoadOrderStateError> { self.load_order.load()?; let mut database = self.database.write()?; - let state = database.condition_evaluator_state_mut(); - state.clear_condition_cache()?; - state.set_active_plugins(&self.load_order.active_plugin_names()); + database.clear_condition_cache(); + database + .condition_evaluator_state_mut() + .set_active_plugins(&self.load_order.active_plugin_names()); Ok(()) } @@ -492,8 +494,8 @@ impl Game { /// well-defined position in the "on disk" state, and that all data sources /// are consistent. If the load order is ambiguous, different applications /// may read different load orders from the same source data. - pub fn is_load_order_ambiguous(&self) -> Result { - self.load_order.is_ambiguous() + pub fn is_load_order_ambiguous(&self) -> Result { + Ok(self.load_order.is_ambiguous()?) } /// Gets the path to the file that holds the list of active plugins. @@ -519,8 +521,9 @@ impl Game { /// There is no way to persist the load order of inactive OpenMW plugins, so /// setting an OpenMW load order will have no effect if the relative order /// of active plugins is unchanged. - pub fn set_load_order(&mut self, load_order: &[&str]) -> Result<(), loadorder::Error> { - self.load_order.set_load_order(load_order) + pub fn set_load_order(&mut self, load_order: &[&str]) -> Result<(), LoadOrderError> { + self.load_order.set_load_order(load_order)?; + Ok(()) } } @@ -562,35 +565,31 @@ fn new_condition_evaluator_state( fn validate_plugin_paths( game_type: GameType, plugin_paths: &[&Path], -) -> Result<(), InvalidArgumentError> { +) -> Result<(), PluginValidationError> { // Check that all plugin filenames are unique. let mut set = HashSet::new(); for path in plugin_paths { let filename = match path.file_name() { Some(f) => f.to_string_lossy(), None => { - return Err(InvalidArgumentError { - message: format!("The path \"{}\" has no filename.", path.display()), - }); + return Err(PluginValidationError::invalid( + path.into(), + InvalidFilenameReason::Empty, + )); } }; if !set.insert(Filename::new(filename.to_string())) { - return Err(InvalidArgumentError { - message: format!("The filename \"{}\" is not unique.", filename), - }); + return Err(PluginValidationError::invalid( + path.into(), + InvalidFilenameReason::NonUnique, + )); } } - let invalid_path = plugin_paths + plugin_paths .par_iter() - .find_any(|path| !is_valid_plugin(game_type, path)); - if let Some(invalid_path) = invalid_path { - return Err(InvalidArgumentError { - message: format!("\"{}\" is not a valid plugin", invalid_path.display()), - }); - } - - Ok(()) + .map(|path| validate_plugin_path_and_header(game_type, path)) + .collect::>() } fn find_archives( @@ -685,7 +684,7 @@ fn resolve_plugin_path(game_type: GameType, data_path: &Path, plugin_path: &Path fn update_loaded_plugin_state<'a>( state: &mut loot_condition_interpreter::State, plugins: impl Iterator, -) -> Result<(), GeneralError> { +) { let mut plugin_versions = Vec::new(); let mut plugin_crcs = Vec::new(); @@ -699,13 +698,24 @@ fn update_loaded_plugin_state<'a>( } } - state.clear_condition_cache()?; + if let Err(e) = state.clear_condition_cache() { + log::error!("The condition cache's lock is poisoned, assigning a new cache"); + *e.into_inner() = HashMap::new(); + } state.set_plugin_versions(&plugin_versions); - state.set_cached_crcs(&plugin_crcs)?; - - Ok(()) + if let Err(e) = state.set_cached_crcs(&plugin_crcs) { + log::error!( + "The condition interpreter's CRC cache's lock is poisoned, clearing the cache and assigning a new value" + ); + let mut cache = e.into_inner(); + cache.clear(); + *cache = plugin_crcs + .into_iter() + .map(|(n, c)| (n.to_lowercase(), c)) + .collect(); + } } #[derive(Clone, Debug, Default, Eq, PartialEq)] diff --git a/src/metadata/error.rs b/src/metadata/error.rs new file mode 100644 index 00000000..f015190a --- /dev/null +++ b/src/metadata/error.rs @@ -0,0 +1,399 @@ +//! Holds all error types related to LOOT metadata. +use std::path::PathBuf; + +use saphyr::Marker; + +use crate::metadata::MessageContent; + +use super::yaml::{YamlObjectType, to_yaml}; + +/// Represents an error that occurred when validating a collection of +/// [MessageContent] objects. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct MultilingualMessageContentsError; + +impl std::fmt::Display for MultilingualMessageContentsError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "multilingual messages must contain a content string that uses the {} language code", + MessageContent::DEFAULT_LANGUAGE + ) + } +} + +impl std::error::Error for MultilingualMessageContentsError {} + +/// Represents an error that occurred while parsing metadata. +#[derive(Debug)] +pub struct ParseMetadataError { + marker: saphyr::Marker, + reason: MetadataParsingErrorReason, +} + +impl ParseMetadataError { + pub(super) fn new(marker: Marker, reason: MetadataParsingErrorReason) -> Self { + Self { marker, reason } + } + + pub(super) fn invalid_condition( + marker: Marker, + condition: String, + cause: loot_condition_interpreter::Error, + ) -> Self { + Self { + marker, + reason: MetadataParsingErrorReason::InvalidCondition(Box::new((condition, cause))), + } + } + + pub(super) fn missing_key( + marker: Marker, + key: &'static str, + yaml_type: YamlObjectType, + ) -> Self { + Self { + marker, + reason: MetadataParsingErrorReason::MissingKey(key, yaml_type), + } + } + + pub(super) fn duplicate_entry(marker: Marker, id: String, yaml_type: YamlObjectType) -> Self { + Self { + marker, + reason: MetadataParsingErrorReason::DuplicateEntry(id, yaml_type), + } + } + + pub(super) fn unexpected_type( + marker: Marker, + yaml_type: YamlObjectType, + expected_type: ExpectedType, + ) -> Self { + Self { + marker, + reason: MetadataParsingErrorReason::UnexpectedType(expected_type, yaml_type), + } + } + + pub(super) fn unexpected_value_type( + marker: Marker, + key: &'static str, + yaml_type: YamlObjectType, + expected_type: ExpectedType, + ) -> Self { + Self { + marker, + reason: MetadataParsingErrorReason::UnexpectedValueType(key, expected_type, yaml_type), + } + } +} + +impl std::fmt::Display for ParseMetadataError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "encountered a YAML parsing error at line {} column {}: {}", + self.marker.line(), + self.marker.col(), + self.reason + ) + } +} + +impl std::error::Error for ParseMetadataError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match &self.reason { + MetadataParsingErrorReason::InvalidCondition(b) => Some(&b.1), + MetadataParsingErrorReason::InvalidRegex(b) => Some(b), + _ => None, + } + } +} + +impl From for ParseMetadataError { + fn from(value: saphyr::ScanError) -> Self { + Self { + marker: *value.marker(), + reason: MetadataParsingErrorReason::Other(Box::new(value)), + } + } +} + +#[derive(Debug)] +pub(super) enum MetadataParsingErrorReason { + InvalidCondition(Box<(String, loot_condition_interpreter::Error)>), + MissingKey(&'static str, YamlObjectType), + InvalidRegex(Box), + InvalidMultilingualMessageContents, + UnexpectedType(ExpectedType, YamlObjectType), + UnexpectedValueType(&'static str, ExpectedType, YamlObjectType), + MissingPlaceholder(String, usize), + NonU32Number(i64), + DuplicateEntry(String, YamlObjectType), + Other(Box), +} + +impl std::fmt::Display for MetadataParsingErrorReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidCondition(b) => { + write!(f, "the condition string \"{}\" is invalid", b.0) + } + Self::MissingKey(key, yaml_object_type) => write!( + f, + "\"{}\" key in \"{}\" map is missing", + key, yaml_object_type + ), + Self::InvalidRegex(_) => { + write!(f, "invalid regex in \"name\" key") + } + Self::InvalidMultilingualMessageContents => MultilingualMessageContentsError.fmt(f), + Self::UnexpectedType(expected_type, yaml_object_type) => write!( + f, + "\"{}\" object must be {}", + yaml_object_type, expected_type + ), + Self::UnexpectedValueType(key, expected_type, yaml_object_type) => write!( + f, + "\"{}\" key in \"{}\" map must be {}", + key, yaml_object_type, expected_type + ), + Self::MissingPlaceholder(sub, placeholder_index) => write!( + f, + "failed to substitute \"{}\" into message, no placeholder {{{}}} was found", + sub, placeholder_index + ), + Self::NonU32Number(i) => { + write!(f, "{} is not valid as a 32-bit unsigned integer", i) + } + Self::DuplicateEntry(id, yaml_object_type) => write!( + f, + "more than one entry exists for {} \"{}\"", + yaml_object_type, id + ), + Self::Other(m) => m.fmt(f), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub(super) enum ExpectedType { + String, + Number, + Array, + Map, + MapOrString, + ArrayOrString, +} + +impl std::fmt::Display for ExpectedType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ExpectedType::String => write!(f, "a string"), + ExpectedType::Number => write!(f, "a number"), + ExpectedType::Array => write!(f, "an array"), + ExpectedType::Map => write!(f, "a map"), + ExpectedType::MapOrString => write!(f, "a map or string"), + ExpectedType::ArrayOrString => write!(f, "an array or string"), + } + } +} + +/// Represents an error encountered while parsing and compiling a regex plugin +/// name. +#[derive(Clone, Debug)] +pub struct RegexError(Box); + +impl std::fmt::Display for RegexError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "encountered a regex error: {}", self.0) + } +} + +impl std::error::Error for RegexError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.0) + } +} + +impl From> for RegexError { + fn from(value: Box) -> Self { + Self(value) + } +} + +/// Represents an error encountered while loading metadata from a file. +#[derive(Debug)] +pub struct LoadMetadataError { + path: PathBuf, + reason: MetadataDocumentParsingError, +} + +impl LoadMetadataError { + pub(super) fn new(path: PathBuf, reason: MetadataDocumentParsingError) -> Self { + Self { + path: path.to_path_buf(), + reason, + } + } + + pub(super) fn from_io_error(path: PathBuf, error: std::io::Error) -> Self { + Self { + path: path.to_path_buf(), + reason: MetadataDocumentParsingError::IoError(error), + } + } +} + +impl std::fmt::Display for LoadMetadataError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "failed to parse the file at \"{}\"", self.path.display()) + } +} + +impl std::error::Error for LoadMetadataError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.reason) + } +} + +#[derive(Debug)] +#[non_exhaustive] +pub(super) enum MetadataDocumentParsingError { + PathNotFound, + NoDocuments, + MoreThanOneDocument(usize), + IoError(std::io::Error), + MetadataParsingError(ParseMetadataError), + YamlMergeKeyError(YamlMergeKeyError), +} + +impl std::fmt::Display for MetadataDocumentParsingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::PathNotFound => write!(f, "path not found"), + Self::NoDocuments => write!(f, "no YAML document found"), + Self::MoreThanOneDocument(n) => write!(f, "expected 1 YAML document, found {}", n), + Self::IoError(_) => write!(f, "an I/O error occurred"), + Self::MetadataParsingError(_) => write!(f, "a metadata parsing error occurred"), + Self::YamlMergeKeyError(_) => { + write!(f, "an error occurred while resolving YAML merge keys",) + } + } + } +} + +impl std::error::Error for MetadataDocumentParsingError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::PathNotFound => None, + Self::NoDocuments => None, + Self::MoreThanOneDocument(_) => None, + Self::IoError(e) => Some(e), + Self::MetadataParsingError(e) => Some(e), + Self::YamlMergeKeyError(e) => Some(e), + } + } +} + +impl From for MetadataDocumentParsingError { + fn from(value: std::io::Error) -> Self { + MetadataDocumentParsingError::IoError(value) + } +} + +impl From for MetadataDocumentParsingError { + fn from(value: ParseMetadataError) -> Self { + MetadataDocumentParsingError::MetadataParsingError(value) + } +} + +impl From for MetadataDocumentParsingError { + fn from(value: YamlMergeKeyError) -> Self { + MetadataDocumentParsingError::YamlMergeKeyError(value) + } +} + +impl From for MetadataDocumentParsingError { + fn from(value: saphyr::ScanError) -> Self { + Self::MetadataParsingError(value.into()) + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub(super) struct YamlMergeKeyError { + value: Box, +} + +impl YamlMergeKeyError { + pub(super) fn new(value: saphyr::MarkedYaml) -> Self { + YamlMergeKeyError { + value: Box::new(value), + } + } +} + +impl std::fmt::Display for YamlMergeKeyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut output = String::new(); + + let yaml = to_yaml(&self.value); + + if saphyr::YamlEmitter::new(&mut output).dump(&yaml).is_ok() { + write!( + f, + "invalid YAML merge key value at line {} column {}: {}", + self.value.span.start.line(), + self.value.span.start.col(), + output + ) + } else { + write!( + f, + "invalid YAML merge key value at line {} column {}: {:?}", + self.value.span.start.line(), + self.value.span.start.col(), + self.value + ) + } + } +} + +impl std::error::Error for YamlMergeKeyError {} + +/// Represents an error that occurred while trying to write metadata to a file. +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct WriteMetadataError { + path: PathBuf, + reason: WriteMetadataErrorReason, +} + +impl WriteMetadataError { + pub(crate) fn new(path: PathBuf, reason: WriteMetadataErrorReason) -> Self { + Self { path, reason } + } +} + +impl std::fmt::Display for WriteMetadataError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.reason { + WriteMetadataErrorReason::ParentDirectoryNotFound => write!( + f, + "the parent directory of the path \"{}\" was not found", + self.path.display() + ), + WriteMetadataErrorReason::PathAlreadyExists => { + write!(f, "the path \"{}\" already exists", self.path.display()) + } + } + } +} + +impl std::error::Error for WriteMetadataError {} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub(crate) enum WriteMetadataErrorReason { + ParentDirectoryNotFound, + PathAlreadyExists, +} diff --git a/src/metadata/file.rs b/src/metadata/file.rs index b157a617..6dce45e8 100644 --- a/src/metadata/file.rs +++ b/src/metadata/file.rs @@ -1,14 +1,14 @@ -use std::str::FromStr; - -use loot_condition_interpreter::Expression; use saphyr::{MarkedYaml, YamlData}; use unicase::UniCase; -use crate::error::{GeneralError, InvalidMultilingualMessageContents, YamlParseError}; - use super::{ + error::ExpectedType, + error::{MultilingualMessageContentsError, ParseMetadataError}, message::{MessageContent, parse_message_contents_yaml, validate_message_contents}, - yaml::{YamlObjectType, as_string_node, get_required_string_value, get_string_value}, + yaml::{ + YamlObjectType, as_string_node, get_required_string_value, get_string_value, + parse_condition, + }, }; /// Represents a file in a game's Data folder, including files in @@ -52,7 +52,7 @@ impl File { pub fn with_detail( mut self, detail: Vec, - ) -> Result { + ) -> Result { validate_message_contents(&detail)?; self.detail = detail; Ok(self) @@ -108,12 +108,12 @@ impl AsRef for &Filename { impl std::fmt::Display for Filename { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) + self.0.fmt(f) } } impl TryFrom<&MarkedYaml> for File { - type Error = GeneralError; + type Error = ParseMetadataError; fn try_from(value: &MarkedYaml) -> Result { match &value.data { @@ -138,26 +138,20 @@ impl TryFrom<&MarkedYaml> for File { None => Vec::new(), }; - let condition = match get_string_value(h, "condition", YamlObjectType::File)? { - Some(n) => { - Expression::from_str(n)?; - Some(n.to_string()) - } - None => None, - }; + let condition = parse_condition(h, YamlObjectType::File)?; Ok(File { name: Filename(UniCase::new(name.to_string())), - display_name: display_name.map(|s| s.to_string()), + display_name: display_name.map(|(_, s)| s.to_string()), detail, condition, }) } - _ => Err(YamlParseError::new( + _ => Err(ParseMetadataError::unexpected_type( value.span.start, - "'file' object must be a map or string".into(), - ) - .into()), + YamlObjectType::File, + ExpectedType::MapOrString, + )), } } } diff --git a/src/metadata/group.rs b/src/metadata/group.rs index 2dd1750d..61cb9972 100644 --- a/src/metadata/group.rs +++ b/src/metadata/group.rs @@ -1,9 +1,9 @@ use saphyr::MarkedYaml; +use super::error::ParseMetadataError; use super::yaml::{ YamlObjectType, get_as_hash, get_required_string_value, get_string_value, get_strings_vec_value, }; -use crate::error::YamlParseError; /// Represents a group to which plugin metadata objects can belong. #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] @@ -70,7 +70,7 @@ impl std::default::Default for Group { } impl TryFrom<&MarkedYaml> for Group { - type Error = YamlParseError; + type Error = ParseMetadataError; fn try_from(value: &MarkedYaml) -> Result { let hash = get_as_hash(value, YamlObjectType::Group)?; @@ -84,7 +84,7 @@ impl TryFrom<&MarkedYaml> for Group { Ok(Group { name: name.to_string(), - description: description.map(|d| d.to_string()), + description: description.map(|d| d.1.to_string()), after_groups: after.iter().map(|a| a.to_string()).collect(), }) } diff --git a/src/metadata/location.rs b/src/metadata/location.rs index 99572e93..b4507a08 100644 --- a/src/metadata/location.rs +++ b/src/metadata/location.rs @@ -1,6 +1,7 @@ use saphyr::{MarkedYaml, YamlData}; -use crate::error::{GeneralError, YamlParseError}; +use super::error::ExpectedType; +use super::error::ParseMetadataError; use super::yaml::{YamlObjectType, get_required_string_value}; @@ -40,7 +41,7 @@ impl Location { } impl TryFrom<&MarkedYaml> for Location { - type Error = GeneralError; + type Error = ParseMetadataError; fn try_from(value: &MarkedYaml) -> Result { match &value.data { @@ -67,11 +68,11 @@ impl TryFrom<&MarkedYaml> for Location { name: Some(name.to_string()), }) } - _ => Err(YamlParseError::new( + _ => Err(ParseMetadataError::unexpected_type( value.span.start, - "'tag' object must be a map or string".into(), - ) - .into()), + YamlObjectType::Location, + ExpectedType::MapOrString, + )), } } } diff --git a/src/metadata/message.rs b/src/metadata/message.rs index e260eaeb..742a5a2f 100644 --- a/src/metadata/message.rs +++ b/src/metadata/message.rs @@ -1,13 +1,15 @@ -use std::str::FromStr; - -use loot_condition_interpreter::Expression; use saphyr::{MarkedYaml, YamlData}; -use super::yaml::{ - YamlObjectType, as_string_node, get_as_hash, get_required_string_value, get_string_value, - get_strings_vec_value, +use super::{ + error::{ + ExpectedType, MetadataParsingErrorReason, MultilingualMessageContentsError, + ParseMetadataError, + }, + yaml::{ + YamlObjectType, as_string_node, get_as_hash, get_required_string_value, + get_strings_vec_value, parse_condition, + }, }; -use crate::error::{GeneralError, InvalidMultilingualMessageContents, YamlParseError}; /// Codes used to indicate the type of a message. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] @@ -163,7 +165,7 @@ impl Message { pub fn multilingual( message_type: MessageType, content: Vec, - ) -> Result { + ) -> Result { validate_message_contents(&content)?; Ok(Self { @@ -198,14 +200,14 @@ impl Message { pub(crate) fn validate_message_contents( contents: &[MessageContent], -) -> Result<(), InvalidMultilingualMessageContents> { +) -> Result<(), MultilingualMessageContentsError> { if contents.len() > 1 { let english_string_exists = contents .iter() .any(|c| c.language == MessageContent::DEFAULT_LANGUAGE); if !english_string_exists { - return Err(InvalidMultilingualMessageContents {}); + return Err(MultilingualMessageContentsError {}); } } @@ -213,7 +215,7 @@ pub(crate) fn validate_message_contents( } impl TryFrom<&MarkedYaml> for MessageContent { - type Error = YamlParseError; + type Error = ParseMetadataError; fn try_from(value: &MarkedYaml) -> Result { let hash = get_as_hash(value, YamlObjectType::MessageContent)?; @@ -233,9 +235,9 @@ impl TryFrom<&MarkedYaml> for MessageContent { pub(crate) fn parse_message_contents_yaml( value: &MarkedYaml, - key: &str, + key: &'static str, parent_yaml_type: YamlObjectType, -) -> Result, GeneralError> { +) -> Result, ParseMetadataError> { let contents = match &value.data { YamlData::String(s) => { vec![MessageContent { @@ -248,24 +250,27 @@ pub(crate) fn parse_message_contents_yaml( .map(MessageContent::try_from) .collect::, _>>()?, _ => { - return Err(YamlParseError::new( + return Err(ParseMetadataError::unexpected_value_type( value.span.start, - format!( - "'{}' key in '{}' map is not a list or string", - key, parent_yaml_type - ), - ) - .into()); + key, + parent_yaml_type, + ExpectedType::ArrayOrString, + )); } }; - validate_message_contents(&contents)?; - - Ok(contents) + if validate_message_contents(&contents).is_err() { + Err(ParseMetadataError::new( + value.span.start, + MetadataParsingErrorReason::InvalidMultilingualMessageContents, + )) + } else { + Ok(contents) + } } impl TryFrom<&MarkedYaml> for Message { - type Error = GeneralError; + type Error = ParseMetadataError; fn try_from(value: &MarkedYaml) -> Result { let hash = get_as_hash(value, YamlObjectType::Message)?; @@ -281,12 +286,11 @@ impl TryFrom<&MarkedYaml> for Message { let mut content = match hash.get(&as_string_node("content")) { Some(n) => parse_message_contents_yaml(n, "content", YamlObjectType::Message)?, None => { - return Err(YamlParseError::missing_key( + return Err(ParseMetadataError::missing_key( value.span.start, "content", YamlObjectType::Message, - ) - .into()); + )); } }; @@ -297,7 +301,10 @@ impl TryFrom<&MarkedYaml> for Message { for (index, sub) in subs.iter().enumerate() { let placeholder = format!("{}", index); if !mc.text.contains(&placeholder) { - return Err(YamlParseError::new(value.span.start, format!("Failed to substitute \"{}\" into message, no placeholder \"{}\" was found", sub, placeholder)).into()); + return Err(ParseMetadataError::new( + value.span.start, + MetadataParsingErrorReason::MissingPlaceholder(sub.to_string(), index), + )); } mc.text = mc.text.replace(&placeholder, sub); @@ -305,13 +312,7 @@ impl TryFrom<&MarkedYaml> for Message { } } - let condition = match get_string_value(hash, "condition", YamlObjectType::Message)? { - Some(c) => { - Expression::from_str(c)?; - Some(c.to_string()) - } - None => None, - }; + let condition = parse_condition(hash, YamlObjectType::Message)?; Ok(Message { message_type, diff --git a/src/metadata/metadata_document.rs b/src/metadata/metadata_document.rs index e7bbc5aa..16c855a4 100644 --- a/src/metadata/metadata_document.rs +++ b/src/metadata/metadata_document.rs @@ -6,9 +6,11 @@ use std::{ use saphyr::{MarkedYaml, YamlData}; -use crate::error::{FileAccessError, GeneralError, YamlMergeKeyError, YamlParseError}; - use super::{ + error::{ + ExpectedType, LoadMetadataError, MetadataDocumentParsingError, ParseMetadataError, + RegexError, WriteMetadataError, YamlMergeKeyError, + }, file::Filename, group::Group, message::Message, @@ -28,12 +30,21 @@ pub struct MetadataDocument { } impl MetadataDocument { - pub fn load(&mut self, file_path: &Path) -> Result<(), GeneralError> { + pub fn load(&mut self, file_path: &Path) -> Result<(), LoadMetadataError> { + if !file_path.exists() { + return Err(LoadMetadataError::new( + file_path.into(), + MetadataDocumentParsingError::PathNotFound, + )); + } + log::trace!("Loading file: {:?}", file_path); - let content = std::fs::read_to_string(file_path)?; + let content = std::fs::read_to_string(file_path) + .map_err(|e| LoadMetadataError::from_io_error(file_path.into(), e))?; - self.load_from_str(&content)?; + self.load_from_str(&content) + .map_err(|e| LoadMetadataError::new(file_path.into(), e))?; log::trace!( "Successfully loaded metadata from file at \"{:?}\".", @@ -47,13 +58,31 @@ impl MetadataDocument { &mut self, masterlist_path: &Path, prelude_path: &Path, - ) -> Result<(), GeneralError> { - let masterlist = std::fs::read_to_string(masterlist_path)?; - let prelude = std::fs::read_to_string(prelude_path)?; + ) -> Result<(), LoadMetadataError> { + if !masterlist_path.exists() { + return Err(LoadMetadataError::new( + masterlist_path.into(), + MetadataDocumentParsingError::PathNotFound, + )); + } + + if !prelude_path.exists() { + return Err(LoadMetadataError::new( + prelude_path.into(), + MetadataDocumentParsingError::PathNotFound, + )); + } + + let masterlist = std::fs::read_to_string(masterlist_path) + .map_err(|e| LoadMetadataError::from_io_error(masterlist_path.into(), e))?; + + let prelude = std::fs::read_to_string(prelude_path) + .map_err(|e| LoadMetadataError::from_io_error(masterlist_path.into(), e))?; let masterlist = replace_prelude(masterlist, prelude); - self.load_from_str(&masterlist)?; + self.load_from_str(&masterlist) + .map_err(|e| LoadMetadataError::new(masterlist_path.into(), e))?; log::trace!( "Successfully loaded metadata from file at \"{:?}\".", @@ -63,27 +92,26 @@ impl MetadataDocument { Ok(()) } - fn load_from_str(&mut self, string: &str) -> Result<(), GeneralError> { + fn load_from_str(&mut self, string: &str) -> Result<(), MetadataDocumentParsingError> { let mut docs = MarkedYaml::load_from_str(string)?; let doc = docs .pop() - .ok_or_else(|| FileAccessError::new("No documents in the loaded YAML".into()))?; + .ok_or_else(|| MetadataDocumentParsingError::NoDocuments)?; if !docs.is_empty() { - return Err(FileAccessError::new(format!( - "YAML file contained more than one document, found {}", - docs.len() + 1 - )) - .into()); + return Err(MetadataDocumentParsingError::MoreThanOneDocument( + docs.len() + 1, + )); } let doc = process_merge_keys(doc)?; let doc = match doc.data { YamlData::Hash(h) => h, _ => { - return Err(YamlParseError::new( + return Err(ParseMetadataError::unexpected_type( doc.span.start, - "The root of the YAML document is not a map.".into(), + YamlObjectType::MetadataDocument, + ExpectedType::Map, ) .into()); } @@ -91,17 +119,18 @@ impl MetadataDocument { let mut plugins: HashMap = HashMap::new(); let mut regex_plugins: Vec = Vec::new(); - for plugin in get_as_slice(&doc, "plugins", YamlObjectType::MetadataDocument)? { - let plugin = PluginMetadata::try_from(plugin)?; + for plugin_yaml in get_as_slice(&doc, "plugins", YamlObjectType::MetadataDocument)? { + let plugin = PluginMetadata::try_from(plugin_yaml)?; if plugin.is_regex_plugin() { regex_plugins.push(plugin); } else { let filename = Filename::new(plugin.name().to_string()); if plugins.contains_key(&filename) { - return Err(FileAccessError::new(format!( - "More than one entry exists for plugin \"{}\"", - plugin.name() - )) + return Err(ParseMetadataError::duplicate_entry( + plugin_yaml.span.start, + plugin.name().to_string(), + YamlObjectType::PluginMetadata, + ) .into()); } plugins.insert(filename, plugin); @@ -116,21 +145,23 @@ impl MetadataDocument { let mut bash_tags = Vec::new(); let mut str_set = HashSet::new(); for bash_tag_yaml in get_as_slice(&doc, "bash_tags", YamlObjectType::MetadataDocument)? { - let bash_tag = match bash_tag_yaml.data.as_str() { + let bash_tag: &str = match bash_tag_yaml.data.as_str() { Some(b) => b, None => { - return Err(YamlParseError::new( + return Err(ParseMetadataError::unexpected_type( bash_tag_yaml.span.start, - "Found a non-string Bash Tag.".into(), + YamlObjectType::BashTagsElement, + ExpectedType::String, ) .into()); } }; if str_set.contains(bash_tag) { - return Err(YamlParseError::new( + return Err(ParseMetadataError::duplicate_entry( bash_tag_yaml.span.start, - format!("More than one entry exists for Bash Tag \"{}\"", bash_tag), + bash_tag.to_string(), + YamlObjectType::BashTagsElement, ) .into()); } @@ -146,9 +177,10 @@ impl MetadataDocument { let name = group.name().to_string(); if group_names.contains(&name) { - return Err(YamlParseError::new( + return Err(ParseMetadataError::duplicate_entry( group_yaml.span.start, - format!("More than one entry exists for group \"{}\"", group.name()), + group.name().to_string(), + YamlObjectType::Group, ) .into()); } @@ -170,7 +202,7 @@ impl MetadataDocument { Ok(()) } - pub fn save(&self, file_path: &Path) -> Result<(), GeneralError> { + pub fn save(&self, file_path: &Path) -> Result<(), WriteMetadataError> { // let mut hash = saphyr::Hash::new(); // hash.insert( @@ -208,7 +240,7 @@ impl MetadataDocument { self.plugins.values().chain(self.regex_plugins.iter()) } - pub fn find_plugin(&self, plugin_name: &str) -> Result, GeneralError> { + pub fn find_plugin(&self, plugin_name: &str) -> Result, RegexError> { let mut metadata = match self.plugins.get(&Filename::new(plugin_name.to_string())) { Some(m) => m.clone(), None => PluginMetadata::new(plugin_name)?, @@ -254,7 +286,6 @@ impl MetadataDocument { fn process_merge_keys(mut yaml: MarkedYaml) -> Result { match yaml.data { - YamlData::Alias(_) => panic!("Alias encountered!"), YamlData::Array(a) => { yaml.data = merge_array_elements(a).map(YamlData::Array)?; Ok(yaml) diff --git a/src/metadata/mod.rs b/src/metadata/mod.rs index 3143ecd5..98a2e02e 100644 --- a/src/metadata/mod.rs +++ b/src/metadata/mod.rs @@ -1,3 +1,5 @@ +//! Holds all types related to LOOT metadata. +pub mod error; mod file; mod group; mod location; diff --git a/src/metadata/plugin_cleaning_data.rs b/src/metadata/plugin_cleaning_data.rs index 5443a23a..ac96e13e 100644 --- a/src/metadata/plugin_cleaning_data.rs +++ b/src/metadata/plugin_cleaning_data.rs @@ -1,8 +1,7 @@ use saphyr::MarkedYaml; -use crate::error::{GeneralError, InvalidMultilingualMessageContents, YamlParseError}; - use super::{ + error::{MultilingualMessageContentsError, ParseMetadataError}, message::{MessageContent, parse_message_contents_yaml, validate_message_contents}, yaml::{YamlObjectType, as_string_node, get_as_hash, get_required_string_value, get_u32_value}, }; @@ -59,7 +58,7 @@ impl PluginCleaningData { pub fn with_detail( mut self, detail: Vec, - ) -> Result { + ) -> Result { validate_message_contents(&detail)?; self.detail = detail; Ok(self) @@ -103,7 +102,7 @@ impl PluginCleaningData { } impl TryFrom<&MarkedYaml> for PluginCleaningData { - type Error = GeneralError; + type Error = ParseMetadataError; fn try_from(value: &MarkedYaml) -> Result { let hash = get_as_hash(value, YamlObjectType::PluginCleaningData)?; @@ -111,12 +110,11 @@ impl TryFrom<&MarkedYaml> for PluginCleaningData { let crc = match get_u32_value(hash, "crc", YamlObjectType::PluginCleaningData)? { Some(n) => n, None => { - return Err(YamlParseError::missing_key( + return Err(ParseMetadataError::missing_key( value.span.start, "crc", YamlObjectType::PluginCleaningData, - ) - .into()); + )); } }; diff --git a/src/metadata/plugin_metadata.rs b/src/metadata/plugin_metadata.rs index f53a445e..bc7eb83b 100644 --- a/src/metadata/plugin_metadata.rs +++ b/src/metadata/plugin_metadata.rs @@ -1,12 +1,10 @@ use fancy_regex::Regex; use saphyr::MarkedYaml; -use crate::{ - error::{GeneralError, YamlParseError}, - regex, -}; +use crate::regex; use super::{ + error::{MetadataParsingErrorReason, ParseMetadataError, RegexError}, file::File, location::Location, message::Message, @@ -37,7 +35,7 @@ pub struct PluginMetadata { impl PluginMetadata { /// Construct a [PluginMetadata] object with no metadata for a plugin with /// the given filename. - pub fn new(name: &str) -> Result> { + pub fn new(name: &str) -> Result { Ok(Self { name: PluginName::new(name)?, ..Default::default() @@ -299,7 +297,7 @@ fn merge_vecs(target: &mut Vec, source: &[T]) { } impl TryFrom<&MarkedYaml> for PluginMetadata { - type Error = GeneralError; + type Error = ParseMetadataError; fn try_from(value: &MarkedYaml) -> Result { let hash = get_as_hash(value, YamlObjectType::PluginMetadata)?; @@ -313,11 +311,10 @@ impl TryFrom<&MarkedYaml> for PluginMetadata { let name = match PluginName::new(name) { Ok(n) => n, Err(e) => { - return Err(YamlParseError::new( + return Err(ParseMetadataError::new( value.span.start, - format!("Invalid regex in \"name\" key: {}", e), - ) - .into()); + MetadataParsingErrorReason::InvalidRegex(e), + )); } }; @@ -334,7 +331,7 @@ impl TryFrom<&MarkedYaml> for PluginMetadata { Ok(PluginMetadata { name, - group: group.map(|g| g.to_string()), + group: group.map(|g| g.1.to_string()), load_after, requirements, incompatibilities, @@ -347,15 +344,12 @@ impl TryFrom<&MarkedYaml> for PluginMetadata { } } -fn get_vec<'a, T: TryFrom<&'a MarkedYaml, Error = GeneralError>>( +fn get_vec<'a, T: TryFrom<&'a MarkedYaml, Error = impl Into>>( hash: &'a saphyr::AnnotatedHash, - key: &str, -) -> Result, GeneralError> -where - GeneralError: From<>::Error>, -{ + key: &'static str, +) -> Result, ParseMetadataError> { get_as_slice(hash, key, YamlObjectType::PluginMetadata)? .iter() - .map(|e| T::try_from(e)) + .map(|e| T::try_from(e).map_err(Into::into)) .collect::, _>>() } diff --git a/src/metadata/tag.rs b/src/metadata/tag.rs index 5bde1e38..99d21d70 100644 --- a/src/metadata/tag.rs +++ b/src/metadata/tag.rs @@ -1,10 +1,8 @@ -use std::str::FromStr; - -use loot_condition_interpreter::Expression; use saphyr::YamlData; -use super::yaml::{YamlObjectType, get_required_string_value, get_string_value}; -use crate::error::{GeneralError, YamlParseError}; +use super::error::ExpectedType; +use super::error::ParseMetadataError; +use super::yaml::{YamlObjectType, get_required_string_value, parse_condition}; /// Represents whether a Bash Tag suggestion is for addition or removal. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] @@ -57,7 +55,7 @@ impl Tag { } impl TryFrom<&saphyr::MarkedYaml> for Tag { - type Error = GeneralError; + type Error = ParseMetadataError; fn try_from(value: &saphyr::MarkedYaml) -> Result { match &value.data { @@ -73,13 +71,7 @@ impl TryFrom<&saphyr::MarkedYaml> for Tag { let name = get_required_string_value(value.span.start, h, "name", YamlObjectType::Tag)?; - let condition = match get_string_value(h, "condition", YamlObjectType::Tag)? { - Some(n) => { - Expression::from_str(n)?; - Some(n.to_string()) - } - None => None, - }; + let condition = parse_condition(h, YamlObjectType::Tag)?; let (name, suggestion) = name_and_suggestion(name); Ok(Tag { @@ -88,11 +80,11 @@ impl TryFrom<&saphyr::MarkedYaml> for Tag { condition, }) } - _ => Err(YamlParseError::new( + _ => Err(ParseMetadataError::unexpected_type( value.span.start, - "'tag' object must be a map or string".into(), - ) - .into()), + YamlObjectType::Tag, + ExpectedType::MapOrString, + )), } } } diff --git a/src/metadata/yaml.rs b/src/metadata/yaml.rs index e7feca4f..7f3ea765 100644 --- a/src/metadata/yaml.rs +++ b/src/metadata/yaml.rs @@ -1,6 +1,9 @@ +use std::str::FromStr; + +use loot_condition_interpreter::Expression; use saphyr::{AnnotatedArray, AnnotatedHash, MarkedYaml, Marker, Yaml, YamlData}; -use crate::error::{GeneralError, YamlParseError}; +use super::error::{ExpectedType, MetadataParsingErrorReason, ParseMetadataError}; #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum YamlObjectType { @@ -13,6 +16,7 @@ pub enum YamlObjectType { PluginMetadata, Tag, MetadataDocument, + BashTagsElement, } impl std::fmt::Display for YamlObjectType { @@ -27,6 +31,7 @@ impl std::fmt::Display for YamlObjectType { YamlObjectType::PluginMetadata => write!(f, "plugin metadata"), YamlObjectType::Tag => write!(f, "tag"), YamlObjectType::MetadataDocument => write!(f, "metadata document"), + YamlObjectType::BashTagsElement => write!(f, "bash tags"), } } } @@ -64,15 +69,17 @@ pub fn as_string_node(value: &str) -> MarkedYaml { pub fn get_string_value<'a>( hash: &'a AnnotatedHash, - key: &str, + key: &'static str, yaml_type: YamlObjectType, -) -> Result, YamlParseError> { +) -> Result, ParseMetadataError> { match hash.get(&as_string_node(key)) { Some(n) => match n.data.as_str() { - Some(n) => Ok(Some(n)), - None => Err(YamlParseError::new( + Some(s) => Ok(Some((n.span.start, s))), + None => Err(ParseMetadataError::unexpected_value_type( n.span.start, - format!("'{}' key in '{}' map is not a string", key, yaml_type), + key, + yaml_type, + ExpectedType::String, )), }, None => Ok(None), @@ -82,35 +89,39 @@ pub fn get_string_value<'a>( pub fn get_required_string_value<'a>( marker: Marker, hash: &'a AnnotatedHash, - key: &str, + key: &'static str, yaml_type: YamlObjectType, -) -> Result<&'a str, YamlParseError> { +) -> Result<&'a str, ParseMetadataError> { match get_string_value(hash, key, yaml_type)? { - Some(n) => Ok(n), - None => Err(YamlParseError::missing_key(marker, key, yaml_type)), + Some(n) => Ok(n.1), + None => Err(ParseMetadataError::missing_key(marker, key, yaml_type)), } } pub fn get_strings_vec_value<'a>( hash: &'a AnnotatedHash, - key: &str, + key: &'static str, yaml_type: YamlObjectType, -) -> Result, YamlParseError> { +) -> Result, ParseMetadataError> { match hash.get(&as_string_node(key)) { Some(n) => match n.data.as_vec() { Some(n) => n .iter() .map(|e| match e.data.as_str() { Some(s) => Ok(s), - None => Err(YamlParseError::new( + None => Err(ParseMetadataError::unexpected_value_type( e.span.start, - "Element in list is not a string".into(), + key, + yaml_type, + ExpectedType::String, )), }) .collect::, _>>(), - None => Err(YamlParseError::new( + None => Err(ParseMetadataError::unexpected_value_type( n.span.start, - format!("'{}' key in '{}' map is not a list", key, yaml_type), + key, + yaml_type, + ExpectedType::Array, )), }, None => Ok(Vec::new()), @@ -120,29 +131,33 @@ pub fn get_strings_vec_value<'a>( pub fn get_as_hash( value: &MarkedYaml, yaml_type: YamlObjectType, -) -> Result<&AnnotatedHash, YamlParseError> { +) -> Result<&AnnotatedHash, ParseMetadataError> { match value.data.as_hash() { Some(h) => Ok(h), - None => Err(YamlParseError::new( + None => Err(ParseMetadataError::unexpected_type( value.span.start, - format!("'{}' object must be a map", yaml_type), + yaml_type, + ExpectedType::Map, )), } } pub fn get_u32_value( hash: &AnnotatedHash, - key: &str, + key: &'static str, yaml_type: YamlObjectType, -) -> Result, GeneralError> { +) -> Result, ParseMetadataError> { match hash.get(&as_string_node(key)) { Some(n) => match n.data.as_i64() { - Some(n) => Ok(Some(n.try_into()?)), - None => Err(YamlParseError::new( + Some(i) => i.try_into().map(Some).map_err(|_| { + ParseMetadataError::new(n.span.start, MetadataParsingErrorReason::NonU32Number(i)) + }), + None => Err(ParseMetadataError::unexpected_value_type( n.span.start, - format!("'{}' key in '{}' map is not a string", key, yaml_type), - ) - .into()), + key, + yaml_type, + ExpectedType::Number, + )), }, None => Ok(None), } @@ -150,18 +165,36 @@ pub fn get_u32_value( pub fn get_as_slice<'a>( hash: &'a saphyr::AnnotatedHash, - key: &str, + key: &'static str, yaml_type: YamlObjectType, -) -> Result<&'a [MarkedYaml], YamlParseError> { +) -> Result<&'a [MarkedYaml], ParseMetadataError> { if let Some(value) = hash.get(&as_string_node(key)) { match value.data.as_vec() { Some(n) => Ok(n.as_slice()), - None => Err(YamlParseError::new( + None => Err(ParseMetadataError::unexpected_value_type( value.span.start, - format!("'{}' key in '{}' map is not an array", key, yaml_type), + key, + yaml_type, + ExpectedType::Array, )), } } else { Ok(&[]) } } + +pub fn parse_condition( + hash: &saphyr::AnnotatedHash, + yaml_type: YamlObjectType, +) -> Result, ParseMetadataError> { + match get_string_value(hash, "condition", yaml_type)? { + Some((marker, s)) => { + let s = s.to_string(); + if let Err(e) = Expression::from_str(&s) { + return Err(ParseMetadataError::invalid_condition(marker, s, e)); + } + Ok(Some(s)) + } + None => Ok(None), + } +} diff --git a/src/plugin/error.rs b/src/plugin/error.rs new file mode 100644 index 00000000..78cee23c --- /dev/null +++ b/src/plugin/error.rs @@ -0,0 +1,140 @@ +use std::path::PathBuf; + +/// Represents an error that occurred while reading a parsed plugin's data. +#[derive(Debug)] +pub struct PluginDataError(esplugin::Error); + +impl std::fmt::Display for PluginDataError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "failed to read plugin data") + } +} + +impl std::error::Error for PluginDataError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.0) + } +} + +impl From for PluginDataError { + fn from(value: esplugin::Error) -> Self { + PluginDataError(value) + } +} + +#[derive(Debug)] +#[non_exhaustive] +pub(crate) enum LoadPluginError { + InvalidFilename(InvalidFilenameReason), + IoError(std::io::Error), + ParsingError(esplugin::Error), + RegexError(Box), +} + +impl std::fmt::Display for LoadPluginError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidFilename(i) => i.fmt(f), + Self::IoError(_) => write!(f, "an I/O error occurred"), + Self::ParsingError(_) => write!(f, "failed to parse plugin data"), + Self::RegexError(_) => write!(f, "failed while using a regex"), + } + } +} + +impl std::error::Error for LoadPluginError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::InvalidFilename(_) => None, + Self::IoError(e) => Some(e), + Self::ParsingError(e) => Some(e), + Self::RegexError(e) => Some(e), + } + } +} + +impl From for LoadPluginError { + fn from(value: std::io::Error) -> Self { + LoadPluginError::IoError(value) + } +} + +impl From for LoadPluginError { + fn from(value: esplugin::Error) -> Self { + LoadPluginError::ParsingError(value) + } +} + +impl From> for LoadPluginError { + fn from(value: Box) -> Self { + LoadPluginError::RegexError(value) + } +} + +#[derive(Debug)] +#[non_exhaustive] +pub(crate) enum InvalidFilenameReason { + Empty, + NonUnicode, + NonUnique, + UnsupportedFileExtension, +} + +impl std::fmt::Display for InvalidFilenameReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Empty => write!(f, "is empty"), + Self::NonUnicode => write!(f, "cannot be represented in UTF-8"), + Self::NonUnique => write!(f, "is not unique"), + Self::UnsupportedFileExtension => { + write!(f, "does not have a supported plugin file extension") + } + } + } +} + +/// Represents an error that occurred when validating plugins before loading them. +#[derive(Debug)] +pub(crate) struct PluginValidationError { + path: PathBuf, + reason: PluginValidationErrorReason, +} + +impl PluginValidationError { + pub(crate) fn new(path: PathBuf, reason: PluginValidationErrorReason) -> Self { + Self { path, reason } + } + + pub(crate) fn invalid(path: PathBuf, reason: InvalidFilenameReason) -> Self { + Self { + path, + reason: PluginValidationErrorReason::InvalidFilename(reason), + } + } +} + +impl std::fmt::Display for PluginValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.reason { + PluginValidationErrorReason::InvalidFilename(i) => write!( + f, + "the path \"{}\" has a filename that {}", + self.path.display(), + i + ), + PluginValidationErrorReason::InvalidPluginHeader => write!( + f, + "the file at \"{}\" does not have a valid plugin header", + self.path.display() + ), + } + } +} + +impl std::error::Error for PluginValidationError {} + +#[derive(Debug)] +pub(crate) enum PluginValidationErrorReason { + InvalidFilename(InvalidFilenameReason), + InvalidPluginHeader, +} diff --git a/src/plugin.rs b/src/plugin/mod.rs similarity index 88% rename from src/plugin.rs rename to src/plugin/mod.rs index 9a8be83c..54e8611c 100644 --- a/src/plugin.rs +++ b/src/plugin/mod.rs @@ -1,3 +1,5 @@ +pub mod error; + use std::{ collections::{BTreeMap, BTreeSet}, fs::File, @@ -13,10 +15,13 @@ use fancy_regex::Regex; use crate::{ GameType, archive::{assets_in_archives, find_associated_archives}, - error::{GeneralError, InvalidArgumentError}, game::GameCache, regex, }; +use error::{ + InvalidFilenameReason, LoadPluginError, PluginDataError, PluginValidationError, + PluginValidationErrorReason, +}; static VERSION_REGEXES: LazyLock> = LazyLock::new(|| { /* The string below matches the range of version strings supported by @@ -77,7 +82,7 @@ impl Plugin { game_cache: &GameCache, plugin_path: &Path, load_scope: LoadScope, - ) -> Result { + ) -> Result { let name = name_string(plugin_path)?; let (parse_options, crc) = if load_scope == LoadScope::HeaderOnly { @@ -152,7 +157,7 @@ impl Plugin { } /// Get the plugin's masters. - pub fn masters(&self) -> Result, GeneralError> { + pub fn masters(&self) -> Result, PluginDataError> { self.plugin .as_ref() .map(|p| p.masters().map_err(Into::into)) @@ -227,7 +232,7 @@ impl Plugin { } /// Check if the plugin is or would be valid as a light plugin. - pub fn is_valid_as_light_plugin(&self) -> Result { + pub fn is_valid_as_light_plugin(&self) -> Result { self.plugin .as_ref() .map(|p| p.is_valid_as_light_plugin().map_err(Into::into)) @@ -235,7 +240,7 @@ impl Plugin { } /// Check if the plugin is or would be valid as a medium plugin. - pub fn is_valid_as_medium_plugin(&self) -> Result { + pub fn is_valid_as_medium_plugin(&self) -> Result { self.plugin .as_ref() .map(|p| p.is_valid_as_medium_plugin().map_err(Into::into)) @@ -243,7 +248,7 @@ impl Plugin { } /// Check if the plugin is or would be valid as an update plugin. - pub fn is_valid_as_update_plugin(&self) -> Result { + pub fn is_valid_as_update_plugin(&self) -> Result { self.plugin .as_ref() .map(|p| p.is_valid_as_update_plugin().map_err(Into::into)) @@ -269,7 +274,7 @@ impl Plugin { /// /// FormIDs are compared for all games apart from Morrowind, which doesn't /// have FormIDs and so has other identifying data compared. - pub fn do_records_overlap(&self, plugin: &Plugin) -> Result { + pub fn do_records_overlap(&self, plugin: &Plugin) -> Result { if let (Some(plugin), Some(other_plugin)) = (&self.plugin, &plugin.plugin) { plugin.overlaps_with(other_plugin).map_err(Into::into) } else { @@ -277,7 +282,7 @@ impl Plugin { } } - pub(crate) fn override_record_count(&self) -> Result { + pub(crate) fn override_record_count(&self) -> Result { self.plugin .as_ref() .map(|p| p.count_override_records().map_err(Into::into)) @@ -317,26 +322,41 @@ impl Plugin { pub(crate) fn resolve_record_ids( &mut self, plugins_metadata: &[esplugin::PluginMetadata], - ) -> Result<(), esplugin::Error> { + ) -> Result<(), PluginDataError> { if let Some(plugin) = &mut self.plugin { - plugin.resolve_record_ids(plugins_metadata) - } else { - Ok(()) + plugin.resolve_record_ids(plugins_metadata)?; } + Ok(()) } } -pub(crate) fn is_valid_plugin(game_type: GameType, plugin_path: &Path) -> bool { +pub(crate) fn validate_plugin_path_and_header( + game_type: GameType, + plugin_path: &Path, +) -> Result<(), PluginValidationError> { if game_type == GameType::OpenMW && has_ascii_extension(plugin_path, "omwscripts") { - true - } else if has_plugin_file_extension(game_type, plugin_path) { - esplugin::Plugin::is_valid(game_type.into(), plugin_path, ParseOptions::header_only()) + Ok(()) + } else if !has_plugin_file_extension(game_type, plugin_path) { + log::debug!( + "The file \"{}\" is not a valid plugin", + plugin_path.display() + ); + Err(PluginValidationError::invalid( + plugin_path.into(), + InvalidFilenameReason::UnsupportedFileExtension, + )) + } else if esplugin::Plugin::is_valid(game_type.into(), plugin_path, ParseOptions::header_only()) + { + Ok(()) } else { log::debug!( "The file \"{}\" is not a valid plugin", plugin_path.display() ); - false + Err(PluginValidationError::new( + plugin_path.into(), + PluginValidationErrorReason::InvalidPluginHeader, + )) } } @@ -381,22 +401,22 @@ pub(crate) fn has_ascii_extension(path: &Path, extension: &str) -> bool { pub(crate) fn plugins_metadata( plugins: &[Plugin], -) -> Result, esplugin::Error> { +) -> Result, PluginDataError> { let esplugins: Vec<_> = plugins.iter().filter_map(|p| p.plugin.as_ref()).collect(); - esplugin::plugins_metadata(&esplugins) + Ok(esplugin::plugins_metadata(&esplugins)?) } -fn name_string(path: &Path) -> Result { +fn name_string(path: &Path) -> Result { match path.file_name() { Some(f) => match f.to_str() { Some(f) => Ok(f.to_string()), - None => Err(InvalidArgumentError { - message: format!("The path \"{:?}\" has a non-Unicode filename", path), - }), + None => Err(LoadPluginError::InvalidFilename( + InvalidFilenameReason::NonUnicode, + )), }, - None => Err(InvalidArgumentError { - message: format!("The path \"{}\" has no filename", path.display()), - }), + None => Err(LoadPluginError::InvalidFilename( + InvalidFilenameReason::Empty, + )), } } diff --git a/src/sorting/error.rs b/src/sorting/error.rs new file mode 100644 index 00000000..2115c840 --- /dev/null +++ b/src/sorting/error.rs @@ -0,0 +1,293 @@ +use std::fmt::Display; + +use crate::{Vertex, plugin::error::PluginDataError}; + +#[derive(Clone, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct UndefinedGroupError { + group_name: String, +} + +impl UndefinedGroupError { + pub(crate) fn new(group_name: String) -> Self { + Self { group_name } + } + + pub(crate) fn into_group_name(self) -> String { + self.group_name + } +} + +impl Display for UndefinedGroupError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "the group \"{}\" does not exist", self.group_name) + } +} + +impl std::error::Error for UndefinedGroupError {} + +#[derive(Clone, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct CyclicInteractionError { + cycle: Vec, +} + +impl CyclicInteractionError { + pub(crate) fn new(cycle: Vec) -> Self { + Self { cycle } + } + + pub(crate) fn into_cycle(self) -> Vec { + self.cycle + } +} + +impl Display for CyclicInteractionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let cycle = display_cycle(&self.cycle); + write!(f, "cyclic interaction detected: {}", cycle) + } +} + +impl std::error::Error for CyclicInteractionError {} + +pub(crate) fn display_cycle(cycle: &[Vertex]) -> String { + cycle + .iter() + .map(|v| { + if let Some(edge_type) = v.out_edge_type() { + format!("{} --[{}]-> ", v.name(), edge_type) + } else { + v.name().to_string() + } + }) + .chain(cycle.first().iter().map(|v| v.name().to_string())) + .collect() +} + +/// Represents an error that occurred while trying to get the path between two +/// groups across the graph formed from group metadata. +#[derive(Debug)] +pub enum GroupsPathError { + UndefinedGroup(String), + CycleFound(Vec), + PathfindingError(Box), +} + +impl Display for GroupsPathError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::UndefinedGroup(g) => write!(f, "the group \"{}\" does not exist", g), + Self::CycleFound(c) => write!(f, "found a cycle: {}", display_cycle(c)), + Self::PathfindingError(_) => write!(f, "failed to find a path in the groups graph"), + } + } +} + +impl std::error::Error for GroupsPathError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::PathfindingError(e) => Some(e.as_ref()), + _ => None, + } + } +} + +impl From for GroupsPathError { + fn from(value: BuildGroupsGraphError) -> Self { + match value { + BuildGroupsGraphError::CycleFound(e) => Self::CycleFound(e.into_cycle()), + BuildGroupsGraphError::UndefinedGroup(e) => Self::UndefinedGroup(e.into_group_name()), + } + } +} + +impl From for GroupsPathError { + fn from(value: UndefinedGroupError) -> Self { + Self::UndefinedGroup(value.into_group_name()) + } +} + +impl From for GroupsPathError { + fn from(value: PathfindingError) -> Self { + Self::PathfindingError(Box::new(value)) + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub(crate) enum BuildGroupsGraphError { + UndefinedGroup(UndefinedGroupError), + CycleFound(CyclicInteractionError), +} + +impl Display for BuildGroupsGraphError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::UndefinedGroup(_) => write!(f, "encountered an undefined group"), + Self::CycleFound(_) => write!(f, "the groups graph is cyclic"), + } + } +} + +impl std::error::Error for BuildGroupsGraphError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::UndefinedGroup(e) => Some(e), + Self::CycleFound(e) => Some(e), + } + } +} + +impl From for BuildGroupsGraphError { + fn from(value: UndefinedGroupError) -> Self { + BuildGroupsGraphError::UndefinedGroup(value) + } +} + +impl From for BuildGroupsGraphError { + fn from(value: CyclicInteractionError) -> Self { + BuildGroupsGraphError::CycleFound(value) + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub(crate) enum PathfindingError { + NegativeCycle, + PrecedingNodeNotFound(String), + FollowingNodeNotFound(String), + EdgeNotFound { + from_group: String, + to_group: String, + }, +} + +impl Display for PathfindingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NegativeCycle => write!( + f, + "encountered a cycle of user-defined \"load after\" relationships" + ), + Self::PrecedingNodeNotFound(n) => write!( + f, + "unexpectedly could not find the node before \"{}\" in the path that was found", + n + ), + Self::FollowingNodeNotFound(n) => write!( + f, + "unexpectedly could not find the node after \"{}\" in the path that was found", + n + ), + Self::EdgeNotFound { + from_group, + to_group, + } => write!( + f, + "unexpectedly could not find the edge going from \"{}\" to \"{}\"", + from_group, to_group + ), + } + } +} + +impl std::error::Error for PathfindingError {} + +#[derive(Debug)] +pub(crate) enum PluginGraphValidationError { + CycleFound(CyclicInteractionError), + PluginDataError(PluginDataError), +} + +impl Display for PluginGraphValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::CycleFound(_) => write!(f, "found a cycle in the plugin graph"), + Self::PluginDataError(_) => write!(f, "failed to read plugin data"), + } + } +} + +impl std::error::Error for PluginGraphValidationError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::CycleFound(e) => Some(e), + Self::PluginDataError(e) => Some(e), + } + } +} + +impl From for PluginGraphValidationError { + fn from(value: CyclicInteractionError) -> Self { + PluginGraphValidationError::CycleFound(value) + } +} + +impl From for PluginGraphValidationError { + fn from(value: PluginDataError) -> Self { + PluginGraphValidationError::PluginDataError(value) + } +} + +#[derive(Debug)] +pub(crate) enum SortingError { + ValidationError(PluginGraphValidationError), + UndefinedGroup(UndefinedGroupError), + CycleFound(CyclicInteractionError), + CycleInvolving(String), + PluginDataError(PluginDataError), + PathfindingError(PathfindingError), +} + +impl Display for SortingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::ValidationError(_) => write!(f, "plugin graph validation failed"), + Self::UndefinedGroup(_) => write!(f, "found an undefined group"), + Self::CycleFound(_) => write!(f, "found a cycle"), + Self::CycleInvolving(n) => write!(f, "found a cycle involving \"{}\"", n), + Self::PluginDataError(_) => write!(f, "failed to read plugin data"), + Self::PathfindingError(_) => write!(f, "failed to find a path in the plugins graph"), + } + } +} + +impl std::error::Error for SortingError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::ValidationError(e) => Some(e), + Self::UndefinedGroup(e) => Some(e), + Self::CycleFound(e) => Some(e), + Self::CycleInvolving(_) => None, + Self::PluginDataError(e) => Some(e), + Self::PathfindingError(e) => Some(e), + } + } +} + +impl From for SortingError { + fn from(value: PluginGraphValidationError) -> Self { + SortingError::ValidationError(value) + } +} + +impl From for SortingError { + fn from(value: UndefinedGroupError) -> Self { + SortingError::UndefinedGroup(value) + } +} + +impl From for SortingError { + fn from(value: CyclicInteractionError) -> Self { + SortingError::CycleFound(value) + } +} + +impl From for SortingError { + fn from(value: PluginDataError) -> Self { + SortingError::PluginDataError(value) + } +} + +impl From for SortingError { + fn from(value: PathfindingError) -> Self { + SortingError::PathfindingError(value) + } +} diff --git a/src/sorting/groups.rs b/src/sorting/groups.rs index 9c7e0a66..8d2bf053 100644 --- a/src/sorting/groups.rs +++ b/src/sorting/groups.rs @@ -4,22 +4,24 @@ use petgraph::{Graph, algo::bellman_ford, graph::NodeIndex}; use crate::{ EdgeType, Vertex, - error::{ - CyclicInteractionError, GeneralError, InvalidArgumentError, PathfindingError, - UndefinedGroupError, - }, metadata::Group, sorting::dfs::find_cycle, + sorting::error::{ + BuildGroupsGraphError, CyclicInteractionError, PathfindingError, UndefinedGroupError, + }, }; -use super::dfs::{DfsVisitor, depth_first_search}; +use super::{ + dfs::{DfsVisitor, depth_first_search}, + error::GroupsPathError, +}; pub type GroupsGraph = Graph; pub fn build_groups_graph( masterlist_groups: &[Group], userlist_groups: &[Group], -) -> Result { +) -> Result { let masterlist_groups = sorted_by_name(masterlist_groups); let userlist_groups = sorted_by_name(userlist_groups); @@ -43,7 +45,7 @@ pub fn build_groups_graph( )?; if let Some(cycle) = find_cycle(&graph, |node| node.clone()) { - Err(CyclicInteractionError { cycle }.into()) + Err(CyclicInteractionError::new(cycle).into()) } else { Ok(graph) } @@ -106,7 +108,7 @@ pub fn find_path( graph: &GroupsGraph, from_group_name: &str, to_group_name: &str, -) -> Result, GeneralError> { +) -> Result, GroupsPathError> { let float_graph: Graph<&String, f32> = graph.map( |_, n| n, |_, e| { @@ -122,9 +124,8 @@ pub fn find_path( let from_vertex = find_node_by_weight(graph, from_group_name)?; let to_vertex = find_node_by_weight(graph, to_group_name)?; - let paths = bellman_ford(&float_graph, from_vertex).map_err(|_| InvalidArgumentError { - message: "Groups graph contains a negative cycle".into(), - })?; + let paths = + bellman_ford(&float_graph, from_vertex).map_err(|_| PathfindingError::NegativeCycle)?; let mut path = vec![Vertex::new(graph[to_vertex].clone())]; let mut current = to_vertex; @@ -141,10 +142,7 @@ pub fn find_path( return Ok(Vec::new()); } _ => { - return Err(PathfindingError::new( - "Could not find a node in the graph while pathfinding".into(), - ) - .into()); + return Err(PathfindingError::PrecedingNodeNotFound(graph[current].clone()).into()); } }; @@ -160,10 +158,10 @@ pub fn find_path( let edge = match graph.find_edge(*preceding_vertex, current) { Some(e) => e, None => { - return Err(PathfindingError::new(format!( - "Could not find edge from \"{}\" to \"{}\"", - graph[*preceding_vertex], graph[current] - )) + return Err(PathfindingError::EdgeNotFound { + from_group: graph[*preceding_vertex].clone(), + to_group: graph[current].clone(), + } .into()); } }; @@ -182,7 +180,7 @@ pub fn find_path( fn find_node_by_weight( graph: &Graph, weight: &str, -) -> Result { +) -> Result { match graph .node_indices() .find(|i| graph.node_weight(*i).map(|w| *w == weight).unwrap_or(false)) @@ -190,9 +188,7 @@ fn find_node_by_weight( Some(n) => Ok(n), None => { log::error!("Can't find group with name {}", weight); - Err(InvalidArgumentError { - message: format!("Can't find group with name {}", weight), - }) + Err(UndefinedGroupError::new(weight.to_string())) } } } diff --git a/src/sorting/mod.rs b/src/sorting/mod.rs index e4739c42..682cb2fd 100644 --- a/src/sorting/mod.rs +++ b/src/sorting/mod.rs @@ -1,4 +1,5 @@ mod dfs; +pub mod error; pub mod groups; pub mod plugins; mod validate; diff --git a/src/sorting/plugins.rs b/src/sorting/plugins.rs index 6e8f1b82..fe96c092 100644 --- a/src/sorting/plugins.rs +++ b/src/sorting/plugins.rs @@ -12,8 +12,9 @@ use petgraph::{ use crate::{ EdgeType, Plugin, - error::{CyclicInteractionError, GeneralError, SortingLogicError, UndefinedGroupError}, metadata::{File, Group, PluginMetadata}, + plugin::error::PluginDataError, + sorting::error::{CyclicInteractionError, PathfindingError, SortingError, UndefinedGroupError}, sorting::groups::{get_default_group_node, sorted_group_nodes}, }; @@ -45,7 +46,7 @@ impl<'a> PluginSortingData<'a> { masterlist_metadata: Option<&PluginMetadata>, user_metadata: Option<&PluginMetadata>, load_order_index: usize, - ) -> Result { + ) -> Result { let override_record_count = plugin.override_record_count()?; Ok(Self { @@ -86,11 +87,11 @@ impl<'a> PluginSortingData<'a> { self.plugin.asset_count() } - pub(super) fn masters(&self) -> Result, GeneralError> { + pub(super) fn masters(&self) -> Result, PluginDataError> { self.plugin.masters() } - fn do_records_overlap(&self, other: &PluginSortingData) -> Result { + fn do_records_overlap(&self, other: &PluginSortingData) -> Result { self.plugin.do_records_overlap(other.plugin) } @@ -145,7 +146,7 @@ impl<'a> PluginsGraph<'a> { self.inner.node_indices() } - fn add_specific_edges(&mut self) -> Result<(), GeneralError> { + fn add_specific_edges(&mut self) -> Result<(), SortingError> { log::trace!("Adding edges based on plugin data and non-group metadata..."); let mut node_index_iter = self.node_indices(); @@ -323,7 +324,7 @@ impl<'a> PluginsGraph<'a> { Ok(()) } - fn add_overlap_edges(&mut self) -> Result<(), GeneralError> { + fn add_overlap_edges(&mut self) -> Result<(), SortingError> { log::trace!("Adding edges for overlapping plugins..."); let mut node_index_iter = self.node_indices(); @@ -419,7 +420,7 @@ impl<'a> PluginsGraph<'a> { Ok(()) } - fn add_tie_break_edges(&mut self) -> Result<(), SortingLogicError> { + fn add_tie_break_edges(&mut self) -> Result<(), PathfindingError> { log::trace!("Adding edges to break ties between plugins..."); // In order for the sort to be performed stably, there must be only one @@ -655,8 +656,9 @@ impl<'a> PluginsGraph<'a> { insert_position + 1 } - fn topological_sort(&self) -> Result, petgraph::algo::Cycle> { + fn topological_sort(&self) -> Result, SortingError> { petgraph::algo::toposort(&self.inner, None) + .map_err(|e| SortingError::CycleInvolving(self[e.node_id()].name().to_string())) } fn is_hamiltonian_path(&mut self, path: &[NodeIndex]) -> Option<(NodeIndex, NodeIndex)> { @@ -698,7 +700,7 @@ impl<'a> PluginsGraph<'a> { &mut self, from: NodeIndex, to: NodeIndex, - ) -> Result>, SortingLogicError> { + ) -> Result>, PathfindingError> { let mut path_finder = PathFinder::new(&self.inner, &mut self.paths_cache, from, to); if bidirectional_bfs(&self.inner, from, to, &mut path_finder) { @@ -721,7 +723,7 @@ pub fn sort_plugins( mut plugins_sorting_data: Vec, groups_graph: &GroupsGraph, early_loading_plugins: &[String], -) -> Result, GeneralError> { +) -> Result, SortingError> { if plugins_sorting_data.is_empty() { return Ok(Vec::new()); } @@ -780,7 +782,7 @@ fn sort_plugins_partition( plugins_sorting_data: Vec, groups_graph: &GroupsGraph, early_loading_plugins: &[String], -) -> Result, GeneralError> { +) -> Result, SortingError> { let mut graph = PluginsGraph::new(); for plugin in plugins_sorting_data { @@ -862,7 +864,7 @@ impl<'a, 'b> PathFinder<'a, 'b> { self.cache.entry(from).or_default().insert(to); } - fn path(&self) -> Result>, SortingLogicError> { + fn path(&self) -> Result>, PathfindingError> { match self.intersection_node { None => Ok(None), Some(intersection_node) => { @@ -879,10 +881,9 @@ impl<'a, 'b> PathFinder<'a, 'b> { self.graph[current_node].name(), path_to_string(self.graph, &path) ); - return Err(SortingLogicError::new(format!( - "Could not find parent vertex of {}", - self.graph[current_node].name() - ))); + return Err(PathfindingError::PrecedingNodeNotFound( + self.graph[current_node].name().to_string(), + )); } } @@ -901,10 +902,9 @@ impl<'a, 'b> PathFinder<'a, 'b> { self.graph[current_node].name(), path_to_string(self.graph, &path) ); - return Err(SortingLogicError::new(format!( - "Could not find child vertex of {}", - self.graph[current_node].name() - ))); + return Err(PathfindingError::FollowingNodeNotFound( + self.graph[current_node].name().to_string(), + )); } } diff --git a/src/sorting/validate.rs b/src/sorting/validate.rs index 27a04384..2182acae 100644 --- a/src/sorting/validate.rs +++ b/src/sorting/validate.rs @@ -4,7 +4,7 @@ use unicase::UniCase; use crate::{ EdgeType, Vertex, - error::{CyclicInteractionError, GeneralError, UndefinedGroupError}, + sorting::error::{CyclicInteractionError, PluginGraphValidationError, UndefinedGroupError}, }; use super::{groups::GroupsGraph, plugins::PluginSortingData}; @@ -32,7 +32,7 @@ pub fn validate_specific_and_hardcoded_edges( blueprint_masters: &[PluginSortingData<'_>], non_masters: &[PluginSortingData<'_>], early_loading_plugins: &[String], -) -> Result<(), GeneralError> { +) -> Result<(), PluginGraphValidationError> { log::trace!("Validating specific and early-loading plugin edges..."); let non_masters_set: HashSet> = @@ -57,7 +57,7 @@ fn validate_masters( masters: &[PluginSortingData<'_>], non_masters: &HashSet>, blueprint_masters: &HashSet>, -) -> Result<(), GeneralError> { +) -> Result<(), PluginGraphValidationError> { log::trace!( "Validating specific and early-loading plugin edges for non-blueprint master files..." ); @@ -69,7 +69,7 @@ fn validate_masters( fn validate_non_masters( non_masters: &[PluginSortingData<'_>], blueprint_masters: &HashSet>, -) -> Result<(), GeneralError> { +) -> Result<(), PluginGraphValidationError> { log::trace!("Validating specific and early-loading plugin edges for non-master files..."); // Pass an empty set of non-masters so that the non-masters don't get validated against themselves. @@ -84,7 +84,7 @@ fn validate_plugin( plugin: &PluginSortingData<'_>, non_masters: &HashSet>, blueprint_masters: &HashSet>, -) -> Result<(), GeneralError> { +) -> Result<(), PluginGraphValidationError> { for master in plugin.masters()? { let key = UniCase::new(master.as_str()); if non_masters.contains(&key) {