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 |