예제 #1
0
class QMemoryDataBlock(QCachedGraphicsItem):

    ADDRESS_LABEL_OFFSET = 20
    LINEAR_INSTRUCTION_OFFSET = 120
    BYTE_AREA_SPACING = 15

    def __init__(self, workspace, infodock, addr, memory_data, bytes_per_line=16, parent=None):
        super().__init__(parent=parent)
        self.workspace = workspace
        self.infodock = infodock
        self.addr = addr
        self.memory_data: MemoryData = memory_data
        self.bytes_per_line: int = bytes_per_line  # TODO: Move it to Conf

        self._addr_text = None
        self._width = None
        self._height = None

        self._bytes = [ ]

        # widgets
        self._addr_item: QGraphicsSimpleTextItem = None
        self._label_item: Optional[QGraphicsSimpleTextItem] = None
        self._line_items: List[Tuple[int,
                                     QGraphicsSimpleTextItem,
                                     List[QGraphicsSimpleTextItem],
                                     List[QGraphicsSimpleTextItem]]] = None

        self._init_widgets()

    #
    # Public methods
    #

    @property
    def width(self):
        return self.boundingRect().width()

    @property
    def height(self):
        return self.boundingRect().height()

    def paint(self, painter, option, widget):

        should_highlight = self.infodock.is_label_selected(self.addr)

        highlight_color = Conf.disasm_view_label_highlight_color
        if should_highlight:
            painter.setBrush(highlight_color)
            painter.setPen(highlight_color)
            painter.drawRect(0, 0, self.width, self.height)

    #
    # Event handlers
    #

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            # unselect all other labels
            self.infodock.unselect_all_labels()
            # select this label
            self.infodock.select_label(self.addr)

    #
    # Private methods
    #

    def _init_widgets(self):

        self._addr_text = "%08x" % self.addr
        self._bytes = [ ]
        if self.memory_data.content:
            for byt in self.memory_data.content:
                self._bytes.append(byt)

        if len(self._bytes) < self.memory_data.size:
            # load more from mapped memory
            start_address = self.memory_data.addr + len(self._bytes)
            size = self.memory_data.size - len(self._bytes)
            try:
                mem_bytes = self.workspace.instance.project.loader.memory.load(start_address, size)
            except KeyError:
                mem_bytes = b""
            self._bytes += [ b for b in mem_bytes ] + [ '??' ] * (size - len(mem_bytes))

        # address
        self._addr_item = QGraphicsSimpleTextItem(self._addr_text, self)
        self._addr_item.setFont(Conf.disasm_font)
        self._addr_item.setBrush(Conf.disasm_view_node_address_color)

        # label
        self._init_label_item()

        # bytes
        self._init_bytes()

        self._layout_items_and_update_size()

    def _init_label_item(self):
        lbl_text = get_label_text(self.addr, self.workspace.instance.kb)
        if lbl_text:
            self._label_item = QGraphicsSimpleTextItem(lbl_text, self)
            self._label_item.setFont(Conf.code_font)
            self._label_item.setBrush(Conf.disasm_view_label_color)
        else:
            if self._label_item is not None:
                self._label_item.setParentItem(None)
                self._label_item = None

    def _init_bytes(self):

        if self._line_items:
            # remove existing items
            for line in self._line_items:
                for item in line:
                    item.setParentItem(None)
            self._line_items = None

        addr = self.addr
        i = 0
        self._line_items = []

        while i < len(self._bytes):

            byte_offset = addr % self.bytes_per_line
            if byte_offset == 0:
                end_pos = i + self.bytes_per_line
            else:
                end_pos = self.bytes_per_line - byte_offset

            all_bytes = self._bytes[i : end_pos]
            # print("... print %#x, %d bytes, byte_offset %d" % (addr, len(all_bytes), byte_offset))
            addr_item, bytes_list, character_list = self._init_line(addr, byte_offset, all_bytes)
            self._line_items.append((byte_offset, addr_item, bytes_list, character_list))

            addr += end_pos - i
            i = end_pos

    def _init_line(self, addr, byte_offset, all_bytes):

        # colors
        printable_byte_color = Conf.disasm_view_printable_byte_color
        printable_char_color = Conf.disasm_view_printable_character_color
        unprintable_byte_color = Conf.disasm_view_unprintable_byte_color
        unprintable_char_color = Conf.disasm_view_unprintable_character_color
        unknown_byte_color = Conf.disasm_view_unknown_byte_color
        unknown_char_color = Conf.disasm_view_unknown_character_color

        # address
        addr_text = "%08x" % addr
        addr_item = QGraphicsSimpleTextItem(addr_text, self)
        addr_item.setBrush(Conf.disasm_view_node_address_color)
        addr_item.setFont(Conf.disasm_font)

        # draw each byte
        bytes_list = [ ]
        for idx, byt in enumerate(all_bytes):
            if type(byt) is int:
                if is_printable(byt):
                    color = printable_byte_color
                else:
                    color = unprintable_byte_color
                o = QGraphicsSimpleTextItem("%02x" % byt, self)
                o.setFont(Conf.disasm_font)
                o.setBrush(color)
            else:  # str, usually because it is an unknown byte, in which case the str is "??"
                o = QGraphicsSimpleTextItem(byt, self)
                o.setBrush(unknown_byte_color)
                o.setFont(Conf.disasm_font)
            bytes_list.append(o)

            line_chars = byte_offset + idx + 1  # the number of existing characters on this line, including spaces
            if line_chars % 8 == 0 and line_chars != self.bytes_per_line:
                # print a deliminator
                o = QGraphicsSimpleTextItem("-", self)
                o.setBrush(Qt.black)
                o.setFont(Conf.disasm_font)
                bytes_list.append(o)

        # printable characters
        character_list = [ ]
        for byt in all_bytes:
            if type(byt) is int:
                if is_printable(byt):
                    color = printable_char_color
                    ch = chr(byt)
                else:
                    color = unprintable_char_color
                    ch = "."
            else:
                color = unknown_char_color
                ch = "?"
            o = QGraphicsSimpleTextItem(ch, self)
            o.setBrush(color)
            o.setFont(Conf.disasm_font)
            character_list.append(o)

        return addr_item, bytes_list, character_list

    def _layout_items_and_update_size(self):

        x, y = 0, 0

        #
        # first line
        #

        # address
        self._addr_item.setPos(x, y)
        x += self._addr_item.boundingRect().width()

        # label
        if self._label_item:
            x += self.ADDRESS_LABEL_OFFSET
            self._label_item.setPos(x, y)

        #
        # the following lines: content
        #

        max_x = x
        x = 0
        y += self._addr_item.boundingRect().height()

        for byte_offset, addr_item, bytes_line, characters_line in self._line_items:
            addr_item.setPos(x, y)
            x += addr_item.boundingRect().width() + self.LINEAR_INSTRUCTION_OFFSET

            # skip byte offset
            byte_width = bytes_line[0].boundingRect().width()
            byte_spacing = byte_width // 2
            x += byte_offset * (byte_width + byte_spacing)

            all_bytes = 0
            pos = 0
            while pos < len(bytes_line):
                byte_ = bytes_line[pos]
                byte_.setPos(x, y)
                x += byte_width

                line_chars = byte_offset + all_bytes + 1  # the number of existing characters on this line, including spaces
                if line_chars % 8 == 0 and line_chars != self.bytes_per_line:
                    # now we get a delimiter
                    pos += 1
                    delimiter = bytes_line[pos]
                    delimiter.setPos(x, y)

                x += byte_spacing
                pos += 1
                all_bytes += 1

            if (byte_offset + all_bytes) % self.bytes_per_line != 0:
                more_chars = self.bytes_per_line - (byte_offset + all_bytes % self.bytes_per_line)
                x += more_chars * (byte_width + byte_spacing)

            x += self.BYTE_AREA_SPACING

            # printable characters
            character_width = characters_line[0].boundingRect().width()
            x += byte_offset * character_width
            for o in characters_line:
                o.setPos(x, y)
                x += character_width

            max_x = max(x, max_x)

            # next line!
            x = 0
            y += bytes_line[0].boundingRect().height()

        self._width = max_x
        self._height = y
        self.recalculate_size()

    def _boundingRect(self):
        return QRectF(0, 0, self._width, self._height)
