Browse Source

filesystem: Added SDL_GlobDirectory() and SDL_GlobStorageDirectory().

Fixes #9287.
Ryan C. Gordon 1 year ago
parent
commit
764207d873

+ 41 - 1
include/SDL3/SDL_filesystem.h

@@ -272,7 +272,11 @@ extern DECLSPEC int SDLCALL SDL_CreateDirectory(const char *path);
 typedef int (SDLCALL *SDL_EnumerateDirectoryCallback)(void *userdata, const char *dirname, const char *fname);
 
 /**
- * Enumerate a directory.
+ * Enumerate a directory through a callback function.
+ *
+ * This function provides every directory entry through an app-provided
+ * callback, called once for each directory entry, until all results have
+ * been provided or the callback returns <= 0.
  *
  * \param path the path of the directory to enumerate
  * \param callback a function that is called for each entry in the directory
@@ -320,6 +324,42 @@ extern DECLSPEC int SDLCALL SDL_RenamePath(const char *oldpath, const char *newp
  */
 extern DECLSPEC int SDLCALL SDL_GetPathInfo(const char *path, SDL_PathInfo *info);
 
+
+#define SDL_GLOBDIR_CASEINSENSITIVE (1 << 0)
+
+/**
+ * Enumerate a directory tree, filtered by pattern, and return a list.
+ *
+ * Files are filtered out if they don't match the string in `pattern`, which
+ * may contain wildcard characters '*' (match everything) and '?' (match one
+ * character). If pattern is NULL, no filtering is done and all results are
+ * returned. Subdirectories are permitted, and are specified with a path
+ * separator of '/'. Wildcard characters '*' and '?' never match a path
+ * separator.
+ *
+ * `flags` may be set to SDL_GLOBDIR_CASEINSENSITIVE to make the pattern
+ * matching case-insensitive.
+ *
+ * The returned array is always NULL-terminated, for your iterating
+ * convenience, but if `count` is non-NULL, on return it will contain the
+ * number of items in the array, not counting the NULL terminator.
+ *
+ * You must free the returned pointer with SDL_free() when done with it.
+ *
+ * \param path the path of the directory to enumerate
+ * \param pattern the pattern that files in the directory must match. Can be NULL.
+ * \param flags `SDL_GLOBDIR_*` bitflags that affect this search.
+ * \param count on return, will be set to the number of items in the returned array. Can be NULL.
+ * \returns an array of strings on success or NULL on failure; call
+ *          SDL_GetError() for more information. The caller should pass the
+ *          returned pointer to SDL_free when done with it.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \threadsafety It is safe to call this function from any thread.
+ */
+extern DECLSPEC char **SDLCALL SDL_GlobDirectory(const char *path, const char *pattern, Uint32 flags, int *count);
+
 /* Ends C function definitions when using C++ */
 #ifdef __cplusplus
 }

+ 39 - 1
include/SDL3/SDL_storage.h

@@ -268,7 +268,11 @@ extern DECLSPEC int SDL_WriteStorageFile(SDL_Storage *storage, const char *path,
 extern DECLSPEC int SDLCALL SDL_CreateStorageDirectory(SDL_Storage *storage, const char *path);
 
 /**
- * Enumerate a directory in a storage container.
+ * Enumerate a directory in a storage container through a callback function.
+ *
+ * This function provides every directory entry through an app-provided
+ * callback, called once for each directory entry, until all results have
+ * been provided or the callback returns <= 0.
  *
  * \param storage a storage container
  * \param path the path of the directory to enumerate
@@ -341,6 +345,40 @@ extern DECLSPEC int SDLCALL SDL_GetStoragePathInfo(SDL_Storage *storage, const c
  */
 extern DECLSPEC Uint64 SDLCALL SDL_GetStorageSpaceRemaining(SDL_Storage *storage);
 
+/**
+ * Enumerate a directory tree, filtered by pattern, and return a list.
+ *
+ * Files are filtered out if they don't match the string in `pattern`, which
+ * may contain wildcard characters '*' (match everything) and '?' (match one
+ * character). If pattern is NULL, no filtering is done and all results are
+ * returned. Subdirectories are permitted, and are specified with a path
+ * separator of '/'. Wildcard characters '*' and '?' never match a path
+ * separator.
+ *
+ * `flags` may be set to SDL_GLOBDIR_CASEINSENSITIVE to make the pattern
+ * matching case-insensitive.
+ *
+ * The returned array is always NULL-terminated, for your iterating
+ * convenience, but if `count` is non-NULL, on return it will contain the
+ * number of items in the array, not counting the NULL terminator.
+ *
+ * You must free the returned pointer with SDL_free() when done with it.
+ *
+ * \param storage a storage container
+ * \param path the path of the directory to enumerate
+ * \param pattern the pattern that files in the directory must match. Can be NULL.
+ * \param flags `SDL_GLOBDIR_*` bitflags that affect this search.
+ * \param count on return, will be set to the number of items in the returned array. Can be NULL.
+ * \returns an array of strings on success or NULL on failure; call
+ *          SDL_GetError() for more information. The caller should pass the
+ *          returned pointer to SDL_free when done with it.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \threadsafety It is safe to call this function from any thread, assuming the `storage` object is thread-safe.
+ */
+extern DECLSPEC char **SDLCALL SDL_GlobStorageDirectory(SDL_Storage *storage, const char *path, const char *pattern, Uint32 flags, int *count);
+
 /* Ends C function definitions when using C++ */
 #ifdef __cplusplus
 }

