yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
platform_paths.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cstdlib>
5#include <vector>
6
7#include "absl/strings/str_cat.h"
8#include "absl/strings/str_replace.h"
9
10#ifdef _WIN32
11#include <shlobj.h>
12#include <windows.h>
13#else
14#include <pwd.h>
15#include <unistd.h>
16
17#include <climits> // For PATH_MAX
18#ifdef __APPLE__
19#include <TargetConditionals.h>
20#include <mach-o/dyld.h> // For _NSGetExecutablePath
21#endif
22#endif
23
24#if defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE
25#define YAZE_APPLE_MOBILE 1
26#endif
27
28namespace yaze {
29namespace util {
30
31std::filesystem::path PlatformPaths::GetHomeDirectory() {
32 try {
33#ifdef _WIN32
34 // Windows: Use USERPROFILE environment variable
35 const char* userprofile = std::getenv("USERPROFILE");
36 if (userprofile && *userprofile) {
37 return std::filesystem::path(userprofile);
38 }
39
40 // Fallback to HOMEDRIVE + HOMEPATH
41 const char* homedrive = std::getenv("HOMEDRIVE");
42 const char* homepath = std::getenv("HOMEPATH");
43 if (homedrive && homepath) {
44 return std::filesystem::path(std::string(homedrive) +
45 std::string(homepath));
46 }
47
48 // Last resort: use temp directory
49 std::error_code ec;
50 auto temp = std::filesystem::temp_directory_path(ec);
51 if (!ec) {
52 return temp;
53 }
54 return std::filesystem::path(".");
55#elif defined(__EMSCRIPTEN__)
56 // Emscripten: Use standard home
57 return std::filesystem::path("/home/web_user");
58#else
59 // Unix/macOS: Use HOME environment variable
60 const char* home = std::getenv("HOME");
61 if (home && *home) {
62 return std::filesystem::path(home);
63 }
64
65 // Fallback: try getpwuid
66 struct passwd* pw = getpwuid(getuid());
67 if (pw && pw->pw_dir) {
68 return std::filesystem::path(pw->pw_dir);
69 }
70
71 // Last resort: current directory (with error handling)
72 std::error_code ec;
73 auto cwd = std::filesystem::current_path(ec);
74 if (!ec) {
75 return cwd;
76 }
77 return std::filesystem::path(".");
78#endif
79 } catch (...) {
80 // If everything fails, return current directory placeholder
81 return std::filesystem::path(".");
82 }
83}
84
85absl::StatusOr<std::filesystem::path> PlatformPaths::GetAppDataDirectory() {
86 if (const char* override_dir = std::getenv("YAZE_APP_DATA_DIR")) {
87 if (*override_dir) {
88 std::filesystem::path app_data(override_dir);
89 auto status = EnsureDirectoryExists(app_data);
90 if (!status.ok()) {
91 return status;
92 }
93 return app_data;
94 }
95 }
96
97#if defined(YAZE_IOS) || defined(YAZE_APPLE_MOBILE)
98 std::filesystem::path home = GetHomeDirectory();
99 if (home.empty() || home == ".") {
100 std::error_code ec;
101 auto temp = std::filesystem::temp_directory_path(ec);
102 if (!ec) {
103 home = temp;
104 }
105 }
106
107 std::filesystem::path app_data =
108 home / "Library" / "Application Support" / ".yaze";
109 auto status = EnsureDirectoryExists(app_data);
110 if (!status.ok()) {
111 app_data = home / "Documents" / ".yaze";
112 status = EnsureDirectoryExists(app_data);
113 if (!status.ok()) {
114 return status;
115 }
116 }
117 return app_data;
118#elif defined(__EMSCRIPTEN__)
119 // Emscripten: Use /.yaze for app data (mounted IDBFS)
120 // Note: The directory structure in WASM is:
121 // /.yaze/roms - ROM files (IDBFS - persistent for session restore)
122 // /.yaze/saves - Save files (IDBFS - persistent)
123 // /.yaze/config - Configuration files (IDBFS - persistent)
124 // /.yaze/projects - Project files (IDBFS - persistent)
125 // /.yaze/prompts - Agent prompts (IDBFS - persistent)
126 // /.yaze/recent - Recent files metadata (IDBFS - persistent)
127 // /.yaze/temp - Temporary files (MEMFS - non-persistent)
128 // Directory creation is handled by FilesystemManager.ensureStandardDirectories()
129 // in src/web/core/filesystem_manager.js, which is called when the FS is ready.
130 std::filesystem::path app_data("/.yaze");
131 return app_data;
132#else
133 std::filesystem::path home = GetHomeDirectory();
134 std::filesystem::path preferred;
135 if (!home.empty() && home != ".") {
136 preferred = home / ".yaze";
137 }
138
139 std::vector<std::filesystem::path> legacy_paths;
140 auto add_legacy_path = [&](const std::filesystem::path& path) {
141 if (path.empty()) {
142 return;
143 }
144 if (std::find(legacy_paths.begin(), legacy_paths.end(), path) ==
145 legacy_paths.end()) {
146 legacy_paths.push_back(path);
147 }
148 };
149
150#ifdef _WIN32
151 wchar_t path[MAX_PATH];
152 if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, 0, path))) {
153 add_legacy_path(std::filesystem::path(path) / "yaze");
154 }
155 if (const char* appdata = std::getenv("APPDATA")) {
156 if (*appdata) {
157 add_legacy_path(std::filesystem::path(appdata) / "yaze");
158 }
159 }
160#elif defined(__APPLE__)
161 if (!home.empty() && home != ".") {
162 add_legacy_path(home / "Library" / "Application Support" / "yaze");
163 add_legacy_path(home / "Library" / "Application Support" / "Yaze");
164 }
165#else
166 if (!home.empty() && home != ".") {
167 add_legacy_path(home / ".config" / "yaze");
168 }
169#endif
170
171 auto try_migrate = [&](const std::filesystem::path& legacy) -> absl::Status {
172 if (legacy.empty() || preferred.empty()) {
173 return absl::OkStatus();
174 }
175 if (!Exists(legacy) || Exists(preferred)) {
176 return absl::OkStatus();
177 }
178
179 std::error_code ec;
180 std::filesystem::rename(legacy, preferred, ec);
181 if (!ec) {
182 return absl::OkStatus();
183 }
184
185 std::filesystem::create_directories(preferred, ec);
186 if (ec) {
187 return absl::InternalError(
188 absl::StrCat("Failed to create .yaze directory: ", ec.message()));
189 }
190
191 std::filesystem::copy(legacy, preferred,
192 std::filesystem::copy_options::recursive |
193 std::filesystem::copy_options::skip_existing,
194 ec);
195 if (ec) {
196 return absl::InternalError(
197 absl::StrCat("Failed to migrate legacy data: ", ec.message()));
198 }
199
200 // Clean up legacy directory after successful copy migration
201 // Use remove_all to handle non-empty directories; ignore errors
202 // since the migration succeeded and legacy data is now in preferred
203 std::filesystem::remove_all(legacy, ec);
204 // Intentionally ignore ec - cleanup failure is non-fatal
205
206 return absl::OkStatus();
207 };
208
209 if (!preferred.empty()) {
210 if (!Exists(preferred)) {
211 for (const auto& legacy : legacy_paths) {
212 if (Exists(legacy)) {
213 (void)try_migrate(legacy);
214 break;
215 }
216 }
217 }
218
219 auto status = EnsureDirectoryExists(preferred);
220 if (status.ok()) {
221 return preferred;
222 }
223 }
224
225 for (const auto& legacy : legacy_paths) {
226 auto status = EnsureDirectoryExists(legacy);
227 if (status.ok()) {
228 return legacy;
229 }
230 }
231
232 if (preferred.empty()) {
233 std::filesystem::path fallback = std::filesystem::current_path() / ".yaze";
234 auto status = EnsureDirectoryExists(fallback);
235 if (!status.ok()) {
236 return status;
237 }
238 return fallback;
239 }
240
241 return absl::InternalError("Failed to resolve app data directory");
242#endif
243}
244
245absl::StatusOr<std::filesystem::path> PlatformPaths::GetConfigDirectory() {
246 // For yaze, config and data directories are the same.
247 // This provides a semantically clearer API for config access.
248 return GetAppDataDirectory();
249}
250
251absl::StatusOr<std::filesystem::path> PlatformPaths::GetImGuiIniPath() {
252 auto config_dir = GetConfigDirectory();
253 if (!config_dir.ok()) {
254 return config_dir.status();
255 }
256 return *config_dir / "imgui.ini";
257}
258
259absl::StatusOr<std::filesystem::path>
261 if (const char* override_dir = std::getenv("YAZE_USER_DOCUMENTS_DIR")) {
262 if (*override_dir) {
263 std::filesystem::path docs_dir(override_dir);
264 auto status = EnsureDirectoryExists(docs_dir);
265 if (!status.ok()) {
266 return status;
267 }
268 return docs_dir;
269 }
270 }
271
272#if defined(YAZE_IOS) || defined(YAZE_APPLE_MOBILE)
273 std::filesystem::path home = GetHomeDirectory();
274 std::filesystem::path docs_dir = home / "Documents" / "Yaze";
275 auto status = EnsureDirectoryExists(docs_dir);
276 if (!status.ok()) {
277 docs_dir = home / "Yaze";
278 status = EnsureDirectoryExists(docs_dir);
279 if (!status.ok()) {
280 return status;
281 }
282 }
283 return docs_dir;
284#elif defined(_WIN32)
285 wchar_t path[MAX_PATH];
286 if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_PERSONAL, NULL, 0, path))) {
287 std::filesystem::path docs_dir = std::filesystem::path(path) / "Yaze";
288 auto status = EnsureDirectoryExists(docs_dir);
289 if (!status.ok()) {
290 return status;
291 }
292 return docs_dir;
293 }
294 // Fallback if SHGetFolderPathW fails
295 std::filesystem::path home = GetHomeDirectory();
296 std::filesystem::path docs_dir = home / "Documents" / "Yaze";
297 auto status = EnsureDirectoryExists(docs_dir);
298 if (!status.ok()) {
299 return status;
300 }
301 return docs_dir;
302#elif defined(__EMSCRIPTEN__)
303 // Emscripten: Use /.yaze/projects for user documents (mounted IDBFS)
304 std::filesystem::path docs_dir("/.yaze/projects");
305 return docs_dir;
306#else
307 // Unix/macOS: Use ~/Documents/Yaze
308 std::filesystem::path home = GetHomeDirectory();
309 std::filesystem::path docs_dir = home / "Documents" / "Yaze";
310 auto status = EnsureDirectoryExists(docs_dir);
311 if (!status.ok()) {
312 // If ~/Documents doesn't exist (e.g. headless servers), fallback to home
313 docs_dir = home / "Yaze";
314 status = EnsureDirectoryExists(docs_dir);
315 if (!status.ok()) {
316 return status;
317 }
318 }
319 return docs_dir;
320#endif
321}
322
323absl::StatusOr<std::filesystem::path>
325 auto docs_result = GetUserDocumentsDirectory();
326 if (!docs_result.ok()) {
327 return docs_result.status();
328 }
329
330 std::filesystem::path subdir_path = *docs_result / subdir;
331 auto status = EnsureDirectoryExists(subdir_path);
332 if (!status.ok()) {
333 return status;
334 }
335 return subdir_path;
336}
337
338absl::StatusOr<std::filesystem::path> PlatformPaths::GetAppDataSubdirectory(
339 const std::string& subdir) {
340 auto app_data_result = GetAppDataDirectory();
341 if (!app_data_result.ok()) {
342 return app_data_result.status();
343 }
344
345 std::filesystem::path subdir_path = *app_data_result / subdir;
346
347 auto status = EnsureDirectoryExists(subdir_path);
348 if (!status.ok()) {
349 return status;
350 }
351
352 return subdir_path;
353}
354
356 const std::filesystem::path& path) {
357 if (Exists(path)) {
358 return absl::OkStatus();
359 }
360
361 std::error_code ec;
362 if (!std::filesystem::create_directories(path, ec)) {
363 if (ec) {
364 return absl::InternalError(
365 absl::StrCat("Failed to create directory: ", path.string(),
366 " - Error: ", ec.message()));
367 }
368 }
369
370 return absl::OkStatus();
371}
372
373bool PlatformPaths::Exists(const std::filesystem::path& path) {
374 std::error_code ec;
375 bool exists = std::filesystem::exists(path, ec);
376 return exists && !ec;
377}
378
379absl::StatusOr<std::filesystem::path> PlatformPaths::GetTempDirectory() {
380#ifdef __EMSCRIPTEN__
381 // Emscripten: Use /.yaze/temp (mounted MEMFS - non-persistent)
382 return std::filesystem::path("/.yaze/temp");
383#else
384 std::error_code ec;
385 std::filesystem::path temp_base = std::filesystem::temp_directory_path(ec);
386
387 if (ec) {
388 return absl::InternalError(
389 absl::StrCat("Failed to get temp directory: ", ec.message()));
390 }
391
392 std::filesystem::path yaze_temp = temp_base / "yaze";
393
394 auto status = EnsureDirectoryExists(yaze_temp);
395 if (!status.ok()) {
396 return status;
397 }
398
399 return yaze_temp;
400#endif
401}
402
404 const std::filesystem::path& path) {
405 // Convert to string and replace backslashes with forward slashes
406 // Forward slashes work on all platforms for display purposes
407 std::string path_str = path.string();
408 return absl::StrReplaceAll(path_str, {{"\\", "/"}});
409}
410
411std::string PlatformPaths::ToNativePath(const std::filesystem::path& path) {
412 // std::filesystem::path::string() already returns the native format
413 return path.string();
414}
415
416absl::StatusOr<std::filesystem::path> PlatformPaths::FindAsset(
417 const std::string& relative_path) {
418 std::vector<std::filesystem::path> search_paths;
419
420 try {
421 // 1. Check compile-time YAZE_ASSETS_PATH if defined
422#ifdef YAZE_ASSETS_PATH
423 try {
424 search_paths.push_back(std::filesystem::path(YAZE_ASSETS_PATH) /
425 relative_path);
426 } catch (...) {
427 // Skip if path construction fails
428 }
429#endif
430
431 // 2. Current working directory + assets/ (cached to avoid repeated calls)
432 static std::filesystem::path cached_cwd;
433 static bool cwd_cached = false;
434
435 if (!cwd_cached) {
436 std::error_code ec;
437 cached_cwd = std::filesystem::current_path(ec);
438 cwd_cached = true; // Only try once to avoid repeated slow calls
439 }
440
441 if (!cached_cwd.empty()) {
442 try {
443 search_paths.push_back(cached_cwd / "assets" / relative_path);
444 } catch (...) {
445 // Skip if path construction fails
446 }
447 }
448
449 // 3. Executable directory + assets/ (cached to avoid repeated OS calls)
450 static std::filesystem::path cached_exe_dir;
451 static bool exe_dir_cached = false;
452
453 if (!exe_dir_cached) {
454 try {
455#ifdef __APPLE__
456 char exe_path[PATH_MAX];
457 uint32_t size = sizeof(exe_path);
458 if (_NSGetExecutablePath(exe_path, &size) == 0) {
459 cached_exe_dir = std::filesystem::path(exe_path).parent_path();
460 }
461#elif defined(__linux__)
462 char exe_path[PATH_MAX];
463 ssize_t len =
464 readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1);
465 if (len != -1) {
466 exe_path[len] = '\0';
467 cached_exe_dir = std::filesystem::path(exe_path).parent_path();
468 }
469#elif defined(_WIN32)
470 wchar_t exe_path[MAX_PATH];
471 if (GetModuleFileNameW(NULL, exe_path, MAX_PATH) != 0) {
472 cached_exe_dir = std::filesystem::path(exe_path).parent_path();
473 }
474#endif
475 } catch (...) {
476 // Skip if exe path detection fails
477 }
478 exe_dir_cached = true; // Only try once
479 }
480
481 if (!cached_exe_dir.empty()) {
482 try {
483 search_paths.push_back(cached_exe_dir / "assets" / relative_path);
484 // Also check parent (for build/bin/yaze case)
485 search_paths.push_back(cached_exe_dir.parent_path() / "assets" /
486 relative_path);
487#if defined(__linux__)
488 // Installed package and extracted FHS archive:
489 // <root>/usr/bin/yaze -> <root>/usr/share/yaze/assets
490 search_paths.push_back(cached_exe_dir.parent_path() / "share" / "yaze" /
491 "assets" / relative_path);
492#endif
493#ifdef __APPLE__
494 // macOS app bundle: exe is at yaze.app/Contents/MacOS/yaze
495 // Assets may be at yaze.app/Contents/Resources/assets/ (inside bundle)
496 // or at ../../../assets/ (same level as .app bundle in DMG)
497 auto contents_dir = cached_exe_dir.parent_path(); // Contents/
498 auto bundle_dir = contents_dir.parent_path(); // yaze.app/
499 auto bundle_parent = bundle_dir.parent_path(); // DMG root
500 search_paths.push_back(contents_dir / "Resources" / "assets" /
501 relative_path);
502 search_paths.push_back(bundle_parent / "assets" / relative_path);
503#endif
504 } catch (...) {
505 // Skip if path construction fails
506 }
507 }
508
509 // 4. Parent directories (for running from build subdirectories)
510 if (!cached_cwd.empty()) {
511 try {
512 auto parent = cached_cwd.parent_path();
513 if (!parent.empty() && parent != cached_cwd) {
514 search_paths.push_back(parent / "assets" / relative_path);
515 auto grandparent = parent.parent_path();
516 if (!grandparent.empty() && grandparent != parent) {
517 search_paths.push_back(grandparent / "assets" / relative_path);
518 }
519 }
520 } catch (...) {
521 // Skip if path operations fail
522 }
523 }
524
525 // 5. User directory ~/.yaze/assets/ (cached home directory)
526 static std::filesystem::path cached_home;
527 static bool home_cached = false;
528
529 if (!home_cached) {
530 try {
531 cached_home = GetHomeDirectory();
532 } catch (...) {
533 // Skip if home lookup fails
534 }
535 home_cached = true; // Only try once
536 }
537
538 if (!cached_home.empty() && cached_home != ".") {
539 try {
540 search_paths.push_back(cached_home / ".yaze" / "assets" /
541 relative_path);
542 } catch (...) {
543 // Skip if path construction fails
544 }
545 }
546
547 // 6. System-wide installation (Unix only)
548#ifndef _WIN32
549 try {
550 search_paths.push_back(
551 std::filesystem::path("/usr/local/share/yaze/assets") /
552 relative_path);
553 search_paths.push_back(std::filesystem::path("/usr/share/yaze/assets") /
554 relative_path);
555 } catch (...) {
556 // Skip if path construction fails
557 }
558#endif
559
560 // Search all paths and return the first one that exists
561 // Limit search to prevent infinite loops on weird filesystems
562 const size_t max_paths_to_check = 20;
563 size_t checked = 0;
564
565 for (const auto& candidate : search_paths) {
566 if (++checked > max_paths_to_check) {
567 break; // Safety limit
568 }
569
570 try {
571 // Use std::filesystem::exists with error code to avoid exceptions
572 std::error_code exists_ec;
573 if (std::filesystem::exists(candidate, exists_ec) && !exists_ec) {
574 // Double-check it's a regular file or directory, not a broken symlink
575 auto status = std::filesystem::status(candidate, exists_ec);
576 if (!exists_ec &&
577 status.type() != std::filesystem::file_type::not_found) {
578 return candidate;
579 }
580 }
581 } catch (...) {
582 // Skip this candidate if checking fails
583 continue;
584 }
585 }
586
587 // If not found, return a simple error
588 return absl::NotFoundError(
589 absl::StrCat("Asset not found: ", relative_path));
590
591 } catch (const std::exception& e) {
592 return absl::InternalError(
593 absl::StrCat("Exception while searching for asset: ", e.what()));
594 } catch (...) {
595 return absl::InternalError("Unknown exception while searching for asset");
596 }
597}
598
599} // namespace util
600} // namespace yaze
static absl::StatusOr< std::filesystem::path > GetImGuiIniPath()
Get the ImGui ini path for YAZE.
static absl::StatusOr< std::filesystem::path > GetTempDirectory()
Get a temporary directory for the application.
static absl::StatusOr< std::filesystem::path > GetAppDataDirectory()
Get the user-specific application data directory for YAZE.
static absl::StatusOr< std::filesystem::path > GetAppDataSubdirectory(const std::string &subdir)
Get a subdirectory within the app data folder.
static std::string ToNativePath(const std::filesystem::path &path)
Convert path to native format.
static absl::StatusOr< std::filesystem::path > GetConfigDirectory()
Get the user-specific configuration directory for YAZE.
static absl::StatusOr< std::filesystem::path > GetUserDocumentsSubdirectory(const std::string &subdir)
Get a subdirectory within the user documents folder.
static absl::StatusOr< std::filesystem::path > GetUserDocumentsDirectory()
Get the user's Documents directory.
static absl::StatusOr< std::filesystem::path > FindAsset(const std::string &relative_path)
Find an asset file in multiple standard locations.
static absl::Status EnsureDirectoryExists(const std::filesystem::path &path)
Ensure a directory exists, creating it if necessary.
static std::string NormalizePathForDisplay(const std::filesystem::path &path)
Normalize path separators for display.
static bool Exists(const std::filesystem::path &path)
Check if a file or directory exists.
static std::filesystem::path GetHomeDirectory()
Get the user's home directory in a cross-platform way.