add virtual filesystem with path sandboxing and quotas (milestone 7 complete)
This commit is contained in:
@@ -299,8 +299,9 @@ TEST(CryptoApi, HashCorrect);
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Milestone 7: Virtual Filesystem
|
## Milestone 7: Virtual Filesystem ✅
|
||||||
|
|
||||||
|
**Status**: Complete
|
||||||
**Goal**: Per-app isolated storage with quotas.
|
**Goal**: Per-app isolated storage with quotas.
|
||||||
**Estimated Files**: 1 new file
|
**Estimated Files**: 1 new file
|
||||||
|
|
||||||
|
|||||||
440
SANDBOX_MILESTONE_7.md
Normal file
440
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
|
||||||
@@ -18,6 +18,7 @@ add_library(mosis-sandbox STATIC
|
|||||||
../src/main/cpp/sandbox/timer_manager.cpp
|
../src/main/cpp/sandbox/timer_manager.cpp
|
||||||
../src/main/cpp/sandbox/json_api.cpp
|
../src/main/cpp/sandbox/json_api.cpp
|
||||||
../src/main/cpp/sandbox/crypto_api.cpp
|
../src/main/cpp/sandbox/crypto_api.cpp
|
||||||
|
../src/main/cpp/sandbox/virtual_fs.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(mosis-sandbox PUBLIC
|
target_include_directories(mosis-sandbox PUBLIC
|
||||||
../src/main/cpp/sandbox
|
../src/main/cpp/sandbox
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "timer_manager.h"
|
#include "timer_manager.h"
|
||||||
#include "json_api.h"
|
#include "json_api.h"
|
||||||
#include "crypto_api.h"
|
#include "crypto_api.h"
|
||||||
|
#include "virtual_fs.h"
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
@@ -1074,6 +1075,190 @@ bool Test_SecureMathRandom(std::string& error_msg) {
|
|||||||
return true;
|
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
|
// MAIN
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
@@ -1176,6 +1361,16 @@ int main(int argc, char* argv[]) {
|
|||||||
harness.AddTest("CryptoHMAC", Test_CryptoHMAC);
|
harness.AddTest("CryptoHMAC", Test_CryptoHMAC);
|
||||||
harness.AddTest("SecureMathRandom", Test_SecureMathRandom);
|
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
|
// Run tests
|
||||||
auto results = harness.Run(filter);
|
auto results = harness.Run(filter);
|
||||||
|
|
||||||
|
|||||||
705
src/main/cpp/sandbox/virtual_fs.cpp
Normal file
705
src/main/cpp/sandbox/virtual_fs.cpp
Normal file
@@ -0,0 +1,705 @@
|
|||||||
|
#include "virtual_fs.h"
|
||||||
|
|
||||||
|
#include <lua.hpp>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
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<unsigned char>(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 -> <app_root>/data/foo.txt
|
||||||
|
// /cache/bar.txt -> <app_root>/cache/bar.txt
|
||||||
|
// /temp/baz.txt -> <app_root>/temp/baz.txt
|
||||||
|
// /shared/x.txt -> <app_root>/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<size_t>(-delta) > m_used_bytes) {
|
||||||
|
m_used_bytes = 0;
|
||||||
|
} else {
|
||||||
|
m_used_bytes = static_cast<size_t>(static_cast<int64_t>(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<std::string> 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<size_t>(fs::file_size(physical_path, ec));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check quota for net change
|
||||||
|
int64_t delta = static_cast<int64_t>(data.size()) - static_cast<int64_t>(old_size);
|
||||||
|
if (delta > 0 && !CheckQuota(static_cast<size_t>(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<size_t>(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<int64_t>(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<size_t>(fs::file_size(physical_path, ec));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs::remove(physical_path, ec)) {
|
||||||
|
error = "failed to delete";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateUsage(-static_cast<int64_t>(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<std::vector<std::string>> 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<std::string> 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<FileStat> 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<size_t>(fs::file_size(physical_path, ec));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ftime = fs::last_write_time(physical_path, ec);
|
||||||
|
auto sctp = std::chrono::time_point_cast<std::chrono::seconds>(
|
||||||
|
std::chrono::clock_cast<std::chrono::system_clock>(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<size_t>(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<int64_t>(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<VirtualFS*>(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<int>(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<lua_Integer>(stat->size));
|
||||||
|
lua_setfield(L, -2, "size");
|
||||||
|
|
||||||
|
lua_pushinteger(L, static_cast<lua_Integer>(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
|
||||||
77
src/main/cpp/sandbox/virtual_fs.h
Normal file
77
src/main/cpp/sandbox/virtual_fs.h
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
#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);
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user