Browse Source

add sdtitle mount (closes #83)

2.0
Ian Burgwin 1 year ago
parent
commit
36174f6481
Signed by: ianburgwin
GPG Key ID: 90725113CA578EAA
  1. 1
      README.md
  2. 1
      ninfs/gui/setupwizard/__init__.py
  3. 73
      ninfs/gui/setupwizard/sdtitle.py
  4. 1
      ninfs/gui/wizardcontainer.py
  5. 194
      ninfs/mount/sdtitle.py
  6. 6
      ninfs/mountinfo.py

1
README.md

@ -15,6 +15,7 @@ Windows, macOS, and Linux are supported. @@ -15,6 +15,7 @@ Windows, macOS, and Linux are supported.
* NCCH (".cxi", ".cfa", ".ncch", ".app")
* Read-only Filesystem (".romfs", "romfs.bin")
* SD Card Contents ("Nintendo 3DS" from SD)
* Installed SD Title Contents ("\*.tmd" and "\*.app" files)
* 3DSX Homebrew (".3dsx")
* Nintendo DS / DSi
* Nintendo DSi NAND backup ("nand\_dsi.bin")

1
ninfs/gui/setupwizard/__init__.py

@ -17,5 +17,6 @@ from .nandbb import BBNandImageSetup @@ -17,5 +17,6 @@ from .nandbb import BBNandImageSetup
from .ncch import NCCHSetup
from .romfs import RomFSSetup
from .sd import SDFilesystemSetup
from .sdtitle import SDTitleSetup
from .srl import SRLSetup
from .threedsx import ThreeDSXSetup

73
ninfs/gui/setupwizard/sdtitle.py

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
# This file is a part of ninfs.
#
# Copyright (c) 2017-2021 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.
import tkinter as tk
from typing import TYPE_CHECKING
from .base import WizardBase
from .. import supportfiles
if TYPE_CHECKING:
from .. import WizardContainer
class SDTitleSetup(WizardBase):
def __init__(self, parent: 'tk.BaseWidget' = None, *, wizardcontainer: 'WizardContainer'):
super().__init__(parent, wizardcontainer=wizardcontainer)
def callback(*_):
main_file = self.main_textbox_var.get().strip()
b9_file = self.b9_textbox_var.get().strip()
self.wizardcontainer.set_next_enabled(main_file and b9_file)
main_container, main_textbox, main_textbox_var = self.make_file_picker('Select the SD Title TMD file:',
'Select SD Title TMD file')
main_container.pack(fill=tk.X, expand=True)
b9_container, b9_textbox, b9_textbox_var = self.make_file_picker('Select the boot9 file:', 'Select boot9 file')
b9_container.pack(fill=tk.X, expand=True)
seeddb_container, seeddb_textbox, seeddb_textbox_var = self.make_file_picker('Select the seeddb file:',
'Select seeddb file')
seeddb_container.pack(fill=tk.X, expand=True)
seed_container, seed_textbox, seed_textbox_var = self.make_entry('OR Enter the seed '
'(optional if seeddb is used):')
seed_container.pack(fill=tk.X, expand=True)
self.main_textbox_var = main_textbox_var
self.b9_textbox_var = b9_textbox_var
self.seeddb_textbox_var = seeddb_textbox_var
self.seed_textbox_var = seed_textbox_var
main_textbox_var.trace_add('write', callback)
b9_textbox_var.trace_add('write', callback)
b9_textbox_var.set(supportfiles.last_b9_file)
seeddb_textbox_var.set(supportfiles.last_seeddb_file)
self.set_header_suffix('SD Title')
def next_pressed(self):
main_file = self.main_textbox_var.get().strip()
b9_file = self.b9_textbox_var.get().strip()
seeddb_file = self.seeddb_textbox_var.get().strip()
seed = self.seed_textbox_var.get().replace(' ', '')
if b9_file:
supportfiles.last_b9_file = b9_file
if seeddb_file:
supportfiles.last_seeddb_file = seeddb_file
args = ['sdtitle', main_file]
if b9_file:
args += ['--boot9', b9_file]
if seed:
args += ['--seed', seed]
elif seeddb_file:
args += ['--seeddb', seeddb_file]
self.wizardcontainer.show_mount_point_selector('SD Title', args)

1
ninfs/gui/wizardcontainer.py

@ -33,6 +33,7 @@ wizard_bases = { @@ -33,6 +33,7 @@ wizard_bases = {
'ncch': NCCHSetup,
'romfs': RomFSSetup,
'sd': SDFilesystemSetup,
'sdtitle': SDTitleSetup,
'srl': SRLSetup,
'threedsx': ThreeDSXSetup,
}

194
ninfs/mount/sdtitle.py

@ -0,0 +1,194 @@ @@ -0,0 +1,194 @@
# This file is a part of ninfs.
#
# Copyright (c) 2017-2021 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.
"""
Mounts installed SD title contents, creating a virtual filesystem of decrypted contents (if encrypted).
"""
import logging
from errno import ENOENT
from glob import glob
from os.path import isfile, join
from stat import S_IFDIR, S_IFREG
from sys import argv
from typing import TYPE_CHECKING
from pyctr.crypto import load_seeddb
from pyctr.type.sdtitle import SDTitleReader, SDTitleSection
from . import _common as _c
# _common imports these from fusepy, and prints an error if it fails; this allows less duplicated code
from ._common import FUSE, FuseOSError, Operations, LoggingMixIn, fuse_get_context, get_time, load_custom_boot9, \
realpath
from .ncch import NCCHContainerMount
from .srl import SRLMount
if TYPE_CHECKING:
from os import DirEntry
from typing import Dict, Tuple, Union
class SDTitleContentsMount(LoggingMixIn, Operations):
fd = 0
total_size = 0
def __init__(self, reader: 'SDTitleReader', g_stat: dict):
self.dirs: Dict[str, Union[NCCHContainerMount, SRLMount]] = {}
self.files: Dict[str, Tuple[Union[int, SDTitleSection], int, int]] = {}
# get status change, modify, and file access times
self.g_stat = g_stat
self.reader = reader
def __del__(self, *args):
try:
self.reader.close()
except AttributeError:
pass
destroy = __del__
def init(self, path):
def add_file(name: str, section: 'Union[SDTitleSection, int]', added_offset: int = 0):
# added offset is used for a few things like meta icon and tmdchunks
if section >= 0:
size = self.reader.content_info[section].size
else:
with self.reader.open_raw_section(section) as f:
size = f.seek(0, 2)
self.files[name] = (section, added_offset, size - added_offset)
add_file('/tmd.bin', SDTitleSection.TitleMetadata)
add_file('/tmdchunks.bin', SDTitleSection.TitleMetadata, 0xB04)
for record in self.reader.content_info:
dirname = f'/{record.cindex:04x}.{record.id}'
is_srl = record.cindex == 0 and self.reader.tmd.title_id[3:5] == '48'
file_ext = 'nds' if is_srl else 'ncch'
filename = f'{dirname}.{file_ext}'
add_file(filename, record.cindex)
try:
if is_srl:
# special case for SRL contents
srl_fp = self.reader.open_raw_section(record.cindex)
self.dirs[dirname] = SRLMount(srl_fp, g_stat=self.g_stat)
else:
mount = NCCHContainerMount(self.reader.contents[record.cindex], g_stat=self.g_stat)
mount.init(path)
self.dirs[dirname] = mount
except Exception as e:
print(f'Failed to mount {filename}: {type(e).__name__}: {e}')
self.total_size += record.size
@_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][2], '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)
section = self.files[path]
with self.reader.open_raw_section(section[0]) as f:
f.seek(offset + section[1])
return f.read(size)
@_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].statfs(_c.remove_first_dir(path))
return {'f_bsize': 4096, 'f_frsize': 4096, 'f_blocks': self.total_size // 4096, 'f_bavail': 0, 'f_bfree': 0,
'f_files': len(self.files)}
def main(prog: str = None, args: list = None):
from argparse import ArgumentParser
if args is None:
args = argv[1:]
parser = ArgumentParser(prog=prog, description='Mount Nintendo 3DS installed SD title contents.',
parents=(_c.default_argp, _c.ctrcrypto_argp, _c.seeddb_argp,
_c.main_args('content', 'tmd file or directory with SD title contents')))
a = parser.parse_args(args)
opts = dict(_c.parse_fuse_opts(a.o))
if a.do:
logging.basicConfig(level=logging.DEBUG, filename=a.do)
if isfile(a.content):
tmd_file = a.content
else:
tmd_file = None
tmds = glob(join(a.content, '*.tmd'))
if tmds:
# if there end up being multiple, this should use the first one that's found, which is probably the
# active one used by the system right now
tmd_file = tmds[0]
else:
exit(f'Could not find a tmd in {a.content}')
sdtitle_stat = get_time(a.content)
load_custom_boot9(a.boot9)
if a.seeddb:
load_seeddb(a.seeddb)
with SDTitleReader(tmd_file, dev=a.dev, seed=a.seed, case_insensitive=True) as r:
mount = SDTitleContentsMount(reader=r, g_stat=sdtitle_stat)
if _c.macos or _c.windows:
opts['fstypename'] = 'SDT'
if _c.macos:
display = r.tmd.title_id.upper()
try:
title = r.contents[0].exefs.icon.get_app_title()
display += f'; ' + r.contents[0].product_code
if title.short_desc != 'unknown':
display += '; ' + title.short_desc
except:
pass
opts['volname'] = f'SD Title Contents ({display})'
elif _c.windows:
# volume label can only be up to 32 chars
try:
title = r.contents[0].exefs.icon.get_app_title().short_desc
if len(title) > 21:
title = title[0:20] + '\u2026' # ellipsis
display = title
except:
display = r.tmd.title_id.upper()
opts['volname'] = f'SD Title ({display})'
FUSE(mount, a.mount_point, foreground=a.fg or a.d, ro=True, nothreads=True, debug=a.d,
fsname=realpath(tmd_file).replace(',', '_'), **opts)

6
ninfs/mountinfo.py

@ -49,6 +49,10 @@ types = { @@ -49,6 +49,10 @@ types = {
'name': 'SD Card Contents',
'info': '"Nintendo 3DS" from SD'
},
'sdtitle': {
'name': 'Installed SD Title Contents',
'info': '"*.tmd" and "*.app" files'
},
'srl': {
'name': 'Nintendo DS ROM image',
'info': '".nds", ".srl"'
@ -75,7 +79,7 @@ aliases = { @@ -75,7 +79,7 @@ aliases = {
}
categories = {
'Nintendo 3DS': ['cci', 'cdn', 'cia', 'exefs', 'nandctr', 'ncch', 'romfs', 'sd', 'threedsx'],
'Nintendo 3DS': ['cci', 'cdn', 'cia', 'exefs', 'nandctr', 'ncch', 'romfs', 'sd', 'sdtitle', 'threedsx'],
'Nintendo DS / DSi': ['nandtwl', 'srl'],
'Nintendo Switch': ['nandhac'],
'iQue Player': ['nandbb']

Loading…
Cancel
Save