Compare commits
77 Commits
a0371dd110
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a85c498c5 | |||
| c2fcf2797b | |||
| e88a58ba39 | |||
| 02f521a528 | |||
| 197d1247a8 | |||
| eef9598e76 | |||
| 405a2a82db | |||
| 4cdad8a519 | |||
| b74a49ed0c | |||
| e1079db93a | |||
| 879e03eed5 | |||
| 53e6e0c7c4 | |||
| 67cea2a8d9 | |||
| e4f97adbdd | |||
| 0ec7bf078b | |||
| 74e89130a8 | |||
| 236e4d8d26 | |||
| b900ae5a00 | |||
| 261e1b8e07 | |||
| a29e913b45 | |||
| 49257dc73c | |||
| dc9eec66b0 | |||
| 7b58f9a269 | |||
| d9e7dacb6d | |||
| 04b4fdcd21 | |||
| 803eaafee6 | |||
| e1dc84698f | |||
| e68629c339 | |||
| bb9ff4b928 | |||
| 57392daaeb | |||
| 2dd3dd60a8 | |||
| f4757b8d92 | |||
| 52f8c5d4f9 | |||
| 711f40b591 | |||
| 26932b2e44 | |||
| 192a06bc0b | |||
| 5bfa47fce9 | |||
| 1d9b9b6893 | |||
| 6b8e469261 | |||
| bf3ba48539 | |||
| 21cdf268f0 | |||
| 3970bc8acf | |||
| eaf0fabc2d | |||
| 91d11c83d6 | |||
| 94924047b7 | |||
| f5f6869029 | |||
| b02e32f2b7 | |||
| c7a326b160 | |||
| 100f8f8ac5 | |||
| 2f778932a4 | |||
| 42601b4750 | |||
| 636a8e00c5 | |||
|
|
cd46be7726 | ||
|
|
019493f944 | ||
| 319b1c35c0 | |||
| 634e007cbc | |||
| 6ee11ca640 | |||
| 8278d5d207 | |||
| 09bbc534bd | |||
| fa28352e08 | |||
| 2ab519d361 | |||
|
|
3406c76973 | ||
|
|
6afc535fad | ||
|
|
ec062bf5ca | ||
| 500965aacb | |||
| a8c2514377 | |||
| 4b58f69461 | |||
| cd01b6254c | |||
| bea0a0007d | |||
| ba74d63a99 | |||
|
|
20dcc50695 | ||
| c7b6ec83d7 | |||
| e2fde3dbce | |||
| 613e84ecf2 | |||
| 7219e72acf | |||
| d1d6f1101b | |||
| bc7cce7226 |
56
.github/workflows/build.yaml
vendored
@@ -9,6 +9,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
BUILD_PROFILE: release-lto
|
||||||
CARGO_BIN_NAME: objdiff
|
CARGO_BIN_NAME: objdiff
|
||||||
CARGO_TARGET_DIR: target
|
CARGO_TARGET_DIR: target
|
||||||
|
|
||||||
@@ -20,17 +21,35 @@ jobs:
|
|||||||
RUSTFLAGS: -D warnings
|
RUSTFLAGS: -D warnings
|
||||||
steps:
|
steps:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: sudo apt-get -y install libgtk-3-dev
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get -y install libgtk-3-dev
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: Setup Rust toolchain
|
- name: Setup Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
components: rustfmt, clippy
|
components: clippy
|
||||||
- name: Cargo check
|
- name: Cargo check
|
||||||
run: cargo check --all-features
|
run: cargo check
|
||||||
- name: Cargo clippy
|
- 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:
|
deny:
|
||||||
name: Deny
|
name: Deny
|
||||||
@@ -50,6 +69,7 @@ jobs:
|
|||||||
|
|
||||||
test:
|
test:
|
||||||
name: Test
|
name: Test
|
||||||
|
if: 'false' # No tests yet
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
platform: [ ubuntu-latest, windows-latest, macos-latest ]
|
platform: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||||
@@ -58,13 +78,15 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: matrix.platform == 'ubuntu-latest'
|
if: matrix.platform == 'ubuntu-latest'
|
||||||
run: sudo apt-get -y install libgtk-3-dev
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get -y install libgtk-3-dev
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: Setup Rust toolchain
|
- name: Setup Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
- name: Cargo test
|
- name: Cargo test
|
||||||
run: cargo test --release --all-features
|
run: cargo test --release
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: Build
|
||||||
@@ -75,21 +97,27 @@ jobs:
|
|||||||
target: x86_64-unknown-linux-gnu
|
target: x86_64-unknown-linux-gnu
|
||||||
name: linux-x86_64
|
name: linux-x86_64
|
||||||
packages: libgtk-3-dev
|
packages: libgtk-3-dev
|
||||||
|
features: default
|
||||||
- platform: windows-latest
|
- platform: windows-latest
|
||||||
target: x86_64-pc-windows-msvc
|
target: x86_64-pc-windows-msvc
|
||||||
name: windows-x86_64
|
name: windows-x86_64
|
||||||
|
features: default
|
||||||
- platform: macos-latest
|
- platform: macos-latest
|
||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
name: macos-x86_64
|
name: macos-x86_64
|
||||||
|
features: default
|
||||||
- platform: macos-latest
|
- platform: macos-latest
|
||||||
target: aarch64-apple-darwin
|
target: aarch64-apple-darwin
|
||||||
name: macos-arm64
|
name: macos-arm64
|
||||||
|
features: default
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: matrix.packages != ''
|
if: matrix.packages != ''
|
||||||
run: sudo apt-get -y install ${{ matrix.packages }}
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get -y install ${{ matrix.packages }}
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: Setup Rust toolchain
|
- name: Setup Rust toolchain
|
||||||
@@ -97,16 +125,16 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
- name: Cargo build
|
- 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
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.name }}
|
name: ${{ matrix.name }}
|
||||||
path: |
|
path: |
|
||||||
${{ env.CARGO_TARGET_DIR }}/release/${{ env.CARGO_BIN_NAME }}
|
${{ env.CARGO_TARGET_DIR }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
|
||||||
${{ env.CARGO_TARGET_DIR }}/release/${{ env.CARGO_BIN_NAME }}.exe
|
${{ env.CARGO_TARGET_DIR }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
|
||||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release/${{ env.CARGO_BIN_NAME }}
|
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
|
||||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release/${{ env.CARGO_BIN_NAME }}.exe
|
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
release:
|
release:
|
||||||
@@ -123,8 +151,8 @@ jobs:
|
|||||||
working-directory: artifacts
|
working-directory: artifacts
|
||||||
run: |
|
run: |
|
||||||
mkdir ../out
|
mkdir ../out
|
||||||
for i in */*/release/$CARGO_BIN_NAME*; do
|
for i in */*/$BUILD_PROFILE/$CARGO_BIN_NAME*; do
|
||||||
mv "$i" "../out/$(sed -E "s/([^/]+)\/[^/]+\/release\/($CARGO_BIN_NAME)/\2-\1/" <<< "$i")"
|
mv "$i" "../out/$(sed -E "s/([^/]+)\/[^/]+\/$BUILD_PROFILE\/($CARGO_BIN_NAME)/\2-\1/" <<< "$i")"
|
||||||
done
|
done
|
||||||
ls -R ../out
|
ls -R ../out
|
||||||
- name: Release
|
- name: Release
|
||||||
|
|||||||
4034
Cargo.lock
generated
84
Cargo.toml
@@ -1,8 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "objdiff"
|
name = "objdiff"
|
||||||
version = "0.2.0"
|
version = "1.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.62"
|
rust-version = "1.70"
|
||||||
authors = ["Luke Street <luke@street.dev>"]
|
authors = ["Luke Street <luke@street.dev>"]
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = "https://github.com/encounter/objdiff"
|
repository = "https://github.com/encounter/objdiff"
|
||||||
@@ -11,40 +11,74 @@ description = """
|
|||||||
A local diffing tool for decompilation projects.
|
A local diffing tool for decompilation projects.
|
||||||
"""
|
"""
|
||||||
publish = false
|
publish = false
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release-lto]
|
||||||
|
inherits = "release"
|
||||||
lto = "thin"
|
lto = "thin"
|
||||||
strip = "debuginfo"
|
strip = "debuginfo"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["wgpu", "wsl"]
|
||||||
|
wgpu = ["eframe/wgpu"]
|
||||||
|
wsl = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.66"
|
anyhow = "1.0.79"
|
||||||
|
byteorder = "1.5.0"
|
||||||
|
bytes = "1.5.0"
|
||||||
cfg-if = "1.0.0"
|
cfg-if = "1.0.0"
|
||||||
const_format = "0.2.30"
|
const_format = "0.2.32"
|
||||||
cwdemangle = { git = "https://github.com/encounter/cwdemangle", rev = "286f3d1d29ee2457db89043782725631845c3e4c" }
|
cwdemangle = "0.1.6"
|
||||||
eframe = { version = "0.19.0", features = ["persistence"] } # , "wgpu"
|
dirs = "5.0.1"
|
||||||
egui = "0.19.0"
|
eframe = { version = "0.25.0", features = ["persistence"] }
|
||||||
egui_extras = "0.19.0"
|
egui = "0.25.0"
|
||||||
flagset = "0.4.3"
|
egui_extras = "0.25.0"
|
||||||
log = "0.4.17"
|
filetime = "0.2.23"
|
||||||
memmap2 = "0.5.8"
|
flagset = "0.4.4"
|
||||||
notify = "5.0.0"
|
float-ord = "0.3.2"
|
||||||
object = { version = "0.30.0", features = ["read_core", "std", "elf"], default-features = false }
|
font-kit = "0.12.0"
|
||||||
ppc750cl = { git = "https://github.com/encounter/ppc750cl", rev = "aa631a33de7882c679afca89350898b87cb3ba3f" }
|
gimli = { version = "0.28.1", default-features = false, features = ["read-all"] }
|
||||||
rabbitizer = { git = "https://github.com/encounter/rabbitizer-rs", rev = "10c279b2ef251c62885b1dcdcfe740b0db8e9956" }
|
globset = { version = "0.4.14", features = ["serde1"] }
|
||||||
rfd = { version = "0.10.0" } # , default-features = false, features = ['xdg-portal']
|
log = "0.4.20"
|
||||||
self_update = "0.32.0"
|
memmap2 = "0.9.3"
|
||||||
|
notify = "6.1.1"
|
||||||
|
object = { version = "0.32.2", features = ["read_core", "std", "elf"], default-features = false }
|
||||||
|
png = "0.17.11"
|
||||||
|
pollster = "0.3.0"
|
||||||
|
ppc750cl = { git = "https://github.com/encounter/ppc750cl", rev = "4a2bbbc6f84dcb76255ab6f3595a8d4a0ce96618" }
|
||||||
|
rabbitizer = "1.8.1"
|
||||||
|
rfd = { version = "0.13.0" } #, default-features = false, features = ['xdg-portal']
|
||||||
|
ron = "0.8.1"
|
||||||
|
semver = "1.0.21"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
thiserror = "1.0.37"
|
serde_json = "1.0.111"
|
||||||
time = { version = "0.3.17", features = ["formatting", "local-offset"] }
|
serde_yaml = "0.9.30"
|
||||||
toml = "0.5.9"
|
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"
|
twox-hash = "1.6.3"
|
||||||
tempfile = "3.3.0"
|
|
||||||
reqwest = "0.11.13"
|
# For Linux static binaries, use rustls
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
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 = { version = "0.11.23", default-features = false, features = ["blocking", "json", "multipart", "default-tls"] }
|
||||||
|
self_update = "0.39.0"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
path-slash = "0.2.1"
|
path-slash = "0.2.1"
|
||||||
winapi = "0.3.9"
|
winapi = "0.3.9"
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.build-dependencies]
|
||||||
|
winres = "0.1.12"
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
exec = "0.3.1"
|
exec = "0.3.1"
|
||||||
|
|
||||||
@@ -58,5 +92,5 @@ console_error_panic_hook = "0.1.7"
|
|||||||
tracing-wasm = "0.2"
|
tracing-wasm = "0.2"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
anyhow = "1.0.66"
|
anyhow = "1.0.79"
|
||||||
vergen = { version = "7.4.3", features = ["build", "cargo", "git"], default-features = false }
|
vergen = { version = "8.3.1", features = ["build", "cargo", "git", "gitcl"] }
|
||||||
|
|||||||
137
README.md
@@ -3,16 +3,142 @@
|
|||||||
[Build Status]: https://github.com/encounter/objdiff/actions/workflows/build.yaml/badge.svg
|
[Build Status]: https://github.com/encounter/objdiff/actions/workflows/build.yaml/badge.svg
|
||||||
[actions]: https://github.com/encounter/objdiff/actions
|
[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)
|
- PowerPC 750CL (GameCube & Wii)
|
||||||
- MIPS (Nintendo 64)
|
- MIPS (Nintendo 64)
|
||||||
|
|
||||||
|
See [Usage](#usage) for more information.
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
### 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
|
Licensed under either of
|
||||||
|
|
||||||
@@ -23,6 +149,5 @@ at your option.
|
|||||||
|
|
||||||
### Contribution
|
### Contribution
|
||||||
|
|
||||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as
|
||||||
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any
|
defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
|
||||||
additional terms or conditions.
|
|
||||||
|
|||||||
BIN
assets/icon.ico
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
assets/icon.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/icon_64.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 144 KiB |
10
build.rs
@@ -1,4 +1,10 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use vergen::{vergen, Config};
|
use vergen::EmitBuilder;
|
||||||
|
|
||||||
fn main() -> Result<()> { vergen(Config::default()) }
|
fn main() -> Result<()> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
winres::WindowsResource::new().set_icon("assets/icon.ico").compile()?;
|
||||||
|
}
|
||||||
|
EmitBuilder::builder().fail_on_error().all_build().all_cargo().all_git().emit()
|
||||||
|
}
|
||||||
|
|||||||
25
deny.toml
@@ -47,9 +47,7 @@ yanked = "warn"
|
|||||||
notice = "warn"
|
notice = "warn"
|
||||||
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||||
# output a note when they are encountered.
|
# output a note when they are encountered.
|
||||||
ignore = [
|
ignore = []
|
||||||
#"RUSTSEC-0000-0000",
|
|
||||||
]
|
|
||||||
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
|
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
|
||||||
# lower than the range specified will be ignored. Note that ignored advisories
|
# lower than the range specified will be ignored. Note that ignored advisories
|
||||||
# will still output a note when they are encountered.
|
# will still output a note when they are encountered.
|
||||||
@@ -72,6 +70,7 @@ unlicensed = "deny"
|
|||||||
allow = [
|
allow = [
|
||||||
"MIT",
|
"MIT",
|
||||||
"Apache-2.0",
|
"Apache-2.0",
|
||||||
|
"Apache-2.0 WITH LLVM-exception",
|
||||||
"ISC",
|
"ISC",
|
||||||
"BSD-2-Clause",
|
"BSD-2-Clause",
|
||||||
"BSD-3-Clause",
|
"BSD-3-Clause",
|
||||||
@@ -81,6 +80,10 @@ allow = [
|
|||||||
"Unicode-DFS-2016",
|
"Unicode-DFS-2016",
|
||||||
"Zlib",
|
"Zlib",
|
||||||
"0BSD",
|
"0BSD",
|
||||||
|
"OFL-1.1",
|
||||||
|
"LicenseRef-UFL-1.0",
|
||||||
|
"OpenSSL",
|
||||||
|
"GPL-3.0",
|
||||||
]
|
]
|
||||||
# List of explictly disallowed licenses
|
# List of explictly disallowed licenses
|
||||||
# See https://spdx.org/licenses/ for list of possible licenses
|
# See https://spdx.org/licenses/ for list of possible licenses
|
||||||
@@ -118,22 +121,22 @@ exceptions = [
|
|||||||
# Some crates don't have (easily) machine readable licensing information,
|
# Some crates don't have (easily) machine readable licensing information,
|
||||||
# adding a clarification entry for it allows you to manually specify the
|
# adding a clarification entry for it allows you to manually specify the
|
||||||
# licensing information
|
# licensing information
|
||||||
#[[licenses.clarify]]
|
[[licenses.clarify]]
|
||||||
# The name of the crate the clarification applies to
|
# The name of the crate the clarification applies to
|
||||||
#name = "ring"
|
name = "ring"
|
||||||
# The optional version constraint for the crate
|
# The optional version constraint for the crate
|
||||||
#version = "*"
|
version = "*"
|
||||||
# The SPDX expression for the license requirements of the crate
|
# The SPDX expression for the license requirements of the crate
|
||||||
#expression = "MIT AND ISC AND OpenSSL"
|
expression = "MIT AND ISC AND OpenSSL"
|
||||||
# One or more files in the crate's source used as the "source of truth" for
|
# One or more files in the crate's source used as the "source of truth" for
|
||||||
# the license expression. If the contents match, the clarification will be used
|
# the license expression. If the contents match, the clarification will be used
|
||||||
# when running the license check, otherwise the clarification will be ignored
|
# when running the license check, otherwise the clarification will be ignored
|
||||||
# and the crate will be checked normally, which may produce warnings or errors
|
# and the crate will be checked normally, which may produce warnings or errors
|
||||||
# depending on the rest of your configuration
|
# depending on the rest of your configuration
|
||||||
#license-files = [
|
license-files = [
|
||||||
# Each entry is a crate relative path, and the (opaque) hash of its contents
|
# Each entry is a crate relative path, and the (opaque) hash of its contents
|
||||||
#{ path = "LICENSE", hash = 0xbd0eed23 }
|
{ path = "LICENSE", hash = 0xbd0eed23 }
|
||||||
#]
|
]
|
||||||
|
|
||||||
[licenses.private]
|
[licenses.private]
|
||||||
# If true, ignores workspace crates that aren't published, or are only
|
# If true, ignores workspace crates that aren't published, or are only
|
||||||
@@ -151,7 +154,7 @@ registries = [
|
|||||||
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
|
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
|
||||||
[bans]
|
[bans]
|
||||||
# Lint level for when multiple versions of the same crate are detected
|
# 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 `*`
|
# Lint level for when a crate version requirement is `*`
|
||||||
wildcards = "allow"
|
wildcards = "allow"
|
||||||
# The graph highlighting used when creating dotgraphs for crates
|
# The graph highlighting used when creating dotgraphs for crates
|
||||||
|
|||||||
875
src/app.rs
@@ -1,204 +1,228 @@
|
|||||||
use std::{
|
use std::{
|
||||||
default::Default,
|
default::Default,
|
||||||
ffi::OsStr,
|
fs,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
Arc, Mutex, RwLock,
|
Arc, Mutex, RwLock,
|
||||||
},
|
},
|
||||||
time::Duration,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use egui::{Color32, FontFamily, FontId, TextStyle};
|
use filetime::FileTime;
|
||||||
|
use globset::{Glob, GlobSet};
|
||||||
use notify::{RecursiveMode, Watcher};
|
use notify::{RecursiveMode, Watcher};
|
||||||
use time::{OffsetDateTime, UtcOffset};
|
use time::UtcOffset;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
app_config::{deserialize_config, AppConfigVersion},
|
||||||
|
config::{build_globset, load_project_config, ProjectObject, ProjectObjectNode, ScratchConfig},
|
||||||
|
diff::DiffAlg,
|
||||||
jobs::{
|
jobs::{
|
||||||
check_update::{queue_check_update, CheckUpdateResult},
|
objdiff::{start_build, ObjDiffConfig},
|
||||||
objdiff::{queue_build, BuildStatus, ObjDiffResult},
|
Job, JobQueue, JobResult, JobStatus,
|
||||||
Job, JobResult, JobState, JobStatus,
|
|
||||||
},
|
},
|
||||||
views::{
|
views::{
|
||||||
config::config_ui, data_diff::data_diff_ui, function_diff::function_diff_ui, jobs::jobs_ui,
|
appearance::{appearance_window, Appearance},
|
||||||
symbol_diff::symbol_diff_ui,
|
config::{
|
||||||
|
config_ui, diff_options_window, project_window, ConfigViewState, CONFIG_DISABLED_TEXT,
|
||||||
|
DEFAULT_WATCH_PATTERNS,
|
||||||
|
},
|
||||||
|
data_diff::data_diff_ui,
|
||||||
|
debug::debug_window,
|
||||||
|
demangle::{demangle_window, DemangleViewState},
|
||||||
|
frame_history::FrameHistory,
|
||||||
|
function_diff::function_diff_ui,
|
||||||
|
jobs::jobs_ui,
|
||||||
|
symbol_diff::{symbol_diff_ui, DiffViewState, View},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[allow(clippy::enum_variant_names)]
|
#[derive(Default)]
|
||||||
#[derive(Default, Eq, PartialEq)]
|
|
||||||
pub enum View {
|
|
||||||
#[default]
|
|
||||||
SymbolDiff,
|
|
||||||
FunctionDiff,
|
|
||||||
DataDiff,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
||||||
pub enum DiffKind {
|
|
||||||
#[default]
|
|
||||||
SplitObj,
|
|
||||||
WholeBinary,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
pub struct DiffConfig {
|
|
||||||
// TODO
|
|
||||||
// pub stripped_symbols: Vec<String>,
|
|
||||||
// pub mapped_symbols: HashMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_COLOR_ROTATION: [Color32; 9] = [
|
|
||||||
Color32::from_rgb(255, 0, 255),
|
|
||||||
Color32::from_rgb(0, 255, 255),
|
|
||||||
Color32::from_rgb(0, 128, 0),
|
|
||||||
Color32::from_rgb(255, 0, 0),
|
|
||||||
Color32::from_rgb(255, 255, 0),
|
|
||||||
Color32::from_rgb(255, 192, 203),
|
|
||||||
Color32::from_rgb(0, 0, 255),
|
|
||||||
Color32::from_rgb(0, 255, 0),
|
|
||||||
Color32::from_rgb(128, 128, 128),
|
|
||||||
];
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize)]
|
|
||||||
#[serde(default)]
|
|
||||||
pub struct ViewConfig {
|
|
||||||
pub ui_font: FontId,
|
|
||||||
pub code_font: FontId,
|
|
||||||
pub diff_colors: Vec<Color32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ViewConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
ui_font: FontId { size: 14.0, family: FontFamily::Proportional },
|
|
||||||
code_font: FontId { size: 14.0, family: FontFamily::Monospace },
|
|
||||||
diff_colors: DEFAULT_COLOR_ROTATION.to_vec(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize)]
|
|
||||||
#[serde(default)]
|
|
||||||
pub struct ViewState {
|
pub struct ViewState {
|
||||||
#[serde(skip)]
|
pub jobs: JobQueue,
|
||||||
pub jobs: Vec<JobState>,
|
pub config_state: ConfigViewState,
|
||||||
#[serde(skip)]
|
pub demangle_state: DemangleViewState,
|
||||||
pub build: Option<Box<ObjDiffResult>>,
|
pub diff_state: DiffViewState,
|
||||||
#[serde(skip)]
|
pub frame_history: FrameHistory,
|
||||||
pub highlighted_symbol: Option<String>,
|
pub show_appearance_config: bool,
|
||||||
#[serde(skip)]
|
|
||||||
pub selected_symbol: Option<String>,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub current_view: View,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub show_config: bool,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub show_demangle: bool,
|
pub show_demangle: bool,
|
||||||
#[serde(skip)]
|
pub show_project_config: bool,
|
||||||
pub demangle_text: String,
|
pub show_diff_options: bool,
|
||||||
#[serde(skip)]
|
pub show_debug: bool,
|
||||||
pub diff_config: DiffConfig,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub search: String,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub utc_offset: UtcOffset,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub check_update: Option<Box<CheckUpdateResult>>,
|
|
||||||
// Config
|
|
||||||
pub diff_kind: DiffKind,
|
|
||||||
pub reverse_fn_order: bool,
|
|
||||||
pub view_config: ViewConfig,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ViewState {
|
/// The configuration for a single object file.
|
||||||
|
#[derive(Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct ObjectConfig {
|
||||||
|
pub name: String,
|
||||||
|
pub target_path: Option<PathBuf>,
|
||||||
|
pub base_path: Option<PathBuf>,
|
||||||
|
pub reverse_fn_order: Option<bool>,
|
||||||
|
pub complete: Option<bool>,
|
||||||
|
pub scratch: Option<ScratchConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
|
pub struct ProjectConfigInfo {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub timestamp: FileTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn bool_true() -> bool { true }
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn default_watch_patterns() -> Vec<Glob> {
|
||||||
|
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct AppConfig {
|
||||||
|
// TODO: https://github.com/ron-rs/ron/pull/455
|
||||||
|
// #[serde(flatten)]
|
||||||
|
// pub version: AppConfigVersion,
|
||||||
|
pub version: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub custom_make: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub selected_wsl_distro: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub project_dir: Option<PathBuf>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub target_obj_dir: Option<PathBuf>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub base_obj_dir: Option<PathBuf>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub selected_obj: Option<ObjectConfig>,
|
||||||
|
#[serde(default = "bool_true")]
|
||||||
|
pub build_base: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub build_target: bool,
|
||||||
|
#[serde(default = "bool_true")]
|
||||||
|
pub rebuild_on_changes: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub auto_update_check: bool,
|
||||||
|
#[serde(default = "default_watch_patterns")]
|
||||||
|
pub watch_patterns: Vec<Glob>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub recent_projects: Vec<PathBuf>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub code_alg: DiffAlg,
|
||||||
|
#[serde(default)]
|
||||||
|
pub data_alg: DiffAlg,
|
||||||
|
#[serde(default)]
|
||||||
|
pub relax_reloc_diffs: bool,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub objects: Vec<ProjectObject>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub object_nodes: Vec<ProjectObjectNode>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub watcher_change: bool,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub config_change: bool,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub obj_change: bool,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub queue_build: bool,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub queue_reload: bool,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub queue_scratch: bool,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub project_config_info: Option<ProjectConfigInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AppConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
jobs: vec![],
|
version: AppConfigVersion::default().version,
|
||||||
build: None,
|
custom_make: None,
|
||||||
highlighted_symbol: None,
|
selected_wsl_distro: None,
|
||||||
selected_symbol: None,
|
project_dir: None,
|
||||||
current_view: Default::default(),
|
target_obj_dir: None,
|
||||||
show_config: false,
|
base_obj_dir: None,
|
||||||
show_demangle: false,
|
selected_obj: None,
|
||||||
demangle_text: String::new(),
|
build_base: true,
|
||||||
diff_config: Default::default(),
|
build_target: false,
|
||||||
search: Default::default(),
|
rebuild_on_changes: true,
|
||||||
utc_offset: UtcOffset::UTC,
|
auto_update_check: true,
|
||||||
check_update: None,
|
watch_patterns: DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(),
|
||||||
diff_kind: Default::default(),
|
recent_projects: vec![],
|
||||||
reverse_fn_order: false,
|
code_alg: Default::default(),
|
||||||
view_config: Default::default(),
|
data_alg: Default::default(),
|
||||||
|
relax_reloc_diffs: false,
|
||||||
|
objects: vec![],
|
||||||
|
object_nodes: vec![],
|
||||||
|
watcher_change: false,
|
||||||
|
config_change: false,
|
||||||
|
obj_change: false,
|
||||||
|
queue_build: false,
|
||||||
|
queue_reload: false,
|
||||||
|
queue_scratch: false,
|
||||||
|
project_config_info: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, serde::Deserialize, serde::Serialize)]
|
impl AppConfig {
|
||||||
#[serde(default)]
|
pub fn set_project_dir(&mut self, path: PathBuf) {
|
||||||
pub struct AppConfig {
|
self.recent_projects.retain(|p| p != &path);
|
||||||
pub custom_make: Option<String>,
|
if self.recent_projects.len() > 9 {
|
||||||
// WSL2 settings
|
self.recent_projects.truncate(9);
|
||||||
#[serde(skip)]
|
}
|
||||||
pub available_wsl_distros: Option<Vec<String>>,
|
self.recent_projects.insert(0, path.clone());
|
||||||
pub selected_wsl_distro: Option<String>,
|
self.project_dir = Some(path);
|
||||||
// Split obj
|
self.target_obj_dir = None;
|
||||||
pub project_dir: Option<PathBuf>,
|
self.base_obj_dir = None;
|
||||||
pub target_obj_dir: Option<PathBuf>,
|
self.selected_obj = None;
|
||||||
pub base_obj_dir: Option<PathBuf>,
|
self.build_target = false;
|
||||||
pub obj_path: Option<String>,
|
self.objects.clear();
|
||||||
pub build_target: bool,
|
self.object_nodes.clear();
|
||||||
// Whole binary
|
self.watcher_change = true;
|
||||||
pub left_obj: Option<PathBuf>,
|
self.config_change = true;
|
||||||
pub right_obj: Option<PathBuf>,
|
self.obj_change = true;
|
||||||
#[serde(skip)]
|
self.queue_build = false;
|
||||||
pub project_dir_change: bool,
|
self.project_config_info = None;
|
||||||
#[serde(skip)]
|
}
|
||||||
pub queue_update_check: bool,
|
|
||||||
pub auto_update_check: bool,
|
pub fn set_target_obj_dir(&mut self, path: PathBuf) {
|
||||||
|
self.target_obj_dir = Some(path);
|
||||||
|
self.selected_obj = None;
|
||||||
|
self.obj_change = true;
|
||||||
|
self.queue_build = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_base_obj_dir(&mut self, path: PathBuf) {
|
||||||
|
self.base_obj_dir = Some(path);
|
||||||
|
self.selected_obj = None;
|
||||||
|
self.obj_change = true;
|
||||||
|
self.queue_build = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_selected_obj(&mut self, object: ObjectConfig) {
|
||||||
|
self.selected_obj = Some(object);
|
||||||
|
self.obj_change = true;
|
||||||
|
self.queue_build = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, serde::Deserialize)]
|
pub type AppConfigRef = Arc<RwLock<AppConfig>>;
|
||||||
#[serde(default)]
|
|
||||||
pub struct ProjectConfig {
|
|
||||||
pub custom_make: Option<String>,
|
|
||||||
pub project_dir: Option<PathBuf>,
|
|
||||||
pub target_obj_dir: Option<PathBuf>,
|
|
||||||
pub base_obj_dir: Option<PathBuf>,
|
|
||||||
pub build_target: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
|
#[derive(Default)]
|
||||||
#[derive(serde::Deserialize, serde::Serialize)]
|
|
||||||
#[serde(default)]
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
|
appearance: Appearance,
|
||||||
view_state: ViewState,
|
view_state: ViewState,
|
||||||
#[serde(skip)]
|
config: AppConfigRef,
|
||||||
config: Arc<RwLock<AppConfig>>,
|
|
||||||
#[serde(skip)]
|
|
||||||
modified: Arc<AtomicBool>,
|
modified: Arc<AtomicBool>,
|
||||||
#[serde(skip)]
|
|
||||||
watcher: Option<notify::RecommendedWatcher>,
|
watcher: Option<notify::RecommendedWatcher>,
|
||||||
#[serde(skip)]
|
|
||||||
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
|
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
|
||||||
#[serde(skip)]
|
|
||||||
should_relaunch: bool,
|
should_relaunch: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for App {
|
pub const APPEARANCE_KEY: &str = "appearance";
|
||||||
fn default() -> Self {
|
pub const CONFIG_KEY: &str = "app_config";
|
||||||
Self {
|
|
||||||
view_state: ViewState::default(),
|
|
||||||
config: Arc::new(Default::default()),
|
|
||||||
modified: Arc::new(Default::default()),
|
|
||||||
watcher: None,
|
|
||||||
relaunch_path: Default::default(),
|
|
||||||
should_relaunch: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const CONFIG_KEY: &str = "app_config";
|
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
/// Called once before the first frame.
|
/// Called once before the first frame.
|
||||||
@@ -207,232 +231,54 @@ impl App {
|
|||||||
utc_offset: UtcOffset,
|
utc_offset: UtcOffset,
|
||||||
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
|
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
// This is also where you can customized the look at feel of egui using
|
|
||||||
// `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`.
|
|
||||||
|
|
||||||
// Load previous app state (if any).
|
// Load previous app state (if any).
|
||||||
// Note that you must enable the `persistence` feature for this to work.
|
// Note that you must enable the `persistence` feature for this to work.
|
||||||
if let Some(storage) = cc.storage {
|
|
||||||
let mut app: App = eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
|
|
||||||
let mut config: AppConfig = eframe::get_value(storage, CONFIG_KEY).unwrap_or_default();
|
|
||||||
if config.project_dir.is_some() {
|
|
||||||
config.project_dir_change = true;
|
|
||||||
}
|
|
||||||
config.queue_update_check = config.auto_update_check;
|
|
||||||
app.config = Arc::new(RwLock::new(config));
|
|
||||||
app.view_state.utc_offset = utc_offset;
|
|
||||||
app.relaunch_path = relaunch_path;
|
|
||||||
app
|
|
||||||
} else {
|
|
||||||
let mut app = Self::default();
|
let mut app = Self::default();
|
||||||
app.view_state.utc_offset = utc_offset;
|
if let Some(storage) = cc.storage {
|
||||||
|
if let Some(appearance) = eframe::get_value::<Appearance>(storage, APPEARANCE_KEY) {
|
||||||
|
app.appearance = appearance;
|
||||||
|
}
|
||||||
|
if let Some(mut config) = deserialize_config(storage) {
|
||||||
|
if config.project_dir.is_some() {
|
||||||
|
config.config_change = true;
|
||||||
|
config.watcher_change = true;
|
||||||
|
}
|
||||||
|
if config.selected_obj.is_some() {
|
||||||
|
config.queue_build = true;
|
||||||
|
}
|
||||||
|
app.view_state.config_state.queue_check_update = config.auto_update_check;
|
||||||
|
app.config = Arc::new(RwLock::new(config));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.appearance.init_fonts(&cc.egui_ctx);
|
||||||
|
app.appearance.utc_offset = utc_offset;
|
||||||
app.relaunch_path = relaunch_path;
|
app.relaunch_path = relaunch_path;
|
||||||
app
|
app
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl eframe::App for App {
|
fn pre_update(&mut self, ctx: &egui::Context) {
|
||||||
/// Called each time the UI needs repainting, which may be many times per second.
|
self.appearance.pre_update(ctx);
|
||||||
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
|
|
||||||
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
|
||||||
if self.should_relaunch {
|
|
||||||
frame.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Self { config, view_state, .. } = self;
|
let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state;
|
||||||
|
|
||||||
{
|
let mut results = vec![];
|
||||||
let config = &view_state.view_config;
|
for (job, result) in jobs.iter_finished() {
|
||||||
let mut style = (*ctx.style()).clone();
|
match result {
|
||||||
style.text_styles.insert(TextStyle::Body, FontId {
|
|
||||||
size: (config.ui_font.size * 0.75).floor(),
|
|
||||||
family: config.ui_font.family.clone(),
|
|
||||||
});
|
|
||||||
style.text_styles.insert(TextStyle::Body, config.ui_font.clone());
|
|
||||||
style.text_styles.insert(TextStyle::Button, config.ui_font.clone());
|
|
||||||
style.text_styles.insert(TextStyle::Heading, FontId {
|
|
||||||
size: (config.ui_font.size * 1.5).floor(),
|
|
||||||
family: config.ui_font.family.clone(),
|
|
||||||
});
|
|
||||||
style.text_styles.insert(TextStyle::Monospace, config.code_font.clone());
|
|
||||||
ctx.set_style(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
|
||||||
egui::menu::bar(ui, |ui| {
|
|
||||||
ui.menu_button("File", |ui| {
|
|
||||||
if ui.button("Show config").clicked() {
|
|
||||||
view_state.show_config = !view_state.show_config;
|
|
||||||
}
|
|
||||||
if ui.button("Quit").clicked() {
|
|
||||||
frame.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ui.menu_button("Tools", |ui| {
|
|
||||||
if ui.button("Demangle").clicked() {
|
|
||||||
view_state.show_demangle = !view_state.show_demangle;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if view_state.current_view == View::FunctionDiff
|
|
||||||
&& matches!(&view_state.build, Some(b) if b.first_status.success && b.second_status.success)
|
|
||||||
{
|
|
||||||
// egui::SidePanel::left("side_panel").show(ctx, |ui| {
|
|
||||||
// if ui.button("Back").clicked() {
|
|
||||||
// view_state.current_view = View::SymbolDiff;
|
|
||||||
// }
|
|
||||||
// ui.separator();
|
|
||||||
// jobs_ui(ui, view_state);
|
|
||||||
// });
|
|
||||||
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
|
||||||
if function_diff_ui(ui, view_state) {
|
|
||||||
view_state
|
|
||||||
.jobs
|
|
||||||
.push(queue_build(config.clone(), view_state.diff_config.clone()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if view_state.current_view == View::DataDiff
|
|
||||||
&& matches!(&view_state.build, Some(b) if b.first_status.success && b.second_status.success)
|
|
||||||
{
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
|
||||||
if data_diff_ui(ui, view_state) {
|
|
||||||
view_state
|
|
||||||
.jobs
|
|
||||||
.push(queue_build(config.clone(), view_state.diff_config.clone()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
egui::SidePanel::left("side_panel").show(ctx, |ui| {
|
|
||||||
config_ui(ui, config, view_state);
|
|
||||||
jobs_ui(ui, view_state);
|
|
||||||
});
|
|
||||||
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
|
||||||
symbol_diff_ui(ui, view_state);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
egui::Window::new("Config").open(&mut view_state.show_config).show(ctx, |ui| {
|
|
||||||
ui.label("UI font:");
|
|
||||||
egui::introspection::font_id_ui(ui, &mut view_state.view_config.ui_font);
|
|
||||||
ui.separator();
|
|
||||||
ui.label("Code font:");
|
|
||||||
egui::introspection::font_id_ui(ui, &mut view_state.view_config.code_font);
|
|
||||||
ui.separator();
|
|
||||||
ui.label("Diff colors:");
|
|
||||||
if ui.button("Reset").clicked() {
|
|
||||||
view_state.view_config.diff_colors = DEFAULT_COLOR_ROTATION.to_vec();
|
|
||||||
}
|
|
||||||
let mut remove_at: Option<usize> = None;
|
|
||||||
let num_colors = view_state.view_config.diff_colors.len();
|
|
||||||
for (idx, color) in view_state.view_config.diff_colors.iter_mut().enumerate() {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.color_edit_button_srgba(color);
|
|
||||||
if num_colors > 1 && ui.small_button("-").clicked() {
|
|
||||||
remove_at = Some(idx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if let Some(idx) = remove_at {
|
|
||||||
view_state.view_config.diff_colors.remove(idx);
|
|
||||||
}
|
|
||||||
if ui.small_button("+").clicked() {
|
|
||||||
view_state.view_config.diff_colors.push(Color32::BLACK);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
egui::Window::new("Demangle").open(&mut view_state.show_demangle).show(ctx, |ui| {
|
|
||||||
ui.text_edit_singleline(&mut view_state.demangle_text);
|
|
||||||
ui.add_space(10.0);
|
|
||||||
if let Some(demangled) =
|
|
||||||
cwdemangle::demangle(&view_state.demangle_text, &Default::default())
|
|
||||||
{
|
|
||||||
ui.scope(|ui| {
|
|
||||||
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
|
|
||||||
ui.colored_label(Color32::LIGHT_BLUE, &demangled);
|
|
||||||
});
|
|
||||||
if ui.button("Copy").clicked() {
|
|
||||||
ui.output().copied_text = demangled;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ui.scope(|ui| {
|
|
||||||
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
|
|
||||||
ui.colored_label(Color32::LIGHT_RED, "[invalid]");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Windows + request_repaint_after breaks dialogs:
|
|
||||||
// https://github.com/emilk/egui/issues/2003
|
|
||||||
if cfg!(windows)
|
|
||||||
|| view_state.jobs.iter().any(|job| {
|
|
||||||
if let Some(handle) = &job.handle {
|
|
||||||
return !handle.is_finished();
|
|
||||||
}
|
|
||||||
false
|
|
||||||
})
|
|
||||||
{
|
|
||||||
ctx.request_repaint();
|
|
||||||
} else {
|
|
||||||
ctx.request_repaint_after(Duration::from_millis(100));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Called by the frame work to save state before shutdown.
|
|
||||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
|
||||||
if let Ok(config) = self.config.read() {
|
|
||||||
eframe::set_value(storage, CONFIG_KEY, &*config);
|
|
||||||
}
|
|
||||||
eframe::set_value(storage, eframe::APP_KEY, self);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn post_rendering(&mut self, _window_size_px: [u32; 2], _frame: &eframe::Frame) {
|
|
||||||
for job in &mut self.view_state.jobs {
|
|
||||||
if let Some(handle) = &job.handle {
|
|
||||||
if !handle.is_finished() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
match job.handle.take().unwrap().join() {
|
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
log::info!("Job {} finished", job.id);
|
log::info!("Job {} finished", job.id);
|
||||||
match result {
|
match result {
|
||||||
JobResult::None => {
|
JobResult::None => {
|
||||||
if let Some(err) = &job.status.read().unwrap().error {
|
if let Some(err) = &job.context.status.read().unwrap().error {
|
||||||
log::error!("{:?}", err);
|
log::error!("{:?}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
JobResult::ObjDiff(state) => {
|
|
||||||
self.view_state.build = Some(state);
|
|
||||||
}
|
|
||||||
JobResult::BinDiff(state) => {
|
|
||||||
self.view_state.build = Some(Box::new(ObjDiffResult {
|
|
||||||
first_status: BuildStatus {
|
|
||||||
success: true,
|
|
||||||
log: "".to_string(),
|
|
||||||
},
|
|
||||||
second_status: BuildStatus {
|
|
||||||
success: true,
|
|
||||||
log: "".to_string(),
|
|
||||||
},
|
|
||||||
first_obj: Some(state.first_obj),
|
|
||||||
second_obj: Some(state.second_obj),
|
|
||||||
time: OffsetDateTime::now_utc(),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
JobResult::CheckUpdate(state) => {
|
|
||||||
self.view_state.check_update = Some(state);
|
|
||||||
}
|
|
||||||
JobResult::Update(state) => {
|
JobResult::Update(state) => {
|
||||||
if let Ok(mut guard) = self.relaunch_path.lock() {
|
if let Ok(mut guard) = self.relaunch_path.lock() {
|
||||||
*guard = Some(state.exe_path);
|
*guard = Some(state.exe_path);
|
||||||
}
|
}
|
||||||
self.should_relaunch = true;
|
self.should_relaunch = true;
|
||||||
}
|
}
|
||||||
|
_ => results.push(result),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -443,12 +289,12 @@ impl eframe::App for App {
|
|||||||
} else {
|
} else {
|
||||||
anyhow::Error::msg("Thread panicked")
|
anyhow::Error::msg("Thread panicked")
|
||||||
};
|
};
|
||||||
let result = job.status.write();
|
let result = job.context.status.write();
|
||||||
if let Ok(mut guard) = result {
|
if let Ok(mut guard) = result {
|
||||||
guard.error = Some(err);
|
guard.error = Some(err);
|
||||||
} else {
|
} else {
|
||||||
drop(result);
|
drop(result);
|
||||||
job.status = Arc::new(RwLock::new(JobStatus {
|
job.context.status = Arc::new(RwLock::new(JobStatus {
|
||||||
title: "Error".to_string(),
|
title: "Error".to_string(),
|
||||||
progress_percent: 0.0,
|
progress_percent: 0.0,
|
||||||
progress_items: None,
|
progress_items: None,
|
||||||
@@ -459,81 +305,292 @@ impl eframe::App for App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
jobs.results.append(&mut results);
|
||||||
|
jobs.clear_finished();
|
||||||
|
|
||||||
|
diff_state.pre_update(jobs, &self.config);
|
||||||
|
config_state.pre_update(jobs, &self.config);
|
||||||
|
debug_assert!(jobs.results.is_empty());
|
||||||
}
|
}
|
||||||
if self.view_state.jobs.iter().any(|v| v.should_remove) {
|
|
||||||
let mut i = 0;
|
fn post_update(&mut self, ctx: &egui::Context) {
|
||||||
while i < self.view_state.jobs.len() {
|
self.appearance.post_update(ctx);
|
||||||
let job = &self.view_state.jobs[i];
|
|
||||||
if job.should_remove
|
let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state;
|
||||||
&& job.handle.is_none()
|
config_state.post_update(ctx, jobs, &self.config);
|
||||||
&& job.status.read().unwrap().error.is_none()
|
diff_state.post_update(ctx, jobs, &self.config);
|
||||||
{
|
|
||||||
self.view_state.jobs.remove(i);
|
let Ok(mut config) = self.config.write() else {
|
||||||
} else {
|
return;
|
||||||
i += 1;
|
};
|
||||||
|
let config = &mut *config;
|
||||||
|
|
||||||
|
if let Some(info) = &config.project_config_info {
|
||||||
|
if file_modified(&info.path, info.timestamp) {
|
||||||
|
config.config_change = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.config_change {
|
||||||
|
config.config_change = false;
|
||||||
|
match load_project_config(config) {
|
||||||
|
Ok(()) => config_state.load_error = None,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to load project config: {e}");
|
||||||
|
config_state.load_error = Some(format!("{e}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(mut config) = self.config.write() {
|
if config.watcher_change {
|
||||||
if config.project_dir_change {
|
|
||||||
drop(self.watcher.take());
|
drop(self.watcher.take());
|
||||||
|
|
||||||
if let Some(project_dir) = &config.project_dir {
|
if let Some(project_dir) = &config.project_dir {
|
||||||
match create_watcher(self.modified.clone(), project_dir) {
|
match build_globset(&config.watch_patterns).map_err(anyhow::Error::new).and_then(
|
||||||
|
|globset| {
|
||||||
|
create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset)
|
||||||
|
.map_err(anyhow::Error::new)
|
||||||
|
},
|
||||||
|
) {
|
||||||
Ok(watcher) => self.watcher = Some(watcher),
|
Ok(watcher) => self.watcher = Some(watcher),
|
||||||
Err(e) => eprintln!("Failed to create watcher: {e}"),
|
Err(e) => log::error!("Failed to create watcher: {e}"),
|
||||||
}
|
}
|
||||||
config.project_dir_change = false;
|
config.watcher_change = false;
|
||||||
self.modified.store(true, Ordering::Relaxed);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.obj_path.is_some() && self.modified.load(Ordering::Relaxed) {
|
if config.obj_change {
|
||||||
if !self
|
*diff_state = Default::default();
|
||||||
.view_state
|
if config.selected_obj.is_some() {
|
||||||
.jobs
|
config.queue_build = true;
|
||||||
.iter()
|
}
|
||||||
.any(|j| j.job_type == Job::ObjDiff && j.handle.is_some())
|
config.obj_change = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.modified.swap(false, Ordering::Relaxed) && config.rebuild_on_changes {
|
||||||
|
config.queue_build = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(result) = &diff_state.build {
|
||||||
|
if let Some(obj) = &result.first_obj {
|
||||||
|
if file_modified(&obj.path, obj.timestamp) {
|
||||||
|
config.queue_reload = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(obj) = &result.second_obj {
|
||||||
|
if file_modified(&obj.path, obj.timestamp) {
|
||||||
|
config.queue_reload = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't clear `queue_build` if a build is running. A file may have been modified during
|
||||||
|
// the build, so we'll start another build after the current one finishes.
|
||||||
|
if config.queue_build && config.selected_obj.is_some() && !jobs.is_running(Job::ObjDiff) {
|
||||||
|
jobs.push(start_build(ctx, ObjDiffConfig::from_config(config)));
|
||||||
|
config.queue_build = false;
|
||||||
|
config.queue_reload = false;
|
||||||
|
} else if config.queue_reload && !jobs.is_running(Job::ObjDiff) {
|
||||||
|
let mut diff_config = ObjDiffConfig::from_config(config);
|
||||||
|
// Don't build, just reload the current files
|
||||||
|
diff_config.build_base = false;
|
||||||
|
diff_config.build_target = false;
|
||||||
|
jobs.push(start_build(ctx, diff_config));
|
||||||
|
config.queue_reload = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for App {
|
||||||
|
/// Called each time the UI needs repainting, which may be many times per second.
|
||||||
|
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
|
||||||
|
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||||
|
if self.should_relaunch {
|
||||||
|
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pre_update(ctx);
|
||||||
|
|
||||||
|
let Self { config, appearance, view_state, .. } = self;
|
||||||
|
let ViewState {
|
||||||
|
jobs,
|
||||||
|
config_state,
|
||||||
|
demangle_state,
|
||||||
|
diff_state,
|
||||||
|
frame_history,
|
||||||
|
show_appearance_config,
|
||||||
|
show_demangle,
|
||||||
|
show_project_config,
|
||||||
|
show_diff_options,
|
||||||
|
show_debug,
|
||||||
|
} = view_state;
|
||||||
|
|
||||||
|
frame_history.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
|
||||||
|
|
||||||
|
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
||||||
|
egui::menu::bar(ui, |ui| {
|
||||||
|
ui.menu_button("File", |ui| {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
if ui.button("Debug…").clicked() {
|
||||||
|
*show_debug = !*show_debug;
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
if ui.button("Project…").clicked() {
|
||||||
|
*show_project_config = !*show_project_config;
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
let recent_projects = if let Ok(guard) = config.read() {
|
||||||
|
guard.recent_projects.clone()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
if recent_projects.is_empty() {
|
||||||
|
ui.add_enabled(false, egui::Button::new("Recent projects…"));
|
||||||
|
} else {
|
||||||
|
ui.menu_button("Recent Projects…", |ui| {
|
||||||
|
if ui.button("Clear").clicked() {
|
||||||
|
config.write().unwrap().recent_projects.clear();
|
||||||
|
};
|
||||||
|
ui.separator();
|
||||||
|
for path in recent_projects {
|
||||||
|
if ui.button(format!("{}", path.display())).clicked() {
|
||||||
|
config.write().unwrap().set_project_dir(path);
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if ui.button("Appearance…").clicked() {
|
||||||
|
*show_appearance_config = !*show_appearance_config;
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
if ui.button("Quit").clicked() {
|
||||||
|
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.menu_button("Tools", |ui| {
|
||||||
|
if ui.button("Demangle…").clicked() {
|
||||||
|
*show_demangle = !*show_demangle;
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.menu_button("Diff Options", |ui| {
|
||||||
|
if ui.button("Algorithm…").clicked() {
|
||||||
|
*show_diff_options = !*show_diff_options;
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
let mut config = config.write().unwrap();
|
||||||
|
let response = ui
|
||||||
|
.checkbox(&mut config.rebuild_on_changes, "Rebuild on changes")
|
||||||
|
.on_hover_text("Automatically re-run the build & diff when files change.");
|
||||||
|
if response.changed() {
|
||||||
|
config.watcher_change = true;
|
||||||
|
};
|
||||||
|
ui.add_enabled(
|
||||||
|
!diff_state.symbol_state.disable_reverse_fn_order,
|
||||||
|
egui::Checkbox::new(
|
||||||
|
&mut diff_state.symbol_state.reverse_fn_order,
|
||||||
|
"Reverse function order (-inline deferred)",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.on_disabled_hover_text(CONFIG_DISABLED_TEXT);
|
||||||
|
ui.checkbox(
|
||||||
|
&mut diff_state.symbol_state.show_hidden_symbols,
|
||||||
|
"Show hidden symbols",
|
||||||
|
);
|
||||||
|
if ui
|
||||||
|
.checkbox(&mut config.relax_reloc_diffs, "Relax relocation diffs")
|
||||||
|
.on_hover_text(
|
||||||
|
"Ignores differences in relocation targets. (Address, name, etc)",
|
||||||
|
)
|
||||||
|
.changed()
|
||||||
{
|
{
|
||||||
self.view_state.jobs.push(queue_build(
|
config.queue_reload = true;
|
||||||
self.config.clone(),
|
|
||||||
self.view_state.diff_config.clone(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
self.modified.store(false, Ordering::Relaxed);
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let build_success = matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success);
|
||||||
|
if diff_state.current_view == View::FunctionDiff && build_success {
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
function_diff_ui(ui, diff_state, appearance);
|
||||||
|
});
|
||||||
|
} else if diff_state.current_view == View::DataDiff && build_success {
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
data_diff_ui(ui, diff_state, appearance);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
egui::SidePanel::left("side_panel").show(ctx, |ui| {
|
||||||
|
egui::ScrollArea::both().show(ui, |ui| {
|
||||||
|
config_ui(ui, config, show_project_config, config_state, appearance);
|
||||||
|
jobs_ui(ui, jobs, appearance);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
symbol_diff_ui(ui, diff_state, appearance);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.queue_update_check {
|
project_window(ctx, config, show_project_config, config_state, appearance);
|
||||||
self.view_state.jobs.push(queue_check_update());
|
appearance_window(ctx, show_appearance_config, appearance);
|
||||||
config.queue_update_check = false;
|
demangle_window(ctx, show_demangle, demangle_state, appearance);
|
||||||
|
diff_options_window(ctx, config, show_diff_options, appearance);
|
||||||
|
debug_window(ctx, show_debug, frame_history, appearance);
|
||||||
|
|
||||||
|
self.post_update(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Called by the frame work to save state before shutdown.
|
||||||
|
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||||
|
if let Ok(config) = self.config.read() {
|
||||||
|
eframe::set_value(storage, CONFIG_KEY, &*config);
|
||||||
}
|
}
|
||||||
|
eframe::set_value(storage, APPEARANCE_KEY, &self.appearance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_watcher(
|
fn create_watcher(
|
||||||
|
ctx: egui::Context,
|
||||||
modified: Arc<AtomicBool>,
|
modified: Arc<AtomicBool>,
|
||||||
project_dir: &Path,
|
project_dir: &Path,
|
||||||
|
patterns: GlobSet,
|
||||||
) -> notify::Result<notify::RecommendedWatcher> {
|
) -> notify::Result<notify::RecommendedWatcher> {
|
||||||
|
let base_dir = project_dir.to_owned();
|
||||||
let mut watcher =
|
let mut watcher =
|
||||||
notify::recommended_watcher(move |res: notify::Result<notify::Event>| match res {
|
notify::recommended_watcher(move |res: notify::Result<notify::Event>| match res {
|
||||||
Ok(event) => {
|
Ok(event) => {
|
||||||
if matches!(event.kind, notify::EventKind::Modify(..)) {
|
if matches!(
|
||||||
let watch_extensions = &[
|
event.kind,
|
||||||
Some(OsStr::new("c")),
|
notify::EventKind::Modify(..)
|
||||||
Some(OsStr::new("cp")),
|
| notify::EventKind::Create(..)
|
||||||
Some(OsStr::new("cpp")),
|
| notify::EventKind::Remove(..)
|
||||||
Some(OsStr::new("h")),
|
) {
|
||||||
Some(OsStr::new("hpp")),
|
for path in &event.paths {
|
||||||
Some(OsStr::new("s")),
|
let Ok(path) = path.strip_prefix(&base_dir) else {
|
||||||
];
|
continue;
|
||||||
if event.paths.iter().any(|p| watch_extensions.contains(&p.extension())) {
|
};
|
||||||
|
if patterns.is_match(path) {
|
||||||
|
log::info!("File modified: {}", path.display());
|
||||||
modified.store(true, Ordering::Relaxed);
|
modified.store(true, Ordering::Relaxed);
|
||||||
|
ctx.request_repaint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => println!("watch error: {e:?}"),
|
}
|
||||||
|
Err(e) => log::error!("watch error: {e:?}"),
|
||||||
})?;
|
})?;
|
||||||
watcher.watch(project_dir, RecursiveMode::Recursive)?;
|
watcher.watch(project_dir, RecursiveMode::Recursive)?;
|
||||||
Ok(watcher)
|
Ok(watcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn file_modified(path: &Path, last_ts: FileTime) -> bool {
|
||||||
|
if let Ok(metadata) = fs::metadata(path) {
|
||||||
|
FileTime::from_last_modification_time(&metadata) != last_ts
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
97
src/app_config.rs
Normal 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
@@ -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()
|
||||||
|
}
|
||||||
691
src/diff.rs
@@ -1,691 +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],
|
|
||||||
) -> Result<()> {
|
|
||||||
let code =
|
|
||||||
&data[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
|
|
||||||
let (_, ins) = match arch {
|
|
||||||
ObjArchitecture::PowerPc => ppc::process_code(code, symbol.address, relocs)?,
|
|
||||||
ObjArchitecture::Mips => {
|
|
||||||
mips::process_code(code, symbol.address, symbol.address + symbol.size, relocs)?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut diff = Vec::<ObjInsDiff>::new();
|
|
||||||
for i in ins {
|
|
||||||
diff.push(ObjInsDiff { ins: Some(i), kind: ObjInsDiffKind::None, ..Default::default() });
|
|
||||||
}
|
|
||||||
resolve_branches(&mut diff);
|
|
||||||
symbol.instructions = diff;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn diff_code(
|
|
||||||
arch: ObjArchitecture,
|
|
||||||
left_data: &[u8],
|
|
||||||
right_data: &[u8],
|
|
||||||
left_symbol: &mut ObjSymbol,
|
|
||||||
right_symbol: &mut ObjSymbol,
|
|
||||||
left_relocs: &[ObjReloc],
|
|
||||||
right_relocs: &[ObjReloc],
|
|
||||||
) -> Result<()> {
|
|
||||||
let left_code = &left_data[left_symbol.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)?,
|
|
||||||
ppc::process_code(right_code, right_symbol.address, right_relocs)?,
|
|
||||||
),
|
|
||||||
ObjArchitecture::Mips => (
|
|
||||||
mips::process_code(
|
|
||||||
left_code,
|
|
||||||
left_symbol.address,
|
|
||||||
left_symbol.address + left_symbol.size,
|
|
||||||
left_relocs,
|
|
||||||
)?,
|
|
||||||
mips::process_code(
|
|
||||||
right_code,
|
|
||||||
right_symbol.address,
|
|
||||||
left_symbol.address + left_symbol.size,
|
|
||||||
right_relocs,
|
|
||||||
)?,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut left_diff = Vec::<ObjInsDiff>::new();
|
|
||||||
let mut right_diff = Vec::<ObjInsDiff>::new();
|
|
||||||
let edit_ops = editops_find(&left_ops, &right_ops);
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut op_iter = edit_ops.iter();
|
|
||||||
let mut left_iter = left_insts.iter();
|
|
||||||
let mut right_iter = right_insts.iter();
|
|
||||||
let mut cur_op = op_iter.next();
|
|
||||||
let mut cur_left = left_iter.next();
|
|
||||||
let mut cur_right = right_iter.next();
|
|
||||||
while let Some(op) = cur_op {
|
|
||||||
let left_addr = op.first_start as u32 * 4;
|
|
||||||
let right_addr = op.second_start as u32 * 4;
|
|
||||||
while let (Some(left), Some(right)) = (cur_left, cur_right) {
|
|
||||||
if (left.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();
|
|
||||||
}
|
|
||||||
LevEditType::Keep => unreachable!(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
cur_op = op_iter.next();
|
|
||||||
}
|
|
||||||
// Finalize
|
|
||||||
while cur_left.is_some() || cur_right.is_some() {
|
|
||||||
left_diff.push(ObjInsDiff { ins: cur_left.cloned(), ..ObjInsDiff::default() });
|
|
||||||
right_diff.push(ObjInsDiff { ins: cur_right.cloned(), ..ObjInsDiff::default() });
|
|
||||||
cur_left = left_iter.next();
|
|
||||||
cur_right = right_iter.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve_branches(&mut left_diff);
|
|
||||||
resolve_branches(&mut right_diff);
|
|
||||||
|
|
||||||
let mut diff_state = InsDiffState::default();
|
|
||||||
for (left, right) in left_diff.iter_mut().zip(right_diff.iter_mut()) {
|
|
||||||
let result = compare_ins(left, right, &mut diff_state)?;
|
|
||||||
left.kind = result.kind;
|
|
||||||
right.kind = result.kind;
|
|
||||||
left.arg_diff = result.left_args_diff;
|
|
||||||
right.arg_diff = result.right_args_diff;
|
|
||||||
}
|
|
||||||
|
|
||||||
let total = left_insts.len();
|
|
||||||
let percent = 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 {
|
|
||||||
if let (Some(left), Some(right)) = (left_reloc, right_reloc) {
|
|
||||||
if left.kind != right.kind {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let name_matches = left.target.name == right.target.name;
|
|
||||||
match (&left.target_section, &right.target_section) {
|
|
||||||
(Some(sl), Some(sr)) => {
|
|
||||||
// Match if section and name or address match
|
|
||||||
sl == sr && (name_matches || 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,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
matches!(right, ObjInsArg::MipsArg(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) => 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) => 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<'a>(obj: &'a mut ObjInfo, name: &str) -> Option<&'a mut ObjSection> {
|
|
||||||
obj.sections.iter_mut().find(|s| s.name == name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_symbol<'a>(symbols: &'a mut [ObjSymbol], name: &str) -> Option<&'a mut ObjSymbol> {
|
|
||||||
symbols.iter_mut().find(|s| s.name == name)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn diff_objs(left: &mut ObjInfo, right: &mut ObjInfo, _diff_config: &DiffConfig) -> Result<()> {
|
|
||||||
for left_section in &mut left.sections {
|
|
||||||
if let Some(right_section) = find_section(right, &left_section.name) {
|
|
||||||
if left_section.kind == ObjSectionKind::Code {
|
|
||||||
for left_symbol in &mut left_section.symbols {
|
|
||||||
if let Some(right_symbol) =
|
|
||||||
find_symbol(&mut right_section.symbols, &left_symbol.name)
|
|
||||||
{
|
|
||||||
left_symbol.diff_symbol = Some(right_symbol.name.clone());
|
|
||||||
right_symbol.diff_symbol = Some(left_symbol.name.clone());
|
|
||||||
diff_code(
|
|
||||||
left.architecture,
|
|
||||||
&left_section.data,
|
|
||||||
&right_section.data,
|
|
||||||
left_symbol,
|
|
||||||
right_symbol,
|
|
||||||
&left_section.relocations,
|
|
||||||
&right_section.relocations,
|
|
||||||
)?;
|
|
||||||
} else {
|
|
||||||
no_diff_code(
|
|
||||||
left.architecture,
|
|
||||||
&left_section.data,
|
|
||||||
left_symbol,
|
|
||||||
&left_section.relocations,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for right_symbol in &mut right_section.symbols {
|
|
||||||
if right_symbol.instructions.is_empty() {
|
|
||||||
no_diff_code(
|
|
||||||
left.architecture,
|
|
||||||
&right_section.data,
|
|
||||||
right_symbol,
|
|
||||||
&right_section.relocations,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if left_section.kind == ObjSectionKind::Data {
|
|
||||||
diff_data(left_section, right_section);
|
|
||||||
// diff_data_symbols(left_section, right_section)?;
|
|
||||||
} else if left_section.kind == ObjSectionKind::Bss {
|
|
||||||
diff_bss_symbols(&mut left_section.symbols, &mut right_section.symbols)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
diff_bss_symbols(&mut left.common, &mut right.common)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn diff_bss_symbols(left_symbols: &mut [ObjSymbol], right_symbols: &mut [ObjSymbol]) -> Result<()> {
|
|
||||||
for left_symbol in left_symbols {
|
|
||||||
if let Some(right_symbol) = find_symbol(right_symbols, &left_symbol.name) {
|
|
||||||
left_symbol.diff_symbol = Some(right_symbol.name.clone());
|
|
||||||
right_symbol.diff_symbol = Some(left_symbol.name.clone());
|
|
||||||
let percent = if left_symbol.size == right_symbol.size { 100.0 } else { 50.0 };
|
|
||||||
left_symbol.match_percent = Some(percent);
|
|
||||||
right_symbol.match_percent = Some(percent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// WIP diff-by-symbol
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn diff_data_symbols(left: &mut ObjSection, right: &mut ObjSection) -> Result<()> {
|
|
||||||
let mut left_ops = Vec::<u32>::with_capacity(left.symbols.len());
|
|
||||||
let mut right_ops = Vec::<u32>::with_capacity(right.symbols.len());
|
|
||||||
for left_symbol in &left.symbols {
|
|
||||||
let data = &left.data
|
|
||||||
[left_symbol.address as usize..(left_symbol.address + left_symbol.size) as usize];
|
|
||||||
let hash = twox_hash::xxh3::hash64(data);
|
|
||||||
left_ops.push(hash as u32);
|
|
||||||
}
|
|
||||||
for symbol in &right.symbols {
|
|
||||||
let data = &right.data[symbol.address as usize..(symbol.address + symbol.size) as usize];
|
|
||||||
let hash = twox_hash::xxh3::hash64(data);
|
|
||||||
right_ops.push(hash as u32);
|
|
||||||
}
|
|
||||||
|
|
||||||
let edit_ops = editops_find(&left_ops, &right_ops);
|
|
||||||
if edit_ops.is_empty() && !left.data.is_empty() {
|
|
||||||
let mut left_iter = left.symbols.iter_mut();
|
|
||||||
let mut right_iter = right.symbols.iter_mut();
|
|
||||||
loop {
|
|
||||||
let (left_symbol, right_symbol) = match (left_iter.next(), right_iter.next()) {
|
|
||||||
(Some(l), Some(r)) => (l, r),
|
|
||||||
(None, None) => break,
|
|
||||||
_ => return Err(anyhow::Error::msg("L/R mismatch in diff_data_symbols")),
|
|
||||||
};
|
|
||||||
let left_data = &left.data
|
|
||||||
[left_symbol.address as usize..(left_symbol.address + left_symbol.size) as usize];
|
|
||||||
let right_data = &right.data[right_symbol.address as usize
|
|
||||||
..(right_symbol.address + right_symbol.size) as usize];
|
|
||||||
|
|
||||||
left.data_diff.push(ObjDataDiff {
|
|
||||||
data: left_data.to_vec(),
|
|
||||||
kind: ObjDataDiffKind::None,
|
|
||||||
len: left_symbol.size as usize,
|
|
||||||
symbol: left_symbol.name.clone(),
|
|
||||||
});
|
|
||||||
right.data_diff.push(ObjDataDiff {
|
|
||||||
data: right_data.to_vec(),
|
|
||||||
kind: ObjDataDiffKind::None,
|
|
||||||
len: right_symbol.size as usize,
|
|
||||||
symbol: right_symbol.name.clone(),
|
|
||||||
});
|
|
||||||
left_symbol.diff_symbol = Some(right_symbol.name.clone());
|
|
||||||
left_symbol.match_percent = Some(100.0);
|
|
||||||
right_symbol.diff_symbol = Some(left_symbol.name.clone());
|
|
||||||
right_symbol.match_percent = Some(100.0);
|
|
||||||
}
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
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::Keep;
|
|
||||||
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::Keep => {}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
LevEditType::Keep => unreachable!(),
|
|
||||||
}
|
|
||||||
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::Keep => {}
|
|
||||||
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
@@ -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
@@ -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(),
|
||||||
|
}];
|
||||||
|
}
|
||||||
162
src/diff/editops.rs
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
/// Adapted from https://crates.io/crates/rapidfuzz
|
||||||
|
// Copyright 2020 maxbachmann
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any
|
||||||
|
// person obtaining a copy of this software and associated
|
||||||
|
// documentation files (the "Software"), to deal in the
|
||||||
|
// Software without restriction, including without
|
||||||
|
// limitation the rights to use, copy, modify, merge,
|
||||||
|
// publish, distribute, sublicense, and/or sell copies of
|
||||||
|
// the Software, and to permit persons to whom the Software
|
||||||
|
// is furnished to do so, subject to the following
|
||||||
|
// conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice
|
||||||
|
// shall be included in all copies or substantial portions
|
||||||
|
// of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||||
|
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||||
|
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||||
|
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||||
|
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||||
|
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
// DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||||
|
pub enum LevEditType {
|
||||||
|
Replace,
|
||||||
|
Insert,
|
||||||
|
Delete,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub struct LevEditOp {
|
||||||
|
pub op_type: LevEditType, /* editing operation type */
|
||||||
|
pub first_start: usize, /* source block position */
|
||||||
|
pub second_start: usize, /* destination position */
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn editops_find<T>(query: &[T], choice: &[T]) -> Vec<LevEditOp>
|
||||||
|
where T: PartialEq {
|
||||||
|
let Affix { prefix_len, suffix_len } = Affix::find(query, choice);
|
||||||
|
|
||||||
|
let first_string = &query[prefix_len..query.len() - suffix_len];
|
||||||
|
let second_string = &choice[prefix_len..choice.len() - suffix_len];
|
||||||
|
|
||||||
|
let matrix_columns = first_string.len() + 1;
|
||||||
|
let matrix_rows = second_string.len() + 1;
|
||||||
|
|
||||||
|
// TODO maybe use an actual matrix for readability
|
||||||
|
let mut cache_matrix: Vec<usize> = vec![0; matrix_rows * matrix_columns];
|
||||||
|
for (i, elem) in cache_matrix.iter_mut().enumerate().take(matrix_rows) {
|
||||||
|
*elem = i;
|
||||||
|
}
|
||||||
|
for i in 1..matrix_columns {
|
||||||
|
cache_matrix[matrix_rows * i] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, char1) in first_string.iter().enumerate() {
|
||||||
|
let mut prev = i * matrix_rows;
|
||||||
|
let current = prev + matrix_rows;
|
||||||
|
let mut x = i + 1;
|
||||||
|
for (p, char2p) in second_string.iter().enumerate() {
|
||||||
|
let mut c3 = cache_matrix[prev] + (char1 != char2p) as usize;
|
||||||
|
prev += 1;
|
||||||
|
x += 1;
|
||||||
|
if x >= c3 {
|
||||||
|
x = c3;
|
||||||
|
}
|
||||||
|
c3 = cache_matrix[prev] + 1;
|
||||||
|
if x > c3 {
|
||||||
|
x = c3;
|
||||||
|
}
|
||||||
|
cache_matrix[current + 1 + p] = x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editops_from_cost_matrix(matrix_columns, matrix_rows, prefix_len, cache_matrix)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn editops_from_cost_matrix(
|
||||||
|
len1: usize,
|
||||||
|
len2: usize,
|
||||||
|
prefix_len: usize,
|
||||||
|
cache_matrix: Vec<usize>,
|
||||||
|
) -> Vec<LevEditOp> {
|
||||||
|
let mut ops = Vec::with_capacity(cache_matrix[len1 * len2 - 1]);
|
||||||
|
let mut dir = 0;
|
||||||
|
let mut i = len1 - 1;
|
||||||
|
let mut j = len2 - 1;
|
||||||
|
let mut p = len1 * len2 - 1;
|
||||||
|
|
||||||
|
//TODO this is still pretty ugly
|
||||||
|
while i > 0 || j > 0 {
|
||||||
|
let current_value = cache_matrix[p];
|
||||||
|
|
||||||
|
// More than one operation can be possible at a time. We use `dir` to
|
||||||
|
// decide when ambiguous.
|
||||||
|
let is_insert = j > 0 && current_value == cache_matrix[p - 1] + 1;
|
||||||
|
let is_delete = i > 0 && current_value == cache_matrix[p - len2] + 1;
|
||||||
|
let is_replace = i > 0 && j > 0 && current_value == cache_matrix[p - len2 - 1] + 1;
|
||||||
|
|
||||||
|
let (op_type, new_dir) = match (dir, is_insert, is_delete, is_replace) {
|
||||||
|
(_, false, false, false) => (None, 0),
|
||||||
|
(-1, true, _, _) => (Some(LevEditType::Insert), -1),
|
||||||
|
(1, _, true, _) => (Some(LevEditType::Delete), 1),
|
||||||
|
(_, _, _, true) => (Some(LevEditType::Replace), 0),
|
||||||
|
(0, true, _, _) => (Some(LevEditType::Insert), -1),
|
||||||
|
(0, _, true, _) => (Some(LevEditType::Delete), 1),
|
||||||
|
_ => panic!("something went terribly wrong"),
|
||||||
|
};
|
||||||
|
|
||||||
|
match new_dir {
|
||||||
|
-1 => {
|
||||||
|
j -= 1;
|
||||||
|
p -= 1;
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
i -= 1;
|
||||||
|
p -= len2;
|
||||||
|
}
|
||||||
|
0 => {
|
||||||
|
i -= 1;
|
||||||
|
j -= 1;
|
||||||
|
p -= len2 + 1;
|
||||||
|
}
|
||||||
|
_ => panic!("something went terribly wrong"),
|
||||||
|
};
|
||||||
|
dir = new_dir;
|
||||||
|
|
||||||
|
if let Some(op_type) = op_type {
|
||||||
|
ops.insert(0, LevEditOp {
|
||||||
|
op_type,
|
||||||
|
first_start: i + prefix_len,
|
||||||
|
second_start: j + prefix_len,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ops
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Affix {
|
||||||
|
pub prefix_len: usize,
|
||||||
|
pub suffix_len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Affix {
|
||||||
|
pub fn find<T>(s1: &[T], s2: &[T]) -> Affix
|
||||||
|
where T: PartialEq {
|
||||||
|
let prefix_len = s1.iter().zip(s2.iter()).take_while(|t| t.0 == t.1).count();
|
||||||
|
let suffix_len = s1[prefix_len..]
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.zip(s2[prefix_len..].iter().rev())
|
||||||
|
.take_while(|t| t.0 == t.1)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
Affix { prefix_len, suffix_len }
|
||||||
|
}
|
||||||
|
}
|
||||||
115
src/diff/mod.rs
Normal 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(())
|
||||||
|
}
|
||||||
258
src/editops.rs
@@ -1,258 +0,0 @@
|
|||||||
/// Adapted from https://crates.io/crates/rapidfuzz
|
|
||||||
// Copyright 2020 maxbachmann
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any
|
|
||||||
// person obtaining a copy of this software and associated
|
|
||||||
// documentation files (the "Software"), to deal in the
|
|
||||||
// Software without restriction, including without
|
|
||||||
// limitation the rights to use, copy, modify, merge,
|
|
||||||
// publish, distribute, sublicense, and/or sell copies of
|
|
||||||
// the Software, and to permit persons to whom the Software
|
|
||||||
// is furnished to do so, subject to the following
|
|
||||||
// conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice
|
|
||||||
// shall be included in all copies or substantial portions
|
|
||||||
// of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
|
||||||
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
|
||||||
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
||||||
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
|
||||||
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
||||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
||||||
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
|
||||||
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
||||||
// DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
|
||||||
pub enum LevEditType {
|
|
||||||
Keep,
|
|
||||||
Replace,
|
|
||||||
Insert,
|
|
||||||
Delete,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub struct LevEditOp {
|
|
||||||
pub op_type: LevEditType, /* editing operation type */
|
|
||||||
pub first_start: usize, /* source block position */
|
|
||||||
pub second_start: usize, /* destination position */
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub struct LevMatchingBlock {
|
|
||||||
pub first_start: usize,
|
|
||||||
pub second_start: usize,
|
|
||||||
pub len: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn editops_find<T>(query: &[T], choice: &[T]) -> Vec<LevEditOp>
|
|
||||||
where T: PartialEq {
|
|
||||||
let string_affix = Affix::find(query, choice);
|
|
||||||
|
|
||||||
let first_string_len = string_affix.first_string_len;
|
|
||||||
let second_string_len = string_affix.second_string_len;
|
|
||||||
let prefix_len = string_affix.prefix_len;
|
|
||||||
let first_string = &query[prefix_len..prefix_len + first_string_len];
|
|
||||||
let second_string = &choice[prefix_len..prefix_len + second_string_len];
|
|
||||||
|
|
||||||
let matrix_columns = first_string_len + 1;
|
|
||||||
let matrix_rows = second_string_len + 1;
|
|
||||||
|
|
||||||
// TODO maybe use an actual matrix for readability
|
|
||||||
let mut cache_matrix: Vec<usize> = vec![0; matrix_rows * matrix_columns];
|
|
||||||
for (i, elem) in cache_matrix.iter_mut().enumerate().take(matrix_rows) {
|
|
||||||
*elem = i;
|
|
||||||
}
|
|
||||||
for i in 1..matrix_columns {
|
|
||||||
cache_matrix[matrix_rows * i] = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i, char1) in first_string.iter().enumerate() {
|
|
||||||
let mut prev = i * matrix_rows;
|
|
||||||
let current = prev + matrix_rows;
|
|
||||||
let mut x = i + 1;
|
|
||||||
for (p, char2p) in second_string.iter().enumerate() {
|
|
||||||
let mut c3 = cache_matrix[prev] + (char1 != char2p) as usize;
|
|
||||||
prev += 1;
|
|
||||||
x += 1;
|
|
||||||
if x >= c3 {
|
|
||||||
x = c3;
|
|
||||||
}
|
|
||||||
c3 = cache_matrix[prev] + 1;
|
|
||||||
if x > c3 {
|
|
||||||
x = c3;
|
|
||||||
}
|
|
||||||
cache_matrix[current + 1 + p] = x;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
editops_from_cost_matrix(
|
|
||||||
first_string,
|
|
||||||
second_string,
|
|
||||||
matrix_columns,
|
|
||||||
matrix_rows,
|
|
||||||
prefix_len,
|
|
||||||
cache_matrix,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn editops_from_cost_matrix<T>(
|
|
||||||
string1: &[T],
|
|
||||||
string2: &[T],
|
|
||||||
len1: usize,
|
|
||||||
len2: usize,
|
|
||||||
prefix_len: usize,
|
|
||||||
cache_matrix: Vec<usize>,
|
|
||||||
) -> Vec<LevEditOp>
|
|
||||||
where
|
|
||||||
T: PartialEq,
|
|
||||||
{
|
|
||||||
let mut dir = 0;
|
|
||||||
|
|
||||||
let mut ops: Vec<LevEditOp> = vec![];
|
|
||||||
ops.reserve(cache_matrix[len1 * len2 - 1]);
|
|
||||||
|
|
||||||
let mut i = len1 - 1;
|
|
||||||
let mut j = len2 - 1;
|
|
||||||
let mut p = len1 * len2 - 1;
|
|
||||||
|
|
||||||
// let string1_chars: Vec<char> = string1.chars().collect();
|
|
||||||
// let string2_chars: Vec<char> = string2.chars().collect();
|
|
||||||
|
|
||||||
//TODO this is still pretty ugly
|
|
||||||
while i > 0 || j > 0 {
|
|
||||||
let current_value = cache_matrix[p];
|
|
||||||
|
|
||||||
let op_type;
|
|
||||||
|
|
||||||
if dir == -1 && j > 0 && current_value == cache_matrix[p - 1] + 1 {
|
|
||||||
op_type = LevEditType::Insert;
|
|
||||||
} else if dir == 1 && i > 0 && current_value == cache_matrix[p - len2] + 1 {
|
|
||||||
op_type = LevEditType::Delete;
|
|
||||||
} else if i > 0
|
|
||||||
&& j > 0
|
|
||||||
&& current_value == cache_matrix[p - len2 - 1]
|
|
||||||
&& string1[i - 1] == string2[j - 1]
|
|
||||||
{
|
|
||||||
op_type = LevEditType::Keep;
|
|
||||||
} else if i > 0 && j > 0 && current_value == cache_matrix[p - len2 - 1] + 1 {
|
|
||||||
op_type = LevEditType::Replace;
|
|
||||||
}
|
|
||||||
/* we can't turn directly from -1 to 1, in this case it would be better
|
|
||||||
* to go diagonally, but check it (dir == 0) */
|
|
||||||
else if dir == 0 && j > 0 && current_value == cache_matrix[p - 1] + 1 {
|
|
||||||
op_type = LevEditType::Insert;
|
|
||||||
} else if dir == 0 && i > 0 && current_value == cache_matrix[p - len2] + 1 {
|
|
||||||
op_type = LevEditType::Delete;
|
|
||||||
} else {
|
|
||||||
panic!("something went terribly wrong");
|
|
||||||
}
|
|
||||||
|
|
||||||
match op_type {
|
|
||||||
LevEditType::Insert => {
|
|
||||||
j -= 1;
|
|
||||||
p -= 1;
|
|
||||||
dir = -1;
|
|
||||||
}
|
|
||||||
LevEditType::Delete => {
|
|
||||||
i -= 1;
|
|
||||||
p -= len2;
|
|
||||||
dir = 1;
|
|
||||||
}
|
|
||||||
LevEditType::Replace => {
|
|
||||||
i -= 1;
|
|
||||||
j -= 1;
|
|
||||||
p -= len2 + 1;
|
|
||||||
dir = 0;
|
|
||||||
}
|
|
||||||
LevEditType::Keep => {
|
|
||||||
i -= 1;
|
|
||||||
j -= 1;
|
|
||||||
p -= len2 + 1;
|
|
||||||
dir = 0;
|
|
||||||
/* LevEditKeep does not has to be stored */
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let edit_op =
|
|
||||||
LevEditOp { op_type, first_start: i + prefix_len, second_start: j + prefix_len };
|
|
||||||
ops.insert(0, edit_op);
|
|
||||||
}
|
|
||||||
|
|
||||||
ops
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Affix {
|
|
||||||
pub prefix_len: usize,
|
|
||||||
pub first_string_len: usize,
|
|
||||||
pub second_string_len: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Affix {
|
|
||||||
pub fn find<T>(first_string: &[T], second_string: &[T]) -> Affix
|
|
||||||
where T: PartialEq {
|
|
||||||
// remove common prefix and suffix (linear vs square runtime for levensthein)
|
|
||||||
let mut first_iter = first_string.iter();
|
|
||||||
let mut second_iter = second_string.iter();
|
|
||||||
|
|
||||||
let mut limit_start = 0;
|
|
||||||
|
|
||||||
let mut first_iter_char = first_iter.next();
|
|
||||||
let mut second_iter_char = second_iter.next();
|
|
||||||
while first_iter_char.is_some() && first_iter_char == second_iter_char {
|
|
||||||
first_iter_char = first_iter.next();
|
|
||||||
second_iter_char = second_iter.next();
|
|
||||||
limit_start += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// save char since the iterator was already consumed
|
|
||||||
let first_iter_cache = first_iter_char;
|
|
||||||
let second_iter_cache = second_iter_char;
|
|
||||||
|
|
||||||
if second_iter_char.is_some() && first_iter_char.is_some() {
|
|
||||||
first_iter_char = first_iter.next_back();
|
|
||||||
second_iter_char = second_iter.next_back();
|
|
||||||
while first_iter_char.is_some() && first_iter_char == second_iter_char {
|
|
||||||
first_iter_char = first_iter.next_back();
|
|
||||||
second_iter_char = second_iter.next_back();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match (first_iter_char, second_iter_char) {
|
|
||||||
(None, None) => {
|
|
||||||
// characters might not match even though they were consumed
|
|
||||||
let remaining_char = (first_iter_cache != second_iter_cache) as usize;
|
|
||||||
Affix {
|
|
||||||
prefix_len: limit_start,
|
|
||||||
first_string_len: remaining_char,
|
|
||||||
second_string_len: remaining_char,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(None, _) => {
|
|
||||||
let remaining_char =
|
|
||||||
(first_iter_cache.is_some() && first_iter_cache != second_iter_char) as usize;
|
|
||||||
Affix {
|
|
||||||
prefix_len: limit_start,
|
|
||||||
first_string_len: remaining_char,
|
|
||||||
second_string_len: second_iter.count() + 1 + remaining_char,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(_, None) => {
|
|
||||||
let remaining_char =
|
|
||||||
(second_iter_cache.is_some() && second_iter_cache != first_iter_char) as usize;
|
|
||||||
Affix {
|
|
||||||
prefix_len: limit_start,
|
|
||||||
first_string_len: first_iter.count() + 1 + remaining_char,
|
|
||||||
second_string_len: remaining_char,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Affix {
|
|
||||||
prefix_len: limit_start,
|
|
||||||
first_string_len: first_iter.count() + 2,
|
|
||||||
second_string_len: second_iter.count() + 2,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
146
src/fonts/matching.rs
Normal 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
@@ -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(())
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ use anyhow::{Context, Result};
|
|||||||
use self_update::{cargo_crate_version, update::Release};
|
use self_update::{cargo_crate_version, update::Release};
|
||||||
|
|
||||||
use crate::{
|
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},
|
update::{build_updater, BIN_NAME},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -14,20 +14,20 @@ pub struct CheckUpdateResult {
|
|||||||
pub found_binary: bool,
|
pub found_binary: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_check_update(status: &Status, cancel: Receiver<()>) -> Result<Box<CheckUpdateResult>> {
|
fn run_check_update(context: &JobContext, cancel: Receiver<()>) -> Result<Box<CheckUpdateResult>> {
|
||||||
update_status(status, "Fetching latest release".to_string(), 0, 1, &cancel)?;
|
update_status(context, "Fetching latest release".to_string(), 0, 1, &cancel)?;
|
||||||
let updater = build_updater().context("Failed to create release updater")?;
|
let updater = build_updater().context("Failed to create release updater")?;
|
||||||
let latest_release = updater.get_latest_release()?;
|
let latest_release = updater.get_latest_release()?;
|
||||||
let update_available =
|
let update_available =
|
||||||
self_update::version::bump_is_greater(cargo_crate_version!(), &latest_release.version)?;
|
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);
|
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 }))
|
Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary }))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn queue_check_update() -> JobState {
|
pub fn start_check_update(ctx: &egui::Context) -> JobState {
|
||||||
queue_job("Check for updates", Job::CheckUpdate, move |status, cancel| {
|
start_job(ctx, "Check for updates", Job::CheckUpdate, move |context, cancel| {
|
||||||
run_check_update(status, cancel).map(JobResult::CheckUpdate)
|
run_check_update(&context, cancel).map(|result| JobResult::CheckUpdate(Some(result)))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
135
src/jobs/create_scratch.rs
Normal 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)))
|
||||||
|
})
|
||||||
|
}
|
||||||
122
src/jobs/mod.rs
@@ -10,31 +10,106 @@ use std::{
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use crate::jobs::{
|
use crate::jobs::{
|
||||||
bindiff::BinDiffResult, check_update::CheckUpdateResult, objdiff::ObjDiffResult,
|
check_update::CheckUpdateResult, create_scratch::CreateScratchResult, objdiff::ObjDiffResult,
|
||||||
update::UpdateResult,
|
update::UpdateResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod bindiff;
|
|
||||||
pub mod check_update;
|
pub mod check_update;
|
||||||
|
pub mod create_scratch;
|
||||||
pub mod objdiff;
|
pub mod objdiff;
|
||||||
pub mod update;
|
pub mod update;
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||||
pub enum Job {
|
pub enum Job {
|
||||||
ObjDiff,
|
ObjDiff,
|
||||||
BinDiff,
|
|
||||||
CheckUpdate,
|
CheckUpdate,
|
||||||
Update,
|
Update,
|
||||||
|
CreateScratch,
|
||||||
}
|
}
|
||||||
pub static JOB_ID: AtomicUsize = AtomicUsize::new(0);
|
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 struct JobState {
|
||||||
pub id: usize,
|
pub id: usize,
|
||||||
pub job_type: Job,
|
pub kind: Job,
|
||||||
pub handle: Option<JoinHandle<JobResult>>,
|
pub handle: Option<JoinHandle<JobResult>>,
|
||||||
pub status: Arc<RwLock<JobStatus>>,
|
pub context: JobContext,
|
||||||
pub cancel: Sender<()>,
|
pub cancel: Sender<()>,
|
||||||
pub should_remove: bool,
|
pub should_remove: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct JobStatus {
|
pub struct JobStatus {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
@@ -43,12 +118,13 @@ pub struct JobStatus {
|
|||||||
pub status: String,
|
pub status: String,
|
||||||
pub error: Option<anyhow::Error>,
|
pub error: Option<anyhow::Error>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum JobResult {
|
pub enum JobResult {
|
||||||
None,
|
None,
|
||||||
ObjDiff(Box<ObjDiffResult>),
|
ObjDiff(Option<Box<ObjDiffResult>>),
|
||||||
BinDiff(Box<BinDiffResult>),
|
CheckUpdate(Option<Box<CheckUpdateResult>>),
|
||||||
CheckUpdate(Box<CheckUpdateResult>),
|
|
||||||
Update(Box<UpdateResult>),
|
Update(Box<UpdateResult>),
|
||||||
|
CreateScratch(Option<Box<CreateScratchResult>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_cancel(rx: &Receiver<()>) -> bool {
|
fn should_cancel(rx: &Receiver<()>) -> bool {
|
||||||
@@ -58,12 +134,11 @@ fn should_cancel(rx: &Receiver<()>) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Status = Arc<RwLock<JobStatus>>;
|
fn start_job(
|
||||||
|
ctx: &egui::Context,
|
||||||
fn queue_job(
|
|
||||||
title: &str,
|
title: &str,
|
||||||
job_type: Job,
|
kind: Job,
|
||||||
run: impl FnOnce(&Status, Receiver<()>) -> Result<JobResult> + Send + 'static,
|
run: impl FnOnce(JobContext, Receiver<()>) -> Result<JobResult> + Send + 'static,
|
||||||
) -> JobState {
|
) -> JobState {
|
||||||
let status = Arc::new(RwLock::new(JobStatus {
|
let status = Arc::new(RwLock::new(JobStatus {
|
||||||
title: title.to_string(),
|
title: title.to_string(),
|
||||||
@@ -72,10 +147,11 @@ fn queue_job(
|
|||||||
status: String::new(),
|
status: String::new(),
|
||||||
error: None,
|
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 (tx, rx) = std::sync::mpsc::channel();
|
||||||
let handle = std::thread::spawn(move || {
|
let handle = std::thread::spawn(move || {
|
||||||
return match run(&status, rx) {
|
return match run(context_inner, rx) {
|
||||||
Ok(state) => state,
|
Ok(state) => state,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if let Ok(mut w) = status.write() {
|
if let Ok(mut w) = status.write() {
|
||||||
@@ -87,24 +163,18 @@ fn queue_job(
|
|||||||
});
|
});
|
||||||
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
|
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
|
||||||
log::info!("Started job {}", id);
|
log::info!("Started job {}", id);
|
||||||
JobState {
|
JobState { id, kind, handle: Some(handle), context, cancel: tx, should_remove: true }
|
||||||
id,
|
|
||||||
job_type,
|
|
||||||
handle: Some(handle),
|
|
||||||
status: status_clone,
|
|
||||||
cancel: tx,
|
|
||||||
should_remove: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_status(
|
fn update_status(
|
||||||
status: &Status,
|
context: &JobContext,
|
||||||
str: String,
|
str: String,
|
||||||
count: u32,
|
count: u32,
|
||||||
total: u32,
|
total: u32,
|
||||||
cancel: &Receiver<()>,
|
cancel: &Receiver<()>,
|
||||||
) -> Result<()> {
|
) -> 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_items = Some([count, total]);
|
||||||
w.progress_percent = count as f32 / total as f32;
|
w.progress_percent = count as f32 / total as f32;
|
||||||
if should_cancel(cancel) {
|
if should_cancel(cancel) {
|
||||||
@@ -113,5 +183,7 @@ fn update_status(
|
|||||||
} else {
|
} else {
|
||||||
w.status = str;
|
w.status = str;
|
||||||
}
|
}
|
||||||
|
drop(w);
|
||||||
|
context.egui.request_repaint();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,77 @@
|
|||||||
use std::{
|
use std::{
|
||||||
path::Path,
|
path::{Path, PathBuf},
|
||||||
process::Command,
|
process::Command,
|
||||||
str::from_utf8,
|
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 time::OffsetDateTime;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{AppConfig, DiffConfig},
|
app::{AppConfig, ObjectConfig},
|
||||||
diff::diff_objs,
|
diff::{diff_objs, DiffAlg, DiffObjConfig},
|
||||||
jobs::{queue_job, update_status, Job, JobResult, JobState, Status},
|
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
|
||||||
obj::{elf, ObjInfo},
|
obj::{elf, ObjInfo},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct BuildStatus {
|
pub struct BuildStatus {
|
||||||
pub success: bool,
|
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 {
|
pub struct ObjDiffResult {
|
||||||
@@ -28,7 +82,14 @@ pub struct ObjDiffResult {
|
|||||||
pub time: OffsetDateTime,
|
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> {
|
match (|| -> Result<BuildStatus> {
|
||||||
let make = config.custom_make.as_deref().unwrap_or("make");
|
let make = config.custom_make.as_deref().unwrap_or("make");
|
||||||
#[cfg(not(windows))]
|
#[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.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
|
||||||
command
|
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 output = command.output().context("Failed to execute build")?;
|
||||||
let stdout = from_utf8(&output.stdout).context("Failed to process stdout")?;
|
let stdout = from_utf8(&output.stdout).context("Failed to process stdout")?;
|
||||||
let stderr = from_utf8(&output.stderr).context("Failed to process stderr")?;
|
let stderr = from_utf8(&output.stderr).context("Failed to process stderr")?;
|
||||||
Ok(BuildStatus {
|
Ok(BuildStatus {
|
||||||
success: output.status.code().unwrap_or(-1) == 0,
|
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,
|
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(
|
fn run_build(
|
||||||
status: &Status,
|
context: &JobContext,
|
||||||
cancel: Receiver<()>,
|
cancel: Receiver<()>,
|
||||||
config: Arc<RwLock<AppConfig>>,
|
config: ObjDiffConfig,
|
||||||
diff_config: DiffConfig,
|
|
||||||
) -> Result<Box<ObjDiffResult>> {
|
) -> Result<Box<ObjDiffResult>> {
|
||||||
let config = config.read().map_err(|_| Error::msg("Failed to lock app config"))?.clone();
|
let obj_config = config.selected_obj.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?;
|
||||||
let obj_path = config.obj_path.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?;
|
let project_dir = config
|
||||||
let project_dir =
|
.build_config
|
||||||
config.project_dir.as_ref().ok_or_else(|| Error::msg("Missing project dir"))?;
|
.project_dir
|
||||||
let mut target_path = config
|
|
||||||
.target_obj_dir
|
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| Error::msg("Missing target obj dir"))?
|
.ok_or_else(|| Error::msg("Missing project dir"))?;
|
||||||
.to_owned();
|
let target_path_rel = if let Some(target_path) = &obj_config.target_path {
|
||||||
target_path.push(obj_path);
|
Some(target_path.strip_prefix(project_dir).map_err(|_| {
|
||||||
let mut base_path =
|
anyhow!(
|
||||||
config.base_obj_dir.as_ref().ok_or_else(|| Error::msg("Missing base obj dir"))?.to_owned();
|
"Target path '{}' doesn't begin with '{}'",
|
||||||
base_path.push(obj_path);
|
target_path.display(),
|
||||||
let target_path_rel = target_path
|
project_dir.display()
|
||||||
.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)
|
|
||||||
} else {
|
} 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 mut total = 3;
|
||||||
let second_status = run_make(project_dir, base_path_rel, &config);
|
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 time = OffsetDateTime::now_utc();
|
||||||
|
|
||||||
let mut first_obj = if first_status.success {
|
let mut first_obj =
|
||||||
update_status(status, format!("Loading target {obj_path}"), 2, total, &cancel)?;
|
match &obj_config.target_path {
|
||||||
Some(elf::read(&target_path)?)
|
Some(target_path) if first_status.success => {
|
||||||
} else {
|
update_status(
|
||||||
None
|
context,
|
||||||
};
|
format!("Loading target {}", target_path_rel.unwrap().display()),
|
||||||
|
2,
|
||||||
let mut second_obj = if second_status.success {
|
total,
|
||||||
update_status(status, format!("Loading base {obj_path}"), 3, total, &cancel)?;
|
&cancel,
|
||||||
Some(elf::read(&base_path)?)
|
)?;
|
||||||
} else {
|
Some(elf::read(target_path).with_context(|| {
|
||||||
None
|
format!("Failed to read object '{}'", target_path.display())
|
||||||
};
|
})?)
|
||||||
|
|
||||||
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)?;
|
|
||||||
}
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
update_status(status, "Complete".to_string(), total, total, &cancel)?;
|
let mut second_obj = match &obj_config.base_path {
|
||||||
|
Some(base_path) if second_status.success => {
|
||||||
|
update_status(
|
||||||
|
context,
|
||||||
|
format!("Loading base {}", base_path_rel.unwrap().display()),
|
||||||
|
3,
|
||||||
|
total,
|
||||||
|
&cancel,
|
||||||
|
)?;
|
||||||
|
Some(
|
||||||
|
elf::read(base_path)
|
||||||
|
.with_context(|| format!("Failed to read object '{}'", base_path.display()))?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
update_status(context, "Performing diff".to_string(), 4, total, &cancel)?;
|
||||||
|
let diff_config = DiffObjConfig {
|
||||||
|
code_alg: config.code_alg,
|
||||||
|
data_alg: config.data_alg,
|
||||||
|
relax_reloc_diffs: config.relax_reloc_diffs,
|
||||||
|
};
|
||||||
|
diff_objs(&diff_config, first_obj.as_mut(), second_obj.as_mut())?;
|
||||||
|
|
||||||
|
update_status(context, "Complete".to_string(), total, total, &cancel)?;
|
||||||
Ok(Box::new(ObjDiffResult { first_status, second_status, first_obj, second_obj, time }))
|
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 {
|
pub fn start_build(ctx: &egui::Context, config: ObjDiffConfig) -> JobState {
|
||||||
queue_job("Object diff", Job::ObjDiff, move |status, cancel| {
|
start_job(ctx, "Object diff", Job::ObjDiff, move |context, cancel| {
|
||||||
run_build(status, cancel, config, diff_config).map(JobResult::ObjDiff)
|
run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
env::{current_dir, current_exe},
|
env::{current_dir, current_exe},
|
||||||
fs,
|
|
||||||
fs::File,
|
fs::File,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::mpsc::Receiver,
|
sync::mpsc::Receiver,
|
||||||
@@ -10,7 +9,7 @@ use anyhow::{Context, Result};
|
|||||||
use const_format::formatcp;
|
use const_format::formatcp;
|
||||||
|
|
||||||
use crate::{
|
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},
|
update::{build_updater, BIN_NAME},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -18,7 +17,7 @@ pub struct UpdateResult {
|
|||||||
pub exe_path: PathBuf,
|
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)?;
|
update_status(status, "Fetching latest release".to_string(), 0, 3, &cancel)?;
|
||||||
let updater = build_updater().context("Failed to create release updater")?;
|
let updater = build_updater().context("Failed to create release updater")?;
|
||||||
let latest_release = updater.get_latest_release()?;
|
let latest_release = updater.get_latest_release()?;
|
||||||
@@ -44,7 +43,7 @@ fn run_update(status: &Status, cancel: Receiver<()>) -> Result<Box<UpdateResult>
|
|||||||
.to_dest(&target_file)?;
|
.to_dest(&target_file)?;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::{fs, os::unix::fs::PermissionsExt};
|
||||||
let mut perms = fs::metadata(&target_file)?.permissions();
|
let mut perms = fs::metadata(&target_file)?.permissions();
|
||||||
perms.set_mode(0o755);
|
perms.set_mode(0o755);
|
||||||
fs::set_permissions(&target_file, perms)?;
|
fs::set_permissions(&target_file, perms)?;
|
||||||
@@ -54,8 +53,8 @@ fn run_update(status: &Status, cancel: Receiver<()>) -> Result<Box<UpdateResult>
|
|||||||
Ok(Box::from(UpdateResult { exe_path: target_file }))
|
Ok(Box::from(UpdateResult { exe_path: target_file }))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn queue_update() -> JobState {
|
pub fn start_update(ctx: &egui::Context) -> JobState {
|
||||||
queue_job("Update app", Job::Update, move |status, cancel| {
|
start_job(ctx, "Update app", Job::Update, move |context, cancel| {
|
||||||
run_update(status, cancel).map(JobResult::Update)
|
run_update(&context, cancel).map(JobResult::Update)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
pub use app::App;
|
pub use app::App;
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
|
mod app_config;
|
||||||
|
mod config;
|
||||||
mod diff;
|
mod diff;
|
||||||
mod editops;
|
mod fonts;
|
||||||
mod jobs;
|
mod jobs;
|
||||||
mod obj;
|
mod obj;
|
||||||
mod update;
|
mod update;
|
||||||
|
|||||||
46
src/main.rs
@@ -1,11 +1,32 @@
|
|||||||
#![warn(clippy::all, rust_2018_idioms)]
|
#![warn(clippy::all, rust_2018_idioms)]
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
#![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 cfg_if::cfg_if;
|
||||||
use time::UtcOffset;
|
use time::UtcOffset;
|
||||||
|
|
||||||
|
fn load_icon() -> Result<egui::IconData> {
|
||||||
|
use bytes::Buf;
|
||||||
|
let decoder = png::Decoder::new(include_bytes!("../assets/icon_64.png").reader());
|
||||||
|
let mut reader = decoder.read_info()?;
|
||||||
|
let mut buf = vec![0; reader.output_buffer_size()];
|
||||||
|
let info = reader.next_frame(&mut buf)?;
|
||||||
|
if info.bit_depth != png::BitDepth::Eight {
|
||||||
|
return Err(Error::msg("Invalid bit depth"));
|
||||||
|
}
|
||||||
|
if info.color_type != png::ColorType::Rgba {
|
||||||
|
return Err(Error::msg("Invalid color type"));
|
||||||
|
}
|
||||||
|
buf.truncate(info.buffer_size());
|
||||||
|
Ok(egui::IconData { rgba: buf, width: info.width, height: info.height })
|
||||||
|
}
|
||||||
|
|
||||||
// When compiling natively:
|
// When compiling natively:
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -19,13 +40,26 @@ fn main() {
|
|||||||
|
|
||||||
let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None));
|
let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None));
|
||||||
let exec_path_clone = exec_path.clone();
|
let exec_path_clone = exec_path.clone();
|
||||||
let native_options = eframe::NativeOptions::default();
|
let mut native_options =
|
||||||
// native_options.renderer = eframe::Renderer::Wgpu;
|
eframe::NativeOptions { follow_system_theme: false, ..Default::default() };
|
||||||
|
match load_icon() {
|
||||||
|
Ok(data) => {
|
||||||
|
native_options.viewport.icon = Some(Arc::new(data));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to load application icon: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(feature = "wgpu")]
|
||||||
|
{
|
||||||
|
native_options.renderer = eframe::Renderer::Wgpu;
|
||||||
|
}
|
||||||
eframe::run_native(
|
eframe::run_native(
|
||||||
"objdiff",
|
"objdiff",
|
||||||
native_options,
|
native_options,
|
||||||
Box::new(move |cc| Box::new(objdiff::App::new(cc, utc_offset, exec_path_clone))),
|
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
|
// Attempt to relaunch application from the updated path
|
||||||
if let Ok(mut guard) = exec_path.lock() {
|
if let Ok(mut guard) = exec_path.lock() {
|
||||||
@@ -35,7 +69,7 @@ fn main() {
|
|||||||
let result = exec::Command::new(path)
|
let result = exec::Command::new(path)
|
||||||
.args(&std::env::args().collect::<Vec<String>>())
|
.args(&std::env::args().collect::<Vec<String>>())
|
||||||
.exec();
|
.exec();
|
||||||
eprintln!("Failed to relaunch: {result:?}");
|
log::error!("Failed to relaunch: {result:?}");
|
||||||
} else {
|
} else {
|
||||||
let result = std::process::Command::new(path)
|
let result = std::process::Command::new(path)
|
||||||
.args(std::env::args())
|
.args(std::env::args())
|
||||||
@@ -43,7 +77,7 @@ fn main() {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.wait();
|
.wait();
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
eprintln!("Failed to relaunch: {:?}", e);
|
log::error!("Failed to relaunch: {:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
188
src/obj/elf.rs
@@ -1,15 +1,13 @@
|
|||||||
use std::{fs, path::Path};
|
use std::{borrow::Cow, collections::BTreeMap, fs, io::Cursor, path::Path};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
|
use byteorder::{BigEndian, ReadBytesExt};
|
||||||
use cwdemangle::demangle;
|
use cwdemangle::demangle;
|
||||||
|
use filetime::FileTime;
|
||||||
use flagset::Flags;
|
use flagset::Flags;
|
||||||
use object::{
|
use object::{
|
||||||
elf::{
|
elf, Architecture, Endianness, File, Object, ObjectSection, ObjectSymbol, RelocationKind,
|
||||||
R_MIPS_26, R_MIPS_HI16, R_MIPS_LO16, R_PPC_ADDR16_HA, R_PPC_ADDR16_HI, R_PPC_ADDR16_LO,
|
RelocationTarget, SectionIndex, SectionKind, Symbol, SymbolKind, SymbolScope, SymbolSection,
|
||||||
R_PPC_EMB_SDA21, R_PPC_REL14, R_PPC_REL24,
|
|
||||||
},
|
|
||||||
Architecture, File, Object, ObjectSection, ObjectSymbol, RelocationKind, RelocationTarget,
|
|
||||||
SectionKind, Symbol, SymbolKind, SymbolSection,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::obj::{
|
use crate::obj::{
|
||||||
@@ -17,19 +15,19 @@ use crate::obj::{
|
|||||||
ObjSymbolFlagSet, ObjSymbolFlags,
|
ObjSymbolFlagSet, ObjSymbolFlags,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn to_obj_section_kind(kind: SectionKind) -> ObjSectionKind {
|
fn to_obj_section_kind(kind: SectionKind) -> Option<ObjSectionKind> {
|
||||||
match kind {
|
match kind {
|
||||||
SectionKind::Text => ObjSectionKind::Code,
|
SectionKind::Text => Some(ObjSectionKind::Code),
|
||||||
SectionKind::Data | SectionKind::ReadOnlyData => ObjSectionKind::Data,
|
SectionKind::Data | SectionKind::ReadOnlyData => Some(ObjSectionKind::Data),
|
||||||
SectionKind::UninitializedData => ObjSectionKind::Bss,
|
SectionKind::UninitializedData => Some(ObjSectionKind::Bss),
|
||||||
_ => panic!("Unhandled section kind {kind:?}"),
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_obj_symbol(obj_file: &File<'_>, symbol: &Symbol<'_, '_>, addend: i64) -> Result<ObjSymbol> {
|
fn to_obj_symbol(obj_file: &File<'_>, symbol: &Symbol<'_, '_>, addend: i64) -> Result<ObjSymbol> {
|
||||||
let mut name = symbol.name().context("Failed to process symbol name")?;
|
let mut name = symbol.name().context("Failed to process symbol name")?;
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
println!("Found empty sym: {symbol:?}");
|
log::warn!("Found empty sym: {symbol:?}");
|
||||||
name = "?";
|
name = "?";
|
||||||
}
|
}
|
||||||
let mut flags = ObjSymbolFlagSet(ObjSymbolFlags::none());
|
let mut flags = ObjSymbolFlagSet(ObjSymbolFlags::none());
|
||||||
@@ -45,6 +43,9 @@ fn to_obj_symbol(obj_file: &File<'_>, symbol: &Symbol<'_, '_>, addend: i64) -> R
|
|||||||
if symbol.is_weak() {
|
if symbol.is_weak() {
|
||||||
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::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) =
|
let section_address = if let Some(section) =
|
||||||
symbol.section_index().and_then(|idx| obj_file.section_by_index(idx).ok())
|
symbol.section_index().and_then(|idx| obj_file.section_by_index(idx).ok())
|
||||||
{
|
{
|
||||||
@@ -73,18 +74,14 @@ fn filter_sections(obj_file: &File<'_>) -> Result<Vec<ObjSection>> {
|
|||||||
if section.size() == 0 {
|
if section.size() == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if section.kind() != SectionKind::Text
|
let Some(kind) = to_obj_section_kind(section.kind()) else {
|
||||||
&& section.kind() != SectionKind::Data
|
|
||||||
&& section.kind() != SectionKind::ReadOnlyData
|
|
||||||
&& section.kind() != SectionKind::UninitializedData
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
};
|
||||||
let name = section.name().context("Failed to process section name")?;
|
let name = section.name().context("Failed to process section name")?;
|
||||||
let data = section.uncompressed_data().context("Failed to read section data")?;
|
let data = section.uncompressed_data().context("Failed to read section data")?;
|
||||||
result.push(ObjSection {
|
result.push(ObjSection {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
kind: to_obj_section_kind(section.kind()),
|
kind,
|
||||||
address: section.address(),
|
address: section.address(),
|
||||||
size: section.size(),
|
size: section.size(),
|
||||||
data: data.to_vec(),
|
data: data.to_vec(),
|
||||||
@@ -133,13 +130,11 @@ fn symbols_by_section(obj_file: &File<'_>, section: &ObjSection) -> Result<Vec<O
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn common_symbols(obj_file: &File<'_>) -> Result<Vec<ObjSymbol>> {
|
fn common_symbols(obj_file: &File<'_>) -> Result<Vec<ObjSymbol>> {
|
||||||
let mut result = Vec::<ObjSymbol>::new();
|
obj_file
|
||||||
for symbol in obj_file.symbols() {
|
.symbols()
|
||||||
if symbol.is_common() {
|
.filter(Symbol::is_common)
|
||||||
result.push(to_obj_symbol(obj_file, &symbol, 0)?);
|
.map(|symbol| to_obj_symbol(obj_file, &symbol, 0))
|
||||||
}
|
.collect::<Result<Vec<ObjSymbol>>>()
|
||||||
}
|
|
||||||
Ok(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_section_symbol(
|
fn find_section_symbol(
|
||||||
@@ -190,11 +185,9 @@ fn find_section_symbol(
|
|||||||
fn relocations_by_section(
|
fn relocations_by_section(
|
||||||
arch: ObjArchitecture,
|
arch: ObjArchitecture,
|
||||||
obj_file: &File<'_>,
|
obj_file: &File<'_>,
|
||||||
section: &mut ObjSection,
|
section: &ObjSection,
|
||||||
) -> Result<Vec<ObjReloc>> {
|
) -> Result<Vec<ObjReloc>> {
|
||||||
let obj_section = obj_file
|
let obj_section = obj_file.section_by_index(SectionIndex(section.index))?;
|
||||||
.section_by_name(§ion.name)
|
|
||||||
.ok_or_else(|| anyhow::Error::msg("Failed to locate section"))?;
|
|
||||||
let mut relocations = Vec::<ObjReloc>::new();
|
let mut relocations = Vec::<ObjReloc>::new();
|
||||||
for (address, reloc) in obj_section.relocations() {
|
for (address, reloc) in obj_section.relocations() {
|
||||||
let symbol = match reloc.target() {
|
let symbol = match reloc.target() {
|
||||||
@@ -212,12 +205,12 @@ fn relocations_by_section(
|
|||||||
RelocationKind::Absolute => ObjRelocKind::Absolute,
|
RelocationKind::Absolute => ObjRelocKind::Absolute,
|
||||||
RelocationKind::Elf(kind) => match arch {
|
RelocationKind::Elf(kind) => match arch {
|
||||||
ObjArchitecture::PowerPc => match kind {
|
ObjArchitecture::PowerPc => match kind {
|
||||||
R_PPC_ADDR16_LO => ObjRelocKind::PpcAddr16Lo,
|
elf::R_PPC_ADDR16_LO => ObjRelocKind::PpcAddr16Lo,
|
||||||
R_PPC_ADDR16_HI => ObjRelocKind::PpcAddr16Hi,
|
elf::R_PPC_ADDR16_HI => ObjRelocKind::PpcAddr16Hi,
|
||||||
R_PPC_ADDR16_HA => ObjRelocKind::PpcAddr16Ha,
|
elf::R_PPC_ADDR16_HA => ObjRelocKind::PpcAddr16Ha,
|
||||||
R_PPC_REL24 => ObjRelocKind::PpcRel24,
|
elf::R_PPC_REL24 => ObjRelocKind::PpcRel24,
|
||||||
R_PPC_REL14 => ObjRelocKind::PpcRel14,
|
elf::R_PPC_REL14 => ObjRelocKind::PpcRel14,
|
||||||
R_PPC_EMB_SDA21 => ObjRelocKind::PpcEmbSda21,
|
elf::R_PPC_EMB_SDA21 => ObjRelocKind::PpcEmbSda21,
|
||||||
_ => {
|
_ => {
|
||||||
return Err(anyhow::Error::msg(format!(
|
return Err(anyhow::Error::msg(format!(
|
||||||
"Unhandled PPC relocation type: {kind}"
|
"Unhandled PPC relocation type: {kind}"
|
||||||
@@ -225,14 +218,14 @@ fn relocations_by_section(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
ObjArchitecture::Mips => match kind {
|
ObjArchitecture::Mips => match kind {
|
||||||
R_MIPS_26 => ObjRelocKind::Mips26,
|
elf::R_MIPS_26 => ObjRelocKind::Mips26,
|
||||||
R_MIPS_HI16 => ObjRelocKind::MipsHi16,
|
elf::R_MIPS_HI16 => ObjRelocKind::MipsHi16,
|
||||||
R_MIPS_LO16 => ObjRelocKind::MipsLo16,
|
elf::R_MIPS_LO16 => ObjRelocKind::MipsLo16,
|
||||||
_ => {
|
elf::R_MIPS_GOT16 => ObjRelocKind::MipsGot16,
|
||||||
return Err(anyhow::Error::msg(format!(
|
elf::R_MIPS_CALL16 => ObjRelocKind::MipsCall16,
|
||||||
"Unhandled MIPS relocation type: {kind}"
|
elf::R_MIPS_GPREL16 => ObjRelocKind::MipsGpRel16,
|
||||||
)))
|
elf::R_MIPS_GPREL32 => ObjRelocKind::MipsGpRel32,
|
||||||
}
|
_ => bail!("Unhandled MIPS relocation type: {kind}"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
@@ -249,47 +242,102 @@ fn relocations_by_section(
|
|||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
// println!("Reloc: {:?}, symbol: {:?}", reloc, symbol);
|
|
||||||
let target = match symbol.kind() {
|
|
||||||
SymbolKind::Text | SymbolKind::Data | SymbolKind::Unknown => {
|
|
||||||
to_obj_symbol(obj_file, &symbol, reloc.addend())
|
|
||||||
}
|
|
||||||
SymbolKind::Section => {
|
|
||||||
let addend = if reloc.has_implicit_addend() {
|
let addend = if reloc.has_implicit_addend() {
|
||||||
let addend = u32::from_be_bytes(
|
let addend = u32::from_be_bytes(
|
||||||
section.data[address as usize..address as usize + 4].try_into()?,
|
section.data[address as usize..address as usize + 4].try_into()?,
|
||||||
);
|
);
|
||||||
match kind {
|
match kind {
|
||||||
ObjRelocKind::Absolute => addend,
|
ObjRelocKind::Absolute => addend as i64,
|
||||||
ObjRelocKind::MipsHi16 | ObjRelocKind::MipsLo16 => addend & 0x0000FFFF,
|
ObjRelocKind::MipsHi16 => ((addend & 0x0000FFFF) << 16) as i32 as i64,
|
||||||
ObjRelocKind::Mips26 => (addend & 0x03FFFFFF) * 4,
|
ObjRelocKind::MipsLo16
|
||||||
_ => todo!(),
|
| ObjRelocKind::MipsGot16
|
||||||
|
| ObjRelocKind::MipsCall16
|
||||||
|
| ObjRelocKind::MipsGpRel16 => (addend & 0x0000FFFF) as i16 as i64,
|
||||||
|
ObjRelocKind::MipsGpRel32 => addend as i32 as i64,
|
||||||
|
ObjRelocKind::Mips26 => ((addend & 0x03FFFFFF) << 2) as i64,
|
||||||
|
_ => bail!("Unsupported implicit relocation {kind:?}"),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let addend = reloc.addend();
|
reloc.addend()
|
||||||
if addend < 0 {
|
|
||||||
return Err(anyhow::Error::msg(format!(
|
|
||||||
"Negative addend in section reloc: {addend}"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
addend as u32
|
|
||||||
};
|
};
|
||||||
|
// println!("Reloc: {reloc:?}, symbol: {symbol:?}, addend: {addend:#X}");
|
||||||
|
let target = match symbol.kind() {
|
||||||
|
SymbolKind::Text | SymbolKind::Data | SymbolKind::Label | SymbolKind::Unknown => {
|
||||||
|
to_obj_symbol(obj_file, &symbol, addend)
|
||||||
|
}
|
||||||
|
SymbolKind::Section => {
|
||||||
|
if addend < 0 {
|
||||||
|
return Err(anyhow::Error::msg(format!("Negative addend in reloc: {addend}")));
|
||||||
|
}
|
||||||
find_section_symbol(obj_file, &symbol, addend as u64)
|
find_section_symbol(obj_file, &symbol, addend as u64)
|
||||||
}
|
}
|
||||||
_ => Err(anyhow::Error::msg(format!(
|
kind => Err(anyhow!("Unhandled relocation symbol type {kind:?}")),
|
||||||
"Unhandled relocation symbol type {:?}",
|
|
||||||
symbol.kind()
|
|
||||||
))),
|
|
||||||
}?;
|
}?;
|
||||||
relocations.push(ObjReloc { kind, address, target, target_section });
|
relocations.push(ObjReloc { kind, address, target, target_section });
|
||||||
}
|
}
|
||||||
Ok(relocations)
|
Ok(relocations)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
let data = section.uncompressed_data()?;
|
||||||
|
let mut reader = Cursor::new(data.as_ref());
|
||||||
|
|
||||||
|
let size = 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>()? 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>()? as u64;
|
||||||
|
map.insert(base_address + address_delta, line_number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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> {
|
pub fn read(obj_path: &Path) -> Result<ObjInfo> {
|
||||||
let data = {
|
let (data, timestamp) = {
|
||||||
let file = fs::File::open(obj_path)?;
|
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 obj_file = File::parse(&*data)?;
|
||||||
let architecture = match obj_file.architecture() {
|
let architecture = match obj_file.architecture() {
|
||||||
@@ -305,8 +353,10 @@ pub fn read(obj_path: &Path) -> Result<ObjInfo> {
|
|||||||
let mut result = ObjInfo {
|
let mut result = ObjInfo {
|
||||||
architecture,
|
architecture,
|
||||||
path: obj_path.to_owned(),
|
path: obj_path.to_owned(),
|
||||||
|
timestamp,
|
||||||
sections: filter_sections(&obj_file)?,
|
sections: filter_sections(&obj_file)?,
|
||||||
common: common_symbols(&obj_file)?,
|
common: common_symbols(&obj_file)?,
|
||||||
|
line_info: line_info(&obj_file)?,
|
||||||
};
|
};
|
||||||
for section in &mut result.sections {
|
for section in &mut result.sections {
|
||||||
section.symbols = symbols_by_section(&obj_file, section)?;
|
section.symbols = symbols_by_section(&obj_file, section)?;
|
||||||
|
|||||||
@@ -1,15 +1,27 @@
|
|||||||
use anyhow::Result;
|
use std::collections::BTreeMap;
|
||||||
use rabbitizer::{config_set_register_fpr_abi_names, Abi, Instruction, SimpleOperandType};
|
|
||||||
|
|
||||||
use crate::obj::{ObjIns, ObjInsArg, ObjReloc};
|
use anyhow::Result;
|
||||||
|
use rabbitizer::{config, Abi, InstrCategory, Instruction, OperandType};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
diff::ProcessCodeResult,
|
||||||
|
obj::{ObjIns, ObjInsArg, ObjReloc},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn configure_rabbitizer() {
|
||||||
|
unsafe {
|
||||||
|
config::RabbitizerConfig_Cfg.reg_names.fpr_abi_names = Abi::O32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn process_code(
|
pub fn process_code(
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
start_address: u64,
|
start_address: u64,
|
||||||
end_address: u64,
|
end_address: u64,
|
||||||
relocs: &[ObjReloc],
|
relocs: &[ObjReloc],
|
||||||
) -> Result<(Vec<u8>, Vec<ObjIns>)> {
|
line_info: &Option<BTreeMap<u64, u64>>,
|
||||||
config_set_register_fpr_abi_names(Abi::RABBITIZER_ABI_O32);
|
) -> Result<ProcessCodeResult> {
|
||||||
|
configure_rabbitizer();
|
||||||
|
|
||||||
let ins_count = data.len() / 4;
|
let ins_count = data.len() / 4;
|
||||||
let mut ops = Vec::<u8>::with_capacity(ins_count);
|
let mut ops = Vec::<u8>::with_capacity(ins_count);
|
||||||
@@ -18,47 +30,62 @@ pub fn process_code(
|
|||||||
for chunk in data.chunks_exact(4) {
|
for chunk in data.chunks_exact(4) {
|
||||||
let reloc = relocs.iter().find(|r| (r.address as u32 & !3) == cur_addr);
|
let reloc = relocs.iter().find(|r| (r.address as u32 & !3) == cur_addr);
|
||||||
let code = u32::from_be_bytes(chunk.try_into()?);
|
let code = u32::from_be_bytes(chunk.try_into()?);
|
||||||
let mut instruction = Instruction::new(code, cur_addr);
|
let instruction = Instruction::new(code, cur_addr, InstrCategory::CPU);
|
||||||
|
|
||||||
let op = instruction.instr_id() as u8;
|
let op = instruction.unique_id as u8;
|
||||||
ops.push(op);
|
ops.push(op);
|
||||||
|
|
||||||
let mnemonic = instruction.instr_id().get_opcode_name().unwrap_or_default().to_string();
|
let mnemonic = instruction.opcode_name().to_string();
|
||||||
let is_branch = instruction.is_branch();
|
let is_branch = instruction.is_branch();
|
||||||
let branch_offset = instruction.branch_offset();
|
let branch_offset = instruction.branch_offset();
|
||||||
let branch_dest =
|
let branch_dest =
|
||||||
if is_branch { Some((cur_addr as i32 + branch_offset) as u32) } else { None };
|
if is_branch { Some((cur_addr as i32 + branch_offset) as u32) } else { None };
|
||||||
let args = instruction
|
|
||||||
.simple_operands()
|
let operands = instruction.get_operands_slice();
|
||||||
.iter()
|
let mut args = Vec::with_capacity(operands.len() + 1);
|
||||||
.map(|op| match op.kind {
|
for op in operands {
|
||||||
SimpleOperandType::Imm | SimpleOperandType::Label => {
|
match op {
|
||||||
|
OperandType::cpu_immediate
|
||||||
|
| OperandType::cpu_label
|
||||||
|
| OperandType::cpu_branch_target_label => {
|
||||||
if is_branch {
|
if is_branch {
|
||||||
ObjInsArg::BranchOffset(branch_offset)
|
args.push(ObjInsArg::BranchOffset(branch_offset));
|
||||||
} else if let Some(reloc) = reloc {
|
} else if let Some(reloc) = reloc {
|
||||||
if matches!(&reloc.target_section, Some(s) if s == ".text")
|
if matches!(&reloc.target_section, Some(s) if s == ".text")
|
||||||
&& reloc.target.address > start_address
|
&& reloc.target.address > start_address
|
||||||
&& reloc.target.address < end_address
|
&& reloc.target.address < end_address
|
||||||
{
|
{
|
||||||
// Inter-function reloc, convert to branch offset
|
// Inter-function reloc, convert to branch offset
|
||||||
ObjInsArg::BranchOffset(reloc.target.address as i32 - cur_addr as i32)
|
args.push(ObjInsArg::BranchOffset(
|
||||||
|
reloc.target.address as i32 - cur_addr as i32,
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
ObjInsArg::Reloc
|
args.push(ObjInsArg::Reloc);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ObjInsArg::MipsArg(op.disassembled.clone())
|
args.push(ObjInsArg::MipsArg(op.disassemble(&instruction, None)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SimpleOperandType::ImmBase => {
|
OperandType::cpu_immediate_base => {
|
||||||
if reloc.is_some() {
|
if reloc.is_some() {
|
||||||
ObjInsArg::RelocWithBase
|
args.push(ObjInsArg::RelocWithBase);
|
||||||
} else {
|
} else {
|
||||||
ObjInsArg::MipsArg(op.disassembled.clone())
|
args.push(ObjInsArg::MipsArgWithBase(
|
||||||
|
OperandType::cpu_immediate.disassemble(&instruction, None),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
args.push(ObjInsArg::MipsArg(
|
||||||
|
OperandType::cpu_rs.disassemble(&instruction, None),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
args.push(ObjInsArg::MipsArg(op.disassemble(&instruction, None)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => ObjInsArg::MipsArg(op.disassembled.clone()),
|
}
|
||||||
})
|
let line = line_info
|
||||||
.collect();
|
.as_ref()
|
||||||
|
.and_then(|map| map.range(..=cur_addr as u64).last().map(|(_, &b)| b));
|
||||||
insts.push(ObjIns {
|
insts.push(ObjIns {
|
||||||
address: cur_addr,
|
address: cur_addr,
|
||||||
code,
|
code,
|
||||||
@@ -67,8 +94,10 @@ pub fn process_code(
|
|||||||
args,
|
args,
|
||||||
reloc: reloc.cloned(),
|
reloc: reloc.cloned(),
|
||||||
branch_dest,
|
branch_dest,
|
||||||
|
line,
|
||||||
|
orig: None,
|
||||||
});
|
});
|
||||||
cur_addr += 4;
|
cur_addr += 4;
|
||||||
}
|
}
|
||||||
Ok((ops, insts))
|
Ok(ProcessCodeResult { ops, insts })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ pub mod elf;
|
|||||||
pub mod mips;
|
pub mod mips;
|
||||||
pub mod ppc;
|
pub mod ppc;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::{collections::BTreeMap, path::PathBuf};
|
||||||
|
|
||||||
|
use filetime::FileTime;
|
||||||
use flagset::{flags, FlagSet};
|
use flagset::{flags, FlagSet};
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||||
@@ -18,6 +19,7 @@ flags! {
|
|||||||
Local,
|
Local,
|
||||||
Weak,
|
Weak,
|
||||||
Common,
|
Common,
|
||||||
|
Hidden,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[derive(Debug, Copy, Clone, Default)]
|
#[derive(Debug, Copy, Clone, Default)]
|
||||||
@@ -37,14 +39,51 @@ pub struct ObjSection {
|
|||||||
pub data_diff: Vec<ObjDataDiff>,
|
pub data_diff: Vec<ObjDataDiff>,
|
||||||
pub match_percent: f32,
|
pub match_percent: f32,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
pub enum ObjInsArg {
|
pub enum ObjInsArg {
|
||||||
PpcArg(ppc750cl::Argument),
|
PpcArg(ppc750cl::Argument),
|
||||||
MipsArg(String),
|
MipsArg(String),
|
||||||
|
MipsArgWithBase(String),
|
||||||
Reloc,
|
Reloc,
|
||||||
RelocWithBase,
|
RelocWithBase,
|
||||||
BranchOffset(i32),
|
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)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct ObjInsArgDiff {
|
pub struct ObjInsArgDiff {
|
||||||
/// Incrementing index for coloring
|
/// Incrementing index for coloring
|
||||||
@@ -83,6 +122,10 @@ pub struct ObjIns {
|
|||||||
pub args: Vec<ObjInsArg>,
|
pub args: Vec<ObjInsArg>,
|
||||||
pub reloc: Option<ObjReloc>,
|
pub reloc: Option<ObjReloc>,
|
||||||
pub branch_dest: Option<u32>,
|
pub branch_dest: Option<u32>,
|
||||||
|
/// Line info
|
||||||
|
pub line: Option<u64>,
|
||||||
|
/// Original (unsimplified) instruction
|
||||||
|
pub orig: Option<String>,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct ObjInsDiff {
|
pub struct ObjInsDiff {
|
||||||
@@ -136,8 +179,10 @@ pub enum ObjArchitecture {
|
|||||||
pub struct ObjInfo {
|
pub struct ObjInfo {
|
||||||
pub architecture: ObjArchitecture,
|
pub architecture: ObjArchitecture,
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
pub timestamp: FileTime,
|
||||||
pub sections: Vec<ObjSection>,
|
pub sections: Vec<ObjSection>,
|
||||||
pub common: Vec<ObjSymbol>,
|
pub common: Vec<ObjSymbol>,
|
||||||
|
pub line_info: Option<BTreeMap<u64, u64>>,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||||
pub enum ObjRelocKind {
|
pub enum ObjRelocKind {
|
||||||
@@ -155,6 +200,10 @@ pub enum ObjRelocKind {
|
|||||||
Mips26,
|
Mips26,
|
||||||
MipsHi16,
|
MipsHi16,
|
||||||
MipsLo16,
|
MipsLo16,
|
||||||
|
MipsGot16,
|
||||||
|
MipsCall16,
|
||||||
|
MipsGpRel16,
|
||||||
|
MipsGpRel32,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ObjReloc {
|
pub struct ObjReloc {
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
use anyhow::Result;
|
use std::collections::BTreeMap;
|
||||||
use ppc750cl::{disasm_iter, Argument};
|
|
||||||
|
|
||||||
use crate::obj::{ObjIns, ObjInsArg, ObjReloc, ObjRelocKind};
|
use anyhow::Result;
|
||||||
|
use ppc750cl::{disasm_iter, Argument, SimplifiedIns};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
diff::ProcessCodeResult,
|
||||||
|
obj::{ObjIns, ObjInsArg, ObjReloc, ObjRelocKind},
|
||||||
|
};
|
||||||
|
|
||||||
// Relative relocation, can be Simm or BranchOffset
|
// Relative relocation, can be Simm or BranchOffset
|
||||||
fn is_relative_arg(arg: &ObjInsArg) -> bool {
|
fn is_relative_arg(arg: &ObjInsArg) -> bool {
|
||||||
@@ -19,7 +24,8 @@ pub fn process_code(
|
|||||||
data: &[u8],
|
data: &[u8],
|
||||||
address: u64,
|
address: u64,
|
||||||
relocs: &[ObjReloc],
|
relocs: &[ObjReloc],
|
||||||
) -> Result<(Vec<u8>, Vec<ObjIns>)> {
|
line_info: &Option<BTreeMap<u64, u64>>,
|
||||||
|
) -> Result<ProcessCodeResult> {
|
||||||
let ins_count = data.len() / 4;
|
let ins_count = data.len() / 4;
|
||||||
let mut ops = Vec::<u8>::with_capacity(ins_count);
|
let mut ops = Vec::<u8>::with_capacity(ins_count);
|
||||||
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
|
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
|
||||||
@@ -37,7 +43,7 @@ pub fn process_code(
|
|||||||
_ => ins.code,
|
_ => ins.code,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let simplified = ins.simplified();
|
let simplified = ins.clone().simplified();
|
||||||
let mut args: Vec<ObjInsArg> = simplified
|
let mut args: Vec<ObjInsArg> = simplified
|
||||||
.args
|
.args
|
||||||
.iter()
|
.iter()
|
||||||
@@ -74,15 +80,20 @@ pub fn process_code(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ops.push(simplified.ins.op as u8);
|
ops.push(simplified.ins.op as u8);
|
||||||
|
let line = line_info
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|map| map.range(..=simplified.ins.addr as u64).last().map(|(_, &b)| b));
|
||||||
insts.push(ObjIns {
|
insts.push(ObjIns {
|
||||||
address: simplified.ins.addr,
|
address: simplified.ins.addr,
|
||||||
code: simplified.ins.code,
|
code: simplified.ins.code,
|
||||||
mnemonic: format!("{}{}", simplified.mnemonic, simplified.suffix),
|
mnemonic: format!("{}{}", simplified.mnemonic, simplified.suffix),
|
||||||
args,
|
args,
|
||||||
reloc: reloc.cloned(),
|
reloc: reloc.cloned(),
|
||||||
op: 0,
|
op: ins.op as u8,
|
||||||
branch_dest: None,
|
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
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,45 +1,47 @@
|
|||||||
use std::{cmp::min, default::Default, mem::take};
|
use std::{cmp::min, default::Default, mem::take};
|
||||||
|
|
||||||
use egui::{text::LayoutJob, Color32, Label, Sense};
|
use egui::{text::LayoutJob, Align, Label, Layout, Sense, Vec2};
|
||||||
use egui_extras::{Size, StripBuilder, TableBuilder};
|
use egui_extras::{Column, TableBuilder};
|
||||||
use time::format_description;
|
use time::format_description;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{View, ViewConfig, ViewState},
|
|
||||||
jobs::Job,
|
|
||||||
obj::{ObjDataDiff, ObjDataDiffKind, ObjInfo, ObjSection},
|
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;
|
const BYTES_PER_ROW: usize = 16;
|
||||||
|
|
||||||
fn find_section<'a>(obj: &'a ObjInfo, section_name: &str) -> Option<&'a ObjSection> {
|
fn find_section<'a>(obj: &'a ObjInfo, selected_symbol: &SymbolReference) -> Option<&'a ObjSection> {
|
||||||
obj.sections.iter().find(|s| s.name == section_name)
|
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) {
|
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);
|
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
|
||||||
}
|
}
|
||||||
let mut job = LayoutJob::default();
|
let mut job = LayoutJob::default();
|
||||||
write_text(
|
write_text(
|
||||||
format!("{address:08X}: ").as_str(),
|
format!("{address:08X}: ").as_str(),
|
||||||
Color32::GRAY,
|
appearance.text_color,
|
||||||
&mut job,
|
&mut job,
|
||||||
config.code_font.clone(),
|
appearance.code_font.clone(),
|
||||||
);
|
);
|
||||||
let mut cur_addr = 0usize;
|
let mut cur_addr = 0usize;
|
||||||
for diff in diffs {
|
for diff in diffs {
|
||||||
let base_color = match diff.kind {
|
let base_color = match diff.kind {
|
||||||
ObjDataDiffKind::None => Color32::GRAY,
|
ObjDataDiffKind::None => appearance.text_color,
|
||||||
ObjDataDiffKind::Replace => Color32::LIGHT_BLUE,
|
ObjDataDiffKind::Replace => appearance.replace_color,
|
||||||
ObjDataDiffKind::Delete => COLOR_RED,
|
ObjDataDiffKind::Delete => appearance.delete_color,
|
||||||
ObjDataDiffKind::Insert => Color32::GREEN,
|
ObjDataDiffKind::Insert => appearance.insert_color,
|
||||||
};
|
};
|
||||||
if diff.data.is_empty() {
|
if diff.data.is_empty() {
|
||||||
let mut str = " ".repeat(diff.len);
|
let mut str = " ".repeat(diff.len);
|
||||||
str.push_str(" ".repeat(diff.len / 8).as_str());
|
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;
|
cur_addr += diff.len;
|
||||||
} else {
|
} else {
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
@@ -50,7 +52,7 @@ fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], config:
|
|||||||
text.push(' ');
|
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 {
|
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();
|
let mut str = " ".to_string();
|
||||||
str.push_str(" ".repeat(n).as_str());
|
str.push_str(" ".repeat(n).as_str());
|
||||||
str.push_str(" ".repeat(n / 8).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 {
|
for diff in diffs {
|
||||||
let base_color = match diff.kind {
|
let base_color = match diff.kind {
|
||||||
ObjDataDiffKind::None => Color32::GRAY,
|
ObjDataDiffKind::None => appearance.text_color,
|
||||||
ObjDataDiffKind::Replace => Color32::LIGHT_BLUE,
|
ObjDataDiffKind::Replace => appearance.replace_color,
|
||||||
ObjDataDiffKind::Delete => COLOR_RED,
|
ObjDataDiffKind::Delete => appearance.delete_color,
|
||||||
ObjDataDiffKind::Insert => Color32::GREEN,
|
ObjDataDiffKind::Insert => appearance.insert_color,
|
||||||
};
|
};
|
||||||
if diff.data.is_empty() {
|
if diff.data.is_empty() {
|
||||||
write_text(
|
write_text(
|
||||||
" ".repeat(diff.len).as_str(),
|
" ".repeat(diff.len).as_str(),
|
||||||
base_color,
|
base_color,
|
||||||
&mut job,
|
&mut job,
|
||||||
config.code_font.clone(),
|
appearance.code_font.clone(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
@@ -85,7 +87,7 @@ fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], config:
|
|||||||
text.push('.');
|
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()));
|
ui.add(Label::new(job).sense(Sense::click()));
|
||||||
@@ -130,132 +132,139 @@ fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> {
|
|||||||
|
|
||||||
fn data_table_ui(
|
fn data_table_ui(
|
||||||
table: TableBuilder<'_>,
|
table: TableBuilder<'_>,
|
||||||
left_obj: &ObjInfo,
|
left_obj: Option<&ObjInfo>,
|
||||||
right_obj: &ObjInfo,
|
right_obj: Option<&ObjInfo>,
|
||||||
section_name: &str,
|
selected_symbol: &SymbolReference,
|
||||||
config: &ViewConfig,
|
config: &Appearance,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
let left_section = find_section(left_obj, section_name)?;
|
let left_section = left_obj.and_then(|obj| find_section(obj, selected_symbol));
|
||||||
let right_section = find_section(right_obj, section_name)?;
|
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 {
|
if total_bytes == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let total_rows = (total_bytes - 1) / BYTES_PER_ROW + 1;
|
let total_rows = (total_bytes - 1) / BYTES_PER_ROW + 1;
|
||||||
|
|
||||||
let left_diffs = split_diffs(&left_section.data_diff);
|
let left_diffs = left_section.map(|section| split_diffs(§ion.data_diff));
|
||||||
let right_diffs = split_diffs(&right_section.data_diff);
|
let right_diffs = right_section.map(|section| split_diffs(§ion.data_diff));
|
||||||
|
|
||||||
table.body(|body| {
|
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;
|
let address = row_index * BYTES_PER_ROW;
|
||||||
row.col(|ui| {
|
row.col(|ui| {
|
||||||
|
if let Some(left_diffs) = &left_diffs {
|
||||||
data_row_ui(ui, address, &left_diffs[row_index], config);
|
data_row_ui(ui, address, &left_diffs[row_index], config);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
row.col(|ui| {
|
row.col(|ui| {
|
||||||
|
if let Some(right_diffs) = &right_diffs {
|
||||||
data_row_ui(ui, address, &right_diffs[row_index], config);
|
data_row_ui(ui, address, &right_diffs[row_index], config);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Some(())
|
Some(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn data_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
|
pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
|
||||||
let mut rebuild = false;
|
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
|
||||||
if let (Some(result), Some(selected_symbol)) = (&view_state.build, &view_state.selected_symbol)
|
else {
|
||||||
{
|
return;
|
||||||
StripBuilder::new(ui)
|
};
|
||||||
.size(Size::exact(20.0))
|
|
||||||
.size(Size::exact(40.0))
|
// Header
|
||||||
.size(Size::remainder())
|
let available_width = ui.available_width();
|
||||||
.vertical(|mut strip| {
|
let column_width = available_width / 2.0;
|
||||||
strip.strip(|builder| {
|
ui.allocate_ui_with_layout(
|
||||||
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
|
Vec2 { x: available_width, y: 100.0 },
|
||||||
strip.cell(|ui| {
|
Layout::left_to_right(Align::Min),
|
||||||
ui.horizontal(|ui| {
|
|ui| {
|
||||||
if ui.button("Back").clicked() {
|
// Left column
|
||||||
view_state.current_view = View::SymbolDiff;
|
ui.allocate_ui_with_layout(
|
||||||
|
Vec2 { x: column_width, y: 100.0 },
|
||||||
|
Layout::top_down(Align::Min),
|
||||||
|
|ui| {
|
||||||
|
ui.set_width(column_width);
|
||||||
|
|
||||||
|
if ui.button("⏴ Back").clicked() {
|
||||||
|
state.current_view = View::SymbolDiff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ui.scope(|ui| {
|
||||||
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
|
ui.style_mut().wrap = Some(false);
|
||||||
|
ui.colored_label(appearance.highlight_color, &selected_symbol.symbol_name);
|
||||||
|
ui.label("Diff target:");
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
strip.cell(|ui| {
|
);
|
||||||
|
|
||||||
|
// Right column
|
||||||
|
ui.allocate_ui_with_layout(
|
||||||
|
Vec2 { x: column_width, y: 100.0 },
|
||||||
|
Layout::top_down(Align::Min),
|
||||||
|
|ui| {
|
||||||
|
ui.set_width(column_width);
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if ui.button("Build").clicked() {
|
if ui
|
||||||
rebuild = true;
|
.add_enabled(!state.build_running, egui::Button::new("Build"))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
state.queue_build = true;
|
||||||
}
|
}
|
||||||
ui.scope(|ui| {
|
ui.scope(|ui| {
|
||||||
ui.style_mut().override_text_style =
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
Some(egui::TextStyle::Monospace);
|
|
||||||
ui.style_mut().wrap = Some(false);
|
ui.style_mut().wrap = Some(false);
|
||||||
if view_state
|
if state.build_running {
|
||||||
.jobs
|
ui.colored_label(appearance.replace_color, "Building…");
|
||||||
.iter()
|
|
||||||
.any(|job| job.job_type == Job::ObjDiff)
|
|
||||||
{
|
|
||||||
ui.label("Building...");
|
|
||||||
} else {
|
} else {
|
||||||
ui.label("Last built:");
|
ui.label("Last built:");
|
||||||
let format =
|
let format =
|
||||||
format_description::parse("[hour]:[minute]:[second]")
|
format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||||
.unwrap();
|
|
||||||
ui.label(
|
ui.label(
|
||||||
result
|
result
|
||||||
.time
|
.time
|
||||||
.to_offset(view_state.utc_offset)
|
.to_offset(appearance.utc_offset)
|
||||||
.format(&format)
|
.format(&format)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
strip.strip(|builder| {
|
|
||||||
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
|
|
||||||
strip.cell(|ui| {
|
|
||||||
ui.scope(|ui| {
|
ui.scope(|ui| {
|
||||||
ui.style_mut().override_text_style =
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
Some(egui::TextStyle::Monospace);
|
|
||||||
ui.style_mut().wrap = Some(false);
|
|
||||||
ui.colored_label(Color32::WHITE, selected_symbol);
|
|
||||||
ui.label("Diff target:");
|
|
||||||
ui.separator();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
strip.cell(|ui| {
|
|
||||||
ui.scope(|ui| {
|
|
||||||
ui.style_mut().override_text_style =
|
|
||||||
Some(egui::TextStyle::Monospace);
|
|
||||||
ui.style_mut().wrap = Some(false);
|
ui.style_mut().wrap = Some(false);
|
||||||
ui.label("");
|
ui.label("");
|
||||||
ui.label("Diff base:");
|
ui.label("Diff base:");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
ui.separator();
|
ui.separator();
|
||||||
});
|
|
||||||
});
|
// Table
|
||||||
});
|
let available_height = ui.available_height();
|
||||||
});
|
|
||||||
strip.cell(|ui| {
|
|
||||||
if let (Some(left_obj), Some(right_obj)) =
|
|
||||||
(&result.first_obj, &result.second_obj)
|
|
||||||
{
|
|
||||||
let table = TableBuilder::new(ui)
|
let table = TableBuilder::new(ui)
|
||||||
.striped(false)
|
.striped(false)
|
||||||
.cell_layout(egui::Layout::left_to_right(egui::Align::Min))
|
.cell_layout(Layout::left_to_right(Align::Min))
|
||||||
.column(Size::relative(0.5))
|
.columns(Column::exact(column_width).clip(true), 2)
|
||||||
.column(Size::relative(0.5))
|
.resizable(false)
|
||||||
.resizable(false);
|
.auto_shrink([false, false])
|
||||||
|
.min_scrolled_height(available_height);
|
||||||
data_table_ui(
|
data_table_ui(
|
||||||
table,
|
table,
|
||||||
left_obj,
|
result.first_obj.as_ref(),
|
||||||
right_obj,
|
result.second_obj.as_ref(),
|
||||||
selected_symbol,
|
selected_symbol,
|
||||||
&view_state.view_config,
|
appearance,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
rebuild
|
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/views/debug.rs
Normal 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
@@ -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
@@ -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
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,62 +1,128 @@
|
|||||||
use std::default::Default;
|
use std::{
|
||||||
|
cmp::{max, Ordering},
|
||||||
|
default::Default,
|
||||||
|
};
|
||||||
|
|
||||||
use cwdemangle::demangle;
|
use cwdemangle::demangle;
|
||||||
use egui::{text::LayoutJob, Color32, FontId, Label, Sense};
|
use egui::{text::LayoutJob, Align, Color32, Label, Layout, RichText, Sense, TextFormat, Vec2};
|
||||||
use egui_extras::{Size, StripBuilder, TableBuilder};
|
use egui_extras::{Column, TableBuilder, TableRow};
|
||||||
use ppc750cl::Argument;
|
use ppc750cl::Argument;
|
||||||
use time::format_description;
|
use time::format_description;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{View, ViewConfig, ViewState},
|
|
||||||
jobs::Job,
|
|
||||||
obj::{
|
obj::{
|
||||||
ObjInfo, ObjIns, ObjInsArg, ObjInsArgDiff, ObjInsDiff, ObjInsDiffKind, ObjReloc,
|
ObjInfo, ObjIns, ObjInsArg, ObjInsArgDiff, ObjInsDiff, ObjInsDiffKind, ObjReloc,
|
||||||
ObjRelocKind, ObjSymbol,
|
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);
|
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 {
|
||||||
if reloc.target.addend != 0 {
|
font_id: appearance.code_font.clone(),
|
||||||
write_text(&format!("+{:X}", reloc.target.addend), color, job, font_id);
|
color: appearance.emphasized_text_color,
|
||||||
|
background: background_color,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
match reloc.target.addend.cmp(&0i64) {
|
||||||
|
Ordering::Greater => write_text(
|
||||||
|
&format!("+{:#X}", reloc.target.addend),
|
||||||
|
color,
|
||||||
|
job,
|
||||||
|
appearance.code_font.clone(),
|
||||||
|
),
|
||||||
|
Ordering::Less => {
|
||||||
|
write_text(
|
||||||
|
&format!("-{:#X}", -reloc.target.addend),
|
||||||
|
color,
|
||||||
|
job,
|
||||||
|
appearance.code_font.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_reloc(reloc: &ObjReloc, color: Color32, job: &mut LayoutJob, font_id: FontId) {
|
fn write_reloc(
|
||||||
|
reloc: &ObjReloc,
|
||||||
|
color: Color32,
|
||||||
|
background_color: Color32,
|
||||||
|
job: &mut LayoutJob,
|
||||||
|
appearance: &Appearance,
|
||||||
|
) {
|
||||||
match reloc.kind {
|
match reloc.kind {
|
||||||
ObjRelocKind::PpcAddr16Lo => {
|
ObjRelocKind::PpcAddr16Lo => {
|
||||||
write_reloc_name(reloc, color, job, font_id.clone());
|
write_reloc_name(reloc, color, background_color, job, appearance);
|
||||||
write_text("@l", color, job, font_id);
|
write_text("@l", color, job, appearance.code_font.clone());
|
||||||
}
|
}
|
||||||
ObjRelocKind::PpcAddr16Hi => {
|
ObjRelocKind::PpcAddr16Hi => {
|
||||||
write_reloc_name(reloc, color, job, font_id.clone());
|
write_reloc_name(reloc, color, background_color, job, appearance);
|
||||||
write_text("@h", color, job, font_id);
|
write_text("@h", color, job, appearance.code_font.clone());
|
||||||
}
|
}
|
||||||
ObjRelocKind::PpcAddr16Ha => {
|
ObjRelocKind::PpcAddr16Ha => {
|
||||||
write_reloc_name(reloc, color, job, font_id.clone());
|
write_reloc_name(reloc, color, background_color, job, appearance);
|
||||||
write_text("@ha", color, job, font_id);
|
write_text("@ha", color, job, appearance.code_font.clone());
|
||||||
}
|
}
|
||||||
ObjRelocKind::PpcEmbSda21 => {
|
ObjRelocKind::PpcEmbSda21 => {
|
||||||
write_reloc_name(reloc, color, job, font_id.clone());
|
write_reloc_name(reloc, color, background_color, job, appearance);
|
||||||
write_text("@sda21", color, job, font_id);
|
write_text("@sda21", color, job, appearance.code_font.clone());
|
||||||
}
|
}
|
||||||
ObjRelocKind::MipsHi16 => {
|
ObjRelocKind::MipsHi16 => {
|
||||||
write_text("%hi(", color, job, font_id.clone());
|
write_text("%hi(", color, job, appearance.code_font.clone());
|
||||||
write_reloc_name(reloc, color, job, font_id.clone());
|
write_reloc_name(reloc, color, background_color, job, appearance);
|
||||||
write_text(")", color, job, font_id);
|
write_text(")", color, job, appearance.code_font.clone());
|
||||||
}
|
}
|
||||||
ObjRelocKind::MipsLo16 => {
|
ObjRelocKind::MipsLo16 => {
|
||||||
write_text("%lo(", color, job, font_id.clone());
|
write_text("%lo(", color, job, appearance.code_font.clone());
|
||||||
write_reloc_name(reloc, color, job, font_id.clone());
|
write_reloc_name(reloc, color, background_color, job, appearance);
|
||||||
write_text(")", color, job, font_id);
|
write_text(")", color, job, appearance.code_font.clone());
|
||||||
}
|
}
|
||||||
ObjRelocKind::Absolute
|
ObjRelocKind::MipsGot16 => {
|
||||||
| ObjRelocKind::PpcRel24
|
write_text("%got(", color, job, appearance.code_font.clone());
|
||||||
| ObjRelocKind::PpcRel14
|
write_reloc_name(reloc, color, background_color, job, appearance);
|
||||||
| ObjRelocKind::Mips26 => {
|
write_text(")", color, job, appearance.code_font.clone());
|
||||||
write_reloc_name(reloc, color, job, font_id);
|
}
|
||||||
|
ObjRelocKind::MipsCall16 => {
|
||||||
|
write_text("%call16(", color, job, appearance.code_font.clone());
|
||||||
|
write_reloc_name(reloc, color, background_color, job, appearance);
|
||||||
|
write_text(")", color, job, appearance.code_font.clone());
|
||||||
|
}
|
||||||
|
ObjRelocKind::MipsGpRel16 => {
|
||||||
|
write_text("%gp_rel(", color, job, appearance.code_font.clone());
|
||||||
|
write_reloc_name(reloc, color, background_color, job, appearance);
|
||||||
|
write_text(")", color, job, appearance.code_font.clone());
|
||||||
|
}
|
||||||
|
ObjRelocKind::PpcRel24 | ObjRelocKind::PpcRel14 | ObjRelocKind::Mips26 => {
|
||||||
|
write_reloc_name(reloc, color, background_color, job, appearance);
|
||||||
|
}
|
||||||
|
ObjRelocKind::Absolute | ObjRelocKind::MipsGpRel32 => {
|
||||||
|
write_text("[INVALID]", color, job, appearance.code_font.clone());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -66,90 +132,170 @@ fn write_ins(
|
|||||||
diff_kind: &ObjInsDiffKind,
|
diff_kind: &ObjInsDiffKind,
|
||||||
args: &[Option<ObjInsArgDiff>],
|
args: &[Option<ObjInsArgDiff>],
|
||||||
base_addr: u32,
|
base_addr: u32,
|
||||||
job: &mut LayoutJob,
|
ui: &mut egui::Ui,
|
||||||
config: &ViewConfig,
|
appearance: &Appearance,
|
||||||
|
ins_view_state: &mut FunctionViewState,
|
||||||
) {
|
) {
|
||||||
let base_color = match diff_kind {
|
let base_color = match diff_kind {
|
||||||
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
|
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
|
||||||
Color32::GRAY
|
appearance.text_color
|
||||||
}
|
}
|
||||||
ObjInsDiffKind::Replace => Color32::LIGHT_BLUE,
|
ObjInsDiffKind::Replace => appearance.replace_color,
|
||||||
ObjInsDiffKind::Delete => COLOR_RED,
|
ObjInsDiffKind::Delete => appearance.delete_color,
|
||||||
ObjInsDiffKind::Insert => Color32::GREEN,
|
ObjInsDiffKind::Insert => appearance.insert_color,
|
||||||
};
|
};
|
||||||
write_text(
|
|
||||||
&format!("{:<11}", ins.mnemonic),
|
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 {
|
match diff_kind {
|
||||||
ObjInsDiffKind::OpMismatch => Color32::LIGHT_BLUE,
|
ObjInsDiffKind::OpMismatch => appearance.replace_color,
|
||||||
_ => base_color,
|
_ => base_color,
|
||||||
},
|
}
|
||||||
job,
|
})
|
||||||
config.code_font.clone(),
|
.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;
|
let mut writing_offset = false;
|
||||||
for (i, arg) in ins.args.iter().enumerate() {
|
for (i, arg) in ins.args.iter().enumerate() {
|
||||||
|
let mut job = LayoutJob::default();
|
||||||
if i == 0 {
|
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 {
|
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()) {
|
let highlighted_arg = match &ins_view_state.highlight {
|
||||||
config.diff_colors[diff.idx % config.diff_colors.len()]
|
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 {
|
} else {
|
||||||
base_color
|
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 {
|
match arg {
|
||||||
ObjInsArg::PpcArg(arg) => match arg {
|
ObjInsArg::PpcArg(arg) => match arg {
|
||||||
Argument::Offset(val) => {
|
Argument::Offset(val) => {
|
||||||
write_text(&format!("{val}"), color, job, config.code_font.clone());
|
job.append(&format!("{val}"), 0.0, text_format);
|
||||||
write_text("(", base_color, job, config.code_font.clone());
|
write_text("(", base_color, &mut job, appearance.code_font.clone());
|
||||||
writing_offset = true;
|
new_writing_offset = true;
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
Argument::Uimm(_) | Argument::Simm(_) => {
|
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 => {
|
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 => {
|
ObjInsArg::RelocWithBase => {
|
||||||
write_reloc(ins.reloc.as_ref().unwrap(), base_color, job, config.code_font.clone());
|
write_reloc(
|
||||||
write_text("(", base_color, job, config.code_font.clone());
|
ins.reloc.as_ref().unwrap(),
|
||||||
writing_offset = true;
|
base_color,
|
||||||
continue;
|
text_format.background,
|
||||||
|
&mut job,
|
||||||
|
appearance,
|
||||||
|
);
|
||||||
|
write_text("(", base_color, &mut job, appearance.code_font.clone());
|
||||||
|
new_writing_offset = true;
|
||||||
}
|
}
|
||||||
ObjInsArg::MipsArg(str) => {
|
ObjInsArg::MipsArg(str) => {
|
||||||
write_text(
|
job.append(str.strip_prefix('$').unwrap_or(str), 0.0, text_format);
|
||||||
str.strip_prefix('$').unwrap_or(str),
|
}
|
||||||
color,
|
ObjInsArg::MipsArgWithBase(str) => {
|
||||||
job,
|
job.append(str.strip_prefix('$').unwrap_or(str), 0.0, text_format);
|
||||||
config.code_font.clone(),
|
write_text("(", base_color, &mut job, appearance.code_font.clone());
|
||||||
);
|
new_writing_offset = true;
|
||||||
}
|
}
|
||||||
ObjInsArg::BranchOffset(offset) => {
|
ObjInsArg::BranchOffset(offset) => {
|
||||||
let addr = offset + ins.address as i32 - base_addr as i32;
|
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 {
|
if writing_offset {
|
||||||
write_text(")", base_color, job, config.code_font.clone());
|
write_text(")", base_color, &mut job, appearance.code_font.clone());
|
||||||
writing_offset = false;
|
}
|
||||||
|
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.scope(|ui| {
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
ui.style_mut().wrap = Some(false);
|
ui.style_mut().wrap = Some(false);
|
||||||
|
|
||||||
ui.label(format!("{:02X?}", ins.code.to_be_bytes()));
|
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 {
|
for arg in &ins.args {
|
||||||
if let ObjInsArg::PpcArg(arg) = arg {
|
if let ObjInsArg::PpcArg(arg) = arg {
|
||||||
match arg {
|
match arg {
|
||||||
@@ -169,13 +315,19 @@ fn ins_hover_ui(ui: &mut egui::Ui, ins: &ObjIns) {
|
|||||||
|
|
||||||
if let Some(reloc) = &ins.reloc {
|
if let Some(reloc) = &ins.reloc {
|
||||||
ui.label(format!("Relocation type: {:?}", reloc.kind));
|
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 {
|
if let Some(section) = &reloc.target_section {
|
||||||
ui.colored_label(Color32::WHITE, format!("Section: {section}"));
|
ui.colored_label(appearance.highlight_color, format!("Section: {section}"));
|
||||||
ui.colored_label(Color32::WHITE, format!("Address: {:x}", reloc.target.address));
|
ui.colored_label(
|
||||||
ui.colored_label(Color32::WHITE, format!("Size: {:x}", reloc.target.size));
|
appearance.highlight_color,
|
||||||
|
format!("Address: {:x}", reloc.target.address),
|
||||||
|
);
|
||||||
|
ui.colored_label(
|
||||||
|
appearance.highlight_color,
|
||||||
|
format!("Size: {:x}", reloc.target.size),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
ui.colored_label(Color32::WHITE, "Extern".to_string());
|
ui.colored_label(appearance.highlight_color, "Extern".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -193,31 +345,31 @@ fn ins_context_menu(ui: &mut egui::Ui, ins: &ObjIns) {
|
|||||||
match arg {
|
match arg {
|
||||||
Argument::Uimm(v) => {
|
Argument::Uimm(v) => {
|
||||||
if ui.button(format!("Copy \"{v}\"")).clicked() {
|
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();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
if ui.button(format!("Copy \"{}\"", v.0)).clicked() {
|
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();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Argument::Simm(v) => {
|
Argument::Simm(v) => {
|
||||||
if ui.button(format!("Copy \"{v}\"")).clicked() {
|
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();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
if ui.button(format!("Copy \"{}\"", v.0)).clicked() {
|
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();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Argument::Offset(v) => {
|
Argument::Offset(v) => {
|
||||||
if ui.button(format!("Copy \"{v}\"")).clicked() {
|
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();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
if ui.button(format!("Copy \"{}\"", v.0)).clicked() {
|
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();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,206 +380,321 @@ fn ins_context_menu(ui: &mut egui::Ui, ins: &ObjIns) {
|
|||||||
if let Some(reloc) = &ins.reloc {
|
if let Some(reloc) = &ins.reloc {
|
||||||
if let Some(name) = &reloc.target.demangled_name {
|
if let Some(name) = &reloc.target.demangled_name {
|
||||||
if ui.button(format!("Copy \"{name}\"")).clicked() {
|
if ui.button(format!("Copy \"{name}\"")).clicked() {
|
||||||
ui.output().copied_text = name.clone();
|
ui.output_mut(|output| output.copied_text = name.clone());
|
||||||
ui.close_menu();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ui.button(format!("Copy \"{}\"", reloc.target.name)).clicked() {
|
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();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_symbol<'a>(obj: &'a ObjInfo, section_name: &str, name: &str) -> Option<&'a ObjSymbol> {
|
fn find_symbol<'a>(obj: &'a ObjInfo, selected_symbol: &SymbolReference) -> Option<&'a ObjSymbol> {
|
||||||
let section = obj.sections.iter().find(|s| s.name == section_name)?;
|
obj.sections.iter().find_map(|section| {
|
||||||
section.symbols.iter().find(|s| s.name == name)
|
section.symbols.iter().find(|symbol| symbol.name == selected_symbol.symbol_name)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
if ins_diff.kind != ObjInsDiffKind::None {
|
||||||
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
|
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
|
||||||
}
|
}
|
||||||
let mut job = LayoutJob::default();
|
let mut job = LayoutJob::default();
|
||||||
if let Some(ins) = &ins_diff.ins {
|
let Some(ins) = &ins_diff.ins else {
|
||||||
|
ui.label("");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let base_color = match ins_diff.kind {
|
let base_color = match ins_diff.kind {
|
||||||
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
|
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
|
||||||
Color32::GRAY
|
appearance.text_color
|
||||||
}
|
}
|
||||||
ObjInsDiffKind::Replace => Color32::LIGHT_BLUE,
|
ObjInsDiffKind::Replace => appearance.replace_color,
|
||||||
ObjInsDiffKind::Delete => COLOR_RED,
|
ObjInsDiffKind::Delete => appearance.delete_color,
|
||||||
ObjInsDiffKind::Insert => Color32::GREEN,
|
ObjInsDiffKind::Insert => appearance.insert_color,
|
||||||
};
|
};
|
||||||
|
let mut pad = 6;
|
||||||
|
if let Some(line) = ins.line {
|
||||||
|
let line_str = format!("{line} ");
|
||||||
write_text(
|
write_text(
|
||||||
&format!("{:<6}", format!("{:x}:", ins.address - symbol.address as u32)),
|
&line_str,
|
||||||
base_color,
|
appearance.deemphasized_text_color,
|
||||||
&mut job,
|
&mut job,
|
||||||
config.code_font.clone(),
|
appearance.code_font.clone(),
|
||||||
);
|
);
|
||||||
if let Some(branch) = &ins_diff.branch_from {
|
pad = 12 - line_str.len();
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
write_ins(ins, &ins_diff.kind, &ins_diff.arg_diff, symbol.address as u32, &mut job, config);
|
let base_addr = symbol.address as u32;
|
||||||
|
let addr_highlight = matches!(
|
||||||
|
&ins_view_state.highlight,
|
||||||
|
HighlightKind::Address(v) if *v == (ins.address - base_addr)
|
||||||
|
);
|
||||||
|
let addr_string = format!("{:x}", ins.address - symbol.address as u32);
|
||||||
|
pad -= addr_string.len();
|
||||||
|
job.append(&addr_string, 0.0, TextFormat {
|
||||||
|
font_id: appearance.code_font.clone(),
|
||||||
|
color: if addr_highlight { appearance.emphasized_text_color } else { base_color },
|
||||||
|
background: if addr_highlight {
|
||||||
|
appearance.deemphasized_text_color
|
||||||
|
} else {
|
||||||
|
Color32::TRANSPARENT
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
if ui
|
||||||
|
.add(Label::new(job).sense(Sense::click()))
|
||||||
|
.context_menu(|ui| ins_context_menu(ui, ins))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
if addr_highlight {
|
||||||
|
ins_view_state.highlight = HighlightKind::None;
|
||||||
|
} else {
|
||||||
|
ins_view_state.highlight = HighlightKind::Address(ins.address - base_addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut job = LayoutJob::default();
|
||||||
|
let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' '));
|
||||||
|
let spacing = space_width * pad as f32;
|
||||||
|
job.append(": ", 0.0, TextFormat {
|
||||||
|
font_id: appearance.code_font.clone(),
|
||||||
|
color: base_color,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
if let Some(branch) = &ins_diff.branch_from {
|
||||||
|
job.append("~> ", spacing, TextFormat {
|
||||||
|
font_id: appearance.code_font.clone(),
|
||||||
|
color: appearance.diff_colors[branch.branch_idx % appearance.diff_colors.len()],
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
job.append(" ", spacing, TextFormat {
|
||||||
|
font_id: appearance.code_font.clone(),
|
||||||
|
color: base_color,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ui.add(Label::new(job));
|
||||||
|
write_ins(ins, &ins_diff.kind, &ins_diff.arg_diff, base_addr, ui, appearance, ins_view_state);
|
||||||
if let Some(branch) = &ins_diff.branch_to {
|
if let Some(branch) = &ins_diff.branch_to {
|
||||||
|
let mut job = LayoutJob::default();
|
||||||
write_text(
|
write_text(
|
||||||
" ~>",
|
" ~>",
|
||||||
config.diff_colors[branch.branch_idx % config.diff_colors.len()],
|
appearance.diff_colors[branch.branch_idx % appearance.diff_colors.len()],
|
||||||
&mut job,
|
&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(
|
||||||
} else {
|
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("");
|
ui.label("");
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn asm_table_ui(
|
fn asm_table_ui(
|
||||||
table: TableBuilder<'_>,
|
table: TableBuilder<'_>,
|
||||||
left_obj: &ObjInfo,
|
left_obj: Option<&ObjInfo>,
|
||||||
right_obj: &ObjInfo,
|
right_obj: Option<&ObjInfo>,
|
||||||
fn_name: &str,
|
selected_symbol: &SymbolReference,
|
||||||
config: &ViewConfig,
|
appearance: &Appearance,
|
||||||
|
ins_view_state: &mut FunctionViewState,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
let left_symbol = find_symbol(left_obj, ".text", fn_name);
|
let left_symbol = left_obj.and_then(|obj| find_symbol(obj, selected_symbol));
|
||||||
let right_symbol = find_symbol(right_obj, ".text", fn_name);
|
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())?;
|
let instructions_len = left_symbol.or(right_symbol).map(|s| s.instructions.len())?;
|
||||||
table.body(|body| {
|
table.body(|body| {
|
||||||
body.rows(config.code_font.size, instructions_len, |row_index, mut row| {
|
body.rows(appearance.code_font.size, instructions_len, |mut row| {
|
||||||
row.col(|ui| {
|
let row_index = row.index();
|
||||||
if let Some(symbol) = left_symbol {
|
if let Some(symbol) = left_symbol {
|
||||||
asm_row_ui(ui, &symbol.instructions[row_index], symbol, config);
|
asm_col_ui(
|
||||||
|
&mut row,
|
||||||
|
&symbol.instructions[row_index],
|
||||||
|
symbol,
|
||||||
|
appearance,
|
||||||
|
ins_view_state,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
empty_col_ui(&mut row);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
row.col(|ui| {
|
|
||||||
if let Some(symbol) = right_symbol {
|
if let Some(symbol) = right_symbol {
|
||||||
asm_row_ui(ui, &symbol.instructions[row_index], symbol, config);
|
asm_col_ui(
|
||||||
|
&mut row,
|
||||||
|
&symbol.instructions[row_index],
|
||||||
|
symbol,
|
||||||
|
appearance,
|
||||||
|
ins_view_state,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
empty_col_ui(&mut row);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
Some(())
|
Some(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn function_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool {
|
pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
|
||||||
let mut rebuild = false;
|
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
|
||||||
if let (Some(result), Some(selected_symbol)) = (&view_state.build, &view_state.selected_symbol)
|
else {
|
||||||
{
|
return;
|
||||||
StripBuilder::new(ui)
|
};
|
||||||
.size(Size::exact(20.0))
|
|
||||||
.size(Size::exact(40.0))
|
// Header
|
||||||
.size(Size::remainder())
|
let available_width = ui.available_width();
|
||||||
.vertical(|mut strip| {
|
let column_width = available_width / 2.0;
|
||||||
strip.strip(|builder| {
|
ui.allocate_ui_with_layout(
|
||||||
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
|
Vec2 { x: available_width, y: 100.0 },
|
||||||
strip.cell(|ui| {
|
Layout::left_to_right(Align::Min),
|
||||||
|
|ui| {
|
||||||
|
// Left column
|
||||||
|
ui.allocate_ui_with_layout(
|
||||||
|
Vec2 { x: column_width, y: 100.0 },
|
||||||
|
Layout::top_down(Align::Min),
|
||||||
|
|ui| {
|
||||||
|
ui.set_width(column_width);
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if ui.button("Back").clicked() {
|
if ui.button("⏴ Back").clicked() {
|
||||||
view_state.current_view = View::SymbolDiff;
|
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(),
|
||||||
|
appearance.code_font.clone(),
|
||||||
|
appearance.highlight_color,
|
||||||
|
column_width,
|
||||||
|
);
|
||||||
|
job.wrap.break_anywhere = true;
|
||||||
|
job.wrap.max_rows = 1;
|
||||||
|
ui.label(job);
|
||||||
|
|
||||||
|
ui.scope(|ui| {
|
||||||
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
|
ui.label("Diff target:");
|
||||||
});
|
});
|
||||||
strip.cell(|ui| {
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Right column
|
||||||
|
ui.allocate_ui_with_layout(
|
||||||
|
Vec2 { x: column_width, y: 100.0 },
|
||||||
|
Layout::top_down(Align::Min),
|
||||||
|
|ui| {
|
||||||
|
ui.set_width(column_width);
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if ui.button("Build").clicked() {
|
if ui
|
||||||
rebuild = true;
|
.add_enabled(!state.build_running, egui::Button::new("Build"))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
state.queue_build = true;
|
||||||
}
|
}
|
||||||
ui.scope(|ui| {
|
ui.scope(|ui| {
|
||||||
ui.style_mut().override_text_style =
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
Some(egui::TextStyle::Monospace);
|
|
||||||
ui.style_mut().wrap = Some(false);
|
ui.style_mut().wrap = Some(false);
|
||||||
if view_state
|
if state.build_running {
|
||||||
.jobs
|
ui.colored_label(appearance.replace_color, "Building…");
|
||||||
.iter()
|
|
||||||
.any(|job| job.job_type == Job::ObjDiff)
|
|
||||||
{
|
|
||||||
ui.label("Building...");
|
|
||||||
} else {
|
} else {
|
||||||
ui.label("Last built:");
|
ui.label("Last built:");
|
||||||
let format =
|
let format =
|
||||||
format_description::parse("[hour]:[minute]:[second]")
|
format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||||
.unwrap();
|
|
||||||
ui.label(
|
ui.label(
|
||||||
result
|
result
|
||||||
.time
|
.time
|
||||||
.to_offset(view_state.utc_offset)
|
.to_offset(appearance.utc_offset)
|
||||||
.format(&format)
|
.format(&format)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
strip.strip(|builder| {
|
|
||||||
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
|
|
||||||
let demangled = demangle(selected_symbol, &Default::default());
|
|
||||||
strip.cell(|ui| {
|
|
||||||
ui.scope(|ui| {
|
ui.scope(|ui| {
|
||||||
ui.style_mut().override_text_style =
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
Some(egui::TextStyle::Monospace);
|
|
||||||
ui.style_mut().wrap = Some(false);
|
|
||||||
ui.colored_label(
|
|
||||||
Color32::WHITE,
|
|
||||||
demangled.as_ref().unwrap_or(selected_symbol),
|
|
||||||
);
|
|
||||||
ui.label("Diff target:");
|
|
||||||
ui.separator();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
strip.cell(|ui| {
|
|
||||||
ui.scope(|ui| {
|
|
||||||
ui.style_mut().override_text_style =
|
|
||||||
Some(egui::TextStyle::Monospace);
|
|
||||||
ui.style_mut().wrap = Some(false);
|
|
||||||
if let Some(match_percent) = result
|
if let Some(match_percent) = result
|
||||||
.second_obj
|
.second_obj
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|obj| find_symbol(obj, ".text", selected_symbol))
|
.and_then(|obj| find_symbol(obj, selected_symbol))
|
||||||
.and_then(|symbol| symbol.match_percent)
|
.and_then(|symbol| symbol.match_percent)
|
||||||
{
|
{
|
||||||
ui.colored_label(
|
ui.colored_label(
|
||||||
match_color_for_symbol(match_percent),
|
match_color_for_symbol(match_percent, appearance),
|
||||||
&format!("{match_percent:.0}%"),
|
&format!("{match_percent:.0}%"),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
ui.colored_label(appearance.replace_color, "Missing");
|
||||||
}
|
}
|
||||||
ui.label("Diff base:");
|
ui.label("Diff base:");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
ui.separator();
|
ui.separator();
|
||||||
});
|
|
||||||
});
|
// Table
|
||||||
});
|
let available_height = ui.available_height();
|
||||||
});
|
|
||||||
strip.cell(|ui| {
|
|
||||||
if let (Some(left_obj), Some(right_obj)) =
|
|
||||||
(&result.first_obj, &result.second_obj)
|
|
||||||
{
|
|
||||||
let table = TableBuilder::new(ui)
|
let table = TableBuilder::new(ui)
|
||||||
.striped(false)
|
.striped(false)
|
||||||
.cell_layout(egui::Layout::left_to_right(egui::Align::Min))
|
.cell_layout(Layout::left_to_right(Align::Min))
|
||||||
.column(Size::relative(0.5))
|
.columns(Column::exact(column_width).clip(true), 2)
|
||||||
.column(Size::relative(0.5))
|
.resizable(false)
|
||||||
.resizable(false);
|
.auto_shrink([false, false])
|
||||||
|
.min_scrolled_height(available_height);
|
||||||
asm_table_ui(
|
asm_table_ui(
|
||||||
table,
|
table,
|
||||||
left_obj,
|
result.first_obj.as_ref(),
|
||||||
right_obj,
|
result.second_obj.as_ref(),
|
||||||
selected_symbol,
|
selected_symbol,
|
||||||
&view_state.view_config,
|
appearance,
|
||||||
|
&mut state.function_state,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
rebuild
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
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");
|
ui.label("Jobs");
|
||||||
|
|
||||||
let mut remove_job: Option<usize> = None;
|
let mut remove_job: Option<usize> = None;
|
||||||
for (idx, job) in view_state.jobs.iter_mut().enumerate() {
|
for job in jobs.iter_mut() {
|
||||||
if let Ok(status) = job.status.read() {
|
let Ok(status) = job.context.status.read() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
ui.group(|ui| {
|
ui.group(|ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(&status.title);
|
ui.label(&status.title);
|
||||||
@@ -15,10 +17,10 @@ pub fn jobs_ui(ui: &mut egui::Ui, view_state: &mut ViewState) {
|
|||||||
if job.handle.is_some() {
|
if job.handle.is_some() {
|
||||||
job.should_remove = true;
|
job.should_remove = true;
|
||||||
if let Err(e) = job.cancel.send(()) {
|
if let Err(e) = job.cancel.send(()) {
|
||||||
eprintln!("Failed to cancel job: {e:?}");
|
log::error!("Failed to cancel job: {e:?}");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
remove_job = Some(idx);
|
remove_job = Some(job.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -29,27 +31,28 @@ pub fn jobs_ui(ui: &mut egui::Ui, view_state: &mut ViewState) {
|
|||||||
bar.ui(ui);
|
bar.ui(ui);
|
||||||
const STATUS_LENGTH: usize = 80;
|
const STATUS_LENGTH: usize = 80;
|
||||||
if let Some(err) = &status.error {
|
if let Some(err) = &status.error {
|
||||||
let err_string = err.to_string();
|
let err_string = format!("{:#}", err);
|
||||||
ui.colored_label(
|
ui.colored_label(
|
||||||
Color32::from_rgb(255, 0, 0),
|
appearance.delete_color,
|
||||||
if err_string.len() > STATUS_LENGTH - 10 {
|
if err_string.len() > STATUS_LENGTH - 10 {
|
||||||
format!("Error: {}...", &err_string[0..STATUS_LENGTH - 10])
|
format!("Error: {}…", &err_string[0..STATUS_LENGTH - 10])
|
||||||
} else {
|
} else {
|
||||||
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
|
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
.on_hover_text_at_pointer(RichText::new(err_string).color(appearance.delete_color));
|
||||||
} else {
|
} else {
|
||||||
ui.label(if status.status.len() > STATUS_LENGTH - 3 {
|
ui.label(if status.status.len() > STATUS_LENGTH - 3 {
|
||||||
format!("{}...", &status.status[0..STATUS_LENGTH - 3])
|
format!("{}…", &status.status[0..STATUS_LENGTH - 3])
|
||||||
} else {
|
} else {
|
||||||
format!("{:width$}", &status.status, width = STATUS_LENGTH)
|
format!("{:width$}", &status.status, width = STATUS_LENGTH)
|
||||||
});
|
})
|
||||||
|
.on_hover_text_at_pointer(&status.status);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(idx) = remove_job {
|
if let Some(idx) = remove_job {
|
||||||
view_state.jobs.remove(idx);
|
jobs.remove(idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
use egui::{text::LayoutJob, Color32, FontId, TextFormat};
|
use egui::{text::LayoutJob, Color32, FontId, TextFormat};
|
||||||
|
|
||||||
|
pub(crate) mod appearance;
|
||||||
pub(crate) mod config;
|
pub(crate) mod config;
|
||||||
pub(crate) mod data_diff;
|
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 function_diff;
|
||||||
pub(crate) mod jobs;
|
pub(crate) mod jobs;
|
||||||
pub(crate) mod symbol_diff;
|
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) {
|
fn write_text(str: &str, color: Color32, job: &mut LayoutJob, font_id: FontId) {
|
||||||
job.append(str, 0.0, TextFormat { font_id, color, ..Default::default() });
|
job.append(str, 0.0, TextFormat::simple(font_id, color));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,129 @@
|
|||||||
|
use std::mem::take;
|
||||||
|
|
||||||
use egui::{
|
use egui::{
|
||||||
text::LayoutJob, CollapsingHeader, Color32, Rgba, ScrollArea, SelectableLabel, Ui, Widget,
|
text::LayoutJob, Align, CollapsingHeader, Color32, Id, Layout, OpenUrl, ScrollArea,
|
||||||
|
SelectableLabel, TextEdit, Ui, Vec2, Widget,
|
||||||
};
|
};
|
||||||
use egui_extras::{Size, StripBuilder};
|
use egui_extras::{Size, StripBuilder};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{View, ViewConfig, ViewState},
|
app::AppConfigRef,
|
||||||
jobs::objdiff::BuildStatus,
|
jobs::{
|
||||||
|
create_scratch::{start_create_scratch, CreateScratchConfig, CreateScratchResult},
|
||||||
|
objdiff::{BuildStatus, ObjDiffResult},
|
||||||
|
Job, JobQueue, JobResult,
|
||||||
|
},
|
||||||
obj::{ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags},
|
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 {
|
if match_percent == 100.0 {
|
||||||
Color32::GREEN
|
appearance.insert_color
|
||||||
} else if match_percent >= 50.0 {
|
} else if match_percent >= 50.0 {
|
||||||
Color32::LIGHT_BLUE
|
appearance.replace_color
|
||||||
} else {
|
} else {
|
||||||
Color32::RED
|
appearance.delete_color
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,88 +134,104 @@ fn symbol_context_menu_ui(ui: &mut Ui, symbol: &ObjSymbol) {
|
|||||||
|
|
||||||
if let Some(name) = &symbol.demangled_name {
|
if let Some(name) = &symbol.demangled_name {
|
||||||
if ui.button(format!("Copy \"{name}\"")).clicked() {
|
if ui.button(format!("Copy \"{name}\"")).clicked() {
|
||||||
ui.output().copied_text = name.clone();
|
ui.output_mut(|output| output.copied_text = name.clone());
|
||||||
ui.close_menu();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ui.button(format!("Copy \"{}\"", symbol.name)).clicked() {
|
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();
|
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.scope(|ui| {
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
ui.style_mut().wrap = Some(false);
|
ui.style_mut().wrap = Some(false);
|
||||||
|
|
||||||
ui.colored_label(Color32::WHITE, format!("Name: {}", symbol.name));
|
ui.colored_label(appearance.highlight_color, format!("Name: {}", symbol.name));
|
||||||
ui.colored_label(Color32::WHITE, format!("Address: {:x}", symbol.address));
|
ui.colored_label(appearance.highlight_color, format!("Address: {:x}", symbol.address));
|
||||||
if symbol.size_known {
|
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 {
|
} 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(
|
fn symbol_ui(
|
||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
symbol: &ObjSymbol,
|
symbol: &ObjSymbol,
|
||||||
section: Option<&ObjSection>,
|
section: Option<&ObjSection>,
|
||||||
highlighted_symbol: &mut Option<String>,
|
state: &mut SymbolViewState,
|
||||||
selected_symbol: &mut Option<String>,
|
appearance: &Appearance,
|
||||||
current_view: &mut View,
|
) -> Option<View> {
|
||||||
config: &ViewConfig,
|
if symbol.flags.0.contains(ObjSymbolFlags::Hidden) && !state.show_hidden_symbols {
|
||||||
) {
|
return None;
|
||||||
|
}
|
||||||
|
let mut ret = None;
|
||||||
let mut job = LayoutJob::default();
|
let mut job = LayoutJob::default();
|
||||||
let name: &str =
|
let name: &str =
|
||||||
if let Some(demangled) = &symbol.demangled_name { demangled } else { &symbol.name };
|
if let Some(demangled) = &symbol.demangled_name { demangled } else { &symbol.name };
|
||||||
let mut selected = false;
|
let mut selected = false;
|
||||||
if let Some(sym) = highlighted_symbol {
|
if let Some(sym) = &state.highlighted_symbol {
|
||||||
selected = sym == &symbol.name;
|
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) {
|
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) {
|
} 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) {
|
} 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) {
|
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 {
|
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(
|
write_text(
|
||||||
&format!("{match_percent:.0}%"),
|
&format!("{match_percent:.0}%"),
|
||||||
match_color_for_symbol(match_percent),
|
match_color_for_symbol(match_percent, appearance),
|
||||||
&mut job,
|
&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)
|
let response = SelectableLabel::new(selected, job)
|
||||||
.ui(ui)
|
.ui(ui)
|
||||||
.context_menu(|ui| symbol_context_menu_ui(ui, symbol))
|
.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 response.clicked() {
|
||||||
if let Some(section) = section {
|
if let Some(section) = section {
|
||||||
if section.kind == ObjSectionKind::Code {
|
if section.kind == ObjSectionKind::Code {
|
||||||
*selected_symbol = Some(symbol.name.clone());
|
state.selected_symbol = Some(SymbolReference {
|
||||||
*current_view = View::FunctionDiff;
|
symbol_name: symbol.name.clone(),
|
||||||
|
section_name: section.name.clone(),
|
||||||
|
});
|
||||||
|
ret = Some(View::FunctionDiff);
|
||||||
} else if section.kind == ObjSectionKind::Data {
|
} else if section.kind == ObjSectionKind::Data {
|
||||||
*selected_symbol = Some(section.name.clone());
|
state.selected_symbol = Some(SymbolReference {
|
||||||
*current_view = View::DataDiff;
|
symbol_name: section.name.clone(),
|
||||||
|
section_name: section.name.clone(),
|
||||||
|
});
|
||||||
|
ret = Some(View::DataDiff);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if response.hovered() {
|
} 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 {
|
fn symbol_matches_search(symbol: &ObjSymbol, search_str: &str) -> bool {
|
||||||
@@ -121,20 +244,15 @@ fn symbol_matches_search(symbol: &ObjSymbol, search_str: &str) -> bool {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[must_use]
|
||||||
fn symbol_list_ui(
|
fn symbol_list_ui(
|
||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
obj: &ObjInfo,
|
obj: &ObjInfo,
|
||||||
highlighted_symbol: &mut Option<String>,
|
state: &mut SymbolViewState,
|
||||||
selected_symbol: &mut Option<String>,
|
lower_search: &str,
|
||||||
current_view: &mut View,
|
appearance: &Appearance,
|
||||||
reverse_function_order: bool,
|
) -> Option<View> {
|
||||||
search: &mut String,
|
let mut ret = None;
|
||||||
config: &ViewConfig,
|
|
||||||
) {
|
|
||||||
ui.text_edit_singleline(search);
|
|
||||||
let lower_search = search.to_ascii_lowercase();
|
|
||||||
|
|
||||||
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
|
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
|
||||||
ui.scope(|ui| {
|
ui.scope(|ui| {
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
@@ -143,160 +261,193 @@ fn symbol_list_ui(
|
|||||||
if !obj.common.is_empty() {
|
if !obj.common.is_empty() {
|
||||||
CollapsingHeader::new(".comm").default_open(true).show(ui, |ui| {
|
CollapsingHeader::new(".comm").default_open(true).show(ui, |ui| {
|
||||||
for symbol in &obj.common {
|
for symbol in &obj.common {
|
||||||
symbol_ui(
|
ret = ret.or(symbol_ui(ui, symbol, None, state, appearance));
|
||||||
ui,
|
|
||||||
symbol,
|
|
||||||
None,
|
|
||||||
highlighted_symbol,
|
|
||||||
selected_symbol,
|
|
||||||
current_view,
|
|
||||||
config,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for section in &obj.sections {
|
for section in &obj.sections {
|
||||||
CollapsingHeader::new(format!("{} ({:x})", section.name, section.size))
|
CollapsingHeader::new(format!("{} ({:x})", section.name, section.size))
|
||||||
|
.id_source(Id::new(section.name.clone()).with(section.index))
|
||||||
.default_open(true)
|
.default_open(true)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
if section.name == ".text" && reverse_function_order {
|
if section.kind == ObjSectionKind::Code && state.reverse_fn_order {
|
||||||
for symbol in section.symbols.iter().rev() {
|
for symbol in section.symbols.iter().rev() {
|
||||||
if !symbol_matches_search(symbol, &lower_search) {
|
if !symbol_matches_search(symbol, lower_search) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
symbol_ui(
|
ret =
|
||||||
ui,
|
ret.or(symbol_ui(ui, symbol, Some(section), state, appearance));
|
||||||
symbol,
|
|
||||||
Some(section),
|
|
||||||
highlighted_symbol,
|
|
||||||
selected_symbol,
|
|
||||||
current_view,
|
|
||||||
config,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for symbol in §ion.symbols {
|
for symbol in §ion.symbols {
|
||||||
if !symbol_matches_search(symbol, &lower_search) {
|
if !symbol_matches_search(symbol, lower_search) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
symbol_ui(
|
ret =
|
||||||
ui,
|
ret.or(symbol_ui(ui, symbol, Some(section), state, appearance));
|
||||||
symbol,
|
|
||||||
Some(section),
|
|
||||||
highlighted_symbol,
|
|
||||||
selected_symbol,
|
|
||||||
current_view,
|
|
||||||
config,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
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| {
|
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.scope(|ui| {
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
ui.style_mut().wrap = Some(false);
|
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) {
|
fn missing_obj_ui(ui: &mut Ui, appearance: &Appearance) {
|
||||||
if 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,
|
|
||||||
) {
|
|
||||||
StripBuilder::new(ui).size(Size::exact(40.0)).size(Size::remainder()).vertical(
|
|
||||||
|mut strip| {
|
|
||||||
strip.strip(|builder| {
|
|
||||||
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
|
|
||||||
strip.cell(|ui| {
|
|
||||||
ui.scope(|ui| {
|
ui.scope(|ui| {
|
||||||
ui.style_mut().override_text_style =
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Header
|
||||||
|
let available_width = ui.available_width();
|
||||||
|
let column_width = available_width / 2.0;
|
||||||
|
ui.allocate_ui_with_layout(
|
||||||
|
Vec2 { x: available_width, y: 100.0 },
|
||||||
|
Layout::left_to_right(Align::Min),
|
||||||
|
|ui| {
|
||||||
|
// Left column
|
||||||
|
ui.allocate_ui_with_layout(
|
||||||
|
Vec2 { x: column_width, y: 100.0 },
|
||||||
|
Layout::top_down(Align::Min),
|
||||||
|
|ui| {
|
||||||
|
ui.set_width(column_width);
|
||||||
|
|
||||||
|
ui.scope(|ui| {
|
||||||
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
ui.style_mut().wrap = Some(false);
|
ui.style_mut().wrap = Some(false);
|
||||||
|
|
||||||
ui.label("Build target:");
|
ui.label("Build target:");
|
||||||
if result.first_status.success {
|
if result.first_status.success {
|
||||||
ui.label("OK");
|
if result.first_obj.is_none() {
|
||||||
|
ui.colored_label(appearance.replace_color, "Missing");
|
||||||
} else {
|
} else {
|
||||||
ui.colored_label(Rgba::from_rgb(1.0, 0.0, 0.0), "Fail");
|
ui.label("OK");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ui.colored_label(appearance.delete_color, "Fail");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ui.separator();
|
|
||||||
});
|
TextEdit::singleline(search).hint_text("Filter symbols").ui(ui);
|
||||||
strip.cell(|ui| {
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Right column
|
||||||
|
ui.allocate_ui_with_layout(
|
||||||
|
Vec2 { x: column_width, y: 100.0 },
|
||||||
|
Layout::top_down(Align::Min),
|
||||||
|
|ui| {
|
||||||
|
ui.set_width(column_width);
|
||||||
|
|
||||||
ui.scope(|ui| {
|
ui.scope(|ui| {
|
||||||
ui.style_mut().override_text_style =
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
Some(egui::TextStyle::Monospace);
|
|
||||||
ui.style_mut().wrap = Some(false);
|
ui.style_mut().wrap = Some(false);
|
||||||
|
|
||||||
ui.label("Build base:");
|
ui.label("Build base:");
|
||||||
if result.second_status.success {
|
if result.second_status.success {
|
||||||
ui.label("OK");
|
if result.second_obj.is_none() {
|
||||||
|
ui.colored_label(appearance.replace_color, "Missing");
|
||||||
} else {
|
} else {
|
||||||
ui.colored_label(Rgba::from_rgb(1.0, 0.0, 0.0), "Fail");
|
ui.label("OK");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ui.colored_label(appearance.delete_color, "Fail");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
|
||||||
|
state.queue_build = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
ui.separator();
|
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| {
|
strip.strip(|builder| {
|
||||||
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
|
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
|
||||||
strip.cell(|ui| {
|
strip.cell(|ui| {
|
||||||
|
ui.push_id("left", |ui| {
|
||||||
if result.first_status.success {
|
if result.first_status.success {
|
||||||
if let Some(obj) = &result.first_obj {
|
if let Some(obj) = &result.first_obj {
|
||||||
ui.push_id("left", |ui| {
|
ret = ret.or(symbol_list_ui(
|
||||||
symbol_list_ui(
|
|
||||||
ui,
|
ui,
|
||||||
obj,
|
obj,
|
||||||
highlighted_symbol,
|
symbol_state,
|
||||||
selected_symbol,
|
&lower_search,
|
||||||
current_view,
|
appearance,
|
||||||
view_state.reverse_fn_order,
|
));
|
||||||
search,
|
} else {
|
||||||
&view_state.view_config,
|
missing_obj_ui(ui, appearance);
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
build_log_ui(ui, &result.first_status);
|
build_log_ui(ui, &result.first_status, appearance);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
strip.cell(|ui| {
|
strip.cell(|ui| {
|
||||||
|
ui.push_id("right", |ui| {
|
||||||
if result.second_status.success {
|
if result.second_status.success {
|
||||||
if let Some(obj) = &result.second_obj {
|
if let Some(obj) = &result.second_obj {
|
||||||
ui.push_id("right", |ui| {
|
ret = ret.or(symbol_list_ui(
|
||||||
symbol_list_ui(
|
|
||||||
ui,
|
ui,
|
||||||
obj,
|
obj,
|
||||||
highlighted_symbol,
|
symbol_state,
|
||||||
selected_symbol,
|
&lower_search,
|
||||||
current_view,
|
appearance,
|
||||||
view_state.reverse_fn_order,
|
));
|
||||||
search,
|
} else {
|
||||||
&view_state.view_config,
|
missing_obj_ui(ui, appearance);
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
build_log_ui(ui, &result.second_status);
|
build_log_ui(ui, &result.second_status, appearance);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
});
|
||||||
);
|
});
|
||||||
|
|
||||||
|
if let Some(view) = ret {
|
||||||
|
*current_view = view;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||