Lattyware / unrpa

A program to extract files from the RPA archive format.
http://www.lattyware.co.uk/projects/unrpa/
GNU General Public License v3.0
607 stars 77 forks source link

Random number added in the archive extractor #37

Open Tristimdorion opened 2 years ago

Tristimdorion commented 2 years ago

What did you try to open the archive with unrpa, and how did it fail?

Extracting files from C:\Temp\DSCS-0.1.1-win\game\dscs.rpa. [0.00%] modules\0005_core\keymap.rpyc

There was an error while trying to extract a file from the archive. If you wish to try and extract as much from the archive as possible, please use --continue-on-error. Error Detail: Traceback (most recent call last): File "C:\Users\omega\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0\LocalCache\local-packages\Python37\site-packages\unrpa__init__.py", line 134, in extract_files version.postprocess(file_view, output_file) File "C:\Users\omega\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0\LocalCache\local-packages\Python37\site-packages\unrpa\versions\version.py", line 24, in postprocess for segment in iter(source.read1, b""): File "C:\Users\omega\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0\LocalCache\local-packages\Python37\site-packages\unrpa\view.py", line 20, in read1 return self.base_read(lambda source: source.read1, amount) File "C:\Users\omega\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0\LocalCache\local-packages\Python37\site-packages\unrpa\view.py", line 34, in base_read return self.base_read(method, amount) File "C:\Users\omega\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0\LocalCache\local-packages\Python37\site-packages\unrpa\view.py", line 37, in base_read raise Exception("End of archive reached before the file should end.") Exception: End of archive reached before the file should end.

Files needed to add support

from __future__ import division, absolute_import, with_statement, print_function, unicode_literals
from renpy.compat import *
import renpy, os.path, sys, types, threading, zlib, re, io, unicodedata
from renpy.compat.pickle import loads
from renpy.webloader import DownloadNeeded
(b'').encode(b'utf-8')

def get_path(fn):
    fn = os.path.join(renpy.config.gamedir, fn)
    dn = os.path.dirname(fn)
    try:
        if not os.path.exists(dn):
            os.makedirs(dn)
    except:
        pass

    return fn

if renpy.android:
    import android.apk
    expansion = os.environ.get(b'ANDROID_EXPANSION', None)
    if expansion is not None:
        print(b'Using expansion file', expansion)
        apks = [
         android.apk.APK(apk=expansion, prefix=b'assets/x-game/'),
         android.apk.APK(apk=expansion, prefix=b'assets/x-renpy/x-common/')]
        game_apks = [
         apks[0]]
    else:
        print(b'Not using expansion file.')
        apks = [
         android.apk.APK(prefix=b'assets/x-game/'),
         android.apk.APK(prefix=b'assets/x-renpy/x-common/')]
        game_apks = [
         apks[0]]
else:
    apks = []
    game_apks = []
archives = []
old_config_archives = None
lower_map = {}
archive_handlers = []

class RPAv3ArchiveHandler(object):

    @staticmethod
    def get_supported_extensions():
        return [b'.rpa']

    @staticmethod
    def get_supported_headers():
        return [b'RPA-3.0 ']

    @staticmethod
    def read_index(infile):
        l = infile.read(40)
        offset = int(l[8:24], 16)
        key = int(l[25:33], 16)
        infile.seek(offset)
        index = loads(zlib.decompress(infile.read()))
        for k in index.keys():
            if len(index[k][0]) == 2:
                index[k] = [ (offset ^ key ^ 3735929054, dlen ^ key ^ 3735929054) for offset, dlen in index[k] ]
            else:
                index[k] = [ (offset ^ key ^ 3735929054, dlen ^ key ^ 3735929054, start) for offset, dlen, start in index[k] ]

        return index

archive_handlers.append(RPAv3ArchiveHandler)

class RPAv2ArchiveHandler(object):

    @staticmethod
    def get_supported_extensions():
        return [b'.rpa']

    @staticmethod
    def get_supported_headers():
        return [b'RPA-2.0 ']

    @staticmethod
    def read_index(infile):
        l = infile.read(24)
        offset = int(l[8:], 16)
        infile.seek(offset)
        index = loads(zlib.decompress(infile.read()))
        return index

archive_handlers.append(RPAv2ArchiveHandler)

