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