./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
129 / 211
61.1%
Branches:
62 / 106
58.5%
Functions:
4 / 4
100.0%

include/DetourModKit/error.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_ERROR_HPP
2 #define DETOURMODKIT_ERROR_HPP
3
4 /**
5 * @file error.hpp
6 * @brief Shared ErrorCode, Error, and Result<T> definitions.
7 * @details Result-tier APIs return `Result<T>`, and DMK_TRY and DMK_TRY_VOID propagate its Error. Best-effort APIs
8 * return their own documented types, for example `bool`, `std::optional`, or `void`, and never an Error.
9 */
10
11 #include "DetourModKit/defines.hpp"
12
13 #include <cstdint>
14 #include <expected>
15 #include <format>
16 #include <string>
17 #include <string_view>
18 #include <type_traits>
19 #include <utility>
20
21 namespace DetourModKit
22 {
23 /** @brief The subsystem of an ErrorCode, held in the high byte of its value. The category values are stable. */
24 enum class ErrorCategory : std::uint8_t
25 {
26 /// Cross-cutting codes such as argument checks, allocation failures, and pattern errors.
27 General = 0x00,
28 /// Inline, mid-function, and VMT hooking.
29 Hook = 0x01,
30 /// AOB cascade, RIP-relative resolve, and string-xref resolution.
31 Scan = 0x02,
32 /// Guarded reads, writes, and protection changes.
33 Memory = 0x03,
34 /// Reverse-RTTI identification and self-heal.
35 Rtti = 0x04,
36 /// Manifest serialization and parsing.
37 Manifest = 0x05,
38 /// Session and bootstrap process lifecycle: start, single-instance gate, and worker spawn.
39 Lifecycle = 0x06
40 };
41
42 /**
43 * @brief The library-wide failure code, with its @ref ErrorCategory in the high byte.
44 * @details Each category block starts at `category << 8`. Only the high byte is stable. The low byte follows
45 * declaration order, so a new enumerator renumbers every later member of its block. Branch on the
46 * enumerator, never on its numeric value. If a log, wire, or telemetry format needs a fixed value, write a
47 * versioned symbol for each enumerator.
48 */
49 enum class ErrorCode : std::uint16_t
50 {
51 // General (0x00xx).
52 /// Success sentinel. An Error that reports a failure never holds it.
53 Ok = 0x0000,
54 /// A factory or operation rejected its arguments, for example an empty name, a null target, or an empty ladder.
55 InvalidArg,
56 /// An allocation failed.
57 OutOfMemory,
58 /// An AOB pattern failed to parse or exceeded the inline-storage cap.
59 BadPattern,
60 /// A pointer-chain walk received a null root.
61 NullChain,
62 /// Last-resort code when no more specific one applies.
63 Unknown,
64
65 // Hook (0x01xx).
66 /// The hook backend allocator was not available.
67 AllocatorNotAvailable = 0x0100,
68 /// The target address to hook was null or unusable.
69 InvalidTargetAddress,
70 /// The supplied detour function pointer was null or unusable.
71 InvalidDetourFunction,
72 /// The trampoline out-pointer was null.
73 InvalidTrampolinePointer,
74 /// A hook with that name is already registered.
75 HookAlreadyExists,
76 /// No hook with that name is registered.
77 HookNotFound,
78 /// The manager is in teardown and rejects new operations.
79 ShutdownInProgress,
80 /// The hook backend reported a failure, or the process coordinator refused for the process lifetime.
81 BackendFailed,
82 /// The hook failed to enable.
83 EnableFailed,
84 /// The hook failed to disable.
85 DisableFailed,
86 /// The hook was in a state that does not permit the requested operation.
87 InvalidHookState,
88 /// The object, for example a VMT instance, was null or invalid.
89 InvalidObject,
90 /// No VMT hook is registered for that object.
91 VmtHookNotFound,
92 /// That VMT slot/method is already hooked.
93 MethodAlreadyHooked,
94 /// That VMT slot/method is not hooked.
95 MethodNotFound,
96 /**
97 * @brief This linked DMK instance already holds the target, and the install asked to refuse a duplicate.
98 * @details The same-kit ledger reports it, with a scope of one linked archive, not the process. A hook that is
99 * created but not enabled also holds a record, so this code does not prove that the prologue is
100 * patched. Drop the prior handle, or clear Options::fail_if_already_hooked to layer on it. A pinned
101 * record (hook::Hook::release, a teardown that failed to restore, or a residue) belongs to no handle
102 * and is never erased. It refuses every later strict install on that address, also after the
103 * allocator reissues the address. A recorded target reports this code even after a foreign module
104 * patches it, because the ledger check runs before the @ref TargetAlreadyHookedByAnotherModule
105 * decode.
106 */
107 TargetAlreadyHookedByThisKit,
108 /**
109 * @brief The target's prologue already branches out of its own module, and the install asked to refuse.
110 * @details The foreign-JMP decode reports it when the same-kit ledger has no record. A destination in no loaded
111 * module also counts, for example a detour in private trampoline memory. This kit owns nothing to
112 * drop, so the caller either layers on the target or abandons it.
113 */
114 TargetAlreadyHookedByAnotherModule,
115 /// A re-entrant call into the guarded path was rejected.
116 ReentrantCallRejected,
117 /**
118 * @brief The target prologue is not safe to relocate.
119 * @details A hook install returns this code, @ref ReadFaulted, and both `TargetAlreadyHooked` codes only for
120 * the settled target. It waits out a held coordinator and a patch window first, so a repeat call on
121 * the same target returns the same code.
122 */
123 TargetPrologueUnsafe,
124 /// An unclassified hook error: an unmapped backend failure, or a failed hook gate acquisition.
125 UnknownError,
126 /**
127 * @brief Refused with no change. A restore, or an arm over the hook's own patch, writes under a newer layered
128 * hook, an arm cannot chain to the layer on the target, or the hook ledger cannot decide the layer order.
129 * An armed lower layer bypasses instead.
130 */
131 LayerConflict,
132 /**
133 * @brief Every mid-hook adapter in the fixed pool is in use, so nothing was patched.
134 * @details Destroy an unneeded mid hook, or hook fewer sites. Inline and VMT hooks are unaffected.
135 */
136 MidHookCapacityExhausted,
137 /// The hook module refuses mutation under its loader-lock precondition.
138 LoaderLockActive,
139 /// A retirement refused with no change, because a layer that cannot restore exceeds the residue budget.
140 LayerResidueLimitReached,
141
142 // Scan (0x02xx).
143 /// No candidates were supplied to the cascade.
144 EmptyCandidates = 0x0200,
145 /// No cascade candidate matched the scanned scope.
146 NoMatch,
147 /// Every byte-candidate pattern failed to parse.
148 AllPatternsInvalid,
149 /// A Direct candidate existed, but none was safe to rebuild as a hooked prologue.
150 PrologueFallbackNotApplicable,
151 /// The supplied module range was not a valid mapped image.
152 InvalidRange,
153 /// read_code_constant: the resolved site did not decode.
154 DecodeFailed,
155 /// read_code_constant: the operand was not the requested kind.
156 UnexpectedShape,
157 /// read_code_constant: the operand index was past the operand count.
158 OperandOutOfRange,
159 /// RIP resolve: the input pointer was null.
160 NullInput,
161 /// RIP resolve: the opcode prefix was not found in the search region.
162 PrefixNotFound,
163 /// RIP resolve: the search region was too small to hold the displacement.
164 RegionTooSmall,
165 /// RIP resolve: the displacement bytes were unreadable.
166 UnreadableDisplacement,
167 /// RIP resolve: the resolved target was not a plausible address.
168 ImplausibleTarget,
169 /** @brief RIP resolve: the last matched prefix resolved to a plausible, unreadable address in Error::detail. */
170 UnreadableTarget,
171 /// String xref: the query text was empty.
172 EmptyQuery,
173 /// String xref: the literal was not found in any readable page.
174 StringNotFound,
175 /// String xref: the literal occurs more than once.
176 StringAmbiguous,
177 /// String xref: no recognized RIP-relative reference resolves to it.
178 NoReference,
179 /// String xref: more than one instruction references it.
180 AmbiguousReference,
181 /// String xref: no prologue within the enclosing-function back-scan window.
182 FunctionNotFound,
183 /// String xref: no pointer-slot store of the loaded pointer follows the reference.
184 StoreNotFound,
185 /// Prologue recovery found a unique site, but identity confirmation rejected it or was missing.
186 PrologueIdentityRejected,
187 /// Export resolve: no usable export with the requested name. @ref scan::resolve_export lists the cases.
188 ExportNotFound,
189 /// Export resolve: the export forwards to another module as a "Dll.Func" string, not code. It fails closed.
190 ExportForwarded,
191 /**
192 * @brief A bounded-jump pattern spent its backtracking work budget, so the traversal stopped short.
193 * @details Distinct from NoMatch: the scan proved nothing about the unvisited positions. Before a retry, add a
194 * literal byte to the leading segment of the pattern, or narrow the scope.
195 */
196 BudgetExceeded,
197 /**
198 * @brief A page-gated sweep skipped a region, so its occurrence count is a lower bound.
199 * @details Distinct from NoMatch: the skipped bytes can hold a match, or a duplicate that makes the result
200 * ambiguous. A concurrent decommit or reprotect of the scanned range causes it. So does a MinGW sweep
201 * with no guarded-read fault handler installed (@ref diagnostics::ScannerFaultEvent).
202 */
203 IncompleteScan,
204 /**
205 * @brief The scan did not prove its result unique, because query-owned storage can participate in it.
206 * @details Raised under the Readable authority rule of @ref scan::Pages, which lists its remedies. Also raised
207 * when the merged exclusion spans overflow their bounded set. The overflow remedy is fewer declared
208 * spans or a narrower scope.
209 */
210 NotAuthoritative,
211 /// String xref: the query text is not well-formed UTF-8, or it violates the embedded-NUL policy.
212 MalformedQueryText,
213 /**
214 * @brief Prologue recovery rebuilt a usable hook shape, but it matched more than one executable site.
215 * @details No single redirected target is trustworthy. Sharpen the surviving tail of the signature.
216 */
217 PrologueFallbackAmbiguous,
218 /**
219 * @brief The selected byte rung no longer resolves the decoded site at the fresh epoch.
220 * @details The selector evidence is stale. Its physical span can fail to match, a bounded-gap result point can
221 * move, or a wildcarded RIP locator can resolve elsewhere.
222 */
223 EvidenceMismatch,
224
225 // Memory (0x03xx).
226 /// The write target address was null.
227 NullTargetAddress = 0x0300,
228 /// The source byte span was null.
229 NullSourceBytes,
230 /// The operation size exceeded the permitted bound.
231 SizeTooLarge,
232 /// The page protection change failed.
233 ProtectionChangeFailed,
234 /// The restore of the original page protection failed.
235 ProtectionRestoreFailed,
236 /**
237 * @brief A guarded read faulted.
238 * @details `Error::detail` holds the address of the fault, inside the requested span. For the small spans of a
239 * typed read, it is the first unreadable byte. A wide span can fault out of order in `memcpy`, so a
240 * later byte can be reported. A span refused before any access reports its start: below
241 * @ref memory::USERSPACE_PTR_MIN, an end past @ref memory::USERSPACE_PTR_MAX, or an end that wraps.
242 * The MinGW fallback, which validates through `VirtualQuery` without a fault, also reports the start.
243 * For @ref memory::walk, the field is the failing hop index, not an address.
244 */
245 ReadFaulted,
246 /// A guarded write failed before it changed any byte. Error::detail holds the target address.
247 WriteFaulted,
248 /**
249 * @brief A guarded write reached a writable page, then faulted on an unwritable or unmapped byte further in.
250 * @details The changed prefix of the span has an unknown length and can be empty. A fixed-width store that
251 * straddles a writable and an unwritable page changes no byte but still reports this code. No byte
252 * outside the requested span changes. Treat the whole target as indeterminate. Error::detail holds the
253 * target address.
254 */
255 WriteMayBePartial,
256 /// A code patch wrote its bytes but the instruction-cache flush failed. Error::detail holds the target address.
257 InstructionFlushFailed,
258 /**
259 * @brief A typed read found bytes that are not a valid object representation of the requested type.
260 * @details An example is a byte other than 0 or 1 for @ref memory::read_bool. No value was formed.
261 * Error::detail holds the source address.
262 */
263 InvalidRepresentation,
264 /**
265 * @brief The caller buffer or source span overlaps the target range, in either direction.
266 * @details The copy primitives require disjoint half-open ranges and refuse an overlap before any byte moves.
267 * Error::detail holds the target address.
268 */
269 OverlappingRanges,
270
271 // Rtti (0x04xx).
272 /// The slot address was null or below the user-mode floor. No read was attempted.
273 BadSlotAddress = 0x0400,
274 /// The slot read faulted, or the qword held a null/low value.
275 UnreadableSlot,
276 /// The slot resolved to neither a pointer-to-object nor a direct object.
277 NoRtti,
278 /// The landmark/fingerprint descriptor is malformed. No memory was touched.
279 BadDescriptor,
280 /// No slot in the window resolved to the expected type.
281 HealNoMatch,
282 /// Equidistant slots both match, or fingerprint deltas tied.
283 HealAmbiguous,
284 /**
285 * @brief A healed offset is not confirmed for use.
286 * @details A required heal missed (Invalid), or an optional heal kept an unconfirmed nominal (Unverified).
287 * The HealedSlot form of @ref rtti::HealRun::heal_into also returns it when the image generation of a
288 * resolved heal is absent or changed, or its evidence changed. @ref rtti::HealedSlot::authorized also
289 * rejects a Confirmed slot with a zero or stale generation. The value must not authorize a mutation.
290 * @ref rtti::HealedSlot::load returns the retained value and its validity.
291 */
292 OffsetNotConfirmed,
293
294 // Manifest (0x05xx).
295 /// The `[manifest]` section or its `schema` key is absent, or the schema is unsupported.
296 MissingHeader = 0x0500,
297 /// A line, key, or value that @ref manifest::parse rejects, as manifest.hpp lists.
298 MalformedLine,
299 /// The file did not open or read, for example because it is missing, locked, denied, or not a regular file.
300 FileOpenFailed,
301 /// The file opened but a later write failed, for example on a full disk, an I/O error, or a failed stream.
302 FileWriteFailed,
303 /**
304 * @brief Two section or key identities collide after case folding or exact/whitespace normalization.
305 * @details The whole manifest fails before parsing or trust evaluation can observe an ambiguous contract.
306 */
307 ManifestIdentityCollision = 0x0504,
308 /**
309 * @brief A raw manifest frames a multi-line (heredoc) value unsafely.
310 * @details The unsafe shapes are an unclosed block, an opener with an empty tag, and a first body line that is
311 * its own terminator. Checked serialization reports an unsafe source value as InvalidArg before it
312 * writes the value.
313 */
314 ManifestFramingUnsafe,
315
316 // Lifecycle (0x06xx).
317 /// The running executable did not match ModInfo::game_process_name. The session declined to load (not a fault).
318 ProcessMismatch = 0x0600,
319 /// The single-instance mutex was already held: another load of this mod is live in the process.
320 InstanceAlreadyRunning,
321 /// start()/bootstrap() was called while a Session is already active in this process (a caller sequencing bug).
322 SessionAlreadyActive,
323 /**
324 * @brief A system lifecycle operation failed.
325 * @details Error::detail holds GetLastError() unless the function that reports it documents another value.
326 */
327 SystemCallFailed,
328 /// A bootstrap lifecycle operation raced a concurrent attach, a drain, or the previous generation's retirement.
329 SessionShutdownInProgress,
330 /// Loader detach already claimed the bootstrap state, so a synchronous drain can no longer be guaranteed.
331 SessionShutdownUnavailable,
332 /**
333 * @brief A synchronous bootstrap drain was refused because the wait can block.
334 * @details The caller possibly holds the loader lock, or it is the bootstrap worker that the drain waits for.
335 */
336 SessionShutdownWouldBlock
337 };
338
339 /** @brief Returns the @ref ErrorCategory in the high byte of @p code. */
340 13 [[nodiscard]] constexpr ErrorCategory category(ErrorCode code) noexcept
341 {
342 13 return static_cast<ErrorCategory>((static_cast<std::uint16_t>(code) >> 8) & 0xFFU);
343 }
344
345 /** @brief Returns a static label for @p value, or "unknown" for an out-of-range value. */
346 9 [[nodiscard]] constexpr std::string_view to_string(ErrorCategory value) noexcept
347 {
348
4/8
✓ Branch 2 → 3 taken 5 times.
✗ Branch 2 → 4 not taken.
✓ Branch 2 → 5 taken 2 times.
✗ Branch 2 → 6 not taken.
✗ Branch 2 → 7 not taken.
✓ Branch 2 → 8 taken 1 time.
✓ Branch 2 → 9 taken 1 time.
✗ Branch 2 → 10 not taken.
9 switch (value)
349 {
350 5 case ErrorCategory::General:
351 5 return "general";
352 ✗ case ErrorCategory::Hook:
353 ✗ return "hook";
354 2 case ErrorCategory::Scan:
355 2 return "scan";
356 ✗ case ErrorCategory::Memory:
357 ✗ return "memory";
358 ✗ case ErrorCategory::Rtti:
359 ✗ return "rtti";
360 1 case ErrorCategory::Manifest:
361 1 return "manifest";
362 1 case ErrorCategory::Lifecycle:
363 1 return "lifecycle";
364 }
365 ✗ return "unknown";
366 }
367
368 /** @brief Returns the static enumerator name of @p code, or "UnknownCode" for an out-of-range value. */
369 84 [[nodiscard]] constexpr std::string_view to_string(ErrorCode code) noexcept
370 {
371
55/92
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 3 times.
✓ Branch 2 → 5 taken 1 time.
✓ Branch 2 → 6 taken 2 times.
✗ Branch 2 → 7 not taken.
✓ Branch 2 → 8 taken 1 time.
✗ Branch 2 → 9 not taken.
✗ Branch 2 → 10 not taken.
✗ Branch 2 → 11 not taken.
✗ Branch 2 → 12 not taken.
✗ Branch 2 → 13 not taken.
✗ Branch 2 → 14 not taken.
✗ Branch 2 → 15 not taken.
✓ Branch 2 → 16 taken 1 time.
✗ Branch 2 → 17 not taken.
✗ Branch 2 → 18 not taken.
✗ Branch 2 → 19 not taken.
✗ Branch 2 → 20 not taken.
✗ Branch 2 → 21 not taken.
✗ Branch 2 → 22 not taken.
✗ Branch 2 → 23 not taken.
✓ Branch 2 → 24 taken 1 time.
✓ Branch 2 → 25 taken 1 time.
✗ Branch 2 → 26 not taken.
✗ Branch 2 → 27 not taken.
✗ Branch 2 → 28 not taken.
✗ Branch 2 → 29 not taken.
✗ Branch 2 → 30 not taken.
✗ Branch 2 → 31 not taken.
✗ Branch 2 → 32 not taken.
✓ Branch 2 → 33 taken 1 time.
✓ Branch 2 → 34 taken 2 times.
✗ Branch 2 → 35 not taken.
✓ Branch 2 → 36 taken 1 time.
✓ Branch 2 → 37 taken 2 times.
✓ Branch 2 → 38 taken 2 times.
✓ Branch 2 → 39 taken 1 time.
✓ Branch 2 → 40 taken 1 time.
✓ Branch 2 → 41 taken 1 time.
✓ Branch 2 → 42 taken 2 times.
✓ Branch 2 → 43 taken 2 times.
✓ Branch 2 → 44 taken 2 times.
✓ Branch 2 → 45 taken 2 times.
✓ Branch 2 → 46 taken 1 time.
✓ Branch 2 → 47 taken 2 times.
✓ Branch 2 → 48 taken 1 time.
✓ Branch 2 → 49 taken 1 time.
✓ Branch 2 → 50 taken 1 time.
✓ Branch 2 → 51 taken 1 time.
✓ Branch 2 → 52 taken 1 time.
✓ Branch 2 → 53 taken 1 time.
✓ Branch 2 → 54 taken 1 time.
✗ Branch 2 → 55 not taken.
✓ Branch 2 → 56 taken 1 time.
✓ Branch 2 → 57 taken 1 time.
✓ Branch 2 → 58 taken 2 times.
✓ Branch 2 → 59 taken 3 times.
✓ Branch 2 → 60 taken 1 time.
✓ Branch 2 → 61 taken 1 time.
✓ Branch 2 → 62 taken 1 time.
✓ Branch 2 → 63 taken 1 time.
✓ Branch 2 → 64 taken 1 time.
✓ Branch 2 → 65 taken 1 time.
✓ Branch 2 → 66 taken 1 time.
✓ Branch 2 → 67 taken 1 time.
✓ Branch 2 → 68 taken 1 time.
✗ Branch 2 → 69 not taken.
✗ Branch 2 → 70 not taken.
✗ Branch 2 → 71 not taken.
✗ Branch 2 → 72 not taken.
✗ Branch 2 → 73 not taken.
✗ Branch 2 → 74 not taken.
✗ Branch 2 → 75 not taken.
✗ Branch 2 → 76 not taken.
✗ Branch 2 → 77 not taken.
✓ Branch 2 → 78 taken 6 times.
✗ Branch 2 → 79 not taken.
✓ Branch 2 → 80 taken 3 times.
✓ Branch 2 → 81 taken 3 times.
✓ Branch 2 → 82 taken 3 times.
✓ Branch 2 → 83 taken 4 times.
✓ Branch 2 → 84 taken 2 times.
✗ Branch 2 → 85 not taken.
✗ Branch 2 → 86 not taken.
✓ Branch 2 → 87 taken 1 time.
✓ Branch 2 → 88 taken 1 time.
✓ Branch 2 → 89 taken 1 time.
✓ Branch 2 → 90 taken 1 time.
✓ Branch 2 → 91 taken 1 time.
✓ Branch 2 → 92 taken 1 time.
✓ Branch 2 → 93 taken 1 time.
✓ Branch 2 → 94 taken 1 time.
84 switch (code)
372 {
373 ✗ case ErrorCode::Ok:
374 ✗ return "Ok";
375 3 case ErrorCode::InvalidArg:
376 3 return "InvalidArg";
377 1 case ErrorCode::OutOfMemory:
378 1 return "OutOfMemory";
379 2 case ErrorCode::BadPattern:
380 2 return "BadPattern";
381 ✗ case ErrorCode::NullChain:
382 ✗ return "NullChain";
383 1 case ErrorCode::Unknown:
384 1 return "Unknown";
385 ✗ case ErrorCode::AllocatorNotAvailable:
386 ✗ return "AllocatorNotAvailable";
387 ✗ case ErrorCode::InvalidTargetAddress:
388 ✗ return "InvalidTargetAddress";
389 ✗ case ErrorCode::InvalidDetourFunction:
390 ✗ return "InvalidDetourFunction";
391 ✗ case ErrorCode::InvalidTrampolinePointer:
392 ✗ return "InvalidTrampolinePointer";
393 ✗ case ErrorCode::HookAlreadyExists:
394 ✗ return "HookAlreadyExists";
395 ✗ case ErrorCode::HookNotFound:
396 ✗ return "HookNotFound";
397 ✗ case ErrorCode::ShutdownInProgress:
398 ✗ return "ShutdownInProgress";
399 1 case ErrorCode::BackendFailed:
400 1 return "BackendFailed";
401 ✗ case ErrorCode::EnableFailed:
402 ✗ return "EnableFailed";
403 ✗ case ErrorCode::DisableFailed:
404 ✗ return "DisableFailed";
405 ✗ case ErrorCode::InvalidHookState:
406 ✗ return "InvalidHookState";
407 ✗ case ErrorCode::InvalidObject:
408 ✗ return "InvalidObject";
409 ✗ case ErrorCode::VmtHookNotFound:
410 ✗ return "VmtHookNotFound";
411 ✗ case ErrorCode::MethodAlreadyHooked:
412 ✗ return "MethodAlreadyHooked";
413 ✗ case ErrorCode::MethodNotFound:
414 ✗ return "MethodNotFound";
415 1 case ErrorCode::TargetAlreadyHookedByThisKit:
416 1 return "TargetAlreadyHookedByThisKit";
417 1 case ErrorCode::TargetAlreadyHookedByAnotherModule:
418 1 return "TargetAlreadyHookedByAnotherModule";
419 ✗ case ErrorCode::ReentrantCallRejected:
420 ✗ return "ReentrantCallRejected";
421 ✗ case ErrorCode::TargetPrologueUnsafe:
422 ✗ return "TargetPrologueUnsafe";
423 ✗ case ErrorCode::UnknownError:
424 ✗ return "UnknownError";
425 ✗ case ErrorCode::LayerConflict:
426 ✗ return "LayerConflict";
427 ✗ case ErrorCode::MidHookCapacityExhausted:
428 ✗ return "MidHookCapacityExhausted";
429 ✗ case ErrorCode::LoaderLockActive:
430 ✗ return "LoaderLockActive";
431 ✗ case ErrorCode::LayerResidueLimitReached:
432 ✗ return "LayerResidueLimitReached";
433 1 case ErrorCode::EmptyCandidates:
434 1 return "EmptyCandidates";
435 2 case ErrorCode::NoMatch:
436 2 return "NoMatch";
437 ✗ case ErrorCode::AllPatternsInvalid:
438 ✗ return "AllPatternsInvalid";
439 1 case ErrorCode::PrologueFallbackNotApplicable:
440 1 return "PrologueFallbackNotApplicable";
441 2 case ErrorCode::PrologueFallbackAmbiguous:
442 2 return "PrologueFallbackAmbiguous";
443 2 case ErrorCode::InvalidRange:
444 2 return "InvalidRange";
445 1 case ErrorCode::DecodeFailed:
446 1 return "DecodeFailed";
447 1 case ErrorCode::UnexpectedShape:
448 1 return "UnexpectedShape";
449 1 case ErrorCode::OperandOutOfRange:
450 1 return "OperandOutOfRange";
451 2 case ErrorCode::NullInput:
452 2 return "NullInput";
453 2 case ErrorCode::PrefixNotFound:
454 2 return "PrefixNotFound";
455 2 case ErrorCode::RegionTooSmall:
456 2 return "RegionTooSmall";
457 2 case ErrorCode::UnreadableDisplacement:
458 2 return "UnreadableDisplacement";
459 1 case ErrorCode::ImplausibleTarget:
460 1 return "ImplausibleTarget";
461 2 case ErrorCode::UnreadableTarget:
462 2 return "UnreadableTarget";
463 1 case ErrorCode::EmptyQuery:
464 1 return "EmptyQuery";
465 1 case ErrorCode::StringNotFound:
466 1 return "StringNotFound";
467 1 case ErrorCode::StringAmbiguous:
468 1 return "StringAmbiguous";
469 1 case ErrorCode::NoReference:
470 1 return "NoReference";
471 1 case ErrorCode::AmbiguousReference:
472 1 return "AmbiguousReference";
473 1 case ErrorCode::FunctionNotFound:
474 1 return "FunctionNotFound";
475 1 case ErrorCode::StoreNotFound:
476 1 return "StoreNotFound";
477 ✗ case ErrorCode::PrologueIdentityRejected:
478 ✗ return "PrologueIdentityRejected";
479 1 case ErrorCode::ExportNotFound:
480 1 return "ExportNotFound";
481 1 case ErrorCode::ExportForwarded:
482 1 return "ExportForwarded";
483 2 case ErrorCode::BudgetExceeded:
484 2 return "BudgetExceeded";
485 3 case ErrorCode::IncompleteScan:
486 3 return "IncompleteScan";
487 1 case ErrorCode::NotAuthoritative:
488 1 return "NotAuthoritative";
489 1 case ErrorCode::MalformedQueryText:
490 1 return "MalformedQueryText";
491 1 case ErrorCode::EvidenceMismatch:
492 1 return "EvidenceMismatch";
493 1 case ErrorCode::NullTargetAddress:
494 1 return "NullTargetAddress";
495 1 case ErrorCode::NullSourceBytes:
496 1 return "NullSourceBytes";
497 1 case ErrorCode::SizeTooLarge:
498 1 return "SizeTooLarge";
499 1 case ErrorCode::ProtectionChangeFailed:
500 1 return "ProtectionChangeFailed";
501 1 case ErrorCode::ProtectionRestoreFailed:
502 1 return "ProtectionRestoreFailed";
503 1 case ErrorCode::ReadFaulted:
504 1 return "ReadFaulted";
505 ✗ case ErrorCode::WriteFaulted:
506 ✗ return "WriteFaulted";
507 ✗ case ErrorCode::WriteMayBePartial:
508 ✗ return "WriteMayBePartial";
509 ✗ case ErrorCode::InstructionFlushFailed:
510 ✗ return "InstructionFlushFailed";
511 ✗ case ErrorCode::InvalidRepresentation:
512 ✗ return "InvalidRepresentation";
513 ✗ case ErrorCode::OverlappingRanges:
514 ✗ return "OverlappingRanges";
515 ✗ case ErrorCode::BadSlotAddress:
516 ✗ return "BadSlotAddress";
517 ✗ case ErrorCode::UnreadableSlot:
518 ✗ return "UnreadableSlot";
519 ✗ case ErrorCode::NoRtti:
520 ✗ return "NoRtti";
521 ✗ case ErrorCode::BadDescriptor:
522 ✗ return "BadDescriptor";
523 6 case ErrorCode::HealNoMatch:
524 6 return "HealNoMatch";
525 ✗ case ErrorCode::HealAmbiguous:
526 ✗ return "HealAmbiguous";
527 3 case ErrorCode::OffsetNotConfirmed:
528 3 return "OffsetNotConfirmed";
529 3 case ErrorCode::MissingHeader:
530 3 return "MissingHeader";
531 3 case ErrorCode::MalformedLine:
532 3 return "MalformedLine";
533 4 case ErrorCode::FileOpenFailed:
534 4 return "FileOpenFailed";
535 2 case ErrorCode::FileWriteFailed:
536 2 return "FileWriteFailed";
537 ✗ case ErrorCode::ManifestIdentityCollision:
538 ✗ return "ManifestIdentityCollision";
539 ✗ case ErrorCode::ManifestFramingUnsafe:
540 ✗ return "ManifestFramingUnsafe";
541 1 case ErrorCode::ProcessMismatch:
542 1 return "ProcessMismatch";
543 1 case ErrorCode::InstanceAlreadyRunning:
544 1 return "InstanceAlreadyRunning";
545 1 case ErrorCode::SessionAlreadyActive:
546 1 return "SessionAlreadyActive";
547 1 case ErrorCode::SystemCallFailed:
548 1 return "SystemCallFailed";
549 1 case ErrorCode::SessionShutdownInProgress:
550 1 return "SessionShutdownInProgress";
551 1 case ErrorCode::SessionShutdownUnavailable:
552 1 return "SessionShutdownUnavailable";
553 1 case ErrorCode::SessionShutdownWouldBlock:
554 1 return "SessionShutdownWouldBlock";
555 }
556 1 return "UnknownCode";
557 }
558
559 /**
560 * @brief One trivially copyable failure record: a code, a static label, and two raw context slots.
561 * @details Construction never allocates or throws, so noexcept paths can build an Error. Only message() allocates.
562 * The function that reports the error, or its ErrorCode, documents what `detail` and `extra` hold.
563 */
564 struct Error
565 {
566 /// The failure code. Its category names the subsystem that raised it.
567 ErrorCode code{ErrorCode::Ok};
568 /// Label of the site that raised the error, for example "scan". It must point at static storage.
569 const char *where{""};
570 /// Primary raw context, for example an address, an instruction pointer, or a failing-hop index.
571 std::uintptr_t detail{0};
572 /// Secondary raw context, for example a candidate index, a slot, a hop count, or an OS error.
573 std::uint32_t extra{0};
574
575 /** @brief Formats one greppable line: "[category] CodeName @ where (detail=0x..., extra=...)". */
576 [[nodiscard]] std::string message() const;
577 };
578
579 // Error construction stays non-throwing only while Error is trivially copyable.
580 static_assert(std::is_trivially_copyable_v<Error>, "Error must stay trivially copyable for the noexcept seed.");
581
582 /** @brief The fallible return type: a @p T on success, an Error on failure. Use `Result<void>` for no value. */
583 template <class T> using Result = std::expected<T, Error>;
584
585 6 inline std::string Error::message() const
586 {
587 // An empty or null label prints as "?".
588
2/4
✓ Branch 2 → 3 taken 6 times.
✗ Branch 2 → 5 not taken.
✓ Branch 3 → 4 taken 6 times.
✗ Branch 3 → 5 not taken.
6 const char *label = (where != nullptr && where[0] != '\0') ? where : "?";
589 return std::format(
590 "[{}] {} @ {} (detail=0x{:X}, extra={})",
591 6 to_string(category(code)),
592 ✗ to_string(code),
593 label,
594 6 detail,
595 6 extra
596
1/2
✓ Branch 9 → 10 taken 6 times.
✗ Branch 9 → 12 not taken.
6 );
597 }
598
599 } // namespace DetourModKit
600
601 /**
602 * @brief Unwraps a `Result<T>` into @p var, or returns its Error from the enclosing function.
603 * @details It expands to three statements, so it cannot be the body of a brace-less `if` or `for`. The enclosing
604 * function must return a `Result` or `std::expected`. Uses with distinct @p var in one scope do not collide.
605 */
606 #define DMK_TRY(var, expr) \
607 auto &&_r_##var = (expr); \
608 if (!_r_##var) \
609 return std::unexpected(_r_##var.error()); \
610 auto var = std::move(*_r_##var)
611
612 /**
613 * @brief Propagates the Error from a `Result<void>`, or from any Result whose value is discarded.
614 * @details It expands to one scoped statement, so it is safe as the body of a brace-less `if` or `for`, and nested uses
615 * do not collide. The enclosing function must return a `Result` or `std::expected`.
616 */
617 #define DMK_TRY_VOID(expr) \
618 do \
619 { \
620 auto &&_r = (expr); \
621 if (!_r) \
622 return std::unexpected(_r.error()); \
623 } while (0)
624
625 #endif // DETOURMODKIT_ERROR_HPP
626