ADR-003: In-Memory Filesystem
Status
Accepted
Context
We need a filesystem for the OS. Browser storage options:
- localStorage: Simple key-value, 5-10MB limit, synchronous
- IndexedDB: Async, larger storage, complex API
- Origin Private File System (OPFS): Filesystem-like, fast, large
- In-memory: Fast, simple, no persistence
Requirements: - Unix-like paths and operations - Reasonable performance - Ideally some persistence - Ability to add virtual filesystems (/proc, /dev, /sys)
Decision
We will use an in-memory filesystem as the primary implementation, with optional OPFS persistence for saving/restoring state.
// From src/vfs/memory.rs
pub struct MemoryFs {
nodes: HashMap<String, Node>,
meta: HashMap<String, NodeMeta>,
handles: Slab<OpenFile>,
}
enum Node {
File(Vec<u8>),
Directory,
Symlink(String),
}
struct NodeMeta {
uid: u32,
gid: u32,
mode: u16,
}
Consequences
Positive
- Simple implementation: HashMap-based tree is easy to understand
- Fast: All operations are memory operations
- Flexible: Easy to add virtual filesystems
- No async complexity: Synchronous operations simplify code
- Isolation: Each session starts fresh (feature, not bug)
- Testable: Easy to set up test fixtures
Negative
- No persistence by default: Data lost on page refresh
- Memory limited: Large files consume browser memory
- Not a real filesystem: No journaling, no block devices
Mitigated
- Persistence: OPFS snapshot/restore for persistence
- Memory: Practical limit is browser's memory (plenty for demos)
Alternatives Considered
1. IndexedDB-backed
- Pro: Persistence, large storage
- Con: Async-only API, complex to wrap in sync interface, slow for small operations
2. OPFS as primary
- Pro: Fast, filesystem-like API, persistent
- Con: Still async, browser support varies, more complex
3. localStorage-backed
- Pro: Simple, synchronous
- Con: 5-10MB limit, string-only values
4. Emscripten FS
- Pro: Mature, many backends
- Con: C-based, doesn't fit Rust idioms
Implementation Details
Virtual Filesystems
The FileSystem trait allows plugging in special filesystems:
pub trait FileSystem {
fn open(&mut self, path: &str, options: OpenOptions) -> io::Result<FileHandle>;
fn read(&mut self, handle: FileHandle, buf: &mut [u8]) -> io::Result<usize>;
fn write(&mut self, handle: FileHandle, data: &[u8]) -> io::Result<usize>;
// ...
}
// Mount points
/proc → ProcFs (process info)
/dev → DevFs (devices: null, zero, random, tty)
/sys → SysFs (kernel info)
Persistence Strategy
Snapshot/restore via serialization (from src/vfs/memory.rs):
#[derive(Serialize, Deserialize)]
pub struct FsSnapshot {
nodes: HashMap<String, Node>,
meta: HashMap<String, NodeMeta>,
version: u32,
}
impl MemoryFs {
pub fn snapshot(&self) -> FsSnapshot { ... }
pub fn restore(snapshot: FsSnapshot) -> Self { ... }
}
Persistence to OPFS is handled by src/vfs/persist.rs.
Lessons Learned
- Start with in-memory, add persistence later
- The FileSystem trait abstraction was worth it for virtual filesystems
- JSON serialization is good enough for snapshots
- Symlinks were tricky to get right (resolution loops, relative paths)