GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 90.6% 793 / 0 / 875
Functions: 100.0% 54 / 0 / 54
Branches: 70.1% 932 / 0 / 1329

src/manifest.cpp
Line Branch Exec Source
1 /**
2 * @file manifest.cpp
3 * @brief This TU implements manifest parse and checked serialization over the INI grammar.
4 * @details The INI parser and emitter are confined to this translation unit, so the simpleini dependency never
5 * reaches a consumer's include path. The schema is a versioned `[manifest]` header, one `[sig.<label>]`
6 * section per contract, and ordered `[sig.<label>.rung.<N>]` sub-sections for the candidate ladder.
7 * Signature compile/adopt, the overlay merge, and the trust gate live in the sibling TU
8 * manifest_overlay.cpp over the shared internal/manifest_record_rules.hpp rule set.
9 */
10
11 #include "DetourModKit/manifest.hpp"
12
13 #include "DetourModKit/hook.hpp"
14
15 #include "internal/manifest_grammar.hpp"
16 #include "internal/manifest_record_rules.hpp"
17 #include "internal/scan_shared.hpp"
18 #include "internal/win_file_stream.hpp"
19
20 #include <SimpleIni.h>
21
22 #include <array>
23 #include <charconv>
24 #include <cstddef>
25 #include <cstdint>
26 #include <format>
27 #include <fstream>
28 #include <limits>
29 #include <new>
30 #include <optional>
31 #include <string>
32 #include <string_view>
33 #include <unordered_set>
34 #include <utility>
35
36 namespace DetourModKit::manifest
37 {
38 namespace
39 {
40 // The manifest uses a case-sensitive INI store after the raw prepass rejects exact, whitespace, and ASCII case
41 // collisions. Canonical keys and verbatim labels then load without another case fold.
42 using ManifestIni = CSimpleIniCaseA;
43
44 // The register token table mirrors hook::Gpr one for one. Both omit rsp and rip deliberately. A token maps to a
45 // register and back without a second source of truth.
46 constexpr std::array<std::string_view, 15> GPR_TOKENS =
47 {"rax", "rbx", "rcx", "rdx", "rsi", "rdi", "rbp", "r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15"};
48 static_assert(GPR_TOKENS.size() == static_cast<std::size_t>(hook::Gpr::R15) + 1);
49 static_assert(
50 GPR_TOKENS[static_cast<std::size_t>(hook::Gpr::Rax)] == "rax" &&
51 GPR_TOKENS[static_cast<std::size_t>(hook::Gpr::Rbx)] == "rbx" &&
52 GPR_TOKENS[static_cast<std::size_t>(hook::Gpr::Rcx)] == "rcx" &&
53 GPR_TOKENS[static_cast<std::size_t>(hook::Gpr::Rdx)] == "rdx" &&
54 GPR_TOKENS[static_cast<std::size_t>(hook::Gpr::Rsi)] == "rsi" &&
55 GPR_TOKENS[static_cast<std::size_t>(hook::Gpr::Rdi)] == "rdi" &&
56 GPR_TOKENS[static_cast<std::size_t>(hook::Gpr::Rbp)] == "rbp" &&
57 GPR_TOKENS[static_cast<std::size_t>(hook::Gpr::R8)] == "r8" &&
58 GPR_TOKENS[static_cast<std::size_t>(hook::Gpr::R9)] == "r9" &&
59 GPR_TOKENS[static_cast<std::size_t>(hook::Gpr::R10)] == "r10" &&
60 GPR_TOKENS[static_cast<std::size_t>(hook::Gpr::R11)] == "r11" &&
61 GPR_TOKENS[static_cast<std::size_t>(hook::Gpr::R12)] == "r12" &&
62 GPR_TOKENS[static_cast<std::size_t>(hook::Gpr::R13)] == "r13" &&
63 GPR_TOKENS[static_cast<std::size_t>(hook::Gpr::R14)] == "r14" &&
64 GPR_TOKENS[static_cast<std::size_t>(hook::Gpr::R15)] == "r15"
65 );
66
67 1213 [[nodiscard]] std::string to_lower(std::string_view text)
68 {
69
1/2
✓ Branch 4 → 5 taken 1213 times.
✗ Branch 4 → 25 not taken.
1213 std::string out(text);
70
2/2
✓ Branch 22 → 8 taken 7411 times.
✓ Branch 22 → 23 taken 1213 times.
9837 for (char &c : out)
71 {
72 // Fold ASCII A-Z by hand: std::tolower on a negative char is undefined behavior, and manifest
73 // tokens are ASCII keywords.
74
4/4
✓ Branch 10 → 11 taken 7227 times.
✓ Branch 10 → 13 taken 184 times.
✓ Branch 11 → 12 taken 134 times.
✓ Branch 11 → 13 taken 7093 times.
7411 if (c >= 'A' && c <= 'Z')
75 {
76 134 c = static_cast<char>(c - 'A' + 'a');
77 }
78 }
79 1213 return out;
80 }
81
82 1761 [[nodiscard]] std::string_view trim(std::string_view text) noexcept
83 {
84
5/8
✓ Branch 2 → 3 taken 3522 times.
✓ Branch 2 → 6 taken 2 times.
✓ Branch 3 → 4 taken 3522 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 3522 times.
✗ Branch 4 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 3522 times.
3524 const auto is_space = [](char c) noexcept { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; };
85
5/6
✓ Branch 5 → 6 taken 1763 times.
✗ Branch 5 → 10 not taken.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 1761 times.
✓ Branch 11 → 3 taken 2 times.
✓ Branch 11 → 12 taken 1761 times.
1763 while (!text.empty() && is_space(text.front()))
86 {
87 2 text.remove_prefix(1);
88 }
89
3/6
✓ Branch 15 → 16 taken 1761 times.
✗ Branch 15 → 20 not taken.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 1761 times.
✗ Branch 21 → 13 not taken.
✓ Branch 21 → 22 taken 1761 times.
1761 while (!text.empty() && is_space(text.back()))
90 {
91 text.remove_suffix(1);
92 }
93 1761 return text;
94 }
95
96 // The whole token must match. A garbage suffix such as "0x1G" or "12abc" causes rejection instead of prefix
97 // truncation. Magnitude is parsed unsigned then signed at the end so a value like INT64_MIN (whose magnitude
98 // does not fit a signed type) still round-trips.
99
100 825 [[nodiscard]] std::optional<unsigned long long> parse_magnitude(std::string_view body) noexcept
101 {
102 825 int base = 10;
103
7/10
✓ Branch 3 → 4 taken 610 times.
✓ Branch 3 → 11 taken 215 times.
✓ Branch 5 → 6 taken 67 times.
✓ Branch 5 → 11 taken 543 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 10 taken 67 times.
✗ Branch 9 → 10 not taken.
✗ Branch 9 → 11 not taken.
✓ Branch 12 → 13 taken 67 times.
✓ Branch 12 → 14 taken 758 times.
825 if (body.size() >= 2 && body[0] == '0' && (body[1] == 'x' || body[1] == 'X'))
104 {
105 67 base = 16;
106 67 body.remove_prefix(2);
107 }
108
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 825 times.
825 if (body.empty())
109 {
110 return std::nullopt;
111 }
112 825 unsigned long long value = 0;
113 825 const char *first = body.data();
114 825 const char *last = body.data() + body.size();
115 825 const auto [ptr, ec] = std::from_chars(first, last, value, base);
116
3/4
✓ Branch 21 → 22 taken 823 times.
✓ Branch 21 → 23 taken 2 times.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 823 times.
825 if (ec != std::errc{} || ptr != last)
117 {
118 2 return std::nullopt;
119 }
120 823 return value;
121 }
122
123 646 [[nodiscard]] std::optional<long long> parse_signed(std::string_view token) noexcept
124 {
125 646 token = trim(token);
126
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 646 times.
646 if (token.empty())
127 {
128 return std::nullopt;
129 }
130 646 bool negative = false;
131
5/6
✓ Branch 7 → 8 taken 646 times.
✗ Branch 7 → 10 not taken.
✓ Branch 9 → 10 taken 5 times.
✓ Branch 9 → 11 taken 641 times.
✓ Branch 12 → 13 taken 5 times.
✓ Branch 12 → 15 taken 641 times.
646 if (token.front() == '+' || token.front() == '-')
132 {
133 5 negative = token.front() == '-';
134 5 token.remove_prefix(1);
135 }
136 646 const std::optional<unsigned long long> magnitude = parse_magnitude(token);
137
2/2
✓ Branch 17 → 18 taken 1 time.
✓ Branch 17 → 19 taken 645 times.
646 if (!magnitude)
138 {
139 1 return std::nullopt;
140 }
141
142 645 constexpr unsigned long long MAX_SIGNED =
143 static_cast<unsigned long long>(std::numeric_limits<long long>::max());
144
2/2
✓ Branch 19 → 20 taken 5 times.
✓ Branch 19 → 31 taken 640 times.
645 if (negative)
145 {
146 5 constexpr unsigned long long MIN_MAGNITUDE = MAX_SIGNED + 1ULL;
147
2/2
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 4 times.
5 if (*magnitude > MIN_MAGNITUDE)
148 {
149 1 return std::nullopt;
150 }
151
2/2
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 28 taken 3 times.
4 if (*magnitude == MIN_MAGNITUDE)
152 {
153 1 return std::numeric_limits<long long>::min();
154 }
155 3 return -static_cast<long long>(*magnitude);
156 }
157
2/2
✓ Branch 32 → 33 taken 1 time.
✓ Branch 32 → 34 taken 639 times.
640 if (*magnitude > MAX_SIGNED)
158 {
159 1 return std::nullopt;
160 }
161 639 return static_cast<long long>(*magnitude);
162 }
163
164 179 [[nodiscard]] std::optional<unsigned long long> parse_unsigned(std::string_view token) noexcept
165 {
166 179 token = trim(token);
167
3/6
✓ Branch 4 → 5 taken 179 times.
✗ Branch 4 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 179 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 179 times.
179 if (token.empty() || token.front() == '-')
168 {
169 return std::nullopt;
170 }
171
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 179 times.
179 if (token.front() == '+')
172 {
173 token.remove_prefix(1);
174 }
175 179 return parse_magnitude(token);
176 }
177
178 // Parse an unsigned token that must fit a byte-wide field (value_width, operand_index, byte_width, xmm_index).
179 14 [[nodiscard]] std::optional<std::uint8_t> parse_u8(std::string_view token) noexcept
180 {
181 14 const std::optional<unsigned long long> value = parse_unsigned(token);
182
3/6
✓ Branch 4 → 5 taken 14 times.
✗ Branch 4 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 14 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 13 taken 14 times.
14 if (!value || *value > 0xFFULL)
183 {
184 return std::nullopt;
185 }
186 14 return static_cast<std::uint8_t>(*value);
187 }
188
189 32 [[nodiscard]] std::optional<bool> parse_bool(std::string_view token)
190 {
191
1/2
✓ Branch 3 → 4 taken 32 times.
✗ Branch 3 → 40 not taken.
32 const std::string lowered = to_lower(trim(token));
192
11/18
✓ Branch 4 → 5 taken 32 times.
✗ Branch 4 → 38 not taken.
✓ Branch 5 → 6 taken 16 times.
✓ Branch 5 → 12 taken 16 times.
✓ Branch 6 → 7 taken 16 times.
✗ Branch 6 → 38 not taken.
✓ Branch 7 → 8 taken 16 times.
✗ Branch 7 → 12 not taken.
✓ Branch 8 → 9 taken 16 times.
✗ Branch 8 → 38 not taken.
✓ Branch 9 → 10 taken 16 times.
✗ Branch 9 → 12 not taken.
✓ Branch 10 → 11 taken 16 times.
✗ Branch 10 → 38 not taken.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 16 times.
✓ Branch 14 → 15 taken 16 times.
✓ Branch 14 → 18 taken 16 times.
32 if (lowered == "true" || lowered == "1" || lowered == "yes" || lowered == "on")
193 {
194 16 return true;
195 }
196
3/18
✓ Branch 18 → 19 taken 16 times.
✗ Branch 18 → 38 not taken.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 26 taken 16 times.
✗ Branch 20 → 21 not taken.
✗ Branch 20 → 38 not taken.
✗ Branch 21 → 22 not taken.
✗ Branch 21 → 26 not taken.
✗ Branch 22 → 23 not taken.
✗ Branch 22 → 38 not taken.
✗ Branch 23 → 24 not taken.
✗ Branch 23 → 26 not taken.
✗ Branch 24 → 25 not taken.
✗ Branch 24 → 38 not taken.
✗ Branch 25 → 26 not taken.
✗ Branch 25 → 27 not taken.
✓ Branch 28 → 29 taken 16 times.
✗ Branch 28 → 32 not taken.
16 if (lowered == "false" || lowered == "0" || lowered == "no" || lowered == "off")
197 {
198 16 return false;
199 }
200 return std::nullopt;
201 32 }
202
203 // The enum token maps emit lowercase tokens and accept them case-insensitively for hand edits.
204
205 219 [[nodiscard]] std::string_view anchor_kind_token(anchor::AnchorKind kind) noexcept
206 {
207
6/10
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 10 times.
✓ Branch 2 → 5 taken 3 times.
✓ Branch 2 → 6 taken 18 times.
✓ Branch 2 → 7 taken 2 times.
✓ Branch 2 → 8 taken 184 times.
✗ Branch 2 → 9 not taken.
✗ Branch 2 → 10 not taken.
✗ Branch 2 → 11 not taken.
✗ Branch 2 → 12 not taken.
219 switch (kind)
208 {
209 2 case anchor::AnchorKind::VtableIdentity:
210 2 return "vtable_identity";
211 10 case anchor::AnchorKind::RipGlobal:
212 10 return "rip_global";
213 3 case anchor::AnchorKind::CodeOperand:
214 3 return "code_operand";
215 18 case anchor::AnchorKind::StringXref:
216 18 return "string_xref";
217 2 case anchor::AnchorKind::ExportName:
218 2 return "export_name";
219 184 case anchor::AnchorKind::Manual:
220 184 return "manual";
221 case anchor::AnchorKind::CallArgHome:
222 return "call_arg_home";
223 case anchor::AnchorKind::Quorum:
224 return "quorum";
225 case anchor::AnchorKind::Unset:
226 return "unset";
227 }
228 return "manual";
229 }
230
231 // Accepts only the six serializable kinds. The composite Quorum and resolver-less CallArgHome remain in-code
232 // constructs, so reject their tokens.
233 708 [[nodiscard]] std::optional<anchor::AnchorKind> parse_anchor_kind(std::string_view token)
234 {
235
1/2
✓ Branch 3 → 4 taken 708 times.
✗ Branch 3 → 42 not taken.
708 const std::string lowered = to_lower(trim(token));
236
3/4
✓ Branch 4 → 5 taken 708 times.
✗ Branch 4 → 40 not taken.
✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 9 taken 706 times.
708 if (lowered == "vtable_identity")
237 {
238 2 return anchor::AnchorKind::VtableIdentity;
239 }
240
3/4
✓ Branch 9 → 10 taken 706 times.
✗ Branch 9 → 40 not taken.
✓ Branch 10 → 11 taken 31 times.
✓ Branch 10 → 14 taken 675 times.
706 if (lowered == "rip_global")
241 {
242 31 return anchor::AnchorKind::RipGlobal;
243 }
244
3/4
✓ Branch 14 → 15 taken 675 times.
✗ Branch 14 → 40 not taken.
✓ Branch 15 → 16 taken 5 times.
✓ Branch 15 → 19 taken 670 times.
675 if (lowered == "code_operand")
245 {
246 5 return anchor::AnchorKind::CodeOperand;
247 }
248
3/4
✓ Branch 19 → 20 taken 670 times.
✗ Branch 19 → 40 not taken.
✓ Branch 20 → 21 taken 22 times.
✓ Branch 20 → 24 taken 648 times.
670 if (lowered == "string_xref")
249 {
250 22 return anchor::AnchorKind::StringXref;
251 }
252
3/4
✓ Branch 24 → 25 taken 648 times.
✗ Branch 24 → 40 not taken.
✓ Branch 25 → 26 taken 1 time.
✓ Branch 25 → 29 taken 647 times.
648 if (lowered == "export_name")
253 {
254 1 return anchor::AnchorKind::ExportName;
255 }
256
3/4
✓ Branch 29 → 30 taken 647 times.
✗ Branch 29 → 40 not taken.
✓ Branch 30 → 31 taken 645 times.
✓ Branch 30 → 34 taken 2 times.
647 if (lowered == "manual")
257 {
258 645 return anchor::AnchorKind::Manual;
259 }
260 2 return std::nullopt;
261 708 }
262
263 16 [[nodiscard]] std::string_view scan_mode_token(scan::Mode mode) noexcept
264 {
265
3/5
✓ Branch 2 → 3 taken 11 times.
✓ Branch 2 → 4 taken 3 times.
✗ Branch 2 → 5 not taken.
✓ Branch 2 → 6 taken 2 times.
✗ Branch 2 → 7 not taken.
16 switch (mode)
266 {
267 11 case scan::Mode::Direct:
268 11 return "direct";
269 3 case scan::Mode::RipRelative:
270 3 return "rip_relative";
271 case scan::Mode::RttiVtable:
272 return "rtti_vtable";
273 2 case scan::Mode::StringXref:
274 2 return "string_xref";
275 }
276 return "direct";
277 }
278
279 61 [[nodiscard]] std::optional<scan::Mode> parse_scan_mode(std::string_view token)
280 {
281
1/2
✓ Branch 3 → 4 taken 61 times.
✗ Branch 3 → 32 not taken.
61 const std::string lowered = to_lower(trim(token));
282
3/4
✓ Branch 4 → 5 taken 61 times.
✗ Branch 4 → 30 not taken.
✓ Branch 5 → 6 taken 48 times.
✓ Branch 5 → 9 taken 13 times.
61 if (lowered == "direct")
283 {
284 48 return scan::Mode::Direct;
285 }
286
3/4
✓ Branch 9 → 10 taken 13 times.
✗ Branch 9 → 30 not taken.
✓ Branch 10 → 11 taken 9 times.
✓ Branch 10 → 14 taken 4 times.
13 if (lowered == "rip_relative")
287 {
288 9 return scan::Mode::RipRelative;
289 }
290
2/4
✓ Branch 14 → 15 taken 4 times.
✗ Branch 14 → 30 not taken.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 19 taken 4 times.
4 if (lowered == "rtti_vtable")
291 {
292 return scan::Mode::RttiVtable;
293 }
294
3/4
✓ Branch 19 → 20 taken 4 times.
✗ Branch 19 → 30 not taken.
✓ Branch 20 → 21 taken 3 times.
✓ Branch 20 → 24 taken 1 time.
4 if (lowered == "string_xref")
295 {
296 3 return scan::Mode::StringXref;
297 }
298 1 return std::nullopt;
299 61 }
300
301 3 [[nodiscard]] std::string_view operand_kind_token(scan::OperandKind kind) noexcept
302 {
303
1/2
✓ Branch 2 → 3 taken 3 times.
✗ Branch 2 → 4 not taken.
3 return kind == scan::OperandKind::MemoryDisplacement ? "memory_displacement" : "immediate";
304 }
305
306 5 [[nodiscard]] std::optional<scan::OperandKind> parse_operand_kind(std::string_view token)
307 {
308
1/2
✓ Branch 3 → 4 taken 5 times.
✗ Branch 3 → 22 not taken.
5 const std::string lowered = to_lower(trim(token));
309
2/4
✓ Branch 4 → 5 taken 5 times.
✗ Branch 4 → 20 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 9 taken 5 times.
5 if (lowered == "immediate")
310 {
311 return scan::OperandKind::Immediate;
312 }
313
2/4
✓ Branch 9 → 10 taken 5 times.
✗ Branch 9 → 20 not taken.
✓ Branch 10 → 11 taken 5 times.
✗ Branch 10 → 14 not taken.
5 if (lowered == "memory_displacement")
314 {
315 5 return scan::OperandKind::MemoryDisplacement;
316 }
317 return std::nullopt;
318 5 }
319
320 20 [[nodiscard]] std::string_view encoding_token(scan::StringEncoding encoding) noexcept
321 {
322
2/2
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 4 taken 17 times.
20 return encoding == scan::StringEncoding::Utf16le ? "utf16le" : "utf8";
323 }
324
325 20 [[nodiscard]] std::optional<scan::StringEncoding> parse_encoding(std::string_view token)
326 {
327
1/2
✓ Branch 3 → 4 taken 20 times.
✗ Branch 3 → 22 not taken.
20 const std::string lowered = to_lower(trim(token));
328
3/4
✓ Branch 4 → 5 taken 20 times.
✗ Branch 4 → 20 not taken.
✓ Branch 5 → 6 taken 15 times.
✓ Branch 5 → 9 taken 5 times.
20 if (lowered == "utf8")
329 {
330 15 return scan::StringEncoding::Utf8;
331 }
332
2/4
✓ Branch 9 → 10 taken 5 times.
✗ Branch 9 → 20 not taken.
✓ Branch 10 → 11 taken 5 times.
✗ Branch 10 → 14 not taken.
5 if (lowered == "utf16le")
333 {
334 5 return scan::StringEncoding::Utf16le;
335 }
336 return std::nullopt;
337 20 }
338
339 2 [[nodiscard]] std::string_view pages_token(scan::Pages pages) noexcept
340 {
341
1/3
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 2 times.
✗ Branch 2 → 5 not taken.
2 switch (pages)
342 {
343 case scan::Pages::Readable:
344 return "readable";
345 2 case scan::Pages::Executable:
346 2 return "executable";
347 }
348 return "invalid";
349 }
350
351 2 [[nodiscard]] std::optional<scan::Pages> parse_pages(std::string_view token)
352 {
353
1/2
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 22 not taken.
2 const std::string lowered = to_lower(trim(token));
354
2/4
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 20 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 9 taken 2 times.
2 if (lowered == "readable")
355 {
356 return scan::Pages::Readable;
357 }
358
3/4
✓ Branch 9 → 10 taken 2 times.
✗ Branch 9 → 20 not taken.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 14 taken 1 time.
2 if (lowered == "executable")
359 {
360 1 return scan::Pages::Executable;
361 }
362 1 return std::nullopt;
363 2 }
364
365 20 [[nodiscard]] std::string_view xref_return_token(scan::XrefReturn mode) noexcept
366 {
367
2/4
✓ Branch 2 → 3 taken 18 times.
✓ Branch 2 → 4 taken 2 times.
✗ Branch 2 → 5 not taken.
✗ Branch 2 → 6 not taken.
20 switch (mode)
368 {
369 18 case scan::XrefReturn::ReferencingInstruction:
370 18 return "instruction";
371 2 case scan::XrefReturn::EnclosingFunction:
372 2 return "function";
373 case scan::XrefReturn::StringPointerSlot:
374 return "pointer_slot";
375 }
376 return "instruction";
377 }
378
379 16 [[nodiscard]] std::optional<scan::XrefReturn> parse_xref_return(std::string_view token)
380 {
381
1/2
✓ Branch 3 → 4 taken 16 times.
✗ Branch 3 → 27 not taken.
16 const std::string lowered = to_lower(trim(token));
382
3/4
✓ Branch 4 → 5 taken 16 times.
✗ Branch 4 → 25 not taken.
✓ Branch 5 → 6 taken 15 times.
✓ Branch 5 → 9 taken 1 time.
16 if (lowered == "instruction")
383 {
384 15 return scan::XrefReturn::ReferencingInstruction;
385 }
386
2/4
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 25 not taken.
✓ Branch 10 → 11 taken 1 time.
✗ Branch 10 → 14 not taken.
1 if (lowered == "function")
387 {
388 1 return scan::XrefReturn::EnclosingFunction;
389 }
390 if (lowered == "pointer_slot")
391 {
392 return scan::XrefReturn::StringPointerSlot;
393 }
394 return std::nullopt;
395 16 }
396
397 84 [[nodiscard]] std::optional<BindingKind> parse_binding_kind(std::string_view token)
398 {
399
1/2
✓ Branch 3 → 4 taken 84 times.
✗ Branch 3 → 32 not taken.
84 const std::string lowered = to_lower(trim(token));
400
3/4
✓ Branch 4 → 5 taken 84 times.
✗ Branch 4 → 30 not taken.
✓ Branch 5 → 6 taken 76 times.
✓ Branch 5 → 9 taken 8 times.
84 if (lowered == "address")
401 {
402 76 return BindingKind::Address;
403 }
404
3/4
✓ Branch 9 → 10 taken 8 times.
✗ Branch 9 → 30 not taken.
✓ Branch 10 → 11 taken 3 times.
✓ Branch 10 → 14 taken 5 times.
8 if (lowered == "pointer_chain")
405 {
406 3 return BindingKind::PointerChain;
407 }
408
3/4
✓ Branch 14 → 15 taken 5 times.
✗ Branch 14 → 30 not taken.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 19 taken 4 times.
5 if (lowered == "mid_hook_register")
409 {
410 1 return BindingKind::MidHookRegister;
411 }
412
3/4
✓ Branch 19 → 20 taken 4 times.
✗ Branch 19 → 30 not taken.
✓ Branch 20 → 21 taken 3 times.
✓ Branch 20 → 24 taken 1 time.
4 if (lowered == "vmt_method")
413 {
414 3 return BindingKind::VmtMethod;
415 }
416 1 return std::nullopt;
417 84 }
418
419 3 [[nodiscard]] std::optional<hook::Gpr> parse_gpr(std::string_view token)
420 {
421
1/2
✓ Branch 3 → 4 taken 3 times.
✗ Branch 3 → 22 not taken.
3 const std::string lowered = to_lower(trim(token));
422
1/2
✓ Branch 15 → 5 taken 5 times.
✗ Branch 15 → 16 not taken.
10 for (std::size_t index = 0; index < GPR_TOKENS.size(); ++index)
423 {
424
2/2
✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 12 taken 2 times.
5 if (lowered == GPR_TOKENS[index])
425 {
426 3 return static_cast<hook::Gpr>(index);
427 }
428 }
429 return std::nullopt;
430 3 }
431
432 2 [[nodiscard]] std::string_view gpr_token(hook::Gpr reg) noexcept
433 {
434 2 const auto index = static_cast<std::size_t>(reg);
435
1/2
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 6 not taken.
2 return index < GPR_TOKENS.size() ? GPR_TOKENS[index] : GPR_TOKENS[0];
436 }
437
438 // Format a signed offset as human-editable hex. Preserve the sign so a negative field offset reads naturally.
439 167 [[nodiscard]] std::string format_signed_hex(long long value)
440 {
441 167 const unsigned long long magnitude =
442
2/2
✓ Branch 2 → 3 taken 5 times.
✓ Branch 2 → 4 taken 162 times.
167 value < 0 ? 0ULL - static_cast<unsigned long long>(value) : static_cast<unsigned long long>(value);
443
2/2
✓ Branch 5 → 6 taken 5 times.
✓ Branch 5 → 8 taken 162 times.
167 if (value < 0)
444 {
445
1/2
✓ Branch 6 → 7 taken 5 times.
✗ Branch 6 → 12 not taken.
5 return std::format("-0x{:X}", magnitude);
446 }
447
1/2
✓ Branch 8 → 9 taken 162 times.
✗ Branch 8 → 13 not taken.
162 return std::format("0x{:X}", magnitude);
448 }
449
450 class ManifestIniBuilder
451 {
452 public:
453 130 ManifestIniBuilder(ManifestIni &ini, const ManifestLimits &limits) noexcept : m_ini(ini), m_limits(limits)
454 {
455 130 }
456
457 366 [[nodiscard]] Result<void> begin_section() noexcept
458 {
459
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 6 taken 365 times.
366 if (m_section_count >= m_limits.max_sections)
460 {
461 1 return fail(ErrorCode::SizeTooLarge, "manifest::serialize_checked");
462 }
463 365 ++m_section_count;
464 365 m_key_count = 0;
465 365 return {};
466 }
467
468 895 [[nodiscard]] Result<void> set(const char *section, const char *key, const char *value)
469 {
470 895 const std::string_view value_view{value};
471
5/6
✓ Branch 3 → 4 taken 894 times.
✓ Branch 3 → 8 taken 1 time.
✓ Branch 5 → 6 taken 894 times.
✗ Branch 5 → 8 not taken.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 14 taken 893 times.
1789 if (m_key_count >= m_limits.max_keys_per_section || value_view.size() > m_limits.max_field_bytes ||
472
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 893 times.
894 value_view.size() > m_limits.max_total_decoded_bytes - m_total_decoded_bytes)
473 {
474 2 return fail(ErrorCode::SizeTooLarge, "manifest::serialize_checked");
475 }
476
4/4
✓ Branch 14 → 15 taken 866 times.
✓ Branch 14 → 23 taken 27 times.
✓ Branch 15 → 16 taken 17 times.
✓ Branch 15 → 19 taken 849 times.
893 if (m_ini.SetValue(section, key, value) < 0)
477 {
478 17 return fail(ErrorCode::OutOfMemory, "manifest::serialize_checked");
479 }
480 849 ++m_key_count;
481 849 m_total_decoded_bytes += value_view.size();
482 849 return {};
483 }
484
485 private:
486 ManifestIni &m_ini;
487 const ManifestLimits &m_limits;
488 std::size_t m_section_count{0};
489 std::size_t m_key_count{0};
490 std::size_t m_total_decoded_bytes{0};
491 };
492
493 class BoundedStringWriter final : public ManifestIni::OutputWriter
494 {
495 public:
496 83 BoundedStringWriter(std::string &output, std::size_t max_bytes) noexcept
497 83 : m_output(output), m_max_bytes(max_bytes)
498 {
499 83 }
500
501 3753 void Write(const char *text) override
502 {
503
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 3753 times.
3753 if (m_exceeded)
504 {
505 1 return;
506 }
507 3753 const std::string_view chunk{text};
508
4/4
✓ Branch 7 → 8 taken 3752 times.
✓ Branch 7 → 12 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 16 taken 3752 times.
7505 if (chunk.size() > m_max_bytes - m_output.size() ||
509
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 3752 times.
3752 chunk.size() > m_output.max_size() - m_output.size())
510 {
511 1 m_exceeded = true;
512 1 return;
513 }
514
2/2
✓ Branch 16 → 17 taken 3748 times.
✓ Branch 16 → 20 taken 4 times.
3752 m_output.append(chunk);
515 }
516
517 61 [[nodiscard]] bool exceeded() const noexcept { return m_exceeded; }
518
519 private:
520 std::string &m_output;
521 std::size_t m_max_bytes;
522 bool m_exceeded{false};
523 };
524
525 // Reads one candidate-ladder rung out of its sub-section. Returns nullopt-shaped failure via the Result so a
526 // bad field fails the whole parse closed (a partially-trusted ladder is worse than none).
527 61 [[nodiscard]] Result<CandidateSpec> parse_rung(const ManifestIni &ini, const char *section)
528 {
529 61 CandidateSpec spec;
530
3/4
✓ Branch 3 → 4 taken 61 times.
✗ Branch 3 → 184 not taken.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 60 times.
61 if (const char *name = ini.GetValue(section, "name", nullptr))
531 {
532
1/2
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 184 not taken.
1 spec.name = name;
533 }
534
535
1/2
✓ Branch 6 → 7 taken 61 times.
✗ Branch 6 → 184 not taken.
61 const char *mode_raw = ini.GetValue(section, "mode", nullptr);
536
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 11 taken 61 times.
61 if (mode_raw == nullptr)
537 {
538 return fail(ErrorCode::MalformedLine, "manifest::parse");
539 }
540
1/2
✓ Branch 12 → 13 taken 61 times.
✗ Branch 12 → 175 not taken.
61 const std::optional<scan::Mode> mode = parse_scan_mode(mode_raw);
541
2/2
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 18 taken 60 times.
61 if (!mode)
542 {
543 1 return fail(ErrorCode::MalformedLine, "manifest::parse");
544 }
545 60 spec.mode = *mode;
546
547
2/4
✓ Branch 20 → 21 taken 57 times.
✗ Branch 20 → 107 not taken.
✓ Branch 20 → 114 taken 3 times.
✗ Branch 20 → 171 not taken.
60 switch (*mode)
548 {
549 57 case scan::Mode::Direct:
550 case scan::Mode::RipRelative:
551 {
552
3/4
✓ Branch 21 → 22 taken 57 times.
✗ Branch 21 → 184 not taken.
✓ Branch 22 → 23 taken 56 times.
✓ Branch 22 → 24 taken 1 time.
57 if (const char *pattern = ini.GetValue(section, "pattern", nullptr))
553 {
554
1/2
✓ Branch 23 → 27 taken 56 times.
✗ Branch 23 → 184 not taken.
56 spec.pattern = pattern;
555 }
556 else
557 {
558 1 return fail(ErrorCode::MalformedLine, "manifest::parse");
559 }
560 // Each decode key belongs to the mode that the emitter uses. Reject a key that the active mode does
561 // not emit. Otherwise the next save drops it without an error.
562
3/4
✓ Branch 27 → 28 taken 56 times.
✗ Branch 27 → 184 not taken.
✓ Branch 28 → 29 taken 2 times.
✓ Branch 28 → 43 taken 54 times.
56 if (const char *walk = ini.GetValue(section, "walk_back", nullptr))
563 {
564 2 const std::optional<long long> value = parse_signed(walk);
565
5/6
✓ Branch 32 → 33 taken 1 time.
✓ Branch 32 → 35 taken 1 time.
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 36 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 37 → 41 taken 1 time.
2 if (*mode != scan::Mode::Direct || !value)
566 {
567 1 return fail(ErrorCode::MalformedLine, "manifest::parse");
568 }
569 1 spec.walk_back = static_cast<std::ptrdiff_t>(*value);
570 }
571 55 bool has_displacement = false;
572
3/4
✓ Branch 43 → 44 taken 55 times.
✗ Branch 43 → 184 not taken.
✓ Branch 44 → 45 taken 8 times.
✓ Branch 44 → 59 taken 47 times.
55 if (const char *disp = ini.GetValue(section, "displacement_at", nullptr))
573 {
574 8 const std::optional<long long> value = parse_signed(disp);
575
5/6
✓ Branch 48 → 49 taken 7 times.
✓ Branch 48 → 51 taken 1 time.
✗ Branch 50 → 51 not taken.
✓ Branch 50 → 52 taken 7 times.
✓ Branch 53 → 54 taken 1 time.
✓ Branch 53 → 57 taken 7 times.
8 if (*mode != scan::Mode::RipRelative || !value)
576 {
577 1 return fail(ErrorCode::MalformedLine, "manifest::parse");
578 }
579 7 spec.displacement_at = static_cast<std::ptrdiff_t>(*value);
580 7 has_displacement = true;
581 }
582 54 bool has_instruction_length = false;
583
3/4
✓ Branch 59 → 60 taken 54 times.
✗ Branch 59 → 184 not taken.
✓ Branch 60 → 61 taken 8 times.
✓ Branch 60 → 75 taken 46 times.
54 if (const char *len = ini.GetValue(section, "instruction_length", nullptr))
584 {
585 8 const std::optional<unsigned long long> value = parse_unsigned(len);
586
5/6
✓ Branch 64 → 65 taken 7 times.
✓ Branch 64 → 67 taken 1 time.
✗ Branch 66 → 67 not taken.
✓ Branch 66 → 68 taken 7 times.
✓ Branch 69 → 70 taken 1 time.
✓ Branch 69 → 73 taken 7 times.
8 if (*mode != scan::Mode::RipRelative || !value)
587 {
588 1 return fail(ErrorCode::MalformedLine, "manifest::parse");
589 }
590 7 spec.instruction_length = static_cast<std::size_t>(*value);
591 7 has_instruction_length = true;
592 }
593 // RipRelative requires both decode offsets. A silent zero default shifts the result by the instruction
594 // length, which resolve_and_gate trusts. The disp32 must occupy four bytes before the instruction end,
595 // with a nonnegative offset and a maximum 15-byte instruction. The rung pattern must witness those four
596 // bytes. A Direct rung carries neither field.
597
2/2
✓ Branch 76 → 77 taken 8 times.
✓ Branch 76 → 106 taken 45 times.
53 if (*mode == scan::Mode::RipRelative)
598 {
599
4/4
✓ Branch 77 → 78 taken 7 times.
✓ Branch 77 → 79 taken 1 time.
✓ Branch 78 → 79 taken 1 time.
✓ Branch 78 → 82 taken 6 times.
8 if (!has_instruction_length || !has_displacement)
600 {
601 2 return fail(ErrorCode::MalformedLine, "manifest::parse");
602 }
603
6/6
✓ Branch 82 → 83 taken 5 times.
✓ Branch 82 → 85 taken 1 time.
✓ Branch 84 → 85 taken 2 times.
✓ Branch 84 → 86 taken 3 times.
✓ Branch 87 → 88 taken 3 times.
✓ Branch 87 → 91 taken 3 times.
11 if (spec.displacement_at < 0 || !scan::is_valid_rip_relative_layout(
604 5 static_cast<std::size_t>(spec.displacement_at),
605 spec.instruction_length
606 ))
607 {
608 3 return fail(ErrorCode::MalformedLine, "manifest::parse");
609 }
610 3 const Result<scan::Pattern> pattern = scan::Pattern::compile(spec.pattern);
611
3/4
✓ Branch 94 → 95 taken 3 times.
✗ Branch 94 → 99 not taken.
✓ Branch 100 → 101 taken 1 time.
✓ Branch 100 → 104 taken 2 times.
6 if (pattern &&
612
2/2
✓ Branch 97 → 98 taken 1 time.
✓ Branch 97 → 99 taken 2 times.
3 !rip_pattern_spans_displacement(*pattern, static_cast<std::size_t>(spec.displacement_at)))
613 {
614 1 return fail(ErrorCode::MalformedLine, "manifest::parse");
615 }
616 }
617 47 break;
618 }
619 case scan::Mode::RttiVtable:
620 {
621 if (const char *mangled = ini.GetValue(section, "mangled", nullptr))
622 {
623 spec.mangled = mangled;
624 }
625 else
626 {
627 return fail(ErrorCode::MalformedLine, "manifest::parse");
628 }
629 break;
630 }
631 3 case scan::Mode::StringXref:
632 {
633
2/4
✓ Branch 114 → 115 taken 3 times.
✗ Branch 114 → 184 not taken.
✓ Branch 115 → 116 taken 3 times.
✗ Branch 115 → 117 not taken.
3 if (const char *text = ini.GetValue(section, "string_text", nullptr))
634 {
635
1/2
✓ Branch 116 → 120 taken 3 times.
✗ Branch 116 → 184 not taken.
3 spec.string_text = text;
636 }
637 else
638 {
639 return fail(ErrorCode::MalformedLine, "manifest::parse");
640 }
641
2/4
✓ Branch 120 → 121 taken 3 times.
✗ Branch 120 → 184 not taken.
✓ Branch 121 → 122 taken 3 times.
✗ Branch 121 → 131 not taken.
3 if (const char *encoding = ini.GetValue(section, "string_encoding", nullptr))
642 {
643
1/2
✓ Branch 123 → 124 taken 3 times.
✗ Branch 123 → 176 not taken.
3 const std::optional<scan::StringEncoding> value = parse_encoding(encoding);
644
1/2
✗ Branch 125 → 126 not taken.
✓ Branch 125 → 129 taken 3 times.
3 if (!value)
645 {
646 return fail(ErrorCode::MalformedLine, "manifest::parse");
647 }
648 3 spec.string_encoding = *value;
649 }
650
3/4
✓ Branch 131 → 132 taken 3 times.
✗ Branch 131 → 184 not taken.
✓ Branch 132 → 133 taken 1 time.
✓ Branch 132 → 142 taken 2 times.
3 if (const char *ret = ini.GetValue(section, "string_return", nullptr))
651 {
652
1/2
✓ Branch 134 → 135 taken 1 time.
✗ Branch 134 → 178 not taken.
1 const std::optional<scan::XrefReturn> value = parse_xref_return(ret);
653
1/2
✗ Branch 136 → 137 not taken.
✓ Branch 136 → 140 taken 1 time.
1 if (!value)
654 {
655 return fail(ErrorCode::MalformedLine, "manifest::parse");
656 }
657 1 spec.string_return = *value;
658 }
659
3/4
✓ Branch 142 → 143 taken 3 times.
✗ Branch 142 → 184 not taken.
✓ Branch 143 → 144 taken 1 time.
✓ Branch 143 → 153 taken 2 times.
3 if (const char *term = ini.GetValue(section, "string_require_terminator", nullptr))
660 {
661
1/2
✓ Branch 145 → 146 taken 1 time.
✗ Branch 145 → 180 not taken.
1 const std::optional<bool> value = parse_bool(term);
662
1/2
✗ Branch 147 → 148 not taken.
✓ Branch 147 → 151 taken 1 time.
1 if (!value)
663 {
664 return fail(ErrorCode::MalformedLine, "manifest::parse");
665 }
666 1 spec.string_require_terminator = *value;
667 }
668
3/4
✓ Branch 153 → 154 taken 3 times.
✗ Branch 153 → 184 not taken.
✓ Branch 154 → 155 taken 1 time.
✓ Branch 154 → 164 taken 2 times.
3 if (const char *broad = ini.GetValue(section, "string_broad_match", nullptr))
669 {
670
1/2
✓ Branch 156 → 157 taken 1 time.
✗ Branch 156 → 182 not taken.
1 const std::optional<bool> value = parse_bool(broad);
671
1/2
✗ Branch 158 → 159 not taken.
✓ Branch 158 → 162 taken 1 time.
1 if (!value)
672 {
673 return fail(ErrorCode::MalformedLine, "manifest::parse");
674 }
675 1 spec.string_broad_match = *value;
676 }
677
2/2
✓ Branch 166 → 167 taken 1 time.
✓ Branch 166 → 170 taken 2 times.
3 if (xref_evidence_is_malformed(spec.string_text, spec.string_encoding))
678 {
679 1 return fail(ErrorCode::MalformedLine, "manifest::parse");
680 }
681 2 break;
682 }
683 }
684 49 return spec;
685 61 }
686
687 // Each accepted-key set mirrors serialize_impl. Reject every other key. Otherwise the parser ignores a
688 // hand-edited value and the next save silently drops it.
689 137 [[nodiscard]] bool manifest_header_key_is_read(std::string_view key) noexcept
690 {
691
4/4
✓ Branch 4 → 5 taken 8 times.
✓ Branch 4 → 8 taken 129 times.
✓ Branch 7 → 8 taken 7 times.
✓ Branch 7 → 9 taken 1 time.
137 return key == "schema" || key == "revision";
692 }
693
694 [[nodiscard]] bool
695 1487 record_key_is_read(std::string_view key, anchor::AnchorKind kind, BindingKind binding) noexcept
696 {
697
8/8
✓ Branch 7 → 8 taken 807 times.
✓ Branch 7 → 20 taken 4 times.
✓ Branch 10 → 11 taken 736 times.
✓ Branch 10 → 20 taken 71 times.
✓ Branch 13 → 14 taken 730 times.
✓ Branch 13 → 20 taken 6 times.
✓ Branch 16 → 17 taken 727 times.
✓ Branch 16 → 20 taken 3 times.
3028 if (key == "kind" || key == "module" || key == "binding" || key == "fingerprint" ||
698
6/6
✓ Branch 4 → 5 taken 811 times.
✓ Branch 4 → 20 taken 676 times.
✓ Branch 19 → 20 taken 2 times.
✓ Branch 19 → 21 taken 725 times.
✓ Branch 22 → 23 taken 762 times.
✓ Branch 22 → 24 taken 725 times.
3028 key == "image_identity" || key == "winning_bytes")
699 {
700 762 return true;
701 }
702 // Binding sub-keys belong to the BindingKind that emits them.
703
8/8
✓ Branch 24 → 25 taken 5 times.
✓ Branch 24 → 32 taken 720 times.
✓ Branch 27 → 28 taken 4 times.
✓ Branch 27 → 31 taken 1 time.
✓ Branch 30 → 31 taken 1 time.
✓ Branch 30 → 32 taken 3 times.
✓ Branch 33 → 34 taken 2 times.
✓ Branch 33 → 35 taken 723 times.
725 if (binding == BindingKind::PointerChain && (key == "offsets" || key == "value_width"))
704 {
705 2 return true;
706 }
707
7/8
✓ Branch 35 → 36 taken 2 times.
✓ Branch 35 → 43 taken 721 times.
✓ Branch 38 → 39 taken 1 time.
✓ Branch 38 → 42 taken 1 time.
✗ Branch 41 → 42 not taken.
✓ Branch 41 → 43 taken 1 time.
✓ Branch 44 → 45 taken 1 time.
✓ Branch 44 → 46 taken 722 times.
723 if (binding == BindingKind::MidHookRegister && (key == "read_register" || key == "xmm_index"))
708 {
709 1 return true;
710 }
711
6/6
✓ Branch 46 → 47 taken 4 times.
✓ Branch 46 → 51 taken 718 times.
✓ Branch 49 → 50 taken 2 times.
✓ Branch 49 → 51 taken 2 times.
✓ Branch 52 → 53 taken 2 times.
✓ Branch 52 → 54 taken 720 times.
722 if (binding == BindingKind::VmtMethod && key == "vmt_index")
712 {
713 2 return true;
714 }
715 // The anchor kind scopes each evidence key.
716
6/8
✓ Branch 54 → 55 taken 3 times.
✓ Branch 54 → 58 taken 9 times.
✓ Branch 54 → 71 taken 82 times.
✓ Branch 54 → 90 taken 624 times.
✓ Branch 54 → 93 taken 1 time.
✓ Branch 54 → 96 taken 1 time.
✗ Branch 54 → 99 not taken.
✗ Branch 54 → 100 not taken.
720 switch (kind)
717 {
718 3 case anchor::AnchorKind::VtableIdentity:
719 3 return key == "mangled";
720 9 case anchor::AnchorKind::CodeOperand:
721
5/6
✓ Branch 60 → 61 taken 6 times.
✓ Branch 60 → 67 taken 3 times.
✓ Branch 63 → 64 taken 3 times.
✓ Branch 63 → 67 taken 3 times.
✓ Branch 66 → 67 taken 3 times.
✗ Branch 66 → 68 not taken.
9 return key == "operand_kind" || key == "operand_index" || key == "byte_width";
722 82 case anchor::AnchorKind::StringXref:
723
6/6
✓ Branch 76 → 77 taken 45 times.
✓ Branch 76 → 86 taken 16 times.
✓ Branch 79 → 80 taken 30 times.
✓ Branch 79 → 86 taken 15 times.
✓ Branch 82 → 83 taken 15 times.
✓ Branch 82 → 86 taken 15 times.
173 return key == "xref_text" || key == "xref_encoding" || key == "xref_return" ||
724
3/4
✓ Branch 73 → 74 taken 61 times.
✓ Branch 73 → 86 taken 21 times.
✓ Branch 85 → 86 taken 15 times.
✗ Branch 85 → 87 not taken.
173 key == "xref_require_terminator" || key == "xref_broad_match";
725 624 case anchor::AnchorKind::Manual:
726 624 return key == "manual_value";
727 1 case anchor::AnchorKind::RipGlobal:
728 1 return key == "pages";
729 1 case anchor::AnchorKind::ExportName:
730 1 return key == "export_name";
731 case anchor::AnchorKind::Quorum:
732 case anchor::AnchorKind::CallArgHome:
733 case anchor::AnchorKind::Unset:
734 return false;
735 }
736 return false;
737 }
738
739 109 [[nodiscard]] bool rung_key_is_read(std::string_view key, scan::Mode mode) noexcept
740 {
741
6/6
✓ Branch 4 → 5 taken 60 times.
✓ Branch 4 → 8 taken 49 times.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 59 times.
✓ Branch 10 → 11 taken 50 times.
✓ Branch 10 → 12 taken 59 times.
109 if (key == "mode" || key == "name")
742 {
743 50 return true;
744 }
745
3/5
✓ Branch 12 → 13 taken 46 times.
✓ Branch 12 → 23 taken 6 times.
✗ Branch 12 → 36 not taken.
✓ Branch 12 → 39 taken 7 times.
✗ Branch 12 → 58 not taken.
59 switch (mode)
746 {
747 46 case scan::Mode::Direct:
748
4/4
✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 19 taken 44 times.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 20 taken 1 time.
46 return key == "pattern" || key == "walk_back";
749 6 case scan::Mode::RipRelative:
750
5/6
✓ Branch 25 → 26 taken 4 times.
✓ Branch 25 → 32 taken 2 times.
✓ Branch 28 → 29 taken 2 times.
✓ Branch 28 → 32 taken 2 times.
✓ Branch 31 → 32 taken 2 times.
✗ Branch 31 → 33 not taken.
6 return key == "pattern" || key == "displacement_at" || key == "instruction_length";
751 case scan::Mode::RttiVtable:
752 return key == "mangled";
753 7 case scan::Mode::StringXref:
754
6/6
✓ Branch 44 → 45 taken 3 times.
✓ Branch 44 → 54 taken 2 times.
✓ Branch 47 → 48 taken 2 times.
✓ Branch 47 → 54 taken 1 time.
✓ Branch 50 → 51 taken 1 time.
✓ Branch 50 → 54 taken 1 time.
14 return key == "string_text" || key == "string_encoding" || key == "string_return" ||
755
3/4
✓ Branch 41 → 42 taken 5 times.
✓ Branch 41 → 54 taken 2 times.
✓ Branch 53 → 54 taken 1 time.
✗ Branch 53 → 55 not taken.
14 key == "string_require_terminator" || key == "string_broad_match";
756 }
757 return false;
758 }
759
760 // Parses the persisted winning-span hex value. Every malformed shape fails closed. This prevents equality
761 // between a short baseline and a live-span prefix.
762 6 [[nodiscard]] std::optional<scan::WinningEvidence> parse_winning_bytes(std::string_view text) noexcept
763 {
764
8/8
✓ Branch 3 → 4 taken 5 times.
✓ Branch 3 → 8 taken 1 time.
✓ Branch 5 → 6 taken 4 times.
✓ Branch 5 → 8 taken 1 time.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 3 times.
✓ Branch 10 → 11 taken 3 times.
✓ Branch 10 → 12 taken 3 times.
6 if (text.empty() || (text.size() % 2) != 0 || text.size() > scan::MAX_MUTATION_WITNESS_BYTES * 2)
765 {
766 3 return std::nullopt;
767 }
768 561 const auto nibble = [](char ch, unsigned &out) noexcept -> bool
769 {
770
3/4
✓ Branch 2 → 3 taken 561 times.
✗ Branch 2 → 5 not taken.
✓ Branch 3 → 4 taken 344 times.
✓ Branch 3 → 5 taken 217 times.
561 if (ch >= '0' && ch <= '9')
771 {
772 344 out = static_cast<unsigned>(ch - '0');
773 }
774
3/4
✓ Branch 5 → 6 taken 217 times.
✗ Branch 5 → 8 not taken.
✓ Branch 6 → 7 taken 216 times.
✓ Branch 6 → 8 taken 1 time.
217 else if (ch >= 'a' && ch <= 'f')
775 {
776 216 out = static_cast<unsigned>(ch - 'a') + 10U;
777 }
778
2/4
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 11 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 1 time.
1 else if (ch >= 'A' && ch <= 'F')
779 {
780 out = static_cast<unsigned>(ch - 'A') + 10U;
781 }
782 else
783 {
784 1 return false;
785 }
786 560 return true;
787 };
788
789 3 scan::WinningEvidence evidence{};
790
2/2
✓ Branch 27 → 13 taken 281 times.
✓ Branch 27 → 28 taken 2 times.
283 for (std::size_t i = 0; i < text.size(); i += 2)
791 {
792 281 unsigned hi = 0;
793 281 unsigned lo = 0;
794
5/6
✓ Branch 15 → 16 taken 280 times.
✓ Branch 15 → 19 taken 1 time.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 280 times.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 24 taken 280 times.
281 if (!nibble(text[i], hi) || !nibble(text[i + 1], lo))
795 {
796 1 return std::nullopt;
797 }
798 280 evidence.bytes[i / 2] = static_cast<std::byte>((hi << 4) | lo);
799 }
800 2 evidence.length = static_cast<std::uint16_t>(text.size() / 2);
801 2 return evidence;
802 }
803
804 // Parses the persisted image-identity value `<timestamp_hex>:<size_of_image_hex>:<section_digest_hex>` into a
805 // scan::ImageIdentity, or nullopt when a field is absent, non-hex, over-wide, or exceeds its 32-bit bound.
806 9 [[nodiscard]] std::optional<scan::ImageIdentity> parse_image_identity(std::string_view text) noexcept
807 {
808 25 const auto hex64 = [](std::string_view field, std::uint64_t &out) noexcept -> bool
809 {
810
3/6
✓ Branch 3 → 4 taken 25 times.
✗ Branch 3 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 25 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 25 times.
25 if (field.empty() || field.size() > 16)
811 {
812 return false;
813 }
814 25 std::uint64_t value = 0;
815
2/2
✓ Branch 23 → 12 taken 164 times.
✓ Branch 23 → 24 taken 24 times.
188 for (const char ch : field)
816 {
817 164 std::uint64_t digit = 0;
818
3/4
✓ Branch 12 → 13 taken 164 times.
✗ Branch 12 → 15 not taken.
✓ Branch 13 → 14 taken 105 times.
✓ Branch 13 → 15 taken 59 times.
164 if (ch >= '0' && ch <= '9')
819 {
820 105 digit = static_cast<std::uint64_t>(ch - '0');
821 }
822
3/4
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 18 taken 58 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 1 time.
59 else if (ch >= 'a' && ch <= 'f')
823 {
824 digit = static_cast<std::uint64_t>(ch - 'a') + 10;
825 }
826
3/4
✓ Branch 18 → 19 taken 59 times.
✗ Branch 18 → 21 not taken.
✓ Branch 19 → 20 taken 58 times.
✓ Branch 19 → 21 taken 1 time.
59 else if (ch >= 'A' && ch <= 'F')
827 {
828 58 digit = static_cast<std::uint64_t>(ch - 'A') + 10;
829 }
830 else
831 {
832 1 return false;
833 }
834 163 value = (value << 4) | digit;
835 }
836 24 out = value;
837 24 return true;
838 };
839
840 9 const std::size_t first = text.find(':');
841
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 9 times.
9 if (first == std::string_view::npos)
842 {
843 return std::nullopt;
844 }
845 9 const std::size_t second = text.find(':', first + 1);
846
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 9 times.
9 if (second == std::string_view::npos)
847 {
848 return std::nullopt;
849 }
850 9 std::uint64_t timestamp = 0;
851 9 std::uint64_t size_of_image = 0;
852 9 std::uint64_t digest = 0;
853 9 if (!hex64(text.substr(0, first), timestamp) ||
854
1/2
✓ Branch 13 → 14 taken 8 times.
✗ Branch 13 → 19 not taken.
8 !hex64(text.substr(first + 1, second - first - 1), size_of_image) ||
855
7/10
✓ Branch 10 → 11 taken 8 times.
✓ Branch 10 → 19 taken 1 time.
✓ Branch 16 → 17 taken 8 times.
✗ Branch 16 → 19 not taken.
✓ Branch 17 → 18 taken 8 times.
✗ Branch 17 → 19 not taken.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 8 times.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 8 times.
17 !hex64(text.substr(second + 1), digest) || timestamp > 0xFFFFFFFFULL || size_of_image > 0xFFFFFFFFULL)
856 {
857 1 return std::nullopt;
858 }
859 8 return scan::ImageIdentity{
860 .timestamp = static_cast<std::uint32_t>(timestamp),
861 .size_of_image = static_cast<std::uint32_t>(size_of_image),
862 .section_digest = digest,
863 8 };
864 }
865
866 // Rejects any key in @p section that @p is_read does not recognize, as MalformedLine.
867 template <class Predicate>
868 863 [[nodiscard]] Result<void> reject_unread_keys(const ManifestIni &ini, const char *section, Predicate is_read)
869 {
870 863 ManifestIni::TNamesDepend keys;
871
5/6
std::expected<void, DetourModKit::Error> DetourModKit::manifest::(anonymous namespace)::reject_unread_keys<DetourModKit::manifest::(anonymous namespace)::parse_impl(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::manifest::ManifestLimits const&)::{lambda(std::basic_string_view<char, std::char_traits<char> >)#2}>(CSimpleIniTempl<char, SI_GenericCase<char>, SI_ConvertA<char> > const&, char const*, DetourModKit::manifest::(anonymous namespace)::parse_impl(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::manifest::ManifestLimits const&)::{lambda(std::basic_string_view<char, std::char_traits<char> >)#2}):
✓ Branch 3 → 4 taken 676 times.
✓ Branch 3 → 20 taken 6 times.
std::expected<void, DetourModKit::Error> DetourModKit::manifest::(anonymous namespace)::reject_unread_keys<DetourModKit::manifest::(anonymous namespace)::parse_impl(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::manifest::ManifestLimits const&)::{lambda(std::basic_string_view<char, std::char_traits<char> >)#3}>(CSimpleIniTempl<char, SI_GenericCase<char>, SI_ConvertA<char> > const&, char const*, DetourModKit::manifest::(anonymous namespace)::parse_impl(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::manifest::ManifestLimits const&)::{lambda(std::basic_string_view<char, std::char_traits<char> >)#3}):
✓ Branch 3 → 4 taken 49 times.
✗ Branch 3 → 20 not taken.
std::expected<void, DetourModKit::Error> DetourModKit::manifest::(anonymous namespace)::reject_unread_keys<DetourModKit::manifest::(anonymous namespace)::parse_impl(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::manifest::ManifestLimits const&)::{lambda(std::basic_string_view<char, std::char_traits<char> >)#1}>(CSimpleIniTempl<char, SI_GenericCase<char>, SI_ConvertA<char> > const&, char const*, DetourModKit::manifest::(anonymous namespace)::parse_impl(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::manifest::ManifestLimits const&)::{lambda(std::basic_string_view<char, std::char_traits<char> >)#1}):
✓ Branch 3 → 4 taken 130 times.
✓ Branch 3 → 20 taken 2 times.
863 ini.GetAllKeys(section, keys);
872
6/6
std::expected<void, DetourModKit::Error> DetourModKit::manifest::(anonymous namespace)::reject_unread_keys<DetourModKit::manifest::(anonymous namespace)::parse_impl(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::manifest::ManifestLimits const&)::{lambda(std::basic_string_view<char, std::char_traits<char> >)#2}>(CSimpleIniTempl<char, SI_GenericCase<char>, SI_ConvertA<char> > const&, char const*, DetourModKit::manifest::(anonymous namespace)::parse_impl(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::manifest::ManifestLimits const&)::{lambda(std::basic_string_view<char, std::char_traits<char> >)#2}):
✓ Branch 15 → 6 taken 1487 times.
✓ Branch 15 → 16 taken 674 times.
std::expected<void, DetourModKit::Error> DetourModKit::manifest::(anonymous namespace)::reject_unread_keys<DetourModKit::manifest::(anonymous namespace)::parse_impl(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::manifest::ManifestLimits const&)::{lambda(std::basic_string_view<char, std::char_traits<char> >)#3}>(CSimpleIniTempl<char, SI_GenericCase<char>, SI_ConvertA<char> > const&, char const*, DetourModKit::manifest::(anonymous namespace)::parse_impl(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::manifest::ManifestLimits const&)::{lambda(std::basic_string_view<char, std::char_traits<char> >)#3}):
✓ Branch 15 → 6 taken 109 times.
✓ Branch 15 → 16 taken 48 times.
std::expected<void, DetourModKit::Error> DetourModKit::manifest::(anonymous namespace)::reject_unread_keys<DetourModKit::manifest::(anonymous namespace)::parse_impl(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::manifest::ManifestLimits const&)::{lambda(std::basic_string_view<char, std::char_traits<char> >)#1}>(CSimpleIniTempl<char, SI_GenericCase<char>, SI_ConvertA<char> > const&, char const*, DetourModKit::manifest::(anonymous namespace)::parse_impl(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::manifest::ManifestLimits const&)::{lambda(std::basic_string_view<char, std::char_traits<char> >)#1}):
✓ Branch 15 → 6 taken 137 times.
✓ Branch 15 → 16 taken 129 times.
2584 for (const ManifestIni::Entry &key : keys)
873 {
874
6/6
std::expected<void, DetourModKit::Error> DetourModKit::manifest::(anonymous namespace)::reject_unread_keys<DetourModKit::manifest::(anonymous namespace)::parse_impl(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::manifest::ManifestLimits const&)::{lambda(std::basic_string_view<char, std::char_traits<char> >)#2}>(CSimpleIniTempl<char, SI_GenericCase<char>, SI_ConvertA<char> > const&, char const*, DetourModKit::manifest::(anonymous namespace)::parse_impl(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::manifest::ManifestLimits const&)::{lambda(std::basic_string_view<char, std::char_traits<char> >)#2}):
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 13 taken 1485 times.
std::expected<void, DetourModKit::Error> DetourModKit::manifest::(anonymous namespace)::reject_unread_keys<DetourModKit::manifest::(anonymous namespace)::parse_impl(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::manifest::ManifestLimits const&)::{lambda(std::basic_string_view<char, std::char_traits<char> >)#3}>(CSimpleIniTempl<char, SI_GenericCase<char>, SI_ConvertA<char> > const&, char const*, DetourModKit::manifest::(anonymous namespace)::parse_impl(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::manifest::ManifestLimits const&)::{lambda(std::basic_string_view<char, std::char_traits<char> >)#3}):
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 13 taken 108 times.
std::expected<void, DetourModKit::Error> DetourModKit::manifest::(anonymous namespace)::reject_unread_keys<DetourModKit::manifest::(anonymous namespace)::parse_impl(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::manifest::ManifestLimits const&)::{lambda(std::basic_string_view<char, std::char_traits<char> >)#1}>(CSimpleIniTempl<char, SI_GenericCase<char>, SI_ConvertA<char> > const&, char const*, DetourModKit::manifest::(anonymous namespace)::parse_impl(std::basic_string_view<char, std::char_traits<char> >, DetourModKit::manifest::ManifestLimits const&)::{lambda(std::basic_string_view<char, std::char_traits<char> >)#1}):
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 13 taken 136 times.
1733 if (!is_read(std::string_view{key.pItem}))
875 {
876 4 return fail(ErrorCode::MalformedLine, "manifest::parse");
877 }
878 }
879 851 return {};
880 863 }
881
882 [[nodiscard]] Result<SignatureRecord>
883 709 parse_record(const ManifestIni &ini, const char *section, std::string label)
884 {
885 709 SignatureRecord record;
886 709 record.label = std::move(label);
887
888
1/2
✓ Branch 6 → 7 taken 709 times.
✗ Branch 6 → 325 not taken.
709 const char *kind_raw = ini.GetValue(section, "kind", nullptr);
889
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 11 taken 708 times.
709 if (kind_raw == nullptr)
890 {
891 1 return fail(ErrorCode::MalformedLine, "manifest::parse");
892 }
893
1/2
✓ Branch 12 → 13 taken 708 times.
✗ Branch 12 → 303 not taken.
708 const std::optional<anchor::AnchorKind> kind = parse_anchor_kind(kind_raw);
894
2/2
✓ Branch 14 → 15 taken 2 times.
✓ Branch 14 → 18 taken 706 times.
708 if (!kind)
895 {
896 2 return fail(ErrorCode::MalformedLine, "manifest::parse");
897 }
898 706 record.kind = *kind;
899
900
3/4
✓ Branch 19 → 20 taken 706 times.
✗ Branch 19 → 325 not taken.
✓ Branch 20 → 21 taken 4 times.
✓ Branch 20 → 22 taken 702 times.
706 if (const char *module = ini.GetValue(section, "module", nullptr))
901 {
902
1/2
✓ Branch 21 → 22 taken 4 times.
✗ Branch 21 → 325 not taken.
4 record.module = module;
903 }
904
905
3/4
✓ Branch 22 → 23 taken 706 times.
✗ Branch 22 → 325 not taken.
✓ Branch 23 → 24 taken 84 times.
✓ Branch 23 → 33 taken 622 times.
706 if (const char *binding_raw = ini.GetValue(section, "binding", nullptr))
906 {
907
1/2
✓ Branch 25 → 26 taken 84 times.
✗ Branch 25 → 304 not taken.
84 const std::optional<BindingKind> binding_kind = parse_binding_kind(binding_raw);
908
2/2
✓ Branch 27 → 28 taken 1 time.
✓ Branch 27 → 31 taken 83 times.
84 if (!binding_kind)
909 {
910 1 return fail(ErrorCode::MalformedLine, "manifest::parse");
911 }
912 83 record.binding.kind = *binding_kind;
913 }
914 // Binding keys belong to the kind that emits them. An inert key still enters the drift fingerprint but
915 // never returns to the file. Reject it instead of silent data loss.
916
3/4
✓ Branch 33 → 34 taken 705 times.
✗ Branch 33 → 325 not taken.
✓ Branch 34 → 35 taken 5 times.
✓ Branch 34 → 63 taken 700 times.
705 if (const char *offsets = ini.GetValue(section, "offsets", nullptr))
917 {
918
2/2
✓ Branch 35 → 36 taken 2 times.
✓ Branch 35 → 39 taken 3 times.
5 if (record.binding.kind != BindingKind::PointerChain)
919 {
920 2 return fail(ErrorCode::MalformedLine, "manifest::parse");
921 }
922 3 std::string_view rest = offsets;
923
1/2
✓ Branch 60 → 41 taken 5 times.
✗ Branch 60 → 61 not taken.
5 while (!rest.empty())
924 {
925 5 const std::size_t comma = rest.find(',');
926
1/2
✓ Branch 42 → 43 taken 5 times.
✗ Branch 42 → 308 not taken.
5 const std::string_view token = trim(rest.substr(0, comma));
927
1/2
✓ Branch 45 → 46 taken 5 times.
✗ Branch 45 → 55 not taken.
5 if (!token.empty())
928 {
929 5 const std::optional<long long> value = parse_signed(token);
930
2/2
✓ Branch 48 → 49 taken 1 time.
✓ Branch 48 → 52 taken 4 times.
5 if (!value)
931 {
932 1 return fail(ErrorCode::MalformedLine, "manifest::parse");
933 }
934
1/2
✓ Branch 53 → 54 taken 4 times.
✗ Branch 53 → 306 not taken.
4 record.binding.offsets.push_back(static_cast<std::ptrdiff_t>(*value));
935 }
936
2/2
✓ Branch 55 → 56 taken 2 times.
✓ Branch 55 → 57 taken 2 times.
4 if (comma == std::string_view::npos)
937 {
938 2 break;
939 }
940 2 rest.remove_prefix(comma + 1);
941 }
942 }
943
3/4
✓ Branch 63 → 64 taken 702 times.
✗ Branch 63 → 325 not taken.
✓ Branch 64 → 65 taken 3 times.
✓ Branch 64 → 78 taken 699 times.
702 if (const char *width = ini.GetValue(section, "value_width", nullptr))
944 {
945 3 const std::optional<std::uint8_t> value = parse_u8(width);
946
5/6
✓ Branch 67 → 68 taken 2 times.
✓ Branch 67 → 70 taken 1 time.
✗ Branch 69 → 70 not taken.
✓ Branch 69 → 71 taken 2 times.
✓ Branch 72 → 73 taken 1 time.
✓ Branch 72 → 76 taken 2 times.
3 if (record.binding.kind != BindingKind::PointerChain || !value)
947 {
948 1 return fail(ErrorCode::MalformedLine, "manifest::parse");
949 }
950 2 record.binding.value_width = *value;
951 }
952
3/4
✓ Branch 78 → 79 taken 701 times.
✗ Branch 78 → 325 not taken.
✓ Branch 79 → 80 taken 3 times.
✓ Branch 79 → 93 taken 698 times.
701 if (const char *reg = ini.GetValue(section, "read_register", nullptr))
953 {
954
1/2
✓ Branch 81 → 82 taken 3 times.
✗ Branch 81 → 310 not taken.
3 const std::optional<hook::Gpr> value = parse_gpr(reg);
955
5/6
✓ Branch 82 → 83 taken 1 time.
✓ Branch 82 → 85 taken 2 times.
✗ Branch 84 → 85 not taken.
✓ Branch 84 → 86 taken 1 time.
✓ Branch 87 → 88 taken 2 times.
✓ Branch 87 → 91 taken 1 time.
3 if (record.binding.kind != BindingKind::MidHookRegister || !value)
956 {
957 2 return fail(ErrorCode::MalformedLine, "manifest::parse");
958 }
959 1 record.binding.read_register = *value;
960 }
961
3/4
✓ Branch 93 → 94 taken 699 times.
✗ Branch 93 → 325 not taken.
✓ Branch 94 → 95 taken 1 time.
✓ Branch 94 → 108 taken 698 times.
699 if (const char *xmm = ini.GetValue(section, "xmm_index", nullptr))
962 {
963 1 const std::optional<std::uint8_t> value = parse_u8(xmm);
964
2/6
✗ Branch 97 → 98 not taken.
✓ Branch 97 → 100 taken 1 time.
✗ Branch 99 → 100 not taken.
✗ Branch 99 → 101 not taken.
✓ Branch 102 → 103 taken 1 time.
✗ Branch 102 → 106 not taken.
1 if (record.binding.kind != BindingKind::MidHookRegister || !value)
965 {
966 1 return fail(ErrorCode::MalformedLine, "manifest::parse");
967 }
968 record.binding.xmm_index = *value;
969 }
970
3/4
✓ Branch 108 → 109 taken 698 times.
✗ Branch 108 → 325 not taken.
✓ Branch 109 → 110 taken 4 times.
✓ Branch 109 → 123 taken 694 times.
698 if (const char *vmt = ini.GetValue(section, "vmt_index", nullptr))
971 {
972 4 const std::optional<unsigned long long> value = parse_unsigned(vmt);
973
5/6
✓ Branch 112 → 113 taken 2 times.
✓ Branch 112 → 115 taken 2 times.
✗ Branch 114 → 115 not taken.
✓ Branch 114 → 116 taken 2 times.
✓ Branch 117 → 118 taken 2 times.
✓ Branch 117 → 121 taken 2 times.
4 if (record.binding.kind != BindingKind::VmtMethod || !value)
974 {
975 2 return fail(ErrorCode::MalformedLine, "manifest::parse");
976 }
977 2 record.binding.vmt_index = static_cast<std::size_t>(*value);
978 }
979
980
3/4
✓ Branch 123 → 124 taken 696 times.
✗ Branch 123 → 325 not taken.
✓ Branch 124 → 125 taken 9 times.
✓ Branch 124 → 134 taken 687 times.
696 if (const char *fingerprint = ini.GetValue(section, "fingerprint", nullptr))
981 {
982 9 const std::optional<unsigned long long> value = parse_unsigned(fingerprint);
983
1/2
✗ Branch 128 → 129 not taken.
✓ Branch 128 → 132 taken 9 times.
9 if (!value)
984 {
985 return fail(ErrorCode::MalformedLine, "manifest::parse");
986 }
987 9 record.expected_fingerprint = static_cast<std::uint64_t>(*value);
988 }
989
3/4
✓ Branch 134 → 135 taken 696 times.
✗ Branch 134 → 325 not taken.
✓ Branch 135 → 136 taken 9 times.
✓ Branch 135 → 151 taken 687 times.
696 if (const char *image_identity = ini.GetValue(section, "image_identity", nullptr))
990 {
991 9 const std::optional<scan::ImageIdentity> parsed = parse_image_identity(image_identity);
992
6/6
✓ Branch 139 → 140 taken 8 times.
✓ Branch 139 → 143 taken 1 time.
✓ Branch 142 → 143 taken 2 times.
✓ Branch 142 → 144 taken 6 times.
✓ Branch 145 → 146 taken 3 times.
✓ Branch 145 → 149 taken 6 times.
9 if (!parsed || !parsed->present())
993 {
994 3 return fail(ErrorCode::MalformedLine, "manifest::parse");
995 }
996 6 record.expected_image_identity = *parsed;
997 }
998
3/4
✓ Branch 151 → 152 taken 693 times.
✗ Branch 151 → 325 not taken.
✓ Branch 152 → 153 taken 6 times.
✓ Branch 152 → 168 taken 687 times.
693 if (const char *winning_bytes = ini.GetValue(section, "winning_bytes", nullptr))
999 {
1000 6 const std::optional<scan::WinningEvidence> parsed = parse_winning_bytes(winning_bytes);
1001
5/6
✓ Branch 156 → 157 taken 2 times.
✓ Branch 156 → 160 taken 4 times.
✗ Branch 159 → 160 not taken.
✓ Branch 159 → 161 taken 2 times.
✓ Branch 162 → 163 taken 4 times.
✓ Branch 162 → 166 taken 2 times.
6 if (!parsed || !parsed->present())
1002 {
1003 4 return fail(ErrorCode::MalformedLine, "manifest::parse");
1004 }
1005 2 record.expected_winning_bytes = *parsed;
1006 }
1007
1008
6/8
✓ Branch 168 → 169 taken 2 times.
✓ Branch 168 → 173 taken 5 times.
✓ Branch 168 → 213 taken 22 times.
✓ Branch 168 → 267 taken 632 times.
✓ Branch 168 → 282 taken 27 times.
✓ Branch 168 → 294 taken 1 time.
✗ Branch 168 → 298 not taken.
✗ Branch 168 → 299 not taken.
689 switch (record.kind)
1009 {
1010 2 case anchor::AnchorKind::VtableIdentity:
1011
2/4
✓ Branch 169 → 170 taken 2 times.
✗ Branch 169 → 325 not taken.
✓ Branch 170 → 171 taken 2 times.
✗ Branch 170 → 172 not taken.
2 if (const char *mangled = ini.GetValue(section, "mangled", nullptr))
1012 {
1013
1/2
✓ Branch 171 → 172 taken 2 times.
✗ Branch 171 → 325 not taken.
2 record.mangled = mangled;
1014 }
1015 2 break;
1016 5 case anchor::AnchorKind::CodeOperand:
1017
2/4
✓ Branch 173 → 174 taken 5 times.
✗ Branch 173 → 325 not taken.
✓ Branch 174 → 175 taken 5 times.
✗ Branch 174 → 184 not taken.
5 if (const char *operand_kind = ini.GetValue(section, "operand_kind", nullptr))
1018 {
1019
1/2
✓ Branch 176 → 177 taken 5 times.
✗ Branch 176 → 312 not taken.
5 const std::optional<scan::OperandKind> value = parse_operand_kind(operand_kind);
1020
1/2
✗ Branch 178 → 179 not taken.
✓ Branch 178 → 182 taken 5 times.
5 if (!value)
1021 {
1022 return fail(ErrorCode::MalformedLine, "manifest::parse");
1023 }
1024 5 record.operand_kind = *value;
1025 }
1026
2/4
✓ Branch 184 → 185 taken 5 times.
✗ Branch 184 → 325 not taken.
✓ Branch 185 → 186 taken 5 times.
✗ Branch 185 → 195 not taken.
5 if (const char *index = ini.GetValue(section, "operand_index", nullptr))
1027 {
1028 5 const std::optional<std::uint8_t> value = parse_u8(index);
1029
1/2
✗ Branch 189 → 190 not taken.
✓ Branch 189 → 193 taken 5 times.
5 if (!value)
1030 {
1031 return fail(ErrorCode::MalformedLine, "manifest::parse");
1032 }
1033 5 record.operand_index = *value;
1034 }
1035
2/4
✓ Branch 195 → 196 taken 5 times.
✗ Branch 195 → 325 not taken.
✓ Branch 196 → 197 taken 5 times.
✗ Branch 196 → 212 not taken.
5 if (const char *width = ini.GetValue(section, "byte_width", nullptr))
1036 {
1037 5 const std::optional<std::uint8_t> value = parse_u8(width);
1038
5/6
✓ Branch 200 → 201 taken 5 times.
✗ Branch 200 → 204 not taken.
✓ Branch 203 → 204 taken 2 times.
✓ Branch 203 → 205 taken 3 times.
✓ Branch 206 → 207 taken 2 times.
✓ Branch 206 → 210 taken 3 times.
5 if (!value || !DetourModKit::detail::valid_code_constant_byte_width(*value))
1039 {
1040 2 return fail(ErrorCode::MalformedLine, "manifest::parse");
1041 }
1042 3 record.byte_width = *value;
1043 }
1044 3 break;
1045 22 case anchor::AnchorKind::StringXref:
1046
2/4
✓ Branch 213 → 214 taken 22 times.
✗ Branch 213 → 325 not taken.
✓ Branch 214 → 215 taken 22 times.
✗ Branch 214 → 216 not taken.
22 if (const char *text = ini.GetValue(section, "xref_text", nullptr))
1047 {
1048
1/2
✓ Branch 215 → 216 taken 22 times.
✗ Branch 215 → 325 not taken.
22 record.xref_text = text;
1049 }
1050
3/4
✓ Branch 216 → 217 taken 22 times.
✗ Branch 216 → 325 not taken.
✓ Branch 217 → 218 taken 17 times.
✓ Branch 217 → 227 taken 5 times.
22 if (const char *encoding = ini.GetValue(section, "xref_encoding", nullptr))
1051 {
1052
1/2
✓ Branch 219 → 220 taken 17 times.
✗ Branch 219 → 314 not taken.
17 const std::optional<scan::StringEncoding> value = parse_encoding(encoding);
1053
1/2
✗ Branch 221 → 222 not taken.
✓ Branch 221 → 225 taken 17 times.
17 if (!value)
1054 {
1055 return fail(ErrorCode::MalformedLine, "manifest::parse");
1056 }
1057 17 record.xref_encoding = *value;
1058 }
1059
3/4
✓ Branch 227 → 228 taken 22 times.
✗ Branch 227 → 325 not taken.
✓ Branch 228 → 229 taken 15 times.
✓ Branch 228 → 238 taken 7 times.
22 if (const char *ret = ini.GetValue(section, "xref_return", nullptr))
1060 {
1061
1/2
✓ Branch 230 → 231 taken 15 times.
✗ Branch 230 → 316 not taken.
15 const std::optional<scan::XrefReturn> value = parse_xref_return(ret);
1062
1/2
✗ Branch 232 → 233 not taken.
✓ Branch 232 → 236 taken 15 times.
15 if (!value)
1063 {
1064 return fail(ErrorCode::MalformedLine, "manifest::parse");
1065 }
1066 15 record.xref_return = *value;
1067 }
1068
3/4
✓ Branch 238 → 239 taken 22 times.
✗ Branch 238 → 325 not taken.
✓ Branch 239 → 240 taken 15 times.
✓ Branch 239 → 249 taken 7 times.
22 if (const char *term = ini.GetValue(section, "xref_require_terminator", nullptr))
1069 {
1070
1/2
✓ Branch 241 → 242 taken 15 times.
✗ Branch 241 → 318 not taken.
15 const std::optional<bool> value = parse_bool(term);
1071
1/2
✗ Branch 243 → 244 not taken.
✓ Branch 243 → 247 taken 15 times.
15 if (!value)
1072 {
1073 return fail(ErrorCode::MalformedLine, "manifest::parse");
1074 }
1075 15 record.xref_require_terminator = *value;
1076 }
1077
3/4
✓ Branch 249 → 250 taken 22 times.
✗ Branch 249 → 325 not taken.
✓ Branch 250 → 251 taken 15 times.
✓ Branch 250 → 260 taken 7 times.
22 if (const char *broad = ini.GetValue(section, "xref_broad_match", nullptr))
1078 {
1079
1/2
✓ Branch 252 → 253 taken 15 times.
✗ Branch 252 → 320 not taken.
15 const std::optional<bool> value = parse_bool(broad);
1080
1/2
✗ Branch 254 → 255 not taken.
✓ Branch 254 → 258 taken 15 times.
15 if (!value)
1081 {
1082 return fail(ErrorCode::MalformedLine, "manifest::parse");
1083 }
1084 15 record.xref_broad_match = *value;
1085 }
1086
2/2
✓ Branch 262 → 263 taken 1 time.
✓ Branch 262 → 266 taken 21 times.
22 if (xref_evidence_is_malformed(record.xref_text, record.xref_encoding))
1087 {
1088 1 return fail(ErrorCode::MalformedLine, "manifest::parse");
1089 }
1090 21 break;
1091 632 case anchor::AnchorKind::Manual:
1092 {
1093 // Require manual_value. Without it, the parser silently overlays a trusted Address{0} over the active
1094 // default. An author who means zero writes `manual_value = 0` explicitly.
1095
1/2
✓ Branch 267 → 268 taken 632 times.
✗ Branch 267 → 322 not taken.
632 const char *manual = ini.GetValue(section, "manual_value", nullptr);
1096
2/2
✓ Branch 268 → 269 taken 1 time.
✓ Branch 268 → 272 taken 631 times.
632 if (manual == nullptr)
1097 {
1098 1 return fail(ErrorCode::MalformedLine, "manifest::parse");
1099 }
1100 631 const std::optional<long long> value = parse_signed(manual);
1101
2/2
✓ Branch 275 → 276 taken 2 times.
✓ Branch 275 → 279 taken 629 times.
631 if (!value)
1102 {
1103 2 return fail(ErrorCode::MalformedLine, "manifest::parse");
1104 }
1105 629 record.manual_value = static_cast<std::int64_t>(*value);
1106 629 break;
1107 }
1108 27 case anchor::AnchorKind::RipGlobal:
1109
3/4
✓ Branch 282 → 283 taken 27 times.
✗ Branch 282 → 325 not taken.
✓ Branch 283 → 284 taken 2 times.
✓ Branch 283 → 293 taken 25 times.
27 if (const char *pages = ini.GetValue(section, "pages", nullptr))
1110 {
1111
1/2
✓ Branch 285 → 286 taken 2 times.
✗ Branch 285 → 323 not taken.
2 const std::optional<scan::Pages> value = parse_pages(pages);
1112
2/2
✓ Branch 287 → 288 taken 1 time.
✓ Branch 287 → 291 taken 1 time.
2 if (!value)
1113 {
1114 1 return fail(ErrorCode::MalformedLine, "manifest::parse");
1115 }
1116 1 record.pages = *value;
1117 }
1118 26 break;
1119 1 case anchor::AnchorKind::ExportName:
1120 // record.module already contains the export module. The compile() empty-evidence gate rejects an empty
1121 // export_name. This mirrors the optional StringXref xref_text read here.
1122
2/4
✓ Branch 294 → 295 taken 1 time.
✗ Branch 294 → 325 not taken.
✓ Branch 295 → 296 taken 1 time.
✗ Branch 295 → 297 not taken.
1 if (const char *export_name = ini.GetValue(section, "export_name", nullptr))
1123 {
1124
1/2
✓ Branch 296 → 297 taken 1 time.
✗ Branch 296 → 325 not taken.
1 record.export_name = export_name;
1125 }
1126 1 break;
1127 case anchor::AnchorKind::CallArgHome:
1128 case anchor::AnchorKind::Quorum:
1129 case anchor::AnchorKind::Unset:
1130 // CallArgHome / Quorum / Unset are unreachable here because parse_anchor_kind rejects their tokens,
1131 // but they are listed so the switch is exhaustive.
1132 break;
1133 }
1134 682 return record;
1135 709 }
1136 } // namespace
1137
1138 namespace
1139 {
1140 /// Maps the public limits onto the grammar limits, so the reader and writer passes share one map.
1141 278 [[nodiscard]] detail::GrammarLimits to_grammar_limits(const ManifestLimits &limits) noexcept
1142 {
1143 return detail::GrammarLimits{
1144 278 .max_file_bytes = limits.max_file_bytes,
1145 278 .max_sections = limits.max_sections,
1146 278 .max_keys_per_section = limits.max_keys_per_section,
1147 278 .max_records = limits.max_records,
1148 278 .max_rungs_per_record = limits.max_rungs_per_record,
1149 278 .max_field_bytes = limits.max_field_bytes,
1150 278 .max_total_decoded_bytes = limits.max_total_decoded_bytes,
1151 278 };
1152 }
1153
1154 218 [[nodiscard]] Result<Manifest> parse_impl(std::string_view text, const ManifestLimits &limits)
1155 {
1156 // Reject ambiguous grammar and every encoded, structural, field, and aggregate excess before the backend
1157 // allocates its store.
1158
4/4
✓ Branch 3 → 4 taken 201 times.
✓ Branch 3 → 238 taken 17 times.
✓ Branch 5 → 6 taken 49 times.
✓ Branch 5 → 10 taken 152 times.
218 DMK_TRY_VOID(detail::validate_manifest_grammar(text, to_grammar_limits(limits), "manifest::parse"));
1159
1160 152 ManifestIni ini;
1161 152 ini.SetMultiKey(false);
1162 // Read heredoc values as multi-line data so an embedded newline stays within one literal. Otherwise, the
1163 // tail becomes a new key and can inject a spurious `binding =` outside the fingerprint gate. Serialization
1164 // enables the same mode, so the pair round-trips.
1165 152 ini.SetMultiLine(true);
1166 // The backend reports allocation failure as SI_NOMEM instead of an exception. Return typed OutOfMemory
1167 // rather than a generic MalformedLine.
1168
2/2
✓ Branch 15 → 16 taken 139 times.
✓ Branch 15 → 278 taken 13 times.
152 const int load_result = ini.LoadData(text.data(), text.size());
1169
2/2
✓ Branch 16 → 17 taken 2 times.
✓ Branch 16 → 20 taken 137 times.
139 if (load_result == SI_NOMEM)
1170 {
1171 2 return fail(ErrorCode::OutOfMemory, "manifest::parse");
1172 }
1173 // The backend reports data above its size ceiling as SI_FILE. This path occurs only when max_file_bytes
1174 // exceeds that ceiling. Keep the typed size code.
1175
1/2
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 24 taken 137 times.
137 if (load_result == SI_FILE)
1176 {
1177 return fail(ErrorCode::SizeTooLarge, "manifest::parse");
1178 }
1179
1/2
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 28 taken 137 times.
137 if (load_result < 0)
1180 {
1181 return fail(ErrorCode::MalformedLine, "manifest::parse");
1182 }
1183
1184 // The `[manifest]` header both proves this is a manifest (not some unrelated INI) and pins the schema. A
1185 // header omission or a schema this build does not understand fails closed, so a future format is never
1186 // misread under the wrong grammar.
1187
1/2
✓ Branch 28 → 29 taken 137 times.
✗ Branch 28 → 278 not taken.
137 const char *schema_raw = ini.GetValue("manifest", "schema", nullptr);
1188
2/2
✓ Branch 29 → 30 taken 2 times.
✓ Branch 29 → 33 taken 135 times.
137 if (schema_raw == nullptr)
1189 {
1190 2 return fail(ErrorCode::MissingHeader, "manifest::parse");
1191 }
1192 135 const std::optional<unsigned long long> schema = parse_unsigned(schema_raw);
1193
5/6
✓ Branch 36 → 37 taken 135 times.
✗ Branch 36 → 39 not taken.
✓ Branch 38 → 39 taken 1 time.
✓ Branch 38 → 40 taken 134 times.
✓ Branch 41 → 42 taken 1 time.
✓ Branch 41 → 45 taken 134 times.
135 if (!schema || *schema != static_cast<unsigned long long>(SCHEMA_VERSION))
1194 {
1195 1 return fail(ErrorCode::MissingHeader, "manifest::parse");
1196 }
1197
1198 // An absent author contract revision is zero and means unversioned. A present value must parse and fit 32
1199 // bits, or the file fails closed.
1200 134 std::uint32_t revision = 0;
1201
3/4
✓ Branch 45 → 46 taken 134 times.
✗ Branch 45 → 278 not taken.
✓ Branch 46 → 47 taken 9 times.
✓ Branch 46 → 61 taken 125 times.
134 if (const char *revision_raw = ini.GetValue("manifest", "revision", nullptr))
1202 {
1203 9 const std::optional<unsigned long long> parsed_revision = parse_unsigned(revision_raw);
1204
6/6
✓ Branch 50 → 51 taken 8 times.
✓ Branch 50 → 53 taken 1 time.
✓ Branch 52 → 53 taken 1 time.
✓ Branch 52 → 54 taken 7 times.
✓ Branch 55 → 56 taken 2 times.
✓ Branch 55 → 59 taken 7 times.
9 if (!parsed_revision || *parsed_revision > 0xFFFFFFFFULL)
1205 {
1206 2 return fail(ErrorCode::MalformedLine, "manifest::parse");
1207 }
1208 7 revision = static_cast<std::uint32_t>(*parsed_revision);
1209 }
1210 // Reject any unread header key before record traversal. An unknown `[manifest]` key fails closed.
1211
4/4
✓ Branch 61 → 62 taken 130 times.
✓ Branch 61 → 240 taken 2 times.
✓ Branch 63 → 64 taken 1 time.
✓ Branch 63 → 68 taken 129 times.
269 DMK_TRY_VOID(reject_unread_keys(
1212 ini,
1213 "manifest",
1214 [](std::string_view key) { return manifest_header_key_is_read(key); }
1215 ));
1216
1217 129 ManifestIni::TNamesDepend sections;
1218
2/2
✓ Branch 69 → 70 taken 124 times.
✓ Branch 69 → 276 taken 5 times.
129 ini.GetAllSections(sections);
1219 // Emit records in the file's load order, so a round-trip and a hand-diff stay stable.
1220
1/2
✓ Branch 70 → 71 taken 124 times.
✗ Branch 70 → 276 not taken.
124 sections.sort(ManifestIni::Entry::LoadOrder());
1221
1222
2/2
✓ Branch 109 → 73 taken 33680 times.
✓ Branch 109 → 110 taken 122 times.
33802 for (const ManifestIni::Entry &entry : sections)
1223 {
1224 33680 const std::string_view name = entry.pItem;
1225
2/2
✓ Branch 76 → 77 taken 32893 times.
✓ Branch 76 → 78 taken 787 times.
33680 if (!name.starts_with("sig."))
1226 {
1227 33607 continue;
1228 }
1229
1230 787 const std::optional<RungSectionName> rung = parse_rung_section_name(name);
1231
2/2
✓ Branch 80 → 81 taken 714 times.
✓ Branch 80 → 82 taken 73 times.
787 if (!rung)
1232 {
1233 714 continue;
1234 }
1235
1236
1/2
✓ Branch 85 → 86 taken 73 times.
✗ Branch 85 → 241 not taken.
146 const std::string parent{rung->parent};
1237 // Each rung needs a record parent that exists. A parent that is itself a rung has no record. Reject
1238 // that parent instead of silent loss.
1239
7/8
✓ Branch 88 → 89 taken 73 times.
✗ Branch 88 → 244 not taken.
✓ Branch 89 → 90 taken 72 times.
✓ Branch 89 → 94 taken 1 time.
✓ Branch 93 → 94 taken 1 time.
✓ Branch 93 → 95 taken 71 times.
✓ Branch 96 → 97 taken 2 times.
✓ Branch 96 → 100 taken 71 times.
73 if (ini.GetSection(parent.c_str()) == nullptr || parse_rung_section_name(parent).has_value())
1240 {
1241 2 return fail(ErrorCode::MalformedLine, "manifest::parse");
1242 }
1243
2/2
✓ Branch 102 → 103 taken 71 times.
✓ Branch 102 → 106 taken 2 times.
73 }
1244
1245 122 std::vector<SignatureRecord> records;
1246
2/2
✓ Branch 225 → 112 taken 33648 times.
✓ Branch 225 → 226 taken 68 times.
33716 for (const ManifestIni::Entry &entry : sections)
1247 {
1248 33648 const std::string_view name = entry.pItem;
1249
6/6
✓ Branch 115 → 116 taken 757 times.
✓ Branch 115 → 119 taken 32891 times.
✓ Branch 118 → 119 taken 47 times.
✓ Branch 118 → 120 taken 710 times.
✓ Branch 121 → 122 taken 32938 times.
✓ Branch 121 → 123 taken 710 times.
33648 if (!name.starts_with("sig.") || parse_rung_section_name(name).has_value())
1250 {
1251 32938 continue;
1252 }
1253
1254
1/2
✓ Branch 123 → 124 taken 710 times.
✗ Branch 123 → 272 not taken.
710 const std::string_view label = name.substr(4);
1255
2/2
✓ Branch 125 → 126 taken 1 time.
✓ Branch 125 → 129 taken 709 times.
710 if (label.empty())
1256 {
1257 1 return fail(ErrorCode::MalformedLine, "manifest::parse");
1258 }
1259
1260
2/4
✓ Branch 131 → 132 taken 709 times.
✗ Branch 131 → 251 not taken.
✓ Branch 132 → 133 taken 709 times.
✗ Branch 132 → 249 not taken.
709 Result<SignatureRecord> record = parse_record(ini, entry.pItem, std::string(label));
1261
2/2
✓ Branch 136 → 137 taken 27 times.
✓ Branch 136 → 141 taken 682 times.
709 if (!record)
1262 {
1263 27 return std::unexpected(record.error());
1264 }
1265 // Reject any key this record's kind and binding do not read (unknown, or evidence inert for the kind).
1266
4/4
✓ Branch 141 → 142 taken 676 times.
✓ Branch 141 → 255 taken 6 times.
✓ Branch 143 → 144 taken 2 times.
✓ Branch 143 → 148 taken 674 times.
2169 DMK_TRY_VOID(reject_unread_keys(
1267 ini,
1268 entry.pItem,
1269 [&](std::string_view key) { return record_key_is_read(key, record->kind, record->binding.kind); }
1270 ));
1271
1272 // Probe rung sub-sections by name until the first gap. This preserves order despite store enumeration.
1273 // Labels that contain dots still work.
1274 674 std::size_t first_missing_rung = 0;
1275 48 for (;; ++first_missing_rung)
1276 {
1277
1/2
✓ Branch 149 → 150 taken 722 times.
✗ Branch 149 → 256 not taken.
722 const std::string rung_section = std::format("{}.rung.{}", entry.pItem, first_missing_rung);
1278
3/4
✓ Branch 151 → 152 taken 722 times.
✗ Branch 151 → 260 not taken.
✓ Branch 152 → 153 taken 661 times.
✓ Branch 152 → 154 taken 61 times.
722 if (ini.GetValue(rung_section.c_str(), "mode", nullptr) == nullptr)
1279 {
1280 661 break;
1281 }
1282
1/2
✓ Branch 155 → 156 taken 61 times.
✗ Branch 155 → 260 not taken.
61 Result<CandidateSpec> rung = parse_rung(ini, rung_section.c_str());
1283
2/2
✓ Branch 157 → 158 taken 12 times.
✓ Branch 157 → 162 taken 49 times.
61 if (!rung)
1284 {
1285 12 return std::unexpected(rung.error());
1286 }
1287 // Reject any key this rung's mode does not read (unknown, or a decode key inert for the mode).
1288
3/4
✓ Branch 163 → 164 taken 49 times.
✗ Branch 163 → 257 not taken.
✓ Branch 165 → 166 taken 1 time.
✓ Branch 165 → 170 taken 48 times.
158 DMK_TRY_VOID(reject_unread_keys(
1289 ini,
1290 rung_section.c_str(),
1291 [&](std::string_view key) { return rung_key_is_read(key, rung->mode); }
1292 ));
1293
1/2
✓ Branch 174 → 175 taken 48 times.
✗ Branch 174 → 258 not taken.
96 record->ladder.push_back(std::move(*rung));
1294
5/5
✓ Branch 177 → 178 taken 48 times.
✓ Branch 177 → 179 taken 13 times.
✓ Branch 181 → 182 taken 48 times.
✓ Branch 181 → 184 taken 661 times.
✓ Branch 181 → 186 taken 13 times.
783 }
1295
1296 // Reject a past-gap orphan or noncanonical index such as `rung.00`. Otherwise, the parser silently
1297 // drops that rung-shaped section.
1298
2/2
✓ Branch 212 → 187 taken 265187 times.
✓ Branch 212 → 213 taken 659 times.
265846 for (const ManifestIni::Entry &maybe_rung_entry : sections)
1299 {
1300 265187 const std::string_view maybe_rung = maybe_rung_entry.pItem;
1301 265187 const std::optional<RungSectionName> rung = parse_rung_section_name(maybe_rung);
1302
4/4
✓ Branch 191 → 192 taken 66 times.
✓ Branch 191 → 203 taken 265121 times.
✓ Branch 194 → 195 taken 50 times.
✓ Branch 194 → 203 taken 16 times.
265237 if (rung && rung->parent == name &&
1303
2/2
✓ Branch 196 → 197 taken 49 times.
✓ Branch 196 → 202 taken 1 time.
50 (rung->index >= first_missing_rung ||
1304
7/10
✓ Branch 198 → 199 taken 49 times.
✗ Branch 198 → 263 not taken.
✓ Branch 201 → 202 taken 1 time.
✓ Branch 201 → 203 taken 48 times.
✓ Branch 204 → 205 taken 49 times.
✓ Branch 204 → 206 taken 265138 times.
✓ Branch 206 → 207 taken 2 times.
✓ Branch 206 → 210 taken 265185 times.
✗ Branch 263 → 264 not taken.
✗ Branch 263 → 265 not taken.
265236 maybe_rung != std::format("{}.rung.{}", name, rung->index)))
1305 {
1306 2 return fail(ErrorCode::MalformedLine, "manifest::parse");
1307 }
1308 }
1309
1310
2/2
✓ Branch 216 → 217 taken 656 times.
✓ Branch 216 → 270 taken 3 times.
1318 records.push_back(std::move(*record));
1311
2/2
✓ Branch 219 → 220 taken 656 times.
✓ Branch 219 → 222 taken 44 times.
709 }
1312 68 return Manifest{
1313 68 .header = {.schema = static_cast<std::uint32_t>(*schema), .revision = revision},
1314 68 .records = std::move(records),
1315 68 };
1316 175 }
1317 } // namespace
1318
1319 218 Result<Manifest> parse(std::string_view text, const ManifestLimits &limits)
1320 {
1321 // Every materialization stage can throw std::bad_alloc. Convert it to a typed atomic failure. No partial
1322 // manifest escapes, and the caller's trusted generation remains untouched.
1323 try
1324 {
1325
2/2
✓ Branch 2 → 3 taken 172 times.
✓ Branch 2 → 5 taken 46 times.
218 return parse_impl(text, limits);
1326 }
1327
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 46 times.
46 catch (const std::bad_alloc &)
1328 {
1329 46 return fail(ErrorCode::OutOfMemory, "manifest::parse");
1330 46 }
1331 }
1332
1333 namespace
1334 {
1335 192 [[nodiscard]] Result<std::string> serialize_impl(const Manifest &manifest, const ManifestLimits &limits)
1336 {
1337 // Validate fields and enums before insertion into the bounded INI store. The builder checks every section,
1338 // key, and decoded value before insertion. The output writer caps encoded bytes at emission.
1339
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 7 taken 191 times.
192 if (manifest.records.size() > limits.max_records)
1340 {
1341 1 return fail(ErrorCode::SizeTooLarge, "manifest::serialize_checked");
1342 }
1343 1722 const auto field_exceeds_limit = [&limits](std::string_view field) noexcept
1344 1722 { return field.size() > limits.max_field_bytes; };
1345 191 std::unordered_set<std::string> seen_labels;
1346
2/2
✓ Branch 159 → 10 taken 319 times.
✓ Branch 159 → 160 taken 130 times.
640 for (const SignatureRecord &record : manifest.records)
1347 {
1348
2/4
✓ Branch 17 → 18 taken 318 times.
✗ Branch 17 → 27 not taken.
✓ Branch 20 → 21 taken 318 times.
✗ Branch 20 → 27 not taken.
955 if (field_exceeds_limit(record.label) || field_exceeds_limit(record.module) ||
1349
7/8
✓ Branch 14 → 15 taken 318 times.
✓ Branch 14 → 27 taken 1 time.
✓ Branch 23 → 24 taken 317 times.
✓ Branch 23 → 27 taken 1 time.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 317 times.
✓ Branch 29 → 30 taken 2 times.
✓ Branch 29 → 33 taken 317 times.
1272 field_exceeds_limit(record.mangled) || field_exceeds_limit(record.xref_text) ||
1350 317 field_exceeds_limit(record.export_name))
1351 {
1352 2 return fail(ErrorCode::SizeTooLarge, "manifest::serialize_checked");
1353 }
1354
5/6
✓ Branch 34 → 35 taken 317 times.
✗ Branch 34 → 749 not taken.
✓ Branch 38 → 39 taken 310 times.
✓ Branch 38 → 59 taken 3 times.
✓ Branch 41 → 42 taken 307 times.
✓ Branch 41 → 59 taken 3 times.
940 if (!label_is_serializable(record.label) || value_is_unserializable(record.module) ||
1355
4/4
✓ Branch 44 → 45 taken 305 times.
✓ Branch 44 → 59 taken 2 times.
✓ Branch 47 → 48 taken 302 times.
✓ Branch 47 → 59 taken 3 times.
922 value_is_unserializable(record.mangled) || value_is_unserializable(record.xref_text) ||
1356
2/2
✓ Branch 50 → 51 taken 301 times.
✓ Branch 50 → 59 taken 1 time.
607 value_is_unserializable(record.export_name) ||
1357 302 xref_evidence_is_malformed(record.xref_text, record.xref_encoding) ||
1358
4/4
✓ Branch 52 → 53 taken 288 times.
✓ Branch 52 → 59 taken 13 times.
✓ Branch 54 → 55 taken 286 times.
✓ Branch 54 → 59 taken 2 times.
301 !record_policy_domains_are_valid(record) || !binding_structure_is_valid(record.binding) ||
1359
6/6
✓ Branch 35 → 36 taken 313 times.
✓ Branch 35 → 59 taken 4 times.
✓ Branch 56 → 57 taken 285 times.
✓ Branch 56 → 59 taken 1 time.
✓ Branch 61 → 62 taken 35 times.
✓ Branch 61 → 65 taken 282 times.
915 !image_identity_is_valid(record.expected_image_identity) ||
1360
2/2
✓ Branch 58 → 59 taken 3 times.
✓ Branch 58 → 60 taken 282 times.
285 !winning_bytes_are_valid(record.expected_winning_bytes))
1361 {
1362 35 return fail(ErrorCode::InvalidArg, "manifest::serialize_checked");
1363 }
1364
5/6
✓ Branch 66 → 67 taken 282 times.
✗ Branch 66 → 747 not taken.
✓ Branch 67 → 68 taken 279 times.
✓ Branch 67 → 745 taken 3 times.
✓ Branch 69 → 70 taken 3 times.
✓ Branch 69 → 73 taken 276 times.
285 if (!seen_labels.insert(to_lower(record.label)).second)
1365 {
1366 3 return fail(ErrorCode::ManifestIdentityCollision, "manifest::serialize_checked");
1367 }
1368
2/2
✓ Branch 74 → 75 taken 1 time.
✓ Branch 74 → 78 taken 275 times.
276 if (record.ladder.size() > limits.max_rungs_per_record)
1369 {
1370 1 return fail(ErrorCode::SizeTooLarge, "manifest::serialize_checked");
1371 }
1372
2/2
✓ Branch 148 → 80 taken 33 times.
✓ Branch 148 → 149 taken 258 times.
566 for (const CandidateSpec &spec : record.ladder)
1373 {
1374
2/4
✓ Branch 87 → 88 taken 33 times.
✗ Branch 87 → 94 not taken.
✓ Branch 90 → 91 taken 33 times.
✗ Branch 90 → 94 not taken.
99 if (field_exceeds_limit(spec.name) || field_exceeds_limit(spec.pattern) ||
1375
3/6
✓ Branch 84 → 85 taken 33 times.
✗ Branch 84 → 94 not taken.
✗ Branch 93 → 94 not taken.
✓ Branch 93 → 95 taken 33 times.
✗ Branch 96 → 97 not taken.
✓ Branch 96 → 100 taken 33 times.
99 field_exceeds_limit(spec.mangled) || field_exceeds_limit(spec.string_text))
1376 {
1377 return fail(ErrorCode::SizeTooLarge, "manifest::serialize_checked");
1378 }
1379
4/4
✓ Branch 105 → 106 taken 27 times.
✓ Branch 105 → 115 taken 3 times.
✓ Branch 108 → 109 taken 24 times.
✓ Branch 108 → 115 taken 3 times.
90 if (value_is_unserializable(spec.name) || value_is_unserializable(spec.pattern) ||
1380
8/8
✓ Branch 102 → 103 taken 30 times.
✓ Branch 102 → 115 taken 3 times.
✓ Branch 111 → 112 taken 21 times.
✓ Branch 111 → 115 taken 3 times.
✓ Branch 114 → 115 taken 1 time.
✓ Branch 114 → 116 taken 20 times.
✓ Branch 117 → 118 taken 13 times.
✓ Branch 117 → 121 taken 20 times.
111 value_is_unserializable(spec.mangled) || value_is_unserializable(spec.string_text) ||
1381 21 xref_evidence_is_malformed(spec.string_text, spec.string_encoding))
1382 {
1383 13 return fail(ErrorCode::InvalidArg, "manifest::serialize_checked");
1384 }
1385 // save() truncates its destination first. Reject a rung that parse_rung's RipRelative gate refuses.
1386 // This preserves the last-known-good file because load() cannot accept the invalid replacement.
1387
2/2
✓ Branch 121 → 122 taken 7 times.
✓ Branch 121 → 139 taken 13 times.
20 if (spec.mode == scan::Mode::RipRelative)
1388 {
1389 7 const Result<scan::Pattern> pattern = scan::Pattern::compile(spec.pattern);
1390 21 if (spec.displacement_at < 0 ||
1391
2/2
✓ Branch 126 → 127 taken 4 times.
✓ Branch 126 → 132 taken 3 times.
7 !scan::is_valid_rip_relative_layout(
1392 7 static_cast<std::size_t>(spec.displacement_at),
1393 7 spec.instruction_length
1394
4/6
✓ Branch 124 → 125 taken 7 times.
✗ Branch 124 → 132 not taken.
✓ Branch 128 → 129 taken 4 times.
✗ Branch 128 → 133 not taken.
✓ Branch 134 → 135 taken 4 times.
✓ Branch 134 → 138 taken 3 times.
18 ) ||
1395 4 (pattern &&
1396
2/2
✓ Branch 131 → 132 taken 1 time.
✓ Branch 131 → 133 taken 3 times.
4 !rip_pattern_spans_displacement(*pattern, static_cast<std::size_t>(spec.displacement_at))))
1397 {
1398 4 return fail(ErrorCode::InvalidArg, "manifest::serialize_checked");
1399 }
1400 }
1401 }
1402 }
1403
1404 130 ManifestIni ini;
1405 130 ini.SetMultiKey(false);
1406 // Emit a value with an embedded newline or edge whitespace as multi-line heredoc data. parse() enables the
1407 // same mode and reconstructs the value verbatim. This completes newline round-trip. Without it, raw output
1408 // truncates an xref literal at `\n`.
1409 130 ini.SetMultiLine(true);
1410 130 ManifestIniBuilder builder{ini, limits};
1411
1/2
✗ Branch 166 → 168 not taken.
✓ Branch 166 → 172 taken 130 times.
130 DMK_TRY_VOID(builder.begin_section());
1412
4/4
✓ Branch 174 → 175 taken 125 times.
✓ Branch 174 → 750 taken 5 times.
✓ Branch 177 → 178 taken 3 times.
✓ Branch 177 → 182 taken 122 times.
135 DMK_TRY_VOID(builder.set("manifest", "schema", std::to_string(SCHEMA_VERSION).c_str()));
1413 // The revision is the author's contract epoch. Omit an unversioned revision to keep its manifest clean.
1414
2/2
✓ Branch 182 → 183 taken 5 times.
✓ Branch 182 → 194 taken 117 times.
122 if (manifest.header.revision != 0)
1415 {
1416
2/4
✓ Branch 185 → 186 taken 5 times.
✗ Branch 185 → 754 not taken.
✗ Branch 188 → 189 not taken.
✓ Branch 188 → 193 taken 5 times.
5 DMK_TRY_VOID(builder.set("manifest", "revision", std::to_string(manifest.header.revision).c_str()));
1417 }
1418
1419
2/2
✓ Branch 716 → 196 taken 220 times.
✓ Branch 716 → 717 taken 83 times.
425 for (const SignatureRecord &record : manifest.records)
1420 {
1421
1/2
✓ Branch 198 → 199 taken 220 times.
✗ Branch 198 → 758 not taken.
220 const std::string section = std::format("sig.{}", record.label);
1422 220 const char *sec = section.c_str();
1423
2/2
✓ Branch 202 → 203 taken 1 time.
✓ Branch 202 → 207 taken 219 times.
220 DMK_TRY_VOID(builder.begin_section());
1424
1425
5/6
✓ Branch 210 → 211 taken 219 times.
✗ Branch 210 → 761 not taken.
✓ Branch 212 → 213 taken 209 times.
✓ Branch 212 → 759 taken 10 times.
✓ Branch 216 → 217 taken 6 times.
✓ Branch 216 → 221 taken 203 times.
239 DMK_TRY_VOID(builder.set(sec, "kind", std::string(anchor_kind_token(record.kind)).c_str()));
1426
2/2
✓ Branch 222 → 223 taken 4 times.
✓ Branch 222 → 232 taken 199 times.
203 if (!record.module.empty())
1427 {
1428
2/4
✓ Branch 224 → 225 taken 4 times.
✗ Branch 224 → 767 not taken.
✗ Branch 226 → 227 not taken.
✓ Branch 226 → 231 taken 4 times.
4 DMK_TRY_VOID(builder.set(sec, "module", record.module.c_str()));
1429 }
1430
1431
5/6
✓ Branch 235 → 236 taken 203 times.
✗ Branch 235 → 770 not taken.
✓ Branch 237 → 238 taken 197 times.
✓ Branch 237 → 768 taken 6 times.
✓ Branch 241 → 242 taken 4 times.
✓ Branch 241 → 246 taken 193 times.
215 DMK_TRY_VOID(
1432 builder.set(sec, "binding", std::string(binding_kind_to_string(record.binding.kind)).c_str())
1433 );
1434
4/5
✓ Branch 246 → 247 taken 2 times.
✓ Branch 246 → 299 taken 2 times.
✓ Branch 246 → 326 taken 2 times.
✓ Branch 246 → 337 taken 187 times.
✗ Branch 246 → 338 not taken.
193 switch (record.binding.kind)
1435 {
1436 2 case BindingKind::PointerChain:
1437 {
1438 2 std::string offsets;
1439
2/2
✓ Branch 275 → 249 taken 4 times.
✓ Branch 275 → 276 taken 2 times.
6 for (std::size_t index = 0; index < record.binding.offsets.size(); ++index)
1440 {
1441 const std::string token =
1442
1/2
✓ Branch 250 → 251 taken 4 times.
✗ Branch 250 → 778 not taken.
4 format_signed_hex(static_cast<long long>(record.binding.offsets[index]));
1443
2/2
✓ Branch 251 → 252 taken 2 times.
✓ Branch 251 → 253 taken 2 times.
4 const std::size_t separator_bytes = index == 0 ? 0 : 2;
1444
2/4
✓ Branch 255 → 256 taken 4 times.
✗ Branch 255 → 259 not taken.
✗ Branch 261 → 262 not taken.
✓ Branch 261 → 265 taken 4 times.
8 if (separator_bytes > limits.max_field_bytes - offsets.size() ||
1445
1/2
✗ Branch 258 → 259 not taken.
✓ Branch 258 → 260 taken 4 times.
4 token.size() > limits.max_field_bytes - offsets.size() - separator_bytes)
1446 {
1447 return fail(ErrorCode::SizeTooLarge, "manifest::serialize_checked");
1448 }
1449
2/2
✓ Branch 265 → 266 taken 2 times.
✓ Branch 265 → 267 taken 2 times.
4 if (index != 0)
1450 {
1451
1/2
✓ Branch 266 → 267 taken 2 times.
✗ Branch 266 → 776 not taken.
2 offsets += ", ";
1452 }
1453
1/2
✓ Branch 267 → 268 taken 4 times.
✗ Branch 267 → 776 not taken.
4 offsets += token;
1454
1/2
✓ Branch 270 → 271 taken 4 times.
✗ Branch 270 → 273 not taken.
4 }
1455
2/4
✓ Branch 277 → 278 taken 2 times.
✗ Branch 277 → 779 not taken.
✗ Branch 279 → 280 not taken.
✓ Branch 279 → 284 taken 2 times.
2 DMK_TRY_VOID(builder.set(sec, "offsets", offsets.c_str()));
1456
2/4
✓ Branch 286 → 287 taken 2 times.
✗ Branch 286 → 780 not taken.
✗ Branch 289 → 290 not taken.
✓ Branch 289 → 294 taken 2 times.
2 DMK_TRY_VOID(builder.set(sec, "value_width", std::to_string(record.binding.value_width).c_str()));
1457 2 break;
1458
1/2
✗ Branch 296 → 297 not taken.
✓ Branch 296 → 298 taken 2 times.
2 }
1459 2 case BindingKind::MidHookRegister:
1460
3/6
✓ Branch 302 → 303 taken 2 times.
✗ Branch 302 → 789 not taken.
✓ Branch 304 → 305 taken 2 times.
✗ Branch 304 → 787 not taken.
✗ Branch 308 → 309 not taken.
✓ Branch 308 → 313 taken 2 times.
2 DMK_TRY_VOID(
1461 builder.set(sec, "read_register", std::string(gpr_token(record.binding.read_register)).c_str())
1462 );
1463
1/2
✗ Branch 313 → 314 not taken.
✓ Branch 313 → 325 taken 2 times.
2 if (record.binding.xmm_index != XMM_INDEX_UNUSED)
1464 {
1465 DMK_TRY_VOID(builder.set(sec, "xmm_index", std::to_string(record.binding.xmm_index).c_str()));
1466 }
1467 2 break;
1468 2 case BindingKind::VmtMethod:
1469
3/6
✓ Branch 326 → 327 taken 2 times.
✗ Branch 326 → 801 not taken.
✓ Branch 328 → 329 taken 2 times.
✗ Branch 328 → 799 not taken.
✗ Branch 331 → 332 not taken.
✓ Branch 331 → 336 taken 2 times.
2 DMK_TRY_VOID(builder.set(sec, "vmt_index", std::to_string(record.binding.vmt_index).c_str()));
1470 2 break;
1471 187 case BindingKind::Address:
1472 187 break;
1473 }
1474
1475
2/2
✓ Branch 338 → 339 taken 8 times.
✓ Branch 338 → 350 taken 185 times.
193 if (record.expected_fingerprint != 0)
1476 {
1477
3/6
✓ Branch 339 → 340 taken 8 times.
✗ Branch 339 → 805 not taken.
✓ Branch 341 → 342 taken 8 times.
✗ Branch 341 → 803 not taken.
✗ Branch 344 → 345 not taken.
✓ Branch 344 → 349 taken 8 times.
8 DMK_TRY_VOID(
1478 builder.set(sec, "fingerprint", std::format("0x{:X}", record.expected_fingerprint).c_str())
1479 );
1480 }
1481 // A captured image identity round-trips as `timestamp:size_of_image:section_digest` in hex. An absent
1482 // value keeps a schema-v1 manifest free of an image baseline.
1483
2/2
✓ Branch 351 → 352 taken 2 times.
✓ Branch 351 → 363 taken 191 times.
193 if (record.expected_image_identity.present())
1484 {
1485
3/6
✓ Branch 352 → 353 taken 2 times.
✗ Branch 352 → 810 not taken.
✓ Branch 354 → 355 taken 2 times.
✗ Branch 354 → 808 not taken.
✗ Branch 357 → 358 not taken.
✓ Branch 357 → 362 taken 2 times.
2 DMK_TRY_VOID(builder.set(
1486 sec,
1487 "image_identity",
1488 std::format(
1489 "{:X}:{:X}:{:X}",
1490 record.expected_image_identity.timestamp,
1491 record.expected_image_identity.size_of_image,
1492 record.expected_image_identity.section_digest
1493 )
1494 .c_str()
1495 ));
1496 }
1497 // The captured matched span round-trips as lowercase hex. An absent value is the default for every
1498 // non-byte rung and keeps the manifest free of a content baseline.
1499
2/2
✓ Branch 364 → 365 taken 3 times.
✓ Branch 364 → 401 taken 190 times.
193 if (record.expected_winning_bytes.present())
1500 {
1501 3 std::string hex;
1502
1/2
✓ Branch 366 → 367 taken 3 times.
✗ Branch 366 → 820 not taken.
3 hex.reserve(static_cast<std::size_t>(record.expected_winning_bytes.length) * 2U);
1503
2/2
✓ Branch 386 → 370 taken 284 times.
✓ Branch 386 → 387 taken 3 times.
574 for (const std::byte value : record.expected_winning_bytes.span())
1504 {
1505
2/4
✓ Branch 374 → 375 taken 284 times.
✗ Branch 374 → 815 not taken.
✓ Branch 375 → 376 taken 284 times.
✗ Branch 375 → 813 not taken.
284 hex += std::format("{:02x}", std::to_integer<unsigned>(value));
1506 }
1507
2/4
✓ Branch 388 → 389 taken 3 times.
✗ Branch 388 → 819 not taken.
✗ Branch 390 → 391 not taken.
✓ Branch 390 → 395 taken 3 times.
3 DMK_TRY_VOID(builder.set(sec, "winning_bytes", hex.c_str()));
1508
1/2
✓ Branch 397 → 398 taken 3 times.
✗ Branch 397 → 400 not taken.
3 }
1509
1510
6/8
✓ Branch 401 → 402 taken 2 times.
✓ Branch 401 → 411 taken 3 times.
✓ Branch 401 → 446 taken 18 times.
✓ Branch 401 → 503 taken 158 times.
✓ Branch 401 → 514 taken 10 times.
✓ Branch 401 → 531 taken 2 times.
✗ Branch 401 → 540 not taken.
✗ Branch 401 → 541 not taken.
193 switch (record.kind)
1511 {
1512 2 case anchor::AnchorKind::VtableIdentity:
1513
2/4
✓ Branch 403 → 404 taken 2 times.
✗ Branch 403 → 823 not taken.
✗ Branch 405 → 406 not taken.
✓ Branch 405 → 410 taken 2 times.
2 DMK_TRY_VOID(builder.set(sec, "mangled", record.mangled.c_str()));
1514 2 break;
1515 3 case anchor::AnchorKind::CodeOperand:
1516
3/6
✓ Branch 414 → 415 taken 3 times.
✗ Branch 414 → 826 not taken.
✓ Branch 416 → 417 taken 3 times.
✗ Branch 416 → 824 not taken.
✗ Branch 420 → 421 not taken.
✓ Branch 420 → 425 taken 3 times.
3 DMK_TRY_VOID(
1517 builder.set(sec, "operand_kind", std::string(operand_kind_token(record.operand_kind)).c_str())
1518 );
1519
2/4
✓ Branch 427 → 428 taken 3 times.
✗ Branch 427 → 832 not taken.
✗ Branch 430 → 431 not taken.
✓ Branch 430 → 435 taken 3 times.
3 DMK_TRY_VOID(builder.set(sec, "operand_index", std::to_string(record.operand_index).c_str()));
1520
2/4
✓ Branch 437 → 438 taken 3 times.
✗ Branch 437 → 836 not taken.
✗ Branch 440 → 441 not taken.
✓ Branch 440 → 445 taken 3 times.
3 DMK_TRY_VOID(builder.set(sec, "byte_width", std::to_string(record.byte_width).c_str()));
1521 3 break;
1522 18 case anchor::AnchorKind::StringXref:
1523
2/4
✓ Branch 447 → 448 taken 18 times.
✗ Branch 447 → 840 not taken.
✗ Branch 449 → 450 not taken.
✓ Branch 449 → 454 taken 18 times.
18 DMK_TRY_VOID(builder.set(sec, "xref_text", record.xref_text.c_str()));
1524
3/6
✓ Branch 457 → 458 taken 18 times.
✗ Branch 457 → 843 not taken.
✓ Branch 459 → 460 taken 18 times.
✗ Branch 459 → 841 not taken.
✗ Branch 463 → 464 not taken.
✓ Branch 463 → 468 taken 18 times.
18 DMK_TRY_VOID(
1525 builder.set(sec, "xref_encoding", std::string(encoding_token(record.xref_encoding)).c_str())
1526 );
1527
3/6
✓ Branch 471 → 472 taken 18 times.
✗ Branch 471 → 851 not taken.
✓ Branch 473 → 474 taken 18 times.
✗ Branch 473 → 849 not taken.
✗ Branch 477 → 478 not taken.
✓ Branch 477 → 482 taken 18 times.
18 DMK_TRY_VOID(
1528 builder.set(sec, "xref_return", std::string(xref_return_token(record.xref_return)).c_str())
1529 );
1530
4/6
✓ Branch 482 → 483 taken 16 times.
✓ Branch 482 → 484 taken 2 times.
✓ Branch 485 → 486 taken 18 times.
✗ Branch 485 → 857 not taken.
✗ Branch 487 → 488 not taken.
✓ Branch 487 → 492 taken 18 times.
18 DMK_TRY_VOID(
1531 builder.set(sec, "xref_require_terminator", record.xref_require_terminator ? "true" : "false")
1532 );
1533
3/6
✗ Branch 492 → 493 not taken.
✓ Branch 492 → 494 taken 18 times.
✓ Branch 495 → 496 taken 18 times.
✗ Branch 495 → 858 not taken.
✗ Branch 497 → 498 not taken.
✓ Branch 497 → 502 taken 18 times.
18 DMK_TRY_VOID(builder.set(sec, "xref_broad_match", record.xref_broad_match ? "true" : "false"));
1534 18 break;
1535 158 case anchor::AnchorKind::Manual:
1536
5/6
✓ Branch 503 → 504 taken 158 times.
✗ Branch 503 → 861 not taken.
✓ Branch 505 → 506 taken 152 times.
✓ Branch 505 → 859 taken 6 times.
✓ Branch 508 → 509 taken 6 times.
✓ Branch 508 → 513 taken 146 times.
164 DMK_TRY_VOID(builder.set(
1537 sec,
1538 "manual_value",
1539 format_signed_hex(static_cast<long long>(record.manual_value)).c_str()
1540 ));
1541 146 break;
1542 10 case anchor::AnchorKind::RipGlobal:
1543
2/2
✓ Branch 514 → 515 taken 2 times.
✓ Branch 514 → 530 taken 8 times.
10 if (record.pages != scan::Pages::Readable)
1544 {
1545
3/6
✓ Branch 518 → 519 taken 2 times.
✗ Branch 518 → 865 not taken.
✓ Branch 520 → 521 taken 2 times.
✗ Branch 520 → 863 not taken.
✗ Branch 524 → 525 not taken.
✓ Branch 524 → 529 taken 2 times.
2 DMK_TRY_VOID(builder.set(sec, "pages", std::string(pages_token(record.pages)).c_str()));
1546 }
1547 10 break;
1548 2 case anchor::AnchorKind::ExportName:
1549 // The shared `module` key above stores the export module. Only the export symbol is kind-specific.
1550
2/4
✓ Branch 532 → 533 taken 2 times.
✗ Branch 532 → 871 not taken.
✗ Branch 534 → 535 not taken.
✓ Branch 534 → 539 taken 2 times.
2 DMK_TRY_VOID(builder.set(sec, "export_name", record.export_name.c_str()));
1551 2 break;
1552 case anchor::AnchorKind::CallArgHome:
1553 case anchor::AnchorKind::Quorum:
1554 case anchor::AnchorKind::Unset:
1555 break;
1556 }
1557
1558
2/2
✓ Branch 701 → 542 taken 16 times.
✓ Branch 701 → 702 taken 181 times.
197 for (std::size_t index = 0; index < record.ladder.size(); ++index)
1559 {
1560 16 const CandidateSpec &spec = record.ladder[index];
1561
1/2
✓ Branch 543 → 544 taken 16 times.
✗ Branch 543 → 872 not taken.
16 const std::string rung_section = std::format("{}.rung.{}", section, index);
1562 16 const char *rsec = rung_section.c_str();
1563
1/2
✗ Branch 547 → 548 not taken.
✓ Branch 547 → 552 taken 16 times.
16 DMK_TRY_VOID(builder.begin_section());
1564
1565
3/6
✓ Branch 555 → 556 taken 16 times.
✗ Branch 555 → 875 not taken.
✓ Branch 557 → 558 taken 16 times.
✗ Branch 557 → 873 not taken.
✗ Branch 561 → 562 not taken.
✓ Branch 561 → 566 taken 16 times.
16 DMK_TRY_VOID(builder.set(rsec, "mode", std::string(scan_mode_token(spec.mode)).c_str()));
1566
2/2
✓ Branch 567 → 568 taken 2 times.
✓ Branch 567 → 577 taken 14 times.
16 if (!spec.name.empty())
1567 {
1568
2/4
✓ Branch 569 → 570 taken 2 times.
✗ Branch 569 → 881 not taken.
✗ Branch 571 → 572 not taken.
✓ Branch 571 → 576 taken 2 times.
2 DMK_TRY_VOID(builder.set(rsec, "name", spec.name.c_str()));
1569 }
1570
3/5
✓ Branch 577 → 578 taken 11 times.
✓ Branch 577 → 599 taken 3 times.
✗ Branch 577 → 628 not taken.
✓ Branch 577 → 637 taken 2 times.
✗ Branch 577 → 694 not taken.
16 switch (spec.mode)
1571 {
1572 11 case scan::Mode::Direct:
1573
2/4
✓ Branch 579 → 580 taken 11 times.
✗ Branch 579 → 882 not taken.
✗ Branch 581 → 582 not taken.
✓ Branch 581 → 586 taken 11 times.
11 DMK_TRY_VOID(builder.set(rsec, "pattern", spec.pattern.c_str()));
1574
2/2
✓ Branch 586 → 587 taken 2 times.
✓ Branch 586 → 598 taken 9 times.
11 if (spec.walk_back != 0)
1575 {
1576
3/6
✓ Branch 587 → 588 taken 2 times.
✗ Branch 587 → 885 not taken.
✓ Branch 589 → 590 taken 2 times.
✗ Branch 589 → 883 not taken.
✗ Branch 592 → 593 not taken.
✓ Branch 592 → 597 taken 2 times.
2 DMK_TRY_VOID(builder.set(
1577 rsec,
1578 "walk_back",
1579 format_signed_hex(static_cast<long long>(spec.walk_back)).c_str()
1580 ));
1581 }
1582 11 break;
1583 3 case scan::Mode::RipRelative:
1584
2/4
✓ Branch 600 → 601 taken 3 times.
✗ Branch 600 → 887 not taken.
✗ Branch 602 → 603 not taken.
✓ Branch 602 → 607 taken 3 times.
3 DMK_TRY_VOID(builder.set(rsec, "pattern", spec.pattern.c_str()));
1585
3/6
✓ Branch 607 → 608 taken 3 times.
✗ Branch 607 → 890 not taken.
✓ Branch 609 → 610 taken 3 times.
✗ Branch 609 → 888 not taken.
✗ Branch 612 → 613 not taken.
✓ Branch 612 → 617 taken 3 times.
3 DMK_TRY_VOID(builder.set(
1586 rsec,
1587 "displacement_at",
1588 format_signed_hex(static_cast<long long>(spec.displacement_at)).c_str()
1589 ));
1590
3/6
✓ Branch 617 → 618 taken 3 times.
✗ Branch 617 → 894 not taken.
✓ Branch 619 → 620 taken 3 times.
✗ Branch 619 → 892 not taken.
✗ Branch 622 → 623 not taken.
✓ Branch 622 → 627 taken 3 times.
3 DMK_TRY_VOID(
1591 builder.set(rsec, "instruction_length", std::to_string(spec.instruction_length).c_str())
1592 );
1593 3 break;
1594 case scan::Mode::RttiVtable:
1595 DMK_TRY_VOID(builder.set(rsec, "mangled", spec.mangled.c_str()));
1596 break;
1597 2 case scan::Mode::StringXref:
1598
2/4
✓ Branch 638 → 639 taken 2 times.
✗ Branch 638 → 897 not taken.
✗ Branch 640 → 641 not taken.
✓ Branch 640 → 645 taken 2 times.
2 DMK_TRY_VOID(builder.set(rsec, "string_text", spec.string_text.c_str()));
1599
3/6
✓ Branch 648 → 649 taken 2 times.
✗ Branch 648 → 900 not taken.
✓ Branch 650 → 651 taken 2 times.
✗ Branch 650 → 898 not taken.
✗ Branch 654 → 655 not taken.
✓ Branch 654 → 659 taken 2 times.
2 DMK_TRY_VOID(builder.set(
1600 rsec,
1601 "string_encoding",
1602 std::string(encoding_token(spec.string_encoding)).c_str()
1603 ));
1604
3/6
✓ Branch 662 → 663 taken 2 times.
✗ Branch 662 → 908 not taken.
✓ Branch 664 → 665 taken 2 times.
✗ Branch 664 → 906 not taken.
✗ Branch 668 → 669 not taken.
✓ Branch 668 → 673 taken 2 times.
2 DMK_TRY_VOID(builder.set(
1605 rsec,
1606 "string_return",
1607 std::string(xref_return_token(spec.string_return)).c_str()
1608 ));
1609
3/6
✓ Branch 673 → 674 taken 2 times.
✗ Branch 673 → 675 not taken.
✓ Branch 676 → 677 taken 2 times.
✗ Branch 676 → 914 not taken.
✗ Branch 678 → 679 not taken.
✓ Branch 678 → 683 taken 2 times.
2 DMK_TRY_VOID(builder.set(
1610 rsec,
1611 "string_require_terminator",
1612 spec.string_require_terminator ? "true" : "false"
1613 ));
1614
3/6
✓ Branch 683 → 684 taken 2 times.
✗ Branch 683 → 685 not taken.
✓ Branch 686 → 687 taken 2 times.
✗ Branch 686 → 915 not taken.
✗ Branch 688 → 689 not taken.
✓ Branch 688 → 693 taken 2 times.
2 DMK_TRY_VOID(
1615 builder.set(rsec, "string_broad_match", spec.string_broad_match ? "true" : "false")
1616 );
1617 2 break;
1618 }
1619
1/2
✓ Branch 696 → 697 taken 16 times.
✗ Branch 696 → 699 not taken.
16 }
1620
2/2
✓ Branch 704 → 705 taken 181 times.
✓ Branch 704 → 709 taken 17 times.
220 }
1621
1622 166 std::string out;
1623 166 BoundedStringWriter writer{out, limits.max_file_bytes};
1624
2/2
✓ Branch 719 → 720 taken 61 times.
✓ Branch 719 → 926 taken 22 times.
83 const SI_Error save_result = ini.Save(writer);
1625
2/2
✓ Branch 721 → 722 taken 1 time.
✓ Branch 721 → 725 taken 60 times.
61 if (writer.exceeded())
1626 {
1627 1 return fail(ErrorCode::SizeTooLarge, "manifest::serialize_checked");
1628 }
1629
1/2
✗ Branch 725 → 726 not taken.
✓ Branch 725 → 729 taken 60 times.
60 if (save_result < 0)
1630 {
1631 return fail(ErrorCode::OutOfMemory, "manifest::serialize_checked");
1632 }
1633 // Re-run the reader's grammar over the emitted bytes so identity and frame checks cannot diverge.
1634
3/4
✓ Branch 731 → 732 taken 48 times.
✓ Branch 731 → 924 taken 12 times.
✗ Branch 733 → 734 not taken.
✓ Branch 733 → 738 taken 48 times.
60 DMK_TRY_VOID(
1635 detail::validate_manifest_grammar(out, to_grammar_limits(limits), "manifest::serialize_checked")
1636 );
1637 48 return out;
1638 252 }
1639 } // namespace
1640
1641 192 Result<std::string> serialize_checked(const Manifest &manifest, const ManifestLimits &limits)
1642 {
1643 // Any emit-stage allocation failure becomes a typed atomic OutOfMemory. The caller retains its current value
1644 // because the partial string never escapes.
1645 try
1646 {
1647
2/2
✓ Branch 2 → 3 taken 128 times.
✓ Branch 2 → 5 taken 64 times.
192 return serialize_impl(manifest, limits);
1648 }
1649
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 64 times.
64 catch (const std::bad_alloc &)
1650 {
1651 64 return fail(ErrorCode::OutOfMemory, "manifest::serialize_checked");
1652 64 }
1653 }
1654
1655 33 Result<Manifest> load(const std::filesystem::path &path, const ManifestLimits &limits)
1656 {
1657 // Materialize only a regular file that stays within the encoded-byte cap. The catch also covers the path
1658 // conversion allocation, which happens before the bounded reader receives the path.
1659 try
1660 {
1661
5/6
✓ Branch 2 → 3 taken 32 times.
✓ Branch 2 → 24 taken 1 time.
✓ Branch 3 → 4 taken 32 times.
✗ Branch 3 → 22 not taken.
✓ Branch 6 → 7 taken 4 times.
✓ Branch 6 → 11 taken 28 times.
61 DMK_TRY(text, ::DetourModKit::detail::read_regular_file_bounded(path.wstring(), limits.max_file_bytes));
1662
1/2
✓ Branch 16 → 17 taken 28 times.
✗ Branch 16 → 25 not taken.
28 return parse(text, limits);
1663 32 }
1664
1/2
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 1 time.
1 catch (const std::bad_alloc &)
1665 {
1666 1 return fail(ErrorCode::OutOfMemory, "manifest::load");
1667 1 }
1668 }
1669
1670 5 Result<void> save(const std::filesystem::path &path, const Manifest &manifest, const ManifestLimits &limits)
1671 {
1672 // Validate and encode before file open. An invalid manifest never truncates a prior readable file.
1673
3/4
✓ Branch 2 → 3 taken 5 times.
✗ Branch 2 → 55 not taken.
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 9 taken 2 times.
7 DMK_TRY(text, serialize_checked(manifest, limits));
1674
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 19 taken 2 times.
2 if (text.size() > static_cast<std::size_t>(std::numeric_limits<std::streamsize>::max()))
1675 {
1676 return fail(ErrorCode::SizeTooLarge, "manifest::save");
1677 }
1678 try
1679 {
1680
1/2
✓ Branch 20 → 21 taken 2 times.
✗ Branch 20 → 44 not taken.
2 std::ofstream out(path, std::ios::binary | std::ios::trunc);
1681
2/4
✓ Branch 21 → 22 taken 2 times.
✗ Branch 21 → 42 not taken.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 26 taken 2 times.
2 if (!out)
1682 {
1683 return fail(ErrorCode::FileOpenFailed, "manifest::save");
1684 }
1685
1/2
✓ Branch 28 → 29 taken 2 times.
✗ Branch 28 → 42 not taken.
2 out.write(text.data(), static_cast<std::streamsize>(text.size()));
1686
1/2
✓ Branch 29 → 30 taken 2 times.
✗ Branch 29 → 42 not taken.
2 out.flush();
1687
2/4
✓ Branch 30 → 31 taken 2 times.
✗ Branch 30 → 42 not taken.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 35 taken 2 times.
2 if (!out)
1688 {
1689 return fail(ErrorCode::FileWriteFailed, "manifest::save");
1690 }
1691 2 return {};
1692 2 }
1693 catch (const std::bad_alloc &)
1694 {
1695 return fail(ErrorCode::OutOfMemory, "manifest::save");
1696 }
1697 5 }
1698
1699 207 std::string_view binding_kind_to_string(BindingKind kind) noexcept
1700 {
1701
4/5
✓ Branch 2 → 3 taken 198 times.
✓ Branch 2 → 4 taken 3 times.
✓ Branch 2 → 5 taken 3 times.
✓ Branch 2 → 6 taken 3 times.
✗ Branch 2 → 7 not taken.
207 switch (kind)
1702 {
1703 198 case BindingKind::Address:
1704 198 return "address";
1705 3 case BindingKind::PointerChain:
1706 3 return "pointer_chain";
1707 3 case BindingKind::MidHookRegister:
1708 3 return "mid_hook_register";
1709 3 case BindingKind::VmtMethod:
1710 3 return "vmt_method";
1711 }
1712 return "address";
1713 }
1714
1715 } // namespace DetourModKit::manifest
1716