class RPAv1ArchiveHandler(object):

    @staticmethod
    def get_supported_extensions():
        return [b'.rpi']

    @staticmethod
    def get_supported_headers():
        return [b'x\x9c']

    @staticmethod
    def read_index(infile):
        return loads(zlib.decompress(infile.read()))

archive_handlers.append(RPAv1ArchiveHandler)

def index_archives():
    global archives
    global old_config_archives
    if old_config_archives == renpy.config.archives:
        return
    else:
        old_config_archives = renpy.config.archives[:]
        lower_map.clear()
        cleardirfiles()
        archives = []
        max_header_length = 0
        for handler in archive_handlers:
            for header in handler.get_supported_headers():
                header_len = len(header)
                if header_len > max_header_length:
                    max_header_length = header_len

        archive_extensions = []
        for handler in archive_handlers:
            for ext in handler.get_supported_extensions():
                if ext not in archive_extensions:
                    archive_extensions.append(ext)

        for prefix in renpy.config.archives:
            for ext in archive_extensions:
                fn = None
                f = None
                try:
                    fn = transfn(prefix + ext)
                    f = open(fn, b'rb')
                except:
                    continue

                with f:
                    file_header = f.read(max_header_length)
                    for handler in archive_handlers:
                        try:
                            archive_handled = False
                            for header in handler.get_supported_headers():
                                if file_header.startswith(header):
                                    f.seek(0, 0)
                                    index = handler.read_index(f)
                                    archives.append((prefix + ext, index))
                                    archive_handled = True
                                    break

                            if archive_handled == True:
                                break
                        except:
                            raise

        for dir, fn in listdirfiles():
            lower_map[unicodedata.normalize(b'NFC', fn.lower())] = fn

        for fn in remote_files:
            lower_map[unicodedata.normalize(b'NFC', fn.lower())] = fn

        return

def walkdir(dir):
    rv = []
    if not os.path.exists(dir) and not renpy.config.developer:
        return rv
    for i in os.listdir(dir):
        if i[0] == b'.':
            continue
        try:
            i = renpy.exports.fsdecode(i)
        except:
            continue

        if os.path.isdir(dir + b'/' + i):
            for fn in walkdir(dir + b'/' + i):
                rv.append(i + b'/' + fn)

        else:
            rv.append(i)

    return rv

game_files = []
common_files = []
loadable_cache = {}
remote_files = {}

def cleardirfiles():
    global common_files
    global game_files
    game_files = []
    common_files = []

scandirfiles_callbacks = []

def scandirfiles():
    seen = set()

    def add(dn, fn, files, seen):
        fn = unicode(fn)
        if fn in seen:
            return
        if fn.startswith(b'cache/'):
            return
        if fn.startswith(b'saves/'):
            return
        files.append((dn, fn))
        seen.add(fn)
        loadable_cache[unicodedata.normalize(b'NFC', fn.lower())] = True

    for i in scandirfiles_callbacks:
        i(add, seen)

def scandirfiles_from_apk(add, seen):
    for apk in apks:
        if apk not in game_apks:
            files = common_files
        else:
            files = game_files
        for f in apk.list():
            f = (b'/').join(i[2:] for i in f.split(b'/'))
            add(None, f, files, seen)

    return

if renpy.android:
    scandirfiles_callbacks.append(scandirfiles_from_apk)

def scandirfiles_from_remote_file(add, seen):
    index_filename = os.path.join(renpy.config.gamedir, b'renpyweb_remote_files.txt')
    if os.path.exists(index_filename):
        files = game_files
        with open(index_filename, b'rb') as (remote_index):
            while True:
                f = remote_index.readline()
                metadata = remote_index.readline()
                if f == b'' or metadata == b'':
                    break
                f = f.rstrip(b'\r\n')
                metadata = metadata.rstrip(b'\r\n')
                entry_type, entry_size = metadata.split(b' ')
                if entry_type == b'image':
                    entry_size = [ int(i) for i in entry_size.split(b',') ]
                add(b'/game', f, files, seen)
                remote_files[f] = {b'type': entry_type, b'size': entry_size}

if renpy.emscripten or os.environ.get(b'RENPY_SIMULATE_DOWNLOAD', False):
    scandirfiles_callbacks.append(scandirfiles_from_remote_file)

