summaryrefslogtreecommitdiff
path: root/python/collaboration
diff options
context:
space:
mode:
authorJosh Ferrell <josh@vector35.com>2024-08-06 16:22:55 -0400
committerJosh Ferrell <josh@vector35.com>2024-08-07 16:26:05 -0400
commitf5097c5f3b2fa4d10056b559a95568b1fe4116d3 (patch)
tree73a4b0613dfbca084ec6226c7c4ad99d5afd03db /python/collaboration
parent264a5cf77a2562ac07d67a9e71babb6a5a14e31c (diff)
Expose Collaboration::ConflictSplitter in API
Diffstat (limited to 'python/collaboration')
-rw-r--r--python/collaboration/examples/custom_conflict_splitter.py42
-rw-r--r--python/collaboration/merge.py158
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")