summaryrefslogtreecommitdiff
path: root/plugins/bntl_utils/cli/src
diff options
context:
space:
mode:
authorMason Reed <mason@vector35.com>2026-02-11 18:04:07 -0800
committerMason Reed <35282038+emesare@users.noreply.github.com>2026-02-23 00:09:44 -0800
commit37008b7fa16837d04c1658868646cad681cbe035 (patch)
tree577c2b62ee47c78a5d31d11f2aa610e441f5c808 /plugins/bntl_utils/cli/src
parent837f8590be80b7c98162e70e4f0c1814b83e9d7b (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.rs79
-rw-r--r--plugins/bntl_utils/cli/src/diff.rs37
-rw-r--r--plugins/bntl_utils/cli/src/dump.rs26
-rw-r--r--plugins/bntl_utils/cli/src/input.rs167
-rw-r--r--plugins/bntl_utils/cli/src/main.rs71
-rw-r--r--plugins/bntl_utils/cli/src/validate.rs66
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),
+ }
+ }
+ });
+ }
+}