From 62f2c7faf11dd7dce8f5b87a49dddfb457c48e67 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Thu, 29 Jan 2026 10:14:17 -0800 Subject: [PATCH 01/25] [Rust] Provide setters useful for creating NTR references Need to expose these setters, even if there usage is limited when dealing with NTR reference, something in the core might be unconditionally retrieving the width or alignment of the type without trying to resolve NTR. --- rust/src/types.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/rust/src/types.rs b/rust/src/types.rs index 4f6f82d2e..bb5b38837 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -123,6 +123,22 @@ impl TypeBuilder { self } + /// Set the width of the type. + /// + /// Typically only done for named type references, which will not have their width set otherwise. + pub fn set_width(&self, width: usize) -> &Self { + unsafe { BNTypeBuilderSetWidth(self.handle, width) } + self + } + + /// Set the alignment of the type. + /// + /// Typically only done for named type references, which will not have their alignment set otherwise. + pub fn set_alignment(&self, alignment: usize) -> &Self { + unsafe { BNTypeBuilderSetAlignment(self.handle, alignment) } + self + } + pub fn set_pointer_base(&self, base_type: PointerBaseType, base_offset: i64) -> &Self { unsafe { BNSetTypeBuilderPointerBase(self.handle, base_type, base_offset) } self @@ -541,6 +557,7 @@ impl Type { // TODO: We need to decide on a public type to represent type width. // TODO: The api uses both `u64` and `usize`, pick one or a new type! + /// The size of the type in bytes. pub fn width(&self) -> u64 { unsafe { BNGetTypeWidth(self.handle) } } From 62e22b398a26195a4fc8a865889079d85005a436 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Thu, 29 Jan 2026 11:29:13 -0800 Subject: [PATCH 02/25] [Rust] Improve API surrounding binary view type libraries --- rust/src/binary_view.rs | 53 +++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/rust/src/binary_view.rs b/rust/src/binary_view.rs index cdf90c07e..9f9faa5bc 100644 --- a/rust/src/binary_view.rs +++ b/rust/src/binary_view.rs @@ -2145,9 +2145,9 @@ pub trait BinaryViewExt: BinaryViewBase { NonNull::new(result).map(|h| unsafe { TypeLibrary::ref_from_raw(h) }) } - /// Should be called by custom py:py:class:`BinaryView` implementations - /// when they have successfully imported an object from a type library (eg a symbol's type). - /// Values recorded with this function will then be queryable via [BinaryViewExt::lookup_imported_object_library]. + /// Should be called by custom [`BinaryView`] implementations when they have successfully + /// imported an object from a type library (eg a symbol's type). Values recorded with this + /// function will then be queryable via [`BinaryViewExt::lookup_imported_object_library`]. /// /// * `lib` - Type Library containing the imported type /// * `name` - Name of the object in the type library @@ -2173,23 +2173,23 @@ pub trait BinaryViewExt: BinaryViewBase { QualifiedName::free_raw(raw_name); } - /// Recursively imports a type from the specified type library, or, if - /// no library was explicitly provided, the first type library associated with the current [BinaryView] - /// that provides the name requested. + /// Recursively imports a type from the specified type library, or, if no library was + /// explicitly provided, the first type library associated with the current [`BinaryView`] that + /// provides the name requested. /// - /// This may have the impact of loading other type libraries as dependencies on other type libraries are lazily resolved - /// when references to types provided by them are first encountered. + /// This may have the impact of loading other type libraries as dependencies on other type + /// libraries are lazily resolved when references to types provided by them are first encountered. /// - /// Note that the name actually inserted into the view may not match the name as it exists in the type library in - /// the event of a name conflict. To aid in this, the [Type] object returned is a `NamedTypeReference` to - /// the deconflicted name used. + /// Note that the name actually inserted into the view may not match the name as it exists in + /// the type library in the event of a name conflict. To aid in this, the [`Type`] object + /// returned is a `NamedTypeReference` to the deconflicted name used. fn import_type_library>( &self, name: T, - mut lib: Option, + lib: Option<&TypeLibrary>, ) -> Option> { let mut lib_ref = lib - .as_mut() + .as_ref() .map(|l| unsafe { l.as_raw() } as *mut _) .unwrap_or(std::ptr::null_mut()); let mut raw_name = QualifiedName::into_raw(name.into()); @@ -2200,22 +2200,23 @@ pub trait BinaryViewExt: BinaryViewBase { (!result.is_null()).then(|| unsafe { Type::ref_from_raw(result) }) } - /// Recursively imports an object from the specified type library, or, if - /// no library was explicitly provided, the first type library associated with the current [BinaryView] - /// that provides the name requested. + /// Recursively imports an object (function) from the specified type library, or, if no library was + /// explicitly provided, the first type library associated with the current [`BinaryView`] that + /// provides the name requested. /// - /// This may have the impact of loading other type libraries as dependencies on other type libraries are lazily resolved - /// when references to types provided by them are first encountered. + /// This may have the impact of loading other type libraries as dependencies on other type + /// libraries are lazily resolved when references to types provided by them are first encountered. /// - /// .. note:: If you are implementing a custom BinaryView and use this method to import object types, - /// you should then call [BinaryViewExt::record_imported_object_library] with the details of where the object is located. + /// NOTE: If you are implementing a custom [`BinaryView`] and use this method to import object types, + /// you should then call [BinaryViewExt::record_imported_object_library] with the details of + /// where the object is located. fn import_type_object>( &self, name: T, - mut lib: Option, + lib: Option<&TypeLibrary>, ) -> Option> { let mut lib_ref = lib - .as_mut() + .as_ref() .map(|l| unsafe { l.as_raw() } as *mut _) .unwrap_or(std::ptr::null_mut()); let mut raw_name = QualifiedName::into_raw(name.into()); @@ -2226,7 +2227,7 @@ pub trait BinaryViewExt: BinaryViewBase { (!result.is_null()).then(|| unsafe { Type::ref_from_raw(result) }) } - /// Recursively imports a type interface given its GUID. + /// Recursively imports a [`Type`] given its GUID from available type libraries. fn import_type_by_guid(&self, guid: &str) -> Option> { let guid = guid.to_cstr(); let result = @@ -2234,7 +2235,7 @@ pub trait BinaryViewExt: BinaryViewBase { (!result.is_null()).then(|| unsafe { Type::ref_from_raw(result) }) } - /// Recursively exports `type_obj` into `lib` as a type with name `name` + /// Recursively exports `type_obj` into `lib` as a type with name `name`. /// /// As other referenced types are encountered, they are either copied into the destination type library or /// else the type library that provided the referenced type is added as a dependency for the destination library. @@ -2256,10 +2257,10 @@ pub trait BinaryViewExt: BinaryViewBase { QualifiedName::free_raw(raw_name); } - /// Recursively exports `type_obj` into `lib` as a type with name `name` + /// Recursively exports `type_obj` into `lib` as a type with name `name`. /// /// As other referenced types are encountered, they are either copied into the destination type library or - /// else the type library that provided the referenced type is added as a dependency for the destination library. + /// else the type library that provided the referenced type is added as a dependency for the destination library. fn export_object_to_library>( &self, lib: &TypeLibrary, From 8c7f0bc9093e400fa02a39bd030a3d43ad090730 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Thu, 29 Jan 2026 11:40:57 -0800 Subject: [PATCH 03/25] [Rust] Pass `type_reference` by ref to `TypeBuilder::named_type` --- rust/src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/src/types.rs b/rust/src/types.rs index bb5b38837..2528237fb 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -407,7 +407,7 @@ impl TypeBuilder { } /// Create a named type reference [`TypeBuilder`]. Analogous to [`Type::named_type`]. - pub fn named_type(type_reference: NamedTypeReference) -> Self { + pub fn named_type(type_reference: &NamedTypeReference) -> Self { let mut is_const = Conf::new(false, MIN_CONFIDENCE).into(); let mut is_volatile = Conf::new(false, MIN_CONFIDENCE).into(); unsafe { From 21796e599eb7fb9a07bbe6f02731fa32fdf4e8ea Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Thu, 5 Feb 2026 12:03:15 -0800 Subject: [PATCH 04/25] [Rust] Add `BinaryViewExt::type_libraries` --- rust/src/binary_view.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rust/src/binary_view.rs b/rust/src/binary_view.rs index 9f9faa5bc..478b82650 100644 --- a/rust/src/binary_view.rs +++ b/rust/src/binary_view.rs @@ -2134,6 +2134,12 @@ pub trait BinaryViewExt: BinaryViewBase { unsafe { TypeContainer::from_raw(type_container_ptr.unwrap()) } } + fn type_libraries(&self) -> Array { + let mut count = 0; + let result = unsafe { BNGetBinaryViewTypeLibraries(self.as_ref().handle, &mut count) }; + unsafe { Array::new(result, count, ()) } + } + /// Make the contents of a type library available for type/import resolution fn add_type_library(&self, library: &TypeLibrary) { unsafe { BNAddBinaryViewTypeLibrary(self.as_ref().handle, library.as_raw()) } From 10eb2df31b4c29b8ee653caba6b0e64be573fc72 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Thu, 5 Feb 2026 12:03:40 -0800 Subject: [PATCH 05/25] [Rust] Impl `Debug` for `BinaryViewType` --- rust/src/custom_binary_view.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/rust/src/custom_binary_view.rs b/rust/src/custom_binary_view.rs index 13a124e9d..4cd499314 100644 --- a/rust/src/custom_binary_view.rs +++ b/rust/src/custom_binary_view.rs @@ -18,6 +18,7 @@ use binaryninjacore_sys::*; pub use binaryninjacore_sys::BNModificationStatus as ModificationStatus; +use std::fmt::Debug; use std::marker::PhantomData; use std::mem::MaybeUninit; use std::os::raw::c_void; @@ -381,6 +382,15 @@ impl BinaryViewTypeBase for BinaryViewType { } } +impl Debug for BinaryViewType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BinaryViewType") + .field("name", &self.name()) + .field("long_name", &self.long_name()) + .finish() + } +} + impl CoreArrayProvider for BinaryViewType { type Raw = *mut BNBinaryViewType; type Context = (); From a86862e1b9677fd1152578c3cbb6c73ed7d06d46 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Thu, 5 Feb 2026 12:03:59 -0800 Subject: [PATCH 06/25] [Rust] Add `Symbol::ordinal` --- rust/src/symbol.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/rust/src/symbol.rs b/rust/src/symbol.rs index 4239dd4d7..6c368f7c5 100644 --- a/rust/src/symbol.rs +++ b/rust/src/symbol.rs @@ -267,6 +267,16 @@ impl Symbol { unsafe { BNGetSymbolAddress(self.handle) } } + /// Get the symbols ordinal, this will return `None` if the symbol ordinal is `0`. + pub fn ordinal(&self) -> Option { + let ordinal = unsafe { BNGetSymbolOrdinal(self.handle) }; + if ordinal == u64::MIN { + None + } else { + Some(ordinal) + } + } + pub fn auto_defined(&self) -> bool { unsafe { BNIsSymbolAutoDefined(self.handle) } } From 205f25b8ff8a12824de58fd64ccea2035f7c4dbd Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Thu, 5 Feb 2026 12:04:29 -0800 Subject: [PATCH 07/25] [Rust] Fix UB when passing include directories to `CoreTypeParser` --- rust/src/types/parser.rs | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/rust/src/types/parser.rs b/rust/src/types/parser.rs index 1ee071882..18fe363ff 100644 --- a/rust/src/types/parser.rs +++ b/rust/src/types/parser.rs @@ -83,10 +83,18 @@ impl TypeParser for CoreTypeParser { platform: &Platform, existing_types: &TypeContainer, options: &[String], - include_dirs: &[String], + include_directories: &[String], ) -> Result> { let source_cstr = BnString::new(source); let file_name_cstr = BnString::new(file_name); + let options: Vec<_> = options.into_iter().map(|o| o.to_cstr()).collect(); + let options_raw: Vec<*const c_char> = options.iter().map(|o| o.as_ptr()).collect(); + let include_directories: Vec<_> = include_directories + .into_iter() + .map(|d| d.to_cstr()) + .collect(); + let include_directories_raw: Vec<*const c_char> = + include_directories.iter().map(|d| d.as_ptr()).collect(); let mut result = std::ptr::null_mut(); let mut errors = std::ptr::null_mut(); let mut error_count = 0; @@ -97,10 +105,10 @@ impl TypeParser for CoreTypeParser { file_name_cstr.as_ptr(), platform.handle, existing_types.handle.as_ptr(), - options.as_ptr() as *const *const c_char, - options.len(), - include_dirs.as_ptr() as *const *const c_char, - include_dirs.len(), + options_raw.as_ptr() as *const *const c_char, + options_raw.len(), + include_directories_raw.as_ptr() as *const *const c_char, + include_directories_raw.len(), &mut result, &mut errors, &mut error_count, @@ -123,11 +131,19 @@ impl TypeParser for CoreTypeParser { platform: &Platform, existing_types: &TypeContainer, options: &[String], - include_dirs: &[String], + include_directories: &[String], auto_type_source: &str, ) -> Result> { let source_cstr = BnString::new(source); let file_name_cstr = BnString::new(file_name); + let options: Vec<_> = options.into_iter().map(|o| o.to_cstr()).collect(); + let options_raw: Vec<*const c_char> = options.iter().map(|o| o.as_ptr()).collect(); + let include_directories: Vec<_> = include_directories + .into_iter() + .map(|d| d.to_cstr()) + .collect(); + let include_directories_raw: Vec<*const c_char> = + include_directories.iter().map(|d| d.as_ptr()).collect(); let auto_type_source = BnString::new(auto_type_source); let mut raw_result = BNTypeParserResult::default(); let mut errors = std::ptr::null_mut(); @@ -139,10 +155,10 @@ impl TypeParser for CoreTypeParser { file_name_cstr.as_ptr(), platform.handle, existing_types.handle.as_ptr(), - options.as_ptr() as *const *const c_char, - options.len(), - include_dirs.as_ptr() as *const *const c_char, - include_dirs.len(), + options_raw.as_ptr() as *const *const c_char, + options_raw.len(), + include_directories_raw.as_ptr() as *const *const c_char, + include_directories_raw.len(), auto_type_source.as_ptr(), &mut raw_result, &mut errors, From 6888eb22bfb49d53b89916afb940ab6f0e7e80d4 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Thu, 5 Feb 2026 12:04:45 -0800 Subject: [PATCH 08/25] [Rust] Impl `Send` and `Sync` for `TypeLibrary` --- rust/src/types/library.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rust/src/types/library.rs b/rust/src/types/library.rs index 89f5e48f4..d1a027403 100644 --- a/rust/src/types/library.rs +++ b/rust/src/types/library.rs @@ -363,3 +363,6 @@ unsafe impl CoreArrayProviderInner for TypeLibrary { Guard::new(Self::from_raw(NonNull::new(*raw).unwrap()), context) } } + +unsafe impl Send for TypeLibrary {} +unsafe impl Sync for TypeLibrary {} From dc3decec6569aa9389617f485562bfe7a226bfee Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Fri, 6 Feb 2026 14:19:09 -0800 Subject: [PATCH 09/25] [Rust] Fix plugins being referenced in `cargo about` output --- arch/msp430/Cargo.toml | 1 + arch/riscv/Cargo.toml | 1 + plugins/dwarf/dwarf_export/Cargo.toml | 1 + plugins/dwarf/dwarf_import/Cargo.toml | 1 + plugins/dwarf/dwarfdump/Cargo.toml | 1 + plugins/dwarf/shared/Cargo.toml | 1 + plugins/idb_import/Cargo.toml | 1 + plugins/pdb-ng/Cargo.toml | 1 + plugins/svd/Cargo.toml | 1 + plugins/warp/Cargo.toml | 1 + plugins/workflow_objc/Cargo.toml | 1 + rust/plugin_examples/data_renderer/Cargo.toml | 1 + view/minidump/Cargo.toml | 1 + 13 files changed, 13 insertions(+) diff --git a/arch/msp430/Cargo.toml b/arch/msp430/Cargo.toml index d5bcc31ee..9931ff330 100644 --- a/arch/msp430/Cargo.toml +++ b/arch/msp430/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" authors = ["jrozner"] edition = "2021" license = "Apache-2.0" +publish = false [dependencies] binaryninja.workspace = true diff --git a/arch/riscv/Cargo.toml b/arch/riscv/Cargo.toml index d5ab661e5..ff9643df8 100644 --- a/arch/riscv/Cargo.toml +++ b/arch/riscv/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" authors = ["Ryan Snyder "] edition = "2021" license = "Apache-2.0" +publish = false [dependencies] binaryninja.workspace = true diff --git a/plugins/dwarf/dwarf_export/Cargo.toml b/plugins/dwarf/dwarf_export/Cargo.toml index 74cd774ee..2abcecd4e 100644 --- a/plugins/dwarf/dwarf_export/Cargo.toml +++ b/plugins/dwarf/dwarf_export/Cargo.toml @@ -3,6 +3,7 @@ name = "dwarf_export" version = "0.1.0" edition = "2021" license = "Apache-2.0" +publish = false [lib] crate-type = ["cdylib"] diff --git a/plugins/dwarf/dwarf_import/Cargo.toml b/plugins/dwarf/dwarf_import/Cargo.toml index d01c60b15..be06fbc28 100644 --- a/plugins/dwarf/dwarf_import/Cargo.toml +++ b/plugins/dwarf/dwarf_import/Cargo.toml @@ -3,6 +3,7 @@ name = "dwarf_import" version = "0.1.0" edition = "2021" license = "Apache-2.0" +publish = false [lib] crate-type = ["cdylib"] diff --git a/plugins/dwarf/dwarfdump/Cargo.toml b/plugins/dwarf/dwarfdump/Cargo.toml index cd13206a1..7af0fa38a 100644 --- a/plugins/dwarf/dwarfdump/Cargo.toml +++ b/plugins/dwarf/dwarfdump/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" authors = ["Kyle Martin "] edition = "2021" license = "Apache-2.0" +publish = false [lib] crate-type = ["cdylib"] diff --git a/plugins/dwarf/shared/Cargo.toml b/plugins/dwarf/shared/Cargo.toml index 19cb963e1..e1404c74d 100644 --- a/plugins/dwarf/shared/Cargo.toml +++ b/plugins/dwarf/shared/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" authors = ["Kyle Martin "] edition = "2021" license = "Apache-2.0" +publish = false [dependencies] binaryninja.workspace = true diff --git a/plugins/idb_import/Cargo.toml b/plugins/idb_import/Cargo.toml index 9ba241d26..ad2d2f6a9 100644 --- a/plugins/idb_import/Cargo.toml +++ b/plugins/idb_import/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" authors = ["Rubens Brandao "] edition = "2021" license = "Apache-2.0" +publish = false [lib] crate-type = ["cdylib"] diff --git a/plugins/pdb-ng/Cargo.toml b/plugins/pdb-ng/Cargo.toml index 08776df22..44cf5f03c 100644 --- a/plugins/pdb-ng/Cargo.toml +++ b/plugins/pdb-ng/Cargo.toml @@ -3,6 +3,7 @@ name = "pdb-import-plugin" version = "0.1.0" edition = "2021" license = "Apache-2.0" +publish = false [lib] crate-type = ["cdylib"] diff --git a/plugins/svd/Cargo.toml b/plugins/svd/Cargo.toml index e4cdc5d88..eafc87074 100644 --- a/plugins/svd/Cargo.toml +++ b/plugins/svd/Cargo.toml @@ -3,6 +3,7 @@ name = "svd_ninja" version = "0.1.0" edition = "2021" license = "Apache-2.0" +publish = false [lib] crate-type = ["cdylib", "lib"] diff --git a/plugins/warp/Cargo.toml b/plugins/warp/Cargo.toml index 3a623e0ab..13516c50c 100644 --- a/plugins/warp/Cargo.toml +++ b/plugins/warp/Cargo.toml @@ -3,6 +3,7 @@ name = "warp_ninja" version = "0.1.0" edition = "2021" license = "Apache-2.0" +publish = false [lib] crate-type = ["lib", "cdylib"] diff --git a/plugins/workflow_objc/Cargo.toml b/plugins/workflow_objc/Cargo.toml index 948fc2de2..07ef9bbef 100644 --- a/plugins/workflow_objc/Cargo.toml +++ b/plugins/workflow_objc/Cargo.toml @@ -3,6 +3,7 @@ name = "workflow_objc" version = "0.1.0" edition = "2021" license = "BSD-3-Clause" +publish = false [lib] crate-type = ["staticlib", "cdylib"] diff --git a/rust/plugin_examples/data_renderer/Cargo.toml b/rust/plugin_examples/data_renderer/Cargo.toml index 5911392fc..2887b738b 100644 --- a/rust/plugin_examples/data_renderer/Cargo.toml +++ b/rust/plugin_examples/data_renderer/Cargo.toml @@ -2,6 +2,7 @@ name = "example_data_renderer" version = "0.1.0" edition = "2021" +publish = false [lib] crate-type = ["cdylib"] diff --git a/view/minidump/Cargo.toml b/view/minidump/Cargo.toml index a39e9172a..adca85800 100644 --- a/view/minidump/Cargo.toml +++ b/view/minidump/Cargo.toml @@ -3,6 +3,7 @@ name = "minidump_bn" version = "0.1.0" edition = "2021" license = "Apache-2.0" +publish = false [lib] crate-type = ["cdylib"] From b3de826d830f68991c19bf7ab68aaaa5dc7b9c70 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Fri, 6 Feb 2026 14:19:49 -0800 Subject: [PATCH 10/25] [Rust] Fix rust version in `binaryninja` crate being outdated Does not really effect anything but just saw it was still referencing old version. --- rust/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 17cb2f0dc..2a03e5acf 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -3,7 +3,7 @@ name = "binaryninja" version = "0.1.0" authors = ["Ryan Snyder ", "Kyle Martin "] edition = "2021" -rust-version = "1.83.0" +rust-version = "1.91.1" license = "Apache-2.0" [features] From 6e6df28f4720abf7f8f13101983580a8f30b0a35 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Fri, 6 Feb 2026 14:20:08 -0800 Subject: [PATCH 11/25] [Rust] Misc documentation improvements --- rust/README.md | 47 +++++++++++++++++++--------------- rust/plugin_examples/README.md | 10 ++++++++ 2 files changed, 37 insertions(+), 20 deletions(-) create mode 100644 rust/plugin_examples/README.md diff --git a/rust/README.md b/rust/README.md index 60c4847c2..55dccdff6 100644 --- a/rust/README.md +++ b/rust/README.md @@ -45,7 +45,7 @@ More examples can be found in [here](https://github.com/Vector35/binaryninja-api ### Requirements -- Having BinaryNinja installed (and your license registered) +- Having [Binary Ninja] installed (and your license registered) - For headless operation you must have a headless supporting license. - Clang - Rust @@ -65,7 +65,7 @@ binaryninjacore-sys = { git = "https://github.com/Vector35/binaryninja-api.git", ``` `build.rs`: -```doctestinjectablerust +```rust fn main() { let link_path = std::env::var_os("DEP_BINARYNINJACORE_PATH").expect("DEP_BINARYNINJACORE_PATH not specified"); @@ -112,14 +112,27 @@ pub extern "C" fn CorePluginInit() -> bool { } ``` -Examples for writing a plugin can be found [here](https://github.com/Vector35/binaryninja-api/tree/dev/plugins). +Examples for writing a plugin can be found [here](https://github.com/Vector35/binaryninja-api/tree/dev/rust/plugin_examples) and [here](https://github.com/Vector35/binaryninja-api/tree/dev/plugins). + +#### Sending Logs + +To send logs from your plugin to Binary Ninja, you can use the [tracing](https://docs.rs/tracing/latest/tracing/) crate. +At the beginning of your plugin's initialization routine, register the tracing subscriber with [`crate::tracing_init!`], +for more details see the documentation of that macro. + +#### Plugin Compatibility + +A built plugin can only be loaded into a compatible Binary Ninja version, this is determined by the ABI version of the +plugin. The ABI version is located at the top of the `binaryninjacore.h` header file, and as such plugins should pin +their binary ninja dependency to a specific tag or commit hash. See the cargo documentation [here](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#choice-of-commit) +for more details. ### Write a Standalone Executable If you have a headless supporting license, you are able to use Binary Ninja as a regular dynamically loaded library. -Standalone executables must initialize the core themselves. `binaryninja::headless::init()` to initialize the core, and -`binaryninja::headless::shutdown()` to shutdown the core. Prefer using `binaryninja::headless::Session` as it will +Standalone executables must initialize the core themselves. [`crate::headless::init()`] to initialize the core, and +[`crate::headless::shutdown()`] to shutdown the core. Prefer using [`crate::headless::Session`] as it will shut down for you once it is dropped. `main.rs`: @@ -131,6 +144,12 @@ fn main() { } ``` +#### Capturing Logs + +To capture logs from Binary Ninja, you can use the [tracing](https://docs.rs/tracing/latest/tracing/) crate. Before initializing +the core but after registering your tracing subscriber, register a [`crate::tracing::TracingLogListener`], for more details see +the documentation for that type. + ## Offline Documentation Offline documentation can be generated like any other rust crate, using `cargo doc`. @@ -161,18 +180,6 @@ it will likely confuse someone else, and you should make an issue or ask for gui #### Attribution -This project makes use of: - - [log] ([log license] - MIT) - - [rayon] ([rayon license] - MIT) - - [thiserror] ([thiserror license] - MIT) - - [serde_json] ([serde_json license] - MIT) - -[log]: https://github.com/rust-lang/log -[log license]: https://github.com/rust-lang/log/blob/master/LICENSE-MIT -[rayon]: https://github.com/rayon-rs/rayon -[rayon license]: https://github.com/rayon-rs/rayon/blob/master/LICENSE-MIT -[thiserror]: https://github.com/dtolnay/thiserror -[thiserror license]: https://github.com/dtolnay/thiserror/blob/master/LICENSE-MIT -[serde_json]: https://github.com/serde-rs/json -[serde_json license]: https://github.com/serde-rs/json/blob/master/LICENSE-MIT -[Binary Ninja]: https://binary.ninja \ No newline at end of file +For attribution, please refer to the [Rust Licenses section](https://docs.binary.ninja/about/open-source.html#rust-licenses) of the user documentation. + +[Binary Ninja]: https://binary.ninja/ \ No newline at end of file diff --git a/rust/plugin_examples/README.md b/rust/plugin_examples/README.md new file mode 100644 index 000000000..1f49df99b --- /dev/null +++ b/rust/plugin_examples/README.md @@ -0,0 +1,10 @@ +# Plugin Examples + +These are examples of plugins that can be used to extend Binary Ninja's functionality. Each directory contains a crate +that when built produces a shared library that can then be placed into the `plugins` directory of your Binary Ninja +installation. + +For more information on installing plugins, refer to the user documentation [here](https://docs.binary.ninja/guide/plugins.html#using-plugins). + +For more examples of plugins, see the [plugins directory](https://github.com/Vector35/binaryninja-api/tree/dev/plugins) +which contains a number of plugins bundled with Binary Ninja. \ No newline at end of file From 9eaa93726309604d8e7c99cf35f100363a8e4207 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Mon, 9 Feb 2026 17:00:22 -0800 Subject: [PATCH 12/25] [Rust] Fix unbalanced ref returned in `RemoteFile::core_file` --- rust/src/collaboration/file.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/src/collaboration/file.rs b/rust/src/collaboration/file.rs index 993e6bc9c..46e27ea5b 100644 --- a/rust/src/collaboration/file.rs +++ b/rust/src/collaboration/file.rs @@ -57,10 +57,10 @@ impl RemoteFile { RemoteFile::get_for_local_database(&database) } - pub fn core_file(&self) -> Result { + pub fn core_file(&self) -> Result, ()> { let result = unsafe { BNRemoteFileGetCoreFile(self.handle.as_ptr()) }; NonNull::new(result) - .map(|handle| unsafe { ProjectFile::from_raw(handle) }) + .map(|handle| unsafe { ProjectFile::ref_from_raw(handle) }) .ok_or(()) } From a0961225411ba9478b8377ed8aa940b29324076f Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Mon, 9 Feb 2026 17:00:52 -0800 Subject: [PATCH 13/25] [Rust] Impl `From` for `QualifiedName` --- rust/src/qualified_name.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rust/src/qualified_name.rs b/rust/src/qualified_name.rs index 62da86b85..aed045a06 100644 --- a/rust/src/qualified_name.rs +++ b/rust/src/qualified_name.rs @@ -193,6 +193,15 @@ impl From for QualifiedName { } } +impl From for QualifiedName { + fn from(value: BnString) -> Self { + Self { + items: vec![value.to_string_lossy().to_string()], + separator: String::from("::"), + } + } +} + impl From<&str> for QualifiedName { fn from(value: &str) -> Self { Self::from(value.to_string()) From 5deabdebb7178e3bb2856a0e1b5a74bab6314ab2 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Mon, 9 Feb 2026 17:01:24 -0800 Subject: [PATCH 14/25] [Rust] Use `PathBuf` instead of `String` for include directories param in `TypeParser` --- rust/src/types/parser.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/rust/src/types/parser.rs b/rust/src/types/parser.rs index 18fe363ff..896327f66 100644 --- a/rust/src/types/parser.rs +++ b/rust/src/types/parser.rs @@ -2,6 +2,7 @@ use binaryninjacore_sys::*; use std::ffi::{c_char, c_void}; use std::fmt::Debug; +use std::path::PathBuf; use std::ptr::NonNull; use crate::platform::Platform; @@ -83,7 +84,7 @@ impl TypeParser for CoreTypeParser { platform: &Platform, existing_types: &TypeContainer, options: &[String], - include_directories: &[String], + include_directories: &[PathBuf], ) -> Result> { let source_cstr = BnString::new(source); let file_name_cstr = BnString::new(file_name); @@ -91,7 +92,7 @@ impl TypeParser for CoreTypeParser { let options_raw: Vec<*const c_char> = options.iter().map(|o| o.as_ptr()).collect(); let include_directories: Vec<_> = include_directories .into_iter() - .map(|d| d.to_cstr()) + .map(|d| d.clone().to_cstr()) .collect(); let include_directories_raw: Vec<*const c_char> = include_directories.iter().map(|d| d.as_ptr()).collect(); @@ -131,7 +132,7 @@ impl TypeParser for CoreTypeParser { platform: &Platform, existing_types: &TypeContainer, options: &[String], - include_directories: &[String], + include_directories: &[PathBuf], auto_type_source: &str, ) -> Result> { let source_cstr = BnString::new(source); @@ -140,7 +141,7 @@ impl TypeParser for CoreTypeParser { let options_raw: Vec<*const c_char> = options.iter().map(|o| o.as_ptr()).collect(); let include_directories: Vec<_> = include_directories .into_iter() - .map(|d| d.to_cstr()) + .map(|d| d.clone().to_cstr()) .collect(); let include_directories_raw: Vec<*const c_char> = include_directories.iter().map(|d| d.as_ptr()).collect(); @@ -238,7 +239,7 @@ pub trait TypeParser { platform: &Platform, existing_types: &TypeContainer, options: &[String], - include_dirs: &[String], + include_dirs: &[PathBuf], ) -> Result>; /// Parse an entire block of source into types, variables, and functions @@ -257,7 +258,7 @@ pub trait TypeParser { platform: &Platform, existing_types: &TypeContainer, options: &[String], - include_dirs: &[String], + include_dirs: &[PathBuf], auto_type_source: &str, ) -> Result>; @@ -556,7 +557,7 @@ unsafe extern "C" fn cb_preprocess_source( let includes_raw = unsafe { std::slice::from_raw_parts(include_dirs, include_dir_count) }; let includes: Vec<_> = includes_raw .iter() - .filter_map(|&r| raw_to_string(r)) + .filter_map(|&r| Some(PathBuf::from(raw_to_string(r)?))) .collect(); match ctxt.preprocess_source( &raw_to_string(source).unwrap(), @@ -616,7 +617,7 @@ unsafe extern "C" fn cb_parse_types_from_source( let includes_raw = unsafe { std::slice::from_raw_parts(include_dirs, include_dir_count) }; let includes: Vec<_> = includes_raw .iter() - .filter_map(|&r| raw_to_string(r)) + .filter_map(|&r| Some(PathBuf::from(raw_to_string(r)?))) .collect(); match ctxt.parse_types_from_source( &raw_to_string(source).unwrap(), From 07d2ee4dd1b6323a572a238656dfe1eb319477d5 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Wed, 11 Feb 2026 20:29:24 -0800 Subject: [PATCH 15/25] [Rust] Fix clippy lints --- rust/src/types/parser.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/rust/src/types/parser.rs b/rust/src/types/parser.rs index 896327f66..9bcdf41aa 100644 --- a/rust/src/types/parser.rs +++ b/rust/src/types/parser.rs @@ -88,10 +88,10 @@ impl TypeParser for CoreTypeParser { ) -> Result> { let source_cstr = BnString::new(source); let file_name_cstr = BnString::new(file_name); - let options: Vec<_> = options.into_iter().map(|o| o.to_cstr()).collect(); + let options: Vec<_> = options.iter().map(|o| o.to_cstr()).collect(); let options_raw: Vec<*const c_char> = options.iter().map(|o| o.as_ptr()).collect(); let include_directories: Vec<_> = include_directories - .into_iter() + .iter() .map(|d| d.clone().to_cstr()) .collect(); let include_directories_raw: Vec<*const c_char> = @@ -106,9 +106,9 @@ impl TypeParser for CoreTypeParser { file_name_cstr.as_ptr(), platform.handle, existing_types.handle.as_ptr(), - options_raw.as_ptr() as *const *const c_char, + options_raw.as_ptr(), options_raw.len(), - include_directories_raw.as_ptr() as *const *const c_char, + include_directories_raw.as_ptr(), include_directories_raw.len(), &mut result, &mut errors, @@ -137,10 +137,10 @@ impl TypeParser for CoreTypeParser { ) -> Result> { let source_cstr = BnString::new(source); let file_name_cstr = BnString::new(file_name); - let options: Vec<_> = options.into_iter().map(|o| o.to_cstr()).collect(); + let options: Vec<_> = options.iter().map(|o| o.to_cstr()).collect(); let options_raw: Vec<*const c_char> = options.iter().map(|o| o.as_ptr()).collect(); let include_directories: Vec<_> = include_directories - .into_iter() + .iter() .map(|d| d.clone().to_cstr()) .collect(); let include_directories_raw: Vec<*const c_char> = @@ -156,9 +156,9 @@ impl TypeParser for CoreTypeParser { file_name_cstr.as_ptr(), platform.handle, existing_types.handle.as_ptr(), - options_raw.as_ptr() as *const *const c_char, + options_raw.as_ptr(), options_raw.len(), - include_directories_raw.as_ptr() as *const *const c_char, + include_directories_raw.as_ptr(), include_directories_raw.len(), auto_type_source.as_ptr(), &mut raw_result, From 80b5b139ea47552012e8ee95bdd6548d1d7b5ca4 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Mon, 9 Feb 2026 17:01:38 -0800 Subject: [PATCH 16/25] [Rust] Impl `Debug` for `RemoteProject` --- rust/src/collaboration/project.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/rust/src/collaboration/project.rs b/rust/src/collaboration/project.rs index fa46e477c..5eff24b01 100644 --- a/rust/src/collaboration/project.rs +++ b/rust/src/collaboration/project.rs @@ -1,4 +1,5 @@ use std::ffi::c_void; +use std::fmt::Debug; use std::path::PathBuf; use std::ptr::NonNull; use std::time::SystemTime; @@ -870,11 +871,22 @@ impl RemoteProject { //} } +impl Debug for RemoteProject { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RemoteProject") + .field("id", &self.id()) + .field("name", &self.name()) + .field("description", &self.description()) + .finish() + } +} + impl PartialEq for RemoteProject { fn eq(&self, other: &Self) -> bool { self.id() == other.id() } } + impl Eq for RemoteProject {} impl ToOwned for RemoteProject { From 25d78c15b8e5b7112c2d9215764af8b48fe19735 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Mon, 9 Feb 2026 17:01:52 -0800 Subject: [PATCH 17/25] [Rust] Impl `Debug` for `RemoteFolder` --- rust/src/collaboration/folder.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/rust/src/collaboration/folder.rs b/rust/src/collaboration/folder.rs index 797bcdd4d..1b2fee5aa 100644 --- a/rust/src/collaboration/folder.rs +++ b/rust/src/collaboration/folder.rs @@ -1,3 +1,4 @@ +use std::fmt::Debug; use super::{Remote, RemoteProject}; use binaryninjacore_sys::*; use std::ptr::NonNull; @@ -125,11 +126,22 @@ impl RemoteFolder { } } +impl Debug for RemoteFolder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RemoteFolder") + .field("id", &self.id()) + .field("name", &self.name()) + .field("description", &self.description()) + .finish() + } +} + impl PartialEq for RemoteFolder { fn eq(&self, other: &Self) -> bool { self.id() == other.id() } } + impl Eq for RemoteFolder {} impl ToOwned for RemoteFolder { From a1bc1640fc8c494fa448b05dd46d93b8866434ef Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Wed, 11 Feb 2026 17:56:26 -0800 Subject: [PATCH 18/25] [Rust] Refactor `FileMetadata` file information - Rename and retype `FileMetadata::filename` and make the assignment required to happen at time of construction. - Add `FileMetadata::display_name` which is only to be used for presentation purposes. - Add `FileMetadata::virtual_path` for containers. - Rename `FileMetadata::modified` to `FileMetadata::is_modified` to be more consistent across codebase. - Add some missing documentation. - Add `BinaryView::from_metadata` with accompanying documentation. --- plugins/dwarf/dwarf_import/src/helpers.rs | 22 ++-- plugins/idb_import/src/lib.rs | 11 +- plugins/pdb-ng/src/lib.rs | 2 +- plugins/warp/src/cache.rs | 2 +- plugins/warp/src/plugin/create.rs | 6 +- rust/examples/decompile.rs | 2 +- rust/examples/disassemble.rs | 2 +- rust/examples/high_level_il.rs | 2 +- rust/examples/medium_level_il.rs | 2 +- rust/src/binary_view.rs | 19 +++- rust/src/file_metadata.rs | 128 ++++++++++++++++++++-- 11 files changed, 158 insertions(+), 40 deletions(-) diff --git a/plugins/dwarf/dwarf_import/src/helpers.rs b/plugins/dwarf/dwarf_import/src/helpers.rs index c7855b250..541fdc424 100644 --- a/plugins/dwarf/dwarf_import/src/helpers.rs +++ b/plugins/dwarf/dwarf_import/src/helpers.rs @@ -12,8 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::ffi::OsStr; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::{str::FromStr, sync::mpsc}; use crate::{DebugInfoBuilderContext, ReaderType}; @@ -590,10 +589,8 @@ pub(crate) fn find_sibling_debug_file(view: &BinaryView) -> Option { return None; } - let full_file_path = view.file().filename().to_string(); - - let debug_file = PathBuf::from(format!("{}.debug", full_file_path)); - let dsym_folder = PathBuf::from(format!("{}.dSYM", full_file_path)); + let debug_file = view.file().file_path().with_extension("debug"); + let dsym_folder = view.file().file_path().with_extension("dSYM"); // Find sibling debug file if debug_file.exists() && debug_file.is_file() { @@ -624,13 +621,12 @@ pub(crate) fn find_sibling_debug_file(view: &BinaryView) -> Option { // Look for dSYM // TODO: look for dSYM in project if dsym_folder.exists() && dsym_folder.is_dir() { - let filename = Path::new(&full_file_path) - .file_name() - .unwrap_or(OsStr::new("")); - - let dsym_file = dsym_folder.join("Contents/Resources/DWARF/").join(filename); // TODO: should this just pull any file out? Can there be multiple files? - if dsym_file.exists() { - return Some(dsym_file.to_string_lossy().to_string()); + if let Some(filename) = view.file().file_path().file_name() { + // TODO: should this just pull any file out? Can there be multiple files? + let dsym_file = dsym_folder.join("Contents/Resources/DWARF/").join(filename); + if dsym_file.exists() { + return Some(dsym_file.to_string_lossy().to_string()); + } } } diff --git a/plugins/idb_import/src/lib.rs b/plugins/idb_import/src/lib.rs index f285f554d..0137f198b 100644 --- a/plugins/idb_import/src/lib.rs +++ b/plugins/idb_import/src/lib.rs @@ -27,8 +27,10 @@ impl CustomDebugInfoParser for IDBDebugInfoParser { project_file.name().as_str().ends_with(".i64") || project_file.name().as_str().ends_with(".idb") } else { - view.file().filename().as_str().ends_with(".i64") - || view.file().filename().as_str().ends_with(".idb") + view.file() + .file_path() + .extension() + .map_or(false, |ext| ext == "i64" || ext == "idb") } } @@ -55,7 +57,10 @@ impl CustomDebugInfoParser for TILDebugInfoParser { if let Some(project_file) = view.file().project_file() { project_file.name().as_str().ends_with(".til") } else { - view.file().filename().as_str().ends_with(".til") + view.file() + .file_path() + .extension() + .map_or(false, |ext| ext == "til") } } diff --git a/plugins/pdb-ng/src/lib.rs b/plugins/pdb-ng/src/lib.rs index 86ae1cdd0..61ff54b29 100644 --- a/plugins/pdb-ng/src/lib.rs +++ b/plugins/pdb-ng/src/lib.rs @@ -617,7 +617,7 @@ impl CustomDebugInfoParser for PDBParser { } // Try in the same directory as the file - let mut potential_path = PathBuf::from(view.file().filename().to_string()); + let mut potential_path = view.file().file_path(); potential_path.pop(); potential_path.push(&info.file_name); if potential_path.exists() { diff --git a/plugins/warp/src/cache.rs b/plugins/warp/src/cache.rs index 41aab4234..3df2dbf8e 100644 --- a/plugins/warp/src/cache.rs +++ b/plugins/warp/src/cache.rs @@ -79,6 +79,6 @@ pub struct CacheDestructor; impl ObjectDestructor for CacheDestructor { fn destruct_view(&self, view: &BinaryView) { clear_type_ref_cache(view); - tracing::debug!("Removed WARP caches for {:?}", view.file().filename()); + tracing::debug!("Removed WARP caches for {}", view.file()); } } diff --git a/plugins/warp/src/plugin/create.rs b/plugins/warp/src/plugin/create.rs index 0c6a2fd5b..2a7488193 100644 --- a/plugins/warp/src/plugin/create.rs +++ b/plugins/warp/src/plugin/create.rs @@ -23,11 +23,7 @@ impl SaveFileField { let default_name = match file.project_file() { None => { // Not in a project, use the file name directly. - file.filename() - .split('/') - .last() - .unwrap_or("file") - .to_string() + file.display_name() } Some(project_file) => project_file.name(), }; diff --git a/rust/examples/decompile.rs b/rust/examples/decompile.rs index 6ba9cbf41..155de81cb 100644 --- a/rust/examples/decompile.rs +++ b/rust/examples/decompile.rs @@ -42,7 +42,7 @@ pub fn main() { .load(&filename) .expect("Couldn't open file!"); - tracing::info!("Filename: `{}`", bv.file().filename()); + tracing::info!("File: `{}`", bv.file()); tracing::info!("File size: `{:#x}`", bv.len()); tracing::info!("Function count: {}", bv.functions().len()); diff --git a/rust/examples/disassemble.rs b/rust/examples/disassemble.rs index ce3ff7656..df33aadec 100644 --- a/rust/examples/disassemble.rs +++ b/rust/examples/disassemble.rs @@ -39,7 +39,7 @@ pub fn main() { .load(&filename) .expect("Couldn't open file!"); - tracing::info!("Filename: `{}`", bv.file().filename()); + tracing::info!("File: `{}`", bv.file()); tracing::info!("File size: `{:#x}`", bv.len()); tracing::info!("Function count: {}", bv.functions().len()); diff --git a/rust/examples/high_level_il.rs b/rust/examples/high_level_il.rs index a099437a2..57b735e96 100644 --- a/rust/examples/high_level_il.rs +++ b/rust/examples/high_level_il.rs @@ -14,7 +14,7 @@ fn main() { .load("/bin/cat") .expect("Couldn't open `/bin/cat`"); - tracing::info!("Filename: `{}`", bv.file().filename()); + tracing::info!("File: `{}`", bv.file()); tracing::info!("File size: `{:#x}`", bv.len()); tracing::info!("Function count: {}", bv.functions().len()); diff --git a/rust/examples/medium_level_il.rs b/rust/examples/medium_level_il.rs index c9412d089..e2c0edee5 100644 --- a/rust/examples/medium_level_il.rs +++ b/rust/examples/medium_level_il.rs @@ -14,7 +14,7 @@ fn main() { .load("/bin/cat") .expect("Couldn't open `/bin/cat`"); - tracing::info!("Filename: `{}`", bv.file().filename()); + tracing::info!("File: `{}`", bv.file()); tracing::info!("File size: `{:#x}`", bv.len()); tracing::info!("Function count: {}", bv.functions().len()); diff --git a/rust/src/binary_view.rs b/rust/src/binary_view.rs index 478b82650..4e35dc8c1 100644 --- a/rust/src/binary_view.rs +++ b/rust/src/binary_view.rs @@ -2476,8 +2476,14 @@ impl BinaryView { Ref::new(Self { handle }) } - pub fn from_path(meta: &mut FileMetadata, file_path: impl AsRef) -> Result> { - let file = file_path.as_ref().to_cstr(); + /// Construct the raw binary view from the given metadata. Before calling this make sure you have + /// a valid file path set for the [`FileMetadata`]. It is required that the [`FileMetadata::file_path`] + /// exist on the local filesystem. + pub fn from_metadata(meta: &FileMetadata) -> Result> { + if !meta.file_path().exists() { + return Err(()); + } + let file = meta.file_path().to_cstr(); let handle = unsafe { BNCreateBinaryDataViewFromFilename(meta.handle, file.as_ptr() as *mut _) }; @@ -2488,6 +2494,15 @@ impl BinaryView { unsafe { Ok(Ref::new(Self { handle })) } } + /// Construct the raw binary view from the given `file_path` and metadata. + /// + /// This will implicitly set the metadata file path and then construct the view. If the metadata + /// already has the desired file path, use [`BinaryView::from_metadata`] instead. + pub fn from_path(meta: &FileMetadata, file_path: impl AsRef) -> Result> { + meta.set_file_path(file_path.as_ref()); + Self::from_metadata(meta) + } + pub fn from_accessor( meta: &FileMetadata, file: &mut FileAccessor, diff --git a/rust/src/file_metadata.rs b/rust/src/file_metadata.rs index 27ceb4af2..f64277500 100644 --- a/rust/src/file_metadata.rs +++ b/rust/src/file_metadata.rs @@ -20,7 +20,7 @@ use binaryninjacore_sys::*; use binaryninjacore_sys::{BNCreateDatabaseWithProgress, BNOpenExistingDatabaseWithProgress}; use std::ffi::c_void; use std::fmt::{Debug, Display, Formatter}; -use std::path::Path; +use std::path::{Path, PathBuf}; use crate::progress::ProgressCallback; use crate::project::file::ProjectFile; @@ -46,12 +46,15 @@ impl FileMetadata { Self::ref_from_raw(unsafe { BNCreateFileMetadata() }) } - pub fn with_filename(name: &str) -> Ref { + /// Build a [`FileMetadata`] with the given `path`, this is uncommon as you are likely to want to + /// open a [`BinaryView`] + pub fn with_file_path(path: &Path) -> Ref { let ret = FileMetadata::new(); - ret.set_filename(name); + ret.set_file_path(path); ret } + /// Closes the [`FileMetadata`] allowing any [`BinaryView`] parented to it to be freed. pub fn close(&self) { unsafe { BNCloseFile(self.handle); @@ -63,22 +66,124 @@ impl FileMetadata { SessionId(raw) } - pub fn filename(&self) -> String { + /// The path to the [`FileMetadata`] on disk. + /// + /// This will not point to the original file on disk, in the event that the file was saved + /// as a BNDB. When a BNDB is opened, the FileMetadata will contain the file path to the database. + /// + /// If you need the original binary file path, use [`FileMetadata::original_file_path`] instead. + /// + /// If you just want a name to present to the user, use [`FileMetadata::display_name`]. + pub fn file_path(&self) -> PathBuf { unsafe { let raw = BNGetFilename(self.handle); - BnString::into_string(raw) + PathBuf::from(BnString::into_string(raw)) } } - pub fn set_filename(&self, name: &str) { + // TODO: To prevent issues we will not allow users to set the file path as it really should be + // TODO: derived at construction and not modified later. + /// Set the files path on disk. + /// + /// This should always be a valid path. + pub(crate) fn set_file_path(&self, name: &Path) { let name = name.to_cstr(); - unsafe { BNSetFilename(self.handle, name.as_ptr()); } } - pub fn modified(&self) -> bool { + /// The display name of the file. Useful for presenting to the user. Can differ from the original + /// name of the file and can be overridden with [`FileMetadata::set_display_name`]. + pub fn display_name(&self) -> String { + let raw_name = unsafe { + let raw = BNGetDisplayName(self.handle); + BnString::into_string(raw) + }; + // Sometimes this display name may return a full path, which is not the intended purpose. + raw_name + .split('/') + .next_back() + .unwrap_or(&raw_name) + .to_string() + } + + /// Set the display name of the file. + /// + /// This can be anything and will not be used for any purpose other than presentation. + pub fn set_display_name(&self, name: &str) { + let name = name.to_cstr(); + unsafe { + BNSetDisplayName(self.handle, name.as_ptr()); + } + } + + /// The path to the original file on disk, if any. + /// + /// It may not be present if the BNDB was saved without it or cleared via [`FileMetadata::clear_original_file_path`]. + /// + /// Only prefer this over [`FileMetadata::file_path`] if you require the original binary location. + pub fn original_file_path(&self) -> Option { + let raw_name = unsafe { + let raw = BNGetOriginalFilename(self.handle); + PathBuf::from(BnString::into_string(raw)) + }; + // If the original file path is empty, or the original file path is pointing to the same file + // as the database itself, we know the original file path does not exist. + if raw_name.as_os_str().is_empty() + || self.is_database_backed() && raw_name == self.file_path() + { + None + } else { + Some(raw_name) + } + } + + /// Set the original file path inside the database. Useful if it has since been cleared from the + /// database, or you have moved the original file. + pub fn set_original_file_path(&self, path: &Path) { + let name = path.to_cstr(); + unsafe { + BNSetOriginalFilename(self.handle, name.as_ptr()); + } + } + + /// Clear the original file path inside the database. This is useful since the original file path + /// may be sensitive information you wish to not share with others. + pub fn clear_original_file_path(&self) { + unsafe { + BNSetOriginalFilename(self.handle, std::ptr::null()); + } + } + + /// The non-filesystem path that describes how this file was derived from the container + /// transform system, detailing the sequence of transform steps and selection names. + /// + /// NOTE: Returns `None` if this [`FileMetadata`] was not processed by the transform system and + /// does not differ from that of the "physical" file path reported by [`FileMetadata::file_path`]. + pub fn virtual_path(&self) -> Option { + unsafe { + let raw = BNGetVirtualPath(self.handle); + let path = BnString::into_string(raw); + // For whatever reason the core may report there being a virtual path as the file path. + // In the case where that occurs, we wish not to report there being one to the user. + match path.is_empty() || path == self.file_path() { + true => None, + false => Some(path), + } + } + } + + /// Sets the non-filesystem path that describes how this file was derived from the container + /// transform system. + pub fn set_virtual_path(&self, path: &str) { + let path = path.to_cstr(); + unsafe { + BNSetVirtualPath(self.handle, path.as_ptr()); + } + } + + pub fn is_modified(&self) -> bool { unsafe { BNIsFileModified(self.handle) } } @@ -372,9 +477,10 @@ impl FileMetadata { impl Debug for FileMetadata { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("FileMetadata") - .field("filename", &self.filename()) + .field("file_path", &self.file_path()) + .field("display_name", &self.display_name()) .field("session_id", &self.session_id()) - .field("modified", &self.modified()) + .field("is_modified", &self.is_modified()) .field("is_analysis_changed", &self.is_analysis_changed()) .field("current_view_type", &self.current_view()) .field("current_offset", &self.current_offset()) @@ -385,7 +491,7 @@ impl Debug for FileMetadata { impl Display for FileMetadata { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.filename()) + f.write_str(&self.display_name()) } } From c506cfd6b52a299fdd9a93d8e913803e5b2e347a Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Wed, 11 Feb 2026 17:57:36 -0800 Subject: [PATCH 19/25] [Rust] Impl `Send` and `Sync` for `RemoteFile` --- rust/src/collaboration/file.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rust/src/collaboration/file.rs b/rust/src/collaboration/file.rs index 46e27ea5b..dbe2b8b9c 100644 --- a/rust/src/collaboration/file.rs +++ b/rust/src/collaboration/file.rs @@ -584,6 +584,7 @@ impl PartialEq for RemoteFile { self.id() == other.id() } } + impl Eq for RemoteFile {} impl ToOwned for RemoteFile { @@ -594,6 +595,9 @@ impl ToOwned for RemoteFile { } } +unsafe impl Send for RemoteFile {} +unsafe impl Sync for RemoteFile {} + unsafe impl RefCountable for RemoteFile { unsafe fn inc_ref(handle: &Self) -> Ref { Ref::new(Self { From 2c6cca5e3f48d6a8fb5b90454a3f7d73b9f59705 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Wed, 11 Feb 2026 17:57:47 -0800 Subject: [PATCH 20/25] [Rust] Impl `Send` and `Sync` for `RemoteFolder` --- rust/src/collaboration/folder.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rust/src/collaboration/folder.rs b/rust/src/collaboration/folder.rs index 1b2fee5aa..3ae84b642 100644 --- a/rust/src/collaboration/folder.rs +++ b/rust/src/collaboration/folder.rs @@ -1,6 +1,6 @@ -use std::fmt::Debug; use super::{Remote, RemoteProject}; use binaryninjacore_sys::*; +use std::fmt::Debug; use std::ptr::NonNull; use crate::project::folder::ProjectFolder; @@ -152,6 +152,9 @@ impl ToOwned for RemoteFolder { } } +unsafe impl Send for RemoteFolder {} +unsafe impl Sync for RemoteFolder {} + unsafe impl RefCountable for RemoteFolder { unsafe fn inc_ref(handle: &Self) -> Ref { Ref::new(Self { From b9fad85ebf249ce39632b1f2039606c1618a3b61 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Wed, 11 Feb 2026 17:57:54 -0800 Subject: [PATCH 21/25] [Rust] Impl `Send` and `Sync` for `RemoteProject` --- rust/src/collaboration/project.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rust/src/collaboration/project.rs b/rust/src/collaboration/project.rs index 5eff24b01..cfcc1db48 100644 --- a/rust/src/collaboration/project.rs +++ b/rust/src/collaboration/project.rs @@ -897,6 +897,9 @@ impl ToOwned for RemoteProject { } } +unsafe impl Send for RemoteProject {} +unsafe impl Sync for RemoteProject {} + unsafe impl RefCountable for RemoteProject { unsafe fn inc_ref(handle: &Self) -> Ref { Ref::new(Self { From fc2fc58fc84c63fc2b061cf8255e88b7584b41d8 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Wed, 11 Feb 2026 17:58:26 -0800 Subject: [PATCH 22/25] [Rust] Add a precondition check to make sure metadata has been pulled before pulling remote projects --- rust/src/collaboration/remote.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rust/src/collaboration/remote.rs b/rust/src/collaboration/remote.rs index c1ad66786..03c808bc2 100644 --- a/rust/src/collaboration/remote.rs +++ b/rust/src/collaboration/remote.rs @@ -312,6 +312,10 @@ impl Remote { &self, mut progress: F, ) -> Result<(), ()> { + if !self.has_loaded_metadata() { + self.load_metadata()?; + } + let success = unsafe { BNRemotePullProjects( self.handle.as_ptr(), From 03e83999989f761f7be9639bc66aed470fd6ebcb Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Wed, 11 Feb 2026 20:18:44 -0800 Subject: [PATCH 23/25] [Rust] Add `OwnedBackgroundTaskGuard` for finishing background task automatically --- rust/src/background_task.rs | 43 +++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/rust/src/background_task.rs b/rust/src/background_task.rs index 1e9933551..94cc33685 100644 --- a/rust/src/background_task.rs +++ b/rust/src/background_task.rs @@ -24,14 +24,40 @@ use crate::string::*; pub type Result = result::Result; +/// An RAII guard for [`BackgroundTask`] to finish the task when dropped. +pub struct OwnedBackgroundTaskGuard { + pub(crate) task: Ref, +} + +impl OwnedBackgroundTaskGuard { + pub fn cancel(&mut self) { + self.task.cancel(); + } + + pub fn is_cancelled(&self) -> bool { + self.task.is_cancelled() + } + + pub fn set_progress_text(&mut self, text: &str) { + self.task.set_progress_text(text); + } +} + +impl Drop for OwnedBackgroundTaskGuard { + fn drop(&mut self) { + self.task.finish(); + } +} + /// A [`BackgroundTask`] does not actually execute any code, only act as a handler, primarily to query /// the status of the task, and to cancel the task. /// -/// If you are looking to execute code in the background consider using rusts threading API, or if you -/// want the core to execute the task on a worker thread, use the [`crate::worker_thread`] API. +/// If you are looking to execute code in the background, consider using rusts threading API, or if you +/// want the core to execute the task on a worker thread, instead use the [`crate::worker_thread`] API. /// -/// NOTE: If you do not call [`BackgroundTask::finish`] or [`BackgroundTask::cancel`] the task will -/// persist even _after_ it has been dropped. +/// NOTE: If you do not call [`BackgroundTask::finish`] or [`BackgroundTask::cancel`], the task will +/// persist even _after_ it has been dropped, use [`OwnedBackgroundTaskGuard`] to ensure the task is +/// finished, see [`BackgroundTask::enter`] for usage. #[derive(PartialEq, Eq, Hash)] pub struct BackgroundTask { pub(crate) handle: *mut BNBackgroundTask, @@ -52,6 +78,15 @@ impl BackgroundTask { unsafe { Ref::new(Self { handle }) } } + /// Creates a [`OwnedBackgroundTaskGuard`] that is responsible for finishing the background task + /// once dropped. Because the status of a task does not dictate the underlying objects' lifetime, + /// this can be safely done without requiring exclusive ownership. + pub fn enter(&self) -> OwnedBackgroundTaskGuard { + OwnedBackgroundTaskGuard { + task: self.to_owned(), + } + } + pub fn can_cancel(&self) -> bool { unsafe { BNCanCancelBackgroundTask(self.handle) } } From 8fbc07687531321a861a201e2fdf20755c1da2d4 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Wed, 11 Feb 2026 20:29:40 -0800 Subject: [PATCH 24/25] [Rust] Update allowed licenses --- about.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/about.toml b/about.toml index 29f0772fd..a0954a66a 100644 --- a/about.toml +++ b/about.toml @@ -9,4 +9,5 @@ accepted = [ "LicenseRef-scancode-google-patent-license-fuchsia", "MPL-2.0", "LicenseRef-scancode-unknown-license-reference", + "BSD-2-Clause" ] \ No newline at end of file From 68fcc03dabe619c840fa032d357427b0ac291aa3 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Wed, 11 Feb 2026 18:04:07 -0800 Subject: [PATCH 25/25] Add BNTL utility plugin Allow users to easily create, diff, dump and validate type libraries Supports the following formats: - C header files (via core type parsers) - Binary files (collects exported and imported functions) - WinMD files (via `windows-metadata` crate) - Existing type library files (for easy fixups) - Apiset files (to resolve through forwarded windows dlls) Can be invoked as a regular plugin via UI commands or via CLI. Processing of type libraries inherently requires external linking, processing will automatically merge and deduplicate colliding type libraries so prefer to use inside a project or a directory and process all information (for a given platform) at once, rather than smaller invocations. --- Cargo.lock | 455 ++++++++- Cargo.toml | 2 + plugins/bntl_utils/CMakeLists.txt | 168 ++++ plugins/bntl_utils/Cargo.toml | 40 + plugins/bntl_utils/README.md | 5 + plugins/bntl_utils/build.rs | 48 + plugins/bntl_utils/cli/Cargo.toml | 15 + plugins/bntl_utils/cli/README.md | 61 ++ plugins/bntl_utils/cli/build.rs | 15 + plugins/bntl_utils/cli/src/create.rs | 74 ++ plugins/bntl_utils/cli/src/diff.rs | 37 + plugins/bntl_utils/cli/src/dump.rs | 26 + plugins/bntl_utils/cli/src/input.rs | 167 ++++ plugins/bntl_utils/cli/src/main.rs | 71 ++ plugins/bntl_utils/cli/src/validate.rs | 66 ++ plugins/bntl_utils/src/command.rs | 66 ++ plugins/bntl_utils/src/command/create.rs | 196 ++++ plugins/bntl_utils/src/command/diff.rs | 103 ++ plugins/bntl_utils/src/command/dump.rs | 52 + plugins/bntl_utils/src/command/validate.rs | 78 ++ plugins/bntl_utils/src/diff.rs | 83 ++ plugins/bntl_utils/src/dump.rs | 146 +++ plugins/bntl_utils/src/helper.rs | 45 + plugins/bntl_utils/src/lib.rs | 61 ++ plugins/bntl_utils/src/process.rs | 932 ++++++++++++++++++ plugins/bntl_utils/src/schema.rs | 58 ++ .../bntl_utils/src/templates/validate.html | 101 ++ plugins/bntl_utils/src/url.rs | 345 +++++++ plugins/bntl_utils/src/validate.rs | 341 +++++++ plugins/bntl_utils/src/winmd.rs | 594 +++++++++++ plugins/bntl_utils/src/winmd/info.rs | 430 ++++++++ plugins/bntl_utils/src/winmd/translate.rs | 654 ++++++++++++ 32 files changed, 5514 insertions(+), 21 deletions(-) create mode 100644 plugins/bntl_utils/CMakeLists.txt create mode 100644 plugins/bntl_utils/Cargo.toml create mode 100644 plugins/bntl_utils/README.md create mode 100644 plugins/bntl_utils/build.rs create mode 100644 plugins/bntl_utils/cli/Cargo.toml create mode 100644 plugins/bntl_utils/cli/README.md create mode 100644 plugins/bntl_utils/cli/build.rs create mode 100644 plugins/bntl_utils/cli/src/create.rs create mode 100644 plugins/bntl_utils/cli/src/diff.rs create mode 100644 plugins/bntl_utils/cli/src/dump.rs create mode 100644 plugins/bntl_utils/cli/src/input.rs create mode 100644 plugins/bntl_utils/cli/src/main.rs create mode 100644 plugins/bntl_utils/cli/src/validate.rs create mode 100644 plugins/bntl_utils/src/command.rs create mode 100644 plugins/bntl_utils/src/command/create.rs create mode 100644 plugins/bntl_utils/src/command/diff.rs create mode 100644 plugins/bntl_utils/src/command/dump.rs create mode 100644 plugins/bntl_utils/src/command/validate.rs create mode 100644 plugins/bntl_utils/src/diff.rs create mode 100644 plugins/bntl_utils/src/dump.rs create mode 100644 plugins/bntl_utils/src/helper.rs create mode 100644 plugins/bntl_utils/src/lib.rs create mode 100644 plugins/bntl_utils/src/process.rs create mode 100644 plugins/bntl_utils/src/schema.rs create mode 100644 plugins/bntl_utils/src/templates/validate.html create mode 100644 plugins/bntl_utils/src/url.rs create mode 100644 plugins/bntl_utils/src/validate.rs create mode 100644 plugins/bntl_utils/src/winmd.rs create mode 100644 plugins/bntl_utils/src/winmd/info.rs create mode 100644 plugins/bntl_utils/src/winmd/translate.rs diff --git a/Cargo.lock b/Cargo.lock index f9401a8a9..785c63443 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,7 +26,7 @@ dependencies = [ "cfg-if", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.8.26", ] [[package]] @@ -260,6 +260,43 @@ dependencies = [ "objc2", ] +[[package]] +name = "bntl_cli" +version = "0.1.0" +dependencies = [ + "binaryninja", + "binaryninjacore-sys", + "bntl_utils", + "clap", + "rayon", + "serde_json", + "thiserror 2.0.12", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "bntl_utils" +version = "0.1.0" +dependencies = [ + "binaryninja", + "binaryninjacore-sys", + "dashmap", + "minijinja", + "minijinja-embed", + "nt-apiset", + "serde", + "serde_json", + "similar", + "tempdir", + "thiserror 2.0.12", + "tracing", + "url", + "uuid", + "walkdir", + "windows-metadata", +] + [[package]] name = "bon" version = "3.6.4" @@ -407,9 +444,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.40" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" dependencies = [ "clap_builder", "clap_derive", @@ -417,9 +454,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.40" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" dependencies = [ "anstream", "anstyle", @@ -429,9 +466,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.40" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck", "proc-macro2", @@ -441,9 +478,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "clipboard-win" @@ -652,6 +689,15 @@ dependencies = [ "rayon", ] +[[package]] +name = "dataview" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daba87f72c730b508641c9fb6411fc9bba73939eed2cab611c399500511880d0" +dependencies = [ + "derive_pod", +] + [[package]] name = "debugid" version = "0.8.0" @@ -681,6 +727,12 @@ dependencies = [ "syn", ] +[[package]] +name = "derive_pod" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ea6706d74fca54e15f1d40b5cf7fe7f764aaec61352a9fcec58fe27e042fc8" + [[package]] name = "directories" version = "6.0.0" @@ -714,6 +766,17 @@ dependencies = [ "objc2", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dwarf_export" version = "0.1.0" @@ -885,6 +948,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -1073,6 +1145,87 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "idb-rs" version = "0.1.12" @@ -1105,6 +1258,27 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "image" version = "0.25.6" @@ -1269,6 +1443,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + [[package]] name = "lock_api" version = "0.4.13" @@ -1285,6 +1465,15 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.7.5" @@ -1394,6 +1583,12 @@ dependencies = [ "libc", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "nom" version = "7.1.3" @@ -1404,6 +1599,29 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nt-apiset" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ab316ee07638762db759975a633e8971c17a346ff2bed93321c5cb2600f024" +dependencies = [ + "bitflags 2.9.1", + "displaydoc", + "nt-string", + "pelite", + "zerocopy 0.6.6", +] + +[[package]] +name = "nt-string" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64f73b19d9405e886b53b9dee286e7fbb622a5276a7fd143c2d8e4dac3a0c6c" +dependencies = [ + "displaydoc", + "widestring", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1642,6 +1860,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "pelite" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88dccf4bd32294364aeb7bd55d749604450e9db54605887551f21baea7617685" +dependencies = [ + "dataview", + "libc", + "no-std-compat", + "pelite-macros", + "winapi", +] + +[[package]] +name = "pelite-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a7cf3f8ecebb0f4895f4892a8be0a0dc81b498f9d56735cb769dc31bf00815b" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1713,6 +1950,15 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1811,9 +2057,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -1821,9 +2067,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -2088,18 +2334,28 @@ checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -2286,6 +2542,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tempdir" version = "0.3.7" @@ -2400,6 +2667,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -2489,11 +2766,15 @@ version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", "parking_lot", + "regex-automata", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] @@ -2526,6 +2807,23 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -2534,13 +2832,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ "getrandom 0.3.3", "js-sys", - "serde", + "serde_core", "sha1_smol", "wasm-bindgen", ] @@ -2785,6 +3083,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi" version = "0.3.9" @@ -2822,6 +3126,11 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-metadata" +version = "0.59.0" +source = "git+https://github.com/microsoft/windows-rs?tag=72#bcc24b5c5fb3fe0a7d00559ceee824abc66e030b" + [[package]] name = "windows-sys" version = "0.59.0" @@ -3078,6 +3387,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + [[package]] name = "x11rb" version = "0.13.1" @@ -3095,13 +3410,57 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" +dependencies = [ + "byteorder", + "zerocopy-derive 0.6.6", +] + [[package]] name = "zerocopy" version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ - "zerocopy-derive", + "zerocopy-derive 0.8.26", +] + +[[package]] +name = "zerocopy-derive" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -3115,6 +3474,60 @@ dependencies = [ "syn", ] +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zstd" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index 7061c201b..ca260b9db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,8 @@ members = [ "plugins/warp/examples/headless", "plugins/workflow_objc", "plugins/workflow_objc/demo", + "plugins/bntl_utils", + "plugins/bntl_utils/cli", ] [workspace.dependencies] diff --git a/plugins/bntl_utils/CMakeLists.txt b/plugins/bntl_utils/CMakeLists.txt new file mode 100644 index 000000000..b1573e374 --- /dev/null +++ b/plugins/bntl_utils/CMakeLists.txt @@ -0,0 +1,168 @@ +cmake_minimum_required(VERSION 3.15 FATAL_ERROR) + +project(bntl_utils) + +if(NOT BN_API_BUILD_EXAMPLES AND NOT BN_INTERNAL_BUILD) + if(NOT BN_API_PATH) + # If we have not already defined the API source directory try and find it. + find_path( + BN_API_PATH + NAMES binaryninjaapi.h + # List of paths to search for the clone of the api + HINTS ../../.. ../../binaryninja/api/ binaryninjaapi binaryninja-api $ENV{BN_API_PATH} + REQUIRED + ) + endif() + set(CARGO_STABLE_VERSION 1.91.1) + add_subdirectory(${BN_API_PATH} binaryninjaapi) +endif() + +file(GLOB_RECURSE PLUGIN_SOURCES CONFIGURE_DEPENDS + ${PROJECT_SOURCE_DIR}/Cargo.toml + ${PROJECT_SOURCE_DIR}/src/*.rs) + +if(CMAKE_BUILD_TYPE MATCHES Debug) + if(DEMO) + set(TARGET_DIR ${PROJECT_BINARY_DIR}/target/dev-demo) + set(CARGO_OPTS --target-dir=${PROJECT_BINARY_DIR}/target --profile=dev-demo) + else() + set(TARGET_DIR ${PROJECT_BINARY_DIR}/target/debug) + set(CARGO_OPTS --target-dir=${PROJECT_BINARY_DIR}/target) + endif() +else() + if(DEMO) + set(TARGET_DIR ${PROJECT_BINARY_DIR}/target/release-demo) + set(CARGO_OPTS --target-dir=${PROJECT_BINARY_DIR}/target --profile=release-demo) + else() + set(TARGET_DIR ${PROJECT_BINARY_DIR}/target/release) + set(CARGO_OPTS --target-dir=${PROJECT_BINARY_DIR}/target --release) + endif() +endif() + +if(FORCE_COLORED_OUTPUT) + set(CARGO_OPTS ${CARGO_OPTS} --color always) +endif() + +if(DEMO) + set(CARGO_FEATURES --features demo --manifest-path ${PROJECT_SOURCE_DIR}/demo/Cargo.toml) + + set(OUTPUT_FILE_NAME ${CMAKE_STATIC_LIBRARY_PREFIX}${PROJECT_NAME}_static${CMAKE_STATIC_LIBRARY_SUFFIX}) + set(OUTPUT_PDB_NAME ${CMAKE_STATIC_LIBRARY_PREFIX}${PROJECT_NAME}.pdb) + set(OUTPUT_FILE_PATH ${CMAKE_BINARY_DIR}/${OUTPUT_FILE_NAME}) + set(OUTPUT_PDB_PATH ${CMAKE_BINARY_DIR}/${OUTPUT_PDB_NAME}) + + set(BINJA_LIB_DIR $) +else() + # NOTE: --no-default-features is set to disable building artifacts used for testing + # NOTE: the linker is looking in the target dir and linking on it apparently. + set(CARGO_FEATURES "--no-default-features") + + set(OUTPUT_FILE_NAME ${CMAKE_SHARED_LIBRARY_PREFIX}${PROJECT_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}) + set(OUTPUT_PDB_NAME ${CMAKE_SHARED_LIBRARY_PREFIX}${PROJECT_NAME}.pdb) + set(OUTPUT_FILE_PATH ${BN_CORE_PLUGIN_DIR}/${OUTPUT_FILE_NAME}) + set(OUTPUT_PDB_PATH ${BN_CORE_PLUGIN_DIR}/${OUTPUT_PDB_NAME}) + + set(BINJA_LIB_DIR ${BN_INSTALL_BIN_DIR}) +endif() + + +add_custom_target(${PROJECT_NAME} ALL DEPENDS ${OUTPUT_FILE_PATH}) +add_dependencies(${PROJECT_NAME} binaryninjaapi) +get_target_property(BN_API_SOURCE_DIR binaryninjaapi SOURCE_DIR) +list(APPEND CMAKE_MODULE_PATH "${BN_API_SOURCE_DIR}/cmake") +find_package(BinaryNinjaCore REQUIRED) + +set_property(TARGET ${PROJECT_NAME} PROPERTY OUTPUT_FILE_PATH ${OUTPUT_FILE_PATH}) + +# Add the whole api to the depends too +file(GLOB API_SOURCES CONFIGURE_DEPENDS + ${BN_API_SOURCE_DIR}/binaryninjacore.h + ${BN_API_SOURCE_DIR}/rust/src/*.rs + ${BN_API_SOURCE_DIR}/rust/binaryninjacore-sys/src/*.rs) + +find_program(RUSTUP_PATH rustup REQUIRED HINTS ~/.cargo/bin) +set(RUSTUP_COMMAND ${RUSTUP_PATH} run ${CARGO_STABLE_VERSION} cargo) + +if(APPLE) + if(UNIVERSAL) + if(CMAKE_BUILD_TYPE MATCHES Debug) + if(DEMO) + set(AARCH64_LIB_PATH ${PROJECT_BINARY_DIR}/target/aarch64-apple-darwin/dev-demo/${OUTPUT_FILE_NAME}) + set(X86_64_LIB_PATH ${PROJECT_BINARY_DIR}/target/x86_64-apple-darwin/dev-demo/${OUTPUT_FILE_NAME}) + else() + set(AARCH64_LIB_PATH ${PROJECT_BINARY_DIR}/target/aarch64-apple-darwin/debug/${OUTPUT_FILE_NAME}) + set(X86_64_LIB_PATH ${PROJECT_BINARY_DIR}/target/x86_64-apple-darwin/debug/${OUTPUT_FILE_NAME}) + endif() + else() + if(DEMO) + set(AARCH64_LIB_PATH ${PROJECT_BINARY_DIR}/target/aarch64-apple-darwin/release-demo/${OUTPUT_FILE_NAME}) + set(X86_64_LIB_PATH ${PROJECT_BINARY_DIR}/target/x86_64-apple-darwin/release-demo/${OUTPUT_FILE_NAME}) + else() + set(AARCH64_LIB_PATH ${PROJECT_BINARY_DIR}/target/aarch64-apple-darwin/release/${OUTPUT_FILE_NAME}) + set(X86_64_LIB_PATH ${PROJECT_BINARY_DIR}/target/x86_64-apple-darwin/release/${OUTPUT_FILE_NAME}) + endif() + endif() + + add_custom_command( + OUTPUT ${OUTPUT_FILE_PATH} + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} clean --target=aarch64-apple-darwin ${CARGO_OPTS} --package binaryninjacore-sys + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} clean --target=x86_64-apple-darwin ${CARGO_OPTS} --package binaryninjacore-sys + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} build --target=aarch64-apple-darwin ${CARGO_OPTS} ${CARGO_FEATURES} + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} build --target=x86_64-apple-darwin ${CARGO_OPTS} ${CARGO_FEATURES} + COMMAND lipo -create ${AARCH64_LIB_PATH} ${X86_64_LIB_PATH} -output ${OUTPUT_FILE_PATH} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${PLUGIN_SOURCES} ${API_SOURCES} + ) + else() + add_custom_command( + OUTPUT ${OUTPUT_FILE_PATH} + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} clean ${CARGO_OPTS} --package binaryninjacore-sys + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} build ${CARGO_OPTS} ${CARGO_FEATURES} + COMMAND ${CMAKE_COMMAND} -E copy ${TARGET_DIR}/${OUTPUT_FILE_NAME} ${OUTPUT_FILE_PATH} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${PLUGIN_SOURCES} ${API_SOURCES} + ) + endif() +elseif(WIN32) + if(DEMO) + add_custom_command( + OUTPUT ${OUTPUT_FILE_PATH} + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BINJA_LIB_DIR} ${RUSTUP_COMMAND} clean ${CARGO_OPTS} --package binaryninjacore-sys + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BINJA_LIB_DIR} ${RUSTUP_COMMAND} build ${CARGO_OPTS} ${CARGO_FEATURES} + COMMAND ${CMAKE_COMMAND} -E copy ${TARGET_DIR}/${OUTPUT_FILE_NAME} ${OUTPUT_FILE_PATH} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${PLUGIN_SOURCES} ${API_SOURCES} + ) + else() + add_custom_command( + OUTPUT ${OUTPUT_FILE_PATH} + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BINJA_LIB_DIR} ${RUSTUP_COMMAND} clean ${CARGO_OPTS} --package binaryninjacore-sys + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BINJA_LIB_DIR} ${RUSTUP_COMMAND} build ${CARGO_OPTS} ${CARGO_FEATURES} + COMMAND ${CMAKE_COMMAND} -E copy ${TARGET_DIR}/${OUTPUT_FILE_NAME} ${OUTPUT_FILE_PATH} + COMMAND ${CMAKE_COMMAND} -E copy ${TARGET_DIR}/${OUTPUT_PDB_NAME} ${OUTPUT_PDB_PATH} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${PLUGIN_SOURCES} ${API_SOURCES} + ) + endif() +else() + add_custom_command( + OUTPUT ${OUTPUT_FILE_PATH} + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BINJA_LIB_DIR} ${RUSTUP_COMMAND} clean ${CARGO_OPTS} --package binaryninjacore-sys + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BINJA_LIB_DIR} ${RUSTUP_COMMAND} build ${CARGO_OPTS} ${CARGO_FEATURES} + COMMAND ${CMAKE_COMMAND} -E copy ${TARGET_DIR}/${OUTPUT_FILE_NAME} ${OUTPUT_FILE_PATH} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${PLUGIN_SOURCES} ${API_SOURCES} + ) +endif() diff --git a/plugins/bntl_utils/Cargo.toml b/plugins/bntl_utils/Cargo.toml new file mode 100644 index 000000000..a3f0780fb --- /dev/null +++ b/plugins/bntl_utils/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "bntl_utils" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +publish = false + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +binaryninja.workspace = true +binaryninjacore-sys.workspace = true +tracing = "0.1" +thiserror = "2.0" +similar = "2.7.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tempdir = "0.3" +nt-apiset = "0.1.0" +url = "2.5" +uuid = "1.20" +walkdir = "2.5" +dashmap = "6.1" + +# For reports +minijinja = "2.10.2" +minijinja-embed = "2.10.2" + +[build-dependencies] +minijinja-embed = "2.10.2" + +# TODO: We need to depend on latest because the windows-metadata crate has not yet been bumped, but depending on the crate +# TODO: with git will mean we pull in all of the data of the crate instead of just the necessary bits, we likely need to +# TODO: wait until the windows-metadata crate is bumped before merging this PR. +# TODO: Relevant PR: https://github.com/microsoft/windows-rs/pull/3799 +# TODO: Relevant issue: https://github.com/microsoft/windows-rs/issues/3887 +[dependencies.windows-metadata] +git = "https://github.com/microsoft/windows-rs" +tag = "72" \ No newline at end of file diff --git a/plugins/bntl_utils/README.md b/plugins/bntl_utils/README.md new file mode 100644 index 000000000..2ad2ab4bc --- /dev/null +++ b/plugins/bntl_utils/README.md @@ -0,0 +1,5 @@ +# BNTL Utilities + +A plugin and CLI tool for processing Binary Ninja type libraries (BNTL). + +For CLI build instructions and usage see [here](./cli/README.md). \ No newline at end of file diff --git a/plugins/bntl_utils/build.rs b/plugins/bntl_utils/build.rs new file mode 100644 index 000000000..648b03715 --- /dev/null +++ b/plugins/bntl_utils/build.rs @@ -0,0 +1,48 @@ +use std::path::PathBuf; + +fn main() { + let link_path = std::env::var_os("DEP_BINARYNINJACORE_PATH") + .expect("DEP_BINARYNINJACORE_PATH not specified"); + + println!("cargo::rustc-link-lib=dylib=binaryninjacore"); + println!("cargo::rustc-link-search={}", link_path.to_str().unwrap()); + + #[cfg(target_os = "linux")] + { + println!( + "cargo::rustc-link-arg=-Wl,-rpath,{0},-L{0}", + link_path.to_string_lossy() + ); + } + + #[cfg(target_os = "macos")] + { + let crate_name = std::env::var("CARGO_PKG_NAME").expect("CARGO_PKG_NAME not set"); + let lib_name = crate_name.replace('-', "_"); + println!( + "cargo::rustc-link-arg=-Wl,-install_name,@rpath/lib{}.dylib", + lib_name + ); + } + + let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR specified"); + let out_dir_path = PathBuf::from(out_dir); + + // Copy all binaries to OUT_DIR for unit tests. + let bin_dir: PathBuf = "fixtures/".into(); + if let Ok(entries) = std::fs::read_dir(bin_dir) { + for entry in entries { + let entry = entry.unwrap(); + let path = entry.path(); + if path.is_file() { + let file_name = path.file_name().unwrap(); + let dest_path = out_dir_path.join(file_name); + std::fs::copy(&path, &dest_path).expect("failed to copy binary to OUT_DIR"); + } + } + } + + println!("cargo::rerun-if-changed=src/templates"); + // Templates used for rendering reports. + minijinja_embed::embed_templates!("src/templates"); +} diff --git a/plugins/bntl_utils/cli/Cargo.toml b/plugins/bntl_utils/cli/Cargo.toml new file mode 100644 index 000000000..863b950d3 --- /dev/null +++ b/plugins/bntl_utils/cli/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "bntl_cli" +version = "0.1.0" +edition = "2024" + +[dependencies] +binaryninja.workspace = true +binaryninjacore-sys.workspace = true +bntl_utils = { path = "../" } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +clap = { version = "4.5.58", features = ["derive"] } +rayon = "1.11" +serde_json = "1.0" +thiserror = "2.0" \ No newline at end of file diff --git a/plugins/bntl_utils/cli/README.md b/plugins/bntl_utils/cli/README.md new file mode 100644 index 000000000..5c4ddcb78 --- /dev/null +++ b/plugins/bntl_utils/cli/README.md @@ -0,0 +1,61 @@ +# Headless BNTL Processor + +Provides headless support for generating, inspecting, and validating Binary Ninja type libraries (BNTL). + +### Building + +> Assuming you have the following: +> - A compatible Binary Ninja with headless usage (see [this documentation](https://docs.binary.ninja/dev/batch.html#batch-processing-and-other-automation-tips) for more information) +> - Clang +> - Rust (currently tested for 1.91.1) +> - Set `BINARYNINJADIR` env variable to your installation directory (see [here](https://docs.binary.ninja/guide/#binary-path) for more details) + > - If this is not set, the -sys crate will try and locate using the default installation path and last run location. + +1. Clone this repository (`git clone https://github.com/Vector35/binaryninja-api/tree/dev`) +2. Build in release (`cargo build --release`) + +If compilation fails because it could not link against binaryninjacore than you should double-check you set `BINARYNINJADIR` correctly. + +Once it finishes you now will have a `bntl_cli` binary in `target/release` for use. + +### Usage + +> Assuming you already have the `bntl_cli` binary and a valid headless compatible Binary Ninja license. + +#### Create + +Generate a new type library from local files or remote projects. + +Examples: + +- `./bntl_cli create sqlite3.dll "windows-x86_64" ./headers/ ./output/` + - Places a single `sqlite.dll.bntl` file in the `output` directory, as headers have no dependency names associated they will be named `sqlite.dll`. +- `./bntl_cli create myproject "windows-x86_64" binaryninja://enterprise/https://enterprise.com/23ce5eaa-f532-4a93-80f2-a7d7f0aed040/ ./output/` + - Downloads and processes all files in the project, placing potentially multiple `.bntl` files in the `output` directory. +- `./bntl_cli create sqlite3.dll "windows-x86_64" ./winmd/ ./output/` + - `winmd` files are also supported as input, they will be processed together. You also probably want to provide some apiset schema files as well. + +#### Dump + +Export a type library back into a C header file for inspection. + +Examples: + +- `./bntl_cli dump sqlite3.dll.bntl ./output/sqlite.h` + +#### Diff + +Compare two type libraries and generate a .diff file containing a similarity ratio. + +Examples: + +- `./bntl_cli diff sqlite3.dll.bntl sqlite3.dll.bntl ./output/sqlite.diff` + +#### Validate + +Check type libraries for common errors, ensuring all referenced types exist across specified platforms. + +Examples: + +- `./bntl_cli validate ./typelibs/ ./output/` + - Pass in a directory containing `.bntl` files to validate, outputting a JSON file for each type library containing any errors. diff --git a/plugins/bntl_utils/cli/build.rs b/plugins/bntl_utils/cli/build.rs new file mode 100644 index 000000000..ed6cec7d2 --- /dev/null +++ b/plugins/bntl_utils/cli/build.rs @@ -0,0 +1,15 @@ +fn main() { + let link_path = std::env::var_os("DEP_BINARYNINJACORE_PATH") + .expect("DEP_BINARYNINJACORE_PATH not specified"); + + println!("cargo::rustc-link-lib=dylib=binaryninjacore"); + println!("cargo::rustc-link-search={}", link_path.to_str().unwrap()); + + #[cfg(not(target_os = "windows"))] + { + println!( + "cargo::rustc-link-arg=-Wl,-rpath,{0},-L{0}", + link_path.to_string_lossy() + ); + } +} diff --git a/plugins/bntl_utils/cli/src/create.rs b/plugins/bntl_utils/cli/src/create.rs new file mode 100644 index 000000000..1a997b5f6 --- /dev/null +++ b/plugins/bntl_utils/cli/src/create.rs @@ -0,0 +1,74 @@ +use crate::input::{Input, ResolvedInput}; +use binaryninja::platform::Platform; +use bntl_utils::process::TypeLibProcessor; +use clap::Args; +use std::path::PathBuf; + +#[derive(Debug, Args)] +pub struct CreateArgs { + /// The name of the type library to create. + /// + /// TODO: Note that this wont be used for inputs which provide a name + pub name: String, + /// TODO: Note that this wont be used for inputs which provide a platform + pub platform: String, + pub input: Input, + pub output_directory: Option, + #[clap(long)] + pub dry_run: bool, +} + +impl CreateArgs { + pub fn execute(&self) { + let Some(_platform) = Platform::by_name(&self.platform) else { + tracing::error!("Failed to find platform: {}", self.platform); + let platforms: Vec<_> = Platform::list_all().iter().map(|p| p.name()).collect(); + tracing::error!("Available platforms: {}", platforms.join(", ")); + panic!("Platform not found"); + }; + + let output_path = self + .output_directory + .clone() + .unwrap_or(PathBuf::from("./output/")); + if output_path.exists() && !output_path.is_dir() { + tracing::error!("Output path {} is not a directory", output_path.display()); + return; + } + std::fs::create_dir_all(&output_path).expect("Failed to create output directory"); + + let processor = TypeLibProcessor::new(&self.name, &self.platform); + // TODO: Need progress indicator here, when downloading files. + let resolved_input = self.input.resolve().expect("Failed to resolve input"); + + let data = match resolved_input { + ResolvedInput::Path(path) => processor.process(&path), + ResolvedInput::Project(project) => processor.process_project(&project), + ResolvedInput::ProjectFolder(project_folder) => { + processor.process_project_folder(&project_folder) + } + ResolvedInput::ProjectFile(project_file) => { + processor.process_project_file(&project_file) + } + } + .expect("Failed to process input"); + + if self.dry_run { + tracing::info!("Dry run enabled, skipping actual type library creation"); + return; + } + + for type_library in data.type_libraries { + let output_path = output_path.join(format!("{}.bntl", type_library.name())); + if type_library.write_to_file(&output_path) { + tracing::info!( + "Created type library '{}': {}", + type_library.name(), + output_path.display() + ); + } else { + tracing::error!("Failed to write type library to {}", output_path.display()); + } + } + } +} diff --git a/plugins/bntl_utils/cli/src/diff.rs b/plugins/bntl_utils/cli/src/diff.rs new file mode 100644 index 000000000..1eedb9701 --- /dev/null +++ b/plugins/bntl_utils/cli/src/diff.rs @@ -0,0 +1,37 @@ +use binaryninja::types::TypeLibrary; +use bntl_utils::diff::TILDiff; +use clap::Args; +use std::path::PathBuf; + +#[derive(Debug, Args)] +pub struct DiffArgs { + pub file_a: PathBuf, + pub file_b: PathBuf, + /// Path to write the `.diff` file to. + pub output_path: PathBuf, + /// Timeout in seconds for the diff operation to complete, if provided the diffing will begin + /// to approximate after the deadline has passed. + #[clap(long)] + pub timeout: Option, +} + +impl DiffArgs { + pub fn execute(&self) { + let type_lib_a = + TypeLibrary::load_from_file(&self.file_a).expect("Failed to load type library"); + let type_lib_b = + TypeLibrary::load_from_file(&self.file_b).expect("Failed to load type library"); + + let diff_result = + match TILDiff::new().diff((&self.file_a, &type_lib_a), (&self.file_b, &type_lib_b)) { + Ok(diff_result) => diff_result, + Err(err) => { + tracing::error!("Failed to diff type libraries: {}", err); + return; + } + }; + tracing::info!("Similarity Ratio: {}", diff_result.ratio); + std::fs::write(&self.output_path, diff_result.diff).unwrap(); + tracing::info!("Diff written to: {}", self.output_path.display()); + } +} diff --git a/plugins/bntl_utils/cli/src/dump.rs b/plugins/bntl_utils/cli/src/dump.rs new file mode 100644 index 000000000..fb25446b0 --- /dev/null +++ b/plugins/bntl_utils/cli/src/dump.rs @@ -0,0 +1,26 @@ +use binaryninja::types::TypeLibrary; +use bntl_utils::dump::TILDump; +use clap::Args; +use std::path::PathBuf; + +#[derive(Debug, Args)] +pub struct DumpArgs { + pub input: PathBuf, + pub output_path: Option, +} + +impl DumpArgs { + pub fn execute(&self) { + let type_lib = + TypeLibrary::load_from_file(&self.input).expect("Failed to load type library"); + let default_output_path = self.input.with_extension("h"); + let output_path = self.output_path.as_ref().unwrap_or(&default_output_path); + let dependencies = + bntl_utils::helper::path_to_type_libraries(&self.input.parent().unwrap()); + let printed_types = TILDump::new() + .with_type_libs(dependencies) + .dump(&type_lib) + .expect("Failed to dump type library"); + std::fs::write(output_path, printed_types).expect("Failed to write type library header"); + } +} diff --git a/plugins/bntl_utils/cli/src/input.rs b/plugins/bntl_utils/cli/src/input.rs new file mode 100644 index 000000000..1e1dbb6ea --- /dev/null +++ b/plugins/bntl_utils/cli/src/input.rs @@ -0,0 +1,167 @@ +use binaryninja::collaboration::RemoteFile; +use binaryninja::project::Project; +use binaryninja::project::file::ProjectFile; +use binaryninja::project::folder::ProjectFolder; +use binaryninja::rc::Ref; +use bntl_utils::url::{BnParsedUrl, BnResource}; +use std::fmt::Display; +use std::path::PathBuf; +use std::str::FromStr; +use thiserror::Error; + +#[derive(Debug)] +pub enum ResolvedInput { + Path(PathBuf), + Project(Ref), + ProjectFolder(Ref), + ProjectFile(Ref), +} + +#[derive(Error, Debug)] +pub enum InputResolveError { + #[error("Resource resolution failed: {0}")] + ResourceError(#[from] bntl_utils::url::BnResourceError), + + #[error("Collaboration API error: {0}")] + CollaborationError(String), + + #[error("Download failed for {url}: status {status}")] + DownloadFailed { url: String, status: u16 }, + + #[error("Download provider error: {0}")] + DownloadProviderError(String), + + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + + #[error("Environment error: {0}")] + EnvError(String), +} + +/// An input to the CLI to locate a "resource", such as a file or directory. +#[derive(Debug, Clone)] +pub enum Input { + /// A URL which references a Binary Ninja resource, such as a remote project or file. + ParsedUrl(BnParsedUrl), + /// A local filesystem path pointing to a file or directory. + LocalPath(PathBuf), +} + +impl Input { + /// Attempt to acquire a path from this input, this can download files over the network and + /// is meant to be called when the file contents are desired. + pub fn resolve(&self) -> Result { + let try_download_file = |file: &RemoteFile| -> Result<(), InputResolveError> { + if !file.core_file().unwrap().exists_on_disk() { + let _span = + tracing::info_span!("Downloading project file", file = %file.name()).entered(); + file.download().map_err(|_| { + InputResolveError::CollaborationError("Failed to download project file".into()) + })?; + } + Ok(()) + }; + + match self { + Input::ParsedUrl(url) => match url.to_resource()? { + BnResource::RemoteProject(project) => { + let files = project.files().map_err(|_| { + InputResolveError::CollaborationError("Failed to get files".into()) + })?; + + for file in &files { + try_download_file(&file)?; + } + + let core = project.core_project().map_err(|_| { + InputResolveError::CollaborationError("Missing core project".into()) + })?; + Ok(ResolvedInput::Project(core)) + } + + BnResource::RemoteProjectFile(file) => { + try_download_file(&file)?; + let core = file.core_file().expect("Missing core file"); + Ok(ResolvedInput::ProjectFile(core)) + } + + BnResource::RemoteProjectFolder(folder) => { + let project = folder.project().map_err(|_| { + InputResolveError::CollaborationError("Failed to get project".into()) + })?; + let files = project.files().map_err(|_| { + InputResolveError::CollaborationError("Failed to get files".into()) + })?; + + for file in &files { + if let Some(file_folder) = file.folder().ok().flatten() { + if file_folder == folder { + try_download_file(&file)?; + } + } + } + + let core = folder.core_folder().map_err(|_| { + InputResolveError::CollaborationError("Missing core folder".into()) + })?; + Ok(ResolvedInput::ProjectFolder(core)) + } + + BnResource::RemoteFile(url) => { + let safe_name = url.to_string().replace(['/', ':', '?'], "_"); + let cached_file_path = std::env::temp_dir().join(safe_name); + if cached_file_path.exists() { + return Ok(ResolvedInput::Path(cached_file_path)); + } + + let download_provider = binaryninja::download::DownloadProvider::try_default() + .expect("Failed to get default download provider"); + let mut instance = download_provider + .create_instance() + .expect("Failed to create download provider instance"); + let _span = + tracing::info_span!("Downloading remote file", url = %url).entered(); + let response = instance + .get(&url.to_string(), Vec::new()) + .map_err(|e| InputResolveError::DownloadProviderError(e.to_string()))?; + if response.is_success() { + std::fs::write(&cached_file_path, response.data)?; + Ok(ResolvedInput::Path(cached_file_path)) + } else { + Err(InputResolveError::DownloadFailed { + url: url.to_string(), + status: response.status_code, + }) + } + } + + BnResource::LocalFile(path) => Ok(ResolvedInput::Path(path.clone())), + }, + Input::LocalPath(path) => Ok(ResolvedInput::Path(path.clone())), + } + } +} + +impl FromStr for Input { + type Err = String; + + fn from_str(s: &str) -> Result { + // Try to parse as a Binary Ninja URL + if s.starts_with("binaryninja:") { + let url = BnParsedUrl::parse(s).map_err(|e| format!("URL Parse Error: {}", e))?; + return Ok(Input::ParsedUrl(url)); + } + + let path = PathBuf::from(s); + Ok(Input::LocalPath(path)) + } +} + +impl Display for Input { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Input::ParsedUrl(url) => write!(f, "{}", url), + Input::LocalPath(path) => write!(f, "{}", path.display()), + } + } +} diff --git a/plugins/bntl_utils/cli/src/main.rs b/plugins/bntl_utils/cli/src/main.rs new file mode 100644 index 000000000..1466da1fd --- /dev/null +++ b/plugins/bntl_utils/cli/src/main.rs @@ -0,0 +1,71 @@ +use clap::Parser; +use tracing::level_filters::LevelFilter; +use tracing_subscriber::EnvFilter; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; + +mod create; +mod diff; +mod dump; +mod input; +mod validate; + +/// Generate, inspect, and validate Binary Ninja type libraries (BNTL) +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Cli { + #[clap(subcommand)] + command: Command, +} + +#[derive(Parser, Debug)] +pub enum Command { + /// Create a new type library from a set of files. + Create(create::CreateArgs), + /// Dump the type library to a C header file. + Dump(dump::DumpArgs), + /// Generate a diff between two type libraries. + Diff(diff::DiffArgs), + /// Validate the type libraries for common errors. + Validate(validate::ValidateArgs), +} + +impl Command { + pub fn execute(&self) { + match self { + Command::Create(args) => { + args.execute(); + } + Command::Dump(args) => { + args.execute(); + } + Command::Diff(args) => { + args.execute(); + } + Command::Validate(args) => { + args.execute(); + } + } + } +} + +fn main() { + let cli = Cli::parse(); + tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer()) + .with( + EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(), + ) + .init(); + + // Capture logs from Binary Ninja + let _listener = binaryninja::tracing::TracingLogListener::new().register(); + + // Initialize Binary Ninja, requires a headless compatible license like commercial or ultimate. + let _session = binaryninja::headless::Session::new() + .expect("Failed to create headless binary ninja session"); + + cli.command.execute(); +} diff --git a/plugins/bntl_utils/cli/src/validate.rs b/plugins/bntl_utils/cli/src/validate.rs new file mode 100644 index 000000000..743ca927a --- /dev/null +++ b/plugins/bntl_utils/cli/src/validate.rs @@ -0,0 +1,66 @@ +use binaryninja::platform::Platform; +use bntl_utils::validate::{TypeLibValidater, ValidateIssue}; +use clap::Args; +use rayon::prelude::*; +use std::collections::HashMap; +use std::path::PathBuf; + +#[derive(Debug, Args)] +pub struct ValidateArgs { + /// Path to the directory containing the type libraries to validate. + /// + /// This must contain all the type libraries referencable. + pub input: PathBuf, + /// Dump validation results to the directory specified. + #[clap(short, long)] + pub output: Option, +} + +impl ValidateArgs { + pub fn execute(&self) { + if let Some(output_dir) = &self.output { + std::fs::create_dir_all(output_dir).expect("Failed to create output directory"); + } + + // TODO: For now we just pass all the type libraries in the containing input directory. + let type_libs = bntl_utils::helper::path_to_type_libraries(&self.input); + type_libs.par_iter().for_each(|type_lib| { + // We run validation per platform. This is to make sure that if we depend on platform + // types that they exist in each one of the specified platforms, not just one of them. + let mut platform_mapped_issues: HashMap> = HashMap::new(); + let available_platforms = type_lib.platform_names(); + + for platform in &available_platforms { + let platform = Platform::by_name(platform).expect("Failed to load platform"); + let mut ctx = TypeLibValidater::new() + .with_type_libraries(type_libs.clone()) + .with_platform(&platform); + let result = ctx.validate(&type_lib); + for issue in &result.issues { + platform_mapped_issues + .entry(issue.clone()) + .or_default() + .push(platform.name().to_string()); + } + + if let Some(output_dir) = &self.output + && !result.issues.is_empty() + { + let dump_path = output_dir + .join(type_lib.name()) + .with_extension(format!("{}.problems.json", platform.name())); + let result = serde_json::to_string_pretty(&result.issues) + .expect("Failed to serialize result"); + std::fs::write(dump_path, result).expect("Failed to write validation result"); + } + } + + for (issue, platforms) in platform_mapped_issues { + match (available_platforms.len(), platforms.len()) { + (1, _) => tracing::error!("{}", issue), + _ => tracing::error!("{}: {}", platforms.join(", "), issue), + } + } + }); + } +} diff --git a/plugins/bntl_utils/src/command.rs b/plugins/bntl_utils/src/command.rs new file mode 100644 index 000000000..39f25b994 --- /dev/null +++ b/plugins/bntl_utils/src/command.rs @@ -0,0 +1,66 @@ +use binaryninja::interaction::{Form, FormInputField}; +use binaryninja::user_directory; +use std::path::PathBuf; + +pub mod create; +pub mod diff; +pub mod dump; +pub mod validate; +// TODO: Load? + +pub struct InputFileField; + +impl InputFileField { + pub fn field() -> FormInputField { + FormInputField::OpenFileName { + prompt: "File Path".to_string(), + // TODO: This is called extension but is really a filter. + extension: None, + default: None, + value: None, + } + } + + pub fn from_form(form: &Form) -> Option { + let field = form.get_field_with_name("File Path")?; + let field_value = field.try_value_string()?; + Some(PathBuf::from(field_value)) + } +} + +pub struct OutputDirectoryField; + +impl OutputDirectoryField { + pub fn field() -> FormInputField { + let type_lib_dir = user_directory().join("typelib"); + FormInputField::DirectoryName { + prompt: "Output Directory".to_string(), + default: Some(type_lib_dir.to_string_lossy().to_string()), + value: None, + } + } + + pub fn from_form(form: &Form) -> Option { + let field = form.get_field_with_name("Output Directory")?; + let field_value = field.try_value_string()?; + Some(PathBuf::from(field_value)) + } +} + +pub struct InputDirectoryField; + +impl InputDirectoryField { + pub fn field() -> FormInputField { + FormInputField::DirectoryName { + prompt: "Input Directory".to_string(), + default: None, + value: None, + } + } + + pub fn from_form(form: &Form) -> Option { + let field = form.get_field_with_name("Input Directory")?; + let field_value = field.try_value_string()?; + Some(PathBuf::from(field_value)) + } +} diff --git a/plugins/bntl_utils/src/command/create.rs b/plugins/bntl_utils/src/command/create.rs new file mode 100644 index 000000000..0dcf6c3b3 --- /dev/null +++ b/plugins/bntl_utils/src/command/create.rs @@ -0,0 +1,196 @@ +use crate::command::{InputDirectoryField, OutputDirectoryField}; +use crate::process::{new_processing_state_background_thread, TypeLibProcessor}; +use crate::validate::TypeLibValidater; +use binaryninja::background_task::BackgroundTask; +use binaryninja::binary_view::{BinaryView, BinaryViewExt}; +use binaryninja::command::Command; +use binaryninja::interaction::{Form, FormInputField, MessageBoxButtonSet, MessageBoxIcon}; +use binaryninja::platform::Platform; +use std::thread; + +pub struct CreateFromCurrentView; + +impl Command for CreateFromCurrentView { + fn action(&self, view: &BinaryView) { + let mut form = Form::new("Create From View"); + // TODO: The choice to select what types to include + form.add_field(OutputDirectoryField::field()); + if !form.prompt() { + return; + } + let output_dir = OutputDirectoryField::from_form(&form).unwrap(); + let Some(default_platform) = view.default_platform() else { + tracing::error!("No default platform set for view"); + return; + }; + + let file_path = view.file().file_path(); + let file_name = file_path.file_name().unwrap_or_default().to_string_lossy(); + let processor = TypeLibProcessor::new(&file_name, &default_platform.name()); + let data = match processor.process_view(file_path, view) { + Ok(data) => data, + Err(err) => { + tracing::error!("Failed to process view: {}", err); + return; + } + } + .prune(); + + let attached_libraries = view + .type_libraries() + .iter() + .map(|t| t.to_owned()) + .chain(data.type_libraries.iter().map(|t| t.to_owned())) + .collect::>(); + let mut validator = TypeLibValidater::new() + .with_platform(&default_platform) + .with_type_libraries(attached_libraries); + + for type_library in data.type_libraries { + let output_path = output_dir.join(format!("{}.bntl", type_library.name())); + + let validation_result = validator.validate(&type_library); + if !validation_result.issues.is_empty() { + tracing::error!( + "Found {} issues in type library '{}'", + validation_result.issues.len(), + type_library.name() + ); + match validation_result.render_report() { + Ok(rendered) => { + view.show_html_report(&type_library.name(), &rendered, ""); + if let Err(e) = std::fs::write(output_path.with_extension("html"), rendered) + { + tracing::error!( + "Failed to write validation report to {}: {}", + output_path.display(), + e + ); + } + } + Err(err) => tracing::error!("Failed to render validation report: {}", err), + } + } + + if type_library.write_to_file(&output_path) { + tracing::info!( + "Created type library '{}': {}", + type_library.name(), + output_path.display() + ); + } else { + tracing::error!("Failed to write type library to {}", output_path.display()); + } + } + } + + fn valid(&self, _view: &BinaryView) -> bool { + true + } +} + +pub struct NameField; + +impl NameField { + pub fn field() -> FormInputField { + FormInputField::TextLine { + prompt: "Dependency Name".to_string(), + default: Some("foo.dll".to_string()), + value: None, + } + } + + pub fn from_form(form: &Form) -> Option { + let field = form.get_field_with_name("Dependency Name")?; + field.try_value_string() + } +} + +pub struct PlatformField; + +impl PlatformField { + pub fn field() -> FormInputField { + FormInputField::TextLine { + prompt: "Platform Name".to_string(), + default: Some("windows-x86_64".to_string()), + value: None, + } + } + + pub fn from_form(form: &Form) -> Option { + let field = form.get_field_with_name("Platform Name")?; + field.try_value_string() + } +} + +pub struct CreateFromDirectory; + +impl CreateFromDirectory { + pub fn execute() { + let mut form = Form::new("Create From Directory"); + // TODO: The choice to select what types to include + form.add_field(InputDirectoryField::field()); + form.add_field(PlatformField::field()); + form.add_field(NameField::field()); + form.add_field(OutputDirectoryField::field()); + if !form.prompt() { + return; + } + let input_dir = InputDirectoryField::from_form(&form).unwrap(); + let platform_name = PlatformField::from_form(&form).unwrap(); + let default_name = NameField::from_form(&form).unwrap(); + let output_dir = OutputDirectoryField::from_form(&form).unwrap(); + + let Some(default_platform) = Platform::by_name(&platform_name) else { + tracing::error!("Invalid platform name: {}", platform_name); + return; + }; + + let processor = TypeLibProcessor::new(&default_name, &default_platform.name()); + + let background_task = BackgroundTask::new("Processing started...", true); + new_processing_state_background_thread(background_task.clone(), processor.state()); + let data = processor.process_directory(&input_dir); + background_task.finish(); + + let pruned_data = match data { + // Prune off empty type libraries, no need to save them. + Ok(data) => data.prune(), + Err(err) => { + binaryninja::interaction::show_message_box( + "Failed to process directory", + &err.to_string(), + MessageBoxButtonSet::OKButtonSet, + MessageBoxIcon::ErrorIcon, + ); + tracing::error!("Failed to create signature file: {}", err); + return; + } + }; + + for type_library in pruned_data.type_libraries { + let output_path = output_dir.join(format!("{}.bntl", type_library.name())); + if type_library.write_to_file(&output_path) { + tracing::info!( + "Created type library '{}': {}", + type_library.name(), + output_path.display() + ); + } else { + tracing::error!("Failed to write type library to {}", output_path.display()); + } + } + } +} + +impl Command for CreateFromDirectory { + fn action(&self, _view: &BinaryView) { + thread::spawn(move || { + CreateFromDirectory::execute(); + }); + } + + fn valid(&self, _view: &BinaryView) -> bool { + true + } +} diff --git a/plugins/bntl_utils/src/command/diff.rs b/plugins/bntl_utils/src/command/diff.rs new file mode 100644 index 000000000..ae93dafe3 --- /dev/null +++ b/plugins/bntl_utils/src/command/diff.rs @@ -0,0 +1,103 @@ +use crate::command::OutputDirectoryField; +use crate::diff::TILDiff; +use binaryninja::background_task::BackgroundTask; +use binaryninja::binary_view::BinaryView; +use binaryninja::command::Command; +use binaryninja::interaction::{Form, FormInputField}; +use binaryninja::types::TypeLibrary; +use std::path::PathBuf; +use std::thread; + +pub struct InputFileAField; + +impl InputFileAField { + pub fn field() -> FormInputField { + FormInputField::OpenFileName { + prompt: "Library A".to_string(), + // TODO: This is called extension but is really a filter. + extension: Some("*.bntl".to_string()), + default: None, + value: None, + } + } + + pub fn from_form(form: &Form) -> Option { + let field = form.get_field_with_name("Library A")?; + let field_value = field.try_value_string()?; + Some(PathBuf::from(field_value)) + } +} + +pub struct InputFileBField; + +impl InputFileBField { + pub fn field() -> FormInputField { + FormInputField::OpenFileName { + prompt: "Library B".to_string(), + // TODO: This is called extension but is really a filter. + extension: Some("*.bntl".to_string()), + default: None, + value: None, + } + } + + pub fn from_form(form: &Form) -> Option { + let field = form.get_field_with_name("Library B")?; + let field_value = field.try_value_string()?; + Some(PathBuf::from(field_value)) + } +} + +pub struct Diff; + +impl Diff { + pub fn execute() { + let mut form = Form::new("Diff type libraries"); + form.add_field(InputFileAField::field()); + form.add_field(InputFileBField::field()); + form.add_field(OutputDirectoryField::field()); + if !form.prompt() { + return; + } + let a_path = InputFileAField::from_form(&form).unwrap(); + let b_path = InputFileBField::from_form(&form).unwrap(); + let output_dir = OutputDirectoryField::from_form(&form).unwrap(); + + let _bg_task = BackgroundTask::new("Diffing type libraries...", false).enter(); + let Some(type_lib_a) = TypeLibrary::load_from_file(&a_path) else { + tracing::error!("Failed to load type library: {}", a_path.display()); + return; + }; + let Some(type_lib_b) = TypeLibrary::load_from_file(&b_path) else { + tracing::error!("Failed to load type library: {}", b_path.display()); + return; + }; + + let diff_result = match TILDiff::new().diff((&a_path, &type_lib_a), (&b_path, &type_lib_b)) + { + Ok(diff_result) => diff_result, + Err(err) => { + tracing::error!("Failed to diff type libraries: {}", err); + return; + } + }; + tracing::info!("Similarity Ratio: {}", diff_result.ratio); + let output_path = output_dir + .join(type_lib_a.dependency_name()) + .with_extension("diff"); + std::fs::write(&output_path, diff_result.diff).unwrap(); + tracing::info!("Diff written to: {}", output_path.display()); + } +} + +impl Command for Diff { + fn action(&self, _view: &BinaryView) { + thread::spawn(move || { + Diff::execute(); + }); + } + + fn valid(&self, _view: &BinaryView) -> bool { + true + } +} diff --git a/plugins/bntl_utils/src/command/dump.rs b/plugins/bntl_utils/src/command/dump.rs new file mode 100644 index 000000000..4b68cd799 --- /dev/null +++ b/plugins/bntl_utils/src/command/dump.rs @@ -0,0 +1,52 @@ +use crate::command::{InputFileField, OutputDirectoryField}; +use crate::dump::TILDump; +use crate::helper::path_to_type_libraries; +use binaryninja::binary_view::BinaryView; +use binaryninja::command::Command; +use binaryninja::interaction::Form; +use binaryninja::types::TypeLibrary; + +pub struct Dump; + +impl Command for Dump { + // TODO: We need a command type that does not require a binary view. + fn action(&self, _view: &BinaryView) { + let mut form = Form::new("Dump to C Header"); + // TODO: The choice to select what to include? + form.add_field(InputFileField::field()); + form.add_field(OutputDirectoryField::field()); + if !form.prompt() { + return; + } + let output_dir = OutputDirectoryField::from_form(&form).unwrap(); + let input_path = InputFileField::from_form(&form).unwrap(); + + let type_lib = match TypeLibrary::load_from_file(&input_path) { + Some(type_lib) => type_lib, + None => { + tracing::error!("Failed to load type library from {}", input_path.display()); + return; + } + }; + + // TODO: Currently we collect input path dependencies from the platform and the parent directory. + let dependencies = path_to_type_libraries(input_path.parent().unwrap()); + let dump = match TILDump::new().with_type_libs(dependencies).dump(&type_lib) { + Ok(dump) => dump, + Err(err) => { + tracing::error!("Failed to dump type library: {}", err); + return; + } + }; + + let output_path = output_dir.join(format!("{}.h", type_lib.name())); + if let Err(e) = std::fs::write(&output_path, dump) { + tracing::error!("Failed to write dump to {}: {}", output_path.display(), e); + } + tracing::info!("Dump written to {}", output_path.display()); + } + + fn valid(&self, _view: &BinaryView) -> bool { + true + } +} diff --git a/plugins/bntl_utils/src/command/validate.rs b/plugins/bntl_utils/src/command/validate.rs new file mode 100644 index 000000000..8f0095ae2 --- /dev/null +++ b/plugins/bntl_utils/src/command/validate.rs @@ -0,0 +1,78 @@ +use crate::helper::path_to_type_libraries; +use crate::validate::TypeLibValidater; +use binaryninja::binary_view::{BinaryView, BinaryViewExt}; +use binaryninja::command::Command; +use binaryninja::interaction::get_open_filename_input; +use binaryninja::platform::Platform; +use binaryninja::types::TypeLibrary; + +pub struct Validate; + +impl Command for Validate { + fn action(&self, _view: &BinaryView) { + let Some(input_path) = + get_open_filename_input("Select a type library to validate", "*.bntl") + else { + return; + }; + + let type_lib = match TypeLibrary::load_from_file(&input_path) { + Some(type_lib) => type_lib, + None => { + tracing::error!("Failed to load type library from {}", input_path.display()); + return; + } + }; + + // Type libraries should always have at least one platform associated with them. + if type_lib.platform_names().is_empty() { + tracing::error!("Type library {} has no platforms!", input_path.display()); + return; + } + + // TODO: Currently we collect input path dependencies from the platform and the parent directory. + let dependencies = path_to_type_libraries(input_path.parent().unwrap()); + + let validator = TypeLibValidater::new().with_type_libraries(dependencies); + // Validate for every platform so that we can find issues in lesser used platforms. + for platform_name in &type_lib.platform_names() { + let Some(platform) = Platform::by_name(platform_name) else { + tracing::error!("Failed to find platform with name {}", platform_name); + continue; + }; + let results = validator + .clone() + .with_platform(&platform) + .validate(&type_lib); + if results.issues.is_empty() { + tracing::info!( + "No issues found for type library {} on platform {}", + type_lib.name(), + platform_name + ); + continue; + } + let rendered = match results.render_report() { + Ok(rendered) => rendered, + Err(err) => { + tracing::error!("Failed to render validation report: {}", err); + continue; + } + }; + let out_path = input_path.with_extension(format!("{}.html", platform_name)); + let out_name = format!("{} ({})", type_lib.name(), platform_name); + _view.show_html_report(&out_name, &rendered, ""); + if let Err(e) = std::fs::write(out_path, rendered) { + tracing::error!( + "Failed to write validation report to {}: {}", + input_path.display(), + e + ); + } + } + } + + fn valid(&self, _view: &BinaryView) -> bool { + true + } +} diff --git a/plugins/bntl_utils/src/diff.rs b/plugins/bntl_utils/src/diff.rs new file mode 100644 index 000000000..18d4dce89 --- /dev/null +++ b/plugins/bntl_utils/src/diff.rs @@ -0,0 +1,83 @@ +use crate::dump::TILDump; +use crate::helper::path_to_type_libraries; +use binaryninja::types::TypeLibrary; +use similar::{Algorithm, TextDiff}; +use std::path::{Path, PathBuf}; +use std::time::Duration; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum TILDiffError { + #[error("Could not determine parent directory for path: {0}")] + InvalidPath(PathBuf), + + #[error("Failed to dump type library: {0}")] + DumpError(String), +} + +pub struct DiffResult { + pub ratio: f32, + pub diff: String, +} + +pub struct TILDiff { + timeout: Duration, +} + +impl TILDiff { + pub fn new() -> Self { + Self { + timeout: Duration::from_secs(180), + } + } + + pub fn with_timeout(mut self, timeout: Duration) -> Self { + self.timeout = timeout; + self + } + + pub fn diff( + &self, + (a_path, a_type_lib): (&Path, &TypeLibrary), + (b_path, b_type_lib): (&Path, &TypeLibrary), + ) -> Result { + let a_parent = a_path + .parent() + .ok_or_else(|| TILDiffError::InvalidPath(a_path.to_path_buf()))?; + let b_parent = b_path + .parent() + .ok_or_else(|| TILDiffError::InvalidPath(b_path.to_path_buf()))?; + + let a_dependencies = path_to_type_libraries(a_parent); + let b_dependencies = path_to_type_libraries(b_parent); + + let dumped_a = TILDump::new() + .with_type_libs(a_dependencies) + .dump(a_type_lib) + .map_err(|e| TILDiffError::DumpError(e.to_string()))?; + + let dumped_b = TILDump::new() + .with_type_libs(b_dependencies) + .dump(b_type_lib) + .map_err(|e| TILDiffError::DumpError(e.to_string()))?; + + let diff = TextDiff::configure() + .algorithm(Algorithm::Patience) + .timeout(self.timeout) + .diff_lines(&dumped_a, &dumped_b); + + let diff_content = diff + .unified_diff() + .context_radius(3) + .header( + a_path.to_string_lossy().as_ref(), + b_path.to_string_lossy().as_ref(), + ) + .to_string(); + + Ok(DiffResult { + ratio: diff.ratio(), + diff: diff_content, + }) + } +} diff --git a/plugins/bntl_utils/src/dump.rs b/plugins/bntl_utils/src/dump.rs new file mode 100644 index 000000000..bfcb03fa6 --- /dev/null +++ b/plugins/bntl_utils/src/dump.rs @@ -0,0 +1,146 @@ +use binaryninja::binary_view::{BinaryView, BinaryViewExt}; +use binaryninja::file_metadata::FileMetadata; +use binaryninja::metadata::{Metadata, MetadataType}; +use binaryninja::platform::Platform; +use binaryninja::rc::Ref; +use binaryninja::types::printer::TokenEscapingType; +use binaryninja::types::{CoreTypePrinter, TypeLibrary}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum TILDumpError { + #[error("Failed to create empty BinaryView")] + ViewCreationFailed, + + #[error("Type library has no associated platforms")] + NoPlatformFound, + + #[error("Platform '{0}' not found in Binary Ninja")] + PlatformNotFound(String), + + #[error("Failed to print types from library")] + PrinterError, + + #[error("Metadata error: {0}")] + MetadataError(String), + + #[error("Unexpected metadata type for 'ordinals': {0:?}")] + UnexpectedMetadataType(MetadataType), +} + +pub struct TILDump { + /// The type libraries that are accessible to the type printer. + available_type_libs: Vec>, +} + +impl TILDump { + pub fn new() -> Self { + Self { + available_type_libs: Vec::new(), + } + } + + pub fn with_type_libs(mut self, type_libs: Vec>) -> Self { + self.available_type_libs = type_libs; + self + } + + pub fn dump(&self, type_lib: &TypeLibrary) -> Result { + let empty_file = FileMetadata::new(); + let empty_bv = BinaryView::from_data(&empty_file, &[]) + .map_err(|_| TILDumpError::ViewCreationFailed)?; + + let type_lib_plats = type_lib.platform_names(); + let platform_name = type_lib_plats + .iter() + .next() + .ok_or(TILDumpError::NoPlatformFound)?; + + let platform_name_str = platform_name.to_string(); + let platform = Platform::by_name(&platform_name_str) + .ok_or_else(|| TILDumpError::PlatformNotFound(platform_name_str))?; + + empty_bv.set_default_platform(&platform); + + for dependency in &self.available_type_libs { + empty_bv.add_type_library(dependency); + } + empty_bv.add_type_library(type_lib); + + for ty in &type_lib.named_types() { + empty_bv.import_type_library(ty.name, None); + } + for obj in &type_lib.named_objects() { + empty_bv.import_type_object(obj.name, None); + } + + let dep_sorted_types = empty_bv.dependency_sorted_types(); + let unsorted_functions = type_lib.named_objects(); + let mut all_types: Vec<_> = dep_sorted_types + .iter() + .chain(unsorted_functions.iter()) + .collect(); + all_types.sort_by_key(|t| t.name.clone()); + + let type_printer = CoreTypePrinter::default(); + let printed_types = type_printer + .print_all_types( + all_types, + &empty_bv, + 4, + TokenEscapingType::NoTokenEscapingType, + ) + .ok_or(TILDumpError::PrinterError)?; + + let mut printed_types_str = printed_types.to_string_lossy().to_string(); + printed_types_str.push_str("\n// TYPE LIBRARY INFORMATION\n"); + + let metadata_lines = type_library_metadata_to_string(type_lib)?; + printed_types_str.push_str(&metadata_lines.join("\n")); + + empty_file.close(); + Ok(printed_types_str) + } +} + +fn type_library_metadata_to_string(type_lib: &TypeLibrary) -> Result, TILDumpError> { + let mut result = Vec::new(); + for alt_name in &type_lib.alternate_names() { + result.push(format!("// ALTERNATE NAME: {}", alt_name)); + } + + let mut add_ordinals = |metadata: Ref| -> Result<(), TILDumpError> { + if let Some(map) = metadata.get_value_store() { + let mut list = map.iter().collect::>(); + list.sort_by_key(|&(key, _)| key.parse::().unwrap_or_default()); + for (key, value) in list { + result.push(format!("// ORDINAL {}: {}", key, value)); + } + } + Ok(()) + }; + + if let Some(ordinal_key) = type_lib.query_metadata("ordinals") { + match ordinal_key.get_type() { + MetadataType::StringDataType => { + let queried_key = ordinal_key.get_string().ok_or_else(|| { + TILDumpError::MetadataError("Failed to get ordinal key string".into()) + })?; + + let queried_key_str = queried_key.to_string_lossy(); + let queried_md = type_lib.query_metadata(&queried_key_str).ok_or_else(|| { + TILDumpError::MetadataError(format!( + "Failed to query metadata for key: {}", + queried_key_str + )) + })?; + + add_ordinals(queried_md)?; + } + MetadataType::KeyValueDataType => add_ordinals(ordinal_key)?, + ty => return Err(TILDumpError::UnexpectedMetadataType(ty)), + } + } + + Ok(result) +} diff --git a/plugins/bntl_utils/src/helper.rs b/plugins/bntl_utils/src/helper.rs new file mode 100644 index 000000000..3fc1a0470 --- /dev/null +++ b/plugins/bntl_utils/src/helper.rs @@ -0,0 +1,45 @@ +use binaryninja::rc::Ref; +use binaryninja::types::{NamedTypeReference, Type, TypeClass, TypeLibrary}; +use std::path::Path; +use walkdir::WalkDir; + +pub fn path_to_type_libraries(path: &Path) -> Vec> { + WalkDir::new(path) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().is_file()) + .filter(|e| e.path().extension().map_or(false, |ext| ext == "bntl")) + .filter_map(|e| TypeLibrary::load_from_file(e.path())) + .collect::>() +} + +pub fn visit_type_reference(ty: &Type, visit: &mut impl FnMut(&NamedTypeReference)) { + if let Some(ntr) = ty.get_named_type_reference() { + visit(&ntr); + } + match ty.type_class() { + TypeClass::StructureTypeClass => { + let structure = ty.get_structure().unwrap(); + for field in structure.members() { + visit_type_reference(&field.ty.contents, visit); + } + for base in structure.base_structures() { + visit(&base.ty); + } + } + TypeClass::PointerTypeClass => { + visit_type_reference(&ty.child_type().unwrap().contents, visit); + } + TypeClass::ArrayTypeClass => { + visit_type_reference(&ty.child_type().unwrap().contents, visit); + } + TypeClass::FunctionTypeClass => { + let params = ty.parameters().unwrap(); + for param in params { + visit_type_reference(¶m.ty.contents, visit); + } + visit_type_reference(&ty.return_value().unwrap().contents, visit); + } + _ => {} + } +} diff --git a/plugins/bntl_utils/src/lib.rs b/plugins/bntl_utils/src/lib.rs new file mode 100644 index 000000000..3c6dbe195 --- /dev/null +++ b/plugins/bntl_utils/src/lib.rs @@ -0,0 +1,61 @@ +mod command; +pub mod diff; +pub mod dump; +pub mod helper; +pub mod process; +pub mod schema; +pub mod url; +pub mod validate; +mod winmd; + +#[no_mangle] +#[allow(non_snake_case)] +pub extern "C" fn CorePluginInit() -> bool { + if plugin_init().is_err() { + tracing::error!("Failed to initialize BNTL Utils plug-in"); + return false; + } + true +} + +fn plugin_init() -> Result<(), ()> { + binaryninja::tracing_init!("BNTL Utils"); + + binaryninja::command::register_command( + "BNTL\\Create\\From Current View", + "Create .bntl files from the current view", + command::create::CreateFromCurrentView {}, + ); + + binaryninja::command::register_command( + "BNTL\\Create\\From Project", + "Create .bntl files from the given project", + command::create::CreateFromCurrentView {}, + ); + + binaryninja::command::register_command( + "BNTL\\Create\\From Directory", + "Create .bntl files from the given directory", + command::create::CreateFromDirectory {}, + ); + + binaryninja::command::register_command( + "BNTL\\Diff", + "Diff two .bntl files and output the difference to a file", + command::diff::Diff {}, + ); + + binaryninja::command::register_command( + "BNTL\\Dump To Header", + "Dump a .bntl file to a header file", + command::dump::Dump {}, + ); + + binaryninja::command::register_command( + "BNTL\\Validate", + "Validate a .bntl file and report the issues", + command::validate::Validate {}, + ); + + Ok(()) +} diff --git a/plugins/bntl_utils/src/process.rs b/plugins/bntl_utils/src/process.rs new file mode 100644 index 000000000..f2adf58e2 --- /dev/null +++ b/plugins/bntl_utils/src/process.rs @@ -0,0 +1,932 @@ +//! Process different types of files into Binary Ninja type libraries. + +use binaryninja::architecture::CoreArchitecture; +use dashmap::DashMap; +use std::collections::{HashMap, HashSet}; +use std::env::temp_dir; +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering::Relaxed; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use thiserror::Error; +use walkdir::WalkDir; + +use crate::schema::BntlSchema; +use crate::winmd::WindowsMetadataImporter; +use binaryninja::background_task::BackgroundTask; +use binaryninja::binary_view::{BinaryView, BinaryViewExt}; +use binaryninja::custom_binary_view::BinaryViewType; +use binaryninja::file_metadata::FileMetadata; +use binaryninja::metadata::Metadata; +use binaryninja::platform::Platform; +use binaryninja::project::file::ProjectFile; +use binaryninja::project::folder::ProjectFolder; +use binaryninja::project::Project; +use binaryninja::qualified_name::QualifiedName; +use binaryninja::rc::Ref; +use binaryninja::section::Section; +use binaryninja::types::{CoreTypeParser, Type, TypeLibrary, TypeParser, TypeParserError}; +use nt_apiset::{ApiSetMap, NtApiSetError}; + +#[derive(Error, Debug)] +pub enum ProcessingError { + #[error("Binary view load error: {0}")] + BinaryViewLoad(PathBuf), + + #[error("Failed to read binary view at offset {0:?} with length {1:?}")] + BinaryViewRead(u64, usize), + + #[error("Failed to read .apiset section: {0}")] + FailedToReadApiSet(#[from] NtApiSetError), + + #[error("Failed to read file: {0}")] + FileRead(std::io::Error), + + #[error("Failed to retrieve path to project file: {0:?}")] + NoPathToProjectFile(Ref), + + #[error("Processing state has been poisoned")] + StatePoisoned, + + #[error("Processing has been cancelled")] + Cancelled, + + #[error("Skipping file: {0}")] + SkippedFile(PathBuf), + + #[error("Failed to find platform: {0}")] + PlatformNotFound(String), + + #[error("Failed to parse types: {0:?}")] + TypeParsingFailed(Vec), + + #[error("Failed to import winmd: {0}")] + WinMdFailedImport(crate::winmd::ImportError), + + #[error("Failed to parse type library: {0}")] + InvalidTypeLibrary(PathBuf), +} + +#[derive(Default, Debug)] +pub struct ProcessingState { + pub cancelled: AtomicBool, + pub files: DashMap, +} + +impl ProcessingState { + pub fn is_cancelled(&self) -> bool { + self.cancelled.load(Relaxed) + } + + pub fn cancel(&self) { + self.cancelled.store(true, Relaxed) + } + + pub fn files_with_state(&self, state: bool) -> usize { + self.files.iter().filter(|f| *f.value() == state).count() + } + + pub fn set_file_state(&self, path: PathBuf, state: bool) { + self.files.insert(path, state); + } + + pub fn total_files(&self) -> usize { + self.files.len() + } +} + +pub fn new_processing_state_background_thread( + task: Ref, + state: Arc, +) { + std::thread::spawn(move || { + let start = Instant::now(); + while !task.is_finished() { + std::thread::sleep(Duration::from_millis(100)); + // Check if the user wants to cancel the processing. + if task.is_cancelled() { + state.cancel(); + } + + let total = state.total_files(); + let processed = state.files_with_state(true); + let unprocessed = state.files_with_state(false); + let completion = (processed as f64 / total as f64) * 100.0; + let elapsed = start.elapsed().as_secs_f32(); + let text = format!( + "Processing {} files... {{{}|{}}} ({:.2}%) [{:.2}s]", + total, unprocessed, processed, completion, elapsed + ); + task.set_progress_text(&text); + } + }); +} + +/// The result of running [`TypeLibProcessor`]. +#[derive(Debug, Clone)] +pub struct ProcessedData { + // TODO: Maybe we instead have an intermediate format that is easier to work with? + pub type_libraries: Vec>, +} + +impl ProcessedData { + pub fn new(type_libraries: Vec>) -> Self { + Self { type_libraries } + } + + /// Prune empty type libraries from the processed data. + /// + /// This is useful if you intend to save the type libraries to disk in a finalized form. + pub fn prune(self) -> Self { + let is_empty = + |tl: &TypeLibrary| tl.named_types().is_empty() && tl.named_objects().is_empty(); + let pruned_type_libraries = self + .type_libraries + .into_iter() + .filter(|tl| !is_empty(tl)) + .collect::>(); + Self::new(pruned_type_libraries) + } + + /// Merges multiple [`ProcessedData`] into one, deduplicating type libraries. + /// + /// This is necessary to allow the [`TypeLibProcessor`] to operate on a wide range of formats whilst + /// also guaranteeing no collisions and valid external references. Without merging libraries with + /// identical dependency names would be separate, which is not a supported scenario when loading + /// type libraries into Binary Ninja. + pub fn merge(list: &[ProcessedData]) -> Self { + let mut type_libraries = Vec::new(); + for data in list { + type_libraries.extend(data.type_libraries.iter().cloned()); + } + + // We merge type libraries with the same dependency name, as that is what needs to be unique + // when we go to load them into Binary Ninja. + let mut mapped_type_libraries: HashMap<(String, CoreArchitecture), Vec>> = + HashMap::new(); + for tl in type_libraries.iter() { + mapped_type_libraries + .entry((tl.dependency_name(), tl.arch())) + .or_default() + .push(tl.clone()); + } + + let mut merged_type_libraries = Vec::new(); + for ((dependency_name, arch), type_libraries) in mapped_type_libraries { + let merged_type_library = TypeLibrary::new(arch, &dependency_name); + merged_type_library.set_dependency_name(&dependency_name); + for tl in type_libraries { + // TODO: Cheap type overrides (if one type is set as void* and the other as Foo* we take Foo*) + for named_type in &tl.named_types() { + merged_type_library.add_named_type(named_type.name.clone(), &named_type.ty); + } + for named_object in &tl.named_objects() { + merged_type_library + .add_named_object(named_object.name.clone(), &named_object.ty); + } + for alt_name in &tl.alternate_names() { + merged_type_library.add_alternate_name(alt_name); + } + for platform_name in &tl.platform_names() { + if let Some(platform) = Platform::by_name(&platform_name) { + merged_type_library.add_platform(&platform); + } else { + // TODO: Upgrade this to an error? + tracing::warn!( + "Unknown platform name when merging '{}': '{}'", + dependency_name, + platform_name + ); + } + } + // TODO: Stealing the type sources is literally impossible there is no getter, incredible... + // TODO: Replace this with a getter to type sources :/ + let tmp_file = temp_dir().join(format!("{}_{}.bntl", dependency_name, tl.guid())); + if tl.write_to_file(&tmp_file) { + let schema = BntlSchema::from_path(&tmp_file); + for type_source in schema.type_sources { + merged_type_library + .add_type_source(type_source.name.into(), &type_source.source); + } + } + // TODO: Enumerate metadata and merge it, most importantly, we need to merge ordinals. + } + merged_type_libraries.push(merged_type_library); + } + + Self::new(merged_type_libraries) + } +} + +pub struct TypeLibProcessor { + state: Arc, + /// The Binary Ninja settings to use when analyzing the binaries. + analysis_settings: serde_json::Value, + /// The default name to use for the type library dependency name (e.g. "sqlite.dll"). + /// + /// When processing information that does not contain the dependency name, this will be used, + /// such as processing header files. We need to set a dependency name, otherwise the library + /// will not be able to be referenced by other libraries and/or the binary view. + /// + /// This dependency name will NOT be used when it can otherwise be inferred by the processing + /// data, if you wish to override the resulting dependency name, you can do so by calling + /// [`TypeLibrary::set_dependency_name`] on the libraries returned via [`ProcessedData::type_libraries`]. + default_dependency_name: String, + /// The default platform name to use when processing (e.g. "windows-x86_64"). + /// + /// When processing information that does not have an associated platform, this will be used, + /// such as processing header files or processing winmd files. When processing binary files, + /// the platform will be derived from the binary view default platform. + /// + /// For WINMD files you typically want to run the processor for each of the following platforms: + /// + /// - "windows-x86_64" + /// - "windows-x86" + /// - "windows-aarch64" + default_platform_name: String, + /// Set the include directories to use when processing header files. These will be passed to the + /// Clang type parser, which will use them to resolve header file includes. + include_directories: Vec, +} + +impl TypeLibProcessor { + pub fn new(default_dependency_name: &str, default_platform_name: &str) -> Self { + Self { + state: Arc::new(ProcessingState::default()), + analysis_settings: serde_json::json!({ + "analysis.linearSweep.autorun": false, + "analysis.mode": "full", + }), + default_dependency_name: default_dependency_name.to_owned(), + default_platform_name: default_platform_name.to_owned(), + include_directories: Vec::new(), + } + } + + /// Retrieve a thread-safe shared reference to the [`ProcessingState`]. + pub fn state(&self) -> Arc { + self.state.clone() + } + + pub fn with_include_directories(mut self, include_directories: Vec) -> Self { + self.include_directories = include_directories; + self + } + + /// Place a call to this in places to interrupt when canceled. + fn check_cancelled(&self) -> Result<(), ProcessingError> { + match self.state.is_cancelled() { + true => Err(ProcessingError::Cancelled), + false => Ok(()), + } + } + + pub fn process(&self, path: &Path) -> Result { + match path.extension() { + Some(ext) if ext == "bntl" => self.process_type_library(&path), + Some(ext) if ext == "h" || ext == "hpp" => self.process_source(path), + // NOTE: A typical processor will not go down this path where we only provide a single + // winmd file to be processed. You almost always want to process multiple winmd files, + // which can be done by passing a directory with the relevant winmd files. + Some(ext) if ext == "winmd" => self.process_winmd(&[path.to_owned()]), + _ if path.is_dir() => self.process_directory(path), + _ => self.process_file(path), + } + } + + pub fn process_directory(&self, path: &Path) -> Result { + // Collect all files in the directory + let files = WalkDir::new(path) + .into_iter() + .filter_map(|e| { + let path = e.ok()?.into_path(); + if path.is_file() { + Some(path) + } else { + None + } + }) + .collect::>(); + + // TODO: Parallel processing of files? + let unmerged_data: Result, _> = files + .iter() + .map(|file| { + self.check_cancelled()?; + self.process(file) + }) + .filter_map(|res| match res { + Ok(result) => Some(Ok(result)), + Err(ProcessingError::SkippedFile(path)) => { + tracing::debug!("Skipping directory file: {:?}", path); + None + } + Err(ProcessingError::Cancelled) => Some(Err(ProcessingError::Cancelled)), + Err(e) => { + tracing::error!("Directory file processing error: {:?}", e); + None + } + }) + .collect(); + + Ok(ProcessedData::merge(&unmerged_data?)) + } + + pub fn process_project(&self, project: &Project) -> Result { + // Inform the state of the new unprocessed project files. + for project_file in &project.files() { + // NOTE: We use the on disk path here because the downstream file state uses that. + if let Some(path) = project_file.path_on_disk() { + self.state.set_file_state(path, false); + } + } + + let data: Result, _> = project + .files() + .iter() + .map(|file| { + self.check_cancelled()?; + self.process_project_file(&file) + }) + .filter_map(|res| match res { + Ok(result) => Some(Ok(result)), + Err(ProcessingError::SkippedFile(path)) => { + tracing::debug!("Skipping project root file: {:?}", path); + None + } + Err(ProcessingError::Cancelled) => Some(Err(ProcessingError::Cancelled)), + Err(e) => { + tracing::error!("Project root file processing error: {:?}", e); + None + } + }) + .collect(); + + Ok(ProcessedData::merge(&data?)) + } + + pub fn process_project_folder( + &self, + project_folder: &ProjectFolder, + ) -> Result { + for project_file in &project_folder.files() { + // NOTE: We use the on disk path here because the downstream file state uses that. + if let Some(path) = project_file.path_on_disk() { + self.state.set_file_state(path, false); + } + } + + let unmerged_data: Result, _> = project_folder + .files() + .iter() + .map(|file| { + self.check_cancelled()?; + self.process_project_file(&file) + }) + .filter_map(|res| match res { + Ok(result) => Some(Ok(result)), + Err(ProcessingError::SkippedFile(path)) => { + tracing::debug!("Skipping project directory file: {:?}", path); + None + } + Err(ProcessingError::Cancelled) => Some(Err(ProcessingError::Cancelled)), + Err(e) => { + tracing::error!("Project folder file processing error: {:?}", e); + None + } + }) + .collect(); + + Ok(ProcessedData::merge(&unmerged_data?)) + } + + pub fn process_project_file( + &self, + project_file: &ProjectFile, + ) -> Result { + let file_name = project_file.name(); + let extension = file_name.split('.').last(); + let path = project_file + .path_on_disk() + .ok_or_else(|| ProcessingError::NoPathToProjectFile(project_file.to_owned()))?; + match extension { + Some(ext) if ext == "bntl" => self.process_type_library(&path), + Some(ext) if ext == "h" || ext == "hpp" => self.process_source(&path), + // NOTE: A typical processor will not go down this path where we only provide a single + // winmd file to be processed. You almost always want to process multiple winmd files, + // which can be done by passing a directory with the relevant winmd files. + Some(ext) if ext == "winmd" => self.process_winmd(&[path]), + _ => self.process_file(&path), + } + } + + // TODO: Process mapping file + // TODO: A json file that maps type names to their type dlls + // TODO: Apples format + + pub fn process_file(&self, path: &Path) -> Result { + // If the file cannot be parsed, it should be skipped to avoid a load error. + if !is_parsable(path) { + return Err(ProcessingError::SkippedFile(path.to_owned())); + } + + let file = binaryninja::load_with_options_and_progress( + &path, + false, + self.analysis_settings.as_str(), + |_pos, _total| { + // TODO: Report progress + true + }, + ) + .ok_or_else(|| ProcessingError::BinaryViewLoad(path.to_owned()))?; + let data = self.process_view(path.to_owned(), &file); + file.file().close(); + data + } + + pub fn process_view( + &self, + path: PathBuf, + view: &BinaryView, + ) -> Result { + self.state.set_file_state(path.to_owned(), false); + let view_platform = view.default_platform().unwrap_or(self.default_platform()?); + let type_library = TypeLibrary::new(view_platform.arch(), &self.default_dependency_name); + type_library.add_platform(&view_platform); + + // TODO: This has to be extremely slow + let platform_types = view_platform + .types() + .iter() + .map(|t| t.name.clone()) + .collect::>(); + let mut type_name_to_library = HashMap::new(); + for tl in view.type_libraries().iter() { + let lib_name = tl.name().to_string(); + for t in tl.named_types().iter() { + type_name_to_library.insert(t.name.clone(), lib_name.clone()); + } + } + + let add_referenced_types = |type_library: &TypeLibrary, ty: &Type| { + crate::helper::visit_type_reference(ty, &mut |ntr| { + let referenced_name = ntr.name(); + if platform_types.contains(&referenced_name) { + // The type referenced comes from the platform, so we do not need to do anything. + } else if let Some(source) = type_name_to_library.get(&referenced_name) { + type_library.add_type_source(referenced_name, source); + } else { + // Type does not belong to another type library, so we add it to the current one. + type_library.add_named_type(referenced_name, ty); + } + }); + }; + + let mut ordinals: HashMap = HashMap::new(); + let functions = view.functions(); + tracing::info!("Adding {} functions", functions.len()); + for func in &functions { + if !func.is_exported() { + continue; + } + let Some(defined_symbol) = func.defined_symbol() else { + tracing::debug!( + "Function '{}' has no defined symbol, skipping...", + func.symbol() + ); + continue; + }; + let qualified_name = QualifiedName::from(defined_symbol.to_string()); + type_library.add_named_object(qualified_name, &func.function_type()); + add_referenced_types(&type_library, &func.function_type()); + + if let Some(ordinal) = defined_symbol.ordinal() { + ordinals.insert(ordinal.to_string(), defined_symbol.to_string()); + } + } + + if !ordinals.is_empty() { + tracing::warn!( + "Found {} ordinals in '{}', adding metadata...", + ordinals.len(), + view.file(), + ); + // TODO: The ordinal version is OSMAJOR_OSMINOR, pull from pe metadata (use object crate) + let key_md: Ref = String::from("ordinals_10_0").into(); + type_library.store_metadata("ordinals", &key_md); + let map_md: Ref = ordinals.into(); + type_library.store_metadata("ordinals_10_0", &map_md); + } + + let mut processed_data = self.process_external_libraries(&view)?; + processed_data.type_libraries.push(type_library); + if let Some(api_set_section) = view.section_by_name(".apiset") { + let processed_api_set = self.process_api_set(&view, &api_set_section)?; + processed_data = ProcessedData::merge(&[processed_data, processed_api_set]); + } + self.state.set_file_state(path.to_owned(), true); + Ok(processed_data) + } + + pub fn process_external_libraries( + &self, + view: &BinaryView, + ) -> Result { + let view_platform = view.default_platform().unwrap_or(self.default_platform()?); + let mut extern_type_libraries = HashMap::new(); + for extern_lib in &view.external_libraries() { + let extern_type_library = TypeLibrary::new(view_platform.arch(), &extern_lib.name()); + extern_type_library.add_platform(&view_platform); + extern_type_library.set_dependency_name(&extern_lib.name()); + extern_type_libraries.insert(extern_lib.name(), extern_type_library); + } + + // Pull import types and add them to respective type libraries. + for extern_loc in &view.external_locations() { + // The source symbol represents the symbol represented in the binary, while the target + // symbol represents the symbol that we intend to map the information to. + let src_sym = extern_loc.source_symbol(); + let Some(extern_lib) = extern_loc.library() else { + tracing::warn!( + "External location '{}' has no library, skipping...", + src_sym + ); + continue; + }; + let Some(extern_type_library) = extern_type_libraries.get_mut(&extern_lib.name()) + else { + tracing::warn!( + "External location '{}' is referencing a detached external library, skipping...", + src_sym + ); + continue; + }; + let Some(src_data_var) = view.data_variable_at_address(src_sym.address()) else { + tracing::debug!( + "External location '{}' has no data variable, skipping...", + src_sym + ); + continue; + }; + if src_data_var.auto_discovered { + // We do not want to record objects which are not modified by the user, otherwise + // we are recording the object each time we visit a binary view, possibly retrieving + // the old definition of the object. + tracing::debug!( + "External location '{}' is auto discovered, skipping...", + src_sym + ); + continue; + } + let target_sym_name = extern_loc + .target_symbol() + .unwrap_or_else(|| src_sym.raw_name()); + // TODO: Need to visit all types referenced and add it to the type library. + extern_type_library.add_named_object(target_sym_name.into(), &src_data_var.ty.contents); + } + + Ok(ProcessedData::new( + extern_type_libraries.values().cloned().collect(), + )) + } + + /// Process API sets on Windows binaries, so we can fill in the alternative names for type libraries + /// we are processing. + /// + /// Creates an empty type library for the host and adds the alternative names to it. This should then + /// be passed to the [`ProcessedData::merge`] set to be merged with the type library of the host name. + /// + /// For more information see: https://learn.microsoft.com/en-us/windows/win32/apiindex/windows-apisets + pub fn process_api_set( + &self, + view: &BinaryView, + section: &Section, + ) -> Result { + let section_bytes = view + .read_buffer(section.start(), section.len()) + .ok_or_else(|| ProcessingError::BinaryViewRead(section.start(), section.len()))?; + let api_set_map = ApiSetMap::try_from_apiset_section_bytes(§ion_bytes.get_data())?; + + let mut target_map: HashMap> = HashMap::new(); + for entry in api_set_map.namespace_entries()? { + let alternative_name = entry.name()?.to_string_lossy(); + for value_entry in entry.value_entries()? { + // TODO: In cases where alt -> kernel32.dll -> kernelbase.dll we currently associate + // TODO: with kernel32.dll as its assumed there is a wrapper function that calls into + // TODO: kernelbase.dll. This keeps us from having to validate against both, in the case + // TODO: of kernelbase.dll being before the function was moved there. + let _forwarder_name = value_entry.name()?.to_string_lossy(); + let target_name = value_entry.value()?.to_string_lossy(); + target_map + .entry(target_name) + .or_default() + .insert(alternative_name.clone()); + } + } + + // Instead of using the view, we use the user-provided platform, the reason is because the + // 'apisetschema.dll' is shared across multiple archs, and we need to be able to merge its data + // with other platforms so that they get the correct alternative names. + let platform = self.default_platform()?; + let mut mapping_type_libraries = Vec::new(); + for (target_name, alternative_names) in target_map { + let type_library = TypeLibrary::new(platform.arch(), &target_name); + for alt_name in alternative_names { + type_library.add_alternate_name(&alt_name); + } + mapping_type_libraries.push(type_library); + } + + Ok(ProcessedData::new(mapping_type_libraries)) + } + + /// We want to be able to process already created type libraries so that they can be consulted + /// during the [`ProcessedData::merge`] step. This lets us add overrides like extra platforms. + pub fn process_type_library(&self, path: &Path) -> Result { + self.state.set_file_state(path.to_owned(), false); + let finalized_type_library = TypeLibrary::load_from_file(&path) + .ok_or_else(|| ProcessingError::InvalidTypeLibrary(path.to_owned()))?; + self.state.set_file_state(path.to_owned(), true); + Ok(ProcessedData::new(vec![finalized_type_library])) + } + + pub fn process_source(&self, path: &Path) -> Result { + self.state.set_file_state(path.to_owned(), false); + let platform = self.default_platform()?; + let parser = + CoreTypeParser::parser_by_name("ClangTypeParser").expect("Failed to get clang parser"); + let platform_type_container = platform.type_container(); + + let header_contents = + std::fs::read_to_string(path).map_err(|e| ProcessingError::FileRead(e))?; + + let file_name = path + .file_name() + .unwrap_or(OsStr::new("source.hpp")) + .to_string_lossy(); + // TODO: Allow specifying options? + let mut include_dirs = self.include_directories.clone(); + if let Some(p) = path.parent() { + include_dirs.push(p.to_owned()); + } + let parsed_types = parser + .parse_types_from_source( + &header_contents, + &file_name, + &platform, + &platform_type_container, + &[], + &include_dirs, + "", + ) + .map_err(|e| ProcessingError::TypeParsingFailed(e))?; + + let type_library = TypeLibrary::new(platform.arch(), &self.default_dependency_name); + type_library.add_platform(&platform); + for ty in parsed_types.types { + type_library.add_named_type(ty.name, &ty.ty); + } + for func in parsed_types.functions { + type_library.add_named_object(func.name, &func.ty); + } + self.state.set_file_state(path.to_owned(), true); + Ok(ProcessedData::new(vec![type_library])) + } + + /// Unlike [`TypeLibProcessor::process_source`] which can pass include directories, this processing + /// requires us to actually load multiple files to parse the correct information. + /// + /// A specific example of this is the "Windows.Wdk.winmd" references types in "Windows.Win32.winmd". + /// If we did not process them together, we would have unresolved references when loading kernel + /// type libraries. + pub fn process_winmd(&self, paths: &[PathBuf]) -> Result { + for path in paths { + self.state.set_file_state(path.to_owned(), false); + } + let platform = self.default_platform()?; + let type_libraries = WindowsMetadataImporter::new() + .with_files(&paths) + .map_err(ProcessingError::WinMdFailedImport)? + .import(&platform) + .map_err(ProcessingError::WinMdFailedImport)?; + for path in paths { + self.state.set_file_state(path.to_owned(), true); + } + Ok(ProcessedData::new(type_libraries)) + } + + pub fn default_platform(&self) -> Result, ProcessingError> { + Platform::by_name(&self.default_platform_name) + .ok_or_else(|| ProcessingError::PlatformNotFound(self.default_platform_name.clone())) + } +} + +pub fn is_parsable(path: &Path) -> bool { + if binaryninja::is_database(path) { + return true; + } + let mut metadata = FileMetadata::with_file_path(&path); + let Ok(view) = BinaryView::from_path(&mut metadata, path) else { + return false; + }; + // If any view type parses this file, consider it for this source. + // All files will have a "Raw" file type, so we account for that. + BinaryViewType::list_valid_types_for(&view).len() > 1 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_parsable() { + let _session = + binaryninja::headless::Session::new().expect("Failed to create headless session"); + let data_dir = Path::new(&env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("Cargo workspace directory") + .join("data"); + let x86_file_path = data_dir.join("x86").join("mfc42.dll.bndb"); + assert!(x86_file_path.exists()); + assert!(is_parsable(&x86_file_path)); + let header_file_path = data_dir.join("headers").join("test.h"); + assert!(header_file_path.exists()); + assert!(!is_parsable(&header_file_path)); + } + + #[test] + fn test_process_winmd() { + let _session = + binaryninja::headless::Session::new().expect("Failed to create headless session"); + let data_dir = Path::new(&env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("Cargo workspace directory") + .join("data"); + let win32_winmd_path = data_dir.join("winmd").join("Windows.Win32.winmd"); + assert!(win32_winmd_path.exists()); + let wdk_winmd_path = data_dir.join("winmd").join("Windows.Wdk.winmd"); + assert!(wdk_winmd_path.exists()); + + let processor = TypeLibProcessor::new("foo", "windows-x86_64"); + let processed_data = processor + .process_winmd(&[win32_winmd_path, wdk_winmd_path]) + .expect("Failed to process winmd"); + assert_eq!(processed_data.type_libraries.len(), 591); + + // Make sure processing a directory will correctly group winmd files. + let processed_folder_data = processor + .process_directory(&data_dir.join("winmd")) + .expect("Failed to process directory"); + assert_eq!(processed_folder_data.type_libraries.len(), 591); + } + + #[test] + fn test_process_source() { + let _session = + binaryninja::headless::Session::new().expect("Failed to create headless session"); + let data_dir = Path::new(&env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("Cargo workspace directory") + .join("data"); + let header_file_path = data_dir.join("headers").join("test.h"); + assert!(header_file_path.exists()); + + let processor = TypeLibProcessor::new("test.dll", "windows-x86_64"); + let processed_data = processor + .process_source(&header_file_path) + .expect("Failed to process source"); + assert_eq!(processed_data.type_libraries.len(), 1); + let processed_library = &processed_data.type_libraries[0]; + assert_eq!(processed_library.name(), "test.dll"); + assert_eq!(processed_library.dependency_name(), "test.dll"); + assert_eq!( + processed_library.platform_names().to_vec(), + vec!["windows-x86_64"] + ); + + processed_library + .get_named_type("MyStruct".into()) + .expect("Failed to get type"); + + // Make sure includes are pulled into the type library. + let header2_file_path = data_dir.join("headers").join("test2.hpp"); + let processed_data_2 = processor + .process_source(&header2_file_path) + .expect("Failed to process source"); + assert_eq!(processed_data_2.type_libraries.len(), 1); + let processed_library_2 = &processed_data_2.type_libraries[0]; + assert_eq!(processed_library_2.name(), "test.dll"); + assert_eq!(processed_library_2.dependency_name(), "test.dll"); + assert_eq!( + processed_library_2.platform_names().to_vec(), + vec!["windows-x86_64"] + ); + processed_library_2 + .get_named_type("MyStruct2".into()) + .expect("Failed to get type"); + processed_library_2 + .get_named_type("MyStruct".into()) + .expect("Failed to get included type"); + } + + #[test] + fn test_process_file() { + let _session = + binaryninja::headless::Session::new().expect("Failed to create headless session"); + let data_dir = Path::new(&env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("Cargo workspace directory") + .join("data"); + let x86_file_path = data_dir.join("x86_64").join("mfc42.dll.bndb"); + assert!(x86_file_path.exists()); + let processor = TypeLibProcessor::new("mfc42.dll", "windows-x86_64"); + let processed_data = processor + .process_file(&x86_file_path) + .expect("Failed to process file"); + assert_eq!(processed_data.type_libraries.len(), 27); + let processed_library = processed_data + .type_libraries + .iter() + .find(|lib| lib.name() == "mfc42.dll") + .expect("Failed to find mfc42.dll library"); + assert_eq!(processed_library.name(), "mfc42.dll"); + assert_eq!(processed_library.dependency_name(), "mfc42.dll"); + assert_eq!( + processed_library.platform_names().to_vec(), + vec!["windows-x86_64"] + ); + } + + #[test] + fn test_process_api_set() { + let _session = + binaryninja::headless::Session::new().expect("Failed to create headless session"); + let data_dir = Path::new(&env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("Cargo workspace directory") + .join("data"); + let apiset_file_path = data_dir.join("apiset").join("apisetschema.dll"); + assert!(apiset_file_path.exists()); + let processor = TypeLibProcessor::new("foo", "windows-x86_64"); + let processed_data = processor + .process_file(&apiset_file_path) + .expect("Failed to process file"); + + assert_eq!(processed_data.type_libraries.len(), 287); + let combase_library = processed_data + .type_libraries + .iter() + .find(|tl| tl.name() == "combase.dll") + .expect("Failed to find combase.dll type library"); + assert_eq!( + combase_library.alternate_names().to_vec(), + vec![ + "api-ms-win-core-com-l1-1-3", + "api-ms-win-core-com-midlproxystub-l1-1-0", + "api-ms-win-core-com-private-l1-1-1", + "api-ms-win-core-com-private-l1-2-0", + "api-ms-win-core-com-private-l1-3-1", + "api-ms-win-core-marshal-l1-1-0", + "api-ms-win-core-winrt-error-l1-1-1", + "api-ms-win-core-winrt-errorprivate-l1-1-1", + "api-ms-win-core-winrt-l1-1-0", + "api-ms-win-core-winrt-registration-l1-1-0", + "api-ms-win-core-winrt-roparameterizediid-l1-1-0", + "api-ms-win-core-winrt-string-l1-1-1", + "api-ms-win-downlevel-ole32-l1-1-0" + ] + ); + } + + #[test] + fn test_data_merging() { + let _session = + binaryninja::headless::Session::new().expect("Failed to create headless session"); + let x86_platform = Platform::by_name("x86").expect("Failed to get x86 platform"); + let x86_windows_platform = + Platform::by_name("windows-x86").expect("Failed to get windows x86 platform"); + // Make two type libraries with the same name, but different dependencies. + let tl1 = TypeLibrary::new(x86_platform.arch(), "foo"); + tl1.set_dependency_name("foo"); + tl1.add_platform(&x86_platform); + tl1.add_named_type("bar".into(), &Type::named_float(3, "bla")); + let tl1_data = ProcessedData::new(vec![tl1]); + + let tl2 = TypeLibrary::new(x86_platform.arch(), "bar"); + tl2.set_dependency_name("foo"); + tl2.add_platform(&x86_windows_platform); + tl2.add_named_type("baz".into(), &Type::named_int(64, false, "fre")); + let tl2_data = ProcessedData::new(vec![tl2]); + + let merged_data = ProcessedData::merge(&[tl1_data, tl2_data]); + assert_eq!(merged_data.type_libraries.len(), 1); + let merged_tl = &merged_data.type_libraries[0]; + assert_eq!(merged_tl.name(), "foo"); + assert_eq!(merged_tl.dependency_name(), "foo"); + assert_eq!(merged_tl.platform_names().len(), 2); + assert_eq!(merged_tl.named_types().len(), 2); + } +} diff --git a/plugins/bntl_utils/src/schema.rs b/plugins/bntl_utils/src/schema.rs new file mode 100644 index 000000000..ff6d40892 --- /dev/null +++ b/plugins/bntl_utils/src/schema.rs @@ -0,0 +1,58 @@ +use binaryninja::types::TypeLibrary; +use serde::Deserialize; +use std::collections::{HashMap, HashSet}; +use std::fs::File; + +#[derive(Deserialize, Debug)] +pub struct BntlSchema { + // The list of library names this library depends on + pub dependencies: Vec, + // Maps internal type IDs or names to their external sources + pub type_sources: Vec, +} + +impl BntlSchema { + pub fn from_file(file: &File) -> Self { + serde_json::from_reader(file).expect("JSON schema mismatch") + } + + pub fn from_path(path: &std::path::Path) -> Self { + match path.extension() { + Some(ext) if ext == "json" => { + Self::from_file(&File::open(path).expect("Failed to open schema file")) + } + Some(ext) if ext == "bntl" => { + // Need to decompress first + let out_path = path.with_extension("json"); + // TODO: Need a way to decompress without writing to disk? e.g. write uncompressed. + if !TypeLibrary::decompress_to_file(path, &out_path) { + panic!("Failed to decompress type library"); + } + Self::from_file( + &File::open(out_path).expect("Failed to open decompressed schema file"), + ) + } + _ => panic!("Invalid schema file extension"), + } + } + + pub fn to_source_map(&self) -> HashMap> { + let mut dependencies_map: HashMap> = HashMap::new(); + for ts in &self.type_sources { + let full_name = ts.name.join("::"); + dependencies_map + .entry(ts.source.clone()) + .or_default() + .insert(full_name); + } + dependencies_map + } +} + +#[derive(Deserialize, Debug)] +pub struct TypeSource { + // The components of the name, e.g., ["std", "string"] + pub name: Vec, + // The name of the dependency library it comes from + pub source: String, +} diff --git a/plugins/bntl_utils/src/templates/validate.html b/plugins/bntl_utils/src/templates/validate.html new file mode 100644 index 000000000..e37af45ba --- /dev/null +++ b/plugins/bntl_utils/src/templates/validate.html @@ -0,0 +1,101 @@ + + + + {# palette() is reading from the QT style sheet FYI #} + + + + +
+

