Exemplo n.º 1
0
class NCCHContainerMount(LoggingMixIn, Operations):
    fd = 0
    romfs_fuse = None
    exefs_fuse = None

    def __init__(self,
                 ncch_fp: BinaryIO,
                 g_stat: os.stat_result,
                 decompress_code: bool = True,
                 dev: bool = False,
                 seeddb: str = None):
        self.crypto = CryptoEngine(dev=dev)

        self.decompress_code = decompress_code
        self.seeddb = seeddb
        self.files: Dict[str, Dict] = {}

        # get status change, modify, and file access times
        self._g_stat = g_stat
        self.g_stat = {
            'st_ctime': int(g_stat.st_ctime),
            'st_mtime': int(g_stat.st_mtime),
            'st_atime': int(g_stat.st_atime)
        }

        ncch_header = ncch_fp.read(0x200)
        self.reader = NCCHReader.from_header(ncch_header)

        self.f = ncch_fp

        if not self.reader.flags.no_crypto:
            # I should figure out what happens if fixed-key crypto is
            #   used along with seed. even though this will never
            #   happen in practice, I would still like to see what
            #   happens if it happens.
            if self.reader.flags.fixed_crypto_key:
                normal_key = FIXED_SYSTEM_KEY if self.reader.program_id & (
                    0x10 << 32) else 0x0
                self.crypto.set_normal_key(Keyslot.NCCH,
                                           normal_key.to_bytes(0x10, 'big'))
            else:
                if self.reader.flags.uses_seed:
                    self.reader.load_seed_from_seeddb()

                self.crypto.set_keyslot(
                    'y', Keyslot.NCCH,
                    readbe(self.reader.get_key_y(original=True)))
                self.crypto.set_keyslot('y', self.reader.extra_keyslot,
                                        readbe(self.reader.get_key_y()))

    def __del__(self, *args):
        try:
            self.f.close()
        except AttributeError:
            pass

    destroy = __del__

    def init(self, path, _setup_romfs=True):
        decrypted_filename = '/decrypted.' + (
            'cxi' if self.reader.flags.executable else 'cfa')

        self.files[decrypted_filename] = {
            'size': self.reader.content_size,
            'offset': 0,
            'enctype': 'fulldec'
        }
        self.files['/ncch.bin'] = {
            'size': 0x200,
            'offset': 0,
            'enctype': 'none'
        }

        if self.reader.check_for_extheader():
            self.files['/extheader.bin'] = {
                'size': 0x800,
                'offset': 0x200,
                'enctype': 'normal',
                'keyslot': Keyslot.NCCH,
                'iv': (self.reader.partition_id << 64 | (0x01 << 56))
            }

        plain_region = self.reader.plain_region
        if plain_region.offset:
            self.files['/plain.bin'] = {
                'size': plain_region.size,
                'offset': plain_region.offset,
                'enctype': 'none'
            }

        logo_region = self.reader.logo_region
        if logo_region.offset:
            self.files['/logo.bin'] = {
                'size': logo_region.size,
                'offset': logo_region.offset,
                'enctype': 'none'
            }

        exefs_region = self.reader.exefs_region
        if exefs_region.offset:
            exefs_type = 'exefs'
            if self.reader.extra_keyslot == Keyslot.NCCH:
                exefs_type = 'normal'
            self.files['/exefs.bin'] = {
                'size': exefs_region.size,
                'offset': exefs_region.offset,
                'enctype': exefs_type,
                'keyslot': Keyslot.NCCH,
                'keyslot_extra': self.reader.extra_keyslot,
                'iv': (self.reader.partition_id << 64 | (0x02 << 56)),
                'keyslot_normal_range': [(0, 0x200)]
            }

            # noinspection PyBroadException
            try:
                # get code compression bit
                decompress = False
                if self.decompress_code and self.reader.check_for_extheader():
                    exh_flag = self.read('/extheader.bin', 1, 0xD, 0)
                    decompress = exh_flag[0] & 1
                exefs_vfp = _c.VirtualFileWrapper(self, '/exefs.bin',
                                                  exefs_region.size)
                exefs_fuse = ExeFSMount(exefs_vfp,
                                        self._g_stat,
                                        decompress_code=decompress,
                                        strict=True)
                self.exefs_fuse = exefs_fuse
            except Exception as e:
                print(f'Failed to mount ExeFS: {type(e).__name__}: {e}')
                self.exefs_fuse = None
            else:
                if not self.reader.flags.no_crypto:
                    for n, ent in self.exefs_fuse.reader.entries.items():
                        if n in {'icon', 'banner'}:
                            self.files['/exefs.bin'][
                                'keyslot_normal_range'].append(
                                    (ent.offset + 0x200, ent.offset + 0x200 +
                                     roundup(ent.size, 0x200)))

        if not self.reader.flags.no_romfs:
            romfs_region = self.reader.romfs_region
            if romfs_region.offset:
                self.files['/romfs.bin'] = {
                    'size': romfs_region.size,
                    'offset': romfs_region.offset,
                    'enctype': 'normal',
                    'keyslot': self.reader.extra_keyslot,
                    'iv': (self.reader.partition_id << 64 | (0x03 << 56))
                }

        if _setup_romfs:
            self.setup_romfs()

        if self.exefs_fuse and '/code.bin' in self.exefs_fuse.files and self.exefs_fuse.decompress_code:
            print('ExeFS: Reading .code...')
            data = self.exefs_fuse.read(
                '/code.bin', self.exefs_fuse.files['/code.bin'].size, 0, 0)
            Thread(target=self.exefs_fuse.init, daemon=True,
                   args=(path, data)).start()

    def setup_romfs(self):
        if '/romfs.bin' in self.files:
            # noinspection PyBroadException
            try:
                romfs_vfp = _c.VirtualFileWrapper(
                    self, '/romfs.bin', self.reader.romfs_region.size)
                # noinspection PyTypeChecker
                romfs_fuse = RomFSMount(romfs_vfp, self._g_stat)
                romfs_fuse.init('/')
                self.romfs_fuse = romfs_fuse
            except Exception as e:
                print(f'Failed to mount RomFS: {type(e).__name__}: {e}')

    def flush(self, path, fh):
        return self.f.flush()

    @_c.ensure_lower_path
    def getattr(self, path, fh=None):
        if path.startswith('/exefs/'):
            return self.exefs_fuse.getattr(_c.remove_first_dir(path), fh)
        elif path.startswith('/romfs/'):
            return self.romfs_fuse.getattr(_c.remove_first_dir(path), fh)
        uid, gid, pid = fuse_get_context()
        if path in {'/', '/romfs', '/exefs'}:
            st = {'st_mode': (S_IFDIR | 0o555), 'st_nlink': 2}
        elif path in self.files:
            st = {
                'st_mode': (S_IFREG | 0o444),
                'st_size': self.files[path]['size'],
                'st_nlink': 1
            }
        else:
            raise FuseOSError(ENOENT)
        return {**st, **self.g_stat, 'st_uid': uid, 'st_gid': gid}

    def open(self, path, flags):
        self.fd += 1
        return self.fd

    @_c.ensure_lower_path
    def readdir(self, path, fh):
        if path.startswith('/exefs'):
            yield from self.exefs_fuse.readdir(_c.remove_first_dir(path), fh)
        elif path.startswith('/romfs'):
            yield from self.romfs_fuse.readdir(_c.remove_first_dir(path), fh)
        elif path == '/':
            yield from ('.', '..')
            yield from (x[1:] for x in self.files)
            if self.exefs_fuse is not None:
                yield 'exefs'
            if self.romfs_fuse is not None:
                yield 'romfs'

    @_c.ensure_lower_path
    def read(self, path, size, offset, fh):
        if path.startswith('/exefs/'):
            return self.exefs_fuse.read(_c.remove_first_dir(path), size,
                                        offset, fh)
        elif path.startswith('/romfs/'):
            return self.romfs_fuse.read(_c.remove_first_dir(path), size,
                                        offset, fh)
        fi = self.files[path]
        real_offset = fi['offset'] + offset
        if fi['offset'] + offset > fi['offset'] + fi['size']:
            return b''
        if offset + size > fi['size']:
            size = fi['size'] - offset

        if fi['enctype'] == 'none' or self.reader.flags.no_crypto:
            # if no encryption, just read and return
            self.f.seek(real_offset)
            data = self.f.read(size)

        elif fi['enctype'] == 'normal':
            self.f.seek(real_offset)
            data = self.f.read(size)
            # thanks Stary2001
            before = offset % 16
            after = (offset + size) % 16
            data = (b'\0' * before) + data + (b'\0' * after)
            iv = fi['iv'] + (offset >> 4)
            data = self.crypto.create_ctr_cipher(
                fi['keyslot'], iv).decrypt(data)[before:size + before]

        elif fi['enctype'] == 'exefs':
            # thanks Stary2001
            before = offset % 0x200
            aligned_real_offset = real_offset - before
            aligned_offset = offset - before
            aligned_size = size + before
            self.f.seek(aligned_real_offset)

            def do_thing(al_offset: int, al_size: int, cut_start: int,
                         cut_end: int):
                end: int = al_offset + (ceil(al_size / 0x200) * 0x200)
                last_chunk_offset = end - 0x200
                # noinspection PyTypeChecker
                for chunk in range(al_offset, end, 0x200):
                    iv = fi['iv'] + (chunk >> 4)
                    keyslot = fi['keyslot_extra']
                    for r in fi['keyslot_normal_range']:
                        if r[0] <= self.f.tell() - fi['offset'] < r[1]:
                            keyslot = fi['keyslot']
                    out = self.crypto.create_ctr_cipher(keyslot, iv).decrypt(
                        self.f.read(0x200))
                    if chunk == al_offset:
                        out = out[cut_start:]
                    if chunk == last_chunk_offset and cut_end != 0x200:
                        out = out[:-cut_end]
                    yield out

            data = b''.join(
                do_thing(aligned_offset, aligned_size, before,
                         0x200 - ((size + before) % 0x200)))

        elif fi['enctype'] == 'fulldec':
            # this could be optimized much better
            before = offset % 0x200
            aligned_real_offset = real_offset - before
            aligned_offset = offset - before
            aligned_size = size + before
            self.f.seek(aligned_real_offset)

            def do_thing(al_offset: int, al_size: int, cut_start: int,
                         cut_end: int):
                end: int = al_offset + (ceil(al_size / 0x200) * 0x200)
                # dict is ordered by default in CPython since 3.6.0
                # and part of the language spec since 3.7.0
                to_read: Dict[str, List[int]] = {}

                if self.reader.check_for_extheader():
                    extheader_start = 0x200
                    extheader_end = 0xA00
                else:
                    extheader_start = extheader_end = 0

                logo = self.reader.logo_region
                logo_start = logo.offset
                logo_end = logo_start + logo.size

                plain = self.reader.plain_region
                plain_start = plain.offset
                plain_end = plain_start + plain.size

                exefs = self.reader.exefs_region
                exefs_start = exefs.offset
                exefs_end = exefs_start + exefs.size

                romfs = self.reader.romfs_region
                romfs_start = romfs.offset
                romfs_end = romfs_start + romfs.size

                for chunk_offset in range(al_offset, end, 0x200):
                    # RomFS check first, since it might be faster
                    if romfs_start <= chunk_offset < romfs_end:
                        name = '/romfs.bin'
                        curr_offset = romfs_start
                    # ExeFS check second, since it might be faster
                    elif exefs_start <= chunk_offset < exefs_end:
                        name = '/exefs.bin'
                        curr_offset = exefs_start
                    # NCCH check, always 0x0 to 0x200
                    elif 0 <= chunk_offset < 0x200:
                        name = '/ncch.bin'
                        curr_offset = 0
                    elif extheader_start <= chunk_offset < extheader_end:
                        name = '/extheader.bin'
                        curr_offset = extheader_start
                    elif logo_start <= chunk_offset < logo_end:
                        name = '/logo.bin'
                        curr_offset = logo_start
                    elif plain_start <= chunk_offset < plain_end:
                        name = '/plain.bin'
                        curr_offset = plain_start
                    else:
                        name = f'raw{chunk_offset}'
                        curr_offset = 0
                    if name not in to_read:
                        to_read[name] = [chunk_offset - curr_offset, 0]
                    to_read[name][1] += 0x200
                    last_name = name

                is_start = True
                for name, info in to_read.items():
                    try:
                        new_data = self.read(name, info[1], info[0], 0)
                        if name == '/ncch.bin':
                            # fix crypto flags
                            ncch_array = bytearray(new_data)
                            ncch_array[0x18B] = 0
                            ncch_array[0x18F] = 4
                            new_data = bytes(ncch_array)
                    except KeyError:
                        # for unknown files
                        self.f.seek(info[0])
                        new_data = self.f.read(info[1])
                    if is_start is True:
                        new_data = new_data[cut_start:]
                        is_start = False
                    # noinspection PyUnboundLocalVariable
                    if name == last_name and cut_end != 0x200:
                        new_data = new_data[:-cut_end]

                    yield new_data

            data = b''.join(
                do_thing(aligned_offset, aligned_size, before,
                         0x200 - ((size + before) % 0x200)))

        else:
            from pprint import pformat
            print(
                '--------------------------------------------------',
                'Warning: unknown file type (this should not happen!)',
                'Please file an issue or contact the developer with the details below.',
                '  https://github.com/ihaveamac/ninfs/issues',
                '--------------------------------------------------',
                f'{path!r}: {pformat(fi)!r}',
                sep='\n')

            data = b'g' * size

        return data

    @_c.ensure_lower_path
    def statfs(self, path):
        if path.startswith('/exefs/'):
            return self.exefs_fuse.statfs(_c.remove_first_dir(path))
        elif path.startswith('/romfs/'):
            return self.romfs_fuse.statfs(_c.remove_first_dir(path))
        else:
            return {
                'f_bsize': 4096,
                'f_blocks': self.reader.content_size // 4096,
                'f_bavail': 0,
                'f_bfree': 0,
                'f_files': len(self.files)
            }
