"""Archive extraction and locating the game directory inside it.""" import os import shutil import subprocess import zipfile from pathlib import Path class ArchiveError(Exception): pass def _safe_target(root: Path, name: str) -> Path | None: target = (root / name).resolve() return target if target.is_relative_to(root.resolve()) else None def extract(archive: Path, filename: str, dest: Path, *, only_scripts: bool = False) -> None: dest.mkdir(parents=True, exist_ok=True) if filename.lower().endswith(".zip") and zipfile.is_zipfile(archive): _extract_zip(archive, dest, only_scripts=only_scripts) return seven = shutil.which("7zz") or shutil.which("7z") if not seven: raise ArchiveError("Для этого формата нужен 7-Zip (7zz), он не установлен") filters = [f"-ir!*{suffix}" for suffix in SCRIPT_EXTS | {".tar"}] if only_scripts else [] proc = subprocess.run([seven, "x", "-y", "-snl-", *filters, f"-o{dest}", str(archive)], capture_output=True, text=True) if proc.returncode not in (0, 1): # 1 = warnings raise ArchiveError(f"7-Zip: {proc.stderr.strip() or proc.stdout.strip()[-500:]}") _drop_escaping_links(dest) # .tar.gz etc. extract to a single inner .tar first inner = list(dest.iterdir()) if len(inner) == 1 and inner[0].suffix == ".tar": tar = inner[0] extract(tar, tar.name, dest / "_tar", only_scripts=only_scripts) tar.unlink() SCRIPT_EXTS = {".rpy", ".rpyc", ".rpa", ".rpym", ".rpymc"} def _extract_zip(archive: Path, dest: Path, *, only_scripts: bool = False) -> None: with zipfile.ZipFile(archive) as zf: for info in zf.infolist(): name = info.filename if only_scripts and Path(name).suffix.lower() not in SCRIPT_EXTS: continue if info.flag_bits & 0x800 == 0: # legacy zips store names in cp437; many were really written as cp866/utf-8 try: name = name.encode("cp437").decode("utf-8") except (UnicodeEncodeError, UnicodeDecodeError): pass target = _safe_target(dest, name) if target is None: continue if info.is_dir(): target.mkdir(parents=True, exist_ok=True) continue target.parent.mkdir(parents=True, exist_ok=True) with zf.open(info) as src, target.open("wb") as out: shutil.copyfileobj(src, out, 1 << 20) def _drop_escaping_links(root: Path) -> None: for dirpath, dirnames, filenames in os.walk(root): for name in dirnames + filenames: p = Path(dirpath, name) if p.is_symlink(): p.unlink() def find_game_dir(root: Path) -> Path: """Find the Ren'Py `game` directory (the one holding scripts or archives).""" best: tuple[int, Path] | None = None for dirpath, _, filenames in os.walk(root): exts = {Path(f).suffix.lower() for f in filenames} if not exts & {".rpy", ".rpyc", ".rpa", ".rpymc"}: continue p = Path(dirpath) depth = len(p.relative_to(root).parts) score = (0 if p.name.lower() == "game" else 100) + depth if p.name.lower() in ("renpy", "common") or "renpy" in [x.lower() for x in p.parts[-3:]]: continue if best is None or score < best[0]: best = (score, p) if best is None: raise ArchiveError("В архиве не найдена игра Ren'Py (нет .rpy/.rpyc/.rpa)") game = best[1] # scripts in a subfolder of game/ — walk up to game/ itself for parent in [game, *game.parents]: if parent.name.lower() == "game": return parent if parent == root: break return game