Exemplo n.º 1
0
class Application(Frame):
    def __init__(self, master=None):
        super().__init__(master)

        self.pack()

        # First row
        f1 = LabelFrame(self, text='NAND file with No$GBA footer', padx=10, pady=10)

        # NAND Button
        self.nand_mode = False

        nand_icon = PhotoImage(data=('R0lGODlhEAAQAIMAAAAAADMzM2ZmZpmZmczMzP///wAAAAAAAAA'
            'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAMAAAYALAAAAAAQAB'
            'AAAARG0MhJaxU4Y2sECAEgikE1CAFRhGMwSMJwBsU6frIgnR/bv'
            'hTPrWUSDnGw3JGU2xmHrsvyU5xGO8ql6+S0AifPW8kCKpcpEQA7'))

        self.nand_button = Button(f1, image=nand_icon, command=self.change_mode, state=DISABLED)
        self.nand_button.image = nand_icon

        self.nand_button.pack(side='left')

        self.nand_file = StringVar()
        Entry(f1, textvariable=self.nand_file, state='readonly', width=40).pack(side='left')

        Button(f1, text='...', command=self.choose_nand).pack(side='left')

        f1.pack(padx=10, pady=10, fill=X)

        # Second row
        f2 = Frame(self)

        # Check box
        self.twilight = IntVar()
        self.twilight.set(1)

        self.chk = Checkbutton(f2, text='Install latest TWiLight Menu++ on custom firmware',
            variable=self.twilight)

        self.chk.pack(padx=10, anchor=W)

        # NAND operation frame
        self.nand_frame = LabelFrame(f2, text='NAND operation', padx=10, pady=10)

        self.nand_operation = IntVar()
        self.nand_operation.set(0)

        Radiobutton(self.nand_frame, text='Remove No$GBA footer', variable=self.nand_operation,
            value=0, command=lambda: self.enable_entries(False)).pack(anchor=W)

        Radiobutton(self.nand_frame, text='Add No$GBA footer', variable=self.nand_operation,
            value=1, command=lambda: self.enable_entries(True)).pack(anchor=W)

        fl = Frame(self.nand_frame)

        self.cid_label = Label(fl, text='eMMC CID', state=DISABLED)
        self.cid_label.pack(anchor=W, padx=(24, 0))

        self.cid = StringVar()
        self.cid_entry = Entry(fl, textvariable=self.cid, width=20, state=DISABLED)
        self.cid_entry.pack(anchor=W, padx=(24, 0))

        fl.pack(side='left')

        fr = Frame(self.nand_frame)

        self.console_id_label = Label(fr, text='Console ID', state=DISABLED)
        self.console_id_label.pack(anchor=W)

        self.console_id = StringVar()
        self.console_id_entry = Entry(fr, textvariable=self.console_id, width=20, state=DISABLED)
        self.console_id_entry.pack(anchor=W)

        fr.pack(side='right')

        f2.pack(fill=X)

        # Third row
        f3 = Frame(self)

        self.start_button = Button(f3, text='Start', width=16, command=self.hiya, state=DISABLED)
        self.start_button.pack(side='left', padx=(0, 5))

        Button(f3, text='Quit', command=root.destroy, width=16).pack(side='left', padx=(5, 0))

        f3.pack(pady=(10, 20))

        self.folders = []
        self.files = []


    ################################################################################################
    def change_mode(self):
        if (self.nand_mode):
            self.nand_frame.pack_forget()
            self.chk.pack(padx=10, anchor=W)
            self.nand_mode = False

        else:
            if askokcancel('Warning', ('You are about to enter NAND mode. Do it only if you know '
                'what you are doing. Proceed?'), icon=WARNING):
                self.chk.pack_forget()
                self.nand_frame.pack(padx=10, pady=(0, 10), fill=X)
                self.nand_mode = True


    ################################################################################################
    def enable_entries(self, status):
        self.cid_label['state'] = (NORMAL if status else DISABLED)
        self.cid_entry['state'] = (NORMAL if status else DISABLED)
        self.console_id_label['state'] = (NORMAL if status else DISABLED)
        self.console_id_entry['state'] = (NORMAL if status else DISABLED)


    ################################################################################################
    def choose_nand(self):
        name = askopenfilename(filetypes=( ( 'nand.bin', '*.bin' ), ( 'DSi-1.mmc', '*.mmc' ) ))
        self.nand_file.set(name)

        self.nand_button['state'] = (NORMAL if name != '' else DISABLED)
        self.start_button['state'] = (NORMAL if name != '' else DISABLED)


    ################################################################################################
    def hiya(self):
        if not self.nand_mode:
            showinfo('Info', 'Now you will be asked to choose the SD card path that will be used '
                'for installing the custom firmware (or an output folder).\n\nIn order to avoid '
                'boot errors please assure it is empty before continuing.')
            self.sd_path = askdirectory()

            # Exit if no path was selected
            if self.sd_path == '':
                return

        # If adding a No$GBA footer, check if CID and ConsoleID values are OK
        elif self.nand_operation.get() == 1:
            cid = self.cid.get()
            console_id = self.console_id.get()

            # Check lengths
            if len(cid) != 32:
                showerror('Error', 'Bad eMMC CID')
                return

            elif len(console_id) != 16:
                showerror('Error', 'Bad Console ID')
                return

            # Parse strings to hex
            try:
                cid = bytearray.fromhex(cid)

            except ValueError:
                showerror('Error', 'Bad eMMC CID')
                return

            try:
                console_id = bytearray(reversed(bytearray.fromhex(console_id)))

            except ValueError:
                showerror('Error', 'Bad Console ID')
                return

        dialog = Toplevel(self)
        # Open as dialog (parent disabled)
        dialog.grab_set()
        dialog.title('Status')
        # Disable maximizing
        dialog.resizable(0, 0)

        frame = Frame(dialog, bd=2, relief=SUNKEN)

        scrollbar = Scrollbar(frame)
        scrollbar.pack(side=RIGHT, fill=Y)

        self.log = ThreadSafeText(frame, bd=0, width=52, height=20,
            yscrollcommand=scrollbar.set)
        self.log.pack()

        scrollbar.config(command=self.log.yview)

        frame.pack()

        Button(dialog, text='Close', command=dialog.destroy, width=16).pack(pady=10)

        # Center in window
        dialog.update_idletasks()
        width = dialog.winfo_width()
        height = dialog.winfo_height()
        dialog.geometry('%dx%d+%d+%d' % (width, height, root.winfo_x() + (root.winfo_width() / 2) -
            (width / 2), root.winfo_y() + (root.winfo_height() / 2) - (height / 2)))

        # Check if we'll be adding a No$GBA footer
        if self.nand_mode and self.nand_operation.get() == 1:
            Thread(target=self.add_footer, args=(cid, console_id)).start()

        else:
            Thread(target=self.check_nand).start()


    ################################################################################################
    def check_nand(self):
        self.log.write('Checking NAND file...')

        # Read the NAND file
        try:
            with open(self.nand_file.get(), 'rb') as f:
                # Go to the No$GBA footer offset
                f.seek(-64, 2)
                # Read the footer's header :-)
                bstr = f.read(0x10)

                if bstr == b'DSi eMMC CID/CPU':
                    # Read the CID
                    bstr = f.read(0x10)
                    self.cid.set(bstr.hex().upper())
                    self.log.write('- eMMC CID: ' + self.cid.get())

                    # Read the console ID
                    bstr = f.read(8)
                    self.console_id.set(bytearray(reversed(bstr)).hex().upper())
                    self.log.write('- Console ID: ' + self.console_id.get())

                    # Check we are removing the No$GBA footer
                    if self.nand_mode:
                        Thread(target=self.remove_footer).start()

                    else:
                        Thread(target=self.get_latest_hiyacfw).start()

                else:
                    self.log.write('ERROR: No$GBA footer not found')

        except IOError as e:
            print(e)
            self.log.write('ERROR: Could not open the file ' +
                path.basename(self.nand_file.get()))


    ################################################################################################
    def get_latest_hiyacfw(self):
        # Try to use already downloaded HiyaCFW archive
        filename = 'HiyaCFW.7z'

        try:
            if path.isfile(filename):
                self.log.write('\nPreparing HiyaCFW...')

            else:
                self.log.write('\nDownloading latest HiyaCFW release...')

                with urlopen('https://github.com/RocketRobz/hiyaCFW/releases/latest/download/' +
                    filename) as src, open(filename, 'wb') as dst:
                    copyfileobj(src, dst)

            self.log.write('- Extracting HiyaCFW archive...')

            proc = Popen([ _7z, 'x', '-bso0', '-y', filename, 'for PC', 'for SDNAND SD card' ])

            ret_val = proc.wait()

            if ret_val == 0:
                self.files.append(filename)
                self.folders.append('for PC')
                self.folders.append('for SDNAND SD card')
                # Got to decrypt NAND if bootloader.nds is present
                Thread(target=self.decrypt_nand if path.isfile('bootloader.nds')
                    else self.extract_bios).start()

            else:
                self.log.write('ERROR: Extractor failed')

        except (URLError, IOError) as e:
            print(e)
            self.log.write('ERROR: Could not get HiyaCFW')

        except OSError as e:
            print(e)
            self.log.write('ERROR: Could not execute ' + exe)


    ################################################################################################
    def extract_bios(self):
        self.log.write('\nExtracting ARM7/ARM9 BIOS from NAND...')

        try:
            proc = Popen([ twltool, 'boot2', '--in', self.nand_file.get() ])

            ret_val = proc.wait()

            if ret_val == 0:
                # Hash arm7.bin
                sha1_hash = sha1()

                with open('arm7.bin', 'rb') as f:
                    sha1_hash.update(f.read())

                self.log.write('- arm7.bin SHA1:\n  ' +
                    sha1_hash.digest().hex().upper())

                # Hash arm9.bin
                sha1_hash = sha1()

                with open('arm9.bin', 'rb') as f:
                    sha1_hash.update(f.read())

                self.log.write('- arm9.bin SHA1:\n  ' +
                    sha1_hash.digest().hex().upper())

                self.files.append('arm7.bin')
                self.files.append('arm9.bin')

                Thread(target=self.patch_bios).start()

            else:
                self.log.write('ERROR: Extractor failed')
                Thread(target=self.clean, args=(True,)).start()

        except OSError as e:
            print(e)
            self.log.write('ERROR: Could not execute ' + exe)
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def patch_bios(self):
        self.log.write('\nPatching ARM7/ARM9 BIOS...')

        try:
            self.patcher(path.join('for PC', 'bootloader files', 'bootloader arm7 patch.ips'),
                'arm7.bin')

            self.patcher(path.join('for PC', 'bootloader files', 'bootloader arm9 patch.ips'),
                'arm9.bin')

            # Hash arm7.bin
            sha1_hash = sha1()

            with open('arm7.bin', 'rb') as f:
                sha1_hash.update(f.read())

            self.log.write('- Patched arm7.bin SHA1:\n  ' +
                sha1_hash.digest().hex().upper())

            # Hash arm9.bin
            sha1_hash = sha1()

            with open('arm9.bin', 'rb') as f:
                sha1_hash.update(f.read())

            self.log.write('- Patched arm9.bin SHA1:\n  ' +
                sha1_hash.digest().hex().upper())

            Thread(target=self.arm9_prepend).start()

        except IOError as e:
            print(e)
            self.log.write('ERROR: Could not patch BIOS')
            Thread(target=self.clean, args=(True,)).start()

        except Exception as e:
            print(e)
            self.log.write('ERROR: Invalid patch header')
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def arm9_prepend(self):
        self.log.write('\nPrepending data to ARM9 BIOS...')

        try:
            with open('arm9.bin', 'rb') as f:
                data = f.read()

            with open('arm9.bin', 'wb') as f:
                with open(path.join('for PC', 'bootloader files',
                    'bootloader arm9 append to start.bin'), 'rb') as pre:
                    f.write(pre.read())

                f.write(data)

            # Hash arm9.bin
            sha1_hash = sha1()

            with open('arm9.bin', 'rb') as f:
                sha1_hash.update(f.read())

            self.log.write('- Prepended arm9.bin SHA1:\n  ' +
                sha1_hash.digest().hex().upper())

            Thread(target=self.make_bootloader).start()

        except IOError as e:
            print(e)
            self.log.write('ERROR: Could not prepend data to ARM9 BIOS')
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def make_bootloader(self):
        self.log.write('\nGenerating new bootloader...')

        exe = (path.join('for PC', 'bootloader files', 'ndstool.exe') if sysname == 'Windows' else
            path.join(sysname, 'ndsblc'))

        try:
            proc = Popen([ exe, '-c', 'bootloader.nds', '-9', 'arm9.bin', '-7', 'arm7.bin', '-t',
                path.join('for PC', 'bootloader files', 'banner.bin'), '-h',
                path.join('for PC', 'bootloader files', 'header.bin') ])

            ret_val = proc.wait()

            if ret_val == 0:
                self.files.append('bootloader.nds')

                # Hash bootloader.nds
                sha1_hash = sha1()

                with open('bootloader.nds', 'rb') as f:
                    sha1_hash.update(f.read())

                self.log.write('- bootloader.nds SHA1:\n  ' +
                    sha1_hash.digest().hex().upper())

                Thread(target=self.decrypt_nand).start()

            else:
                self.log.write('ERROR: Generator failed')
                Thread(target=self.clean, args=(True,)).start()

        except OSError as e:
            print(e)
            self.log.write('ERROR: Could not execute ' + exe)
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def decrypt_nand(self):
        self.log.write('\nDecrypting NAND...')

        try:
            proc = Popen([ twltool, 'nandcrypt', '--in', self.nand_file.get(), '--out',
                self.console_id.get() + '.img' ])

            ret_val = proc.wait()
            print("\n")

            if ret_val == 0:
                self.files.append(self.console_id.get() + '.img')

                Thread(target=self.win_extract_nand if sysname == 'Windows'
                    else self.extract_nand).start()

            else:
                self.log.write('ERROR: Decryptor failed')
                Thread(target=self.clean, args=(True,)).start()

        except OSError as e:
            print(e)
            self.log.write('ERROR: Could not execute ' + exe)
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def win_extract_nand(self):
        self.log.write('\nExtracting files from NAND...')

        try:
            proc = Popen([ _7z, 'x', '-bso0', '-y', self.console_id.get() + '.img', '0.fat' ])

            ret_val = proc.wait()

            if ret_val == 0:
                self.files.append('0.fat')

                proc = Popen([ _7z, 'x', '-bso0', '-y', '-o' + self.sd_path, '0.fat' ])

                ret_val = proc.wait()

                if ret_val == 0:
                    Thread(target=self.get_launcher).start()

                else:
                    self.log.write('ERROR: Extractor failed, please update 7-Zip')
                    Thread(target=self.clean, args=(True,)).start()

            else:
                self.log.write('ERROR: Extractor failed')
                Thread(target=self.clean, args=(True,)).start()

        except OSError as e:
            print(e)
            self.log.write('ERROR: Could not execute ' + exe)
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def extract_nand(self):
        self.log.write('\nExtracting files from NAND...')

        try:
            # DSi first partition offset: 0010EE00h
            proc = Popen([ fatcat, '-O', '1109504', '-x', self.sd_path,
                self.console_id.get() + '.img' ])

            ret_val = proc.wait()

            if ret_val == 0:
                Thread(target=self.get_launcher).start()

            else:
                self.log.write('ERROR: Extractor failed')
                Thread(target=self.clean, args=(True,)).start()

        except OSError as e:
            print(e)
            self.log.write('ERROR: Could not execute ' + exe)
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def get_launcher(self):
        app = self.detect_region()

        # Stop if no supported region was found
        if not app:
            Thread(target=self.clean, args=(True,)).start()
            return

        # Delete contents of the launcher folder as it will be replaced by the one from HiyaCFW
        launcher_folder = path.join(self.sd_path, 'title', '00030017', app, 'content')

        # Walk through all files in the launcher content folder
        for file in listdir(launcher_folder):
            file = path.join(launcher_folder, file)

            # Set current file as read/write in case we are in Windows and unlaunch was installed
            # in the NAND. For Linux and MacOS fatcat doesn't keep file attributes
            if sysname == 'Windows':
                chmod(file, 438)

            # Delete current file
            remove(file)

        # Try to use already downloaded launcher
        try:
            if path.isfile(self.launcher_region):
                self.log.write('\nPreparing ' + self.launcher_region + ' launcher...')

            else:
                self.log.write('\nDownloading ' + self.launcher_region + ' launcher...')

                with urlopen('https://raw.githubusercontent.com'
                    '/mondul/HiyaCFW-Helper/master/launchers/' +
                    self.launcher_region) as src, open(self.launcher_region, 'wb') as dst:
                    copyfileobj(src, dst)

            self.log.write('- Decrypting launcher...')

            proc = Popen([ _7z, 'x', '-bso0', '-y', '-p' + app, self.launcher_region,
                '00000002.app' ])

            ret_val = proc.wait()

            if ret_val == 0:
                self.files.append(self.launcher_region)
                self.files.append('00000002.app')

                # Hash 00000002.app
                sha1_hash = sha1()

                with open('00000002.app', 'rb') as f:
                    sha1_hash.update(f.read())

                self.log.write('- Patched launcher SHA1:\n  ' +
                    sha1_hash.digest().hex().upper())

                Thread(target=self.install_hiyacfw, args=(launcher_folder,)).start()

            else:
                self.log.write('ERROR: Extractor failed')
                Thread(target=self.clean, args=(True,)).start()

        except IOError as e:
            print(e)
            self.log.write('ERROR: Could not download ' + self.launcher_region + ' launcher')
            Thread(target=self.clean, args=(True,)).start()

        except OSError as e:
            print(e)
            self.log.write('ERROR: Could not execute ' + exe)
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def install_hiyacfw(self, launcher_folder):
        self.log.write('\nCopying HiyaCFW files...')

        # Reset copied files cache
        _path_created.clear()

        copy_tree('for SDNAND SD card', self.sd_path, update=1)
        copyfile('bootloader.nds', path.join(self.sd_path, 'hiya', 'bootloader.nds'))
        copyfile('00000002.app', path.join(launcher_folder, '00000002.app'))

        Thread(target=self.get_latest_twilight if self.twilight.get() == 1 else self.clean).start()


    ################################################################################################
    def get_latest_twilight(self):
        # Try to use already downloaded TWiLight Menu++ archive
        filename = 'TWiLightMenu.7z'

        try:
            if path.isfile(filename):
                self.log.write('\nPreparing TWiLight Menu++...')

            else:
                self.log.write('\nDownloading latest TWiLight Menu++ release...')

                with urlopen('https://github.com/DS-Homebrew/TWiLightMenu/releases/latest/download/' +
                    filename) as src, open(filename, 'wb') as dst:
                    copyfileobj(src, dst)

            self.log.write('- Extracting ' + filename[:-3] + ' archive...')

            proc = Popen([ _7z, 'x', '-bso0', '-y', filename, '_nds', 'DSi - CFW users',
                'DSi&3DS - SD card users', 'roms' ])

            ret_val = proc.wait()

            if ret_val == 0:
                self.files.append(filename)
                self.folders.append('DSi - CFW users')
                self.folders.append('_nds')
                self.folders.append('DSi&3DS - SD card users')
                self.folders.append('roms')
                Thread(target=self.install_twilight, args=(filename[:-3],)).start()

            else:
                self.log.write('ERROR: Extractor failed')
                Thread(target=self.clean, args=(True,)).start()

        except (URLError, IOError) as e:
            print(e)
            self.log.write('ERROR: Could not get TWiLight Menu++')
            Thread(target=self.clean, args=(True,)).start()

        except OSError as e:
            print(e)
            self.log.write('ERROR: Could not execute ' + exe)
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def install_twilight(self, name):
        self.log.write('\nCopying ' + name + ' files...')

        copy_tree(path.join('DSi - CFW users', 'SDNAND root'), self.sd_path, update=1)
        copy_tree('_nds', path.join(self.sd_path, '_nds'))
        copy_tree('DSi&3DS - SD card users', self.sd_path, update=1)
        copy_tree('roms', path.join(self.sd_path, 'roms'))

        Thread(target=self.clean).start()


    ################################################################################################
    def clean(self, err=False):
        self.log.write('\nCleaning...')

        while len(self.folders) > 0:
            rmtree(self.folders.pop(), ignore_errors=True)

        while len(self.files) > 0:
            try:
                remove(self.files.pop())

            except:
                pass

        if err:
            self.log.write('Done')
            return

        self.log.write('Done!\nEject your SD card and insert it into your DSi')


    ################################################################################################
    def patcher(self, patchpath, filepath):
        patch_size = path.getsize(patchpath)

        patchfile = open(patchpath, 'rb')

        if patchfile.read(5) != b'PATCH':
            patchfile.close()
            raise Exception()

        target = open(filepath, 'r+b')

        # Read First Record
        r = patchfile.read(3)

        while patchfile.tell() not in [ patch_size, patch_size - 3 ]:
            # Unpack 3-byte pointers.
            offset = self.unpack_int(r)
            # Read size of data chunk
            r = patchfile.read(2)
            size = self.unpack_int(r)

            if size == 0:  # RLE Record
                r = patchfile.read(2)
                rle_size = self.unpack_int(r)
                data = patchfile.read(1) * rle_size

            else:
                data = patchfile.read(size)

            # Write to file
            target.seek(offset)
            target.write(data)
            # Read Next Record
            r = patchfile.read(3)

        if patch_size - 3 == patchfile.tell():
            trim_size = self.unpack_int(patchfile.read(3))
            target.truncate(trim_size)

        # Cleanup
        target.close()
        patchfile.close()


    ################################################################################################
    def unpack_int(self, bstr):
        # Read an n-byte big-endian integer from a byte string
        ( ret_val, ) = unpack_from('>I', b'\x00' * (4 - len(bstr)) + bstr)
        return ret_val


    ################################################################################################
    def detect_region(self):
        REGION_CODES = {
            '484e4145': 'USA',
            '484e414a': 'JAP',
            '484e414b': 'KOR',
            '484e4150': 'EUR',
            '484e4155': 'AUS'
        }

        # Autodetect console region
        try:
            for app in listdir(path.join(self.sd_path, 'title', '00030017')):
                for file in listdir(path.join(self.sd_path, 'title', '00030017', app, 'content')):
                    if file.endswith('.app'):
                        try:
                            self.log.write('- Detected ' + REGION_CODES[app] + ' console NAND dump')
                            self.launcher_region = REGION_CODES[app]
                            return app

                        except KeyError:
                            self.log.write('ERROR: Unsupported console region')
                            return False

            self.log.write('ERROR: Could not detect console region')

        except OSError as e:
            self.log.write('ERROR: ' + e.strerror + ': ' + e.filename)

        return False


    ################################################################################################
    def remove_footer(self):
        self.log.write('\nRemoving No$GBA footer...')

        file = self.console_id.get() + '-no-footer.bin'

        try:
            copyfile(self.nand_file.get(), file)

            # Back-up footer info
            with open(self.console_id.get() + '-info.txt', 'w') as f:
                f.write('eMMC CID: ' + self.cid.get() + '\r\n')
                f.write('Console ID: ' + self.console_id.get() + '\r\n')

            with open(file, 'r+b') as f:
                # Go to the No$GBA footer offset
                f.seek(-64, 2)
                # Remove footer
                f.truncate()

            self.log.write('\nDone!\nModified NAND stored as\n' + file +
                '\nStored footer info in ' + self.console_id.get() + '-info.txt')

        except IOError as e:
            print(e)
            self.log.write('ERROR: Could not open the file ' +
                path.basename(self.nand_file.get()))


    ################################################################################################
    def add_footer(self, cid, console_id):
        self.log.write('Adding No$GBA footer...')

        file = self.console_id.get() + '-footer.bin'

        try:
            copyfile(self.nand_file.get(), file)

            with open(file, 'r+b') as f:
                # Go to the No$GBA footer offset
                f.seek(-64, 2)
                # Read the footer's header :-)
                bstr = f.read(0x10)

                # Check if it already has a footer
                if bstr == b'DSi eMMC CID/CPU':
                    self.log.write('ERROR: File already has a No$GBA footer')
                    f.close()
                    remove(file)
                    return

                # Go to the end of file
                f.seek(0, 2)
                # Write footer
                f.write(b'DSi eMMC CID/CPU')
                f.write(cid)
                f.write(console_id)
                f.write(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
                    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')

            self.log.write('\nDone!\nModified NAND stored as\n' + file)

        except IOError as e:
            print(e)
            self.log.write('ERROR: Could not open the file ' +
                path.basename(self.nand_file.get()))
Exemplo n.º 2
0
class Application(Frame):
    def __init__(self, master=None):
        super().__init__(master)

        self.pack()

        # First row
        f1 = LabelFrame(self, text='NAND file with No$GBA footer', padx=10, pady=10)

        # NAND Button
        self.nand_mode = False

        nand_icon = PhotoImage(data=('R0lGODlhEAAQAIMAAAAAADMzM2ZmZpmZmczMzP///wAAAAAAAAA'
            'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAMAAAYALAAAAAAQAB'
            'AAAARG0MhJaxU4Y2sECAEgikE1CAFRhGMwSMJwBsU6frIgnR/bv'
            'hTPrWUSDnGw3JGU2xmHrsvyU5xGO8ql6+S0AifPW8kCKpcpEQA7'))

        self.nand_button = Button(f1, image=nand_icon, command=self.change_mode, state=DISABLED)
        self.nand_button.image = nand_icon

        self.nand_button.pack(side='left')

        self.nand_file = StringVar()
        Entry(f1, textvariable=self.nand_file, state='readonly', width=40).pack(side='left')

        Button(f1, text='...', command=self.choose_nand).pack(side='left')

        f1.pack(padx=10, pady=10, fill=X)

        # Second row
        f2 = Frame(self)

        # Check box
        self.twilight = IntVar()
        self.twilight.set(1)

        self.chk = Checkbutton(f2, text='Install latest TWiLight Menu++ on custom firmware',
            variable=self.twilight)

        self.chk.pack(padx=10, anchor=W)

        # NAND operation frame
        self.nand_frame = LabelFrame(f2, text='NAND operation', padx=10, pady=10)

        self.nand_operation = IntVar()
        self.nand_operation.set(0)

        Radiobutton(self.nand_frame, text='Uninstall unlaunch or install v1.4 stable',
            variable=self.nand_operation, value=0,
            command=lambda: self.enable_entries(False)).pack(anchor=W)

        Radiobutton(self.nand_frame, text='Remove No$GBA footer', variable=self.nand_operation,
            value=1, command=lambda: self.enable_entries(False)).pack(anchor=W)

        Radiobutton(self.nand_frame, text='Add No$GBA footer', variable=self.nand_operation,
            value=2, command=lambda: self.enable_entries(True)).pack(anchor=W)

        fl = Frame(self.nand_frame)

        self.cid_label = Label(fl, text='eMMC CID', state=DISABLED)
        self.cid_label.pack(anchor=W, padx=(24, 0))

        self.cid = StringVar()
        self.cid_entry = Entry(fl, textvariable=self.cid, width=20, state=DISABLED)
        self.cid_entry.pack(anchor=W, padx=(24, 0))

        fl.pack(side='left')

        fr = Frame(self.nand_frame)

        self.console_id_label = Label(fr, text='Console ID', state=DISABLED)
        self.console_id_label.pack(anchor=W)

        self.console_id = StringVar()
        self.console_id_entry = Entry(fr, textvariable=self.console_id, width=20, state=DISABLED)
        self.console_id_entry.pack(anchor=W)

        fr.pack(side='right')

        f2.pack(fill=X)

        # Third row
        f3 = Frame(self)

        self.start_button = Button(f3, text='Start', width=16, command=self.hiya, state=DISABLED)
        self.start_button.pack(side='left', padx=(0, 5))

        Button(f3, text='Quit', command=root.destroy, width=16).pack(side='left', padx=(5, 0))

        f3.pack(pady=(10, 20))

        self.folders = []
        self.files = []


    ################################################################################################
    def change_mode(self):
        if (self.nand_mode):
            self.nand_frame.pack_forget()
            self.chk.pack(padx=10, anchor=W)
            self.nand_mode = False

        else:
            if askokcancel('Warning', ('You are about to enter NAND mode. Do it only if you know '
                'what you are doing. Proceed?'), icon=WARNING):
                self.chk.pack_forget()
                self.nand_frame.pack(padx=10, pady=(0, 10), fill=X)
                self.nand_mode = True


    ################################################################################################
    def enable_entries(self, status):
        self.cid_label['state'] = (NORMAL if status else DISABLED)
        self.cid_entry['state'] = (NORMAL if status else DISABLED)
        self.console_id_label['state'] = (NORMAL if status else DISABLED)
        self.console_id_entry['state'] = (NORMAL if status else DISABLED)


    ################################################################################################
    def choose_nand(self):
        name = askopenfilename(filetypes=( ( 'nand.bin', '*.bin' ), ( 'DSi-1.mmc', '*.mmc' ) ))
        self.nand_file.set(name)

        self.nand_button['state'] = (NORMAL if name != '' else DISABLED)
        self.start_button['state'] = (NORMAL if name != '' else DISABLED)


    ################################################################################################
    def hiya(self):
        if not self.nand_mode:
            showinfo('Info', 'Now you will be asked to choose the SD card path that will be used '
                'for installing the custom firmware (or an output folder).\n\nIn order to avoid '
                'boot errors please assure it is empty before continuing.')
            self.sd_path = askdirectory()

            # Exit if no path was selected
            if self.sd_path == '':
                return

        # If adding a No$GBA footer, check if CID and ConsoleID values are OK
        elif self.nand_operation.get() == 2:
            cid = self.cid.get()
            console_id = self.console_id.get()

            # Check lengths
            if len(cid) != 32:
                showerror('Error', 'Bad eMMC CID')
                return

            elif len(console_id) != 16:
                showerror('Error', 'Bad Console ID')
                return

            # Parse strings to hex
            try:
                cid = cid.decode('hex')

            except TypeError:
                showerror('Error', 'Bad eMMC CID')
                return

            try:
                console_id = bytearray(reversed(console_id.decode('hex')))

            except TypeError:
                showerror('Error', 'Bad Console ID')
                return

        dialog = Toplevel(self)
        # Open as dialog (parent disabled)
        dialog.grab_set()
        dialog.title('Status')
        # Disable maximizing
        dialog.resizable(0, 0)

        frame = Frame(dialog, bd=2, relief=SUNKEN)

        scrollbar = Scrollbar(frame)
        scrollbar.pack(side=RIGHT, fill=Y)

        self.log = ThreadSafeText(frame, bd=0, width=52, height=20,
            yscrollcommand=scrollbar.set)
        self.log.pack()

        scrollbar.config(command=self.log.yview)

        frame.pack()

        Button(dialog, text='Close', command=dialog.destroy, width=16).pack(pady=10)

        # Center in window
        dialog.update_idletasks()
        width = dialog.winfo_width()
        height = dialog.winfo_height()
        dialog.geometry('%dx%d+%d+%d' % (width, height, root.winfo_x() + (root.winfo_width() / 2) -
            (width / 2), root.winfo_y() + (root.winfo_height() / 2) - (height / 2)))

        # Check if we'll be adding a No$GBA footer
        if self.nand_mode and self.nand_operation.get() == 2:
            Thread(target=self.add_footer, args=(cid, console_id)).start()

        else:
            Thread(target=self.check_nand).start()


    ################################################################################################
    def check_nand(self):
        self.log.write('Checking NAND file...')

        # Read the NAND file
        try:
            with open(self.nand_file.get(), 'rb') as f:
                # Go to the No$GBA footer offset
                f.seek(-64, 2)
                # Read the footer's header :-)
                bstr = f.read(0x10)

                if bstr == b'DSi eMMC CID/CPU':
                    # Read the CID
                    bstr = f.read(0x10)
                    self.cid.set(bstr.hex().upper())
                    self.log.write('- eMMC CID: ' + self.cid.get())

                    # Read the console ID
                    bstr = f.read(8)
                    self.console_id.set(bytearray(reversed(bstr)).hex().upper())
                    self.log.write('- Console ID: ' + self.console_id.get())

                    # Check we are making an unlaunch operation or removing the No$GBA footer
                    if self.nand_mode:
                        if self.nand_operation.get() == 0:
                            Thread(target=self.decrypt_nand).start()

                        else:
                            Thread(target=self.remove_footer).start()
                            pass

                    else:
                        Thread(target=self.get_latest_hiyacfw).start()

                else:
                    self.log.write('ERROR: No$GBA footer not found')

        except IOError:
            self.log.write('ERROR: Could not open the file ' +
                path.basename(self.nand_file.get()))


    ################################################################################################
    def get_latest_hiyacfw(self):
        filename = 'HiyaCFW.7z'

        try:
            self.log.write('\nDownloading latest HiyaCFW release...')

            conn = urlopen('https://api.github.com/repos/RocketRobz/hiyaCFW/releases/latest')
            latest = jsonify(conn)
            conn.close()

            urlretrieve(latest['assets'][0]['browser_download_url'], filename)

            self.log.write('- Extracting HiyaCFW archive...')

            exe = path.join(sysname, '7za')

            proc = Popen([ exe, 'x', '-bso0', '-y', filename, 'for PC', 'for SDNAND SD card' ])

            ret_val = proc.wait()

            if ret_val == 0:
                self.files.append(filename)
                self.folders.append('for PC')
                self.folders.append('for SDNAND SD card')
                # Got to decrypt NAND if bootloader.nds is present
                Thread(target=self.decrypt_nand if path.isfile('bootloader.nds')
                    else self.extract_bios).start()

            else:
                self.log.write('ERROR: Extractor failed')

        except (URLError, IOError) as e:
            self.log.write('ERROR: Could not get HiyaCFW')

        except OSError:
            self.log.write('ERROR: Could not execute ' + exe)


    ################################################################################################
    def extract_bios(self):
        self.log.write('\nExtracting ARM7/ARM9 BIOS from NAND...')

        exe = path.join(sysname, 'twltool')

        try:
            proc = Popen([ exe, 'boot2', '--in', self.nand_file.get() ])

            ret_val = proc.wait()

            if ret_val == 0:
                # Hash arm7.bin
                sha1_hash = sha1()

                with open('arm7.bin', 'rb') as f:
                    sha1_hash.update(f.read())

                self.log.write('- arm7.bin SHA1:\n  ' +
                    sha1_hash.digest().hex().upper())

                # Hash arm9.bin
                sha1_hash = sha1()

                with open('arm9.bin', 'rb') as f:
                    sha1_hash.update(f.read())

                self.log.write('- arm9.bin SHA1:\n  ' +
                    sha1_hash.digest().hex().upper())

                self.files.append('arm7.bin')
                self.files.append('arm9.bin')

                Thread(target=self.patch_bios).start()

            else:
                self.log.write('ERROR: Extractor failed')
                Thread(target=self.clean, args=(True,)).start()

        except OSError:
            self.log.write('ERROR: Could not execute ' + exe)
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def patch_bios(self):
        self.log.write('\nPatching ARM7/ARM9 BIOS...')

        try:
            self.patcher(path.join('for PC', 'bootloader files', 'bootloader arm7 patch.ips'),
                'arm7.bin')

            self.patcher(path.join('for PC', 'bootloader files', 'bootloader arm9 patch.ips'),
                'arm9.bin')

            # Hash arm7.bin
            sha1_hash = sha1()

            with open('arm7.bin', 'rb') as f:
                sha1_hash.update(f.read())

            self.log.write('- Patched arm7.bin SHA1:\n  ' +
                sha1_hash.digest().hex().upper())

            # Hash arm9.bin
            sha1_hash = sha1()

            with open('arm9.bin', 'rb') as f:
                sha1_hash.update(f.read())

            self.log.write('- Patched arm9.bin SHA1:\n  ' +
                sha1_hash.digest().hex().upper())

            Thread(target=self.arm9_prepend).start()

        except IOError:
            self.log.write('ERROR: Could not patch BIOS')
            Thread(target=self.clean, args=(True,)).start()

        except Exception:
            self.log.write('ERROR: Invalid patch header')
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def arm9_prepend(self):
        self.log.write('\nPrepending data to ARM9 BIOS...')

        try:
            with open('arm9.bin', 'rb') as f:
                data = f.read()

            with open('arm9.bin', 'wb') as f:
                with open(path.join('for PC', 'bootloader files',
                    'bootloader arm9 append to start.bin'), 'rb') as pre:
                    f.write(pre.read())

                f.write(data)

            # Hash arm9.bin
            sha1_hash = sha1()

            with open('arm9.bin', 'rb') as f:
                sha1_hash.update(f.read())

            self.log.write('- Prepended arm9.bin SHA1:\n  ' +
                sha1_hash.digest().hex().upper())

            Thread(target=self.make_bootloader).start()

        except IOError:
            self.log.write('ERROR: Could not prepend data to ARM9 BIOS')
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def make_bootloader(self):
        self.log.write('\nGenerating new bootloader...')

        exe = (path.join('for PC', 'bootloader files', 'ndstool') if sysname == 'Windows' else
            path.join(sysname, 'ndsblc'))

        try:
            proc = Popen([ exe, '-c', 'bootloader.nds', '-9', 'arm9.bin', '-7', 'arm7.bin', '-t',
                path.join('for PC', 'bootloader files', 'banner.bin'), '-h',
                path.join('for PC', 'bootloader files', 'header.bin') ])

            ret_val = proc.wait()

            if ret_val == 0:
                # Hash bootloader.nds
                sha1_hash = sha1()

                with open('bootloader.nds', 'rb') as f:
                    sha1_hash.update(f.read())

                self.log.write('- bootloader.nds SHA1:\n  ' +
                    sha1_hash.digest().hex().upper())

                Thread(target=self.decrypt_nand).start()

            else:
                self.log.write('ERROR: Generator failed')
                Thread(target=self.clean, args=(True,)).start()

        except OSError:
            self.log.write('ERROR: Could not execute ' + exe)
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def decrypt_nand(self):
        self.log.write('\nDecrypting NAND...')

        exe = path.join(sysname, 'twltool')

        try:
            proc = Popen([ exe, 'nandcrypt', '--in', self.nand_file.get(), '--out',
                self.console_id.get() + '.img' ])

            ret_val = proc.wait()

            if ret_val == 0:
                if not self.nand_mode:
                    self.files.append(self.console_id.get() + '.img')

                Thread(target=self.mount_nand).start()

            else:
                self.log.write('ERROR: Decryptor failed')
                Thread(target=self.clean, args=(True,)).start()

        except OSError:
            self.log.write('ERROR: Could not execute ' + exe)
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def mount_nand(self):
        self.log.write('\nMounting decrypted NAND...')

        try:
            if sysname == 'Windows':
                exe = osfmount

                cmd = [ osfmount, '-a', '-t', 'file', '-f', self.console_id.get() + '.img', '-m',
                    '#:', '-o', 'ro,rem' ]

                if self.nand_mode:
                    cmd[-1] = 'rw,rem'

                proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)

                outs, errs = proc.communicate()

                if proc.returncode == 0:
                    self.mounted = search(r'[a-zA-Z]:\s', outs.decode('utf-8')).group(0).strip()
                    self.log.write('- Mounted on drive ' + self.mounted)

                else:
                    self.log.write('ERROR: Mounter failed')
                    Thread(target=self.clean, args=(True,)).start()
                    return

            elif sysname == 'Darwin':
                exe = 'hdiutil'

                cmd = [ exe, 'attach', '-imagekey', 'diskimage-class=CRawDiskImage', '-nomount',
                    self.console_id.get() + '.img' ]

                if not self.nand_mode:
                    cmd.insert(2, '-readonly')

                proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)

                outs, errs = proc.communicate()

                if proc.returncode == 0:
                    self.raw_disk = search(r'^\/dev\/disk\d+', outs.decode('utf-8')).group(0)
                    self.log.write('- Mounted raw disk on ' + self.raw_disk)

                    cmd = [ exe, 'mount', self.raw_disk + 's1' ]

                    if not self.nand_mode:
                        cmd.insert(2, '-readonly')

                    proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)

                    outs, errs = proc.communicate()

                    if proc.returncode == 0:
                        self.mounted = search(r'\/Volumes\/.+', outs.decode('utf-8')).group(0)
                        self.log.write('- Mounted volume on ' + self.mounted)

                    else:
                        self.log.write('ERROR: Mounter failed')
                        Thread(target=self.clean, args=(True,)).start()
                        return

                else:
                    self.log.write('ERROR: Mounter failed')
                    Thread(target=self.clean, args=(True,)).start()
                    return

            else:  # Linux
                exe = 'losetup'

                cmd = [ exe, '-P', '-f', '--show', self.console_id.get() + '.img' ]

                if not self.nand_mode:
                    cmd.insert(2, '-r')

                proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
                outs, errs = proc.communicate()

                if proc.returncode == 0:
                    self.loop_dev = search(r'\/dev\/loop\d+', outs.decode('utf-8')).group(0)
                    self.log.write('- Mounted loop device on ' + self.loop_dev)

                    exe = 'mount'

                    self.mounted = '/mnt'

                    cmd = [ exe, '-t', 'vfat', self.loop_dev + 'p1', self.mounted ]

                    if not self.nand_mode:
                        cmd.insert(1, '-r')

                    proc = Popen(cmd)

                    ret_val = proc.wait()

                    if ret_val == 0:
                        self.log.write('- Mounted partition on ' + self.mounted)

                    else:
                        self.log.write('ERROR: Mounter failed')
                        Thread(target=self.clean, args=(True,)).start()
                        return

                else:
                    self.log.write('ERROR: Mounter failed')
                    Thread(target=self.clean, args=(True,)).start()
                    return

            # Check we are making an unlaunch operation
            Thread(target=self.unlaunch_proc if self.nand_mode else self.extract_nand).start()

        except OSError:
            self.log.write('ERROR: Could not execute ' + exe)
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def extract_nand(self):
        self.log.write('\nExtracting files from NAND...')

        err = False

        # Reset copied files cache
        _path_created.clear()
        try:
            copy_tree(self.mounted, self.sd_path, preserve_mode=0, update=1)

        except:
            self.log.write('ERROR: Extractor failed')
            err = True

        Thread(target=self.unmount_nand, args=(err,)).start()


    ################################################################################################
    def unmount_nand(self, err=False):
        self.log.write('\nUnmounting NAND...')

        try:
            if sysname == 'Windows':
                exe = osfmount

                proc = Popen([ osfmount, '-D', '-m', self.mounted ])

            elif sysname == 'Darwin':
                exe = 'hdiutil'

                proc = Popen([ exe, 'detach', self.raw_disk ])

            else:  # Linux
                exe = 'umount'

                proc = Popen([ exe, self.mounted ])

                ret_val = proc.wait()

                if ret_val == 0:
                    exe = 'losetup'

                    proc = Popen([ exe, '-d', self.loop_dev ])

                else:
                    self.log.write('ERROR: Unmounter failed')
                    Thread(target=self.clean, args=(True,)).start()
                    return

            ret_val = proc.wait()

            if ret_val == 0:
                if err:
                    Thread(target=self.clean, args=(True,)).start()

                else:
                    Thread(target=self.encrypt_nand if self.nand_mode
                        else self.get_launcher).start()

            else:
                self.log.write('ERROR: Unmounter failed')
                Thread(target=self.clean, args=(True,)).start()

        except OSError:
            self.log.write('ERROR: Could not execute ' + exe)
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def get_launcher(self):
        app = self.detect_region()

        # Stop if no supported region was found
        if not app:
            self.files.append('bootloader.nds')
            Thread(target=self.clean, args=(True,)).start()
            return

        # Check if unlaunch was installed on the NAND dump
        tmd = path.join(self.sd_path, 'title', '00030017', app, 'content', 'title.tmd')

        if path.getsize(tmd) > 520:
            self.log.write('- WARNING: Unlaunch installed on the NAND dump')

        # Delete title.tmd in case it does not get overwritten
        remove(tmd)

        try:
            self.log.write('\nDownloading ' + self.launcher_region + ' launcher...')

            urlretrieve('https://raw.githubusercontent.com/mondul/HiyaCFW-Helper/master/'
                'launchers/' + self.launcher_region, self.launcher_region)

            self.log.write('- Decrypting launcher...')

            exe = path.join(sysname, '7za')

            proc = Popen([ exe, 'x', '-bso0', '-y', '-p' + app, self.launcher_region,
                '00000002.app' ])

            ret_val = proc.wait()

            if ret_val == 0:
                self.files.append(self.launcher_region)

                # Hash 00000002.app
                sha1_hash = sha1()

                with open('00000002.app', 'rb') as f:
                    sha1_hash.update(f.read())

                self.log.write('- Patched launcher SHA1:\n  ' +
                    sha1_hash.digest().hex().upper())

                Thread(target=self.install_hiyacfw, args=(path.join(self.sd_path, 'title',
                    '00030017', app, 'content', '00000002.app'),)).start()

            else:
                self.log.write('ERROR: Extractor failed')
                Thread(target=self.clean, args=(True,)).start()

        except IOError:
            self.log.write('ERROR: Could not download ' + self.launcher_region + ' launcher')
            Thread(target=self.clean, args=(True,)).start()

        except OSError:
            self.log.write('ERROR: Could not execute ' + exe)
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def install_hiyacfw(self, launcher_path):
        self.log.write('\nCopying HiyaCFW files...')

        copy_tree('for SDNAND SD card', self.sd_path, update=1)
        move('bootloader.nds', path.join(self.sd_path, 'hiya', 'bootloader.nds'))
        move('00000002.app', launcher_path)

        Thread(target=self.get_latest_twilight if self.twilight.get() == 1 else self.clean).start()


    ################################################################################################
    def get_latest_twilight(self):
        filename = 'TWiLightMenu.7z'

        try:
            self.log.write('\nDownloading latest TWiLight Menu++ release...')

            conn = urlopen('https://api.github.com/repos/RocketRobz/TWiLightMenu/releases/'
                'latest')
            latest = jsonify(conn)
            conn.close()

            urlretrieve(latest['assets'][0]['browser_download_url'], filename)

            self.log.write('- Extracting ' + filename[:-3] + ' archive...')

            exe = path.join(sysname, '7za')

            proc = Popen([ exe, 'x', '-bso0', '-y', filename, '_nds', 'DSi - CFW users',
                'DSi&3DS - SD card users', 'roms' ])

            ret_val = proc.wait()

            if ret_val == 0:
                self.files.append(filename)
                self.folders.append('DSi - CFW users')
                self.folders.append('DSi&3DS - SD card users')
                Thread(target=self.install_twilight, args=(filename[:-3],)).start()

            else:
                self.log.write('ERROR: Extractor failed')
                Thread(target=self.clean, args=(True,)).start()

        except (URLError, IOError) as e:
            self.log.write('ERROR: Could not get TWiLight Menu++')
            Thread(target=self.clean, args=(True,)).start()

        except OSError:
            self.log.write('ERROR: Could not execute ' + exe)
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def install_twilight(self, name):
        self.log.write('\nCopying ' + name + ' files...')

        copy_tree(path.join('DSi - CFW users', 'SDNAND root'), self.sd_path, update=1)
        move('_nds', path.join(self.sd_path, '_nds'))
        copy_tree('DSi&3DS - SD card users', self.sd_path, update=1)
        move('roms', path.join(self.sd_path, 'roms'))

        # Set files as read-only
        twlcfg0 = path.join(self.sd_path, 'shared1', 'TWLCFG0.dat')
        twlcfg1 = path.join(self.sd_path, 'shared1', 'TWLCFG1.dat')

        if sysname == 'Darwin':
            Popen([ 'chflags', 'uchg', twlcfg0, twlcfg1 ]).wait()

        elif sysname == 'Linux':
            Popen([ path.join('Linux', 'fatattr'), '+R', twlcfg0, twlcfg1 ]).wait()

        else:
            chmod(twlcfg0, 292)
            chmod(twlcfg1, 292)

        Thread(target=self.clean).start()


    ################################################################################################
    def clean(self, err=False):
        self.log.write('\nCleaning...')

        while len(self.folders) > 0:
            rmtree(self.folders.pop(), ignore_errors=True)

        while len(self.files) > 0:
            try:
                remove(self.files.pop())

            except:
                pass

        if err:
            self.log.write('Done')
            return

        # Get logged user in Linux
        if sysname == 'Linux':
            from os import getlogin

            # Workaround for some Linux systems where this function does not work
            try:
                ug = getlogin()

            except OSError:
                ug = 'root'

        if (self.nand_mode):
            file = self.console_id.get() + self.suffix + '.bin'

            rename(self.console_id.get() + '.img', file)

            # Change owner of the file in Linux
            if sysname == 'Linux':
                Popen([ 'chown', '-R', ug + ':' + ug, file ]).wait()

            self.log.write('\nDone!\nModified NAND stored as\n' + file)
            return

        # Change owner of the out folder in Linux
        if sysname == 'Linux':
            Popen([ 'chown', '-R', ug + ':' + ug, self.sd_path ]).wait()

        self.log.write('Done!\nExtract your SD card and insert it into your DSi')


    ################################################################################################
    def patcher(self, patchpath, filepath):
        patch_size = path.getsize(patchpath)

        patchfile = open(patchpath, 'rb')

        if patchfile.read(5) != b'PATCH':
            patchfile.close()
            raise Exception()

        target = open(filepath, 'r+b')

        # Read First Record
        r = patchfile.read(3)

        while patchfile.tell() not in [ patch_size, patch_size - 3 ]:
            # Unpack 3-byte pointers.
            offset = self.unpack_int(r)
            # Read size of data chunk
            r = patchfile.read(2)
            size = self.unpack_int(r)

            if size == 0:  # RLE Record
                r = patchfile.read(2)
                rle_size = self.unpack_int(r)
                data = patchfile.read(1) * rle_size

            else:
                data = patchfile.read(size)

            # Write to file
            target.seek(offset)
            target.write(data)
            # Read Next Record
            r = patchfile.read(3)

        if patch_size - 3 == patchfile.tell():
            trim_size = self.unpack_int(patchfile.read(3))
            target.truncate(trim_size)

        # Cleanup
        target.close()
        patchfile.close()


    ################################################################################################
    def unpack_int(self, bstr):
        # Read an n-byte big-endian integer from a byte string
        ( ret_val, ) = unpack_from('>I', b'\x00' * (4 - len(bstr)) + bstr)
        return ret_val


    ################################################################################################
    def detect_region(self):
        REGION_CODES = {
            '484e4145': 'USA',
            '484e414a': 'JAP',
            '484e4150': 'EUR',
            '484e4155': 'AUS'
        }

        # Autodetect console region
        base = self.mounted if self.nand_mode else self.sd_path

        try:
            for app in listdir(path.join(base, 'title', '00030017')):
                for file in listdir(path.join(base, 'title', '00030017', app, 'content')):
                    if file.endswith('.app'):
                        try:
                            self.log.write('- Detected ' + REGION_CODES[app] + ' console NAND dump')
                            self.launcher_region = REGION_CODES[app]
                            return app

                        except KeyError:
                            self.log.write('ERROR: Unsupported console region')
                            return False

            self.log.write('ERROR: Could not detect console region')

        except OSError as e:
            self.log.write('ERROR: ' + e.strerror + ': ' + e.filename)

        return False


    ################################################################################################
    def unlaunch_proc(self):
        self.log.write('\nChecking unlaunch status...')

        app = self.detect_region()

        # Stop if no supported region was found
        if not app:
            # TODO: Unmount NAND
            return

        tmd = path.join(self.mounted, 'title', '00030017', app, 'content', 'title.tmd')

        tmd_size = path.getsize(tmd)

        if tmd_size == 520:
            self.log.write('- Not installed. Downloading v1.4...')

            try:
                filename = urlretrieve('http://problemkaputt.de/unlau14.zip')[0]

                exe = path.join(sysname, '7za')

                proc = Popen([ exe, 'x', '-bso0', '-y', filename, 'UNLAUNCH.DSI' ])

                ret_val = proc.wait()

                if ret_val == 0:
                    self.files.append(filename)
                    self.files.append('UNLAUNCH.DSI')

                    self.log.write('- Installing unlaunch...')

                    self.suffix = '-unlaunch'

                    with open(tmd, 'ab') as f:
                        with open('UNLAUNCH.DSI', 'rb') as unl:
                            f.write(unl.read())

                    # Set files as read-only
                    for file in listdir(path.join(self.mounted, 'title', '00030017', app,
                        'content')):
                        file = path.join(self.mounted, 'title', '00030017', app, 'content', file)

                        if sysname == 'Darwin':
                            Popen([ 'chflags', 'uchg', file ]).wait()

                        elif sysname == 'Linux':
                            Popen([ path.join('Linux', 'fatattr'), '+R', file ]).wait()
   
                        else:
                            chmod(file, 292)

                else:
                    self.log.write('ERROR: Extractor failed')
                    # TODO: Unmount NAND


            except IOError:
                self.log.write('ERROR: Could not get unlaunch')
                # TODO: Unmount NAND

            except OSError:
                self.log.write('ERROR: Could not execute ' + exe)
                # TODO: Unmount NAND

        else:
            self.log.write('- Installed. Uninstalling...')

            self.suffix = '-no-unlaunch'

            # Set files as read-write
            for file in listdir(path.join(self.mounted, 'title', '00030017', app, 'content')):
                file = path.join(self.mounted, 'title', '00030017', app, 'content', file)

                if sysname == 'Darwin':
                    Popen([ 'chflags', 'nouchg', file ]).wait()

                elif sysname == 'Linux':
                    Popen([ path.join('Linux', 'fatattr'), '-R', file ]).wait()
   
                else:
                    chmod(file, 438)

            with open(tmd, 'r+b') as f:
                f.truncate(520)

        Thread(target=self.unmount_nand).start()


    ################################################################################################
    def encrypt_nand(self):
        self.log.write('\nEncrypting back NAND...')

        exe = path.join(sysname, 'twltool')

        try:
            proc = Popen([ exe, 'nandcrypt', '--in', self.console_id.get() + '.img' ])

            ret_val = proc.wait()

            if ret_val == 0:
                Thread(target=self.clean).start()

            else:
                self.log.write('ERROR: Encryptor failed')

        except OSError:
            self.log.write('ERROR: Could not execute ' + exe)


    ################################################################################################
    def remove_footer(self):
        self.log.write('\nRemoving No$GBA footer...')

        file = self.console_id.get() + '-no-footer.bin'

        try:
            copyfile(self.nand_file.get(), file)

            # Back-up footer info
            with open(self.console_id.get() + '-info.txt', 'wb') as f:
                f.write('eMMC CID: ' + self.cid.get() + '\r\n')
                f.write('Console ID: ' + self.console_id.get() + '\r\n')

            with open(file, 'r+b') as f:
                # Go to the No$GBA footer offset
                f.seek(-64, 2)
                # Remove footer
                f.truncate()

            # Change owner of the file in Linux
            if sysname == 'Linux':
                from os import getlogin

                ug = getlogin()

                Popen([ 'chown', '-R', ug + ':' + ug, file ]).wait()

            self.log.write('\nDone!\nModified NAND stored as\n' + file +
                '\nStored footer info in ' + self.console_id.get() + '-info.txt')

        except IOError:
            self.log.write('ERROR: Could not open the file ' +
                path.basename(self.nand_file.get()))


    ################################################################################################
    def add_footer(self, cid, console_id):
        self.log.write('Adding No$GBA footer...')

        file = self.console_id.get() + '-footer.bin'

        try:
            copyfile(self.nand_file.get(), file)

            with open(file, 'r+b') as f:
                # Go to the No$GBA footer offset
                f.seek(-64, 2)
                # Read the footer's header :-)
                bstr = f.read(0x10)

                # Check if it already has a footer
                if bstr == b'DSi eMMC CID/CPU':
                    self.log.write('ERROR: File already has a No$GBA footer')
                    f.close()
                    remove(file)
                    return;

                # Go to the end of file
                f.seek(0, 2)
                # Write footer
                f.write(b'DSi eMMC CID/CPU')
                f.write(cid)
                f.write(console_id)
                f.write('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
                    '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')

            self.log.write('\nDone!\nModified NAND stored as\n' + file)

        except IOError:
            self.log.write('ERROR: Could not open the file ' +
                path.basename(self.nand_file.get()))