Exemplo n.º 2
0
class CDNContentsMount(LoggingMixIn, Operations):
    fd = 0

    # get the real path by returning self.cdn_dir + path
    def rp(self, path):
        return os.path.join(self.cdn_dir, path)

    def __init__(self, tmd_file: str = None, cdn_dir: str = None, dec_key: str = None, dev: bool = False,
                 seeddb: str = None, boot9: str = None):
        if tmd_file:
            self.cdn_dir = os.path.dirname(tmd_file)
        else:
            self.cdn_dir = cdn_dir
            tmd_file = os.path.join(cdn_dir, 'tmd')

        self.crypto = CryptoEngine(boot9=boot9, dev=dev)

        self.cdn_content_size = 0
        self.dev = dev
        self.seeddb = seeddb

        # get status change, modify, and file access times
        try:
            self.g_stat = get_time(tmd_file)
        except FileNotFoundError:
            exit('Could not find "tmd" in directory')

        self.tmd = TitleMetadataReader.from_file(tmd_file)

        # noinspection PyUnboundLocalVariable
        self.title_id = self.tmd.title_id

        if not os.path.isfile(self.rp('cetk')):
            if not dec_key:
                exit('cetk not found. Provide the ticket or decrypted titlekey with --dec-key.')

        if dec_key:
            try:
                titlekey = bytes.fromhex(dec_key)
                if len(titlekey) != 16:
                    exit('--dec-key input is not 32 hex characters.')
            except ValueError:
                exit('Failed to convert --dec-key input to bytes. Non-hex character likely found, or is not '
                     '32 hex characters.')
            # noinspection PyUnboundLocalVariable
            self.crypto.set_normal_key(Keyslot.DecryptedTitlekey, titlekey)
        else:
            with open(self.rp('cetk'), 'rb') as tik:
                # load ticket
                self.crypto.load_from_ticket(tik.read(0x350))

        # create virtual files
        self.files = {'/ticket.bin': {'size': 0x350, 'type': 'raw', 'real_filepath': self.rp('cetk')},
                      '/tmd.bin': {'size': 0xB04 + self.tmd.content_count * CHUNK_RECORD_SIZE, 'offset': 0,
                                   'type': 'raw', 'real_filepath': self.rp('tmd')},
                      '/tmdchunks.bin': {'size': self.tmd.content_count * CHUNK_RECORD_SIZE, 'offset': 0xB04,
                                         'type': 'raw', 'real_filepath': self.rp('tmd')}}

        self.dirs: Dict[str, NCCHContainerMount] = {}

    def init(self, path):
        # read contents to generate virtual files
        for chunk in self.tmd.chunk_records:
            if os.path.isfile(self.rp(chunk.id)):
                real_filename = chunk.id
            elif os.path.isfile(self.rp(chunk.id.upper())):
                real_filename = chunk.id.upper()
            else:
                print(f'Content {chunk.cindex:04}:{chunk.id} not found, will not be included.')
                continue
            f_stat = os.stat(self.rp(real_filename))
            if chunk.size != f_stat.st_size:
                print('Warning: TMD Content size and filesize of', chunk.id, 'are different.')
            self.cdn_content_size += chunk.size
            is_srl = chunk.cindex == 0 and self.title_id[3:5] == '48'
            file_ext = 'nds' if is_srl else 'ncch'
            filename = f'/{chunk.cindex:04x}.{chunk.id}.{file_ext}'
            self.files[filename] = {'size': chunk.size, 'index': chunk.cindex.to_bytes(2, 'big'),
                                    'type': 'enc' if chunk.type.encrypted else 'raw',
                                    'real_filepath': self.rp(real_filename)}

            dirname = f'/{chunk.cindex:04x}.{chunk.id}'
            # noinspection PyBroadException
            try:
                f_time = get_time(f_stat)
                content_vfp = _c.VirtualFileWrapper(self, filename, chunk.size)
                # boot9 is not passed here, as CryptoEngine has already set up the keys at the beginning.
                if is_srl:
                    content_fuse = SRLMount(content_vfp, g_stat=f_time)
                else:
                    content_reader = NCCHReader(content_vfp, dev=self.dev, seeddb=self.seeddb)
                    content_fuse = NCCHContainerMount(content_reader, g_stat=f_time)
                content_fuse.init(path)
                self.dirs[dirname] = content_fuse
            except Exception as e:
                print(f'Failed to mount {filename}: {type(e).__name__}: {e}')

    @_c.ensure_lower_path
    def getattr(self, path, fh=None):
        first_dir = _c.get_first_dir(path)
        if first_dir in self.dirs:
            return self.dirs[first_dir].getattr(_c.remove_first_dir(path), fh)
        uid, gid, pid = fuse_get_context()
        if path == '/' or path in self.dirs:
            st = {'st_mode': (S_IFDIR | 0o555), 'st_nlink': 2}
        elif path in self.files:
            st = {'st_mode': (S_IFREG | 0o444), 'st_size': self.files[path]['size'], 'st_nlink': 1}
        else:
            raise FuseOSError(ENOENT)
        return {**st, **self.g_stat, 'st_uid': uid, 'st_gid': gid}

    def open(self, path, flags):
        self.fd += 1
        return self.fd

    @_c.ensure_lower_path
    def readdir(self, path, fh):
        first_dir = _c.get_first_dir(path)
        if first_dir in self.dirs:
            yield from self.dirs[first_dir].readdir(_c.remove_first_dir(path), fh)
        else:
            yield from ('.', '..')
            yield from (x[1:] for x in self.files)
            yield from (x[1:] for x in self.dirs)

    @_c.ensure_lower_path
    def read(self, path, size, offset, fh):
        first_dir = _c.get_first_dir(path)
        if first_dir in self.dirs:
            return self.dirs[first_dir].read(_c.remove_first_dir(path), size, offset, fh)
        fi = self.files[path]
        real_size = size
        with open(fi['real_filepath'], 'rb') as f:
            if fi['type'] == 'raw':
                # if raw, just read and return
                f.seek(offset)
                data = f.read(size)

            elif fi['type'] == 'enc':
                # if encrypted, the block needs to be decrypted first
                # CBC requires a full block (0x10 in this case). and the previous
                #   block is used as the IV. so that's quite a bit to read if the
                #   application requires just a few bytes.
                # thanks Stary2001
                before = offset % 16
                if size % 16 != 0:
                    size = size + 16 - size % 16
                if offset - before == 0:
                    # use the initial value if reading from the first block
                    iv = fi['index'] + (b'\0' * 14)
                else:
                    # use the previous block if reading anywhere else
                    f.seek(offset - before - 0x10)
                    iv = f.read(0x10)
                # read to block size
                f.seek(offset - before)
                # adding 0x10 to the size fixes some kind of decryption bug
                data = self.crypto.create_cbc_cipher(Keyslot.DecryptedTitlekey,
                                                     iv).decrypt(f.read(size + 0x10))[before:real_size + before]

            else:
                from pprint import pformat
                print('--------------------------------------------------',
                      'Warning: unknown file type (this should not happen!)',
                      'Please file an issue or contact the developer with the details below.',
                      '  https://github.com/ihaveamac/ninfs/issues',
                      '--------------------------------------------------',
                      f'{path!r}: {pformat(fi)!r}', sep='\n')

                data = b'g' * size

            return data

    @_c.ensure_lower_path
    def statfs(self, path):
        first_dir = _c.get_first_dir(path)
        if first_dir in self.dirs:
            return self.dirs[first_dir].read(_c.remove_first_dir(path))
        return {'f_bsize': 4096, 'f_blocks': self.cdn_content_size // 4096, 'f_bavail': 0, 'f_bfree': 0,
                'f_files': len(self.files)}
