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
45static 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
75static 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
125char *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
259char *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*/
359static 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 }
477error2:
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
489static 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
518char *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
604append_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