mirror of
https://github.com/ihaveamac/custom-install.git
synced 2025-12-06 06:41:45 +00:00
146 lines
4.6 KiB
Python
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)
|