11 KiB
11 KiB
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
- VirtualFS class - Path mapping and validation
- Lua fs API - read, write, append, delete, exists, list, mkdir, stat
- Quota tracking - Per-app disk usage limits
- 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/ → <app_root>/data/ (persistent app data)
/cache/ → <app_root>/cache/ (clearable cache)
/temp/ → <app_root>/temp/ (session-only, auto-cleared)
/shared/ → <shared_root>/ (requires storage.shared permission)
For testing, app_root is configurable (e.g., ./test_apps/<app_id>/).
2. VirtualFS Class
// virtual_fs.h
#pragma once
#include <string>
#include <vector>
#include <cstdint>
#include <optional>
#include <functional>
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<std::string> 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<std::vector<std::string>> List(const std::string& path, std::string& error);
bool MakeDir(const std::string& path, std::string& error);
std::optional<FileStat> 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<bool(const std::string&)> 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
- Must start with virtual root:
/data/,/cache/,/temp/, or/shared/ - No path traversal: Reject
..components - No absolute escapes: After prefix, must stay within sandbox
- Max depth: Limit directory nesting (default 10)
- Max length: Limit path string length (default 256 chars)
- Valid characters: Alphanumeric,
-,_,.,/
4. Lua API
-- 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
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
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
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
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
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
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
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
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:
Test_VirtualFSReadWrite- Basic read/write operationsTest_VirtualFSBlocksTraversal- Path traversal preventionTest_VirtualFSEnforcesQuota- Quota enforcementTest_VirtualFSCleansUpTemp- Temp directory cleanupTest_VirtualFSList- Directory listingTest_VirtualFSStat- File stat informationTest_VirtualFSLuaIntegration- Lua API integrationTest_VirtualFSMaxFileSize- File size limit enforcement
Dependencies
- Milestone 1 (LuaSandbox)
- Milestone 4 (Path validation - reuse ValidatePath logic)
- C++ filesystem library (
<filesystem>)
Next Steps
After Milestone 7 passes:
- Milestone 8: SQLite Database
- Milestone 9: Network - HTTP