Skip to content

Commit f5097c5

Browse files
committed
Expose Collaboration::ConflictSplitter in API
1 parent 264a5cf commit f5097c5

File tree

3 files changed

+220
-0
lines changed

3 files changed

+220
-0
lines changed

binaryninjacore.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ extern "C"
291291
typedef struct BNCollaborationUndoEntry BNCollaborationUndoEntry;
292292
typedef struct BNCollaborationUser BNCollaborationUser;
293293
typedef struct BNAnalysisMergeConflict BNAnalysisMergeConflict;
294+
typedef struct BNAnalysisMergeConflictSplitter BNAnalysisMergeConflictSplitter;
294295
typedef struct BNTypeArchiveMergeConflict BNTypeArchiveMergeConflict;
295296
typedef struct BNCollaborationLazyT BNCollaborationLazyT;
296297
typedef struct BNUndoAction BNUndoAction;
@@ -3301,6 +3302,18 @@ extern "C"
33013302
BinaryConflictDataType
33023303
} BNMergeConflictDataType;
33033304

3305+
typedef struct BNAnalysisMergeConflictSplitterCallbacks
3306+
{
3307+
void* context;
3308+
char* (*getName)(void* context);
3309+
void (*reset)(void* context);
3310+
void (*finished)(void* context);
3311+
bool (*canSplit)(void* context, const char* key, const BNAnalysisMergeConflict* conflict);
3312+
bool (*split)(void* context, const char* originalKey, const BNAnalysisMergeConflict* originalConflict, BNKeyValueStore* result, char*** newKeys, BNAnalysisMergeConflict*** newConflicts, size_t* newCount);
3313+
void (*freeName)(void* context, char* name);
3314+
void (*freeKeyList)(void* context, char** keyList, size_t count);
3315+
void (*freeConflictList)(void* context, BNAnalysisMergeConflict** conflictList, size_t count);
3316+
} BNAnalysisMergeConflictSplitterCallbacks;
33043317

33053318
typedef bool(*BNProgressFunction)(void*, size_t, size_t);
33063319
typedef bool(*BNCollaborationAnalysisConflictHandler)(void*, const char** keys, BNAnalysisMergeConflict** conflicts, size_t conflictCount);
@@ -7580,6 +7593,13 @@ extern "C"
75807593
BINARYNINJACOREAPI char* BNCollaborationChangesetGetName(BNCollaborationChangeset* changeset);
75817594
BINARYNINJACOREAPI bool BNCollaborationChangesetSetName(BNCollaborationChangeset* changeset, const char* name);
75827595

7596+
// AnalysisMergeConflictSplitter
7597+
BINARYNINJACOREAPI BNAnalysisMergeConflictSplitter* BNRegisterAnalysisMergeConflictSplitter(BNAnalysisMergeConflictSplitterCallbacks* callbacks);
7598+
BINARYNINJACOREAPI BNAnalysisMergeConflictSplitter** BNGetAnalysisMergeConflictSplitterList(size_t* count);
7599+
BINARYNINJACOREAPI void BNFreeAnalysisMergeConflictSplitterList(BNAnalysisMergeConflictSplitter** splitters, size_t count);
7600+
BINARYNINJACOREAPI char* BNAnalysisMergeConflictSplitterGetName(BNAnalysisMergeConflictSplitter* splitter);
7601+
BINARYNINJACOREAPI bool BNAnalysisMergeConflictSplitterCanSplit(BNAnalysisMergeConflictSplitter* splitter, const char* key, BNAnalysisMergeConflict* conflict);
7602+
BINARYNINJACOREAPI bool BNAnalysisMergeConflictSplitterSplit(BNAnalysisMergeConflictSplitter* splitter, const char* originalKey, BNAnalysisMergeConflict* originalConflict, BNKeyValueStore* result, char*** newKeys, BNAnalysisMergeConflict*** newConflicts, size_t* newCount);
75837603

