diff options
| author | Mason Reed <mason@vector35.com> | 2026-02-11 18:04:07 -0800 |
|---|---|---|
| committer | Mason Reed <35282038+emesare@users.noreply.github.com> | 2026-02-23 00:09:44 -0800 |
| commit | 37008b7fa16837d04c1658868646cad681cbe035 (patch) | |
| tree | 577c2b62ee47c78a5d31d11f2aa610e441f5c808 /plugins/bntl_utils/cli/src | |
| parent | 837f8590be80b7c98162e70e4f0c1814b83e9d7b (diff) | |
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.
Diffstat (limited to 'plugins/bntl_utils/cli/src')
| -rw-r--r-- | plugins/bntl_utils/cli/src/create.rs | 79 | ||||
| -rw-r--r-- | plugins/bntl_utils/cli/src/diff.rs | 37 | ||||
| -rw-r--r-- | plugins/bntl_utils/cli/src/dump.rs | 26 | ||||
| -rw-r--r-- | plugins/bntl_utils/cli/src/input.rs | 167 | ||||
| -rw-r--r-- | plugins/bntl_utils/cli/src/main.rs | 71 | ||||
| -rw-r--r-- | plugins/bntl_utils/cli/src/validate.rs | 66 |
6 files changed, 446 insertions, 0 deletions
diff --git a/plugins/bntl_utils/cli/src/create.rs b/plugins/bntl_utils/cli/src/create.rs new file mode 100644 index 00000000..a56b1cdb --- /dev/null +++ b/plugins/bntl_utils/cli/src/create.rs @@ -0,0 +1,79 @@ +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<PathBuf>, + #[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 { + // Place the type libraries in a folder with the architecture name, as that is necessary + // information for the user to correctly place the following type libraries in the user directory. + let arch_output_path = output_path.join(type_library.arch().name()); + std::fs::create_dir_all(&arch_output_path) + .expect("Failed to create architecture directory"); + let output_path = arch_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 00000000..1eedb970 --- /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<u64>, +} + +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 00000000..fb25446b --- /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<PathBuf>, +} + +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 00000000..1e1dbb6e --- /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<Project>), + ProjectFolder(Ref<ProjectFolder>), + ProjectFile(Ref<ProjectFile>), +} + +#[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<ResolvedInput, InputResolveError> { + 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<Self, Self::Err> { + // 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 00000000..1466da1f --- /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 00000000..743ca927 --- /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<PathBuf>, +} + +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<ValidateIssue, Vec<String>> = 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), + } + } + }); + } +} |
