imping_runtime/error.rs
1//! The error type.
2
3use crate::module::Build;
4use core::fmt;
5
6/// The result type of this crate's fallible functions.
7pub type Result<T> = core::result::Result<T, Error>;
8
9/// Everything that can go wrong finding a module or hooking a function.
10///
11/// Messages name the module, and the address relative to its base where one
12/// is involved.
13#[derive(Debug)]
14#[non_exhaustive]
15pub enum Error {
16 /// The module is not loaded in this process.
17 ModuleNotLoaded {
18 /// The module's file name.
19 module: &'static str,
20 },
21 /// The loaded module is none of the builds the bindings accept.
22 BuildMismatch {
23 /// The module's file name.
24 module: &'static str,
25 /// The build that is loaded.
26 found: Build,
27 /// The builds the bindings accept.
28 expected: &'static [Build],
29 },
30 /// The loaded module's headers could not be read.
31 InvalidImage {
32 /// The module's file name.
33 module: &'static str,
34 },
35 /// The module has not been initialized; call its `init` first.
36 NotInitialized {
37 /// The module's file name.
38 module: &'static str,
39 },
40 /// A function does not start with the bytes the bindings expect, and
41 /// nothing else found it; see [`Mismatch`].
42 VerificationFailed(Box<Mismatch>),
43 /// The loaded build's IR lacks the function, and neither the byte pattern
44 /// of a build that has it nor its class's vtable found it.
45 NotInBuild {
46 /// The module's file name.
47 module: &'static str,
48 /// The function's name, including its namespace.
49 item: &'static str,
50 /// The label of the loaded build.
51 build: &'static str,
52 /// Why the first pattern tried matched nothing, if there was one.
53 scan: Option<imping_hook::Error>,
54 },
55 /// The function is already hooked by this mod: a [`Hook`](crate::Hook) for
56 /// it is still alive.
57 AlreadyHooked {
58 /// The module's file name.
59 module: &'static str,
60 /// The function's address relative to the module's base.
61 rva: u32,
62 },
63 /// The hooking engine failed to install, enable or disable a hook.
64 Hook {
65 /// The module's file name.
66 module: &'static str,
67 /// The function's address relative to the module's base.
68 rva: u32,
69 /// The engine's error.
70 source: imping_hook::Error,
71 },
72 /// This platform is not supported; only Windows is.
73 UnsupportedPlatform,
74}
75
76impl fmt::Display for Error {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 match self {
79 Error::ModuleNotLoaded { module } => {
80 write!(f, "`{module}` is not loaded in this process")
81 }
82 Error::BuildMismatch {
83 module,
84 found,
85 expected,
86 } => {
87 write!(
88 f,
89 "`{module}` is a different build than the bindings accept: \
90 found timestamp {:#x}, image size {:#x}; accepted ",
91 found.timestamp, found.image_size
92 )?;
93 if expected.is_empty() {
94 return f.write_str("none");
95 }
96 for (i, build) in expected.iter().enumerate() {
97 if i > 0 {
98 f.write_str(", ")?;
99 }
100 write!(f, "{:#x}/{:#x}", build.timestamp, build.image_size)?;
101 }
102 Ok(())
103 }
104 Error::InvalidImage { module } => write!(f, "`{module}` has unreadable PE headers"),
105 Error::NotInitialized { module } => {
106 write!(f, "`{module}` is not initialized; call its `init()` first")
107 }
108 Error::VerificationFailed(mismatch) => mismatch.fmt(f),
109 Error::NotInBuild {
110 module,
111 item,
112 build,
113 scan,
114 } => {
115 write!(f, "`{item}` isn't in the {build} IR of `{module}`")?;
116 match scan {
117 Some(scan) => write!(f, ", and its pattern matched nothing ({scan})"),
118 None => f.write_str(", and no other build has a pattern for it"),
119 }
120 }
121 Error::AlreadyHooked { module, rva } => {
122 write!(f, "`{module}`+{rva:#x} is already hooked by this mod")
123 }
124 Error::Hook {
125 module,
126 rva,
127 source,
128 } => write!(f, "failed to hook `{module}`+{rva:#x}: {source}"),
129 Error::UnsupportedPlatform => {
130 f.write_str("imping-runtime does not support this platform yet")
131 }
132 }
133 }
134}
135
136impl core::error::Error for Error {
137 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
138 match self {
139 Error::Hook { source, .. } => Some(source),
140 Error::VerificationFailed(mismatch) => match &mismatch.scan {
141 Some(source) => Some(source),
142 None => None,
143 },
144 Error::NotInBuild {
145 scan: Some(source), ..
146 } => Some(source),
147 _ => None,
148 }
149 }
150}
151
152/// Why a function wasn't where the bindings expect, and why nothing else found
153/// it: the details of [`Error::VerificationFailed`].
154///
155/// Boxed, so the error stays small as ways to find a function are added.
156///
157/// # Examples
158///
159/// ```no_run
160/// # fn check(error: imping_runtime::Error) {
161/// use imping_runtime::{Error, VtableMiss};
162///
163/// if let Error::VerificationFailed(mismatch) = &error {
164/// if let Some(VtableMiss::ClassNotFound { type_name }) = &mismatch.vtable {
165/// eprintln!("{type_name} is gone from this build");
166/// }
167/// }
168/// # }
169/// ```
170#[derive(Debug)]
171#[non_exhaustive]
172pub struct Mismatch {
173 /// The module's file name.
174 pub module: &'static str,
175 /// The function's address relative to the module's base.
176 pub rva: u32,
177 /// The bytes the bindings expect.
178 pub expected: &'static [u8],
179 /// The bytes found, empty if they'd be past the end of the module.
180 pub found: Vec<u8>,
181 /// Why the function's byte pattern didn't find it, if it has one and
182 /// that's why.
183 pub scan: Option<imping_hook::Error>,
184 /// Why its class's vtable didn't lead to it, if it's in one.
185 pub vtable: Option<VtableMiss>,
186}
187
188impl fmt::Display for Mismatch {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 let Mismatch { module, rva, .. } = self;
191 if self.found.is_empty() {
192 write!(
193 f,
194 "`{module}`+{rva:#x} is past the end of the module; the build differs"
195 )?;
196 } else {
197 write!(
198 f,
199 "`{module}`+{rva:#x} starts with {}, expected {}; the build differs or another \
200 mod hooked it",
201 Hex(&self.found),
202 Hex(self.expected)
203 )?;
204 }
205 if let Some(scan) = &self.scan {
206 write!(f, ", and {scan}")?;
207 }
208 match &self.vtable {
209 Some(vtable) => write!(f, ", and {vtable}"),
210 None => Ok(()),
211 }
212 }
213}
214
215/// Why a function couldn't be found through its class's vtable.
216///
217/// Part of a [`Mismatch`]; type names are MSVC's decorated ones, such as
218/// `.?AVEntity@game@@`.
219#[derive(Debug, Clone, PartialEq, Eq)]
220#[non_exhaustive]
221pub enum VtableMiss {
222 /// The module has no type information for the class.
223 ClassNotFound {
224 /// The class's type name.
225 type_name: &'static str,
226 },
227 /// The class has no vtable at this offset.
228 VtableNotFound {
229 /// The class's type name.
230 type_name: &'static str,
231 /// The locator's offset, which tells apart a class's vtables.
232 offset: u32,
233 },
234 /// The slot holds a function that doesn't start like this one.
235 ShapeDiffers {
236 /// The class's type name.
237 type_name: &'static str,
238 /// The slot's index.
239 index: u32,
240 /// The bytes the slot's function starts with, empty if it's outside
241 /// the module.
242 found: Vec<u8>,
243 },
244}
245
246impl fmt::Display for VtableMiss {
247 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248 match self {
249 VtableMiss::ClassNotFound { type_name } => {
250 write!(f, "class `{type_name}` was not found")
251 }
252 VtableMiss::VtableNotFound { type_name, offset } => {
253 write!(f, "class `{type_name}` has no vtable at offset {offset}")
254 }
255 VtableMiss::ShapeDiffers {
256 type_name,
257 index,
258 found,
259 } if found.is_empty() => {
260 write!(f, "slot {index} of `{type_name}` points outside the module")
261 }
262 VtableMiss::ShapeDiffers {
263 type_name,
264 index,
265 found,
266 } => write!(
267 f,
268 "slot {index} of `{type_name}` starts with {}",
269 Hex(found)
270 ),
271 }
272 }
273}
274
275/// Bytes as space-separated lowercase hex, e.g. `48 83 ec 28`.
276struct Hex<'a>(&'a [u8]);
277
278impl fmt::Display for Hex<'_> {
279 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280 for (i, byte) in self.0.iter().enumerate() {
281 if i > 0 {
282 f.write_str(" ")?;
283 }
284 write!(f, "{byte:02x}")?;
285 }
286 Ok(())
287 }
288}