master ohmu/fastfetch-openbsd / src / common / impl / io_windows.c
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}