mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild an Android expansion OBB from the matching Apktool-decoded tree."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def read_no_compress(config: Path) -> tuple[str, list[str]]:
|
||||
lines = config.read_text(encoding="utf-8").splitlines()
|
||||
name = next((line.split(":", 1)[1].strip().strip("'\"")
|
||||
for line in lines if line.startswith("apkFileName:")), None)
|
||||
if not name:
|
||||
raise ValueError(f"{config} has no apkFileName")
|
||||
patterns: list[str] = []
|
||||
in_list = False
|
||||
for line in lines:
|
||||
if line == "doNotCompress:":
|
||||
in_list = True
|
||||
continue
|
||||
if in_list and line.startswith("- "):
|
||||
patterns.append(line[2:].strip().strip("'\"").replace("\\", "/"))
|
||||
elif in_list and line and not line.startswith(" "):
|
||||
break
|
||||
if not patterns:
|
||||
raise ValueError(f"{config} has no doNotCompress list")
|
||||
return name, patterns
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("decoded_obb", help="decoded OBB root containing apktool.yml and assets/")
|
||||
parser.add_argument("output", help="path for the rebuilt .obb")
|
||||
parser.add_argument("--check-only", action="store_true", help="validate inputs and print a packing summary")
|
||||
args = parser.parse_args()
|
||||
|
||||
root = Path(args.decoded_obb).resolve()
|
||||
config = root / "apktool.yml"
|
||||
assets = root / "assets"
|
||||
if not config.is_file() or not assets.is_dir():
|
||||
print("decoded OBB must contain apktool.yml and assets/", file=sys.stderr)
|
||||
return 2
|
||||
output = Path(args.output).resolve()
|
||||
if output.exists() and not args.check_only:
|
||||
print(f"refusing to overwrite existing output: {output}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
try:
|
||||
obb_name, patterns = read_no_compress(config)
|
||||
files = sorted((path for path in assets.rglob("*") if path.is_file()),
|
||||
key=lambda path: path.relative_to(root).as_posix())
|
||||
names = {path.relative_to(root).as_posix(): path for path in files}
|
||||
exact = {pattern for pattern in patterns if pattern in names}
|
||||
extension_rules = {pattern.lower().lstrip(".") for pattern in patterns if "/" not in pattern and pattern not in names}
|
||||
unknown = [pattern for pattern in patterns if pattern not in names and pattern not in extension_rules]
|
||||
if unknown:
|
||||
raise ValueError(f"unmatched doNotCompress entries: {unknown[:8]}")
|
||||
stored = {name for name in names if name in exact or
|
||||
Path(name).suffix.lower().lstrip(".") in extension_rules}
|
||||
matched_extensions = {Path(name).suffix.lower().lstrip(".") for name in names
|
||||
if Path(name).suffix.lower().lstrip(".") in extension_rules}
|
||||
missing_extensions = sorted(extension_rules - matched_extensions)
|
||||
if missing_extensions:
|
||||
raise ValueError(f"doNotCompress extensions have no matching file: {missing_extensions}")
|
||||
uncompressed_bytes = sum(path.stat().st_size for name, path in names.items() if name in stored)
|
||||
total_bytes = sum(path.stat().st_size for path in files)
|
||||
if output.name != obb_name:
|
||||
raise ValueError(f"output filename must be {obb_name!r}, not {output.name!r}")
|
||||
except (OSError, ValueError) as exc:
|
||||
print(f"cannot package decoded OBB: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
print(f"source: {root}")
|
||||
print(f"output: {output}")
|
||||
print(f"entries: {len(files):,}; uncompressed bytes: {total_bytes:,}")
|
||||
print(f"stored entries: {len(stored):,}; stored bytes: {uncompressed_bytes:,}; deflated entries: {len(files) - len(stored):,}")
|
||||
if args.check_only:
|
||||
return 0
|
||||
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd, temporary_name = tempfile.mkstemp(prefix=output.name + ".", suffix=".tmp", dir=output.parent)
|
||||
os.close(fd)
|
||||
temporary = Path(temporary_name)
|
||||
try:
|
||||
with zipfile.ZipFile(temporary, "w", allowZip64=True, compression=zipfile.ZIP_DEFLATED,
|
||||
compresslevel=6) as archive:
|
||||
for path in files:
|
||||
name = path.relative_to(root).as_posix()
|
||||
archive.write(path, arcname=name,
|
||||
compress_type=zipfile.ZIP_STORED if name in stored else zipfile.ZIP_DEFLATED,
|
||||
compresslevel=None if name in stored else 6)
|
||||
# Validate every CRC and path before publishing the archive at its final name.
|
||||
with zipfile.ZipFile(temporary, "r") as archive:
|
||||
bad_entry = archive.testzip()
|
||||
if bad_entry is not None:
|
||||
raise ValueError(f"archive CRC check failed at {bad_entry}")
|
||||
if set(archive.namelist()) != set(names):
|
||||
raise ValueError("archive entry list differs from the decoded assets tree")
|
||||
os.replace(temporary, output)
|
||||
except Exception:
|
||||
temporary.unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
print(f"created {output} ({output.stat().st_size:,} bytes); CRC and entry-list checks passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user