Exemplo n.º 3
0
class NCCHContainerMount(LoggingMixIn, Operations):
    fd = 0
    _exefs_mounted = False
    _romfs_mounted = False
    romfs_fuse = None
    exefs_fuse = None

    def __init__(self, ncch_fp: BinaryIO, g_stat: os.stat_result, decompress_code: bool = True, dev: bool = False,
                 seeddb: str = None):
        self.crypto = CryptoEngine(dev=dev)

        self.decompress_code = decompress_code
        self.seeddb = seeddb
        self.files: Dict[str, Dict] = {}

        # get status change, modify, and file access times
        self._g_stat = g_stat
        self.g_stat = {'st_ctime': int(g_stat.st_ctime), 'st_mtime': int(g_stat.st_mtime),
                       'st_atime': int(g_stat.st_atime)}

        ncch_header = ncch_fp.read(0x200)
        self.reader = NCCHReader.from_header(ncch_header)

        self.f = ncch_fp

        if not self.reader.flags.no_crypto:
            # I should figure out what happens if fixed-key crypto is
            #   used along with seed. even though this will never
            #   happen in practice, I would still like to see what
            #   happens if it happens.
            if self.reader.flags.fixed_crypto_key:
                normal_key = FIXED_SYSTEM_KEY if self.reader.program_id & (0x10 << 32) else 0x0
                self.crypto.set_normal_key(0x2C, normal_key.to_bytes(0x10, 'big'))
            else:
                if self.reader.flags.uses_seed:
                    self.reader.load_seed_from_seeddb()

                self.crypto.set_keyslot('y', 0x2C, readbe(self.reader.get_key_y(original=True)))
                self.crypto.set_keyslot('y', self.reader.extra_keyslot,
                                        readbe(self.reader.get_key_y()))

    def __del__(self, *args):
        try:
            self.f.close()
        except AttributeError:
            pass

    destroy = __del__

    def init(self, path):
        decrypted_filename = '/decrypted.' + ('cxi' if self.reader.flags.executable else 'cfa')

        self.files[decrypted_filename] = {'size': self.reader.content_size, 'offset': 0, 'enctype': 'fulldec'}
        self.files['/ncch.bin'] = {'size': 0x200, 'offset': 0, 'enctype': 'none'}

        if self.reader.check_for_extheader():
            self.files['/extheader.bin'] = {'size': 0x800, 'offset': 0x200, 'enctype': 'normal',
                                            'keyslot': 0x2C, 'iv': (self.reader.partition_id << 64 | (0x01 << 56))}

        plain_region = self.reader.plain_region
        if plain_region.offset:
            self.files['/plain.bin'] = {'size': plain_region.size, 'offset': plain_region.offset, 'enctype': 'none'}

        logo_region = self.reader.logo_region
        if logo_region.offset:
            self.files['/logo.bin'] = {'size': logo_region.size, 'offset': logo_region.offset, 'enctype': 'none'}

        exefs_region = self.reader.exefs_region
        if exefs_region.offset:
            self.files['/exefs.bin'] = {'size': exefs_region.size, 'offset': exefs_region.offset, 'enctype': 'exefs',
                                        'keyslot': 0x2C, 'keyslot_extra': self.reader.extra_keyslot,
                                        'iv': (self.reader.partition_id << 64 | (0x02 << 56)),
                                        'keyslot_normal_range': [(0, 0x200)]}

            # noinspection PyBroadException
            try:
                # get code compression bit
                decompress = False
                if self.decompress_code and self.reader.check_for_extheader():
                    exh_flag = self.read('/extheader.bin', 1, 0xD, 0)
                    decompress = exh_flag[0] & 1
                exefs_vfp = _c.VirtualFileWrapper(self, '/exefs.bin', exefs_region.size)
                exefs_fuse = ExeFSMount(exefs_vfp, self._g_stat, decompress_code=decompress, strict=True)
                exefs_fuse.init(path)
                self.exefs_fuse = exefs_fuse
            except Exception as e:
                print(f'Failed to mount ExeFS: {type(e).__name__}: {e}')
            else:
                if not self.reader.flags.no_crypto:
                    for n, ent in self.exefs_fuse.reader.entries.items():
                        if n in {'icon', 'banner'}:
                            self.files['/exefs.bin']['keyslot_normal_range'].append(
                                (ent.offset + 0x200, ent.offset + 0x200 + roundup(ent.size, 0x200)))

        if not self.reader.flags.no_romfs:
            romfs_region = self.reader.romfs_region
            if romfs_region.offset:
                self.files['/romfs.bin'] = {'size': romfs_region.size, 'offset': romfs_region.offset,
                                            'enctype': 'normal', 'keyslot': self.reader.extra_keyslot,
                                            'iv': (self.reader.partition_id << 64 | (0x03 << 56))}

                # noinspection PyBroadException
                try:
                    romfs_vfp = _c.VirtualFileWrapper(self, '/romfs.bin', romfs_region.size)
                    romfs_fuse = RomFSMount(romfs_vfp, self._g_stat)
                    romfs_fuse.init(path)
                    self.romfs_fuse = romfs_fuse
                except Exception as e:
                    print(f'Failed to mount RomFS: {type(e).__name__}: {e}')

    def flush(self, path, fh):
        return self.f.flush()

    @_c.ensure_lower_path
    def getattr(self, path, fh=None):
        if path.startswith('/exefs/'):
            return self.exefs_fuse.getattr(_c.remove_first_dir(path), fh)
        elif path.startswith('/romfs/'):
            return self.romfs_fuse.getattr(_c.remove_first_dir(path), fh)
        uid, gid, pid = fuse_get_context()
        if path in {'/', '/romfs', '/exefs'}:
            st = {'st_mode': (S_IFDIR | 0o555), 'st_nlink': 2}
        elif path in self.files:
            st = {'st_mode': (S_IFREG | 0o444), 'st_size': self.files[path]['size'], 'st_nlink': 1}
        else:
            raise FuseOSError(ENOENT)
        return {**st, **self.g_stat, 'st_uid': uid, 'st_gid': gid}

    def open(self, path, flags):
        self.fd += 1
        return self.fd

    @_c.ensure_lower_path
    def readdir(self, path, fh):
        if path.startswith('/exefs'):
            yield from self.exefs_fuse.readdir(_c.remove_first_dir(path), fh)
        elif path.startswith('/romfs'):
            yield from self.romfs_fuse.readdir(_c.remove_first_dir(path), fh)
        elif path == '/':
            yield from ('.', '..')
            yield from (x[1:] for x in self.files)
            if self.exefs_fuse is not None:
                yield 'exefs'
            if self.romfs_fuse is not None:
                yield 'romfs'

    @_c.ensure_lower_path
    def read(self, path, size, offset, fh):
        if path.startswith('/exefs/'):
            return self.exefs_fuse.read(_c.remove_first_dir(path), size, offset, fh)
        elif path.startswith('/romfs/'):
            return self.romfs_fuse.read(_c.remove_first_dir(path), size, offset, fh)
        fi = self.files[path]
        real_offset = fi['offset'] + offset
        if fi['offset'] + offset > fi['offset'] + fi['size']:
            return b''
        if offset + size > fi['size']:
            size = fi['size'] - offset

        if fi['enctype'] == 'none' or self.reader.flags.no_crypto:
            # if no encryption, just read and return
            self.f.seek(real_offset)
            data = self.f.read(size)

        elif fi['enctype'] == 'normal':
            self.f.seek(real_offset)
            data = self.f.read(size)
            # thanks Stary2001
            before = offset % 16
            after = (offset + size) % 16
            data = (b'\0' * before) + data + (b'\0' * after)
            iv = fi['iv'] + (offset >> 4)
            data = self.crypto.create_ctr_cipher(fi['keyslot'], iv).decrypt(data)[before:size + before]

        elif fi['enctype'] == 'exefs':
            # thanks Stary2001
            before = offset % 0x200
            aligned_real_offset = real_offset - before
            aligned_offset = offset - before
            aligned_size = size + before
            self.f.seek(aligned_real_offset)
            data = b''
            # noinspection PyTypeChecker
            for chunk in range(ceil(aligned_size / 0x200)):
                iv = fi['iv'] + ((aligned_offset + (chunk * 0x200)) >> 4)
                keyslot = fi['keyslot_extra']
                for r in fi['keyslot_normal_range']:
                    if r[0] <= self.f.tell() - fi['offset'] < r[1]:
                        keyslot = fi['keyslot']
                data += self.crypto.create_ctr_cipher(keyslot, iv).decrypt(self.f.read(0x200))

            data = data[before:size + before]

        elif fi['enctype'] == 'fulldec':
            # this could be optimized much better
            before = offset % 0x200
            aligned_real_offset = real_offset - before
            aligned_offset = offset - before
            aligned_size = size + before
            self.f.seek(aligned_real_offset)
            data = b''
            files_to_read = OrderedDict()
            # noinspection PyTypeChecker
            for chunk in range(ceil(aligned_size / 0x200)):
                new_offset = (aligned_offset + (chunk * 0x200))
                added = False
                for fname, attrs in self.files.items():
                    if attrs['enctype'] == 'fulldec':
                        continue
                    if attrs['offset'] <= new_offset < attrs['offset'] + attrs['size']:
                        if fname not in files_to_read:
                            files_to_read[fname] = [new_offset - attrs['offset'], 0]
                        files_to_read[fname][1] += 0x200
                        added = True
                if not added:
                    files_to_read[f'raw{chunk}'] = [new_offset, 0x200]

            for fname, info in files_to_read.items():
                try:
                    new_data = self.read(fname, info[1], info[0], 0)
                    if fname == '/ncch.bin':
                        # fix crypto flags
                        ncch_array = bytearray(new_data)
                        ncch_array[0x18B] = 0
                        ncch_array[0x18F] = 4
                        new_data = bytes(ncch_array)
                except KeyError:
                    # for unknown files
                    self.f.seek(info[0])
                    new_data = self.f.read(info[1])

                data += new_data

            data = data[before:size + before]

        else:
            from pprint import pformat
            print('--------------------------------------------------',
                  'Warning: unknown file type (this should not happen!)',
                  'Please file an issue or contact the developer with the details below.',
                  '  https://github.com/ihaveamac/fuse-3ds/issues',
                  '--------------------------------------------------',
                  f'{path!r}: {pformat(fi)!r}', sep='\n')

            data = b'g' * size

        return data

    @_c.ensure_lower_path
    def statfs(self, path):
        if path.startswith('/exefs/'):
            return self.exefs_fuse.statfs(_c.remove_first_dir(path))
        elif path.startswith('/romfs/'):
            return self.romfs_fuse.statfs(_c.remove_first_dir(path))
        else:
            return {'f_bsize': 4096, 'f_blocks': self.reader.content_size // 4096, 'f_bavail': 0, 'f_bfree': 0,
                    'f_files': len(self.files)}