Initial commit

This commit is contained in:
Luke Street 2022-09-08 17:19:20 -04:00
commit cb3c6062c7
22 changed files with 5061 additions and 0 deletions

61
.github/workflows/build.yaml vendored Normal file
View File

@ -0,0 +1,61 @@
name: Build
on: [ push, pull_request ]
jobs:
check:
name: Check
runs-on: ubuntu-latest
strategy:
matrix:
toolchain: [ stable, 1.61.0, nightly ]
fail-fast: false
env:
RUSTFLAGS: -D warnings
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.toolchain }}
override: true
components: rustfmt, clippy
- uses: EmbarkStudios/cargo-deny-action@v1
- uses: actions-rs/cargo@v1
with:
command: check
args: --all-features
- uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-features
build:
name: Build
strategy:
matrix:
platform: [ ubuntu-latest, macos-latest, windows-latest ]
toolchain: [ stable, 1.61.0, nightly ]
fail-fast: false
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.toolchain }}
override: true
- uses: actions-rs/cargo@v1
with:
command: test
args: --release --all-features
- uses: actions-rs/cargo@v1
with:
command: build
args: --release --all-features
- uses: actions/upload-artifact@v2
with:
name: ${{ matrix.platform }}-${{ matrix.toolchain }}
path: |
target/release/objdiff
target/release/objdiff.exe

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Rust
target/
**/*.rs.bk
generated/
# cargo-mobile
.cargo/
/gen
# macOS
.DS_Store
# JetBrains
.idea
# Generated SPIR-V
*.spv
# project
textures/
android.keystore
*.frag
*.vert
*.metal

2445
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

36
Cargo.toml Normal file
View File

@ -0,0 +1,36 @@
[package]
name = "objdiff"
version = "0.1.0"
edition = "2021"
rust-version = "1.61"
authors = ["Luke Street <luke@street.dev>"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff"
readme = "README.md"
description = """
A tool for decompilation projects.
"""
[dependencies]
egui = "0.19.0"
eframe = { version = "0.19.0", features = ["persistence"] } # , "wgpu"
serde = { version = "1", features = ["derive"] }
anyhow = "1.0.63"
thiserror = "1.0.33"
flagset = "0.4.3"
object = "0.29.0"
notify = "5.0.0"
cwdemangle = "0.1.1"
log = "0.4.17"
rfd = { version = "0.10.0" } # , default-features = false, features = ['xdg-portal']
egui_extras = "0.19.0"
ppc750cl = { git = "https://github.com/terorie/ppc750cl" }
# 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.6"
tracing-wasm = "0.2"

28
README.md Normal file
View File

@ -0,0 +1,28 @@
# objdiff [![Build Status]][actions]
[Build Status]: https://github.com/encounter/objdiff/actions/workflows/build.yaml/badge.svg
[actions]: https://github.com/encounter/objdiff/actions
A tool for decompilation projects.
Currently supports:
- PowerPC 750CL (GameCube & Wii)
**WARNING:** Very early & unstable.
![Screenshot](assets/screen-diff.png)
### License
Licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any
additional terms or conditions.

BIN
assets/screen-diff.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

210
deny.toml Normal file
View File

@ -0,0 +1,210 @@
# This template contains all of the possible sections and their default values
# Note that all fields that take a lint level have these possible values:
# * deny - An error will be produced and the check will fail
# * warn - A warning will be produced, but the check will not fail
# * allow - No warning or error will be produced, though in some cases a note
# will be
# The values provided in this template are the default values that will be used
# when any section or field is not specified in your own configuration
# If 1 or more target triples (and optionally, target_features) are specified,
# only the specified targets will be checked when running `cargo deny check`.
# This means, if a particular package is only ever used as a target specific
# dependency, such as, for example, the `nix` crate only being used via the
# `target_family = "unix"` configuration, that only having windows targets in
# this list would mean the nix crate, as well as any of its exclusive
# dependencies not shared by any other crates, would be ignored, as the target
# list here is effectively saying which targets you are building for.
targets = [
# The triple can be any string, but only the target triples built in to
# rustc (as of 1.40) can be checked against actual config expressions
#{ triple = "x86_64-unknown-linux-musl" },
# You can also specify which target_features you promise are enabled for a
# particular target. target_features are currently not validated against
# the actual valid features supported by the target architecture.
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
]
# This section is considered when running `cargo deny check advisories`
# More documentation for the advisories section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
[advisories]
# The path where the advisory database is cloned/fetched into
db-path = "~/.cargo/advisory-db"
# The url(s) of the advisory databases to use
db-urls = ["https://github.com/rustsec/advisory-db"]
# The lint level for security vulnerabilities
vulnerability = "deny"
# The lint level for unmaintained crates
unmaintained = "warn"
# The lint level for crates that have been yanked from their source registry
yanked = "warn"
# The lint level for crates with security notices. Note that as of
# 2019-12-17 there are no security notice advisories in
# https://github.com/rustsec/advisory-db
notice = "warn"
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
ignore = [
#"RUSTSEC-0000-0000",
]
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
# lower than the range specified will be ignored. Note that ignored advisories
# will still output a note when they are encountered.
# * None - CVSS Score 0.0
# * Low - CVSS Score 0.1 - 3.9
# * Medium - CVSS Score 4.0 - 6.9
# * High - CVSS Score 7.0 - 8.9
# * Critical - CVSS Score 9.0 - 10.0
#severity-threshold =
# This section is considered when running `cargo deny check licenses`
# More documentation for the licenses section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
[licenses]
# The lint level for crates which do not have a detectable license
unlicensed = "deny"
# List of explictly allowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
allow = [
"MIT",
"Apache-2.0",
"ISC",
"BSD-2-Clause",
"BSD-3-Clause",
"BSL-1.0",
"CC0-1.0",
"MPL-2.0",
"Unicode-DFS-2016",
"Zlib",
]
# List of explictly disallowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
deny = [
#"Nokia",
]
# Lint level for licenses considered copyleft
copyleft = "warn"
# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
# * both - The license will be approved if it is both OSI-approved *AND* FSF
# * either - The license will be approved if it is either OSI-approved *OR* FSF
# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
# * neither - This predicate is ignored and the default lint level is used
allow-osi-fsf-free = "neither"
# Lint level used when no other predicates are matched
# 1. License isn't in the allow or deny lists
# 2. License isn't copyleft
# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
default = "deny"
# The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the
# canonical license text of a valid SPDX license file.
# [possible values: any between 0.0 and 1.0].
confidence-threshold = 0.8
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
# aren't accepted for every possible crate as with the normal allow list
exceptions = [
# Each entry is the crate and version constraint, and its specific allow
# list
#{ allow = ["Zlib"], name = "adler32", version = "*" },
]
# Some crates don't have (easily) machine readable licensing information,
# adding a clarification entry for it allows you to manually specify the
# licensing information
#[[licenses.clarify]]
# The name of the crate the clarification applies to
#name = "ring"
# The optional version constraint for the crate
#version = "*"
# The SPDX expression for the license requirements of the crate
#expression = "MIT AND ISC AND OpenSSL"
# One or more files in the crate's source used as the "source of truth" for
# the license expression. If the contents match, the clarification will be used
# when running the license check, otherwise the clarification will be ignored
# and the crate will be checked normally, which may produce warnings or errors
# depending on the rest of your configuration
#license-files = [
# Each entry is a crate relative path, and the (opaque) hash of its contents
#{ path = "LICENSE", hash = 0xbd0eed23 }
#]
[licenses.private]
# If true, ignores workspace crates that aren't published, or are only
# published to private registries
ignore = false
# One or more private registries that you might publish crates to, if a crate
# is only published to private registries, and ignore is true, the crate will
# not have its license(s) checked
registries = [
#"https://sekretz.com/registry
]
# This section is considered when running `cargo deny check bans`.
# More documentation about the 'bans' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
[bans]
# Lint level for when multiple versions of the same crate are detected
multiple-versions = "warn"
# Lint level for when a crate version requirement is `*`
wildcards = "allow"
# The graph highlighting used when creating dotgraphs for crates
# with multiple versions
# * lowest-version - The path to the lowest versioned duplicate is highlighted
# * simplest-path - The path to the version with the fewest edges is highlighted
# * all - Both lowest-version and simplest-path are used
highlight = "all"
# List of crates that are allowed. Use with care!
allow = [
#{ name = "ansi_term", version = "=0.11.0" },
]
# List of crates to deny
deny = [
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#{ name = "ansi_term", version = "=0.11.0" },
#
# Wrapper crates can optionally be specified to allow the crate when it
# is a direct dependency of the otherwise banned crate
#{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
]
# Certain crates/versions that will be skipped when doing duplicate detection.
skip = [
#{ name = "ansi_term", version = "=0.11.0" },
]
# Similarly to `skip` allows you to skip certain crates during duplicate
# detection. Unlike skip, it also includes the entire tree of transitive
# dependencies starting at the specified crate, up to a certain depth, which is
# by default infinite
skip-tree = [
#{ name = "ansi_term", version = "=0.11.0", depth = 20 },
]
# This section is considered when running `cargo deny check sources`.
# More documentation about the 'sources' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
[sources]
# Lint level for what to happen when a crate from a crate registry that is not
# in the allow list is encountered
unknown-registry = "warn"
# Lint level for what to happen when a crate from a git repository that is not
# in the allow list is encountered
unknown-git = "warn"
# List of URLs for allowed crate registries. Defaults to the crates.io index
# if not specified. If it is specified but empty, no registries are allowed.
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
# List of URLs for allowed Git repositories
allow-git = []
[sources.allow-org]
# 1 or more github.com organizations to allow git sources for
github = ["encounter", "terorie"]
# 1 or more gitlab.com organizations to allow git sources for
#gitlab = [""]
# 1 or more bitbucket.org organizations to allow git sources for
#bitbucket = [""]

8
rustfmt.toml Normal file
View File

@ -0,0 +1,8 @@
fn_single_line = true
group_imports = "StdExternalCrate"
imports_granularity = "Crate"
overflow_delimited_expr = true
reorder_impl_items = true
use_field_init_shorthand = true
use_small_heuristics = "Max"
where_single_line = true

266
src/app.rs Normal file
View File

@ -0,0 +1,266 @@
use std::{
default::Default,
ffi::OsStr,
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, Ordering},
Arc, RwLock,
},
time::Duration,
};
use eframe::Frame;
use notify::{RecursiveMode, Watcher};
use crate::{
jobs::{
build::{queue_build, BuildResult},
Job, JobResult, JobState,
},
views::{
config::config_ui, function_diff::function_diff_ui, jobs::jobs_ui,
symbol_diff::symbol_diff_ui,
},
};
#[derive(Default, Eq, PartialEq)]
pub enum View {
#[default]
SymbolDiff,
FunctionDiff,
}
#[derive(Default, serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct ViewState {
#[serde(skip)]
pub jobs: Vec<JobState>,
#[serde(skip)]
pub build: Option<Box<BuildResult>>,
#[serde(skip)]
pub highlighted_symbol: Option<String>,
#[serde(skip)]
pub selected_symbol: Option<String>,
#[serde(skip)]
pub current_view: View,
// Config
pub reverse_fn_order: bool,
}
#[derive(Default, Clone, serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct AppConfig {
pub project_dir: Option<PathBuf>,
pub build_asm_dir: Option<PathBuf>,
pub build_src_dir: Option<PathBuf>,
pub build_obj: Option<String>,
#[serde(skip)]
pub project_dir_change: bool,
}
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct App {
view_state: ViewState,
#[serde(skip)]
config: Arc<RwLock<AppConfig>>,
#[serde(skip)]
modified: Arc<AtomicBool>,
#[serde(skip)]
watcher: Option<notify::RecommendedWatcher>,
}
impl Default for App {
fn default() -> Self {
Self {
view_state: ViewState::default(),
config: Arc::new(Default::default()),
modified: Arc::new(Default::default()),
watcher: None,
}
}
}
const CONFIG_KEY: &str = "app_config";
impl App {
/// Called once before the first frame.
pub fn new(cc: &eframe::CreationContext<'_>) -> 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.
if let Some(storage) = cc.storage {
let mut app: App = eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
let mut config: AppConfig = eframe::get_value(storage, CONFIG_KEY).unwrap_or_default();
if config.project_dir.is_some() {
config.project_dir_change = true;
}
app.config = Arc::new(RwLock::new(config));
app
} else {
Self::default()
}
}
}
impl eframe::App for App {
/// Called each time the UI needs repainting, which may be many times per second.
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
fn update(&mut self, ctx: &egui::Context, frame: &mut Frame) {
let Self { config, view_state, .. } = self;
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
ui.menu_button("File", |ui| {
if ui.button("Quit").clicked() {
frame.close();
}
});
});
});
if view_state.current_view == View::FunctionDiff
&& matches!(&view_state.build, Some(b) if b.first_status.success && b.second_status.success)
{
egui::SidePanel::left("side_panel").show(ctx, |ui| {
if ui.button("Back").clicked() {
view_state.current_view = View::SymbolDiff;
}
ui.separator();
jobs_ui(ui, view_state);
});
egui::CentralPanel::default().show(ctx, |ui| {
function_diff_ui(ui, view_state);
});
} else {
egui::SidePanel::left("side_panel").show(ctx, |ui| {
ui.heading("Config");
config_ui(ui, config, view_state);
jobs_ui(ui, view_state);
});
egui::CentralPanel::default().show(ctx, |ui| {
symbol_diff_ui(ui, view_state);
});
}
if view_state.jobs.iter().any(|job| {
if let Some(handle) = &job.handle {
return !handle.is_finished();
}
false
}) {
ctx.request_repaint();
} else {
ctx.request_repaint();
ctx.request_repaint_after(Duration::from_millis(100));
}
}
/// Called by the frame work to save state before shutdown.
fn save(&mut self, storage: &mut dyn eframe::Storage) {
if let Ok(config) = self.config.read() {
eframe::set_value(storage, CONFIG_KEY, &*config);
}
eframe::set_value(storage, eframe::APP_KEY, self);
}
fn post_rendering(&mut self, _window_size_px: [u32; 2], _frame: &Frame) {
for job in &mut self.view_state.jobs {
if let Some(handle) = &job.handle {
if !handle.is_finished() {
continue;
}
match job.handle.take().unwrap().join() {
Ok(result) => {
log::info!("Job {} finished", job.id);
match result {
JobResult::None => {
if let Some(err) = &job.status.read().unwrap().error {
log::error!("{:?}", err);
}
}
JobResult::Build(state) => {
self.view_state.build = Some(state);
}
}
}
Err(e) => {
log::error!("Failed to join job handle: {:?}", e);
}
}
}
}
if self.view_state.jobs.iter().any(|v| v.should_remove) {
let mut i = 0;
while i < self.view_state.jobs.len() {
let job = &self.view_state.jobs[i];
if job.should_remove && job.handle.is_none() {
self.view_state.jobs.remove(i);
} else {
i += 1;
}
}
}
if let Ok(mut config) = self.config.write() {
if config.project_dir_change {
drop(self.watcher.take());
if let Some(project_dir) = &config.project_dir {
match create_watcher(self.modified.clone(), project_dir) {
Ok(watcher) => self.watcher = Some(watcher),
Err(e) => eprintln!("Failed to create watcher: {}", e),
}
config.project_dir_change = false;
self.modified.store(true, Ordering::Relaxed);
}
}
if let Some(build_obj) = &config.build_obj {
if self.modified.load(Ordering::Relaxed) {
if !self
.view_state
.jobs
.iter()
.any(|j| j.job_type == Job::Build && j.handle.is_some())
{
self.view_state
.jobs
.push(queue_build(build_obj.clone(), self.config.clone()));
}
self.modified.store(false, Ordering::Relaxed);
}
}
}
}
}
fn create_watcher(
modified: Arc<AtomicBool>,
project_dir: &Path,
) -> notify::Result<notify::RecommendedWatcher> {
let mut watcher =
notify::recommended_watcher(move |res: notify::Result<notify::Event>| match res {
Ok(event) => {
if matches!(event.kind, notify::EventKind::Modify(..)) {
let watch_extensions = &[
Some(OsStr::new("c")),
Some(OsStr::new("cp")),
Some(OsStr::new("cpp")),
Some(OsStr::new("h")),
Some(OsStr::new("hpp")),
];
if event.paths.iter().any(|p| watch_extensions.contains(&p.extension())) {
modified.store(true, Ordering::Relaxed);
}
}
}
Err(e) => println!("watch error: {:?}", e),
})?;
watcher.watch(project_dir, RecursiveMode::Recursive)?;
Ok(watcher)
}

396
src/diff.rs Normal file
View File

@ -0,0 +1,396 @@
use std::collections::BTreeMap;
use anyhow::Result;
use ppc750cl::{disasm_iter, Argument};
use crate::{
editops::{editops_find, LevEditType},
obj::{
ObjInfo, ObjIns, ObjInsArg, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjInsDiff,
ObjInsDiffKind, ObjReloc, ObjRelocKind, ObjSection, ObjSectionKind, ObjSymbol,
ObjSymbolFlags,
},
};
// Relative relocation, can be Simm or BranchDest
fn is_relative_arg(arg: &ObjInsArg) -> bool {
matches!(arg, ObjInsArg::Arg(arg) if matches!(arg, Argument::Simm(_) | Argument::BranchDest(_)))
}
// Relative or absolute relocation, can be Uimm, Simm or Offset
fn is_rel_abs_arg(arg: &ObjInsArg) -> bool {
matches!(arg, ObjInsArg::Arg(arg) if matches!(arg, Argument::Uimm(_) | Argument::Simm(_) | Argument::Offset(_)))
}
fn is_offset_arg(arg: &ObjInsArg) -> bool { matches!(arg, ObjInsArg::Arg(Argument::Offset(_))) }
fn process_code(data: &[u8], address: u64, relocs: &[ObjReloc]) -> Result<(Vec<u8>, Vec<ObjIns>)> {
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.simplified();
let mut args: Vec<ObjInsArg> =
simplified.args.iter().map(|a| ObjInsArg::Arg(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::RelocOffset } else { ObjInsArg::Reloc };
}
_ => {}
}
}
ops.push(simplified.ins.op as u8);
let suffix = simplified.ins.suffix();
insts.push(ObjIns {
ins: simplified.ins,
mnemonic: format!("{}{}", simplified.mnemonic, suffix),
args,
reloc: reloc.cloned(),
});
}
Ok((ops, insts))
}
pub fn diff_code(
left_data: &[u8],
right_data: &[u8],
left_symbol: &mut ObjSymbol,
right_symbol: &mut ObjSymbol,
left_relocs: &[ObjReloc],
right_relocs: &[ObjReloc],
) -> Result<()> {
let left_code =
&left_data[left_symbol.address as usize..(left_symbol.address + left_symbol.size) as usize];
let (left_ops, left_insts) = process_code(left_code, left_symbol.address, left_relocs)?;
let right_code = &right_data
[right_symbol.address as usize..(right_symbol.address + right_symbol.size) as usize];
let (right_ops, right_insts) = process_code(right_code, right_symbol.address, right_relocs)?;
let mut left_diff = Vec::<ObjInsDiff>::new();
let mut right_diff = Vec::<ObjInsDiff>::new();
let edit_ops = editops_find(&left_ops, &right_ops);
{
let mut op_iter = edit_ops.iter();
let mut left_iter = left_insts.iter();
let mut right_iter = right_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.ins.addr - 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.ins.addr - left_symbol.address as u32) != left_addr {
return Err(anyhow::Error::msg("Instruction address mismatch (left)"));
}
if (right.ins.addr - 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();
}
LevEditType::Keep => unreachable!(),
}
} 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();
}
}
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_insts.len();
let percent = ((total - diff_state.diff_count) as f32 / total as f32) * 100.0;
left_symbol.match_percent = percent;
right_symbol.match_percent = percent;
left_symbol.instructions = left_diff;
right_symbol.instructions = right_diff;
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.ins.addr, 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.ins.branch_dest().and_then(|dest| addr_map.get(&dest)) {
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 reloc_eq(left_reloc: Option<&ObjReloc>, right_reloc: Option<&ObjReloc>) -> bool {
if let (Some(left), Some(right)) = (left_reloc, right_reloc) {
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 || left.target.address == right.target.address)
}
(Some(_), None) => false,
(None, Some(_)) => {
// Match if possibly stripped weak symbol
name_matches && right.target.flags.0.contains(ObjSymbolFlags::Weak)
}
(None, None) => name_matches,
}
} else {
false
}
}
fn arg_eq(
left: &ObjInsArg,
right: &ObjInsArg,
left_diff: &ObjInsDiff,
right_diff: &ObjInsDiff,
) -> bool {
return match left {
ObjInsArg::Arg(l) => match right {
ObjInsArg::Arg(r) => match r {
Argument::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)
}
_ => 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::RelocOffset => {
matches!(right, ObjInsArg::RelocOffset)
&& 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()),
)
}
};
}
#[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.ins.op != right_ins.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::Arg(arg) => format!("{}", arg),
ObjInsArg::Reloc | ObjInsArg::RelocOffset => String::new(),
};
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::Arg(arg) => format!("{}", arg),
ObjInsArg::Reloc | ObjInsArg::RelocOffset => String::new(),
};
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)
}
fn find_section<'a>(obj: &'a mut ObjInfo, name: &str) -> Option<&'a mut ObjSection> {
obj.sections.iter_mut().find(|s| s.name == name)
}
fn find_symbol<'a>(symbols: &'a mut [ObjSymbol], name: &str) -> Option<&'a mut ObjSymbol> {
symbols.iter_mut().find(|s| s.name == name)
}
pub fn diff_objs(left: &mut ObjInfo, right: &mut ObjInfo) -> Result<()> {
for left_section in &mut left.sections {
if let Some(right_section) = find_section(right, &left_section.name) {
for left_symbol in &mut left_section.symbols {
if let Some(right_symbol) =
find_symbol(&mut right_section.symbols, &left_symbol.name)
{
left_symbol.diff_symbol = Some(right_symbol.name.clone());
right_symbol.diff_symbol = Some(left_symbol.name.clone());
if left_section.kind == ObjSectionKind::Code {
diff_code(
&left_section.data,
&right_section.data,
left_symbol,
right_symbol,
&left_section.relocations,
&right_section.relocations,
)?;
}
}
}
}
}
Ok(())
}

253
src/editops.rs Normal file
View File

@ -0,0 +1,253 @@
/// 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 {
Keep,
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 */
}
#[derive(Debug, PartialEq, Eq)]
pub struct LevMatchingBlock {
pub first_start: usize,
pub second_start: usize,
pub len: usize,
}
pub fn editops_find(query: &[u8], choice: &[u8]) -> Vec<LevEditOp> {
let string_affix = Affix::find(query, choice);
let first_string_len = string_affix.first_string_len;
let second_string_len = string_affix.second_string_len;
let prefix_len = string_affix.prefix_len;
let first_string = &query[prefix_len..prefix_len + first_string_len];
let second_string = &choice[prefix_len..prefix_len + second_string_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(
first_string,
second_string,
matrix_columns,
matrix_rows,
prefix_len,
cache_matrix,
)
}
fn editops_from_cost_matrix(
string1: &[u8],
string2: &[u8],
len1: usize,
len2: usize,
prefix_len: usize,
cache_matrix: Vec<usize>,
) -> Vec<LevEditOp> {
let mut dir = 0;
let mut ops: Vec<LevEditOp> = vec![];
ops.reserve(cache_matrix[len1 * len2 - 1]);
let mut i = len1 - 1;
let mut j = len2 - 1;
let mut p = len1 * len2 - 1;
// let string1_chars: Vec<char> = string1.chars().collect();
// let string2_chars: Vec<char> = string2.chars().collect();
//TODO this is still pretty ugly
while i > 0 || j > 0 {
let current_value = cache_matrix[p];
let op_type;
if dir == -1 && j > 0 && current_value == cache_matrix[p - 1] + 1 {
op_type = LevEditType::Insert;
} else if dir == 1 && i > 0 && current_value == cache_matrix[p - len2] + 1 {
op_type = LevEditType::Delete;
} else if i > 0
&& j > 0
&& current_value == cache_matrix[p - len2 - 1]
&& string1[i - 1] == string2[j - 1]
{
op_type = LevEditType::Keep;
} else if i > 0 && j > 0 && current_value == cache_matrix[p - len2 - 1] + 1 {
op_type = LevEditType::Replace;
}
/* we can't turn directly from -1 to 1, in this case it would be better
* to go diagonally, but check it (dir == 0) */
else if dir == 0 && j > 0 && current_value == cache_matrix[p - 1] + 1 {
op_type = LevEditType::Insert;
} else if dir == 0 && i > 0 && current_value == cache_matrix[p - len2] + 1 {
op_type = LevEditType::Delete;
} else {
panic!("something went terribly wrong");
}
match op_type {
LevEditType::Insert => {
j -= 1;
p -= 1;
dir = -1;
}
LevEditType::Delete => {
i -= 1;
p -= len2;
dir = 1;
}
LevEditType::Replace => {
i -= 1;
j -= 1;
p -= len2 + 1;
dir = 0;
}
LevEditType::Keep => {
i -= 1;
j -= 1;
p -= len2 + 1;
dir = 0;
/* LevEditKeep does not has to be stored */
continue;
}
};
let edit_op =
LevEditOp { op_type, first_start: i + prefix_len, second_start: j + prefix_len };
ops.insert(0, edit_op);
}
ops
}
pub struct Affix {
pub prefix_len: usize,
pub first_string_len: usize,
pub second_string_len: usize,
}
impl Affix {
pub fn find(first_string: &[u8], second_string: &[u8]) -> Affix {
// remove common prefix and suffix (linear vs square runtime for levensthein)
let mut first_iter = first_string.iter();
let mut second_iter = second_string.iter();
let mut limit_start = 0;
let mut first_iter_char = first_iter.next();
let mut second_iter_char = second_iter.next();
while first_iter_char.is_some() && first_iter_char == second_iter_char {
first_iter_char = first_iter.next();
second_iter_char = second_iter.next();
limit_start += 1;
}
// save char since the iterator was already consumed
let first_iter_cache = first_iter_char;
let second_iter_cache = second_iter_char;
if second_iter_char.is_some() && first_iter_char.is_some() {
first_iter_char = first_iter.next_back();
second_iter_char = second_iter.next_back();
while first_iter_char.is_some() && first_iter_char == second_iter_char {
first_iter_char = first_iter.next_back();
second_iter_char = second_iter.next_back();
}
}
match (first_iter_char, second_iter_char) {
(None, None) => {
// characters might not match even though they were consumed
let remaining_char = (first_iter_cache != second_iter_cache) as usize;
Affix {
prefix_len: limit_start,
first_string_len: remaining_char,
second_string_len: remaining_char,
}
}
(None, _) => {
let remaining_char =
(first_iter_cache.is_some() && first_iter_cache != second_iter_char) as usize;
Affix {
prefix_len: limit_start,
first_string_len: remaining_char,
second_string_len: second_iter.count() + 1 + remaining_char,
}
}
(_, None) => {
let remaining_char =
(second_iter_cache.is_some() && second_iter_cache != first_iter_char) as usize;
Affix {
prefix_len: limit_start,
first_string_len: first_iter.count() + 1 + remaining_char,
second_string_len: remaining_char,
}
}
_ => Affix {
prefix_len: limit_start,
first_string_len: first_iter.count() + 2,
second_string_len: second_iter.count() + 2,
},
}
}
}

242
src/elf.rs Normal file
View File

@ -0,0 +1,242 @@
use std::{fs, path::Path};
use anyhow::{Context, Result};
use cwdemangle::demangle;
use flagset::Flags;
use object::{
Object, ObjectSection, ObjectSymbol, RelocationKind, RelocationTarget, SectionKind, SymbolKind,
SymbolSection,
};
use crate::obj::{
ObjInfo, ObjReloc, ObjRelocKind, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet,
ObjSymbolFlags,
};
fn to_obj_section_kind(kind: SectionKind) -> ObjSectionKind {
match kind {
SectionKind::Text => ObjSectionKind::Code,
SectionKind::Data | SectionKind::ReadOnlyData => ObjSectionKind::Data,
SectionKind::UninitializedData => ObjSectionKind::Bss,
_ => panic!("Unhandled section kind {:?}", kind),
}
}
fn to_obj_symbol(symbol: &object::Symbol<'_, '_>) -> Result<ObjSymbol> {
let mut name = symbol.name().context("Failed to process symbol name")?;
if name.is_empty() {
println!("Found empty sym: {:?}", symbol);
name = "?";
}
let mut flags = ObjSymbolFlagSet(ObjSymbolFlags::none());
if symbol.is_global() {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Global);
}
if symbol.is_local() {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Local);
}
if symbol.is_common() {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Common);
}
if symbol.is_weak() {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Weak);
}
Ok(ObjSymbol {
name: name.to_string(),
demangled_name: demangle(name),
address: symbol.address(),
size: symbol.size(),
size_known: symbol.size() != 0,
flags,
diff_symbol: None,
instructions: vec![],
match_percent: 0.0,
})
}
const R_PPC_ADDR16_LO: u32 = 4;
const R_PPC_ADDR16_HI: u32 = 5;
const R_PPC_ADDR16_HA: u32 = 6;
const R_PPC_REL24: u32 = 10;
const R_PPC_REL14: u32 = 11;
const R_PPC_EMB_SDA21: u32 = 109;
fn filter_sections(obj_file: &object::File<'_>) -> Result<Vec<ObjSection>> {
let mut result = Vec::<ObjSection>::new();
for section in obj_file.sections() {
if section.size() == 0 {
continue;
}
if section.kind() != SectionKind::Text
&& section.kind() != SectionKind::Data
&& section.kind() != SectionKind::ReadOnlyData
&& section.kind() != SectionKind::UninitializedData
{
continue;
}
let name = section.name().context("Failed to process section name")?;
let data = section.data().context("Failed to read section data")?;
result.push(ObjSection {
name: name.to_string(),
kind: to_obj_section_kind(section.kind()),
address: section.address(),
size: section.size(),
data: data.to_vec(),
index: section.index().0,
symbols: Vec::new(),
relocations: Vec::new(),
});
}
result.sort_by(|a, b| a.name.cmp(&b.name));
Ok(result)
}
fn symbols_by_section(obj_file: &object::File<'_>, section: &ObjSection) -> 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 symbol.is_local() && section.kind == ObjSectionKind::Code {
// TODO strip local syms in diff?
let name = symbol.name().context("Failed to process symbol name")?;
if name.starts_with("lbl_") {
continue;
}
}
result.push(to_obj_symbol(&symbol)?);
}
}
}
result.sort_by_key(|v| v.address);
let mut iter = result.iter_mut().peekable();
while let Some(symbol) = iter.next() {
if symbol.size == 0 {
if let Some(next_symbol) = iter.peek() {
symbol.size = next_symbol.address - symbol.address;
} else {
symbol.size = (section.address + section.size) - symbol.address;
}
}
}
Ok(result)
}
fn common_symbols(obj_file: &object::File<'_>) -> Result<Vec<ObjSymbol>> {
let mut result = Vec::<ObjSymbol>::new();
for symbol in obj_file.symbols() {
if symbol.is_common() {
result.push(to_obj_symbol(&symbol)?);
}
}
Ok(result)
}
fn locate_section_symbol(
obj_file: &object::File<'_>,
target: &object::Symbol<'_, '_>,
address: u64,
) -> Result<ObjSymbol> {
let section_index =
target.section_index().ok_or_else(|| anyhow::Error::msg("Unknown section index"))?;
for symbol in obj_file.symbols() {
if !matches!(symbol.section_index(), Some(idx) if idx == section_index) {
continue;
}
if symbol.kind() == SymbolKind::Section || symbol.address() != address {
continue;
}
return to_obj_symbol(&symbol);
}
Err(anyhow::Error::msg("Failed to locate reloc offset sym"))
}
fn relocations_by_section(
obj_file: &object::File<'_>,
section: &ObjSection,
) -> Result<Vec<ObjReloc>> {
let obj_section = obj_file
.section_by_name(&section.name)
.ok_or_else(|| anyhow::Error::msg("Failed to locate section"))?;
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 kind {
R_PPC_ADDR16_LO => ObjRelocKind::PpcAddr16Lo,
R_PPC_ADDR16_HI => ObjRelocKind::PpcAddr16Hi,
R_PPC_ADDR16_HA => ObjRelocKind::PpcAddr16Ha,
R_PPC_REL24 => ObjRelocKind::PpcRel24,
R_PPC_REL14 => ObjRelocKind::PpcRel14,
R_PPC_EMB_SDA21 => ObjRelocKind::PpcEmbSda21,
_ => {
return Err(anyhow::Error::msg(format!(
"Unhandled ELF relocation type: {}",
kind
)))
}
},
_ => {
return Err(anyhow::Error::msg(format!(
"Unhandled relocation type: {:?}",
reloc.kind()
)))
}
};
let target_section = match symbol.section() {
SymbolSection::Common => Some(".comm".to_string()),
SymbolSection::Section(idx) => obj_file
.section_by_index(idx)
.map(|s| s.name().map(|s| s.to_string()).ok())
.ok()
.flatten(),
_ => None,
};
// println!("Reloc: {:?}", reloc.addend());
let target = match symbol.kind() {
SymbolKind::Text | SymbolKind::Data | SymbolKind::Unknown => to_obj_symbol(&symbol),
SymbolKind::Section => {
let addend = reloc.addend();
if addend < 0 {
Err(anyhow::Error::msg(format!("Negative addend in section reloc: {}", addend)))
} else {
locate_section_symbol(obj_file, &symbol, addend as u64)
}
}
_ => Err(anyhow::Error::msg(format!(
"Unhandled relocation symbol type {:?}",
symbol.kind()
))),
}?;
relocations.push(ObjReloc { kind, address, target, target_section });
}
Ok(relocations)
}
pub fn read(obj_path: &Path) -> Result<ObjInfo> {
let bin_data = fs::read(obj_path)?;
let obj_file = object::File::parse(&*bin_data)?;
let mut result = ObjInfo {
path: obj_path.to_owned(),
sections: filter_sections(&obj_file)?,
common: common_symbols(&obj_file)?,
};
for section in &mut result.sections {
section.symbols = symbols_by_section(&obj_file, section)?;
section.relocations = relocations_by_section(&obj_file, section)?;
}
Ok(result)
}

