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