Exemple #1
0
class BridgeDock(QDockWidget):
    def __init__(self, parent, api: PluginApi) -> None:
        super().__init__('', parent)
        self.api = api
        self.ui = Ui_BridgeDock()
        self.ui.setupUi(self)
        self.server_thread = None

        self.observer = None
        self.modified_timer = None
        self.slot_server_running(False)

        self.ui.pushButtonStartServer.clicked.connect(self.slot_start_server)
        self.ui.pushButtonStopServer.clicked.connect(self.slot_stop_server)
        self.ui.toolButtonLoadFolder.clicked.connect(
            self.slot_edit_load_folder)
        self.ui.toolButtonSaveFolder.clicked.connect(
            self.slot_edit_save_folder)

        self.ui.labelConnectionStatus.setText('Server not yet running.')

        # Initially load from repo folder
        self.ui.lineEditLoadFolder.setText(settings.get_repo_location())

        self.visibilityChanged.connect(self.slot_visibility_changed)

    def slot_visibility_changed(self, visible: bool) -> None:
        if not visible and self.server_thread is not None:
            self.slot_stop_server()

    def slot_server_running(self, running: bool) -> None:
        if running:
            self.ui.pushButtonStartServer.setVisible(False)
            self.ui.pushButtonStopServer.setVisible(True)
        else:
            self.ui.pushButtonStartServer.setVisible(True)
            self.ui.pushButtonStopServer.setVisible(False)

    def slot_start_server(self) -> None:

        if self.ui.checkBoxCopySaves.isChecked(
        ) and self.ui.lineEditSaveFolder.text().strip() == '':
            self.api.show_error(
                'Entity Explorer Bridge',
                'You need to set the folder where to store the copies.')
            return

        self.server_thread = QThread()
        self.server_worker = ServerWorker()
        self.server_worker.signal_connected.connect(self.slot_connected)
        self.server_worker.signal_disconnected.connect(self.slot_disconnected)
        self.server_worker.signal_error.connect(self.slot_error)
        self.server_worker.signal_started.connect(self.slot_server_started)
        self.server_worker.signal_shutdown.connect(self.slot_server_stopped)
        self.server_worker.moveToThread(self.server_thread)
        self.server_thread.started.connect(self.server_worker.process)
        self.server_thread.start()
        self.slot_server_running(True)
        self.set_folders_active(False)

    def set_folders_active(self, active: bool) -> None:
        self.ui.lineEditLoadFolder.setEnabled(active)
        self.ui.toolButtonLoadFolder.setEnabled(active)
        self.ui.checkBoxCopySaves.setEnabled(active)
        self.ui.lineEditSaveFolder.setEnabled(active)
        self.ui.toolButtonSaveFolder.setEnabled(active)

    def slot_stop_server(self) -> None:
        # Shutdown needs to be triggered by the server thread, so send a request
        requests.get('http://localhost:10243/shutdown')
        self.set_folders_active(True)

    def slot_connected(self) -> None:
        self.ui.labelConnectionStatus.setText(
            'Connected to Entity Explorer instance.')

    def slot_disconnected(self) -> None:
        self.ui.labelConnectionStatus.setText(
            'Disconnected from Entity Explorer instance.')

    def slot_server_started(self) -> None:
        self.slot_server_running(True)
        self.ui.labelConnectionStatus.setText(
            'Server running. Please connect Entity Explorer instance.')
        self.start_watchdog()

    def slot_server_stopped(self) -> None:
        self.slot_server_running(False)
        self.server_thread.terminate()
        self.ui.labelConnectionStatus.setText('Server stopped.')
        self.stop_watchdog()

    def slot_error(self, error: str) -> None:
        self.slot_server_running(False)
        self.server_thread.terminate()
        self.api.show_error('Entity Explorer Bridge', error)

    def slot_edit_load_folder(self):
        dir = QFileDialog.getExistingDirectory(
            self, 'Folder in which the save states are stored by mGBA',
            self.ui.lineEditLoadFolder.text())
        print(dir)
        if dir is not None:
            self.ui.lineEditLoadFolder.setText(dir)

    def slot_edit_save_folder(self):
        dir = QFileDialog.getExistingDirectory(
            self, 'Folder in which all save states should be copied',
            self.ui.lineEditSaveFolder.text())
        if dir is not None:
            self.ui.lineEditSaveFolder.setText(dir)

    def start_watchdog(self):
        if self.observer is not None:
            print('Already observing')
            return
        patterns = [
            '*.ss0', '*.ss1', '*.ss2', '*.ss3', '*.ss4', '*.ss5', '*.ss6',
            '*.ss7', '*.ss8', '*.ss9', '*.State'
        ]
        ignore_patterns = None
        ignore_directories = False
        case_sensitive = True
        self.event_handler = PatternMatchingEventHandler(
            patterns, ignore_patterns, ignore_directories, case_sensitive)
        self.event_handler.on_modified = self.on_file_modified

        path = self.ui.lineEditLoadFolder.text()
        self.observer = Observer()
        self.observer.schedule(self.event_handler, path, recursive=False)
        self.observer.start()

    def stop_watchdog(self):
        if self.observer is not None:
            self.observer.stop()
            self.observer.join()
            self.observer = None

    # https://stackoverflow.com/a/66907107
    def debounce(wait_time):
        """
        Decorator that will debounce a function so that it is called after wait_time seconds
        If it is called multiple times, will wait for the last call to be debounced and run only this one.
        """
        def decorator(function):
            def debounced(*args, **kwargs):
                def call_function():
                    debounced._timer = None
                    return function(*args, **kwargs)

                # if we already have a call to the function currently waiting to be executed, reset the timer
                if debounced._timer is not None:
                    debounced._timer.cancel()

                # after wait_time, call the function provided to the decorator with its arguments
                debounced._timer = threading.Timer(wait_time, call_function)
                debounced._timer.start()

            debounced._timer = None
            return debounced

        return decorator

    @debounce(0.1)
    def on_file_modified(self, event):
        with open(event.src_path, 'rb') as file:
            bytes = file.read()
            self.server_worker.slot_send_save_state(event.src_path, bytes)

            if self.ui.checkBoxCopySaves.isChecked(
            ) and self.ui.lineEditSaveFolder.text():
                name = os.path.basename(event.src_path)
                name = datetime.now().strftime('%Y-%m-%d_%H_%M_%S_%f_') + name
                with open(
                        os.path.join(self.ui.lineEditSaveFolder.text(), name),
                        'wb') as output:
                    output.write(bytes)
