1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
use thiserror::Error;
use crate::PathHash;
/// A value that is not a valid [`Slug`](crate::Slug).
#[derive(Debug, Error)]
#[error("Invalid slug: {value}")]
pub struct InvalidSlugError {
value: String,
}
impl InvalidSlugError {
pub(crate) fn new(value: impl Into<String>) -> Self {
Self {
value: value.into(),
}
}
/// The value that was rejected.
pub fn value(&self) -> &str {
&self.value
}
}
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum ModpkgError {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Reader(#[from] ltk_io_ext::ReaderError),
/// The archive's binary layout could not be parsed.
///
/// The underlying parser error is boxed rather than named, so the parser
/// this crate happens to use is not part of its public API.
#[error("Malformed modpkg")]
Malformed(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("Invalid modpkg header size: {header_size}, actual size: {actual_size}")]
InvalidHeaderSize { header_size: u32, actual_size: u64 },
#[error("Missing metadata chunk")]
MissingMetadata,
#[error("Missing base layer")]
MissingBaseLayer,
#[error("Invalid modpkg compression type: {0}")]
InvalidCompressionType(u8),
#[error("Invalid modpkg license type: {0}")]
InvalidLicenseType(u8),
#[error("Invalid modpkg magic: {0}")]
InvalidMagic(u64),
/// The container format version, not the mod's own version.
#[error("Unsupported modpkg format version: {0}")]
UnsupportedFormatVersion(u32),
/// Inconsistent chunk content
#[error("Inconsistent duplicate chunk: {0}")]
ChunksInconsistent(PathHash),
/// No chunk answers to this path.
///
/// Also raised at mount, for a package whose chunk table names a path its
/// path table does not hold: a chunk that cannot be named cannot be
/// extracted, so the package is refused whole rather than part of the way
/// through an unpack.
#[error("Chunk not found: {0}")]
MissingChunk(PathHash),
#[error("Invalid meta chunk: must not belong to any layer or wad")]
InvalidMetaChunk,
/// The metadata declares a hashtable whose chunk the package does not hold.
#[error("Missing hashtable chunk: {path}")]
MissingHashtable { path: String },
/// A declared hashtable chunk does not fit the table grammar.
#[error("Invalid hashtable: {path}")]
InvalidHashtable {
path: String,
source: ltk_hashtable::HashtableReadError,
},
/// A chunk path, layer name or WAD name leaves the directory the package
/// would be extracted to, so extracting it would write outside it.
///
/// The package is refused whole, at mount, before any of it is read: a
/// package carrying such a name is not one to extract part of.
#[error("Package path escapes the output directory: {0}")]
EscapingPath(String),
#[error(transparent)]
MsgpackDecode(#[from] rmp_serde::decode::Error),
#[error(transparent)]
MsgpackEncode(#[from] rmp_serde::encode::Error),
}
// Kept as a `From` impl so `?` still works across the read path. The conversion
// names `binrw::Error`, but the variant does not, so matching on a decode
// failure never forces a caller to depend on binrw.
impl From<binrw::Error> for ModpkgError {
fn from(error: binrw::Error) -> Self {
Self::Malformed(Box::new(error))
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Pass-through variants are `#[error(transparent)]` so that a caller which
/// prints only the top-level error still sees the cause. Wrapping them in a
/// message like "IO error" hides it from anyone not walking the chain.
#[test]
fn pass_through_display_carries_the_cause() {
let error = ModpkgError::from(std::io::Error::new(
std::io::ErrorKind::NotFound,
"the file is gone",
));
assert!(error.to_string().contains("the file is gone"), "{error}");
}
/// A variant that adds context keeps its own message, and must not also
/// inline the source, or a chain walker prints it twice.
#[test]
fn contextual_display_does_not_embed_its_source() {
let error = ModpkgError::Malformed(Box::new(std::io::Error::other("inner detail")));
let source = std::error::Error::source(&error).unwrap().to_string();
assert!(!error.to_string().contains(&source), "{error}");
}
}