Compare commits

...

34 Commits

Author SHA1 Message Date
c65e87c382 Version 2.5.0 2024-12-08 21:48:21 -07:00
1756b9f6c5 Repaint after view action 2024-12-08 21:42:33 -07:00
303f2938a2 Update dependencies 2024-12-08 21:40:13 -07:00
526e031251 Experimental objdiff-cli diff auto-rebuild 2024-12-08 21:40:13 -07:00
LagoLunatic
10b2a9c129 PPC: Display data values on hover for pools as well (#140)
* Fix missing dependency feature for objdiff-gui

* Update .gitignore

* PPC: Display data values on hover for pools as well

* Tooltip data display: Format floats and doubles better

Floats and doubles will now always be displayed with a decimal point and one digit after it, even if they are whole numbers. Floats will also have the f suffix. This is so you can tell the data type just by glancing at the value.

* Move big functions to bottom ppc.rs

* Clear pool relocs in volatile registers on function call

This fixes some false positives.

* Revert ObjArch API changes, add fake target symbol hack

Because we no longer have access to the actual symbol name via sections, guess_data_type can no longer detect the String data type for pooled references.

* Add hack to detect strings via the addi opcode

* Move hack to resolve placeholder symbol into process_code_symbol

* Merge reloc and fake_pool_reloc fields of ObjIns
2024-12-03 22:50:05 -07:00
LagoLunatic
abe68ef2f2 objdiff-gui: Implement keyboard shortcuts (#139)
* Fix missing dependency feature for objdiff-gui

* Update .gitignore

* Add enter and back hotkeys

* Add scroll hotkeys

* Add hotkeys to select the next symbol above/below the current one in the listing

* Do not clear highlighted symbol when backing out of diff view

* Do not clear highlighted symbol when hovering mouse over an unpaired symbol

* Auto-scroll the keyboard-selected symbols into view if offscreen

* Fix some hotkeys stealing input from focused widgets

e.g. The symbol list was stealing the W/S key presses when typing into the symbol filter text edit.

If the user actually wants to use these shortcuts while a widget is focused, they can simply press the escape key to unfocus all widgets and then press the shortcut.

* Add Ctrl+F/S shortcuts for focusing the object and symbol filter text edits

* Add space as alternative to enter hotkey

This is for consistency with egui's builtint enter/space hotkey for interacting with the focused widget.

* Add hotkeys to change target and base functions

* Split function diff view: Enable PageUp/PageDown/Home/End for scrolling

* Add escape as an alternative to back hotkey

* Fix auto-scrolling to highlighted symbol only working for the left side

The flag is cleared after one scroll to avoid doing it continuously, but this breaks when we need to scroll to both the left and the right symbol at the same time. So now each side has its own flag to keep track of this state independently.

* Simplify clearing of the autoscroll flag, remove &mut State

* Found a better place to clear the autoscroll flag

DiffViewState::post_update is where the flag gets set, so clearing it right before that at the start of the function seems to make the most sense, instead of doing it in App::update.
2024-12-02 21:51:37 -07:00
LagoLunatic
304df96411 Display decoded rlwinm info to hover tooltip (#141)
* Fix missing dependency feature for objdiff-gui

* Update .gitignore

* Display decoded rlwinm info to hover tooltip

* Remove trailing newline when displaying decoded rlwinm info

* Change variable name

* Also update variable name in rlwinm.rs
2024-12-02 21:40:05 -07:00
7aa878b48e Update all dependencies & clippy fixes 2024-12-01 22:22:35 -07:00
a119d9a6dd Add scratch preset_id field for decomp.me
Resolves #133
2024-11-07 09:27:13 -07:00
robojumper
ebf653816a Combine nested otherwise empty directories in objects view (#137) 2024-11-07 08:21:39 -07:00
424434edd6 Experimental ARM64 support
Based on yaxpeax-arm, but with a heavy dose of
custom code to work around its limitations.

Please report any issues or unhandled relocations.
2024-10-31 17:39:12 -06:00
7f14b684bf Ignore PlainText segments when diffing 2024-10-31 17:27:27 -06:00
c5da7f7dd5 Show diff color when symbols differ 2024-10-31 17:26:59 -06:00
2fd655850a Ignore Absolute relocations and log warning 2024-10-31 17:24:49 -06:00
79bd7317c1 Match BranchDest->Reloc with relaxed relocation diffs 2024-10-31 17:24:33 -06:00
21f8f2407c Relax symbol comparison logic
The Ghidra delinker plugin emits functions with type STT_OBJECT,
rather than STT_FUNC. The current logic was preventing these from
being compared based on their symbol type. Relax this condition
for now.
2024-10-29 22:46:02 -06:00
d2b7a9ef25 Fix missing common BSS symbols
Resolves #128
2024-10-28 17:54:49 -06:00
2cf9cf24d6 Version v2.3.3 2024-10-20 20:01:35 -07:00
Anghelo Carvajal
5ef3416457 Improve dependency gating on objdiff-core (#126)
* Reduce dependencies for no features

* Add missing deps to every feature

* Add missing `dep:`s

* Gate even more deps behind features

Removes dependency on tsify-next / wasm-bindgen unless
compiling with the wasm feature by using `#[cfg_attr]`

* Fix wasm

---------

Co-authored-by: Luke Street <luke@street.dev>
2024-10-20 19:04:29 -07:00
Aetias
6ff8d002f7 Fix panic when parsing DWARF 2 line info for empty section (#125)
* Fix panic when parsing DWARF 2 line info for empty section

* Fix panic when parsing DWARF 2 line info for empty section
May as well remove both unwraps :p
2024-10-19 09:39:18 -06:00
9ca157d717 Lighten default blue diff color
The old default was very dark and blended in with
the dark theme's background.
2024-10-18 17:51:29 -06:00
Steven Casper
67b63311fc Fix data tooltip panic (#123)
* Fix data tooltip panic

Prevents panicing when attempting to display the data tooltip for a symbol that is too large by just using as many bytes as needed from the begging of the symbol.

* Don't attempt to interpret wrongly sized data

* Reference data display improvment issue

* Log failure to display a symbol's value
2024-10-14 22:03:30 -06:00
72ea1c8911 ci: Use rust-lld on Windows 2024-10-12 18:57:49 -06:00
d4a540857d ci: Add Rust workspace cache 2024-10-12 18:41:42 -06:00
676488433f Fix resolving symbols for section-relative relocations
Also fixes MIPS `j` handling when jumping within the function.

Reworks `ObjReloc` struct to be a little more sensible.
2024-10-11 18:09:18 -06:00
83de98b5ee Version v2.3.1 2024-10-10 22:58:33 -06:00
c1ba4e91d1 ci: Setup python venv for cargo-zigbuild 2024-10-10 22:39:31 -06:00
575900024d Avoid resetting diff state on unit config reload 2024-10-10 22:31:04 -06:00
cbe299e859 Fix logic issue with 0-sized symbols
Fixes #119
2024-10-10 22:20:48 -06:00
741d93e211 Add symbol mapping feature (#118)
This allows users to "map" (or "link") symbols with different names so that they can be compared without having to update either the target or base objects. Symbol mappings are persisted in objdiff.json, so generators will need to ensure that they're preserved when updating. (Example: d1334bb79e)

Resolves #117
2024-10-09 21:44:18 -06:00
603dbd6882 Round match percent down before display
Ensures that 100% isn't displayed until it's a
perfect match.
2024-10-07 20:17:56 -06:00
6fb0a63de2 Click on empty space in row to clear highlight
Resolves #116
2024-10-07 19:53:16 -06:00
ab2e84a2c6 Deprioritize generated GCC symbols in find_section_symbol
Resolves #115
2024-10-07 19:49:52 -06:00
9596051cb4 Allow collapsing sidebar in symbols view 2024-10-07 19:46:16 -06:00
60 changed files with 8451 additions and 2962 deletions

5
.cargo/config.toml Normal file
View File

@@ -0,0 +1,5 @@
[target.x86_64-pc-windows-msvc]
linker = "rust-lld"
[target.aarch64-pc-windows-msvc]
linker = "rust-lld"

View File

@@ -30,6 +30,8 @@ jobs:
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
components: clippy components: clippy
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
- name: Cargo check - name: Cargo check
run: cargo check --all-features --all-targets run: cargo check --all-features --all-targets
- name: Cargo clippy - name: Cargo clippy
@@ -85,6 +87,8 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Rust toolchain - name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
- name: Cargo test - name: Cargo test
run: cargo test --release --all-features run: cargo test --release --all-features
@@ -142,11 +146,19 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install cargo-zigbuild - name: Install cargo-zigbuild
if: matrix.build == 'zigbuild' if: matrix.build == 'zigbuild'
run: pip install ziglang==0.13.0 cargo-zigbuild==0.19.1 run: |
python3 -m venv .venv
. .venv/bin/activate
echo PATH=$PATH >> $GITHUB_ENV
pip install ziglang==0.13.0 cargo-zigbuild==0.19.1
- name: Setup Rust toolchain - name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: Cargo build - name: Cargo build
run: > run: >
cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }} cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
@@ -198,6 +210,10 @@ jobs:
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: Cargo build - name: Cargo build
run: > run: >
cargo build --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }} cargo build --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}

6
.gitignore vendored
View File

@@ -3,10 +3,6 @@ target/
**/*.rs.bk **/*.rs.bk
generated/ generated/
# cargo-mobile
.cargo/
/gen
# macOS # macOS
.DS_Store .DS_Store
@@ -22,4 +18,4 @@ android.keystore
*.frag *.frag
*.vert *.vert
*.metal *.metal
.vscode/launch.json .vscode/

1298
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@ strip = "debuginfo"
codegen-units = 1 codegen-units = 1
[workspace.package] [workspace.package]
version = "2.2.2" version = "2.5.0"
authors = ["Luke Street <luke@street.dev>"] authors = ["Luke Street <luke@street.dev>"]
edition = "2021" edition = "2021"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"

View File

@@ -20,6 +20,7 @@ Supports:
- MIPS (N64, PS1, PS2, PSP) - MIPS (N64, PS1, PS2, PSP)
- x86 (COFF only at the moment) - x86 (COFF only at the moment)
- ARM (GBA, DS, 3DS) - ARM (GBA, DS, 3DS)
- ARM64 (Switch, experimental)
See [Usage](#usage) for more information. See [Usage](#usage) for more information.

View File

@@ -133,6 +133,13 @@
}, },
"metadata": { "metadata": {
"ref": "#/$defs/metadata" "ref": "#/$defs/metadata"
},
"symbol_mappings": {
"type": "object",
"description": "Manual symbol mappings from target to base.",
"additionalProperties": {
"type": "string"
}
} }
} }
}, },

View File

@@ -70,10 +70,10 @@ feature-depth = 1
# A list of advisory IDs to ignore. Note that ignored advisories will still # A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered. # output a note when they are encountered.
ignore = [ ignore = [
"RUSTSEC-2024-0370",
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" }, #{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish #"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" }, #{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
{ id = "RUSTSEC-2024-0384", reason = "Unmaintained indirect dependency" },
] ]
# If this is true, then cargo deny will use the git executable to fetch advisory database. # If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library. # If this is false, then it uses a built-in git library.
@@ -98,7 +98,7 @@ allow = [
"BSL-1.0", "BSL-1.0",
"CC0-1.0", "CC0-1.0",
"MPL-2.0", "MPL-2.0",
"Unicode-DFS-2016", "Unicode-3.0",
"Zlib", "Zlib",
"0BSD", "0BSD",
"OFL-1.1", "OFL-1.1",
@@ -240,7 +240,7 @@ allow-git = []
[sources.allow-org] [sources.allow-org]
# github.com organizations to allow git sources for # github.com organizations to allow git sources for
github = ["encounter"] github = ["notify-rs"]
# gitlab.com organizations to allow git sources for # gitlab.com organizations to allow git sources for
gitlab = [] gitlab = []
# bitbucket.org organizations to allow git sources for # bitbucket.org organizations to allow git sources for

View File

@@ -14,13 +14,13 @@ publish = false
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
argp = "0.3" argp = "0.4"
crossterm = "0.28" crossterm = "0.28"
enable-ansi-support = "0.2" enable-ansi-support = "0.2"
memmap2 = "0.9" memmap2 = "0.9"
objdiff-core = { path = "../objdiff-core", features = ["all"] } objdiff-core = { path = "../objdiff-core", features = ["all"] }
prost = "0.13" prost = "0.13"
ratatui = "0.28" ratatui = "0.29"
rayon = "1.10" rayon = "1.10"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"

View File

@@ -1,42 +1,50 @@
use std::{ use std::{
fs, fs,
io::stdout, io::stdout,
mem,
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr, str::FromStr,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
task::{Wake, Waker},
time::Duration,
}; };
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use argp::FromArgs; use argp::FromArgs;
use crossterm::{ use crossterm::{
event, event,
event::{ event::{DisableMouseCapture, EnableMouseCapture},
DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseButton,
MouseEventKind,
},
terminal::{ terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, SetTitle, disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, SetTitle,
}, },
}; };
use event::KeyModifiers;
use objdiff_core::{ use objdiff_core::{
bindings::diff::DiffResult, bindings::diff::DiffResult,
config::{ProjectConfig, ProjectObject}, build::{
watcher::{create_watcher, Watcher},
BuildConfig,
},
config::{build_globset, default_watch_patterns, ProjectConfig, ProjectObject},
diff, diff,
diff::{ diff::ObjDiff,
display::{display_diff, DiffText, HighlightKind}, jobs::{
DiffObjsResult, ObjDiff, ObjInsDiffKind, ObjSymbolDiff, objdiff::{start_build, ObjDiffConfig},
Job, JobQueue, JobResult,
}, },
obj, obj,
obj::{ObjInfo, ObjSectionKind, ObjSymbol, SymbolRef}, obj::ObjInfo,
};
use ratatui::{
prelude::*,
widgets::{Block, Borders, Clear, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
}; };
use ratatui::prelude::*;
use crate::util::{ use crate::{
util::{
output::{write_output, OutputFormat}, output::{write_output, OutputFormat},
term::crossterm_panic_handler, term::crossterm_panic_handler,
},
views::{function_diff::FunctionDiffUi, EventControlFlow, EventResult, UiView},
}; };
#[derive(FromArgs, PartialEq, Debug)] #[derive(FromArgs, PartialEq, Debug)]
@@ -102,7 +110,12 @@ pub fn run(args: Args) -> Result<()> {
let unit_path = let unit_path =
PathBuf::from_str(u).ok().and_then(|p| fs::canonicalize(p).ok()); PathBuf::from_str(u).ok().and_then(|p| fs::canonicalize(p).ok());
let Some(object) = project_config.objects.iter_mut().find_map(|obj| { let Some(object) = project_config
.units
.as_deref_mut()
.unwrap_or_default()
.iter_mut()
.find_map(|obj| {
if obj.name.as_deref() == Some(u) { if obj.name.as_deref() == Some(u) {
resolve_paths(obj); resolve_paths(obj);
return Some(obj); return Some(obj);
@@ -121,7 +134,8 @@ pub fn run(args: Args) -> Result<()> {
} }
None None
}) else { })
else {
bail!("Unit not found: {}", u) bail!("Unit not found: {}", u)
}; };
@@ -129,7 +143,13 @@ pub fn run(args: Args) -> Result<()> {
} else if let Some(symbol_name) = &args.symbol { } else if let Some(symbol_name) = &args.symbol {
let mut idx = None; let mut idx = None;
let mut count = 0usize; let mut count = 0usize;
for (i, obj) in project_config.objects.iter_mut().enumerate() { for (i, obj) in project_config
.units
.as_deref_mut()
.unwrap_or_default()
.iter_mut()
.enumerate()
{
resolve_paths(obj); resolve_paths(obj);
if obj if obj
@@ -148,7 +168,7 @@ pub fn run(args: Args) -> Result<()> {
} }
match (count, idx) { match (count, idx) {
(0, None) => bail!("Symbol not found: {}", symbol_name), (0, None) => bail!("Symbol not found: {}", symbol_name),
(1, Some(i)) => &mut project_config.objects[i], (1, Some(i)) => &mut project_config.units_mut()[i],
(2.., Some(_)) => bail!( (2.., Some(_)) => bail!(
"Multiple instances of {} were found, try specifying a unit", "Multiple instances of {} were found, try specifying a unit",
symbol_name symbol_name
@@ -197,6 +217,94 @@ fn run_oneshot(
Ok(()) Ok(())
} }
pub struct AppState {
pub jobs: JobQueue,
pub waker: Arc<TermWaker>,
pub project_dir: Option<PathBuf>,
pub project_config: Option<ProjectConfig>,
pub target_path: Option<PathBuf>,
pub base_path: Option<PathBuf>,
pub left_obj: Option<(ObjInfo, ObjDiff)>,
pub right_obj: Option<(ObjInfo, ObjDiff)>,
pub prev_obj: Option<(ObjInfo, ObjDiff)>,
pub reload_time: Option<time::OffsetDateTime>,
pub time_format: Vec<time::format_description::FormatItem<'static>>,
pub relax_reloc_diffs: bool,
pub watcher: Option<Watcher>,
pub modified: Arc<AtomicBool>,
}
fn create_objdiff_config(state: &AppState) -> ObjDiffConfig {
ObjDiffConfig {
build_config: BuildConfig {
project_dir: state.project_dir.clone(),
custom_make: state
.project_config
.as_ref()
.and_then(|c| c.custom_make.as_ref())
.cloned(),
custom_args: state
.project_config
.as_ref()
.and_then(|c| c.custom_args.as_ref())
.cloned(),
selected_wsl_distro: None,
},
build_base: state.project_config.as_ref().is_some_and(|p| p.build_base.unwrap_or(true)),
build_target: state
.project_config
.as_ref()
.is_some_and(|p| p.build_target.unwrap_or(false)),
target_path: state.target_path.clone(),
base_path: state.base_path.clone(),
diff_obj_config: diff::DiffObjConfig {
relax_reloc_diffs: state.relax_reloc_diffs,
..Default::default() // TODO
},
symbol_mappings: Default::default(),
selecting_left: None,
selecting_right: None,
}
}
impl AppState {
fn reload(&mut self) -> Result<()> {
let config = create_objdiff_config(self);
self.jobs.push_once(Job::ObjDiff, || start_build(Waker::from(self.waker.clone()), config));
Ok(())
}
fn check_jobs(&mut self) -> Result<bool> {
let mut redraw = false;
self.jobs.collect_results();
for result in mem::take(&mut self.jobs.results) {
match result {
JobResult::None => unreachable!("Unexpected JobResult::None"),
JobResult::ObjDiff(result) => {
let result = result.unwrap();
self.left_obj = result.first_obj;
self.right_obj = result.second_obj;
self.reload_time = Some(result.time);
redraw = true;
}
JobResult::CheckUpdate(_) => todo!("CheckUpdate"),
JobResult::Update(_) => todo!("Update"),
JobResult::CreateScratch(_) => todo!("CreateScratch"),
}
}
Ok(redraw)
}
}
#[derive(Default)]
pub struct TermWaker(pub AtomicBool);
impl Wake for TermWaker {
fn wake(self: Arc<Self>) { self.0.store(true, Ordering::Relaxed); }
fn wake_by_ref(self: &Arc<Self>) { self.0.store(true, Ordering::Relaxed); }
}
fn run_interactive( fn run_interactive(
args: Args, args: Args,
target_path: Option<PathBuf>, target_path: Option<PathBuf>,
@@ -206,32 +314,38 @@ fn run_interactive(
let Some(symbol_name) = &args.symbol else { bail!("Interactive mode requires a symbol name") }; let Some(symbol_name) = &args.symbol else { bail!("Interactive mode requires a symbol name") };
let time_format = time::format_description::parse_borrowed::<2>("[hour]:[minute]:[second]") let time_format = time::format_description::parse_borrowed::<2>("[hour]:[minute]:[second]")
.context("Failed to parse time format")?; .context("Failed to parse time format")?;
let mut state = Box::new(FunctionDiffUi { let mut state = AppState {
relax_reloc_diffs: args.relax_reloc_diffs, jobs: Default::default(),
left_highlight: HighlightKind::None, waker: Default::default(),
right_highlight: HighlightKind::None, project_dir: args.project.clone(),
scroll_x: 0, project_config,
scroll_state_x: ScrollbarState::default(),
scroll_y: 0,
scroll_state_y: ScrollbarState::default(),
per_page: 0,
num_rows: 0,
symbol_name: symbol_name.clone(),
target_path, target_path,
base_path, base_path,
project_config,
left_obj: None, left_obj: None,
right_obj: None, right_obj: None,
prev_obj: None, prev_obj: None,
diff_result: DiffObjsResult::default(),
left_sym: None,
right_sym: None,
prev_sym: None,
reload_time: None, reload_time: None,
time_format, time_format,
open_options: false, relax_reloc_diffs: args.relax_reloc_diffs,
three_way: false, watcher: None,
}); modified: Default::default(),
};
if let Some(project_dir) = &state.project_dir {
let watch_patterns = state
.project_config
.as_ref()
.and_then(|c| c.watch_patterns.as_ref())
.cloned()
.unwrap_or_else(default_watch_patterns);
state.watcher = Some(create_watcher(
state.modified.clone(),
project_dir,
build_globset(&watch_patterns)?,
Waker::from(state.waker.clone()),
)?);
}
let mut view: Box<dyn UiView> =
Box::new(FunctionDiffUi { symbol_name: symbol_name.clone(), ..Default::default() });
state.reload()?; state.reload()?;
crossterm_panic_handler(); crossterm_panic_handler();
@@ -245,16 +359,12 @@ fn run_interactive(
let backend = CrosstermBackend::new(stdout()); let backend = CrosstermBackend::new(stdout());
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend)?;
'outer: loop {
let mut result = EventResult { redraw: true, ..Default::default() }; let mut result = EventResult { redraw: true, ..Default::default() };
loop { 'outer: loop {
if result.redraw { if result.redraw {
terminal.draw(|f| loop { terminal.draw(|f| loop {
result.redraw = false; result.redraw = false;
state.draw(f, &mut result); view.draw(&state, f, &mut result);
if state.open_options {
state.draw_options(f, &mut result);
}
result.click_xy = None; result.click_xy = None;
if !result.redraw { if !result.redraw {
break; break;
@@ -263,13 +373,29 @@ fn run_interactive(
f.buffer_mut().reset(); f.buffer_mut().reset();
})?; })?;
} }
match state.handle_event(event::read()?) { loop {
if event::poll(Duration::from_millis(100))? {
match view.handle_event(&mut state, event::read()?) {
EventControlFlow::Break => break 'outer, EventControlFlow::Break => break 'outer,
EventControlFlow::Continue(r) => result = r, EventControlFlow::Continue(r) => result = r,
EventControlFlow::Reload => break, EventControlFlow::Reload => {
}
}
state.reload()?; state.reload()?;
result.redraw = true;
}
}
break;
} else if state.waker.0.swap(false, Ordering::Relaxed) {
if state.modified.swap(false, Ordering::Relaxed) {
state.reload()?;
}
result.redraw = true;
break;
}
}
if state.check_jobs()? {
result.redraw = true;
view.reload(&state)?;
}
} }
// Reset terminal // Reset terminal
@@ -278,676 +404,3 @@ fn run_interactive(
terminal.show_cursor()?; terminal.show_cursor()?;
Ok(()) Ok(())
} }
#[inline]
fn get_symbol(obj: Option<&ObjInfo>, sym: Option<SymbolRef>) -> Option<&ObjSymbol> {
Some(obj?.section_symbol(sym?).1)
}
#[inline]
fn get_symbol_diff(obj: Option<&ObjDiff>, sym: Option<SymbolRef>) -> Option<&ObjSymbolDiff> {
Some(obj?.symbol_diff(sym?))
}
fn find_function(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
for (section_idx, section) in obj.sections.iter().enumerate() {
if section.kind != ObjSectionKind::Code {
continue;
}
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
if symbol.name == name {
return Some(SymbolRef { section_idx, symbol_idx });
}
}
}
None
}
#[allow(dead_code)]
struct FunctionDiffUi {
relax_reloc_diffs: bool,
left_highlight: HighlightKind,
right_highlight: HighlightKind,
scroll_x: usize,
scroll_state_x: ScrollbarState,
scroll_y: usize,
scroll_state_y: ScrollbarState,
per_page: usize,
num_rows: usize,
symbol_name: String,
target_path: Option<PathBuf>,
base_path: Option<PathBuf>,
project_config: Option<ProjectConfig>,
left_obj: Option<ObjInfo>,
right_obj: Option<ObjInfo>,
prev_obj: Option<ObjInfo>,
diff_result: DiffObjsResult,
left_sym: Option<SymbolRef>,
right_sym: Option<SymbolRef>,
prev_sym: Option<SymbolRef>,
reload_time: Option<time::OffsetDateTime>,
time_format: Vec<time::format_description::FormatItem<'static>>,
open_options: bool,
three_way: bool,
}
#[derive(Default)]
struct EventResult {
redraw: bool,
click_xy: Option<(u16, u16)>,
}
enum EventControlFlow {
Break,
Continue(EventResult),
Reload,
}
impl FunctionDiffUi {
fn draw(&mut self, f: &mut Frame, result: &mut EventResult) {
let chunks = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).split(f.area());
let header_chunks = Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(2),
])
.split(chunks[0]);
let content_chunks = if self.three_way {
Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(2),
])
.split(chunks[1])
} else {
Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(2),
])
.split(chunks[1])
};
self.per_page = chunks[1].height.saturating_sub(2) as usize;
let max_scroll_y = self.num_rows.saturating_sub(self.per_page);
if self.scroll_y > max_scroll_y {
self.scroll_y = max_scroll_y;
}
self.scroll_state_y =
self.scroll_state_y.content_length(max_scroll_y).position(self.scroll_y);
let mut line_l = Line::default();
line_l
.spans
.push(Span::styled(self.symbol_name.clone(), Style::new().fg(Color::White).bold()));
f.render_widget(line_l, header_chunks[0]);
let mut line_r = Line::default();
if let Some(percent) = get_symbol_diff(self.diff_result.right.as_ref(), self.right_sym)
.and_then(|s| s.match_percent)
{
line_r.spans.push(Span::styled(
format!("{:.2}% ", percent),
Style::new().fg(match_percent_color(percent)),
));
}
let reload_time = self
.reload_time
.as_ref()
.and_then(|t| t.format(&self.time_format).ok())
.unwrap_or_else(|| "N/A".to_string());
line_r.spans.push(Span::styled(
format!("Last reload: {}", reload_time),
Style::new().fg(Color::White),
));
f.render_widget(line_r, header_chunks[2]);
let mut left_text = None;
let mut left_highlight = None;
let mut max_width = 0;
if let (Some(symbol), Some(symbol_diff)) = (
get_symbol(self.left_obj.as_ref(), self.left_sym),
get_symbol_diff(self.diff_result.left.as_ref(), self.left_sym),
) {
let mut text = Text::default();
let rect = content_chunks[0].inner(Margin::new(0, 1));
left_highlight = self.print_sym(
&mut text,
symbol,
symbol_diff,
rect,
&self.left_highlight,
result,
false,
);
max_width = max_width.max(text.width());
left_text = Some(text);
}
let mut right_text = None;
let mut right_highlight = None;
let mut margin_text = None;
if let (Some(symbol), Some(symbol_diff)) = (
get_symbol(self.right_obj.as_ref(), self.right_sym),
get_symbol_diff(self.diff_result.right.as_ref(), self.right_sym),
) {
let mut text = Text::default();
let rect = content_chunks[2].inner(Margin::new(0, 1));
right_highlight = self.print_sym(
&mut text,
symbol,
symbol_diff,
rect,
&self.right_highlight,
result,
false,
);
max_width = max_width.max(text.width());
right_text = Some(text);
// Render margin
let mut text = Text::default();
let rect = content_chunks[1].inner(Margin::new(1, 1));
self.print_margin(&mut text, symbol_diff, rect);
margin_text = Some(text);
}
let mut prev_text = None;
let mut prev_margin_text = None;
if self.three_way {
if let (Some(symbol), Some(symbol_diff)) = (
get_symbol(self.prev_obj.as_ref(), self.prev_sym),
get_symbol_diff(self.diff_result.prev.as_ref(), self.prev_sym),
) {
let mut text = Text::default();
let rect = content_chunks[4].inner(Margin::new(0, 1));
self.print_sym(
&mut text,
symbol,
symbol_diff,
rect,
&self.right_highlight,
result,
true,
);
max_width = max_width.max(text.width());
prev_text = Some(text);
// Render margin
let mut text = Text::default();
let rect = content_chunks[3].inner(Margin::new(1, 1));
self.print_margin(&mut text, symbol_diff, rect);
prev_margin_text = Some(text);
}
}
let max_scroll_x =
max_width.saturating_sub(content_chunks[0].width.min(content_chunks[2].width) as usize);
if self.scroll_x > max_scroll_x {
self.scroll_x = max_scroll_x;
}
self.scroll_state_x =
self.scroll_state_x.content_length(max_scroll_x).position(self.scroll_x);
if let Some(text) = left_text {
// Render left column
f.render_widget(
Paragraph::new(text)
.block(
Block::new()
.borders(Borders::TOP)
.border_style(Style::new().fg(Color::Gray))
.title_style(Style::new().bold())
.title("TARGET"),
)
.scroll((0, self.scroll_x as u16)),
content_chunks[0],
);
}
if let Some(text) = margin_text {
f.render_widget(text, content_chunks[1].inner(Margin::new(1, 1)));
}
if let Some(text) = right_text {
f.render_widget(
Paragraph::new(text)
.block(
Block::new()
.borders(Borders::TOP)
.border_style(Style::new().fg(Color::Gray))
.title_style(Style::new().bold())
.title("CURRENT"),
)
.scroll((0, self.scroll_x as u16)),
content_chunks[2],
);
}
if self.three_way {
if let Some(text) = prev_margin_text {
f.render_widget(text, content_chunks[3].inner(Margin::new(1, 1)));
}
let block = Block::new()
.borders(Borders::TOP)
.border_style(Style::new().fg(Color::Gray))
.title_style(Style::new().bold())
.title("SAVED");
if let Some(text) = prev_text {
f.render_widget(
Paragraph::new(text).block(block.clone()).scroll((0, self.scroll_x as u16)),
content_chunks[4],
);
} else {
f.render_widget(block, content_chunks[4]);
}
}
// Render scrollbars
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight).begin_symbol(None).end_symbol(None),
chunks[1].inner(Margin::new(0, 1)),
&mut self.scroll_state_y,
);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol(""),
content_chunks[0],
&mut self.scroll_state_x,
);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol(""),
content_chunks[2],
&mut self.scroll_state_x,
);
if self.three_way {
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol(""),
content_chunks[4],
&mut self.scroll_state_x,
);
}
if let Some(new_highlight) = left_highlight {
if new_highlight == self.left_highlight {
if self.left_highlight != self.right_highlight {
self.right_highlight = self.left_highlight.clone();
} else {
self.left_highlight = HighlightKind::None;
self.right_highlight = HighlightKind::None;
}
} else {
self.left_highlight = new_highlight;
}
result.redraw = true;
} else if let Some(new_highlight) = right_highlight {
if new_highlight == self.right_highlight {
if self.left_highlight != self.right_highlight {
self.left_highlight = self.right_highlight.clone();
} else {
self.left_highlight = HighlightKind::None;
self.right_highlight = HighlightKind::None;
}
} else {
self.right_highlight = new_highlight;
}
result.redraw = true;
}
}
fn draw_options(&mut self, f: &mut Frame, _result: &mut EventResult) {
let percent_x = 50;
let percent_y = 50;
let popup_rect = Layout::vertical([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(f.area())[1];
let popup_rect = Layout::horizontal([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_rect)[1];
let popup = Block::default()
.borders(Borders::ALL)
.title("Options")
.title_style(Style::default().fg(Color::White).bg(Color::Black));
f.render_widget(Clear, popup_rect);
f.render_widget(popup, popup_rect);
}
fn handle_event(&mut self, event: Event) -> EventControlFlow {
let mut result = EventResult::default();
match event {
Event::Key(event)
if matches!(event.kind, KeyEventKind::Press | KeyEventKind::Repeat) =>
{
match event.code {
// Quit
KeyCode::Esc | KeyCode::Char('q') => return EventControlFlow::Break,
// Page up
KeyCode::PageUp => {
self.page_up(false);
result.redraw = true;
}
// Page up (shift + space)
KeyCode::Char(' ') if event.modifiers.contains(KeyModifiers::SHIFT) => {
self.page_up(false);
result.redraw = true;
}
// Page down
KeyCode::Char(' ') | KeyCode::PageDown => {
self.page_down(false);
result.redraw = true;
}
// Page down (ctrl + f)
KeyCode::Char('f') if event.modifiers.contains(KeyModifiers::CONTROL) => {
self.page_down(false);
result.redraw = true;
}
// Page up (ctrl + b)
KeyCode::Char('b') if event.modifiers.contains(KeyModifiers::CONTROL) => {
self.page_up(false);
result.redraw = true;
}
// Half page down (ctrl + d)
KeyCode::Char('d') if event.modifiers.contains(KeyModifiers::CONTROL) => {
self.page_down(true);
result.redraw = true;
}
// Half page up (ctrl + u)
KeyCode::Char('u') if event.modifiers.contains(KeyModifiers::CONTROL) => {
self.page_up(true);
result.redraw = true;
}
// Scroll down
KeyCode::Down | KeyCode::Char('j') => {
self.scroll_y += 1;
result.redraw = true;
}
// Scroll up
KeyCode::Up | KeyCode::Char('k') => {
self.scroll_y = self.scroll_y.saturating_sub(1);
result.redraw = true;
}
// Scroll to start
KeyCode::Char('g') => {
self.scroll_y = 0;
result.redraw = true;
}
// Scroll to end
KeyCode::Char('G') => {
self.scroll_y = self.num_rows;
result.redraw = true;
}
// Reload
KeyCode::Char('r') => {
result.redraw = true;
return EventControlFlow::Reload;
}
// Scroll right
KeyCode::Right | KeyCode::Char('l') => {
self.scroll_x += 1;
result.redraw = true;
}
// Scroll left
KeyCode::Left | KeyCode::Char('h') => {
self.scroll_x = self.scroll_x.saturating_sub(1);
result.redraw = true;
}
// Toggle relax relocation diffs
KeyCode::Char('x') => {
self.relax_reloc_diffs = !self.relax_reloc_diffs;
result.redraw = true;
return EventControlFlow::Reload;
}
// Toggle three-way diff
KeyCode::Char('3') => {
self.three_way = !self.three_way;
result.redraw = true;
}
// Toggle options
KeyCode::Char('o') => {
self.open_options = !self.open_options;
result.redraw = true;
}
_ => {}
}
}
Event::Mouse(event) => match event.kind {
MouseEventKind::ScrollDown => {
self.scroll_y += 3;
result.redraw = true;
}
MouseEventKind::ScrollUp => {
self.scroll_y = self.scroll_y.saturating_sub(3);
result.redraw = true;
}
MouseEventKind::ScrollRight => {
self.scroll_x += 3;
result.redraw = true;
}
MouseEventKind::ScrollLeft => {
self.scroll_x = self.scroll_x.saturating_sub(3);
result.redraw = true;
}
MouseEventKind::Down(MouseButton::Left) => {
result.click_xy = Some((event.column, event.row));
result.redraw = true;
}
_ => {}
},
Event::Resize(_, _) => {
result.redraw = true;
}
_ => {}
}
EventControlFlow::Continue(result)
}
fn page_up(&mut self, half: bool) {
self.scroll_y = self.scroll_y.saturating_sub(self.per_page / if half { 2 } else { 1 });
}
fn page_down(&mut self, half: bool) {
self.scroll_y += self.per_page / if half { 2 } else { 1 };
}
#[allow(clippy::too_many_arguments)]
fn print_sym(
&self,
out: &mut Text<'static>,
symbol: &ObjSymbol,
symbol_diff: &ObjSymbolDiff,
rect: Rect,
highlight: &HighlightKind,
result: &EventResult,
only_changed: bool,
) -> Option<HighlightKind> {
let base_addr = symbol.address;
let mut new_highlight = None;
for (y, ins_diff) in symbol_diff
.instructions
.iter()
.skip(self.scroll_y)
.take(rect.height as usize)
.enumerate()
{
if only_changed && ins_diff.kind == ObjInsDiffKind::None {
out.lines.push(Line::default());
continue;
}
let mut sx = rect.x;
let sy = rect.y + y as u16;
let mut line = Line::default();
display_diff(ins_diff, base_addr, |text| -> Result<()> {
let label_text;
let mut base_color = match ins_diff.kind {
ObjInsDiffKind::None
| ObjInsDiffKind::OpMismatch
| ObjInsDiffKind::ArgMismatch => Color::Gray,
ObjInsDiffKind::Replace => Color::Cyan,
ObjInsDiffKind::Delete => Color::Red,
ObjInsDiffKind::Insert => Color::Green,
};
let mut pad_to = 0;
match text {
DiffText::Basic(text) => {
label_text = text.to_string();
}
DiffText::BasicColor(s, idx) => {
label_text = s.to_string();
base_color = COLOR_ROTATION[idx % COLOR_ROTATION.len()];
}
DiffText::Line(num) => {
label_text = format!("{num} ");
base_color = Color::DarkGray;
pad_to = 5;
}
DiffText::Address(addr) => {
label_text = format!("{:x}:", addr);
pad_to = 5;
}
DiffText::Opcode(mnemonic, _op) => {
label_text = mnemonic.to_string();
if ins_diff.kind == ObjInsDiffKind::OpMismatch {
base_color = Color::Blue;
}
pad_to = 8;
}
DiffText::Argument(arg, diff) => {
label_text = arg.to_string();
if let Some(diff) = diff {
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
}
}
DiffText::BranchDest(addr, diff) => {
label_text = format!("{addr:x}");
if let Some(diff) = diff {
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
}
}
DiffText::Symbol(sym) => {
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
label_text = name.clone();
base_color = Color::White;
}
DiffText::Spacing(n) => {
line.spans.push(Span::raw(" ".repeat(n)));
sx += n as u16;
return Ok(());
}
DiffText::Eol => {
return Ok(());
}
}
let len = label_text.len();
let highlighted = *highlight == text;
if let Some((cx, cy)) = result.click_xy {
if cx >= sx && cx < sx + len as u16 && cy == sy {
new_highlight = Some(text.into());
}
}
let mut style = Style::new().fg(base_color);
if highlighted {
style = style.bg(Color::DarkGray);
}
line.spans.push(Span::styled(label_text, style));
sx += len as u16;
if pad_to > len {
let pad = (pad_to - len) as u16;
line.spans.push(Span::raw(" ".repeat(pad as usize)));
sx += pad;
}
Ok(())
})
.unwrap();
out.lines.push(line);
}
new_highlight
}
fn print_margin(&self, out: &mut Text, symbol: &ObjSymbolDiff, rect: Rect) {
for ins_diff in symbol.instructions.iter().skip(self.scroll_y).take(rect.height as usize) {
if ins_diff.kind != ObjInsDiffKind::None {
out.lines.push(Line::raw(match ins_diff.kind {
ObjInsDiffKind::Delete => "<",
ObjInsDiffKind::Insert => ">",
_ => "|",
}));
} else {
out.lines.push(Line::raw(" "));
}
}
}
fn reload(&mut self) -> Result<()> {
let prev = self.right_obj.take();
let config = diff::DiffObjConfig {
relax_reloc_diffs: self.relax_reloc_diffs,
..Default::default() // TODO
};
let target = self
.target_path
.as_deref()
.map(|p| {
obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display()))
})
.transpose()?;
let base = self
.base_path
.as_deref()
.map(|p| {
obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display()))
})
.transpose()?;
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), prev.as_ref())?;
let left_sym = target.as_ref().and_then(|o| find_function(o, &self.symbol_name));
let right_sym = base.as_ref().and_then(|o| find_function(o, &self.symbol_name));
let prev_sym = prev.as_ref().and_then(|o| find_function(o, &self.symbol_name));
self.num_rows = match (
get_symbol_diff(result.left.as_ref(), left_sym),
get_symbol_diff(result.right.as_ref(), right_sym),
) {
(Some(l), Some(r)) => l.instructions.len().max(r.instructions.len()),
(Some(l), None) => l.instructions.len(),
(None, Some(r)) => r.instructions.len(),
(None, None) => bail!("Symbol not found: {}", self.symbol_name),
};
self.left_obj = target;
self.right_obj = base;
self.prev_obj = prev;
self.diff_result = result;
self.left_sym = left_sym;
self.right_sym = right_sym;
self.prev_sym = prev_sym;
self.reload_time = time::OffsetDateTime::now_local().ok();
Ok(())
}
}
pub const COLOR_ROTATION: [Color; 7] = [
Color::Magenta,
Color::Cyan,
Color::Green,
Color::Red,
Color::Yellow,
Color::Blue,
Color::Green,
];
pub fn match_percent_color(match_percent: f32) -> Color {
if match_percent == 100.0 {
Color::Green
} else if match_percent >= 50.0 {
Color::LightBlue
} else {
Color::LightRed
}
}

