Files
custom-install/pyctr/type/cci.py
2019-11-12 00:29:11 -08:00

146 lines
4.6 KiB
Python

# This file is a part of ninfs.
#
# Copyright (c) 2017-2019 Ian Burgwin
# This file is licensed under The MIT License (MIT).
# You can find the full license text in LICENSE.md in the root of this project.
from enum import IntEnum
from threading import Lock
from typing import TYPE_CHECKING, NamedTuple
from ..common import PyCTRError
from ..fileio import SubsectionIO
from ..type.ncch import NCCHReader
from ..util import readle
if TYPE_CHECKING:
from typing import BinaryIO, Dict, Union
CCI_MEDIA_UNIT = 0x200
class CCIError(PyCTRError):
"""Generic error for CCI operations."""
class InvalidCCIError(CCIError):
"""Invalid CCI header exception."""
class CCISection(IntEnum):
Header = -3
CardInfo = -2
DevInfo = -1
Application = 0
Manual = 1
DownloadPlayChild = 2
Unk3 = 3
Unk4 = 4
Unk5 = 5
UpdateOld3DS = 6
UpdateNew3DS = 7
class CCIRegion(NamedTuple):
section: 'Union[int, CCISection]'
offset: int
size: int
class CCIReader:
"""Class for the 3DS CCI container."""
closed = False
def __init__(self, fp: 'Union[str, BinaryIO]', *, case_insensitive: bool = True, dev: bool = False,
load_contents: bool = True, assume_decrypted: bool = False):
if isinstance(fp, str):
fp = open(fp, 'rb')
# store the starting offset so the CCI can be read from any point in the base file
self._start = fp.tell()
self._fp = fp
# store case-insensitivity for RomFSReader
self._case_insensitive = case_insensitive
# threading lock
self._lock = Lock()
# ignore the signature, we don't need it
self._fp.seek(0x100, 1)
header = fp.read(0x100)
if header[0:4] != b'NCSD':
raise InvalidCCIError('NCSD magic not found')
# make sure the Media ID is not 00, which is used for the NAND header
self.media_id = header[0x8:0x10][::-1].hex()
if self.media_id == '00' * 8:
raise InvalidCCIError('Media ID is ' + self.media_id)
self.image_size = readle(header[4:8]) * CCI_MEDIA_UNIT
# this contains the location of each section
self.sections: Dict[CCISection, CCIRegion] = {}
# this contains loaded sections
self.contents: Dict[CCISection, NCCHReader] = {}
def add_region(section: 'CCISection', offset: int, size: int):
region = CCIRegion(section=section, offset=offset, size=size)
self.sections[section] = region
# add each part of the header
add_region(CCISection.Header, 0, 0x200)
add_region(CCISection.CardInfo, 0x200, 0x1000)
add_region(CCISection.DevInfo, 0x1200, 0x300)
# use a CCISection value for section keys
partition_sections = [x for x in CCISection if x >= 0]
part_raw = header[0x20:0x60]
# the first content always starts at 0x4000 but this code makes no assumptions about it
for idx, info_offset in enumerate(range(0, 0x40, 0x8)):
part_info = part_raw[info_offset:info_offset + 8]
part_offset = int.from_bytes(part_info[0:4], 'little') * CCI_MEDIA_UNIT
part_size = int.from_bytes(part_info[4:8], 'little') * CCI_MEDIA_UNIT
if part_offset:
section_id = partition_sections[idx]
add_region(section_id, part_offset, part_size)
if load_contents:
content_fp = self.open_raw_section(section_id)
self.contents[section_id] = NCCHReader(content_fp, case_insensitive=case_insensitive, dev=dev,
assume_decrypted=assume_decrypted)
def close(self):
self.closed = True
try:
self._fp.close()
except AttributeError:
pass
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
__del__ = close
def __repr__(self):
info = [('media_id', self.media_id)]
try:
info.append(('title_name',
repr(self.contents[CCISection.Application].exefs.icon.get_app_title().short_desc)))
except KeyError:
info.append(('title_name', 'unknown'))
info.append(('partition_count', len(self.contents)))
info_final = " ".join(x + ": " + str(y) for x, y in info)
return f'<{type(self).__name__} {info_final}>'
def open_raw_section(self, section: 'CCISection'):
"""Open a raw CCI section for reading."""
region = self.sections[section]
return SubsectionIO(self._fp, self._start + region.offset, region.size)