move docs to docs/ folder, merge architecture files, update references
This commit is contained in:
440
docs/SANDBOX_MILESTONE_7.md
Normal file
440
docs/SANDBOX_MILESTONE_7.md
Normal file
@@ -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/ → <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
|
||||
|
||||
```cpp
|
||||
// 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
|
||||
|
||||
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 (`<filesystem>`)
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
After Milestone 7 passes:
|
||||
1. Milestone 8: SQLite Database
|
||||
2. Milestone 9: Network - HTTP
|
||||
Reference in New Issue
Block a user