From 9a7d2bcebfa2b65f90c5fbe42095772859cfef42 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 27 Feb 2024 18:47:51 -0700 Subject: [PATCH] Experimental objdiff-cli (WIP) --- Cargo.lock | 233 ++++++++++++- Cargo.toml | 1 + objdiff-cli/Cargo.toml | 29 ++ objdiff-cli/build.rs | 9 + objdiff-cli/src/argp_version.rs | 64 ++++ objdiff-cli/src/cmd/diff.rs | 322 ++++++++++++++++++ objdiff-cli/src/cmd/mod.rs | 2 + objdiff-cli/src/cmd/report.rs | 229 +++++++++++++ objdiff-cli/src/main.rs | 142 ++++++++ objdiff-cli/src/util/mod.rs | 1 + objdiff-cli/src/util/term.rs | 15 + objdiff-core/Cargo.toml | 11 +- objdiff-core/src/config/mod.rs | 148 ++++++++ objdiff-core/src/diff/display.rs | 175 ++++++++++ objdiff-core/src/diff/mod.rs | 2 + objdiff-core/src/lib.rs | 2 + objdiff-core/src/obj/ppc.rs | 18 +- objdiff-gui/Cargo.toml | 2 +- objdiff-gui/src/app.rs | 16 +- objdiff-gui/src/config.rs | 149 +------- objdiff-gui/src/views/appearance.rs | 11 +- objdiff-gui/src/views/config.rs | 12 +- objdiff-gui/src/views/function_diff.rs | 449 +++++++------------------ 23 files changed, 1541 insertions(+), 501 deletions(-) create mode 100644 objdiff-cli/Cargo.toml create mode 100644 objdiff-cli/build.rs create mode 100644 objdiff-cli/src/argp_version.rs create mode 100644 objdiff-cli/src/cmd/diff.rs create mode 100644 objdiff-cli/src/cmd/mod.rs create mode 100644 objdiff-cli/src/cmd/report.rs create mode 100644 objdiff-cli/src/main.rs create mode 100644 objdiff-cli/src/util/mod.rs create mode 100644 objdiff-cli/src/util/term.rs create mode 100644 objdiff-core/src/config/mod.rs create mode 100644 objdiff-core/src/diff/display.rs diff --git a/Cargo.lock b/Cargo.lock index 3d2f94f..274d7d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -226,6 +226,27 @@ dependencies = [ "serde", ] +[[package]] +name = "argp" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c16c577a1a3b720a90eb2127bd0ae61530a71064d1a6babaaaa87f6174b9f1" +dependencies = [ + "argp_derive", +] + +[[package]] +name = "argp_derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe3763c8b5e0ef2f7d0df26daa671808cc75e2d81547f63ccca96bf045e41799" +dependencies = [ + "proc-macro2", + "pulldown-cmark", + "quote", + "syn 1.0.109", +] + [[package]] name = "arrayref" version = "0.3.7" @@ -1062,12 +1083,56 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.4.2", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -1356,6 +1421,12 @@ dependencies = [ "winit", ] +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + [[package]] name = "emath" version = "0.26.2" @@ -1366,6 +1437,15 @@ dependencies = [ "serde", ] +[[package]] +name = "enable-ansi-support" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4ff3ae2a9aa54bf7ee0983e59303224de742818c1822d89f07da9856d9bc60" +dependencies = [ + "windows-sys 0.42.0", +] + [[package]] name = "encode_unicode" version = "0.3.6" @@ -1854,6 +1934,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" version = "0.2.12" @@ -1901,8 +1990,8 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.5", + "regex-syntax 0.8.2", "serde", ] @@ -2294,6 +2383,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + [[package]] name = "itoa" version = "1.0.10" @@ -2465,6 +2560,15 @@ dependencies = [ "libc", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "memchr" version = "2.7.1" @@ -2859,6 +2963,25 @@ dependencies = [ "objc", ] +[[package]] +name = "objdiff-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "argp", + "crossterm", + "enable-ansi-support", + "log", + "objdiff-core", + "rayon", + "serde", + "serde_json", + "supports-color", + "tracing", + "tracing-attributes", + "tracing-subscriber", +] + [[package]] name = "objdiff-core" version = "1.0.0" @@ -2869,13 +2992,17 @@ dependencies = [ "filetime", "flagset", "gimli", + "globset", "log", "memmap2", "num-traits", "object", "ppc750cl", "rabbitizer", + "semver", "serde", + "serde_json", + "serde_yaml", "similar", "twox-hash", ] @@ -3235,6 +3362,18 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" +[[package]] +name = "pulldown-cmark" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" +dependencies = [ + "bitflags 2.4.2", + "getopts", + "memchr", + "unicase", +] + [[package]] name = "quick-xml" version = "0.23.1" @@ -3315,6 +3454,26 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42a9830a0e1b9fb145ebb365b8bc4ccd75f290f98c0247deafbbe2c75cefb544" +[[package]] +name = "rayon" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.3.5" @@ -3352,8 +3511,17 @@ checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.5", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -3364,9 +3532,15 @@ checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.2", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.2" @@ -3782,6 +3956,27 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -3925,6 +4120,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +[[package]] +name = "supports-color" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9829b314621dfc575df4e409e79f9d6a66a3bd707ab73f23cb4aa3a854ac854f" +dependencies = [ + "is_ci", +] + [[package]] name = "syn" version = "1.0.109" @@ -4268,10 +4472,14 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] @@ -4887,6 +5095,21 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index 31e139d..bf52f16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "objdiff-cli", "objdiff-core", "objdiff-gui", ] diff --git a/objdiff-cli/Cargo.toml b/objdiff-cli/Cargo.toml new file mode 100644 index 0000000..9c1a5b7 --- /dev/null +++ b/objdiff-cli/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "objdiff-cli" +version = "0.1.0" +edition = "2021" +rust-version = "1.70" +authors = ["Luke Street "] +license = "MIT OR Apache-2.0" +repository = "https://github.com/encounter/objdiff" +readme = "../README.md" +description = """ +A local diffing tool for decompilation projects. +""" +publish = false +build = "build.rs" + +[dependencies] +crossterm = "0.27.0" +anyhow = "1.0.80" +argp = "0.3.0" +enable-ansi-support = "0.2.1" +log = "0.4.20" +objdiff-core = { path = "../objdiff-core", features = ["all"] } +supports-color = "3.0.0" +tracing = "0.1.40" +tracing-attributes = "0.1.27" +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1.0.111" +rayon = "1.8.1" diff --git a/objdiff-cli/build.rs b/objdiff-cli/build.rs new file mode 100644 index 0000000..a488c56 --- /dev/null +++ b/objdiff-cli/build.rs @@ -0,0 +1,9 @@ +fn main() { + let output = std::process::Command::new("git") + .args(["rev-parse", "HEAD"]) + .output() + .expect("Failed to execute git"); + let rev = String::from_utf8(output.stdout).expect("Failed to parse git output"); + println!("cargo:rustc-env=GIT_COMMIT_SHA={rev}"); + println!("cargo:rustc-rerun-if-changed=.git/HEAD"); +} diff --git a/objdiff-cli/src/argp_version.rs b/objdiff-cli/src/argp_version.rs new file mode 100644 index 0000000..eccce05 --- /dev/null +++ b/objdiff-cli/src/argp_version.rs @@ -0,0 +1,64 @@ +// Originally from https://gist.github.com/suluke/e0c672492126be0a4f3b4f0e1115d77c +//! Extend `argp` to be better integrated with the `cargo` ecosystem +//! +//! For now, this only adds a --version/-V option which causes early-exit. +use std::ffi::OsStr; + +use argp::{parser::ParseGlobalOptions, EarlyExit, FromArgs, TopLevelCommand}; + +struct ArgsOrVersion(T) +where T: FromArgs; + +impl TopLevelCommand for ArgsOrVersion where T: FromArgs {} + +impl FromArgs for ArgsOrVersion +where T: FromArgs +{ + fn _from_args( + command_name: &[&str], + args: &[&OsStr], + parent: Option<&mut dyn ParseGlobalOptions>, + ) -> Result { + /// Also use argp for catching `--version`-only invocations + #[derive(FromArgs)] + struct Version { + /// Print version information and exit. + #[argp(switch, short = 'V')] + pub version: bool, + } + + match Version::from_args(command_name, args) { + Ok(v) => { + if v.version { + println!( + "{} {} {}", + command_name.first().unwrap_or(&""), + env!("CARGO_PKG_VERSION"), + env!("GIT_COMMIT_SHA"), + ); + std::process::exit(0); + } else { + // Pass through empty arguments + T::_from_args(command_name, args, parent).map(Self) + } + } + Err(exit) => match exit { + EarlyExit::Help(_help) => { + // TODO: Chain help info from Version + // For now, we just put the switch on T as well + T::from_args(command_name, &["--help"]).map(Self) + } + EarlyExit::Err(_) => T::_from_args(command_name, args, parent).map(Self), + }, + } + } +} + +/// Create a `FromArgs` type from the current process’s `env::args`. +/// +/// This function will exit early from the current process if argument parsing was unsuccessful or if information like `--help` was requested. +/// Error messages will be printed to stderr, and `--help` output to stdout. +pub fn from_env() -> T +where T: TopLevelCommand { + argp::parse_args_or_exit::>(argp::DEFAULT).0 +} diff --git a/objdiff-cli/src/cmd/diff.rs b/objdiff-cli/src/cmd/diff.rs new file mode 100644 index 0000000..e9c61f8 --- /dev/null +++ b/objdiff-cli/src/cmd/diff.rs @@ -0,0 +1,322 @@ +use std::{ + io::{stdout, Write}, + path::PathBuf, +}; + +use anyhow::{bail, Context, Result}; +use argp::FromArgs; +use crossterm::{ + cursor::{Hide, MoveRight, MoveTo, Show}, + event, + event::{ + DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseEventKind, + }, + style::{Color, PrintStyledContent, Stylize}, + terminal::{ + disable_raw_mode, enable_raw_mode, size as terminal_size, Clear, ClearType, + EnterAlternateScreen, LeaveAlternateScreen, SetTitle, + }, +}; +use event::KeyModifiers; +use objdiff_core::{ + diff, + diff::display::{display_diff, DiffText}, + obj, + obj::{ObjInfo, ObjInsDiffKind, ObjSection, ObjSectionKind, ObjSymbol}, +}; + +use crate::util::term::crossterm_panic_handler; + +#[derive(FromArgs, PartialEq, Debug)] +/// Diff two object files. +#[argp(subcommand, name = "diff")] +pub struct Args { + #[argp(positional)] + /// Target object file + target: PathBuf, + #[argp(positional)] + /// Base object file + base: PathBuf, + #[argp(option, short = 's')] + /// Function symbol to diff + symbol: String, +} + +pub fn run(args: Args) -> Result<()> { + let mut target = obj::elf::read(&args.target) + .with_context(|| format!("Loading {}", args.target.display()))?; + let mut base = + obj::elf::read(&args.base).with_context(|| format!("Loading {}", args.base.display()))?; + let config = diff::DiffObjConfig::default(); + diff::diff_objs(&config, Some(&mut target), Some(&mut base))?; + + let left_sym = find_function(&target, &args.symbol); + let right_sym = find_function(&base, &args.symbol); + let max_len = match (left_sym, 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: {}", args.symbol), + }; + + crossterm_panic_handler(); + enable_raw_mode()?; + crossterm::queue!( + stdout(), + EnterAlternateScreen, + SetTitle(format!("{} - objdiff", args.symbol)), + Hide, + EnableMouseCapture, + )?; + + let mut redraw = true; + let mut skip = 0; + loop { + let y_offset = 2; + let (sx, sy) = terminal_size()?; + let per_page = sy as usize - y_offset; + if redraw { + let mut w = stdout().lock(); + crossterm::queue!( + w, + Clear(ClearType::All), + MoveTo(0, 0), + PrintStyledContent(args.symbol.clone().with(Color::White)), + MoveTo(0, 1), + PrintStyledContent(" ".repeat(sx as usize).underlined()), + MoveTo(0, 1), + PrintStyledContent("TARGET ".underlined()), + MoveTo(sx / 2, 0), + PrintStyledContent("Last built: 18:24:20".with(Color::White)), + MoveTo(sx / 2, 1), + PrintStyledContent("BASE ".underlined()), + )?; + if let Some(percent) = right_sym.and_then(|(_, s)| s.match_percent) { + crossterm::queue!( + w, + PrintStyledContent( + format!("{:.2}%", percent).with(match_percent_color(percent)).underlined() + ) + )?; + } + + if skip > max_len - per_page { + skip = max_len - per_page; + } + if let Some((_, symbol)) = left_sym { + print_sym(&mut w, symbol, 0, y_offset as u16, sx / 2 - 1, sy, skip)?; + } + if let Some((_, symbol)) = right_sym { + print_sym(&mut w, symbol, sx / 2, y_offset as u16, sx, sy, skip)?; + } + w.flush()?; + redraw = false; + } + + match event::read()? { + Event::Key(event) + if matches!(event.kind, KeyEventKind::Press | KeyEventKind::Repeat) => + { + match event.code { + // Quit + KeyCode::Esc | KeyCode::Char('q') => break, + // Page up + KeyCode::PageUp => { + skip = skip.saturating_sub(per_page); + redraw = true; + } + // Page up (shift + space) + KeyCode::Char(' ') if event.modifiers.contains(KeyModifiers::SHIFT) => { + skip = skip.saturating_sub(per_page); + redraw = true; + } + // Page down + KeyCode::Char(' ') | KeyCode::PageDown => { + skip += per_page; + redraw = true; + } + // Scroll down + KeyCode::Down | KeyCode::Char('j') => { + skip += 1; + redraw = true; + } + // Scroll up + KeyCode::Up | KeyCode::Char('k') => { + skip = skip.saturating_sub(1); + redraw = true; + } + // Scroll to start + KeyCode::Char('g') => { + skip = 0; + redraw = true; + } + // Scroll to end + KeyCode::Char('G') => { + skip = max_len; + redraw = true; + } + _ => {} + } + } + Event::Mouse(event) => match event.kind { + MouseEventKind::ScrollDown => { + skip += 3; + redraw = true; + } + MouseEventKind::ScrollUp => { + skip = skip.saturating_sub(3); + redraw = true; + } + _ => {} + }, + Event::Resize(_, _) => redraw = true, + _ => {} + } + } + + // Reset terminal + crossterm::execute!(stdout(), LeaveAlternateScreen, Show, DisableMouseCapture)?; + disable_raw_mode()?; + Ok(()) +} + +fn find_function<'a>(obj: &'a ObjInfo, name: &str) -> Option<(&'a ObjSection, &'a ObjSymbol)> { + for section in &obj.sections { + if section.kind != ObjSectionKind::Code { + continue; + } + for symbol in §ion.symbols { + if symbol.name == name { + return Some((section, symbol)); + } + } + } + None +} + +fn print_sym( + w: &mut W, + symbol: &ObjSymbol, + sx: u16, + mut sy: u16, + max_sx: u16, + max_sy: u16, + skip: usize, +) -> Result<()> +where + W: Write, +{ + let base_addr = symbol.address as u32; + for ins_diff in symbol.instructions.iter().skip(skip) { + let mut sx = sx; + if ins_diff.kind != ObjInsDiffKind::None && sx > 2 { + crossterm::queue!(w, MoveTo(sx - 2, sy))?; + let s = match ins_diff.kind { + ObjInsDiffKind::Delete => "< ", + ObjInsDiffKind::Insert => "> ", + _ => "| ", + }; + crossterm::queue!(w, PrintStyledContent(s.with(Color::DarkGrey)))?; + } else { + crossterm::queue!(w, MoveTo(sx, sy))?; + } + display_diff(ins_diff, base_addr, |text| { + let mut label_text; + let mut base_color = match ins_diff.kind { + ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => { + Color::Grey + } + ObjInsDiffKind::Replace => Color::DarkCyan, + ObjInsDiffKind::Delete => Color::DarkRed, + ObjInsDiffKind::Insert => Color::DarkGreen, + }; + 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::DarkGrey; + 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::BranchTarget(addr) => { + label_text = format!("{addr:x}"); + } + 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) => { + crossterm::queue!(w, MoveRight(n as u16))?; + sx += n as u16; + return Ok(()); + } + DiffText::Eol => { + sy += 1; + return Ok(()); + } + } + let len = label_text.len(); + if sx >= max_sx { + return Ok(()); + } + label_text.truncate(max_sx as usize - sx as usize); + crossterm::queue!(w, PrintStyledContent(label_text.with(base_color)))?; + sx += len as u16; + if pad_to > len { + let pad = (pad_to - len) as u16; + crossterm::queue!(w, MoveRight(pad))?; + sx += pad; + } + Ok(()) + })?; + if sy >= max_sy { + break; + } + } + Ok(()) +} + +pub const COLOR_ROTATION: [Color; 8] = [ + Color::Magenta, + Color::Cyan, + Color::Green, + Color::Red, + Color::Yellow, + Color::DarkMagenta, + 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::Blue + } else { + Color::Red + } +} diff --git a/objdiff-cli/src/cmd/mod.rs b/objdiff-cli/src/cmd/mod.rs new file mode 100644 index 0000000..406f9dc --- /dev/null +++ b/objdiff-cli/src/cmd/mod.rs @@ -0,0 +1,2 @@ +pub mod diff; +pub mod report; diff --git a/objdiff-cli/src/cmd/report.rs b/objdiff-cli/src/cmd/report.rs new file mode 100644 index 0000000..b3bbcd3 --- /dev/null +++ b/objdiff-cli/src/cmd/report.rs @@ -0,0 +1,229 @@ +use std::{ + collections::HashSet, + fs::File, + io::{BufWriter, Write}, + path::{Path, PathBuf}, + time::Instant, +}; + +use anyhow::{bail, Context, Result}; +use argp::FromArgs; +use objdiff_core::{ + config::ProjectObject, + diff, obj, + obj::{ObjSectionKind, ObjSymbolFlags}, +}; +use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; + +#[derive(FromArgs, PartialEq, Debug)] +/// Generate a report from a project. +#[argp(subcommand, name = "report")] +pub struct Args { + #[argp(option, short = 'p')] + /// Project directory + project: Option, + #[argp(option, short = 'o')] + /// Output JSON file + output: Option, + #[argp(switch, short = 'd')] + /// Deduplicate global and weak symbols + deduplicate: bool, +} + +#[derive(Debug, Clone, Default, serde::Serialize)] +struct Report { + fuzzy_match_percent: f32, + total_size: u64, + matched_size: u64, + matched_size_percent: f32, + total_functions: u32, + matched_functions: u32, + matched_functions_percent: f32, + units: Vec, +} + +#[derive(Debug, Clone, Default, serde::Serialize)] +struct ReportUnit { + name: String, + match_percent: f32, + total_size: u64, + matched_size: u64, + total_functions: u32, + matched_functions: u32, + #[serde(skip_serializing_if = "Option::is_none")] + complete: Option, + functions: Vec, +} + +#[derive(Debug, Clone, Default, serde::Serialize)] +struct ReportFunction { + name: String, + #[serde(skip_serializing_if = "Option::is_none")] + demangled_name: Option, + size: u64, + match_percent: f32, +} + +pub fn run(args: Args) -> Result<()> { + let project_dir = args.project.as_deref().unwrap_or_else(|| Path::new(".")); + log::info!("Loading project {}", project_dir.display()); + + let config = objdiff_core::config::try_project_config(project_dir); + let Some((Ok(mut project), _)) = config else { + bail!("No project configuration found"); + }; + log::info!( + "Generating report for {} units (using {} threads)", + project.objects.len(), + if args.deduplicate { 1 } else { rayon::current_num_threads() } + ); + + let start = Instant::now(); + let mut report = Report::default(); + let mut existing_functions: HashSet = HashSet::new(); + if args.deduplicate { + // If deduplicating, we need to run single-threaded + for object in &mut project.objects { + if let Some(unit) = report_object( + object, + project_dir, + project.target_dir.as_deref(), + project.base_dir.as_deref(), + Some(&mut existing_functions), + )? { + report.units.push(unit); + } + } + } else { + let units = project + .objects + .par_iter_mut() + .map(|object| { + report_object( + object, + project_dir, + project.target_dir.as_deref(), + project.base_dir.as_deref(), + None, + ) + }) + .collect::>>>()?; + report.units = units.into_iter().flatten().collect::>(); + } + for unit in &report.units { + report.fuzzy_match_percent += unit.match_percent * unit.total_size as f32; + report.total_size += unit.total_size; + report.matched_size += unit.matched_size; + report.total_functions += unit.total_functions; + report.matched_functions += unit.matched_functions; + } + if report.total_size == 0 { + report.fuzzy_match_percent = 100.0; + } else { + report.fuzzy_match_percent /= report.total_size as f32; + } + report.matched_size_percent = if report.total_size == 0 { + 100.0 + } else { + report.matched_size as f32 / report.total_size as f32 * 100.0 + }; + report.matched_functions_percent = if report.total_functions == 0 { + 100.0 + } else { + report.matched_functions as f32 / report.total_functions as f32 * 100.0 + }; + let duration = start.elapsed(); + log::info!("Report generated in {}.{:03}s", duration.as_secs(), duration.subsec_millis()); + if let Some(output) = &args.output { + log::info!("Writing to {}", output.display()); + let mut output = BufWriter::new( + File::create(output) + .with_context(|| format!("Failed to create file {}", output.display()))?, + ); + serde_json::to_writer_pretty(&mut output, &report)?; + output.flush()?; + } else { + serde_json::to_writer_pretty(std::io::stdout(), &report)?; + } + Ok(()) +} + +fn report_object( + object: &mut ProjectObject, + project_dir: &Path, + target_dir: Option<&Path>, + base_dir: Option<&Path>, + mut existing_functions: Option<&mut HashSet>, +) -> Result> { + object.resolve_paths(project_dir, target_dir, base_dir); + match (&object.target_path, &object.base_path) { + (None, Some(_)) if object.complete != Some(true) => { + log::warn!("Skipping object without target: {}", object.name()); + return Ok(None); + } + (None, None) => { + log::warn!("Skipping object without target or base: {}", object.name()); + return Ok(None); + } + _ => {} + } + // println!("Checking {}", object.name()); + let mut target = object + .target_path + .as_ref() + .map(|p| obj::elf::read(p).with_context(|| format!("Failed to open {}", p.display()))) + .transpose()?; + let mut base = object + .base_path + .as_ref() + .map(|p| obj::elf::read(p).with_context(|| format!("Failed to open {}", p.display()))) + .transpose()?; + let config = diff::DiffObjConfig { relax_reloc_diffs: true, ..Default::default() }; + diff::diff_objs(&config, target.as_mut(), base.as_mut())?; + let mut unit = ReportUnit { name: object.name().to_string(), ..Default::default() }; + let obj = target.as_ref().or(base.as_ref()).unwrap(); + for section in &obj.sections { + if section.kind != ObjSectionKind::Code { + continue; + } + for symbol in §ion.symbols { + if symbol.size == 0 { + continue; + } + if let Some(existing_functions) = &mut existing_functions { + if (symbol.flags.0.contains(ObjSymbolFlags::Global) + || symbol.flags.0.contains(ObjSymbolFlags::Weak)) + && !existing_functions.insert(symbol.name.clone()) + { + continue; + } + } + let match_percent = symbol.match_percent.unwrap_or(if object.complete == Some(true) { + 100.0 + } else { + 0.0 + }); + unit.match_percent += match_percent * symbol.size as f32; + unit.total_size += symbol.size; + if match_percent == 100.0 { + unit.matched_size += symbol.size; + } + unit.functions.push(ReportFunction { + name: symbol.name.clone(), + demangled_name: symbol.demangled_name.clone(), + size: symbol.size, + match_percent, + }); + if match_percent == 100.0 { + unit.matched_functions += 1; + } + unit.total_functions += 1; + } + } + if unit.total_size == 0 { + unit.match_percent = 100.0; + } else { + unit.match_percent /= unit.total_size as f32; + } + Ok(Some(unit)) +} diff --git a/objdiff-cli/src/main.rs b/objdiff-cli/src/main.rs new file mode 100644 index 0000000..b33b365 --- /dev/null +++ b/objdiff-cli/src/main.rs @@ -0,0 +1,142 @@ +mod argp_version; +mod cmd; +mod util; + +use std::{env, ffi::OsStr, path::PathBuf, str::FromStr}; + +use anyhow::{Error, Result}; +use argp::{FromArgValue, FromArgs}; +use enable_ansi_support::enable_ansi_support; +use supports_color::Stream; +use tracing_subscriber::{filter::LevelFilter, EnvFilter}; + +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +enum LogLevel { + Error, + Warn, + Info, + Debug, + Trace, +} + +impl FromStr for LogLevel { + type Err = (); + + fn from_str(s: &str) -> Result { + Ok(match s { + "error" => Self::Error, + "warn" => Self::Warn, + "info" => Self::Info, + "debug" => Self::Debug, + "trace" => Self::Trace, + _ => return Err(()), + }) + } +} + +impl ToString for LogLevel { + fn to_string(&self) -> String { + match self { + LogLevel::Error => "error", + LogLevel::Warn => "warn", + LogLevel::Info => "info", + LogLevel::Debug => "debug", + LogLevel::Trace => "trace", + } + .to_string() + } +} + +impl FromArgValue for LogLevel { + fn from_arg_value(value: &OsStr) -> Result { + String::from_arg_value(value) + .and_then(|s| Self::from_str(&s).map_err(|_| "Invalid log level".to_string())) + } +} + +#[derive(FromArgs, PartialEq, Debug)] +/// Yet another GameCube/Wii decompilation toolkit. +struct TopLevel { + #[argp(subcommand)] + command: SubCommand, + #[argp(option, short = 'C')] + /// Change working directory. + chdir: Option, + #[argp(option, short = 'L')] + /// Minimum logging level. (Default: info) + /// Possible values: error, warn, info, debug, trace + log_level: Option, + /// Print version information and exit. + #[argp(switch, short = 'V')] + version: bool, + /// Disable color output. (env: NO_COLOR) + #[argp(switch)] + no_color: bool, +} + +#[derive(FromArgs, PartialEq, Debug)] +#[argp(subcommand)] +enum SubCommand { + Diff(cmd::diff::Args), + Report(cmd::report::Args), +} + +// Duplicated from supports-color so we can check early. +fn env_no_color() -> bool { + match env::var("NO_COLOR").as_deref() { + Ok("") | Ok("0") | Err(_) => false, + Ok(_) => true, + } +} + +fn main() { + let args: TopLevel = argp_version::from_env(); + let use_colors = if args.no_color || env_no_color() { + false + } else { + // Try to enable ANSI support on Windows. + let _ = enable_ansi_support(); + // Disable isatty check for supports-color. (e.g. when used with ninja) + env::set_var("IGNORE_IS_TERMINAL", "1"); + supports_color::on(Stream::Stdout).is_some_and(|c| c.has_basic) + }; + + let format = + tracing_subscriber::fmt::format().with_ansi(use_colors).with_target(false).without_time(); + let builder = tracing_subscriber::fmt().event_format(format); + if let Some(level) = args.log_level { + builder + .with_max_level(match level { + LogLevel::Error => LevelFilter::ERROR, + LogLevel::Warn => LevelFilter::WARN, + LogLevel::Info => LevelFilter::INFO, + LogLevel::Debug => LevelFilter::DEBUG, + LogLevel::Trace => LevelFilter::TRACE, + }) + .init(); + } else { + builder + .with_env_filter( + EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(), + ) + .init(); + } + + let mut result = Ok(()); + if let Some(dir) = &args.chdir { + result = env::set_current_dir(dir).map_err(|e| { + Error::new(e) + .context(format!("Failed to change working directory to '{}'", dir.display())) + }); + } + result = result.and_then(|_| match args.command { + SubCommand::Diff(c_args) => cmd::diff::run(c_args), + SubCommand::Report(c_args) => cmd::report::run(c_args), + }); + if let Err(e) = result { + eprintln!("Failed: {e:?}"); + std::process::exit(1); + } +} diff --git a/objdiff-cli/src/util/mod.rs b/objdiff-cli/src/util/mod.rs new file mode 100644 index 0000000..5ff0ecd --- /dev/null +++ b/objdiff-cli/src/util/mod.rs @@ -0,0 +1 @@ +pub mod term; diff --git a/objdiff-cli/src/util/term.rs b/objdiff-cli/src/util/term.rs new file mode 100644 index 0000000..8dc6344 --- /dev/null +++ b/objdiff-cli/src/util/term.rs @@ -0,0 +1,15 @@ +use std::panic; + +pub fn crossterm_panic_handler() { + let original_hook = panic::take_hook(); + panic::set_hook(Box::new(move |panic_info| { + crossterm::execute!( + std::io::stderr(), + crossterm::terminal::LeaveAlternateScreen, + crossterm::event::DisableMouseCapture + ) + .unwrap(); + crossterm::terminal::disable_raw_mode().unwrap(); + original_hook(panic_info); + })); +} diff --git a/objdiff-core/Cargo.toml b/objdiff-core/Cargo.toml index d78eec5..4d03021 100644 --- a/objdiff-core/Cargo.toml +++ b/objdiff-core/Cargo.toml @@ -12,14 +12,15 @@ A local diffing tool for decompilation projects. """ [features] -all = ["dwarf", "mips", "ppc"] +all = ["config", "dwarf", "mips", "ppc"] any-arch = [] # Implicit, used to check if any arch is enabled +config = [] dwarf = ["gimli"] mips = ["any-arch", "rabbitizer"] ppc = ["any-arch", "cwdemangle", "ppc750cl"] [dependencies] -anyhow = "1.0.79" +anyhow = "1.0.80" byteorder = "1.5.0" cwdemangle = { version = "0.1.6", optional = true } filetime = "0.2.23" @@ -34,3 +35,9 @@ rabbitizer = { version = "1.8.1", optional = true } serde = { version = "1", features = ["derive"] } similar = "2.4.0" twox-hash = "1.6.3" + +# config +globset = { version = "0.4.14", features = ["serde1"] } +semver = "1.0.21" +serde_json = "1.0.111" +serde_yaml = "0.9.30" diff --git a/objdiff-core/src/config/mod.rs b/objdiff-core/src/config/mod.rs new file mode 100644 index 0000000..33bd421 --- /dev/null +++ b/objdiff-core/src/config/mod.rs @@ -0,0 +1,148 @@ +use std::{ + fs::File, + io::Read, + path::{Path, PathBuf}, +}; + +use anyhow::Result; +use filetime::FileTime; +use globset::{Glob, GlobSet, GlobSetBuilder}; + +#[inline] +fn bool_true() -> bool { true } + +#[derive(Default, Clone, serde::Deserialize)] +pub struct ProjectConfig { + #[serde(default)] + pub min_version: Option, + #[serde(default)] + pub custom_make: Option, + #[serde(default)] + pub target_dir: Option, + #[serde(default)] + pub base_dir: Option, + #[serde(default = "bool_true")] + pub build_base: bool, + #[serde(default)] + pub build_target: bool, + #[serde(default)] + pub watch_patterns: Option>, + #[serde(default, alias = "units")] + pub objects: Vec, +} + +#[derive(Default, Clone, serde::Deserialize)] +pub struct ProjectObject { + #[serde(default)] + pub name: Option, + #[serde(default)] + pub path: Option, + #[serde(default)] + pub target_path: Option, + #[serde(default)] + pub base_path: Option, + #[serde(default)] + pub reverse_fn_order: Option, + #[serde(default)] + pub complete: Option, + #[serde(default)] + pub scratch: Option, +} + +impl ProjectObject { + pub fn name(&self) -> &str { + if let Some(name) = &self.name { + name + } else if let Some(path) = &self.path { + path.to_str().unwrap_or("[invalid path]") + } else { + "[unknown]" + } + } + + pub fn resolve_paths( + &mut self, + project_dir: &Path, + target_obj_dir: Option<&Path>, + base_obj_dir: Option<&Path>, + ) { + if let (Some(target_obj_dir), Some(path), None) = + (target_obj_dir, &self.path, &self.target_path) + { + self.target_path = Some(target_obj_dir.join(path)); + } else if let Some(path) = &self.target_path { + self.target_path = Some(project_dir.join(path)); + } + if let (Some(base_obj_dir), Some(path), None) = (base_obj_dir, &self.path, &self.base_path) + { + self.base_path = Some(base_obj_dir.join(path)); + } else if let Some(path) = &self.base_path { + self.base_path = Some(project_dir.join(path)); + } + } +} + +#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct ScratchConfig { + #[serde(default)] + pub platform: Option, + #[serde(default)] + pub compiler: Option, + #[serde(default)] + pub c_flags: Option, + #[serde(default)] + pub ctx_path: Option, + #[serde(default)] + pub build_ctx: bool, +} + +pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.yml", "objdiff.yaml", "objdiff.json"]; + +pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[ + "*.c", "*.cp", "*.cpp", "*.cxx", "*.h", "*.hp", "*.hpp", "*.hxx", "*.s", "*.S", "*.asm", + "*.inc", "*.py", "*.yml", "*.txt", "*.json", +]; + +#[derive(Clone, Eq, PartialEq)] +pub struct ProjectConfigInfo { + pub path: PathBuf, + pub timestamp: FileTime, +} + +pub fn try_project_config(dir: &Path) -> Option<(Result, ProjectConfigInfo)> { + for filename in CONFIG_FILENAMES.iter() { + let config_path = dir.join(filename); + let Ok(mut file) = File::open(&config_path) else { + continue; + }; + let metadata = file.metadata(); + if let Ok(metadata) = metadata { + if !metadata.is_file() { + continue; + } + let ts = FileTime::from_last_modification_time(&metadata); + let config = match filename.contains("json") { + true => read_json_config(&mut file), + false => read_yml_config(&mut file), + }; + return Some((config, ProjectConfigInfo { path: config_path, timestamp: ts })); + } + } + None +} + +fn read_yml_config(reader: &mut R) -> Result { + Ok(serde_yaml::from_reader(reader)?) +} + +fn read_json_config(reader: &mut R) -> Result { + Ok(serde_json::from_reader(reader)?) +} + +pub fn build_globset(vec: &[Glob]) -> std::result::Result { + let mut builder = GlobSetBuilder::new(); + for glob in vec { + builder.add(glob.clone()); + } + builder.build() +} diff --git a/objdiff-core/src/diff/display.rs b/objdiff-core/src/diff/display.rs new file mode 100644 index 0000000..25f4b0b --- /dev/null +++ b/objdiff-core/src/diff/display.rs @@ -0,0 +1,175 @@ +use std::cmp::Ordering; + +use anyhow::{bail, Result}; + +use crate::obj::{ + ObjInsArg, ObjInsArgDiff, ObjInsArgValue, ObjInsDiff, ObjReloc, ObjRelocKind, ObjSymbol, +}; + +#[derive(Debug, Clone)] +pub enum DiffText<'a> { + /// Basic text + Basic(&'a str), + /// Colored text + BasicColor(&'a str, usize), + /// Line number + Line(usize), + /// Instruction address + Address(u32), + /// Instruction mnemonic + Opcode(&'a str, u8), + /// Instruction argument + Argument(&'a ObjInsArgValue, Option<&'a ObjInsArgDiff>), + /// Branch target + BranchTarget(u32), + /// Symbol name + Symbol(&'a ObjSymbol), + /// Number of spaces + Spacing(usize), + /// End of line + Eol, +} + +pub fn display_diff( + ins_diff: &ObjInsDiff, + base_addr: u32, + mut cb: impl FnMut(DiffText) -> Result<()>, +) -> Result<()> { + let Some(ins) = &ins_diff.ins else { + cb(DiffText::Eol)?; + return Ok(()); + }; + if let Some(line) = ins.line { + cb(DiffText::Line(line as usize))?; + } + cb(DiffText::Address(ins.address - base_addr))?; + if let Some(branch) = &ins_diff.branch_from { + cb(DiffText::BasicColor(" ~> ", branch.branch_idx))?; + } else { + cb(DiffText::Spacing(4))?; + } + cb(DiffText::Opcode(&ins.mnemonic, ins.op))?; + let mut writing_offset = false; + for (i, arg) in ins.args.iter().enumerate() { + if i == 0 { + cb(DiffText::Spacing(1))?; + } + if i > 0 && !writing_offset { + cb(DiffText::Basic(", "))?; + } + let mut new_writing_offset = false; + match arg { + ObjInsArg::Arg(v) => { + let diff = ins_diff.arg_diff.get(i).and_then(|o| o.as_ref()); + cb(DiffText::Argument(v, diff))?; + } + ObjInsArg::ArgWithBase(v) => { + let diff = ins_diff.arg_diff.get(i).and_then(|o| o.as_ref()); + cb(DiffText::Argument(v, diff))?; + cb(DiffText::Basic("("))?; + new_writing_offset = true; + } + ObjInsArg::Reloc => { + display_reloc(ins.reloc.as_ref().unwrap(), &mut cb)?; + } + ObjInsArg::RelocWithBase => { + display_reloc(ins.reloc.as_ref().unwrap(), &mut cb)?; + cb(DiffText::Basic("("))?; + new_writing_offset = true; + } + ObjInsArg::BranchOffset(offset) => { + let addr = offset + ins.address as i32 - base_addr as i32; + cb(DiffText::BranchTarget(addr as u32))?; + } + } + if writing_offset { + cb(DiffText::Basic(")"))?; + } + writing_offset = new_writing_offset; + } + if let Some(branch) = &ins_diff.branch_to { + cb(DiffText::BasicColor(" ~>", branch.branch_idx))?; + } + cb(DiffText::Eol)?; + Ok(()) +} + +fn display_reloc_name(reloc: &ObjReloc, mut cb: impl FnMut(DiffText) -> Result<()>) -> Result<()> { + cb(DiffText::Symbol(&reloc.target))?; + match reloc.target.addend.cmp(&0i64) { + Ordering::Greater => cb(DiffText::Basic(&format!("+{:#X}", reloc.target.addend))), + Ordering::Less => cb(DiffText::Basic(&format!("-{:#X}", -reloc.target.addend))), + _ => Ok(()), + } +} + +fn display_reloc(reloc: &ObjReloc, mut cb: impl FnMut(DiffText) -> Result<()>) -> Result<()> { + match reloc.kind { + #[cfg(feature = "ppc")] + ObjRelocKind::PpcAddr16Lo => { + display_reloc_name(reloc, &mut cb)?; + cb(DiffText::Basic("@l"))?; + } + #[cfg(feature = "ppc")] + ObjRelocKind::PpcAddr16Hi => { + display_reloc_name(reloc, &mut cb)?; + cb(DiffText::Basic("@h"))?; + } + #[cfg(feature = "ppc")] + ObjRelocKind::PpcAddr16Ha => { + display_reloc_name(reloc, &mut cb)?; + cb(DiffText::Basic("@ha"))?; + } + #[cfg(feature = "ppc")] + ObjRelocKind::PpcEmbSda21 => { + display_reloc_name(reloc, &mut cb)?; + cb(DiffText::Basic("@sda21"))?; + } + #[cfg(feature = "ppc")] + ObjRelocKind::PpcRel24 | ObjRelocKind::PpcRel14 => { + display_reloc_name(reloc, &mut cb)?; + } + #[cfg(feature = "mips")] + ObjRelocKind::MipsHi16 => { + cb(DiffText::Basic("%hi("))?; + display_reloc_name(reloc, &mut cb)?; + cb(DiffText::Basic(")"))?; + } + #[cfg(feature = "mips")] + ObjRelocKind::MipsLo16 => { + cb(DiffText::Basic("%lo("))?; + display_reloc_name(reloc, &mut cb)?; + cb(DiffText::Basic(")"))?; + } + #[cfg(feature = "mips")] + ObjRelocKind::MipsGot16 => { + cb(DiffText::Basic("%got("))?; + display_reloc_name(reloc, &mut cb)?; + cb(DiffText::Basic(")"))?; + } + #[cfg(feature = "mips")] + ObjRelocKind::MipsCall16 => { + cb(DiffText::Basic("%call16("))?; + display_reloc_name(reloc, &mut cb)?; + cb(DiffText::Basic(")"))?; + } + #[cfg(feature = "mips")] + ObjRelocKind::MipsGpRel16 => { + cb(DiffText::Basic("%gp_rel("))?; + display_reloc_name(reloc, &mut cb)?; + cb(DiffText::Basic(")"))?; + } + #[cfg(feature = "mips")] + ObjRelocKind::Mips26 => { + display_reloc_name(reloc, &mut cb)?; + } + #[cfg(feature = "mips")] + ObjRelocKind::MipsGpRel32 => { + bail!("unimplemented: mips gp_rel32"); + } + ObjRelocKind::Absolute => { + cb(DiffText::Basic("[INVALID]"))?; + } + } + Ok(()) +} diff --git a/objdiff-core/src/diff/mod.rs b/objdiff-core/src/diff/mod.rs index 1334f74..c3e7686 100644 --- a/objdiff-core/src/diff/mod.rs +++ b/objdiff-core/src/diff/mod.rs @@ -1,5 +1,6 @@ pub mod code; pub mod data; +pub mod display; pub mod editops; use anyhow::Result; @@ -22,6 +23,7 @@ pub enum DiffAlg { Lcs, } +#[derive(Debug, Clone, Default, Eq, PartialEq)] pub struct DiffObjConfig { pub code_alg: DiffAlg, pub data_alg: DiffAlg, diff --git a/objdiff-core/src/lib.rs b/objdiff-core/src/lib.rs index a27b944..1374b74 100644 --- a/objdiff-core/src/lib.rs +++ b/objdiff-core/src/lib.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "config")] +pub mod config; pub mod diff; pub mod obj; pub mod util; diff --git a/objdiff-core/src/obj/ppc.rs b/objdiff-core/src/obj/ppc.rs index 10f82f4..381f801 100644 --- a/objdiff-core/src/obj/ppc.rs +++ b/objdiff-core/src/obj/ppc.rs @@ -78,13 +78,17 @@ pub fn process_code( ObjRelocKind::PpcAddr16Hi | ObjRelocKind::PpcAddr16Ha | ObjRelocKind::PpcAddr16Lo => { - let arg = args.iter_mut().rfind(|a| is_rel_abs_arg(a)).ok_or_else(|| { - anyhow::Error::msg("Failed to locate rel/abs arg for reloc") - })?; - *arg = if is_offset_arg(arg) { - ObjInsArg::RelocWithBase - } else { - ObjInsArg::Reloc + match args.iter_mut().rfind(|a| is_rel_abs_arg(a)) { + Some(arg) => { + *arg = if is_offset_arg(arg) { + ObjInsArg::RelocWithBase + } else { + ObjInsArg::Reloc + }; + } + None => { + log::warn!("Failed to locate rel/abs arg for reloc"); + } }; } _ => {} diff --git a/objdiff-gui/Cargo.toml b/objdiff-gui/Cargo.toml index a942f13..11c88ed 100644 --- a/objdiff-gui/Cargo.toml +++ b/objdiff-gui/Cargo.toml @@ -19,7 +19,7 @@ wgpu = ["eframe/wgpu"] wsl = [] [dependencies] -anyhow = "1.0.79" +anyhow = "1.0.80" bytes = "1.5.0" cfg-if = "1.0.0" const_format = "0.2.32" diff --git a/objdiff-gui/src/app.rs b/objdiff-gui/src/app.rs index 559579c..933552e 100644 --- a/objdiff-gui/src/app.rs +++ b/objdiff-gui/src/app.rs @@ -12,12 +12,17 @@ use std::{ use filetime::FileTime; use globset::{Glob, GlobSet}; use notify::{RecursiveMode, Watcher}; -use objdiff_core::diff::DiffAlg; +use objdiff_core::{ + config::{ + build_globset, ProjectConfigInfo, ProjectObject, ScratchConfig, DEFAULT_WATCH_PATTERNS, + }, + diff::DiffAlg, +}; use time::UtcOffset; use crate::{ app_config::{deserialize_config, AppConfigVersion}, - config::{build_globset, load_project_config, ProjectObject, ProjectObjectNode, ScratchConfig}, + config::{load_project_config, ProjectObjectNode}, jobs::{ objdiff::{start_build, ObjDiffConfig}, Job, JobQueue, JobResult, JobStatus, @@ -26,7 +31,6 @@ use crate::{ appearance::{appearance_window, Appearance}, config::{ config_ui, diff_options_window, project_window, ConfigViewState, CONFIG_DISABLED_TEXT, - DEFAULT_WATCH_PATTERNS, }, data_diff::data_diff_ui, debug::debug_window, @@ -63,12 +67,6 @@ pub struct ObjectConfig { pub scratch: Option, } -#[derive(Clone, Eq, PartialEq)] -pub struct ProjectConfigInfo { - pub path: PathBuf, - pub timestamp: FileTime, -} - #[inline] fn bool_true() -> bool { true } diff --git a/objdiff-gui/src/config.rs b/objdiff-gui/src/config.rs index 6e90aae..c1ef7d0 100644 --- a/objdiff-gui/src/config.rs +++ b/objdiff-gui/src/config.rs @@ -1,84 +1,10 @@ -use std::{ - fs::File, - io::Read, - path::{Component, Path, PathBuf}, -}; +use std::path::{Component, Path}; use anyhow::{ensure, Result}; -use filetime::FileTime; -use globset::{Glob, GlobSet, GlobSetBuilder}; +use globset::Glob; +use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS}; -use crate::{ - app::{AppConfig, ProjectConfigInfo}, - views::config::DEFAULT_WATCH_PATTERNS, -}; - -#[inline] -fn bool_true() -> bool { true } - -#[derive(Default, Clone, serde::Deserialize)] -pub struct ProjectConfig { - #[serde(default)] - pub min_version: Option, - #[serde(default)] - pub custom_make: Option, - #[serde(default)] - pub target_dir: Option, - #[serde(default)] - pub base_dir: Option, - #[serde(default = "bool_true")] - pub build_base: bool, - #[serde(default)] - pub build_target: bool, - #[serde(default)] - pub watch_patterns: Option>, - #[serde(default, alias = "units")] - pub objects: Vec, -} - -#[derive(Default, Clone, serde::Deserialize)] -pub struct ProjectObject { - #[serde(default)] - pub name: Option, - #[serde(default)] - pub path: Option, - #[serde(default)] - pub target_path: Option, - #[serde(default)] - pub base_path: Option, - #[serde(default)] - pub reverse_fn_order: Option, - #[serde(default)] - pub complete: Option, - #[serde(default)] - pub scratch: Option, -} - -#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct ScratchConfig { - #[serde(default)] - pub platform: Option, - #[serde(default)] - pub compiler: Option, - #[serde(default)] - pub c_flags: Option, - #[serde(default)] - pub ctx_path: Option, - #[serde(default)] - pub build_ctx: bool, -} - -impl ProjectObject { - pub fn name(&self) -> &str { - if let Some(name) = &self.name { - name - } else if let Some(path) = &self.path { - path.to_str().unwrap_or("[invalid path]") - } else { - "[unknown]" - } - } -} +use crate::app::AppConfig; #[derive(Clone)] pub enum ProjectObjectNode { @@ -109,8 +35,8 @@ fn find_dir<'a>( fn build_nodes( objects: &[ProjectObject], project_dir: &Path, - target_obj_dir: &Option, - base_obj_dir: &Option, + target_obj_dir: Option<&Path>, + base_obj_dir: Option<&Path>, ) -> Vec { let mut nodes = vec![]; for object in objects { @@ -131,28 +57,13 @@ fn build_nodes( } } let mut object = Box::new(object.clone()); - if let (Some(target_obj_dir), Some(path), None) = - (target_obj_dir, &object.path, &object.target_path) - { - object.target_path = Some(target_obj_dir.join(path)); - } else if let Some(path) = &object.target_path { - object.target_path = Some(project_dir.join(path)); - } - if let (Some(base_obj_dir), Some(path), None) = - (base_obj_dir, &object.path, &object.base_path) - { - object.base_path = Some(base_obj_dir.join(path)); - } else if let Some(path) = &object.base_path { - object.base_path = Some(project_dir.join(path)); - } + object.resolve_paths(project_dir, target_obj_dir, base_obj_dir); let filename = path.file_name().unwrap().to_str().unwrap().to_string(); out_nodes.push(ProjectObjectNode::File(filename, object)); } nodes } -pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.yml", "objdiff.yaml", "objdiff.json"]; - pub fn load_project_config(config: &mut AppConfig) -> Result<()> { let Some(project_dir) = &config.project_dir else { return Ok(()); @@ -178,47 +89,13 @@ pub fn load_project_config(config: &mut AppConfig) -> Result<()> { }); config.watcher_change = true; config.objects = project_config.objects; - config.object_nodes = - build_nodes(&config.objects, project_dir, &config.target_obj_dir, &config.base_obj_dir); + config.object_nodes = build_nodes( + &config.objects, + project_dir, + config.target_obj_dir.as_deref(), + config.base_obj_dir.as_deref(), + ); config.project_config_info = Some(info); } Ok(()) } - -fn try_project_config(dir: &Path) -> Option<(Result, ProjectConfigInfo)> { - for filename in CONFIG_FILENAMES.iter() { - let config_path = dir.join(filename); - let Ok(mut file) = File::open(&config_path) else { - continue; - }; - let metadata = file.metadata(); - if let Ok(metadata) = metadata { - if !metadata.is_file() { - continue; - } - let ts = FileTime::from_last_modification_time(&metadata); - let config = match filename.contains("json") { - true => read_json_config(&mut file), - false => read_yml_config(&mut file), - }; - return Some((config, ProjectConfigInfo { path: config_path, timestamp: ts })); - } - } - None -} - -fn read_yml_config(reader: &mut R) -> Result { - Ok(serde_yaml::from_reader(reader)?) -} - -fn read_json_config(reader: &mut R) -> Result { - Ok(serde_json::from_reader(reader)?) -} - -pub fn build_globset(vec: &[Glob]) -> std::result::Result { - let mut builder = GlobSetBuilder::new(); - for glob in vec { - builder.add(glob.clone()); - } - builder.build() -} diff --git a/objdiff-gui/src/views/appearance.rs b/objdiff-gui/src/views/appearance.rs index c7f8623..11ec978 100644 --- a/objdiff-gui/src/views/appearance.rs +++ b/objdiff-gui/src/views/appearance.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use egui::{text::LayoutJob, Color32, FontFamily, FontId, TextStyle, Widget}; +use egui::{text::LayoutJob, Color32, FontFamily, FontId, TextFormat, TextStyle, Widget}; use time::UtcOffset; use crate::fonts::load_font_if_needed; @@ -185,6 +185,15 @@ impl Appearance { } } } + + pub fn code_text_format(&self, base_color: Color32, highlight: bool) -> TextFormat { + TextFormat { + font_id: self.code_font.clone(), + color: if highlight { self.emphasized_text_color } else { base_color }, + background: if highlight { self.deemphasized_text_color } else { Color32::TRANSPARENT }, + ..Default::default() + } + } } pub const DEFAULT_COLOR_ROTATION: [Color32; 9] = [ diff --git a/objdiff-gui/src/views/config.rs b/objdiff-gui/src/views/config.rs index c1dc5da..4b7e506 100644 --- a/objdiff-gui/src/views/config.rs +++ b/objdiff-gui/src/views/config.rs @@ -14,12 +14,15 @@ use egui::{ SelectableLabel, TextFormat, Widget, WidgetText, }; use globset::Glob; -use objdiff_core::diff::DiffAlg; +use objdiff_core::{ + config::{ProjectObject, DEFAULT_WATCH_PATTERNS}, + diff::DiffAlg, +}; use self_update::cargo_crate_version; use crate::{ app::{AppConfig, AppConfigRef, ObjectConfig}, - config::{ProjectObject, ProjectObjectNode}, + config::ProjectObjectNode, jobs::{ check_update::{start_check_update, CheckUpdateResult}, update::start_update, @@ -131,11 +134,6 @@ impl ConfigViewState { } } -pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[ - "*.c", "*.cp", "*.cpp", "*.cxx", "*.h", "*.hp", "*.hpp", "*.hxx", "*.s", "*.S", "*.asm", - "*.inc", "*.py", "*.yml", "*.txt", "*.json", -]; - #[cfg(all(windows, feature = "wsl"))] fn process_utf16(bytes: &[u8]) -> Result { let u16_bytes: Vec = bytes diff --git a/objdiff-gui/src/views/function_diff.rs b/objdiff-gui/src/views/function_diff.rs index ffd812e..0830419 100644 --- a/objdiff-gui/src/views/function_diff.rs +++ b/objdiff-gui/src/views/function_diff.rs @@ -1,22 +1,16 @@ -use std::{ - cmp::{max, Ordering}, - default::Default, -}; +use std::default::Default; -use egui::{ - text::LayoutJob, Align, Color32, Label, Layout, RichText, Sense, TextFormat, Vec2, Widget, -}; +use egui::{text::LayoutJob, Align, Label, Layout, Sense, Vec2, Widget}; use egui_extras::{Column, TableBuilder, TableRow}; -use objdiff_core::obj::{ - ObjInfo, ObjIns, ObjInsArg, ObjInsArgDiff, ObjInsArgValue, ObjInsDiff, ObjInsDiffKind, - ObjReloc, ObjRelocKind, ObjSymbol, +use objdiff_core::{ + diff::display::{display_diff, DiffText}, + obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjInsDiff, ObjInsDiffKind, ObjSymbol}, }; use time::format_description; use crate::views::{ appearance::Appearance, symbol_diff::{match_color_for_symbol, DiffViewState, SymbolReference, View}, - write_text, }; #[derive(Default)] @@ -24,252 +18,29 @@ pub enum HighlightKind { #[default] None, Opcode(u8), - Arg(ObjInsArg), + Arg(ObjInsArgValue), Symbol(String), Address(u32), } +impl PartialEq for HighlightKind { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (HighlightKind::None, HighlightKind::None) => false, + (HighlightKind::Opcode(a), HighlightKind::Opcode(b)) => a == b, + (HighlightKind::Arg(a), HighlightKind::Arg(b)) => a.loose_eq(b), + (HighlightKind::Symbol(a), HighlightKind::Symbol(b)) => a == b, + (HighlightKind::Address(a), HighlightKind::Address(b)) => a == b, + _ => false, + } + } +} + #[derive(Default)] pub struct FunctionViewState { pub highlight: HighlightKind, } -fn write_reloc_name( - reloc: &ObjReloc, - color: Color32, - background_color: Color32, - job: &mut LayoutJob, - appearance: &Appearance, -) { - let name = reloc.target.demangled_name.as_ref().unwrap_or(&reloc.target.name); - job.append(name, 0.0, TextFormat { - font_id: appearance.code_font.clone(), - color: appearance.emphasized_text_color, - background: background_color, - ..Default::default() - }); - match reloc.target.addend.cmp(&0i64) { - Ordering::Greater => write_text( - &format!("+{:#X}", reloc.target.addend), - color, - job, - appearance.code_font.clone(), - ), - Ordering::Less => { - write_text( - &format!("-{:#X}", -reloc.target.addend), - color, - job, - appearance.code_font.clone(), - ); - } - _ => {} - } -} - -fn write_reloc( - reloc: &ObjReloc, - color: Color32, - background_color: Color32, - job: &mut LayoutJob, - appearance: &Appearance, -) { - match reloc.kind { - ObjRelocKind::PpcAddr16Lo => { - write_reloc_name(reloc, color, background_color, job, appearance); - write_text("@l", color, job, appearance.code_font.clone()); - } - ObjRelocKind::PpcAddr16Hi => { - write_reloc_name(reloc, color, background_color, job, appearance); - write_text("@h", color, job, appearance.code_font.clone()); - } - ObjRelocKind::PpcAddr16Ha => { - write_reloc_name(reloc, color, background_color, job, appearance); - write_text("@ha", color, job, appearance.code_font.clone()); - } - ObjRelocKind::PpcEmbSda21 => { - write_reloc_name(reloc, color, background_color, job, appearance); - write_text("@sda21", color, job, appearance.code_font.clone()); - } - ObjRelocKind::MipsHi16 => { - write_text("%hi(", color, job, appearance.code_font.clone()); - write_reloc_name(reloc, color, background_color, job, appearance); - write_text(")", color, job, appearance.code_font.clone()); - } - ObjRelocKind::MipsLo16 => { - write_text("%lo(", color, job, appearance.code_font.clone()); - write_reloc_name(reloc, color, background_color, job, appearance); - write_text(")", color, job, appearance.code_font.clone()); - } - ObjRelocKind::MipsGot16 => { - write_text("%got(", color, job, appearance.code_font.clone()); - write_reloc_name(reloc, color, background_color, job, appearance); - write_text(")", color, job, appearance.code_font.clone()); - } - ObjRelocKind::MipsCall16 => { - write_text("%call16(", color, job, appearance.code_font.clone()); - write_reloc_name(reloc, color, background_color, job, appearance); - write_text(")", color, job, appearance.code_font.clone()); - } - ObjRelocKind::MipsGpRel16 => { - write_text("%gp_rel(", color, job, appearance.code_font.clone()); - write_reloc_name(reloc, color, background_color, job, appearance); - write_text(")", color, job, appearance.code_font.clone()); - } - ObjRelocKind::PpcRel24 | ObjRelocKind::PpcRel14 | ObjRelocKind::Mips26 => { - write_reloc_name(reloc, color, background_color, job, appearance); - } - ObjRelocKind::Absolute | ObjRelocKind::MipsGpRel32 => { - write_text("[INVALID]", color, job, appearance.code_font.clone()); - } - }; -} - -fn write_ins( - ins: &ObjIns, - diff_kind: &ObjInsDiffKind, - args: &[Option], - base_addr: u32, - ui: &mut egui::Ui, - appearance: &Appearance, - ins_view_state: &mut FunctionViewState, -) { - let base_color = match diff_kind { - ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => { - appearance.text_color - } - ObjInsDiffKind::Replace => appearance.replace_color, - ObjInsDiffKind::Delete => appearance.delete_color, - ObjInsDiffKind::Insert => appearance.insert_color, - }; - - let highlighted_op = - matches!(ins_view_state.highlight, HighlightKind::Opcode(op) if op == ins.op); - let op_label = RichText::new(ins.mnemonic.clone()) - .font(appearance.code_font.clone()) - .color(if highlighted_op { - appearance.emphasized_text_color - } else { - match diff_kind { - ObjInsDiffKind::OpMismatch => appearance.replace_color, - _ => base_color, - } - }) - .background_color(if highlighted_op { - appearance.deemphasized_text_color - } else { - Color32::TRANSPARENT - }); - let response = Label::new(op_label).sense(Sense::click()).ui(ui); - response.context_menu(|ui| ins_context_menu(ui, ins)); - if response.clicked() { - if highlighted_op { - ins_view_state.highlight = HighlightKind::None; - } else { - ins_view_state.highlight = HighlightKind::Opcode(ins.op); - } - } - let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' ')); - ui.add_space(space_width * (max(11, ins.mnemonic.len()) - ins.mnemonic.len()) as f32); - - let mut writing_offset = false; - for (i, arg) in ins.args.iter().enumerate() { - let mut job = LayoutJob::default(); - if i == 0 { - write_text(" ", base_color, &mut job, appearance.code_font.clone()); - } - if i > 0 && !writing_offset { - write_text(", ", base_color, &mut job, appearance.code_font.clone()); - } - let highlighted_arg = match &ins_view_state.highlight { - HighlightKind::Symbol(v) => { - matches!(arg, ObjInsArg::Reloc | ObjInsArg::RelocWithBase) - && matches!(&ins.reloc, Some(reloc) if &reloc.target.name == v) - } - HighlightKind::Address(v) => { - matches!(arg, ObjInsArg::BranchOffset(offset) if (offset + ins.address as i32 - base_addr as i32) as u32 == *v) - } - HighlightKind::Arg(v) => v.loose_eq(arg), - _ => false, - }; - let color = if highlighted_arg { - appearance.emphasized_text_color - } else if let Some(diff) = args.get(i).and_then(|a| a.as_ref()) { - appearance.diff_colors[diff.idx % appearance.diff_colors.len()] - } else { - base_color - }; - let text_format = TextFormat { - font_id: appearance.code_font.clone(), - color, - background: if highlighted_arg { - appearance.deemphasized_text_color - } else { - Color32::TRANSPARENT - }, - ..Default::default() - }; - let mut new_writing_offset = false; - match arg { - ObjInsArg::Arg(arg) => { - job.append(&arg.to_string(), 0.0, text_format); - } - ObjInsArg::ArgWithBase(arg) => { - job.append(&arg.to_string(), 0.0, text_format); - write_text("(", base_color, &mut job, appearance.code_font.clone()); - new_writing_offset = true; - } - ObjInsArg::Reloc => { - write_reloc( - ins.reloc.as_ref().unwrap(), - base_color, - text_format.background, - &mut job, - appearance, - ); - } - ObjInsArg::RelocWithBase => { - write_reloc( - ins.reloc.as_ref().unwrap(), - base_color, - text_format.background, - &mut job, - appearance, - ); - write_text("(", base_color, &mut job, appearance.code_font.clone()); - new_writing_offset = true; - } - ObjInsArg::BranchOffset(offset) => { - let addr = offset + ins.address as i32 - base_addr as i32; - job.append(&format!("{addr:x}"), 0.0, text_format); - } - } - if writing_offset { - write_text(")", base_color, &mut job, appearance.code_font.clone()); - } - // For text selection / copy - if i == ins.args.len() - 1 { - write_text("\n", base_color, &mut job, appearance.code_font.clone()); - } - writing_offset = new_writing_offset; - let response = Label::new(job).sense(Sense::click()).ui(ui); - response.context_menu(|ui| ins_context_menu(ui, ins)); - if response.clicked() { - if highlighted_arg { - ins_view_state.highlight = HighlightKind::None; - } else if matches!(arg, ObjInsArg::Reloc | ObjInsArg::RelocWithBase) { - ins_view_state.highlight = - HighlightKind::Symbol(ins.reloc.as_ref().unwrap().target.name.clone()); - } else if let ObjInsArg::BranchOffset(offset) = arg { - ins_view_state.highlight = - HighlightKind::Address((offset + ins.address as i32 - base_addr as i32) as u32); - } else { - ins_view_state.highlight = HighlightKind::Arg(arg.clone()); - } - } - } -} - fn ins_hover_ui(ui: &mut egui::Ui, ins: &ObjIns, appearance: &Appearance) { ui.scope(|ui| { ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); @@ -370,6 +141,98 @@ fn find_symbol<'a>(obj: &'a ObjInfo, selected_symbol: &SymbolReference) -> Optio }) } +fn diff_text_ui( + ui: &mut egui::Ui, + text: DiffText<'_>, + ins_diff: &ObjInsDiff, + appearance: &Appearance, + ins_view_state: &mut FunctionViewState, + space_width: f32, +) { + let label_text; + let mut base_color = match ins_diff.kind { + ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => { + appearance.text_color + } + ObjInsDiffKind::Replace => appearance.replace_color, + ObjInsDiffKind::Delete => appearance.delete_color, + ObjInsDiffKind::Insert => appearance.insert_color, + }; + let mut pad_to = 0; + let mut highlight_kind = HighlightKind::None; + match text { + DiffText::Basic(text) => { + label_text = text.to_string(); + } + DiffText::BasicColor(s, idx) => { + label_text = s.to_string(); + base_color = appearance.diff_colors[idx % appearance.diff_colors.len()]; + } + DiffText::Line(num) => { + label_text = num.to_string(); + base_color = appearance.deemphasized_text_color; + pad_to = 5; + } + DiffText::Address(addr) => { + label_text = format!("{:x}:", addr); + pad_to = 5; + highlight_kind = HighlightKind::Address(addr); + } + DiffText::Opcode(mnemonic, op) => { + label_text = mnemonic.to_string(); + if ins_diff.kind == ObjInsDiffKind::OpMismatch { + base_color = appearance.replace_color; + } + pad_to = 8; + highlight_kind = HighlightKind::Opcode(op); + } + DiffText::Argument(arg, diff) => { + label_text = arg.to_string(); + if let Some(diff) = diff { + base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()] + } + highlight_kind = HighlightKind::Arg(arg.clone()); + } + DiffText::BranchTarget(addr) => { + label_text = format!("{addr:x}"); + highlight_kind = HighlightKind::Address(addr); + } + DiffText::Symbol(sym) => { + let name = sym.demangled_name.as_ref().unwrap_or(&sym.name); + label_text = name.clone(); + base_color = appearance.emphasized_text_color; + highlight_kind = HighlightKind::Symbol(name.clone()); + } + DiffText::Spacing(n) => { + ui.add_space(n as f32 * space_width); + return; + } + DiffText::Eol => { + label_text = "\n".to_string(); + } + } + + let len = label_text.len(); + let highlight = ins_view_state.highlight == highlight_kind; + let response = Label::new(LayoutJob::single_section( + label_text, + appearance.code_text_format(base_color, highlight), + )) + .sense(Sense::click()) + .ui(ui); + response.context_menu(|ui| ins_context_menu(ui, ins_diff.ins.as_ref().unwrap())); + if response.clicked() { + if highlight { + ins_view_state.highlight = HighlightKind::None; + } else { + ins_view_state.highlight = highlight_kind; + } + } + if len < pad_to { + ui.add_space((pad_to - len) as f32 * space_width); + } +} + fn asm_row_ui( ui: &mut egui::Ui, ins_diff: &ObjInsDiff, @@ -381,91 +244,12 @@ fn asm_row_ui( if ins_diff.kind != ObjInsDiffKind::None { ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color); } - let mut job = LayoutJob::default(); - let Some(ins) = &ins_diff.ins else { - ui.label(""); - return; - }; - - let base_color = match ins_diff.kind { - ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => { - appearance.text_color - } - ObjInsDiffKind::Replace => appearance.replace_color, - ObjInsDiffKind::Delete => appearance.delete_color, - ObjInsDiffKind::Insert => appearance.insert_color, - }; - let mut pad = 6; - if let Some(line) = ins.line { - let line_str = format!("{line} "); - write_text( - &line_str, - appearance.deemphasized_text_color, - &mut job, - appearance.code_font.clone(), - ); - pad = 12 - line_str.len(); - } - let base_addr = symbol.address as u32; - let addr_highlight = matches!( - &ins_view_state.highlight, - HighlightKind::Address(v) if *v == (ins.address - base_addr) - ); - let addr_string = format!("{:x}", ins.address - symbol.address as u32); - pad -= addr_string.len(); - job.append(&addr_string, 0.0, TextFormat { - font_id: appearance.code_font.clone(), - color: if addr_highlight { appearance.emphasized_text_color } else { base_color }, - background: if addr_highlight { - appearance.deemphasized_text_color - } else { - Color32::TRANSPARENT - }, - ..Default::default() - }); - let response = Label::new(job).sense(Sense::click()).selectable(false).ui(ui); - response.context_menu(|ui| ins_context_menu(ui, ins)); - if response.clicked() { - if addr_highlight { - ins_view_state.highlight = HighlightKind::None; - } else { - ins_view_state.highlight = HighlightKind::Address(ins.address - base_addr); - } - } - - let mut job = LayoutJob::default(); let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' ')); - let spacing = space_width * pad as f32; - job.append(": ", 0.0, TextFormat { - font_id: appearance.code_font.clone(), - color: base_color, - ..Default::default() - }); - if let Some(branch) = &ins_diff.branch_from { - job.append("~> ", spacing, TextFormat { - font_id: appearance.code_font.clone(), - color: appearance.diff_colors[branch.branch_idx % appearance.diff_colors.len()], - ..Default::default() - }); - } else { - job.append(" ", spacing, TextFormat { - font_id: appearance.code_font.clone(), - color: base_color, - ..Default::default() - }); - } - Label::new(job).selectable(false).ui(ui); - write_ins(ins, &ins_diff.kind, &ins_diff.arg_diff, base_addr, ui, appearance, ins_view_state); - if let Some(branch) = &ins_diff.branch_to { - let mut job = LayoutJob::default(); - write_text( - " ~>", - appearance.diff_colors[branch.branch_idx % appearance.diff_colors.len()], - &mut job, - appearance.code_font.clone(), - ); - Label::new(job).selectable(false).ui(ui); - } + display_diff(ins_diff, symbol.address as u32, |text| { + diff_text_ui(ui, text, ins_diff, appearance, ins_view_state, space_width); + Ok(()) + }) + .unwrap(); } fn asm_col_ui( @@ -480,7 +264,6 @@ fn asm_col_ui( }); if let Some(ins) = &ins_diff.ins { response.on_hover_ui_at_pointer(|ui| ins_hover_ui(ui, ins, appearance)); - // .context_menu(|ui| ins_context_menu(ui, ins)); } }