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 defined(YAZE_IOS) || defined(YAZE_APPLE_MOBILE)
262 std::filesystem::path home = GetHomeDirectory();
263 std::filesystem::path docs_dir = home / "Documents" / "Yaze";
264 auto status = EnsureDirectoryExists(docs_dir);
265 if (!status.ok()) {
266 docs_dir = home / "Yaze";
267 status = EnsureDirectoryExists(docs_dir);
268 if (!status.ok()) {
269 return status;
270 }
271 }
272 return docs_dir;
273#elif defined(_WIN32)
274 wchar_t path[MAX_PATH];
275 if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_PERSONAL, NULL, 0, path))) {
276 std::filesystem::path docs_dir = std::filesystem::path(path) / "Yaze";
277 auto status = EnsureDirectoryExists(docs_dir);
278 if (!status.ok()) {
279 return status;
280 }
281 return docs_dir;
282 }
283 // Fallback if SHGetFolderPathW fails
284 std::filesystem::path home = GetHomeDirectory();
285 std::filesystem::path docs_dir = home / "Documents" / "Yaze";
286 auto status = EnsureDirectoryExists(docs_dir);
287 if (!status.ok()) {
288 return status;
289 }
290 return docs_dir;
291#elif defined(__EMSCRIPTEN__)
292 // Emscripten: Use /.yaze/projects for user documents (mounted IDBFS)
293 std::filesystem::path docs_dir("/.yaze/projects");
294 return docs_dir;
295#else
296 // Unix/macOS: Use ~/Documents/Yaze
297 std::filesystem::path home = GetHomeDirectory();
298 std::filesystem::path docs_dir = home / "Documents" / "Yaze";
299 auto status = EnsureDirectoryExists(docs_dir);
300 if (!status.ok()) {
301 // If ~/Documents doesn't exist (e.g. headless servers), fallback to home
302 docs_dir = home / "Yaze";
303 status = EnsureDirectoryExists(docs_dir);
304 if (!status.ok()) {
305 return status;
306 }
307 }
308 return docs_dir;
309#endif
310}
311
312absl::StatusOr<std::filesystem::path>
314 auto docs_result = GetUserDocumentsDirectory();
315 if (!docs_result.ok()) {
316 return docs_result.status();
317 }
318
319 std::filesystem::path subdir_path = *docs_result / subdir;
320 auto status = EnsureDirectoryExists(subdir_path);
321 if (!status.ok()) {
322 return status;
323 }
324 return subdir_path;
325}
326
327absl::StatusOr<std::filesystem::path> PlatformPaths::GetAppDataSubdirectory(
328 const std::string& subdir) {
329 auto app_data_result = GetAppDataDirectory();
330 if (!app_data_result.ok()) {
331 return app_data_result.status();
332 }
333
334 std::filesystem::path subdir_path = *app_data_result / subdir;
335
336 auto status = EnsureDirectoryExists(subdir_path);
337 if (!status.ok()) {
338 return status;
339 }
340
341 return subdir_path;
342}
343
345 const std::filesystem::path& path) {
346 if (Exists(path)) {
347 return absl::OkStatus();
348 }
349
350 std::error_code ec;
351 if (!std::filesystem::create_directories(path, ec)) {
352 if (ec) {
353 return absl::InternalError(
354 absl::StrCat("Failed to create directory: ", path.string(),
355 " - Error: ", ec.message()));
356 }
357 }
358
359 return absl::OkStatus();
360}
361
362bool PlatformPaths::Exists(const std::filesystem::path& path) {
363 std::error_code ec;
364 bool exists = std::filesystem::exists(path, ec);
365 return exists && !ec;
366}
367
368absl::StatusOr<std::filesystem::path> PlatformPaths::GetTempDirectory() {
369#ifdef __EMSCRIPTEN__
370 // Emscripten: Use /.yaze/temp (mounted MEMFS - non-persistent)
371 return std::filesystem::path("/.yaze/temp");
372#else
373 std::error_code ec;
374 std::filesystem::path temp_base = std::filesystem::temp_directory_path(ec);
375
376 if (ec) {
377 return absl::InternalError(
378 absl::StrCat("Failed to get temp directory: ", ec.message()));
379 }
380
381 std::filesystem::path yaze_temp = temp_base / "yaze";
382
383 auto status = EnsureDirectoryExists(yaze_temp);
384 if (!status.ok()) {
385 return status;
386 }
387
388 return yaze_temp;
389#endif
390}
391
393 const std::filesystem::path& path) {
394 // Convert to string and replace backslashes with forward slashes
395 // Forward slashes work on all platforms for display purposes
396 std::string path_str = path.string();
397 return absl::StrReplaceAll(path_str, {{"\\", "/"}});
398}
399
400std::string PlatformPaths::ToNativePath(const std::filesystem::path& path) {
401 // std::filesystem::path::string() already returns the native format
402 return path.string();
403}
404
405absl::StatusOr<std::filesystem::path> PlatformPaths::FindAsset(
406 const std::string& relative_path) {
407 std::vector<std::filesystem::path> search_paths;
408
409 try {
410 // 1. Check compile-time YAZE_ASSETS_PATH if defined
411#ifdef YAZE_ASSETS_PATH
412 try {
413 search_paths.push_back(std::filesystem::path(YAZE_ASSETS_PATH) /
414 relative_path);
415 } catch (...) {
416 // Skip if path construction fails
417 }
418#endif
419
420 // 2. Current working directory + assets/ (cached to avoid repeated calls)
421 static std::filesystem::path cached_cwd;
422 static bool cwd_cached = false;
423
424 if (!cwd_cached) {
425 std::error_code ec;
426 cached_cwd = std::filesystem::current_path(ec);
427 cwd_cached = true; // Only try once to avoid repeated slow calls
428 }
429
430 if (!cached_cwd.empty()) {
431 try {
432 search_paths.push_back(cached_cwd / "assets" / relative_path);
433 } catch (...) {
434 // Skip if path construction fails
435 }
436 }
437
438 // 3. Executable directory + assets/ (cached to avoid repeated OS calls)
439 static std::filesystem::path cached_exe_dir;
440 static bool exe_dir_cached = false;
441
442 if (!exe_dir_cached) {
443 try {
444#ifdef __APPLE__
445 char exe_path[PATH_MAX];
446 uint32_t size = sizeof(exe_path);
447 if (_NSGetExecutablePath(exe_path, &size) == 0) {
448 cached_exe_dir = std::filesystem::path(exe_path).parent_path();
449 }
450#elif defined(__linux__)
451 char exe_path[PATH_MAX];
452 ssize_t len =
453 readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1);
454 if (len != -1) {
455 exe_path[len] = '\0';
456 cached_exe_dir = std::filesystem::path(exe_path).parent_path();
457 }
458#elif defined(_WIN32)
459 wchar_t exe_path[MAX_PATH];
460 if (GetModuleFileNameW(NULL, exe_path, MAX_PATH) != 0) {
461 cached_exe_dir = std::filesystem::path(exe_path).parent_path();
462 }
463#endif
464 } catch (...) {
465 // Skip if exe path detection fails
466 }
467 exe_dir_cached = true; // Only try once
468 }
469
470 if (!cached_exe_dir.empty()) {
471 try {
472 search_paths.push_back(cached_exe_dir / "assets" / relative_path);
473 // Also check parent (for build/bin/yaze case)
474 search_paths.push_back(cached_exe_dir.parent_path() / "assets" /
475 relative_path);
476#ifdef __APPLE__
477 // macOS app bundle: exe is at yaze.app/Contents/MacOS/yaze
478 // Assets may be at yaze.app/Contents/Resources/assets/ (inside bundle)
479 // or at ../../../assets/ (same level as .app bundle in DMG)
480 auto contents_dir = cached_exe_dir.parent_path(); // Contents/
481 auto bundle_dir = contents_dir.parent_path(); // yaze.app/
482 auto bundle_parent = bundle_dir.parent_path(); // DMG root
483 search_paths.push_back(contents_dir / "Resources" / "assets" /
484 relative_path);
485 search_paths.push_back(bundle_parent / "assets" / relative_path);
486#endif
487 } catch (...) {
488 // Skip if path construction fails
489 }
490 }
491
492 // 4. Parent directories (for running from build subdirectories)
493 if (!cached_cwd.empty()) {
494 try {
495 auto parent = cached_cwd.parent_path();
496 if (!parent.empty() && parent != cached_cwd) {
497 search_paths.push_back(parent / "assets" / relative_path);
498 auto grandparent = parent.parent_path();
499 if (!grandparent.empty() && grandparent != parent) {
500 search_paths.push_back(grandparent / "assets" / relative_path);
501 }
502 }
503 } catch (...) {
504 // Skip if path operations fail
505 }
506 }
507
508 // 5. User directory ~/.yaze/assets/ (cached home directory)
509 static std::filesystem::path cached_home;
510 static bool home_cached = false;
511
512 if (!home_cached) {
513 try {
514 cached_home = GetHomeDirectory();
515 } catch (...) {
516 // Skip if home lookup fails
517 }
518 home_cached = true; // Only try once
519 }
520
521 if (!cached_home.empty() && cached_home != ".") {
522 try {
523 search_paths.push_back(cached_home / ".yaze" / "assets" /
524 relative_path);
525 } catch (...) {
526 // Skip if path construction fails
527 }
528 }
529
530 // 6. System-wide installation (Unix only)
531#ifndef _WIN32
532 try {
533 search_paths.push_back(
534 std::filesystem::path("/usr/local/share/yaze/assets") /
535 relative_path);
536 search_paths.push_back(std::filesystem::path("/usr/share/yaze/assets") /
537 relative_path);
538 } catch (...) {
539 // Skip if path construction fails
540 }
541#endif
542
543 // Search all paths and return the first one that exists
544 // Limit search to prevent infinite loops on weird filesystems
545 const size_t max_paths_to_check = 20;
546 size_t checked = 0;
547
548 for (const auto& candidate : search_paths) {
549 if (++checked > max_paths_to_check) {
550 break; // Safety limit
551 }
552
553 try {
554 // Use std::filesystem::exists with error code to avoid exceptions
555 std::error_code exists_ec;
556 if (std::filesystem::exists(candidate, exists_ec) && !exists_ec) {
557 // Double-check it's a regular file or directory, not a broken symlink
558 auto status = std::filesystem::status(candidate, exists_ec);
559 if (!exists_ec &&
560 status.type() != std::filesystem::file_type::not_found) {
561 return candidate;
562 }
563 }
564 } catch (...) {
565 // Skip this candidate if checking fails
566 continue;
567 }
568 }
569
570 // If not found, return a simple error
571 return absl::NotFoundError(
572 absl::StrCat("Asset not found: ", relative_path));
573
574 } catch (const std::exception& e) {
575 return absl::InternalError(
576 absl::StrCat("Exception while searching for asset: ", e.what()));
577 } catch (...) {
578 return absl::InternalError("Unknown exception while searching for asset");
579 }
580}
581
582} // namespace util
583} // 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.