Compare commits

..

30 Commits

Author SHA1 Message Date
Luke Street 0a85c498c5 Version 1.0.0 2024-01-22 00:32:54 -07:00
Luke Street c2fcf2797b Export function to decomp.me scratch (beta) 2024-01-22 00:31:43 -07:00
Luke Street e88a58ba39 Option to relax relocation diffs
Ignores differences in relocation targets. (Address, name, etc)

Resolves #34
2024-01-22 00:14:03 -07:00
Luke Street 02f521a528 Disable more options when project config is loaded 2024-01-21 23:58:10 -07:00
Luke Street 197d1247a8 Highlight: Consider uimm/simm/offset all equivalent
Fixes #33
2024-01-21 23:48:12 -07:00
Luke Street eef9598e76 Add DWARF 2+ line info support
Resolves #37
2024-01-21 23:38:52 -07:00
Luke Street 405a2a82db Upgrade all dependencies (+ egui/eframe 0.25.0) 2024-01-20 23:41:48 -07:00
Luke Street 4cdad8a519 Re-enable wgpu and wsl features; rework WSL config
Improve build failure log view & add copy buttons
2024-01-20 23:29:05 -07:00
Luke Street b74a49ed0c Upgrade to egui/eframe 0.24.1 2023-12-11 13:36:00 -05:00
Luke Street e1079db93a Add font loading & configuration 2023-11-28 23:13:51 -05:00
Luke Street 879e03eed5 All one line (thanks PowerShell) 2023-11-27 19:55:17 -05:00
Luke Street 53e6e0c7c4 Build macOS with wgpu enabled 2023-11-27 19:52:12 -05:00
Luke Street 67cea2a8d9 Update README.md 2023-11-24 23:17:35 -05:00
Luke Street e4f97adbdd Version 0.6.1 2023-11-22 00:11:33 -05:00
Luke Street 0ec7bf078b Highlight: Consider reg offsets and signed immediates equivalent
Fixes #32
2023-11-22 00:07:21 -05:00
Luke Street 74e89130a8 Repaint rework: more responsive, less energy
Previously, we repainted every frame on Windows at full refresh rate.
This is an enormous waste, as the UI will be static most of the time.
This was to work around a bug with `rfd` + `eframe`.
On other platforms, we only repainted every frame when a job was running,
which was better, but still not ideal. We also had a 100ms deadline, so
we'd repaint at ~10fps minimum to catch new events (file watcher, jobs).

This removes all repaint logic from the main loop and moves it into the
individual places where we change state from another thread.
For example, the file watcher thread will now immediately notify egui
to repaint, rather than relying on the 100ms deadline we had previously.
Jobs, when updating their status, also notify egui to repaint.

For `rfd` file dialogs, this migrates to using the async API built on top of
a polling thread + `pollster`. This interacts better with `eframe` on Windows.
Overall, this should reduce repaints and improve responsiveness to
file changes and background tasks.
2023-11-21 14:34:26 -05:00
Luke Street 236e4d8d26 CI updates, update deny.toml, clippy fix 2023-11-21 12:16:15 -05:00
Luke Street b900ae5a00 Disable WSL integration
With WSL, objdiff is unable to get filesystem notifications.
It's recommended to run objdiff natively on Windows, so having this option
is more confusing than useful.
2023-11-21 12:15:41 -05:00
Luke Street 261e1b8e07 Upgrade all dependencies 2023-11-21 11:57:02 -05:00
Luke Street a29e913b45 Add "Incomplete" filter to object tree
Allows filtering out objects marked as "complete".
2023-11-21 11:50:11 -05:00
Luke Street 49257dc73c Better logic to reload previous file on app start
Before, if "Rebuild on changes" was disabled, the last file
wouldn't be properly loaded when starting.
2023-11-21 11:49:26 -05:00
Luke Street dc9eec66b0 Configurable diff algorithms & new default algorithm
Uses the similar crate to support new diff algorithms:
- Patience (new default)
- Levenshtein (old default)
- Myers
- LCS (Longest Common Subsequence)

Options in "Diff Options" -> "Algorithm..."
2023-11-21 11:48:18 -05:00
Luke Street 7b58f9a269 Adjust "Diffable" to exclude missing target objects 2023-10-09 12:47:22 -04:00
Luke Street d9e7dacb6d Version 0.5.1 2023-10-07 14:49:01 -04:00
Luke Street 04b4fdcd21 Reload objects when changed externally
Uses file modification timestamp polling for project config and objects to avoid unneeded complexity from the filesystem notification watcher.

Allows disabling `build_base` as well for projects using an external build system.
2023-10-07 14:48:34 -04:00
Luke Street 803eaafee6 Hide hidden symbols by default; add "Diff Options" to menu 2023-10-07 13:27:12 -04:00
Luke Street e1dc84698f Restore context menu on highlightable fields
Fixes #30
2023-10-07 13:04:08 -04:00
Luke Street e68629c339 Update ppc750cl (subi{,s,c} mnemonics, capstone-style CR bits) 2023-10-06 01:22:26 -04:00
Luke Street bb9ff4b928 Update all dependencies 2023-10-05 23:55:01 -04:00
Luke Street 57392daaeb Implement click-to-highlight
Highlights registers, instructions, arguments, symbols or addresses on click.