def scandirfiles_from_filesystem(add, seen):
    for i in renpy.config.searchpath:
        if renpy.config.commondir and i == renpy.config.commondir:
            files = common_files
        else:
            files = game_files
        i = os.path.join(renpy.config.basedir, i)
        for j in walkdir(i):
            add(i, j, files, seen)

scandirfiles_callbacks.append(scandirfiles_from_filesystem)

def scandirfiles_from_archives(add, seen):
    files = game_files
    for _prefix, index in archives:
        for j in index:
            add(None, j, files, seen)

    return

scandirfiles_callbacks.append(scandirfiles_from_archives)

def listdirfiles(common=True):
    if not game_files and not common_files:
        scandirfiles()
    if common:
        return game_files + common_files
    else:
        return list(game_files)

class SubFile(object):

    def __init__(self, fn, base, length, start):
        self.fn = fn
        self.f = None
        self.base = base
        self.offset = 0
        self.length = length
        self.start = start
        if not self.start:
            self.name = fn
        else:
            self.name = None
        return

    def open(self):
        self.f = open(self.fn, b'rb')
        self.f.seek(self.base)

    def __enter__(self):
        return self

    def __exit__(self, _type, value, tb):
        self.close()
        return False

    def read(self, length=None):
        if self.f is None:
            self.open()
        maxlength = self.length - self.offset
        if length is not None:
            length = min(length, maxlength)
        else:
            length = maxlength
        rv1 = self.start[self.offset:self.offset + length]
        length -= len(rv1)
        self.offset += len(rv1)
        if length:
            rv2 = self.f.read(length)
            self.offset += len(rv2)
        else:
            rv2 = b''
        return rv1 + rv2

    def readline(self, length=None):
        if self.f is None:
            self.open()
        maxlength = self.length - self.offset
        if length is not None:
            length = min(length, maxlength)
        else:
            length = maxlength
        if self.offset < len(self.start):
            rv = b''
            while length:
                c = self.read(1)
                rv += c
                if c == b'\n':
                    break
                length -= 1

            return rv
        rv = self.f.readline(length)
        self.offset += len(rv)
        return rv

    def readlines(self, length=None):
        rv = []
        while True:
            l = self.readline(length)
            if not l:
                break
            if length is not None:
                length -= len(l)
                if l < 0:
                    break
            rv.append(l)

        return rv

    def xreadlines(self):
        return self

    def __iter__(self):
        return self

    def __next__(self):
        rv = self.readline()
        if not rv:
            raise StopIteration()
        return rv

    next = __next__

    def flush(self):
        pass

    def seek(self, offset, whence=0):
        if self.f is None:
            self.open()
        if whence == 0:
            offset = offset
        elif whence == 1:
            offset = self.offset + offset
        elif whence == 2:
            offset = self.length + offset
        if offset > self.length:
            offset = self.length
        self.offset = offset
        offset = offset - len(self.start)
        if offset < 0:
            offset = 0
        self.f.seek(offset + self.base)
        return

    def tell(self):
        return self.offset

    def close(self):
        if self.f is not None:
            self.f.close()
            self.f = None
        return

    def write(self, s):
        raise Exception(b'Write not supported by SubFile')

open_file = open
if b'RENPY_FORCE_SUBFILE' in os.environ:

    def open_file(name, mode):
        f = open(name, mode)
        f.seek(0, 2)
        length = f.tell()
        f.seek(0, 0)
        return SubFile(f, 0, length, b'')

file_open_callbacks = []

def load_core(name):
    name = lower_map.get(unicodedata.normalize(b'NFC', name.lower()), name)
    for i in file_open_callbacks:
        rv = i(name)
        if rv is not None:
            return rv

    return

def load_from_file_open_callback(name):
    if renpy.config.file_open_callback:
        return renpy.config.file_open_callback(name)
    else:
        return

file_open_callbacks.append(load_from_file_open_callback)

def load_from_filesystem(name):
    if not renpy.config.force_archives:
        try:
            fn = transfn(name)
            return open_file(fn, b'rb')
        except:
            pass

    return

file_open_callbacks.append(load_from_filesystem)

def load_from_apk(name):
    for apk in apks:
        prefixed_name = (b'/').join(b'x-' + i for i in name.split(b'/'))
        try:
            return apk.open(prefixed_name)
        except IOError:
            pass

    return

if renpy.android:
    file_open_callbacks.append(load_from_apk)