107
src/jobs/build.rs Normal file
View File

@ -0,0 +1,107 @@
use std::{
path::Path,
process::Command,
str::from_utf8,
sync::{mpsc::Receiver, Arc, RwLock},
};
use anyhow::{Context, Error, Result};
use crate::{
app::AppConfig,
diff::diff_objs,
elf,
jobs::{queue_job, update_status, Job, JobResult, JobState, Status},
obj::ObjInfo,
};
pub struct BuildStatus {
pub success: bool,
pub log: String,
}
pub struct BuildResult {
pub first_status: BuildStatus,
pub second_status: BuildStatus,
pub first_obj: Option<ObjInfo>,
pub second_obj: Option<ObjInfo>,
}
fn run_make(cwd: &Path, arg: &Path) -> BuildStatus {
match (|| -> Result<BuildStatus> {
let output = Command::new("make")
.current_dir(cwd)
.arg(arg)
.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!("{}\n{}", stdout, stderr),
})
})() {
Ok(status) => status,
Err(e) => BuildStatus { success: false, log: e.to_string() },
}
}
fn run_build(
status: &Status,
cancel: Receiver<()>,
obj_path: String,
config: Arc<RwLock<AppConfig>>,
) -> Result<Box<BuildResult>> {
let config = config.read().map_err(|_| Error::msg("Failed to lock app config"))?.clone();
let project_dir =
config.project_dir.as_ref().ok_or_else(|| Error::msg("Missing project dir"))?;
let mut asm_path = config
.build_asm_dir
.as_ref()
.ok_or_else(|| Error::msg("Missing build asm dir"))?
.to_owned();
asm_path.push(&obj_path);
let mut src_path = config
.build_src_dir
.as_ref()
.ok_or_else(|| Error::msg("Missing build src dir"))?
.to_owned();
src_path.push(&obj_path);
let asm_path_rel =
asm_path.strip_prefix(project_dir).context("Failed to create relative asm obj path")?;
let src_path_rel =
src_path.strip_prefix(project_dir).context("Failed to create relative src obj path")?;
update_status(status, format!("Building asm {}", obj_path), 0, 5, &cancel)?;
let first_status = run_make(project_dir, asm_path_rel);
update_status(status, format!("Building src {}", obj_path), 1, 5, &cancel)?;
let second_status = run_make(project_dir, src_path_rel);
let mut first_obj = if first_status.success {
update_status(status, format!("Loading asm {}", obj_path), 2, 5, &cancel)?;
Some(elf::read(&asm_path)?)
} else {
None
};
let mut second_obj = if second_status.success {
update_status(status, format!("Loading src {}", obj_path), 3, 5, &cancel)?;
Some(elf::read(&src_path)?)
} else {
None
};
if let (Some(first_obj), Some(second_obj)) = (&mut first_obj, &mut second_obj) {
update_status(status, "Performing diff".to_string(), 4, 5, &cancel)?;
diff_objs(first_obj, second_obj)?;
}
update_status(status, "Complete".to_string(), 5, 5, &cancel)?;
Ok(Box::new(BuildResult { first_status, second_status, first_obj, second_obj }))
}
pub fn queue_build(obj_path: String, config: Arc<RwLock<AppConfig>>) -> JobState {
queue_job(Job::Build, move |status, cancel| {
run_build(status, cancel, obj_path, config).map(JobResult::Build)
})
}

