Spaces:
Running on Zero
Running on Zero
| """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 | |
| 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 | |
| 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) | |
| 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()) | |