Compare commits

...

52 Commits
v0.6.0 ... main

Author SHA1 Message Date
cadmic 8b36fa4fc6
Fix size of .note.split section (#61)
* Fix size of .note.split section

* clippy fix

---------

Co-authored-by: Luke Street <luke@street.dev>
2024-05-16 18:29:24 -06:00
Luke Street 660e6c879e Update README.md 2024-05-15 19:01:55 -06:00
Aetias db726a68a6
Strip distro root prefix (#58) 2024-05-15 18:56:08 -06:00
Aetias b457453639
Add custom make args (#59) 2024-05-15 18:53:14 -06:00
Luke Street 3e5008524e cargo fmt & cargo deny fix 2024-04-30 20:45:45 -06:00
Luke Street 2c46286aff Update all dependencies & use ppc750cl InsIter 2024-04-30 20:06:04 -06:00
Luke Street 106652ae7d Fix PPC branch display; update README.md 2024-03-22 23:06:41 -06:00
Luke Street 30d14870ef Update ppc750cl, add Itanium demangler & cleanup 2024-03-21 21:36:50 -06:00
Luke Street e7991cb28d cargo fmt 2024-03-18 22:56:57 -06:00
Luke Street 4dfc28fc68 Diff cleanup & fixes 2024-03-18 22:56:13 -06:00
Luke Street 3c74b89f15 Restructure diffing code & initial 3-way diffing (WIP) 2024-03-18 18:10:18 -06:00
Luke Street 1343f4fd2b cargo fmt 2024-03-17 12:20:25 -06:00
Luke Street 9df98f263e Move all architecture-specific code into modules
No more scattered relocation handling and
feature checks. Everything will go through
the ObjArch trait, which makes it easier
to add new architectures going forward.
2024-03-17 12:16:47 -06:00
Luke Street bbe49eb8b4 Initial x86 support
Includes a bit of work to make adding new
architectures easier in the future
2024-03-16 23:30:27 -06:00
Luke Street aecb078b2a ci: Update sccache-action version 2024-03-13 18:34:28 -06:00
Luke Street a5668b484b Update all dependencies 2024-03-13 18:20:46 -06:00
Luke Street ef41e393d4 Resolve dependency advisories 2024-03-04 18:19:08 -07:00
Luke Street 20e42a499a Rework .splitmeta, now .note.split
Uses actual ELF .note format, which is
more standard and handled better by mwld.
2024-03-04 18:06:21 -07:00
Luke Street c39795ae2c Use actual decomp.me host 2024-03-04 18:03:32 -07:00
Luke Street 49ee9b44aa Remove "Algorithm" menu item 2024-03-04 18:03:20 -07:00
Robin Avery 341c1d4b33
Fix release CI (and add `sccache`) (#52)
* Fix release CI (and add `sccache`)

* Rename `objdiff-gui` binary to `objdiff`
2024-03-02 22:42:24 -07:00
Robin Avery 9f4a1e86cd
objdiff-cli diff: Reduce duplicate key event code (#51) 2024-03-02 18:47:54 -07:00
Robin Avery ed5d092b11
objdiff-cli diff: Support "Relax relocation diffs" (#50)
Bound to the `-x` flag or the `x` key.
2024-03-02 18:47:18 -07:00
Robin Avery 023dd7a55b
objdiff-cli diff: Accept any kind of unit path (#48)
* objdiff-cli diff: Accept any kind of unit path

* Appease clippy

* Call `resolve_paths` in slightly fewer cases
2024-03-01 18:18:27 -07:00
Luke Street 3b1249e1ab objdiff-cli diff: Add horizontal scrolling 2024-03-01 01:30:47 -07:00
Luke Street cb13638e07 objdiff-cli: Migrate to ratatui for rendering 2024-03-01 01:03:17 -07:00
Robin Avery 37ddbb7f4a
cli: Log to stderr instead of stdout (#46)
Fixes pipe issues.
2024-02-29 22:27:10 -07:00
Robin Avery b80d361e91
cli report: Generate virtual addresses as uppercase (#45)
Matches dtk symbols.txt and most projects' identifiers.
2024-02-29 22:22:59 -07:00
Robin Avery fd27f4d0cd
cli diff: Resolve object and project if not specified (#44)
* cli diff: Resolve object and project if not specified

* Make `symbol` positional

* Short circuit ambiguous matches

* Tighten argument matching

* Speed up function lookup
2024-02-29 22:22:41 -07:00
Robin Avery 5cfd04fd4f
Add `#[serde(default)]` to `ReportFunction::address` (#43) 2024-02-29 11:21:30 -07:00
Luke Street 5b9ac93c08 ci: Build both objdiff-cli and objdiff-gui 2024-02-28 21:52:35 -07:00
Luke Street 39a13f4d36 objdiff-cli diff & report changes, support .splitmeta object section
- Add `objdiff-cli report changes` for diffing two reports
- Unify some click-to-highlight logic between CLI and GUI
- Load .splitmeta section for extra object metadata (original virtual addr, etc)
- More work on objdiff-cli diff
2024-02-28 21:44:53 -07:00
Ryan Burns 28348606bf
Handle ^F, ^B, ^U and ^D readline shortcuts in pager (#42) 2024-02-28 19:33:15 -07:00
Luke Street fb24063c54 objdiff-cli diff: Click-to-highlight & build fixes 2024-02-27 22:52:18 -07:00
Luke Street cff6a230a3 Remove alternate diff algorithms, only keep Patience 2024-02-27 21:18:42 -07:00
Luke Street 9a7d2bcebf Experimental objdiff-cli (WIP) 2024-02-27 18:47:51 -07:00
Luke Street 4eba5f71b0 Split into objdiff-core / objdiff-gui; update egui to 0.26.2 2024-02-26 18:48:48 -07:00
Luke Street 0a85c498c5 Version 1.0.0 2024-01-22 00:32:54 -07:00
Luke Street c2fcf2797b Export function to decomp.me scratch (beta) 2024-01-22 00:31:43 -07:00
Luke Street e88a58ba39 Option to relax relocation diffs
Ignores differences in relocation targets. (Address, name, etc)

Resolves #34
2024-01-22 00:14:03 -07:00
Luke Street 02f521a528 Disable more options when project config is loaded 2024-01-21 23:58:10 -07:00
Luke Street 197d1247a8 Highlight: Consider uimm/simm/offset all equivalent
Fixes #33
2024-01-21 23:48:12 -07:00
Luke Street eef9598e76 Add DWARF 2+ line info support
Resolves #37
2024-01-21 23:38:52 -07:00
Luke Street 405a2a82db Upgrade all dependencies (+ egui/eframe 0.25.0) 2024-01-20 23:41:48 -07:00
Luke Street 4cdad8a519 Re-enable wgpu and wsl features; rework WSL config
Improve build failure log view & add copy buttons
2024-01-20 23:29:05 -07:00
Luke Street b74a49ed0c Upgrade to egui/eframe 0.24.1 2023-12-11 13:36:00 -05:00
Luke Street e1079db93a Add font loading & configuration 2023-11-28 23:13:51 -05:00
Luke Street 879e03eed5 All one line (thanks PowerShell) 2023-11-27 19:55:17 -05:00
Luke Street 53e6e0c7c4 Build macOS with wgpu enabled 2023-11-27 19:52:12 -05:00
Luke Street 67cea2a8d9
Update README.md 2023-11-24 23:17:35 -05:00
Luke Street e4f97adbdd Version 0.6.1 2023-11-22 00:11:33 -05:00
Luke Street 0ec7bf078b Highlight: Consider reg offsets and signed immediates equivalent
Fixes #32
2023-11-22 00:07:21 -05:00
68 changed files with 8374 additions and 4420 deletions

View File

@ -10,7 +10,6 @@ on:
env:
BUILD_PROFILE: release-lto
CARGO_BIN_NAME: objdiff
CARGO_TARGET_DIR: target
jobs:
@ -25,14 +24,22 @@ jobs:
sudo apt-get update
sudo apt-get -y install libgtk-3-dev
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
components: clippy
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.4
- name: Cargo check
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
run: cargo check
- name: Cargo clippy
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
run: cargo clippy
fmt:
@ -42,7 +49,7 @@ jobs:
RUSTFLAGS: -D warnings
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Rust toolchain
# We use nightly options in rustfmt.toml
uses: dtolnay/rust-toolchain@nightly
@ -62,7 +69,7 @@ jobs:
# Prevent new advisories from failing CI
continue-on-error: ${{ matrix.checks == 'advisories' }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v1
with:
command: check ${{ matrix.checks }}
@ -82,10 +89,15 @@ jobs:
sudo apt-get update
sudo apt-get -y install libgtk-3-dev
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.4
- name: Cargo test
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
run: cargo test --release
build:
@ -97,15 +109,19 @@ jobs:
target: x86_64-unknown-linux-gnu
name: linux-x86_64
packages: libgtk-3-dev
features: default
- platform: windows-latest
target: x86_64-pc-windows-msvc
name: windows-x86_64
features: default
- platform: macos-latest
target: x86_64-apple-darwin
name: macos-x86_64
features: default
- platform: macos-latest
target: aarch64-apple-darwin
name: macos-arm64
features: default
fail-fast: false
runs-on: ${{ matrix.platform }}
steps:
@ -115,22 +131,29 @@ jobs:
sudo apt-get update
sudo apt-get -y install ${{ matrix.packages }}
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.4
- name: Cargo build
run: cargo build --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }} --bin ${{ env.CARGO_BIN_NAME }}
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
run: >
cargo build --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
--bin objdiff-cli --bin objdiff --features ${{ matrix.features }}
- name: Upload artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.name }}
path: |
${{ env.CARGO_TARGET_DIR }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
${{ env.CARGO_TARGET_DIR }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/objdiff-cli
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/objdiff-cli.exe
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/objdiff
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/objdiff.exe
if-no-files-found: error
release:
@ -138,17 +161,31 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
needs: [ build ]
permissions:
contents: write
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Rename artifacts
working-directory: artifacts
run: |
set -euo pipefail
mkdir ../out
for i in */*/$BUILD_PROFILE/$CARGO_BIN_NAME*; do
mv "$i" "../out/$(sed -E "s/([^/]+)\/[^/]+\/$BUILD_PROFILE\/($CARGO_BIN_NAME)/\2-\1/" <<< "$i")"
for dir in */; do
for file in "$dir"*; do
base=$(basename "$file")
name="${base%.*}"
ext="${base##*.}"
if [ "$ext" = "$base" ]; then
ext=""
else
ext=".$ext"
fi
dst="../out/${name}-${dir%/}${ext}"
mv "$file" "$dst"
done
done
ls -R ../out
- name: Release

3304
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,92 +1,12 @@
[package]
name = "objdiff"
version = "0.6.0"
edition = "2021"
rust-version = "1.70"
authors = ["Luke Street <luke@street.dev>"]
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"
[workspace]
members = [
"objdiff-cli",
"objdiff-core",
"objdiff-gui",
]
resolver = "2"
[profile.release-lto]
inherits = "release"
lto = "thin"
strip = "debuginfo"
[features]
default = []
wgpu = ["eframe/wgpu"]
wsl = []
[dependencies]
anyhow = "1.0.75"
byteorder = "1.5.0"
bytes = "1.5.0"
cfg-if = "1.0.0"
const_format = "0.2.32"
cwdemangle = "0.1.6"
dirs = "5.0.1"
eframe = { version = "0.23.0", features = ["persistence"] }
egui = "0.23.0"
egui_extras = "0.23.0"
filetime = "0.2.22"
flagset = "0.4.4"
globset = { version = "0.4.13", features = ["serde1"] }
log = "0.4.20"
memmap2 = "0.9.0"
notify = "6.1.1"
object = { version = "0.32.1", features = ["read_core", "std", "elf"], default-features = false }
png = "0.17.10"
pollster = "0.3.0"
ppc750cl = { git = "https://github.com/encounter/ppc750cl", rev = "4a2bbbc6f84dcb76255ab6f3595a8d4a0ce96618" }
rabbitizer = "1.8.0"
rfd = { version = "0.12.1" } #, default-features = false, features = ['xdg-portal']
ron = "0.8.1"
semver = "1.0.20"
serde = { version = "1", features = ["derive"] }
serde_json = "1.0.108"
serde_yaml = "0.9.27"
similar = "2.3.0"
tempfile = "3.8.1"
thiserror = "1.0.50"
time = { version = "0.3.30", features = ["formatting", "local-offset"] }
toml = "0.8.8"
twox-hash = "1.6.3"
# For Linux static binaries, use rustls
[target.'cfg(target_os = "linux")'.dependencies]
reqwest = { version = "0.11.22", default-features = false, features = ["blocking", "json", "rustls"] }
self_update = { version = "0.39.0", default-features = false, features = ["rustls"] }
# For all other platforms, use native TLS
[target.'cfg(not(target_os = "linux"))'.dependencies]
reqwest = "0.11.22"
self_update = "0.39.0"
[target.'cfg(windows)'.dependencies]
path-slash = "0.2.1"
winapi = "0.3.9"
[target.'cfg(windows)'.build-dependencies]
winres = "0.1.12"
[target.'cfg(unix)'.dependencies]
exec = "0.3.1"
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tracing-subscriber = "0.3"
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.7"
tracing-wasm = "0.2"
[build-dependencies]
anyhow = "1.0.75"
vergen = { version = "8.2.6", features = ["build", "cargo", "git", "gitcl"] }

View File

@ -3,11 +3,20 @@
[Build Status]: https://github.com/encounter/objdiff/actions/workflows/build.yaml/badge.svg
[actions]: https://github.com/encounter/objdiff/actions
A local diffing tool for decompilation projects.
A local diffing tool for decompilation projects. Inspired by [decomp.me](https://decomp.me) and [asm-differ](https://github.com/simonlindholm/asm-differ).
Features:
- Compare entire object files: functions and data.
- Built-in symbol demangling for C++. (CodeWarrior, Itanium & MSVC)
- Automatic rebuild on source file changes.
- Project integration via [configuration file](#configuration).
- Search and filter all of a project's objects and quickly switch.
- Click to highlight all instances of values and registers.
Supports:
- PowerPC 750CL (GameCube & Wii)
- MIPS (Nintendo 64)
- MIPS (Nintendo 64 & PS2)
- x86 (COFF only at the moment)
See [Usage](#usage) for more information.
@ -50,6 +59,10 @@ file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it
// objdiff.json
{
"custom_make": "ninja",
"custom_args": [
"-d",
"keeprsp"
],
// Only required if objects use "path" instead of "target_path" and "base_path".
"target_dir": "build/asm",
@ -83,7 +96,10 @@ file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it
```
`custom_make` _(optional)_: By default, objdiff will use `make` to build the project.
If the project uses a different build system (e.g. `ninja`), specify it here.
If the project uses a different build system (e.g. `ninja`), specify it here.
The build command will be `[custom_make] [custom_args] path/to/object.o`.
`custom_args` _(optional)_: Additional arguments to pass to the build command prior to the object path.
`target_dir` _(optional)_: Relative from the root of the project, this where the "target" or "expected" objects are located.
These are the **intended result** of the match.
@ -118,7 +134,17 @@ If not specified, objdiff will use the default patterns listed above.
> `reverse_fn_order` _(optional)_: Displays function symbols in reversed order.
Used to support MWCC's `-inline deferred` option, which reverses the order of functions in the object file.
## Building
Install Rust via [rustup](https://rustup.rs).
```shell
$ git clone https://github.com/encounter/objdiff.git
$ cd objdiff
$ cargo run --release
# or, for wgpu backend (recommended on macOS)
$ cargo run --release --features wgpu
```
## License

29
objdiff-cli/Cargo.toml Normal file
View File

@ -0,0 +1,29 @@
[package]
name = "objdiff-cli"
version = "0.1.0"
edition = "2021"
rust-version = "1.70"
authors = ["Luke Street <luke@street.dev>"]
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]
anyhow = "1.0.82"
argp = "0.3.0"
crossterm = "0.27.0"
enable-ansi-support = "0.2.1"
objdiff-core = { path = "../objdiff-core", features = ["all"] }
ratatui = "0.26.2"
rayon = "1.10.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1.0.116"
supports-color = "3.0.0"
time = { version = "0.3.36", features = ["formatting", "local-offset"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }

9
objdiff-cli/build.rs Normal file
View File

@ -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");
}

View File

@ -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>(T)
where T: FromArgs;
impl<T> TopLevelCommand for ArgsOrVersion<T> where T: FromArgs {}
impl<T> FromArgs for ArgsOrVersion<T>
where T: FromArgs
{
fn _from_args(
command_name: &[&str],
args: &[&OsStr],
parent: Option<&mut dyn ParseGlobalOptions>,
) -> Result<Self, EarlyExit> {
/// 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 processs `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>() -> T
where T: TopLevelCommand {
argp::parse_args_or_exit::<ArgsOrVersion<T>>(argp::DEFAULT).0
}

871
objdiff-cli/src/cmd/diff.rs Normal file
View File

@ -0,0 +1,871 @@
use std::{fs, io::stdout, path::PathBuf, str::FromStr};
use anyhow::{bail, Context, Result};
use argp::FromArgs;
use crossterm::{
event,
event::{
DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseButton,
MouseEventKind,
},
terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, SetTitle,
},
};
use event::KeyModifiers;
use objdiff_core::{
config::{ProjectConfig, ProjectObject},
diff,
diff::{
display::{display_diff, DiffText, HighlightKind},
DiffObjsResult, ObjDiff, ObjInsDiffKind, ObjSymbolDiff,
},
obj,
obj::{ObjInfo, ObjSectionKind, ObjSymbol, SymbolRef},
};
use ratatui::{
prelude::*,
widgets::{Block, Borders, Clear, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
};
use crate::util::term::crossterm_panic_handler;
#[derive(FromArgs, PartialEq, Debug)]
/// Diff two object files.
#[argp(subcommand, name = "diff")]
pub struct Args {
#[argp(option, short = '1')]
/// Target object file
target: Option<PathBuf>,
#[argp(option, short = '2')]
/// Base object file
base: Option<PathBuf>,
#[argp(option, short = 'p')]
/// Project directory
project: Option<PathBuf>,
#[argp(option, short = 'u')]
/// Unit name within project
unit: Option<String>,
#[argp(switch, short = 'x')]
/// Relax relocation diffs
relax_reloc_diffs: bool,
#[argp(positional)]
/// Function symbol to diff
symbol: String,
}
pub fn run(args: Args) -> Result<()> {
let (target_path, base_path, project_config) =
match (&args.target, &args.base, &args.project, &args.unit) {
(Some(t), Some(b), None, None) => (Some(t.clone()), Some(b.clone()), None),
(None, None, p, u) => {
let project = match p {
Some(project) => project.clone(),
_ => std::env::current_dir().context("Failed to get the current directory")?,
};
let Some((project_config, project_config_info)) =
objdiff_core::config::try_project_config(&project)
else {
bail!("Project config not found in {}", &project.display())
};
let mut project_config = project_config.with_context(|| {
format!("Reading project config {}", project_config_info.path.display())
})?;
let object = {
let resolve_paths = |o: &mut ProjectObject| {
o.resolve_paths(
&project,
project_config.target_dir.as_deref(),
project_config.base_dir.as_deref(),
)
};
if let Some(u) = u {
let unit_path =
PathBuf::from_str(u).ok().and_then(|p| fs::canonicalize(p).ok());
let Some(object) = project_config.objects.iter_mut().find_map(|obj| {
if obj.name.as_deref() == Some(u) {
resolve_paths(obj);
return Some(obj);
}
let up = unit_path.as_deref()?;
resolve_paths(obj);
if [&obj.base_path, &obj.target_path]
.into_iter()
.filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok()))
.any(|p| p == up)
{
return Some(obj);
}
None
}) else {
bail!("Unit not found: {}", u)
};
object
} else {
let mut idx = None;
let mut count = 0usize;
for (i, obj) in project_config.objects.iter_mut().enumerate() {
resolve_paths(obj);
if obj
.target_path
.as_deref()
.map(|o| obj::read::has_function(o, &args.symbol))
.transpose()?
.unwrap_or(false)
{
idx = Some(i);
count += 1;
if count > 1 {
break;
}
}
}
match (count, idx) {
(0, None) => bail!("Symbol not found: {}", &args.symbol),
(1, Some(i)) => &mut project_config.objects[i],
(2.., Some(_)) => bail!(
"Multiple instances of {} were found, try specifying a unit",
&args.symbol
),
_ => unreachable!(),
}
}
};
let target_path = object.target_path.clone();
let base_path = object.base_path.clone();
(target_path, base_path, Some(project_config))
}
_ => bail!("Either target and base or project and unit must be specified"),
};
let time_format = time::format_description::parse_borrowed::<2>("[hour]:[minute]:[second]")
.context("Failed to parse time format")?;
let mut state = Box::new(FunctionDiffUi {
relax_reloc_diffs: args.relax_reloc_diffs,
left_highlight: HighlightKind::None,
right_highlight: HighlightKind::None,
scroll_x: 0,
scroll_state_x: ScrollbarState::default(),
scroll_y: 0,
scroll_state_y: ScrollbarState::default(),
per_page: 0,
num_rows: 0,
symbol_name: args.symbol.clone(),
target_path,
base_path,
project_config,
left_obj: None,
right_obj: None,
prev_obj: None,
diff_result: DiffObjsResult::default(),
left_sym: None,
right_sym: None,
prev_sym: None,
reload_time: None,
time_format,
open_options: false,
three_way: false,
});
state.reload()?;
crossterm_panic_handler();
enable_raw_mode()?;
crossterm::queue!(
stdout(),
EnterAlternateScreen,
EnableMouseCapture,
SetTitle(format!("{} - objdiff", args.symbol)),
)?;
let backend = CrosstermBackend::new(stdout());
let mut terminal = Terminal::new(backend)?;
'outer: loop {
let mut result = EventResult { redraw: true, ..Default::default() };
loop {
if result.redraw {
terminal.draw(|f| loop {
result.redraw = false;
state.draw(f, &mut result);
if state.open_options {
state.draw_options(f, &mut result);
}
result.click_xy = None;
if !result.redraw {
break;
}
// Clear buffer on redraw
f.buffer_mut().reset();
})?;
}
match state.handle_event(event::read()?) {
EventControlFlow::Break => break 'outer,
EventControlFlow::Continue(r) => result = r,
EventControlFlow::Reload => break,
}
}
state.reload()?;
}
// Reset terminal
disable_raw_mode()?;
crossterm::execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture)?;
terminal.show_cursor()?;
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.size());
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).gray().title("TARGET".bold()))
.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).gray().title("CURRENT".bold()))
.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).gray().title("SAVED".bold());
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.size())[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) => {
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) => {
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 target = self
.target_path
.as_deref()
.map(|p| obj::read::read(p).with_context(|| format!("Loading {}", p.display())))
.transpose()?;
let base = self
.base_path
.as_deref()
.map(|p| obj::read::read(p).with_context(|| format!("Loading {}", p.display())))
.transpose()?;
let config = diff::DiffObjConfig {
relax_reloc_diffs: self.relax_reloc_diffs,
space_between_args: true, // TODO
x86_formatter: Default::default(), // TODO
};
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

@ -0,0 +1,2 @@
pub mod diff;
pub mod report;

View File

@ -0,0 +1,496 @@
use std::{
collections::HashSet,
fs::File,
io::{BufReader, 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};
use tracing::{info, warn};
#[derive(FromArgs, PartialEq, Debug)]
/// Commands for processing NVIDIA Shield TV alf files.
#[argp(subcommand, name = "report")]
pub struct Args {
#[argp(subcommand)]
command: SubCommand,
}
#[derive(FromArgs, PartialEq, Debug)]
#[argp(subcommand)]
pub enum SubCommand {
Generate(GenerateArgs),
Changes(ChangesArgs),
}
#[derive(FromArgs, PartialEq, Debug)]
/// Generate a report from a project.
#[argp(subcommand, name = "generate")]
pub struct GenerateArgs {
#[argp(option, short = 'p')]
/// Project directory
project: Option<PathBuf>,
#[argp(option, short = 'o')]
/// Output JSON file
output: Option<PathBuf>,
#[argp(switch, short = 'd')]
/// Deduplicate global and weak symbols
deduplicate: bool,
}
#[derive(FromArgs, PartialEq, Debug)]
/// List any changes from a previous report.
#[argp(subcommand, name = "changes")]
pub struct ChangesArgs {
#[argp(positional)]
/// Previous report JSON file
previous: PathBuf,
#[argp(positional)]
/// Current report JSON file
current: PathBuf,
#[argp(option, short = 'o')]
/// Output JSON file
output: Option<PathBuf>,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
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<ReportUnit>,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct ReportUnit {
name: String,
fuzzy_match_percent: f32,
total_size: u64,
matched_size: u64,
total_functions: u32,
matched_functions: u32,
#[serde(skip_serializing_if = "Option::is_none")]
complete: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
module_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
module_id: Option<u32>,
functions: Vec<ReportFunction>,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct ReportFunction {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
demangled_name: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_hex",
deserialize_with = "deserialize_hex"
)]
address: Option<u64>,
size: u64,
fuzzy_match_percent: f32,
}
pub fn run(args: Args) -> Result<()> {
match args.command {
SubCommand::Generate(args) => generate(args),
SubCommand::Changes(args) => changes(args),
}
}
fn generate(args: GenerateArgs) -> Result<()> {
let project_dir = args.project.as_deref().unwrap_or_else(|| Path::new("."));
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");
};
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<String> = 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::<Result<Vec<Option<ReportUnit>>>>()?;
report.units = units.into_iter().flatten().collect();
}
for unit in &report.units {
report.fuzzy_match_percent += unit.fuzzy_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();
info!("Report generated in {}.{:03}s", duration.as_secs(), duration.subsec_millis());
if let Some(output) = &args.output {
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<String>>,
) -> Result<Option<ReportUnit>> {
object.resolve_paths(project_dir, target_dir, base_dir);
match (&object.target_path, &object.base_path) {
(None, Some(_)) if object.complete != Some(true) => {
warn!("Skipping object without target: {}", object.name());
return Ok(None);
}
(None, None) => {
warn!("Skipping object without target or base: {}", object.name());
return Ok(None);
}
_ => {}
}
// println!("Checking {}", object.name());
let target = object
.target_path
.as_ref()
.map(|p| obj::read::read(p).with_context(|| format!("Failed to open {}", p.display())))
.transpose()?;
let base = object
.base_path
.as_ref()
.map(|p| obj::read::read(p).with_context(|| format!("Failed to open {}", p.display())))
.transpose()?;
let config = diff::DiffObjConfig { relax_reloc_diffs: true, ..Default::default() };
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?;
let mut unit = ReportUnit {
name: object.name().to_string(),
complete: object.complete,
module_name: target
.as_ref()
.and_then(|o| o.split_meta.as_ref())
.and_then(|m| m.module_name.clone()),
module_id: target.as_ref().and_then(|o| o.split_meta.as_ref()).and_then(|m| m.module_id),
..Default::default()
};
let obj = target.as_ref().or(base.as_ref()).unwrap();
let obj_diff = result.left.as_ref().or(result.right.as_ref()).unwrap();
for (section, section_diff) in obj.sections.iter().zip(&obj_diff.sections) {
if section.kind != ObjSectionKind::Code {
continue;
}
for (symbol, symbol_diff) in section.symbols.iter().zip(&section_diff.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_diff.match_percent.unwrap_or_else(|| {
// Support cases where we don't have a target object,
// assume complete means 100% match
if object.complete == Some(true) {
100.0
} else {
0.0
}
});
unit.fuzzy_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,
fuzzy_match_percent: match_percent,
address: symbol.virtual_address,
});
if match_percent == 100.0 {
unit.matched_functions += 1;
}
unit.total_functions += 1;
}
}
if unit.total_size == 0 {
unit.fuzzy_match_percent = 100.0;
} else {
unit.fuzzy_match_percent /= unit.total_size as f32;
}
Ok(Some(unit))
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct Changes {
from: ChangeInfo,
to: ChangeInfo,
units: Vec<ChangeUnit>,
}
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
struct ChangeInfo {
fuzzy_match_percent: f32,
total_size: u64,
matched_size: u64,
matched_size_percent: f32,
total_functions: u32,
matched_functions: u32,
matched_functions_percent: f32,
}
impl From<&Report> for ChangeInfo {
fn from(report: &Report) -> Self {
Self {
fuzzy_match_percent: report.fuzzy_match_percent,
total_size: report.total_size,
matched_size: report.matched_size,
matched_size_percent: report.matched_size_percent,
total_functions: report.total_functions,
matched_functions: report.matched_functions,
matched_functions_percent: report.matched_functions_percent,
}
}
}
impl From<&ReportUnit> for ChangeInfo {
fn from(value: &ReportUnit) -> Self {
Self {
fuzzy_match_percent: value.fuzzy_match_percent,
total_size: value.total_size,
matched_size: value.matched_size,
matched_size_percent: if value.total_size == 0 {
100.0
} else {
value.matched_size as f32 / value.total_size as f32 * 100.0
},
total_functions: value.total_functions,
matched_functions: value.matched_functions,
matched_functions_percent: if value.total_functions == 0 {
100.0
} else {
value.matched_functions as f32 / value.total_functions as f32 * 100.0
},
}
}
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct ChangeUnit {
name: String,
from: Option<ChangeInfo>,
to: Option<ChangeInfo>,
functions: Vec<ChangeFunction>,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct ChangeFunction {
name: String,
from: Option<ChangeFunctionInfo>,
to: Option<ChangeFunctionInfo>,
}
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
struct ChangeFunctionInfo {
fuzzy_match_percent: f32,
size: u64,
}
impl From<&ReportFunction> for ChangeFunctionInfo {
fn from(value: &ReportFunction) -> Self {
Self { fuzzy_match_percent: value.fuzzy_match_percent, size: value.size }
}
}
fn changes(args: ChangesArgs) -> Result<()> {
let previous = read_report(&args.previous)?;
let current = read_report(&args.current)?;
let mut changes = Changes {
from: ChangeInfo::from(&previous),
to: ChangeInfo::from(&current),
units: vec![],
};
for prev_unit in &previous.units {
let prev_unit_info = ChangeInfo::from(prev_unit);
let curr_unit = current.units.iter().find(|u| u.name == prev_unit.name);
let curr_unit_info = curr_unit.map(ChangeInfo::from);
let mut functions = vec![];
if let Some(curr_unit) = curr_unit {
for prev_func in &prev_unit.functions {
let prev_func_info = ChangeFunctionInfo::from(prev_func);
let curr_func = curr_unit.functions.iter().find(|f| f.name == prev_func.name);
let curr_func_info = curr_func.map(ChangeFunctionInfo::from);
if let Some(curr_func_info) = curr_func_info {
if prev_func_info != curr_func_info {
functions.push(ChangeFunction {
name: prev_func.name.clone(),
from: Some(prev_func_info),
to: Some(curr_func_info),
});
}
} else {
functions.push(ChangeFunction {
name: prev_func.name.clone(),
from: Some(prev_func_info),
to: None,
});
}
}
for curr_func in &curr_unit.functions {
if !prev_unit.functions.iter().any(|f| f.name == curr_func.name) {
functions.push(ChangeFunction {
name: curr_func.name.clone(),
from: None,
to: Some(ChangeFunctionInfo::from(curr_func)),
});
}
}
} else {
for prev_func in &prev_unit.functions {
functions.push(ChangeFunction {
name: prev_func.name.clone(),
from: Some(ChangeFunctionInfo::from(prev_func)),
to: None,
});
}
}
if !functions.is_empty() || !matches!(&curr_unit_info, Some(v) if v == &prev_unit_info) {
changes.units.push(ChangeUnit {
name: prev_unit.name.clone(),
from: Some(prev_unit_info),
to: curr_unit_info,
functions,
});
}
}
for curr_unit in &current.units {
if !previous.units.iter().any(|u| u.name == curr_unit.name) {
changes.units.push(ChangeUnit {
name: curr_unit.name.clone(),
from: None,
to: Some(ChangeInfo::from(curr_unit)),
functions: curr_unit
.functions
.iter()
.map(|f| ChangeFunction {
name: f.name.clone(),
from: None,
to: Some(ChangeFunctionInfo::from(f)),
})
.collect(),
});
}
}
if let Some(output) = &args.output {
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, &changes)?;
output.flush()?;
} else {
serde_json::to_writer_pretty(std::io::stdout(), &changes)?;
}
Ok(())
}
fn read_report(path: &Path) -> Result<Report> {
serde_json::from_reader(BufReader::new(
File::open(path).with_context(|| format!("Failed to open {}", path.display()))?,
))
.with_context(|| format!("Failed to read report {}", path.display()))
}
fn serialize_hex<S>(x: &Option<u64>, s: S) -> Result<S::Ok, S::Error>
where S: serde::Serializer {
if let Some(x) = x {
s.serialize_str(&format!("{:#X}", x))
} else {
s.serialize_none()
}
}
fn deserialize_hex<'de, D>(d: D) -> Result<Option<u64>, D::Error>
where D: serde::Deserializer<'de> {
use serde::Deserialize;
let s = String::deserialize(d)?;
if s.is_empty() {
Ok(None)
} else if !s.starts_with("0x") {
Err(serde::de::Error::custom("expected hex string"))
} else {
u64::from_str_radix(&s[2..], 16).map(Some).map_err(serde::de::Error::custom)
}
}

141
objdiff-cli/src/main.rs Normal file
View File

@ -0,0 +1,141 @@
mod argp_version;
mod cmd;
mod util;
use std::{env, ffi::OsStr, fmt::Display, 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<Self, Self::Err> {
Ok(match s {
"error" => Self::Error,
"warn" => Self::Warn,
"info" => Self::Info,
"debug" => Self::Debug,
"trace" => Self::Trace,
_ => return Err(()),
})
}
}
impl Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
LogLevel::Error => "error",
LogLevel::Warn => "warn",
LogLevel::Info => "info",
LogLevel::Debug => "debug",
LogLevel::Trace => "trace",
})
}
}
impl FromArgValue for LogLevel {
fn from_arg_value(value: &OsStr) -> Result<Self, String> {
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<PathBuf>,
#[argp(option, short = 'L')]
/// Minimum logging level. (Default: info)
/// Possible values: error, warn, info, debug, trace
log_level: Option<LogLevel>,
/// 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).with_writer(std::io::stderr);
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);
}
}

View File

@ -0,0 +1 @@
pub mod term;

View File

@ -0,0 +1,16 @@
use std::{io::stdout, panic};
use crossterm::{
cursor::Show,
event::DisableMouseCapture,
terminal::{disable_raw_mode, LeaveAlternateScreen},
};
pub fn crossterm_panic_handler() {
let original_hook = panic::take_hook();
panic::set_hook(Box::new(move |panic_info| {
let _ = crossterm::execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture, Show);
let _ = disable_raw_mode();
original_hook(panic_info);
}));
}

