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