ohmu
·
2026-10-15
1#pragma once
2
3#include "common/FFstrbuf.h"
4#include "common/FFlist.h"
5
6#ifdef _WIN32
7 #include <fileapi.h>
8 #include <handleapi.h>
9 #include <io.h>
10 #include "common/windows/nt.h"
11typedef HANDLE FFNativeFD;
12 #define FF_INVALID_FD INVALID_HANDLE_VALUE
13#else
14 #include <unistd.h>
15 #include <dirent.h>
16 #include <sys/stat.h>
17 #include <errno.h>
18 #include <limits.h>
19 #include <fcntl.h>
20typedef int FFNativeFD;
21 #define FF_INVALID_FD (-1)
22 // procfs's file can be changed between read calls such as /proc/meminfo and /proc/uptime.
23 // one safe way to read correct data is reading the whole file in a single read syscall
24 #define PROC_FILE_BUFFSIZ (32 * 1024)
25#endif
26
27#ifdef _WIN32
28 #ifndef O_CLOEXEC
29 #define O_CLOEXEC 0
30 #endif
31 #ifndef O_RDONLY
32 #define O_RDONLY 0
33 #endif
34 #ifndef O_DIRECTORY
35 #define O_DIRECTORY 0200000
36 #endif
37
38// Only O_RDONLY is supported
39[[gnu::nonnull(2)]] HANDLE openat(HANDLE dfd, const char* fileName, int oflag);
40[[gnu::nonnull(2)]] HANDLE openatW(HANDLE dfd, const wchar_t* fileName, uint16_t fileNameLen, bool directory);
41#endif
42
43static inline bool ffIsValidNativeFD(FFNativeFD fd) {
44#ifndef _WIN32
45 return fd >= 0;
46#else
47 // https://devblogs.microsoft.com/oldnewthing/20040302-00/?p=40443
48 return fd != INVALID_HANDLE_VALUE && fd != nullptr;
49#endif
50}
51
52[[gnu::always_inline, gnu::nonnull(1)]]
53static inline void wrapClose(FFNativeFD* pfd) {
54 if (ffIsValidNativeFD(*pfd)) {
55#ifndef _WIN32
56 close(*pfd);
57#else
58 NtClose(*pfd);
59#endif
60 }
61}
62#define FF_AUTO_CLOSE_FD [[gnu::cleanup(wrapClose)]]
63
64static inline FFNativeFD FFUnixFD2NativeFD(int unixfd) {
65#ifndef _WIN32
66 return unixfd;
67#else
68 return (FFNativeFD) _get_osfhandle(unixfd);
69#endif
70}
71
72[[gnu::nonnull(3)]]
73static inline bool ffWriteFDData(FFNativeFD fd, size_t dataSize, const void* data) {
74#ifndef _WIN32
75 return write(fd, data, dataSize) != -1;
76#else
77 DWORD written;
78 return WriteFile(fd, data, (DWORD) dataSize, &written, nullptr) && written == dataSize;
79#endif
80}
81
82[[gnu::nonnull(2)]]
83static inline bool ffWriteFDBuffer(FFNativeFD fd, const FFstrbuf* content) {
84 return ffWriteFDData(fd, content->length, content->chars);
85}
86
87[[gnu::nonnull(1, 3)]]
88bool ffWriteFileData(const char* fileName, size_t dataSize, const void* data);
89
90[[gnu::nonnull(1, 2)]]
91static inline bool ffWriteFileBuffer(const char* fileName, const FFstrbuf* buffer) {
92 return ffWriteFileData(fileName, buffer->length, buffer->chars);
93}
94
95[[gnu::nonnull(3)]]
96static inline ssize_t ffReadFDData(FFNativeFD fd, size_t dataSize, void* data) {
97#ifndef _WIN32
98 return read(fd, data, dataSize);
99#else
100 DWORD bytesRead;
101 if (!ReadFile(fd, data, (DWORD) dataSize, &bytesRead, nullptr)) {
102 return -1;
103 }
104
105 return (ssize_t) bytesRead;
106#endif
107}
108
109[[gnu::nonnull(2)]] bool ffAppendFDBuffer(FFNativeFD fd, FFstrbuf* buffer);
110
111[[gnu::nonnull(1, 3)]] static inline ssize_t ffReadFileData(const char* fileName, size_t dataSize, void* data) {
112 FF_AUTO_CLOSE_FD FFNativeFD fd =
113#ifndef _WIN32
114 open(fileName, O_RDONLY | O_CLOEXEC);
115#else
116 CreateFileA(fileName, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
117#endif
118
119 if (!ffIsValidNativeFD(fd)) {
120 return -1;
121 }
122
123 return ffReadFDData(fd, dataSize, data);
124}
125
126[[gnu::nonnull(2, 4)]] static inline ssize_t ffReadFileDataRelative(FFNativeFD dfd, const char* fileName, size_t dataSize, void* data) {
127 FF_AUTO_CLOSE_FD FFNativeFD fd = openat(dfd, fileName, O_RDONLY | O_CLOEXEC);
128 if (!ffIsValidNativeFD(fd)) {
129 return -1;
130 }
131
132 return ffReadFDData(fd, dataSize, data);
133}
134
135// `CreateFileA` reads the path in the ANSI code page, so a UTF-8 path holding anything outside ASCII
136// names a different file, or none at all. The wide API is the one that takes what UTF-8 decodes to,
137// and the conversion is done the same way ffPathExists() below does it.
138[[gnu::nonnull(1, 2)]] static inline bool ffAppendFileBuffer(const char* fileName, FFstrbuf* buffer) {
139#ifdef _WIN32
140 wchar_t fileNameW[MAX_PATH];
141 if (!NT_SUCCESS(RtlUTF8ToUnicodeN(fileNameW, (ULONG) sizeof(fileNameW), nullptr, fileName, (ULONG) strlen(fileName) + 1))) {
142 return false;
143 }
144 FF_AUTO_CLOSE_FD FFNativeFD fd = CreateFileW(fileNameW, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
145#else
146 FF_AUTO_CLOSE_FD FFNativeFD fd = open(fileName, O_RDONLY | O_CLOEXEC);
147#endif
148
149 if (!ffIsValidNativeFD(fd)) {
150 return false;
151 }
152
153 return ffAppendFDBuffer(fd, buffer);
154}
155
156[[gnu::nonnull(2, 3)]] static inline bool ffAppendFileBufferRelative(FFNativeFD dfd, const char* fileName, FFstrbuf* buffer) {
157 FF_AUTO_CLOSE_FD FFNativeFD fd = openat(dfd, fileName, O_RDONLY | O_CLOEXEC);
158 if (!ffIsValidNativeFD(fd)) {
159 return false;
160 }
161
162 return ffAppendFDBuffer(fd, buffer);
163}
164
165[[gnu::nonnull(2)]] static inline bool ffReadFDBuffer(FFNativeFD fd, FFstrbuf* buffer) {
166 ffStrbufClear(buffer);
167 return ffAppendFDBuffer(fd, buffer);
168}
169
170[[gnu::nonnull(1, 2)]] static inline bool ffReadFileBuffer(const char* fileName, FFstrbuf* buffer) {
171 ffStrbufClear(buffer);
172 return ffAppendFileBuffer(fileName, buffer);
173}
174
175[[gnu::nonnull(2, 3)]] static inline bool ffReadFileBufferRelative(FFNativeFD dfd, const char* fileName, FFstrbuf* buffer) {
176 ffStrbufClear(buffer);
177 return ffAppendFileBufferRelative(dfd, fileName, buffer);
178}
179
180typedef enum FFPathType: uint8_t {
181 FF_PATHTYPE_FILE = 1 << 0,
182 FF_PATHTYPE_DIRECTORY = 1 << 1,
183 FF_PATHTYPE_ANY = FF_PATHTYPE_FILE | FF_PATHTYPE_DIRECTORY,
184} FFPathType;
185
186[[gnu::nonnull(1)]] static inline bool ffPathExists(const char* path, FFPathType pathType) {
187#ifdef _WIN32
188
189 wchar_t wPath[MAX_PATH];
190 if (!NT_SUCCESS(RtlUTF8ToUnicodeN(wPath, (ULONG) sizeof(wPath), nullptr, path, (ULONG) strlen(path) + 1))) {
191 return false;
192 }
193
194 DWORD attr = GetFileAttributesW(wPath);
195
196 if (attr == INVALID_FILE_ATTRIBUTES) {
197 return false;
198 }
199
200 if (pathType & FF_PATHTYPE_FILE && !(attr & FILE_ATTRIBUTE_DIRECTORY)) {
201 return true;
202 }
203
204 if (pathType & FF_PATHTYPE_DIRECTORY && (attr & FILE_ATTRIBUTE_DIRECTORY)) {
205 return true;
206 }
207
208#else
209
210 if (pathType == FF_PATHTYPE_ANY) {
211 // Zero overhead
212 return access(path, F_OK) == 0;
213 } else {
214 struct stat fileStat;
215 if (stat(path, &fileStat) != 0) {
216 return false;
217 }
218
219 unsigned int mode = fileStat.st_mode & S_IFMT;
220
221 if (pathType & FF_PATHTYPE_FILE && mode != S_IFDIR) {
222 return true;
223 }
224
225 if (pathType & FF_PATHTYPE_DIRECTORY && mode == S_IFDIR) {
226 return true;
227 }
228 }
229
230#endif
231
232 return false;
233}
234
235[[gnu::nonnull(1, 2)]] bool ffPathExpandEnv(const char* in, FFstrbuf* out);
236
237#define FF_IO_TERM_RESP_WAIT_MS 1000 // Terminal may respond slowly, especially when printing large image/gif logos. Found on iTerm
238
239[[gnu::format(scanf, 3, 4), gnu::nonnull(1, 3)]] const char* ffGetTerminalResponse(const char* request, int nParams, const char* format, ...);
240
241// Not thread safe!
242// Returns the previous state, which callers routinely discard (see ffUnsuppressIO), so not `nodiscard`
243bool ffSuppressIO(bool suppress);
244
245static inline void ffUnsuppressIO(bool* suppressed) {
246 if (!*suppressed) {
247 return;
248 }
249 ffSuppressIO(false);
250 *suppressed = false;
251}
252
253#define FF_SUPPRESS_IO() [[maybe_unused, gnu::cleanup(ffUnsuppressIO)]] bool io_suppressed__ = ffSuppressIO(true)
254
255[[gnu::nonnull(1)]] void ffListFilesRecursively(const char* path, bool pretty);
256
257[[gnu::nonnull(1), gnu::always_inline]] static inline void wrapFclose(FILE** pfile) {
258 if (*pfile) {
259 fclose(*pfile);
260 }
261}
262#define FF_AUTO_CLOSE_FILE [[gnu::cleanup(wrapFclose)]]
263
264[[gnu::nonnull(1), gnu::always_inline]]
265#ifndef _WIN32
266static inline void wrapClosedir(DIR** pdir) {
267 if (*pdir) {
268 closedir(*pdir);
269 }
270}
271#else
272static inline void wrapClosedir(HANDLE* pdir) {
273 if (*pdir) {
274 FindClose(*pdir);
275 }
276}
277#endif
278#define FF_AUTO_CLOSE_DIR [[gnu::cleanup(wrapClosedir)]]
279
280[[gnu::nonnull(1, 2, 3)]] static inline bool ffSearchUserConfigFile(const FFlist* configDirs, const char* fileSubpath, FFstrbuf* result) {
281 // configDirs is a list of FFstrbufs include the trailing slash
282 FF_LIST_FOR_EACH (FFstrbuf, dir, *configDirs) {
283 ffStrbufClear(result);
284 ffStrbufAppend(result, dir);
285 ffStrbufAppendS(result, fileSubpath);
286 if (ffPathExists(result->chars, FF_PATHTYPE_FILE)) {
287 return true;
288 }
289 }
290
291 return false;
292}
293
294FFNativeFD ffGetNullFD(void);
295// Whether a descriptor is attached to a real terminal, i.e. whether a human is looking at it.
296//
297// Do not use `isatty()` for this on Windows. It is `_isatty()`, which answers "is this a character
298// device" rather than "is this a console", and `NUL` -- as well as MSYS2's `/dev/null` -- is a
299// character device. A redirected run therefore looks interactive, which is how
300// `fastfetch --gen-config <path> > /dev/null` ends up on the interactive path and blocks forever.
301bool ffIsTerminal(int fd);
302// Returns whether the file was removed; callers that only want it gone discard that, so not `nodiscard`
303[[gnu::nonnull(1)]] bool ffRemoveFile(const char* fileName);
304// Modification time of a file, in milliseconds since the Unix epoch, or 0 if it can not be read.
305// The representation is uniform across platforms so that a value derived from it means the
306// same thing everywhere, which matters for callers that store it as a cache key.
307[[gnu::nonnull(1)]] uint64_t ffPathGetMtime(const char* path);