Compare commits

..

3 Commits

Author SHA1 Message Date
Luke Street a0371dd110 Cargo clippy & cargo deny fixes 2022-12-06 18:09:19 -05:00
Luke Street 771a141110 Version 0.2.0
- Update checker & auto-updater
- Configure font sizes and diff colors
- Data diffing bug fixes & improvements
- Bug fix for low match percent
- Improvements to Jobs UI (cancel, dismiss errors)
- "Demangle" tool

Closes #6, #13, #17, #19
2022-12-06 17:53:32 -05:00
Luke Street 2f2efb4711 Update build workflow yml 2022-12-06 12:10:30 -05:00
27 changed files with 2276 additions and 517 deletions

View File

@ -1,68 +1,133 @@
name: Build
on: [ push, pull_request ]
on:
pull_request:
push:
paths-ignore:
- '*.md'
- 'LICENSE*'
workflow_dispatch:
env:
CARGO_BIN_NAME: objdiff
CARGO_TARGET_DIR: target
jobs:
check:
name: Check
runs-on: ubuntu-latest
strategy:
matrix:
toolchain: [ stable, 1.62.0, nightly ]
fail-fast: false
env:
RUSTFLAGS: -D warnings
steps:
- name: Install dependencies
run: |
sudo apt install libgtk-3-dev
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
run: sudo apt-get -y install libgtk-3-dev
- name: Checkout
uses: actions/checkout@v3
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
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
- name: Cargo check
run: cargo check --all-features
- name: Cargo clippy
run: cargo clippy --all-features
build:
name: Build
deny:
name: Deny
runs-on: ubuntu-latest
strategy:
matrix:
platform: [ ubuntu-latest, macos-latest, windows-latest ]
toolchain: [ stable, 1.62.0, nightly ]
checks:
- advisories
- bans licenses sources
# Prevent new advisories from failing CI
continue-on-error: ${{ matrix.checks == 'advisories' }}
steps:
- uses: actions/checkout@v3
- uses: EmbarkStudios/cargo-deny-action@v1
with:
command: check ${{ matrix.checks }}
test:
name: Test
strategy:
matrix:
platform: [ ubuntu-latest, windows-latest, macos-latest ]
fail-fast: false
runs-on: ${{ matrix.platform }}
steps:
- name: Install dependencies
if: matrix.platform == 'ubuntu-latest'
run: |
sudo apt install libgtk-3-dev
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
run: sudo apt-get -y install libgtk-3-dev
- name: Checkout
uses: actions/checkout@v3
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cargo test
run: cargo test --release --all-features
build:
name: Build
strategy:
matrix:
include:
- platform: ubuntu-latest
target: x86_64-unknown-linux-gnu
name: linux-x86_64
packages: libgtk-3-dev
- platform: windows-latest
target: x86_64-pc-windows-msvc
name: windows-x86_64
- platform: macos-latest
target: x86_64-apple-darwin
name: macos-x86_64
- platform: macos-latest
target: aarch64-apple-darwin
name: macos-arm64
fail-fast: false
runs-on: ${{ matrix.platform }}
steps:
- name: Install dependencies
if: matrix.packages != ''
run: sudo apt-get -y install ${{ matrix.packages }}
- name: Checkout
uses: actions/checkout@v3
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
profile: minimal
toolchain: ${{ matrix.toolchain }}
override: true
- uses: actions-rs/cargo@v1
targets: ${{ matrix.target }}
- name: Cargo build
run: cargo build --release --all-features --target ${{ matrix.target }} --bin ${{ env.CARGO_BIN_NAME }}
- name: Upload artifacts
uses: actions/upload-artifact@v3
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 }}
name: ${{ matrix.name }}
path: |
target/release/objdiff
target/release/objdiff.exe
${{ env.CARGO_TARGET_DIR }}/release/${{ env.CARGO_BIN_NAME }}
${{ env.CARGO_TARGET_DIR }}/release/${{ env.CARGO_BIN_NAME }}.exe
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release/${{ env.CARGO_BIN_NAME }}
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release/${{ env.CARGO_BIN_NAME }}.exe
if-no-files-found: error
release:
name: Release
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
needs: [ build ]
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
with:
path: artifacts
- name: Rename artifacts
working-directory: artifacts
run: |
mkdir ../out
for i in */*/release/$CARGO_BIN_NAME*; do
mv "$i" "../out/$(sed -E "s/([^/]+)\/[^/]+\/release\/($CARGO_BIN_NAME)/\2-\1/" <<< "$i")"
done
ls -R ../out
- name: Release
uses: softprops/action-gh-release@v1
with:
files: out/*

1339
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "objdiff"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
rust-version = "1.62"
authors = ["Luke Street <luke@street.dev>"]
@ -8,35 +8,55 @@ license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff"
readme = "README.md"
description = """
A tool for decompilation projects.
A local diffing tool for decompilation projects.
"""
publish = false
[profile.release]
lto = "thin"
strip = "debuginfo"
[dependencies]
egui = "0.19.0"
anyhow = "1.0.66"
cfg-if = "1.0.0"
const_format = "0.2.30"
cwdemangle = { git = "https://github.com/encounter/cwdemangle", rev = "286f3d1d29ee2457db89043782725631845c3e4c" }
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 = { git = "https://github.com/encounter/cwdemangle", rev = "64e8b3e083343783c5b3b6329ea940f375b057b3" }
log = "0.4.17"
rfd = { version = "0.10.0" } # , default-features = false, features = ['xdg-portal']
egui = "0.19.0"
egui_extras = "0.19.0"
flagset = "0.4.3"
log = "0.4.17"
memmap2 = "0.5.8"
notify = "5.0.0"
object = { version = "0.30.0", features = ["read_core", "std", "elf"], default-features = false }
ppc750cl = { git = "https://github.com/encounter/ppc750cl", rev = "aa631a33de7882c679afca89350898b87cb3ba3f" }
rabbitizer = { git = "https://github.com/encounter/rabbitizer-rs", rev = "10c279b2ef251c62885b1dcdcfe740b0db8e9956" }
time = { version = "0.3.14", features = ["formatting", "local-offset"] }
rfd = { version = "0.10.0" } # , default-features = false, features = ['xdg-portal']
self_update = "0.32.0"
serde = { version = "1", features = ["derive"] }
thiserror = "1.0.37"
time = { version = "0.3.17", features = ["formatting", "local-offset"] }
toml = "0.5.9"
twox-hash = "1.6.3"
tempfile = "3.3.0"
reqwest = "0.11.13"
[target.'cfg(windows)'.dependencies]
path-slash = "0.2.0"
path-slash = "0.2.1"
winapi = "0.3.9"
[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.6"
console_error_panic_hook = "0.1.7"
tracing-wasm = "0.2"
[build-dependencies]
anyhow = "1.0.66"
vergen = { version = "7.4.3", features = ["build", "cargo", "git"], default-features = false }

201
LICENSE-APACHE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2022 Luke Street.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

21
LICENSE-MIT Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright 2022 Luke Street.
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.

View File

@ -3,14 +3,12 @@
[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.
A local diffing tool for decompilation projects.
Currently supports:
- PowerPC 750CL (GameCube & Wii)
- MIPS (Nintendo 64)
**WARNING:** Very early & unstable.
![Symbol Screenshot](assets/screen-symbols.png)
![Diff Screenshot](assets/screen-diff.png)

4
build.rs Normal file
View File

@ -0,0 +1,4 @@
use anyhow::Result;
use vergen::{vergen, Config};
fn main() -> Result<()> { vergen(Config::default()) }

View File

@ -80,6 +80,7 @@ allow = [
"MPL-2.0",
"Unicode-DFS-2016",
"Zlib",
"0BSD",
]
# List of explictly disallowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
@ -203,7 +204,7 @@ allow-git = []
[sources.allow-org]
# 1 or more github.com organizations to allow git sources for
github = ["encounter", "terorie"]
github = ["encounter"]
# 1 or more gitlab.com organizations to allow git sources for
#gitlab = [""]
# 1 or more bitbucket.org organizations to allow git sources for

View File

@ -2,22 +2,23 @@ use std::{
default::Default,
ffi::OsStr,
path::{Path, PathBuf},
rc::Rc,
sync::{
atomic::{AtomicBool, Ordering},
Arc, RwLock,
Arc, Mutex, RwLock,
},
time::Duration,
};
use eframe::Frame;
use egui::Widget;
use egui::{Color32, FontFamily, FontId, TextStyle};
use notify::{RecursiveMode, Watcher};
use time::{OffsetDateTime, UtcOffset};
use crate::{
jobs::{
check_update::{queue_check_update, CheckUpdateResult},
objdiff::{queue_build, BuildStatus, ObjDiffResult},
Job, JobResult, JobState,
Job, JobResult, JobState, JobStatus,
},
views::{
config::config_ui, data_diff::data_diff_ui, function_diff::function_diff_ui, jobs::jobs_ui,
@ -48,6 +49,36 @@ pub struct DiffConfig {
// pub mapped_symbols: HashMap<String, String>,
}
const DEFAULT_COLOR_ROTATION: [Color32; 9] = [
Color32::from_rgb(255, 0, 255),
Color32::from_rgb(0, 255, 255),
Color32::from_rgb(0, 128, 0),
Color32::from_rgb(255, 0, 0),
Color32::from_rgb(255, 255, 0),
Color32::from_rgb(255, 192, 203),
Color32::from_rgb(0, 0, 255),
Color32::from_rgb(0, 255, 0),
Color32::from_rgb(128, 128, 128),
];
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct ViewConfig {
pub ui_font: FontId,
pub code_font: FontId,
pub diff_colors: Vec<Color32>,
}
impl Default for ViewConfig {
fn default() -> Self {
Self {
ui_font: FontId { size: 14.0, family: FontFamily::Proportional },
code_font: FontId { size: 14.0, family: FontFamily::Monospace },
diff_colors: DEFAULT_COLOR_ROTATION.to_vec(),
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct ViewState {
@ -64,14 +95,21 @@ pub struct ViewState {
#[serde(skip)]
pub show_config: bool,
#[serde(skip)]
pub show_demangle: bool,
#[serde(skip)]
pub demangle_text: String,
#[serde(skip)]
pub diff_config: DiffConfig,
#[serde(skip)]
pub search: String,
#[serde(skip)]
pub utc_offset: UtcOffset,
#[serde(skip)]
pub check_update: Option<Box<CheckUpdateResult>>,
// Config
pub diff_kind: DiffKind,
pub reverse_fn_order: bool,
pub view_config: ViewConfig,
}
impl Default for ViewState {
@ -83,11 +121,15 @@ impl Default for ViewState {
selected_symbol: None,
current_view: Default::default(),
show_config: false,
show_demangle: false,
demangle_text: String::new(),
diff_config: Default::default(),
search: Default::default(),
utc_offset: UtcOffset::UTC,
check_update: None,
diff_kind: Default::default(),
reverse_fn_order: false,
view_config: Default::default(),
}
}
}
@ -95,7 +137,7 @@ impl Default for ViewState {
#[derive(Default, Clone, serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct AppConfig {
pub custom_make: String,
pub custom_make: Option<String>,
// WSL2 settings
#[serde(skip)]
pub available_wsl_distros: Option<Vec<String>>,
@ -111,6 +153,19 @@ pub struct AppConfig {
pub right_obj: Option<PathBuf>,
#[serde(skip)]
pub project_dir_change: bool,
#[serde(skip)]
pub queue_update_check: bool,
pub auto_update_check: bool,
}
#[derive(Default, Clone, serde::Deserialize)]
#[serde(default)]
pub struct ProjectConfig {
pub custom_make: Option<String>,
pub project_dir: Option<PathBuf>,
pub target_obj_dir: Option<PathBuf>,
pub base_obj_dir: Option<PathBuf>,
pub build_target: bool,
}
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
@ -124,6 +179,10 @@ pub struct App {
modified: Arc<AtomicBool>,
#[serde(skip)]
watcher: Option<notify::RecommendedWatcher>,
#[serde(skip)]
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
#[serde(skip)]
should_relaunch: bool,
}
impl Default for App {
@ -133,6 +192,8 @@ impl Default for App {
config: Arc::new(Default::default()),
modified: Arc::new(Default::default()),
watcher: None,
relaunch_path: Default::default(),
should_relaunch: false,
}
}
}
@ -141,7 +202,11 @@ const CONFIG_KEY: &str = "app_config";
impl App {
/// Called once before the first frame.
pub fn new(cc: &eframe::CreationContext<'_>, utc_offset: UtcOffset) -> Self {
pub fn new(
cc: &eframe::CreationContext<'_>,
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`.
@ -153,11 +218,16 @@ impl App {
if config.project_dir.is_some() {
config.project_dir_change = true;
}
config.queue_update_check = config.auto_update_check;
app.config = Arc::new(RwLock::new(config));
app.view_state.utc_offset = utc_offset;
app.relaunch_path = relaunch_path;
app
} else {
Self::default()
let mut app = Self::default();
app.view_state.utc_offset = utc_offset;
app.relaunch_path = relaunch_path;
app
}
}
}
@ -165,17 +235,44 @@ impl App {
impl eframe::App for App {
/// Called each time the UI needs repainting, which may be many times per second.
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
fn update(&mut self, ctx: &egui::Context, frame: &mut Frame) {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
if self.should_relaunch {
frame.close();
return;
}
let Self { config, view_state, .. } = self;
{
let config = &view_state.view_config;
let mut style = (*ctx.style()).clone();
style.text_styles.insert(TextStyle::Body, FontId {
size: (config.ui_font.size * 0.75).floor(),
family: config.ui_font.family.clone(),
});
style.text_styles.insert(TextStyle::Body, config.ui_font.clone());
style.text_styles.insert(TextStyle::Button, config.ui_font.clone());
style.text_styles.insert(TextStyle::Heading, FontId {
size: (config.ui_font.size * 1.5).floor(),
family: config.ui_font.family.clone(),
});
style.text_styles.insert(TextStyle::Monospace, config.code_font.clone());
ctx.set_style(style);
}
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
ui.menu_button("File", |ui| {
if ui.button("Show config").clicked() {
view_state.show_config = !view_state.show_config;
}
if ui.button("Quit").clicked() {
frame.close();
}
if ui.button("Show config").clicked() {
view_state.show_config = !view_state.show_config;
});
ui.menu_button("Tools", |ui| {
if ui.button("Demangle").clicked() {
view_state.show_demangle = !view_state.show_demangle;
}
});
});
@ -221,31 +318,53 @@ impl eframe::App for App {
}
egui::Window::new("Config").open(&mut view_state.show_config).show(ctx, |ui| {
ui.label("Diff type:");
if egui::RadioButton::new(
view_state.diff_kind == DiffKind::SplitObj,
"Split object diff",
)
.ui(ui)
.on_hover_text("Compare individual object files")
.clicked()
{
view_state.diff_kind = DiffKind::SplitObj;
}
if egui::RadioButton::new(
view_state.diff_kind == DiffKind::WholeBinary,
"Whole binary diff",
)
.ui(ui)
.on_hover_text("Compare two full binaries")
.clicked()
{
view_state.diff_kind = DiffKind::WholeBinary;
}
ui.label("UI font:");
egui::introspection::font_id_ui(ui, &mut view_state.view_config.ui_font);
ui.separator();
ui.label("Code font:");
egui::introspection::font_id_ui(ui, &mut view_state.view_config.code_font);
ui.separator();
ui.label("Diff colors:");
if ui.button("Reset").clicked() {
view_state.view_config.diff_colors = DEFAULT_COLOR_ROTATION.to_vec();
}
let mut remove_at: Option<usize> = None;
let num_colors = view_state.view_config.diff_colors.len();
for (idx, color) in view_state.view_config.diff_colors.iter_mut().enumerate() {
ui.horizontal(|ui| {
ui.color_edit_button_srgba(color);
if num_colors > 1 && ui.small_button("-").clicked() {
remove_at = Some(idx);
}
});
}
if let Some(idx) = remove_at {
view_state.view_config.diff_colors.remove(idx);
}
if ui.small_button("+").clicked() {
view_state.view_config.diff_colors.push(Color32::BLACK);
}
});
egui::Window::new("Demangle").open(&mut view_state.show_demangle).show(ctx, |ui| {
ui.text_edit_singleline(&mut view_state.demangle_text);
ui.add_space(10.0);
if let Some(demangled) =
cwdemangle::demangle(&view_state.demangle_text, &Default::default())
{
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
ui.colored_label(Color32::LIGHT_BLUE, &demangled);
});
if ui.button("Copy").clicked() {
ui.output().copied_text = demangled;
}
} else {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
ui.colored_label(Color32::LIGHT_RED, "[invalid]");
});
}
});
// Windows + request_repaint_after breaks dialogs:
@ -272,7 +391,7 @@ impl eframe::App for App {
eframe::set_value(storage, eframe::APP_KEY, self);
}
fn post_rendering(&mut self, _window_size_px: [u32; 2], _frame: &Frame) {
fn post_rendering(&mut self, _window_size_px: [u32; 2], _frame: &eframe::Frame) {
for job in &mut self.view_state.jobs {
if let Some(handle) = &job.handle {
if !handle.is_finished() {
@ -305,10 +424,38 @@ impl eframe::App for App {
time: OffsetDateTime::now_utc(),
}));
}
JobResult::CheckUpdate(state) => {
self.view_state.check_update = Some(state);
}
JobResult::Update(state) => {
if let Ok(mut guard) = self.relaunch_path.lock() {
*guard = Some(state.exe_path);
}
self.should_relaunch = true;
}
}
Err(e) => {
log::error!("Failed to join job handle: {:?}", e);
}
Err(err) => {
let err = if let Some(msg) = err.downcast_ref::<&'static str>() {
anyhow::Error::msg(*msg)
} else if let Some(msg) = err.downcast_ref::<String>() {
anyhow::Error::msg(msg.clone())
} else {
anyhow::Error::msg("Thread panicked")
};
let result = job.status.write();
if let Ok(mut guard) = result {
guard.error = Some(err);
} else {
drop(result);
job.status = Arc::new(RwLock::new(JobStatus {
title: "Error".to_string(),
progress_percent: 0.0,
progress_items: None,
status: "".to_string(),
error: Some(err),
}));
}
}
}
}
@ -334,7 +481,7 @@ impl eframe::App for App {
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),
Err(e) => eprintln!("Failed to create watcher: {e}"),
}
config.project_dir_change = false;
self.modified.store(true, Ordering::Relaxed);
@ -355,6 +502,11 @@ impl eframe::App for App {
}
self.modified.store(false, Ordering::Relaxed);
}
if config.queue_update_check {
self.view_state.jobs.push(queue_check_update());
config.queue_update_check = false;
}
}
}
}
@ -380,7 +532,7 @@ fn create_watcher(
}
}
}
Err(e) => println!("watch error: {:?}", e),
Err(e) => println!("watch error: {e:?}"),
})?;
watcher.watch(project_dir, RecursiveMode::Recursive)?;
Ok(watcher)

View File

@ -12,6 +12,30 @@ use crate::{
},
};
fn no_diff_code(
arch: ObjArchitecture,
data: &[u8],
symbol: &mut ObjSymbol,
relocs: &[ObjReloc],
) -> Result<()> {
let code =
&data[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
let (_, ins) = match arch {
ObjArchitecture::PowerPc => ppc::process_code(code, symbol.address, relocs)?,
ObjArchitecture::Mips => {
mips::process_code(code, symbol.address, symbol.address + symbol.size, relocs)?
}
};
let mut diff = Vec::<ObjInsDiff>::new();
for i in ins {
diff.push(ObjInsDiff { ins: Some(i), kind: ObjInsDiffKind::None, ..Default::default() });
}
resolve_branches(&mut diff);
symbol.instructions = diff;
Ok(())
}
pub fn diff_code(
arch: ObjArchitecture,
left_data: &[u8],
@ -133,8 +157,8 @@ pub fn diff_code(
} else {
((total - diff_state.diff_count) as f32 / total as f32) * 100.0
};
left_symbol.match_percent = percent;
right_symbol.match_percent = percent;
left_symbol.match_percent = Some(percent);
right_symbol.match_percent = Some(percent);
left_symbol.instructions = left_diff;
right_symbol.instructions = right_diff;
@ -217,7 +241,7 @@ fn arg_eq(
) -> bool {
return match left {
ObjInsArg::PpcArg(l) => match right {
ObjInsArg::PpcArg(r) => format!("{}", l) == format!("{}", r),
ObjInsArg::PpcArg(r) => format!("{l}") == format!("{r}"),
_ => false,
},
ObjInsArg::Reloc => {
@ -288,10 +312,10 @@ fn compare_ins(
state.diff_count += 1;
}
let a_str = match a {
ObjInsArg::PpcArg(arg) => format!("{}", arg),
ObjInsArg::PpcArg(arg) => format!("{arg}"),
ObjInsArg::Reloc | ObjInsArg::RelocWithBase => String::new(),
ObjInsArg::MipsArg(str) => str.clone(),
ObjInsArg::BranchOffset(arg) => format!("{}", arg),
ObjInsArg::BranchOffset(arg) => format!("{arg}"),
};
let a_diff = if let Some(idx) = state.left_args_idx.get(&a_str) {
ObjInsArgDiff { idx: *idx }
@ -302,10 +326,10 @@ fn compare_ins(
ObjInsArgDiff { idx }
};
let b_str = match b {
ObjInsArg::PpcArg(arg) => format!("{}", arg),
ObjInsArg::PpcArg(arg) => format!("{arg}"),
ObjInsArg::Reloc | ObjInsArg::RelocWithBase => String::new(),
ObjInsArg::MipsArg(str) => str.clone(),
ObjInsArg::BranchOffset(arg) => format!("{}", arg),
ObjInsArg::BranchOffset(arg) => format!("{arg}"),
};
let b_diff = if let Some(idx) = state.right_args_idx.get(&b_str) {
ObjInsArgDiff { idx: *idx }
@ -356,13 +380,101 @@ pub fn diff_objs(left: &mut ObjInfo, right: &mut ObjInfo, _diff_config: &DiffCon
&left_section.relocations,
&right_section.relocations,
)?;
} else {
no_diff_code(
left.architecture,
&left_section.data,
left_symbol,
&left_section.relocations,
)?;
}
}
for right_symbol in &mut right_section.symbols {
if right_symbol.instructions.is_empty() {
no_diff_code(
left.architecture,
&right_section.data,
right_symbol,
&right_section.relocations,
)?;
}
}
} else if left_section.kind == ObjSectionKind::Data {
diff_data(left_section, right_section);
// diff_data_symbols(left_section, right_section)?;
} else if left_section.kind == ObjSectionKind::Bss {
diff_bss_symbols(&mut left_section.symbols, &mut right_section.symbols)?;
}
}
}
diff_bss_symbols(&mut left.common, &mut right.common)?;
Ok(())
}
fn diff_bss_symbols(left_symbols: &mut [ObjSymbol], right_symbols: &mut [ObjSymbol]) -> Result<()> {
for left_symbol in left_symbols {
if let Some(right_symbol) = find_symbol(right_symbols, &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)]
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(())
}
@ -373,11 +485,13 @@ fn diff_data(left: &mut ObjSection, right: &mut ObjSection) {
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;
}
@ -390,23 +504,7 @@ fn diff_data(left: &mut ObjSection, right: &mut ObjSection) {
let mut cur_left_data = Vec::<u8>::new();
let mut cur_right_data = Vec::<u8>::new();
for op in edit_ops {
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,
});
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,
});
right_cur = op.second_start;
}
if cur_op != op.op_type {
if cur_op != op.op_type || left_cur < op.first_start || right_cur < op.second_start {
match cur_op {
LevEditType::Keep => {}
LevEditType::Replace => {
@ -418,11 +516,13 @@ fn diff_data(left: &mut ObjSection, right: &mut ObjSection) {
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 => {
@ -432,11 +532,13 @@ fn diff_data(left: &mut ObjSection, right: &mut ObjSection) {
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 => {
@ -446,15 +548,35 @@ fn diff_data(left: &mut ObjSection, right: &mut ObjSection) {
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]);
@ -504,11 +626,13 @@ fn diff_data(left: &mut ObjSection, right: &mut ObjSection) {
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 => {
@ -518,11 +642,13 @@ fn diff_data(left: &mut ObjSection, right: &mut ObjSection) {
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 => {
@ -532,15 +658,34 @@ fn diff_data(left: &mut ObjSection, right: &mut ObjSection) {
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;
}

View File

@ -47,7 +47,8 @@ pub struct LevMatchingBlock {
pub len: usize,
}
pub fn editops_find(query: &[u8], choice: &[u8]) -> Vec<LevEditOp> {
pub fn editops_find<T>(query: &[T], choice: &[T]) -> Vec<LevEditOp>
where T: PartialEq {
let string_affix = Affix::find(query, choice);
let first_string_len = string_affix.first_string_len;
@ -96,14 +97,17 @@ pub fn editops_find(query: &[u8], choice: &[u8]) -> Vec<LevEditOp> {
)
}
fn editops_from_cost_matrix(
string1: &[u8],
string2: &[u8],
fn editops_from_cost_matrix<T>(
string1: &[T],
string2: &[T],
len1: usize,
len2: usize,
prefix_len: usize,
cache_matrix: Vec<usize>,
) -> Vec<LevEditOp> {
) -> Vec<LevEditOp>
where
T: PartialEq,
{
let mut dir = 0;
let mut ops: Vec<LevEditOp> = vec![];
@ -187,7 +191,8 @@ pub struct Affix {
}
impl Affix {
pub fn find(first_string: &[u8], second_string: &[u8]) -> Affix {
pub fn find<T>(first_string: &[T], second_string: &[T]) -> Affix
where T: PartialEq {
// 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();

View File

@ -38,7 +38,7 @@ fn run_build(
}
pub fn queue_bindiff(config: Arc<RwLock<AppConfig>>) -> JobState {
queue_job(Job::BinDiff, move |status, cancel| {
queue_job("Binary diff", Job::BinDiff, move |status, cancel| {
run_build(status, cancel, config).map(JobResult::BinDiff)
})
}

33
src/jobs/check_update.rs Normal file
View File

@ -0,0 +1,33 @@
use std::sync::mpsc::Receiver;
use anyhow::{Context, Result};
use self_update::{cargo_crate_version, update::Release};
use crate::{
jobs::{queue_job, update_status, Job, JobResult, JobState, Status},
update::{build_updater, BIN_NAME},
};
pub struct CheckUpdateResult {
pub update_available: bool,
pub latest_release: Release,
pub found_binary: bool,
}
fn run_check_update(status: &Status, cancel: Receiver<()>) -> Result<Box<CheckUpdateResult>> {
update_status(status, "Fetching latest release".to_string(), 0, 1, &cancel)?;
let updater = build_updater().context("Failed to create release updater")?;
let latest_release = updater.get_latest_release()?;
let update_available =
self_update::version::bump_is_greater(cargo_crate_version!(), &latest_release.version)?;
let found_binary = latest_release.assets.iter().any(|a| a.name == BIN_NAME);
update_status(status, "Complete".to_string(), 1, 1, &cancel)?;
Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary }))
}
pub fn queue_check_update() -> JobState {
queue_job("Check for updates", Job::CheckUpdate, move |status, cancel| {
run_check_update(status, cancel).map(JobResult::CheckUpdate)
})
}

View File

@ -9,15 +9,22 @@ use std::{
use anyhow::Result;
use crate::jobs::{bindiff::BinDiffResult, objdiff::ObjDiffResult};
use crate::jobs::{
bindiff::BinDiffResult, check_update::CheckUpdateResult, objdiff::ObjDiffResult,
update::UpdateResult,
};
pub mod bindiff;
pub mod check_update;
pub mod objdiff;
pub mod update;
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum Job {
ObjDiff,
BinDiff,
CheckUpdate,
Update,
}
pub static JOB_ID: AtomicUsize = AtomicUsize::new(0);
pub struct JobState {
@ -40,6 +47,8 @@ pub enum JobResult {
None,
ObjDiff(Box<ObjDiffResult>),
BinDiff(Box<BinDiffResult>),
CheckUpdate(Box<CheckUpdateResult>),
Update(Box<UpdateResult>),
}
fn should_cancel(rx: &Receiver<()>) -> bool {
@ -52,14 +61,15 @@ fn should_cancel(rx: &Receiver<()>) -> bool {
type Status = Arc<RwLock<JobStatus>>;
fn queue_job(
title: &str,
job_type: Job,
run: impl FnOnce(&Status, Receiver<()>) -> Result<JobResult> + Send + 'static,
) -> JobState {
let status = Arc::new(RwLock::new(JobStatus {
title: String::new(),
title: title.to_string(),
progress_percent: 0.0,
progress_items: None,
status: "".to_string(),
status: String::new(),
error: None,
}));
let status_clone = status.clone();

View File

@ -19,6 +19,7 @@ pub struct BuildStatus {
pub success: bool,
pub log: String,
}
pub struct ObjDiffResult {
pub first_status: BuildStatus,
pub second_status: BuildStatus,
@ -29,7 +30,7 @@ pub struct ObjDiffResult {
fn run_make(cwd: &Path, arg: &Path, config: &AppConfig) -> BuildStatus {
match (|| -> Result<BuildStatus> {
let make = if config.custom_make.is_empty() { "make" } else { &config.custom_make };
let make = config.custom_make.as_deref().unwrap_or("make");
#[cfg(not(windows))]
let mut command = {
let mut command = Command::new(make);
@ -66,7 +67,7 @@ fn run_make(cwd: &Path, arg: &Path, config: &AppConfig) -> BuildStatus {
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),
log: format!("{stdout}\n{stderr}"),
})
})() {
Ok(status) => status,
@ -101,26 +102,26 @@ fn run_build(
let total = if config.build_target { 5 } else { 4 };
let first_status = if config.build_target {
update_status(status, format!("Building target {}", obj_path), 0, total, &cancel)?;
update_status(status, format!("Building target {obj_path}"), 0, total, &cancel)?;
run_make(project_dir, target_path_rel, &config)
} else {
BuildStatus { success: true, log: String::new() }
};
update_status(status, format!("Building base {}", obj_path), 1, total, &cancel)?;
update_status(status, format!("Building base {obj_path}"), 1, total, &cancel)?;
let second_status = run_make(project_dir, base_path_rel, &config);
let time = OffsetDateTime::now_utc();
let mut first_obj = if first_status.success {
update_status(status, format!("Loading target {}", obj_path), 2, total, &cancel)?;
update_status(status, format!("Loading target {obj_path}"), 2, total, &cancel)?;
Some(elf::read(&target_path)?)
} else {
None
};
let mut second_obj = if second_status.success {
update_status(status, format!("Loading base {}", obj_path), 3, total, &cancel)?;
update_status(status, format!("Loading base {obj_path}"), 3, total, &cancel)?;
Some(elf::read(&base_path)?)
} else {
None
@ -136,7 +137,7 @@ fn run_build(
}
pub fn queue_build(config: Arc<RwLock<AppConfig>>, diff_config: DiffConfig) -> JobState {
queue_job(Job::ObjDiff, move |status, cancel| {
queue_job("Object diff", Job::ObjDiff, move |status, cancel| {
run_build(status, cancel, config, diff_config).map(JobResult::ObjDiff)
})
}

61
src/jobs/update.rs Normal file
View File

@ -0,0 +1,61 @@
use std::{
env::{current_dir, current_exe},
fs,
fs::File,
path::PathBuf,
sync::mpsc::Receiver,
};
use anyhow::{Context, Result};
use const_format::formatcp;
use crate::{
jobs::{queue_job, update_status, Job, JobResult, JobState, Status},
update::{build_updater, BIN_NAME},
};
pub struct UpdateResult {
pub exe_path: PathBuf,
}
fn run_update(status: &Status, cancel: Receiver<()>) -> Result<Box<UpdateResult>> {
update_status(status, "Fetching latest release".to_string(), 0, 3, &cancel)?;
let updater = build_updater().context("Failed to create release updater")?;
let latest_release = updater.get_latest_release()?;
let asset = latest_release
.assets
.iter()
.find(|a| a.name == BIN_NAME)
.ok_or_else(|| anyhow::Error::msg(formatcp!("No release asset for {}", BIN_NAME)))?;
update_status(status, "Downloading release".to_string(), 1, 3, &cancel)?;
let tmp_dir = tempfile::Builder::new().prefix("update").tempdir_in(current_dir()?)?;
let tmp_path = tmp_dir.path().join(&asset.name);
let tmp_file = File::create(&tmp_path)?;
self_update::Download::from_url(&asset.download_url)
.set_header(reqwest::header::ACCEPT, "application/octet-stream".parse()?)
.download_to(&tmp_file)?;
update_status(status, "Extracting release".to_string(), 2, 3, &cancel)?;
let tmp_file = tmp_dir.path().join("replacement_tmp");
let target_file = current_exe()?;
self_update::Move::from_source(&tmp_path)
.replace_using_temp(&tmp_file)
.to_dest(&target_file)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&target_file)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&target_file, perms)?;
}
update_status(status, "Complete".to_string(), 3, 3, &cancel)?;
Ok(Box::from(UpdateResult { exe_path: target_file }))
}
pub fn queue_update() -> JobState {
queue_job("Update app", Job::Update, move |status, cancel| {
run_update(status, cancel).map(JobResult::Update)
})
}

View File

@ -7,4 +7,5 @@ mod diff;
mod editops;
mod jobs;
mod obj;
mod update;
mod views;

View File

@ -1,6 +1,9 @@
#![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};
use cfg_if::cfg_if;
use time::UtcOffset;
// When compiling natively:
@ -12,15 +15,40 @@ fn main() {
// Because localtime_r is unsound in multithreaded apps,
// we must call this before initializing eframe.
// https://github.com/time-rs/time/issues/293
let utc_offset = UtcOffset::current_local_offset().unwrap();
let utc_offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC);
let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None));
let exec_path_clone = exec_path.clone();
let native_options = eframe::NativeOptions::default();
// native_options.renderer = eframe::Renderer::Wgpu;
eframe::run_native(
"objdiff",
native_options,
Box::new(move |cc| Box::new(objdiff::App::new(cc, utc_offset))),
Box::new(move |cc| Box::new(objdiff::App::new(cc, utc_offset, exec_path_clone))),
);
// Attempt to relaunch application from the updated path
if let Ok(mut guard) = exec_path.lock() {
if let Some(path) = guard.take() {
cfg_if! {
if #[cfg(unix)] {
let result = exec::Command::new(path)
.args(&std::env::args().collect::<Vec<String>>())
.exec();
eprintln!("Failed to relaunch: {result:?}");
} else {
let result = std::process::Command::new(path)
.args(std::env::args())
.spawn()
.unwrap()
.wait();
if let Err(e) = result {
eprintln!("Failed to relaunch: {:?}", e);
}
}
}
}
};
}
// when compiling to web using trunk.

View File

@ -22,14 +22,14 @@ fn to_obj_section_kind(kind: SectionKind) -> ObjSectionKind {
SectionKind::Text => ObjSectionKind::Code,
SectionKind::Data | SectionKind::ReadOnlyData => ObjSectionKind::Data,
SectionKind::UninitializedData => ObjSectionKind::Bss,
_ => panic!("Unhandled section kind {:?}", kind),
_ => panic!("Unhandled section kind {kind:?}"),
}
}
fn to_obj_symbol(obj_file: &File<'_>, symbol: &Symbol<'_, '_>, addend: i64) -> Result<ObjSymbol> {
let mut name = symbol.name().context("Failed to process symbol name")?;
if name.is_empty() {
println!("Found empty sym: {:?}", symbol);
println!("Found empty sym: {symbol:?}");
name = "?";
}
let mut flags = ObjSymbolFlagSet(ObjSymbolFlags::none());
@ -63,7 +63,7 @@ fn to_obj_symbol(obj_file: &File<'_>, symbol: &Symbol<'_, '_>, addend: i64) -> R
addend,
diff_symbol: None,
instructions: vec![],
match_percent: 0.0,
match_percent: None,
})
}
@ -81,7 +81,7 @@ fn filter_sections(obj_file: &File<'_>) -> Result<Vec<ObjSection>> {
continue;
}
let name = section.name().context("Failed to process section name")?;
let data = section.data().context("Failed to read section data")?;
let data = section.uncompressed_data().context("Failed to read section data")?;
result.push(ObjSection {
name: name.to_string(),
kind: to_obj_section_kind(section.kind()),
@ -183,7 +183,7 @@ fn find_section_symbol(
addend: offset_addr as i64,
diff_symbol: None,
instructions: vec![],
match_percent: 0.0,
match_percent: None,
})
}
@ -220,8 +220,7 @@ fn relocations_by_section(
R_PPC_EMB_SDA21 => ObjRelocKind::PpcEmbSda21,
_ => {
return Err(anyhow::Error::msg(format!(
"Unhandled PPC relocation type: {}",
kind
"Unhandled PPC relocation type: {kind}"
)))
}
},
@ -231,8 +230,7 @@ fn relocations_by_section(
R_MIPS_LO16 => ObjRelocKind::MipsLo16,
_ => {
return Err(anyhow::Error::msg(format!(
"Unhandled MIPS relocation type: {}",
kind
"Unhandled MIPS relocation type: {kind}"
)))
}
},
@ -271,8 +269,7 @@ fn relocations_by_section(
let addend = reloc.addend();
if addend < 0 {
return Err(anyhow::Error::msg(format!(
"Negative addend in section reloc: {}",
addend
"Negative addend in section reloc: {addend}"
)));
}
addend as u32
@ -290,8 +287,11 @@ fn relocations_by_section(
}
pub fn read(obj_path: &Path) -> Result<ObjInfo> {
let bin_data = fs::read(obj_path)?;
let obj_file = File::parse(&*bin_data)?;
let data = {
let file = fs::File::open(obj_path)?;
unsafe { memmap2::Mmap::map(&file) }?
};
let obj_file = File::parse(&*data)?;
let architecture = match obj_file.architecture() {
Architecture::PowerPc => ObjArchitecture::PowerPc,
Architecture::Mips => ObjArchitecture::Mips,

View File

@ -109,6 +109,7 @@ pub struct ObjDataDiff {
pub data: Vec<u8>,
pub kind: ObjDataDiffKind,
pub len: usize,
pub symbol: String,
}
#[derive(Debug, Clone)]
pub struct ObjSymbol {
@ -124,7 +125,7 @@ pub struct ObjSymbol {
// Diff
pub diff_symbol: Option<String>,
pub instructions: Vec<ObjInsDiff>,
pub match_percent: f32,
pub match_percent: Option<f32>,
}
#[derive(Debug, Copy, Clone)]
pub enum ObjArchitecture {

37
src/update.rs Normal file
View File

@ -0,0 +1,37 @@
use cfg_if::cfg_if;
use const_format::formatcp;
use self_update::{cargo_crate_version, update::ReleaseUpdate};
pub const OS: &str = std::env::consts::OS;
cfg_if! {
if #[cfg(target_arch = "aarch64")] {
cfg_if! {
if #[cfg(any(windows, target_os = "macos"))] {
pub const ARCH: &str = "arm64";
} else {
pub const ARCH: &str = std::env::consts::ARCH;
}
}
} else if #[cfg(target_arch = "arm")] {
pub const ARCH: &str = "armv7l";
} else {
pub const ARCH: &str = std::env::consts::ARCH;
}
}
pub const GITHUB_USER: &str = "encounter";
pub const GITHUB_REPO: &str = "objdiff";
pub const BIN_NAME: &str =
formatcp!("{}-{}-{}{}", GITHUB_REPO, OS, ARCH, std::env::consts::EXE_SUFFIX);
pub const RELEASE_URL: &str =
formatcp!("https://github.com/{}/{}/releases/latest", GITHUB_USER, GITHUB_REPO);
pub fn build_updater() -> self_update::errors::Result<Box<dyn ReleaseUpdate>> {
self_update::backends::github::Update::configure()
.repo_owner(GITHUB_USER)
.repo_name(GITHUB_REPO)
.bin_name(BIN_NAME)
.no_confirm(true)
.show_output(false)
.current_version(cargo_crate_version!())
.build()
}

View File

@ -4,10 +4,14 @@ use std::sync::{Arc, RwLock};
#[cfg(windows)]
use anyhow::{Context, Result};
use const_format::formatcp;
use egui::{output::OpenUrl, Color32};
use self_update::cargo_crate_version;
use crate::{
app::{AppConfig, DiffKind, ViewState},
jobs::{bindiff::queue_bindiff, objdiff::queue_build},
jobs::{bindiff::queue_bindiff, objdiff::queue_build, update::queue_update},
update::RELEASE_URL,
};
#[cfg(windows)]
@ -57,8 +61,47 @@ pub fn config_ui(ui: &mut egui::Ui, config: &Arc<RwLock<AppConfig>>, view_state:
left_obj,
right_obj,
project_dir_change,
queue_update_check,
auto_update_check,
} = &mut *config_guard;
ui.heading("Updates");
ui.checkbox(auto_update_check, "Check for updates on startup");
if ui.button("Check now").clicked() {
*queue_update_check = true;
}
ui.label(format!("Current version: {}", cargo_crate_version!())).on_hover_ui_at_pointer(|ui| {
ui.label(formatcp!("Git branch: {}", env!("VERGEN_GIT_BRANCH")));
ui.label(formatcp!("Git commit: {}", env!("VERGEN_GIT_SHA")));
ui.label(formatcp!("Build target: {}", env!("VERGEN_CARGO_TARGET_TRIPLE")));
ui.label(formatcp!("Build type: {}", env!("VERGEN_CARGO_PROFILE")));
});
if let Some(state) = &view_state.check_update {
ui.label(format!("Latest version: {}", state.latest_release.version));
if state.update_available {
ui.colored_label(Color32::LIGHT_GREEN, "Update available");
ui.horizontal(|ui| {
if state.found_binary && ui
.button("Automatic")
.on_hover_text_at_pointer(
"Automatically download and replace the current build",
)
.clicked() {
view_state.jobs.push(queue_update());
}
if ui
.button("Manual")
.on_hover_text_at_pointer("Open a link to the latest release on GitHub")
.clicked()
{
ui.output().open_url =
Some(OpenUrl { url: RELEASE_URL.to_string(), new_tab: true });
}
});
}
}
ui.separator();
ui.heading("Build config");
#[cfg(windows)]
@ -82,7 +125,14 @@ pub fn config_ui(ui: &mut egui::Ui, config: &Arc<RwLock<AppConfig>>, view_state:
}
ui.label("Custom make program:");
ui.text_edit_singleline(custom_make);
let mut custom_make_str = custom_make.clone().unwrap_or_default();
if ui.text_edit_singleline(&mut custom_make_str).changed() {
if custom_make_str.is_empty() {
*custom_make = None;
} else {
*custom_make = Some(custom_make_str);
}
}
ui.separator();

View File

@ -5,10 +5,10 @@ use egui_extras::{Size, StripBuilder, TableBuilder};
use time::format_description;
use crate::{
app::{View, ViewState},
app::{View, ViewConfig, ViewState},
jobs::Job,
obj::{ObjDataDiff, ObjDataDiffKind, ObjInfo, ObjSection},
views::{write_text, COLOR_RED, FONT_SIZE},
views::{write_text, COLOR_RED},
};
const BYTES_PER_ROW: usize = 16;
@ -17,12 +17,17 @@ fn find_section<'a>(obj: &'a ObjInfo, section_name: &str) -> Option<&'a ObjSecti
obj.sections.iter().find(|s| s.name == section_name)
}
fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff]) {
fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], config: &ViewConfig) {
if diffs.iter().any(|d| d.kind != ObjDataDiffKind::None) {
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
}
let mut job = LayoutJob::default();
write_text(format!("{:08X}: ", address).as_str(), Color32::GRAY, &mut job);
write_text(
format!("{address:08X}: ").as_str(),
Color32::GRAY,
&mut job,
config.code_font.clone(),
);
let mut cur_addr = 0usize;
for diff in diffs {
let base_color = match diff.kind {
@ -34,18 +39,18 @@ fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff]) {
if diff.data.is_empty() {
let mut str = " ".repeat(diff.len);
str.push_str(" ".repeat(diff.len / 8).as_str());
write_text(str.as_str(), base_color, &mut job);
write_text(str.as_str(), base_color, &mut job, config.code_font.clone());
cur_addr += diff.len;
} else {
let mut text = String::new();
for byte in &diff.data {
text.push_str(format!("{:02X} ", byte).as_str());
text.push_str(format!("{byte:02X} ").as_str());
cur_addr += 1;
if cur_addr % 8 == 0 {
text.push(' ');
}
}
write_text(text.as_str(), base_color, &mut job);
write_text(text.as_str(), base_color, &mut job, config.code_font.clone());
}
}
if cur_addr < BYTES_PER_ROW {
@ -53,9 +58,9 @@ fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff]) {
let mut str = " ".to_string();
str.push_str(" ".repeat(n).as_str());
str.push_str(" ".repeat(n / 8).as_str());
write_text(str.as_str(), Color32::GRAY, &mut job);
write_text(str.as_str(), Color32::GRAY, &mut job, config.code_font.clone());
}
write_text(" ", Color32::GRAY, &mut job);
write_text(" ", Color32::GRAY, &mut job, config.code_font.clone());
for diff in diffs {
let base_color = match diff.kind {
ObjDataDiffKind::None => Color32::GRAY,
@ -64,7 +69,12 @@ fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff]) {
ObjDataDiffKind::Insert => Color32::GREEN,
};
if diff.data.is_empty() {
write_text(" ".repeat(diff.len).as_str(), base_color, &mut job);
write_text(
" ".repeat(diff.len).as_str(),
base_color,
&mut job,
config.code_font.clone(),
);
} else {
let mut text = String::new();
for byte in &diff.data {
@ -75,7 +85,7 @@ fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff]) {
text.push('.');
}
}
write_text(text.as_str(), base_color, &mut job);
write_text(text.as_str(), base_color, &mut job, config.code_font.clone());
}
}
ui.add(Label::new(job).sense(Sense::click()));
@ -101,6 +111,8 @@ fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> {
},
kind: diff.kind,
len,
// TODO
symbol: String::new(),
});
remaining_in_row -= len;
cur_len += len;
@ -121,6 +133,7 @@ fn data_table_ui(
left_obj: &ObjInfo,
right_obj: &ObjInfo,
section_name: &str,
config: &ViewConfig,
) -> Option<()> {
let left_section = find_section(left_obj, section_name)?;
let right_section = find_section(right_obj, section_name)?;
@ -135,13 +148,13 @@ fn data_table_ui(
let right_diffs = split_diffs(&right_section.data_diff);
table.body(|body| {
body.rows(FONT_SIZE, total_rows, |row_index, mut row| {
body.rows(config.code_font.size, total_rows, |row_index, mut row| {
let address = row_index * BYTES_PER_ROW;
row.col(|ui| {
data_row_ui(ui, address, &left_diffs[row_index]);
data_row_ui(ui, address, &left_diffs[row_index], config);
});
row.col(|ui| {
data_row_ui(ui, address, &right_diffs[row_index]);
data_row_ui(ui, address, &right_diffs[row_index], config);
});
});
});
@ -233,7 +246,13 @@ pub fn data_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
.column(Size::relative(0.5))
.column(Size::relative(0.5))
.resizable(false);
data_table_ui(table, left_obj, right_obj, selected_symbol);
data_table_ui(
table,
left_obj,
right_obj,
selected_symbol,
&view_state.view_config,
);
}
});
});

View File

@ -1,62 +1,62 @@
use std::default::Default;
use cwdemangle::demangle;
use egui::{text::LayoutJob, Color32, Label, Sense};
use egui::{text::LayoutJob, Color32, FontId, Label, Sense};
use egui_extras::{Size, StripBuilder, TableBuilder};
use ppc750cl::Argument;
use time::format_description;
use crate::{
app::{View, ViewState},
app::{View, ViewConfig, ViewState},
jobs::Job,
obj::{
ObjInfo, ObjIns, ObjInsArg, ObjInsArgDiff, ObjInsDiff, ObjInsDiffKind, ObjReloc,
ObjRelocKind, ObjSymbol,
},
views::{symbol_diff::match_color_for_symbol, write_text, COLOR_RED, FONT_SIZE},
views::{symbol_diff::match_color_for_symbol, write_text, COLOR_RED},
};
fn write_reloc_name(reloc: &ObjReloc, color: Color32, job: &mut LayoutJob) {
fn write_reloc_name(reloc: &ObjReloc, color: Color32, job: &mut LayoutJob, font_id: FontId) {
let name = reloc.target.demangled_name.as_ref().unwrap_or(&reloc.target.name);
write_text(name, Color32::LIGHT_GRAY, job);
write_text(name, Color32::LIGHT_GRAY, job, font_id.clone());
if reloc.target.addend != 0 {
write_text(&format!("+{:X}", reloc.target.addend), color, job);
write_text(&format!("+{:X}", reloc.target.addend), color, job, font_id);
}
}
fn write_reloc(reloc: &ObjReloc, color: Color32, job: &mut LayoutJob) {
fn write_reloc(reloc: &ObjReloc, color: Color32, job: &mut LayoutJob, font_id: FontId) {
match reloc.kind {
ObjRelocKind::PpcAddr16Lo => {
write_reloc_name(reloc, color, job);
write_text("@l", color, job);
write_reloc_name(reloc, color, job, font_id.clone());
write_text("@l", color, job, font_id);
}
ObjRelocKind::PpcAddr16Hi => {
write_reloc_name(reloc, color, job);
write_text("@h", color, job);
write_reloc_name(reloc, color, job, font_id.clone());
write_text("@h", color, job, font_id);
}
ObjRelocKind::PpcAddr16Ha => {
write_reloc_name(reloc, color, job);
write_text("@ha", color, job);
write_reloc_name(reloc, color, job, font_id.clone());
write_text("@ha", color, job, font_id);
}
ObjRelocKind::PpcEmbSda21 => {
write_reloc_name(reloc, color, job);
write_text("@sda21", color, job);
write_reloc_name(reloc, color, job, font_id.clone());
write_text("@sda21", color, job, font_id);
}
ObjRelocKind::MipsHi16 => {
write_text("%hi(", color, job);
write_reloc_name(reloc, color, job);
write_text(")", color, job);
write_text("%hi(", color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone());
write_text(")", color, job, font_id);
}
ObjRelocKind::MipsLo16 => {
write_text("%lo(", color, job);
write_reloc_name(reloc, color, job);
write_text(")", color, job);
write_text("%lo(", color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone());
write_text(")", color, job, font_id);
}
ObjRelocKind::Absolute
| ObjRelocKind::PpcRel24
| ObjRelocKind::PpcRel14
| ObjRelocKind::Mips26 => {
write_reloc_name(reloc, color, job);
write_reloc_name(reloc, color, job, font_id);
}
};
}
@ -67,6 +67,7 @@ fn write_ins(
args: &[Option<ObjInsArgDiff>],
base_addr: u32,
job: &mut LayoutJob,
config: &ViewConfig,
) {
let base_color = match diff_kind {
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
@ -83,54 +84,60 @@ fn write_ins(
_ => base_color,
},
job,
config.code_font.clone(),
);
let mut writing_offset = false;
for (i, arg) in ins.args.iter().enumerate() {
if i == 0 {
write_text(" ", base_color, job);
write_text(" ", base_color, job, config.code_font.clone());
}
if i > 0 && !writing_offset {
write_text(", ", base_color, job);
write_text(", ", base_color, job, config.code_font.clone());
}
let color = if let Some(diff) = args.get(i).and_then(|a| a.as_ref()) {
COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
config.diff_colors[diff.idx % config.diff_colors.len()]
} else {
base_color
};
match arg {
ObjInsArg::PpcArg(arg) => match arg {
Argument::Offset(val) => {
write_text(&format!("{}", val), color, job);
write_text("(", base_color, job);
write_text(&format!("{val}"), color, job, config.code_font.clone());
write_text("(", base_color, job, config.code_font.clone());
writing_offset = true;
continue;
}
Argument::Uimm(_) | Argument::Simm(_) => {
write_text(&format!("{}", arg), color, job);
write_text(&format!("{arg}"), color, job, config.code_font.clone());
}
_ => {
write_text(&format!("{}", arg), color, job);
write_text(&format!("{arg}"), color, job, config.code_font.clone());
}
},
ObjInsArg::Reloc => {
write_reloc(ins.reloc.as_ref().unwrap(), base_color, job);
write_reloc(ins.reloc.as_ref().unwrap(), base_color, job, config.code_font.clone());
}
ObjInsArg::RelocWithBase => {
write_reloc(ins.reloc.as_ref().unwrap(), base_color, job);
write_text("(", base_color, job);
write_reloc(ins.reloc.as_ref().unwrap(), base_color, job, config.code_font.clone());
write_text("(", base_color, job, config.code_font.clone());
writing_offset = true;
continue;
}
ObjInsArg::MipsArg(str) => {
write_text(str.strip_prefix('$').unwrap_or(str), color, job);
write_text(
str.strip_prefix('$').unwrap_or(str),
color,
job,
config.code_font.clone(),
);
}
ObjInsArg::BranchOffset(offset) => {
let addr = offset + ins.address as i32 - base_addr as i32;
write_text(&format!("{:x}", addr), color, job);
write_text(&format!("{addr:x}"), color, job, config.code_font.clone());
}
}
if writing_offset {
write_text(")", base_color, job);
write_text(")", base_color, job, config.code_font.clone());
writing_offset = false;
}
}
@ -164,7 +171,7 @@ fn ins_hover_ui(ui: &mut egui::Ui, ins: &ObjIns) {
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!("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 {
@ -185,8 +192,8 @@ fn ins_context_menu(ui: &mut egui::Ui, ins: &ObjIns) {
if let ObjInsArg::PpcArg(arg) = arg {
match arg {
Argument::Uimm(v) => {
if ui.button(format!("Copy \"{}\"", v)).clicked() {
ui.output().copied_text = format!("{}", 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() {
@ -195,8 +202,8 @@ fn ins_context_menu(ui: &mut egui::Ui, ins: &ObjIns) {
}
}
Argument::Simm(v) => {
if ui.button(format!("Copy \"{}\"", v)).clicked() {
ui.output().copied_text = format!("{}", 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() {
@ -205,8 +212,8 @@ fn ins_context_menu(ui: &mut egui::Ui, ins: &ObjIns) {
}
}
Argument::Offset(v) => {
if ui.button(format!("Copy \"{}\"", v)).clicked() {
ui.output().copied_text = format!("{}", 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() {
@ -220,7 +227,7 @@ fn ins_context_menu(ui: &mut egui::Ui, ins: &ObjIns) {
}
if let Some(reloc) = &ins.reloc {
if let Some(name) = &reloc.target.demangled_name {
if ui.button(format!("Copy \"{}\"", name)).clicked() {
if ui.button(format!("Copy \"{name}\"")).clicked() {
ui.output().copied_text = name.clone();
ui.close_menu();
}
@ -233,24 +240,12 @@ fn ins_context_menu(ui: &mut egui::Ui, ins: &ObjIns) {
});
}
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) {
fn asm_row_ui(ui: &mut egui::Ui, ins_diff: &ObjInsDiff, symbol: &ObjSymbol, config: &ViewConfig) {
if ins_diff.kind != ObjInsDiffKind::None {
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
}
@ -268,15 +263,26 @@ fn asm_row_ui(ui: &mut egui::Ui, ins_diff: &ObjInsDiff, symbol: &ObjSymbol) {
&format!("{:<6}", format!("{:x}:", ins.address - symbol.address as u32)),
base_color,
&mut job,
config.code_font.clone(),
);
if let Some(branch) = &ins_diff.branch_from {
write_text("~> ", COLOR_ROTATION[branch.branch_idx % COLOR_ROTATION.len()], &mut job);
write_text(
"~> ",
config.diff_colors[branch.branch_idx % config.diff_colors.len()],
&mut job,
config.code_font.clone(),
);
} else {
write_text(" ", base_color, &mut job);
write_text(" ", base_color, &mut job, config.code_font.clone());
}
write_ins(ins, &ins_diff.kind, &ins_diff.arg_diff, symbol.address as u32, &mut job);
write_ins(ins, &ins_diff.kind, &ins_diff.arg_diff, symbol.address as u32, &mut job, config);
if let Some(branch) = &ins_diff.branch_to {
write_text(" ~>", COLOR_ROTATION[branch.branch_idx % COLOR_ROTATION.len()], &mut job);
write_text(
" ~>",
config.diff_colors[branch.branch_idx % config.diff_colors.len()],
&mut job,
config.code_font.clone(),
);
}
ui.add(Label::new(job).sense(Sense::click()))
.on_hover_ui_at_pointer(|ui| ins_hover_ui(ui, ins))
@ -291,16 +297,22 @@ fn asm_table_ui(
left_obj: &ObjInfo,
right_obj: &ObjInfo,
fn_name: &str,
config: &ViewConfig,
) -> Option<()> {
let left_symbol = find_symbol(left_obj, ".text", fn_name)?;
let right_symbol = find_symbol(right_obj, ".text", fn_name)?;
let left_symbol = find_symbol(left_obj, ".text", fn_name);
let right_symbol = find_symbol(right_obj, ".text", fn_name);
let instructions_len = left_symbol.or(right_symbol).map(|s| s.instructions.len())?;
table.body(|body| {
body.rows(FONT_SIZE, left_symbol.instructions.len(), |row_index, mut row| {
body.rows(config.code_font.size, instructions_len, |row_index, mut row| {
row.col(|ui| {
asm_row_ui(ui, &left_symbol.instructions[row_index], left_symbol);
if let Some(symbol) = left_symbol {
asm_row_ui(ui, &symbol.instructions[row_index], symbol, config);
}
});
row.col(|ui| {
asm_row_ui(ui, &right_symbol.instructions[row_index], right_symbol);
if let Some(symbol) = right_symbol {
asm_row_ui(ui, &symbol.instructions[row_index], symbol, config);
}
});
});
});
@ -379,15 +391,17 @@ pub fn function_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
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)
if let Some(match_percent) = result
.second_obj
.as_ref()
.and_then(|obj| find_symbol(obj, ".text", selected_symbol))
.and_then(|symbol| symbol.match_percent)
{
ui.colored_label(
match_color_for_symbol(symbol),
&format!("{:.0}%", symbol.match_percent),
match_color_for_symbol(match_percent),
&format!("{match_percent:.0}%"),
);
}
}
ui.label("Diff base:");
ui.separator();
});
@ -404,7 +418,13 @@ pub fn function_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
.column(Size::relative(0.5))
.column(Size::relative(0.5))
.resizable(false);
asm_table_ui(table, left_obj, right_obj, selected_symbol);
asm_table_ui(
table,
left_obj,
right_obj,
selected_symbol,
&view_state.view_config,
);
}
});
});

View File

@ -2,13 +2,26 @@ use egui::{Color32, ProgressBar, Widget};
use crate::app::ViewState;
pub fn jobs_ui(ui: &mut egui::Ui, view_state: &ViewState) {
pub fn jobs_ui(ui: &mut egui::Ui, view_state: &mut ViewState) {
ui.label("Jobs");
for job in &view_state.jobs {
let mut remove_job: Option<usize> = None;
for (idx, job) in view_state.jobs.iter_mut().enumerate() {
if let Ok(status) = job.status.read() {
ui.group(|ui| {
ui.horizontal(|ui| {
ui.label(&status.title);
if ui.small_button("").clicked() {
if job.handle.is_some() {
job.should_remove = true;
if let Err(e) = job.cancel.send(()) {
eprintln!("Failed to cancel job: {e:?}");
}
} else {
remove_job = Some(idx);
}
}
});
let mut bar = ProgressBar::new(status.progress_percent);
if let Some(items) = &status.progress_items {
bar = bar.text(format!("{} / {}", items[0], items[1]));
@ -35,4 +48,8 @@ pub fn jobs_ui(ui: &mut egui::Ui, view_state: &ViewState) {
});
}
}
if let Some(idx) = remove_job {
view_state.jobs.remove(idx);
}
}

View File

@ -1,4 +1,4 @@
use egui::{text::LayoutJob, Color32, FontFamily, FontId, TextFormat};
use egui::{text::LayoutJob, Color32, FontId, TextFormat};
pub(crate) mod config;
pub(crate) mod data_diff;
@ -6,11 +6,8 @@ pub(crate) mod function_diff;
pub(crate) mod jobs;
pub(crate) mod symbol_diff;
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_text(str: &str, color: Color32, job: &mut LayoutJob, font_id: FontId) {
job.append(str, 0.0, TextFormat { font_id, color, ..Default::default() });
}

View File

@ -4,16 +4,16 @@ use egui::{
use egui_extras::{Size, StripBuilder};
use crate::{
app::{View, ViewState},
app::{View, ViewConfig, ViewState},
jobs::objdiff::BuildStatus,
obj::{ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags},
views::write_text,
};
pub fn match_color_for_symbol(symbol: &ObjSymbol) -> Color32 {
if symbol.match_percent == 100.0 {
pub fn match_color_for_symbol(match_percent: f32) -> Color32 {
if match_percent == 100.0 {
Color32::GREEN
} else if symbol.match_percent >= 50.0 {
} else if match_percent >= 50.0 {
Color32::LIGHT_BLUE
} else {
Color32::RED
@ -26,7 +26,7 @@ fn symbol_context_menu_ui(ui: &mut Ui, symbol: &ObjSymbol) {
ui.style_mut().wrap = Some(false);
if let Some(name) = &symbol.demangled_name {
if ui.button(format!("Copy \"{}\"", name)).clicked() {
if ui.button(format!("Copy \"{name}\"")).clicked() {
ui.output().copied_text = name.clone();
ui.close_menu();
}
@ -45,7 +45,11 @@ fn symbol_hover_ui(ui: &mut Ui, symbol: &ObjSymbol) {
ui.colored_label(Color32::WHITE, format!("Name: {}", symbol.name));
ui.colored_label(Color32::WHITE, format!("Address: {:x}", symbol.address));
if symbol.size_known {
ui.colored_label(Color32::WHITE, format!("Size: {:x}", symbol.size));
} else {
ui.colored_label(Color32::WHITE, format!("Size: {:x} (assumed)", symbol.size));
}
});
}
@ -56,6 +60,7 @@ fn symbol_ui(
highlighted_symbol: &mut Option<String>,
selected_symbol: &mut Option<String>,
current_view: &mut View,
config: &ViewConfig,
) {
let mut job = LayoutJob::default();
let name: &str =
@ -64,28 +69,29 @@ fn symbol_ui(
if let Some(sym) = highlighted_symbol {
selected = sym == &symbol.name;
}
write_text("[", Color32::GRAY, &mut job);
write_text("[", Color32::GRAY, &mut job, config.code_font.clone());
if symbol.flags.0.contains(ObjSymbolFlags::Common) {
write_text("c", Color32::from_rgb(0, 255, 255), &mut job);
write_text("c", Color32::from_rgb(0, 255, 255), &mut job, config.code_font.clone());
} else if symbol.flags.0.contains(ObjSymbolFlags::Global) {
write_text("g", Color32::GREEN, &mut job);
write_text("g", Color32::GREEN, &mut job, config.code_font.clone());
} else if symbol.flags.0.contains(ObjSymbolFlags::Local) {
write_text("l", Color32::GRAY, &mut job);
write_text("l", Color32::GRAY, &mut job, config.code_font.clone());
}
if symbol.flags.0.contains(ObjSymbolFlags::Weak) {
write_text("w", Color32::GRAY, &mut job);
write_text("w", Color32::GRAY, &mut job, config.code_font.clone());
}
write_text("] ", Color32::GRAY, &mut job);
if symbol.match_percent > 0.0 {
write_text("(", Color32::GRAY, &mut job);
write_text("] ", Color32::GRAY, &mut job, config.code_font.clone());
if let Some(match_percent) = symbol.match_percent {
write_text("(", Color32::GRAY, &mut job, config.code_font.clone());
write_text(
&format!("{:.0}%", symbol.match_percent),
match_color_for_symbol(symbol),
&format!("{match_percent:.0}%"),
match_color_for_symbol(match_percent),
&mut job,
config.code_font.clone(),
);
write_text(") ", Color32::GRAY, &mut job);
write_text(") ", Color32::GRAY, &mut job, config.code_font.clone());
}
write_text(name, Color32::WHITE, &mut job);
write_text(name, Color32::WHITE, &mut job, config.code_font.clone());
let response = SelectableLabel::new(selected, job)
.ui(ui)
.context_menu(|ui| symbol_context_menu_ui(ui, symbol))
@ -115,6 +121,7 @@ fn symbol_matches_search(symbol: &ObjSymbol, search_str: &str) -> bool {
.unwrap_or(false)
}
#[allow(clippy::too_many_arguments)]
fn symbol_list_ui(
ui: &mut Ui,
obj: &ObjInfo,
@ -123,6 +130,7 @@ fn symbol_list_ui(
current_view: &mut View,
reverse_function_order: bool,
search: &mut String,
config: &ViewConfig,
) {
ui.text_edit_singleline(search);
let lower_search = search.to_ascii_lowercase();
@ -142,6 +150,7 @@ fn symbol_list_ui(
highlighted_symbol,
selected_symbol,
current_view,
config,
);
}
});
@ -163,6 +172,7 @@ fn symbol_list_ui(
highlighted_symbol,
selected_symbol,
current_view,
config,
);
}
} else {
@ -177,6 +187,7 @@ fn symbol_list_ui(
highlighted_symbol,
selected_symbol,
current_view,
config,
);
}
}
@ -255,6 +266,7 @@ pub fn symbol_diff_ui(ui: &mut Ui, view_state: &mut ViewState) {
current_view,
view_state.reverse_fn_order,
search,
&view_state.view_config,
);
});
}
@ -274,6 +286,7 @@ pub fn symbol_diff_ui(ui: &mut Ui, view_state: &mut ViewState) {
current_view,
view_state.reverse_fn_order,
search,
&view_state.view_config,
);
});
}