From 1c0a3ff46802fccaaed11a2eef8b918593cd049c Mon Sep 17 00:00:00 2001 From: smueller Date: Thu, 28 May 2026 17:20:50 +0200 Subject: [PATCH] refactor(obfuscate_release): enhance asset processing and validation logic for JS and CSS files --- .../obfuscate_release.cpython-314.pyc | Bin 13123 -> 18493 bytes scripts/obfuscate_release.py | 152 ++++++++++++++---- 2 files changed, 118 insertions(+), 34 deletions(-) diff --git a/scripts/__pycache__/obfuscate_release.cpython-314.pyc b/scripts/__pycache__/obfuscate_release.cpython-314.pyc index 475c68f4eac09741fed4f2a823e0f1a7aa39348a..0051a92739143bdd083ea1115c0245e875b476db 100644 GIT binary patch literal 18493 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 aw$&kQyV!-cJE`QA!BD(k;mRi{A! diff --git a/scripts/obfuscate_release.py b/scripts/obfuscate_release.py index 9f0fa6e..0c4df20 100644 --- a/scripts/obfuscate_release.py +++ b/scripts/obfuscate_release.py @@ -3,15 +3,17 @@ from __future__ import annotations import argparse import hashlib -import os import re import shutil import subprocess import sys +import tempfile +from collections import defaultdict from pathlib import Path TEXT_EXTENSIONS = {".php", ".html", ".htm", ".xml", ".txt", ".js", ".css"} +HASH_SUFFIX_RE = re.compile(r"\.[a-f0-9]{12}$", re.I) def strip_comments_keep_strings(text: str) -> str: @@ -174,42 +176,93 @@ def minify_js_fallback(text: str) -> str: return text.strip() -def run_cmd(command: list[str], cwd: Path, input_text: str | None = None) -> str: +def canonical_asset_base(stem: str) -> str: + name = stem + while HASH_SUFFIX_RE.search(name): + name = HASH_SUFFIX_RE.sub("", name) + return name + + +def is_skipped_asset(path: Path) -> bool: + lowered = path.as_posix().lower() + if ".min." in path.name or ".obf." in path.name or ".deob." in path.name: + return True + if "deobfuscated" in lowered: + return True + return False + + +def is_valid_source_content(content: str) -> bool: + if "[javascript-obfuscator-cli]" in content: + return False + return len(content.strip()) >= 20 + + +def collect_asset_groups(asset_root: Path) -> dict[tuple[Path, str, str], list[Path]]: + groups: dict[tuple[Path, str, str], list[Path]] = defaultdict(list) + for ext in (".js", ".css"): + for file_path in sorted(asset_root.rglob(f"*{ext}")): + if is_skipped_asset(file_path): + continue + base = canonical_asset_base(file_path.stem) + key = (file_path.parent, base, ext) + groups[key].append(file_path) + return groups + + +def pick_source_file(paths: list[Path], base: str, ext: str) -> Path: + plain = paths[0].parent / f"{base}{ext}" + if plain in paths: + return plain + return min(paths, key=lambda p: len(p.name)) + + +def run_cmd(command: list[str], cwd: Path) -> None: proc = subprocess.run( command, cwd=str(cwd), - input=input_text, text=True, capture_output=True, check=False, ) if proc.returncode != 0: - raise RuntimeError(proc.stderr.strip() or "command failed") - return proc.stdout + raise RuntimeError(proc.stderr.strip() or proc.stdout.strip() or "command failed") def process_js(path: Path, cwd: Path) -> None: original = path.read_text(encoding="utf-8") + if not is_valid_source_content(original): + raise ValueError(f"invalid or corrupted JS source: {path}") + if shutil.which("npx"): + tmpdir = Path(tempfile.mkdtemp()) try: - minified = run_cmd( + src = tmpdir / "input.js" + out = tmpdir / "output.js" + src.write_text(original, encoding="utf-8") + run_cmd( [ "npx", "--yes", "terser", + str(src), + "-o", + str(src), "--compress", "--mangle", "--comments", "false", ], cwd, - input_text=original, ) - obfuscated = run_cmd( + run_cmd( [ "npx", "--yes", "javascript-obfuscator", + str(src), + "--output", + str(out), "--compact", "true", "--control-flow-flattening", @@ -224,38 +277,51 @@ def process_js(path: Path, cwd: Path) -> None: "browser-no-eval", "--source-map", "false", - "--output", - "stdout", ], cwd, - input_text=minified, ) - path.write_text(obfuscated.strip() + "\n", encoding="utf-8") + if not out.exists(): + raise RuntimeError("obfuscator produced no output file") + result = out.read_text(encoding="utf-8") + if not is_valid_source_content(result): + raise RuntimeError("obfuscator output looks invalid") + path.write_text(result.strip() + "\n", encoding="utf-8") return except Exception: pass + finally: + shutil.rmtree(tmpdir, ignore_errors=True) + path.write_text(minify_js_fallback(original) + "\n", encoding="utf-8") def process_css(path: Path, cwd: Path) -> None: original = path.read_text(encoding="utf-8") if shutil.which("npx"): + tmpdir = Path(tempfile.mkdtemp()) try: - minified = run_cmd( + src = tmpdir / "input.css" + out = tmpdir / "output.css" + src.write_text(original, encoding="utf-8") + run_cmd( [ "npx", "--yes", "clean-css-cli", + str(src), + "-o", + str(out), "--skip-rebase", "-O2", ], cwd, - input_text=original, ) - path.write_text(minified.strip() + "\n", encoding="utf-8") + path.write_text(out.read_text(encoding="utf-8").strip() + "\n", encoding="utf-8") return except Exception: pass + finally: + shutil.rmtree(tmpdir, ignore_errors=True) path.write_text(minify_css_fallback(original) + "\n", encoding="utf-8") @@ -266,8 +332,7 @@ def process_php(path: Path) -> None: def hash_file(path: Path) -> str: - digest = hashlib.sha256(path.read_bytes()).hexdigest()[:12] - return digest + return hashlib.sha256(path.read_bytes()).hexdigest()[:12] def replace_references(root: Path, mapping: dict[str, str]) -> None: @@ -279,7 +344,7 @@ def replace_references(root: Path, mapping: dict[str, str]) -> None: except UnicodeDecodeError: continue updated = content - for src, dst in mapping.items(): + for src, dst in sorted(mapping.items(), key=lambda item: len(item[0]), reverse=True): updated = updated.replace(src, dst) updated = updated.replace("/" + src, "/" + dst) if updated != content: @@ -289,17 +354,30 @@ def replace_references(root: Path, mapping: dict[str, str]) -> None: def build_hash_mapping(public_root: Path) -> dict[str, str]: mapping: dict[str, str] = {} asset_root = public_root / "assets" - for ext in (".js", ".css"): - for file_path in sorted(asset_root.rglob(f"*{ext}")): - if ".min." in file_path.name or ".obf." in file_path.name: - continue - digest = hash_file(file_path) - new_name = f"{file_path.stem}.{digest}{file_path.suffix}" - new_path = file_path.with_name(new_name) - file_path.rename(new_path) - rel_old = file_path.relative_to(public_root).as_posix() - rel_new = new_path.relative_to(public_root).as_posix() - mapping[rel_old] = rel_new + if not asset_root.exists(): + return mapping + + groups = collect_asset_groups(asset_root) + for (parent, base, ext), paths in groups.items(): + source = pick_source_file(paths, base, ext) + digest = hash_file(source) + target = parent / f"{base}.{digest}{ext}" + rel_new = target.relative_to(public_root).as_posix() + + for old in paths: + rel_old = old.relative_to(public_root).as_posix() + if rel_old != rel_new: + mapping[rel_old] = rel_new + + if source != target: + if target.exists(): + target.unlink() + source.replace(target) + + for old in paths: + if old != target and old.exists(): + old.unlink() + return mapping @@ -316,11 +394,17 @@ def main() -> int: print("public directory not found", file=sys.stderr) return 1 - for js in sorted(public_root.rglob("*.js")): - process_js(js, repo_root) - for css in sorted(public_root.rglob("*.css")): - process_css(css, repo_root) - for php in sorted((repo_root / "public").rglob("*.php")): + asset_root = public_root / "assets" + if asset_root.exists(): + groups = collect_asset_groups(asset_root) + for (_parent, base, ext), paths in sorted(groups.items()): + source = pick_source_file(paths, base, ext) + if ext == ".js": + process_js(source, repo_root) + else: + process_css(source, repo_root) + + for php in sorted(public_root.rglob("*.php")): process_php(php) for php in sorted((repo_root / "backend").rglob("*.php")): process_php(php)