def load_from_archive(name):
    for prefix, index in archives:
        if name not in index:
            continue
        afn = transfn(prefix)
        data = []
        if len(index[name]) == 1:
            t = index[name][0]
            if len(t) == 2:
                offset, dlen = t
                start = b''
            else:
                offset, dlen, start = t
            rv = SubFile(afn, offset, dlen, start)
        else:
            with open(afn, b'rb') as (f):
                for offset, dlen in index[name]:
                    f.seek(offset)
                    data.append(f.read(dlen))

                rv = io.BytesIO((b'').join(data))
        return rv

    return

file_open_callbacks.append(load_from_archive)

def load_from_remote_file(name):
    if name in remote_files:
        raise DownloadNeeded(relpath=name, rtype=remote_files[name][b'type'], size=remote_files[name][b'size'])
    return

if renpy.emscripten or os.environ.get(b'RENPY_SIMULATE_DOWNLOAD', False):
    file_open_callbacks.append(load_from_remote_file)

def check_name(name):
    if renpy.config.reject_backslash and b'\\' in name:
        raise Exception(b"Backslash in filename, use '/' instead: %r" % name)
    if renpy.config.reject_relative:
        split = name.split(b'/')
        if b'.' in split or b'..' in split:
            raise Exception(b"Filenames may not contain relative directories like '.' and '..': %r" % name)

def get_prefixes(tl=True):
    rv = []
    if tl:
        language = renpy.game.preferences.language
    else:
        language = None
    for prefix in renpy.config.search_prefixes:
        if language is not None:
            rv.append(renpy.config.tl_directory + b'/' + language + b'/' + prefix)
        rv.append(prefix)

    return rv

def load(name, tl=True):
    if renpy.display.predict.predicting:
        if threading.current_thread().name == b'MainThread':
            if not (renpy.emscripten or os.environ.get(b'RENPY_SIMULATE_DOWNLOAD', False)):
                raise Exception((b'Refusing to open {} while predicting.').format(name))
    if renpy.config.reject_backslash and b'\\' in name:
        raise Exception(b"Backslash in filename, use '/' instead: %r" % name)
    name = re.sub(b'/+', b'/', name).lstrip(b'/')
    for p in get_prefixes(tl):
        rv = load_core(p + name)
        if rv is not None:
            return rv

    raise IOError(b"Couldn't find file '%s'." % name)
    return

def loadable_core(name):
    name = lower_map.get(unicodedata.normalize(b'NFC', name.lower()), name)
    if name in loadable_cache:
        return loadable_cache[name]
    try:
        transfn(name)
        loadable_cache[name] = True
        return True
    except:
        pass

    for apk in apks:
        prefixed_name = (b'/').join(b'x-' + i for i in name.split(b'/'))
        if prefixed_name in apk.info:
            loadable_cache[name] = True
            return True

    for _prefix, index in archives:
        if name in index:
            loadable_cache[name] = True
            return True

    if name in remote_files:
        loadable_cache[name] = True
        return name
    loadable_cache[name] = False
    return False

def loadable(name):
    name = name.lstrip(b'/')
    if renpy.config.loadable_callback is not None and renpy.config.loadable_callback(name):
        return True
    else:
        for p in get_prefixes():
            if loadable_core(p + name):
                return True

        return False

def transfn(name):
    name = name.lstrip(b'/')
    if renpy.config.reject_backslash and b'\\' in name:
        raise Exception(b"Backslash in filename, use '/' instead: %r" % name)
    name = lower_map.get(unicodedata.normalize(b'NFC', name.lower()), name)
    if isinstance(name, bytes):
        name = name.decode(b'utf-8')
    for d in renpy.config.searchpath:
        fn = os.path.join(renpy.config.basedir, d, name)
        add_auto(fn)
        if os.path.isfile(fn):
            return fn

    raise Exception(b"Couldn't find file '%s'." % name)

hash_cache = dict()

def get_hash(name):
    rv = hash_cache.get(name, None)
    if rv is not None:
        return rv
    else:
        rv = 0
        try:
            f = load(name)
            while True:
                data = f.read(1048576)
                if not data:
                    break
                rv = zlib.adler32(data, rv)

        except:
            pass

        hash_cache[name] = rv
        return rv

