1 | /* |
2 | Simple DirectMedia Layer |
3 | Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org> |
4 | |
5 | This software is provided 'as-is', without any express or implied |
6 | warranty. In no event will the authors be held liable for any damages |
7 | arising from the use of this software. |
8 | |
9 | Permission is granted to anyone to use this software for any purpose, |
10 | including commercial applications, and to alter it and redistribute it |
11 | freely, subject to the following restrictions: |
12 | |
13 | 1. The origin of this software must not be misrepresented; you must not |
14 | claim that you wrote the original software. If you use this software |
15 | in a product, an acknowledgment in the product documentation would be |
16 | appreciated but is not required. |
17 | 2. Altered source versions must be plainly marked as such, and must not be |
18 | misrepresented as being the original software. |
19 | 3. This notice may not be removed or altered from any source distribution. |
20 | */ |
21 | #include "SDL_internal.h" |
22 | |
23 | #ifdef SDL_FILESYSTEM_UNIX |
24 | |
25 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ |
26 | // System dependent filesystem routines |
27 | |
28 | #include "../SDL_sysfilesystem.h" |
29 | |
30 | #include <stdio.h> |
31 | #include <sys/stat.h> |
32 | #include <sys/types.h> |
33 | #include <dirent.h> |
34 | #include <errno.h> |
35 | #include <fcntl.h> |
36 | #include <limits.h> |
37 | #include <stdlib.h> |
38 | #include <string.h> |
39 | #include <unistd.h> |
40 | |
41 | #if defined(SDL_PLATFORM_FREEBSD) || defined(SDL_PLATFORM_OPENBSD) |
42 | #include <sys/sysctl.h> |
43 | #endif |
44 | |
45 | static char *readSymLink(const char *path) |
46 | { |
47 | char *result = NULL; |
48 | ssize_t len = 64; |
49 | ssize_t rc = -1; |
50 | |
51 | while (1) { |
52 | char *ptr = (char *)SDL_realloc(result, (size_t)len); |
53 | if (!ptr) { |
54 | break; |
55 | } |
56 | |
57 | result = ptr; |
58 | |
59 | rc = readlink(path, result, len); |
60 | if (rc == -1) { |
61 | break; // not a symlink, i/o error, etc. |
62 | } else if (rc < len) { |
63 | result[rc] = '\0'; // readlink doesn't null-terminate. |
64 | return result; // we're good to go. |
65 | } |
66 | |
67 | len *= 2; // grow buffer, try again. |
68 | } |
69 | |
70 | SDL_free(result); |
71 | return NULL; |
72 | } |
73 | |
74 | #ifdef SDL_PLATFORM_OPENBSD |
75 | static char *search_path_for_binary(const char *bin) |
76 | { |
77 | const char *envr_real = SDL_getenv("PATH" ); |
78 | char *envr; |
79 | size_t alloc_size; |
80 | char *exe = NULL; |
81 | char *start = envr; |
82 | char *ptr; |
83 | |
84 | if (!envr_real) { |
85 | SDL_SetError("No $PATH set" ); |
86 | return NULL; |
87 | } |
88 | |
89 | envr = SDL_strdup(envr_real); |
90 | if (!envr) { |
91 | return NULL; |
92 | } |
93 | |
94 | SDL_assert(bin != NULL); |
95 | |
96 | alloc_size = SDL_strlen(bin) + SDL_strlen(envr) + 2; |
97 | exe = (char *)SDL_malloc(alloc_size); |
98 | |
99 | do { |
100 | ptr = SDL_strchr(start, ':'); // find next $PATH separator. |
101 | if (ptr != start) { |
102 | if (ptr) { |
103 | *ptr = '\0'; |
104 | } |
105 | |
106 | // build full binary path... |
107 | SDL_snprintf(exe, alloc_size, "%s%s%s" , start, (ptr && (ptr[-1] == '/')) ? "" : "/" , bin); |
108 | |
109 | if (access(exe, X_OK) == 0) { // Exists as executable? We're done. |
110 | SDL_free(envr); |
111 | return exe; |
112 | } |
113 | } |
114 | start = ptr + 1; // start points to beginning of next element. |
115 | } while (ptr); |
116 | |
117 | SDL_free(envr); |
118 | SDL_free(exe); |
119 | |
120 | SDL_SetError("Process not found in $PATH" ); |
121 | return NULL; // doesn't exist in path. |
122 | } |
123 | #endif |
124 | |
125 | char *SDL_SYS_GetBasePath(void) |
126 | { |
127 | char *result = NULL; |
128 | |
129 | #ifdef SDL_PLATFORM_FREEBSD |
130 | char fullpath[PATH_MAX]; |
131 | size_t buflen = sizeof(fullpath); |
132 | const int mib[] = { CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1 }; |
133 | if (sysctl(mib, SDL_arraysize(mib), fullpath, &buflen, NULL, 0) != -1) { |
134 | result = SDL_strdup(fullpath); |
135 | if (!result) { |
136 | return NULL; |
137 | } |
138 | } |
139 | #endif |
140 | #ifdef SDL_PLATFORM_OPENBSD |
141 | // Please note that this will fail if the process was launched with a relative path and $PWD + the cwd have changed, or argv is altered. So don't do that. Or add a new sysctl to OpenBSD. |
142 | char **cmdline; |
143 | size_t len; |
144 | const int mib[] = { CTL_KERN, KERN_PROC_ARGS, getpid(), KERN_PROC_ARGV }; |
145 | if (sysctl(mib, 4, NULL, &len, NULL, 0) != -1) { |
146 | char *exe, *pwddst; |
147 | char *realpathbuf = (char *)SDL_malloc(PATH_MAX + 1); |
148 | if (!realpathbuf) { |
149 | return NULL; |
150 | } |
151 | |
152 | cmdline = SDL_malloc(len); |
153 | if (!cmdline) { |
154 | SDL_free(realpathbuf); |
155 | return NULL; |
156 | } |
157 | |
158 | sysctl(mib, 4, cmdline, &len, NULL, 0); |
159 | |
160 | exe = cmdline[0]; |
161 | pwddst = NULL; |
162 | if (SDL_strchr(exe, '/') == NULL) { // not a relative or absolute path, check $PATH for it |
163 | exe = search_path_for_binary(cmdline[0]); |
164 | } else { |
165 | if (exe && *exe == '.') { |
166 | const char *pwd = SDL_getenv("PWD" ); |
167 | if (pwd && *pwd) { |
168 | SDL_asprintf(&pwddst, "%s/%s" , pwd, exe); |
169 | } |
170 | } |
171 | } |
172 | |
173 | if (exe) { |
174 | if (!pwddst) { |
175 | if (realpath(exe, realpathbuf) != NULL) { |
176 | result = realpathbuf; |
177 | } |
178 | } else { |
179 | if (realpath(pwddst, realpathbuf) != NULL) { |
180 | result = realpathbuf; |
181 | } |
182 | SDL_free(pwddst); |
183 | } |
184 | |
185 | if (exe != cmdline[0]) { |
186 | SDL_free(exe); |
187 | } |
188 | } |
189 | |
190 | if (!result) { |
191 | SDL_free(realpathbuf); |
192 | } |
193 | |
194 | SDL_free(cmdline); |
195 | } |
196 | #endif |
197 | |
198 | // is a Linux-style /proc filesystem available? |
199 | if (!result && (access("/proc" , F_OK) == 0)) { |
200 | /* !!! FIXME: after 2.0.6 ships, let's delete this code and just |
201 | use the /proc/%llu version. There's no reason to have |
202 | two copies of this plus all the #ifdefs. --ryan. */ |
203 | #ifdef SDL_PLATFORM_FREEBSD |
204 | result = readSymLink("/proc/curproc/file" ); |
205 | #elif defined(SDL_PLATFORM_NETBSD) |
206 | result = readSymLink("/proc/curproc/exe" ); |
207 | #elif defined(SDL_PLATFORM_SOLARIS) |
208 | result = readSymLink("/proc/self/path/a.out" ); |
209 | #else |
210 | result = readSymLink("/proc/self/exe" ); // linux. |
211 | if (!result) { |
212 | // older kernels don't have /proc/self ... try PID version... |
213 | char path[64]; |
214 | const int rc = SDL_snprintf(path, sizeof(path), |
215 | "/proc/%llu/exe" , |
216 | (unsigned long long)getpid()); |
217 | if ((rc > 0) && (rc < sizeof(path))) { |
218 | result = readSymLink(path); |
219 | } |
220 | } |
221 | #endif |
222 | } |
223 | |
224 | #ifdef SDL_PLATFORM_SOLARIS // try this as a fallback if /proc didn't pan out |
225 | if (!result) { |
226 | const char *path = getexecname(); |
227 | if ((path) && (path[0] == '/')) { // must be absolute path... |
228 | result = SDL_strdup(path); |
229 | if (!result) { |
230 | return NULL; |
231 | } |
232 | } |
233 | } |
234 | #endif |
235 | /* If we had access to argv[0] here, we could check it for a path, |
236 | or troll through $PATH looking for it, too. */ |
237 | |
238 | if (result) { // chop off filename. |
239 | char *ptr = SDL_strrchr(result, '/'); |
240 | if (ptr) { |
241 | *(ptr + 1) = '\0'; |
242 | } else { // shouldn't happen, but just in case... |
243 | SDL_free(result); |
244 | result = NULL; |
245 | } |
246 | } |
247 | |
248 | if (result) { |
249 | // try to shrink buffer... |
250 | char *ptr = (char *)SDL_realloc(result, SDL_strlen(result) + 1); |
251 | if (ptr) { |
252 | result = ptr; // oh well if it failed. |
253 | } |
254 | } |
255 | |
256 | return result; |
257 | } |
258 | |
259 | char *SDL_SYS_GetPrefPath(const char *org, const char *app) |
260 | { |
261 | /* |
262 | * We use XDG's base directory spec, even if you're not on Linux. |
263 | * This isn't strictly correct, but the results are relatively sane |
264 | * in any case. |
265 | * |
266 | * http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html |
267 | */ |
268 | const char *envr = SDL_getenv("XDG_DATA_HOME" ); |
269 | const char *append; |
270 | char *result = NULL; |
271 | char *ptr = NULL; |
272 | size_t len = 0; |
273 | |
274 | if (!app) { |
275 | SDL_InvalidParamError("app" ); |
276 | return NULL; |
277 | } |
278 | if (!org) { |
279 | org = "" ; |
280 | } |
281 | |
282 | if (!envr) { |
283 | // You end up with "$HOME/.local/share/Game Name 2" |
284 | envr = SDL_getenv("HOME" ); |
285 | if (!envr) { |
286 | // we could take heroic measures with /etc/passwd, but oh well. |
287 | SDL_SetError("neither XDG_DATA_HOME nor HOME environment is set" ); |
288 | return NULL; |
289 | } |
290 | append = "/.local/share/" ; |
291 | } else { |
292 | append = "/" ; |
293 | } |
294 | |
295 | len = SDL_strlen(envr); |
296 | if (envr[len - 1] == '/') { |
297 | append += 1; |
298 | } |
299 | |
300 | len += SDL_strlen(append) + SDL_strlen(org) + SDL_strlen(app) + 3; |
301 | result = (char *)SDL_malloc(len); |
302 | if (!result) { |
303 | return NULL; |
304 | } |
305 | |
306 | if (*org) { |
307 | (void)SDL_snprintf(result, len, "%s%s%s/%s/" , envr, append, org, app); |
308 | } else { |
309 | (void)SDL_snprintf(result, len, "%s%s%s/" , envr, append, app); |
310 | } |
311 | |
312 | for (ptr = result + 1; *ptr; ptr++) { |
313 | if (*ptr == '/') { |
314 | *ptr = '\0'; |
315 | if (mkdir(result, 0700) != 0 && errno != EEXIST) { |
316 | goto error; |
317 | } |
318 | *ptr = '/'; |
319 | } |
320 | } |
321 | if (mkdir(result, 0700) != 0 && errno != EEXIST) { |
322 | error: |
323 | SDL_SetError("Couldn't create directory '%s': '%s'" , result, strerror(errno)); |
324 | SDL_free(result); |
325 | return NULL; |
326 | } |
327 | |
328 | return result; |
329 | } |
330 | |
331 | /* |
332 | The two functions below (prefixed with `xdg_`) have been copied from: |
333 | https://gitlab.freedesktop.org/xdg/xdg-user-dirs/-/blob/master/xdg-user-dir-lookup.c |
334 | and have been adapted to work with SDL. They are licensed under the following |
335 | terms: |
336 | |
337 | Copyright (c) 2007 Red Hat, Inc. |
338 | |
339 | Permission is hereby granted, free of charge, to any person |
340 | obtaining a copy of this software and associated documentation files |
341 | (the "Software"), to deal in the Software without restriction, |
342 | including without limitation the rights to use, copy, modify, merge, |
343 | publish, distribute, sublicense, and/or sell copies of the Software, |
344 | and to permit persons to whom the Software is furnished to do so, |
345 | subject to the following conditions: |
346 | |
347 | The above copyright notice and this permission notice shall be |
348 | included in all copies or substantial portions of the Software. |
349 | |
350 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
351 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
352 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
353 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
354 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
355 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
356 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
357 | SOFTWARE. |
358 | */ |
359 | static char *xdg_user_dir_lookup_with_fallback (const char *type, const char *fallback) |
360 | { |
361 | FILE *file; |
362 | const char *home_dir, *config_home; |
363 | char *config_file; |
364 | char buffer[512]; |
365 | char *user_dir; |
366 | char *p, *d; |
367 | int len; |
368 | int relative; |
369 | size_t l; |
370 | |
371 | home_dir = SDL_getenv("HOME" ); |
372 | |
373 | if (!home_dir) |
374 | goto error; |
375 | |
376 | config_home = SDL_getenv("XDG_CONFIG_HOME" ); |
377 | if (!config_home || config_home[0] == 0) |
378 | { |
379 | l = SDL_strlen (home_dir) + SDL_strlen ("/.config/user-dirs.dirs" ) + 1; |
380 | config_file = (char*) SDL_malloc (l); |
381 | if (!config_file) |
382 | goto error; |
383 | |
384 | SDL_strlcpy (config_file, home_dir, l); |
385 | SDL_strlcat (config_file, "/.config/user-dirs.dirs" , l); |
386 | } |
387 | else |
388 | { |
389 | l = SDL_strlen (config_home) + SDL_strlen ("/user-dirs.dirs" ) + 1; |
390 | config_file = (char*) SDL_malloc (l); |
391 | if (!config_file) |
392 | goto error; |
393 | |
394 | SDL_strlcpy (config_file, config_home, l); |
395 | SDL_strlcat (config_file, "/user-dirs.dirs" , l); |
396 | } |
397 | |
398 | file = fopen (config_file, "r" ); |
399 | SDL_free (config_file); |
400 | if (!file) |
401 | goto error; |
402 | |
403 | user_dir = NULL; |
404 | while (fgets (buffer, sizeof (buffer), file)) |
405 | { |
406 | // Remove newline at end |
407 | len = SDL_strlen (buffer); |
408 | if (len > 0 && buffer[len-1] == '\n') |
409 | buffer[len-1] = 0; |
410 | |
411 | p = buffer; |
412 | while (*p == ' ' || *p == '\t') |
413 | p++; |
414 | |
415 | if (SDL_strncmp (p, "XDG_" , 4) != 0) |
416 | continue; |
417 | p += 4; |
418 | if (SDL_strncmp (p, type, SDL_strlen (type)) != 0) |
419 | continue; |
420 | p += SDL_strlen (type); |
421 | if (SDL_strncmp (p, "_DIR" , 4) != 0) |
422 | continue; |
423 | p += 4; |
424 | |
425 | while (*p == ' ' || *p == '\t') |
426 | p++; |
427 | |
428 | if (*p != '=') |
429 | continue; |
430 | p++; |
431 | |
432 | while (*p == ' ' || *p == '\t') |
433 | p++; |
434 | |
435 | if (*p != '"') |
436 | continue; |
437 | p++; |
438 | |
439 | relative = 0; |
440 | if (SDL_strncmp (p, "$HOME/" , 6) == 0) |
441 | { |
442 | p += 6; |
443 | relative = 1; |
444 | } |
445 | else if (*p != '/') |
446 | continue; |
447 | |
448 | SDL_free (user_dir); |
449 | if (relative) |
450 | { |
451 | l = SDL_strlen (home_dir) + 1 + SDL_strlen (p) + 1; |
452 | user_dir = (char*) SDL_malloc (l); |
453 | if (!user_dir) |
454 | goto error2; |
455 | |
456 | SDL_strlcpy (user_dir, home_dir, l); |
457 | SDL_strlcat (user_dir, "/" , l); |
458 | } |
459 | else |
460 | { |
461 | user_dir = (char*) SDL_malloc (SDL_strlen (p) + 1); |
462 | if (!user_dir) |
463 | goto error2; |
464 | |
465 | *user_dir = 0; |
466 | } |
467 | |
468 | d = user_dir + SDL_strlen (user_dir); |
469 | while (*p && *p != '"') |
470 | { |
471 | if ((*p == '\\') && (*(p+1) != 0)) |
472 | p++; |
473 | *d++ = *p++; |
474 | } |
475 | *d = 0; |
476 | } |
477 | error2: |
478 | fclose (file); |
479 | |
480 | if (user_dir) |
481 | return user_dir; |
482 | |
483 | error: |
484 | if (fallback) |
485 | return SDL_strdup (fallback); |
486 | return NULL; |
487 | } |
488 | |
489 | static char *xdg_user_dir_lookup (const char *type) |
490 | { |
491 | const char *home_dir; |
492 | char *dir, *user_dir; |
493 | |
494 | dir = xdg_user_dir_lookup_with_fallback(type, NULL); |
495 | if (dir) |
496 | return dir; |
497 | |
498 | home_dir = SDL_getenv("HOME" ); |
499 | |
500 | if (!home_dir) |
501 | return NULL; |
502 | |
503 | // Special case desktop for historical compatibility |
504 | if (SDL_strcmp(type, "DESKTOP" ) == 0) { |
505 | size_t length = SDL_strlen(home_dir) + SDL_strlen("/Desktop" ) + 1; |
506 | user_dir = (char*) SDL_malloc(length); |
507 | if (!user_dir) |
508 | return NULL; |
509 | |
510 | SDL_strlcpy(user_dir, home_dir, length); |
511 | SDL_strlcat(user_dir, "/Desktop" , length); |
512 | return user_dir; |
513 | } |
514 | |
515 | return NULL; |
516 | } |
517 | |
518 | char *SDL_SYS_GetUserFolder(SDL_Folder folder) |
519 | { |
520 | const char *param = NULL; |
521 | char *result; |
522 | char *newresult; |
523 | |
524 | /* According to `man xdg-user-dir`, the possible values are: |
525 | DESKTOP |
526 | DOWNLOAD |
527 | TEMPLATES |
528 | PUBLICSHARE |
529 | DOCUMENTS |
530 | MUSIC |
531 | PICTURES |
532 | VIDEOS |
533 | */ |
534 | switch(folder) { |
535 | case SDL_FOLDER_HOME: |
536 | param = SDL_getenv("HOME" ); |
537 | |
538 | if (!param) { |
539 | SDL_SetError("No $HOME environment variable available" ); |
540 | return NULL; |
541 | } |
542 | |
543 | result = SDL_strdup(param); |
544 | goto append_slash; |
545 | |
546 | case SDL_FOLDER_DESKTOP: |
547 | param = "DESKTOP" ; |
548 | break; |
549 | |
550 | case SDL_FOLDER_DOCUMENTS: |
551 | param = "DOCUMENTS" ; |
552 | break; |
553 | |
554 | case SDL_FOLDER_DOWNLOADS: |
555 | param = "DOWNLOAD" ; |
556 | break; |
557 | |
558 | case SDL_FOLDER_MUSIC: |
559 | param = "MUSIC" ; |
560 | break; |
561 | |
562 | case SDL_FOLDER_PICTURES: |
563 | param = "PICTURES" ; |
564 | break; |
565 | |
566 | case SDL_FOLDER_PUBLICSHARE: |
567 | param = "PUBLICSHARE" ; |
568 | break; |
569 | |
570 | case SDL_FOLDER_SAVEDGAMES: |
571 | SDL_SetError("Saved Games folder unavailable on XDG" ); |
572 | return NULL; |
573 | |
574 | case SDL_FOLDER_SCREENSHOTS: |
575 | SDL_SetError("Screenshots folder unavailable on XDG" ); |
576 | return NULL; |
577 | |
578 | case SDL_FOLDER_TEMPLATES: |
579 | param = "TEMPLATES" ; |
580 | break; |
581 | |
582 | case SDL_FOLDER_VIDEOS: |
583 | param = "VIDEOS" ; |
584 | break; |
585 | |
586 | default: |
587 | SDL_SetError("Invalid SDL_Folder: %d" , (int) folder); |
588 | return NULL; |
589 | } |
590 | |
591 | /* param *should* to be set to something at this point, but just in case */ |
592 | if (!param) { |
593 | SDL_SetError("No corresponding XDG user directory" ); |
594 | return NULL; |
595 | } |
596 | |
597 | result = xdg_user_dir_lookup(param); |
598 | |
599 | if (!result) { |
600 | SDL_SetError("XDG directory not available" ); |
601 | return NULL; |
602 | } |
603 | |
604 | append_slash: |
605 | newresult = (char *) SDL_realloc(result, SDL_strlen(result) + 2); |
606 | |
607 | if (!newresult) { |
608 | SDL_free(result); |
609 | return NULL; |
610 | } |
611 | |
612 | result = newresult; |
613 | SDL_strlcat(result, "/" , SDL_strlen(result) + 2); |
614 | |
615 | return result; |
616 | } |
617 | |
618 | #endif // SDL_FILESYSTEM_UNIX |
619 | |