From 4be934b74e392428cbd5ec88e40d6abe05f9ffe9 Mon Sep 17 00:00:00 2001 From: ergrelet Date: Sat, 9 Mar 2024 03:25:07 +0100 Subject: [PATCH] WIP: Implement cross-reference listing --- resym/src/resym_app.rs | 110 +++++++++++++++++++++++---- resym/src/ui_components/type_list.rs | 14 +++- resym_core/src/backend.rs | 27 +++++++ resym_core/src/frontend.rs | 4 +- resym_core/src/pdb_file.rs | 69 +++++++++++++++++ 5 files changed, 207 insertions(+), 17 deletions(-) diff --git a/resym/src/resym_app.rs b/resym/src/resym_app.rs index ae1f6e3..8257f42 100644 --- a/resym/src/resym_app.rs +++ b/resym/src/resym_app.rs @@ -40,23 +40,36 @@ impl From for PDBSlot { } } +/// Tabs available for the left-side panel #[derive(PartialEq)] -enum ExplorerTab { +enum LeftPanelTab { TypeSearch, ModuleBrowsing, } +/// Tabs available for the bottom panel +#[derive(PartialEq)] +enum BottomPanelTab { + Console, + Xrefs, +} + /// Struct that represents our GUI application. /// It contains the whole application's context at all time. pub struct ResymApp { current_mode: ResymAppMode, - explorer_selected_tab: ExplorerTab, + // Components used in the left-side panel + left_panel_selected_tab: LeftPanelTab, type_search: TextSearchComponent, type_list: TypeListComponent, module_search: TextSearchComponent, module_tree: ModuleTreeComponent, code_view: CodeViewComponent, + // Components used in the bottom panel + bottom_panel_selected_tab: BottomPanelTab, console: ConsoleComponent, + xref_list: TypeListComponent, + // Other components settings: SettingsComponent, #[cfg(feature = "http")] open_url: OpenURLComponent, @@ -141,13 +154,15 @@ impl ResymApp { log::info!("{} {}", PKG_NAME, PKG_VERSION); Ok(Self { current_mode: ResymAppMode::Idle, - explorer_selected_tab: ExplorerTab::TypeSearch, + left_panel_selected_tab: LeftPanelTab::TypeSearch, type_search: TextSearchComponent::new(), type_list: TypeListComponent::new(), module_search: TextSearchComponent::new(), module_tree: ModuleTreeComponent::new(), code_view: CodeViewComponent::new(), + bottom_panel_selected_tab: BottomPanelTab::Console, console: ConsoleComponent::new(logger), + xref_list: TypeListComponent::new(), settings: SettingsComponent::new(app_settings), #[cfg(feature = "http")] open_url: OpenURLComponent::new(), @@ -184,20 +199,20 @@ impl ResymApp { .show(ctx, |ui| { ui.horizontal(|ui| { ui.selectable_value( - &mut self.explorer_selected_tab, - ExplorerTab::TypeSearch, + &mut self.left_panel_selected_tab, + LeftPanelTab::TypeSearch, "Search types", ); ui.selectable_value( - &mut self.explorer_selected_tab, - ExplorerTab::ModuleBrowsing, + &mut self.left_panel_selected_tab, + LeftPanelTab::ModuleBrowsing, "Browse modules", ); }); ui.separator(); - match self.explorer_selected_tab { - ExplorerTab::TypeSearch => { + match self.left_panel_selected_tab { + LeftPanelTab::TypeSearch => { // Callback run when the search query changes let on_query_update = |search_query: &str| { // Update filtered list if filter has changed @@ -239,7 +254,7 @@ impl ResymApp { ui, ); } - ExplorerTab::ModuleBrowsing => { + LeftPanelTab::ModuleBrowsing => { // Callback run when the search query changes let on_query_update = |search_query: &str| match self.current_mode { ResymAppMode::Browsing(..) | ResymAppMode::Comparing(..) => { @@ -305,18 +320,44 @@ impl ResymApp { }); } + /// Update/render the bottom panel component and its sub-components fn update_bottom_panel(&mut self, ctx: &egui::Context) { egui::TopBottomPanel::bottom("bottom_panel") .min_height(100.0) .resizable(true) .show(ctx, |ui| { - // Console panel ui.vertical(|ui| { - ui.label("Console"); + // Tab headers + ui.horizontal(|ui| { + ui.selectable_value( + &mut self.bottom_panel_selected_tab, + BottomPanelTab::Console, + "Console", + ); + ui.selectable_value( + &mut self.bottom_panel_selected_tab, + BottomPanelTab::Xrefs, + "Xrefs to", + ); + }); ui.add_space(4.0); - // Update the console component - self.console.update(ui); + // Tab body + match self.bottom_panel_selected_tab { + BottomPanelTab::Console => { + // Console panel + self.console.update(ui); + } + BottomPanelTab::Xrefs => { + // Update Xref list + self.xref_list.update( + &self.settings.app_settings, + &self.current_mode, + &self.backend, + ui, + ); + } + } }); }); } @@ -335,13 +376,35 @@ impl ResymApp { // Start displaying buttons from the right #[cfg_attr(target_arch = "wasm32", allow(unused_variables))] ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { + // Fetures only available in "Browsing" mode if let ResymAppMode::Browsing(..) = self.current_mode { - // Save button handling + // Save button // Note: not available on wasm32 #[cfg(not(target_arch = "wasm32"))] if ui.button("💾 Save (Ctrl+S)").clicked() { self.start_save_reconstruted_content(); } + + // Cross-references button + if let Some(selected_type_index) = self.type_list.selected_type_index() { + if ui.button("🔍 Find Xrefs to").clicked() { + log::info!( + "Looking for cross-references for type #0x{:x}...", + selected_type_index.0 + ); + if let Err(err) = self.backend.send_command( + BackendCommand::ListTypeCrossReferences( + ResymPDBSlots::Main as usize, + selected_type_index, + ), + ) { + log::error!( + "Failed to list cross-references to type #0x{:x}: {err}", + selected_type_index.0 + ); + } + } + } } }); }); @@ -598,6 +661,23 @@ impl ResymApp { FrontendCommand::UpdateFilteredTypes(filtered_types) => { self.type_list.update_type_list(filtered_types); } + + FrontendCommand::ListTypeCrossReferencesResult(xref_list_result) => { + match xref_list_result { + Err(err) => { + log::error!("Failed to list cross-references: {err}"); + } + Ok(xref_list) => { + let xref_count = xref_list.len(); + log::info!("{xref_count} cross-references found!"); + + // Update xref list component + self.xref_list.update_type_list(xref_list); + // Switch to xref tab + self.bottom_panel_selected_tab = BottomPanelTab::Xrefs; + } + } + } } } } diff --git a/resym/src/ui_components/type_list.rs b/resym/src/ui_components/type_list.rs index 7633252..b041027 100644 --- a/resym/src/ui_components/type_list.rs +++ b/resym/src/ui_components/type_list.rs @@ -1,7 +1,7 @@ use eframe::egui::{self, ScrollArea, TextStyle}; use resym_core::{ backend::{Backend, BackendCommand}, - frontend::TypeList, + frontend::{TypeIndex, TypeList}, }; use crate::{mode::ResymAppMode, resym_app::ResymPDBSlots, settings::ResymAppSettings}; @@ -19,6 +19,12 @@ impl TypeListComponent { } } + pub fn selected_type_index(&self) -> Option { + self.filtered_type_list + .get(self.selected_row) + .map(|tuple| tuple.1) + } + pub fn update_type_list(&mut self, type_list: TypeList) { self.filtered_type_list = type_list; self.selected_row = usize::MAX; @@ -37,6 +43,12 @@ impl TypeListComponent { ui.with_layout( egui::Layout::top_down(egui::Align::Min).with_cross_justify(true), |ui| { + if num_rows == 0 { + // Display a default message to make it obvious the list is empty + ui.label("No results"); + return; + } + ScrollArea::vertical() .auto_shrink([false, false]) .show_rows(ui, row_height, num_rows, |ui, row_range| { diff --git a/resym_core/src/backend.rs b/resym_core/src/backend.rs index bf0ed7c..e6e14e0 100644 --- a/resym_core/src/backend.rs +++ b/resym_core/src/backend.rs @@ -95,6 +95,8 @@ pub enum BackendCommand { PrimitiveReconstructionFlavor, bool, ), + /// Retrieve a list of all types that reference the given type + ListTypeCrossReferences(PDBSlot, pdb::TypeIndex), } /// Struct that represents the backend. The backend is responsible @@ -481,6 +483,14 @@ fn worker_thread_routine( } } } + + BackendCommand::ListTypeCrossReferences(pdb_slot, type_index) => { + if let Some(pdb_file) = pdb_files.get(&pdb_slot) { + let xref_list = list_type_xrefs_command(&mut pdb_file.borrow_mut(), type_index); + frontend_controller + .send_command(FrontendCommand::ListTypeCrossReferencesResult(xref_list))?; + } + } } } @@ -757,3 +767,20 @@ fn filter_modules_regular( .collect() } } + +fn list_type_xrefs_command<'p, T>( + pdb_file: &mut PdbFile<'p, T>, + type_index: pdb::TypeIndex, +) -> Result> +where + T: io::Seek + io::Read + std::fmt::Debug + 'p, +{ + let xref_start = Instant::now(); + let xref_list = pdb_file.get_xrefs_for_type(type_index)?; + log::debug!( + "Xref resolution took {} ms", + xref_start.elapsed().as_millis() + ); + + Ok(xref_list) +} diff --git a/resym_core/src/frontend.rs b/resym_core/src/frontend.rs index 0511f73..98ac462 100644 --- a/resym_core/src/frontend.rs +++ b/resym_core/src/frontend.rs @@ -1,6 +1,7 @@ use crate::{backend::PDBSlot, diffing::Diff, error::Result}; -pub type TypeList = Vec<(String, pdb::TypeIndex)>; +pub type TypeIndex = pdb::TypeIndex; +pub type TypeList = Vec<(String, TypeIndex)>; pub type ModuleList = Vec<(String, usize)>; pub enum FrontendCommand { @@ -13,6 +14,7 @@ pub enum FrontendCommand { ReconstructModuleResult(Result), UpdateModuleList(Result), DiffResult(Result), + ListTypeCrossReferencesResult(Result), } pub trait FrontendController { diff --git a/resym_core/src/pdb_file.rs b/resym_core/src/pdb_file.rs index 349303d..5082050 100644 --- a/resym_core/src/pdb_file.rs +++ b/resym_core/src/pdb_file.rs @@ -61,6 +61,7 @@ where pub type_information: pdb::TypeInformation<'p>, pub debug_information: pdb::DebugInformation<'p>, pub file_path: PathBuf, + pub xref_map: DashMap>, _pdb: pdb::PDB<'p, T>, } @@ -81,6 +82,7 @@ impl<'p> PdbFile<'p, File> { type_information, debug_information, file_path: pdb_file_path.to_owned(), + xref_map: DashMap::default(), _pdb: pdb, }; pdb_file.load_symbols()?; @@ -108,6 +110,7 @@ impl<'p> PdbFile<'p, PDBDataSource> { type_information, debug_information, file_path: pdb_file_name.into(), + xref_map: DashMap::default(), _pdb: pdb, }; pdb_file.load_symbols()?; @@ -133,6 +136,7 @@ impl<'p> PdbFile<'p, PDBDataSource> { type_information, debug_information, file_path: pdb_file_name.into(), + xref_map: DashMap::default(), _pdb: pdb, }; pdb_file.load_symbols()?; @@ -604,4 +608,69 @@ where type_data.reconstruct(&fmt_configuration, &mut reconstruction_output)?; Ok(reconstruction_output) } + + pub fn get_xrefs_for_type( + &mut self, + type_index: pdb::TypeIndex, + ) -> Result> { + // Generate xref cache if empty + if self.xref_map.is_empty() { + // 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); + } + } + + // Iterate through all types + let xref_map: DashMap> = DashMap::default(); + let mut type_iter = self.type_information.iter(); + while let Some(type_item) = type_iter.next()? { + let current_type_index = type_item.index(); + // Reconstruct type and retrieve referenced types + let mut type_data = pdb_types::Data::new(); + let mut needed_types = pdb_types::TypeSet::new(); + type_data.add( + &type_finder, + &self.forwarder_to_complete_type, + current_type_index, + &PrimitiveReconstructionFlavor::Raw, + &mut needed_types, + )?; + + par_iter_if_available!(needed_types).for_each(|t| { + if let Some(mut xref_list) = xref_map.get_mut(t) { + xref_list.push(current_type_index); + } else { + xref_map.insert(*t, vec![current_type_index]); + } + }); + } + + // Update cache + self.xref_map = xref_map; + } + + // Query xref cache + if let Some(xref_list) = self.xref_map.get(&type_index) { + // Convert the xref list into a proper Name+TypeIndex tuple list + let xref_type_list = xref_list + .iter() + .map(|xref_type_index| { + let tuple = par_iter_if_available!(self.complete_type_list) + .find_first(|(_, type_index)| type_index == xref_type_index) + .expect("`complete_type_list` should contain type index"); + + tuple.clone() + }) + .collect(); + + Ok(xref_type_list) + } else { + // TODO: error + Ok(vec![]) + } + } }