| 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_filesystem_c.h" | 
| 25 | #include "SDL_sysfilesystem.h" | 
| 26 | #include "../stdlib/SDL_sysstdlib.h" | 
| 27 |  | 
| 28 | bool SDL_RemovePath(const char *path) | 
| 29 | { | 
| 30 |     if (!path) { | 
| 31 |         return SDL_InvalidParamError("path" ); | 
| 32 |     } | 
| 33 |     return SDL_SYS_RemovePath(path); | 
| 34 | } | 
| 35 |  | 
| 36 | bool SDL_RenamePath(const char *oldpath, const char *newpath) | 
| 37 | { | 
| 38 |     if (!oldpath) { | 
| 39 |         return SDL_InvalidParamError("oldpath" ); | 
| 40 |     } else if (!newpath) { | 
| 41 |         return SDL_InvalidParamError("newpath" ); | 
| 42 |     } | 
| 43 |     return SDL_SYS_RenamePath(oldpath, newpath); | 
| 44 | } | 
| 45 |  | 
| 46 | bool SDL_CopyFile(const char *oldpath, const char *newpath) | 
| 47 | { | 
| 48 |     if (!oldpath) { | 
| 49 |         return SDL_InvalidParamError("oldpath" ); | 
| 50 |     } else if (!newpath) { | 
| 51 |         return SDL_InvalidParamError("newpath" ); | 
| 52 |     } | 
| 53 |     return SDL_SYS_CopyFile(oldpath, newpath); | 
| 54 | } | 
| 55 |  | 
| 56 | bool SDL_CreateDirectory(const char *path) | 
| 57 | { | 
| 58 |     if (!path) { | 
| 59 |         return SDL_InvalidParamError("path" ); | 
| 60 |     } | 
| 61 |  | 
| 62 |     bool retval = SDL_SYS_CreateDirectory(path); | 
| 63 |     if (!retval && *path) {  // maybe we're missing parent directories? | 
| 64 |         char *parents = SDL_strdup(path); | 
| 65 |         if (!parents) { | 
| 66 |             return false;  // oh well. | 
| 67 |         } | 
| 68 |  | 
| 69 |         // in case there was a separator at the end of the path and it was | 
| 70 |         // upsetting something, chop it off. | 
| 71 |         const size_t slen = SDL_strlen(parents); | 
| 72 |         #ifdef SDL_PLATFORM_WINDOWS | 
| 73 |         if ((parents[slen - 1] == '/') || (parents[slen - 1] == '\\')) | 
| 74 |         #else | 
| 75 |         if (parents[slen - 1] == '/') | 
| 76 |         #endif | 
| 77 |         { | 
| 78 |             parents[slen - 1] = '\0'; | 
| 79 |             retval = SDL_SYS_CreateDirectory(parents); | 
| 80 |         } | 
| 81 |  | 
| 82 |         if (!retval) { | 
| 83 |             for (char *ptr = parents; *ptr; ptr++) { | 
| 84 |                 const char ch = *ptr; | 
| 85 |                 #ifdef SDL_PLATFORM_WINDOWS | 
| 86 |                 const bool issep = (ch == '/') || (ch == '\\'); | 
| 87 |                 if (issep && ((ptr - parents) == 2) && (parents[1] == ':')) { | 
| 88 |                     continue;  // it's just the drive letter, skip it. | 
| 89 |                 } | 
| 90 |                 #else | 
| 91 |                 const bool issep = (ch == '/'); | 
| 92 |                 if (issep && ((ptr - parents) == 0)) { | 
| 93 |                     continue; // it's just the root directory, skip it. | 
| 94 |                 } | 
| 95 |                 #endif | 
| 96 |  | 
| 97 |                 if (issep) { | 
| 98 |                     *ptr = '\0'; | 
| 99 |                     // (this does not fail if the path already exists as a directory.) | 
| 100 |                     retval = SDL_SYS_CreateDirectory(parents); | 
| 101 |                     if (!retval) {  // still failing when making parents? Give up. | 
| 102 |                         break; | 
| 103 |                     } | 
| 104 |                     *ptr = ch; | 
| 105 |                 } | 
| 106 |             } | 
| 107 |  | 
| 108 |             // last chance: did it work this time? | 
| 109 |             retval = SDL_SYS_CreateDirectory(parents); | 
| 110 |         } | 
| 111 |  | 
| 112 |         SDL_free(parents); | 
| 113 |     } | 
| 114 |     return retval; | 
| 115 | } | 
| 116 |  | 
| 117 | bool SDL_EnumerateDirectory(const char *path, SDL_EnumerateDirectoryCallback callback, void *userdata) | 
| 118 | { | 
| 119 |     if (!path) { | 
| 120 |         return SDL_InvalidParamError("path" ); | 
| 121 |     } else if (!callback) { | 
| 122 |         return SDL_InvalidParamError("callback" ); | 
| 123 |     } | 
| 124 |     return SDL_SYS_EnumerateDirectory(path, callback, userdata); | 
| 125 | } | 
| 126 |  | 
| 127 | bool SDL_GetPathInfo(const char *path, SDL_PathInfo *info) | 
| 128 | { | 
| 129 |     SDL_PathInfo dummy; | 
| 130 |  | 
| 131 |     if (!info) { | 
| 132 |         info = &dummy; | 
| 133 |     } | 
| 134 |     SDL_zerop(info); | 
| 135 |  | 
| 136 |     if (!path) { | 
| 137 |         return SDL_InvalidParamError("path" ); | 
| 138 |     } | 
| 139 |  | 
| 140 |     return SDL_SYS_GetPathInfo(path, info); | 
| 141 | } | 
| 142 |  | 
| 143 | static bool EverythingMatch(const char *pattern, const char *str, bool *matched_to_dir) | 
| 144 | { | 
| 145 |     SDL_assert(pattern == NULL); | 
| 146 |     SDL_assert(str != NULL); | 
| 147 |     SDL_assert(matched_to_dir != NULL); | 
| 148 |  | 
| 149 |     *matched_to_dir = true; | 
| 150 |     return true;  // everything matches! | 
| 151 | } | 
| 152 |  | 
| 153 | // this is just '*' and '?', with '/' matching nothing. | 
| 154 | static bool WildcardMatch(const char *pattern, const char *str, bool *matched_to_dir) | 
| 155 | { | 
| 156 |     SDL_assert(pattern != NULL); | 
| 157 |     SDL_assert(str != NULL); | 
| 158 |     SDL_assert(matched_to_dir != NULL); | 
| 159 |  | 
| 160 |     const char *str_backtrack = NULL; | 
| 161 |     const char *pattern_backtrack = NULL; | 
| 162 |     char sch_backtrack = 0; | 
| 163 |     char sch = *str; | 
| 164 |     char pch = *pattern; | 
| 165 |  | 
| 166 |     while (sch) { | 
| 167 |         if (pch == '*') { | 
| 168 |             str_backtrack = str; | 
| 169 |             pattern_backtrack = ++pattern; | 
| 170 |             sch_backtrack = sch; | 
| 171 |             pch = *pattern; | 
| 172 |         } else if (pch == sch) { | 
| 173 |             if (pch == '/') { | 
| 174 |                 str_backtrack = pattern_backtrack = NULL; | 
| 175 |             } | 
| 176 |             sch = *(++str); | 
| 177 |             pch = *(++pattern); | 
| 178 |         } else if ((pch == '?') && (sch != '/')) {  // end of string (checked at `while`) or path separator do not match '?'. | 
| 179 |             sch = *(++str); | 
| 180 |             pch = *(++pattern); | 
| 181 |         } else if (!pattern_backtrack || (sch_backtrack == '/')) { // we didn't have a match. Are we in a '*' and NOT on a path separator? Keep going. Otherwise, fail. | 
| 182 |             *matched_to_dir = false; | 
| 183 |             return false; | 
| 184 |         } else {  // still here? Wasn't a match, but we're definitely in a '*' pattern. | 
| 185 |             str = ++str_backtrack; | 
| 186 |             pattern = pattern_backtrack; | 
| 187 |             sch_backtrack = sch; | 
| 188 |             sch = *str; | 
| 189 |             pch = *pattern; | 
| 190 |         } | 
| 191 |  | 
| 192 |         #ifdef SDL_PLATFORM_WINDOWS | 
| 193 |         if (sch == '\\') { | 
| 194 |             sch = '/'; | 
| 195 |         } | 
| 196 |         #endif | 
| 197 |     } | 
| 198 |  | 
| 199 |     // '*' at the end can be ignored, they are allowed to match nothing. | 
| 200 |     while (pch == '*') { | 
| 201 |         pch = *(++pattern); | 
| 202 |     } | 
| 203 |  | 
| 204 |     *matched_to_dir = ((pch == '/') || (pch == '\0'));  // end of string and the pattern is complete or failed at a '/'? We should descend into this directory. | 
| 205 |  | 
| 206 |     return (pch == '\0');  // survived the whole pattern? That's a match! | 
| 207 | } | 
| 208 |  | 
| 209 |  | 
| 210 | // Note that this will currently encode illegal codepoints: UTF-16 surrogates, 0xFFFE, and 0xFFFF. | 
| 211 | // and a codepoint > 0x10FFFF will fail the same as if there wasn't enough memory. | 
| 212 | // clean this up if you want to move this to SDL_string.c. | 
| 213 | static size_t EncodeCodepointToUtf8(char *ptr, Uint32 cp, size_t remaining) | 
| 214 | { | 
| 215 |     if (cp < 0x80) {  // fits in a single UTF-8 byte. | 
| 216 |         if (remaining) { | 
| 217 |             *ptr = (char) cp; | 
| 218 |             return 1; | 
| 219 |         } | 
| 220 |     } else if (cp < 0x800) {  // fits in 2 bytes. | 
| 221 |         if (remaining >= 2) { | 
| 222 |             ptr[0] = (char) ((cp >> 6) | 128 | 64); | 
| 223 |             ptr[1] = (char) (cp & 0x3F) | 128; | 
| 224 |             return 2; | 
| 225 |         } | 
| 226 |     } else if (cp < 0x10000) { // fits in 3 bytes. | 
| 227 |         if (remaining >= 3) { | 
| 228 |             ptr[0] = (char) ((cp >> 12) | 128 | 64 | 32); | 
| 229 |             ptr[1] = (char) ((cp >> 6) & 0x3F) | 128; | 
| 230 |             ptr[2] = (char) (cp & 0x3F) | 128; | 
| 231 |             return 3; | 
| 232 |         } | 
| 233 |     } else if (cp <= 0x10FFFF) {  // fits in 4 bytes. | 
| 234 |         if (remaining >= 4) { | 
| 235 |             ptr[0] = (char) ((cp >> 18) | 128 | 64 | 32 | 16); | 
| 236 |             ptr[1] = (char) ((cp >> 12) & 0x3F) | 128; | 
| 237 |             ptr[2] = (char) ((cp >> 6) & 0x3F) | 128; | 
| 238 |             ptr[3] = (char) (cp & 0x3F) | 128; | 
| 239 |             return 4; | 
| 240 |         } | 
| 241 |     } | 
| 242 |  | 
| 243 |     return 0; | 
| 244 | } | 
| 245 |  | 
| 246 | static char *CaseFoldUtf8String(const char *fname) | 
| 247 | { | 
| 248 |     SDL_assert(fname != NULL); | 
| 249 |     const size_t allocation = (SDL_strlen(fname) + 1) * 3 * 4; | 
| 250 |     char *result = (char *) SDL_malloc(allocation);  // lazy: just allocating the max needed. | 
| 251 |     if (!result) { | 
| 252 |         return NULL; | 
| 253 |     } | 
| 254 |  | 
| 255 |     Uint32 codepoint; | 
| 256 |     char *ptr = result; | 
| 257 |     size_t remaining = allocation; | 
| 258 |     while ((codepoint = SDL_StepUTF8(&fname, NULL)) != 0) { | 
| 259 |         Uint32 folded[3]; | 
| 260 |         const int num_folded = SDL_CaseFoldUnicode(codepoint, folded); | 
| 261 |         SDL_assert(num_folded > 0); | 
| 262 |         SDL_assert(num_folded <= SDL_arraysize(folded)); | 
| 263 |         for (int i = 0; i < num_folded; i++) { | 
| 264 |             SDL_assert(remaining > 0); | 
| 265 |             const size_t rc = EncodeCodepointToUtf8(ptr, folded[i], remaining); | 
| 266 |             SDL_assert(rc > 0); | 
| 267 |             SDL_assert(rc < remaining); | 
| 268 |             remaining -= rc; | 
| 269 |             ptr += rc; | 
| 270 |         } | 
| 271 |     } | 
| 272 |  | 
| 273 |     SDL_assert(remaining > 0); | 
| 274 |     remaining--; | 
| 275 |     *ptr = '\0'; | 
| 276 |  | 
| 277 |     if (remaining > 0) { | 
| 278 |         SDL_assert(allocation > remaining); | 
| 279 |         ptr = (char *)SDL_realloc(result, allocation - remaining);  // shrink it down. | 
| 280 |         if (ptr) {  // shouldn't fail, but if it does, `result` is still valid. | 
| 281 |             result = ptr; | 
| 282 |         } | 
| 283 |     } | 
| 284 |  | 
| 285 |     return result; | 
| 286 | } | 
| 287 |  | 
| 288 |  | 
| 289 | typedef struct GlobDirCallbackData | 
| 290 | { | 
| 291 |     bool (*matcher)(const char *pattern, const char *str, bool *matched_to_dir); | 
| 292 |     const char *pattern; | 
| 293 |     int num_entries; | 
| 294 |     SDL_GlobFlags flags; | 
| 295 |     SDL_GlobEnumeratorFunc enumerator; | 
| 296 |     SDL_GlobGetPathInfoFunc getpathinfo; | 
| 297 |     void *fsuserdata; | 
| 298 |     size_t basedirlen; | 
| 299 |     SDL_IOStream *string_stream; | 
| 300 | } GlobDirCallbackData; | 
| 301 |  | 
| 302 | static SDL_EnumerationResult SDLCALL GlobDirectoryCallback(void *userdata, const char *dirname, const char *fname) | 
| 303 | { | 
| 304 |     SDL_assert(userdata != NULL); | 
| 305 |     SDL_assert(dirname != NULL); | 
| 306 |     SDL_assert(fname != NULL); | 
| 307 |  | 
| 308 |     //SDL_Log("GlobDirectoryCallback('%s', '%s')", dirname, fname); | 
| 309 |  | 
| 310 |     GlobDirCallbackData *data = (GlobDirCallbackData *) userdata; | 
| 311 |  | 
| 312 |     // !!! FIXME: if we're careful, we can keep a single buffer in `data` that we push and pop paths off the end of as we walk the tree, | 
| 313 |     // !!! FIXME: and only casefold the new pieces instead of allocating and folding full paths for all of this. | 
| 314 |  | 
| 315 |     char *fullpath = NULL; | 
| 316 |     if (SDL_asprintf(&fullpath, "%s%s" , dirname, fname) < 0) { | 
| 317 |         return SDL_ENUM_FAILURE; | 
| 318 |     } | 
| 319 |  | 
| 320 |     char *folded = NULL; | 
| 321 |     if (data->flags & SDL_GLOB_CASEINSENSITIVE) { | 
| 322 |         folded = CaseFoldUtf8String(fullpath); | 
| 323 |         if (!folded) { | 
| 324 |             return SDL_ENUM_FAILURE; | 
| 325 |         } | 
| 326 |     } | 
| 327 |  | 
| 328 |     bool matched_to_dir = false; | 
| 329 |     const bool matched = data->matcher(data->pattern, (folded ? folded : fullpath) + data->basedirlen, &matched_to_dir); | 
| 330 |     //SDL_Log("GlobDirectoryCallback: Considered %spath='%s' vs pattern='%s': %smatched (matched_to_dir=%s)", folded ? "(folded) " : "", (folded ? folded : fullpath) + data->basedirlen, data->pattern, matched ? "" : "NOT ", matched_to_dir ? "TRUE" : "FALSE"); | 
| 331 |     SDL_free(folded); | 
| 332 |  | 
| 333 |     if (matched) { | 
| 334 |         const char *subpath = fullpath + data->basedirlen; | 
| 335 |         const size_t slen = SDL_strlen(subpath) + 1; | 
| 336 |         if (SDL_WriteIO(data->string_stream, subpath, slen) != slen) { | 
| 337 |             SDL_free(fullpath); | 
| 338 |             return SDL_ENUM_FAILURE;  // stop enumerating, return failure to the app. | 
| 339 |         } | 
| 340 |         data->num_entries++; | 
| 341 |     } | 
| 342 |  | 
| 343 |     SDL_EnumerationResult result = SDL_ENUM_CONTINUE;  // keep enumerating by default. | 
| 344 |     if (matched_to_dir) { | 
| 345 |         SDL_PathInfo info; | 
| 346 |         if (data->getpathinfo(fullpath, &info, data->fsuserdata) && (info.type == SDL_PATHTYPE_DIRECTORY)) { | 
| 347 |             //SDL_Log("GlobDirectoryCallback: Descending into subdir '%s'", fname); | 
| 348 |             if (!data->enumerator(fullpath, GlobDirectoryCallback, data, data->fsuserdata)) { | 
| 349 |                 result = SDL_ENUM_FAILURE; | 
| 350 |             } | 
| 351 |         } | 
| 352 |     } | 
| 353 |  | 
| 354 |     SDL_free(fullpath); | 
| 355 |  | 
| 356 |     return result; | 
| 357 | } | 
| 358 |  | 
| 359 | char **SDL_InternalGlobDirectory(const char *path, const char *pattern, SDL_GlobFlags flags, int *count, SDL_GlobEnumeratorFunc enumerator, SDL_GlobGetPathInfoFunc getpathinfo, void *userdata) | 
| 360 | { | 
| 361 |     int dummycount; | 
| 362 |     if (!count) { | 
| 363 |         count = &dummycount; | 
| 364 |     } | 
| 365 |     *count = 0; | 
| 366 |  | 
| 367 |     if (!path) { | 
| 368 |         SDL_InvalidParamError("path" ); | 
| 369 |         return NULL; | 
| 370 |     } | 
| 371 |  | 
| 372 |     // if path ends with any slash, chop them off, so we don't confuse the pattern matcher later. | 
| 373 |     char *pathcpy = NULL; | 
| 374 |     size_t pathlen = SDL_strlen(path); | 
| 375 |     if ((pathlen > 1) && ((path[pathlen-1] == '/') || (path[pathlen-1] == '\\'))) { | 
| 376 |         pathcpy = SDL_strdup(path); | 
| 377 |         if (!pathcpy) { | 
| 378 |             return NULL; | 
| 379 |         } | 
| 380 |         char *ptr = &pathcpy[pathlen-1]; | 
| 381 |         while ((ptr >= pathcpy) && ((*ptr == '/') || (*ptr == '\\'))) { | 
| 382 |             *(ptr--) = '\0'; | 
| 383 |         } | 
| 384 |         path = pathcpy; | 
| 385 |     } | 
| 386 |  | 
| 387 |     if (!pattern) { | 
| 388 |         flags &= ~SDL_GLOB_CASEINSENSITIVE;  // avoid some unnecessary allocations and work later. | 
| 389 |     } | 
| 390 |  | 
| 391 |     char *folded = NULL; | 
| 392 |     if (flags & SDL_GLOB_CASEINSENSITIVE) { | 
| 393 |         SDL_assert(pattern != NULL); | 
| 394 |         folded = CaseFoldUtf8String(pattern); | 
| 395 |         if (!folded) { | 
| 396 |             SDL_free(pathcpy); | 
| 397 |             return NULL; | 
| 398 |         } | 
| 399 |     } | 
| 400 |  | 
| 401 |     GlobDirCallbackData data; | 
| 402 |     SDL_zero(data); | 
| 403 |     data.string_stream = SDL_IOFromDynamicMem(); | 
| 404 |     if (!data.string_stream) { | 
| 405 |         SDL_free(folded); | 
| 406 |         SDL_free(pathcpy); | 
| 407 |         return NULL; | 
| 408 |     } | 
| 409 |  | 
| 410 |     if (!pattern) { | 
| 411 |         data.matcher = EverythingMatch;  // no pattern? Everything matches. | 
| 412 |  | 
| 413 |     // !!! FIXME | 
| 414 |     //} else if (flags & SDL_GLOB_GITIGNORE) { | 
| 415 |     //    data.matcher = GitIgnoreMatch; | 
| 416 |  | 
| 417 |     } else { | 
| 418 |         data.matcher = WildcardMatch; | 
| 419 |     } | 
| 420 |  | 
| 421 |     data.pattern = folded ? folded : pattern; | 
| 422 |     data.flags = flags; | 
| 423 |     data.enumerator = enumerator; | 
| 424 |     data.getpathinfo = getpathinfo; | 
| 425 |     data.fsuserdata = userdata; | 
| 426 |     data.basedirlen = *path ? (SDL_strlen(path) + 1) : 0;  // +1 for the '/' we'll be adding. | 
| 427 |  | 
| 428 |  | 
| 429 |     char **result = NULL; | 
| 430 |     if (data.enumerator(path, GlobDirectoryCallback, &data, data.fsuserdata)) { | 
| 431 |         const size_t streamlen = (size_t) SDL_GetIOSize(data.string_stream); | 
| 432 |         const size_t buflen = streamlen + ((data.num_entries + 1) * sizeof (char *));  // +1 for NULL terminator at end of array. | 
| 433 |         result = (char **) SDL_malloc(buflen); | 
| 434 |         if (result) { | 
| 435 |             if (data.num_entries > 0) { | 
| 436 |                 Sint64 iorc = SDL_SeekIO(data.string_stream, 0, SDL_IO_SEEK_SET); | 
| 437 |                 SDL_assert(iorc == 0);  // this should never fail for a memory stream! | 
| 438 |                 char *ptr = (char *) (result + (data.num_entries + 1)); | 
| 439 |                 iorc = SDL_ReadIO(data.string_stream, ptr, streamlen); | 
| 440 |                 SDL_assert(iorc == (Sint64) streamlen);  // this should never fail for a memory stream! | 
| 441 |                 for (int i = 0; i < data.num_entries; i++) { | 
| 442 |                     result[i] = ptr; | 
| 443 |                     ptr += SDL_strlen(ptr) + 1; | 
| 444 |                 } | 
| 445 |             } | 
| 446 |             result[data.num_entries] = NULL;  // NULL terminate the list. | 
| 447 |             *count = data.num_entries; | 
| 448 |         } | 
| 449 |     } | 
| 450 |  | 
| 451 |     SDL_CloseIO(data.string_stream); | 
| 452 |     SDL_free(folded); | 
| 453 |     SDL_free(pathcpy); | 
| 454 |  | 
| 455 |     return result; | 
| 456 | } | 
| 457 |  | 
| 458 | static bool GlobDirectoryGetPathInfo(const char *path, SDL_PathInfo *info, void *userdata) | 
| 459 | { | 
| 460 |     return SDL_GetPathInfo(path, info); | 
| 461 | } | 
| 462 |  | 
| 463 | static bool GlobDirectoryEnumerator(const char *path, SDL_EnumerateDirectoryCallback cb, void *cbuserdata, void *userdata) | 
| 464 | { | 
| 465 |     return SDL_EnumerateDirectory(path, cb, cbuserdata); | 
| 466 | } | 
| 467 |  | 
| 468 | char **SDL_GlobDirectory(const char *path, const char *pattern, SDL_GlobFlags flags, int *count) | 
| 469 | { | 
| 470 |     //SDL_Log("SDL_GlobDirectory('%s', '%s') ...", path, pattern); | 
| 471 |     return SDL_InternalGlobDirectory(path, pattern, flags, count, GlobDirectoryEnumerator, GlobDirectoryGetPathInfo, NULL); | 
| 472 | } | 
| 473 |  | 
| 474 |  | 
| 475 | static char *CachedBasePath = NULL; | 
| 476 |  | 
| 477 | const char *SDL_GetBasePath(void) | 
| 478 | { | 
| 479 |     if (!CachedBasePath) { | 
| 480 |         CachedBasePath = SDL_SYS_GetBasePath(); | 
| 481 |     } | 
| 482 |     return CachedBasePath; | 
| 483 | } | 
| 484 |  | 
| 485 |  | 
| 486 | static char *CachedUserFolders[SDL_FOLDER_COUNT]; | 
| 487 |  | 
| 488 | const char *SDL_GetUserFolder(SDL_Folder folder) | 
| 489 | { | 
| 490 |     const int idx = (int) folder; | 
| 491 |     if ((idx < 0) || (idx >= SDL_arraysize(CachedUserFolders))) { | 
| 492 |         SDL_InvalidParamError("folder" ); | 
| 493 |         return NULL; | 
| 494 |     } | 
| 495 |  | 
| 496 |     if (!CachedUserFolders[idx]) { | 
| 497 |         CachedUserFolders[idx] = SDL_SYS_GetUserFolder(folder); | 
| 498 |     } | 
| 499 |     return CachedUserFolders[idx]; | 
| 500 | } | 
| 501 |  | 
| 502 |  | 
| 503 | char *SDL_GetPrefPath(const char *org, const char *app) | 
| 504 | { | 
| 505 |     return SDL_SYS_GetPrefPath(org, app); | 
| 506 | } | 
| 507 |  | 
| 508 | char *SDL_GetCurrentDirectory(void) | 
| 509 | { | 
| 510 |     return SDL_SYS_GetCurrentDirectory(); | 
| 511 | } | 
| 512 |  | 
| 513 | void SDL_InitFilesystem(void) | 
| 514 | { | 
| 515 | } | 
| 516 |  | 
| 517 | void SDL_QuitFilesystem(void) | 
| 518 | { | 
| 519 |     if (CachedBasePath) { | 
| 520 |         SDL_free(CachedBasePath); | 
| 521 |         CachedBasePath = NULL; | 
| 522 |     } | 
| 523 |     for (int i = 0; i < SDL_arraysize(CachedUserFolders); i++) { | 
| 524 |         if (CachedUserFolders[i]) { | 
| 525 |             SDL_free(CachedUserFolders[i]); | 
| 526 |             CachedUserFolders[i] = NULL; | 
| 527 |         } | 
| 528 |     } | 
| 529 | } | 
| 530 |  | 
| 531 |  |