class RenpyImporter(object):

    def __init__(self, prefix=b''):
        self.prefix = prefix

    def translate(self, fullname, prefix=None):
        if prefix is None:
            prefix = self.prefix
        try:
            if not isinstance(fullname, str):
                fullname = fullname.decode(b'utf-8')
            fn = prefix + fullname.replace(b'.', b'/')
        except:
            return

        if loadable(fn + b'.py'):
            return fn + b'.py'
        else:
            if loadable(fn + b'/__init__.py'):
                return fn + b'/__init__.py'
            return

    def find_module(self, fullname, path=None):
        if path is not None:
            for i in path:
                if self.translate(fullname, i):
                    return RenpyImporter(i)

        if self.translate(fullname):
            return self
        else:
            return

    def load_module(self, fullname):
        filename = self.translate(fullname, self.prefix)
        pyname = pystr(fullname)
        mod = sys.modules.setdefault(pyname, types.ModuleType(pyname))
        mod.__name__ = pyname
        mod.__file__ = filename
        mod.__loader__ = self
        if filename.endswith(b'__init__.py'):
            mod.__path__ = [
             filename[:-len(b'__init__.py')]]
        for encoding in [b'utf-8', b'latin-1']:
            try:
                source = load(filename).read().decode(encoding)
                if source and source[0] == b'\ufeff':
                    source = source[1:]
                source = source.encode(b'raw_unicode_escape')
                source = source.replace(b'\r', b'')
                code = compile(source, filename, b'exec', renpy.python.old_compile_flags, 1)
                break
            except:
                if encoding == b'latin-1':
                    raise

        exec code in mod.__dict__
        return sys.modules[fullname]

    def get_data(self, filename):
        return load(filename).read()

meta_backup = []

def add_python_directory(path):
    if path and not path.endswith(b'/'):
        path = path + b'/'
    sys.meta_path.insert(0, RenpyImporter(path))

def init_importer():
    meta_backup[:] = sys.meta_path
    add_python_directory(b'python-packages/')
    add_python_directory(b'')

def quit_importer():
    sys.meta_path[:] = meta_backup

needs_autoreload = set()
auto_mtimes = {}
auto_thread = None
auto_quit_flag = True
auto_lock = threading.Condition()
auto_blacklisted = renpy.object.Sentinel(b'auto_blacklisted')

def auto_mtime(fn):
    try:
        return os.path.getmtime(fn)
    except:
        return

    return

def add_auto(fn, force=False):
    fn = fn.replace(b'\\', b'/')
    if not renpy.autoreload:
        return
    if fn in auto_mtimes and not force:
        return
    for e in renpy.config.autoreload_blacklist:
        if fn.endswith(e):
            with auto_lock:
                auto_mtimes[fn] = auto_blacklisted
            return

    mtime = auto_mtime(fn)
    with auto_lock:
        auto_mtimes[fn] = mtime

def auto_thread_function():
    global auto_quit_flag
    global needs_autoreload
    while True:
        with auto_lock:
            auto_lock.wait(1.5)
            if auto_quit_flag:
                return
            items = list(auto_mtimes.items())
        for fn, mtime in items:
            if mtime is auto_blacklisted:
                continue
            if auto_mtime(fn) != mtime:
                with auto_lock:
                    if auto_mtime(fn) != auto_mtimes[fn]:
                        needs_autoreload.add(fn)

def check_autoreload():
    while needs_autoreload:
        fn = next(iter(needs_autoreload))
        mtime = auto_mtime(fn)
        with auto_lock:
            needs_autoreload.discard(fn)
            auto_mtimes[fn] = mtime
        if not renpy.autoreload:
            return
        for regex, func in renpy.config.autoreload_functions:
            if re.search(regex, fn, re.I):
                fn = os.path.relpath(fn, renpy.config.gamedir).replace(b'\\', b'/')
                func(fn)
                break
        else:
            renpy.exports.reload_script()

def auto_init():
    global auto_quit_flag
    global auto_thread
    global needs_autoreload
    needs_autoreload = set()
    if not renpy.autoreload:
        return
    auto_quit_flag = False
    auto_thread = threading.Thread(target=auto_thread_function)
    auto_thread.daemon = True
    auto_thread.start()

def auto_quit():
    global auto_quit_flag
    if auto_thread is None:
        return
    else:
        auto_quit_flag = True
        with auto_lock:
            auto_lock.notify_all()
        auto_thread.join()
        return

Additional context

It seems a random number is added to the key and offset to determine the index in the archive extractor. NSFW link to game