Spaces:
Sleeping
Sleeping
| """Built-in Unix tools reimplemented in Python so they work on Windows. | |
| These are what the OS simulator uses for cat / ls / sha256sum / base64 / | |
| xxd / tr / … . External tools (python, openssl, …) are executed via | |
| subprocess from :mod:`app.sandbox.terminal`. | |
| """ | |
| import base64 | |
| import binascii | |
| import hashlib | |
| import os | |
| from typing import Optional | |
| from app.core.constants import HELP_TEXT | |
| def _safe_path(workdir: str, filename: str) -> Optional[str]: | |
| """Resolve filename safely within workdir, preventing path traversal. | |
| Returns the resolved path if safe, None if traversal detected. | |
| """ | |
| # Strip leading slashes and replace .. | |
| cleaned = filename.lstrip("/\\").replace("..", "_") | |
| target = os.path.normpath(os.path.join(workdir, cleaned)) | |
| # Verify the resolved path is within workdir | |
| real_workdir = os.path.realpath(workdir) | |
| real_target = os.path.realpath(target) | |
| if not real_target.startswith(real_workdir + os.sep) and real_target != real_workdir: | |
| return None | |
| return target | |
| def shell_builtins(args: list[str], workdir: str) -> Optional[dict]: | |
| """Run a built-in command. Returns a ``{stdout, stderr, exitCode}`` | |
| result dict, or ``None`` if the tool is not a built-in (caller should | |
| fall through to subprocess).""" | |
| tool = args[0] if args else "" | |
| rest = args[1:] | |
| try: | |
| if tool == "cat": | |
| if not rest: | |
| return {"stdout": "", "stderr": "cat: missing filename", "exitCode": 1} | |
| out, err = [], "" | |
| for fname in rest: | |
| p = _safe_path(workdir, fname) | |
| if p is None: | |
| err += f"cat: {fname}: Path traversal blocked\n" | |
| continue | |
| if not os.path.isfile(p): | |
| err += f"cat: {fname}: No such file\n" | |
| continue | |
| with open(p, "r", encoding="utf-8", errors="replace") as f: | |
| out.append(f.read()) | |
| return {"stdout": "\n".join(out), "stderr": err, "exitCode": 0 if not err else 1} | |
| if tool == "ls": | |
| entries = sorted(os.listdir(workdir)) | |
| lines = [] | |
| for n in entries: | |
| if n.startswith("."): | |
| continue | |
| p = os.path.join(workdir, n) | |
| if os.path.isdir(p): | |
| lines.append(f"<DIR> {n}") | |
| else: | |
| lines.append(f" {n}") | |
| return {"stdout": "\n".join(lines), "stderr": "", "exitCode": 0} | |
| if tool == "pwd": | |
| return {"stdout": workdir, "stderr": "", "exitCode": 0} | |
| if tool == "echo": | |
| return {"stdout": " ".join(rest), "stderr": "", "exitCode": 0} | |
| if tool == "whoami": | |
| return {"stdout": os.environ.get("USERNAME") or os.environ.get("USER") or "student", "stderr": "", "exitCode": 0} | |
| if tool == "clear": | |
| return {"stdout": "\x1b[2J\x1b[H", "stderr": "", "exitCode": 0, "clear": True} | |
| if tool == "help": | |
| return {"stdout": HELP_TEXT, "stderr": "", "exitCode": 0} | |
| if tool in ("sha256sum", "md5sum", "sha1sum"): | |
| algo = {"sha256sum": "sha256", "md5sum": "md5", "sha1sum": "sha1"}[tool] | |
| if not rest: | |
| return {"stdout": "", "stderr": f"{tool}: missing filename", "exitCode": 1} | |
| out, err = [], "" | |
| for fname in rest: | |
| p = _safe_path(workdir, fname) | |
| if p is None: | |
| err += f"{tool}: {fname}: Path traversal blocked\n" | |
| continue | |
| if not os.path.isfile(p): | |
| err += f"{tool}: {fname}: No such file\n" | |
| continue | |
| with open(p, "rb") as f: | |
| h = hashlib.new(algo, f.read()).hexdigest() | |
| out.append(f"{h} {fname}") | |
| return {"stdout": "\n".join(out), "stderr": err, "exitCode": 0 if not err else 1} | |
| if tool == "base64": | |
| decode = "-d" in rest | |
| args2 = [a for a in rest if a != "-d"] | |
| if not args2: | |
| import sys | |
| data = sys.stdin.read() if not sys.stdin.isatty() else b"" | |
| if decode: | |
| return {"stdout": base64.b64decode(data).decode("utf-8", "replace"), "stderr": "", "exitCode": 0} | |
| return {"stdout": base64.b64encode(data).decode(), "stderr": "", "exitCode": 0} | |
| out, err = [], "" | |
| for fname in args2: | |
| p = os.path.join(workdir, fname.lstrip("/\\").replace("..", "_")) | |
| if not os.path.isfile(p): | |
| err += f"base64: {fname}: No such file\n" | |
| continue | |
| with open(p, "rb") as f: | |
| data = f.read() | |
| if decode: | |
| out.append(base64.b64decode(data).decode("utf-8", "replace")) | |
| else: | |
| out.append(base64.b64encode(data).decode()) | |
| return {"stdout": "\n".join(out), "stderr": err, "exitCode": 0 if not err else 1} | |
| if tool == "xxd": | |
| if not rest: | |
| return {"stdout": "", "stderr": "xxd: missing filename", "exitCode": 1} | |
| p = os.path.join(workdir, rest[0].lstrip("/\\").replace("..", "_")) | |
| if not os.path.isfile(p): | |
| return {"stdout": "", "stderr": f"xxd: {rest[0]}: No such file", "exitCode": 1} | |
| with open(p, "rb") as f: | |
| data = f.read() | |
| return {"stdout": binascii.hexlify(data).decode(), "stderr": "", "exitCode": 0} | |
| if tool == "tr": | |
| if len(rest) < 2: | |
| return {"stdout": "", "stderr": "tr: usage: tr SET1 SET2", "exitCode": 1} | |
| set1, set2 = rest[0], rest[1] | |
| if len(rest) > 2: | |
| src = " ".join(rest[2:]) | |
| for a, b in zip(set1, set2): | |
| src = src.replace(a, b) | |
| return {"stdout": src, "stderr": "", "exitCode": 0} | |
| return {"stdout": "", "stderr": "tr: no input", "exitCode": 1} | |
| if tool == "file": | |
| if not rest: | |
| return {"stdout": "", "stderr": "file: missing filename", "exitCode": 1} | |
| fname = rest[0] | |
| p = os.path.join(workdir, fname.lstrip("/\\").replace("..", "_")) | |
| if not os.path.isfile(p): | |
| return {"stdout": "", "stderr": f"file: {fname}: No such file", "exitCode": 1} | |
| with open(p, "rb") as f: | |
| header = f.read(8) | |
| if header.startswith(b"\xff\xd8\xff"): | |
| return {"stdout": f"{fname}: JPEG image data, JFIF standard 1.01", "stderr": "", "exitCode": 0} | |
| if header.startswith(b"\x89PNG\r\n\x1a\n"): | |
| return {"stdout": f"{fname}: PNG image data, 8-bit/color RGBA, non-interlaced", "stderr": "", "exitCode": 0} | |
| if header.startswith(b"PK\x03\x04"): | |
| return {"stdout": f"{fname}: Zip archive data, at least v2.0 to extract", "stderr": "", "exitCode": 0} | |
| with open(p, "rb") as f: | |
| content = f.read() | |
| if b"PK\x03\x04" in content: | |
| if content.startswith(b"\xff\xd8\xff"): | |
| return {"stdout": f"{fname}: JPEG image data (with appended Zip archive data)", "stderr": "", "exitCode": 0} | |
| return {"stdout": f"{fname}: Zip archive data (appended)", "stderr": "", "exitCode": 0} | |
| try: | |
| content.decode("utf-8") | |
| return {"stdout": f"{fname}: ASCII text", "stderr": "", "exitCode": 0} | |
| except UnicodeDecodeError: | |
| return {"stdout": f"{fname}: data", "stderr": "", "exitCode": 0} | |
| if tool == "strings": | |
| if not rest: | |
| return {"stdout": "", "stderr": "strings: missing filename", "exitCode": 1} | |
| fname = rest[0] | |
| p = os.path.join(workdir, fname.lstrip("/\\").replace("..", "_")) | |
| if not os.path.isfile(p): | |
| return {"stdout": "", "stderr": f"strings: {fname}: No such file", "exitCode": 1} | |
| with open(p, "rb") as f: | |
| data = f.read() | |
| out = [] | |
| curr = [] | |
| for b in data: | |
| if 32 <= b <= 126 or b == 10 or b == 13: | |
| curr.append(chr(b)) | |
| else: | |
| if len(curr) >= 4: | |
| out.append("".join(curr).strip()) | |
| curr = [] | |
| if len(curr) >= 4: | |
| out.append("".join(curr).strip()) | |
| out = [line for line in out if line] | |
| return {"stdout": "\n".join(out[:1000]), "stderr": "", "exitCode": 0} | |
| if tool in ("unzip", "unzip-file"): | |
| if not rest: | |
| return {"stdout": "", "stderr": "unzip: missing filename", "exitCode": 1} | |
| fname = rest[0] | |
| args_clean = [a for a in rest if not a.startswith("-")] | |
| if not args_clean: | |
| return {"stdout": "", "stderr": "unzip: missing filename", "exitCode": 1} | |
| fname = args_clean[0] | |
| p = os.path.join(workdir, fname.lstrip("/\\").replace("..", "_")) | |
| if not os.path.isfile(p): | |
| return {"stdout": "", "stderr": f"unzip: {fname}: No such file", "exitCode": 1} | |
| import zipfile | |
| try: | |
| out_lines = [f"Archive: {fname}"] | |
| with zipfile.ZipFile(p, 'r') as zip_ref: | |
| # Validate all paths before extraction (Zip Slip prevention) | |
| for info in zip_ref.infolist(): | |
| target = os.path.realpath(os.path.join(workdir, info.filename)) | |
| if not target.startswith(os.path.realpath(workdir) + os.sep) and target != os.path.realpath(workdir): | |
| return { | |
| "stdout": "", | |
| "stderr": f"unzip: blocked path traversal: {info.filename}", | |
| "exitCode": 1, | |
| } | |
| # Safe to extract | |
| zip_ref.extractall(workdir) | |
| for info in zip_ref.infolist(): | |
| out_lines.append(f" extracting: {info.filename}") | |
| return {"stdout": "\n".join(out_lines), "stderr": "", "exitCode": 0} | |
| except Exception as e: | |
| return {"stdout": "", "stderr": "unzip: error occurred", "exitCode": 1} | |
| except Exception as e: | |
| return {"stdout": "", "stderr": f"{tool}: {e}", "exitCode": 1} | |
| return None # not a builtin | |