Skip to content

Commit 33ed90f

Browse files
committed
Add Python format string provider API
1 parent 3121eab commit 33ed90f

2 files changed

Lines changed: 205 additions & 0 deletions

File tree

python/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
from .renderlayer import *
8585
from .constantrenderer import *
8686
from .stringrecognizer import *
87+
from .formatstringresolutionprovider import *
8788
# We import each of these by name to prevent conflicts between
8889
# log.py and the function 'log' which we don't import below
8990
from .log import (
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# Copyright (c) 2015-2026 Vector 35 Inc
2+
#
3+
# Permission is hereby granted, free of charge, to any person obtaining a copy
4+
# of this software and associated documentation files (the "Software"), to
5+
# deal in the Software without restriction, including without limitation the
6+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7+
# sell copies of the Software, and to permit persons to whom the Software is
8+
# furnished to do so, subject to the following conditions:
9+
#
10+
# The above copyright notice and this permission notice shall be included in
11+
# all copies or substantial portions of the Software.
12+
#
13+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
19+
# IN THE SOFTWARE.
20+
21+
import ctypes
22+
from typing import Any, List, Optional
23+
24+
import binaryninja
25+
import binaryninja._binaryninjacore as core
26+
from . import platform as _platform
27+
from . import types as _types
28+
from .log import log_error_for_exception
29+
30+
31+
class _FormatStringResolutionProviderMetaclass(type):
32+
def __iter__(self):
33+
binaryninja._init_plugins()
34+
count = ctypes.c_ulonglong()
35+
providers = core.BNGetFormatStringResolutionProviderList(count)
36+
try:
37+
for i in range(count.value):
38+
yield FormatStringResolutionProvider(providers[i])
39+
finally:
40+
core.BNFreeFormatStringResolutionProviderList(providers)
41+
42+
def __getitem__(self, value):
43+
binaryninja._init_plugins()
44+
provider = core.BNGetFormatStringResolutionProviderByName(str(value))
45+
if provider is None:
46+
raise KeyError(f"'{value}' is not a valid format string resolution provider")
47+
return FormatStringResolutionProvider(provider)
48+
49+
def __contains__(cls: '_FormatStringResolutionProviderMetaclass', name: object) -> bool:
50+
if not isinstance(name, str):
51+
return False
52+
try:
53+
cls[name]
54+
return True
55+
except KeyError:
56+
return False
57+
58+
def get(
59+
cls: '_FormatStringResolutionProviderMetaclass', name: str, default: Any = None
60+
) -> Optional['FormatStringResolutionProvider']:
61+
try:
62+
return cls[name]
63+
except KeyError:
64+
if default is not None:
65+
return default
66+
return None
67+
68+
69+
class FormatStringResolutionProvider(metaclass=_FormatStringResolutionProviderMetaclass):
70+
"""
71+
``FormatStringResolutionProvider`` resolves the variadic argument types described by a format string.
72+
73+
To implement a provider, subclass this class, set :py:attr:`name`, implement
74+
:py:meth:`perform_is_valid`, and call :py:meth:`register`. The confidence attached to each returned
75+
:py:class:`Type` is preserved when the result crosses the core API boundary.
76+
"""
77+
name = None
78+
_registered_providers = []
79+
80+
def __init__(self, handle=None):
81+
self._pending_type_lists = {}
82+
if handle is not None:
83+
self.handle = core.handle_of_type(handle, core.BNFormatStringResolutionProvider)
84+
self.__dict__["name"] = core.BNGetFormatStringResolutionProviderName(handle)
85+
86+
def __repr__(self):
87+
return f"<FormatStringResolutionProvider: {self.name}>"
88+
89+
def register(self):
90+
"""Register this provider with the Binary Ninja core."""
91+
if self.__class__.name is None:
92+
raise ValueError("name is missing")
93+
if hasattr(self, "handle"):
94+
raise ValueError("provider is already registered")
95+
96+
self._cb = core.BNFormatStringResolutionProviderCallbacks()
97+
self._cb.context = 0
98+
self._cb.isValid = self._cb.isValid.__class__(self._is_valid)
99+
self._cb.freeTypeList = self._cb.freeTypeList.__class__(self._free_type_list)
100+
self.handle = core.BNRegisterFormatStringResolutionProvider(self.__class__.name, self._cb)
101+
assert self.handle is not None, "core.BNRegisterFormatStringResolutionProvider returned None"
102+
self.__class__._registered_providers.append(self)
103+
104+
def _is_valid(self, ctxt, format_string, platform, types, count) -> bool:
105+
types[0] = None
106+
count[0] = 0
107+
try:
108+
platform_obj = None
109+
if platform:
110+
platform_obj = _platform.CorePlatform._from_cache(
111+
core.BNNewPlatformReference(platform))
112+
113+
result = self.perform_is_valid(core.pyNativeStr(format_string), platform_obj)
114+
if result is None:
115+
return False
116+
117+
resolved_types = list(result)
118+
for resolved_type in resolved_types:
119+
if not isinstance(resolved_type, _types.Type):
120+
raise TypeError("perform_is_valid must return Type objects")
121+
122+
count[0] = len(resolved_types)
123+
if not resolved_types:
124+
return True
125+
126+
output_buf = (core.BNTypeWithConfidence * len(resolved_types))()
127+
created_count = 0
128+
try:
129+
for i, resolved_type in enumerate(resolved_types):
130+
output_buf[i].type = core.BNNewTypeReference(resolved_type.handle)
131+
output_buf[i].confidence = resolved_type.confidence
132+
created_count += 1
133+
except Exception:
134+
for i in range(created_count):
135+
core.BNFreeType(output_buf[i].type)
136+
raise
137+
138+
output_ptr = ctypes.cast(output_buf, ctypes.POINTER(core.BNTypeWithConfidence))
139+
key = ctypes.cast(output_ptr, ctypes.c_void_p).value
140+
self._pending_type_lists[key] = (output_ptr, output_buf, len(resolved_types))
141+
types[0] = output_ptr
142+
return True
143+
except Exception:
144+
types[0] = None
145+
count[0] = 0
146+
log_error_for_exception("Unhandled Python exception in FormatStringResolutionProvider._is_valid")
147+
return False
148+
149+
def _free_type_list(self, ctxt, type_list, count):
150+
try:
151+
key = ctypes.cast(type_list, ctypes.c_void_p).value
152+
if key not in self._pending_type_lists:
153+
raise ValueError("freeing type list that wasn't allocated")
154+
_, output_buf, output_count = self._pending_type_lists.pop(key)
155+
for i in range(output_count):
156+
core.BNFreeType(output_buf[i].type)
157+
except Exception:
158+
log_error_for_exception("Unhandled Python exception in FormatStringResolutionProvider._free_type_list")
159+
160+
def perform_is_valid(
161+
self, format_string: str, platform: Optional['_platform.Platform']
162+
) -> Optional[List['_types.Type']]:
163+
"""
164+
Resolve the argument types described by ``format_string`` for ``platform``.
165+
166+
Return ``None`` when the format is invalid, an empty list for a valid format with no arguments,
167+
or a list of :py:class:`Type` objects for a valid format. Override this method in custom providers.
168+
"""
169+
raise NotImplementedError("Not implemented")
170+
171+
def is_valid(
172+
self, format_string: str, platform: Optional['_platform.Platform']
173+
) -> Optional[List['_types.Type']]:
174+
"""
175+
Resolve the argument types described by ``format_string`` for ``platform``.
176+
177+
The returned type objects carry the confidence supplied by the provider. ``None`` denotes an invalid
178+
format; an empty list denotes a valid format that consumes no arguments.
179+
"""
180+
if not isinstance(format_string, str):
181+
raise TypeError("format_string must be a string")
182+
if platform is not None and not isinstance(platform, _platform.Platform):
183+
raise TypeError("platform must be a Platform or None")
184+
if not hasattr(self, "handle"):
185+
raise ValueError("provider is not registered")
186+
187+
type_list = ctypes.POINTER(core.BNTypeWithConfidence)()
188+
count = ctypes.c_ulonglong()
189+
valid = core.BNFormatStringResolutionProviderIsValid(
190+
self.handle, format_string, platform.handle if platform is not None else None, type_list, count)
191+
if not valid:
192+
if type_list:
193+
core.BNFreeTypeWithConfidenceList(type_list, count.value)
194+
return None
195+
196+
try:
197+
return [
198+
_types.Type.create(
199+
core.BNNewTypeReference(type_list[i].type), platform=platform,
200+
confidence=type_list[i].confidence)
201+
for i in range(count.value)
202+
]
203+
finally:
204+
core.BNFreeTypeWithConfidenceList(type_list, count.value)

0 commit comments

Comments
 (0)