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 | 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 | 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 | 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 | 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 |