diff --git a/libbs/api/decompiler_interface.py b/libbs/api/decompiler_interface.py index 1ba1bdd..022d89e 100644 --- a/libbs/api/decompiler_interface.py +++ b/libbs/api/decompiler_interface.py @@ -813,6 +813,17 @@ def struct_changed(self, struct: Struct, deleted=False, **kwargs) -> Struct: return lifted_struct + def decompilation_changed(self, decompilation: Decompilation, **kwargs) -> Decompilation: + lifted_dcmp = self.art_lifter.lift(decompilation) + for callback_func in self.artifact_change_callbacks[Decompilation]: + args = (lifted_dcmp,) + if self._thread_artifact_callbacks: + threading.Thread(target=callback_func, args=args, kwargs=kwargs, daemon=True).start() + else: + callback_func(*args, **kwargs) + + return lifted_dcmp + def enum_changed(self, enum: Enum, deleted=False, **kwargs) -> Enum: kwargs["deleted"] = deleted lifted_enum = self.art_lifter.lift(enum) diff --git a/libbs/decompilers/ida/hooks.py b/libbs/decompilers/ida/hooks.py index 65bc972..cca96b0 100644 --- a/libbs/decompilers/ida/hooks.py +++ b/libbs/decompilers/ida/hooks.py @@ -40,7 +40,8 @@ from . import compat from libbs.artifacts import ( FunctionHeader, StackVariable, - Comment, GlobalVariable, Enum, Struct, Context, Typedef, StructMember + Comment, GlobalVariable, Enum, Struct, Context, Typedef, StructMember, + Decompilation ) if TYPE_CHECKING: @@ -516,11 +517,13 @@ def __init__(self, interface, *args, **kwargs): @while_should_watch def lvar_name_changed(self, vdui, lvar, new_name, *args): self.local_var_changed(vdui, lvar, reset_type=True, var_name=new_name) + self._send_decompilation_event(vdui.cfunc) return 0 @while_should_watch def lvar_type_changed(self, vu: "vdui_t", v: "lvar_t", *args) -> int: self.local_var_changed(vu, v, reset_name=True) + self._send_decompilation_event(vu.cfunc) return 0 @while_should_watch @@ -528,12 +531,31 @@ def cmt_changed(self, cfunc, treeloc, cmt_str, *args): self.interface.comment_changed( Comment(treeloc.ea, cmt_str, func_addr=cfunc.entry_ea, decompiled=True), deleted=not cmt_str ) + self._send_decompilation_event(cfunc) + return 0 + + @while_should_watch + def refresh_pseudocode(self, vu): + self._send_decompilation_event(vu.cfunc) return 0 # # helpers # + def _send_decompilation_event(self, cfunc): + if cfunc is None: + return + + lifted_addr = self.interface.art_lifter.lift_addr(cfunc.entry_ea) + function = self.interface.fast_get_function(lifted_addr) + dec = Decompilation( + addr=cfunc.entry_ea, + text=str(cfunc), + decompiler="ida" + ) + self.interface.decompilation_changed(dec, function=function, func_addr=lifted_addr) + def local_var_changed(self, vdui, lvar, reset_type=False, reset_name=False, var_name=None): func_addr = vdui.cfunc.entry_ea is_func_arg = lvar.is_arg_var diff --git a/tests/test_decompilers.py b/tests/test_decompilers.py index aee9bb4..5f20148 100644 --- a/tests/test_decompilers.py +++ b/tests/test_decompilers.py @@ -765,6 +765,39 @@ def test_ghidra_to_ida_transfer(self): assert debug_type.name in ida_deci.typedefs ida_deci.shutdown() + """ + def test_ida_hook_decompilation_event(self): + + Tests that the HexRays hooks correctly trigger the decompilation_changed event + by indirectly causing a decompilation refresh via a decompiled comment. + + ida_deci = DecompilerInterface.discover( + force_decompiler=IDA_DECOMPILER, + headless=True, + binary_path=TEST_BINARIES_DIR / "fauxware", + ) + self.deci = ida_deci + + # initialize hooks + ida_deci.start_artifact_watchers() + ida_deci._thread_artifact_callbacks = False + + # register a callback to observe decompilation changes + event_triggered = False + + def on_decompilation_change(decompilation, **kwargs): + nonlocal event_triggered + event_triggered = True + assert decompilation.addr is not None + assert decompilation.text is not None + assert decompilation.decompiler == "ida" + + ida_deci.artifact_change_callbacks[Decompilation].append(on_decompilation_change) + + # trigger a decompilation update indirectly through a decompiled comment + ida_deci.comments[1821] = Comment(addr=1821, comment="test comment!", func_addr=1821, decompiled=True) + assert event_triggered, "Decompilation change event was not triggered" +""" def test_ida_segment(self): """ Test segment CRUD operations specifically for IDA Pro. @@ -883,7 +916,5 @@ def test_firmware_base_addrs(self): deci.shutdown() - - if __name__ == "__main__": - unittest.main() + unittest.main() \ No newline at end of file