Exemple #2
0
class BridgeDock(QDockWidget):
    def __init__(self, parent, api: PluginApi) -> None:
        super().__init__('', parent)
        self.api = api
        self.ui = Ui_BridgeDock()
        self.ui.setupUi(self)
        self.server_thread = None

        self.observer = None
        self.modified_timer = None
        self.slot_server_running(False)

        self.ui.pushButtonStartServer.clicked.connect(self.slot_start_server)
        self.ui.pushButtonStopServer.clicked.connect(self.slot_stop_server)

        self.ui.labelConnectionStatus.setText('Server not yet running.')

        self.visibilityChanged.connect(self.slot_visibility_changed)

    def slot_visibility_changed(self, visible: bool) -> None:
        if not visible and self.server_thread is not None:
            self.slot_stop_server()

    def slot_server_running(self, running: bool) -> None:
        if running:
            self.ui.pushButtonStartServer.setVisible(False)
            self.ui.pushButtonStopServer.setVisible(True)
        else:
            self.ui.pushButtonStartServer.setVisible(True)
            self.ui.pushButtonStopServer.setVisible(False)

    def slot_start_server(self) -> None:
        self.server_thread = QThread()
        self.server_worker = ServerWorker()
        self.server_worker.signal_connected.connect(self.slot_connected)
        self.server_worker.signal_error.connect(self.slot_error)
        self.server_worker.signal_started.connect(self.slot_server_started)
        self.server_worker.signal_shutdown.connect(self.slot_server_stopped)
        self.server_worker.signal_script_addr.connect(self.slot_script_addr)
        self.server_worker.moveToThread(self.server_thread)
        self.server_thread.started.connect(self.server_worker.process)
        self.server_thread.start()
        self.slot_server_running(True)

    def slot_stop_server(self) -> None:
        # Shutdown needs to be triggered by the server thread, so send a request
        requests.get('http://*****:*****@') or stripped.endswith(':'):
                output += f'{line}\n'
                continue

            if '.ifdef' in stripped:
                if not ifdef_stack[-1]:
                    ifdef_stack.append(False)
                    output += f'{line}\n'
                    continue
                # TODO check variant
                is_usa = stripped.split(' ')[1] == 'USA'
                ifdef_stack.append(is_usa)
                output += f'{line}\n'
                continue
            if '.ifndef' in stripped:
                if not ifdef_stack[-1]:
                    ifdef_stack.append(False)
                    output += f'{line}\n'
                    continue
                is_usa = stripped.split(' ')[1] == 'USA'
                ifdef_stack.append(not is_usa)
                output += f'{line}\n'
                continue
            if '.else' in stripped:
                if ifdef_stack[-2]:
                    # If the outermost ifdef is not true, this else does not change the validiness of this ifdef
                    ifdef_stack[-1] = not ifdef_stack[-1]
                output += f'{line}\n'
                continue
            if '.endif' in stripped:
                ifdef_stack.pop()
                output += f'{line}\n'
                continue

            if not ifdef_stack[-1]:
                # Not defined for this variant
                output += f'{line}\n'
                continue

            if current_instruction >= len(instructions):
                # TODO maybe even not print additional lines?
                output += f'{line}\n'
                continue
            addr = instructions[current_instruction].addr
            prefix = ''
            if addr == script_offset:
                prefix = '>'
            output += f'{addr:03d}| {prefix}{line}\t\n'
            current_instruction += 1
            if stripped.startswith('SCRIPT_END'):
                break
        self.ui.labelCode.setText(output)