104
src/jobs/mod.rs Normal file
View File

@ -0,0 +1,104 @@
use std::{
sync::{
atomic::{AtomicUsize, Ordering},
mpsc::{Receiver, Sender, TryRecvError},
Arc, RwLock,
},
thread::JoinHandle,
};
use anyhow::Result;
use crate::jobs::build::BuildResult;
pub mod build;
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum Job {
Build,
}
pub static JOB_ID: AtomicUsize = AtomicUsize::new(0);
pub struct JobState {
pub id: usize,
pub job_type: Job,
pub handle: Option<JoinHandle<JobResult>>,
pub status: Arc<RwLock<JobStatus>>,
pub cancel: Sender<()>,
pub should_remove: bool,
}
#[derive(Default)]
pub struct JobStatus {
pub title: String,
pub progress_percent: f32,
pub progress_items: Option<[u32; 2]>,
pub status: String,
pub error: Option<anyhow::Error>,
}
pub enum JobResult {
None,
Build(Box<BuildResult>),
}
fn should_cancel(rx: &Receiver<()>) -> bool {
match rx.try_recv() {
Ok(_) | Err(TryRecvError::Disconnected) => true,
Err(_) => false,
}
}
type Status = Arc<RwLock<JobStatus>>;
fn queue_job(
job_type: Job,
run: impl FnOnce(&Status, Receiver<()>) -> Result<JobResult> + Send + 'static,
) -> JobState {
let status = Arc::new(RwLock::new(JobStatus {
title: String::new(),
progress_percent: 0.0,
progress_items: None,
status: "".to_string(),
error: None,
}));
let status_clone = status.clone();
let (tx, rx) = std::sync::mpsc::channel();
let handle = std::thread::spawn(move || {
return match run(&status, rx) {
Ok(state) => state,
Err(e) => {
if let Ok(mut w) = status.write() {
w.error = Some(e);
}
JobResult::None
}
};
});
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
log::info!("Started job {}", id);
JobState {
id,
job_type,
handle: Some(handle),
status: status_clone,
cancel: tx,
should_remove: true,
}
}
fn update_status(
status: &Status,
str: String,
count: u32,
total: u32,
cancel: &Receiver<()>,
) -> Result<()> {
let mut w = status.write().map_err(|_| anyhow::Error::msg("Failed to lock job status"))?;
w.progress_items = Some([count, total]);
w.progress_percent = count as f32 / total as f32;
if should_cancel(cancel) {
w.status = "Cancelled".to_string();
return Err(anyhow::Error::msg("Cancelled"));
} else {
w.status = str;
}
Ok(())
}

