Source code for rpgmaker_mv_decoder.projectdecoder

"""Class for decoding a project"""


import struct
from pathlib import Path, PurePath
from typing import List, TypeVar

import click
import magic

from rpgmaker_mv_decoder.callbacks import Callbacks
from rpgmaker_mv_decoder.clickdisplay import ClickDisplay
from rpgmaker_mv_decoder.constants import OCT_STREAM, RPG_MAKER_MV_MAGIC
from rpgmaker_mv_decoder.exceptions import FileFormatError, RPGMakerHeaderError
from rpgmaker_mv_decoder.project import Project
from rpgmaker_mv_decoder.utils import int_xor

_T = TypeVar("_T", bound="ProjectDecoder")


[docs]class ProjectDecoder(Project): """Handles a project and runs operations""" def __init__( self: _T, source: PurePath, destination: PurePath, key: str, callbacks: Callbacks = Callbacks(), ) -> _T: """`ProjectDecoder` constructor Args: - `source` (`PurePath`): Where to find the files to decode - `destination` (`PurePath`): Where to save the files to decode - `key` (`str`): Key to use when decoding - `callbacks` (`Callback`, optional): Callbacks to run on events.\ Defaults to `Callback()`. Returns: - `ProjectDecoder`: object to run actions on """ Project.__init__(self, source, destination, key, callbacks) def _get_output_filename(self: _T, filename: Path, data: bytes = None) -> str: """`_get_output_filename` Returns a file name for the specified file If data is not `None`, uses libmagic to figure out the actual file type and place a proper extension on the file. Otherwise it uses the original name to generate the extension. Args: - `filename` (`Path`): Original file path. - `data` (`bytes`, optional): File data (decoded) for libmagic. \ Defaults to `None`. Raises: - `FileFormatError`: If libmagic can't determine the file type\ or the existing file extension is unknown. Returns: - `str`: The decoded file extension """ output_file: PurePath = self.project_paths.output_directory.joinpath( PurePath(filename).relative_to(self.project_paths.source) ) if data: filetype: str = magic.from_buffer(data, mime=True) if filetype == OCT_STREAM: raise FileFormatError( f'"{filetype}" == "{OCT_STREAM}"', "Found octlet stream, key is probably incorrect.", ) return output_file.with_suffix("." + filetype.split("/")[-1]) if not filename: raise ValueError("data and filename are both None") if filename.suffix == ".rpgmvp": return output_file.with_suffix(".png") if filename.suffix == ".rpgmvo": return output_file.with_suffix(".ogg") raise FileFormatError( f'"{filename.suffix}"', f'Unknown extension "{filename.suffix}"', )
[docs] def decode_header(self: _T, file_header: bytes) -> bytes: """`decode_header` take a RPGMaker header and return the key or the actual file header Check's the first 16 bytes for the standard RPGMaker header, then drops them. Takes the next 16 bytes and either calculates the key based on a PNG image, or uses the specify key to decode. If png_ihdr_data is provided, checks that the IHDR section checksums correctly. Args: - `file_header` (`bytes`): First 32 bytes from the file, 16 bytes are the RPGMaker header,\ followed by 16 bytes of the file header Raises: - `RPGMakerHeaderError`: The header doesn't match RPGMaker's header Returns: - `bytes`: If key was None, the key needed for a PNG image header, otherwise the decoded\ file header. """ file_id: bytes header: bytes (file_id, header) = struct.unpack("!16s16s", file_header) if file_id != RPG_MAKER_MV_MAGIC: raise RPGMakerHeaderError( f'"{file_id.hex()}" != "{RPG_MAKER_MV_MAGIC.hex()}"', "First 16 bytes of this file do not match the RPGMaker header, " "is this a RPGMaker file?", ) return int_xor(bytes.fromhex(self.key), header)
[docs] def decode_file(self: _T, input_file: PurePath, detect_type: bool) -> bool: """`decode_file` Takes a path and decodes a file Args: - `input_file` (`PurePath`): File to read and modify - `detect_type` (`bool`): True means generate file extensions based on\ file contents Returns: - `bool`: True if the operation should continue """ output_file = self._get_output_filename(input_file) data: bytes with click.open_file(input_file, "rb") as file: data: bytes = self.decode_header(file.read(32)) data += file.read() if detect_type: output_file = self._get_output_filename(input_file, data) return self._save_file(output_file, data)
[docs] def decode( self: _T, detect_type: bool, ) -> None: """`decode` Decodes a project Args: - `detect_type` (`bool`): True means generate file extensions based on\ file contents """ self._callbacks.info(f"Reading from: '{self.project_paths.source}'") self._callbacks.info(f"Writing to: '{self.project_paths.output_directory}'") files: List[Path] = self.project_paths.encoded_files click_display = ClickDisplay(files) with click.progressbar( files, label="Decoding files", width=0, item_show_func=click_display.show_item, ) as files_to_decode: filename: Path for filename in files_to_decode: if self._callbacks.progressbar(files_to_decode): break try: if not self.decode_file(filename, detect_type): break except RPGMakerHeaderError: warning_text: str = f'Invalid header found on "{filename}", skipping.' self._callbacks.warning(warning_text) except FileFormatError: self._callbacks.warning( "Found octlet stream, key is probably incorrect, " f"skipping {click.format_filename(str(filename))}" ) self._callbacks.progressbar(None)