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 | 290323 | [[nodiscard]] bool is_space(char c) noexcept | |
| 25 | { | ||
| 26 |
7/8✓ Branch 2 → 3 taken 286550 times.
✓ Branch 2 → 6 taken 3773 times.
✓ Branch 3 → 4 taken 286550 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 284952 times.
✓ Branch 4 → 6 taken 1598 times.
✓ Branch 5 → 6 taken 71467 times.
✓ Branch 5 → 7 taken 213485 times.
|
290323 | return c == ' ' || c == '\t' || c == '\r' || c == '\n'; |
| 27 | } | ||
| 28 | |||
| 29 | 10761876 | [[nodiscard]] bool is_newline(char c) noexcept | |
| 30 | { | ||
| 31 |
4/4✓ Branch 2 → 3 taken 10760609 times.
✓ Branch 2 → 4 taken 1267 times.
✓ Branch 3 → 4 taken 69947 times.
✓ Branch 3 → 5 taken 10690662 times.
|
10761876 | return c == '\r' || c == '\n'; |
| 32 | } | ||
| 33 | |||
| 34 | 74970 | [[nodiscard]] std::string_view rtrim(std::string_view text) noexcept | |
| 35 | { | ||
| 36 |
6/6✓ Branch 5 → 6 taken 78733 times.
✓ Branch 5 → 10 taken 7 times.
✓ Branch 8 → 9 taken 3770 times.
✓ Branch 8 → 10 taken 74963 times.
✓ Branch 11 → 3 taken 3770 times.
✓ Branch 11 → 12 taken 74970 times.
|
78740 | while (!text.empty() && is_space(text.back())) |
| 37 | { | ||
| 38 | 3770 | text.remove_suffix(1); | |
| 39 | } | ||
| 40 | 74970 | return text; | |
| 41 | } | ||
| 42 | |||
| 43 | 71137 | [[nodiscard]] std::string to_lower(std::string_view text) | |
| 44 | { | ||
| 45 |
1/2✓ Branch 4 → 5 taken 71137 times.
✗ Branch 4 → 25 not taken.
|
71137 | std::string out(text); |
| 46 |
2/2✓ Branch 22 → 8 taken 890965 times.
✓ Branch 22 → 23 taken 71137 times.
|
1033239 | for (char &c : out) |
| 47 | { | ||
| 48 |
4/4✓ Branch 10 → 11 taken 514660 times.
✓ Branch 10 → 13 taken 376305 times.
✓ Branch 11 → 12 taken 405 times.
✓ Branch 11 → 13 taken 514255 times.
|
890965 | if (c >= 'A' && c <= 'Z') |
| 49 | { | ||
| 50 | 405 | c = static_cast<char>(c - 'A' + 'a'); | |
| 51 | } | ||
| 52 | } | ||
| 53 | 71137 | return out; | |
| 54 | } | ||
| 55 | |||
| 56 | 49 | [[nodiscard]] std::unexpected<Error> fail(ErrorCode code, const char *context) noexcept | |
| 57 | { | ||
| 58 | 49 | return std::unexpected(Error{code, context}); | |
| 59 | } | ||
| 60 | |||
| 61 | 3802 | [[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 3799 times.
|
3802 | if (bytes > limit - total) |
| 64 | { | ||
| 65 | 3 | return false; | |
| 66 | } | ||
| 67 | 3799 | total += bytes; | |
| 68 | 3799 | return true; | |
| 69 | } | ||
| 70 | |||
| 71 | 1547 | [[nodiscard]] std::string_view rung_parent(std::string_view name) noexcept | |
| 72 | { | ||
| 73 | 1547 | const std::size_t marker = name.rfind(".rung."); | |
| 74 |
2/2✓ Branch 3 → 4 taken 1418 times.
✓ Branch 3 → 5 taken 129 times.
|
1547 | if (marker == std::string_view::npos) |
| 75 | { | ||
| 76 | 1418 | return {}; | |
| 77 | } | ||
| 78 | 129 | const std::string_view tail = name.substr(marker + 6); | |
| 79 |
1/2✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 129 times.
|
129 | if (tail.empty()) |
| 80 | { | ||
| 81 | ✗ | return {}; | |
| 82 | } | ||
| 83 | 129 | std::size_t index = 0; | |
| 84 |
2/2✓ Branch 17 → 11 taken 175 times.
✓ Branch 17 → 18 taken 129 times.
|
304 | for (const char c : tail) |
| 85 | { | ||
| 86 |
2/4✓ Branch 11 → 12 taken 175 times.
✗ Branch 11 → 13 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 175 times.
|
175 | if (c < '0' || c > '9') |
| 87 | { | ||
| 88 | ✗ | return {}; | |
| 89 | } | ||
| 90 | 175 | const std::size_t digit = static_cast<std::size_t>(c - '0'); | |
| 91 | 175 | 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 175 times.
|
175 | if (index > (MAX_INDEX - digit) / 10U) |
| 93 | { | ||
| 94 | ✗ | return {}; | |
| 95 | } | ||
| 96 | 175 | index = (index * 10U) + digit; | |
| 97 | } | ||
| 98 | 129 | return name.substr(0, marker); | |
| 99 | } | ||
| 100 | } // namespace | ||
| 101 | |||
| 102 | 278 | Result<void> validate_manifest_grammar(std::string_view text, const GrammarLimits &limits, const char *context) | |
| 103 | { | ||
| 104 |
2/2✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 7 taken 276 times.
|
278 | if (text.size() > limits.max_file_bytes) |
| 105 | { | ||
| 106 | 2 | return fail(ErrorCode::SizeTooLarge, context); | |
| 107 | } | ||
| 108 | // The backend strips a leading UTF-8 BOM before tokenizing, so a BOM-prefixed first line is invisible here yet | ||
| 109 | // parsed by the store: its section or key identities would escape collision detection. The checked serializer | ||
| 110 | // never emits a BOM, so rejection is round-trip safe. | ||
| 111 |
2/2✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 12 taken 275 times.
|
276 | if (text.starts_with("\xEF\xBB\xBF")) |
| 112 | { | ||
| 113 | 1 | return fail(ErrorCode::MalformedLine, context); | |
| 114 | } | ||
| 115 | // The backend's tokenizer is NUL-terminated and silently stops at the first '\0', so every byte after it would | ||
| 116 | // be validated here yet never loaded: a record could vanish without an error. The checked serializer never | ||
| 117 | // emits a NUL, so rejection is round-trip safe. | ||
| 118 |
2/2✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 17 taken 274 times.
|
275 | if (text.find('\0') != std::string_view::npos) |
| 119 | { | ||
| 120 | 1 | return fail(ErrorCode::MalformedLine, context); | |
| 121 | } | ||
| 122 | |||
| 123 | 274 | std::unordered_set<std::string> seen_sections; | |
| 124 | 274 | std::unordered_set<std::string> seen_keys; // reset on each section | |
| 125 | 274 | std::unordered_map<std::string, std::size_t> rung_counts; | |
| 126 | 274 | std::size_t section_count = 0; | |
| 127 | 274 | std::size_t key_count = 0; | |
| 128 | 274 | std::size_t record_count = 0; | |
| 129 | 274 | std::size_t total_bytes = 0; | |
| 130 | |||
| 131 | 274 | const std::size_t size = text.size(); | |
| 132 | 274 | std::size_t pos = 0; | |
| 133 | |||
| 134 | 71208 | const auto line_end_from = [&](std::size_t start) noexcept | |
| 135 | { | ||
| 136 |
5/6✓ Branch 4 → 5 taken 9859553 times.
✗ Branch 4 → 9 not taken.
✓ Branch 7 → 8 taken 9788345 times.
✓ Branch 7 → 9 taken 71208 times.
✓ Branch 10 → 3 taken 9788345 times.
✓ Branch 10 → 11 taken 71208 times.
|
9859553 | while (start < size && !is_newline(text[start])) |
| 137 | { | ||
| 138 | 9788345 | ++start; | |
| 139 | } | ||
| 140 | 71208 | return start; | |
| 141 | 274 | }; | |
| 142 | // Consume one line terminator: a lone `\r`, a lone `\n`, or a `\r\n` pair, matching the backend's SkipNewLine. | ||
| 143 | 96 | const auto skip_newline = [&](std::size_t at) noexcept | |
| 144 | { | ||
| 145 |
5/6✓ Branch 2 → 3 taken 96 times.
✗ Branch 2 → 6 not taken.
✓ Branch 4 → 5 taken 72 times.
✓ Branch 4 → 6 taken 24 times.
✓ Branch 7 → 8 taken 72 times.
✓ Branch 7 → 15 taken 24 times.
|
96 | if (at < size && text[at] == '\r') |
| 146 | { | ||
| 147 | 72 | ++at; | |
| 148 |
3/6✓ Branch 8 → 9 taken 72 times.
✗ Branch 8 → 12 not taken.
✓ Branch 10 → 11 taken 72 times.
✗ Branch 10 → 12 not taken.
✓ Branch 13 → 14 taken 72 times.
✗ Branch 13 → 22 not taken.
|
72 | if (at < size && text[at] == '\n') |
| 149 | { | ||
| 150 | 72 | ++at; | |
| 151 | } | ||
| 152 | } | ||
| 153 |
3/6✓ Branch 15 → 16 taken 24 times.
✗ Branch 15 → 19 not taken.
✓ Branch 17 → 18 taken 24 times.
✗ Branch 17 → 19 not taken.
✓ Branch 20 → 21 taken 24 times.
✗ Branch 20 → 22 not taken.
|
24 | else if (at < size && text[at] == '\n') |
| 154 | { | ||
| 155 | 24 | ++at; | |
| 156 | } | ||
| 157 | 96 | return at; | |
| 158 | 274 | }; | |
| 159 | |||
| 160 |
2/2✓ Branch 284 → 22 taken 71344 times.
✓ Branch 284 → 285 taken 3 times.
|
71347 | while (pos < size) |
| 161 | { | ||
| 162 | // Skip whitespace runs, which folds away leading indentation, blank lines, and every line terminator. | ||
| 163 |
6/6✓ Branch 24 → 25 taken 144212 times.
✓ Branch 24 → 29 taken 197 times.
✓ Branch 27 → 28 taken 73065 times.
✓ Branch 27 → 29 taken 71147 times.
✓ Branch 30 → 23 taken 73065 times.
✓ Branch 30 → 31 taken 71344 times.
|
144409 | while (pos < size && is_space(text[pos])) |
| 164 | { | ||
| 165 | 73065 | ++pos; | |
| 166 | } | ||
| 167 |
2/2✓ Branch 31 → 32 taken 197 times.
✓ Branch 31 → 33 taken 71147 times.
|
71344 | if (pos >= size) |
| 168 | { | ||
| 169 | 197 | break; | |
| 170 | } | ||
| 171 | |||
| 172 | // Comment line. | ||
| 173 |
5/6✓ Branch 34 → 35 taken 71147 times.
✗ Branch 34 → 37 not taken.
✓ Branch 36 → 37 taken 1 time.
✓ Branch 36 → 38 taken 71146 times.
✓ Branch 39 → 40 taken 1 time.
✓ Branch 39 → 42 taken 71146 times.
|
71147 | if (text[pos] == ';' || text[pos] == '#') |
| 174 | { | ||
| 175 | 1 | pos = line_end_from(pos); | |
| 176 | 67345 | continue; | |
| 177 | } | ||
| 178 | |||
| 179 | // Section header. | ||
| 180 |
2/2✓ Branch 43 → 44 taken 67375 times.
✓ Branch 43 → 158 taken 3771 times.
|
71146 | if (text[pos] == '[') |
| 181 | { | ||
| 182 | 67375 | ++pos; | |
| 183 |
5/6✓ Branch 46 → 47 taken 67378 times.
✗ Branch 46 → 51 not taken.
✓ Branch 49 → 50 taken 3 times.
✓ Branch 49 → 51 taken 67375 times.
✓ Branch 52 → 45 taken 3 times.
✓ Branch 52 → 53 taken 67375 times.
|
67378 | while (pos < size && is_space(text[pos])) |
| 184 | { | ||
| 185 | 3 | ++pos; | |
| 186 | } | ||
| 187 | 67375 | const std::size_t name_start = pos; | |
| 188 |
7/8✓ Branch 55 → 56 taken 928391 times.
✗ Branch 55 → 62 not taken.
✓ Branch 57 → 58 taken 861017 times.
✓ Branch 57 → 62 taken 67374 times.
✓ Branch 60 → 61 taken 861016 times.
✓ Branch 60 → 62 taken 1 time.
✓ Branch 63 → 54 taken 861016 times.
✓ Branch 63 → 64 taken 67375 times.
|
928391 | while (pos < size && text[pos] != ']' && !is_newline(text[pos])) |
| 189 | { | ||
| 190 | 861016 | ++pos; | |
| 191 | } | ||
| 192 |
5/6✓ Branch 64 → 65 taken 67375 times.
✗ Branch 64 → 67 not taken.
✓ Branch 66 → 67 taken 1 time.
✓ Branch 66 → 68 taken 67374 times.
✓ Branch 69 → 70 taken 1 time.
✓ Branch 69 → 73 taken 67374 times.
|
67375 | if (pos >= size || text[pos] != ']') |
| 193 | { | ||
| 194 | // No closing bracket. The backend does not discard this line: FindEntry points its section cursor | ||
| 195 | // at the name before testing for `]`, and on the miss it resumes the scan without clearing that | ||
| 196 | // cursor, so the next key line's NUL terminator folds the unterminated name plus an embedded | ||
| 197 | // newline into a section identity this pass never validated. A `sig.`-prefixed record can reach | ||
| 198 | // the store past every collision, prefix, and size check. A canonical manifest never opens a | ||
| 199 | // bracket it does not close, so fail closed. | ||
| 200 | 1 | return fail(ErrorCode::MalformedLine, context); | |
| 201 | } | ||
| 202 |
1/2✓ Branch 73 → 74 taken 67374 times.
✗ Branch 73 → 305 not taken.
|
67374 | const std::string_view name = rtrim(text.substr(name_start, pos - name_start)); |
| 203 | 67374 | pos = line_end_from(pos); | |
| 204 | |||
| 205 |
1/2✓ Branch 76 → 77 taken 67374 times.
✗ Branch 76 → 305 not taken.
|
67374 | std::string folded = to_lower(name); |
| 206 | // An empty section name (`[]`, `[ ]`) is the backend's implicit default section, which also holds any | ||
| 207 | // keys before the first header; re-opening it would let a key collision split across the `[]` escape | ||
| 208 | // the per-section namespace. A canonical manifest never names it, so reject it outright. | ||
| 209 |
2/2✓ Branch 78 → 79 taken 1 time.
✓ Branch 78 → 82 taken 67373 times.
|
67374 | if (folded.empty()) |
| 210 | { | ||
| 211 | 1 | return fail(ErrorCode::MalformedLine, context); | |
| 212 | } | ||
| 213 | // The `manifest` header and every `sig.` prefix must be canonical lowercase, else the case-sensitive | ||
| 214 | // store would silently drop the section (a lost record). Fail closed instead. | ||
| 215 |
9/10✓ Branch 82 → 83 taken 67373 times.
✗ Branch 82 → 291 not taken.
✓ Branch 83 → 84 taken 273 times.
✓ Branch 83 → 87 taken 67100 times.
✓ Branch 86 → 87 taken 272 times.
✓ Branch 86 → 91 taken 1 time.
✓ Branch 88 → 89 taken 1560 times.
✓ Branch 88 → 92 taken 65812 times.
✓ Branch 93 → 94 taken 2 times.
✓ Branch 93 → 97 taken 67371 times.
|
136305 | if ((folded == "manifest" && name != "manifest") || |
| 216 |
2/2✓ Branch 90 → 91 taken 1 time.
✓ Branch 90 → 92 taken 1559 times.
|
68932 | (folded.starts_with("sig.") && !name.starts_with("sig."))) |
| 217 | { | ||
| 218 | 2 | return fail(ErrorCode::MalformedLine, context); | |
| 219 | } | ||
| 220 | // The folded, trimmed name is the merge key: two sections reaching it by case, whitespace, or exact | ||
| 221 | // repetition would collapse into one before the trust gate. | ||
| 222 |
2/2✓ Branch 97 → 98 taken 2 times.
✓ Branch 97 → 101 taken 67369 times.
|
67371 | if (section_count >= limits.max_sections) |
| 223 | { | ||
| 224 | 2 | return fail(ErrorCode::SizeTooLarge, context); | |
| 225 | } | ||
| 226 |
4/4✓ Branch 103 → 104 taken 67358 times.
✓ Branch 103 → 292 taken 11 times.
✓ Branch 104 → 105 taken 8 times.
✓ Branch 104 → 108 taken 67350 times.
|
67369 | if (!seen_sections.insert(std::move(folded)).second) |
| 227 | { | ||
| 228 | 8 | return fail(ErrorCode::ManifestIdentityCollision, context); | |
| 229 | } | ||
| 230 | 67350 | ++section_count; | |
| 231 | |||
| 232 | // Classify record vs rung on the raw name, not the fold: parse() reads `.rung.` case-sensitively and | ||
| 233 | // treats a miscased marker as an ordinary label, so folding here would charge a legitimate record | ||
| 234 | // (e.g. `sig.a.RUNG.0`, label `a.RUNG.0`) against the rung cap the two passes must agree on. | ||
| 235 |
2/2✓ Branch 109 → 110 taken 1547 times.
✓ Branch 109 → 152 taken 65803 times.
|
67350 | if (name.starts_with("sig.")) |
| 236 | { | ||
| 237 | 1547 | const std::string_view parent = rung_parent(name); | |
| 238 |
6/6✓ Branch 112 → 113 taken 129 times.
✓ Branch 112 → 116 taken 1418 times.
✓ Branch 114 → 115 taken 1 time.
✓ Branch 114 → 116 taken 128 times.
✓ Branch 117 → 118 taken 1 time.
✓ Branch 117 → 121 taken 1546 times.
|
1547 | if (!parent.empty() && parent.size() <= 4U) |
| 239 | { | ||
| 240 | 1 | return fail(ErrorCode::MalformedLine, context); | |
| 241 | } | ||
| 242 |
4/6✓ Branch 122 → 123 taken 1418 times.
✓ Branch 122 → 124 taken 128 times.
✓ Branch 123 → 125 taken 1418 times.
✗ Branch 123 → 302 not taken.
✓ Branch 124 → 125 taken 128 times.
✗ Branch 124 → 302 not taken.
|
1546 | const std::string_view label = parent.empty() ? name.substr(4) : parent.substr(4); |
| 243 |
2/2✓ Branch 126 → 127 taken 1 time.
✓ Branch 126 → 130 taken 1545 times.
|
1546 | if (label.size() > limits.max_field_bytes) |
| 244 | { | ||
| 245 | 1 | return fail(ErrorCode::SizeTooLarge, context); | |
| 246 | } | ||
| 247 |
2/2✓ Branch 131 → 132 taken 1417 times.
✓ Branch 131 → 137 taken 128 times.
|
1545 | if (parent.empty()) |
| 248 | { | ||
| 249 |
2/2✓ Branch 132 → 133 taken 2 times.
✓ Branch 132 → 136 taken 1415 times.
|
1417 | if (record_count >= limits.max_records) |
| 250 | { | ||
| 251 | 2 | return fail(ErrorCode::SizeTooLarge, context); | |
| 252 | } | ||
| 253 | 1415 | ++record_count; | |
| 254 | } | ||
| 255 | else | ||
| 256 | { | ||
| 257 |
2/4✓ Branch 139 → 140 taken 128 times.
✗ Branch 139 → 295 not taken.
✓ Branch 140 → 141 taken 128 times.
✗ Branch 140 → 293 not taken.
|
256 | auto count_it = rung_counts.try_emplace(std::string(parent), 0).first; |
| 258 |
2/2✓ Branch 144 → 145 taken 2 times.
✓ Branch 144 → 148 taken 126 times.
|
128 | if (count_it->second >= limits.max_rungs_per_record) |
| 259 | { | ||
| 260 | 2 | return fail(ErrorCode::SizeTooLarge, context); | |
| 261 | } | ||
| 262 | 126 | ++count_it->second; | |
| 263 | } | ||
| 264 | } | ||
| 265 | 67344 | seen_keys.clear(); | |
| 266 | 67344 | key_count = 0; | |
| 267 | 67344 | continue; | |
| 268 |
2/2✓ Branch 155 → 156 taken 19 times.
✓ Branch 155 → 157 taken 67344 times.
|
67374 | } |
| 269 | |||
| 270 | // Key line. The key runs to the first `=` or newline. | ||
| 271 | 3771 | const std::size_t key_start = pos; | |
| 272 |
7/8✓ Branch 160 → 161 taken 37547 times.
✗ Branch 160 → 167 not taken.
✓ Branch 162 → 163 taken 33780 times.
✓ Branch 162 → 167 taken 3767 times.
✓ Branch 165 → 166 taken 33776 times.
✓ Branch 165 → 167 taken 4 times.
✓ Branch 168 → 159 taken 33776 times.
✓ Branch 168 → 169 taken 3771 times.
|
37547 | while (pos < size && text[pos] != '=' && !is_newline(text[pos])) |
| 273 | { | ||
| 274 | 33776 | ++pos; | |
| 275 | } | ||
| 276 |
5/6✓ Branch 169 → 170 taken 3771 times.
✗ Branch 169 → 172 not taken.
✓ Branch 171 → 172 taken 4 times.
✓ Branch 171 → 173 taken 3767 times.
✓ Branch 174 → 175 taken 4 times.
✓ Branch 174 → 178 taken 3767 times.
|
3771 | if (pos >= size || text[pos] != '=') |
| 277 | { | ||
| 278 | // The backend discards a noncomment line with no `=`. A dropped separator can restore a default value. | ||
| 279 | 4 | return fail(ErrorCode::MalformedLine, context); | |
| 280 | } | ||
| 281 |
2/2✓ Branch 178 → 179 taken 4 times.
✓ Branch 178 → 182 taken 3763 times.
|
3767 | if (pos == key_start) |
| 282 | { | ||
| 283 | // The backend discards an empty key without entry into heredoc mode. | ||
| 284 | 4 | return fail(ErrorCode::MalformedLine, context); | |
| 285 | } | ||
| 286 |
1/2✓ Branch 182 → 183 taken 3763 times.
✗ Branch 182 → 311 not taken.
|
3763 | const std::string_view key = rtrim(text.substr(key_start, pos - key_start)); |
| 287 | 3763 | ++pos; // past '=' | |
| 288 |
8/10✓ Branch 186 → 187 taken 7526 times.
✗ Branch 186 → 195 not taken.
✓ Branch 189 → 190 taken 7525 times.
✓ Branch 189 → 195 taken 1 time.
✓ Branch 191 → 192 taken 3762 times.
✓ Branch 191 → 194 taken 3763 times.
✗ Branch 193 → 194 not taken.
✓ Branch 193 → 195 taken 3762 times.
✓ Branch 196 → 185 taken 3763 times.
✓ Branch 196 → 197 taken 3763 times.
|
7526 | while (pos < size && !is_newline(text[pos]) && (text[pos] == ' ' || text[pos] == '\t')) |
| 289 | { | ||
| 290 | 3763 | ++pos; | |
| 291 | } | ||
| 292 | 3763 | const std::size_t value_start = pos; | |
| 293 | 3763 | const std::size_t value_end = line_end_from(pos); | |
| 294 |
1/2✓ Branch 198 → 199 taken 3763 times.
✗ Branch 198 → 311 not taken.
|
3763 | const std::string_view value = rtrim(text.substr(value_start, value_end - value_start)); |
| 295 | 3763 | pos = value_end; | |
| 296 | |||
| 297 |
1/2✓ Branch 200 → 201 taken 3763 times.
✗ Branch 200 → 311 not taken.
|
3763 | std::string folded_key = to_lower(key); |
| 298 |
2/2✓ Branch 203 → 204 taken 1 time.
✓ Branch 203 → 207 taken 3762 times.
|
3763 | if (key != folded_key) |
| 299 | { | ||
| 300 | 1 | return fail(ErrorCode::MalformedLine, context); | |
| 301 | } | ||
| 302 |
2/2✓ Branch 207 → 208 taken 2 times.
✓ Branch 207 → 211 taken 3760 times.
|
3762 | if (key_count >= limits.max_keys_per_section) |
| 303 | { | ||
| 304 | 2 | return fail(ErrorCode::SizeTooLarge, context); | |
| 305 | } | ||
| 306 |
4/4✓ Branch 213 → 214 taken 3742 times.
✓ Branch 213 → 306 taken 18 times.
✓ Branch 214 → 215 taken 1 time.
✓ Branch 214 → 218 taken 3741 times.
|
3760 | if (!seen_keys.insert(std::move(folded_key)).second) |
| 307 | { | ||
| 308 | 1 | return fail(ErrorCode::ManifestIdentityCollision, context); | |
| 309 | } | ||
| 310 | 3741 | ++key_count; | |
| 311 | |||
| 312 |
2/2✓ Branch 219 → 220 taken 28 times.
✓ Branch 219 → 265 taken 3713 times.
|
3741 | if (value.starts_with("<<<")) |
| 313 | { | ||
| 314 | // A heredoc runs until a line whose trailing-trimmed form EQUALS the tag (case-sensitive, matching the | ||
| 315 | // case-sensitive store). An unterminated block would absorb every record below it into this value. | ||
| 316 |
1/2✓ Branch 220 → 221 taken 28 times.
✗ Branch 220 → 308 not taken.
|
28 | const std::string_view tag = value.substr(3); |
| 317 | // An empty tag terminates differently here and in the backend: the backend's terminator trim never | ||
| 318 | // removes a line's first character, so a whitespace-only line closes the block for this pass but not | ||
| 319 | // for the store, desynchronizing every section below. The checked serializer never emits a tagless | ||
| 320 | // heredoc, so rejection is round-trip safe. | ||
| 321 |
2/2✓ Branch 222 → 223 taken 2 times.
✓ Branch 222 → 226 taken 26 times.
|
28 | if (tag.empty()) |
| 322 | { | ||
| 323 | 2 | return fail(ErrorCode::ManifestFramingUnsafe, context); | |
| 324 | } | ||
| 325 | 26 | pos = skip_newline(pos); | |
| 326 | 26 | std::size_t body_bytes = 0; | |
| 327 | 26 | bool has_body_line = false; | |
| 328 | 26 | bool closed = false; | |
| 329 |
2/2✓ Branch 253 → 228 taken 70 times.
✓ Branch 253 → 254 taken 2 times.
|
72 | while (pos < size) |
| 330 | { | ||
| 331 | 70 | const std::size_t body_start = pos; | |
| 332 | 70 | const std::size_t body_end = line_end_from(pos); | |
| 333 |
1/2✓ Branch 229 → 230 taken 70 times.
✗ Branch 229 → 307 not taken.
|
70 | const std::string_view body_line = text.substr(body_start, body_end - body_start); |
| 334 | 70 | pos = skip_newline(body_end); | |
| 335 |
2/2✓ Branch 233 → 234 taken 23 times.
✓ Branch 233 → 239 taken 47 times.
|
70 | if (rtrim(body_line) == tag) |
| 336 | { | ||
| 337 | // A terminator as the first body line is not an empty value in the backend: its | ||
| 338 | // LoadMultiLineText leaves the value cursor on that line and restores the line break it | ||
| 339 | // NUL-tested, so the store loads the tag line plus every byte up to the next NUL the parser | ||
| 340 | // writes. This pass models that content as empty and never charges it against the caps, and | ||
| 341 | // the content can carry a raw `\r` or a leading `<<<` past the shared validator. An empty | ||
| 342 | // value is emitted raw, never as a heredoc, so rejection is round-trip safe. A blank body line | ||
| 343 | // before the terminator is a genuinely empty value in both tokenizers and stays accepted. | ||
| 344 |
2/2✓ Branch 234 → 235 taken 4 times.
✓ Branch 234 → 238 taken 19 times.
|
23 | if (!has_body_line) |
| 345 | { | ||
| 346 | 4 | return fail(ErrorCode::ManifestFramingUnsafe, context); | |
| 347 | } | ||
| 348 | 19 | closed = true; | |
| 349 | 19 | break; | |
| 350 | } | ||
| 351 |
5/6✓ Branch 239 → 240 taken 25 times.
✓ Branch 239 → 242 taken 22 times.
✓ Branch 241 → 242 taken 25 times.
✗ Branch 241 → 245 not taken.
✓ Branch 247 → 248 taken 1 time.
✓ Branch 247 → 251 taken 46 times.
|
94 | if ((has_body_line && !consume_bytes(1, limits.max_field_bytes, body_bytes)) || |
| 352 |
2/2✓ Branch 244 → 245 taken 1 time.
✓ Branch 244 → 246 taken 46 times.
|
47 | !consume_bytes(body_line.size(), limits.max_field_bytes, body_bytes)) |
| 353 | { | ||
| 354 | 1 | return fail(ErrorCode::SizeTooLarge, context); | |
| 355 | } | ||
| 356 | 46 | has_body_line = true; | |
| 357 | } | ||
| 358 |
2/2✓ Branch 254 → 255 taken 2 times.
✓ Branch 254 → 258 taken 19 times.
|
21 | if (!closed) |
| 359 | { | ||
| 360 | 2 | return fail(ErrorCode::ManifestFramingUnsafe, context); | |
| 361 | } | ||
| 362 |
1/2✗ Branch 259 → 260 not taken.
✓ Branch 259 → 263 taken 19 times.
|
19 | if (!consume_bytes(body_bytes, limits.max_total_decoded_bytes, total_bytes)) |
| 363 | { | ||
| 364 | ✗ | return fail(ErrorCode::SizeTooLarge, context); | |
| 365 | } | ||
| 366 | } | ||
| 367 | else | ||
| 368 | { | ||
| 369 |
2/2✓ Branch 266 → 267 taken 2 times.
✓ Branch 266 → 270 taken 3711 times.
|
3713 | if (value.size() > limits.max_field_bytes) |
| 370 | { | ||
| 371 | 2 | return fail(ErrorCode::SizeTooLarge, context); | |
| 372 | } | ||
| 373 |
2/2✓ Branch 272 → 273 taken 2 times.
✓ Branch 272 → 276 taken 3709 times.
|
3711 | if (!consume_bytes(value.size(), limits.max_total_decoded_bytes, total_bytes)) |
| 374 | { | ||
| 375 | 2 | return fail(ErrorCode::SizeTooLarge, context); | |
| 376 | } | ||
| 377 | } | ||
| 378 |
2/2✓ Branch 278 → 279 taken 3728 times.
✓ Branch 278 → 282 taken 17 times.
|
3763 | } |
| 379 | 200 | return {}; | |
| 380 | 332 | } | |
| 381 | } // namespace DetourModKit::manifest::detail | ||
| 382 |