75847604
#ifdef __cplusplus
75857605
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# coding=utf-8
2+
# Copyright (c) 2015-2024 Vector 35 Inc
3+
#
4+
# Permission is hereby granted, free of charge, to any person obtaining a copy
5+
# of this software and associated documentation files (the "Software"), to
6+
# deal in the Software without restriction, including without limitation the
7+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
8+
# sell copies of the Software, and to permit persons to whom the Software is
9+
# furnished to do so, subject to the following conditions:
10+
#
11+
# The above copyright notice and this permission notice shall be included in
12+
# all copies or substantial portions of the Software.
13+
#
14+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20+
# IN THE SOFTWARE.
21+
import random
22+
from typing import Dict, Optional
23+
24+
from binaryninja.collaboration.merge import ConflictSplitter, MergeConflict
25+
from binaryninja.database import KeyValueStore
26+
27+
28+
class RNGConflictSplitter(ConflictSplitter):
29+
def get_name(self) -> str:
30+
return "RNG Conflict Splitter"
31+
32+
def can_split(self, key: str, conflict: MergeConflict):
33+
# Only handle metadata entries
34+
return conflict.type == "value_store_entry"
35+
36+
def split(self, key: str, conflict: MergeConflict, result: KeyValueStore) -> Optional[Dict[str, MergeConflict]]:
37+
# Choose a random side to win
38+
conflict.success(random.choice([conflict.first, conflict.second]))
39+
return {}
40+
41+
splitter = RNGConflictSplitter()
42+
splitter.register()

python/collaboration/merge.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from .. import _binaryninjacore as core
99
from ..enums import MergeConflictDataType
10+
from ..database import KeyValueStore
1011
from . import util
1112

