├── tests ├── __init__.py ├── data │ ├── box-types.txt │ ├── empty-types.txt │ ├── structs.txt │ ├── variants.txt │ └── misc-types.txt └── test_parse.py ├── binja_plugin ├── __init__.py ├── plugin.py ├── actions.py └── type_import.py ├── requirements.txt ├── dev-requirements.txt ├── images └── std-sys-windows-types-border.png ├── __init__.py ├── .github └── workflows │ ├── release-please.yml │ └── ci.yml ├── pyproject.toml ├── noxfile.py ├── LICENSE ├── CHANGELOG.md ├── plugin.json ├── .gitignore ├── parse.py └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /binja_plugin/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyparsing>=3.0.0 2 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | black==23.3.0 2 | mypy==1.2.0 3 | ruff==0.0.261 4 | pytest==7.3.1 -------------------------------------------------------------------------------- /images/std-sys-windows-types-border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cxiao/rust_type_layout_helper_bn/HEAD/images/std-sys-windows-types-border.png -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | import importlib 3 | 4 | importlib.import_module("binaryninja") 5 | from .binja_plugin.plugin import plugin_init 6 | 7 | plugin_init() 8 | except ImportError: 9 | pass 10 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | name: release-please 6 | jobs: 7 | release-please: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: google-github-actions/release-please-action@v3 11 | with: 12 | release-type: simple 13 | package-name: release-please-action -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | ignore = [ 3 | # Line length violations 4 | "E501", 5 | # f-string without placeholders 6 | "F541" 7 | ] 8 | 9 | select = [ 10 | # Pyflakes 11 | "F", 12 | # Pycodestyle 13 | "E", 14 | # isort 15 | "I001" 16 | ] 17 | 18 | [tool.mypy] 19 | ignore_missing_imports = true 20 | check_untyped_defs = true -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | name: ci 6 | jobs: 7 | lint: 8 | name: lint 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: wntrblm/nox@2022.11.21 13 | with: 14 | python-versions: "3.7, 3.8, 3.9, 3.10, 3.11" 15 | - run: nox -s lint 16 | test: 17 | name: test 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | - uses: wntrblm/nox@2022.11.21 22 | with: 23 | python-versions: "3.7, 3.8, 3.9, 3.10, 3.11" 24 | - run: nox -s test -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | import nox 2 | 3 | 4 | @nox.session 5 | def format(session): 6 | session.install("-r", "dev-requirements.txt") 7 | session.run("black", ".") 8 | 9 | 10 | @nox.session(python=["3.7", "3.8", "3.9", "3.10", "3.11"]) 11 | def lint(session): 12 | session.install("-r", "dev-requirements.txt") 13 | session.install("-r", "requirements.txt") 14 | 15 | session.run("ruff", ".") 16 | session.run("mypy", ".") 17 | 18 | 19 | @nox.session(python=["3.7", "3.8", "3.9", "3.10", "3.11"]) 20 | def test(session): 21 | session.install("-r", "dev-requirements.txt") 22 | session.install("-r", "requirements.txt") 23 | 24 | session.run("pytest", "-vv", ".") 25 | -------------------------------------------------------------------------------- /tests/data/box-types.txt: -------------------------------------------------------------------------------- 1 | print-type-size type: `std::boxed::Box std::ops::Fn(&'a std::panic::PanicInfo<'b>) + std::marker::Send + std::marker::Sync>`: 16 bytes, alignment: 8 bytes 2 | print-type-size field `.1`: 0 bytes 3 | print-type-size field `.0`: 16 bytes 4 | print-type-size type: `std::boxed::Box`: 16 bytes, alignment: 8 bytes 5 | print-type-size field `.1`: 0 bytes 6 | print-type-size field `.0`: 16 bytes 7 | print-type-size type: `std::boxed::Box`: 16 bytes, alignment: 8 bytes 8 | print-type-size field `.1`: 0 bytes 9 | print-type-size field `.0`: 16 bytes -------------------------------------------------------------------------------- /binja_plugin/plugin.py: -------------------------------------------------------------------------------- 1 | from binaryninja.log import Logger 2 | from binaryninja.plugin import PluginCommand 3 | 4 | from . import actions 5 | 6 | logger = Logger(session_id=0, logger_name=__name__) 7 | 8 | PLUGIN_NAME = "Rust Type Layout Helper" 9 | 10 | plugin_commands = [ 11 | ( 12 | f"{PLUGIN_NAME}\\Load File...", 13 | "Load a new type layout file", 14 | actions.action_load_type_layout_file, 15 | ) 16 | ] 17 | 18 | 19 | def plugin_init(): 20 | for command_name, command_description, command_action in plugin_commands: 21 | PluginCommand.register( 22 | name=command_name, description=command_description, action=command_action 23 | ) 24 | -------------------------------------------------------------------------------- /tests/data/empty-types.txt: -------------------------------------------------------------------------------- 1 | print-type-size type: `std::marker::PhantomData<&std::sys::windows::args::Arg>`: 0 bytes, alignment: 1 bytes 2 | print-type-size type: `std::marker::PhantomData<&u8>`: 0 bytes, alignment: 1 bytes 3 | print-type-size type: `std::marker::PhantomData<(alloc::collections::btree::node::marker::Immut<'_>, alloc::collections::btree::node::marker::Leaf)>`: 0 bytes, alignment: 1 bytes 4 | print-type-size type: `std::marker::PhantomData<(alloc::collections::btree::node::marker::Immut<'_>, alloc::collections::btree::node::marker::LeafOrInternal)>`: 0 bytes, alignment: 1 bytes 5 | print-type-size type: `std::marker::PhantomData<(alloc::collections::btree::node::marker::Owned, alloc::collections::btree::node::marker::LeafOrInternal)>`: 0 bytes, alignment: 1 bytes 6 | print-type-size type: `std::marker::PhantomData<*mut ()>`: 0 bytes, alignment: 1 bytes 7 | print-type-size type: `std::marker::PhantomData<[std::mem::MaybeUninit]>`: 0 bytes, alignment: 1 bytes 8 | print-type-size type: `std::marker::PhantomData<[u8]>`: 0 bytes, alignment: 1 bytes -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Cindy Xiao 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /binja_plugin/actions.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from pprint import pformat 3 | 4 | from binaryninja.binaryview import BinaryView 5 | from binaryninja.interaction import get_open_filename_input 6 | from binaryninja.log import Logger 7 | from pyparsing import ParseException 8 | 9 | from ..parse import parse 10 | from .type_import import create_binary_view_types 11 | 12 | logger = Logger(session_id=0, logger_name=__name__) 13 | 14 | 15 | def action_load_type_layout_file(bv: BinaryView): 16 | type_layout_file_path = get_open_filename_input( 17 | prompt="Open a Rust type layout information file" 18 | ) 19 | logger.log_info(f"{type_layout_file_path}") 20 | 21 | if type_layout_file_path is not None: 22 | type_layout_file_path = Path(type_layout_file_path) 23 | with open(type_layout_file_path) as type_layout_file: 24 | try: 25 | parsed_rust_types = parse(type_layout_file) 26 | logger.log_info(f"{pformat(parsed_rust_types)}") 27 | create_binary_view_types(bv=bv, rust_types=parsed_rust_types) 28 | except ParseException as err: 29 | logger.log_error( 30 | f"Failed to parse the provided Rust type layout file {type_layout_file_path}: {err.explain()}" 31 | ) 32 | -------------------------------------------------------------------------------- /tests/data/structs.txt: -------------------------------------------------------------------------------- 1 | print-type-size type: `std::sys::windows::c::WIN32_FIND_DATAW`: 592 bytes, alignment: 4 bytes 2 | print-type-size field `.dwFileAttributes`: 4 bytes 3 | print-type-size field `.ftCreationTime`: 8 bytes 4 | print-type-size field `.ftLastAccessTime`: 8 bytes 5 | print-type-size field `.ftLastWriteTime`: 8 bytes 6 | print-type-size field `.nFileSizeHigh`: 4 bytes 7 | print-type-size field `.nFileSizeLow`: 4 bytes 8 | print-type-size field `.dwReserved0`: 4 bytes 9 | print-type-size field `.dwReserved1`: 4 bytes 10 | print-type-size field `.cFileName`: 520 bytes 11 | print-type-size field `.cAlternateFileName`: 28 bytes 12 | print-type-size type: `std::sys::windows::c::fd_set`: 520 bytes, alignment: 8 bytes 13 | print-type-size field `.fd_count`: 4 bytes 14 | print-type-size padding: 4 bytes 15 | print-type-size field `.fd_array`: 512 bytes, alignment: 8 bytes 16 | print-type-size type: `std::backtrace::BacktraceFrame`: 312 bytes, alignment: 8 bytes 17 | print-type-size field `.frame`: 288 bytes 18 | print-type-size field `.symbols`: 24 bytes 19 | print-type-size type: `std::io::BufWriter`: 32 bytes, alignment: 8 bytes 20 | print-type-size field `.buf`: 24 bytes 21 | print-type-size field `.inner`: 5 bytes 22 | print-type-size field `.panicked`: 1 bytes 23 | print-type-size end padding: 2 bytes -------------------------------------------------------------------------------- /tests/data/variants.txt: -------------------------------------------------------------------------------- 1 | print-type-size type: `std::sys_common::net::SocketAddrCRepr`: 28 bytes, alignment: 4 bytes 2 | print-type-size variant `SocketAddrCRepr`: 28 bytes 3 | print-type-size field `.v4`: 16 bytes 4 | print-type-size field `.v6`: 28 bytes, offset: 0 bytes, alignment: 4 bytes 5 | print-type-size type: `Number`: 24 bytes, alignment: 8 bytes 6 | print-type-size discriminant: 8 bytes 7 | print-type-size variant `Complex`: 16 bytes 8 | print-type-size field `.real`: 8 bytes 9 | print-type-size field `.imaginary`: 8 bytes 10 | print-type-size variant `Integer`: 8 bytes 11 | print-type-size field `.0`: 8 bytes 12 | print-type-size variant `Float`: 8 bytes 13 | print-type-size field `.0`: 8 bytes 14 | print-type-size type: `core::num::fmt::Part<'_>`: 24 bytes, alignment: 8 bytes 15 | print-type-size discriminant: 2 bytes 16 | print-type-size variant `Copy`: 22 bytes 17 | print-type-size padding: 6 bytes 18 | print-type-size field `.0`: 16 bytes, alignment: 8 bytes 19 | print-type-size variant `Zero`: 14 bytes 20 | print-type-size padding: 6 bytes 21 | print-type-size field `.0`: 8 bytes, alignment: 8 bytes 22 | print-type-size variant `Num`: 2 bytes 23 | print-type-size field `.0`: 2 bytes 24 | print-type-size type: `std::str::pattern::SearchStep`: 24 bytes, alignment: 8 bytes 25 | print-type-size discriminant: 8 bytes 26 | print-type-size variant `Match`: 16 bytes 27 | print-type-size field `.0`: 8 bytes 28 | print-type-size field `.1`: 8 bytes 29 | print-type-size variant `Reject`: 16 bytes 30 | print-type-size field `.0`: 8 bytes 31 | print-type-size field `.1`: 8 bytes 32 | print-type-size variant `Done`: 0 bytes -------------------------------------------------------------------------------- /tests/data/misc-types.txt: -------------------------------------------------------------------------------- 1 | print-type-size type: `std::char::EscapeDefault`: 16 bytes, alignment: 8 bytes 2 | print-type-size field `.state`: 16 bytes 3 | print-type-size type: `std::char::EscapeDefaultState`: 16 bytes, alignment: 8 bytes 4 | print-type-size variant `Unicode`: 16 bytes 5 | print-type-size field `.0`: 16 bytes 6 | print-type-size variant `Char`: 4 bytes 7 | print-type-size field `.0`: 4 bytes 8 | print-type-size variant `Backslash`: 4 bytes 9 | print-type-size field `.0`: 4 bytes 10 | print-type-size variant `Done`: 0 bytes 11 | print-type-size type: `std::char::EscapeUnicode`: 16 bytes, alignment: 8 bytes 12 | print-type-size field `.hex_digit_idx`: 8 bytes 13 | print-type-size field `.c`: 4 bytes 14 | print-type-size field `.state`: 1 bytes 15 | print-type-size end padding: 3 bytes 16 | print-type-size type: `std::collections::Bound`: 16 bytes, alignment: 8 bytes 17 | print-type-size discriminant: 8 bytes 18 | print-type-size variant `Included`: 8 bytes 19 | print-type-size field `.0`: 8 bytes 20 | print-type-size variant `Excluded`: 8 bytes 21 | print-type-size field `.0`: 8 bytes 22 | print-type-size variant `Unbounded`: 0 bytes 23 | print-type-size type: `std::collections::TryReserveError`: 16 bytes, alignment: 8 bytes 24 | print-type-size field `.kind`: 16 bytes 25 | print-type-size type: `std::collections::TryReserveErrorKind`: 16 bytes, alignment: 8 bytes 26 | print-type-size variant `AllocError`: 16 bytes 27 | print-type-size field `.non_exhaustive`: 0 bytes 28 | print-type-size field `.layout`: 16 bytes 29 | print-type-size variant `CapacityOverflow`: 0 bytes 30 | print-type-size type: `std::collections::hash_map::RandomState`: 16 bytes, alignment: 8 bytes 31 | print-type-size field `.k0`: 8 bytes 32 | print-type-size field `.k1`: 8 bytes 33 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.0 (2023-04-27) 4 | 5 | 6 | ### Features 7 | 8 | * Add basic parsing of print-type-size output ([ed88c48](https://github.com/cxiao/rust_type_layout_helper_bn/commit/ed88c48bd4d19022ad416e475091ee7d03721472)) 9 | * Add basic struct and field parsing ([84494f6](https://github.com/cxiao/rust_type_layout_helper_bn/commit/84494f6bd2d2a4ea72dfee84ea79904b41432bc4)) 10 | * Add Binary Ninja plugin import stub in module __init__.py ([e15e5df](https://github.com/cxiao/rust_type_layout_helper_bn/commit/e15e5dfa84028b5616cefc423317efd0a3ddc5c5)) 11 | * Add discriminant entries to corresponding created enum type ([768b848](https://github.com/cxiao/rust_type_layout_helper_bn/commit/768b8488182c0434dc708b84738526e3779e0397)) 12 | * Add plugin action to add types from Rust type layout file ([75b99ea](https://github.com/cxiao/rust_type_layout_helper_bn/commit/75b99ea8bc088d899011d4d4a747495288b440ba)) 13 | * Add support for offset in fields ([2d440ed](https://github.com/cxiao/rust_type_layout_helper_bn/commit/2d440ed4191f4c29c30b0a1ea94a5c39f4a46904)) 14 | * Expose parsing interface ([2cc1469](https://github.com/cxiao/rust_type_layout_helper_bn/commit/2cc1469491687e8308cc6039974ddcdc2537a383)) 15 | * Make parse.py into a utility for printing parsed data ([12c6e56](https://github.com/cxiao/rust_type_layout_helper_bn/commit/12c6e563798a3c6e5ebc77f20a7f867210432e67)) 16 | * Parse into predefined dataclass types ([f006c92](https://github.com/cxiao/rust_type_layout_helper_bn/commit/f006c92049eebd26ff247bc0b8dc196b3381a446)) 17 | * Parse lists of multiple types ([da76be8](https://github.com/cxiao/rust_type_layout_helper_bn/commit/da76be848ec82af44be6e92dc35a1025e219705a)) 18 | * Parse variants ([adb2b0b](https://github.com/cxiao/rust_type_layout_helper_bn/commit/adb2b0b48271c4f420d53849c4445430079f9fe6)) 19 | * Remove redundant usage of `pyparsing.Group` ([7a1aa70](https://github.com/cxiao/rust_type_layout_helper_bn/commit/7a1aa706a8188ba8dfaea8e24317112f92d4b275)) 20 | * Support creating types containing Variant, Discriminant ([c405c33](https://github.com/cxiao/rust_type_layout_helper_bn/commit/c405c332202b7470f99b91a18ffdb625a67cb8a5)) 21 | 22 | 23 | ### Bug Fixes 24 | 25 | * Make real integers when parsing variants ([cb7223c](https://github.com/cxiao/rust_type_layout_helper_bn/commit/cb7223c0a573bad5490dba811c9e7ffd7e96ad1a)) 26 | * Only consider parsing a success if all contents of file are parsed ([94329c4](https://github.com/cxiao/rust_type_layout_helper_bn/commit/94329c4dcce0583591a43b509d9273e67c172e40)) 27 | -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginmetadataversion": 2, 3 | "name": "Rust Type Layout Helper", 4 | "type": [ 5 | "helper" 6 | ], 7 | "api": [ 8 | "python3" 9 | ], 10 | "description": "An extremely experimental Binary Ninja importer for the type layout information emitted by the -Zprint-type-sizes flag of the Rust compiler.", 11 | "longdescription": "", 12 | "license": { 13 | "name": "MIT", 14 | "text": "Copyright (c) 2023 Cindy Xiao\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." 15 | }, 16 | "platforms": [ 17 | "Darwin", 18 | "Linux", 19 | "Windows" 20 | ], 21 | "installinstructions": { 22 | "Darwin": "Installation via the Binary Ninja plugin manager is recommended. For alternative installation instructions, see the [Installation section in the README on the GitHub repository for this plugin](https://github.com/cxiao/rust_type_layout_helper_bn#installation).", 23 | "Linux": "Installation via the Binary Ninja plugin manager is recommended. For alternative installation instructions, see the [Installation section in the README on the GitHub repository for this plugin](https://github.com/cxiao/rust_type_layout_helper_bn#installation).", 24 | "Windows": "Installation via the Binary Ninja plugin manager is recommended. For alternative installation instructions, see the [Installation section in the README on the GitHub repository for this plugin](https://github.com/cxiao/rust_type_layout_helper_bn#installation)." 25 | }, 26 | "dependencies": {}, 27 | "version": "1.0.0", 28 | "author": "Cindy Xiao", 29 | "minimumbinaryninjaversion": 3814 30 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | # ruff 163 | .ruff_cache/ -------------------------------------------------------------------------------- /binja_plugin/type_import.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from binaryninja.binaryview import BinaryView 4 | from binaryninja.log import Logger 5 | from binaryninja.types import ( 6 | ArrayType, 7 | EnumerationBuilder, 8 | IntegerType, 9 | QualifiedName, 10 | StructureBuilder, 11 | StructureType, 12 | StructureVariant, 13 | Type, 14 | VoidType, 15 | ) 16 | 17 | from ..parse import Discriminant, Field, Padding, Variant 18 | from ..parse import Type as RustType 19 | 20 | logger = Logger(session_id=0, logger_name=__name__) 21 | 22 | 23 | def _create_bn_type_from_field_size(field_size: int) -> Type: 24 | if field_size == 0: 25 | return VoidType.create() 26 | elif field_size in (1, 2, 4, 8, 16): 27 | return IntegerType.create(field_size) 28 | else: 29 | return ArrayType.create(Type.char(), field_size) 30 | 31 | 32 | def _create_variant_struct(rust_variant: Variant) -> StructureType: 33 | bn_struct = StructureBuilder.create(packed=True) 34 | 35 | if rust_variant.fields is not None: 36 | for rust_struct_field in rust_variant.fields: 37 | if isinstance(rust_struct_field, Field): 38 | bn_field_type = _create_bn_type_from_field_size( 39 | rust_struct_field.field_size 40 | ) 41 | bn_struct.append(type=bn_field_type, name=rust_struct_field.field_name) 42 | elif isinstance(rust_struct_field, Padding): 43 | bn_field_type = _create_bn_type_from_field_size( 44 | rust_struct_field.padding_size 45 | ) 46 | bn_struct.append(type=bn_field_type, name="_padding") 47 | 48 | if bn_struct.width != rust_variant.variant_size: 49 | logger.log_error( 50 | f"Size of created variant struct ({bn_struct.width} bytes) does not match size of parsed Rust variant struct ({rust_variant.variant_size} bytes)" 51 | ) 52 | 53 | return bn_struct.immutable_copy() 54 | 55 | 56 | def _create_bn_types_for_rust_type(bv: BinaryView, rust_type: RustType): 57 | """ 58 | Note that for Rust enums, i.e. `RustType`s which hold 59 | a `Discriminant` and `Variant`(s), 60 | this function creates the following Binary Ninja types, 61 | - A new enum type for the discriminant. 62 | - A new struct type for each variant. 63 | - A new struct type containing: 64 | - The enum type for the discriminant. 65 | - A union whose members are the struct types for each variant. 66 | """ 67 | 68 | bn_struct = StructureBuilder.create(packed=True) 69 | rust_field_types = [type(field) for field in rust_type.fields] 70 | 71 | # The Rust type is a sum type, i.e. a Rust enum. 72 | if Variant in rust_field_types: 73 | # TODO: Handle repr(C) unions correctly; 74 | # it seems these are not being created with the correct size. 75 | # For example, see SocketAddrCRepr: 76 | # https://github.com/rust-lang/rust/blob/8a778ca1e35e4a8df95c00d800100d95e63e7722/library/std/src/sys_common/net.rs#L725 77 | bn_variants_union = StructureBuilder.create( 78 | packed=True, type=StructureVariant.UnionStructureType 79 | ) 80 | 81 | # It is possible to have a sum type with variants, but with no discriminant. 82 | bn_discriminant_enum_name: Optional[str] = None 83 | for rust_type_field in rust_type.fields: 84 | if isinstance(rust_type_field, Discriminant): 85 | bn_discriminant_enum_name = f"{rust_type.type_name}::discriminant" 86 | bn_discriminant_enum = EnumerationBuilder.create( 87 | width=rust_type_field.discriminant_size 88 | ) 89 | 90 | bv.define_user_type( 91 | name=bn_discriminant_enum_name, 92 | type_obj=bn_discriminant_enum, 93 | ) 94 | bn_struct.append( 95 | type=Type.named_type_from_registered_type( 96 | view=bv, 97 | name=bn_discriminant_enum_name, 98 | ), 99 | name="discriminant", 100 | ) 101 | elif isinstance(rust_type_field, Variant): 102 | bn_variant_struct_name = ( 103 | f"{rust_type.type_name}::{rust_type_field.variant_name}" 104 | ) 105 | bn_variant_struct = _create_variant_struct(rust_type_field) 106 | 107 | bv.define_user_type( 108 | name=bn_variant_struct_name, 109 | type_obj=bn_variant_struct, 110 | ) 111 | bn_variants_union.append( 112 | type=Type.named_type_from_registered_type( 113 | view=bv, 114 | name=bn_variant_struct_name, 115 | ), 116 | name=rust_type_field.variant_name, 117 | ) 118 | if bn_discriminant_enum_name is not None: 119 | # Modify the existing discriminant enum type 120 | # to add an entry for this variant. 121 | with Type.builder( 122 | bv=bv, 123 | name=QualifiedName(bn_discriminant_enum_name), 124 | ) as bn_discriminant_enum: 125 | # The discriminant value used to represent each variant 126 | # does not necessarily match the ordering of those variants 127 | # in the type layout information, i.e. the first variant 128 | # is not necessarily discriminant value 0, etc. 129 | # The information emitted by rustc's `print-type-sizes` flag 130 | # also does not include the discriminant value for each variant. 131 | # Therefore, all variants are assigned a discriminant value of -1. 132 | bn_discriminant_enum.append(rust_type_field.variant_name, -1) 133 | 134 | bn_struct.append( 135 | type=bn_variants_union, 136 | name=f"{rust_type.type_name}::variants", 137 | ) 138 | 139 | # The Rust type is a product type, i.e. a Rust struct. 140 | else: 141 | for rust_type_field in rust_type.fields: 142 | if isinstance(rust_type_field, Field): 143 | bn_field_type = _create_bn_type_from_field_size( 144 | rust_type_field.field_size 145 | ) 146 | bn_struct.append(type=bn_field_type, name=rust_type_field.field_name) 147 | elif isinstance(rust_type_field, Padding): 148 | bn_field_type = _create_bn_type_from_field_size( 149 | rust_type_field.padding_size 150 | ) 151 | bn_struct.append(type=bn_field_type, name="_padding") 152 | 153 | if bn_struct.width != rust_type.type_size: 154 | logger.log_error( 155 | f"Created struct for Rust type {rust_type.type_name} has size ({bn_struct.width} bytes) which does not match size of parsed Rust type ({rust_type.type_size} bytes)" 156 | ) 157 | 158 | bv.define_user_type(name=rust_type.type_name, type_obj=bn_struct) 159 | 160 | 161 | def create_binary_view_types( 162 | bv: BinaryView, 163 | rust_types: List[RustType], 164 | ): 165 | for rust_type in rust_types: 166 | _create_bn_types_for_rust_type(bv=bv, rust_type=rust_type) 167 | 168 | # TODO: If every variant is zero-sized, it can be an enum. 169 | # The discriminant size is then used to calculate the enum width. 170 | -------------------------------------------------------------------------------- /parse.py: -------------------------------------------------------------------------------- 1 | import string 2 | from dataclasses import dataclass 3 | from typing import List, Optional, TextIO, Union 4 | 5 | from pyparsing import ( 6 | Keyword, 7 | Literal, 8 | Opt, 9 | ParserElement, 10 | Word, 11 | ZeroOrMore, 12 | nums, 13 | ) 14 | 15 | 16 | @dataclass 17 | class Field: 18 | field_name: str 19 | field_size: int 20 | field_offset_bytes: Optional[int] 21 | field_alignment_bytes: Optional[int] 22 | 23 | 24 | @dataclass 25 | class Padding: 26 | padding_size: int 27 | 28 | 29 | @dataclass 30 | class Discriminant: 31 | discriminant_size: int 32 | 33 | 34 | @dataclass 35 | class Variant: 36 | variant_name: str 37 | variant_size: int 38 | fields: Optional[List[Union[Field, Padding]]] 39 | 40 | 41 | @dataclass 42 | class Type: 43 | type_name: str 44 | type_size: int 45 | type_alignment_bytes: int 46 | fields: List[Union[Field, Padding, Discriminant, Variant]] 47 | 48 | 49 | def _type_definition_line() -> ParserElement: 50 | line_marker = Keyword("print-type-size") 51 | type_marker = Keyword("type:") 52 | name = ( 53 | Literal("`") 54 | # Because type names can include spaces, 55 | # such as in `Result`, 56 | # we need to use string.printable rather than 57 | # pyparsing.printable, which excludes spaces. 58 | + Word(string.printable, exclude_chars="`").set_results_name("type_name") 59 | + Literal("`") 60 | + Literal(":") 61 | ) 62 | size = Word(nums).set_results_name("type_size") + Keyword("bytes") 63 | alignment_marker = Keyword("alignment:") 64 | alignment_bytes = Word(nums).set_results_name("type_alignment_bytes") + Keyword( 65 | "bytes" 66 | ) 67 | alignment_information = alignment_marker + alignment_bytes 68 | 69 | type_definition = ( 70 | line_marker + type_marker + name + size + Literal(",") + alignment_information 71 | ) 72 | 73 | return type_definition 74 | 75 | 76 | def _field_definition_line() -> ParserElement: 77 | line_marker = Keyword("print-type-size") 78 | field_marker = Keyword("field") 79 | name = ( 80 | Literal("`") 81 | + Word(string.printable, exclude_chars="`").set_results_name("field_name") 82 | + Literal("`") 83 | + Literal(":") 84 | ) 85 | size = Word(nums).set_results_name("field_size") + Keyword("bytes") 86 | 87 | offset_marker = Keyword("offset:") 88 | offset_bytes = Word(nums).set_results_name("field_offset_bytes") + Keyword("bytes") 89 | offset_information = offset_marker + offset_bytes 90 | 91 | alignment_marker = Keyword("alignment:") 92 | alignment_bytes = Word(nums).set_results_name("field_alignment_bytes") + Keyword( 93 | "bytes" 94 | ) 95 | alignment_information = alignment_marker + alignment_bytes 96 | 97 | field_definition = ( 98 | line_marker 99 | + field_marker 100 | + name 101 | + size 102 | + Opt(Literal(",") + offset_information) 103 | + Opt(Literal(",") + alignment_information) 104 | ) 105 | 106 | field_definition.set_parse_action( 107 | lambda results: Field( 108 | field_name=results.field_name, # type: ignore 109 | field_size=int(results.field_size), # type: ignore 110 | field_offset_bytes=int(results.field_offset_bytes) if results.field_offset_bytes else None, # type: ignore 111 | field_alignment_bytes=int(results.field_alignment_bytes) # type: ignore 112 | if results.field_alignment_bytes 113 | else None, 114 | ) 115 | ) 116 | 117 | return field_definition 118 | 119 | 120 | def _padding_definition_line() -> ParserElement: 121 | line_marker = Keyword("print-type-size") 122 | padding_marker = Keyword("padding:") 123 | size = Word(nums).set_results_name("padding_size") + Keyword("bytes") 124 | 125 | padding_definition = line_marker + padding_marker + size 126 | 127 | padding_definition.set_parse_action( 128 | lambda results: Padding(int(results.padding_size)) # type: ignore 129 | ) 130 | 131 | return padding_definition 132 | 133 | 134 | def _end_padding_definition_line() -> ParserElement: 135 | line_marker = Keyword("print-type-size") 136 | padding_marker = Keyword("end padding:") 137 | size = Word(nums).set_results_name("padding_size") + Keyword("bytes") 138 | 139 | padding_definition = line_marker + padding_marker + size 140 | 141 | padding_definition.set_parse_action( 142 | lambda results: Padding(int(results.padding_size)) # type: ignore 143 | ) 144 | 145 | return padding_definition 146 | 147 | 148 | def _variant_definition_line() -> ParserElement: 149 | line_marker = Keyword("print-type-size") 150 | variant_marker = Keyword("variant") 151 | name = ( 152 | Literal("`") 153 | + Word(string.printable, exclude_chars="`").set_results_name("variant_name") 154 | + Literal("`") 155 | + Literal(":") 156 | ) 157 | size = Word(nums).set_results_name("variant_size") + Keyword("bytes") 158 | 159 | variant_definition = line_marker + variant_marker + name + size 160 | 161 | return variant_definition 162 | 163 | 164 | def _discriminant_definition_line() -> ParserElement: 165 | line_marker = Keyword("print-type-size") 166 | discriminant_marker = Keyword("discriminant:") 167 | size = Word(nums).set_results_name("discriminant_size") + Keyword("bytes") 168 | 169 | discriminant_definition = line_marker + discriminant_marker + size 170 | 171 | discriminant_definition.set_parse_action( 172 | lambda results: Discriminant(int(results.discriminant_size)) # type: ignore 173 | ) 174 | 175 | return discriminant_definition 176 | 177 | 178 | def _variant() -> ParserElement: 179 | variant = _variant_definition_line() + ZeroOrMore( 180 | _field_definition_line() 181 | | _padding_definition_line() 182 | | _end_padding_definition_line() 183 | ).set_results_name("fields") 184 | 185 | variant.set_parse_action( 186 | lambda results: Variant( 187 | variant_name=results.variant_name, # type: ignore 188 | variant_size=int(results.variant_size), # type: ignore 189 | fields=results.fields.as_list(), # type: ignore 190 | ) 191 | ) 192 | 193 | return variant 194 | 195 | 196 | def parse(data: TextIO) -> List: 197 | type_definition = _type_definition_line() + ZeroOrMore( 198 | _field_definition_line() 199 | | _padding_definition_line() 200 | | _end_padding_definition_line() 201 | | _discriminant_definition_line() 202 | | _variant() 203 | ).set_results_name("fields") 204 | 205 | type_definition.set_parse_action( 206 | lambda results: Type( 207 | type_name=results.type_name, # type: ignore 208 | type_size=int(results.type_size), # type: ignore 209 | type_alignment_bytes=int(results.type_alignment_bytes), # type: ignore 210 | fields=results.fields.as_list(), # type: ignore 211 | ) 212 | ) 213 | 214 | types = ZeroOrMore(type_definition) 215 | return types.parse_file(data, parse_all=True).as_list() 216 | 217 | 218 | if __name__ == "__main__": 219 | from argparse import ArgumentParser 220 | from pprint import pprint 221 | 222 | argparser = ArgumentParser() 223 | argparser.add_argument( 224 | "print_type_sizes_output_file", 225 | help="A file containing Rust type layout information, which is the output of `rustc +nightly -Zprint-type-sizes`", 226 | ) 227 | args = argparser.parse_args() 228 | 229 | with open(args.print_type_sizes_output_file, "r") as f: 230 | result = parse(f) 231 | pprint(result) 232 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Binary Ninja Rust Type Layout Helper Plugin 🦀 2 | 3 | An extremely experimental Binary Ninja importer for the type layout information emitted by the [`-Zprint-type-sizes` flag](https://nnethercote.github.io/perf-book/type-sizes.html) of the Rust compiler. 4 | 5 | This plugin is meant to help reverse engineers with the following: 6 | - Getting a sense of how, in general, Rust data structures are laid out in memory. 7 | - Getting more comfortable with certain core data structures which appear in Rust binaries. 8 | 9 | ![A screenshot of Binary Ninja's Types view in the sidebar, showing the imported definitions and layouts of several Rust types from `std::sys::windows`.](images/std-sys-windows-types-border.png) 10 | 11 | ## How to use this plugin 12 | 13 | Compile some Rust code with the following options: 14 | 15 | MacOS / Linux: 16 | 17 | ```sh 18 | cargo clean 19 | RUSTFLAGS=-Zprint-type-sizes cargo +nightly build -j 1 > type-sizes.txt 20 | ``` 21 | 22 | Windows (Powershell): 23 | 24 | ```powershell 25 | cargo clean 26 | $env:RUSTFLAGS="-Zprint-type-sizes"; cargo +nightly build -j 1 > type-sizes.txt 27 | ``` 28 | 29 | The following are all necessary for this to work: 30 | - `cargo clean` is required before you do the build, i.e. this needs to be a completely fresh build. This is required to avoid missing information in the output. 31 | - `-Zprint-type-sizes` in the `RUSTFLAGS` passed to rustc. This flag is what actually triggers `rustc` to produce the type information. 32 | - `+nightly` passed to cargo, as the `print-type-sizes` flag is only supported on nightly toolchain builds. 33 | - `-j 1` to avoid shuffled lines in the output. 34 | 35 | You should see output like this in the generated `type-sizes.txt` file: 36 | 37 | ``` 38 | print-type-size type: `core::num::dec2flt::decimal::Decimal`: 784 bytes, alignment: 8 bytes 39 | print-type-size field `.digits`: 768 bytes 40 | print-type-size field `.num_digits`: 8 bytes 41 | print-type-size field `.decimal_point`: 4 bytes 42 | print-type-size field `.truncated`: 1 bytes 43 | print-type-size end padding: 3 bytes 44 | print-type-size type: `std::result::Result`: 616 bytes, alignment: 8 bytes 45 | print-type-size variant `Ok`: 616 bytes 46 | print-type-size field `.0`: 616 bytes 47 | print-type-size variant `Err`: 8 bytes 48 | print-type-size field `.0`: 8 bytes 49 | print-type-size type: `std::sys::windows::fs::ReadDir`: 616 bytes, alignment: 8 bytes 50 | print-type-size field `.handle`: 8 bytes 51 | print-type-size field `.root`: 8 bytes 52 | print-type-size field `.first`: 596 bytes 53 | print-type-size end padding: 4 bytes 54 | print-type-size type: `std::option::Option>`: 608 bytes, alignment: 8 bytes 55 | print-type-size discriminant: 8 bytes 56 | print-type-size variant `Some`: 600 bytes 57 | print-type-size field `.0`: 600 bytes 58 | print-type-size variant `None`: 0 bytes 59 | [...] 60 | ``` 61 | 62 | You can now use the _Plugins > Rust Type Layout Helper - Load File..._ command to import the contents of this file into Binary Ninja. The following types in Binary Ninja wil be created from the types shown in the example above: 63 | 64 | ```c 65 | struct core::num::dec2flt::decimal::Decimal __packed 66 | { 67 | char .digits[0x300]; 68 | int64_t .num_digits; 69 | int32_t .decimal_point; 70 | char .truncated; 71 | char _padding[0x3]; 72 | }; 73 | 74 | struct std::result::Result __packed 75 | { 76 | union __packed 77 | { 78 | struct std::result::Result::Ok Ok; 79 | struct std::result::Result::Err Err; 80 | } std::result::Result::variants; 81 | }; 82 | 83 | struct std::result::Result::Err __packed 84 | { 85 | int64_t .0; 86 | }; 87 | 88 | struct std::result::Result::Ok __packed 89 | { 90 | char .0[0x268]; 91 | }; 92 | 93 | struct std::sys::windows::fs::ReadDir __packed 94 | { 95 | int64_t .handle; 96 | int64_t .root; 97 | char .first[0x254]; 98 | int32_t _padding; 99 | }; 100 | 101 | struct std::option::Option> __packed 102 | { 103 | enum std::option::Option>::discriminant discriminant; 104 | union __packed 105 | { 106 | struct std::option::Option>::Some Some; 107 | struct std::option::Option>::None None; 108 | } std::option::Option>::variants; 109 | }; 110 | 111 | struct std::option::Option>::None __packed 112 | { 113 | }; 114 | 115 | struct std::option::Option>::Some __packed 116 | { 117 | char .0[0x258]; 118 | }; 119 | 120 | enum std::option::Option>::discriminant : uint64_t 121 | { 122 | Some = 0xffffffffffffffff, 123 | None = 0xffffffffffffffff 124 | }; 125 | ``` 126 | 127 | ## Caveats and future work 128 | 129 | There are some caveats to using this: 130 | - The layout of data types is not stable, and can change between compilations! 131 | - Only the nightly builds of rustc supports the `print-type-sizes` flag. 132 | - Binary Ninja's support for working with unions in the decompilation is currently quite poor (see [Vector35/binaryninja-api#1013](https://github.com/Vector35/binaryninja-api/issues/1013), [Vector35/binaryninja-api#4218](https://github.com/Vector35/binaryninja-api/issues/4218)). This may make it difficult to work with the generated `variants` unions, such as `std::option::Option>::variants` in the example above. 133 | - When importing sum types (i.e. Rust enums), the discriminant value used to represent each variant in the sum type does not necessarily match the ordering of those variant in the type layout information, i.e. the first variant is not necessarily discriminant value 0, etc. The information emitted by rustc's `print-type-sizes` flag also does not include the discriminant value for each variant. Therefore, all variants are assigned a discriminant value of -1. To determine the actual determinant value, it is up to the user to reverse the code where the sum type is used. 134 | 135 | In the future it would be nice to: 136 | - Add scripts / plugins to import the type information into IDA and Ghidra. 137 | - Use a Rust compiler plugin to emit better type information than we get from `-Zprint-type-sizes`? Maybe a combination of the information we get from `-Zprint-type-sizes` and `#[rustc_layout(...)]`. It would also be nice to emit the type information in a format which is slightly easier to parse (e.g. JSON). 138 | 139 | ## Installation 140 | 141 | This plugin can be installed via either: 142 | 143 | 1) Searching for the _Rust Type Layout Helper_ plugin in Binary Ninja's built-in plugin manager (_Plugins > Manage Plugins_). _This is the recommended method._ 144 | 145 | 2) Cloning this repository into your user plugins folder. 146 | - The [location of the user plugins folder will vary depending on the platform Binary Ninja is installed on](https://docs.binary.ninja/guide/index.html#user-folder). The easiest way to find the location of the folder is via the _Plugins > Open Plugin Folder..._ command. 147 | - If you are performing an installation via this method, you must also install this plugin's Python dependencies manually. This can be done by either: 148 | - Running the _Install python3 module..._ command (via the Command Palette), and pasting the contents of [`requirements.txt`](requirements.txt) in this repository into the dialog window. 149 | - Running `pip install -r requirements.txt` in the Python environment used by Binary Ninja. 150 | 151 | This plugin requires Python >= 3.7, and Binary Ninja version >= 3.2.3814. 152 | 153 | ## Development 154 | 155 | ### Setting up a development environment 156 | 157 | To set up a development environment, including setting up a Python virtual environment: 158 | 159 | ``` 160 | python -m venv .venv && . .venv/bin/activate 161 | pip install -r requirements.txt 162 | pip install -r dev-requirements.txt 163 | python $PATH_TO_BINARY_NINJA_INSTALLATION/scripts/install_api.py 164 | ``` 165 | 166 | For formatting, linting, and running unit tests locally, install [Nox](https://nox.thea.codes/en/stable/tutorial.html), then: 167 | 168 | ``` 169 | nox 170 | ``` 171 | 172 | You can also invoke each task separately; see [noxfile.py](noxfile.py) for more details on available tasks: 173 | 174 | ``` 175 | nox -s format 176 | nox -s lint 177 | nox -s test 178 | ``` 179 | 180 | Linting and unit testing (both against multiple Python versions) are also set up in CI on [GitHub Actions](.github/workflows/ci.yml). 181 | 182 | ### Testing local versions of the plugin 183 | 184 | To test the plugin locally in your own Binary Ninja installation during development, create a symbolic link between your development folder, and the [Binary Ninja user plugins folder](https://docs.binary.ninja/guide/index.html#user-folder), so that your development folder is loaded by Binary Ninja on startup as a plugin. 185 | 186 | - MacOS: 187 | 188 | ```sh 189 | ln -s --relative . ~/Library/Application\ Support/Binary\ Ninja/plugins/rust_type_layout_helper 190 | ``` 191 | 192 | - Linux: 193 | 194 | ```sh 195 | ln -s --relative . ~/.binaryninja/plugins/rust_type_layout_helper 196 | ``` 197 | 198 | - Windows (Powershell): 199 | ```powershell 200 | New-Item -ItemType Junction -Value $(Get-Location) -Path "$env:APPDATA\Binary Ninja\plugins\rust_type_layout_helper" 201 | ``` 202 | 203 | You should then change the values of the following Python settings in Binary Ninja to point to inside your development folder's virtual environment: 204 | 205 | - `python.binaryOverride`: Set this to the path of the Python interpreter inside your development virtual environment, e.g. `$DEVELOPMENT_FOLDER/rust_type_layout_helper/.venv/bin/python/` 206 | - `python.virtualenv`: Set this to the path of the `site-packages` directory inside your development virtual environment, e.g. `$DEVELOPMENT_FOLDER/rust_type_layout_helper/.venv/lib/python3.11/site-packages` 207 | 208 | ## Acknowledgements and resources 209 | 210 | The compilation instructions for emitting type information are taken from the instructions in the [`top-type-sizes` crate, by Paul Loyd](https://github.com/loyd/top-type-sizes). 211 | -------------------------------------------------------------------------------- /tests/test_parse.py: -------------------------------------------------------------------------------- 1 | import io 2 | from pathlib import Path 3 | 4 | import pytest 5 | from pyparsing import ParseException 6 | 7 | from ..parse import Discriminant, Field, Padding, Type, Variant, parse 8 | 9 | test_type_parse_data = [ 10 | ( 11 | "box-types.txt", 12 | [ 13 | Type( 14 | type_name="std::boxed::Box std::ops::Fn(&'a std::panic::PanicInfo<'b>) + std::marker::Send + std::marker::Sync>", 15 | type_size=16, 16 | type_alignment_bytes=8, 17 | fields=[ 18 | Field( 19 | field_name=".1", 20 | field_size=0, 21 | field_offset_bytes=None, 22 | field_alignment_bytes=None, 23 | ), 24 | Field( 25 | field_name=".0", 26 | field_size=16, 27 | field_offset_bytes=None, 28 | field_alignment_bytes=None, 29 | ), 30 | ], 31 | ), 32 | Type( 33 | type_name="std::boxed::Box", 34 | type_size=16, 35 | type_alignment_bytes=8, 36 | fields=[ 37 | Field( 38 | field_name=".1", 39 | field_size=0, 40 | field_offset_bytes=None, 41 | field_alignment_bytes=None, 42 | ), 43 | Field( 44 | field_name=".0", 45 | field_size=16, 46 | field_offset_bytes=None, 47 | field_alignment_bytes=None, 48 | ), 49 | ], 50 | ), 51 | Type( 52 | type_name="std::boxed::Box", 53 | type_size=16, 54 | type_alignment_bytes=8, 55 | fields=[ 56 | Field( 57 | field_name=".1", 58 | field_size=0, 59 | field_offset_bytes=None, 60 | field_alignment_bytes=None, 61 | ), 62 | Field( 63 | field_name=".0", 64 | field_size=16, 65 | field_offset_bytes=None, 66 | field_alignment_bytes=None, 67 | ), 68 | ], 69 | ), 70 | ], 71 | ), 72 | ( 73 | "empty-types.txt", 74 | [ 75 | Type( 76 | type_name="std::marker::PhantomData<&std::sys::windows::args::Arg>", 77 | type_size=0, 78 | type_alignment_bytes=1, 79 | fields=[], 80 | ), 81 | Type( 82 | type_name="std::marker::PhantomData<&u8>", 83 | type_size=0, 84 | type_alignment_bytes=1, 85 | fields=[], 86 | ), 87 | Type( 88 | type_name="std::marker::PhantomData<(alloc::collections::btree::node::marker::Immut<'_>, alloc::collections::btree::node::marker::Leaf)>", 89 | type_size=0, 90 | type_alignment_bytes=1, 91 | fields=[], 92 | ), 93 | Type( 94 | type_name="std::marker::PhantomData<(alloc::collections::btree::node::marker::Immut<'_>, alloc::collections::btree::node::marker::LeafOrInternal)>", 95 | type_size=0, 96 | type_alignment_bytes=1, 97 | fields=[], 98 | ), 99 | Type( 100 | type_name="std::marker::PhantomData<(alloc::collections::btree::node::marker::Owned, alloc::collections::btree::node::marker::LeafOrInternal)>", 101 | type_size=0, 102 | type_alignment_bytes=1, 103 | fields=[], 104 | ), 105 | Type( 106 | type_name="std::marker::PhantomData<*mut ()>", 107 | type_size=0, 108 | type_alignment_bytes=1, 109 | fields=[], 110 | ), 111 | Type( 112 | type_name="std::marker::PhantomData<[std::mem::MaybeUninit]>", 113 | type_size=0, 114 | type_alignment_bytes=1, 115 | fields=[], 116 | ), 117 | Type( 118 | type_name="std::marker::PhantomData<[u8]>", 119 | type_size=0, 120 | type_alignment_bytes=1, 121 | fields=[], 122 | ), 123 | ], 124 | ), 125 | ( 126 | "misc-types.txt", 127 | [ 128 | Type( 129 | type_name="std::char::EscapeDefault", 130 | type_size=16, 131 | type_alignment_bytes=8, 132 | fields=[ 133 | Field( 134 | field_name=".state", 135 | field_size=16, 136 | field_offset_bytes=None, 137 | field_alignment_bytes=None, 138 | ) 139 | ], 140 | ), 141 | Type( 142 | type_name="std::char::EscapeDefaultState", 143 | type_size=16, 144 | type_alignment_bytes=8, 145 | fields=[ 146 | Variant( 147 | variant_name="Unicode", 148 | variant_size=16, 149 | fields=[ 150 | Field( 151 | field_name=".0", 152 | field_size=16, 153 | field_offset_bytes=None, 154 | field_alignment_bytes=None, 155 | ) 156 | ], 157 | ), 158 | Variant( 159 | variant_name="Char", 160 | variant_size=4, 161 | fields=[ 162 | Field( 163 | field_name=".0", 164 | field_size=4, 165 | field_offset_bytes=None, 166 | field_alignment_bytes=None, 167 | ) 168 | ], 169 | ), 170 | Variant( 171 | variant_name="Backslash", 172 | variant_size=4, 173 | fields=[ 174 | Field( 175 | field_name=".0", 176 | field_size=4, 177 | field_offset_bytes=None, 178 | field_alignment_bytes=None, 179 | ) 180 | ], 181 | ), 182 | Variant(variant_name="Done", variant_size=0, fields=[]), 183 | ], 184 | ), 185 | Type( 186 | type_name="std::char::EscapeUnicode", 187 | type_size=16, 188 | type_alignment_bytes=8, 189 | fields=[ 190 | Field( 191 | field_name=".hex_digit_idx", 192 | field_size=8, 193 | field_offset_bytes=None, 194 | field_alignment_bytes=None, 195 | ), 196 | Field( 197 | field_name=".c", 198 | field_size=4, 199 | field_offset_bytes=None, 200 | field_alignment_bytes=None, 201 | ), 202 | Field( 203 | field_name=".state", 204 | field_size=1, 205 | field_offset_bytes=None, 206 | field_alignment_bytes=None, 207 | ), 208 | Padding(padding_size=3), 209 | ], 210 | ), 211 | Type( 212 | type_name="std::collections::Bound", 213 | type_size=16, 214 | type_alignment_bytes=8, 215 | fields=[ 216 | Discriminant(discriminant_size=8), 217 | Variant( 218 | variant_name="Included", 219 | variant_size=8, 220 | fields=[ 221 | Field( 222 | field_name=".0", 223 | field_size=8, 224 | field_offset_bytes=None, 225 | field_alignment_bytes=None, 226 | ) 227 | ], 228 | ), 229 | Variant( 230 | variant_name="Excluded", 231 | variant_size=8, 232 | fields=[ 233 | Field( 234 | field_name=".0", 235 | field_size=8, 236 | field_offset_bytes=None, 237 | field_alignment_bytes=None, 238 | ) 239 | ], 240 | ), 241 | Variant(variant_name="Unbounded", variant_size=0, fields=[]), 242 | ], 243 | ), 244 | Type( 245 | type_name="std::collections::TryReserveError", 246 | type_size=16, 247 | type_alignment_bytes=8, 248 | fields=[ 249 | Field( 250 | field_name=".kind", 251 | field_size=16, 252 | field_offset_bytes=None, 253 | field_alignment_bytes=None, 254 | ) 255 | ], 256 | ), 257 | Type( 258 | type_name="std::collections::TryReserveErrorKind", 259 | type_size=16, 260 | type_alignment_bytes=8, 261 | fields=[ 262 | Variant( 263 | variant_name="AllocError", 264 | variant_size=16, 265 | fields=[ 266 | Field( 267 | field_name=".non_exhaustive", 268 | field_size=0, 269 | field_offset_bytes=None, 270 | field_alignment_bytes=None, 271 | ), 272 | Field( 273 | field_name=".layout", 274 | field_size=16, 275 | field_offset_bytes=None, 276 | field_alignment_bytes=None, 277 | ), 278 | ], 279 | ), 280 | Variant(variant_name="CapacityOverflow", variant_size=0, fields=[]), 281 | ], 282 | ), 283 | Type( 284 | type_name="std::collections::hash_map::RandomState", 285 | type_size=16, 286 | type_alignment_bytes=8, 287 | fields=[ 288 | Field( 289 | field_name=".k0", 290 | field_size=8, 291 | field_offset_bytes=None, 292 | field_alignment_bytes=None, 293 | ), 294 | Field( 295 | field_name=".k1", 296 | field_size=8, 297 | field_offset_bytes=None, 298 | field_alignment_bytes=None, 299 | ), 300 | ], 301 | ), 302 | ], 303 | ), 304 | ( 305 | "structs.txt", 306 | [ 307 | Type( 308 | type_name="std::sys::windows::c::WIN32_FIND_DATAW", 309 | type_size=592, 310 | type_alignment_bytes=4, 311 | fields=[ 312 | Field( 313 | field_name=".dwFileAttributes", 314 | field_size=4, 315 | field_offset_bytes=None, 316 | field_alignment_bytes=None, 317 | ), 318 | Field( 319 | field_name=".ftCreationTime", 320 | field_size=8, 321 | field_offset_bytes=None, 322 | field_alignment_bytes=None, 323 | ), 324 | Field( 325 | field_name=".ftLastAccessTime", 326 | field_size=8, 327 | field_offset_bytes=None, 328 | field_alignment_bytes=None, 329 | ), 330 | Field( 331 | field_name=".ftLastWriteTime", 332 | field_size=8, 333 | field_offset_bytes=None, 334 | field_alignment_bytes=None, 335 | ), 336 | Field( 337 | field_name=".nFileSizeHigh", 338 | field_size=4, 339 | field_offset_bytes=None, 340 | field_alignment_bytes=None, 341 | ), 342 | Field( 343 | field_name=".nFileSizeLow", 344 | field_size=4, 345 | field_offset_bytes=None, 346 | field_alignment_bytes=None, 347 | ), 348 | Field( 349 | field_name=".dwReserved0", 350 | field_size=4, 351 | field_offset_bytes=None, 352 | field_alignment_bytes=None, 353 | ), 354 | Field( 355 | field_name=".dwReserved1", 356 | field_size=4, 357 | field_offset_bytes=None, 358 | field_alignment_bytes=None, 359 | ), 360 | Field( 361 | field_name=".cFileName", 362 | field_size=520, 363 | field_offset_bytes=None, 364 | field_alignment_bytes=None, 365 | ), 366 | Field( 367 | field_name=".cAlternateFileName", 368 | field_size=28, 369 | field_offset_bytes=None, 370 | field_alignment_bytes=None, 371 | ), 372 | ], 373 | ), 374 | Type( 375 | type_name="std::sys::windows::c::fd_set", 376 | type_size=520, 377 | type_alignment_bytes=8, 378 | fields=[ 379 | Field( 380 | field_name=".fd_count", 381 | field_size=4, 382 | field_offset_bytes=None, 383 | field_alignment_bytes=None, 384 | ), 385 | Padding(padding_size=4), 386 | Field( 387 | field_name=".fd_array", 388 | field_size=512, 389 | field_offset_bytes=None, 390 | field_alignment_bytes=8, 391 | ), 392 | ], 393 | ), 394 | Type( 395 | type_name="std::backtrace::BacktraceFrame", 396 | type_size=312, 397 | type_alignment_bytes=8, 398 | fields=[ 399 | Field( 400 | field_name=".frame", 401 | field_size=288, 402 | field_offset_bytes=None, 403 | field_alignment_bytes=None, 404 | ), 405 | Field( 406 | field_name=".symbols", 407 | field_size=24, 408 | field_offset_bytes=None, 409 | field_alignment_bytes=None, 410 | ), 411 | ], 412 | ), 413 | Type( 414 | type_name="std::io::BufWriter", 415 | type_size=32, 416 | type_alignment_bytes=8, 417 | fields=[ 418 | Field( 419 | field_name=".buf", 420 | field_size=24, 421 | field_offset_bytes=None, 422 | field_alignment_bytes=None, 423 | ), 424 | Field( 425 | field_name=".inner", 426 | field_size=5, 427 | field_offset_bytes=None, 428 | field_alignment_bytes=None, 429 | ), 430 | Field( 431 | field_name=".panicked", 432 | field_size=1, 433 | field_offset_bytes=None, 434 | field_alignment_bytes=None, 435 | ), 436 | Padding(padding_size=2), 437 | ], 438 | ), 439 | ], 440 | ), 441 | ( 442 | "variants.txt", 443 | [ 444 | Type( 445 | type_name="std::sys_common::net::SocketAddrCRepr", 446 | type_size=28, 447 | type_alignment_bytes=4, 448 | fields=[ 449 | Variant( 450 | variant_name="SocketAddrCRepr", 451 | variant_size=28, 452 | fields=[ 453 | Field( 454 | field_name=".v4", 455 | field_size=16, 456 | field_offset_bytes=None, 457 | field_alignment_bytes=None, 458 | ), 459 | Field( 460 | field_name=".v6", 461 | field_size=28, 462 | field_offset_bytes=0, 463 | field_alignment_bytes=4, 464 | ), 465 | ], 466 | ) 467 | ], 468 | ), 469 | Type( 470 | type_name="Number", 471 | type_size=24, 472 | type_alignment_bytes=8, 473 | fields=[ 474 | Discriminant(discriminant_size=8), 475 | Variant( 476 | variant_name="Complex", 477 | variant_size=16, 478 | fields=[ 479 | Field( 480 | field_name=".real", 481 | field_size=8, 482 | field_offset_bytes=None, 483 | field_alignment_bytes=None, 484 | ), 485 | Field( 486 | field_name=".imaginary", 487 | field_size=8, 488 | field_offset_bytes=None, 489 | field_alignment_bytes=None, 490 | ), 491 | ], 492 | ), 493 | Variant( 494 | variant_name="Integer", 495 | variant_size=8, 496 | fields=[ 497 | Field( 498 | field_name=".0", 499 | field_size=8, 500 | field_offset_bytes=None, 501 | field_alignment_bytes=None, 502 | ) 503 | ], 504 | ), 505 | Variant( 506 | variant_name="Float", 507 | variant_size=8, 508 | fields=[ 509 | Field( 510 | field_name=".0", 511 | field_size=8, 512 | field_offset_bytes=None, 513 | field_alignment_bytes=None, 514 | ) 515 | ], 516 | ), 517 | ], 518 | ), 519 | Type( 520 | type_name="core::num::fmt::Part<'_>", 521 | type_size=24, 522 | type_alignment_bytes=8, 523 | fields=[ 524 | Discriminant(discriminant_size=2), 525 | Variant( 526 | variant_name="Copy", 527 | variant_size=22, 528 | fields=[ 529 | Padding(padding_size=6), 530 | Field( 531 | field_name=".0", 532 | field_size=16, 533 | field_offset_bytes=None, 534 | field_alignment_bytes=8, 535 | ), 536 | ], 537 | ), 538 | Variant( 539 | variant_name="Zero", 540 | variant_size=14, 541 | fields=[ 542 | Padding(padding_size=6), 543 | Field( 544 | field_name=".0", 545 | field_size=8, 546 | field_offset_bytes=None, 547 | field_alignment_bytes=8, 548 | ), 549 | ], 550 | ), 551 | Variant( 552 | variant_name="Num", 553 | variant_size=2, 554 | fields=[ 555 | Field( 556 | field_name=".0", 557 | field_size=2, 558 | field_offset_bytes=None, 559 | field_alignment_bytes=None, 560 | ) 561 | ], 562 | ), 563 | ], 564 | ), 565 | Type( 566 | type_name="std::str::pattern::SearchStep", 567 | type_size=24, 568 | type_alignment_bytes=8, 569 | fields=[ 570 | Discriminant(discriminant_size=8), 571 | Variant( 572 | variant_name="Match", 573 | variant_size=16, 574 | fields=[ 575 | Field( 576 | field_name=".0", 577 | field_size=8, 578 | field_offset_bytes=None, 579 | field_alignment_bytes=None, 580 | ), 581 | Field( 582 | field_name=".1", 583 | field_size=8, 584 | field_offset_bytes=None, 585 | field_alignment_bytes=None, 586 | ), 587 | ], 588 | ), 589 | Variant( 590 | variant_name="Reject", 591 | variant_size=16, 592 | fields=[ 593 | Field( 594 | field_name=".0", 595 | field_size=8, 596 | field_offset_bytes=None, 597 | field_alignment_bytes=None, 598 | ), 599 | Field( 600 | field_name=".1", 601 | field_size=8, 602 | field_offset_bytes=None, 603 | field_alignment_bytes=None, 604 | ), 605 | ], 606 | ), 607 | Variant(variant_name="Done", variant_size=0, fields=[]), 608 | ], 609 | ), 610 | ], 611 | ), 612 | ] 613 | 614 | 615 | @pytest.mark.parametrize("data_filename, data_parsed_expected", test_type_parse_data) 616 | def test_type_parse(data_filename, data_parsed_expected): 617 | data_filepath = Path("tests") / Path("data") / data_filename 618 | with open(data_filepath, "r") as type_sizes_file: 619 | result = parse(type_sizes_file) 620 | assert result == data_parsed_expected 621 | 622 | 623 | def test_parse_failure(): 624 | test_parse_failure_data = """print-type-size type: `std::marker::PhantomData<*mut ()>`: 0 bytes, alignment: 1 bytes 625 | print-type-size ty""" 626 | with io.StringIO(test_parse_failure_data) as type_sizes_file: 627 | with pytest.raises(ParseException) as err: 628 | parse(type_sizes_file) 629 | assert "Expected end of text" in err.value.explain() 630 | --------------------------------------------------------------------------------