From 027a9523bec1fa8953de3462e616aaa8a950b55c Mon Sep 17 00:00:00 2001 From: Jon Booth Date: Mon, 9 Oct 2023 17:55:57 +1300 Subject: [PATCH] Change the FileSystem:Open (and io.open) commands to require a URI scheme path rather than having an explicit root argument. Valid schemes are "user://" and "data://" Also implement all of FileSystem in c++. --- data/libs/FileSystem.lua | 27 ---- .../{FileSystemBase.lua => FileSystem.lua} | 30 +++- src/lua/LuaFileSystem.cpp | 135 +++++++++++++----- src/lua/LuaFileSystem.h | 6 +- src/lua/core/Sandbox.cpp | 33 +---- 5 files changed, 129 insertions(+), 102 deletions(-) delete mode 100644 data/libs/FileSystem.lua rename data/meta/{FileSystemBase.lua => FileSystem.lua} (51%) diff --git a/data/libs/FileSystem.lua b/data/libs/FileSystem.lua deleted file mode 100644 index cd9912757b1..00000000000 --- a/data/libs/FileSystem.lua +++ /dev/null @@ -1,27 +0,0 @@ ----@class FileSystem : FileSystemBase -local FileSystem = package.core["FileSystem"] - ---- Wrapper for our patched io.open that ensures files are opened inside the sandbox. ---- Prefer using this to io.open ---- ---- Files in the user folder can be read or written to ---- Files in the data folder are read only. ---- ---- ---- ---- Example: ---- > f = FileSystem.Open( "USER", "my_file.txt", "w" ) ---- > f:write( "file contents" ) ---- > f:close() ---- ----@param root string A FileSystemRoot constant. Can be either "DATA" or "USER" ----@param filename string The name of the file to open, relative to the root ----@param mode string|nil The mode to open the file in, defaults to read only ----@return file A lua io file -function FileSystem.Open( root, filename, mode ) - if not mode then mode = "r" end - - return io.open( filename, mode, root ) -end - -return FileSystem diff --git a/data/meta/FileSystemBase.lua b/data/meta/FileSystem.lua similarity index 51% rename from data/meta/FileSystemBase.lua rename to data/meta/FileSystem.lua index 20418a4145b..f09c6ece6c6 100644 --- a/data/meta/FileSystemBase.lua +++ b/data/meta/FileSystem.lua @@ -6,8 +6,8 @@ ---@meta ----@class FileSystemBase -local FileSystemBase = {} +---@class FileSystem +local FileSystem = {} ---@param root string A FileSystemRoot constant. Can be either "DATA" or "USER" ---@return string[] files A list of files as full paths from the root @@ -15,17 +15,35 @@ local FileSystemBase = {} --- --- Example: --- > local files, dirs = FileSystem.ReadDirectory(root, path) -function FileSystemBase.ReadDirectory(root, path) end +function FileSystem.ReadDirectory(root, path) end --- Join the passed arguments into a path, correctly handling separators and . --- and .. special dirs. --- ---@param arg string[] A list of path elements to be joined ---@return string The joined path elements -function FileSystemBase.JoinPath( ... ) end +function FileSystem.JoinPath( ... ) end ---@param dir_name string The name of the folder to create in the user directory ---@return boolean Success -function FileSystemBase.MakeUserDataDirectory( dir_name ) end +function FileSystem.MakeUserDataDirectory( dir_name ) end -return FileSystemBase +--- Wrapper for our patched io.open that ensures files are opened inside the sandbox. +--- Prefer using this to io.open +--- +--- Files in the user folder can be read or written to +--- Files in the data folder are read only. +--- +--- +--- +--- Example: +--- > f = FileSystem.Open( "user://my_file.txt", "w" ) +--- > f:write( "file contents" ) +--- > f:close() +--- +---@param filename string The name of the file to open, must start either user:// or data:// +---@param mode string|nil The mode to open the file in, defaults to read only. Only user location files can be written +---@return file A lua io file +function FileSystem.Open( dir_name ) end + +return FileSystem diff --git a/src/lua/LuaFileSystem.cpp b/src/lua/LuaFileSystem.cpp index c1ecf79ea9e..00c0e0acf2d 100644 --- a/src/lua/LuaFileSystem.cpp +++ b/src/lua/LuaFileSystem.cpp @@ -7,6 +7,105 @@ #include "LuaObject.h" #include "Pi.h" +#include "core/StringUtils.h" + +class ParsedFilePath +{ +public: + ParsedFilePath(lua_State* l, const char* luaPath); + + // Probably not needed as if it isn't valid we'll already fail. + bool IsValid() const { return m_fs != nullptr; } + + bool ValidateWritePermission(lua_State* l) const; + FileSystem::FileSource& GetFileSource() const { return *m_fs; } + const std::string_view& GetRelativePath() const { return m_fs_path; } + + std::string CalculateAbsolutePath(lua_State* l) const; +private: + std::string_view m_luaPath; + std::string_view m_scheme; + std::string_view m_fs_path; + FileSystem::FileSource* m_fs; +}; + +ParsedFilePath::ParsedFilePath(lua_State* l, const char* luaPath) + : m_luaPath(luaPath) +{ + if (starts_with_ci(m_luaPath, "user://")) + { + m_fs = &FileSystem::userFiles; + m_scheme = m_luaPath.substr(0, 7);// strlen("user://")); + } + else if (starts_with_ci(m_luaPath, "data://")) + { + m_fs = &FileSystem::gameDataFiles; + m_scheme = m_luaPath.substr(0, 7);// strlen("data://")); + } + else + { + luaL_error(l, "'%s' does not have a valid scheme, must be user:// or data://", luaPath); + m_fs = nullptr; + return; + } + m_fs_path = m_luaPath; + m_fs_path.remove_prefix(m_scheme.length()); +} + +bool ParsedFilePath::ValidateWritePermission(lua_State* l) const +{ + if (m_fs == &FileSystem::userFiles) + { + return true; + } + luaL_error(l, "'%s' does not support file operations that modify it, only things in the user folder do", m_luaPath.data()); + return false; +} + +std::string ParsedFilePath::CalculateAbsolutePath(lua_State* l) const +{ + try + { + return std::move(m_fs->Lookup(std::string(m_fs_path)).GetAbsolutePath()); + } + catch (std::invalid_argument e) + { + luaL_error(l, "'%s' is not a valid file in its scheme root' - Is the file location within the root?", m_luaPath.data()); + return ""; + } +} + +static lua_CFunction l_original_io_open = nullptr; + +void LuaFileSystem::register_raw_io_open_function(lua_CFunction open) +{ + l_original_io_open = open; +} + +int LuaFileSystem::l_patched_io_open(lua_State* L) +{ + ParsedFilePath path = ParsedFilePath(L, lua_tostring(L, 1)); + const char* root_cstr = lua_tostring(L, 2); + for (const char* c = root_cstr; *c != 0; ++c ) + { + if (*c != 'r') + { + if (!path.ValidateWritePermission(L)) + { + return 0; + } + break; + } + } + + ::std::string abs_path = path.CalculateAbsolutePath(L); + lua_pushlstring(L, abs_path.c_str(), abs_path.length() ); + lua_replace(L, 1); + + const int rv = l_original_io_open(L); + + return rv; +} /* * Interface: FileSystem * @@ -35,41 +134,6 @@ FileSystem::FileSource* get_filesytem_for_root(LuaFileSystem::Root root) return fs; } -std::string LuaFileSystem::lua_path_to_fs_path(lua_State* l, const char* root_name, const char* path, const char* access) -{ - const LuaFileSystem::Root root = static_cast(LuaConstants::GetConstant(l, "FileSystemRoot", root_name)); - - if (root == LuaFileSystem::ROOT_DATA) - { - // check the acces mode is allowed: - - // default mode is read only - if (access[0] != 0) - { - if (access[0] != 'r' || access[1] != 0) - { - // we are requesting an access mode not allowed for the user folder - // as you can't write there. - luaL_error(l, "'%s' is not valid for opening a file in root '%s'", access, root_name); - return ""; - } - } - } - - FileSystem::FileSource* fs = get_filesytem_for_root(root); - assert(fs); - - try - { - return std::move(fs->Lookup(path).GetAbsolutePath()); - } - catch (std::invalid_argument e) - { - luaL_error(l, "'%s' is not valid for opening a file in root '%s' - Is the file location within the root?", path, root_name); - return ""; - } -} - static void push_date_time(lua_State *l, const Time::DateTime &dt) { int year, month, day, hour, minute, second; @@ -237,6 +301,7 @@ void LuaFileSystem::Register() { "ReadDirectory", l_filesystem_read_dir }, { "JoinPath", l_filesystem_join_path }, { "MakeUserDataDirectory", l_filesystem_make_user_directory }, + { "Open", l_patched_io_open }, { 0, 0 } }; diff --git a/src/lua/LuaFileSystem.h b/src/lua/LuaFileSystem.h index 1cfdb571782..a03893caab2 100644 --- a/src/lua/LuaFileSystem.h +++ b/src/lua/LuaFileSystem.h @@ -7,6 +7,7 @@ #include struct lua_State; +typedef int (*lua_CFunction) (lua_State* L); namespace LuaFileSystem { void Register(); @@ -16,9 +17,8 @@ namespace LuaFileSystem { ROOT_DATA }; - // will throw lua errors if not allowed - // and return an empty string - std::string lua_path_to_fs_path(lua_State* l, const char* root, const char* path, const char* access); + void register_raw_io_open_function(lua_CFunction open); + int l_patched_io_open(lua_State* L); } // namespace LuaFileSystem #endif diff --git a/src/lua/core/Sandbox.cpp b/src/lua/core/Sandbox.cpp index 950e11aa433..ae13786d725 100644 --- a/src/lua/core/Sandbox.cpp +++ b/src/lua/core/Sandbox.cpp @@ -106,35 +106,6 @@ static int l_log_warning(lua_State *L) return 0; } -static lua_CFunction l_original_io_open = nullptr; - -static int l_patched_io_open(lua_State* L) -{ - if (lua_gettop(L) != 3) - { - luaL_error(L, "Wrong number of arguments for io.open(). You should be using FileSystem.Open() instead"); - return 0; - } - const char* path_arg = lua_tostring(L, 1); - const char* access_arg = lua_tostring(L, 2); - const char* root_arg = lua_tostring(L, 3); - - std::string path = LuaFileSystem::lua_path_to_fs_path(L, root_arg, path_arg, access_arg); - - if (path.length() == 0) - { - luaL_error(L, "attempt to access filesystem in an invalid user folder location"); - return 0; - } - - lua_pushstring(L, path.c_str()); - lua_replace(L, 1); - - const int rv = l_original_io_open(L); - - return rv; -} - static const luaL_Reg STANDARD_LIBS[] = { { "_G", luaopen_base }, { LUA_COLIBNAME, luaopen_coroutine }, @@ -202,13 +173,13 @@ void pi_lua_open_standard_base(lua_State *L) lua_getfield(L, -1, "open"); assert(lua_iscfunction(L, -1)); - l_original_io_open = lua_tocfunction(L, -1); + LuaFileSystem::register_raw_io_open_function(lua_tocfunction(L, -1)); lua_pop(L, 1); // pop the io table lua_getglobal(L, LUA_IOLIBNAME); // patch io.open so we can check the path - lua_pushcfunction(L, l_patched_io_open); + lua_pushcfunction(L, LuaFileSystem::l_patched_io_open); lua_setfield(L, -2, "open"); // remove io.popen as we don't want people running apps