54
objdiff-core/Cargo.toml Normal file
View File

@ -0,0 +1,54 @@
[package]
name = "objdiff-core"
version = "1.0.0"
edition = "2021"
rust-version = "1.70"
authors = ["Luke Street <luke@street.dev>"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff"
readme = "../README.md"
description = """
A local diffing tool for decompilation projects.
"""
[features]
all = ["config", "dwarf", "mips", "ppc", "x86"]
any-arch = [] # Implicit, used to check if any arch is enabled
config = ["globset", "semver", "serde_json", "serde_yaml"]
dwarf = ["gimli"]
mips = ["any-arch", "rabbitizer"]
ppc = ["any-arch", "cwdemangle", "ppc750cl"]
x86 = ["any-arch", "cpp_demangle", "iced-x86", "msvc-demangler"]
[dependencies]
anyhow = "1.0.82"
byteorder = "1.5.0"
filetime = "0.2.23"
flagset = "0.4.5"
log = "0.4.21"
memmap2 = "0.9.4"
num-traits = "0.2.18"
object = { version = "0.35.0", features = ["read_core", "std", "elf", "pe"], default-features = false }
serde = { version = "1", features = ["derive"] }
similar = { version = "2.5.0", default-features = false }
# config
globset = { version = "0.4.14", features = ["serde1"], optional = true }
semver = { version = "1.0.22", optional = true }
serde_json = { version = "1.0.116", optional = true }
serde_yaml = { version = "0.9.34", optional = true }
# dwarf
gimli = { version = "0.29.0", default-features = false, features = ["read-all"], optional = true }
# ppc
cwdemangle = { version = "1.0.0", optional = true }
ppc750cl = { git = "https://github.com/encounter/ppc750cl", rev = "6cbd7d888c7082c2c860f66cbb9848d633f753ed", optional = true }
# mips
rabbitizer = { version = "1.10.0", optional = true }
# x86
cpp_demangle = { version = "0.4.3", optional = true }
iced-x86 = { version = "1.21.0", default-features = false, features = ["std", "decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums"], optional = true }
msvc-demangler = { version = "0.10.0", optional = true }

View File

@ -0,0 +1,208 @@
use std::borrow::Cow;
use anyhow::{bail, Result};
use object::{elf, Endian, Endianness, File, Object, Relocation, RelocationFlags};
use rabbitizer::{config, Abi, InstrCategory, Instruction, OperandType};
use crate::{
arch::{ObjArch, ProcessCodeResult},
diff::DiffObjConfig,
obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection, SymbolRef},
};
fn configure_rabbitizer() {
unsafe {
config::RabbitizerConfig_Cfg.reg_names.fpr_abi_names = Abi::O32;
}
}
pub struct ObjArchMips {
pub endianness: Endianness,
}
impl ObjArchMips {
pub fn new(object: &File) -> Result<Self> {
configure_rabbitizer();
Ok(Self { endianness: object.endianness() })
}
}
impl ObjArch for ObjArchMips {
fn process_code(
&self,
obj: &ObjInfo,
symbol_ref: SymbolRef,
config: &DiffObjConfig,
) -> Result<ProcessCodeResult> {
let (section, symbol) = obj.section_symbol(symbol_ref);
let code = &section.data
[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
let start_address = symbol.address;
let end_address = symbol.address + symbol.size;
let ins_count = code.len() / 4;
let mut ops = Vec::<u16>::with_capacity(ins_count);
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
let mut cur_addr = start_address as u32;
for chunk in code.chunks_exact(4) {
let reloc = section.relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
let code = self.endianness.read_u32_bytes(chunk.try_into()?);
let instruction = Instruction::new(code, cur_addr, InstrCategory::CPU);
let op = instruction.unique_id as u16;
ops.push(op);
let mnemonic = instruction.opcode_name().to_string();
let is_branch = instruction.is_branch();
let branch_offset = instruction.branch_offset();
let branch_dest = if is_branch {
cur_addr.checked_add_signed(branch_offset).map(|a| a as u64)
} else {
None
};
let operands = instruction.get_operands_slice();
let mut args = Vec::with_capacity(operands.len() + 1);
for (idx, op) in operands.iter().enumerate() {
if idx > 0 {
args.push(ObjInsArg::PlainText(config.separator().into()));
}
match op {
OperandType::cpu_immediate
| OperandType::cpu_label
| OperandType::cpu_branch_target_label => {
if let Some(branch_dest) = branch_dest {
args.push(ObjInsArg::BranchDest(branch_dest));
} else if let Some(reloc) = reloc {
if matches!(&reloc.target_section, Some(s) if s == ".text")
&& reloc.target.address > start_address
&& reloc.target.address < end_address
{
args.push(ObjInsArg::BranchDest(reloc.target.address));
} else {
push_reloc(&mut args, reloc)?;
}
} else {
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
op.disassemble(&instruction, None).into(),
)));
}
}
OperandType::cpu_immediate_base => {
if let Some(reloc) = reloc {
push_reloc(&mut args, reloc)?;
} else {
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
OperandType::cpu_immediate.disassemble(&instruction, None).into(),
)));
}
args.push(ObjInsArg::PlainText("(".into()));
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
OperandType::cpu_rs.disassemble(&instruction, None).into(),
)));
args.push(ObjInsArg::PlainText(")".into()));
}
_ => {
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
op.disassemble(&instruction, None).into(),
)));
}
}
}
let line = obj
.line_info
.as_ref()
.and_then(|map| map.range(..=cur_addr as u64).last().map(|(_, &b)| b));
insts.push(ObjIns {
address: cur_addr as u64,
size: 4,
op,
mnemonic,
args,
reloc: reloc.cloned(),
branch_dest,
line,
orig: None,
});
cur_addr += 4;
}
Ok(ProcessCodeResult { ops, insts })
}
fn implcit_addend(
&self,
section: &ObjSection,
address: u64,
reloc: &Relocation,
) -> Result<i64> {
let data = section.data[address as usize..address as usize + 4].try_into()?;
let addend = self.endianness.read_u32_bytes(data);
Ok(match reloc.flags() {
RelocationFlags::Elf { r_type: elf::R_MIPS_32 } => addend as i64,
RelocationFlags::Elf { r_type: elf::R_MIPS_HI16 } => {
((addend & 0x0000FFFF) << 16) as i32 as i64
}
RelocationFlags::Elf {
r_type:
elf::R_MIPS_LO16 | elf::R_MIPS_GOT16 | elf::R_MIPS_CALL16 | elf::R_MIPS_GPREL16,
} => (addend & 0x0000FFFF) as i16 as i64,
RelocationFlags::Elf { r_type: elf::R_MIPS_26 } => ((addend & 0x03FFFFFF) << 2) as i64,
flags => bail!("Unsupported MIPS implicit relocation {flags:?}"),
})
}
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
match flags {
RelocationFlags::Elf { r_type } => match r_type {
elf::R_MIPS_HI16 => Cow::Borrowed("R_MIPS_HI16"),
elf::R_MIPS_LO16 => Cow::Borrowed("R_MIPS_LO16"),
elf::R_MIPS_GOT16 => Cow::Borrowed("R_MIPS_GOT16"),
elf::R_MIPS_CALL16 => Cow::Borrowed("R_MIPS_CALL16"),
elf::R_MIPS_GPREL16 => Cow::Borrowed("R_MIPS_GPREL16"),
elf::R_MIPS_32 => Cow::Borrowed("R_MIPS_32"),
elf::R_MIPS_26 => Cow::Borrowed("R_MIPS_26"),
_ => Cow::Owned(format!("<{flags:?}>")),
},
_ => Cow::Owned(format!("<{flags:?}>")),
}
}
}
fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {
match reloc.flags {
RelocationFlags::Elf { r_type } => match r_type {
elf::R_MIPS_HI16 => {
args.push(ObjInsArg::PlainText("%hi(".into()));
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText(")".into()));
}
elf::R_MIPS_LO16 => {
args.push(ObjInsArg::PlainText("%lo(".into()));
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText(")".into()));
}
elf::R_MIPS_GOT16 => {
args.push(ObjInsArg::PlainText("%got(".into()));
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText(")".into()));
}
elf::R_MIPS_CALL16 => {
args.push(ObjInsArg::PlainText("%call16(".into()));
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText(")".into()));
}
elf::R_MIPS_GPREL16 => {
args.push(ObjInsArg::PlainText("%gp_rel(".into()));
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText(")".into()));
}
elf::R_MIPS_32 | elf::R_MIPS_26 => {
args.push(ObjInsArg::Reloc);
}
_ => bail!("Unsupported ELF MIPS relocation type {r_type}"),
},
flags => panic!("Unsupported MIPS relocation flags {flags:?}"),
}
Ok(())
}

View File

@ -0,0 +1,49 @@
use std::borrow::Cow;
use anyhow::{bail, Result};
use object::{Architecture, Object, Relocation, RelocationFlags};
use crate::{
diff::DiffObjConfig,
obj::{ObjInfo, ObjIns, ObjSection, SymbolRef},
};
#[cfg(feature = "mips")]
mod mips;
#[cfg(feature = "ppc")]
mod ppc;
#[cfg(feature = "x86")]
mod x86;
pub trait ObjArch: Send + Sync {
fn process_code(
&self,
obj: &ObjInfo,
symbol_ref: SymbolRef,
config: &DiffObjConfig,
) -> Result<ProcessCodeResult>;
fn implcit_addend(&self, section: &ObjSection, address: u64, reloc: &Relocation)
-> Result<i64>;
fn demangle(&self, _name: &str) -> Option<String> { None }
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str>;
}
pub struct ProcessCodeResult {
pub ops: Vec<u16>,
pub insts: Vec<ObjIns>,
}
pub fn new_arch(object: &object::File) -> Result<Box<dyn ObjArch>> {
Ok(match object.architecture() {
#[cfg(feature = "ppc")]
Architecture::PowerPc => Box::new(ppc::ObjArchPpc::new(object)?),
#[cfg(feature = "mips")]
Architecture::Mips => Box::new(mips::ObjArchMips::new(object)?),
#[cfg(feature = "x86")]
Architecture::I386 | Architecture::X86_64 => Box::new(x86::ObjArchX86::new(object)?),
arch => bail!("Unsupported architecture: {arch:?}"),
})
}

View File

@ -0,0 +1,211 @@
use std::borrow::Cow;
use anyhow::{bail, Result};
use object::{elf, File, Relocation, RelocationFlags};
use ppc750cl::{Argument, InsIter, GPR};
use crate::{
arch::{ObjArch, ProcessCodeResult},
diff::DiffObjConfig,
obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection, SymbolRef},
};
// Relative relocation, can be Simm, Offset or BranchDest
fn is_relative_arg(arg: &Argument) -> bool {
matches!(arg, Argument::Simm(_) | Argument::Offset(_) | Argument::BranchDest(_))
}
// Relative or absolute relocation, can be Uimm, Simm or Offset
fn is_rel_abs_arg(arg: &Argument) -> bool {
matches!(arg, Argument::Uimm(_) | Argument::Simm(_) | Argument::Offset(_))
}
fn is_offset_arg(arg: &Argument) -> bool { matches!(arg, Argument::Offset(_)) }
pub struct ObjArchPpc {}
impl ObjArchPpc {
pub fn new(_file: &File) -> Result<Self> { Ok(Self {}) }
}
impl ObjArch for ObjArchPpc {
fn process_code(
&self,
obj: &ObjInfo,
symbol_ref: SymbolRef,
config: &DiffObjConfig,
) -> Result<ProcessCodeResult> {
let (section, symbol) = obj.section_symbol(symbol_ref);
let code = &section.data
[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
let ins_count = code.len() / 4;
let mut ops = Vec::<u16>::with_capacity(ins_count);
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
for (cur_addr, mut ins) in InsIter::new(code, symbol.address as u32) {
let reloc = section.relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
if let Some(reloc) = reloc {
// Zero out relocations
ins.code = match reloc.flags {
RelocationFlags::Elf { r_type: elf::R_PPC_EMB_SDA21 } => ins.code & !0x1FFFFF,
RelocationFlags::Elf { r_type: elf::R_PPC_REL24 } => ins.code & !0x3FFFFFC,
RelocationFlags::Elf { r_type: elf::R_PPC_REL14 } => ins.code & !0xFFFC,
RelocationFlags::Elf {
r_type: elf::R_PPC_ADDR16_HI | elf::R_PPC_ADDR16_HA | elf::R_PPC_ADDR16_LO,
} => ins.code & !0xFFFF,
_ => ins.code,
};
}
let orig = ins.basic().to_string();
let simplified = ins.simplified();
let mut reloc_arg = None;
if let Some(reloc) = reloc {
match reloc.flags {
RelocationFlags::Elf { r_type: elf::R_PPC_EMB_SDA21 } => {
reloc_arg = Some(1);
}
RelocationFlags::Elf { r_type: elf::R_PPC_REL24 | elf::R_PPC_REL14 } => {
reloc_arg = simplified.args.iter().rposition(is_relative_arg);
}
RelocationFlags::Elf {
r_type: elf::R_PPC_ADDR16_HI | elf::R_PPC_ADDR16_HA | elf::R_PPC_ADDR16_LO,
} => {
reloc_arg = simplified.args.iter().rposition(is_rel_abs_arg);
}
_ => {}
}
}
let mut args = vec![];
let mut branch_dest = None;
let mut writing_offset = false;
for (idx, arg) in simplified.args_iter().enumerate() {
if idx > 0 && !writing_offset {
args.push(ObjInsArg::PlainText(config.separator().into()));
}
if reloc_arg == Some(idx) {
let reloc = reloc.unwrap();
push_reloc(&mut args, reloc)?;
// For @sda21, we can omit the register argument
if matches!(reloc.flags, RelocationFlags::Elf { r_type: elf::R_PPC_EMB_SDA21 })
// Sanity check: the next argument should be r0
&& matches!(simplified.args.get(idx + 1), Some(Argument::GPR(GPR(0))))
{
break;
}
} else {
match arg {
Argument::Simm(simm) => {
args.push(ObjInsArg::Arg(ObjInsArgValue::Signed(simm.0 as i64)));
}
Argument::Uimm(uimm) => {
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(uimm.0 as u64)));
}
Argument::Offset(offset) => {
args.push(ObjInsArg::Arg(ObjInsArgValue::Signed(offset.0 as i64)));
}
Argument::BranchDest(dest) => {
let dest = cur_addr.wrapping_add_signed(dest.0) as u64;
args.push(ObjInsArg::BranchDest(dest));
branch_dest = Some(dest);
}
_ => {
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
arg.to_string().into(),
)));
}
};
}
if writing_offset {
args.push(ObjInsArg::PlainText(")".into()));
writing_offset = false;
}
if is_offset_arg(arg) {
args.push(ObjInsArg::PlainText("(".into()));
writing_offset = true;
}
}
ops.push(ins.op as u16);
let line = obj
.line_info
.as_ref()
.and_then(|map| map.range(..=cur_addr as u64).last().map(|(_, &b)| b));
insts.push(ObjIns {
address: cur_addr as u64,
size: 4,
mnemonic: simplified.mnemonic.to_string(),
args,
reloc: reloc.cloned(),
op: ins.op as u16,
branch_dest,
line,
orig: Some(orig),
});
}
Ok(ProcessCodeResult { ops, insts })
}
fn implcit_addend(
&self,
_section: &ObjSection,
address: u64,
reloc: &Relocation,
) -> Result<i64> {
bail!("Unsupported PPC implicit relocation {:#x}:{:?}", address, reloc.flags())
}
fn demangle(&self, name: &str) -> Option<String> {
cwdemangle::demangle(name, &cwdemangle::DemangleOptions::default())
}
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
match flags {
RelocationFlags::Elf { r_type } => match r_type {
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_HA => Cow::Borrowed("R_PPC_ADDR16_HA"),
elf::R_PPC_EMB_SDA21 => Cow::Borrowed("R_PPC_EMB_SDA21"),
elf::R_PPC_ADDR32 => Cow::Borrowed("R_PPC_ADDR32"),
elf::R_PPC_UADDR32 => Cow::Borrowed("R_PPC_UADDR32"),
elf::R_PPC_REL24 => Cow::Borrowed("R_PPC_REL24"),
elf::R_PPC_REL14 => Cow::Borrowed("R_PPC_REL14"),
_ => Cow::Owned(format!("<{flags:?}>")),
},
_ => Cow::Owned(format!("<{flags:?}>")),
}
}
}
fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {
match reloc.flags {
RelocationFlags::Elf { r_type } => match r_type {
elf::R_PPC_ADDR16_LO => {
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText("@l".into()));
}
elf::R_PPC_ADDR16_HI => {
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText("@h".into()));
}
elf::R_PPC_ADDR16_HA => {
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText("@ha".into()));
}
elf::R_PPC_EMB_SDA21 => {
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText("@sda21".into()));
}
elf::R_PPC_ADDR32 | elf::R_PPC_UADDR32 | elf::R_PPC_REL24 | elf::R_PPC_REL14 => {
args.push(ObjInsArg::Reloc);
}
_ => bail!("Unsupported ELF PPC relocation type {r_type}"),
},
flags => bail!("Unsupported PPC relocation kind: {flags:?}"),
};
Ok(())
}

View File

@ -0,0 +1,341 @@
use std::borrow::Cow;
use anyhow::{anyhow, bail, ensure, Result};
use iced_x86::{
Decoder, DecoderOptions, DecoratorKind, Formatter, FormatterOutput, FormatterTextKind,
GasFormatter, Instruction, IntelFormatter, MasmFormatter, NasmFormatter, NumberKind, OpKind,
PrefixKind, Register,
};
use object::{pe, Endian, Endianness, File, Object, Relocation, RelocationFlags};
use crate::{
arch::{ObjArch, ProcessCodeResult},
diff::{DiffObjConfig, X86Formatter},
obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, SymbolRef},
};
pub struct ObjArchX86 {
bits: u32,
endianness: Endianness,
}
impl ObjArchX86 {
pub fn new(object: &File) -> Result<Self> {
Ok(Self { bits: if object.is_64() { 64 } else { 32 }, endianness: object.endianness() })
}
}
impl ObjArch for ObjArchX86 {
fn process_code(
&self,
obj: &ObjInfo,
symbol_ref: SymbolRef,
config: &DiffObjConfig,
) -> Result<ProcessCodeResult> {
let (section, symbol) = obj.section_symbol(symbol_ref);
let code = &section.data
[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
let mut result = ProcessCodeResult { ops: Vec::new(), insts: Vec::new() };
let mut decoder = Decoder::with_ip(self.bits, code, symbol.address, DecoderOptions::NONE);
let mut formatter: Box<dyn Formatter> = match config.x86_formatter {
X86Formatter::Intel => Box::new(IntelFormatter::new()),
X86Formatter::Gas => Box::new(GasFormatter::new()),
X86Formatter::Nasm => Box::new(NasmFormatter::new()),
X86Formatter::Masm => Box::new(MasmFormatter::new()),
};
formatter.options_mut().set_space_after_operand_separator(config.space_between_args);
let mut output = InstructionFormatterOutput {
formatted: String::new(),
ins: ObjIns {
address: 0,
size: 0,
op: 0,
mnemonic: String::new(),
args: vec![],
reloc: None,
branch_dest: None,
line: None,
orig: None,
},
error: None,
ins_operands: vec![],
};
let mut instruction = Instruction::default();
while decoder.can_decode() {
decoder.decode_out(&mut instruction);
let address = instruction.ip();
let op = instruction.mnemonic() as u16;
let reloc = section
.relocations
.iter()
.find(|r| r.address >= address && r.address < address + instruction.len() as u64);
output.ins = ObjIns {
address,
size: instruction.len() as u8,
op,
mnemonic: String::new(),
args: vec![],
reloc: reloc.cloned(),
branch_dest: None,
line: obj.line_info.as_ref().and_then(|m| m.get(&address).cloned()),
orig: None,
};
// Run the formatter, which will populate output.ins
formatter.format(&instruction, &mut output);
if let Some(error) = output.error.take() {
return Err(error);
}
ensure!(output.ins_operands.len() == output.ins.args.len());
output.ins.orig = Some(output.formatted.clone());
// Make sure we've put the relocation somewhere in the instruction
if reloc.is_some() && !output.ins.args.iter().any(|a| matches!(a, ObjInsArg::Reloc)) {
let mut found = replace_arg(
OpKind::Memory,
ObjInsArg::Reloc,
&mut output.ins.args,
&instruction,
&output.ins_operands,
)?;
if !found {
found = replace_arg(
OpKind::Immediate32,
ObjInsArg::Reloc,
&mut output.ins.args,
&instruction,
&output.ins_operands,
)?;
}
ensure!(found, "x86: Failed to find operand for Absolute relocation");
}
if reloc.is_some() && !output.ins.args.iter().any(|a| matches!(a, ObjInsArg::Reloc)) {
bail!("Failed to find relocation in instruction");
}
result.ops.push(op);
result.insts.push(output.ins.clone());
// Clear for next iteration
output.formatted.clear();
output.ins_operands.clear();
}
Ok(result)
}
fn implcit_addend(
&self,
section: &ObjSection,
address: u64,
reloc: &Relocation,
) -> Result<i64> {
match reloc.flags() {
RelocationFlags::Coff { typ: pe::IMAGE_REL_I386_DIR32 | pe::IMAGE_REL_I386_REL32 } => {
let data = section.data[address as usize..address as usize + 4].try_into()?;
Ok(self.endianness.read_i32_bytes(data) as i64)
}
flags => bail!("Unsupported x86 implicit relocation {flags:?}"),
}
}
fn demangle(&self, name: &str) -> Option<String> {
if name.starts_with('?') {
msvc_demangler::demangle(name, msvc_demangler::DemangleFlags::llvm()).ok()
} else {
cpp_demangle::Symbol::new(name)
.ok()
.and_then(|s| s.demangle(&cpp_demangle::DemangleOptions::default()).ok())
}
}
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
match flags {
RelocationFlags::Coff { typ } => match typ {
pe::IMAGE_REL_I386_DIR32 => Cow::Borrowed("IMAGE_REL_I386_DIR32"),
pe::IMAGE_REL_I386_REL32 => Cow::Borrowed("IMAGE_REL_I386_REL32"),
_ => Cow::Owned(format!("<{flags:?}>")),
},
_ => Cow::Owned(format!("<{flags:?}>")),
}
}
}
fn replace_arg(
from: OpKind,
to: ObjInsArg,
args: &mut [ObjInsArg],
instruction: &Instruction,
ins_operands: &[Option<u32>],
) -> Result<bool> {
let mut replace = None;
for i in 0..instruction.op_count() {
let op_kind = instruction.op_kind(i);
if op_kind == from {
replace = Some(i);
break;
}
}
if let Some(i) = replace {
for (j, arg) in args.iter_mut().enumerate() {
if ins_operands[j] == Some(i) {
*arg = to;
return Ok(true);
}
}
}
Ok(false)
}
struct InstructionFormatterOutput {
formatted: String,
ins: ObjIns,
error: Option<anyhow::Error>,
ins_operands: Vec<Option<u32>>,
}
impl InstructionFormatterOutput {
fn push_signed(&mut self, value: i64) {
// The formatter writes the '-' operator and then gives us a negative value,
// so convert it to a positive value to avoid double negatives
if value < 0
&& matches!(self.ins.args.last(), Some(ObjInsArg::Arg(ObjInsArgValue::Opaque(v))) if v == "-")
{
self.ins.args.push(ObjInsArg::Arg(ObjInsArgValue::Signed(value.wrapping_abs())));
} else {
self.ins.args.push(ObjInsArg::Arg(ObjInsArgValue::Signed(value)));
}
}
}
impl FormatterOutput for InstructionFormatterOutput {
fn write(&mut self, text: &str, kind: FormatterTextKind) {
self.formatted.push_str(text);
// Skip whitespace after the mnemonic
if self.ins.args.is_empty() && kind == FormatterTextKind::Text {
return;
}
self.ins_operands.push(None);
match kind {
FormatterTextKind::Text | FormatterTextKind::Punctuation => {
self.ins.args.push(ObjInsArg::PlainText(text.to_string().into()));
}
FormatterTextKind::Keyword | FormatterTextKind::Operator => {
self.ins.args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(text.to_string().into())));
}
_ => {
if self.error.is_none() {
self.error = Some(anyhow!("x86: Unsupported FormatterTextKind {:?}", kind));
}
}
}
}
fn write_prefix(&mut self, _instruction: &Instruction, text: &str, _prefix: PrefixKind) {
self.formatted.push_str(text);
self.ins_operands.push(None);
self.ins.args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(text.to_string().into())));
}
fn write_mnemonic(&mut self, _instruction: &Instruction, text: &str) {
self.formatted.push_str(text);
self.ins.mnemonic = text.to_string();
}
fn write_number(
&mut self,
_instruction: &Instruction,
_operand: u32,
instruction_operand: Option<u32>,
text: &str,
value: u64,
number_kind: NumberKind,
kind: FormatterTextKind,
) {
self.formatted.push_str(text);
self.ins_operands.push(instruction_operand);
// Handle relocations
match kind {
FormatterTextKind::LabelAddress => {
if let Some(reloc) = self.ins.reloc.as_ref() {
if matches!(reloc.flags, RelocationFlags::Coff {
typ: pe::IMAGE_REL_I386_DIR32
}) {
self.ins.args.push(ObjInsArg::Reloc);
return;
} else if self.error.is_none() {
self.error = Some(anyhow!(
"x86: Unsupported LabelAddress relocation flags {:?}",
reloc.flags
));
}
}
self.ins.args.push(ObjInsArg::BranchDest(value));
self.ins.branch_dest = Some(value);
return;
}
FormatterTextKind::FunctionAddress => {
if let Some(reloc) = self.ins.reloc.as_ref() {
if matches!(reloc.flags, RelocationFlags::Coff {
typ: pe::IMAGE_REL_I386_REL32
}) {
self.ins.args.push(ObjInsArg::Reloc);
return;
} else if self.error.is_none() {
self.error = Some(anyhow!(
"x86: Unsupported FunctionAddress relocation flags {:?}",
reloc.flags
));
}
}
}
_ => {}
}
match number_kind {
NumberKind::Int8 => {
self.push_signed(value as i8 as i64);
}
NumberKind::Int16 => {
self.push_signed(value as i16 as i64);
}
NumberKind::Int32 => {
self.push_signed(value as i32 as i64);
}
NumberKind::Int64 => {
self.push_signed(value as i64);
}
NumberKind::UInt8 | NumberKind::UInt16 | NumberKind::UInt32 | NumberKind::UInt64 => {
self.ins.args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(value)));
}
}
}
fn write_decorator(
&mut self,
_instruction: &Instruction,
_operand: u32,
instruction_operand: Option<u32>,
text: &str,
_decorator: DecoratorKind,
) {
self.formatted.push_str(text);
self.ins_operands.push(instruction_operand);
self.ins.args.push(ObjInsArg::PlainText(text.to_string().into()));
}
fn write_register(
&mut self,
_instruction: &Instruction,
_operand: u32,
instruction_operand: Option<u32>,
text: &str,
_register: Register,
) {
self.formatted.push_str(text);
self.ins_operands.push(instruction_operand);
self.ins.args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(text.to_string().into())));
}
}

View File

@ -0,0 +1,167 @@
use std::{
fs::File,
io::Read,
path::{Path, PathBuf},
};
use anyhow::{anyhow, Context, 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<String>,
#[serde(default)]
pub custom_make: Option<String>,
#[serde(default)]
pub custom_args: Option<Vec<String>>,
#[serde(default)]
pub target_dir: Option<PathBuf>,
#[serde(default)]
pub base_dir: Option<PathBuf>,
#[serde(default = "bool_true")]
pub build_base: bool,
#[serde(default)]
pub build_target: bool,
#[serde(default)]
pub watch_patterns: Option<Vec<Glob>>,
#[serde(default, alias = "units")]
pub objects: Vec<ProjectObject>,
}
#[derive(Default, Clone, serde::Deserialize)]
pub struct ProjectObject {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub path: Option<PathBuf>,
#[serde(default)]
pub target_path: Option<PathBuf>,
#[serde(default)]
pub base_path: Option<PathBuf>,
#[serde(default)]
pub reverse_fn_order: Option<bool>,
#[serde(default)]
pub complete: Option<bool>,
#[serde(default)]
pub scratch: Option<ScratchConfig>,
}
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<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,
}
pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"];
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<ProjectConfig>, 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 mut result = match filename.contains("json") {
true => read_json_config(&mut file),
false => read_yml_config(&mut file),
};
if let Ok(config) = &result {
// Validate min_version if present
if let Err(e) = validate_min_version(config) {
result = Err(e);
}
}
return Some((result, ProjectConfigInfo { path: config_path, timestamp: ts }));
}
}
None
}
fn validate_min_version(config: &ProjectConfig) -> Result<()> {
let Some(min_version) = &config.min_version else { return Ok(()) };
let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))
.context("Failed to parse package version")?;
match semver::VersionReq::parse(&format!(">={min_version}")) {
Ok(version_req) if version_req.matches(&version) => Ok(()),
Ok(_) => Err(anyhow!("Project requires objdiff version {min_version} or higher")),
Err(e) => Err(anyhow::Error::new(e).context("Failed to parse min_version")),
}
}
fn read_yml_config<R: Read>(reader: &mut R) -> Result<ProjectConfig> {
Ok(serde_yaml::from_reader(reader)?)
}
fn read_json_config<R: Read>(reader: &mut R) -> Result<ProjectConfig> {
Ok(serde_json::from_reader(reader)?)
}
pub fn build_globset(vec: &[Glob]) -> std::result::Result<GlobSet, globset::Error> {
let mut builder = GlobSetBuilder::new();
for glob in vec {
builder.add(glob.clone());
}
builder.build()
}

View File

