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) }
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)}
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)}