+ 2 - 0
src/dynapi/SDL_dynapi.sym

@@ -481,6 +481,8 @@ SDL3_0.0.0 {
     SDL_GetWindowSizeInPixels;
     SDL_GetWindowSurface;
     SDL_GetWindowTitle;
+    SDL_GlobDirectory;
+    SDL_GlobStorageDirectory;
     SDL_HapticEffectSupported;
     SDL_HapticRumbleSupported;
     SDL_HasARMSIMD;

+ 2 - 0
src/dynapi/SDL_dynapi_overrides.h

@@ -506,6 +506,8 @@
 #define SDL_GetWindowSizeInPixels SDL_GetWindowSizeInPixels_REAL
 #define SDL_GetWindowSurface SDL_GetWindowSurface_REAL
 #define SDL_GetWindowTitle SDL_GetWindowTitle_REAL
+#define SDL_GlobDirectory SDL_GlobDirectory_REAL
+#define SDL_GlobStorageDirectory SDL_GlobStorageDirectory_REAL
 #define SDL_HapticEffectSupported SDL_HapticEffectSupported_REAL
 #define SDL_HapticRumbleSupported SDL_HapticRumbleSupported_REAL
 #define SDL_HasARMSIMD SDL_HasARMSIMD_REAL

+ 2 - 0
src/dynapi/SDL_dynapi_procs.h

@@ -537,6 +537,8 @@ SDL_DYNAPI_PROC(int,SDL_GetWindowSize,(SDL_Window *a, int *b, int *c),(a,b,c),re
 SDL_DYNAPI_PROC(int,SDL_GetWindowSizeInPixels,(SDL_Window *a, int *b, int *c),(a,b,c),return)
 SDL_DYNAPI_PROC(SDL_Surface*,SDL_GetWindowSurface,(SDL_Window *a),(a),return)
 SDL_DYNAPI_PROC(const char*,SDL_GetWindowTitle,(SDL_Window *a),(a),return)
+SDL_DYNAPI_PROC(char**,SDL_GlobDirectory,(const char *a, const char *b, Uint32 c, int *d),(a,b,c,d),return)
+SDL_DYNAPI_PROC(char**,SDL_GlobStorageDirectory,(SDL_Storage *a, const char *b, const char *c, Uint32 d, int *e),(a,b,c,d,e),return)
 SDL_DYNAPI_PROC(SDL_bool,SDL_HapticEffectSupported,(SDL_Haptic *a, const SDL_HapticEffect *b),(a,b),return)
 SDL_DYNAPI_PROC(SDL_bool,SDL_HapticRumbleSupported,(SDL_Haptic *a),(a),return)
 SDL_DYNAPI_PROC(SDL_bool,SDL_HasARMSIMD,(void),(),return)

+ 277 - 0
src/filesystem/SDL_filesystem.c

@@ -21,6 +21,7 @@
 
 #include "SDL_internal.h"
 #include "SDL_sysfilesystem.h"
+#include "../stdlib/SDL_sysstdlib.h"
 
 int SDL_RemovePath(const char *path)
 {
@@ -74,3 +75,279 @@ int SDL_GetPathInfo(const char *path, SDL_PathInfo *info)
 
     return SDL_SYS_GetPathInfo(path, info);
 }
+
+static SDL_bool EverythingMatch(const char *pattern, const char *str, SDL_bool *matched_to_dir)
+{
+    SDL_assert(pattern == NULL);
+    SDL_assert(str != NULL);
+    SDL_assert(matched_to_dir != NULL);
+
+    *matched_to_dir = SDL_TRUE;
+    return SDL_TRUE;  // everything matches!
+}
+
+// this is just '*' and '?', with '/' matching nothing.
+static SDL_bool WildcardMatch(const char *pattern, const char *str, SDL_bool *matched_to_dir)
+{
+    SDL_assert(pattern != NULL);
+    SDL_assert(str != NULL);
+    SDL_assert(matched_to_dir != NULL);
+
+    const char *str_backtrack = NULL;
+    const char *pattern_backtrack = NULL;
+    char sch_backtrack = 0;
+    char sch = *str;
+    char pch = *pattern;
+
+    while (sch) {
+        if (pch == '*') {
+            str_backtrack = str;
+            pattern_backtrack = ++pattern;
+            sch_backtrack = sch;
+            pch = *pattern;
+        } else if (pch == sch) {
+            if (pch == '/') {
+                str_backtrack = pattern_backtrack = NULL;
+            }
+            sch = *(++str);
+            pch = *(++pattern);
+        } else if ((pch == '?') && (sch != '/')) {  // end of string (checked at `while`) or path separator do not match '?'.
+            sch = *(++str);
+            pch = *(++pattern);
+        } 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.
+            *matched_to_dir = SDL_FALSE;
+            return SDL_FALSE;
+        } else {  // still here? Wasn't a match, but we're definitely in a '*' pattern.
+            str = ++str_backtrack;
+            pattern = pattern_backtrack;
+            sch_backtrack = sch;
+            sch = *str;
+            pch = *pattern;
+        }
+    }
+
+    // '*' at the end can be ignored, they are allowed to match nothing.
+    while (pch == '*') {
+        pch = *(++pattern);
+    }
+
+    *matched_to_dir = ((pch == '/') || (pch == '\0'));  // end of string and the pattern is complete or failed at a '/'? We should descend into this directory.
+
+    return (pch == '\0');  // survived the whole pattern? That's a match!
+}
+
+static char *CaseFoldUtf8String(const char *fname)
+{
+    SDL_assert(fname != NULL);
+    const size_t allocation = (SDL_strlen(fname) + 1) * 3;
+    char *retval = (char *) SDL_malloc(allocation);  // lazy: just allocating the max needed.
+    if (!retval) {
+        return NULL;
+    }
+
+    Uint32 codepoint;
+    size_t written = 0;
+    while ((codepoint = SDL_StepUTF8(&fname, 4)) != 0) {
+        Uint32 folded[3];
+        const int num_folded = SDL_CaseFoldUnicode(codepoint, folded);
+        SDL_assert(num_folded > 0);
+        SDL_assert(num_folded <= SDL_arraysize(folded));
+        for (int i = 0; i < num_folded; i++) {
+            SDL_assert(written < allocation);
+            retval[written++] = folded[i];
+        }
+    }
+
+    SDL_assert(written < allocation);
+    retval[written++] = '\0';
+
+    if (written < allocation) {
+        void *ptr = SDL_realloc(retval, written);  // shrink it down.
+        if (ptr) {  // shouldn't fail, but if it does, `retval` is still valid.
+            retval = (char *) ptr;
+        }
+    }
+
+    return retval;
+}
+
+
+typedef struct GlobDirCallbackData
+{
+    SDL_bool (*matcher)(const char *pattern, const char *str, SDL_bool *matched_to_dir);
+    const char *pattern;
+    int num_entries;
+    Uint32 flags;
+    SDL_GlobEnumeratorFunc enumerator;
+    SDL_GlobGetPathInfoFunc getpathinfo;
+    void *fsuserdata;
+    size_t basedirlen;
+    SDL_IOStream *string_stream;
+} GlobDirCallbackData;
+
+static int SDLCALL GlobDirectoryCallback(void *userdata, const char *dirname, const char *fname)
+{
+    SDL_assert(userdata != NULL);
+    SDL_assert(dirname != NULL);
+    SDL_assert(fname != NULL);
+
+    //SDL_Log("GlobDirectoryCallback('%s', '%s')", dirname, fname);
+
+    GlobDirCallbackData *data = (GlobDirCallbackData *) userdata;
+
+    // !!! 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,
+    // !!! FIXME: and only casefold the new pieces instead of allocating and folding full paths for all of this.
+
+    char *fullpath = NULL;
+    if (SDL_asprintf(&fullpath, "%s/%s", dirname, fname) < 0) {
+        return -1;
+    }
+
+    char *folded = NULL;
+    if (data->flags & SDL_GLOBDIR_CASEINSENSITIVE) {
+        folded = CaseFoldUtf8String(fullpath);
+        if (!folded) {
+            return -1;
+        }
+    }
+
+    SDL_bool matched_to_dir = SDL_FALSE;
+    const SDL_bool matched = data->matcher(data->pattern, (folded ? folded : fullpath) + data->basedirlen, &matched_to_dir);
+    //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");
+    SDL_free(folded);
+
+    if (matched) {
+        const char *subpath = fullpath + data->basedirlen;
+        const size_t slen = SDL_strlen(subpath) + 1;
+        if (SDL_WriteIO(data->string_stream, subpath, slen) != slen) {
+            SDL_free(fullpath);
+            return -1;  // stop enumerating, return failure to the app.
+        }
+        data->num_entries++;
+    }
+
+    int retval = 1;  // keep enumerating by default.
+    if (matched_to_dir) {
+        SDL_PathInfo info;
+        if ((data->getpathinfo(fullpath, &info, data->fsuserdata) == 0) && (info.type == SDL_PATHTYPE_DIRECTORY)) {
+            //SDL_Log("GlobDirectoryCallback: Descending into subdir '%s'", fname);
+            if (data->enumerator(fullpath, GlobDirectoryCallback, data, data->fsuserdata) < 0) {
+                retval = -1;
+            }
+        }
+    }
+
+    SDL_free(fullpath);
+
+    return retval;
+}
+
+char **SDL_InternalGlobDirectory(const char *path, const char *pattern, Uint32 flags, int *count, SDL_GlobEnumeratorFunc enumerator, SDL_GlobGetPathInfoFunc getpathinfo, void *userdata)
+{
+    int dummycount;
+    if (!count) {
+        count = &dummycount;
+    }
+    *count = 0;
+
+    if (!path) {
+        SDL_InvalidParamError("path");
+        return NULL;
+    }
+
+    // if path ends with any '/', chop them off, so we don't confuse the pattern matcher later.
+    char *pathcpy = NULL;
+    size_t pathlen = SDL_strlen(path);
+    if (pathlen && (path[pathlen-1] == '/')) {
+        pathcpy = SDL_strdup(path);
+        if (!pathcpy) {
+            return NULL;
+        }
+        char *ptr = &pathcpy[pathlen-1];
+        while ((ptr >= pathcpy) && (*ptr == '/')) {
+            *(ptr--) = '\0';
+        }
+        path = pathcpy;
+    }
+
+    char *folded = NULL;
+    if (pattern && (flags & SDL_GLOBDIR_CASEINSENSITIVE)) {
+        folded = CaseFoldUtf8String(pattern);
+        if (!folded) {
+            SDL_free(pathcpy);
+            return NULL;
+        }
+    }
+
+    GlobDirCallbackData data;
+    SDL_zero(data);
+    data.string_stream = SDL_IOFromDynamicMem();
+    if (!data.string_stream) {
+        SDL_free(folded);
+        SDL_free(pathcpy);
+        return NULL;
+    }
+
+    if (!pattern) {
+        data.matcher = EverythingMatch;  // no pattern? Everything matches.
+
+    // !!! FIXME
+    //} else if (flags & SDL_GLOBDIR_GITIGNORE) {
+    //    data.matcher = GitIgnoreMatch;
+
+    } else {
+        data.matcher = WildcardMatch;
+    }
+
+    data.pattern = folded ? folded : pattern;
+    data.flags = flags;
+    data.enumerator = enumerator;
+    data.getpathinfo = getpathinfo;
+    data.fsuserdata = userdata;
+    data.basedirlen = SDL_strlen(path) + 1;  // +1 for the '/' we'll be adding.
+
+    char **retval = NULL;
+    if (data.enumerator(path, GlobDirectoryCallback, &data, data.fsuserdata) == 0) {
+        const size_t streamlen = (size_t) SDL_GetIOSize(data.string_stream);
+        const size_t buflen = streamlen + ((data.num_entries + 1) * sizeof (char *));  // +1 for NULL terminator at end of array.
+        retval = (char **) SDL_malloc(buflen);
+        if (retval) {
+            if (data.num_entries > 0) {
+                Sint64 iorc = SDL_SeekIO(data.string_stream, 0, SDL_IO_SEEK_SET);
+                SDL_assert(iorc == 0);  // this should never fail for a memory stream!
+                char *ptr = (char *) (retval + (data.num_entries + 1));
+                iorc = SDL_ReadIO(data.string_stream, ptr, streamlen);
+                SDL_assert(iorc == (Sint64) streamlen);  // this should never fail for a memory stream!
+                for (int i = 0; i < data.num_entries; i++) {
+                    retval[i] = ptr;
+                    ptr += SDL_strlen(ptr) + 1;
+                }
+            }
+            retval[data.num_entries] = NULL;  // NULL terminate the list.
+            *count = data.num_entries;
+        }
+    }
+
+    SDL_CloseIO(data.string_stream);
+    SDL_free(folded);
+    SDL_free(pathcpy);
+
+    return retval;
+}
+
+static int GlobDirectoryGetPathInfo(const char *path, SDL_PathInfo *info, void *userdata)
+{
+    return SDL_GetPathInfo(path, info);
+}
+
+static int GlobDirectoryEnumerator(const char *path, SDL_EnumerateDirectoryCallback cb, void *cbuserdata, void *userdata)
+{
+    return SDL_EnumerateDirectory(path, cb, cbuserdata);
+}
+
+char **SDL_GlobDirectory(const char *path, const char *pattern, Uint32 flags, int *count)
+{
+    //SDL_Log("SDL_GlobDirectory('%s', '%s') ...", path, pattern);
+    return SDL_InternalGlobDirectory(path, pattern, flags, count, GlobDirectoryEnumerator, GlobDirectoryGetPathInfo, NULL);
+}
+

+ 4 - 0
src/filesystem/SDL_sysfilesystem.h

@@ -28,5 +28,9 @@ int SDL_SYS_RenamePath(const char *oldpath, const char *newpath);
 int SDL_SYS_CreateDirectory(const char *path);
 int SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info);
 
