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
28bool SDL_RemovePath(const char *path)
29{
30 if (!path) {
31 return SDL_InvalidParamError("path");
32 }
33 return SDL_SYS_RemovePath(path);
34}
35
36bool 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
46bool 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
56bool 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
117bool 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
127bool 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
143static 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.
154static 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.
213static 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
246static 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
289typedef 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
302static 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
359char **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
458static bool GlobDirectoryGetPathInfo(const char *path, SDL_PathInfo *info, void *userdata)
459{
460 return SDL_GetPathInfo(path, info);
461}
462
463static bool GlobDirectoryEnumerator(const char *path, SDL_EnumerateDirectoryCallback cb, void *cbuserdata, void *userdata)
464{
465 return SDL_EnumerateDirectory(path, cb, cbuserdata);
466}
467
468char **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
475static char *CachedBasePath = NULL;
476
477const char *SDL_GetBasePath(void)
478{
479 if (!CachedBasePath) {
480 CachedBasePath = SDL_SYS_GetBasePath();
481 }
482 return CachedBasePath;
483}
484
485
486static char *CachedUserFolders[SDL_FOLDER_COUNT];
487
488const 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
503char *SDL_GetPrefPath(const char *org, const char *app)
504{
505 return SDL_SYS_GetPrefPath(org, app);
506}
507
508char *SDL_GetCurrentDirectory(void)
509{
510 return SDL_SYS_GetCurrentDirectory();
511}
512
513void SDL_InitFilesystem(void)
514{
515}
516
517void 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