10 Commits

Author SHA1 Message Date
Ian Burgwin
794eb8750f custominstall: fix incorrect return value causing TypeError in ci-gui 2020-10-14 17:37:05 -07:00
Ian Burgwin
b34bba2543 ci-gui: print command line args if save3ds_fuse fails 2020-09-03 19:45:11 -07:00
Ian Burgwin
40cfd955cc custominstall: set platform to win32 if it's msys (close #2) 2020-07-28 09:32:18 -07:00
Ian Burgwin
bbcfb6fef1 ci-gui: support adding CDN folders 2020-07-28 06:33:18 -07:00
Ian Burgwin
1e3e15c969 custominstall: support CDN contents (close #27) 2020-07-28 06:21:22 -07:00
Ian Burgwin
48f92579ce custominstall: use bytes(cia.tmd) instead of reading tmd file directly 2020-07-28 06:15:27 -07:00
Ian Burgwin
06f70e37dc requirements: bump pyctr to 0.4.3 2020-07-28 06:01:13 -07:00
Ian Burgwin
44787ebc87 requirements: bump pyctr to 0.4.2 2020-07-28 04:51:03 -07:00
Ian Burgwin
399bb97238 custominstall: read all CIAs before installing
Prevents the issue of one having a corrupt header or something causing an issue in the middle of writing.
2020-07-28 02:33:08 -07:00
Ian Burgwin
6da2ed3343 ci-gui: use abspath when getting file parent
This should the initial directory setting for file dialogs work more reliably.
2020-07-28 01:29:59 -07:00
3 changed files with 74 additions and 39 deletions

View File

@@ -37,7 +37,7 @@ if is_windows:
except ModuleNotFoundError: except ModuleNotFoundError:
pass pass
file_parent = dirname(__file__) file_parent = dirname(abspath(__file__))
# automatically load boot9 if it's in the current directory # automatically load boot9 if it's in the current directory
b9_paths.insert(0, join(file_parent, 'boot9.bin')) b9_paths.insert(0, join(file_parent, 'boot9.bin'))
@@ -191,6 +191,18 @@ class CustomInstallGUI(ttk.Frame):
add_cias = ttk.Button(listbox_buttons, text='Add CIAs', command=add_cias_callback) add_cias = ttk.Button(listbox_buttons, text='Add CIAs', command=add_cias_callback)
add_cias.grid(row=0, column=0) add_cias.grid(row=0, column=0)
def add_cias_callback():
d = fd.askdirectory(parent=parent, title='Select folder containing title contents in CDN format',
initialdir=file_parent)
if d:
if isfile(join(d, 'tmd')):
self.add_cia(d)
else:
self.show_error('tmd file not found in the CDN directory:\n' + d)
add_cias = ttk.Button(listbox_buttons, text='Add CDN title folder', command=add_cias_callback)
add_cias.grid(row=0, column=1)
def add_dirs_callback(): def add_dirs_callback():
d = fd.askdirectory(parent=parent, title='Select folder containing CIA files', initialdir=file_parent) d = fd.askdirectory(parent=parent, title='Select folder containing CIA files', initialdir=file_parent)
if d: if d:
@@ -199,7 +211,7 @@ class CustomInstallGUI(ttk.Frame):
self.add_cia(f.path) self.add_cia(f.path)
add_dirs = ttk.Button(listbox_buttons, text='Add folder', command=add_dirs_callback) add_dirs = ttk.Button(listbox_buttons, text='Add folder', command=add_dirs_callback)
add_dirs.grid(row=0, column=1) add_dirs.grid(row=0, column=2)
def remove_selected_callback(): def remove_selected_callback():
indexes = self.cia_listbox.curselection() indexes = self.cia_listbox.curselection()
@@ -209,7 +221,7 @@ class CustomInstallGUI(ttk.Frame):
n += 1 n += 1
remove_selected = ttk.Button(listbox_buttons, text='Remove selected', command=remove_selected_callback) remove_selected = ttk.Button(listbox_buttons, text='Remove selected', command=remove_selected_callback)
remove_selected.grid(row=0, column=2) remove_selected.grid(row=0, column=3)
# ---------------------------------------------------------------- # # ---------------------------------------------------------------- #
# create listbox # create listbox
@@ -355,7 +367,6 @@ class CustomInstallGUI(ttk.Frame):
installer = CustomInstall(boot9=boot9, installer = CustomInstall(boot9=boot9,
seeddb=seeddb, seeddb=seeddb,
movable=movable_sed, movable=movable_sed,
cias=cias,
sd=sd_root, sd=sd_root,
skip_contents=self.skip_contents_var.get() == 1, skip_contents=self.skip_contents_var.get() == 1,
overwrite_saves=self.overwrite_saves_var.get() == 1) overwrite_saves=self.overwrite_saves_var.get() == 1)
@@ -393,12 +404,21 @@ class CustomInstallGUI(ttk.Frame):
installer.event.on_error += ci_on_error installer.event.on_error += ci_on_error
installer.event.on_cia_start += ci_on_cia_start installer.event.on_cia_start += ci_on_cia_start
try:
installer.prepare_titles(cias)
except Exception as e:
for line in format_exception(*exc_info()):
for line2 in line.split('\n')[:-1]:
installer.log(line2)
self.show_error('An error occurred when trying to read the files.')
self.open_console()
if taskbar: if taskbar:
taskbar.SetProgressState(self.hwnd, tbl.TBPF_NORMAL) taskbar.SetProgressState(self.hwnd, tbl.TBPF_NORMAL)
def install(): def install():
try: try:
result, copied_3dsx = installer.start(continue_on_fail=False) result, copied_3dsx = installer.start()
if result is True: if result is True:
self.log('Done!') self.log('Done!')
if copied_3dsx: if copied_3dsx:

View File

@@ -6,12 +6,14 @@
from argparse import ArgumentParser from argparse import ArgumentParser
from os import makedirs, scandir from os import makedirs, scandir
from os.path import dirname, join, isfile from os.path import dirname, join, isdir, isfile
from random import randint from random import randint
from hashlib import sha256 from hashlib import sha256
from locale import getpreferredencoding from locale import getpreferredencoding
from pprint import pformat
from shutil import copyfile from shutil import copyfile
import sys import sys
from sys import platform, executable
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from traceback import format_exception from traceback import format_exception
from typing import BinaryIO, TYPE_CHECKING from typing import BinaryIO, TYPE_CHECKING
@@ -19,22 +21,26 @@ import subprocess
if TYPE_CHECKING: if TYPE_CHECKING:
from os import PathLike from os import PathLike
from typing import Union from typing import List, Union
from events import Events from events import Events
from pyctr.crypto import CryptoEngine, Keyslot, load_seeddb, get_seed from pyctr.crypto import CryptoEngine, Keyslot, load_seeddb, get_seed
from pyctr.type.cia import CIAReader, CIASection from pyctr.type.cdn import CDNReader
from pyctr.type.cia import CIAReader, CIAError
from pyctr.type.ncch import NCCHSection from pyctr.type.ncch import NCCHSection
from pyctr.util import roundup from pyctr.util import roundup
is_windows = sys.platform == 'win32' is_windows = platform == 'win32'
if platform == 'msys':
platform = 'win32'
# used to run the save3ds_fuse binary next to the script # used to run the save3ds_fuse binary next to the script
frozen = getattr(sys, 'frozen', False) frozen = getattr(sys, 'frozen', False)
script_dir: str script_dir: str
if frozen: if frozen:
script_dir = dirname(sys.executable) script_dir = dirname(executable)
else: else:
script_dir = dirname(__file__) script_dir = dirname(__file__)
@@ -141,10 +147,7 @@ def save_cifinish(path: 'Union[PathLike, bytes, str]', data: dict):
class CustomInstall: class CustomInstall:
def __init__(self, boot9, seeddb, movable, sd, cifinish_out=None,
cia: CIAReader
def __init__(self, boot9, seeddb, movable, cias, sd, cifinish_out=None,
overwrite_saves=False, skip_contents=False): overwrite_saves=False, skip_contents=False):
self.event = Events() self.event = Events()
self.log_lines = [] # Stores all info messages for user to view self.log_lines = [] # Stores all info messages for user to view
@@ -152,7 +155,7 @@ class CustomInstall:
self.crypto = CryptoEngine(boot9=boot9) self.crypto = CryptoEngine(boot9=boot9)
self.crypto.setup_sd_key_from_file(movable) self.crypto.setup_sd_key_from_file(movable)
self.seeddb = seeddb self.seeddb = seeddb
self.cias = cias self.readers: 'List[Union[CDNReader, CIAReader]]' = []
self.sd = sd self.sd = sd
self.skip_contents = skip_contents self.skip_contents = skip_contents
self.overwrite_saves = overwrite_saves self.overwrite_saves = overwrite_saves
@@ -171,11 +174,29 @@ class CustomInstall:
if fire_event: if fire_event:
self.event.update_percentage((total_read / size) * 100, total_read / 1048576, size / 1048576) self.event.update_percentage((total_read / size) * 100, total_read / 1048576, size / 1048576)
def start(self, continue_on_fail=True): def prepare_titles(self, paths: 'List[PathLike]'):
readers = []
for path in paths:
self.log(f'Reading {path}')
if isdir(path):
# try the default tmd file
reader = CDNReader(join(path, 'tmd'))
else:
try:
reader = CIAReader(path)
except CIAError:
# if there was an error with parsing the CIA header,
# the file would be tried in CDNReader next (assuming it's a tmd)
# any other error should be propagated to the caller
reader = CDNReader(path)
readers.append(reader)
self.readers = readers
def start(self):
if frozen: if frozen:
save3ds_fuse_path = join(script_dir, 'bin', 'save3ds_fuse') save3ds_fuse_path = join(script_dir, 'bin', 'save3ds_fuse')
else: else:
save3ds_fuse_path = join(script_dir, 'bin', sys.platform, 'save3ds_fuse') save3ds_fuse_path = join(script_dir, 'bin', platform, 'save3ds_fuse')
if is_windows: if is_windows:
save3ds_fuse_path += '.exe' save3ds_fuse_path += '.exe'
if not isfile(save3ds_fuse_path): if not isfile(save3ds_fuse_path):
@@ -204,22 +225,10 @@ class CustomInstall:
# Now loop through all provided cia files # Now loop through all provided cia files
for idx, c in enumerate(self.cias): for idx, cia in enumerate(self.readers):
self.log('Reading ' + c)
try:
cia = CIAReader(c)
except Exception as e:
self.event.on_error(sys.exc_info())
if continue_on_fail:
continue
else:
return None, False
self.event.on_cia_start(idx) self.event.on_cia_start(idx)
self.cia = cia
tid_parts = (cia.tmd.title_id[0:8], cia.tmd.title_id[8:16]) tid_parts = (cia.tmd.title_id[0:8], cia.tmd.title_id[8:16])
try: try:
@@ -282,10 +291,9 @@ class CustomInstall:
# write the tmd # write the tmd
enc_path = content_root_cmd + '/' + tmd_filename enc_path = content_root_cmd + '/' + tmd_filename
self.log(f'Writing {enc_path}...') self.log(f'Writing {enc_path}...')
with cia.open_raw_section(CIASection.TitleMetadata) as s:
with open(join(content_root, tmd_filename), 'wb') as o: with open(join(content_root, tmd_filename), 'wb') as o:
self.copy_with_progress(s, o, cia.sections[CIASection.TitleMetadata].size, enc_path, with self.crypto.create_ctr_io(Keyslot.SD, o, self.crypto.sd_path_to_iv(enc_path)) as e:
fire_event=False) e.write(bytes(cia.tmd))
# write each content # write each content
for co in cia.content_info: for co in cia.content_info:
@@ -439,7 +447,10 @@ class CustomInstall:
if out.returncode: if out.returncode:
for l in out.stdout.split('\n'): for l in out.stdout.split('\n'):
self.log(l) self.log(l)
return False self.log('Command line:')
for l in pformat(out.args).split('\n'):
self.log(l)
return False, False
for title_id, entry in title_info_entries.items(): for title_id, entry in title_info_entries.items():
# write the title info entry to the temp directory # write the title info entry to the temp directory
@@ -456,6 +467,9 @@ class CustomInstall:
if out.returncode: if out.returncode:
for l in out.stdout.split('\n'): for l in out.stdout.split('\n'):
self.log(l) self.log(l)
self.log('Command line:')
for l in pformat(out.args).split('\n'):
self.log(l)
return False, False return False, False
finalize_3dsx_orig_path = join(script_dir, 'custom-install-finalize.3dsx') finalize_3dsx_orig_path = join(script_dir, 'custom-install-finalize.3dsx')
@@ -536,7 +550,6 @@ if __name__ == "__main__":
installer = CustomInstall(boot9=args.boot9, installer = CustomInstall(boot9=args.boot9,
seeddb=args.seeddb, seeddb=args.seeddb,
cias=args.cia,
movable=args.movable, movable=args.movable,
sd=args.sd, sd=args.sd,
overwrite_saves=args.overwrite_saves, overwrite_saves=args.overwrite_saves,
@@ -558,7 +571,9 @@ if __name__ == "__main__":
installer.event.update_percentage += percent_handle installer.event.update_percentage += percent_handle
installer.event.on_error += error installer.event.on_error += error
result, copied_3dsx = installer.start(continue_on_fail=False) installer.prepare_titles(args.cia)
result, copied_3dsx = installer.start()
if result is False: if result is False:
# save3ds_fuse failed # save3ds_fuse failed
installer.log('NOTE: Once save3ds_fuse is fixed, run the same command again with --skip-contents') installer.log('NOTE: Once save3ds_fuse is fixed, run the same command again with --skip-contents')

View File

@@ -1,3 +1,3 @@
pycryptodomex==3.9.8 pycryptodomex==3.9.8
events==0.3 events==0.3
pyctr==0.4.1 pyctr==0.4.3