zcbot/deploy/sandbox/package_scan.py

85 lines
3.0 KiB
Python

"""可信临时包扫描器;只解析 dist-info 元数据,不 import 用户代码。"""
from __future__ import annotations
import csv
import email.parser
import json
from pathlib import Path
BASE = Path("/sandbox/base-python-packages.json")
ROOT = Path("/tmp/.local/lib")
def safe_file(root: Path, path: Path) -> bool:
try:
path.relative_to(root)
except ValueError:
return False
current = root
for part in path.relative_to(root).parts:
current = current / part
if current.is_symlink():
return False
return path.is_file()
def safe_dir(root: Path, path: Path) -> bool:
try:
rel = path.relative_to(root)
except ValueError:
return False
current = root
for part in rel.parts:
current = current / part
if current.is_symlink():
return False
return path.is_dir()
def canonical(name: str) -> str:
return name.lower().replace("_", "-").replace(".", "-")
def scan_packages(root: Path, base: dict) -> list[dict]:
out = []
if root.is_dir():
for site in sorted(root.glob("python*/site-packages")):
if not safe_dir(root, site):
continue
for dist in sorted(site.glob("*.dist-info")):
if len(out) >= 5000:
return out
meta_path = dist / "METADATA"
if not safe_file(site, meta_path) or meta_path.stat().st_size > 1024 * 1024:
continue
msg = email.parser.Parser().parsestr(meta_path.read_text(encoding="utf-8", errors="replace"), headersonly=True)
name, version = msg.get("Name", "").strip(), msg.get("Version", "").strip()
if not name:
continue
total = 0
record = dist / "RECORD"
if safe_file(site, record) and record.stat().st_size <= 32 * 1024 * 1024:
for index, row in enumerate(csv.reader(record.read_text(encoding="utf-8", errors="replace").splitlines())):
if index >= 200_000:
break
if not row:
continue
candidate = site / row[0]
if safe_file(site, candidate):
try: total += candidate.stat().st_size
except OSError: pass
key = canonical(name)
old = base.get(key)
change = "added" if old is None else ("reinstall" if old == version else "override")
out.append({"name": name, "version": version, "base_version": old,
"change": change, "direct_requested": safe_file(site, dist / "REQUESTED") or safe_file(site, dist / "direct_url.json"),
"installed_bytes": total})
return out
def main() -> None:
base = json.loads(BASE.read_text(encoding="utf-8"))
print(json.dumps(scan_packages(ROOT, base), ensure_ascii=False))
if __name__ == "__main__":
main()