class ProjectItemIcon(QGraphicsRectItem):
    def __init__(self, toolbox, x, y, w, h, project_item, icon_file,
                 icon_color, background_color):
        """Base class for project item icons drawn in Design View.

        Args:
            toolbox (ToolBoxUI): QMainWindow instance
            x (float): Icon x coordinate
            y (float): Icon y coordinate
            w (float): Icon width
            h (float): Icon height
            project_item (ProjectItem): Item
            icon_file (str): Path to icon resource
            icon_color (QColor): Icon's color
            background_color (QColor): Background color
        """
        super().__init__()
        self._toolbox = toolbox
        self._project_item = project_item
        self._moved_on_scene = False
        self.renderer = QSvgRenderer()
        self.svg_item = QGraphicsSvgItem()
        self.colorizer = QGraphicsColorizeEffect()
        self.setRect(QRectF(x, y, w, h))  # Set ellipse coordinates and size
        self.text_font_size = 10  # point size
        # Make item name graphics item.
        name = project_item.name if project_item else ""
        self.name_item = QGraphicsSimpleTextItem(name)
        shadow_effect = QGraphicsDropShadowEffect()
        shadow_effect.setOffset(1)
        shadow_effect.setEnabled(False)
        self.setGraphicsEffect(shadow_effect)
        self.set_name_attributes()  # Set font, size, position, etc.
        # Make connector buttons
        self.connectors = dict(
            bottom=ConnectorButton(self, toolbox, position="bottom"),
            left=ConnectorButton(self, toolbox, position="left"),
            right=ConnectorButton(self, toolbox, position="right"),
        )
        # Make exclamation and rank icons
        self.exclamation_icon = ExclamationIcon(self)
        self.rank_icon = RankIcon(self)
        # Group the drawn items together by setting the background rectangle as the parent of other QGraphicsItems
        # NOTE: setting the parent item moves the items as one!
        self.name_item.setParentItem(self)
        for conn in self.connectors.values():
            conn.setParentItem(self)
        self.svg_item.setParentItem(self)
        self.exclamation_icon.setParentItem(self)
        self.rank_icon.setParentItem(self)
        brush = QBrush(background_color)
        self._setup(brush, icon_file, icon_color)
        # Add items to scene
        scene = self._toolbox.ui.graphicsView.scene()
        scene.addItem(self)

    def _setup(self, brush, svg, svg_color):
        """Setup item's attributes.

        Args:
            brush (QBrush): Used in filling the background rectangle
            svg (str): Path to SVG icon file
            svg_color (QColor): Color of SVG icon
        """
        self.setPen(QPen(Qt.black, 1, Qt.SolidLine))
        self.setBrush(brush)
        self.colorizer.setColor(svg_color)
        # Load SVG
        loading_ok = self.renderer.load(svg)
        if not loading_ok:
            self._toolbox.msg_error.emit(
                "Loading SVG icon from resource:{0} failed".format(svg))
            return
        size = self.renderer.defaultSize()
        self.svg_item.setSharedRenderer(self.renderer)
        self.svg_item.setElementId(
            "")  # guess empty string loads the whole file
        dim_max = max(size.width(), size.height())
        rect_w = self.rect().width()  # Parent rect width
        margin = 32
        self.svg_item.setScale((rect_w - margin) / dim_max)
        x_offset = (rect_w - self.svg_item.sceneBoundingRect().width()) / 2
        y_offset = (rect_w - self.svg_item.sceneBoundingRect().height()) / 2
        self.svg_item.setPos(self.rect().x() + x_offset,
                             self.rect().y() + y_offset)
        self.svg_item.setGraphicsEffect(self.colorizer)
        self.setFlag(QGraphicsItem.ItemIsMovable, enabled=True)
        self.setFlag(QGraphicsItem.ItemIsSelectable, enabled=True)
        self.setFlag(QGraphicsItem.ItemIsFocusable, enabled=True)
        self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges, enabled=True)
        self.setAcceptHoverEvents(True)
        self.setCursor(Qt.PointingHandCursor)
        # Set exclamation and rank icons position
        self.exclamation_icon.setPos(
            self.rect().topRight() -
            self.exclamation_icon.sceneBoundingRect().topRight())
        self.rank_icon.setPos(self.rect().topLeft())

    def name(self):
        """Returns name of the item that is represented by this icon."""
        return self._project_item.name

    def update_name_item(self, new_name):
        """Set a new text to name item. Used when a project item is renamed."""
        self.name_item.setText(new_name)
        self.set_name_attributes()

    def set_name_attributes(self):
        """Set name QGraphicsSimpleTextItem attributes (font, size, position, etc.)"""
        # Set font size and style
        font = self.name_item.font()
        font.setPointSize(self.text_font_size)
        font.setBold(True)
        self.name_item.setFont(font)
        # Set name item position (centered on top of the master icon)
        name_width = self.name_item.boundingRect().width()
        name_height = self.name_item.boundingRect().height()
        self.name_item.setPos(
            self.rect().x() + self.rect().width() / 2 - name_width / 2,
            self.rect().y() - name_height - 4)

    def conn_button(self, position="left"):
        """Returns items connector button (QWidget)."""
        return self.connectors.get(position, self.connectors["left"])

    def outgoing_links(self):
        return [
            l for conn in self.connectors.values()
            for l in conn.outgoing_links()
        ]

    def incoming_links(self):
        return [
            l for conn in self.connectors.values()
            for l in conn.incoming_links()
        ]

    def hoverEnterEvent(self, event):
        """Sets a drop shadow effect to icon when mouse enters its boundaries.

        Args:
            event (QGraphicsSceneMouseEvent): Event
        """
        self.prepareGeometryChange()
        self.graphicsEffect().setEnabled(True)
        event.accept()

    def hoverLeaveEvent(self, event):
        """Disables the drop shadow when mouse leaves icon boundaries.

        Args:
            event (QGraphicsSceneMouseEvent): Event
        """
        self.prepareGeometryChange()
        self.graphicsEffect().setEnabled(False)
        event.accept()

    def mouseMoveEvent(self, event):
        """Moves icon(s) while the mouse button is pressed.
        Update links that are connected to selected icons.

        Args:
            event (QGraphicsSceneMouseEvent): Event
        """
        super().mouseMoveEvent(event)
        selected_icons = set(x for x in self.scene().selectedItems()
                             if isinstance(x, ProjectItemIcon))
        links = set(link for icon in selected_icons
                    for conn in icon.connectors.values()
                    for link in conn.links)
        for link in links:
            link.update_geometry()

    def mouseReleaseEvent(self, event):
        if self._moved_on_scene:
            self._moved_on_scene = False
            scene = self.scene()
            scene.shrink_if_needed()
            scene.item_move_finished.emit(self)
        super().mouseReleaseEvent(event)

    def contextMenuEvent(self, event):
        """Show item context menu.

        Args:
            event (QGraphicsSceneMouseEvent): Mouse event
        """
        self.scene().clearSelection()
        self.setSelected(True)
        self._toolbox.show_item_image_context_menu(event.screenPos(),
                                                   self.name())

    def keyPressEvent(self, event):
        """Handles deleting and rotating the selected
        item when dedicated keys are pressed.

        Args:
            event (QKeyEvent): Key event
        """
        if event.key() == Qt.Key_Delete and self.isSelected():
            ind = self._toolbox.project_item_model.find_item(self.name())
            delete_int = int(self._toolbox.qsettings().value(
                "appSettings/deleteData", defaultValue="0"))
            delete_bool = delete_int != 0
            self._toolbox.remove_item(ind, delete_item=delete_bool)
            event.accept()
        elif event.key() == Qt.Key_R and self.isSelected():
            # TODO:
            # 1. Change name item text direction when rotating
            # 2. Save rotation into project file
            rect = self.mapToScene(self.boundingRect()).boundingRect()
            center = rect.center()
            t = QTransform()
            t.translate(center.x(), center.y())
            t.rotate(90)
            t.translate(-center.x(), -center.y())
            self.setPos(t.map(self.pos()))
            self.setRotation(self.rotation() + 90)
            links = set(lnk for conn in self.connectors.values()
                        for lnk in conn.links)
            for link in links:
                link.update_geometry()
            event.accept()
        else:
            super().keyPressEvent(event)

    def itemChange(self, change, value):
        """
        Reacts to item removal and position changes.

        In particular, destroys the drop shadow effect when the items is removed from a scene
        and keeps track of item's movements on the scene.

        Args:
            change (GraphicsItemChange): a flag signalling the type of the change
            value: a value related to the change

        Returns:
             Whatever super() does with the value parameter
        """
        if change == QGraphicsItem.ItemScenePositionHasChanged:
            self._moved_on_scene = True
        elif change == QGraphicsItem.GraphicsItemChange.ItemSceneChange and value is None:
            self.prepareGeometryChange()
            self.setGraphicsEffect(None)
        return super().itemChange(change, value)

    def show_item_info(self):
        """Update GUI to show the details of the selected item."""
        ind = self._toolbox.project_item_model.find_item(self.name())
        self._toolbox.ui.treeView_project.setCurrentIndex(ind)