ohmu
·
2026-10-15
1#include "fastfetch.h"
2#include "common/debug.h"
3#include "common/io.h"
4#include "common/strutil.h"
5#include "common/time.h"
6#include "common/windows/nt.h"
7#include "common/windows/unicode.h"
8
9#include <windows.h>
10
11// TODO: long paths on Windows.
12//
13// Every path that crosses into the wide API from this file is converted into a fixed
14// `wchar_t[MAX_PATH]` buffer, and `RtlUTF8ToUnicodeN` refuses to fill it for a path of MAX_PATH
15// characters or more (STATUS_BUFFER_TOO_SMALL) -- which makes each of those call sites a silent
16// failure above that length rather than an error anyone sees. Raising a buffer on its own does not
17// help: the wide APIs accept a longer path only either with a `\\?\` prefix on every absolute path
18// (which also switches off the normalization the rest of the code relies on) or with a
19// `longPathAware` manifest plus the LongPathsEnabled registry value.
20//
21// The plan is the second route: define MAX_PATH as 32768 program-wide, add the manifest flag, and
22// size these buffers from the UTF-8 input rather than from the constant. It has to be done in one
23// pass, which is why it is recorded here instead of being applied at the call sites the review
24// happened to look at.
25static bool createSubfolders(wchar_t* fileName) {
26 HANDLE hRoot = ffGetPeb()->ProcessParameters->CurrentDirectory.Handle;
27 bool closeRoot = false;
28 wchar_t* ptr = fileName;
29
30 // Absolute drive path: C:\...
31 if (ffCharIsEnglishAlphabet((char) ptr[0]) && ptr[1] == L':' && ptr[2] == L'\\') {
32 wchar_t saved = ptr[3];
33 ptr[3] = L'\0';
34
35 hRoot = CreateFileW(
36 fileName,
37 FILE_LIST_DIRECTORY | FILE_TRAVERSE | SYNCHRONIZE,
38 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
39 nullptr,
40 OPEN_EXISTING,
41 FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_FLAG_BACKUP_SEMANTICS,
42 nullptr);
43
44 ptr[3] = saved;
45 if (hRoot == INVALID_HANDLE_VALUE) {
46 return false;
47 }
48
49 closeRoot = true;
50 ptr += 3; // skip "C:\"
51 }
52 // UNC path: \\server\share\...
53 else if (ptr[0] == L'\\' && ptr[1] == L'\\') {
54 wchar_t* serverEnd = wcschr(ptr + 2, L'\\');
55 if (serverEnd == nullptr) {
56 return false;
57 }
58
59 wchar_t* shareEnd = wcschr(serverEnd + 1, L'\\');
60 if (shareEnd == nullptr) {
61 return true; // no parent subfolder exists before file name
62 }
63
64 wchar_t saved = *shareEnd;
65 *shareEnd = L'\0';
66
67 hRoot = CreateFileW(
68 fileName,
69 FILE_LIST_DIRECTORY | FILE_TRAVERSE | SYNCHRONIZE,
70 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
71 nullptr,
72 OPEN_EXISTING,
73 FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_FLAG_BACKUP_SEMANTICS,
74 nullptr);
75
76 *shareEnd = saved;
77 if (hRoot == INVALID_HANDLE_VALUE) {
78 return false;
79 }
80
81 closeRoot = true;
82 ptr = shareEnd + 1; // first component under share
83 }
84 // Rooted path on current drive: \foo\bar
85 else if (ptr[0] == L'\\') {
86 UNICODE_STRING* dosPath = &ffGetPeb()->ProcessParameters->CurrentDirectory.DosPath;
87 wchar_t driveRoot[] = { dosPath->Buffer[0], L':', L'\\', L'\0' };
88 hRoot = CreateFileW(
89 driveRoot,
90 FILE_LIST_DIRECTORY | FILE_TRAVERSE | SYNCHRONIZE,
91 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
92 nullptr,
93 OPEN_EXISTING,
94 FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_FLAG_BACKUP_SEMANTICS,
95 nullptr);
96 if (hRoot == INVALID_HANDLE_VALUE) {
97 return false;
98 }
99 closeRoot = true;
100 ptr++; // skip leading '\'
101 }
102
103 while (true) {
104 wchar_t* token = wcschr(ptr, L'\\');
105 if (token == nullptr) {
106 break;
107 }
108
109 // Skip empty path segments caused by duplicated '\'
110 if (token == ptr) {
111 ptr = token + 1;
112 continue;
113 }
114
115 HANDLE hNew = INVALID_HANDLE_VALUE;
116 IO_STATUS_BLOCK iosb = {};
117
118 NTSTATUS status = NtCreateFile(
119 &hNew,
120 FILE_LIST_DIRECTORY | FILE_TRAVERSE | SYNCHRONIZE,
121 &(OBJECT_ATTRIBUTES) {
122 .Length = sizeof(OBJECT_ATTRIBUTES),
123 .RootDirectory = hRoot,
124 .ObjectName = &(UNICODE_STRING) {
125 .Buffer = ptr,
126 .Length = (USHORT) ((USHORT) (token - ptr) * sizeof(wchar_t)),
127 .MaximumLength = (USHORT) ((USHORT) (token - ptr) * sizeof(wchar_t)),
128 },
129 .Attributes = OBJ_CASE_INSENSITIVE,
130 },
131 &iosb,
132 nullptr,
133 FILE_ATTRIBUTE_NORMAL,
134 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
135 FILE_OPEN_IF,
136 FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT,
137 nullptr,
138 0);
139
140 if (!NT_SUCCESS(status)) {
141 if (closeRoot && hRoot != INVALID_HANDLE_VALUE) {
142 NtClose(hRoot);
143 }
144 return false;
145 }
146
147 if (closeRoot && hRoot != INVALID_HANDLE_VALUE) {
148 NtClose(hRoot);
149 }
150 hRoot = hNew;
151 closeRoot = true;
152
153 ptr = token + 1;
154 }
155
156 if (closeRoot && hRoot != INVALID_HANDLE_VALUE) {
157 NtClose(hRoot);
158 }
159
160 return true;
161}
162
163bool ffWriteFileData(const char* fileName, size_t dataSize, const void* data) {
164 wchar_t fileNameW[MAX_PATH];
165 ULONG len = 0;
166 if (!NT_SUCCESS(RtlUTF8ToUnicodeN(fileNameW, (ULONG) sizeof(fileNameW), &len, fileName, (ULONG) strlen(fileName) + 1))) {
167 return false;
168 }
169
170 for (ULONG i = 0; i < len / sizeof(wchar_t); ++i) {
171 if (fileNameW[i] == L'/') {
172 fileNameW[i] = L'\\';
173 }
174 }
175
176 FF_AUTO_CLOSE_FD HANDLE handle = CreateFileW(fileNameW, GENERIC_WRITE, FILE_SHARE_WRITE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
177 if (handle == INVALID_HANDLE_VALUE) {
178 DWORD errorCode = GetLastError();
179 FF_DEBUG("Failed to open file: %s - %s", fileName, ffDebugWin32Error(errorCode));
180 if (errorCode == ERROR_PATH_NOT_FOUND) {
181 if (!createSubfolders(fileNameW)) {
182 return false;
183 }
184 handle = CreateFileW(fileNameW, GENERIC_WRITE, FILE_SHARE_WRITE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
185 if (handle == INVALID_HANDLE_VALUE) {
186 return false;
187 }
188 } else {
189 return false;
190 }
191 }
192
193 DWORD written;
194 return !!WriteFile(handle, data, (DWORD) dataSize, &written, nullptr);
195}
196
197static inline void readWithLength(HANDLE handle, FFstrbuf* buffer, uint32_t length) {
198 ffStrbufEnsureFixedLengthFree(buffer, length);
199 DWORD bytesRead = 0;
200 while (
201 length > 0 &&
202 ReadFile(handle, buffer->chars + buffer->length, length, &bytesRead, nullptr) != FALSE &&
203 bytesRead > 0) {
204 buffer->length += (uint32_t) bytesRead;
205 length -= (uint32_t) bytesRead;
206 }
207}
208
209static inline void readUntilEOF(HANDLE handle, FFstrbuf* buffer) {
210 ffStrbufEnsureFree(buffer, 31);
211 uint32_t available = ffStrbufGetFree(buffer);
212 DWORD bytesRead = 0;
213 while (
214 ReadFile(handle, buffer->chars + buffer->length, available, &bytesRead, nullptr) != FALSE &&
215 bytesRead > 0) {
216 buffer->length += (uint32_t) bytesRead;
217 if ((uint32_t) bytesRead == available) {
218 ffStrbufEnsureFree(buffer, buffer->allocated - 1); // Doubles capacity every round. -1 for the null byte.
219 }
220 available = ffStrbufGetFree(buffer);
221 }
222}
223
224bool ffAppendFDBuffer(HANDLE handle, FFstrbuf* buffer) {
225 FILE_STANDARD_INFORMATION fileInfo;
226 IO_STATUS_BLOCK iosb;
227 if (!NT_SUCCESS(NtQueryInformationFile(handle, &iosb, &fileInfo, sizeof(fileInfo), FileStandardInformation))) {
228 fileInfo.EndOfFile.QuadPart = 0;
229 }
230
231 if (fileInfo.EndOfFile.QuadPart > 0) {
232 readWithLength(handle, buffer, (uint32_t) fileInfo.EndOfFile.QuadPart);
233 } else {
234 readUntilEOF(handle, buffer);
235 }
236
237 buffer->chars[buffer->length] = '\0';
238
239 return buffer->length > 0;
240}
241
242HANDLE openatW(HANDLE dfd, const wchar_t* fileName, uint16_t fileNameLen, bool directory) {
243 assert(fileNameLen <= 0x7FFF);
244
245 HANDLE hFile;
246 IO_STATUS_BLOCK iosb = {};
247 if (!NT_SUCCESS(NtOpenFile(&hFile,
248 (directory ? FILE_LIST_DIRECTORY | FILE_TRAVERSE : FILE_READ_DATA | FILE_READ_EA) | FILE_READ_ATTRIBUTES | SYNCHRONIZE,
249 &(OBJECT_ATTRIBUTES) {
250 .Length = sizeof(OBJECT_ATTRIBUTES),
251 .RootDirectory = dfd,
252 .ObjectName = &(UNICODE_STRING) {
253 .Buffer = (PWSTR) fileName,
254 .Length = fileNameLen * (USHORT) sizeof(wchar_t),
255 .MaximumLength = (fileNameLen + 1) * (USHORT) sizeof(wchar_t),
256 },
257 .Attributes = OBJ_CASE_INSENSITIVE,
258 },
259 &iosb,
260 FILE_SHARE_READ | (directory ? FILE_SHARE_WRITE | FILE_SHARE_DELETE : 0),
261 FILE_SYNCHRONOUS_IO_NONALERT | (directory ? FILE_DIRECTORY_FILE : FILE_NON_DIRECTORY_FILE)))) {
262 return INVALID_HANDLE_VALUE;
263 }
264
265 return hFile;
266}
267
268HANDLE openat(HANDLE dfd, const char* fileName, int oflag) {
269 wchar_t fileNameW[MAX_PATH];
270 ULONG len;
271 if (!NT_SUCCESS(RtlUTF8ToUnicodeN(fileNameW, (ULONG) sizeof(fileNameW), &len, fileName, (ULONG) strlen(fileName) + 1))) {
272 return INVALID_HANDLE_VALUE;
273 }
274 // Implies `fileNameW[len] = L'\0';` and `len` includes the null terminator
275 len /= sizeof(wchar_t); // convert from bytes to characters
276
277 for (uint32_t i = 0; i < len - 1; ++i) {
278 if (fileNameW[i] == L'/') {
279 fileNameW[i] = L'\\';
280 }
281 }
282
283 return openatW(dfd, fileNameW, (uint16_t) (len - 1), !!(oflag & O_DIRECTORY));
284}
285
286bool ffPathExpandEnv(const char* in, FFstrbuf* out) {
287 if (in[0] == '~') {
288 if ((in[1] == '/' || in[1] == '\\' || in[1] == '\0') && !ffStrContainsC(in, '%')) {
289 ffStrbufSet(out, &instance.state.platform.homeDir);
290 ffStrbufAppendS(out, in + 1);
291 return true;
292 }
293 }
294
295 wchar_t pathInW[MAX_PATH], pathOutW[MAX_PATH];
296 ULONG len = (ULONG) strlen(in);
297 if (!NT_SUCCESS(RtlUTF8ToUnicodeN(pathInW, (ULONG) sizeof(pathInW), &len, in, len))) {
298 return false;
299 }
300 len /= sizeof(wchar_t); // convert from bytes to characters
301
302 SIZE_T outLen; // in characters, including null terminator
303 if (!NT_SUCCESS(RtlExpandEnvironmentStrings(nullptr, pathInW, len, pathOutW, ARRAY_SIZE(pathOutW), &outLen))) {
304 return false;
305 }
306
307 ffStrbufSetNWS(out, (uint32_t) outLen - 1, pathOutW);
308 return true;
309}
310
311bool ffSuppressIO(bool suppress) {
312#ifndef NDEBUG
313 if (instance.config.display.debugMode) {
314 return false;
315 }
316#endif
317
318 static bool init = false;
319 static HANDLE hOrigOut = INVALID_HANDLE_VALUE;
320 static HANDLE hOrigErr = INVALID_HANDLE_VALUE;
321 HANDLE hNullFile = ffGetNullFD();
322 static int fOrigOut = -1;
323 static int fOrigErr = -1;
324 static int fNullFile = -1;
325
326 if (!init) {
327 if (!suppress) {
328 return true;
329 }
330
331 hOrigOut = GetStdHandle(STD_OUTPUT_HANDLE);
332 hOrigErr = GetStdHandle(STD_ERROR_HANDLE);
333 fOrigOut = _dup(STDOUT_FILENO);
334 fOrigErr = _dup(STDERR_FILENO);
335 fNullFile = _open_osfhandle((intptr_t) hNullFile, 0);
336
337 init = true;
338 }
339 if (hNullFile == INVALID_HANDLE_VALUE || fNullFile == -1) {
340 return false;
341 }
342
343 fflush(stdout);
344 fflush(stderr);
345
346 SetStdHandle(STD_OUTPUT_HANDLE, suppress ? hNullFile : hOrigOut);
347 SetStdHandle(STD_ERROR_HANDLE, suppress ? hNullFile : hOrigErr);
348 _dup2(suppress ? fNullFile : fOrigOut, STDOUT_FILENO);
349 _dup2(suppress ? fNullFile : fOrigErr, STDERR_FILENO);
350
351 return true;
352}
353
354void listFilesRecursively(uint32_t baseLength, FFstrbuf* folder, uint8_t indentation, const char* folderName, bool pretty) {
355 uint32_t folderLength = folder->length;
356
357 if (pretty && folderName != nullptr) {
358 for (uint8_t i = 0; i < indentation - 1; i++) {
359 fputs(" | ", stdout);
360 }
361 printf("%s/\n", folderName);
362 }
363
364 ffStrbufAppendC(folder, '*');
365 // The wide API on purpose: every path fastfetch holds is UTF-8, and FindFirstFileA would read
366 // it in the active code page, so a directory whose name the code page cannot represent -- a
367 // Chinese or Japanese name under an English or German locale, for one -- answers
368 // ERROR_PATH_NOT_FOUND and the whole listing disappears. The names it returns are converted
369 // back to UTF-8 below.
370 wchar_t wideFolder[PATH_MAX];
371 if (!NT_SUCCESS(RtlUTF8ToUnicodeN(wideFolder, sizeof(wideFolder), nullptr, folder->chars, folder->length + 1))) {
372 ffStrbufTrimRight(folder, '*');
373 return;
374 }
375 WIN32_FIND_DATAW entry;
376 HANDLE hFind = FindFirstFileW(wideFolder, &entry);
377 ffStrbufTrimRight(folder, '*');
378 if (hFind == INVALID_HANDLE_VALUE) {
379 return;
380 }
381
382 do {
383 if (entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
384 if (entry.cFileName[0] == L'.') {
385 continue;
386 }
387
388 ffStrbufSubstrBefore(folder, folderLength);
389 ffStrbufAppendWS(folder, entry.cFileName);
390 ffStrbufAppendC(folder, '/');
391 FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateWS(entry.cFileName);
392 listFilesRecursively(baseLength, folder, (uint8_t) (indentation + 1), name.chars, pretty);
393 ffStrbufSubstrBefore(folder, folderLength);
394 continue;
395 }
396
397 if (pretty) {
398 for (uint8_t i = 0; i < indentation; i++) {
399 fputs(" | ", stdout);
400 }
401 } else {
402 fputs(folder->chars + baseLength, stdout);
403 }
404
405 FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateWS(entry.cFileName);
406 ffStrbufPutTo(&name, stdout);
407 } while (FindNextFileW(hFind, &entry));
408 FindClose(hFind);
409}
410
411void ffListFilesRecursively(const char* path, bool pretty) {
412 FF_STRBUF_AUTO_DESTROY folder = ffStrbufCreateS(path);
413 ffStrbufEnsureEndsWithC(&folder, '/');
414 listFilesRecursively(folder.length, &folder, 0, nullptr, pretty);
415}
416
417const char* ffGetTerminalResponse(const char* request, int nParams, const char* format, ...) {
418 HANDLE hInput = GetStdHandle(STD_INPUT_HANDLE);
419 FF_AUTO_CLOSE_FD HANDLE hConin = INVALID_HANDLE_VALUE;
420 DWORD inputMode = 0;
421 bool hasInputMode = !!GetConsoleMode(hInput, &inputMode);
422 if (!hasInputMode) {
423 hConin = CreateFileW(L"CONIN$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, nullptr);
424 hInput = hConin;
425 hasInputMode = !!GetConsoleMode(hInput, &inputMode);
426 }
427 SetConsoleMode(hInput, 0);
428
429 FlushConsoleInputBuffer(hInput);
430
431 {
432 DWORD bytes = 0;
433 HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
434 FF_AUTO_CLOSE_FD HANDLE hConout = INVALID_HANDLE_VALUE;
435 DWORD outputMode;
436 if (!GetConsoleMode(hOutput, &outputMode)) {
437 hConout = CreateFileW(L"CONOUT$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, nullptr);
438 hOutput = hConout;
439 }
440 WriteFile(hOutput, request, (DWORD) strlen(request), &bytes, nullptr);
441 }
442
443 while (true) {
444 NTSTATUS status = NtWaitForSingleObject(hInput, FALSE, &(LARGE_INTEGER) { .QuadPart = (int64_t) FF_IO_TERM_RESP_WAIT_MS * -10000 });
445 if (status != STATUS_WAIT_0) {
446 FF_DEBUG("NtWaitForSingleObject() failed or timeout: %s", ffDebugNtStatus(status));
447 SetConsoleMode(hInput, inputMode);
448 return "NtWaitForSingleObject() failed or timeout";
449 }
450
451 // Ignore all unexpected input events
452 INPUT_RECORD record;
453 DWORD len = 0;
454 if (!PeekConsoleInputW(hInput, &record, 1, &len)) {
455 break;
456 }
457
458 if (
459 record.EventType == KEY_EVENT &&
460 record.Event.KeyEvent.uChar.UnicodeChar != L'\r' &&
461 record.Event.KeyEvent.uChar.UnicodeChar != L'\n') {
462 break;
463 } else {
464 ReadConsoleInputW(hInput, &record, 1, &len);
465 }
466 }
467
468 va_list args;
469 va_start(args, format);
470
471 char buffer[1024];
472 uint32_t bytesRead = 0;
473
474 while (true) {
475 DWORD bytes = 0;
476 BOOL readOk = ReadFile(hInput, buffer + bytesRead, (DWORD) (sizeof(buffer) - 1 - bytesRead), &bytes, nullptr);
477 if (!readOk || bytes == 0) {
478 FF_DEBUG("ReadFile() %s, bytes=%lu: %s", readOk ? "returned no data" : "failed", bytes, readOk ? "no error" : ffDebugWin32Error(GetLastError()));
479 va_end(args);
480 return "ReadFile() failed";
481 }
482
483 bytesRead += bytes;
484 if (__builtin_expect(bytesRead >= sizeof(buffer) - 1, false)) {
485 va_end(args);
486 return "terminal response buffer overflow";
487 }
488 buffer[bytesRead] = '\0';
489
490 va_list cargs;
491 va_copy(cargs, args);
492 int ret = vsscanf(buffer, format, cargs);
493 va_end(cargs);
494
495 if (ret <= 0) {
496 va_end(args);
497 FF_DEBUG("vsscanf(buffer, format, args) failed: %s", strerror(errno));
498 return "vsscanf(buffer, format, args) failed";
499 }
500 if (ret >= nParams) {
501 break;
502 }
503 }
504
505 if (hasInputMode) {
506 SetConsoleMode(hInput, inputMode);
507 }
508
509 va_end(args);
510
511 return nullptr;
512}
513
514FFNativeFD ffGetNullFD(void) {
515 static FFNativeFD hNullFile = INVALID_HANDLE_VALUE;
516 if (hNullFile != INVALID_HANDLE_VALUE) {
517 return hNullFile;
518 }
519 hNullFile = CreateFileW(
520 L"NUL",
521 GENERIC_READ | GENERIC_WRITE,
522 FILE_SHARE_WRITE,
523 0,
524 OPEN_EXISTING,
525 0,
526 &(SECURITY_ATTRIBUTES) {
527 .nLength = sizeof(SECURITY_ATTRIBUTES),
528 .lpSecurityDescriptor = nullptr,
529 .bInheritHandle = TRUE,
530 });
531 return hNullFile;
532}
533
534bool ffIsTerminal(int fd) {
535 intptr_t handle = _get_osfhandle(fd);
536 if (handle == -1 || handle == -2) {
537 // -1: an invalid fd; -2: a valid fd with no file behind it (`_get_osfhandle`'s way of
538 // saying FF_INVALID_FD). Neither is a terminal.
539 return false;
540 }
541 DWORD mode;
542 return GetConsoleMode((HANDLE) handle, &mode) != 0;
543}
544
545bool ffRemoveFile(const char* fileName) {
546 wchar_t fileNameW[MAX_PATH]; // see the long path TODO at the top of this file
547 ULONG len;
548 if (!NT_SUCCESS(RtlUTF8ToUnicodeN(fileNameW, (ULONG) sizeof(fileNameW), &len, fileName, (ULONG) strlen(fileName) + 1))) {
549 return false;
550 }
551
552 bool ret = DeleteFileW(fileNameW) != FALSE;
553 FF_DEBUG("Deleting file: %s - %s", fileName, ret ? "Success" : ffDebugWin32Error(GetLastError()));
554 return ret;
555}
556
557uint64_t ffPathGetMtime(const char* path) {
558 wchar_t fileNameW[MAX_PATH]; // see the long path TODO at the top of this file
559 ULONG len;
560 if (!NT_SUCCESS(RtlUTF8ToUnicodeN(fileNameW, (ULONG) sizeof(fileNameW), &len, path, (ULONG) strlen(path) + 1))) {
561 return 0;
562 }
563
564 FF_AUTO_CLOSE_FD HANDLE handle = CreateFileW(fileNameW, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
565
566 if (handle == INVALID_HANDLE_VALUE) { // file doesn't exist or isn't accessible
567 return 0;
568 }
569
570 FILE_BASIC_INFORMATION fileInfo;
571 IO_STATUS_BLOCK iosb;
572 if (!NT_SUCCESS(NtQueryInformationFile(handle, &iosb, &fileInfo, sizeof(fileInfo), FileBasicInformation))) {
573 return 0;
574 }
575
576 return ffFileTimeToUnixMs((uint64_t) fileInfo.LastWriteTime.QuadPart);
577}