filesystem: Added SDL_GlobDirectory() and SDL_GlobStorageDirectory().

Fixes #9287.
main
Ryan C. Gordon 2024-03-18 15:32:04 -04:00
parent 810656962c
commit 764207d873
9 changed files with 398 additions and 2 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
}

View File

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

View File

@ -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);
}

View File

@ -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());