diff --git a/CHANGELOG.md b/CHANGELOG.md index e820841..7667266 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ - Allow opening a PDB file and diffing two PDB files using drag and drop (@learn-more) - Add support for the `wasm32-unknown-unknown` target - Publish a web version of `resym` from the `main` branch automatically -- Allowing loading PDBs from URLs +- Allowing loading PDBs from URLs (but the feature can be disabled at build time) +- Implement basic module reconstruction and diffing capabilities +- Add 3 new commands to `resymc`: `list-modules`, `dump-module`, `diff-module` ### Changed diff --git a/Cargo.lock b/Cargo.lock index 450488e..bab2261 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2968,6 +2968,7 @@ checksum = "216080ab382b992234dda86873c18d4c48358f5cfcb70fd693d7f6f2131b628b" name = "resym" version = "0.3.0" dependencies = [ + "ahash", "anyhow", "console_error_panic_hook", "crossbeam-channel", diff --git a/resym/Cargo.toml b/resym/Cargo.toml index d8ab722..1a8e890 100644 --- a/resym/Cargo.toml +++ b/resym/Cargo.toml @@ -36,6 +36,10 @@ rfd = "0.11" # Note(ergrelet): `fancy-regex` is less performant than `onig` at the moment # but is more portable (i.e., compiles to wasm32) syntect = { version = "5.0", default-features = false, features=["default-fancy"] } +ahash = { version = "0.8", default-features = false, features = [ + "no-rng", # we don't need DOS-protection, so we let users opt-in to it instead + "std", +] } # Web: [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/resym/src/lib.rs b/resym/src/lib.rs index acb6f91..fee58d0 100644 --- a/resym/src/lib.rs +++ b/resym/src/lib.rs @@ -3,6 +3,10 @@ mod frontend; #[cfg(target_arch = "wasm32")] mod mode; #[cfg(target_arch = "wasm32")] +mod module_tree; +#[cfg(target_arch = "wasm32")] +mod module_tree_view; +#[cfg(target_arch = "wasm32")] mod resym_app; #[cfg(target_arch = "wasm32")] mod settings; diff --git a/resym/src/main.rs b/resym/src/main.rs index aae7450..422cd55 100644 --- a/resym/src/main.rs +++ b/resym/src/main.rs @@ -2,6 +2,8 @@ mod frontend; mod mode; +mod module_tree; +mod module_tree_view; mod resym_app; mod settings; mod syntax_highlighting; diff --git a/resym/src/module_tree.rs b/resym/src/module_tree.rs new file mode 100644 index 0000000..4dabe98 --- /dev/null +++ b/resym/src/module_tree.rs @@ -0,0 +1,418 @@ +use std::{collections::HashMap, path::Path, sync::Arc}; + +use anyhow::{anyhow, Result}; + +const MODULE_PATH_SEPARATOR: &str = "\\"; + +/// Tree of module paths, plus info at the leaves. +/// +/// The tree contains a list of subtrees, and so on recursively. +pub struct ModuleTreeNode { + /// Full path to the root of this tree + pub path: ModulePath, + /// Direct descendants of this (sub)tree + pub children: HashMap, + + /// Information on the module (only available for leaves) + pub module_info: Option, +} + +impl ModuleTreeNode { + /// Add a module to the tree + pub fn add_module_by_path( + &mut self, + module_path: ModulePath, + module_info: ModuleInfo, + ) -> Result<()> { + if !module_path.is_descendant_of(&self.path) { + return Err(anyhow!("Module doesn't belong to the tree")); + } + + // Direct child of ours + if module_path.is_child_of(&self.path) { + let result = match module_path.last() { + None => return Err(anyhow!("Module path is empty")), + Some(last_part) => { + // Direct child of the current node, add it to our children + self.add_child(last_part, Some(module_info)); + + Ok(()) + } + }; + return result; + } + + // Not a direct child of ours, pass it down to our children if we have any + for child_node in self.children.values_mut() { + if module_path.is_descendant_of(&child_node.path) { + return child_node.add_module_by_path(module_path, module_info); + } + } + + // Not a direct child of ours, no children to pass it to. + // We need to create the missing descendant(s) + let start_index = self.path.len(); + let end_index = module_path.len(); + let mut current_node = self; + for i in start_index..end_index { + let new_child_part = &module_path.path.parts[i]; + let new_child_is_leaf = i == end_index - 1; + let new_child_module_info = if new_child_is_leaf { + Some(module_info) + } else { + None + }; + current_node.add_child(new_child_part, new_child_module_info); + // Note: if `get_mut` fails, it means there's a bug in our code + current_node = current_node + .children + .get_mut(new_child_part) + .expect("key should exist in map"); + } + + Ok(()) + } + + fn add_child(&mut self, path: &ModulePathPart, module_info: Option) { + self.children.insert( + path.clone(), + ModuleTreeNode { + path: self.path.join(&ModulePath::new(vec![path.clone()])), + children: Default::default(), + module_info, + }, + ); + } +} + +impl Default for ModuleTreeNode { + fn default() -> Self { + Self { + path: ModulePath::root(), + children: Default::default(), + module_info: Default::default(), + } + } +} + +#[derive(Copy, Clone)] +pub struct ModuleInfo { + pub pdb_index: usize, +} + +type ModulePathPart = String; + +#[derive(Clone, Eq, PartialEq)] +pub struct ModulePath { + /// precomputed hash + hash: ModulePathHash, + + // [`Arc`] used for cheap cloning, and to keep down the size of [`ModulePath`]. + // We mostly use the hash for lookups and comparisons anyway! + path: Arc, +} + +impl ModulePath { + #[inline] + pub fn root() -> Self { + Self::from(ModulePathImpl::root()) + } + + #[inline] + pub fn new(parts: Vec) -> Self { + Self::from(parts) + } + + #[inline] + pub fn iter(&self) -> impl Iterator { + self.path.iter() + } + + pub fn last(&self) -> Option<&ModulePathPart> { + self.path.last() + } + + #[inline] + pub fn as_slice(&self) -> &[ModulePathPart] { + self.path.as_slice() + } + + #[inline] + pub fn is_root(&self) -> bool { + self.path.is_root() + } + + /// Is this a strict descendant of the given path. + #[inline] + pub fn is_descendant_of(&self, other: &ModulePath) -> bool { + other.len() < self.len() && self.path.iter().zip(other.iter()).all(|(a, b)| a == b) + } + + /// Is this a direct child of the other path. + #[inline] + pub fn is_child_of(&self, other: &ModulePath) -> bool { + other.len() + 1 == self.len() && self.path.iter().zip(other.iter()).all(|(a, b)| a == b) + } + + /// Number of parts + #[inline] + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.path.len() + } + + #[inline] + pub fn hash(&self) -> ModulePathHash { + self.hash + } + + /// Precomputed 64-bit hash. + #[inline] + pub fn hash64(&self) -> u64 { + self.hash.hash64() + } + + /// Return [`None`] if root. + #[must_use] + pub fn parent(&self) -> Option { + self.path.parent().map(Self::from) + } + + pub fn join(&self, other: &Self) -> Self { + self.iter().chain(other.iter()).cloned().collect() + } +} + +impl FromIterator for ModulePath { + fn from_iter>(parts: T) -> Self { + Self::new(parts.into_iter().collect()) + } +} + +impl<'a> FromIterator<&'a ModulePathPart> for ModulePath { + fn from_iter>(parts: T) -> Self { + Self::new(parts.into_iter().cloned().collect()) + } +} + +impl From for ModulePath { + #[inline] + fn from(path: ModulePathImpl) -> Self { + Self { + hash: ModulePathHash(Hash64::hash(&path)), + path: Arc::new(path), + } + } +} + +impl From> for ModulePath { + #[inline] + fn from(path: Vec) -> Self { + Self { + hash: ModulePathHash(Hash64::hash(&path)), + path: Arc::new(ModulePathImpl { parts: path }), + } + } +} + +impl From<&[ModulePathPart]> for ModulePath { + #[inline] + fn from(path: &[ModulePathPart]) -> Self { + Self::from(path.to_vec()) + } +} + +impl From<&str> for ModulePath { + #[inline] + fn from(path: &str) -> Self { + Self::from(parse_module_path(path)) + } +} + +impl From for ModulePath { + #[inline] + fn from(path: String) -> Self { + Self::from(path.as_str()) + } +} + +impl ToString for ModulePath { + fn to_string(&self) -> String { + self.path.parts.join(MODULE_PATH_SEPARATOR) + } +} + +impl From for String { + #[inline] + fn from(path: ModulePath) -> Self { + path.to_string() + } +} + +fn parse_module_path(path: &str) -> Vec { + let path = Path::new(path); + let parts = path + .components() + .filter_map(|component| match component { + std::path::Component::RootDir => None, + std::path::Component::CurDir => Some(ModulePathPart::from(".")), + std::path::Component::ParentDir => Some(ModulePathPart::from("..")), + std::path::Component::Prefix(windows_prefix) => Some(ModulePathPart::from( + windows_prefix.as_os_str().to_str().unwrap_or_default(), + )), + std::path::Component::Normal(part) => { + Some(ModulePathPart::from(part.to_str().unwrap_or_default())) + } + }) + .collect(); + + parts +} + +#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct ModulePathImpl { + parts: Vec, +} + +impl ModulePathImpl { + #[inline] + pub fn root() -> Self { + Self { parts: vec![] } + } + + #[inline] + pub fn new(parts: Vec) -> Self { + Self { parts } + } + + #[inline] + pub fn as_slice(&self) -> &[ModulePathPart] { + self.parts.as_slice() + } + + #[inline] + pub fn is_root(&self) -> bool { + self.parts.is_empty() + } + + /// Number of components + #[inline] + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.parts.len() + } + + #[inline] + pub fn iter(&self) -> impl Iterator { + self.parts.iter() + } + + #[inline] + pub fn last(&self) -> Option<&ModulePathPart> { + self.parts.last() + } + + #[inline] + pub fn push(&mut self, comp: ModulePathPart) { + self.parts.push(comp); + } + + /// Return [`None`] if root. + #[must_use] + pub fn parent(&self) -> Option { + if self.parts.is_empty() { + None + } else { + Some(Self::new(self.parts[..(self.parts.len() - 1)].to_vec())) + } + } +} + +/// A 64 bit hash of [`ModulePath`] with very small risk of collision. +#[derive(Copy, Clone, Eq)] +pub struct ModulePathHash(Hash64); + +impl ModulePathHash { + /// Sometimes used as the hash of `None`. + pub const NONE: ModulePathHash = ModulePathHash(Hash64::ZERO); + + /// From an existing u64. Use this only for data conversions. + #[inline] + pub fn from_u64(i: u64) -> Self { + Self(Hash64::from_u64(i)) + } + + #[inline] + pub fn hash64(&self) -> u64 { + self.0.hash64() + } + + #[inline] + pub fn is_some(&self) -> bool { + *self != Self::NONE + } + + #[inline] + pub fn is_none(&self) -> bool { + *self == Self::NONE + } +} + +impl std::hash::Hash for ModulePathHash { + #[inline] + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} + +impl std::cmp::PartialEq for ModulePathHash { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0.eq(&other.0) + } +} + +#[derive(Copy, Clone, Eq)] +pub struct Hash64(u64); + +impl Hash64 { + pub const ZERO: Hash64 = Hash64(0); + + pub fn hash(value: impl std::hash::Hash + Copy) -> Self { + Self(hash(value)) + } + + /// From an existing u64. Use this only for data conversions. + #[inline] + pub fn from_u64(i: u64) -> Self { + Self(i) + } + + #[inline] + pub fn hash64(&self) -> u64 { + self.0 + } +} + +impl std::hash::Hash for Hash64 { + #[inline] + fn hash(&self, state: &mut H) { + state.write_u64(self.0); + } +} + +impl std::cmp::PartialEq for Hash64 { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +pub const HASH_RANDOM_STATE: ahash::RandomState = ahash::RandomState::with_seeds(0, 1, 2, 3); + +/// Hash the given value. +#[inline] +fn hash(value: impl std::hash::Hash) -> u64 { + // Don't use ahash::AHasher::default() since it uses a random number for seeding the hasher on every application start. + HASH_RANDOM_STATE.hash_one(&value) +} diff --git a/resym/src/module_tree_view.rs b/resym/src/module_tree_view.rs new file mode 100644 index 0000000..8f91394 --- /dev/null +++ b/resym/src/module_tree_view.rs @@ -0,0 +1,137 @@ +use crate::module_tree::{ModuleInfo, ModulePath, ModuleTreeNode}; + +const MODULE_PATH_SEPARATOR: &str = "\\"; + +pub struct ModuleTreeView { + /// Direct descendants of this (sub)tree + pub children: Vec, +} + +impl ModuleTreeView { + pub fn new() -> Self { + ModuleTreeView { + children: Default::default(), + } + } + + /// Create a new `ModuleTreeView` from a `ModuleTreeNode` by merging all + /// nodes which only have 1 child together, recursively. + /// + /// This allows reducing the depth of the tree without losing information. + /// The idea is to reduce the "size" of the tree to ease browsing. + pub fn from_tree_node(root_node: ModuleTreeNode) -> Self { + let mut root_node_children: Vec = root_node + .children + .into_iter() + .map(|(name, node)| ModuleTreeViewNode { + tree_node: node, + name, + children: Default::default(), + }) + .collect(); + + for view_node in root_node_children.iter_mut() { + populate_tree_view(view_node); + } + // Sort children + root_node_children.sort_by(sort_tree_view_leaves); + + ModuleTreeView { + children: root_node_children, + } + } +} + +pub struct ModuleTreeViewNode { + /// Backing node + tree_node: ModuleTreeNode, + /// Node name + pub name: String, + /// Direct descendants of this (sub)tree + pub children: Vec, +} + +impl ModuleTreeViewNode { + #[inline] + pub fn new(name: String, tree_node: ModuleTreeNode) -> Self { + ModuleTreeViewNode { + tree_node, + name, + children: Default::default(), + } + } + + #[inline] + pub fn is_leaf(&self) -> bool { + self.children.is_empty() + } + + #[inline] + pub fn path(&self) -> &ModulePath { + &self.tree_node.path + } + + #[inline] + pub fn module_info(&self) -> Option { + self.tree_node.module_info + } +} + +pub fn populate_tree_view(view_node: &mut ModuleTreeViewNode) { + let tree_node_children = std::mem::take(&mut view_node.tree_node.children); + match tree_node_children.len() { + 0 => { + // Nothing to do + } + 1 => { + // Merge with unique child, if that child is not a leaf + let (unique_child_name, unique_child_node) = tree_node_children + .into_iter() + .next() + .expect("map should contain one element"); + + let mut child_view_node = ModuleTreeViewNode::new(unique_child_name, unique_child_node); + // Populate the child node + populate_tree_view(&mut child_view_node); + + if child_view_node.is_leaf() { + // Child is a leaf, keep it as a child + view_node.children.push(child_view_node); + } else { + // Child isn't a leaf, merge with it + view_node.tree_node = child_view_node.tree_node; + view_node.name = format!( + "{}{}{}", + view_node.name, MODULE_PATH_SEPARATOR, child_view_node.name + ); + view_node.children = child_view_node.children; + } + } + _ => { + // Merge children with their descendants + for (child_name, child_node) in tree_node_children.into_iter() { + let mut child_view_node = ModuleTreeViewNode::new(child_name, child_node); + + // Populate the child node + populate_tree_view(&mut child_view_node); + view_node.children.push(child_view_node); + } + // Sort children + view_node.children.sort_by(sort_tree_view_leaves); + } + } +} + +fn sort_tree_view_leaves(lhs: &ModuleTreeViewNode, rhs: &ModuleTreeViewNode) -> std::cmp::Ordering { + if lhs.is_leaf() == rhs.is_leaf() { + // Compare names when both nodes are leaves or inner nodes + lhs.name.cmp(&rhs.name) + } else { + // Else, put inner nodes before leaves + if lhs.is_leaf() { + std::cmp::Ordering::Greater + } else { + std::cmp::Ordering::Less + } + } +} diff --git a/resym/src/resym_app.rs b/resym/src/resym_app.rs index eb9d865..660b329 100644 --- a/resym/src/resym_app.rs +++ b/resym/src/resym_app.rs @@ -15,10 +15,11 @@ use crate::ui_components::OpenURLComponent; use crate::{ frontend::EguiFrontendController, mode::ResymAppMode, + module_tree::{ModuleInfo, ModulePath}, settings::ResymAppSettings, ui_components::{ - CodeViewComponent, ConsoleComponent, SettingsComponent, TypeListComponent, - TypeSearchComponent, + CodeViewComponent, ConsoleComponent, ModuleTreeComponent, SettingsComponent, + TextSearchComponent, TypeListComponent, }, }; @@ -33,18 +34,27 @@ pub enum ResymPDBSlots { Diff = 1, } -impl Into for ResymPDBSlots { - fn into(self) -> PDBSlot { - self as PDBSlot +impl From for PDBSlot { + fn from(val: ResymPDBSlots) -> Self { + val as PDBSlot } } +#[derive(PartialEq)] +enum ExplorerTab { + TypeSearch, + ModuleBrowsing, +} + /// Struct that represents our GUI application. /// It contains the whole application's context at all time. pub struct ResymApp { current_mode: ResymAppMode, - type_search: TypeSearchComponent, + explorer_selected_tab: ExplorerTab, + type_search: TextSearchComponent, type_list: TypeListComponent, + module_search: TextSearchComponent, + module_tree: ModuleTreeComponent, code_view: CodeViewComponent, console: ConsoleComponent, settings: SettingsComponent, @@ -131,8 +141,11 @@ impl ResymApp { log::info!("{} {}", PKG_NAME, PKG_VERSION); Ok(Self { current_mode: ResymAppMode::Idle, - type_search: TypeSearchComponent::new(), + explorer_selected_tab: ExplorerTab::TypeSearch, + type_search: TextSearchComponent::new(), type_list: TypeListComponent::new(), + module_search: TextSearchComponent::new(), + module_tree: ModuleTreeComponent::new(), code_view: CodeViewComponent::new(), console: ConsoleComponent::new(logger), settings: SettingsComponent::new(app_settings), @@ -169,25 +182,126 @@ impl ResymApp { .default_width(250.0) .width_range(100.0..=f32::INFINITY) .show(ctx, |ui| { - ui.label("Search"); - ui.add_space(4.0); - - // Update the type search bar - self.type_search.update( - &self.settings.app_settings, - &self.current_mode, - &self.backend, - ui, - ); - ui.add_space(4.0); - - // Update the type list - self.type_list.update( - &self.settings.app_settings, - &self.current_mode, - &self.backend, - ui, - ); + ui.horizontal(|ui| { + ui.selectable_value( + &mut self.explorer_selected_tab, + ExplorerTab::TypeSearch, + "Search types", + ); + ui.selectable_value( + &mut self.explorer_selected_tab, + ExplorerTab::ModuleBrowsing, + "Browse modules", + ); + }); + ui.separator(); + + match self.explorer_selected_tab { + ExplorerTab::TypeSearch => { + // Callback run when the search query changes + let on_query_update = |search_query: &str| { + // Update filtered list if filter has changed + let result = if let ResymAppMode::Comparing(..) = self.current_mode { + self.backend + .send_command(BackendCommand::UpdateTypeFilterMerged( + vec![ + ResymPDBSlots::Main as usize, + ResymPDBSlots::Diff as usize, + ], + search_query.to_string(), + self.settings.app_settings.search_case_insensitive, + self.settings.app_settings.search_use_regex, + )) + } else { + self.backend.send_command(BackendCommand::UpdateTypeFilter( + ResymPDBSlots::Main as usize, + search_query.to_string(), + self.settings.app_settings.search_case_insensitive, + self.settings.app_settings.search_use_regex, + )) + }; + if let Err(err) = result { + log::error!("Failed to update type filter value: {}", err); + } + }; + + // Update the type search bar + ui.label("Search"); + self.type_search.update(ui, &on_query_update); + ui.separator(); + ui.add_space(4.0); + + // Update the type list + self.type_list.update( + &self.settings.app_settings, + &self.current_mode, + &self.backend, + ui, + ); + } + ExplorerTab::ModuleBrowsing => { + // Callback run when the search query changes + let on_query_update = |search_query: &str| match self.current_mode { + ResymAppMode::Browsing(..) | ResymAppMode::Comparing(..) => { + // Request a module list update + if let Err(err) = + self.backend.send_command(BackendCommand::ListModules( + ResymPDBSlots::Main as usize, + search_query.to_string(), + self.settings.app_settings.search_case_insensitive, + self.settings.app_settings.search_use_regex, + )) + { + log::error!("Failed to update module list: {}", err); + } + } + _ => {} + }; + // Update the type search bar + ui.label("Search"); + self.module_search.update(ui, &on_query_update); + ui.separator(); + ui.add_space(4.0); + + // Callback run when a module is selected in the tree + let on_module_selected = + |module_path: &ModulePath, module_info: &ModuleInfo| match self + .current_mode + { + ResymAppMode::Browsing(..) => { + if let Err(err) = self.backend.send_command( + BackendCommand::ReconstructModuleByIndex( + ResymPDBSlots::Main as usize, + module_info.pdb_index, + self.settings.app_settings.primitive_types_flavor, + self.settings.app_settings.print_header, + ), + ) { + log::error!("Failed to reconstruct module: {}", err); + } + } + + ResymAppMode::Comparing(..) => { + if let Err(err) = + self.backend.send_command(BackendCommand::DiffModuleByPath( + ResymPDBSlots::Main as usize, + ResymPDBSlots::Diff as usize, + module_path.to_string(), + self.settings.app_settings.primitive_types_flavor, + self.settings.app_settings.print_header, + )) + { + log::error!("Failed to reconstruct type diff: {}", err); + } + } + + _ => log::error!("Invalid application state"), + }; + + // Update the module list + self.module_tree.update(ctx, ui, &on_module_selected); + } + } }); } @@ -230,7 +344,7 @@ impl ResymApp { } }); }); - ui.add_space(4.0); + ui.separator(); // Update the code view component self.code_view @@ -313,6 +427,17 @@ impl ResymApp { { log::error!("Failed to update type filter value: {}", err); } + // Request a module list update + if let Err(err) = + self.backend.send_command(BackendCommand::ListModules( + ResymPDBSlots::Main as usize, + String::default(), + false, + false, + )) + { + log::error!("Failed to update module list: {}", err); + } } else if pdb_slot == ResymPDBSlots::Diff as usize { self.current_mode = ResymAppMode::Comparing( String::default(), @@ -357,7 +482,12 @@ impl ResymApp { FrontendCommand::ReconstructTypeResult(type_reconstruction_result) => { match type_reconstruction_result { Err(err) => { - log::error!("Failed to reconstruct type: {}", err); + let error_msg = format!("Failed to reconstruct type: {}", err); + log::error!("{}", &error_msg); + + // Show an empty "reconstruted" view + self.current_mode = + ResymAppMode::Browsing(Default::default(), 0, error_msg); } Ok(reconstructed_type) => { let last_line_number = 1 + reconstructed_type.lines().count(); @@ -375,9 +505,54 @@ impl ResymApp { } } - FrontendCommand::DiffTypeResult(type_diff_result) => match type_diff_result { + FrontendCommand::UpdateModuleList(module_list_result) => match module_list_result { Err(err) => { - log::error!("Failed to diff type: {}", err); + log::error!("Failed to retrieve module list: {}", err); + } + Ok(module_list) => { + self.module_tree.set_module_list(module_list); + } + }, + + FrontendCommand::ReconstructModuleResult(module_reconstruction_result) => { + match module_reconstruction_result { + Err(err) => { + let error_msg = format!("Failed to reconstruct module: {}", err); + log::error!("{}", &error_msg); + + // Show an empty "reconstruted" view + self.current_mode = + ResymAppMode::Browsing(Default::default(), 0, error_msg); + } + Ok(reconstructed_module) => { + let last_line_number = 1 + reconstructed_module.lines().count(); + let line_numbers = + (1..last_line_number).fold(String::default(), |mut acc, e| { + let _r = writeln!(&mut acc, "{e}"); + acc + }); + self.current_mode = ResymAppMode::Browsing( + line_numbers, + last_line_number, + reconstructed_module, + ); + } + } + } + + FrontendCommand::DiffResult(type_diff_result) => match type_diff_result { + Err(err) => { + let error_msg = format!("Failed to generate diff: {}", err); + log::error!("{}", &error_msg); + + // Show an empty "reconstruted" view + self.current_mode = ResymAppMode::Comparing( + Default::default(), + Default::default(), + 0, + vec![], + error_msg, + ); } Ok(type_diff) => { let mut last_line_number = 1; diff --git a/resym/src/ui_components/mod.rs b/resym/src/ui_components/mod.rs index aec69e1..3f10b59 100644 --- a/resym/src/ui_components/mod.rs +++ b/resym/src/ui_components/mod.rs @@ -1,15 +1,17 @@ mod code_view; mod console; +mod module_tree; #[cfg(feature = "http")] mod open_url; mod settings; +mod text_search; mod type_list; -mod type_search; pub use code_view::*; pub use console::*; +pub use module_tree::*; #[cfg(feature = "http")] pub use open_url::*; pub use settings::*; +pub use text_search::*; pub use type_list::*; -pub use type_search::*; diff --git a/resym/src/ui_components/module_tree.rs b/resym/src/ui_components/module_tree.rs new file mode 100644 index 0000000..a23d592 --- /dev/null +++ b/resym/src/ui_components/module_tree.rs @@ -0,0 +1,111 @@ +use std::cell::RefCell; + +use eframe::egui::{self, ScrollArea}; + +use resym_core::frontend::ModuleList; + +use crate::{ + module_tree::{ModuleInfo, ModulePath, ModuleTreeNode}, + module_tree_view::{ModuleTreeView, ModuleTreeViewNode}, +}; + +/// UI component in charge of rendering a tree of PDB modules +pub struct ModuleTreeComponent { + /// Tree data + module_tree_view: ModuleTreeView, + /// Index of the currently selected module + selected_module: RefCell, +} + +impl ModuleTreeComponent { + pub fn new() -> Self { + Self { + module_tree_view: ModuleTreeView::new(), + selected_module: usize::MAX.into(), + } + } + + /// Update the list of modules that the tree contains + pub fn set_module_list(&mut self, module_list: ModuleList) { + // Generate the module tree + let mut root_tree_node = ModuleTreeNode::default(); + for (module_path, module_index) in module_list.iter() { + let module_path = ModulePath::from(module_path.as_str()); + // Add module to the tree + if let Err(err) = root_tree_node.add_module_by_path( + module_path, + ModuleInfo { + pdb_index: *module_index, + }, + ) { + // Log error and continue + log::warn!("Failed to add module to tree: {}", err); + } + } + // Get a view of the module tree and store it + self.module_tree_view = ModuleTreeView::from_tree_node(root_tree_node); + } + + /// Update/render the UI component + pub fn update( + &self, + ctx: &egui::Context, + ui: &mut egui::Ui, + on_module_selected: &CB, + ) { + ScrollArea::vertical() + .auto_shrink([false, false]) + .show(ui, |ui| { + self.module_tree_view.children.iter().for_each(|view_node| { + self.update_module_tree(ctx, ui, view_node, on_module_selected); + }); + }); + } + + fn update_module_tree( + &self, + ctx: &egui::Context, + ui: &mut egui::Ui, + view_node: &ModuleTreeViewNode, + on_module_selected: &CB, + ) { + if view_node.is_leaf() { + self.update_module_leaf(ui, view_node, on_module_selected); + } else { + egui::collapsing_header::CollapsingState::load_with_default_open( + ctx, + ui.id().with(view_node.path().hash()), + false, + ) + .show_header(ui, |ui| { + ui.label(&view_node.name); + }) + .body(|ui| { + view_node.children.iter().for_each(|view_node| { + self.update_module_tree(ctx, ui, view_node, on_module_selected); + }); + }); + } + } + + fn update_module_leaf( + &self, + ui: &mut egui::Ui, + view_node: &ModuleTreeViewNode, + on_module_selected: &CB, + ) { + if let Some(ref module_info) = view_node.module_info() { + if ui + .selectable_label( + *self.selected_module.borrow() == module_info.pdb_index, + &view_node.name, + ) + .clicked() + { + *self.selected_module.borrow_mut() = module_info.pdb_index; + // Invoke event callback + on_module_selected(view_node.path(), module_info); + } + } + } +} diff --git a/resym/src/ui_components/text_search.rs b/resym/src/ui_components/text_search.rs new file mode 100644 index 0000000..26658bc --- /dev/null +++ b/resym/src/ui_components/text_search.rs @@ -0,0 +1,20 @@ +use eframe::egui; + +pub struct TextSearchComponent { + search_filter: String, +} + +impl TextSearchComponent { + pub fn new() -> Self { + Self { + search_filter: String::default(), + } + } + + /// Update/render the UI component + pub fn update(&mut self, ui: &mut egui::Ui, on_query_update: &CB) { + if ui.text_edit_singleline(&mut self.search_filter).changed() { + on_query_update(self.search_filter.as_str()); + } + } +} diff --git a/resym/src/ui_components/type_search.rs b/resym/src/ui_components/type_search.rs deleted file mode 100644 index a87ceba..0000000 --- a/resym/src/ui_components/type_search.rs +++ /dev/null @@ -1,46 +0,0 @@ -use eframe::egui; -use resym_core::backend::{Backend, BackendCommand}; - -use crate::{mode::ResymAppMode, resym_app::ResymPDBSlots, settings::ResymAppSettings}; - -pub struct TypeSearchComponent { - search_filter: String, -} - -impl TypeSearchComponent { - pub fn new() -> Self { - Self { - search_filter: String::default(), - } - } - - pub fn update( - &mut self, - app_settings: &ResymAppSettings, - current_mode: &ResymAppMode, - backend: &Backend, - ui: &mut egui::Ui, - ) { - if ui.text_edit_singleline(&mut self.search_filter).changed() { - // Update filtered list if filter has changed - let result = if let ResymAppMode::Comparing(..) = current_mode { - backend.send_command(BackendCommand::UpdateTypeFilterMerged( - vec![ResymPDBSlots::Main as usize, ResymPDBSlots::Diff as usize], - self.search_filter.clone(), - app_settings.search_case_insensitive, - app_settings.search_use_regex, - )) - } else { - backend.send_command(BackendCommand::UpdateTypeFilter( - ResymPDBSlots::Main as usize, - self.search_filter.clone(), - app_settings.search_case_insensitive, - app_settings.search_use_regex, - )) - }; - if let Err(err) = result { - log::error!("Failed to update type filter value: {}", err); - } - } - } -} diff --git a/resym_core/src/backend.rs b/resym_core/src/backend.rs index 005ec62..bf0ed7c 100644 --- a/resym_core/src/backend.rs +++ b/resym_core/src/backend.rs @@ -11,7 +11,8 @@ use rayon::{ #[cfg(all(not(feature = "rayon"), not(target_arch = "wasm32")))] use std::thread::{self, JoinHandle}; use std::{ - collections::{BTreeMap, BTreeSet}, + cell::RefCell, + collections::{BTreeSet, HashMap}, io, sync::Arc, }; @@ -20,13 +21,13 @@ use std::{path::PathBuf, time::Instant}; #[cfg(all(not(feature = "rayon"), target_arch = "wasm32"))] use wasm_thread::{self as thread, JoinHandle}; -use crate::pdb_file::PDBDataSource; +use crate::{diffing::diff_module_by_path, pdb_file::PDBDataSource}; use crate::{ - cond_par_iter, cond_sort_by, diffing::diff_type_by_name, error::{Result, ResymCoreError}, frontend::FrontendCommand, - frontend::FrontendController, + frontend::{FrontendController, ModuleList}, + par_iter_if_available, par_sort_by_if_available, pdb_file::PdbFile, pdb_types::{include_headers_for_flavor, PrimitiveReconstructionFlavor}, PKG_VERSION, @@ -72,7 +73,11 @@ pub enum BackendCommand { /// Retrieve a list of types that match the given filter for multiple PDBs /// and merge the result. UpdateTypeFilterMerged(Vec, String, bool, bool), - /// Reconstruct a diff of a type given its name. + /// Retrieve the list of all modules in a given PDB. + ListModules(PDBSlot, String, bool, bool), + /// Reconstruct a module given its index for a given PDB. + ReconstructModuleByIndex(PDBSlot, usize, PrimitiveReconstructionFlavor, bool), + /// Reconstruct the diff of a type given its name. DiffTypeByName( PDBSlot, PDBSlot, @@ -82,6 +87,14 @@ pub enum BackendCommand { bool, bool, ), + /// Reconstruct the diff of a module given its path. + DiffModuleByPath( + PDBSlot, + PDBSlot, + String, + PrimitiveReconstructionFlavor, + bool, + ), } /// Struct that represents the backend. The backend is responsible @@ -153,12 +166,12 @@ impl Backend { } /// Main backend routine. This processes commands sent by the frontend and sends -/// the result back. +/// results back. fn worker_thread_routine( rx_worker: Receiver, frontend_controller: Arc, ) -> Result<()> { - let mut pdb_files: BTreeMap> = BTreeMap::new(); + let mut pdb_files: HashMap>> = HashMap::new(); while let Ok(command) = rx_worker.recv() { match command { #[cfg(not(target_arch = "wasm32"))] @@ -170,8 +183,11 @@ fn worker_thread_routine( Ok(loaded_pdb_file) => { frontend_controller .send_command(FrontendCommand::LoadPDBResult(Ok(pdb_slot)))?; - if let Some(pdb_file) = pdb_files.insert(pdb_slot, loaded_pdb_file) { - log::info!("'{}' has been unloaded.", pdb_file.file_path.display()); + if let Some(pdb_file) = pdb_files.insert(pdb_slot, loaded_pdb_file.into()) { + log::info!( + "'{}' has been unloaded.", + pdb_file.borrow().file_path.display() + ); } log::info!( "'{}' has been loaded successfully!", @@ -189,8 +205,11 @@ fn worker_thread_routine( Ok(loaded_pdb_file) => { frontend_controller .send_command(FrontendCommand::LoadPDBResult(Ok(pdb_slot)))?; - if let Some(pdb_file) = pdb_files.insert(pdb_slot, loaded_pdb_file) { - log::info!("'{}' has been unloaded.", pdb_file.file_path.display()); + if let Some(pdb_file) = pdb_files.insert(pdb_slot, loaded_pdb_file.into()) { + log::info!( + "'{}' has been unloaded.", + pdb_file.borrow().file_path.display() + ); } log::info!("'{}' has been loaded successfully!", pdb_name); } @@ -205,8 +224,11 @@ fn worker_thread_routine( Ok(loaded_pdb_file) => { frontend_controller .send_command(FrontendCommand::LoadPDBResult(Ok(pdb_slot)))?; - if let Some(pdb_file) = pdb_files.insert(pdb_slot, loaded_pdb_file) { - log::info!("'{}' has been unloaded.", pdb_file.file_path.display()); + if let Some(pdb_file) = pdb_files.insert(pdb_slot, loaded_pdb_file.into()) { + log::info!( + "'{}' has been unloaded.", + pdb_file.borrow().file_path.display() + ); } log::info!("'{}' has been loaded successfully!", pdb_name); } @@ -255,7 +277,10 @@ fn worker_thread_routine( log::error!("Trying to unload an inexistent PDB"); } Some(pdb_file) => { - log::info!("'{}' has been unloaded.", pdb_file.file_path.display()); + log::info!( + "'{}' has been unloaded.", + pdb_file.borrow().file_path.display() + ); } }, @@ -269,7 +294,7 @@ fn worker_thread_routine( ) => { if let Some(pdb_file) = pdb_files.get(&pdb_slot) { let reconstructed_type_result = reconstruct_type_by_index_command( - pdb_file, + &pdb_file.borrow(), type_index, primitives_flavor, print_header, @@ -292,7 +317,7 @@ fn worker_thread_routine( ) => { if let Some(pdb_file) = pdb_files.get(&pdb_slot) { let reconstructed_type_result = reconstruct_type_by_name_command( - pdb_file, + &pdb_file.borrow(), &type_name, primitives_flavor, print_header, @@ -313,7 +338,7 @@ fn worker_thread_routine( ) => { if let Some(pdb_file) = pdb_files.get(&pdb_slot) { let reconstructed_type_result = reconstruct_all_types_command( - pdb_file, + &pdb_file.borrow(), primitives_flavor, print_header, print_access_specifiers, @@ -332,7 +357,7 @@ fn worker_thread_routine( ) => { if let Some(pdb_file) = pdb_files.get(&pdb_slot) { let filtered_type_list = update_type_filter_command( - pdb_file, + &pdb_file.borrow(), &search_filter, case_insensitive_search, use_regex, @@ -353,7 +378,7 @@ fn worker_thread_routine( for pdb_slot in pdb_slots { if let Some(pdb_file) = pdb_files.get(&pdb_slot) { let filtered_type_list = update_type_filter_command( - pdb_file, + &pdb_file.borrow(), &search_filter, case_insensitive_search, use_regex, @@ -372,6 +397,43 @@ fn worker_thread_routine( ))?; } + BackendCommand::ReconstructModuleByIndex( + pdb_slot, + module_index, + primitives_flavor, + print_header, + ) => { + if let Some(pdb_file) = pdb_files.get_mut(&pdb_slot) { + let reconstructed_module_result = reconstruct_module_by_index_command( + &mut pdb_file.borrow_mut(), + module_index, + primitives_flavor, + print_header, + ); + frontend_controller.send_command(FrontendCommand::ReconstructModuleResult( + reconstructed_module_result, + ))?; + } + } + + BackendCommand::ListModules( + pdb_slot, + search_filter, + case_insensitive_search, + use_regex, + ) => { + if let Some(pdb_file) = pdb_files.get(&pdb_slot) { + let module_list = list_modules_command( + &pdb_file.borrow(), + &search_filter, + case_insensitive_search, + use_regex, + ); + frontend_controller + .send_command(FrontendCommand::UpdateModuleList(module_list))?; + } + } + BackendCommand::DiffTypeByName( pdb_from_slot, pdb_to_slot, @@ -384,8 +446,8 @@ fn worker_thread_routine( if let Some(pdb_file_from) = pdb_files.get(&pdb_from_slot) { if let Some(pdb_file_to) = pdb_files.get(&pdb_to_slot) { let type_diff_result = diff_type_by_name( - pdb_file_from, - pdb_file_to, + &pdb_file_from.borrow(), + &pdb_file_to.borrow(), &type_name, primitives_flavor, print_header, @@ -393,7 +455,29 @@ fn worker_thread_routine( print_access_specifiers, ); frontend_controller - .send_command(FrontendCommand::DiffTypeResult(type_diff_result))?; + .send_command(FrontendCommand::DiffResult(type_diff_result))?; + } + } + } + + BackendCommand::DiffModuleByPath( + pdb_from_slot, + pdb_to_slot, + module_path, + primitives_flavor, + print_header, + ) => { + if let Some(pdb_file_from) = pdb_files.get(&pdb_from_slot) { + if let Some(pdb_file_to) = pdb_files.get(&pdb_to_slot) { + let module_diff_result = diff_module_by_path( + &mut pdb_file_from.borrow_mut(), + &mut pdb_file_to.borrow_mut(), + &module_path, + primitives_flavor, + print_header, + ); + frontend_controller + .send_command(FrontendCommand::DiffResult(module_diff_result))?; } } } @@ -412,7 +496,7 @@ fn reconstruct_type_by_index_command<'p, T>( print_access_specifiers: bool, ) -> Result where - T: io::Seek + io::Read + 'p, + T: io::Seek + io::Read + std::fmt::Debug + 'p, { let data = pdb_file.reconstruct_type_by_type_index( type_index, @@ -437,7 +521,7 @@ fn reconstruct_type_by_name_command<'p, T>( print_access_specifiers: bool, ) -> Result where - T: io::Seek + io::Read + 'p, + T: io::Seek + io::Read + std::fmt::Debug + 'p, { let data = pdb_file.reconstruct_type_by_name( type_name, @@ -460,7 +544,7 @@ fn reconstruct_all_types_command<'p, T>( print_access_specifiers: bool, ) -> Result where - T: io::Seek + io::Read + 'p, + T: io::Seek + io::Read + std::fmt::Debug + 'p, { let data = pdb_file.reconstruct_all_types(primitives_flavor, print_access_specifiers)?; if print_header { @@ -471,6 +555,24 @@ where } } +fn reconstruct_module_by_index_command<'p, T>( + pdb_file: &mut PdbFile<'p, T>, + module_index: usize, + primitives_flavor: PrimitiveReconstructionFlavor, + print_header: bool, +) -> Result +where + T: io::Seek + io::Read + std::fmt::Debug + 'p, +{ + let data = pdb_file.reconstruct_module_by_index(module_index, primitives_flavor)?; + if print_header { + let file_header = generate_file_header(pdb_file, primitives_flavor, true); + Ok(format!("{file_header}\n{data}")) + } else { + Ok(data) + } +} + fn generate_file_header( pdb_file: &PdbFile, primitives_flavor: PrimitiveReconstructionFlavor, @@ -531,7 +633,7 @@ where if sort_by_index { // Order types by type index, so the order is deterministic // (i.e., independent from DashMap's hash function) - cond_sort_by!(filtered_type_list, |lhs, rhs| lhs.1.cmp(&rhs.1)); + par_sort_by_if_available!(filtered_type_list, |lhs, rhs| lhs.1.cmp(&rhs.1)); } log::debug!( @@ -554,7 +656,7 @@ fn filter_types_regex( { // In case of error, return an empty result Err(_) => vec![], - Ok(regex) => cond_par_iter!(type_list) + Ok(regex) => par_iter_if_available!(type_list) .filter(|r| regex.find(&r.0).is_some()) .cloned() .collect(), @@ -569,12 +671,87 @@ fn filter_types_regular( ) -> Vec<(String, pdb::TypeIndex)> { if case_insensitive_search { let search_filter = search_filter.to_lowercase(); - cond_par_iter!(type_list) + par_iter_if_available!(type_list) + .filter(|r| r.0.to_lowercase().contains(&search_filter)) + .cloned() + .collect() + } else { + par_iter_if_available!(type_list) + .filter(|r| r.0.contains(search_filter)) + .cloned() + .collect() + } +} + +fn list_modules_command<'p, T>( + pdb_file: &PdbFile<'p, T>, + search_filter: &str, + case_insensitive_search: bool, + use_regex: bool, +) -> Result +where + T: io::Seek + io::Read + std::fmt::Debug + 'p, +{ + let filter_start = Instant::now(); + + let filtered_module_list = if search_filter.is_empty() { + // No need to filter + pdb_file.module_list()? + } else if use_regex { + filter_modules_regex( + &pdb_file.module_list()?, + search_filter, + case_insensitive_search, + ) + } else { + filter_modules_regular( + &pdb_file.module_list()?, + search_filter, + case_insensitive_search, + ) + }; + + log::debug!( + "Module filtering took {} ms", + filter_start.elapsed().as_millis() + ); + + Ok(filtered_module_list) +} + +/// Filter module list with a regular expression +fn filter_modules_regex( + module_list: &[(String, usize)], + search_filter: &str, + case_insensitive_search: bool, +) -> Vec<(String, usize)> { + match regex::RegexBuilder::new(search_filter) + .case_insensitive(case_insensitive_search) + .build() + { + // In case of error, return an empty result + Err(_) => vec![], + Ok(regex) => par_iter_if_available!(module_list) + .filter(|r| regex.find(&r.0).is_some()) + .cloned() + .collect(), + } +} + +/// Filter module list with a plain (sub-)string +fn filter_modules_regular( + module_list: &[(String, usize)], + search_filter: &str, + case_insensitive_search: bool, +) -> Vec<(String, usize)> { + if case_insensitive_search { + let search_filter = search_filter.to_lowercase(); + par_iter_if_available!(module_list) .filter(|r| r.0.to_lowercase().contains(&search_filter)) .cloned() .collect() } else { - cond_par_iter!(type_list) + par_iter_if_available!(module_list) .filter(|r| r.0.contains(search_filter)) .cloned() .collect() diff --git a/resym_core/src/diffing.rs b/resym_core/src/diffing.rs index 18a724b..d9bda76 100644 --- a/resym_core/src/diffing.rs +++ b/resym_core/src/diffing.rs @@ -17,7 +17,7 @@ pub type DiffChange = ChangeTag; pub type DiffIndices = (Option, Option); #[derive(Default)] -pub struct DiffedType { +pub struct Diff { pub metadata: Vec<(DiffIndices, DiffChange)>, pub data: String, } @@ -35,11 +35,12 @@ pub fn diff_type_by_name<'p, T>( print_header: bool, reconstruct_dependencies: bool, print_access_specifiers: bool, -) -> Result +) -> Result where - T: io::Seek + io::Read + 'p, + T: io::Seek + io::Read + std::fmt::Debug + 'p, { let diff_start = Instant::now(); + // Prepend header if needed let (mut reconstructed_type_from, mut reconstructed_type_to) = if print_header { let diff_header = generate_diff_header(pdb_file_from, pdb_file_to); @@ -48,7 +49,7 @@ where (String::default(), String::default()) }; - // Reconstruct type from both PDBs + // Reconstruct types from both PDBs { let reconstructed_type_from_tmp = pdb_file_from .reconstruct_type_by_name( @@ -74,29 +75,59 @@ where reconstructed_type_to.push_str(&reconstructed_type_to_tmp); } - // Diff reconstructed reprensentations - let mut diff_metadata = vec![]; - let mut diff_data = String::default(); + // Diff reconstructed representations + let diff = generate_diff(&reconstructed_type_from, &reconstructed_type_to)?; + log::debug!("Type diffing took {} ms", diff_start.elapsed().as_millis()); + + Ok(diff) +} + +pub fn diff_module_by_path<'p, T>( + pdb_file_from: &mut PdbFile<'p, T>, + pdb_file_to: &mut PdbFile<'p, T>, + module_path: &str, + primitives_flavor: PrimitiveReconstructionFlavor, + print_header: bool, +) -> Result +where + T: io::Seek + io::Read + std::fmt::Debug + 'p, +{ + let diff_start = Instant::now(); + + // Prepend header if needed + let (mut reconstructed_module_from, mut reconstructed_module_to) = if print_header { + let mut diff_header = generate_diff_header(pdb_file_from, pdb_file_to); + diff_header.push('\n'); + + (diff_header.clone(), diff_header) + } else { + (String::default(), String::default()) + }; + + // Reconstruct modules from both PDBs { - let reconstructed_type_diff = - TextDiff::from_lines(&reconstructed_type_from, &reconstructed_type_to); - for change in reconstructed_type_diff.iter_all_changes() { - diff_metadata.push(((change.old_index(), change.new_index()), change.tag())); - let prefix = match change.tag() { - ChangeTag::Insert => "+", - ChangeTag::Delete => "-", - ChangeTag::Equal => " ", - }; - write!(&mut diff_data, "{prefix}{change}")?; + let reconstructed_type_from_tmp = pdb_file_from + .reconstruct_module_by_path(module_path, primitives_flavor) + .unwrap_or_default(); + let reconstructed_type_to_tmp = pdb_file_to + .reconstruct_module_by_path(module_path, primitives_flavor) + .unwrap_or_default(); + if reconstructed_type_from_tmp.is_empty() && reconstructed_type_to_tmp.is_empty() { + // Make it obvious an error occured + return Err(ResymCoreError::ModuleNotFoundError(module_path.to_owned())); } + reconstructed_module_from.push_str(&reconstructed_type_from_tmp); + reconstructed_module_to.push_str(&reconstructed_type_to_tmp); } - log::debug!("Type diffing took {} ms", diff_start.elapsed().as_millis()); + // Diff reconstructed representations + let diff = generate_diff(&reconstructed_module_from, &reconstructed_module_to)?; + log::debug!( + "Module diffing took {} ms", + diff_start.elapsed().as_millis() + ); - Ok(DiffedType { - metadata: diff_metadata, - data: diff_data, - }) + Ok(diff) } fn generate_diff_header<'p, T>( @@ -127,3 +158,25 @@ where PKG_VERSION, ) } + +fn generate_diff(str_from: &str, str_to: &str) -> Result { + let mut diff_metadata = vec![]; + let mut diff_data = String::default(); + { + let reconstructed_type_diff = TextDiff::from_lines(str_from, str_to); + for change in reconstructed_type_diff.iter_all_changes() { + diff_metadata.push(((change.old_index(), change.new_index()), change.tag())); + let prefix = match change.tag() { + ChangeTag::Insert => "+", + ChangeTag::Delete => "-", + ChangeTag::Equal => " ", + }; + write!(&mut diff_data, "{prefix}{change}")?; + } + } + + Ok(Diff { + metadata: diff_metadata, + data: diff_data, + }) +} diff --git a/resym_core/src/error.rs b/resym_core/src/error.rs index 27e6140..6fab68f 100644 --- a/resym_core/src/error.rs +++ b/resym_core/src/error.rs @@ -45,6 +45,16 @@ pub enum ResymCoreError { #[error("type not found: {0}")] TypeNameNotFoundError(String), + /// Error returned when querying for a module by path, that isn't present in + /// the PDB file. + #[error("module not found: {0}")] + ModuleNotFoundError(String), + + /// Error returned when querying for a module's information, that isn't available in + /// the PDB file. + #[error("module info not found: {0}")] + ModuleInfoNotFoundError(String), + /// Error returned when parsing a `PrimitiveReconstructionFlavor` from a string fails. #[error("invalid primitive type flavor: {0}")] ParsePrimitiveFlavorError(String), diff --git a/resym_core/src/frontend.rs b/resym_core/src/frontend.rs index 887b9ec..0511f73 100644 --- a/resym_core/src/frontend.rs +++ b/resym_core/src/frontend.rs @@ -1,6 +1,7 @@ -use crate::{backend::PDBSlot, diffing::DiffedType, error::Result}; +use crate::{backend::PDBSlot, diffing::Diff, error::Result}; pub type TypeList = Vec<(String, pdb::TypeIndex)>; +pub type ModuleList = Vec<(String, usize)>; pub enum FrontendCommand { LoadPDBResult(Result), @@ -9,7 +10,9 @@ pub enum FrontendCommand { LoadURLResult(Result<(PDBSlot, String, Vec)>), UpdateFilteredTypes(TypeList), ReconstructTypeResult(Result), - DiffTypeResult(Result), + ReconstructModuleResult(Result), + UpdateModuleList(Result), + DiffResult(Result), } pub trait FrontendController { diff --git a/resym_core/src/lib.rs b/resym_core/src/lib.rs index 7f54d43..af1631f 100644 --- a/resym_core/src/lib.rs +++ b/resym_core/src/lib.rs @@ -13,14 +13,14 @@ const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); /// Macro used to switch between iterators depending on rayon's availability #[macro_export] #[cfg(not(feature = "rayon"))] -macro_rules! cond_par_iter { +macro_rules! par_iter_if_available { ($expression:expr) => { $expression.iter() }; } #[macro_export] #[cfg(feature = "rayon")] -macro_rules! cond_par_iter { +macro_rules! par_iter_if_available { ($expression:expr) => { $expression.par_iter() }; @@ -29,14 +29,14 @@ macro_rules! cond_par_iter { /// Macro used to switch between functions depending on rayon's availability #[macro_export] #[cfg(not(feature = "rayon"))] -macro_rules! cond_sort_by { +macro_rules! par_sort_by_if_available { ($expression:expr, $($x:tt)*) => { $expression.sort_by($($x)*) }; } #[macro_export] #[cfg(feature = "rayon")] -macro_rules! cond_sort_by { +macro_rules! par_sort_by_if_available { ($expression:expr, $($x:tt)*) => { $expression.par_sort_by($($x)*) }; diff --git a/resym_core/src/pdb_file.rs b/resym_core/src/pdb_file.rs index b30409f..349303d 100644 --- a/resym_core/src/pdb_file.rs +++ b/resym_core/src/pdb_file.rs @@ -15,9 +15,12 @@ use std::{ use std::{fs::File, path::Path, time::Instant}; use crate::{ - cond_par_iter, error::{Result, ResymCoreError}, - pdb_types::{self, is_unnamed_type, DataFormatConfiguration, PrimitiveReconstructionFlavor}, + frontend::ModuleList, + par_iter_if_available, + pdb_types::{ + self, is_unnamed_type, type_name, DataFormatConfiguration, PrimitiveReconstructionFlavor, + }, }; /// Wrapper for different buffer types processed by `resym` @@ -56,6 +59,7 @@ where pub forwarder_to_complete_type: Arc>, pub machine_type: pdb::MachineType, pub type_information: pdb::TypeInformation<'p>, + pub debug_information: pdb::DebugInformation<'p>, pub file_path: PathBuf, _pdb: pdb::PDB<'p, T>, } @@ -67,6 +71,7 @@ impl<'p> PdbFile<'p, File> { let file = PDBDataSource::File(File::open(pdb_file_path)?); let mut pdb = pdb::PDB::open(file)?; let type_information = pdb.type_information()?; + let debug_information = pdb.debug_information()?; let machine_type = pdb.debug_information()?.machine_type()?; let mut pdb_file = PdbFile { @@ -74,6 +79,7 @@ impl<'p> PdbFile<'p, File> { forwarder_to_complete_type: Arc::new(DashMap::default()), machine_type, type_information, + debug_information, file_path: pdb_file_path.to_owned(), _pdb: pdb, }; @@ -92,6 +98,7 @@ impl<'p> PdbFile<'p, PDBDataSource> { let reader = PDBDataSource::Vec(io::Cursor::new(pdb_file_data)); let mut pdb = pdb::PDB::open(reader)?; let type_information = pdb.type_information()?; + let debug_information = pdb.debug_information()?; let machine_type = pdb.debug_information()?.machine_type()?; let mut pdb_file = PdbFile { @@ -99,6 +106,7 @@ impl<'p> PdbFile<'p, PDBDataSource> { forwarder_to_complete_type: Arc::new(DashMap::default()), machine_type, type_information, + debug_information, file_path: pdb_file_name.into(), _pdb: pdb, }; @@ -115,6 +123,7 @@ impl<'p> PdbFile<'p, PDBDataSource> { let reader = PDBDataSource::SharedArray(io::Cursor::new(pdb_file_data)); let mut pdb = pdb::PDB::open(reader)?; let type_information = pdb.type_information()?; + let debug_information = pdb.debug_information()?; let machine_type = pdb.debug_information()?.machine_type()?; let mut pdb_file = PdbFile { @@ -122,6 +131,7 @@ impl<'p> PdbFile<'p, PDBDataSource> { forwarder_to_complete_type: Arc::new(DashMap::default()), machine_type, type_information, + debug_information, file_path: pdb_file_name.into(), _pdb: pdb, }; @@ -133,7 +143,7 @@ impl<'p> PdbFile<'p, PDBDataSource> { impl<'p, T> PdbFile<'p, T> where - T: io::Seek + io::Read + 'p, + T: io::Seek + io::Read + std::fmt::Debug + 'p, { fn load_symbols(&mut self) -> Result<()> { // Build the list of complete types @@ -206,7 +216,7 @@ where // Resolve forwarder references to their corresponding complete type, in parallel let fwd_start = Instant::now(); - cond_par_iter!(forwarders).for_each(|(fwd_name, fwd_type_id)| { + par_iter_if_available!(forwarders).for_each(|(fwd_name, fwd_type_id)| { if let Some(complete_type_index) = complete_symbol_map.get(fwd_name) { self.forwarder_to_complete_type .insert(*fwd_type_id, *complete_type_index); @@ -345,6 +355,144 @@ where ) } + pub fn module_list(&self) -> Result { + let module_list = self + .debug_information + .modules()? + .enumerate() + .map(|(index, module)| Ok((module.module_name().into_owned(), index))); + + Ok(module_list.collect()?) + } + + pub fn reconstruct_module_by_path( + &mut self, + module_path: &str, + primitives_flavor: PrimitiveReconstructionFlavor, + ) -> Result { + // Find index for module + let mut modules = self.debug_information.modules()?; + let module_index = modules.position(|module| Ok(module.module_name() == module_path))?; + + match module_index { + None => Err(ResymCoreError::ModuleNotFoundError(format!( + "Module '{}' not found", + module_path + ))), + Some(module_index) => self.reconstruct_module_by_index(module_index, primitives_flavor), + } + } + + pub fn reconstruct_module_by_index( + &mut self, + module_index: usize, + primitives_flavor: PrimitiveReconstructionFlavor, + ) -> Result { + let mut modules = self.debug_information.modules()?; + let module = modules.nth(module_index)?.ok_or_else(|| { + ResymCoreError::ModuleInfoNotFoundError(format!("Module #{} not found", module_index)) + })?; + + let module_info = self._pdb.module_info(&module)?.ok_or_else(|| { + ResymCoreError::ModuleInfoNotFoundError(format!( + "No module information present for '{}'", + module.object_file_name() + )) + })?; + + // Populate our `TypeFinder` + let mut type_finder = self.type_information.finder(); + { + let mut type_iter = self.type_information.iter(); + while (type_iter.next()?).is_some() { + type_finder.update(&type_iter); + } + } + + let mut result = String::default(); + module_info.symbols()?.for_each(|symbol| { + let mut needed_types = pdb_types::TypeSet::new(); + + match symbol.parse()? { + pdb::SymbolData::UserDefinedType(udt) => { + if let Ok(type_name) = type_name( + &type_finder, + &self.forwarder_to_complete_type, + udt.type_index, + &primitives_flavor, + &mut needed_types, + ) { + if type_name.0 == "..." { + // No type + result += + format!("{}; // Missing type information\n", udt.name).as_str(); + } else { + result += + format!("using {} = {}{};\n", udt.name, type_name.0, type_name.1) + .as_str(); + } + } + } + pdb::SymbolData::Procedure(procedure) => { + if let Ok(type_name) = type_name( + &type_finder, + &self.forwarder_to_complete_type, + procedure.type_index, + &primitives_flavor, + &mut needed_types, + ) { + if type_name.0 == "..." { + // No type + result += format!( + "void {}(); // CodeSize={} (missing type information)\n", + procedure.name, procedure.len + ) + .as_str(); + } else { + result += format!( + "{}{}{}; // CodeSize={}\n", + type_name.0, procedure.name, type_name.1, procedure.len + ) + .as_str(); + } + } + } + pdb::SymbolData::Data(data) => { + if let Ok(type_name) = type_name( + &type_finder, + &self.forwarder_to_complete_type, + data.type_index, + &primitives_flavor, + &mut needed_types, + ) { + if type_name.0 == "..." { + // No type + result += + format!("{}; // Missing type information\n", data.name).as_str(); + } else { + result += + format!("{} {}{};\n", type_name.0, data.name, type_name.1).as_str(); + } + } + } + pdb::SymbolData::UsingNamespace(namespace) => { + result += format!("using namespace {};\n", namespace.name).as_str(); + } + pdb::SymbolData::AnnotationReference(annotation) => { + // TODO(ergrelet): update when support for annotations + // (symbol kind 0x1019) has been implemented in `pdb` + result += format!("__annotation(); // {}\n", annotation.name).as_str(); + } + // Ignore + _ => {} + } + + Ok(()) + })?; + + Ok(result) + } + fn reconstruct_type_by_type_index_internal( &self, type_finder: &pdb::TypeFinder, diff --git a/resym_core/src/pdb_types/mod.rs b/resym_core/src/pdb_types/mod.rs index bb6e3c9..c645407 100644 --- a/resym_core/src/pdb_types/mod.rs +++ b/resym_core/src/pdb_types/mod.rs @@ -757,7 +757,7 @@ fn find_unnamed_unions_in_struct(fields: &[Field]) -> Vec> { // Third step of the "state machine", add new fields to the union. let union_info = unions_found_temp .get_mut(&(curr_union_offset_range.start, 0)) - .unwrap(); + .expect("key should exist in map"); union_info.0.end = i + 1; // Update the union's size union_info.1 = std::cmp::max( diff --git a/resym_core/tests/module_diffing.rs b/resym_core/tests/module_diffing.rs new file mode 100644 index 0000000..7671a5d --- /dev/null +++ b/resym_core/tests/module_diffing.rs @@ -0,0 +1,30 @@ +use std::path::Path; + +use resym_core::{ + diffing::diff_module_by_path, pdb_file::PdbFile, pdb_types::PrimitiveReconstructionFlavor, +}; + +const TEST_PDB_FROM_FILE_PATH: &str = "tests/data/test_diff_from.pdb"; +const TEST_PDB_TO_FILE_PATH: &str = "tests/data/test_diff_to.pdb"; +// TODO(ergrelet): replace with a more interesting module when support for more +// symbol kinds is implemented in the `pdb` crate +const TEST_MODULE_PATH: &str = "d:\\a01\\_work\\43\\s\\Intermediate\\vctools\\msvcrt.nativeproj_607447030\\objd\\amd64\\exe_main.obj"; + +#[test] +fn test_module_diffing_by_path() { + let mut pdb_file_from = PdbFile::load_from_file(Path::new(TEST_PDB_FROM_FILE_PATH)) + .expect("load test_diff_from.pdb"); + let mut pdb_file_to = + PdbFile::load_from_file(Path::new(TEST_PDB_TO_FILE_PATH)).expect("load test_diff_to.pdb"); + + let module_diff = diff_module_by_path( + &mut pdb_file_from, + &mut pdb_file_to, + TEST_MODULE_PATH, + PrimitiveReconstructionFlavor::Portable, + true, + ) + .unwrap_or_else(|err| panic!("module diffing failed: {err}")); + + insta::assert_snapshot!("module_diffing_by_path", module_diff.data); +} diff --git a/resym_core/tests/module_dumping.rs b/resym_core/tests/module_dumping.rs new file mode 100644 index 0000000..a775af9 --- /dev/null +++ b/resym_core/tests/module_dumping.rs @@ -0,0 +1,61 @@ +use std::path::Path; + +use resym_core::{pdb_file::PdbFile, pdb_types::PrimitiveReconstructionFlavor}; + +const TEST_PDB_FILE_PATH: &str = "tests/data/test.pdb"; +const TEST_MODULE_INDEX: usize = 27; +const TEST_MODULE_PATH: &str = "D:\\a\\_work\\1\\s\\Intermediate\\crt\\vcstartup\\build\\xmd\\msvcrt_kernel32\\msvcrt_kernel32.nativeproj\\objd\\amd64\\default_local_stdio_options.obj"; + +#[test] +fn test_module_dumping_by_path_portable() { + let mut pdb_file = + PdbFile::load_from_file(Path::new(TEST_PDB_FILE_PATH)).expect("load test.pdb"); + + let module_dump = pdb_file + .reconstruct_module_by_path(TEST_MODULE_PATH, PrimitiveReconstructionFlavor::Portable) + .unwrap_or_else(|err| panic!("module dumping failed: {err}")); + + insta::assert_snapshot!("module_dumping_by_path_portable", module_dump); +} + +#[test] +fn test_module_dumping_by_index_portable() { + test_module_dumping_by_index_internal( + "module_dumping_by_index_portable", + TEST_MODULE_INDEX, + PrimitiveReconstructionFlavor::Portable, + ); +} + +#[test] +fn test_module_dumping_by_index_microsoft() { + test_module_dumping_by_index_internal( + "module_dumping_by_index_microsoft", + TEST_MODULE_INDEX, + PrimitiveReconstructionFlavor::Microsoft, + ); +} + +#[test] +fn test_module_dumping_by_index_raw() { + test_module_dumping_by_index_internal( + "module_dumping_by_index_raw", + TEST_MODULE_INDEX, + PrimitiveReconstructionFlavor::Raw, + ); +} + +fn test_module_dumping_by_index_internal( + snapshot_name: &str, + module_index: usize, + primitives_flavor: PrimitiveReconstructionFlavor, +) { + let mut pdb_file = + PdbFile::load_from_file(Path::new(TEST_PDB_FILE_PATH)).expect("load test.pdb"); + + let module_dump = pdb_file + .reconstruct_module_by_index(module_index, primitives_flavor) + .unwrap_or_else(|_| panic!("module dumping")); + + insta::assert_snapshot!(snapshot_name, module_dump); +} diff --git a/resym_core/tests/module_listing.rs b/resym_core/tests/module_listing.rs new file mode 100644 index 0000000..a808cc0 --- /dev/null +++ b/resym_core/tests/module_listing.rs @@ -0,0 +1,22 @@ +use std::path::Path; + +use resym_core::pdb_file::PdbFile; + +const TEST_PDB_FILE_PATH: &str = "tests/data/test.pdb"; + +#[test] +fn test_module_listing() { + let pdb_file = PdbFile::load_from_file(Path::new(TEST_PDB_FILE_PATH)).expect("load test.pdb"); + + let module_list = pdb_file + .module_list() + .unwrap_or_else(|err| panic!("module listing failed: {err}")); + + let snapshot_name = "module_listing"; + let snapshot_data = module_list + .into_iter() + .fold(String::new(), |acc, (mod_name, mod_id)| { + format!("{acc}\n{mod_id} {mod_name}") + }); + insta::assert_snapshot!(snapshot_name, snapshot_data); +} diff --git a/resym_core/tests/snapshots/module_diffing__module_diffing_by_path.snap b/resym_core/tests/snapshots/module_diffing__module_diffing_by_path.snap new file mode 100644 index 0000000..3ec4342 --- /dev/null +++ b/resym_core/tests/snapshots/module_diffing__module_diffing_by_path.snap @@ -0,0 +1,35 @@ +--- +source: resym_core/tests/module_diffing.rs +expression: module_diff.data +--- + // + // Showing differences between two PDB files: + // + // Reference PDB file: tests/data/test_diff_from.pdb + // Image architecture: Amd64 + // + // New PDB file: tests/data/test_diff_to.pdb + // Image architecture: Amd64 + // + // Information extracted with resym v0.3.0 + // + + using namespace std; + int32_t (* pre_c_initializer)(); + int32_t (* post_pgo_initializer)(); + void (* pre_cpp_initializer)(); + using PUWSTR_C = const wchar_t*; + using TP_CALLBACK_ENVIRON_V3 = _TP_CALLBACK_ENVIRON_V3; + int32_t (__scrt_common_main)(); // CodeSize=19 + int32_t (__scrt_common_main_seh)(); // CodeSize=414 + void `__scrt_common_main_seh'::`1'::filt$0(); // CodeSize=48 (missing type information) + int32_t (__scrt_narrow_argv_policy::__scrt_narrow_argv_policy::configure_argv)(); // CodeSize=21 + int32_t (__scrt_narrow_environment_policy::__scrt_narrow_environment_policy::initialize_environment)(); // CodeSize=14 + int32_t (invoke_main)(); // CodeSize=62 + int32_t (post_pgo_initialization)(); // CodeSize=16 + int32_t (pre_c_initialization)(); // CodeSize=178 + void (pre_cpp_initialization)(); // CodeSize=26 + void (__scrt_main_policy::__scrt_main_policy::set_app_type)(); // CodeSize=19 + void (__scrt_file_policy::__scrt_file_policy::set_commode)(); // CodeSize=29 + void (__scrt_file_policy::__scrt_file_policy::set_fmode)(); // CodeSize=21 + uint32_t (mainCRTStartup)(void*); // CodeSize=19 diff --git a/resym_core/tests/snapshots/module_dumping__module_dumping_by_index_microsoft.snap b/resym_core/tests/snapshots/module_dumping__module_dumping_by_index_microsoft.snap new file mode 100644 index 0000000..955e218 --- /dev/null +++ b/resym_core/tests/snapshots/module_dumping__module_dumping_by_index_microsoft.snap @@ -0,0 +1,10 @@ +--- +source: resym_core/tests/module_dumping.rs +expression: module_dump +--- +using namespace std; +using PUWSTR_C = const WCHAR*; +using TP_CALLBACK_ENVIRON_V3 = _TP_CALLBACK_ENVIRON_V3; +PULONGLONG (__local_stdio_scanf_options)(); // CodeSize=8 +ULONGLONG _OptionsStorage; +VOID (__scrt_initialize_default_local_stdio_options)(); // CodeSize=69 diff --git a/resym_core/tests/snapshots/module_dumping__module_dumping_by_index_portable.snap b/resym_core/tests/snapshots/module_dumping__module_dumping_by_index_portable.snap new file mode 100644 index 0000000..2c7db53 --- /dev/null +++ b/resym_core/tests/snapshots/module_dumping__module_dumping_by_index_portable.snap @@ -0,0 +1,10 @@ +--- +source: resym_core/tests/module_dumping.rs +expression: module_dump +--- +using namespace std; +using PUWSTR_C = const wchar_t*; +using TP_CALLBACK_ENVIRON_V3 = _TP_CALLBACK_ENVIRON_V3; +uint64_t* (__local_stdio_scanf_options)(); // CodeSize=8 +uint64_t _OptionsStorage; +void (__scrt_initialize_default_local_stdio_options)(); // CodeSize=69 diff --git a/resym_core/tests/snapshots/module_dumping__module_dumping_by_index_raw.snap b/resym_core/tests/snapshots/module_dumping__module_dumping_by_index_raw.snap new file mode 100644 index 0000000..7392ae5 --- /dev/null +++ b/resym_core/tests/snapshots/module_dumping__module_dumping_by_index_raw.snap @@ -0,0 +1,10 @@ +--- +source: resym_core/tests/module_dumping.rs +expression: module_dump +--- +using namespace std; +using PUWSTR_C = const wchar_t*; +using TP_CALLBACK_ENVIRON_V3 = _TP_CALLBACK_ENVIRON_V3; +unsigned __int64* (__local_stdio_scanf_options)(); // CodeSize=8 +unsigned __int64 _OptionsStorage; +void (__scrt_initialize_default_local_stdio_options)(); // CodeSize=69 diff --git a/resym_core/tests/snapshots/module_dumping__module_dumping_by_path_portable.snap b/resym_core/tests/snapshots/module_dumping__module_dumping_by_path_portable.snap new file mode 100644 index 0000000..2c7db53 --- /dev/null +++ b/resym_core/tests/snapshots/module_dumping__module_dumping_by_path_portable.snap @@ -0,0 +1,10 @@ +--- +source: resym_core/tests/module_dumping.rs +expression: module_dump +--- +using namespace std; +using PUWSTR_C = const wchar_t*; +using TP_CALLBACK_ENVIRON_V3 = _TP_CALLBACK_ENVIRON_V3; +uint64_t* (__local_stdio_scanf_options)(); // CodeSize=8 +uint64_t _OptionsStorage; +void (__scrt_initialize_default_local_stdio_options)(); // CodeSize=69 diff --git a/resym_core/tests/snapshots/module_listing__module_listing.snap b/resym_core/tests/snapshots/module_listing__module_listing.snap new file mode 100644 index 0000000..c659f25 --- /dev/null +++ b/resym_core/tests/snapshots/module_listing__module_listing.snap @@ -0,0 +1,58 @@ +--- +source: resym_core/tests/module_listing.rs +expression: snapshot_data +--- +0 C:\Users\Henry\source\repos\symbol_zoo\x64\Debug\symbol_zoo.obj +1 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\stack.obj +2 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\init.obj +3 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\debugger_jmc.obj +4 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\gshandlereh4.obj +5 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\chkstk.obj +6 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\amdsecgs.obj +7 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\std_type_info_static.obj +8 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\gs_cookie.obj +9 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\exe_main.obj +10 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\error.obj +11 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\userapi.obj +12 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\gshandler.obj +13 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\gs_report.obj +14 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\delete_scalar_size.obj +15 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\utility.obj +16 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\gs_support.obj +17 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\matherr.obj +18 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\argv_mode.obj +19 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\commit_mode.obj +20 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\file_mode.obj +21 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\new_mode.obj +22 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\thread_locale.obj +23 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\tncleanup.obj +24 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\env_mode.obj +25 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\invalid_parameter_handler.obj +26 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\denormal_control.obj +27 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\default_local_stdio_options.obj +28 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\matherr_detection.obj +29 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\dyn_tls_init.obj +30 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\dyn_tls_dtor.obj +31 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\utility_desktop.obj +32 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\initsect.obj +33 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\initializers.obj +34 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\guard_support.obj +35 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\pdblkup.obj +36 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\delete_scalar.obj +37 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\cpu_disp.obj +38 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\ucrt_detection.obj +39 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\guard_dispatch.obj +40 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\guard_xfg_dispatch.obj +41 VCRUNTIME140D.dll +42 VCRUNTIME140_1D.dll +43 ucrtbased.dll +44 KERNEL32.dll +45 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\ucrt_stubs.obj +46 D:\a\_work\1\s\Intermediate\crt\vcstartup\build\xmd\msvcrt_kernel32\msvcrt_kernel32.nativeproj\objd\amd64\loadcfg.obj +47 D:\a\_work\1\s\Intermediate\crt\vcruntime\build\base\xmd\vcruntime_kernel32\vcruntime_kernel32.nativeproj\objd\amd64\softmemtag.obj +48 * Linker Generated Manifest RES * +49 Import:KERNEL32.dll +50 Import:VCRUNTIME140_1D.dll +51 Import:VCRUNTIME140D.dll +52 Import:ucrtbased.dll +53 * Linker * diff --git a/resymc/src/main.rs b/resymc/src/main.rs index e803fa5..0ca8483 100644 --- a/resymc/src/main.rs +++ b/resymc/src/main.rs @@ -1,34 +1,23 @@ mod frontend; +mod resymc_app; +mod resymc_options; mod syntax_highlighting; -use std::{fs::File, io::Write, path::PathBuf, sync::Arc}; - -use anyhow::{anyhow, Result}; -use resym_core::{ - backend::{Backend, BackendCommand, PDBSlot}, - frontend::FrontendCommand, - pdb_types::PrimitiveReconstructionFlavor, - syntax_highlighting::CodeTheme, -}; +use anyhow::Result; +use resym_core::pdb_types::PrimitiveReconstructionFlavor; use structopt::StructOpt; -use crate::{frontend::CLIFrontendController, syntax_highlighting::highlight_code}; - -const PKG_NAME: &str = env!("CARGO_PKG_NAME"); - -/// Slot for the single PDB or for the PDB we're diffing from -const PDB_MAIN_SLOT: PDBSlot = 0; -/// Slot used for the PDB we're diffing to -const PDB_DIFF_TO_SLOT: PDBSlot = 1; +use crate::resymc_app::ResymcApp; +use crate::resymc_options::ResymcOptions; fn main() -> Result<()> { env_logger::init(); let app = ResymcApp::new()?; // Process command and options - let opt = ResymOptions::from_args(); + let opt = ResymcOptions::from_args(); match opt { - ResymOptions::List { + ResymcOptions::List { pdb_path, type_name_filter, output_file_path, @@ -41,7 +30,7 @@ fn main() -> Result<()> { use_regex, output_file_path, ), - ResymOptions::Dump { + ResymcOptions::Dump { pdb_path, type_name, output_file_path, @@ -60,7 +49,7 @@ fn main() -> Result<()> { highlight_syntax, output_file_path, ), - ResymOptions::DumpAll { + ResymcOptions::DumpAll { pdb_path, output_file_path, primitive_types_flavor, @@ -77,7 +66,7 @@ fn main() -> Result<()> { highlight_syntax, output_file_path, ), - ResymOptions::Diff { + ResymcOptions::Diff { from_pdb_path, to_pdb_path, type_name, @@ -98,413 +87,50 @@ fn main() -> Result<()> { highlight_syntax, output_file_path, ), - } -} - -#[derive(Debug, StructOpt)] -#[structopt( - name = PKG_NAME, - about = "resymc is a utility that allows browsing and extracting types from PDB files." -)] -enum ResymOptions { - /// List types from a given PDB file - List { - /// Path to the PDB file - pdb_path: PathBuf, - /// Search filter - type_name_filter: String, - /// Path of the output file - output_file_path: Option, - /// Do not match case - #[structopt(short = "i", long)] - case_insensitive: bool, - /// Use regular expressions - #[structopt(short = "r", long)] - use_regex: bool, - }, - /// Dump type from a given PDB file - Dump { - /// Path to the PDB file - pdb_path: PathBuf, - /// Name of the type to extract - type_name: String, - /// Path of the output file - output_file_path: Option, - /// Representation of primitive types - #[structopt(short = "f", long)] - primitive_types_flavor: Option, - /// Print header - #[structopt(short = "h", long)] - print_header: bool, - /// Print declarations of referenced types - #[structopt(short = "d", long)] - print_dependencies: bool, - /// Print C++ access specifiers - #[structopt(short = "a", long)] - print_access_specifiers: bool, - /// Highlight C++ output - #[structopt(short = "H", long)] - highlight_syntax: bool, - }, - /// Dump all types from a given PDB file - DumpAll { - /// Path to the PDB file - pdb_path: PathBuf, - /// Path of the output file - output_file_path: Option, - /// Representation of primitive types - #[structopt(short = "f", long)] - primitive_types_flavor: Option, - /// Print header - #[structopt(short = "h", long)] - print_header: bool, - /// Print C++ access specifiers - #[structopt(short = "a", long)] - print_access_specifiers: bool, - /// Highlight C++ output - #[structopt(short = "H", long)] - highlight_syntax: bool, - }, - /// Compute diff for a type between two given PDB files - Diff { - /// Path of the PDB file to compute the diff from - from_pdb_path: PathBuf, - /// Path of the PDB file to compute the diff to - to_pdb_path: PathBuf, - /// Name of the type to diff - type_name: String, - /// Path of the output file - output_file_path: Option, - /// Representation of primitive types - #[structopt(short = "f", long)] - primitive_types_flavor: Option, - /// Print header - #[structopt(short = "h", long)] - print_header: bool, - /// Print declarations of referenced types - #[structopt(short = "d", long)] - print_dependencies: bool, - /// Print C++ access specifiers - #[structopt(short = "a", long)] - print_access_specifiers: bool, - /// Highlight C++ output and add/deleted lines - #[structopt(short = "H", long)] - highlight_syntax: bool, - }, -} - -/// Struct that represents our CLI application. -/// It contains the whole application's context at all time. -struct ResymcApp { - frontend_controller: Arc, - backend: Backend, -} - -impl ResymcApp { - fn new() -> Result { - // Initialize backend - let (tx_ui, rx_ui) = crossbeam_channel::unbounded::(); - let frontend_controller = Arc::new(CLIFrontendController::new(tx_ui, rx_ui)); - let backend = Backend::new(frontend_controller.clone())?; - - Ok(Self { - frontend_controller, - backend, - }) - } - - fn list_types_command( - &self, - pdb_path: PathBuf, - type_name_filter: String, - case_insensitive: bool, - use_regex: bool, - output_file_path: Option, - ) -> Result<()> { - // Request the backend to load the PDB - self.backend - .send_command(BackendCommand::LoadPDBFromPath(PDB_MAIN_SLOT, pdb_path))?; - // Wait for the backend to finish loading the PDB - if let FrontendCommand::LoadPDBResult(result) = self.frontend_controller.rx_ui.recv()? { - if let Err(err) = result { - return Err(anyhow!("Failed to load PDB: {}", err)); - } - } else { - return Err(anyhow!("Invalid response received from the backend?")); - } - - // Queue a request for the backend to return the list of types that - // match the given filter - self.backend.send_command(BackendCommand::UpdateTypeFilter( - PDB_MAIN_SLOT, - type_name_filter, + ResymcOptions::ListModules { + pdb_path, + module_path_filter, + output_file_path, case_insensitive, use_regex, - ))?; - // Wait for the backend to finish filtering types - if let FrontendCommand::UpdateFilteredTypes(type_list) = - self.frontend_controller.rx_ui.recv()? - { - // Dump output - if let Some(output_file_path) = output_file_path { - let mut output_file = File::create(output_file_path)?; - for (type_name, _) in type_list { - writeln!(output_file, "{}", &type_name)?; - } - } else { - for (type_name, _) in type_list { - println!("{type_name}"); - } - } - Ok(()) - } else { - Err(anyhow!("Invalid response received from the backend?")) - } - } - - #[allow(clippy::too_many_arguments)] - fn dump_types_command( - &self, - pdb_path: PathBuf, - type_name: Option, - primitive_types_flavor: PrimitiveReconstructionFlavor, - print_header: bool, - print_dependencies: bool, - print_access_specifiers: bool, - highlight_syntax: bool, - output_file_path: Option, - ) -> Result<()> { - // Request the backend to load the PDB - self.backend - .send_command(BackendCommand::LoadPDBFromPath(PDB_MAIN_SLOT, pdb_path))?; - // Wait for the backend to finish loading the PDB - if let FrontendCommand::LoadPDBResult(result) = self.frontend_controller.rx_ui.recv()? { - if let Err(err) = result { - return Err(anyhow!("Failed to load PDB: {}", err)); - } - } else { - return Err(anyhow!("Invalid response received from the backend?")); - } - - // Queue a request for the backend to reconstruct the given type - if let Some(type_name) = type_name { - self.backend - .send_command(BackendCommand::ReconstructTypeByName( - PDB_MAIN_SLOT, - type_name, - primitive_types_flavor, - print_header, - print_dependencies, - print_access_specifiers, - ))?; - } else { - self.backend - .send_command(BackendCommand::ReconstructAllTypes( - PDB_MAIN_SLOT, - primitive_types_flavor, - print_header, - print_access_specifiers, - ))?; - } - // Wait for the backend to finish filtering types - if let FrontendCommand::ReconstructTypeResult(reconstructed_type_result) = - self.frontend_controller.rx_ui.recv()? - { - match reconstructed_type_result { - Err(err) => Err(err.into()), - Ok(reconstructed_type) => { - // Dump output - if let Some(output_file_path) = output_file_path { - let mut output_file = File::create(output_file_path)?; - output_file.write_all(reconstructed_type.as_bytes())?; - } else if highlight_syntax { - let theme = CodeTheme::default(); - if let Some(colorized_reconstructed_type) = - highlight_code(&theme, &reconstructed_type, None) - { - println!("{colorized_reconstructed_type}"); - } - } else { - println!("{reconstructed_type}"); - } - Ok(()) - } - } - } else { - Err(anyhow!("Invalid response received from the backend?")) - } - } - - #[allow(clippy::too_many_arguments)] - fn diff_type_command( - &self, - from_pdb_path: PathBuf, - to_pdb_path: PathBuf, - type_name: String, - primitive_types_flavor: PrimitiveReconstructionFlavor, - print_header: bool, - print_dependencies: bool, - print_access_specifiers: bool, - highlight_syntax: bool, - output_file_path: Option, - ) -> Result<()> { - // Request the backend to load the first PDB - self.backend.send_command(BackendCommand::LoadPDBFromPath( - PDB_MAIN_SLOT, - from_pdb_path.clone(), - ))?; - // Wait for the backend to finish loading the PDB - if let FrontendCommand::LoadPDBResult(result) = self.frontend_controller.rx_ui.recv()? { - if let Err(err) = result { - return Err(anyhow!( - "Failed to load PDB '{}': {}", - from_pdb_path.display(), - err - )); - } - } else { - return Err(anyhow!("Invalid response received from the backend?")); - } - - // Request the backend to load the second PDB - self.backend.send_command(BackendCommand::LoadPDBFromPath( - PDB_DIFF_TO_SLOT, - to_pdb_path.clone(), - ))?; - // Wait for the backend to finish loading the PDB - if let FrontendCommand::LoadPDBResult(result) = self.frontend_controller.rx_ui.recv()? { - if let Err(err) = result { - return Err(anyhow!( - "Failed to load PDB '{}': {}", - to_pdb_path.display(), - err - )); - } - } else { - return Err(anyhow!("Invalid response received from the backend?")); - } - - // Queue a request for the backend to diff the given type - self.backend.send_command(BackendCommand::DiffTypeByName( - PDB_MAIN_SLOT, - PDB_DIFF_TO_SLOT, - type_name, + } => app.list_modules_command( + pdb_path, + module_path_filter, + case_insensitive, + use_regex, + output_file_path, + ), + ResymcOptions::DumpModule { + pdb_path, + module_id, + output_file_path, primitive_types_flavor, print_header, - print_dependencies, - print_access_specifiers, - ))?; - // Wait for the backend to finish - if let FrontendCommand::DiffTypeResult(reconstructed_type_diff_result) = - self.frontend_controller.rx_ui.recv()? - { - match reconstructed_type_diff_result { - Err(err) => Err(err.into()), - Ok(reconstructed_type_diff) => { - // Dump output - if let Some(output_file_path) = output_file_path { - let mut output_file = File::create(output_file_path)?; - output_file.write_all(reconstructed_type_diff.data.as_bytes())?; - } else if highlight_syntax { - let theme = CodeTheme::default(); - let line_descriptions = - reconstructed_type_diff - .metadata - .iter() - .fold(vec![], |mut acc, e| { - acc.push(e.1); - acc - }); - if let Some(colorized_reconstructed_type) = highlight_code( - &theme, - &reconstructed_type_diff.data, - Some(line_descriptions), - ) { - println!("{colorized_reconstructed_type}"); - } - } else { - println!("{}", reconstructed_type_diff.data); - } - Ok(()) - } - } - } else { - Err(anyhow!("Invalid response received from the backend?")) - } - } -} - -#[cfg(test)] -mod tests { - use std::fs; - - use super::*; - - use tempdir::TempDir; - - const TEST_PDB_FILE_PATH: &str = "../resym_core/tests/data/test.pdb"; - - #[test] - fn list_types_command_invalid_pdb_path() { - let app = ResymcApp::new().expect("ResymcApp creation failed"); - let pdb_path = PathBuf::new(); - // The command should fail - assert!(app - .list_types_command( - pdb_path, - "resym_test::StructTest".to_string(), - false, - false, - None, - ) - .is_err()); - } - - #[test] - fn list_types_command_stdio_successful() { - let app = ResymcApp::new().expect("ResymcApp creation failed"); - let pdb_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(TEST_PDB_FILE_PATH); - // The command should succeed - assert!(app - .list_types_command( - pdb_path, - "resym_test::StructTest".to_string(), - false, - false, - None, - ) - .is_ok()); - } - - #[test] - fn list_types_command_file_successful() { - let app = ResymcApp::new().expect("ResymcApp creation failed"); - let pdb_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(TEST_PDB_FILE_PATH); - let tmp_dir = - TempDir::new("list_types_command_file_successful").expect("TempDir creation failed"); - let output_path = tmp_dir.path().join("output.txt"); - // The command should succeed - assert!(app - .list_types_command( - pdb_path, - "resym_test::ClassWithNestedDeclarationsTest".to_string(), - false, - false, - Some(output_path.clone()), - ) - .is_ok()); - - // Check output file's content - let output = fs::read_to_string(output_path).expect("Failed to read output file"); - assert_eq!( - output, - concat!( - "resym_test::ClassWithNestedDeclarationsTest::NestEnum\n", - "resym_test::ClassWithNestedDeclarationsTest\n", - "resym_test::ClassWithNestedDeclarationsTest::NestedUnion\n", - "resym_test::ClassWithNestedDeclarationsTest::NestedClass\n", - "resym_test::ClassWithNestedDeclarationsTest::NestedStruct\n" - ) - ); + highlight_syntax, + } => app.dump_module_command( + pdb_path, + module_id, + primitive_types_flavor.unwrap_or(PrimitiveReconstructionFlavor::Portable), + print_header, + highlight_syntax, + output_file_path, + ), + ResymcOptions::DiffModule { + from_pdb_path, + to_pdb_path, + module_path, + output_file_path, + primitive_types_flavor, + print_header, + highlight_syntax, + } => app.diff_module_command( + from_pdb_path, + to_pdb_path, + module_path, + primitive_types_flavor.unwrap_or(PrimitiveReconstructionFlavor::Portable), + print_header, + highlight_syntax, + output_file_path, + ), } } diff --git a/resymc/src/resymc_app.rs b/resymc/src/resymc_app.rs new file mode 100644 index 0000000..c261730 --- /dev/null +++ b/resymc/src/resymc_app.rs @@ -0,0 +1,877 @@ +use std::{fs::File, io::Write, path::PathBuf, sync::Arc}; + +use anyhow::{anyhow, Result}; +use resym_core::{ + backend::{Backend, BackendCommand, PDBSlot}, + frontend::FrontendCommand, + pdb_types::PrimitiveReconstructionFlavor, + syntax_highlighting::CodeTheme, +}; + +use crate::{frontend::CLIFrontendController, syntax_highlighting::highlight_code}; + +/// Slot for the single PDB or for the PDB we're diffing from +const PDB_MAIN_SLOT: PDBSlot = 0; +/// Slot used for the PDB we're diffing to +const PDB_DIFF_TO_SLOT: PDBSlot = 1; + +/// Struct that represents our CLI application. +/// It contains the whole application's context at all time. +pub struct ResymcApp { + frontend_controller: Arc, + backend: Backend, +} + +impl ResymcApp { + pub fn new() -> Result { + // Initialize backend + let (tx_ui, rx_ui) = crossbeam_channel::unbounded::(); + let frontend_controller = Arc::new(CLIFrontendController::new(tx_ui, rx_ui)); + let backend = Backend::new(frontend_controller.clone())?; + + Ok(Self { + frontend_controller, + backend, + }) + } + + pub fn list_types_command( + &self, + pdb_path: PathBuf, + type_name_filter: String, + case_insensitive: bool, + use_regex: bool, + output_file_path: Option, + ) -> Result<()> { + // Request the backend to load the PDB + self.backend + .send_command(BackendCommand::LoadPDBFromPath(PDB_MAIN_SLOT, pdb_path))?; + // Wait for the backend to finish loading the PDB + if let FrontendCommand::LoadPDBResult(result) = self.frontend_controller.rx_ui.recv()? { + if let Err(err) = result { + return Err(anyhow!("Failed to load PDB: {}", err)); + } + } else { + return Err(anyhow!("Invalid response received from the backend?")); + } + + // Queue a request for the backend to return the list of types that + // match the given filter + self.backend.send_command(BackendCommand::UpdateTypeFilter( + PDB_MAIN_SLOT, + type_name_filter, + case_insensitive, + use_regex, + ))?; + // Wait for the backend to finish filtering types + if let FrontendCommand::UpdateFilteredTypes(type_list) = + self.frontend_controller.rx_ui.recv()? + { + // Dump output + if let Some(output_file_path) = output_file_path { + let mut output_file = File::create(output_file_path)?; + for (type_name, _) in type_list { + writeln!(output_file, "{}", &type_name)?; + } + } else { + for (type_name, _) in type_list { + println!("{type_name}"); + } + } + Ok(()) + } else { + Err(anyhow!("Invalid response received from the backend?")) + } + } + + #[allow(clippy::too_many_arguments)] + pub fn dump_types_command( + &self, + pdb_path: PathBuf, + type_name: Option, + primitive_types_flavor: PrimitiveReconstructionFlavor, + print_header: bool, + print_dependencies: bool, + print_access_specifiers: bool, + highlight_syntax: bool, + output_file_path: Option, + ) -> Result<()> { + // Request the backend to load the PDB + self.backend + .send_command(BackendCommand::LoadPDBFromPath(PDB_MAIN_SLOT, pdb_path))?; + // Wait for the backend to finish loading the PDB + if let FrontendCommand::LoadPDBResult(result) = self.frontend_controller.rx_ui.recv()? { + if let Err(err) = result { + return Err(anyhow!("Failed to load PDB: {}", err)); + } + } else { + return Err(anyhow!("Invalid response received from the backend?")); + } + + // Queue a request for the backend to reconstruct the given type + if let Some(type_name) = type_name { + self.backend + .send_command(BackendCommand::ReconstructTypeByName( + PDB_MAIN_SLOT, + type_name, + primitive_types_flavor, + print_header, + print_dependencies, + print_access_specifiers, + ))?; + } else { + self.backend + .send_command(BackendCommand::ReconstructAllTypes( + PDB_MAIN_SLOT, + primitive_types_flavor, + print_header, + print_access_specifiers, + ))?; + } + // Wait for the backend to finish filtering types + if let FrontendCommand::ReconstructTypeResult(reconstructed_type_result) = + self.frontend_controller.rx_ui.recv()? + { + let reconstructed_type = reconstructed_type_result?; + // Dump output + if let Some(output_file_path) = output_file_path { + let mut output_file = File::create(output_file_path)?; + output_file.write_all(reconstructed_type.as_bytes())?; + } else if highlight_syntax { + let theme = CodeTheme::default(); + if let Some(colorized_reconstructed_type) = + highlight_code(&theme, &reconstructed_type, None) + { + println!("{colorized_reconstructed_type}"); + } + } else { + println!("{reconstructed_type}"); + } + + Ok(()) + } else { + Err(anyhow!("Invalid response received from the backend?")) + } + } + + #[allow(clippy::too_many_arguments)] + pub fn diff_type_command( + &self, + from_pdb_path: PathBuf, + to_pdb_path: PathBuf, + type_name: String, + primitive_types_flavor: PrimitiveReconstructionFlavor, + print_header: bool, + print_dependencies: bool, + print_access_specifiers: bool, + highlight_syntax: bool, + output_file_path: Option, + ) -> Result<()> { + // Request the backend to load the first PDB + self.backend.send_command(BackendCommand::LoadPDBFromPath( + PDB_MAIN_SLOT, + from_pdb_path.clone(), + ))?; + // Wait for the backend to finish loading the PDB + if let FrontendCommand::LoadPDBResult(result) = self.frontend_controller.rx_ui.recv()? { + if let Err(err) = result { + return Err(anyhow!( + "Failed to load PDB '{}': {}", + from_pdb_path.display(), + err + )); + } + } else { + return Err(anyhow!("Invalid response received from the backend?")); + } + + // Request the backend to load the second PDB + self.backend.send_command(BackendCommand::LoadPDBFromPath( + PDB_DIFF_TO_SLOT, + to_pdb_path.clone(), + ))?; + // Wait for the backend to finish loading the PDB + if let FrontendCommand::LoadPDBResult(result) = self.frontend_controller.rx_ui.recv()? { + if let Err(err) = result { + return Err(anyhow!( + "Failed to load PDB '{}': {}", + to_pdb_path.display(), + err + )); + } + } else { + return Err(anyhow!("Invalid response received from the backend?")); + } + + // Queue a request for the backend to diff the given type + self.backend.send_command(BackendCommand::DiffTypeByName( + PDB_MAIN_SLOT, + PDB_DIFF_TO_SLOT, + type_name, + primitive_types_flavor, + print_header, + print_dependencies, + print_access_specifiers, + ))?; + // Wait for the backend to finish + if let FrontendCommand::DiffResult(reconstructed_type_diff_result) = + self.frontend_controller.rx_ui.recv()? + { + let reconstructed_type_diff = reconstructed_type_diff_result?; + // Dump output + if let Some(output_file_path) = output_file_path { + let mut output_file = File::create(output_file_path)?; + output_file.write_all(reconstructed_type_diff.data.as_bytes())?; + } else if highlight_syntax { + let theme = CodeTheme::default(); + let line_descriptions = + reconstructed_type_diff + .metadata + .iter() + .fold(vec![], |mut acc, e| { + acc.push(e.1); + acc + }); + if let Some(colorized_reconstructed_type) = highlight_code( + &theme, + &reconstructed_type_diff.data, + Some(line_descriptions), + ) { + println!("{colorized_reconstructed_type}"); + } + } else { + println!("{}", reconstructed_type_diff.data); + } + + Ok(()) + } else { + Err(anyhow!("Invalid response received from the backend?")) + } + } + + pub fn list_modules_command( + &self, + pdb_path: PathBuf, + module_path_filter: String, + case_insensitive: bool, + use_regex: bool, + output_file_path: Option, + ) -> Result<()> { + // Request the backend to load the PDB + self.backend + .send_command(BackendCommand::LoadPDBFromPath(PDB_MAIN_SLOT, pdb_path))?; + // Wait for the backend to finish loading the PDB + if let FrontendCommand::LoadPDBResult(result) = self.frontend_controller.rx_ui.recv()? { + if let Err(err) = result { + return Err(anyhow!("Failed to load PDB: {}", err)); + } + } else { + return Err(anyhow!("Invalid response received from the backend?")); + } + + // Queue a request for the backend to return the list of all modules + self.backend.send_command(BackendCommand::ListModules( + PDB_MAIN_SLOT, + module_path_filter, + case_insensitive, + use_regex, + ))?; + // Wait for the backend to finish listing modules + if let FrontendCommand::UpdateModuleList(module_list_result) = + self.frontend_controller.rx_ui.recv()? + { + // Dump output + let module_list = module_list_result?; + if let Some(output_file_path) = output_file_path { + let mut output_file = File::create(output_file_path)?; + for (module_path, module_id) in module_list { + writeln!(output_file, "Mod {module_id:04} | '{module_path}'")?; + } + } else { + for (module_path, module_id) in module_list { + println!("Mod {module_id:04} | '{module_path}'"); + } + } + + Ok(()) + } else { + Err(anyhow!("Invalid response received from the backend?")) + } + } + + pub fn dump_module_command( + &self, + pdb_path: PathBuf, + module_id: usize, + primitive_types_flavor: PrimitiveReconstructionFlavor, + print_header: bool, + highlight_syntax: bool, + output_file_path: Option, + ) -> Result<()> { + // Request the backend to load the PDB + self.backend + .send_command(BackendCommand::LoadPDBFromPath(PDB_MAIN_SLOT, pdb_path))?; + // Wait for the backend to finish loading the PDB + if let FrontendCommand::LoadPDBResult(result) = self.frontend_controller.rx_ui.recv()? { + if let Err(err) = result { + return Err(anyhow!("Failed to load PDB: {}", err)); + } + } else { + return Err(anyhow!("Invalid response received from the backend?")); + } + + // Queue a request for the backend to reconstruct the given module + self.backend + .send_command(BackendCommand::ReconstructModuleByIndex( + PDB_MAIN_SLOT, + module_id, + primitive_types_flavor, + print_header, + ))?; + // Wait for the backend to finish filtering types + if let FrontendCommand::ReconstructModuleResult(reconstructed_module) = + self.frontend_controller.rx_ui.recv()? + { + let reconstructed_module = reconstructed_module?; + // Dump output + if let Some(output_file_path) = output_file_path { + let mut output_file = File::create(output_file_path)?; + output_file.write_all(reconstructed_module.as_bytes())?; + } else if highlight_syntax { + let theme = CodeTheme::default(); + if let Some(colorized_reconstructed_type) = + highlight_code(&theme, &reconstructed_module, None) + { + println!("{colorized_reconstructed_type}"); + } + } else { + println!("{reconstructed_module}"); + } + Ok(()) + } else { + Err(anyhow!("Invalid response received from the backend?")) + } + } + + #[allow(clippy::too_many_arguments)] + pub fn diff_module_command( + &self, + from_pdb_path: PathBuf, + to_pdb_path: PathBuf, + module_path: String, + primitive_types_flavor: PrimitiveReconstructionFlavor, + print_header: bool, + highlight_syntax: bool, + output_file_path: Option, + ) -> Result<()> { + // Request the backend to load the first PDB + self.backend.send_command(BackendCommand::LoadPDBFromPath( + PDB_MAIN_SLOT, + from_pdb_path.clone(), + ))?; + // Wait for the backend to finish loading the PDB + if let FrontendCommand::LoadPDBResult(result) = self.frontend_controller.rx_ui.recv()? { + if let Err(err) = result { + return Err(anyhow!( + "Failed to load PDB '{}': {}", + from_pdb_path.display(), + err + )); + } + } else { + return Err(anyhow!("Invalid response received from the backend?")); + } + + // Request the backend to load the second PDB + self.backend.send_command(BackendCommand::LoadPDBFromPath( + PDB_DIFF_TO_SLOT, + to_pdb_path.clone(), + ))?; + // Wait for the backend to finish loading the PDB + if let FrontendCommand::LoadPDBResult(result) = self.frontend_controller.rx_ui.recv()? { + if let Err(err) = result { + return Err(anyhow!( + "Failed to load PDB '{}': {}", + to_pdb_path.display(), + err + )); + } + } else { + return Err(anyhow!("Invalid response received from the backend?")); + } + + // Queue a request for the backend to diff the given module + self.backend.send_command(BackendCommand::DiffModuleByPath( + PDB_MAIN_SLOT, + PDB_DIFF_TO_SLOT, + module_path, + primitive_types_flavor, + print_header, + ))?; + // Wait for the backend to finish + if let FrontendCommand::DiffResult(reconstructed_module_diff_result) = + self.frontend_controller.rx_ui.recv()? + { + let reconstructed_module_diff = reconstructed_module_diff_result?; + // Dump output + if let Some(output_file_path) = output_file_path { + let mut output_file = File::create(output_file_path)?; + output_file.write_all(reconstructed_module_diff.data.as_bytes())?; + } else if highlight_syntax { + let theme = CodeTheme::default(); + let line_descriptions = + reconstructed_module_diff + .metadata + .iter() + .fold(vec![], |mut acc, e| { + acc.push(e.1); + acc + }); + if let Some(colorized_reconstructed_module) = highlight_code( + &theme, + &reconstructed_module_diff.data, + Some(line_descriptions), + ) { + println!("{colorized_reconstructed_module}"); + } + } else { + println!("{}", reconstructed_module_diff.data); + } + + Ok(()) + } else { + Err(anyhow!("Invalid response received from the backend?")) + } + } +} + +#[cfg(test)] +mod tests { + use std::fs; + + use super::*; + + use tempdir::TempDir; + + const TEST_PDB_FILE_PATH: &str = "../resym_core/tests/data/test.pdb"; + const TEST_PDB_FROM_FILE_PATH: &str = "../resym_core/tests/data/test_diff_from.pdb"; + const TEST_PDB_TO_FILE_PATH: &str = "../resym_core/tests/data/test_diff_to.pdb"; + + // List types + #[test] + fn list_types_command_invalid_pdb_path() { + let app = ResymcApp::new().expect("ResymcApp creation failed"); + let pdb_path = PathBuf::new(); + // The command should fail + assert!(app + .list_types_command( + pdb_path, + "resym_test::StructTest".to_string(), + false, + false, + None, + ) + .is_err()); + } + + #[test] + fn list_types_command_stdio_successful() { + let app = ResymcApp::new().expect("ResymcApp creation failed"); + let pdb_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(TEST_PDB_FILE_PATH); + // The command should succeed + assert!(app + .list_types_command( + pdb_path, + "resym_test::StructTest".to_string(), + true, + true, + None, + ) + .is_ok()); + } + + #[test] + fn list_types_command_file_successful() { + let app = ResymcApp::new().expect("ResymcApp creation failed"); + let pdb_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(TEST_PDB_FILE_PATH); + let tmp_dir = + TempDir::new("list_types_command_file_successful").expect("TempDir creation failed"); + let output_path = tmp_dir.path().join("output.txt"); + // The command should succeed + assert!(app + .list_types_command( + pdb_path, + "resym_test::ClassWithNestedDeclarationsTest".to_string(), + false, + false, + Some(output_path.clone()), + ) + .is_ok()); + + // Check output file's content + let output = fs::read_to_string(output_path).expect("Failed to read output file"); + assert_eq!( + output, + concat!( + "resym_test::ClassWithNestedDeclarationsTest::NestEnum\n", + "resym_test::ClassWithNestedDeclarationsTest\n", + "resym_test::ClassWithNestedDeclarationsTest::NestedUnion\n", + "resym_test::ClassWithNestedDeclarationsTest::NestedClass\n", + "resym_test::ClassWithNestedDeclarationsTest::NestedStruct\n" + ) + ); + } + + // Dump types + #[test] + fn dump_types_command_invalid_pdb_path() { + let app = ResymcApp::new().expect("ResymcApp creation failed"); + let pdb_path = PathBuf::new(); + // The command should fail + assert!(app + .dump_types_command( + pdb_path, + None, + PrimitiveReconstructionFlavor::Microsoft, + false, + false, + false, + false, + None + ) + .is_err()); + } + + #[test] + fn dump_types_command_stdio_successful() { + let app = ResymcApp::new().expect("ResymcApp creation failed"); + let pdb_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(TEST_PDB_FILE_PATH); + + // The command should succeed + assert!(app + .dump_types_command( + pdb_path, + None, + PrimitiveReconstructionFlavor::Microsoft, + true, + true, + true, + true, + None + ) + .is_ok()); + } + + #[test] + fn dump_types_command_file_successful() { + let app = ResymcApp::new().expect("ResymcApp creation failed"); + let pdb_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(TEST_PDB_FILE_PATH); + let tmp_dir = + TempDir::new("dump_types_command_file_successful").expect("TempDir creation failed"); + let output_path = tmp_dir.path().join("output.txt"); + + // The command should succeed + assert!(app + .dump_types_command( + pdb_path, + Some("resym_test::ClassWithNestedDeclarationsTest".to_string()), + PrimitiveReconstructionFlavor::Microsoft, + false, + false, + false, + false, + Some(output_path.clone()), + ) + .is_ok()); + + // Check output file's content + let output = fs::read_to_string(output_path).expect("Failed to read output file"); + assert_eq!( + output, + concat!("\nclass resym_test::ClassWithNestedDeclarationsTest { /* Size=0x1 */\n};\n") + ); + } + + // Diff type + #[test] + fn diff_type_command_invalid_pdb_path() { + let app = ResymcApp::new().expect("ResymcApp creation failed"); + let pdb_path_from = PathBuf::new(); + let pdb_path_to = PathBuf::new(); + + // The command should fail + assert!(app + .diff_type_command( + pdb_path_from, + pdb_path_to, + "".to_string(), + PrimitiveReconstructionFlavor::Microsoft, + false, + false, + false, + false, + None + ) + .is_err()); + } + #[test] + fn diff_type_command_stdio_successful() { + let app = ResymcApp::new().expect("ResymcApp creation failed"); + let pdb_path_from = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(TEST_PDB_FROM_FILE_PATH); + let pdb_path_to = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(TEST_PDB_TO_FILE_PATH); + + // The command should succeed + assert!(app + .diff_type_command( + pdb_path_from, + pdb_path_to, + "UserStructAddAndReplace".to_string(), + PrimitiveReconstructionFlavor::Microsoft, + true, + true, + true, + true, + None + ) + .is_ok()); + } + + #[test] + fn diff_type_command_file_successful() { + let app = ResymcApp::new().expect("ResymcApp creation failed"); + let pdb_path_from = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(TEST_PDB_FROM_FILE_PATH); + let pdb_path_to = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(TEST_PDB_TO_FILE_PATH); + + let tmp_dir = + TempDir::new("diff_type_command_file_successful").expect("TempDir creation failed"); + let output_path = tmp_dir.path().join("output.txt"); + + // The command should succeed + assert!(app + .diff_type_command( + pdb_path_from, + pdb_path_to, + "UserStructAddAndReplace".to_string(), + PrimitiveReconstructionFlavor::Portable, + false, + false, + false, + false, + Some(output_path.clone()), + ) + .is_ok()); + + // Check output file's content + let output = fs::read_to_string(output_path).expect("Failed to read output file"); + assert_eq!( + output, + concat!( + " \n-struct UserStructAddAndReplace { /* Size=0x10 */\n", + "- /* 0x0000 */ int32_t field1;\n- /* 0x0004 */ char field2;\n", + "- /* 0x0008 */ void* field3;\n+struct UserStructAddAndReplace { /* Size=0x28 */\n", + "+ /* 0x0000 */ int32_t before1;\n+ /* 0x0004 */ int32_t field1;\n", + "+ /* 0x0008 */ int32_t between12;\n+ /* 0x000c */ char field2;\n", + "+ /* 0x0010 */ int32_t between23;\n+ /* 0x0018 */ void* field3;\n", + "+ /* 0x0020 */ int32_t after3;\n };\n", + ) + ); + } + + // List modules + #[test] + fn list_modules_command_invalid_pdb_path() { + let app = ResymcApp::new().expect("ResymcApp creation failed"); + let pdb_path = PathBuf::new(); + // The command should fail + assert!(app + .list_modules_command(pdb_path, "*".to_string(), false, false, None) + .is_err()); + } + + #[test] + fn list_modules_command_stdio_successful() { + let app = ResymcApp::new().expect("ResymcApp creation failed"); + let pdb_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(TEST_PDB_FILE_PATH); + // The command should succeed + assert!(app + .list_modules_command(pdb_path, "*".to_string(), true, true, None) + .is_ok()); + } + + #[test] + fn list_modules_command_file_successful() { + let app = ResymcApp::new().expect("ResymcApp creation failed"); + let pdb_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(TEST_PDB_FILE_PATH); + let tmp_dir = + TempDir::new("list_modules_command_file_successful").expect("TempDir creation failed"); + let output_path = tmp_dir.path().join("output.txt"); + // The command should succeed + assert!(app + .list_modules_command( + pdb_path, + "*".to_string(), + false, + false, + Some(output_path.clone()), + ) + .is_ok()); + + // Check output file's content + let output = fs::read_to_string(output_path).expect("Failed to read output file"); + assert_eq!( + output, + concat!( + "Mod 0048 | '* Linker Generated Manifest RES *'\n", + "Mod 0053 | '* Linker *'\n" + ) + ); + } + + // Dump module + #[test] + fn dump_module_command_invalid_pdb_path() { + let app = ResymcApp::new().expect("ResymcApp creation failed"); + let pdb_path = PathBuf::new(); + // The command should fail + assert!(app + .dump_module_command( + pdb_path, + 9, // exe_main.obj + PrimitiveReconstructionFlavor::Microsoft, + false, + false, + None + ) + .is_err()); + } + + #[test] + fn dump_module_command_stdio_successful() { + let app = ResymcApp::new().expect("ResymcApp creation failed"); + let pdb_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(TEST_PDB_FILE_PATH); + // The command should succeed + assert!(app + .dump_module_command( + pdb_path, + 9, // exe_main.obj + PrimitiveReconstructionFlavor::Microsoft, + true, + true, + None + ) + .is_ok()); + } + + #[test] + fn dump_module_command_file_successful() { + let app = ResymcApp::new().expect("ResymcApp creation failed"); + let pdb_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(TEST_PDB_FILE_PATH); + let tmp_dir = + TempDir::new("dump_module_command_file_successful").expect("TempDir creation failed"); + let output_path = tmp_dir.path().join("output.txt"); + // The command should succeed + assert!(app + .dump_module_command( + pdb_path, + 27, // default_local_stdio_options.obj + PrimitiveReconstructionFlavor::Portable, + false, + false, + Some(output_path.clone()), + ) + .is_ok()); + + // Check output file's content + let output = fs::read_to_string(output_path).expect("Failed to read output file"); + assert_eq!( + output, + concat!( + "using namespace std;\n", + "using PUWSTR_C = const wchar_t*;\n", + "using TP_CALLBACK_ENVIRON_V3 = _TP_CALLBACK_ENVIRON_V3;\n", + "uint64_t* (__local_stdio_scanf_options)(); // CodeSize=8\n", + "uint64_t _OptionsStorage;\n", + "void (__scrt_initialize_default_local_stdio_options)(); // CodeSize=69\n", + ) + ); + } + + // Diff module + #[test] + fn diff_module_command_invalid_pdb_path() { + let app = ResymcApp::new().expect("ResymcApp creation failed"); + let pdb_path_from = PathBuf::new(); + let pdb_path_to = PathBuf::new(); + + // The command should fail + assert!(app + .diff_module_command( + pdb_path_from, + pdb_path_to, + "d:\\a01\\_work\\43\\s\\Intermediate\\vctools\\msvcrt.nativeproj_607447030\\objd\\amd64\\exe_main.obj".to_string(), + PrimitiveReconstructionFlavor::Microsoft, + false, + false, + None + ) + .is_err()); + } + + #[test] + fn diff_module_command_stdio_successful() { + let app = ResymcApp::new().expect("ResymcApp creation failed"); + let pdb_path_from = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(TEST_PDB_FROM_FILE_PATH); + let pdb_path_to = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(TEST_PDB_TO_FILE_PATH); + + // The command should succeed + assert!(app + .diff_module_command( + pdb_path_from, + pdb_path_to, + "d:\\a01\\_work\\43\\s\\Intermediate\\vctools\\msvcrt.nativeproj_607447030\\objd\\amd64\\exe_main.obj".to_string(), + PrimitiveReconstructionFlavor::Microsoft, + true, + true, + None + ) + .is_ok()); + } + + #[test] + fn diff_module_command_file_successful() { + let app = ResymcApp::new().expect("ResymcApp creation failed"); + let pdb_path_from = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(TEST_PDB_FROM_FILE_PATH); + let pdb_path_to = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(TEST_PDB_TO_FILE_PATH); + + let tmp_dir = + TempDir::new("diff_module_command_file_successful").expect("TempDir creation failed"); + let output_path = tmp_dir.path().join("output.txt"); + + // The command should succeed + assert!(app + .diff_module_command( + pdb_path_from, + pdb_path_to, + "d:\\a01\\_work\\43\\s\\Intermediate\\vctools\\msvcrt.nativeproj_607447030\\objd\\amd64\\default_local_stdio_options.obj".to_string(), + PrimitiveReconstructionFlavor::Portable, + false, + false, + Some(output_path.clone()), + ) + .is_ok()); + + // Check output file's content + let output = fs::read_to_string(output_path).expect("Failed to read output file"); + assert_eq!( + output, + concat!( + " using namespace std;\n", + " using PUWSTR_C = const wchar_t*;\n", + " using TP_CALLBACK_ENVIRON_V3 = _TP_CALLBACK_ENVIRON_V3;\n", + " uint64_t* (__local_stdio_scanf_options)(); // CodeSize=8\n", + " uint64_t _OptionsStorage;\n", + " void (__scrt_initialize_default_local_stdio_options)(); // CodeSize=69\n", + ) + ); + } +} diff --git a/resymc/src/resymc_options.rs b/resymc/src/resymc_options.rs new file mode 100644 index 0000000..0606510 --- /dev/null +++ b/resymc/src/resymc_options.rs @@ -0,0 +1,151 @@ +use std::path::PathBuf; + +use resym_core::pdb_types::PrimitiveReconstructionFlavor; +use structopt::StructOpt; + +const PKG_NAME: &str = env!("CARGO_PKG_NAME"); + +#[derive(Debug, StructOpt)] +#[structopt( + name = PKG_NAME, + about = "resymc is a utility that allows browsing and extracting types from PDB files." +)] +pub enum ResymcOptions { + /// List types from a given PDB file + List { + /// Path to the PDB file + pdb_path: PathBuf, + /// Search filter + type_name_filter: String, + /// Path of the output file + output_file_path: Option, + /// Do not match case + #[structopt(short = "i", long)] + case_insensitive: bool, + /// Use regular expressions + #[structopt(short = "r", long)] + use_regex: bool, + }, + /// Dump type from a given PDB file + Dump { + /// Path to the PDB file + pdb_path: PathBuf, + /// Name of the type to extract + type_name: String, + /// Path of the output file + output_file_path: Option, + /// Representation of primitive types + #[structopt(short = "f", long)] + primitive_types_flavor: Option, + /// Print header + #[structopt(short = "h", long)] + print_header: bool, + /// Print declarations of referenced types + #[structopt(short = "d", long)] + print_dependencies: bool, + /// Print C++ access specifiers + #[structopt(short = "a", long)] + print_access_specifiers: bool, + /// Highlight C++ output + #[structopt(short = "H", long)] + highlight_syntax: bool, + }, + /// Dump all types from a given PDB file + DumpAll { + /// Path to the PDB file + pdb_path: PathBuf, + /// Path of the output file + output_file_path: Option, + /// Representation of primitive types + #[structopt(short = "f", long)] + primitive_types_flavor: Option, + /// Print header + #[structopt(short = "h", long)] + print_header: bool, + /// Print C++ access specifiers + #[structopt(short = "a", long)] + print_access_specifiers: bool, + /// Highlight C++ output + #[structopt(short = "H", long)] + highlight_syntax: bool, + }, + /// Compute diff for a type between two given PDB files + Diff { + /// Path of the PDB file to compute the diff from + from_pdb_path: PathBuf, + /// Path of the PDB file to compute the diff to + to_pdb_path: PathBuf, + /// Name of the type to diff + type_name: String, + /// Path of the output file + output_file_path: Option, + /// Representation of primitive types + #[structopt(short = "f", long)] + primitive_types_flavor: Option, + /// Print header + #[structopt(short = "h", long)] + print_header: bool, + /// Print declarations of referenced types + #[structopt(short = "d", long)] + print_dependencies: bool, + /// Print C++ access specifiers + #[structopt(short = "a", long)] + print_access_specifiers: bool, + /// Highlight C++ output and add/deleted lines + #[structopt(short = "H", long)] + highlight_syntax: bool, + }, + /// List modules from a given PDB file + ListModules { + /// Path to the PDB file + pdb_path: PathBuf, + /// Search filter + module_path_filter: String, + /// Path of the output file + output_file_path: Option, + /// Do not match case + #[structopt(short = "i", long)] + case_insensitive: bool, + /// Use regular expressions + #[structopt(short = "r", long)] + use_regex: bool, + }, + /// Dump module from a given PDB file + DumpModule { + /// Path to the PDB file + pdb_path: PathBuf, + /// ID of the module to dump + module_id: usize, + /// Path of the output file + output_file_path: Option, + /// Representation of primitive types + #[structopt(short = "f", long)] + primitive_types_flavor: Option, + /// Print header + #[structopt(short = "h", long)] + print_header: bool, + /// Highlight C++ output + #[structopt(short = "H", long)] + highlight_syntax: bool, + }, + /// Compute diff for a module between two given PDB files + DiffModule { + /// Path of the PDB file to compute the diff from + from_pdb_path: PathBuf, + /// Path of the PDB file to compute the diff to + to_pdb_path: PathBuf, + /// Path of the module to diff + module_path: String, + /// Path of the output file + output_file_path: Option, + /// Representation of primitive types + #[structopt(short = "f", long)] + primitive_types_flavor: Option, + /// Print header + #[structopt(short = "h", long)] + print_header: bool, + /// Highlight C++ output and add/deleted lines + #[structopt(short = "H", long)] + highlight_syntax: bool, + }, +}