From 764207d87364b85787d57767f0d9fcf3d173dee3 Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Mon, 18 Mar 2024 15:32:04 -0400 Subject: [PATCH] filesystem: Added SDL_GlobDirectory() and SDL_GlobStorageDirectory(). Fixes #9287. --- include/SDL3/SDL_filesystem.h | 42 ++++- include/SDL3/SDL_storage.h | 40 ++++- src/dynapi/SDL_dynapi.sym | 2 + src/dynapi/SDL_dynapi_overrides.h | 2 + src/dynapi/SDL_dynapi_procs.h | 2 + src/filesystem/SDL_filesystem.c | 277 +++++++++++++++++++++++++++++ src/filesystem/SDL_sysfilesystem.h | 4 + src/storage/SDL_storage.c | 18 ++ test/testfilesystem.c | 13 ++ 9 files changed, 398 insertions(+), 2 deletions(-) diff --git a/include/SDL3/SDL_filesystem.h b/include/SDL3/SDL_filesystem.h index 36ce139a7..851ea9bd4 100644 --- a/include/SDL3/SDL_filesystem.h +++ b/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 } diff --git a/include/SDL3/SDL_storage.h b/include/SDL3/SDL_storage.h index 61a75c389..4abc08bc4 100644 --- a/include/SDL3/SDL_storage.h +++ b/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 } diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index a63155052..9f940757e 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/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; diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h index b94b0e836..b03ffe9e5 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/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 diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index 6d7e8825d..aadd2e53c 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/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) diff --git a/src/filesystem/SDL_filesystem.c b/src/filesystem/SDL_filesystem.c index 951d52f04..7a3bd7275 100644 --- a/src/filesystem/SDL_filesystem.c +++ b/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); +} + diff --git a/src/filesystem/SDL_sysfilesystem.h b/src/filesystem/SDL_sysfilesystem.h index 97f009fd2..a41dd5cfe 100644 --- a/src/filesystem/SDL_sysfilesystem.h +++ b/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 diff --git a/src/storage/SDL_storage.c b/src/storage/SDL_storage.c index a23a380be..dff355d24 100644 --- a/src/storage/SDL_storage.c +++ b/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); +} + diff --git a/test/testfilesystem.c b/test/testfilesystem.c index 5bd031427..521774647 100644 --- a/test/testfilesystem.c +++ b/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());