11
src/lib.rs Normal file
View File

@ -0,0 +1,11 @@
#![warn(clippy::all, rust_2018_idioms)]
pub use app::App;
mod app;
mod diff;
mod editops;
mod elf;
mod jobs;
mod obj;
mod views;

31
src/main.rs Normal file
View File

@ -0,0 +1,31 @@
#![warn(clippy::all, rust_2018_idioms)]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
// When compiling natively:
#[cfg(not(target_arch = "wasm32"))]
fn main() {
// Log to stdout (if you run with `RUST_LOG=debug`).
tracing_subscriber::fmt::init();
let native_options = eframe::NativeOptions::default();
// native_options.renderer = eframe::Renderer::Wgpu;
eframe::run_native("objdiff", native_options, Box::new(|cc| Box::new(objdiff::App::new(cc))));
}
// when compiling to web using trunk.
#[cfg(target_arch = "wasm32")]
fn main() {
// Make sure panics are logged using `console.error`.
console_error_panic_hook::set_once();
// Redirect tracing to console.log and friends:
tracing_wasm::set_as_global_default();
let web_options = eframe::WebOptions::default();
eframe::start_web(
"the_canvas_id", // hardcode it
web_options,
Box::new(|cc| Box::new(eframe_template::TemplateApp::new(cc))),
)
.expect("failed to start eframe");
}