@ -0,0 +1,327 @@
use std::{
cmp::max,
collections::BTreeMap,
time::{Duration, Instant},
};
use anyhow::Result;
use similar::{capture_diff_slices_deadline, Algorithm};
use crate::{
arch::ProcessCodeResult,
diff::{
DiffObjConfig, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjInsDiff, ObjInsDiffKind,
ObjSymbolDiff,
},
obj::{ObjInfo, ObjInsArg, ObjReloc, ObjSymbol, ObjSymbolFlags, SymbolRef},
};
pub fn no_diff_code(
obj: &ObjInfo,
symbol_ref: SymbolRef,
config: &DiffObjConfig,
) -> Result<ObjSymbolDiff> {
let out = obj.arch.process_code(obj, symbol_ref, config)?;
let mut diff = Vec::<ObjInsDiff>::new();
for i in out.insts {
diff.push(ObjInsDiff { ins: Some(i), kind: ObjInsDiffKind::None, ..Default::default() });
}
resolve_branches(&mut diff);
Ok(ObjSymbolDiff { diff_symbol: None, instructions: diff, match_percent: None })
}
pub fn diff_code(
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left_symbol_ref: SymbolRef,
right_symbol_ref: SymbolRef,
config: &DiffObjConfig,
) -> Result<(ObjSymbolDiff, ObjSymbolDiff)> {
let left_out = left_obj.arch.process_code(left_obj, left_symbol_ref, config)?;
let right_out = left_obj.arch.process_code(right_obj, right_symbol_ref, config)?;
let mut left_diff = Vec::<ObjInsDiff>::new();
let mut right_diff = Vec::<ObjInsDiff>::new();
diff_instructions(&mut left_diff, &mut right_diff, &left_out, &right_out)?;
resolve_branches(&mut left_diff);
resolve_branches(&mut right_diff);
let mut diff_state = InsDiffState::default();
for (left, right) in left_diff.iter_mut().zip(right_diff.iter_mut()) {
let result = compare_ins(config, left, right, &mut diff_state)?;
left.kind = result.kind;
right.kind = result.kind;
left.arg_diff = result.left_args_diff;
right.arg_diff = result.right_args_diff;
}
let total = left_out.insts.len();
let percent = if diff_state.diff_count >= total {
0.0
} else {
((total - diff_state.diff_count) as f32 / total as f32) * 100.0
};
Ok((
ObjSymbolDiff {
diff_symbol: Some(right_symbol_ref),
instructions: left_diff,
match_percent: Some(percent),
},
ObjSymbolDiff {
diff_symbol: Some(left_symbol_ref),
instructions: right_diff,
match_percent: Some(percent),
},
))
}
fn diff_instructions(
left_diff: &mut Vec<ObjInsDiff>,
right_diff: &mut Vec<ObjInsDiff>,
left_code: &ProcessCodeResult,
right_code: &ProcessCodeResult,
) -> Result<()> {
let deadline = Instant::now() + Duration::from_secs(5);
let ops = capture_diff_slices_deadline(
Algorithm::Patience,
&left_code.ops,
&right_code.ops,
Some(deadline),
);
if ops.is_empty() {
left_diff.extend(
left_code
.insts
.iter()
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
);
right_diff.extend(
right_code
.insts
.iter()
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
);
return Ok(());
}
for op in ops {
let (_tag, left_range, right_range) = op.as_tag_tuple();
let len = max(left_range.len(), right_range.len());
left_diff.extend(
left_code.insts[left_range.clone()]
.iter()
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
);
right_diff.extend(
right_code.insts[right_range.clone()]
.iter()
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
);
if left_range.len() < len {
left_diff.extend((left_range.len()..len).map(|_| ObjInsDiff::default()));
}
if right_range.len() < len {
right_diff.extend((right_range.len()..len).map(|_| ObjInsDiff::default()));
}
}
Ok(())
}
fn resolve_branches(vec: &mut [ObjInsDiff]) {
let mut branch_idx = 0usize;
// Map addresses to indices
let mut addr_map = BTreeMap::<u64, usize>::new();
for (i, ins_diff) in vec.iter().enumerate() {
if let Some(ins) = &ins_diff.ins {
addr_map.insert(ins.address, i);
}
}
// Generate branches
let mut branches = BTreeMap::<usize, ObjInsBranchFrom>::new();
for (i, ins_diff) in vec.iter_mut().enumerate() {
if let Some(ins) = &ins_diff.ins {
if let Some(ins_idx) = ins.branch_dest.and_then(|a| addr_map.get(&a)) {
if let Some(branch) = branches.get_mut(ins_idx) {
ins_diff.branch_to =
Some(ObjInsBranchTo { ins_idx: *ins_idx, branch_idx: branch.branch_idx });
branch.ins_idx.push(i);
} else {
ins_diff.branch_to = Some(ObjInsBranchTo { ins_idx: *ins_idx, branch_idx });
branches.insert(*ins_idx, ObjInsBranchFrom { ins_idx: vec![i], branch_idx });
branch_idx += 1;
}
}
}
}
// Store branch from
for (i, branch) in branches {
vec[i].branch_from = Some(branch);
}
}
fn address_eq(left: &ObjSymbol, right: &ObjSymbol) -> bool {
left.address as i64 + left.addend == right.address as i64 + right.addend
}
fn reloc_eq(
config: &DiffObjConfig,
left_reloc: Option<&ObjReloc>,
right_reloc: Option<&ObjReloc>,
) -> bool {
let (Some(left), Some(right)) = (left_reloc, right_reloc) else {
return false;
};
if left.flags != right.flags {
return false;
}
if config.relax_reloc_diffs {
return true;
}
let name_matches = left.target.name == right.target.name;
match (&left.target_section, &right.target_section) {
(Some(sl), Some(sr)) => {
// Match if section and name or address match
sl == sr && (name_matches || address_eq(&left.target, &right.target))
}
(Some(_), None) => false,
(None, Some(_)) => {
// Match if possibly stripped weak symbol
name_matches && right.target.flags.0.contains(ObjSymbolFlags::Weak)
}
(None, None) => name_matches,
}
}
fn arg_eq(
config: &DiffObjConfig,
left: &ObjInsArg,
right: &ObjInsArg,
left_diff: &ObjInsDiff,
right_diff: &ObjInsDiff,
) -> bool {
return match left {
ObjInsArg::PlainText(l) => match right {
ObjInsArg::PlainText(r) => l == r,
_ => false,
},
ObjInsArg::Arg(l) => match right {
ObjInsArg::Arg(r) => l == r,
// 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,
},
ObjInsArg::Reloc => {
matches!(right, ObjInsArg::Reloc)
&& reloc_eq(
config,
left_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(_) => {
// Compare dest instruction idx after diffing
left_diff.branch_to.as_ref().map(|b| b.ins_idx)
== right_diff.branch_to.as_ref().map(|b| b.ins_idx)
}
};
}
#[derive(Default)]
struct InsDiffState {
diff_count: usize,
left_arg_idx: usize,
right_arg_idx: usize,
left_args_idx: BTreeMap<String, usize>,
right_args_idx: BTreeMap<String, usize>,
}
#[derive(Default)]
struct InsDiffResult {
kind: ObjInsDiffKind,
left_args_diff: Vec<Option<ObjInsArgDiff>>,
right_args_diff: Vec<Option<ObjInsArgDiff>>,
}
fn compare_ins(
config: &DiffObjConfig,
left: &ObjInsDiff,
right: &ObjInsDiff,
state: &mut InsDiffState,
) -> Result<InsDiffResult> {
let mut result = InsDiffResult::default();
if let (Some(left_ins), Some(right_ins)) = (&left.ins, &right.ins) {
if left_ins.args.len() != right_ins.args.len()
|| left_ins.op != right_ins.op
// Check if any PlainText segments differ (punctuation and spacing)
// This indicates a more significant difference than a simple arg mismatch
|| !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
result.kind = ObjInsDiffKind::Replace;
state.diff_count += 1;
return Ok(result);
}
if left_ins.mnemonic != right_ins.mnemonic {
// Same op but different mnemonic, still cmp args
result.kind = ObjInsDiffKind::OpMismatch;
state.diff_count += 1;
}
for (a, b) in left_ins.args.iter().zip(&right_ins.args) {
if arg_eq(config, a, b, left, right) {
result.left_args_diff.push(None);
result.right_args_diff.push(None);
} else {
if result.kind == ObjInsDiffKind::None {
result.kind = ObjInsDiffKind::ArgMismatch;
state.diff_count += 1;
}
let a_str = match a {
ObjInsArg::PlainText(arg) => arg.to_string(),
ObjInsArg::Arg(arg) => arg.to_string(),
ObjInsArg::Reloc => String::new(),
ObjInsArg::BranchDest(arg) => format!("{arg}"),
};
let a_diff = if let Some(idx) = state.left_args_idx.get(&a_str) {
ObjInsArgDiff { idx: *idx }
} else {
let idx = state.left_arg_idx;
state.left_args_idx.insert(a_str, idx);
state.left_arg_idx += 1;
ObjInsArgDiff { idx }
};
let b_str = match b {
ObjInsArg::PlainText(arg) => arg.to_string(),
ObjInsArg::Arg(arg) => arg.to_string(),
ObjInsArg::Reloc => String::new(),
ObjInsArg::BranchDest(arg) => format!("{arg}"),
};
let b_diff = if let Some(idx) = state.right_args_idx.get(&b_str) {
ObjInsArgDiff { idx: *idx }
} else {
let idx = state.right_arg_idx;
state.right_args_idx.insert(b_str, idx);
state.right_arg_idx += 1;
ObjInsArgDiff { idx }
};
result.left_args_diff.push(Some(a_diff));
result.right_args_diff.push(Some(b_diff));
}
}
} else if left.ins.is_some() {
result.kind = ObjInsDiffKind::Delete;
state.diff_count += 1;
} else {
result.kind = ObjInsDiffKind::Insert;
state.diff_count += 1;
}
Ok(result)
}

View File

@ -0,0 +1,131 @@
use std::{
cmp::{max, min, Ordering},
time::{Duration, Instant},
};
use anyhow::Result;
use similar::{capture_diff_slices_deadline, Algorithm};
use crate::{
diff::{ObjDataDiff, ObjDataDiffKind, ObjSectionDiff, ObjSymbolDiff},
obj::{ObjInfo, ObjSection, SymbolRef},
};
pub fn diff_bss_symbol(
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left_symbol_ref: SymbolRef,
right_symbol_ref: SymbolRef,
) -> Result<(ObjSymbolDiff, ObjSymbolDiff)> {
let (_, left_symbol) = left_obj.section_symbol(left_symbol_ref);
let (_, right_symbol) = right_obj.section_symbol(right_symbol_ref);
let percent = if left_symbol.size == right_symbol.size { 100.0 } else { 50.0 };
Ok((
ObjSymbolDiff {
diff_symbol: Some(right_symbol_ref),
instructions: vec![],
match_percent: Some(percent),
},
ObjSymbolDiff {
diff_symbol: Some(left_symbol_ref),
instructions: vec![],
match_percent: Some(percent),
},
))
}
pub fn no_diff_bss_symbol(_obj: &ObjInfo, _symbol_ref: SymbolRef) -> ObjSymbolDiff {
ObjSymbolDiff { diff_symbol: None, instructions: vec![], match_percent: Some(0.0) }
}
pub fn diff_data(
left: &ObjSection,
right: &ObjSection,
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
let deadline = Instant::now() + Duration::from_secs(5);
let ops =
capture_diff_slices_deadline(Algorithm::Patience, &left.data, &right.data, Some(deadline));
let mut left_diff = Vec::<ObjDataDiff>::new();
let mut right_diff = Vec::<ObjDataDiff>::new();
for op in ops {
let (tag, left_range, right_range) = op.as_tag_tuple();
let left_len = left_range.len();
let right_len = right_range.len();
let mut len = max(left_len, right_len);
let kind = match tag {
similar::DiffTag::Equal => ObjDataDiffKind::None,
similar::DiffTag::Delete => ObjDataDiffKind::Delete,
similar::DiffTag::Insert => ObjDataDiffKind::Insert,
similar::DiffTag::Replace => {
// Ensure replacements are equal length
len = min(left_len, right_len);
ObjDataDiffKind::Replace
}
};
let left_data = &left.data[left_range];
let right_data = &right.data[right_range];
left_diff.push(ObjDataDiff {
data: left_data[..min(len, left_data.len())].to_vec(),
kind,
len,
..Default::default()
});
right_diff.push(ObjDataDiff {
data: right_data[..min(len, right_data.len())].to_vec(),
kind,
len,
..Default::default()
});
if kind == ObjDataDiffKind::Replace {
match left_len.cmp(&right_len) {
Ordering::Less => {
let len = right_len - left_len;
left_diff.push(ObjDataDiff {
data: vec![],
kind: ObjDataDiffKind::Insert,
len,
..Default::default()
});
right_diff.push(ObjDataDiff {
data: right_data[left_len..right_len].to_vec(),
kind: ObjDataDiffKind::Insert,
len,
..Default::default()
});
}
Ordering::Greater => {
let len = left_len - right_len;
left_diff.push(ObjDataDiff {
data: left_data[right_len..left_len].to_vec(),
kind: ObjDataDiffKind::Delete,
len,
..Default::default()
});
right_diff.push(ObjDataDiff {
data: vec![],
kind: ObjDataDiffKind::Delete,
len,
..Default::default()
});
}
Ordering::Equal => {}
}
}
}
Ok((
ObjSectionDiff {
symbols: vec![],
data_diff: left_diff,
// TODO
match_percent: None,
},
ObjSectionDiff {
symbols: vec![],
data_diff: right_diff,
// TODO
match_percent: None,
},
))
}

View File

@ -0,0 +1,130 @@
use std::cmp::Ordering;
use crate::{
diff::{ObjInsArgDiff, ObjInsDiff},
obj::{ObjInsArg, ObjInsArgValue, ObjReloc, 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(u64),
/// Instruction mnemonic
Opcode(&'a str, u16),
/// Instruction argument
Argument(&'a ObjInsArgValue, Option<&'a ObjInsArgDiff>),
/// Branch destination
BranchDest(u64),
/// Symbol name
Symbol(&'a ObjSymbol),
/// Number of spaces
Spacing(usize),
/// End of line
Eol,
}
#[derive(Default, Clone, PartialEq, Eq)]
pub enum HighlightKind {
#[default]
None,
Opcode(u16),
Arg(ObjInsArgValue),
Symbol(String),
Address(u64),
}
pub fn display_diff<E>(
ins_diff: &ObjInsDiff,
base_addr: u64,
mut cb: impl FnMut(DiffText) -> Result<(), E>,
) -> Result<(), E> {
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))?;
for (i, arg) in ins.args.iter().enumerate() {
if i == 0 {
cb(DiffText::Spacing(1))?;
}
match arg {
ObjInsArg::PlainText(s) => {
cb(DiffText::Basic(s))?;
}
ObjInsArg::Arg(v) => {
let diff = ins_diff.arg_diff.get(i).and_then(|o| o.as_ref());
cb(DiffText::Argument(v, diff))?;
}
ObjInsArg::Reloc => {
display_reloc_name(ins.reloc.as_ref().unwrap(), &mut cb)?;
}
ObjInsArg::BranchDest(dest) => {
if let Some(dest) = dest.checked_sub(base_addr) {
cb(DiffText::BranchDest(dest))?;
} else {
cb(DiffText::Basic("<unknown>"))?;
}
}
}
}
if let Some(branch) = &ins_diff.branch_to {
cb(DiffText::BasicColor(" ~>", branch.branch_idx))?;
}
cb(DiffText::Eol)?;
Ok(())
}
fn display_reloc_name<E>(
reloc: &ObjReloc,
mut cb: impl FnMut(DiffText) -> Result<(), E>,
) -> Result<(), E> {
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(()),
}
}
impl PartialEq<DiffText<'_>> for HighlightKind {
fn eq(&self, other: &DiffText) -> bool {
match (self, other) {
(HighlightKind::Opcode(a), DiffText::Opcode(_, b)) => a == b,
(HighlightKind::Arg(a), DiffText::Argument(b, _)) => a.loose_eq(b),
(HighlightKind::Symbol(a), DiffText::Symbol(b)) => a == &b.name,
(HighlightKind::Address(a), DiffText::Address(b) | DiffText::BranchDest(b)) => a == b,
_ => false,
}
}
}
impl PartialEq<HighlightKind> for DiffText<'_> {
fn eq(&self, other: &HighlightKind) -> bool { other.eq(self) }
}
impl From<DiffText<'_>> for HighlightKind {
fn from(value: DiffText<'_>) -> Self {
match value {
DiffText::Opcode(_, op) => HighlightKind::Opcode(op),
DiffText::Argument(arg, _) => HighlightKind::Arg(arg.clone()),
DiffText::Symbol(sym) => HighlightKind::Symbol(sym.name.to_string()),
DiffText::Address(addr) | DiffText::BranchDest(addr) => HighlightKind::Address(addr),
_ => HighlightKind::None,
}
}
}

View File

@ -0,0 +1,445 @@
mod code;
mod data;
pub mod display;
use std::collections::HashSet;
use anyhow::Result;
use crate::{
diff::{
code::{diff_code, no_diff_code},
data::{diff_bss_symbol, diff_data, no_diff_bss_symbol},
},
obj::{ObjInfo, ObjIns, ObjSectionKind, SymbolRef},
};
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum X86Formatter {
#[default]
Intel,
Gas,
Nasm,
Masm,
}
#[inline]
const fn default_true() -> bool { true }
#[derive(Debug, Clone, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct DiffObjConfig {
pub relax_reloc_diffs: bool,
#[serde(default = "default_true")]
pub space_between_args: bool,
pub x86_formatter: X86Formatter,
}
impl DiffObjConfig {
pub fn separator(&self) -> &'static str {
if self.space_between_args {
", "
} else {
","
}
}
}
#[derive(Debug, Clone)]
pub struct ObjSectionDiff {
pub symbols: Vec<ObjSymbolDiff>,
pub data_diff: Vec<ObjDataDiff>,
pub match_percent: Option<f32>,
}
impl ObjSectionDiff {
fn merge(&mut self, other: ObjSectionDiff) {
// symbols ignored
self.data_diff = other.data_diff;
self.match_percent = other.match_percent;
}
}
#[derive(Debug, Clone, Default)]
pub struct ObjSymbolDiff {
pub diff_symbol: Option<SymbolRef>,
pub instructions: Vec<ObjInsDiff>,
pub match_percent: Option<f32>,
}
#[derive(Debug, Clone, Default)]
pub struct ObjInsDiff {
pub ins: Option<ObjIns>,
/// Diff kind
pub kind: ObjInsDiffKind,
/// Branches from instruction
pub branch_from: Option<ObjInsBranchFrom>,
/// Branches to instruction
pub branch_to: Option<ObjInsBranchTo>,
/// Arg diffs
pub arg_diff: Vec<Option<ObjInsArgDiff>>,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
pub enum ObjInsDiffKind {
#[default]
None,
OpMismatch,
ArgMismatch,
Replace,
Delete,
Insert,
}
#[derive(Debug, Clone, Default)]
pub struct ObjDataDiff {
pub data: Vec<u8>,
pub kind: ObjDataDiffKind,
pub len: usize,
pub symbol: String,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
pub enum ObjDataDiffKind {
#[default]
None,
Replace,
Delete,
Insert,
}
#[derive(Debug, Copy, Clone)]
pub struct ObjInsArgDiff {
/// Incrementing index for coloring
pub idx: usize,
}
#[derive(Debug, Clone)]
pub struct ObjInsBranchFrom {
/// Source instruction indices
pub ins_idx: Vec<usize>,
/// Incrementing index for coloring
pub branch_idx: usize,
}
#[derive(Debug, Clone)]
pub struct ObjInsBranchTo {
/// Target instruction index
pub ins_idx: usize,
/// Incrementing index for coloring
pub branch_idx: usize,
}
#[derive(Default)]
pub struct ObjDiff {
pub sections: Vec<ObjSectionDiff>,
pub common: Vec<ObjSymbolDiff>,
}
impl ObjDiff {
pub fn new_from_obj(obj: &ObjInfo) -> Self {
let mut result = Self {
sections: Vec::with_capacity(obj.sections.len()),
common: Vec::with_capacity(obj.common.len()),
};
for section in &obj.sections {
let mut symbols = Vec::with_capacity(section.symbols.len());
for _ in &section.symbols {
symbols.push(ObjSymbolDiff {
diff_symbol: None,
instructions: vec![],
match_percent: None,
});
}
result.sections.push(ObjSectionDiff {
symbols,
data_diff: vec![ObjDataDiff {
data: section.data.clone(),
kind: ObjDataDiffKind::None,
len: section.data.len(),
symbol: section.name.clone(),
}],
match_percent: None,
});
}
for _ in &obj.common {
result.common.push(ObjSymbolDiff {
diff_symbol: None,
instructions: vec![],
match_percent: None,
});
}
result
}
#[inline]
pub fn section_diff(&self, section_idx: usize) -> &ObjSectionDiff {
&self.sections[section_idx]
}
#[inline]
pub fn section_diff_mut(&mut self, section_idx: usize) -> &mut ObjSectionDiff {
&mut self.sections[section_idx]
}
#[inline]
pub fn symbol_diff(&self, symbol_ref: SymbolRef) -> &ObjSymbolDiff {
&self.section_diff(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx]
}
#[inline]
pub fn symbol_diff_mut(&mut self, symbol_ref: SymbolRef) -> &mut ObjSymbolDiff {
&mut self.section_diff_mut(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx]
}
}
#[derive(Default)]
pub struct DiffObjsResult {
pub left: Option<ObjDiff>,
pub right: Option<ObjDiff>,
pub prev: Option<ObjDiff>,
}
pub fn diff_objs(
config: &DiffObjConfig,
left: Option<&ObjInfo>,
right: Option<&ObjInfo>,
prev: Option<&ObjInfo>,
) -> Result<DiffObjsResult> {
let symbol_matches = matching_symbols(left, right, prev)?;
let section_matches = matching_sections(left, right)?;
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 prev = prev.map(|p| (p, ObjDiff::new_from_obj(p)));
for symbol_match in symbol_matches {
match symbol_match {
SymbolMatch {
left: Some(left_symbol_ref),
right: Some(right_symbol_ref),
prev: prev_symbol_ref,
section_kind,
} => {
let (left_obj, left_out) = left.as_mut().unwrap();
let (right_obj, right_out) = right.as_mut().unwrap();
match section_kind {
ObjSectionKind::Code => {
let (left_diff, right_diff) = diff_code(
left_obj,
right_obj,
left_symbol_ref,
right_symbol_ref,
config,
)?;
*left_out.symbol_diff_mut(left_symbol_ref) = left_diff;
*right_out.symbol_diff_mut(right_symbol_ref) = right_diff;
if let Some(prev_symbol_ref) = prev_symbol_ref {
let (prev_obj, prev_out) = prev.as_mut().unwrap();
let (_, prev_diff) = diff_code(
right_obj,
prev_obj,
right_symbol_ref,
prev_symbol_ref,
config,
)?;
*prev_out.symbol_diff_mut(prev_symbol_ref) = prev_diff;
}
}
ObjSectionKind::Data => {
// TODO diff data symbol
}
ObjSectionKind::Bss => {
let (left_diff, right_diff) = diff_bss_symbol(
left_obj,
right_obj,
left_symbol_ref,
right_symbol_ref,
)?;
*left_out.symbol_diff_mut(left_symbol_ref) = left_diff;
*right_out.symbol_diff_mut(right_symbol_ref) = right_diff;
}
}
}
SymbolMatch { left: Some(left_symbol_ref), right: None, prev: _, section_kind } => {
let (left_obj, left_out) = left.as_mut().unwrap();
match section_kind {
ObjSectionKind::Code => {
*left_out.symbol_diff_mut(left_symbol_ref) =
no_diff_code(left_obj, left_symbol_ref, config)?;
}
ObjSectionKind::Data => {}
ObjSectionKind::Bss => {
*left_out.symbol_diff_mut(left_symbol_ref) =
no_diff_bss_symbol(left_obj, left_symbol_ref);
}
}
}
SymbolMatch { left: None, right: Some(right_symbol_ref), prev: _, section_kind } => {
let (right_obj, right_out) = right.as_mut().unwrap();
match section_kind {
ObjSectionKind::Code => {
*right_out.symbol_diff_mut(right_symbol_ref) =
no_diff_code(right_obj, right_symbol_ref, config)?;
}
ObjSectionKind::Data => {}
ObjSectionKind::Bss => {
*right_out.symbol_diff_mut(right_symbol_ref) =
no_diff_bss_symbol(right_obj, right_symbol_ref);
}
}
}
SymbolMatch { left: None, right: None, .. } => {
// Should not happen
}
}
}
for section_match in section_matches {
if let SectionMatch {
left: Some(left_section_idx),
right: Some(right_section_idx),
section_kind,
} = section_match
{
let (left_obj, left_out) = left.as_mut().unwrap();
let (right_obj, right_out) = right.as_mut().unwrap();
let left_section = &left_obj.sections[left_section_idx];
let right_section = &right_obj.sections[right_section_idx];
match section_kind {
ObjSectionKind::Code => {
// TODO?
}
ObjSectionKind::Data => {
let (left_diff, right_diff) = diff_data(left_section, right_section)?;
left_out.section_diff_mut(left_section_idx).merge(left_diff);
right_out.section_diff_mut(right_section_idx).merge(right_diff);
}
ObjSectionKind::Bss => {
// TODO
}
}
}
}
Ok(DiffObjsResult {
left: left.map(|(_, o)| o),
right: right.map(|(_, o)| o),
prev: prev.map(|(_, o)| o),
})
}
#[derive(Copy, Clone, Eq, PartialEq)]
struct SymbolMatch {
left: Option<SymbolRef>,
right: Option<SymbolRef>,
prev: Option<SymbolRef>,
section_kind: ObjSectionKind,
}
#[derive(Copy, Clone, Eq, PartialEq)]
struct SectionMatch {
left: Option<usize>,
right: Option<usize>,
section_kind: ObjSectionKind,
}
/// Find matching symbols between each object.
fn matching_symbols(
left: Option<&ObjInfo>,
right: Option<&ObjInfo>,
prev: Option<&ObjInfo>,
) -> Result<Vec<SymbolMatch>> {
let mut matches = Vec::new();
let mut right_used = HashSet::new();
if let Some(left) = left {
for (section_idx, section) in left.sections.iter().enumerate() {
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
let symbol_match = SymbolMatch {
left: Some(SymbolRef { section_idx, symbol_idx }),
right: find_symbol(right, &symbol.name, section.kind),
prev: find_symbol(prev, &symbol.name, section.kind),
section_kind: section.kind,
};
matches.push(symbol_match);
if let Some(right) = symbol_match.right {
right_used.insert(right);
}
}
}
}
if let Some(right) = right {
for (section_idx, section) in right.sections.iter().enumerate() {
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
let symbol_ref = SymbolRef { section_idx, symbol_idx };
if right_used.contains(&symbol_ref) {
continue;
}
matches.push(SymbolMatch {
left: None,
right: Some(symbol_ref),
prev: find_symbol(prev, &symbol.name, section.kind),
section_kind: section.kind,
});
}
}
}
Ok(matches)
}
fn find_symbol(
obj: Option<&ObjInfo>,
name: &str,
section_kind: ObjSectionKind,
) -> Option<SymbolRef> {
for (section_idx, section) in obj?.sections.iter().enumerate() {
if section.kind != section_kind {
continue;
}
let symbol_idx = match section.symbols.iter().position(|symbol| symbol.name == name) {
Some(symbol_idx) => symbol_idx,
None => continue,
};
return Some(SymbolRef { section_idx, symbol_idx });
}
None
}
/// Find matching sections between each object.
fn matching_sections(left: Option<&ObjInfo>, right: Option<&ObjInfo>) -> Result<Vec<SectionMatch>> {
let mut matches = Vec::new();
if let Some(left) = left {
for (section_idx, section) in left.sections.iter().enumerate() {
matches.push(SectionMatch {
left: Some(section_idx),
right: find_section(right, &section.name, section.kind),
section_kind: section.kind,
});
}
}
if let Some(right) = right {
for (section_idx, section) in right.sections.iter().enumerate() {
if matches.iter().any(|m| m.right == Some(section_idx)) {
continue;
}
matches.push(SectionMatch {
left: None,
right: Some(section_idx),
section_kind: section.kind,
});
}
}
Ok(matches)
}
fn find_section(obj: Option<&ObjInfo>, name: &str, section_kind: ObjSectionKind) -> Option<usize> {
for (section_idx, section) in obj?.sections.iter().enumerate() {
if section.kind != section_kind {
continue;
}
if section.name == name {
return Some(section_idx);
}
}
None
}

9
objdiff-core/src/lib.rs Normal file
View File

@ -0,0 +1,9 @@
pub mod arch;
#[cfg(feature = "config")]
pub mod config;
pub mod diff;
pub mod obj;
pub mod util;
#[cfg(not(feature = "any-arch"))]
compile_error!("At least one architecture feature must be enabled.");

155
objdiff-core/src/obj/mod.rs Normal file
View File

