Skip to content

Commit

Permalink
Impove metadata specification
Browse files Browse the repository at this point in the history
  • Loading branch information
twizmwazin committed Jul 13, 2023
1 parent 26967b9 commit 0ccbb11
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 86 deletions.
136 changes: 51 additions & 85 deletions angrmanagement/plugins/plugin_description.py
Original file line number Diff line number Diff line change
@@ -1,101 +1,67 @@
from typing import List
from dataclasses import dataclass, field
import pathlib
from typing import List, Optional, Dict

import marshmallow.validate
import marshmallow_dataclass
import tomlkit
from tomlkit.items import AoT, Integer, String, Table
from marshmallow_dataclass import dataclass as dataclass_with_schema


class PluginDescription:
"""
Describes an angr management plugin. Can be generated from plugin.toml.
"""

def __init__(self):
# Metadata
self.plugin_metadata_version: int = None
@dataclass_with_schema
class MetadataDescription:
version: int = field(metadata={"validate": marshmallow.validate.OneOf([0])})

# Plugin
self.name: str = ""
self.shortname: str = ""
self.version: str = ""
self.description: str = ""
self.long_description: str = ""
self.platforms: List[str] = []
self.min_angr_vesion: str = ""
self.author = ""
self.entrypoints: List[str] = []
self.require_workspace: bool = True
self.has_url_actions: bool = False

# file path
self.plugin_file_path: str = ""

@classmethod
def load_single_plugin(cls, data: Table) -> "PluginDescription":
desc = PluginDescription()
@dataclass_with_schema
class PackageDescription:
"""
Describes a plugin package.
"""
name: str = field()
version: str = field()
platforms: List[str] = field(default_factory=lambda: ["any"])
site_packages: Optional[str] = field(default=None)
authors: List[str] = field(default_factory=list)
description: str = field(default="")
long_description: str = field(default="")

desc.name = data.get("name", None)
if not isinstance(desc.name, String):
raise TypeError(f'"name" must be a String instance, not a {type(desc.name)}')
if not desc.name:
raise TypeError('"name" cannot be empty')

desc.shortname = data.get("shortname", None)
if not isinstance(desc.shortname, String):
raise TypeError(f'"shortname" must be a String instance, not a {type(desc.shortname)}')
if not desc.shortname:
raise TypeError('"shortname" cannot be empty')
@dataclass_with_schema
class PluginDescription:
"""
Describes an angr management plugin. Can be generated from plugin.toml.
"""
name: str = field()
entrypoint: str = field()
platforms: Optional[List[str]] = field(default=None)
description: str = field(default="")
requires_workspace: bool = field(default=False)

desc.entrypoints = data.get("entrypoints", "")
if not isinstance(desc.entrypoints, List) or not all(
isinstance(entrypoint, String) for entrypoint in desc.entrypoints
):
raise TypeError('"entrypoints" must be a List of String instances')
if not desc.entrypoints:
raise TypeError('"entrypoints" cannot be empty')

# optional
desc.version = data.get("version", "")
desc.description = data.get("description", "")
desc.long_description = data.get("long_description", "")
desc.platforms = data.get("platforms", "")
desc.min_angr_vesion = data.get("min_angr_version", "")
desc.author = data.get("author", "")
desc.require_workspace = data.get("require_workspace", True)
desc.has_url_actions = data.get("has_url_actions", False)
@dataclass
class PluginConfigFileDescription:
"""
Describes a plugin config file.
"""
metadata: MetadataDescription = field()
package: PackageDescription = field()
plugins: Dict[str, PluginDescription] = field(default_factory=dict)

return desc

@classmethod
def from_toml(cls, file_path: str) -> List["PluginDescription"]:
with open(file_path, encoding="utf-8") as f:
data = tomlkit.load(f)
PluginConfigSchema = marshmallow_dataclass.class_schema(PluginConfigFileDescription)()

# load metadata
outer_desc = PluginDescription()
if "meta" in data and "plugin_metadata_version" in data["meta"]:
if isinstance(data["meta"]["plugin_metadata_version"], Integer):
outer_desc.plugin_metadata_version = data["meta"]["plugin_metadata_version"].unwrap()

if outer_desc.plugin_metadata_version is None:
raise TypeError("Cannot find plugin_metadata_version")
if outer_desc.plugin_metadata_version != 0:
raise TypeError(f"Unsupported plugin metadata version {outer_desc.plugin_metadata_version}")
def from_toml_string(toml_string: str) -> PluginConfigFileDescription:
"""
Load a plugin config file from a TOML string.
"""
return PluginConfigSchema.load(tomlkit.parse(toml_string))

descs = []
# load plugin information
if "plugins" in data and isinstance(data["plugins"], AoT):
# multiple plugins to load!
for plugin in data["plugins"]:
desc = PluginDescription.load_single_plugin(plugin)
desc.plugin_metadata_version = outer_desc.plugin_metadata_version
desc.plugin_file_path = file_path
descs.append(desc)
elif "plugin" in data:
desc = PluginDescription.load_single_plugin(data["plugin"])
desc.plugin_metadata_version = outer_desc.plugin_metadata_version
desc.plugin_file_path = file_path
descs.append(desc)
else:
raise TypeError('Cannot find any "plugin" or "plugins" table.')

return descs
def from_toml_file(toml_file: pathlib.Path) -> PluginConfigFileDescription:
"""
Load a plugin config file from a TOML file.
"""
with open(toml_file, "r") as f:
return from_toml_string(f.read())
2 changes: 1 addition & 1 deletion angrmanagement/ui/widgets/qdisasm_statusbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from PySide6.QtWidgets import QComboBox, QFileDialog, QFrame, QHBoxLayout, QLabel, QPushButton