132
src/obj.rs Normal file
View File

@ -0,0 +1,132 @@
use std::path::PathBuf;
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,
}
}
#[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>,
}
#[derive(Debug, Clone)]
pub enum ObjInsArg {
Arg(ppc750cl::Argument),
Reloc,
RelocOffset,
}
#[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 ins: ppc750cl::Ins,
pub mnemonic: String,
pub args: Vec<ObjInsArg>,
pub reloc: Option<ObjReloc>,
}
#[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, Clone)]
pub struct ObjSymbol {
pub name: String,
pub demangled_name: Option<String>,
pub address: u64,
pub size: u64,
pub size_known: bool,
pub flags: ObjSymbolFlagSet,
// Diff
pub diff_symbol: Option<String>,
pub instructions: Vec<ObjInsDiff>,
pub match_percent: f32,
}
#[derive(Debug, Clone)]
pub struct ObjInfo {
pub path: PathBuf,
pub sections: Vec<ObjSection>,
pub common: Vec<ObjSymbol>,
}
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum ObjRelocKind {
Absolute,
PpcAddr16Hi,
PpcAddr16Ha,
PpcAddr16Lo,
// PpcAddr32,
// PpcRel32,
// PpcAddr24,
PpcRel24,
// PpcAddr14,
PpcRel14,
PpcEmbSda21,
}
#[derive(Debug, Clone)]
pub struct ObjReloc {
pub kind: ObjRelocKind,
pub address: u64,
pub target: ObjSymbol,
pub target_section: Option<String>,
}
// #[derive(Debug, Clone)]
// pub struct ObjInsDiff {
// pub kind: ObjInsDiffKind,
// pub left: Option<ObjIns>,
// pub right: Option<ObjIns>,
// }