Exemplo n.º 3
0
class Application(Frame):
    def __init__(self, master=None):
        super().__init__(master)

        self.pack()

        # First row
        f1 = LabelFrame(self, text='NAND file with No$GBA footer', padx=10, pady=10)

        # NAND Button
        self.nand_mode = False

        nand_icon = PhotoImage(data=('R0lGODlhEAAQAIMAAAAAADMzM2ZmZpmZmczMzP///wAAAAAAAAA'
            'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAMAAAYALAAAAAAQAB'
            'AAAARG0MhJaxU4Y2sECAEgikE1CAFRhGMwSMJwBsU6frIgnR/bv'
            'hTPrWUSDnGw3JGU2xmHrsvyU5xGO8ql6+S0AifPW8kCKpcpEQA7'))

        self.nand_button = Button(f1, image=nand_icon, command=self.change_mode, state=DISABLED)
        self.nand_button.image = nand_icon

        self.nand_button.pack(side='left')

        self.nand_file = StringVar()
        Entry(f1, textvariable=self.nand_file, state='readonly', width=40).pack(side='left')

        Button(f1, text='...', command=self.choose_nand).pack(side='left')

        f1.pack(padx=10, pady=10, fill=X)

        # Second row
        f2 = Frame(self)

        # Check box
        self.twilight = IntVar()
        self.twilight.set(1)

        self.chk = Checkbutton(f2, text='Install latest TWiLight Menu++ on custom firmware',
            variable=self.twilight)

        self.chk.pack(padx=10, anchor=W)

        # NAND operation frame
        self.nand_frame = LabelFrame(f2, text='NAND operation', padx=10, pady=10)

        self.nand_operation = IntVar()
        self.nand_operation.set(0)

        Radiobutton(self.nand_frame, text='Uninstall unlaunch or install v1.4 stable',
            variable=self.nand_operation, value=0,
            command=lambda: self.enable_entries(False)).pack(anchor=W)

        Radiobutton(self.nand_frame, text='Remove No$GBA footer', variable=self.nand_operation,
            value=1, command=lambda: self.enable_entries(False)).pack(anchor=W)

        Radiobutton(self.nand_frame, text='Add No$GBA footer', variable=self.nand_operation,
            value=2, command=lambda: self.enable_entries(True)).pack(anchor=W)

        fl = Frame(self.nand_frame)

        self.cid_label = Label(fl, text='eMMC CID', state=DISABLED)
        self.cid_label.pack(anchor=W, padx=(24, 0))

        self.cid = StringVar()
        self.cid_entry = Entry(fl, textvariable=self.cid, width=20, state=DISABLED)
        self.cid_entry.pack(anchor=W, padx=(24, 0))

        fl.pack(side='left')

        fr = Frame(self.nand_frame)

        self.console_id_label = Label(fr, text='Console ID', state=DISABLED)
        self.console_id_label.pack(anchor=W)

        self.console_id = StringVar()
        self.console_id_entry = Entry(fr, textvariable=self.console_id, width=20, state=DISABLED)
        self.console_id_entry.pack(anchor=W)

        fr.pack(side='right')

        f2.pack(fill=X)

        # Third row
        f3 = Frame(self)

        self.start_button = Button(f3, text='Start', width=16, command=self.hiya, state=DISABLED)
        self.start_button.pack(side='left', padx=(0, 5))

        Button(f3, text='Quit', command=root.destroy, width=16).pack(side='left', padx=(5, 0))

        f3.pack(pady=(10, 20))

        self.folders = []
        self.files = []


    ################################################################################################
    def change_mode(self):
        if (self.nand_mode):
            self.nand_frame.pack_forget()
            self.chk.pack(padx=10, anchor=W)
            self.nand_mode = False

        else:
            if askokcancel('Warning', ('You are about to enter NAND mode. Do it only if you know '
                'what you are doing. Proceed?'), icon=WARNING):
                self.chk.pack_forget()
                self.nand_frame.pack(padx=10, pady=(0, 10), fill=X)
                self.nand_mode = True


    ################################################################################################
    def enable_entries(self, status):
        self.cid_label['state'] = (NORMAL if status else DISABLED)
        self.cid_entry['state'] = (NORMAL if status else DISABLED)
        self.console_id_label['state'] = (NORMAL if status else DISABLED)
        self.console_id_entry['state'] = (NORMAL if status else DISABLED)


    ################################################################################################
    def choose_nand(self):
        name = askopenfilename(filetypes=( ( 'nand.bin', '*.bin' ), ( 'DSi-1.mmc', '*.mmc' ) ))
        self.nand_file.set(name)

        self.nand_button['state'] = (NORMAL if name != '' else DISABLED)
        self.start_button['state'] = (NORMAL if name != '' else DISABLED)


    ################################################################################################
    def hiya(self):
        if not self.nand_mode:
            showinfo('Info', 'Now you will be asked to choose the SD card path that will be used '
                'for installing the custom firmware (or an output folder).\n\nIn order to avoid '
                'boot errors please assure it is empty before continuing.')
            self.sd_path = askdirectory()

            # Exit if no path was selected
            if self.sd_path == '':
                return

        # If adding a No$GBA footer, check if CID and ConsoleID values are OK
        elif self.nand_operation.get() == 2:
            cid = self.cid.get()
            console_id = self.console_id.get()

            # Check lengths
            if len(cid) != 32:
                showerror('Error', 'Bad eMMC CID')
                return

            elif len(console_id) != 16:
                showerror('Error', 'Bad Console ID')
                return

            # Parse strings to hex
            try:
                cid = bytearray.fromhex(cid)

            except ValueError:
                showerror('Error', 'Bad eMMC CID')
                return

            try:
                console_id = bytearray(reversed(bytearray.fromhex(console_id)))

            except ValueError:
                showerror('Error', 'Bad Console ID')
                return

        dialog = Toplevel(self)
        # Open as dialog (parent disabled)
        dialog.grab_set()
        dialog.title('Status')
        # Disable maximizing
        dialog.resizable(0, 0)

        frame = Frame(dialog, bd=2, relief=SUNKEN)

        scrollbar = Scrollbar(frame)
        scrollbar.pack(side=RIGHT, fill=Y)

        self.log = ThreadSafeText(frame, bd=0, width=52, height=20,
            yscrollcommand=scrollbar.set)
        self.log.pack()

        scrollbar.config(command=self.log.yview)

        frame.pack()

        Button(dialog, text='Close', command=dialog.destroy, width=16).pack(pady=10)

        # Center in window
        dialog.update_idletasks()
        width = dialog.winfo_width()
        height = dialog.winfo_height()
        dialog.geometry('%dx%d+%d+%d' % (width, height, root.winfo_x() + (root.winfo_width() / 2) -
            (width / 2), root.winfo_y() + (root.winfo_height() / 2) - (height / 2)))

        # Check if we'll be adding a No$GBA footer
        if self.nand_mode and self.nand_operation.get() == 2:
            Thread(target=self.add_footer, args=(cid, console_id)).start()

        else:
            Thread(target=self.check_nand).start()


    ################################################################################################
    def check_nand(self):
        self.log.write('Checking NAND file...')

        # Read the NAND file
        try:
            with open(self.nand_file.get(), 'rb') as f:
                # Go to the No$GBA footer offset
                f.seek(-64, 2)
                # Read the footer's header :-)
                bstr = f.read(0x10)

                if bstr == b'DSi eMMC CID/CPU':
                    # Read the CID
                    bstr = f.read(0x10)
                    self.cid.set(bstr.hex().upper())
                    self.log.write('- eMMC CID: ' + self.cid.get())

                    # Read the console ID
                    bstr = f.read(8)
                    self.console_id.set(bytearray(reversed(bstr)).hex().upper())
                    self.log.write('- Console ID: ' + self.console_id.get())

                    # Check we are making an unlaunch operation or removing the No$GBA footer
                    if self.nand_mode:
                        if self.nand_operation.get() == 0:
                            Thread(target=self.decrypt_nand).start()

                        else:
                            Thread(target=self.remove_footer).start()
                            pass

                    else:
                        Thread(target=self.get_latest_hiyacfw).start()

                else:
                    self.log.write('ERROR: No$GBA footer not found')

        except IOError as e:
            print(e)
            self.log.write('ERROR: Could not open the file ' +
                path.basename(self.nand_file.get()))


    ################################################################################################
    def get_latest_hiyacfw(self):
        # Try to use already downloaded HiyaCFW archive
        filename = 'HiyaCFW.7z'

        try:
            if path.isfile(filename):
                self.log.write('\nPreparing HiyaCFW...')

            else:
                self.log.write('\nDownloading latest HiyaCFW release...')

                conn = urlopen('https://api.github.com/repos/RocketRobz/hiyaCFW/releases/latest')
                latest = jsonify(conn)
                conn.close()

                with urlopen(latest['assets'][0]
                    ['browser_download_url']) as src, open(filename, 'wb') as dst:
                    copyfileobj(src, dst)

            self.log.write('- Extracting HiyaCFW archive...')

            exe = path.join(sysname, '7za')

            proc = Popen([ exe, 'x', '-bso0', '-y', filename, 'for PC', 'for SDNAND SD card' ])

            ret_val = proc.wait()

            if ret_val == 0:
                self.files.append(filename)
                self.folders.append('for PC')
                self.folders.append('for SDNAND SD card')
                # Got to decrypt NAND if bootloader.nds is present
                Thread(target=self.decrypt_nand if path.isfile('bootloader.nds')
                    else self.extract_bios).start()

            else:
                self.log.write('ERROR: Extractor failed')

        except (URLError, IOError) as e:
            print(e)
            self.log.write('ERROR: Could not get HiyaCFW')

        except OSError as e:
            print(e)
            self.log.write('ERROR: Could not execute ' + exe)


    ################################################################################################
    def extract_bios(self):
        self.log.write('\nExtracting ARM7/ARM9 BIOS from NAND...')

        exe = path.join(sysname, 'twltool')

        try:
            proc = Popen([ exe, 'boot2', '--in', self.nand_file.get() ])

            ret_val = proc.wait()

            if ret_val == 0:
                # Hash arm7.bin
                sha1_hash = sha1()

                with open('arm7.bin', 'rb') as f:
                    sha1_hash.update(f.read())

                self.log.write('- arm7.bin SHA1:\n  ' +
                    sha1_hash.digest().hex().upper())

                # Hash arm9.bin
                sha1_hash = sha1()

                with open('arm9.bin', 'rb') as f:
                    sha1_hash.update(f.read())

                self.log.write('- arm9.bin SHA1:\n  ' +
                    sha1_hash.digest().hex().upper())

                self.files.append('arm7.bin')
                self.files.append('arm9.bin')

                Thread(target=self.patch_bios).start()

            else:
                self.log.write('ERROR: Extractor failed')
                Thread(target=self.clean, args=(True,)).start()

        except OSError as e:
            print(e)
            self.log.write('ERROR: Could not execute ' + exe)
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def patch_bios(self):
        self.log.write('\nPatching ARM7/ARM9 BIOS...')

        try:
            self.patcher(path.join('for PC', 'bootloader files', 'bootloader arm7 patch.ips'),
                'arm7.bin')

            self.patcher(path.join('for PC', 'bootloader files', 'bootloader arm9 patch.ips'),
                'arm9.bin')

            # Hash arm7.bin
            sha1_hash = sha1()

            with open('arm7.bin', 'rb') as f:
                sha1_hash.update(f.read())

            self.log.write('- Patched arm7.bin SHA1:\n  ' +
                sha1_hash.digest().hex().upper())

            # Hash arm9.bin
            sha1_hash = sha1()

            with open('arm9.bin', 'rb') as f:
                sha1_hash.update(f.read())

            self.log.write('- Patched arm9.bin SHA1:\n  ' +
                sha1_hash.digest().hex().upper())

            Thread(target=self.arm9_prepend).start()

        except IOError as e:
            print(e)
            self.log.write('ERROR: Could not patch BIOS')
            Thread(target=self.clean, args=(True,)).start()

        except Exception as e:
            print(e)
            self.log.write('ERROR: Invalid patch header')
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def arm9_prepend(self):
        self.log.write('\nPrepending data to ARM9 BIOS...')

        try:
            with open('arm9.bin', 'rb') as f:
                data = f.read()

            with open('arm9.bin', 'wb') as f:
                with open(path.join('for PC', 'bootloader files',
                    'bootloader arm9 append to start.bin'), 'rb') as pre:
                    f.write(pre.read())

                f.write(data)

            # Hash arm9.bin
            sha1_hash = sha1()

            with open('arm9.bin', 'rb') as f:
                sha1_hash.update(f.read())

            self.log.write('- Prepended arm9.bin SHA1:\n  ' +
                sha1_hash.digest().hex().upper())

            Thread(target=self.make_bootloader).start()

        except IOError as e:
            print(e)
            self.log.write('ERROR: Could not prepend data to ARM9 BIOS')
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def make_bootloader(self):
        self.log.write('\nGenerating new bootloader...')

        exe = (path.join('for PC', 'bootloader files', 'ndstool') if sysname == 'Windows' else
            path.join(sysname, 'ndsblc'))

        try:
            proc = Popen([ exe, '-c', 'bootloader.nds', '-9', 'arm9.bin', '-7', 'arm7.bin', '-t',
                path.join('for PC', 'bootloader files', 'banner.bin'), '-h',
                path.join('for PC', 'bootloader files', 'header.bin') ])

            ret_val = proc.wait()

            if ret_val == 0:
                # Hash bootloader.nds
                sha1_hash = sha1()

                with open('bootloader.nds', 'rb') as f:
                    sha1_hash.update(f.read())

                self.log.write('- bootloader.nds SHA1:\n  ' +
                    sha1_hash.digest().hex().upper())

                Thread(target=self.decrypt_nand).start()

            else:
                self.log.write('ERROR: Generator failed')
                Thread(target=self.clean, args=(True,)).start()

        except OSError as e:
            print(e)
            self.log.write('ERROR: Could not execute ' + exe)
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def decrypt_nand(self):
        self.log.write('\nDecrypting NAND...')

        exe = path.join(sysname, 'twltool')

        try:
            proc = Popen([ exe, 'nandcrypt', '--in', self.nand_file.get(), '--out',
                self.console_id.get() + '.img' ])

            ret_val = proc.wait()
            print("\n")

            if ret_val == 0:
                if not self.nand_mode:
                    self.files.append(self.console_id.get() + '.img')

                Thread(target=self.mount_nand).start()

            else:
                self.log.write('ERROR: Decryptor failed')
                Thread(target=self.clean, args=(True,)).start()

        except OSError as e:
            print(e)
            self.log.write('ERROR: Could not execute ' + exe)
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def mount_nand(self):
        self.log.write('\nMounting decrypted NAND...')

        try:
            if sysname == 'Windows':
                exe = osfmount

                cmd = [ osfmount, '-a', '-t', 'file', '-f', self.console_id.get() + '.img', '-m',
                    '#:', '-o', 'ro,rem' ]

                if self.nand_mode:
                    cmd[-1] = 'rw,rem'

                proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)

                outs, errs = proc.communicate()

                if proc.returncode == 0:
                    self.mounted = search(r'[a-zA-Z]:\s', outs.decode('utf-8')).group(0).strip()
                    self.log.write('- Mounted on drive ' + self.mounted)

                else:
                    self.log.write('ERROR: Mounter failed')
                    Thread(target=self.clean, args=(True,)).start()
                    return

            elif sysname == 'Darwin':
                exe = 'hdiutil'

                cmd = [ exe, 'attach', '-imagekey', 'diskimage-class=CRawDiskImage', '-nomount',
                    self.console_id.get() + '.img' ]

                if not self.nand_mode:
                    cmd.insert(2, '-readonly')

                proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)

                outs, errs = proc.communicate()

                if proc.returncode == 0:
                    self.raw_disk = search(r'^\/dev\/disk\d+', outs.decode('utf-8')).group(0)
                    self.log.write('- Mounted raw disk on ' + self.raw_disk)

                    cmd = [ exe, 'mount', self.raw_disk + 's1' ]

                    if not self.nand_mode:
                        cmd.insert(2, '-readonly')

                    proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)

                    outs, errs = proc.communicate()

                    if proc.returncode == 0:
                        self.mounted = search(r'\/Volumes\/.+', outs.decode('utf-8')).group(0)
                        self.log.write('- Mounted volume on ' + self.mounted)

                    else:
                        self.log.write('ERROR: Mounter failed')
                        Thread(target=self.clean, args=(True,)).start()
                        return

                else:
                    self.log.write('ERROR: Mounter failed')
                    Thread(target=self.clean, args=(True,)).start()
                    return

            else:  # Linux
                exe = 'losetup'

                cmd = [ exe, '-P', '-f', '--show', self.console_id.get() + '.img' ]

                if not self.nand_mode:
                    cmd.insert(2, '-r')

                proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
                outs, errs = proc.communicate()

                if proc.returncode == 0:
                    self.loop_dev = search(r'\/dev\/loop\d+', outs.decode('utf-8')).group(0)
                    self.log.write('- Mounted loop device on ' + self.loop_dev)

                    exe = 'mount'

                    self.mounted = '/mnt'

                    cmd = [ exe, '-t', 'vfat', self.loop_dev + 'p1', self.mounted ]

                    if not self.nand_mode:
                        cmd.insert(1, '-r')

                    proc = Popen(cmd)

                    ret_val = proc.wait()

                    if ret_val == 0:
                        self.log.write('- Mounted partition on ' + self.mounted)

                    else:
                        self.log.write('ERROR: Mounter failed')
                        Thread(target=self.clean, args=(True,)).start()
                        return

                else:
                    self.log.write('ERROR: Mounter failed')
                    Thread(target=self.clean, args=(True,)).start()
                    return

            # Check we are making an unlaunch operation
            Thread(target=self.unlaunch_proc if self.nand_mode else self.extract_nand).start()

        except OSError as e:
            print(e)
            self.log.write('ERROR: Could not execute ' + exe)
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def extract_nand(self):
        self.log.write('\nExtracting files from NAND...')

        err = False

        # Reset copied files cache
        _path_created.clear()
        try:
            copy_tree(self.mounted, self.sd_path, preserve_mode=0, update=1)

        except Exception as e:
            print(e)
            self.log.write('ERROR: Extractor failed')
            err = True

        Thread(target=self.unmount_nand, args=(err,)).start()


    ################################################################################################
    def unmount_nand(self, err=False):
        self.log.write('\nUnmounting NAND...')

        try:
            if sysname == 'Windows':
                exe = osfmount

                proc = Popen([ osfmount, '-D', '-m', self.mounted ])

            elif sysname == 'Darwin':
                exe = 'hdiutil'

                proc = Popen([ exe, 'detach', self.raw_disk ])

            else:  # Linux
                exe = 'umount'

                proc = Popen([ exe, self.mounted ])

                ret_val = proc.wait()

                if ret_val == 0:
                    exe = 'losetup'

                    proc = Popen([ exe, '-d', self.loop_dev ])

                else:
                    self.log.write('ERROR: Unmounter failed')
                    Thread(target=self.clean, args=(True,)).start()
                    return

            ret_val = proc.wait()

            if ret_val == 0:
                if err:
                    Thread(target=self.clean, args=(True,)).start()

                else:
                    Thread(target=self.encrypt_nand if self.nand_mode
                        else self.get_launcher).start()

            else:
                self.log.write('ERROR: Unmounter failed')
                Thread(target=self.clean, args=(True,)).start()

        except OSError as e:
            print(e)
            self.log.write('ERROR: Could not execute ' + exe)
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def get_launcher(self):
        app = self.detect_region()

        # Stop if no supported region was found
        if not app:
            self.files.append('bootloader.nds')
            Thread(target=self.clean, args=(True,)).start()
            return

        # Check if unlaunch was installed on the NAND dump
        tmd = path.join(self.sd_path, 'title', '00030017', app, 'content', 'title.tmd')

        if path.getsize(tmd) > 520:
            self.log.write('- WARNING: Unlaunch installed on the NAND dump')

        # Delete title.tmd in case it does not get overwritten
        remove(tmd)

        # Try to use already downloaded launcher
        try:
            if path.isfile(self.launcher_region):
                self.log.write('\nPreparing ' + self.launcher_region + ' launcher...')

            else:
                self.log.write('\nDownloading ' + self.launcher_region + ' launcher...')

                with urlopen('https://raw.githubusercontent.com'
                    '/mondul/HiyaCFW-Helper/master/launchers/' +
                    self.launcher_region) as src, open(self.launcher_region, 'wb') as dst:
                    copyfileobj(src, dst)

            self.log.write('- Decrypting launcher...')

            exe = path.join(sysname, '7za')

            proc = Popen([ exe, 'x', '-bso0', '-y', '-p' + app, self.launcher_region,
                '00000002.app' ])

            ret_val = proc.wait()

            if ret_val == 0:
                self.files.append(self.launcher_region)

                # Hash 00000002.app
                sha1_hash = sha1()

                with open('00000002.app', 'rb') as f:
                    sha1_hash.update(f.read())

                self.log.write('- Patched launcher SHA1:\n  ' +
                    sha1_hash.digest().hex().upper())

                Thread(target=self.install_hiyacfw, args=(path.join(self.sd_path, 'title',
                    '00030017', app, 'content', '00000002.app'),)).start()

            else:
                self.log.write('ERROR: Extractor failed')
                Thread(target=self.clean, args=(True,)).start()

        except IOError as e:
            print(e)
            self.log.write('ERROR: Could not download ' + self.launcher_region + ' launcher')
            Thread(target=self.clean, args=(True,)).start()

        except OSError as e:
            print(e)
            self.log.write('ERROR: Could not execute ' + exe)
            Thread(target=self.clean, args=(True,)).start()


    ################################################################################################
    def install_hiyacfw(self, launcher_path):
        self.log.write('\nCopying HiyaCFW files...')

        copy_tree('for SDNAND SD card', self.sd_path, update=1)
        move('bootloader.nds', path.join(self.sd_path, 'hiya', 'bootloader.nds'))
        move('00000002.app', launcher_path)

        Thread(target=self.get_latest_twilight if self.twilight.get() == 1 else self.clean).start()


    ################################################################################################
    def get_latest_twilight(self):
        filename = 'TWiLightMenu.7z'

        try:
            self.log.write('\nDownloading latest TWiLight Menu++ release...')

            conn = urlopen('https://api.github.com/repos/DS-Homebrew/TWiLightMenu/releases/'
                'latest')
            latest = jsonify(conn)
            conn.close()

                with urlopen(latest['assets'][0]
                    ['browser_download_url']) as src, open(filename, 'wb') as dst:
                    copyfileobj(src, dst)

            self.log.write('- Extracting ' + filename[:-3] + ' archive...')

            exe = path.join(sysname, '7za')

            proc = Popen([ exe, 'x', '-bso0', '-y', filename, '_nds', 'DSi - CFW users',
                'DSi&3DS - SD card users', 'roms' ])

            ret_val = proc.wait()

            if ret_val == 0:
                self.files.append(filename)
                self.folders.append('DSi - CFW users')
                self.folders.append('DSi&3DS - SD card users')
                Thread(target=self.install_twilight, args=(filename[:-3],)).start()

            else:
                self.log.write('ERROR: Extractor failed')
                Thread(target=self.clean, args=(True,)).start()

        except (URLError, IOError) as e:
            print(e)
            self.log.write('ERROR: Could not get TWiLight Menu++')
            Thread(target=self.clean, args=(True,)).start()