+typedef int (*SDL_GlobEnumeratorFunc)(const char *path, SDL_EnumerateDirectoryCallback cb, void *cbuserdata, void *userdata);
+typedef int (*SDL_GlobGetPathInfoFunc)(const char *path, SDL_PathInfo *info, void *userdata);
+char **SDL_InternalGlobDirectory(const char *path, const char *pattern, Uint32 flags, int *count, SDL_GlobEnumeratorFunc enumerator, SDL_GlobGetPathInfoFunc getpathinfo, void *userdata);
+
 #endif
 

+ 18 - 0
src/storage/SDL_storage.c

@@ -22,6 +22,7 @@
 #include "SDL_internal.h"
 
 #include "SDL_sysstorage.h"
+#include "../filesystem/SDL_sysfilesystem.h"
 
 /* Available title storage drivers */
 static TitleStorageBootStrap *titlebootstrap[] = {
@@ -321,3 +322,20 @@ Uint64 SDL_GetStorageSpaceRemaining(SDL_Storage *storage)
 
     return storage->iface.space_remaining(storage->userdata);
 }
+
+static int GlobStorageDirectoryGetPathInfo(const char *path, SDL_PathInfo *info, void *userdata)
+{
+    return SDL_GetStoragePathInfo((SDL_Storage *) userdata, path, info);
+}
+
+static int GlobStorageDirectoryEnumerator(const char *path, SDL_EnumerateDirectoryCallback cb, void *cbuserdata, void *userdata)
+{
+    return SDL_EnumerateStorageDirectory((SDL_Storage *) userdata, path, cb, cbuserdata);
+}
+
+char **SDL_GlobStorageDirectory(SDL_Storage *storage, const char *path, const char *pattern, Uint32 flags, int *count)
+{
+    CHECK_STORAGE_MAGIC_RET(NULL)
+    return SDL_InternalGlobDirectory(path, pattern, flags, count, GlobStorageDirectoryEnumerator, GlobStorageDirectoryGetPathInfo, storage);
+}
+

+ 13 - 0
test/testfilesystem.c

@@ -110,10 +110,23 @@ int main(int argc, char *argv[])
     }
 
     if (base_path) {
+        char **globlist;
+
         if (SDL_EnumerateDirectory(base_path, enum_callback, NULL) < 0) {
             SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Base path enumeration failed!");
         }
 
+        globlist = SDL_GlobDirectory(base_path, "*/test*/Test*", SDL_GLOBDIR_CASEINSENSITIVE, NULL);
+        if (!globlist) {
+            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Base path globbing failed!");
+        } else {
+            int i;
+            for (i = 0; globlist[i]; i++) {
+                SDL_Log("GLOB[%d]: '%s'", i, globlist[i]);
+            }
+            SDL_free(globlist);
+        }
+
         /* !!! FIXME: put this in a subroutine and make it test more thoroughly (and put it in testautomation). */
         if (SDL_CreateDirectory("testfilesystem-test") == -1) {
             SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateDirectory('testfilesystem-test') failed: %s", SDL_GetError());