Compare commits

..

49 Commits

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

Resolves #34
2024-01-22 00:14:03 -07:00
02f521a528 Disable more options when project config is loaded 2024-01-21 23:58:10 -07:00
197d1247a8 Highlight: Consider uimm/simm/offset all equivalent
Fixes #33
2024-01-21 23:48:12 -07:00
eef9598e76 Add DWARF 2+ line info support
Resolves #37
2024-01-21 23:38:52 -07:00
405a2a82db Upgrade all dependencies (+ egui/eframe 0.25.0) 2024-01-20 23:41:48 -07:00
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
b74a49ed0c Upgrade to egui/eframe 0.24.1 2023-12-11 13:36:00 -05:00
e1079db93a Add font loading & configuration 2023-11-28 23:13:51 -05:00
879e03eed5 All one line (thanks PowerShell) 2023-11-27 19:55:17 -05:00
53e6e0c7c4 Build macOS with wgpu enabled 2023-11-27 19:52:12 -05:00
67cea2a8d9 Update README.md 2023-11-24 23:17:35 -05:00
e4f97adbdd Version 0.6.1 2023-11-22 00:11:33 -05:00
0ec7bf078b Highlight: Consider reg offsets and signed immediates equivalent
Fixes #32
2023-11-22 00:07:21 -05:00
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
236e4d8d26 CI updates, update deny.toml, clippy fix 2023-11-21 12:16:15 -05:00
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
261e1b8e07 Upgrade all dependencies 2023-11-21 11:57:02 -05:00
a29e913b45 Add "Incomplete" filter to object tree
Allows filtering out objects marked as "complete".
2023-11-21 11:50:11 -05:00
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
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
7b58f9a269 Adjust "Diffable" to exclude missing target objects 2023-10-09 12:47:22 -04:00
d9e7dacb6d Version 0.5.1 2023-10-07 14:49:01 -04:00
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
803eaafee6 Hide hidden symbols by default; add "Diff Options" to menu 2023-10-07 13:27:12 -04:00
e1dc84698f Restore context menu on highlightable fields
Fixes #30
2023-10-07 13:04:08 -04:00
e68629c339 Update ppc750cl (subi{,s,c} mnemonics, capstone-style CR bits) 2023-10-06 01:22:26 -04:00
bb9ff4b928 Update all dependencies 2023-10-05 23:55:01 -04:00
57392daaeb Implement click-to-highlight
Highlights registers, instructions, arguments, symbols or addresses on click.

Resolves #7
2023-10-05 23:40:45 -04:00
2dd3dd60a8 Update webpki (advisory fix) 2023-10-05 00:01:09 -04:00
f4757b8d92 Version 0.4.4
Add `#[serde(default)]` to new AppConfig field
2023-10-04 23:52:00 -04:00
52f8c5d4f9 Add "Recent Projects" to file menu 2023-10-03 13:52:16 -04:00
711f40b591 I forgot to bump the Cargo.toml version, oops 2023-09-10 00:24:53 -04:00
26932b2e44 Support min_version field in objdiff.json 2023-09-09 23:54:25 -04:00
192a06bc0b Project configuration improvements
- Support `completed` field for objects in project config. In object tree, displays red for incomplete, green for complete.
- Add support for one-sided diffs. A project can include objects without an associated source file for viewing.
- Add versioning to AppConfig, supporting upgrades without losing user configuration.
2023-09-09 23:43:12 -04:00
5bfa47fce9 Update webpki, rustls-webpki 2023-09-03 09:42:26 -04:00
1d9b9b6893 clippy fix 2023-09-03 09:31:12 -04:00
6b8e469261 Project configuration fixes & improvements
- Allow config to specify object "target_path" and "base_path" explicitly, rather than relying on relative path from the "target_dir" and "base_dir". Useful for more complex directory layouts.
- Fix watch_patterns in project config not using default.
- Fix "Rebuild on changes" not defaulting to true.
- Keep watching project config updates even when "Rebuild on changes" is false.
- Disable some configuration options when loaded from project config file.
2023-09-03 09:28:46 -04:00
bf3ba48539 Match watch_patterns with project-relative paths 2023-08-14 00:21:56 -04:00
21cdf268f0 Update README.md 2023-08-12 14:41:19 -04:00
3970bc8acf Document configuration file & more cleanup 2023-08-12 14:18:09 -04:00
eaf0fabc2d Updates to Objects pane & config improvements 2023-08-09 21:53:04 -04:00
91d11c83d6 Refactor state & config structs, various cleanup 2023-08-09 21:53:04 -04:00
94924047b7 Job state handling cleanup 2023-08-09 19:39:06 -04:00
f5f6869029 Start project config file support & rework UI 2023-08-07 20:11:56 -04:00
b02e32f2b7 Add dark/light theme toggle (light theme WIP) 2023-07-15 11:17:59 -04:00
c7a326b160 Update all dependencies (again) 2023-07-06 10:37:57 -04:00
100f8f8ac5 Update all dependencies 2023-05-11 02:47:57 -04:00
41 changed files with 7463 additions and 3044 deletions

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