Exemple #3
0
class BridgeDock(QDockWidget):
    def __init__(self, parent, api: PluginApi) -> None:
        super().__init__('', parent)
        self.api = api
        self.ui = Ui_BridgeDock()
        self.ui.setupUi(self)
        self.server_thread = None

        self.symbols = None
        self.rom = None
        self.data_extractor_plugin = None

        self.slot_server_running(False)

        self.ui.pushButtonStartServer.clicked.connect(self.slot_start_server)
        self.ui.pushButtonStopServer.clicked.connect(self.slot_stop_server)
        self.ui.pushButtonUpload.clicked.connect(self.slot_upload_function)
        self.ui.pushButtonDownload.clicked.connect(self.slot_download_function)
        self.ui.pushButtonCopyJs.clicked.connect(self.slot_copy_js_code)
        self.ui.pushButtonGoTo.clicked.connect(self.slot_goto)
        self.ui.pushButtonDecompile.clicked.connect(self.slot_decompile)
        self.ui.pushButtonGlobalTypes.clicked.connect(self.slot_global_types)
        self.ui.pushButtonUploadAndDecompile.clicked.connect(
            self.slot_upload_and_decompile)

        self.enable_function_group(False)
        self.ui.labelConnectionStatus.setText('Server not yet running.')
        self.visibilityChanged.connect(self.slot_close)

    def slot_close(self, visibility: bool) -> None:
        # TODO temporarily disable until a good way to detect dock closing is found
        pass
        #if not visibility and self.server_thread is not None:
        #    self.slot_stop_server()

    def slot_server_running(self, running: bool) -> None:
        if running:
            self.ui.pushButtonStartServer.setVisible(False)
            self.ui.pushButtonStopServer.setVisible(True)
        else:
            self.ui.pushButtonStartServer.setVisible(True)
            self.ui.pushButtonStopServer.setVisible(False)

    def slot_start_server(self) -> None:
        self.server_thread = QThread()
        self.server_worker = ServerWorker()
        self.server_worker.signal_connected.connect(self.slot_connected)
        self.server_worker.signal_disconnected.connect(self.slot_disconnected)
        self.server_worker.signal_error.connect(self.slot_error)
        self.server_worker.signal_c_code.connect(self.slot_received_c_code)
        self.server_worker.signal_started.connect(self.slot_server_started)
        self.server_worker.signal_shutdown.connect(self.slot_server_stopped)
        self.server_worker.signal_extract_data.connect(self.slot_extract_data)
        self.server_worker.signal_fetch_decompilation.connect(
            self.slot_fetch_decompilation)
        self.server_worker.signal_upload_function.connect(
            self.slot_download_requested)
        self.server_worker.moveToThread(self.server_thread)
        self.server_thread.started.connect(self.server_worker.process)
        self.server_thread.start()
        self.slot_server_running(True)

    def enable_function_group(self, enabled: bool) -> None:
        self.ui.lineEditFunctionName.setEnabled(enabled)
        self.ui.pushButtonUpload.setEnabled(enabled)
        self.ui.pushButtonDownload.setEnabled(enabled)
        self.ui.pushButtonGoTo.setEnabled(enabled)
        self.ui.pushButtonDecompile.setEnabled(enabled)
        self.ui.pushButtonUploadAndDecompile.setEnabled(enabled)

    def slot_stop_server(self) -> None:
        # Shutdown needs to be triggered by the server thread, so send a request
        requests.get('http://localhost:10241/shutdown')

    def slot_upload_function(self) -> None:
        self.upload_function(True)

    # Returns true if the user accepted the uploading
    def upload_function(self, include_function: bool) -> bool:
        # TODO try catch all of the slots?
        (err, asm, src,
         signature) = get_code(self.ui.lineEditFunctionName.text().strip(),
                               include_function)
        if err:
            self.api.show_error('CExplore Bridge', asm)
            return

        if NO_CONFIRMS:
            # For pros also directly go to the function in Ghidra and apply the signature
            self.slot_goto()
            #self.apply_function_signature(self.ui.lineEditFunctionName.text().strip(), signature)

        if NO_CONFIRMS or self.api.show_question(
                'CExplore Bridge',
                f'Replace code in CExplore with {self.ui.lineEditFunctionName.text().strip()}?'
        ):
            self.server_worker.slot_send_asm_code(extract_USA_asm(asm))
            self.server_worker.slot_send_c_code(src)
            if not NO_CONFIRMS:
                self.api.show_message(
                    'CExplore Bridge',
                    f'Uploaded code of {self.ui.lineEditFunctionName.text().strip()}.'
                )
            return True
        return False

    def slot_download_requested(self, name: str) -> None:
        self.ui.lineEditFunctionName.setText(name)
        self.slot_download_function()

    def slot_download_function(self) -> None:
        self.enable_function_group(False)
        self.server_worker.slot_request_c_code()

    def slot_received_c_code(self, code: str) -> None:

        self.enable_function_group(True)
        (includes, header, src) = split_code(code)
        dialog = ReceivedDialog(self)
        dialog.signal_matching.connect(self.slot_store_matching)
        dialog.signal_nonmatching.connect(self.slot_store_nonmatching)
        dialog.show_code(includes, header, src)

    def slot_store_matching(self, includes: str, header: str,
                            code: str) -> None:
        self.store(includes, header, code, True)

    def slot_store_nonmatching(self, includes: str, header: str,
                               code: str) -> None:
        self.store(includes, header, code, False)

    def store(self, includes: str, header: str, code: str,
              matching: bool) -> None:
        (err, msg) = store_code(self.ui.lineEditFunctionName.text().strip(),
                                includes, header, code, matching)
        if err:
            self.api.show_error('CExplore Bridge', msg)
            return
        if not NO_CONFIRMS:
            self.api.show_message(
                'CExplore Bridge',
                f'Sucessfully replaced code of {self.ui.lineEditFunctionName.text().strip()}.'
            )

    def slot_copy_js_code(self) -> None:
        QApplication.clipboard().setText(
            'javascript:var script = document.createElement("script");script.src = "http://localhost:10241/static/bridge.js";document.body.appendChild(script);'
        )
        self.api.show_message(
            'CExplore Bridge',
            'Copied JS code to clipboard.\nPaste it as the url to a bookmark.\nThen go open the CExplore instance and click on the bookmark to connect.'
        )

    def slot_connected(self) -> None:
        self.ui.labelConnectionStatus.setText(
            'Connected to CExplore instance.')
        self.enable_function_group(True)
        self.ui.pushButtonCopyJs.setVisible(False)

    def slot_disconnected(self) -> None:
        self.ui.labelConnectionStatus.setText(
            'Disconnected from CExplore instance.')
        self.enable_function_group(False)

    def slot_server_started(self) -> None:
        self.slot_server_running(True)
        self.ui.labelConnectionStatus.setText(
            'Server running. Please connect CExplore instance.')

    def slot_server_stopped(self) -> None:
        self.slot_server_running(False)
        self.server_thread.terminate()
        self.enable_function_group(False)
        self.ui.pushButtonCopyJs.setVisible(True)
        self.ui.labelConnectionStatus.setText('Server stopped.')

    def slot_error(self, error: str) -> None:
        self.slot_server_running(False)
        self.server_thread.terminate()
        self.enable_function_group(False)
        self.ui.pushButtonCopyJs.setVisible(True)
        self.api.show_error('CExplore Bridge', error)

    def slot_goto(self) -> None:
        try:
            r = requests.get('http://localhost:10242/goto/' +
                             self.ui.lineEditFunctionName.text().strip())
            if r.status_code != 200:
                self.api.show_error('CExplore Bridge', r.text)
                return
        except requests.exceptions.RequestException as e:
            self.api.show_error(
                'CExplore Bridge',
                'Could not reach Ghidra server. Did you start the script?')

    def slot_fetch_decompilation(self, name: str) -> None:
        self.ui.lineEditFunctionName.setText(name)
        self.slot_decompile()

    def slot_decompile(self) -> None:
        try:
            r = requests.get('http://localhost:10242/decompile/' +
                             self.ui.lineEditFunctionName.text().strip())
            if r.status_code != 200:
                self.api.show_error('CExplore Bridge', r.text)
                return
            result = r.text
            code = improve_decompilation(result)
            self.server_worker.slot_add_c_code(code)
        except requests.exceptions.RequestException as e:
            self.api.show_error(
                'CExplore Bridge',
                'Could not reach Ghidra server. Did you start the script?')
        except Exception as e:
            self.api.show_error('CExplore Bridge',
                                'An unknown error occured: ' + str(e))

    def slot_upload_and_decompile(self) -> None:
        # Upload, but don't include the function.
        if self.upload_function(False):
            # Now add the decompiled function.
            self.slot_decompile()

    def slot_global_types(self) -> None:
        globals = find_globals()
        success = True
        for definition in globals:
            if not self.apply_global_type(definition):
                success = False
                break

        # Also apply function signatures from file
        if success:
            signatures = read_signatures_from_file()
            for signature in signatures:
                if not self.apply_function_signature(signature.function,
                                                     signature.signature):
                    success = False
                    break

        if success:
            self.api.show_message('CExplore Bridge',
                                  'Applied all global types.')

    def apply_global_type(self, definition: TypeDefinition) -> bool:
        try:
            url = 'http://localhost:10242/globalType/' + definition.name + '/' + definition.dataType + '/' + definition.length
            print(url)
            r = requests.get(url)
            if r.status_code != 200:
                self.api.show_error('CExplore Bridge', r.text)
                return False
            return True
        except requests.exceptions.RequestException as e:
            self.api.show_error(
                'CExplore Bridge',
                'Could not reach Ghidra server. Did you start the script?')
            return False

    def apply_function_signature(self, name: str, signature: str) -> bool:
        try:
            url = 'http://localhost:10242/functionType/' + name + '/' + signature
            print(url)
            r = requests.get(url)
            if r.status_code != 200:
                self.api.show_error('CExplore Bridge', r.text)
                return False
            return True
        except requests.exceptions.RequestException as e:
            self.api.show_error(
                'CExplore Bridge',
                'Could not reach Ghidra server. Did you start the script?')
            return False

    def slot_extract_data(self, text: str) -> None:
        if self.symbols is None:
            # First need to load symbols
            self.symbols = get_symbol_database().get_symbols(RomVariant.CUSTOM)
            if self.symbols is None:
                self.server_worker.slot_extracted_data({
                    'status':
                    'error',
                    'text':
                    'No symbols for rom CUSTOM loaded'
                })
                return

        if self.data_extractor_plugin is None:
            self.data_extractor_plugin = get_plugin('data_extractor',
                                                    'DataExtractorPlugin')
            if self.data_extractor_plugin is None:
                self.server_worker.slot_extracted_data({
                    'status':
                    'error',
                    'text':
                    'Data Extractor plugin not loaded'
                })
                return

        if self.rom is None:
            self.rom = get_rom(RomVariant.CUSTOM)
            if self.rom is None:
                self.server_worker.slot_extracted_data({
                    'status':
                    'error',
                    'text':
                    'CUSTOM rom could not be loaded'
                })
                return

        try:
            result = self.data_extractor_plugin.instance.extract_data(
                text, self.symbols, self.rom)
            if result is not None:
                self.server_worker.slot_extracted_data({
                    'status': 'ok',
                    'text': result
                })
        except Exception as e:
            traceback.print_exc()
            self.server_worker.slot_extracted_data({
                'status': 'error',
                'text': str(e)
            })