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