From 669215df43f6e17b384c08d3320fb2c2cbaa3312 Mon Sep 17 00:00:00 2001 From: Zion Leonahenahe Basque Date: Tue, 12 Nov 2024 16:21:49 -0700 Subject: [PATCH] Feat: Add API for tracking mouse movements (#136) --- libbs/__init__.py | 2 +- libbs/api/decompiler_interface.py | 2 ++ libbs/artifacts/context.py | 22 +++++++++++++++++++++- libbs/decompilers/ida/compat.py | 14 ++++++++++---- libbs/decompilers/ida/hooks.py | 27 ++++++++++++++++++++++----- 5 files changed, 56 insertions(+), 11 deletions(-) diff --git a/libbs/__init__.py b/libbs/__init__.py index 304f97f..52962ce 100644 --- a/libbs/__init__.py +++ b/libbs/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.6.0" +__version__ = "2.7.0" import logging diff --git a/libbs/api/decompiler_interface.py b/libbs/api/decompiler_interface.py index 7539469..d971620 100644 --- a/libbs/api/decompiler_interface.py +++ b/libbs/api/decompiler_interface.py @@ -66,6 +66,7 @@ def __init__( decompiler_closed_callbacks: Optional[List[Callable]] = None, thread_artifact_callbacks: bool = True, force_click_recording: bool = False, + track_mouse_moves: bool = False, ): self.name = name self.art_lifter = artifact_lifter @@ -86,6 +87,7 @@ def __init__( self.gui_plugin = None self.artifact_watchers_started = False self.force_click_recording = force_click_recording + self.track_mouse_moves = track_mouse_moves # locks self.artifact_write_lock = threading.Lock() diff --git a/libbs/artifacts/context.py b/libbs/artifacts/context.py index 3c1bc91..f497059 100644 --- a/libbs/artifacts/context.py +++ b/libbs/artifacts/context.py @@ -4,12 +4,20 @@ class Context(Artifact): + ACT_VIEW_OPEN = "view_open" + ACT_MOUSE_CLICK = "mouse_click" + ACT_MOUSE_MOVE = "mouse_move" + ACT_UNKNOWN = "unknown" + __slots__ = Artifact.__slots__ + ( "addr", "func_addr", "line_number", + "col_number", "screen_name", - "variable" + "variable", + "action", + "extras", ) def __init__( @@ -17,15 +25,21 @@ def __init__( addr: Optional[int] = None, func_addr: Optional[int] = None, line_number: Optional[int] = None, + col_number: Optional[int] = None, screen_name: Optional[str] = None, variable: Optional[str] = None, + action: Optional[str] = None, + extras: Optional[dict] = None, **kwargs ): self.addr = addr self.func_addr = func_addr self.line_number = line_number + self.col_number = col_number self.screen_name = screen_name self.variable = variable + self.action: str = action or self.ACT_UNKNOWN + self.extras = extras or {} super().__init__(**kwargs) def __str__(self): @@ -37,5 +51,11 @@ def __str__(self): post_text = hex(self.addr) + post_text if self.line_number is not None: post_text += f" line={self.line_number}" + if self.col_number is not None: + post_text += f" col={self.col_number}" + if self.action != self.ACT_UNKNOWN: + post_text += f" action={self.action}" + if self.extras: + post_text += f" extras={self.extras}" return f"" diff --git a/libbs/decompilers/ida/compat.py b/libbs/decompilers/ida/compat.py index 25343d5..3505cfe 100644 --- a/libbs/decompilers/ida/compat.py +++ b/libbs/decompilers/ida/compat.py @@ -1671,27 +1671,33 @@ def get_decompiler_version() -> typing.Optional[Version]: return vers -def view_to_bs_context(view, get_var=True) -> typing.Optional[Context]: +def view_to_bs_context(view, get_var=True, action: str = Context.ACT_UNKNOWN) -> typing.Optional[Context]: form_type = idaapi.get_widget_type(view) if form_type is None: return None form_to_type_name = get_form_to_type_name() view_name = form_to_type_name.get(form_type, "unknown") - ctx = Context(screen_name=view_name) + ctx = Context(screen_name=view_name, action=action) if view_name in FUNC_FORMS: ctx.addr = idaapi.get_screen_ea() func = idaapi.get_func(ctx.addr) if func is not None: ctx.func_addr = func.start_ea - if get_var and view_name == "decompilation": + # exit early when we are still rendering the screen (no real click info) + if action == Context.ACT_MOUSE_MOVE: + return ctx + + if view_name == "decompilation" and get_var: # get lvar info at cursor vu = idaapi.get_widget_vdui(view) if vu and vu.item: lvar = vu.item.get_lvar() if lvar: ctx.variable = lvar.name - ctx.line_number = vu.cpos.lnnum if vu.cpos else None + if vu.cpos is not None: + ctx.line_number = vu.cpos.lnnum + ctx.col_number = vu.cpos.x return ctx diff --git a/libbs/decompilers/ida/hooks.py b/libbs/decompilers/ida/hooks.py index ad40476..b547c81 100644 --- a/libbs/decompilers/ida/hooks.py +++ b/libbs/decompilers/ida/hooks.py @@ -89,26 +89,43 @@ def __init__(self, interface: "IDAInterface"): super(ScreenHook, self).__init__() def view_click(self, view, event): - self._handle_view_event(view, click=True) + self._handle_view_event(view, action_type=Context.ACT_MOUSE_CLICK) def view_activated(self, view: "TWidget *"): - self._handle_view_event(view) + self._handle_view_event(view, action_type=Context.ACT_VIEW_OPEN) - def _handle_view_event(self, view, click=False): + def view_mouse_moved(self, view: "TWidget *", event: "view_mouse_event_t"): + if self.interface.track_mouse_moves: + self._handle_view_event(view, ida_event=event, action_type=Context.ACT_MOUSE_MOVE) + + def _handle_view_event(self, view, action_type=Context.ACT_UNKNOWN, ida_event=None): if self.interface.force_click_recording or self.interface.artifact_watchers_started: # drop ctx for speed when the artifact watches have not been officially started, and we are not clicking - if (self.interface.force_click_recording and not self.interface.artifact_watchers_started) and not click: + if (self.interface.force_click_recording and not self.interface.artifact_watchers_started) and \ + action_type != Context.ACT_MOUSE_CLICK: return - ctx = compat.view_to_bs_context(view) + ctx = compat.view_to_bs_context(view, action=action_type) if ctx is None: return + # handle special case of mouse move + if action_type == Context.ACT_MOUSE_MOVE and ida_event is not None: + ctx.line_number = ida_event.renderer_pos.cy + ctx.col_number = ida_event.renderer_pos.cx + if ctx.screen_name == "disassembly" and ida_event.renderer_pos.node != -1: + # TODO: this is not an addr, but the node number in graph view + ctx.extras['node'] = ida_event.renderer_pos.node + elif ctx.screen_name == "decompilation": + # TODO: the address is useless here! + ctx.addr = ctx.func_addr + ctx = self.interface.art_lifter.lift(ctx) self.interface._gui_active_context = ctx self.interface.gui_context_changed(ctx) + class IDAHotkeyHook(ida_kernwin.UI_Hooks): def __init__(self, keys_to_pass, uiptr): super().__init__()