# 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