87
src/views/config.rs Normal file
View File

@ -0,0 +1,87 @@
use std::sync::{Arc, RwLock};
use crate::{
app::{AppConfig, ViewState},
jobs::build::queue_build,
};
pub fn config_ui(ui: &mut egui::Ui, config: &Arc<RwLock<AppConfig>>, view_state: &mut ViewState) {
let mut config_guard = config.write().unwrap();
let AppConfig { project_dir, project_dir_change, build_asm_dir, build_src_dir, build_obj } =
&mut *config_guard;
if ui.button("Select project dir").clicked() {
if let Some(path) = rfd::FileDialog::new().pick_folder() {
*project_dir = Some(path);
*project_dir_change = true;
*build_asm_dir = None;
*build_src_dir = None;
*build_obj = None;
}
}
if let Some(dir) = project_dir {
ui.label(dir.to_string_lossy());
}
ui.separator();
if let Some(project_dir) = project_dir {
if ui.button("Select asm build dir").clicked() {
if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() {
*build_asm_dir = Some(path);
*build_obj = None;
}
}
if let Some(dir) = build_asm_dir {
ui.label(dir.to_string_lossy());
}
ui.separator();
if ui.button("Select src build dir").clicked() {
if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() {
*build_src_dir = Some(path);
*build_obj = None;
}
}
if let Some(dir) = build_src_dir {
ui.label(dir.to_string_lossy());
}
ui.separator();
}
if let Some(build_src_dir) = build_src_dir {
if ui.button("Select obj").clicked() {
if let Some(path) = rfd::FileDialog::new()
.set_directory(&build_src_dir)
.add_filter("Object file", &["o"])
.pick_file()
{
let mut new_build_obj: Option<String> = None;
if let Ok(obj_path) = path.strip_prefix(&build_src_dir) {
new_build_obj = Some(obj_path.display().to_string());
} else if let Some(build_asm_dir) = build_asm_dir {
if let Ok(obj_path) = path.strip_prefix(&build_asm_dir) {
new_build_obj = Some(obj_path.display().to_string());
}
}
if let Some(new_build_obj) = new_build_obj {
*build_obj = Some(new_build_obj.clone());
view_state.jobs.push(queue_build(new_build_obj, config.clone()));
}
}
}
if let Some(build_obj) = build_obj {
ui.label(&*build_obj);
if ui.button("Build").clicked() {
view_state.jobs.push(queue_build(build_obj.clone(), config.clone()));
}
}
ui.separator();
}
ui.checkbox(&mut view_state.reverse_fn_order, "Reverse function order (deferred)");
ui.separator();
}

338
src/views/function_diff.rs Normal file
View File

