mirror of
https://github.com/loot/libloot.git
synced 2026-07-27 14:16:01 -07:00
Rework error handling
This commit is contained in:
@@ -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
|
||||
|
||||
+13
-16
@@ -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<Self, Self::Error> {
|
||||
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<T: BufRead + Seek>(
|
||||
mut reader: T,
|
||||
) -> Result<BTreeMap<u64, BTreeSet<u64>>, GeneralError> {
|
||||
) -> Result<BTreeMap<u64, BTreeSet<u64>>, 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<T: BufRead + Seek>(
|
||||
let file_hashes: &mut BTreeSet<u64> = 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+29
-34
@@ -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<Self, Self::Error> {
|
||||
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<T: BufRead>(
|
||||
mut reader: T,
|
||||
) -> Result<BTreeMap<u64, BTreeSet<u64>>, GeneralError> {
|
||||
) -> Result<BTreeMap<u64, BTreeSet<u64>>, 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<T: BufRead>(
|
||||
&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<T: BufRead, const U: usize>(
|
||||
mut reader: T,
|
||||
header: &Header,
|
||||
read_folder_record: impl Fn(&[u8]) -> FolderRecord,
|
||||
) -> Result<BTreeMap<u64, BTreeSet<u64>>, GeneralError> {
|
||||
) -> Result<BTreeMap<u64, BTreeSet<u64>>, ArchiveParsingError> {
|
||||
let mut folders_buffer: Vec<u8> = 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<T: BufRead, const U: usize>(
|
||||
|
||||
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<T: BufRead, const U: usize>(
|
||||
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<T: BufRead, const U: usize>(
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<std::io::Error> for ArchiveParsingError {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
ArchiveParsingError::IoError(value)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
mod ba2;
|
||||
mod bsa;
|
||||
mod error;
|
||||
mod find;
|
||||
mod parse;
|
||||
|
||||
|
||||
+17
-16
@@ -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<BTreeMap<u64, BTreeSet<u64>>, GeneralError> {
|
||||
let mut reader = BufReader::new(File::open(archive_path)?);
|
||||
) -> Result<BTreeMap<u64, BTreeSet<u64>>, 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),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Option<PluginMetadata>, 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::<Result<Vec<_>, _>>()?,
|
||||
);
|
||||
|
||||
metadata.set_tags(
|
||||
metadata
|
||||
.tags()
|
||||
.iter()
|
||||
.filter_map(|t| filter_map_on_condition(t, t.condition(), state))
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
);
|
||||
|
||||
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<bool, loot_condition_interpreter::Error> {
|
||||
if let Some(condition) = condition {
|
||||
Expression::from_str(condition).and_then(|e| e.eval(state))
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn filter_map_on_condition<T: Clone>(
|
||||
item: &T,
|
||||
condition: Option<&str>,
|
||||
state: &loot_condition_interpreter::State,
|
||||
) -> Option<Result<T, loot_condition_interpreter::Error>> {
|
||||
evaluate_condition(condition, state)
|
||||
.map(|r| r.then(|| item.clone()))
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn filter_files_on_conditions(
|
||||
files: &[File],
|
||||
state: &loot_condition_interpreter::State,
|
||||
) -> Result<Vec<File>, loot_condition_interpreter::Error> {
|
||||
files
|
||||
.iter()
|
||||
.filter_map(|file| filter_map_on_condition(file, file.condition(), state))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
}
|
||||
|
||||
fn filter_cleaning_data_on_conditions(
|
||||
plugin_name: &str,
|
||||
cleaning_info: &[PluginCleaningData],
|
||||
state: &loot_condition_interpreter::State,
|
||||
) -> Result<Vec<PluginCleaningData>, 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::<Result<Vec<_>, _>>()
|
||||
}
|
||||
@@ -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<loot_condition_interpreter::Error>);
|
||||
|
||||
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<loot_condition_interpreter::Error> 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<loot_condition_interpreter::Error> for MetadataRetrievalError {
|
||||
fn from(value: loot_condition_interpreter::Error) -> Self {
|
||||
MetadataRetrievalError::ConditionEvaluationError(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RegexError> for MetadataRetrievalError {
|
||||
fn from(value: RegexError) -> Self {
|
||||
MetadataRetrievalError::RegexError(value)
|
||||
}
|
||||
}
|
||||
@@ -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<Vec<Message>, GeneralError> {
|
||||
) -> Result<Vec<Message>, 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<Vec<Vertex>, GeneralError> {
|
||||
) -> Result<Vec<Vertex>, 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<Option<PluginMetadata>, GeneralError> {
|
||||
) -> Result<Option<PluginMetadata>, 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<Option<PluginMetadata>, GeneralError> {
|
||||
let metadata = self.userlist.find_plugin(plugin_name);
|
||||
) -> Result<Option<PluginMetadata>, 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<Group> {
|
||||
|
||||
groups
|
||||
}
|
||||
|
||||
fn evaluate_all_conditions(
|
||||
mut metadata: PluginMetadata,
|
||||
state: &loot_condition_interpreter::State,
|
||||
) -> Result<Option<PluginMetadata>, 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::<Result<Vec<_>, _>>()?,
|
||||
);
|
||||
|
||||
metadata.set_tags(
|
||||
metadata
|
||||
.tags()
|
||||
.iter()
|
||||
.filter_map(|t| filter_map_on_condition(t, t.condition(), state))
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
);
|
||||
|
||||
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<bool, loot_condition_interpreter::Error> {
|
||||
if let Some(condition) = condition {
|
||||
Expression::from_str(condition).and_then(|e| e.eval(state))
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
fn filter_map_on_condition<T: Clone>(
|
||||
item: &T,
|
||||
condition: Option<&str>,
|
||||
state: &loot_condition_interpreter::State,
|
||||
) -> Option<Result<T, loot_condition_interpreter::Error>> {
|
||||
evaluate_condition(condition, state)
|
||||
.map(|r| r.then(|| item.clone()))
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn filter_files_on_conditions(
|
||||
files: &[File],
|
||||
state: &loot_condition_interpreter::State,
|
||||
) -> Result<Vec<File>, loot_condition_interpreter::Error> {
|
||||
files
|
||||
.iter()
|
||||
.filter_map(|file| filter_map_on_condition(file, file.condition(), state))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
}
|
||||
|
||||
fn filter_cleaning_data_on_conditions(
|
||||
plugin_name: &str,
|
||||
cleaning_info: &[PluginCleaningData],
|
||||
state: &loot_condition_interpreter::State,
|
||||
) -> Result<Vec<PluginCleaningData>, 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::<Result<Vec<_>, _>>()
|
||||
}
|
||||
+230
-308
File diff suppressed because it is too large
Load Diff
+80
-70
@@ -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<Self, GeneralError> {
|
||||
pub fn new(game_type: GameType, game_path: &Path) -> Result<Self, GameHandleCreationError> {
|
||||
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<Self, GeneralError> {
|
||||
) -> Result<Self, GameHandleCreationError> {
|
||||
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<Vec<Plugin>, GeneralError> {
|
||||
) -> Result<Vec<Plugin>, 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<Plugin>) -> Result<(), GeneralError> {
|
||||
fn store_plugins(&mut self, plugins: Vec<Plugin>) -> 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<Vec<String>, GeneralError> {
|
||||
pub fn sort_plugins(&self, plugin_names: &[&str]) -> Result<Vec<String>, 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::<Result<Vec<_>, _>>()?;
|
||||
|
||||
@@ -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::<Result<Vec<_>, _>>()?;
|
||||
|
||||
@@ -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<bool, loadorder::Error> {
|
||||
self.load_order.is_ambiguous()
|
||||
pub fn is_load_order_ambiguous(&self) -> Result<bool, LoadOrderError> {
|
||||
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::<Result<(), PluginValidationError>>()
|
||||
}
|
||||
|
||||
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<Item = &'a Plugin>,
|
||||
) -> 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)]
|
||||
|
||||
@@ -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<saphyr::ScanError> 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<fancy_regex::Error>),
|
||||
InvalidMultilingualMessageContents,
|
||||
UnexpectedType(ExpectedType, YamlObjectType),
|
||||
UnexpectedValueType(&'static str, ExpectedType, YamlObjectType),
|
||||
MissingPlaceholder(String, usize),
|
||||
NonU32Number(i64),
|
||||
DuplicateEntry(String, YamlObjectType),
|
||||
Other(Box<saphyr::ScanError>),
|
||||
}
|
||||
|
||||
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<fancy_regex::Error>);
|
||||
|
||||
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<Box<fancy_regex::Error>> for RegexError {
|
||||
fn from(value: Box<fancy_regex::Error>) -> 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<std::io::Error> for MetadataDocumentParsingError {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
MetadataDocumentParsingError::IoError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseMetadataError> for MetadataDocumentParsingError {
|
||||
fn from(value: ParseMetadataError) -> Self {
|
||||
MetadataDocumentParsingError::MetadataParsingError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<YamlMergeKeyError> for MetadataDocumentParsingError {
|
||||
fn from(value: YamlMergeKeyError) -> Self {
|
||||
MetadataDocumentParsingError::YamlMergeKeyError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<saphyr::ScanError> for MetadataDocumentParsingError {
|
||||
fn from(value: saphyr::ScanError) -> Self {
|
||||
Self::MetadataParsingError(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub(super) struct YamlMergeKeyError {
|
||||
value: Box<saphyr::MarkedYaml>,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
+15
-21
@@ -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<MessageContent>,
|
||||
) -> Result<Self, InvalidMultilingualMessageContents> {
|
||||
) -> Result<Self, MultilingualMessageContentsError> {
|
||||
validate_message_contents(&detail)?;
|
||||
self.detail = detail;
|
||||
Ok(self)
|
||||
@@ -108,12 +108,12 @@ impl AsRef<str> 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<Self, Self::Error> {
|
||||
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,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Self, Self::Error> {
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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<Self, Self::Error> {
|
||||
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,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+36
-35
@@ -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<MessageContent>,
|
||||
) -> Result<Self, GeneralError> {
|
||||
) -> Result<Self, MultilingualMessageContentsError> {
|
||||
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<Self, Self::Error> {
|
||||
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<Vec<MessageContent>, GeneralError> {
|
||||
) -> Result<Vec<MessageContent>, 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::<Result<Vec<MessageContent>, _>>()?,
|
||||
_ => {
|
||||
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<Self, Self::Error> {
|
||||
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,
|
||||
|
||||
@@ -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<Filename, PluginMetadata> = HashMap::new();
|
||||
let mut regex_plugins: Vec<PluginMetadata> = 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<Option<PluginMetadata>, GeneralError> {
|
||||
pub fn find_plugin(&self, plugin_name: &str) -> Result<Option<PluginMetadata>, 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<MarkedYaml, YamlMergeKeyError> {
|
||||
match yaml.data {
|
||||
YamlData::Alias(_) => panic!("Alias encountered!"),
|
||||
YamlData::Array(a) => {
|
||||
yaml.data = merge_array_elements(a).map(YamlData::Array)?;
|
||||
Ok(yaml)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Holds all types related to LOOT metadata.
|
||||
pub mod error;
|
||||
mod file;
|
||||
mod group;
|
||||
mod location;
|
||||
|
||||
@@ -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<MessageContent>,
|
||||
) -> Result<Self, InvalidMultilingualMessageContents> {
|
||||
) -> Result<Self, MultilingualMessageContentsError> {
|
||||
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<Self, Self::Error> {
|
||||
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());
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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<Self, Box<fancy_regex::Error>> {
|
||||
pub fn new(name: &str) -> Result<Self, RegexError> {
|
||||
Ok(Self {
|
||||
name: PluginName::new(name)?,
|
||||
..Default::default()
|
||||
@@ -299,7 +297,7 @@ fn merge_vecs<T: Clone + PartialEq>(target: &mut Vec<T>, source: &[T]) {
|
||||
}
|
||||
|
||||
impl TryFrom<&MarkedYaml> for PluginMetadata {
|
||||
type Error = GeneralError;
|
||||
type Error = ParseMetadataError;
|
||||
|
||||
fn try_from(value: &MarkedYaml) -> Result<Self, Self::Error> {
|
||||
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<ParseMetadataError>>>(
|
||||
hash: &'a saphyr::AnnotatedHash<MarkedYaml>,
|
||||
key: &str,
|
||||
) -> Result<Vec<T>, GeneralError>
|
||||
where
|
||||
GeneralError: From<<T as TryFrom<&'a MarkedYaml>>::Error>,
|
||||
{
|
||||
key: &'static str,
|
||||
) -> Result<Vec<T>, 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::<Result<Vec<T>, _>>()
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user