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