@ -0,0 +1,338 @@
use std::default::Default;
use cwdemangle::demangle;
use egui::{text::LayoutJob, Color32, FontFamily, FontId, Label, Sense, TextFormat};
use egui_extras::{Size, StripBuilder, TableBuilder};
use ppc750cl::Argument;
use crate::{
app::ViewState,
obj::{
ObjInfo, ObjIns, ObjInsArg, ObjInsArgDiff, ObjInsDiff, ObjInsDiffKind, ObjReloc,
ObjRelocKind, ObjSymbol,
},
views::symbol_diff::match_color_for_symbol,
};
const FONT_SIZE: f32 = 14.0;
const FONT_ID: FontId = FontId::new(FONT_SIZE, FontFamily::Monospace);
const COLOR_RED: Color32 = Color32::from_rgb(200, 40, 41);
fn write_text(str: &str, color: Color32, job: &mut LayoutJob) {
job.append(str, 0.0, TextFormat { font_id: FONT_ID, color, ..Default::default() });
}
fn write_reloc(reloc: &ObjReloc, job: &mut LayoutJob) {
let name = reloc.target.demangled_name.as_ref().unwrap_or(&reloc.target.name);
write_text(name, Color32::LIGHT_GRAY, job);
match reloc.kind {
ObjRelocKind::PpcAddr16Lo => write_text("@l", Color32::GRAY, job),
ObjRelocKind::PpcAddr16Hi => write_text("@h", Color32::GRAY, job),
ObjRelocKind::PpcAddr16Ha => write_text("@ha", Color32::GRAY, job),
ObjRelocKind::PpcEmbSda21 => write_text("@sda21", Color32::GRAY, job),
_ => {}
};
}
fn write_ins(
ins: &ObjIns,
diff_kind: &ObjInsDiffKind,
args: &[Option<ObjInsArgDiff>],
base_addr: u32,
job: &mut LayoutJob,
) {
let base_color = match diff_kind {
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
Color32::GRAY
}
ObjInsDiffKind::Replace => Color32::LIGHT_BLUE,
ObjInsDiffKind::Delete => COLOR_RED,
ObjInsDiffKind::Insert => Color32::GREEN,
};
write_text(
&ins.mnemonic,
match diff_kind {
ObjInsDiffKind::OpMismatch => Color32::LIGHT_BLUE,
_ => base_color,
},
job,
);
let mut writing_offset = false;
for (i, arg) in ins.args.iter().enumerate() {
if i == 0 {
write_text(" ", base_color, job);
}
if i > 0 && !writing_offset {
write_text(", ", base_color, job);
}
let color = if let Some(diff) = args.get(i).and_then(|a| a.as_ref()) {
COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
} else {
base_color
};
match arg {
ObjInsArg::Arg(arg) => match arg {
Argument::Offset(val) => {
write_text(&format!("{}", val), color, job);
write_text("(", base_color, job);
writing_offset = true;
continue;
}
Argument::BranchDest(dest) => {
let addr = dest.0 + ins.ins.addr as i32 - base_addr as i32;
write_text(&format!("{:x}", addr), color, job);
}
Argument::Uimm(_) | Argument::Simm(_) => {
write_text(&format!("{}", arg), color, job);
}
_ => {
write_text(&format!("{}", arg), color, job);
}
},
ObjInsArg::Reloc => {
write_reloc(ins.reloc.as_ref().unwrap(), job);
}
ObjInsArg::RelocOffset => {
write_reloc(ins.reloc.as_ref().unwrap(), job);
write_text("(", base_color, job);
writing_offset = true;
continue;
}
}
if writing_offset {
write_text(")", base_color, job);
writing_offset = false;
}
}
}
fn ins_hover_ui(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);
ui.label(format!("{:02X?}", ins.ins.code.to_be_bytes()));
for arg in &ins.args {
if let ObjInsArg::Arg(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(Color32::WHITE, format!("Name: {}", reloc.target.name));
if let Some(section) = &reloc.target_section {
ui.colored_label(Color32::WHITE, format!("Section: {}", section));
ui.colored_label(Color32::WHITE, format!("Address: {:x}", reloc.target.address));
ui.colored_label(Color32::WHITE, format!("Size: {:x}", reloc.target.size));
} else {
ui.colored_label(Color32::WHITE, "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 {
Argument::Uimm(v) => {
if ui.button(format!("Copy \"{}\"", v)).clicked() {
ui.output().copied_text = format!("{}", v);
ui.close_menu();
}
if ui.button(format!("Copy \"{}\"", v.0)).clicked() {
ui.output().copied_text = format!("{}", v.0);
ui.close_menu();
}
}
Argument::Simm(v) => {
if ui.button(format!("Copy \"{}\"", v)).clicked() {
ui.output().copied_text = format!("{}", v);
ui.close_menu();
}
if ui.button(format!("Copy \"{}\"", v.0)).clicked() {
ui.output().copied_text = format!("{}", v.0);
ui.close_menu();
}
}
Argument::Offset(v) => {
if ui.button(format!("Copy \"{}\"", v)).clicked() {
ui.output().copied_text = format!("{}", v);
ui.close_menu();
}
if ui.button(format!("Copy \"{}\"", v.0)).clicked() {
ui.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().copied_text = name.clone();
ui.close_menu();
}
}
if ui.button(format!("Copy \"{}\"", reloc.target.name)).clicked() {
ui.output().copied_text = reloc.target.name.clone();
ui.close_menu();
}
}
});
}
const 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(128, 128, 128),
];
fn find_symbol<'a>(obj: &'a ObjInfo, section_name: &str, name: &str) -> Option<&'a ObjSymbol> {
let section = obj.sections.iter().find(|s| s.name == section_name)?;
section.symbols.iter().find(|s| s.name == name)
}
fn asm_row_ui(ui: &mut egui::Ui, ins_diff: &ObjInsDiff, symbol: &ObjSymbol) {
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();
if let Some(ins) = &ins_diff.ins {
let base_color = match ins_diff.kind {
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
Color32::GRAY
}
ObjInsDiffKind::Replace => Color32::LIGHT_BLUE,
ObjInsDiffKind::Delete => COLOR_RED,
ObjInsDiffKind::Insert => Color32::GREEN,
};
write_text(
&format!("{:<6}", format!("{:x}:", ins.ins.addr - symbol.address as u32)),
base_color,
&mut job,
);
if let Some(branch) = &ins_diff.branch_from {
write_text("~> ", COLOR_ROTATION[branch.branch_idx % COLOR_ROTATION.len()], &mut job);
} else {
write_text(" ", base_color, &mut job);
}
write_ins(ins, &ins_diff.kind, &ins_diff.arg_diff, symbol.address as u32, &mut job);
if let Some(branch) = &ins_diff.branch_to {
write_text(" ~>", COLOR_ROTATION[branch.branch_idx % COLOR_ROTATION.len()], &mut job);
}
ui.add(Label::new(job).sense(Sense::click()))
.on_hover_ui_at_pointer(|ui| ins_hover_ui(ui, ins))
.context_menu(|ui| ins_context_menu(ui, ins));
} else {
ui.label("");
}
}
fn asm_table_ui(
table: TableBuilder<'_>,
left_obj: &ObjInfo,
right_obj: &ObjInfo,
fn_name: &str,
) -> Option<()> {
let left_symbol = find_symbol(left_obj, ".text", fn_name)?;
let right_symbol = find_symbol(right_obj, ".text", fn_name)?;
table.body(|body| {
body.rows(FONT_SIZE, left_symbol.instructions.len(), |row_index, mut row| {
row.col(|ui| {
asm_row_ui(ui, &left_symbol.instructions[row_index], left_symbol);
});
row.col(|ui| {
asm_row_ui(ui, &right_symbol.instructions[row_index], right_symbol);
});
});
});
Some(())
}
pub fn function_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) {
if let (Some(result), Some(selected_symbol)) = (&view_state.build, &view_state.selected_symbol)
{
StripBuilder::new(ui).size(Size::exact(40.0)).size(Size::remainder()).vertical(
|mut strip| {
strip.strip(|builder| {
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
let demangled = demangle(selected_symbol);
strip.cell(|ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style =
Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.colored_label(
Color32::WHITE,
demangled.as_ref().unwrap_or(selected_symbol),
);
ui.label("Diff asm:");
ui.separator();
});
});
strip.cell(|ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style =
Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
if let Some(obj) = &result.second_obj {
if let Some(symbol) = find_symbol(obj, ".text", selected_symbol)
{
ui.colored_label(
match_color_for_symbol(symbol),
&format!("{:.0}%", symbol.match_percent),
);
}
}
ui.label("Diff src:");
ui.separator();
});
});
});
});
strip.cell(|ui| {
if let (Some(left_obj), Some(right_obj)) =
(&result.first_obj, &result.second_obj)
{
let table = TableBuilder::new(ui)
.striped(false)
.cell_layout(egui::Layout::left_to_right(egui::Align::Min))
.column(Size::relative(0.5))
.column(Size::relative(0.5))
.resizable(false);
asm_table_ui(table, left_obj, right_obj, selected_symbol);
}
});
},
);
}
}

