summaryrefslogtreecommitdiff
path: root/plugins/bntl_utils/cli/src/input.rs
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/input.rs
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/input.rs')
-rw-r--r--plugins/bntl_utils/cli/src/input.rs167
1 files changed, 167 insertions, 0 deletions
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()),
+ }
+ }
+}