"""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"
{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