@ -0,0 +1,155 @@
pub mod read;
pub mod split_meta;
use std::{borrow::Cow, collections::BTreeMap, fmt, path::PathBuf};
use filetime::FileTime;
use flagset::{flags, FlagSet};
use object::RelocationFlags;
use split_meta::SplitMeta;
use crate::{arch::ObjArch, util::ReallySigned};
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum ObjSectionKind {
Code,
Data,
Bss,
}
flags! {
pub enum ObjSymbolFlags: u8 {
Global,
Local,
Weak,
Common,
Hidden,
}
}
#[derive(Debug, Copy, Clone, Default)]
pub struct ObjSymbolFlagSet(pub FlagSet<ObjSymbolFlags>);
#[derive(Debug, Clone)]
pub struct ObjSection {
pub name: String,
pub kind: ObjSectionKind,
pub address: u64,
pub size: u64,
pub data: Vec<u8>,
pub orig_index: usize,
pub symbols: Vec<ObjSymbol>,
pub relocations: Vec<ObjReloc>,
pub virtual_address: Option<u64>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ObjInsArgValue {
Signed(i64),
Unsigned(u64),
Opaque(Cow<'static, str>),
}
impl ObjInsArgValue {
pub fn loose_eq(&self, other: &ObjInsArgValue) -> bool {
match (self, other) {
(ObjInsArgValue::Signed(a), ObjInsArgValue::Signed(b)) => a == b,
(ObjInsArgValue::Unsigned(a), ObjInsArgValue::Unsigned(b)) => a == b,
(ObjInsArgValue::Signed(a), ObjInsArgValue::Unsigned(b))
| (ObjInsArgValue::Unsigned(b), ObjInsArgValue::Signed(a)) => *a as u64 == *b,
(ObjInsArgValue::Opaque(a), ObjInsArgValue::Opaque(b)) => a == b,
_ => false,
}
}
}
impl fmt::Display for ObjInsArgValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ObjInsArgValue::Signed(v) => write!(f, "{:#x}", ReallySigned(*v)),
ObjInsArgValue::Unsigned(v) => write!(f, "{:#x}", v),
ObjInsArgValue::Opaque(v) => write!(f, "{}", v),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ObjInsArg {
PlainText(Cow<'static, str>),
Arg(ObjInsArgValue),
Reloc,
BranchDest(u64),
}
impl ObjInsArg {
pub fn loose_eq(&self, other: &ObjInsArg) -> bool {
match (self, other) {
(ObjInsArg::Arg(a), ObjInsArg::Arg(b)) => a.loose_eq(b),
(ObjInsArg::Reloc, ObjInsArg::Reloc) => true,
(ObjInsArg::BranchDest(a), ObjInsArg::BranchDest(b)) => a == b,
_ => false,
}
}
}
#[derive(Debug, Clone)]
pub struct ObjIns {
pub address: u64,
pub size: u8,
pub op: u16,
pub mnemonic: String,
pub args: Vec<ObjInsArg>,
pub reloc: Option<ObjReloc>,
pub branch_dest: Option<u64>,
/// Line number
pub line: Option<u64>,
/// Original (unsimplified) instruction
pub orig: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ObjSymbol {
pub name: String,
pub demangled_name: Option<String>,
pub address: u64,
pub section_address: u64,
pub size: u64,
pub size_known: bool,
pub flags: ObjSymbolFlagSet,
pub addend: i64,
/// Original virtual address (from .note.split section)
pub virtual_address: Option<u64>,
}
pub struct ObjInfo {
pub arch: Box<dyn ObjArch>,
pub path: PathBuf,
pub timestamp: FileTime,
pub sections: Vec<ObjSection>,
/// Common BSS symbols
pub common: Vec<ObjSymbol>,
/// Line number info (.line or .debug_line section)
pub line_info: Option<BTreeMap<u64, u64>>,
/// Split object metadata (.note.split section)
pub split_meta: Option<SplitMeta>,
}
#[derive(Debug, Clone)]
pub struct ObjReloc {
pub flags: RelocationFlags,
pub address: u64,
pub target: ObjSymbol,
pub target_section: Option<String>,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SymbolRef {
pub section_idx: usize,
pub symbol_idx: usize,
}
impl ObjInfo {
pub fn section_symbol(&self, symbol_ref: SymbolRef) -> (&ObjSection, &ObjSymbol) {
let section = &self.sections[symbol_ref.section_idx];
let symbol = &section.symbols[symbol_ref.symbol_idx];
(section, symbol)
}
}

View File

@ -1,18 +1,20 @@
use std::{collections::BTreeMap, fs, io::Cursor, path::Path};
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{anyhow, bail, ensure, Context, Result};
use byteorder::{BigEndian, ReadBytesExt};
use cwdemangle::demangle;
use filetime::FileTime;
use flagset::Flags;
use object::{
elf, Architecture, File, Object, ObjectSection, ObjectSymbol, RelocationKind, RelocationTarget,
SectionIndex, SectionKind, Symbol, SymbolKind, SymbolScope, SymbolSection,
BinaryFormat, File, Object, ObjectSection, ObjectSymbol, RelocationTarget, SectionIndex,
SectionKind, Symbol, SymbolKind, SymbolScope, SymbolSection,
};
use crate::obj::{
ObjArchitecture, ObjInfo, ObjReloc, ObjRelocKind, ObjSection, ObjSectionKind, ObjSymbol,
ObjSymbolFlagSet, ObjSymbolFlags,
use crate::{
arch::{new_arch, ObjArch},
obj::{
split_meta::{SplitMeta, SPLITMETA_SECTION},
ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags,
},
};
fn to_obj_section_kind(kind: SectionKind) -> Option<ObjSectionKind> {
@ -24,7 +26,13 @@ fn to_obj_section_kind(kind: SectionKind) -> Option<ObjSectionKind> {
}
}
fn to_obj_symbol(obj_file: &File<'_>, symbol: &Symbol<'_, '_>, addend: i64) -> Result<ObjSymbol> {
fn to_obj_symbol(
arch: &dyn ObjArch,
obj_file: &File<'_>,
symbol: &Symbol<'_, '_>,
addend: i64,
split_meta: Option<&SplitMeta>,
) -> Result<ObjSymbol> {
let mut name = symbol.name().context("Failed to process symbol name")?;
if name.is_empty() {
log::warn!("Found empty sym: {symbol:?}");
@ -43,7 +51,7 @@ fn to_obj_symbol(obj_file: &File<'_>, symbol: &Symbol<'_, '_>, addend: i64) -> R
if symbol.is_weak() {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Weak);
}
if symbol.scope() == SymbolScope::Linkage {
if obj_file.format() == BinaryFormat::Elf && symbol.scope() == SymbolScope::Linkage {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Hidden);
}
let section_address = if let Some(section) =
@ -53,22 +61,25 @@ fn to_obj_symbol(obj_file: &File<'_>, symbol: &Symbol<'_, '_>, addend: i64) -> R
} else {
symbol.address()
};
let demangled_name = arch.demangle(name);
// Find the virtual address for the symbol if available
let virtual_address = split_meta
.and_then(|m| m.virtual_addresses.as_ref())
.and_then(|v| v.get(symbol.index().0).cloned());
Ok(ObjSymbol {
name: name.to_string(),
demangled_name: demangle(name, &Default::default()),
demangled_name,
address: symbol.address(),
section_address,
size: symbol.size(),
size_known: symbol.size() != 0,
flags,
addend,
diff_symbol: None,
instructions: vec![],
match_percent: None,
virtual_address,
})
}
fn filter_sections(obj_file: &File<'_>) -> Result<Vec<ObjSection>> {
fn filter_sections(obj_file: &File<'_>, split_meta: Option<&SplitMeta>) -> Result<Vec<ObjSection>> {
let mut result = Vec::<ObjSection>::new();
for section in obj_file.sections() {
if section.size() == 0 {
@ -79,31 +90,46 @@ fn filter_sections(obj_file: &File<'_>) -> Result<Vec<ObjSection>> {
};
let name = section.name().context("Failed to process section name")?;
let data = section.uncompressed_data().context("Failed to read section data")?;
// Find the virtual address for the section symbol if available
let section_symbol = obj_file.symbols().find(|s| {
s.kind() == SymbolKind::Section && s.section_index() == Some(section.index())
});
let virtual_address = section_symbol.and_then(|s| {
split_meta
.and_then(|m| m.virtual_addresses.as_ref())
.and_then(|v| v.get(s.index().0).cloned())
});
result.push(ObjSection {
name: name.to_string(),
kind,
address: section.address(),
size: section.size(),
data: data.to_vec(),
index: section.index().0,
orig_index: section.index().0,
symbols: Vec::new(),
relocations: Vec::new(),
data_diff: vec![],
match_percent: 0.0,
virtual_address,
});
}
result.sort_by(|a, b| a.name.cmp(&b.name));
Ok(result)
}
fn symbols_by_section(obj_file: &File<'_>, section: &ObjSection) -> Result<Vec<ObjSymbol>> {
fn symbols_by_section(
arch: &dyn ObjArch,
obj_file: &File<'_>,
section: &ObjSection,
split_meta: Option<&SplitMeta>,
) -> Result<Vec<ObjSymbol>> {
let mut result = Vec::<ObjSymbol>::new();
for symbol in obj_file.symbols() {
if symbol.kind() == SymbolKind::Section {
continue;
}
if let Some(index) = symbol.section().index() {
if index.0 == section.index {
if index.0 == section.orig_index {
if symbol.is_local() && section.kind == ObjSectionKind::Code {
// TODO strip local syms in diff?
let name = symbol.name().context("Failed to process symbol name")?;
@ -111,7 +137,7 @@ fn symbols_by_section(obj_file: &File<'_>, section: &ObjSection) -> Result<Vec<O
continue;
}
}
result.push(to_obj_symbol(obj_file, &symbol, 0)?);
result.push(to_obj_symbol(arch, obj_file, &symbol, 0, split_meta)?);
}
}
}
@ -129,18 +155,24 @@ fn symbols_by_section(obj_file: &File<'_>, section: &ObjSection) -> Result<Vec<O
Ok(result)
}
fn common_symbols(obj_file: &File<'_>) -> Result<Vec<ObjSymbol>> {
fn common_symbols(
arch: &dyn ObjArch,
obj_file: &File<'_>,
split_meta: Option<&SplitMeta>,
) -> Result<Vec<ObjSymbol>> {
obj_file
.symbols()
.filter(Symbol::is_common)
.map(|symbol| to_obj_symbol(obj_file, &symbol, 0))
.map(|symbol| to_obj_symbol(arch, obj_file, &symbol, 0, split_meta))
.collect::<Result<Vec<ObjSymbol>>>()
}
fn find_section_symbol(
arch: &dyn ObjArch,
obj_file: &File<'_>,
target: &Symbol<'_, '_>,
address: u64,
split_meta: Option<&SplitMeta>,
) -> Result<ObjSymbol> {
let section_index =
target.section_index().ok_or_else(|| anyhow::Error::msg("Unknown section index"))?;
@ -160,7 +192,7 @@ fn find_section_symbol(
}
continue;
}
return to_obj_symbol(obj_file, &symbol, 0);
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())
@ -176,65 +208,38 @@ fn find_section_symbol(
size_known: false,
flags: Default::default(),
addend: offset_addr as i64,
diff_symbol: None,
instructions: vec![],
match_percent: None,
virtual_address: None,
})
}
fn relocations_by_section(
arch: ObjArchitecture,
arch: &dyn ObjArch,
obj_file: &File<'_>,
section: &ObjSection,
split_meta: Option<&SplitMeta>,
) -> Result<Vec<ObjReloc>> {
let obj_section = obj_file.section_by_index(SectionIndex(section.index))?;
let obj_section = obj_file.section_by_index(SectionIndex(section.orig_index))?;
let mut relocations = Vec::<ObjReloc>::new();
for (address, reloc) in obj_section.relocations() {
let symbol = match reloc.target() {
RelocationTarget::Symbol(idx) => obj_file
.symbol_by_index(idx)
.context("Failed to locate relocation target symbol")?,
_ => {
return Err(anyhow::Error::msg(format!(
"Unhandled relocation target: {:?}",
reloc.target()
)));
}
};
let kind = match reloc.kind() {
RelocationKind::Absolute => ObjRelocKind::Absolute,
RelocationKind::Elf(kind) => match arch {
ObjArchitecture::PowerPc => match kind {
elf::R_PPC_ADDR16_LO => ObjRelocKind::PpcAddr16Lo,
elf::R_PPC_ADDR16_HI => ObjRelocKind::PpcAddr16Hi,
elf::R_PPC_ADDR16_HA => ObjRelocKind::PpcAddr16Ha,
elf::R_PPC_REL24 => ObjRelocKind::PpcRel24,
elf::R_PPC_REL14 => ObjRelocKind::PpcRel14,
elf::R_PPC_EMB_SDA21 => ObjRelocKind::PpcEmbSda21,
_ => {
return Err(anyhow::Error::msg(format!(
"Unhandled PPC relocation type: {kind}"
)))
}
},
ObjArchitecture::Mips => match kind {
elf::R_MIPS_26 => ObjRelocKind::Mips26,
elf::R_MIPS_HI16 => ObjRelocKind::MipsHi16,
elf::R_MIPS_LO16 => ObjRelocKind::MipsLo16,
elf::R_MIPS_GOT16 => ObjRelocKind::MipsGot16,
elf::R_MIPS_CALL16 => ObjRelocKind::MipsCall16,
elf::R_MIPS_GPREL16 => ObjRelocKind::MipsGpRel16,
elf::R_MIPS_GPREL32 => ObjRelocKind::MipsGpRel32,
_ => bail!("Unhandled MIPS relocation type: {kind}"),
},
},
_ => {
return Err(anyhow::Error::msg(format!(
"Unhandled relocation type: {:?}",
reloc.kind()
)))
RelocationTarget::Symbol(idx) => {
if idx.0 == u32::MAX as usize {
// ???
continue;
}
let Ok(symbol) = obj_file.symbol_by_index(idx) else {
log::warn!(
"Failed to locate relocation {:#x} target symbol {}",
address,
idx.0
);
continue;
};
symbol
}
_ => bail!("Unhandled relocation target: {:?}", reloc.target()),
};
let flags = reloc.flags(); // TODO validate reloc here?
let target_section = match symbol.section() {
SymbolSection::Common => Some(".comm".to_string()),
SymbolSection::Section(idx) => {
@ -243,42 +248,29 @@ fn relocations_by_section(
_ => None,
};
let addend = if reloc.has_implicit_addend() {
let addend = u32::from_be_bytes(
section.data[address as usize..address as usize + 4].try_into()?,
);
match kind {
ObjRelocKind::Absolute => addend as i64,
ObjRelocKind::MipsHi16 => ((addend & 0x0000FFFF) << 16) as i32 as i64,
ObjRelocKind::MipsLo16
| ObjRelocKind::MipsGot16
| ObjRelocKind::MipsCall16
| ObjRelocKind::MipsGpRel16 => (addend & 0x0000FFFF) as i16 as i64,
ObjRelocKind::MipsGpRel32 => addend as i32 as i64,
ObjRelocKind::Mips26 => ((addend & 0x03FFFFFF) << 2) as i64,
_ => bail!("Unsupported implicit relocation {kind:?}"),
}
arch.implcit_addend(section, address, &reloc)?
} else {
reloc.addend()
};
// println!("Reloc: {reloc:?}, symbol: {symbol:?}, addend: {addend:#X}");
let target = match symbol.kind() {
SymbolKind::Text | SymbolKind::Data | SymbolKind::Label | SymbolKind::Unknown => {
to_obj_symbol(obj_file, &symbol, addend)
to_obj_symbol(arch, obj_file, &symbol, addend, split_meta)
}
SymbolKind::Section => {
if addend < 0 {
return Err(anyhow::Error::msg(format!("Negative addend in reloc: {addend}")));
}
find_section_symbol(obj_file, &symbol, addend as u64)
ensure!(addend >= 0, "Negative addend in reloc: {addend}");
find_section_symbol(arch, obj_file, &symbol, addend as u64, split_meta)
}
kind => Err(anyhow!("Unhandled relocation symbol type {kind:?}")),
}?;
relocations.push(ObjReloc { kind, address, target, target_section });
relocations.push(ObjReloc { flags, address, target, target_section });
}
Ok(relocations)
}
fn line_info(obj_file: &File<'_>) -> Result<Option<BTreeMap<u32, u32>>> {
fn line_info(obj_file: &File<'_>) -> Result<Option<BTreeMap<u64, u64>>> {
// DWARF 1.1
let mut map = BTreeMap::new();
if let Some(section) = obj_file.section_by_name(".line") {
if section.size() == 0 {
return Ok(None);
@ -286,22 +278,52 @@ fn line_info(obj_file: &File<'_>) -> Result<Option<BTreeMap<u32, u32>>> {
let data = section.uncompressed_data()?;
let mut reader = Cursor::new(data.as_ref());
let mut map = BTreeMap::new();
let size = reader.read_u32::<BigEndian>()?;
let base_address = reader.read_u32::<BigEndian>()?;
let base_address = reader.read_u32::<BigEndian>()? as u64;
while reader.position() < size as u64 {
let line_number = reader.read_u32::<BigEndian>()?;
let line_number = reader.read_u32::<BigEndian>()? as u64;
let statement_pos = reader.read_u16::<BigEndian>()?;
if statement_pos != 0xFFFF {
log::warn!("Unhandled statement pos {}", statement_pos);
}
let address_delta = reader.read_u32::<BigEndian>()?;
let address_delta = reader.read_u32::<BigEndian>()? as u64;
map.insert(base_address + address_delta, line_number);
}
// log::debug!("Line info: {map:#X?}");
return Ok(Some(map));
}
Ok(None)
// DWARF 2+
#[cfg(feature = "dwarf")]
{
let dwarf_cow = gimli::DwarfSections::load(|id| {
Ok::<_, gimli::Error>(
obj_file
.section_by_name(id.name())
.and_then(|section| section.uncompressed_data().ok())
.unwrap_or(std::borrow::Cow::Borrowed(&[][..])),
)
})?;
let endian = match obj_file.endianness() {
object::Endianness::Little => gimli::RunTimeEndian::Little,
object::Endianness::Big => gimli::RunTimeEndian::Big,
};
let dwarf = dwarf_cow.borrow(|section| gimli::EndianSlice::new(section, endian));
let mut iter = dwarf.units();
while let Some(header) = iter.next()? {
let unit = dwarf.unit(header)?;
if let Some(program) = unit.line_program.clone() {
let mut rows = program.rows();
while let Some((_header, row)) = rows.next_row()? {
if let Some(line) = row.line() {
map.insert(row.address(), line.get());
}
}
}
}
}
if map.is_empty() {
return Ok(None);
}
Ok(Some(map))
}
pub fn read(obj_path: &Path) -> Result<ObjInfo> {
@ -311,27 +333,42 @@ pub fn read(obj_path: &Path) -> Result<ObjInfo> {
(unsafe { memmap2::Mmap::map(&file) }?, timestamp)
};
let obj_file = File::parse(&*data)?;
let architecture = match obj_file.architecture() {
Architecture::PowerPc => ObjArchitecture::PowerPc,
Architecture::Mips => ObjArchitecture::Mips,
_ => {
return Err(anyhow::Error::msg(format!(
"Unsupported architecture: {:?}",
obj_file.architecture()
)))
}
};
let mut result = ObjInfo {
architecture,
let arch = new_arch(&obj_file)?;
let split_meta = split_meta(&obj_file)?;
let mut sections = filter_sections(&obj_file, split_meta.as_ref())?;
for section in &mut sections {
section.symbols =
symbols_by_section(arch.as_ref(), &obj_file, section, split_meta.as_ref())?;
section.relocations =
relocations_by_section(arch.as_ref(), &obj_file, section, split_meta.as_ref())?;
}
let common = common_symbols(arch.as_ref(), &obj_file, split_meta.as_ref())?;
Ok(ObjInfo {
arch,
path: obj_path.to_owned(),
timestamp,
sections: filter_sections(&obj_file)?,
common: common_symbols(&obj_file)?,
sections,
common,
line_info: line_info(&obj_file)?,
};
for section in &mut result.sections {
section.symbols = symbols_by_section(&obj_file, section)?;
section.relocations = relocations_by_section(architecture, &obj_file, section)?;
}
Ok(result)
split_meta,
})
}
pub fn has_function(obj_path: &Path, symbol_name: &str) -> Result<bool> {
let data = {
let file = fs::File::open(obj_path)?;
unsafe { memmap2::Mmap::map(&file) }?
};
Ok(File::parse(&*data)?
.symbol_by_name(symbol_name)
.filter(|o| o.kind() == SymbolKind::Text)
.is_some())
}
fn split_meta(obj_file: &File<'_>) -> Result<Option<SplitMeta>> {
Ok(if let Some(section) = obj_file.section_by_name(SPLITMETA_SECTION) {
Some(SplitMeta::from_section(section, obj_file.endianness(), obj_file.is_64())?)
} else {
None
})
}

View File

@ -0,0 +1,223 @@
use std::{io, io::Write};
use object::{elf::SHT_NOTE, Endian, ObjectSection};
pub const SPLITMETA_SECTION: &str = ".note.split";
pub const SHT_SPLITMETA: u32 = SHT_NOTE;
pub const ELF_NOTE_SPLIT: &[u8] = b"Split";
/// This is used to store metadata about the source of an object file,
/// such as the original virtual addresses and the tool that wrote it.
#[derive(Debug, Default, Clone)]
pub struct SplitMeta {
/// The tool that generated the object. Informational only.
pub generator: Option<String>,
/// The name of the source module. (e.g. the DOL or REL name)
pub module_name: Option<String>,
/// The ID of the source module. (e.g. the DOL or REL ID)
pub module_id: Option<u32>,
/// Original virtual addresses of each symbol in the object.
/// Index 0 is the ELF null symbol.
pub virtual_addresses: Option<Vec<u64>>,
}
const NT_SPLIT_GENERATOR: u32 = u32::from_be_bytes(*b"GENR");
const NT_SPLIT_MODULE_NAME: u32 = u32::from_be_bytes(*b"MODN");
const NT_SPLIT_MODULE_ID: u32 = u32::from_be_bytes(*b"MODI");
const NT_SPLIT_VIRTUAL_ADDRESSES: u32 = u32::from_be_bytes(*b"VIRT");
impl SplitMeta {
pub fn from_section<E>(section: object::Section, e: E, is_64: bool) -> io::Result<Self>
where E: Endian {
let mut result = SplitMeta::default();
let data = section.uncompressed_data().map_err(object_io_error)?;
let mut iter = NoteIterator::new(data.as_ref(), section.align(), e, is_64)?;
while let Some(note) = iter.next(e)? {
if note.name != ELF_NOTE_SPLIT {
continue;
}
match note.n_type {
NT_SPLIT_GENERATOR => {
let string = String::from_utf8(note.desc.to_vec())
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
result.generator = Some(string);
}
NT_SPLIT_MODULE_NAME => {
let string = String::from_utf8(note.desc.to_vec())
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
result.module_name = Some(string);
}
NT_SPLIT_MODULE_ID => {
result.module_id =
Some(e.read_u32_bytes(note.desc.try_into().map_err(|_| {
io::Error::new(io::ErrorKind::InvalidData, "Invalid module ID size")
})?));
}
NT_SPLIT_VIRTUAL_ADDRESSES => {
let vec = if is_64 {
let mut vec = vec![0u64; note.desc.len() / 8];
for (i, v) in vec.iter_mut().enumerate() {
*v =
e.read_u64_bytes(note.desc[i * 8..(i + 1) * 8].try_into().unwrap());
}
vec
} else {
let mut vec = vec![0u64; note.desc.len() / 4];
for (i, v) in vec.iter_mut().enumerate() {
*v = e.read_u32_bytes(note.desc[i * 4..(i + 1) * 4].try_into().unwrap())
as u64;
}
vec
};
result.virtual_addresses = Some(vec);
}
_ => {
// Ignore unknown sections
}
}
}
Ok(result)
}
pub fn to_writer<E, W>(&self, writer: &mut W, e: E, is_64: bool) -> io::Result<()>
where
E: Endian,
W: Write + ?Sized,
{
if let Some(generator) = &self.generator {
write_note_header(writer, e, NT_SPLIT_GENERATOR, generator.len())?;
writer.write_all(generator.as_bytes())?;
align_data_to_4(writer, generator.len())?;
}
if let Some(module_name) = &self.module_name {
write_note_header(writer, e, NT_SPLIT_MODULE_NAME, module_name.len())?;
writer.write_all(module_name.as_bytes())?;
align_data_to_4(writer, module_name.len())?;
}
if let Some(module_id) = self.module_id {
write_note_header(writer, e, NT_SPLIT_MODULE_ID, 4)?;
writer.write_all(&e.write_u32_bytes(module_id))?;
}
if let Some(virtual_addresses) = &self.virtual_addresses {
let count = virtual_addresses.len();
let size = if is_64 { count * 8 } else { count * 4 };
write_note_header(writer, e, NT_SPLIT_VIRTUAL_ADDRESSES, size)?;
if is_64 {
for &addr in virtual_addresses {
writer.write_all(&e.write_u64_bytes(addr))?;
}
} else {
for &addr in virtual_addresses {
writer.write_all(&e.write_u32_bytes(addr as u32))?;
}
}
}
Ok(())
}
pub fn write_size(&self, is_64: bool) -> usize {
let mut size = 0;
if let Some(generator) = self.generator.as_deref() {
size += NOTE_HEADER_SIZE + generator.len();
size = align_size_to_4(size);
}
if let Some(module_name) = self.module_name.as_deref() {
size += NOTE_HEADER_SIZE + module_name.len();
size = align_size_to_4(size);
}
if self.module_id.is_some() {
size += NOTE_HEADER_SIZE + 4;
size = align_size_to_4(size);
}
if let Some(virtual_addresses) = self.virtual_addresses.as_deref() {
size += NOTE_HEADER_SIZE + if is_64 { 8 } else { 4 } * virtual_addresses.len();
size = align_size_to_4(size);
}
size
}
}
/// Convert an object::read::Error to an io::Error.
fn object_io_error(err: object::read::Error) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, err)
}
/// An ELF note entry.
struct Note<'data> {
n_type: u32,
name: &'data [u8],
desc: &'data [u8],
}
/// object::read::elf::NoteIterator is awkward to use generically,
/// so wrap it in our own iterator.
enum NoteIterator<'data, E>
where E: Endian
{
B32(object::read::elf::NoteIterator<'data, object::elf::FileHeader32<E>>),
B64(object::read::elf::NoteIterator<'data, object::elf::FileHeader64<E>>),
}
impl<'data, E> NoteIterator<'data, E>
where E: Endian
{
fn new(data: &'data [u8], align: u64, e: E, is_64: bool) -> io::Result<Self> {
Ok(if is_64 {
NoteIterator::B64(
object::read::elf::NoteIterator::new(e, align, data).map_err(object_io_error)?,
)
} else {
NoteIterator::B32(
object::read::elf::NoteIterator::new(e, align as u32, data)
.map_err(object_io_error)?,
)
})
}
fn next(&mut self, e: E) -> io::Result<Option<Note<'data>>> {
match self {
NoteIterator::B32(iter) => Ok(iter.next().map_err(object_io_error)?.map(|note| Note {
n_type: note.n_type(e),
name: note.name(),
desc: note.desc(),
})),
NoteIterator::B64(iter) => Ok(iter.next().map_err(object_io_error)?.map(|note| Note {
n_type: note.n_type(e),
name: note.name(),
desc: note.desc(),
})),
}
}
}
fn align_size_to_4(size: usize) -> usize { (size + 3) & !3 }
fn align_data_to_4<W: Write + ?Sized>(writer: &mut W, len: usize) -> io::Result<()> {
const ALIGN_BYTES: &[u8] = &[0; 4];
if len % 4 != 0 {
writer.write_all(&ALIGN_BYTES[..4 - len % 4])?;
}
Ok(())
}
// ELF note format:
// Name Size | 4 bytes (integer)
// Desc Size | 4 bytes (integer)
// Type | 4 bytes (usually interpreted as an integer)
// Name | variable size, padded to a 4 byte boundary
// Desc | variable size, padded to a 4 byte boundary
const NOTE_HEADER_SIZE: usize = 12 + ((ELF_NOTE_SPLIT.len() + 4) & !3);
fn write_note_header<E, W>(writer: &mut W, e: E, kind: u32, desc_len: usize) -> io::Result<()>
where
E: Endian,
W: Write + ?Sized,
{
writer.write_all(&e.write_u32_bytes(ELF_NOTE_SPLIT.len() as u32 + 1))?; // Name Size
writer.write_all(&e.write_u32_bytes(desc_len as u32))?; // Desc Size
writer.write_all(&e.write_u32_bytes(kind))?; // Type
writer.write_all(ELF_NOTE_SPLIT)?; // Name
writer.write_all(&[0; 1])?; // Null terminator
align_data_to_4(writer, ELF_NOTE_SPLIT.len() + 1)?;
Ok(())
}

24
objdiff-core/src/util.rs Normal file
View File

@ -0,0 +1,24 @@
use std::fmt::{LowerHex, UpperHex};
use num_traits::PrimInt;
// https://stackoverflow.com/questions/44711012/how-do-i-format-a-signed-integer-to-a-sign-aware-hexadecimal-representation
pub(crate) struct ReallySigned<N: PrimInt>(pub(crate) N);
impl<N: PrimInt> LowerHex for ReallySigned<N> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let num = self.0.to_i64().unwrap();
let prefix = if f.alternate() { "0x" } else { "" };
let bare_hex = format!("{:x}", num.abs());
f.pad_integral(num >= 0, prefix, &bare_hex)
}
}
impl<N: PrimInt> UpperHex for ReallySigned<N> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let num = self.0.to_i64().unwrap();
let prefix = if f.alternate() { "0x" } else { "" };
let bare_hex = format!("{:X}", num.abs());
f.pad_integral(num >= 0, prefix, &bare_hex)
}
}

83
objdiff-gui/Cargo.toml Normal file
View File

@ -0,0 +1,83 @@
[package]
name = "objdiff-gui"
version = "1.0.0"
edition = "2021"
rust-version = "1.70"
authors = ["Luke Street <luke@street.dev>"]
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"
[[bin]]
name = "objdiff"
path = "src/main.rs"
[features]
default = ["wgpu", "wsl"]
wgpu = ["eframe/wgpu"]
wsl = []
[dependencies]
anyhow = "1.0.82"
bytes = "1.6.0"
cfg-if = "1.0.0"
const_format = "0.2.32"
cwdemangle = "1.0.0"
dirs = "5.0.1"
eframe = { version = "0.27.2", features = ["persistence"] }
egui = "0.27.2"
egui_extras = "0.27.2"
filetime = "0.2.23"
float-ord = "0.3.2"
font-kit = "0.13.0"
globset = { version = "0.4.14", features = ["serde1"] }
log = "0.4.21"
notify = "6.1.1"
objdiff-core = { path = "../objdiff-core", features = ["all"] }
png = "0.17.13"
pollster = "0.3.0"
rfd = { version = "0.14.1" } #, default-features = false, features = ['xdg-portal']
ron = "0.8.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1.0.116"
shell-escape = "0.1.5"
tempfile = "3.10.1"
time = { version = "0.3.36", features = ["formatting", "local-offset"] }
# For Linux static binaries, use rustls
[target.'cfg(target_os = "linux")'.dependencies]
reqwest = { version = "0.12.4", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"] }
self_update = { version = "0.40.0", default-features = false, features = ["rustls"] }
# For all other platforms, use native TLS
[target.'cfg(not(target_os = "linux"))'.dependencies]
reqwest = { version = "0.12.4", default-features = false, features = ["blocking", "json", "multipart", "default-tls"] }
self_update = "0.40.0"
[target.'cfg(windows)'.dependencies]
path-slash = "0.2.1"
winapi = "0.3.9"
[target.'cfg(windows)'.build-dependencies]
winres = "0.1.12"
[target.'cfg(unix)'.dependencies]
exec = "0.3.1"
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tracing-subscriber = "0.3"
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.7"
tracing-wasm = "0.2"
[build-dependencies]
anyhow = "1.0.82"
vergen = { version = "8.3.1", features = ["build", "cargo", "git", "gitcl"] }

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

@ -12,12 +12,17 @@ use std::{
use filetime::FileTime;
use globset::{Glob, GlobSet};
use notify::{RecursiveMode, Watcher};
use objdiff_core::{
config::{
build_globset, ProjectConfigInfo, ProjectObject, ScratchConfig, DEFAULT_WATCH_PATTERNS,
},
diff::DiffObjConfig,
};
use time::UtcOffset;
use crate::{
app_config::{deserialize_config, AppConfigVersion},
config::{build_globset, load_project_config, ProjectObject, ProjectObjectNode},
diff::DiffAlg,
config::{load_project_config, ProjectObjectNode},
jobs::{
objdiff::{start_build, ObjDiffConfig},
Job, JobQueue, JobResult, JobStatus,
@ -25,7 +30,7 @@ use crate::{
views::{
appearance::{appearance_window, Appearance},
config::{
config_ui, diff_options_window, project_window, ConfigViewState, DEFAULT_WATCH_PATTERNS,
config_ui, diff_config_window, project_window, ConfigViewState, CONFIG_DISABLED_TEXT,
},
data_diff::data_diff_ui,
debug::debug_window,
@ -47,7 +52,7 @@ pub struct ViewState {
pub show_appearance_config: bool,
pub show_demangle: bool,
pub show_project_config: bool,
pub show_diff_options: bool,
pub show_diff_config: bool,
pub show_debug: bool,
}
@ -59,12 +64,7 @@ pub struct ObjectConfig {
pub base_path: Option<PathBuf>,
pub reverse_fn_order: Option<bool>,
pub complete: Option<bool>,
}
#[derive(Clone, Eq, PartialEq)]
pub struct ProjectConfigInfo {
pub path: PathBuf,
pub timestamp: FileTime,
pub scratch: Option<ScratchConfig>,
}
#[inline]
@ -84,6 +84,8 @@ pub struct AppConfig {
#[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>,
@ -106,9 +108,7 @@ pub struct AppConfig {
#[serde(default)]
pub recent_projects: Vec<PathBuf>,
#[serde(default)]
pub code_alg: DiffAlg,
#[serde(default)]
pub data_alg: DiffAlg,
pub diff_obj_config: DiffObjConfig,
#[serde(skip)]
pub objects: Vec<ProjectObject>,
@ -133,6 +133,7 @@ impl Default for AppConfig {
Self {
version: AppConfigVersion::default().version,
custom_make: None,
custom_args: None,
selected_wsl_distro: None,
project_dir: None,
target_obj_dir: None,
@ -144,8 +145,7 @@ impl Default for AppConfig {
auto_update_check: true,
watch_patterns: DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(),
recent_projects: vec![],
code_alg: Default::default(),
data_alg: Default::default(),
diff_obj_config: Default::default(),
objects: vec![],
object_nodes: vec![],
watcher_change: false,
@ -223,9 +223,6 @@ impl App {
utc_offset: UtcOffset,
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
) -> Self {
// This is also where you can customized the look at feel of egui using
// `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`.
// Load previous app state (if any).
// Note that you must enable the `persistence` feature for this to work.
let mut app = Self::default();
@ -245,12 +242,15 @@ impl App {
app.config = Arc::new(RwLock::new(config));
}
}
app.appearance.init_fonts(&cc.egui_ctx);
app.appearance.utc_offset = utc_offset;
app.relaunch_path = relaunch_path;
app
}
fn pre_update(&mut self) {
fn pre_update(&mut self, ctx: &egui::Context) {
self.appearance.pre_update(ctx);
let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state;
let mut results = vec![];
@ -290,7 +290,7 @@ impl App {
title: "Error".to_string(),
progress_percent: 0.0,
progress_items: None,
status: "".to_string(),
status: String::new(),
error: Some(err),
}));
}
@ -306,9 +306,11 @@ impl App {
}
fn post_update(&mut self, ctx: &egui::Context) {
self.appearance.post_update(ctx);
let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state;
config_state.post_update(ctx, jobs, &self.config);
diff_state.post_update(&self.config);
diff_state.post_update(ctx, jobs, &self.config);
let Ok(mut config) = self.config.write() else {
return;
@ -362,12 +364,12 @@ impl App {
}
if let Some(result) = &diff_state.build {
if let Some(obj) = &result.first_obj {
if let Some((obj, _)) = &result.first_obj {
if file_modified(&obj.path, obj.timestamp) {
config.queue_reload = true;
}
}
if let Some(obj) = &result.second_obj {
if let Some((obj, _)) = &result.second_obj {
if file_modified(&obj.path, obj.timestamp) {
config.queue_reload = true;
}
@ -396,15 +398,13 @@ impl eframe::App for App {
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
if self.should_relaunch {
frame.close();
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
return;
}
self.pre_update();
self.pre_update(ctx);
let Self { config, appearance, view_state, .. } = self;
ctx.set_style(appearance.apply(ctx.style().as_ref()));
let ViewState {
jobs,
config_state,
@ -414,7 +414,7 @@ impl eframe::App for App {
show_appearance_config,
show_demangle,
show_project_config,
show_diff_options,
show_diff_config,
show_debug,
} = view_state;
@ -458,7 +458,7 @@ impl eframe::App for App {
ui.close_menu();
}
if ui.button("Quit").clicked() {
frame.close();
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
});
ui.menu_button("Tools", |ui| {
@ -468,8 +468,8 @@ impl eframe::App for App {
}
});
ui.menu_button("Diff Options", |ui| {
if ui.button("Algorithm").clicked() {
*show_diff_options = !*show_diff_options;
if ui.button("More").clicked() {
*show_diff_config = !*show_diff_config;
ui.close_menu();
}
let mut config = config.write().unwrap();
@ -486,13 +486,32 @@ impl eframe::App for App {
"Reverse function order (-inline deferred)",
),
)
.on_disabled_hover_text(
"Option disabled because it's set by the project configuration file.",
);
.on_disabled_hover_text(CONFIG_DISABLED_TEXT);
ui.checkbox(
&mut diff_state.symbol_state.show_hidden_symbols,
"Show hidden symbols",
);
if ui
.checkbox(
&mut config.diff_obj_config.relax_reloc_diffs,
"Relax relocation diffs",
)
.on_hover_text(
"Ignores differences in relocation targets. (Address, name, etc)",
)
.changed()
{
config.queue_reload = true;
}
if ui
.checkbox(
&mut config.diff_obj_config.space_between_args,
"Space between args",
)
.changed()
{
config.queue_reload = true;
}
});
});
});
@ -522,7 +541,7 @@ impl eframe::App for App {
project_window(ctx, config, show_project_config, config_state, appearance);
appearance_window(ctx, show_appearance_config, appearance);
demangle_window(ctx, show_demangle, demangle_state, appearance);
diff_options_window(ctx, config, show_diff_options, appearance);
diff_config_window(ctx, config, show_diff_config, appearance);
debug_window(ctx, show_debug, frame_history, appearance);
self.post_update(ctx);

View File

@ -60,6 +60,7 @@ impl ObjectConfigV0 {
base_path: Some(self.base_path),
reverse_fn_order: self.reverse_fn_order,
complete: None,
scratch: None,
}
}
}

93
objdiff-gui/src/config.rs Normal file
View File

@ -0,0 +1,93 @@
use std::path::{Component, Path};
use anyhow::Result;
use globset::Glob;
use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS};
use crate::app::AppConfig;
#[derive(Clone)]
pub enum ProjectObjectNode {
File(String, Box<ProjectObject>),
Dir(String, Vec<ProjectObjectNode>),
}
fn find_dir<'a>(
name: &str,
nodes: &'a mut Vec<ProjectObjectNode>,
) -> &'a mut Vec<ProjectObjectNode> {
if let Some(index) = nodes
.iter()
.position(|node| matches!(node, ProjectObjectNode::Dir(dir_name, _) if dir_name == name))
{
if let ProjectObjectNode::Dir(_, children) = &mut nodes[index] {
return children;
}
} else {
nodes.push(ProjectObjectNode::Dir(name.to_string(), vec![]));
if let Some(ProjectObjectNode::Dir(_, children)) = nodes.last_mut() {
return children;
}
}
unreachable!();
}
fn build_nodes(
objects: &[ProjectObject],
project_dir: &Path,
target_obj_dir: Option<&Path>,
base_obj_dir: Option<&Path>,
) -> Vec<ProjectObjectNode> {
let mut nodes = vec![];
for object in objects {
let mut out_nodes = &mut nodes;
let path = if let Some(name) = &object.name {
Path::new(name)
} else if let Some(path) = &object.path {
path
} else {
continue;
};
if let Some(parent) = path.parent() {
for component in parent.components() {
if let Component::Normal(name) = component {
let name = name.to_str().unwrap();
out_nodes = find_dir(name, out_nodes);
}
}
}
let 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();
out_nodes.push(ProjectObjectNode::File(filename, object));
}
nodes
}
pub fn load_project_config(config: &mut AppConfig) -> Result<()> {
let Some(project_dir) = &config.project_dir else {
return Ok(());
};
if let Some((result, info)) = try_project_config(project_dir) {
let project_config = result?;
config.custom_make = project_config.custom_make;
config.custom_args = project_config.custom_args;
config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p));
config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p));
config.build_base = project_config.build_base;
config.build_target = project_config.build_target;
config.watch_patterns = project_config.watch_patterns.unwrap_or_else(|| {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
});
config.watcher_change = true;
config.objects = project_config.objects;
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(())
}

View File

@ -0,0 +1,146 @@
// font-kit/src/matching.rs
//
// Copyright © 2018 The Pathfinder Project Developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! Determines the closest font matching a description per the CSS Fonts Level 3 specification.
use float_ord::FloatOrd;
use font_kit::{
error::SelectionError,
properties::{Properties, Stretch, Style, Weight},
};
/// This follows CSS Fonts Level 3 § 5.2 [1].
///
/// https://drafts.csswg.org/css-fonts-3/#font-style-matching
pub fn find_best_match(
candidates: &[Properties],
query: &Properties,
) -> Result<usize, SelectionError> {
// Step 4.
let mut matching_set: Vec<usize> = (0..candidates.len()).collect();
if matching_set.is_empty() {
return Err(SelectionError::NotFound);
}
// Step 4a (`font-stretch`).
let matching_stretch = if matching_set
.iter()
.any(|&index| candidates[index].stretch == query.stretch)
{
// Exact match.
query.stretch
} else if query.stretch <= Stretch::NORMAL {
// Closest width, first checking narrower values and then wider values.
match matching_set
.iter()
.filter(|&&index| candidates[index].stretch < query.stretch)
.min_by_key(|&&index| FloatOrd(query.stretch.0 - candidates[index].stretch.0))
{
Some(&matching_index) => candidates[matching_index].stretch,
None => {
let matching_index = *matching_set
.iter()
.min_by_key(|&&index| FloatOrd(candidates[index].stretch.0 - query.stretch.0))
.unwrap();
candidates[matching_index].stretch
}
}
} else {
// Closest width, first checking wider values and then narrower values.
match matching_set
.iter()
.filter(|&&index| candidates[index].stretch > query.stretch)
.min_by_key(|&&index| FloatOrd(candidates[index].stretch.0 - query.stretch.0))
{
Some(&matching_index) => candidates[matching_index].stretch,
None => {
let matching_index = *matching_set
.iter()
.min_by_key(|&&index| FloatOrd(query.stretch.0 - candidates[index].stretch.0))
.unwrap();
candidates[matching_index].stretch
}
}
};
matching_set.retain(|&index| candidates[index].stretch == matching_stretch);
// Step 4b (`font-style`).
let style_preference = match query.style {
Style::Italic => [Style::Italic, Style::Oblique, Style::Normal],
Style::Oblique => [Style::Oblique, Style::Italic, Style::Normal],
Style::Normal => [Style::Normal, Style::Oblique, Style::Italic],
};
let matching_style = *style_preference
.iter()
.find(|&query_style| {
matching_set.iter().any(|&index| candidates[index].style == *query_style)
})
.unwrap();
matching_set.retain(|&index| candidates[index].style == matching_style);
// Step 4c (`font-weight`).
//
// The spec doesn't say what to do if the weight is between 400 and 500 exclusive, so we
// just use 450 as the cutoff.
let matching_weight =
if matching_set.iter().any(|&index| candidates[index].weight == query.weight) {
query.weight
} else if query.weight >= Weight(400.0)
&& query.weight < Weight(450.0)
&& matching_set.iter().any(|&index| candidates[index].weight == Weight(500.0))
{
// Check 500 first.
Weight(500.0)
} else if query.weight >= Weight(450.0)
&& query.weight <= Weight(500.0)
&& matching_set.iter().any(|&index| candidates[index].weight == Weight(400.0))
{
// Check 400 first.
Weight(400.0)
} else if query.weight <= Weight(500.0) {
// Closest weight, first checking thinner values and then fatter ones.
match matching_set
.iter()
.filter(|&&index| candidates[index].weight <= query.weight)
.min_by_key(|&&index| FloatOrd(query.weight.0 - candidates[index].weight.0))
{
Some(&matching_index) => candidates[matching_index].weight,
None => {
let matching_index = *matching_set
.iter()
.min_by_key(|&&index| FloatOrd(candidates[index].weight.0 - query.weight.0))
.unwrap();
candidates[matching_index].weight
}
}
} else {
// Closest weight, first checking fatter values and then thinner ones.
match matching_set
.iter()
.filter(|&&index| candidates[index].weight >= query.weight)
.min_by_key(|&&index| FloatOrd(candidates[index].weight.0 - query.weight.0))
{
Some(&matching_index) => candidates[matching_index].weight,
None => {
let matching_index = *matching_set
.iter()
.min_by_key(|&&index| FloatOrd(query.weight.0 - candidates[index].weight.0))
.unwrap();
candidates[matching_index].weight
}
}
};
matching_set.retain(|&index| candidates[index].weight == matching_weight);
// Step 4d concerns `font-size`, but fonts in `font-kit` are unsized, so we ignore that.
// Return the result.
matching_set.into_iter().next().ok_or(SelectionError::NotFound)
}

View File

@ -0,0 +1,107 @@
pub mod matching;
use std::{borrow::Cow, fs, sync::Arc};
use anyhow::{Context, Result};
use crate::fonts::matching::find_best_match;
pub struct LoadedFontFamily {
pub family_name: String,
pub fonts: Vec<font_kit::font::Font>,
pub handles: Vec<font_kit::handle::Handle>,
// pub properties: Vec<font_kit::properties::Properties>,
pub default_index: usize,
}
pub struct LoadedFont {
// pub font_name: String,
pub font_data: egui::FontData,
}
pub fn load_font_family(
source: &font_kit::source::SystemSource,
name: &str,
) -> Option<LoadedFontFamily> {
let family_handle = source.select_family_by_name(name).ok()?;
if family_handle.fonts().is_empty() {
log::warn!("No fonts found for family '{}'", name);
return None;
}
let handles = family_handle.fonts().to_vec();
let mut loaded = Vec::with_capacity(handles.len());
for handle in handles.iter() {
match font_kit::loaders::default::Font::from_handle(handle) {
Ok(font) => loaded.push(font),
Err(err) => {
log::warn!("Failed to load font '{}': {}", name, err);
return None;
}
}
}
let properties = loaded.iter().map(|f| f.properties()).collect::<Vec<_>>();
let default_index =
find_best_match(&properties, &font_kit::properties::Properties::new()).unwrap_or(0);
let font_family_name =
loaded.first().map(|f| f.family_name()).unwrap_or_else(|| name.to_string());
Some(LoadedFontFamily {
family_name: font_family_name,
fonts: loaded,
handles,
// properties,
default_index,
})
}
pub fn load_font(handle: &font_kit::handle::Handle) -> Result<LoadedFont> {
// let loaded = font_kit::loaders::default::Font::from_handle(handle)?;
let data = match handle {
font_kit::handle::Handle::Memory { bytes, font_index } => egui::FontData {
font: Cow::Owned(bytes.to_vec()),
index: *font_index,
tweak: Default::default(),
},
font_kit::handle::Handle::Path { path, font_index } => {
let vec = fs::read(path).with_context(|| {
format!("Failed to load font '{}' (index {})", path.display(), font_index)
})?;
egui::FontData { font: Cow::Owned(vec), index: *font_index, tweak: Default::default() }
}
};
Ok(LoadedFont {
// font_name: loaded.full_name(),
font_data: data,
})
}
pub fn load_font_if_needed(
ctx: &egui::Context,
source: &font_kit::source::SystemSource,
font_id: &egui::FontId,
base_family: egui::FontFamily,
fonts: &mut egui::FontDefinitions,
) -> Result<()> {
if fonts.families.contains_key(&font_id.family) {
return Ok(());
}
let family_name = match &font_id.family {
egui::FontFamily::Proportional | egui::FontFamily::Monospace => return Ok(()),
egui::FontFamily::Name(v) => v,
};
let family = load_font_family(source, family_name)
.with_context(|| format!("Failed to load font family '{}'", family_name))?;
let default_fonts = fonts.families.get(&base_family).cloned().unwrap_or_default();
// FIXME clean up
let default_font_ref = family.fonts.get(family.default_index).unwrap();
let default_font = family.handles.get(family.default_index).unwrap();
let default_font_data = load_font(default_font).unwrap();
log::info!("Loaded font family '{}'", family.family_name);
fonts.font_data.insert(default_font_ref.full_name(), default_font_data.font_data);
fonts
.families
.entry(egui::FontFamily::Name(Arc::from(family.family_name)))
.or_insert_with(|| default_fonts)
.insert(0, default_font_ref.full_name());
ctx.set_fonts(fonts.clone());
Ok(())
}

View File

@ -0,0 +1,134 @@
use std::{fs, path::PathBuf, sync::mpsc::Receiver};
use anyhow::{anyhow, bail, Context, Result};
use const_format::formatcp;
use crate::{
app::AppConfig,
jobs::{
objdiff::{run_make, BuildConfig, BuildStatus},
start_job, update_status, Job, JobContext, JobResult, JobState,
},
};
#[derive(Debug, Clone)]
pub struct CreateScratchConfig {
pub build_config: BuildConfig,
pub context_path: Option<PathBuf>,
pub build_context: bool,
// Scratch fields
pub compiler: String,
pub platform: String,
pub compiler_flags: String,
pub function_name: String,
pub target_obj: PathBuf,
}
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)]
pub struct CreateScratchResult {
pub scratch_url: String,
}
#[derive(Debug, Default, Clone, serde::Deserialize)]
struct CreateScratchResponse {
pub slug: String,
pub claim_token: String,
}
const API_HOST: &str = "https://decomp.me";
fn run_create_scratch(
status: &JobContext,
cancel: Receiver<()>,
config: CreateScratchConfig,
) -> Result<Box<CreateScratchResult>> {
let project_dir =
config.build_config.project_dir.as_ref().ok_or_else(|| anyhow!("Missing project dir"))?;
let mut context = None;
if let Some(context_path) = &config.context_path {
if config.build_context {
update_status(status, "Building context".to_string(), 0, 2, &cancel)?;
match run_make(&config.build_config, context_path) {
BuildStatus { success: true, .. } => {}
BuildStatus { success: false, stdout, stderr, .. } => {
bail!("Failed to build context:\n{stdout}\n{stderr}")
}
}
}
let context_path = project_dir.join(context_path);
context = Some(
fs::read_to_string(&context_path)
.map_err(|e| anyhow!("Failed to read {}: {}", context_path.display(), e))?,
);
}
update_status(status, "Creating scratch".to_string(), 1, 2, &cancel)?;
let diff_flags = [format!("--disassemble={}", config.function_name)];
let diff_flags = serde_json::to_string(&diff_flags).unwrap();
let obj_path = project_dir.join(&config.target_obj);
let file = reqwest::blocking::multipart::Part::file(&obj_path)
.with_context(|| format!("Failed to open {}", obj_path.display()))?;
let form = reqwest::blocking::multipart::Form::new()
.text("compiler", config.compiler.clone())
.text("platform", config.platform.clone())
.text("compiler_flags", config.compiler_flags.clone())
.text("diff_label", config.function_name.clone())
.text("diff_flags", diff_flags)
.text("context", context.unwrap_or_default())
.text("source_code", "// Move related code from Context tab to here")
.part("target_obj", file);
let client = reqwest::blocking::Client::new();
let response = client
.post(formatcp!("{API_HOST}/api/scratch"))
.multipart(form)
.send()
.map_err(|e| anyhow!("Failed to send request: {}", e))?;
if !response.status().is_success() {
return Err(anyhow!("Failed to create scratch: {}", response.text()?));
}
let body: CreateScratchResponse = response.json().context("Failed to parse response")?;
let scratch_url = format!("{API_HOST}/scratch/{}/claim?token={}", body.slug, body.claim_token);
update_status(status, "Complete".to_string(), 2, 2, &cancel)?;
Ok(Box::from(CreateScratchResult { scratch_url }))
}
pub fn start_create_scratch(ctx: &egui::Context, config: CreateScratchConfig) -> JobState {
start_job(ctx, "Create scratch", Job::CreateScratch, move |context, cancel| {
run_create_scratch(&context, cancel, config)
.map(|result| JobResult::CreateScratch(Some(result)))
})
}

View File

@ -9,9 +9,13 @@ use std::{
use anyhow::Result;
use crate::jobs::{check_update::CheckUpdateResult, objdiff::ObjDiffResult, update::UpdateResult};
use crate::jobs::{
check_update::CheckUpdateResult, create_scratch::CreateScratchResult, objdiff::ObjDiffResult,
update::UpdateResult,
};
pub mod check_update;
pub mod create_scratch;
pub mod objdiff;
pub mod update;
@ -20,6 +24,7 @@ pub enum Job {
ObjDiff,
CheckUpdate,
Update,
CreateScratch,
}
pub static JOB_ID: AtomicUsize = AtomicUsize::new(0);
@ -119,6 +124,7 @@ pub enum JobResult {
ObjDiff(Option<Box<ObjDiffResult>>),
CheckUpdate(Option<Box<CheckUpdateResult>>),
Update(Box<UpdateResult>),
CreateScratch(Option<Box<CreateScratchResult>>),
}
fn should_cancel(rx: &Receiver<()>) -> bool {

View File

@ -0,0 +1,279 @@
use std::{
path::{Path, PathBuf},
process::Command,
str::from_utf8,
sync::mpsc::Receiver,
};
use anyhow::{anyhow, Context, 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>>,
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()
};
};
match run_make_cmd(config, cwd, arg) {
Ok(status) => status,
Err(e) => BuildStatus { success: false, stderr: e.to_string(), ..Default::default() },
}
}
fn run_make_cmd(config: &BuildConfig, cwd: &Path, arg: &Path) -> Result<BuildStatus> {
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 = command.output().context("Failed to execute build")?;
let stdout = from_utf8(&output.stdout).context("Failed to process stdout")?;
let stderr = from_utf8(&output.stderr).context("Failed to process stderr")?;
Ok(BuildStatus {
success: output.status.code().unwrap_or(-1) == 0,
cmdline,
stdout: stdout.to_string(),
stderr: stderr.to_string(),
})
}
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 = 3;
if config.build_target && target_path_rel.is_some() {
total += 1;
}
if config.build_base && base_path_rel.is_some() {
total += 1;
}
let first_status = match target_path_rel {
Some(target_path_rel) if config.build_target => {
update_status(
context,
format!("Building target {}", target_path_rel.display()),
0,
total,
&cancel,
)?;
run_make(&config.build_config, target_path_rel)
}
_ => BuildStatus::default(),
};
let second_status = match base_path_rel {
Some(base_path_rel) if config.build_base => {
update_status(
context,
format!("Building base {}", base_path_rel.display()),
0,
total,
&cancel,
)?;
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()),
2,
total,
&cancel,
)?;
Some(read::read(target_path).with_context(|| {
format!("Failed to read object '{}'", target_path.display())
})?)
}
_ => 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()),
3,
total,
&cancel,
)?;
Some(
read::read(base_path)
.with_context(|| format!("Failed to read object '{}'", base_path.display()))?,
)
}
_ => None,
};
update_status(context, "Performing diff".to_string(), 4, total, &cancel)?;
let result = diff_objs(&config.diff_obj_config, first_obj.as_ref(), second_obj.as_ref(), None)?;
update_status(context, "Complete".to_string(), total, 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, "Object diff", Job::ObjDiff, move |context, cancel| {
run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
})
}

View File

@ -1,27 +1,34 @@
#![warn(clippy::all, rust_2018_idioms)]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use std::{path::PathBuf, rc::Rc, sync::Mutex};
mod app;
mod app_config;
mod config;
mod fonts;
mod jobs;
mod update;
mod views;
use anyhow::{Error, Result};
use std::{
path::PathBuf,
rc::Rc,
sync::{Arc, Mutex},
};
use anyhow::{ensure, Result};
use cfg_if::cfg_if;
use eframe::IconData;
use time::UtcOffset;
fn load_icon() -> Result<IconData> {
fn load_icon() -> Result<egui::IconData> {
use bytes::Buf;
let decoder = png::Decoder::new(include_bytes!("../assets/icon_64.png").reader());
let mut reader = decoder.read_info()?;
let mut buf = vec![0; reader.output_buffer_size()];
let info = reader.next_frame(&mut buf)?;
if info.bit_depth != png::BitDepth::Eight {
return Err(Error::msg("Invalid bit depth"));
}
if info.color_type != png::ColorType::Rgba {
return Err(Error::msg("Invalid color type"));
}
ensure!(info.bit_depth == png::BitDepth::Eight);
ensure!(info.color_type == png::ColorType::Rgba);
buf.truncate(info.buffer_size());
Ok(IconData { rgba: buf, width: info.width, height: info.height })
Ok(egui::IconData { rgba: buf, width: info.width, height: info.height })
}
// When compiling natively:
@ -41,7 +48,7 @@ fn main() {
eframe::NativeOptions { follow_system_theme: false, ..Default::default() };
match load_icon() {
Ok(data) => {
native_options.icon_data = Some(data);
native_options.viewport.icon = Some(Arc::new(data));
}
Err(e) => {
log::warn!("Failed to load application icon: {}", e);
@ -54,7 +61,7 @@ fn main() {
eframe::run_native(
"objdiff",
native_options,
Box::new(move |cc| Box::new(objdiff::App::new(cc, utc_offset, exec_path_clone))),
Box::new(move |cc| Box::new(app::App::new(cc, utc_offset, exec_path_clone))),
)
.expect("Failed to run eframe application");

View File

@ -0,0 +1,311 @@
use std::sync::Arc;
use egui::{text::LayoutJob, Color32, FontFamily, FontId, TextFormat, TextStyle, Widget};
use time::UtcOffset;
use crate::fonts::load_font_if_needed;
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct Appearance {
pub ui_font: FontId,
pub code_font: FontId,
pub diff_colors: Vec<Color32>,
pub theme: eframe::Theme,
// Applied by theme
#[serde(skip)]
pub text_color: Color32, // GRAY
#[serde(skip)]
pub emphasized_text_color: Color32, // LIGHT_GRAY
#[serde(skip)]
pub deemphasized_text_color: Color32, // DARK_GRAY
#[serde(skip)]
pub highlight_color: Color32, // WHITE
#[serde(skip)]
pub replace_color: Color32, // LIGHT_BLUE
#[serde(skip)]
pub insert_color: Color32, // GREEN
#[serde(skip)]
pub delete_color: Color32, // RED
// Global
#[serde(skip)]
pub utc_offset: UtcOffset,
#[serde(skip)]
pub fonts: FontState,
#[serde(skip)]
pub next_ui_font: Option<FontId>,
#[serde(skip)]
pub next_code_font: Option<FontId>,
}
pub struct FontState {
definitions: egui::FontDefinitions,
source: font_kit::source::SystemSource,
family_names: Vec<String>,
// loaded_families: HashMap<String, LoadedFontFamily>,
}
const DEFAULT_UI_FONT: FontId = FontId { size: 12.0, family: FontFamily::Proportional };
const DEFAULT_CODE_FONT: FontId = FontId { size: 14.0, family: FontFamily::Monospace };
impl Default for Appearance {
fn default() -> Self {
Self {
ui_font: DEFAULT_UI_FONT,
code_font: DEFAULT_CODE_FONT,
diff_colors: DEFAULT_COLOR_ROTATION.to_vec(),
theme: eframe::Theme::Dark,
text_color: Color32::GRAY,
emphasized_text_color: Color32::LIGHT_GRAY,
deemphasized_text_color: Color32::DARK_GRAY,
highlight_color: Color32::WHITE,
replace_color: Color32::LIGHT_BLUE,
insert_color: Color32::GREEN,
delete_color: Color32::from_rgb(200, 40, 41),
utc_offset: UtcOffset::UTC,
fonts: FontState::default(),
next_ui_font: None,
next_code_font: None,
}
}
}
impl Default for FontState {
fn default() -> Self {
Self {
definitions: Default::default(),
source: font_kit::source::SystemSource::new(),
family_names: Default::default(),
// loaded_families: Default::default(),
}
}
}
impl Appearance {
pub fn pre_update(&mut self, ctx: &egui::Context) {
let mut style = ctx.style().as_ref().clone();
style.text_styles.insert(TextStyle::Body, FontId {
size: (self.ui_font.size * 0.75).floor(),
family: self.ui_font.family.clone(),
});
style.text_styles.insert(TextStyle::Body, self.ui_font.clone());
style.text_styles.insert(TextStyle::Button, self.ui_font.clone());
style.text_styles.insert(TextStyle::Heading, FontId {
size: (self.ui_font.size * 1.5).floor(),
family: self.ui_font.family.clone(),
});
style.text_styles.insert(TextStyle::Monospace, self.code_font.clone());
match self.theme {
eframe::Theme::Dark => {
style.visuals = egui::Visuals::dark();
self.text_color = Color32::GRAY;
self.emphasized_text_color = Color32::LIGHT_GRAY;
self.deemphasized_text_color = Color32::DARK_GRAY;
self.highlight_color = Color32::WHITE;
self.replace_color = Color32::LIGHT_BLUE;
self.insert_color = Color32::GREEN;
self.delete_color = Color32::from_rgb(200, 40, 41);
}
eframe::Theme::Light => {
style.visuals = egui::Visuals::light();
self.text_color = Color32::GRAY;
self.emphasized_text_color = Color32::DARK_GRAY;
self.deemphasized_text_color = Color32::LIGHT_GRAY;
self.highlight_color = Color32::BLACK;
self.replace_color = Color32::DARK_BLUE;
self.insert_color = Color32::DARK_GREEN;
self.delete_color = Color32::from_rgb(200, 40, 41);
}
}
ctx.set_style(style);
}
pub fn post_update(&mut self, ctx: &egui::Context) {
// Load fonts for next frame
if let Some(next_ui_font) = self.next_ui_font.take() {
match load_font_if_needed(
ctx,
&self.fonts.source,
&next_ui_font,
DEFAULT_UI_FONT.family,
&mut self.fonts.definitions,
) {
Ok(()) => self.ui_font = next_ui_font,
Err(e) => {
log::error!("Failed to load font: {}", e)
}
}
}
if let Some(next_code_font) = self.next_code_font.take() {
match load_font_if_needed(
ctx,
&self.fonts.source,
&next_code_font,
DEFAULT_CODE_FONT.family,
&mut self.fonts.definitions,
) {
Ok(()) => self.code_font = next_code_font,
Err(e) => {
log::error!("Failed to load font: {}", e)
}
}
}
}
pub fn init_fonts(&mut self, ctx: &egui::Context) {
self.fonts.family_names = self.fonts.source.all_families().unwrap_or_default();
match load_font_if_needed(
ctx,
&self.fonts.source,
&self.ui_font,
DEFAULT_UI_FONT.family,
&mut self.fonts.definitions,
) {
Ok(_) => {}
Err(e) => {
log::error!("Failed to load font: {}", e);
// Revert to default
self.ui_font = DEFAULT_UI_FONT;
}
}
match load_font_if_needed(
ctx,
&self.fonts.source,
&self.code_font,
DEFAULT_CODE_FONT.family,
&mut self.fonts.definitions,
) {
Ok(_) => {}
Err(e) => {
log::error!("Failed to load font: {}", e);
// Revert to default
self.code_font = DEFAULT_CODE_FONT;
}
}
}
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] = [
Color32::from_rgb(255, 0, 255),
Color32::from_rgb(0, 255, 255),
Color32::from_rgb(0, 128, 0),
Color32::from_rgb(255, 0, 0),
Color32::from_rgb(255, 255, 0),
Color32::from_rgb(255, 192, 203),
Color32::from_rgb(0, 0, 255),
Color32::from_rgb(0, 255, 0),
Color32::from_rgb(213, 138, 138),
];
fn font_id_ui(
ui: &mut egui::Ui,
label: &str,
mut font_id: FontId,
default: FontId,
appearance: &Appearance,
) -> Option<FontId> {
ui.push_id(label, |ui| {
let font_size = font_id.size;
let label_job = LayoutJob::simple(
font_id.family.to_string(),
font_id.clone(),
appearance.text_color,
0.0,
);
let mut changed = ui
.horizontal(|ui| {
ui.label(label);
let mut changed = egui::Slider::new(&mut font_id.size, 4.0..=40.0)
.max_decimals(1)
.ui(ui)
.changed();
if ui.button("Reset").clicked() {
font_id = default;
changed = true;
}
changed
})
.inner;
let family = &mut font_id.family;
changed |= egui::ComboBox::from_label("Font family")
.selected_text(label_job)
.width(font_size * 20.0)
.show_ui(ui, |ui| {
let mut result = false;
result |= ui
.selectable_value(family, FontFamily::Proportional, "Proportional (built-in)")
.changed();
result |= ui
.selectable_value(family, FontFamily::Monospace, "Monospace (built-in)")
.changed();
for family_name in &appearance.fonts.family_names {
result |= ui
.selectable_value(
family,
FontFamily::Name(Arc::from(family_name.as_str())),
family_name,
)
.changed();
}
result
})
.inner
.unwrap_or(false);
changed.then_some(font_id)
})
.inner
}
pub fn appearance_window(ctx: &egui::Context, show: &mut bool, appearance: &mut Appearance) {
egui::Window::new("Appearance").open(show).show(ctx, |ui| {
egui::ComboBox::from_label("Theme")
.selected_text(format!("{:?}", appearance.theme))
.show_ui(ui, |ui| {
ui.selectable_value(&mut appearance.theme, eframe::Theme::Dark, "Dark");
ui.selectable_value(&mut appearance.theme, eframe::Theme::Light, "Light");
});
ui.separator();
appearance.next_ui_font =
font_id_ui(ui, "UI font:", appearance.ui_font.clone(), DEFAULT_UI_FONT, appearance);
ui.separator();
appearance.next_code_font = font_id_ui(
ui,
"Code font:",
appearance.code_font.clone(),
DEFAULT_CODE_FONT,
appearance,
);
ui.separator();
ui.label("Diff colors:");
if ui.button("Reset").clicked() {
appearance.diff_colors = DEFAULT_COLOR_ROTATION.to_vec();
}
let mut remove_at: Option<usize> = None;
let num_colors = appearance.diff_colors.len();
for (idx, color) in appearance.diff_colors.iter_mut().enumerate() {
ui.horizontal(|ui| {
ui.color_edit_button_srgba(color);
if num_colors > 1 && ui.small_button("-").clicked() {
remove_at = Some(idx);
}
});
}
if let Some(idx) = remove_at {
appearance.diff_colors.remove(idx);
}
if ui.small_button("+").clicked() {
appearance.diff_colors.push(Color32::BLACK);
}
});
}

View File

@ -1,4 +1,4 @@
#[cfg(feature = "wsl")]
#[cfg(all(windows, feature = "wsl"))]
use std::string::FromUtf16Error;
use std::{
borrow::Cow,
@ -6,20 +6,23 @@ use std::{
path::{PathBuf, MAIN_SEPARATOR},
};
#[cfg(feature = "wsl")]
#[cfg(all(windows, feature = "wsl"))]
use anyhow::{Context, Result};
use const_format::formatcp;
use egui::{
output::OpenUrl, text::LayoutJob, CollapsingHeader, FontFamily, FontId, RichText,
SelectableLabel, TextFormat, Widget, WidgetText,
SelectableLabel, TextFormat, Widget,
};
use globset::Glob;
use objdiff_core::{
config::{ProjectObject, DEFAULT_WATCH_PATTERNS},
diff::X86Formatter,
};
use self_update::cargo_crate_version;
use crate::{
app::{AppConfig, AppConfigRef, ObjectConfig},
config::{ProjectObject, ProjectObjectNode},
diff::DiffAlg,
config::ProjectObjectNode,
jobs::{
check_update::{start_check_update, CheckUpdateResult},
update::start_update,
@ -46,7 +49,7 @@ pub struct ConfigViewState {
pub object_search: String,
pub filter_diffable: bool,
pub filter_incomplete: bool,
#[cfg(feature = "wsl")]
#[cfg(all(windows, feature = "wsl"))]
pub available_wsl_distros: Option<Vec<String>>,
pub file_dialog_state: FileDialogState,
}
@ -93,6 +96,7 @@ impl ConfigViewState {
base_path: Some(path),
reverse_fn_order: None,
complete: None,
scratch: None,
});
} else if let Ok(obj_path) = path.strip_prefix(target_dir) {
let base_path = base_dir.join(obj_path);
@ -102,6 +106,7 @@ impl ConfigViewState {
base_path: Some(base_path),
reverse_fn_order: None,
complete: None,
scratch: None,
});
}
}
@ -129,12 +134,7 @@ 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(feature = "wsl")]
#[cfg(all(windows, feature = "wsl"))]
fn process_utf16(bytes: &[u8]) -> Result<String, FromUtf16Error> {
let u16_bytes: Vec<u16> = bytes
.chunks_exact(2)
@ -143,7 +143,7 @@ fn process_utf16(bytes: &[u8]) -> Result<String, FromUtf16Error> {
String::from_utf16(&u16_bytes)
}
#[cfg(feature = "wsl")]
#[cfg(all(windows, feature = "wsl"))]
fn wsl_cmd(args: &[&str]) -> Result<String> {
use std::{os::windows::process::CommandExt, process::Command};
let output = Command::new("wsl")
@ -154,7 +154,7 @@ fn wsl_cmd(args: &[&str]) -> Result<String> {
process_utf16(&output.stdout).context("Failed to process stdout")
}
#[cfg(feature = "wsl")]
#[cfg(all(windows, feature = "wsl"))]
fn fetch_wsl2_distros() -> Vec<String> {
wsl_cmd(&["-l", "-q"])
.map(|stdout| {
@ -176,7 +176,6 @@ pub fn config_ui(
) {
let mut config_guard = config.write().unwrap();
let AppConfig {
selected_wsl_distro,
target_obj_dir,
base_obj_dir,
selected_obj,
@ -227,27 +226,6 @@ pub fn config_ui(
}
ui.separator();
#[cfg(feature = "wsl")]
{
ui.heading("Build");
if state.available_wsl_distros.is_none() {
state.available_wsl_distros = Some(fetch_wsl2_distros());
}
egui::ComboBox::from_label("Run in WSL2")
.selected_text(selected_wsl_distro.as_ref().unwrap_or(&"Disabled".to_string()))
.show_ui(ui, |ui| {
ui.selectable_value(selected_wsl_distro, None, "Disabled");
for distro in state.available_wsl_distros.as_ref().unwrap() {
ui.selectable_value(selected_wsl_distro, Some(distro.clone()), distro);
}
});
ui.separator();
}
#[cfg(not(feature = "wsl"))]
{
let _ = selected_wsl_distro;
}
ui.horizontal(|ui| {
ui.heading("Project");
if ui.button(RichText::new("Settings")).clicked() {
@ -415,6 +393,7 @@ fn display_object(
base_path: object.base_path.clone(),
reverse_fn_order: object.reverse_fn_order,
complete: object.complete,
scratch: object.scratch.clone(),
});
}
}
@ -540,6 +519,9 @@ fn format_path(path: &Option<PathBuf>, appearance: &Appearance) -> RichText {
RichText::new(text).color(color).family(FontFamily::Monospace)
}
pub const CONFIG_DISABLED_TEXT: &str =
"Option disabled because it's set by the project configuration file.";
fn pick_folder_ui(
ui: &mut egui::Ui,
dir: &Option<PathBuf>,
@ -552,6 +534,7 @@ fn pick_folder_ui(
subheading(ui, label, appearance);
ui.link(HELP_ICON).on_hover_ui(tooltip);
ui.add_enabled(enabled, egui::Button::new("Select"))
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
});
ui.label(format_path(dir, appearance));
response.inner
@ -620,7 +603,7 @@ fn split_obj_config_ui(
ui.separator();
ui.horizontal(|ui| {
subheading(ui, "Custom make program", appearance);
subheading(ui, "Build program", appearance);
ui.link(HELP_ICON).on_hover_ui(|ui| {
let mut job = LayoutJob::default();
job.append("By default, objdiff will build with ", 0.0, text_format.clone());
@ -642,13 +625,38 @@ fn split_obj_config_ui(
});
});
let mut custom_make_str = config.custom_make.clone().unwrap_or_default();
if ui.text_edit_singleline(&mut custom_make_str).changed() {
if ui
.add_enabled(
config.project_config_info.is_none(),
egui::TextEdit::singleline(&mut custom_make_str).hint_text("make"),
)
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.changed()
{
if custom_make_str.is_empty() {
config.custom_make = None;
} else {
config.custom_make = Some(custom_make_str);
}
}
#[cfg(all(windows, feature = "wsl"))]
{
if state.available_wsl_distros.is_none() {
state.available_wsl_distros = Some(fetch_wsl2_distros());
}
egui::ComboBox::from_label("Run in WSL2")
.selected_text(config.selected_wsl_distro.as_ref().unwrap_or(&"Disabled".to_string()))
.show_ui(ui, |ui| {
ui.selectable_value(&mut config.selected_wsl_distro, None, "Disabled");
for distro in state.available_wsl_distros.as_ref().unwrap() {
ui.selectable_value(
&mut config.selected_wsl_distro,
Some(distro.clone()),
distro,
);
}
});
}
ui.separator();
if let Some(project_dir) = config.project_dir.clone() {
@ -679,7 +687,12 @@ fn split_obj_config_ui(
FileDialogResult::TargetDir,
);
}
ui.checkbox(&mut config.build_target, "Build target objects").on_hover_ui(|ui| {
ui.add_enabled(
config.project_config_info.is_none(),
egui::Checkbox::new(&mut config.build_target, "Build target objects"),
)
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.on_hover_ui(|ui| {
let mut job = LayoutJob::default();
job.append(
"Tells the build system to produce the target object.\n",
@ -730,7 +743,12 @@ fn split_obj_config_ui(
FileDialogResult::BaseDir,
);
}
ui.checkbox(&mut config.build_base, "Build base objects").on_hover_ui(|ui| {
ui.add_enabled(
config.project_config_info.is_none(),
egui::Checkbox::new(&mut config.build_base, "Build base objects"),
)
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.on_hover_ui(|ui| {
let mut job = LayoutJob::default();
job.append(
"Tells the build system to produce the base object.\n",
@ -773,7 +791,11 @@ fn split_obj_config_ui(
ui.horizontal(|ui| {
ui.label(RichText::new("File patterns").color(appearance.text_color));
if ui.button("Reset").clicked() {
if ui
.add_enabled(config.project_config_info.is_none(), egui::Button::new("Reset"))
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.clicked()
{
config.watch_patterns =
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect();
config.watcher_change = true;
@ -787,7 +809,11 @@ fn split_obj_config_ui(
.color(appearance.text_color)
.family(FontFamily::Monospace),
);
if ui.small_button("-").clicked() {
if ui
.add_enabled(config.project_config_info.is_none(), egui::Button::new("-").small())
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.clicked()
{
remove_at = Some(idx);
}
});
@ -797,8 +823,16 @@ fn split_obj_config_ui(
config.watcher_change = true;
}
ui.horizontal(|ui| {
egui::TextEdit::singleline(&mut state.watch_pattern_text).desired_width(100.0).show(ui);
if ui.small_button("+").clicked() {
ui.add_enabled(
config.project_config_info.is_none(),
egui::TextEdit::singleline(&mut state.watch_pattern_text).desired_width(100.0),
)
.on_disabled_hover_text(CONFIG_DISABLED_TEXT);
if ui
.add_enabled(config.project_config_info.is_none(), egui::Button::new("+").small())
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.clicked()
{
if let Ok(glob) = Glob::new(&state.watch_pattern_text) {
config.watch_patterns.push(glob);
config.watcher_change = true;
@ -808,69 +842,35 @@ fn split_obj_config_ui(
});
}
pub fn diff_options_window(
pub fn diff_config_window(
ctx: &egui::Context,
config: &AppConfigRef,
show: &mut bool,
appearance: &Appearance,
) {
let mut config_guard = config.write().unwrap();
egui::Window::new("Diff Options").open(show).show(ctx, |ui| {
diff_options_ui(ui, &mut config_guard, appearance);
egui::Window::new("Diff Config").open(show).show(ctx, |ui| {
diff_config_ui(ui, &mut config_guard, appearance);
});
}
fn diff_options_ui(ui: &mut egui::Ui, config: &mut AppConfig, appearance: &Appearance) {
let mut job = LayoutJob::default();
job.append(
"Current default: ",
0.0,
TextFormat::simple(appearance.ui_font.clone(), appearance.text_color),
);
job.append(
diff_alg_to_string(DiffAlg::default()),
0.0,
TextFormat::simple(appearance.ui_font.clone(), appearance.emphasized_text_color),
);
ui.label(job);
let mut job = LayoutJob::default();
job.append(
"Previous default: ",
0.0,
TextFormat::simple(appearance.ui_font.clone(), appearance.text_color),
);
job.append(
"Levenshtein",
0.0,
TextFormat::simple(appearance.ui_font.clone(), appearance.emphasized_text_color),
);
ui.label(job);
ui.label("Please provide feedback!");
if diff_alg_ui(ui, "Code diff algorithm", &mut config.code_alg) {
config.queue_reload = true;
}
if diff_alg_ui(ui, "Data diff algorithm", &mut config.data_alg) {
config.queue_reload = true;
}
}
fn diff_alg_ui(ui: &mut egui::Ui, label: impl Into<WidgetText>, alg: &mut DiffAlg) -> bool {
let response = egui::ComboBox::from_label(label)
.selected_text(diff_alg_to_string(*alg))
fn diff_config_ui(ui: &mut egui::Ui, config: &mut AppConfig, _appearance: &Appearance) {
egui::ComboBox::new("x86_formatter", "X86 Format")
.selected_text(format!("{:?}", config.diff_obj_config.x86_formatter))
.show_ui(ui, |ui| {
ui.selectable_value(alg, DiffAlg::Patience, "Patience").changed()
| ui.selectable_value(alg, DiffAlg::Levenshtein, "Levenshtein").changed()
| ui.selectable_value(alg, DiffAlg::Myers, "Myers").changed()
| ui.selectable_value(alg, DiffAlg::Lcs, "LCS").changed()
for &formatter in
&[X86Formatter::Intel, X86Formatter::Gas, X86Formatter::Nasm, X86Formatter::Masm]
{
if ui
.selectable_label(
config.diff_obj_config.x86_formatter == formatter,
format!("{:?}", formatter),
)
.clicked()
{
config.diff_obj_config.x86_formatter = formatter;
config.queue_reload = true;
}
}
});
response.inner.unwrap_or(false)
}
const fn diff_alg_to_string(alg: DiffAlg) -> &'static str {
match alg {
DiffAlg::Patience => "Patience",
DiffAlg::Levenshtein => "Levenshtein",
DiffAlg::Lcs => "LCS",
DiffAlg::Myers => "Myers",
}
}

View File

@ -1,22 +1,23 @@
use std::{cmp::min, default::Default, mem::take};
use egui::{text::LayoutJob, Align, Label, Layout, Sense, Vec2};
use egui::{text::LayoutJob, Align, Label, Layout, Sense, Vec2, Widget};
use egui_extras::{Column, TableBuilder};
use objdiff_core::{
diff::{ObjDataDiff, ObjDataDiffKind, ObjDiff},
obj::ObjInfo,
};
use time::format_description;
use crate::{
obj::{ObjDataDiff, ObjDataDiffKind, ObjInfo, ObjSection},
views::{
appearance::Appearance,
symbol_diff::{DiffViewState, SymbolReference, View},
write_text,
},
use crate::views::{
appearance::Appearance,
symbol_diff::{DiffViewState, SymbolRefByName, View},
write_text,
};
const BYTES_PER_ROW: usize = 16;
fn find_section<'a>(obj: &'a ObjInfo, selected_symbol: &SymbolReference) -> Option<&'a ObjSection> {
obj.sections.iter().find(|section| section.name == selected_symbol.section_name)
fn find_section(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<usize> {
obj.sections.iter().position(|section| section.name == selected_symbol.section_name)
}
fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appearance: &Appearance) {
@ -90,7 +91,7 @@ fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appeara
write_text(text.as_str(), base_color, &mut job, appearance.code_font.clone());
}
}
ui.add(Label::new(job).sense(Sense::click()));
Label::new(job).sense(Sense::click()).ui(ui);
// .on_hover_ui_at_pointer(|ui| ins_hover_ui(ui, ins))
// .context_menu(|ui| ins_context_menu(ui, ins));
}
@ -132,16 +133,21 @@ fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> {
fn data_table_ui(
table: TableBuilder<'_>,
left_obj: Option<&ObjInfo>,
right_obj: Option<&ObjInfo>,
selected_symbol: &SymbolReference,
left_obj: Option<&(ObjInfo, ObjDiff)>,
right_obj: Option<&(ObjInfo, ObjDiff)>,
selected_symbol: &SymbolRefByName,
config: &Appearance,
) -> Option<()> {
let left_section = left_obj.and_then(|obj| find_section(obj, selected_symbol));
let right_section = right_obj.and_then(|obj| find_section(obj, selected_symbol));
let left_section = left_obj.and_then(|(obj, diff)| {
find_section(obj, selected_symbol).map(|i| (&obj.sections[i], &diff.sections[i]))
});
let right_section = right_obj.and_then(|(obj, diff)| {
find_section(obj, selected_symbol).map(|i| (&obj.sections[i], &diff.sections[i]))
});
let total_bytes = left_section
.or(right_section)?
.1
.data_diff
.iter()
.fold(0usize, |accum, item| accum + item.len);
@ -150,11 +156,12 @@ fn data_table_ui(
}
let total_rows = (total_bytes - 1) / BYTES_PER_ROW + 1;
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 left_diffs = left_section.map(|(_, section)| split_diffs(&section.data_diff));
let right_diffs = right_section.map(|(_, section)| split_diffs(&section.data_diff));
table.body(|body| {
body.rows(config.code_font.size, total_rows, |row_index, mut row| {
body.rows(config.code_font.size, total_rows, |mut row| {
let row_index = row.index();
let address = row_index * BYTES_PER_ROW;
row.col(|ui| {
if let Some(left_diffs) = &left_diffs {
@ -191,7 +198,7 @@ pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &A
|ui| {
ui.set_width(column_width);
if ui.button("Back").clicked() {
if ui.button("Back").clicked() {
state.current_view = View::SymbolDiff;
}
@ -251,6 +258,7 @@ pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &A
ui.separator();
// Table
ui.style_mut().interaction.selectable_labels = false;
let available_height = ui.available_height();
let table = TableBuilder::new(ui)
.striped(false)

View File

@ -0,0 +1,450 @@
use std::default::Default;
use egui::{text::LayoutJob, Align, Label, Layout, Sense, Vec2, Widget};
use egui_extras::{Column, TableBuilder, TableRow};
use objdiff_core::{
arch::ObjArch,
diff::{
display::{display_diff, DiffText, HighlightKind},
ObjDiff, ObjInsDiff, ObjInsDiffKind,
},
obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSymbol, SymbolRef},
};
use time::format_description;
use crate::views::{
appearance::Appearance,
symbol_diff::{match_color_for_symbol, DiffViewState, SymbolRefByName, View},
};
#[derive(Default)]
pub struct FunctionViewState {
pub highlight: HighlightKind,
}
fn ins_hover_ui(
ui: &mut egui::Ui,
arch: &dyn ObjArch,
section: &ObjSection,
ins: &ObjIns,
appearance: &Appearance,
) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
let offset = ins.address - section.address;
ui.label(format!(
"{:02X?}",
&section.data[offset as usize..(offset + ins.size as u64) as usize]
));
if let Some(orig) = &ins.orig {
ui.label(format!("Original: {}", orig));
}
for arg in &ins.args {
if let ObjInsArg::Arg(arg) = arg {
match arg {
ObjInsArgValue::Signed(v) => {
ui.label(format!("{arg} == {v}"));
}
ObjInsArgValue::Unsigned(v) => {
ui.label(format!("{arg} == {v}"));
}
_ => {}
}
}
}
if let Some(reloc) = &ins.reloc {
ui.label(format!("Relocation type: {}", arch.display_reloc(reloc.flags)));
ui.colored_label(appearance.highlight_color, format!("Name: {}", reloc.target.name));
if let Some(section) = &reloc.target_section {
ui.colored_label(appearance.highlight_color, format!("Section: {section}"));
ui.colored_label(
appearance.highlight_color,
format!("Address: {:x}", reloc.target.address),
);
ui.colored_label(
appearance.highlight_color,
format!("Size: {:x}", reloc.target.size),
);
} else {
ui.colored_label(appearance.highlight_color, "Extern".to_string());
}
}
});
}
fn ins_context_menu(ui: &mut egui::Ui, ins: &ObjIns) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
// if ui.button("Copy hex").clicked() {}
for arg in &ins.args {
if let ObjInsArg::Arg(arg) = arg {
match arg {
ObjInsArgValue::Signed(v) => {
if ui.button(format!("Copy \"{arg}\"")).clicked() {
ui.output_mut(|output| output.copied_text = arg.to_string());
ui.close_menu();
}
if ui.button(format!("Copy \"{v}\"")).clicked() {
ui.output_mut(|output| output.copied_text = v.to_string());
ui.close_menu();
}
}
ObjInsArgValue::Unsigned(v) => {
if ui.button(format!("Copy \"{arg}\"")).clicked() {
ui.output_mut(|output| output.copied_text = arg.to_string());
ui.close_menu();
}
if ui.button(format!("Copy \"{v}\"")).clicked() {
ui.output_mut(|output| output.copied_text = v.to_string());
ui.close_menu();
}
}
_ => {}
}
}
}
if let Some(reloc) = &ins.reloc {
if let Some(name) = &reloc.target.demangled_name {
if ui.button(format!("Copy \"{name}\"")).clicked() {
ui.output_mut(|output| output.copied_text.clone_from(name));
ui.close_menu();
}
}
if ui.button(format!("Copy \"{}\"", reloc.target.name)).clicked() {
ui.output_mut(|output| output.copied_text.clone_from(&reloc.target.name));
ui.close_menu();
}
}
});
}
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 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;
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;
}
DiffText::Opcode(mnemonic, _op) => {
label_text = mnemonic.to_string();
if ins_diff.kind == ObjInsDiffKind::OpMismatch {
base_color = appearance.replace_color;
}
pad_to = 8;
}
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()]
}
}
DiffText::BranchDest(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 = appearance.emphasized_text_color;
}
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 == text;
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 = text.into();
}
}
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,
symbol: &ObjSymbol,
appearance: &Appearance,
ins_view_state: &mut FunctionViewState,
) {
ui.spacing_mut().item_spacing.x = 0.0;
if ins_diff.kind != ObjInsDiffKind::None {
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
}
let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' '));
display_diff(ins_diff, symbol.address, |text| {
diff_text_ui(ui, text, ins_diff, appearance, ins_view_state, space_width);
Ok::<_, ()>(())
})
.unwrap();
}
fn asm_col_ui(
row: &mut TableRow<'_, '_>,
obj: &(ObjInfo, ObjDiff),
symbol_ref: SymbolRef,
appearance: &Appearance,
ins_view_state: &mut FunctionViewState,
) {
let (section, symbol) = obj.0.section_symbol(symbol_ref);
let ins_diff = &obj.1.symbol_diff(symbol_ref).instructions[row.index()];
let (_, response) = row.col(|ui| {
asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state);
});
if let Some(ins) = &ins_diff.ins {
response.on_hover_ui_at_pointer(|ui| {
ins_hover_ui(ui, obj.0.arch.as_ref(), section, ins, appearance)
});
}
}
fn empty_col_ui(row: &mut TableRow<'_, '_>) {
row.col(|ui| {
ui.label("");
});
}
fn asm_table_ui(
table: TableBuilder<'_>,
left_obj: Option<&(ObjInfo, ObjDiff)>,
right_obj: Option<&(ObjInfo, ObjDiff)>,
selected_symbol: &SymbolRefByName,
appearance: &Appearance,
ins_view_state: &mut FunctionViewState,
) -> Option<()> {
let left_symbol = left_obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol));
let right_symbol = right_obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol));
let instructions_len = match (left_symbol, right_symbol) {
(Some(left_symbol_ref), Some(right_symbol_ref)) => {
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();
debug_assert_eq!(left_len, right_len);
left_len
}
(Some(left_symbol_ref), None) => {
left_obj.unwrap().1.symbol_diff(left_symbol_ref).instructions.len()
}
(None, Some(right_symbol_ref)) => {
right_obj.unwrap().1.symbol_diff(right_symbol_ref).instructions.len()
}
(None, None) => return None,
};
table.body(|body| {
body.rows(appearance.code_font.size, instructions_len, |mut row| {
if let (Some(left_obj), Some(left_symbol_ref)) = (left_obj, left_symbol) {
asm_col_ui(&mut row, left_obj, left_symbol_ref, appearance, ins_view_state);
} else {
empty_col_ui(&mut row);
}
if let (Some(right_obj), Some(right_symbol_ref)) = (right_obj, right_symbol) {
asm_col_ui(&mut row, right_obj, right_symbol_ref, appearance, ins_view_state);
} else {
empty_col_ui(&mut row);
}
});
});
Some(())
}
pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
else {
return;
};
// Header
let available_width = ui.available_width();
let column_width = available_width / 2.0;
ui.allocate_ui_with_layout(
Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min),
|ui| {
// 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| {
if ui.button("⏴ Back").clicked() {
state.current_view = View::SymbolDiff;
}
ui.separator();
if ui
.add_enabled(
!state.scratch_running && state.scratch_available,
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()
{
state.queue_scratch = true;
}
});
let name = selected_symbol
.demangled_symbol_name
.as_deref()
.unwrap_or(&selected_symbol.symbol_name);
let mut job = LayoutJob::simple(
name.to_string(),
appearance.code_font.clone(),
appearance.highlight_color,
column_width,
);
job.wrap.break_anywhere = true;
job.wrap.max_rows = 1;
ui.label(job);
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
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.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
if state.build_running {
ui.colored_label(appearance.replace_color, "Building…");
} else {
ui.label("Last built:");
let format =
format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label(
result
.time
.to_offset(appearance.utc_offset)
.format(&format)
.unwrap(),
);
}
});
});
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
if let Some(match_percent) = result
.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)
{
ui.colored_label(
match_color_for_symbol(match_percent, appearance),
&format!("{match_percent:.0}%"),
);
} else {
ui.colored_label(appearance.replace_color, "Missing");
}
ui.label("Diff base:");
});
},
);
},
);
ui.separator();
// Table
ui.style_mut().interaction.selectable_labels = false;
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), 2)
.resizable(false)
.auto_shrink([false, false])
.min_scrolled_height(available_height);
asm_table_ui(
table,
result.first_obj.as_ref(),
result.second_obj.as_ref(),
selected_symbol,
appearance,
&mut state.function_state,
);
}

View File

@ -1,23 +1,28 @@
use std::mem::take;
use egui::{
text::LayoutJob, Align, CollapsingHeader, Color32, Layout, ScrollArea, SelectableLabel,
TextEdit, Ui, Vec2, Widget,
text::LayoutJob, Align, CollapsingHeader, Color32, Id, Layout, OpenUrl, ScrollArea,
SelectableLabel, TextEdit, Ui, Vec2, Widget,
};
use egui_extras::{Size, StripBuilder};
use objdiff_core::{
diff::{ObjDiff, ObjSymbolDiff},
obj::{ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags},
};
use crate::{
app::AppConfigRef,
jobs::{
create_scratch::{start_create_scratch, CreateScratchConfig, CreateScratchResult},
objdiff::{BuildStatus, ObjDiffResult},
Job, JobQueue, JobResult,
},
obj::{ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags},
views::{appearance::Appearance, function_diff::FunctionViewState, write_text},
};
pub struct SymbolReference {
pub struct SymbolRefByName {
pub symbol_name: String,
pub demangled_symbol_name: Option<String>,
pub section_name: String,
}
@ -33,18 +38,22 @@ pub enum View {
#[derive(Default)]
pub struct DiffViewState {
pub build: Option<Box<ObjDiffResult>>,
pub scratch: Option<Box<CreateScratchResult>>,
pub current_view: View,
pub symbol_state: SymbolViewState,
pub function_state: FunctionViewState,
pub search: String,
pub queue_build: bool,
pub build_running: bool,
pub scratch_available: bool,
pub queue_scratch: bool,
pub scratch_running: bool,
}
#[derive(Default)]
pub struct SymbolViewState {
pub highlighted_symbol: Option<String>,
pub selected_symbol: Option<SymbolReference>,
pub selected_symbol: Option<SymbolRefByName>,
pub reverse_fn_order: bool,
pub disable_reverse_fn_order: bool,
pub show_hidden_symbols: bool,
@ -52,15 +61,19 @@ pub struct SymbolViewState {
impl DiffViewState {
pub fn pre_update(&mut self, jobs: &mut JobQueue, config: &AppConfigRef) {
jobs.results.retain_mut(|result| {
if let JobResult::ObjDiff(result) = result {
jobs.results.retain_mut(|result| match result {
JobResult::ObjDiff(result) => {
self.build = take(result);
false
} else {
true
}
JobResult::CreateScratch(result) => {
self.scratch = take(result);
false
}
_ => true,
});
self.build_running = jobs.is_running(Job::ObjDiff);
self.scratch_running = jobs.is_running(Job::CreateScratch);
self.symbol_state.disable_reverse_fn_order = false;
if let Ok(config) = config.read() {
@ -70,16 +83,41 @@ impl DiffViewState {
self.symbol_state.disable_reverse_fn_order = true;
}
}
self.scratch_available = CreateScratchConfig::is_available(&config);
}
}
pub fn post_update(&mut self, config: &AppConfigRef) {
pub fn post_update(&mut self, ctx: &egui::Context, jobs: &mut JobQueue, config: &AppConfigRef) {
if let Some(result) = take(&mut self.scratch) {
ctx.output_mut(|o| o.open_url = Some(OpenUrl::new_tab(result.scratch_url)));
}
if self.queue_build {
self.queue_build = false;
if let Ok(mut config) = config.write() {
config.queue_build = true;
}
}
if self.queue_scratch {
self.queue_scratch = false;
if let Some(function_name) =
self.symbol_state.selected_symbol.as_ref().map(|sym| sym.symbol_name.clone())
{
if let Ok(config) = config.read() {
match CreateScratchConfig::from_config(&config, function_name) {
Ok(config) => {
jobs.push_once(Job::CreateScratch, || {
start_create_scratch(ctx, config)
});
}
Err(err) => {
log::error!("Failed to create scratch config: {err}");
}
}
}
}
}
}
}
@ -100,14 +138,20 @@ fn symbol_context_menu_ui(ui: &mut Ui, symbol: &ObjSymbol) {
if let Some(name) = &symbol.demangled_name {
if ui.button(format!("Copy \"{name}\"")).clicked() {
ui.output_mut(|output| output.copied_text = name.clone());
ui.output_mut(|output| output.copied_text.clone_from(name));
ui.close_menu();
}
}
if ui.button(format!("Copy \"{}\"", symbol.name)).clicked() {
ui.output_mut(|output| output.copied_text = symbol.name.clone());
ui.output_mut(|output| output.copied_text.clone_from(&symbol.name));
ui.close_menu();
}
if let Some(address) = symbol.virtual_address {
if ui.button(format!("Copy \"{:#x}\" (virtual address)", address)).clicked() {
ui.output_mut(|output| output.copied_text = format!("{:#x}", address));
ui.close_menu();
}
}
});
}
@ -126,6 +170,12 @@ fn symbol_hover_ui(ui: &mut Ui, symbol: &ObjSymbol, appearance: &Appearance) {
format!("Size: {:x} (assumed)", symbol.size),
);
}
if let Some(address) = symbol.virtual_address {
ui.colored_label(
appearance.highlight_color,
format!("Virtual address: {:#x}", address),
);
}
});
}
@ -133,6 +183,7 @@ fn symbol_hover_ui(ui: &mut Ui, symbol: &ObjSymbol, appearance: &Appearance) {
fn symbol_ui(
ui: &mut Ui,
symbol: &ObjSymbol,
symbol_diff: &ObjSymbolDiff,
section: Option<&ObjSection>,
state: &mut SymbolViewState,
appearance: &Appearance,
@ -163,7 +214,7 @@ fn symbol_ui(
write_text("h", appearance.deemphasized_text_color, &mut job, appearance.code_font.clone());
}
write_text("] ", appearance.text_color, &mut job, appearance.code_font.clone());
if let Some(match_percent) = symbol.match_percent {
if let Some(match_percent) = symbol_diff.match_percent {
write_text("(", appearance.text_color, &mut job, appearance.code_font.clone());
write_text(
&format!("{match_percent:.0}%"),
@ -176,19 +227,21 @@ fn symbol_ui(
write_text(name, appearance.highlight_color, &mut job, appearance.code_font.clone());
let response = SelectableLabel::new(selected, job)
.ui(ui)
.context_menu(|ui| symbol_context_menu_ui(ui, symbol))
.on_hover_ui_at_pointer(|ui| symbol_hover_ui(ui, symbol, appearance));
response.context_menu(|ui| symbol_context_menu_ui(ui, symbol));
if response.clicked() {
if let Some(section) = section {
if section.kind == ObjSectionKind::Code {
state.selected_symbol = Some(SymbolReference {
state.selected_symbol = Some(SymbolRefByName {
symbol_name: symbol.name.clone(),
demangled_symbol_name: symbol.demangled_name.clone(),
section_name: section.name.clone(),
});
ret = Some(View::FunctionDiff);
} else if section.kind == ObjSectionKind::Data {
state.selected_symbol = Some(SymbolReference {
state.selected_symbol = Some(SymbolRefByName {
symbol_name: section.name.clone(),
demangled_symbol_name: None,
section_name: section.name.clone(),
});
ret = Some(View::DataDiff);
@ -213,7 +266,7 @@ fn symbol_matches_search(symbol: &ObjSymbol, search_str: &str) -> bool {
#[must_use]
fn symbol_list_ui(
ui: &mut Ui,
obj: &ObjInfo,
obj: &(ObjInfo, ObjDiff),
state: &mut SymbolViewState,
lower_search: &str,
appearance: &Appearance,
@ -224,33 +277,50 @@ fn symbol_list_ui(
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
if !obj.common.is_empty() {
if !obj.0.common.is_empty() {
CollapsingHeader::new(".comm").default_open(true).show(ui, |ui| {
for symbol in &obj.common {
ret = ret.or(symbol_ui(ui, symbol, None, state, appearance));
for (symbol, symbol_diff) in obj.0.common.iter().zip(&obj.1.common) {
ret = ret.or(symbol_ui(ui, symbol, symbol_diff, None, state, appearance));
}
});
}
for section in &obj.sections {
for (section, section_diff) in obj.0.sections.iter().zip(&obj.1.sections) {
CollapsingHeader::new(format!("{} ({:x})", section.name, section.size))
.id_source(Id::new(section.name.clone()).with(section.orig_index))
.default_open(true)
.show(ui, |ui| {
if section.kind == ObjSectionKind::Code && state.reverse_fn_order {
for symbol in section.symbols.iter().rev() {
for (symbol, symbol_diff) in
section.symbols.iter().zip(&section_diff.symbols).rev()
{
if !symbol_matches_search(symbol, lower_search) {
continue;
}
ret =
ret.or(symbol_ui(ui, symbol, Some(section), state, appearance));
ret = ret.or(symbol_ui(
ui,
symbol,
symbol_diff,
Some(section),
state,
appearance,
));
}
} else {
for symbol in &section.symbols {
for (symbol, symbol_diff) in
section.symbols.iter().zip(&section_diff.symbols)
{
if !symbol_matches_search(symbol, lower_search) {
continue;
}
ret =
ret.or(symbol_ui(ui, symbol, Some(section), state, appearance));
ret = ret.or(symbol_ui(
ui,
symbol,
symbol_diff,
Some(section),
state,
appearance,
));
}
}
});
@ -262,11 +332,23 @@ fn symbol_list_ui(
fn build_log_ui(ui: &mut Ui, status: &BuildStatus, appearance: &Appearance) {
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
ui.horizontal(|ui| {
if ui.button("Copy command").clicked() {
ui.output_mut(|output| output.copied_text.clone_from(&status.cmdline));
}
if ui.button("Copy log").clicked() {
ui.output_mut(|output| {
output.copied_text = format!("{}\n{}", status.stdout, status.stderr)
});
}
});
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.colored_label(appearance.replace_color, &status.log);
ui.label(&status.cmdline);
ui.colored_label(appearance.replace_color, &status.stdout);
ui.colored_label(appearance.delete_color, &status.stderr);
});
});
}

View File

@ -1,207 +0,0 @@
use std::{
fs::File,
io::Read,
path::{Component, Path, PathBuf},
};
use anyhow::{bail, Result};
use filetime::FileTime;
use globset::{Glob, GlobSet, GlobSetBuilder};
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<String>,
#[serde(default)]
pub custom_make: Option<String>,
#[serde(default)]
pub target_dir: Option<PathBuf>,
#[serde(default)]
pub base_dir: Option<PathBuf>,
#[serde(default = "bool_true")]
pub build_base: bool,
#[serde(default)]
pub build_target: bool,
#[serde(default)]
pub watch_patterns: Option<Vec<Glob>>,
#[serde(default, alias = "units")]
pub objects: Vec<ProjectObject>,
}
#[derive(Default, Clone, serde::Deserialize)]
pub struct ProjectObject {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub path: Option<PathBuf>,
#[serde(default)]
pub target_path: Option<PathBuf>,
#[serde(default)]
pub base_path: Option<PathBuf>,
#[serde(default)]
pub reverse_fn_order: Option<bool>,
#[serde(default)]
pub complete: Option<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]"
}
}
}
#[derive(Clone)]
pub enum ProjectObjectNode {
File(String, ProjectObject),
Dir(String, Vec<ProjectObjectNode>),
}
fn find_dir<'a>(
name: &str,
nodes: &'a mut Vec<ProjectObjectNode>,
) -> &'a mut Vec<ProjectObjectNode> {
if let Some(index) = nodes
.iter()
.position(|node| matches!(node, ProjectObjectNode::Dir(dir_name, _) if dir_name == name))
{
if let ProjectObjectNode::Dir(_, children) = &mut nodes[index] {
return children;
}
} else {
nodes.push(ProjectObjectNode::Dir(name.to_string(), vec![]));
if let Some(ProjectObjectNode::Dir(_, children)) = nodes.last_mut() {
return children;
}
}
unreachable!();
}
fn build_nodes(
objects: &[ProjectObject],
project_dir: &Path,
target_obj_dir: &Option<PathBuf>,
base_obj_dir: &Option<PathBuf>,
) -> Vec<ProjectObjectNode> {
let mut nodes = vec![];
for object in objects {
let mut out_nodes = &mut nodes;
let path = if let Some(name) = &object.name {
Path::new(name)
} else if let Some(path) = &object.path {
path
} else {
continue;
};
if let Some(parent) = path.parent() {
for component in parent.components() {
if let Component::Normal(name) = component {
let name = name.to_str().unwrap();
out_nodes = find_dir(name, out_nodes);
}
}
}
let mut object = 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));
}
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(());
};
if let Some((result, info)) = try_project_config(project_dir) {
let project_config = result?;
if let Some(min_version) = &project_config.min_version {
let version_str = env!("CARGO_PKG_VERSION");
let version = semver::Version::parse(version_str).unwrap();
let version_req = semver::VersionReq::parse(&format!(">={min_version}"))?;
if !version_req.matches(&version) {
bail!("Project requires objdiff version {} or higher", min_version);
}
}
config.custom_make = project_config.custom_make;
config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p));
config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p));
config.build_base = project_config.build_base;
config.build_target = project_config.build_target;
config.watch_patterns = project_config.watch_patterns.unwrap_or_else(|| {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
});
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.project_config_info = Some(info);
}
Ok(())
}
fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, 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<R: Read>(reader: &mut R) -> Result<ProjectConfig> {
Ok(serde_yaml::from_reader(reader)?)
}
fn read_json_config<R: Read>(reader: &mut R) -> Result<ProjectConfig> {
Ok(serde_json::from_reader(reader)?)
}
pub fn build_globset(vec: &[Glob]) -> std::result::Result<GlobSet, globset::Error> {
let mut builder = GlobSetBuilder::new();
for glob in vec {
builder.add(glob.clone());
}
builder.build()
}

View File

@ -1,478 +0,0 @@
use std::{
cmp::max,
collections::BTreeMap,
time::{Duration, Instant},
};
use anyhow::Result;
use similar::{capture_diff_slices_deadline, Algorithm};
use crate::{
diff::{
editops::{editops_find, LevEditType},
DiffAlg, ProcessCodeResult,
},
obj::{
mips, ppc, ObjArchitecture, ObjInfo, ObjInsArg, ObjInsArgDiff, ObjInsBranchFrom,
ObjInsBranchTo, ObjInsDiff, ObjInsDiffKind, ObjReloc, ObjSymbol, ObjSymbolFlags,
},
};
pub fn no_diff_code(
arch: ObjArchitecture,
data: &[u8],
symbol: &mut ObjSymbol,
relocs: &[ObjReloc],
line_info: &Option<BTreeMap<u32, u32>>,
) -> Result<()> {
let code =
&data[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
let out = match arch {
ObjArchitecture::PowerPc => ppc::process_code(code, symbol.address, relocs, line_info)?,
ObjArchitecture::Mips => mips::process_code(
code,
symbol.address,
symbol.address + symbol.size,
relocs,
line_info,
)?,
};
let mut diff = Vec::<ObjInsDiff>::new();
for i in out.insts {
diff.push(ObjInsDiff { ins: Some(i), kind: ObjInsDiffKind::None, ..Default::default() });
}
resolve_branches(&mut diff);
symbol.instructions = diff;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn diff_code(
alg: DiffAlg,
arch: ObjArchitecture,
left_data: &[u8],
right_data: &[u8],
left_symbol: &mut ObjSymbol,
right_symbol: &mut ObjSymbol,
left_relocs: &[ObjReloc],
right_relocs: &[ObjReloc],
left_line_info: &Option<BTreeMap<u32, u32>>,
right_line_info: &Option<BTreeMap<u32, u32>>,
) -> Result<()> {
let left_code = &left_data[left_symbol.section_address as usize
..(left_symbol.section_address + left_symbol.size) as usize];
let right_code = &right_data[right_symbol.section_address as usize
..(right_symbol.section_address + right_symbol.size) as usize];
let (left_out, right_out) = match arch {
ObjArchitecture::PowerPc => (
ppc::process_code(left_code, left_symbol.address, left_relocs, left_line_info)?,
ppc::process_code(right_code, right_symbol.address, right_relocs, right_line_info)?,
),
ObjArchitecture::Mips => (
mips::process_code(
left_code,
left_symbol.address,
left_symbol.address + left_symbol.size,
left_relocs,
left_line_info,
)?,
mips::process_code(
right_code,
right_symbol.address,
left_symbol.address + left_symbol.size,
right_relocs,
right_line_info,
)?,
),
};
let mut left_diff = Vec::<ObjInsDiff>::new();
let mut right_diff = Vec::<ObjInsDiff>::new();
match alg {
DiffAlg::Levenshtein => {
diff_instructions_lev(
&mut left_diff,
&mut right_diff,
left_symbol,
right_symbol,
&left_out,
&right_out,
)?;
}
DiffAlg::Lcs => {
diff_instructions_similar(
Algorithm::Lcs,
&mut left_diff,
&mut right_diff,
&left_out,
&right_out,
)?;
}
DiffAlg::Myers => {
diff_instructions_similar(
Algorithm::Myers,
&mut left_diff,
&mut right_diff,
&left_out,
&right_out,
)?;
}
DiffAlg::Patience => {
diff_instructions_similar(
Algorithm::Patience,
&mut left_diff,
&mut right_diff,
&left_out,
&right_out,
)?;
}
}
resolve_branches(&mut left_diff);
resolve_branches(&mut right_diff);
let mut diff_state = InsDiffState::default();
for (left, right) in left_diff.iter_mut().zip(right_diff.iter_mut()) {
let result = compare_ins(left, right, &mut diff_state)?;
left.kind = result.kind;
right.kind = result.kind;
left.arg_diff = result.left_args_diff;
right.arg_diff = result.right_args_diff;
}
let total = left_out.insts.len();
let percent = if diff_state.diff_count >= total {
0.0
} else {
((total - diff_state.diff_count) as f32 / total as f32) * 100.0
};
left_symbol.match_percent = Some(percent);
right_symbol.match_percent = Some(percent);
left_symbol.instructions = left_diff;
right_symbol.instructions = right_diff;
Ok(())
}
fn diff_instructions_similar(
alg: Algorithm,
left_diff: &mut Vec<ObjInsDiff>,
right_diff: &mut Vec<ObjInsDiff>,
left_code: &ProcessCodeResult,
right_code: &ProcessCodeResult,
) -> Result<()> {
let deadline = Instant::now() + Duration::from_secs(5);
let ops = capture_diff_slices_deadline(alg, &left_code.ops, &right_code.ops, Some(deadline));
if ops.is_empty() {
left_diff.extend(
left_code
.insts
.iter()
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
);
right_diff.extend(
right_code
.insts
.iter()
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
);
return Ok(());
}
for op in ops {
let (_tag, left_range, right_range) = op.as_tag_tuple();
let len = max(left_range.len(), right_range.len());
left_diff.extend(
left_code.insts[left_range.clone()]
.iter()
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
);
right_diff.extend(
right_code.insts[right_range.clone()]
.iter()
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
);
if left_range.len() < len {
left_diff.extend((left_range.len()..len).map(|_| ObjInsDiff::default()));
}
if right_range.len() < len {
right_diff.extend((right_range.len()..len).map(|_| ObjInsDiff::default()));
}
}
Ok(())
}
fn diff_instructions_lev(
left_diff: &mut Vec<ObjInsDiff>,
right_diff: &mut Vec<ObjInsDiff>,
left_symbol: &ObjSymbol,
right_symbol: &ObjSymbol,
left_code: &ProcessCodeResult,
right_code: &ProcessCodeResult,
) -> Result<()> {
let edit_ops = editops_find(&left_code.ops, &right_code.ops);
let mut op_iter = edit_ops.iter();
let mut left_iter = left_code.insts.iter();
let mut right_iter = right_code.insts.iter();
let mut cur_op = op_iter.next();
let mut cur_left = left_iter.next();
let mut cur_right = right_iter.next();
while let Some(op) = cur_op {
let left_addr = op.first_start as u32 * 4;
let right_addr = op.second_start as u32 * 4;
while let (Some(left), Some(right)) = (cur_left, cur_right) {
if (left.address - left_symbol.address as u32) < left_addr {
left_diff.push(ObjInsDiff { ins: Some(left.clone()), ..ObjInsDiff::default() });
right_diff.push(ObjInsDiff { ins: Some(right.clone()), ..ObjInsDiff::default() });
} else {
break;
}
cur_left = left_iter.next();
cur_right = right_iter.next();
}
if let (Some(left), Some(right)) = (cur_left, cur_right) {
if (left.address - left_symbol.address as u32) != left_addr {
return Err(anyhow::Error::msg("Instruction address mismatch (left)"));
}
if (right.address - right_symbol.address as u32) != right_addr {
return Err(anyhow::Error::msg("Instruction address mismatch (right)"));
}
match op.op_type {
LevEditType::Replace => {
left_diff.push(ObjInsDiff { ins: Some(left.clone()), ..ObjInsDiff::default() });
right_diff
.push(ObjInsDiff { ins: Some(right.clone()), ..ObjInsDiff::default() });
cur_left = left_iter.next();
cur_right = right_iter.next();
}
LevEditType::Insert => {
left_diff.push(ObjInsDiff::default());
right_diff
.push(ObjInsDiff { ins: Some(right.clone()), ..ObjInsDiff::default() });
cur_right = right_iter.next();
}
LevEditType::Delete => {
left_diff.push(ObjInsDiff { ins: Some(left.clone()), ..ObjInsDiff::default() });
right_diff.push(ObjInsDiff::default());
cur_left = left_iter.next();
}
}
} else {
break;
}
cur_op = op_iter.next();
}
// Finalize
while cur_left.is_some() || cur_right.is_some() {
left_diff.push(ObjInsDiff { ins: cur_left.cloned(), ..ObjInsDiff::default() });
right_diff.push(ObjInsDiff { ins: cur_right.cloned(), ..ObjInsDiff::default() });
cur_left = left_iter.next();
cur_right = right_iter.next();
}
Ok(())
}
fn resolve_branches(vec: &mut [ObjInsDiff]) {
let mut branch_idx = 0usize;
// Map addresses to indices
let mut addr_map = BTreeMap::<u32, usize>::new();
for (i, ins_diff) in vec.iter().enumerate() {
if let Some(ins) = &ins_diff.ins {
addr_map.insert(ins.address, i);
}
}
// Generate branches
let mut branches = BTreeMap::<usize, ObjInsBranchFrom>::new();
for (i, ins_diff) in vec.iter_mut().enumerate() {
if let Some(ins) = &ins_diff.ins {
// if ins.ins.is_blr() || ins.reloc.is_some() {
// continue;
// }
if let Some(ins_idx) = ins
.args
.iter()
.find_map(|a| if let ObjInsArg::BranchOffset(offs) = a { Some(offs) } else { None })
.and_then(|offs| addr_map.get(&((ins.address as i32 + offs) as u32)))
{
if let Some(branch) = branches.get_mut(ins_idx) {
ins_diff.branch_to =
Some(ObjInsBranchTo { ins_idx: *ins_idx, branch_idx: branch.branch_idx });
branch.ins_idx.push(i);
} else {
ins_diff.branch_to = Some(ObjInsBranchTo { ins_idx: *ins_idx, branch_idx });
branches.insert(*ins_idx, ObjInsBranchFrom { ins_idx: vec![i], branch_idx });
branch_idx += 1;
}
}
}
}
// Store branch from
for (i, branch) in branches {
vec[i].branch_from = Some(branch);
}
}
fn address_eq(left: &ObjSymbol, right: &ObjSymbol) -> bool {
left.address as i64 + left.addend == right.address as i64 + right.addend
}
fn reloc_eq(left_reloc: Option<&ObjReloc>, right_reloc: Option<&ObjReloc>) -> bool {
let (Some(left), Some(right)) = (left_reloc, right_reloc) else {
return false;
};
if left.kind != right.kind {
return false;
}
let name_matches = left.target.name == right.target.name;
match (&left.target_section, &right.target_section) {
(Some(sl), Some(sr)) => {
// Match if section and name or address match
sl == sr && (name_matches || address_eq(&left.target, &right.target))
}
(Some(_), None) => false,
(None, Some(_)) => {
// Match if possibly stripped weak symbol
name_matches && right.target.flags.0.contains(ObjSymbolFlags::Weak)
}
(None, None) => name_matches,
}
}
fn arg_eq(
left: &ObjInsArg,
right: &ObjInsArg,
left_diff: &ObjInsDiff,
right_diff: &ObjInsDiff,
) -> bool {
return match left {
ObjInsArg::PpcArg(l) => match right {
ObjInsArg::PpcArg(r) => format!("{l}") == format!("{r}"),
_ => false,
},
ObjInsArg::Reloc => {
matches!(right, ObjInsArg::Reloc)
&& reloc_eq(
left_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
)
}
ObjInsArg::RelocWithBase => {
matches!(right, ObjInsArg::RelocWithBase)
&& reloc_eq(
left_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
)
}
ObjInsArg::MipsArg(ls) | ObjInsArg::MipsArgWithBase(ls) => {
matches!(right, ObjInsArg::MipsArg(rs) | ObjInsArg::MipsArgWithBase(rs) if ls == rs)
}
ObjInsArg::BranchOffset(_) => {
// Compare dest instruction idx after diffing
left_diff.branch_to.as_ref().map(|b| b.ins_idx)
== right_diff.branch_to.as_ref().map(|b| b.ins_idx)
}
};
}
#[derive(Default)]
struct InsDiffState {
diff_count: usize,
left_arg_idx: usize,
right_arg_idx: usize,
left_args_idx: BTreeMap<String, usize>,
right_args_idx: BTreeMap<String, usize>,
}
#[derive(Default)]
struct InsDiffResult {
kind: ObjInsDiffKind,
left_args_diff: Vec<Option<ObjInsArgDiff>>,
right_args_diff: Vec<Option<ObjInsArgDiff>>,
}
fn compare_ins(
left: &ObjInsDiff,
right: &ObjInsDiff,
state: &mut InsDiffState,
) -> Result<InsDiffResult> {
let mut result = InsDiffResult::default();
if let (Some(left_ins), Some(right_ins)) = (&left.ins, &right.ins) {
if left_ins.args.len() != right_ins.args.len() || left_ins.op != right_ins.op {
// Totally different op
result.kind = ObjInsDiffKind::Replace;
state.diff_count += 1;
return Ok(result);
}
if left_ins.mnemonic != right_ins.mnemonic {
// Same op but different mnemonic, still cmp args
result.kind = ObjInsDiffKind::OpMismatch;
state.diff_count += 1;
}
for (a, b) in left_ins.args.iter().zip(&right_ins.args) {
if arg_eq(a, b, left, right) {
result.left_args_diff.push(None);
result.right_args_diff.push(None);
} else {
if result.kind == ObjInsDiffKind::None {
result.kind = ObjInsDiffKind::ArgMismatch;
state.diff_count += 1;
}
let a_str = match a {
ObjInsArg::PpcArg(arg) => format!("{arg}"),
ObjInsArg::Reloc | ObjInsArg::RelocWithBase => String::new(),
ObjInsArg::MipsArg(str) | ObjInsArg::MipsArgWithBase(str) => str.clone(),
ObjInsArg::BranchOffset(arg) => format!("{arg}"),
};
let a_diff = if let Some(idx) = state.left_args_idx.get(&a_str) {
ObjInsArgDiff { idx: *idx }
} else {
let idx = state.left_arg_idx;
state.left_args_idx.insert(a_str, idx);
state.left_arg_idx += 1;
ObjInsArgDiff { idx }
};
let b_str = match b {
ObjInsArg::PpcArg(arg) => format!("{arg}"),
ObjInsArg::Reloc | ObjInsArg::RelocWithBase => String::new(),
ObjInsArg::MipsArg(str) | ObjInsArg::MipsArgWithBase(str) => str.clone(),
ObjInsArg::BranchOffset(arg) => format!("{arg}"),
};
let b_diff = if let Some(idx) = state.right_args_idx.get(&b_str) {
ObjInsArgDiff { idx: *idx }
} else {
let idx = state.right_arg_idx;
state.right_args_idx.insert(b_str, idx);
state.right_arg_idx += 1;
ObjInsArgDiff { idx }
};
result.left_args_diff.push(Some(a_diff));
result.right_args_diff.push(Some(b_diff));
}
}
} else if left.ins.is_some() {
result.kind = ObjInsDiffKind::Delete;
state.diff_count += 1;
} else {
result.kind = ObjInsDiffKind::Insert;
state.diff_count += 1;
}
Ok(result)
}
pub fn find_section_and_symbol(obj: &ObjInfo, name: &str) -> Option<(usize, usize)> {
for (section_idx, section) in obj.sections.iter().enumerate() {
let symbol_idx = match section.symbols.iter().position(|symbol| symbol.name == name) {
Some(symbol_idx) => symbol_idx,
None => continue,
};
return Some((section_idx, symbol_idx));
}
None
}

View File

@ -1,406 +0,0 @@
use std::{
cmp::{max, min, Ordering},
mem::take,
time::{Duration, Instant},
};
use anyhow::{bail, Result};
use similar::{capture_diff_slices_deadline, Algorithm};
use crate::{
diff::{
editops::{editops_find, LevEditType},
DiffAlg,
},
obj::{ObjDataDiff, ObjDataDiffKind, ObjSection, ObjSymbol},
};
pub fn diff_data(alg: DiffAlg, left: &mut ObjSection, right: &mut ObjSection) -> Result<()> {
match alg {
DiffAlg::Levenshtein => diff_data_lev(left, right),
DiffAlg::Lcs => diff_data_similar(Algorithm::Lcs, left, right),
DiffAlg::Myers => diff_data_similar(Algorithm::Myers, left, right),
DiffAlg::Patience => diff_data_similar(Algorithm::Patience, left, right),
}
}
pub fn diff_bss_symbols(
left_symbols: &mut [ObjSymbol],
right_symbols: &mut [ObjSymbol],
) -> Result<()> {
for left_symbol in left_symbols {
if let Some(right_symbol) = right_symbols.iter_mut().find(|s| s.name == left_symbol.name) {
left_symbol.diff_symbol = Some(right_symbol.name.clone());
right_symbol.diff_symbol = Some(left_symbol.name.clone());
let percent = if left_symbol.size == right_symbol.size { 100.0 } else { 50.0 };
left_symbol.match_percent = Some(percent);
right_symbol.match_percent = Some(percent);
}
}
Ok(())
}
// WIP diff-by-symbol
#[allow(dead_code)]
pub fn diff_data_symbols(left: &mut ObjSection, right: &mut ObjSection) -> Result<()> {
let mut left_ops = Vec::<u32>::with_capacity(left.symbols.len());
let mut right_ops = Vec::<u32>::with_capacity(right.symbols.len());
for left_symbol in &left.symbols {
let data = &left.data
[left_symbol.address as usize..(left_symbol.address + left_symbol.size) as usize];
let hash = twox_hash::xxh3::hash64(data);
left_ops.push(hash as u32);
}
for symbol in &right.symbols {
let data = &right.data[symbol.address as usize..(symbol.address + symbol.size) as usize];
let hash = twox_hash::xxh3::hash64(data);
right_ops.push(hash as u32);
}
let edit_ops = editops_find(&left_ops, &right_ops);
if edit_ops.is_empty() && !left.data.is_empty() {
let mut left_iter = left.symbols.iter_mut();
let mut right_iter = right.symbols.iter_mut();
loop {
let (left_symbol, right_symbol) = match (left_iter.next(), right_iter.next()) {
(Some(l), Some(r)) => (l, r),
(None, None) => break,
_ => return Err(anyhow::Error::msg("L/R mismatch in diff_data_symbols")),
};
let left_data = &left.data
[left_symbol.address as usize..(left_symbol.address + left_symbol.size) as usize];
let right_data = &right.data[right_symbol.address as usize
..(right_symbol.address + right_symbol.size) as usize];
left.data_diff.push(ObjDataDiff {
data: left_data.to_vec(),
kind: ObjDataDiffKind::None,
len: left_symbol.size as usize,
symbol: left_symbol.name.clone(),
});
right.data_diff.push(ObjDataDiff {
data: right_data.to_vec(),
kind: ObjDataDiffKind::None,
len: right_symbol.size as usize,
symbol: right_symbol.name.clone(),
});
left_symbol.diff_symbol = Some(right_symbol.name.clone());
left_symbol.match_percent = Some(100.0);
right_symbol.diff_symbol = Some(left_symbol.name.clone());
right_symbol.match_percent = Some(100.0);
}
return Ok(());
}
Ok(())
}
pub fn diff_data_similar(
alg: Algorithm,
left: &mut ObjSection,
right: &mut ObjSection,
) -> Result<()> {
let deadline = Instant::now() + Duration::from_secs(5);
let ops = capture_diff_slices_deadline(alg, &left.data, &right.data, Some(deadline));
let mut left_diff = Vec::<ObjDataDiff>::new();
let mut right_diff = Vec::<ObjDataDiff>::new();
for op in ops {
let (tag, left_range, right_range) = op.as_tag_tuple();
let left_len = left_range.len();
let right_len = right_range.len();
let mut len = max(left_len, right_len);
let kind = match tag {
similar::DiffTag::Equal => ObjDataDiffKind::None,
similar::DiffTag::Delete => ObjDataDiffKind::Delete,
similar::DiffTag::Insert => ObjDataDiffKind::Insert,
similar::DiffTag::Replace => {
// Ensure replacements are equal length
len = min(left_len, right_len);
ObjDataDiffKind::Replace
}
};
let left_data = &left.data[left_range];
let right_data = &right.data[right_range];
left_diff.push(ObjDataDiff {
data: left_data[..min(len, left_data.len())].to_vec(),
kind,
len,
..Default::default()
});
right_diff.push(ObjDataDiff {
data: right_data[..min(len, right_data.len())].to_vec(),
kind,
len,
..Default::default()
});
if kind == ObjDataDiffKind::Replace {
match left_len.cmp(&right_len) {
Ordering::Less => {
let len = right_len - left_len;
left_diff.push(ObjDataDiff {
data: vec![],
kind: ObjDataDiffKind::Insert,
len,
..Default::default()
});
right_diff.push(ObjDataDiff {
data: right_data[left_len..right_len].to_vec(),
kind: ObjDataDiffKind::Insert,
len,
..Default::default()
});
}
Ordering::Greater => {
let len = left_len - right_len;
left_diff.push(ObjDataDiff {
data: left_data[right_len..left_len].to_vec(),
kind: ObjDataDiffKind::Delete,
len,
..Default::default()
});
right_diff.push(ObjDataDiff {
data: vec![],
kind: ObjDataDiffKind::Delete,
len,
..Default::default()
});
}
Ordering::Equal => {}
}
}
}
left.data_diff = left_diff;
right.data_diff = right_diff;
Ok(())
}
pub fn diff_data_lev(left: &mut ObjSection, right: &mut ObjSection) -> Result<()> {
let matrix_size = (left.data.len() as u64).saturating_mul(right.data.len() as u64);
if matrix_size > 1_000_000_000 {
bail!(
"Data section {} too large for Levenshtein diff ({} * {} = {})",
left.name,
left.data.len(),
right.data.len(),
matrix_size
);
}
let edit_ops = editops_find(&left.data, &right.data);
if edit_ops.is_empty() && !left.data.is_empty() {
left.data_diff = vec![ObjDataDiff {
data: left.data.clone(),
kind: ObjDataDiffKind::None,
len: left.data.len(),
symbol: String::new(),
}];
right.data_diff = vec![ObjDataDiff {
data: right.data.clone(),
kind: ObjDataDiffKind::None,
len: right.data.len(),
symbol: String::new(),
}];
return Ok(());
}
let mut left_diff = Vec::<ObjDataDiff>::new();
let mut right_diff = Vec::<ObjDataDiff>::new();
let mut left_cur = 0usize;
let mut right_cur = 0usize;
let mut cur_op = LevEditType::Replace;
let mut cur_left_data = Vec::<u8>::new();
let mut cur_right_data = Vec::<u8>::new();
for op in edit_ops {
if cur_op != op.op_type || left_cur < op.first_start || right_cur < op.second_start {
match cur_op {
LevEditType::Replace => {
let left_data = take(&mut cur_left_data);
let right_data = take(&mut cur_right_data);
let left_data_len = left_data.len();
let right_data_len = right_data.len();
left_diff.push(ObjDataDiff {
data: left_data,
kind: ObjDataDiffKind::Replace,
len: left_data_len,
symbol: String::new(),
});
right_diff.push(ObjDataDiff {
data: right_data,
kind: ObjDataDiffKind::Replace,
len: right_data_len,
symbol: String::new(),
});
}
LevEditType::Insert => {
let right_data = take(&mut cur_right_data);
let right_data_len = right_data.len();
left_diff.push(ObjDataDiff {
data: vec![],
kind: ObjDataDiffKind::Insert,
len: right_data_len,
symbol: String::new(),
});
right_diff.push(ObjDataDiff {
data: right_data,
kind: ObjDataDiffKind::Insert,
len: right_data_len,
symbol: String::new(),
});
}
LevEditType::Delete => {
let left_data = take(&mut cur_left_data);
let left_data_len = left_data.len();
left_diff.push(ObjDataDiff {
data: left_data,
kind: ObjDataDiffKind::Delete,
len: left_data_len,
symbol: String::new(),
});
right_diff.push(ObjDataDiff {
data: vec![],
kind: ObjDataDiffKind::Delete,
len: left_data_len,
symbol: String::new(),
});
}
}
}
if left_cur < op.first_start {
left_diff.push(ObjDataDiff {
data: left.data[left_cur..op.first_start].to_vec(),
kind: ObjDataDiffKind::None,
len: op.first_start - left_cur,
symbol: String::new(),
});
left_cur = op.first_start;
}
if right_cur < op.second_start {
right_diff.push(ObjDataDiff {
data: right.data[right_cur..op.second_start].to_vec(),
kind: ObjDataDiffKind::None,
len: op.second_start - right_cur,
symbol: String::new(),
});
right_cur = op.second_start;
}
match op.op_type {
LevEditType::Replace => {
cur_left_data.push(left.data[left_cur]);
cur_right_data.push(right.data[right_cur]);
left_cur += 1;
right_cur += 1;
}
LevEditType::Insert => {
cur_right_data.push(right.data[right_cur]);
right_cur += 1;
}
LevEditType::Delete => {
cur_left_data.push(left.data[left_cur]);
left_cur += 1;
}
}
cur_op = op.op_type;
}
// if left_cur < left.data.len() {
// let len = left.data.len() - left_cur;
// left_diff.push(ObjDataDiff {
// data: left.data[left_cur..].to_vec(),
// kind: ObjDataDiffKind::Delete,
// len,
// });
// right_diff.push(ObjDataDiff { data: vec![], kind: ObjDataDiffKind::Delete, len });
// } else if right_cur < right.data.len() {
// let len = right.data.len() - right_cur;
// left_diff.push(ObjDataDiff { data: vec![], kind: ObjDataDiffKind::Insert, len });
// right_diff.push(ObjDataDiff {
// data: right.data[right_cur..].to_vec(),
// kind: ObjDataDiffKind::Insert,
// len,
// });
// }
// TODO: merge with above
match cur_op {
LevEditType::Replace => {
let left_data = take(&mut cur_left_data);
let right_data = take(&mut cur_right_data);
let left_data_len = left_data.len();
let right_data_len = right_data.len();
left_diff.push(ObjDataDiff {
data: left_data,
kind: ObjDataDiffKind::Replace,
len: left_data_len,
symbol: String::new(),
});
right_diff.push(ObjDataDiff {
data: right_data,
kind: ObjDataDiffKind::Replace,
len: right_data_len,
symbol: String::new(),
});
}
LevEditType::Insert => {
let right_data = take(&mut cur_right_data);
let right_data_len = right_data.len();
left_diff.push(ObjDataDiff {
data: vec![],
kind: ObjDataDiffKind::Insert,
len: right_data_len,
symbol: String::new(),
});
right_diff.push(ObjDataDiff {
data: right_data,
kind: ObjDataDiffKind::Insert,
len: right_data_len,
symbol: String::new(),
});
}
LevEditType::Delete => {
let left_data = take(&mut cur_left_data);
let left_data_len = left_data.len();
left_diff.push(ObjDataDiff {
data: left_data,
kind: ObjDataDiffKind::Delete,
len: left_data_len,
symbol: String::new(),
});
right_diff.push(ObjDataDiff {
data: vec![],
kind: ObjDataDiffKind::Delete,
len: left_data_len,
symbol: String::new(),
});
}
}
if left_cur < left.data.len() {
left_diff.push(ObjDataDiff {
data: left.data[left_cur..].to_vec(),
kind: ObjDataDiffKind::None,
len: left.data.len() - left_cur,
symbol: String::new(),
});
}
if right_cur < right.data.len() {
right_diff.push(ObjDataDiff {
data: right.data[right_cur..].to_vec(),
kind: ObjDataDiffKind::None,
len: right.data.len() - right_cur,
symbol: String::new(),
});
}
left.data_diff = left_diff;
right.data_diff = right_diff;
Ok(())
}
pub fn no_diff_data(section: &mut ObjSection) {
section.data_diff = vec![ObjDataDiff {
data: section.data.clone(),
kind: ObjDataDiffKind::None,
len: section.data.len(),
symbol: String::new(),
}];
}

View File

@ -1,162 +0,0 @@
/// Adapted from https://crates.io/crates/rapidfuzz
// Copyright 2020 maxbachmann
//
// Permission is hereby granted, free of charge, to any
// person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the
// Software without restriction, including without
// limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software
// is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice
// shall be included in all copies or substantial portions
// of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum LevEditType {
Replace,
Insert,
Delete,
}
#[derive(Debug, PartialEq, Eq)]
pub struct LevEditOp {
pub op_type: LevEditType, /* editing operation type */
pub first_start: usize, /* source block position */
pub second_start: usize, /* destination position */
}
pub fn editops_find<T>(query: &[T], choice: &[T]) -> Vec<LevEditOp>
where T: PartialEq {
let Affix { prefix_len, suffix_len } = Affix::find(query, choice);
let first_string = &query[prefix_len..query.len() - suffix_len];
let second_string = &choice[prefix_len..choice.len() - suffix_len];
let matrix_columns = first_string.len() + 1;
let matrix_rows = second_string.len() + 1;
// TODO maybe use an actual matrix for readability
let mut cache_matrix: Vec<usize> = vec![0; matrix_rows * matrix_columns];
for (i, elem) in cache_matrix.iter_mut().enumerate().take(matrix_rows) {
*elem = i;
}
for i in 1..matrix_columns {
cache_matrix[matrix_rows * i] = i;
}
for (i, char1) in first_string.iter().enumerate() {
let mut prev = i * matrix_rows;
let current = prev + matrix_rows;
let mut x = i + 1;
for (p, char2p) in second_string.iter().enumerate() {
let mut c3 = cache_matrix[prev] + (char1 != char2p) as usize;
prev += 1;
x += 1;
if x >= c3 {
x = c3;
}
c3 = cache_matrix[prev] + 1;
if x > c3 {
x = c3;
}
cache_matrix[current + 1 + p] = x;
}
}
editops_from_cost_matrix(matrix_columns, matrix_rows, prefix_len, cache_matrix)
}
fn editops_from_cost_matrix(
len1: usize,
len2: usize,
prefix_len: usize,
cache_matrix: Vec<usize>,
) -> Vec<LevEditOp> {
let mut ops = Vec::with_capacity(cache_matrix[len1 * len2 - 1]);
let mut dir = 0;
let mut i = len1 - 1;
let mut j = len2 - 1;
let mut p = len1 * len2 - 1;
//TODO this is still pretty ugly
while i > 0 || j > 0 {
let current_value = cache_matrix[p];
// More than one operation can be possible at a time. We use `dir` to
// decide when ambiguous.
let is_insert = j > 0 && current_value == cache_matrix[p - 1] + 1;
let is_delete = i > 0 && current_value == cache_matrix[p - len2] + 1;
let is_replace = i > 0 && j > 0 && current_value == cache_matrix[p - len2 - 1] + 1;
let (op_type, new_dir) = match (dir, is_insert, is_delete, is_replace) {
(_, false, false, false) => (None, 0),
(-1, true, _, _) => (Some(LevEditType::Insert), -1),
(1, _, true, _) => (Some(LevEditType::Delete), 1),
(_, _, _, true) => (Some(LevEditType::Replace), 0),
(0, true, _, _) => (Some(LevEditType::Insert), -1),
(0, _, true, _) => (Some(LevEditType::Delete), 1),
_ => panic!("something went terribly wrong"),
};
match new_dir {
-1 => {
j -= 1;
p -= 1;
}
1 => {
i -= 1;
p -= len2;
}
0 => {
i -= 1;
j -= 1;
p -= len2 + 1;
}
_ => panic!("something went terribly wrong"),
};
dir = new_dir;
if let Some(op_type) = op_type {
ops.insert(0, LevEditOp {
op_type,
first_start: i + prefix_len,
second_start: j + prefix_len,
});
}
}
ops
}
pub struct Affix {
pub prefix_len: usize,
pub suffix_len: usize,
}
impl Affix {
pub fn find<T>(s1: &[T], s2: &[T]) -> Affix
where T: PartialEq {
let prefix_len = s1.iter().zip(s2.iter()).take_while(|t| t.0 == t.1).count();
let suffix_len = s1[prefix_len..]
.iter()
.rev()
.zip(s2[prefix_len..].iter().rev())
.take_while(|t| t.0 == t.1)
.count();
Affix { prefix_len, suffix_len }
}
}

View File

@ -1,114 +0,0 @@
pub mod code;
pub mod data;
pub mod editops;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use crate::{
diff::{
code::{diff_code, find_section_and_symbol, no_diff_code},
data::{diff_bss_symbols, diff_data, no_diff_data},
},
obj::{ObjInfo, ObjIns, ObjSectionKind},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum DiffAlg {
#[default]
Patience,
Levenshtein,
Myers,
Lcs,
}
pub struct DiffObjConfig {
pub code_alg: DiffAlg,
pub data_alg: DiffAlg,
}
pub struct ProcessCodeResult {
pub ops: Vec<u8>,
pub insts: Vec<ObjIns>,
}
pub fn diff_objs(
config: &DiffObjConfig,
mut left: Option<&mut ObjInfo>,
mut right: Option<&mut ObjInfo>,
) -> Result<()> {
if let Some(left) = left.as_mut() {
for left_section in &mut left.sections {
if left_section.kind == ObjSectionKind::Code {
for left_symbol in &mut left_section.symbols {
if let Some((right, (right_section_idx, right_symbol_idx))) =
right.as_mut().and_then(|obj| {
find_section_and_symbol(obj, &left_symbol.name).map(|s| (obj, s))
})
{
let right_section = &mut right.sections[right_section_idx];
let right_symbol = &mut right_section.symbols[right_symbol_idx];
left_symbol.diff_symbol = Some(right_symbol.name.clone());
right_symbol.diff_symbol = Some(left_symbol.name.clone());
diff_code(
config.code_alg,
left.architecture,
&left_section.data,
&right_section.data,
left_symbol,
right_symbol,
&left_section.relocations,
&right_section.relocations,
&left.line_info,
&right.line_info,
)?;
} else {
no_diff_code(
left.architecture,
&left_section.data,
left_symbol,
&left_section.relocations,
&left.line_info,
)?;
}
}
} else if let Some(right_section) = right
.as_mut()
.and_then(|obj| obj.sections.iter_mut().find(|s| s.name == left_section.name))
{
if left_section.kind == ObjSectionKind::Data {
diff_data(config.data_alg, left_section, right_section)?;
} else if left_section.kind == ObjSectionKind::Bss {
diff_bss_symbols(&mut left_section.symbols, &mut right_section.symbols)?;
}
} else if left_section.kind == ObjSectionKind::Data {
no_diff_data(left_section);
}
}
}
if let Some(right) = right.as_mut() {
for right_section in right.sections.iter_mut() {
if right_section.kind == ObjSectionKind::Code {
for right_symbol in &mut right_section.symbols {
if right_symbol.instructions.is_empty() {
no_diff_code(
right.architecture,
&right_section.data,
right_symbol,
&right_section.relocations,
&right.line_info,
)?;
}
}
} else if right_section.kind == ObjSectionKind::Data
&& right_section.data_diff.is_empty()
{
no_diff_data(right_section);
}
}
}
if let (Some(left), Some(right)) = (left, right) {
diff_bss_symbols(&mut left.common, &mut right.common)?;
}
Ok(())
}

View File

@ -1,218 +0,0 @@
use std::{
path::{Path, PathBuf},
process::Command,
str::from_utf8,
sync::mpsc::Receiver,
};
use anyhow::{anyhow, Context, Error, Result};
use time::OffsetDateTime;
use crate::{
app::{AppConfig, ObjectConfig},
diff::{diff_objs, DiffAlg, DiffObjConfig},
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
obj::{elf, ObjInfo},
};
pub struct BuildStatus {
pub success: bool,
pub log: String,
}
pub struct ObjDiffConfig {
pub build_base: bool,
pub build_target: bool,
pub custom_make: Option<String>,
pub project_dir: Option<PathBuf>,
pub selected_obj: Option<ObjectConfig>,
pub selected_wsl_distro: Option<String>,
pub code_alg: DiffAlg,
pub data_alg: DiffAlg,
}
impl ObjDiffConfig {
pub(crate) fn from_config(config: &AppConfig) -> Self {
ObjDiffConfig {
build_base: config.build_base,
build_target: config.build_target,
custom_make: config.custom_make.clone(),
project_dir: config.project_dir.clone(),
selected_obj: config.selected_obj.clone(),
selected_wsl_distro: config.selected_wsl_distro.clone(),
code_alg: config.code_alg,
data_alg: config.data_alg,
}
}
}
pub struct ObjDiffResult {
pub first_status: BuildStatus,
pub second_status: BuildStatus,
pub first_obj: Option<ObjInfo>,
pub second_obj: Option<ObjInfo>,
pub time: OffsetDateTime,
}
fn run_make(cwd: &Path, arg: &Path, config: &ObjDiffConfig) -> BuildStatus {
match (|| -> Result<BuildStatus> {
let make = config.custom_make.as_deref().unwrap_or("make");
#[cfg(not(windows))]
let mut command = {
let mut command = Command::new(make);
command.current_dir(cwd).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 {
command
.arg("--cd")
.arg(cwd)
.arg("-d")
.arg(distro)
.arg("--")
.arg(make)
.arg(arg.to_slash_lossy().as_ref());
} else {
command.current_dir(cwd).arg(arg.to_slash_lossy().as_ref());
}
command.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
command
};
let output = command.output().context("Failed to execute build")?;
let stdout = from_utf8(&output.stdout).context("Failed to process stdout")?;
let stderr = from_utf8(&output.stderr).context("Failed to process stderr")?;
Ok(BuildStatus {
success: output.status.code().unwrap_or(-1) == 0,
log: format!("{stdout}\n{stderr}"),
})
})() {
Ok(status) => status,
Err(e) => BuildStatus { success: false, log: e.to_string() },
}
}
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.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 = 3;
if config.build_target && target_path_rel.is_some() {
total += 1;
}
if config.build_base && base_path_rel.is_some() {
total += 1;
}
let first_status = match target_path_rel {
Some(target_path_rel) if config.build_target => {
update_status(
context,
format!("Building target {}", target_path_rel.display()),
0,
total,
&cancel,
)?;
run_make(project_dir, target_path_rel, &config)
}
_ => BuildStatus { success: true, log: String::new() },
};
let second_status = match base_path_rel {
Some(base_path_rel) if config.build_base => {
update_status(
context,
format!("Building base {}", base_path_rel.display()),
0,
total,
&cancel,
)?;
run_make(project_dir, base_path_rel, &config)
}
_ => BuildStatus { success: true, log: String::new() },
};
let time = OffsetDateTime::now_utc();
let mut 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()),
2,
total,
&cancel,
)?;
Some(elf::read(target_path).with_context(|| {
format!("Failed to read object '{}'", target_path.display())
})?)
}
_ => None,
};
let mut 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()),
3,
total,
&cancel,
)?;
Some(
elf::read(base_path)
.with_context(|| format!("Failed to read object '{}'", base_path.display()))?,
)
}
_ => None,
};
update_status(context, "Performing diff".to_string(), 4, total, &cancel)?;
let diff_config = DiffObjConfig { code_alg: config.code_alg, data_alg: config.data_alg };
diff_objs(&diff_config, first_obj.as_mut(), second_obj.as_mut())?;
update_status(context, "Complete".to_string(), total, total, &cancel)?;
Ok(Box::new(ObjDiffResult { first_status, second_status, first_obj, second_obj, time }))
}
pub fn start_build(ctx: &egui::Context, config: ObjDiffConfig) -> JobState {
start_job(ctx, "Object diff", Job::ObjDiff, move |context, cancel| {
run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
})
}

View File

@ -1,12 +0,0 @@
#![warn(clippy::all, rust_2018_idioms)]
pub use app::App;
mod app;
mod app_config;
mod config;
mod diff;
mod jobs;
mod obj;
mod update;
mod views;

View File

@ -1,102 +0,0 @@
use std::collections::BTreeMap;
use anyhow::Result;
use rabbitizer::{config, Abi, InstrCategory, Instruction, OperandType};
use crate::{
diff::ProcessCodeResult,
obj::{ObjIns, ObjInsArg, ObjReloc},
};
fn configure_rabbitizer() {
unsafe {
config::RabbitizerConfig_Cfg.reg_names.fpr_abi_names = Abi::O32;
}
}
pub fn process_code(
data: &[u8],
start_address: u64,
end_address: u64,
relocs: &[ObjReloc],
line_info: &Option<BTreeMap<u32, u32>>,
) -> Result<ProcessCodeResult> {
configure_rabbitizer();
let ins_count = data.len() / 4;
let mut ops = Vec::<u8>::with_capacity(ins_count);
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
let mut cur_addr = start_address as u32;
for chunk in data.chunks_exact(4) {
let reloc = relocs.iter().find(|r| (r.address as u32 & !3) == cur_addr);
let code = u32::from_be_bytes(chunk.try_into()?);
let instruction = Instruction::new(code, cur_addr, InstrCategory::CPU);
let op = instruction.unique_id as u8;
ops.push(op);
let mnemonic = instruction.opcode_name().to_string();
let is_branch = instruction.is_branch();
let branch_offset = instruction.branch_offset();
let branch_dest =
if is_branch { Some((cur_addr as i32 + branch_offset) as u32) } else { None };
let operands = instruction.get_operands_slice();
let mut args = Vec::with_capacity(operands.len() + 1);
for op in operands {
match op {
OperandType::cpu_immediate
| OperandType::cpu_label
| OperandType::cpu_branch_target_label => {
if is_branch {
args.push(ObjInsArg::BranchOffset(branch_offset));
} else if let Some(reloc) = reloc {
if matches!(&reloc.target_section, Some(s) if s == ".text")
&& reloc.target.address > start_address
&& reloc.target.address < end_address
{
// Inter-function reloc, convert to branch offset
args.push(ObjInsArg::BranchOffset(
reloc.target.address as i32 - cur_addr as i32,
));
} else {
args.push(ObjInsArg::Reloc);
}
} else {
args.push(ObjInsArg::MipsArg(op.disassemble(&instruction, None)));
}
}
OperandType::cpu_immediate_base => {
if reloc.is_some() {
args.push(ObjInsArg::RelocWithBase);
} else {
args.push(ObjInsArg::MipsArgWithBase(
OperandType::cpu_immediate.disassemble(&instruction, None),
));
}
args.push(ObjInsArg::MipsArg(
OperandType::cpu_rs.disassemble(&instruction, None),
));
}
_ => {
args.push(ObjInsArg::MipsArg(op.disassemble(&instruction, None)));
}
}
}
let line =
line_info.as_ref().and_then(|map| map.range(..=cur_addr).last().map(|(_, &b)| b));
insts.push(ObjIns {
address: cur_addr,
code,
op,
mnemonic,
args,
reloc: reloc.cloned(),
branch_dest,
line,
orig: None,
});
cur_addr += 4;
}
Ok(ProcessCodeResult { ops, insts })
}

View File

@ -1,180 +0,0 @@
pub mod elf;
pub mod mips;
pub mod ppc;
use std::{collections::BTreeMap, path::PathBuf};
use filetime::FileTime;
use flagset::{flags, FlagSet};
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum ObjSectionKind {
Code,
Data,
Bss,
}
flags! {
pub enum ObjSymbolFlags: u8 {
Global,
Local,
Weak,
Common,
Hidden,
}
}
#[derive(Debug, Copy, Clone, Default)]
pub struct ObjSymbolFlagSet(pub(crate) FlagSet<ObjSymbolFlags>);
#[derive(Debug, Clone)]
pub struct ObjSection {
pub name: String,
pub kind: ObjSectionKind,
pub address: u64,
pub size: u64,
pub data: Vec<u8>,
pub index: usize,
pub symbols: Vec<ObjSymbol>,
pub relocations: Vec<ObjReloc>,
// Diff
pub data_diff: Vec<ObjDataDiff>,
pub match_percent: f32,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ObjInsArg {
PpcArg(ppc750cl::Argument),
MipsArg(String),
MipsArgWithBase(String),
Reloc,
RelocWithBase,
BranchOffset(i32),
}
#[derive(Debug, Copy, Clone)]
pub struct ObjInsArgDiff {
/// Incrementing index for coloring
pub idx: usize,
}
#[derive(Debug, Clone)]
pub struct ObjInsBranchFrom {
/// Source instruction indices
pub ins_idx: Vec<usize>,
/// Incrementing index for coloring
pub branch_idx: usize,
}
#[derive(Debug, Clone)]
pub struct ObjInsBranchTo {
/// Target instruction index
pub ins_idx: usize,
/// Incrementing index for coloring
pub branch_idx: usize,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
pub enum ObjInsDiffKind {
#[default]
None,
OpMismatch,
ArgMismatch,
Replace,
Delete,
Insert,
}
#[derive(Debug, Clone)]
pub struct ObjIns {
pub address: u32,
pub code: u32,
pub op: u8,
pub mnemonic: String,
pub args: Vec<ObjInsArg>,
pub reloc: Option<ObjReloc>,
pub branch_dest: Option<u32>,
/// Line info
pub line: Option<u32>,
/// Original (unsimplified) instruction
pub orig: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ObjInsDiff {
pub ins: Option<ObjIns>,
/// Diff kind
pub kind: ObjInsDiffKind,
/// Branches from instruction
pub branch_from: Option<ObjInsBranchFrom>,
/// Branches to instruction
pub branch_to: Option<ObjInsBranchTo>,
/// Arg diffs
pub arg_diff: Vec<Option<ObjInsArgDiff>>,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
pub enum ObjDataDiffKind {
#[default]
None,
Replace,
Delete,
Insert,
}
#[derive(Debug, Clone, Default)]
pub struct ObjDataDiff {
pub data: Vec<u8>,
pub kind: ObjDataDiffKind,
pub len: usize,
pub symbol: String,
}
#[derive(Debug, Clone)]
pub struct ObjSymbol {
pub name: String,
pub demangled_name: Option<String>,
pub address: u64,
pub section_address: u64,
pub size: u64,
pub size_known: bool,
pub flags: ObjSymbolFlagSet,
pub addend: i64,
// Diff
pub diff_symbol: Option<String>,
pub instructions: Vec<ObjInsDiff>,
pub match_percent: Option<f32>,
}
#[derive(Debug, Copy, Clone)]
pub enum ObjArchitecture {
PowerPc,
Mips,
}
#[derive(Debug, Clone)]
pub struct ObjInfo {
pub architecture: ObjArchitecture,
pub path: PathBuf,
pub timestamp: FileTime,
pub sections: Vec<ObjSection>,
pub common: Vec<ObjSymbol>,
pub line_info: Option<BTreeMap<u32, u32>>,
}
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum ObjRelocKind {
Absolute,
PpcAddr16Hi,
PpcAddr16Ha,
PpcAddr16Lo,
// PpcAddr32,
// PpcRel32,
// PpcAddr24,
PpcRel24,
// PpcAddr14,
PpcRel14,
PpcEmbSda21,
Mips26,
MipsHi16,
MipsLo16,
MipsGot16,
MipsCall16,
MipsGpRel16,
MipsGpRel32,
}
#[derive(Debug, Clone)]
pub struct ObjReloc {
pub kind: ObjRelocKind,
pub address: u64,
pub target: ObjSymbol,
pub target_section: Option<String>,
}

View File

@ -1,99 +0,0 @@
use std::collections::BTreeMap;
use anyhow::Result;
use ppc750cl::{disasm_iter, Argument, SimplifiedIns};
use crate::{
diff::ProcessCodeResult,
obj::{ObjIns, ObjInsArg, ObjReloc, ObjRelocKind},
};
// Relative relocation, can be Simm or BranchOffset
fn is_relative_arg(arg: &ObjInsArg) -> bool {
matches!(arg, ObjInsArg::PpcArg(Argument::Simm(_)) | ObjInsArg::BranchOffset(_))
}
// Relative or absolute relocation, can be Uimm, Simm or Offset
fn is_rel_abs_arg(arg: &ObjInsArg) -> bool {
matches!(arg, ObjInsArg::PpcArg(arg) if matches!(arg, Argument::Uimm(_) | Argument::Simm(_) | Argument::Offset(_)))
}
fn is_offset_arg(arg: &ObjInsArg) -> bool { matches!(arg, ObjInsArg::PpcArg(Argument::Offset(_))) }
pub fn process_code(
data: &[u8],
address: u64,
relocs: &[ObjReloc],
line_info: &Option<BTreeMap<u32, u32>>,
) -> Result<ProcessCodeResult> {
let ins_count = data.len() / 4;
let mut ops = Vec::<u8>::with_capacity(ins_count);
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
for mut ins in disasm_iter(data, address as u32) {
let reloc = relocs.iter().find(|r| (r.address as u32 & !3) == ins.addr);
if let Some(reloc) = reloc {
// Zero out relocations
ins.code = match reloc.kind {
ObjRelocKind::PpcEmbSda21 => ins.code & !0x1FFFFF,
ObjRelocKind::PpcRel24 => ins.code & !0x3FFFFFC,
ObjRelocKind::PpcRel14 => ins.code & !0xFFFC,
ObjRelocKind::PpcAddr16Hi
| ObjRelocKind::PpcAddr16Ha
| ObjRelocKind::PpcAddr16Lo => ins.code & !0xFFFF,
_ => ins.code,
};
}
let simplified = ins.clone().simplified();
let mut args: Vec<ObjInsArg> = simplified
.args
.iter()
.map(|a| match a {
Argument::BranchDest(dest) => ObjInsArg::BranchOffset(dest.0),
_ => ObjInsArg::PpcArg(a.clone()),
})
.collect();
if let Some(reloc) = reloc {
match reloc.kind {
ObjRelocKind::PpcEmbSda21 => {
args = vec![args[0].clone(), ObjInsArg::Reloc];
}
ObjRelocKind::PpcRel24 | ObjRelocKind::PpcRel14 => {
let arg = args
.iter_mut()
.rfind(|a| is_relative_arg(a))
.ok_or_else(|| anyhow::Error::msg("Failed to locate rel arg for reloc"))?;
*arg = ObjInsArg::Reloc;
}
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
};
}
_ => {}
}
}
ops.push(simplified.ins.op as u8);
let line = line_info
.as_ref()
.and_then(|map| map.range(..=simplified.ins.addr).last().map(|(_, &b)| b));
insts.push(ObjIns {
address: simplified.ins.addr,
code: simplified.ins.code,
mnemonic: format!("{}{}", simplified.mnemonic, simplified.suffix),
args,
reloc: reloc.cloned(),
op: ins.op as u8,
branch_dest: None,
line,
orig: Some(format!("{}", SimplifiedIns::basic_form(ins))),
});
}
Ok(ProcessCodeResult { ops, insts })
}

View File

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

View File

@ -1,686 +0,0 @@
use std::{
cmp::{max, Ordering},
default::Default,
};
use cwdemangle::demangle;
use eframe::emath::Align;
use egui::{text::LayoutJob, Color32, Label, Layout, RichText, Sense, TextFormat, Vec2};
use egui_extras::{Column, TableBuilder, TableRow};
use ppc750cl::Argument;
use time::format_description;
use crate::{
obj::{
ObjInfo, ObjIns, ObjInsArg, ObjInsArgDiff, ObjInsDiff, ObjInsDiffKind, ObjReloc,
ObjRelocKind, ObjSymbol,
},
views::{
appearance::Appearance,
symbol_diff::{match_color_for_symbol, DiffViewState, SymbolReference, View},
write_text,
},
};
#[derive(Default)]
pub enum HighlightKind {
#[default]
None,
Opcode(u8),
Arg(ObjInsArg),
Symbol(String),
Address(u32),
}
#[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<ObjInsArgDiff>],
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
});
if ui
.add(Label::new(op_label).sense(Sense::click()))
.context_menu(|ui| ins_context_menu(ui, ins))
.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 == 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::PpcArg(arg) => match arg {
Argument::Offset(val) => {
job.append(&format!("{val}"), 0.0, text_format);
write_text("(", base_color, &mut job, appearance.code_font.clone());
new_writing_offset = true;
}
Argument::Uimm(_) | Argument::Simm(_) => {
job.append(&format!("{arg}"), 0.0, text_format);
}
_ => {
job.append(&format!("{arg}"), 0.0, text_format);
}
},
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::MipsArg(str) => {
job.append(str.strip_prefix('$').unwrap_or(str), 0.0, text_format);
}
ObjInsArg::MipsArgWithBase(str) => {
job.append(str.strip_prefix('$').unwrap_or(str), 0.0, text_format);
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());
}
writing_offset = new_writing_offset;
if ui
.add(Label::new(job).sense(Sense::click()))
.context_menu(|ui| ins_context_menu(ui, ins))
.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);
ui.style_mut().wrap = Some(false);
ui.label(format!("{:02X?}", ins.code.to_be_bytes()));
if let Some(orig) = &ins.orig {
ui.label(format!("Original: {}", orig));
}
for arg in &ins.args {
if let ObjInsArg::PpcArg(arg) = arg {
match arg {
Argument::Uimm(v) => {
ui.label(format!("{} == {}", v, v.0));
}
Argument::Simm(v) => {
ui.label(format!("{} == {}", v, v.0));
}
Argument::Offset(v) => {
ui.label(format!("{} == {}", v, v.0));
}
_ => {}
}
}
}
if let Some(reloc) = &ins.reloc {
ui.label(format!("Relocation type: {:?}", reloc.kind));
ui.colored_label(appearance.highlight_color, format!("Name: {}", reloc.target.name));
if let Some(section) = &reloc.target_section {
ui.colored_label(appearance.highlight_color, format!("Section: {section}"));
ui.colored_label(
appearance.highlight_color,
format!("Address: {:x}", reloc.target.address),
);
ui.colored_label(
appearance.highlight_color,
format!("Size: {:x}", reloc.target.size),
);
} else {
ui.colored_label(appearance.highlight_color, "Extern".to_string());
}
}
});
}
fn ins_context_menu(ui: &mut egui::Ui, ins: &ObjIns) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
// if ui.button("Copy hex").clicked() {}
for arg in &ins.args {
if let ObjInsArg::PpcArg(arg) = arg {
match arg {
Argument::Uimm(v) => {
if ui.button(format!("Copy \"{v}\"")).clicked() {
ui.output_mut(|output| output.copied_text = format!("{v}"));
ui.close_menu();
}
if ui.button(format!("Copy \"{}\"", v.0)).clicked() {
ui.output_mut(|output| output.copied_text = format!("{}", v.0));
ui.close_menu();
}
}
Argument::Simm(v) => {
if ui.button(format!("Copy \"{v}\"")).clicked() {
ui.output_mut(|output| output.copied_text = format!("{v}"));
ui.close_menu();
}
if ui.button(format!("Copy \"{}\"", v.0)).clicked() {
ui.output_mut(|output| output.copied_text = format!("{}", v.0));
ui.close_menu();
}
}
Argument::Offset(v) => {
if ui.button(format!("Copy \"{v}\"")).clicked() {
ui.output_mut(|output| output.copied_text = format!("{v}"));
ui.close_menu();
}
if ui.button(format!("Copy \"{}\"", v.0)).clicked() {
ui.output_mut(|output| output.copied_text = format!("{}", v.0));
ui.close_menu();
}
}
_ => {}
}
}
}
if let Some(reloc) = &ins.reloc {
if let Some(name) = &reloc.target.demangled_name {
if ui.button(format!("Copy \"{name}\"")).clicked() {
ui.output_mut(|output| output.copied_text = name.clone());
ui.close_menu();
}
}
if ui.button(format!("Copy \"{}\"", reloc.target.name)).clicked() {
ui.output_mut(|output| output.copied_text = reloc.target.name.clone());
ui.close_menu();
}
}
});
}
fn find_symbol<'a>(obj: &'a ObjInfo, selected_symbol: &SymbolReference) -> Option<&'a ObjSymbol> {
obj.sections.iter().find_map(|section| {
section.symbols.iter().find(|symbol| symbol.name == selected_symbol.symbol_name)
})
}
fn asm_row_ui(
ui: &mut egui::Ui,
ins_diff: &ObjInsDiff,
symbol: &ObjSymbol,
appearance: &Appearance,
ins_view_state: &mut FunctionViewState,
) {
ui.spacing_mut().item_spacing.x = 0.0;
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()
});
if ui
.add(Label::new(job).sense(Sense::click()))
.context_menu(|ui| ins_context_menu(ui, ins))
.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()
});
}
ui.add(Label::new(job));
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(),
);
ui.add(Label::new(job));
}
}
fn asm_col_ui(
row: &mut TableRow<'_, '_>,
ins_diff: &ObjInsDiff,
symbol: &ObjSymbol,
appearance: &Appearance,
ins_view_state: &mut FunctionViewState,
) {
let (_, response) = row.col(|ui| {
asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state);
});
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);
});
}
}
fn empty_col_ui(row: &mut TableRow<'_, '_>) {
row.col(|ui| {
ui.label("");
});
}
fn asm_table_ui(
table: TableBuilder<'_>,
left_obj: Option<&ObjInfo>,
right_obj: Option<&ObjInfo>,
selected_symbol: &SymbolReference,
appearance: &Appearance,
ins_view_state: &mut FunctionViewState,
) -> Option<()> {
let left_symbol = left_obj.and_then(|obj| find_symbol(obj, selected_symbol));
let right_symbol = right_obj.and_then(|obj| find_symbol(obj, selected_symbol));
let instructions_len = left_symbol.or(right_symbol).map(|s| s.instructions.len())?;
table.body(|body| {
body.rows(appearance.code_font.size, instructions_len, |row_index, mut row| {
if let Some(symbol) = left_symbol {
asm_col_ui(
&mut row,
&symbol.instructions[row_index],
symbol,
appearance,
ins_view_state,
);
} else {
empty_col_ui(&mut row);
}
if let Some(symbol) = right_symbol {
asm_col_ui(
&mut row,
&symbol.instructions[row_index],
symbol,
appearance,
ins_view_state,
);
} else {
empty_col_ui(&mut row);
}
});
});
Some(())
}
pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
else {
return;
};
// Header
let available_width = ui.available_width();
let column_width = available_width / 2.0;
ui.allocate_ui_with_layout(
Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min),
|ui| {
// 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);
if ui.button("Back").clicked() {
state.current_view = View::SymbolDiff;
}
let demangled = demangle(&selected_symbol.symbol_name, &Default::default());
let name = demangled.as_deref().unwrap_or(&selected_symbol.symbol_name);
let mut job = LayoutJob::simple(
name.to_string(),
appearance.code_font.clone(),
appearance.highlight_color,
column_width,
);
job.wrap.break_anywhere = true;
job.wrap.max_rows = 1;
ui.label(job);
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
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.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
if state.build_running {
ui.colored_label(appearance.replace_color, "Building…");
} else {
ui.label("Last built:");
let format =
format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label(
result
.time
.to_offset(appearance.utc_offset)
.format(&format)
.unwrap(),
);
}
});
});
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
if let Some(match_percent) = result
.second_obj
.as_ref()
.and_then(|obj| find_symbol(obj, selected_symbol))
.and_then(|symbol| symbol.match_percent)
{
ui.colored_label(
match_color_for_symbol(match_percent, appearance),
&format!("{match_percent:.0}%"),
);
} else {
ui.colored_label(appearance.replace_color, "Missing");
}
ui.label("Diff base:");
});
},
);
},
);
ui.separator();
// Table
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), 2)
.resizable(false)
.auto_shrink([false, false])
.min_scrolled_height(available_height);
asm_table_ui(
table,
result.first_obj.as_ref(),
result.second_obj.as_ref(),
selected_symbol,
appearance,
&mut state.function_state,
);
}