# 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
