build(python): vendor rpycdec
This commit is contained in:
317
third_party/python/rpycdec-0.1.13+489e9c6/src/rpycdec/cli.py
vendored
Normal file
317
third_party/python/rpycdec-0.1.13+489e9c6/src/rpycdec/cli.py
vendored
Normal file
@@ -0,0 +1,317 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from rpycdec.apk import extract_apk
|
||||
from rpycdec.decompile import decompile
|
||||
from rpycdec.rpa import create_rpa, extract_rpa
|
||||
from rpycdec.save import dump_save_info, extract_save, generate_new_key, restore_save
|
||||
from rpycdec.translate import extract_translations
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def regular_expression(value: str) -> str:
|
||||
"""Validate a command-line regular expression."""
|
||||
try:
|
||||
re.compile(value)
|
||||
except re.error as exc:
|
||||
raise argparse.ArgumentTypeError(f"invalid regular expression: {exc}") from exc
|
||||
return value
|
||||
|
||||
|
||||
def non_empty_suffix(value: str) -> str:
|
||||
"""Validate a command-line file suffix."""
|
||||
if not value:
|
||||
raise argparse.ArgumentTypeError("suffix must not be empty")
|
||||
return value
|
||||
|
||||
|
||||
def decompile_files(srcs: list[str], **kwargs):
|
||||
"""
|
||||
decompile rpyc file or directory.
|
||||
"""
|
||||
for src in srcs:
|
||||
decompile(
|
||||
src,
|
||||
output_path=kwargs.get("output"),
|
||||
dis=kwargs.get("disassemble", False),
|
||||
)
|
||||
|
||||
|
||||
def extract_rpa_files(srcs: list[str], **kwargs):
|
||||
"""
|
||||
extract rpa archive.
|
||||
"""
|
||||
for src in srcs:
|
||||
with open(src, "rb") as f:
|
||||
output_path = kwargs.get("output") or os.path.dirname(src)
|
||||
extract_rpa(
|
||||
f,
|
||||
output_dir=output_path,
|
||||
expressions=kwargs.get("expressions"),
|
||||
suffixes=kwargs.get("suffixes"),
|
||||
)
|
||||
|
||||
|
||||
def create_rpa_archive(srcs: list[str], output: str, **kwargs):
|
||||
"""
|
||||
create rpa archive.
|
||||
"""
|
||||
create_rpa(
|
||||
output,
|
||||
srcs,
|
||||
base_dir=kwargs.get("base"),
|
||||
)
|
||||
|
||||
|
||||
def run_extract_translations(
|
||||
srcs: list[str], language: str, output: str | None = None, **kwargs
|
||||
):
|
||||
"""
|
||||
extract translations from rpy files.
|
||||
"""
|
||||
for src in srcs:
|
||||
# Default output to current directory, extract_translations will create tl/{language}/
|
||||
dest = output or "."
|
||||
extract_translations(
|
||||
game_dir=src,
|
||||
output_dir=dest,
|
||||
language=language,
|
||||
include_dialogues=kwargs.get("include_dialogues", True),
|
||||
include_strings=kwargs.get("include_strings", True),
|
||||
empty_translation=kwargs.get("empty_translation", False),
|
||||
)
|
||||
|
||||
|
||||
def extract_game_from_apk(apk_path: str, **kwargs):
|
||||
"""
|
||||
Extract Ren'Py game from Android APK file.
|
||||
"""
|
||||
extract_apk(
|
||||
apk_path=apk_path,
|
||||
output_dir=kwargs.get("output"),
|
||||
)
|
||||
|
||||
|
||||
SECURITY_WARNING = (
|
||||
"\033[33mSecurity Warning: This tool uses a restricted pickle unpickler with "
|
||||
"whitelist-based class loading to mitigate arbitrary code execution risks. "
|
||||
"However, no safeguard is perfect. Only process files from trusted sources.\033[0m"
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
command line tool entry.
|
||||
"""
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
argparser = argparse.ArgumentParser()
|
||||
argparser.add_argument(
|
||||
"--verbose", "-v", action="store_true", help="verbose output"
|
||||
)
|
||||
subparsers = argparser.add_subparsers(
|
||||
title="subcommands", dest="command", help="subcommand help"
|
||||
)
|
||||
|
||||
decompile_parser = subparsers.add_parser("decompile", help="decompile rpyc file")
|
||||
decompile_parser.add_argument("src", nargs="+", help="rpyc file or directory")
|
||||
decompile_parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
help="output path",
|
||||
)
|
||||
decompile_parser.add_argument(
|
||||
"--disassemble", "-d", action="store_true", help="disassemble rpyc file"
|
||||
)
|
||||
decompile_parser.set_defaults(
|
||||
func=lambda args: decompile_files(args.src, **vars(args))
|
||||
)
|
||||
|
||||
unrpa_parser = subparsers.add_parser("unrpa", help="extract rpa archive")
|
||||
unrpa_parser.add_argument("file", nargs=1, help="rpa archive")
|
||||
unrpa_parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
help="output path",
|
||||
)
|
||||
unrpa_parser.add_argument(
|
||||
"--expression",
|
||||
"-e",
|
||||
dest="expressions",
|
||||
action="append",
|
||||
type=regular_expression,
|
||||
metavar="REGEX",
|
||||
help=(
|
||||
"extract archive paths matching this regular expression; may be repeated"
|
||||
),
|
||||
)
|
||||
unrpa_parser.add_argument(
|
||||
"--suffix",
|
||||
"-s",
|
||||
dest="suffixes",
|
||||
action="extend",
|
||||
nargs="+",
|
||||
type=non_empty_suffix,
|
||||
metavar="SUFFIX",
|
||||
help=(
|
||||
"extract archive paths ending with any of these suffixes "
|
||||
"(case-insensitive); may be repeated"
|
||||
),
|
||||
)
|
||||
unrpa_parser.set_defaults(
|
||||
func=lambda args: extract_rpa_files(args.file, **vars(args))
|
||||
)
|
||||
|
||||
rpa_parser = subparsers.add_parser("rpa", help="create rpa archive")
|
||||
rpa_parser.add_argument("src", nargs="+", help="file or directory to add")
|
||||
rpa_parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
required=True,
|
||||
help="output .rpa archive path",
|
||||
)
|
||||
rpa_parser.add_argument(
|
||||
"--base",
|
||||
help=(
|
||||
"base directory for archive names "
|
||||
"(default: source directory for a single directory, otherwise common parent)"
|
||||
),
|
||||
)
|
||||
rpa_parser.set_defaults(
|
||||
func=lambda args: create_rpa_archive(
|
||||
args.src,
|
||||
args.output,
|
||||
base=args.base,
|
||||
)
|
||||
)
|
||||
|
||||
extract_game_parser = subparsers.add_parser(
|
||||
"extract-game", help="extract Ren'Py game from Android APK file"
|
||||
)
|
||||
extract_game_parser.add_argument("apk", help="Android APK file path")
|
||||
extract_game_parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
help="output directory (default: APK filename without extension)",
|
||||
)
|
||||
extract_game_parser.set_defaults(
|
||||
func=lambda args: extract_game_from_apk(args.apk, **vars(args))
|
||||
)
|
||||
|
||||
extract_translate_parser = subparsers.add_parser(
|
||||
"extract-translate",
|
||||
help="""extract translations from rpy files and save to tl/<language> directory.""",
|
||||
)
|
||||
extract_translate_parser.add_argument(
|
||||
"src", nargs="+", help="source game directory"
|
||||
)
|
||||
extract_translate_parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
help="output directory (default: current directory, creates tl/<language>/)",
|
||||
)
|
||||
extract_translate_parser.add_argument(
|
||||
"--language",
|
||||
"-l",
|
||||
required=True,
|
||||
help="target language code (e.g. chinese, japanese)",
|
||||
)
|
||||
extract_translate_parser.add_argument(
|
||||
"--no-dialogues",
|
||||
action="store_true",
|
||||
help="skip dialogue translations",
|
||||
)
|
||||
extract_translate_parser.add_argument(
|
||||
"--no-strings",
|
||||
action="store_true",
|
||||
help="skip string translations",
|
||||
)
|
||||
extract_translate_parser.add_argument(
|
||||
"--empty",
|
||||
action="store_true",
|
||||
help="generate empty translations (default: copy original)",
|
||||
)
|
||||
extract_translate_parser.set_defaults(
|
||||
func=lambda args: run_extract_translations(
|
||||
args.src,
|
||||
args.language,
|
||||
output=args.output,
|
||||
include_dialogues=not args.no_dialogues,
|
||||
include_strings=not args.no_strings,
|
||||
empty_translation=args.empty,
|
||||
)
|
||||
)
|
||||
|
||||
save_parser = subparsers.add_parser("save", help="save operations")
|
||||
save_subparsers = save_parser.add_subparsers(
|
||||
title="save subcommands", dest="save_command", help="save subcommand help"
|
||||
)
|
||||
save_extract_parser = save_subparsers.add_parser(
|
||||
"extract", help="extract save data to JSON files for editing"
|
||||
)
|
||||
save_extract_parser.add_argument("path", nargs=1, help="save-file path")
|
||||
save_extract_parser.add_argument("dest", nargs="?", help="extract to directory")
|
||||
save_extract_parser.add_argument(
|
||||
"--disassemble", "-d", action="store_true", help="print pickle disassembly"
|
||||
)
|
||||
save_extract_parser.add_argument(
|
||||
"--verbose", "-V", action="store_true", help="verbose class loading output"
|
||||
)
|
||||
save_extract_parser.set_defaults(
|
||||
func=lambda args: extract_save(
|
||||
args.path[0],
|
||||
args.dest or "",
|
||||
dissemble=args.disassemble,
|
||||
verbose=getattr(args, "verbose", False),
|
||||
)
|
||||
)
|
||||
|
||||
save_restore_parser = save_subparsers.add_parser(
|
||||
"restore", help="restore save data from extracted JSON directory"
|
||||
)
|
||||
save_restore_parser.add_argument("path", nargs=1, help="extracted directory")
|
||||
save_restore_parser.add_argument("dest", nargs="?", help="restore to save-file")
|
||||
save_restore_parser.add_argument(
|
||||
"--key", "-k", help="path to security_keys.txt for re-signing"
|
||||
)
|
||||
save_restore_parser.set_defaults(
|
||||
func=lambda args: restore_save(
|
||||
args.path[0], args.dest or "", key_file=args.key or ""
|
||||
)
|
||||
)
|
||||
|
||||
save_info_parser = save_subparsers.add_parser(
|
||||
"info", help="show save file information"
|
||||
)
|
||||
save_info_parser.add_argument("path", nargs=1, help="save-file path")
|
||||
save_info_parser.set_defaults(func=lambda args: dump_save_info(args.path[0]))
|
||||
|
||||
save_genkey_parser = save_subparsers.add_parser(
|
||||
"genkey", help="generate a new signing key (requires ecdsa library)"
|
||||
)
|
||||
save_genkey_parser.add_argument(
|
||||
"output",
|
||||
nargs="?",
|
||||
default="security_keys.txt",
|
||||
help="output key file path (default: security_keys.txt)",
|
||||
)
|
||||
save_genkey_parser.set_defaults(func=lambda args: generate_new_key(args.output))
|
||||
|
||||
args = argparser.parse_args()
|
||||
if args.verbose:
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
if not args.command:
|
||||
argparser.print_help()
|
||||
return
|
||||
|
||||
# Show security warning for commands that process pickle data
|
||||
if args.command in ("decompile", "save", "unrpa") and not os.environ.get(
|
||||
"RPYCDEC_NO_WARNING"
|
||||
):
|
||||
print(SECURITY_WARNING, file=sys.stderr)
|
||||
|
||||
args.func(args)
|
||||
Reference in New Issue
Block a user