1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3
4#include <base/dbg.h>
5#include <base/fs.h>
6#include <base/log.h>
7#include <base/secure.h>
8#include <base/str.h>
9#include <base/types.h>
10#include <base/windows.h>
11
12#include <cerrno>
13#include <cstring>
14
15#if defined(CONF_FAMILY_UNIX)
16#include <dirent.h>
17#include <sys/stat.h>
18#include <unistd.h>
19
20#include <cctype> // tolower
21#include <cstdio> // rename
22#include <cstdlib> // getenv
23#elif defined(CONF_FAMILY_WINDOWS)
24#include <io.h>
25#include <shlobj.h> // SHGetKnownFolderPath
26#include <shlwapi.h>
27#include <windows.h>
28
29#include <string>
30#else
31#error NOT IMPLEMENTED
32#endif
33
34#if defined(CONF_PLATFORM_MACOS)
35#include <mach-o/dyld.h> // _NSGetExecutablePath
36#endif
37
38int fs_makedir(const char *path)
39{
40#if defined(CONF_FAMILY_WINDOWS)
41 const std::wstring wide_path = windows_utf8_to_wide(path);
42 if(CreateDirectoryW(wide_path.c_str(), nullptr) != 0)
43 {
44 return 0;
45 }
46 const DWORD error = GetLastError();
47 if(error == ERROR_ALREADY_EXISTS)
48 {
49 return 0;
50 }
51 log_error("filesystem", "Failed to create folder '%s' (%ld '%s')", path, error, windows_format_system_message(error).c_str());
52 return -1;
53#else
54#if defined(CONF_PLATFORM_HAIKU)
55 if(fs_is_dir(path))
56 {
57 return 0;
58 }
59#endif
60 if(mkdir(path: path, mode: 0755) == 0 || errno == EEXIST)
61 {
62 return 0;
63 }
64 log_error("filesystem", "Failed to create folder '%s' (%d '%s')", path, errno, strerror(errno));
65 return -1;
66#endif
67}
68
69int fs_makedir_rec_for(const char *path)
70{
71 char buffer[IO_MAX_PATH_LENGTH];
72 str_copy(dst&: buffer, src: path);
73 for(int index = 1; buffer[index] != '\0'; ++index)
74 {
75 // Do not try to create folder for drive letters on Windows,
76 // as this is not necessary and may fail for system drives.
77 if((buffer[index] == '/' || buffer[index] == '\\') && buffer[index + 1] != '\0' && buffer[index - 1] != ':')
78 {
79 buffer[index] = '\0';
80 if(fs_makedir(path: buffer) < 0)
81 {
82 return -1;
83 }
84 buffer[index] = '/';
85 }
86 }
87 return 0;
88}
89
90int fs_removedir(const char *path)
91{
92#if defined(CONF_FAMILY_WINDOWS)
93 const std::wstring wide_path = windows_utf8_to_wide(path);
94 if(RemoveDirectoryW(wide_path.c_str()) != 0)
95 {
96 return 0;
97 }
98 const DWORD error = GetLastError();
99 if(error == ERROR_FILE_NOT_FOUND)
100 {
101 return 0;
102 }
103 log_error("filesystem", "Failed to remove folder '%s' (%ld '%s')", path, error, windows_format_system_message(error).c_str());
104 return -1;
105#else
106 if(rmdir(path: path) == 0 || errno == ENOENT)
107 {
108 return 0;
109 }
110 log_error("filesystem", "Failed to remove folder '%s' (%d '%s')", path, errno, strerror(errno));
111 return -1;
112#endif
113}
114
115#if defined(CONF_FAMILY_WINDOWS)
116static inline time_t filetime_to_unixtime(LPFILETIME filetime)
117{
118 time_t t;
119 ULARGE_INTEGER li;
120 li.LowPart = filetime->dwLowDateTime;
121 li.HighPart = filetime->dwHighDateTime;
122
123 li.QuadPart /= 10000000; // 100ns to 1s
124 li.QuadPart -= 11644473600LL; // Windows epoch is in the past
125
126 t = li.QuadPart;
127 return t == (time_t)li.QuadPart ? t : (time_t)-1;
128}
129#endif
130
131void fs_listdir(const char *dir, FS_LISTDIR_CALLBACK cb, int type, void *user)
132{
133#if defined(CONF_FAMILY_WINDOWS)
134 char buffer[IO_MAX_PATH_LENGTH];
135 str_format(buffer, sizeof(buffer), "%s/*", dir);
136 const std::wstring wide_buffer = windows_utf8_to_wide(buffer);
137
138 WIN32_FIND_DATAW finddata;
139 HANDLE handle = FindFirstFileW(wide_buffer.c_str(), &finddata);
140 if(handle == INVALID_HANDLE_VALUE)
141 return;
142
143 do
144 {
145 const std::optional<std::string> current_entry = windows_wide_to_utf8(finddata.cFileName);
146 if(!current_entry.has_value())
147 {
148 log_error("filesystem", "ERROR: file/folder name containing invalid UTF-16 found in folder '%s'", dir);
149 continue;
150 }
151 if(cb(current_entry.value().c_str(), (finddata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0, type, user))
152 break;
153 } while(FindNextFileW(handle, &finddata));
154
155 FindClose(handle);
156#else
157 DIR *dir_handle = opendir(name: dir);
158 if(dir_handle == nullptr)
159 return;
160
161 char buffer[IO_MAX_PATH_LENGTH];
162 str_format(buffer, buffer_size: sizeof(buffer), format: "%s/", dir);
163 size_t length = str_length(str: buffer);
164 while(true)
165 {
166 struct dirent *entry = readdir(dirp: dir_handle);
167 if(entry == nullptr)
168 break;
169 if(!str_utf8_check(str: entry->d_name))
170 {
171 log_error("filesystem", "ERROR: file/folder name containing invalid UTF-8 found in folder '%s'", dir);
172 continue;
173 }
174 str_copy(dst: buffer + length, src: entry->d_name, dst_size: sizeof(buffer) - length);
175 if(cb(entry->d_name, fs_is_dir(path: buffer), type, user))
176 break;
177 }
178
179 closedir(dirp: dir_handle);
180#endif
181}
182
183void fs_listdir_fileinfo(const char *dir, FS_LISTDIR_CALLBACK_FILEINFO cb, int type, void *user)
184{
185#if defined(CONF_FAMILY_WINDOWS)
186 char buffer[IO_MAX_PATH_LENGTH];
187 str_format(buffer, sizeof(buffer), "%s/*", dir);
188 const std::wstring wide_buffer = windows_utf8_to_wide(buffer);
189
190 WIN32_FIND_DATAW finddata;
191 HANDLE handle = FindFirstFileW(wide_buffer.c_str(), &finddata);
192 if(handle == INVALID_HANDLE_VALUE)
193 return;
194
195 do
196 {
197 const std::optional<std::string> current_entry = windows_wide_to_utf8(finddata.cFileName);
198 if(!current_entry.has_value())
199 {
200 log_error("filesystem", "ERROR: file/folder name containing invalid UTF-16 found in folder '%s'", dir);
201 continue;
202 }
203
204 CFsFileInfo info;
205 info.m_pName = current_entry.value().c_str();
206 info.m_TimeCreated = filetime_to_unixtime(&finddata.ftCreationTime);
207 info.m_TimeModified = filetime_to_unixtime(&finddata.ftLastWriteTime);
208
209 if(cb(&info, (finddata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0, type, user))
210 break;
211 } while(FindNextFileW(handle, &finddata));
212
213 FindClose(handle);
214#else
215 DIR *dir_handle = opendir(name: dir);
216 if(dir_handle == nullptr)
217 return;
218
219 char buffer[IO_MAX_PATH_LENGTH];
220 str_format(buffer, buffer_size: sizeof(buffer), format: "%s/", dir);
221 size_t length = str_length(str: buffer);
222
223 while(true)
224 {
225 struct dirent *entry = readdir(dirp: dir_handle);
226 if(entry == nullptr)
227 break;
228 if(!str_utf8_check(str: entry->d_name))
229 {
230 log_error("filesystem", "ERROR: file/folder name containing invalid UTF-8 found in folder '%s'", dir);
231 continue;
232 }
233 str_copy(dst: buffer + length, src: entry->d_name, dst_size: sizeof(buffer) - length);
234 time_t created = -1, modified = -1;
235 if(fs_file_time(name: buffer, created: &created, modified: &modified) != 0)
236 {
237 log_warn("filesystem", "Failed to determine file time of '%s'", buffer);
238 }
239
240 CFsFileInfo info;
241 info.m_pName = entry->d_name;
242 info.m_TimeCreated = created;
243 info.m_TimeModified = modified;
244
245 if(cb(&info, fs_is_dir(path: buffer), type, user))
246 break;
247 }
248
249 closedir(dirp: dir_handle);
250#endif
251}
252
253int fs_chdir(const char *path)
254{
255#if defined(CONF_FAMILY_WINDOWS)
256 const std::wstring wide_path = windows_utf8_to_wide(path);
257 return SetCurrentDirectoryW(wide_path.c_str()) != 0 ? 0 : 1;
258#else
259 return chdir(path: path) ? 1 : 0;
260#endif
261}
262
263char *fs_getcwd(char *buffer, int buffer_size)
264{
265#if defined(CONF_FAMILY_WINDOWS)
266 const DWORD size_needed = GetCurrentDirectoryW(0, nullptr);
267 std::wstring wide_current_dir(size_needed, L'0');
268 dbg_assert(GetCurrentDirectoryW(size_needed, wide_current_dir.data()) == size_needed - 1, "GetCurrentDirectoryW failure");
269 const std::optional<std::string> current_dir = windows_wide_to_utf8(wide_current_dir.c_str());
270 if(!current_dir.has_value())
271 {
272 buffer[0] = '\0';
273 return nullptr;
274 }
275 str_copy(buffer, current_dir.value().c_str(), buffer_size);
276 fs_normalize_path(buffer);
277 return buffer;
278#else
279 char *result = getcwd(buf: buffer, size: buffer_size);
280 if(result == nullptr || !str_utf8_check(str: result))
281 {
282 buffer[0] = '\0';
283 return nullptr;
284 }
285 return result;
286#endif
287}
288
289int fs_storage_path(const char *appname, char *path, int max)
290{
291#if defined(CONF_FAMILY_WINDOWS)
292 WCHAR *wide_home = nullptr;
293 if(SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, /* current user */ nullptr, &wide_home) != S_OK)
294 {
295 log_error("filesystem", "ERROR: could not determine location of Roaming/AppData folder");
296 CoTaskMemFree(wide_home);
297 path[0] = '\0';
298 return -1;
299 }
300 const std::optional<std::string> home = windows_wide_to_utf8(wide_home);
301 CoTaskMemFree(wide_home);
302 if(!home.has_value())
303 {
304 log_error("filesystem", "ERROR: path of Roaming/AppData folder contains invalid UTF-16");
305 path[0] = '\0';
306 return -1;
307 }
308 str_copy(path, home.value().c_str(), max);
309 fs_normalize_path(path);
310 str_append(path, "/", max);
311 str_append(path, appname, max);
312 return 0;
313#else
314 char *home = getenv(name: "HOME");
315 if(!home)
316 {
317 path[0] = '\0';
318 return -1;
319 }
320
321 if(!str_utf8_check(str: home))
322 {
323 log_error("filesystem", "ERROR: the HOME environment variable contains invalid UTF-8");
324 path[0] = '\0';
325 return -1;
326 }
327
328#if defined(CONF_PLATFORM_HAIKU)
329 str_format(path, max, "%s/config/settings/%s", home, appname);
330#elif defined(CONF_PLATFORM_MACOS)
331 str_format(path, max, "%s/Library/Application Support/%s", home, appname);
332#else
333 if(str_comp(a: appname, b: "Teeworlds") == 0)
334 {
335 // fallback for old directory for Teeworlds compatibility
336 str_format(buffer: path, buffer_size: max, format: "%s/.%s", home, appname);
337 }
338 else
339 {
340 char *data_home = getenv(name: "XDG_DATA_HOME");
341 if(data_home)
342 {
343 if(!str_utf8_check(str: data_home))
344 {
345 log_error("filesystem", "ERROR: the XDG_DATA_HOME environment variable contains invalid UTF-8");
346 path[0] = '\0';
347 return -1;
348 }
349 str_format(buffer: path, buffer_size: max, format: "%s/%s", data_home, appname);
350 }
351 else
352 str_format(buffer: path, buffer_size: max, format: "%s/.local/share/%s", home, appname);
353 }
354 for(int i = str_length(str: path) - str_length(str: appname); path[i]; i++)
355 path[i] = tolower(c: (unsigned char)path[i]);
356#endif
357
358 return 0;
359#endif
360}
361
362int fs_executable_path(char *buffer, int buffer_size)
363{
364 // https://stackoverflow.com/a/1024937
365#if defined(CONF_FAMILY_WINDOWS)
366 wchar_t wide_path[IO_MAX_PATH_LENGTH];
367 if(GetModuleFileNameW(nullptr, wide_path, std::size(wide_path)) == 0 || GetLastError() != ERROR_SUCCESS)
368 {
369 buffer[0] = '\0';
370 return -1;
371 }
372 const std::optional<std::string> path = windows_wide_to_utf8(wide_path);
373 if(!path.has_value())
374 {
375 buffer[0] = '\0';
376 return -1;
377 }
378 str_copy(buffer, path.value().c_str(), buffer_size);
379 return 0;
380#elif defined(CONF_PLATFORM_MACOS)
381 // Get the size
382 uint32_t path_size = 0;
383 _NSGetExecutablePath(nullptr, &path_size);
384
385 char *path = (char *)malloc(path_size);
386 if(_NSGetExecutablePath(path, &path_size) != 0)
387 {
388 free(path);
389 buffer[0] = '\0';
390 return -1;
391 }
392 str_copy(buffer, path, buffer_size);
393 free(path);
394 return 0;
395#else
396 char path[IO_MAX_PATH_LENGTH];
397 static const char *NAMES[] = {
398 "/proc/self/exe", // Linux, Android
399 "/proc/curproc/exe", // NetBSD
400 "/proc/curproc/file", // DragonFly
401 };
402 for(auto &name : NAMES)
403 {
404 if(ssize_t bytes_written = readlink(path: name, buf: path, len: sizeof(path) - 1); bytes_written != -1)
405 {
406 path[bytes_written] = '\0'; // readlink does NOT null-terminate
407 // if the file gets deleted or replaced (not renamed) linux appends (deleted) to the symlink (see https://man7.org/linux/man-pages/man5/proc_pid_exe.5.html)
408 if(const char *deleted = str_endswith(str: path, suffix: " (deleted)"); deleted != nullptr)
409 {
410 path[deleted - path] = '\0';
411 }
412 str_copy(dst: buffer, src: path, dst_size: buffer_size);
413 return 0;
414 }
415 }
416 buffer[0] = '\0';
417 return -1;
418#endif
419}
420
421int fs_is_file(const char *path)
422{
423#if defined(CONF_FAMILY_WINDOWS)
424 const std::wstring wide_path = windows_utf8_to_wide(path);
425 DWORD attributes = GetFileAttributesW(wide_path.c_str());
426 return attributes != INVALID_FILE_ATTRIBUTES && !(attributes & FILE_ATTRIBUTE_DIRECTORY) ? 1 : 0;
427#else
428 struct stat sb;
429 if(stat(file: path, buf: &sb) == -1)
430 return 0;
431 return S_ISREG(sb.st_mode) ? 1 : 0;
432#endif
433}
434
435int fs_is_dir(const char *path)
436{
437#if defined(CONF_FAMILY_WINDOWS)
438 const std::wstring wide_path = windows_utf8_to_wide(path);
439 DWORD attributes = GetFileAttributesW(wide_path.c_str());
440 return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) ? 1 : 0;
441#else
442 struct stat sb;
443 if(stat(file: path, buf: &sb) == -1)
444 return 0;
445 return S_ISDIR(sb.st_mode) ? 1 : 0;
446#endif
447}
448
449int fs_is_relative_path(const char *path)
450{
451#if defined(CONF_FAMILY_WINDOWS)
452 const std::wstring wide_path = windows_utf8_to_wide(path);
453 return PathIsRelativeW(wide_path.c_str()) ? 1 : 0;
454#else
455 return path[0] == '/' ? 0 : 1; // yes, it's that simple
456#endif
457}
458
459const char *fs_filename(const char *path)
460{
461 for(const char *filename = path + str_length(str: path); filename >= path; --filename)
462 {
463 if(filename[0] == '/' || filename[0] == '\\')
464 return filename + 1;
465 }
466 return path;
467}
468
469void fs_split_file_extension(const char *filename, char *name, size_t name_size, char *extension, size_t extension_size)
470{
471 dbg_assert(name != nullptr || extension != nullptr, "name or extension parameter required");
472 dbg_assert(name == nullptr || name_size > 0, "name_size invalid");
473 dbg_assert(extension == nullptr || extension_size > 0, "extension_size invalid");
474
475 const char *last_dot = str_rchr(haystack: filename, needle: '.');
476 if(last_dot == nullptr || last_dot == filename)
477 {
478 if(extension != nullptr)
479 extension[0] = '\0';
480 if(name != nullptr)
481 str_copy(dst: name, src: filename, dst_size: name_size);
482 }
483 else
484 {
485 if(extension != nullptr)
486 str_copy(dst: extension, src: last_dot + 1, dst_size: extension_size);
487 if(name != nullptr)
488 str_truncate(dst: name, dst_size: name_size, src: filename, truncation_len: last_dot - filename);
489 }
490}
491
492int fs_parent_dir(char *path)
493{
494 char *parent = nullptr;
495 for(; *path; ++path)
496 {
497 if(*path == '/' || *path == '\\')
498 parent = path;
499 }
500
501 if(parent)
502 {
503 *parent = 0;
504 return 0;
505 }
506 return 1;
507}
508
509void fs_normalize_path(char *path)
510{
511 for(int i = 0; path[i] != '\0';)
512 {
513 if(path[i] == '\\')
514 {
515 path[i] = '/';
516 }
517 if(i > 0 && path[i] == '/' && path[i + 1] == '\0')
518 {
519 path[i] = '\0';
520 --i;
521 }
522 else
523 {
524 ++i;
525 }
526 }
527}
528
529int fs_remove(const char *filename)
530{
531#if defined(CONF_FAMILY_WINDOWS)
532 if(fs_is_dir(filename))
533 {
534 // Not great, but otherwise using this function on a folder would only rename the folder but fail to delete it.
535 return 1;
536 }
537 const std::wstring wide_filename = windows_utf8_to_wide(filename);
538
539 unsigned random_num;
540 secure_random_fill(&random_num, sizeof(random_num));
541 std::wstring wide_filename_temp;
542 do
543 {
544 char suffix[64];
545 str_format(suffix, sizeof(suffix), ".%08X.toberemoved", random_num);
546 wide_filename_temp = wide_filename + windows_utf8_to_wide(suffix);
547 ++random_num;
548 } while(GetFileAttributesW(wide_filename_temp.c_str()) != INVALID_FILE_ATTRIBUTES);
549
550 // The DeleteFileW function only marks the file for deletion but the deletion may not take effect immediately, which can
551 // cause subsequent operations using this filename to fail until all handles are closed. The MoveFileExW function with the
552 // MOVEFILE_WRITE_THROUGH flag is guaranteed to wait for the file to be moved on disk, so we first rename the file to be
553 // deleted to a random temporary name and then mark that for deletion, to ensure that the filename is usable immediately.
554 if(MoveFileExW(wide_filename.c_str(), wide_filename_temp.c_str(), MOVEFILE_WRITE_THROUGH) == 0)
555 {
556 const DWORD error = GetLastError();
557 if(error == ERROR_FILE_NOT_FOUND)
558 {
559 return 0; // Success: Renaming failed because the original file did not exist.
560 }
561 const std::string filename_temp = windows_wide_to_utf8(wide_filename_temp.c_str()).value_or("(invalid filename)");
562 log_error("filesystem", "Failed to rename file '%s' to '%s' for removal (%ld '%s')", filename, filename_temp.c_str(), error, windows_format_system_message(error).c_str());
563 return 1;
564 }
565 if(DeleteFileW(wide_filename_temp.c_str()) != 0)
566 {
567 return 0; // Success: Marked the renamed file for deletion successfully.
568 }
569 const DWORD error = GetLastError();
570 if(error == ERROR_FILE_NOT_FOUND)
571 {
572 return 0; // Success: Another process deleted the renamed file we were about to delete?!
573 }
574 const std::string filename_temp = windows_wide_to_utf8(wide_filename_temp.c_str()).value_or("(invalid filename)");
575 log_error("filesystem", "Failed to remove file '%s' (%ld '%s')", filename_temp.c_str(), error, windows_format_system_message(error).c_str());
576 // Success: While the temporary could not be deleted, this is also considered success because the original file does not exist anymore.
577 // Callers of this function expect that the original file does not exist anymore if and only if the function succeeded.
578 return 0;
579#else
580 if(unlink(name: filename) == 0 || errno == ENOENT)
581 {
582 return 0;
583 }
584 log_error("filesystem", "Failed to remove file '%s' (%d '%s')", filename, errno, strerror(errno));
585 return 1;
586#endif
587}
588
589int fs_rename(const char *oldname, const char *newname)
590{
591#if defined(CONF_FAMILY_WINDOWS)
592 // Target file must be deleted first, else rename fails on Windows when the target file has open handles.
593 // Ignore the result and try to perform the rename anyway.
594 (void)fs_remove(newname);
595
596 const std::wstring wide_oldname = windows_utf8_to_wide(oldname);
597 const std::wstring wide_newname = windows_utf8_to_wide(newname);
598 if(MoveFileExW(wide_oldname.c_str(), wide_newname.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH) != 0)
599 {
600 return 0;
601 }
602 const DWORD error = GetLastError();
603 log_error("filesystem", "Failed to rename file '%s' to '%s' (%ld '%s')", oldname, newname, error, windows_format_system_message(error).c_str());
604 return 1;
605#else
606 if(rename(old: oldname, new: newname) == 0)
607 {
608 return 0;
609 }
610 log_error("filesystem", "Failed to rename file '%s' to '%s' (%d '%s')", oldname, newname, errno, strerror(errno));
611 return 1;
612#endif
613}
614
615int fs_file_time(const char *name, time_t *created, time_t *modified)
616{
617#if defined(CONF_FAMILY_WINDOWS)
618 WIN32_FIND_DATAW finddata;
619 const std::wstring wide_name = windows_utf8_to_wide(name);
620 HANDLE handle = FindFirstFileW(wide_name.c_str(), &finddata);
621 if(handle == INVALID_HANDLE_VALUE)
622 return 1;
623
624 *created = filetime_to_unixtime(&finddata.ftCreationTime);
625 *modified = filetime_to_unixtime(&finddata.ftLastWriteTime);
626 FindClose(handle);
627#elif defined(CONF_FAMILY_UNIX)
628 struct stat sb;
629 if(stat(file: name, buf: &sb))
630 return 1;
631
632 *created = sb.st_ctime;
633 *modified = sb.st_mtime;
634#else
635#error not implemented
636#endif
637
638 return 0;
639}
640