Compare commits

..

7 Commits

25 changed files with 2156 additions and 1190 deletions

806
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "objdiff"
version = "0.3.2"
version = "0.4.0"
edition = "2021"
rust-version = "1.65"
authors = ["Luke Street <luke@street.dev>"]
@@ -23,39 +23,43 @@ wgpu = ["eframe/wgpu"]
[dependencies]
anyhow = "1.0.71"
byteorder = "1.4.3"
bytes = "1.4.0"
cfg-if = "1.0.0"
const_format = "0.2.30"
const_format = "0.2.31"
cwdemangle = "0.1.5"
eframe = { version = "0.21.3", features = ["persistence"] }
egui = "0.21.0"
egui_extras = "0.21.0"
dirs = "5.0.1"
eframe = { version = "0.22.0", features = ["persistence"] }
egui = "0.22.0"
egui_extras = "0.22.0"
flagset = "0.4.3"
log = "0.4.17"
memmap2 = "0.6.1"
notify = "5.1.0"
globset = { version = "0.4.13", features = ["serde1"] }
log = "0.4.19"
memmap2 = "0.7.1"
notify = "6.0.1"
object = { version = "0.31.1", features = ["read_core", "std", "elf"], default-features = false }
png = "0.17.8"
png = "0.17.9"
ppc750cl = { git = "https://github.com/terorie/ppc750cl", rev = "9ae36eef34aa6d74e00972c7671f547a2acfd0aa" }
rabbitizer = "1.7.1"
rfd = { version = "0.11.3" } #, default-features = false, features = ['xdg-portal']
rabbitizer = "1.7.4"
rfd = { version = "0.11.4" } #, default-features = false, features = ['xdg-portal']
serde = { version = "1", features = ["derive"] }
tempfile = "3.5.0"
thiserror = "1.0.40"
time = { version = "0.3.21", features = ["formatting", "local-offset"] }
toml = "0.7.3"
serde_json = "1.0.104"
serde_yaml = "0.9.25"
tempfile = "3.6.0"
thiserror = "1.0.41"
time = { version = "0.3.22", features = ["formatting", "local-offset"] }
toml = "0.7.6"
twox-hash = "1.6.3"
byteorder = "1.4.3"
# For Linux static binaries, use rustls
[target.'cfg(target_os = "linux")'.dependencies]
reqwest = { version = "0.11.17", default-features = false, features = ["blocking", "json", "rustls"] }
self_update = { version = "0.36.0", default-features = false, features = ["rustls"] }
reqwest = { version = "0.11.18", default-features = false, features = ["blocking", "json", "rustls"] }
self_update = { version = "0.37.0", default-features = false, features = ["rustls"] }
# For all other platforms, use native TLS
[target.'cfg(not(target_os = "linux"))'.dependencies]
reqwest = "0.11.17"
self_update = "0.36.0"
reqwest = "0.11.18"
self_update = "0.37.0"
[target.'cfg(windows)'.dependencies]
path-slash = "0.2.1"
@@ -78,4 +82,4 @@ tracing-wasm = "0.2"
[build-dependencies]
anyhow = "1.0.71"
vergen = { version = "8.1.3", features = ["build", "cargo", "git", "gitcl"] }
vergen = { version = "8.2.4", features = ["build", "cargo", "git", "gitcl"] }

View File

