GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 87.7% 100 / 0 / 114
Functions: 100.0% 7 / 0 / 7
Branches: 65.7% 88 / 0 / 134

src/drift_manifest.cpp
Line Branch Exec Source
1 /**
2 * @file drift_manifest.cpp
3 * @brief Durable serialization of self-heal drift reports.
4 */
5
6 #include "DetourModKit/drift_manifest.hpp"
7
8 #include <charconv>
9 #include <fstream>
10 #include <iterator>
11 #include <string>
12
13 namespace DetourModKit
14 {
15 namespace Rtti
16 {
17 namespace
18 {
19 constexpr std::string_view MANIFEST_HEADER = "# DetourModKit drift manifest v1";
20 constexpr char FIELD_SEP = '\t';
21
22 // Stable round-trip tokens for HealError, deliberately distinct from the verbose human-readable
23 // heal_error_to_string text (which is for logs):
24 // a manifest must parse back even if the log wording is reworded.
25 5 [[nodiscard]] std::string_view heal_error_token(HealError error) noexcept
26 {
27
2/4
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 4 taken 1 time.
✗ Branch 2 → 5 not taken.
✗ Branch 2 → 6 not taken.
5 switch (error)
28 {
29 4 case HealError::BadDescriptor:
30 4 return "BadDescriptor";
31 1 case HealError::NoMatch:
32 1 return "NoMatch";
33 case HealError::Ambiguous:
34 return "Ambiguous";
35 }
36 return "BadDescriptor";
37 }
38
39 6 [[nodiscard]] bool parse_heal_error(std::string_view token, HealError &out) noexcept
40 {
41
2/2
✓ Branch 4 → 5 taken 5 times.
✓ Branch 4 → 6 taken 1 time.
6 if (token == "BadDescriptor")
42 {
43 5 out = HealError::BadDescriptor;
44 5 return true;
45 }
46
1/2
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 10 not taken.
1 if (token == "NoMatch")
47 {
48 1 out = HealError::NoMatch;
49 1 return true;
50 }
51 if (token == "Ambiguous")
52 {
53 out = HealError::Ambiguous;
54 return true;
55 }
56 return false;
57 }
58
59 // Parses a decimal (possibly negative) offset that must span the whole field.
60 19 [[nodiscard]] bool parse_offset(std::string_view field, std::ptrdiff_t &out) noexcept
61 {
62
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 19 times.
19 if (field.empty())
63 {
64 return false;
65 }
66 19 const char *const begin = field.data();
67 19 const char *const end = field.data() + field.size();
68 19 const auto result = std::from_chars(begin, end, out);
69
3/4
✓ Branch 9 → 10 taken 18 times.
✓ Branch 9 → 12 taken 1 time.
✓ Branch 10 → 11 taken 18 times.
✗ Branch 10 → 12 not taken.
19 return result.ec == std::errc{} && result.ptr == end;
70 }
71 } // anonymous namespace
72
73 5 std::string serialize_drift_report(std::span<const DriftEntry> entries)
74 {
75 5 std::string out;
76
1/2
✓ Branch 5 → 6 taken 5 times.
✗ Branch 5 → 60 not taken.
5 out.append(MANIFEST_HEADER.data(), MANIFEST_HEADER.size());
77
1/2
✓ Branch 6 → 7 taken 5 times.
✗ Branch 6 → 60 not taken.
5 out.push_back('\n');
78
2/2
✓ Branch 46 → 9 taken 5 times.
✓ Branch 46 → 47 taken 5 times.
15 for (const DriftEntry &entry : entries)
79 {
80
1/2
✓ Branch 13 → 14 taken 5 times.
✗ Branch 13 → 58 not taken.
5 out.append(entry.name.data(), entry.name.size());
81
1/2
✓ Branch 14 → 15 taken 5 times.
✗ Branch 14 → 58 not taken.
5 out.push_back(FIELD_SEP);
82
2/4
✓ Branch 15 → 16 taken 5 times.
✗ Branch 15 → 51 not taken.
✓ Branch 16 → 17 taken 5 times.
✗ Branch 16 → 49 not taken.
5 out.append(std::to_string(entry.nominal_offset));
83
1/2
✓ Branch 18 → 19 taken 5 times.
✗ Branch 18 → 58 not taken.
5 out.push_back(FIELD_SEP);
84
2/4
✓ Branch 19 → 20 taken 5 times.
✗ Branch 19 → 54 not taken.
✓ Branch 20 → 21 taken 5 times.
✗ Branch 20 → 52 not taken.
5 out.append(std::to_string(entry.healed_offset));
85
1/2
✓ Branch 22 → 23 taken 5 times.
✗ Branch 22 → 58 not taken.
5 out.push_back(FIELD_SEP);
86
2/4
✓ Branch 23 → 24 taken 5 times.
✗ Branch 23 → 57 not taken.
✓ Branch 24 → 25 taken 5 times.
✗ Branch 24 → 55 not taken.
5 out.append(std::to_string(entry.delta));
87
1/2
✓ Branch 26 → 27 taken 5 times.
✗ Branch 26 → 58 not taken.
5 out.push_back(FIELD_SEP);
88
3/4
✓ Branch 27 → 28 taken 3 times.
✓ Branch 27 → 29 taken 2 times.
✓ Branch 30 → 31 taken 5 times.
✗ Branch 30 → 58 not taken.
5 out.push_back(entry.ok ? '1' : '0');
89
1/2
✓ Branch 31 → 32 taken 5 times.
✗ Branch 31 → 58 not taken.
5 out.push_back(FIELD_SEP);
90 5 const std::string_view token = heal_error_token(entry.error);
91
1/2
✓ Branch 35 → 36 taken 5 times.
✗ Branch 35 → 58 not taken.
5 out.append(token.data(), token.size());
92
1/2
✓ Branch 36 → 37 taken 5 times.
✗ Branch 36 → 58 not taken.
5 out.push_back('\n');
93 }
94 5 return out;
95 }
96
97 11 std::expected<std::vector<DriftRecord>, ManifestError> parse_drift_report(std::string_view text)
98 {
99 11 std::vector<DriftRecord> records;
100 11 bool header_seen = false;
101 11 std::size_t pos = 0;
102
2/2
✓ Branch 91 → 3 taken 27 times.
✓ Branch 91 → 92 taken 7 times.
34 while (pos <= text.size())
103 {
104 27 const std::size_t newline = text.find('\n', pos);
105 std::string_view line =
106
4/6
✓ Branch 4 → 5 taken 7 times.
✓ Branch 4 → 6 taken 20 times.
✓ Branch 5 → 7 taken 7 times.
✗ Branch 5 → 107 not taken.
✓ Branch 6 → 7 taken 20 times.
✗ Branch 6 → 107 not taken.
27 (newline == std::string_view::npos) ? text.substr(pos) : text.substr(pos, newline - pos);
107
2/2
✓ Branch 7 → 8 taken 7 times.
✓ Branch 7 → 10 taken 20 times.
27 pos = (newline == std::string_view::npos) ? text.size() + 1 : newline + 1;
108
109 // Strip a trailing CR (CRLF input) and skip blank lines.
110
6/6
✓ Branch 12 → 13 taken 20 times.
✓ Branch 12 → 16 taken 7 times.
✓ Branch 14 → 15 taken 4 times.
✓ Branch 14 → 16 taken 16 times.
✓ Branch 17 → 18 taken 4 times.
✓ Branch 17 → 19 taken 23 times.
27 if (!line.empty() && line.back() == '\r')
111 {
112 4 line.remove_suffix(1);
113 }
114
2/2
✓ Branch 20 → 21 taken 9 times.
✓ Branch 20 → 22 taken 18 times.
27 if (line.empty())
115 {
116 17 continue;
117 }
118
119
2/2
✓ Branch 22 → 23 taken 10 times.
✓ Branch 22 → 29 taken 8 times.
18 if (!header_seen)
120 {
121
2/2
✓ Branch 24 → 25 taken 2 times.
✓ Branch 24 → 28 taken 8 times.
10 if (line != MANIFEST_HEADER)
122 {
123 2 return std::unexpected(ManifestError::MissingHeader);
124 }
125 8 header_seen = true;
126 8 continue;
127 }
128
129 // Split into exactly six tab-separated fields; any other count is malformed.
130 8 std::string_view fields[6];
131 8 std::size_t field_count = 0;
132 8 std::size_t field_pos = 0;
133 8 bool too_many = false;
134 while (true)
135 {
136 45 const std::size_t sep = line.find(FIELD_SEP, field_pos);
137 const std::string_view field = (sep == std::string_view::npos)
138
3/4
✓ Branch 31 → 32 taken 8 times.
✓ Branch 31 → 33 taken 37 times.
✓ Branch 32 → 34 taken 8 times.
✗ Branch 32 → 100 not taken.
45 ? line.substr(field_pos)
139
1/2
✓ Branch 33 → 34 taken 37 times.
✗ Branch 33 → 100 not taken.
37 : line.substr(field_pos, sep - field_pos);
140
1/2
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 36 taken 45 times.
45 if (field_count >= 6)
141 {
142 too_many = true;
143 break;
144 }
145 45 fields[field_count++] = field;
146
2/2
✓ Branch 36 → 37 taken 8 times.
✓ Branch 36 → 38 taken 37 times.
45 if (sep == std::string_view::npos)
147 {
148 8 break;
149 }
150 37 field_pos = sep + 1;
151 37 }
152
3/4
✓ Branch 39 → 40 taken 8 times.
✗ Branch 39 → 41 not taken.
✓ Branch 40 → 41 taken 1 time.
✓ Branch 40 → 44 taken 7 times.
8 if (too_many || field_count != 6)
153 {
154 1 return std::unexpected(ManifestError::MalformedLine);
155 }
156
157 7 DriftRecord record;
158
1/2
✓ Branch 47 → 48 taken 7 times.
✗ Branch 47 → 101 not taken.
7 record.name = std::string(fields[0]);
159
5/6
✓ Branch 52 → 53 taken 6 times.
✓ Branch 52 → 57 taken 1 time.
✓ Branch 54 → 55 taken 6 times.
✗ Branch 54 → 57 not taken.
✓ Branch 59 → 60 taken 1 time.
✓ Branch 59 → 63 taken 6 times.
13 if (!parse_offset(fields[1], record.nominal_offset) || !parse_offset(fields[2], record.healed_offset) ||
160
1/2
✗ Branch 56 → 57 not taken.
✓ Branch 56 → 58 taken 6 times.
6 !parse_offset(fields[3], record.delta))
161 {
162 1 return std::unexpected(ManifestError::MalformedLine);
163 }
164
2/2
✓ Branch 65 → 66 taken 4 times.
✓ Branch 65 → 67 taken 2 times.
6 if (fields[4] == "1")
165 {
166 4 record.ok = true;
167 }
168
1/2
✓ Branch 69 → 70 taken 2 times.
✗ Branch 69 → 71 not taken.
2 else if (fields[4] == "0")
169 {
170 2 record.ok = false;
171 }
172 else
173 {
174 return std::unexpected(ManifestError::MalformedLine);
175 }
176
1/2
✗ Branch 75 → 76 not taken.
✓ Branch 75 → 79 taken 6 times.
6 if (!parse_heal_error(fields[5], record.error))
177 {
178 return std::unexpected(ManifestError::MalformedLine);
179 }
180
1/2
✓ Branch 81 → 82 taken 6 times.
✗ Branch 81 → 105 not taken.
6 records.push_back(std::move(record));
181
2/2
✓ Branch 84 → 85 taken 6 times.
✓ Branch 84 → 88 taken 1 time.
7 }
182
183
2/2
✓ Branch 92 → 93 taken 1 time.
✓ Branch 92 → 96 taken 6 times.
7 if (!header_seen)
184 {
185 1 return std::unexpected(ManifestError::MissingHeader);
186 }
187 6 return records;
188 11 }
189
190 1 bool write_drift_report_to_file(const std::string &path, std::span<const DriftEntry> entries)
191 {
192 // Binary mode so the '\n' line endings written here are not translated to
193 // CRLF; the parser tolerates either, but a stable on-disk form is clearer.
194
1/2
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 21 not taken.
1 std::ofstream file(path, std::ios::binary | std::ios::trunc);
195
2/4
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 19 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 1 time.
1 if (!file)
196 {
197 return false;
198 }
199
1/2
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 19 not taken.
1 const std::string text = serialize_drift_report(entries);
200
1/2
✓ Branch 10 → 11 taken 1 time.
✗ Branch 10 → 17 not taken.
1 file.write(text.data(), static_cast<std::streamsize>(text.size()));
201
1/2
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 17 not taken.
1 return static_cast<bool>(file);
202 1 }
203
204 4 std::expected<std::vector<DriftRecord>, ManifestError> read_drift_report_from_file(const std::string &path)
205 {
206
1/2
✓ Branch 2 → 3 taken 4 times.
✗ Branch 2 → 30 not taken.
4 std::ifstream file(path, std::ios::binary);
207
3/4
✓ Branch 3 → 4 taken 4 times.
✗ Branch 3 → 28 not taken.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 8 taken 3 times.
4 if (!file)
208 {
209 // An open failure (missing file, lock, permission, or a directory) is distinct from a
210 // present-but-corrupt manifest: the latter flows through parse_drift_report and reports MissingHeader /
211 // MalformedLine.
212 1 return std::unexpected(ManifestError::FileOpenFailed);
213 }
214
1/2
✓ Branch 12 → 13 taken 3 times.
✗ Branch 12 → 21 not taken.
3 const std::string text((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
215
1/2
✓ Branch 15 → 16 taken 3 times.
✗ Branch 15 → 26 not taken.
3 return parse_drift_report(text);
216 4 }
217 } // namespace Rtti
218 } // namespace DetourModKit
219