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 | |