Skip to content

Commit f78bc87

Browse files
committed
Help detect missing plugin dependencies.
1 parent 2038242 commit f78bc87

3 files changed

Lines changed: 121 additions & 11 deletions

File tree

binaryninjacore.h

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3280,6 +3280,34 @@ extern "C"
32803280
ScriptExecutionCancelled
32813281
};
32823282

3283+
BN_ENUM(uint8_t, BNPluginDependencyDecision)
3284+
{
3285+
PluginDependencyReinstall,
3286+
PluginDependencyDisable,
3287+
PluginDependencyLoadAnyway
3288+
};
3289+
3290+
typedef struct BNPluginDependencyIssue
3291+
{
3292+
const char* repository;
3293+
const char* name;
3294+
const char* path;
3295+
const char* dependencies;
3296+
} BNPluginDependencyIssue;
3297+
3298+
typedef struct BNPluginDependencyStartupCallbacks
3299+
{
3300+
void* context;
3301+
bool (*checkDependencies)(void* ctxt, const BNPluginDependencyIssue* issues, size_t count,
3302+
BNPluginDependencyDecision* decisions);
3303+
} BNPluginDependencyStartupCallbacks;
3304+
3305+
typedef struct BNScriptingProviderModuleInstalledCallbacks
3306+
{
3307+
void* context;
3308+
bool (*moduleInstalled)(void* ctxt, const char* modules);
3309+
} BNScriptingProviderModuleInstalledCallbacks;
3310+
32833311