Type Library Validation Report

+
+ +{% for issue in issues %} +
+ + {% if issue.DuplicateGUID %} + Duplicate GUID + The GUID {{ issue.DuplicateGUID.guid }} is already used by {{ issue.DuplicateGUID.existing_library }}. + + {% elif issue.DuplicateDependencyName %} + Dependency Name Collision + The name {{ issue.DuplicateDependencyName.name }} is already provided by {{ issue.DuplicateDependencyName.existing_library }}. + + {% elif issue.InvalidMetadata %} + Invalid Metadata + Key: {{ issue.InvalidMetadata.key }} | Issue: {{ issue.InvalidMetadata.issue }} + + {% elif issue.DuplicateOrdinal %} + Duplicate Ordinal + Ordinal #{{ issue.DuplicateOrdinal.ordinal }} is assigned to {{ issue.DuplicateOrdinal.existing_name }} and {{ issue.DuplicateOrdinal.duplicate_name }}. + + {% elif issue.NoPlatform %} + Missing Platform + The type library has no target platform associated with it. + + {% elif issue.UnresolvedExternalReference %} + Unresolved External Reference + Type {{ issue.UnresolvedExternalReference.name }} (in {{ issue.UnresolvedExternalReference.container }}) has no source. + + {% elif issue.UnresolvedSourceReference %} + Unresolved Source Reference + Type {{ issue.UnresolvedSourceReference.name }} was not found in expected source {{ issue.UnresolvedSourceReference.source }}. + + {% elif issue.UnresolvedTypeLibrary %} + Unresolved Type Library + Could not find dependency library file for {{ issue.UnresolvedTypeLibrary.name }}. + {% endif %} + +
+{% endfor %} + + + \ No newline at end of file diff --git a/plugins/bntl_utils/src/url.rs b/plugins/bntl_utils/src/url.rs new file mode 100644 index 000000000..16fe75d20 --- /dev/null +++ b/plugins/bntl_utils/src/url.rs @@ -0,0 +1,345 @@ +use binaryninja::collaboration::{RemoteFile, RemoteFolder, RemoteProject}; +use binaryninja::rc::Ref; +use std::fmt::Display; +use std::path::PathBuf; +use thiserror::Error; +use url::Url; +use uuid::Uuid; + +#[derive(Error, Debug, PartialEq)] +pub enum BnUrlParsingError { + #[error("Invalid URL format: {0}")] + UrlParseError(#[from] url::ParseError), + + #[error("Invalid scheme: expected 'binaryninja', found '{0}'")] + InvalidScheme(String), + + #[error("Invalid Enterprise path: missing server or project GUID")] + InvalidEnterprisePath, + + #[error("Invalid server URL in enterprise path")] + InvalidServerUrl, + + #[error("Invalid UUID: {0}")] + InvalidUuid(#[from] uuid::Error), + + #[error("Unknown or unsupported URL format")] + UnknownFormat, +} + +#[derive(Error, Debug)] +pub enum BnResourceError { + #[error("Enterprise server not found for address: {0}")] + RemoteNotFound(String), + + #[error("Remote connection error: {0}")] + RemoteConnectionError(String), + + #[error("Project not found with GUID: {0}")] + ProjectNotFound(String), + + #[error("Project resource not found with GUID: {0}")] + ItemNotFound(String), + + #[error("Local filesystem error: {0}")] + IoError(#[from] std::io::Error), +} + +#[derive(Debug, Clone)] +pub enum BnResource { + RemoteProject(Ref), + RemoteProjectFile(Ref), + RemoteProjectFolder(Ref), + /// A remote file. + RemoteFile(Url), + /// A regular file on the local filesystem. + LocalFile(PathBuf), +} + +// TODO: Make the BnUrl from this. +impl Display for BnResource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BnResource::RemoteProject(project) => write!(f, "RemoteProject({})", project.id()), + BnResource::RemoteProjectFile(file) => write!(f, "RemoteFile({})", file.id()), + BnResource::RemoteProjectFolder(folder) => write!(f, "RemoteFolder({})", folder.id()), + BnResource::RemoteFile(url) => write!(f, "RemoteFile({})", url), + BnResource::LocalFile(path) => write!(f, "LocalFile({})", path.display()), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum BnParsedUrlKind { + Enterprise { + server: Url, + project_guid: Uuid, + /// Optional GUID of the project item, currently can be a folder or a file. + item_guid: Option, + }, + // TODO: Local projects? + RemoteFile(Url), + LocalFile(PathBuf), +} + +#[derive(Debug, Clone)] +pub struct BnParsedUrl { + pub kind: BnParsedUrlKind, + pub expression: Option, +} + +impl BnParsedUrl { + pub fn parse(input: &str) -> Result { + let parsed = Url::parse(input)?; + if parsed.scheme() != "binaryninja" { + return Err(BnUrlParsingError::InvalidScheme( + parsed.scheme().to_string(), + )); + } + + let expression = parsed + .query_pairs() + .find(|(k, _)| k == "expr") + .map(|(_, v)| v.into_owned()); + + let kind = match parsed.host_str() { + // TODO: This should really go down the same path as the remote file parsing, if it + // TODO: matches the host of an enterprise server... But that requires us to change how + // TODO: the core outputs these enterprise URLs... + // Case: binaryninja://enterprise/... + Some("enterprise") => { + let segments: Vec<&str> = + parsed.path().split('/').filter(|s| !s.is_empty()).collect(); + + if segments.len() < 3 { + return Err(BnUrlParsingError::InvalidEnterprisePath); + } + + let (server_parts, resource_parts) = if segments.len() >= 4 { + ( + &segments[..segments.len() - 2], + &segments[segments.len() - 2..], + ) + } else { + ( + &segments[..segments.len() - 1], + &segments[segments.len() - 1..], + ) + }; + + BnParsedUrlKind::Enterprise { + server: Url::parse(&server_parts.join("/")) + .map_err(|_| BnUrlParsingError::InvalidServerUrl)?, + project_guid: Uuid::parse_str(resource_parts[0])?, + item_guid: resource_parts + .get(1) + .map(|s| Uuid::parse_str(s)) + .transpose()?, + } + } + // Case: binaryninja:///bin/ls + None | Some("") + if parsed.path().starts_with('/') && !parsed.path().starts_with("/https") => + { + BnParsedUrlKind::LocalFile(PathBuf::from(parsed.path())) + } + // Case: binaryninja:https://... + _ => { + let path = parsed.path(); + if path.starts_with("https:/") || path.starts_with("http:/") { + let nested_url = path.replacen(":/", "://", 1); + BnParsedUrlKind::RemoteFile( + Url::parse(&nested_url).map_err(BnUrlParsingError::UrlParseError)?, + ) + } else { + return Err(BnUrlParsingError::UnknownFormat); + } + } + }; + + Ok(BnParsedUrl { kind, expression }) + } + + pub fn to_resource(&self) -> Result { + match &self.kind { + BnParsedUrlKind::Enterprise { + server, + project_guid, + item_guid, + } => { + // NOTE: We must strip the trailing slash from the server URL, because the core will + // not accept it otherwise, we should probably have a fuzzy get_remote_by_address here, + // so we can accept either with or without the trailing slash, but for now we'll just + // strip it. + let server_addr = server.as_str().strip_suffix('/').unwrap_or(server.as_str()); + let remote = binaryninja::collaboration::get_remote_by_address(server_addr) + .ok_or_else(|| BnResourceError::RemoteNotFound(server_addr.to_string()))?; + if !remote.is_connected() { + remote.connect().map_err(|_| { + BnResourceError::RemoteConnectionError(server_addr.to_string()) + })?; + } + + let project = remote + .get_project_by_id(&project_guid.to_string()) + .ok() + .flatten() + .ok_or_else(|| BnResourceError::ProjectNotFound(project_guid.to_string()))?; + + match item_guid { + Some(item_guid) => { + let item_guid_str = item_guid.to_string(); + + // Check if it's a folder first + if let Some(folder) = + project.get_folder_by_id(&item_guid_str).ok().flatten() + { + return Ok(BnResource::RemoteProjectFolder(folder)); + } + + // Then check if it's a file + let file = project + .get_file_by_id(&item_guid_str) + .ok() + .flatten() + .ok_or_else(|| BnResourceError::ItemNotFound(item_guid_str))?; + + Ok(BnResource::RemoteProjectFile(file)) + } + None => Ok(BnResource::RemoteProject(project)), + } + } + BnParsedUrlKind::RemoteFile(remote_url) => { + Ok(BnResource::RemoteFile(remote_url.clone())) + } + BnParsedUrlKind::LocalFile(local_path) => Ok(BnResource::LocalFile(local_path.clone())), + } + } +} + +impl Display for BnParsedUrl { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.kind { + BnParsedUrlKind::Enterprise { + server, + project_guid, + item_guid, + } => write!( + f, + "binaryninja://enterprise/{}/{}{}", + server.as_str().strip_suffix('/').unwrap_or(server.as_str()), + project_guid, + item_guid + .map(|guid| format!("/{}", guid)) + .unwrap_or_default() + ), + BnParsedUrlKind::RemoteFile(remote_url) => write!(f, "{}", remote_url), + BnParsedUrlKind::LocalFile(local_path) => { + write!(f, "binaryninja:///{}", local_path.display()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_enterprise_full() { + let input = "binaryninja://enterprise/https://enterprise.test.com/0268b954-0d7b-41c3-a603-960a59fdd0f7/0268b954-0d7b-41c3-a603-960a59fdd0f6?expr=sub_1234"; + let action = BnParsedUrl::parse(input).unwrap(); + + if let BnParsedUrlKind::Enterprise { + server, + project_guid, + item_guid: project_file, + } = action.kind + { + assert_eq!(server.as_str(), "https://enterprise.test.com/"); + assert_eq!( + project_guid, + Uuid::parse_str("0268b954-0d7b-41c3-a603-960a59fdd0f7").unwrap() + ); + assert_eq!( + project_file, + Some(Uuid::parse_str("0268b954-0d7b-41c3-a603-960a59fdd0f6").unwrap()) + ); + } else { + panic!("Wrong target type"); + } + assert_eq!(action.expression, Some("sub_1234".to_string())); + } + + #[test] + fn test_parse_enterprise_no_file() { + let input = "binaryninja://enterprise/https://enterprise.test.com/0268b954-0d7b-41c3-a603-960a59fdd0f7/"; + let action = BnParsedUrl::parse(input).unwrap(); + + if let BnParsedUrlKind::Enterprise { + project_guid, + item_guid: project_file, + .. + } = action.kind + { + assert_eq!( + project_guid, + Uuid::parse_str("0268b954-0d7b-41c3-a603-960a59fdd0f7").unwrap() + ); + assert_eq!(project_file, None); + } else { + panic!("Wrong target type"); + } + } + + #[test] + fn test_parse_remote_file() { + let input = "binaryninja:https://captf.com/2015/plaidctf/pwnable/datastore.elf?expr=main"; + let action = BnParsedUrl::parse(input).unwrap(); + + match action.kind { + BnParsedUrlKind::RemoteFile(url) => { + assert_eq!(url.host_str(), Some("captf.com")); + assert!(url.path().ends_with("datastore.elf")); + } + _ => panic!("Expected RemoteFile"), + } + assert_eq!(action.expression, Some("main".to_string())); + } + + #[test] + fn test_parse_local_file() { + let input = "binaryninja:///bin/ls?expr=sub_2830"; + let action = BnParsedUrl::parse(input).unwrap(); + + match action.kind { + BnParsedUrlKind::LocalFile(path) => assert_eq!(path.to_string_lossy(), "/bin/ls"), + _ => panic!("Expected LocalFile"), + } + assert_eq!(action.expression, Some("sub_2830".to_string())); + } + + #[test] + fn test_invalid_scheme() { + let input = "https://google.com"; + let result = BnParsedUrl::parse(input); + assert!(matches!(result, Err(BnUrlParsingError::InvalidScheme(_)))); + } + + #[test] + fn test_missing_enterprise_guid() { + let input = "binaryninja://enterprise/https://internal.us/"; + let result = BnParsedUrl::parse(input); + assert_eq!( + result.unwrap_err(), + BnUrlParsingError::InvalidEnterprisePath + ); + } + + #[test] + fn test_invalid_uuid_format() { + let input = "binaryninja://enterprise/https://internal.us/not-a-uuid/"; + let result = BnParsedUrl::parse(input); + assert!(matches!(result, Err(BnUrlParsingError::InvalidUuid(_)))); + } +} diff --git a/plugins/bntl_utils/src/validate.rs b/plugins/bntl_utils/src/validate.rs new file mode 100644 index 000000000..3c863c641 --- /dev/null +++ b/plugins/bntl_utils/src/validate.rs @@ -0,0 +1,341 @@ +use crate::schema::BntlSchema; +use binaryninja::platform::Platform; +use binaryninja::qualified_name::QualifiedName; +use binaryninja::rc::Ref; +use binaryninja::types::TypeLibrary; +use minijinja::{context, Environment}; +use serde::Serialize; +use std::collections::{HashMap, HashSet}; +use std::env::temp_dir; +use std::fmt::Display; + +#[derive(Debug, PartialEq, PartialOrd, Clone, Eq, Hash, Serialize)] +pub enum ValidateIssue { + DuplicateGUID { + guid: String, + existing_library: String, + }, + DuplicateDependencyName { + name: String, + existing_library: String, + }, + InvalidMetadata { + key: String, + issue: String, + }, + DuplicateOrdinal { + ordinal: u64, + existing_name: String, + duplicate_name: String, + }, + NoPlatform, + UnresolvedExternalReference { + name: String, + container: String, + }, + UnresolvedSourceReference { + name: String, + source: String, + }, + UnresolvedTypeLibrary { + name: String, + }, // TODO: Overlapping type name of platform? + // TODO: E.g. a type is found in the type library, and also in the platform. +} + +impl Display for ValidateIssue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ValidateIssue::DuplicateGUID { + guid, + existing_library, + } => { + write!( + f, + "Duplicate GUID: '{}' is already used by library '{}'", + guid, existing_library + ) + } + ValidateIssue::DuplicateDependencyName { + name, + existing_library, + } => { + write!( + f, + "Duplicate Dependency Name: '{}' is already provided by '{}'", + name, existing_library + ) + } + ValidateIssue::InvalidMetadata { key, issue } => { + write!(f, "Invalid Metadata: Key '{}' - {}", key, issue) + } + ValidateIssue::DuplicateOrdinal { + ordinal, + existing_name, + duplicate_name, + } => { + write!( + f, + "Duplicate Ordinal: #{} is assigned to both '{}' and '{}'", + ordinal, existing_name, duplicate_name + ) + } + ValidateIssue::NoPlatform => { + write!( + f, + "Missing Platform: The type library has no target platform associated with it" + ) + } + ValidateIssue::UnresolvedExternalReference { name, container } => { + write!( + f, + "Unresolved External Reference: Type '{}' referenced inside '{}' is marked as external but has no source", + name, container + ) + } + ValidateIssue::UnresolvedSourceReference { name, source } => { + write!( + f, + "Unresolved Source Reference: Type '{}' expects source '{}', but it wasn't found there", + name, source + ) + } + ValidateIssue::UnresolvedTypeLibrary { name } => { + write!( + f, + "Unresolved Type Library: Could not find dependency library file for '{}'", + name + ) + } + } + } +} + +#[derive(Debug, Default)] +pub struct ValidateResult { + pub issues: Vec, +} + +impl ValidateResult { + /// Render the validation report as HTML. + pub fn render_report(&self) -> Result { + let mut environment = Environment::new(); + // Remove trailing lines for blocks, this is required for Markdown tables. + environment.set_trim_blocks(true); + minijinja_embed::load_templates!(&mut environment); + let tmpl = environment.get_template("validate.html")?; + tmpl.render(context!(issues => self.issues)) + } +} + +#[derive(Debug, Default, Clone)] +pub struct TypeLibValidater { + pub seen_guids: HashMap, + // TODO: This needs to be by platform as well. + pub seen_dependency_names: HashMap, + /// These are the type libraries that are accessible to the type library under validation. + /// + /// Used to validate external references. + pub type_libraries: Vec>, + /// Built from the available type libraries. + pub valid_external_references: HashSet, +} + +impl TypeLibValidater { + pub fn new() -> Self { + Self { + seen_guids: HashMap::new(), + seen_dependency_names: HashMap::new(), + type_libraries: Vec::new(), + valid_external_references: HashSet::new(), + } + } + + /// These are the type libraries that are accessible to the type library under validation. + /// + /// Used to validate external references. + pub fn with_type_libraries(mut self, type_libraries: Vec>) -> Self { + self.type_libraries = type_libraries; + for type_lib in &self.type_libraries { + for ty in &type_lib.named_types() { + self.valid_external_references.insert(ty.name); + } + for obj in &type_lib.named_objects() { + self.valid_external_references.insert(obj.name); + } + } + self + } + + /// The platform that is accessible to the type library under validation. + /// + /// Used to validate external references. + pub fn with_platform(mut self, platform: &Platform) -> Self { + for ty in &platform.types() { + self.valid_external_references.insert(ty.name); + } + self + } + + pub fn validate(&mut self, type_lib: &TypeLibrary) -> ValidateResult { + let mut result = ValidateResult::default(); + + if type_lib.platform_names().is_empty() { + result.issues.push(ValidateIssue::NoPlatform); + } + + if let Some(issue) = self.validate_guid(type_lib) { + result.issues.push(issue); + } + + if let Some(issue) = self.validate_dependency_name(type_lib) { + result.issues.push(issue); + } + + result.issues.extend(self.validate_ordinals(type_lib)); + result + .issues + .extend(self.validate_external_references(type_lib)); + + // TODO: This is currently disabled because it's too slow. + // result.issues.extend(self.validate_source_files(type_lib)); + + result + } + + pub fn validate_guid(&mut self, type_lib: &TypeLibrary) -> Option { + match self.seen_guids.insert(type_lib.guid(), type_lib.name()) { + None => None, + Some(existing_library) => Some(ValidateIssue::DuplicateGUID { + guid: type_lib.guid(), + existing_library, + }), + } + } + + pub fn validate_dependency_name(&mut self, type_lib: &TypeLibrary) -> Option { + match self + .seen_dependency_names + .insert(type_lib.dependency_name(), type_lib.name()) + { + None => None, + Some(existing_library) => Some(ValidateIssue::DuplicateDependencyName { + name: type_lib.dependency_name(), + existing_library, + }), + } + } + + pub fn validate_source_files(&self, type_lib: &TypeLibrary) -> Vec { + let mut issues = Vec::new(); + let tmp_type_lib_path = temp_dir().join(type_lib.name()); + type_lib.write_to_file(&tmp_type_lib_path); + let schema = BntlSchema::from_path(&tmp_type_lib_path); + for (src, types) in schema.to_source_map() { + let Some(dep_type_lib) = self.type_libraries.iter().find(|tl| tl.name() == src) else { + issues.push(ValidateIssue::UnresolvedTypeLibrary { + name: src.to_string(), + }); + continue; + }; + + for ty in &types { + let qualified_name = QualifiedName::from(ty); + let is_named_ty = dep_type_lib + .get_named_type(qualified_name.clone()) + .is_none(); + let is_named_obj = dep_type_lib.get_named_object(qualified_name).is_none(); + if !is_named_ty && !is_named_obj { + issues.push(ValidateIssue::UnresolvedSourceReference { + name: ty.to_string(), + source: src.to_string(), + }); + } + } + } + issues + } + + pub fn validate_external_references(&self, type_lib: &TypeLibrary) -> Vec { + let mut issues = Vec::new(); + for ty in &type_lib.named_types() { + crate::helper::visit_type_reference(&ty.ty, &mut |ntr| { + if !self.valid_external_references.contains(&ntr.name()) { + issues.push(ValidateIssue::UnresolvedExternalReference { + name: ntr.name().to_string(), + container: ty.name.to_string(), + }); + } + }) + } + for obj in &type_lib.named_objects() { + crate::helper::visit_type_reference(&obj.ty, &mut |ntr| { + if !self.valid_external_references.contains(&ntr.name()) { + issues.push(ValidateIssue::UnresolvedExternalReference { + name: ntr.name().to_string(), + container: obj.name.to_string(), + }); + } + }) + } + issues + } + + pub fn validate_ordinals(&self, type_lib: &TypeLibrary) -> Vec { + let Some(metadata_key_md) = type_lib.query_metadata("metadata") else { + return vec![]; + }; + let Some(metadata_key_str) = metadata_key_md.get_string() else { + return vec![ValidateIssue::InvalidMetadata { + key: "metadata".to_owned(), + issue: "Expected string".to_owned(), + }]; + }; + + let Some(metadata_map_md) = type_lib.query_metadata(&metadata_key_str.to_string_lossy()) + else { + return vec![ValidateIssue::InvalidMetadata { + key: metadata_key_str.to_string_lossy().to_string(), + issue: "Missing metadata map key".to_owned(), + }]; + }; + + let Some(metadata_map) = metadata_map_md.get_value_store() else { + return vec![ValidateIssue::InvalidMetadata { + key: metadata_key_str.to_string_lossy().to_string(), + issue: "Expected value store".to_owned(), + }]; + }; + + let mut discovered_ordinals = HashMap::new(); + let mut issues = Vec::new(); + for (key, value) in metadata_map.iter() { + let Ok(ordinal_num) = key.parse::() else { + issues.push(ValidateIssue::InvalidMetadata { + key: key.to_string(), + issue: "Expected ordinal number".to_owned(), + }); + continue; + }; + + let Some(value_bn_str) = value.get_string() else { + issues.push(ValidateIssue::InvalidMetadata { + key: key.to_string(), + issue: "Expected string".to_owned(), + }); + continue; + }; + let value_str = value_bn_str.to_string_lossy().to_string(); + + match discovered_ordinals.insert(ordinal_num, value_str.clone()) { + None => (), + Some(existing_ordinal) => issues.push(ValidateIssue::DuplicateOrdinal { + ordinal: ordinal_num, + existing_name: existing_ordinal, + duplicate_name: value_str, + }), + } + } + issues + } +} diff --git a/plugins/bntl_utils/src/winmd.rs b/plugins/bntl_utils/src/winmd.rs new file mode 100644 index 000000000..7622fe891 --- /dev/null +++ b/plugins/bntl_utils/src/winmd.rs @@ -0,0 +1,594 @@ +//! Import windows metadata types into a Binary Ninja type library. + +use std::collections::HashMap; +use std::num::NonZeroUsize; +use std::path::PathBuf; +use thiserror::Error; + +use binaryninja::architecture::Architecture; +use binaryninja::platform::Platform; +use binaryninja::qualified_name::QualifiedName; +use binaryninja::rc::Ref; +use binaryninja::types::{ + EnumerationBuilder, FunctionParameter, MemberAccess, MemberScope, NamedTypeReference, + NamedTypeReferenceClass, StructureBuilder, StructureType, Type, TypeBuilder, TypeLibrary, +}; + +use info::{LibraryName, MetadataFunctionInfo, MetadataInfo, MetadataTypeInfo, MetadataTypeKind}; + +pub mod info; +pub mod translate; + +#[derive(Error, Debug)] +pub enum ImportError { + #[error("no files were provided")] + NoFiles, + #[error("the type name '{0}' is not handled")] + UnhandledType(String), + #[error("failed to translate windows metadata")] + TransactionError(#[from] translate::TranslationError), + #[error("the type '{0}' has an unhandled size")] + UnhandledTypeSize(&'static str), +} + +#[derive(Debug)] +pub struct WindowsMetadataImporter { + info: MetadataInfo, + // TODO: If we can replace / add this with type libraries we can make multi-pass importer. + type_lookup: HashMap<(String, String), MetadataTypeInfo>, + address_size: usize, + integer_size: usize, +} + +impl WindowsMetadataImporter { + pub fn new() -> Self { + Self { + info: MetadataInfo::default(), + type_lookup: HashMap::new(), + address_size: 8, + integer_size: 8, + } + } + + #[allow(dead_code)] + pub fn new_with_info(info: MetadataInfo) -> Self { + let mut res = Self::new(); + res.info = info; + res.build_type_lookup(); + res + } + + pub fn with_files(mut self, paths: &[PathBuf]) -> Result { + let mut files = Vec::new(); + for path in paths { + let file = windows_metadata::reader::File::read(path).expect("Failed to read file"); + files.push(file); + } + self.info = translate::WindowsMetadataTranslator::new().translate(files)?; + // We updated info, so we must rebuild the lookup table. + self.build_type_lookup(); + Ok(self) + } + + pub fn with_platform(mut self, platform: &Platform) -> Self { + // TODO: platform.address_size() + self.address_size = platform.arch().address_size(); + self.integer_size = platform.arch().default_integer_size(); + self + } + + /// Build the lookup table for us to use when referencing types. + /// + /// Should be called anytime we update `self.info`. + fn build_type_lookup(&mut self) { + for ty in &self.info.types { + if let Some(_existing) = self + .type_lookup + .insert((ty.namespace.clone(), ty.name.clone()), ty.clone()) + { + tracing::warn!( + "Duplicate type name '{}' found when building type lookup", + ty.name + ); + } + } + } + + pub fn import(&self, platform: &Platform) -> Result>, ImportError> { + // TODO: We need to take all of these enums and figure out where to put them. + // let mut test = self.info.clone(); + // let constant_enums = test.create_constant_enums(); + // // TODO: Creating zero width enums + // // test.types.extend(constant_enums); + // // let blah: Vec<_> = constant_enums.iter().take(10).collect(); + // // for enum_kind in blah { + // // println!("{:?}", enum_kind); + // // } + let partitioned_info = self.info.partitioned(); + + let mut type_libs = Vec::new(); + for (name, info) in partitioned_info.libraries { + let type_lib_name = match name { + LibraryName::Module(module_name) => module_name.clone(), + LibraryName::Namespace(ns_name) => { + // TODO: We might need to do something different for namespaced type libraries in the future. + ns_name.clone() + } + }; + let til = TypeLibrary::new(platform.arch(), &type_lib_name); + til.add_platform(platform); + til.set_dependency_name(&type_lib_name); + for ty in &info.metadata.types { + self.import_type(&til, &ty)?; + } + for func in &info.metadata.functions { + self.import_function(&til, &func)?; + } + for (name, library_name) in &info.external_references { + let qualified_name = QualifiedName::from(name.clone()); + match library_name { + LibraryName::Namespace(source) => { + // TODO: We might need to do something different for namespaced type libraries in the future. + til.add_type_source(qualified_name, source); + } + LibraryName::Module(source) => { + til.add_type_source(qualified_name, source); + } + } + } + + type_libs.push(til); + } + + Ok(type_libs) + } + + pub fn import_function( + &self, + til: &TypeLibrary, + func: &MetadataFunctionInfo, + ) -> Result<(), ImportError> { + // TODO: Handle ordinals? Ordinals exist in binaries that need to be parsed, maybe we + // TODO: make another handler for that + let qualified_name = QualifiedName::from(func.name.clone()); + let ty = self.convert_type_kind(&func.ty)?; + til.add_named_object(qualified_name, &ty); + Ok(()) + } + + pub fn import_type( + &self, + til: &TypeLibrary, + type_info: &MetadataTypeInfo, + ) -> Result<(), ImportError> { + let qualified_name = QualifiedName::from(type_info.name.clone()); + let ty = self.convert_type_kind(&type_info.kind)?; + til.add_named_type(qualified_name, &ty); + Ok(()) + } + + pub fn convert_type_kind(&self, kind: &MetadataTypeKind) -> Result, ImportError> { + match kind { + MetadataTypeKind::Void => Ok(Type::void()), + MetadataTypeKind::Bool { size: None } => Ok(Type::bool()), + MetadataTypeKind::Bool { size: Some(size) } => { + Ok(TypeBuilder::bool().set_width(*size).finalize()) + } + MetadataTypeKind::Integer { size, is_signed } => { + Ok(Type::int(size.unwrap_or(self.integer_size), *is_signed)) + } + MetadataTypeKind::Character { size: 1 } => Ok(Type::int(1, true)), + MetadataTypeKind::Character { size } => Ok(Type::wide_char(*size)), + MetadataTypeKind::Float { size } => Ok(Type::float(*size)), + MetadataTypeKind::Pointer { + is_const, + is_pointee_const: _is_pointee_const, + target, + } => { + let target_ty = self.convert_type_kind(target)?; + Ok(Type::pointer_of_width( + &target_ty, + self.address_size, + *is_const, + false, + None, + )) + } + MetadataTypeKind::Array { element, count } => { + let element_ty = self.convert_type_kind(element)?; + Ok(Type::array(&element_ty, *count as u64)) + } + MetadataTypeKind::Struct { fields, is_packed } => { + let mut structure = StructureBuilder::new(); + // Current offset in bytes + let mut current_byte_offset = 0usize; + + // TODO: Change how this operates now that we have an is_packed flag. + // Used to add tail padding to satisfy alignment requirements. + let mut max_alignment = 0usize; + // We need to look ahead to figure out when bitfields end and adjust current_byte_offset accordingly. + let mut field_iter = fields.iter().peekable(); + while let Some(field) = field_iter.next() { + let field_ty = self.convert_type_kind(&field.ty)?; + let field_size = self.type_kind_size(&field.ty)?; + let field_alignment = self.type_kind_alignment(&field.ty)?; + max_alignment = max_alignment.max(field_alignment); + if let Some((bit_pos, bit_width)) = field.bitfield { + let current_bit_offset = current_byte_offset * 8; + let field_bit_offset = current_bit_offset + bit_pos as usize; + // TODO: member access and member scope have definitions inside winmd we can use. + structure.insert_bitwise( + &field_ty, + &field.name, + field_bit_offset as u64, + Some(bit_width), + false, + MemberAccess::PublicAccess, + MemberScope::NoScope, + ); + + if let Some(next_field) = field_iter.peek() { + if next_field.bitfield.is_some() { + // Continue as if we are in the same storage unit (no alignment) + current_byte_offset = (current_bit_offset + bit_width as usize) / 8; + } else { + // Find the start of the storage unit. + // if we are at byte 1 of u32 (align 4), storage starts at 0. + // if we are at byte 5 of u32 (align 4), storage starts at 4. + let storage_start = + (current_byte_offset / field_alignment) * field_alignment; + // Jump to the end of that storage unit. + current_byte_offset = storage_start + field_size; + } + } + } else { + // Align the field placement based on the current field alignment. + let aligned_current_offset = + align_up(current_byte_offset as u64, field_alignment as u64); + structure.insert( + &field_ty, + &field.name, + aligned_current_offset, + false, + MemberAccess::PublicAccess, + MemberScope::NoScope, + ); + current_byte_offset = aligned_current_offset as usize + field_size; + } + } + structure.alignment(max_alignment); + + // TODO: Only add tail padding if we are not packed? I think we still need to do more. + if *is_packed { + structure.packed(true); + } else { + let total_size = align_up(current_byte_offset as u64, max_alignment as u64); + structure.width(total_size); + } + + Ok(Type::structure(&structure.finalize())) + } + MetadataTypeKind::Enum { ty, variants } => { + let enum_ty = self.convert_type_kind(ty)?; + let mut builder = EnumerationBuilder::new(); + for (name, value) in variants { + builder.insert(name, *value); + } + Ok(Type::enumeration( + &builder.finalize(), + NonZeroUsize::new(enum_ty.width() as usize) + .ok_or_else(|| ImportError::UnhandledTypeSize("Enum with zero width"))?, + enum_ty.is_signed().contents, + )) + } + MetadataTypeKind::Function { + params, + return_type, + is_vararg, + } => { + let return_ty = self.convert_type_kind(return_type)?; + let mut bn_params = Vec::new(); + for param in params { + let param_ty = self.convert_type_kind(¶m.ty)?; + bn_params.push(FunctionParameter::new(param_ty, param.name.clone(), None)); + } + Ok(Type::function(&return_ty, bn_params, *is_vararg)) + } + MetadataTypeKind::Reference { name, namespace } => { + // We are required to set the ID here since type libraries seem to only look up through + // the ID, and never fall back to name lookup. This is strange considering you must also + // set the types source to the given library, which seems counterintuitive. + // TODO: Add kind to ntr. + let ntr = NamedTypeReference::new_with_id( + NamedTypeReferenceClass::TypedefNamedTypeClass, + &format!("{}::{}", namespace, name), + name, + ); + // TODO: Type alignment? + let type_size = self.type_kind_size(kind)?; + Ok(TypeBuilder::named_type(&ntr) + .set_width(type_size) + .set_alignment(type_size) + .finalize()) + } + MetadataTypeKind::Union { fields } => { + let mut union = StructureBuilder::new(); + union.structure_type(StructureType::UnionStructureType); + + let mut max_alignment = 0usize; + // We need to look ahead to figure out when bitfields end and adjust current_byte_offset accordingly. + let mut field_iter = fields.iter().peekable(); + while let Some(field) = field_iter.next() { + let field_ty = self.convert_type_kind(&field.ty)?; + let field_alignment = self.type_kind_alignment(&field.ty)?; + max_alignment = max_alignment.max(field_alignment); + union.insert( + &field_ty, + &field.name, + 0, + false, + MemberAccess::PublicAccess, + MemberScope::NoScope, + ); + } + + union.alignment(max_alignment); + Ok(Type::structure(&union.finalize())) + } + } + } + + /// Retrieve the size of a type kind in bytes, references to types will be looked up + /// such that we can determine the size of structures with references as fields. + pub fn type_kind_size(&self, kind: &MetadataTypeKind) -> Result { + match kind { + MetadataTypeKind::Void => Ok(0), + MetadataTypeKind::Bool { size } => Ok(size.unwrap_or(self.integer_size)), + MetadataTypeKind::Integer { size, .. } => Ok(size.unwrap_or(self.integer_size)), + MetadataTypeKind::Character { size } => Ok(*size), + MetadataTypeKind::Float { size } => Ok(*size), + MetadataTypeKind::Pointer { .. } => Ok(self.address_size), + MetadataTypeKind::Array { element, count } => { + let elem_size = self.type_kind_size(element)?; + Ok(elem_size * *count) + } + MetadataTypeKind::Struct { fields, is_packed } => { + let mut current_offset = 0usize; + let mut max_struct_alignment = 1usize; + for field in fields { + let field_size = self.type_kind_size(&field.ty)?; + let field_alignment = if *is_packed { + 1 + } else { + self.type_kind_alignment(&field.ty)? + }; + max_struct_alignment = max_struct_alignment.max(field_alignment); + current_offset = + align_up(current_offset as u64, field_alignment as u64) as usize; + current_offset += field_size; + } + // Tail padding is only needed if not packed. + let final_alignment = if *is_packed { 1 } else { max_struct_alignment }; + let total_size = align_up(current_offset as u64, final_alignment as u64) as usize; + Ok(total_size) + } + MetadataTypeKind::Union { fields } => { + let mut largest_field_size = 0usize; + for field in fields { + let field_size = self.type_kind_size(&field.ty)?; + largest_field_size = largest_field_size.max(field_size); + } + Ok(largest_field_size) + } + MetadataTypeKind::Enum { ty, .. } => self.type_kind_size(ty), + MetadataTypeKind::Function { .. } => Err(ImportError::UnhandledTypeSize( + "Function types are not sized", + )), + MetadataTypeKind::Reference { name, namespace } => { + // Look up the type and return its size. + let Some(ty_info) = self.type_lookup.get(&(namespace.clone(), name.clone())) else { + // This should really only happen if we did not specify all the required winmd files. + tracing::error!( + "Failed to find type '{}' when looking up type size for reference", + name + ); + return Ok(1); + }; + self.type_kind_size(&ty_info.kind) + } + } + } + + pub fn type_kind_alignment(&self, kind: &MetadataTypeKind) -> Result { + match kind { + MetadataTypeKind::Bool { size: None } => Ok(1), + MetadataTypeKind::Bool { size } => Ok(size.unwrap_or(self.integer_size)), + // TODO: Clean this stuff up. + MetadataTypeKind::Character { size } => Ok(*size), + MetadataTypeKind::Integer { size: Some(1), .. } => Ok(1), + MetadataTypeKind::Integer { size: Some(2), .. } => Ok(2), + MetadataTypeKind::Integer { size: Some(4), .. } => Ok(4), + MetadataTypeKind::Integer { size: Some(8), .. } + | MetadataTypeKind::Float { size: 8 } + | MetadataTypeKind::Pointer { .. } => Ok(self.address_size), // 8 on x64 + MetadataTypeKind::Array { element, .. } => self.type_kind_alignment(element), + MetadataTypeKind::Struct { fields, is_packed } => { + if *is_packed { + return Ok(1); + } + let mut max_align = 1usize; + for field in fields { + max_align = max_align.max(self.type_kind_alignment(&field.ty)?); + } + Ok(max_align) + } + MetadataTypeKind::Union { fields } => { + let mut max_align = 1usize; + for field in fields { + max_align = max_align.max(self.type_kind_alignment(&field.ty)?); + } + Ok(max_align) + } + MetadataTypeKind::Reference { name, namespace } => { + let Some(ty_info) = self.type_lookup.get(&(namespace.clone(), name.clone())) else { + // TODO: Failed to find it in local type lookup, try type libraries? + tracing::error!( + "Failed to find type '{}' when looking up type alignment for reference", + name + ); + return Ok(4); + }; + self.type_kind_alignment(&ty_info.kind) + } + _ => Ok(4), + } + } +} + +// Aligns an offset up to the nearest multiple of `align`. +fn align_up(offset: u64, align: u64) -> u64 { + if align == 0 { + return offset; + } + let mask = align - 1; + (offset + mask) & !mask +} + +#[cfg(test)] +mod tests { + use super::info::{ + MetadataFieldInfo, MetadataImportInfo, MetadataImportMethod, MetadataModuleInfo, + }; + use super::*; + use binaryninja::architecture::CoreArchitecture; + use binaryninja::types::TypeClass; + + #[test] + fn test_import_type() { + // We must initialize binary ninja to access architectures. + let _session = binaryninja::headless::Session::new().expect("Failed to create session"); + + let mut info = MetadataInfo::default(); + info.functions = vec![MetadataFunctionInfo { + name: "MyFunction".to_string(), + ty: MetadataTypeKind::Function { + params: vec![], + return_type: Box::new(MetadataTypeKind::Void), + is_vararg: false, + }, + namespace: "Win32.Test".to_string(), + import_info: Some(MetadataImportInfo { + method: MetadataImportMethod::ByName("MyFunction".to_string()), + module: MetadataModuleInfo { + name: "TestModule.dll".to_string(), + }, + }), + }]; + info.types = vec![ + MetadataTypeInfo { + name: "Bar".to_string(), + kind: MetadataTypeKind::Integer { + size: Some(4), + is_signed: true, + }, + namespace: "Win32.Test".to_string(), + }, + MetadataTypeInfo { + name: "TestType".to_string(), + kind: MetadataTypeKind::Struct { + fields: vec![ + MetadataFieldInfo { + name: "field1".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(4), + is_signed: false, + }, + is_const: false, + bitfield: None, + }, + // TODO: Add more fields to verify bitfields, and const fields. + MetadataFieldInfo { + name: "field2_0".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(4), + is_signed: true, + }, + is_const: true, + bitfield: Some((0, 1)), + }, + MetadataFieldInfo { + name: "field2_1".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(4), + is_signed: true, + }, + is_const: true, + bitfield: Some((1, 1)), + }, + MetadataFieldInfo { + name: "field3".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(2), + is_signed: true, + }, + is_const: true, + bitfield: None, + }, + MetadataFieldInfo { + name: "field4".to_string(), + ty: MetadataTypeKind::Pointer { + is_pointee_const: false, + is_const: false, + target: Box::new(MetadataTypeKind::Reference { + namespace: "Win32.Test".to_string(), + name: "Bar".to_string(), + }), + }, + is_const: false, + bitfield: None, + }, + ], + is_packed: false, + }, + namespace: "Foo".to_string(), + }, + ]; + let importer = WindowsMetadataImporter::new_with_info(info); + let x86 = CoreArchitecture::by_name("x86").expect("No x86 architecture"); + let platform = Platform::by_name("windows-x86").expect("No windows-x86 platform"); + let type_libraries = importer.import(&platform).expect("Failed to import types"); + assert_eq!(type_libraries.len(), 1); + let til = type_libraries.first().expect("No type libraries"); + assert_eq!(til.named_types().len(), 1); + let first_ty = til + .named_types() + .iter() + .next() + .expect("No types in library"); + assert_eq!(first_ty.name.to_string(), "TestType"); + assert_eq!(first_ty.ty.type_class(), TypeClass::StructureTypeClass); + let first_ty_struct = first_ty + .ty + .get_structure() + .expect("Type is not a structure"); + assert_eq!(first_ty_struct.members().len(), 5); + let mut structure_fields = first_ty_struct.members().iter(); + + for member in first_ty_struct.members() { + println!(" +{}: {}", member.offset, member.name.to_string()) + } + + // TODO: Finish this! + assert!(false); + // let first_member = structure_fields.next().expect("No fields in structure"); + // assert_eq!(first_member.name.to_string(), "field1"); + // assert_eq!(first_member.ty, TypeClass::IntegerTypeClass); + // let second_member = structure_fields.next().expect("No fields in structure"); + // assert_eq!(second_member.name.to_string(), "field2_0"); + // assert_eq!(second_member.type.type_class(), TypeClass::IntegerTypeClass); + // let third_member = structure_fields.next().expect("No fields in structure"); + // assert_eq!(third_member.name.to_string(), "field2_1"); + // assert_eq!(third_member.type.type_class(), TypeClass::IntegerTypeClass); + // let fourth_member = structure_fields.next().expect("No fields in structure"); + } +} diff --git a/plugins/bntl_utils/src/winmd/info.rs b/plugins/bntl_utils/src/winmd/info.rs new file mode 100644 index 000000000..4db30c7a7 --- /dev/null +++ b/plugins/bntl_utils/src/winmd/info.rs @@ -0,0 +1,430 @@ +//! Metadata information extracted from Windows metadata files. +//! +//! While we could use the direct representation, this is easier to work with. + +use std::collections::{HashMap, HashSet}; + +#[derive(Debug, Default, Clone)] +pub struct MetadataInfo { + pub types: Vec, + pub functions: Vec, + pub constants: Vec, +} + +impl MetadataInfo { + /// Partitions the metadata into a map of libraries, where each library contains types and functions + /// that belong to that library. This is used when mapping metadata info to type libraries. + pub fn partitioned(&self) -> PartitionedMetadataInfo { + let mut result_map: HashMap = HashMap::new(); + + // Map of namespace to module names that use it. + let mut namespace_dependencies: HashMap> = HashMap::new(); + for func in &self.functions { + if let Some(import) = &func.import_info { + namespace_dependencies + .entry(func.namespace.clone()) + .or_default() + .insert(import.module.name.clone()); + } + } + + let namespace_to_library_name = |ns: &str| -> LibraryName { + match namespace_dependencies.get(ns) { + Some(modules) if modules.len() == 1 => { + LibraryName::Module(modules.iter().next().unwrap().clone()) + } + _ => LibraryName::Namespace(ns.to_string()), + } + }; + + for func in &self.functions { + let dest_lib = match &func.import_info { + Some(info) => LibraryName::Module(info.module.name.clone()), + None => LibraryName::Namespace(func.namespace.clone()), + }; + let entry = result_map.entry(dest_lib.clone()).or_default(); + func.ty.visit_references(&mut |ns, name| { + let library_name = namespace_to_library_name(ns); + if dest_lib != library_name { + entry + .external_references + .insert(name.to_string(), library_name); + } + }); + entry.metadata.functions.push(func.clone()); + } + + for ty in &self.types { + let dest_lib = namespace_to_library_name(&ty.namespace); + let entry = result_map.entry(dest_lib.clone()).or_default(); + ty.kind.visit_references(&mut |ns, name| { + let library_name = namespace_to_library_name(ns); + if dest_lib != library_name { + entry + .external_references + .insert(name.to_string(), library_name); + } + }); + entry.metadata.types.push(ty.clone()); + } + + for constant in &self.constants { + let dest_lib = namespace_to_library_name(&constant.namespace); + let entry = result_map.entry(dest_lib.clone()).or_default(); + constant.ty.visit_references(&mut |ns, name| { + let library_name = namespace_to_library_name(ns); + if dest_lib != library_name { + entry + .external_references + .insert(name.to_string(), library_name); + } + }); + entry.metadata.constants.push(constant.clone()); + } + + PartitionedMetadataInfo { + libraries: result_map, + } + } + + pub fn create_constant_enums(&self) -> Vec { + // Group constants by their type, if there are multiple constants with the same type, we + // will make an enum out of them, once that is done, we will take overlapping constants + // and prioritize certain namespaces over others. + // TODO: Add some more structured types here, this is a crazy map. + let mut grouped_constants: HashMap< + (String, String), + HashMap>, + > = HashMap::new(); + for constant in &self.constants { + let MetadataTypeKind::Reference { name, namespace } = &constant.ty else { + // TODO: We should optionally provide a way to group constants like these into an enumeration. + // Skipping constant `WDS_MC_TRACE_VERBOSE` with non-reference type `Integer { size: Some(4), is_signed: false }` + // Skipping constant `WDS_MC_TRACE_INFO` with non-reference type `Integer { size: Some(4), is_signed: false }` + // Skipping constant `WDS_MC_TRACE_WARNING` with non-reference type `Integer { size: Some(4), is_signed: false }` + // Skipping constant `WDS_MC_TRACE_ERROR` with non-reference type `Integer { size: Some(4), is_signed: false }` + // Skipping constant `WDS_MC_TRACE_FATAL` with non-reference type `Integer { size: Some(4), is_signed: false }` + tracing::debug!( + "Skipping constant `{}` with non-reference type `{:?}`", + constant.name, + constant.ty + ); + continue; + }; + grouped_constants + .entry((namespace.clone(), name.clone())) + .or_default() + .entry(constant.value) + .or_default() + .push(constant.clone()); + } + + let mut enums = Vec::new(); + for ((enum_namespace, enum_name), mapped_values) in grouped_constants { + let mut variants = Vec::new(); + for (_, group_variants) in mapped_values { + let sorted_group_variants = + sort_metadata_constants_by_proximity(&enum_namespace, group_variants); + let enum_variants: Vec<_> = sorted_group_variants + .iter() + .map(|info| (info.name.clone(), info.value)) + .collect(); + variants.extend(enum_variants); + } + + let enum_kind = MetadataTypeKind::Enum { + ty: Box::new(MetadataTypeKind::Void), + variants, + }; + + enums.push(MetadataTypeInfo { + name: enum_name, + kind: enum_kind, + namespace: enum_namespace, + }); + } + enums + } + + #[allow(dead_code)] + fn update_stale_references(&mut self) { + let mut valid_type_map = HashMap::new(); + for ty in self.types.iter() { + valid_type_map.insert(ty.name.clone(), ty.clone()); + } + + for ty in self.types.iter_mut() { + ty.kind.visit_references_mut(&mut |node| { + let MetadataTypeKind::Reference { name, namespace } = node else { + tracing::error!( + "`visit_references_mut` did not return a reference! {:?}", + node + ); + return; + }; + if let Some(survivor) = valid_type_map.get(name) { + if namespace != &survivor.namespace { + tracing::debug!( + "Updating stale namespace reference `{}` to `{}` for `{}`", + namespace, + survivor.namespace, + name + ); + *namespace = survivor.namespace.clone(); + } + } + }); + } + } +} + +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub enum LibraryName { + /// A synthetic library with no associated module name. + /// + /// The shared library is "synthetic" in the sense that a binary view cannot reference it directly. + Namespace(String), + /// A real module with a name (e.g. "info.dll"), these libraries can be referenced directly by a binary view. + Module(String), +} + +#[derive(Debug, Clone, Default)] +pub struct LibraryInfo { + pub metadata: MetadataInfo, + /// A map of externally referenced names to their library names. + /// + /// This is required when resolving type references to other libraries. + pub external_references: HashMap, +} + +#[derive(Debug, Default)] +pub struct PartitionedMetadataInfo { + pub libraries: HashMap, +} + +// TODO: ModuleRef (computable from ModuleInfo and the underlying core module) +// TODO: Put a ModuleRef in all places where a module is associated. +#[derive(Debug, Clone)] +pub struct MetadataModuleInfo { + /// The modules name on disk, this is used to determine the imported + /// function name when loading type information from a type library. + pub name: String, +} + +#[derive(Debug, Clone)] +pub struct MetadataTypeInfo { + pub name: String, + pub kind: MetadataTypeKind, + /// The namespace of the type, e.x. "Windows.Win32.Foundation" + /// + /// This is used to help determine what library this information belongs to. When we go to import + /// this information (along with others), we will build a tree of information where each node + /// corresponds to the namespace, and each child node corresponds to a sub-namespace. Then import + /// info will be enumerated to determine if the type can only ever belong to a single import module + /// if the type is only used in a single module, we will place it in that type library. If the namespace + /// can reference more than one module, we will place it in a common type library named after + /// the namespace itself, it can only ever be referenced by another type library and as such should + /// only contain types and no functions. + /// + /// For more information see [`PartitionedMetadataInfo`]. + pub namespace: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MetadataTypeKind { + Void, + Bool { + // NOTE: Weird optional, if None we actually default the size to integer size! + size: Option, + }, + Integer { + size: Option, + is_signed: bool, + }, + Character { + size: usize, + }, + Float { + size: usize, + }, + Pointer { + is_const: bool, + is_pointee_const: bool, + target: Box, + }, + Array { + element: Box, + count: usize, + }, + Struct { + fields: Vec, + is_packed: bool, + }, + Union { + fields: Vec, + }, + Enum { + ty: Box, + variants: Vec<(String, u64)>, + }, + Function { + params: Vec, + return_type: Box, + is_vararg: bool, + }, + Reference { + // TODO: Generics may also be passed here. + /// The namespace of the referenced type, e.x. "Windows.Win32.Foundation" + namespace: String, + /// The referenced type name, e.x. "BOOL" + name: String, + }, +} + +impl MetadataTypeKind { + pub(crate) fn visit_references(&self, callback: &mut F) + where + F: FnMut(&str, &str), + { + match self { + MetadataTypeKind::Reference { namespace, name } => { + callback(namespace, name); + } + MetadataTypeKind::Pointer { target, .. } => { + target.visit_references(callback); + } + MetadataTypeKind::Array { element, .. } => { + element.visit_references(callback); + } + MetadataTypeKind::Struct { fields, .. } => { + for field in fields { + field.ty.visit_references(callback); + } + } + MetadataTypeKind::Enum { ty, .. } => { + ty.visit_references(callback); + } + MetadataTypeKind::Function { + params, + return_type, + .. + } => { + for param in params { + param.ty.visit_references(callback); + } + return_type.visit_references(callback); + } + _ => {} + } + } + + #[allow(dead_code)] + pub(crate) fn visit_references_mut(&mut self, callback: &mut F) + where + F: FnMut(&mut MetadataTypeKind), + { + match self { + MetadataTypeKind::Reference { .. } => { + callback(self); + } + MetadataTypeKind::Pointer { target, .. } => { + target.visit_references_mut(callback); + } + MetadataTypeKind::Array { element, .. } => { + element.visit_references_mut(callback); + } + MetadataTypeKind::Struct { fields, .. } | MetadataTypeKind::Union { fields, .. } => { + for field in fields { + field.ty.visit_references_mut(callback); + } + } + MetadataTypeKind::Enum { ty, .. } => { + ty.visit_references_mut(callback); + } + MetadataTypeKind::Function { + params, + return_type, + .. + } => { + for param in params { + param.ty.visit_references_mut(callback); + } + return_type.visit_references_mut(callback); + } + _ => {} + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MetadataFieldInfo { + pub name: String, + pub ty: MetadataTypeKind, + pub is_const: bool, + /// This is only set for bitfields, The first value is the bit position within the associated byte, + /// and the second is the bit width. + /// + /// NOTE: The bit position can never be greater than `7`. + pub bitfield: Option<(u8, u8)>, + // TODO: Attributes ( virtual, static, etc...) +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MetadataParameterInfo { + pub name: String, + pub ty: MetadataTypeKind, + // TODO: Attributes (in, out, etc...) +} + +#[allow(dead_code)] +#[derive(Debug, Clone)] +pub enum MetadataImportMethod { + ByName(String), + ByOrdinal(u32), +} + +#[derive(Debug, Clone)] +pub struct MetadataImportInfo { + #[allow(dead_code)] + pub method: MetadataImportMethod, + pub module: MetadataModuleInfo, +} + +#[derive(Debug, Clone)] +pub struct MetadataFunctionInfo { + pub name: String, + /// This will only ever be [`MetadataTypeKind::Function`]. + pub ty: MetadataTypeKind, + pub namespace: String, + pub import_info: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MetadataConstantInfo { + pub name: String, + pub namespace: String, + pub ty: MetadataTypeKind, + pub value: u64, +} + +pub fn sort_metadata_constants_by_proximity( + reference: &str, + mut candidates: Vec, +) -> Vec { + let ref_parts: Vec<&str> = reference.split('.').collect(); + candidates.sort_by_cached_key(|info| { + // Extract the namespace string from the metadata info + let ns = &info.namespace; + let cand_parts = ns.split('.'); + + let score = ref_parts + .iter() + .zip(cand_parts) + .take_while(|(a, b)| *a == b) + .count(); + + // Sort by highest score first, then alphabetically by namespace + (std::cmp::Reverse(score), ns.clone()) + }); + candidates +} diff --git a/plugins/bntl_utils/src/winmd/translate.rs b/plugins/bntl_utils/src/winmd/translate.rs new file mode 100644 index 000000000..01ea54a55 --- /dev/null +++ b/plugins/bntl_utils/src/winmd/translate.rs @@ -0,0 +1,654 @@ +//! Translate windows metadata into a self-contained structure, for later use. + +use super::info::{ + MetadataConstantInfo, MetadataFieldInfo, MetadataFunctionInfo, MetadataImportInfo, + MetadataImportMethod, MetadataInfo, MetadataModuleInfo, MetadataParameterInfo, + MetadataTypeInfo, MetadataTypeKind, +}; +use std::collections::{HashMap, HashSet}; +use thiserror::Error; +use windows_metadata::reader::TypeCategory; +use windows_metadata::{ + AsRow, FieldAttributes, HasAttributes, MethodCallAttributes, Type, TypeAttributes, Value, +}; + +pub const BITFIELD_ATTR: &str = "NativeBitfieldAttribute"; +pub const CONST_ATTR: &str = "ConstAttribute"; +pub const FNPTR_ATTR: &str = "UnmanagedFunctionPointerAttribute"; +pub const _STRUCT_SIZE_ATTR: &str = "StructSizeFieldAttribute"; +pub const API_CONTRACT_ATTR: &str = "ApiContractAttribute"; + +#[derive(Error, Debug)] +pub enum TranslationError { + #[error("no files were provided")] + NoFiles, + #[error("the type name '{0}' is not handled")] + UnhandledType(String), + #[error("the attribute '{0}' is not supported")] + UnsupportedAttribute(String), +} + +pub struct WindowsMetadataTranslator { + // TODO: Allow this to be customized by user. + /// Replace references to a given name with a different one. + /// + /// This allows you to move types to a different namespace or rename them and be certain all + /// references to that type are updated. + remapped_references: HashMap<(&'static str, &'static str), (&'static str, &'static str)>, +} + +impl WindowsMetadataTranslator { + pub fn new() -> Self { + // TODO: Move this to a static array. + let mut remapped_references = HashMap::new(); + remapped_references.insert(("System", "Guid"), ("Windows.Win32.Foundation", "Guid")); + Self { + remapped_references, + } + } + + pub fn translate( + &self, + files: Vec, + ) -> Result { + if files.is_empty() { + return Err(TranslationError::NoFiles); + } + let index = windows_metadata::reader::TypeIndex::new(files); + self.translate_index(&index) + } + + pub fn translate_index( + &self, + index: &windows_metadata::reader::TypeIndex, + ) -> Result { + let mut functions = Vec::new(); + let mut types = Vec::new(); + let mut constants = Vec::new(); + + // TODO: Move this somewhere else? + // Add synthetic types here. + types.extend([ + MetadataTypeInfo { + name: "Guid".to_string(), + kind: MetadataTypeKind::Struct { + fields: vec![ + MetadataFieldInfo { + name: "Data1".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(4), + is_signed: false, + }, + is_const: false, + bitfield: None, + }, + MetadataFieldInfo { + name: "Data2".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(2), + is_signed: false, + }, + is_const: false, + bitfield: None, + }, + MetadataFieldInfo { + name: "Data3".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(2), + is_signed: false, + }, + is_const: false, + bitfield: None, + }, + MetadataFieldInfo { + name: "Data4".to_string(), + ty: MetadataTypeKind::Array { + element: Box::new(MetadataTypeKind::Integer { + size: Some(1), + is_signed: false, + }), + count: 8, + }, + is_const: false, + bitfield: None, + }, + ], + is_packed: false, + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "HANDLE".to_string(), + kind: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Void), + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "HINSTANCE".to_string(), + kind: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Void), + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "HMODULE".to_string(), + kind: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Void), + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "PCSTR".to_string(), + kind: MetadataTypeKind::Pointer { + is_const: true, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Character { size: 1 }), + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "PCWSTR".to_string(), + kind: MetadataTypeKind::Pointer { + is_const: true, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Character { size: 2 }), + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "PSTR".to_string(), + kind: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Character { size: 1 }), + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "PWSTR".to_string(), + kind: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Character { size: 2 }), + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "UNICODE_STRING".to_string(), + kind: MetadataTypeKind::Struct { + fields: vec![ + MetadataFieldInfo { + name: "Length".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(2), + is_signed: false, + }, + is_const: false, + bitfield: None, + }, + MetadataFieldInfo { + name: "MaximumLength".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(2), + is_signed: false, + }, + is_const: false, + bitfield: None, + }, + MetadataFieldInfo { + name: "Buffer".to_string(), + ty: MetadataTypeKind::Reference { + namespace: "Windows.Win32.Foundation".to_string(), + name: "PWSTR".to_string(), + }, + is_const: false, + bitfield: None, + }, + ], + is_packed: false, + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "BOOLEAN".to_string(), + kind: MetadataTypeKind::Bool { size: Some(1) }, + namespace: "Windows.Win32.Security".to_string(), + }, + MetadataTypeInfo { + name: "BOOL".to_string(), + // BOOL is integer sized, not char sized like a typical bool value. + kind: MetadataTypeKind::Bool { size: None }, + namespace: "Windows.Win32.Security".to_string(), + }, + ]); + + for entry in index.types() { + match entry.category() { + TypeCategory::Interface => { + let (interface_ty, interface_vtable_ty) = self.translate_interface(&entry)?; + types.push(interface_ty); + types.push(interface_vtable_ty); + } + TypeCategory::Class => { + let (cls_functions, cls_constants) = self.translate_class(&entry)?; + functions.extend(cls_functions); + constants.extend(cls_constants); + } + TypeCategory::Enum => { + types.push(self.translate_enum(&entry)?); + } + TypeCategory::Struct => { + // Skip marker type structures. + if entry.has_attribute(API_CONTRACT_ATTR) { + continue; + } + types.push(self.translate_struct(&entry)?); + } + TypeCategory::Delegate => { + types.push(self.translate_delegate(&entry)?); + } + TypeCategory::Attribute => { + // We will pull attributes directly from the other entries. + } + } + } + + // Remove duplicate types within the same namespace, the first one wins. This is what allows + // us to override types by placing the overrides in the type list before traversing the index. + let mut tracked_names = HashSet::<(String, String)>::new(); + types.retain(|ty| { + let ty_name = (ty.namespace.clone(), ty.name.clone()); + tracked_names.insert(ty_name) + }); + + Ok(MetadataInfo { + types, + functions, + constants, + }) + } + + pub fn translate_struct( + &self, + structure: &windows_metadata::reader::TypeDef, + ) -> Result { + let mut fields = Vec::new(); + + let nested: Result, _> = structure + .index() + .nested(structure.clone()) + .map(|n| { + // TODO: Are all nested fields a struct? + let nested_ty = self.translate_struct(&n)?; + Ok((n.name().to_string(), nested_ty)) + }) + .collect(); + let nested = nested?; + + for field in structure.fields() { + let mut field_ty = self.translate_type(&field.ty())?; + // TODO: This is kinda ugly. + // Handle nested structures by unwrapping the reference. + let mut nested_ty = None; + field_ty.visit_references(&mut |_, name| { + nested_ty = nested.get(name).cloned().map(|n| n.kind); + }); + field_ty = nested_ty.unwrap_or(field_ty); + + // Bitfields are special, they are a "fake" field that we need to look at the attributes of + // to unwrap the real fields that are contained within the storage type. + if field.has_attribute(BITFIELD_ATTR) { + for bitfield in field.attributes() { + let bitfield_values = bitfield.value(); + let mut values = bitfield_values.iter(); + let Some((_, Value::Utf8(bitfield_name))) = values.next() else { + continue; + }; + let Some((_, Value::I64(bitfield_pos))) = values.next() else { + continue; + }; + let Some((_, Value::I64(bitfield_width))) = values.next() else { + continue; + }; + // is_private, is_public, is_virtual + fields.push(MetadataFieldInfo { + name: bitfield_name.clone(), + ty: field_ty.clone(), + is_const: field.has_attribute(CONST_ATTR), + bitfield: Some((*bitfield_pos as u8, *bitfield_width as u8)), + }); + } + } else { + fields.push(MetadataFieldInfo { + name: field.name().to_string(), + ty: field_ty, + is_const: field.has_attribute(CONST_ATTR), + bitfield: None, + }); + } + } + + let mut is_packed = false; + if let Some(_layout) = structure.class_layout() { + is_packed = _layout.packing_size() == 1; + } + + // ExplicitLayout seems to denote a union layout. + let kind = if structure.flags().contains(TypeAttributes::ExplicitLayout) { + MetadataTypeKind::Union { fields } + } else { + MetadataTypeKind::Struct { fields, is_packed } + }; + + Ok(MetadataTypeInfo { + name: structure.name().to_string(), + kind, + namespace: structure.namespace().to_string(), + }) + } + + pub fn translate_class( + &self, + class: &windows_metadata::reader::TypeDef, + ) -> Result<(Vec, Vec), TranslationError> { + let namespace = class.namespace().to_string(); + let mut functions = Vec::new(); + for method in class.methods() { + match self.translate_method(&method) { + Ok(mut func) => { + func.namespace = namespace.clone(); + functions.push(func); + } + Err(e) => tracing::warn!("Failed to translate method {}: {}", method.name(), e), + } + } + + let mut constants = Vec::new(); + for field in class.fields() { + if let Some(constant) = field + .constant() + .map(|c| self.value_to_u64(&c.value())) + .flatten() + { + constants.push(MetadataConstantInfo { + name: field.name().to_string(), + namespace: namespace.clone(), + ty: self.translate_type(&field.ty())?, + value: constant, + }); + } else { + tracing::debug!("Field {} is not a constant, skipping...", field.name()); + } + } + + Ok((functions, constants)) + } + + pub fn translate_method( + &self, + method: &windows_metadata::reader::MethodDef, + ) -> Result { + // TODO: Pass generics here? generic_params seems always empty? Even windows-rs doesn't use it. + let signature = method.signature(&[]); + let func_params: Result, TranslationError> = method + .params() + .filter(|p| !p.name().is_empty()) + .zip(signature.types) + .map(|(param, param_ty)| { + Ok(MetadataParameterInfo { + name: param.name().to_string(), + ty: self.translate_type(¶m_ty)?, + }) + }) + .collect(); + let func_ty = MetadataTypeKind::Function { + params: func_params?, + return_type: Box::new(self.translate_type(&signature.return_type)?), + is_vararg: signature.flags.contains(MethodCallAttributes::VARARG), + }; + + let import_info = method + .impl_map() + .map(|impl_map| self.import_info_from_map(&impl_map)); + + Ok(MetadataFunctionInfo { + name: method.name().to_string(), + ty: func_ty, + // NOTE: This will be set by the associated class entry once returned. + namespace: "".to_string(), + import_info, + }) + } + + pub fn translate_delegate( + &self, + delegate: &windows_metadata::reader::TypeDef, + ) -> Result { + if !delegate.has_attribute(FNPTR_ATTR) { + return Err(TranslationError::UnsupportedAttribute( + FNPTR_ATTR.to_string(), + )); + } + let invoke_method = delegate + .methods() + .find(|m| m.name() == "Invoke") + .expect("Invoke method not found"); + let translated_invoke_method = self.translate_method(&invoke_method)?; + Ok(MetadataTypeInfo { + name: delegate.name().to_string(), + kind: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(translated_invoke_method.ty), + }, + namespace: delegate.namespace().to_string(), + }) + } + + pub fn translate_interface( + &self, + interface: &windows_metadata::reader::TypeDef, + ) -> Result<(MetadataTypeInfo, MetadataTypeInfo), TranslationError> { + let mut vtable_fields = Vec::new(); + for meth in interface.methods() { + let meth_ty = self.translate_method(&meth)?; + vtable_fields.push(MetadataFieldInfo { + name: meth.name().to_string(), + ty: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(meth_ty.ty), + }, + is_const: false, + bitfield: None, + }) + } + + let interface_ns = interface.namespace(); + let interface_ty = MetadataTypeInfo { + name: interface.name().to_string(), + kind: MetadataTypeKind::Struct { + fields: vec![MetadataFieldInfo { + name: "vtable".to_string(), + ty: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Reference { + namespace: interface_ns.to_string(), + name: format!("{}VTable", interface.name()), + }), + }, + is_const: false, + bitfield: None, + }], + is_packed: false, + }, + namespace: interface_ns.to_string(), + }; + let interface_vtable_ty = MetadataTypeInfo { + name: format!("{}VTable", interface.name()), + kind: MetadataTypeKind::Struct { + fields: Vec::new(), + is_packed: false, + }, + namespace: interface_ns.to_string(), + }; + Ok((interface_ty, interface_vtable_ty)) + } + + pub fn translate_enum( + &self, + _enum: &windows_metadata::reader::TypeDef, + ) -> Result { + let mut variants = Vec::new(); + let mut last_constant = 0; + let mut enum_ty = MetadataTypeKind::Integer { + size: None, + is_signed: true, + }; + for variant in _enum.fields() { + if variant.flags().contains(FieldAttributes::RTSpecialName) { + // Skip the hidden "value__" field. + continue; + } + // Pull the enums type from the constant if it exists. + // Otherwise, we will fall back to void and use a default type when importing. + if let Some(constant) = variant.constant() { + enum_ty = self.translate_type(&constant.ty())?; + } + let variant_constant = variant + .constant() + .map(|c| self.value_to_u64(&c.value())) + .flatten() + .unwrap_or(last_constant); + let variant_name = variant.name().to_string(); + variants.push((variant_name, variant_constant)); + last_constant = variant_constant; + } + Ok(MetadataTypeInfo { + name: _enum.name().to_string(), + kind: MetadataTypeKind::Enum { + ty: Box::new(enum_ty), + variants, + }, + namespace: _enum.namespace().to_string(), + }) + } + + pub fn translate_type(&self, ty: &Type) -> Result { + match ty { + Type::Void => Ok(MetadataTypeKind::Void), + Type::Bool => Ok(MetadataTypeKind::Bool { size: Some(1) }), + Type::Char => Ok(MetadataTypeKind::Character { size: 1 }), + Type::I8 => Ok(MetadataTypeKind::Integer { + size: Some(1), + is_signed: true, + }), + Type::U8 => Ok(MetadataTypeKind::Integer { + size: Some(1), + is_signed: false, + }), + Type::I16 => Ok(MetadataTypeKind::Integer { + size: Some(2), + is_signed: true, + }), + Type::U16 => Ok(MetadataTypeKind::Integer { + size: Some(2), + is_signed: false, + }), + Type::I32 => Ok(MetadataTypeKind::Integer { + size: Some(4), + is_signed: true, + }), + Type::U32 => Ok(MetadataTypeKind::Integer { + size: Some(4), + is_signed: false, + }), + Type::I64 => Ok(MetadataTypeKind::Integer { + size: Some(8), + is_signed: true, + }), + Type::U64 => Ok(MetadataTypeKind::Integer { + size: Some(8), + is_signed: false, + }), + Type::F32 => Ok(MetadataTypeKind::Float { size: 4 }), + Type::F64 => Ok(MetadataTypeKind::Float { size: 8 }), + Type::ISize => Ok(MetadataTypeKind::Integer { + size: None, + is_signed: true, + }), + Type::USize => Ok(MetadataTypeKind::Integer { + size: None, + is_signed: false, + }), + Type::Name(name) => { + if let Some((remapped_ns, remapped_name)) = + self.remapped_references.get(&(&name.namespace, &name.name)) + { + Ok(MetadataTypeKind::Reference { + namespace: remapped_ns.to_string(), + name: remapped_name.to_string(), + }) + } else { + Ok(MetadataTypeKind::Reference { + namespace: name.namespace.clone(), + name: name.name.clone(), + }) + } + } + Type::PtrMut(target, _) => Ok(MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(self.translate_type(target)?), + }), + Type::PtrConst(target, _) => { + Ok(MetadataTypeKind::Pointer { + is_const: false, + // TODO: I think this might be pointee const? + is_pointee_const: true, + target: Box::new(self.translate_type(target)?), + }) + } + Type::ArrayFixed(elem_ty, count) => Ok(MetadataTypeKind::Array { + element: Box::new(self.translate_type(elem_ty)?), + count: *count, + }), + other => Err(TranslationError::UnhandledType(format!("{:?}", other))), + } + } + + pub fn import_info_from_map( + &self, + map: &windows_metadata::reader::ImplMap, + ) -> MetadataImportInfo { + MetadataImportInfo { + method: MetadataImportMethod::ByName(map.import_name().to_string()), + module: MetadataModuleInfo { + name: map.import_scope().name().to_string(), + }, + } + } + + pub fn value_to_u64(&self, value: &Value) -> Option { + match value { + Value::Bool(b) => Some(*b as u64), + Value::U8(i) => Some(*i as u64), + Value::I8(i) => Some(*i as u64), + Value::U16(i) => Some(*i as u64), + Value::I16(i) => Some(*i as u64), + Value::U32(i) => Some(*i as u64), + Value::I32(i) => Some(*i as u64), + Value::U64(i) => Some(*i), + Value::I64(i) => Some(*i as u64), + _ => None, + } + } +}