#include "virtual_fs.h" #include #include #include #include #include #include namespace fs = std::filesystem; namespace mosis { //============================================================================= // VIRTUALFS IMPLEMENTATION //============================================================================= VirtualFS::VirtualFS(const std::string& app_id, const std::string& app_root, const VirtualFSLimits& limits) : m_app_id(app_id) , m_app_root(app_root) , m_limits(limits) { // Ensure app root exists std::error_code ec; fs::create_directories(m_app_root, ec); // Recalculate usage on startup RecalculateUsage(); } VirtualFS::~VirtualFS() { } //============================================================================= // PATH VALIDATION //============================================================================= bool VirtualFS::IsValidPathChar(char c) { // Allow alphanumeric, dash, underscore, dot, forward slash return std::isalnum(static_cast(c)) || c == '-' || c == '_' || c == '.' || c == '/'; } int VirtualFS::GetPathDepth(const std::string& path) { int depth = 0; for (char c : path) { if (c == '/') depth++; } return depth; } bool VirtualFS::ValidatePath(const std::string& virtual_path, std::string& error) { // Check length if (virtual_path.length() > m_limits.max_path_length) { error = "path too long"; return false; } // Must start with / if (virtual_path.empty() || virtual_path[0] != '/') { error = "path must start with /"; return false; } // Check valid prefix bool valid_prefix = false; if (virtual_path.find("/data/") == 0 || virtual_path == "/data") { valid_prefix = true; } else if (virtual_path.find("/cache/") == 0 || virtual_path == "/cache") { valid_prefix = true; } else if (virtual_path.find("/temp/") == 0 || virtual_path == "/temp") { valid_prefix = true; } else if (virtual_path.find("/shared/") == 0 || virtual_path == "/shared") { // Check permission for shared if (CheckPermission && !CheckPermission("storage.shared")) { error = "permission denied: storage.shared required"; return false; } valid_prefix = true; } if (!valid_prefix) { error = "invalid path prefix (must be /data/, /cache/, /temp/, or /shared/)"; return false; } // Check for path traversal if (virtual_path.find("..") != std::string::npos) { error = "path traversal not allowed"; return false; } // Check for double slashes (except at start) if (virtual_path.find("//") != std::string::npos) { error = "invalid path (double slashes)"; return false; } // Check all characters are valid for (char c : virtual_path) { if (!IsValidPathChar(c)) { error = "invalid character in path"; return false; } } // Check depth if (GetPathDepth(virtual_path) > m_limits.max_path_depth) { error = "path too deep"; return false; } return true; } std::string VirtualFS::ResolvePath(const std::string& virtual_path) { // Map virtual path to physical path // /data/foo.txt -> /data/foo.txt // /cache/bar.txt -> /cache/bar.txt // /temp/baz.txt -> /temp/baz.txt // /shared/x.txt -> /shared/x.txt fs::path base(m_app_root); // Remove leading slash and append std::string relative = virtual_path.substr(1); // Remove leading / return (base / relative).string(); } //============================================================================= // FILE OPERATIONS //============================================================================= bool VirtualFS::EnsureParentDir(const std::string& path) { fs::path p(path); fs::path parent = p.parent_path(); if (parent.empty()) return true; std::error_code ec; fs::create_directories(parent, ec); return !ec; } void VirtualFS::UpdateUsage(int64_t delta) { if (delta < 0 && static_cast(-delta) > m_used_bytes) { m_used_bytes = 0; } else { m_used_bytes = static_cast(static_cast(m_used_bytes) + delta); } } bool VirtualFS::CheckQuota(size_t additional_bytes, std::string& error) { if (m_used_bytes + additional_bytes > m_limits.max_quota_bytes) { error = "quota exceeded"; return false; } return true; } std::optional VirtualFS::Read(const std::string& path, std::string& error) { if (!ValidatePath(path, error)) { return std::nullopt; } std::string physical_path = ResolvePath(path); std::ifstream file(physical_path, std::ios::binary); if (!file) { error = "file not found"; return std::nullopt; } std::ostringstream ss; ss << file.rdbuf(); return ss.str(); } bool VirtualFS::Write(const std::string& path, const std::string& data, std::string& error) { if (!ValidatePath(path, error)) { return false; } // Check file size limit if (data.size() > m_limits.max_file_size) { error = "file size limit exceeded"; return false; } std::string physical_path = ResolvePath(path); // Get current file size if exists (for quota calculation) size_t old_size = 0; std::error_code ec; if (fs::exists(physical_path, ec)) { old_size = static_cast(fs::file_size(physical_path, ec)); } // Check quota for net change int64_t delta = static_cast(data.size()) - static_cast(old_size); if (delta > 0 && !CheckQuota(static_cast(delta), error)) { return false; } // Ensure parent directory exists if (!EnsureParentDir(physical_path)) { error = "failed to create parent directory"; return false; } std::ofstream file(physical_path, std::ios::binary | std::ios::trunc); if (!file) { error = "failed to open file for writing"; return false; } file.write(data.data(), data.size()); if (!file) { error = "failed to write data"; return false; } file.close(); UpdateUsage(delta); return true; } bool VirtualFS::Append(const std::string& path, const std::string& data, std::string& error) { if (!ValidatePath(path, error)) { return false; } std::string physical_path = ResolvePath(path); // Get current file size size_t current_size = 0; std::error_code ec; if (fs::exists(physical_path, ec)) { current_size = static_cast(fs::file_size(physical_path, ec)); } // Check file size limit if (current_size + data.size() > m_limits.max_file_size) { error = "file size limit exceeded"; return false; } // Check quota if (!CheckQuota(data.size(), error)) { return false; } // Ensure parent directory exists if (!EnsureParentDir(physical_path)) { error = "failed to create parent directory"; return false; } std::ofstream file(physical_path, std::ios::binary | std::ios::app); if (!file) { error = "failed to open file for appending"; return false; } file.write(data.data(), data.size()); if (!file) { error = "failed to append data"; return false; } file.close(); UpdateUsage(static_cast(data.size())); return true; } bool VirtualFS::Delete(const std::string& path, std::string& error) { if (!ValidatePath(path, error)) { return false; } std::string physical_path = ResolvePath(path); std::error_code ec; if (!fs::exists(physical_path, ec)) { error = "file not found"; return false; } // Get size before deletion size_t file_size = 0; if (fs::is_regular_file(physical_path, ec)) { file_size = static_cast(fs::file_size(physical_path, ec)); } if (!fs::remove(physical_path, ec)) { error = "failed to delete"; return false; } UpdateUsage(-static_cast(file_size)); return true; } bool VirtualFS::Exists(const std::string& path) { std::string error; if (!ValidatePath(path, error)) { return false; } std::string physical_path = ResolvePath(path); std::error_code ec; return fs::exists(physical_path, ec); } std::optional> VirtualFS::List(const std::string& path, std::string& error) { if (!ValidatePath(path, error)) { return std::nullopt; } std::string physical_path = ResolvePath(path); std::error_code ec; if (!fs::exists(physical_path, ec) || !fs::is_directory(physical_path, ec)) { error = "directory not found"; return std::nullopt; } std::vector entries; for (const auto& entry : fs::directory_iterator(physical_path, ec)) { entries.push_back(entry.path().filename().string()); } if (ec) { error = "failed to list directory"; return std::nullopt; } return entries; } bool VirtualFS::MakeDir(const std::string& path, std::string& error) { if (!ValidatePath(path, error)) { return false; } std::string physical_path = ResolvePath(path); std::error_code ec; if (!fs::create_directories(physical_path, ec) && ec) { error = "failed to create directory"; return false; } return true; } std::optional VirtualFS::Stat(const std::string& path, std::string& error) { if (!ValidatePath(path, error)) { return std::nullopt; } std::string physical_path = ResolvePath(path); std::error_code ec; if (!fs::exists(physical_path, ec)) { error = "file not found"; return std::nullopt; } FileStat stat; stat.is_dir = fs::is_directory(physical_path, ec); if (stat.is_dir) { stat.size = 0; } else { stat.size = static_cast(fs::file_size(physical_path, ec)); } auto ftime = fs::last_write_time(physical_path, ec); // Convert file_time_type to system_clock (portable workaround for clock_cast) auto file_time_ns = ftime.time_since_epoch(); auto sys_time_ns = std::chrono::duration_cast(file_time_ns); stat.modified = sys_time_ns.count(); return stat; } //============================================================================= // CLEANUP //============================================================================= void VirtualFS::DeleteDirectoryRecursive(const std::string& path) { std::error_code ec; fs::remove_all(path, ec); } size_t VirtualFS::CalculateDirectorySize(const std::string& path) { size_t total = 0; std::error_code ec; if (!fs::exists(path, ec)) { return 0; } for (const auto& entry : fs::recursive_directory_iterator(path, ec)) { if (fs::is_regular_file(entry, ec)) { total += static_cast(fs::file_size(entry, ec)); } } return total; } void VirtualFS::RecalculateUsage() { m_used_bytes = CalculateDirectorySize(m_app_root); } void VirtualFS::ClearTemp() { fs::path temp_path = fs::path(m_app_root) / "temp"; std::error_code ec; if (fs::exists(temp_path, ec)) { size_t temp_size = CalculateDirectorySize(temp_path.string()); DeleteDirectoryRecursive(temp_path.string()); UpdateUsage(-static_cast(temp_size)); } } void VirtualFS::ClearAll() { DeleteDirectoryRecursive(m_app_root); m_used_bytes = 0; } //============================================================================= // LUA API //============================================================================= static const char* VFS_KEY = "__mosis_vfs"; static VirtualFS* GetVFS(lua_State* L) { lua_getfield(L, LUA_REGISTRYINDEX, VFS_KEY); if (lua_islightuserdata(L, -1)) { VirtualFS* vfs = static_cast(lua_touserdata(L, -1)); lua_pop(L, 1); return vfs; } lua_pop(L, 1); return nullptr; } // fs.read(path) -> content|nil, error static int lua_fs_read(lua_State* L) { VirtualFS* vfs = GetVFS(L); if (!vfs) { lua_pushnil(L); lua_pushstring(L, "VirtualFS not initialized"); return 2; } const char* path = luaL_checkstring(L, 1); std::string error; auto content = vfs->Read(path, error); if (content) { lua_pushlstring(L, content->data(), content->size()); return 1; } else { lua_pushnil(L); lua_pushstring(L, error.c_str()); return 2; } } // fs.write(path, data) -> bool, error static int lua_fs_write(lua_State* L) { VirtualFS* vfs = GetVFS(L); if (!vfs) { lua_pushboolean(L, 0); lua_pushstring(L, "VirtualFS not initialized"); return 2; } const char* path = luaL_checkstring(L, 1); size_t len; const char* data = luaL_checklstring(L, 2, &len); std::string error; if (vfs->Write(path, std::string(data, len), error)) { lua_pushboolean(L, 1); return 1; } else { lua_pushboolean(L, 0); lua_pushstring(L, error.c_str()); return 2; } } // fs.append(path, data) -> bool, error static int lua_fs_append(lua_State* L) { VirtualFS* vfs = GetVFS(L); if (!vfs) { lua_pushboolean(L, 0); lua_pushstring(L, "VirtualFS not initialized"); return 2; } const char* path = luaL_checkstring(L, 1); size_t len; const char* data = luaL_checklstring(L, 2, &len); std::string error; if (vfs->Append(path, std::string(data, len), error)) { lua_pushboolean(L, 1); return 1; } else { lua_pushboolean(L, 0); lua_pushstring(L, error.c_str()); return 2; } } // fs.delete(path) -> bool, error static int lua_fs_delete(lua_State* L) { VirtualFS* vfs = GetVFS(L); if (!vfs) { lua_pushboolean(L, 0); lua_pushstring(L, "VirtualFS not initialized"); return 2; } const char* path = luaL_checkstring(L, 1); std::string error; if (vfs->Delete(path, error)) { lua_pushboolean(L, 1); return 1; } else { lua_pushboolean(L, 0); lua_pushstring(L, error.c_str()); return 2; } } // fs.exists(path) -> bool static int lua_fs_exists(lua_State* L) { VirtualFS* vfs = GetVFS(L); if (!vfs) { lua_pushboolean(L, 0); return 1; } const char* path = luaL_checkstring(L, 1); lua_pushboolean(L, vfs->Exists(path) ? 1 : 0); return 1; } // fs.list(path) -> array|nil, error static int lua_fs_list(lua_State* L) { VirtualFS* vfs = GetVFS(L); if (!vfs) { lua_pushnil(L); lua_pushstring(L, "VirtualFS not initialized"); return 2; } const char* path = luaL_checkstring(L, 1); std::string error; auto entries = vfs->List(path, error); if (entries) { lua_createtable(L, static_cast(entries->size()), 0); int i = 1; for (const auto& name : *entries) { lua_pushlstring(L, name.c_str(), name.size()); lua_rawseti(L, -2, i++); } return 1; } else { lua_pushnil(L); lua_pushstring(L, error.c_str()); return 2; } } // fs.mkdir(path) -> bool, error static int lua_fs_mkdir(lua_State* L) { VirtualFS* vfs = GetVFS(L); if (!vfs) { lua_pushboolean(L, 0); lua_pushstring(L, "VirtualFS not initialized"); return 2; } const char* path = luaL_checkstring(L, 1); std::string error; if (vfs->MakeDir(path, error)) { lua_pushboolean(L, 1); return 1; } else { lua_pushboolean(L, 0); lua_pushstring(L, error.c_str()); return 2; } } // fs.stat(path) -> {size, modified, isDir}|nil, error static int lua_fs_stat(lua_State* L) { VirtualFS* vfs = GetVFS(L); if (!vfs) { lua_pushnil(L); lua_pushstring(L, "VirtualFS not initialized"); return 2; } const char* path = luaL_checkstring(L, 1); std::string error; auto stat = vfs->Stat(path, error); if (stat) { lua_createtable(L, 0, 3); lua_pushinteger(L, static_cast(stat->size)); lua_setfield(L, -2, "size"); lua_pushinteger(L, static_cast(stat->modified)); lua_setfield(L, -2, "modified"); lua_pushboolean(L, stat->is_dir ? 1 : 0); lua_setfield(L, -2, "isDir"); return 1; } else { lua_pushnil(L); lua_pushstring(L, error.c_str()); return 2; } } // Helper to set a global in the real _G (bypassing any proxy) static void SetGlobalInRealG(lua_State* L, const char* name) { // Stack: value to set as global // Get _G (might be a proxy) lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS); // Check if it has a metatable with __index (proxy pattern) if (lua_getmetatable(L, -1)) { lua_getfield(L, -1, "__index"); if (lua_istable(L, -1)) { // Found real _G through proxy's __index // Stack: value, proxy, mt, real_G lua_pushvalue(L, -4); // Copy value lua_setfield(L, -2, name); // real_G[name] = value lua_pop(L, 4); // pop real_G, mt, proxy, original value return; } lua_pop(L, 2); // pop __index, metatable } // No proxy, set directly in _G // Stack: value, _G lua_pushvalue(L, -2); // Copy value lua_setfield(L, -2, name); // _G[name] = value lua_pop(L, 2); // pop _G, original value } void RegisterVirtualFS(lua_State* L, VirtualFS* vfs) { // Store VFS in registry lua_pushlightuserdata(L, vfs); lua_setfield(L, LUA_REGISTRYINDEX, VFS_KEY); // Create fs table lua_newtable(L); lua_pushcfunction(L, lua_fs_read); lua_setfield(L, -2, "read"); lua_pushcfunction(L, lua_fs_write); lua_setfield(L, -2, "write"); lua_pushcfunction(L, lua_fs_append); lua_setfield(L, -2, "append"); lua_pushcfunction(L, lua_fs_delete); lua_setfield(L, -2, "delete"); lua_pushcfunction(L, lua_fs_exists); lua_setfield(L, -2, "exists"); lua_pushcfunction(L, lua_fs_list); lua_setfield(L, -2, "list"); lua_pushcfunction(L, lua_fs_mkdir); lua_setfield(L, -2, "mkdir"); lua_pushcfunction(L, lua_fs_stat); lua_setfield(L, -2, "stat"); // Set as global (bypassing proxy) SetGlobalInRealG(L, "fs"); } } // namespace mosis