Exemplo n.º 4
0
class AnalysisToolsManager:
    def __init__(self, controller):
        self.tab = None
        self.controller = controller
        self.tk_format = utils.TkFormat(self.controller.config_info)

        self.analysis_dialog = None
        self.exclude_artifacts = IntVar()
        self.abs_val = IntVar()
        self.use_max_for_centers = IntVar()
        self.use_delta = IntVar()
        self.neg_depth = IntVar()

        self.normalize_entry = None
        self.right_zoom_entry = None
        self.right_zoom_entry2 = None
        self.left_zoom_entry = None
        self.left_zoom_entry2 = None
        self.left_slope_entry = None
        self.right_slope_entry = None
        self.slopes_listbox = None
        self.abs_val_check = None
        self.use_max_for_centers_check = None
        self.use_delta_check = None
        self.neg_depth_check = None
        self.exclude_artifacts_check = None
        self.extra_analysis_check_frame = None
        self.plot_slope_var = None
        self.offset_entry = None
        self.offset_sample_var = None
        self.plot_slope_menu = None
        self.plot_slope_button = None

        self.analyze_var = None

        self.outer_slope_frame = None
        self.slope_results_frame = None

    def show(self, tab):
        self.tab = tab
        self.tab.freeze(
        )  # You have to finish dealing with this before, say, opening another analysis box.
        buttons = {
            "reset": {
                self.select_tab: [],
                self.tab.reset: [],
                self.uncheck_exclude_artifacts: [],
                self.disable_plot: [],
                # utils.thread_lift_widget: [],
            },
            "close": {},
        }
        self.analysis_dialog = VerticalScrolledDialog(self.controller,
                                                      "Analyze Data",
                                                      "",
                                                      buttons=buttons,
                                                      button_width=13)
        self.analysis_dialog.top.attributes("-topmost", True)

        outer_normalize_frame = Frame(self.analysis_dialog.interior,
                                      bg=self.tk_format.bg,
                                      padx=self.tk_format.padx,
                                      pady=15,
                                      highlightthickness=1)
        outer_normalize_frame.pack(expand=True, fill=BOTH)
        slope_title_label = Label(outer_normalize_frame,
                                  text="Normalize:",
                                  bg=self.tk_format.bg,
                                  fg=self.tk_format.textcolor)
        slope_title_label.pack()
        normalize_frame = Frame(outer_normalize_frame,
                                bg=self.tk_format.bg,
                                padx=self.tk_format.padx,
                                pady=15)
        normalize_frame.pack()

        normalize_label = Label(normalize_frame,
                                text="Wavelength (nm):",
                                bg=self.tk_format.bg,
                                fg=self.tk_format.textcolor)
        self.normalize_entry = Entry(
            normalize_frame,
            width=7,
            bd=self.tk_format.bd,
            bg=self.tk_format.entry_background,
            selectbackground=self.tk_format.selectbackground,
            selectforeground=self.tk_format.selectforeground,
        )
        normalize_button = Button(
            normalize_frame,
            text="Apply",
            command=self.normalize,
            width=6,
            fg=self.tk_format.buttontextcolor,
            bg=self.tk_format.buttonbackgroundcolor,
            bd=self.tk_format.bd,
        )
        normalize_button.config(
            fg=self.tk_format.buttontextcolor,
            highlightbackground=self.tk_format.highlightbackgroundcolor,
            bg=self.tk_format.buttonbackgroundcolor,
        )
        normalize_button.pack(side=RIGHT, padx=(10, 10))
        self.normalize_entry.pack(side=RIGHT, padx=self.tk_format.padx)
        normalize_label.pack(side=RIGHT, padx=self.tk_format.padx)

        outer_offset_frame = Frame(self.analysis_dialog.interior,
                                   bg=self.tk_format.bg,
                                   padx=self.tk_format.padx,
                                   pady=15,
                                   highlightthickness=1)
        outer_offset_frame.pack(expand=True, fill=BOTH)
        slope_title_label = Label(outer_offset_frame,
                                  text="Add offset to sample:",
                                  bg=self.tk_format.bg,
                                  fg=self.tk_format.textcolor)
        slope_title_label.pack(pady=(0, 15))
        offset_sample_frame = Frame(outer_offset_frame,
                                    bg=self.tk_format.bg,
                                    padx=self.tk_format.padx,
                                    pady=self.tk_format.pady)
        offset_sample_frame.pack()
        offset_sample_label = Label(offset_sample_frame,
                                    text="Sample: ",
                                    bg=self.tk_format.bg,
                                    fg=self.tk_format.textcolor)
        offset_sample_label.pack(side=LEFT)
        self.offset_sample_var = StringVar()
        sample_names = []
        repeats = False
        max_len = 0
        for sample in self.tab.samples:
            if sample.name in sample_names:
                repeats = True
            else:
                sample_names.append(sample.name)
                max_len = np.max([max_len, len(sample.name)])
        if repeats:
            sample_names = []
            for sample in self.tab.samples:
                sample_names.append(sample.title + ": " + sample.name)
                max_len = np.max([max_len, len(sample_names[-1])])
        self.offset_sample_var.set(sample_names[0])

        # pylint: disable = no-value-for-parameter
        offset_menu = OptionMenu(offset_sample_frame, self.offset_sample_var,
                                 *sample_names)
        offset_menu.configure(
            width=max_len,
            highlightbackground=self.tk_format.highlightbackgroundcolor)
        offset_menu.pack(side=LEFT)
        offset_frame = Frame(outer_offset_frame,
                             bg=self.tk_format.bg,
                             padx=self.tk_format.padx,
                             pady=15)
        offset_frame.pack()
        offset_label = Label(offset_frame,
                             text="Offset:",
                             bg=self.tk_format.bg,
                             fg=self.tk_format.textcolor)
        self.offset_entry = Entry(
            offset_frame,
            width=7,
            bd=self.tk_format.bd,
            bg=self.tk_format.entry_background,
            selectbackground=self.tk_format.selectbackground,
            selectforeground=self.tk_format.selectforeground,
        )
        offset_button = Button(
            offset_frame,
            text="Apply",
            command=self.offset,
            width=6,
            fg=self.tk_format.buttontextcolor,
            bg=self.tk_format.buttonbackgroundcolor,
            bd=self.tk_format.bd,
        )
        offset_button.config(
            fg=self.tk_format.buttontextcolor,
            highlightbackground=self.tk_format.highlightbackgroundcolor,
            bg=self.tk_format.buttonbackgroundcolor,
        )
        offset_button.pack(side=RIGHT, padx=(10, 10))
        self.offset_entry.pack(side=RIGHT, padx=self.tk_format.padx)
        offset_label.pack(side=RIGHT, padx=self.tk_format.padx)

        outer_outer_zoom_frame = Frame(self.analysis_dialog.interior,
                                       bg=self.tk_format.bg,
                                       padx=self.tk_format.padx,
                                       pady=15,
                                       highlightthickness=1)
        outer_outer_zoom_frame.pack(expand=True, fill=BOTH)

        zoom_title_frame = Frame(outer_outer_zoom_frame, bg=self.tk_format.bg)
        zoom_title_frame.pack(pady=(5, 10))
        zoom_title_label = Label(zoom_title_frame,
                                 text="Adjust plot x and y limits:",
                                 bg=self.tk_format.bg,
                                 fg=self.tk_format.textcolor)
        zoom_title_label.pack(side=LEFT, pady=(0, 4))

        outer_zoom_frame = Frame(outer_outer_zoom_frame,
                                 bg=self.tk_format.bg,
                                 padx=self.tk_format.padx)
        outer_zoom_frame.pack(expand=True, fill=BOTH, pady=(0, 10))
        zoom_frame = Frame(outer_zoom_frame,
                           bg=self.tk_format.bg,
                           padx=self.tk_format.padx)
        zoom_frame.pack()

        zoom_label = Label(zoom_frame,
                           text="x1:",
                           bg=self.tk_format.bg,
                           fg=self.tk_format.textcolor)
        self.left_zoom_entry = Entry(
            zoom_frame,
            width=7,
            bd=self.tk_format.bd,
            bg=self.tk_format.entry_background,
            selectbackground=self.tk_format.selectbackground,
            selectforeground=self.tk_format.selectforeground,
        )
        zoom_label2 = Label(zoom_frame,
                            text="x2:",
                            bg=self.tk_format.bg,
                            fg=self.tk_format.textcolor)
        self.right_zoom_entry = Entry(
            zoom_frame,
            width=7,
            bd=self.tk_format.bd,
            bg=self.tk_format.entry_background,
            selectbackground=self.tk_format.selectbackground,
            selectforeground=self.tk_format.selectforeground,
        )
        zoom_button = Button(
            zoom_frame,
            text="Apply",
            command=self.apply_x,
            width=7,
            fg=self.tk_format.buttontextcolor,
            bg=self.tk_format.buttonbackgroundcolor,
            bd=self.tk_format.bd,
        )
        zoom_button.config(
            fg=self.tk_format.buttontextcolor,
            highlightbackground=self.tk_format.highlightbackgroundcolor,
            bg=self.tk_format.buttonbackgroundcolor,
        )
        zoom_button.pack(side=RIGHT, padx=(10, 10))
        self.right_zoom_entry.pack(side=RIGHT, padx=self.tk_format.padx)
        zoom_label2.pack(side=RIGHT, padx=self.tk_format.padx)
        self.left_zoom_entry.pack(side=RIGHT, padx=self.tk_format.padx)
        zoom_label.pack(side=RIGHT, padx=self.tk_format.padx)

        outer_zoom_frame2 = Frame(outer_outer_zoom_frame,
                                  bg=self.tk_format.bg,
                                  padx=self.tk_format.padx)
        outer_zoom_frame2.pack(expand=True, fill=BOTH, pady=(0, 10))
        zoom_frame2 = Frame(outer_zoom_frame2,
                            bg=self.tk_format.bg,
                            padx=self.tk_format.padx)
        zoom_frame2.pack()
        zoom_label3 = Label(zoom_frame2,
                            text="y1:",
                            bg=self.tk_format.bg,
                            fg=self.tk_format.textcolor)
        self.left_zoom_entry2 = Entry(
            zoom_frame2,
            width=7,
            bd=self.tk_format.bd,
            bg=self.tk_format.entry_background,
            selectbackground=self.tk_format.selectbackground,
            selectforeground=self.tk_format.selectforeground,
        )
        zoom_label4 = Label(zoom_frame2,
                            text="y2:",
                            bg=self.tk_format.bg,
                            fg=self.tk_format.textcolor)
        self.right_zoom_entry2 = Entry(
            zoom_frame2,
            width=7,
            bd=self.tk_format.bd,
            bg=self.tk_format.entry_background,
            selectbackground=self.tk_format.selectbackground,
            selectforeground=self.tk_format.selectforeground,
        )
        zoom_button2 = Button(
            zoom_frame2,
            text="Apply",
            command=self.apply_y,
            width=7,
            fg=self.tk_format.buttontextcolor,
            bg=self.tk_format.buttonbackgroundcolor,
            bd=self.tk_format.bd,
        )
        zoom_button2.config(
            fg=self.tk_format.buttontextcolor,
            highlightbackground=self.tk_format.highlightbackgroundcolor,
            bg=self.tk_format.buttonbackgroundcolor,
        )

        zoom_button2.pack(side=RIGHT, padx=(10, 10))
        self.right_zoom_entry2.pack(side=RIGHT, padx=self.tk_format.padx)
        zoom_label4.pack(side=RIGHT, padx=self.tk_format.padx)
        self.left_zoom_entry2.pack(side=RIGHT, padx=self.tk_format.padx)
        zoom_label3.pack(side=RIGHT, padx=self.tk_format.padx)

        outer_outer_slope_frame = Frame(self.analysis_dialog.interior,
                                        bg=self.tk_format.bg,
                                        padx=self.tk_format.padx,
                                        pady=15,
                                        highlightthickness=1)
        outer_outer_slope_frame.pack(expand=True, fill=BOTH)

        self.outer_slope_frame = Frame(outer_outer_slope_frame,
                                       bg=self.tk_format.bg,
                                       padx=self.tk_format.padx)
        self.outer_slope_frame.pack(expand=True, fill=BOTH, pady=(0, 10))
        slope_title_frame = Frame(self.outer_slope_frame, bg=self.tk_format.bg)
        slope_title_frame.pack(pady=(5, 5))
        slope_title_label = Label(slope_title_frame,
                                  text="Analyze ",
                                  bg=self.tk_format.bg,
                                  fg=self.tk_format.textcolor)
        slope_title_label.pack(side=LEFT, pady=(0, 4))
        self.analyze_var = StringVar()
        self.analyze_var.set("slope")
        analyze_menu = OptionMenu(
            slope_title_frame,
            self.analyze_var,
            "slope",
            "band depth",
            "band center",
            "reflectance",
            # "reciprocity",
            "difference",
            command=self.disable_plot,
        )
        analyze_menu.configure(
            width=10,
            highlightbackground=self.tk_format.highlightbackgroundcolor)
        analyze_menu.pack(side=LEFT)

        # We'll put checkboxes for additional options into this frame at the time the user selects a given option (e.g.
        # select 'difference' from menu, add option to calculate differences based on absolute value
        self.extra_analysis_check_frame = Frame(self.outer_slope_frame,
                                                bg=self.tk_format.bg,
                                                padx=self.tk_format.padx)
        self.extra_analysis_check_frame.pack()

        # Note that we are not packing this checkbutton yet.
        self.abs_val_check = Checkbutton(
            self.extra_analysis_check_frame,
            selectcolor=self.tk_format.check_bg,
            fg=self.tk_format.textcolor,
            text=" Use absolute values for average differences",
            bg=self.tk_format.bg,
            pady=self.tk_format.pady,
            highlightthickness=0,
            variable=self.abs_val,
        )

        self.use_max_for_centers_check = Checkbutton(
            self.extra_analysis_check_frame,
            selectcolor=self.tk_format.check_bg,
            fg=self.tk_format.textcolor,
            text=
            " If band max is more prominent than\nband min, use to find center.",
            bg=self.tk_format.bg,
            pady=self.tk_format.pady,
            highlightthickness=0,
            variable=self.use_max_for_centers,
        )
        self.use_max_for_centers_check.select()

        self.use_delta_check = Checkbutton(
            self.extra_analysis_check_frame,
            selectcolor=self.tk_format.check_bg,
            fg=self.tk_format.textcolor,
            text=" Center at max \u0394" +
            "R from continuum  \nrather than spectral min/max. ",
            bg=self.tk_format.bg,
            pady=self.tk_format.pady,
            highlightthickness=0,
            variable=self.use_delta,
        )
        self.use_delta_check.select()

        self.neg_depth_check = Checkbutton(
            self.extra_analysis_check_frame,
            selectcolor=self.tk_format.check_bg,
            fg=self.tk_format.textcolor,
            text=
            " If band max is more prominent than \nband min, report negative depth.",
            bg=self.tk_format.bg,
            pady=self.tk_format.pady,
            highlightthickness=0,
            variable=self.neg_depth,
        )
        self.neg_depth_check.select()

        slope_frame = Frame(self.outer_slope_frame,
                            bg=self.tk_format.bg,
                            padx=self.tk_format.padx,
                            highlightthickness=0)
        slope_frame.pack(pady=(15, 0))

        slope_label = Label(slope_frame,
                            text="x1:",
                            bg=self.tk_format.bg,
                            fg=self.tk_format.textcolor)
        self.left_slope_entry = Entry(
            slope_frame,
            width=7,
            bd=self.tk_format.bd,
            bg=self.tk_format.entry_background,
            selectbackground=self.tk_format.selectbackground,
            selectforeground=self.tk_format.selectforeground,
        )
        slope_label_2 = Label(slope_frame,
                              text="x2:",
                              bg=self.tk_format.bg,
                              fg=self.tk_format.textcolor)
        self.right_slope_entry = Entry(
            slope_frame,
            width=7,
            bd=self.tk_format.bd,
            bg=self.tk_format.entry_background,
            selectbackground=self.tk_format.selectbackground,
            selectforeground=self.tk_format.selectforeground,
        )
        slope_button = Button(
            slope_frame,
            text="Calculate",
            command=self.calculate,
            width=7,
            fg=self.tk_format.buttontextcolor,
            bg=self.tk_format.buttonbackgroundcolor,
            bd=self.tk_format.bd,
        )
        slope_button.config(
            fg=self.tk_format.buttontextcolor,
            highlightbackground=self.tk_format.highlightbackgroundcolor,
            bg=self.tk_format.buttonbackgroundcolor,
        )

        slope_button.pack(side=RIGHT, padx=(10, 10))
        self.right_slope_entry.pack(side=RIGHT, padx=self.tk_format.padx)
        slope_label_2.pack(side=RIGHT, padx=self.tk_format.padx)
        self.left_slope_entry.pack(side=RIGHT, padx=self.tk_format.padx)
        slope_label.pack(side=RIGHT, padx=self.tk_format.padx)
        self.slope_results_frame = Frame(self.outer_slope_frame,
                                         bg=self.tk_format.bg)
        self.slope_results_frame.pack(
            fill=BOTH, expand=True
        )  # We'll put a listbox with slope info in here later after calculating.

        outer_plot_slope_frame = Frame(outer_outer_slope_frame,
                                       bg=self.tk_format.bg,
                                       padx=self.tk_format.padx,
                                       pady=10)
        outer_plot_slope_frame.pack(expand=True, fill=BOTH)
        plot_slope_frame = Frame(outer_plot_slope_frame,
                                 bg=self.tk_format.bg,
                                 padx=self.tk_format.padx)
        plot_slope_frame.pack(side=RIGHT)
        plot_slope_label = Label(plot_slope_frame,
                                 text="Plot as a function of",
                                 bg=self.tk_format.bg,
                                 fg=self.tk_format.textcolor)
        self.plot_slope_var = StringVar()
        self.plot_slope_var.set("e")
        self.plot_slope_menu = OptionMenu(plot_slope_frame,
                                          self.plot_slope_var, "e", "i", "g",
                                          "e,i", "theta", "az, e")
        self.plot_slope_menu.configure(
            width=2,
            highlightbackground=self.tk_format.highlightbackgroundcolor)
        self.plot_slope_button = Button(
            plot_slope_frame,
            text="Plot",
            command=self.plot,
            width=7,
            fg=self.tk_format.buttontextcolor,
            bg=self.tk_format.buttonbackgroundcolor,
            bd=self.tk_format.bd,
        )
        self.plot_slope_button.config(
            fg=self.tk_format.buttontextcolor,
            highlightbackground=self.tk_format.highlightbackgroundcolor,
            bg=self.tk_format.buttonbackgroundcolor,
            state=DISABLED,
        )
        self.plot_slope_button.pack(side=RIGHT, padx=(10, 10))
        self.plot_slope_menu.pack(side=RIGHT, padx=self.tk_format.padx)
        plot_slope_label.pack(side=RIGHT, padx=self.tk_format.padx)

        exclude_artifacts_frame = Frame(self.analysis_dialog.interior,
                                        bg=self.tk_format.bg,
                                        padx=self.tk_format.padx,
                                        pady=15,
                                        highlightthickness=1)
        exclude_artifacts_frame.pack(fill=BOTH, expand=True)

        self.exclude_artifacts_check = Checkbutton(
            exclude_artifacts_frame,
            selectcolor=self.tk_format.check_bg,
            fg=self.tk_format.textcolor,
            text=
            " Exclude data susceptible to artifacts\n (high g, 1000-1400 nm)  ",
            bg=self.tk_format.bg,
            pady=self.tk_format.pady,
            highlightthickness=0,
            variable=self.exclude_artifacts,
            # TODO: check if the comma is meant to be there in the lambda definition
            command=lambda x="foo", : self.tab.set_exclude_artifacts(
                self.exclude_artifacts.get()),
        )
        self.exclude_artifacts_check.pack()
        if self.tab.exclude_artifacts:
            self.exclude_artifacts_check.select()

        self.analysis_dialog.interior.configure(highlightthickness=1,
                                                highlightcolor="white")

    def calculate(self):
        try:
            self.controller.view_notebook.select(self.tab.top)
        except TclError:
            print("Error selecting tab in analysis_tools_manager.calculate().")
            print(self.tab)
            pass
        artifact_warning = False

        if self.analyze_var.get() == "slope":
            left, right, slopes, artifact_warning = self.tab.calculate_slopes(
                self.left_slope_entry.get(), self.right_slope_entry.get())
            self.update_entries(left, right)
            self.populate_listbox(slopes)
            self.update_plot_menu(["e", "i", "g", "e,i", "theta", "az, e"])

        elif self.analyze_var.get() == "band depth":
            left, right, depths, artifact_warning = self.tab.calculate_band_depths(
                self.left_slope_entry.get(),
                self.right_slope_entry.get(),
                self.neg_depth.get(),
                self.use_delta.get(),
            )
            self.update_entries(left, right)
            self.populate_listbox(depths)
            self.update_plot_menu(["e", "i", "g", "e,i", "theta", "az, e"])

        elif self.analyze_var.get() == "band center":
            left, right, centers, artifact_warning = self.tab.calculate_band_centers(
                self.left_slope_entry.get(),
                self.right_slope_entry.get(),
                self.use_max_for_centers.get(),
                self.use_delta.get(),
            )
            self.update_entries(left, right)
            self.populate_listbox(centers)
            self.update_plot_menu(["e", "i", "g", "e,i", "theta", "az, e"])

        elif self.analyze_var.get() == "reflectance":
            left, right, reflectance, artifact_warning = self.tab.calculate_avg_reflectance(
                self.left_slope_entry.get(), self.right_slope_entry.get())
            self.update_entries(left, right)
            self.populate_listbox(reflectance)
            self.update_plot_menu(["e", "i", "g", "e,i", "theta", "az, e"])

        elif self.analyze_var.get() == "reciprocity":
            left, right, reciprocity, artifact_warning = self.tab.calculate_reciprocity(
                self.left_slope_entry.get(), self.right_slope_entry.get())
            self.update_entries(left, right)
            self.populate_listbox(reciprocity)
            self.update_plot_menu(["e", "i", "g", "e,i"])

        elif self.analyze_var.get() == "difference":
            left, right, error, artifact_warning = self.tab.calculate_error(
                self.left_slope_entry.get(), self.right_slope_entry.get(),
                self.abs_val.get())
            # Tab validates left and right values. If they are no good, put in min and max wavelengths available.
            self.update_entries(left, right)
            self.populate_listbox(error)
            self.update_plot_menu(["\u03bb", "e,i"])

        if artifact_warning:
            ErrorDialog(
                self, "Warning",
                "Warning: Excluding data potentially\ninfluenced by artifacts from 1000-1400 nm."
            )

        self.analysis_dialog.min_height = 1000
        self.analysis_dialog.update()

    def update_plot_menu(self, plot_options):
        self.plot_slope_var.set(plot_options[0])
        self.plot_slope_menu["menu"].delete(0, "end")

        # Insert list of new options (tk._setit hooks them up to var)
        max_len = len(plot_options[0])
        for option in plot_options:
            max_len = np.max([max_len, len(option)])
            # pylint: disable = protected-access
            self.plot_slope_menu["menu"].add_command(label=option,
                                                     command=tkinter._setit(
                                                         self.plot_slope_var,
                                                         option))
        self.plot_slope_menu.configure(width=max_len)

    def update_entries(self, left, right):
        self.left_slope_entry.delete(0, "end")
        self.left_slope_entry.insert(0, str(left))
        self.right_slope_entry.delete(0, "end")
        self.right_slope_entry.insert(0, str(right))

    def populate_listbox(self, results):
        if len(results) > 0:
            self.slope_results_frame.pack(fill=BOTH,
                                          expand=True,
                                          pady=(10, 10))
            try:
                self.slopes_listbox.delete(0, "end")
            except (AttributeError, TclError):
                self.slopes_listbox = utils.ScrollableListbox(
                    self.slope_results_frame,
                    self.tk_format.bg,
                    self.tk_format.entry_background,
                    self.tk_format.listboxhighlightcolor,
                    selectmode=EXTENDED,
                )
                self.slopes_listbox.configure(height=8)
            for result in results:
                self.slopes_listbox.insert("end", result)
            self.slopes_listbox.pack(fill=BOTH, expand=True)
            self.plot_slope_button.configure(state=NORMAL)

    def plot(self):
        if self.analyze_var.get() == "slope":
            self.tab.plot_slopes(self.plot_slope_var.get())
        elif self.analyze_var.get() == "band depth":
            self.tab.plot_band_depths(self.plot_slope_var.get())
        elif self.analyze_var.get() == "band center":
            self.tab.plot_band_centers(self.plot_slope_var.get())
        elif self.analyze_var.get() == "reflectance":
            self.tab.plot_avg_reflectance(self.plot_slope_var.get())
        elif self.analyze_var.get() == "reciprocity":
            self.tab.plot_reciprocity(self.plot_slope_var.get())
        elif self.analyze_var.get() == "difference":
            new = self.tab.plot_error(self.plot_slope_var.get())
            if self.plot_slope_var.get() == "\u03bb":
                x1 = float(self.left_slope_entry.get())
                x2 = float(self.right_slope_entry.get())
                new.adjust_x(x1, x2)
        # TODO: plots not always fully updating
        #  (e.g. contour plot labels not showing up until you do a screen wiggle.

        # utils.thread_lift_widget(self.analysis_dialog.top)

    def normalize(self):
        self.select_tab()
        try:
            self.slopes_listbox.delete(0, "end")
            self.plot_slope_button.configure(state="disabled")
        except (AttributeError, TclError):
            pass
        self.tab.normalize(self.normalize_entry.get())
        # thread = Thread(target=utils.lift_widget, args=(self.analysis_dialog.top,))
        # thread.start()

    def offset(self):
        self.tab.offset(self.offset_sample_var.get(), self.offset_entry.get())

        # This doesn't work - it hangs between thread.start() and thread.join(). Likely because of calls to canvas.draw()
        # thread = Thread(target=self.tab.offset, args=(self.offset_sample_var.get(), self.offset_entry.get()))
        # thread.start()
        # thread.join()
        # utils.lift_widget(self.analysis_dialog.top)

    def remove_topmost(self):
        print("removing!!")
        self.analysis_dialog.top.attributes("-topmost", False)

    def apply_x(self):
        self.controller.view_notebook.select(self.tab.top)

        try:
            x1 = float(self.left_zoom_entry.get())
            x2 = float(self.right_zoom_entry.get())
            self.tab.adjust_x(x1, x2)
            # utils.lift_widget(self.analysis_dialog.top)
        except ValueError:
            # utils.lift_widget(self.analysis_dialog.top)
            ErrorDialog(
                self,
                title="Invalid Zoom Range",
                label="Error! Invalid x limits: " +
                self.left_zoom_entry.get() + ", " +
                self.right_zoom_entry.get(),
            )

    def apply_y(self):
        self.controller.view_notebook.select(self.tab.top)
        try:
            y1 = float(self.left_zoom_entry2.get())
            y2 = float(self.right_zoom_entry2.get())
            self.tab.adjust_y(y1, y2)
            # utils.lift_widget(self.analysis_dialog.top)
        except ValueError:
            # utils.lift_widget(self.analysis_dialog.top)
            ErrorDialog(
                self,
                title="Invalid Zoom Range",
                label="Error! Invalid y limits: " +
                self.left_zoom_entry2.get() + ", " +
                self.right_zoom_entry2.get(),
            )

    def uncheck_exclude_artifacts(self):
        self.exclude_artifacts.set(0)
        self.exclude_artifacts_check.deselect()
        # utils.lift_widget(self.analysis_dialog.top)

    def disable_plot(self, analyze_var="None"):
        try:
            self.slopes_listbox.delete(0, "end")
        except (AttributeError, TclError):
            pass
        self.plot_slope_button.configure(state="disabled")

        if analyze_var == "difference":
            self.analysis_dialog.frame.min_height = 850
            self.neg_depth_check.pack_forget()
            self.use_max_for_centers_check.pack_forget()
            self.use_delta_check.pack_forget()
            self.abs_val_check.pack()
            self.extra_analysis_check_frame.pack()

        elif analyze_var == "band center":
            self.analysis_dialog.frame.min_height = 1000
            self.neg_depth_check.pack_forget()
            self.abs_val_check.pack_forget()
            self.use_delta_check.pack_forget()
            self.use_max_for_centers_check.pack()
            self.use_delta_check.pack()
            self.extra_analysis_check_frame.pack()

        elif analyze_var == "band depth":
            self.analysis_dialog.frame.min_height = 1000
            self.abs_val_check.pack_forget()
            self.use_max_for_centers_check.pack_forget()
            self.use_delta_check.pack_forget()
            self.neg_depth_check.pack()
            self.use_delta_check.pack()
            self.extra_analysis_check_frame.pack()

        else:
            self.analysis_dialog.frame.min_height = 850
            self.abs_val_check.pack_forget()
            self.neg_depth_check.pack_forget()
            self.use_max_for_centers_check.pack_forget()
            self.use_delta_check.pack_forget()

            self.extra_analysis_check_frame.grid_propagate(0)
            self.extra_analysis_check_frame.configure(
                height=1)  # for some reason 0 doesn't work.
            self.extra_analysis_check_frame.pack()
            self.outer_slope_frame.pack()
        # utils.lift_widget(self.analysis_dialog.top)

    # def calculate_photometric_variability(self):
    #     photo_var = self.tab.calculate_photometric_variability(
    #         self.right_photo_var_entry.get(), self.left_photo_var_entry.get()
    #     )
    #     try:
    #         self.photo_var_listbox.delete(0, "end")
    #     except:
    #         self.photo_var_listbox = utils.ScrollableListbox(
    #             self.photo_var_results_frame,
    #             self.tk_format.bg,
    #             self.tk_format.entry_background,
    #             self.tk_format.listboxhighlightcolor,
    #             selectmode=EXTENDED,
    #         )
    #     for var in photo_var:
    #         self.photo_var_listbox.insert("end", var)
    #     self.photo_var_listbox.pack(fill=BOTH, expand=True)

    def select_tab(self):
        self.controller.view_notebook.select(self.tab.top)