Scrypt / scrypt /sandbox /vfs.py
IMJONEZZ's picture
SCRYPT: initial commit — game, sandbox, Warden, Space web layer
9fca766
Raw
History Blame Contribute Delete
5.56 kB
"""The sandboxed virtual filesystem.
A pure in-memory tree. Nothing here ever touches the host filesystem —
that invariant is what makes the Warden's threats safe to carry out.
"""
from __future__ import annotations
from dataclasses import dataclass, field
class VfsError(Exception):
pass
@dataclass
class FileNode:
name: str
content: str = ""
# Locked files refuse to open until unlocked (puzzle artifacts).
locked: bool = False
password: str = ""
# Archives: contents revealed by `unzip` with the right password.
archive: dict[str, str] | None = None
# Set by `chmod +x` (the daemon puzzle checks this).
executable: bool = False
@dataclass
class DirNode:
name: str
children: dict[str, "FileNode | DirNode"] = field(default_factory=dict)
Node = FileNode | DirNode
class VFS:
def __init__(self) -> None:
self.root = DirNode(name="/")
self.cwd: list[str] = ["home", "drifter"]
self.mkdir("/home/drifter")
# ----------------------------------------------------------- resolving
def _parts(self, path: str) -> list[str]:
if path.startswith("~"):
path = "/home/drifter" + path[1:]
parts = list(self.cwd) if not path.startswith("/") else []
for piece in path.split("/"):
if piece in ("", "."):
continue
if piece == "..":
if parts:
parts.pop()
else:
parts.append(piece)
return parts
def _node_at(self, parts: list[str]) -> Node | None:
node: Node = self.root
for piece in parts:
if not isinstance(node, DirNode) or piece not in node.children:
return None
node = node.children[piece]
return node
def resolve(self, path: str) -> Node | None:
return self._node_at(self._parts(path))
def path_of(self, parts: list[str]) -> str:
return "/" + "/".join(parts)
@property
def cwd_path(self) -> str:
return self.path_of(self.cwd)
# ----------------------------------------------------------- mutation
def mkdir(self, path: str) -> DirNode:
parts = self._parts(path)
node: Node = self.root
for piece in parts:
assert isinstance(node, DirNode)
if piece not in node.children:
node.children[piece] = DirNode(name=piece)
node = node.children[piece]
if not isinstance(node, DirNode):
raise VfsError(f"{piece}: not a directory")
return node
def write(self, path: str, content: str, **kwargs) -> FileNode:
parts = self._parts(path)
if not parts:
raise VfsError("cannot write to /")
parent = self.mkdir(self.path_of(parts[:-1]))
f = FileNode(name=parts[-1], content=content, **kwargs)
parent.children[parts[-1]] = f
return f
def remove(self, path: str, recursive: bool = False) -> int:
"""Delete a node. Returns number of files removed (for the horror)."""
parts = self._parts(path)
if not parts:
raise VfsError("cannot remove /")
parent = self._node_at(parts[:-1])
if not isinstance(parent, DirNode) or parts[-1] not in parent.children:
raise VfsError(f"{path}: no such file or directory")
node = parent.children[parts[-1]]
if isinstance(node, DirNode):
if not recursive:
raise VfsError(f"{path}: is a directory")
count = sum(1 for _ in self.iter_files(node))
else:
count = 1
del parent.children[parts[-1]]
return count
def chdir(self, path: str) -> None:
parts = self._parts(path)
node = self._node_at(parts)
if node is None:
raise VfsError(f"{path}: no such file or directory")
if not isinstance(node, DirNode):
raise VfsError(f"{path}: not a directory")
self.cwd = parts
# ----------------------------------------------------------- reading
def read(self, path: str) -> str:
node = self.resolve(path)
if node is None:
raise VfsError(f"{path}: no such file or directory")
if isinstance(node, DirNode):
raise VfsError(f"{path}: is a directory")
if node.locked:
raise VfsError(f"{path}: permission denied")
return node.content
def listing(self, path: str = ".", show_hidden: bool = False) -> list[Node]:
node = self.resolve(path)
if node is None:
raise VfsError(f"{path}: no such file or directory")
if isinstance(node, FileNode):
return [node]
items = [
child
for name, child in sorted(node.children.items())
if show_hidden or not name.startswith(".")
]
return items
def iter_files(self, node: Node | None = None, prefix: str = ""):
"""Yield (path, FileNode) for every file under node (default: root)."""
node = node or self.root
if isinstance(node, FileNode):
yield prefix or node.name, node
return
for name, child in sorted(node.children.items()):
child_path = f"{prefix}/{name}"
if isinstance(child, FileNode):
yield child_path, child
else:
yield from self.iter_files(child, child_path)
def total_files(self) -> int:
return sum(1 for _ in self.iter_files())