@@ -5,14 +5,87 @@
A local diffing tool for decompilation projects.
Currently supports:
Supports:
- PowerPC 750CL (GameCube & Wii)
- MIPS (Nintendo 64)
See [Usage](#usage) for more information.
![Symbol Screenshot](assets/screen-symbols.png)
![Diff Screenshot](assets/screen-diff.png)
### License
## Usage
objdiff works by comparing two relocatable object files (`.o`). The objects are expected to have the same relative path from the "target" and "base" directories.
For example, if the target ("expected") object is located at `build/asm/MetroTRK/mslsupp.o` and the base ("actual") object
is located at `build/src/MetroTRK/mslsupp.o`, the following configuration would be used:
- Target build directory: `build/asm`
- Base build directory: `build/src`
- Object: `MetroTRK/mslsupp.o`
objdiff will then execute the build system from the project directory to build both objects:
```sh
$ make build/asm/MetroTRK/mslsupp.o # Only if "Build target object" is enabled
$ make build/src/MetroTRK/mslsupp.o
```
The objects will then be compared and the results will be displayed in the UI.
See [Configuration](#configuration) for more information.
## Configuration
While **not required** (most settings can be specified in the UI), projects can add an `objdiff.json` (or `objdiff.yaml`, `objdiff.yml`) file to configure the tool automatically. The configuration file must be located in the root project directory.
```json5
// objdiff.json
{
"custom_make": "ninja",
"target_dir": "build/mp1.0/asm",
"base_dir": "build/mp1.0/src",
"build_target": true,
"watch_patterns": [
"*.c",
"*.cp",
"*.cpp",
"*.h",
"*.hpp",
"*.py"
],
"objects": [
{
"path": "MetroTRK/mslsupp.o",
"name": "MetroTRK/mslsupp",
"reverse_fn_order": false
},
// ...
]
}
```
- `custom_make` _(optional)_: By default, objdiff will use `make` to build the project.
If the project uses a different build system (e.g. `ninja`), specify it here.
- `target_dir`: Relative from the root of the project, this where the "target" or "expected" objects are located.
These are the **intended result** of the match.
- `base_dir`: Relative from the root of the project, this is where the "base" or "actual" objects are located.
These are objects built from the **current source code**.
- `build_target`: If true, objdiff will tell the build system to build the target objects before diffing (e.g. `make path/to/target.o`).
This is useful if the target objects are not built by default or can change based on project configuration or edits to assembly files.
Requires the build system to be configured properly.
- `watch_patterns` _(optional)_: A list of glob patterns to watch for changes. ([Supported syntax](https://docs.rs/globset/latest/globset/#syntax))
If any of these files change, objdiff will automatically rebuild the objects and re-compare them.
- `objects` _(optional)_: If specified, objdiff will display a list of objects in the sidebar for easy navigation.
- `path`: Relative path to the object from the `target_dir` and `base_dir`.
- `name` _(optional)_: The name of the object in the UI. If not specified, the object's `path` will be used.
- `reverse_fn_order` _(optional)_: Displays function symbols in reversed order.
Used to support MWCC's `-inline deferred` option, which reverses the order of functions in the object file.
## License
Licensed under either of

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 144 KiB

View File

@@ -48,9 +48,11 @@ notice = "warn"
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
ignore = [
# git2 (build dependency)
"RUSTSEC-2023-0002",
"RUSTSEC-2023-0003",
"RUSTSEC-2023-0022",
"RUSTSEC-2023-0023",
"RUSTSEC-2023-0024",
"RUSTSEC-2023-0034",
"RUSTSEC-2023-0044",
]
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
# lower than the range specified will be ignored. Note that ignored advisories
@@ -86,6 +88,7 @@ allow = [
"OFL-1.1",
"LicenseRef-UFL-1.0",
"OpenSSL",
"GPL-3.0",
]
# List of explictly disallowed licenses
# See https://spdx.org/licenses/ for list of possible licenses

View File

@@ -1,6 +1,5 @@
use std::{
default::Default,
ffi::OsStr,
path::{Path, PathBuf},
rc::Rc,
sync::{
@@ -10,199 +9,115 @@ use std::{
time::Duration,
};
use egui::{Color32, FontFamily, FontId, TextStyle};
use globset::{Glob, GlobSet, GlobSetBuilder};
use notify::{RecursiveMode, Watcher};
use time::{OffsetDateTime, UtcOffset};
use time::UtcOffset;
use crate::{
jobs::{
check_update::{queue_check_update, CheckUpdateResult},
objdiff::{queue_build, BuildStatus, ObjDiffResult},
Job, JobResult, JobState, JobStatus,
config::{
build_globset, load_project_config, ProjectObject, ProjectObjectNode, CONFIG_FILENAMES,
},
jobs::{objdiff::start_build, Job, JobQueue, JobResult, JobStatus},
views::{
config::config_ui, data_diff::data_diff_ui, function_diff::function_diff_ui, jobs::jobs_ui,
symbol_diff::symbol_diff_ui,
appearance::{appearance_window, Appearance},
config::{config_ui, project_window, ConfigViewState},
data_diff::data_diff_ui,
demangle::{demangle_window, DemangleViewState},
function_diff::function_diff_ui,
jobs::jobs_ui,
symbol_diff::{symbol_diff_ui, DiffViewState, View},
},
};
#[allow(clippy::enum_variant_names)]
#[derive(Default, Eq, PartialEq)]
pub enum View {
#[default]
SymbolDiff,
FunctionDiff,
DataDiff,
}
#[derive(Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum DiffKind {
#[default]
SplitObj,
WholeBinary,
}
#[derive(Default, Clone)]
pub struct DiffConfig {
// TODO
// pub stripped_symbols: Vec<String>,
// pub mapped_symbols: HashMap<String, String>,
}
const DEFAULT_COLOR_ROTATION: [Color32; 9] = [
Color32::from_rgb(255, 0, 255),
Color32::from_rgb(0, 255, 255),
Color32::from_rgb(0, 128, 0),
Color32::from_rgb(255, 0, 0),
Color32::from_rgb(255, 255, 0),
Color32::from_rgb(255, 192, 203),
Color32::from_rgb(0, 0, 255),
Color32::from_rgb(0, 255, 0),
Color32::from_rgb(213, 138, 138),
];
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct ViewConfig {
pub ui_font: FontId,
pub code_font: FontId,
pub diff_colors: Vec<Color32>,
pub reverse_fn_order: bool,
}
impl Default for ViewConfig {
fn default() -> Self {
Self {
ui_font: FontId { size: 12.0, family: FontFamily::Proportional },
code_font: FontId { size: 14.0, family: FontFamily::Monospace },
diff_colors: DEFAULT_COLOR_ROTATION.to_vec(),
reverse_fn_order: false,
}
}
}
pub struct SymbolReference {
pub symbol_name: String,
pub section_name: String,
}
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
#[derive(Default)]
pub struct ViewState {
#[serde(skip)]
pub jobs: Vec<JobState>,
#[serde(skip)]
pub build: Option<Box<ObjDiffResult>>,
#[serde(skip)]
pub highlighted_symbol: Option<String>,
#[serde(skip)]
pub selected_symbol: Option<SymbolReference>,
#[serde(skip)]
pub current_view: View,
#[serde(skip)]
pub show_config: bool,
#[serde(skip)]
pub jobs: JobQueue,
pub config_state: ConfigViewState,
pub demangle_state: DemangleViewState,
pub diff_state: DiffViewState,
pub show_appearance_config: bool,
pub show_demangle: bool,
#[serde(skip)]
pub demangle_text: String,
#[serde(skip)]
pub diff_config: DiffConfig,
#[serde(skip)]
pub search: String,
#[serde(skip)]
pub utc_offset: UtcOffset,
#[serde(skip)]
pub check_update: Option<Box<CheckUpdateResult>>,
// Config
pub diff_kind: DiffKind,
pub view_config: ViewConfig,
}
impl Default for ViewState {
fn default() -> Self {
Self {
jobs: vec![],
build: None,
highlighted_symbol: None,
selected_symbol: None,
current_view: Default::default(),
show_config: false,
show_demangle: false,
demangle_text: String::new(),
diff_config: Default::default(),
search: Default::default(),
utc_offset: UtcOffset::UTC,
check_update: None,
diff_kind: Default::default(),
view_config: Default::default(),
}
}
pub show_project_config: bool,
}
#[derive(Default, Clone, serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct AppConfig {
pub custom_make: Option<String>,
// WSL2 settings
#[serde(skip)]
pub available_wsl_distros: Option<Vec<String>>,
pub selected_wsl_distro: Option<String>,
// Split obj
pub project_dir: Option<PathBuf>,
pub target_obj_dir: Option<PathBuf>,
pub base_obj_dir: Option<PathBuf>,
pub obj_path: Option<String>,
pub build_target: bool,
// Whole binary
pub left_obj: Option<PathBuf>,
pub right_obj: Option<PathBuf>,
#[serde(skip)]
pub project_dir_change: bool,
#[serde(skip)]
pub queue_update_check: bool,
pub watcher_enabled: bool,
pub auto_update_check: bool,
pub watch_patterns: Vec<Glob>,
#[serde(skip)]
pub objects: Vec<ProjectObject>,
#[serde(skip)]
pub object_nodes: Vec<ProjectObjectNode>,
#[serde(skip)]
pub watcher_change: bool,
#[serde(skip)]
pub config_change: bool,
#[serde(skip)]
pub obj_change: bool,
#[serde(skip)]
pub queue_build: bool,
}
#[derive(Default, Clone, serde::Deserialize)]
#[serde(default)]
pub struct ProjectConfig {
pub custom_make: Option<String>,
pub project_dir: Option<PathBuf>,
pub target_obj_dir: Option<PathBuf>,
pub base_obj_dir: Option<PathBuf>,
pub build_target: bool,
}
impl AppConfig {
pub fn set_project_dir(&mut self, path: PathBuf) {
self.project_dir = Some(path);
self.target_obj_dir = None;
self.base_obj_dir = None;
self.obj_path = None;
self.build_target = false;
self.objects.clear();
self.object_nodes.clear();
self.watcher_change = true;
self.config_change = true;
self.obj_change = true;
self.queue_build = false;
}
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct App {
view_state: ViewState,
#[serde(skip)]
config: Arc<RwLock<AppConfig>>,
#[serde(skip)]
modified: Arc<AtomicBool>,
#[serde(skip)]
watcher: Option<notify::RecommendedWatcher>,
#[serde(skip)]
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
#[serde(skip)]
should_relaunch: bool,
}
pub fn set_target_obj_dir(&mut self, path: PathBuf) {
self.target_obj_dir = Some(path);
self.obj_path = None;
self.obj_change = true;
self.queue_build = false;
}
impl Default for App {
fn default() -> Self {
Self {
view_state: ViewState::default(),
config: Arc::new(Default::default()),
modified: Arc::new(Default::default()),
watcher: None,
relaunch_path: Default::default(),
should_relaunch: false,
}
pub fn set_base_obj_dir(&mut self, path: PathBuf) {
self.base_obj_dir = Some(path);
self.obj_path = None;
self.obj_change = true;
self.queue_build = false;
}
pub fn set_obj_path(&mut self, path: String) {
self.obj_path = Some(path);
self.obj_change = true;
self.queue_build = false;
}
}
pub type AppConfigRef = Arc<RwLock<AppConfig>>;
#[derive(Default)]
pub struct App {
appearance: Appearance,
view_state: ViewState,
config: AppConfigRef,
modified: Arc<AtomicBool>,
config_modified: Arc<AtomicBool>,
watcher: Option<notify::RecommendedWatcher>,
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
should_relaunch: bool,
}
const APPEARANCE_KEY: &str = "appearance";
const CONFIG_KEY: &str = "app_config";
impl App {
@@ -217,194 +132,32 @@ impl App {
// Load previous app state (if any).
// Note that you must enable the `persistence` feature for this to work.
let mut app = Self::default();
if let Some(storage) = cc.storage {
let mut app: App = eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
let mut config: AppConfig = eframe::get_value(storage, CONFIG_KEY).unwrap_or_default();
if config.project_dir.is_some() {
config.project_dir_change = true;
if let Some(appearance) = eframe::get_value::<Appearance>(storage, APPEARANCE_KEY) {
app.appearance = appearance;
}
config.queue_update_check = config.auto_update_check;
app.config = Arc::new(RwLock::new(config));
app.view_state.utc_offset = utc_offset;
app.relaunch_path = relaunch_path;
app
} else {
let mut app = Self::default();
app.view_state.utc_offset = utc_offset;
app.relaunch_path = relaunch_path;
app
}
}
}
impl eframe::App for App {
/// Called each time the UI needs repainting, which may be many times per second.
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
if self.should_relaunch {
frame.close();
return;
}
let Self { config, view_state, .. } = self;
{
let config = &view_state.view_config;
let mut style = (*ctx.style()).clone();
style.text_styles.insert(TextStyle::Body, FontId {
size: (config.ui_font.size * 0.75).floor(),
family: config.ui_font.family.clone(),
});
style.text_styles.insert(TextStyle::Body, config.ui_font.clone());
style.text_styles.insert(TextStyle::Button, config.ui_font.clone());
style.text_styles.insert(TextStyle::Heading, FontId {
size: (config.ui_font.size * 1.5).floor(),
family: config.ui_font.family.clone(),
});
style.text_styles.insert(TextStyle::Monospace, config.code_font.clone());
ctx.set_style(style);
}
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
ui.menu_button("File", |ui| {
if ui.button("Show config").clicked() {
view_state.show_config = !view_state.show_config;
}
if ui.button("Quit").clicked() {
frame.close();
}
});
ui.menu_button("Tools", |ui| {
if ui.button("Demangle").clicked() {
view_state.show_demangle = !view_state.show_demangle;
}
});
});
});
if view_state.current_view == View::FunctionDiff
&& matches!(&view_state.build, Some(b) if b.first_status.success && b.second_status.success)
{
// egui::SidePanel::left("side_panel").show(ctx, |ui| {
// if ui.button("Back").clicked() {
// view_state.current_view = View::SymbolDiff;
// }
// ui.separator();
// jobs_ui(ui, view_state);
// });
egui::CentralPanel::default().show(ctx, |ui| {
if function_diff_ui(ui, view_state) {
view_state
.jobs
.push(queue_build(config.clone(), view_state.diff_config.clone()));
if let Some(mut config) = eframe::get_value::<AppConfig>(storage, CONFIG_KEY) {
if config.project_dir.is_some() {
config.config_change = true;
config.watcher_change = true;
app.modified.store(true, Ordering::Relaxed);
}
});
} else if view_state.current_view == View::DataDiff
&& matches!(&view_state.build, Some(b) if b.first_status.success && b.second_status.success)
{
egui::CentralPanel::default().show(ctx, |ui| {
if data_diff_ui(ui, view_state) {
view_state
.jobs
.push(queue_build(config.clone(), view_state.diff_config.clone()));
}
});
} else {
egui::SidePanel::left("side_panel").show(ctx, |ui| {
config_ui(ui, config, view_state);
jobs_ui(ui, view_state);
});
egui::CentralPanel::default().show(ctx, |ui| {
symbol_diff_ui(ui, view_state);
});
}
egui::Window::new("Config").open(&mut view_state.show_config).show(ctx, |ui| {
ui.label("UI font:");
egui::introspection::font_id_ui(ui, &mut view_state.view_config.ui_font);
ui.separator();
ui.label("Code font:");
egui::introspection::font_id_ui(ui, &mut view_state.view_config.code_font);
ui.separator();
ui.label("Diff colors:");
if ui.button("Reset").clicked() {
view_state.view_config.diff_colors = DEFAULT_COLOR_ROTATION.to_vec();
}
let mut remove_at: Option<usize> = None;
let num_colors = view_state.view_config.diff_colors.len();
for (idx, color) in view_state.view_config.diff_colors.iter_mut().enumerate() {
ui.horizontal(|ui| {
ui.color_edit_button_srgba(color);
if num_colors > 1 && ui.small_button("-").clicked() {
remove_at = Some(idx);
}
});
}
if let Some(idx) = remove_at {
view_state.view_config.diff_colors.remove(idx);
}
if ui.small_button("+").clicked() {
view_state.view_config.diff_colors.push(Color32::BLACK);
}
});
egui::Window::new("Demangle").open(&mut view_state.show_demangle).show(ctx, |ui| {
ui.text_edit_singleline(&mut view_state.demangle_text);
ui.add_space(10.0);
if let Some(demangled) =
cwdemangle::demangle(&view_state.demangle_text, &Default::default())
{
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
ui.colored_label(Color32::LIGHT_BLUE, &demangled);
});
if ui.button("Copy").clicked() {
ui.output_mut(|output| output.copied_text = demangled);
}
} else {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
ui.colored_label(Color32::LIGHT_RED, "[invalid]");
});
}
});
// Windows + request_repaint_after breaks dialogs:
// https://github.com/emilk/egui/issues/2003
if cfg!(windows)
|| view_state.jobs.iter().any(|job| {
if let Some(handle) = &job.handle {
return !handle.is_finished();
}
false
})
{
ctx.request_repaint();
} else {
ctx.request_repaint_after(Duration::from_millis(100));
app.view_state.config_state.queue_check_update = config.auto_update_check;
app.config = Arc::new(RwLock::new(config));
}
}
app.appearance.utc_offset = utc_offset;
app.relaunch_path = relaunch_path;
app
}
/// Called by the frame work to save state before shutdown.
fn save(&mut self, storage: &mut dyn eframe::Storage) {
if let Ok(config) = self.config.read() {
eframe::set_value(storage, CONFIG_KEY, &*config);
}
eframe::set_value(storage, eframe::APP_KEY, self);
}
fn pre_update(&mut self) {
let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state;
fn post_rendering(&mut self, _window_size_px: [u32; 2], _frame: &eframe::Frame) {
for job in &mut self.view_state.jobs {
let Some(handle) = &job.handle else {
continue;
};
if !handle.is_finished() {
continue;
}
match job.handle.take().unwrap().join() {
let mut results = vec![];
for (job, result) in jobs.iter_finished() {
match result {
Ok(result) => {
log::info!("Job {} finished", job.id);
match result {
@@ -413,27 +166,13 @@ impl eframe::App for App {
log::error!("{:?}", err);
}
}
JobResult::ObjDiff(state) => {
self.view_state.build = Some(state);
}
JobResult::BinDiff(state) => {
self.view_state.build = Some(Box::new(ObjDiffResult {
first_status: BuildStatus { success: true, log: "".to_string() },
second_status: BuildStatus { success: true, log: "".to_string() },
first_obj: Some(state.first_obj),
second_obj: Some(state.second_obj),
time: OffsetDateTime::now_utc(),
}));
}
JobResult::CheckUpdate(state) => {
self.view_state.check_update = Some(state);
}
JobResult::Update(state) => {
if let Ok(mut guard) = self.relaunch_path.lock() {
*guard = Some(state.exe_path);
}
self.should_relaunch = true;
}
_ => results.push(result),
}
}
Err(err) => {
@@ -460,79 +199,201 @@ impl eframe::App for App {
}
}
}
if self.view_state.jobs.iter().any(|v| v.should_remove) {
let mut i = 0;
while i < self.view_state.jobs.len() {
let job = &self.view_state.jobs[i];
if job.should_remove
&& job.handle.is_none()
&& job.status.read().unwrap().error.is_none()
{
self.view_state.jobs.remove(i);
} else {
i += 1;
jobs.results.append(&mut results);
jobs.clear_finished();
diff_state.pre_update(jobs, &self.config);
config_state.pre_update(jobs);
debug_assert!(jobs.results.is_empty());
}
fn post_update(&mut self) {
let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state;
config_state.post_update(jobs, &self.config);
diff_state.post_update(jobs, &self.config);
let Ok(mut config) = self.config.write() else {
return;
};
let config = &mut *config;
if self.config_modified.swap(false, Ordering::Relaxed) {
config.config_change = true;
}
if config.config_change {
config.config_change = false;
match load_project_config(config) {
Ok(()) => config_state.load_error = None,
Err(e) => {
log::error!("Failed to load project config: {e}");
config_state.load_error = Some(format!("{e}"));
}
}
}
if let Ok(mut config) = self.config.write() {
if config.project_dir_change {
drop(self.watcher.take());
if let Some(project_dir) = &config.project_dir {
match create_watcher(self.modified.clone(), project_dir) {
if config.watcher_change {
drop(self.watcher.take());
if let Some(project_dir) = &config.project_dir {
if !config.watch_patterns.is_empty() {
match build_globset(&config.watch_patterns)
.map_err(anyhow::Error::new)
.and_then(|globset| {
create_watcher(
self.modified.clone(),
self.config_modified.clone(),
project_dir,
globset,
)
.map_err(anyhow::Error::new)
}) {
Ok(watcher) => self.watcher = Some(watcher),
Err(e) => eprintln!("Failed to create watcher: {e}"),
Err(e) => log::error!("Failed to create watcher: {e}"),
}
config.project_dir_change = false;
self.modified.store(true, Ordering::Relaxed);
}
}
if config.obj_path.is_some() && self.modified.load(Ordering::Relaxed) {
if !self
.view_state
.jobs
.iter()
.any(|j| j.job_type == Job::ObjDiff && j.handle.is_some())
{
self.view_state.jobs.push(queue_build(
self.config.clone(),
self.view_state.diff_config.clone(),
));
}
self.modified.store(false, Ordering::Relaxed);
}
if config.queue_update_check {
self.view_state.jobs.push(queue_check_update());
config.queue_update_check = false;
config.watcher_change = false;
}
}
if config.obj_change {
*diff_state = Default::default();
if config.obj_path.is_some() {
config.queue_build = true;
}
config.obj_change = false;
}
if self.modified.swap(false, Ordering::Relaxed) {
config.queue_build = true;
}
// Don't clear `queue_build` if a build is running. A file may have been modified during
// the build, so we'll start another build after the current one finishes.
if config.queue_build && config.obj_path.is_some() && !jobs.is_running(Job::ObjDiff) {
jobs.push(start_build(self.config.clone()));
config.queue_build = false;
}
}
}
impl eframe::App for App {
/// Called each time the UI needs repainting, which may be many times per second.
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
if self.should_relaunch {
frame.close();
return;
}
self.pre_update();
let Self { config, appearance, view_state, .. } = self;
ctx.set_style(appearance.apply(ctx.style().as_ref()));
let ViewState {
jobs,
show_appearance_config,
demangle_state,
show_demangle,
diff_state,
config_state,
show_project_config,
} = view_state;
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
ui.menu_button("File", |ui| {
if ui.button("Appearance…").clicked() {
*show_appearance_config = !*show_appearance_config;
ui.close_menu();
}
if ui.button("Quit").clicked() {
frame.close();
}
});
ui.menu_button("Tools", |ui| {
if ui.button("Demangle…").clicked() {
*show_demangle = !*show_demangle;
ui.close_menu();
}
});
});
});
let build_success = matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success);
if diff_state.current_view == View::FunctionDiff && build_success {
egui::CentralPanel::default().show(ctx, |ui| {
function_diff_ui(ui, diff_state, appearance);
});
} else if diff_state.current_view == View::DataDiff && build_success {
egui::CentralPanel::default().show(ctx, |ui| {
data_diff_ui(ui, diff_state, appearance);
});
} else {
egui::SidePanel::left("side_panel").show(ctx, |ui| {
egui::ScrollArea::both().show(ui, |ui| {
config_ui(ui, config, show_project_config, config_state, appearance);
jobs_ui(ui, jobs, appearance);
});
});
egui::CentralPanel::default().show(ctx, |ui| {
symbol_diff_ui(ui, diff_state, appearance);
});
}
project_window(ctx, config, show_project_config, config_state, appearance);
appearance_window(ctx, show_appearance_config, appearance);
demangle_window(ctx, show_demangle, demangle_state, appearance);
self.post_update();
// Windows + request_repaint_after breaks dialogs:
// https://github.com/emilk/egui/issues/2003
if cfg!(windows) || self.view_state.jobs.any_running() {
ctx.request_repaint();
} else {
ctx.request_repaint_after(Duration::from_millis(100));
}
}
/// Called by the frame work to save state before shutdown.
fn save(&mut self, storage: &mut dyn eframe::Storage) {
if let Ok(config) = self.config.read() {
eframe::set_value(storage, CONFIG_KEY, &*config);
}
eframe::set_value(storage, APPEARANCE_KEY, &self.appearance);
}
}
fn create_watcher(
modified: Arc<AtomicBool>,
config_modified: Arc<AtomicBool>,
project_dir: &Path,
patterns: GlobSet,
) -> notify::Result<notify::RecommendedWatcher> {
let mut config_patterns = GlobSetBuilder::new();
for filename in CONFIG_FILENAMES {
config_patterns.add(Glob::new(&format!("**/{filename}")).unwrap());
}
let config_patterns = config_patterns.build().unwrap();
let mut watcher =
notify::recommended_watcher(move |res: notify::Result<notify::Event>| match res {
Ok(event) => {
if matches!(event.kind, notify::EventKind::Modify(..)) {
let watch_extensions = &[
Some(OsStr::new("c")),
Some(OsStr::new("cp")),
Some(OsStr::new("cpp")),
Some(OsStr::new("h")),
Some(OsStr::new("hpp")),
Some(OsStr::new("s")),
];
if event.paths.iter().any(|p| watch_extensions.contains(&p.extension())) {
modified.store(true, Ordering::Relaxed);
for path in &event.paths {
if config_patterns.is_match(path) {
config_modified.store(true, Ordering::Relaxed);
}
if patterns.is_match(path) {
modified.store(true, Ordering::Relaxed);
}
}
}
}
Err(e) => println!("watch error: {e:?}"),
Err(e) => log::error!("watch error: {e:?}"),
})?;
watcher.watch(project_dir, RecursiveMode::Recursive)?;
Ok(watcher)

126
src/config.rs Normal file
View File

@@ -0,0 +1,126 @@
use std::{
fs::File,
path::{Component, Path, PathBuf},
};
use anyhow::{Context, Result};
use globset::{Glob, GlobSet, GlobSetBuilder};
use crate::app::AppConfig;
#[derive(Default, Clone, serde::Deserialize)]
#[serde(default)]
pub struct ProjectConfig {
pub custom_make: Option<String>,
pub target_dir: Option<PathBuf>,
pub base_dir: Option<PathBuf>,
pub build_target: bool,
pub watch_patterns: Vec<Glob>,
#[serde(alias = "units")]
pub objects: Vec<ProjectObject>,
}
#[derive(Default, Clone, serde::Deserialize)]
pub struct ProjectObject {
pub name: Option<String>,
pub path: PathBuf,
pub reverse_fn_order: Option<bool>,
}
#[derive(Clone)]
pub enum ProjectObjectNode {
File(String, ProjectObject),
Dir(String, Vec<ProjectObjectNode>),
}
fn find_dir<'a>(
name: &str,
nodes: &'a mut Vec<ProjectObjectNode>,
) -> &'a mut Vec<ProjectObjectNode> {
if let Some(index) = nodes
.iter()
.position(|node| matches!(node, ProjectObjectNode::Dir(dir_name, _) if dir_name == name))
{
if let ProjectObjectNode::Dir(_, children) = &mut nodes[index] {
return children;
}
} else {
nodes.push(ProjectObjectNode::Dir(name.to_string(), vec![]));
if let Some(ProjectObjectNode::Dir(_, children)) = nodes.last_mut() {
return children;
}
}
unreachable!();
}
fn build_nodes(objects: &[ProjectObject]) -> Vec<ProjectObjectNode> {
let mut nodes = vec![];
for object in objects {
let mut out_nodes = &mut nodes;
let path = object.name.as_ref().map(Path::new).unwrap_or(&object.path);
if let Some(parent) = path.parent() {
for component in parent.components() {
if let Component::Normal(name) = component {
let name = name.to_str().unwrap();
out_nodes = find_dir(name, out_nodes);
}
}
}
let filename = path.file_name().unwrap().to_str().unwrap().to_string();
out_nodes.push(ProjectObjectNode::File(filename, object.clone()));
}
nodes
}
pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.yml", "objdiff.yaml", "objdiff.json"];
pub fn load_project_config(config: &mut AppConfig) -> Result<()> {
let Some(project_dir) = &config.project_dir else {
return Ok(());
};
if let Some(result) = try_project_config(project_dir) {
let project_config = result?;
config.custom_make = project_config.custom_make;
config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p));
config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p));
config.build_target = project_config.build_target;
config.watch_patterns = project_config.watch_patterns;
config.watcher_change = true;
config.objects = project_config.objects;
config.object_nodes = build_nodes(&config.objects);
}
Ok(())
}
fn try_project_config(dir: &Path) -> Option<Result<ProjectConfig>> {
for filename in CONFIG_FILENAMES.iter() {
let config_path = dir.join(filename);
if config_path.is_file() {
return match filename.contains("json") {
true => Some(read_json_config(&config_path)),
false => Some(read_yml_config(&config_path)),
};
}
}
None
}
fn read_yml_config(config_path: &Path) -> Result<ProjectConfig> {
let mut reader = File::open(config_path)
.with_context(|| format!("Failed to open config file '{}'", config_path.display()))?;
Ok(serde_yaml::from_reader(&mut reader)?)
}
fn read_json_config(config_path: &Path) -> Result<ProjectConfig> {
let mut reader = File::open(config_path)
.with_context(|| format!("Failed to open config file '{}'", config_path.display()))?;
Ok(serde_json::from_reader(&mut reader)?)
}
pub fn build_globset(vec: &[Glob]) -> std::result::Result<GlobSet, globset::Error> {
let mut builder = GlobSetBuilder::new();
for glob in vec {
builder.add(glob.clone());
}
builder.build()
}

