1from typing import Type, Dict, Optional, List, TYPE_CHECKING
2
3from angr.errors import AngrNoPluginError
4
5import logging
6l = logging.getLogger(name=__name__)
7
8
9class PluginHub:
10    """
11    A plugin hub is an object which contains many plugins, as well as the notion of a "preset", or a
12    backer that can provide default implementations of plugins which cater to a certain
13    circumstance.
14
15    Objects in angr like the SimState, the Analyses hub, the SimEngine selector, etc all use this
16    model to unify their mechanisms for automatically collecting and selecting components to use. If
17    you're familiar with design patterns this is a configurable Strategy Pattern.
18
19    Each PluginHub subclass should have a corresponding Plugin subclass, and perhaps a PluginPreset
20    subclass if it wants its presets to be able to specify anything more interesting than a list of
21    defaults.
22    """
23
24    def __init__(self):
25        super(PluginHub, self).__init__()
26        self._active_plugins = {} # type: Dict[str, SimStatePlugin]
27        self._active_preset = None # type: Optional[PluginPreset]
28        self._provided_by_preset = [] # type: List[int]
29
30    #
31    #   Class methods for registration
32    #
33
34    _presets = None # not a dict so different subclasses don't share instances
35
36    @classmethod
37    def register_default(cls, name, plugin_cls, preset='default'):
38        if cls._presets is None or preset not in cls._presets:
39            l.error("Preset %s does not exist yet...", preset)
40            return
41        cls._presets[preset].add_default_plugin(name, plugin_cls)
42
43    @classmethod
44    def register_preset(cls, name, preset):
45        """
46        Register a preset instance with the class of the hub it corresponds to. This allows individual plugin objects to
47        automatically register themselves with a preset by using a classmethod of their own with only the name of the
48        preset to register with.
49        """
50        if cls._presets is None:
51            cls._presets = {}
52        cls._presets[name] = preset
53
54    #
55    #   Python magic methods
56    #
57
58    def __getstate__(self):
59        return self._active_plugins, self._active_preset, self._provided_by_preset
60
61    def __setstate__(self, s):
62        plugins, preset, provided = s
63        self._active_preset = preset
64        self._active_plugins = {}
65        self._provided_by_preset = provided
66
67        for name, plugin in plugins.items():
68            if name not in self._active_plugins:
69                self.register_plugin(name, plugin)
70
71    def __getattr__(self, name: str) -> 'SimStatePlugin':
72        try:
73            return self.get_plugin(name)
74        except AngrNoPluginError:
75            raise AttributeError(name)
76
77    def __dir__(self):
78        out = set(self.__dict__)
79        out.update(self._active_plugins)
80        if self.has_plugin_preset:
81            out.update(self._active_preset.list_default_plugins())
82
83        q = [type(self)]
84        while q:
85            cls = q.pop(0)
86            out.update(cls.__dict__)
87            for base in cls.__bases__:
88                if base is not object:
89                    q.append(base)
90
91        return sorted(out)
92
93    #
94    #   Methods for managing the current plugin preset
95    #
96
97    @property
98    def plugin_preset(self):
99        """
100        Get the current active plugin preset
101        """
102        return self._active_preset
103
104    @property
105    def has_plugin_preset(self) -> bool:
106        """
107        Check whether or not there is a plugin preset in use on this hub right now
108        """
109        return self._active_preset is not None
110
111    def use_plugin_preset(self, preset):
112        """
113        Apply a preset to the hub. If there was a previously active preset, discard it.
114
115        Preset can be either the string name of a preset or a PluginPreset instance.
116        """
117        if isinstance(preset, str):
118            try:
119                preset = self._presets[preset]
120            except (AttributeError, KeyError):
121                raise AngrNoPluginError("There is no preset named %s" % preset)
122
123        elif not isinstance(preset, PluginPreset):
124            raise ValueError("Argument must be an instance of PluginPreset: %s" % preset)
125
126        if self._active_preset:
127            l.warning("Overriding active preset %s with %s", self._active_preset, preset)
128            self.discard_plugin_preset()
129
130        preset.activate(self)
131        self._active_preset = preset
132
133    def discard_plugin_preset(self):
134        """
135        Discard the current active preset. Will release any active plugins that could have come from the old preset.
136        """
137        if self.has_plugin_preset:
138            for name, plugin in list(self._active_plugins.items()):
139                if id(plugin) in self._provided_by_preset:
140                    self.release_plugin(name)
141            self._active_preset.deactivate(self)
142        self._active_preset = None
143
144    #
145    #   Methods for managing the current active plugins
146    #
147
148    def get_plugin(self, name: str) -> 'SimStatePlugin':
149        """
150        Get the plugin named ``name``. If no such plugin is currently active, try to activate a new
151        one using the current preset.
152        """
153        if name in self._active_plugins:
154            return self._active_plugins[name]
155
156        elif self.has_plugin_preset:
157            plugin_cls = self._active_preset.request_plugin(name)
158            plugin = self._init_plugin(plugin_cls)
159
160            # Remember that this plugin was provided by preset.
161            self._provided_by_preset.append(id(plugin))
162
163            self.register_plugin(name, plugin)
164            return plugin
165
166        else:
167            raise AngrNoPluginError("No such plugin: %s" % name)
168
169    def _init_plugin(self, plugin_cls: Type['SimStatePlugin']) -> 'SimStatePlugin':  # pylint: disable=no-self-use
170        """
171        Perform any initialization actions on plugin before it is added to the list of active plugins.
172
173        :param plugin_cls:
174        """
175        return plugin_cls()
176
177    def has_plugin(self, name):
178        """
179        Return whether or not a plugin with the name ``name`` is curently active.
180        """
181        return name in self._active_plugins
182
183    def register_plugin(self, name: str, plugin):
184        """
185        Add a new plugin ``plugin`` with name ``name`` to the active plugins.
186        """
187        if self.has_plugin(name):
188            self.release_plugin(name)
189        self._active_plugins[name] = plugin
190        setattr(self, name, plugin)
191        return plugin
192
193    def release_plugin(self, name):
194        """
195        Deactivate and remove the plugin with name ``name``.
196        """
197        plugin = self._active_plugins[name]
198        if id(plugin) in self._provided_by_preset:
199            self._provided_by_preset.remove(id(plugin))
200
201        del self._active_plugins[name]
202        delattr(self, name)
203
204
205class PluginPreset:
206    """
207    A plugin preset object contains a mapping from name to a plugin class.
208    A preset can be active on a hub, which will cause it to handle requests for plugins which are not already present on the hub.
209
210    Unlike Plugins and PluginHubs, instances of PluginPresets are defined on the module level for individual presets.
211    You should register the preset instance with a hub to allow plugins to easily add themselves to the preset without an explicit reference to the preset itself.
212    """
213
214    def __init__(self):
215        self._default_plugins = {} # type: Dict[str, Type['SimStatePlugin']]
216
217    def activate(self, hub):  # pylint:disable=no-self-use,unused-argument
218        """
219        This method is called when the preset becomes active on a hub.
220        """
221        return
222
223    def deactivate(self, hub):  # pylint:disable=no-self-use,unused-argument
224        """
225        This method is called when the preset is discarded from the hub.
226        """
227        return
228
229    def add_default_plugin(self, name, plugin_cls):
230        """
231        Add a plugin to the preset.
232        """
233        self._default_plugins[name] = plugin_cls
234
235    def list_default_plugins(self):
236        """
237        Return a list of the names of available default plugins.
238        """
239        return self._default_plugins.keys()
240
241    def request_plugin(self, name: str) -> Type['SimStatePlugin']:
242        """
243        Return the plugin class which is registered under the name ``name``, or raise NoPlugin if
244        the name isn't available.
245        """
246        try:
247            return self._default_plugins[name]
248        except KeyError:
249            raise AngrNoPluginError("There is no plugin named %s" % name)
250
251    def copy(self):
252        """
253        Return a copy of self.
254        """
255        cls = self.__class__
256        result = cls.__new__(cls)
257        result._default_plugins = dict(self._default_plugins)  # pylint:disable=protected-access
258        return result
259
260
261class PluginVendor(PluginHub):
262    """
263    A specialized hub which serves only as a plugin vendor, never having any "active" plugins.
264    It will directly return the plugins provided by the preset instead of instanciating them.
265    """
266
267    def release_plugin(self, name):
268        pass
269
270    def register_plugin(self, name, plugin):
271        pass
272
273    def __dir__(self):
274        x = super(PluginVendor, self).__dir__()
275        x.remove('release_plugin')
276        x.remove('register_plugin')
277        x.remove('has_plugin')
278        return x
279
280
281class VendorPreset(PluginPreset):
282    """
283    A specialized preset class for use with the PluginVendor.
284    """
285    ...
286
287if TYPE_CHECKING:
288    from ..state_plugins import SimStatePlugin
289