./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
833 / 903
92.2%
Branches:
946 / 1295
73.1%
Functions:
69 / 69
100.0%

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