diff options
| author | Josh Ferrell <josh@vector35.com> | 2024-08-06 16:22:55 -0400 |
|---|---|---|
| committer | Josh Ferrell <josh@vector35.com> | 2024-08-07 16:26:05 -0400 |
| commit | f5097c5f3b2fa4d10056b559a95568b1fe4116d3 (patch) | |
| tree | 73a4b0613dfbca084ec6226c7c4ad99d5afd03db /python/collaboration | |
| parent | 264a5cf77a2562ac07d67a9e71babb6a5a14e31c (diff) | |
Expose Collaboration::ConflictSplitter in API
Diffstat (limited to 'python/collaboration')
| -rw-r--r-- | python/collaboration/examples/custom_conflict_splitter.py | 42 | ||||
| -rw-r--r-- | python/collaboration/merge.py | 158 |
2 files changed, 200 insertions, 0 deletions
diff --git a/python/collaboration/examples/custom_conflict_splitter.py b/python/collaboration/examples/custom_conflict_splitter.py new file mode 100644 index 00000000..a1c04172 --- /dev/null +++ b/python/collaboration/examples/custom_conflict_splitter.py @@ -0,0 +1,42 @@ +# coding=utf-8 +# Copyright (c) 2015-2024 Vector 35 Inc +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. +import random +from typing import Dict, Optional + +from binaryninja.collaboration.merge import ConflictSplitter, MergeConflict +from binaryninja.database import KeyValueStore + + +class RNGConflictSplitter(ConflictSplitter): + def get_name(self) -> str: + return "RNG Conflict Splitter" + + def can_split(self, key: str, conflict: MergeConflict): + # Only handle metadata entries + return conflict.type == "value_store_entry" + + def split(self, key: str, conflict: MergeConflict, result: KeyValueStore) -> Optional[Dict[str, MergeConflict]]: + # Choose a random side to win + conflict.success(random.choice([conflict.first, conflict.second])) + return {} + +splitter = RNGConflictSplitter() +splitter.register() diff --git a/python/collaboration/merge.py b/python/collaboration/merge.py index af06d602..2c64d304 100644 --- a/python/collaboration/merge.py +++ b/python/collaboration/merge.py @@ -7,6 +7,7 @@ from typing import Dict, Optional from .. import _binaryninjacore as core from ..enums import MergeConflictDataType +from ..database import KeyValueStore from . import util from ..database import Database, Snapshot @@ -238,3 +239,160 @@ class ConflictHandler: :return: True if all conflicts were successfully merged """ raise NotImplementedError("Not implemented") + + +class ConflictSplitter: + """ + Helper class that takes one merge conflict and splits it into multiple conflicts + Eg takes conflicts for View/symbols and splits to one conflict per symbol + """ + + def __init__(self, handle=None): + if handle is not None: + self._handle = handle + + def register(self): + self._cb = core.BNAnalysisMergeConflictSplitterCallbacks() + self._cb.context = 0 + self._cb.getName = self._cb.getName.__class__(self._get_name) + self._cb.reset = self._cb.reset.__class__(self._reset) + self._cb.finished = self._cb.finished.__class__(self._finished) + self._cb.canSplit = self._cb.canSplit.__class__(self._can_split) + self._cb.split = self._cb.split.__class__(self._split) + self._cb.freeName = self._cb.freeName.__class__(self._free_name) + self._cb.freeKeyList = self._cb.freeKeyList.__class__(self._free_key_list) + self._cb.freeConflictList = self._cb.freeConflictList.__class__(self._free_conflict_list) + self._handle = core.BNRegisterAnalysisMergeConflictSplitter(self._cb) + self._split_keys = None + self._split_conflicts = None + + + def _get_name(self, ctxt: ctypes.c_void_p) -> ctypes.c_char_p: + try: + return core.BNAllocString(core.cstr(self.name)) + except: + # Not sure why your get_name() would throw but let's handle it anyway + traceback.print_exc(file=sys.stderr) + return core.BNAllocString(core.cstr(type(self).__name__)) + + def _reset(self, ctxt: ctypes.c_void_p): + try: + self.reset() + except: + traceback.print_exc(file=sys.stderr) + + def _finished(self, ctxt: ctypes.c_void_p): + try: + self.finished() + except: + traceback.print_exc(file=sys.stderr) + + def _can_split(self, ctxt: ctypes.c_void_p, key: ctypes.c_char_p, conflict: core.BNAnalysisMergeConflictHandle) -> bool: + try: + py_conflict = MergeConflict(handle=conflict) + return self.can_split(core.pyNativeStr(key), py_conflict) + except: + traceback.print_exc(file=sys.stderr) + return False + + def _split( + self, + ctxt: ctypes.c_void_p, + original_key: ctypes.c_char_p, + original_conflict: core.BNAnalysisMergeConflictHandle, + result_kvs: core.BNKeyValueStoreHandle, + new_keys: ctypes.POINTER(ctypes.POINTER(ctypes.c_char_p)), + new_conflicts: ctypes.POINTER(core.BNAnalysisMergeConflictHandle), + new_count: ctypes.POINTER(ctypes.c_size_t) + ) -> bool: + try: + py_original_conflict = MergeConflict(handle=original_conflict) + py_result_kvs = KeyValueStore(handle=ctypes.cast(result_kvs, core.BNKeyValueStoreHandle)) + result = self.split(core.pyNativeStr(original_key), py_original_conflict, py_result_kvs) + + if result is None: + return False + + new_count[0] = ctypes.c_size_t(len(result)) + new_keys[0] = (ctypes.c_char_p * len(result))() + new_conflicts[0] = (core.BNAnalysisMergeConflictHandle * len(result))() + + self._split_keys = [] + self._split_conflicts = [] + + for (i, (key, conflict)) in enumerate(result): + self._split_keys.append(key) + self._split_conflicts.append(conflict) + + new_keys[0][i] = core.cstr(self._split_keys[-1]) + new_conflicts[0][i] = self._split_conflicts[-1]._handle + + return True + except: + traceback.print_exc(file=sys.stderr) + return False + + def _free_name(self, ctxt: ctypes.c_void_p, name: ctypes.c_char_p): + core.BNFreeString(name) + + def _free_key_list(self, ctxt: ctypes.c_void_p, key_list: ctypes.POINTER(ctypes.c_char_p), count: ctypes.c_size_t): + del self._split_keys + + def _free_conflict_list(self, ctxt: ctypes.c_void_p, conflict_list: core.BNAnalysisMergeConflictHandle, count: ctypes.c_size_t): + del self._split_conflicts + + @property + def name(self): + """ + Get a friendly name for the splitter + + :return: Name of the splitter + """ + return self.get_name() + + def get_name(self) -> str: + """ + Get a friendly name for the splitter + + :return: Name of the splitter + """ + return type(self).__name__ + + def reset(self): + """ + Reset any internal state the splitter may hold during the merge + """ + return + + def finished(self): + """ + Clean up any internal state after the merge operation has finished + """ + return + + @abc.abstractmethod + def can_split(self, key: str, conflict: MergeConflict) -> bool: + """ + Test if the splitter applies to a given conflict (by key). + + :param key: Key of the conflicting field + :param conflict: Conflict data + :return: True if this splitter should be used on the conflict + """ + raise NotImplementedError("Not implemented") + + @abc.abstractmethod + def split(self, key: str, conflict: MergeConflict, result: KeyValueStore) -> Optional[Dict[str, MergeConflict]]: + """ + Split a field conflict into any number of alternate conflicts. + Note: Returned conflicts will also be checked for splitting, beware infinite loops! + If this function raises, it will be treated as returning None + + :param key: Original conflicting field's key + :param conflict: Original conflict data + :param result: Kvs structure containing the result of all splits. You should use the original conflict's + success() function in most cases unless you specifically want to write a new key to this. + :return: A collection of conflicts into which the original conflict was split, or None if + this splitter cannot handle the conflict + """ + raise NotImplementedError("Not implemented") |
