Пример #1
0
class Application(Frame):
    def __init__(self, master=None):
        Frame.__init__(self, 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.encode(getpreferredencoding()))

        self.nand_button['state'] = (NORMAL if self.nand_file.get() != '' else
                                     DISABLED)
        self.start_button['state'] = (NORMAL if self.nand_file.get() != '' 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(hexlify(bstr).upper())
                    self.log.write('- eMMC CID: ' + self.cid.get())

                    # Read the console ID
                    bstr = f.read(8)
                    self.console_id.set(
                        hexlify(bytearray(reversed(bstr))).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):
        # 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()

                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.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')
            if self.twilight.get():
                self.log.write(
                    '\nPlease download the latest versions of HiyaCFW and\nTWiLight Menu++ from:'
                )
                self.log.write(
                    '\nhttps://github.com/RocketRobz/hiyaCFW/releases')
                self.log.write(
                    'https://github.com/RocketRobz/TWiLightMenu/releases')
                self.log.write('\nThen place the files in this folder.')
            else:
                self.log.write(
                    '\nPlease download the latest version of HiyaCFW from:')
                self.log.write(
                    '\nhttps://github.com/RocketRobz/hiyaCFW/releases')
                self.log.write('\nThen place the file in this folder.')

        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  ' +
                               hexlify(sha1_hash.digest()).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  ' +
                               hexlify(sha1_hash.digest()).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  ' +
                           hexlify(sha1_hash.digest()).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  ' +
                           hexlify(sha1_hash.digest()).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  ' +
                           hexlify(sha1_hash.digest()).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  ' +
                               hexlify(sha1_hash.digest()).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).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).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).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).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
        if path.getsize(
                path.join(self.sd_path, 'title', '00030017', app, 'content',
                          'title.tmd')) > 520:
            self.log.write('- WARNING: Unlaunch installed on the NAND dump')

        # 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...')

                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:
                # 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  ' +
                               hexlify(sha1_hash.digest()).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...')

        try:
            self.log.write('- Deleting stock launcher title.tmd...')
            if self.launcher_region == 'USA':
                remove(
                    path.join(self.sd_path, 'title', '00030017', '484e4145',
                              'content', 'title.tmd'))
            if self.launcher_region == 'JAP':
                remove(
                    path.join(self.sd_path, 'title', '00030017', '484e414a',
                              'content', 'title.tmd'))
            if self.launcher_region == 'EUR':
                remove(
                    path.join(self.sd_path, 'title', '00030017', '484e4150',
                              'content', 'title.tmd'))
            if self.launcher_region == 'AUS':
                remove(
                    path.join(self.sd_path, 'title', '00030017', '484e4155',
                              'content', 'title.tmd'))
        except:
            pass

        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 = False

        # Release archives names
        names = ('TWiLightMenu.7z', 'DSiMenuPP.7z', 'DSiMenuPlusPlus.7z',
                 'SRLoader.7z')

        for name in names:
            if (path.isfile(name)):
                filename = name
                break

        try:
            if filename:
                self.log.write('\nPreparing custom firmware...')

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

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

                filename = names[0]
                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, 'Autoboot for HiyaCFW',
                'CFW - SDNAND root', 'DSiWare (' + self.launcher_region + ')',
                '_nds', 'roms', 'BOOT.NDS'
            ])

            ret_val = proc.wait()

            if ret_val == 0:
                self.folders.append('Autoboot for HiyaCFW')
                self.folders.append('CFW - SDNAND root')
                self.folders.append('DSiWare (' + self.launcher_region + ')')
                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('CFW - SDNAND root', self.sd_path, update=1)
        move('_nds', path.join(self.sd_path, '_nds'))
        move('roms', path.join(self.sd_path, 'roms'))
        move('BOOT.NDS', path.join(self.sd_path, 'BOOT.NDS'))
        copy_tree('DSiWare (' + self.launcher_region + ')',
                  path.join(self.sd_path, 'roms', 'dsiware'),
                  update=1)
        move(path.join('Autoboot for HiyaCFW', 'autoboot.bin'),
             path.join(self.sd_path, 'hiya', 'autoboot.bin'))

        # 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)

        # Generate launchargs
        for app in listdir(path.join(self.sd_path, 'title', '00030004')):
            try:
                for title in listdir(
                        path.join(self.sd_path, 'title', '00030004', app,
                                  'content')):
                    if title.endswith('.app'):
                        with open(
                                path.join(self.sd_path, 'roms', 'dsiware',
                                          app + '.launcharg'),
                                'w') as launcharg:
                            launcharg.write('sd:/title/00030004/' + app + '/')

            except:
                pass

        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()))