include/DetourModKit/detail/pattern_core.hpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #ifndef DETOURMODKIT_DETAIL_PATTERN_CORE_HPP | ||
| 2 | #define DETOURMODKIT_DETAIL_PATTERN_CORE_HPP | ||
| 3 | |||
| 4 | /** | ||
| 5 | * @file pattern_core.hpp | ||
| 6 | * @brief Logger-free, heap-free constexpr core that parses the AOB mini-DSL and selects a rarest-byte anchor. | ||
| 7 | * @details The public scan::Pattern type needs to parse the same "48 8B ?? E8 ? ? ? ?" DSL in two very different | ||
| 8 | * contexts: scan::Pattern::compile() at run time (from a configuration string, returning a Result) and | ||
| 9 | * scan::Pattern::literal() at COMPILE time (from an in-source string literal, where a typo must be a build | ||
| 10 | * error). A single parser cannot serve both if it touches the heap (a consteval result may not own | ||
| 11 | * non-transient heap storage) or the logging singleton (not constexpr-callable). This core is therefore a | ||
| 12 | * pure constexpr function that writes into fixed-size arrays and reports failure through a status enum, with | ||
| 13 | * no std::vector and no Logger anywhere in its body. Both Pattern entry points call it and only differ in how | ||
| 14 | * they react to a non-Ok status (compile() maps it to an Error, literal() turns it into a hard compile error). | ||
| 15 | * | ||
| 16 | * The byte/mask encoding, the match semantics, and the rarest-byte anchor heuristic are shared with the | ||
| 17 | * heap-backed runtime scan engine so every parser entry point resolves the same DSL identically; the engine's | ||
| 18 | * runtime haystack-frequency selection can override this compile-time anchor, which serves as the fallback | ||
| 19 | * when no haystack histogram is available. | ||
| 20 | * @note Unlike the other detail/ headers, both axes line up here: scan.hpp needs this implementation support at | ||
| 21 | * compile time, so it sits in the detail/ directory and declares its types in the ::detail namespace. Directory | ||
| 22 | * placement and namespace placement are independent. | ||
| 23 | */ | ||
| 24 | |||
| 25 | #include <array> | ||
| 26 | #include <cstddef> | ||
| 27 | #include <cstdint> | ||
| 28 | #include <span> | ||
| 29 | #include <string_view> | ||
| 30 | |||
| 31 | namespace DetourModKit::detail | ||
| 32 | { | ||
| 33 | /** | ||
| 34 | * @brief Inline-storage cap for a compiled pattern, in bytes. | ||
| 35 | * @details The cap is baked into the std::array member type so that a compiled Pattern is a literal type a | ||
| 36 | * consteval result can return by value (a value owning heap storage cannot be returned from consteval). | ||
| 37 | * Game AOB signatures are short; 128 covers every shipped consumer literal with wide headroom and also | ||
| 38 | * admits the longest exercised runtime patterns, since literal() and compile() share this one storage. | ||
| 39 | */ | ||
| 40 | inline constexpr std::size_t MAX_PATTERN_BYTES = 128; | ||
| 41 | |||
| 42 | /** | ||
| 43 | * @brief Anchor sentinel meaning "no fully-known byte exists to anchor on". | ||
| 44 | * @details Set to the cap so it is always one past any valid index. An all-wildcard or nibble-only pattern has no | ||
| 45 | * full byte the prefilter can memchr for and resolves through a masked compare at every position instead. | ||
| 46 | */ | ||
| 47 | inline constexpr std::size_t NO_ANCHOR = MAX_PATTERN_BYTES; | ||
| 48 | |||
| 49 | /** | ||
| 50 | * @brief Maximum number of bounded-jump gaps a single pattern may carry. | ||
| 51 | * @details A bounded jump (`[X-Y]`) splits the fixed byte stream into segments; each jump records one gap between | ||
| 52 | * two fixed runs. `PatternBuffer::jumps` is a fixed `std::array` sized to this cap, and every value | ||
| 53 | * Pattern (and every Candidate that holds one) carries it inline, so the cap is kept to a small handful of | ||
| 54 | * gaps: a real signature anchors on a few stable points and rarely needs more than one or two gaps. A | ||
| 55 | * pattern that names more gaps fails closed at parse with TooManyJumps rather than silently truncating. | ||
| 56 | */ | ||
| 57 | inline constexpr std::size_t MAX_PATTERN_JUMPS = 8; | ||
| 58 | |||
| 59 | /** | ||
| 60 | * @brief Upper bound on a single jump gap's maximum skip, in bytes. | ||
| 61 | * @details A jump range is deliberately bounded (this is a bounded-jump dialect, not YARA's unbounded `[X-]`): the | ||
| 62 | * gap consumes match-window bytes and each extra byte of span multiplies the backtracking matcher's work, | ||
| 63 | * so an unbounded gap could turn a scan into a region-wide sweep. Capping the span bounds each gap's | ||
| 64 | * contribution to that work (see try_segments_at for the full cost profile). A gap whose upper bound | ||
| 65 | * exceeds this is rejected at parse. | ||
| 66 | */ | ||
| 67 | inline constexpr std::size_t MAX_JUMP_SPAN = 256; | ||
| 68 | |||
| 69 | /** | ||
| 70 | * @brief Per-start-position ceiling on bounded-jump backtracking node visits. | ||
| 71 | * @details The segmented matcher is deliberately simple and unmemoized: on a miss it may try every skip value of | ||
| 72 | * every gap, so a pathological all-wildcard, wide-gap pattern can approach the product of the gap spans at | ||
| 73 | * one start position. This budget converts that per-position product into a fixed ceiling while preserving | ||
| 74 | * the region-level linear sweep. Exhausting the budget fails the current placement closed (no match) | ||
| 75 | * rather than hanging. The assertion keeps the budget above the linear per-position work of a well-formed | ||
| 76 | * pattern, so ordinary literal-anchored signatures finish before the cap. | ||
| 77 | */ | ||
| 78 | inline constexpr std::size_t SEGMENT_MATCH_STEP_BUDGET = 1u << 16; | ||
| 79 | static_assert( | ||
| 80 | SEGMENT_MATCH_STEP_BUDGET >= MAX_PATTERN_JUMPS * MAX_JUMP_SPAN, | ||
| 81 | "The per-position work budget must exceed the linear per-position cost of a well-formed pattern." | ||
| 82 | ); | ||
| 83 | |||
| 84 | /** | ||
| 85 | * @struct PatternJump | ||
| 86 | * @brief One bounded gap between two fixed byte runs (segments) of a compiled pattern. | ||
| 87 | * @details A jump lets a pattern tolerate a variable-length span between two stable anchors (an instruction whose | ||
| 88 | * encoding size shifts when the compiler's output moves), which a fixed run of wildcards cannot: | ||
| 89 | * `?? ?? ??` matches exactly three bytes, while `[2-5]` matches any two-to-five-byte gap. @ref position is | ||
| 90 | * the index in the concatenated fixed byte stream that the gap precedes (strictly inside `(0, length)`, | ||
| 91 | * since a jump can neither lead nor trail the pattern nor sit adjacent to another jump). @ref min_skip / | ||
| 92 | * @ref max_skip bound the gap; `min_skip == max_skip` is an exact `[N]` jump. | ||
| 93 | */ | ||
| 94 | struct PatternJump | ||
| 95 | { | ||
| 96 | /// Index in the fixed byte stream the gap precedes; the boundary between segment i and segment i+1. | ||
| 97 | std::size_t position{0}; | ||
| 98 | /// Fewest bytes the gap may skip before the following segment. | ||
| 99 | std::size_t min_skip{0}; | ||
| 100 | /// Most bytes the gap may skip before the following segment; >= min_skip and <= MAX_JUMP_SPAN. | ||
| 101 | std::size_t max_skip{0}; | ||
| 102 | }; | ||
| 103 | |||
| 104 | /** | ||
| 105 | * @enum PatternStatus | ||
| 106 | * @brief Outcome of parsing an AOB DSL string in the constexpr core. | ||
| 107 | */ | ||
| 108 | enum class PatternStatus : std::uint8_t | ||
| 109 | { | ||
| 110 | /// Parsed successfully into at least one byte. | ||
| 111 | Ok, | ||
| 112 | /// The input held no byte tokens (empty, whitespace-only, or only an offset marker). | ||
| 113 | Empty, | ||
| 114 | /// A token was not a recognized DSL form. | ||
| 115 | InvalidToken, | ||
| 116 | /// The pattern exceeded MAX_PATTERN_BYTES byte tokens. | ||
| 117 | TooLong, | ||
| 118 | /// More than one offset marker was present. | ||
| 119 | DuplicateOffset, | ||
| 120 | /// A `[...]` jump token was malformed, out of range, or illegally placed (leading, trailing, or adjacent). | ||
| 121 | InvalidJump, | ||
| 122 | /// The pattern named more bounded jumps than MAX_PATTERN_JUMPS. | ||
| 123 | TooManyJumps | ||
| 124 | }; | ||
| 125 | |||
| 126 | /** | ||
| 127 | * @struct PatternBuffer | ||
| 128 | * @brief The compiled byte/mask representation plus the offset marker, bounded-jump gaps, and the selected anchor. | ||
| 129 | * @details A literal-type aggregate (no heap): @ref bytes and @ref mask are fixed arrays sized to the cap, and | ||
| 130 | * only the first @ref length entries are meaningful. @ref offset is the "point of interest" the optional | ||
| 131 | * `|` marker records (0 when absent, which coincides with the match start). @ref anchor is the index of | ||
| 132 | * the rarest fully-known byte, or @ref NO_ANCHOR when none exists. | ||
| 133 | * | ||
| 134 | * A pattern with bounded jumps splits its fixed byte stream into segments. @ref bytes / @ref mask hold the | ||
| 135 | * segments concatenated with no gap bytes; @ref jumps records where a gap sits and how wide it may be. A | ||
| 136 | * jump-free pattern has @ref jump_count == 0 and takes the same single fixed-width match path. The anchor | ||
| 137 | * is deliberately confined to segment 0 (the bytes before the first jump), because the matcher locates | ||
| 138 | * that first fixed run and then extends across the variable gaps: a byte in a later segment sits at an | ||
| 139 | * address that shifts with the gap, so it cannot drive the memchr prefilter. | ||
| 140 | */ | ||
| 141 | struct PatternBuffer | ||
| 142 | { | ||
| 143 | /// Pattern byte values; only entries [0, length) are valid. | ||
| 144 | std::array<std::byte, MAX_PATTERN_BYTES> bytes{}; | ||
| 145 | /// Per-byte match mask (0xFF literal, 0x00 wildcard, 0xF0 high nibble, 0x0F low nibble). | ||
| 146 | std::array<std::byte, MAX_PATTERN_BYTES> mask{}; | ||
| 147 | /// Number of valid byte entries (all segments concatenated, gaps excluded). | ||
| 148 | std::size_t length{0}; | ||
| 149 | /// Result offset recorded by the `|` marker; 0 when no marker is present. | ||
| 150 | std::size_t offset{0}; | ||
| 151 | /// Index of the rarest fully-known byte in segment 0, or NO_ANCHOR when segment 0 has no full byte. | ||
| 152 | std::size_t anchor{NO_ANCHOR}; | ||
| 153 | /// Bounded-jump gaps in ascending position order; only entries [0, jump_count) are valid. | ||
| 154 | std::array<PatternJump, MAX_PATTERN_JUMPS> jumps{}; | ||
| 155 | /// Number of valid jump gaps; 0 for a plain (single-segment) pattern. | ||
| 156 | std::size_t jump_count{0}; | ||
| 157 | }; | ||
| 158 | |||
| 159 | /** | ||
| 160 | * @struct PatternParse | ||
| 161 | * @brief A parse status paired with the buffer it produced (valid only when status == Ok). | ||
| 162 | */ | ||
| 163 | struct PatternParse | ||
| 164 | { | ||
| 165 | /// Parse outcome. | ||
| 166 | PatternStatus status{PatternStatus::Empty}; | ||
| 167 | /// The compiled representation; meaningful only when @ref status is Ok. | ||
| 168 | PatternBuffer buffer{}; | ||
| 169 | }; | ||
| 170 | |||
| 171 | /// Maps a hex digit to its value 0-15, or -1 if @p ch is not a hex digit. | ||
| 172 | 39784 | [[nodiscard]] constexpr int hex_digit(char ch) noexcept | |
| 173 | { | ||
| 174 |
3/4✓ Branch 2 → 3 taken 39784 times.
✗ Branch 2 → 5 not taken.
✓ Branch 3 → 4 taken 34047 times.
✓ Branch 3 → 5 taken 5737 times.
|
39784 | if (ch >= '0' && ch <= '9') |
| 175 | { | ||
| 176 | 34047 | return ch - '0'; | |
| 177 | } | ||
| 178 |
3/4✓ Branch 5 → 6 taken 3 times.
✓ Branch 5 → 8 taken 5734 times.
✓ Branch 6 → 7 taken 3 times.
✗ Branch 6 → 8 not taken.
|
5737 | if (ch >= 'a' && ch <= 'f') |
| 179 | { | ||
| 180 | 3 | return ch - 'a' + 10; | |
| 181 | } | ||
| 182 |
4/4✓ Branch 8 → 9 taken 5715 times.
✓ Branch 8 → 11 taken 19 times.
✓ Branch 9 → 10 taken 5699 times.
✓ Branch 9 → 11 taken 16 times.
|
5734 | if (ch >= 'A' && ch <= 'F') |
| 183 | { | ||
| 184 | 5699 | return ch - 'A' + 10; | |
| 185 | } | ||
| 186 | 35 | return -1; | |
| 187 | } | ||
| 188 | |||
| 189 | /// True for the token separators the DSL splits on (space, tab, CR, LF, form feed, vertical tab). | ||
| 190 | 102331 | [[nodiscard]] constexpr bool is_token_space(char ch) noexcept | |
| 191 | { | ||
| 192 |
9/12✓ Branch 2 → 3 taken 62235 times.
✓ Branch 2 → 8 taken 40096 times.
✓ Branch 3 → 4 taken 62227 times.
✓ Branch 3 → 8 taken 8 times.
✓ Branch 4 → 5 taken 62227 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 62226 times.
✓ Branch 5 → 8 taken 1 time.
✓ Branch 6 → 7 taken 62226 times.
✗ Branch 6 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 62227 times.
|
102331 | return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == '\f' || ch == '\v'; |
| 193 | } | ||
| 194 | |||
| 195 | /** | ||
| 196 | * @brief Scores how common a byte is in typical x64 .text; lower means rarer and a better scan anchor. | ||
| 197 | * @param value The fully-known byte to score. | ||
| 198 | * @return A frequency class where 0 is the rarest (any byte outside the common-opcode table). | ||
| 199 | * @details Reproduces the runtime engine's table verbatim so the compile-time anchor matches the byte the engine | ||
| 200 | * would have chosen. The listed bytes are the usual high-frequency suspects (padding, INT3/NOP fill, REX | ||
| 201 | * prefixes, common MOV/two-byte-opcode/branch leads); anything else is treated as rare and preferred. | ||
| 202 | */ | ||
| 203 | 2561 | [[nodiscard]] constexpr std::uint8_t byte_frequency_class(std::uint8_t value) noexcept | |
| 204 | { | ||
| 205 |
11/13✓ Branch 2 → 3 taken 133 times.
✓ Branch 2 → 4 taken 11 times.
✓ Branch 2 → 5 taken 94 times.
✓ Branch 2 → 6 taken 208 times.
✓ Branch 2 → 7 taken 228 times.
✓ Branch 2 → 8 taken 127 times.
✓ Branch 2 → 9 taken 4 times.
✓ Branch 2 → 10 taken 4 times.
✗ Branch 2 → 11 not taken.
✓ Branch 2 → 12 taken 40 times.
✗ Branch 2 → 13 not taken.
✓ Branch 2 → 14 taken 2 times.
✓ Branch 2 → 15 taken 1710 times.
|
2561 | switch (value) |
| 206 | { | ||
| 207 | 133 | case 0x00: | |
| 208 | 133 | return 10; | |
| 209 | 11 | case 0xCC: | |
| 210 | 11 | return 9; | |
| 211 | 94 | case 0x90: | |
| 212 | 94 | return 9; | |
| 213 | 208 | case 0xFF: | |
| 214 | 208 | return 8; | |
| 215 | 228 | case 0x48: | |
| 216 | 228 | return 8; | |
| 217 | 127 | case 0x8B: | |
| 218 | 127 | return 7; | |
| 219 | 4 | case 0x89: | |
| 220 | 4 | return 7; | |
| 221 | 4 | case 0x0F: | |
| 222 | 4 | return 7; | |
| 223 | ✗ | case 0xE8: | |
| 224 | ✗ | return 6; | |
| 225 | 40 | case 0xE9: | |
| 226 | 40 | return 6; | |
| 227 | ✗ | case 0x83: | |
| 228 | ✗ | return 6; | |
| 229 | 2 | case 0xC3: | |
| 230 | 2 | return 5; | |
| 231 | 1710 | default: | |
| 232 | 1710 | return 0; | |
| 233 | } | ||
| 234 | } | ||
| 235 | |||
| 236 | /** | ||
| 237 | * @brief Picks the index of the rarest fully-known byte in segment 0 to drive the prefilter. | ||
| 238 | * @param buffer A parsed buffer (only segment 0, i.e. [0, segment-0 end), is inspected). | ||
| 239 | * @return The index of the lowest-frequency 0xFF-masked byte in segment 0, or NO_ANCHOR if it has no full byte. | ||
| 240 | * @details Only a fully-known byte can anchor, because the prefilter sweeps with a single-byte memchr that cannot | ||
| 241 | * search for a partial nibble. The search is confined to segment 0 (the fixed run before the first bounded | ||
| 242 | * jump): the matcher finds that run first and extends across the variable gaps, so a byte in a later | ||
| 243 | * segment lands at a gap-dependent address the memchr prefilter cannot target. The scan keeps the | ||
| 244 | * lowest-scoring candidate seen so far and stops early once it finds a rarest-class byte (score 0), since | ||
| 245 | * no later byte can beat it. | ||
| 246 | */ | ||
| 247 | 413 | [[nodiscard]] constexpr std::size_t select_anchor(const PatternBuffer &buffer) noexcept | |
| 248 | { | ||
| 249 | // Segment 0 spans [0, first-jump position); with no jumps it is the whole pattern, so a plain pattern anchors | ||
| 250 | // over its entire length. | ||
| 251 |
2/2✓ Branch 2 → 3 taken 35 times.
✓ Branch 2 → 5 taken 378 times.
|
413 | const std::size_t segment0_end = (buffer.jump_count > 0) ? buffer.jumps[0].position : buffer.length; |
| 252 | 413 | std::size_t best = NO_ANCHOR; | |
| 253 | 413 | std::uint8_t best_score = 0xFF; | |
| 254 |
2/2✓ Branch 18 → 7 taken 919 times.
✓ Branch 18 → 19 taken 42 times.
|
961 | for (std::size_t index = 0; index < segment0_end; ++index) |
| 255 | { | ||
| 256 |
2/2✓ Branch 8 → 9 taken 101 times.
✓ Branch 8 → 10 taken 818 times.
|
919 | if (buffer.mask[index] != std::byte{0xFF}) |
| 257 | { | ||
| 258 | 101 | continue; | |
| 259 | } | ||
| 260 | 1636 | const std::uint8_t score = byte_frequency_class(std::to_integer<std::uint8_t>(buffer.bytes[index])); | |
| 261 |
2/2✓ Branch 14 → 15 taken 634 times.
✓ Branch 14 → 17 taken 184 times.
|
818 | if (score < best_score) |
| 262 | { | ||
| 263 | 634 | best = index; | |
| 264 | 634 | best_score = score; | |
| 265 |
2/2✓ Branch 15 → 16 taken 371 times.
✓ Branch 15 → 17 taken 263 times.
|
634 | if (score == 0) |
| 266 | { | ||
| 267 | 371 | break; | |
| 268 | } | ||
| 269 | } | ||
| 270 | } | ||
| 271 | 413 | return best; | |
| 272 | } | ||
| 273 | |||
| 274 | /** | ||
| 275 | * @struct JumpParse | ||
| 276 | * @brief Outcome of parsing one `[...]` bounded-jump token: a validity flag plus the resolved skip bounds. | ||
| 277 | */ | ||
| 278 | struct JumpParse | ||
| 279 | { | ||
| 280 | /// True when the token was a well-formed, in-range bounded jump. | ||
| 281 | bool ok{false}; | ||
| 282 | /// Fewest bytes the gap skips. | ||
| 283 | std::size_t min_skip{0}; | ||
| 284 | /// Most bytes the gap skips. | ||
| 285 | std::size_t max_skip{0}; | ||
| 286 | }; | ||
| 287 | |||
| 288 | /** | ||
| 289 | * @brief Parses a whitespace-stripped bounded-jump token: `[X]` (exact) or `[X-Y]` (range). | ||
| 290 | * @param token The token including its brackets, e.g. "[2-5]". | ||
| 291 | * @return A JumpParse with ok == true and the resolved bounds, or ok == false on any malformed / out-of-range form. | ||
| 292 | * @details Rejects (returns ok == false) anything that is not a faithful bounded jump: missing brackets, empty or | ||
| 293 | * non-decimal content, trailing junk after the number(s), an inverted range (max < min), a bound above | ||
| 294 | * MAX_JUMP_SPAN, and YARA's unbounded `[X-]` form (this dialect is deliberately bounded so every match | ||
| 295 | * attempt stays a predictable cost). `[N]` is the exact-skip shorthand for `[N-N]`, including the harmless | ||
| 296 | * no-op `[0]`. | ||
| 297 | */ | ||
| 298 | 153 | [[nodiscard]] constexpr JumpParse parse_jump_token(std::string_view token) noexcept | |
| 299 | { | ||
| 300 | 153 | JumpParse result{}; | |
| 301 |
6/8✓ Branch 3 → 4 taken 149 times.
✓ Branch 3 → 8 taken 4 times.
✓ Branch 5 → 6 taken 149 times.
✗ Branch 5 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 149 times.
✓ Branch 10 → 11 taken 4 times.
✓ Branch 10 → 12 taken 149 times.
|
153 | if (token.size() < 3 || token.front() != '[' || token.back() != ']') |
| 302 | { | ||
| 303 | 4 | return result; | |
| 304 | } | ||
| 305 | 149 | const std::string_view inner = token.substr(1, token.size() - 2); | |
| 306 | |||
| 307 | 149 | std::size_t cursor = 0; | |
| 308 | // The minimum bound is mandatory: a jump always names at least one number. | ||
| 309 |
6/8✓ Branch 15 → 16 taken 149 times.
✗ Branch 15 → 20 not taken.
✓ Branch 17 → 18 taken 149 times.
✗ Branch 17 → 20 not taken.
✓ Branch 19 → 20 taken 1 time.
✓ Branch 19 → 21 taken 148 times.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 24 taken 148 times.
|
149 | if (cursor >= inner.size() || inner[cursor] < '0' || inner[cursor] > '9') |
| 310 | { | ||
| 311 | 1 | return result; | |
| 312 | } | ||
| 313 | 148 | std::size_t min_value = 0; | |
| 314 |
7/8✓ Branch 30 → 31 taken 275 times.
✓ Branch 30 → 36 taken 34 times.
✓ Branch 32 → 33 taken 161 times.
✓ Branch 32 → 36 taken 114 times.
✓ Branch 34 → 35 taken 161 times.
✗ Branch 34 → 36 not taken.
✓ Branch 37 → 25 taken 161 times.
✓ Branch 37 → 38 taken 148 times.
|
309 | while (cursor < inner.size() && inner[cursor] >= '0' && inner[cursor] <= '9') |
| 315 | { | ||
| 316 | 161 | min_value = min_value * 10 + static_cast<std::size_t>(inner[cursor] - '0'); | |
| 317 |
1/2✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 161 times.
|
161 | if (min_value > MAX_JUMP_SPAN) |
| 318 | { | ||
| 319 | // Also guards against integer overflow: any in-range bound is <= MAX_JUMP_SPAN, so bailing here keeps | ||
| 320 | // the accumulator far from wrapping. | ||
| 321 | ✗ | return result; | |
| 322 | } | ||
| 323 | 161 | ++cursor; | |
| 324 | } | ||
| 325 | |||
| 326 | 148 | std::size_t max_value = min_value; | |
| 327 |
2/2✓ Branch 39 → 40 taken 114 times.
✓ Branch 39 → 67 taken 34 times.
|
148 | if (cursor < inner.size()) |
| 328 | { | ||
| 329 |
1/2✗ Branch 41 → 42 not taken.
✓ Branch 41 → 43 taken 114 times.
|
114 | if (inner[cursor] != '-') |
| 330 | { | ||
| 331 | ✗ | return result; | |
| 332 | } | ||
| 333 | 114 | ++cursor; | |
| 334 | // A dash with no following digits is YARA's unbounded `[X-]`, which this bounded dialect does not admit. | ||
| 335 |
6/8✓ Branch 44 → 45 taken 112 times.
✓ Branch 44 → 49 taken 2 times.
✓ Branch 46 → 47 taken 112 times.
✗ Branch 46 → 49 not taken.
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 50 taken 112 times.
✓ Branch 51 → 52 taken 2 times.
✓ Branch 51 → 53 taken 112 times.
|
114 | if (cursor >= inner.size() || inner[cursor] < '0' || inner[cursor] > '9') |
| 336 | { | ||
| 337 | 2 | return result; | |
| 338 | } | ||
| 339 | 112 | max_value = 0; | |
| 340 |
6/8✓ Branch 59 → 60 taken 253 times.
✓ Branch 59 → 65 taken 111 times.
✓ Branch 61 → 62 taken 253 times.
✗ Branch 61 → 65 not taken.
✓ Branch 63 → 64 taken 253 times.
✗ Branch 63 → 65 not taken.
✓ Branch 66 → 54 taken 253 times.
✓ Branch 66 → 67 taken 111 times.
|
364 | while (cursor < inner.size() && inner[cursor] >= '0' && inner[cursor] <= '9') |
| 341 | { | ||
| 342 | 253 | max_value = max_value * 10 + static_cast<std::size_t>(inner[cursor] - '0'); | |
| 343 |
2/2✓ Branch 55 → 56 taken 1 time.
✓ Branch 55 → 57 taken 252 times.
|
253 | if (max_value > MAX_JUMP_SPAN) |
| 344 | { | ||
| 345 | 1 | return result; | |
| 346 | } | ||
| 347 | 252 | ++cursor; | |
| 348 | } | ||
| 349 | } | ||
| 350 | |||
| 351 |
5/6✓ Branch 68 → 69 taken 145 times.
✗ Branch 68 → 70 not taken.
✓ Branch 69 → 70 taken 3 times.
✓ Branch 69 → 71 taken 142 times.
✓ Branch 72 → 73 taken 3 times.
✓ Branch 72 → 74 taken 142 times.
|
145 | if (cursor != inner.size() || max_value < min_value) |
| 352 | { | ||
| 353 | // Trailing junk after the bounds, or an inverted range. | ||
| 354 | 3 | return result; | |
| 355 | } | ||
| 356 | 142 | result.ok = true; | |
| 357 | 142 | result.min_skip = min_value; | |
| 358 | 142 | result.max_skip = max_value; | |
| 359 | 142 | return result; | |
| 360 | } | ||
| 361 | |||
| 362 | /** | ||
| 363 | * @struct PatternBufferSink | ||
| 364 | * @brief The fixed-array storage sink for the compile-time parse: caps at MAX_PATTERN_BYTES / MAX_PATTERN_JUMPS. | ||
| 365 | * @details The compile-time Pattern must be a literal type a consteval result can return, so its byte / mask / jump | ||
| 366 | * storage is a fixed array and appending past the cap fails (a TooLong / TooManyJumps parse). The shared | ||
| 367 | * parser (@ref parse_pattern_into) writes every token through a sink so the grammar has one | ||
| 368 | * implementation; the runtime engine supplies its own heap-backed sink with no byte cap, which is why the | ||
| 369 | * same grammar serves both without imposing the literal-storage cap on runtime patterns. | ||
| 370 | */ | ||
| 371 | struct PatternBufferSink | ||
| 372 | { | ||
| 373 | /// The compiled buffer being filled; only meaningful once the parse returns Ok. | ||
| 374 | PatternBuffer buffer{}; | ||
| 375 | |||
| 376 | /// Fixed bytes appended so far. | ||
| 377 | 825 | [[nodiscard]] constexpr std::size_t length() const noexcept { return buffer.length; } | |
| 378 | /// Jump gaps appended so far. | ||
| 379 | 414 | [[nodiscard]] constexpr std::size_t jump_count() const noexcept { return buffer.jump_count; } | |
| 380 | |||
| 381 | /// Appends one fixed byte; returns false when the fixed-array cap is reached (parser maps this to TooLong). | ||
| 382 | 4281 | [[nodiscard]] constexpr bool add_byte(std::byte value, std::byte mask) noexcept | |
| 383 | { | ||
| 384 |
2/2✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 4279 times.
|
4281 | if (buffer.length >= MAX_PATTERN_BYTES) |
| 385 | { | ||
| 386 | 2 | return false; | |
| 387 | } | ||
| 388 | 4279 | buffer.bytes[buffer.length] = value; | |
| 389 | 4279 | buffer.mask[buffer.length] = mask; | |
| 390 | 4279 | ++buffer.length; | |
| 391 | 4279 | return true; | |
| 392 | } | ||
| 393 | |||
| 394 | /// Records a gap; returns false when the jump cap is reached (parser maps this to TooManyJumps). | ||
| 395 | 91 | [[nodiscard]] constexpr bool add_jump(std::size_t position, std::size_t min_skip, std::size_t max_skip) noexcept | |
| 396 | { | ||
| 397 |
2/2✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 90 times.
|
91 | if (buffer.jump_count >= MAX_PATTERN_JUMPS) |
| 398 | { | ||
| 399 | 1 | return false; | |
| 400 | } | ||
| 401 | 90 | buffer.jumps[buffer.jump_count] = PatternJump{position, min_skip, max_skip}; | |
| 402 | 90 | ++buffer.jump_count; | |
| 403 | 90 | return true; | |
| 404 | } | ||
| 405 | |||
| 406 | /// Records the `|` marker position in the fixed byte stream. | ||
| 407 | 7 | constexpr void set_offset(std::size_t position) noexcept { buffer.offset = position; } | |
| 408 | }; | ||
| 409 | |||
| 410 | /** | ||
| 411 | * @brief The single AOB DSL grammar, parsing into any storage sink (fixed-array or heap-backed). | ||
| 412 | * @param dsl The whitespace-separated token string, e.g. "48 8B 05 ?? ?? ?? ??". | ||
| 413 | * @param sink The storage sink; its add_byte / add_jump / set_offset / length / jump_count drive where tokens land. | ||
| 414 | * @return The parse status. On Ok the sink holds the compiled bytes / mask / jumps / offset (the anchor, which is | ||
| 415 | * storage-specific, is computed by the caller afterwards). | ||
| 416 | * @details One implementation of the grammar serves both the compile-time Pattern and the runtime EnginePattern. | ||
| 417 | * Whitespace splits the tokens, and leading or trailing whitespace is ignored. Recognized tokens: | ||
| 418 | * - two hex digits (`48`) -> that byte, mask 0xFF (fully known) | ||
| 419 | * - `??` or `?` -> any byte, mask 0x00 (full wildcard) | ||
| 420 | * - hex digit then `?` (`4?`) -> high nibble fixed, mask 0xF0 | ||
| 421 | * - `?` then hex digit (`?5`) -> low nibble fixed, mask 0x0F | ||
| 422 | * - `[X]` / `[X-Y]` -> bounded jump: skip exactly X, or between X and Y, bytes before the next | ||
| 423 | * segment; splits the pattern into segments recorded as jumps | ||
| 424 | * - `|` -> offset marker: records the position of the NEXT byte (or the length | ||
| 425 | * when trailing) as the result offset; permitted at most once | ||
| 426 | * Any other token shape fails with InvalidToken. An input with no byte tokens fails with Empty. A sink | ||
| 427 | * that rejects an append fails with TooLong (byte cap) or TooManyJumps (jump cap). | ||
| 428 | * | ||
| 429 | * Every segment must be a non-empty fixed run, so a jump must not lead or trail the pattern and two | ||
| 430 | * jumps must not be adjacent. A violation is InvalidJump. The `|` marker records a position in the fixed | ||
| 431 | * byte stream. When a pattern also carries jumps, the resolver adds the actual gap bytes at match time, | ||
| 432 | * so the marker still points at the right run. | ||
| 433 | * @note Not noexcept. The compile-time fixed-array sink never allocates, so the literal path cannot throw. A | ||
| 434 | * heap-backed runtime sink can throw bad_alloc on an unbounded pattern, and a noexcept mark turns that OOM | ||
| 435 | * into a std::terminate. `parse_aob` catches the throw and fails closed to nullopt instead. | ||
| 436 | */ | ||
| 437 | 1292 | template <class Sink> [[nodiscard]] constexpr PatternStatus parse_pattern_into(std::string_view dsl, Sink &sink) | |
| 438 | { | ||
| 439 | 1292 | bool offset_marked = false; | |
| 440 | |||
| 441 | // Index of the most recent segment boundary: 0 at the start, then the fixed length at each jump. A jump is only | ||
| 442 | // legal once at least one fixed byte has been added since this boundary, which enforces "no leading jump" and | ||
| 443 | // "no two adjacent jumps" in one check, and the end-of-parse comparison against it catches a trailing jump. | ||
| 444 | 1292 | std::size_t last_boundary = 0; | |
| 445 | |||
| 446 | 1292 | std::size_t cursor = 0; | |
| 447 | 1292 | const std::size_t end = dsl.size(); | |
| 448 |
4/4DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::detail::PatternBufferSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::detail::PatternBufferSink&):
✓ Branch 104 → 4 taken 8384 times.
✓ Branch 104 → 105 taken 416 times.
DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::(anonymous namespace)::EnginePatternSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::(anonymous namespace)::EnginePatternSink&):
✓ Branch 104 → 4 taken 32240 times.
✓ Branch 104 → 105 taken 842 times.
|
41882 | while (cursor < end) |
| 449 | { | ||
| 450 |
4/4DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::detail::PatternBufferSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::detail::PatternBufferSink&):
✓ Branch 6 → 7 taken 3989 times.
✓ Branch 6 → 8 taken 4395 times.
DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::(anonymous namespace)::EnginePatternSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::(anonymous namespace)::EnginePatternSink&):
✓ Branch 6 → 7 taken 16061 times.
✓ Branch 6 → 8 taken 16179 times.
|
40624 | if (is_token_space(dsl[cursor])) |
| 451 | { | ||
| 452 | 20050 | ++cursor; | |
| 453 | 20208 | continue; | |
| 454 | } | ||
| 455 | |||
| 456 | 20574 | const std::size_t token_start = cursor; | |
| 457 |
12/12DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::detail::PatternBufferSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::detail::PatternBufferSink&):
✓ Branch 10 → 11 taken 13118 times.
✓ Branch 10 → 15 taken 393 times.
✓ Branch 13 → 14 taken 9116 times.
✓ Branch 13 → 15 taken 4002 times.
✓ Branch 16 → 9 taken 9116 times.
✓ Branch 16 → 17 taken 4395 times.
DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::(anonymous namespace)::EnginePatternSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::(anonymous namespace)::EnginePatternSink&):
✓ Branch 10 → 11 taken 48594 times.
✓ Branch 10 → 15 taken 124 times.
✓ Branch 13 → 14 taken 32540 times.
✓ Branch 13 → 15 taken 16056 times.
✓ Branch 16 → 9 taken 32539 times.
✓ Branch 16 → 17 taken 16181 times.
|
62229 | while (cursor < end && !is_token_space(dsl[cursor])) |
| 458 | { | ||
| 459 | 41655 | ++cursor; | |
| 460 | } | ||
| 461 |
2/4DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::detail::PatternBufferSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::detail::PatternBufferSink&):
✓ Branch 17 → 18 taken 4395 times.
✗ Branch 17 → 118 not taken.
DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::(anonymous namespace)::EnginePatternSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::(anonymous namespace)::EnginePatternSink&):
✓ Branch 17 → 18 taken 16180 times.
✗ Branch 17 → 119 not taken.
|
20576 | const std::string_view token = dsl.substr(token_start, cursor - token_start); |
| 462 | |||
| 463 | // Offset marker: the position of interest is wherever the next byte lands. Placed at the very end, that is | ||
| 464 | // one past the final byte (offset == length), which is exactly the value the sink already holds. | ||
| 465 |
12/12DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::detail::PatternBufferSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::detail::PatternBufferSink&):
✓ Branch 19 → 20 taken 10 times.
✓ Branch 19 → 23 taken 4385 times.
✓ Branch 21 → 22 taken 8 times.
✓ Branch 21 → 23 taken 2 times.
✓ Branch 24 → 25 taken 8 times.
✓ Branch 24 → 30 taken 4387 times.
DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::(anonymous namespace)::EnginePatternSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::(anonymous namespace)::EnginePatternSink&):
✓ Branch 19 → 20 taken 18 times.
✓ Branch 19 → 23 taken 16162 times.
✓ Branch 21 → 22 taken 15 times.
✓ Branch 21 → 23 taken 3 times.
✓ Branch 24 → 25 taken 15 times.
✓ Branch 24 → 30 taken 16165 times.
|
20575 | if (token.size() == 1 && token[0] == '|') |
| 466 | { | ||
| 467 |
4/4DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::detail::PatternBufferSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::detail::PatternBufferSink&):
✓ Branch 25 → 26 taken 1 time.
✓ Branch 25 → 27 taken 7 times.
DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::(anonymous namespace)::EnginePatternSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::(anonymous namespace)::EnginePatternSink&):
✓ Branch 25 → 26 taken 1 time.
✓ Branch 25 → 27 taken 14 times.
|
23 | if (offset_marked) |
| 468 | { | ||
| 469 | 33 | return PatternStatus::DuplicateOffset; | |
| 470 | } | ||
| 471 | 21 | offset_marked = true; | |
| 472 | 21 | sink.set_offset(sink.length()); | |
| 473 | 21 | continue; | |
| 474 | } | ||
| 475 | |||
| 476 | // Bounded jump: `[X]` or `[X-Y]`. A token that opens with `[` is always intended as a jump, so a malformed | ||
| 477 | // bracket form is a hard InvalidJump rather than falling through to the byte-token parser (which would | ||
| 478 | // otherwise misreport it as a generic InvalidToken). | ||
| 479 |
10/12DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::detail::PatternBufferSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::detail::PatternBufferSink&):
✓ Branch 31 → 32 taken 4387 times.
✗ Branch 31 → 35 not taken.
✓ Branch 33 → 34 taken 100 times.
✓ Branch 33 → 35 taken 4287 times.
✓ Branch 36 → 37 taken 100 times.
✓ Branch 36 → 55 taken 4287 times.
DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::(anonymous namespace)::EnginePatternSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::(anonymous namespace)::EnginePatternSink&):
✓ Branch 31 → 32 taken 16165 times.
✗ Branch 31 → 35 not taken.
✓ Branch 33 → 34 taken 53 times.
✓ Branch 33 → 35 taken 16112 times.
✓ Branch 36 → 37 taken 53 times.
✓ Branch 36 → 55 taken 16112 times.
|
20552 | if (!token.empty() && token.front() == '[') |
| 480 | { | ||
| 481 | 153 | const JumpParse jump = parse_jump_token(token); | |
| 482 |
4/4DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::detail::PatternBufferSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::detail::PatternBufferSink&):
✓ Branch 38 → 39 taken 7 times.
✓ Branch 38 → 40 taken 93 times.
DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::(anonymous namespace)::EnginePatternSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::(anonymous namespace)::EnginePatternSink&):
✓ Branch 38 → 39 taken 4 times.
✓ Branch 38 → 40 taken 49 times.
|
153 | if (!jump.ok) |
| 483 | { | ||
| 484 | 16 | return PatternStatus::InvalidJump; | |
| 485 | } | ||
| 486 | // A jump must sit between two non-empty fixed runs: reject a leading jump (no byte yet) and a jump | ||
| 487 | // adjacent to the previous one (no byte added since the last boundary). Both collapse to this one test. | ||
| 488 |
12/12DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::detail::PatternBufferSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::detail::PatternBufferSink&):
✓ Branch 41 → 42 taken 92 times.
✓ Branch 41 → 44 taken 1 time.
✓ Branch 43 → 44 taken 1 time.
✓ Branch 43 → 45 taken 91 times.
✓ Branch 46 → 47 taken 2 times.
✓ Branch 46 → 48 taken 91 times.
DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::(anonymous namespace)::EnginePatternSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::(anonymous namespace)::EnginePatternSink&):
✓ Branch 41 → 42 taken 48 times.
✓ Branch 41 → 44 taken 1 time.
✓ Branch 43 → 44 taken 1 time.
✓ Branch 43 → 45 taken 47 times.
✓ Branch 46 → 47 taken 2 times.
✓ Branch 46 → 48 taken 47 times.
|
142 | if (sink.length() == 0 || sink.length() == last_boundary) |
| 489 | { | ||
| 490 | 4 | return PatternStatus::InvalidJump; | |
| 491 | } | ||
| 492 |
4/6DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::detail::PatternBufferSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::detail::PatternBufferSink&):
✓ Branch 50 → 51 taken 1 time.
✓ Branch 50 → 52 taken 90 times.
DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::(anonymous namespace)::EnginePatternSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::(anonymous namespace)::EnginePatternSink&):
✓ Branch 49 → 50 taken 47 times.
✗ Branch 49 → 118 not taken.
✗ Branch 50 → 51 not taken.
✓ Branch 50 → 52 taken 47 times.
|
138 | if (!sink.add_jump(sink.length(), jump.min_skip, jump.max_skip)) |
| 493 | { | ||
| 494 | 1 | return PatternStatus::TooManyJumps; | |
| 495 | } | ||
| 496 | 137 | last_boundary = sink.length(); | |
| 497 | 137 | continue; | |
| 498 | 137 | } | |
| 499 | |||
| 500 | 20399 | std::byte byte_value{0x00}; | |
| 501 | 20399 | std::byte mask_value{0x00}; | |
| 502 |
12/12DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::detail::PatternBufferSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::detail::PatternBufferSink&):
✓ Branch 56 → 57 taken 4283 times.
✓ Branch 56 → 62 taken 4 times.
✓ Branch 58 → 59 taken 424 times.
✓ Branch 58 → 62 taken 3859 times.
✓ Branch 60 → 61 taken 421 times.
✓ Branch 60 → 62 taken 3 times.
DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::(anonymous namespace)::EnginePatternSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::(anonymous namespace)::EnginePatternSink&):
✓ Branch 56 → 57 taken 16108 times.
✓ Branch 56 → 62 taken 4 times.
✓ Branch 58 → 59 taken 82 times.
✓ Branch 58 → 62 taken 16026 times.
✓ Branch 60 → 61 taken 77 times.
✓ Branch 60 → 62 taken 5 times.
|
20399 | const bool double_wildcard = token.size() == 2 && token[0] == '?' && token[1] == '?'; |
| 503 |
8/8DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::detail::PatternBufferSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::detail::PatternBufferSink&):
✓ Branch 64 → 65 taken 2 times.
✓ Branch 64 → 68 taken 4285 times.
✓ Branch 66 → 67 taken 1 time.
✓ Branch 66 → 68 taken 1 time.
DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::(anonymous namespace)::EnginePatternSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::(anonymous namespace)::EnginePatternSink&):
✓ Branch 64 → 65 taken 3 times.
✓ Branch 64 → 68 taken 16109 times.
✓ Branch 66 → 67 taken 2 times.
✓ Branch 66 → 68 taken 1 time.
|
20399 | const bool single_wildcard = token.size() == 1 && token[0] == '?'; |
| 504 |
8/8DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::detail::PatternBufferSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::detail::PatternBufferSink&):
✓ Branch 69 → 70 taken 3866 times.
✓ Branch 69 → 97 taken 421 times.
✓ Branch 70 → 71 taken 3865 times.
✓ Branch 70 → 97 taken 1 time.
DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::(anonymous namespace)::EnginePatternSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::(anonymous namespace)::EnginePatternSink&):
✓ Branch 69 → 70 taken 16035 times.
✓ Branch 69 → 97 taken 77 times.
✓ Branch 70 → 71 taken 16033 times.
✓ Branch 70 → 97 taken 2 times.
|
20399 | if (double_wildcard || single_wildcard) |
| 505 | { | ||
| 506 | // Full wildcard: any byte matches, so both value and mask stay zero. | ||
| 507 | } | ||
| 508 |
4/4DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::detail::PatternBufferSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::detail::PatternBufferSink&):
✓ Branch 72 → 73 taken 3862 times.
✓ Branch 72 → 96 taken 3 times.
DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::(anonymous namespace)::EnginePatternSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::(anonymous namespace)::EnginePatternSink&):
✓ Branch 72 → 73 taken 16030 times.
✓ Branch 72 → 96 taken 2 times.
|
19898 | else if (token.size() == 2) |
| 509 | { | ||
| 510 | 19892 | const int high = hex_digit(token[0]); | |
| 511 | 19892 | const int low = hex_digit(token[1]); | |
| 512 |
8/8DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::detail::PatternBufferSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::detail::PatternBufferSink&):
✓ Branch 77 → 78 taken 3856 times.
✓ Branch 77 → 80 taken 6 times.
✓ Branch 78 → 79 taken 3852 times.
✓ Branch 78 → 80 taken 4 times.
DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::(anonymous namespace)::EnginePatternSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::(anonymous namespace)::EnginePatternSink&):
✓ Branch 77 → 78 taken 16020 times.
✓ Branch 77 → 80 taken 9 times.
✓ Branch 78 → 79 taken 16012 times.
✓ Branch 78 → 80 taken 8 times.
|
19891 | if (high >= 0 && low >= 0) |
| 513 | { | ||
| 514 | 19864 | byte_value = static_cast<std::byte>(static_cast<unsigned char>((high << 4) | low)); | |
| 515 | 19864 | mask_value = std::byte{0xFF}; | |
| 516 | } | ||
| 517 |
10/12DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::detail::PatternBufferSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::detail::PatternBufferSink&):
✓ Branch 80 → 81 taken 4 times.
✓ Branch 80 → 84 taken 6 times.
✓ Branch 82 → 83 taken 4 times.
✗ Branch 82 → 84 not taken.
✓ Branch 85 → 86 taken 4 times.
✓ Branch 85 → 87 taken 6 times.
DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::(anonymous namespace)::EnginePatternSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::(anonymous namespace)::EnginePatternSink&):
✓ Branch 80 → 81 taken 7 times.
✓ Branch 80 → 84 taken 10 times.
✓ Branch 82 → 83 taken 7 times.
✗ Branch 82 → 84 not taken.
✓ Branch 85 → 86 taken 7 times.
✓ Branch 85 → 87 taken 10 times.
|
27 | else if (high >= 0 && token[1] == '?') |
| 518 | { | ||
| 519 | 11 | byte_value = static_cast<std::byte>(static_cast<unsigned char>(high << 4)); | |
| 520 | 11 | mask_value = std::byte{0xF0}; | |
| 521 | } | ||
| 522 |
10/12DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::detail::PatternBufferSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::detail::PatternBufferSink&):
✓ Branch 88 → 89 taken 3 times.
✓ Branch 88 → 91 taken 3 times.
✓ Branch 89 → 90 taken 3 times.
✗ Branch 89 → 91 not taken.
✓ Branch 92 → 93 taken 3 times.
✓ Branch 92 → 94 taken 3 times.
DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::(anonymous namespace)::EnginePatternSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::(anonymous namespace)::EnginePatternSink&):
✓ Branch 88 → 89 taken 5 times.
✓ Branch 88 → 91 taken 5 times.
✓ Branch 89 → 90 taken 5 times.
✗ Branch 89 → 91 not taken.
✓ Branch 92 → 93 taken 5 times.
✓ Branch 92 → 94 taken 5 times.
|
16 | else if (token[0] == '?' && low >= 0) |
| 523 | { | ||
| 524 | 8 | byte_value = static_cast<std::byte>(static_cast<unsigned char>(low)); | |
| 525 | 8 | mask_value = std::byte{0x0F}; | |
| 526 | } | ||
| 527 | else | ||
| 528 | { | ||
| 529 | 8 | return PatternStatus::InvalidToken; | |
| 530 | } | ||
| 531 | } | ||
| 532 | else | ||
| 533 | { | ||
| 534 | 5 | return PatternStatus::InvalidToken; | |
| 535 | } | ||
| 536 | |||
| 537 |
5/6DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::detail::PatternBufferSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::detail::PatternBufferSink&):
✓ Branch 98 → 99 taken 2 times.
✓ Branch 98 → 100 taken 4279 times.
DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::(anonymous namespace)::EnginePatternSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::(anonymous namespace)::EnginePatternSink&):
✓ Branch 97 → 98 taken 16103 times.
✓ Branch 97 → 119 taken 1 time.
✗ Branch 98 → 99 not taken.
✓ Branch 98 → 100 taken 16103 times.
|
20384 | if (!sink.add_byte(byte_value, mask_value)) |
| 538 | { | ||
| 539 | 2 | return PatternStatus::TooLong; | |
| 540 | } | ||
| 541 | } | ||
| 542 | |||
| 543 |
4/4DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::detail::PatternBufferSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::detail::PatternBufferSink&):
✓ Branch 106 → 107 taken 2 times.
✓ Branch 106 → 108 taken 414 times.
DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::(anonymous namespace)::EnginePatternSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::(anonymous namespace)::EnginePatternSink&):
✓ Branch 106 → 107 taken 4 times.
✓ Branch 106 → 108 taken 838 times.
|
1258 | if (sink.length() == 0) |
| 544 | { | ||
| 545 | 6 | return PatternStatus::Empty; | |
| 546 | } | ||
| 547 | |||
| 548 | // A trailing jump leaves an empty final segment (no fixed byte was added after the last gap). last_boundary | ||
| 549 | // still equals the fixed length in that case, so this catches "48 8B [2-5]" without a separate flag. | ||
| 550 |
12/12DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::detail::PatternBufferSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::detail::PatternBufferSink&):
✓ Branch 109 → 110 taken 36 times.
✓ Branch 109 → 113 taken 378 times.
✓ Branch 111 → 112 taken 1 time.
✓ Branch 111 → 113 taken 35 times.
✓ Branch 114 → 115 taken 1 time.
✓ Branch 114 → 116 taken 413 times.
DetourModKit::detail::PatternStatus DetourModKit::detail::parse_pattern_into<DetourModKit::(anonymous namespace)::EnginePatternSink>(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::(anonymous namespace)::EnginePatternSink&):
✓ Branch 109 → 110 taken 21 times.
✓ Branch 109 → 113 taken 816 times.
✓ Branch 111 → 112 taken 1 time.
✓ Branch 111 → 113 taken 20 times.
✓ Branch 114 → 115 taken 1 time.
✓ Branch 114 → 116 taken 836 times.
|
1252 | if (sink.jump_count() > 0 && last_boundary == sink.length()) |
| 551 | { | ||
| 552 | 2 | return PatternStatus::InvalidJump; | |
| 553 | } | ||
| 554 | |||
| 555 | 1249 | return PatternStatus::Ok; | |
| 556 | } | ||
| 557 | |||
| 558 | /** | ||
| 559 | * @brief Parses an AOB DSL string into a fixed-array PatternBuffer (the compile-time / value-Pattern storage). | ||
| 560 | * @param dsl The whitespace-separated token string, e.g. "48 8B 05 ?? ?? ?? ??". | ||
| 561 | * @return A PatternParse whose status is Ok (with a filled buffer) or a specific failure; a pattern with more than | ||
| 562 | * MAX_PATTERN_BYTES fixed bytes fails with TooLong (the fixed-array cap the literal type imposes). | ||
| 563 | * @details A thin wrapper over @ref parse_pattern_into with the capped fixed-array sink, plus the segment-0 | ||
| 564 | * rarest-byte anchor computed on success. The runtime engine parses the same grammar through a heap-backed | ||
| 565 | * sink that has no byte cap, so a long runtime pattern is not bound by this literal-storage limit. | ||
| 566 | */ | ||
| 567 | 435 | [[nodiscard]] constexpr PatternParse parse_pattern(std::string_view dsl) noexcept | |
| 568 | { | ||
| 569 | 435 | PatternParse result{}; | |
| 570 | 435 | PatternBufferSink sink{}; | |
| 571 | 435 | result.status = parse_pattern_into(dsl, sink); | |
| 572 |
2/2✓ Branch 3 → 4 taken 413 times.
✓ Branch 3 → 6 taken 22 times.
|
435 | if (result.status == PatternStatus::Ok) |
| 573 | { | ||
| 574 | 413 | result.buffer = sink.buffer; | |
| 575 | 413 | result.buffer.anchor = select_anchor(result.buffer); | |
| 576 | } | ||
| 577 | 435 | return result; | |
| 578 | } | ||
| 579 | |||
| 580 | /** | ||
| 581 | * @brief The fewest bytes any match of @p buffer can occupy (fixed bytes plus every gap's minimum skip). | ||
| 582 | * @details A jump-free pattern's minimum span is just its length. With gaps the shortest possible match still | ||
| 583 | * consumes each gap's lower bound, so this is the true minimum window a match needs. | ||
| 584 | */ | ||
| 585 | 31 | [[nodiscard]] constexpr std::size_t min_match_length(const PatternBuffer &buffer) noexcept | |
| 586 | { | ||
| 587 | 31 | std::size_t total = buffer.length; | |
| 588 |
2/2✓ Branch 5 → 3 taken 53 times.
✓ Branch 5 → 6 taken 31 times.
|
84 | for (std::size_t i = 0; i < buffer.jump_count; ++i) |
| 589 | { | ||
| 590 | 53 | total += buffer.jumps[i].min_skip; | |
| 591 | } | ||
| 592 | 31 | return total; | |
| 593 | } | ||
| 594 | |||
| 595 | /** | ||
| 596 | * @brief The fewest physical bytes from the offset-applied result point through the end of any match. | ||
| 597 | * @details Gaps strictly after the fixed-stream marker contribute their minimum skip. A gap at the marker sits | ||
| 598 | * before the following segment's first byte, so it does not extend the suffix from that point. | ||
| 599 | */ | ||
| 600 | 42 | [[nodiscard]] constexpr std::size_t min_match_suffix_length(const PatternBuffer &buffer) noexcept | |
| 601 | { | ||
| 602 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 42 times.
|
42 | if (buffer.offset > buffer.length) |
| 603 | { | ||
| 604 | ✗ | return 0; | |
| 605 | } | ||
| 606 | 42 | std::size_t total = buffer.length - buffer.offset; | |
| 607 |
2/2✓ Branch 10 → 5 taken 6 times.
✓ Branch 10 → 11 taken 42 times.
|
48 | for (std::size_t i = 0; i < buffer.jump_count; ++i) |
| 608 | { | ||
| 609 |
2/2✓ Branch 6 → 7 taken 3 times.
✓ Branch 6 → 9 taken 3 times.
|
6 | if (buffer.jumps[i].position > buffer.offset) |
| 610 | { | ||
| 611 | 3 | total += buffer.jumps[i].min_skip; | |
| 612 | } | ||
| 613 | } | ||
| 614 | 42 | return total; | |
| 615 | } | ||
| 616 | |||
| 617 | /** | ||
| 618 | * @brief The most bytes any match of @p buffer can occupy (fixed bytes plus every gap's maximum skip). | ||
| 619 | * @details The upper bound on a match's span. The page-gated scanner uses it to size the cross-region carry so a | ||
| 620 | * match straddling a protection boundary is still found, and to bound the needle self-exclusion window. | ||
| 621 | */ | ||
| 622 | 4 | [[nodiscard]] constexpr std::size_t max_match_length(const PatternBuffer &buffer) noexcept | |
| 623 | { | ||
| 624 | 4 | std::size_t total = buffer.length; | |
| 625 |
2/2✓ Branch 5 → 3 taken 18 times.
✓ Branch 5 → 6 taken 4 times.
|
22 | for (std::size_t i = 0; i < buffer.jump_count; ++i) |
| 626 | { | ||
| 627 | 18 | total += buffer.jumps[i].max_skip; | |
| 628 | } | ||
| 629 | 4 | return total; | |
| 630 | } | ||
| 631 | |||
| 632 | /** | ||
| 633 | * @brief Masked-compares one fixed segment run against a window at a given position. | ||
| 634 | * @param buffer The compiled pattern. | ||
| 635 | * @param window The candidate byte window. | ||
| 636 | * @param window_pos Offset into @p window at which the run must appear. | ||
| 637 | * @param body_begin First fixed-byte index of the run (inclusive). | ||
| 638 | * @param body_end One-past-last fixed-byte index of the run. | ||
| 639 | * @return True when the run fits in the window from @p window_pos and every masked byte agrees. | ||
| 640 | * @details The per-byte test is the same `(memory ^ pattern) & mask == 0` the scan engine uses, so a wildcard byte | ||
| 641 | * always agrees and a nibble mask compares only its fixed nibble. A run that would read past the window | ||
| 642 | * end cannot match. | ||
| 643 | */ | ||
| 644 | 131125 | [[nodiscard]] constexpr bool run_matches_at( | |
| 645 | const PatternBuffer &buffer, | ||
| 646 | std::span<const std::byte> window, | ||
| 647 | std::size_t window_pos, | ||
| 648 | std::size_t body_begin, | ||
| 649 | std::size_t body_end | ||
| 650 | ) noexcept | ||
| 651 | { | ||
| 652 | 131125 | const std::size_t run_length = body_end - body_begin; | |
| 653 |
5/6✓ Branch 3 → 4 taken 131125 times.
✗ Branch 3 → 6 not taken.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 131124 times.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 131124 times.
|
131125 | if (window_pos > window.size() || run_length > window.size() - window_pos) |
| 654 | { | ||
| 655 | 1 | return false; | |
| 656 | } | ||
| 657 |
2/2✓ Branch 21 → 11 taken 131147 times.
✓ Branch 21 → 22 taken 556 times.
|
131703 | for (std::size_t i = 0; i < run_length; ++i) |
| 658 | { | ||
| 659 | const std::byte masked_diff = | ||
| 660 | 262294 | (window[window_pos + i] ^ buffer.bytes[body_begin + i]) & buffer.mask[body_begin + i]; | |
| 661 |
2/2✓ Branch 18 → 19 taken 130568 times.
✓ Branch 18 → 20 taken 579 times.
|
131147 | if (masked_diff != std::byte{0x00}) |
| 662 | { | ||
| 663 | 130568 | return false; | |
| 664 | } | ||
| 665 | } | ||
| 666 | 556 | return true; | |
| 667 | } | ||
| 668 | |||
| 669 | /** | ||
| 670 | * @brief Backtracking segment match: does segment @p segment_index (and all that follow) match at @p window_pos? | ||
| 671 | * @details Matches the segment's fixed run, then for the gap that follows tries every skip in [min, max] in | ||
| 672 | * ascending order, recursing into the next segment. Ascending-skip order makes the overall match the | ||
| 673 | * leftmost feasible placement. Backtracking is required because a greedy choice for one segment can strand | ||
| 674 | * a later one: an earlier gap position that lets the tail match must be found even if a nearer position | ||
| 675 | * fails. Recursion DEPTH is bounded by the segment count (<= MAX_PATTERN_JUMPS + 1), and total WORK is | ||
| 676 | * bounded by | ||
| 677 | * @p steps, a shared node-visit counter for this one placement tree. On budget exhaustion the placement | ||
| 678 | * fails closed. In practice each segment run fails fast on its first literal byte, so a real signature | ||
| 679 | * (few gaps, literal-anchored segments) prunes to near-linear and never approaches the budget. | ||
| 680 | */ | ||
| 681 | 131127 | [[nodiscard]] constexpr bool try_segments_at( | |
| 682 | const PatternBuffer &buffer, | ||
| 683 | std::span<const std::byte> window, | ||
| 684 | std::size_t segment_index, | ||
| 685 | std::size_t window_pos, | ||
| 686 | std::size_t &steps | ||
| 687 | ) noexcept | ||
| 688 | { | ||
| 689 |
2/2✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 131125 times.
|
131127 | if (++steps > SEGMENT_MATCH_STEP_BUDGET) |
| 690 | { | ||
| 691 | 2 | return false; | |
| 692 | } | ||
| 693 | |||
| 694 |
2/2✓ Branch 4 → 5 taken 131101 times.
✓ Branch 4 → 7 taken 24 times.
|
131125 | const std::size_t segment_begin = (segment_index == 0) ? 0 : buffer.jumps[segment_index - 1].position; |
| 695 | const std::size_t segment_end = | ||
| 696 |
2/2✓ Branch 8 → 9 taken 546 times.
✓ Branch 8 → 11 taken 130579 times.
|
131125 | (segment_index < buffer.jump_count) ? buffer.jumps[segment_index].position : buffer.length; |
| 697 |
2/2✓ Branch 13 → 14 taken 130569 times.
✓ Branch 13 → 15 taken 556 times.
|
131125 | if (!run_matches_at(buffer, window, window_pos, segment_begin, segment_end)) |
| 698 | { | ||
| 699 | 130569 | return false; | |
| 700 | } | ||
| 701 |
2/2✓ Branch 15 → 16 taken 14 times.
✓ Branch 15 → 17 taken 542 times.
|
556 | if (segment_index == buffer.jump_count) |
| 702 | { | ||
| 703 | // The last segment matched, so the whole pattern matched at this start position. | ||
| 704 | 14 | return true; | |
| 705 | } | ||
| 706 | 542 | const std::size_t after = window_pos + (segment_end - segment_begin); | |
| 707 | 542 | const PatternJump &gap = buffer.jumps[segment_index]; | |
| 708 | 542 | const std::size_t available = window.size() - after; | |
| 709 |
2/2✓ Branch 28 → 20 taken 131103 times.
✓ Branch 28 → 29 taken 513 times.
|
131616 | for (std::size_t skip = gap.min_skip; skip <= gap.max_skip; ++skip) |
| 710 | { | ||
| 711 |
1/2✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 131103 times.
|
131103 | if (skip > available) |
| 712 | { | ||
| 713 | ✗ | break; | |
| 714 | } | ||
| 715 |
2/2✓ Branch 23 → 24 taken 13 times.
✓ Branch 23 → 25 taken 131090 times.
|
131103 | if (try_segments_at(buffer, window, segment_index + 1, after + skip, steps)) |
| 716 | { | ||
| 717 | 13 | return true; | |
| 718 | } | ||
| 719 |
2/2✓ Branch 25 → 26 taken 16 times.
✓ Branch 25 → 27 taken 131074 times.
|
131090 | if (steps > SEGMENT_MATCH_STEP_BUDGET) |
| 720 | { | ||
| 721 | 16 | return false; | |
| 722 | } | ||
| 723 | } | ||
| 724 | 513 | return false; | |
| 725 | } | ||
| 726 | |||
| 727 | /** | ||
| 728 | * @brief Tests whether @p buffer matches at the start of @p window, honoring any bounded jumps. | ||
| 729 | * @return True when a placement of every segment and gap fits in the window with all masked bytes agreeing. | ||
| 730 | * @details For a jump-free pattern this is exactly the single fixed-width masked compare (one segment covering the | ||
| 731 | * whole length); a pattern with gaps runs the backtracking search (see try_segments_at for its cost | ||
| 732 | * profile). A window shorter than the pattern's minimum span can never match. | ||
| 733 | */ | ||
| 734 | [[nodiscard]] constexpr bool | ||
| 735 | 26 | matches_buffer_at(const PatternBuffer &buffer, std::span<const std::byte> window) noexcept | |
| 736 | { | ||
| 737 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 26 times.
|
26 | if (buffer.length == 0) |
| 738 | { | ||
| 739 | ✗ | return false; | |
| 740 | } | ||
| 741 |
2/2✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 8 taken 24 times.
|
26 | if (window.size() < min_match_length(buffer)) |
| 742 | { | ||
| 743 | 2 | return false; | |
| 744 | } | ||
| 745 | 24 | std::size_t steps = 0; | |
| 746 | 24 | return try_segments_at(buffer, window, 0, 0, steps); | |
| 747 | } | ||
| 748 | |||
| 749 | } // namespace DetourModKit::detail | ||
| 750 | |||
| 751 | #endif // DETOURMODKIT_DETAIL_PATTERN_CORE_HPP | ||
| 752 |