From 2bb083fd7df30d3c027af809b86d684a56bf7085 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Sun, 18 Jan 2026 15:04:46 +0100 Subject: [PATCH] add virtual filesystem with path sandboxing and quotas (milestone 7 complete) --- SANDBOX_MILESTONES.md | 3 +- SANDBOX_MILESTONE_7.md | 440 +++++++++++++++++ sandbox-test/CMakeLists.txt | 1 + sandbox-test/src/main.cpp | 195 ++++++++ src/main/cpp/sandbox/virtual_fs.cpp | 705 ++++++++++++++++++++++++++++ src/main/cpp/sandbox/virtual_fs.h | 77 +++ 6 files changed, 1420 insertions(+), 1 deletion(-) create mode 100644 SANDBOX_MILESTONE_7.md create mode 100644 src/main/cpp/sandbox/virtual_fs.cpp create mode 100644 src/main/cpp/sandbox/virtual_fs.h diff --git a/SANDBOX_MILESTONES.md b/SANDBOX_MILESTONES.md index 5f27448..448ad15 100644 --- a/SANDBOX_MILESTONES.md +++ b/SANDBOX_MILESTONES.md @@ -299,8 +299,9 @@ TEST(CryptoApi, HashCorrect); --- -## Milestone 7: Virtual Filesystem +## Milestone 7: Virtual Filesystem ✅ +**Status**: Complete **Goal**: Per-app isolated storage with quotas. **Estimated Files**: 1 new file diff --git a/SANDBOX_MILESTONE_7.md b/SANDBOX_MILESTONE_7.md new file mode 100644 index 0000000..c76ed1a --- /dev/null +++ b/SANDBOX_MILESTONE_7.md @@ -0,0 +1,440 @@ +# Milestone 7: Virtual Filesystem + +**Status**: ✅ Complete +**Goal**: Per-app isolated storage with quotas. + +--- + +## Overview + +This milestone implements a sandboxed virtual filesystem for Lua apps: +- Per-app isolated storage directories +- Path validation to prevent escapes +- Quota enforcement to limit disk usage +- Session-only temp storage that cleans up automatically + +### Key Deliverables + +1. **VirtualFS class** - Path mapping and validation +2. **Lua fs API** - read, write, append, delete, exists, list, mkdir, stat +3. **Quota tracking** - Per-app disk usage limits +4. **Temp cleanup** - Clear temp files on app stop + +--- + +## File Structure + +``` +src/main/cpp/sandbox/ +├── virtual_fs.h # NEW - VirtualFS header +└── virtual_fs.cpp # NEW - VirtualFS implementation +``` + +--- + +## Implementation Details + +### 1. Virtual Path Mapping + +``` +Virtual Path → Physical Path +───────────────────────────────────────────────────────────── +/data/ → /data/ (persistent app data) +/cache/ → /cache/ (clearable cache) +/temp/ → /temp/ (session-only, auto-cleared) +/shared/ → / (requires storage.shared permission) +``` + +For testing, `app_root` is configurable (e.g., `./test_apps//`). + +### 2. VirtualFS Class + +```cpp +// virtual_fs.h +#pragma once + +#include +#include +#include +#include +#include + +struct lua_State; + +namespace mosis { + +struct FileStat { + size_t size; + int64_t modified; // Unix timestamp + bool is_dir; +}; + +struct VirtualFSLimits { + size_t max_quota_bytes = 50 * 1024 * 1024; // 50 MB per app + size_t max_file_size = 10 * 1024 * 1024; // 10 MB per file + int max_path_depth = 10; // Max directory depth + size_t max_path_length = 256; // Max path string length +}; + +class VirtualFS { +public: + VirtualFS(const std::string& app_id, + const std::string& app_root, + const VirtualFSLimits& limits = VirtualFSLimits{}); + ~VirtualFS(); + + // Path operations + bool ValidatePath(const std::string& virtual_path, std::string& error); + std::string ResolvePath(const std::string& virtual_path); + + // File operations + std::optional Read(const std::string& path, std::string& error); + bool Write(const std::string& path, const std::string& data, std::string& error); + bool Append(const std::string& path, const std::string& data, std::string& error); + bool Delete(const std::string& path, std::string& error); + bool Exists(const std::string& path); + std::optional> List(const std::string& path, std::string& error); + bool MakeDir(const std::string& path, std::string& error); + std::optional Stat(const std::string& path, std::string& error); + + // Quota management + size_t GetUsedBytes() const { return m_used_bytes; } + size_t GetQuotaBytes() const { return m_limits.max_quota_bytes; } + void RecalculateUsage(); + + // Cleanup + void ClearTemp(); + void ClearAll(); // For testing + + // Permission check callback (set by sandbox) + std::function CheckPermission; + +private: + std::string m_app_id; + std::string m_app_root; + VirtualFSLimits m_limits; + size_t m_used_bytes = 0; + + bool EnsureParentDir(const std::string& path); + void UpdateUsage(int64_t delta); + bool CheckQuota(size_t additional_bytes, std::string& error); + int GetPathDepth(const std::string& path); +}; + +// Register fs.* APIs as globals +void RegisterVirtualFS(lua_State* L, VirtualFS* vfs); + +} // namespace mosis +``` + +### 3. Path Validation Rules + +1. **Must start with virtual root**: `/data/`, `/cache/`, `/temp/`, or `/shared/` +2. **No path traversal**: Reject `..` components +3. **No absolute escapes**: After prefix, must stay within sandbox +4. **Max depth**: Limit directory nesting (default 10) +5. **Max length**: Limit path string length (default 256 chars) +6. **Valid characters**: Alphanumeric, `-`, `_`, `.`, `/` + +### 4. Lua API + +```lua +-- Read file contents +local content, err = fs.read("/data/config.json") +if not content then + print("Error: " .. err) +end + +-- Write file (creates parent dirs) +local ok, err = fs.write("/data/config.json", '{"key": "value"}') + +-- Append to file +fs.append("/data/log.txt", "New line\n") + +-- Delete file or empty directory +fs.delete("/data/old_file.txt") + +-- Check existence +if fs.exists("/data/config.json") then + -- file exists +end + +-- List directory contents +local files, err = fs.list("/data/") +for _, name in ipairs(files) do + print(name) +end + +-- Create directory +fs.mkdir("/data/subdir") + +-- Get file info +local stat, err = fs.stat("/data/config.json") +if stat then + print("Size: " .. stat.size) + print("Modified: " .. stat.modified) + print("Is dir: " .. tostring(stat.isDir)) +end +``` + +### 5. Quota Enforcement + +- Track total bytes used per app +- Check before each write/append +- Return error if quota would be exceeded +- Recalculate on startup (in case of crashes) + +### 6. Permission Requirements + +| Operation | Permission Required | +|-----------|---------------------| +| `/data/*` | None (auto-granted) | +| `/cache/*` | None (auto-granted) | +| `/temp/*` | None (auto-granted) | +| `/shared/*` | `storage.shared` | + +--- + +## Test Cases + +### Test 1: Read/Write in App Dir + +```cpp +bool Test_VirtualFSReadWrite(std::string& error_msg) { + std::string test_root = "test_vfs_app"; + mosis::VirtualFS vfs("test.app", test_root); + + std::string err; + + // Write a file + EXPECT_TRUE(vfs.Write("/data/test.txt", "Hello World", err)); + + // Read it back + auto content = vfs.Read("/data/test.txt", err); + EXPECT_TRUE(content.has_value()); + EXPECT_TRUE(*content == "Hello World"); + + // Cleanup + vfs.ClearAll(); + return true; +} +``` + +### Test 2: Blocks Path Traversal + +```cpp +bool Test_VirtualFSBlocksTraversal(std::string& error_msg) { + std::string test_root = "test_vfs_app"; + mosis::VirtualFS vfs("test.app", test_root); + + std::string err; + + // Should reject traversal + EXPECT_FALSE(vfs.Write("/data/../../../etc/passwd", "hack", err)); + EXPECT_TRUE(err.find("traversal") != std::string::npos || + err.find("invalid") != std::string::npos); + + // Should reject absolute paths + EXPECT_FALSE(vfs.Write("/etc/passwd", "hack", err)); + + vfs.ClearAll(); + return true; +} +``` + +### Test 3: Enforces Quota + +```cpp +bool Test_VirtualFSEnforcesQuota(std::string& error_msg) { + std::string test_root = "test_vfs_app"; + mosis::VirtualFSLimits limits; + limits.max_quota_bytes = 1024; // 1 KB quota for testing + mosis::VirtualFS vfs("test.app", test_root, limits); + + std::string err; + + // Write should succeed + std::string small_data(500, 'a'); + EXPECT_TRUE(vfs.Write("/data/file1.txt", small_data, err)); + + // Second write should fail (exceeds quota) + std::string large_data(600, 'b'); + EXPECT_FALSE(vfs.Write("/data/file2.txt", large_data, err)); + EXPECT_TRUE(err.find("quota") != std::string::npos); + + vfs.ClearAll(); + return true; +} +``` + +### Test 4: Cleans Up Temp + +```cpp +bool Test_VirtualFSCleansUpTemp(std::string& error_msg) { + std::string test_root = "test_vfs_app"; + mosis::VirtualFS vfs("test.app", test_root); + + std::string err; + + // Write to temp + EXPECT_TRUE(vfs.Write("/temp/session.txt", "temp data", err)); + EXPECT_TRUE(vfs.Exists("/temp/session.txt")); + + // Clear temp + vfs.ClearTemp(); + + // Should be gone + EXPECT_FALSE(vfs.Exists("/temp/session.txt")); + + vfs.ClearAll(); + return true; +} +``` + +### Test 5: List Directory + +```cpp +bool Test_VirtualFSList(std::string& error_msg) { + std::string test_root = "test_vfs_app"; + mosis::VirtualFS vfs("test.app", test_root); + + std::string err; + + // Create some files + vfs.Write("/data/file1.txt", "content1", err); + vfs.Write("/data/file2.txt", "content2", err); + vfs.MakeDir("/data/subdir", err); + + // List directory + auto files = vfs.List("/data/", err); + EXPECT_TRUE(files.has_value()); + EXPECT_TRUE(files->size() == 3); + + vfs.ClearAll(); + return true; +} +``` + +### Test 6: File Stat + +```cpp +bool Test_VirtualFSStat(std::string& error_msg) { + std::string test_root = "test_vfs_app"; + mosis::VirtualFS vfs("test.app", test_root); + + std::string err; + + // Write a file + vfs.Write("/data/test.txt", "Hello", err); + + // Get stat + auto stat = vfs.Stat("/data/test.txt", err); + EXPECT_TRUE(stat.has_value()); + EXPECT_TRUE(stat->size == 5); + EXPECT_FALSE(stat->is_dir); + + // Directory stat + vfs.MakeDir("/data/subdir", err); + auto dir_stat = vfs.Stat("/data/subdir", err); + EXPECT_TRUE(dir_stat.has_value()); + EXPECT_TRUE(dir_stat->is_dir); + + vfs.ClearAll(); + return true; +} +``` + +### Test 7: Lua Integration + +```cpp +bool Test_VirtualFSLuaIntegration(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + + std::string test_root = "test_vfs_lua"; + mosis::VirtualFS vfs("test.app", test_root); + mosis::RegisterVirtualFS(sandbox.GetState(), &vfs); + + std::string script = R"( + -- Write and read + local ok, err = fs.write("/data/test.txt", "Hello from Lua") + assert(ok, "write failed: " .. (err or "")) + + local content, err = fs.read("/data/test.txt") + assert(content == "Hello from Lua", "content mismatch") + + -- Check exists + assert(fs.exists("/data/test.txt"), "file should exist") + assert(not fs.exists("/data/nonexistent.txt"), "file should not exist") + + -- Stat + local stat = fs.stat("/data/test.txt") + assert(stat.size == 14, "size should be 14") + assert(not stat.isDir, "should not be dir") + )"; + + bool ok = sandbox.LoadString(script, "vfs_test"); + vfs.ClearAll(); + + if (!ok) { + error_msg = "Lua test failed: " + sandbox.GetLastError(); + return false; + } + return true; +} +``` + +### Test 8: Max File Size + +```cpp +bool Test_VirtualFSMaxFileSize(std::string& error_msg) { + std::string test_root = "test_vfs_app"; + mosis::VirtualFSLimits limits; + limits.max_file_size = 100; // 100 bytes max + limits.max_quota_bytes = 10000; // Large quota + mosis::VirtualFS vfs("test.app", test_root, limits); + + std::string err; + + // Small file should succeed + EXPECT_TRUE(vfs.Write("/data/small.txt", std::string(50, 'a'), err)); + + // Large file should fail + EXPECT_FALSE(vfs.Write("/data/large.txt", std::string(200, 'b'), err)); + EXPECT_TRUE(err.find("size") != std::string::npos); + + vfs.ClearAll(); + return true; +} +``` + +--- + +## Acceptance Criteria + +All tests must pass: + +- [x] `Test_VirtualFSReadWrite` - Basic read/write operations +- [x] `Test_VirtualFSBlocksTraversal` - Path traversal prevention +- [x] `Test_VirtualFSEnforcesQuota` - Quota enforcement +- [x] `Test_VirtualFSCleansUpTemp` - Temp directory cleanup +- [x] `Test_VirtualFSList` - Directory listing +- [x] `Test_VirtualFSStat` - File stat information +- [x] `Test_VirtualFSLuaIntegration` - Lua API integration +- [x] `Test_VirtualFSMaxFileSize` - File size limit enforcement + +--- + +## Dependencies + +- Milestone 1 (LuaSandbox) +- Milestone 4 (Path validation - reuse ValidatePath logic) +- C++ filesystem library (``) + +--- + +## Next Steps + +After Milestone 7 passes: +1. Milestone 8: SQLite Database +2. Milestone 9: Network - HTTP diff --git a/sandbox-test/CMakeLists.txt b/sandbox-test/CMakeLists.txt index 3e564be..4473c7d 100644 --- a/sandbox-test/CMakeLists.txt +++ b/sandbox-test/CMakeLists.txt @@ -18,6 +18,7 @@ add_library(mosis-sandbox STATIC ../src/main/cpp/sandbox/timer_manager.cpp ../src/main/cpp/sandbox/json_api.cpp ../src/main/cpp/sandbox/crypto_api.cpp + ../src/main/cpp/sandbox/virtual_fs.cpp ) target_include_directories(mosis-sandbox PUBLIC ../src/main/cpp/sandbox diff --git a/sandbox-test/src/main.cpp b/sandbox-test/src/main.cpp index 9c83357..34fccfd 100644 --- a/sandbox-test/src/main.cpp +++ b/sandbox-test/src/main.cpp @@ -9,6 +9,7 @@ #include "timer_manager.h" #include "json_api.h" #include "crypto_api.h" +#include "virtual_fs.h" #include #include #include @@ -1074,6 +1075,190 @@ bool Test_SecureMathRandom(std::string& error_msg) { return true; } +//============================================================================= +// MILESTONE 7: VIRTUAL FILESYSTEM TESTS +//============================================================================= + +bool Test_VirtualFSReadWrite(std::string& error_msg) { + std::string test_root = "test_vfs_app"; + mosis::VirtualFS vfs("test.app", test_root); + + std::string err; + + // Write a file + EXPECT_TRUE(vfs.Write("/data/test.txt", "Hello World", err)); + + // Read it back + auto content = vfs.Read("/data/test.txt", err); + EXPECT_TRUE(content.has_value()); + EXPECT_TRUE(*content == "Hello World"); + + // Cleanup + vfs.ClearAll(); + return true; +} + +bool Test_VirtualFSBlocksTraversal(std::string& error_msg) { + std::string test_root = "test_vfs_app"; + mosis::VirtualFS vfs("test.app", test_root); + + std::string err; + + // Should reject traversal + EXPECT_FALSE(vfs.Write("/data/../../../etc/passwd", "hack", err)); + EXPECT_TRUE(err.find("traversal") != std::string::npos || + err.find("invalid") != std::string::npos); + + // Should reject paths without valid prefix + err.clear(); + EXPECT_FALSE(vfs.Write("/etc/passwd", "hack", err)); + + vfs.ClearAll(); + return true; +} + +bool Test_VirtualFSEnforcesQuota(std::string& error_msg) { + std::string test_root = "test_vfs_app"; + mosis::VirtualFSLimits limits; + limits.max_quota_bytes = 1024; // 1 KB quota for testing + mosis::VirtualFS vfs("test.app", test_root, limits); + + std::string err; + + // Write should succeed + std::string small_data(500, 'a'); + EXPECT_TRUE(vfs.Write("/data/file1.txt", small_data, err)); + + // Second write should fail (exceeds quota) + std::string large_data(600, 'b'); + EXPECT_FALSE(vfs.Write("/data/file2.txt", large_data, err)); + EXPECT_TRUE(err.find("quota") != std::string::npos); + + vfs.ClearAll(); + return true; +} + +bool Test_VirtualFSCleansUpTemp(std::string& error_msg) { + std::string test_root = "test_vfs_app"; + mosis::VirtualFS vfs("test.app", test_root); + + std::string err; + + // Write to temp + EXPECT_TRUE(vfs.Write("/temp/session.txt", "temp data", err)); + EXPECT_TRUE(vfs.Exists("/temp/session.txt")); + + // Clear temp + vfs.ClearTemp(); + + // Should be gone + EXPECT_FALSE(vfs.Exists("/temp/session.txt")); + + vfs.ClearAll(); + return true; +} + +bool Test_VirtualFSList(std::string& error_msg) { + std::string test_root = "test_vfs_app"; + mosis::VirtualFS vfs("test.app", test_root); + + std::string err; + + // Create some files + vfs.Write("/data/file1.txt", "content1", err); + vfs.Write("/data/file2.txt", "content2", err); + vfs.MakeDir("/data/subdir", err); + + // List directory + auto files = vfs.List("/data/", err); + EXPECT_TRUE(files.has_value()); + EXPECT_TRUE(files->size() == 3); + + vfs.ClearAll(); + return true; +} + +bool Test_VirtualFSStat(std::string& error_msg) { + std::string test_root = "test_vfs_app"; + mosis::VirtualFS vfs("test.app", test_root); + + std::string err; + + // Write a file + vfs.Write("/data/test.txt", "Hello", err); + + // Get stat + auto stat = vfs.Stat("/data/test.txt", err); + EXPECT_TRUE(stat.has_value()); + EXPECT_TRUE(stat->size == 5); + EXPECT_FALSE(stat->is_dir); + + // Directory stat + vfs.MakeDir("/data/subdir", err); + auto dir_stat = vfs.Stat("/data/subdir", err); + EXPECT_TRUE(dir_stat.has_value()); + EXPECT_TRUE(dir_stat->is_dir); + + vfs.ClearAll(); + return true; +} + +bool Test_VirtualFSLuaIntegration(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + + std::string test_root = "test_vfs_lua"; + mosis::VirtualFS vfs("test.app", test_root); + mosis::RegisterVirtualFS(sandbox.GetState(), &vfs); + + std::string script = R"( + -- Write and read + local ok, err = fs.write("/data/test.txt", "Hello from Lua") + assert(ok, "write failed: " .. (err or "")) + + local content, err = fs.read("/data/test.txt") + assert(content == "Hello from Lua", "content mismatch: " .. (content or "nil")) + + -- Check exists + assert(fs.exists("/data/test.txt"), "file should exist") + assert(not fs.exists("/data/nonexistent.txt"), "file should not exist") + + -- Stat + local stat = fs.stat("/data/test.txt") + assert(stat.size == 14, "size should be 14, got " .. tostring(stat.size)) + assert(not stat.isDir, "should not be dir") + )"; + + bool ok = sandbox.LoadString(script, "vfs_test"); + vfs.ClearAll(); + + if (!ok) { + error_msg = "Lua test failed: " + sandbox.GetLastError(); + return false; + } + return true; +} + +bool Test_VirtualFSMaxFileSize(std::string& error_msg) { + std::string test_root = "test_vfs_app"; + mosis::VirtualFSLimits limits; + limits.max_file_size = 100; // 100 bytes max + limits.max_quota_bytes = 10000; // Large quota + mosis::VirtualFS vfs("test.app", test_root, limits); + + std::string err; + + // Small file should succeed + EXPECT_TRUE(vfs.Write("/data/small.txt", std::string(50, 'a'), err)); + + // Large file should fail + EXPECT_FALSE(vfs.Write("/data/large.txt", std::string(200, 'b'), err)); + EXPECT_TRUE(err.find("size") != std::string::npos); + + vfs.ClearAll(); + return true; +} + //============================================================================= // MAIN //============================================================================= @@ -1176,6 +1361,16 @@ int main(int argc, char* argv[]) { harness.AddTest("CryptoHMAC", Test_CryptoHMAC); harness.AddTest("SecureMathRandom", Test_SecureMathRandom); + // Milestone 7: Virtual Filesystem + harness.AddTest("VirtualFSReadWrite", Test_VirtualFSReadWrite); + harness.AddTest("VirtualFSBlocksTraversal", Test_VirtualFSBlocksTraversal); + harness.AddTest("VirtualFSEnforcesQuota", Test_VirtualFSEnforcesQuota); + harness.AddTest("VirtualFSCleansUpTemp", Test_VirtualFSCleansUpTemp); + harness.AddTest("VirtualFSList", Test_VirtualFSList); + harness.AddTest("VirtualFSStat", Test_VirtualFSStat); + harness.AddTest("VirtualFSLuaIntegration", Test_VirtualFSLuaIntegration); + harness.AddTest("VirtualFSMaxFileSize", Test_VirtualFSMaxFileSize); + // Run tests auto results = harness.Run(filter); diff --git a/src/main/cpp/sandbox/virtual_fs.cpp b/src/main/cpp/sandbox/virtual_fs.cpp new file mode 100644 index 0000000..4cea0df --- /dev/null +++ b/src/main/cpp/sandbox/virtual_fs.cpp @@ -0,0 +1,705 @@ +#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); + auto sctp = std::chrono::time_point_cast( + std::chrono::clock_cast(ftime)); + stat.modified = sctp.time_since_epoch().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 diff --git a/src/main/cpp/sandbox/virtual_fs.h b/src/main/cpp/sandbox/virtual_fs.h new file mode 100644 index 0000000..21e1044 --- /dev/null +++ b/src/main/cpp/sandbox/virtual_fs.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include +#include +#include + +struct lua_State; + +namespace mosis { + +struct FileStat { + size_t size; + int64_t modified; // Unix timestamp + bool is_dir; +}; + +struct VirtualFSLimits { + size_t max_quota_bytes = 50 * 1024 * 1024; // 50 MB per app + size_t max_file_size = 10 * 1024 * 1024; // 10 MB per file + int max_path_depth = 10; // Max directory depth + size_t max_path_length = 256; // Max path string length +}; + +class VirtualFS { +public: + VirtualFS(const std::string& app_id, + const std::string& app_root, + const VirtualFSLimits& limits = VirtualFSLimits{}); + ~VirtualFS(); + + // Path operations + bool ValidatePath(const std::string& virtual_path, std::string& error); + std::string ResolvePath(const std::string& virtual_path); + + // File operations + std::optional Read(const std::string& path, std::string& error); + bool Write(const std::string& path, const std::string& data, std::string& error); + bool Append(const std::string& path, const std::string& data, std::string& error); + bool Delete(const std::string& path, std::string& error); + bool Exists(const std::string& path); + std::optional> List(const std::string& path, std::string& error); + bool MakeDir(const std::string& path, std::string& error); + std::optional Stat(const std::string& path, std::string& error); + + // Quota management + size_t GetUsedBytes() const { return m_used_bytes; } + size_t GetQuotaBytes() const { return m_limits.max_quota_bytes; } + void RecalculateUsage(); + + // Cleanup + void ClearTemp(); + void ClearAll(); // For testing + + // Permission check callback (set by sandbox) + std::function CheckPermission; + +private: + std::string m_app_id; + std::string m_app_root; + VirtualFSLimits m_limits; + size_t m_used_bytes = 0; + + bool EnsureParentDir(const std::string& path); + void UpdateUsage(int64_t delta); + bool CheckQuota(size_t additional_bytes, std::string& error); + int GetPathDepth(const std::string& path); + bool IsValidPathChar(char c); + void DeleteDirectoryRecursive(const std::string& path); + size_t CalculateDirectorySize(const std::string& path); +}; + +// Register fs.* APIs as globals +void RegisterVirtualFS(lua_State* L, VirtualFS* vfs); + +} // namespace mosis