summaryrefslogtreecommitdiff
path: root/src/common/fs/path_util.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/common/fs/path_util.cpp')
-rw-r--r--src/common/fs/path_util.cpp432
1 files changed, 432 insertions, 0 deletions
diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp
new file mode 100644
index 000000000..8b732a21c
--- /dev/null
+++ b/src/common/fs/path_util.cpp
@@ -0,0 +1,432 @@
1// Copyright 2021 yuzu Emulator Project
2// Licensed under GPLv2 or any later version
3// Refer to the license.txt file included.
4
5#include <algorithm>
6#include <unordered_map>
7
8#include "common/fs/fs.h"
9#include "common/fs/fs_paths.h"
10#include "common/fs/path_util.h"
11#include "common/logging/log.h"
12
13#ifdef _WIN32
14#include <shlobj.h> // Used in GetExeDirectory()
15#else
16#include <cstdlib> // Used in Get(Home/Data)Directory()
17#include <pwd.h> // Used in GetHomeDirectory()
18#include <sys/types.h> // Used in GetHomeDirectory()
19#include <unistd.h> // Used in GetDataDirectory()
20#endif
21
22#ifdef __APPLE__
23#include <sys/param.h> // Used in GetBundleDirectory()
24
25// CFURL contains __attribute__ directives that gcc does not know how to parse, so we need to just
26// ignore them if we're not using clang. The macro is only used to prevent linking against
27// functions that don't exist on older versions of macOS, and the worst case scenario is a linker
28// error, so this is perfectly safe, just inconvenient.
29#ifndef __clang__
30#define availability(...)
31#endif
32#include <CoreFoundation/CFBundle.h> // Used in GetBundleDirectory()
33#include <CoreFoundation/CFString.h> // Used in GetBundleDirectory()
34#include <CoreFoundation/CFURL.h> // Used in GetBundleDirectory()
35#ifdef availability
36#undef availability
37#endif
38#endif
39
40#ifndef MAX_PATH
41#ifdef _WIN32
42// This is the maximum number of UTF-16 code units permissible in Windows file paths
43#define MAX_PATH 260
44#else
45// This is the maximum number of UTF-8 code units permissible in all other OSes' file paths
46#define MAX_PATH 1024
47#endif
48#endif
49
50namespace Common::FS {
51
52namespace fs = std::filesystem;
53
54/**
55 * The PathManagerImpl is a singleton allowing to manage the mapping of
56 * YuzuPath enums to real filesystem paths.
57 * This class provides 2 functions: GetYuzuPathImpl and SetYuzuPathImpl.
58 * These are used by GetYuzuPath and SetYuzuPath respectively to get or modify
59 * the path mapped by the YuzuPath enum.
60 */
61class PathManagerImpl {
62public:
63 static PathManagerImpl& GetInstance() {
64 static PathManagerImpl path_manager_impl;
65
66 return path_manager_impl;
67 }
68
69 PathManagerImpl(const PathManagerImpl&) = delete;
70 PathManagerImpl& operator=(const PathManagerImpl&) = delete;
71
72 PathManagerImpl(PathManagerImpl&&) = delete;
73 PathManagerImpl& operator=(PathManagerImpl&&) = delete;
74
75 [[nodiscard]] const fs::path& GetYuzuPathImpl(YuzuPath yuzu_path) {
76 return yuzu_paths.at(yuzu_path);
77 }
78
79 void SetYuzuPathImpl(YuzuPath yuzu_path, const fs::path& new_path) {
80 yuzu_paths.insert_or_assign(yuzu_path, new_path);
81 }
82
83private:
84 PathManagerImpl() {
85#ifdef _WIN32
86 auto yuzu_path = GetExeDirectory() / PORTABLE_DIR;
87
88 if (!IsDir(yuzu_path)) {
89 yuzu_path = GetAppDataRoamingDirectory() / YUZU_DIR;
90 }
91
92 GenerateYuzuPath(YuzuPath::YuzuDir, yuzu_path);
93 GenerateYuzuPath(YuzuPath::CacheDir, yuzu_path / CACHE_DIR);
94 GenerateYuzuPath(YuzuPath::ConfigDir, yuzu_path / CONFIG_DIR);
95#else
96 auto yuzu_path = GetCurrentDir() / PORTABLE_DIR;
97
98 if (Exists(yuzu_path) && IsDir(yuzu_path)) {
99 GenerateYuzuPath(YuzuPath::YuzuDir, yuzu_path);
100 GenerateYuzuPath(YuzuPath::CacheDir, yuzu_path / CACHE_DIR);
101 GenerateYuzuPath(YuzuPath::ConfigDir, yuzu_path / CONFIG_DIR);
102 } else {
103 yuzu_path = GetDataDirectory("XDG_DATA_HOME") / YUZU_DIR;
104
105 GenerateYuzuPath(YuzuPath::YuzuDir, yuzu_path);
106 GenerateYuzuPath(YuzuPath::CacheDir, GetDataDirectory("XDG_CACHE_HOME") / YUZU_DIR);
107 GenerateYuzuPath(YuzuPath::ConfigDir, GetDataDirectory("XDG_CONFIG_HOME") / YUZU_DIR);
108 }
109#endif
110
111 GenerateYuzuPath(YuzuPath::DumpDir, yuzu_path / DUMP_DIR);
112 GenerateYuzuPath(YuzuPath::KeysDir, yuzu_path / KEYS_DIR);
113 GenerateYuzuPath(YuzuPath::LoadDir, yuzu_path / LOAD_DIR);
114 GenerateYuzuPath(YuzuPath::LogDir, yuzu_path / LOG_DIR);
115 GenerateYuzuPath(YuzuPath::NANDDir, yuzu_path / NAND_DIR);
116 GenerateYuzuPath(YuzuPath::ScreenshotsDir, yuzu_path / SCREENSHOTS_DIR);
117 GenerateYuzuPath(YuzuPath::SDMCDir, yuzu_path / SDMC_DIR);
118 GenerateYuzuPath(YuzuPath::ShaderDir, yuzu_path / SHADER_DIR);
119 }
120
121 ~PathManagerImpl() = default;
122
123 void GenerateYuzuPath(YuzuPath yuzu_path, const fs::path& new_path) {
124 void(FS::CreateDir(new_path));
125
126 SetYuzuPathImpl(yuzu_path, new_path);
127 }
128
129 std::unordered_map<YuzuPath, fs::path> yuzu_paths;
130};
131
132std::string PathToUTF8String(const fs::path& path) {
133 const auto utf8_string = path.u8string();
134
135 return std::string{utf8_string.begin(), utf8_string.end()};
136}
137
138bool ValidatePath(const fs::path& path) {
139 if (path.empty()) {
140 LOG_ERROR(Common_Filesystem, "Input path is empty, path={}", PathToUTF8String(path));
141 return false;
142 }
143
144#ifdef _WIN32
145 if (path.u16string().size() >= MAX_PATH) {
146 LOG_ERROR(Common_Filesystem, "Input path is too long, path={}", PathToUTF8String(path));
147 return false;
148 }
149#else
150 if (path.u8string().size() >= MAX_PATH) {
151 LOG_ERROR(Common_Filesystem, "Input path is too long, path={}", PathToUTF8String(path));
152 return false;
153 }
154#endif
155
156 return true;
157}
158
159fs::path ConcatPath(const fs::path& first, const fs::path& second) {
160 const bool second_has_dir_sep = IsDirSeparator(second.u8string().front());
161
162 if (!second_has_dir_sep) {
163 return (first / second).lexically_normal();
164 }
165
166 fs::path concat_path = first;
167 concat_path += second;
168
169 return concat_path.lexically_normal();
170}
171
172fs::path ConcatPathSafe(const fs::path& base, const fs::path& offset) {
173 const auto concatenated_path = ConcatPath(base, offset);
174
175 if (!IsPathSandboxed(base, concatenated_path)) {
176 return base;
177 }
178
179 return concatenated_path;
180}
181
182bool IsPathSandboxed(const fs::path& base, const fs::path& path) {
183 const auto base_string = RemoveTrailingSeparators(base.lexically_normal()).u8string();
184 const auto path_string = RemoveTrailingSeparators(path.lexically_normal()).u8string();
185
186 if (path_string.size() < base_string.size()) {
187 return false;
188 }
189
190 return base_string.compare(0, base_string.size(), path_string, 0, base_string.size()) == 0;
191}
192
193bool IsDirSeparator(char character) {
194 return character == '/' || character == '\\';
195}
196
197bool IsDirSeparator(char8_t character) {
198 return character == u8'/' || character == u8'\\';
199}
200
201fs::path RemoveTrailingSeparators(const fs::path& path) {
202 if (path.empty()) {
203 return path;
204 }
205
206 auto string_path = path.u8string();
207
208 while (IsDirSeparator(string_path.back())) {
209 string_path.pop_back();
210 }
211
212 return fs::path{string_path};
213}
214
215const fs::path& GetYuzuPath(YuzuPath yuzu_path) {
216 return PathManagerImpl::GetInstance().GetYuzuPathImpl(yuzu_path);
217}
218
219std::string GetYuzuPathString(YuzuPath yuzu_path) {
220 return PathToUTF8String(GetYuzuPath(yuzu_path));
221}
222
223void SetYuzuPath(YuzuPath yuzu_path, const fs::path& new_path) {
224 if (!FS::IsDir(new_path)) {
225 LOG_ERROR(Common_Filesystem, "Filesystem object at new_path={} is not a directory",
226 PathToUTF8String(new_path));
227 return;
228 }
229
230 PathManagerImpl::GetInstance().SetYuzuPathImpl(yuzu_path, new_path);
231}
232
233#ifdef _WIN32
234
235fs::path GetExeDirectory() {
236 wchar_t exe_path[MAX_PATH];
237
238 GetModuleFileNameW(nullptr, exe_path, MAX_PATH);
239
240 if (!exe_path) {
241 LOG_ERROR(Common_Filesystem,
242 "Failed to get the path to the executable of the current process");
243 }
244
245 return fs::path{exe_path}.parent_path();
246}
247
248fs::path GetAppDataRoamingDirectory() {
249 PWSTR appdata_roaming_path = nullptr;
250
251 SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &appdata_roaming_path);
252
253 auto fs_appdata_roaming_path = fs::path{appdata_roaming_path};
254
255 CoTaskMemFree(appdata_roaming_path);
256
257 if (fs_appdata_roaming_path.empty()) {
258 LOG_ERROR(Common_Filesystem, "Failed to get the path to the %APPDATA% directory");
259 }
260
261 return fs_appdata_roaming_path;
262}
263
264#else
265
266fs::path GetHomeDirectory() {
267 const char* home_env_var = getenv("HOME");
268
269 if (home_env_var) {
270 return fs::path{home_env_var};
271 }
272
273 LOG_INFO(Common_Filesystem,
274 "$HOME is not defined in the environment variables, "
275 "attempting to query passwd to get the home path of the current user");
276
277 const auto* pw = getpwuid(getuid());
278
279 if (!pw) {
280 LOG_ERROR(Common_Filesystem, "Failed to get the home path of the current user");
281 return {};
282 }
283
284 return fs::path{pw->pw_dir};
285}
286
287fs::path GetDataDirectory(const std::string& env_name) {
288 const char* data_env_var = getenv(env_name.c_str());
289
290 if (data_env_var) {
291 return fs::path{data_env_var};
292 }
293
294 if (env_name == "XDG_DATA_HOME") {
295 return GetHomeDirectory() / ".local/share";
296 } else if (env_name == "XDG_CACHE_HOME") {
297 return GetHomeDirectory() / ".cache";
298 } else if (env_name == "XDG_CONFIG_HOME") {
299 return GetHomeDirectory() / ".config";
300 }
301
302 return {};
303}
304
305#endif
306
307#ifdef __APPLE__
308
309fs::path GetBundleDirectory() {
310 char app_bundle_path[MAXPATHLEN];
311
312 // Get the main bundle for the app
313 CFURLRef bundle_ref = CFBundleCopyBundleURL(CFBundleGetMainBundle());
314 CFStringRef bundle_path = CFURLCopyFileSystemPath(bundle_ref, kCFURLPOSIXPathStyle);
315
316 CFStringGetFileSystemRepresentation(bundle_path, app_bundle_path, sizeof(app_bundle_path));
317
318 CFRelease(bundle_ref);
319 CFRelease(bundle_path);
320
321 return fs::path{app_bundle_path};
322}
323
324#endif
325
326// vvvvvvvvvv Deprecated vvvvvvvvvv //
327
328std::string_view RemoveTrailingSlash(std::string_view path) {
329 if (path.empty()) {
330 return path;
331 }
332
333 if (path.back() == '\\' || path.back() == '/') {
334 path.remove_suffix(1);
335 return path;
336 }
337
338 return path;
339}
340
341std::vector<std::string> SplitPathComponents(std::string_view filename) {
342 std::string copy(filename);
343 std::replace(copy.begin(), copy.end(), '\\', '/');
344 std::vector<std::string> out;
345
346 std::stringstream stream(copy);
347 std::string item;
348 while (std::getline(stream, item, '/')) {
349 out.push_back(std::move(item));
350 }
351
352 return out;
353}
354
355std::string SanitizePath(std::string_view path_, DirectorySeparator directory_separator) {
356 std::string path(path_);
357 char type1 = directory_separator == DirectorySeparator::BackwardSlash ? '/' : '\\';
358 char type2 = directory_separator == DirectorySeparator::BackwardSlash ? '\\' : '/';
359
360 if (directory_separator == DirectorySeparator::PlatformDefault) {
361#ifdef _WIN32
362 type1 = '/';
363 type2 = '\\';
364#endif
365 }
366
367 std::replace(path.begin(), path.end(), type1, type2);
368
369 auto start = path.begin();
370#ifdef _WIN32
371 // allow network paths which start with a double backslash (e.g. \\server\share)
372 if (start != path.end())
373 ++start;
374#endif
375 path.erase(std::unique(start, path.end(),
376 [type2](char c1, char c2) { return c1 == type2 && c2 == type2; }),
377 path.end());
378 return std::string(RemoveTrailingSlash(path));
379}
380
381std::string_view GetParentPath(std::string_view path) {
382 const auto name_bck_index = path.rfind('\\');
383 const auto name_fwd_index = path.rfind('/');
384 std::size_t name_index;
385
386 if (name_bck_index == std::string_view::npos || name_fwd_index == std::string_view::npos) {
387 name_index = std::min(name_bck_index, name_fwd_index);
388 } else {
389 name_index = std::max(name_bck_index, name_fwd_index);
390 }
391
392 return path.substr(0, name_index);
393}
394
395std::string_view GetPathWithoutTop(std::string_view path) {
396 if (path.empty()) {
397 return path;
398 }
399
400 while (path[0] == '\\' || path[0] == '/') {
401 path.remove_prefix(1);
402 if (path.empty()) {
403 return path;
404 }
405 }
406
407 const auto name_bck_index = path.find('\\');
408 const auto name_fwd_index = path.find('/');
409 return path.substr(std::min(name_bck_index, name_fwd_index) + 1);
410}
411
412std::string_view GetFilename(std::string_view path) {
413 const auto name_index = path.find_last_of("\\/");
414
415 if (name_index == std::string_view::npos) {
416 return {};
417 }
418
419 return path.substr(name_index + 1);
420}
421
422std::string_view GetExtensionFromFilename(std::string_view name) {
423 const std::size_t index = name.rfind('.');
424
425 if (index == std::string_view::npos) {
426 return {};
427 }
428
429 return name.substr(index + 1);
430}
431
432} // namespace Common::FS