From fe753b36d8e3f8350c3e927cd6f52bff3d8b1724 Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 21 Sep 2026 16:46:05 -0700 Subject: [PATCH 1/9] Add ArchiveInterface; --- src/modules/filesystem/filesystem.c | 76 +++++++++++++++++------------ 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/src/modules/filesystem/filesystem.c b/src/modules/filesystem/filesystem.c index 9787313b9..8dad95b13 100644 --- a/src/modules/filesystem/filesystem.c +++ b/src/modules/filesystem/filesystem.c @@ -46,9 +46,7 @@ typedef struct { uint64_t offset; } Handle; -struct Archive { - atomic_uint ref; - struct Archive* next; +typedef struct { bool (*open)(Archive* archive, const char* path, Handle* handle); bool (*close)(Archive* archive, Handle* handle); bool (*read)(Archive* archive, Handle* handle, uint8_t* data, size_t size, size_t* count); @@ -56,6 +54,12 @@ struct Archive { bool (*fsize)(Archive* archive, Handle* handle, uint64_t* size); bool (*stat)(Archive* archive, const char* path, fs_info* info, bool needTime); void (*list)(Archive* archive, const char* path, fs_list_cb callback, void* context); +} ArchiveInterface; + +struct Archive { + atomic_uint ref; + struct Archive* next; + ArchiveInterface* vtable; char* path; char* mountpoint; size_t pathLength; @@ -330,7 +334,7 @@ static Archive* archiveStat(const char* p, fs_info* info, bool needTime) { FOREACH_ARCHIVE(archive) { if (archiveContains(archive, path, length)) { - if (archive->stat(archive, path, info, needTime)) { + if (archive->vtable->stat(archive, path, info, needTime)) { return archive; } } else if (mountpointContains(archive, path, length)) { @@ -396,18 +400,18 @@ void* lovrFilesystemRead(const char* p, size_t* size) { continue; } - if (!archive->open(archive, path, &handle)) { + if (!archive->vtable->open(archive, path, &handle)) { continue; } uint64_t bytes; - if (!archive->fsize(archive, &handle, &bytes)) { - archive->close(archive, &handle); + if (!archive->vtable->fsize(archive, &handle, &bytes)) { + archive->vtable->close(archive, &handle); return NULL; } if (bytes > SIZE_MAX) { - archive->close(archive, &handle); + archive->vtable->close(archive, &handle); lovrSetError("File is too big"); return NULL; } @@ -415,11 +419,11 @@ void* lovrFilesystemRead(const char* p, size_t* size) { *size = (size_t) bytes; void* data = lovrMalloc(*size); - if (archive->read(archive, &handle, data, *size, size)) { - archive->close(archive, &handle); + if (archive->vtable->read(archive, &handle, data, *size, size)) { + archive->vtable->close(archive, &handle); return data; } else { - archive->close(archive, &handle); + archive->vtable->close(archive, &handle); lovrFree(data); return NULL; } @@ -436,7 +440,7 @@ void lovrFilesystemGetDirectoryItems(const char* p, void (*callback)(void* conte if (sanitize(p, path, &length)) { FOREACH_ARCHIVE(archive) { if (archive->mountLength == 0) { - archive->list(archive, path, callback, context); + archive->vtable->list(archive, path, callback, context); } else if (memcmp(archive->mountpoint, path, MIN(length, archive->mountLength))) { continue; } else if (length < archive->mountLength && (archive->mountpoint[length] == '/' || length == 0)) { @@ -449,7 +453,7 @@ void lovrFilesystemGetDirectoryItems(const char* p, void (*callback)(void* conte buffer[sublength] = '\0'; callback(context, buffer); } else if (path[archive->mountLength] == '/' || path[archive->mountLength] == '\0') { - archive->list(archive, path, callback, context); + archive->vtable->list(archive, path, callback, context); } } } @@ -680,6 +684,16 @@ static void dir_list(Archive* archive, const char* path, fs_list_cb callback, vo } } +static ArchiveInterface ArchiveDir = { + .open = dir_open, + .close = dir_close, + .read = dir_read, + .seek = dir_seek, + .fsize = dir_fsize, + .stat = dir_stat, + .list = dir_list +}; + // Archive: zip static uint16_t readu16(const uint8_t* p) { uint16_t x; memcpy(&x, p, sizeof(x)); return x; } @@ -1045,6 +1059,16 @@ static void zip_list(Archive* archive, const char* path, fs_list_cb callback, vo } } +static ArchiveInterface ArchiveZip = { + .open = zip_open, + .close = zip_close, + .read = zip_read, + .seek = zip_seek, + .fsize = zip_fsize, + .stat = zip_stat, + .list = zip_list +}; + // Archive Archive* lovrArchiveCreate(const char* path, const char* mountpoint, const char* root) { @@ -1057,21 +1081,9 @@ Archive* lovrArchiveCreate(const char* path, const char* mountpoint, const char* archive->ref = 1; if (info.type == FILE_DIRECTORY) { - archive->open = dir_open; - archive->close = dir_close; - archive->read = dir_read; - archive->seek = dir_seek; - archive->fsize = dir_fsize; - archive->stat = dir_stat; - archive->list = dir_list; + archive->vtable = &ArchiveDir; } else if (zip_init(archive, path, root)) { - archive->open = zip_open; - archive->close = zip_close; - archive->read = zip_read; - archive->seek = zip_seek; - archive->fsize = zip_fsize; - archive->stat = zip_stat; - archive->list = zip_list; + archive->vtable = &ArchiveZip; } else { lovrFree(archive); return NULL; @@ -1112,7 +1124,7 @@ File* lovrFileCreate(const char* p, OpenMode mode) { if (mode == OPEN_READ) { FOREACH_ARCHIVE(a) { - if (archiveContains(a, path, length) && a->open(a, path, &handle)) { + if (archiveContains(a, path, length) && a->vtable->open(a, path, &handle)) { archive = a; break; } @@ -1142,7 +1154,7 @@ File* lovrFileCreate(const char* p, OpenMode mode) { void lovrFileDestroy(void* ref) { File* file = ref; - if (file->archive) file->archive->close(file->archive, &file->handle); + if (file->archive) file->archive->vtable->close(file->archive, &file->handle); lovrRelease(file->archive, lovrArchiveDestroy); lovrFree(file->path); lovrFree(file); @@ -1157,12 +1169,12 @@ OpenMode lovrFileGetMode(File* file) { } bool lovrFileGetSize(File* file, uint64_t* size) { - return file->archive->fsize(file->archive, &file->handle, size); + return file->archive->vtable->fsize(file->archive, &file->handle, size); } bool lovrFileRead(File* file, void* data, size_t size, size_t* count) { lovrCheck(file->mode == OPEN_READ, "File was not opened for reading"); - return file->archive->read(file->archive, &file->handle, data, size, count); + return file->archive->vtable->read(file->archive, &file->handle, data, size, count); } bool lovrFileWrite(File* file, const void* data, size_t size, size_t* count) { @@ -1171,7 +1183,7 @@ bool lovrFileWrite(File* file, const void* data, size_t size, size_t* count) { } bool lovrFileSeek(File* file, uint64_t offset) { - return file->archive->seek(file->archive, &file->handle, offset); + return file->archive->vtable->seek(file->archive, &file->handle, offset); } uint64_t lovrFileTell(File* file) { From fcb5f73651cb9b57453c18ca440575b78794cfa0 Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 21 Sep 2026 19:51:35 -0700 Subject: [PATCH 2/9] Add lovrFilesystemGetInfo; --- src/api/l_filesystem.c | 31 ++++++++++++++------- src/core/fs.c | 6 ++--- src/core/fs.h | 6 ++--- src/modules/filesystem/filesystem.c | 42 +++++++---------------------- src/modules/filesystem/filesystem.h | 16 ++++++++--- 5 files changed, 49 insertions(+), 52 deletions(-) diff --git a/src/api/l_filesystem.c b/src/api/l_filesystem.c index 4cbb5c41c..cbecd278a 100644 --- a/src/api/l_filesystem.c +++ b/src/api/l_filesystem.c @@ -201,10 +201,10 @@ static int l_lovrFilesystemGetIdentity(lua_State* L) { static int l_lovrFilesystemGetLastModified(lua_State* L) { const char* path = luaL_checkstring(L, 1); - uint64_t modtime; - bool success = lovrFilesystemGetLastModified(path, &modtime); + FileInfo info; + bool success = lovrFilesystemGetInfo(path, &info, true); if (!success) return luax_pushnilerror(L); - lua_pushinteger(L, modtime); + lua_pushinteger(L, info.lastModified); return 1; } @@ -226,11 +226,17 @@ static int l_lovrFilesystemGetSaveDirectory(lua_State* L) { static int l_lovrFilesystemGetSize(lua_State* L) { const char* path = luaL_checkstring(L, 1); - uint64_t size; - bool success = lovrFilesystemGetSize(path, &size); + FileInfo info; + bool success = lovrFilesystemGetInfo(path, &info, false); if (!success) return luax_pushnilerror(L); - lua_pushinteger(L, size); - return 1; + if (info.type != FILE_REGULAR) { + lua_pushnil(L); + lua_pushliteral(L, "Is directory"); + return 2; + } else { + lua_pushinteger(L, info.size); + return 1; + } } static int l_lovrFilesystemGetSource(lua_State* L) { @@ -271,13 +277,17 @@ static int l_lovrFilesystemGetWorkingDirectory(lua_State* L) { static int l_lovrFilesystemIsDirectory(lua_State* L) { const char* path = luaL_checkstring(L, 1); - lua_pushboolean(L, lovrFilesystemIsDirectory(path)); + FileInfo info; + bool success = lovrFilesystemGetInfo(path, &info, false); + lua_pushboolean(L, success && info.type == FILE_DIRECTORY); return 1; } static int l_lovrFilesystemIsFile(lua_State* L) { const char* path = luaL_checkstring(L, 1); - lua_pushboolean(L, lovrFilesystemIsFile(path)); + FileInfo info; + bool success = lovrFilesystemGetInfo(path, &info, false); + lua_pushboolean(L, success && info.type == FILE_REGULAR); return 1; } @@ -444,7 +454,8 @@ static int luaLoader(lua_State* L) { if (*p == ';' || *p == '\0') { *f = '\0'; - if (lovrFilesystemIsFile(filename)) { + FileInfo info; + if (lovrFilesystemGetInfo(filename, &info, false) && info.type == FILE_REGULAR) { return luax_loadfile(L, filename, debug, NULL); } diff --git a/src/core/fs.c b/src/core/fs.c index c8a9a98c5..e81235992 100644 --- a/src/core/fs.c +++ b/src/core/fs.c @@ -154,7 +154,7 @@ fs_error fs_stat(const char* path, fs_info* info) { } FILETIME lastModified = attributes.ftLastWriteTime; - info->type = (attributes.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? FILE_DIRECTORY : FILE_REGULAR; + info->type = (attributes.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? FS_DIRECTORY : FS_REGULAR; info->lastModified = ((uint64_t) lastModified.dwHighDateTime << 32) | lastModified.dwLowDateTime; info->lastModified /= 10000000ULL; // Convert windows 100ns ticks to seconds info->lastModified -= 11644473600ULL; // Convert windows epoch (1601) to POSIX epoch (1970) @@ -301,7 +301,7 @@ fs_error fs_fstat(fs_handle file, fs_info* info) { int result = fstat(file.fd, &stats); info->size = (uint64_t) stats.st_size; info->lastModified = (uint64_t) stats.st_mtime; - info->type = S_ISDIR(stats.st_mode) ? FILE_DIRECTORY : FILE_REGULAR; + info->type = S_ISDIR(stats.st_mode) ? FS_DIRECTORY : FS_REGULAR; return check(result); } @@ -338,7 +338,7 @@ fs_error fs_stat(const char* path, fs_info* info) { int result = stat(path, &stats); info->size = (uint64_t) stats.st_size; info->lastModified = (uint64_t) stats.st_mtime; - info->type = S_ISDIR(stats.st_mode) ? FILE_DIRECTORY : FILE_REGULAR; + info->type = S_ISDIR(stats.st_mode) ? FS_DIRECTORY : FS_REGULAR; return check(result); } diff --git a/src/core/fs.h b/src/core/fs.h index fa620210a..723a48564 100644 --- a/src/core/fs.h +++ b/src/core/fs.h @@ -22,14 +22,14 @@ typedef enum { } fs_error; typedef enum { - FILE_DIRECTORY, - FILE_REGULAR + FS_DIRECTORY, + FS_REGULAR } fs_type; typedef struct { + fs_type type; uint64_t size; uint64_t lastModified; - fs_type type; } fs_info; typedef void fs_list_cb(void*, const char*); diff --git a/src/modules/filesystem/filesystem.c b/src/modules/filesystem/filesystem.c index 8dad95b13..5a4573528 100644 --- a/src/modules/filesystem/filesystem.c +++ b/src/modules/filesystem/filesystem.c @@ -258,7 +258,7 @@ void lovrFilesystemWatch(void) { const char* path = state.source; #endif fs_info info; - if (!watcher.id && fs_stat(path, &info) == FS_OK && info.type == FILE_DIRECTORY) { + if (!watcher.id && fs_stat(path, &info) == FS_OK && info.type == FS_DIRECTORY) { dmon_init(); watcher = dmon_watch(path, onFileEvent, DMON_WATCHFLAGS_RECURSIVE, NULL); } @@ -339,7 +339,7 @@ static Archive* archiveStat(const char* p, fs_info* info, bool needTime) { } } else if (mountpointContains(archive, path, length)) { // Virtual directory - info->type = FILE_DIRECTORY; + info->type = FS_DIRECTORY; info->lastModified = ~0ull; info->size = 0; return archive; @@ -356,34 +356,12 @@ const char* lovrFilesystemGetRealDirectory(const char* path) { return archive ? archive->path : NULL; } -bool lovrFilesystemIsFile(const char* path) { - fs_info info; - return archiveStat(path, &info, false) && info.type == FILE_REGULAR; -} - -bool lovrFilesystemIsDirectory(const char* path) { - fs_info info; - return archiveStat(path, &info, false) && info.type == FILE_DIRECTORY; -} - -bool lovrFilesystemGetSize(const char* path, uint64_t* size) { - fs_info info; - if (archiveStat(path, &info, false)) { - if (info.type == FILE_REGULAR) { - *size = info.size; - return true; - } else { - return lovrSetError("Is directory"); - } - } else { - return false; - } -} - -bool lovrFilesystemGetLastModified(const char* path, uint64_t* modtime) { - fs_info info; - if (archiveStat(path, &info, true)) { - *modtime = info.lastModified; +bool lovrFilesystemGetInfo(const char* path, FileInfo* info, bool needTime) { + fs_info stat; + if (archiveStat(path, &stat, needTime)) { + info->type = (FileType) stat.type; + info->size = stat.size; + info->lastModified = stat.lastModified; return true; } else { return false; @@ -1043,7 +1021,7 @@ static bool zip_stat(Archive* archive, const char* path, fs_info* info, bool nee } info->size = node->uncompressedSize; - info->type = node->directory ? FILE_DIRECTORY : FILE_REGULAR; + info->type = node->directory ? FS_DIRECTORY : FS_REGULAR; return true; } @@ -1080,7 +1058,7 @@ Archive* lovrArchiveCreate(const char* path, const char* mountpoint, const char* Archive* archive = lovrCalloc(sizeof(Archive)); archive->ref = 1; - if (info.type == FILE_DIRECTORY) { + if (info.type == FS_DIRECTORY) { archive->vtable = &ArchiveDir; } else if (zip_init(archive, path, root)) { archive->vtable = &ArchiveZip; diff --git a/src/modules/filesystem/filesystem.h b/src/modules/filesystem/filesystem.h index 5c25e69d5..ea93eec52 100644 --- a/src/modules/filesystem/filesystem.h +++ b/src/modules/filesystem/filesystem.h @@ -16,6 +16,17 @@ typedef enum { FILE_RENAME } FileAction; +typedef enum { + FILE_DIRECTORY, + FILE_REGULAR +} FileType; + +typedef struct { + FileType type; + uint64_t size; + uint64_t lastModified; +} FileInfo; + bool lovrFilesystemInit(void); void lovrFilesystemDestroy(void); bool lovrFilesystemSetSource(const char* source); @@ -26,10 +37,7 @@ void lovrFilesystemUnwatch(void); bool lovrFilesystemMount(const char* path, const char* mountpoint, bool append, const char *root); bool lovrFilesystemUnmount(const char* path); const char* lovrFilesystemGetRealDirectory(const char* path); -bool lovrFilesystemIsFile(const char* path); -bool lovrFilesystemIsDirectory(const char* path); -bool lovrFilesystemGetSize(const char* path, uint64_t* size); -bool lovrFilesystemGetLastModified(const char* path, uint64_t* modtime); +bool lovrFilesystemGetInfo(const char* path, FileInfo* info, bool needTime); void* lovrFilesystemRead(const char* path, size_t* size); void lovrFilesystemGetDirectoryItems(const char* path, void (*callback)(void* context, const char* path), void* context); const char* lovrFilesystemGetIdentity(void); From 57f842d4658576eff401b8c7dc6aba215fb71dd3 Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 21 Sep 2026 19:58:07 -0700 Subject: [PATCH 3/9] Reorganize filesystem bindings; --- src/api/l_filesystem.c | 372 ++++++++++++++++++++--------------------- 1 file changed, 186 insertions(+), 186 deletions(-) diff --git a/src/api/l_filesystem.c b/src/api/l_filesystem.c index cbecd278a..2fb9eca16 100644 --- a/src/api/l_filesystem.c +++ b/src/api/l_filesystem.c @@ -96,38 +96,107 @@ static int luax_loadfile(lua_State* L, const char* path, const char* debug, cons } } -static int l_lovrFilesystemAppend(lua_State* L) { - const char* path = luaL_checkstring(L, 1); +static int l_lovrFilesystemGetSource(lua_State* L) { + const char* source = lovrFilesystemGetSource(); - size_t size; - const char* data; - Blob* blob = luax_totype(L, 2, Blob); - if (blob) { - data = blob->data; - size = blob->size; - } else if (lua_type(L, 2) == LUA_TSTRING) { - data = lua_tolstring(L, 2, &size); + if (source) { + lua_pushstring(L, source); } else { - return luax_typeerror(L, 2, "string or Blob"); + lua_pushnil(L); } - return luax_pushsuccess(L, lovrFilesystemWrite(path, data, size, true)); + return 1; } -static int l_lovrFilesystemCreateDirectory(lua_State* L) { +static int l_lovrFilesystemSetSource(lua_State* L) { + const char* source = luaL_checkstring(L, 1); + luax_assert(L, lovrFilesystemSetSource(source)); + return 0; +} + +static int l_lovrFilesystemIsFused(lua_State* L) { + lua_pushboolean(L, lovrFilesystemIsFused()); + return 1; +} + +static int l_lovrFilesystemWatch(lua_State* L) { + lovrFilesystemWatch(); + return 0; +} + +static int l_lovrFilesystemUnwatch(lua_State* L) { + lovrFilesystemUnwatch(); + return 0; +} + +static int l_lovrFilesystemMount(lua_State* L) { const char* path = luaL_checkstring(L, 1); - return luax_pushsuccess(L, lovrFilesystemCreateDirectory(path)); + const char* mountpoint = luaL_optstring(L, 2, NULL); + bool append = lua_toboolean(L, 3); + const char* root = luaL_optstring(L, 4, NULL); + return luax_pushsuccess(L, lovrFilesystemMount(path, mountpoint, append, root)); } -static int l_lovrFilesystemGetAppdataDirectory(lua_State* L) { - char buffer[LOVR_PATH_MAX]; +static int l_lovrFilesystemUnmount(lua_State* L) { + const char* path = luaL_checkstring(L, 1); + lua_pushboolean(L, lovrFilesystemUnmount(path)); + return 1; +} - if (lovrFilesystemGetAppdataDirectory(buffer, sizeof(buffer))) { - lua_pushstring(L, buffer); - } else { +static int l_lovrFilesystemGetRealDirectory(lua_State* L) { + const char* path = luaL_checkstring(L, 1); + lua_pushstring(L, lovrFilesystemGetRealDirectory(path)); + return 1; +} + +static int l_lovrFilesystemIsDirectory(lua_State* L) { + const char* path = luaL_checkstring(L, 1); + FileInfo info; + bool success = lovrFilesystemGetInfo(path, &info, false); + lua_pushboolean(L, success && info.type == FILE_DIRECTORY); + return 1; +} + +static int l_lovrFilesystemIsFile(lua_State* L) { + const char* path = luaL_checkstring(L, 1); + FileInfo info; + bool success = lovrFilesystemGetInfo(path, &info, false); + lua_pushboolean(L, success && info.type == FILE_REGULAR); + return 1; +} + +static int l_lovrFilesystemGetSize(lua_State* L) { + const char* path = luaL_checkstring(L, 1); + FileInfo info; + bool success = lovrFilesystemGetInfo(path, &info, false); + if (!success) return luax_pushnilerror(L); + if (info.type != FILE_REGULAR) { lua_pushnil(L); + lua_pushliteral(L, "Is directory"); + return 2; + } else { + lua_pushinteger(L, info.size); + return 1; } +} + +static int l_lovrFilesystemGetLastModified(lua_State* L) { + const char* path = luaL_checkstring(L, 1); + FileInfo info; + bool success = lovrFilesystemGetInfo(path, &info, true); + if (!success) return luax_pushnilerror(L); + lua_pushinteger(L, info.lastModified); + return 1; +} +static int l_lovrFilesystemRead(lua_State* L) { + const char* path = luaL_checkstring(L, 1); + + size_t size; + void* data = lovrFilesystemRead(path, &size); + if (!data) return luax_pushnilerror(L); + lua_pushlstring(L, data, size); + lovrFree(data); return 1; } @@ -162,33 +231,6 @@ static int l_lovrFilesystemGetDirectoryItems(lua_State* L) { return 1; } -static int l_lovrFilesystemGetBundlePath(lua_State* L) { - char buffer[LOVR_PATH_MAX]; - const char* root; - - if (lovrFilesystemGetBundlePath(buffer, sizeof(buffer), &root)) { - lua_pushstring(L, buffer); - lua_pushstring(L, root); - } else { - lua_pushnil(L); - lua_pushnil(L); - } - - return 2; -} - -static int l_lovrFilesystemGetExecutablePath(lua_State* L) { - char buffer[LOVR_PATH_MAX]; - - if (lovrFilesystemGetExecutablePath(buffer, sizeof(buffer))) { - lua_pushstring(L, buffer); - } else { - lua_pushnil(L); - } - - return 1; -} - static int l_lovrFilesystemGetIdentity(lua_State* L) { const char* identity = lovrFilesystemGetIdentity(); if (identity) { @@ -199,51 +241,94 @@ static int l_lovrFilesystemGetIdentity(lua_State* L) { return 1; } -static int l_lovrFilesystemGetLastModified(lua_State* L) { - const char* path = luaL_checkstring(L, 1); - FileInfo info; - bool success = lovrFilesystemGetInfo(path, &info, true); - if (!success) return luax_pushnilerror(L); - lua_pushinteger(L, info.lastModified); +static int l_lovrFilesystemSetIdentity(lua_State* L) { + const char* identity = luaL_checkstring(L, 1); + bool precedence = lua_toboolean(L, 2); + luax_assert(L, lovrFilesystemSetIdentity(identity, precedence)); + return 0; +} + +static int l_lovrFilesystemGetSaveDirectory(lua_State* L) { + lua_pushstring(L, lovrFilesystemGetSaveDirectory()); return 1; } -static int l_lovrFilesystemGetRealDirectory(lua_State* L) { +static int l_lovrFilesystemCreateDirectory(lua_State* L) { const char* path = luaL_checkstring(L, 1); - lua_pushstring(L, lovrFilesystemGetRealDirectory(path)); - return 1; + return luax_pushsuccess(L, lovrFilesystemCreateDirectory(path)); } -static int l_lovrFilesystemGetRequirePath(lua_State* L) { - lua_pushstring(L, lovrFilesystemGetRequirePath()); - return 1; +static int l_lovrFilesystemRemove(lua_State* L) { + const char* path = luaL_checkstring(L, 1); + return luax_pushsuccess(L, lovrFilesystemRemove(path)); } -static int l_lovrFilesystemGetSaveDirectory(lua_State* L) { - lua_pushstring(L, lovrFilesystemGetSaveDirectory()); - return 1; +static int l_lovrFilesystemWrite(lua_State* L) { + const char* path = luaL_checkstring(L, 1); + size_t size; + const char* data; + Blob* blob = luax_totype(L, 2, Blob); + if (blob) { + data = blob->data; + size = blob->size; + } else if (lua_type(L, 2) == LUA_TSTRING) { + data = lua_tolstring(L, 2, &size); + } else { + return luax_typeerror(L, 2, "string or Blob"); + } + return luax_pushsuccess(L, lovrFilesystemWrite(path, data, size, false)); } -static int l_lovrFilesystemGetSize(lua_State* L) { +static int l_lovrFilesystemAppend(lua_State* L) { const char* path = luaL_checkstring(L, 1); - FileInfo info; - bool success = lovrFilesystemGetInfo(path, &info, false); - if (!success) return luax_pushnilerror(L); - if (info.type != FILE_REGULAR) { + + size_t size; + const char* data; + Blob* blob = luax_totype(L, 2, Blob); + if (blob) { + data = blob->data; + size = blob->size; + } else if (lua_type(L, 2) == LUA_TSTRING) { + data = lua_tolstring(L, 2, &size); + } else { + return luax_typeerror(L, 2, "string or Blob"); + } + + return luax_pushsuccess(L, lovrFilesystemWrite(path, data, size, true)); +} + +static int l_lovrFilesystemGetAppdataDirectory(lua_State* L) { + char buffer[LOVR_PATH_MAX]; + + if (lovrFilesystemGetAppdataDirectory(buffer, sizeof(buffer))) { + lua_pushstring(L, buffer); + } else { lua_pushnil(L); - lua_pushliteral(L, "Is directory"); - return 2; + } + + return 1; +} + +static int l_lovrFilesystemGetBundlePath(lua_State* L) { + char buffer[LOVR_PATH_MAX]; + const char* root; + + if (lovrFilesystemGetBundlePath(buffer, sizeof(buffer), &root)) { + lua_pushstring(L, buffer); + lua_pushstring(L, root); } else { - lua_pushinteger(L, info.size); - return 1; + lua_pushnil(L); + lua_pushnil(L); } + + return 2; } -static int l_lovrFilesystemGetSource(lua_State* L) { - const char* source = lovrFilesystemGetSource(); +static int l_lovrFilesystemGetExecutablePath(lua_State* L) { + char buffer[LOVR_PATH_MAX]; - if (source) { - lua_pushstring(L, source); + if (lovrFilesystemGetExecutablePath(buffer, sizeof(buffer))) { + lua_pushstring(L, buffer); } else { lua_pushnil(L); } @@ -275,25 +360,14 @@ static int l_lovrFilesystemGetWorkingDirectory(lua_State* L) { return 1; } -static int l_lovrFilesystemIsDirectory(lua_State* L) { - const char* path = luaL_checkstring(L, 1); - FileInfo info; - bool success = lovrFilesystemGetInfo(path, &info, false); - lua_pushboolean(L, success && info.type == FILE_DIRECTORY); - return 1; -} - -static int l_lovrFilesystemIsFile(lua_State* L) { - const char* path = luaL_checkstring(L, 1); - FileInfo info; - bool success = lovrFilesystemGetInfo(path, &info, false); - lua_pushboolean(L, success && info.type == FILE_REGULAR); +static int l_lovrFilesystemGetRequirePath(lua_State* L) { + lua_pushstring(L, lovrFilesystemGetRequirePath()); return 1; } -static int l_lovrFilesystemIsFused(lua_State* L) { - lua_pushboolean(L, lovrFilesystemIsFused()); - return 1; +static int l_lovrFilesystemSetRequirePath(lua_State* L) { + lovrFilesystemSetRequirePath(luaL_checkstring(L, 1)); + return 0; } static int l_lovrFilesystemLoad(lua_State* L) { @@ -304,14 +378,6 @@ static int l_lovrFilesystemLoad(lua_State* L) { return luax_loadfile(L, path, debug, mode); } -static int l_lovrFilesystemMount(lua_State* L) { - const char* path = luaL_checkstring(L, 1); - const char* mountpoint = luaL_optstring(L, 2, NULL); - bool append = lua_toboolean(L, 3); - const char* root = luaL_optstring(L, 4, NULL); - return luax_pushsuccess(L, lovrFilesystemMount(path, mountpoint, append, root)); -} - static int l_lovrFilesystemNewBlob(lua_State* L) { const char* path = luaL_checkstring(L, 1); size_t size; @@ -323,72 +389,6 @@ static int l_lovrFilesystemNewBlob(lua_State* L) { return 1; } -static int l_lovrFilesystemRead(lua_State* L) { - const char* path = luaL_checkstring(L, 1); - - size_t size; - void* data = lovrFilesystemRead(path, &size); - if (!data) return luax_pushnilerror(L); - lua_pushlstring(L, data, size); - lovrFree(data); - return 1; -} - -static int l_lovrFilesystemRemove(lua_State* L) { - const char* path = luaL_checkstring(L, 1); - return luax_pushsuccess(L, lovrFilesystemRemove(path)); -} - -static int l_lovrFilesystemSetIdentity(lua_State* L) { - const char* identity = luaL_checkstring(L, 1); - bool precedence = lua_toboolean(L, 2); - luax_assert(L, lovrFilesystemSetIdentity(identity, precedence)); - return 0; -} - -static int l_lovrFilesystemSetRequirePath(lua_State* L) { - lovrFilesystemSetRequirePath(luaL_checkstring(L, 1)); - return 0; -} - -static int l_lovrFilesystemSetSource(lua_State* L) { - const char* source = luaL_checkstring(L, 1); - luax_assert(L, lovrFilesystemSetSource(source)); - return 0; -} - -static int l_lovrFilesystemUnmount(lua_State* L) { - const char* path = luaL_checkstring(L, 1); - lua_pushboolean(L, lovrFilesystemUnmount(path)); - return 1; -} - -static int l_lovrFilesystemUnwatch(lua_State* L) { - lovrFilesystemUnwatch(); - return 0; -} - -static int l_lovrFilesystemWatch(lua_State* L) { - lovrFilesystemWatch(); - return 0; -} - -static int l_lovrFilesystemWrite(lua_State* L) { - const char* path = luaL_checkstring(L, 1); - size_t size; - const char* data; - Blob* blob = luax_totype(L, 2, Blob); - if (blob) { - data = blob->data; - size = blob->size; - } else if (lua_type(L, 2) == LUA_TSTRING) { - data = lua_tolstring(L, 2, &size); - } else { - return luax_typeerror(L, 2, "string or Blob"); - } - return luax_pushsuccess(L, lovrFilesystemWrite(path, data, size, false)); -} - static int l_lovrFilesystemNewFile(lua_State* L) { const char* path = luaL_checkstring(L, 1); OpenMode mode = luax_checkenum(L, 2, OpenMode, NULL); @@ -400,36 +400,36 @@ static int l_lovrFilesystemNewFile(lua_State* L) { } static const luaL_Reg lovrFilesystem[] = { - { "append", l_lovrFilesystemAppend }, + { "setSource", l_lovrFilesystemSetSource }, + { "getSource", l_lovrFilesystemGetSource }, + { "isFused", l_lovrFilesystemIsFused }, + { "watch", l_lovrFilesystemWatch }, + { "unwatch", l_lovrFilesystemUnwatch }, + { "mount", l_lovrFilesystemMount }, + { "unmount", l_lovrFilesystemUnmount }, + { "getRealDirectory", l_lovrFilesystemGetRealDirectory }, + { "isDirectory", l_lovrFilesystemIsDirectory }, + { "isFile", l_lovrFilesystemIsFile }, + { "getSize", l_lovrFilesystemGetSize }, + { "getLastModified", l_lovrFilesystemGetLastModified }, + { "read", l_lovrFilesystemRead }, + { "getDirectoryItems", l_lovrFilesystemGetDirectoryItems }, + { "getIdentity", l_lovrFilesystemGetIdentity }, + { "setIdentity", l_lovrFilesystemSetIdentity }, + { "getSaveDirectory", l_lovrFilesystemGetSaveDirectory }, { "createDirectory", l_lovrFilesystemCreateDirectory }, + { "remove", l_lovrFilesystemRemove }, + { "write", l_lovrFilesystemWrite }, + { "append", l_lovrFilesystemAppend }, { "getAppdataDirectory", l_lovrFilesystemGetAppdataDirectory }, { "getBundlePath", l_lovrFilesystemGetBundlePath }, - { "getDirectoryItems", l_lovrFilesystemGetDirectoryItems }, { "getExecutablePath", l_lovrFilesystemGetExecutablePath }, - { "getIdentity", l_lovrFilesystemGetIdentity }, - { "getLastModified", l_lovrFilesystemGetLastModified }, - { "getRealDirectory", l_lovrFilesystemGetRealDirectory }, - { "getRequirePath", l_lovrFilesystemGetRequirePath }, - { "getSaveDirectory", l_lovrFilesystemGetSaveDirectory }, - { "getSize", l_lovrFilesystemGetSize }, - { "getSource", l_lovrFilesystemGetSource }, { "getUserDirectory", l_lovrFilesystemGetUserDirectory }, { "getWorkingDirectory", l_lovrFilesystemGetWorkingDirectory }, - { "isDirectory", l_lovrFilesystemIsDirectory }, - { "isFile", l_lovrFilesystemIsFile }, - { "isFused", l_lovrFilesystemIsFused }, + { "getRequirePath", l_lovrFilesystemGetRequirePath }, + { "setRequirePath", l_lovrFilesystemSetRequirePath }, { "load", l_lovrFilesystemLoad }, - { "mount", l_lovrFilesystemMount }, { "newBlob", l_lovrFilesystemNewBlob }, - { "read", l_lovrFilesystemRead }, - { "remove", l_lovrFilesystemRemove }, - { "setIdentity", l_lovrFilesystemSetIdentity }, - { "setRequirePath", l_lovrFilesystemSetRequirePath }, - { "setSource", l_lovrFilesystemSetSource }, - { "unmount", l_lovrFilesystemUnmount }, - { "unwatch", l_lovrFilesystemUnwatch }, - { "watch", l_lovrFilesystemWatch }, - { "write", l_lovrFilesystemWrite }, { "newFile", l_lovrFilesystemNewFile }, { NULL, NULL } }; From 0e3005817ed1be4fdc6dc561c5e3e27c8b6cc0af Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 21 Sep 2026 20:00:25 -0700 Subject: [PATCH 4/9] lovr.filesystem.exists; --- CHANGES.md | 1 + src/api/l_filesystem.c | 6 ++++++ src/modules/filesystem/filesystem.c | 8 +++++--- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d6bcfba8b..6038ae0c7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -27,6 +27,7 @@ dev - Add `OriginType` parameter to `ModelData` node transform accessors. - Add `ModelData:getTriangles`. - Add `timeBeforeSleep` and `sleepVelocityThreshold` settings to `lovr.physics.newWorld`. +- Add `lovr.filesystem.exists`. ### Change diff --git a/src/api/l_filesystem.c b/src/api/l_filesystem.c index 2fb9eca16..bee55b914 100644 --- a/src/api/l_filesystem.c +++ b/src/api/l_filesystem.c @@ -149,6 +149,11 @@ static int l_lovrFilesystemGetRealDirectory(lua_State* L) { return 1; } +static int l_lovrFilesystemExists(lua_State* L) { + lua_pushboolean(L, lovrFilesystemGetInfo(luaL_checkstring(L, 1), NULL, false)); + return 1; +} + static int l_lovrFilesystemIsDirectory(lua_State* L) { const char* path = luaL_checkstring(L, 1); FileInfo info; @@ -408,6 +413,7 @@ static const luaL_Reg lovrFilesystem[] = { { "mount", l_lovrFilesystemMount }, { "unmount", l_lovrFilesystemUnmount }, { "getRealDirectory", l_lovrFilesystemGetRealDirectory }, + { "exists", l_lovrFilesystemExists }, { "isDirectory", l_lovrFilesystemIsDirectory }, { "isFile", l_lovrFilesystemIsFile }, { "getSize", l_lovrFilesystemGetSize }, diff --git a/src/modules/filesystem/filesystem.c b/src/modules/filesystem/filesystem.c index 5a4573528..783fc72db 100644 --- a/src/modules/filesystem/filesystem.c +++ b/src/modules/filesystem/filesystem.c @@ -359,9 +359,11 @@ const char* lovrFilesystemGetRealDirectory(const char* path) { bool lovrFilesystemGetInfo(const char* path, FileInfo* info, bool needTime) { fs_info stat; if (archiveStat(path, &stat, needTime)) { - info->type = (FileType) stat.type; - info->size = stat.size; - info->lastModified = stat.lastModified; + if (info) { + info->type = (FileType) stat.type; + info->size = stat.size; + info->lastModified = stat.lastModified; + } return true; } else { return false; From 8086153452e2a7d3ecaa02b0125d197e042d4450 Mon Sep 17 00:00:00 2001 From: bjorn Date: Thu, 24 Sep 2026 12:04:05 -0700 Subject: [PATCH 5/9] Writable archives; --- src/api/api.h | 1 + src/api/l_filesystem.c | 14 +- src/modules/filesystem/filesystem.c | 244 +++++++++++++++++++--------- src/modules/filesystem/filesystem.h | 15 +- 4 files changed, 185 insertions(+), 89 deletions(-) diff --git a/src/api/api.h b/src/api/api.h index 5eda20975..a2db14db9 100644 --- a/src/api/api.h +++ b/src/api/api.h @@ -55,6 +55,7 @@ extern StringEntry lovrMaterialTexture[]; extern StringEntry lovrMeshStorage[]; extern StringEntry lovrModelDrawMode[]; extern StringEntry lovrMotorMode[]; +extern StringEntry lovrMountMode[]; extern StringEntry lovrMouseMode[]; extern StringEntry lovrOpenMode[]; extern StringEntry lovrOriginType[]; diff --git a/src/api/l_filesystem.c b/src/api/l_filesystem.c index bee55b914..999667d70 100644 --- a/src/api/l_filesystem.c +++ b/src/api/l_filesystem.c @@ -19,6 +19,12 @@ StringEntry lovrFileAction[] = { { 0 } }; +StringEntry lovrMountMode[] = { + [MOUNT_READ] = ENTRY("read"), + [MOUNT_READWRITE] = ENTRY("readwrite"), + { 0 } +}; + StringEntry lovrOpenMode[] = { [OPEN_READ] = ENTRY("r"), [OPEN_WRITE] = ENTRY("w"), @@ -132,9 +138,11 @@ static int l_lovrFilesystemUnwatch(lua_State* L) { static int l_lovrFilesystemMount(lua_State* L) { const char* path = luaL_checkstring(L, 1); const char* mountpoint = luaL_optstring(L, 2, NULL); - bool append = lua_toboolean(L, 3); - const char* root = luaL_optstring(L, 4, NULL); - return luax_pushsuccess(L, lovrFilesystemMount(path, mountpoint, append, root)); + int index = 3; + MountMode mode = lua_type(L, 3) == LUA_TSTRING ? luax_checkenum(L, index++, MountMode, "read") : MOUNT_READ; + bool append = lua_toboolean(L, index++); + const char* root = luaL_optstring(L, index++, NULL); + return luax_pushsuccess(L, lovrFilesystemMount(path, mountpoint, mode, append, root)); } static int l_lovrFilesystemUnmount(lua_State* L) { diff --git a/src/modules/filesystem/filesystem.c b/src/modules/filesystem/filesystem.c index 783fc72db..41a860e33 100644 --- a/src/modules/filesystem/filesystem.c +++ b/src/modules/filesystem/filesystem.c @@ -47,23 +47,28 @@ typedef struct { } Handle; typedef struct { - bool (*open)(Archive* archive, const char* path, Handle* handle); + bool (*open)(Archive* archive, const char* path, OpenMode mode, Handle* handle); bool (*close)(Archive* archive, Handle* handle); bool (*read)(Archive* archive, Handle* handle, uint8_t* data, size_t size, size_t* count); + bool (*write)(Archive* archive, Handle* handle, const uint8_t* data, size_t size, size_t* count); bool (*seek)(Archive* archive, Handle* handle, uint64_t offset); bool (*fsize)(Archive* archive, Handle* handle, uint64_t* size); bool (*stat)(Archive* archive, const char* path, fs_info* info, bool needTime); + bool (*remove)(Archive* archive, const char* path); + bool (*mkdir)(Archive* archive, const char* path); void (*list)(Archive* archive, const char* path, fs_list_cb callback, void* context); } ArchiveInterface; struct Archive { atomic_uint ref; + MountMode mode; struct Archive* next; ArchiveInterface* vtable; char* path; char* mountpoint; size_t pathLength; size_t mountLength; + uint32_t mountDepth; uint8_t* data; size_t size; map_t lookup; @@ -188,7 +193,7 @@ bool lovrFilesystemInit(void) { state.identity[length] = '\0'; state.savePath[cursor - 6] = '/'; state.savePathLength = cursor; - if (!lovrFilesystemMount(state.savePath, NULL, false, NULL)) { + if (!lovrFilesystemMount(state.savePath, NULL, MOUNT_READWRITE, false, NULL)) { state.identity[0] = '\0'; } } @@ -280,14 +285,14 @@ bool lovrFilesystemIsFused(void) { // Archives -bool lovrFilesystemMount(const char* path, const char* mountpoint, bool append, const char* root) { +bool lovrFilesystemMount(const char* path, const char* mountpoint, MountMode mode, bool append, const char* root) { FOREACH_ARCHIVE(archive) { if (!strcmp(archive->path, path)) { return lovrSetError("Already mounted"); } } - Archive* archive = lovrArchiveCreate(path, mountpoint, root); + Archive* archive = lovrArchiveCreate(path, mountpoint, mode, root); if (!archive) return false; if (append) { @@ -325,6 +330,33 @@ static bool mountpointContains(Archive* archive, const char* path, size_t length return length < archive->mountLength && archive->mountpoint[length] == '/' && !memcmp(path, archive->mountpoint, length); } +static Archive* findWriteArchive(const char* path, size_t length) { + uint32_t maxDepth = 0; + Archive* deepest = NULL; + + FOREACH_ARCHIVE(archive) { + if (archive->mode == MOUNT_READ) { + continue; + } + + if (!archiveContains(archive, path, length)) { + continue; + } + + if (!deepest || archive->mountDepth > maxDepth) { + maxDepth = archive->mountDepth; + deepest = archive; + } + } + + if (!deepest) { + lovrSetError("No writable archive found"); + return NULL; + } + + return deepest; +} + static Archive* archiveStat(const char* p, fs_info* info, bool needTime) { char path[1024]; size_t length = sizeof(path); @@ -370,6 +402,8 @@ bool lovrFilesystemGetInfo(const char* path, FileInfo* info, bool needTime) { } } +// IO + void* lovrFilesystemRead(const char* p, size_t* size) { Handle handle; char path[1024]; @@ -380,7 +414,7 @@ void* lovrFilesystemRead(const char* p, size_t* size) { continue; } - if (!archive->vtable->open(archive, path, &handle)) { + if (!archive->vtable->open(archive, path, OPEN_READ, &handle)) { continue; } @@ -414,6 +448,57 @@ void* lovrFilesystemRead(const char* p, size_t* size) { return NULL; } +bool lovrFilesystemWrite(const char* p, const char* content, size_t size, bool append) { + char path[1024]; + size_t length = sizeof(path); + if (!sanitize(p, path, &length)) { + return false; + } + + Archive* archive = findWriteArchive(path, length); + if (!archive) return false; + + Handle handle; + if (!archive->vtable->open(archive, path, append ? OPEN_APPEND : OPEN_WRITE, &handle)) { + return false; + } + + size_t bytesWritten = 0; + if (!archive->vtable->write(archive, &handle, (const uint8_t*) content, size, &bytesWritten)) { + archive->vtable->close(archive, &handle); + return false; + } + + if (bytesWritten != size) { + archive->vtable->close(archive, &handle); + return lovrSetError("Incomplete write"); + } + + return archive->vtable->close(archive, &handle); +} + +bool lovrFilesystemRemove(const char* p) { + char path[1024]; + size_t length = sizeof(path); + if (!sanitize(p, path, &length)) { + return false; + } + + Archive* archive = findWriteArchive(path, length); + return archive && archive->vtable->remove(archive, path); +} + +bool lovrFilesystemCreateDirectory(const char* p) { + char path[1024]; + size_t length = sizeof(path); + if (!sanitize(p, path, &length)) { + return false; + } + + Archive* archive = findWriteArchive(path, length); + return archive && archive->vtable->mkdir(archive, path); +} + void lovrFilesystemGetDirectoryItems(const char* p, void (*callback)(void* context, const char* path), void* context) { char path[1024]; size_t length = sizeof(path); @@ -439,7 +524,7 @@ void lovrFilesystemGetDirectoryItems(const char* p, void (*callback)(void* conte } } -// Writing +// Paths const char* lovrFilesystemGetIdentity(void) { return state.identity[0] == '\0' ? NULL : state.identity; @@ -506,7 +591,7 @@ bool lovrFilesystemSetIdentity(const char* identity, bool precedence) { memcpy(state.identity, identity, length + 1); // Mount the fully resolved save path - if (!lovrFilesystemMount(state.savePath, NULL, !precedence, NULL)) { + if (!lovrFilesystemMount(state.savePath, NULL, MOUNT_READWRITE, !precedence, NULL)) { state.identity[0] = '\0'; return false; } @@ -518,61 +603,6 @@ const char* lovrFilesystemGetSaveDirectory(void) { return state.savePath; } -bool lovrFilesystemCreateDirectory(const char* path) { - char resolved[LOVR_PATH_MAX]; - if (!valid(path) || !concat(resolved, state.savePath, state.savePathLength, path, strlen(path))) { - return false; - } - - char* cursor = resolved + state.savePathLength; - while (*cursor == '/') cursor++; - - while (*cursor) { - if (*cursor == '/') { - *cursor = '\0'; - fs_mkdir(resolved); - *cursor = '/'; - } - cursor++; - } - - return checkfs(fs_mkdir(resolved)); -} - -bool lovrFilesystemRemove(const char* path) { - char resolved[LOVR_PATH_MAX]; - if (!valid(path) || !concat(resolved, state.savePath, state.savePathLength, path, strlen(path))) { - return false; - } - - return checkfs(fs_remove(resolved)); -} - -bool lovrFilesystemWrite(const char* path, const char* content, size_t size, bool append) { - char resolved[LOVR_PATH_MAX]; - if (!valid(path) || !concat(resolved, state.savePath, state.savePathLength, path, strlen(path))) { - return false; - } - - fs_handle file; - if (!checkfs(fs_open(resolved, append ? 'a' : 'w', &file))) { - return false; - } - - size_t count; - if (!checkfs(fs_write(file, content, size, &count))) { - return false; - } - - if (count != size) { - return lovrSetError("Incomplete write"); - } - - return checkfs(fs_close(file)); -} - -// Paths - size_t lovrFilesystemGetAppdataDirectory(char* buffer, size_t size) { return os_get_data_directory(buffer, size); } @@ -615,9 +645,10 @@ static bool dir_resolve(Archive* archive, const char* fullpath, char* buffer) { return concat(buffer, archive->path, archive->pathLength, path, strlen(path)); } -static bool dir_open(Archive* archive, const char* path, Handle* handle) { +static bool dir_open(Archive* archive, const char* path, OpenMode mode, Handle* handle) { char resolved[LOVR_PATH_MAX]; - return dir_resolve(archive, path, resolved) && checkfs(fs_open(resolved, 'r', &handle->file)); + char modes[] = { 'r', 'w', 'a' }; + return dir_resolve(archive, path, resolved) && checkfs(fs_open(resolved, modes[mode], &handle->file)); } static bool dir_close(Archive* archive, Handle* handle) { @@ -633,6 +664,15 @@ static bool dir_read(Archive* archive, Handle* handle, uint8_t* data, size_t siz } } +static bool dir_write(Archive* archive, Handle* handle, const uint8_t* data, size_t size, size_t* count) { + if (checkfs(fs_write(handle->file, data, size, count))) { + handle->offset += *count; + return true; + } else { + return false; + } +} + static bool dir_seek(Archive* archive, Handle* handle, uint64_t offset) { if (checkfs(fs_seek(handle->file, offset))) { handle->offset = offset; @@ -657,6 +697,33 @@ static bool dir_stat(Archive* archive, const char* path, fs_info* info, bool nee return dir_resolve(archive, path, resolved) && checkfs(fs_stat(resolved, info)); } +static bool dir_remove(Archive* archive, const char* path) { + char resolved[LOVR_PATH_MAX]; + return dir_resolve(archive, path, resolved) && checkfs(fs_remove(resolved)); +} + +static bool dir_mkdir(Archive* archive, const char* path) { + char resolved[LOVR_PATH_MAX]; + + if (!dir_resolve(archive, path, resolved)) { + return false; + } + + char* cursor = resolved + archive->mountLength; + while (*cursor == '/') cursor++; + + while (*cursor) { + if (*cursor == '/') { + *cursor = '\0'; + fs_mkdir(resolved); + *cursor = '/'; + } + cursor++; + } + + return checkfs(fs_mkdir(resolved)); +} + static void dir_list(Archive* archive, const char* path, fs_list_cb callback, void* context) { char resolved[LOVR_PATH_MAX]; if (dir_resolve(archive, path, resolved)) { @@ -668,9 +735,12 @@ static ArchiveInterface ArchiveDir = { .open = dir_open, .close = dir_close, .read = dir_read, + .write = dir_write, .seek = dir_seek, .fsize = dir_fsize, .stat = dir_stat, + .remove = dir_remove, + .mkdir = dir_mkdir, .list = dir_list }; @@ -685,7 +755,9 @@ static void zip_free(Archive* archive) { if (archive->data) fs_unmap(archive->data, archive->size); } -static bool zip_init(Archive* archive, const char* filename, const char* root) { +static bool zip_init(Archive* archive, const char* filename, MountMode mode, const char* root) { + lovrCheck(mode == MOUNT_READ, "Zip archives must be mounted in readonly mode"); + // Map the zip file into memory if (!checkfs(fs_map(filename, (void**) &archive->data, &archive->size))) { return false; @@ -862,7 +934,9 @@ static zip_node* zip_resolve(Archive* archive, const char* fullpath) { return index == MAP_NIL ? NULL : &archive->nodes.data[index]; } -static bool zip_open(Archive* archive, const char* path, Handle* handle) { +static bool zip_open(Archive* archive, const char* path, OpenMode mode, Handle* handle) { + lovrCheck(mode == OPEN_READ, "Read only"); + handle->node = zip_resolve(archive, path); if (!handle->node) { @@ -1051,7 +1125,7 @@ static ArchiveInterface ArchiveZip = { // Archive -Archive* lovrArchiveCreate(const char* path, const char* mountpoint, const char* root) { +Archive* lovrArchiveCreate(const char* path, const char* mountpoint, MountMode mode, const char* root) { fs_info info; if (!checkfs(fs_stat(path, &info))) { return NULL; @@ -1059,10 +1133,11 @@ Archive* lovrArchiveCreate(const char* path, const char* mountpoint, const char* Archive* archive = lovrCalloc(sizeof(Archive)); archive->ref = 1; + archive->mode = mode; if (info.type == FS_DIRECTORY) { archive->vtable = &ArchiveDir; - } else if (zip_init(archive, path, root)) { + } else if (zip_init(archive, path, mode, root)) { archive->vtable = &ArchiveZip; } else { lovrFree(archive); @@ -1073,6 +1148,11 @@ Archive* lovrArchiveCreate(const char* path, const char* mountpoint, const char* size_t length = strlen(mountpoint); archive->mountpoint = lovrMalloc(length + 1); archive->mountLength = normalize(mountpoint, length, archive->mountpoint); + const char* slash = archive->mountpoint; + while (slash) { + archive->mountDepth++; + slash = strchr(slash + 1, '/'); + } } archive->pathLength = strlen(path); @@ -1102,24 +1182,26 @@ File* lovrFileCreate(const char* p, OpenMode mode) { Handle handle = { 0 }; Archive* archive = NULL; - if (mode == OPEN_READ) { - FOREACH_ARCHIVE(a) { - if (archiveContains(a, path, length) && a->vtable->open(a, path, &handle)) { - archive = a; - break; - } + FOREACH_ARCHIVE(a) { + if (a->mode == MOUNT_READ && mode != OPEN_READ) { + continue; } - lovrAssert(archive, "File not found"); - } else { - char fullpath[LOVR_PATH_MAX]; - if (!concat(fullpath, state.savePath, state.savePathLength, path, length)) { - return NULL; + if (!archiveContains(a, path, length)) { + continue; } - if (!checkfs(fs_open(fullpath, mode == OPEN_APPEND ? 'a' : 'w', &handle.file))) { - return NULL; + if (!a->vtable->open(a, path, mode, &handle)) { + continue; } + + archive = a; + break; + } + + if (!archive) { + lovrSetError(mode == OPEN_READ ? "File not found" : "Failed to open file for writing"); + return NULL; } File* file = lovrCalloc(sizeof(File)); diff --git a/src/modules/filesystem/filesystem.h b/src/modules/filesystem/filesystem.h index ea93eec52..42b1fa13c 100644 --- a/src/modules/filesystem/filesystem.h +++ b/src/modules/filesystem/filesystem.h @@ -27,6 +27,11 @@ typedef struct { uint64_t lastModified; } FileInfo; +typedef enum { + MOUNT_READ, + MOUNT_READWRITE +} MountMode; + bool lovrFilesystemInit(void); void lovrFilesystemDestroy(void); bool lovrFilesystemSetSource(const char* source); @@ -34,18 +39,18 @@ const char* lovrFilesystemGetSource(void); bool lovrFilesystemIsFused(void); void lovrFilesystemWatch(void); void lovrFilesystemUnwatch(void); -bool lovrFilesystemMount(const char* path, const char* mountpoint, bool append, const char *root); +bool lovrFilesystemMount(const char* path, const char* mountpoint, MountMode mode, bool append, const char *root); bool lovrFilesystemUnmount(const char* path); const char* lovrFilesystemGetRealDirectory(const char* path); bool lovrFilesystemGetInfo(const char* path, FileInfo* info, bool needTime); void* lovrFilesystemRead(const char* path, size_t* size); +bool lovrFilesystemWrite(const char* path, const char* content, size_t size, bool append); +bool lovrFilesystemRemove(const char* path); +bool lovrFilesystemCreateDirectory(const char* path); void lovrFilesystemGetDirectoryItems(const char* path, void (*callback)(void* context, const char* path), void* context); const char* lovrFilesystemGetIdentity(void); bool lovrFilesystemSetIdentity(const char* identity, bool precedence); const char* lovrFilesystemGetSaveDirectory(void); -bool lovrFilesystemCreateDirectory(const char* path); -bool lovrFilesystemRemove(const char* path); -bool lovrFilesystemWrite(const char* path, const char* content, size_t size, bool append); size_t lovrFilesystemGetAppdataDirectory(char* buffer, size_t size); size_t lovrFilesystemGetBundlePath(char* buffer, size_t size, const char** root); size_t lovrFilesystemGetExecutablePath(char* buffer, size_t size); @@ -56,7 +61,7 @@ void lovrFilesystemSetRequirePath(const char* requirePath); // Archive -Archive* lovrArchiveCreate(const char* path, const char* mountpoint, const char* root); +Archive* lovrArchiveCreate(const char* path, const char* mountpoint, MountMode mode, const char* root); void lovrArchiveDestroy(void* ref); // File From 0141915b8b735a70e7358be725c1eda28543a8f5 Mon Sep 17 00:00:00 2001 From: bjorn Date: Thu, 24 Sep 2026 12:56:17 -0700 Subject: [PATCH 6/9] Refactor Archive path handling; --- src/modules/filesystem/filesystem.c | 214 +++++++++++++++------------- test/lovr/filesystem.lua | 1 + 2 files changed, 118 insertions(+), 97 deletions(-) diff --git a/src/modules/filesystem/filesystem.c b/src/modules/filesystem/filesystem.c index 41a860e33..76d59fe34 100644 --- a/src/modules/filesystem/filesystem.c +++ b/src/modules/filesystem/filesystem.c @@ -319,18 +319,36 @@ bool lovrFilesystemUnmount(const char* path) { return false; } -static bool archiveContains(Archive* archive, const char* path, size_t length) { - if (archive->mountLength == 0) return true; - if (length == archive->mountLength && !memcmp(path, archive->mountpoint, archive->mountLength)) return true; - if (length > archive->mountLength && path[archive->mountLength] == '/' && !memcmp(path, archive->mountpoint, archive->mountLength)) return true; +// If the Archive contains the path, sets the "local" path (path with mountpoint stripped off) +static bool getLocalPath(Archive* archive, const char* path, size_t length, const char** localPath) { + if (archive->mountLength == 0) { + *localPath = path; + return true; + } + + if (length < archive->mountLength) { + return false; + } + + if (memcmp(path, archive->mountpoint, archive->mountLength)) { + return false; + } + + if (path[archive->mountLength] == '/' || path[archive->mountLength] == '\0') { + *localPath = path + archive->mountLength + (path[archive->mountLength] == '/'); + return true; + } + return false; } -static bool mountpointContains(Archive* archive, const char* path, size_t length) { - return length < archive->mountLength && archive->mountpoint[length] == '/' && !memcmp(path, archive->mountpoint, length); +// Checks if a path is part of an Archive's mountpoint but isn't in the Archive itself +static bool isVirtualDirectory(Archive* archive, const char* path, size_t length) { + return length < archive->mountLength && (length == 0 || archive->mountpoint[length] == '/') && !memcmp(path, archive->mountpoint, length); } -static Archive* findWriteArchive(const char* path, size_t length) { +// Finds the writable Archive with the deepest mountpoint containing the path +static Archive* findWriteArchive(const char* path, size_t length, const char** localPath) { uint32_t maxDepth = 0; Archive* deepest = NULL; @@ -339,13 +357,15 @@ static Archive* findWriteArchive(const char* path, size_t length) { continue; } - if (!archiveContains(archive, path, length)) { + const char* local; + if (!getLocalPath(archive, path, length, &local)) { continue; } if (!deepest || archive->mountDepth > maxDepth) { maxDepth = archive->mountDepth; deepest = archive; + *localPath = local; } } @@ -357,7 +377,7 @@ static Archive* findWriteArchive(const char* path, size_t length) { return deepest; } -static Archive* archiveStat(const char* p, fs_info* info, bool needTime) { +const char* lovrFilesystemGetRealDirectory(const char* p) { char path[1024]; size_t length = sizeof(path); if (!sanitize(p, path, &length)) { @@ -365,86 +385,95 @@ static Archive* archiveStat(const char* p, fs_info* info, bool needTime) { } FOREACH_ARCHIVE(archive) { - if (archiveContains(archive, path, length)) { - if (archive->vtable->stat(archive, path, info, needTime)) { - return archive; - } - } else if (mountpointContains(archive, path, length)) { - // Virtual directory - info->type = FS_DIRECTORY; - info->lastModified = ~0ull; - info->size = 0; - return archive; + fs_info info; + const char* localPath; + if (getLocalPath(archive, path, length, &localPath) && archive->vtable->stat(archive, localPath, &info, false)) { + return archive->path; + } else if (isVirtualDirectory(archive, path, length)) { + return archive->path; } } - lovrSetError("File not found"); + lovrSetError("Not found"); return NULL; } -const char* lovrFilesystemGetRealDirectory(const char* path) { - fs_info info; - Archive* archive = archiveStat(path, &info, false); - return archive ? archive->path : NULL; -} +// IO + +bool lovrFilesystemGetInfo(const char* p, FileInfo* info, bool needTime) { + char path[1024]; + size_t length = sizeof(path); + if (!sanitize(p, path, &length)) { + return NULL; + } -bool lovrFilesystemGetInfo(const char* path, FileInfo* info, bool needTime) { - fs_info stat; - if (archiveStat(path, &stat, needTime)) { - if (info) { - info->type = (FileType) stat.type; - info->size = stat.size; - info->lastModified = stat.lastModified; + FOREACH_ARCHIVE(archive) { + fs_info fsinfo; + const char* localPath; + if (getLocalPath(archive, path, length, &localPath)) { + if (archive->vtable->stat(archive, localPath, &fsinfo, needTime)) { + if (info) { + info->type = (FileType) fsinfo.type; + info->size = fsinfo.size; + info->lastModified = fsinfo.lastModified; + } + return true; + } + } else if (isVirtualDirectory(archive, path, length)) { + info->type = FS_DIRECTORY; + info->lastModified = ~0ull; + info->size = 0; + return true; } - return true; - } else { - return false; } -} -// IO + return lovrSetError("File not found"); +} void* lovrFilesystemRead(const char* p, size_t* size) { - Handle handle; char path[1024]; size_t length = sizeof(path); - if (sanitize(p, path, &length)) { - FOREACH_ARCHIVE(archive) { - if (!archiveContains(archive, path, length)) { - continue; - } - - if (!archive->vtable->open(archive, path, OPEN_READ, &handle)) { - continue; - } + if (!sanitize(p, path, &length)) { + return NULL; + } - uint64_t bytes; - if (!archive->vtable->fsize(archive, &handle, &bytes)) { - archive->vtable->close(archive, &handle); - return NULL; - } + FOREACH_ARCHIVE(archive) { + const char* localPath; + if (!getLocalPath(archive, path, length, &localPath)) { + continue; + } - if (bytes > SIZE_MAX) { - archive->vtable->close(archive, &handle); - lovrSetError("File is too big"); - return NULL; - } + Handle handle; + if (!archive->vtable->open(archive, localPath, OPEN_READ, &handle)) { + continue; + } - *size = (size_t) bytes; - void* data = lovrMalloc(*size); + uint64_t bytes; + if (!archive->vtable->fsize(archive, &handle, &bytes)) { + archive->vtable->close(archive, &handle); + return NULL; + } - if (archive->vtable->read(archive, &handle, data, *size, size)) { - archive->vtable->close(archive, &handle); - return data; - } else { - archive->vtable->close(archive, &handle); - lovrFree(data); - return NULL; - } + if (bytes > SIZE_MAX) { + archive->vtable->close(archive, &handle); + lovrSetError("File is too big"); + return NULL; } - lovrSetError("File not found"); + *size = (size_t) bytes; + void* data = lovrMalloc(*size); + + if (archive->vtable->read(archive, &handle, data, *size, size)) { + archive->vtable->close(archive, &handle); + return data; + } else { + archive->vtable->close(archive, &handle); + lovrFree(data); + return NULL; + } } + + lovrSetError("Not found"); return NULL; } @@ -455,11 +484,12 @@ bool lovrFilesystemWrite(const char* p, const char* content, size_t size, bool a return false; } - Archive* archive = findWriteArchive(path, length); + const char* localPath; + Archive* archive = findWriteArchive(path, length, &localPath); if (!archive) return false; Handle handle; - if (!archive->vtable->open(archive, path, append ? OPEN_APPEND : OPEN_WRITE, &handle)) { + if (!archive->vtable->open(archive, localPath, append ? OPEN_APPEND : OPEN_WRITE, &handle)) { return false; } @@ -484,8 +514,9 @@ bool lovrFilesystemRemove(const char* p) { return false; } - Archive* archive = findWriteArchive(path, length); - return archive && archive->vtable->remove(archive, path); + const char* localPath; + Archive* archive = findWriteArchive(path, length, &localPath); + return archive && archive->vtable->remove(archive, localPath); } bool lovrFilesystemCreateDirectory(const char* p) { @@ -495,7 +526,8 @@ bool lovrFilesystemCreateDirectory(const char* p) { return false; } - Archive* archive = findWriteArchive(path, length); + const char* localPath; + Archive* archive = findWriteArchive(path, length, &localPath); return archive && archive->vtable->mkdir(archive, path); } @@ -504,21 +536,17 @@ void lovrFilesystemGetDirectoryItems(const char* p, void (*callback)(void* conte size_t length = sizeof(path); if (sanitize(p, path, &length)) { FOREACH_ARCHIVE(archive) { - if (archive->mountLength == 0) { + const char* localPath; + if (getLocalPath(archive, path, length, &localPath)) { archive->vtable->list(archive, path, callback, context); - } else if (memcmp(archive->mountpoint, path, MIN(length, archive->mountLength))) { - continue; - } else if (length < archive->mountLength && (archive->mountpoint[length] == '/' || length == 0)) { - size_t offset = length > 0 ? length + 1 : 0; - const char* leaf = archive->mountpoint + offset; - const char* slash = strchr(leaf, '/'); - size_t sublength = slash ? slash - leaf : archive->mountLength - offset; + } else if (isVirtualDirectory(archive, path, length)) { char buffer[1024]; - memcpy(buffer, leaf, sublength); + const char* start = length ? archive->mountpoint + length + 1 : archive->mountpoint; + const char* slash = strchr(start, '/'); + size_t sublength = slash ? slash - start : archive->mountpoint + archive->mountLength - start; + memcpy(buffer, start, sublength); buffer[sublength] = '\0'; callback(context, buffer); - } else if (path[archive->mountLength] == '/' || path[archive->mountLength] == '\0') { - archive->vtable->list(archive, path, callback, context); } } } @@ -634,14 +662,7 @@ void lovrFilesystemSetRequirePath(const char* requirePath) { // Archive: dir -static bool dir_resolve(Archive* archive, const char* fullpath, char* buffer) { - if (strlen(fullpath) == archive->mountLength) { - memcpy(buffer, archive->path, archive->pathLength); - buffer[archive->pathLength] = '\0'; - return true; - } - - const char* path = fullpath + (archive->mountLength ? archive->mountLength + 1 : 0); +static bool dir_resolve(Archive* archive, const char* path, char* buffer) { return concat(buffer, archive->path, archive->pathLength, path, strlen(path)); } @@ -924,9 +945,7 @@ static bool zip_init(Archive* archive, const char* filename, MountMode mode, con return true; } -static zip_node* zip_resolve(Archive* archive, const char* fullpath) { - if (strlen(fullpath) <= archive->mountLength) return &archive->nodes.data[0]; - const char* path = fullpath + (archive->mountLength ? archive->mountLength + 1 : 0); +static zip_node* zip_resolve(Archive* archive, const char* path) { size_t length = strlen(path); if (length == 0) return &archive->nodes.data[0]; uint64_t hash = hash64(path, length); @@ -1187,11 +1206,12 @@ File* lovrFileCreate(const char* p, OpenMode mode) { continue; } - if (!archiveContains(a, path, length)) { + const char* localPath; + if (!getLocalPath(a, path, length, &localPath)) { continue; } - if (!a->vtable->open(a, path, mode, &handle)) { + if (!a->vtable->open(a, localPath, mode, &handle)) { continue; } diff --git a/test/lovr/filesystem.lua b/test/lovr/filesystem.lua index 9dde60c00..ffb7f5ff5 100644 --- a/test/lovr/filesystem.lua +++ b/test/lovr/filesystem.lua @@ -78,6 +78,7 @@ group('filesystem', function() expect(lovr.filesystem.mount(archive, 'base')).to.equal(true) expect(lovr.filesystem.isDirectory('a')).to.equal(false) expect(lovr.filesystem.isDirectory('ba')).to.equal(false) + expect(lovr.filesystem.isDirectory('base')).to.equal(true) expect(lovr.filesystem.isDirectory('base/asdf')).to.equal(false) expect(lovr.filesystem.isDirectory('base/a')).to.equal(true) expect(lovr.filesystem.isDirectory('base/a/b')).to.equal(true) From 108dc69701e7742f540f653a77c1a8a3d2820dc0 Mon Sep 17 00:00:00 2001 From: bjorn Date: Thu, 24 Sep 2026 16:26:01 -0700 Subject: [PATCH 7/9] lovr.filesystem supports absolute paths; --- src/modules/filesystem/filesystem.c | 389 +++++++++++++++++----------- 1 file changed, 237 insertions(+), 152 deletions(-) diff --git a/src/modules/filesystem/filesystem.c b/src/modules/filesystem/filesystem.c index 76d59fe34..295b7a84c 100644 --- a/src/modules/filesystem/filesystem.c +++ b/src/modules/filesystem/filesystem.c @@ -53,7 +53,7 @@ typedef struct { bool (*write)(Archive* archive, Handle* handle, const uint8_t* data, size_t size, size_t* count); bool (*seek)(Archive* archive, Handle* handle, uint64_t offset); bool (*fsize)(Archive* archive, Handle* handle, uint64_t* size); - bool (*stat)(Archive* archive, const char* path, fs_info* info, bool needTime); + bool (*stat)(Archive* archive, const char* path, FileInfo* info, bool needTime); bool (*remove)(Archive* archive, const char* path); bool (*mkdir)(Archive* archive, const char* path); void (*list)(Archive* archive, const char* path, fs_list_cb callback, void* context); @@ -86,6 +86,7 @@ struct File { static atomic_uint ref; static struct { + Archive* root; Archive* archives; size_t savePathLength; char savePath[1024]; @@ -94,6 +95,8 @@ static struct { char identity[64]; } state; +// Helpers + static bool checkfs(int result) { switch (result) { case FS_OK: return true; @@ -114,6 +117,18 @@ static bool checkfs(int result) { } } +static bool isSlash(char c) { + return c == '/' || c == '\\'; +} + +static bool isAbsolute(const char* path) { +#ifdef _WIN32 + return (isSlash(path[0]) && path[1] == path[0]) || (path[0] && path[1] == ':' && isSlash(path[2])); +#else + return path[0] == '/'; +#endif +} + // Rejects any path component that would escape the virtual filesystem (./, ../, :, and \) static bool valid(const char* path) { if (path[0] == '.' && (path[1] == '\0' || path[1] == '.')) { @@ -134,16 +149,6 @@ static bool valid(const char* path) { return true; } -// Does not work with empty strings -static bool concat(char* buffer, const char* p1, size_t length1, const char* p2, size_t length2) { - if (length1 + 1 + length2 >= LOVR_PATH_MAX) return lovrSetError("Path is too long"); - memcpy(buffer + length1 + 1, p2, length2); - buffer[length1 + 1 + length2] = '\0'; - memcpy(buffer, p1, length1); - buffer[length1] = '/'; - return true; -} - // Buffer must be at least as big as path length static size_t normalize(const char* path, size_t length, char* buffer) { size_t i = 0; @@ -168,6 +173,66 @@ static bool sanitize(const char* path, char* buffer, size_t* length) { return true; } +// If the Archive contains the path, sets the "local" path (path with mountpoint stripped off) +static bool getLocalPath(Archive* archive, const char* path, size_t length, const char** localPath) { + if (archive->mountLength == 0) { + *localPath = path; + return true; + } + + if (length < archive->mountLength) { + return false; + } + + if (memcmp(path, archive->mountpoint, archive->mountLength)) { + return false; + } + + if (path[archive->mountLength] == '/' || path[archive->mountLength] == '\0') { + *localPath = path + archive->mountLength + (path[archive->mountLength] == '/'); + return true; + } + + return false; +} + +// Checks if a path is part of an Archive's mountpoint but isn't in the Archive itself +static bool isVirtualDirectory(Archive* archive, const char* path, size_t length) { + return length < archive->mountLength && (length == 0 || archive->mountpoint[length] == '/') && !memcmp(path, archive->mountpoint, length); +} + +// Finds the writable Archive with the deepest mountpoint containing the path +static Archive* findWriteArchive(const char* path, size_t length, const char** localPath) { + uint32_t maxDepth = 0; + Archive* deepest = NULL; + + FOREACH_ARCHIVE(archive) { + if (archive->mode == MOUNT_READ) { + continue; + } + + const char* local; + if (!getLocalPath(archive, path, length, &local)) { + continue; + } + + if (!deepest || archive->mountDepth > maxDepth) { + maxDepth = archive->mountDepth; + deepest = archive; + *localPath = local; + } + } + + if (!deepest) { + lovrSetError("No writable archive found"); + return NULL; + } + + return deepest; +} + +// Entry + bool lovrFilesystemInit(void) { if (!lovrModuleAcquire(&ref)) return true; @@ -177,6 +242,9 @@ bool lovrFilesystemInit(void) { lovrFilesystemSetRequirePath("?.lua;?/init.lua"); #endif + state.root = lovrArchiveCreate(NULL, NULL, MOUNT_READWRITE, NULL); + if (!state.root) return false; + // On Android, the save directory is mounted early, because the identity is fixed to the package // name and it is convenient to be able to load main.lua and conf.lua from the save directory, // which requires it to be mounted early in the boot process. @@ -211,6 +279,7 @@ void lovrFilesystemDestroy(void) { lovrRelease(archive, lovrArchiveDestroy); archive = next; } + lovrRelease(state.root, lovrArchiveDestroy); lovrFilesystemUnwatch(); lovrFree(state.requirePath); memset(&state, 0, sizeof(state)); @@ -319,65 +388,11 @@ bool lovrFilesystemUnmount(const char* path) { return false; } -// If the Archive contains the path, sets the "local" path (path with mountpoint stripped off) -static bool getLocalPath(Archive* archive, const char* path, size_t length, const char** localPath) { - if (archive->mountLength == 0) { - *localPath = path; - return true; - } - - if (length < archive->mountLength) { - return false; - } - - if (memcmp(path, archive->mountpoint, archive->mountLength)) { - return false; - } - - if (path[archive->mountLength] == '/' || path[archive->mountLength] == '\0') { - *localPath = path + archive->mountLength + (path[archive->mountLength] == '/'); - return true; - } - - return false; -} - -// Checks if a path is part of an Archive's mountpoint but isn't in the Archive itself -static bool isVirtualDirectory(Archive* archive, const char* path, size_t length) { - return length < archive->mountLength && (length == 0 || archive->mountpoint[length] == '/') && !memcmp(path, archive->mountpoint, length); -} - -// Finds the writable Archive with the deepest mountpoint containing the path -static Archive* findWriteArchive(const char* path, size_t length, const char** localPath) { - uint32_t maxDepth = 0; - Archive* deepest = NULL; - - FOREACH_ARCHIVE(archive) { - if (archive->mode == MOUNT_READ) { - continue; - } - - const char* local; - if (!getLocalPath(archive, path, length, &local)) { - continue; - } - - if (!deepest || archive->mountDepth > maxDepth) { - maxDepth = archive->mountDepth; - deepest = archive; - *localPath = local; - } - } - - if (!deepest) { - lovrSetError("No writable archive found"); - return NULL; +const char* lovrFilesystemGetRealDirectory(const char* p) { + if (isAbsolute(p)) { + return p; } - return deepest; -} - -const char* lovrFilesystemGetRealDirectory(const char* p) { char path[1024]; size_t length = sizeof(path); if (!sanitize(p, path, &length)) { @@ -385,7 +400,7 @@ const char* lovrFilesystemGetRealDirectory(const char* p) { } FOREACH_ARCHIVE(archive) { - fs_info info; + FileInfo info; const char* localPath; if (getLocalPath(archive, path, length, &localPath) && archive->vtable->stat(archive, localPath, &info, false)) { return archive->path; @@ -401,6 +416,10 @@ const char* lovrFilesystemGetRealDirectory(const char* p) { // IO bool lovrFilesystemGetInfo(const char* p, FileInfo* info, bool needTime) { + if (isAbsolute(p)) { + return state.root->vtable->stat(state.root, p, info, needTime); + } + char path[1024]; size_t length = sizeof(path); if (!sanitize(p, path, &length)) { @@ -408,21 +427,15 @@ bool lovrFilesystemGetInfo(const char* p, FileInfo* info, bool needTime) { } FOREACH_ARCHIVE(archive) { - fs_info fsinfo; const char* localPath; if (getLocalPath(archive, path, length, &localPath)) { - if (archive->vtable->stat(archive, localPath, &fsinfo, needTime)) { - if (info) { - info->type = (FileType) fsinfo.type; - info->size = fsinfo.size; - info->lastModified = fsinfo.lastModified; - } + if (archive->vtable->stat(archive, localPath, info, needTime)) { return true; } } else if (isVirtualDirectory(archive, path, length)) { - info->type = FS_DIRECTORY; - info->lastModified = ~0ull; + info->type = FILE_DIRECTORY; info->size = 0; + info->lastModified = ~0ull; return true; } } @@ -431,62 +444,81 @@ bool lovrFilesystemGetInfo(const char* p, FileInfo* info, bool needTime) { } void* lovrFilesystemRead(const char* p, size_t* size) { - char path[1024]; - size_t length = sizeof(path); - if (!sanitize(p, path, &length)) { - return NULL; - } + Archive* archive = NULL; + Handle handle; - FOREACH_ARCHIVE(archive) { - const char* localPath; - if (!getLocalPath(archive, path, length, &localPath)) { - continue; + if (isAbsolute(p)) { + if (state.root->vtable->open(state.root, p, OPEN_READ, &handle)) { + archive = state.root; + } else { + return NULL; } - - Handle handle; - if (!archive->vtable->open(archive, localPath, OPEN_READ, &handle)) { - continue; + } else { + char path[1024]; + size_t length = sizeof(path); + if (!sanitize(p, path, &length)) { + return NULL; } - uint64_t bytes; - if (!archive->vtable->fsize(archive, &handle, &bytes)) { - archive->vtable->close(archive, &handle); - return NULL; + FOREACH_ARCHIVE(a) { + const char* localPath; + if (!getLocalPath(a, path, length, &localPath)) { + continue; + } + + if (a->vtable->open(a, localPath, OPEN_READ, &handle)) { + archive = a; + break; + } } - if (bytes > SIZE_MAX) { - archive->vtable->close(archive, &handle); - lovrSetError("File is too big"); + if (!archive) { + lovrSetError("Not found"); return NULL; } + } - *size = (size_t) bytes; - void* data = lovrMalloc(*size); + uint64_t bytes; + if (!archive->vtable->fsize(archive, &handle, &bytes)) { + archive->vtable->close(archive, &handle); + return NULL; + } - if (archive->vtable->read(archive, &handle, data, *size, size)) { - archive->vtable->close(archive, &handle); - return data; - } else { - archive->vtable->close(archive, &handle); - lovrFree(data); - return NULL; - } + if (bytes > SIZE_MAX) { + archive->vtable->close(archive, &handle); + lovrSetError("File is too big"); + return NULL; } - lovrSetError("Not found"); - return NULL; + *size = (size_t) bytes; + void* data = lovrMalloc(*size); + if (!archive->vtable->read(archive, &handle, data, *size, size)) { + archive->vtable->close(archive, &handle); + lovrFree(data); + return NULL; + } + + archive->vtable->close(archive, &handle); + return data; } bool lovrFilesystemWrite(const char* p, const char* content, size_t size, bool append) { - char path[1024]; - size_t length = sizeof(path); - if (!sanitize(p, path, &length)) { - return false; - } - + Archive* archive; const char* localPath; - Archive* archive = findWriteArchive(path, length, &localPath); - if (!archive) return false; + + if (isAbsolute(p)) { + archive = state.root; + localPath = p; + } else { + char path[1024]; + size_t length = sizeof(path); + if (!sanitize(p, path, &length)) { + return false; + } + + archive = findWriteArchive(path, length, &localPath); + if (!archive) return false; + } Handle handle; if (!archive->vtable->open(archive, localPath, append ? OPEN_APPEND : OPEN_WRITE, &handle)) { @@ -508,45 +540,67 @@ bool lovrFilesystemWrite(const char* p, const char* content, size_t size, bool a } bool lovrFilesystemRemove(const char* p) { - char path[1024]; - size_t length = sizeof(path); - if (!sanitize(p, path, &length)) { - return false; + Archive* archive; + const char* localPath; + + if (isAbsolute(p)) { + archive = state.root; + localPath = p; + } else { + char path[1024]; + size_t length = sizeof(path); + if (!sanitize(p, path, &length)) { + return false; + } + + archive = findWriteArchive(path, length, &localPath); + if (!archive) return false; } - const char* localPath; - Archive* archive = findWriteArchive(path, length, &localPath); - return archive && archive->vtable->remove(archive, localPath); + return archive->vtable->remove(archive, localPath); } bool lovrFilesystemCreateDirectory(const char* p) { - char path[1024]; - size_t length = sizeof(path); - if (!sanitize(p, path, &length)) { - return false; + Archive* archive; + const char* localPath; + + if (isAbsolute(p)) { + archive = state.root; + localPath = p; + } else { + char path[1024]; + size_t length = sizeof(path); + if (!sanitize(p, path, &length)) { + return false; + } + + archive = findWriteArchive(path, length, &localPath); + if (!archive) return false; } - const char* localPath; - Archive* archive = findWriteArchive(path, length, &localPath); - return archive && archive->vtable->mkdir(archive, path); + return archive && archive->vtable->mkdir(archive, localPath); } void lovrFilesystemGetDirectoryItems(const char* p, void (*callback)(void* context, const char* path), void* context) { - char path[1024]; - size_t length = sizeof(path); - if (sanitize(p, path, &length)) { - FOREACH_ARCHIVE(archive) { - const char* localPath; - if (getLocalPath(archive, path, length, &localPath)) { - archive->vtable->list(archive, path, callback, context); - } else if (isVirtualDirectory(archive, path, length)) { - char buffer[1024]; - const char* start = length ? archive->mountpoint + length + 1 : archive->mountpoint; - const char* slash = strchr(start, '/'); - size_t sublength = slash ? slash - start : archive->mountpoint + archive->mountLength - start; - memcpy(buffer, start, sublength); - buffer[sublength] = '\0'; - callback(context, buffer); + if (isAbsolute(p)) { + fs_list(p, callback, context); + } else { + char path[1024]; + size_t length = sizeof(path); + if (sanitize(p, path, &length)) { + FOREACH_ARCHIVE(archive) { + const char* localPath; + if (getLocalPath(archive, path, length, &localPath)) { + archive->vtable->list(archive, localPath, callback, context); + } else if (isVirtualDirectory(archive, path, length)) { + char buffer[1024]; + const char* start = length ? archive->mountpoint + length + 1 : archive->mountpoint; + const char* slash = strchr(start, '/'); + size_t sublength = slash ? slash - start : archive->mountpoint + archive->mountLength - start; + memcpy(buffer, start, sublength); + buffer[sublength] = '\0'; + callback(context, buffer); + } } } } @@ -663,7 +717,19 @@ void lovrFilesystemSetRequirePath(const char* requirePath) { // Archive: dir static bool dir_resolve(Archive* archive, const char* path, char* buffer) { - return concat(buffer, archive->path, archive->pathLength, path, strlen(path)); + if (archive->path) { + size_t length1 = archive->pathLength; + size_t length2 = strlen(path); + if (length1 + 1 + length2 >= LOVR_PATH_MAX) return lovrSetError("Path is too long"); + memcpy(buffer + length1 + 1, path, length2); + buffer[length1 + 1 + length2] = '\0'; + memcpy(buffer, archive->path, length1); + buffer[length1] = '/'; + return true; + } else { + memcpy(buffer, path, strlen(path) + 1); + return true; + } } static bool dir_open(Archive* archive, const char* path, OpenMode mode, Handle* handle) { @@ -713,9 +779,19 @@ static bool dir_fsize(Archive* archive, Handle* handle, uint64_t* size) { } } -static bool dir_stat(Archive* archive, const char* path, fs_info* info, bool needTime) { +static bool dir_stat(Archive* archive, const char* path, FileInfo* info, bool needTime) { char resolved[LOVR_PATH_MAX]; - return dir_resolve(archive, path, resolved) && checkfs(fs_stat(resolved, info)); + fs_info fsinfo; + if (dir_resolve(archive, path, resolved) && checkfs(fs_stat(resolved, &fsinfo))) { + if (info) { + info->type = (FileType) fsinfo.type; + info->size = fsinfo.size; + info->lastModified = fsinfo.lastModified; + } + return true; + } else { + return false; + } } static bool dir_remove(Archive* archive, const char* path) { @@ -1097,7 +1173,7 @@ static bool zip_fsize(Archive* archive, Handle* handle, uint64_t* size) { return true; } -static bool zip_stat(Archive* archive, const char* path, fs_info* info, bool needTime) { +static bool zip_stat(Archive* archive, const char* path, FileInfo* info, bool needTime) { zip_node* node = zip_resolve(archive, path); if (!node) return lovrSetError("File not found"); @@ -1116,7 +1192,7 @@ static bool zip_stat(Archive* archive, const char* path, fs_info* info, bool nee } info->size = node->uncompressedSize; - info->type = node->directory ? FS_DIRECTORY : FS_REGULAR; + info->type = node->directory ? FILE_DIRECTORY : FILE_REGULAR; return true; } @@ -1145,6 +1221,15 @@ static ArchiveInterface ArchiveZip = { // Archive Archive* lovrArchiveCreate(const char* path, const char* mountpoint, MountMode mode, const char* root) { + // Root archive + if (!path) { + Archive* archive = lovrCalloc(sizeof(Archive)); + archive->ref = 1; + archive->mode = mode; + archive->vtable = &ArchiveDir; + return archive; + } + fs_info info; if (!checkfs(fs_stat(path, &info))) { return NULL; From 1dfd93b33db6962c05740742c2c3a018cae3ee3a Mon Sep 17 00:00:00 2001 From: bjorn Date: Thu, 24 Sep 2026 17:44:48 -0700 Subject: [PATCH 8/9] lovr.filedropped; GLFW only for now. --- src/api/l_event.c | 6 ++++++ src/core/os.h | 2 ++ src/core/os_android.c | 4 ++++ src/core/os_glfw.h | 21 +++++++++++++++++++-- src/core/os_linux.c | 4 ++++ src/core/os_wasm.c | 4 ++++ src/core/os_win32.c | 4 ++++ src/modules/event/event.c | 4 ++++ src/modules/event/event.h | 6 ++++++ src/modules/system/system.c | 9 +++++++++ 10 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/api/l_event.c b/src/api/l_event.c index 61acbd604..d4df11d81 100644 --- a/src/api/l_event.c +++ b/src/api/l_event.c @@ -32,6 +32,7 @@ StringEntry lovrEventType[] = { [EVENT_THREAD_ERROR] = ENTRY("threaderror"), #endif [EVENT_FILECHANGED] = ENTRY("filechanged"), + [EVENT_FILEDROPPED] = ENTRY("filedropped"), [EVENT_PERMISSION] = ENTRY("permission"), { 0 } }; @@ -131,6 +132,11 @@ static int nextEvent(lua_State* L) { lovrFree(event.data.file.oldpath); return 4; + case EVENT_FILEDROPPED: + lua_pushstring(L, event.data.drop.path); + lovrFree(event.data.drop.path); + return 2; + case EVENT_PERMISSION: luax_pushenum(L, Permission, event.data.permission.permission); lua_pushboolean(L, event.data.permission.granted); diff --git a/src/core/os.h b/src/core/os.h index f0249af1d..115e277d1 100644 --- a/src/core/os.h +++ b/src/core/os.h @@ -147,6 +147,7 @@ typedef void fn_text(uint32_t codepoint); typedef void fn_mouse_button(int button, bool pressed); typedef void fn_mouse_move(double x, double y); typedef void fn_mousewheel_move(double deltaX, double deltaY); +typedef void fn_file_drop(const char* path); typedef void fn_permission(os_permission permission, bool granted); bool os_init(void); @@ -174,6 +175,7 @@ void os_on_text(fn_text* callback); void os_on_mouse_button(fn_mouse_button* callback); void os_on_mouse_move(fn_mouse_move* callback); void os_on_mousewheel_move(fn_mousewheel_move* callback); +void os_on_file_drop(fn_file_drop* callback); void os_on_permission(fn_permission* callback); bool os_window_open(const os_window_config* config); diff --git a/src/core/os_android.c b/src/core/os_android.c index fa2278209..b6de7c213 100644 --- a/src/core/os_android.c +++ b/src/core/os_android.c @@ -338,6 +338,10 @@ void os_on_mousewheel_move(fn_mousewheel_move* callback) { // } +void os_on_file_drop(fn_file_drop* callback) { + // +} + void os_on_permission(fn_permission* callback) { state.onPermissionEvent = callback; } diff --git a/src/core/os_glfw.h b/src/core/os_glfw.h index ab9ad7d2a..d8e774e5d 100644 --- a/src/core/os_glfw.h +++ b/src/core/os_glfw.h @@ -64,8 +64,11 @@ void os_on_mouse_move(fn_mouse_move* callback) { // } -void os_on_mousewheel_move(fn_wheel_move* callback) -{ +void os_on_mousewheel_move(fn_wheel_move* callback) { + // +} + +void os_on_file_drop(fn_file_drop* callback) { // } @@ -136,6 +139,7 @@ static struct { fn_mouse_button* onMouseButton; fn_mouse_move* onMouseMove; fn_mousewheel_move* onMouseWheelMove; + fn_file_drop* onFileDrop; uint32_t width; uint32_t height; bool fullscreen; @@ -314,6 +318,14 @@ static void onMouseWheelMove(GLFWwindow* window, double deltaX, double deltaY) { } } +static void onFileDrop(GLFWwindow* window, int count, const char** paths) { + if (glfwState.onFileDrop) { + for (int i = 0; i < count; i++) { + glfwState.onFileDrop(paths[i]); + } + } +} + const char* os_get_clipboard_text(void) { return glfwGetClipboardString(NULL); } @@ -404,6 +416,7 @@ bool os_window_open(const os_window_config* config) { glfwSetMouseButtonCallback(glfwState.window, onMouseButton); glfwSetCursorPosCallback(glfwState.window, onMouseMove); glfwSetScrollCallback(glfwState.window, onMouseWheelMove); + glfwSetDropCallback(glfwState.window, onFileDrop); glfwState.width = config->width; glfwState.height = config->height; glfwState.fullscreen = config->fullscreen; @@ -502,6 +515,10 @@ void os_on_mousewheel_move(fn_mousewheel_move* callback) { glfwState.onMouseWheelMove = callback; } +void os_on_file_drop(fn_file_drop* callback) { + glfwState.onFileDrop = callback; +} + void os_get_mouse_position(double* x, double* y) { if (glfwState.window) { glfwGetCursorPos(glfwState.window, x, y); diff --git a/src/core/os_linux.c b/src/core/os_linux.c index 54347c97f..cf1b68b78 100644 --- a/src/core/os_linux.c +++ b/src/core/os_linux.c @@ -434,6 +434,10 @@ void os_on_mousewheel_move(fn_mousewheel_move* callback) { state.onWheelMove = callback; } +void os_on_file_drop(fn_file_drop* callback) { + // +} + bool os_window_open(const os_window_config* config) { state.connection = xcb_connect(NULL, NULL); diff --git a/src/core/os_wasm.c b/src/core/os_wasm.c index 3ed8aa4c0..f9f01986b 100644 --- a/src/core/os_wasm.c +++ b/src/core/os_wasm.c @@ -282,6 +282,10 @@ void os_poll_events(double timeout) { // } +void os_on_file_drop(fn_file_drop* callback) { + // +} + void os_on_permission(fn_permission* callback) { // } diff --git a/src/core/os_win32.c b/src/core/os_win32.c index f688636bc..67310f4b6 100644 --- a/src/core/os_win32.c +++ b/src/core/os_win32.c @@ -489,6 +489,10 @@ void os_on_mousewheel_move(fn_mousewheel_move* callback) { state.onWheelMove = callback; } +void os_on_file_drop(fn_file_drop* callback) { + // +} + bool os_window_open(const os_window_config* config) { if (state.window) { return true; diff --git a/src/modules/event/event.c b/src/modules/event/event.c index 0619dda6c..1dbce7641 100644 --- a/src/modules/event/event.c +++ b/src/modules/event/event.c @@ -60,6 +60,10 @@ void lovrEventPush(Event event) { event.data.file.oldpath = lovrStrdup(event.data.file.oldpath); } + if (event.type == EVENT_FILEDROPPED) { + event.data.drop.path = lovrStrdup(event.data.drop.path); + } + mtx_lock(&state.lock); arr_push(&state.events, event); mtx_unlock(&state.lock); diff --git a/src/modules/event/event.h b/src/modules/event/event.h index 0adad3b40..c336610e5 100644 --- a/src/modules/event/event.h +++ b/src/modules/event/event.h @@ -32,6 +32,7 @@ typedef enum { EVENT_THREAD_ERROR, #endif EVENT_FILECHANGED, + EVENT_FILEDROPPED, EVENT_PERMISSION, EVENT_CUSTOM } EventType; @@ -94,6 +95,10 @@ typedef struct { int action; } FileEvent; +typedef struct { + char* path; +} DropEvent; + typedef struct { uint32_t permission; bool granted; @@ -117,6 +122,7 @@ typedef union { MouseWheelEvent wheel; ThreadEvent thread; FileEvent file; + DropEvent drop; PermissionEvent permission; CustomEvent custom; } EventData; diff --git a/src/modules/system/system.c b/src/modules/system/system.c index f108599f5..acad72a6e 100644 --- a/src/modules/system/system.c +++ b/src/modules/system/system.c @@ -78,6 +78,13 @@ static void onPermission(os_permission permission, bool granted) { }); } +static void onFileDrop(const char* path) { + lovrEventPush((Event) { + .type = EVENT_FILEDROPPED, + .data.drop.path = (char*) path + }); +} + static void onQuit(void) { lovrEventPush((Event) { .type = EVENT_QUIT, @@ -108,6 +115,7 @@ bool lovrSystemInit(void) { os_on_mouse_button(onMouseButton); os_on_mouse_move(onMouseMove); os_on_mousewheel_move(onWheelMove); + os_on_file_drop(onFileDrop); os_on_permission(onPermission); os_get_mouse_position(&state.mouseX, &state.mouseY); lovrModuleReady(&ref); @@ -118,6 +126,7 @@ void lovrSystemDestroy(void) { if (!lovrModuleRelease(&ref)) return; os_on_key(NULL); os_on_text(NULL); + os_on_file_drop(NULL); os_on_permission(NULL); memset(&state, 0, sizeof(state)); lovrModuleReset(&ref); From df38f5da404b50d9f6f94a88c44a675b89484141 Mon Sep 17 00:00:00 2001 From: bjorn Date: Sun, 27 Sep 2026 18:30:59 -0700 Subject: [PATCH 9/9] Fix windows; --- src/core/fs.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/fs.c b/src/core/fs.c index e81235992..e3622c6e3 100644 --- a/src/core/fs.c +++ b/src/core/fs.c @@ -90,7 +90,7 @@ fs_error fs_fstat(fs_handle file, fs_info* info) { } info->size = size.QuadPart; info->lastModified = 0; - info->type = FILE_REGULAR; + info->type = FS_REGULAR; return FS_OK; }