diff --git a/include/SDL3/SDL_filesystem.h b/include/SDL3/SDL_filesystem.h index b45f41380f..8811a9ae95 100644 --- a/include/SDL3/SDL_filesystem.h +++ b/include/SDL3/SDL_filesystem.h @@ -138,6 +138,83 @@ extern DECLSPEC char *SDLCALL SDL_GetBasePath(void); */ extern DECLSPEC char *SDLCALL SDL_GetPrefPath(const char *org, const char *app); +/** + * The type of the OS-provided default folder for a specific purpose. + * + * Note that the Trash folder isn't included here, because trashing files usually + * involves extra OS-specific functionality to remember the file's original + * location. + * + * \sa SDL_GetPath + */ +typedef enum +{ + /** The folder which contains all of the current user's data, preferences, + and documents. It usually contains most of the other folders. If a + requested folder does not exist, the home folder can be considered a safe + fallback to store a user's documents. Supported on Windows, macOS and + Unix with XDG. */ + SDL_FOLDER_HOME, + /** The folder of files that are displayed on the desktop. Note that the + existence of a desktop folder does not guarantee that the system does + show icons on its desktop; certain GNU/Linux distros with a graphical + environment may not have desktop icons. Supported on Windows, macOS and + Unix with XDG. */ + SDL_FOLDER_DESKTOP, + /** General document files, possibly application-specific. This is a good + place to save a user's projects. Supported on Windows, macOS and Unix + with XDG. */ + SDL_FOLDER_DOCUMENTS, + /** Generic landing folder for files downloaded from the internet. Supported + on Windows Vista and later, macOS and Unix with XDG. */ + SDL_FOLDER_DOWNLOADS, + /** Music files that can be played using a standard music player (mp3, + ogg...). Supported on Windows, macOS and Unix with XDG. */ + SDL_FOLDER_MUSIC, + /** Image files that can be displayed using a standard viewer (png, + jpg...). Supported on Windows, macOS and Unix with XDG. */ + SDL_FOLDER_PICTURES, + /** Files that are meant to be shared with other users on the same + computer. Supported on macOS and Unix with XDG. */ + SDL_FOLDER_PUBLICSHARE, + /** Save files for games. Supported on Windows Vista and later. */ + SDL_FOLDER_SAVEDGAMES, + /** Application screenshots. Supported on Windows Vista and later. */ + SDL_FOLDER_SCREENSHOTS, + /** Template files to be used when the user requests the desktop environment + to create a new file in a certain folder, such as "New Text File.txt". + Any file in the Templates folder can be used as a starting point for a + new file. Supported on Windows, macOS and Unix with XDG. */ + SDL_FOLDER_TEMPLATES, + /** Video files that can be played using a standard video player (mp4, + webm...). On macOS, this is the "Movies" folder. Supported on Windows, + macOS and Unix with XDG. */ + SDL_FOLDER_VIDEOS, +} SDL_Folder; + +/** + * Finds the most suitable OS-provided folder for @p folder, and returns its + * path in OS-specific notation. + * + * Many OSes provide certain standard folders for certain purposes, such as + * storing pictures, music or videos for a certain user. This function gives + * the path for many of those special locations. + * + * Note that the function is expensive, and should be called once at the + * beginning of the execution and kept for as long as needed. + * + * The returned value is owned by the caller and should be freed with + * SDL_free(). + * + * If NULL is returned, the error may be obtained with SDL_GetError(). + * + * \returns Either a null-terminated C string containing the full path to the + * folder, or NULL if an error happened. + * + * \sa SDL_Folder + */ +extern DECLSPEC char *SDLCALL SDL_GetPath(SDL_Folder folder); + /* Ends C function definitions when using C++ */ #ifdef __cplusplus } diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index 3870424b20..3927b26234 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/src/dynapi/SDL_dynapi.sym @@ -851,6 +851,7 @@ SDL3_0.0.0 { SDL_TryLockRWLockForWriting; SDL_UnlockRWLock; SDL_DestroyRWLock; + SDL_GetPath; # extra symbols go here (don't modify this line) local: *; }; diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h index 5c78a8c00e..d96acd6150 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/src/dynapi/SDL_dynapi_overrides.h @@ -877,3 +877,4 @@ #define SDL_TryLockRWLockForWriting SDL_TryLockRWLockForWriting_REAL #define SDL_UnlockRWLock SDL_UnlockRWLock_REAL #define SDL_DestroyRWLock SDL_DestroyRWLock_REAL +#define SDL_GetPath SDL_GetPath_REAL diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index e59b2d9de3..9ebfecd4e5 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/src/dynapi/SDL_dynapi_procs.h @@ -922,3 +922,4 @@ SDL_DYNAPI_PROC(int,SDL_TryLockRWLockForReading,(SDL_RWLock *a),(a),return) SDL_DYNAPI_PROC(int,SDL_TryLockRWLockForWriting,(SDL_RWLock *a),(a),return) SDL_DYNAPI_PROC(int,SDL_UnlockRWLock,(SDL_RWLock *a),(a),return) SDL_DYNAPI_PROC(void,SDL_DestroyRWLock,(SDL_RWLock *a),(a),) +SDL_DYNAPI_PROC(char*,SDL_GetPath,(SDL_Folder a),(a),return) diff --git a/src/filesystem/cocoa/SDL_sysfilesystem.m b/src/filesystem/cocoa/SDL_sysfilesystem.m index ddf86ddcfd..c9880efe45 100644 --- a/src/filesystem/cocoa/SDL_sysfilesystem.m +++ b/src/filesystem/cocoa/SDL_sysfilesystem.m @@ -132,4 +132,118 @@ SDL_GetPrefPath(const char *org, const char *app) } } +char * +SDL_GetPath(SDL_Folder folder) +{ + @autoreleasepool { +#if TARGET_OS_TV + SDL_SetError("tvOS does not have persistent storage"); + return NULL; +#else + char *retval = NULL; + const char* base; + NSArray *array; + NSSearchPathDirectory dir; + NSString *str; + char *ptr; + + switch (folder) + { + case SDL_FOLDER_HOME: + base = SDL_getenv("HOME"); + + if (!base) + { + SDL_SetError("No $HOME environment variable available"); + } + + retval = SDL_strdup(base); + + if (!retval) + SDL_OutOfMemory(); + + return retval; + + case SDL_FOLDER_DESKTOP: + dir = NSDesktopDirectory; + break; + + case SDL_FOLDER_DOCUMENTS: + dir = NSDocumentDirectory; + break; + + case SDL_FOLDER_DOWNLOADS: + dir = NSDownloadsDirectory; + break; + + case SDL_FOLDER_MUSIC: + dir = NSMusicDirectory; + break; + + case SDL_FOLDER_PICTURES: + dir = NSPicturesDirectory; + break; + + case SDL_FOLDER_PUBLICSHARE: + dir = NSSharedPublicDirectory; + break; + + case SDL_FOLDER_SAVEDGAMES: + SDL_SetError("Saved games folder not supported on Cocoa"); + return NULL; + + case SDL_FOLDER_SCREENSHOTS: + SDL_SetError("Screenshots folder not supported on Cocoa"); + return NULL; + + case SDL_FOLDER_TEMPLATES: + SDL_SetError("Templates folder not supported on Cocoa"); + return NULL; + + case SDL_FOLDER_VIDEOS: + dir = NSMoviesDirectory; + break; + + default: + SDL_SetError("Invalid SDL_Folder: %d", (int) folder); + return NULL; + }; + + array = NSSearchPathForDirectoriesInDomains(dir, NSUserDomainMask, YES); + + if ([array count] <= 0) + { + SDL_SetError("Directory not found"); + return NULL; + } + + str = [array objectAtIndex:0]; + base = [str fileSystemRepresentation]; + if (!base) + { + SDL_SetError("Couldn't get folder path"); + return NULL; + } + + retval = SDL_strdup(base); + if (retval == NULL) + { + SDL_OutOfMemory(); + return NULL; + } + + for (ptr = retval + 1; *ptr; ptr++) { + if (*ptr == '/') { + *ptr = '\0'; + mkdir(retval, 0700); + *ptr = '/'; + } + } + mkdir(retval, 0700); + + return retval; +#endif /* TARGET_OS_TV */ + } +} + #endif /* SDL_FILESYSTEM_COCOA */ diff --git a/src/filesystem/dummy/SDL_sysfilesystem.c b/src/filesystem/dummy/SDL_sysfilesystem.c index 644a3bc8a6..d341c740f5 100644 --- a/src/filesystem/dummy/SDL_sysfilesystem.c +++ b/src/filesystem/dummy/SDL_sysfilesystem.c @@ -39,4 +39,11 @@ SDL_GetPrefPath(const char *org, const char *app) return NULL; } +char * +SDL_GetPath(SDL_Folder folder) +{ + SDL_Unsupported(); + return NULL; +} + #endif /* SDL_FILESYSTEM_DUMMY || SDL_FILESYSTEM_DISABLED */ diff --git a/src/filesystem/unix/SDL_sysfilesystem.c b/src/filesystem/unix/SDL_sysfilesystem.c index cd4af65a7f..e3ca8e1987 100644 --- a/src/filesystem/unix/SDL_sysfilesystem.c +++ b/src/filesystem/unix/SDL_sysfilesystem.c @@ -27,6 +27,7 @@ #include #include +#include #include #include #include @@ -332,4 +333,287 @@ SDL_GetPrefPath(const char *org, const char *app) return retval; } +/* + The two functions below (prefixed with `xdg_`) have been copied from: + https://gitlab.freedesktop.org/xdg/xdg-user-dirs/-/blob/master/xdg-user-dir-lookup.c + and have been adapted to work with SDL. They are licensed under the following + terms: + + Copyright (c) 2007 Red Hat, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ +static char * +xdg_user_dir_lookup_with_fallback (const char *type, const char *fallback) +{ + FILE *file; + char *home_dir, *config_home, *config_file; + char buffer[512]; + char *user_dir; + char *p, *d; + int len; + int relative; + size_t l; + + home_dir = SDL_getenv ("HOME"); + + if (home_dir == NULL) + goto error; + + config_home = SDL_getenv ("XDG_CONFIG_HOME"); + if (config_home == NULL || config_home[0] == 0) + { + l = SDL_strlen (home_dir) + SDL_strlen ("/.config/user-dirs.dirs") + 1; + config_file = (char*) SDL_malloc (l); + if (config_file == NULL) + goto error; + + SDL_strlcpy (config_file, home_dir, l); + SDL_strlcat (config_file, "/.config/user-dirs.dirs", l); + } + else + { + l = SDL_strlen (config_home) + SDL_strlen ("/user-dirs.dirs") + 1; + config_file = (char*) SDL_malloc (l); + if (config_file == NULL) + goto error; + + SDL_strlcpy (config_file, config_home, l); + SDL_strlcat (config_file, "/user-dirs.dirs", l); + } + + file = fopen (config_file, "r"); + SDL_free (config_file); + if (file == NULL) + goto error; + + user_dir = NULL; + while (fgets (buffer, sizeof (buffer), file)) + { + /* Remove newline at end */ + len = SDL_strlen (buffer); + if (len > 0 && buffer[len-1] == '\n') + buffer[len-1] = 0; + + p = buffer; + while (*p == ' ' || *p == '\t') + p++; + + if (SDL_strncmp (p, "XDG_", 4) != 0) + continue; + p += 4; + if (SDL_strncmp (p, type, SDL_strlen (type)) != 0) + continue; + p += strlen (type); + if (SDL_strncmp (p, "_DIR", 4) != 0) + continue; + p += 4; + + while (*p == ' ' || *p == '\t') + p++; + + if (*p != '=') + continue; + p++; + + while (*p == ' ' || *p == '\t') + p++; + + if (*p != '"') + continue; + p++; + + relative = 0; + if (SDL_strncmp (p, "$HOME/", 6) == 0) + { + p += 6; + relative = 1; + } + else if (*p != '/') + continue; + + SDL_free (user_dir); + if (relative) + { + l = SDL_strlen (home_dir) + 1 + SDL_strlen (p) + 1; + user_dir = (char*) SDL_malloc (l); + if (user_dir == NULL) + goto error2; + + SDL_strlcpy (user_dir, home_dir, l); + SDL_strlcat (user_dir, "/", l); + } + else + { + user_dir = (char*) SDL_malloc (SDL_strlen (p) + 1); + if (user_dir == NULL) + goto error2; + + *user_dir = 0; + } + + d = user_dir + SDL_strlen (user_dir); + while (*p && *p != '"') + { + if ((*p == '\\') && (*(p+1) != 0)) + p++; + *d++ = *p++; + } + *d = 0; + } +error2: + fclose (file); + + if (user_dir) + return user_dir; + + error: + if (fallback) + return SDL_strdup (fallback); + return NULL; +} + +static char * +xdg_user_dir_lookup (const char *type) +{ + char *dir, *home_dir, *user_dir; + + dir = xdg_user_dir_lookup_with_fallback(type, NULL); + if (dir != NULL) + return dir; + + home_dir = SDL_getenv("HOME"); + + if (home_dir == NULL) + return NULL; + + /* Special case desktop for historical compatibility */ + if (SDL_strcmp(type, "DESKTOP") == 0) + { + user_dir = (char*) SDL_malloc(SDL_strlen(home_dir) + + SDL_strlen("/Desktop") + 1); + if (user_dir == NULL) + return NULL; + + strcpy(user_dir, home_dir); + strcat(user_dir, "/Desktop"); + return user_dir; + } + + return NULL; +} + +char * +SDL_GetPath(SDL_Folder folder) +{ + const char *param = NULL; + char *retval; + + /* According to `man xdg-user-dir`, the possible values are: + DESKTOP + DOWNLOAD + TEMPLATES + PUBLICSHARE + DOCUMENTS + MUSIC + PICTURES + VIDEOS + */ + switch(folder) + { + case SDL_FOLDER_HOME: + param = SDL_getenv("HOME"); + + if (!param) + { + SDL_SetError("No $HOME environment variable available"); + } + + retval = SDL_strdup(param); + + if (!retval) + SDL_OutOfMemory(); + + return retval; + + case SDL_FOLDER_DESKTOP: + param = "DESKTOP"; + break; + + case SDL_FOLDER_DOCUMENTS: + param = "DOCUMENTS"; + break; + + case SDL_FOLDER_DOWNLOADS: + param = "DOWNLOAD"; + break; + + case SDL_FOLDER_MUSIC: + param = "MUSIC"; + break; + + case SDL_FOLDER_PICTURES: + param = "PICTURES"; + break; + + case SDL_FOLDER_PUBLICSHARE: + param = "PUBLICSHARE"; + break; + + case SDL_FOLDER_SAVEDGAMES: + SDL_SetError("Saved Games folder unavailable on XDG"); + return NULL; + + case SDL_FOLDER_SCREENSHOTS: + SDL_SetError("Screenshots folder unavailable on XDG"); + return NULL; + + case SDL_FOLDER_TEMPLATES: + param = "TEMPLATES"; + break; + + case SDL_FOLDER_VIDEOS: + param = "VIDEOS"; + break; + + default: + SDL_SetError("Invalid SDL_Folder: %d", (int) folder); + return NULL; + } + + /* param *should* to be set to something at this point, but just in case */ + if (!param) + { + SDL_SetError("No corresponding XDG user directory"); + return NULL; + } + + retval = xdg_user_dir_lookup(param); + + if (!retval) + { + SDL_SetError("XDG directory not available"); + } + + return retval; +} + #endif /* SDL_FILESYSTEM_UNIX */ diff --git a/src/filesystem/windows/SDL_sysfilesystem.c b/src/filesystem/windows/SDL_sysfilesystem.c index 753ca091ed..46acf098bb 100644 --- a/src/filesystem/windows/SDL_sysfilesystem.c +++ b/src/filesystem/windows/SDL_sysfilesystem.c @@ -26,7 +26,14 @@ /* System dependent filesystem routines */ #include "../../core/windows/SDL_windows.h" +#include +#include #include +#include +/* Lowercase is necessary for Wine */ +#include +#include +#include char * SDL_GetBasePath(void) @@ -166,6 +173,179 @@ SDL_GetPrefPath(const char *org, const char *app) return retval; } +char * +SDL_GetPath(SDL_Folder folder) +{ + typedef HRESULT (*SHGKFP)(REFKNOWNFOLDERID, DWORD, HANDLE, PWSTR *); + char *retval; + HMODULE lib = LoadLibrary(L"Shell32.dll"); + SHGKFP SHGetKnownFolderPath_ = (SHGKFP) GetProcAddress(lib, + "SHGetKnownFolderPath"); + + if (!SHGetKnownFolderPath_) + { + int type; + HRESULT result; + wchar_t path[MAX_PATH]; + + switch (folder) + { + case SDL_FOLDER_HOME: + type = CSIDL_PROFILE; + break; + + case SDL_FOLDER_DESKTOP: + type = CSIDL_DESKTOP; + break; + + case SDL_FOLDER_DOCUMENTS: + type = CSIDL_MYDOCUMENTS; + break; + + case SDL_FOLDER_DOWNLOADS: + SDL_SetError("Downloads folder unavailable before Vista"); + return NULL; + + case SDL_FOLDER_MUSIC: + type = CSIDL_MYMUSIC; + break; + + case SDL_FOLDER_PICTURES: + type = CSIDL_MYPICTURES; + break; + + case SDL_FOLDER_PUBLICSHARE: + SDL_SetError("Public share unavailable on Windows"); + return NULL; + + case SDL_FOLDER_SAVEDGAMES: + SDL_SetError("Saved games unavailable before Vista"); + return NULL; + + case SDL_FOLDER_SCREENSHOTS: + SDL_SetError("Screenshots folder unavailable before Vista"); + return NULL; + + case SDL_FOLDER_TEMPLATES: + type = CSIDL_TEMPLATES; + break; + + case SDL_FOLDER_VIDEOS: + type = CSIDL_MYVIDEO; + break; + + default: + SDL_SetError("Unsupported SDL_Folder on Windows before Vista: %d", + (int) folder); + return NULL; + }; + + /* Create the OS-specific folder if it doesn't already exist */ + type |= CSIDL_FLAG_CREATE; + +#if 0 + /* Apparently the oldest, but not supported in modern Windows */ + HRESULT result = SHGetSpecialFolderPath(NULL, path, type, TRUE); +#endif + + /* Windows 2000/XP and later, deprecated as of Windows 10 (still + available), available in Wine (tested 6.0.3) */ + result = SHGetFolderPathW(NULL, type, NULL, SHGFP_TYPE_CURRENT, path); + + /* use `!= TRUE` for SHGetSpecialFolderPath */ + if (result != S_OK) + { + SDL_SetError("Couldn't get folder, windows-specific error: %ld", + result); + return NULL; + } + + retval = (char *) SDL_malloc((SDL_wcslen(path) + 1) * 2); + if (retval == NULL) + { + SDL_OutOfMemory(); + return NULL; + } + retval = WIN_StringToUTF8W(path); + return retval; + } + else + { + KNOWNFOLDERID type; + HRESULT result; + wchar_t *path; + + switch (folder) + { + case SDL_FOLDER_HOME: + type = FOLDERID_Profile; + break; + + case SDL_FOLDER_DESKTOP: + type = FOLDERID_Desktop; + break; + + case SDL_FOLDER_DOCUMENTS: + type = FOLDERID_Documents; + break; + + case SDL_FOLDER_DOWNLOADS: + type = FOLDERID_Downloads; + break; + + case SDL_FOLDER_MUSIC: + type = FOLDERID_Music; + break; + + case SDL_FOLDER_PICTURES: + type = FOLDERID_Pictures; + break; + + case SDL_FOLDER_PUBLICSHARE: + SDL_SetError("Public share unavailable on Windows"); + return NULL; + + case SDL_FOLDER_SAVEDGAMES: + type = FOLDERID_SavedGames; + break; + + case SDL_FOLDER_SCREENSHOTS: + type = FOLDERID_Screenshots; + break; + + case SDL_FOLDER_TEMPLATES: + type = FOLDERID_Templates; + break; + + case SDL_FOLDER_VIDEOS: + type = FOLDERID_Videos; + break; + + default: + SDL_SetError("Invalid SDL_Folder: %d", (int) folder); + return NULL; + }; + + result = SHGetKnownFolderPath_(&type, KF_FLAG_CREATE, NULL, &path); + + if (result != S_OK) + { + SDL_SetError("Couldn't get folder, windows-specific error: %ld", + result); + return NULL; + } + + retval = (char *) SDL_malloc((SDL_wcslen(path) + 1) * 2); + if (retval == NULL) + { + SDL_OutOfMemory(); + return NULL; + } + retval = WIN_StringToUTF8W(path); + return retval; + } +} + #endif /* SDL_FILESYSTEM_WINDOWS */ #ifdef SDL_FILESYSTEM_XBOX @@ -182,4 +362,11 @@ SDL_GetPrefPath(const char *org, const char *app) SDL_Unsupported(); return NULL; } + +char * +SDL_GetPath(SDL_Folder folder) +{ + SDL_Unsupported(); + return NULL; +} #endif /* SDL_FILESYSTEM_XBOX */