View File

@@ -3,7 +3,6 @@ use std::{collections::BTreeMap, mem::take};
use anyhow::Result;
use crate::{
app::DiffConfig,
editops::{editops_find, LevEditType},
obj::{
mips, ppc, ObjArchitecture, ObjDataDiff, ObjDataDiffKind, ObjInfo, ObjInsArg,
@@ -373,7 +372,7 @@ fn find_section_and_symbol(obj: &ObjInfo, name: &str) -> Option<(usize, usize)>
None
}
pub fn diff_objs(left: &mut ObjInfo, right: &mut ObjInfo, _diff_config: &DiffConfig) -> Result<()> {
pub fn diff_objs(left: &mut ObjInfo, right: &mut ObjInfo) -> Result<()> {
for left_section in &mut left.sections {
if left_section.kind == ObjSectionKind::Code {
for left_symbol in &mut left_section.symbols {
@@ -406,7 +405,9 @@ pub fn diff_objs(left: &mut ObjInfo, right: &mut ObjInfo, _diff_config: &DiffCon
}
}
} else {
let Some(right_section) = right.sections.iter_mut().find(|s| s.name == left_section.name) else {
let Some(right_section) =
right.sections.iter_mut().find(|s| s.name == left_section.name)
else {
continue;
};
if left_section.kind == ObjSectionKind::Data {

View File

@@ -1,44 +0,0 @@
use std::sync::{mpsc::Receiver, Arc, RwLock};
use anyhow::{Error, Result};
use crate::{
app::{AppConfig, DiffConfig},
diff::diff_objs,
jobs::{queue_job, update_status, Job, JobResult, JobState, Status},
obj::{elf, ObjInfo},
};
pub struct BinDiffResult {
pub first_obj: ObjInfo,
pub second_obj: ObjInfo,
}
fn run_build(
status: &Status,
cancel: Receiver<()>,
config: Arc<RwLock<AppConfig>>,
) -> Result<Box<BinDiffResult>> {
let config = config.read().map_err(|_| Error::msg("Failed to lock app config"))?.clone();
let target_path =
config.left_obj.as_ref().ok_or_else(|| Error::msg("Missing target obj path"))?;
let base_path = config.right_obj.as_ref().ok_or_else(|| Error::msg("Missing base obj path"))?;
update_status(status, "Loading target obj".to_string(), 0, 3, &cancel)?;
let mut left_obj = elf::read(target_path)?;
update_status(status, "Loading base obj".to_string(), 1, 3, &cancel)?;
let mut right_obj = elf::read(base_path)?;
update_status(status, "Performing diff".to_string(), 2, 3, &cancel)?;
diff_objs(&mut left_obj, &mut right_obj, &DiffConfig::default() /* TODO */)?;
update_status(status, "Complete".to_string(), 3, 3, &cancel)?;
Ok(Box::new(BinDiffResult { first_obj: left_obj, second_obj: right_obj }))
}
pub fn queue_bindiff(config: Arc<RwLock<AppConfig>>) -> JobState {
queue_job("Binary diff", Job::BinDiff, move |status, cancel| {
run_build(status, cancel, config).map(JobResult::BinDiff)
})
}

View File

@@ -4,7 +4,7 @@ use anyhow::{Context, Result};
use self_update::{cargo_crate_version, update::Release};
use crate::{
jobs::{queue_job, update_status, Job, JobResult, JobState, Status},
jobs::{start_job, update_status, Job, JobResult, JobState, JobStatusRef},
update::{build_updater, BIN_NAME},
};
@@ -14,7 +14,7 @@ pub struct CheckUpdateResult {
pub found_binary: bool,
}
fn run_check_update(status: &Status, cancel: Receiver<()>) -> Result<Box<CheckUpdateResult>> {
fn run_check_update(status: &JobStatusRef, cancel: Receiver<()>) -> Result<Box<CheckUpdateResult>> {
update_status(status, "Fetching latest release".to_string(), 0, 1, &cancel)?;
let updater = build_updater().context("Failed to create release updater")?;
let latest_release = updater.get_latest_release()?;
@@ -26,8 +26,8 @@ fn run_check_update(status: &Status, cancel: Receiver<()>) -> Result<Box<CheckUp
Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary }))
}
pub fn queue_check_update() -> JobState {
queue_job("Check for updates", Job::CheckUpdate, move |status, cancel| {
run_check_update(status, cancel).map(JobResult::CheckUpdate)
pub fn start_check_update() -> JobState {
start_job("Check for updates", Job::CheckUpdate, move |status, cancel| {
run_check_update(status, cancel).map(|result| JobResult::CheckUpdate(Some(result)))
})
}

View File

@@ -9,12 +9,8 @@ use std::{
use anyhow::Result;
use crate::jobs::{
bindiff::BinDiffResult, check_update::CheckUpdateResult, objdiff::ObjDiffResult,
update::UpdateResult,
};
use crate::jobs::{check_update::CheckUpdateResult, objdiff::ObjDiffResult, update::UpdateResult};
pub mod bindiff;
pub mod check_update;
pub mod objdiff;
pub mod update;
@@ -22,19 +18,88 @@ pub mod update;
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum Job {
ObjDiff,
BinDiff,
CheckUpdate,
Update,
}
pub static JOB_ID: AtomicUsize = AtomicUsize::new(0);
#[derive(Default)]
pub struct JobQueue {
pub jobs: Vec<JobState>,
pub results: Vec<JobResult>,
}
impl JobQueue {
/// Adds a job to the queue.
#[inline]
pub fn push(&mut self, state: JobState) { self.jobs.push(state); }
/// Adds a job to the queue if a job of the given kind is not already running.
#[inline]
pub fn push_once(&mut self, job: Job, func: impl FnOnce() -> JobState) {
if !self.is_running(job) {
self.push(func());
}
}
/// Returns whether a job of the given kind is running.
pub fn is_running(&self, kind: Job) -> bool {
self.jobs.iter().any(|j| j.kind == kind && j.handle.is_some())
}
/// Returns whether any job is running.
pub fn any_running(&self) -> bool {
self.jobs.iter().any(|job| {
if let Some(handle) = &job.handle {
return !handle.is_finished();
}
false
})
}
/// Iterates over all jobs mutably.
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut JobState> + '_ { self.jobs.iter_mut() }
/// Iterates over all finished jobs, returning the job state and the result.
pub fn iter_finished(
&mut self,
) -> impl Iterator<Item = (&mut JobState, std::thread::Result<JobResult>)> + '_ {
self.jobs.iter_mut().filter_map(|job| {
if let Some(handle) = &job.handle {
if !handle.is_finished() {
return None;
}
let result = job.handle.take().unwrap().join();
return Some((job, result));
}
None
})
}
/// Clears all finished jobs.
pub fn clear_finished(&mut self) {
self.jobs.retain(|job| {
!(job.should_remove
&& job.handle.is_none()
&& job.status.read().unwrap().error.is_none())
});
}
/// Removes a job from the queue given its ID.
pub fn remove(&mut self, id: usize) { self.jobs.retain(|job| job.id != id); }
}
pub type JobStatusRef = Arc<RwLock<JobStatus>>;
pub struct JobState {
pub id: usize,
pub job_type: Job,
pub kind: Job,
pub handle: Option<JoinHandle<JobResult>>,
pub status: Arc<RwLock<JobStatus>>,
pub status: JobStatusRef,
pub cancel: Sender<()>,
pub should_remove: bool,
}
#[derive(Default)]
pub struct JobStatus {
pub title: String,
@@ -43,11 +108,11 @@ pub struct JobStatus {
pub status: String,
pub error: Option<anyhow::Error>,
}
pub enum JobResult {
None,
ObjDiff(Box<ObjDiffResult>),
BinDiff(Box<BinDiffResult>),
CheckUpdate(Box<CheckUpdateResult>),
ObjDiff(Option<Box<ObjDiffResult>>),
CheckUpdate(Option<Box<CheckUpdateResult>>),
Update(Box<UpdateResult>),
}
@@ -58,12 +123,10 @@ fn should_cancel(rx: &Receiver<()>) -> bool {
}
}
type Status = Arc<RwLock<JobStatus>>;
fn queue_job(
fn start_job(
title: &str,
job_type: Job,
run: impl FnOnce(&Status, Receiver<()>) -> Result<JobResult> + Send + 'static,
kind: Job,
run: impl FnOnce(&JobStatusRef, Receiver<()>) -> Result<JobResult> + Send + 'static,
) -> JobState {
let status = Arc::new(RwLock::new(JobStatus {
title: title.to_string(),
@@ -89,7 +152,7 @@ fn queue_job(
log::info!("Started job {}", id);
JobState {
id,
job_type,
kind,
handle: Some(handle),
status: status_clone,
cancel: tx,
@@ -98,7 +161,7 @@ fn queue_job(
}
fn update_status(
status: &Status,
status: &JobStatusRef,
str: String,
count: u32,
total: u32,

View File

@@ -1,17 +1,12 @@
use std::{
path::Path,
process::Command,
str::from_utf8,
sync::{mpsc::Receiver, Arc, RwLock},
};
use std::{path::Path, process::Command, str::from_utf8, sync::mpsc::Receiver};
use anyhow::{Context, Error, Result};
use time::OffsetDateTime;
use crate::{
app::{AppConfig, DiffConfig},
app::{AppConfig, AppConfigRef},
diff::diff_objs,
jobs::{queue_job, update_status, Job, JobResult, JobState, Status},
jobs::{start_job, update_status, Job, JobResult, JobState, JobStatusRef},
obj::{elf, ObjInfo},
};
@@ -76,10 +71,9 @@ fn run_make(cwd: &Path, arg: &Path, config: &AppConfig) -> BuildStatus {
}
fn run_build(
status: &Status,
status: &JobStatusRef,
cancel: Receiver<()>,
config: Arc<RwLock<AppConfig>>,
diff_config: DiffConfig,
config: AppConfigRef,
) -> Result<Box<ObjDiffResult>> {
let config = config.read().map_err(|_| Error::msg("Failed to lock app config"))?.clone();
let obj_path = config.obj_path.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?;
@@ -129,15 +123,15 @@ fn run_build(
if let (Some(first_obj), Some(second_obj)) = (&mut first_obj, &mut second_obj) {
update_status(status, "Performing diff".to_string(), 4, total, &cancel)?;
diff_objs(first_obj, second_obj, &diff_config)?;
diff_objs(first_obj, second_obj)?;
}
update_status(status, "Complete".to_string(), total, total, &cancel)?;
Ok(Box::new(ObjDiffResult { first_status, second_status, first_obj, second_obj, time }))
}
pub fn queue_build(config: Arc<RwLock<AppConfig>>, diff_config: DiffConfig) -> JobState {
queue_job("Object diff", Job::ObjDiff, move |status, cancel| {
run_build(status, cancel, config, diff_config).map(JobResult::ObjDiff)
pub fn start_build(config: AppConfigRef) -> JobState {
start_job("Object diff", Job::ObjDiff, move |status, cancel| {
run_build(status, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
})
}

View File

@@ -9,7 +9,7 @@ use anyhow::{Context, Result};
use const_format::formatcp;
use crate::{
jobs::{queue_job, update_status, Job, JobResult, JobState, Status},
jobs::{start_job, update_status, Job, JobResult, JobState, JobStatusRef},
update::{build_updater, BIN_NAME},
};
@@ -17,7 +17,7 @@ pub struct UpdateResult {
pub exe_path: PathBuf,
}
fn run_update(status: &Status, cancel: Receiver<()>) -> Result<Box<UpdateResult>> {
fn run_update(status: &JobStatusRef, cancel: Receiver<()>) -> Result<Box<UpdateResult>> {
update_status(status, "Fetching latest release".to_string(), 0, 3, &cancel)?;
let updater = build_updater().context("Failed to create release updater")?;
let latest_release = updater.get_latest_release()?;
@@ -53,8 +53,8 @@ fn run_update(status: &Status, cancel: Receiver<()>) -> Result<Box<UpdateResult>
Ok(Box::from(UpdateResult { exe_path: target_file }))
}
pub fn queue_update() -> JobState {
queue_job("Update app", Job::Update, move |status, cancel| {
pub fn start_update() -> JobState {
start_job("Update app", Job::Update, move |status, cancel| {
run_update(status, cancel).map(JobResult::Update)
})
}

View File

@@ -3,6 +3,7 @@
pub use app::App;
mod app;
mod config;
mod diff;
mod editops;
mod jobs;

View File

@@ -37,7 +37,8 @@ fn main() {
let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None));
let exec_path_clone = exec_path.clone();
let mut native_options = eframe::NativeOptions::default();
let mut native_options =
eframe::NativeOptions { follow_system_theme: false, ..Default::default() };
match load_icon() {
Ok(data) => {
native_options.icon_data = Some(data);
@@ -65,7 +66,7 @@ fn main() {
let result = exec::Command::new(path)
.args(&std::env::args().collect::<Vec<String>>())
.exec();
eprintln!("Failed to relaunch: {result:?}");
log::error!("Failed to relaunch: {result:?}");
} else {
let result = std::process::Command::new(path)
.args(std::env::args())
@@ -73,7 +74,7 @@ fn main() {
.unwrap()
.wait();
if let Err(e) = result {
eprintln!("Failed to relaunch: {:?}", e);
log::error!("Failed to relaunch: {:?}", e);
}
}
}

View File

@@ -26,7 +26,7 @@ fn to_obj_section_kind(kind: SectionKind) -> Option<ObjSectionKind> {
fn to_obj_symbol(obj_file: &File<'_>, symbol: &Symbol<'_, '_>, addend: i64) -> Result<ObjSymbol> {
let mut name = symbol.name().context("Failed to process symbol name")?;
if name.is_empty() {
println!("Found empty sym: {symbol:?}");
log::warn!("Found empty sym: {symbol:?}");
name = "?";
}
let mut flags = ObjSymbolFlagSet(ObjSymbolFlags::none());
@@ -181,7 +181,7 @@ fn find_section_symbol(
fn relocations_by_section(
arch: ObjArchitecture,
obj_file: &File<'_>,
section: &mut ObjSection,
section: &ObjSection,
) -> Result<Vec<ObjReloc>> {
let obj_section = obj_file.section_by_index(SectionIndex(section.index))?;
let mut relocations = Vec::<ObjReloc>::new();
@@ -294,7 +294,7 @@ fn line_info(obj_file: &File<'_>) -> Result<Option<BTreeMap<u32, u32>>> {
let address_delta = reader.read_u32::<BigEndian>()?;
map.insert(base_address + address_delta, line_number);
}
println!("Line info: {map:#X?}");
log::debug!("Line info: {map:#X?}");
return Ok(Some(map));
}
Ok(None)

139
src/views/appearance.rs Normal file
View File

@@ -0,0 +1,139 @@
use egui::{Color32, FontFamily, FontId, TextStyle};
use time::UtcOffset;
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct Appearance {
pub ui_font: FontId,
pub code_font: FontId,
pub diff_colors: Vec<Color32>,
pub theme: eframe::Theme,
// Applied by theme
#[serde(skip)]
pub text_color: Color32, // GRAY
#[serde(skip)]
pub emphasized_text_color: Color32, // LIGHT_GRAY
#[serde(skip)]
pub deemphasized_text_color: Color32, // DARK_GRAY
#[serde(skip)]
pub highlight_color: Color32, // WHITE
#[serde(skip)]
pub replace_color: Color32, // LIGHT_BLUE
#[serde(skip)]
pub insert_color: Color32, // GREEN
#[serde(skip)]
pub delete_color: Color32, // RED
// Global
#[serde(skip)]
pub utc_offset: UtcOffset,
}
impl Default for Appearance {
fn default() -> Self {
Self {
ui_font: FontId { size: 12.0, family: FontFamily::Proportional },
code_font: FontId { size: 14.0, family: FontFamily::Monospace },
diff_colors: DEFAULT_COLOR_ROTATION.to_vec(),
theme: eframe::Theme::Dark,
text_color: Color32::GRAY,
emphasized_text_color: Color32::LIGHT_GRAY,
deemphasized_text_color: Color32::DARK_GRAY,
highlight_color: Color32::WHITE,
replace_color: Color32::LIGHT_BLUE,
insert_color: Color32::GREEN,
delete_color: Color32::from_rgb(200, 40, 41),
utc_offset: UtcOffset::UTC,
}
}
}
impl Appearance {
pub fn apply(&mut self, style: &egui::Style) -> egui::Style {
let mut style = style.clone();
style.text_styles.insert(TextStyle::Body, FontId {
size: (self.ui_font.size * 0.75).floor(),
family: self.ui_font.family.clone(),
});
style.text_styles.insert(TextStyle::Body, self.ui_font.clone());
style.text_styles.insert(TextStyle::Button, self.ui_font.clone());
style.text_styles.insert(TextStyle::Heading, FontId {
size: (self.ui_font.size * 1.5).floor(),
family: self.ui_font.family.clone(),
});
style.text_styles.insert(TextStyle::Monospace, self.code_font.clone());
match self.theme {
eframe::Theme::Dark => {
style.visuals = egui::Visuals::dark();
self.text_color = Color32::GRAY;
self.emphasized_text_color = Color32::LIGHT_GRAY;
self.deemphasized_text_color = Color32::DARK_GRAY;
self.highlight_color = Color32::WHITE;
self.replace_color = Color32::LIGHT_BLUE;
self.insert_color = Color32::GREEN;
self.delete_color = Color32::from_rgb(200, 40, 41);
}
eframe::Theme::Light => {
style.visuals = egui::Visuals::light();
self.text_color = Color32::GRAY;
self.emphasized_text_color = Color32::DARK_GRAY;
self.deemphasized_text_color = Color32::LIGHT_GRAY;
self.highlight_color = Color32::BLACK;
self.replace_color = Color32::DARK_BLUE;
self.insert_color = Color32::DARK_GREEN;
self.delete_color = Color32::from_rgb(200, 40, 41);
}
}
style
}
}
pub const DEFAULT_COLOR_ROTATION: [Color32; 9] = [
Color32::from_rgb(255, 0, 255),
Color32::from_rgb(0, 255, 255),
Color32::from_rgb(0, 128, 0),
Color32::from_rgb(255, 0, 0),
Color32::from_rgb(255, 255, 0),
Color32::from_rgb(255, 192, 203),
Color32::from_rgb(0, 0, 255),
Color32::from_rgb(0, 255, 0),
Color32::from_rgb(213, 138, 138),
];
pub fn appearance_window(ctx: &egui::Context, show: &mut bool, appearance: &mut Appearance) {
egui::Window::new("Appearance").open(show).show(ctx, |ui| {
egui::ComboBox::from_label("Theme")
.selected_text(format!("{:?}", appearance.theme))
.show_ui(ui, |ui| {
ui.selectable_value(&mut appearance.theme, eframe::Theme::Dark, "Dark");
ui.selectable_value(&mut appearance.theme, eframe::Theme::Light, "Light");
});
ui.label("UI font:");
egui::introspection::font_id_ui(ui, &mut appearance.ui_font);
ui.separator();
ui.label("Code font:");
egui::introspection::font_id_ui(ui, &mut appearance.code_font);
ui.separator();
ui.label("Diff colors:");
if ui.button("Reset").clicked() {
appearance.diff_colors = DEFAULT_COLOR_ROTATION.to_vec();
}
let mut remove_at: Option<usize> = None;
let num_colors = appearance.diff_colors.len();
for (idx, color) in appearance.diff_colors.iter_mut().enumerate() {
ui.horizontal(|ui| {
ui.color_edit_button_srgba(color);
if num_colors > 1 && ui.small_button("-").clicked() {
remove_at = Some(idx);
}
});
}
if let Some(idx) = remove_at {
appearance.diff_colors.remove(idx);
}
if ui.small_button("+").clicked() {
appearance.diff_colors.push(Color32::BLACK);
}
});
}

View File

@@ -1,19 +1,89 @@
#[cfg(windows)]
use std::string::FromUtf16Error;
use std::sync::{Arc, RwLock};
use std::{
borrow::Cow,
mem::take,
path::{PathBuf, MAIN_SEPARATOR},
};
#[cfg(windows)]
use anyhow::{Context, Result};
use const_format::formatcp;
use egui::{output::OpenUrl, Color32};
use egui::{
output::OpenUrl, text::LayoutJob, CollapsingHeader, FontFamily, FontId, RichText,
SelectableLabel, TextFormat, Widget,
};
use globset::Glob;
use self_update::cargo_crate_version;
use crate::{
app::{AppConfig, DiffKind, ViewState},
jobs::{bindiff::queue_bindiff, objdiff::queue_build, update::queue_update},
app::{AppConfig, AppConfigRef},
config::{ProjectObject, ProjectObjectNode},
jobs::{
check_update::{start_check_update, CheckUpdateResult},
update::start_update,
Job, JobQueue, JobResult,
},
update::RELEASE_URL,
views::appearance::Appearance,
};
#[derive(Default)]
pub struct ConfigViewState {
pub check_update: Option<Box<CheckUpdateResult>>,
pub check_update_running: bool,
pub queue_check_update: bool,
pub update_running: bool,
pub queue_update: bool,
pub build_running: bool,
pub queue_build: bool,
pub watch_pattern_text: String,
pub load_error: Option<String>,
pub object_search: String,
#[cfg(windows)]
pub available_wsl_distros: Option<Vec<String>>,
}
impl ConfigViewState {
pub fn pre_update(&mut self, jobs: &mut JobQueue) {
jobs.results.retain_mut(|result| {
if let JobResult::CheckUpdate(result) = result {
self.check_update = take(result);
false
} else {
true
}
});
self.build_running = jobs.is_running(Job::ObjDiff);
self.check_update_running = jobs.is_running(Job::CheckUpdate);
self.update_running = jobs.is_running(Job::Update);
}
pub fn post_update(&mut self, jobs: &mut JobQueue, config: &AppConfigRef) {
if self.queue_build {
self.queue_build = false;
if let Ok(mut config) = config.write() {
config.queue_build = true;
}
}
if self.queue_check_update {
self.queue_check_update = false;
jobs.push_once(Job::CheckUpdate, start_check_update);
}
if self.queue_update {
self.queue_update = false;
jobs.push_once(Job::Update, start_update);
}
}
}
const DEFAULT_WATCH_PATTERNS: &[&str] = &[
"*.c", "*.cp", "*.cpp", "*.cxx", "*.h", "*.hp", "*.hpp", "*.hxx", "*.s", "*.S", "*.asm",
"*.inc", "*.py", "*.yml", "*.txt", "*.json",
];
#[cfg(windows)]
fn process_utf16(bytes: &[u8]) -> Result<String, FromUtf16Error> {
let u16_bytes: Vec<u16> = bytes
@@ -47,28 +117,29 @@ fn fetch_wsl2_distros() -> Vec<String> {
.unwrap_or_default()
}
pub fn config_ui(ui: &mut egui::Ui, config: &Arc<RwLock<AppConfig>>, view_state: &mut ViewState) {
pub fn config_ui(
ui: &mut egui::Ui,
config: &AppConfigRef,
show_config_window: &mut bool,
state: &mut ConfigViewState,
appearance: &Appearance,
) {
let mut config_guard = config.write().unwrap();
let AppConfig {
custom_make,
available_wsl_distros,
selected_wsl_distro,
project_dir,
target_obj_dir,
base_obj_dir,
obj_path,
build_target,
left_obj,
right_obj,
project_dir_change,
queue_update_check,
auto_update_check,
objects,
object_nodes,
..
} = &mut *config_guard;
ui.heading("Updates");
ui.checkbox(auto_update_check, "Check for updates on startup");
if ui.button("Check now").clicked() {
*queue_update_check = true;
if ui.add_enabled(!state.check_update_running, egui::Button::new("Check now")).clicked() {
state.queue_check_update = true;
}
ui.label(format!("Current version: {}", cargo_crate_version!())).on_hover_ui_at_pointer(|ui| {
ui.label(formatcp!("Git branch: {}", env!("VERGEN_GIT_BRANCH")));
@@ -76,20 +147,20 @@ pub fn config_ui(ui: &mut egui::Ui, config: &Arc<RwLock<AppConfig>>, view_state:
ui.label(formatcp!("Build target: {}", env!("VERGEN_CARGO_TARGET_TRIPLE")));
ui.label(formatcp!("Debug: {}", env!("VERGEN_CARGO_DEBUG")));
});
if let Some(state) = &view_state.check_update {
ui.label(format!("Latest version: {}", state.latest_release.version));
if state.update_available {
ui.colored_label(Color32::LIGHT_GREEN, "Update available");
if let Some(result) = &state.check_update {
ui.label(format!("Latest version: {}", result.latest_release.version));
if result.update_available {
ui.colored_label(appearance.insert_color, "Update available");
ui.horizontal(|ui| {
if state.found_binary
if result.found_binary
&& ui
.button("Automatic")
.add_enabled(!state.update_running, egui::Button::new("Automatic"))
.on_hover_text_at_pointer(
"Automatically download and replace the current build",
)
.clicked()
{
view_state.jobs.push(queue_update());
state.queue_update = true;
}
if ui
.button("Manual")
@@ -106,149 +177,501 @@ pub fn config_ui(ui: &mut egui::Ui, config: &Arc<RwLock<AppConfig>>, view_state:
}
ui.separator();
ui.heading("Build config");
#[cfg(windows)]
{
if available_wsl_distros.is_none() {
*available_wsl_distros = Some(fetch_wsl2_distros());
ui.heading("Build");
if state.available_wsl_distros.is_none() {
state.available_wsl_distros = Some(fetch_wsl2_distros());
}
egui::ComboBox::from_label("Run in WSL2")
.selected_text(selected_wsl_distro.as_ref().unwrap_or(&"None".to_string()))
.selected_text(selected_wsl_distro.as_ref().unwrap_or(&"Disabled".to_string()))
.show_ui(ui, |ui| {
ui.selectable_value(selected_wsl_distro, None, "None");
for distro in available_wsl_distros.as_ref().unwrap() {
ui.selectable_value(selected_wsl_distro, None, "Disabled");
for distro in state.available_wsl_distros.as_ref().unwrap() {
ui.selectable_value(selected_wsl_distro, Some(distro.clone()), distro);
}
});
ui.separator();
}
#[cfg(not(windows))]
{
let _ = available_wsl_distros;
let _ = selected_wsl_distro;
}
ui.label("Custom make program:");
let mut custom_make_str = custom_make.clone().unwrap_or_default();
if ui.text_edit_singleline(&mut custom_make_str).changed() {
if custom_make_str.is_empty() {
*custom_make = None;
} else {
*custom_make = Some(custom_make_str);
ui.horizontal(|ui| {
ui.heading("Project");
if ui.button(RichText::new("Settings")).clicked() {
*show_config_window = true;
}
}
});
ui.separator();
ui.heading("Project config");
if view_state.diff_kind == DiffKind::SplitObj {
if ui.button("Select project dir").clicked() {
if let Some(path) = rfd::FileDialog::new().pick_folder() {
*project_dir = Some(path);
*project_dir_change = true;
*target_obj_dir = None;
*base_obj_dir = None;
*obj_path = None;
}
}
if let Some(dir) = project_dir {
ui.label(dir.to_string_lossy());
}
ui.separator();
if let Some(project_dir) = project_dir {
if ui.button("Select target build dir").clicked() {
if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder()
{
*target_obj_dir = Some(path);
*obj_path = None;
}
}
if let Some(dir) = target_obj_dir {
ui.label(dir.to_string_lossy());
}
ui.checkbox(build_target, "Build target");
ui.separator();
if ui.button("Select base build dir").clicked() {
if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder()
{
*base_obj_dir = Some(path);
*obj_path = None;
}
}
if let Some(dir) = base_obj_dir {
ui.label(dir.to_string_lossy());
}
ui.separator();
}
if let (Some(base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) {
if ui.button("Select obj").clicked() {
if let (Some(base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) {
let mut new_build_obj = obj_path.clone();
if objects.is_empty() {
if ui.button("Select object").clicked() {
if let Some(path) = rfd::FileDialog::new()
.set_directory(&target_dir)
.add_filter("Object file", &["o", "elf"])
.pick_file()
{
let mut new_build_obj: Option<String> = None;
if let Ok(obj_path) = path.strip_prefix(&base_dir) {
new_build_obj = Some(obj_path.display().to_string());
} else if let Ok(obj_path) = path.strip_prefix(&target_dir) {
new_build_obj = Some(obj_path.display().to_string());
}
if let Some(new_build_obj) = new_build_obj {
*obj_path = Some(new_build_obj);
view_state
.jobs
.push(queue_build(config.clone(), view_state.diff_config.clone()));
}
}
}
if let Some(obj) = obj_path {
ui.label(&*obj);
if ui.button("Build").clicked() {
view_state
.jobs
.push(queue_build(config.clone(), view_state.diff_config.clone()));
ui.label(
RichText::new(&*obj)
.color(appearance.replace_color)
.family(FontFamily::Monospace),
);
}
} else {
let had_search = !state.object_search.is_empty();
egui::TextEdit::singleline(&mut state.object_search).hint_text("Filter").ui(ui);
let mut root_open = None;
let mut node_open = NodeOpen::Default;
ui.horizontal(|ui| {
if ui.small_button("").on_hover_text_at_pointer("Collapse all").clicked() {
root_open = Some(false);
node_open = NodeOpen::Close;
}
if ui.small_button("").on_hover_text_at_pointer("Expand all").clicked() {
root_open = Some(true);
node_open = NodeOpen::Open;
}
if ui
.add_enabled(obj_path.is_some(), egui::Button::new("").small())
.on_hover_text_at_pointer("Current object")
.clicked()
{
root_open = Some(true);
node_open = NodeOpen::Object;
}
});
if state.object_search.is_empty() {
if had_search {
root_open = Some(true);
node_open = NodeOpen::Object;
}
} else if !had_search {
root_open = Some(true);
node_open = NodeOpen::Open;
}
ui.separator();
}
} else if view_state.diff_kind == DiffKind::WholeBinary {
if ui.button("Select left obj").clicked() {
if let Some(path) =
rfd::FileDialog::new().add_filter("Object file", &["o", "elf"]).pick_file()
{
*left_obj = Some(path);
}
}
if let Some(obj) = left_obj {
ui.label(obj.to_string_lossy());
CollapsingHeader::new(RichText::new("🗀 Objects").font(FontId {
size: appearance.ui_font.size,
family: appearance.code_font.family.clone(),
}))
.open(root_open)
.default_open(true)
.show(ui, |ui| {
let mut nodes = Cow::Borrowed(object_nodes);
if !state.object_search.is_empty() {
let search = state.object_search.to_ascii_lowercase();
nodes = Cow::Owned(
object_nodes.iter().filter_map(|node| filter_node(node, &search)).collect(),
);
}
ui.style_mut().wrap = Some(false);
for node in nodes.iter() {
display_node(ui, &mut new_build_obj, node, appearance, node_open);
}
});
}
if ui.button("Select right obj").clicked() {
if let Some(path) =
rfd::FileDialog::new().add_filter("Object file", &["o", "elf"]).pick_file()
{
*right_obj = Some(path);
if new_build_obj != *obj_path {
if let Some(obj) = new_build_obj {
// Will set obj_changed, which will trigger a rebuild
config_guard.set_obj_path(obj);
}
}
if let Some(obj) = right_obj {
ui.label(obj.to_string_lossy());
if config_guard.obj_path.is_some()
&& ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked()
{
state.queue_build = true;
}
} else {
ui.colored_label(appearance.delete_color, "Missing project settings");
}
if let (Some(_), Some(_)) = (left_obj, right_obj) {
if ui.button("Build").clicked() {
view_state.jobs.push(queue_bindiff(config.clone()));
ui.separator();
}
fn display_object(
ui: &mut egui::Ui,
obj_path: &mut Option<String>,
name: &str,
object: &ProjectObject,
appearance: &Appearance,
) {
let path_string = object.path.to_string_lossy().to_string();
let selected = matches!(obj_path, Some(path) if path == &path_string);
let color = if selected { appearance.emphasized_text_color } else { appearance.text_color };
if SelectableLabel::new(
selected,
RichText::new(name)
.font(FontId {
size: appearance.ui_font.size,
family: appearance.code_font.family.clone(),
})
.color(color),
)
.ui(ui)
.clicked()
{
*obj_path = Some(path_string);
}
}
#[derive(Default, Copy, Clone, PartialEq, Eq, Debug)]
enum NodeOpen {
#[default]
Default,
Open,
Close,
Object,
}
fn display_node(
ui: &mut egui::Ui,
obj_path: &mut Option<String>,
node: &ProjectObjectNode,
appearance: &Appearance,
node_open: NodeOpen,
) {
match node {
ProjectObjectNode::File(name, object) => {
display_object(ui, obj_path, name, object, appearance);
}
ProjectObjectNode::Dir(name, children) => {
let contains_obj = obj_path.as_ref().map(|path| contains_node(node, path));
let open = match node_open {
NodeOpen::Default => None,
NodeOpen::Open => Some(true),
NodeOpen::Close => Some(false),
NodeOpen::Object => contains_obj,
};
let color = if contains_obj == Some(true) {
appearance.replace_color
} else {
appearance.text_color
};
CollapsingHeader::new(
RichText::new(name)
.font(FontId {
size: appearance.ui_font.size,
family: appearance.code_font.family.clone(),
})
.color(color),
)
.open(open)
.show(ui, |ui| {
for node in children {
display_node(ui, obj_path, node, appearance, node_open);
}
});
}
}
}
fn contains_node(node: &ProjectObjectNode, path: &str) -> bool {
match node {
ProjectObjectNode::File(_, object) => {
let path_string = object.path.to_string_lossy().to_string();
path == path_string
}
ProjectObjectNode::Dir(_, children) => {
children.iter().any(|node| contains_node(node, path))
}
}
}
fn filter_node(node: &ProjectObjectNode, search: &str) -> Option<ProjectObjectNode> {
match node {
ProjectObjectNode::File(name, _) => {
if name.to_ascii_lowercase().contains(search) {
Some(node.clone())
} else {
None
}
}
ProjectObjectNode::Dir(name, children) => {
if name.to_ascii_lowercase().contains(search) {
return Some(node.clone());
}
let new_children =
children.iter().filter_map(|child| filter_node(child, search)).collect::<Vec<_>>();
if !new_children.is_empty() {
Some(ProjectObjectNode::Dir(name.clone(), new_children))
} else {
None
}
}
}
ui.checkbox(&mut view_state.view_config.reverse_fn_order, "Reverse function order (deferred)");
ui.separator();
}
const HELP_ICON: &str = "";
fn subheading(ui: &mut egui::Ui, text: &str, appearance: &Appearance) {
ui.label(
RichText::new(text).size(appearance.ui_font.size).color(appearance.emphasized_text_color),
);
}
fn format_path(path: &Option<PathBuf>, appearance: &Appearance) -> RichText {
let mut color = appearance.replace_color;
let text = if let Some(dir) = path {
if let Some(rel) = dirs::home_dir().and_then(|home| dir.strip_prefix(&home).ok()) {
format!("~{}{}", MAIN_SEPARATOR, rel.display())
} else {
format!("{}", dir.display())
}
} else {
color = appearance.delete_color;
"[none]".to_string()
};
RichText::new(text).color(color).family(FontFamily::Monospace)
}
fn pick_folder_ui(
ui: &mut egui::Ui,
dir: &Option<PathBuf>,
label: &str,
tooltip: impl FnOnce(&mut egui::Ui),
appearance: &Appearance,
) -> egui::Response {
let response = ui.horizontal(|ui| {
subheading(ui, label, appearance);
ui.link(HELP_ICON).on_hover_ui(tooltip);
ui.button("Select")
});
ui.label(format_path(dir, appearance));
response.inner
}
pub fn project_window(
ctx: &egui::Context,
config: &AppConfigRef,
show: &mut bool,
state: &mut ConfigViewState,
appearance: &Appearance,
) {
let mut config_guard = config.write().unwrap();
egui::Window::new("Project").open(show).show(ctx, |ui| {
split_obj_config_ui(ui, &mut config_guard, state, appearance);
});
if let Some(error) = &state.load_error {
let mut open = true;
egui::Window::new("Error").open(&mut open).show(ctx, |ui| {
ui.label("Failed to load project config:");
ui.colored_label(appearance.delete_color, error);
});
if !open {
state.load_error = None;
}
}
}
fn split_obj_config_ui(
ui: &mut egui::Ui,
config: &mut AppConfig,
state: &mut ConfigViewState,
appearance: &Appearance,
) {
let text_format = TextFormat::simple(appearance.ui_font.clone(), appearance.text_color);
let code_format = TextFormat::simple(
FontId { size: appearance.ui_font.size, family: appearance.code_font.family.clone() },
appearance.emphasized_text_color,
);
let response = pick_folder_ui(
ui,
&config.project_dir,
"Project directory",
|ui| {
let mut job = LayoutJob::default();
job.append("The root project directory.\n\n", 0.0, text_format.clone());
job.append(
"If a configuration file exists, it will be loaded automatically.",
0.0,
text_format.clone(),
);
ui.label(job);
},
appearance,
);
if response.clicked() {
if let Some(path) = rfd::FileDialog::new().pick_folder() {
config.set_project_dir(path);
}
}
ui.separator();
ui.horizontal(|ui| {
subheading(ui, "Custom make program", appearance);
ui.link(HELP_ICON).on_hover_ui(|ui| {
let mut job = LayoutJob::default();
job.append("By default, objdiff will build with ", 0.0, text_format.clone());
job.append("make", 0.0, code_format.clone());
job.append(
".\nIf the project uses a different build system (e.g. ",
0.0,
text_format.clone(),
);
job.append("ninja", 0.0, code_format.clone());
job.append(
"), specify it here.\nThe program must be in your ",
0.0,
text_format.clone(),
);
job.append("PATH", 0.0, code_format.clone());
job.append(".", 0.0, text_format.clone());
ui.label(job);
});
});
let mut custom_make_str = config.custom_make.clone().unwrap_or_default();
if ui.text_edit_singleline(&mut custom_make_str).changed() {
if custom_make_str.is_empty() {
config.custom_make = None;
} else {
config.custom_make = Some(custom_make_str);
}
}
ui.separator();
if let Some(project_dir) = config.project_dir.clone() {
let response = pick_folder_ui(
ui,
&config.target_obj_dir,
"Target build directory",
|ui| {
let mut job = LayoutJob::default();
job.append(
"This contains the \"target\" or \"expected\" objects, which are the intended result of the match.\n\n",
0.0,
text_format.clone(),
);
job.append(
"These are usually created by the project's build system or assembled.",
0.0,
text_format.clone(),
);
ui.label(job);
},
appearance,
);
if response.clicked() {
if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() {
config.set_target_obj_dir(path);
}
}
ui.checkbox(&mut config.build_target, "Build target objects").on_hover_ui(|ui| {
let mut job = LayoutJob::default();
job.append(
"Tells the build system to produce the target object.\n",
0.0,
text_format.clone(),
);
job.append("For example, this would call ", 0.0, text_format.clone());
job.append("make path/to/target.o", 0.0, code_format.clone());
job.append(".\n\n", 0.0, text_format.clone());
job.append(
"This is useful if the target objects are not already built\n",
0.0,
text_format.clone(),
);
job.append(
"or if they can change based on project configuration,\n",
0.0,
text_format.clone(),
);
job.append(
"but requires that the build system is configured correctly.",
0.0,
text_format.clone(),
);
ui.label(job);
});
ui.separator();
let response = pick_folder_ui(
ui,
&config.base_obj_dir,
"Base build directory",
|ui| {
let mut job = LayoutJob::default();
job.append(
"This contains the objects built from your decompiled code.",
0.0,
text_format.clone(),
);
ui.label(job);
},
appearance,
);
if response.clicked() {
if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() {
config.set_base_obj_dir(path);
}
}
ui.separator();
}
subheading(ui, "Watch settings", appearance);
let response =
ui.checkbox(&mut config.watcher_enabled, "Rebuild on changes").on_hover_ui(|ui| {
let mut job = LayoutJob::default();
job.append(
"Automatically re-run the build & diff when files change.",
0.0,
text_format.clone(),
);
ui.label(job);
});
if response.changed() {
config.watcher_change = true;
};
ui.horizontal(|ui| {
ui.label(RichText::new("File patterns").color(appearance.text_color));
if ui.button("Reset").clicked() {
config.watch_patterns =
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect();
config.watcher_change = true;
}
});
let mut remove_at: Option<usize> = None;
for (idx, glob) in config.watch_patterns.iter().enumerate() {
ui.horizontal(|ui| {
ui.label(
RichText::new(format!("{}", glob))
.color(appearance.text_color)
.family(FontFamily::Monospace),
);
if ui.small_button("-").clicked() {
remove_at = Some(idx);
}
});
}
if let Some(idx) = remove_at {
config.watch_patterns.remove(idx);
config.watcher_change = true;
}
ui.horizontal(|ui| {
egui::TextEdit::singleline(&mut state.watch_pattern_text).desired_width(100.0).show(ui);
if ui.small_button("+").clicked() {
if let Ok(glob) = Glob::new(&state.watch_pattern_text) {
config.watch_patterns.push(glob);
config.watcher_change = true;
state.watch_pattern_text.clear();
}
}
});
}

View File

@@ -1,14 +1,16 @@
use std::{cmp::min, default::Default, mem::take};
use egui::{text::LayoutJob, Align, Color32, Label, Layout, Sense, Vec2};
use egui::{text::LayoutJob, Align, Label, Layout, Sense, Vec2};
use egui_extras::{Column, TableBuilder};
use time::format_description;
use crate::{
app::{SymbolReference, View, ViewConfig, ViewState},
jobs::Job,
obj::{ObjDataDiff, ObjDataDiffKind, ObjInfo, ObjSection},
views::{write_text, COLOR_RED},
views::{
appearance::Appearance,
symbol_diff::{DiffViewState, SymbolReference, View},
write_text,
},
};
const BYTES_PER_ROW: usize = 16;
@@ -17,29 +19,29 @@ fn find_section<'a>(obj: &'a ObjInfo, selected_symbol: &SymbolReference) -> Opti
obj.sections.iter().find(|section| section.name == selected_symbol.section_name)
}
fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], config: &ViewConfig) {
fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appearance: &Appearance) {
if diffs.iter().any(|d| d.kind != ObjDataDiffKind::None) {
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
}
let mut job = LayoutJob::default();
write_text(
format!("{address:08X}: ").as_str(),
Color32::GRAY,
appearance.text_color,
&mut job,
config.code_font.clone(),
appearance.code_font.clone(),
);
let mut cur_addr = 0usize;
for diff in diffs {
let base_color = match diff.kind {
ObjDataDiffKind::None => Color32::GRAY,
ObjDataDiffKind::Replace => Color32::LIGHT_BLUE,
ObjDataDiffKind::Delete => COLOR_RED,
ObjDataDiffKind::Insert => Color32::GREEN,
ObjDataDiffKind::None => appearance.text_color,
ObjDataDiffKind::Replace => appearance.replace_color,
ObjDataDiffKind::Delete => appearance.delete_color,
ObjDataDiffKind::Insert => appearance.insert_color,
};
if diff.data.is_empty() {
let mut str = " ".repeat(diff.len);
str.push_str(" ".repeat(diff.len / 8).as_str());
write_text(str.as_str(), base_color, &mut job, config.code_font.clone());
write_text(str.as_str(), base_color, &mut job, appearance.code_font.clone());
cur_addr += diff.len;
} else {
let mut text = String::new();
@@ -50,7 +52,7 @@ fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], config:
text.push(' ');
}
}
write_text(text.as_str(), base_color, &mut job, config.code_font.clone());
write_text(text.as_str(), base_color, &mut job, appearance.code_font.clone());
}
}
if cur_addr < BYTES_PER_ROW {
@@ -58,22 +60,22 @@ fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], config:
let mut str = " ".to_string();
str.push_str(" ".repeat(n).as_str());
str.push_str(" ".repeat(n / 8).as_str());
write_text(str.as_str(), Color32::GRAY, &mut job, config.code_font.clone());
write_text(str.as_str(), appearance.text_color, &mut job, appearance.code_font.clone());
}
write_text(" ", Color32::GRAY, &mut job, config.code_font.clone());
write_text(" ", appearance.text_color, &mut job, appearance.code_font.clone());
for diff in diffs {
let base_color = match diff.kind {
ObjDataDiffKind::None => Color32::GRAY,
ObjDataDiffKind::Replace => Color32::LIGHT_BLUE,
ObjDataDiffKind::Delete => COLOR_RED,
ObjDataDiffKind::Insert => Color32::GREEN,
ObjDataDiffKind::None => appearance.text_color,
ObjDataDiffKind::Replace => appearance.replace_color,
ObjDataDiffKind::Delete => appearance.delete_color,
ObjDataDiffKind::Insert => appearance.insert_color,
};
if diff.data.is_empty() {
write_text(
" ".repeat(diff.len).as_str(),
base_color,
&mut job,
config.code_font.clone(),
appearance.code_font.clone(),
);
} else {
let mut text = String::new();
@@ -85,7 +87,7 @@ fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], config:
text.push('.');
}
}
write_text(text.as_str(), base_color, &mut job, config.code_font.clone());
write_text(text.as_str(), base_color, &mut job, appearance.code_font.clone());
}
}
ui.add(Label::new(job).sense(Sense::click()));
@@ -133,7 +135,7 @@ fn data_table_ui(
left_obj: &ObjInfo,
right_obj: &ObjInfo,
selected_symbol: &SymbolReference,
config: &ViewConfig,
config: &Appearance,
) -> Option<()> {
let left_section = find_section(left_obj, selected_symbol)?;
let right_section = find_section(right_obj, selected_symbol)?;
@@ -161,10 +163,10 @@ fn data_table_ui(
Some(())
}
pub fn data_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
let mut rebuild = false;
let (Some(result), Some(selected_symbol)) = (&view_state.build, &view_state.selected_symbol) else {
return rebuild;
pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
else {
return;
};
// Header
@@ -182,13 +184,13 @@ pub fn data_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
ui.set_width(column_width);
if ui.button("Back").clicked() {
view_state.current_view = View::SymbolDiff;
state.current_view = View::SymbolDiff;
}
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.colored_label(Color32::WHITE, &selected_symbol.symbol_name);
ui.colored_label(appearance.highlight_color, &selected_symbol.symbol_name);
ui.label("Diff target:");
});
},
@@ -202,14 +204,17 @@ pub fn data_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
ui.set_width(column_width);
ui.horizontal(|ui| {
if ui.button("Build").clicked() {
rebuild = true;
if ui
.add_enabled(!state.build_running, egui::Button::new("Build"))
.clicked()
{
state.queue_build = true;
}
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
if view_state.jobs.iter().any(|job| job.job_type == Job::ObjDiff) {
ui.label("Building...");
if state.build_running {
ui.colored_label(appearance.replace_color, "Building");
} else {
ui.label("Last built:");
let format =
@@ -217,7 +222,7 @@ pub fn data_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
ui.label(
result
.time
.to_offset(view_state.utc_offset)
.to_offset(appearance.utc_offset)
.format(&format)
.unwrap(),
);
@@ -247,8 +252,6 @@ pub fn data_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
.resizable(false)
.auto_shrink([false, false])
.min_scrolled_height(available_height);
data_table_ui(table, left_obj, right_obj, selected_symbol, &view_state.view_config);
data_table_ui(table, left_obj, right_obj, selected_symbol, appearance);
}
rebuild
}

34
src/views/demangle.rs Normal file
View File

@@ -0,0 +1,34 @@
use egui::TextStyle;
use crate::views::appearance::Appearance;
#[derive(Default)]
pub struct DemangleViewState {
pub text: String,
}
pub fn demangle_window(
ctx: &egui::Context,
show: &mut bool,
state: &mut DemangleViewState,
appearance: &Appearance,
) {
egui::Window::new("Demangle").open(show).show(ctx, |ui| {
ui.text_edit_singleline(&mut state.text);
ui.add_space(10.0);
if let Some(demangled) = cwdemangle::demangle(&state.text, &Default::default()) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
ui.colored_label(appearance.replace_color, &demangled);
});
if ui.button("Copy").clicked() {
ui.output_mut(|output| output.copied_text = demangled);
}
} else {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
ui.colored_label(appearance.replace_color, "[invalid]");
});
}
});
}

View File

@@ -8,18 +8,26 @@ use ppc750cl::Argument;
use time::format_description;
use crate::{
app::{SymbolReference, View, ViewConfig, ViewState},
jobs::Job,
obj::{
ObjInfo, ObjIns, ObjInsArg, ObjInsArgDiff, ObjInsDiff, ObjInsDiffKind, ObjReloc,
ObjRelocKind, ObjSymbol,
},
views::{symbol_diff::match_color_for_symbol, write_text, COLOR_RED},
views::{
appearance::Appearance,
symbol_diff::{match_color_for_symbol, DiffViewState, SymbolReference, View},
write_text,
},
};
fn write_reloc_name(reloc: &ObjReloc, color: Color32, job: &mut LayoutJob, font_id: FontId) {
fn write_reloc_name(
reloc: &ObjReloc,
color: Color32,
job: &mut LayoutJob,
font_id: FontId,
appearance: &Appearance,
) {
let name = reloc.target.demangled_name.as_ref().unwrap_or(&reloc.target.name);
write_text(name, Color32::LIGHT_GRAY, job, font_id.clone());
write_text(name, appearance.emphasized_text_color, job, font_id.clone());
match reloc.target.addend.cmp(&0i64) {
Ordering::Greater => {
write_text(&format!("+{:#X}", reloc.target.addend), color, job, font_id)
@@ -31,51 +39,57 @@ fn write_reloc_name(reloc: &ObjReloc, color: Color32, job: &mut LayoutJob, font_
}
}
fn write_reloc(reloc: &ObjReloc, color: Color32, job: &mut LayoutJob, font_id: FontId) {
fn write_reloc(
reloc: &ObjReloc,
color: Color32,
job: &mut LayoutJob,
font_id: FontId,
appearance: &Appearance,
) {
match reloc.kind {
ObjRelocKind::PpcAddr16Lo => {
write_reloc_name(reloc, color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone(), appearance);
write_text("@l", color, job, font_id);
}
ObjRelocKind::PpcAddr16Hi => {
write_reloc_name(reloc, color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone(), appearance);
write_text("@h", color, job, font_id);
}
ObjRelocKind::PpcAddr16Ha => {
write_reloc_name(reloc, color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone(), appearance);
write_text("@ha", color, job, font_id);
}
ObjRelocKind::PpcEmbSda21 => {
write_reloc_name(reloc, color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone(), appearance);
write_text("@sda21", color, job, font_id);
}
ObjRelocKind::MipsHi16 => {
write_text("%hi(", color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone(), appearance);
write_text(")", color, job, font_id);
}
ObjRelocKind::MipsLo16 => {
write_text("%lo(", color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone(), appearance);
write_text(")", color, job, font_id);
}
ObjRelocKind::MipsGot16 => {
write_text("%got(", color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone(), appearance);
write_text(")", color, job, font_id);
}
ObjRelocKind::MipsCall16 => {
write_text("%call16(", color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone(), appearance);
write_text(")", color, job, font_id);
}
ObjRelocKind::MipsGpRel16 => {
write_text("%gp_rel(", color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone(), appearance);
write_text(")", color, job, font_id);
}
ObjRelocKind::PpcRel24 | ObjRelocKind::PpcRel14 | ObjRelocKind::Mips26 => {
write_reloc_name(reloc, color, job, font_id);
write_reloc_name(reloc, color, job, font_id, appearance);
}
ObjRelocKind::Absolute | ObjRelocKind::MipsGpRel32 => {
write_text("[INVALID]", color, job, font_id);
@@ -89,59 +103,71 @@ fn write_ins(
args: &[Option<ObjInsArgDiff>],
base_addr: u32,
job: &mut LayoutJob,
config: &ViewConfig,
appearance: &Appearance,
) {
let base_color = match diff_kind {
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
Color32::GRAY
appearance.text_color
}
ObjInsDiffKind::Replace => Color32::LIGHT_BLUE,
ObjInsDiffKind::Delete => COLOR_RED,
ObjInsDiffKind::Insert => Color32::GREEN,
ObjInsDiffKind::Replace => appearance.replace_color,
ObjInsDiffKind::Delete => appearance.delete_color,
ObjInsDiffKind::Insert => appearance.insert_color,
};
write_text(
&format!("{:<11}", ins.mnemonic),
match diff_kind {
ObjInsDiffKind::OpMismatch => Color32::LIGHT_BLUE,
ObjInsDiffKind::OpMismatch => appearance.replace_color,
_ => base_color,
},
job,
config.code_font.clone(),
appearance.code_font.clone(),
);
let mut writing_offset = false;
for (i, arg) in ins.args.iter().enumerate() {
if i == 0 {
write_text(" ", base_color, job, config.code_font.clone());
write_text(" ", base_color, job, appearance.code_font.clone());
}
if i > 0 && !writing_offset {
write_text(", ", base_color, job, config.code_font.clone());
write_text(", ", base_color, job, appearance.code_font.clone());
}
let color = if let Some(diff) = args.get(i).and_then(|a| a.as_ref()) {
config.diff_colors[diff.idx % config.diff_colors.len()]
appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
} else {
base_color
};
match arg {
ObjInsArg::PpcArg(arg) => match arg {
Argument::Offset(val) => {
write_text(&format!("{val}"), color, job, config.code_font.clone());
write_text("(", base_color, job, config.code_font.clone());
write_text(&format!("{val}"), color, job, appearance.code_font.clone());
write_text("(", base_color, job, appearance.code_font.clone());
writing_offset = true;
continue;
}
Argument::Uimm(_) | Argument::Simm(_) => {
write_text(&format!("{arg}"), color, job, config.code_font.clone());
write_text(&format!("{arg}"), color, job, appearance.code_font.clone());
}
_ => {
write_text(&format!("{arg}"), color, job, config.code_font.clone());
write_text(&format!("{arg}"), color, job, appearance.code_font.clone());
}
},
ObjInsArg::Reloc => {
write_reloc(ins.reloc.as_ref().unwrap(), base_color, job, config.code_font.clone());
write_reloc(
ins.reloc.as_ref().unwrap(),
base_color,
job,
appearance.code_font.clone(),
appearance,
);
}
ObjInsArg::RelocWithBase => {
write_reloc(ins.reloc.as_ref().unwrap(), base_color, job, config.code_font.clone());
write_text("(", base_color, job, config.code_font.clone());
write_reloc(
ins.reloc.as_ref().unwrap(),
base_color,
job,
appearance.code_font.clone(),
appearance,
);
write_text("(", base_color, job, appearance.code_font.clone());
writing_offset = true;
continue;
}
@@ -150,7 +176,7 @@ fn write_ins(
str.strip_prefix('$').unwrap_or(str),
color,
job,
config.code_font.clone(),
appearance.code_font.clone(),
);
}
ObjInsArg::MipsArgWithBase(str) => {
@@ -158,25 +184,25 @@ fn write_ins(
str.strip_prefix('$').unwrap_or(str),
color,
job,
config.code_font.clone(),
appearance.code_font.clone(),
);
write_text("(", base_color, job, config.code_font.clone());
write_text("(", base_color, job, appearance.code_font.clone());
writing_offset = true;
continue;
}
ObjInsArg::BranchOffset(offset) => {
let addr = offset + ins.address as i32 - base_addr as i32;
write_text(&format!("{addr:x}"), color, job, config.code_font.clone());
write_text(&format!("{addr:x}"), color, job, appearance.code_font.clone());
}
}
if writing_offset {
write_text(")", base_color, job, config.code_font.clone());
write_text(")", base_color, job, appearance.code_font.clone());
writing_offset = false;
}
}
}
fn ins_hover_ui(ui: &mut egui::Ui, ins: &ObjIns) {
fn ins_hover_ui(ui: &mut egui::Ui, ins: &ObjIns, appearance: &Appearance) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
@@ -202,13 +228,19 @@ fn ins_hover_ui(ui: &mut egui::Ui, ins: &ObjIns) {
if let Some(reloc) = &ins.reloc {
ui.label(format!("Relocation type: {:?}", reloc.kind));
ui.colored_label(Color32::WHITE, format!("Name: {}", reloc.target.name));
ui.colored_label(appearance.highlight_color, format!("Name: {}", reloc.target.name));
if let Some(section) = &reloc.target_section {
ui.colored_label(Color32::WHITE, format!("Section: {section}"));
ui.colored_label(Color32::WHITE, format!("Address: {:x}", reloc.target.address));
ui.colored_label(Color32::WHITE, format!("Size: {:x}", reloc.target.size));
ui.colored_label(appearance.highlight_color, format!("Section: {section}"));
ui.colored_label(
appearance.highlight_color,
format!("Address: {:x}", reloc.target.address),
);
ui.colored_label(
appearance.highlight_color,
format!("Size: {:x}", reloc.target.size),
);
} else {
ui.colored_label(Color32::WHITE, "Extern".to_string());
ui.colored_label(appearance.highlight_color, "Extern".to_string());
}
}
});
@@ -279,7 +311,12 @@ fn find_symbol<'a>(obj: &'a ObjInfo, selected_symbol: &SymbolReference) -> Optio
})
}
fn asm_row_ui(ui: &mut egui::Ui, ins_diff: &ObjInsDiff, symbol: &ObjSymbol, config: &ViewConfig) {
fn asm_row_ui(
ui: &mut egui::Ui,
ins_diff: &ObjInsDiff,
symbol: &ObjSymbol,
appearance: &Appearance,
) {
if ins_diff.kind != ObjInsDiffKind::None {
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
}
@@ -291,45 +328,50 @@ fn asm_row_ui(ui: &mut egui::Ui, ins_diff: &ObjInsDiff, symbol: &ObjSymbol, conf
let base_color = match ins_diff.kind {
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
Color32::GRAY
appearance.text_color
}
ObjInsDiffKind::Replace => Color32::LIGHT_BLUE,
ObjInsDiffKind::Delete => COLOR_RED,
ObjInsDiffKind::Insert => Color32::GREEN,
ObjInsDiffKind::Replace => appearance.replace_color,
ObjInsDiffKind::Delete => appearance.delete_color,
ObjInsDiffKind::Insert => appearance.insert_color,
};
let mut pad = 6;
if let Some(line) = ins.line {
let line_str = format!("{line} ");
write_text(&line_str, Color32::DARK_GRAY, &mut job, config.code_font.clone());
write_text(
&line_str,
appearance.deemphasized_text_color,
&mut job,
appearance.code_font.clone(),
);
pad = 12 - line_str.len();
}
write_text(
&format!("{:<1$}", format!("{:x}: ", ins.address - symbol.address as u32), pad),
base_color,
&mut job,
config.code_font.clone(),
appearance.code_font.clone(),
);
if let Some(branch) = &ins_diff.branch_from {
write_text(
"~> ",
config.diff_colors[branch.branch_idx % config.diff_colors.len()],
appearance.diff_colors[branch.branch_idx % appearance.diff_colors.len()],
&mut job,
config.code_font.clone(),
appearance.code_font.clone(),
);
} else {
write_text(" ", base_color, &mut job, config.code_font.clone());
write_text(" ", base_color, &mut job, appearance.code_font.clone());
}
write_ins(ins, &ins_diff.kind, &ins_diff.arg_diff, symbol.address as u32, &mut job, config);
write_ins(ins, &ins_diff.kind, &ins_diff.arg_diff, symbol.address as u32, &mut job, appearance);
if let Some(branch) = &ins_diff.branch_to {
write_text(
" ~>",
config.diff_colors[branch.branch_idx % config.diff_colors.len()],
appearance.diff_colors[branch.branch_idx % appearance.diff_colors.len()],
&mut job,
config.code_font.clone(),
appearance.code_font.clone(),
);
}
ui.add(Label::new(job).sense(Sense::click()))
.on_hover_ui_at_pointer(|ui| ins_hover_ui(ui, ins))
.on_hover_ui_at_pointer(|ui| ins_hover_ui(ui, ins, appearance))
.context_menu(|ui| ins_context_menu(ui, ins));
}
@@ -338,21 +380,21 @@ fn asm_table_ui(
left_obj: &ObjInfo,
right_obj: &ObjInfo,
selected_symbol: &SymbolReference,
config: &ViewConfig,
appearance: &Appearance,
) -> Option<()> {
let left_symbol = find_symbol(left_obj, selected_symbol);
let right_symbol = find_symbol(right_obj, selected_symbol);
let instructions_len = left_symbol.or(right_symbol).map(|s| s.instructions.len())?;
table.body(|body| {
body.rows(config.code_font.size, instructions_len, |row_index, mut row| {
body.rows(appearance.code_font.size, instructions_len, |row_index, mut row| {
row.col(|ui| {
if let Some(symbol) = left_symbol {
asm_row_ui(ui, &symbol.instructions[row_index], symbol, config);
asm_row_ui(ui, &symbol.instructions[row_index], symbol, appearance);
}
});
row.col(|ui| {
if let Some(symbol) = right_symbol {
asm_row_ui(ui, &symbol.instructions[row_index], symbol, config);
asm_row_ui(ui, &symbol.instructions[row_index], symbol, appearance);
}
});
});
@@ -360,10 +402,10 @@ fn asm_table_ui(
Some(())
}
pub fn function_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
let mut rebuild = false;
let (Some(result), Some(selected_symbol)) = (&view_state.build, &view_state.selected_symbol) else {
return rebuild;
pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
else {
return;
};
// Header
@@ -381,15 +423,15 @@ pub fn function_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
ui.set_width(column_width);
if ui.button("Back").clicked() {
view_state.current_view = View::SymbolDiff;
state.current_view = View::SymbolDiff;
}
let demangled = demangle(&selected_symbol.symbol_name, &Default::default());
let name = demangled.as_deref().unwrap_or(&selected_symbol.symbol_name);
let mut job = LayoutJob::simple(
name.to_string(),
view_state.view_config.code_font.clone(),
Color32::WHITE,
appearance.code_font.clone(),
appearance.highlight_color,
column_width,
);
job.wrap.break_anywhere = true;
@@ -411,14 +453,17 @@ pub fn function_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
ui.set_width(column_width);
ui.horizontal(|ui| {
if ui.button("Build").clicked() {
rebuild = true;
if ui
.add_enabled(!state.build_running, egui::Button::new("Build"))
.clicked()
{
state.queue_build = true;
}
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
if view_state.jobs.iter().any(|job| job.job_type == Job::ObjDiff) {
ui.label("Building...");
if state.build_running {
ui.colored_label(appearance.replace_color, "Building");
} else {
ui.label("Last built:");
let format =
@@ -426,7 +471,7 @@ pub fn function_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
ui.label(
result
.time
.to_offset(view_state.utc_offset)
.to_offset(appearance.utc_offset)
.format(&format)
.unwrap(),
);
@@ -443,7 +488,7 @@ pub fn function_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
.and_then(|symbol| symbol.match_percent)
{
ui.colored_label(
match_color_for_symbol(match_percent),
match_color_for_symbol(match_percent, appearance),
&format!("{match_percent:.0}%"),
);
} else {
@@ -467,7 +512,6 @@ pub fn function_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
.resizable(false)
.auto_shrink([false, false])
.min_scrolled_height(available_height);
asm_table_ui(table, left_obj, right_obj, selected_symbol, &view_state.view_config);
asm_table_ui(table, left_obj, right_obj, selected_symbol, appearance);
}
rebuild
}

View File

@@ -1,12 +1,12 @@
use egui::{Color32, ProgressBar, Widget};
use egui::{ProgressBar, Widget};
use crate::app::ViewState;
use crate::{jobs::JobQueue, views::appearance::Appearance};
pub fn jobs_ui(ui: &mut egui::Ui, view_state: &mut ViewState) {
pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) {
ui.label("Jobs");
let mut remove_job: Option<usize> = None;
for (idx, job) in view_state.jobs.iter_mut().enumerate() {
for job in jobs.iter_mut() {
let Ok(status) = job.status.read() else {
continue;
};
@@ -17,10 +17,10 @@ pub fn jobs_ui(ui: &mut egui::Ui, view_state: &mut ViewState) {
if job.handle.is_some() {
job.should_remove = true;
if let Err(e) = job.cancel.send(()) {
eprintln!("Failed to cancel job: {e:?}");
log::error!("Failed to cancel job: {e:?}");
}
} else {
remove_job = Some(idx);
remove_job = Some(job.id);
}
}
});
@@ -33,16 +33,16 @@ pub fn jobs_ui(ui: &mut egui::Ui, view_state: &mut ViewState) {
if let Some(err) = &status.error {
let err_string = err.to_string();
ui.colored_label(
Color32::from_rgb(255, 0, 0),
appearance.delete_color,
if err_string.len() > STATUS_LENGTH - 10 {
format!("Error: {}...", &err_string[0..STATUS_LENGTH - 10])
format!("Error: {}", &err_string[0..STATUS_LENGTH - 10])
} else {
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
},
);
} else {
ui.label(if status.status.len() > STATUS_LENGTH - 3 {
format!("{}...", &status.status[0..STATUS_LENGTH - 3])
format!("{}", &status.status[0..STATUS_LENGTH - 3])
} else {
format!("{:width$}", &status.status, width = STATUS_LENGTH)
});
@@ -51,6 +51,6 @@ pub fn jobs_ui(ui: &mut egui::Ui, view_state: &mut ViewState) {
}
if let Some(idx) = remove_job {
view_state.jobs.remove(idx);
jobs.remove(idx);
}
}

View File

@@ -1,13 +1,14 @@
use egui::{text::LayoutJob, Color32, FontId, TextFormat};
pub(crate) mod appearance;
pub(crate) mod config;
pub(crate) mod data_diff;
pub(crate) mod demangle;
pub(crate) mod function_diff;
pub(crate) mod jobs;
pub(crate) mod symbol_diff;
const COLOR_RED: Color32 = Color32::from_rgb(200, 40, 41);
#[inline]
fn write_text(str: &str, color: Color32, job: &mut LayoutJob, font_id: FontId) {
job.append(str, 0.0, TextFormat::simple(font_id, color));
}

View File

@@ -1,3 +1,5 @@
use std::mem::take;
use egui::{
text::LayoutJob, Align, CollapsingHeader, Color32, Layout, Rgba, ScrollArea, SelectableLabel,
TextEdit, Ui, Vec2, Widget,
@@ -5,19 +7,92 @@ use egui::{
use egui_extras::{Size, StripBuilder};
use crate::{
app::{SymbolReference, View, ViewConfig, ViewState},
jobs::objdiff::BuildStatus,
app::AppConfigRef,
jobs::{
objdiff::{BuildStatus, ObjDiffResult},
Job, JobQueue, JobResult,
},
obj::{ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags},
views::write_text,
views::{appearance::Appearance, write_text},
};
pub fn match_color_for_symbol(match_percent: f32) -> Color32 {
pub struct SymbolReference {
pub symbol_name: String,
pub section_name: String,
}
#[allow(clippy::enum_variant_names)]
#[derive(Default, Eq, PartialEq, Copy, Clone)]
pub enum View {
#[default]
SymbolDiff,
FunctionDiff,
DataDiff,
}
#[derive(Default)]
pub struct DiffViewState {
pub build: Option<Box<ObjDiffResult>>,
pub current_view: View,
pub symbol_state: SymbolViewState,
pub search: String,
pub queue_build: bool,
pub build_running: bool,
}
#[derive(Default)]
pub struct SymbolViewState {
pub highlighted_symbol: Option<String>,
pub selected_symbol: Option<SymbolReference>,
pub reverse_fn_order: bool,
pub disable_reverse_fn_order: bool,
}
impl DiffViewState {
pub fn pre_update(&mut self, jobs: &mut JobQueue, config: &AppConfigRef) {
jobs.results.retain_mut(|result| {
if let JobResult::ObjDiff(result) = result {
self.build = take(result);
false
} else {
true
}
});
self.build_running = jobs.is_running(Job::ObjDiff);
self.symbol_state.disable_reverse_fn_order = false;
if let Ok(config) = config.read() {
if let Some(obj_path) = &config.obj_path {
if let Some(object) = config.objects.iter().find(|object| {
let path_string = object.path.to_string_lossy().to_string();
&path_string == obj_path
}) {
if let Some(value) = object.reverse_fn_order {
self.symbol_state.reverse_fn_order = value;
self.symbol_state.disable_reverse_fn_order = true;
}
}
}
}
}
pub fn post_update(&mut self, _jobs: &mut JobQueue, config: &AppConfigRef) {
if self.queue_build {
self.queue_build = false;
if let Ok(mut config) = config.write() {
config.queue_build = true;
}
}
}
}
pub fn match_color_for_symbol(match_percent: f32, appearance: &Appearance) -> Color32 {
if match_percent == 100.0 {
Color32::GREEN
appearance.insert_color
} else if match_percent >= 50.0 {
Color32::LIGHT_BLUE
appearance.replace_color
} else {
Color32::RED
appearance.delete_color
}
}
@@ -39,83 +114,87 @@ fn symbol_context_menu_ui(ui: &mut Ui, symbol: &ObjSymbol) {
});
}
fn symbol_hover_ui(ui: &mut Ui, symbol: &ObjSymbol) {
fn symbol_hover_ui(ui: &mut Ui, symbol: &ObjSymbol, appearance: &Appearance) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.colored_label(Color32::WHITE, format!("Name: {}", symbol.name));
ui.colored_label(Color32::WHITE, format!("Address: {:x}", symbol.address));
ui.colored_label(appearance.highlight_color, format!("Name: {}", symbol.name));
ui.colored_label(appearance.highlight_color, format!("Address: {:x}", symbol.address));
if symbol.size_known {
ui.colored_label(Color32::WHITE, format!("Size: {:x}", symbol.size));
ui.colored_label(appearance.highlight_color, format!("Size: {:x}", symbol.size));
} else {
ui.colored_label(Color32::WHITE, format!("Size: {:x} (assumed)", symbol.size));
ui.colored_label(
appearance.highlight_color,
format!("Size: {:x} (assumed)", symbol.size),
);
}
});
}
#[must_use]
fn symbol_ui(
ui: &mut Ui,
symbol: &ObjSymbol,
section: Option<&ObjSection>,
highlighted_symbol: &mut Option<String>,
selected_symbol: &mut Option<SymbolReference>,
current_view: &mut View,
config: &ViewConfig,
) {
state: &mut SymbolViewState,
appearance: &Appearance,
) -> Option<View> {
let mut ret = None;
let mut job = LayoutJob::default();
let name: &str =
if let Some(demangled) = &symbol.demangled_name { demangled } else { &symbol.name };
let mut selected = false;
if let Some(sym) = highlighted_symbol {
if let Some(sym) = &state.highlighted_symbol {
selected = sym == &symbol.name;
}
write_text("[", Color32::GRAY, &mut job, config.code_font.clone());
write_text("[", appearance.text_color, &mut job, appearance.code_font.clone());
if symbol.flags.0.contains(ObjSymbolFlags::Common) {
write_text("c", Color32::from_rgb(0, 255, 255), &mut job, config.code_font.clone());
write_text("c", appearance.replace_color, &mut job, appearance.code_font.clone());
} else if symbol.flags.0.contains(ObjSymbolFlags::Global) {
write_text("g", Color32::GREEN, &mut job, config.code_font.clone());
write_text("g", appearance.insert_color, &mut job, appearance.code_font.clone());
} else if symbol.flags.0.contains(ObjSymbolFlags::Local) {
write_text("l", Color32::GRAY, &mut job, config.code_font.clone());
write_text("l", appearance.text_color, &mut job, appearance.code_font.clone());
}
if symbol.flags.0.contains(ObjSymbolFlags::Weak) {
write_text("w", Color32::GRAY, &mut job, config.code_font.clone());
write_text("w", appearance.text_color, &mut job, appearance.code_font.clone());
}
write_text("] ", Color32::GRAY, &mut job, config.code_font.clone());
write_text("] ", appearance.text_color, &mut job, appearance.code_font.clone());
if let Some(match_percent) = symbol.match_percent {
write_text("(", Color32::GRAY, &mut job, config.code_font.clone());
write_text("(", appearance.text_color, &mut job, appearance.code_font.clone());
write_text(
&format!("{match_percent:.0}%"),
match_color_for_symbol(match_percent),
match_color_for_symbol(match_percent, appearance),
&mut job,
config.code_font.clone(),
appearance.code_font.clone(),
);
write_text(") ", Color32::GRAY, &mut job, config.code_font.clone());
write_text(") ", appearance.text_color, &mut job, appearance.code_font.clone());
}
write_text(name, Color32::WHITE, &mut job, config.code_font.clone());
write_text(name, appearance.highlight_color, &mut job, appearance.code_font.clone());
let response = SelectableLabel::new(selected, job)
.ui(ui)
.context_menu(|ui| symbol_context_menu_ui(ui, symbol))
.on_hover_ui_at_pointer(|ui| symbol_hover_ui(ui, symbol));
.on_hover_ui_at_pointer(|ui| symbol_hover_ui(ui, symbol, appearance));
if response.clicked() {
if let Some(section) = section {
if section.kind == ObjSectionKind::Code {
*selected_symbol = Some(SymbolReference {
state.selected_symbol = Some(SymbolReference {
symbol_name: symbol.name.clone(),
section_name: section.name.clone(),
});
*current_view = View::FunctionDiff;
ret = Some(View::FunctionDiff);
} else if section.kind == ObjSectionKind::Data {
*selected_symbol = Some(SymbolReference {
state.selected_symbol = Some(SymbolReference {
symbol_name: section.name.clone(),
section_name: section.name.clone(),
});
*current_view = View::DataDiff;
ret = Some(View::DataDiff);
}
}
} else if response.hovered() {
*highlighted_symbol = Some(symbol.name.clone());
state.highlighted_symbol = Some(symbol.name.clone());
}
ret
}
fn symbol_matches_search(symbol: &ObjSymbol, search_str: &str) -> bool {
@@ -128,16 +207,15 @@ fn symbol_matches_search(symbol: &ObjSymbol, search_str: &str) -> bool {
.unwrap_or(false)
}
#[allow(clippy::too_many_arguments)]
#[must_use]
fn symbol_list_ui(
ui: &mut Ui,
obj: &ObjInfo,
highlighted_symbol: &mut Option<String>,
selected_symbol: &mut Option<SymbolReference>,
current_view: &mut View,
state: &mut SymbolViewState,
lower_search: &str,
config: &ViewConfig,
) {
appearance: &Appearance,
) -> Option<View> {
let mut ret = None;
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
@@ -146,15 +224,7 @@ fn symbol_list_ui(
if !obj.common.is_empty() {
CollapsingHeader::new(".comm").default_open(true).show(ui, |ui| {
for symbol in &obj.common {
symbol_ui(
ui,
symbol,
None,
highlighted_symbol,
selected_symbol,
current_view,
config,
);
ret = ret.or(symbol_ui(ui, symbol, None, state, appearance));
}
});
}
@@ -163,62 +233,44 @@ fn symbol_list_ui(
CollapsingHeader::new(format!("{} ({:x})", section.name, section.size))
.default_open(true)
.show(ui, |ui| {
if section.kind == ObjSectionKind::Code && config.reverse_fn_order {
if section.kind == ObjSectionKind::Code && state.reverse_fn_order {
for symbol in section.symbols.iter().rev() {
if !symbol_matches_search(symbol, lower_search) {
continue;
}
symbol_ui(
ui,
symbol,
Some(section),
highlighted_symbol,
selected_symbol,
current_view,
config,
);
ret =
ret.or(symbol_ui(ui, symbol, Some(section), state, appearance));
}
} else {
for symbol in &section.symbols {
if !symbol_matches_search(symbol, lower_search) {
continue;
}
symbol_ui(
ui,
symbol,
Some(section),
highlighted_symbol,
selected_symbol,
current_view,
config,
);
ret =
ret.or(symbol_ui(ui, symbol, Some(section), state, appearance));
}
}
});
}
});
});
ret
}
fn build_log_ui(ui: &mut Ui, status: &BuildStatus) {
fn build_log_ui(ui: &mut Ui, status: &BuildStatus, appearance: &Appearance) {
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.colored_label(Color32::from_rgb(255, 0, 0), &status.log);
ui.colored_label(appearance.replace_color, &status.log);
});
});
}
pub fn symbol_diff_ui(ui: &mut Ui, view_state: &mut ViewState) {
let (Some(result), highlighted_symbol, selected_symbol, current_view, search) = (
&view_state.build,
&mut view_state.highlighted_symbol,
&mut view_state.selected_symbol,
&mut view_state.current_view,
&mut view_state.search,
) else {
pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appearance) {
let DiffViewState { build, current_view, symbol_state, search, .. } = state;
let Some(result) = build else {
return;
};
@@ -270,6 +322,14 @@ pub fn symbol_diff_ui(ui: &mut Ui, view_state: &mut ViewState) {
ui.colored_label(Rgba::from_rgb(1.0, 0.0, 0.0), "Fail");
}
});
ui.add_enabled(
!symbol_state.disable_reverse_fn_order,
egui::Checkbox::new(
&mut symbol_state.reverse_fn_order,
"Reverse function order (-inline deferred)",
),
);
},
);
},
@@ -277,6 +337,7 @@ pub fn symbol_diff_ui(ui: &mut Ui, view_state: &mut ViewState) {
ui.separator();
// Table
let mut ret = None;
let lower_search = search.to_ascii_lowercase();
StripBuilder::new(ui).size(Size::remainder()).vertical(|mut strip| {
strip.strip(|builder| {
@@ -285,18 +346,16 @@ pub fn symbol_diff_ui(ui: &mut Ui, view_state: &mut ViewState) {
ui.push_id("left", |ui| {
if result.first_status.success {
if let Some(obj) = &result.first_obj {
symbol_list_ui(
ret = ret.or(symbol_list_ui(
ui,
obj,
highlighted_symbol,
selected_symbol,
current_view,
symbol_state,
&lower_search,
&view_state.view_config,
);
appearance,
));
}
} else {
build_log_ui(ui, &result.first_status);
build_log_ui(ui, &result.first_status, appearance);
}
});
});
@@ -304,22 +363,24 @@ pub fn symbol_diff_ui(ui: &mut Ui, view_state: &mut ViewState) {
ui.push_id("right", |ui| {
if result.second_status.success {
if let Some(obj) = &result.second_obj {
symbol_list_ui(
ret = ret.or(symbol_list_ui(
ui,
obj,
highlighted_symbol,
selected_symbol,
current_view,
symbol_state,
&lower_search,
&view_state.view_config,
);
appearance,
));
}
} else {
build_log_ui(ui, &result.second_status);
build_log_ui(ui, &result.second_status, appearance);
}
});
});
});
});
});
if let Some(view) = ret {
*current_view = view;
}
}