GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 60.8% 127 / 0 / 209
Functions: 100.0% 4 / 0 / 4
Branches: 58.1% 61 / 0 / 105

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-bearing APIs use `Result<T>` and the propagation macros below. Best-effort query APIs retain their
8 * documented `bool`, `std::optional`, or `void` contracts. ErrorCode stores its subsystem category in the high
9 * byte so category recovery needs no lookup table.
10 */
11
12 #include "DetourModKit/defines.hpp"
13
14 #include <cstdint>
15 #include <expected>
16 #include <format>
17 #include <string>
18 #include <string_view>
19 #include <type_traits>
20 #include <utility>
21
22 namespace DetourModKit
23 {
24 /**
25 * @enum ErrorCategory
26 * @brief The subsystem an ErrorCode belongs to, recovered from the high byte of its value.
27 * @details Categories remain stable so persisted or logged numeric codes retain their meaning.
28 */
29 enum class ErrorCategory : std::uint8_t
30 {
31 /// Cross-cutting codes such as argument checks, allocation failures, and pattern errors.
32 General = 0x00,
33 /// Inline, mid-function, and VMT hooking.
34 Hook = 0x01,
35 /// AOB cascade, RIP-relative resolve, and string-xref resolution.
36 Scan = 0x02,
37 /// Guarded reads, writes, and protection changes.
38 Memory = 0x03,
39 /// Reverse-RTTI identification and self-heal.
40 Rtti = 0x04,
41 /// Manifest serialization and parsing.
42 Manifest = 0x05,
43 /// Session / bootstrap process lifecycle (start, single-instance gating, worker spawn).
44 Lifecycle = 0x06
45 };
46
47 /**
48 * @enum ErrorCode
49 * @brief The flat library-wide failure code, tagged by subsystem in its high byte.
50 * @details Each block is based at `category << 8`; the high byte names the @ref ErrorCategory and the low byte is
51 * the ordinal within that block. Only the high byte is stable: the low byte follows declaration order, so
52 * inserting an enumerator renumbers every later member of its block. Branch on the enumerator, never on
53 * its numeric value. A log format, wire protocol, or telemetry field that needs a fixed external
54 * representation owns a versioned enumerator-to-symbol mapping and writes that symbol; persisting the raw
55 * number silently remaps every later code in the block the next time one is inserted.
56 */
57 enum class ErrorCode : std::uint16_t
58 {
59 // General (0x00xx): cross-cutting failures.
60 /// Success sentinel; never stored in an Error that is actually surfaced as a failure.
61 Ok = 0x0000,
62 /// A factory/operation rejected its arguments (empty name, null target, empty ladder, ...).
63 InvalidArg,
64 /// An allocation failed; constructed without allocating so the noexcept batch seed can use it.
65 OutOfMemory,
66 /// An AOB pattern failed to parse or exceeded the inline-storage cap.
67 BadPattern,
68 /// A pointer-chain walk was handed a null root.
69 NullChain,
70 /// Last-resort code when no more specific one applies.
71 Unknown,
72
73 // Hook failures (0x01xx).
74 /// The hook backend allocator could not be obtained.
75 AllocatorNotAvailable = 0x0100,
76 /// The target address to hook was null or unusable.
77 InvalidTargetAddress,
78 /// The supplied detour function pointer was null or unusable.
79 InvalidDetourFunction,
80 /// The trampoline out-pointer was null.
81 InvalidTrampolinePointer,
82 /// A hook with that name is already registered.
83 HookAlreadyExists,
84 /// No hook with that name is registered.
85 HookNotFound,
86 /// The manager is tearing down and rejects new operations.
87 ShutdownInProgress,
88 /// The underlying hooking backend reported a failure.
89 BackendFailed,
90 /// Enabling (arming) the hook failed.
91 EnableFailed,
92 /// Disabling the hook failed.
93 DisableFailed,
94 /// The hook was in a state that does not permit the requested operation.
95 InvalidHookState,
96 /// The object (e.g. VMT instance) was null or invalid.
97 InvalidObject,
98 /// No VMT hook is registered for that object.
99 VmtHookNotFound,
100 /// That VMT slot/method is already hooked.
101 MethodAlreadyHooked,
102 /// That VMT slot/method is not hooked.
103 MethodNotFound,
104 /**
105 * @brief This linked DMK instance already holds the target, and the install asked to refuse a duplicate.
106 * @details Reported by the same-kit ledger, whose scope is one linked archive rather than the process. The
107 * record covers a hook that is created but not yet armed as well as an armed one, so the prologue is
108 * not necessarily patched. Drop the prior handle, or clear Options::fail_if_already_hooked to layer
109 * deliberately. A record a pin left behind (hook::Hook::release, or a teardown that could not restore)
110 * belongs to no handle and refuses every later strict install on that target for good. Records are
111 * keyed by address and a pinned one is never erased, so an address freed and reissued by the
112 * allocator can carry a record for a target this kit no longer holds. This code is also what a
113 * recorded target reports when a foreign module has since patched it: the record is found first and
114 * the prologue decode behind @ref TargetAlreadyHookedByAnotherModule never runs.
115 */
116 TargetAlreadyHookedByThisKit,
117 /**
118 * @brief The target's prologue already branches out of its own module, and the install asked to refuse.
119 * @details Reported by the foreign-JMP decode, which runs only when the same-kit ledger has no record. The
120 * decode refuses every branch whose destination does not resolve to the target's own module, which
121 * includes a destination in no loaded module at all: a detour parked in private trampoline memory is
122 * the usual shape. The responses differ from @ref TargetAlreadyHookedByThisKit: nothing this kit owns
123 * can be dropped, so the caller either coexists by layering or abandons the target.
124 */
125 TargetAlreadyHookedByAnotherModule,
126 /// A re-entrant call into the guarded path was rejected.
127 ReentrantCallRejected,
128 /// The target prologue could not be relocated safely.
129 TargetPrologueUnsafe,
130 /// An unclassified hook error: an unmapped backend failure, or a hook gate that could not be acquired.
131 UnknownError,
132 /**
133 * @brief The operation would have altered target bytes a newer layered hook on the same target still owns.
134 * @details Refused without changing anything. Tear down or disable the newer layer first.
135 */
136 LayerConflict,
137 /**
138 * @brief Every mid-hook adapter is in use; no further mid hook can be installed until one is destroyed.
139 * @details A mid hook needs one adapter from a fixed pool, because the backend's callback signature carries no
140 * user-data parameter and a distinct function is the only way to pass per-hook identity. Nothing was
141 * patched. Destroy a mid hook you no longer need, or hook fewer sites; inline and VMT hooks are
142 * unaffected.
143 */
144 MidHookCapacityExhausted,
145 /// The hook module refuses mutation under its loader-lock precondition.
146 LoaderLockActive,
147
148 // Scan (0x02xx): cascade resolve + read_code_constant + RIP resolve + string xref
149 /// No candidates were supplied to the cascade.
150 EmptyCandidates = 0x0200,
151 /// No cascade candidate matched the scanned scope.
152 NoMatch,
153 /// Every byte-candidate pattern failed to parse.
154 AllPatternsInvalid,
155 /// A Direct candidate existed, but none could be rebuilt safely as a hooked prologue.
156 PrologueFallbackNotApplicable,
157 /// The supplied module range was not a valid mapped image.
158 InvalidRange,
159 /// read_code_constant: the resolved site did not decode.
160 DecodeFailed,
161 /// read_code_constant: the operand was not the requested kind.
162 UnexpectedShape,
163 /// read_code_constant: the operand index was past the operand count.
164 OperandOutOfRange,
165 /// RIP resolve: the input pointer was null.
166 NullInput,
167 /// RIP resolve: the opcode prefix was not found in the search region.
168 PrefixNotFound,
169 /// RIP resolve: the search region was too small to hold the displacement.
170 RegionTooSmall,
171 /// RIP resolve: the displacement bytes could not be read.
172 UnreadableDisplacement,
173 /// RIP resolve: the resolved target was not a plausible address.
174 ImplausibleTarget,
175 /**
176 * @brief RIP resolve: the last matched prefix resolved plausibly to an unreadable target.
177 * @details `Error::detail` holds the last unreadable target address.
178 */
179 UnreadableTarget,
180 /// String xref: the query text was empty.
181 EmptyQuery,
182 /// String xref: the literal was not found in any readable page.
183 StringNotFound,
184 /// String xref: the literal occurs more than once.
185 StringAmbiguous,
186 /// String xref: no recognized RIP-relative reference resolves to it.
187 NoReference,
188 /// String xref: more than one instruction references it.
189 AmbiguousReference,
190 /// String xref: no prologue within the enclosing-function back-scan window.
191 FunctionNotFound,
192 /// String xref: no pointer-slot store of the loaded pointer follows the reference.
193 StoreNotFound,
194 /// Prologue recovery found a unique site, but identity confirmation rejected it or was missing.
195 PrologueIdentityRejected,
196 /// Export resolve: the module's export directory holds no name matching the requested export (or has none).
197 ExportNotFound,
198 /// Export resolve: the export is a forwarder to another module (a "Dll.Func" string, not code); fails closed.
199 ExportForwarded,
200 /**
201 * @brief A bounded-jump pattern spent its backtracking work budget, so the traversal stopped short.
202 * @details Distinct from NoMatch: the scan proved nothing about the unvisited positions. Add a literal byte to
203 * the pattern's leading segment, or narrow the scope, then retry.
204 */
205 BudgetExceeded,
206 /**
207 * @brief A page-gated sweep skipped a region that faulted mid-scan, so its occurrence count is a lower bound.
208 * @details Distinct from NoMatch: a match (or a duplicate that would have made the result ambiguous) may live
209 * in the skipped bytes. Caused by a concurrent decommit or reprotect of the scanned range.
210 */
211 IncompleteScan,
212 /**
213 * @brief The scan could not prove its result unique because query-owned storage may participate in it.
214 * @details Raised by a readable-page scan whose scope is not confined to one mapped image or one reserved
215 * allocation: DMK cannot discover caller-retained copies of the query bytes, so a match in that scope
216 * is not authoritative. Confine the scope, scan Pages::Executable, or supply those copies as
217 * exclusions. Also raised when more exclusion spans are declared than the bounded set holds after
218 * merging, which would leave some query storage visible to the sweep; declare fewer, or narrow the
219 * scope so fewer of them are in range.
220 */
221 NotAuthoritative,
222 /// String xref: the query text is not well-formed UTF-8, or it violates the embedded-NUL policy.
223 MalformedQueryText,
224 /**
225 * @brief Prologue recovery rebuilt a usable hook shape, but it matched more than one executable site.
226 * @details Distinct from NoMatch and PrologueFallbackNotApplicable: the rebuilt pattern collides at two or
227 * more sites, so no single redirected target can be trusted. Sharpen the signature's surviving tail.
228 */
229 PrologueFallbackAmbiguous,
230 /**
231 * @brief The selected byte rung no longer resolves the decoded site at the fresh epoch.
232 * @details The selector evidence is stale.
233 * Its physical span can fail to match. A bounded-gap result point can move.
234 * A wildcarded RIP locator can resolve elsewhere.
235 * The read fails closed because the decoded operand lacks valid evidence.
236 */
237 EvidenceMismatch,
238
239 // Memory (0x03xx): guarded memory and protection failures.
240 /// The write target address was null.
241 NullTargetAddress = 0x0300,
242 /// The source byte span was null.
243 NullSourceBytes,
244 /// The operation size exceeded the permitted bound.
245 SizeTooLarge,
246 /// Changing page protection failed.
247 ProtectionChangeFailed,
248 /// Restoring the original page protection failed.
249 ProtectionRestoreFailed,
250 /**
251 * A guarded read faulted. `Error::detail` holds an address inside the requested span that could not be read,
252 * so a span crossing into an unmapped or protected page names that page rather than the span start. It is the
253 * address the copy actually faulted on, which is the first unreadable byte for the small spans a typed read
254 * issues, but need not be for a span wide enough that the platform's `memcpy` touches bytes out of order. A
255 * span refused before any access (below @ref memory::USERSPACE_PTR_MIN, an end that wraps the address space,
256 * or an end past @ref memory::USERSPACE_PTR_MAX), and the MinGW fallback that validates through `VirtualQuery`
257 * instead of faulting, have no faulting byte and report the requested start instead. For @ref memory::walk the
258 * field is the failing hop index, not an address.
259 */
260 ReadFaulted,
261 /// A guarded in-place write faulted with no byte modified: the target was not writable. Error::detail holds it.
262 WriteFaulted,
263 /**
264 * A guarded write faulted after the copy may already have modified a prefix of the span (it reached a writable
265 * page, then faulted on an unwritable or unmapped byte further in). The changed prefix has an unknown length
266 * and can be empty: a fixed-width store retires as one instruction, so a store that straddles a writable page
267 * and an unwritable one faults with no byte changed and still reports this code. No byte outside the requested
268 * span was written. Treat the whole target as indeterminate. @ref WriteFaulted is the stronger result, because
269 * it guarantees that no byte changed. Error::detail holds the target address.
270 */
271 WriteMayBePartial,
272 /// A code patch wrote its bytes but the instruction-cache flush failed. Error::detail holds the target address.
273 InstructionFlushFailed,
274 /**
275 * A typed read encountered a byte pattern that is not a valid object representation of the requested type (for
276 * example a foreign byte other than 0 or 1 decoded through @ref memory::read_bool). No value was formed.
277 * Error::detail holds the source address.
278 */
279 InvalidRepresentation,
280 /**
281 * The caller-supplied buffer or source span intersects the target range. The copy primitives require the two
282 * half-open ranges to be disjoint and refuse an intersecting pair in either direction before any byte moves.
283 * Error::detail holds the target address.
284 */
285 OverlappingRanges,
286
287 // Rtti (0x04xx): reverse identification and healing failures.
288 /// The slot address was null or below the user-mode floor; no read was attempted.
289 BadSlotAddress = 0x0400,
290 /// The slot read faulted, or the qword held a null/low value.
291 UnreadableSlot,
292 /// The slot resolved to neither a pointer-to-object nor a direct object.
293 NoRtti,
294 /// The landmark/fingerprint descriptor is malformed; no memory was touched.
295 BadDescriptor,
296 /// No slot in the window resolved to the expected type.
297 HealNoMatch,
298 /// Equidistant slots both match, or fingerprint deltas tied.
299 HealAmbiguous,
300 /**
301 * A validity-bearing healed-offset slot was not @ref rtti::OffsetValidity::Confirmed for consumption: a
302 * required heal missed (the slot is Invalid) or an optional heal retained an unconfirmed nominal (Unverified).
303 * The value must not authorize a mutation. Consult @ref rtti::HealedSlot::load for the retained value and its
304 * validity.
305 */
306 OffsetNotConfirmed,
307
308 // Manifest failures (0x05xx).
309 /// The first non-blank line was not the manifest header.
310 MissingHeader = 0x0500,
311 /// A record line had the wrong field count or an unparseable field.
312 MalformedLine,
313 /// The file could not be opened (missing, locked, denied, or not a regular file).
314 FileOpenFailed,
315 /// The file opened but a subsequent write failed (disk full, an I/O error, or the stream went bad mid-write).
316 FileWriteFailed,
317 /**
318 * @brief Two section or key identities collide after case folding or exact/whitespace normalization.
319 * @details Fails the whole manifest before parsing or trust evaluation can observe an ambiguous contract.
320 */
321 ManifestIdentityCollision = 0x0504,
322 /**
323 * @brief A raw manifest frames a multi-line (heredoc) value unsafely: the block is never closed, its opener
324 * carries an empty tag, or its first body line is its own terminator.
325 * @details Each shape reads differently in the INI backend than any safe model of it, so the value (and every
326 * section below it) could silently change identity. Checked serialization reports unsafe source
327 * values as InvalidArg before emitting them.
328 */
329 ManifestFramingUnsafe,
330
331 // Lifecycle (0x06xx): Session / bootstrap process lifecycle
332 /// The running executable did not match ModInfo::game_process_name; the session declined to load (not a fault).
333 ProcessMismatch = 0x0600,
334 /// The single-instance mutex was already held: another load of this mod is already live in the process.
335 InstanceAlreadyRunning,
336 /// start()/bootstrap() was called while a Session is already active in this process (a caller sequencing bug).
337 SessionAlreadyActive,
338 /// A Win32 lifecycle operation failed; Error::detail = GetLastError().
339 SystemCallFailed,
340 /// A bootstrap lifecycle operation raced a concurrent attach, a drain, or the previous generation's retirement.
341 SessionShutdownInProgress,
342 /// Loader detach already claimed the bootstrap state, so a synchronous drain can no longer be guaranteed.
343 SessionShutdownUnavailable,
344 /**
345 * @brief A synchronous bootstrap drain was refused because waiting would block.
346 * @details Either the calling thread may hold the Windows loader lock, or it is the bootstrap worker itself,
347 * whose exit the drain would otherwise wait for.
348 */
349 SessionShutdownWouldBlock
350 };
351
352 /**
353 * @brief Recovers the subsystem category of an ErrorCode from its high byte.
354 * @param code The error code.
355 * @return The ErrorCategory the code belongs to.
356 * @details Pure shift, no lookup table. Every enumerator is based at `category << 8`, so the high byte IS the
357 * category and cannot drift out of sync with the codes.
358 */
359 10 [[nodiscard]] constexpr ErrorCategory category(ErrorCode code) noexcept
360 {
361 10 return static_cast<ErrorCategory>((static_cast<std::uint16_t>(code) >> 8) & 0xFFU);
362 }
363
364 /**
365 * @brief Returns a short human-readable label for a subsystem category.
366 * @param value The category.
367 * @return A static string view; "unknown" for an out-of-range value.
368 */
369 6 [[nodiscard]] constexpr std::string_view to_string(ErrorCategory value) noexcept
370 {
371
4/8
✓ Branch 2 → 3 taken 2 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.
6 switch (value)
372 {
373 2 case ErrorCategory::General:
374 2 return "general";
375 case ErrorCategory::Hook:
376 return "hook";
377 2 case ErrorCategory::Scan:
378 2 return "scan";
379 case ErrorCategory::Memory:
380 return "memory";
381 case ErrorCategory::Rtti:
382 return "rtti";
383 1 case ErrorCategory::Manifest:
384 1 return "manifest";
385 1 case ErrorCategory::Lifecycle:
386 1 return "lifecycle";
387 }
388 return "unknown";
389 }
390
391 /**
392 * @brief Returns the enumerator name for an ErrorCode.
393 * @param code The error code.
394 * @return A static string view naming the code; "UnknownCode" for an out-of-range value.
395 * @details Every named enumerator is listed, so `-Wswitch` flags a future code added without a label here.
396 */
397 80 [[nodiscard]] constexpr std::string_view to_string(ErrorCode code) noexcept
398 {
399
54/91
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1 time.
✓ Branch 2 → 5 taken 1 time.
✓ Branch 2 → 6 taken 2 times.
✗ Branch 2 → 7 not taken.
✗ Branch 2 → 8 not taken.
✗ 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 taken 1 time.
✓ Branch 2 → 33 taken 2 times.
✗ Branch 2 → 34 not taken.
✓ Branch 2 → 35 taken 1 time.
✓ Branch 2 → 36 taken 2 times.
✓ Branch 2 → 37 taken 2 times.
✓ Branch 2 → 38 taken 1 time.
✓ Branch 2 → 39 taken 1 time.
✓ Branch 2 → 40 taken 1 time.
✓ Branch 2 → 41 taken 2 times.
✓ Branch 2 → 42 taken 2 times.
✓ Branch 2 → 43 taken 2 times.
✓ Branch 2 → 44 taken 2 times.
✓ Branch 2 → 45 taken 1 time.
✓ Branch 2 → 46 taken 2 times.
✓ Branch 2 → 47 taken 1 time.
✓ 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 not taken.
✓ Branch 2 → 55 taken 1 time.
✓ Branch 2 → 56 taken 1 time.
✓ Branch 2 → 57 taken 2 times.
✓ Branch 2 → 58 taken 2 times.
✓ Branch 2 → 59 taken 1 time.
✓ 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 not taken.
✗ 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 taken 6 times.
✗ Branch 2 → 78 not taken.
✓ Branch 2 → 79 taken 3 times.
✓ Branch 2 → 80 taken 3 times.
✓ Branch 2 → 81 taken 3 times.
✓ Branch 2 → 82 taken 4 times.
✓ Branch 2 → 83 taken 2 times.
✗ Branch 2 → 84 not taken.
✗ Branch 2 → 85 not taken.
✓ Branch 2 → 86 taken 1 time.
✓ 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.
80 switch (code)
400 {
401 case ErrorCode::Ok:
402 return "Ok";
403 1 case ErrorCode::InvalidArg:
404 1 return "InvalidArg";
405 1 case ErrorCode::OutOfMemory:
406 1 return "OutOfMemory";
407 2 case ErrorCode::BadPattern:
408 2 return "BadPattern";
409 case ErrorCode::NullChain:
410 return "NullChain";
411 case ErrorCode::Unknown:
412 return "Unknown";
413 case ErrorCode::AllocatorNotAvailable:
414 return "AllocatorNotAvailable";
415 case ErrorCode::InvalidTargetAddress:
416 return "InvalidTargetAddress";
417 case ErrorCode::InvalidDetourFunction:
418 return "InvalidDetourFunction";
419 case ErrorCode::InvalidTrampolinePointer:
420 return "InvalidTrampolinePointer";
421 case ErrorCode::HookAlreadyExists:
422 return "HookAlreadyExists";
423 case ErrorCode::HookNotFound:
424 return "HookNotFound";
425 case ErrorCode::ShutdownInProgress:
426 return "ShutdownInProgress";
427 1 case ErrorCode::BackendFailed:
428 1 return "BackendFailed";
429 case ErrorCode::EnableFailed:
430 return "EnableFailed";
431 case ErrorCode::DisableFailed:
432 return "DisableFailed";
433 case ErrorCode::InvalidHookState:
434 return "InvalidHookState";
435 case ErrorCode::InvalidObject:
436 return "InvalidObject";
437 case ErrorCode::VmtHookNotFound:
438 return "VmtHookNotFound";
439 case ErrorCode::MethodAlreadyHooked:
440 return "MethodAlreadyHooked";
441 case ErrorCode::MethodNotFound:
442 return "MethodNotFound";
443 1 case ErrorCode::TargetAlreadyHookedByThisKit:
444 1 return "TargetAlreadyHookedByThisKit";
445 1 case ErrorCode::TargetAlreadyHookedByAnotherModule:
446 1 return "TargetAlreadyHookedByAnotherModule";
447 case ErrorCode::ReentrantCallRejected:
448 return "ReentrantCallRejected";
449 case ErrorCode::TargetPrologueUnsafe:
450 return "TargetPrologueUnsafe";
451 case ErrorCode::UnknownError:
452 return "UnknownError";
453 case ErrorCode::LayerConflict:
454 return "LayerConflict";
455 case ErrorCode::MidHookCapacityExhausted:
456 return "MidHookCapacityExhausted";
457 case ErrorCode::LoaderLockActive:
458 return "LoaderLockActive";
459 1 case ErrorCode::EmptyCandidates:
460 1 return "EmptyCandidates";
461 2 case ErrorCode::NoMatch:
462 2 return "NoMatch";
463 case ErrorCode::AllPatternsInvalid:
464 return "AllPatternsInvalid";
465 1 case ErrorCode::PrologueFallbackNotApplicable:
466 1 return "PrologueFallbackNotApplicable";
467 2 case ErrorCode::PrologueFallbackAmbiguous:
468 2 return "PrologueFallbackAmbiguous";
469 2 case ErrorCode::InvalidRange:
470 2 return "InvalidRange";
471 1 case ErrorCode::DecodeFailed:
472 1 return "DecodeFailed";
473 1 case ErrorCode::UnexpectedShape:
474 1 return "UnexpectedShape";
475 1 case ErrorCode::OperandOutOfRange:
476 1 return "OperandOutOfRange";
477 2 case ErrorCode::NullInput:
478 2 return "NullInput";
479 2 case ErrorCode::PrefixNotFound:
480 2 return "PrefixNotFound";
481 2 case ErrorCode::RegionTooSmall:
482 2 return "RegionTooSmall";
483 2 case ErrorCode::UnreadableDisplacement:
484 2 return "UnreadableDisplacement";
485 1 case ErrorCode::ImplausibleTarget:
486 1 return "ImplausibleTarget";
487 2 case ErrorCode::UnreadableTarget:
488 2 return "UnreadableTarget";
489 1 case ErrorCode::EmptyQuery:
490 1 return "EmptyQuery";
491 1 case ErrorCode::StringNotFound:
492 1 return "StringNotFound";
493 1 case ErrorCode::StringAmbiguous:
494 1 return "StringAmbiguous";
495 1 case ErrorCode::NoReference:
496 1 return "NoReference";
497 1 case ErrorCode::AmbiguousReference:
498 1 return "AmbiguousReference";
499 1 case ErrorCode::FunctionNotFound:
500 1 return "FunctionNotFound";
501 1 case ErrorCode::StoreNotFound:
502 1 return "StoreNotFound";
503 case ErrorCode::PrologueIdentityRejected:
504 return "PrologueIdentityRejected";
505 1 case ErrorCode::ExportNotFound:
506 1 return "ExportNotFound";
507 1 case ErrorCode::ExportForwarded:
508 1 return "ExportForwarded";
509 2 case ErrorCode::BudgetExceeded:
510 2 return "BudgetExceeded";
511 2 case ErrorCode::IncompleteScan:
512 2 return "IncompleteScan";
513 1 case ErrorCode::NotAuthoritative:
514 1 return "NotAuthoritative";
515 1 case ErrorCode::MalformedQueryText:
516 1 return "MalformedQueryText";
517 1 case ErrorCode::EvidenceMismatch:
518 1 return "EvidenceMismatch";
519 1 case ErrorCode::NullTargetAddress:
520 1 return "NullTargetAddress";
521 1 case ErrorCode::NullSourceBytes:
522 1 return "NullSourceBytes";
523 1 case ErrorCode::SizeTooLarge:
524 1 return "SizeTooLarge";
525 1 case ErrorCode::ProtectionChangeFailed:
526 1 return "ProtectionChangeFailed";
527 1 case ErrorCode::ProtectionRestoreFailed:
528 1 return "ProtectionRestoreFailed";
529 1 case ErrorCode::ReadFaulted:
530 1 return "ReadFaulted";
531 case ErrorCode::WriteFaulted:
532 return "WriteFaulted";
533 case ErrorCode::WriteMayBePartial:
534 return "WriteMayBePartial";
535 case ErrorCode::InstructionFlushFailed:
536 return "InstructionFlushFailed";
537 case ErrorCode::InvalidRepresentation:
538 return "InvalidRepresentation";
539 case ErrorCode::OverlappingRanges:
540 return "OverlappingRanges";
541 case ErrorCode::BadSlotAddress:
542 return "BadSlotAddress";
543 case ErrorCode::UnreadableSlot:
544 return "UnreadableSlot";
545 case ErrorCode::NoRtti:
546 return "NoRtti";
547 case ErrorCode::BadDescriptor:
548 return "BadDescriptor";
549 6 case ErrorCode::HealNoMatch:
550 6 return "HealNoMatch";
551 case ErrorCode::HealAmbiguous:
552 return "HealAmbiguous";
553 3 case ErrorCode::OffsetNotConfirmed:
554 3 return "OffsetNotConfirmed";
555 3 case ErrorCode::MissingHeader:
556 3 return "MissingHeader";
557 3 case ErrorCode::MalformedLine:
558 3 return "MalformedLine";
559 4 case ErrorCode::FileOpenFailed:
560 4 return "FileOpenFailed";
561 2 case ErrorCode::FileWriteFailed:
562 2 return "FileWriteFailed";
563 case ErrorCode::ManifestIdentityCollision:
564 return "ManifestIdentityCollision";
565 case ErrorCode::ManifestFramingUnsafe:
566 return "ManifestFramingUnsafe";
567 1 case ErrorCode::ProcessMismatch:
568 1 return "ProcessMismatch";
569 1 case ErrorCode::InstanceAlreadyRunning:
570 1 return "InstanceAlreadyRunning";
571 1 case ErrorCode::SessionAlreadyActive:
572 1 return "SessionAlreadyActive";
573 1 case ErrorCode::SystemCallFailed:
574 1 return "SystemCallFailed";
575 1 case ErrorCode::SessionShutdownInProgress:
576 1 return "SessionShutdownInProgress";
577 1 case ErrorCode::SessionShutdownUnavailable:
578 1 return "SessionShutdownUnavailable";
579 1 case ErrorCode::SessionShutdownWouldBlock:
580 1 return "SessionShutdownWouldBlock";
581 }
582 1 return "UnknownCode";
583 }
584
585 /**
586 * @struct Error
587 * @brief One trivially copyable failure record: a code plus a static label and two raw context slots.
588 * @details Construction never allocates (it is a plain aggregate of a code, a pointer, and two integers), so an
589 * Error can be built on the noexcept batch/seed paths where throwing would terminate. Only message()
590 * allocates. `where` is a `const char *` by strong convention pointing at a static/literal label (e.g.
591 * "scan", "hook::inline"); that documents intent and keeps the common case dangle-free, but it is a
592 * convention the type cannot enforce. A caller can still hand it a pointer into a transient buffer, so
593 * callers must pass only static storage. The two raw slots carry whatever context the raising code
594 * documents for that ErrorCode (an address, a failing-hop index, a candidate ordinal, ...).
595 */
596 struct Error
597 {
598 /// The failure code; its category names the raising subsystem.
599 ErrorCode code{ErrorCode::Ok};
600 /// Static/literal label for the raising site, e.g. "scan" or "hook::inline".
601 const char *where{""};
602 /// Primary raw context: address / instruction pointer / failing-hop index, per the code's documentation.
603 std::uintptr_t detail{0};
604 /// Secondary raw context: candidate index / slot / hop count, per the code's documentation.
605 std::uint32_t extra{0};
606
607 /**
608 * @brief Composes a single greppable diagnostic line for this error.
609 * @return A formatted string: "[category] CodeName @ where (detail=0x..., extra=...)".
610 * @details The ONLY allocating member. It is never called on a hot path: the noexcept batch paths pre-seed
611 * their result vectors with Errors and only ever format them later, off the critical section.
612 */
613 [[nodiscard]] std::string message() const;
614 };
615
616 // The non-allocating-construction guarantee that the noexcept batch seed relies on is only true while Error stays
617 // trivially copyable (a code, a pointer, two integers). Pin it so a future field with a non-trivial type cannot
618 // silently make Error construction able to throw.
619 static_assert(std::is_trivially_copyable_v<Error>, "Error must stay trivially copyable for the noexcept seed.");
620
621 /**
622 * @brief The single fallible-return alias: a value of type @p T on success, an Error on failure.
623 * @tparam T The success type; use `Result<void>` for an operation that returns no value.
624 */
625 template <class T> using Result = std::expected<T, Error>;
626
627 3 inline std::string Error::message() const
628 {
629 // Keep the distinctive code name whole and lead with the subsystem so the line stays both human-readable and
630 // greppable by category. An empty/absent label is rendered as "?" rather than a blank gap.
631
2/4
✓ Branch 2 → 3 taken 3 times.
✗ Branch 2 → 5 not taken.
✓ Branch 3 → 4 taken 3 times.
✗ Branch 3 → 5 not taken.
3 const char *label = (where != nullptr && where[0] != '\0') ? where : "?";
632 return std::format(
633 "[{}] {} @ {} (detail=0x{:X}, extra={})",
634 3 to_string(category(code)),
635 to_string(code),
636 label,
637 3 detail,
638 3 extra
639
1/2
✓ Branch 9 → 10 taken 3 times.
✗ Branch 9 → 12 not taken.
3 );
640 }
641
642 } // namespace DetourModKit
643
644 /**
645 * @def DMK_TRY
646 * @brief Unwraps a `Result<T>` into @p var, or returns the propagated Error from the enclosing function.
647 * @details The value-binding propagation form. It expands to three statements (bind the result, short-circuit on
648 * failure, move the value out), so it must live in a braced block. It cannot be the sole controlled
649 * statement of a brace-less `if`/`for`. The enclosing function must itself return a `Result`/`std::expected`
650 * so the `std::unexpected(...)` early-return is well-formed. The temporary is named after @p var, so several
651 * DMK_TRY uses in one scope never collide.
652 */
653 #define DMK_TRY(var, expr) \
654 auto &&_r_##var = (expr); \
655 if (!_r_##var) \
656 return std::unexpected(_r_##var.error()); \
657 auto var = std::move(*_r_##var)
658
659 /**
660 * @def DMK_TRY_VOID
661 * @brief Propagates the Error from a `Result<void>` (or any Result whose value is discarded), binding nothing.
662 * @details The void form has no value to bind, so it wraps its temporary in a `do { ... } while (0)` block. That
663 * gives it a fresh scope (so nested uses never collide on the temporary name) and makes the whole macro a
664 * single statement that IS safe as the controlled statement of a brace-less `if`/`for`. As with DMK_TRY, the
665 * enclosing function must return a `Result`/`std::expected`.
666 */
667 #define DMK_TRY_VOID(expr) \
668 do \
669 { \
670 auto &&_r = (expr); \
671 if (!_r) \
672 return std::unexpected(_r.error()); \
673 } while (0)
674
675 #endif // DETOURMODKIT_ERROR_HPP
676