Skip to main content

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}