| 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 | |
| 22 | #include "SDL_internal.h" |
| 23 | |
| 24 | #include "../SDL_sysstorage.h" |
| 25 | |
| 26 | |
| 27 | static char *GENERIC_INTERNAL_CreateFullPath(const char *base, const char *relative) |
| 28 | { |
| 29 | char *result = NULL; |
| 30 | SDL_asprintf(&result, "%s%s" , base ? base : "" , relative); |
| 31 | return result; |
| 32 | } |
| 33 | |
| 34 | static bool GENERIC_CloseStorage(void *userdata) |
| 35 | { |
| 36 | SDL_free(userdata); |
| 37 | return true; |
| 38 | } |
| 39 | |
| 40 | typedef struct GenericEnumerateData |
| 41 | { |
| 42 | size_t base_len; |
| 43 | SDL_EnumerateDirectoryCallback real_callback; |
| 44 | void *real_userdata; |
| 45 | } GenericEnumerateData; |
| 46 | |
| 47 | static SDL_EnumerationResult SDLCALL GENERIC_EnumerateDirectory(void *userdata, const char *dirname, const char *fname) |
| 48 | { |
| 49 | // SDL_EnumerateDirectory will return the full path, so for Storage we |
| 50 | // can take the base directory and add its length to the dirname string, |
| 51 | // effectively trimming the root without having to strdup anything. |
| 52 | const GenericEnumerateData *wrap_data = (GenericEnumerateData *)userdata; |
| 53 | |
| 54 | dirname += wrap_data->base_len; // skip the base, just return the part inside of the Storage. |
| 55 | |
| 56 | #ifdef SDL_PLATFORM_WINDOWS |
| 57 | char *dirnamecpy = NULL; |
| 58 | const size_t slen = SDL_strlen(dirname); |
| 59 | if (slen && (dirname[slen - 1] == '\\')) { |
| 60 | dirnamecpy = SDL_strdup(dirname); |
| 61 | if (!dirnamecpy) { |
| 62 | return SDL_ENUM_FAILURE; |
| 63 | } |
| 64 | dirnamecpy[slen - 1] = '/'; // storage layer always uses '/' path separators. |
| 65 | dirname = dirnamecpy; |
| 66 | } |
| 67 | const SDL_EnumerationResult retval = wrap_data->real_callback(wrap_data->real_userdata, dirname, fname); |
| 68 | SDL_free(dirnamecpy); |
| 69 | return retval; |
| 70 | #else |
| 71 | return wrap_data->real_callback(wrap_data->real_userdata, dirname, fname); |
| 72 | #endif |
| 73 | } |
| 74 | |
| 75 | static bool GENERIC_EnumerateStorageDirectory(void *userdata, const char *path, SDL_EnumerateDirectoryCallback callback, void *callback_userdata) |
| 76 | { |
| 77 | bool result = false; |
| 78 | GenericEnumerateData wrap_data; |
| 79 | |
| 80 | char *fullpath = GENERIC_INTERNAL_CreateFullPath((char *)userdata, path); |
| 81 | if (fullpath) { |
| 82 | wrap_data.base_len = SDL_strlen((char *)userdata); |
| 83 | wrap_data.real_callback = callback; |
| 84 | wrap_data.real_userdata = callback_userdata; |
| 85 | |
| 86 | result = SDL_EnumerateDirectory(fullpath, GENERIC_EnumerateDirectory, &wrap_data); |
| 87 | |
| 88 | SDL_free(fullpath); |
| 89 | } |
| 90 | return result; |
| 91 | } |
| 92 | |
| 93 | static bool GENERIC_GetStoragePathInfo(void *userdata, const char *path, SDL_PathInfo *info) |
| 94 | { |
| 95 | bool result = false; |
| 96 | |
| 97 | char *fullpath = GENERIC_INTERNAL_CreateFullPath((char *)userdata, path); |
| 98 | if (fullpath) { |
| 99 | result = SDL_GetPathInfo(fullpath, info); |
| 100 | |
| 101 | SDL_free(fullpath); |
| 102 | } |
| 103 | return result; |
| 104 | } |
| 105 | |
| 106 | static bool GENERIC_ReadStorageFile(void *userdata, const char *path, void *destination, Uint64 length) |
| 107 | { |
| 108 | bool result = false; |
| 109 | |
| 110 | if (length > SDL_SIZE_MAX) { |
| 111 | return SDL_SetError("Read size exceeds SDL_SIZE_MAX" ); |
| 112 | } |
| 113 | |
| 114 | char *fullpath = GENERIC_INTERNAL_CreateFullPath((char *)userdata, path); |
| 115 | if (fullpath) { |
| 116 | SDL_IOStream *stream = SDL_IOFromFile(fullpath, "rb" ); |
| 117 | if (stream) { |
| 118 | // FIXME: Should SDL_ReadIO use u64 now...? |
| 119 | if (SDL_ReadIO(stream, destination, (size_t)length) == length) { |
| 120 | result = true; |
| 121 | } else { |
| 122 | SDL_SetError("File length did not exactly match the destination length" ); |
| 123 | } |
| 124 | SDL_CloseIO(stream); |
| 125 | } |
| 126 | SDL_free(fullpath); |
| 127 | } |
| 128 | return result; |
| 129 | } |
| 130 | |
| 131 | static bool GENERIC_WriteStorageFile(void *userdata, const char *path, const void *source, Uint64 length) |
| 132 | { |
| 133 | // TODO: Recursively create subdirectories with SDL_CreateDirectory |
| 134 | bool result = false; |
| 135 | |
| 136 | if (length > SDL_SIZE_MAX) { |
| 137 | return SDL_SetError("Write size exceeds SDL_SIZE_MAX" ); |
| 138 | } |
| 139 | |
| 140 | char *fullpath = GENERIC_INTERNAL_CreateFullPath((char *)userdata, path); |
| 141 | if (fullpath) { |
| 142 | SDL_IOStream *stream = SDL_IOFromFile(fullpath, "wb" ); |
| 143 | |
| 144 | if (stream) { |
| 145 | // FIXME: Should SDL_WriteIO use u64 now...? |
| 146 | if (SDL_WriteIO(stream, source, (size_t)length) == length) { |
| 147 | result = true; |
| 148 | } else { |
| 149 | SDL_SetError("Resulting file length did not exactly match the source length" ); |
| 150 | } |
| 151 | SDL_CloseIO(stream); |
| 152 | } |
| 153 | SDL_free(fullpath); |
| 154 | } |
| 155 | return result; |
| 156 | } |
| 157 | |
| 158 | static bool GENERIC_CreateStorageDirectory(void *userdata, const char *path) |
| 159 | { |
| 160 | // TODO: Recursively create subdirectories with SDL_CreateDirectory |
| 161 | bool result = false; |
| 162 | |
| 163 | char *fullpath = GENERIC_INTERNAL_CreateFullPath((char *)userdata, path); |
| 164 | if (fullpath) { |
| 165 | result = SDL_CreateDirectory(fullpath); |
| 166 | |
| 167 | SDL_free(fullpath); |
| 168 | } |
| 169 | return result; |
| 170 | } |
| 171 | |
| 172 | static bool GENERIC_RemoveStoragePath(void *userdata, const char *path) |
| 173 | { |
| 174 | bool result = false; |
| 175 | |
| 176 | char *fullpath = GENERIC_INTERNAL_CreateFullPath((char *)userdata, path); |
| 177 | if (fullpath) { |
| 178 | result = SDL_RemovePath(fullpath); |
| 179 | |
| 180 | SDL_free(fullpath); |
| 181 | } |
| 182 | return result; |
| 183 | } |
| 184 | |
| 185 | static bool GENERIC_RenameStoragePath(void *userdata, const char *oldpath, const char *newpath) |
| 186 | { |
| 187 | bool result = false; |
| 188 | |
| 189 | char *fulloldpath = GENERIC_INTERNAL_CreateFullPath((char *)userdata, oldpath); |
| 190 | char *fullnewpath = GENERIC_INTERNAL_CreateFullPath((char *)userdata, newpath); |
| 191 | if (fulloldpath && fullnewpath) { |
| 192 | result = SDL_RenamePath(fulloldpath, fullnewpath); |
| 193 | } |
| 194 | SDL_free(fulloldpath); |
| 195 | SDL_free(fullnewpath); |
| 196 | |
| 197 | return result; |
| 198 | } |
| 199 | |
| 200 | static bool GENERIC_CopyStorageFile(void *userdata, const char *oldpath, const char *newpath) |
| 201 | { |
| 202 | bool result = false; |
| 203 | |
| 204 | char *fulloldpath = GENERIC_INTERNAL_CreateFullPath((char *)userdata, oldpath); |
| 205 | char *fullnewpath = GENERIC_INTERNAL_CreateFullPath((char *)userdata, newpath); |
| 206 | if (fulloldpath && fullnewpath) { |
| 207 | result = SDL_CopyFile(fulloldpath, fullnewpath); |
| 208 | } |
| 209 | SDL_free(fulloldpath); |
| 210 | SDL_free(fullnewpath); |
| 211 | |
| 212 | return result; |
| 213 | } |
| 214 | |
| 215 | static Uint64 GENERIC_GetStorageSpaceRemaining(void *userdata) |
| 216 | { |
| 217 | // TODO: There's totally a way to query a folder root's quota... |
| 218 | return SDL_MAX_UINT64; |
| 219 | } |
| 220 | |
| 221 | static const SDL_StorageInterface GENERIC_title_iface = { |
| 222 | sizeof(SDL_StorageInterface), |
| 223 | GENERIC_CloseStorage, |
| 224 | NULL, // ready |
| 225 | GENERIC_EnumerateStorageDirectory, |
| 226 | GENERIC_GetStoragePathInfo, |
| 227 | GENERIC_ReadStorageFile, |
| 228 | NULL, // write_file |
| 229 | NULL, // mkdir |
| 230 | NULL, // remove |
| 231 | NULL, // rename |
| 232 | NULL, // copy |
| 233 | NULL // space_remaining |
| 234 | }; |
| 235 | |
| 236 | static SDL_Storage *GENERIC_Title_Create(const char *override, SDL_PropertiesID props) |
| 237 | { |
| 238 | SDL_Storage *result = NULL; |
| 239 | char *basepath = NULL; |
| 240 | |
| 241 | if (override != NULL) { |
| 242 | // make sure override has a path separator at the end. If you're not on Windows and used '\\', that's on you. |
| 243 | const size_t slen = SDL_strlen(override); |
| 244 | const bool need_sep = (!slen || ((override[slen-1] != '/') && (override[slen-1] != '\\'))); |
| 245 | if (SDL_asprintf(&basepath, "%s%s" , override, need_sep ? "/" : "" ) == -1) { |
| 246 | return NULL; |
| 247 | } |
| 248 | } else { |
| 249 | const char *base = SDL_GetBasePath(); |
| 250 | basepath = base ? SDL_strdup(base) : NULL; |
| 251 | } |
| 252 | |
| 253 | if (basepath != NULL) { |
| 254 | result = SDL_OpenStorage(&GENERIC_title_iface, basepath); |
| 255 | if (result == NULL) { |
| 256 | SDL_free(basepath); // otherwise CloseStorage will free it. |
| 257 | } |
| 258 | } |
| 259 | |
| 260 | return result; |
| 261 | } |
| 262 | |
| 263 | TitleStorageBootStrap GENERIC_titlebootstrap = { |
| 264 | "generic" , |
| 265 | "SDL generic title storage driver" , |
| 266 | GENERIC_Title_Create |
| 267 | }; |
| 268 | |
| 269 | static const SDL_StorageInterface GENERIC_user_iface = { |
| 270 | sizeof(SDL_StorageInterface), |
| 271 | GENERIC_CloseStorage, |
| 272 | NULL, // ready |
| 273 | GENERIC_EnumerateStorageDirectory, |
| 274 | GENERIC_GetStoragePathInfo, |
| 275 | GENERIC_ReadStorageFile, |
| 276 | GENERIC_WriteStorageFile, |
| 277 | GENERIC_CreateStorageDirectory, |
| 278 | GENERIC_RemoveStoragePath, |
| 279 | GENERIC_RenameStoragePath, |
| 280 | GENERIC_CopyStorageFile, |
| 281 | GENERIC_GetStorageSpaceRemaining |
| 282 | }; |
| 283 | |
| 284 | static SDL_Storage *GENERIC_User_Create(const char *org, const char *app, SDL_PropertiesID props) |
| 285 | { |
| 286 | SDL_Storage *result; |
| 287 | char *prefpath = SDL_GetPrefPath(org, app); |
| 288 | if (prefpath == NULL) { |
| 289 | return NULL; |
| 290 | } |
| 291 | |
| 292 | result = SDL_OpenStorage(&GENERIC_user_iface, prefpath); |
| 293 | if (result == NULL) { |
| 294 | SDL_free(prefpath); // otherwise CloseStorage will free it. |
| 295 | } |
| 296 | return result; |
| 297 | } |
| 298 | |
| 299 | UserStorageBootStrap GENERIC_userbootstrap = { |
| 300 | "generic" , |
| 301 | "SDL generic user storage driver" , |
| 302 | GENERIC_User_Create |
| 303 | }; |
| 304 | |
| 305 | static const SDL_StorageInterface GENERIC_file_iface = { |
| 306 | sizeof(SDL_StorageInterface), |
| 307 | GENERIC_CloseStorage, |
| 308 | NULL, // ready |
| 309 | GENERIC_EnumerateStorageDirectory, |
| 310 | GENERIC_GetStoragePathInfo, |
| 311 | GENERIC_ReadStorageFile, |
| 312 | GENERIC_WriteStorageFile, |
| 313 | GENERIC_CreateStorageDirectory, |
| 314 | GENERIC_RemoveStoragePath, |
| 315 | GENERIC_RenameStoragePath, |
| 316 | GENERIC_CopyStorageFile, |
| 317 | GENERIC_GetStorageSpaceRemaining |
| 318 | }; |
| 319 | |
| 320 | SDL_Storage *GENERIC_OpenFileStorage(const char *path) |
| 321 | { |
| 322 | SDL_Storage *result; |
| 323 | size_t len = 0; |
| 324 | char *basepath = NULL; |
| 325 | |
| 326 | if (path) { |
| 327 | len += SDL_strlen(path); |
| 328 | } |
| 329 | if (len > 0) { |
| 330 | #ifdef SDL_PLATFORM_WINDOWS |
| 331 | const bool appended_separator = (path[len-1] == '/') || (path[len-1] == '\\'); |
| 332 | #else |
| 333 | const bool appended_separator = (path[len-1] == '/'); |
| 334 | #endif |
| 335 | if (appended_separator) { |
| 336 | basepath = SDL_strdup(path); |
| 337 | if (!basepath) { |
| 338 | return NULL; |
| 339 | } |
| 340 | } else { |
| 341 | if (SDL_asprintf(&basepath, "%s/" , path) < 0) { |
| 342 | return NULL; |
| 343 | } |
| 344 | } |
| 345 | } |
| 346 | result = SDL_OpenStorage(&GENERIC_file_iface, basepath); |
| 347 | if (result == NULL) { |
| 348 | SDL_free(basepath); |
| 349 | } |
| 350 | return result; |
| 351 | } |
| 352 | |