Skip to content

Commit

Permalink
getUserDirectory: return libretro Save Directory instead of $USER env
Browse files Browse the repository at this point in the history
 - Fixes a mem corruption bug: adding a trailing slash was corrupting the env table of the process
 - converted implementation to use `lual_dostring` thus performing non-trivial string operations within the safe GC'd confines of Lua

getUserDirectory returns nil if the libretro frontend doesn't support it. This is done rather than using a substitution because there is no suitable substitution that is guaranteed to be writable. It's better to fail on returning a valid path than to fail in unexpected ways later on when trying to write data to the directory. The love/lutro game app will need to check for hand handle such cases appropriately if it wants to be well-behaved running under libretro.

It occurs to me that it could be swell if there was an IDE and/or C preprocessor that allowed embedding lua into C code. Something similar perhaps to how one can embed PHP code inside HTML, as an example.
  • Loading branch information
jstine35 committed Jan 21, 2025
1 parent cf3f5ca commit 3a86b49
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 35 deletions.
102 changes: 72 additions & 30 deletions filesystem.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@
#include <stdlib.h>
#include <string.h>

int _raw_get_user_writable_dir(lua_State *L)
{
char* savedir;
if ((*settings.environ_cb)(RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY, &savedir)) {
lua_pushstring(L, savedir);
return 1;
}

// in the case that no savedir is available: return nil rather than seeking a fallback.
return 0;
}

int lutro_filesystem_preload(lua_State *L)
{
static const luaL_Reg fs_funcs[] = {
Expand All @@ -23,17 +35,76 @@ int lutro_filesystem_preload(lua_State *L)
{ "getRequirePath", fs_getRequirePath },
{ "load", fs_load },
{ "setIdentity", fs_setIdentity },
{ "getUserDirectory", fs_getUserDirectory },
{ "getAppdataDirectory", fs_getAppdataDirectory },
{ "isDirectory", fs_isDirectory },
{ "isFile", fs_isFile },
{ "createDirectory", fs_createDirectory },
{ "getDirectoryItems", fs_getDirectoryItems },

// internal helpers for lua-authored functions.
{ "_raw_get_user_writable_dir", _raw_get_user_writable_dir },
{NULL, NULL}
};

lutro_newlib(L, fs_funcs, "filesystem");

// lutro.filesystem._appendTrailingSlash - appends a trailing slash to match behavior of LOVE.
// Because slash or backslash is platform dependent, this implementation takes the laziest
// approach: if the incoming path has a trailing backslash then it checks if the OS environment
// is Windows (WINDIR) and if yes then it doesn't append a trailing slash. It does not attempt
// to append trialing backslashes. Windows tolerates forward slash and mixed-slash paths well.
if (1) {
int ret = luaL_dostring(L, "\
lutro.filesystem._appendTrailingSlash = function(path)\n\
if path:sub(-1) == '\\\\' then\n\
local winDir = os.getenv('WINDIR')\n\
if winDir ~= nil and winDir ~= '' then\n\
path = path .. '/'\n\
end\n\
end\n\
if path:sub(-1) ~= '/' then\n\
path = path .. '/'\n\
end\n\
return path\n\
end");

if (ret) {
// don't hard-fail - no need to block apps that might not use the function.
lutro_errorf("failed to assign lutro.filesystem.getUserDirectory: %s\n", lua_tostring(L, -1));
lua_pop(L, 1); // pop error message
}
}

// lutro.filesystem.getUserDirectory
//
// Returns the writable save directory provided by libretro frontend. The directory
// may or may not be sandboxed according to a current user, depending on the design
// and capabilities of the platform OS. Funtion may return nil if the libretro frontend
// does not provide a writable save directory.
//
// implementation notes:
// - UserDirectory should always have a trailing slash (as confirmed on Love2D)
// - Cache the UserDir to avoid costly re-calculation of env var lookups. UserDir and all env vars
// are static for the lifetime of the process.
// - https://love2d.org/wiki/love.filesystem.getUserDirectory

if (1) {
int ret = luaL_dostring(L, "\
lutro.filesystem.getUserDirectory = function()\n\
local savedir = lutro.filesystem._raw_get_user_writable_dir()\n\
if savedir == nil or savedir == '' then\n\
return nil\n\
end\n\
return lutro.filesystem._appendTrailingSlash(savedir)\n\
end");

if (ret) {
// don't hard-fail - no need to block apps that might not use the function.
lutro_errorf("failed to assign lutro.filesystem.getUserDirectory: %s\n", lua_tostring(L, -1));
lua_pop(L, 1); // pop error message
}
}

return 1;
}

Expand Down Expand Up @@ -201,35 +272,6 @@ int fs_setIdentity(lua_State *L)
return 0;
}

/**
* lutro.filesystem.getUserDirectory
*
* https://love2d.org/wiki/love.filesystem.getUserDirectory
*/
int fs_getUserDirectory(lua_State *L)
{
// Retrieve the user's home directory from environment variables.
char *homedir = getenv("HOME");
if (homedir == NULL) {
homedir = getenv("HOMEDRIVE");
if (homedir == NULL) {
// TODO: Figure out what to do when HOME isn't available.
homedir = ".";
}
}

// If needed, append a / at the end of the homedir.
size_t len = strlen(homedir);
if (homedir[len] != '/') {
homedir[len++] = '/';
homedir[len] = '\0';
}

lua_pushstring(L, homedir);

return 1;
}

/**
* lutro.filesystem.getAppdataDirectory
*
Expand Down
2 changes: 1 addition & 1 deletion filesystem.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ int fs_setIdentity(lua_State *L);
int fs_isFile(lua_State *L);
int fs_isDirectory(lua_State *L);
int fs_createDirectory(lua_State *L);
int fs_getUserDirectory(lua_State *L);
int fs_getAppdataDirectory(lua_State *L);
int fs_getWorkingDirectory(lua_State *L);
int fs_getDirectoryItems(lua_State *L);

#endif // FILESYSTEM_H
13 changes: 9 additions & 4 deletions test/unit/modules/filesystem.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@ function lutro.filesystem.getAppdataDirectoryTest()
end

function lutro.filesystem.getUserDirectoryTest()
local homeDir = lutro.filesystem.getUserDirectory()
-- @todo Find out how to make os.getenv('HOME') work on Windows?
local luaHomeDir = os.getenv("HOME")
unit.assertEquals(homeDir, luaHomeDir)
-- UserDirectory should always have a trailing slash. This is easy to verify.
-- Other aspects of UserDirectory are platform specific and non-trivial to calculate and there
-- isn't much value to trying to replicate it here.
local userDir = lutro.filesystem.getUserDirectory()
local userDirFixed = getUserDir
if userDirFixed:sub(-1) ~= '/' then
userDirFixed = userDirFixed .. '/'
end
unit.assertEquals(userDir, userDirFixed)
end

function lutro.filesystem.getDirectoryItemsTest()
Expand Down

0 comments on commit 3a86b49

Please sign in to comment.