./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
179 / 183
97.8%
Branches:
203 / 232
87.5%
Functions:
14 / 14
100.0%

src/internal/manifest_grammar.cpp
Line Branch Exec Source
1 /**
2 * @file manifest_grammar.cpp
3 * @brief Raw-text prepass that closes manifest identity and framing collisions before the INI backend can merge them.
4 * @details CSimpleIniCaseA merges duplicate sections, retains the last duplicate key, and absorbs an unterminated
5 * heredoc through end of file. This pass uses the same whitespace, section, line-break, and heredoc rules.
6 * It rejects lost identities, unclosed heredocs, discarded key lines, and resource-cap excesses before store
7 * allocation. One shared token model prevents a collision from escape through a backend difference.
8 */
9
10 #include "internal/manifest_grammar.hpp"
11
12 #include <limits>
13 #include <string>
14 #include <string_view>
15 #include <unordered_map>
16 #include <unordered_set>
17
18 namespace DetourModKit::manifest::detail
19 {
20 namespace
21 {
22 // The backend's IsSpace: a space, tab, or either newline character. Used for blank-line and trailing-name
23 // trimming exactly as FindEntry does.
24 504341 [[nodiscard]] bool is_space(char c) noexcept
25 {
26
7/8
✓ Branch 2 → 3 taken 432626 times.
✓ Branch 2 → 6 taken 71715 times.
✓ Branch 3 → 4 taken 432626 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 430476 times.
✓ Branch 4 → 6 taken 2150 times.
✓ Branch 5 → 6 taken 107959 times.
✓ Branch 5 → 7 taken 322517 times.
504341 return c == ' ' || c == '\t' || c == '\r' || c == '\n';
27 }
28
29 11409617 [[nodiscard]] bool is_newline(char c) noexcept
30 {
31
4/4
✓ Branch 2 → 3 taken 11407950 times.
✓ Branch 2 → 4 taken 1667 times.
✓ Branch 3 → 4 taken 105894 times.
✓ Branch 3 → 5 taken 11302056 times.
11409617 return c == '\r' || c == '\n';
32 }
33
34 179256 [[nodiscard]] std::string_view rtrim(std::string_view text) noexcept
35 {
36
6/6
✓ Branch 5 → 6 taken 250959 times.
✓ Branch 5 → 10 taken 9 times.
✓ Branch 8 → 9 taken 71712 times.
✓ Branch 8 → 10 taken 179247 times.
✓ Branch 11 → 3 taken 71712 times.
✓ Branch 11 → 12 taken 179256 times.
250968 while (!text.empty() && is_space(text.back()))
37 {
38 71712 text.remove_suffix(1);
39 }
40 179256 return text;
41 }
42
43 107473 [[nodiscard]] std::string to_lower(std::string_view text)
44 {
45
2/2
✓ Branch 4 → 5 taken 107472 times.
✓ Branch 4 → 25 taken 1 time.
107473 std::string out(text);
46
2/2
✓ Branch 22 → 8 taken 955544 times.
✓ Branch 22 → 23 taken 107472 times.
1170488 for (char &c : out)
47 {
48
4/4
✓ Branch 10 → 11 taken 701904 times.
✓ Branch 10 → 13 taken 253640 times.
✓ Branch 11 → 12 taken 401 times.
✓ Branch 11 → 13 taken 701503 times.
955544 if (c >= 'A' && c <= 'Z')
49 {
50 401 c = static_cast<char>(c - 'A' + 'a');
51 }
52 }
53 107472 return out;
54 }
55
56 55 [[nodiscard]] std::unexpected<Error> fail(ErrorCode code, const char *context) noexcept
57 {
58 55 return std::unexpected(Error{code, context});
59 }
60
61 71737 [[nodiscard]] bool consume_bytes(std::size_t bytes, std::size_t limit, std::size_t &total) noexcept
62 {
63
2/2
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 4 taken 71734 times.
71737 if (bytes > limit - total)
64 {
65 3 return false;
66 }
67 71734 total += bytes;
68 71734 return true;
69 }
70
71 35442 [[nodiscard]] std::string_view rung_parent(std::string_view name) noexcept
72 {
73 35442 const std::size_t marker = name.rfind(".rung.");
74
2/2
✓ Branch 3 → 4 taken 1486 times.
✓ Branch 3 → 5 taken 33956 times.
35442 if (marker == std::string_view::npos)
75 {
76 1486 return {};
77 }
78 33956 const std::string_view tail = name.substr(marker + 6);
79
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 33956 times.
33956 if (tail.empty())
80 {
81 ✗ return {};
82 }
83 33956 std::size_t index = 0;
84
2/2
✓ Branch 17 → 11 taken 146942 times.
✓ Branch 17 → 18 taken 33956 times.
180898 for (const char c : tail)
85 {
86
2/4
✓ Branch 11 → 12 taken 146942 times.
✗ Branch 11 → 13 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 146942 times.
146942 if (c < '0' || c > '9')
87 {
88 ✗ return {};
89 }
90 146942 const std::size_t digit = static_cast<std::size_t>(c - '0');
91 146942 constexpr std::size_t MAX_INDEX = std::numeric_limits<std::size_t>::max();
92
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 146942 times.
146942 if (index > (MAX_INDEX - digit) / 10U)
93 {
94 ✗ return {};
95 }
96 146942 index = (index * 10U) + digit;
97 }
98 33956 return name.substr(0, marker);
99 }
100
101 107553 [[nodiscard]] std::size_t line_end_from(std::string_view text, std::size_t start) noexcept
102 {
103
5/6
✓ Branch 5 → 6 taken 10238823 times.
✗ Branch 5 → 10 not taken.
✓ Branch 8 → 9 taken 10131270 times.
✓ Branch 8 → 10 taken 107553 times.
✓ Branch 11 → 3 taken 10131270 times.
✓ Branch 11 → 12 taken 107553 times.
10238823 while (start < text.size() && !is_newline(text[start]))
104 {
105 10131270 ++start;
106 }
107 107553 return start;
108 }
109
110 /// Consumes one line terminator as the backend's SkipNewLine does: a lone `\r`, a lone `\n`, or a `\r\n` pair.
111 96 [[nodiscard]] std::size_t skip_newline(std::string_view text, std::size_t at) noexcept
112 {
113
5/6
✓ Branch 3 → 4 taken 96 times.
✗ Branch 3 → 7 not taken.
✓ Branch 5 → 6 taken 72 times.
✓ Branch 5 → 7 taken 24 times.
✓ Branch 8 → 9 taken 72 times.
✓ Branch 8 → 17 taken 24 times.
96 if (at < text.size() && text[at] == '\r')
114 {
115 72 ++at;
116
3/6
✓ Branch 10 → 11 taken 72 times.
✗ Branch 10 → 14 not taken.
✓ Branch 12 → 13 taken 72 times.
✗ Branch 12 → 14 not taken.
✓ Branch 15 → 16 taken 72 times.
✗ Branch 15 → 25 not taken.
72 if (at < text.size() && text[at] == '\n')
117 {
118 72 ++at;
119 }
120 }
121
3/6
✓ Branch 18 → 19 taken 24 times.
✗ Branch 18 → 22 not taken.
✓ Branch 20 → 21 taken 24 times.
✗ Branch 20 → 22 not taken.
✓ Branch 23 → 24 taken 24 times.
✗ Branch 23 → 25 not taken.
24 else if (at < text.size() && text[at] == '\n')
122 {
123 24 ++at;
124 }
125 96 return at;
126 }
127
128 /// Rejects a section name that parse() never reads or whose folded form repeats, and charges the section cap.
129 35776 [[nodiscard]] Result<void> register_section(
130 std::string_view name,
131 const GrammarLimits &limits,
132 const char *context,
133 std::unordered_set<std::string> &seen_sections,
134 std::size_t &section_count
135 )
136 {
137 // parse() reads only the canonical lowercase `manifest` header and `sig.` sections. Any other name,
138 // including a miscased one, holds state that parse() never reads. An empty name (`[]`, `[ ]`) also
139 // reopens the backend's implicit default section, where a key collision escapes the per-section
140 // namespace. This pass fails closed instead.
141
6/6
✓ Branch 4 → 5 taken 35467 times.
✓ Branch 4 → 8 taken 309 times.
✓ Branch 6 → 7 taken 8 times.
✓ Branch 6 → 8 taken 35459 times.
✓ Branch 9 → 10 taken 8 times.
✓ Branch 9 → 13 taken 35768 times.
35776 if (name != "manifest" && !name.starts_with("sig."))
142 {
143 8 return fail(ErrorCode::MalformedLine, context);
144 }
145
2/2
✓ Branch 13 → 14 taken 35767 times.
✓ Branch 13 → 32 taken 1 time.
35768 std::string folded = to_lower(name);
146 // The folded, trimmed name is the merge key. The backend merges two sections that reach it by case,
147 // whitespace, or exact repetition into one before the trust gate.
148
2/2
✓ Branch 14 → 15 taken 2 times.
✓ Branch 14 → 18 taken 35765 times.
35767 if (section_count >= limits.max_sections)
149 {
150 2 return fail(ErrorCode::SizeTooLarge, context);
151 }
152
4/4
✓ Branch 20 → 21 taken 35752 times.
✓ Branch 20 → 29 taken 13 times.
✓ Branch 21 → 22 taken 8 times.
✓ Branch 21 → 25 taken 35744 times.
35765 if (!seen_sections.insert(std::move(folded)).second)
153 {
154 8 return fail(ErrorCode::ManifestIdentityCollision, context);
155 }
156 35744 ++section_count;
157 35744 return {};
158 35767 }
159
160 /**
161 * @brief Rejects a `sig.` rung without a parent label or a `sig.` label over the field cap, then charges the
162 * record or rung cap.
163 * @details A rung charges the rung cap of its parent record. A name without the `sig.` prefix passes with no
164 * check or charge.
165 */
166 35744 [[nodiscard]] Result<void> charge_signature_section(
167 std::string_view name,
168 const GrammarLimits &limits,
169 const char *context,
170 std::unordered_map<std::string, std::size_t> &rung_counts,
171 std::size_t &record_count
172 )
173 {
174 // Classify a record or a rung by the raw name, not the folded name. parse() reads `.rung.` case-sensitively
175 // and treats a miscased marker as an ordinary label. This pass and parse() must charge the same cap, and a
176 // fold counts the legitimate record `sig.a.RUNG.0` (label `a.RUNG.0`) against a rung cap.
177
2/2
✓ Branch 3 → 4 taken 35442 times.
✓ Branch 3 → 46 taken 302 times.
35744 if (name.starts_with("sig."))
178 {
179 35442 const std::string_view parent = rung_parent(name);
180
6/6
✓ Branch 6 → 7 taken 33956 times.
✓ Branch 6 → 10 taken 1486 times.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 33955 times.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 15 taken 35441 times.
35442 if (!parent.empty() && parent.size() <= 4U)
181 {
182 1 return fail(ErrorCode::MalformedLine, context);
183 }
184
4/6
✓ Branch 16 → 17 taken 1486 times.
✓ Branch 16 → 18 taken 33955 times.
✓ Branch 17 → 19 taken 1486 times.
✗ Branch 17 → 57 not taken.
✓ Branch 18 → 19 taken 33955 times.
✗ Branch 18 → 57 not taken.
35441 const std::string_view label = parent.empty() ? name.substr(4) : parent.substr(4);
185
2/2
✓ Branch 20 → 21 taken 1 time.
✓ Branch 20 → 24 taken 35440 times.
35441 if (label.size() > limits.max_field_bytes)
186 {
187 1 return fail(ErrorCode::SizeTooLarge, context);
188 }
189
2/2
✓ Branch 25 → 26 taken 1485 times.
✓ Branch 25 → 31 taken 33955 times.
35440 if (parent.empty())
190 {
191
2/2
✓ Branch 26 → 27 taken 2 times.
✓ Branch 26 → 30 taken 1483 times.
1485 if (record_count >= limits.max_records)
192 {
193 2 return fail(ErrorCode::SizeTooLarge, context);
194 }
195 1483 ++record_count;
196 }
197 else
198 {
199
3/4
✓ Branch 33 → 34 taken 33955 times.
✗ Branch 33 → 50 not taken.
✓ Branch 34 → 35 taken 33953 times.
✓ Branch 34 → 48 taken 2 times.
67912 auto count_it = rung_counts.try_emplace(std::string(parent), 0).first;
200
2/2
✓ Branch 38 → 39 taken 2 times.
✓ Branch 38 → 42 taken 33951 times.
33953 if (count_it->second >= limits.max_rungs_per_record)
201 {
202 2 return fail(ErrorCode::SizeTooLarge, context);
203 }
204 33951 ++count_it->second;
205 }
206 }
207 35736 return {};
208 }
209
210 /// Rejects a non-lowercase or repeated key and charges it against the per-section key cap.
211 71705 [[nodiscard]] Result<void> register_key(
212 std::string_view key,
213 const GrammarLimits &limits,
214 const char *context,
215 std::unordered_set<std::string> &seen_keys,
216 std::size_t &key_count
217 )
218 {
219
1/2
✓ Branch 2 → 3 taken 71705 times.
✗ Branch 2 → 27 not taken.
71705 std::string folded_key = to_lower(key);
220
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 9 taken 71704 times.
71705 if (key != folded_key)
221 {
222 1 return fail(ErrorCode::MalformedLine, context);
223 }
224
2/2
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 13 taken 71702 times.
71704 if (key_count >= limits.max_keys_per_section)
225 {
226 2 return fail(ErrorCode::SizeTooLarge, context);
227 }
228
4/4
✓ Branch 15 → 16 taken 71677 times.
✓ Branch 15 → 24 taken 25 times.
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 20 taken 71676 times.
71702 if (!seen_keys.insert(std::move(folded_key)).second)
229 {
230 1 return fail(ErrorCode::ManifestIdentityCollision, context);
231 }
232 71676 ++key_count;
233 71676 return {};
234 71705 }
235
236 /**
237 * @brief Rejects unsafe heredoc framing and charges a key value against the field and aggregate caps.
238 * @details On entry, `pos` indexes the end of the value line. A heredoc value advances `pos` past its
239 * terminator line, and any other value leaves it unchanged.
240 */
241 71676 [[nodiscard]] Result<void> consume_value(
242 std::string_view text,
243 std::string_view value,
244 const GrammarLimits &limits,
245 const char *context,
246 std::size_t &pos,
247 std::size_t &total_bytes
248 ) noexcept
249 {
250
2/2
✓ Branch 3 → 4 taken 28 times.
✓ Branch 3 → 50 taken 71648 times.
71676 if (value.starts_with("<<<"))
251 {
252 // A heredoc runs until a line whose trailing-trimmed form EQUALS the tag (case-sensitive, matching the
253 // case-sensitive store). An unterminated block would absorb every record below it into this value.
254 28 const std::string_view tag = value.substr(3);
255 // An empty tag terminates differently here and in the backend: the backend's terminator trim never
256 // removes a line's first character, so a whitespace-only line closes the block for this pass but not
257 // for the store, desynchronizing every section below. The checked serializer never emits a tagless
258 // heredoc, so rejection is round-trip safe.
259
2/2
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 10 taken 26 times.
28 if (tag.empty())
260 {
261 2 return fail(ErrorCode::ManifestFramingUnsafe, context);
262 }
263 26 pos = skip_newline(text, pos);
264 26 std::size_t body_bytes = 0;
265 26 bool has_body_line = false;
266 26 bool closed = false;
267
2/2
✓ Branch 38 → 12 taken 70 times.
✓ Branch 38 → 39 taken 2 times.
72 while (pos < text.size())
268 {
269 70 const std::size_t body_start = pos;
270 70 const std::size_t body_end = line_end_from(text, pos);
271 70 const std::string_view body_line = text.substr(body_start, body_end - body_start);
272 70 pos = skip_newline(text, body_end);
273
2/2
✓ Branch 17 → 18 taken 23 times.
✓ Branch 17 → 23 taken 47 times.
70 if (rtrim(body_line) == tag)
274 {
275 // A terminator as the first body line is not an empty value in the backend: its
276 // LoadMultiLineText leaves the value cursor on that line and restores the line break it
277 // NUL-tested, so the store loads the tag line plus every byte up to the next NUL the parser
278 // writes. This pass models that content as empty and never charges it against the caps, and
279 // the content can carry a raw `\r` or a leading `<<<` past the shared validator. An empty
280 // value is emitted raw, never as a heredoc, so rejection is round-trip safe. A blank body line
281 // before the terminator is a genuinely empty value in both tokenizers and stays accepted.
282
2/2
✓ Branch 18 → 19 taken 4 times.
✓ Branch 18 → 22 taken 19 times.
23 if (!has_body_line)
283 {
284 4 return fail(ErrorCode::ManifestFramingUnsafe, context);
285 }
286 19 closed = true;
287 19 break;
288 }
289
5/6
✓ Branch 23 → 24 taken 25 times.
✓ Branch 23 → 26 taken 22 times.
✓ Branch 25 → 26 taken 25 times.
✗ Branch 25 → 29 not taken.
✓ Branch 31 → 32 taken 1 time.
✓ Branch 31 → 35 taken 46 times.
94 if ((has_body_line && !consume_bytes(1, limits.max_field_bytes, body_bytes)) ||
290
2/2
✓ Branch 28 → 29 taken 1 time.
✓ Branch 28 → 30 taken 46 times.
47 !consume_bytes(body_line.size(), limits.max_field_bytes, body_bytes))
291 {
292 1 return fail(ErrorCode::SizeTooLarge, context);
293 }
294 46 has_body_line = true;
295 }
296
2/2
✓ Branch 39 → 40 taken 2 times.
✓ Branch 39 → 43 taken 19 times.
21 if (!closed)
297 {
298 2 return fail(ErrorCode::ManifestFramingUnsafe, context);
299 }
300
1/2
✗ Branch 44 → 45 not taken.
✓ Branch 44 → 48 taken 19 times.
19 if (!consume_bytes(body_bytes, limits.max_total_decoded_bytes, total_bytes))
301 {
302 ✗ return fail(ErrorCode::SizeTooLarge, context);
303 }
304 }
305 else
306 {
307
2/2
✓ Branch 51 → 52 taken 2 times.
✓ Branch 51 → 55 taken 71646 times.
71648 if (value.size() > limits.max_field_bytes)
308 {
309 2 return fail(ErrorCode::SizeTooLarge, context);
310 }
311
2/2
✓ Branch 57 → 58 taken 2 times.
✓ Branch 57 → 61 taken 71644 times.
71646 if (!consume_bytes(value.size(), limits.max_total_decoded_bytes, total_bytes))
312 {
313 2 return fail(ErrorCode::SizeTooLarge, context);
314 }
315 }
316 71663 return {};
317 }
318 } // namespace
319
320 316 Result<void> validate_manifest_grammar(std::string_view text, const GrammarLimits &limits, const char *context)
321 {
322
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 7 taken 314 times.
316 if (text.size() > limits.max_file_bytes)
323 {
324 2 return fail(ErrorCode::SizeTooLarge, context);
325 }
326 // The backend strips a leading UTF-8 BOM before it tokenizes, so the store and this pass read a BOM-prefixed
327 // first line differently. The checked serializer never emits a BOM, so rejection is round-trip safe.
328
2/2
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 12 taken 313 times.
314 if (text.starts_with("\xEF\xBB\xBF"))
329 {
330 1 return fail(ErrorCode::MalformedLine, context);
331 }
332 // The backend's tokenizer is NUL-terminated and stops at the first '\0'. Without this check, this pass
333 // validates the bytes after that NUL, which the store never loads, and a record vanishes without an error.
334 // The checked serializer never emits a NUL, so rejection is round-trip safe.
335
2/2
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 17 taken 312 times.
313 if (text.find('\0') != std::string_view::npos)
336 {
337 1 return fail(ErrorCode::MalformedLine, context);
338 }
339
340 312 std::unordered_set<std::string> seen_sections;
341 312 std::unordered_set<std::string> seen_keys; // reset on each section
342 312 std::unordered_map<std::string, std::size_t> rung_counts;
343 312 std::size_t section_count = 0;
344 312 std::size_t key_count = 0;
345 312 std::size_t record_count = 0;
346 312 std::size_t total_bytes = 0;
347
348 312 const std::size_t size = text.size();
349 312 std::size_t pos = 0;
350
351
2/2
✓ Branch 157 → 22 taken 107710 times.
✓ Branch 157 → 158 taken 3 times.
107713 while (pos < size)
352 {
353 // One whitespace run absorbs leading indentation, blank lines, and every line terminator.
354
6/6
✓ Branch 24 → 25 taken 217602 times.
✓ Branch 24 → 29 taken 217 times.
✓ Branch 27 → 28 taken 110109 times.
✓ Branch 27 → 29 taken 107493 times.
✓ Branch 30 → 23 taken 110109 times.
✓ Branch 30 → 31 taken 107710 times.
217819 while (pos < size && is_space(text[pos]))
355 {
356 110109 ++pos;
357 }
358
2/2
✓ Branch 31 → 32 taken 217 times.
✓ Branch 31 → 33 taken 107493 times.
107710 if (pos >= size)
359 {
360 217 break;
361 }
362
363
6/6
✓ Branch 34 → 35 taken 107492 times.
✓ Branch 34 → 37 taken 1 time.
✓ Branch 36 → 37 taken 1 time.
✓ Branch 36 → 38 taken 107491 times.
✓ Branch 39 → 40 taken 2 times.
✓ Branch 39 → 42 taken 107491 times.
107493 if (text[pos] == ';' || text[pos] == '#')
364 {
365 2 pos = line_end_from(text, pos);
366 35738 continue;
367 }
368
369
2/2
✓ Branch 43 → 44 taken 35777 times.
✓ Branch 43 → 93 taken 71714 times.
107491 if (text[pos] == '[')
370 {
371 35777 ++pos;
372
5/6
✓ Branch 46 → 47 taken 35780 times.
✗ Branch 46 → 51 not taken.
✓ Branch 49 → 50 taken 3 times.
✓ Branch 49 → 51 taken 35777 times.
✓ Branch 52 → 45 taken 3 times.
✓ Branch 52 → 53 taken 35777 times.
35780 while (pos < size && is_space(text[pos]))
373 {
374 3 ++pos;
375 }
376 35777 const std::size_t name_start = pos;
377
7/8
✓ Branch 55 → 56 taken 586904 times.
✗ Branch 55 → 62 not taken.
✓ Branch 57 → 58 taken 551128 times.
✓ Branch 57 → 62 taken 35776 times.
✓ Branch 60 → 61 taken 551127 times.
✓ Branch 60 → 62 taken 1 time.
✓ Branch 63 → 54 taken 551127 times.
✓ Branch 63 → 64 taken 35777 times.
586904 while (pos < size && text[pos] != ']' && !is_newline(text[pos]))
378 {
379 551127 ++pos;
380 }
381
5/6
✓ Branch 64 → 65 taken 35777 times.
✗ Branch 64 → 67 not taken.
✓ Branch 66 → 67 taken 1 time.
✓ Branch 66 → 68 taken 35776 times.
✓ Branch 69 → 70 taken 1 time.
✓ Branch 69 → 73 taken 35776 times.
35777 if (pos >= size || text[pos] != ']')
382 {
383 // The backend does not discard a line with no closing bracket. FindEntry points its section cursor
384 // at the name before it tests for `]`. On the miss, it resumes the scan and leaves that cursor set.
385 // The next key line's NUL terminator then folds the unterminated name and an embedded newline into
386 // a section identity that this pass never validates. A `sig.`-prefixed record can then reach the
387 // store past every collision, prefix, and size check. A canonical manifest never opens a bracket
388 // that it does not close, so this pass fails closed.
389 1 return fail(ErrorCode::MalformedLine, context);
390 }
391
1/2
✓ Branch 73 → 74 taken 35776 times.
✗ Branch 73 → 166 not taken.
35776 const std::string_view name = rtrim(text.substr(name_start, pos - name_start));
392 35776 pos = line_end_from(text, pos);
393
394
4/4
✓ Branch 76 → 77 taken 35762 times.
✓ Branch 76 → 164 taken 14 times.
✓ Branch 78 → 79 taken 18 times.
✓ Branch 78 → 83 taken 35744 times.
35776 DMK_TRY_VOID(register_section(name, limits, context, seen_sections, section_count));
395
4/4
✓ Branch 83 → 84 taken 35742 times.
✓ Branch 83 → 165 taken 2 times.
✓ Branch 85 → 86 taken 6 times.
✓ Branch 85 → 90 taken 35736 times.
35744 DMK_TRY_VOID(charge_signature_section(name, limits, context, rung_counts, record_count));
396 35736 seen_keys.clear();
397 35736 key_count = 0;
398 35736 continue;
399 35736 }
400
401 // Any other line is a key line, and its key runs to the first `=` or newline. A key before the first header
402 // lands in the backend's implicit default section, which parse() never reads, so this pass fails closed.
403
2/2
✓ Branch 93 → 94 taken 1 time.
✓ Branch 93 → 97 taken 71713 times.
71714 if (section_count == 0)
404 {
405 1 return fail(ErrorCode::MalformedLine, context);
406 }
407 71713 const std::size_t key_start = pos;
408
7/8
✓ Branch 99 → 100 taken 547965 times.
✗ Branch 99 → 106 not taken.
✓ Branch 101 → 102 taken 476256 times.
✓ Branch 101 → 106 taken 71709 times.
✓ Branch 104 → 105 taken 476252 times.
✓ Branch 104 → 106 taken 4 times.
✓ Branch 107 → 98 taken 476252 times.
✓ Branch 107 → 108 taken 71713 times.
547965 while (pos < size && text[pos] != '=' && !is_newline(text[pos]))
409 {
410 476252 ++pos;
411 }
412
5/6
✓ Branch 108 → 109 taken 71713 times.
✗ Branch 108 → 111 not taken.
✓ Branch 110 → 111 taken 4 times.
✓ Branch 110 → 112 taken 71709 times.
✓ Branch 113 → 114 taken 4 times.
✓ Branch 113 → 117 taken 71709 times.
71713 if (pos >= size || text[pos] != '=')
413 {
414 // The backend discards a noncomment line with no `=`. A dropped separator can restore a default value.
415 4 return fail(ErrorCode::MalformedLine, context);
416 }
417
2/2
✓ Branch 117 → 118 taken 4 times.
✓ Branch 117 → 121 taken 71705 times.
71709 if (pos == key_start)
418 {
419 // The backend discards an empty key without entry into heredoc mode.
420 4 return fail(ErrorCode::MalformedLine, context);
421 }
422
1/2
✓ Branch 121 → 122 taken 71705 times.
✗ Branch 121 → 168 not taken.
71705 const std::string_view key = rtrim(text.substr(key_start, pos - key_start));
423 71705 ++pos;
424
8/10
✓ Branch 125 → 126 taken 143410 times.
✗ Branch 125 → 134 not taken.
✓ Branch 128 → 129 taken 143407 times.
✓ Branch 128 → 134 taken 3 times.
✓ Branch 130 → 131 taken 71702 times.
✓ Branch 130 → 133 taken 71705 times.
✗ Branch 132 → 133 not taken.
✓ Branch 132 → 134 taken 71702 times.
✓ Branch 135 → 124 taken 71705 times.
✓ Branch 135 → 136 taken 71705 times.
143410 while (pos < size && !is_newline(text[pos]) && (text[pos] == ' ' || text[pos] == '\t'))
425 {
426 71705 ++pos;
427 }
428 71705 const std::size_t value_start = pos;
429 71705 const std::size_t value_end = line_end_from(text, pos);
430
1/2
✓ Branch 137 → 138 taken 71705 times.
✗ Branch 137 → 168 not taken.
71705 const std::string_view value = rtrim(text.substr(value_start, value_end - value_start));
431 71705 pos = value_end;
432
433
4/4
✓ Branch 139 → 140 taken 71680 times.
✓ Branch 139 → 167 taken 25 times.
✓ Branch 141 → 142 taken 4 times.
✓ Branch 141 → 146 taken 71676 times.
71705 DMK_TRY_VOID(register_key(key, limits, context, seen_keys, key_count));
434
2/2
✓ Branch 148 → 149 taken 13 times.
✓ Branch 148 → 153 taken 71663 times.
71676 DMK_TRY_VOID(consume_value(text, value, limits, context, pos, total_bytes));
435 }
436 220 return {};
437 394 }
438 } // namespace DetourModKit::manifest::detail
439