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 fs_file_time(name: buffer, created: &created, modified: &modified);
236
237 CFsFileInfo info;
238 info.m_pName = entry->d_name;
239 info.m_TimeCreated = created;
240 info.m_TimeModified = modified;
241
242 if(cb(&info, fs_is_dir(path: buffer), type, user))
243 break;
244 }
245
246 closedir(dirp: dir_handle);
247#endif
248}
249
250int fs_chdir(const char *path)
251{
252#if defined(CONF_FAMILY_WINDOWS)
253 const std::wstring wide_path = windows_utf8_to_wide(path);
254 return SetCurrentDirectoryW(wide_path.c_str()) != 0 ? 0 : 1;
255#else
256 return chdir(path: path) ? 1 : 0;
257#endif
258}
259
260char *fs_getcwd(char *buffer, int buffer_size)
261{
262#if defined(CONF_FAMILY_WINDOWS)
263 const DWORD size_needed = GetCurrentDirectoryW(0, nullptr);
264 std::wstring wide_current_dir(size_needed, L'0');
265 dbg_assert(GetCurrentDirectoryW(size_needed, wide_current_dir.data()) == size_needed - 1, "GetCurrentDirectoryW failure");
266 const std::optional<std::string> current_dir = windows_wide_to_utf8(wide_current_dir.c_str());
267 if(!current_dir.has_value())
268 {
269 buffer[0] = '\0';
270 return nullptr;
271 }
272 str_copy(buffer, current_dir.value().c_str(), buffer_size);
273 fs_normalize_path(buffer);
274 return buffer;
275#else
276 char *result = getcwd(buf: buffer, size: buffer_size);
277 if(result == nullptr || !str_utf8_check(str: result))
278 {
279 buffer[0] = '\0';
280 return nullptr;
281 }
282 return result;
283#endif
284}
285
286int fs_storage_path(const char *appname, char *path, int max)
287{
288#if defined(CONF_FAMILY_WINDOWS)
289 WCHAR *wide_home = nullptr;
290 if(SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, /* current user */ nullptr, &wide_home) != S_OK)
291 {
292 log_error("filesystem", "ERROR: could not determine location of Roaming/AppData folder");
293 CoTaskMemFree(wide_home);
294 path[0] = '\0';
295 return -1;
296 }
297 const std::optional<std::string> home = windows_wide_to_utf8(wide_home);
298 CoTaskMemFree(wide_home);
299 if(!home.has_value())
300 {
301 log_error("filesystem", "ERROR: path of Roaming/AppData folder contains invalid UTF-16");
302 path[0] = '\0';
303 return -1;
304 }
305 str_copy(path, home.value().c_str(), max);
306 fs_normalize_path(path);
307 str_append(path, "/", max);
308 str_append(path, appname, max);
309 return 0;
310#else
311 char *home = getenv(name: "HOME");
312 if(!home)
313 {
314 path[0] = '\0';
315 return -1;
316 }
317
318 if(!str_utf8_check(str: home))
319 {
320 log_error("filesystem", "ERROR: the HOME environment variable contains invalid UTF-8");
321 path[0] = '\0';
322 return -1;
323 }
324
325#if defined(CONF_PLATFORM_HAIKU)
326 str_format(path, max, "%s/config/settings/%s", home, appname);
327#elif defined(CONF_PLATFORM_MACOS)
328 str_format(path, max, "%s/Library/Application Support/%s", home, appname);
329#else
330 if(str_comp(a: appname, b: "Teeworlds") == 0)
331 {
332 // fallback for old directory for Teeworlds compatibility
333 str_format(buffer: path, buffer_size: max, format: "%s/.%s", home, appname);
334 }
335 else
336 {
337 char *data_home = getenv(name: "XDG_DATA_HOME");
338 if(data_home)
339 {
340 if(!str_utf8_check(str: data_home))
341 {
342 log_error("filesystem", "ERROR: the XDG_DATA_HOME environment variable contains invalid UTF-8");
343 path[0] = '\0';
344 return -1;
345 }
346 str_format(buffer: path, buffer_size: max, format: "%s/%s", data_home, appname);
347 }
348 else
349 str_format(buffer: path, buffer_size: max, format: "%s/.local/share/%s", home, appname);
350 }
351 for(int i = str_length(str: path) - str_length(str: appname); path[i]; i++)
352 path[i] = tolower(c: (unsigned char)path[i]);
353#endif
354
355 return 0;
356#endif
357}
358
359int fs_executable_path(char *buffer, int buffer_size)
360{
361 // https://stackoverflow.com/a/1024937
362#if defined(CONF_FAMILY_WINDOWS)
363 wchar_t wide_path[IO_MAX_PATH_LENGTH];
364 if(GetModuleFileNameW(nullptr, wide_path, std::size(wide_path)) == 0 || GetLastError() != ERROR_SUCCESS)
365 {
366 buffer[0] = '\0';
367 return -1;
368 }
369 const std::optional<std::string> path = windows_wide_to_utf8(wide_path);
370 if(!path.has_value())
371 {
372 buffer[0] = '\0';
373 return -1;
374 }
375 str_copy(buffer, path.value().c_str(), buffer_size);
376 return 0;
377#elif defined(CONF_PLATFORM_MACOS)
378 // Get the size
379 uint32_t path_size = 0;
380 _NSGetExecutablePath(nullptr, &path_size);
381
382 char *path = (char *)malloc(path_size);
383 if(_NSGetExecutablePath(path, &path_size) != 0)
384 {
385 free(path);
386 buffer[0] = '\0';
387 return -1;
388 }
389 str_copy(buffer, path, buffer_size);
390 free(path);
391 return 0;
392#else
393 char path[IO_MAX_PATH_LENGTH];
394 static const char *NAMES[] = {
395 "/proc/self/exe", // Linux, Android
396 "/proc/curproc/exe", // NetBSD
397 "/proc/curproc/file", // DragonFly
398 };
399 for(auto &name : NAMES)
400 {
401 if(ssize_t bytes_written = readlink(path: name, buf: path, len: sizeof(path) - 1); bytes_written != -1)
402 {
403 path[bytes_written] = '\0'; // readlink does NOT null-terminate
404 // 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)
405 if(const char *deleted = str_endswith(str: path, suffix: " (deleted)"); deleted != nullptr)
406 {
407 path[deleted - path] = '\0';
408 }
409 str_copy(dst: buffer, src: path, dst_size: buffer_size);
410 return 0;
411 }
412 }
413 buffer[0] = '\0';
414 return -1;
415#endif
416}
417
418int fs_is_file(const char *path)
419{
420#if defined(CONF_FAMILY_WINDOWS)
421 const std::wstring wide_path = windows_utf8_to_wide(path);
422 DWORD attributes = GetFileAttributesW(wide_path.c_str());
423 return attributes != INVALID_FILE_ATTRIBUTES && !(attributes & FILE_ATTRIBUTE_DIRECTORY) ? 1 : 0;
424#else
425 struct stat sb;
426 if(stat(file: path, buf: &sb) == -1)
427 return 0;
428 return S_ISREG(sb.st_mode) ? 1 : 0;
429#endif
430}
431
432int fs_is_dir(const char *path)
433{
434#if defined(CONF_FAMILY_WINDOWS)
435 const std::wstring wide_path = windows_utf8_to_wide(path);
436 DWORD attributes = GetFileAttributesW(wide_path.c_str());
437 return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) ? 1 : 0;
438#else
439 struct stat sb;
440 if(stat(file: path, buf: &sb) == -1)
441 return 0;
442 return S_ISDIR(sb.st_mode) ? 1 : 0;
443#endif
444}
445
446int fs_is_relative_path(const char *path)
447{
448#if defined(CONF_FAMILY_WINDOWS)
449 const std::wstring wide_path = windows_utf8_to_wide(path);
450 return PathIsRelativeW(wide_path.c_str()) ? 1 : 0;
451#else
452 return path[0] == '/' ? 0 : 1; // yes, it's that simple
453#endif
454}
455
456const char *fs_filename(const char *path)
457{
458 for(const char *filename = path + str_length(str: path); filename >= path; --filename)
459 {
460 if(filename[0] == '/' || filename[0] == '\\')
461 return filename + 1;
462 }
463 return path;
464}
465
466void fs_split_file_extension(const char *filename, char *name, size_t name_size, char *extension, size_t extension_size)
467{
468 dbg_assert(name != nullptr || extension != nullptr, "name or extension parameter required");
469 dbg_assert(name == nullptr || name_size > 0, "name_size invalid");
470 dbg_assert(extension == nullptr || extension_size > 0, "extension_size invalid");
471
472 const char *last_dot = str_rchr(haystack: filename, needle: '.');
473 if(last_dot == nullptr || last_dot == filename)
474 {
475 if(extension != nullptr)
476 extension[0] = '\0';
477 if(name != nullptr)
478 str_copy(dst: name, src: filename, dst_size: name_size);
479 }
480 else
481 {
482 if(extension != nullptr)
483 str_copy(dst: extension, src: last_dot + 1, dst_size: extension_size);
484 if(name != nullptr)
485 str_truncate(dst: name, dst_size: name_size, src: filename, truncation_len: last_dot - filename);
486 }
487}
488
489int fs_parent_dir(char *path)
490{
491 char *parent = nullptr;
492 for(; *path; ++path)
493 {
494 if(*path == '/' || *path == '\\')
495 parent = path;
496 }
497
498 if(parent)
499 {
500 *parent = 0;
501 return 0;
502 }
503 return 1;
504}
505
506void fs_normalize_path(char *path)
507{
508 for(int i = 0; path[i] != '\0';)
509 {
510 if(path[i] == '\\')
511 {
512 path[i] = '/';
513 }
514 if(i > 0 && path[i] == '/' && path[i + 1] == '\0')
515 {
516 path[i] = '\0';
517 --i;
518 }
519 else
520 {
521 ++i;
522 }
523 }
524}
525
526int fs_remove(const char *filename)
527{
528#if defined(CONF_FAMILY_WINDOWS)
529 if(fs_is_dir(filename))
530 {
531 // Not great, but otherwise using this function on a folder would only rename the folder but fail to delete it.
532 return 1;
533 }
534 const std::wstring wide_filename = windows_utf8_to_wide(filename);
535
536 unsigned random_num;
537 secure_random_fill(&random_num, sizeof(random_num));
538 std::wstring wide_filename_temp;
539 do
540 {
541 char suffix[64];
542 str_format(suffix, sizeof(suffix), ".%08X.toberemoved", random_num);
543 wide_filename_temp = wide_filename + windows_utf8_to_wide(suffix);
544 ++random_num;
545 } while(GetFileAttributesW(wide_filename_temp.c_str()) != INVALID_FILE_ATTRIBUTES);
546
547 // The DeleteFileW function only marks the file for deletion but the deletion may not take effect immediately, which can
548 // cause subsequent operations using this filename to fail until all handles are closed. The MoveFileExW function with the
549 // MOVEFILE_WRITE_THROUGH flag is guaranteed to wait for the file to be moved on disk, so we first rename the file to be
550 // deleted to a random temporary name and then mark that for deletion, to ensure that the filename is usable immediately.
551 if(MoveFileExW(wide_filename.c_str(), wide_filename_temp.c_str(), MOVEFILE_WRITE_THROUGH) == 0)
552 {
553 const DWORD error = GetLastError();
554 if(error == ERROR_FILE_NOT_FOUND)
555 {
556 return 0; // Success: Renaming failed because the original file did not exist.
557 }
558 const std::string filename_temp = windows_wide_to_utf8(wide_filename_temp.c_str()).value_or("(invalid filename)");
559 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());
560 return 1;
561 }
562 if(DeleteFileW(wide_filename_temp.c_str()) != 0)
563 {
564 return 0; // Success: Marked the renamed file for deletion successfully.
565 }
566 const DWORD error = GetLastError();
567 if(error == ERROR_FILE_NOT_FOUND)
568 {
569 return 0; // Success: Another process deleted the renamed file we were about to delete?!
570 }
571 const std::string filename_temp = windows_wide_to_utf8(wide_filename_temp.c_str()).value_or("(invalid filename)");
572 log_error("filesystem", "Failed to remove file '%s' (%ld '%s')", filename_temp.c_str(), error, windows_format_system_message(error).c_str());
573 // Success: While the temporary could not be deleted, this is also considered success because the original file does not exist anymore.
574 // Callers of this function expect that the original file does not exist anymore if and only if the function succeeded.
575 return 0;
576#else
577 if(unlink(name: filename) == 0 || errno == ENOENT)
578 {
579 return 0;
580 }
581 log_error("filesystem", "Failed to remove file '%s' (%d '%s')", filename, errno, strerror(errno));
582 return 1;
583#endif
584}
585
586int fs_rename(const char *oldname, const char *newname)
587{
588#if defined(CONF_FAMILY_WINDOWS)
589 const std::wstring wide_oldname = windows_utf8_to_wide(oldname);
590 const std::wstring wide_newname = windows_utf8_to_wide(newname);
591 if(MoveFileExW(wide_oldname.c_str(), wide_newname.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH) != 0)
592 {
593 return 0;
594 }
595 const DWORD error = GetLastError();
596 log_error("filesystem", "Failed to rename file '%s' to '%s' (%ld '%s')", oldname, newname, error, windows_format_system_message(error).c_str());
597 return 1;
598#else
599 if(rename(old: oldname, new: newname) == 0)
600 {
601 return 0;
602 }
603 log_error("filesystem", "Failed to rename file '%s' to '%s' (%d '%s')", oldname, newname, errno, strerror(errno));
604 return 1;
605#endif
606}
607
608int fs_file_time(const char *name, time_t *created, time_t *modified)
609{
610#if defined(CONF_FAMILY_WINDOWS)
611 WIN32_FIND_DATAW finddata;
612 const std::wstring wide_name = windows_utf8_to_wide(name);
613 HANDLE handle = FindFirstFileW(wide_name.c_str(), &finddata);
614 if(handle == INVALID_HANDLE_VALUE)
615 return 1;
616
617 *created = filetime_to_unixtime(&finddata.ftCreationTime);
618 *modified = filetime_to_unixtime(&finddata.ftLastWriteTime);
619 FindClose(handle);
620#elif defined(CONF_FAMILY_UNIX)
621 struct stat sb;
622 if(stat(file: name, buf: &sb))
623 return 1;
624
625 *created = sb.st_ctime;
626 *modified = sb.st_mtime;
627#else
628#error not implemented
629#endif
630
631 return 0;
632}
633