Resolves #7
2023-10-05 23:40:45 -04:00
36 changed files with 4892 additions and 2254 deletions
+33 -11
View File
@@ -9,6 +9,7 @@ on:
workflow_dispatch:
env:
BUILD_PROFILE: release-lto
CARGO_BIN_NAME: objdiff
CARGO_TARGET_DIR: target
@@ -28,11 +29,27 @@ jobs:
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
components: clippy
- name: Cargo check
run: cargo check --all-features
run: cargo check
- name: Cargo clippy
run: cargo clippy --all-features
run: cargo clippy
fmt:
name: Format
runs-on: ubuntu-latest
env:
RUSTFLAGS: -D warnings
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Rust toolchain
# We use nightly options in rustfmt.toml
uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- name: Cargo fmt
run: cargo fmt --all --check
deny:
name: Deny
@@ -52,6 +69,7 @@ jobs:
test:
name: Test
if: 'false' # No tests yet
strategy:
matrix:
platform: [ ubuntu-latest, windows-latest, macos-latest ]
@@ -68,7 +86,7 @@ jobs:
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cargo test
run: cargo test --release --all-features
run: cargo test --release
build:
name: Build
@@ -79,15 +97,19 @@ jobs:
target: x86_64-unknown-linux-gnu
name: linux-x86_64
packages: libgtk-3-dev
features: default
- platform: windows-latest
target: x86_64-pc-windows-msvc
name: windows-x86_64
features: default
- platform: macos-latest
target: x86_64-apple-darwin
name: macos-x86_64
features: default
- platform: macos-latest
target: aarch64-apple-darwin
name: macos-arm64
features: default
fail-fast: false
runs-on: ${{ matrix.platform }}
steps:
@@ -103,16 +125,16 @@ jobs:
with:
targets: ${{ matrix.target }}
- name: Cargo build
run: cargo build --release --all-features --target ${{ matrix.target }} --bin ${{ env.CARGO_BIN_NAME }}
run: cargo build --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }} --bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }}
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.name }}
path: |
${{ 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
${{ env.CARGO_TARGET_DIR }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
${{ env.CARGO_TARGET_DIR }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
if-no-files-found: error
release:
@@ -129,8 +151,8 @@ jobs:
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")"
for i in */*/$BUILD_PROFILE/$CARGO_BIN_NAME*; do
mv "$i" "../out/$(sed -E "s/([^/]+)\/[^/]+\/$BUILD_PROFILE\/($CARGO_BIN_NAME)/\2-\1/" <<< "$i")"
done
ls -R ../out
- name: Release
Generated
+1870 -1051
View File
File diff suppressed because it is too large Load Diff
+44 -35
View File
@@ -1,8 +1,8 @@
[package]
name = "objdiff"
version = "0.4.4"
version = "1.0.0"
edition = "2021"
rust-version = "1.65"
rust-version = "1.70"
authors = ["Luke Street <luke@street.dev>"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff"
@@ -13,55 +13,64 @@ A local diffing tool for decompilation projects.
publish = false
build = "build.rs"
[profile.release]
[profile.release-lto]
inherits = "release"
lto = "thin"
strip = "debuginfo"
[features]
default = []
default = ["wgpu", "wsl"]
wgpu = ["eframe/wgpu"]
wsl = []
[dependencies]
anyhow = "1.0.71"
byteorder = "1.4.3"
bytes = "1.4.0"
anyhow = "1.0.79"
byteorder = "1.5.0"
bytes = "1.5.0"
cfg-if = "1.0.0"
const_format = "0.2.31"
const_format = "0.2.32"
cwdemangle = "0.1.6"
dirs = "5.0.1"
eframe = { version = "0.22.0", features = ["persistence"] }
egui = "0.22.0"
egui_extras = "0.22.0"
flagset = "0.4.3"
globset = { version = "0.4.13", features = ["serde1"] }
log = "0.4.19"
memmap2 = "0.7.1"
notify = "6.0.1"
object = { version = "0.31.1", features = ["read_core", "std", "elf"], default-features = false }
png = "0.17.9"
ppc750cl = { git = "https://github.com/terorie/ppc750cl", rev = "9ae36eef34aa6d74e00972c7671f547a2acfd0aa" }
rabbitizer = "1.7.4"
rfd = { version = "0.11.4" } #, default-features = false, features = ['xdg-portal']
ron = "0.8.0"
semver = "1.0.17"
eframe = { version = "0.25.0", features = ["persistence"] }
egui = "0.25.0"
egui_extras = "0.25.0"
filetime = "0.2.23"
flagset = "0.4.4"
float-ord = "0.3.2"
font-kit = "0.12.0"
gimli = { version = "0.28.1", default-features = false, features = ["read-all"] }
globset = { version = "0.4.14", features = ["serde1"] }
log = "0.4.20"
memmap2 = "0.9.3"
notify = "6.1.1"
object = { version = "0.32.2", features = ["read_core", "std", "elf"], default-features = false }
png = "0.17.11"
pollster = "0.3.0"
ppc750cl = { git = "https://github.com/encounter/ppc750cl", rev = "4a2bbbc6f84dcb76255ab6f3595a8d4a0ce96618" }
rabbitizer = "1.8.1"
rfd = { version = "0.13.0" } #, default-features = false, features = ['xdg-portal']
ron = "0.8.1"
semver = "1.0.21"
serde = { version = "1", features = ["derive"] }
serde_json = "1.0.104"
serde_yaml = "0.9.25"
tempfile = "3.6.0"
thiserror = "1.0.41"
time = { version = "0.3.22", features = ["formatting", "local-offset"] }
toml = "0.7.6"
serde_json = "1.0.111"
serde_yaml = "0.9.30"
shell-escape = "0.1.5"
similar = "2.4.0"
tempfile = "3.9.0"
thiserror = "1.0.56"
time = { version = "0.3.31", features = ["formatting", "local-offset"] }
toml = "0.8.8"
twox-hash = "1.6.3"
# For Linux static binaries, use rustls
[target.'cfg(target_os = "linux")'.dependencies]
reqwest = { version = "0.11.18", default-features = false, features = ["blocking", "json", "rustls"] }
self_update = { version = "0.37.0", default-features = false, features = ["rustls"] }
reqwest = { version = "0.11.23", default-features = false, features = ["blocking", "json", "multipart", "rustls"] }
self_update = { version = "0.39.0", default-features = false, features = ["rustls"] }
# For all other platforms, use native TLS
[target.'cfg(not(target_os = "linux"))'.dependencies]
reqwest = "0.11.18"
self_update = "0.37.0"
reqwest = { version = "0.11.23", default-features = false, features = ["blocking", "json", "multipart", "default-tls"] }
self_update = "0.39.0"
[target.'cfg(windows)'.dependencies]
path-slash = "0.2.1"
@@ -83,5 +92,5 @@ console_error_panic_hook = "0.1.7"
tracing-wasm = "0.2"
[build-dependencies]
anyhow = "1.0.71"
vergen = { version = "8.2.4", features = ["build", "cargo", "git", "gitcl"] }
anyhow = "1.0.79"
vergen = { version = "8.3.1", features = ["build", "cargo", "git", "gitcl"] }
+19 -1
View File
@@ -3,7 +3,15 @@
[Build Status]: https://github.com/encounter/objdiff/actions/workflows/build.yaml/badge.svg
[actions]: https://github.com/encounter/objdiff/actions
A local diffing tool for decompilation projects.
A local diffing tool for decompilation projects. Inspired by [decomp.me](https://decomp.me) and [asm-differ](https://github.com/simonlindholm/asm-differ).
Features:
- Compare entire object files: functions and data.
- Built-in symbol demangling for C++.
- Automatic rebuild on source file changes.
- Project integration via [configuration file](#configuration).
- Search and filter all of a project's objects and quickly switch.
- Click to highlight all instances of values and registers.
Supports:
- PowerPC 750CL (GameCube & Wii)
@@ -118,7 +126,17 @@ If not specified, objdiff will use the default patterns listed above.
> `reverse_fn_order` _(optional)_: Displays function symbols in reversed order.
Used to support MWCC's `-inline deferred` option, which reverses the order of functions in the object file.
## Building
Install Rust via [rustup](https://rustup.rs).
```shell
$ git clone https://github.com/encounter/objdiff.git
$ cd objdiff
$ cargo run --release
# or, for wgpu backend (recommended on macOS)
$ cargo run --release --features wgpu
```
## License
+3 -8
View File
@@ -47,13 +47,7 @@ yanked = "warn"
notice = "warn"
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
ignore = [
"RUSTSEC-2023-0022",
"RUSTSEC-2023-0023",
"RUSTSEC-2023-0024",
"RUSTSEC-2023-0034",
"RUSTSEC-2023-0044",
]
ignore = []
# 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.
@@ -76,6 +70,7 @@ unlicensed = "deny"
allow = [
"MIT",
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"ISC",
"BSD-2-Clause",
"BSD-3-Clause",
@@ -159,7 +154,7 @@ registries = [
# 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"
multiple-versions = "allow"
# Lint level for when a crate version requirement is `*`
wildcards = "allow"
# The graph highlighting used when creating dotgraphs for crates
+170 -68
View File
@@ -1,29 +1,37 @@
use std::{
default::Default,
fs,
path::{Path, PathBuf},
rc::Rc,
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex, RwLock,
},
time::Duration,
};
use globset::{Glob, GlobSet, GlobSetBuilder};
use filetime::FileTime;
use globset::{Glob, GlobSet};
use notify::{RecursiveMode, Watcher};
use time::UtcOffset;
use crate::{
app_config::{deserialize_config, AppConfigVersion},
config::{
build_globset, load_project_config, ProjectObject, ProjectObjectNode, CONFIG_FILENAMES,
config::{build_globset, load_project_config, ProjectObject, ProjectObjectNode, ScratchConfig},
diff::DiffAlg,
jobs::{
objdiff::{start_build, ObjDiffConfig},
Job, JobQueue, JobResult, JobStatus,
},
jobs::{objdiff::start_build, Job, JobQueue, JobResult, JobStatus},
views::{
appearance::{appearance_window, Appearance},
config::{config_ui, project_window, ConfigViewState, DEFAULT_WATCH_PATTERNS},
config::{
config_ui, diff_options_window, project_window, ConfigViewState, CONFIG_DISABLED_TEXT,
DEFAULT_WATCH_PATTERNS,
},
data_diff::data_diff_ui,
debug::debug_window,
demangle::{demangle_window, DemangleViewState},
frame_history::FrameHistory,
function_diff::function_diff_ui,
jobs::jobs_ui,
symbol_diff::{symbol_diff_ui, DiffViewState, View},
@@ -36,9 +44,12 @@ pub struct ViewState {
pub config_state: ConfigViewState,
pub demangle_state: DemangleViewState,
pub diff_state: DiffViewState,
pub frame_history: FrameHistory,
pub show_appearance_config: bool,
pub show_demangle: bool,
pub show_project_config: bool,
pub show_diff_options: bool,
pub show_debug: bool,
}
/// The configuration for a single object file.
@@ -49,6 +60,13 @@ pub struct ObjectConfig {
pub base_path: Option<PathBuf>,
pub reverse_fn_order: Option<bool>,
pub complete: Option<bool>,
pub scratch: Option<ScratchConfig>,
}
#[derive(Clone, Eq, PartialEq)]
pub struct ProjectConfigInfo {
pub path: PathBuf,
pub timestamp: FileTime,
}
#[inline]
@@ -77,6 +95,8 @@ pub struct AppConfig {
pub base_obj_dir: Option<PathBuf>,
#[serde(default)]
pub selected_obj: Option<ObjectConfig>,
#[serde(default = "bool_true")]
pub build_base: bool,
#[serde(default)]
pub build_target: bool,
#[serde(default = "bool_true")]
@@ -87,6 +107,12 @@ pub struct AppConfig {
pub watch_patterns: Vec<Glob>,
#[serde(default)]
pub recent_projects: Vec<PathBuf>,
#[serde(default)]
pub code_alg: DiffAlg,
#[serde(default)]
pub data_alg: DiffAlg,
#[serde(default)]
pub relax_reloc_diffs: bool,
#[serde(skip)]
pub objects: Vec<ProjectObject>,
@@ -101,7 +127,11 @@ pub struct AppConfig {
#[serde(skip)]
pub queue_build: bool,
#[serde(skip)]
pub project_config_loaded: bool,
pub queue_reload: bool,
#[serde(skip)]
pub queue_scratch: bool,
#[serde(skip)]
pub project_config_info: Option<ProjectConfigInfo>,
}
impl Default for AppConfig {
@@ -114,18 +144,24 @@ impl Default for AppConfig {
target_obj_dir: None,
base_obj_dir: None,
selected_obj: None,
build_base: true,
build_target: false,
rebuild_on_changes: true,
auto_update_check: true,
watch_patterns: DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(),
recent_projects: vec![],
code_alg: Default::default(),
data_alg: Default::default(),
relax_reloc_diffs: false,
objects: vec![],
object_nodes: vec![],
watcher_change: false,
config_change: false,
obj_change: false,
queue_build: false,
project_config_loaded: false,
queue_reload: false,
queue_scratch: false,
project_config_info: None,
}
}
}
@@ -148,7 +184,7 @@ impl AppConfig {
self.config_change = true;
self.obj_change = true;
self.queue_build = false;
self.project_config_loaded = false;
self.project_config_info = None;
}
pub fn set_target_obj_dir(&mut self, path: PathBuf) {
@@ -180,7 +216,6 @@ pub struct App {
view_state: ViewState,
config: AppConfigRef,
modified: Arc<AtomicBool>,
config_modified: Arc<AtomicBool>,
watcher: Option<notify::RecommendedWatcher>,
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
should_relaunch: bool,
@@ -196,9 +231,6 @@ impl App {
utc_offset: UtcOffset,
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
) -> Self {
// This is also where you can customized the look at feel of egui using
// `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`.
// Load previous app state (if any).
// Note that you must enable the `persistence` feature for this to work.
let mut app = Self::default();
@@ -210,18 +242,23 @@ impl App {
if config.project_dir.is_some() {
config.config_change = true;
config.watcher_change = true;
app.modified.store(true, Ordering::Relaxed);
}
if config.selected_obj.is_some() {
config.queue_build = true;
}
app.view_state.config_state.queue_check_update = config.auto_update_check;
app.config = Arc::new(RwLock::new(config));
}
}
app.appearance.init_fonts(&cc.egui_ctx);
app.appearance.utc_offset = utc_offset;
app.relaunch_path = relaunch_path;
app
}
fn pre_update(&mut self) {
fn pre_update(&mut self, ctx: &egui::Context) {
self.appearance.pre_update(ctx);
let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state;
let mut results = vec![];
@@ -231,7 +268,7 @@ impl App {
log::info!("Job {} finished", job.id);
match result {
JobResult::None => {
if let Some(err) = &job.status.read().unwrap().error {
if let Some(err) = &job.context.status.read().unwrap().error {
log::error!("{:?}", err);
}
}
@@ -252,12 +289,12 @@ impl App {
} else {
anyhow::Error::msg("Thread panicked")
};
let result = job.status.write();
let result = job.context.status.write();
if let Ok(mut guard) = result {
guard.error = Some(err);
} else {
drop(result);
job.status = Arc::new(RwLock::new(JobStatus {
job.context.status = Arc::new(RwLock::new(JobStatus {
title: "Error".to_string(),
progress_percent: 0.0,
progress_items: None,
@@ -272,22 +309,26 @@ impl App {
jobs.clear_finished();
diff_state.pre_update(jobs, &self.config);
config_state.pre_update(jobs);
config_state.pre_update(jobs, &self.config);
debug_assert!(jobs.results.is_empty());
}
fn post_update(&mut self) {
fn post_update(&mut self, ctx: &egui::Context) {
self.appearance.post_update(ctx);
let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state;
config_state.post_update(jobs, &self.config);
diff_state.post_update(jobs, &self.config);
config_state.post_update(ctx, jobs, &self.config);
diff_state.post_update(ctx, jobs, &self.config);
let Ok(mut config) = self.config.write() else {
return;
};
let config = &mut *config;
if self.config_modified.swap(false, Ordering::Relaxed) {
config.config_change = true;
if let Some(info) = &config.project_config_info {
if file_modified(&info.path, info.timestamp) {
config.config_change = true;
}
}
if config.config_change {
@@ -305,21 +346,14 @@ impl App {
drop(self.watcher.take());
if let Some(project_dir) = &config.project_dir {
if !config.watch_patterns.is_empty() {
match build_globset(&config.watch_patterns)
.map_err(anyhow::Error::new)
.and_then(|globset| {
create_watcher(
self.modified.clone(),
self.config_modified.clone(),
project_dir,
globset,
)
match build_globset(&config.watch_patterns).map_err(anyhow::Error::new).and_then(
|globset| {
create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset)
.map_err(anyhow::Error::new)
}) {
Ok(watcher) => self.watcher = Some(watcher),
Err(e) => log::error!("Failed to create watcher: {e}"),
}
},
) {
Ok(watcher) => self.watcher = Some(watcher),
Err(e) => log::error!("Failed to create watcher: {e}"),
}
config.watcher_change = false;
}
@@ -337,11 +371,32 @@ impl App {
config.queue_build = true;
}
if let Some(result) = &diff_state.build {
if let Some(obj) = &result.first_obj {
if file_modified(&obj.path, obj.timestamp) {
config.queue_reload = true;
}
}
if let Some(obj) = &result.second_obj {
if file_modified(&obj.path, obj.timestamp) {
config.queue_reload = true;
}
}
}
// Don't clear `queue_build` if a build is running. A file may have been modified during
// the build, so we'll start another build after the current one finishes.
if config.queue_build && config.selected_obj.is_some() && !jobs.is_running(Job::ObjDiff) {
jobs.push(start_build(self.config.clone()));
jobs.push(start_build(ctx, ObjDiffConfig::from_config(config)));
config.queue_build = false;
config.queue_reload = false;
} else if config.queue_reload && !jobs.is_running(Job::ObjDiff) {
let mut diff_config = ObjDiffConfig::from_config(config);
// Don't build, just reload the current files
diff_config.build_base = false;
diff_config.build_target = false;
jobs.push(start_build(ctx, diff_config));
config.queue_reload = false;
}
}
}
@@ -351,37 +406,53 @@ impl eframe::App for App {
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
if self.should_relaunch {
frame.close();
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
return;
}
self.pre_update();
self.pre_update(ctx);
let Self { config, appearance, view_state, .. } = self;
ctx.set_style(appearance.apply(ctx.style().as_ref()));
let ViewState {
jobs,
show_appearance_config,
demangle_state,
show_demangle,
diff_state,
config_state,
demangle_state,
diff_state,
frame_history,
show_appearance_config,
show_demangle,
show_project_config,
show_diff_options,
show_debug,
} = view_state;
frame_history.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
ui.menu_button("File", |ui| {
#[cfg(debug_assertions)]
if ui.button("Debug…").clicked() {
*show_debug = !*show_debug;
ui.close_menu();
}
if ui.button("Project…").clicked() {
*show_project_config = !*show_project_config;
ui.close_menu();
}
let recent_projects = if let Ok(guard) = config.read() {
guard.recent_projects.clone()
} else {
vec![]
};
if recent_projects.is_empty() {
ui.add_enabled(false, egui::Button::new("Recent Projects…"));
ui.add_enabled(false, egui::Button::new("Recent projects…"));
} else {
ui.menu_button("Recent Projects…", |ui| {
if ui.button("Clear").clicked() {
config.write().unwrap().recent_projects.clear();
};
ui.separator();
for path in recent_projects {
if ui.button(format!("{}", path.display())).clicked() {
config.write().unwrap().set_project_dir(path);
@@ -395,7 +466,7 @@ impl eframe::App for App {
ui.close_menu();
}
if ui.button("Quit").clicked() {
frame.close();
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
});
ui.menu_button("Tools", |ui| {
@@ -404,6 +475,40 @@ impl eframe::App for App {
ui.close_menu();
}
});
ui.menu_button("Diff Options", |ui| {
if ui.button("Algorithm…").clicked() {
*show_diff_options = !*show_diff_options;
ui.close_menu();
}
let mut config = config.write().unwrap();
let response = ui
.checkbox(&mut config.rebuild_on_changes, "Rebuild on changes")
.on_hover_text("Automatically re-run the build & diff when files change.");
if response.changed() {
config.watcher_change = true;
};
ui.add_enabled(
!diff_state.symbol_state.disable_reverse_fn_order,
egui::Checkbox::new(
&mut diff_state.symbol_state.reverse_fn_order,
"Reverse function order (-inline deferred)",
),
)
.on_disabled_hover_text(CONFIG_DISABLED_TEXT);
ui.checkbox(
&mut diff_state.symbol_state.show_hidden_symbols,
"Show hidden symbols",
);
if ui
.checkbox(&mut config.relax_reloc_diffs, "Relax relocation diffs")
.on_hover_text(
"Ignores differences in relocation targets. (Address, name, etc)",
)
.changed()
{
config.queue_reload = true;
}
});
});
});
@@ -432,16 +537,10 @@ impl eframe::App for App {
project_window(ctx, config, show_project_config, config_state, appearance);
appearance_window(ctx, show_appearance_config, appearance);
demangle_window(ctx, show_demangle, demangle_state, appearance);
diff_options_window(ctx, config, show_diff_options, appearance);
debug_window(ctx, show_debug, frame_history, appearance);
self.post_update();
// Windows + request_repaint_after breaks dialogs:
// https://github.com/emilk/egui/issues/2003
if cfg!(windows) || self.view_state.jobs.any_running() {
ctx.request_repaint();
} else {
ctx.request_repaint_after(Duration::from_millis(100));
}
self.post_update(ctx);
}
/// Called by the frame work to save state before shutdown.
@@ -454,17 +553,11 @@ impl eframe::App for App {
}
fn create_watcher(
ctx: egui::Context,
modified: Arc<AtomicBool>,
config_modified: Arc<AtomicBool>,
project_dir: &Path,
patterns: GlobSet,
) -> notify::Result<notify::RecommendedWatcher> {
let mut config_patterns = GlobSetBuilder::new();
for filename in CONFIG_FILENAMES {
config_patterns.add(Glob::new(filename).unwrap());
}
let config_patterns = config_patterns.build().unwrap();
let base_dir = project_dir.to_owned();
let mut watcher =
notify::recommended_watcher(move |res: notify::Result<notify::Event>| match res {
@@ -479,10 +572,10 @@ fn create_watcher(
let Ok(path) = path.strip_prefix(&base_dir) else {
continue;
};
if config_patterns.is_match(path) {
config_modified.store(true, Ordering::Relaxed);
} else if patterns.is_match(path) {
if patterns.is_match(path) {
log::info!("File modified: {}", path.display());
modified.store(true, Ordering::Relaxed);
ctx.request_repaint();
}
}
}
@@ -492,3 +585,12 @@ fn create_watcher(
watcher.watch(project_dir, RecursiveMode::Recursive)?;
Ok(watcher)
}
#[inline]
fn file_modified(path: &Path, last_ts: FileTime) -> bool {
if let Ok(metadata) = fs::metadata(path) {
FileTime::from_last_modification_time(&metadata) != last_ts
} else {
false
}
}
+1
View File
@@ -60,6 +60,7 @@ impl ObjectConfigV0 {
base_path: Some(self.base_path),
reverse_fn_order: self.reverse_fn_order,
complete: None,
scratch: None,
}
}
}
+64 -21
View File
@@ -1,34 +1,71 @@
use std::{
fs::File,
io::Read,
path::{Component, Path, PathBuf},
};
use anyhow::{bail, Context, Result};
use anyhow::{bail, Result};
use filetime::FileTime;
use globset::{Glob, GlobSet, GlobSetBuilder};
use crate::{app::AppConfig, views::config::DEFAULT_WATCH_PATTERNS};
use crate::{
app::{AppConfig, ProjectConfigInfo},
views::config::DEFAULT_WATCH_PATTERNS,
};
#[inline]
fn bool_true() -> bool { true }
#[derive(Default, Clone, serde::Deserialize)]
#[serde(default)]
pub struct ProjectConfig {
#[serde(default)]
pub min_version: Option<String>,
#[serde(default)]
pub custom_make: Option<String>,
#[serde(default)]
pub target_dir: Option<PathBuf>,
#[serde(default)]
pub base_dir: Option<PathBuf>,
#[serde(default = "bool_true")]
pub build_base: bool,
#[serde(default)]
pub build_target: bool,
#[serde(default)]
pub watch_patterns: Option<Vec<Glob>>,
#[serde(alias = "units")]
#[serde(default, alias = "units")]
pub objects: Vec<ProjectObject>,
}
#[derive(Default, Clone, serde::Deserialize)]
pub struct ProjectObject {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub path: Option<PathBuf>,
#[serde(default)]
pub target_path: Option<PathBuf>,
#[serde(default)]
pub base_path: Option<PathBuf>,
#[serde(default)]
pub reverse_fn_order: Option<bool>,
#[serde(default)]
pub complete: Option<bool>,
#[serde(default)]
pub scratch: Option<ScratchConfig>,
}
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ScratchConfig {
#[serde(default)]
pub platform: Option<String>,
#[serde(default)]
pub compiler: Option<String>,
#[serde(default)]
pub c_flags: Option<String>,
#[serde(default)]
pub ctx_path: Option<PathBuf>,
#[serde(default)]
pub build_ctx: bool,
}
impl ProjectObject {
@@ -45,7 +82,7 @@ impl ProjectObject {
#[derive(Clone)]
pub enum ProjectObjectNode {
File(String, ProjectObject),
File(String, Box<ProjectObject>),
Dir(String, Vec<ProjectObjectNode>),
}
@@ -93,7 +130,7 @@ fn build_nodes(
}
}
}
let mut object = object.clone();
let mut object = Box::new(object.clone());
if let (Some(target_obj_dir), Some(path), None) =
(target_obj_dir, &object.path, &object.target_path)
{
@@ -120,7 +157,7 @@ pub fn load_project_config(config: &mut AppConfig) -> Result<()> {
let Some(project_dir) = &config.project_dir else {
return Ok(());
};
if let Some(result) = try_project_config(project_dir) {
if let Some((result, info)) = try_project_config(project_dir) {
let project_config = result?;
if let Some(min_version) = &project_config.min_version {
let version_str = env!("CARGO_PKG_VERSION");
@@ -133,6 +170,7 @@ pub fn load_project_config(config: &mut AppConfig) -> Result<()> {
config.custom_make = project_config.custom_make;
config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p));
config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p));
config.build_base = project_config.build_base;
config.build_target = project_config.build_target;
config.watch_patterns = project_config.watch_patterns.unwrap_or_else(|| {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
@@ -141,34 +179,39 @@ pub fn load_project_config(config: &mut AppConfig) -> Result<()> {
config.objects = project_config.objects;
config.object_nodes =
build_nodes(&config.objects, project_dir, &config.target_obj_dir, &config.base_obj_dir);
config.project_config_loaded = true;
config.project_config_info = Some(info);
}
Ok(())
}
fn try_project_config(dir: &Path) -> Option<Result<ProjectConfig>> {
fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
for filename in CONFIG_FILENAMES.iter() {
let config_path = dir.join(filename);
if config_path.is_file() {
return match filename.contains("json") {
true => Some(read_json_config(&config_path)),
false => Some(read_yml_config(&config_path)),
let Ok(mut file) = File::open(&config_path) else {
continue;
};
let metadata = file.metadata();
if let Ok(metadata) = metadata {
if !metadata.is_file() {
continue;
}
let ts = FileTime::from_last_modification_time(&metadata);
let config = match filename.contains("json") {
true => read_json_config(&mut file),
false => read_yml_config(&mut file),
};
return Some((config, ProjectConfigInfo { path: config_path, timestamp: ts }));
}
}
None
}
fn read_yml_config(config_path: &Path) -> Result<ProjectConfig> {
let mut reader = File::open(config_path)
.with_context(|| format!("Failed to open config file '{}'", config_path.display()))?;
Ok(serde_yaml::from_reader(&mut reader)?)
fn read_yml_config<R: Read>(reader: &mut R) -> Result<ProjectConfig> {
Ok(serde_yaml::from_reader(reader)?)
}
fn read_json_config(config_path: &Path) -> Result<ProjectConfig> {
let mut reader = File::open(config_path)
.with_context(|| format!("Failed to open config file '{}'", config_path.display()))?;
Ok(serde_json::from_reader(&mut reader)?)
fn read_json_config<R: Read>(reader: &mut R) -> Result<ProjectConfig> {
Ok(serde_json::from_reader(reader)?)
}
pub fn build_globset(vec: &[Glob]) -> std::result::Result<GlobSet, globset::Error> {
-735
View File
File diff suppressed because it is too large Load Diff
+489
View File
@@ -0,0 +1,489 @@
use std::{
cmp::max,
collections::BTreeMap,
time::{Duration, Instant},
};
use anyhow::Result;
use similar::{capture_diff_slices_deadline, Algorithm};
use crate::{
diff::{
editops::{editops_find, LevEditType},
DiffAlg, DiffObjConfig, ProcessCodeResult,
},
obj::{
mips, ppc, ObjArchitecture, ObjInfo, ObjInsArg, ObjInsArgDiff, ObjInsBranchFrom,
ObjInsBranchTo, ObjInsDiff, ObjInsDiffKind, ObjReloc, ObjSymbol, ObjSymbolFlags,
},
};
pub fn no_diff_code(
arch: ObjArchitecture,
data: &[u8],
symbol: &mut ObjSymbol,
relocs: &[ObjReloc],
line_info: &Option<BTreeMap<u64, u64>>,
) -> Result<()> {
let code =
&data[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
let out = match arch {
ObjArchitecture::PowerPc => ppc::process_code(code, symbol.address, relocs, line_info)?,
ObjArchitecture::Mips => mips::process_code(
code,
symbol.address,
symbol.address + symbol.size,
relocs,
line_info,
)?,
};
let mut diff = Vec::<ObjInsDiff>::new();
for i in out.insts {
diff.push(ObjInsDiff { ins: Some(i), kind: ObjInsDiffKind::None, ..Default::default() });
}
resolve_branches(&mut diff);
symbol.instructions = diff;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn diff_code(
config: &DiffObjConfig,
arch: ObjArchitecture,
left_data: &[u8],
right_data: &[u8],
left_symbol: &mut ObjSymbol,
right_symbol: &mut ObjSymbol,
left_relocs: &[ObjReloc],
right_relocs: &[ObjReloc],
left_line_info: &Option<BTreeMap<u64, u64>>,
right_line_info: &Option<BTreeMap<u64, u64>>,
) -> Result<()> {
let left_code = &left_data[left_symbol.section_address as usize
..(left_symbol.section_address + left_symbol.size) as usize];
let right_code = &right_data[right_symbol.section_address as usize
..(right_symbol.section_address + right_symbol.size) as usize];
let (left_out, right_out) = match arch {
ObjArchitecture::PowerPc => (
ppc::process_code(left_code, left_symbol.address, left_relocs, left_line_info)?,
ppc::process_code(right_code, right_symbol.address, right_relocs, right_line_info)?,
),
ObjArchitecture::Mips => (
mips::process_code(
left_code,
left_symbol.address,
left_symbol.address + left_symbol.size,
left_relocs,
left_line_info,
)?,
mips::process_code(
right_code,
right_symbol.address,
left_symbol.address + left_symbol.size,
right_relocs,
right_line_info,
)?,
),
};
let mut left_diff = Vec::<ObjInsDiff>::new();
let mut right_diff = Vec::<ObjInsDiff>::new();
match config.code_alg {
DiffAlg::Levenshtein => {
diff_instructions_lev(
&mut left_diff,
&mut right_diff,
left_symbol,
right_symbol,
&left_out,
&right_out,
)?;
}
DiffAlg::Lcs => {
diff_instructions_similar(
Algorithm::Lcs,
&mut left_diff,
&mut right_diff,
&left_out,
&right_out,
)?;
}
DiffAlg::Myers => {
diff_instructions_similar(
Algorithm::Myers,
&mut left_diff,
&mut right_diff,
&left_out,
&right_out,
)?;
}
DiffAlg::Patience => {
diff_instructions_similar(
Algorithm::Patience,
&mut left_diff,
&mut right_diff,
&left_out,
&right_out,
)?;
}
}
resolve_branches(&mut left_diff);
resolve_branches(&mut right_diff);
let mut diff_state = InsDiffState::default();
for (left, right) in left_diff.iter_mut().zip(right_diff.iter_mut()) {
let result = compare_ins(config, left, right, &mut diff_state)?;
left.kind = result.kind;
right.kind = result.kind;
left.arg_diff = result.left_args_diff;
right.arg_diff = result.right_args_diff;
}
let total = left_out.insts.len();
let percent = if diff_state.diff_count >= total {
0.0
} else {
((total - diff_state.diff_count) as f32 / total as f32) * 100.0
};
left_symbol.match_percent = Some(percent);
right_symbol.match_percent = Some(percent);
left_symbol.instructions = left_diff;
right_symbol.instructions = right_diff;
Ok(())
}
fn diff_instructions_similar(
alg: Algorithm,
left_diff: &mut Vec<ObjInsDiff>,
right_diff: &mut Vec<ObjInsDiff>,
left_code: &ProcessCodeResult,
right_code: &ProcessCodeResult,
) -> Result<()> {
let deadline = Instant::now() + Duration::from_secs(5);
let ops = capture_diff_slices_deadline(alg, &left_code.ops, &right_code.ops, Some(deadline));
if ops.is_empty() {
left_diff.extend(
left_code
.insts
.iter()
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
);
right_diff.extend(
right_code
.insts
.iter()
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
);
return Ok(());
}
for op in ops {
let (_tag, left_range, right_range) = op.as_tag_tuple();
let len = max(left_range.len(), right_range.len());
left_diff.extend(
left_code.insts[left_range.clone()]
.iter()
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
);
right_diff.extend(
right_code.insts[right_range.clone()]
.iter()
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
);
if left_range.len() < len {
left_diff.extend((left_range.len()..len).map(|_| ObjInsDiff::default()));
}
if right_range.len() < len {
right_diff.extend((right_range.len()..len).map(|_| ObjInsDiff::default()));
}
}
Ok(())
}
fn diff_instructions_lev(
left_diff: &mut Vec<ObjInsDiff>,
right_diff: &mut Vec<ObjInsDiff>,
left_symbol: &ObjSymbol,
right_symbol: &ObjSymbol,
left_code: &ProcessCodeResult,
right_code: &ProcessCodeResult,
) -> Result<()> {
let edit_ops = editops_find(&left_code.ops, &right_code.ops);
let mut op_iter = edit_ops.iter();
let mut left_iter = left_code.insts.iter();
let mut right_iter = right_code.insts.iter();
let mut cur_op = op_iter.next();
let mut cur_left = left_iter.next();
let mut cur_right = right_iter.next();
while let Some(op) = cur_op {
let left_addr = op.first_start as u32 * 4;
let right_addr = op.second_start as u32 * 4;
while let (Some(left), Some(right)) = (cur_left, cur_right) {
if (left.address - left_symbol.address as u32) < left_addr {
left_diff.push(ObjInsDiff { ins: Some(left.clone()), ..ObjInsDiff::default() });
right_diff.push(ObjInsDiff { ins: Some(right.clone()), ..ObjInsDiff::default() });
} else {
break;
}
cur_left = left_iter.next();
cur_right = right_iter.next();
}
if let (Some(left), Some(right)) = (cur_left, cur_right) {
if (left.address - left_symbol.address as u32) != left_addr {
return Err(anyhow::Error::msg("Instruction address mismatch (left)"));
}
if (right.address - right_symbol.address as u32) != right_addr {
return Err(anyhow::Error::msg("Instruction address mismatch (right)"));
}
match op.op_type {
LevEditType::Replace => {
left_diff.push(ObjInsDiff { ins: Some(left.clone()), ..ObjInsDiff::default() });
right_diff
.push(ObjInsDiff { ins: Some(right.clone()), ..ObjInsDiff::default() });
cur_left = left_iter.next();
cur_right = right_iter.next();
}
LevEditType::Insert => {
left_diff.push(ObjInsDiff::default());
right_diff
.push(ObjInsDiff { ins: Some(right.clone()), ..ObjInsDiff::default() });
cur_right = right_iter.next();
}
LevEditType::Delete => {
left_diff.push(ObjInsDiff { ins: Some(left.clone()), ..ObjInsDiff::default() });
right_diff.push(ObjInsDiff::default());
cur_left = left_iter.next();
}
}
} else {
break;
}
cur_op = op_iter.next();
}
// Finalize
while cur_left.is_some() || cur_right.is_some() {
left_diff.push(ObjInsDiff { ins: cur_left.cloned(), ..ObjInsDiff::default() });
right_diff.push(ObjInsDiff { ins: cur_right.cloned(), ..ObjInsDiff::default() });
cur_left = left_iter.next();
cur_right = right_iter.next();
}
Ok(())
}
fn resolve_branches(vec: &mut [ObjInsDiff]) {
let mut branch_idx = 0usize;
// Map addresses to indices
let mut addr_map = BTreeMap::<u32, usize>::new();
for (i, ins_diff) in vec.iter().enumerate() {
if let Some(ins) = &ins_diff.ins {
addr_map.insert(ins.address, i);
}
}
// Generate branches
let mut branches = BTreeMap::<usize, ObjInsBranchFrom>::new();
for (i, ins_diff) in vec.iter_mut().enumerate() {
if let Some(ins) = &ins_diff.ins {
// if ins.ins.is_blr() || ins.reloc.is_some() {
// continue;
// }
if let Some(ins_idx) = ins
.args
.iter()
.find_map(|a| if let ObjInsArg::BranchOffset(offs) = a { Some(offs) } else { None })
.and_then(|offs| addr_map.get(&((ins.address as i32 + offs) as u32)))
{
if let Some(branch) = branches.get_mut(ins_idx) {
ins_diff.branch_to =
Some(ObjInsBranchTo { ins_idx: *ins_idx, branch_idx: branch.branch_idx });
branch.ins_idx.push(i);
} else {
ins_diff.branch_to = Some(ObjInsBranchTo { ins_idx: *ins_idx, branch_idx });
branches.insert(*ins_idx, ObjInsBranchFrom { ins_idx: vec![i], branch_idx });
branch_idx += 1;
}
}
}
}
// Store branch from
for (i, branch) in branches {
vec[i].branch_from = Some(branch);
}
}
fn address_eq(left: &ObjSymbol, right: &ObjSymbol) -> bool {
left.address as i64 + left.addend == right.address as i64 + right.addend
}
fn reloc_eq(
config: &DiffObjConfig,
left_reloc: Option<&ObjReloc>,
right_reloc: Option<&ObjReloc>,
) -> bool {
let (Some(left), Some(right)) = (left_reloc, right_reloc) else {
return false;
};
if left.kind != right.kind {
return false;
}
if config.relax_reloc_diffs {
return true;
}
let name_matches = left.target.name == right.target.name;
match (&left.target_section, &right.target_section) {
(Some(sl), Some(sr)) => {
// Match if section and name or address match
sl == sr && (name_matches || address_eq(&left.target, &right.target))
}
(Some(_), None) => false,
(None, Some(_)) => {
// Match if possibly stripped weak symbol
name_matches && right.target.flags.0.contains(ObjSymbolFlags::Weak)
}
(None, None) => name_matches,
}
}
fn arg_eq(
config: &DiffObjConfig,
left: &ObjInsArg,
right: &ObjInsArg,
left_diff: &ObjInsDiff,
right_diff: &ObjInsDiff,
) -> bool {
return match left {
ObjInsArg::PpcArg(l) => match right {
ObjInsArg::PpcArg(r) => format!("{l}") == format!("{r}"),
_ => false,
},
ObjInsArg::Reloc => {
matches!(right, ObjInsArg::Reloc)
&& reloc_eq(
config,
left_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
)
}
ObjInsArg::RelocWithBase => {
matches!(right, ObjInsArg::RelocWithBase)
&& reloc_eq(
config,
left_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
)
}
ObjInsArg::MipsArg(ls) | ObjInsArg::MipsArgWithBase(ls) => {
matches!(right, ObjInsArg::MipsArg(rs) | ObjInsArg::MipsArgWithBase(rs) if ls == rs)
}
ObjInsArg::BranchOffset(_) => {
// Compare dest instruction idx after diffing
left_diff.branch_to.as_ref().map(|b| b.ins_idx)
== right_diff.branch_to.as_ref().map(|b| b.ins_idx)
}
};
}
#[derive(Default)]
struct InsDiffState {
diff_count: usize,
left_arg_idx: usize,
right_arg_idx: usize,
left_args_idx: BTreeMap<String, usize>,
right_args_idx: BTreeMap<String, usize>,
}
#[derive(Default)]
struct InsDiffResult {
kind: ObjInsDiffKind,
left_args_diff: Vec<Option<ObjInsArgDiff>>,
right_args_diff: Vec<Option<ObjInsArgDiff>>,
}
fn compare_ins(
config: &DiffObjConfig,
left: &ObjInsDiff,
right: &ObjInsDiff,
state: &mut InsDiffState,
) -> Result<InsDiffResult> {
let mut result = InsDiffResult::default();
if let (Some(left_ins), Some(right_ins)) = (&left.ins, &right.ins) {
if left_ins.args.len() != right_ins.args.len() || left_ins.op != right_ins.op {
// Totally different op
result.kind = ObjInsDiffKind::Replace;
state.diff_count += 1;
return Ok(result);
}
if left_ins.mnemonic != right_ins.mnemonic {
// Same op but different mnemonic, still cmp args
result.kind = ObjInsDiffKind::OpMismatch;
state.diff_count += 1;
}
for (a, b) in left_ins.args.iter().zip(&right_ins.args) {
if arg_eq(config, a, b, left, right) {
result.left_args_diff.push(None);
result.right_args_diff.push(None);
} else {
if result.kind == ObjInsDiffKind::None {
result.kind = ObjInsDiffKind::ArgMismatch;
state.diff_count += 1;
}
let a_str = match a {
ObjInsArg::PpcArg(arg) => format!("{arg}"),
ObjInsArg::Reloc | ObjInsArg::RelocWithBase => String::new(),
ObjInsArg::MipsArg(str) | ObjInsArg::MipsArgWithBase(str) => str.clone(),
ObjInsArg::BranchOffset(arg) => format!("{arg}"),
};
let a_diff = if let Some(idx) = state.left_args_idx.get(&a_str) {
ObjInsArgDiff { idx: *idx }
} else {
let idx = state.left_arg_idx;
state.left_args_idx.insert(a_str, idx);
state.left_arg_idx += 1;
ObjInsArgDiff { idx }
};
let b_str = match b {
ObjInsArg::PpcArg(arg) => format!("{arg}"),
ObjInsArg::Reloc | ObjInsArg::RelocWithBase => String::new(),
ObjInsArg::MipsArg(str) | ObjInsArg::MipsArgWithBase(str) => str.clone(),
ObjInsArg::BranchOffset(arg) => format!("{arg}"),
};
let b_diff = if let Some(idx) = state.right_args_idx.get(&b_str) {
ObjInsArgDiff { idx: *idx }
} else {
let idx = state.right_arg_idx;
state.right_args_idx.insert(b_str, idx);
state.right_arg_idx += 1;
ObjInsArgDiff { idx }
};
result.left_args_diff.push(Some(a_diff));
result.right_args_diff.push(Some(b_diff));
}
}
} else if left.ins.is_some() {
result.kind = ObjInsDiffKind::Delete;
state.diff_count += 1;
} else {
result.kind = ObjInsDiffKind::Insert;
state.diff_count += 1;
}
Ok(result)
}
pub fn find_section_and_symbol(obj: &ObjInfo, name: &str) -> Option<(usize, usize)> {
for (section_idx, section) in obj.sections.iter().enumerate() {
let symbol_idx = match section.symbols.iter().position(|symbol| symbol.name == name) {
Some(symbol_idx) => symbol_idx,
None => continue,
};
return Some((section_idx, symbol_idx));
}
None
}
+406
View File
@@ -0,0 +1,406 @@
use std::{
cmp::{max, min, Ordering},
mem::take,
time::{Duration, Instant},
};
use anyhow::{bail, Result};
use similar::{capture_diff_slices_deadline, Algorithm};
use crate::{
diff::{
editops::{editops_find, LevEditType},
DiffAlg,
},
obj::{ObjDataDiff, ObjDataDiffKind, ObjSection, ObjSymbol},
};
pub fn diff_data(alg: DiffAlg, left: &mut ObjSection, right: &mut ObjSection) -> Result<()> {
match alg {
DiffAlg::Levenshtein => diff_data_lev(left, right),
DiffAlg::Lcs => diff_data_similar(Algorithm::Lcs, left, right),
DiffAlg::Myers => diff_data_similar(Algorithm::Myers, left, right),
DiffAlg::Patience => diff_data_similar(Algorithm::Patience, left, right),
}
}
pub fn diff_bss_symbols(
left_symbols: &mut [ObjSymbol],
right_symbols: &mut [ObjSymbol],
) -> Result<()> {
for left_symbol in left_symbols {
if let Some(right_symbol) = right_symbols.iter_mut().find(|s| s.name == left_symbol.name) {
left_symbol.diff_symbol = Some(right_symbol.name.clone());
right_symbol.diff_symbol = Some(left_symbol.name.clone());
let percent = if left_symbol.size == right_symbol.size { 100.0 } else { 50.0 };
left_symbol.match_percent = Some(percent);
right_symbol.match_percent = Some(percent);
}
}
Ok(())
}
// WIP diff-by-symbol
#[allow(dead_code)]
pub fn diff_data_symbols(left: &mut ObjSection, right: &mut ObjSection) -> Result<()> {
let mut left_ops = Vec::<u32>::with_capacity(left.symbols.len());
let mut right_ops = Vec::<u32>::with_capacity(right.symbols.len());
for left_symbol in &left.symbols {
let data = &left.data
[left_symbol.address as usize..(left_symbol.address + left_symbol.size) as usize];
let hash = twox_hash::xxh3::hash64(data);
left_ops.push(hash as u32);
}
for symbol in &right.symbols {
let data = &right.data[symbol.address as usize..(symbol.address + symbol.size) as usize];
let hash = twox_hash::xxh3::hash64(data);
right_ops.push(hash as u32);
}
let edit_ops = editops_find(&left_ops, &right_ops);
if edit_ops.is_empty() && !left.data.is_empty() {
let mut left_iter = left.symbols.iter_mut();
let mut right_iter = right.symbols.iter_mut();
loop {
let (left_symbol, right_symbol) = match (left_iter.next(), right_iter.next()) {
(Some(l), Some(r)) => (l, r),
(None, None) => break,
_ => return Err(anyhow::Error::msg("L/R mismatch in diff_data_symbols")),
};
let left_data = &left.data
[left_symbol.address as usize..(left_symbol.address + left_symbol.size) as usize];
let right_data = &right.data[right_symbol.address as usize
..(right_symbol.address + right_symbol.size) as usize];
left.data_diff.push(ObjDataDiff {
data: left_data.to_vec(),
kind: ObjDataDiffKind::None,
len: left_symbol.size as usize,
symbol: left_symbol.name.clone(),
});
right.data_diff.push(ObjDataDiff {
data: right_data.to_vec(),
kind: ObjDataDiffKind::None,
len: right_symbol.size as usize,
symbol: right_symbol.name.clone(),
});
left_symbol.diff_symbol = Some(right_symbol.name.clone());
left_symbol.match_percent = Some(100.0);
right_symbol.diff_symbol = Some(left_symbol.name.clone());
right_symbol.match_percent = Some(100.0);
}
return Ok(());
}
Ok(())
}
pub fn diff_data_similar(
alg: Algorithm,
left: &mut ObjSection,
right: &mut ObjSection,
) -> Result<()> {
let deadline = Instant::now() + Duration::from_secs(5);
let ops = capture_diff_slices_deadline(alg, &left.data, &right.data, Some(deadline));
let mut left_diff = Vec::<ObjDataDiff>::new();
let mut right_diff = Vec::<ObjDataDiff>::new();
for op in ops {
let (tag, left_range, right_range) = op.as_tag_tuple();
let left_len = left_range.len();
let right_len = right_range.len();
let mut len = max(left_len, right_len);
let kind = match tag {
similar::DiffTag::Equal => ObjDataDiffKind::None,
similar::DiffTag::Delete => ObjDataDiffKind::Delete,
similar::DiffTag::Insert => ObjDataDiffKind::Insert,
similar::DiffTag::Replace => {
// Ensure replacements are equal length
len = min(left_len, right_len);
ObjDataDiffKind::Replace
}
};
let left_data = &left.data[left_range];
let right_data = &right.data[right_range];
left_diff.push(ObjDataDiff {
data: left_data[..min(len, left_data.len())].to_vec(),
kind,
len,
..Default::default()
});
right_diff.push(ObjDataDiff {
data: right_data[..min(len, right_data.len())].to_vec(),
kind,
len,
..Default::default()
});
if kind == ObjDataDiffKind::Replace {
match left_len.cmp(&right_len) {
Ordering::Less => {
let len = right_len - left_len;
left_diff.push(ObjDataDiff {
data: vec![],
kind: ObjDataDiffKind::Insert,
len,
..Default::default()
});
right_diff.push(ObjDataDiff {
data: right_data[left_len..right_len].to_vec(),
kind: ObjDataDiffKind::Insert,
len,
..Default::default()
});
}
Ordering::Greater => {
let len = left_len - right_len;
left_diff.push(ObjDataDiff {
data: left_data[right_len..left_len].to_vec(),
kind: ObjDataDiffKind::Delete,
len,
..Default::default()
});
right_diff.push(ObjDataDiff {
data: vec![],
kind: ObjDataDiffKind::Delete,
len,
..Default::default()
});
}
Ordering::Equal => {}
}
}
}
left.data_diff = left_diff;
right.data_diff = right_diff;
Ok(())
}
pub fn diff_data_lev(left: &mut ObjSection, right: &mut ObjSection) -> Result<()> {
let matrix_size = (left.data.len() as u64).saturating_mul(right.data.len() as u64);
if matrix_size > 1_000_000_000 {
bail!(
"Data section {} too large for Levenshtein diff ({} * {} = {})",
left.name,
left.data.len(),
right.data.len(),
matrix_size
);
}
let edit_ops = editops_find(&left.data, &right.data);
if edit_ops.is_empty() && !left.data.is_empty() {
left.data_diff = vec![ObjDataDiff {
data: left.data.clone(),
kind: ObjDataDiffKind::None,
len: left.data.len(),
symbol: String::new(),
}];
right.data_diff = vec![ObjDataDiff {
data: right.data.clone(),
kind: ObjDataDiffKind::None,
len: right.data.len(),
symbol: String::new(),
}];
return Ok(());
}
let mut left_diff = Vec::<ObjDataDiff>::new();
let mut right_diff = Vec::<ObjDataDiff>::new();
let mut left_cur = 0usize;
let mut right_cur = 0usize;
let mut cur_op = LevEditType::Replace;
let mut cur_left_data = Vec::<u8>::new();
let mut cur_right_data = Vec::<u8>::new();
for op in edit_ops {
if cur_op != op.op_type || left_cur < op.first_start || right_cur < op.second_start {
match cur_op {
LevEditType::Replace => {
let left_data = take(&mut cur_left_data);
let right_data = take(&mut cur_right_data);
let left_data_len = left_data.len();
let right_data_len = right_data.len();
left_diff.push(ObjDataDiff {
data: left_data,
kind: ObjDataDiffKind::Replace,
len: left_data_len,
symbol: String::new(),
});
right_diff.push(ObjDataDiff {
data: right_data,
kind: ObjDataDiffKind::Replace,
len: right_data_len,
symbol: String::new(),
});
}
LevEditType::Insert => {
let right_data = take(&mut cur_right_data);
let right_data_len = right_data.len();
left_diff.push(ObjDataDiff {
data: vec![],
kind: ObjDataDiffKind::Insert,
len: right_data_len,
symbol: String::new(),
});
right_diff.push(ObjDataDiff {
data: right_data,
kind: ObjDataDiffKind::Insert,
len: right_data_len,
symbol: String::new(),
});
}
LevEditType::Delete => {
let left_data = take(&mut cur_left_data);
let left_data_len = left_data.len();
left_diff.push(ObjDataDiff {
data: left_data,
kind: ObjDataDiffKind::Delete,
len: left_data_len,
symbol: String::new(),
});
right_diff.push(ObjDataDiff {
data: vec![],
kind: ObjDataDiffKind::Delete,
len: left_data_len,
symbol: String::new(),
});
}
}
}
if left_cur < op.first_start {
left_diff.push(ObjDataDiff {
data: left.data[left_cur..op.first_start].to_vec(),
kind: ObjDataDiffKind::None,
len: op.first_start - left_cur,
symbol: String::new(),
});
left_cur = op.first_start;
}
if right_cur < op.second_start {
right_diff.push(ObjDataDiff {
data: right.data[right_cur..op.second_start].to_vec(),
kind: ObjDataDiffKind::None,
len: op.second_start - right_cur,
symbol: String::new(),
});
right_cur = op.second_start;
}
match op.op_type {
LevEditType::Replace => {
cur_left_data.push(left.data[left_cur]);
cur_right_data.push(right.data[right_cur]);
left_cur += 1;
right_cur += 1;
}
LevEditType::Insert => {
cur_right_data.push(right.data[right_cur]);
right_cur += 1;
}
LevEditType::Delete => {
cur_left_data.push(left.data[left_cur]);
left_cur += 1;
}
}
cur_op = op.op_type;
}
// if left_cur < left.data.len() {
// let len = left.data.len() - left_cur;
// left_diff.push(ObjDataDiff {
// data: left.data[left_cur..].to_vec(),
// kind: ObjDataDiffKind::Delete,
// len,
// });
// right_diff.push(ObjDataDiff { data: vec![], kind: ObjDataDiffKind::Delete, len });
// } else if right_cur < right.data.len() {
// let len = right.data.len() - right_cur;
// left_diff.push(ObjDataDiff { data: vec![], kind: ObjDataDiffKind::Insert, len });
// right_diff.push(ObjDataDiff {
// data: right.data[right_cur..].to_vec(),
// kind: ObjDataDiffKind::Insert,
// len,
// });
// }
// TODO: merge with above
match cur_op {
LevEditType::Replace => {
let left_data = take(&mut cur_left_data);
let right_data = take(&mut cur_right_data);
let left_data_len = left_data.len();
let right_data_len = right_data.len();
left_diff.push(ObjDataDiff {
data: left_data,
kind: ObjDataDiffKind::Replace,
len: left_data_len,
symbol: String::new(),
});
right_diff.push(ObjDataDiff {
data: right_data,
kind: ObjDataDiffKind::Replace,
len: right_data_len,
symbol: String::new(),
});
}
LevEditType::Insert => {
let right_data = take(&mut cur_right_data);
let right_data_len = right_data.len();
left_diff.push(ObjDataDiff {
data: vec![],
kind: ObjDataDiffKind::Insert,
len: right_data_len,
symbol: String::new(),
});
right_diff.push(ObjDataDiff {
data: right_data,
kind: ObjDataDiffKind::Insert,
len: right_data_len,
symbol: String::new(),
});
}
LevEditType::Delete => {
let left_data = take(&mut cur_left_data);
let left_data_len = left_data.len();
left_diff.push(ObjDataDiff {
data: left_data,
kind: ObjDataDiffKind::Delete,
len: left_data_len,
symbol: String::new(),
});
right_diff.push(ObjDataDiff {
data: vec![],
kind: ObjDataDiffKind::Delete,
len: left_data_len,
symbol: String::new(),
});
}
}
if left_cur < left.data.len() {
left_diff.push(ObjDataDiff {
data: left.data[left_cur..].to_vec(),
kind: ObjDataDiffKind::None,
len: left.data.len() - left_cur,
symbol: String::new(),
});
}
if right_cur < right.data.len() {
right_diff.push(ObjDataDiff {
data: right.data[right_cur..].to_vec(),
kind: ObjDataDiffKind::None,
len: right.data.len() - right_cur,
symbol: String::new(),
});
}
left.data_diff = left_diff;
right.data_diff = right_diff;
Ok(())
}
pub fn no_diff_data(section: &mut ObjSection) {
section.data_diff = vec![ObjDataDiff {
data: section.data.clone(),
kind: ObjDataDiffKind::None,
len: section.data.len(),
symbol: String::new(),
}];
}
+115
View File
@@ -0,0 +1,115 @@
pub mod code;
pub mod data;
pub mod editops;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use crate::{
diff::{
code::{diff_code, find_section_and_symbol, no_diff_code},
data::{diff_bss_symbols, diff_data, no_diff_data},
},
obj::{ObjInfo, ObjIns, ObjSectionKind},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum DiffAlg {
#[default]
Patience,
Levenshtein,
Myers,
Lcs,
}
pub struct DiffObjConfig {
pub code_alg: DiffAlg,
pub data_alg: DiffAlg,
pub relax_reloc_diffs: bool,
}
pub struct ProcessCodeResult {
pub ops: Vec<u8>,
pub insts: Vec<ObjIns>,
}
pub fn diff_objs(
config: &DiffObjConfig,
mut left: Option<&mut ObjInfo>,
mut right: Option<&mut ObjInfo>,
) -> Result<()> {
if let Some(left) = left.as_mut() {
for left_section in &mut left.sections {
if left_section.kind == ObjSectionKind::Code {
for left_symbol in &mut left_section.symbols {
if let Some((right, (right_section_idx, right_symbol_idx))) =
right.as_mut().and_then(|obj| {
find_section_and_symbol(obj, &left_symbol.name).map(|s| (obj, s))
})
{
let right_section = &mut right.sections[right_section_idx];
let right_symbol = &mut right_section.symbols[right_symbol_idx];
left_symbol.diff_symbol = Some(right_symbol.name.clone());
right_symbol.diff_symbol = Some(left_symbol.name.clone());
diff_code(
config,
left.architecture,
&left_section.data,
&right_section.data,
left_symbol,
right_symbol,
&left_section.relocations,
&right_section.relocations,
&left.line_info,
&right.line_info,
)?;
} else {
no_diff_code(
left.architecture,
&left_section.data,
left_symbol,
&left_section.relocations,
&left.line_info,
)?;
}
}
} else if let Some(right_section) = right
.as_mut()
.and_then(|obj| obj.sections.iter_mut().find(|s| s.name == left_section.name))
{
if left_section.kind == ObjSectionKind::Data {
diff_data(config.data_alg, left_section, right_section)?;
} else if left_section.kind == ObjSectionKind::Bss {
diff_bss_symbols(&mut left_section.symbols, &mut right_section.symbols)?;
}
} else if left_section.kind == ObjSectionKind::Data {
no_diff_data(left_section);
}
}
}
if let Some(right) = right.as_mut() {
for right_section in right.sections.iter_mut() {
if right_section.kind == ObjSectionKind::Code {
for right_symbol in &mut right_section.symbols {
if right_symbol.instructions.is_empty() {
no_diff_code(
right.architecture,
&right_section.data,
right_symbol,
&right_section.relocations,
&right.line_info,
)?;
}
}
} else if right_section.kind == ObjSectionKind::Data
&& right_section.data_diff.is_empty()
{
no_diff_data(right_section);
}
}
}
if let (Some(left), Some(right)) = (left, right) {
diff_bss_symbols(&mut left.common, &mut right.common)?;
}
Ok(())
}
+146
View File
@@ -0,0 +1,146 @@
// font-kit/src/matching.rs
//
// Copyright © 2018 The Pathfinder Project Developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! Determines the closest font matching a description per the CSS Fonts Level 3 specification.
use float_ord::FloatOrd;
use font_kit::{
error::SelectionError,
properties::{Properties, Stretch, Style, Weight},
};
/// This follows CSS Fonts Level 3 § 5.2 [1].
///
/// https://drafts.csswg.org/css-fonts-3/#font-style-matching
pub fn find_best_match(
candidates: &[Properties],
query: &Properties,
) -> Result<usize, SelectionError> {
// Step 4.
let mut matching_set: Vec<usize> = (0..candidates.len()).collect();
if matching_set.is_empty() {
return Err(SelectionError::NotFound);
}
// Step 4a (`font-stretch`).
let matching_stretch = if matching_set
.iter()
.any(|&index| candidates[index].stretch == query.stretch)
{
// Exact match.
query.stretch
} else if query.stretch <= Stretch::NORMAL {
// Closest width, first checking narrower values and then wider values.
match matching_set
.iter()
.filter(|&&index| candidates[index].stretch < query.stretch)
.min_by_key(|&&index| FloatOrd(query.stretch.0 - candidates[index].stretch.0))
{
Some(&matching_index) => candidates[matching_index].stretch,
None => {
let matching_index = *matching_set
.iter()
.min_by_key(|&&index| FloatOrd(candidates[index].stretch.0 - query.stretch.0))
.unwrap();
candidates[matching_index].stretch
}
}
} else {
// Closest width, first checking wider values and then narrower values.
match matching_set
.iter()
.filter(|&&index| candidates[index].stretch > query.stretch)
.min_by_key(|&&index| FloatOrd(candidates[index].stretch.0 - query.stretch.0))
{
Some(&matching_index) => candidates[matching_index].stretch,
None => {
let matching_index = *matching_set
.iter()
.min_by_key(|&&index| FloatOrd(query.stretch.0 - candidates[index].stretch.0))
.unwrap();
candidates[matching_index].stretch
}
}
};
matching_set.retain(|&index| candidates[index].stretch == matching_stretch);
// Step 4b (`font-style`).
let style_preference = match query.style {
Style::Italic => [Style::Italic, Style::Oblique, Style::Normal],
Style::Oblique => [Style::Oblique, Style::Italic, Style::Normal],
Style::Normal => [Style::Normal, Style::Oblique, Style::Italic],
};
let matching_style = *style_preference
.iter()
.find(|&query_style| {
matching_set.iter().any(|&index| candidates[index].style == *query_style)
})
.unwrap();
matching_set.retain(|&index| candidates[index].style == matching_style);
// Step 4c (`font-weight`).
//
// The spec doesn't say what to do if the weight is between 400 and 500 exclusive, so we
// just use 450 as the cutoff.
let matching_weight =
if matching_set.iter().any(|&index| candidates[index].weight == query.weight) {
query.weight
} else if query.weight >= Weight(400.0)
&& query.weight < Weight(450.0)
&& matching_set.iter().any(|&index| candidates[index].weight == Weight(500.0))
{
// Check 500 first.
Weight(500.0)
} else if query.weight >= Weight(450.0)
&& query.weight <= Weight(500.0)
&& matching_set.iter().any(|&index| candidates[index].weight == Weight(400.0))
{
// Check 400 first.
Weight(400.0)
} else if query.weight <= Weight(500.0) {
// Closest weight, first checking thinner values and then fatter ones.
match matching_set
.iter()
.filter(|&&index| candidates[index].weight <= query.weight)
.min_by_key(|&&index| FloatOrd(query.weight.0 - candidates[index].weight.0))
{
Some(&matching_index) => candidates[matching_index].weight,
None => {
let matching_index = *matching_set
.iter()
.min_by_key(|&&index| FloatOrd(candidates[index].weight.0 - query.weight.0))
.unwrap();
candidates[matching_index].weight
}
}
} else {
// Closest weight, first checking fatter values and then thinner ones.
match matching_set
.iter()
.filter(|&&index| candidates[index].weight >= query.weight)
.min_by_key(|&&index| FloatOrd(candidates[index].weight.0 - query.weight.0))
{
Some(&matching_index) => candidates[matching_index].weight,
None => {
let matching_index = *matching_set
.iter()
.min_by_key(|&&index| FloatOrd(query.weight.0 - candidates[index].weight.0))
.unwrap();
candidates[matching_index].weight
}
}
};
matching_set.retain(|&index| candidates[index].weight == matching_weight);
// Step 4d concerns `font-size`, but fonts in `font-kit` are unsized, so we ignore that.
// Return the result.
matching_set.into_iter().next().ok_or(SelectionError::NotFound)
}
+104
View File
@@ -0,0 +1,104 @@
pub mod matching;
use std::{borrow::Cow, fs, sync::Arc};
use anyhow::{Context, Result};
use crate::fonts::matching::find_best_match;
pub struct LoadedFontFamily {
pub family_name: String,
pub fonts: Vec<font_kit::font::Font>,
pub handles: Vec<font_kit::handle::Handle>,
pub properties: Vec<font_kit::properties::Properties>,
pub default_index: usize,
}
pub struct LoadedFont {
pub font_name: String,
pub font_data: egui::FontData,
}
pub fn load_font_family(
source: &font_kit::source::SystemSource,
name: &str,
) -> Option<LoadedFontFamily> {
let family_handle = source.select_family_by_name(name).ok()?;
if family_handle.fonts().is_empty() {
log::warn!("No fonts found for family '{}'", name);
return None;
}
let handles = family_handle.fonts().to_vec();
let mut loaded = Vec::with_capacity(handles.len());
for handle in handles.iter() {
match font_kit::loaders::default::Font::from_handle(handle) {
Ok(font) => loaded.push(font),
Err(err) => {
log::warn!("Failed to load font '{}': {}", name, err);
return None;
}
}
}
let properties = loaded.iter().map(|f| f.properties()).collect::<Vec<_>>();
let default_index =
find_best_match(&properties, &font_kit::properties::Properties::new()).unwrap_or(0);
let font_family_name =
loaded.first().map(|f| f.family_name()).unwrap_or_else(|| name.to_string());
Some(LoadedFontFamily {
family_name: font_family_name,
fonts: loaded,
handles,
properties,
default_index,
})
}
pub fn load_font(handle: &font_kit::handle::Handle) -> Result<LoadedFont> {
let loaded = font_kit::loaders::default::Font::from_handle(handle)?;
let data = match handle {
font_kit::handle::Handle::Memory { bytes, font_index } => egui::FontData {
font: Cow::Owned(bytes.to_vec()),
index: *font_index,
tweak: Default::default(),
},
font_kit::handle::Handle::Path { path, font_index } => {
let vec = fs::read(path).with_context(|| {
format!("Failed to load font '{}' (index {})", path.display(), font_index)
})?;
egui::FontData { font: Cow::Owned(vec), index: *font_index, tweak: Default::default() }
}
};
Ok(LoadedFont { font_name: loaded.full_name(), font_data: data })
}
pub fn load_font_if_needed(
ctx: &egui::Context,
source: &font_kit::source::SystemSource,
font_id: &egui::FontId,
base_family: egui::FontFamily,
fonts: &mut egui::FontDefinitions,
) -> Result<()> {
if fonts.families.contains_key(&font_id.family) {
return Ok(());
}
let family_name = match &font_id.family {
egui::FontFamily::Proportional | egui::FontFamily::Monospace => return Ok(()),
egui::FontFamily::Name(v) => v,
};
let family = load_font_family(source, family_name)
.with_context(|| format!("Failed to load font family '{}'", family_name))?;
let default_fonts = fonts.families.get(&base_family).cloned().unwrap_or_default();
// FIXME clean up
let default_font_ref = family.fonts.get(family.default_index).unwrap();
let default_font = family.handles.get(family.default_index).unwrap();
let default_font_data = load_font(default_font).unwrap();
log::info!("Loaded font family '{}'", family.family_name);
fonts.font_data.insert(default_font_ref.full_name(), default_font_data.font_data);
fonts
.families
.entry(egui::FontFamily::Name(Arc::from(family.family_name)))
.or_insert_with(|| default_fonts)
.insert(0, default_font_ref.full_name());
ctx.set_fonts(fonts.clone());
Ok(())
}
+7 -7
View File
@@ -4,7 +4,7 @@ use anyhow::{Context, Result};
use self_update::{cargo_crate_version, update::Release};
use crate::{
jobs::{start_job, update_status, Job, JobResult, JobState, JobStatusRef},
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
update::{build_updater, BIN_NAME},
};
@@ -14,20 +14,20 @@ pub struct CheckUpdateResult {
pub found_binary: bool,
}
fn run_check_update(status: &JobStatusRef, cancel: Receiver<()>) -> Result<Box<CheckUpdateResult>> {
update_status(status, "Fetching latest release".to_string(), 0, 1, &cancel)?;
fn run_check_update(context: &JobContext, cancel: Receiver<()>) -> Result<Box<CheckUpdateResult>> {
update_status(context, "Fetching latest release".to_string(), 0, 1, &cancel)?;
let updater = build_updater().context("Failed to create release updater")?;
let latest_release = updater.get_latest_release()?;
let update_available =
self_update::version::bump_is_greater(cargo_crate_version!(), &latest_release.version)?;
let found_binary = latest_release.assets.iter().any(|a| a.name == BIN_NAME);
update_status(status, "Complete".to_string(), 1, 1, &cancel)?;
update_status(context, "Complete".to_string(), 1, 1, &cancel)?;
Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary }))
}
pub fn start_check_update() -> JobState {
start_job("Check for updates", Job::CheckUpdate, move |status, cancel| {
run_check_update(status, cancel).map(|result| JobResult::CheckUpdate(Some(result)))
pub fn start_check_update(ctx: &egui::Context) -> JobState {
start_job(ctx, "Check for updates", Job::CheckUpdate, move |context, cancel| {
run_check_update(&context, cancel).map(|result| JobResult::CheckUpdate(Some(result)))
})
}
+135
View File
@@ -0,0 +1,135 @@
use std::{fs, path::PathBuf, sync::mpsc::Receiver};
use anyhow::{anyhow, bail, Context, Result};
use const_format::formatcp;
use crate::{
app::AppConfig,
jobs::{
objdiff::{run_make, BuildConfig, BuildStatus},
start_job, update_status, Job, JobContext, JobResult, JobState,
},
};
#[derive(Debug, Clone)]
pub struct CreateScratchConfig {
pub build_config: BuildConfig,
pub context_path: Option<PathBuf>,
pub build_context: bool,
// Scratch fields
pub compiler: String,
pub platform: String,
pub compiler_flags: String,
pub function_name: String,
pub target_obj: PathBuf,
}
impl CreateScratchConfig {
pub(crate) fn from_config(config: &AppConfig, function_name: String) -> Result<Self> {
let Some(selected_obj) = &config.selected_obj else {
bail!("No object selected");
};
let Some(target_path) = &selected_obj.target_path else {
bail!("No target path for {}", selected_obj.name);
};
let Some(scratch_config) = &selected_obj.scratch else {
bail!("No scratch configuration for {}", selected_obj.name);
};
Ok(Self {
build_config: BuildConfig::from_config(config),
context_path: scratch_config.ctx_path.clone(),
build_context: scratch_config.build_ctx,
compiler: scratch_config.compiler.clone().unwrap_or_default(),
platform: scratch_config.platform.clone().unwrap_or_default(),
compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(),
function_name,
target_obj: target_path.to_path_buf(),
})
}
pub fn is_available(config: &AppConfig) -> bool {
let Some(selected_obj) = &config.selected_obj else {
return false;
};
selected_obj.target_path.is_some() && selected_obj.scratch.is_some()
}
}
#[derive(Default, Debug, Clone)]
pub struct CreateScratchResult {
pub scratch_url: String,
}
#[derive(Debug, Default, Clone, serde::Deserialize)]
struct CreateScratchResponse {
pub slug: String,
pub claim_token: String,
}
const API_HOST: &str = "http://127.0.0.1:8000";
const WEB_HOST: &str = "http://localhost:8080";
fn run_create_scratch(
status: &JobContext,
cancel: Receiver<()>,
config: CreateScratchConfig,
) -> Result<Box<CreateScratchResult>> {
let project_dir =
config.build_config.project_dir.as_ref().ok_or_else(|| anyhow!("Missing project dir"))?;
let mut context = None;
if let Some(context_path) = &config.context_path {
if config.build_context {
update_status(status, "Building context".to_string(), 0, 2, &cancel)?;
match run_make(&config.build_config, context_path) {
BuildStatus { success: true, .. } => {}
BuildStatus { success: false, stdout, stderr, .. } => {
bail!("Failed to build context:\n{stdout}\n{stderr}")
}
}
}
let context_path = project_dir.join(context_path);
context = Some(
fs::read_to_string(&context_path)
.map_err(|e| anyhow!("Failed to read {}: {}", context_path.display(), e))?,
);
}
update_status(status, "Creating scratch".to_string(), 1, 2, &cancel)?;
let diff_flags = [format!("--disassemble={}", config.function_name)];
let diff_flags = serde_json::to_string(&diff_flags).unwrap();
let obj_path = project_dir.join(&config.target_obj);
let file = reqwest::blocking::multipart::Part::file(&obj_path)
.with_context(|| format!("Failed to open {}", obj_path.display()))?;
let form = reqwest::blocking::multipart::Form::new()
.text("compiler", config.compiler.clone())
.text("platform", config.platform.clone())
.text("compiler_flags", config.compiler_flags.clone())
.text("diff_label", config.function_name.clone())
.text("diff_flags", diff_flags)
.text("context", context.unwrap_or_default())
.text("source_code", "// Move related code from Context tab to here")
.part("target_obj", file);
let client = reqwest::blocking::Client::new();
let response = client
.post(formatcp!("{API_HOST}/api/scratch"))
.multipart(form)
.send()
.map_err(|e| anyhow!("Failed to send request: {}", e))?;
if !response.status().is_success() {
return Err(anyhow!("Failed to create scratch: {}", response.text()?));
}
let body: CreateScratchResponse = response.json().context("Failed to parse response")?;
let scratch_url = format!("{WEB_HOST}/scratch/{}/claim?token={}", body.slug, body.claim_token);
update_status(status, "Complete".to_string(), 2, 2, &cancel)?;
Ok(Box::from(CreateScratchResult { scratch_url }))
}
pub fn start_create_scratch(ctx: &egui::Context, config: CreateScratchConfig) -> JobState {
start_job(ctx, "Create scratch", Job::CreateScratch, move |context, cancel| {
run_create_scratch(&context, cancel, config)
.map(|result| JobResult::CreateScratch(Some(result)))
})
}
+26 -17
View File
@@ -9,9 +9,13 @@ use std::{
use anyhow::Result;
use crate::jobs::{check_update::CheckUpdateResult, objdiff::ObjDiffResult, update::UpdateResult};
use crate::jobs::{
check_update::CheckUpdateResult, create_scratch::CreateScratchResult, objdiff::ObjDiffResult,
update::UpdateResult,
};
pub mod check_update;
pub mod create_scratch;
pub mod objdiff;
pub mod update;
@@ -20,6 +24,7 @@ pub enum Job {
ObjDiff,
CheckUpdate,
Update,
CreateScratch,
}
pub static JOB_ID: AtomicUsize = AtomicUsize::new(0);
@@ -48,6 +53,7 @@ impl JobQueue {
}
/// Returns whether any job is running.
#[allow(dead_code)]
pub fn any_running(&self) -> bool {
self.jobs.iter().any(|job| {
if let Some(handle) = &job.handle {
@@ -81,7 +87,7 @@ impl JobQueue {
self.jobs.retain(|job| {
!(job.should_remove
&& job.handle.is_none()
&& job.status.read().unwrap().error.is_none())
&& job.context.status.read().unwrap().error.is_none())
});
}
@@ -89,13 +95,17 @@ impl JobQueue {
pub fn remove(&mut self, id: usize) { self.jobs.retain(|job| job.id != id); }
}
pub type JobStatusRef = Arc<RwLock<JobStatus>>;
#[derive(Clone)]
pub struct JobContext {
pub status: Arc<RwLock<JobStatus>>,
pub egui: egui::Context,
}
pub struct JobState {
pub id: usize,
pub kind: Job,
pub handle: Option<JoinHandle<JobResult>>,
pub status: JobStatusRef,
pub context: JobContext,
pub cancel: Sender<()>,
pub should_remove: bool,
}
@@ -114,6 +124,7 @@ pub enum JobResult {
ObjDiff(Option<Box<ObjDiffResult>>),
CheckUpdate(Option<Box<CheckUpdateResult>>),
Update(Box<UpdateResult>),
CreateScratch(Option<Box<CreateScratchResult>>),
}
fn should_cancel(rx: &Receiver<()>) -> bool {
@@ -124,9 +135,10 @@ fn should_cancel(rx: &Receiver<()>) -> bool {
}
fn start_job(
ctx: &egui::Context,
title: &str,
kind: Job,
run: impl FnOnce(&JobStatusRef, Receiver<()>) -> Result<JobResult> + Send + 'static,
run: impl FnOnce(JobContext, Receiver<()>) -> Result<JobResult> + Send + 'static,
) -> JobState {
let status = Arc::new(RwLock::new(JobStatus {
title: title.to_string(),
@@ -135,10 +147,11 @@ fn start_job(
status: String::new(),
error: None,
}));
let status_clone = status.clone();
let context = JobContext { status: status.clone(), egui: ctx.clone() };
let context_inner = JobContext { status: status.clone(), egui: ctx.clone() };
let (tx, rx) = std::sync::mpsc::channel();
let handle = std::thread::spawn(move || {
return match run(&status, rx) {
return match run(context_inner, rx) {
Ok(state) => state,
Err(e) => {
if let Ok(mut w) = status.write() {
@@ -150,24 +163,18 @@ fn start_job(
});
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
log::info!("Started job {}", id);
JobState {
id,
kind,
handle: Some(handle),
status: status_clone,
cancel: tx,
should_remove: true,
}
JobState { id, kind, handle: Some(handle), context, cancel: tx, should_remove: true }
}
fn update_status(
status: &JobStatusRef,
context: &JobContext,
str: String,
count: u32,
total: u32,
cancel: &Receiver<()>,
) -> Result<()> {
let mut w = status.write().map_err(|_| anyhow::Error::msg("Failed to lock job status"))?;
let mut w =
context.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) {
@@ -176,5 +183,7 @@ fn update_status(
} else {
w.status = str;
}
drop(w);
context.egui.request_repaint();
Ok(())
}
+118 -36
View File
@@ -1,18 +1,77 @@
use std::{path::Path, process::Command, str::from_utf8, sync::mpsc::Receiver};
use std::{
path::{Path, PathBuf},
process::Command,
str::from_utf8,
sync::mpsc::Receiver,
};
use anyhow::{anyhow, Context, Error, Result};
use time::OffsetDateTime;
use crate::{
app::{AppConfig, AppConfigRef},
diff::diff_objs,
jobs::{start_job, update_status, Job, JobResult, JobState, JobStatusRef},
app::{AppConfig, ObjectConfig},
diff::{diff_objs, DiffAlg, DiffObjConfig},
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
obj::{elf, ObjInfo},
};
pub struct BuildStatus {
pub success: bool,
pub log: String,
pub cmdline: String,
pub stdout: String,
pub stderr: String,
}
impl Default for BuildStatus {
fn default() -> Self {
BuildStatus {
success: true,
cmdline: String::new(),
stdout: String::new(),
stderr: String::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct BuildConfig {
pub project_dir: Option<PathBuf>,
pub custom_make: Option<String>,
pub selected_wsl_distro: Option<String>,
}
impl BuildConfig {
pub(crate) fn from_config(config: &AppConfig) -> Self {
Self {
project_dir: config.project_dir.clone(),
custom_make: config.custom_make.clone(),
selected_wsl_distro: config.selected_wsl_distro.clone(),
}
}
}
pub struct ObjDiffConfig {
pub build_config: BuildConfig,
pub build_base: bool,
pub build_target: bool,
pub selected_obj: Option<ObjectConfig>,
pub code_alg: DiffAlg,
pub data_alg: DiffAlg,
pub relax_reloc_diffs: bool,
}
impl ObjDiffConfig {
pub(crate) fn from_config(config: &AppConfig) -> Self {
Self {
build_config: BuildConfig::from_config(config),
build_base: config.build_base,
build_target: config.build_target,
selected_obj: config.selected_obj.clone(),
code_alg: config.code_alg,
data_alg: config.data_alg,
relax_reloc_diffs: config.relax_reloc_diffs,
}
}
}
pub struct ObjDiffResult {
@@ -23,7 +82,14 @@ pub struct ObjDiffResult {
pub time: OffsetDateTime,
}
fn run_make(cwd: &Path, arg: &Path, config: &AppConfig) -> BuildStatus {
pub(crate) fn run_make(config: &BuildConfig, arg: &Path) -> BuildStatus {
let Some(cwd) = &config.project_dir else {
return BuildStatus {
success: false,
stderr: "Missing project dir".to_string(),
..Default::default()
};
};
match (|| -> Result<BuildStatus> {
let make = config.custom_make.as_deref().unwrap_or("make");
#[cfg(not(windows))]
@@ -57,28 +123,38 @@ fn run_make(cwd: &Path, arg: &Path, config: &AppConfig) -> BuildStatus {
command.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
command
};
let mut cmdline =
shell_escape::escape(command.get_program().to_string_lossy()).into_owned();
for arg in command.get_args() {
cmdline.push(' ');
cmdline.push_str(shell_escape::escape(arg.to_string_lossy()).as_ref());
}
let output = command.output().context("Failed to execute build")?;
let stdout = from_utf8(&output.stdout).context("Failed to process stdout")?;
let stderr = from_utf8(&output.stderr).context("Failed to process stderr")?;
Ok(BuildStatus {
success: output.status.code().unwrap_or(-1) == 0,
log: format!("{stdout}\n{stderr}"),
cmdline,
stdout: stdout.to_string(),
stderr: stderr.to_string(),
})
})() {
Ok(status) => status,
Err(e) => BuildStatus { success: false, log: e.to_string() },
Err(e) => BuildStatus { success: false, stderr: e.to_string(), ..Default::default() },
}
}
fn run_build(
status: &JobStatusRef,
context: &JobContext,
cancel: Receiver<()>,
config: AppConfigRef,
config: ObjDiffConfig,
) -> Result<Box<ObjDiffResult>> {
let config = config.read().map_err(|_| Error::msg("Failed to lock app config"))?.clone();
let obj_config = config.selected_obj.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?;
let project_dir =
config.project_dir.as_ref().ok_or_else(|| Error::msg("Missing project dir"))?;
let project_dir = config
.build_config
.project_dir
.as_ref()
.ok_or_else(|| Error::msg("Missing project dir"))?;
let target_path_rel = if let Some(target_path) = &obj_config.target_path {
Some(target_path.strip_prefix(project_dir).map_err(|_| {
anyhow!(
@@ -106,34 +182,35 @@ fn run_build(
if config.build_target && target_path_rel.is_some() {
total += 1;
}
if base_path_rel.is_some() {
if config.build_base && base_path_rel.is_some() {
total += 1;
}
let first_status = match target_path_rel {
Some(target_path_rel) if config.build_target => {
update_status(
status,
context,
format!("Building target {}", target_path_rel.display()),
0,
total,
&cancel,
)?;
run_make(project_dir, target_path_rel, &config)
run_make(&config.build_config, target_path_rel)
}
_ => BuildStatus { success: true, log: String::new() },
_ => BuildStatus::default(),
};
let second_status = if let Some(base_path_rel) = base_path_rel {
update_status(
status,
format!("Building base {}", base_path_rel.display()),
1,
total,
&cancel,
)?;
run_make(project_dir, base_path_rel, &config)
} else {
BuildStatus { success: true, log: String::new() }
let second_status = match base_path_rel {
Some(base_path_rel) if config.build_base => {
update_status(
context,
format!("Building base {}", base_path_rel.display()),
0,
total,
&cancel,
)?;
run_make(&config.build_config, base_path_rel)
}
_ => BuildStatus::default(),
};
let time = OffsetDateTime::now_utc();
@@ -142,7 +219,7 @@ fn run_build(
match &obj_config.target_path {
Some(target_path) if first_status.success => {
update_status(
status,
context,
format!("Loading target {}", target_path_rel.unwrap().display()),
2,
total,
@@ -158,7 +235,7 @@ fn run_build(
let mut second_obj = match &obj_config.base_path {
Some(base_path) if second_status.success => {
update_status(
status,
context,
format!("Loading base {}", base_path_rel.unwrap().display()),
3,
total,
@@ -172,15 +249,20 @@ fn run_build(
_ => None,
};
update_status(status, "Performing diff".to_string(), 4, total, &cancel)?;
diff_objs(first_obj.as_mut(), second_obj.as_mut())?;
update_status(context, "Performing diff".to_string(), 4, total, &cancel)?;
let diff_config = DiffObjConfig {
code_alg: config.code_alg,
data_alg: config.data_alg,
relax_reloc_diffs: config.relax_reloc_diffs,
};
diff_objs(&diff_config, first_obj.as_mut(), second_obj.as_mut())?;
update_status(status, "Complete".to_string(), total, total, &cancel)?;
update_status(context, "Complete".to_string(), total, total, &cancel)?;
Ok(Box::new(ObjDiffResult { first_status, second_status, first_obj, second_obj, time }))
}
pub fn start_build(config: AppConfigRef) -> JobState {
start_job("Object diff", Job::ObjDiff, move |status, cancel| {
run_build(status, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
pub fn start_build(ctx: &egui::Context, config: ObjDiffConfig) -> JobState {
start_job(ctx, "Object diff", Job::ObjDiff, move |context, cancel| {
run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
})
}
+5 -5
View File
@@ -9,7 +9,7 @@ use anyhow::{Context, Result};
use const_format::formatcp;
use crate::{
jobs::{start_job, update_status, Job, JobResult, JobState, JobStatusRef},
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
update::{build_updater, BIN_NAME},
};
@@ -17,7 +17,7 @@ pub struct UpdateResult {
pub exe_path: PathBuf,
}
fn run_update(status: &JobStatusRef, cancel: Receiver<()>) -> Result<Box<UpdateResult>> {
fn run_update(status: &JobContext, 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()?;
@@ -53,8 +53,8 @@ fn run_update(status: &JobStatusRef, cancel: Receiver<()>) -> Result<Box<UpdateR
Ok(Box::from(UpdateResult { exe_path: target_file }))
}
pub fn start_update() -> JobState {
start_job("Update app", Job::Update, move |status, cancel| {
run_update(status, cancel).map(JobResult::Update)
pub fn start_update(ctx: &egui::Context) -> JobState {
start_job(ctx, "Update app", Job::Update, move |context, cancel| {
run_update(&context, cancel).map(JobResult::Update)
})
}

Some files were not shown because too many files have changed in this diff Show More