3665
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
[package]
name = "objdiff"
version = "0.3.1"
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"
@@ -11,50 +11,66 @@ description = """
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.68"
bytes = "1.3.0"
anyhow = "1.0.79"
byteorder = "1.5.0"
bytes = "1.5.0"
cfg-if = "1.0.0"
const_format = "0.2.30"
cwdemangle = "0.1.5"
eframe = { version = "0.20.1", features = ["persistence"] }
egui = "0.20.1"
egui_extras = "0.20.0"
flagset = "0.4.3"
log = "0.4.17"
memmap2 = "0.5.8"
notify = "5.0.0"
object = { version = "0.30.2", features = ["read_core", "std", "elf"], default-features = false }
png = "0.17.7"
ppc750cl = { git = "https://github.com/terorie/ppc750cl", rev = "9ae36eef34aa6d74e00972c7671f547a2acfd0aa" }
rabbitizer = "1.5.8"
rfd = { version = "0.10.0" } #, default-features = false, features = ['xdg-portal']
const_format = "0.2.32"
cwdemangle = "0.1.6"
dirs = "5.0.1"
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"] }
tempfile = "3.3.0"
thiserror = "1.0.38"
time = { version = "0.3.17", features = ["formatting", "local-offset"] }
toml = "0.5.11"
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"
byteorder = "1.4.3"
# For Linux static binaries, use rustls
[target.'cfg(target_os = "linux")'.dependencies]
reqwest = { version = "0.11.14", default-features = false, features = ["blocking", "json", "rustls"] }
self_update = { version = "0.34.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.14"
self_update = "0.34.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"
@@ -76,5 +92,5 @@ console_error_panic_hook = "0.1.7"
tracing-wasm = "0.2"
[build-dependencies]
anyhow = "1.0.68"
vergen = { version = "7.5.0", features = ["build", "cargo", "git"], default-features = false }
anyhow = "1.0.79"
vergen = { version = "8.3.1", features = ["build", "cargo", "git", "gitcl"] }

137
README.md
View File

@@ -3,16 +3,142 @@
[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).
Currently supports:
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)
- MIPS (Nintendo 64)
See [Usage](#usage) for more information.
![Symbol Screenshot](assets/screen-symbols.png)
![Diff Screenshot](assets/screen-diff.png)
### License
## Usage
objdiff works by comparing two relocatable object files (`.o`). The objects are expected to have the same relative path
from the "target" and "base" directories.
For example, if the target ("expected") object is located at `build/asm/MetroTRK/mslsupp.o` and the base ("actual")
object is located at `build/src/MetroTRK/mslsupp.o`, the following configuration would be used:
- Target build directory: `build/asm`
- Base build directory: `build/src`
- Object: `MetroTRK/mslsupp.o`
objdiff will then execute the build system from the project directory to build both objects:
```sh
$ make build/asm/MetroTRK/mslsupp.o # Only if "Build target object" is enabled
$ make build/src/MetroTRK/mslsupp.o
```
The objects will then be compared and the results will be displayed in the UI.
See [Configuration](#configuration) for more information.
## Configuration
While **not required** (most settings can be specified in the UI), projects can add an `objdiff.json` (or
`objdiff.yaml`, `objdiff.yml`) file to configure the tool automatically. The configuration file must be located in
the root project directory.
If your project has a generator script (e.g. `configure.py`), it's recommended to generate the objdiff configuration
file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it from being committed.
```json5
// objdiff.json
{
"custom_make": "ninja",
// Only required if objects use "path" instead of "target_path" and "base_path".
"target_dir": "build/asm",
"base_dir": "build/src",
"build_target": true,
"watch_patterns": [
"*.c",
"*.cp",
"*.cpp",
"*.h",
"*.hpp",
"*.py"
],
"objects": [
{
"name": "main/MetroTRK/mslsupp",
// Option 1: Relative to target_dir and base_dir
"path": "MetroTRK/mslsupp.o",
// Option 2: Explicit paths from project root
// Useful for more complex directory layouts
"target_path": "build/asm/MetroTRK/mslsupp.o",
"base_path": "build/src/MetroTRK/mslsupp.o",
"reverse_fn_order": false
},
// ...
]
}
```
`custom_make` _(optional)_: By default, objdiff will use `make` to build the project.
If the project uses a different build system (e.g. `ninja`), specify it here.
`target_dir` _(optional)_: Relative from the root of the project, this where the "target" or "expected" objects are located.
These are the **intended result** of the match.
`base_dir` _(optional)_: Relative from the root of the project, this is where the "base" or "actual" objects are located.
These are objects built from the **current source code**.
`build_target`: If true, objdiff will tell the build system to build the target objects before diffing (e.g.
`make path/to/target.o`).
This is useful if the target objects are not built by default or can change based on project configuration or edits
to assembly files.
Requires the build system to be configured properly.
`watch_patterns` _(optional)_: A list of glob patterns to watch for changes.
([Supported syntax](https://docs.rs/globset/latest/globset/#syntax))
If any of these files change, objdiff will automatically rebuild the objects and re-compare them.
If not specified, objdiff will use the default patterns listed above.
`objects` _(optional)_: If specified, objdiff will display a list of objects in the sidebar for easy navigation.
> `name` _(optional)_: The name of the object in the UI. If not specified, the object's `path` will be used.
>
> `path`: Relative path to the object from the `target_dir` and `base_dir`.
> Requires `target_dir` and `base_dir` to be specified.
>
> `target_path`: Path to the target object from the project root.
> Required if `path` is not specified.
>
> `base_path`: Path to the base object from the project root.
> Required if `path` is not specified.
>
> `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
Licensed under either of
@@ -23,6 +149,5 @@ at your option.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any
additional terms or conditions.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as
defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 144 KiB

View File

@@ -1,10 +1,10 @@
use anyhow::Result;
use vergen::{vergen, Config};
use vergen::EmitBuilder;
fn main() -> Result<()> {
#[cfg(windows)]
{
winres::WindowsResource::new().set_icon("assets/icon.ico").compile()?;
}
vergen(Config::default())
EmitBuilder::builder().fail_on_error().all_build().all_cargo().all_git().emit()
}

View File

@@ -47,11 +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 = [
# git2 (build dependency)
"RUSTSEC-2023-0002",
"RUSTSEC-2023-0003",
]
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.
@@ -74,6 +70,7 @@ unlicensed = "deny"
allow = [
"MIT",
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"ISC",
"BSD-2-Clause",
"BSD-3-Clause",
@@ -86,6 +83,7 @@ allow = [
"OFL-1.1",
"LicenseRef-UFL-1.0",
"OpenSSL",
"GPL-3.0",
]
# List of explictly disallowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
@@ -156,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

File diff suppressed because it is too large Load Diff

97
src/app_config.rs Normal file
View File

@@ -0,0 +1,97 @@
use std::path::PathBuf;
use eframe::Storage;
use globset::Glob;
use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY};
#[derive(Clone, serde::Deserialize, serde::Serialize)]
pub struct AppConfigVersion {
pub version: u32,
}
impl Default for AppConfigVersion {
fn default() -> Self { Self { version: 1 } }
}
/// Deserialize the AppConfig from storage, handling upgrades from older versions.
pub fn deserialize_config(storage: &dyn Storage) -> Option<AppConfig> {
let str = storage.get_string(CONFIG_KEY)?;
match ron::from_str::<AppConfigVersion>(&str) {
Ok(version) => match version.version {
1 => from_str::<AppConfig>(&str),
_ => {
log::warn!("Unknown config version: {}", version.version);
None
}
},
Err(e) => {
log::warn!("Failed to decode config version: {e}");
// Try to decode as v0
from_str::<AppConfigV0>(&str).map(|c| c.into_config())
}
}
}
fn from_str<T>(str: &str) -> Option<T>
where T: serde::de::DeserializeOwned {
match ron::from_str(str) {
Ok(config) => Some(config),
Err(err) => {
log::warn!("Failed to decode config: {err}");
None
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct ObjectConfigV0 {
pub name: String,
pub target_path: PathBuf,
pub base_path: PathBuf,
pub reverse_fn_order: Option<bool>,
}
impl ObjectConfigV0 {
fn into_config(self) -> ObjectConfig {
ObjectConfig {
name: self.name,
target_path: Some(self.target_path),
base_path: Some(self.base_path),
reverse_fn_order: self.reverse_fn_order,
complete: None,
scratch: None,
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct AppConfigV0 {
pub custom_make: Option<String>,
pub selected_wsl_distro: Option<String>,
pub project_dir: Option<PathBuf>,
pub target_obj_dir: Option<PathBuf>,
pub base_obj_dir: Option<PathBuf>,
pub selected_obj: Option<ObjectConfigV0>,
pub build_target: bool,
pub auto_update_check: bool,
pub watch_patterns: Vec<Glob>,
}
impl AppConfigV0 {
fn into_config(self) -> AppConfig {
log::info!("Upgrading configuration from v0");
AppConfig {
custom_make: self.custom_make,
selected_wsl_distro: self.selected_wsl_distro,
project_dir: self.project_dir,
target_obj_dir: self.target_obj_dir,
base_obj_dir: self.base_obj_dir,
selected_obj: self.selected_obj.map(|obj| obj.into_config()),
build_target: self.build_target,
auto_update_check: self.auto_update_check,
watch_patterns: self.watch_patterns,
..Default::default()
}
}
}

223
src/config.rs Normal file
View File

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

View File

@@ -1,711 +0,0 @@
use std::{collections::BTreeMap, mem::take};
use anyhow::Result;
use crate::{
app::DiffConfig,
editops::{editops_find, LevEditType},
obj::{
mips, ppc, ObjArchitecture, ObjDataDiff, ObjDataDiffKind, ObjInfo, ObjInsArg,
ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjInsDiff, ObjInsDiffKind, ObjReloc,
ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags,
},
};
fn no_diff_code(
arch: ObjArchitecture,
data: &[u8],
symbol: &mut ObjSymbol,
relocs: &[ObjReloc],
line_info: &Option<BTreeMap<u32, u32>>,
) -> Result<()> {
let code =
&data[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
let (_, ins) = 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 ins {
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(
arch: ObjArchitecture,
left_data: &[u8],
right_data: &[u8],
left_symbol: &mut ObjSymbol,
right_symbol: &mut ObjSymbol,
left_relocs: &[ObjReloc],
right_relocs: &[ObjReloc],
left_line_info: &Option<BTreeMap<u32, u32>>,
right_line_info: &Option<BTreeMap<u32, u32>>,
) -> Result<()> {
let left_code = &left_data[left_symbol.section_address as usize
..(left_symbol.section_address + left_symbol.size) as usize];
let right_code = &right_data[right_symbol.section_address as usize
..(right_symbol.section_address + right_symbol.size) as usize];
let ((left_ops, left_insts), (right_ops, right_insts)) = 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();
let edit_ops = editops_find(&left_ops, &right_ops);
{
let mut op_iter = edit_ops.iter();
let mut left_iter = left_insts.iter();
let mut right_iter = right_insts.iter();
let mut cur_op = op_iter.next();
let mut cur_left = left_iter.next();
let mut cur_right = right_iter.next();
while let Some(op) = cur_op {
let left_addr = op.first_start as u32 * 4;
let right_addr = op.second_start as u32 * 4;
while let (Some(left), Some(right)) = (cur_left, cur_right) {
if (left.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();
}
}
resolve_branches(&mut left_diff);
resolve_branches(&mut right_diff);
let mut diff_state = InsDiffState::default();
for (left, right) in left_diff.iter_mut().zip(right_diff.iter_mut()) {
let result = compare_ins(left, right, &mut diff_state)?;
left.kind = result.kind;
right.kind = result.kind;
left.arg_diff = result.left_args_diff;
right.arg_diff = result.right_args_diff;
}
let total = left_insts.len();
let percent = 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 resolve_branches(vec: &mut [ObjInsDiff]) {
let mut branch_idx = 0usize;
// Map addresses to indices
let mut addr_map = BTreeMap::<u32, usize>::new();
for (i, ins_diff) in vec.iter().enumerate() {
if let Some(ins) = &ins_diff.ins {
addr_map.insert(ins.address, i);
}
}
// Generate branches
let mut branches = BTreeMap::<usize, ObjInsBranchFrom>::new();
for (i, ins_diff) in vec.iter_mut().enumerate() {
if let Some(ins) = &ins_diff.ins {
// if ins.ins.is_blr() || ins.reloc.is_some() {
// continue;
// }
if let Some(ins_idx) = ins
.args
.iter()
.find_map(|a| if let ObjInsArg::BranchOffset(offs) = a { Some(offs) } else { None })
.and_then(|offs| addr_map.get(&((ins.address as i32 + offs) as u32)))
{
if let Some(branch) = branches.get_mut(ins_idx) {
ins_diff.branch_to =
Some(ObjInsBranchTo { ins_idx: *ins_idx, branch_idx: branch.branch_idx });
branch.ins_idx.push(i);
} else {
ins_diff.branch_to = Some(ObjInsBranchTo { ins_idx: *ins_idx, branch_idx });
branches.insert(*ins_idx, ObjInsBranchFrom { ins_idx: vec![i], branch_idx });
branch_idx += 1;
}
}
}
}
// Store branch from
for (i, branch) in branches {
vec[i].branch_from = Some(branch);
}
}
fn address_eq(left: &ObjSymbol, right: &ObjSymbol) -> bool {
left.address as i64 + left.addend == right.address as i64 + right.addend
}
fn reloc_eq(left_reloc: Option<&ObjReloc>, right_reloc: Option<&ObjReloc>) -> bool {
let (Some(left), Some(right)) = (left_reloc, right_reloc) else {
return false;
};
if left.kind != right.kind {
return false;
}
let name_matches = left.target.name == right.target.name;
match (&left.target_section, &right.target_section) {
(Some(sl), Some(sr)) => {
// Match if section and name or address match
sl == sr && (name_matches || address_eq(&left.target, &right.target))
}
(Some(_), None) => false,
(None, Some(_)) => {
// Match if possibly stripped weak symbol
name_matches && right.target.flags.0.contains(ObjSymbolFlags::Weak)
}
(None, None) => name_matches,
}
}
fn arg_eq(
left: &ObjInsArg,
right: &ObjInsArg,
left_diff: &ObjInsDiff,
right_diff: &ObjInsDiff,
) -> bool {
return match left {
ObjInsArg::PpcArg(l) => match right {
ObjInsArg::PpcArg(r) => format!("{l}") == format!("{r}"),
_ => false,
},
ObjInsArg::Reloc => {
matches!(right, ObjInsArg::Reloc)
&& reloc_eq(
left_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
)
}
ObjInsArg::RelocWithBase => {
matches!(right, ObjInsArg::RelocWithBase)
&& reloc_eq(
left_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
)
}
ObjInsArg::MipsArg(ls) | ObjInsArg::MipsArgWithBase(ls) => {
matches!(right, ObjInsArg::MipsArg(rs) | ObjInsArg::MipsArgWithBase(rs) if ls == rs)
}
ObjInsArg::BranchOffset(_) => {
// Compare dest instruction idx after diffing
left_diff.branch_to.as_ref().map(|b| b.ins_idx)
== right_diff.branch_to.as_ref().map(|b| b.ins_idx)
}
};
}
#[derive(Default)]
struct InsDiffState {
diff_count: usize,
left_arg_idx: usize,
right_arg_idx: usize,
left_args_idx: BTreeMap<String, usize>,
right_args_idx: BTreeMap<String, usize>,
}
#[derive(Default)]
struct InsDiffResult {
kind: ObjInsDiffKind,
left_args_diff: Vec<Option<ObjInsArgDiff>>,
right_args_diff: Vec<Option<ObjInsArgDiff>>,
}
fn compare_ins(
left: &ObjInsDiff,
right: &ObjInsDiff,
state: &mut InsDiffState,
) -> Result<InsDiffResult> {
let mut result = InsDiffResult::default();
if let (Some(left_ins), Some(right_ins)) = (&left.ins, &right.ins) {
if left_ins.args.len() != right_ins.args.len() || left_ins.op != right_ins.op {
// Totally different op
result.kind = ObjInsDiffKind::Replace;
state.diff_count += 1;
return Ok(result);
}
if left_ins.mnemonic != right_ins.mnemonic {
// Same op but different mnemonic, still cmp args
result.kind = ObjInsDiffKind::OpMismatch;
state.diff_count += 1;
}
for (a, b) in left_ins.args.iter().zip(&right_ins.args) {
if arg_eq(a, b, left, right) {
result.left_args_diff.push(None);
result.right_args_diff.push(None);
} else {
if result.kind == ObjInsDiffKind::None {
result.kind = ObjInsDiffKind::ArgMismatch;
state.diff_count += 1;
}
let a_str = match a {
ObjInsArg::PpcArg(arg) => format!("{arg}"),
ObjInsArg::Reloc | ObjInsArg::RelocWithBase => String::new(),
ObjInsArg::MipsArg(str) | ObjInsArg::MipsArgWithBase(str) => str.clone(),
ObjInsArg::BranchOffset(arg) => format!("{arg}"),
};
let a_diff = if let Some(idx) = state.left_args_idx.get(&a_str) {
ObjInsArgDiff { idx: *idx }
} else {
let idx = state.left_arg_idx;
state.left_args_idx.insert(a_str, idx);
state.left_arg_idx += 1;
ObjInsArgDiff { idx }
};
let b_str = match b {
ObjInsArg::PpcArg(arg) => format!("{arg}"),
ObjInsArg::Reloc | ObjInsArg::RelocWithBase => String::new(),
ObjInsArg::MipsArg(str) | ObjInsArg::MipsArgWithBase(str) => str.clone(),
ObjInsArg::BranchOffset(arg) => format!("{arg}"),
};
let b_diff = if let Some(idx) = state.right_args_idx.get(&b_str) {
ObjInsArgDiff { idx: *idx }
} else {
let idx = state.right_arg_idx;
state.right_args_idx.insert(b_str, idx);
state.right_arg_idx += 1;
ObjInsArgDiff { idx }
};
result.left_args_diff.push(Some(a_diff));
result.right_args_diff.push(Some(b_diff));
}
}
} else if left.ins.is_some() {
result.kind = ObjInsDiffKind::Delete;
state.diff_count += 1;
} else {
result.kind = ObjInsDiffKind::Insert;
state.diff_count += 1;
}
Ok(result)
}
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
}
pub fn diff_objs(left: &mut ObjInfo, right: &mut ObjInfo, _diff_config: &DiffConfig) -> Result<()> {
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_section_idx, right_symbol_idx)) =
find_section_and_symbol(right, &left_symbol.name)
{
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(
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 {
let Some(right_section) = right.sections.iter_mut().find(|s| s.name == left_section.name) else {
continue;
};
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)?;
}
}
}
for right_section in right.sections.iter_mut().filter(|s| s.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,
)?;
}
}
}
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) = 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)]
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(())
}
fn diff_data(left: &mut ObjSection, right: &mut ObjSection) {
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;
}
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;
}

489
src/diff/code.rs Normal file
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
src/diff/data.rs Normal file
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(),
}];
}

View File

@@ -76,18 +76,15 @@ where T: PartialEq {
cache_matrix[current + 1 + p] = x;
}
}
editops_from_cost_matrix::<T>(matrix_columns, matrix_rows, prefix_len, cache_matrix)
editops_from_cost_matrix(matrix_columns, matrix_rows, prefix_len, cache_matrix)
}
fn editops_from_cost_matrix<T>(
fn editops_from_cost_matrix(
len1: usize,
len2: usize,
prefix_len: usize,
cache_matrix: Vec<usize>,
) -> Vec<LevEditOp>
where
T: PartialEq,
{
) -> Vec<LevEditOp> {
let mut ops = Vec::with_capacity(cache_matrix[len1 * len2 - 1]);
let mut dir = 0;
let mut i = len1 - 1;

115
src/diff/mod.rs Normal file
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
src/fonts/matching.rs Normal file
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
src/fonts/mod.rs Normal file
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(())
}

View File

@@ -1,44 +0,0 @@
use std::sync::{mpsc::Receiver, Arc, RwLock};
use anyhow::{Error, Result};
use crate::{
app::{AppConfig, DiffConfig},
diff::diff_objs,
jobs::{queue_job, update_status, Job, JobResult, JobState, Status},
obj::{elf, ObjInfo},
};
pub struct BinDiffResult {
pub first_obj: ObjInfo,
pub second_obj: ObjInfo,
}
fn run_build(
status: &Status,
cancel: Receiver<()>,
config: Arc<RwLock<AppConfig>>,
) -> Result<Box<BinDiffResult>> {
let config = config.read().map_err(|_| Error::msg("Failed to lock app config"))?.clone();
let target_path =
config.left_obj.as_ref().ok_or_else(|| Error::msg("Missing target obj path"))?;
let base_path = config.right_obj.as_ref().ok_or_else(|| Error::msg("Missing base obj path"))?;
update_status(status, "Loading target obj".to_string(), 0, 3, &cancel)?;
let mut left_obj = elf::read(target_path)?;
update_status(status, "Loading base obj".to_string(), 1, 3, &cancel)?;
let mut right_obj = elf::read(base_path)?;
update_status(status, "Performing diff".to_string(), 2, 3, &cancel)?;
diff_objs(&mut left_obj, &mut right_obj, &DiffConfig::default() /* TODO */)?;
update_status(status, "Complete".to_string(), 3, 3, &cancel)?;
Ok(Box::new(BinDiffResult { first_obj: left_obj, second_obj: right_obj }))
}
pub fn queue_bindiff(config: Arc<RwLock<AppConfig>>) -> JobState {
queue_job("Binary diff", Job::BinDiff, move |status, cancel| {
run_build(status, cancel, config).map(JobResult::BinDiff)
})
}

View File

@@ -4,7 +4,7 @@ use anyhow::{Context, Result};
use self_update::{cargo_crate_version, update::Release};
use crate::{
jobs::{queue_job, update_status, Job, JobResult, JobState, Status},
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: &Status, 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 queue_check_update() -> JobState {
queue_job("Check for updates", Job::CheckUpdate, move |status, cancel| {
run_check_update(status, cancel).map(JobResult::CheckUpdate)
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
src/jobs/create_scratch.rs Normal file
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)))
})
}

View File

@@ -10,31 +10,106 @@ use std::{
use anyhow::Result;
use crate::jobs::{
bindiff::BinDiffResult, check_update::CheckUpdateResult, objdiff::ObjDiffResult,
check_update::CheckUpdateResult, create_scratch::CreateScratchResult, objdiff::ObjDiffResult,
update::UpdateResult,
};
pub mod bindiff;
pub mod check_update;
pub mod create_scratch;
pub mod objdiff;
pub mod update;
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum Job {
ObjDiff,
BinDiff,
CheckUpdate,
Update,
CreateScratch,
}
pub static JOB_ID: AtomicUsize = AtomicUsize::new(0);
#[derive(Default)]
pub struct JobQueue {
pub jobs: Vec<JobState>,
pub results: Vec<JobResult>,
}
impl JobQueue {
/// Adds a job to the queue.
#[inline]
pub fn push(&mut self, state: JobState) { self.jobs.push(state); }
/// Adds a job to the queue if a job of the given kind is not already running.
#[inline]
pub fn push_once(&mut self, job: Job, func: impl FnOnce() -> JobState) {
if !self.is_running(job) {
self.push(func());
}
}
/// Returns whether a job of the given kind is running.
pub fn is_running(&self, kind: Job) -> bool {
self.jobs.iter().any(|j| j.kind == kind && j.handle.is_some())
}
/// 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 {
return !handle.is_finished();
}
false
})
}
/// Iterates over all jobs mutably.
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut JobState> + '_ { self.jobs.iter_mut() }
/// Iterates over all finished jobs, returning the job state and the result.
pub fn iter_finished(
&mut self,
) -> impl Iterator<Item = (&mut JobState, std::thread::Result<JobResult>)> + '_ {
self.jobs.iter_mut().filter_map(|job| {
if let Some(handle) = &job.handle {
if !handle.is_finished() {
return None;
}
let result = job.handle.take().unwrap().join();
return Some((job, result));
}
None
})
}
/// Clears all finished jobs.
pub fn clear_finished(&mut self) {
self.jobs.retain(|job| {
!(job.should_remove
&& job.handle.is_none()
&& job.context.status.read().unwrap().error.is_none())
});
}
/// Removes a job from the queue given its ID.
pub fn remove(&mut self, id: usize) { self.jobs.retain(|job| job.id != id); }
}
#[derive(Clone)]
pub struct JobContext {
pub status: Arc<RwLock<JobStatus>>,
pub egui: egui::Context,
}
pub struct JobState {
pub id: usize,
pub job_type: Job,
pub kind: Job,
pub handle: Option<JoinHandle<JobResult>>,
pub status: Arc<RwLock<JobStatus>>,
pub context: JobContext,
pub cancel: Sender<()>,
pub should_remove: bool,
}
#[derive(Default)]
pub struct JobStatus {
pub title: String,
@@ -43,12 +118,13 @@ pub struct JobStatus {
pub status: String,
pub error: Option<anyhow::Error>,
}
pub enum JobResult {
None,
ObjDiff(Box<ObjDiffResult>),
BinDiff(Box<BinDiffResult>),
CheckUpdate(Box<CheckUpdateResult>),
ObjDiff(Option<Box<ObjDiffResult>>),
CheckUpdate(Option<Box<CheckUpdateResult>>),
Update(Box<UpdateResult>),
CreateScratch(Option<Box<CreateScratchResult>>),
}
fn should_cancel(rx: &Receiver<()>) -> bool {
@@ -58,12 +134,11 @@ fn should_cancel(rx: &Receiver<()>) -> bool {
}
}
type Status = Arc<RwLock<JobStatus>>;
fn queue_job(
fn start_job(
ctx: &egui::Context,
title: &str,
job_type: Job,
run: impl FnOnce(&Status, Receiver<()>) -> Result<JobResult> + Send + 'static,
kind: Job,
run: impl FnOnce(JobContext, Receiver<()>) -> Result<JobResult> + Send + 'static,
) -> JobState {
let status = Arc::new(RwLock::new(JobStatus {
title: title.to_string(),
@@ -72,10 +147,11 @@ fn queue_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() {
@@ -87,24 +163,18 @@ fn queue_job(
});
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
log::info!("Started job {}", id);
JobState {
id,
job_type,
handle: Some(handle),
status: status_clone,
cancel: tx,
should_remove: true,
}
JobState { id, kind, handle: Some(handle), context, cancel: tx, should_remove: true }
}
fn update_status(
status: &Status,
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) {
@@ -113,5 +183,7 @@ fn update_status(
} else {
w.status = str;
}
drop(w);
context.egui.request_repaint();
Ok(())
}

View File

@@ -1,23 +1,77 @@
use std::{
path::Path,
path::{Path, PathBuf},
process::Command,
str::from_utf8,
sync::{mpsc::Receiver, Arc, RwLock},
sync::mpsc::Receiver,
};
use anyhow::{Context, Error, Result};
use anyhow::{anyhow, Context, Error, Result};
use time::OffsetDateTime;
use crate::{
app::{AppConfig, DiffConfig},
diff::diff_objs,
jobs::{queue_job, update_status, Job, JobResult, JobState, Status},
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 {
@@ -28,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))]
@@ -62,82 +123,146 @@ 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: &Status,
context: &JobContext,
cancel: Receiver<()>,
config: Arc<RwLock<AppConfig>>,
diff_config: DiffConfig,
config: ObjDiffConfig,
) -> Result<Box<ObjDiffResult>> {
let config = config.read().map_err(|_| Error::msg("Failed to lock app config"))?.clone();
let obj_path = config.obj_path.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 mut target_path = config
.target_obj_dir
let obj_config = config.selected_obj.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?;
let project_dir = config
.build_config
.project_dir
.as_ref()
.ok_or_else(|| Error::msg("Missing target obj dir"))?
.to_owned();
target_path.push(obj_path);
let mut base_path =
config.base_obj_dir.as_ref().ok_or_else(|| Error::msg("Missing base obj dir"))?.to_owned();
base_path.push(obj_path);
let target_path_rel = target_path
.strip_prefix(project_dir)
.context("Failed to create relative target obj path")?;
let base_path_rel =
base_path.strip_prefix(project_dir).context("Failed to create relative base obj path")?;
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)?;
run_make(project_dir, target_path_rel, &config)
.ok_or_else(|| Error::msg("Missing project dir"))?;
let target_path_rel = if let Some(target_path) = &obj_config.target_path {
Some(target_path.strip_prefix(project_dir).map_err(|_| {
anyhow!(
"Target path '{}' doesn't begin with '{}'",
target_path.display(),
project_dir.display()
)
})?)
} else {
BuildStatus { success: true, log: String::new() }
None
};
let base_path_rel = if let Some(base_path) = &obj_config.base_path {
Some(base_path.strip_prefix(project_dir).map_err(|_| {
anyhow!(
"Base path '{}' doesn't begin with '{}'",
base_path.display(),
project_dir.display()
)
})?)
} else {
None
};
update_status(status, format!("Building base {obj_path}"), 1, total, &cancel)?;
let second_status = run_make(project_dir, base_path_rel, &config);
let mut total = 3;
if config.build_target && target_path_rel.is_some() {
total += 1;
}
if config.build_base && base_path_rel.is_some() {
total += 1;
}
let first_status = match target_path_rel {
Some(target_path_rel) if config.build_target => {
update_status(
context,
format!("Building target {}", target_path_rel.display()),
0,
total,
&cancel,
)?;
run_make(&config.build_config, target_path_rel)
}
_ => BuildStatus::default(),
};
let second_status = match base_path_rel {
Some(base_path_rel) if config.build_base => {
update_status(
context,
format!("Building base {}", base_path_rel.display()),
0,
total,
&cancel,
)?;
run_make(&config.build_config, base_path_rel)
}
_ => BuildStatus::default(),
};
let time = OffsetDateTime::now_utc();
let mut first_obj = if first_status.success {
update_status(status, format!("Loading target {obj_path}"), 2, total, &cancel)?;
Some(elf::read(&target_path)?)
} else {
None
let mut first_obj =
match &obj_config.target_path {
Some(target_path) if first_status.success => {
update_status(
context,
format!("Loading target {}", target_path_rel.unwrap().display()),
2,
total,
&cancel,
)?;
Some(elf::read(target_path).with_context(|| {
format!("Failed to read object '{}'", target_path.display())
})?)
}
_ => None,
};
let mut second_obj = match &obj_config.base_path {
Some(base_path) if second_status.success => {
update_status(
context,
format!("Loading base {}", base_path_rel.unwrap().display()),
3,
total,
&cancel,
)?;
Some(
elf::read(base_path)
.with_context(|| format!("Failed to read object '{}'", base_path.display()))?,
)
}
_ => None,
};
let mut second_obj = if second_status.success {
update_status(status, format!("Loading base {obj_path}"), 3, total, &cancel)?;
Some(elf::read(&base_path)?)
} else {
None
update_status(context, "Performing diff".to_string(), 4, total, &cancel)?;
let diff_config = DiffObjConfig {
code_alg: config.code_alg,
data_alg: config.data_alg,
relax_reloc_diffs: config.relax_reloc_diffs,
};
diff_objs(&diff_config, first_obj.as_mut(), second_obj.as_mut())?;
if let (Some(first_obj), Some(second_obj)) = (&mut first_obj, &mut second_obj) {
update_status(status, "Performing diff".to_string(), 4, total, &cancel)?;
diff_objs(first_obj, second_obj, &diff_config)?;
}
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 queue_build(config: Arc<RwLock<AppConfig>>, diff_config: DiffConfig) -> JobState {
queue_job("Object diff", Job::ObjDiff, move |status, cancel| {
run_build(status, cancel, config, diff_config).map(JobResult::ObjDiff)
pub fn start_build(ctx: &egui::Context, config: ObjDiffConfig) -> JobState {
start_job(ctx, "Object diff", Job::ObjDiff, move |context, cancel| {
run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
})
}

View File

@@ -9,7 +9,7 @@ use anyhow::{Context, Result};
use const_format::formatcp;
use crate::{
jobs::{queue_job, update_status, Job, JobResult, JobState, Status},
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: &Status, 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: &Status, cancel: Receiver<()>) -> Result<Box<UpdateResult>
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)
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)
})
}

View File

@@ -3,8 +3,10 @@
pub use app::App;
mod app;
mod app_config;
mod config;
mod diff;
mod editops;
mod fonts;
mod jobs;
mod obj;
mod update;

View File

@@ -1,14 +1,17 @@
#![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 std::{
path::PathBuf,
rc::Rc,
sync::{Arc, Mutex},
};
use anyhow::{Error, Result};
use cfg_if::cfg_if;
use eframe::IconData;
use time::UtcOffset;
fn load_icon() -> Result<IconData> {
fn load_icon() -> Result<egui::IconData> {
use bytes::Buf;
let decoder = png::Decoder::new(include_bytes!("../assets/icon_64.png").reader());
let mut reader = decoder.read_info()?;
@@ -21,7 +24,7 @@ fn load_icon() -> Result<IconData> {
return Err(Error::msg("Invalid color type"));
}
buf.truncate(info.buffer_size());
Ok(IconData { rgba: buf, width: info.width, height: info.height })
Ok(egui::IconData { rgba: buf, width: info.width, height: info.height })
}
// When compiling natively:
@@ -37,10 +40,11 @@ fn main() {
let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None));
let exec_path_clone = exec_path.clone();
let mut native_options = eframe::NativeOptions::default();
let mut native_options =
eframe::NativeOptions { follow_system_theme: false, ..Default::default() };
match load_icon() {
Ok(data) => {
native_options.icon_data = Some(data);
native_options.viewport.icon = Some(Arc::new(data));
}
Err(e) => {
log::warn!("Failed to load application icon: {}", e);
@@ -54,7 +58,8 @@ fn main() {
"objdiff",
native_options,
Box::new(move |cc| Box::new(objdiff::App::new(cc, utc_offset, exec_path_clone))),
);
)
.expect("Failed to run eframe application");
// Attempt to relaunch application from the updated path
if let Ok(mut guard) = exec_path.lock() {
@@ -64,7 +69,7 @@ fn main() {
let result = exec::Command::new(path)
.args(&std::env::args().collect::<Vec<String>>())
.exec();
eprintln!("Failed to relaunch: {result:?}");
log::error!("Failed to relaunch: {result:?}");
} else {
let result = std::process::Command::new(path)
.args(std::env::args())
@@ -72,7 +77,7 @@ fn main() {
.unwrap()
.wait();
if let Err(e) = result {
eprintln!("Failed to relaunch: {:?}", e);
log::error!("Failed to relaunch: {:?}", e);
}
}
}

View File

@@ -1,12 +1,13 @@
use std::{collections::BTreeMap, fs, io::Cursor, path::Path};
use std::{borrow::Cow, collections::BTreeMap, fs, io::Cursor, path::Path};
use anyhow::{anyhow, bail, Context, Result};
use byteorder::{BigEndian, ReadBytesExt};
use cwdemangle::demangle;
use filetime::FileTime;
use flagset::Flags;
use object::{
elf, Architecture, File, Object, ObjectSection, ObjectSymbol, RelocationKind, RelocationTarget,
SectionIndex, SectionKind, Symbol, SymbolKind, SymbolSection,
elf, Architecture, Endianness, File, Object, ObjectSection, ObjectSymbol, RelocationKind,
RelocationTarget, SectionIndex, SectionKind, Symbol, SymbolKind, SymbolScope, SymbolSection,
};
use crate::obj::{
@@ -26,7 +27,7 @@ fn to_obj_section_kind(kind: SectionKind) -> Option<ObjSectionKind> {
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:?}");
log::warn!("Found empty sym: {symbol:?}");
name = "?";
}
let mut flags = ObjSymbolFlagSet(ObjSymbolFlags::none());
@@ -42,6 +43,9 @@ fn to_obj_symbol(obj_file: &File<'_>, symbol: &Symbol<'_, '_>, addend: i64) -> R
if symbol.is_weak() {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Weak);
}
if symbol.scope() == SymbolScope::Linkage {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Hidden);
}
let section_address = if let Some(section) =
symbol.section_index().and_then(|idx| obj_file.section_by_index(idx).ok())
{
@@ -126,7 +130,8 @@ fn symbols_by_section(obj_file: &File<'_>, section: &ObjSection) -> Result<Vec<O
}
fn common_symbols(obj_file: &File<'_>) -> Result<Vec<ObjSymbol>> {
obj_file.symbols()
obj_file
.symbols()
.filter(Symbol::is_common)
.map(|symbol| to_obj_symbol(obj_file, &symbol, 0))
.collect::<Result<Vec<ObjSymbol>>>()
@@ -180,7 +185,7 @@ fn find_section_symbol(
fn relocations_by_section(
arch: ObjArchitecture,
obj_file: &File<'_>,
section: &mut ObjSection,
section: &ObjSection,
) -> Result<Vec<ObjReloc>> {
let obj_section = obj_file.section_by_index(SectionIndex(section.index))?;
let mut relocations = Vec::<ObjReloc>::new();
@@ -273,7 +278,9 @@ fn relocations_by_section(
Ok(relocations)
}
fn line_info(obj_file: &File<'_>) -> Result<Option<BTreeMap<u32, u32>>> {
fn line_info(obj_file: &File<'_>) -> Result<Option<BTreeMap<u64, u64>>> {
// DWARF 1.1
let mut map = BTreeMap::new();
if let Some(section) = obj_file.section_by_name(".line") {
if section.size() == 0 {
return Ok(None);
@@ -281,28 +288,56 @@ fn line_info(obj_file: &File<'_>) -> Result<Option<BTreeMap<u32, u32>>> {
let data = section.uncompressed_data()?;
let mut reader = Cursor::new(data.as_ref());
let mut map = BTreeMap::new();
let size = reader.read_u32::<BigEndian>()?;
let base_address = reader.read_u32::<BigEndian>()?;
let base_address = reader.read_u32::<BigEndian>()? as u64;
while reader.position() < size as u64 {
let line_number = reader.read_u32::<BigEndian>()?;
let line_number = reader.read_u32::<BigEndian>()? as u64;
let statement_pos = reader.read_u16::<BigEndian>()?;
if statement_pos != 0xFFFF {
log::warn!("Unhandled statement pos {}", statement_pos);
}
let address_delta = reader.read_u32::<BigEndian>()?;
let address_delta = reader.read_u32::<BigEndian>()? as u64;
map.insert(base_address + address_delta, line_number);
}
println!("Line info: {map:#X?}");
return Ok(Some(map));
}
Ok(None)
// DWARF 2+
let dwarf_cow = gimli::Dwarf::load(|id| {
Ok::<_, gimli::Error>(
obj_file
.section_by_name(id.name())
.and_then(|section| section.uncompressed_data().ok())
.unwrap_or(Cow::Borrowed(&[][..])),
)
})?;
let endian = match obj_file.endianness() {
Endianness::Little => gimli::RunTimeEndian::Little,
Endianness::Big => gimli::RunTimeEndian::Big,
};
let dwarf = dwarf_cow.borrow(|section| gimli::EndianSlice::new(section, endian));
let mut iter = dwarf.units();
while let Some(header) = iter.next()? {
let unit = dwarf.unit(header)?;
if let Some(program) = unit.line_program.clone() {
let mut rows = program.rows();
while let Some((_header, row)) = rows.next_row()? {
if let Some(line) = row.line() {
map.insert(row.address(), line.get());
}
}
}
}
if map.is_empty() {
return Ok(None);
}
Ok(Some(map))
}
pub fn read(obj_path: &Path) -> Result<ObjInfo> {
let data = {
let (data, timestamp) = {
let file = fs::File::open(obj_path)?;
unsafe { memmap2::Mmap::map(&file) }?
let timestamp = FileTime::from_last_modification_time(&file.metadata()?);
(unsafe { memmap2::Mmap::map(&file) }?, timestamp)
};
let obj_file = File::parse(&*data)?;
let architecture = match obj_file.architecture() {
@@ -318,6 +353,7 @@ pub fn read(obj_path: &Path) -> Result<ObjInfo> {
let mut result = ObjInfo {
architecture,
path: obj_path.to_owned(),
timestamp,
sections: filter_sections(&obj_file)?,
common: common_symbols(&obj_file)?,
line_info: line_info(&obj_file)?,

View File

@@ -3,7 +3,10 @@ use std::collections::BTreeMap;
use anyhow::Result;
use rabbitizer::{config, Abi, InstrCategory, Instruction, OperandType};
use crate::obj::{ObjIns, ObjInsArg, ObjReloc};
use crate::{
diff::ProcessCodeResult,
obj::{ObjIns, ObjInsArg, ObjReloc},
};
fn configure_rabbitizer() {
unsafe {
@@ -16,8 +19,8 @@ pub fn process_code(
start_address: u64,
end_address: u64,
relocs: &[ObjReloc],
line_info: &Option<BTreeMap<u32, u32>>,
) -> Result<(Vec<u8>, Vec<ObjIns>)> {
line_info: &Option<BTreeMap<u64, u64>>,
) -> Result<ProcessCodeResult> {
configure_rabbitizer();
let ins_count = data.len() / 4;
@@ -80,8 +83,9 @@ pub fn process_code(
}
}
}
let line =
line_info.as_ref().and_then(|map| map.range(..=cur_addr).last().map(|(_, &b)| b));
let line = line_info
.as_ref()
.and_then(|map| map.range(..=cur_addr as u64).last().map(|(_, &b)| b));
insts.push(ObjIns {
address: cur_addr,
code,
@@ -91,8 +95,9 @@ pub fn process_code(
reloc: reloc.cloned(),
branch_dest,
line,
orig: None,
});
cur_addr += 4;
}
Ok((ops, insts))
Ok(ProcessCodeResult { ops, insts })
}

View File

@@ -4,6 +4,7 @@ pub mod ppc;
use std::{collections::BTreeMap, path::PathBuf};
use filetime::FileTime;
use flagset::{flags, FlagSet};
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
@@ -18,6 +19,7 @@ flags! {
Local,
Weak,
Common,
Hidden,
}
}
#[derive(Debug, Copy, Clone, Default)]
@@ -37,7 +39,8 @@ pub struct ObjSection {
pub data_diff: Vec<ObjDataDiff>,
pub match_percent: f32,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ObjInsArg {
PpcArg(ppc750cl::Argument),
MipsArg(String),
@@ -46,6 +49,41 @@ pub enum ObjInsArg {
RelocWithBase,
BranchOffset(i32),
}
impl ObjInsArg {
pub fn loose_eq(&self, other: &ObjInsArg) -> bool {
match (self, other) {
(ObjInsArg::PpcArg(a), ObjInsArg::PpcArg(b)) => {
a == b
|| match (a, b) {
// Consider Simm and Offset equivalent
(ppc750cl::Argument::Simm(simm), ppc750cl::Argument::Offset(off))
| (ppc750cl::Argument::Offset(off), ppc750cl::Argument::Simm(simm)) => {
simm.0 == off.0
}
// Consider Uimm and Offset equivalent
(ppc750cl::Argument::Uimm(uimm), ppc750cl::Argument::Offset(off))
| (ppc750cl::Argument::Offset(off), ppc750cl::Argument::Uimm(uimm)) => {
uimm.0 == off.0 as u16
}
// Consider Uimm and Simm equivalent
(ppc750cl::Argument::Uimm(uimm), ppc750cl::Argument::Simm(simm))
| (ppc750cl::Argument::Simm(simm), ppc750cl::Argument::Uimm(uimm)) => {
uimm.0 == simm.0 as u16
}
_ => false,
}
}
(ObjInsArg::MipsArg(a), ObjInsArg::MipsArg(b)) => a == b,
(ObjInsArg::MipsArgWithBase(a), ObjInsArg::MipsArgWithBase(b)) => a == b,
(ObjInsArg::Reloc, ObjInsArg::Reloc) => true,
(ObjInsArg::RelocWithBase, ObjInsArg::RelocWithBase) => true,
(ObjInsArg::BranchOffset(a), ObjInsArg::BranchOffset(b)) => a == b,
_ => false,
}
}
}
#[derive(Debug, Copy, Clone)]
pub struct ObjInsArgDiff {
/// Incrementing index for coloring
@@ -85,7 +123,9 @@ pub struct ObjIns {
pub reloc: Option<ObjReloc>,
pub branch_dest: Option<u32>,
/// Line info
pub line: Option<u32>,
pub line: Option<u64>,
/// Original (unsimplified) instruction
pub orig: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ObjInsDiff {
@@ -139,9 +179,10 @@ pub enum ObjArchitecture {
pub struct ObjInfo {
pub architecture: ObjArchitecture,
pub path: PathBuf,
pub timestamp: FileTime,
pub sections: Vec<ObjSection>,
pub common: Vec<ObjSymbol>,
pub line_info: Option<BTreeMap<u32, u32>>,
pub line_info: Option<BTreeMap<u64, u64>>,
}
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum ObjRelocKind {

View File

@@ -1,9 +1,12 @@
use std::collections::BTreeMap;
use anyhow::Result;
use ppc750cl::{disasm_iter, Argument};
use ppc750cl::{disasm_iter, Argument, SimplifiedIns};
use crate::obj::{ObjIns, ObjInsArg, ObjReloc, ObjRelocKind};
use crate::{
diff::ProcessCodeResult,
obj::{ObjIns, ObjInsArg, ObjReloc, ObjRelocKind},
};
// Relative relocation, can be Simm or BranchOffset
fn is_relative_arg(arg: &ObjInsArg) -> bool {
@@ -21,8 +24,8 @@ pub fn process_code(
data: &[u8],
address: u64,
relocs: &[ObjReloc],
line_info: &Option<BTreeMap<u32, u32>>,
) -> Result<(Vec<u8>, Vec<ObjIns>)> {
line_info: &Option<BTreeMap<u64, u64>>,
) -> Result<ProcessCodeResult> {
let ins_count = data.len() / 4;
let mut ops = Vec::<u8>::with_capacity(ins_count);
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
@@ -40,7 +43,7 @@ pub fn process_code(
_ => ins.code,
};
}
let simplified = ins.simplified();
let simplified = ins.clone().simplified();
let mut args: Vec<ObjInsArg> = simplified
.args
.iter()
@@ -79,17 +82,18 @@ pub fn process_code(
ops.push(simplified.ins.op as u8);
let line = line_info
.as_ref()
.and_then(|map| map.range(..=simplified.ins.addr).last().map(|(_, &b)| b));
.and_then(|map| map.range(..=simplified.ins.addr as u64).last().map(|(_, &b)| b));
insts.push(ObjIns {
address: simplified.ins.addr,
code: simplified.ins.code,
mnemonic: format!("{}{}", simplified.mnemonic, simplified.suffix),
args,
reloc: reloc.cloned(),
op: 0,
op: ins.op as u8,
branch_dest: None,
line,
orig: Some(format!("{}", SimplifiedIns::basic_form(ins))),
});
}
Ok((ops, insts))
Ok(ProcessCodeResult { ops, insts })
}

302
src/views/appearance.rs Normal file
View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,16 @@
use std::{cmp::min, default::Default, mem::take};
use egui::{text::LayoutJob, Align, Color32, Label, Layout, Sense, Vec2};
use egui::{text::LayoutJob, Align, Label, Layout, Sense, Vec2};
use egui_extras::{Column, TableBuilder};
use time::format_description;
use crate::{
app::{SymbolReference, View, ViewConfig, ViewState},
jobs::Job,
obj::{ObjDataDiff, ObjDataDiffKind, ObjInfo, ObjSection},
views::{write_text, COLOR_RED},
views::{
appearance::Appearance,
symbol_diff::{DiffViewState, SymbolReference, View},
write_text,
},
};
const BYTES_PER_ROW: usize = 16;
@@ -17,29 +19,29 @@ fn find_section<'a>(obj: &'a ObjInfo, selected_symbol: &SymbolReference) -> Opti
obj.sections.iter().find(|section| section.name == selected_symbol.section_name)
}
fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], config: &ViewConfig) {
fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appearance: &Appearance) {
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!("{address:08X}: ").as_str(),
Color32::GRAY,
appearance.text_color,
&mut job,
config.code_font.clone(),
appearance.code_font.clone(),
);
let mut cur_addr = 0usize;
for diff in diffs {
let base_color = match diff.kind {
ObjDataDiffKind::None => Color32::GRAY,
ObjDataDiffKind::Replace => Color32::LIGHT_BLUE,
ObjDataDiffKind::Delete => COLOR_RED,
ObjDataDiffKind::Insert => Color32::GREEN,
ObjDataDiffKind::None => appearance.text_color,
ObjDataDiffKind::Replace => appearance.replace_color,
ObjDataDiffKind::Delete => appearance.delete_color,
ObjDataDiffKind::Insert => appearance.insert_color,
};
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, config.code_font.clone());
write_text(str.as_str(), base_color, &mut job, appearance.code_font.clone());
cur_addr += diff.len;
} else {
let mut text = String::new();
@@ -50,7 +52,7 @@ fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], config:
text.push(' ');
}
}
write_text(text.as_str(), base_color, &mut job, config.code_font.clone());
write_text(text.as_str(), base_color, &mut job, appearance.code_font.clone());
}
}
if cur_addr < BYTES_PER_ROW {
@@ -58,22 +60,22 @@ fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], config:
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, config.code_font.clone());
write_text(str.as_str(), appearance.text_color, &mut job, appearance.code_font.clone());
}
write_text(" ", Color32::GRAY, &mut job, config.code_font.clone());
write_text(" ", appearance.text_color, &mut job, appearance.code_font.clone());
for diff in diffs {
let base_color = match diff.kind {
ObjDataDiffKind::None => Color32::GRAY,
ObjDataDiffKind::Replace => Color32::LIGHT_BLUE,
ObjDataDiffKind::Delete => COLOR_RED,
ObjDataDiffKind::Insert => Color32::GREEN,
ObjDataDiffKind::None => appearance.text_color,
ObjDataDiffKind::Replace => appearance.replace_color,
ObjDataDiffKind::Delete => appearance.delete_color,
ObjDataDiffKind::Insert => appearance.insert_color,
};
if diff.data.is_empty() {
write_text(
" ".repeat(diff.len).as_str(),
base_color,
&mut job,
config.code_font.clone(),
appearance.code_font.clone(),
);
} else {
let mut text = String::new();
@@ -85,7 +87,7 @@ fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], config:
text.push('.');
}
}
write_text(text.as_str(), base_color, &mut job, config.code_font.clone());
write_text(text.as_str(), base_color, &mut job, appearance.code_font.clone());
}
}
ui.add(Label::new(job).sense(Sense::click()));
@@ -130,41 +132,50 @@ fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> {
fn data_table_ui(
table: TableBuilder<'_>,
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left_obj: Option<&ObjInfo>,
right_obj: Option<&ObjInfo>,
selected_symbol: &SymbolReference,
config: &ViewConfig,
config: &Appearance,
) -> Option<()> {
let left_section = find_section(left_obj, selected_symbol)?;
let right_section = find_section(right_obj, selected_symbol)?;
let left_section = left_obj.and_then(|obj| find_section(obj, selected_symbol));
let right_section = right_obj.and_then(|obj| find_section(obj, selected_symbol));
let total_bytes = left_section.data_diff.iter().fold(0usize, |accum, item| accum + item.len);
let total_bytes = left_section
.or(right_section)?
.data_diff
.iter()
.fold(0usize, |accum, item| accum + item.len);
if total_bytes == 0 {
return None;
}
let total_rows = (total_bytes - 1) / BYTES_PER_ROW + 1;
let left_diffs = split_diffs(&left_section.data_diff);
let right_diffs = split_diffs(&right_section.data_diff);
let left_diffs = left_section.map(|section| split_diffs(&section.data_diff));
let right_diffs = right_section.map(|section| split_diffs(&section.data_diff));
table.body(|body| {
body.rows(config.code_font.size, total_rows, |row_index, mut row| {
body.rows(config.code_font.size, total_rows, |mut row| {
let row_index = row.index();
let address = row_index * BYTES_PER_ROW;
row.col(|ui| {
data_row_ui(ui, address, &left_diffs[row_index], config);
if let Some(left_diffs) = &left_diffs {
data_row_ui(ui, address, &left_diffs[row_index], config);
}
});
row.col(|ui| {
data_row_ui(ui, address, &right_diffs[row_index], config);
if let Some(right_diffs) = &right_diffs {
data_row_ui(ui, address, &right_diffs[row_index], config);
}
});
});
});
Some(())
}
pub fn data_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
let mut rebuild = false;
let (Some(result), Some(selected_symbol)) = (&view_state.build, &view_state.selected_symbol) else {
return rebuild;
pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
else {
return;
};
// Header
@@ -181,14 +192,14 @@ pub fn data_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
|ui| {
ui.set_width(column_width);
if ui.button("Back").clicked() {
view_state.current_view = View::SymbolDiff;
if ui.button("Back").clicked() {
state.current_view = View::SymbolDiff;
}
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.colored_label(Color32::WHITE, &selected_symbol.symbol_name);
ui.colored_label(appearance.highlight_color, &selected_symbol.symbol_name);
ui.label("Diff target:");
});
},
@@ -202,14 +213,17 @@ pub fn data_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
ui.set_width(column_width);
ui.horizontal(|ui| {
if ui.button("Build").clicked() {
rebuild = true;
if ui
.add_enabled(!state.build_running, egui::Button::new("Build"))
.clicked()
{
state.queue_build = true;
}
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
if view_state.jobs.iter().any(|job| job.job_type == Job::ObjDiff) {
ui.label("Building...");
if state.build_running {
ui.colored_label(appearance.replace_color, "Building");
} else {
ui.label("Last built:");
let format =
@@ -217,7 +231,7 @@ pub fn data_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
ui.label(
result
.time
.to_offset(view_state.utc_offset)
.to_offset(appearance.utc_offset)
.format(&format)
.unwrap(),
);
@@ -238,17 +252,19 @@ pub fn data_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
ui.separator();
// Table
if let (Some(left_obj), Some(right_obj)) = (&result.first_obj, &result.second_obj) {
let available_height = ui.available_height();
let table = TableBuilder::new(ui)
.striped(false)
.cell_layout(Layout::left_to_right(Align::Min))
.columns(Column::exact(column_width).clip(true), 2)
.resizable(false)
.auto_shrink([false, false])
.min_scrolled_height(available_height);
data_table_ui(table, left_obj, right_obj, selected_symbol, &view_state.view_config);
}
rebuild
let available_height = ui.available_height();
let table = TableBuilder::new(ui)
.striped(false)
.cell_layout(Layout::left_to_right(Align::Min))
.columns(Column::exact(column_width).clip(true), 2)
.resizable(false)
.auto_shrink([false, false])
.min_scrolled_height(available_height);
data_table_ui(
table,
result.first_obj.as_ref(),
result.second_obj.as_ref(),
selected_symbol,
appearance,
);
}

17
src/views/debug.rs Normal file
View File

@@ -0,0 +1,17 @@
use crate::views::{appearance::Appearance, frame_history::FrameHistory};
pub fn debug_window(
ctx: &egui::Context,
show: &mut bool,
frame_history: &mut FrameHistory,
appearance: &Appearance,
) {
egui::Window::new("Debug").open(show).show(ctx, |ui| {
debug_ui(ui, frame_history, appearance);
});
}
fn debug_ui(ui: &mut egui::Ui, frame_history: &mut FrameHistory, _appearance: &Appearance) {
ui.label(format!("Repainting the UI each frame. FPS: {:.1}", frame_history.fps()));
frame_history.ui(ui);
}

34
src/views/demangle.rs Normal file
View File

@@ -0,0 +1,34 @@
use egui::TextStyle;
use crate::views::appearance::Appearance;
#[derive(Default)]
pub struct DemangleViewState {
pub text: String,
}
pub fn demangle_window(
ctx: &egui::Context,
show: &mut bool,
state: &mut DemangleViewState,
appearance: &Appearance,
) {
egui::Window::new("Demangle").open(show).show(ctx, |ui| {
ui.text_edit_singleline(&mut state.text);
ui.add_space(10.0);
if let Some(demangled) = cwdemangle::demangle(&state.text, &Default::default()) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
ui.colored_label(appearance.replace_color, &demangled);
});
if ui.button("Copy").clicked() {
ui.output_mut(|output| output.copied_text = demangled);
}
} else {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
ui.colored_label(appearance.replace_color, "[invalid]");
});
}
});
}

51
src/views/file.rs Normal file
View File

@@ -0,0 +1,51 @@
use std::{future::Future, path::PathBuf, pin::Pin, thread::JoinHandle};
use pollster::FutureExt;
use rfd::FileHandle;
#[derive(Default)]
pub enum FileDialogResult {
#[default]
None,
ProjectDir(PathBuf),
TargetDir(PathBuf),
BaseDir(PathBuf),
Object(PathBuf),
}
#[derive(Default)]
pub struct FileDialogState {
thread: Option<JoinHandle<FileDialogResult>>,
}
impl FileDialogState {
pub fn queue<InitCb, ResultCb>(&mut self, init: InitCb, result_cb: ResultCb)
where
InitCb: FnOnce() -> Pin<Box<dyn Future<Output = Option<FileHandle>> + Send>>,
ResultCb: FnOnce(PathBuf) -> FileDialogResult + Send + 'static,
{
if self.thread.is_some() {
return;
}
let future = init();
self.thread = Some(std::thread::spawn(move || {
if let Some(handle) = future.block_on() {
result_cb(PathBuf::from(handle))
} else {
FileDialogResult::None
}
}));
}
pub fn poll(&mut self) -> FileDialogResult {
if let Some(thread) = &mut self.thread {
if thread.is_finished() {
self.thread.take().unwrap().join().unwrap_or(FileDialogResult::None)
} else {
FileDialogResult::None
}
} else {
FileDialogResult::None
}
}
}

142
src/views/frame_history.rs Normal file
View File

@@ -0,0 +1,142 @@
// From https://github.com/emilk/egui/blob/e037489ac20a9e419715ae75d205a8baa117c3cf/crates/egui_demo_app/src/frame_history.rs
// Copyright (c) 2018-2021 Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
//
// 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.
use egui::util::History;
pub struct FrameHistory {
frame_times: History<f32>,
}
impl Default for FrameHistory {
fn default() -> Self {
let max_age: f32 = 1.0;
let max_len = (max_age * 300.0).round() as usize;
Self { frame_times: History::new(0..max_len, max_age) }
}
}
impl FrameHistory {
// Called first
pub fn on_new_frame(&mut self, now: f64, previous_frame_time: Option<f32>) {
let previous_frame_time = previous_frame_time.unwrap_or_default();
if let Some(latest) = self.frame_times.latest_mut() {
*latest = previous_frame_time; // rewrite history now that we know
}
self.frame_times.add(now, previous_frame_time); // projected
}
pub fn mean_frame_time(&self) -> f32 { self.frame_times.average().unwrap_or_default() }
pub fn fps(&self) -> f32 { 1.0 / self.frame_times.mean_time_interval().unwrap_or_default() }
pub fn ui(&mut self, ui: &mut egui::Ui) {
ui.label(format!("Mean CPU usage: {:.2} ms / frame", 1e3 * self.mean_frame_time()))
.on_hover_text(
"Includes egui layout and tessellation time.\n\
Does not include GPU usage, nor overhead for sending data to GPU.",
);
egui::warn_if_debug_build(ui);
if !cfg!(target_arch = "wasm32") {
egui::CollapsingHeader::new("📊 CPU usage history").default_open(false).show(
ui,
|ui| {
self.graph(ui);
},
);
}
}
fn graph(&mut self, ui: &mut egui::Ui) -> egui::Response {
use egui::*;
ui.label("egui CPU usage history");
let history = &self.frame_times;
// TODO(emilk): we should not use `slider_width` as default graph width.
let height = ui.spacing().slider_width;
let size = vec2(ui.available_size_before_wrap().x, height);
let (rect, response) = ui.allocate_at_least(size, Sense::hover());
let style = ui.style().noninteractive();
let graph_top_cpu_usage = 0.010;
let graph_rect = Rect::from_x_y_ranges(history.max_age()..=0.0, graph_top_cpu_usage..=0.0);
let to_screen = emath::RectTransform::from_to(graph_rect, rect);
let mut shapes = Vec::with_capacity(3 + 2 * history.len());
shapes.push(Shape::Rect(epaint::RectShape::new(
rect,
style.rounding,
ui.visuals().extreme_bg_color,
ui.style().noninteractive().bg_stroke,
)));
let rect = rect.shrink(4.0);
let color = ui.visuals().text_color();
let line_stroke = Stroke::new(1.0, color);
if let Some(pointer_pos) = response.hover_pos() {
let y = pointer_pos.y;
shapes.push(Shape::line_segment(
[pos2(rect.left(), y), pos2(rect.right(), y)],
line_stroke,
));
let cpu_usage = to_screen.inverse().transform_pos(pointer_pos).y;
let text = format!("{:.1} ms", 1e3 * cpu_usage);
shapes.push(ui.fonts(|f| {
Shape::text(
f,
pos2(rect.left(), y),
egui::Align2::LEFT_BOTTOM,
text,
TextStyle::Monospace.resolve(ui.style()),
color,
)
}));
}
let circle_color = color;
let radius = 2.0;
let right_side_time = ui.input(|i| i.time); // Time at right side of screen
for (time, cpu_usage) in history.iter() {
let age = (right_side_time - time) as f32;
let pos = to_screen.transform_pos_clamped(Pos2::new(age, cpu_usage));
shapes.push(Shape::line_segment([pos2(pos.x, rect.bottom()), pos], line_stroke));
if cpu_usage < graph_top_cpu_usage {
shapes.push(Shape::circle_filled(pos, radius, circle_color));
}
}
ui.painter().extend(shapes);
response
}
}

View File

@@ -1,84 +1,128 @@
use std::{cmp::Ordering, default::Default};
use std::{
cmp::{max, Ordering},
default::Default,
};
use cwdemangle::demangle;
use eframe::emath::Align;
use egui::{text::LayoutJob, Color32, FontId, Label, Layout, Sense, Vec2};
use egui_extras::{Column, TableBuilder};
use egui::{text::LayoutJob, Align, Color32, Label, Layout, RichText, Sense, TextFormat, Vec2};
use egui_extras::{Column, TableBuilder, TableRow};
use ppc750cl::Argument;
use time::format_description;
use crate::{
app::{SymbolReference, 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},
views::{
appearance::Appearance,
symbol_diff::{match_color_for_symbol, DiffViewState, SymbolReference, View},
write_text,
},
};
fn write_reloc_name(reloc: &ObjReloc, color: Color32, job: &mut LayoutJob, font_id: FontId) {
#[derive(Default)]
pub enum HighlightKind {
#[default]
None,
Opcode(u8),
Arg(ObjInsArg),
Symbol(String),
Address(u32),
}
#[derive(Default)]
pub struct FunctionViewState {
pub highlight: HighlightKind,
}
fn write_reloc_name(
reloc: &ObjReloc,
color: Color32,
background_color: Color32,
job: &mut LayoutJob,
appearance: &Appearance,
) {
let name = reloc.target.demangled_name.as_ref().unwrap_or(&reloc.target.name);
write_text(name, Color32::LIGHT_GRAY, job, font_id.clone());
job.append(name, 0.0, TextFormat {
font_id: appearance.code_font.clone(),
color: appearance.emphasized_text_color,
background: background_color,
..Default::default()
});
match reloc.target.addend.cmp(&0i64) {
Ordering::Greater => {
write_text(&format!("+{:#X}", reloc.target.addend), color, job, font_id)
}
Ordering::Greater => write_text(
&format!("+{:#X}", reloc.target.addend),
color,
job,
appearance.code_font.clone(),
),
Ordering::Less => {
write_text(&format!("-{:#X}", -reloc.target.addend), color, job, font_id);
write_text(
&format!("-{:#X}", -reloc.target.addend),
color,
job,
appearance.code_font.clone(),
);
}
_ => {}
}
}
fn write_reloc(reloc: &ObjReloc, color: Color32, job: &mut LayoutJob, font_id: FontId) {
fn write_reloc(
reloc: &ObjReloc,
color: Color32,
background_color: Color32,
job: &mut LayoutJob,
appearance: &Appearance,
) {
match reloc.kind {
ObjRelocKind::PpcAddr16Lo => {
write_reloc_name(reloc, color, job, font_id.clone());
write_text("@l", color, job, font_id);
write_reloc_name(reloc, color, background_color, job, appearance);
write_text("@l", color, job, appearance.code_font.clone());
}
ObjRelocKind::PpcAddr16Hi => {
write_reloc_name(reloc, color, job, font_id.clone());
write_text("@h", color, job, font_id);
write_reloc_name(reloc, color, background_color, job, appearance);
write_text("@h", color, job, appearance.code_font.clone());
}
ObjRelocKind::PpcAddr16Ha => {
write_reloc_name(reloc, color, job, font_id.clone());
write_text("@ha", color, job, font_id);
write_reloc_name(reloc, color, background_color, job, appearance);
write_text("@ha", color, job, appearance.code_font.clone());
}
ObjRelocKind::PpcEmbSda21 => {
write_reloc_name(reloc, color, job, font_id.clone());
write_text("@sda21", color, job, font_id);
write_reloc_name(reloc, color, background_color, job, appearance);
write_text("@sda21", color, job, appearance.code_font.clone());
}
ObjRelocKind::MipsHi16 => {
write_text("%hi(", color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone());
write_text(")", color, job, font_id);
write_text("%hi(", color, job, appearance.code_font.clone());
write_reloc_name(reloc, color, background_color, job, appearance);
write_text(")", color, job, appearance.code_font.clone());
}
ObjRelocKind::MipsLo16 => {
write_text("%lo(", color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone());
write_text(")", color, job, font_id);
write_text("%lo(", color, job, appearance.code_font.clone());
write_reloc_name(reloc, color, background_color, job, appearance);
write_text(")", color, job, appearance.code_font.clone());
}
ObjRelocKind::MipsGot16 => {
write_text("%got(", color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone());
write_text(")", color, job, font_id);
write_text("%got(", color, job, appearance.code_font.clone());
write_reloc_name(reloc, color, background_color, job, appearance);
write_text(")", color, job, appearance.code_font.clone());
}
ObjRelocKind::MipsCall16 => {
write_text("%call16(", color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone());
write_text(")", color, job, font_id);
write_text("%call16(", color, job, appearance.code_font.clone());
write_reloc_name(reloc, color, background_color, job, appearance);
write_text(")", color, job, appearance.code_font.clone());
}
ObjRelocKind::MipsGpRel16 => {
write_text("%gp_rel(", color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone());
write_text(")", color, job, font_id);
write_text("%gp_rel(", color, job, appearance.code_font.clone());
write_reloc_name(reloc, color, background_color, job, appearance);
write_text(")", color, job, appearance.code_font.clone());
}
ObjRelocKind::PpcRel24 | ObjRelocKind::PpcRel14 | ObjRelocKind::Mips26 => {
write_reloc_name(reloc, color, job, font_id);
write_reloc_name(reloc, color, background_color, job, appearance);
}
ObjRelocKind::Absolute | ObjRelocKind::MipsGpRel32 => {
write_text("[INVALID]", color, job, font_id);
write_text("[INVALID]", color, job, appearance.code_font.clone());
}
};
}
@@ -88,101 +132,170 @@ fn write_ins(
diff_kind: &ObjInsDiffKind,
args: &[Option<ObjInsArgDiff>],
base_addr: u32,
job: &mut LayoutJob,
config: &ViewConfig,
ui: &mut egui::Ui,
appearance: &Appearance,
ins_view_state: &mut FunctionViewState,
) {
let base_color = match diff_kind {
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
Color32::GRAY
appearance.text_color
}
ObjInsDiffKind::Replace => Color32::LIGHT_BLUE,
ObjInsDiffKind::Delete => COLOR_RED,
ObjInsDiffKind::Insert => Color32::GREEN,
ObjInsDiffKind::Replace => appearance.replace_color,
ObjInsDiffKind::Delete => appearance.delete_color,
ObjInsDiffKind::Insert => appearance.insert_color,
};
write_text(
&format!("{:<11}", ins.mnemonic),
match diff_kind {
ObjInsDiffKind::OpMismatch => Color32::LIGHT_BLUE,
_ => base_color,
},
job,
config.code_font.clone(),
);
let highlighted_op =
matches!(ins_view_state.highlight, HighlightKind::Opcode(op) if op == ins.op);
let op_label = RichText::new(ins.mnemonic.clone())
.font(appearance.code_font.clone())
.color(if highlighted_op {
appearance.emphasized_text_color
} else {
match diff_kind {
ObjInsDiffKind::OpMismatch => appearance.replace_color,
_ => base_color,
}
})
.background_color(if highlighted_op {
appearance.deemphasized_text_color
} else {
Color32::TRANSPARENT
});
if ui
.add(Label::new(op_label).sense(Sense::click()))
.context_menu(|ui| ins_context_menu(ui, ins))
.clicked()
{
if highlighted_op {
ins_view_state.highlight = HighlightKind::None;
} else {
ins_view_state.highlight = HighlightKind::Opcode(ins.op);
}
}
let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' '));
ui.add_space(space_width * (max(11, ins.mnemonic.len()) - ins.mnemonic.len()) as f32);
let mut writing_offset = false;
for (i, arg) in ins.args.iter().enumerate() {
let mut job = LayoutJob::default();
if i == 0 {
write_text(" ", base_color, job, config.code_font.clone());
write_text(" ", base_color, &mut job, appearance.code_font.clone());
}
if i > 0 && !writing_offset {
write_text(", ", base_color, job, config.code_font.clone());
write_text(", ", base_color, &mut job, appearance.code_font.clone());
}
let color = if let Some(diff) = args.get(i).and_then(|a| a.as_ref()) {
config.diff_colors[diff.idx % config.diff_colors.len()]
let highlighted_arg = match &ins_view_state.highlight {
HighlightKind::Symbol(v) => {
matches!(arg, ObjInsArg::Reloc | ObjInsArg::RelocWithBase)
&& matches!(&ins.reloc, Some(reloc) if &reloc.target.name == v)
}
HighlightKind::Address(v) => {
matches!(arg, ObjInsArg::BranchOffset(offset) if (offset + ins.address as i32 - base_addr as i32) as u32 == *v)
}
HighlightKind::Arg(v) => v.loose_eq(arg),
_ => false,
};
let color = if highlighted_arg {
appearance.emphasized_text_color
} else if let Some(diff) = args.get(i).and_then(|a| a.as_ref()) {
appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
} else {
base_color
};
let text_format = TextFormat {
font_id: appearance.code_font.clone(),
color,
background: if highlighted_arg {
appearance.deemphasized_text_color
} else {
Color32::TRANSPARENT
},
..Default::default()
};
let mut new_writing_offset = false;
match arg {
ObjInsArg::PpcArg(arg) => match arg {
Argument::Offset(val) => {
write_text(&format!("{val}"), color, job, config.code_font.clone());
write_text("(", base_color, job, config.code_font.clone());
writing_offset = true;
continue;
job.append(&format!("{val}"), 0.0, text_format);
write_text("(", base_color, &mut job, appearance.code_font.clone());
new_writing_offset = true;
}
Argument::Uimm(_) | Argument::Simm(_) => {
write_text(&format!("{arg}"), color, job, config.code_font.clone());
job.append(&format!("{arg}"), 0.0, text_format);
}
_ => {
write_text(&format!("{arg}"), color, job, config.code_font.clone());
job.append(&format!("{arg}"), 0.0, text_format);
}
},
ObjInsArg::Reloc => {
write_reloc(ins.reloc.as_ref().unwrap(), base_color, job, config.code_font.clone());
write_reloc(
ins.reloc.as_ref().unwrap(),
base_color,
text_format.background,
&mut job,
appearance,
);
}
ObjInsArg::RelocWithBase => {
write_reloc(ins.reloc.as_ref().unwrap(), base_color, job, config.code_font.clone());
write_text("(", base_color, job, config.code_font.clone());
writing_offset = true;
continue;
write_reloc(
ins.reloc.as_ref().unwrap(),
base_color,
text_format.background,
&mut job,
appearance,
);
write_text("(", base_color, &mut job, appearance.code_font.clone());
new_writing_offset = true;
}
ObjInsArg::MipsArg(str) => {
write_text(
str.strip_prefix('$').unwrap_or(str),
color,
job,
config.code_font.clone(),
);
job.append(str.strip_prefix('$').unwrap_or(str), 0.0, text_format);
}
ObjInsArg::MipsArgWithBase(str) => {
write_text(
str.strip_prefix('$').unwrap_or(str),
color,
job,
config.code_font.clone(),
);
write_text("(", base_color, job, config.code_font.clone());
writing_offset = true;
continue;
job.append(str.strip_prefix('$').unwrap_or(str), 0.0, text_format);
write_text("(", base_color, &mut job, appearance.code_font.clone());
new_writing_offset = true;
}
ObjInsArg::BranchOffset(offset) => {
let addr = offset + ins.address as i32 - base_addr as i32;
write_text(&format!("{addr:x}"), color, job, config.code_font.clone());
job.append(&format!("{addr:x}"), 0.0, text_format);
}
}
if writing_offset {
write_text(")", base_color, job, config.code_font.clone());
writing_offset = false;
write_text(")", base_color, &mut job, appearance.code_font.clone());
}
writing_offset = new_writing_offset;
if ui
.add(Label::new(job).sense(Sense::click()))
.context_menu(|ui| ins_context_menu(ui, ins))
.clicked()
{
if highlighted_arg {
ins_view_state.highlight = HighlightKind::None;
} else if matches!(arg, ObjInsArg::Reloc | ObjInsArg::RelocWithBase) {
ins_view_state.highlight =
HighlightKind::Symbol(ins.reloc.as_ref().unwrap().target.name.clone());
} else if let ObjInsArg::BranchOffset(offset) = arg {
ins_view_state.highlight =
HighlightKind::Address((offset + ins.address as i32 - base_addr as i32) as u32);
} else {
ins_view_state.highlight = HighlightKind::Arg(arg.clone());
}
}
}
}
fn ins_hover_ui(ui: &mut egui::Ui, ins: &ObjIns) {
fn ins_hover_ui(ui: &mut egui::Ui, ins: &ObjIns, appearance: &Appearance) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.label(format!("{:02X?}", ins.code.to_be_bytes()));
if let Some(orig) = &ins.orig {
ui.label(format!("Original: {}", orig));
}
for arg in &ins.args {
if let ObjInsArg::PpcArg(arg) = arg {
match arg {
@@ -202,13 +315,19 @@ fn ins_hover_ui(ui: &mut egui::Ui, ins: &ObjIns) {
if let Some(reloc) = &ins.reloc {
ui.label(format!("Relocation type: {:?}", reloc.kind));
ui.colored_label(Color32::WHITE, format!("Name: {}", reloc.target.name));
ui.colored_label(appearance.highlight_color, format!("Name: {}", reloc.target.name));
if let Some(section) = &reloc.target_section {
ui.colored_label(Color32::WHITE, format!("Section: {section}"));
ui.colored_label(Color32::WHITE, format!("Address: {:x}", reloc.target.address));
ui.colored_label(Color32::WHITE, format!("Size: {:x}", reloc.target.size));
ui.colored_label(appearance.highlight_color, format!("Section: {section}"));
ui.colored_label(
appearance.highlight_color,
format!("Address: {:x}", reloc.target.address),
);
ui.colored_label(
appearance.highlight_color,
format!("Size: {:x}", reloc.target.size),
);
} else {
ui.colored_label(Color32::WHITE, "Extern".to_string());
ui.colored_label(appearance.highlight_color, "Extern".to_string());
}
}
});
@@ -226,31 +345,31 @@ fn ins_context_menu(ui: &mut egui::Ui, ins: &ObjIns) {
match arg {
Argument::Uimm(v) => {
if ui.button(format!("Copy \"{v}\"")).clicked() {
ui.output().copied_text = format!("{v}");
ui.output_mut(|output| output.copied_text = format!("{v}"));
ui.close_menu();
}
if ui.button(format!("Copy \"{}\"", v.0)).clicked() {
ui.output().copied_text = format!("{}", v.0);
ui.output_mut(|output| output.copied_text = format!("{}", v.0));
ui.close_menu();
}
}
Argument::Simm(v) => {
if ui.button(format!("Copy \"{v}\"")).clicked() {
ui.output().copied_text = format!("{v}");
ui.output_mut(|output| output.copied_text = format!("{v}"));
ui.close_menu();
}
if ui.button(format!("Copy \"{}\"", v.0)).clicked() {
ui.output().copied_text = format!("{}", v.0);
ui.output_mut(|output| output.copied_text = format!("{}", v.0));
ui.close_menu();
}
}
Argument::Offset(v) => {
if ui.button(format!("Copy \"{v}\"")).clicked() {
ui.output().copied_text = format!("{v}");
ui.output_mut(|output| output.copied_text = format!("{v}"));
ui.close_menu();
}
if ui.button(format!("Copy \"{}\"", v.0)).clicked() {
ui.output().copied_text = format!("{}", v.0);
ui.output_mut(|output| output.copied_text = format!("{}", v.0));
ui.close_menu();
}
}
@@ -261,12 +380,12 @@ 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() {
ui.output().copied_text = name.clone();
ui.output_mut(|output| output.copied_text = name.clone());
ui.close_menu();
}
}
if ui.button(format!("Copy \"{}\"", reloc.target.name)).clicked() {
ui.output().copied_text = reloc.target.name.clone();
ui.output_mut(|output| output.copied_text = reloc.target.name.clone());
ui.close_menu();
}
}
@@ -279,7 +398,14 @@ fn find_symbol<'a>(obj: &'a ObjInfo, selected_symbol: &SymbolReference) -> Optio
})
}
fn asm_row_ui(ui: &mut egui::Ui, ins_diff: &ObjInsDiff, symbol: &ObjSymbol, config: &ViewConfig) {
fn asm_row_ui(
ui: &mut egui::Ui,
ins_diff: &ObjInsDiff,
symbol: &ObjSymbol,
appearance: &Appearance,
ins_view_state: &mut FunctionViewState,
) {
ui.spacing_mut().item_spacing.x = 0.0;
if ins_diff.kind != ObjInsDiffKind::None {
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
}
@@ -291,79 +417,159 @@ fn asm_row_ui(ui: &mut egui::Ui, ins_diff: &ObjInsDiff, symbol: &ObjSymbol, conf
let base_color = match ins_diff.kind {
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
Color32::GRAY
appearance.text_color
}
ObjInsDiffKind::Replace => Color32::LIGHT_BLUE,
ObjInsDiffKind::Delete => COLOR_RED,
ObjInsDiffKind::Insert => Color32::GREEN,
ObjInsDiffKind::Replace => appearance.replace_color,
ObjInsDiffKind::Delete => appearance.delete_color,
ObjInsDiffKind::Insert => appearance.insert_color,
};
let mut pad = 6;
if let Some(line) = ins.line {
let line_str = format!("{line} ");
write_text(&line_str, Color32::DARK_GRAY, &mut job, config.code_font.clone());
write_text(
&line_str,
appearance.deemphasized_text_color,
&mut job,
appearance.code_font.clone(),
);
pad = 12 - line_str.len();
}
write_text(
&format!("{:<1$}", format!("{:x}: ", ins.address - symbol.address as u32), pad),
base_color,
&mut job,
config.code_font.clone(),
let base_addr = symbol.address as u32;
let addr_highlight = matches!(
&ins_view_state.highlight,
HighlightKind::Address(v) if *v == (ins.address - base_addr)
);
if let Some(branch) = &ins_diff.branch_from {
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, config.code_font.clone());
let addr_string = format!("{:x}", ins.address - symbol.address as u32);
pad -= addr_string.len();
job.append(&addr_string, 0.0, TextFormat {
font_id: appearance.code_font.clone(),
color: if addr_highlight { appearance.emphasized_text_color } else { base_color },
background: if addr_highlight {
appearance.deemphasized_text_color
} else {
Color32::TRANSPARENT
},
..Default::default()
});
if ui
.add(Label::new(job).sense(Sense::click()))
.context_menu(|ui| ins_context_menu(ui, ins))
.clicked()
{
if addr_highlight {
ins_view_state.highlight = HighlightKind::None;
} else {
ins_view_state.highlight = HighlightKind::Address(ins.address - base_addr);
}
}
write_ins(ins, &ins_diff.kind, &ins_diff.arg_diff, symbol.address as u32, &mut job, config);
let mut job = LayoutJob::default();
let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' '));
let spacing = space_width * pad as f32;
job.append(": ", 0.0, TextFormat {
font_id: appearance.code_font.clone(),
color: base_color,
..Default::default()
});
if let Some(branch) = &ins_diff.branch_from {
job.append("~> ", spacing, TextFormat {
font_id: appearance.code_font.clone(),
color: appearance.diff_colors[branch.branch_idx % appearance.diff_colors.len()],
..Default::default()
});
} else {
job.append(" ", spacing, TextFormat {
font_id: appearance.code_font.clone(),
color: base_color,
..Default::default()
});
}
ui.add(Label::new(job));
write_ins(ins, &ins_diff.kind, &ins_diff.arg_diff, base_addr, ui, appearance, ins_view_state);
if let Some(branch) = &ins_diff.branch_to {
let mut job = LayoutJob::default();
write_text(
" ~>",
config.diff_colors[branch.branch_idx % config.diff_colors.len()],
appearance.diff_colors[branch.branch_idx % appearance.diff_colors.len()],
&mut job,
config.code_font.clone(),
appearance.code_font.clone(),
);
ui.add(Label::new(job));
}
ui.add(Label::new(job).sense(Sense::click()))
.on_hover_ui_at_pointer(|ui| ins_hover_ui(ui, ins))
.context_menu(|ui| ins_context_menu(ui, ins));
}
fn asm_col_ui(
row: &mut TableRow<'_, '_>,
ins_diff: &ObjInsDiff,
symbol: &ObjSymbol,
appearance: &Appearance,
ins_view_state: &mut FunctionViewState,
) {
let (_, response) = row.col(|ui| {
asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state);
});
if let Some(ins) = &ins_diff.ins {
response
.on_hover_ui_at_pointer(|ui| {
ins_hover_ui(ui, ins, appearance);
})
.context_menu(|ui| {
ins_context_menu(ui, ins);
});
}
}
fn empty_col_ui(row: &mut TableRow<'_, '_>) {
row.col(|ui| {
ui.label("");
});
}
fn asm_table_ui(
table: TableBuilder<'_>,
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left_obj: Option<&ObjInfo>,
right_obj: Option<&ObjInfo>,
selected_symbol: &SymbolReference,
config: &ViewConfig,
appearance: &Appearance,
ins_view_state: &mut FunctionViewState,
) -> Option<()> {
let left_symbol = find_symbol(left_obj, selected_symbol);
let right_symbol = find_symbol(right_obj, selected_symbol);
let left_symbol = left_obj.and_then(|obj| find_symbol(obj, selected_symbol));
let right_symbol = right_obj.and_then(|obj| find_symbol(obj, selected_symbol));
let instructions_len = left_symbol.or(right_symbol).map(|s| s.instructions.len())?;
table.body(|body| {
body.rows(config.code_font.size, instructions_len, |row_index, mut row| {
row.col(|ui| {
if let Some(symbol) = left_symbol {
asm_row_ui(ui, &symbol.instructions[row_index], symbol, config);
}
});
row.col(|ui| {
if let Some(symbol) = right_symbol {
asm_row_ui(ui, &symbol.instructions[row_index], symbol, config);
}
});
body.rows(appearance.code_font.size, instructions_len, |mut row| {
let row_index = row.index();
if let Some(symbol) = left_symbol {
asm_col_ui(
&mut row,
&symbol.instructions[row_index],
symbol,
appearance,
ins_view_state,
);
} else {
empty_col_ui(&mut row);
}
if let Some(symbol) = right_symbol {
asm_col_ui(
&mut row,
&symbol.instructions[row_index],
symbol,
appearance,
ins_view_state,
);
} else {
empty_col_ui(&mut row);
}
});
});
Some(())
}
pub fn function_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
let mut rebuild = false;
let (Some(result), Some(selected_symbol)) = (&view_state.build, &view_state.selected_symbol) else {
return rebuild;
pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
else {
return;
};
// Header
@@ -380,16 +586,30 @@ pub fn function_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
|ui| {
ui.set_width(column_width);
if ui.button("Back").clicked() {
view_state.current_view = View::SymbolDiff;
}
ui.horizontal(|ui| {
if ui.button("⏴ Back").clicked() {
state.current_view = View::SymbolDiff;
}
ui.separator();
if ui
.add_enabled(
!state.scratch_running && state.scratch_available,
egui::Button::new("📲 decomp.me"),
)
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
.on_disabled_hover_text("Scratch configuration missing")
.clicked()
{
state.queue_scratch = true;
}
});
let demangled = demangle(&selected_symbol.symbol_name, &Default::default());
let name = demangled.as_deref().unwrap_or(&selected_symbol.symbol_name);
let mut job = LayoutJob::simple(
name.to_string(),
view_state.view_config.code_font.clone(),
Color32::WHITE,
appearance.code_font.clone(),
appearance.highlight_color,
column_width,
);
job.wrap.break_anywhere = true;
@@ -411,14 +631,17 @@ pub fn function_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
ui.set_width(column_width);
ui.horizontal(|ui| {
if ui.button("Build").clicked() {
rebuild = true;
if ui
.add_enabled(!state.build_running, egui::Button::new("Build"))
.clicked()
{
state.queue_build = true;
}
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
if view_state.jobs.iter().any(|job| job.job_type == Job::ObjDiff) {
ui.label("Building...");
if state.build_running {
ui.colored_label(appearance.replace_color, "Building");
} else {
ui.label("Last built:");
let format =
@@ -426,7 +649,7 @@ pub fn function_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
ui.label(
result
.time
.to_offset(view_state.utc_offset)
.to_offset(appearance.utc_offset)
.format(&format)
.unwrap(),
);
@@ -443,11 +666,11 @@ pub fn function_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
.and_then(|symbol| symbol.match_percent)
{
ui.colored_label(
match_color_for_symbol(match_percent),
match_color_for_symbol(match_percent, appearance),
&format!("{match_percent:.0}%"),
);
} else {
ui.label("");
ui.colored_label(appearance.replace_color, "Missing");
}
ui.label("Diff base:");
});
@@ -458,16 +681,20 @@ pub fn function_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
ui.separator();
// Table
if let (Some(left_obj), Some(right_obj)) = (&result.first_obj, &result.second_obj) {
let available_height = ui.available_height();
let table = TableBuilder::new(ui)
.striped(false)
.cell_layout(Layout::left_to_right(Align::Min))
.columns(Column::exact(column_width).clip(true), 2)
.resizable(false)
.auto_shrink([false, false])
.min_scrolled_height(available_height);
asm_table_ui(table, left_obj, right_obj, selected_symbol, &view_state.view_config);
}
rebuild
let available_height = ui.available_height();
let table = TableBuilder::new(ui)
.striped(false)
.cell_layout(Layout::left_to_right(Align::Min))
.columns(Column::exact(column_width).clip(true), 2)
.resizable(false)
.auto_shrink([false, false])
.min_scrolled_height(available_height);
asm_table_ui(
table,
result.first_obj.as_ref(),
result.second_obj.as_ref(),
selected_symbol,
appearance,
&mut state.function_state,
);
}

View File

@@ -1,13 +1,13 @@
use egui::{Color32, ProgressBar, Widget};
use egui::{ProgressBar, RichText, Widget};
use crate::app::ViewState;
use crate::{jobs::JobQueue, views::appearance::Appearance};
pub fn jobs_ui(ui: &mut egui::Ui, view_state: &mut ViewState) {
pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) {
ui.label("Jobs");
let mut remove_job: Option<usize> = None;
for (idx, job) in view_state.jobs.iter_mut().enumerate() {
let Ok(status) = job.status.read() else {
for job in jobs.iter_mut() {
let Ok(status) = job.context.status.read() else {
continue;
};
ui.group(|ui| {
@@ -17,10 +17,10 @@ pub fn jobs_ui(ui: &mut egui::Ui, view_state: &mut ViewState) {
if job.handle.is_some() {
job.should_remove = true;
if let Err(e) = job.cancel.send(()) {
eprintln!("Failed to cancel job: {e:?}");
log::error!("Failed to cancel job: {e:?}");
}
} else {
remove_job = Some(idx);
remove_job = Some(job.id);
}
}
});
@@ -31,26 +31,28 @@ pub fn jobs_ui(ui: &mut egui::Ui, view_state: &mut ViewState) {
bar.ui(ui);
const STATUS_LENGTH: usize = 80;
if let Some(err) = &status.error {
let err_string = err.to_string();
let err_string = format!("{:#}", err);
ui.colored_label(
Color32::from_rgb(255, 0, 0),
appearance.delete_color,
if err_string.len() > STATUS_LENGTH - 10 {
format!("Error: {}...", &err_string[0..STATUS_LENGTH - 10])
format!("Error: {}", &err_string[0..STATUS_LENGTH - 10])
} else {
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
},
);
)
.on_hover_text_at_pointer(RichText::new(err_string).color(appearance.delete_color));
} else {
ui.label(if status.status.len() > STATUS_LENGTH - 3 {
format!("{}...", &status.status[0..STATUS_LENGTH - 3])
format!("{}", &status.status[0..STATUS_LENGTH - 3])
} else {
format!("{:width$}", &status.status, width = STATUS_LENGTH)
});
})
.on_hover_text_at_pointer(&status.status);
}
});
}
if let Some(idx) = remove_job {
view_state.jobs.remove(idx);
jobs.remove(idx);
}
}

View File

@@ -1,13 +1,17 @@
use egui::{text::LayoutJob, Color32, FontId, TextFormat};
pub(crate) mod appearance;
pub(crate) mod config;
pub(crate) mod data_diff;
pub(crate) mod debug;
pub(crate) mod demangle;
pub(crate) mod file;
pub(crate) mod frame_history;
pub(crate) mod function_diff;
pub(crate) mod jobs;
pub(crate) mod symbol_diff;
const COLOR_RED: Color32 = Color32::from_rgb(200, 40, 41);
#[inline]
fn write_text(str: &str, color: Color32, job: &mut LayoutJob, font_id: FontId) {
job.append(str, 0.0, TextFormat::simple(font_id, color));
}

View File

@@ -1,23 +1,129 @@
use std::mem::take;
use egui::{
text::LayoutJob, Align, CollapsingHeader, Color32, Layout, Rgba, ScrollArea, SelectableLabel,
TextEdit, Ui, Vec2, Widget,
text::LayoutJob, Align, CollapsingHeader, Color32, Id, Layout, OpenUrl, ScrollArea,
SelectableLabel, TextEdit, Ui, Vec2, Widget,
};
use egui_extras::{Size, StripBuilder};
use crate::{
app::{SymbolReference, View, ViewConfig, ViewState},
jobs::objdiff::BuildStatus,
app::AppConfigRef,
jobs::{
create_scratch::{start_create_scratch, CreateScratchConfig, CreateScratchResult},
objdiff::{BuildStatus, ObjDiffResult},
Job, JobQueue, JobResult,
},
obj::{ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags},
views::write_text,
views::{appearance::Appearance, function_diff::FunctionViewState, write_text},
};
pub fn match_color_for_symbol(match_percent: f32) -> Color32 {
pub struct SymbolReference {
pub symbol_name: String,
pub section_name: String,
}
#[allow(clippy::enum_variant_names)]
#[derive(Default, Eq, PartialEq, Copy, Clone)]
pub enum View {
#[default]
SymbolDiff,
FunctionDiff,
DataDiff,
}
#[derive(Default)]
pub struct DiffViewState {
pub build: Option<Box<ObjDiffResult>>,
pub scratch: Option<Box<CreateScratchResult>>,
pub current_view: View,
pub symbol_state: SymbolViewState,
pub function_state: FunctionViewState,
pub search: String,
pub queue_build: bool,
pub build_running: bool,
pub scratch_available: bool,
pub queue_scratch: bool,
pub scratch_running: bool,
}
#[derive(Default)]
pub struct SymbolViewState {
pub highlighted_symbol: Option<String>,
pub selected_symbol: Option<SymbolReference>,
pub reverse_fn_order: bool,
pub disable_reverse_fn_order: bool,
pub show_hidden_symbols: bool,
}
impl DiffViewState {
pub fn pre_update(&mut self, jobs: &mut JobQueue, config: &AppConfigRef) {
jobs.results.retain_mut(|result| match result {
JobResult::ObjDiff(result) => {
self.build = take(result);
false
}
JobResult::CreateScratch(result) => {
self.scratch = take(result);
false
}
_ => true,
});
self.build_running = jobs.is_running(Job::ObjDiff);
self.scratch_running = jobs.is_running(Job::CreateScratch);
self.symbol_state.disable_reverse_fn_order = false;
if let Ok(config) = config.read() {
if let Some(obj_config) = &config.selected_obj {
if let Some(value) = obj_config.reverse_fn_order {
self.symbol_state.reverse_fn_order = value;
self.symbol_state.disable_reverse_fn_order = true;
}
}
self.scratch_available = CreateScratchConfig::is_available(&config);
}
}
pub fn post_update(&mut self, ctx: &egui::Context, jobs: &mut JobQueue, config: &AppConfigRef) {
if let Some(result) = take(&mut self.scratch) {
ctx.output_mut(|o| o.open_url = Some(OpenUrl::new_tab(result.scratch_url)));
}
if self.queue_build {
self.queue_build = false;
if let Ok(mut config) = config.write() {
config.queue_build = true;
}
}
if self.queue_scratch {
self.queue_scratch = false;
if let Some(function_name) =
self.symbol_state.selected_symbol.as_ref().map(|sym| sym.symbol_name.clone())
{
if let Ok(config) = config.read() {
match CreateScratchConfig::from_config(&config, function_name) {
Ok(config) => {
jobs.push_once(Job::CreateScratch, || {
start_create_scratch(ctx, config)
});
}
Err(err) => {
log::error!("Failed to create scratch config: {err}");
}
}
}
}
}
}
}
pub fn match_color_for_symbol(match_percent: f32, appearance: &Appearance) -> Color32 {
if match_percent == 100.0 {
Color32::GREEN
appearance.insert_color
} else if match_percent >= 50.0 {
Color32::LIGHT_BLUE
appearance.replace_color
} else {
Color32::RED
appearance.delete_color
}
}
@@ -28,94 +134,104 @@ fn symbol_context_menu_ui(ui: &mut Ui, symbol: &ObjSymbol) {
if let Some(name) = &symbol.demangled_name {
if ui.button(format!("Copy \"{name}\"")).clicked() {
ui.output().copied_text = name.clone();
ui.output_mut(|output| output.copied_text = name.clone());
ui.close_menu();
}
}
if ui.button(format!("Copy \"{}\"", symbol.name)).clicked() {
ui.output().copied_text = symbol.name.clone();
ui.output_mut(|output| output.copied_text = symbol.name.clone());
ui.close_menu();
}
});
}
fn symbol_hover_ui(ui: &mut Ui, symbol: &ObjSymbol) {
fn symbol_hover_ui(ui: &mut Ui, symbol: &ObjSymbol, appearance: &Appearance) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.colored_label(Color32::WHITE, format!("Name: {}", symbol.name));
ui.colored_label(Color32::WHITE, format!("Address: {:x}", symbol.address));
ui.colored_label(appearance.highlight_color, format!("Name: {}", symbol.name));
ui.colored_label(appearance.highlight_color, format!("Address: {:x}", symbol.address));
if symbol.size_known {
ui.colored_label(Color32::WHITE, format!("Size: {:x}", symbol.size));
ui.colored_label(appearance.highlight_color, format!("Size: {:x}", symbol.size));
} else {
ui.colored_label(Color32::WHITE, format!("Size: {:x} (assumed)", symbol.size));
ui.colored_label(
appearance.highlight_color,
format!("Size: {:x} (assumed)", symbol.size),
);
}
});
}
#[must_use]
fn symbol_ui(
ui: &mut Ui,
symbol: &ObjSymbol,
section: Option<&ObjSection>,
highlighted_symbol: &mut Option<String>,
selected_symbol: &mut Option<SymbolReference>,
current_view: &mut View,
config: &ViewConfig,
) {
state: &mut SymbolViewState,
appearance: &Appearance,
) -> Option<View> {
if symbol.flags.0.contains(ObjSymbolFlags::Hidden) && !state.show_hidden_symbols {
return None;
}
let mut ret = None;
let mut job = LayoutJob::default();
let name: &str =
if let Some(demangled) = &symbol.demangled_name { demangled } else { &symbol.name };
let mut selected = false;
if let Some(sym) = highlighted_symbol {
if let Some(sym) = &state.highlighted_symbol {
selected = sym == &symbol.name;
}
write_text("[", Color32::GRAY, &mut job, config.code_font.clone());
write_text("[", appearance.text_color, &mut job, appearance.code_font.clone());
if symbol.flags.0.contains(ObjSymbolFlags::Common) {
write_text("c", Color32::from_rgb(0, 255, 255), &mut job, config.code_font.clone());
write_text("c", appearance.replace_color, &mut job, appearance.code_font.clone());
} else if symbol.flags.0.contains(ObjSymbolFlags::Global) {
write_text("g", Color32::GREEN, &mut job, config.code_font.clone());
write_text("g", appearance.insert_color, &mut job, appearance.code_font.clone());
} else if symbol.flags.0.contains(ObjSymbolFlags::Local) {
write_text("l", Color32::GRAY, &mut job, config.code_font.clone());
write_text("l", appearance.text_color, &mut job, appearance.code_font.clone());
}
if symbol.flags.0.contains(ObjSymbolFlags::Weak) {
write_text("w", Color32::GRAY, &mut job, config.code_font.clone());
write_text("w", appearance.text_color, &mut job, appearance.code_font.clone());
}
write_text("] ", Color32::GRAY, &mut job, config.code_font.clone());
if symbol.flags.0.contains(ObjSymbolFlags::Hidden) {
write_text("h", appearance.deemphasized_text_color, &mut job, appearance.code_font.clone());
}
write_text("] ", appearance.text_color, &mut job, appearance.code_font.clone());
if let Some(match_percent) = symbol.match_percent {
write_text("(", Color32::GRAY, &mut job, config.code_font.clone());
write_text("(", appearance.text_color, &mut job, appearance.code_font.clone());
write_text(
&format!("{match_percent:.0}%"),
match_color_for_symbol(match_percent),
match_color_for_symbol(match_percent, appearance),
&mut job,
config.code_font.clone(),
appearance.code_font.clone(),
);
write_text(") ", Color32::GRAY, &mut job, config.code_font.clone());
write_text(") ", appearance.text_color, &mut job, appearance.code_font.clone());
}
write_text(name, Color32::WHITE, &mut job, config.code_font.clone());
write_text(name, appearance.highlight_color, &mut job, appearance.code_font.clone());
let response = SelectableLabel::new(selected, job)
.ui(ui)
.context_menu(|ui| symbol_context_menu_ui(ui, symbol))
.on_hover_ui_at_pointer(|ui| symbol_hover_ui(ui, symbol));
.on_hover_ui_at_pointer(|ui| symbol_hover_ui(ui, symbol, appearance));
if response.clicked() {
if let Some(section) = section {
if section.kind == ObjSectionKind::Code {
*selected_symbol = Some(SymbolReference {
state.selected_symbol = Some(SymbolReference {
symbol_name: symbol.name.clone(),
section_name: section.name.clone(),
});
*current_view = View::FunctionDiff;
ret = Some(View::FunctionDiff);
} else if section.kind == ObjSectionKind::Data {
*selected_symbol = Some(SymbolReference {
state.selected_symbol = Some(SymbolReference {
symbol_name: section.name.clone(),
section_name: section.name.clone(),
});
*current_view = View::DataDiff;
ret = Some(View::DataDiff);
}
}
} else if response.hovered() {
*highlighted_symbol = Some(symbol.name.clone());
state.highlighted_symbol = Some(symbol.name.clone());
}
ret
}
fn symbol_matches_search(symbol: &ObjSymbol, search_str: &str) -> bool {
@@ -128,16 +244,15 @@ fn symbol_matches_search(symbol: &ObjSymbol, search_str: &str) -> bool {
.unwrap_or(false)
}
#[allow(clippy::too_many_arguments)]
#[must_use]
fn symbol_list_ui(
ui: &mut Ui,
obj: &ObjInfo,
highlighted_symbol: &mut Option<String>,
selected_symbol: &mut Option<SymbolReference>,
current_view: &mut View,
state: &mut SymbolViewState,
lower_search: &str,
config: &ViewConfig,
) {
appearance: &Appearance,
) -> Option<View> {
let mut ret = None;
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
@@ -146,79 +261,75 @@ fn symbol_list_ui(
if !obj.common.is_empty() {
CollapsingHeader::new(".comm").default_open(true).show(ui, |ui| {
for symbol in &obj.common {
symbol_ui(
ui,
symbol,
None,
highlighted_symbol,
selected_symbol,
current_view,
config,
);
ret = ret.or(symbol_ui(ui, symbol, None, state, appearance));
}
});
}
for section in &obj.sections {
CollapsingHeader::new(format!("{} ({:x})", section.name, section.size))
.id_source(Id::new(section.name.clone()).with(section.index))
.default_open(true)
.show(ui, |ui| {
if section.kind == ObjSectionKind::Code && config.reverse_fn_order {
if section.kind == ObjSectionKind::Code && state.reverse_fn_order {
for symbol in section.symbols.iter().rev() {
if !symbol_matches_search(symbol, lower_search) {
continue;
}
symbol_ui(
ui,
symbol,
Some(section),
highlighted_symbol,
selected_symbol,
current_view,
config,
);
ret =
ret.or(symbol_ui(ui, symbol, Some(section), state, appearance));
}
} else {
for symbol in &section.symbols {
if !symbol_matches_search(symbol, lower_search) {
continue;
}
symbol_ui(
ui,
symbol,
Some(section),
highlighted_symbol,
selected_symbol,
current_view,
config,
);
ret =
ret.or(symbol_ui(ui, symbol, Some(section), state, appearance));
}
}
});
}
});
});
ret
}
fn build_log_ui(ui: &mut Ui, status: &BuildStatus) {
fn build_log_ui(ui: &mut Ui, status: &BuildStatus, appearance: &Appearance) {
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
ui.horizontal(|ui| {
if ui.button("Copy command").clicked() {
ui.output_mut(|output| output.copied_text = status.cmdline.clone());
}
if ui.button("Copy log").clicked() {
ui.output_mut(|output| {
output.copied_text = format!("{}\n{}", status.stdout, status.stderr)
});
}
});
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.colored_label(Color32::from_rgb(255, 0, 0), &status.log);
ui.label(&status.cmdline);
ui.colored_label(appearance.replace_color, &status.stdout);
ui.colored_label(appearance.delete_color, &status.stderr);
});
});
}
pub fn symbol_diff_ui(ui: &mut Ui, view_state: &mut ViewState) {
let (Some(result), highlighted_symbol, selected_symbol, current_view, search) = (
&view_state.build,
&mut view_state.highlighted_symbol,
&mut view_state.selected_symbol,
&mut view_state.current_view,
&mut view_state.search,
) else {
fn missing_obj_ui(ui: &mut Ui, appearance: &Appearance) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.colored_label(appearance.replace_color, "No object configured");
});
}
pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appearance) {
let DiffViewState { build, current_view, symbol_state, search, .. } = state;
let Some(result) = build else {
return;
};
@@ -242,9 +353,13 @@ pub fn symbol_diff_ui(ui: &mut Ui, view_state: &mut ViewState) {
ui.label("Build target:");
if result.first_status.success {
ui.label("OK");
if result.first_obj.is_none() {
ui.colored_label(appearance.replace_color, "Missing");
} else {
ui.label("OK");
}
} else {
ui.colored_label(Rgba::from_rgb(1.0, 0.0, 0.0), "Fail");
ui.colored_label(appearance.delete_color, "Fail");
}
});
@@ -265,11 +380,19 @@ pub fn symbol_diff_ui(ui: &mut Ui, view_state: &mut ViewState) {
ui.label("Build base:");
if result.second_status.success {
ui.label("OK");
if result.second_obj.is_none() {
ui.colored_label(appearance.replace_color, "Missing");
} else {
ui.label("OK");
}
} else {
ui.colored_label(Rgba::from_rgb(1.0, 0.0, 0.0), "Fail");
ui.colored_label(appearance.delete_color, "Fail");
}
});
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
state.queue_build = true;
}
},
);
},
@@ -277,49 +400,54 @@ pub fn symbol_diff_ui(ui: &mut Ui, view_state: &mut ViewState) {
ui.separator();
// Table
let mut ret = None;
let lower_search = search.to_ascii_lowercase();
StripBuilder::new(ui).size(Size::remainder()).vertical(|mut strip| {
strip.strip(|builder| {
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
strip.cell(|ui| {
if result.first_status.success {
if let Some(obj) = &result.first_obj {
ui.push_id("left", |ui| {
symbol_list_ui(
ui.push_id("left", |ui| {
if result.first_status.success {
if let Some(obj) = &result.first_obj {
ret = ret.or(symbol_list_ui(
ui,
obj,
highlighted_symbol,
selected_symbol,
current_view,
symbol_state,
&lower_search,
&view_state.view_config,
);
});
appearance,
));
} else {
missing_obj_ui(ui, appearance);
}
} else {
build_log_ui(ui, &result.first_status, appearance);
}
} else {
build_log_ui(ui, &result.first_status);
}
});
});
strip.cell(|ui| {
if result.second_status.success {
if let Some(obj) = &result.second_obj {
ui.push_id("right", |ui| {
symbol_list_ui(
ui.push_id("right", |ui| {
if result.second_status.success {
if let Some(obj) = &result.second_obj {
ret = ret.or(symbol_list_ui(
ui,
obj,
highlighted_symbol,
selected_symbol,
current_view,
symbol_state,
&lower_search,
&view_state.view_config,
);
});
appearance,
));
} else {
missing_obj_ui(ui, appearance);
}
} else {
build_log_ui(ui, &result.second_status, appearance);
}
} else {
build_log_ui(ui, &result.second_status);
}
});
});
});
});
});
if let Some(view) = ret {
*current_view = view;
}
}