1213
from ..database import Database, Snapshot
@@ -238,3 +239,160 @@ def handle(self, conflicts: Dict[str, MergeConflict]) -> bool:
238239
:return: True if all conflicts were successfully merged
239240
"""
240241
raise NotImplementedError("Not implemented")
242+
243+
244+
class ConflictSplitter:
245+
"""
246+
Helper class that takes one merge conflict and splits it into multiple conflicts
247+
Eg takes conflicts for View/symbols and splits to one conflict per symbol
248+
"""
249+
250+
def __init__(self, handle=None):
251+
if handle is not None:
252+
self._handle = handle
253+
254+
def register(self):
255+
self._cb = core.BNAnalysisMergeConflictSplitterCallbacks()
256+
self._cb.context = 0
257+
self._cb.getName = self._cb.getName.__class__(self._get_name)
258+
self._cb.reset = self._cb.reset.__class__(self._reset)
259+
self._cb.finished = self._cb.finished.__class__(self._finished)
260+
self._cb.canSplit = self._cb.canSplit.__class__(self._can_split)
261+
self._cb.split = self._cb.split.__class__(self._split)
262+
self._cb.freeName = self._cb.freeName.__class__(self._free_name)
263+
self._cb.freeKeyList = self._cb.freeKeyList.__class__(self._free_key_list)
264+
self._cb.freeConflictList = self._cb.freeConflictList.__class__(self._free_conflict_list)
265+
self._handle = core.BNRegisterAnalysisMergeConflictSplitter(self._cb)
266+
self._split_keys = None
267+
self._split_conflicts = None
268+
269+
270+
def _get_name(self, ctxt: ctypes.c_void_p) -> ctypes.c_char_p:
271+
try:
272+
return core.BNAllocString(core.cstr(self.name))
273+
except:
274+
# Not sure why your get_name() would throw but let's handle it anyway
275+
traceback.print_exc(file=sys.stderr)
276+
return core.BNAllocString(core.cstr(type(self).__name__))
277+
278+
def _reset(self, ctxt: ctypes.c_void_p):
279+
try:
280+
self.reset()
281+
except:
282+
traceback.print_exc(file=sys.stderr)
283+
284+
def _finished(self, ctxt: ctypes.c_void_p):
285+
try:
286+
self.finished()
287+
except:
288+
traceback.print_exc(file=sys.stderr)
289+
290+
def _can_split(self, ctxt: ctypes.c_void_p, key: ctypes.c_char_p, conflict: core.BNAnalysisMergeConflictHandle) -> bool:
291+
try:
292+
py_conflict = MergeConflict(handle=conflict)
293+
return self.can_split(core.pyNativeStr(key), py_conflict)
294+
except:
295+
traceback.print_exc(file=sys.stderr)
296+
return False
297+
298+
def _split(
299+
self,
300+
ctxt: ctypes.c_void_p,
301+
original_key: ctypes.c_char_p,
302+
original_conflict: core.BNAnalysisMergeConflictHandle,
303+
result_kvs: core.BNKeyValueStoreHandle,
304+
new_keys: ctypes.POINTER(ctypes.POINTER(ctypes.c_char_p)),
305+
new_conflicts: ctypes.POINTER(core.BNAnalysisMergeConflictHandle),
306+
new_count: ctypes.POINTER(ctypes.c_size_t)
307+
) -> bool:
308+
try:
309+
py_original_conflict = MergeConflict(handle=original_conflict)
310+
py_result_kvs = KeyValueStore(handle=ctypes.cast(result_kvs, core.BNKeyValueStoreHandle))
311+
result = self.split(core.pyNativeStr(original_key), py_original_conflict, py_result_kvs)
312+
313+
if result is None:
314+
return False
315+
316+
new_count[0] = ctypes.c_size_t(len(result))
317+
new_keys[0] = (ctypes.c_char_p * len(result))()
318+
new_conflicts[0] = (core.BNAnalysisMergeConflictHandle * len(result))()
319+
320+
self._split_keys = []
321+
self._split_conflicts = []
322+
323+
for (i, (key, conflict)) in enumerate(result):
324+
self._split_keys.append(key)
325+
self._split_conflicts.append(conflict)
326+
327+
new_keys[0][i] = core.cstr(self._split_keys[-1])
328+
new_conflicts[0][i] = self._split_conflicts[-1]._handle
329+
330+
return True
331+
except:
332+
traceback.print_exc(file=sys.stderr)
333+
return False
334+
335+
def _free_name(self, ctxt: ctypes.c_void_p, name: ctypes.c_char_p):
336+
core.BNFreeString(name)
337+
338+
def _free_key_list(self, ctxt: ctypes.c_void_p, key_list: ctypes.POINTER(ctypes.c_char_p), count: ctypes.c_size_t):
339+
del self._split_keys
340+
341+
def _free_conflict_list(self, ctxt: ctypes.c_void_p, conflict_list: core.BNAnalysisMergeConflictHandle, count: ctypes.c_size_t):
342+
del self._split_conflicts
343+
344+
@property
345+
def name(self):
346+
"""
347+
Get a friendly name for the splitter
348+
349+
:return: Name of the splitter
350+
"""
351+
return self.get_name()
352+
353+
def get_name(self) -> str:
354+
"""
355+
Get a friendly name for the splitter
356+
357+
:return: Name of the splitter
358+
"""
359+
return type(self).__name__
360+
361+
def reset(self):
362+
"""
363+
Reset any internal state the splitter may hold during the merge
364+
"""
365+
return
366+
367+
def finished(self):
368+
"""
369+
Clean up any internal state after the merge operation has finished
370+
"""
371+
return
372+
373+
@abc.abstractmethod
374+
def can_split(self, key: str, conflict: MergeConflict) -> bool:
375+
"""
376+
Test if the splitter applies to a given conflict (by key).
377+
378+
:param key: Key of the conflicting field
379+
:param conflict: Conflict data
380+
:return: True if this splitter should be used on the conflict
381+
"""
382+
raise NotImplementedError("Not implemented")
383+
384+
@abc.abstractmethod
385+
def split(self, key: str, conflict: MergeConflict, result: KeyValueStore) -> Optional[Dict[str, MergeConflict]]:
386+
"""
387+
Split a field conflict into any number of alternate conflicts.
388+
Note: Returned conflicts will also be checked for splitting, beware infinite loops!
389+
If this function raises, it will be treated as returning None
390+
391+
:param key: Original conflicting field's key
392+
:param conflict: Original conflict data
393+
:param result: Kvs structure containing the result of all splits. You should use the original conflict's
394+
success() function in most cases unless you specifically want to write a new key to this.
395+
:return: A collection of conflicts into which the original conflict was split, or None if
396+
this splitter cannot handle the conflict
397+
"""
398+
raise NotImplementedError("Not implemented")

0 commit comments

Comments
 (0)