View File

@@ -94,7 +94,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
}; };
info!( info!(
"Generating report for {} units (using {} threads)", "Generating report for {} units (using {} threads)",
project.objects.len(), project.units().len(),
if args.deduplicate { 1 } else { rayon::current_num_threads() } if args.deduplicate { 1 } else { rayon::current_num_threads() }
); );
@@ -103,7 +103,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
let mut existing_functions: HashSet<String> = HashSet::new(); let mut existing_functions: HashSet<String> = HashSet::new();
if args.deduplicate { if args.deduplicate {
// If deduplicating, we need to run single-threaded // If deduplicating, we need to run single-threaded
for object in &mut project.objects { for object in project.units.as_deref_mut().unwrap_or_default() {
if let Some(unit) = report_object( if let Some(unit) = report_object(
object, object,
project_dir, project_dir,
@@ -116,7 +116,9 @@ fn generate(args: GenerateArgs) -> Result<()> {
} }
} else { } else {
let vec = project let vec = project
.objects .units
.as_deref_mut()
.unwrap_or_default()
.par_iter_mut() .par_iter_mut()
.map(|object| { .map(|object| {
report_object( report_object(
@@ -132,7 +134,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
} }
let measures = units.iter().flat_map(|u| u.measures.into_iter()).collect(); let measures = units.iter().flat_map(|u| u.measures.into_iter()).collect();
let mut categories = Vec::new(); let mut categories = Vec::new();
for category in &project.progress_categories { for category in project.progress_categories() {
categories.push(ReportCategory { categories.push(ReportCategory {
id: category.id.clone(), id: category.id.clone(),
name: category.name.clone(), name: category.name.clone(),

View File

@@ -1,6 +1,7 @@
mod argp_version; mod argp_version;
mod cmd; mod cmd;
mod util; mod util;
mod views;
// musl's allocator is very slow, so use mimalloc when targeting musl. // musl's allocator is very slow, so use mimalloc when targeting musl.
// Otherwise, use the system allocator to avoid extra code size. // Otherwise, use the system allocator to avoid extra code size.

View File

@@ -0,0 +1,652 @@
use anyhow::{bail, Result};
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind};
use objdiff_core::{
diff::{
display::{display_diff, DiffText, HighlightKind},
ObjDiff, ObjInsDiffKind, ObjSymbolDiff,
},
obj::{ObjInfo, ObjSectionKind, ObjSymbol, SymbolRef},
};
use ratatui::{
prelude::*,
widgets::{Block, Borders, Clear, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
Frame,
};
use super::{EventControlFlow, EventResult, UiView};
use crate::cmd::diff::AppState;
#[allow(dead_code)]
#[derive(Default)]
pub struct FunctionDiffUi {
pub symbol_name: String,
pub left_highlight: HighlightKind,
pub right_highlight: HighlightKind,
pub scroll_x: usize,
pub scroll_state_x: ScrollbarState,
pub scroll_y: usize,
pub scroll_state_y: ScrollbarState,
pub per_page: usize,
pub num_rows: usize,
pub left_sym: Option<SymbolRef>,
pub right_sym: Option<SymbolRef>,
pub prev_sym: Option<SymbolRef>,
pub open_options: bool,
pub three_way: bool,
}
impl UiView for FunctionDiffUi {
fn draw(&mut self, state: &AppState, f: &mut Frame, result: &mut EventResult) {
let chunks = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).split(f.area());
let header_chunks = Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(2),
])
.split(chunks[0]);
let content_chunks = if self.three_way {
Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(2),
])
.split(chunks[1])
} else {
Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(2),
])
.split(chunks[1])
};
self.per_page = chunks[1].height.saturating_sub(2) as usize;
let max_scroll_y = self.num_rows.saturating_sub(self.per_page);
if self.scroll_y > max_scroll_y {
self.scroll_y = max_scroll_y;
}
self.scroll_state_y =
self.scroll_state_y.content_length(max_scroll_y).position(self.scroll_y);
let mut line_l = Line::default();
line_l
.spans
.push(Span::styled(self.symbol_name.clone(), Style::new().fg(Color::White).bold()));
f.render_widget(line_l, header_chunks[0]);
let mut line_r = Line::default();
if let Some(percent) =
get_symbol(state.right_obj.as_ref(), self.right_sym).and_then(|(_, d)| d.match_percent)
{
line_r.spans.push(Span::styled(
format!("{:.2}% ", percent),
Style::new().fg(match_percent_color(percent)),
));
}
let reload_time = state
.reload_time
.as_ref()
.and_then(|t| t.format(&state.time_format).ok())
.unwrap_or_else(|| "N/A".to_string());
line_r.spans.push(Span::styled(
format!("Last reload: {}", reload_time),
Style::new().fg(Color::White),
));
line_r.spans.push(Span::styled(
format!(" ({} jobs)", state.jobs.jobs.len()),
Style::new().fg(Color::LightYellow),
));
f.render_widget(line_r, header_chunks[2]);
let mut left_text = None;
let mut left_highlight = None;
let mut max_width = 0;
if let Some((symbol, symbol_diff)) = get_symbol(state.left_obj.as_ref(), self.left_sym) {
let mut text = Text::default();
let rect = content_chunks[0].inner(Margin::new(0, 1));
left_highlight = self.print_sym(
&mut text,
symbol,
symbol_diff,
rect,
&self.left_highlight,
result,
false,
);
max_width = max_width.max(text.width());
left_text = Some(text);
}
let mut right_text = None;
let mut right_highlight = None;
let mut margin_text = None;
if let Some((symbol, symbol_diff)) = get_symbol(state.right_obj.as_ref(), self.right_sym) {
let mut text = Text::default();
let rect = content_chunks[2].inner(Margin::new(0, 1));
right_highlight = self.print_sym(
&mut text,
symbol,
symbol_diff,
rect,
&self.right_highlight,
result,
false,
);
max_width = max_width.max(text.width());
right_text = Some(text);
// Render margin
let mut text = Text::default();
let rect = content_chunks[1].inner(Margin::new(1, 1));
self.print_margin(&mut text, symbol_diff, rect);
margin_text = Some(text);
}
let mut prev_text = None;
let mut prev_margin_text = None;
if self.three_way {
if let Some((symbol, symbol_diff)) = get_symbol(state.prev_obj.as_ref(), self.prev_sym)
{
let mut text = Text::default();
let rect = content_chunks[4].inner(Margin::new(0, 1));
self.print_sym(
&mut text,
symbol,
symbol_diff,
rect,
&self.right_highlight,
result,
true,
);
max_width = max_width.max(text.width());
prev_text = Some(text);
// Render margin
let mut text = Text::default();
let rect = content_chunks[3].inner(Margin::new(1, 1));
self.print_margin(&mut text, symbol_diff, rect);
prev_margin_text = Some(text);
}
}
let max_scroll_x =
max_width.saturating_sub(content_chunks[0].width.min(content_chunks[2].width) as usize);
if self.scroll_x > max_scroll_x {
self.scroll_x = max_scroll_x;
}
self.scroll_state_x =
self.scroll_state_x.content_length(max_scroll_x).position(self.scroll_x);
if let Some(text) = left_text {
// Render left column
f.render_widget(
Paragraph::new(text)
.block(
Block::new()
.borders(Borders::TOP)
.border_style(Style::new().fg(Color::Gray))
.title_style(Style::new().bold())
.title("TARGET"),
)
.scroll((0, self.scroll_x as u16)),
content_chunks[0],
);
}
if let Some(text) = margin_text {
f.render_widget(text, content_chunks[1].inner(Margin::new(1, 1)));
}
if let Some(text) = right_text {
f.render_widget(
Paragraph::new(text)
.block(
Block::new()
.borders(Borders::TOP)
.border_style(Style::new().fg(Color::Gray))
.title_style(Style::new().bold())
.title("CURRENT"),
)
.scroll((0, self.scroll_x as u16)),
content_chunks[2],
);
}
if self.three_way {
if let Some(text) = prev_margin_text {
f.render_widget(text, content_chunks[3].inner(Margin::new(1, 1)));
}
let block = Block::new()
.borders(Borders::TOP)
.border_style(Style::new().fg(Color::Gray))
.title_style(Style::new().bold())
.title("SAVED");
if let Some(text) = prev_text {
f.render_widget(
Paragraph::new(text).block(block.clone()).scroll((0, self.scroll_x as u16)),
content_chunks[4],
);
} else {
f.render_widget(block, content_chunks[4]);
}
}
// Render scrollbars
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight).begin_symbol(None).end_symbol(None),
chunks[1].inner(Margin::new(0, 1)),
&mut self.scroll_state_y,
);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol(""),
content_chunks[0],
&mut self.scroll_state_x,
);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol(""),
content_chunks[2],
&mut self.scroll_state_x,
);
if self.three_way {
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol(""),
content_chunks[4],
&mut self.scroll_state_x,
);
}
if let Some(new_highlight) = left_highlight {
if new_highlight == self.left_highlight {
if self.left_highlight != self.right_highlight {
self.right_highlight = self.left_highlight.clone();
} else {
self.left_highlight = HighlightKind::None;
self.right_highlight = HighlightKind::None;
}
} else {
self.left_highlight = new_highlight;
}
result.redraw = true;
} else if let Some(new_highlight) = right_highlight {
if new_highlight == self.right_highlight {
if self.left_highlight != self.right_highlight {
self.left_highlight = self.right_highlight.clone();
} else {
self.left_highlight = HighlightKind::None;
self.right_highlight = HighlightKind::None;
}
} else {
self.right_highlight = new_highlight;
}
result.redraw = true;
}
if self.open_options {
self.draw_options(f, result);
}
}
fn handle_event(&mut self, state: &mut AppState, event: Event) -> EventControlFlow {
let mut result = EventResult::default();
match event {
Event::Key(event)
if matches!(event.kind, KeyEventKind::Press | KeyEventKind::Repeat) =>
{
match event.code {
// Quit
KeyCode::Esc | KeyCode::Char('q') => return EventControlFlow::Break,
// Page up
KeyCode::PageUp => {
self.page_up(false);
result.redraw = true;
}
// Page up (shift + space)
KeyCode::Char(' ') if event.modifiers.contains(KeyModifiers::SHIFT) => {
self.page_up(false);
result.redraw = true;
}
// Page down
KeyCode::Char(' ') | KeyCode::PageDown => {
self.page_down(false);
result.redraw = true;
}
// Page down (ctrl + f)
KeyCode::Char('f') if event.modifiers.contains(KeyModifiers::CONTROL) => {
self.page_down(false);
result.redraw = true;
}
// Page up (ctrl + b)
KeyCode::Char('b') if event.modifiers.contains(KeyModifiers::CONTROL) => {
self.page_up(false);
result.redraw = true;
}
// Half page down (ctrl + d)
KeyCode::Char('d') if event.modifiers.contains(KeyModifiers::CONTROL) => {
self.page_down(true);
result.redraw = true;
}
// Half page up (ctrl + u)
KeyCode::Char('u') if event.modifiers.contains(KeyModifiers::CONTROL) => {
self.page_up(true);
result.redraw = true;
}
// Scroll down
KeyCode::Down | KeyCode::Char('j') => {
self.scroll_y += 1;
result.redraw = true;
}
// Scroll up
KeyCode::Up | KeyCode::Char('k') => {
self.scroll_y = self.scroll_y.saturating_sub(1);
result.redraw = true;
}
// Scroll to start
KeyCode::Char('g') => {
self.scroll_y = 0;
result.redraw = true;
}
// Scroll to end
KeyCode::Char('G') => {
self.scroll_y = self.num_rows;
result.redraw = true;
}
// Reload
KeyCode::Char('r') => {
result.redraw = true;
return EventControlFlow::Reload;
}
// Scroll right
KeyCode::Right | KeyCode::Char('l') => {
self.scroll_x += 1;
result.redraw = true;
}
// Scroll left
KeyCode::Left | KeyCode::Char('h') => {
self.scroll_x = self.scroll_x.saturating_sub(1);
result.redraw = true;
}
// Toggle relax relocation diffs
KeyCode::Char('x') => {
state.relax_reloc_diffs = !state.relax_reloc_diffs;
result.redraw = true;
return EventControlFlow::Reload;
}
// Toggle three-way diff
KeyCode::Char('3') => {
self.three_way = !self.three_way;
result.redraw = true;
}
// Toggle options
KeyCode::Char('o') => {
self.open_options = !self.open_options;
result.redraw = true;
}
_ => {}
}
}
Event::Mouse(event) => match event.kind {
MouseEventKind::ScrollDown => {
self.scroll_y += 3;
result.redraw = true;
}
MouseEventKind::ScrollUp => {
self.scroll_y = self.scroll_y.saturating_sub(3);
result.redraw = true;
}
MouseEventKind::ScrollRight => {
self.scroll_x += 3;
result.redraw = true;
}
MouseEventKind::ScrollLeft => {
self.scroll_x = self.scroll_x.saturating_sub(3);
result.redraw = true;
}
MouseEventKind::Down(MouseButton::Left) => {
result.click_xy = Some((event.column, event.row));
result.redraw = true;
}
_ => {}
},
Event::Resize(_, _) => {
result.redraw = true;
}
_ => {}
}
EventControlFlow::Continue(result)
}
fn reload(&mut self, state: &AppState) -> Result<()> {
let left_sym =
state.left_obj.as_ref().and_then(|(o, _)| find_function(o, &self.symbol_name));
let right_sym =
state.right_obj.as_ref().and_then(|(o, _)| find_function(o, &self.symbol_name));
let prev_sym =
state.prev_obj.as_ref().and_then(|(o, _)| find_function(o, &self.symbol_name));
self.num_rows = match (
get_symbol(state.left_obj.as_ref(), left_sym),
get_symbol(state.right_obj.as_ref(), right_sym),
) {
(Some((_l, ld)), Some((_r, rd))) => ld.instructions.len().max(rd.instructions.len()),
(Some((_l, ld)), None) => ld.instructions.len(),
(None, Some((_r, rd))) => rd.instructions.len(),
(None, None) => bail!("Symbol not found: {}", self.symbol_name),
};
self.left_sym = left_sym;
self.right_sym = right_sym;
self.prev_sym = prev_sym;
Ok(())
}
}
impl FunctionDiffUi {
pub fn draw_options(&mut self, f: &mut Frame, _result: &mut EventResult) {
let percent_x = 50;
let percent_y = 50;
let popup_rect = Layout::vertical([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(f.area())[1];
let popup_rect = Layout::horizontal([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_rect)[1];
let popup = Block::default()
.borders(Borders::ALL)
.title("Options")
.title_style(Style::default().fg(Color::White).bg(Color::Black));
f.render_widget(Clear, popup_rect);
f.render_widget(popup, popup_rect);
}
fn page_up(&mut self, half: bool) {
self.scroll_y = self.scroll_y.saturating_sub(self.per_page / if half { 2 } else { 1 });
}
fn page_down(&mut self, half: bool) {
self.scroll_y += self.per_page / if half { 2 } else { 1 };
}
#[allow(clippy::too_many_arguments)]
fn print_sym(
&self,
out: &mut Text<'static>,
symbol: &ObjSymbol,
symbol_diff: &ObjSymbolDiff,
rect: Rect,
highlight: &HighlightKind,
result: &EventResult,
only_changed: bool,
) -> Option<HighlightKind> {
let base_addr = symbol.address;
let mut new_highlight = None;
for (y, ins_diff) in symbol_diff
.instructions
.iter()
.skip(self.scroll_y)
.take(rect.height as usize)
.enumerate()
{
if only_changed && ins_diff.kind == ObjInsDiffKind::None {
out.lines.push(Line::default());
continue;
}
let mut sx = rect.x;
let sy = rect.y + y as u16;
let mut line = Line::default();
display_diff(ins_diff, base_addr, |text| -> Result<()> {
let label_text;
let mut base_color = match ins_diff.kind {
ObjInsDiffKind::None
| ObjInsDiffKind::OpMismatch
| ObjInsDiffKind::ArgMismatch => Color::Gray,
ObjInsDiffKind::Replace => Color::Cyan,
ObjInsDiffKind::Delete => Color::Red,
ObjInsDiffKind::Insert => Color::Green,
};
let mut pad_to = 0;
match text {
DiffText::Basic(text) => {
label_text = text.to_string();
}
DiffText::BasicColor(s, idx) => {
label_text = s.to_string();
base_color = COLOR_ROTATION[idx % COLOR_ROTATION.len()];
}
DiffText::Line(num) => {
label_text = format!("{num} ");
base_color = Color::DarkGray;
pad_to = 5;
}
DiffText::Address(addr) => {
label_text = format!("{:x}:", addr);
pad_to = 5;
}
DiffText::Opcode(mnemonic, _op) => {
label_text = mnemonic.to_string();
if ins_diff.kind == ObjInsDiffKind::OpMismatch {
base_color = Color::Blue;
}
pad_to = 8;
}
DiffText::Argument(arg, diff) => {
label_text = arg.to_string();
if let Some(diff) = diff {
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
}
}
DiffText::BranchDest(addr, diff) => {
label_text = format!("{addr:x}");
if let Some(diff) = diff {
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
}
}
DiffText::Symbol(sym, diff) => {
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
label_text = name.clone();
if let Some(diff) = diff {
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
} else {
base_color = Color::White;
}
}
DiffText::Spacing(n) => {
line.spans.push(Span::raw(" ".repeat(n)));
sx += n as u16;
return Ok(());
}
DiffText::Eol => {
return Ok(());
}
}
let len = label_text.len();
let highlighted = *highlight == text;
if let Some((cx, cy)) = result.click_xy {
if cx >= sx && cx < sx + len as u16 && cy == sy {
new_highlight = Some(text.into());
}
}
let mut style = Style::new().fg(base_color);
if highlighted {
style = style.bg(Color::DarkGray);
}
line.spans.push(Span::styled(label_text, style));
sx += len as u16;
if pad_to > len {
let pad = (pad_to - len) as u16;
line.spans.push(Span::raw(" ".repeat(pad as usize)));
sx += pad;
}
Ok(())
})
.unwrap();
out.lines.push(line);
}
new_highlight
}
fn print_margin(&self, out: &mut Text, symbol: &ObjSymbolDiff, rect: Rect) {
for ins_diff in symbol.instructions.iter().skip(self.scroll_y).take(rect.height as usize) {
if ins_diff.kind != ObjInsDiffKind::None {
out.lines.push(Line::raw(match ins_diff.kind {
ObjInsDiffKind::Delete => "<",
ObjInsDiffKind::Insert => ">",
_ => "|",
}));
} else {
out.lines.push(Line::raw(" "));
}
}
}
}
pub const COLOR_ROTATION: [Color; 7] = [
Color::Magenta,
Color::Cyan,
Color::Green,
Color::Red,
Color::Yellow,
Color::Blue,
Color::Green,
];
pub fn match_percent_color(match_percent: f32) -> Color {
if match_percent == 100.0 {
Color::Green
} else if match_percent >= 50.0 {
Color::LightBlue
} else {
Color::LightRed
}
}
#[inline]
fn get_symbol(
obj: Option<&(ObjInfo, ObjDiff)>,
sym: Option<SymbolRef>,
) -> Option<(&ObjSymbol, &ObjSymbolDiff)> {
let (obj, diff) = obj?;
let sym = sym?;
Some((obj.section_symbol(sym).1, diff.symbol_diff(sym)))
}
fn find_function(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
for (section_idx, section) in obj.sections.iter().enumerate() {
if section.kind != ObjSectionKind::Code {
continue;
}
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
if symbol.name == name {
return Some(SymbolRef { section_idx, symbol_idx });
}
}
}
None
}

View File

@@ -0,0 +1,25 @@
use anyhow::Result;
use crossterm::event::Event;
use ratatui::Frame;
use crate::cmd::diff::AppState;
pub mod function_diff;
#[derive(Default)]
pub struct EventResult {
pub redraw: bool,
pub click_xy: Option<(u16, u16)>,
}
pub enum EventControlFlow {
Break,
Continue(EventResult),
Reload,
}
pub trait UiView {
fn draw(&mut self, state: &AppState, f: &mut Frame, result: &mut EventResult);
fn handle_event(&mut self, state: &mut AppState, event: Event) -> EventControlFlow;
fn reload(&mut self, state: &AppState) -> Result<()>;
}

View File

@@ -16,36 +16,39 @@ documentation = "https://docs.rs/objdiff-core"
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[features] [features]
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "bindings"] all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "arm64", "bindings", "build"]
any-arch = [] # Implicit, used to check if any arch is enabled any-arch = ["config", "dep:bimap", "dep:strum", "dep:similar", "dep:flagset", "dep:log", "dep:memmap2", "dep:byteorder", "dep:num-traits"] # Implicit, used to check if any arch is enabled
config = ["globset", "semver", "serde_json", "serde_yaml"] bindings = ["dep:serde_json", "dep:prost", "dep:pbjson", "dep:serde", "dep:prost-build", "dep:pbjson-build"]
dwarf = ["gimli"] build = ["dep:shell-escape", "dep:path-slash", "dep:winapi", "dep:notify", "dep:notify-debouncer-full", "dep:reqwest", "dep:self_update", "dep:tempfile", "dep:time"]
mips = ["any-arch", "rabbitizer"] config = ["dep:bimap", "dep:globset", "dep:semver", "dep:serde_json", "dep:serde_yaml", "dep:serde", "dep:filetime"]
ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"] dwarf = ["dep:gimli"]
x86 = ["any-arch", "cpp_demangle", "iced-x86", "msvc-demangler"] mips = ["any-arch", "dep:rabbitizer"]
arm = ["any-arch", "cpp_demangle", "unarm", "arm-attr"] ppc = ["any-arch", "dep:cwdemangle", "dep:cwextab", "dep:ppc750cl"]
bindings = ["serde_json", "prost", "pbjson"] x86 = ["any-arch", "dep:cpp_demangle", "dep:iced-x86", "dep:msvc-demangler"]
wasm = ["bindings", "console_error_panic_hook", "console_log"] arm = ["any-arch", "dep:cpp_demangle", "dep:unarm", "dep:arm-attr"]
arm64 = ["any-arch", "dep:cpp_demangle", "dep:yaxpeax-arch", "dep:yaxpeax-arm"]
wasm = ["bindings", "any-arch", "dep:console_error_panic_hook", "dep:console_log", "dep:wasm-bindgen", "dep:tsify-next", "dep:log"]
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = ["all"] features = ["all"]
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
byteorder = "1.5" bimap = { version = "0.6", features = ["serde"], optional = true }
filetime = "0.2" byteorder = { version = "1.5", optional = true }
flagset = "0.4" filetime = { version = "0.2", optional = true }
log = "0.4" flagset = { version = "0.4", optional = true }
memmap2 = "0.9" log = { version = "0.4", optional = true }
num-traits = "0.2" memmap2 = { version = "0.9", optional = true }
num-traits = { version = "0.2", optional = true }
object = { version = "0.36", features = ["read_core", "std", "elf", "pe"], default-features = false } object = { version = "0.36", features = ["read_core", "std", "elf", "pe"], default-features = false }
pbjson = { version = "0.7", optional = true } pbjson = { version = "0.7", optional = true }
prost = { version = "0.13", optional = true } prost = { version = "0.13", optional = true }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"], optional = true }
similar = { version = "2.6", default-features = false } similar = { version = "2.6", default-features = false, optional = true }
strum = { version = "0.26", features = ["derive"] } strum = { version = "0.26", features = ["derive"], optional = true }
wasm-bindgen = "0.2" wasm-bindgen = { version = "0.2", optional = true }
tsify-next = { version = "0.5", default-features = false, features = ["js"] } tsify-next = { version = "0.5", default-features = false, features = ["js"], optional = true }
console_log = { version = "1.0", optional = true } console_log = { version = "1.0", optional = true }
console_error_panic_hook = { version = "0.1", optional = true } console_error_panic_hook = { version = "0.1", optional = true }
@@ -75,6 +78,31 @@ msvc-demangler = { version = "0.10", optional = true }
unarm = { version = "1.6", optional = true } unarm = { version = "1.6", optional = true }
arm-attr = { version = "0.1", optional = true } arm-attr = { version = "0.1", optional = true }
# arm64
yaxpeax-arch = { version = "0.3", default-features = false, features = ["std"], optional = true }
yaxpeax-arm = { version = "0.3", default-features = false, features = ["std"], optional = true }
# build
notify = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2", optional = true }
notify-debouncer-full = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2", optional = true }
shell-escape = { version = "0.1", optional = true }
tempfile = { version = "3.14", optional = true }
time = { version = "0.3", optional = true }
[target.'cfg(windows)'.dependencies]
path-slash = { version = "0.2", optional = true }
winapi = { version = "0.3", optional = true }
# For Linux static binaries, use rustls
[target.'cfg(target_os = "linux")'.dependencies]
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"], optional = true }
self_update = { version = "0.41", default-features = false, features = ["rustls"], optional = true }
# For all other platforms, use native TLS
[target.'cfg(not(target_os = "linux"))'.dependencies]
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "default-tls"], optional = true }
self_update = { version = "0.41", optional = true }
[build-dependencies] [build-dependencies]
prost-build = "0.13" prost-build = { version = "0.13", optional = true }
pbjson-build = "0.7" pbjson-build = { version = "0.7", optional = true }

View File

@@ -11,4 +11,5 @@ objdiff-core contains the core functionality of [objdiff](https://github.com/enc
- **`ppc`**: Enables the PowerPC backend powered by [ppc750cl](https://github.com/encounter/ppc750cl). - **`ppc`**: Enables the PowerPC backend powered by [ppc750cl](https://github.com/encounter/ppc750cl).
- **`x86`**: Enables the x86 backend powered by [iced-x86](https://crates.io/crates/iced-x86). - **`x86`**: Enables the x86 backend powered by [iced-x86](https://crates.io/crates/iced-x86).
- **`arm`**: Enables the ARM backend powered by [unarm](https://github.com/AetiasHax/unarm). - **`arm`**: Enables the ARM backend powered by [unarm](https://github.com/AetiasHax/unarm).
- **`arm64`**: Enables the ARM64 backend powered by [yaxpeax-arm](https://github.com/iximeow/yaxpeax-arm).
- **`bindings`**: Enables serialization and deserialization of objdiff data structures. - **`bindings`**: Enables serialization and deserialization of objdiff data structures.

View File

@@ -1,6 +1,11 @@
use std::path::{Path, PathBuf};
fn main() { fn main() {
#[cfg(feature = "bindings")]
compile_protos();
}
#[cfg(feature = "bindings")]
fn compile_protos() {
use std::path::{Path, PathBuf};
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("protos"); let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("protos");
let descriptor_path = root.join("proto_descriptor.bin"); let descriptor_path = root.join("proto_descriptor.bin");
println!("cargo:rerun-if-changed={}", descriptor_path.display()); println!("cargo:rerun-if-changed={}", descriptor_path.display());

View File

@@ -124,11 +124,9 @@ impl ObjArch for ObjArchArm {
.get(&SectionIndex(section_index)) .get(&SectionIndex(section_index))
.map(|x| x.as_slice()) .map(|x| x.as_slice())
.unwrap_or(&fallback_mappings); .unwrap_or(&fallback_mappings);
let first_mapping_idx = let first_mapping_idx = mapping_symbols
match mapping_symbols.binary_search_by_key(&start_addr, |x| x.address) { .binary_search_by_key(&start_addr, |x| x.address)
Ok(idx) => idx, .unwrap_or_else(|idx| idx - 1);
Err(idx) => idx - 1,
};
let first_mapping = mapping_symbols[first_mapping_idx].mapping; let first_mapping = mapping_symbols[first_mapping_idx].mapping;
let mut mappings_iter = let mut mappings_iter =
@@ -215,7 +213,7 @@ impl ObjArch for ObjArchArm {
address: address as u64, address: address as u64,
size: (parser.address - address) as u8, size: (parser.address - address) as u8,
op: ins.opcode_id(), op: ins.opcode_id(),
mnemonic: parsed_ins.mnemonic.to_string(), mnemonic: Cow::Borrowed(parsed_ins.mnemonic),
args, args,
reloc, reloc,
branch_dest, branch_dest,
@@ -234,7 +232,7 @@ impl ObjArch for ObjArchArm {
section: &ObjSection, section: &ObjSection,
address: u64, address: u64,
reloc: &Relocation, reloc: &Relocation,
) -> anyhow::Result<i64> { ) -> Result<i64> {
let address = address as usize; let address = address as usize;
Ok(match reloc.flags() { Ok(match reloc.flags() {
// ARM calls // ARM calls

File diff suppressed because it is too large Load Diff

View File

@@ -83,7 +83,7 @@ impl ObjArch for ObjArchMips {
&self, &self,
address: u64, address: u64,
code: &[u8], code: &[u8],
_section_index: usize, section_index: usize,
relocations: &[ObjReloc], relocations: &[ObjReloc],
line_info: &BTreeMap<u64, u32>, line_info: &BTreeMap<u64, u32>,
config: &DiffObjConfig, config: &DiffObjConfig,
@@ -119,7 +119,7 @@ impl ObjArch for ObjArchMips {
let op = instruction.unique_id as u16; let op = instruction.unique_id as u16;
ops.push(op); ops.push(op);
let mnemonic = instruction.opcode_name().to_string(); let mnemonic = instruction.opcode_name();
let is_branch = instruction.is_branch(); let is_branch = instruction.is_branch();
let branch_offset = instruction.branch_offset(); let branch_offset = instruction.branch_offset();
let mut branch_dest = if is_branch { let mut branch_dest = if is_branch {
@@ -140,11 +140,18 @@ impl ObjArch for ObjArchMips {
| OperandType::cpu_label | OperandType::cpu_label
| OperandType::cpu_branch_target_label => { | OperandType::cpu_branch_target_label => {
if let Some(reloc) = reloc { if let Some(reloc) = reloc {
if matches!(&reloc.target_section, Some(s) if s == ".text") // If the relocation target is within the current function, we can
&& reloc.target.address > start_address // convert it into a relative branch target. Note that we check
&& reloc.target.address < end_address // target_address > start_address instead of >= so that recursive
// tail calls are not considered branch targets.
let target_address =
reloc.target.address.checked_add_signed(reloc.addend);
if reloc.target.orig_section_index == Some(section_index)
&& matches!(target_address, Some(addr) if addr > start_address && addr < end_address)
{ {
args.push(ObjInsArg::BranchDest(reloc.target.address)); let target_address = target_address.unwrap();
args.push(ObjInsArg::BranchDest(target_address));
branch_dest = Some(target_address);
} else { } else {
push_reloc(&mut args, reloc)?; push_reloc(&mut args, reloc)?;
branch_dest = None; branch_dest = None;
@@ -195,7 +202,7 @@ impl ObjArch for ObjArchMips {
address: cur_addr as u64, address: cur_addr as u64,
size: 4, size: 4,
op, op,
mnemonic, mnemonic: Cow::Borrowed(mnemonic),
args, args,
reloc: reloc.cloned(), reloc: reloc.cloned(),
branch_dest, branch_dest,

View File

@@ -12,6 +12,8 @@ use crate::{
#[cfg(feature = "arm")] #[cfg(feature = "arm")]
mod arm; mod arm;
#[cfg(feature = "arm64")]
mod arm64;
#[cfg(feature = "mips")] #[cfg(feature = "mips")]
pub mod mips; pub mod mips;
#[cfg(feature = "ppc")] #[cfg(feature = "ppc")]
@@ -35,8 +37,21 @@ pub enum DataType {
impl DataType { impl DataType {
pub fn display_bytes<Endian: ByteOrder>(&self, bytes: &[u8]) -> Option<String> { pub fn display_bytes<Endian: ByteOrder>(&self, bytes: &[u8]) -> Option<String> {
if self.required_len().is_some_and(|l| bytes.len() < l) { if self.required_len().is_some_and(|l| bytes.len() < l) {
log::warn!("Failed to display a symbol value for a symbol whose size is too small for instruction referencing it.");
return None; return None;
} }
let mut bytes = bytes;
if self.required_len().is_some_and(|l| bytes.len() > l) {
// If the symbol's size is larger a single instance of this data type, we take just the
// bytes necessary for one of them in order to display the first element of the array.
bytes = &bytes[0..self.required_len().unwrap()];
// TODO: Attempt to interpret large symbols as arrays of a smaller type and show all
// elements of the array instead. https://github.com/encounter/objdiff/issues/124
// However, note that the stride of an array can not always be determined just by the
// data type guessed by the single instruction accessing it. There can also be arrays of
// structs that contain multiple elements of different types, so if other elements after
// the first one were to be displayed in this manner, they may be inaccurate.
}
match self { match self {
DataType::Int8 => { DataType::Int8 => {
@@ -80,10 +95,10 @@ impl DataType {
} }
} }
DataType::Float => { DataType::Float => {
format!("Float: {}", Endian::read_f32(bytes)) format!("Float: {:?}f", Endian::read_f32(bytes))
} }
DataType::Double => { DataType::Double => {
format!("Double: {}", Endian::read_f64(bytes)) format!("Double: {:?}", Endian::read_f64(bytes))
} }
DataType::Bytes => { DataType::Bytes => {
format!("Bytes: {:#?}", bytes) format!("Bytes: {:#?}", bytes)
@@ -161,6 +176,8 @@ pub fn new_arch(object: &File) -> Result<Box<dyn ObjArch>> {
Architecture::I386 | Architecture::X86_64 => Box::new(x86::ObjArchX86::new(object)?), Architecture::I386 | Architecture::X86_64 => Box::new(x86::ObjArchX86::new(object)?),
#[cfg(feature = "arm")] #[cfg(feature = "arm")]
Architecture::Arm => Box::new(arm::ObjArchArm::new(object)?), Architecture::Arm => Box::new(arm::ObjArchArm::new(object)?),
#[cfg(feature = "arm64")]
Architecture::Aarch64 => Box::new(arm64::ObjArchArm64::new(object)?),
arch => bail!("Unsupported architecture: {arch:?}"), arch => bail!("Unsupported architecture: {arch:?}"),
}) })
} }

View File

@@ -1,4 +1,7 @@
use std::{borrow::Cow, collections::BTreeMap}; use std::{
borrow::Cow,
collections::{BTreeMap, HashMap},
};
use anyhow::{bail, ensure, Result}; use anyhow::{bail, ensure, Result};
use byteorder::BigEndian; use byteorder::BigEndian;
@@ -7,7 +10,7 @@ use object::{
elf, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, RelocationTarget, elf, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, RelocationTarget,
Symbol, SymbolKind, Symbol, SymbolKind,
}; };
use ppc750cl::{Argument, InsIter, Opcode, GPR}; use ppc750cl::{Argument, InsIter, Opcode, ParsedIns, GPR};
use crate::{ use crate::{
arch::{DataType, ObjArch, ProcessCodeResult}, arch::{DataType, ObjArch, ProcessCodeResult},
@@ -49,6 +52,8 @@ impl ObjArch for ObjArchPpc {
let ins_count = code.len() / 4; let ins_count = code.len() / 4;
let mut ops = Vec::<u16>::with_capacity(ins_count); let mut ops = Vec::<u16>::with_capacity(ins_count);
let mut insts = Vec::<ObjIns>::with_capacity(ins_count); let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
let fake_pool_reloc_for_addr =
generate_fake_pool_reloc_for_addr_mapping(address, code, relocations);
for (cur_addr, mut ins) in InsIter::new(code, address as u32) { for (cur_addr, mut ins) in InsIter::new(code, address as u32) {
let reloc = relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr); let reloc = relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
if let Some(reloc) = reloc { if let Some(reloc) = reloc {
@@ -143,9 +148,9 @@ impl ObjArch for ObjArchPpc {
insts.push(ObjIns { insts.push(ObjIns {
address: cur_addr as u64, address: cur_addr as u64,
size: 4, size: 4,
mnemonic: simplified.mnemonic.to_string(), mnemonic: Cow::Borrowed(simplified.mnemonic),
args, args,
reloc: reloc.cloned(), reloc: reloc.or(fake_pool_reloc_for_addr.get(&cur_addr)).cloned(),
op: ins.op as u16, op: ins.op as u16,
branch_dest, branch_dest,
line, line,
@@ -173,6 +178,7 @@ impl ObjArch for ObjArchPpc {
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> { fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
match flags { match flags {
RelocationFlags::Elf { r_type } => match r_type { RelocationFlags::Elf { r_type } => match r_type {
elf::R_PPC_NONE => Cow::Borrowed("R_PPC_NONE"), // We use this for fake pool relocs
elf::R_PPC_ADDR16_LO => Cow::Borrowed("R_PPC_ADDR16_LO"), elf::R_PPC_ADDR16_LO => Cow::Borrowed("R_PPC_ADDR16_LO"),
elf::R_PPC_ADDR16_HI => Cow::Borrowed("R_PPC_ADDR16_HI"), elf::R_PPC_ADDR16_HI => Cow::Borrowed("R_PPC_ADDR16_HI"),
elf::R_PPC_ADDR16_HA => Cow::Borrowed("R_PPC_ADDR16_HA"), elf::R_PPC_ADDR16_HA => Cow::Borrowed("R_PPC_ADDR16_HA"),
@@ -188,26 +194,22 @@ impl ObjArch for ObjArchPpc {
} }
fn guess_data_type(&self, instruction: &ObjIns) -> Option<super::DataType> { fn guess_data_type(&self, instruction: &ObjIns) -> Option<super::DataType> {
// Always shows the first string of the table. Not ideal, but it's really hard to find
// the actual string being referenced.
if instruction.reloc.as_ref().is_some_and(|r| r.target.name.starts_with("@stringBase")) { if instruction.reloc.as_ref().is_some_and(|r| r.target.name.starts_with("@stringBase")) {
return Some(DataType::String); return Some(DataType::String);
} }
match Opcode::from(instruction.op as u8) { let op = Opcode::from(instruction.op as u8);
Opcode::Lbz | Opcode::Lbzu | Opcode::Lbzux | Opcode::Lbzx => Some(DataType::Int8), if let Some(ty) = guess_data_type_from_load_store_inst_op(op) {
Opcode::Lhz | Opcode::Lhzu | Opcode::Lhzux | Opcode::Lhzx => Some(DataType::Int16), Some(ty)
Opcode::Lha | Opcode::Lhau | Opcode::Lhaux | Opcode::Lhax => Some(DataType::Int16), } else if op == Opcode::Addi {
Opcode::Lwz | Opcode::Lwzu | Opcode::Lwzux | Opcode::Lwzx => Some(DataType::Int32), // Assume that any addi instruction that references a local symbol is loading a string.
Opcode::Lfs | Opcode::Lfsu | Opcode::Lfsux | Opcode::Lfsx => Some(DataType::Float), // This hack is not ideal and results in tons of false positives where it will show
Opcode::Lfd | Opcode::Lfdu | Opcode::Lfdux | Opcode::Lfdx => Some(DataType::Double), // garbage strings (e.g. misinterpreting arrays, float literals, etc).
// But not all strings are in the @stringBase pool, so the condition above that checks
Opcode::Stb | Opcode::Stbu | Opcode::Stbux | Opcode::Stbx => Some(DataType::Int8), // the target symbol name would miss some.
Opcode::Sth | Opcode::Sthu | Opcode::Sthux | Opcode::Sthx => Some(DataType::Int16), Some(DataType::String)
Opcode::Stw | Opcode::Stwu | Opcode::Stwux | Opcode::Stwx => Some(DataType::Int32), } else {
Opcode::Stfs | Opcode::Stfsu | Opcode::Stfsux | Opcode::Stfsx => Some(DataType::Float), None
Opcode::Stfd | Opcode::Stfdu | Opcode::Stfdux | Opcode::Stfdx => Some(DataType::Double),
_ => None,
} }
} }
@@ -381,3 +383,196 @@ fn make_symbol_ref(symbol: &Symbol) -> Result<ExtabSymbolRef> {
let demangled_name = cwdemangle::demangle(&name, &cwdemangle::DemangleOptions::default()); let demangled_name = cwdemangle::demangle(&name, &cwdemangle::DemangleOptions::default());
Ok(ExtabSymbolRef { original_index: symbol.index().0, name, demangled_name }) Ok(ExtabSymbolRef { original_index: symbol.index().0, name, demangled_name })
} }
fn guess_data_type_from_load_store_inst_op(inst_op: Opcode) -> Option<DataType> {
match inst_op {
Opcode::Lbz | Opcode::Lbzu | Opcode::Lbzux | Opcode::Lbzx => Some(DataType::Int8),
Opcode::Lhz | Opcode::Lhzu | Opcode::Lhzux | Opcode::Lhzx => Some(DataType::Int16),
Opcode::Lha | Opcode::Lhau | Opcode::Lhaux | Opcode::Lhax => Some(DataType::Int16),
Opcode::Lwz | Opcode::Lwzu | Opcode::Lwzux | Opcode::Lwzx => Some(DataType::Int32),
Opcode::Lfs | Opcode::Lfsu | Opcode::Lfsux | Opcode::Lfsx => Some(DataType::Float),
Opcode::Lfd | Opcode::Lfdu | Opcode::Lfdux | Opcode::Lfdx => Some(DataType::Double),
Opcode::Stb | Opcode::Stbu | Opcode::Stbux | Opcode::Stbx => Some(DataType::Int8),
Opcode::Sth | Opcode::Sthu | Opcode::Sthux | Opcode::Sthx => Some(DataType::Int16),
Opcode::Stw | Opcode::Stwu | Opcode::Stwux | Opcode::Stwx => Some(DataType::Int32),
Opcode::Stfs | Opcode::Stfsu | Opcode::Stfsux | Opcode::Stfsx => Some(DataType::Float),
Opcode::Stfd | Opcode::Stfdu | Opcode::Stfdux | Opcode::Stfdx => Some(DataType::Double),
_ => None,
}
}
// Given an instruction, determine if it could accessing data at the address in a register.
// If so, return the offset added to the register's address, the register containing that address,
// and (optionally) which destination register the address is being copied into.
fn get_offset_and_addr_gpr_for_possible_pool_reference(
opcode: Opcode,
simplified: &ParsedIns,
) -> Option<(i16, GPR, Option<GPR>)> {
let args = &simplified.args;
if guess_data_type_from_load_store_inst_op(opcode).is_some() {
match (args[1], args[2]) {
(Argument::Offset(offset), Argument::GPR(addr_src_gpr)) => {
// e.g. lwz. Immediate offset.
Some((offset.0, addr_src_gpr, None))
}
(Argument::GPR(addr_src_gpr), Argument::GPR(_offset_gpr)) => {
// e.g. lwzx. The offset is in a register and was likely calculated from an index.
// Treat the offset as being 0 in this case to show the first element of the array.
// It may be possible to show all elements by figuring out the stride of the array
// from the calculations performed on the index before it's put into offset_gpr, but
// this would be much more complicated, so it's not currently done.
Some((0, addr_src_gpr, None))
}
_ => None,
}
} else {
// If it's not a load/store instruction, there's two more possibilities we need to handle.
// 1. It could be loading a pointer to a string.
// 2. It could be moving the relocation address plus an offset into a different register to
// load from later.
// If either of these match, we also want to return the destination register that the
// address is being copied into so that we can detect any future references to that new
// register as well.
match (opcode, args[0], args[1], args[2]) {
(
Opcode::Addi,
Argument::GPR(addr_dst_gpr),
Argument::GPR(addr_src_gpr),
Argument::Simm(simm),
) => Some((simm.0, addr_src_gpr, Some(addr_dst_gpr))),
(
Opcode::Or,
Argument::GPR(addr_dst_gpr),
Argument::GPR(addr_src_gpr),
Argument::None,
) => Some((0, addr_src_gpr, Some(addr_dst_gpr))), // `mr` or `mr.`
_ => None,
}
}
}
// We create a fake relocation for an instruction, vaguely simulating what the actual relocation
// might have looked like if it wasn't pooled. This is so minimal changes are needed to display
// pooled accesses vs non-pooled accesses. We set the relocation type to R_PPC_NONE to indicate that
// there isn't really a relocation here, as copying the pool relocation's type wouldn't make sense.
// Also, if this instruction is accessing the middle of a symbol instead of the start, we add an
// addend to indicate that.
fn make_fake_pool_reloc(offset: i16, cur_addr: u32, pool_reloc: &ObjReloc) -> Option<ObjReloc> {
let offset_from_pool = pool_reloc.addend + offset as i64;
let target_address = pool_reloc.target.address.checked_add_signed(offset_from_pool)?;
let orig_section_index = pool_reloc.target.orig_section_index?;
// We also need to create a fake target symbol to go inside our fake relocation.
// This is because we don't have access to list of all symbols in this section, so we can't find
// the real symbol yet. Instead we make a placeholder that has the correct `orig_section_index`
// and `address` fields, and then later on when this information is displayed to the user, we
// can find the real symbol by searching through the object's section's symbols for one that
// contains this address.
let fake_target_symbol = ObjSymbol {
name: "".to_string(),
demangled_name: None,
address: target_address,
section_address: 0,
size: 0,
size_known: false,
kind: Default::default(),
flags: Default::default(),
orig_section_index: Some(orig_section_index),
virtual_address: None,
original_index: None,
bytes: vec![],
};
// The addend is also fake because we don't know yet if the `target_address` here is the exact
// start of the symbol or if it's in the middle of it.
let fake_addend = 0;
Some(ObjReloc {
flags: RelocationFlags::Elf { r_type: elf::R_PPC_NONE },
address: cur_addr as u64,
target: fake_target_symbol,
addend: fake_addend,
})
}
// Searches through all instructions in a function, determining which registers have the addresses
// of pooled data relocations in them, finding which instructions load data from those addresses,
// and constructing a mapping of the address of that instruction to a "fake pool relocation" that
// simulates what that instruction's relocation would look like if data hadn't been pooled.
// Limitations: This method currently only goes through the instructions in a function in linear
// order, from start to finish. It does *not* follow any branches. This means that it could have
// false positives or false negatives in determining which relocation is currently loaded in which
// register at any given point in the function, as control flow is not respected.
// There are currently no known examples of this method producing inaccurate results in reality, but
// if examples are found, it may be possible to update this method to also follow all branches so
// that it produces more accurate results.
fn generate_fake_pool_reloc_for_addr_mapping(
address: u64,
code: &[u8],
relocations: &[ObjReloc],
) -> HashMap<u32, ObjReloc> {
let mut active_pool_relocs = HashMap::new();
let mut pool_reloc_for_addr = HashMap::new();
for (cur_addr, ins) in InsIter::new(code, address as u32) {
let simplified = ins.simplified();
let reloc = relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
if let Some(reloc) = reloc {
// This instruction has a real relocation, so it may be a pool load we want to keep
// track of.
let args = &simplified.args;
match (ins.op, args[0], args[1], args[2]) {
(
Opcode::Addi,
Argument::GPR(addr_dst_gpr),
Argument::GPR(_addr_src_gpr),
Argument::Simm(_simm),
) => {
active_pool_relocs.insert(addr_dst_gpr.0, reloc.clone()); // `lis` + `addi`
}
(
Opcode::Ori,
Argument::GPR(addr_dst_gpr),
Argument::GPR(_addr_src_gpr),
Argument::Uimm(_uimm),
) => {
active_pool_relocs.insert(addr_dst_gpr.0, reloc.clone()); // `lis` + `ori`
}
(Opcode::B, _, _, _) => {
if simplified.mnemonic == "bl" {
// When encountering a function call, clear any active pool relocations from
// the volatile registers (r0, r3-r12), but not the nonvolatile registers.
active_pool_relocs.remove(&0);
for gpr in 3..12 {
active_pool_relocs.remove(&gpr);
}
}
}
_ => {}
}
} else if let Some((offset, addr_src_gpr, addr_dst_gpr)) =
get_offset_and_addr_gpr_for_possible_pool_reference(ins.op, &simplified)
{
// This instruction doesn't have a real relocation, so it may be a reference to one of
// the already-loaded pools.
if let Some(pool_reloc) = active_pool_relocs.get(&addr_src_gpr.0) {
if let Some(fake_pool_reloc) = make_fake_pool_reloc(offset, cur_addr, pool_reloc) {
pool_reloc_for_addr.insert(cur_addr, fake_pool_reloc);
}
if let Some(addr_dst_gpr) = addr_dst_gpr {
// If the address of the pool relocation got copied into another register, we
// need to keep track of it in that register too as future instructions may
// reference the symbol indirectly via this new register, instead of the
// register the symbol's address was originally loaded into.
// For example, the start of the function might `lis` + `addi` the start of the
// ...data pool into r25, and then later the start of a loop will `addi` r25
// with the offset within the .data section of an array variable into r21.
// Then the body of the loop will `lwzx` one of the array elements from r21.
let mut new_reloc = pool_reloc.clone();
new_reloc.addend += offset as i64;
active_pool_relocs.insert(addr_dst_gpr.0, new_reloc);
}
}
}
}
pool_reloc_for_addr
}

View File

@@ -51,7 +51,7 @@ impl ObjArch for ObjArchX86 {
address: 0, address: 0,
size: 0, size: 0,
op: 0, op: 0,
mnemonic: String::new(), mnemonic: Cow::Borrowed("<invalid>"),
args: vec![], args: vec![],
reloc: None, reloc: None,
branch_dest: None, branch_dest: None,
@@ -76,7 +76,7 @@ impl ObjArch for ObjArchX86 {
address, address,
size: instruction.len() as u8, size: instruction.len() as u8,
op, op,
mnemonic: String::new(), mnemonic: Cow::Borrowed("<invalid>"),
args: vec![], args: vec![],
reloc: reloc.cloned(), reloc: reloc.cloned(),
branch_dest: None, branch_dest: None,
@@ -242,7 +242,8 @@ impl FormatterOutput for InstructionFormatterOutput {
fn write_mnemonic(&mut self, _instruction: &Instruction, text: &str) { fn write_mnemonic(&mut self, _instruction: &Instruction, text: &str) {
self.formatted.push_str(text); self.formatted.push_str(text);
self.ins.mnemonic = text.to_string(); // TODO: can iced-x86 guarantee 'static here?
self.ins.mnemonic = Cow::Owned(text.to_string());
} }
fn write_number( fn write_number(

View File

@@ -1,3 +1,4 @@
#![allow(clippy::needless_lifetimes)] // Generated serde code
use crate::{ use crate::{
diff::{ diff::{
ObjDataDiff, ObjDataDiffKind, ObjDiff, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjDataDiff, ObjDataDiffKind, ObjDiff, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo,
@@ -70,9 +71,13 @@ impl FunctionDiff {
// let (_section, symbol) = object.section_symbol(symbol_ref); // let (_section, symbol) = object.section_symbol(symbol_ref);
// Symbol::from(symbol) // Symbol::from(symbol)
// }); // });
let instructions = symbol_diff.instructions.iter().map(InstructionDiff::from).collect(); let instructions = symbol_diff
.instructions
.iter()
.map(|ins_diff| InstructionDiff::new(object, ins_diff))
.collect();
Self { Self {
symbol: Some(Symbol::from(symbol)), symbol: Some(Symbol::new(symbol)),
// diff_symbol, // diff_symbol,
instructions, instructions,
match_percent: symbol_diff.match_percent, match_percent: symbol_diff.match_percent,
@@ -90,8 +95,8 @@ impl DataDiff {
} }
} }
impl<'a> From<&'a ObjSymbol> for Symbol { impl Symbol {
fn from(value: &'a ObjSymbol) -> Self { pub fn new(value: &ObjSymbol) -> Self {
Self { Self {
name: value.name.to_string(), name: value.name.to_string(),
demangled_name: value.demangled_name.clone(), demangled_name: value.demangled_name.clone(),
@@ -122,29 +127,29 @@ fn symbol_flags(value: ObjSymbolFlagSet) -> u32 {
flags flags
} }
impl<'a> From<&'a ObjIns> for Instruction { impl Instruction {
fn from(value: &'a ObjIns) -> Self { pub fn new(object: &ObjInfo, instruction: &ObjIns) -> Self {
Self { Self {
address: value.address, address: instruction.address,
size: value.size as u32, size: instruction.size as u32,
opcode: value.op as u32, opcode: instruction.op as u32,
mnemonic: value.mnemonic.clone(), mnemonic: instruction.mnemonic.to_string(),
formatted: value.formatted.clone(), formatted: instruction.formatted.clone(),
arguments: value.args.iter().map(Argument::from).collect(), arguments: instruction.args.iter().map(Argument::new).collect(),
relocation: value.reloc.as_ref().map(Relocation::from), relocation: instruction.reloc.as_ref().map(|reloc| Relocation::new(object, reloc)),
branch_dest: value.branch_dest, branch_dest: instruction.branch_dest,
line_number: value.line, line_number: instruction.line,
original: value.orig.clone(), original: instruction.orig.clone(),
} }
} }
} }
impl<'a> From<&'a ObjInsArg> for Argument { impl Argument {
fn from(value: &'a ObjInsArg) -> Self { pub fn new(value: &ObjInsArg) -> Self {
Self { Self {
value: Some(match value { value: Some(match value {
ObjInsArg::PlainText(s) => argument::Value::PlainText(s.to_string()), ObjInsArg::PlainText(s) => argument::Value::PlainText(s.to_string()),
ObjInsArg::Arg(v) => argument::Value::Argument(ArgumentValue::from(v)), ObjInsArg::Arg(v) => argument::Value::Argument(ArgumentValue::new(v)),
ObjInsArg::Reloc => argument::Value::Relocation(ArgumentRelocation {}), ObjInsArg::Reloc => argument::Value::Relocation(ArgumentRelocation {}),
ObjInsArg::BranchDest(dest) => argument::Value::BranchDest(*dest), ObjInsArg::BranchDest(dest) => argument::Value::BranchDest(*dest),
}), }),
@@ -152,8 +157,8 @@ impl<'a> From<&'a ObjInsArg> for Argument {
} }
} }
impl From<&ObjInsArgValue> for ArgumentValue { impl ArgumentValue {
fn from(value: &ObjInsArgValue) -> Self { pub fn new(value: &ObjInsArgValue) -> Self {
Self { Self {
value: Some(match value { value: Some(match value {
ObjInsArgValue::Signed(v) => argument_value::Value::Signed(*v), ObjInsArgValue::Signed(v) => argument_value::Value::Signed(*v),
@@ -164,42 +169,39 @@ impl From<&ObjInsArgValue> for ArgumentValue {
} }
} }
impl<'a> From<&'a ObjReloc> for Relocation { impl Relocation {
fn from(value: &ObjReloc) -> Self { pub fn new(object: &ObjInfo, reloc: &ObjReloc) -> Self {
Self { Self {
r#type: match value.flags { r#type: match reloc.flags {
object::RelocationFlags::Elf { r_type } => r_type, object::RelocationFlags::Elf { r_type } => r_type,
object::RelocationFlags::MachO { r_type, .. } => r_type as u32, object::RelocationFlags::MachO { r_type, .. } => r_type as u32,
object::RelocationFlags::Coff { typ } => typ as u32, object::RelocationFlags::Coff { typ } => typ as u32,
object::RelocationFlags::Xcoff { r_rtype, .. } => r_rtype as u32, object::RelocationFlags::Xcoff { r_rtype, .. } => r_rtype as u32,
_ => unreachable!(), _ => unreachable!(),
}, },
type_name: String::new(), // TODO type_name: object.arch.display_reloc(reloc.flags).into_owned(),
target: Some(RelocationTarget::from(&value.target)), target: Some(RelocationTarget {
symbol: Some(Symbol::new(&reloc.target)),
addend: reloc.addend,
}),
} }
} }
} }
impl<'a> From<&'a ObjSymbol> for RelocationTarget { impl InstructionDiff {
fn from(value: &'a ObjSymbol) -> Self { pub fn new(object: &ObjInfo, instruction_diff: &ObjInsDiff) -> Self {
Self { symbol: Some(Symbol::from(value)), addend: value.addend }
}
}
impl<'a> From<&'a ObjInsDiff> for InstructionDiff {
fn from(value: &'a ObjInsDiff) -> Self {
Self { Self {
instruction: value.ins.as_ref().map(Instruction::from), instruction: instruction_diff.ins.as_ref().map(|ins| Instruction::new(object, ins)),
diff_kind: DiffKind::from(value.kind) as i32, diff_kind: DiffKind::from(instruction_diff.kind) as i32,
branch_from: value.branch_from.as_ref().map(InstructionBranchFrom::from), branch_from: instruction_diff.branch_from.as_ref().map(InstructionBranchFrom::new),
branch_to: value.branch_to.as_ref().map(InstructionBranchTo::from), branch_to: instruction_diff.branch_to.as_ref().map(InstructionBranchTo::new),
arg_diff: value.arg_diff.iter().map(ArgumentDiff::from).collect(), arg_diff: instruction_diff.arg_diff.iter().map(ArgumentDiff::new).collect(),
} }
} }
} }
impl From<&Option<ObjInsArgDiff>> for ArgumentDiff { impl ArgumentDiff {
fn from(value: &Option<ObjInsArgDiff>) -> Self { pub fn new(value: &Option<ObjInsArgDiff>) -> Self {
Self { diff_index: value.as_ref().map(|v| v.idx as u32) } Self { diff_index: value.as_ref().map(|v| v.idx as u32) }
} }
} }
@@ -228,8 +230,8 @@ impl From<ObjDataDiffKind> for DiffKind {
} }
} }
impl<'a> From<&'a ObjInsBranchFrom> for InstructionBranchFrom { impl InstructionBranchFrom {
fn from(value: &'a ObjInsBranchFrom) -> Self { pub fn new(value: &ObjInsBranchFrom) -> Self {
Self { Self {
instruction_index: value.ins_idx.iter().map(|&x| x as u32).collect(), instruction_index: value.ins_idx.iter().map(|&x| x as u32).collect(),
branch_index: value.branch_idx as u32, branch_index: value.branch_idx as u32,
@@ -237,8 +239,8 @@ impl<'a> From<&'a ObjInsBranchFrom> for InstructionBranchFrom {
} }
} }
impl<'a> From<&'a ObjInsBranchTo> for InstructionBranchTo { impl InstructionBranchTo {
fn from(value: &'a ObjInsBranchTo) -> Self { pub fn new(value: &ObjInsBranchTo) -> Self {
Self { instruction_index: value.ins_idx as u32, branch_index: value.branch_idx as u32 } Self { instruction_index: value.ins_idx as u32, branch_index: value.branch_idx as u32 }
} }
} }

View File

@@ -1,3 +1,4 @@
#![allow(clippy::needless_lifetimes)] // Generated serde code
use std::ops::AddAssign; use std::ops::AddAssign;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
@@ -173,8 +174,7 @@ impl Report {
continue; continue;
} }
fn is_sub_category(id: &str, parent: &str, sep: char) -> bool { fn is_sub_category(id: &str, parent: &str, sep: char) -> bool {
id.starts_with(parent) id.starts_with(parent) && id.get(parent.len()..).is_some_and(|s| s.starts_with(sep))
&& id.get(parent.len()..).map_or(false, |s| s.starts_with(sep))
} }
let mut sub_categories = self let mut sub_categories = self
.categories .categories

View File

@@ -0,0 +1,106 @@
pub mod watcher;
use std::{
path::{Path, PathBuf},
process::Command,
};
pub struct BuildStatus {
pub success: bool,
pub cmdline: String,
pub stdout: String,
pub stderr: String,
}
impl Default for BuildStatus {
fn default() -> Self {
BuildStatus {
success: true,
cmdline: String::new(),
stdout: String::new(),
stderr: String::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct BuildConfig {
pub project_dir: Option<PathBuf>,
pub custom_make: Option<String>,
pub custom_args: Option<Vec<String>>,
#[allow(unused)]
pub selected_wsl_distro: Option<String>,
}
pub fn run_make(config: &BuildConfig, arg: &Path) -> BuildStatus {
let Some(cwd) = &config.project_dir else {
return BuildStatus {
success: false,
stderr: "Missing project dir".to_string(),
..Default::default()
};
};
let make = config.custom_make.as_deref().unwrap_or("make");
let make_args = config.custom_args.as_deref().unwrap_or(&[]);
#[cfg(not(windows))]
let mut command = {
let mut command = Command::new(make);
command.current_dir(cwd).args(make_args).arg(arg);
command
};
#[cfg(windows)]
let mut command = {
use std::os::windows::process::CommandExt;
use path_slash::PathExt;
let mut command = if config.selected_wsl_distro.is_some() {
Command::new("wsl")
} else {
Command::new(make)
};
if let Some(distro) = &config.selected_wsl_distro {
// Strip distro root prefix \\wsl.localhost\{distro}
let wsl_path_prefix = format!("\\\\wsl.localhost\\{}", distro);
let cwd = match cwd.strip_prefix(wsl_path_prefix) {
Ok(new_cwd) => format!("/{}", new_cwd.to_slash_lossy().as_ref()),
Err(_) => cwd.to_string_lossy().to_string(),
};
command
.arg("--cd")
.arg(cwd)
.arg("-d")
.arg(distro)
.arg("--")
.arg(make)
.args(make_args)
.arg(arg.to_slash_lossy().as_ref());
} else {
command.current_dir(cwd).args(make_args).arg(arg.to_slash_lossy().as_ref());
}
command.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
command
};
let mut cmdline = shell_escape::escape(command.get_program().to_string_lossy()).into_owned();
for arg in command.get_args() {
cmdline.push(' ');
cmdline.push_str(shell_escape::escape(arg.to_string_lossy()).as_ref());
}
let output = match command.output() {
Ok(output) => output,
Err(e) => {
return BuildStatus {
success: false,
cmdline,
stdout: Default::default(),
stderr: e.to_string(),
};
}
};
// Try from_utf8 first to avoid copying the buffer if it's valid, then fall back to from_utf8_lossy
let stdout = String::from_utf8(output.stdout)
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
let stderr = String::from_utf8(output.stderr)
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
BuildStatus { success: output.status.success(), cmdline, stdout, stderr }
}

View File

@@ -0,0 +1,75 @@
use std::{
fs,
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
task::Waker,
time::Duration,
};
use globset::GlobSet;
use notify::RecursiveMode;
use notify_debouncer_full::{new_debouncer_opt, DebounceEventResult};
pub type Watcher = notify_debouncer_full::Debouncer<
notify::RecommendedWatcher,
notify_debouncer_full::RecommendedCache,
>;
pub struct WatcherState {
pub config_path: Option<PathBuf>,
pub left_obj_path: Option<PathBuf>,
pub right_obj_path: Option<PathBuf>,
pub patterns: GlobSet,
}
pub fn create_watcher(
modified: Arc<AtomicBool>,
project_dir: &Path,
patterns: GlobSet,
waker: Waker,
) -> notify::Result<Watcher> {
let base_dir = fs::canonicalize(project_dir)?;
let base_dir_clone = base_dir.clone();
let timeout = Duration::from_millis(200);
let config = notify::Config::default().with_poll_interval(Duration::from_secs(2));
let mut debouncer = new_debouncer_opt(
timeout,
None,
move |result: DebounceEventResult| match result {
Ok(events) => {
let mut any_match = false;
for event in events.iter() {
if !matches!(
event.kind,
notify::EventKind::Modify(..)
| notify::EventKind::Create(..)
| notify::EventKind::Remove(..)
) {
continue;
}
for path in &event.paths {
let Ok(path) = path.strip_prefix(&base_dir_clone) else {
continue;
};
if patterns.is_match(path) {
// log::info!("File modified: {}", path.display());
any_match = true;
}
}
}
if any_match {
modified.store(true, Ordering::Relaxed);
waker.wake_by_ref();
}
}
Err(errors) => errors.iter().for_each(|e| log::error!("Watch error: {e:?}")),
},
notify_debouncer_full::RecommendedCache::new(),
config,
)?;
debouncer.watch(base_dir, RecursiveMode::Recursive)?;
Ok(debouncer)
}

View File

@@ -1,77 +1,100 @@
use std::{ use std::{
fs,
fs::File, fs::File,
io::{BufReader, Read}, io::{BufReader, BufWriter, Read},
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use bimap::BiBTreeMap;
use filetime::FileTime; use filetime::FileTime;
use globset::{Glob, GlobSet, GlobSetBuilder}; use globset::{Glob, GlobSet, GlobSetBuilder};
#[inline] #[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
fn bool_true() -> bool { true }
#[derive(Default, Clone, serde::Deserialize)]
pub struct ProjectConfig { pub struct ProjectConfig {
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub min_version: Option<String>, pub min_version: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub custom_make: Option<String>, pub custom_make: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub custom_args: Option<Vec<String>>, pub custom_args: Option<Vec<String>>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub target_dir: Option<PathBuf>, pub target_dir: Option<PathBuf>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub base_dir: Option<PathBuf>, pub base_dir: Option<PathBuf>,
#[serde(default = "bool_true")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub build_base: bool, pub build_base: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub build_target: bool, pub build_target: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub watch_patterns: Option<Vec<Glob>>, pub watch_patterns: Option<Vec<Glob>>,
#[serde(default, alias = "units")] #[serde(default, alias = "objects", skip_serializing_if = "Option::is_none")]
pub objects: Vec<ProjectObject>, pub units: Option<Vec<ProjectObject>>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub progress_categories: Vec<ProjectProgressCategory>, pub progress_categories: Option<Vec<ProjectProgressCategory>>,
} }
#[derive(Default, Clone, serde::Deserialize)] impl ProjectConfig {
#[inline]
pub fn units(&self) -> &[ProjectObject] { self.units.as_deref().unwrap_or_default() }
#[inline]
pub fn units_mut(&mut self) -> &mut Vec<ProjectObject> {
self.units.get_or_insert_with(Vec::new)
}
#[inline]
pub fn progress_categories(&self) -> &[ProjectProgressCategory] {
self.progress_categories.as_deref().unwrap_or_default()
}
#[inline]
pub fn progress_categories_mut(&mut self) -> &mut Vec<ProjectProgressCategory> {
self.progress_categories.get_or_insert_with(Vec::new)
}
}
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectObject { pub struct ProjectObject {
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>, pub name: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<PathBuf>, pub path: Option<PathBuf>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub target_path: Option<PathBuf>, pub target_path: Option<PathBuf>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub base_path: Option<PathBuf>, pub base_path: Option<PathBuf>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
#[deprecated(note = "Use metadata.reverse_fn_order")] #[deprecated(note = "Use metadata.reverse_fn_order")]
pub reverse_fn_order: Option<bool>, pub reverse_fn_order: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
#[deprecated(note = "Use metadata.complete")] #[deprecated(note = "Use metadata.complete")]
pub complete: Option<bool>, pub complete: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub scratch: Option<ScratchConfig>, pub scratch: Option<ScratchConfig>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<ProjectObjectMetadata>, pub metadata: Option<ProjectObjectMetadata>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub symbol_mappings: Option<SymbolMappings>,
} }
#[derive(Default, Clone, serde::Deserialize)] pub type SymbolMappings = BiBTreeMap<String, String>;
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectObjectMetadata { pub struct ProjectObjectMetadata {
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub complete: Option<bool>, pub complete: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub reverse_fn_order: Option<bool>, pub reverse_fn_order: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub source_path: Option<String>, pub source_path: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub progress_categories: Option<Vec<String>>, pub progress_categories: Option<Vec<String>>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub auto_generated: Option<bool>, pub auto_generated: Option<bool>,
} }
#[derive(Default, Clone, serde::Deserialize)] #[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectProgressCategory { pub struct ProjectProgressCategory {
#[serde(default)] #[serde(default)]
pub id: String, pub id: String,
@@ -112,12 +135,12 @@ impl ProjectObject {
} }
pub fn complete(&self) -> Option<bool> { pub fn complete(&self) -> Option<bool> {
#[allow(deprecated)] #[expect(deprecated)]
self.metadata.as_ref().and_then(|m| m.complete).or(self.complete) self.metadata.as_ref().and_then(|m| m.complete).or(self.complete)
} }
pub fn reverse_fn_order(&self) -> Option<bool> { pub fn reverse_fn_order(&self) -> Option<bool> {
#[allow(deprecated)] #[expect(deprecated)]
self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order) self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order)
} }
@@ -132,16 +155,18 @@ impl ProjectObject {
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ScratchConfig { pub struct ScratchConfig {
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub platform: Option<String>, pub platform: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub compiler: Option<String>, pub compiler: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub c_flags: Option<String>, pub c_flags: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub ctx_path: Option<PathBuf>, pub ctx_path: Option<PathBuf>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub build_ctx: bool, pub build_ctx: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub preset_id: Option<u32>,
} }
pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"]; pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"];
@@ -151,10 +176,14 @@ pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[
"*.inc", "*.py", "*.yml", "*.txt", "*.json", "*.inc", "*.py", "*.yml", "*.txt", "*.json",
]; ];
pub fn default_watch_patterns() -> Vec<Glob> {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
}
#[derive(Clone, Eq, PartialEq)] #[derive(Clone, Eq, PartialEq)]
pub struct ProjectConfigInfo { pub struct ProjectConfigInfo {
pub path: PathBuf, pub path: PathBuf,
pub timestamp: FileTime, pub timestamp: Option<FileTime>,
} }
pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> { pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
@@ -180,12 +209,41 @@ pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectC
result = Err(e); result = Err(e);
} }
} }
return Some((result, ProjectConfigInfo { path: config_path, timestamp: ts })); return Some((result, ProjectConfigInfo { path: config_path, timestamp: Some(ts) }));
} }
} }
None None
} }
pub fn save_project_config(
config: &ProjectConfig,
info: &ProjectConfigInfo,
) -> Result<ProjectConfigInfo> {
if let Some(last_ts) = info.timestamp {
// Check if the file has changed since we last read it
if let Ok(metadata) = fs::metadata(&info.path) {
let ts = FileTime::from_last_modification_time(&metadata);
if ts != last_ts {
return Err(anyhow!("Config file has changed since last read"));
}
}
}
let mut writer =
BufWriter::new(File::create(&info.path).context("Failed to create config file")?);
let ext = info.path.extension().and_then(|ext| ext.to_str()).unwrap_or("json");
match ext {
"json" => serde_json::to_writer_pretty(&mut writer, config).context("Failed to write JSON"),
"yml" | "yaml" => {
serde_yaml::to_writer(&mut writer, config).context("Failed to write YAML")
}
_ => Err(anyhow!("Unknown config file extension: {ext}")),
}?;
let file = writer.into_inner().context("Failed to flush file")?;
let metadata = file.metadata().context("Failed to get file metadata")?;
let ts = FileTime::from_last_modification_time(&metadata);
Ok(ProjectConfigInfo { path: info.path.clone(), timestamp: Some(ts) })
}
fn validate_min_version(config: &ProjectConfig) -> Result<()> { fn validate_min_version(config: &ProjectConfig) -> Result<()> {
let Some(min_version) = &config.min_version else { return Ok(()) }; let Some(min_version) = &config.min_version else { return Ok(()) };
let version = semver::Version::parse(env!("CARGO_PKG_VERSION")) let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))

View File

@@ -9,7 +9,7 @@ use crate::{
DiffObjConfig, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjInsDiff, ObjInsDiffKind, DiffObjConfig, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjInsDiff, ObjInsDiffKind,
ObjSymbolDiff, ObjSymbolDiff,
}, },
obj::{ObjInfo, ObjInsArg, ObjReloc, ObjSymbol, ObjSymbolFlags, SymbolRef}, obj::{ObjInfo, ObjInsArg, ObjReloc, ObjSection, ObjSymbol, ObjSymbolFlags, SymbolRef},
}; };
pub fn process_code_symbol( pub fn process_code_symbol(
@@ -21,14 +21,30 @@ pub fn process_code_symbol(
let section = section.ok_or_else(|| anyhow!("Code symbol section not found"))?; let section = section.ok_or_else(|| anyhow!("Code symbol section not found"))?;
let code = &section.data let code = &section.data
[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize]; [symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
obj.arch.process_code( let mut res = obj.arch.process_code(
symbol.address, symbol.address,
code, code,
section.orig_index, section.orig_index,
&section.relocations, &section.relocations,
&section.line_info, &section.line_info,
config, config,
) )?;
for inst in res.insts.iter_mut() {
if let Some(reloc) = &mut inst.reloc {
if reloc.target.size == 0 && reloc.target.name.is_empty() {
// Fake target symbol we added as a placeholder. We need to find the real one.
if let Some(real_target) =
find_symbol_matching_fake_symbol_in_sections(&reloc.target, &obj.sections)
{
reloc.addend = (reloc.target.address - real_target.address) as i64;
reloc.target = real_target;
}
}
}
}
Ok(res)
} }
pub fn no_diff_code(out: &ProcessCodeResult, symbol_ref: SymbolRef) -> Result<ObjSymbolDiff> { pub fn no_diff_code(out: &ProcessCodeResult, symbol_ref: SymbolRef) -> Result<ObjSymbolDiff> {
@@ -41,10 +57,12 @@ pub fn no_diff_code(out: &ProcessCodeResult, symbol_ref: SymbolRef) -> Result<Ob
}); });
} }
resolve_branches(&mut diff); resolve_branches(&mut diff);
Ok(ObjSymbolDiff { symbol_ref, diff_symbol: None, instructions: diff, match_percent: None }) Ok(ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: diff, match_percent: None })
} }
pub fn diff_code( pub fn diff_code(
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left_out: &ProcessCodeResult, left_out: &ProcessCodeResult,
right_out: &ProcessCodeResult, right_out: &ProcessCodeResult,
left_symbol_ref: SymbolRef, left_symbol_ref: SymbolRef,
@@ -60,14 +78,14 @@ pub fn diff_code(
let mut diff_state = InsDiffState::default(); let mut diff_state = InsDiffState::default();
for (left, right) in left_diff.iter_mut().zip(right_diff.iter_mut()) { for (left, right) in left_diff.iter_mut().zip(right_diff.iter_mut()) {
let result = compare_ins(config, left, right, &mut diff_state)?; let result = compare_ins(config, left_obj, right_obj, left, right, &mut diff_state)?;
left.kind = result.kind; left.kind = result.kind;
right.kind = result.kind; right.kind = result.kind;
left.arg_diff = result.left_args_diff; left.arg_diff = result.left_args_diff;
right.arg_diff = result.right_args_diff; right.arg_diff = result.right_args_diff;
} }
let total = left_out.insts.len(); let total = left_out.insts.len().max(right_out.insts.len());
let percent = if diff_state.diff_count >= total { let percent = if diff_state.diff_count >= total {
0.0 0.0
} else { } else {
@@ -77,13 +95,13 @@ pub fn diff_code(
Ok(( Ok((
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: left_symbol_ref, symbol_ref: left_symbol_ref,
diff_symbol: Some(right_symbol_ref), target_symbol: Some(right_symbol_ref),
instructions: left_diff, instructions: left_diff,
match_percent: Some(percent), match_percent: Some(percent),
}, },
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: right_symbol_ref, symbol_ref: right_symbol_ref,
diff_symbol: Some(left_symbol_ref), target_symbol: Some(left_symbol_ref),
instructions: right_diff, instructions: right_diff,
match_percent: Some(percent), match_percent: Some(percent),
}, },
@@ -170,12 +188,33 @@ fn resolve_branches(vec: &mut [ObjInsDiff]) {
} }
} }
fn address_eq(left: &ObjSymbol, right: &ObjSymbol) -> bool { fn address_eq(left: &ObjReloc, right: &ObjReloc) -> bool {
left.address as i64 + left.addend == right.address as i64 + right.addend left.target.address as i64 + left.addend == right.target.address as i64 + right.addend
}
fn section_name_eq(
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left_orig_section_index: usize,
right_orig_section_index: usize,
) -> bool {
let Some(left_section) =
left_obj.sections.iter().find(|s| s.orig_index == left_orig_section_index)
else {
return false;
};
let Some(right_section) =
right_obj.sections.iter().find(|s| s.orig_index == right_orig_section_index)
else {
return false;
};
left_section.name == right_section.name
} }
fn reloc_eq( fn reloc_eq(
config: &DiffObjConfig, config: &DiffObjConfig,
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left_reloc: Option<&ObjReloc>, left_reloc: Option<&ObjReloc>,
right_reloc: Option<&ObjReloc>, right_reloc: Option<&ObjReloc>,
) -> bool { ) -> bool {
@@ -189,29 +228,32 @@ fn reloc_eq(
return true; return true;
} }
let name_matches = left.target.name == right.target.name; let symbol_name_matches = left.target.name == right.target.name;
match (&left.target_section, &right.target_section) { match (&left.target.orig_section_index, &right.target.orig_section_index) {
(Some(sl), Some(sr)) => { (Some(sl), Some(sr)) => {
// Match if section and name or address match // Match if section and name or address match
sl == sr && (name_matches || address_eq(&left.target, &right.target)) section_name_eq(left_obj, right_obj, *sl, *sr)
&& (symbol_name_matches || address_eq(left, right))
} }
(Some(_), None) => false, (Some(_), None) => false,
(None, Some(_)) => { (None, Some(_)) => {
// Match if possibly stripped weak symbol // Match if possibly stripped weak symbol
name_matches && right.target.flags.0.contains(ObjSymbolFlags::Weak) symbol_name_matches && right.target.flags.0.contains(ObjSymbolFlags::Weak)
} }
(None, None) => name_matches, (None, None) => symbol_name_matches,
} }
} }
fn arg_eq( fn arg_eq(
config: &DiffObjConfig, config: &DiffObjConfig,
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left: &ObjInsArg, left: &ObjInsArg,
right: &ObjInsArg, right: &ObjInsArg,
left_diff: &ObjInsDiff, left_diff: &ObjInsDiff,
right_diff: &ObjInsDiff, right_diff: &ObjInsDiff,
) -> bool { ) -> bool {
return match left { match left {
ObjInsArg::PlainText(l) => match right { ObjInsArg::PlainText(l) => match right {
ObjInsArg::PlainText(r) => l == r, ObjInsArg::PlainText(r) => l == r,
_ => false, _ => false,
@@ -227,16 +269,24 @@ fn arg_eq(
matches!(right, ObjInsArg::Reloc) matches!(right, ObjInsArg::Reloc)
&& reloc_eq( && reloc_eq(
config, config,
left_obj,
right_obj,
left_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()), left_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()), right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
) )
} }
ObjInsArg::BranchDest(_) => { ObjInsArg::BranchDest(_) => match right {
// Compare dest instruction idx after diffing // Compare dest instruction idx after diffing
ObjInsArg::BranchDest(_) => {
left_diff.branch_to.as_ref().map(|b| b.ins_idx) left_diff.branch_to.as_ref().map(|b| b.ins_idx)
== right_diff.branch_to.as_ref().map(|b| b.ins_idx) == right_diff.branch_to.as_ref().map(|b| b.ins_idx)
} }
}; // If relocations are relaxed, match if left is a constant and right is a reloc
// Useful for instances where the target object is created without relocations
ObjInsArg::Reloc => config.relax_reloc_diffs,
_ => false,
},
}
} }
#[derive(Default)] #[derive(Default)]
@@ -257,21 +307,18 @@ struct InsDiffResult {
fn compare_ins( fn compare_ins(
config: &DiffObjConfig, config: &DiffObjConfig,
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left: &ObjInsDiff, left: &ObjInsDiff,
right: &ObjInsDiff, right: &ObjInsDiff,
state: &mut InsDiffState, state: &mut InsDiffState,
) -> Result<InsDiffResult> { ) -> Result<InsDiffResult> {
let mut result = InsDiffResult::default(); let mut result = InsDiffResult::default();
if let (Some(left_ins), Some(right_ins)) = (&left.ins, &right.ins) { if let (Some(left_ins), Some(right_ins)) = (&left.ins, &right.ins) {
if left_ins.args.len() != right_ins.args.len() // Count only non-PlainText args
|| left_ins.op != right_ins.op let left_args_count = left_ins.iter_args().count();
// Check if any PlainText segments differ (punctuation and spacing) let right_args_count = right_ins.iter_args().count();
// This indicates a more significant difference than a simple arg mismatch if left_args_count != right_args_count || left_ins.op != right_ins.op {
|| !left_ins.args.iter().zip(&right_ins.args).all(|(a, b)| match (a, b) {
(ObjInsArg::PlainText(l), ObjInsArg::PlainText(r)) => l == r,
_ => true,
})
{
// Totally different op // Totally different op
result.kind = ObjInsDiffKind::Replace; result.kind = ObjInsDiffKind::Replace;
state.diff_count += 1; state.diff_count += 1;
@@ -282,8 +329,8 @@ fn compare_ins(
result.kind = ObjInsDiffKind::OpMismatch; result.kind = ObjInsDiffKind::OpMismatch;
state.diff_count += 1; state.diff_count += 1;
} }
for (a, b) in left_ins.args.iter().zip(&right_ins.args) { for (a, b) in left_ins.iter_args().zip(right_ins.iter_args()) {
if arg_eq(config, a, b, left, right) { if arg_eq(config, left_obj, right_obj, a, b, left, right) {
result.left_args_diff.push(None); result.left_args_diff.push(None);
result.right_args_diff.push(None); result.right_args_diff.push(None);
} else { } else {
@@ -294,8 +341,11 @@ fn compare_ins(
let a_str = match a { let a_str = match a {
ObjInsArg::PlainText(arg) => arg.to_string(), ObjInsArg::PlainText(arg) => arg.to_string(),
ObjInsArg::Arg(arg) => arg.to_string(), ObjInsArg::Arg(arg) => arg.to_string(),
ObjInsArg::Reloc => String::new(), ObjInsArg::Reloc => left_ins
ObjInsArg::BranchDest(arg) => format!("{arg}"), .reloc
.as_ref()
.map_or_else(|| "<unknown>".to_string(), |r| r.target.name.clone()),
ObjInsArg::BranchDest(arg) => arg.to_string(),
}; };
let a_diff = if let Some(idx) = state.left_args_idx.get(&a_str) { let a_diff = if let Some(idx) = state.left_args_idx.get(&a_str) {
ObjInsArgDiff { idx: *idx } ObjInsArgDiff { idx: *idx }
@@ -308,8 +358,11 @@ fn compare_ins(
let b_str = match b { let b_str = match b {
ObjInsArg::PlainText(arg) => arg.to_string(), ObjInsArg::PlainText(arg) => arg.to_string(),
ObjInsArg::Arg(arg) => arg.to_string(), ObjInsArg::Arg(arg) => arg.to_string(),
ObjInsArg::Reloc => String::new(), ObjInsArg::Reloc => right_ins
ObjInsArg::BranchDest(arg) => format!("{arg}"), .reloc
.as_ref()
.map_or_else(|| "<unknown>".to_string(), |r| r.target.name.clone()),
ObjInsArg::BranchDest(arg) => arg.to_string(),
}; };
let b_diff = if let Some(idx) = state.right_args_idx.get(&b_str) { let b_diff = if let Some(idx) = state.right_args_idx.get(&b_str) {
ObjInsArgDiff { idx: *idx } ObjInsArgDiff { idx: *idx }
@@ -332,3 +385,16 @@ fn compare_ins(
} }
Ok(result) Ok(result)
} }
fn find_symbol_matching_fake_symbol_in_sections(
fake_symbol: &ObjSymbol,
sections: &[ObjSection],
) -> Option<ObjSymbol> {
let orig_section_index = fake_symbol.orig_section_index?;
let section = sections.iter().find(|s| s.orig_index == orig_section_index)?;
let real_symbol = section
.symbols
.iter()
.find(|s| s.size > 0 && (s.address..s.address + s.size).contains(&fake_symbol.address))?;
Some(real_symbol.clone())
}

View File

@@ -20,13 +20,13 @@ pub fn diff_bss_symbol(
Ok(( Ok((
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: left_symbol_ref, symbol_ref: left_symbol_ref,
diff_symbol: Some(right_symbol_ref), target_symbol: Some(right_symbol_ref),
instructions: vec![], instructions: vec![],
match_percent: Some(percent), match_percent: Some(percent),
}, },
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: right_symbol_ref, symbol_ref: right_symbol_ref,
diff_symbol: Some(left_symbol_ref), target_symbol: Some(left_symbol_ref),
instructions: vec![], instructions: vec![],
match_percent: Some(percent), match_percent: Some(percent),
}, },
@@ -34,7 +34,7 @@ pub fn diff_bss_symbol(
} }
pub fn no_diff_symbol(_obj: &ObjInfo, symbol_ref: SymbolRef) -> ObjSymbolDiff { pub fn no_diff_symbol(_obj: &ObjInfo, symbol_ref: SymbolRef) -> ObjSymbolDiff {
ObjSymbolDiff { symbol_ref, diff_symbol: None, instructions: vec![], match_percent: None } ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: vec![], match_percent: None }
} }
/// Compare the data sections of two object files. /// Compare the data sections of two object files.
@@ -158,13 +158,13 @@ pub fn diff_data_symbol(
Ok(( Ok((
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: left_symbol_ref, symbol_ref: left_symbol_ref,
diff_symbol: Some(right_symbol_ref), target_symbol: Some(right_symbol_ref),
instructions: vec![], instructions: vec![],
match_percent: Some(match_percent), match_percent: Some(match_percent),
}, },
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: right_symbol_ref, symbol_ref: right_symbol_ref,
diff_symbol: Some(left_symbol_ref), target_symbol: Some(left_symbol_ref),
instructions: vec![], instructions: vec![],
match_percent: Some(match_percent), match_percent: Some(match_percent),
}, },

View File

@@ -22,14 +22,14 @@ pub enum DiffText<'a> {
/// Branch destination /// Branch destination
BranchDest(u64, Option<&'a ObjInsArgDiff>), BranchDest(u64, Option<&'a ObjInsArgDiff>),
/// Symbol name /// Symbol name
Symbol(&'a ObjSymbol), Symbol(&'a ObjSymbol, Option<&'a ObjInsArgDiff>),
/// Number of spaces /// Number of spaces
Spacing(usize), Spacing(usize),
/// End of line /// End of line
Eol, Eol,
} }
#[derive(Default, Clone, PartialEq, Eq)] #[derive(Debug, Default, Clone, PartialEq, Eq)]
pub enum HighlightKind { pub enum HighlightKind {
#[default] #[default]
None, None,
@@ -58,20 +58,23 @@ pub fn display_diff<E>(
cb(DiffText::Spacing(4))?; cb(DiffText::Spacing(4))?;
} }
cb(DiffText::Opcode(&ins.mnemonic, ins.op))?; cb(DiffText::Opcode(&ins.mnemonic, ins.op))?;
let mut arg_diff_idx = 0; // non-PlainText index
for (i, arg) in ins.args.iter().enumerate() { for (i, arg) in ins.args.iter().enumerate() {
if i == 0 { if i == 0 {
cb(DiffText::Spacing(1))?; cb(DiffText::Spacing(1))?;
} }
let diff = ins_diff.arg_diff.get(i).and_then(|o| o.as_ref()); let diff = ins_diff.arg_diff.get(arg_diff_idx).and_then(|o| o.as_ref());
match arg { match arg {
ObjInsArg::PlainText(s) => { ObjInsArg::PlainText(s) => {
cb(DiffText::Basic(s))?; cb(DiffText::Basic(s))?;
} }
ObjInsArg::Arg(v) => { ObjInsArg::Arg(v) => {
cb(DiffText::Argument(v, diff))?; cb(DiffText::Argument(v, diff))?;
arg_diff_idx += 1;
} }
ObjInsArg::Reloc => { ObjInsArg::Reloc => {
display_reloc_name(ins.reloc.as_ref().unwrap(), &mut cb)?; display_reloc_name(ins.reloc.as_ref().unwrap(), &mut cb, diff)?;
arg_diff_idx += 1;
} }
ObjInsArg::BranchDest(dest) => { ObjInsArg::BranchDest(dest) => {
if let Some(dest) = dest.checked_sub(base_addr) { if let Some(dest) = dest.checked_sub(base_addr) {
@@ -79,6 +82,7 @@ pub fn display_diff<E>(
} else { } else {
cb(DiffText::Basic("<unknown>"))?; cb(DiffText::Basic("<unknown>"))?;
} }
arg_diff_idx += 1;
} }
} }
} }
@@ -92,11 +96,12 @@ pub fn display_diff<E>(
fn display_reloc_name<E>( fn display_reloc_name<E>(
reloc: &ObjReloc, reloc: &ObjReloc,
mut cb: impl FnMut(DiffText) -> Result<(), E>, mut cb: impl FnMut(DiffText) -> Result<(), E>,
diff: Option<&ObjInsArgDiff>,
) -> Result<(), E> { ) -> Result<(), E> {
cb(DiffText::Symbol(&reloc.target))?; cb(DiffText::Symbol(&reloc.target, diff))?;
match reloc.target.addend.cmp(&0i64) { match reloc.addend.cmp(&0i64) {
Ordering::Greater => cb(DiffText::Basic(&format!("+{:#x}", reloc.target.addend))), Ordering::Greater => cb(DiffText::Basic(&format!("+{:#x}", reloc.addend))),
Ordering::Less => cb(DiffText::Basic(&format!("-{:#x}", -reloc.target.addend))), Ordering::Less => cb(DiffText::Basic(&format!("-{:#x}", -reloc.addend))),
_ => Ok(()), _ => Ok(()),
} }
} }
@@ -106,7 +111,7 @@ impl PartialEq<DiffText<'_>> for HighlightKind {
match (self, other) { match (self, other) {
(HighlightKind::Opcode(a), DiffText::Opcode(_, b)) => a == b, (HighlightKind::Opcode(a), DiffText::Opcode(_, b)) => a == b,
(HighlightKind::Arg(a), DiffText::Argument(b, _)) => a.loose_eq(b), (HighlightKind::Arg(a), DiffText::Argument(b, _)) => a.loose_eq(b),
(HighlightKind::Symbol(a), DiffText::Symbol(b)) => a == &b.name, (HighlightKind::Symbol(a), DiffText::Symbol(b, _)) => a == &b.name,
(HighlightKind::Address(a), DiffText::Address(b) | DiffText::BranchDest(b, _)) => { (HighlightKind::Address(a), DiffText::Address(b) | DiffText::BranchDest(b, _)) => {
a == b a == b
} }
@@ -124,7 +129,7 @@ impl From<DiffText<'_>> for HighlightKind {
match value { match value {
DiffText::Opcode(_, op) => HighlightKind::Opcode(op), DiffText::Opcode(_, op) => HighlightKind::Opcode(op),
DiffText::Argument(arg, _) => HighlightKind::Arg(arg.clone()), DiffText::Argument(arg, _) => HighlightKind::Arg(arg.clone()),
DiffText::Symbol(sym) => HighlightKind::Symbol(sym.name.to_string()), DiffText::Symbol(sym, _) => HighlightKind::Symbol(sym.name.to_string()),
DiffText::Address(addr) | DiffText::BranchDest(addr, _) => HighlightKind::Address(addr), DiffText::Address(addr) | DiffText::BranchDest(addr, _) => HighlightKind::Address(addr),
_ => HighlightKind::None, _ => HighlightKind::None,
} }

View File

@@ -3,6 +3,7 @@ use std::collections::HashSet;
use anyhow::Result; use anyhow::Result;
use crate::{ use crate::{
config::SymbolMappings,
diff::{ diff::{
code::{diff_code, no_diff_code, process_code_symbol}, code::{diff_code, no_diff_code, process_code_symbol},
data::{ data::{
@@ -10,7 +11,7 @@ use crate::{
diff_generic_section, no_diff_symbol, diff_generic_section, no_diff_symbol,
}, },
}, },
obj::{ObjInfo, ObjIns, ObjSection, ObjSectionKind, ObjSymbol, SymbolRef}, obj::{ObjInfo, ObjIns, ObjSection, ObjSectionKind, ObjSymbol, SymbolRef, SECTION_COMMON},
}; };
pub mod code; pub mod code;
@@ -28,8 +29,8 @@ pub mod display;
serde::Serialize, serde::Serialize,
strum::VariantArray, strum::VariantArray,
strum::EnumMessage, strum::EnumMessage,
tsify_next::Tsify,
)] )]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum X86Formatter { pub enum X86Formatter {
#[default] #[default]
#[strum(message = "Intel (default)")] #[strum(message = "Intel (default)")]
@@ -53,8 +54,8 @@ pub enum X86Formatter {
serde::Serialize, serde::Serialize,
strum::VariantArray, strum::VariantArray,
strum::EnumMessage, strum::EnumMessage,
tsify_next::Tsify,
)] )]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum MipsAbi { pub enum MipsAbi {
#[default] #[default]
#[strum(message = "Auto (default)")] #[strum(message = "Auto (default)")]
@@ -78,8 +79,8 @@ pub enum MipsAbi {
serde::Serialize, serde::Serialize,
strum::VariantArray, strum::VariantArray,
strum::EnumMessage, strum::EnumMessage,
tsify_next::Tsify,
)] )]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum MipsInstrCategory { pub enum MipsInstrCategory {
#[default] #[default]
#[strum(message = "Auto (default)")] #[strum(message = "Auto (default)")]
@@ -107,8 +108,8 @@ pub enum MipsInstrCategory {
serde::Serialize, serde::Serialize,
strum::VariantArray, strum::VariantArray,
strum::EnumMessage, strum::EnumMessage,
tsify_next::Tsify,
)] )]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum ArmArchVersion { pub enum ArmArchVersion {
#[default] #[default]
#[strum(message = "Auto (default)")] #[strum(message = "Auto (default)")]
@@ -132,8 +133,8 @@ pub enum ArmArchVersion {
serde::Serialize, serde::Serialize,
strum::VariantArray, strum::VariantArray,
strum::EnumMessage, strum::EnumMessage,
tsify_next::Tsify,
)] )]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum ArmR9Usage { pub enum ArmR9Usage {
#[default] #[default]
#[strum( #[strum(
@@ -153,14 +154,17 @@ pub enum ArmR9Usage {
#[inline] #[inline]
const fn default_true() -> bool { true } const fn default_true() -> bool { true }
#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize, tsify_next::Tsify)] #[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[tsify(from_wasm_abi)] #[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))]
#[serde(default)] #[serde(default)]
pub struct DiffObjConfig { pub struct DiffObjConfig {
pub relax_reloc_diffs: bool, pub relax_reloc_diffs: bool,
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub space_between_args: bool, pub space_between_args: bool,
pub combine_data_sections: bool, pub combine_data_sections: bool,
#[serde(default)]
pub symbol_mappings: MappingConfig,
// x86 // x86
pub x86_formatter: X86Formatter, pub x86_formatter: X86Formatter,
// MIPS // MIPS
@@ -182,6 +186,7 @@ impl Default for DiffObjConfig {
relax_reloc_diffs: false, relax_reloc_diffs: false,
space_between_args: true, space_between_args: true,
combine_data_sections: false, combine_data_sections: false,
symbol_mappings: Default::default(),
x86_formatter: Default::default(), x86_formatter: Default::default(),
mips_abi: Default::default(), mips_abi: Default::default(),
mips_instr_category: Default::default(), mips_instr_category: Default::default(),
@@ -223,8 +228,10 @@ impl ObjSectionDiff {
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ObjSymbolDiff { pub struct ObjSymbolDiff {
/// The symbol ref this object
pub symbol_ref: SymbolRef, pub symbol_ref: SymbolRef,
pub diff_symbol: Option<SymbolRef>, /// The symbol ref in the _other_ object that this symbol was diffed against
pub target_symbol: Option<SymbolRef>,
pub instructions: Vec<ObjInsDiff>, pub instructions: Vec<ObjInsDiff>,
pub match_percent: Option<f32>, pub match_percent: Option<f32>,
} }
@@ -238,7 +245,7 @@ pub struct ObjInsDiff {
pub branch_from: Option<ObjInsBranchFrom>, pub branch_from: Option<ObjInsBranchFrom>,
/// Branches to instruction /// Branches to instruction
pub branch_to: Option<ObjInsBranchTo>, pub branch_to: Option<ObjInsBranchTo>,
/// Arg diffs /// Arg diffs (only contains non-PlainText args)
pub arg_diff: Vec<Option<ObjInsArgDiff>>, pub arg_diff: Vec<Option<ObjInsArgDiff>>,
} }
@@ -294,8 +301,13 @@ pub struct ObjInsBranchTo {
#[derive(Default)] #[derive(Default)]
pub struct ObjDiff { pub struct ObjDiff {
/// A list of all section diffs in the object.
pub sections: Vec<ObjSectionDiff>, pub sections: Vec<ObjSectionDiff>,
/// Common BSS symbols don't live in a section, so they're stored separately.
pub common: Vec<ObjSymbolDiff>, pub common: Vec<ObjSymbolDiff>,
/// If `selecting_left` or `selecting_right` is set, this is the list of symbols
/// that are being mapped to the other object.
pub mapping_symbols: Vec<ObjSymbolDiff>,
} }
impl ObjDiff { impl ObjDiff {
@@ -303,13 +315,14 @@ impl ObjDiff {
let mut result = Self { let mut result = Self {
sections: Vec::with_capacity(obj.sections.len()), sections: Vec::with_capacity(obj.sections.len()),
common: Vec::with_capacity(obj.common.len()), common: Vec::with_capacity(obj.common.len()),
mapping_symbols: vec![],
}; };
for (section_idx, section) in obj.sections.iter().enumerate() { for (section_idx, section) in obj.sections.iter().enumerate() {
let mut symbols = Vec::with_capacity(section.symbols.len()); let mut symbols = Vec::with_capacity(section.symbols.len());
for (symbol_idx, _) in section.symbols.iter().enumerate() { for (symbol_idx, _) in section.symbols.iter().enumerate() {
symbols.push(ObjSymbolDiff { symbols.push(ObjSymbolDiff {
symbol_ref: SymbolRef { section_idx, symbol_idx }, symbol_ref: SymbolRef { section_idx, symbol_idx },
diff_symbol: None, target_symbol: None,
instructions: vec![], instructions: vec![],
match_percent: None, match_percent: None,
}); });
@@ -327,8 +340,8 @@ impl ObjDiff {
} }
for (symbol_idx, _) in obj.common.iter().enumerate() { for (symbol_idx, _) in obj.common.iter().enumerate() {
result.common.push(ObjSymbolDiff { result.common.push(ObjSymbolDiff {
symbol_ref: SymbolRef { section_idx: obj.sections.len(), symbol_idx }, symbol_ref: SymbolRef { section_idx: SECTION_COMMON, symbol_idx },
diff_symbol: None, target_symbol: None,
instructions: vec![], instructions: vec![],
match_percent: None, match_percent: None,
}); });
@@ -348,7 +361,7 @@ impl ObjDiff {
#[inline] #[inline]
pub fn symbol_diff(&self, symbol_ref: SymbolRef) -> &ObjSymbolDiff { pub fn symbol_diff(&self, symbol_ref: SymbolRef) -> &ObjSymbolDiff {
if symbol_ref.section_idx == self.sections.len() { if symbol_ref.section_idx == SECTION_COMMON {
&self.common[symbol_ref.symbol_idx] &self.common[symbol_ref.symbol_idx]
} else { } else {
&self.section_diff(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx] &self.section_diff(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx]
@@ -357,7 +370,7 @@ impl ObjDiff {
#[inline] #[inline]
pub fn symbol_diff_mut(&mut self, symbol_ref: SymbolRef) -> &mut ObjSymbolDiff { pub fn symbol_diff_mut(&mut self, symbol_ref: SymbolRef) -> &mut ObjSymbolDiff {
if symbol_ref.section_idx == self.sections.len() { if symbol_ref.section_idx == SECTION_COMMON {
&mut self.common[symbol_ref.symbol_idx] &mut self.common[symbol_ref.symbol_idx]
} else { } else {
&mut self.section_diff_mut(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx] &mut self.section_diff_mut(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx]
@@ -378,7 +391,7 @@ pub fn diff_objs(
right: Option<&ObjInfo>, right: Option<&ObjInfo>,
prev: Option<&ObjInfo>, prev: Option<&ObjInfo>,
) -> Result<DiffObjsResult> { ) -> Result<DiffObjsResult> {
let symbol_matches = matching_symbols(left, right, prev)?; let symbol_matches = matching_symbols(left, right, prev, &config.symbol_mappings)?;
let section_matches = matching_sections(left, right)?; let section_matches = matching_sections(left, right)?;
let mut left = left.map(|p| (p, ObjDiff::new_from_obj(p))); let mut left = left.map(|p| (p, ObjDiff::new_from_obj(p)));
let mut right = right.map(|p| (p, ObjDiff::new_from_obj(p))); let mut right = right.map(|p| (p, ObjDiff::new_from_obj(p)));
@@ -399,6 +412,8 @@ pub fn diff_objs(
let left_code = process_code_symbol(left_obj, left_symbol_ref, config)?; let left_code = process_code_symbol(left_obj, left_symbol_ref, config)?;
let right_code = process_code_symbol(right_obj, right_symbol_ref, config)?; let right_code = process_code_symbol(right_obj, right_symbol_ref, config)?;
let (left_diff, right_diff) = diff_code( let (left_diff, right_diff) = diff_code(
left_obj,
right_obj,
&left_code, &left_code,
&right_code, &right_code,
left_symbol_ref, left_symbol_ref,
@@ -412,6 +427,8 @@ pub fn diff_objs(
let (prev_obj, prev_out) = prev.as_mut().unwrap(); let (prev_obj, prev_out) = prev.as_mut().unwrap();
let prev_code = process_code_symbol(prev_obj, prev_symbol_ref, config)?; let prev_code = process_code_symbol(prev_obj, prev_symbol_ref, config)?;
let (_, prev_diff) = diff_code( let (_, prev_diff) = diff_code(
left_obj,
right_obj,
&right_code, &right_code,
&prev_code, &prev_code,
right_symbol_ref, right_symbol_ref,
@@ -529,6 +546,17 @@ pub fn diff_objs(
} }
} }
if let (Some((right_obj, right_out)), Some((left_obj, left_out))) =
(right.as_mut(), left.as_mut())
{
if let Some(right_name) = &config.symbol_mappings.selecting_left {
generate_mapping_symbols(right_obj, right_name, left_obj, left_out, config)?;
}
if let Some(left_name) = &config.symbol_mappings.selecting_right {
generate_mapping_symbols(left_obj, left_name, right_obj, right_out, config)?;
}
}
Ok(DiffObjsResult { Ok(DiffObjsResult {
left: left.map(|(_, o)| o), left: left.map(|(_, o)| o),
right: right.map(|(_, o)| o), right: right.map(|(_, o)| o),
@@ -536,6 +564,63 @@ pub fn diff_objs(
}) })
} }
/// When we're selecting a symbol to use as a comparison, we'll create comparisons for all
/// symbols in the other object that match the selected symbol's section and kind. This allows
/// us to display match percentages for all symbols in the other object that could be selected.
fn generate_mapping_symbols(
base_obj: &ObjInfo,
base_name: &str,
target_obj: &ObjInfo,
target_out: &mut ObjDiff,
config: &DiffObjConfig,
) -> Result<()> {
let Some(base_symbol_ref) = symbol_ref_by_name(base_obj, base_name) else {
return Ok(());
};
let (base_section, _base_symbol) = base_obj.section_symbol(base_symbol_ref);
let Some(base_section) = base_section else {
return Ok(());
};
let base_code = match base_section.kind {
ObjSectionKind::Code => Some(process_code_symbol(base_obj, base_symbol_ref, config)?),
_ => None,
};
for (target_section_index, target_section) in
target_obj.sections.iter().enumerate().filter(|(_, s)| s.kind == base_section.kind)
{
for (target_symbol_index, _target_symbol) in target_section.symbols.iter().enumerate() {
let target_symbol_ref =
SymbolRef { section_idx: target_section_index, symbol_idx: target_symbol_index };
match base_section.kind {
ObjSectionKind::Code => {
let target_code = process_code_symbol(target_obj, target_symbol_ref, config)?;
let (left_diff, _right_diff) = diff_code(
target_obj,
base_obj,
&target_code,
base_code.as_ref().unwrap(),
target_symbol_ref,
base_symbol_ref,
config,
)?;
target_out.mapping_symbols.push(left_diff);
}
ObjSectionKind::Data => {
let (left_diff, _right_diff) =
diff_data_symbol(target_obj, base_obj, target_symbol_ref, base_symbol_ref)?;
target_out.mapping_symbols.push(left_diff);
}
ObjSectionKind::Bss => {
let (left_diff, _right_diff) =
diff_bss_symbol(target_obj, base_obj, target_symbol_ref, base_symbol_ref)?;
target_out.mapping_symbols.push(left_diff);
}
}
}
}
Ok(())
}
#[derive(Copy, Clone, Eq, PartialEq)] #[derive(Copy, Clone, Eq, PartialEq)]
struct SymbolMatch { struct SymbolMatch {
left: Option<SymbolRef>, left: Option<SymbolRef>,
@@ -551,19 +636,115 @@ struct SectionMatch {
section_kind: ObjSectionKind, section_kind: ObjSectionKind,
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Deserialize, serde::Serialize)]
pub struct MappingConfig {
/// Manual symbol mappings
pub mappings: SymbolMappings,
/// The right object symbol name that we're selecting a left symbol for
pub selecting_left: Option<String>,
/// The left object symbol name that we're selecting a right symbol for
pub selecting_right: Option<String>,
}
fn symbol_ref_by_name(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
for (section_idx, section) in obj.sections.iter().enumerate() {
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
if symbol.name == name {
return Some(SymbolRef { section_idx, symbol_idx });
}
}
}
None
}
fn apply_symbol_mappings(
left: &ObjInfo,
right: &ObjInfo,
mapping_config: &MappingConfig,
left_used: &mut HashSet<SymbolRef>,
right_used: &mut HashSet<SymbolRef>,
matches: &mut Vec<SymbolMatch>,
) -> Result<()> {
// If we're selecting a symbol to use as a comparison, mark it as used
// This ensures that we don't match it to another symbol at any point
if let Some(left_name) = &mapping_config.selecting_left {
if let Some(left_symbol) = symbol_ref_by_name(left, left_name) {
left_used.insert(left_symbol);
}
}
if let Some(right_name) = &mapping_config.selecting_right {
if let Some(right_symbol) = symbol_ref_by_name(right, right_name) {
right_used.insert(right_symbol);
}
}
// Apply manual symbol mappings
for (left_name, right_name) in &mapping_config.mappings {
let Some(left_symbol) = symbol_ref_by_name(left, left_name) else {
continue;
};
if left_used.contains(&left_symbol) {
continue;
}
let Some(right_symbol) = symbol_ref_by_name(right, right_name) else {
continue;
};
if right_used.contains(&right_symbol) {
continue;
}
let left_section = &left.sections[left_symbol.section_idx];
let right_section = &right.sections[right_symbol.section_idx];
if left_section.kind != right_section.kind {
log::warn!(
"Symbol section kind mismatch: {} ({:?}) vs {} ({:?})",
left_name,
left_section.kind,
right_name,
right_section.kind
);
continue;
}
matches.push(SymbolMatch {
left: Some(left_symbol),
right: Some(right_symbol),
prev: None, // TODO
section_kind: left_section.kind,
});
left_used.insert(left_symbol);
right_used.insert(right_symbol);
}
Ok(())
}
/// Find matching symbols between each object. /// Find matching symbols between each object.
fn matching_symbols( fn matching_symbols(
left: Option<&ObjInfo>, left: Option<&ObjInfo>,
right: Option<&ObjInfo>, right: Option<&ObjInfo>,
prev: Option<&ObjInfo>, prev: Option<&ObjInfo>,
mappings: &MappingConfig,
) -> Result<Vec<SymbolMatch>> { ) -> Result<Vec<SymbolMatch>> {
let mut matches = Vec::new(); let mut matches = Vec::new();
let mut left_used = HashSet::new();
let mut right_used = HashSet::new(); let mut right_used = HashSet::new();
if let Some(left) = left { if let Some(left) = left {
if let Some(right) = right {
apply_symbol_mappings(
left,
right,
mappings,
&mut left_used,
&mut right_used,
&mut matches,
)?;
}
for (section_idx, section) in left.sections.iter().enumerate() { for (section_idx, section) in left.sections.iter().enumerate() {
for (symbol_idx, symbol) in section.symbols.iter().enumerate() { for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
let symbol_ref = SymbolRef { section_idx, symbol_idx };
if left_used.contains(&symbol_ref) {
continue;
}
let symbol_match = SymbolMatch { let symbol_match = SymbolMatch {
left: Some(SymbolRef { section_idx, symbol_idx }), left: Some(symbol_ref),
right: find_symbol(right, symbol, section, Some(&right_used)), right: find_symbol(right, symbol, section, Some(&right_used)),
prev: find_symbol(prev, symbol, section, None), prev: find_symbol(prev, symbol, section, None),
section_kind: section.kind, section_kind: section.kind,
@@ -575,8 +756,12 @@ fn matching_symbols(
} }
} }
for (symbol_idx, symbol) in left.common.iter().enumerate() { for (symbol_idx, symbol) in left.common.iter().enumerate() {
let symbol_ref = SymbolRef { section_idx: SECTION_COMMON, symbol_idx };
if left_used.contains(&symbol_ref) {
continue;
}
let symbol_match = SymbolMatch { let symbol_match = SymbolMatch {
left: Some(SymbolRef { section_idx: left.sections.len(), symbol_idx }), left: Some(symbol_ref),
right: find_common_symbol(right, symbol), right: find_common_symbol(right, symbol),
prev: find_common_symbol(prev, symbol), prev: find_common_symbol(prev, symbol),
section_kind: ObjSectionKind::Bss, section_kind: ObjSectionKind::Bss,
@@ -603,7 +788,7 @@ fn matching_symbols(
} }
} }
for (symbol_idx, symbol) in right.common.iter().enumerate() { for (symbol_idx, symbol) in right.common.iter().enumerate() {
let symbol_ref = SymbolRef { section_idx: right.sections.len(), symbol_idx }; let symbol_ref = SymbolRef { section_idx: SECTION_COMMON, symbol_idx };
if right_used.contains(&symbol_ref) { if right_used.contains(&symbol_ref) {
continue; continue;
} }
@@ -696,7 +881,7 @@ fn find_common_symbol(obj: Option<&ObjInfo>, in_symbol: &ObjSymbol) -> Option<Sy
let obj = obj?; let obj = obj?;
for (symbol_idx, symbol) in obj.common.iter().enumerate() { for (symbol_idx, symbol) in obj.common.iter().enumerate() {
if symbol.name == in_symbol.name { if symbol.name == in_symbol.name {
return Some(SymbolRef { section_idx: obj.sections.len(), symbol_idx }); return Some(SymbolRef { section_idx: SECTION_COMMON, symbol_idx });
} }
} }
None None

View File

@@ -0,0 +1,50 @@
use std::{sync::mpsc::Receiver, task::Waker};
use anyhow::{Context, Result};
use self_update::{
cargo_crate_version,
update::{Release, ReleaseUpdate},
};
use crate::jobs::{start_job, update_status, Job, JobContext, JobResult, JobState};
pub struct CheckUpdateConfig {
pub build_updater: fn() -> Result<Box<dyn ReleaseUpdate>>,
pub bin_names: Vec<String>,
}
pub struct CheckUpdateResult {
pub update_available: bool,
pub latest_release: Release,
pub found_binary: Option<String>,
}
fn run_check_update(
context: &JobContext,
cancel: Receiver<()>,
config: CheckUpdateConfig,
) -> Result<Box<CheckUpdateResult>> {
update_status(context, "Fetching latest release".to_string(), 0, 1, &cancel)?;
let updater = (config.build_updater)().context("Failed to create release updater")?;
let latest_release = updater.get_latest_release()?;
let update_available =
self_update::version::bump_is_greater(cargo_crate_version!(), &latest_release.version)?;
// Find the binary name in the release assets
let mut found_binary = None;
for bin_name in &config.bin_names {
if latest_release.assets.iter().any(|a| &a.name == bin_name) {
found_binary = Some(bin_name.clone());
break;
}
}
update_status(context, "Complete".to_string(), 1, 1, &cancel)?;
Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary }))
}
pub fn start_check_update(waker: Waker, config: CheckUpdateConfig) -> JobState {
start_job(waker, "Check for updates", Job::CheckUpdate, move |context, cancel| {
run_check_update(&context, cancel, config)
.map(|result| JobResult::CheckUpdate(Some(result)))
})
}

View File

@@ -1,14 +1,10 @@
use std::{fs, path::PathBuf, sync::mpsc::Receiver}; use std::{fs, path::PathBuf, sync::mpsc::Receiver, task::Waker};
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use const_format::formatcp;
use crate::{ use crate::{
app::AppConfig, build::{run_make, BuildConfig, BuildStatus},
jobs::{ jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
objdiff::{run_make, BuildConfig, BuildStatus},
start_job, update_status, Job, JobContext, JobResult, JobState,
},
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -23,37 +19,7 @@ pub struct CreateScratchConfig {
pub compiler_flags: String, pub compiler_flags: String,
pub function_name: String, pub function_name: String,
pub target_obj: PathBuf, pub target_obj: PathBuf,
} pub preset_id: Option<u32>,
impl CreateScratchConfig {
pub(crate) fn from_config(config: &AppConfig, function_name: String) -> Result<Self> {
let Some(selected_obj) = &config.selected_obj else {
bail!("No object selected");
};
let Some(target_path) = &selected_obj.target_path else {
bail!("No target path for {}", selected_obj.name);
};
let Some(scratch_config) = &selected_obj.scratch else {
bail!("No scratch configuration for {}", selected_obj.name);
};
Ok(Self {
build_config: BuildConfig::from_config(config),
context_path: scratch_config.ctx_path.clone(),
build_context: scratch_config.build_ctx,
compiler: scratch_config.compiler.clone().unwrap_or_default(),
platform: scratch_config.platform.clone().unwrap_or_default(),
compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(),
function_name,
target_obj: target_path.to_path_buf(),
})
}
pub fn is_available(config: &AppConfig) -> bool {
let Some(selected_obj) = &config.selected_obj else {
return false;
};
selected_obj.target_path.is_some() && selected_obj.scratch.is_some()
}
} }
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)]
@@ -97,22 +63,25 @@ fn run_create_scratch(
update_status(status, "Creating scratch".to_string(), 1, 2, &cancel)?; update_status(status, "Creating scratch".to_string(), 1, 2, &cancel)?;
let diff_flags = [format!("--disassemble={}", config.function_name)]; let diff_flags = [format!("--disassemble={}", config.function_name)];
let diff_flags = serde_json::to_string(&diff_flags).unwrap(); let diff_flags = serde_json::to_string(&diff_flags)?;
let obj_path = project_dir.join(&config.target_obj); let obj_path = project_dir.join(&config.target_obj);
let file = reqwest::blocking::multipart::Part::file(&obj_path) let file = reqwest::blocking::multipart::Part::file(&obj_path)
.with_context(|| format!("Failed to open {}", obj_path.display()))?; .with_context(|| format!("Failed to open {}", obj_path.display()))?;
let form = reqwest::blocking::multipart::Form::new() let mut form = reqwest::blocking::multipart::Form::new()
.text("compiler", config.compiler.clone()) .text("compiler", config.compiler.clone())
.text("platform", config.platform.clone()) .text("platform", config.platform.clone())
.text("compiler_flags", config.compiler_flags.clone()) .text("compiler_flags", config.compiler_flags.clone())
.text("diff_label", config.function_name.clone()) .text("diff_label", config.function_name.clone())
.text("diff_flags", diff_flags) .text("diff_flags", diff_flags)
.text("context", context.unwrap_or_default()) .text("context", context.unwrap_or_default())
.text("source_code", "// Move related code from Context tab to here") .text("source_code", "// Move related code from Context tab to here");
.part("target_obj", file); if let Some(preset) = config.preset_id {
form = form.text("preset", preset.to_string());
}
form = form.part("target_obj", file);
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
let response = client let response = client
.post(formatcp!("{API_HOST}/api/scratch")) .post(format!("{API_HOST}/api/scratch"))
.multipart(form) .multipart(form)
.send() .send()
.map_err(|e| anyhow!("Failed to send request: {}", e))?; .map_err(|e| anyhow!("Failed to send request: {}", e))?;
@@ -126,8 +95,8 @@ fn run_create_scratch(
Ok(Box::from(CreateScratchResult { scratch_url })) Ok(Box::from(CreateScratchResult { scratch_url }))
} }
pub fn start_create_scratch(ctx: &egui::Context, config: CreateScratchConfig) -> JobState { pub fn start_create_scratch(waker: Waker, config: CreateScratchConfig) -> JobState {
start_job(ctx, "Create scratch", Job::CreateScratch, move |context, cancel| { start_job(waker, "Create scratch", Job::CreateScratch, move |context, cancel| {
run_create_scratch(&context, cancel, config) run_create_scratch(&context, cancel, config)
.map(|result| JobResult::CreateScratch(Some(result))) .map(|result| JobResult::CreateScratch(Some(result)))
}) })

View File

@@ -4,6 +4,7 @@ use std::{
mpsc::{Receiver, Sender, TryRecvError}, mpsc::{Receiver, Sender, TryRecvError},
Arc, RwLock, Arc, RwLock,
}, },
task::Waker,
thread::JoinHandle, thread::JoinHandle,
}; };
@@ -53,7 +54,6 @@ impl JobQueue {
} }
/// Returns whether any job is running. /// Returns whether any job is running.
#[allow(dead_code)]
pub fn any_running(&self) -> bool { pub fn any_running(&self) -> bool {
self.jobs.iter().any(|job| { self.jobs.iter().any(|job| {
if let Some(handle) = &job.handle { if let Some(handle) = &job.handle {
@@ -96,12 +96,53 @@ impl JobQueue {
/// Removes a job from the queue given its ID. /// Removes a job from the queue given its ID.
pub fn remove(&mut self, id: usize) { self.jobs.retain(|job| job.id != id); } pub fn remove(&mut self, id: usize) { self.jobs.retain(|job| job.id != id); }
/// Collects the results of all finished jobs and handles any errors.
pub fn collect_results(&mut self) {
let mut results = vec![];
for (job, result) in self.iter_finished() {
match result {
Ok(result) => {
match result {
JobResult::None => {
// Job context contains the error
}
_ => results.push(result),
}
}
Err(err) => {
let err = if let Some(msg) = err.downcast_ref::<&'static str>() {
anyhow::Error::msg(*msg)
} else if let Some(msg) = err.downcast_ref::<String>() {
anyhow::Error::msg(msg.clone())
} else {
anyhow::Error::msg("Thread panicked")
};
let result = job.context.status.write();
if let Ok(mut guard) = result {
guard.error = Some(err);
} else {
drop(result);
job.context.status = Arc::new(RwLock::new(JobStatus {
title: "Error".to_string(),
progress_percent: 0.0,
progress_items: None,
status: String::new(),
error: Some(err),
}));
}
}
}
}
self.results.append(&mut results);
self.clear_finished();
}
} }
#[derive(Clone)] #[derive(Clone)]
pub struct JobContext { pub struct JobContext {
pub status: Arc<RwLock<JobStatus>>, pub status: Arc<RwLock<JobStatus>>,
pub egui: egui::Context, pub waker: Waker,
} }
pub struct JobState { pub struct JobState {
@@ -137,7 +178,7 @@ fn should_cancel(rx: &Receiver<()>) -> bool {
} }
fn start_job( fn start_job(
ctx: &egui::Context, waker: Waker,
title: &str, title: &str,
kind: Job, kind: Job,
run: impl FnOnce(JobContext, Receiver<()>) -> Result<JobResult> + Send + 'static, run: impl FnOnce(JobContext, Receiver<()>) -> Result<JobResult> + Send + 'static,
@@ -149,11 +190,10 @@ fn start_job(
status: String::new(), status: String::new(),
error: None, error: None,
})); }));
let context = JobContext { status: status.clone(), egui: ctx.clone() }; let context = JobContext { status: status.clone(), waker: waker.clone() };
let context_inner = JobContext { status: status.clone(), egui: ctx.clone() }; let context_inner = JobContext { status: status.clone(), waker };
let (tx, rx) = std::sync::mpsc::channel(); let (tx, rx) = std::sync::mpsc::channel();
let handle = std::thread::spawn(move || { let handle = std::thread::spawn(move || match run(context_inner, rx) {
return match run(context_inner, rx) {
Ok(state) => state, Ok(state) => state,
Err(e) => { Err(e) => {
if let Ok(mut w) = status.write() { if let Ok(mut w) = status.write() {
@@ -161,10 +201,9 @@ fn start_job(
} }
JobResult::None JobResult::None
} }
};
}); });
let id = JOB_ID.fetch_add(1, Ordering::Relaxed); let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
log::info!("Started job {}", id); // log::info!("Started job {}", id); TODO
JobState { id, kind, handle: Some(handle), context, cancel: tx } JobState { id, kind, handle: Some(handle), context, cancel: tx }
} }
@@ -186,6 +225,6 @@ fn update_status(
w.status = str; w.status = str;
} }
drop(w); drop(w);
context.egui.request_repaint(); context.waker.wake_by_ref();
Ok(()) Ok(())
} }

View File

@@ -0,0 +1,199 @@
use std::{path::PathBuf, sync::mpsc::Receiver, task::Waker};
use anyhow::{anyhow, Error, Result};
use time::OffsetDateTime;
use crate::{
build::{run_make, BuildConfig, BuildStatus},
config::SymbolMappings,
diff::{diff_objs, DiffObjConfig, MappingConfig, ObjDiff},
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
obj::{read, ObjInfo},
};
pub struct ObjDiffConfig {
pub build_config: BuildConfig,
pub build_base: bool,
pub build_target: bool,
pub target_path: Option<PathBuf>,
pub base_path: Option<PathBuf>,
pub diff_obj_config: DiffObjConfig,
pub symbol_mappings: SymbolMappings,
pub selecting_left: Option<String>,
pub selecting_right: Option<String>,
}
pub struct ObjDiffResult {
pub first_status: BuildStatus,
pub second_status: BuildStatus,
pub first_obj: Option<(ObjInfo, ObjDiff)>,
pub second_obj: Option<(ObjInfo, ObjDiff)>,
pub time: OffsetDateTime,
}
fn run_build(
context: &JobContext,
cancel: Receiver<()>,
mut config: ObjDiffConfig,
) -> Result<Box<ObjDiffResult>> {
// Use the per-object symbol mappings, we don't set mappings globally
config.diff_obj_config.symbol_mappings = MappingConfig {
mappings: config.symbol_mappings,
selecting_left: config.selecting_left,
selecting_right: config.selecting_right,
};
let mut target_path_rel = None;
let mut base_path_rel = None;
if config.build_target || config.build_base {
let project_dir = config
.build_config
.project_dir
.as_ref()
.ok_or_else(|| Error::msg("Missing project dir"))?;
if let Some(target_path) = &config.target_path {
target_path_rel = Some(target_path.strip_prefix(project_dir).map_err(|_| {
anyhow!(
"Target path '{}' doesn't begin with '{}'",
target_path.display(),
project_dir.display()
)
})?);
}
if let Some(base_path) = &config.base_path {
base_path_rel = Some(base_path.strip_prefix(project_dir).map_err(|_| {
anyhow!(
"Base path '{}' doesn't begin with '{}'",
base_path.display(),
project_dir.display()
)
})?);
};
}
let mut total = 1;
if config.build_target && target_path_rel.is_some() {
total += 1;
}
if config.build_base && base_path_rel.is_some() {
total += 1;
}
if config.target_path.is_some() {
total += 1;
}
if config.base_path.is_some() {
total += 1;
}
let mut step_idx = 0;
let mut first_status = match target_path_rel {
Some(target_path_rel) if config.build_target => {
update_status(
context,
format!("Building target {}", target_path_rel.display()),
step_idx,
total,
&cancel,
)?;
step_idx += 1;
run_make(&config.build_config, target_path_rel)
}
_ => BuildStatus::default(),
};
let mut second_status = match base_path_rel {
Some(base_path_rel) if config.build_base => {
update_status(
context,
format!("Building base {}", base_path_rel.display()),
step_idx,
total,
&cancel,
)?;
step_idx += 1;
run_make(&config.build_config, base_path_rel)
}
_ => BuildStatus::default(),
};
let time = OffsetDateTime::now_utc();
let first_obj = match &config.target_path {
Some(target_path) if first_status.success => {
update_status(
context,
format!("Loading target {}", target_path.display()),
step_idx,
total,
&cancel,
)?;
step_idx += 1;
match read::read(target_path, &config.diff_obj_config) {
Ok(obj) => Some(obj),
Err(e) => {
first_status = BuildStatus {
success: false,
stdout: format!("Loading object '{}'", target_path.display()),
stderr: format!("{:#}", e),
..Default::default()
};
None
}
}
}
Some(_) => {
step_idx += 1;
None
}
_ => None,
};
let second_obj = match &config.base_path {
Some(base_path) if second_status.success => {
update_status(
context,
format!("Loading base {}", base_path.display()),
step_idx,
total,
&cancel,
)?;
step_idx += 1;
match read::read(base_path, &config.diff_obj_config) {
Ok(obj) => Some(obj),
Err(e) => {
second_status = BuildStatus {
success: false,
stdout: format!("Loading object '{}'", base_path.display()),
stderr: format!("{:#}", e),
..Default::default()
};
None
}
}
}
Some(_) => {
step_idx += 1;
None
}
_ => None,
};
update_status(context, "Performing diff".to_string(), step_idx, total, &cancel)?;
step_idx += 1;
let result = diff_objs(&config.diff_obj_config, first_obj.as_ref(), second_obj.as_ref(), None)?;
update_status(context, "Complete".to_string(), step_idx, total, &cancel)?;
Ok(Box::new(ObjDiffResult {
first_status,
second_status,
first_obj: first_obj.and_then(|o| result.left.map(|d| (o, d))),
second_obj: second_obj.and_then(|o| result.right.map(|d| (o, d))),
time,
}))
}
pub fn start_build(waker: Waker, config: ObjDiffConfig) -> JobState {
start_job(waker, "Build", Job::ObjDiff, move |context, cancel| {
run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
})
}

View File

@@ -3,14 +3,19 @@ use std::{
fs::File, fs::File,
path::PathBuf, path::PathBuf,
sync::mpsc::Receiver, sync::mpsc::Receiver,
task::Waker,
}; };
use anyhow::{Context, Result}; use anyhow::{Context, Result};
pub use self_update; // Re-export self_update crate
use self_update::update::ReleaseUpdate;
use crate::{ use crate::jobs::{start_job, update_status, Job, JobContext, JobResult, JobState};
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
update::build_updater, pub struct UpdateConfig {
}; pub build_updater: fn() -> Result<Box<dyn ReleaseUpdate>>,
pub bin_name: String,
}
pub struct UpdateResult { pub struct UpdateResult {
pub exe_path: PathBuf, pub exe_path: PathBuf,
@@ -19,16 +24,15 @@ pub struct UpdateResult {
fn run_update( fn run_update(
status: &JobContext, status: &JobContext,
cancel: Receiver<()>, cancel: Receiver<()>,
bin_name: String, config: UpdateConfig,
) -> Result<Box<UpdateResult>> { ) -> Result<Box<UpdateResult>> {
update_status(status, "Fetching latest release".to_string(), 0, 3, &cancel)?; update_status(status, "Fetching latest release".to_string(), 0, 3, &cancel)?;
let updater = build_updater().context("Failed to create release updater")?; let updater = (config.build_updater)().context("Failed to create release updater")?;
let latest_release = updater.get_latest_release()?; let latest_release = updater.get_latest_release()?;
let asset = latest_release let asset =
.assets latest_release.assets.iter().find(|a| a.name == config.bin_name).ok_or_else(|| {
.iter() anyhow::Error::msg(format!("No release asset for {}", config.bin_name))
.find(|a| a.name == bin_name) })?;
.ok_or_else(|| anyhow::Error::msg(format!("No release asset for {bin_name}")))?;
update_status(status, "Downloading release".to_string(), 1, 3, &cancel)?; update_status(status, "Downloading release".to_string(), 1, 3, &cancel)?;
let tmp_dir = tempfile::Builder::new().prefix("update").tempdir_in(current_dir()?)?; let tmp_dir = tempfile::Builder::new().prefix("update").tempdir_in(current_dir()?)?;
@@ -47,9 +51,7 @@ fn run_update(
#[cfg(unix)] #[cfg(unix)]
{ {
use std::{fs, os::unix::fs::PermissionsExt}; use std::{fs, os::unix::fs::PermissionsExt};
let mut perms = fs::metadata(&target_file)?.permissions(); fs::set_permissions(&target_file, fs::Permissions::from_mode(0o755))?;
perms.set_mode(0o755);
fs::set_permissions(&target_file, perms)?;
} }
tmp_dir.close()?; tmp_dir.close()?;
@@ -57,8 +59,8 @@ fn run_update(
Ok(Box::from(UpdateResult { exe_path: target_file })) Ok(Box::from(UpdateResult { exe_path: target_file }))
} }
pub fn start_update(ctx: &egui::Context, bin_name: String) -> JobState { pub fn start_update(waker: Waker, config: UpdateConfig) -> JobState {
start_job(ctx, "Update app", Job::Update, move |context, cancel| { start_job(waker, "Update app", Job::Update, move |context, cancel| {
run_update(&context, cancel, bin_name).map(JobResult::Update) run_update(&context, cancel, config).map(JobResult::Update)
}) })
} }

View File

@@ -2,10 +2,15 @@
pub mod arch; pub mod arch;
#[cfg(feature = "bindings")] #[cfg(feature = "bindings")]
pub mod bindings; pub mod bindings;
#[cfg(feature = "build")]
pub mod build;
#[cfg(feature = "config")] #[cfg(feature = "config")]
pub mod config; pub mod config;
#[cfg(feature = "any-arch")] #[cfg(feature = "any-arch")]
pub mod diff; pub mod diff;
#[cfg(feature = "build")]
pub mod jobs;
#[cfg(feature = "any-arch")] #[cfg(feature = "any-arch")]
pub mod obj; pub mod obj;
#[cfg(feature = "any-arch")]
pub mod util; pub mod util;

View File

@@ -85,6 +85,9 @@ pub enum ObjInsArg {
} }
impl ObjInsArg { impl ObjInsArg {
#[inline]
pub fn is_plain_text(&self) -> bool { matches!(self, ObjInsArg::PlainText(_)) }
pub fn loose_eq(&self, other: &ObjInsArg) -> bool { pub fn loose_eq(&self, other: &ObjInsArg) -> bool {
match (self, other) { match (self, other) {
(ObjInsArg::Arg(a), ObjInsArg::Arg(b)) => a.loose_eq(b), (ObjInsArg::Arg(a), ObjInsArg::Arg(b)) => a.loose_eq(b),
@@ -100,7 +103,7 @@ pub struct ObjIns {
pub address: u64, pub address: u64,
pub size: u8, pub size: u8,
pub op: u16, pub op: u16,
pub mnemonic: String, pub mnemonic: Cow<'static, str>,
pub args: Vec<ObjInsArg>, pub args: Vec<ObjInsArg>,
pub reloc: Option<ObjReloc>, pub reloc: Option<ObjReloc>,
pub branch_dest: Option<u64>, pub branch_dest: Option<u64>,
@@ -112,6 +115,23 @@ pub struct ObjIns {
pub orig: Option<String>, pub orig: Option<String>,
} }
impl ObjIns {
/// Iterate over non-PlainText arguments.
#[inline]
pub fn iter_args(&self) -> impl DoubleEndedIterator<Item = &ObjInsArg> {
self.args.iter().filter(|a| !a.is_plain_text())
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
pub enum ObjSymbolKind {
#[default]
Unknown,
Function,
Object,
Section,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ObjSymbol { pub struct ObjSymbol {
pub name: String, pub name: String,
@@ -120,8 +140,9 @@ pub struct ObjSymbol {
pub section_address: u64, pub section_address: u64,
pub size: u64, pub size: u64,
pub size_known: bool, pub size_known: bool,
pub kind: ObjSymbolKind,
pub flags: ObjSymbolFlagSet, pub flags: ObjSymbolFlagSet,
pub addend: i64, pub orig_section_index: Option<usize>,
/// Original virtual address (from .note.split section) /// Original virtual address (from .note.split section)
pub virtual_address: Option<u64>, pub virtual_address: Option<u64>,
/// Original index in object symbol table /// Original index in object symbol table
@@ -145,7 +166,7 @@ pub struct ObjReloc {
pub flags: RelocationFlags, pub flags: RelocationFlags,
pub address: u64, pub address: u64,
pub target: ObjSymbol, pub target: ObjSymbol,
pub target_section: Option<String>, pub addend: i64,
} }
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
@@ -154,9 +175,11 @@ pub struct SymbolRef {
pub symbol_idx: usize, pub symbol_idx: usize,
} }
pub const SECTION_COMMON: usize = usize::MAX - 1;
impl ObjInfo { impl ObjInfo {
pub fn section_symbol(&self, symbol_ref: SymbolRef) -> (Option<&ObjSection>, &ObjSymbol) { pub fn section_symbol(&self, symbol_ref: SymbolRef) -> (Option<&ObjSection>, &ObjSymbol) {
if symbol_ref.section_idx == self.sections.len() { if symbol_ref.section_idx == SECTION_COMMON {
let symbol = &self.common[symbol_ref.symbol_idx]; let symbol = &self.common[symbol_ref.symbol_idx];
return (None, symbol); return (None, symbol);
} }

View File

@@ -13,8 +13,8 @@ use object::{
endian::LittleEndian as LE, endian::LittleEndian as LE,
pe::{ImageAuxSymbolFunctionBeginEnd, ImageLinenumber}, pe::{ImageAuxSymbolFunctionBeginEnd, ImageLinenumber},
read::coff::{CoffFile, CoffHeader, ImageSymbol}, read::coff::{CoffFile, CoffHeader, ImageSymbol},
BinaryFormat, File, Object, ObjectSection, ObjectSymbol, RelocationTarget, SectionIndex, BinaryFormat, File, Object, ObjectSection, ObjectSymbol, RelocationTarget, Section,
SectionKind, Symbol, SymbolIndex, SymbolKind, SymbolScope, SymbolSection, SectionIndex, SectionKind, Symbol, SymbolIndex, SymbolKind, SymbolScope,
}; };
use crate::{ use crate::{
@@ -23,6 +23,7 @@ use crate::{
obj::{ obj::{
split_meta::{SplitMeta, SPLITMETA_SECTION}, split_meta::{SplitMeta, SPLITMETA_SECTION},
ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags, ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags,
ObjSymbolKind,
}, },
util::{read_u16, read_u32}, util::{read_u16, read_u32},
}; };
@@ -40,7 +41,6 @@ fn to_obj_symbol(
arch: &dyn ObjArch, arch: &dyn ObjArch,
obj_file: &File<'_>, obj_file: &File<'_>,
symbol: &Symbol<'_, '_>, symbol: &Symbol<'_, '_>,
addend: i64,
split_meta: Option<&SplitMeta>, split_meta: Option<&SplitMeta>,
) -> Result<ObjSymbol> { ) -> Result<ObjSymbol> {
let mut name = symbol.name().context("Failed to process symbol name")?; let mut name = symbol.name().context("Failed to process symbol name")?;
@@ -64,10 +64,8 @@ fn to_obj_symbol(
if obj_file.format() == BinaryFormat::Elf && symbol.scope() == SymbolScope::Linkage { if obj_file.format() == BinaryFormat::Elf && symbol.scope() == SymbolScope::Linkage {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Hidden); flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Hidden);
} }
if arch #[cfg(feature = "ppc")]
.ppc() if arch.ppc().and_then(|a| a.extab.as_ref()).is_some_and(|e| e.contains_key(&symbol.index().0))
.and_then(|a| a.extab.as_ref())
.map_or(false, |e| e.contains_key(&symbol.index().0))
{ {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::HasExtra); flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::HasExtra);
} }
@@ -94,6 +92,13 @@ fn to_obj_symbol(
}) })
.unwrap_or(&[]); .unwrap_or(&[]);
let kind = match symbol.kind() {
SymbolKind::Text => ObjSymbolKind::Function,
SymbolKind::Data => ObjSymbolKind::Object,
SymbolKind::Section => ObjSymbolKind::Section,
_ => ObjSymbolKind::Unknown,
};
Ok(ObjSymbol { Ok(ObjSymbol {
name: name.to_string(), name: name.to_string(),
demangled_name, demangled_name,
@@ -101,8 +106,9 @@ fn to_obj_symbol(
section_address, section_address,
size: symbol.size(), size: symbol.size(),
size_known: symbol.size() != 0, size_known: symbol.size() != 0,
kind,
flags, flags,
addend, orig_section_index: symbol.section_index().map(|i| i.0),
virtual_address, virtual_address,
original_index: Some(symbol.index().0), original_index: Some(symbol.index().0),
bytes: bytes.to_vec(), bytes: bytes.to_vec(),
@@ -152,16 +158,15 @@ fn symbols_by_section(
arch: &dyn ObjArch, arch: &dyn ObjArch,
obj_file: &File<'_>, obj_file: &File<'_>,
section: &ObjSection, section: &ObjSection,
section_symbols: &[Symbol<'_, '_>],
split_meta: Option<&SplitMeta>, split_meta: Option<&SplitMeta>,
name_counts: &mut HashMap<String, u32>, name_counts: &mut HashMap<String, u32>,
) -> Result<Vec<ObjSymbol>> { ) -> Result<Vec<ObjSymbol>> {
let mut result = Vec::<ObjSymbol>::new(); let mut result = Vec::<ObjSymbol>::new();
for symbol in obj_file.symbols() { for symbol in section_symbols {
if symbol.kind() == SymbolKind::Section { if symbol.kind() == SymbolKind::Section {
continue; continue;
} }
if let Some(index) = symbol.section().index() {
if index.0 == section.orig_index {
if symbol.is_local() && section.kind == ObjSectionKind::Code { if symbol.is_local() && section.kind == ObjSectionKind::Code {
// TODO strip local syms in diff? // TODO strip local syms in diff?
let name = symbol.name().context("Failed to process symbol name")?; let name = symbol.name().context("Failed to process symbol name")?;
@@ -169,9 +174,7 @@ fn symbols_by_section(
continue; continue;
} }
} }
result.push(to_obj_symbol(arch, obj_file, &symbol, 0, split_meta)?); result.push(to_obj_symbol(arch, obj_file, symbol, split_meta)?);
}
}
} }
result.sort_by(|a, b| a.address.cmp(&b.address).then(a.size.cmp(&b.size))); result.sort_by(|a, b| a.address.cmp(&b.address).then(a.size.cmp(&b.size)));
let mut iter = result.iter_mut().peekable(); let mut iter = result.iter_mut().peekable();
@@ -182,6 +185,13 @@ fn symbols_by_section(
} else { } else {
symbol.size = (section.address + section.size) - symbol.address; symbol.size = (section.address + section.size) - symbol.address;
} }
// Set symbol kind if we ended up with a non-zero size
if symbol.kind == ObjSymbolKind::Unknown && symbol.size > 0 {
symbol.kind = match section.kind {
ObjSectionKind::Code => ObjSymbolKind::Function,
ObjSectionKind::Data | ObjSectionKind::Bss => ObjSymbolKind::Object,
};
}
} }
} }
if result.is_empty() { if result.is_empty() {
@@ -199,8 +209,12 @@ fn symbols_by_section(
section_address: 0, section_address: 0,
size: section.size, size: section.size,
size_known: true, size_known: true,
kind: match section.kind {
ObjSectionKind::Code => ObjSymbolKind::Function,
ObjSectionKind::Data | ObjSectionKind::Bss => ObjSymbolKind::Object,
},
flags: Default::default(), flags: Default::default(),
addend: 0, orig_section_index: Some(section.orig_index),
virtual_address: None, virtual_address: None,
original_index: None, original_index: None,
bytes: Vec::new(), bytes: Vec::new(),
@@ -217,51 +231,75 @@ fn common_symbols(
obj_file obj_file
.symbols() .symbols()
.filter(Symbol::is_common) .filter(Symbol::is_common)
.map(|symbol| to_obj_symbol(arch, obj_file, &symbol, 0, split_meta)) .map(|symbol| to_obj_symbol(arch, obj_file, &symbol, split_meta))
.collect::<Result<Vec<ObjSymbol>>>() .collect::<Result<Vec<ObjSymbol>>>()
} }
const LOW_PRIORITY_SYMBOLS: &[&str] =
&["__gnu_compiled_c", "__gnu_compiled_cplusplus", "gcc2_compiled."];
fn best_symbol<'r, 'data, 'file>(
symbols: &'r [Symbol<'data, 'file>],
address: u64,
) -> Option<&'r Symbol<'data, 'file>> {
let mut closest_symbol_index = match symbols.binary_search_by_key(&address, |s| s.address()) {
Ok(index) => Some(index),
Err(index) => index.checked_sub(1),
}?;
// The binary search may not find the first symbol at the address, so work backwards
let target_address = symbols[closest_symbol_index].address();
while let Some(prev_index) = closest_symbol_index.checked_sub(1) {
if symbols[prev_index].address() != target_address {
break;
}
closest_symbol_index = prev_index;
}
let mut best_symbol: Option<&'r Symbol<'data, 'file>> = None;
for symbol in symbols.iter().skip(closest_symbol_index) {
if symbol.address() > address {
break;
}
if symbol.kind() == SymbolKind::Section
|| (symbol.size() > 0 && (symbol.address() + symbol.size()) <= address)
{
continue;
}
// TODO priority ranking with visibility, etc
if let Some(best) = best_symbol {
if LOW_PRIORITY_SYMBOLS.contains(&best.name().unwrap_or_default())
&& !LOW_PRIORITY_SYMBOLS.contains(&symbol.name().unwrap_or_default())
{
best_symbol = Some(symbol);
}
} else {
best_symbol = Some(symbol);
}
}
best_symbol
}
fn find_section_symbol( fn find_section_symbol(
arch: &dyn ObjArch, arch: &dyn ObjArch,
obj_file: &File<'_>, obj_file: &File<'_>,
target: &Symbol<'_, '_>, section: &Section,
section_symbols: &[Symbol<'_, '_>],
address: u64, address: u64,
split_meta: Option<&SplitMeta>, split_meta: Option<&SplitMeta>,
) -> Result<ObjSymbol> { ) -> Result<ObjSymbol> {
let section_index = if let Some(symbol) = best_symbol(section_symbols, address) {
target.section_index().ok_or_else(|| anyhow::Error::msg("Unknown section index"))?; return to_obj_symbol(arch, obj_file, symbol, split_meta);
let section = obj_file.section_by_index(section_index)?;
let mut closest_symbol: Option<Symbol<'_, '_>> = None;
for symbol in obj_file.symbols() {
if !matches!(symbol.section_index(), Some(idx) if idx == section_index) {
continue;
} }
if symbol.kind() == SymbolKind::Section || symbol.address() != address { // Fallback to section symbol
if symbol.address() < address
&& symbol.size() != 0
&& (closest_symbol.is_none()
|| matches!(&closest_symbol, Some(s) if s.address() <= symbol.address()))
{
closest_symbol = Some(symbol);
}
continue;
}
return to_obj_symbol(arch, obj_file, &symbol, 0, split_meta);
}
let (name, offset) = closest_symbol
.and_then(|s| s.name().map(|n| (n, s.address())).ok())
.or_else(|| section.name().map(|n| (n, section.address())).ok())
.unwrap_or(("<unknown>", 0));
let offset_addr = address - offset;
Ok(ObjSymbol { Ok(ObjSymbol {
name: name.to_string(), name: section.name()?.to_string(),
demangled_name: None, demangled_name: None,
address: offset, address: section.address(),
section_address: address - section.address(), section_address: 0,
size: 0, size: 0,
size_known: false, size_known: false,
kind: ObjSymbolKind::Section,
flags: Default::default(), flags: Default::default(),
addend: offset_addr as i64, orig_section_index: Some(section.index().0),
virtual_address: None, virtual_address: None,
original_index: None, original_index: None,
bytes: Vec::new(), bytes: Vec::new(),
@@ -272,6 +310,7 @@ fn relocations_by_section(
arch: &dyn ObjArch, arch: &dyn ObjArch,
obj_file: &File<'_>, obj_file: &File<'_>,
section: &ObjSection, section: &ObjSection,
section_symbols: &[Vec<Symbol<'_, '_>>],
split_meta: Option<&SplitMeta>, split_meta: Option<&SplitMeta>,
) -> Result<Vec<ObjReloc>> { ) -> Result<Vec<ObjReloc>> {
let obj_section = obj_file.section_by_index(SectionIndex(section.orig_index))?; let obj_section = obj_file.section_by_index(SectionIndex(section.orig_index))?;
@@ -293,33 +332,43 @@ fn relocations_by_section(
}; };
symbol symbol
} }
RelocationTarget::Absolute => {
log::warn!("Ignoring absolute relocation @ {}:{:#x}", section.name, address);
continue;
}
_ => bail!("Unhandled relocation target: {:?}", reloc.target()), _ => bail!("Unhandled relocation target: {:?}", reloc.target()),
}; };
let flags = reloc.flags(); // TODO validate reloc here? let flags = reloc.flags(); // TODO validate reloc here?
let target_section = match symbol.section() { let mut addend = if reloc.has_implicit_addend() {
SymbolSection::Common => Some(".comm".to_string()),
SymbolSection::Section(idx) => {
obj_file.section_by_index(idx).and_then(|s| s.name().map(|s| s.to_string())).ok()
}
_ => None,
};
let addend = if reloc.has_implicit_addend() {
arch.implcit_addend(obj_file, section, address, &reloc)? arch.implcit_addend(obj_file, section, address, &reloc)?
} else { } else {
reloc.addend() reloc.addend()
}; };
// println!("Reloc: {reloc:?}, symbol: {symbol:?}, addend: {addend:#x}");
let target = match symbol.kind() { let target = match symbol.kind() {
SymbolKind::Text | SymbolKind::Data | SymbolKind::Label | SymbolKind::Unknown => { SymbolKind::Text | SymbolKind::Data | SymbolKind::Label | SymbolKind::Unknown => {
to_obj_symbol(arch, obj_file, &symbol, addend, split_meta) to_obj_symbol(arch, obj_file, &symbol, split_meta)?
} }
SymbolKind::Section => { SymbolKind::Section => {
ensure!(addend >= 0, "Negative addend in reloc: {addend}"); ensure!(addend >= 0, "Negative addend in section reloc: {addend}");
find_section_symbol(arch, obj_file, &symbol, addend as u64, split_meta) let section_index = symbol
.section_index()
.ok_or_else(|| anyhow!("Section symbol {symbol:?} has no section index"))?;
let section = obj_file.section_by_index(section_index)?;
let symbol = find_section_symbol(
arch,
obj_file,
&section,
&section_symbols[section_index.0],
addend as u64,
split_meta,
)?;
// Adjust addend to be relative to the selected symbol
addend = (symbol.address - section.address()) as i64;
symbol
} }
kind => Err(anyhow!("Unhandled relocation symbol type {kind:?}")), kind => bail!("Unhandled relocation symbol type {kind:?}"),
}?; };
relocations.push(ObjReloc { flags, address, target, target_section }); relocations.push(ObjReloc { flags, address, target, addend });
} }
Ok(relocations) Ok(relocations)
} }
@@ -384,9 +433,9 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection], obj_data: &[u8])
let mut text_sections = let mut text_sections =
obj_file.sections().filter(|s| s.kind() == SectionKind::Text); obj_file.sections().filter(|s| s.kind() == SectionKind::Text);
let section_index = text_sections.next().map(|s| s.index().0); let section_index = text_sections.next().map(|s| s.index().0);
let mut lines = section_index.map(|index| { let mut lines = section_index
&mut sections.iter_mut().find(|s| s.orig_index == index).unwrap().line_info .and_then(|index| sections.iter_mut().find(|s| s.orig_index == index))
}); .map(|s| &mut s.line_info);
let mut rows = program.rows(); let mut rows = program.rows();
while let Some((_header, row)) = rows.next_row()? { while let Some((_header, row)) = rows.next_row()? {
@@ -397,13 +446,9 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection], obj_data: &[u8])
// The next row is the start of a new sequence, which means we must // The next row is the start of a new sequence, which means we must
// advance to the next .text section. // advance to the next .text section.
let section_index = text_sections.next().map(|s| s.index().0); let section_index = text_sections.next().map(|s| s.index().0);
lines = section_index.map(|index| { lines = section_index
&mut sections .and_then(|index| sections.iter_mut().find(|s| s.orig_index == index))
.iter_mut() .map(|s| &mut s.line_info);
.find(|s| s.orig_index == index)
.unwrap()
.line_info
});
} }
} }
} }
@@ -539,8 +584,9 @@ fn update_combined_symbol(symbol: ObjSymbol, address_change: i64) -> Result<ObjS
section_address: (symbol.section_address as i64 + address_change).try_into()?, section_address: (symbol.section_address as i64 + address_change).try_into()?,
size: symbol.size, size: symbol.size,
size_known: symbol.size_known, size_known: symbol.size_known,
kind: symbol.kind,
flags: symbol.flags, flags: symbol.flags,
addend: symbol.addend, orig_section_index: symbol.orig_section_index,
virtual_address: if let Some(virtual_address) = symbol.virtual_address { virtual_address: if let Some(virtual_address) = symbol.virtual_address {
Some((virtual_address as i64 + address_change).try_into()?) Some((virtual_address as i64 + address_change).try_into()?)
} else { } else {
@@ -567,7 +613,7 @@ fn combine_sections(section: ObjSection, combine: ObjSection) -> Result<ObjSecti
flags: reloc.flags, flags: reloc.flags,
address: (reloc.address as i64 + address_change).try_into()?, address: (reloc.address as i64 + address_change).try_into()?,
target: reloc.target, // TODO: Should be updated? target: reloc.target, // TODO: Should be updated?
target_section: reloc.target_section, // TODO: Same as above addend: reloc.addend,
}); });
} }
@@ -647,18 +693,40 @@ pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result<ObjInfo> {
let obj_file = File::parse(data)?; let obj_file = File::parse(data)?;
let arch = new_arch(&obj_file)?; let arch = new_arch(&obj_file)?;
let split_meta = split_meta(&obj_file)?; let split_meta = split_meta(&obj_file)?;
// Create sorted symbol list for each section
let mut section_symbols = Vec::with_capacity(obj_file.sections().count());
for section in obj_file.sections() {
let mut symbols = obj_file
.symbols()
.filter(|s| s.section_index() == Some(section.index()))
.collect::<Vec<_>>();
symbols.sort_by_key(|s| s.address());
let section_index = section.index().0;
if section_index >= section_symbols.len() {
section_symbols.resize_with(section_index + 1, Vec::new);
}
section_symbols[section_index] = symbols;
}
let mut sections = filter_sections(&obj_file, split_meta.as_ref())?; let mut sections = filter_sections(&obj_file, split_meta.as_ref())?;
let mut name_counts: HashMap<String, u32> = HashMap::new(); let mut section_name_counts: HashMap<String, u32> = HashMap::new();
for section in &mut sections { for section in &mut sections {
section.symbols = symbols_by_section( section.symbols = symbols_by_section(
arch.as_ref(), arch.as_ref(),
&obj_file, &obj_file,
section, section,
&section_symbols[section.orig_index],
split_meta.as_ref(),
&mut section_name_counts,
)?;
section.relocations = relocations_by_section(
arch.as_ref(),
&obj_file,
section,
&section_symbols,
split_meta.as_ref(), split_meta.as_ref(),
&mut name_counts,
)?; )?;
section.relocations =
relocations_by_section(arch.as_ref(), &obj_file, section, split_meta.as_ref())?;
} }
if config.combine_data_sections { if config.combine_data_sections {
combine_data_sections(&mut sections)?; combine_data_sections(&mut sections)?;

View File

@@ -25,11 +25,11 @@ wsl = []
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
bytes = "1.7" bytes = "1.9"
cfg-if = "1.0" cfg-if = "1.0"
const_format = "0.2" const_format = "0.2"
cwdemangle = "1.0" cwdemangle = "1.0"
cwextab = "1.0.2" cwextab = "1.0"
dirs = "5.0" dirs = "5.0"
egui = "0.29" egui = "0.29"
egui_extras = "0.29" egui_extras = "0.29"
@@ -38,11 +38,10 @@ float-ord = "0.3"
font-kit = "0.14" font-kit = "0.14"
globset = { version = "0.4", features = ["serde1"] } globset = { version = "0.4", features = ["serde1"] }
log = "0.4" log = "0.4"
notify = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2" }
objdiff-core = { path = "../objdiff-core", features = ["all"] } objdiff-core = { path = "../objdiff-core", features = ["all"] }
open = "5.3" open = "5.3"
png = "0.17" png = "0.17"
pollster = "0.3" pollster = "0.4"
regex = "1.11" regex = "1.11"
rfd = { version = "0.15" } #, default-features = false, features = ['xdg-portal'] rfd = { version = "0.15" } #, default-features = false, features = ['xdg-portal']
rlwinmdec = "1.0" rlwinmdec = "1.0"
@@ -51,7 +50,6 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
shell-escape = "0.1" shell-escape = "0.1"
strum = { version = "0.26", features = ["derive"] } strum = { version = "0.26", features = ["derive"] }
tempfile = "3.13"
time = { version = "0.3", features = ["formatting", "local-offset"] } time = { version = "0.3", features = ["formatting", "local-offset"] }
# Keep version in sync with egui # Keep version in sync with egui
@@ -76,18 +74,7 @@ features = [
optional = true optional = true
default-features = false default-features = false
# For Linux static binaries, use rustls
[target.'cfg(target_os = "linux")'.dependencies]
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"] }
self_update = { version = "0.41", default-features = false, features = ["rustls"] }
# For all other platforms, use native TLS
[target.'cfg(not(target_os = "linux"))'.dependencies]
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "default-tls"] }
self_update = "0.41"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
path-slash = "0.2"
winapi = "0.3" winapi = "0.3"
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
@@ -95,7 +82,7 @@ exec = "0.3"
# native: # native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tracing-subscriber = "0.3" tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# web: # web:
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]

View File

@@ -11,23 +11,22 @@ use std::{
}; };
use filetime::FileTime; use filetime::FileTime;
use globset::{Glob, GlobSet}; use globset::Glob;
use notify::{RecursiveMode, Watcher};
use objdiff_core::{ use objdiff_core::{
build::watcher::{create_watcher, Watcher},
config::{ config::{
build_globset, ProjectConfigInfo, ProjectObject, ScratchConfig, DEFAULT_WATCH_PATTERNS, build_globset, default_watch_patterns, save_project_config, ProjectConfig,
ProjectConfigInfo, ProjectObject, ScratchConfig, SymbolMappings, DEFAULT_WATCH_PATTERNS,
}, },
diff::DiffObjConfig, diff::DiffObjConfig,
jobs::{Job, JobQueue, JobResult},
}; };
use time::UtcOffset; use time::UtcOffset;
use crate::{ use crate::{
app_config::{deserialize_config, AppConfigVersion}, app_config::{deserialize_config, AppConfigVersion},
config::{load_project_config, ProjectObjectNode}, config::{load_project_config, ProjectObjectNode},
jobs::{ jobs::{create_objdiff_config, egui_waker, start_build},
objdiff::{start_build, ObjDiffConfig},
Job, JobQueue, JobResult, JobStatus,
},
views::{ views::{
appearance::{appearance_window, Appearance}, appearance::{appearance_window, Appearance},
config::{ config::{
@@ -42,11 +41,10 @@ use crate::{
graphics::{graphics_window, GraphicsConfig, GraphicsViewState}, graphics::{graphics_window, GraphicsConfig, GraphicsViewState},
jobs::{jobs_menu_ui, jobs_window}, jobs::{jobs_menu_ui, jobs_window},
rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState}, rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState},
symbol_diff::{symbol_diff_ui, DiffViewState, View}, symbol_diff::{symbol_diff_ui, DiffViewAction, DiffViewNavigation, DiffViewState, View},
}, },
}; };
#[derive(Default)]
pub struct ViewState { pub struct ViewState {
pub jobs: JobQueue, pub jobs: JobQueue,
pub config_state: ConfigViewState, pub config_state: ConfigViewState,
@@ -63,10 +61,34 @@ pub struct ViewState {
pub show_debug: bool, pub show_debug: bool,
pub show_graphics: bool, pub show_graphics: bool,
pub show_jobs: bool, pub show_jobs: bool,
pub show_side_panel: bool,
}
impl Default for ViewState {
fn default() -> Self {
Self {
jobs: Default::default(),
config_state: Default::default(),
demangle_state: Default::default(),
rlwinm_decode_state: Default::default(),
diff_state: Default::default(),
graphics_state: Default::default(),
frame_history: Default::default(),
show_appearance_config: false,
show_demangle: false,
show_rlwinm_decode: false,
show_project_config: false,
show_arch_config: false,
show_debug: false,
show_graphics: false,
show_jobs: false,
show_side_panel: true,
}
}
} }
/// The configuration for a single object file. /// The configuration for a single object file.
#[derive(Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ObjectConfig { pub struct ObjectConfig {
pub name: String, pub name: String,
pub target_path: Option<PathBuf>, pub target_path: Option<PathBuf>,
@@ -75,16 +97,28 @@ pub struct ObjectConfig {
pub complete: Option<bool>, pub complete: Option<bool>,
pub scratch: Option<ScratchConfig>, pub scratch: Option<ScratchConfig>,
pub source_path: Option<String>, pub source_path: Option<String>,
#[serde(default)]
pub symbol_mappings: SymbolMappings,
}
impl From<&ProjectObject> for ObjectConfig {
fn from(object: &ProjectObject) -> Self {
Self {
name: object.name().to_string(),
target_path: object.target_path.clone(),
base_path: object.base_path.clone(),
reverse_fn_order: object.reverse_fn_order(),
complete: object.complete(),
scratch: object.scratch.clone(),
source_path: object.source_path().cloned(),
symbol_mappings: object.symbol_mappings.clone().unwrap_or_default(),
}
}
} }
#[inline] #[inline]
fn bool_true() -> bool { true } fn bool_true() -> bool { true }
#[inline]
fn default_watch_patterns() -> Vec<Glob> {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
}
pub struct AppState { pub struct AppState {
pub config: AppConfig, pub config: AppConfig,
pub objects: Vec<ProjectObject>, pub objects: Vec<ProjectObject>,
@@ -94,8 +128,14 @@ pub struct AppState {
pub obj_change: bool, pub obj_change: bool,
pub queue_build: bool, pub queue_build: bool,
pub queue_reload: bool, pub queue_reload: bool,
pub current_project_config: Option<ProjectConfig>,
pub project_config_info: Option<ProjectConfigInfo>, pub project_config_info: Option<ProjectConfigInfo>,
pub last_mod_check: Instant, pub last_mod_check: Instant,
/// The right object symbol name that we're selecting a left symbol for
pub selecting_left: Option<String>,
/// The left object symbol name that we're selecting a right symbol for
pub selecting_right: Option<String>,
pub config_error: Option<String>,
} }
impl Default for AppState { impl Default for AppState {
@@ -109,8 +149,12 @@ impl Default for AppState {
obj_change: false, obj_change: false,
queue_build: false, queue_build: false,
queue_reload: false, queue_reload: false,
current_project_config: None,
project_config_info: None, project_config_info: None,
last_mod_check: Instant::now(), last_mod_check: Instant::now(),
selecting_left: None,
selecting_right: None,
config_error: None,
} }
} }
} }
@@ -191,7 +235,10 @@ impl AppState {
self.config_change = true; self.config_change = true;
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
self.current_project_config = None;
self.project_config_info = None; self.project_config_info = None;
self.selecting_left = None;
self.selecting_right = None;
} }
pub fn set_target_obj_dir(&mut self, path: PathBuf) { pub fn set_target_obj_dir(&mut self, path: PathBuf) {
@@ -199,6 +246,8 @@ impl AppState {
self.config.selected_obj = None; self.config.selected_obj = None;
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
self.selecting_left = None;
self.selecting_right = None;
} }
pub fn set_base_obj_dir(&mut self, path: PathBuf) { pub fn set_base_obj_dir(&mut self, path: PathBuf) {
@@ -206,12 +255,132 @@ impl AppState {
self.config.selected_obj = None; self.config.selected_obj = None;
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
self.selecting_left = None;
self.selecting_right = None;
} }
pub fn set_selected_obj(&mut self, object: ObjectConfig) { pub fn set_selected_obj(&mut self, config: ObjectConfig) {
self.config.selected_obj = Some(object); let mut unit_changed = true;
if let Some(existing) = self.config.selected_obj.as_ref() {
if existing == &config {
// Don't reload the object if there were no changes
return;
}
if existing.name == config.name {
unit_changed = false;
}
}
self.config.selected_obj = Some(config);
if unit_changed {
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
self.selecting_left = None;
self.selecting_right = None;
} else {
self.queue_build = true;
}
}
pub fn clear_selected_obj(&mut self) {
self.config.selected_obj = None;
self.obj_change = true;
self.queue_build = false;
self.selecting_left = None;
self.selecting_right = None;
}
pub fn set_selecting_left(&mut self, right: &str) {
let Some(object) = self.config.selected_obj.as_mut() else {
return;
};
object.symbol_mappings.remove_by_right(right);
self.selecting_left = Some(right.to_string());
self.queue_reload = true;
self.save_config();
}
pub fn set_selecting_right(&mut self, left: &str) {
let Some(object) = self.config.selected_obj.as_mut() else {
return;
};
object.symbol_mappings.remove_by_left(left);
self.selecting_right = Some(left.to_string());
self.queue_reload = true;
self.save_config();
}
pub fn set_symbol_mapping(&mut self, left: String, right: String) {
let Some(object) = self.config.selected_obj.as_mut() else {
log::warn!("No selected object");
return;
};
self.selecting_left = None;
self.selecting_right = None;
if left == right {
object.symbol_mappings.remove_by_left(&left);
object.symbol_mappings.remove_by_right(&right);
} else {
object.symbol_mappings.insert(left.clone(), right.clone());
}
self.queue_reload = true;
self.save_config();
}
pub fn clear_selection(&mut self) {
self.selecting_left = None;
self.selecting_right = None;
self.queue_reload = true;
}
pub fn clear_mappings(&mut self) {
self.selecting_left = None;
self.selecting_right = None;
if let Some(object) = self.config.selected_obj.as_mut() {
object.symbol_mappings.clear();
}
self.queue_reload = true;
self.save_config();
}
pub fn is_selecting_symbol(&self) -> bool {
self.selecting_left.is_some() || self.selecting_right.is_some()
}
pub fn save_config(&mut self) {
let (Some(config), Some(info)) =
(self.current_project_config.as_mut(), self.project_config_info.as_mut())
else {
return;
};
// Update the project config with the current state
if let Some(object) = self.config.selected_obj.as_ref() {
if let Some(existing) = config.units.as_mut().and_then(|v| {
v.iter_mut().find(|u| u.name.as_ref().is_some_and(|n| n == &object.name))
}) {
existing.symbol_mappings = if object.symbol_mappings.is_empty() {
None
} else {
Some(object.symbol_mappings.clone())
};
}
if let Some(existing) =
self.objects.iter_mut().find(|u| u.name.as_ref().is_some_and(|n| n == &object.name))
{
existing.symbol_mappings = if object.symbol_mappings.is_empty() {
None
} else {
Some(object.symbol_mappings.clone())
};
}
}
// Save the updated project config
match save_project_config(config, info) {
Ok(new_info) => *info = new_info,
Err(e) => {
log::error!("Failed to save project config: {e}");
self.config_error = Some(format!("Failed to save project config: {e}"));
}
}
} }
} }
@@ -223,7 +392,7 @@ pub struct App {
view_state: ViewState, view_state: ViewState,
state: AppStateRef, state: AppStateRef,
modified: Arc<AtomicBool>, modified: Arc<AtomicBool>,
watcher: Option<notify::RecommendedWatcher>, watcher: Option<Watcher>,
app_path: Option<PathBuf>, app_path: Option<PathBuf>,
relaunch_path: Rc<Mutex<Option<PathBuf>>>, relaunch_path: Rc<Mutex<Option<PathBuf>>>,
should_relaunch: bool, should_relaunch: bool,
@@ -298,83 +467,61 @@ impl App {
let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state; let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state;
let mut results = vec![]; jobs.collect_results();
for (job, result) in jobs.iter_finished() { jobs.results.retain(|result| match result {
match result {
Ok(result) => {
log::info!("Job {} finished", job.id);
match result {
JobResult::None => {
if let Some(err) = &job.context.status.read().unwrap().error {
log::error!("{:?}", err);
}
}
JobResult::Update(state) => { JobResult::Update(state) => {
if let Ok(mut guard) = self.relaunch_path.lock() { if let Ok(mut guard) = self.relaunch_path.lock() {
*guard = Some(state.exe_path); *guard = Some(state.exe_path.clone());
self.should_relaunch = true; self.should_relaunch = true;
} }
false
} }
_ => results.push(result), _ => true,
} });
}
Err(err) => {
let err = if let Some(msg) = err.downcast_ref::<&'static str>() {
anyhow::Error::msg(*msg)
} else if let Some(msg) = err.downcast_ref::<String>() {
anyhow::Error::msg(msg.clone())
} else {
anyhow::Error::msg("Thread panicked")
};
let result = job.context.status.write();
if let Ok(mut guard) = result {
guard.error = Some(err);
} else {
drop(result);
job.context.status = Arc::new(RwLock::new(JobStatus {
title: "Error".to_string(),
progress_percent: 0.0,
progress_items: None,
status: String::new(),
error: Some(err),
}));
}
}
}
}
jobs.results.append(&mut results);
jobs.clear_finished();
diff_state.pre_update(jobs, &self.state); diff_state.pre_update(jobs, &self.state);
config_state.pre_update(jobs, &self.state); config_state.pre_update(jobs, &self.state);
debug_assert!(jobs.results.is_empty()); debug_assert!(jobs.results.is_empty());
} }
fn post_update(&mut self, ctx: &egui::Context) { fn post_update(&mut self, ctx: &egui::Context, action: Option<DiffViewAction>) {
if action.is_some() {
ctx.request_repaint();
}
self.appearance.post_update(ctx); self.appearance.post_update(ctx);
let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state; let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state;
config_state.post_update(ctx, jobs, &self.state); config_state.post_update(ctx, jobs, &self.state);
diff_state.post_update(ctx, jobs, &self.state); diff_state.post_update(action, ctx, jobs, &self.state);
let Ok(mut state) = self.state.write() else { let Ok(mut state) = self.state.write() else {
return; return;
}; };
let state = &mut *state; let state = &mut *state;
let mut mod_check = false;
if state.last_mod_check.elapsed().as_millis() >= 500 {
state.last_mod_check = Instant::now();
mod_check = true;
}
if mod_check {
if let Some(info) = &state.project_config_info { if let Some(info) = &state.project_config_info {
if file_modified(&info.path, info.timestamp) { if let Some(last_ts) = info.timestamp {
if file_modified(&info.path, last_ts) {
state.config_change = true; state.config_change = true;
} }
} }
}
}
if state.config_change { if state.config_change {
state.config_change = false; state.config_change = false;
match load_project_config(state) { match load_project_config(state) {
Ok(()) => config_state.load_error = None, Ok(()) => state.config_error = None,
Err(e) => { Err(e) => {
log::error!("Failed to load project config: {e}"); log::error!("Failed to load project config: {e}");
config_state.load_error = Some(format!("{e}")); state.config_error = Some(format!("{e}"));
} }
} }
} }
@@ -386,7 +533,7 @@ impl App {
match build_globset(&state.config.watch_patterns) match build_globset(&state.config.watch_patterns)
.map_err(anyhow::Error::new) .map_err(anyhow::Error::new)
.and_then(|globset| { .and_then(|globset| {
create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset) create_watcher(self.modified.clone(), project_dir, globset, egui_waker(ctx))
.map_err(anyhow::Error::new) .map_err(anyhow::Error::new)
}) { }) {
Ok(watcher) => self.watcher = Some(watcher), Ok(watcher) => self.watcher = Some(watcher),
@@ -409,8 +556,7 @@ impl App {
} }
if let Some(result) = &diff_state.build { if let Some(result) = &diff_state.build {
if state.last_mod_check.elapsed().as_millis() >= 500 { if mod_check {
state.last_mod_check = Instant::now();
if let Some((obj, _)) = &result.first_obj { if let Some((obj, _)) = &result.first_obj {
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) { if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
if file_modified(path, timestamp) { if file_modified(path, timestamp) {
@@ -434,15 +580,15 @@ impl App {
&& state.config.selected_obj.is_some() && state.config.selected_obj.is_some()
&& !jobs.is_running(Job::ObjDiff) && !jobs.is_running(Job::ObjDiff)
{ {
jobs.push(start_build(ctx, ObjDiffConfig::from_config(&state.config))); start_build(ctx, jobs, create_objdiff_config(state));
state.queue_build = false; state.queue_build = false;
state.queue_reload = false; state.queue_reload = false;
} else if state.queue_reload && !jobs.is_running(Job::ObjDiff) { } else if state.queue_reload && !jobs.is_running(Job::ObjDiff) {
let mut diff_config = ObjDiffConfig::from_config(&state.config); let mut diff_config = create_objdiff_config(state);
// Don't build, just reload the current files // Don't build, just reload the current files
diff_config.build_base = false; diff_config.build_base = false;
diff_config.build_target = false; diff_config.build_target = false;
jobs.push(start_build(ctx, diff_config)); start_build(ctx, jobs, diff_config);
state.queue_reload = false; state.queue_reload = false;
} }
@@ -485,12 +631,26 @@ impl eframe::App for App {
show_debug, show_debug,
show_graphics, show_graphics,
show_jobs, show_jobs,
show_side_panel,
} = view_state; } = view_state;
frame_history.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage); frame_history.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
let side_panel_available = diff_state.current_view == View::SymbolDiff;
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| { egui::menu::bar(ui, |ui| {
if ui
.add_enabled(
side_panel_available,
egui::Button::new(if *show_side_panel { "" } else { "" }),
)
.on_hover_text("Toggle side panel")
.clicked()
{
*show_side_panel = !*show_side_panel;
}
ui.separator();
ui.menu_button("File", |ui| { ui.menu_button("File", |ui| {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
if ui.button("Debug…").clicked() { if ui.button("Debug…").clicked() {
@@ -599,6 +759,11 @@ impl eframe::App for App {
{ {
state.queue_reload = true; state.queue_reload = true;
} }
if ui.button("Clear custom symbol mappings").clicked() {
state.clear_mappings();
diff_state.post_build_nav = Some(DiffViewNavigation::symbol_diff());
state.queue_reload = true;
}
}); });
ui.separator(); ui.separator();
if jobs_menu_ui(ui, jobs, appearance) { if jobs_menu_ui(ui, jobs, appearance) {
@@ -607,31 +772,28 @@ impl eframe::App for App {
}); });
}); });
let build_success = matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success); if side_panel_available {
if diff_state.current_view == View::FunctionDiff && build_success { egui::SidePanel::left("side_panel").show_animated(ctx, *show_side_panel, |ui| {
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 if diff_state.current_view == View::ExtabDiff && build_success {
egui::CentralPanel::default().show(ctx, |ui| {
extab_diff_ui(ui, diff_state, appearance);
});
} else {
egui::SidePanel::left("side_panel").show(ctx, |ui| {
egui::ScrollArea::both().show(ui, |ui| { egui::ScrollArea::both().show(ui, |ui| {
config_ui(ui, state, show_project_config, config_state, appearance); config_ui(ui, state, show_project_config, config_state, appearance);
}); });
}); });
egui::CentralPanel::default().show(ctx, |ui| {
symbol_diff_ui(ui, diff_state, appearance);
});
} }
let mut action = None;
egui::CentralPanel::default().show(ctx, |ui| {
let build_success = matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success);
action = if diff_state.current_view == View::FunctionDiff && build_success {
function_diff_ui(ui, diff_state, appearance)
} else if diff_state.current_view == View::DataDiff && build_success {
data_diff_ui(ui, diff_state, appearance)
} else if diff_state.current_view == View::ExtabDiff && build_success {
extab_diff_ui(ui, diff_state, appearance)
} else {
symbol_diff_ui(ui, diff_state, appearance)
};
});
project_window(ctx, state, show_project_config, config_state, appearance); project_window(ctx, state, show_project_config, config_state, appearance);
appearance_window(ctx, show_appearance_config, appearance); appearance_window(ctx, show_appearance_config, appearance);
demangle_window(ctx, show_demangle, demangle_state, appearance); demangle_window(ctx, show_demangle, demangle_state, appearance);
@@ -641,10 +803,10 @@ impl eframe::App for App {
graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance); graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance);
jobs_window(ctx, show_jobs, jobs, appearance); jobs_window(ctx, show_jobs, jobs, appearance);
self.post_update(ctx); self.post_update(ctx, action);
} }
/// Called by the frame work to save state before shutdown. /// Called by the framework to save state before shutdown.
fn save(&mut self, storage: &mut dyn eframe::Storage) { fn save(&mut self, storage: &mut dyn eframe::Storage) {
if let Ok(state) = self.state.read() { if let Ok(state) = self.state.read() {
eframe::set_value(storage, CONFIG_KEY, &state.config); eframe::set_value(storage, CONFIG_KEY, &state.config);
@@ -653,40 +815,6 @@ impl eframe::App for App {
} }
} }
fn create_watcher(
ctx: egui::Context,
modified: Arc<AtomicBool>,
project_dir: &Path,
patterns: GlobSet,
) -> notify::Result<notify::RecommendedWatcher> {
let base_dir = project_dir.to_owned();
let mut watcher =
notify::recommended_watcher(move |res: notify::Result<notify::Event>| match res {
Ok(event) => {
if matches!(
event.kind,
notify::EventKind::Modify(..)
| notify::EventKind::Create(..)
| notify::EventKind::Remove(..)
) {
for path in &event.paths {
let Ok(path) = path.strip_prefix(&base_dir) else {
continue;
};
if patterns.is_match(path) {
log::info!("File modified: {}", path.display());
modified.store(true, Ordering::Relaxed);
ctx.request_repaint();
}
}
}
}
Err(e) => log::error!("watch error: {e:?}"),
})?;
watcher.watch(project_dir, RecursiveMode::Recursive)?;
Ok(watcher)
}
#[inline] #[inline]
fn file_modified(path: &Path, last_ts: FileTime) -> bool { fn file_modified(path: &Path, last_ts: FileTime) -> bool {
if let Ok(metadata) = fs::metadata(path) { if let Ok(metadata) = fs::metadata(path) {

View File

@@ -2,6 +2,10 @@ use std::path::PathBuf;
use eframe::Storage; use eframe::Storage;
use globset::Glob; use globset::Glob;
use objdiff_core::{
config::ScratchConfig,
diff::{ArmArchVersion, ArmR9Usage, DiffObjConfig, MipsAbi, MipsInstrCategory, X86Formatter},
};
use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY}; use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY};
@@ -11,7 +15,7 @@ pub struct AppConfigVersion {
} }
impl Default for AppConfigVersion { impl Default for AppConfigVersion {
fn default() -> Self { Self { version: 1 } } fn default() -> Self { Self { version: 2 } }
} }
/// Deserialize the AppConfig from storage, handling upgrades from older versions. /// Deserialize the AppConfig from storage, handling upgrades from older versions.
@@ -19,7 +23,8 @@ pub fn deserialize_config(storage: &dyn Storage) -> Option<AppConfig> {
let str = storage.get_string(CONFIG_KEY)?; let str = storage.get_string(CONFIG_KEY)?;
match ron::from_str::<AppConfigVersion>(&str) { match ron::from_str::<AppConfigVersion>(&str) {
Ok(version) => match version.version { Ok(version) => match version.version {
1 => from_str::<AppConfig>(&str), 2 => from_str::<AppConfig>(&str),
1 => from_str::<AppConfigV1>(&str).map(|c| c.into_config()),
_ => { _ => {
log::warn!("Unknown config version: {}", version.version); log::warn!("Unknown config version: {}", version.version);
None None
@@ -44,6 +49,181 @@ where T: serde::de::DeserializeOwned {
} }
} }
#[derive(serde::Deserialize, serde::Serialize)]
pub struct ScratchConfigV1 {
#[serde(default)]
pub platform: Option<String>,
#[serde(default)]
pub compiler: Option<String>,
#[serde(default)]
pub c_flags: Option<String>,
#[serde(default)]
pub ctx_path: Option<PathBuf>,
#[serde(default)]
pub build_ctx: bool,
}
impl ScratchConfigV1 {
fn into_config(self) -> ScratchConfig {
ScratchConfig {
platform: self.platform,
compiler: self.compiler,
c_flags: self.c_flags,
ctx_path: self.ctx_path,
build_ctx: self.build_ctx.then_some(true),
preset_id: None,
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct ObjectConfigV1 {
pub name: String,
pub target_path: Option<PathBuf>,
pub base_path: Option<PathBuf>,
pub reverse_fn_order: Option<bool>,
pub complete: Option<bool>,
pub scratch: Option<ScratchConfigV1>,
pub source_path: Option<String>,
}
impl ObjectConfigV1 {
fn into_config(self) -> ObjectConfig {
ObjectConfig {
name: self.name,
target_path: self.target_path,
base_path: self.base_path,
reverse_fn_order: self.reverse_fn_order,
complete: self.complete,
scratch: self.scratch.map(|scratch| scratch.into_config()),
source_path: self.source_path,
..Default::default()
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct DiffObjConfigV1 {
pub relax_reloc_diffs: bool,
#[serde(default = "bool_true")]
pub space_between_args: bool,
pub combine_data_sections: bool,
// x86
pub x86_formatter: X86Formatter,
// MIPS
pub mips_abi: MipsAbi,
pub mips_instr_category: MipsInstrCategory,
// ARM
pub arm_arch_version: ArmArchVersion,
pub arm_unified_syntax: bool,
pub arm_av_registers: bool,
pub arm_r9_usage: ArmR9Usage,
pub arm_sl_usage: bool,
pub arm_fp_usage: bool,
pub arm_ip_usage: bool,
}
impl Default for DiffObjConfigV1 {
fn default() -> Self {
Self {
relax_reloc_diffs: false,
space_between_args: true,
combine_data_sections: false,
x86_formatter: Default::default(),
mips_abi: Default::default(),
mips_instr_category: Default::default(),
arm_arch_version: Default::default(),
arm_unified_syntax: true,
arm_av_registers: false,
arm_r9_usage: Default::default(),
arm_sl_usage: false,
arm_fp_usage: false,
arm_ip_usage: false,
}
}
}
impl DiffObjConfigV1 {
fn into_config(self) -> DiffObjConfig {
DiffObjConfig {
relax_reloc_diffs: self.relax_reloc_diffs,
space_between_args: self.space_between_args,
combine_data_sections: self.combine_data_sections,
x86_formatter: self.x86_formatter,
mips_abi: self.mips_abi,
mips_instr_category: self.mips_instr_category,
arm_arch_version: self.arm_arch_version,
arm_unified_syntax: self.arm_unified_syntax,
arm_av_registers: self.arm_av_registers,
arm_r9_usage: self.arm_r9_usage,
arm_sl_usage: self.arm_sl_usage,
arm_fp_usage: self.arm_fp_usage,
arm_ip_usage: self.arm_ip_usage,
..Default::default()
}
}
}
#[inline]
fn bool_true() -> bool { true }
#[derive(serde::Deserialize, serde::Serialize)]
pub struct AppConfigV1 {
pub version: u32,
#[serde(default)]
pub custom_make: Option<String>,
#[serde(default)]
pub custom_args: Option<Vec<String>>,
#[serde(default)]
pub selected_wsl_distro: Option<String>,
#[serde(default)]
pub project_dir: Option<PathBuf>,
#[serde(default)]
pub target_obj_dir: Option<PathBuf>,
#[serde(default)]
pub base_obj_dir: Option<PathBuf>,
#[serde(default)]
pub selected_obj: Option<ObjectConfigV1>,
#[serde(default = "bool_true")]
pub build_base: bool,
#[serde(default)]
pub build_target: bool,
#[serde(default = "bool_true")]
pub rebuild_on_changes: bool,
#[serde(default)]
pub auto_update_check: bool,
#[serde(default)]
pub watch_patterns: Vec<Glob>,
#[serde(default)]
pub recent_projects: Vec<PathBuf>,
#[serde(default)]
pub diff_obj_config: DiffObjConfigV1,
}
impl AppConfigV1 {
fn into_config(self) -> AppConfig {
log::info!("Upgrading configuration from v1");
AppConfig {
custom_make: self.custom_make,
custom_args: self.custom_args,
selected_wsl_distro: self.selected_wsl_distro,
project_dir: self.project_dir,
target_obj_dir: self.target_obj_dir,
base_obj_dir: self.base_obj_dir,
selected_obj: self.selected_obj.map(|obj| obj.into_config()),
build_base: self.build_base,
build_target: self.build_target,
rebuild_on_changes: self.rebuild_on_changes,
auto_update_check: self.auto_update_check,
watch_patterns: self.watch_patterns,
recent_projects: self.recent_projects,
diff_obj_config: self.diff_obj_config.into_config(),
..Default::default()
}
}
}
#[derive(serde::Deserialize, serde::Serialize)] #[derive(serde::Deserialize, serde::Serialize)]
pub struct ObjectConfigV0 { pub struct ObjectConfigV0 {
pub name: String, pub name: String,
@@ -59,9 +239,7 @@ impl ObjectConfigV0 {
target_path: Some(self.target_path), target_path: Some(self.target_path),
base_path: Some(self.base_path), base_path: Some(self.base_path),
reverse_fn_order: self.reverse_fn_order, reverse_fn_order: self.reverse_fn_order,
complete: None, ..Default::default()
scratch: None,
source_path: None,
} }
} }
} }

View File

@@ -4,14 +4,29 @@ use anyhow::Result;
use globset::Glob; use globset::Glob;
use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS}; use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS};
use crate::app::AppState; use crate::app::{AppState, ObjectConfig};
#[derive(Clone)] #[derive(Clone)]
pub enum ProjectObjectNode { pub enum ProjectObjectNode {
File(String, Box<ProjectObject>), Unit(String, usize),
Dir(String, Vec<ProjectObjectNode>), Dir(String, Vec<ProjectObjectNode>),
} }
fn join_single_dir_entries(nodes: &mut Vec<ProjectObjectNode>) {
for node in nodes {
if let ProjectObjectNode::Dir(my_name, my_nodes) = node {
join_single_dir_entries(my_nodes);
// If this directory consists of a single sub-directory...
if let [ProjectObjectNode::Dir(sub_name, sub_nodes)] = &mut my_nodes[..] {
// ... join the two names with a path separator and eliminate the layer
*my_name += "/";
*my_name += sub_name;
*my_nodes = std::mem::take(sub_nodes);
}
}
}
}
fn find_dir<'a>( fn find_dir<'a>(
name: &str, name: &str,
nodes: &'a mut Vec<ProjectObjectNode>, nodes: &'a mut Vec<ProjectObjectNode>,
@@ -33,17 +48,18 @@ fn find_dir<'a>(
} }
fn build_nodes( fn build_nodes(
objects: &[ProjectObject], units: &mut [ProjectObject],
project_dir: &Path, project_dir: &Path,
target_obj_dir: Option<&Path>, target_obj_dir: Option<&Path>,
base_obj_dir: Option<&Path>, base_obj_dir: Option<&Path>,
) -> Vec<ProjectObjectNode> { ) -> Vec<ProjectObjectNode> {
let mut nodes = vec![]; let mut nodes = vec![];
for object in objects { for (idx, unit) in units.iter_mut().enumerate() {
unit.resolve_paths(project_dir, target_obj_dir, base_obj_dir);
let mut out_nodes = &mut nodes; let mut out_nodes = &mut nodes;
let path = if let Some(name) = &object.name { let path = if let Some(name) = &unit.name {
Path::new(name) Path::new(name)
} else if let Some(path) = &object.path { } else if let Some(path) = &unit.path {
path path
} else { } else {
continue; continue;
@@ -56,11 +72,17 @@ fn build_nodes(
} }
} }
} }
let mut object = Box::new(object.clone());
object.resolve_paths(project_dir, target_obj_dir, base_obj_dir);
let filename = path.file_name().unwrap().to_str().unwrap().to_string(); let filename = path.file_name().unwrap().to_str().unwrap().to_string();
out_nodes.push(ProjectObjectNode::File(filename, object)); out_nodes.push(ProjectObjectNode::Unit(filename, idx));
} }
// Within the top-level module directories, join paths. Leave the
// top-level name intact though since it's the module name.
for node in &mut nodes {
if let ProjectObjectNode::Dir(_, sub_nodes) = node {
join_single_dir_entries(sub_nodes);
}
}
nodes nodes
} }
@@ -70,24 +92,36 @@ pub fn load_project_config(state: &mut AppState) -> Result<()> {
}; };
if let Some((result, info)) = try_project_config(project_dir) { if let Some((result, info)) = try_project_config(project_dir) {
let project_config = result?; let project_config = result?;
state.config.custom_make = project_config.custom_make; state.config.custom_make = project_config.custom_make.clone();
state.config.custom_args = project_config.custom_args; state.config.custom_args = project_config.custom_args.clone();
state.config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p)); state.config.target_obj_dir =
state.config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p)); project_config.target_dir.as_deref().map(|p| project_dir.join(p));
state.config.build_base = project_config.build_base; state.config.base_obj_dir = project_config.base_dir.as_deref().map(|p| project_dir.join(p));
state.config.build_target = project_config.build_target; state.config.build_base = project_config.build_base.unwrap_or(true);
state.config.watch_patterns = project_config.watch_patterns.unwrap_or_else(|| { state.config.build_target = project_config.build_target.unwrap_or(false);
state.config.watch_patterns = project_config.watch_patterns.clone().unwrap_or_else(|| {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect() DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
}); });
state.watcher_change = true; state.watcher_change = true;
state.objects = project_config.objects; state.objects = project_config.units.clone().unwrap_or_default();
state.object_nodes = build_nodes( state.object_nodes = build_nodes(
&state.objects, &mut state.objects,
project_dir, project_dir,
state.config.target_obj_dir.as_deref(), state.config.target_obj_dir.as_deref(),
state.config.base_obj_dir.as_deref(), state.config.base_obj_dir.as_deref(),
); );
state.current_project_config = Some(project_config);
state.project_config_info = Some(info); state.project_config_info = Some(info);
// Reload selected object
if let Some(selected_obj) = &state.config.selected_obj {
if let Some(obj) = state.objects.iter().find(|o| o.name() == selected_obj.name) {
let config = ObjectConfig::from(obj);
state.set_selected_obj(config);
} else {
state.clear_selected_obj();
}
}
} }
Ok(()) Ok(())
} }

108
objdiff-gui/src/hotkeys.rs Normal file
View File

@@ -0,0 +1,108 @@
use egui::{
style::ScrollAnimation, vec2, Context, Key, KeyboardShortcut, Modifiers, PointerButton,
};
fn any_widget_focused(ctx: &Context) -> bool { ctx.memory(|mem| mem.focused().is_some()) }
pub fn enter_pressed(ctx: &Context) -> bool {
if any_widget_focused(ctx) {
return false;
}
ctx.input_mut(|i| {
i.key_pressed(Key::Enter)
|| i.key_pressed(Key::Space)
|| i.pointer.button_pressed(PointerButton::Extra2)
})
}
pub fn back_pressed(ctx: &Context) -> bool {
if any_widget_focused(ctx) {
return false;
}
ctx.input_mut(|i| {
i.key_pressed(Key::Backspace)
|| i.key_pressed(Key::Escape)
|| i.pointer.button_pressed(PointerButton::Extra1)
})
}
pub fn up_pressed(ctx: &Context) -> bool {
if any_widget_focused(ctx) {
return false;
}
ctx.input_mut(|i| i.key_pressed(Key::ArrowUp) || i.key_pressed(Key::W))
}
pub fn down_pressed(ctx: &Context) -> bool {
if any_widget_focused(ctx) {
return false;
}
ctx.input_mut(|i| i.key_pressed(Key::ArrowDown) || i.key_pressed(Key::S))
}
pub fn page_up_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::PageUp)) }
pub fn page_down_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::PageDown)) }
pub fn home_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::Home)) }
pub fn end_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::End)) }
pub fn check_scroll_hotkeys(ui: &mut egui::Ui, include_small_increments: bool) {
let ui_height = ui.available_rect_before_wrap().height();
if up_pressed(ui.ctx()) && include_small_increments {
ui.scroll_with_delta_animation(vec2(0.0, ui_height / 10.0), ScrollAnimation::none());
} else if down_pressed(ui.ctx()) && include_small_increments {
ui.scroll_with_delta_animation(vec2(0.0, -ui_height / 10.0), ScrollAnimation::none());
} else if page_up_pressed(ui.ctx()) {
ui.scroll_with_delta_animation(vec2(0.0, ui_height), ScrollAnimation::none());
} else if page_down_pressed(ui.ctx()) {
ui.scroll_with_delta_animation(vec2(0.0, -ui_height), ScrollAnimation::none());
} else if home_pressed(ui.ctx()) {
ui.scroll_with_delta_animation(vec2(0.0, f32::INFINITY), ScrollAnimation::none());
} else if end_pressed(ui.ctx()) {
ui.scroll_with_delta_animation(vec2(0.0, -f32::INFINITY), ScrollAnimation::none());
}
}
pub fn consume_up_key(ctx: &Context) -> bool {
if any_widget_focused(ctx) {
return false;
}
ctx.input_mut(|i| {
i.consume_key(Modifiers::NONE, Key::ArrowUp) || i.consume_key(Modifiers::NONE, Key::W)
})
}
pub fn consume_down_key(ctx: &Context) -> bool {
if any_widget_focused(ctx) {
return false;
}
ctx.input_mut(|i| {
i.consume_key(Modifiers::NONE, Key::ArrowDown) || i.consume_key(Modifiers::NONE, Key::S)
})
}
const OBJECT_FILTER_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::F);
pub fn consume_object_filter_shortcut(ctx: &Context) -> bool {
ctx.input_mut(|i| i.consume_shortcut(&OBJECT_FILTER_SHORTCUT))
}
const SYMBOL_FILTER_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::S);
pub fn consume_symbol_filter_shortcut(ctx: &Context) -> bool {
ctx.input_mut(|i| i.consume_shortcut(&SYMBOL_FILTER_SHORTCUT))
}
const CHANGE_TARGET_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::T);
pub fn consume_change_target_shortcut(ctx: &Context) -> bool {
ctx.input_mut(|i| i.consume_shortcut(&CHANGE_TARGET_SHORTCUT))
}
const CHANGE_BASE_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::B);
pub fn consume_change_base_shortcut(ctx: &Context) -> bool {
ctx.input_mut(|i| i.consume_shortcut(&CHANGE_BASE_SHORTCUT))
}

138
objdiff-gui/src/jobs.rs Normal file
View File

@@ -0,0 +1,138 @@
use std::{
sync::Arc,
task::{Wake, Waker},
};
use anyhow::{bail, Result};
use jobs::create_scratch;
use objdiff_core::{
build::BuildConfig,
jobs,
jobs::{check_update::CheckUpdateConfig, objdiff, update::UpdateConfig, Job, JobQueue},
};
use crate::{
app::{AppConfig, AppState},
update::{build_updater, BIN_NAME_NEW, BIN_NAME_OLD},
};
struct EguiWaker(egui::Context);
impl Wake for EguiWaker {
fn wake(self: Arc<Self>) { self.0.request_repaint(); }
fn wake_by_ref(self: &Arc<Self>) { self.0.request_repaint(); }
}
pub fn egui_waker(ctx: &egui::Context) -> Waker { Waker::from(Arc::new(EguiWaker(ctx.clone()))) }
pub fn is_create_scratch_available(config: &AppConfig) -> bool {
let Some(selected_obj) = &config.selected_obj else {
return false;
};
selected_obj.target_path.is_some() && selected_obj.scratch.is_some()
}
pub fn start_create_scratch(
ctx: &egui::Context,
jobs: &mut JobQueue,
state: &AppState,
function_name: String,
) {
match create_scratch_config(state, function_name) {
Ok(config) => {
jobs.push_once(Job::CreateScratch, || {
create_scratch::start_create_scratch(egui_waker(ctx), config)
});
}
Err(err) => {
log::error!("Failed to create scratch config: {err}");
}
}
}
fn create_scratch_config(
state: &AppState,
function_name: String,
) -> Result<create_scratch::CreateScratchConfig> {
let Some(selected_obj) = &state.config.selected_obj else {
bail!("No object selected");
};
let Some(target_path) = &selected_obj.target_path else {
bail!("No target path for {}", selected_obj.name);
};
let Some(scratch_config) = &selected_obj.scratch else {
bail!("No scratch configuration for {}", selected_obj.name);
};
Ok(create_scratch::CreateScratchConfig {
build_config: BuildConfig::from(&state.config),
context_path: scratch_config.ctx_path.clone(),
build_context: scratch_config.build_ctx.unwrap_or(false),
compiler: scratch_config.compiler.clone().unwrap_or_default(),
platform: scratch_config.platform.clone().unwrap_or_default(),
compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(),
function_name,
target_obj: target_path.to_path_buf(),
preset_id: scratch_config.preset_id,
})
}
impl From<&AppConfig> for BuildConfig {
fn from(config: &AppConfig) -> Self {
Self {
project_dir: config.project_dir.clone(),
custom_make: config.custom_make.clone(),
custom_args: config.custom_args.clone(),
selected_wsl_distro: config.selected_wsl_distro.clone(),
}
}
}
pub fn create_objdiff_config(state: &AppState) -> objdiff::ObjDiffConfig {
objdiff::ObjDiffConfig {
build_config: BuildConfig::from(&state.config),
build_base: state.config.build_base,
build_target: state.config.build_target,
target_path: state
.config
.selected_obj
.as_ref()
.and_then(|obj| obj.target_path.as_ref())
.cloned(),
base_path: state
.config
.selected_obj
.as_ref()
.and_then(|obj| obj.base_path.as_ref())
.cloned(),
diff_obj_config: state.config.diff_obj_config.clone(),
symbol_mappings: state
.config
.selected_obj
.as_ref()
.map(|obj| &obj.symbol_mappings)
.cloned()
.unwrap_or_default(),
selecting_left: state.selecting_left.clone(),
selecting_right: state.selecting_right.clone(),
}
}
pub fn start_build(ctx: &egui::Context, jobs: &mut JobQueue, config: objdiff::ObjDiffConfig) {
jobs.push_once(Job::ObjDiff, || objdiff::start_build(egui_waker(ctx), config));
}
pub fn start_check_update(ctx: &egui::Context, jobs: &mut JobQueue) {
jobs.push_once(Job::Update, || {
jobs::check_update::start_check_update(egui_waker(ctx), CheckUpdateConfig {
build_updater,
bin_names: vec![BIN_NAME_NEW.to_string(), BIN_NAME_OLD.to_string()],
})
});
}
pub fn start_update(ctx: &egui::Context, jobs: &mut JobQueue, bin_name: String) {
jobs.push_once(Job::Update, || {
jobs::update::start_update(egui_waker(ctx), UpdateConfig { build_updater, bin_name })
});
}

View File

@@ -1,39 +0,0 @@
use std::sync::mpsc::Receiver;
use anyhow::{Context, Result};
use self_update::{cargo_crate_version, update::Release};
use crate::{
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
update::{build_updater, BIN_NAME_NEW, BIN_NAME_OLD},
};
pub struct CheckUpdateResult {
pub update_available: bool,
pub latest_release: Release,
pub found_binary: Option<String>,
}
fn run_check_update(context: &JobContext, cancel: Receiver<()>) -> Result<Box<CheckUpdateResult>> {
update_status(context, "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()?;
let update_available =
self_update::version::bump_is_greater(cargo_crate_version!(), &latest_release.version)?;
// Find the binary name in the release assets
let found_binary = latest_release
.assets
.iter()
.find(|a| a.name == BIN_NAME_NEW)
.or_else(|| latest_release.assets.iter().find(|a| a.name == BIN_NAME_OLD))
.map(|a| a.name.clone());
update_status(context, "Complete".to_string(), 1, 1, &cancel)?;
Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary }))
}
pub fn start_check_update(ctx: &egui::Context) -> JobState {
start_job(ctx, "Check for updates", Job::CheckUpdate, move |context, cancel| {
run_check_update(&context, cancel).map(|result| JobResult::CheckUpdate(Some(result)))
})
}

View File

@@ -1,317 +0,0 @@
use std::{
path::{Path, PathBuf},
process::Command,
sync::mpsc::Receiver,
};
use anyhow::{anyhow, Error, Result};
use objdiff_core::{
diff::{diff_objs, DiffObjConfig, ObjDiff},
obj::{read, ObjInfo},
};
use time::OffsetDateTime;
use crate::{
app::{AppConfig, ObjectConfig},
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
};
pub struct BuildStatus {
pub success: bool,
pub cmdline: String,
pub stdout: String,
pub stderr: String,
}
impl Default for BuildStatus {
fn default() -> Self {
BuildStatus {
success: true,
cmdline: String::new(),
stdout: String::new(),
stderr: String::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct BuildConfig {
pub project_dir: Option<PathBuf>,
pub custom_make: Option<String>,
pub custom_args: Option<Vec<String>>,
#[allow(unused)]
pub selected_wsl_distro: Option<String>,
}
impl BuildConfig {
pub(crate) fn from_config(config: &AppConfig) -> Self {
Self {
project_dir: config.project_dir.clone(),
custom_make: config.custom_make.clone(),
custom_args: config.custom_args.clone(),
selected_wsl_distro: config.selected_wsl_distro.clone(),
}
}
}
pub struct ObjDiffConfig {
pub build_config: BuildConfig,
pub build_base: bool,
pub build_target: bool,
pub selected_obj: Option<ObjectConfig>,
pub diff_obj_config: DiffObjConfig,
}
impl ObjDiffConfig {
pub(crate) fn from_config(config: &AppConfig) -> Self {
Self {
build_config: BuildConfig::from_config(config),
build_base: config.build_base,
build_target: config.build_target,
selected_obj: config.selected_obj.clone(),
diff_obj_config: config.diff_obj_config.clone(),
}
}
}
pub struct ObjDiffResult {
pub first_status: BuildStatus,
pub second_status: BuildStatus,
pub first_obj: Option<(ObjInfo, ObjDiff)>,
pub second_obj: Option<(ObjInfo, ObjDiff)>,
pub time: OffsetDateTime,
}
pub(crate) fn run_make(config: &BuildConfig, arg: &Path) -> BuildStatus {
let Some(cwd) = &config.project_dir else {
return BuildStatus {
success: false,
stderr: "Missing project dir".to_string(),
..Default::default()
};
};
let make = config.custom_make.as_deref().unwrap_or("make");
let make_args = config.custom_args.as_deref().unwrap_or(&[]);
#[cfg(not(windows))]
let mut command = {
let mut command = Command::new(make);
command.current_dir(cwd).args(make_args).arg(arg);
command
};
#[cfg(windows)]
let mut command = {
use std::os::windows::process::CommandExt;
use path_slash::PathExt;
let mut command = if config.selected_wsl_distro.is_some() {
Command::new("wsl")
} else {
Command::new(make)
};
if let Some(distro) = &config.selected_wsl_distro {
// Strip distro root prefix \\wsl.localhost\{distro}
let wsl_path_prefix = format!("\\\\wsl.localhost\\{}", distro);
let cwd = match cwd.strip_prefix(wsl_path_prefix) {
Ok(new_cwd) => format!("/{}", new_cwd.to_slash_lossy().as_ref()),
Err(_) => cwd.to_string_lossy().to_string(),
};
command
.arg("--cd")
.arg(cwd)
.arg("-d")
.arg(distro)
.arg("--")
.arg(make)
.args(make_args)
.arg(arg.to_slash_lossy().as_ref());
} else {
command.current_dir(cwd).args(make_args).arg(arg.to_slash_lossy().as_ref());
}
command.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
command
};
let mut cmdline = shell_escape::escape(command.get_program().to_string_lossy()).into_owned();
for arg in command.get_args() {
cmdline.push(' ');
cmdline.push_str(shell_escape::escape(arg.to_string_lossy()).as_ref());
}
let output = match command.output() {
Ok(output) => output,
Err(e) => {
return BuildStatus {
success: false,
cmdline,
stdout: Default::default(),
stderr: e.to_string(),
};
}
};
// Try from_utf8 first to avoid copying the buffer if it's valid, then fall back to from_utf8_lossy
let stdout = String::from_utf8(output.stdout)
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
let stderr = String::from_utf8(output.stderr)
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
BuildStatus { success: output.status.success(), cmdline, stdout, stderr }
}
fn run_build(
context: &JobContext,
cancel: Receiver<()>,
config: ObjDiffConfig,
) -> Result<Box<ObjDiffResult>> {
let obj_config = config.selected_obj.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?;
let project_dir = config
.build_config
.project_dir
.as_ref()
.ok_or_else(|| Error::msg("Missing project dir"))?;
let target_path_rel = if let Some(target_path) = &obj_config.target_path {
Some(target_path.strip_prefix(project_dir).map_err(|_| {
anyhow!(
"Target path '{}' doesn't begin with '{}'",
target_path.display(),
project_dir.display()
)
})?)
} else {
None
};
let base_path_rel = if let Some(base_path) = &obj_config.base_path {
Some(base_path.strip_prefix(project_dir).map_err(|_| {
anyhow!(
"Base path '{}' doesn't begin with '{}'",
base_path.display(),
project_dir.display()
)
})?)
} else {
None
};
let mut total = 1;
if config.build_target && target_path_rel.is_some() {
total += 1;
}
if config.build_base && base_path_rel.is_some() {
total += 1;
}
if target_path_rel.is_some() {
total += 1;
}
if base_path_rel.is_some() {
total += 1;
}
let mut step_idx = 0;
let mut first_status = match target_path_rel {
Some(target_path_rel) if config.build_target => {
update_status(
context,
format!("Building target {}", target_path_rel.display()),
step_idx,
total,
&cancel,
)?;
step_idx += 1;
run_make(&config.build_config, target_path_rel)
}
_ => BuildStatus::default(),
};
let mut second_status = match base_path_rel {
Some(base_path_rel) if config.build_base => {
update_status(
context,
format!("Building base {}", base_path_rel.display()),
step_idx,
total,
&cancel,
)?;
step_idx += 1;
run_make(&config.build_config, base_path_rel)
}
_ => BuildStatus::default(),
};
let time = OffsetDateTime::now_utc();
let first_obj = match &obj_config.target_path {
Some(target_path) if first_status.success => {
update_status(
context,
format!("Loading target {}", target_path_rel.unwrap().display()),
step_idx,
total,
&cancel,
)?;
step_idx += 1;
match read::read(target_path, &config.diff_obj_config) {
Ok(obj) => Some(obj),
Err(e) => {
first_status = BuildStatus {
success: false,
stdout: format!("Loading object '{}'", target_path.display()),
stderr: format!("{:#}", e),
..Default::default()
};
None
}
}
}
Some(_) => {
step_idx += 1;
None
}
_ => None,
};
let second_obj = match &obj_config.base_path {
Some(base_path) if second_status.success => {
update_status(
context,
format!("Loading base {}", base_path_rel.unwrap().display()),
step_idx,
total,
&cancel,
)?;
step_idx += 1;
match read::read(base_path, &config.diff_obj_config) {
Ok(obj) => Some(obj),
Err(e) => {
second_status = BuildStatus {
success: false,
stdout: format!("Loading object '{}'", base_path.display()),
stderr: format!("{:#}", e),
..Default::default()
};
None
}
}
}
Some(_) => {
step_idx += 1;
None
}
_ => None,
};
update_status(context, "Performing diff".to_string(), step_idx, total, &cancel)?;
step_idx += 1;
let result = diff_objs(&config.diff_obj_config, first_obj.as_ref(), second_obj.as_ref(), None)?;
update_status(context, "Complete".to_string(), step_idx, total, &cancel)?;
Ok(Box::new(ObjDiffResult {
first_status,
second_status,
first_obj: first_obj.and_then(|o| result.left.map(|d| (o, d))),
second_obj: second_obj.and_then(|o| result.right.map(|d| (o, d))),
time,
}))
}
pub fn start_build(ctx: &egui::Context, config: ObjDiffConfig) -> JobState {
start_job(ctx, "Build", Job::ObjDiff, move |context, cancel| {
run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
})
}

View File

@@ -4,6 +4,7 @@ mod app;
mod app_config; mod app_config;
mod config; mod config;
mod fonts; mod fonts;
mod hotkeys;
mod jobs; mod jobs;
mod update; mod update;
mod views; mod views;

View File

@@ -1,5 +1,7 @@
use anyhow::Result;
use cfg_if::cfg_if; use cfg_if::cfg_if;
use const_format::formatcp; use const_format::formatcp;
use objdiff_core::jobs::update::self_update;
use self_update::{cargo_crate_version, update::ReleaseUpdate}; use self_update::{cargo_crate_version, update::ReleaseUpdate};
pub const OS: &str = std::env::consts::OS; pub const OS: &str = std::env::consts::OS;
@@ -26,8 +28,8 @@ pub const BIN_NAME_OLD: &str = formatcp!("objdiff-{}-{}{}", OS, ARCH, std::env::
pub const RELEASE_URL: &str = pub const RELEASE_URL: &str =
formatcp!("https://github.com/{}/{}/releases/latest", GITHUB_USER, GITHUB_REPO); formatcp!("https://github.com/{}/{}/releases/latest", GITHUB_USER, GITHUB_REPO);
pub fn build_updater() -> self_update::errors::Result<Box<dyn ReleaseUpdate>> { pub fn build_updater() -> Result<Box<dyn ReleaseUpdate>> {
self_update::backends::github::Update::configure() Ok(self_update::backends::github::Update::configure()
.repo_owner(GITHUB_USER) .repo_owner(GITHUB_USER)
.repo_name(GITHUB_REPO) .repo_name(GITHUB_REPO)
// bin_name is required, but unused? // bin_name is required, but unused?
@@ -35,5 +37,5 @@ pub fn build_updater() -> self_update::errors::Result<Box<dyn ReleaseUpdate>> {
.no_confirm(true) .no_confirm(true)
.show_output(false) .show_output(false)
.current_version(cargo_crate_version!()) .current_version(cargo_crate_version!())
.build() .build()?)
} }

View File

@@ -205,7 +205,7 @@ pub const DEFAULT_COLOR_ROTATION: [Color32; 9] = [
Color32::from_rgb(255, 0, 0), Color32::from_rgb(255, 0, 0),
Color32::from_rgb(255, 255, 0), Color32::from_rgb(255, 255, 0),
Color32::from_rgb(255, 192, 203), Color32::from_rgb(255, 192, 203),
Color32::from_rgb(0, 0, 255), Color32::from_rgb(128, 128, 255),
Color32::from_rgb(0, 255, 0), Color32::from_rgb(0, 255, 0),
Color32::from_rgb(213, 138, 138), Color32::from_rgb(213, 138, 138),
]; ];

View File

@@ -0,0 +1,82 @@
use egui::{Align, Layout, Sense, Vec2};
use egui_extras::{Column, Size, StripBuilder, TableBuilder, TableRow};
pub fn render_header(
ui: &mut egui::Ui,
available_width: f32,
num_columns: usize,
mut add_contents: impl FnMut(&mut egui::Ui, usize),
) {
let column_width = available_width / num_columns as f32;
ui.allocate_ui_with_layout(
Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min),
|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
for i in 0..num_columns {
ui.allocate_ui_with_layout(
Vec2 { x: column_width, y: 100.0 },
Layout::top_down(Align::Min),
|ui| {
ui.set_width(column_width);
add_contents(ui, i);
},
);
}
},
);
ui.separator();
}
pub fn render_table(
ui: &mut egui::Ui,
available_width: f32,
num_columns: usize,
row_height: f32,
total_rows: usize,
mut add_contents: impl FnMut(&mut TableRow, usize),
) {
ui.style_mut().interaction.selectable_labels = false;
let column_width = available_width / num_columns as f32;
let available_height = ui.available_height();
let table = TableBuilder::new(ui)
.striped(false)
.cell_layout(Layout::left_to_right(Align::Min))
.columns(Column::exact(column_width).clip(true), num_columns)
.resizable(false)
.auto_shrink([false, false])
.min_scrolled_height(available_height)
.sense(Sense::click());
table.body(|body| {
body.rows(row_height, total_rows, |mut row| {
row.set_hovered(false); // Disable hover effect
for i in 0..num_columns {
add_contents(&mut row, i);
}
});
});
}
pub fn render_strips(
ui: &mut egui::Ui,
available_width: f32,
num_columns: usize,
mut add_contents: impl FnMut(&mut egui::Ui, usize),
) {
let column_width = available_width / num_columns as f32;
StripBuilder::new(ui).size(Size::remainder()).clip(true).vertical(|mut strip| {
strip.strip(|builder| {
builder.sizes(Size::exact(column_width), num_columns).clip(true).horizontal(
|mut strip| {
for i in 0..num_columns {
strip.cell(|ui| {
ui.push_id(i, |ui| {
add_contents(ui, i);
});
});
}
},
);
});
});
}

View File

@@ -15,17 +15,15 @@ use globset::Glob;
use objdiff_core::{ use objdiff_core::{
config::{ProjectObject, DEFAULT_WATCH_PATTERNS}, config::{ProjectObject, DEFAULT_WATCH_PATTERNS},
diff::{ArmArchVersion, ArmR9Usage, MipsAbi, MipsInstrCategory, X86Formatter}, diff::{ArmArchVersion, ArmR9Usage, MipsAbi, MipsInstrCategory, X86Formatter},
jobs::{check_update::CheckUpdateResult, Job, JobQueue, JobResult},
}; };
use strum::{EnumMessage, VariantArray}; use strum::{EnumMessage, VariantArray};
use crate::{ use crate::{
app::{AppConfig, AppState, AppStateRef, ObjectConfig}, app::{AppConfig, AppState, AppStateRef, ObjectConfig},
config::ProjectObjectNode, config::ProjectObjectNode,
jobs::{ hotkeys,
check_update::{start_check_update, CheckUpdateResult}, jobs::{start_check_update, start_update},
update::start_update,
Job, JobQueue, JobResult,
},
update::RELEASE_URL, update::RELEASE_URL,
views::{ views::{
appearance::Appearance, appearance::Appearance,
@@ -43,7 +41,6 @@ pub struct ConfigViewState {
pub build_running: bool, pub build_running: bool,
pub queue_build: bool, pub queue_build: bool,
pub watch_pattern_text: String, pub watch_pattern_text: String,
pub load_error: Option<String>,
pub object_search: String, pub object_search: String,
pub filter_diffable: bool, pub filter_diffable: bool,
pub filter_incomplete: bool, pub filter_incomplete: bool,
@@ -93,10 +90,7 @@ impl ConfigViewState {
name: obj_path.display().to_string(), name: obj_path.display().to_string(),
target_path: Some(target_path), target_path: Some(target_path),
base_path: Some(path), base_path: Some(path),
reverse_fn_order: None, ..Default::default()
complete: None,
scratch: None,
source_path: None,
}); });
} else if let Ok(obj_path) = path.strip_prefix(target_dir) { } else if let Ok(obj_path) = path.strip_prefix(target_dir) {
let base_path = base_dir.join(obj_path); let base_path = base_dir.join(obj_path);
@@ -104,10 +98,7 @@ impl ConfigViewState {
name: obj_path.display().to_string(), name: obj_path.display().to_string(),
target_path: Some(path), target_path: Some(path),
base_path: Some(base_path), base_path: Some(base_path),
reverse_fn_order: None, ..Default::default()
complete: None,
scratch: None,
source_path: None,
}); });
} }
} }
@@ -125,11 +116,11 @@ impl ConfigViewState {
if self.queue_check_update { if self.queue_check_update {
self.queue_check_update = false; self.queue_check_update = false;
jobs.push_once(Job::CheckUpdate, || start_check_update(ctx)); start_check_update(ctx, jobs);
} }
if let Some(bin_name) = self.queue_update.take() { if let Some(bin_name) = self.queue_update.take() {
jobs.push_once(Job::Update, || start_update(ctx, bin_name)); start_update(ctx, jobs, bin_name);
} }
} }
} }
@@ -230,7 +221,10 @@ pub fn config_ui(
} }
}); });
let mut new_selected_obj = selected_obj.clone(); let selected_index = selected_obj.as_ref().and_then(|selected_obj| {
objects.iter().position(|obj| obj.name.as_ref() == Some(&selected_obj.name))
});
let mut new_selected_index = selected_index;
if objects.is_empty() { if objects.is_empty() {
if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) { if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) {
if ui.button("Select object").clicked() { if ui.button("Select object").clicked() {
@@ -258,7 +252,11 @@ pub fn config_ui(
} }
} else { } else {
let had_search = !config_state.object_search.is_empty(); let had_search = !config_state.object_search.is_empty();
let response =
egui::TextEdit::singleline(&mut config_state.object_search).hint_text("Filter").ui(ui); egui::TextEdit::singleline(&mut config_state.object_search).hint_text("Filter").ui(ui);
if hotkeys::consume_object_filter_shortcut(ui.ctx()) {
response.request_focus();
}
let mut root_open = None; let mut root_open = None;
let mut node_open = NodeOpen::Default; let mut node_open = NodeOpen::Default;
@@ -316,6 +314,7 @@ pub fn config_ui(
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
for node in object_nodes.iter().filter_map(|node| { for node in object_nodes.iter().filter_map(|node| {
filter_node( filter_node(
objects,
node, node,
&search, &search,
config_state.filter_diffable, config_state.filter_diffable,
@@ -325,8 +324,9 @@ pub fn config_ui(
}) { }) {
display_node( display_node(
ui, ui,
&mut new_selected_obj, &mut new_selected_index,
project_dir.as_deref(), project_dir.as_deref(),
objects,
&node, &node,
appearance, appearance,
node_open, node_open,
@@ -334,10 +334,11 @@ pub fn config_ui(
} }
}); });
} }
if new_selected_obj != *selected_obj { if new_selected_index != selected_index {
if let Some(obj) = new_selected_obj { if let Some(idx) = new_selected_index {
// Will set obj_changed, which will trigger a rebuild // Will set obj_changed, which will trigger a rebuild
state_guard.set_selected_obj(obj); let config = ObjectConfig::from(&objects[idx]);
state_guard.set_selected_obj(config);
} }
} }
if state_guard.config.selected_obj.is_some() if state_guard.config.selected_obj.is_some()
@@ -347,16 +348,17 @@ pub fn config_ui(
} }
} }
fn display_object( fn display_unit(
ui: &mut egui::Ui, ui: &mut egui::Ui,
selected_obj: &mut Option<ObjectConfig>, selected_obj: &mut Option<usize>,
project_dir: Option<&Path>, project_dir: Option<&Path>,
name: &str, name: &str,
object: &ProjectObject, units: &[ProjectObject],
index: usize,
appearance: &Appearance, appearance: &Appearance,
) { ) {
let object_name = object.name(); let object = &units[index];
let selected = matches!(selected_obj, Some(obj) if obj.name == object_name); let selected = *selected_obj == Some(index);
let color = if selected { let color = if selected {
appearance.emphasized_text_color appearance.emphasized_text_color
} else if let Some(complete) = object.complete() { } else if let Some(complete) = object.complete() {
@@ -381,18 +383,8 @@ fn display_object(
if get_source_path(project_dir, object).is_some() { if get_source_path(project_dir, object).is_some() {
response.context_menu(|ui| object_context_ui(ui, object, project_dir)); response.context_menu(|ui| object_context_ui(ui, object, project_dir));
} }
// Always recreate ObjectConfig if selected, in case the project config changed. if response.clicked() {
// ObjectConfig is compared using equality, so this won't unnecessarily trigger a rebuild. *selected_obj = Some(index);
if selected || response.clicked() {
*selected_obj = Some(ObjectConfig {
name: object_name.to_string(),
target_path: object.target_path.clone(),
base_path: object.base_path.clone(),
reverse_fn_order: object.reverse_fn_order(),
complete: object.complete(),
scratch: object.scratch.clone(),
source_path: object.source_path().cloned(),
});
} }
} }
@@ -427,18 +419,19 @@ enum NodeOpen {
fn display_node( fn display_node(
ui: &mut egui::Ui, ui: &mut egui::Ui,
selected_obj: &mut Option<ObjectConfig>, selected_obj: &mut Option<usize>,
project_dir: Option<&Path>, project_dir: Option<&Path>,
units: &[ProjectObject],
node: &ProjectObjectNode, node: &ProjectObjectNode,
appearance: &Appearance, appearance: &Appearance,
node_open: NodeOpen, node_open: NodeOpen,
) { ) {
match node { match node {
ProjectObjectNode::File(name, object) => { ProjectObjectNode::Unit(name, idx) => {
display_object(ui, selected_obj, project_dir, name, object, appearance); display_unit(ui, selected_obj, project_dir, name, units, *idx, appearance);
} }
ProjectObjectNode::Dir(name, children) => { ProjectObjectNode::Dir(name, children) => {
let contains_obj = selected_obj.as_ref().map(|path| contains_node(node, path)); let contains_obj = selected_obj.map(|idx| contains_node(node, idx));
let open = match node_open { let open = match node_open {
NodeOpen::Default => None, NodeOpen::Default => None,
NodeOpen::Open => Some(true), NodeOpen::Open => Some(true),
@@ -461,16 +454,16 @@ fn display_node(
.open(open) .open(open)
.show(ui, |ui| { .show(ui, |ui| {
for node in children { for node in children {
display_node(ui, selected_obj, project_dir, node, appearance, node_open); display_node(ui, selected_obj, project_dir, units, node, appearance, node_open);
} }
}); });
} }
} }
} }
fn contains_node(node: &ProjectObjectNode, selected_obj: &ObjectConfig) -> bool { fn contains_node(node: &ProjectObjectNode, selected_obj: usize) -> bool {
match node { match node {
ProjectObjectNode::File(_, object) => object.name() == selected_obj.name, ProjectObjectNode::Unit(_, idx) => *idx == selected_obj,
ProjectObjectNode::Dir(_, children) => { ProjectObjectNode::Dir(_, children) => {
children.iter().any(|node| contains_node(node, selected_obj)) children.iter().any(|node| contains_node(node, selected_obj))
} }
@@ -478,6 +471,7 @@ fn contains_node(node: &ProjectObjectNode, selected_obj: &ObjectConfig) -> bool
} }
fn filter_node( fn filter_node(
units: &[ProjectObject],
node: &ProjectObjectNode, node: &ProjectObjectNode,
search: &str, search: &str,
filter_diffable: bool, filter_diffable: bool,
@@ -485,12 +479,12 @@ fn filter_node(
show_hidden: bool, show_hidden: bool,
) -> Option<ProjectObjectNode> { ) -> Option<ProjectObjectNode> {
match node { match node {
ProjectObjectNode::File(name, object) => { ProjectObjectNode::Unit(name, idx) => {
let unit = &units[*idx];
if (search.is_empty() || name.to_ascii_lowercase().contains(search)) if (search.is_empty() || name.to_ascii_lowercase().contains(search))
&& (!filter_diffable && (!filter_diffable || (unit.base_path.is_some() && unit.target_path.is_some()))
|| (object.base_path.is_some() && object.target_path.is_some())) && (!filter_incomplete || matches!(unit.complete(), None | Some(false)))
&& (!filter_incomplete || matches!(object.complete(), None | Some(false))) && (show_hidden || !unit.hidden())
&& (show_hidden || !object.hidden())
{ {
Some(node.clone()) Some(node.clone())
} else { } else {
@@ -501,7 +495,14 @@ fn filter_node(
let new_children = children let new_children = children
.iter() .iter()
.filter_map(|child| { .filter_map(|child| {
filter_node(child, search, filter_diffable, filter_incomplete, show_hidden) filter_node(
units,
child,
search,
filter_diffable,
filter_incomplete,
show_hidden,
)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if !new_children.is_empty() { if !new_children.is_empty() {
@@ -570,14 +571,14 @@ pub fn project_window(
split_obj_config_ui(ui, &mut state_guard, config_state, appearance); split_obj_config_ui(ui, &mut state_guard, config_state, appearance);
}); });
if let Some(error) = &config_state.load_error { if let Some(error) = &state_guard.config_error {
let mut open = true; let mut open = true;
egui::Window::new("Error").open(&mut open).show(ctx, |ui| { egui::Window::new("Error").open(&mut open).show(ctx, |ui| {
ui.label("Failed to load project config:"); ui.label("Failed to load project config:");
ui.colored_label(appearance.delete_color, error); ui.colored_label(appearance.delete_color, error);
}); });
if !open { if !open {
config_state.load_error = None; state_guard.config_error = None;
} }
} }
} }

View File

@@ -1,23 +1,26 @@
use std::{cmp::min, default::Default, mem::take}; use std::{cmp::min, default::Default, mem::take};
use egui::{text::LayoutJob, Align, Label, Layout, Sense, Vec2, Widget}; use egui::{text::LayoutJob, Id, Label, RichText, Sense, Widget};
use egui_extras::{Column, TableBuilder};
use objdiff_core::{ use objdiff_core::{
diff::{ObjDataDiff, ObjDataDiffKind, ObjDiff}, diff::{ObjDataDiff, ObjDataDiffKind, ObjDiff},
obj::ObjInfo, obj::ObjInfo,
}; };
use time::format_description; use time::format_description;
use crate::views::{ use crate::{
hotkeys,
views::{
appearance::Appearance, appearance::Appearance,
symbol_diff::{DiffViewState, SymbolRefByName, View}, column_layout::{render_header, render_table},
symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState},
write_text, write_text,
},
}; };
const BYTES_PER_ROW: usize = 16; const BYTES_PER_ROW: usize = 16;
fn find_section(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<usize> { fn find_section(obj: &ObjInfo, section_name: &str) -> Option<usize> {
obj.sections.iter().position(|section| section.name == selected_symbol.section_name) obj.sections.iter().position(|section| section.name == section_name)
} }
fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appearance: &Appearance) { fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appearance: &Appearance) {
@@ -131,20 +134,37 @@ fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> {
split_diffs split_diffs
} }
#[derive(Clone, Copy)]
struct SectionDiffContext<'a> {
obj: &'a ObjInfo,
diff: &'a ObjDiff,
section_index: Option<usize>,
}
impl<'a> SectionDiffContext<'a> {
pub fn new(obj: Option<&'a (ObjInfo, ObjDiff)>, section_name: Option<&str>) -> Option<Self> {
obj.map(|(obj, diff)| Self {
obj,
diff,
section_index: section_name.and_then(|section_name| find_section(obj, section_name)),
})
}
#[inline]
pub fn has_section(&self) -> bool { self.section_index.is_some() }
}
fn data_table_ui( fn data_table_ui(
table: TableBuilder<'_>, ui: &mut egui::Ui,
left_obj: Option<&(ObjInfo, ObjDiff)>, available_width: f32,
right_obj: Option<&(ObjInfo, ObjDiff)>, left_ctx: Option<SectionDiffContext<'_>>,
selected_symbol: &SymbolRefByName, right_ctx: Option<SectionDiffContext<'_>>,
config: &Appearance, config: &Appearance,
) -> Option<()> { ) -> Option<()> {
let left_section = left_obj.and_then(|(obj, diff)| { let left_section = left_ctx
find_section(obj, selected_symbol).map(|i| (&obj.sections[i], &diff.sections[i])) .and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i])));
}); let right_section = right_ctx
let right_section = right_obj.and_then(|(obj, diff)| { .and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i])));
find_section(obj, selected_symbol).map(|i| (&obj.sections[i], &diff.sections[i]))
});
let total_bytes = left_section let total_bytes = left_section
.or(right_section)? .or(right_section)?
.1 .1
@@ -159,72 +179,80 @@ fn data_table_ui(
let left_diffs = left_section.map(|(_, section)| split_diffs(&section.data_diff)); let left_diffs = left_section.map(|(_, section)| split_diffs(&section.data_diff));
let right_diffs = right_section.map(|(_, section)| split_diffs(&section.data_diff)); let right_diffs = right_section.map(|(_, section)| split_diffs(&section.data_diff));
table.body(|body| { hotkeys::check_scroll_hotkeys(ui, true);
body.rows(config.code_font.size, total_rows, |mut row| {
let row_index = row.index(); render_table(ui, available_width, 2, config.code_font.size, total_rows, |row, column| {
let address = row_index * BYTES_PER_ROW; let i = row.index();
let address = i * BYTES_PER_ROW;
row.col(|ui| { row.col(|ui| {
if column == 0 {
if let Some(left_diffs) = &left_diffs { if let Some(left_diffs) = &left_diffs {
data_row_ui(ui, address, &left_diffs[row_index], config); data_row_ui(ui, address, &left_diffs[i], config);
} }
}); } else if column == 1 {
row.col(|ui| {
if let Some(right_diffs) = &right_diffs { if let Some(right_diffs) = &right_diffs {
data_row_ui(ui, address, &right_diffs[row_index], config); data_row_ui(ui, address, &right_diffs[i], config);
}
} }
});
}); });
}); });
Some(()) Some(())
} }
pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) { #[must_use]
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol) pub fn data_diff_ui(
else { ui: &mut egui::Ui,
return; state: &DiffViewState,
appearance: &Appearance,
) -> Option<DiffViewAction> {
let mut ret = None;
let Some(result) = &state.build else {
return ret;
}; };
let section_name =
state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref()).or_else(
|| state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref()),
);
let left_ctx = SectionDiffContext::new(result.first_obj.as_ref(), section_name);
let right_ctx = SectionDiffContext::new(result.second_obj.as_ref(), section_name);
// If both sides are missing a symbol, switch to symbol diff view
if !right_ctx.is_some_and(|ctx| ctx.has_section())
&& !left_ctx.is_some_and(|ctx| ctx.has_section())
{
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
}
// Header // Header
let available_width = ui.available_width(); let available_width = ui.available_width();
let column_width = available_width / 2.0; render_header(ui, available_width, 2, |ui, column| {
ui.allocate_ui_with_layout( if column == 0 {
Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min),
|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
// Left column // Left column
ui.allocate_ui_with_layout( if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) {
Vec2 { x: column_width, y: 100.0 }, ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
Layout::top_down(Align::Min),
|ui| {
ui.set_width(column_width);
if ui.button("⏴ Back").clicked() {
state.current_view = View::SymbolDiff;
} }
ui.scope(|ui| { if let Some(section) =
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); left_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i]))
ui.colored_label(appearance.highlight_color, &selected_symbol.symbol_name);
ui.label("Diff target:");
});
},
);
// Right column
ui.allocate_ui_with_layout(
Vec2 { x: column_width, y: 100.0 },
Layout::top_down(Align::Min),
|ui| {
ui.set_width(column_width);
ui.horizontal(|ui| {
if ui
.add_enabled(!state.build_running, egui::Button::new("Build"))
.clicked()
{ {
state.queue_build = true; ui.label(
RichText::new(section.name.clone())
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
} else {
ui.label(
RichText::new("Missing")
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
}
} else if column == 1 {
// Right column
ui.horizontal(|ui| {
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
ret = Some(DiffViewAction::Build);
} }
ui.scope(|ui| { ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
@@ -232,45 +260,38 @@ pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &A
ui.colored_label(appearance.replace_color, "Building…"); ui.colored_label(appearance.replace_color, "Building…");
} else { } else {
ui.label("Last built:"); ui.label("Last built:");
let format = let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label( ui.label(
result result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
.time
.to_offset(appearance.utc_offset)
.format(&format)
.unwrap(),
); );
} }
}); });
}); });
ui.scope(|ui| { if let Some(section) =
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); right_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i]))
ui.label(""); {
ui.label("Diff base:"); ui.label(
RichText::new(section.name.clone())
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
} else {
ui.label(
RichText::new("Missing")
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
}
}
}); });
},
);
},
);
ui.separator();
// Table // Table
ui.style_mut().interaction.selectable_labels = false; let id =
let available_height = ui.available_height(); Id::new(state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref()))
let table = TableBuilder::new(ui) .with(state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref()));
.striped(false) ui.push_id(id, |ui| {
.cell_layout(Layout::left_to_right(Align::Min)) data_table_ui(ui, available_width, left_ctx, right_ctx, appearance);
.columns(Column::exact(column_width).clip(true), 2) });
.resizable(false) ret
.auto_shrink([false, false])
.min_scrolled_height(available_height);
data_table_ui(
table,
result.first_obj.as_ref(),
result.second_obj.as_ref(),
selected_symbol,
appearance,
);
} }

View File

@@ -1,28 +1,23 @@
use egui::{Align, Layout, ScrollArea, Ui, Vec2}; use egui::{RichText, ScrollArea};
use egui_extras::{Size, StripBuilder};
use objdiff_core::{ use objdiff_core::{
arch::ppc::ExceptionInfo, arch::ppc::ExceptionInfo,
diff::ObjDiff, obj::{ObjInfo, ObjSymbol},
obj::{ObjInfo, ObjSymbol, SymbolRef},
}; };
use time::format_description; use time::format_description;
use crate::views::{ use crate::{
hotkeys,
views::{
appearance::Appearance, appearance::Appearance,
symbol_diff::{match_color_for_symbol, DiffViewState, SymbolRefByName, View}, column_layout::{render_header, render_strips},
function_diff::FunctionDiffContext,
symbol_diff::{
match_color_for_symbol, DiffViewAction, DiffViewNavigation, DiffViewState,
SymbolRefByName, View,
},
},
}; };
fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<SymbolRef> {
for (section_idx, section) in obj.sections.iter().enumerate() {
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
if symbol.name == selected_symbol.symbol_name {
return Some(SymbolRef { section_idx, symbol_idx });
}
}
}
None
}
fn decode_extab(extab: &ExceptionInfo) -> String { fn decode_extab(extab: &ExceptionInfo) -> String {
let mut text = String::from(""); let mut text = String::from("");
@@ -48,14 +43,12 @@ fn find_extab_entry<'a>(obj: &'a ObjInfo, symbol: &ObjSymbol) -> Option<&'a Exce
} }
fn extab_text_ui( fn extab_text_ui(
ui: &mut Ui, ui: &mut egui::Ui,
obj: &(ObjInfo, ObjDiff), ctx: FunctionDiffContext<'_>,
symbol_ref: SymbolRef, symbol: &ObjSymbol,
appearance: &Appearance, appearance: &Appearance,
) -> Option<()> { ) -> Option<()> {
let (_section, symbol) = obj.0.section_symbol(symbol_ref); if let Some(extab_entry) = find_extab_entry(ctx.obj, symbol) {
if let Some(extab_entry) = find_extab_entry(&obj.0, symbol) {
let text = decode_extab(extab_entry); let text = decode_extab(extab_entry);
ui.colored_label(appearance.replace_color, &text); ui.colored_label(appearance.replace_color, &text);
return Some(()); return Some(());
@@ -65,79 +58,131 @@ fn extab_text_ui(
} }
fn extab_ui( fn extab_ui(
ui: &mut Ui, ui: &mut egui::Ui,
obj: Option<&(ObjInfo, ObjDiff)>, ctx: FunctionDiffContext<'_>,
selected_symbol: &SymbolRefByName,
appearance: &Appearance, appearance: &Appearance,
_left: bool, _column: usize,
) { ) {
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
ui.scope(|ui| { ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
let symbol = obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol)); if let Some((_section, symbol)) =
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
if let (Some(object), Some(symbol_ref)) = (obj, symbol) { {
extab_text_ui(ui, object, symbol_ref, appearance); extab_text_ui(ui, ctx, symbol, appearance);
} }
}); });
}); });
} }
pub fn extab_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) { #[must_use]
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol) pub fn extab_diff_ui(
else { ui: &mut egui::Ui,
return; state: &DiffViewState,
appearance: &Appearance,
) -> Option<DiffViewAction> {
let mut ret = None;
let Some(result) = &state.build else {
return ret;
}; };
let mut left_ctx = FunctionDiffContext::new(
result.first_obj.as_ref(),
state.symbol_state.left_symbol.as_ref(),
);
let mut right_ctx = FunctionDiffContext::new(
result.second_obj.as_ref(),
state.symbol_state.right_symbol.as_ref(),
);
// If one side is missing a symbol, but the diff process found a match, use that symbol
let left_diff_symbol = left_ctx.and_then(|ctx| {
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
});
let right_diff_symbol = right_ctx.and_then(|ctx| {
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
});
if left_diff_symbol.is_some() && right_ctx.is_some_and(|ctx| !ctx.has_symbol()) {
let (right_section, right_symbol) =
right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap());
let symbol_ref = SymbolRefByName::new(right_symbol, right_section);
right_ctx = FunctionDiffContext::new(result.second_obj.as_ref(), Some(&symbol_ref));
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
view: Some(View::FunctionDiff),
left_symbol: state.symbol_state.left_symbol.clone(),
right_symbol: Some(symbol_ref),
}));
} else if right_diff_symbol.is_some() && left_ctx.is_some_and(|ctx| !ctx.has_symbol()) {
let (left_section, left_symbol) =
left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap());
let symbol_ref = SymbolRefByName::new(left_symbol, left_section);
left_ctx = FunctionDiffContext::new(result.first_obj.as_ref(), Some(&symbol_ref));
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
view: Some(View::FunctionDiff),
left_symbol: Some(symbol_ref),
right_symbol: state.symbol_state.right_symbol.clone(),
}));
}
// If both sides are missing a symbol, switch to symbol diff view
if right_ctx.is_some_and(|ctx| !ctx.has_symbol())
&& left_ctx.is_some_and(|ctx| !ctx.has_symbol())
{
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
}
// Header // Header
let available_width = ui.available_width(); let available_width = ui.available_width();
let column_width = available_width / 2.0; render_header(ui, available_width, 2, |ui, column| {
ui.allocate_ui_with_layout( if column == 0 {
Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min),
|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
// Left column // Left column
ui.allocate_ui_with_layout(
Vec2 { x: column_width, y: 100.0 },
Layout::top_down(Align::Min),
|ui| {
ui.set_width(column_width);
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.button("⏴ Back").clicked() { if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) {
state.current_view = View::SymbolDiff; ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
}
ui.separator();
if ui
.add_enabled(
!state.scratch_running
&& state.scratch_available
&& left_ctx.is_some_and(|ctx| ctx.has_symbol()),
egui::Button::new("📲 decomp.me"),
)
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
.on_disabled_hover_text("Scratch configuration missing")
.clicked()
{
if let Some((_section, symbol)) = left_ctx.and_then(|ctx| {
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
}) {
ret = Some(DiffViewAction::CreateScratch(symbol.name.clone()));
}
} }
}); });
let name = selected_symbol if let Some((_section, symbol)) = left_ctx
.demangled_symbol_name .and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)))
.as_deref()
.unwrap_or(&selected_symbol.symbol_name);
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.colored_label(appearance.highlight_color, name);
ui.label("Diff target:");
});
},
);
// Right column
ui.allocate_ui_with_layout(
Vec2 { x: column_width, y: 100.0 },
Layout::top_down(Align::Min),
|ui| {
ui.set_width(column_width);
ui.horizontal(|ui| {
if ui
.add_enabled(!state.build_running, egui::Button::new("Build"))
.clicked()
{ {
state.queue_build = true; let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
ui.label(
RichText::new(name)
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
} else {
ui.label(
RichText::new("Missing")
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
}
} else if column == 1 {
// Right column
ui.horizontal(|ui| {
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
ret = Some(DiffViewAction::Build);
} }
ui.scope(|ui| { ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
@@ -145,57 +190,64 @@ pub fn extab_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &
ui.colored_label(appearance.replace_color, "Building…"); ui.colored_label(appearance.replace_color, "Building…");
} else { } else {
ui.label("Last built:"); ui.label("Last built:");
let format = let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label( ui.label(
result result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
.time
.to_offset(appearance.utc_offset)
.format(&format)
.unwrap(),
); );
} }
}); });
ui.separator();
if ui
.add_enabled(state.source_path_available, egui::Button::new("🖹 Source file"))
.on_hover_text_at_pointer("Open the source file in the default editor")
.on_disabled_hover_text("Source file metadata missing")
.clicked()
{
ret = Some(DiffViewAction::OpenSourcePath);
}
}); });
ui.scope(|ui| { if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ctx.symbol_ref.map(|symbol_ref| {
if let Some(match_percent) = result (ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref))
.second_obj
.as_ref()
.and_then(|(obj, diff)| {
find_symbol(obj, selected_symbol).map(|sref| {
&diff.sections[sref.section_idx].symbols[sref.symbol_idx]
}) })
}) }) {
.and_then(|symbol| symbol.match_percent) let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
{ ui.label(
ui.colored_label( RichText::new(name)
match_color_for_symbol(match_percent, appearance), .font(appearance.code_font.clone())
format!("{match_percent:.0}%"), .color(appearance.highlight_color),
);
if let Some(match_percent) = symbol_diff.match_percent {
ui.label(
RichText::new(format!("{:.0}%", match_percent.floor()))
.font(appearance.code_font.clone())
.color(match_color_for_symbol(match_percent, appearance)),
); );
} else {
ui.colored_label(appearance.replace_color, "Missing");
} }
ui.label("Diff base:"); } else {
ui.label(
RichText::new("Missing")
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
}
}
}); });
},
); hotkeys::check_scroll_hotkeys(ui, true);
},
);
ui.separator();
// Table // Table
StripBuilder::new(ui).size(Size::remainder()).vertical(|mut strip| { render_strips(ui, available_width, 2, |ui, column| {
strip.strip(|builder| { if column == 0 {
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| { if let Some(ctx) = left_ctx {
strip.cell(|ui| { extab_ui(ui, ctx, appearance, column);
extab_ui(ui, result.first_obj.as_ref(), selected_symbol, appearance, true); }
}); } else if column == 1 {
strip.cell(|ui| { if let Some(ctx) = right_ctx {
extab_ui(ui, result.second_obj.as_ref(), selected_symbol, appearance, false); extab_ui(ui, ctx, appearance, column);
}); }
}); }
});
}); });
ret
} }

View File

@@ -1,28 +1,31 @@
use std::default::Default; use std::{cmp::Ordering, default::Default};
use egui::{text::LayoutJob, Align, Label, Layout, Response, Sense, Vec2, Widget}; use egui::{text::LayoutJob, Id, Label, Response, RichText, Sense, Widget};
use egui_extras::{Column, TableBuilder, TableRow}; use egui_extras::TableRow;
use objdiff_core::{ use objdiff_core::{
arch::ObjArch,
diff::{ diff::{
display::{display_diff, DiffText, HighlightKind}, display::{display_diff, DiffText, HighlightKind},
ObjDiff, ObjInsDiff, ObjInsDiffKind, ObjDiff, ObjInsDiff, ObjInsDiffKind,
}, },
obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSymbol, SymbolRef}, obj::{
ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSectionKind, ObjSymbol,
SymbolRef,
},
}; };
use time::format_description; use time::format_description;
use crate::views::{ use crate::{
hotkeys,
views::{
appearance::Appearance, appearance::Appearance,
symbol_diff::{match_color_for_symbol, DiffViewState, SymbolRefByName, View}, column_layout::{render_header, render_strips, render_table},
symbol_diff::{
match_color_for_symbol, symbol_list_ui, DiffViewAction, DiffViewNavigation,
DiffViewState, SymbolDiffContext, SymbolFilter, SymbolRefByName, SymbolViewState, View,
},
},
}; };
#[derive(Copy, Clone, Eq, PartialEq)]
enum ColumnId {
Left,
Right,
}
#[derive(Default)] #[derive(Default)]
pub struct FunctionViewState { pub struct FunctionViewState {
left_highlight: HighlightKind, left_highlight: HighlightKind,
@@ -30,16 +33,17 @@ pub struct FunctionViewState {
} }
impl FunctionViewState { impl FunctionViewState {
fn highlight(&self, column: ColumnId) -> &HighlightKind { pub fn highlight(&self, column: usize) -> &HighlightKind {
match column { match column {
ColumnId::Left => &self.left_highlight, 0 => &self.left_highlight,
ColumnId::Right => &self.right_highlight, 1 => &self.right_highlight,
_ => &HighlightKind::None,
} }
} }
fn set_highlight(&mut self, column: ColumnId, highlight: HighlightKind) { pub fn set_highlight(&mut self, column: usize, highlight: HighlightKind) {
match column { match column {
ColumnId::Left => { 0 => {
if highlight == self.left_highlight { if highlight == self.left_highlight {
if highlight == self.right_highlight { if highlight == self.right_highlight {
self.left_highlight = HighlightKind::None; self.left_highlight = HighlightKind::None;
@@ -51,7 +55,7 @@ impl FunctionViewState {
self.left_highlight = highlight; self.left_highlight = highlight;
} }
} }
ColumnId::Right => { 1 => {
if highlight == self.right_highlight { if highlight == self.right_highlight {
if highlight == self.left_highlight { if highlight == self.left_highlight {
self.left_highlight = HighlightKind::None; self.left_highlight = HighlightKind::None;
@@ -63,13 +67,19 @@ impl FunctionViewState {
self.right_highlight = highlight; self.right_highlight = highlight;
} }
} }
_ => {}
} }
} }
pub fn clear_highlight(&mut self) {
self.left_highlight = HighlightKind::None;
self.right_highlight = HighlightKind::None;
}
} }
fn ins_hover_ui( fn ins_hover_ui(
ui: &mut egui::Ui, ui: &mut egui::Ui,
arch: &dyn ObjArch, obj: &ObjInfo,
section: &ObjSection, section: &ObjSection,
ins: &ObjIns, ins: &ObjIns,
symbol: &ObjSymbol, symbol: &ObjSymbol,
@@ -112,28 +122,48 @@ fn ins_hover_ui(
} }
if let Some(reloc) = &ins.reloc { if let Some(reloc) = &ins.reloc {
ui.label(format!("Relocation type: {}", arch.display_reloc(reloc.flags))); ui.label(format!("Relocation type: {}", obj.arch.display_reloc(reloc.flags)));
ui.colored_label(appearance.highlight_color, format!("Name: {}", reloc.target.name)); let addend_str = match reloc.addend.cmp(&0i64) {
if let Some(section) = &reloc.target_section { Ordering::Greater => format!("+{:x}", reloc.addend),
ui.colored_label(appearance.highlight_color, format!("Section: {section}")); Ordering::Less => format!("-{:x}", -reloc.addend),
_ => "".to_string(),
};
ui.colored_label( ui.colored_label(
appearance.highlight_color, appearance.highlight_color,
format!("Address: {:x}", reloc.target.address), format!("Name: {}{}", reloc.target.name, addend_str),
);
if let Some(orig_section_index) = reloc.target.orig_section_index {
if let Some(section) =
obj.sections.iter().find(|s| s.orig_index == orig_section_index)
{
ui.colored_label(
appearance.highlight_color,
format!("Section: {}", section.name),
);
}
ui.colored_label(
appearance.highlight_color,
format!("Address: {:x}{}", reloc.target.address, addend_str),
); );
ui.colored_label( ui.colored_label(
appearance.highlight_color, appearance.highlight_color,
format!("Size: {:x}", reloc.target.size), format!("Size: {:x}", reloc.target.size),
); );
if let Some(s) = arch if reloc.addend >= 0 && reloc.target.bytes.len() > reloc.addend as usize {
.guess_data_type(ins) if let Some(s) = obj.arch.guess_data_type(ins).and_then(|ty| {
.and_then(|ty| arch.display_data_type(ty, &reloc.target.bytes)) obj.arch.display_data_type(ty, &reloc.target.bytes[reloc.addend as usize..])
{ }) {
ui.colored_label(appearance.highlight_color, s); ui.colored_label(appearance.highlight_color, s);
} }
}
} else { } else {
ui.colored_label(appearance.highlight_color, "Extern".to_string()); ui.colored_label(appearance.highlight_color, "Extern".to_string());
} }
} }
if let Some(decoded) = rlwinmdec::decode(&ins.formatted) {
ui.colored_label(appearance.highlight_color, decoded.trim());
}
}); });
} }
@@ -218,17 +248,19 @@ fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<Symbo
None None
} }
#[allow(clippy::too_many_arguments)] #[must_use]
#[expect(clippy::too_many_arguments)]
fn diff_text_ui( fn diff_text_ui(
ui: &mut egui::Ui, ui: &mut egui::Ui,
text: DiffText<'_>, text: DiffText<'_>,
ins_diff: &ObjInsDiff, ins_diff: &ObjInsDiff,
appearance: &Appearance, appearance: &Appearance,
ins_view_state: &mut FunctionViewState, ins_view_state: &FunctionViewState,
column: ColumnId, column: usize,
space_width: f32, space_width: f32,
response_cb: impl Fn(Response) -> Response, response_cb: impl Fn(Response) -> Response,
) { ) -> Option<DiffViewAction> {
let mut ret = None;
let label_text; let label_text;
let mut base_color = match ins_diff.kind { let mut base_color = match ins_diff.kind {
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => { ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
@@ -275,14 +307,18 @@ fn diff_text_ui(
base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()] base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
} }
} }
DiffText::Symbol(sym) => { DiffText::Symbol(sym, diff) => {
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name); let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
label_text = name.clone(); label_text = name.clone();
if let Some(diff) = diff {
base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
} else {
base_color = appearance.emphasized_text_color; base_color = appearance.emphasized_text_color;
} }
}
DiffText::Spacing(n) => { DiffText::Spacing(n) => {
ui.add_space(n as f32 * space_width); ui.add_space(n as f32 * space_width);
return; return ret;
} }
DiffText::Eol => { DiffText::Eol => {
label_text = "\n".to_string(); label_text = "\n".to_string();
@@ -299,22 +335,25 @@ fn diff_text_ui(
.ui(ui); .ui(ui);
response = response_cb(response); response = response_cb(response);
if response.clicked() { if response.clicked() {
ins_view_state.set_highlight(column, text.into()); ret = Some(DiffViewAction::SetDiffHighlight(column, text.into()));
} }
if len < pad_to { if len < pad_to {
ui.add_space((pad_to - len) as f32 * space_width); ui.add_space((pad_to - len) as f32 * space_width);
} }
ret
} }
#[must_use]
fn asm_row_ui( fn asm_row_ui(
ui: &mut egui::Ui, ui: &mut egui::Ui,
ins_diff: &ObjInsDiff, ins_diff: &ObjInsDiff,
symbol: &ObjSymbol, symbol: &ObjSymbol,
appearance: &Appearance, appearance: &Appearance,
ins_view_state: &mut FunctionViewState, ins_view_state: &FunctionViewState,
column: ColumnId, column: usize,
response_cb: impl Fn(Response) -> Response, response_cb: impl Fn(Response) -> Response,
) { ) -> Option<DiffViewAction> {
let mut ret = None;
ui.spacing_mut().item_spacing.x = 0.0; ui.spacing_mut().item_spacing.x = 0.0;
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
if ins_diff.kind != ObjInsDiffKind::None { if ins_diff.kind != ObjInsDiffKind::None {
@@ -322,7 +361,7 @@ fn asm_row_ui(
} }
let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' ')); let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' '));
display_diff(ins_diff, symbol.address, |text| { display_diff(ins_diff, symbol.address, |text| {
diff_text_ui( if let Some(action) = diff_text_ui(
ui, ui,
text, text,
ins_diff, ins_diff,
@@ -331,166 +370,383 @@ fn asm_row_ui(
column, column,
space_width, space_width,
&response_cb, &response_cb,
); ) {
ret = Some(action);
}
Ok::<_, ()>(()) Ok::<_, ()>(())
}) })
.unwrap(); .unwrap();
ret
} }
#[must_use]
fn asm_col_ui( fn asm_col_ui(
row: &mut TableRow<'_, '_>, row: &mut TableRow<'_, '_>,
obj: &(ObjInfo, ObjDiff), ctx: FunctionDiffContext<'_>,
symbol_ref: SymbolRef,
appearance: &Appearance, appearance: &Appearance,
ins_view_state: &mut FunctionViewState, ins_view_state: &FunctionViewState,
column: ColumnId, column: usize,
) { ) -> Option<DiffViewAction> {
let (section, symbol) = obj.0.section_symbol(symbol_ref); let mut ret = None;
let section = section.unwrap(); let symbol_ref = ctx.symbol_ref?;
let ins_diff = &obj.1.symbol_diff(symbol_ref).instructions[row.index()]; let (section, symbol) = ctx.obj.section_symbol(symbol_ref);
let section = section?;
let ins_diff = &ctx.diff.symbol_diff(symbol_ref).instructions[row.index()];
let response_cb = |response: Response| { let response_cb = |response: Response| {
if let Some(ins) = &ins_diff.ins { if let Some(ins) = &ins_diff.ins {
response.context_menu(|ui| ins_context_menu(ui, section, ins, symbol)); response.context_menu(|ui| ins_context_menu(ui, section, ins, symbol));
response.on_hover_ui_at_pointer(|ui| { response.on_hover_ui_at_pointer(|ui| {
ins_hover_ui(ui, obj.0.arch.as_ref(), section, ins, symbol, appearance) ins_hover_ui(ui, ctx.obj, section, ins, symbol, appearance)
}) })
} else { } else {
response response
} }
}; };
let (_, response) = row.col(|ui| { let (_, response) = row.col(|ui| {
asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state, column, response_cb); if let Some(action) =
asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state, column, response_cb)
{
ret = Some(action);
}
}); });
response_cb(response); response_cb(response);
ret
} }
fn empty_col_ui(row: &mut TableRow<'_, '_>) { #[must_use]
row.col(|ui| {
ui.label("");
});
}
fn asm_table_ui( fn asm_table_ui(
table: TableBuilder<'_>, ui: &mut egui::Ui,
left_obj: Option<&(ObjInfo, ObjDiff)>, available_width: f32,
right_obj: Option<&(ObjInfo, ObjDiff)>, left_ctx: Option<FunctionDiffContext<'_>>,
selected_symbol: &SymbolRefByName, right_ctx: Option<FunctionDiffContext<'_>>,
appearance: &Appearance, appearance: &Appearance,
ins_view_state: &mut FunctionViewState, ins_view_state: &FunctionViewState,
) -> Option<()> { symbol_state: &SymbolViewState,
let left_symbol = left_obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol)); ) -> Option<DiffViewAction> {
let right_symbol = right_obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol)); let mut ret = None;
let instructions_len = match (left_symbol, right_symbol) { let left_len = left_ctx.and_then(|ctx| {
(Some(left_symbol_ref), Some(right_symbol_ref)) => { ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len())
let left_len = left_obj.unwrap().1.symbol_diff(left_symbol_ref).instructions.len(); });
let right_len = right_obj.unwrap().1.symbol_diff(right_symbol_ref).instructions.len(); let right_len = right_ctx.and_then(|ctx| {
debug_assert_eq!(left_len, right_len); ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len())
});
let instructions_len = match (left_len, right_len) {
(Some(left_len), Some(right_len)) => {
if left_len != right_len {
ui.label("Instruction count mismatch");
return None;
}
left_len left_len
} }
(Some(left_symbol_ref), None) => { (Some(left_len), None) => left_len,
left_obj.unwrap().1.symbol_diff(left_symbol_ref).instructions.len() (None, Some(right_len)) => right_len,
(None, None) => {
ui.label("No symbol selected");
return None;
} }
(None, Some(right_symbol_ref)) => {
right_obj.unwrap().1.symbol_diff(right_symbol_ref).instructions.len()
}
(None, None) => return None,
}; };
table.body(|body| { if left_len.is_some() && right_len.is_some() {
body.rows(appearance.code_font.size, instructions_len, |mut row| { // Joint view
if let (Some(left_obj), Some(left_symbol_ref)) = (left_obj, left_symbol) { hotkeys::check_scroll_hotkeys(ui, true);
asm_col_ui( render_table(
&mut row, ui,
left_obj, available_width,
2,
appearance.code_font.size,
instructions_len,
|row, column| {
if column == 0 {
if let Some(ctx) = left_ctx {
if let Some(action) =
asm_col_ui(row, ctx, appearance, ins_view_state, column)
{
ret = Some(action);
}
}
} else if column == 1 {
if let Some(ctx) = right_ctx {
if let Some(action) =
asm_col_ui(row, ctx, appearance, ins_view_state, column)
{
ret = Some(action);
}
}
if row.response().clicked() {
ret = Some(DiffViewAction::ClearDiffHighlight);
}
}
},
);
} else {
// Split view, one side is the symbol list
render_strips(ui, available_width, 2, |ui, column| {
if column == 0 {
if let Some(ctx) = left_ctx {
if ctx.has_symbol() {
hotkeys::check_scroll_hotkeys(ui, false);
render_table(
ui,
available_width / 2.0,
1,
appearance.code_font.size,
instructions_len,
|row, column| {
if let Some(action) =
asm_col_ui(row, ctx, appearance, ins_view_state, column)
{
ret = Some(action);
}
if row.response().clicked() {
ret = Some(DiffViewAction::ClearDiffHighlight);
}
},
);
} else if let Some((right_ctx, right_symbol_ref)) =
right_ctx.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| (ctx, symbol_ref)))
{
if let Some(action) = symbol_list_ui(
ui,
SymbolDiffContext { obj: ctx.obj, diff: ctx.diff },
None,
symbol_state,
SymbolFilter::Mapping(right_symbol_ref),
appearance,
column,
) {
match action {
DiffViewAction::Navigate(DiffViewNavigation {
left_symbol: Some(left_symbol_ref),
..
}) => {
let (right_section, right_symbol) =
right_ctx.obj.section_symbol(right_symbol_ref);
ret = Some(DiffViewAction::SetMapping(
match right_section.map(|s| s.kind) {
Some(ObjSectionKind::Code) => View::FunctionDiff,
_ => View::SymbolDiff,
},
left_symbol_ref, left_symbol_ref,
appearance, SymbolRefByName::new(right_symbol, right_section),
ins_view_state, ));
ColumnId::Left,
);
} else {
empty_col_ui(&mut row);
} }
if let (Some(right_obj), Some(right_symbol_ref)) = (right_obj, right_symbol) { _ => {
asm_col_ui( ret = Some(action);
&mut row, }
right_obj, }
}
}
} else {
ui.label("No left object");
}
} else if column == 1 {
if let Some(ctx) = right_ctx {
if ctx.has_symbol() {
hotkeys::check_scroll_hotkeys(ui, false);
render_table(
ui,
available_width / 2.0,
1,
appearance.code_font.size,
instructions_len,
|row, column| {
if let Some(action) =
asm_col_ui(row, ctx, appearance, ins_view_state, column)
{
ret = Some(action);
}
if row.response().clicked() {
ret = Some(DiffViewAction::ClearDiffHighlight);
}
},
);
} else if let Some((left_ctx, left_symbol_ref)) =
left_ctx.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| (ctx, symbol_ref)))
{
if let Some(action) = symbol_list_ui(
ui,
SymbolDiffContext { obj: ctx.obj, diff: ctx.diff },
None,
symbol_state,
SymbolFilter::Mapping(left_symbol_ref),
appearance,
column,
) {
match action {
DiffViewAction::Navigate(DiffViewNavigation {
right_symbol: Some(right_symbol_ref),
..
}) => {
let (left_section, left_symbol) =
left_ctx.obj.section_symbol(left_symbol_ref);
ret = Some(DiffViewAction::SetMapping(
match left_section.map(|s| s.kind) {
Some(ObjSectionKind::Code) => View::FunctionDiff,
_ => View::SymbolDiff,
},
SymbolRefByName::new(left_symbol, left_section),
right_symbol_ref, right_symbol_ref,
appearance, ));
ins_view_state, }
ColumnId::Right, _ => {
); ret = Some(action);
}
}
}
}
} else { } else {
empty_col_ui(&mut row); ui.label("No right object");
}
} }
}); });
}); }
Some(()) ret
} }
pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) { #[derive(Clone, Copy)]
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol) pub struct FunctionDiffContext<'a> {
else { pub obj: &'a ObjInfo,
return; pub diff: &'a ObjDiff,
pub symbol_ref: Option<SymbolRef>,
}
impl<'a> FunctionDiffContext<'a> {
pub fn new(
obj: Option<&'a (ObjInfo, ObjDiff)>,
selected_symbol: Option<&SymbolRefByName>,
) -> Option<Self> {
obj.map(|(obj, diff)| Self {
obj,
diff,
symbol_ref: selected_symbol.and_then(|s| find_symbol(obj, s)),
})
}
#[inline]
pub fn has_symbol(&self) -> bool { self.symbol_ref.is_some() }
}
#[must_use]
pub fn function_diff_ui(
ui: &mut egui::Ui,
state: &DiffViewState,
appearance: &Appearance,
) -> Option<DiffViewAction> {
let mut ret = None;
let Some(result) = &state.build else {
return ret;
}; };
let mut left_ctx = FunctionDiffContext::new(
result.first_obj.as_ref(),
state.symbol_state.left_symbol.as_ref(),
);
let mut right_ctx = FunctionDiffContext::new(
result.second_obj.as_ref(),
state.symbol_state.right_symbol.as_ref(),
);
// If one side is missing a symbol, but the diff process found a match, use that symbol
let left_diff_symbol = left_ctx.and_then(|ctx| {
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
});
let right_diff_symbol = right_ctx.and_then(|ctx| {
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
});
if left_diff_symbol.is_some() && right_ctx.is_some_and(|ctx| !ctx.has_symbol()) {
let (right_section, right_symbol) =
right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap());
let symbol_ref = SymbolRefByName::new(right_symbol, right_section);
right_ctx = FunctionDiffContext::new(result.second_obj.as_ref(), Some(&symbol_ref));
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
view: Some(View::FunctionDiff),
left_symbol: state.symbol_state.left_symbol.clone(),
right_symbol: Some(symbol_ref),
}));
} else if right_diff_symbol.is_some() && left_ctx.is_some_and(|ctx| !ctx.has_symbol()) {
let (left_section, left_symbol) =
left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap());
let symbol_ref = SymbolRefByName::new(left_symbol, left_section);
left_ctx = FunctionDiffContext::new(result.first_obj.as_ref(), Some(&symbol_ref));
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
view: Some(View::FunctionDiff),
left_symbol: Some(symbol_ref),
right_symbol: state.symbol_state.right_symbol.clone(),
}));
}
// If both sides are missing a symbol, switch to symbol diff view
if right_ctx.is_some_and(|ctx| !ctx.has_symbol())
&& left_ctx.is_some_and(|ctx| !ctx.has_symbol())
{
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
}
// Header // Header
let available_width = ui.available_width(); let available_width = ui.available_width();
let column_width = available_width / 2.0; render_header(ui, available_width, 2, |ui, column| {
ui.allocate_ui_with_layout( if column == 0 {
Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min),
|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
// Left column // Left column
ui.allocate_ui_with_layout(
Vec2 { x: column_width, y: 100.0 },
Layout::top_down(Align::Min),
|ui| {
ui.set_width(column_width);
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.button("⏴ Back").clicked() { if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) {
state.current_view = View::SymbolDiff; ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
} }
ui.separator(); ui.separator();
if ui if ui
.add_enabled( .add_enabled(
!state.scratch_running && state.scratch_available, !state.scratch_running
&& state.scratch_available
&& left_ctx.is_some_and(|ctx| ctx.has_symbol()),
egui::Button::new("📲 decomp.me"), egui::Button::new("📲 decomp.me"),
) )
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)") .on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
.on_disabled_hover_text("Scratch configuration missing") .on_disabled_hover_text("Scratch configuration missing")
.clicked() .clicked()
{ {
state.queue_scratch = true; if let Some((_section, symbol)) = left_ctx.and_then(|ctx| {
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
}) {
ret = Some(DiffViewAction::CreateScratch(symbol.name.clone()));
}
} }
}); });
let name = selected_symbol if let Some((_section, symbol)) = left_ctx
.demangled_symbol_name .and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)))
.as_deref()
.unwrap_or(&selected_symbol.symbol_name);
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.colored_label(appearance.highlight_color, name);
ui.label("Diff target:");
});
},
);
// Right column
ui.allocate_ui_with_layout(
Vec2 { x: column_width, y: 100.0 },
Layout::top_down(Align::Min),
|ui| {
ui.set_width(column_width);
ui.horizontal(|ui| {
if ui
.add_enabled(!state.build_running, egui::Button::new("Build"))
.clicked()
{ {
state.queue_build = true; let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
ui.label(
RichText::new(name)
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
if right_ctx.is_some_and(|m| m.has_symbol())
&& (ui
.button("Change target")
.on_hover_text_at_pointer("Choose a different symbol to use as the target")
.clicked()
|| hotkeys::consume_change_target_shortcut(ui.ctx()))
{
if let Some(symbol_ref) = state.symbol_state.right_symbol.as_ref() {
ret = Some(DiffViewAction::SelectingLeft(symbol_ref.clone()));
}
}
} else {
ui.label(
RichText::new("Missing")
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
ui.label(
RichText::new("Choose target symbol")
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
}
} else if column == 1 {
// Right column
ui.horizontal(|ui| {
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
ret = Some(DiffViewAction::Build);
} }
ui.scope(|ui| { ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
@@ -498,74 +754,91 @@ pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance
ui.colored_label(appearance.replace_color, "Building…"); ui.colored_label(appearance.replace_color, "Building…");
} else { } else {
ui.label("Last built:"); ui.label("Last built:");
let format = let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label( ui.label(
result result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
.time
.to_offset(appearance.utc_offset)
.format(&format)
.unwrap(),
); );
} }
}); });
ui.separator(); ui.separator();
if ui if ui
.add_enabled( .add_enabled(state.source_path_available, egui::Button::new("🖹 Source file"))
state.source_path_available,
egui::Button::new("🖹 Source file"),
)
.on_hover_text_at_pointer("Open the source file in the default editor") .on_hover_text_at_pointer("Open the source file in the default editor")
.on_disabled_hover_text("Source file metadata missing") .on_disabled_hover_text("Source file metadata missing")
.clicked() .clicked()
{ {
state.queue_open_source_path = true; ret = Some(DiffViewAction::OpenSourcePath);
} }
}); });
ui.scope(|ui| { if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ctx.symbol_ref.map(|symbol_ref| {
if let Some(match_percent) = result (ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref))
.second_obj
.as_ref()
.and_then(|(obj, diff)| {
find_symbol(obj, selected_symbol).map(|sref| {
&diff.sections[sref.section_idx].symbols[sref.symbol_idx]
}) })
}) }) {
.and_then(|symbol| symbol.match_percent) let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
{ ui.label(
ui.colored_label( RichText::new(name)
match_color_for_symbol(match_percent, appearance), .font(appearance.code_font.clone())
format!("{match_percent:.0}%"), .color(appearance.highlight_color),
);
ui.horizontal(|ui| {
if let Some(match_percent) = symbol_diff.match_percent {
ui.label(
RichText::new(format!("{:.0}%", match_percent.floor()))
.font(appearance.code_font.clone())
.color(match_color_for_symbol(match_percent, appearance)),
); );
} else {
ui.colored_label(appearance.replace_color, "Missing");
} }
ui.label("Diff base:"); if left_ctx.is_some_and(|m| m.has_symbol()) {
});
},
);
},
);
ui.separator(); ui.separator();
if ui
.button("Change base")
.on_hover_text_at_pointer(
"Choose a different symbol to use as the base",
)
.clicked()
|| hotkeys::consume_change_base_shortcut(ui.ctx())
{
if let Some(symbol_ref) = state.symbol_state.left_symbol.as_ref() {
ret = Some(DiffViewAction::SelectingRight(symbol_ref.clone()));
}
}
}
});
} else {
ui.label(
RichText::new("Missing")
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
ui.label(
RichText::new("Choose base symbol")
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
}
}
});
// Table // Table
ui.style_mut().interaction.selectable_labels = false; let id = Id::new(state.symbol_state.left_symbol.as_ref().map(|s| s.symbol_name.as_str()))
let available_height = ui.available_height(); .with(state.symbol_state.right_symbol.as_ref().map(|s| s.symbol_name.as_str()));
let table = TableBuilder::new(ui) if let Some(action) = ui
.striped(false) .push_id(id, |ui| {
.cell_layout(Layout::left_to_right(Align::Min))
.columns(Column::exact(column_width).clip(true), 2)
.resizable(false)
.auto_shrink([false, false])
.min_scrolled_height(available_height);
asm_table_ui( asm_table_ui(
table, ui,
result.first_obj.as_ref(), available_width,
result.second_obj.as_ref(), left_ctx,
selected_symbol, right_ctx,
appearance, appearance,
&mut state.function_state, &state.function_state,
); &state.symbol_state,
)
})
.inner
{
ret = Some(action);
}
ret
} }

