Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Random number added in the archive extractor #37

Open
Tristimdorion opened this issue Jan 8, 2022 · 0 comments
Open

Random number added in the archive extractor #37

Tristimdorion opened this issue Jan 8, 2022 · 0 comments
Labels
New Archive Variant Requests to support new variants of the RPA archive format.

Comments

@Tristimdorion
Copy link

Tristimdorion commented Jan 8, 2022

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

@Tristimdorion Tristimdorion added the New Archive Variant Requests to support new variants of the RPA archive format. label Jan 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
New Archive Variant Requests to support new variants of the RPA archive format.
Projects
None yet
Development

No branches or pull requests

1 participant