GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 91.0% 162 / 0 / 178
Functions: 100.0% 22 / 0 / 22
Branches: 76.0% 111 / 0 / 146

src/internal/win_file_stream.cpp
Line Branch Exec Source
1 #include "internal/win_file_stream.hpp"
2
3 #include <windows.h>
4 #include <algorithm>
5 #include <array>
6 #include <cassert>
7 #include <climits>
8 #include <cstddef>
9 #include <cstring>
10 #include <new>
11 #include <string>
12
13 namespace DetourModKit::detail
14 {
15 #if defined(DMK_ENABLE_TEST_SEAMS)
16 void (*g_read_regular_file_after_size_probe)(const std::wstring &path) = nullptr;
17 int (*g_win_file_write_override)(void *handle, const void *data, unsigned long size, unsigned long *written) =
18 nullptr;
19 int (*g_win_file_close_override)(void *handle) = nullptr;
20 #endif
21
22 namespace
23 {
24 2073 BOOL stream_write_file(void *handle, const void *data, DWORD size, DWORD *written) noexcept
25 {
26 #if defined(DMK_ENABLE_TEST_SEAMS)
27
2/2
✓ Branch 2 → 3 taken 10 times.
✓ Branch 2 → 5 taken 2063 times.
2073 if (g_win_file_write_override != nullptr)
28 {
29 10 return g_win_file_write_override(handle, data, size, written);
30 }
31 #endif
32 2063 return WriteFile(static_cast<HANDLE>(handle), data, size, written, nullptr);
33 }
34
35 662 BOOL stream_close_handle(void *handle) noexcept
36 {
37 #if defined(DMK_ENABLE_TEST_SEAMS)
38
2/2
✓ Branch 2 → 3 taken 5 times.
✓ Branch 2 → 5 taken 657 times.
662 if (g_win_file_close_override != nullptr)
39 {
40 5 return g_win_file_close_override(handle);
41 }
42 #endif
43 657 return CloseHandle(static_cast<HANDLE>(handle));
44 }
45 } // namespace
46
47 2078 WinFileStreamBuf::WinFileStreamBuf() noexcept : m_handle(INVALID_HANDLE_VALUE), m_buffer{}
48 {
49 // A closed buffer has no writable put area. This routes even a one-byte write through overflow(), where the
50 // closed-handle check reports failure instead of caching a byte that a later open discards.
51 2078 setp(nullptr, nullptr);
52 2078 }
53
54 521 WinFileStreamBuf::~WinFileStreamBuf() noexcept
55 {
56 // Best-effort force path: one drain attempt, then close the handle unconditionally. A destructor has no
57 // return channel, so unlike close() it cannot retain a handle for a retry.
58
2/2
✓ Branch 3 → 4 taken 51 times.
✓ Branch 3 → 7 taken 470 times.
521 if (is_open())
59 {
60 51 (void)flush_buffer();
61 51 (void)stream_close_handle(m_handle);
62 51 m_handle = INVALID_HANDLE_VALUE;
63 51 setp(nullptr, nullptr);
64 }
65 521 }
66
67 2071 bool WinFileStreamBuf::open(const std::wstring &path, std::ios_base::openmode mode)
68 {
69 // A current file that cannot be closed cleanly is retained, buffered bytes included, and the reopen is
70 // refused: discarding the handle here would silently drop the undrained tail.
71
6/6
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 7 taken 2069 times.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 2070 times.
2071 if (is_open() && !close())
72 {
73 1 return false;
74 }
75
76 2070 const bool append = (mode & std::ios_base::app) != 0;
77
78 // Append mode requests FILE_APPEND_DATA so the OS positions every WriteFile at the current end of file
79 // atomically; concurrent appenders then cannot interleave or overwrite each other ([B-33],
80 // AppendMode_ConcurrentAppendersPreserveEveryByte). Truncating ("out") mode keeps GENERIC_WRITE +
81 // CREATE_ALWAYS.
82
2/2
✓ Branch 11 → 12 taken 410 times.
✓ Branch 11 → 13 taken 1660 times.
2070 const DWORD access = append ? FILE_APPEND_DATA : GENERIC_WRITE;
83
2/2
✓ Branch 14 → 15 taken 410 times.
✓ Branch 14 → 16 taken 1660 times.
2070 const DWORD creation = append ? OPEN_ALWAYS : CREATE_ALWAYS;
84
85 2070 m_handle = CreateFileW(
86 path.c_str(),
87 access,
88 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
89 nullptr,
90 creation,
91 FILE_ATTRIBUTE_NORMAL,
92 nullptr
93 );
94
95
2/2
✓ Branch 19 → 20 taken 11 times.
✓ Branch 19 → 21 taken 2059 times.
2070 if (m_handle == INVALID_HANDLE_VALUE)
96 {
97 11 return false;
98 }
99
100 6177 setp(m_buffer.data(), m_buffer.data() + BUFFER_SIZE);
101 2059 return true;
102 }
103
104 96 bool WinFileStreamBuf::open(const std::string &path, std::ios_base::openmode mode)
105 {
106 // Convert narrow string to wide. Try UTF-8 first, fall back to ACP.
107
1/2
✓ Branch 3 → 4 taken 96 times.
✗ Branch 3 → 27 not taken.
96 int wide_len = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path.c_str(), -1, nullptr, 0);
108 96 UINT code_page = CP_UTF8;
109
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 8 taken 95 times.
96 if (wide_len <= 0)
110 {
111 1 code_page = CP_ACP;
112
1/2
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 27 not taken.
1 wide_len = MultiByteToWideChar(CP_ACP, 0, path.c_str(), -1, nullptr, 0);
113 }
114
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 96 times.
96 if (wide_len <= 0)
115 {
116 return false;
117 }
118
119
1/2
✓ Branch 12 → 13 taken 96 times.
✗ Branch 12 → 22 not taken.
96 std::wstring wide_path(static_cast<size_t>(wide_len - 1), L'\0');
120
1/2
✓ Branch 16 → 17 taken 96 times.
✗ Branch 16 → 25 not taken.
96 MultiByteToWideChar(code_page, 0, path.c_str(), -1, wide_path.data(), wide_len);
121
122
1/2
✓ Branch 17 → 18 taken 96 times.
✗ Branch 17 → 25 not taken.
96 return open(wide_path, mode);
123 96 }
124
125 94171 bool WinFileStreamBuf::is_open() const noexcept
126 {
127 94171 return m_handle != INVALID_HANDLE_VALUE;
128 }
129
130 616 bool WinFileStreamBuf::close() noexcept
131 {
132
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 614 times.
616 if (!is_open())
133 {
134 2 return true;
135 }
136
137 // A failed drain keeps the handle open and the unwritten tail buffered, so nothing is lost and a retry can
138 // finish the job. A failed CloseHandle likewise retains the handle rather than nulling it: only the
139 // destructor discards a handle it could not close.
140
2/2
✓ Branch 6 → 7 taken 3 times.
✓ Branch 6 → 8 taken 611 times.
614 if (!flush_buffer())
141 {
142 3 return false;
143 }
144
2/2
✓ Branch 9 → 10 taken 4 times.
✓ Branch 9 → 11 taken 607 times.
611 if (stream_close_handle(m_handle) == 0)
145 {
146 4 return false;
147 }
148 607 m_handle = INVALID_HANDLE_VALUE;
149 607 setp(nullptr, nullptr);
150 607 return true;
151 }
152
153 4 WinFileStreamBuf::int_type WinFileStreamBuf::overflow(int_type ch)
154 {
155
2/2
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 5 taken 1 time.
4 if (!is_open())
156 {
157 3 return traits_type::eof();
158 }
159
160
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 1 time.
1 if (!flush_buffer())
161 {
162 return traits_type::eof();
163 }
164
165
1/2
✓ Branch 10 → 11 taken 1 time.
✗ Branch 10 → 14 not taken.
1 if (!traits_type::eq_int_type(ch, traits_type::eof()))
166 {
167 1 *pptr() = traits_type::to_char_type(ch);
168 1 pbump(1);
169 }
170
171 1 return traits_type::not_eof(ch);
172 }
173
174 2787 int WinFileStreamBuf::sync()
175 {
176
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 2785 times.
2787 if (!is_open())
177 {
178 2 return -1;
179 }
180
181
2/2
✓ Branch 6 → 7 taken 2780 times.
✓ Branch 6 → 8 taken 5 times.
2785 return flush_buffer() ? 0 : -1;
182 }
183
184 80163 std::streamsize WinFileStreamBuf::xsputn(const char *s, std::streamsize count)
185 {
186
6/6
✓ Branch 3 → 4 taken 80162 times.
✓ Branch 3 → 5 taken 1 time.
✓ Branch 4 → 5 taken 4 times.
✓ Branch 4 → 6 taken 80158 times.
✓ Branch 7 → 8 taken 5 times.
✓ Branch 7 → 9 taken 80158 times.
80163 if (!is_open() || count <= 0)
187 {
188 5 return 0;
189 }
190
191 80158 std::streamsize written = 0;
192
193
2/2
✓ Branch 27 → 10 taken 80165 times.
✓ Branch 27 → 28 taken 80158 times.
160323 while (written < count)
194 {
195 80165 const auto available = static_cast<std::streamsize>(epptr() - pptr());
196 80165 const auto to_copy = std::min(count - written, available);
197
198
1/2
✓ Branch 13 → 14 taken 80165 times.
✗ Branch 13 → 20 not taken.
80165 if (to_copy > 0)
199 {
200 80165 std::memcpy(pptr(), s + written, static_cast<size_t>(to_copy));
201
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 80165 times.
80165 assert(to_copy <= INT_MAX);
202 80165 pbump(static_cast<int>(to_copy));
203 80165 written += to_copy;
204 }
205
206
2/2
✓ Branch 22 → 23 taken 10 times.
✓ Branch 22 → 26 taken 80155 times.
80165 if (pptr() == epptr())
207 {
208
1/2
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 10 times.
10 if (!flush_buffer())
209 {
210 break;
211 }
212 }
213 }
214
215 80158 return written;
216 }
217
218 3461 bool WinFileStreamBuf::flush_buffer() noexcept
219 {
220 3461 const auto count = static_cast<DWORD>(pptr() - pbase());
221
2/2
✓ Branch 4 → 5 taken 1389 times.
✓ Branch 4 → 6 taken 2072 times.
3461 if (count == 0)
222 {
223 1389 return true;
224 }
225
226 // WriteFile may satisfy only part of the request, reporting success with bytes_written < count (a short write
227 // on a near-full volume, a pipe-backed handle, or an interrupted write), so drain the remainder in a loop.
228 // A hard error, zero-byte success, or impossible over-reported count aborts without advancing the cursor.
229 2072 const char *cursor = pbase();
230 2072 DWORD remaining = count;
231 2072 bool drained = true;
232
2/2
✓ Branch 14 → 8 taken 2073 times.
✓ Branch 14 → 15 taken 2063 times.
4136 while (remaining != 0)
233 {
234 2073 DWORD bytes_written = 0;
235 2073 const BOOL result = stream_write_file(m_handle, cursor, remaining, &bytes_written);
236
6/6
✓ Branch 9 → 10 taken 2066 times.
✓ Branch 9 → 12 taken 7 times.
✓ Branch 10 → 11 taken 2065 times.
✓ Branch 10 → 12 taken 1 time.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 2064 times.
2073 if (result == 0 || bytes_written == 0 || bytes_written > remaining)
237 {
238 9 drained = false;
239 9 break;
240 }
241 2064 cursor += bytes_written;
242 2064 remaining -= bytes_written;
243 }
244
245 // On a failed drain the unwritten tail is compacted to the buffer front and stays in the put area, so the
246 // bytes remain recoverable by a later flush or close instead of being silently discarded with the reset.
247 6216 setp(m_buffer.data(), m_buffer.data() + BUFFER_SIZE);
248
3/4
✓ Branch 20 → 21 taken 9 times.
✓ Branch 20 → 25 taken 2063 times.
✓ Branch 21 → 22 taken 9 times.
✗ Branch 21 → 25 not taken.
2072 if (!drained && remaining != 0)
249 {
250 9 std::memmove(m_buffer.data(), cursor, remaining);
251 9 pbump(static_cast<int>(remaining));
252 }
253 2072 return drained;
254 }
255
256
1/2
✓ Branch 3 → 4 taken 1982 times.
✗ Branch 3 → 6 not taken.
1982 WinFileStream::WinFileStream() : std::ostream(&m_buf) {}
257
258
1/2
✓ Branch 3 → 4 taken 66 times.
✗ Branch 3 → 11 not taken.
66 WinFileStream::WinFileStream(const std::string &path, std::ios_base::openmode mode) : std::ostream(&m_buf)
259 {
260
1/4
DetourModKit::detail::WinFileStream::WinFileStream(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::_Ios_Openmode):
✓ Branch 5 → 6 taken 66 times.
✗ Branch 5 → 7 not taken.
DetourModKit::detail::WinFileStream::WinFileStream(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::_Ios_Openmode):
✗ Branch 4 → 5 not taken.
✗ Branch 4 → 6 not taken.
66 open(path, mode);
261 66 }
262
263 491 WinFileStream::~WinFileStream() noexcept = default;
264
265 70 void WinFileStream::open(const std::string &path, std::ios_base::openmode mode)
266 {
267 70 clear();
268
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 68 times.
70 if (!m_buf.open(path, mode))
269 {
270 2 setstate(std::ios_base::failbit);
271 }
272 70 }
273
274 1974 void WinFileStream::open(const std::wstring &path, std::ios_base::openmode mode)
275 {
276 1974 clear();
277
2/2
✓ Branch 4 → 5 taken 8 times.
✓ Branch 4 → 6 taken 1966 times.
1974 if (!m_buf.open(path, mode))
278 {
279 8 setstate(std::ios_base::failbit);
280 }
281 1974 }
282
283 7998 bool WinFileStream::is_open() const noexcept
284 {
285 7998 return m_buf.is_open();
286 }
287
288 587 void WinFileStream::close() noexcept
289 {
290 // A failed drain or CloseHandle surfaces as failbit, matching std::ofstream::close; the buffer retains the
291 // undrained tail and the handle, so the caller can observe the failure and retry.
292
2/2
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 5 taken 584 times.
587 if (!m_buf.close())
293 {
294 try
295 {
296
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 1 time.
3 setstate(std::ios_base::failbit);
297 }
298 1 catch (...)
299 {
300 // Only a caller-armed exception mask throws here; the failbit is already recorded.
301 1 }
302 }
303 587 }
304
305 43 Result<std::string> read_regular_file_bounded(const std::wstring &path, std::size_t max_bytes)
306 {
307
1/2
✓ Branch 3 → 4 taken 43 times.
✗ Branch 3 → 96 not taken.
43 const HANDLE handle = CreateFileW(
308 path.c_str(),
309 GENERIC_READ,
310 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
311 nullptr,
312 OPEN_EXISTING,
313 FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,
314 nullptr
315 );
316
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 9 taken 41 times.
43 if (handle == INVALID_HANDLE_VALUE)
317 {
318
1/2
✓ Branch 5 → 6 taken 2 times.
✗ Branch 5 → 78 not taken.
2 return std::unexpected(Error{ErrorCode::FileOpenFailed, "read_regular_file_bounded", GetLastError()});
319 }
320 class HandleGuard final
321 {
322 public:
323 41 explicit HandleGuard(HANDLE handle) noexcept : m_handle(handle) {}
324 41 ~HandleGuard() noexcept { CloseHandle(m_handle); }
325 HandleGuard(const HandleGuard &) = delete;
326 HandleGuard &operator=(const HandleGuard &) = delete;
327 HandleGuard(HandleGuard &&) = delete;
328 HandleGuard &operator=(HandleGuard &&) = delete;
329
330 private:
331 HANDLE m_handle;
332 };
333 41 const HandleGuard guard{handle};
334
335 // A manifest is a regular disk file. A pipe, mailslot, console, or character device reports a non-disk type;
336 // reading one blocks or streams bytes no size precheck can bound, so it is refused as unopenable rather than
337 // trusted as a manifest source.
338
3/4
✓ Branch 10 → 11 taken 41 times.
✗ Branch 10 → 94 not taken.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 15 taken 40 times.
41 if (GetFileType(handle) != FILE_TYPE_DISK)
339 {
340 1 return std::unexpected(Error{ErrorCode::FileOpenFailed, "read_regular_file_bounded"});
341 }
342
343 40 LARGE_INTEGER size{};
344
4/8
✓ Branch 15 → 16 taken 40 times.
✗ Branch 15 → 94 not taken.
✓ Branch 16 → 17 taken 40 times.
✗ Branch 16 → 18 not taken.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 40 times.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 25 taken 40 times.
40 if (GetFileSizeEx(handle, &size) == 0 || size.QuadPart < 0)
345 {
346 return std::unexpected(Error{ErrorCode::FileOpenFailed, "read_regular_file_bounded", GetLastError()});
347 }
348
2/2
✓ Branch 25 → 26 taken 3 times.
✓ Branch 25 → 29 taken 37 times.
40 if (static_cast<unsigned long long>(size.QuadPart) > static_cast<unsigned long long>(max_bytes))
349 {
350 3 return std::unexpected(Error{ErrorCode::SizeTooLarge, "read_regular_file_bounded"});
351 }
352
353 37 std::string content;
354 37 const std::size_t expected_size = static_cast<std::size_t>(size.QuadPart);
355
1/2
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 35 taken 37 times.
37 if (expected_size > content.max_size())
356 {
357 return std::unexpected(Error{ErrorCode::SizeTooLarge, "read_regular_file_bounded"});
358 }
359 #if defined(DMK_ENABLE_TEST_SEAMS)
360
2/2
✓ Branch 35 → 36 taken 3 times.
✓ Branch 35 → 37 taken 34 times.
37 if (g_read_regular_file_after_size_probe != nullptr)
361 {
362
1/2
✓ Branch 36 → 37 taken 3 times.
✗ Branch 36 → 92 not taken.
3 g_read_regular_file_after_size_probe(path);
363 }
364 #endif
365 try
366 {
367
2/2
✓ Branch 37 → 38 taken 35 times.
✓ Branch 37 → 85 taken 2 times.
37 content.reserve(expected_size);
368 35 std::array<char, std::size_t{64} * 1024> buffer{};
369 35 std::size_t total = 0;
370 for (;;)
371 {
372 67 DWORD read = 0;
373
2/4
✓ Branch 43 → 44 taken 67 times.
✗ Branch 43 → 84 not taken.
✗ Branch 44 → 45 not taken.
✓ Branch 44 → 49 taken 67 times.
134 if (ReadFile(handle, buffer.data(), static_cast<DWORD>(buffer.size()), &read, nullptr) == 0)
374 {
375 return std::unexpected(
376 Error{ErrorCode::FileOpenFailed, "read_regular_file_bounded", GetLastError()}
377 );
378 }
379
2/2
✓ Branch 49 → 50 taken 33 times.
✓ Branch 49 → 51 taken 34 times.
67 if (read == 0)
380 {
381 33 break;
382 }
383 // Bound against the running total, not the stale precheck: a writer that extends the file after the
384 // size query cannot push the read past the cap.
385 34 const std::size_t bytes_read = static_cast<std::size_t>(read);
386
5/6
✓ Branch 51 → 52 taken 33 times.
✓ Branch 51 → 54 taken 1 time.
✗ Branch 53 → 54 not taken.
✓ Branch 53 → 55 taken 33 times.
✓ Branch 56 → 57 taken 1 time.
✓ Branch 56 → 60 taken 33 times.
34 if (bytes_read > max_bytes - total || bytes_read > content.max_size() - total)
387 {
388 1 return std::unexpected(Error{ErrorCode::SizeTooLarge, "read_regular_file_bounded"});
389 }
390
2/2
✓ Branch 60 → 61 taken 1 time.
✓ Branch 60 → 64 taken 32 times.
33 if (bytes_read > expected_size - total)
391 {
392 1 return std::unexpected(Error{ErrorCode::FileOpenFailed, "read_regular_file_bounded"});
393 }
394
1/2
✓ Branch 66 → 67 taken 32 times.
✗ Branch 66 → 84 not taken.
32 content.append(buffer.data(), bytes_read);
395 32 total += bytes_read;
396 32 }
397
2/2
✓ Branch 50 → 69 taken 1 time.
✓ Branch 50 → 72 taken 32 times.
33 if (total != expected_size)
398 {
399 1 return std::unexpected(Error{ErrorCode::FileOpenFailed, "read_regular_file_bounded"});
400 }
401 }
402
1/2
✗ Branch 86 → 87 not taken.
✓ Branch 86 → 88 taken 2 times.
2 catch (const std::bad_alloc &)
403 {
404 2 return std::unexpected(Error{ErrorCode::OutOfMemory, "read_regular_file_bounded"});
405 2 }
406 32 return content;
407 41 }
408
409 } // namespace DetourModKit::detail
410