./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
268 / 275
97.5%
Branches:
277 / 309
89.6%
Functions:
19 / 19
100.0%

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 `scan::Pattern::compile()` and the consteval `scan::Pattern::literal()` both parse through this core. It
8 * uses fixed-size arrays because a consteval result cannot own heap storage. The runtime scan engine shares
9 * its grammar and byte/mask encoding. A scan can replace this anchor with a haystack-based one.
10 * @note `scan.hpp` includes this header. docs/design/public-api.md owns the `detail/` placement rule.
11 */
12
13 #include <array>
14 #include <cstddef>
15 #include <cstdint>
16 #include <span>
17 #include <string_view>
18
19 namespace DetourModKit::detail
20 {
21 /** @brief Inline-storage cap for a compiled pattern, in bytes. `literal()` and `compile()` share this cap. */
22 inline constexpr std::size_t MAX_PATTERN_BYTES = 128;
23
24 /** @brief Anchor sentinel for "segment 0 has no fully-known byte", one past any valid index. */
25 inline constexpr std::size_t NO_ANCHOR = MAX_PATTERN_BYTES;
26
27 /** @brief Cap on bounded-jump gaps in one pattern. A pattern with more gaps fails at parse with TooManyJumps. */
28 inline constexpr std::size_t MAX_PATTERN_JUMPS = 8;
29
30 /** @brief Cap on the maximum skip of one jump gap, in bytes. The parser rejects a larger bound. */
31 inline constexpr std::size_t MAX_JUMP_SPAN = 256;
32
33 /** @brief Per-start-position cap on matcher node visits. Past the cap, that placement reports no match. */
34 inline constexpr std::size_t SEGMENT_MATCH_STEP_BUDGET = 1u << 16;
35 static_assert(
36 SEGMENT_MATCH_STEP_BUDGET >= MAX_PATTERN_JUMPS * MAX_JUMP_SPAN,
37 "The per-position work budget must exceed the linear per-position cost of a well-formed pattern."
38 );
39
40 /**
41 * @brief One bounded gap between two fixed byte runs (segments) of a compiled pattern.
42 * @details @ref position lies strictly inside `(0, length)`. An exact `[N]` jump has `min_skip == max_skip`.
43 */
44 struct PatternJump
45 {
46 /// Index in the fixed byte stream that the gap precedes: the boundary between segment i and segment i+1.
47 std::size_t position{0};
48 /// Fewest bytes the gap can skip before the following segment.
49 std::size_t min_skip{0};
50 /// Most bytes the gap can skip before the following segment, in [min_skip, MAX_JUMP_SPAN].
51 std::size_t max_skip{0};
52 };
53
54 /** @brief Outcome of an AOB DSL parse in the constexpr core. */
55 enum class PatternStatus : std::uint8_t
56 {
57 /// Parsed successfully into at least one byte.
58 Ok,
59 /// The input held no byte tokens (empty, whitespace-only, or only an offset marker).
60 Empty,
61 /// A token was not a recognized DSL form.
62 InvalidToken,
63 /// The pattern exceeded MAX_PATTERN_BYTES byte tokens.
64 TooLong,
65 /// More than one offset marker was present.
66 DuplicateOffset,
67 /// A `[...]` jump token was malformed, out of range, or illegally placed (leading, trailing, or adjacent).
68 InvalidJump,
69 /// The pattern named more bounded jumps than MAX_PATTERN_JUMPS.
70 TooManyJumps
71 };
72
73 /** @brief A compiled pattern: segment bytes and masks with no gap bytes, plus offset, jumps, and anchor. */
74 struct PatternBuffer
75 {
76 /// Pattern byte values. Only entries [0, length) are valid.
77 std::array<std::byte, MAX_PATTERN_BYTES> bytes{};
78 /// Per-byte match mask (0xFF fully known, 0x00 wildcard, 0xF0 high nibble, 0x0F low nibble).
79 std::array<std::byte, MAX_PATTERN_BYTES> mask{};
80 /// Number of valid byte entries: all segments concatenated, gaps excluded.
81 std::size_t length{0};
82 /// Result offset that the `|` marker records, or 0 (the match start) when no marker is present.
83 std::size_t offset{0};
84 /// Index of the rarest fully-known byte in segment 0, or NO_ANCHOR when segment 0 has none.
85 std::size_t anchor{NO_ANCHOR};
86 /// Bounded-jump gaps in ascending position order. Only entries [0, jump_count) are valid.
87 std::array<PatternJump, MAX_PATTERN_JUMPS> jumps{};
88 /// Number of valid jump gaps, 0 for a plain (single-segment) pattern.
89 std::size_t jump_count{0};
90 };
91
92 /** @brief A parse status paired with the buffer it produced. */
93 struct PatternParse
94 {
95 /// Parse outcome.
96 PatternStatus status{PatternStatus::Empty};
97 /// The compiled representation, meaningful only when @ref status is Ok.
98 PatternBuffer buffer{};
99 };
100
101 /// Maps a hex digit to its value 0 to 15, or -1 if @p ch is not a hex digit.
102 1570974 [[nodiscard]] constexpr int hex_digit(char ch) noexcept
103 {
104
3/4
✓ Branch 2 → 3 taken 1570974 times.
✗ Branch 2 → 5 not taken.
✓ Branch 3 → 4 taken 498840 times.
✓ Branch 3 → 5 taken 1072134 times.
1570974 if (ch >= '0' && ch <= '9')
105 {
106 498840 return ch - '0';
107 }
108
3/4
✓ Branch 5 → 6 taken 138908 times.
✓ Branch 5 → 8 taken 933226 times.
✓ Branch 6 → 7 taken 138908 times.
✗ Branch 6 → 8 not taken.
1072134 if (ch >= 'a' && ch <= 'f')
109 {
110 138908 return ch - 'a' + 10;
111 }
112
4/4
✓ Branch 8 → 9 taken 423991 times.
✓ Branch 8 → 11 taken 509235 times.
✓ Branch 9 → 10 taken 423951 times.
✓ Branch 9 → 11 taken 40 times.
933226 if (ch >= 'A' && ch <= 'F')
113 {
114 423951 return ch - 'A' + 10;
115 }
116 509275 return -1;
117 }
118
119 /// True for a character that separates DSL tokens.
120 7587711 [[nodiscard]] constexpr bool is_token_space(char ch) noexcept
121 {
122
9/12
✓ Branch 2 → 3 taken 4732920 times.
✓ Branch 2 → 8 taken 2854791 times.
✓ Branch 3 → 4 taken 4732912 times.
✓ Branch 3 → 8 taken 8 times.
✓ Branch 4 → 5 taken 4732912 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 4732911 times.
✓ Branch 5 → 8 taken 1 time.
✓ Branch 6 → 7 taken 4732911 times.
✗ Branch 6 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 4732911 times.
7587711 return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == '\f' || ch == '\v';
123 }
124
125 /**
126 * @brief Scores how common @p value is in typical x64 .text. A lower score is a rarer byte, and 0 is the rarest.
127 * @details The runtime scan engine and `sighealth` score with this same table.
128 */
129 14967 [[nodiscard]] constexpr std::uint8_t byte_frequency_class(std::uint8_t value) noexcept
130 {
131
13/13
✓ Branch 2 → 3 taken 165 times.
✓ Branch 2 → 4 taken 107 times.
✓ Branch 2 → 5 taken 134 times.
✓ Branch 2 → 6 taken 286 times.
✓ Branch 2 → 7 taken 287 times.
✓ Branch 2 → 8 taken 191 times.
✓ Branch 2 → 9 taken 28 times.
✓ Branch 2 → 10 taken 46 times.
✓ Branch 2 → 11 taken 38 times.
✓ Branch 2 → 12 taken 104 times.
✓ Branch 2 → 13 taken 36 times.
✓ Branch 2 → 14 taken 52 times.
✓ Branch 2 → 15 taken 13493 times.
14967 switch (value)
132 {
133 165 case 0x00:
134 165 return 10;
135 107 case 0xCC:
136 107 return 9;
137 134 case 0x90:
138 134 return 9;
139 286 case 0xFF:
140 286 return 8;
141 287 case 0x48:
142 287 return 8;
143 191 case 0x8B:
144 191 return 7;
145 28 case 0x89:
146 28 return 7;
147 46 case 0x0F:
148 46 return 7;
149 38 case 0xE8:
150 38 return 6;
151 104 case 0xE9:
152 104 return 6;
153 36 case 0x83:
154 36 return 6;
155 52 case 0xC3:
156 52 return 5;
157 13493 default:
158 13493 return 0;
159 }
160 }
161
162 /**
163 * @brief Picks the rarest fully-known byte in segment 0 (the run before the first jump) as the prefilter anchor.
164 * @return Its index (the lowest index on a score tie), or NO_ANCHOR when segment 0 has no fully-known byte.
165 */
166 20623 [[nodiscard]] constexpr std::size_t select_anchor(const PatternBuffer &buffer) noexcept
167 {
168
2/2
✓ Branch 2 → 3 taken 19066 times.
✓ Branch 2 → 5 taken 1557 times.
20623 const std::size_t segment0_end = (buffer.jump_count > 0) ? buffer.jumps[0].position : buffer.length;
169 20623 std::size_t best = NO_ANCHOR;
170 20623 std::uint8_t best_score = 0xFF;
171
2/2
✓ Branch 18 → 7 taken 61450 times.
✓ Branch 18 → 19 taken 8486 times.
69936 for (std::size_t index = 0; index < segment0_end; ++index)
172 {
173
2/2
✓ Branch 8 → 9 taken 48243 times.
✓ Branch 8 → 10 taken 13207 times.
61450 if (buffer.mask[index] != std::byte{0xFF})
174 {
175 48243 continue;
176 }
177 26414 const std::uint8_t score = byte_frequency_class(std::to_integer<std::uint8_t>(buffer.bytes[index]));
178
2/2
✓ Branch 14 → 15 taken 13017 times.
✓ Branch 14 → 17 taken 190 times.
13207 if (score < best_score)
179 {
180 13017 best = index;
181 13017 best_score = score;
182
2/2
✓ Branch 15 → 16 taken 12137 times.
✓ Branch 15 → 17 taken 880 times.
13017 if (score == 0)
183 {
184 12137 break;
185 }
186 }
187 }
188 20623 return best;
189 }
190
191 /** @brief Parse outcome of one `[...]` bounded-jump token: a validity flag plus the resolved skip bounds. */
192 struct JumpParse
193 {
194 /// True when the token was a well-formed, in-range bounded jump.
195 bool ok{false};
196 /// Fewest bytes the gap skips.
197 std::size_t min_skip{0};
198 /// Most bytes the gap skips.
199 std::size_t max_skip{0};
200 };
201
202 /**
203 * @brief Parses a whitespace-stripped bounded-jump token: `[X]` (exact, `[0]` is a valid no-op) or `[X-Y]` (range).
204 * @details It rejects missing brackets, empty or non-decimal content, trailing junk, an inverted range, a bound
205 * above MAX_JUMP_SPAN, and the unbounded `[X-]` form.
206 */
207 125308 [[nodiscard]] constexpr JumpParse parse_jump_token(std::string_view token) noexcept
208 {
209 125308 JumpParse result{};
210
6/8
✓ Branch 3 → 4 taken 125304 times.
✓ Branch 3 → 8 taken 4 times.
✓ Branch 5 → 6 taken 125304 times.
✗ Branch 5 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 125304 times.
✓ Branch 10 → 11 taken 4 times.
✓ Branch 10 → 12 taken 125304 times.
125308 if (token.size() < 3 || token.front() != '[' || token.back() != ']')
211 {
212 4 return result;
213 }
214 125304 const std::string_view inner = token.substr(1, token.size() - 2);
215
216 125304 std::size_t cursor = 0;
217 // A jump always names at least one number.
218
6/8
✓ Branch 15 → 16 taken 125304 times.
✗ Branch 15 → 20 not taken.
✓ Branch 17 → 18 taken 125304 times.
✗ Branch 17 → 20 not taken.
✓ Branch 19 → 20 taken 1 time.
✓ Branch 19 → 21 taken 125303 times.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 24 taken 125303 times.
125304 if (cursor >= inner.size() || inner[cursor] < '0' || inner[cursor] > '9')
219 {
220 1 return result;
221 }
222 125303 std::size_t min_value = 0;
223
7/8
✓ Branch 30 → 31 taken 384174 times.
✓ Branch 30 → 36 taken 63319 times.
✓ Branch 32 → 33 taken 322190 times.
✓ Branch 32 → 36 taken 61984 times.
✓ Branch 34 → 35 taken 322190 times.
✗ Branch 34 → 36 not taken.
✓ Branch 37 → 25 taken 322190 times.
✓ Branch 37 → 38 taken 125303 times.
447493 while (cursor < inner.size() && inner[cursor] >= '0' && inner[cursor] <= '9')
224 {
225 322190 min_value = min_value * 10 + static_cast<std::size_t>(inner[cursor] - '0');
226
1/2
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 322190 times.
322190 if (min_value > MAX_JUMP_SPAN)
227 {
228 // This early exit also keeps the accumulator far from overflow.
229 ✗ return result;
230 }
231 322190 ++cursor;
232 }
233
234 125303 std::size_t max_value = min_value;
235
2/2
✓ Branch 39 → 40 taken 61984 times.
✓ Branch 39 → 67 taken 63319 times.
125303 if (cursor < inner.size())
236 {
237
1/2
✗ Branch 41 → 42 not taken.
✓ Branch 41 → 43 taken 61984 times.
61984 if (inner[cursor] != '-')
238 {
239 ✗ return result;
240 }
241 61984 ++cursor;
242 // A dash with no digits after it is the unbounded `[X-]` form, which the parser rejects.
243
6/8
✓ Branch 44 → 45 taken 61982 times.
✓ Branch 44 → 49 taken 2 times.
✓ Branch 46 → 47 taken 61982 times.
✗ Branch 46 → 49 not taken.
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 50 taken 61982 times.
✓ Branch 51 → 52 taken 2 times.
✓ Branch 51 → 53 taken 61982 times.
61984 if (cursor >= inner.size() || inner[cursor] < '0' || inner[cursor] > '9')
244 {
245 2 return result;
246 }
247 61982 max_value = 0;
248
6/8
✓ Branch 59 → 60 taken 180196 times.
✓ Branch 59 → 65 taken 61981 times.
✓ Branch 61 → 62 taken 180196 times.
✗ Branch 61 → 65 not taken.
✓ Branch 63 → 64 taken 180196 times.
✗ Branch 63 → 65 not taken.
✓ Branch 66 → 54 taken 180196 times.
✓ Branch 66 → 67 taken 61981 times.
242177 while (cursor < inner.size() && inner[cursor] >= '0' && inner[cursor] <= '9')
249 {
250 180196 max_value = max_value * 10 + static_cast<std::size_t>(inner[cursor] - '0');
251
2/2
✓ Branch 55 → 56 taken 1 time.
✓ Branch 55 → 57 taken 180195 times.
180196 if (max_value > MAX_JUMP_SPAN)
252 {
253 1 return result;
254 }
255 180195 ++cursor;
256 }
257 }
258
259
5/6
✓ Branch 68 → 69 taken 125300 times.
✗ Branch 68 → 70 not taken.
✓ Branch 69 → 70 taken 3 times.
✓ Branch 69 → 71 taken 125297 times.
✓ Branch 72 → 73 taken 3 times.
✓ Branch 72 → 74 taken 125297 times.
125300 if (cursor != inner.size() || max_value < min_value)
260 {
261 // Trailing junk after the bounds, or an inverted range.
262 3 return result;
263 }
264 125297 result.ok = true;
265 125297 result.min_skip = min_value;
266 125297 result.max_skip = max_value;
267 125297 return result;
268 }
269
270 /** @brief The fixed-array sink of @ref parse_pattern, capped at MAX_PATTERN_BYTES and MAX_PATTERN_JUMPS. */
271 struct PatternBufferSink
272 {
273 /// The buffer that the parse fills, meaningful only after the parse returns Ok.
274 PatternBuffer buffer{};
275
276 /// Fixed bytes appended so far.
277 559766 [[nodiscard]] constexpr std::size_t length() const noexcept { return buffer.length; }
278 /// Jump gaps appended so far.
279 20624 [[nodiscard]] constexpr std::size_t jump_count() const noexcept { return buffer.jump_count; }
280
281 /// Appends one fixed byte, or returns false at the byte cap (the parser reports TooLong).
282 1276997 [[nodiscard]] constexpr bool add_byte(std::byte value, std::byte mask) noexcept
283 {
284
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 1276995 times.
1276997 if (buffer.length >= MAX_PATTERN_BYTES)
285 {
286 2 return false;
287 }
288 1276995 buffer.bytes[buffer.length] = value;
289 1276995 buffer.mask[buffer.length] = mask;
290 1276995 ++buffer.length;
291 1276995 return true;
292 }
293
294 /// Records a gap, or returns false at the jump cap (the parser reports TooManyJumps).
295 125246 [[nodiscard]] constexpr bool add_jump(std::size_t position, std::size_t min_skip, std::size_t max_skip) noexcept
296 {
297
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 125245 times.
125246 if (buffer.jump_count >= MAX_PATTERN_JUMPS)
298 {
299 1 return false;
300 }
301 125245 buffer.jumps[buffer.jump_count] = PatternJump{position, min_skip, max_skip};
302 125245 ++buffer.jump_count;
303 125245 return true;
304 }
305
306 /// Records the `|` marker position in the fixed byte stream.
307 19087 constexpr void set_offset(std::size_t position) noexcept { buffer.offset = position; }
308 };
309
310 /**
311 * @brief The single AOB DSL grammar, which parses into any storage sink (fixed-array or heap-backed).
312 * @param sink A sink that provides add_byte, add_jump, set_offset, length, and jump_count.
313 * @return On Ok, the sink holds the bytes, mask, jumps, and offset. The caller selects the anchor.
314 * @details Whitespace splits the tokens. Recognized tokens:
315 * - two hex digits (`48`): that byte, mask 0xFF (fully known).
316 * - `??` or `?`: any byte, mask 0x00 (full wildcard).
317 * - hex digit then `?` (`4?`): high nibble known, mask 0xF0.
318 * - `?` then hex digit (`?5`): low nibble known, mask 0x0F.
319 * - `[X]` or `[X-Y]`: a bounded jump that skips exactly X, or X to Y, bytes before the next segment.
320 * - `|`: the offset marker, at most once. The result offset is the position of the next byte, or the
321 * length when the marker trails.
322 *
323 * Any other token fails with InvalidToken. A malformed token that opens with `[` fails with InvalidJump.
324 * An input with only whitespace and at most one `|` fails with Empty. A sink that rejects an append fails
325 * with TooLong (byte cap) or TooManyJumps (jump cap).
326 *
327 * Every segment must be a non-empty fixed run, so a jump cannot lead, trail, or be adjacent to another
328 * jump. A violation is InvalidJump. The `|` marker records a position in the fixed byte stream. When the
329 * pattern also has jumps, the runtime scan engine adds the actual gap bytes at match time.
330 * @note It propagates an exception that the sink throws. `parse_aob` catches the `bad_alloc` of its heap-backed
331 * sink and returns `OutOfMemory`. docs/design/resolution.md owns the noexcept rule.
332 */
333 21566 template <class Sink> [[nodiscard]] constexpr PatternStatus parse_pattern_into(std::string_view dsl, Sink &sink)
334 {
335 21566 bool offset_marked = false;
336
337 // The fixed length at the most recent jump, or 0. A new jump needs a fixed byte after this boundary, which
338 // rejects a leading jump and two adjacent jumps. The end-of-parse check against it rejects a trailing jump.
339 21566 std::size_t last_boundary = 0;
340
341 21566 std::size_t cursor = 0;
342 21566 const std::size_t end = dsl.size();
343
4/4
DetourModKit::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 2832088 times.
✓ Branch 104 → 105 taken 20626 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 33443 times.
✓ Branch 104 → 105 taken 858 times.
2887015 while (cursor < end)
344 {
345
4/4
DetourModKit::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 1410730 times.
✓ Branch 6 → 8 taken 1421358 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 16645 times.
✓ Branch 6 → 8 taken 16798 times.
2865531 if (is_token_space(dsl[cursor]))
346 {
347 1427375 ++cursor;
348 1571768 continue;
349 }
350
351 1438156 const std::size_t token_start = cursor;
352
12/12
DetourModKit::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 4671728 times.
✓ Branch 10 → 15 taken 10609 times.
✓ Branch 13 → 14 taken 3260979 times.
✓ Branch 13 → 15 taken 1410749 times.
✓ Branch 16 → 9 taken 3260979 times.
✓ Branch 16 → 17 taken 1421358 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 50452 times.
✓ Branch 10 → 15 taken 122 times.
✓ Branch 13 → 14 taken 33776 times.
✓ Branch 13 → 15 taken 16676 times.
✓ Branch 16 → 9 taken 33776 times.
✓ Branch 16 → 17 taken 16798 times.
4732911 while (cursor < end && !is_token_space(dsl[cursor]))
353 {
354 3294755 ++cursor;
355 }
356
2/4
DetourModKit::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 1421358 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 16798 times.
✗ Branch 17 → 119 not taken.
1438156 const std::string_view token = dsl.substr(token_start, cursor - token_start);
357
358 // Offset marker.
359
12/12
DetourModKit::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 145915 times.
✓ Branch 19 → 23 taken 1275443 times.
✓ Branch 21 → 22 taken 19088 times.
✓ Branch 21 → 23 taken 126827 times.
✓ Branch 24 → 25 taken 19088 times.
✓ Branch 24 → 30 taken 1402270 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 16780 times.
✓ Branch 21 → 22 taken 15 times.
✓ Branch 21 → 23 taken 3 times.
✓ Branch 24 → 25 taken 15 times.
✓ Branch 24 → 30 taken 16783 times.
1438156 if (token.size() == 1 && token[0] == '|')
360 {
361
4/4
DetourModKit::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 19087 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.
19103 if (offset_marked)
362 {
363 45 return PatternStatus::DuplicateOffset;
364 }
365 19101 offset_marked = true;
366 19101 sink.set_offset(sink.length());
367 19101 continue;
368 }
369
370 // Bounded jump.
371
10/12
DetourModKit::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 1402270 times.
✗ Branch 31 → 35 not taken.
✓ Branch 33 → 34 taken 125255 times.
✓ Branch 33 → 35 taken 1277015 times.
✓ Branch 36 → 37 taken 125255 times.
✓ Branch 36 → 55 taken 1277015 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 16783 times.
✗ Branch 31 → 35 not taken.
✓ Branch 33 → 34 taken 53 times.
✓ Branch 33 → 35 taken 16730 times.
✓ Branch 36 → 37 taken 53 times.
✓ Branch 36 → 55 taken 16730 times.
1419053 if (!token.empty() && token.front() == '[')
372 {
373 125308 const JumpParse jump = parse_jump_token(token);
374
4/4
DetourModKit::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 125248 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.
125308 if (!jump.ok)
375 {
376 16 return PatternStatus::InvalidJump;
377 }
378
12/12
DetourModKit::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 125247 times.
✓ Branch 41 → 44 taken 1 time.
✓ Branch 43 → 44 taken 1 time.
✓ Branch 43 → 45 taken 125246 times.
✓ Branch 46 → 47 taken 2 times.
✓ Branch 46 → 48 taken 125246 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.
125297 if (sink.length() == 0 || sink.length() == last_boundary)
379 {
380 4 return PatternStatus::InvalidJump;
381 }
382
4/6
DetourModKit::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 125245 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.
125293 if (!sink.add_jump(sink.length(), jump.min_skip, jump.max_skip))
383 {
384 1 return PatternStatus::TooManyJumps;
385 }
386 125292 last_boundary = sink.length();
387 125292 continue;
388 125292 }
389
390 1293745 std::byte byte_value{0x00};
391 1293745 std::byte mask_value{0x00};
392
12/12
DetourModKit::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 1150186 times.
✓ Branch 56 → 62 taken 126829 times.
✓ Branch 58 → 59 taken 635768 times.
✓ Branch 58 → 62 taken 514418 times.
✓ Branch 60 → 61 taken 381348 times.
✓ Branch 60 → 62 taken 254420 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 16726 times.
✓ Branch 56 → 62 taken 4 times.
✓ Branch 58 → 59 taken 82 times.
✓ Branch 58 → 62 taken 16644 times.
✓ Branch 60 → 61 taken 77 times.
✓ Branch 60 → 62 taken 5 times.
1293745 const bool double_wildcard = token.size() == 2 && token[0] == '?' && token[1] == '?';
393
8/8
DetourModKit::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 126827 times.
✓ Branch 64 → 68 taken 1150188 times.
✓ Branch 66 → 67 taken 126826 times.
✓ 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 16727 times.
✓ Branch 66 → 67 taken 2 times.
✓ Branch 66 → 68 taken 1 time.
1293745 const bool single_wildcard = token.size() == 1 && token[0] == '?';
394
8/8
DetourModKit::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 895667 times.
✓ Branch 69 → 97 taken 381348 times.
✓ Branch 70 → 71 taken 768841 times.
✓ Branch 70 → 97 taken 126826 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 69 → 70 taken 16653 times.
✓ Branch 69 → 97 taken 77 times.
✓ Branch 70 → 71 taken 16651 times.
✓ Branch 70 → 97 taken 2 times.
1293745 if (double_wildcard || single_wildcard)
395 {
396 // Full wildcard: any byte matches, so both value and mask stay zero.
397 }
398
4/4
DetourModKit::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 768838 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 16649 times.
✓ Branch 72 → 96 taken 2 times.
785492 else if (token.size() == 2)
399 {
400 785487 const int high = hex_digit(token[0]);
401 785487 const int low = hex_digit(token[1]);
402
8/8
DetourModKit::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 514403 times.
✓ Branch 77 → 80 taken 254435 times.
✓ Branch 78 → 79 taken 259600 times.
✓ Branch 78 → 80 taken 254803 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 16639 times.
✓ Branch 77 → 80 taken 10 times.
✓ Branch 78 → 79 taken 16632 times.
✓ Branch 78 → 80 taken 7 times.
785487 if (high >= 0 && low >= 0)
403 {
404 276232 byte_value = static_cast<std::byte>(static_cast<unsigned char>((high << 4) | low));
405 276232 mask_value = std::byte{0xFF};
406 }
407
10/12
DetourModKit::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 254803 times.
✓ Branch 80 → 84 taken 254435 times.
✓ Branch 82 → 83 taken 254803 times.
✗ Branch 82 → 84 not taken.
✓ Branch 85 → 86 taken 254803 times.
✓ Branch 85 → 87 taken 254435 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.
509255 else if (high >= 0 && token[1] == '?')
408 {
409 254810 byte_value = static_cast<std::byte>(static_cast<unsigned char>(high << 4));
410 254810 mask_value = std::byte{0xF0};
411 }
412
10/12
DetourModKit::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 254420 times.
✓ Branch 88 → 91 taken 15 times.
✓ Branch 89 → 90 taken 254420 times.
✗ Branch 89 → 91 not taken.
✓ Branch 92 → 93 taken 254420 times.
✓ Branch 92 → 94 taken 15 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.
254445 else if (token[0] == '?' && low >= 0)
413 {
414 254425 byte_value = static_cast<std::byte>(static_cast<unsigned char>(low));
415 254425 mask_value = std::byte{0x0F};
416 }
417 else
418 {
419 20 return PatternStatus::InvalidToken;
420 }
421 }
422 else
423 {
424 5 return PatternStatus::InvalidToken;
425 }
426
427
5/6
DetourModKit::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 1276995 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 16686 times.
✓ Branch 97 → 119 taken 37 times.
✗ Branch 98 → 99 not taken.
✓ Branch 98 → 100 taken 16686 times.
1293720 if (!sink.add_byte(byte_value, mask_value))
428 {
429 2 return PatternStatus::TooLong;
430 }
431 }
432
433
4/4
DetourModKit::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 20624 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 854 times.
21484 if (sink.length() == 0)
434 {
435 6 return PatternStatus::Empty;
436 }
437
438 // A trailing jump, as in "48 8B [2-5]", leaves last_boundary equal to the fixed length.
439
12/12
DetourModKit::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 19067 times.
✓ Branch 109 → 113 taken 1557 times.
✓ Branch 111 → 112 taken 1 time.
✓ Branch 111 → 113 taken 19066 times.
✓ Branch 114 → 115 taken 1 time.
✓ Branch 114 → 116 taken 20623 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 833 times.
✓ Branch 111 → 112 taken 1 time.
✓ Branch 111 → 113 taken 20 times.
✓ Branch 114 → 115 taken 1 time.
✓ Branch 114 → 116 taken 853 times.
21478 if (sink.jump_count() > 0 && last_boundary == sink.length())
440 {
441 2 return PatternStatus::InvalidJump;
442 }
443
444 21476 return PatternStatus::Ok;
445 }
446
447 /** @brief Parses @p dsl through @ref parse_pattern_into into scan::Pattern storage, then selects its anchor. */
448 20657 [[nodiscard]] constexpr PatternParse parse_pattern(std::string_view dsl) noexcept
449 {
450 20657 PatternParse result{};
451 20657 PatternBufferSink sink{};
452 20657 result.status = parse_pattern_into(dsl, sink);
453
2/2
✓ Branch 3 → 4 taken 20623 times.
✓ Branch 3 → 6 taken 34 times.
20657 if (result.status == PatternStatus::Ok)
454 {
455 20623 result.buffer = sink.buffer;
456 20623 result.buffer.anchor = select_anchor(result.buffer);
457 }
458 20657 return result;
459 }
460
461 /** @brief The fewest bytes any match of @p buffer can occupy: fixed bytes plus every gap's minimum skip. */
462 31 [[nodiscard]] constexpr std::size_t min_match_length(const PatternBuffer &buffer) noexcept
463 {
464 31 std::size_t total = buffer.length;
465
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)
466 {
467 53 total += buffer.jumps[i].min_skip;
468 }
469 31 return total;
470 }
471
472 /**
473 * @brief The fewest physical bytes from the offset-applied result point through the end of any match.
474 * @details Only gaps after the marker position add their minimum skip. A gap at the marker position precedes the
475 * result point.
476 */
477 59 [[nodiscard]] constexpr std::size_t min_match_suffix_length(const PatternBuffer &buffer) noexcept
478 {
479
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 59 times.
59 if (buffer.offset > buffer.length)
480 {
481 ✗ return 0;
482 }
483 59 std::size_t total = buffer.length - buffer.offset;
484
2/2
✓ Branch 10 → 5 taken 6 times.
✓ Branch 10 → 11 taken 59 times.
65 for (std::size_t i = 0; i < buffer.jump_count; ++i)
485 {
486
2/2
✓ Branch 6 → 7 taken 3 times.
✓ Branch 6 → 9 taken 3 times.
6 if (buffer.jumps[i].position > buffer.offset)
487 {
488 3 total += buffer.jumps[i].min_skip;
489 }
490 }
491 59 return total;
492 }
493
494 /** @brief The most bytes any match of @p buffer can occupy: fixed bytes plus every gap's maximum skip. */
495 4 [[nodiscard]] constexpr std::size_t max_match_length(const PatternBuffer &buffer) noexcept
496 {
497 4 std::size_t total = buffer.length;
498
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)
499 {
500 18 total += buffer.jumps[i].max_skip;
501 }
502 4 return total;
503 }
504
505 /**
506 * @brief Masked-compares fixed bytes [@p body_begin, @p body_end) against @p window at offset @p window_pos.
507 * @return True when the run fits in the window and `(memory ^ pattern) & mask == 0` for every byte of the run.
508 */
509 131125 [[nodiscard]] constexpr bool run_matches_at(
510 const PatternBuffer &buffer,
511 std::span<const std::byte> window,
512 std::size_t window_pos,
513 std::size_t body_begin,
514 std::size_t body_end
515 ) noexcept
516 {
517 131125 const std::size_t run_length = body_end - body_begin;
518
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)
519 {
520 1 return false;
521 }
522
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)
523 {
524 const std::byte masked_diff =
525 262294 (window[window_pos + i] ^ buffer.bytes[body_begin + i]) & buffer.mask[body_begin + i];
526
2/2
✓ Branch 18 → 19 taken 130568 times.
✓ Branch 18 → 20 taken 579 times.
131147 if (masked_diff != std::byte{0x00})
527 {
528 130568 return false;
529 }
530 }
531 556 return true;
532 }
533
534 /**
535 * @brief Tests whether segment @p segment_index and every later segment match at @p window_pos. It backtracks.
536 * @param steps The node-visit count for this start position. Start it at 0. See SEGMENT_MATCH_STEP_BUDGET.
537 * @details Recursion depth is at most MAX_PATTERN_JUMPS + 1.
538 */
539 131127 [[nodiscard]] constexpr bool try_segments_at(
540 const PatternBuffer &buffer,
541 std::span<const std::byte> window,
542 std::size_t segment_index,
543 std::size_t window_pos,
544 std::size_t &steps
545 ) noexcept
546 {
547
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 131125 times.
131127 if (++steps > SEGMENT_MATCH_STEP_BUDGET)
548 {
549 2 return false;
550 }
551
552
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;
553 const std::size_t segment_end =
554
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;
555
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))
556 {
557 130569 return false;
558 }
559
2/2
✓ Branch 15 → 16 taken 14 times.
✓ Branch 15 → 17 taken 542 times.
556 if (segment_index == buffer.jump_count)
560 {
561 14 return true;
562 }
563 542 const std::size_t after = window_pos + (segment_end - segment_begin);
564 542 const PatternJump &gap = buffer.jumps[segment_index];
565 542 const std::size_t available = window.size() - after;
566
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)
567 {
568
1/2
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 131103 times.
131103 if (skip > available)
569 {
570 ✗ break;
571 }
572
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))
573 {
574 13 return true;
575 }
576
2/2
✓ Branch 25 → 26 taken 16 times.
✓ Branch 25 → 27 taken 131074 times.
131090 if (steps > SEGMENT_MATCH_STEP_BUDGET)
577 {
578 16 return false;
579 }
580 }
581 513 return false;
582 }
583
584 /**
585 * @brief Tests whether @p buffer, bounded jumps included, matches at the start of @p window.
586 * @return True when a search within SEGMENT_MATCH_STEP_BUDGET finds a placement of every segment and gap that fits
587 * in the window and matches every masked byte.
588 */
589 [[nodiscard]] constexpr bool
590 26 matches_buffer_at(const PatternBuffer &buffer, std::span<const std::byte> window) noexcept
591 {
592
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 26 times.
26 if (buffer.length == 0)
593 {
594 ✗ return false;
595 }
596
2/2
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 8 taken 24 times.
26 if (window.size() < min_match_length(buffer))
597 {
598 2 return false;
599 }
600 24 std::size_t steps = 0;
601 24 return try_segments_at(buffer, window, 0, 0, steps);
602 }
603
604 } // namespace DetourModKit::detail
605
606 #endif // DETOURMODKIT_DETAIL_PATTERN_CORE_HPP
607