from angrmanagement.ui.menus.disasm_options_menu import DisasmOptionsMenu
from angrmanagement.ui.toolbars import NavToolbar
from angrmanagement.ui.toolbars.nav_toolbar import NavToolbar

from .qdisasm_base_control import DisassemblyLevel
from .qdisasm_graph import QDisassemblyGraph
Expand Down
1 change: 1 addition & 0 deletions docs/TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This directory should be adapted into a sphinx project to be hosted on readthedocs with other angr documentation.
42 changes: 42 additions & 0 deletions docs/plugin_metadata_spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
Plugin Metadata Specification
=============================

Each plugin directory must have a plugin.toml file. A plugin metadata file has
two types of sections: `meta` and `plugin`. Each plugin.toml must contain one
meta section, but may contain
Below are tables of keys for each
section.

## Meta section
| Key | Type | Required | Notes |
| ------- | ------- | -------- | ------------------- |
| version | integer | yes | Currently version 0 |


## Package section
| Key | Type | Required | Default | Notes |
| ---------------- | ---------------- | -------- | ------- | --------- |
| name | string | yes | | |
| version | string | yes | | |
| platforms | list[string] | no | ["any"] | See below |
| site_packages | Optional[string] | no | None | |
| authors | list[string] | no | [] | |
| description | string | no | "" | |
| long-description | string | no | "" | |


### Values for `platforms`
angr management treats `"any"` as matching all platfoms. Otherwise, angr
management checks if Python's `sys.platform` starts with any of the listed
strings. See https://docs.python.org/3/library/sys.html#sys.platform to learn
more about the `sys.platform` value in Python.


## Plugin section
| Key | Type | Required | Default | Notes |
| ------------------ | ---------------------- | -------- | --------------------- | ------------------------------------------ |
| name | string | yes | | |
| entrypoint | string | yes | | Use file.py::ClassName syntax, like pytest |
| platforms | Optional[list[string]] | no | package.site-packages | overrides package default if configured |
| description | string | no | "" | |
| requires_workspace | bool | no | false | |
42 changes: 42 additions & 0 deletions docs/plugin_spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Plugin specification

angr management supports loading plugins that can extend and adapt the
functionality of angr management itself. In order to create an angr management
plugin, all that is needed is a directory containing two files: a `plugin.toml`
and a Python file where that plugin is implemented. For example, this is valid
plugin package directory layout:

```
example-plugin/
example_plugin.py
plugin.toml
```

Inside `example_plugin.py`, a super minimal plugin example looks like:

```py
import angrmanagement.plugins.BasePlugin

class ExamplePlugin(BasePlugin):
pass
```

A valid `plugin.toml` that would allow this plugin to be loaded would look like
this:

```toml
[metadata]
version = 0

[package]
name = "example-plugin"
version = "1.0"

[plugin.example]
name = "Example Plugin"
version = "1.0"
entrypoints = ["example_plugin.py::ExamplePlugin"]
```

For more information what fields are available in a plugin.toml, se the
[plugin metadata specification](./plugin_metadata_spec.md).
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ install_requires =
tomlkit
pyobjc-framework-Cocoa;platform_system == "Darwin"
thefuzz[speedup]
marshmallow~=3.19
marshmallow-dataclass~=8.5
python_requires = >=3.8
include_package_data = True

Expand Down
87 changes: 87 additions & 0 deletions tests/test_plugin_desciption_loading.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import unittest

import tomlkit

from angrmanagement.plugins.plugin_description import (
PackageDescription,
PluginDescription,
from_toml_string,
MetadataDescription,
MetadataDescription,
MetadataDescription,
)


class TestPluginDescriptionLoading(unittest.TestCase):
def test_metadta_section(self):
test_data = "version = 0"
MetadataDescription.Schema().load(tomlkit.parse(test_data))

def test_metadata_section_invalid_version(self):
test_data = "version = 1_000_000"
with self.assertRaises(Exception):
MetadataDescription.Schema().load(tomlkit.parse(test_data))

def test_minimal_package(self):
test_data = """
name = "example"
version = "1.0"
"""
PackageDescription.Schema().load(tomlkit.parse(test_data))

def test_minimal_plugin(self):
test_data = """
name = "Example"
entrypoint = "example.py::ExamplePlugin"
"""
PluginDescription.Schema().load(tomlkit.parse(test_data))

def test_minimal(self):
test_data = """
[metadata]
version = 0
[package]
name = "example"
version = "1.0"
[plugin.example]
name = "Example"
version = "1.0"
entrypoints = ["example.py::ExamplePlugin"]
"""
from_toml_string(test_data)

def test_multiple(self):
test_data = """
[metadata]
version = 0
[package]
name = "example"
version = "1.0"
platforms = ["any"]
site_pacakges = "site-packages"
authors = ["Example"]
description = "An example plugin package"
long-description = "An example plugin package for testing angr management"
[plugin.example1]
name = "Example 1"
entrypoints = ["example.py::ExamplePlugin1"]
platforms = ["linux"]
description = "An example plugin for testing angr management on linuz"
requires-workspace = false
[plugin.example2]
name = "Example 2"
entrypoints = ["example.py::ExamplePlugin2"]
platforms = ["win32", "cygwin"]
description = "An example plugin for testing angr management on windows"
requires-workspace = true
"""
from_toml_string(test_data)


if __name__ == "__main__":
unittest.main()

0 comments on commit 0ccbb11

Please sign in to comment.