View File

@@ -1,11 +1,9 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use egui::{ProgressBar, RichText, Widget}; use egui::{ProgressBar, RichText, Widget};
use objdiff_core::jobs::{JobQueue, JobStatus};
use crate::{ use crate::views::appearance::Appearance;
jobs::{JobQueue, JobStatus},
views::appearance::Appearance,
};
pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) { pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) {
if ui.button("Clear").clicked() { if ui.button("Clear").clicked() {

View File

@@ -1,6 +1,7 @@
use egui::{text::LayoutJob, Color32, FontId, TextFormat}; use egui::{text::LayoutJob, Color32, FontId, TextFormat};
pub(crate) mod appearance; pub(crate) mod appearance;
pub(crate) mod column_layout;
pub(crate) mod config; pub(crate) mod config;
pub(crate) mod data_diff; pub(crate) mod data_diff;
pub(crate) mod debug; pub(crate) mod debug;

View File

@@ -16,13 +16,13 @@ pub fn rlwinm_decode_window(
egui::Window::new("Rlwinm Decoder").open(show).show(ctx, |ui| { egui::Window::new("Rlwinm Decoder").open(show).show(ctx, |ui| {
ui.text_edit_singleline(&mut state.text); ui.text_edit_singleline(&mut state.text);
ui.add_space(10.0); ui.add_space(10.0);
if let Some(demangled) = rlwinmdec::decode(&state.text) { if let Some(decoded) = rlwinmdec::decode(&state.text) {
ui.scope(|ui| { ui.scope(|ui| {
ui.style_mut().override_text_style = Some(TextStyle::Monospace); ui.style_mut().override_text_style = Some(TextStyle::Monospace);
ui.colored_label(appearance.replace_color, &demangled); ui.colored_label(appearance.replace_color, decoded.trim());
}); });
if ui.button("Copy").clicked() { if ui.button("Copy").clicked() {
ui.output_mut(|output| output.copied_text = demangled); ui.output_mut(|output| output.copied_text = decoded);
} }
} else { } else {
ui.scope(|ui| { ui.scope(|ui| {

File diff suppressed because it is too large Load Diff