1 // Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project
2 // Licensed under GPLv2 or any later version
3 // Refer to the license.txt file included.
4
5 #include <array>
6 #include <limits>
7 #include <memory>
8 #include <sstream>
9 #include <unordered_map>
10 #include "common/assert.h"
11 #include "common/common_funcs.h"
12 #include "common/common_paths.h"
13 #include "common/file_util.h"
14 #include "common/logging/log.h"
15 #include "core/settings.h"
16
17 #ifdef _WIN32
18 #include <windows.h>
19 // windows.h needs to be included before other windows headers
20 #include <direct.h> // getcwd
21 #include <io.h>
22 #include <shellapi.h>
23 #include <shlobj.h> // for SHGetFolderPath
24 #include <tchar.h>
25 #include "common/string_util.h"
26
27 #ifdef _MSC_VER
28 // 64 bit offsets for MSVC
29 #define fseeko _fseeki64
30 #define ftello _ftelli64
31 #define fileno _fileno
32 #endif
33
34 // 64 bit offsets for MSVC and MinGW. MinGW also needs this for using _wstat64
35 #define stat _stat64
36 #define fstat _fstat64
37
38 #else
39 #ifdef __APPLE__
40 #include <sys/param.h>
41 #endif
42 #include <cctype>
43 #include <cerrno>
44 #include <cstdlib>
45 #include <cstring>
46 #include <dirent.h>
47 #include <pwd.h>
48 #include <unistd.h>
49 #endif
50
51 #if defined(__APPLE__)
52 // CFURL contains __attribute__ directives that gcc does not know how to parse, so we need to just
53 // ignore them if we're not using clang. The macro is only used to prevent linking against
54 // functions that don't exist on older versions of macOS, and the worst case scenario is a linker
55 // error, so this is perfectly safe, just inconvenient.
56 #ifndef __clang__
57 #define availability(...)
58 #endif
59 #include <CoreFoundation/CFBundle.h>
60 #include <CoreFoundation/CFString.h>
61 #include <CoreFoundation/CFURL.h>
62 #ifdef availability
63 #undef availability
64 #endif
65
66 #endif
67
68 #include <algorithm>
69 #include <sys/stat.h>
70
71 #ifndef S_ISDIR
72 #define S_ISDIR(m) (((m)&S_IFMT) == S_IFDIR)
73 #endif
74
75 // This namespace has various generic functions related to files and paths.
76 // The code still needs a ton of cleanup.
77 // REMEMBER: strdup considered harmful!
78 namespace FileUtil {
79
80 // Remove any ending forward slashes from directory paths
81 // Modifies argument.
StripTailDirSlashes(std::string & fname)82 static void StripTailDirSlashes(std::string& fname) {
83 if (fname.length() <= 1) {
84 return;
85 }
86
87 std::size_t i = fname.length();
88 while (i > 0 && fname[i - 1] == DIR_SEP_CHR) {
89 --i;
90 }
91 fname.resize(i);
92 }
93
Exists(const std::string & filename)94 bool Exists(const std::string& filename) {
95 struct stat file_info;
96
97 std::string copy(filename);
98 StripTailDirSlashes(copy);
99
100 #ifdef _WIN32
101 // Windows needs a slash to identify a driver root
102 if (copy.size() != 0 && copy.back() == ':')
103 copy += DIR_SEP_CHR;
104
105 int result = _wstat64(Common::UTF8ToUTF16W(copy).c_str(), &file_info);
106 #else
107 int result = stat(copy.c_str(), &file_info);
108 #endif
109
110 return (result == 0);
111 }
112
IsDirectory(const std::string & filename)113 bool IsDirectory(const std::string& filename) {
114 struct stat file_info;
115
116 std::string copy(filename);
117 StripTailDirSlashes(copy);
118
119 #ifdef _WIN32
120 // Windows needs a slash to identify a driver root
121 if (copy.size() != 0 && copy.back() == ':')
122 copy += DIR_SEP_CHR;
123
124 int result = _wstat64(Common::UTF8ToUTF16W(copy).c_str(), &file_info);
125 #else
126 int result = stat(copy.c_str(), &file_info);
127 #endif
128
129 if (result < 0) {
130 LOG_DEBUG(Common_Filesystem, "stat failed on {}: {}", filename, GetLastErrorMsg());
131 return false;
132 }
133
134 return S_ISDIR(file_info.st_mode);
135 }
136
Delete(const std::string & filename)137 bool Delete(const std::string& filename) {
138 LOG_TRACE(Common_Filesystem, "file {}", filename);
139
140 // Return true because we care about the file no
141 // being there, not the actual delete.
142 if (!Exists(filename)) {
143 LOG_DEBUG(Common_Filesystem, "{} does not exist", filename);
144 return true;
145 }
146
147 // We can't delete a directory
148 if (IsDirectory(filename)) {
149 LOG_ERROR(Common_Filesystem, "Failed: {} is a directory", filename);
150 return false;
151 }
152
153 #ifdef _WIN32
154 if (!DeleteFileW(Common::UTF8ToUTF16W(filename).c_str())) {
155 LOG_ERROR(Common_Filesystem, "DeleteFile failed on {}: {}", filename, GetLastErrorMsg());
156 return false;
157 }
158 #else
159 if (unlink(filename.c_str()) == -1) {
160 LOG_ERROR(Common_Filesystem, "unlink failed on {}: {}", filename, GetLastErrorMsg());
161 return false;
162 }
163 #endif
164
165 return true;
166 }
167
CreateDir(const std::string & path)168 bool CreateDir(const std::string& path) {
169 LOG_TRACE(Common_Filesystem, "directory {}", path);
170 #ifdef _WIN32
171 if (::CreateDirectoryW(Common::UTF8ToUTF16W(path).c_str(), nullptr))
172 return true;
173 DWORD error = GetLastError();
174 if (error == ERROR_ALREADY_EXISTS) {
175 LOG_DEBUG(Common_Filesystem, "CreateDirectory failed on {}: already exists", path);
176 return true;
177 }
178 LOG_ERROR(Common_Filesystem, "CreateDirectory failed on {}: {}", path, error);
179 return false;
180 #else
181 if (mkdir(path.c_str(), 0755) == 0)
182 return true;
183
184 int err = errno;
185
186 if (err == EEXIST) {
187 LOG_DEBUG(Common_Filesystem, "mkdir failed on {}: already exists", path);
188 return true;
189 }
190
191 LOG_ERROR(Common_Filesystem, "mkdir failed on {}: {}", path, strerror(err));
192 return false;
193 #endif
194 }
195
CreateFullPath(const std::string & fullPath)196 bool CreateFullPath(const std::string& fullPath) {
197 int panicCounter = 100;
198 LOG_TRACE(Common_Filesystem, "path {}", fullPath);
199
200 if (FileUtil::Exists(fullPath)) {
201 LOG_DEBUG(Common_Filesystem, "path exists {}", fullPath);
202 return true;
203 }
204
205 std::size_t position = 0;
206 while (true) {
207 // Find next sub path
208 position = fullPath.find(DIR_SEP_CHR, position);
209
210 // we're done, yay!
211 if (position == fullPath.npos)
212 return true;
213
214 // Include the '/' so the first call is CreateDir("/") rather than CreateDir("")
215 std::string const subPath(fullPath.substr(0, position + 1));
216 if (!FileUtil::IsDirectory(subPath) && !FileUtil::CreateDir(subPath)) {
217 LOG_ERROR(Common, "CreateFullPath: directory creation failed");
218 return false;
219 }
220
221 // A safety check
222 panicCounter--;
223 if (panicCounter <= 0) {
224 LOG_ERROR(Common, "CreateFullPath: directory structure is too deep");
225 return false;
226 }
227 position++;
228 }
229 }
230
DeleteDir(const std::string & filename)231 bool DeleteDir(const std::string& filename) {
232 LOG_TRACE(Common_Filesystem, "directory {}", filename);
233
234 // check if a directory
235 if (!FileUtil::IsDirectory(filename)) {
236 LOG_ERROR(Common_Filesystem, "Not a directory {}", filename);
237 return false;
238 }
239
240 #ifdef _WIN32
241 if (::RemoveDirectoryW(Common::UTF8ToUTF16W(filename).c_str()))
242 return true;
243 #else
244 if (rmdir(filename.c_str()) == 0)
245 return true;
246 #endif
247 LOG_ERROR(Common_Filesystem, "failed {}: {}", filename, GetLastErrorMsg());
248
249 return false;
250 }
251
Rename(const std::string & srcFilename,const std::string & destFilename)252 bool Rename(const std::string& srcFilename, const std::string& destFilename) {
253 LOG_TRACE(Common_Filesystem, "{} --> {}", srcFilename, destFilename);
254 #ifdef _WIN32
255 if (_wrename(Common::UTF8ToUTF16W(srcFilename).c_str(),
256 Common::UTF8ToUTF16W(destFilename).c_str()) == 0)
257 return true;
258 #else
259 if (rename(srcFilename.c_str(), destFilename.c_str()) == 0)
260 return true;
261 #endif
262 LOG_ERROR(Common_Filesystem, "failed {} --> {}: {}", srcFilename, destFilename,
263 GetLastErrorMsg());
264 return false;
265 }
266
Copy(const std::string & srcFilename,const std::string & destFilename)267 bool Copy(const std::string& srcFilename, const std::string& destFilename) {
268 LOG_TRACE(Common_Filesystem, "{} --> {}", srcFilename, destFilename);
269 #ifdef _WIN32
270 if (CopyFileW(Common::UTF8ToUTF16W(srcFilename).c_str(),
271 Common::UTF8ToUTF16W(destFilename).c_str(), FALSE))
272 return true;
273
274 LOG_ERROR(Common_Filesystem, "failed {} --> {}: {}", srcFilename, destFilename,
275 GetLastErrorMsg());
276 return false;
277 #else
278 using CFilePointer = std::unique_ptr<FILE, decltype(&std::fclose)>;
279
280 // Open input file
281 CFilePointer input{fopen(srcFilename.c_str(), "rb"), std::fclose};
282 if (!input) {
283 LOG_ERROR(Common_Filesystem, "opening input failed {} --> {}: {}", srcFilename,
284 destFilename, GetLastErrorMsg());
285 return false;
286 }
287
288 // open output file
289 CFilePointer output{fopen(destFilename.c_str(), "wb"), std::fclose};
290 if (!output) {
291 LOG_ERROR(Common_Filesystem, "opening output failed {} --> {}: {}", srcFilename,
292 destFilename, GetLastErrorMsg());
293 return false;
294 }
295
296 // copy loop
297 std::array<char, 1024> buffer;
298 while (!feof(input.get())) {
299 // read input
300 std::size_t rnum = fread(buffer.data(), sizeof(char), buffer.size(), input.get());
301 if (rnum != buffer.size()) {
302 if (ferror(input.get()) != 0) {
303 LOG_ERROR(Common_Filesystem, "failed reading from source, {} --> {}: {}",
304 srcFilename, destFilename, GetLastErrorMsg());
305 return false;
306 }
307 }
308
309 // write output
310 std::size_t wnum = fwrite(buffer.data(), sizeof(char), rnum, output.get());
311 if (wnum != rnum) {
312 LOG_ERROR(Common_Filesystem, "failed writing to output, {} --> {}: {}", srcFilename,
313 destFilename, GetLastErrorMsg());
314 return false;
315 }
316 }
317
318 return true;
319 #endif
320 }
321
GetSize(const std::string & filename)322 u64 GetSize(const std::string& filename) {
323 if (!Exists(filename)) {
324 LOG_ERROR(Common_Filesystem, "failed {}: No such file", filename);
325 return 0;
326 }
327
328 if (IsDirectory(filename)) {
329 LOG_ERROR(Common_Filesystem, "failed {}: is a directory", filename);
330 return 0;
331 }
332
333 struct stat buf;
334 #ifdef _WIN32
335 if (_wstat64(Common::UTF8ToUTF16W(filename).c_str(), &buf) == 0)
336 #else
337 if (stat(filename.c_str(), &buf) == 0)
338 #endif
339 {
340 LOG_TRACE(Common_Filesystem, "{}: {}", filename, buf.st_size);
341 return buf.st_size;
342 }
343
344 LOG_ERROR(Common_Filesystem, "Stat failed {}: {}", filename, GetLastErrorMsg());
345 return 0;
346 }
347
GetSize(const int fd)348 u64 GetSize(const int fd) {
349 struct stat buf;
350 if (fstat(fd, &buf) != 0) {
351 LOG_ERROR(Common_Filesystem, "GetSize: stat failed {}: {}", fd, GetLastErrorMsg());
352 return 0;
353 }
354 return buf.st_size;
355 }
356
GetSize(FILE * f)357 u64 GetSize(FILE* f) {
358 // can't use off_t here because it can be 32-bit
359 u64 pos = ftello(f);
360 if (fseeko(f, 0, SEEK_END) != 0) {
361 LOG_ERROR(Common_Filesystem, "GetSize: seek failed {}: {}", fmt::ptr(f), GetLastErrorMsg());
362 return 0;
363 }
364 u64 size = ftello(f);
365 if ((size != pos) && (fseeko(f, pos, SEEK_SET) != 0)) {
366 LOG_ERROR(Common_Filesystem, "GetSize: seek failed {}: {}", fmt::ptr(f), GetLastErrorMsg());
367 return 0;
368 }
369 return size;
370 }
371
CreateEmptyFile(const std::string & filename)372 bool CreateEmptyFile(const std::string& filename) {
373 LOG_TRACE(Common_Filesystem, "{}", filename);
374
375 if (!FileUtil::IOFile(filename, "wb").IsOpen()) {
376 LOG_ERROR(Common_Filesystem, "failed {}: {}", filename, GetLastErrorMsg());
377 return false;
378 }
379
380 return true;
381 }
382
ForeachDirectoryEntry(u64 * num_entries_out,const std::string & directory,DirectoryEntryCallable callback)383 bool ForeachDirectoryEntry(u64* num_entries_out, const std::string& directory,
384 DirectoryEntryCallable callback) {
385 LOG_TRACE(Common_Filesystem, "directory {}", directory);
386
387 // How many files + directories we found
388 u64 found_entries = 0;
389
390 // Save the status of callback function
391 bool callback_error = false;
392
393 #ifdef _WIN32
394 // Find the first file in the directory.
395 WIN32_FIND_DATAW ffd;
396
397 HANDLE handle_find = FindFirstFileW(Common::UTF8ToUTF16W(directory + "\\*").c_str(), &ffd);
398 if (handle_find == INVALID_HANDLE_VALUE) {
399 FindClose(handle_find);
400 return false;
401 }
402 // windows loop
403 do {
404 const std::string virtual_name(Common::UTF16ToUTF8(ffd.cFileName));
405 #else
406 DIR* dirp = opendir(directory.c_str());
407 if (!dirp)
408 return false;
409
410 // non windows loop
411 while (struct dirent* result = readdir(dirp)) {
412 const std::string virtual_name(result->d_name);
413 #endif
414
415 if (virtual_name == "." || virtual_name == "..")
416 continue;
417
418 u64 ret_entries = 0;
419 if (!callback(&ret_entries, directory, virtual_name)) {
420 callback_error = true;
421 break;
422 }
423 found_entries += ret_entries;
424
425 #ifdef _WIN32
426 } while (FindNextFileW(handle_find, &ffd) != 0);
427 FindClose(handle_find);
428 #else
429 }
430 closedir(dirp);
431 #endif
432
433 if (callback_error)
434 return false;
435
436 // num_entries_out is allowed to be specified nullptr, in which case we shouldn't try to set it
437 if (num_entries_out != nullptr)
438 *num_entries_out = found_entries;
439 return true;
440 }
441
ScanDirectoryTree(const std::string & directory,FSTEntry & parent_entry,unsigned int recursion)442 u64 ScanDirectoryTree(const std::string& directory, FSTEntry& parent_entry,
443 unsigned int recursion) {
444 const auto callback = [recursion, &parent_entry](u64* num_entries_out,
445 const std::string& directory,
446 const std::string& virtual_name) -> bool {
447 FSTEntry entry;
448 entry.virtualName = virtual_name;
449 entry.physicalName = directory + DIR_SEP + virtual_name;
450
451 if (IsDirectory(entry.physicalName)) {
452 entry.isDirectory = true;
453 // is a directory, lets go inside if we didn't recurse to often
454 if (recursion > 0) {
455 entry.size = ScanDirectoryTree(entry.physicalName, entry, recursion - 1);
456 *num_entries_out += entry.size;
457 } else {
458 entry.size = 0;
459 }
460 } else { // is a file
461 entry.isDirectory = false;
462 entry.size = GetSize(entry.physicalName);
463 }
464 (*num_entries_out)++;
465
466 // Push into the tree
467 parent_entry.children.push_back(std::move(entry));
468 return true;
469 };
470
471 u64 num_entries;
472 return ForeachDirectoryEntry(&num_entries, directory, callback) ? num_entries : 0;
473 }
474
GetAllFilesFromNestedEntries(FSTEntry & directory,std::vector<FSTEntry> & output)475 void GetAllFilesFromNestedEntries(FSTEntry& directory, std::vector<FSTEntry>& output) {
476 std::vector<FSTEntry> files;
477 for (auto& entry : directory.children) {
478 if (entry.isDirectory) {
479 GetAllFilesFromNestedEntries(entry, output);
480 } else {
481 output.push_back(entry);
482 }
483 }
484 }
485
DeleteDirRecursively(const std::string & directory,unsigned int recursion)486 bool DeleteDirRecursively(const std::string& directory, unsigned int recursion) {
487 const auto callback = [recursion](u64* num_entries_out, const std::string& directory,
488 const std::string& virtual_name) -> bool {
489 std::string new_path = directory + DIR_SEP_CHR + virtual_name;
490
491 if (IsDirectory(new_path)) {
492 if (recursion == 0)
493 return false;
494 return DeleteDirRecursively(new_path, recursion - 1);
495 }
496 return Delete(new_path);
497 };
498
499 if (!ForeachDirectoryEntry(nullptr, directory, callback))
500 return false;
501
502 // Delete the outermost directory
503 FileUtil::DeleteDir(directory);
504 return true;
505 }
506
CopyDir(const std::string & source_path,const std::string & dest_path)507 void CopyDir(const std::string& source_path, const std::string& dest_path) {
508 #ifndef _WIN32
509 if (source_path == dest_path)
510 return;
511 if (!FileUtil::Exists(source_path))
512 return;
513 if (!FileUtil::Exists(dest_path))
514 FileUtil::CreateFullPath(dest_path);
515
516 DIR* dirp = opendir(source_path.c_str());
517 if (!dirp)
518 return;
519
520 while (struct dirent* result = readdir(dirp)) {
521 const std::string virtualName(result->d_name);
522 // check for "." and ".."
523 if (((virtualName[0] == '.') && (virtualName[1] == '\0')) ||
524 ((virtualName[0] == '.') && (virtualName[1] == '.') && (virtualName[2] == '\0')))
525 continue;
526
527 std::string source, dest;
528 source = source_path + virtualName;
529 dest = dest_path + virtualName;
530 if (IsDirectory(source)) {
531 source += '/';
532 dest += '/';
533 if (!FileUtil::Exists(dest))
534 FileUtil::CreateFullPath(dest);
535 CopyDir(source, dest);
536 } else if (!FileUtil::Exists(dest))
537 FileUtil::Copy(source, dest);
538 }
539 closedir(dirp);
540 #endif
541 }
542
GetCurrentDir()543 std::optional<std::string> GetCurrentDir() {
544 // Get the current working directory (getcwd uses malloc)
545 #ifdef _WIN32
546 wchar_t* dir = _wgetcwd(nullptr, 0);
547 if (!dir) {
548 #else
549 char* dir = getcwd(nullptr, 0);
550 if (!dir) {
551 #endif
552 LOG_ERROR(Common_Filesystem, "GetCurrentDirectory failed: {}", GetLastErrorMsg());
553 return {};
554 }
555 #ifdef _WIN32
556 std::string strDir = Common::UTF16ToUTF8(dir);
557 #else
558 std::string strDir = dir;
559 #endif
560 free(dir);
561 return strDir;
562 } // namespace FileUtil
563
564 bool SetCurrentDir(const std::string& directory) {
565 #ifdef _WIN32
566 return _wchdir(Common::UTF8ToUTF16W(directory).c_str()) == 0;
567 #else
568 return chdir(directory.c_str()) == 0;
569 #endif
570 }
571
572 #if defined(__APPLE__)
573 std::string GetBundleDirectory() {
574 CFURLRef BundleRef;
575 char AppBundlePath[MAXPATHLEN];
576 // Get the main bundle for the app
577 BundleRef = CFBundleCopyBundleURL(CFBundleGetMainBundle());
578 CFStringRef BundlePath = CFURLCopyFileSystemPath(BundleRef, kCFURLPOSIXPathStyle);
579 CFStringGetFileSystemRepresentation(BundlePath, AppBundlePath, sizeof(AppBundlePath));
580 CFRelease(BundleRef);
581 CFRelease(BundlePath);
582
583 return AppBundlePath;
584 }
585 #endif
586
587 #ifdef _WIN32
588 const std::string& GetExeDirectory() {
589 static std::string exe_path;
590 if (exe_path.empty()) {
591 wchar_t wchar_exe_path[2048];
592 GetModuleFileNameW(nullptr, wchar_exe_path, 2048);
593 exe_path = Common::UTF16ToUTF8(wchar_exe_path);
594 exe_path = exe_path.substr(0, exe_path.find_last_of('\\'));
595 }
596 return exe_path;
597 }
598
599 std::string AppDataRoamingDirectory() {
600 PWSTR pw_local_path = nullptr;
601 // Only supported by Windows Vista or later
602 SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &pw_local_path);
603 std::string local_path = Common::UTF16ToUTF8(pw_local_path);
604 CoTaskMemFree(pw_local_path);
605 return local_path;
606 }
607 #else
608 /**
609 * @return The user’s home directory on POSIX systems
610 */
611 static const std::string& GetHomeDirectory() {
612 static std::string home_path;
613 if (home_path.empty()) {
614 const char* envvar = getenv("HOME");
615 if (envvar) {
616 home_path = envvar;
617 } else {
618 auto pw = getpwuid(getuid());
619 ASSERT_MSG(pw,
620 "$HOME isn’t defined, and the current user can’t be found in /etc/passwd.");
621 home_path = pw->pw_dir;
622 }
623 }
624 return home_path;
625 }
626
627 /**
628 * Follows the XDG Base Directory Specification to get a directory path
629 * @param envvar The XDG environment variable to get the value from
630 * @return The directory path
631 * @sa http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
632 */
633 static const std::string GetUserDirectory(const std::string& envvar) {
634 const char* directory = getenv(envvar.c_str());
635
636 std::string user_dir;
637 if (directory) {
638 user_dir = directory;
639 } else {
640 std::string subdirectory;
641 if (envvar == "XDG_DATA_HOME")
642 subdirectory = DIR_SEP ".local" DIR_SEP "share";
643 else if (envvar == "XDG_CONFIG_HOME")
644 subdirectory = DIR_SEP ".config";
645 else if (envvar == "XDG_CACHE_HOME")
646 subdirectory = DIR_SEP ".cache";
647 else
648 ASSERT_MSG(false, "Unknown XDG variable {}.", envvar);
649 user_dir = GetHomeDirectory() + subdirectory;
650 }
651
652 ASSERT_MSG(!user_dir.empty(), "User directory {} musn’t be empty.", envvar);
653 ASSERT_MSG(user_dir[0] == '/', "User directory {} must be absolute.", envvar);
654
655 return user_dir;
656 }
657 #endif
658
659 std::string GetSysDirectory() {
660 std::string sysDir;
661
662 #if defined(__APPLE__)
663 sysDir = GetBundleDirectory();
664 sysDir += DIR_SEP;
665 sysDir += SYSDATA_DIR;
666 #else
667 sysDir = SYSDATA_DIR;
668 #endif
669 sysDir += DIR_SEP;
670
671 LOG_DEBUG(Common_Filesystem, "Setting to {}:", sysDir);
672 return sysDir;
673 }
674
675 namespace {
676 std::unordered_map<UserPath, std::string> g_paths;
677 }
678
679 void SetUserPath(const std::string& path) {
680 std::string& user_path = g_paths[UserPath::UserDir];
681
682 if (!path.empty() && CreateFullPath(path)) {
683 LOG_INFO(Common_Filesystem, "Using {} as the user directory", path);
684 user_path = path;
685 g_paths.emplace(UserPath::ConfigDir, user_path + CONFIG_DIR DIR_SEP);
686 g_paths.emplace(UserPath::CacheDir, user_path + CACHE_DIR DIR_SEP);
687 } else {
688 #ifdef _WIN32
689 user_path = GetExeDirectory() + DIR_SEP USERDATA_DIR DIR_SEP;
690 if (!FileUtil::IsDirectory(user_path)) {
691 user_path = AppDataRoamingDirectory() + DIR_SEP EMU_DATA_DIR DIR_SEP;
692 } else {
693 LOG_INFO(Common_Filesystem, "Using the local user directory");
694 }
695
696 g_paths.emplace(UserPath::ConfigDir, user_path + CONFIG_DIR DIR_SEP);
697 g_paths.emplace(UserPath::CacheDir, user_path + CACHE_DIR DIR_SEP);
698 #elif ANDROID
699 if (FileUtil::Exists(DIR_SEP SDCARD_DIR)) {
700 user_path = DIR_SEP SDCARD_DIR DIR_SEP EMU_DATA_DIR DIR_SEP;
701 g_paths.emplace(UserPath::ConfigDir, user_path + CONFIG_DIR DIR_SEP);
702 g_paths.emplace(UserPath::CacheDir, user_path + CACHE_DIR DIR_SEP);
703 }
704 #else
705 if (FileUtil::Exists(ROOT_DIR DIR_SEP USERDATA_DIR)) {
706 user_path = ROOT_DIR DIR_SEP USERDATA_DIR DIR_SEP;
707 g_paths.emplace(UserPath::ConfigDir, user_path + CONFIG_DIR DIR_SEP);
708 g_paths.emplace(UserPath::CacheDir, user_path + CACHE_DIR DIR_SEP);
709 } else {
710 std::string data_dir = GetUserDirectory("XDG_DATA_HOME");
711 std::string config_dir = GetUserDirectory("XDG_CONFIG_HOME");
712 std::string cache_dir = GetUserDirectory("XDG_CACHE_HOME");
713
714 user_path = data_dir + DIR_SEP EMU_DATA_DIR DIR_SEP;
715 g_paths.emplace(UserPath::ConfigDir, config_dir + DIR_SEP EMU_DATA_DIR DIR_SEP);
716 g_paths.emplace(UserPath::CacheDir, cache_dir + DIR_SEP EMU_DATA_DIR DIR_SEP);
717 }
718 #endif
719 }
720
721 g_paths.emplace(UserPath::SDMCDir, !Settings::values.sdmc_dir.empty()
722 ? Settings::values.sdmc_dir
723 : user_path + SDMC_DIR DIR_SEP);
724 g_paths.emplace(UserPath::NANDDir, !Settings::values.nand_dir.empty()
725 ? Settings::values.nand_dir
726 : user_path + NAND_DIR DIR_SEP);
727 g_paths.emplace(UserPath::SysDataDir, user_path + SYSDATA_DIR DIR_SEP);
728 // TODO: Put the logs in a better location for each OS
729 g_paths.emplace(UserPath::LogDir, user_path + LOG_DIR DIR_SEP);
730 g_paths.emplace(UserPath::CheatsDir, user_path + CHEATS_DIR DIR_SEP);
731 g_paths.emplace(UserPath::DLLDir, user_path + DLL_DIR DIR_SEP);
732 g_paths.emplace(UserPath::ShaderDir, user_path + SHADER_DIR DIR_SEP);
733 g_paths.emplace(UserPath::DumpDir, user_path + DUMP_DIR DIR_SEP);
734 g_paths.emplace(UserPath::LoadDir, user_path + LOAD_DIR DIR_SEP);
735 g_paths.emplace(UserPath::StatesDir, user_path + STATES_DIR DIR_SEP);
736 }
737
738 std::string g_currentRomPath{};
739
740 void SetCurrentRomPath(const std::string& path) {
741 g_currentRomPath = path;
742 }
743
744 bool StringReplace(std::string& haystack, const std::string& a, const std::string& b, bool swap) {
745 const auto& needle = swap ? b : a;
746 const auto& replacement = swap ? a : b;
747 if (needle.empty()) {
748 return false;
749 }
750 auto index = haystack.find(needle, 0);
751 if (index == std::string::npos) {
752 return false;
753 }
754 haystack.replace(index, needle.size(), replacement);
755 return true;
756 }
757
758 std::string SerializePath(const std::string& input, bool is_saving) {
759 auto result = input;
760 StringReplace(result, "%CITRA_ROM_FILE%", g_currentRomPath, is_saving);
761 StringReplace(result, "%CITRA_USER_DIR%", GetUserPath(UserPath::UserDir), is_saving);
762 return result;
763 }
764
765 const std::string& GetUserPath(UserPath path) {
766 // Set up all paths and files on the first run
767 if (g_paths.empty())
768 SetUserPath();
769 return g_paths[path];
770 }
771
772 const void UpdateUserPath(UserPath path, const std::string& filename) {
773 g_paths[path] = filename + DIR_SEP;
774 }
775
776 std::size_t WriteStringToFile(bool text_file, const std::string& filename, std::string_view str) {
777 return IOFile(filename, text_file ? "w" : "wb").WriteString(str);
778 }
779
780 std::size_t ReadFileToString(bool text_file, const std::string& filename, std::string& str) {
781 IOFile file(filename, text_file ? "r" : "rb");
782
783 if (!file.IsOpen())
784 return 0;
785
786 str.resize(static_cast<u32>(file.GetSize()));
787 return file.ReadArray(str.data(), str.size());
788 }
789
790 void SplitFilename83(const std::string& filename, std::array<char, 9>& short_name,
791 std::array<char, 4>& extension) {
792 const std::string forbidden_characters = ".\"/\\[]:;=, ";
793
794 // On a FAT32 partition, 8.3 names are stored as a 11 bytes array, filled with spaces.
795 short_name = {{' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '\0'}};
796 extension = {{' ', ' ', ' ', '\0'}};
797
798 std::string::size_type point = filename.rfind('.');
799 if (point == filename.size() - 1)
800 point = filename.rfind('.', point);
801
802 // Get short name.
803 int j = 0;
804 for (char letter : filename.substr(0, point)) {
805 if (forbidden_characters.find(letter, 0) != std::string::npos)
806 continue;
807 if (j == 8) {
808 // TODO(Link Mauve): also do that for filenames containing a space.
809 // TODO(Link Mauve): handle multiple files having the same short name.
810 short_name[6] = '~';
811 short_name[7] = '1';
812 break;
813 }
814 short_name[j++] = toupper(letter);
815 }
816
817 // Get extension.
818 if (point != std::string::npos) {
819 j = 0;
820 for (char letter : filename.substr(point + 1, 3))
821 extension[j++] = toupper(letter);
822 }
823 }
824
825 std::vector<std::string> SplitPathComponents(std::string_view filename) {
826 std::string copy(filename);
827 std::replace(copy.begin(), copy.end(), '\\', '/');
828 std::vector<std::string> out;
829
830 std::stringstream stream(copy);
831 std::string item;
832 while (std::getline(stream, item, '/')) {
833 out.push_back(std::move(item));
834 }
835
836 return out;
837 }
838
839 std::string_view GetParentPath(std::string_view path) {
840 const auto name_bck_index = path.rfind('\\');
841 const auto name_fwd_index = path.rfind('/');
842 std::size_t name_index;
843
844 if (name_bck_index == std::string_view::npos || name_fwd_index == std::string_view::npos) {
845 name_index = std::min(name_bck_index, name_fwd_index);
846 } else {
847 name_index = std::max(name_bck_index, name_fwd_index);
848 }
849
850 return path.substr(0, name_index);
851 }
852
853 std::string_view GetPathWithoutTop(std::string_view path) {
854 if (path.empty()) {
855 return path;
856 }
857
858 while (path[0] == '\\' || path[0] == '/') {
859 path.remove_prefix(1);
860 if (path.empty()) {
861 return path;
862 }
863 }
864
865 const auto name_bck_index = path.find('\\');
866 const auto name_fwd_index = path.find('/');
867 return path.substr(std::min(name_bck_index, name_fwd_index) + 1);
868 }
869
870 std::string_view GetFilename(std::string_view path) {
871 const auto name_index = path.find_last_of("\\/");
872
873 if (name_index == std::string_view::npos) {
874 return {};
875 }
876
877 return path.substr(name_index + 1);
878 }
879
880 std::string_view GetExtensionFromFilename(std::string_view name) {
881 const std::size_t index = name.rfind('.');
882
883 if (index == std::string_view::npos) {
884 return {};
885 }
886
887 return name.substr(index + 1);
888 }
889
890 std::string_view RemoveTrailingSlash(std::string_view path) {
891 if (path.empty()) {
892 return path;
893 }
894
895 if (path.back() == '\\' || path.back() == '/') {
896 path.remove_suffix(1);
897 return path;
898 }
899
900 return path;
901 }
902
903 std::string SanitizePath(std::string_view path_, DirectorySeparator directory_separator) {
904 std::string path(path_);
905 char type1 = directory_separator == DirectorySeparator::BackwardSlash ? '/' : '\\';
906 char type2 = directory_separator == DirectorySeparator::BackwardSlash ? '\\' : '/';
907
908 if (directory_separator == DirectorySeparator::PlatformDefault) {
909 #ifdef _WIN32
910 type1 = '/';
911 type2 = '\\';
912 #endif
913 }
914
915 std::replace(path.begin(), path.end(), type1, type2);
916
917 auto start = path.begin();
918 #ifdef _WIN32
919 // allow network paths which start with a double backslash (e.g. \\server\share)
920 if (start != path.end())
921 ++start;
922 #endif
923 path.erase(std::unique(start, path.end(),
924 [type2](char c1, char c2) { return c1 == type2 && c2 == type2; }),
925 path.end());
926 return std::string(RemoveTrailingSlash(path));
927 }
928
929 IOFile::IOFile() = default;
930
931 IOFile::IOFile(const std::string& filename, const char openmode[], int flags)
932 : filename(filename), openmode(openmode), flags(flags) {
933 Open();
934 }
935
936 IOFile::~IOFile() {
937 Close();
938 }
939
940 IOFile::IOFile(IOFile&& other) noexcept {
941 Swap(other);
942 }
943
944 IOFile& IOFile::operator=(IOFile&& other) noexcept {
945 Swap(other);
946 return *this;
947 }
948
949 void IOFile::Swap(IOFile& other) noexcept {
950 std::swap(m_file, other.m_file);
951 std::swap(m_good, other.m_good);
952 std::swap(filename, other.filename);
953 std::swap(openmode, other.openmode);
954 std::swap(flags, other.flags);
955 }
956
957 bool IOFile::Open() {
958 Close();
959
960 #ifdef _WIN32
961 if (flags != 0) {
962 m_file = _wfsopen(Common::UTF8ToUTF16W(filename).c_str(),
963 Common::UTF8ToUTF16W(openmode).c_str(), flags);
964 m_good = m_file != nullptr;
965 } else {
966 m_good = _wfopen_s(&m_file, Common::UTF8ToUTF16W(filename).c_str(),
967 Common::UTF8ToUTF16W(openmode).c_str()) == 0;
968 }
969 #else
970 m_file = std::fopen(filename.c_str(), openmode.c_str());
971 m_good = m_file != nullptr;
972 #endif
973
974 return m_good;
975 }
976
977 bool IOFile::Close() {
978 if (!IsOpen() || 0 != std::fclose(m_file))
979 m_good = false;
980
981 m_file = nullptr;
982 return m_good;
983 }
984
985 u64 IOFile::GetSize() const {
986 if (IsOpen())
987 return FileUtil::GetSize(m_file);
988
989 return 0;
990 }
991
992 bool IOFile::Seek(s64 off, int origin) {
993 if (!IsOpen() || 0 != fseeko(m_file, off, origin))
994 m_good = false;
995
996 return m_good;
997 }
998
999 u64 IOFile::Tell() const {
1000 if (IsOpen())
1001 return ftello(m_file);
1002
1003 return std::numeric_limits<u64>::max();
1004 }
1005
1006 bool IOFile::Flush() {
1007 if (!IsOpen() || 0 != std::fflush(m_file))
1008 m_good = false;
1009
1010 return m_good;
1011 }
1012
1013 std::size_t IOFile::ReadImpl(void* data, std::size_t length, std::size_t data_size) {
1014 if (!IsOpen()) {
1015 m_good = false;
1016 return std::numeric_limits<std::size_t>::max();
1017 }
1018
1019 if (length == 0) {
1020 return 0;
1021 }
1022
1023 DEBUG_ASSERT(data != nullptr);
1024
1025 return std::fread(data, data_size, length, m_file);
1026 }
1027
1028 std::size_t IOFile::WriteImpl(const void* data, std::size_t length, std::size_t data_size) {
1029 if (!IsOpen()) {
1030 m_good = false;
1031 return std::numeric_limits<std::size_t>::max();
1032 }
1033
1034 if (length == 0) {
1035 return 0;
1036 }
1037
1038 DEBUG_ASSERT(data != nullptr);
1039
1040 return std::fwrite(data, data_size, length, m_file);
1041 }
1042
1043 bool IOFile::Resize(u64 size) {
1044 if (!IsOpen() || 0 !=
1045 #ifdef _WIN32
1046 // ector: _chsize sucks, not 64-bit safe
1047 // F|RES: changed to _chsize_s. i think it is 64-bit safe
1048 _chsize_s(_fileno(m_file), size)
1049 #else
1050 // TODO: handle 64bit and growing
1051 ftruncate(fileno(m_file), size)
1052 #endif
1053 )
1054 m_good = false;
1055
1056 return m_good;
1057 }
1058
1059 } // namespace FileUtil
1060