32843312
typedef struct BNScriptingInstanceCallbacks
32853313
{
@@ -8127,6 +8155,10 @@ extern "C"
81278155
BINARYNINJACOREAPI bool BNLoadScriptingProviderModule(
81288156
BNScriptingProvider* provider, const char* repository, const char* module, bool force);
81298157
BINARYNINJACOREAPI bool BNInstallScriptingProviderModules(BNScriptingProvider* provider, const char* modules);
8158+
BINARYNINJACOREAPI bool BNIsScriptingProviderModuleInstalled(BNScriptingProvider* provider, const char* modules);
8159+
BINARYNINJACOREAPI void BNSetScriptingProviderModuleInstalledCallback(BNScriptingProvider* provider,
8160+
BNScriptingProviderModuleInstalledCallbacks* callbacks);
8161+
BINARYNINJACOREAPI void BNSetPluginDependencyStartupCallback(BNPluginDependencyStartupCallbacks* callbacks);
81308162

81318163
BINARYNINJACOREAPI BNScriptingInstance* BNInitScriptingInstance(
81328164
BNScriptingProvider* provider, BNScriptingInstanceCallbacks* callbacks);

python/requirementcheck.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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+
22+
import json
23+
from typing import Dict, Iterable, List, Optional
24+
25+
26+
def pip_requirements_from_dependency_metadata(dependencies: bytes) -> List[str]:
27+
raw_text = dependencies.decode("utf-8")
28+
try:
29+
# Dependencies might be specified in JSON format, which we need to translate to text.
30+
dependencies_json = json.loads(raw_text)
31+
dependency_text = dependencies_json.get("pip", "")
32+
except json.JSONDecodeError:
33+
# If we can't parse input as JSON, it's probably already in text format.
34+
dependency_text = raw_text
35+
return [line.split('#', 1)[0].strip() for line in dependency_text.split('\n') if line.split('#', 1)[0].strip()]
36+
37+
38+
def pip_requirements_satisfied(requirements: Iterable[str], installed_versions: Optional[Dict[str, str]] = None) -> bool:
39+
try:
40+
from importlib import metadata as importlib_metadata
41+
from packaging.requirements import Requirement
42+
from packaging.utils import canonicalize_name
43+
except Exception:
44+
from importlib import metadata as importlib_metadata
45+
from pip._vendor.packaging.requirements import Requirement
46+
from pip._vendor.packaging.utils import canonicalize_name
47+
48+
if installed_versions is None:
49+
installed_versions = {}
50+
for dist in importlib_metadata.distributions():
51+
name = dist.metadata.get("Name")
52+
if name:
53+
installed_versions[canonicalize_name(name)] = dist.version
54+
else:
55+
installed_versions = {canonicalize_name(name): version for name, version in installed_versions.items()}
56+
57+
for requirement_text in requirements:
58+
requirement = Requirement(requirement_text)
59+
if requirement.marker is not None and not requirement.marker.evaluate():
60+
continue
61+
62+
version = installed_versions.get(canonicalize_name(requirement.name))
63+
if version is None:
64+
return False
65+
if requirement.specifier and not requirement.specifier.contains(version, prereleases=True):
66+
return False
67+
68+
return True

python/scriptingprovider.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
from . import function
5151
from . import log
5252
from .pluginmanager import RepositoryManager
53+
from .requirementcheck import pip_requirements_from_dependency_metadata, pip_requirements_satisfied
5354
from .enums import ScriptingProviderExecuteResult, ScriptingProviderInputReadyState
5455
from .settings import Settings
5556
from .enums import SettingsScope
@@ -476,8 +477,11 @@ def register(self) -> None:
476477
self._cb.createInstance = self._cb.createInstance.__class__(self._create_instance)
477478
self._cb.loadModule = self._cb.loadModule.__class__(self._load_module)
478479
self._cb.installModules = self._cb.installModules.__class__(self._install_modules)
479-
self._cb.moduleInstalled = self._cb.installModules.__class__(self._module_installed)
480480
self.handle = core.BNRegisterScriptingProvider(self.__class__.name, self.__class__.apiName, self._cb)
481+
self._module_installed_cb = core.BNScriptingProviderModuleInstalledCallbacks()
482+
self._module_installed_cb.context = None
483+
self._module_installed_cb.moduleInstalled = self._module_installed_cb.moduleInstalled.__class__(self._module_installed)
484+
core.BNSetScriptingProviderModuleInstalledCallback(self.handle, self._module_installed_cb)
481485
self.__class__._registered_providers.append(self)
482486

483487
def _create_instance(self, ctxt):
@@ -505,7 +509,7 @@ def _load_module(self, ctx, repo_path: bytes, plugin_path: bytes, force: bool) -
505509
def _install_modules(self, ctx, modules: bytes) -> bool:
506510
return False
507511

508-
def _module_installed(self, ctx, module: str) -> bool:
512+
def _module_installed(self, ctx, modules: bytes) -> bool:
509513
return False
510514

511515

@@ -1291,12 +1295,7 @@ def _get_python_environment(self, using_bundled_python: bool=False) -> Optional[
12911295

12921296
def _install_modules(self, ctx, _modules: bytes) -> bool:
12931297
# This callback should not be called directly
1294-
dependencies_json = json.loads(_modules.decode("utf-8"))
1295-
modules = ""
1296-
if "pip" in dependencies_json:
1297-
if len(dependencies_json["pip"].strip()) == 0:
1298-
return True
1299-
modules = [line.split('#', 1)[0].strip() for line in dependencies_json["pip"].split('\n') if line.split('#', 1)[0].strip()]
1298+
modules = pip_requirements_from_dependency_metadata(_modules)
13001299
if len(modules) == 0:
13011300
return True
13021301
python_lib = settings.Settings().get_string("python.interpreter")
@@ -1358,10 +1357,21 @@ def _install_modules(self, ctx, _modules: bytes) -> bool:
13581357
)
13591358
return status
13601359

1361-
def _module_installed(self, ctx, module: str) -> bool:
1362-
if self._python_bin is None:
1360+
def _module_installed(self, ctx, _modules: bytes) -> bool:
1361+
try:
1362+
modules = pip_requirements_from_dependency_metadata(_modules)
1363+
except Exception:
1364+
logger.log_error_for_exception("Failed to parse plugin dependency metadata")
1365+
return False
1366+
1367+
if len(modules) == 0:
1368+
return True
1369+
1370+
try:
1371+
return pip_requirements_satisfied(modules)
1372+
except Exception:
1373+
logger.log_error_for_exception("Failed to check plugin dependency requirements")
13631374
return False
1364-
return re.split('>|=|,', module.strip(), 1)[0] in self._satisfied_dependencies(self._python_bin)
13651375

13661376
@classmethod
13671377
def register_magic_variable(

0 commit comments

Comments
 (0)