./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
157 / 172
91.3%
Branches:
108 / 138
78.3%
Functions:
22 / 22
100.0%

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