summaryrefslogtreecommitdiff
path: root/plugins/bntl_utils/src/validate.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/src/validate.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/src/validate.rs')
-rw-r--r--plugins/bntl_utils/src/validate.rs347
1 files changed, 347 insertions, 0 deletions
diff --git a/plugins/bntl_utils/src/validate.rs b/plugins/bntl_utils/src/validate.rs
new file mode 100644
index 00000000..ecd0978d
--- /dev/null
+++ b/plugins/bntl_utils/src/validate.rs
@@ -0,0 +1,347 @@
+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<ValidateIssue>,
+}
+
+impl ValidateResult {
+ /// Render the validation report as HTML.
+ pub fn render_report(&self) -> Result<String, minijinja::Error> {
+ 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<String, String>,
+ // TODO: This needs to be by platform as well.
+ pub seen_dependency_names: HashMap<String, String>,
+ /// These are the type libraries that are accessible to the type library under validation.
+ ///
+ /// Used to validate external references.
+ pub type_libraries: Vec<Ref<TypeLibrary>>,
+ /// Built from the available type libraries.
+ pub valid_external_references: HashSet<QualifiedName>,
+}
+
+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<Ref<TypeLibrary>>) -> 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<ValidateIssue> {
+ 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<ValidateIssue> {
+ 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<ValidateIssue> {
+ let mut issues = Vec::new();
+ let tmp_type_lib_path = temp_dir().join(type_lib.name());
+ if !type_lib.decompress_to_file(&tmp_type_lib_path) {
+ tracing::error!(
+ "Failed to decompress type library to temporary file: {}",
+ type_lib.name()
+ );
+ return issues;
+ }
+ 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<ValidateIssue> {
+ 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<ValidateIssue> {
+ 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::<u64>() 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
+ }
+}