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 <cstdlib>
4
5#include "absl/strings/str_cat.h"
6#include "absl/strings/str_replace.h"
7
8#ifdef _WIN32
9#include <shlobj.h>
10#include <windows.h>
11#else
12#include <pwd.h>
13#include <unistd.h>
14
15#include <climits> // For PATH_MAX
16#ifdef __APPLE__
17#include <TargetConditionals.h>
18#include <mach-o/dyld.h> // For _NSGetExecutablePath
19#endif
20#endif
21
22#if defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE
23#define YAZE_APPLE_MOBILE 1
24#endif
25
26namespace yaze {
27namespace util {
28
29std::filesystem::path PlatformPaths::GetHomeDirectory() {
30 try {
31#ifdef _WIN32
32 // Windows: Use USERPROFILE environment variable
33 const char* userprofile = std::getenv("USERPROFILE");
34 if (userprofile && *userprofile) {
35 return std::filesystem::path(userprofile);
36 }
37
38 // Fallback to HOMEDRIVE + HOMEPATH
39 const char* homedrive = std::getenv("HOMEDRIVE");
40 const char* homepath = std::getenv("HOMEPATH");
41 if (homedrive && homepath) {
42 return std::filesystem::path(std::string(homedrive) +
43 std::string(homepath));
44 }
45
46 // Last resort: use temp directory
47 std::error_code ec;
48 auto temp = std::filesystem::temp_directory_path(ec);
49 if (!ec) {
50 return temp;
51 }
52 return std::filesystem::path(".");
53#elif defined(__EMSCRIPTEN__)
54 // Emscripten: Use standard home
55 return std::filesystem::path("/home/web_user");
56#else
57 // Unix/macOS: Use HOME environment variable
58 const char* home = std::getenv("HOME");
59 if (home && *home) {
60 return std::filesystem::path(home);
61 }
62
63 // Fallback: try getpwuid
64 struct passwd* pw = getpwuid(getuid());
65 if (pw && pw->pw_dir) {
66 return std::filesystem::path(pw->pw_dir);
67 }
68
69 // Last resort: current directory (with error handling)
70 std::error_code ec;
71 auto cwd = std::filesystem::current_path(ec);
72 if (!ec) {
73 return cwd;
74 }
75 return std::filesystem::path(".");
76#endif
77 } catch (...) {
78 // If everything fails, return current directory placeholder
79 return std::filesystem::path(".");
80 }
81}
82
83absl::StatusOr<std::filesystem::path> PlatformPaths::GetAppDataDirectory() {
84#if defined(YAZE_IOS) || defined(YAZE_APPLE_MOBILE)
85 std::filesystem::path home = GetHomeDirectory();
86 if (home.empty() || home == ".") {
87 std::error_code ec;
88 auto temp = std::filesystem::temp_directory_path(ec);
89 if (!ec) {
90 home = temp;
91 }
92 }
93
94 std::filesystem::path app_data =
95 home / "Library" / "Application Support" / "yaze";
96 auto status = EnsureDirectoryExists(app_data);
97 if (!status.ok()) {
98 app_data = home / "Documents" / "yaze";
99 status = EnsureDirectoryExists(app_data);
100 if (!status.ok()) {
101 return status;
102 }
103 }
104 return app_data;
105#elif defined(_WIN32)
106 wchar_t path[MAX_PATH];
107 if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, 0, path))) {
108 std::filesystem::path app_data = std::filesystem::path(path) / "yaze";
109 auto status = EnsureDirectoryExists(app_data);
110 if (!status.ok()) {
111 return status;
112 }
113 return app_data;
114 }
115 // Fallback if SHGetFolderPathW fails
116 std::filesystem::path home = GetHomeDirectory();
117 std::filesystem::path app_data = home / "yaze_data";
118 auto status = EnsureDirectoryExists(app_data);
119 if (!status.ok()) {
120 return status;
121 }
122 return app_data;
123#elif defined(__EMSCRIPTEN__)
124 // Emscripten: Use /config for app data (mounted IDBFS)
125 // Note: The directory structure in WASM is:
126 // /config - Configuration files (IDBFS - persistent)
127 // /saves - Save files (IDBFS - persistent)
128 // /projects - Project files (IDBFS - persistent)
129 // /prompts - Agent prompts (IDBFS - persistent)
130 // /roms - ROM files (IDBFS - persistent for session restore)
131 // /recent - Recent files metadata (IDBFS - persistent)
132 // /temp - Temporary files (MEMFS - non-persistent)
133 std::filesystem::path app_data("/config");
134 // We assume the mount point exists or will be created by initialization
135 return app_data;
136#else
137 // Unix/macOS: Use ~/.yaze for simplicity and consistency
138 // This is simpler than XDG or Application Support for a dev tool
139 std::filesystem::path home = GetHomeDirectory();
140 std::filesystem::path app_data = home / ".yaze";
141 auto status = EnsureDirectoryExists(app_data);
142 if (!status.ok()) {
143 return status;
144 }
145 return app_data;
146#endif
147}
148
149absl::StatusOr<std::filesystem::path> PlatformPaths::GetConfigDirectory() {
150 // For yaze, config and data directories are the same.
151 // This provides a semantically clearer API for config access.
152 return GetAppDataDirectory();
153}
154
155absl::StatusOr<std::filesystem::path> PlatformPaths::GetUserDocumentsDirectory() {
156#if defined(YAZE_IOS) || defined(YAZE_APPLE_MOBILE)
157 std::filesystem::path home = GetHomeDirectory();
158 std::filesystem::path docs_dir = home / "Documents" / "Yaze";
159 auto status = EnsureDirectoryExists(docs_dir);
160 if (!status.ok()) {
161 docs_dir = home / "Yaze";
162 status = EnsureDirectoryExists(docs_dir);
163 if (!status.ok()) {
164 return status;
165 }
166 }
167 return docs_dir;
168#elif defined(_WIN32)
169 wchar_t path[MAX_PATH];
170 if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_PERSONAL, NULL, 0, path))) {
171 std::filesystem::path docs_dir = std::filesystem::path(path) / "Yaze";
172 auto status = EnsureDirectoryExists(docs_dir);
173 if (!status.ok()) {
174 return status;
175 }
176 return docs_dir;
177 }
178 // Fallback if SHGetFolderPathW fails
179 std::filesystem::path home = GetHomeDirectory();
180 std::filesystem::path docs_dir = home / "Documents" / "Yaze";
181 auto status = EnsureDirectoryExists(docs_dir);
182 if (!status.ok()) {
183 return status;
184 }
185 return docs_dir;
186#elif defined(__EMSCRIPTEN__)
187 // Emscripten: Use /projects for user documents (mounted IDBFS)
188 std::filesystem::path docs_dir("/projects");
189 return docs_dir;
190#else
191 // Unix/macOS: Use ~/Documents/Yaze
192 std::filesystem::path home = GetHomeDirectory();
193 std::filesystem::path docs_dir = home / "Documents" / "Yaze";
194 auto status = EnsureDirectoryExists(docs_dir);
195 if (!status.ok()) {
196 // If ~/Documents doesn't exist (e.g. headless servers), fallback to home
197 docs_dir = home / "Yaze";
198 status = EnsureDirectoryExists(docs_dir);
199 if (!status.ok()) {
200 return status;
201 }
202 }
203 return docs_dir;
204#endif
205}
206
207absl::StatusOr<std::filesystem::path> PlatformPaths::GetUserDocumentsSubdirectory(
208 const std::string& subdir) {
209 auto docs_result = GetUserDocumentsDirectory();
210 if (!docs_result.ok()) {
211 return docs_result.status();
212 }
213
214 std::filesystem::path subdir_path = *docs_result / subdir;
215 auto status = EnsureDirectoryExists(subdir_path);
216 if (!status.ok()) {
217 return status;
218 }
219 return subdir_path;
220}
221
222absl::StatusOr<std::filesystem::path> PlatformPaths::GetAppDataSubdirectory(
223 const std::string& subdir) {
224 auto app_data_result = GetAppDataDirectory();
225 if (!app_data_result.ok()) {
226 return app_data_result.status();
227 }
228
229 std::filesystem::path subdir_path = *app_data_result / subdir;
230
231 auto status = EnsureDirectoryExists(subdir_path);
232 if (!status.ok()) {
233 return status;
234 }
235
236 return subdir_path;
237}
238
240 const std::filesystem::path& path) {
241 if (Exists(path)) {
242 return absl::OkStatus();
243 }
244
245 std::error_code ec;
246 if (!std::filesystem::create_directories(path, ec)) {
247 if (ec) {
248 return absl::InternalError(
249 absl::StrCat("Failed to create directory: ", path.string(),
250 " - Error: ", ec.message()));
251 }
252 }
253
254 return absl::OkStatus();
255}
256
257bool PlatformPaths::Exists(const std::filesystem::path& path) {
258 std::error_code ec;
259 bool exists = std::filesystem::exists(path, ec);
260 return exists && !ec;
261}
262
263absl::StatusOr<std::filesystem::path> PlatformPaths::GetTempDirectory() {
264#ifdef __EMSCRIPTEN__
265 // Emscripten: Use /temp (mounted MEMFS - non-persistent)
266 return std::filesystem::path("/temp");
267#else
268 std::error_code ec;
269 std::filesystem::path temp_base = std::filesystem::temp_directory_path(ec);
270
271 if (ec) {
272 return absl::InternalError(
273 absl::StrCat("Failed to get temp directory: ", ec.message()));
274 }
275
276 std::filesystem::path yaze_temp = temp_base / "yaze";
277
278 auto status = EnsureDirectoryExists(yaze_temp);
279 if (!status.ok()) {
280 return status;
281 }
282
283 return yaze_temp;
284#endif
285}
286
288 const std::filesystem::path& path) {
289 // Convert to string and replace backslashes with forward slashes
290 // Forward slashes work on all platforms for display purposes
291 std::string path_str = path.string();
292 return absl::StrReplaceAll(path_str, {{"\\", "/"}});
293}
294
295std::string PlatformPaths::ToNativePath(const std::filesystem::path& path) {
296 // std::filesystem::path::string() already returns the native format
297 return path.string();
298}
299
300absl::StatusOr<std::filesystem::path> PlatformPaths::FindAsset(
301 const std::string& relative_path) {
302 std::vector<std::filesystem::path> search_paths;
303
304 try {
305 // 1. Check compile-time YAZE_ASSETS_PATH if defined
306#ifdef YAZE_ASSETS_PATH
307 try {
308 search_paths.push_back(std::filesystem::path(YAZE_ASSETS_PATH) /
309 relative_path);
310 } catch (...) {
311 // Skip if path construction fails
312 }
313#endif
314
315 // 2. Current working directory + assets/ (cached to avoid repeated calls)
316 static std::filesystem::path cached_cwd;
317 static bool cwd_cached = false;
318
319 if (!cwd_cached) {
320 std::error_code ec;
321 cached_cwd = std::filesystem::current_path(ec);
322 cwd_cached = true; // Only try once to avoid repeated slow calls
323 }
324
325 if (!cached_cwd.empty()) {
326 try {
327 search_paths.push_back(cached_cwd / "assets" / relative_path);
328 } catch (...) {
329 // Skip if path construction fails
330 }
331 }
332
333 // 3. Executable directory + assets/ (cached to avoid repeated OS calls)
334 static std::filesystem::path cached_exe_dir;
335 static bool exe_dir_cached = false;
336
337 if (!exe_dir_cached) {
338 try {
339#ifdef __APPLE__
340 char exe_path[PATH_MAX];
341 uint32_t size = sizeof(exe_path);
342 if (_NSGetExecutablePath(exe_path, &size) == 0) {
343 cached_exe_dir = std::filesystem::path(exe_path).parent_path();
344 }
345#elif defined(__linux__)
346 char exe_path[PATH_MAX];
347 ssize_t len =
348 readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1);
349 if (len != -1) {
350 exe_path[len] = '\0';
351 cached_exe_dir = std::filesystem::path(exe_path).parent_path();
352 }
353#elif defined(_WIN32)
354 wchar_t exe_path[MAX_PATH];
355 if (GetModuleFileNameW(NULL, exe_path, MAX_PATH) != 0) {
356 cached_exe_dir = std::filesystem::path(exe_path).parent_path();
357 }
358#endif
359 } catch (...) {
360 // Skip if exe path detection fails
361 }
362 exe_dir_cached = true; // Only try once
363 }
364
365 if (!cached_exe_dir.empty()) {
366 try {
367 search_paths.push_back(cached_exe_dir / "assets" / relative_path);
368 // Also check parent (for build/bin/yaze case)
369 search_paths.push_back(cached_exe_dir.parent_path() / "assets" /
370 relative_path);
371#ifdef __APPLE__
372 // macOS app bundle: exe is at yaze.app/Contents/MacOS/yaze
373 // Assets may be at yaze.app/Contents/Resources/assets/ (inside bundle)
374 // or at ../../../assets/ (same level as .app bundle in DMG)
375 auto contents_dir = cached_exe_dir.parent_path(); // Contents/
376 auto bundle_dir = contents_dir.parent_path(); // yaze.app/
377 auto bundle_parent = bundle_dir.parent_path(); // DMG root
378 search_paths.push_back(contents_dir / "Resources" / "assets" /
379 relative_path);
380 search_paths.push_back(bundle_parent / "assets" / relative_path);
381#endif
382 } catch (...) {
383 // Skip if path construction fails
384 }
385 }
386
387 // 4. Parent directories (for running from build subdirectories)
388 if (!cached_cwd.empty()) {
389 try {
390 auto parent = cached_cwd.parent_path();
391 if (!parent.empty() && parent != cached_cwd) {
392 search_paths.push_back(parent / "assets" / relative_path);
393 auto grandparent = parent.parent_path();
394 if (!grandparent.empty() && grandparent != parent) {
395 search_paths.push_back(grandparent / "assets" / relative_path);
396 }
397 }
398 } catch (...) {
399 // Skip if path operations fail
400 }
401 }
402
403 // 5. User directory ~/.yaze/assets/ (cached home directory)
404 static std::filesystem::path cached_home;
405 static bool home_cached = false;
406
407 if (!home_cached) {
408 try {
409 cached_home = GetHomeDirectory();
410 } catch (...) {
411 // Skip if home lookup fails
412 }
413 home_cached = true; // Only try once
414 }
415
416 if (!cached_home.empty() && cached_home != ".") {
417 try {
418 search_paths.push_back(cached_home / ".yaze" / "assets" /
419 relative_path);
420 } catch (...) {
421 // Skip if path construction fails
422 }
423 }
424
425 // 6. System-wide installation (Unix only)
426#ifndef _WIN32
427 try {
428 search_paths.push_back(
429 std::filesystem::path("/usr/local/share/yaze/assets") /
430 relative_path);
431 search_paths.push_back(std::filesystem::path("/usr/share/yaze/assets") /
432 relative_path);
433 } catch (...) {
434 // Skip if path construction fails
435 }
436#endif
437
438 // Search all paths and return the first one that exists
439 // Limit search to prevent infinite loops on weird filesystems
440 const size_t max_paths_to_check = 20;
441 size_t checked = 0;
442
443 for (const auto& candidate : search_paths) {
444 if (++checked > max_paths_to_check) {
445 break; // Safety limit
446 }
447
448 try {
449 // Use std::filesystem::exists with error code to avoid exceptions
450 std::error_code exists_ec;
451 if (std::filesystem::exists(candidate, exists_ec) && !exists_ec) {
452 // Double-check it's a regular file or directory, not a broken symlink
453 auto status = std::filesystem::status(candidate, exists_ec);
454 if (!exists_ec &&
455 status.type() != std::filesystem::file_type::not_found) {
456 return candidate;
457 }
458 }
459 } catch (...) {
460 // Skip this candidate if checking fails
461 continue;
462 }
463 }
464
465 // If not found, return a simple error
466 return absl::NotFoundError(
467 absl::StrCat("Asset not found: ", relative_path));
468
469 } catch (const std::exception& e) {
470 return absl::InternalError(
471 absl::StrCat("Exception while searching for asset: ", e.what()));
472 } catch (...) {
473 return absl::InternalError("Unknown exception while searching for asset");
474 }
475}
476
477} // namespace util
478} // namespace 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.