38
src/views/jobs.rs Normal file
View File

@ -0,0 +1,38 @@
use egui::{Color32, ProgressBar, Widget};
use crate::app::ViewState;
pub fn jobs_ui(ui: &mut egui::Ui, view_state: &ViewState) {
ui.label("Jobs");
for job in &view_state.jobs {
if let Ok(status) = job.status.read() {
ui.group(|ui| {
ui.label(&status.title);
let mut bar = ProgressBar::new(status.progress_percent);
if let Some(items) = &status.progress_items {
bar = bar.text(format!("{} / {}", items[0], items[1]));
}
bar.ui(ui);
const STATUS_LENGTH: usize = 80;
if let Some(err) = &status.error {
let err_string = err.to_string();
ui.colored_label(
Color32::from_rgb(255, 0, 0),
if err_string.len() > STATUS_LENGTH - 10 {
format!("Error: {}...", &err_string[0..STATUS_LENGTH - 10])
} else {
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
},
);
} else {
ui.label(if status.status.len() > STATUS_LENGTH - 3 {
format!("{}...", &status.status[0..STATUS_LENGTH - 3])
} else {
format!("{:width$}", &status.status, width = STATUS_LENGTH)
});
}
});
}
}
}

4
src/views/mod.rs Normal file
View File

@ -0,0 +1,4 @@
pub(crate) mod config;
pub(crate) mod function_diff;
pub(crate) mod jobs;
pub(crate) mod symbol_diff;

240
src/views/symbol_diff.rs Normal file
View File

@ -0,0 +1,240 @@
use egui::{
text::LayoutJob, CollapsingHeader, Color32, FontFamily, FontId, Rgba, ScrollArea,
SelectableLabel, TextFormat, Ui, Widget,
};
use egui_extras::{Size, StripBuilder};
use crate::{
app::{View, ViewState},
jobs::build::BuildStatus,
obj::{ObjInfo, ObjSymbol, ObjSymbolFlags},
};
pub fn match_color_for_symbol(symbol: &ObjSymbol) -> Color32 {
if symbol.match_percent == 100.0 {
Color32::GREEN
} else if symbol.match_percent >= 50.0 {
Color32::LIGHT_BLUE
} else {
Color32::RED
}
}
fn symbol_ui(
ui: &mut Ui,
symbol: &ObjSymbol,
highlighted_symbol: &mut Option<String>,
selected_symbol: &mut Option<String>,
current_view: &mut View,
) {
let mut job = LayoutJob::default();
let name: &str =
if let Some(demangled) = &symbol.demangled_name { demangled } else { &symbol.name };
let mut selected = false;
if let Some(sym) = highlighted_symbol {
selected = sym == &symbol.name;
}
let font_id = FontId::new(14.0, FontFamily::Monospace);
job.append("[", 0.0, TextFormat {
font_id: font_id.clone(),
color: Color32::GRAY,
..Default::default()
});
if symbol.flags.0.contains(ObjSymbolFlags::Common) {
job.append("c", 0.0, TextFormat {
font_id: font_id.clone(),
color: Color32::from_rgb(0, 255, 255),
..Default::default()
});
} else if symbol.flags.0.contains(ObjSymbolFlags::Global) {
job.append("g", 0.0, TextFormat {
font_id: font_id.clone(),
color: Color32::GREEN,
..Default::default()
});
} else if symbol.flags.0.contains(ObjSymbolFlags::Local) {
job.append("l", 0.0, TextFormat {
font_id: font_id.clone(),
color: Color32::GRAY,
..Default::default()
});
}
if symbol.flags.0.contains(ObjSymbolFlags::Weak) {
job.append("w", 0.0, TextFormat {
font_id: font_id.clone(),
color: Color32::GRAY,
..Default::default()
});
}
job.append("] (", 0.0, TextFormat {
font_id: font_id.clone(),
color: Color32::GRAY,
..Default::default()
});
job.append(&format!("{:.0}%", symbol.match_percent), 0.0, TextFormat {
font_id: font_id.clone(),
color: match_color_for_symbol(symbol),
..Default::default()
});
job.append(") ", 0.0, TextFormat {
font_id: font_id.clone(),
color: Color32::GRAY,
..Default::default()
});
job.append(name, 0.0, TextFormat { font_id, color: Color32::WHITE, ..Default::default() });
let response = SelectableLabel::new(selected, job).ui(ui);
if response.clicked() {
*selected_symbol = Some(symbol.name.clone());
*current_view = View::FunctionDiff;
} else if response.hovered() {
*highlighted_symbol = Some(symbol.name.clone());
}
}
fn symbol_list_ui(
ui: &mut Ui,
obj: &ObjInfo,
highlighted_symbol: &mut Option<String>,
selected_symbol: &mut Option<String>,
current_view: &mut View,
reverse_function_order: bool,
) {
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
if !obj.common.is_empty() {
CollapsingHeader::new(".comm").default_open(true).show(ui, |ui| {
for symbol in &obj.common {
symbol_ui(ui, symbol, highlighted_symbol, selected_symbol, current_view);
}
});
}
for section in &obj.sections {
CollapsingHeader::new(format!("{} ({:x})", section.name, section.size))
.default_open(true)
.show(ui, |ui| {
if section.name == ".text" && reverse_function_order {
for symbol in section.symbols.iter().rev() {
symbol_ui(
ui,
symbol,
highlighted_symbol,
selected_symbol,
current_view,
);
}
} else {
for symbol in &section.symbols {
symbol_ui(
ui,
symbol,
highlighted_symbol,
selected_symbol,
current_view,
);
}
}
});
}
});
});
}
fn build_log_ui(ui: &mut Ui, status: &BuildStatus) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.colored_label(Color32::from_rgb(255, 0, 0), &status.log);
});
}
pub fn symbol_diff_ui(ui: &mut Ui, view_state: &mut ViewState) {
if let (Some(result), highlighted_symbol, selected_symbol, current_view) = (
&view_state.build,
&mut view_state.highlighted_symbol,
&mut view_state.selected_symbol,
&mut view_state.current_view,
) {
StripBuilder::new(ui).size(Size::exact(40.0)).size(Size::remainder()).vertical(
|mut strip| {
strip.strip(|builder| {
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
strip.cell(|ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style =
Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.label("Build asm:");
if result.first_status.success {
ui.label("OK");
} else {
ui.colored_label(Rgba::from_rgb(1.0, 0.0, 0.0), "Fail");
}
});
ui.separator();
});
strip.cell(|ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style =
Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.label("Build src:");
if result.second_status.success {
ui.label("OK");
} else {
ui.colored_label(Rgba::from_rgb(1.0, 0.0, 0.0), "Fail");
}
});
ui.separator();
});
});
});
strip.strip(|builder| {
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
strip.cell(|ui| {
if result.first_status.success {
if let Some(obj) = &result.first_obj {
ui.push_id("left", |ui| {
symbol_list_ui(
ui,
obj,
highlighted_symbol,
selected_symbol,
current_view,
view_state.reverse_fn_order,
);
});
}
} else {
build_log_ui(ui, &result.first_status);
}
});
strip.cell(|ui| {
if result.second_status.success {
if let Some(obj) = &result.second_obj {
ui.push_id("right", |ui| {
symbol_list_ui(
ui,
obj,
highlighted_symbol,
selected_symbol,
current_view,
view_state.reverse_fn_order,
);
});
}
} else {
build_log_ui(ui, &result.second_status);
}
});
});
});
},
);
}
}