Compare commits
245 Commits
v0.2.0
...
221c1a8cda
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
221c1a8cda | ||
| 7aa878b48e | |||
|
|
0b7afa9f1e | ||
|
|
5841d4db8a | ||
|
|
7a360d4a99 | ||
|
|
40c97a1bc3 | ||
| a119d9a6dd | |||
|
|
ebf653816a | ||
| 424434edd6 | |||
| 7f14b684bf | |||
| c5da7f7dd5 | |||
| 2fd655850a | |||
| 79bd7317c1 | |||
| 21f8f2407c | |||
| d2b7a9ef25 | |||
| 2cf9cf24d6 | |||
|
|
5ef3416457 | ||
|
|
6ff8d002f7 | ||
| 9ca157d717 | |||
|
|
67b63311fc | ||
| 72ea1c8911 | |||
| d4a540857d | |||
| 676488433f | |||
| 83de98b5ee | |||
| c1ba4e91d1 | |||
| 575900024d | |||
| cbe299e859 | |||
| 741d93e211 | |||
| 603dbd6882 | |||
| 6fb0a63de2 | |||
| ab2e84a2c6 | |||
| 9596051cb4 | |||
| a5d9d8282e | |||
|
|
3287a0f65c | ||
|
|
fab9c62dfb | ||
| 08cd768260 | |||
| 8acaaf528c | |||
| 6e881a74e1 | |||
| cc1bc44e69 | |||
| c7b85518ab | |||
| bb039a1445 | |||
| 8fc142d316 | |||
| b0123b3f83 | |||
| 2ec17aee9b | |||
| ec9731e1e5 | |||
|
|
a06382c27e | ||
| e013638c5a | |||
| 70ab82f1f7 | |||
| c5896689cf | |||
| 67719dd93e | |||
| 258e141017 | |||
| dbdda55065 | |||
|
|
a43320af1f | ||
|
|
35bbd40f5d | ||
|
|
c1cb4b0b19 | ||
| 2379853faa | |||
| 5e1aff180f | |||
| 3846a7d315 | |||
| dcf209aac5 | |||
| c7e6394628 | |||
| 235dc7f517 | |||
|
|
199c07e975 | ||
| 56a5a61825 | |||
| 3d2236de82 | |||
| bcc5871cd8 | |||
|
|
7d0d7df54c | ||
| 0221a2d54d | |||
|
|
bc687173c0 | ||
| e1ae369d17 | |||
| ce05d6d6c0 | |||
| c16a926d9b | |||
|
|
a32d99923c | ||
| 68606dfdcb | |||
| b4650b660a | |||
| 195379968c | |||
|
|
3bd8aaee41 | ||
| 1f4175dc21 | |||
| 0fccae1049 | |||
| 8250d26b77 | |||
| fd555a6e0f | |||
| 3710b6a91e | |||
| faebddbc5e | |||
| a733a950a3 | |||
| cad9b70632 | |||
| cf937b0be9 | |||
| 23b6d33a98 | |||
| f17ee83622 | |||
| 615ec4c50a | |||
| 2cc10b0d06 | |||
| 8091941448 | |||
| de74dfdba7 | |||
| 177bd5e895 | |||
| e1ccee1e73 | |||
| 952b6a63c3 | |||
|
|
09cc9952df | ||
| fc598af329 | |||
| 871407622d | |||
| e3fff7b0dc | |||
|
|
75b0e7d9e5 | ||
|
|
9f71ce9fea | ||
|
|
d9fb48853e | ||
| 233839346a | |||
| 95615c2ec5 | |||
|
|
97981160f4 | ||
|
|
1fd901a863 | ||
| 759d55994a | |||
| 9710ccc38a | |||
| 79cd460333 | |||
|
|
a5a6a3928e | ||
| fc54e93681 | |||
| c9b11db2fa | |||
|
|
b991960080 | ||
| 425dc8546b | |||
| 9e04357d9f | |||
| 6037c12ad0 | |||
| b15f643713 | |||
| 3f82c1a50f | |||
| 0ea6242669 | |||
| 0c20a0d9cd | |||
| f30b3cfae2 | |||
| 9e57a66a05 | |||
| e254af5acf | |||
|
|
320efcb8cb | ||
| 7148b51fe0 | |||
| dc0c170db9 | |||
|
|
31e9c14681 | ||
| 94f1f07b00 | |||
|
|
f5b5a612fc | ||
| 22a24f37f5 | |||
|
|
854dc9e4f5 | ||
| 5bfaaaaf65 | |||
|
|
8b36fa4fc6 | ||
| 660e6c879e | |||
|
|
db726a68a6 | ||
|
|
b457453639 | ||
| 3e5008524e | |||
| 2c46286aff | |||
| 106652ae7d | |||
| 30d14870ef | |||
| e7991cb28d | |||
| 4dfc28fc68 | |||
| 3c74b89f15 | |||
| 1343f4fd2b | |||
| 9df98f263e | |||
| bbe49eb8b4 | |||
| aecb078b2a | |||
| a5668b484b | |||
| ef41e393d4 | |||
| 20e42a499a | |||
| c39795ae2c | |||
| 49ee9b44aa | |||
|
|
341c1d4b33 | ||
|
|
9f4a1e86cd | ||
|
|
ed5d092b11 | ||
|
|
023dd7a55b | ||
| 3b1249e1ab | |||
| cb13638e07 | |||
|
|
37ddbb7f4a | ||
|
|
b80d361e91 | ||
|
|
fd27f4d0cd | ||
|
|
5cfd04fd4f | ||
| 5b9ac93c08 | |||
| 39a13f4d36 | |||
|
|
28348606bf | ||
| fb24063c54 | |||
| cff6a230a3 | |||
| 9a7d2bcebf | |||
| 4eba5f71b0 | |||
| 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 |
5
.cargo/config.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
linker = "rust-lld"
|
||||
|
||||
[target.aarch64-pc-windows-msvc]
|
||||
linker = "rust-lld"
|
||||
204
.github/workflows/build.yaml
vendored
@@ -9,8 +9,9 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_BIN_NAME: objdiff
|
||||
BUILD_PROFILE: release-lto
|
||||
CARGO_TARGET_DIR: target
|
||||
CARGO_INCREMENTAL: 0
|
||||
|
||||
jobs:
|
||||
check:
|
||||
@@ -20,17 +21,37 @@ jobs:
|
||||
RUSTFLAGS: -D warnings
|
||||
steps:
|
||||
- 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
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
components: clippy
|
||||
- name: Cache Rust workspace
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Cargo check
|
||||
run: cargo check --all-features
|
||||
run: cargo check --all-features --all-targets
|
||||
- name: Cargo clippy
|
||||
run: cargo clippy --all-features
|
||||
run: cargo clippy --all-features --all-targets
|
||||
|
||||
fmt:
|
||||
name: Format
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Rust toolchain
|
||||
# We use nightly options in rustfmt.toml
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: rustfmt
|
||||
- name: Cargo fmt
|
||||
run: cargo fmt --all --check
|
||||
|
||||
deny:
|
||||
name: Deny
|
||||
@@ -43,13 +64,14 @@ jobs:
|
||||
# Prevent new advisories from failing CI
|
||||
continue-on-error: ${{ matrix.checks == 'advisories' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||
with:
|
||||
command: check ${{ matrix.checks }}
|
||||
|
||||
test:
|
||||
name: Test
|
||||
if: 'false' # No tests yet
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
@@ -58,16 +80,102 @@ jobs:
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
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
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Cache Rust workspace
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Cargo test
|
||||
run: cargo test --release --all-features
|
||||
|
||||
build:
|
||||
name: Build
|
||||
build-cli:
|
||||
name: Build objdiff-cli
|
||||
env:
|
||||
CARGO_BIN_NAME: objdiff-cli
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- platform: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
name: linux-x86_64
|
||||
build: zigbuild
|
||||
features: default
|
||||
- platform: ubuntu-latest
|
||||
target: i686-unknown-linux-musl
|
||||
name: linux-i686
|
||||
build: zigbuild
|
||||
features: default
|
||||
- platform: ubuntu-latest
|
||||
target: aarch64-unknown-linux-musl
|
||||
name: linux-aarch64
|
||||
build: zigbuild
|
||||
features: default
|
||||
- platform: windows-latest
|
||||
target: i686-pc-windows-msvc
|
||||
name: windows-x86
|
||||
build: build
|
||||
features: default
|
||||
- platform: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
name: windows-x86_64
|
||||
build: build
|
||||
features: default
|
||||
- platform: windows-latest
|
||||
target: aarch64-pc-windows-msvc
|
||||
name: windows-arm64
|
||||
build: build
|
||||
features: default
|
||||
- platform: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
name: macos-x86_64
|
||||
build: build
|
||||
features: default
|
||||
- platform: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
name: macos-arm64
|
||||
build: build
|
||||
features: default
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install cargo-zigbuild
|
||||
if: matrix.build == 'zigbuild'
|
||||
run: |
|
||||
python3 -m venv .venv
|
||||
. .venv/bin/activate
|
||||
echo PATH=$PATH >> $GITHUB_ENV
|
||||
pip install ziglang==0.13.0 cargo-zigbuild==0.19.1
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- name: Cache Rust workspace
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: ${{ matrix.target }}
|
||||
- name: Cargo build
|
||||
run: >
|
||||
cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
|
||||
--bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }}
|
||||
path: |
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
|
||||
if-no-files-found: error
|
||||
|
||||
build-gui:
|
||||
name: Build objdiff-gui
|
||||
env:
|
||||
CARGO_BIN_NAME: objdiff
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -75,59 +183,101 @@ jobs:
|
||||
target: x86_64-unknown-linux-gnu
|
||||
name: linux-x86_64
|
||||
packages: libgtk-3-dev
|
||||
features: default
|
||||
- platform: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
name: windows-x86_64
|
||||
features: default
|
||||
- platform: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
name: macos-x86_64
|
||||
features: default
|
||||
- platform: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
name: macos-arm64
|
||||
features: default
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
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
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- name: Cargo build
|
||||
run: cargo build --release --all-features --target ${{ matrix.target }} --bin ${{ env.CARGO_BIN_NAME }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Cache Rust workspace
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
name: ${{ matrix.name }}
|
||||
key: ${{ matrix.target }}
|
||||
- name: Cargo build
|
||||
run: >
|
||||
cargo build --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
|
||||
--bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }}
|
||||
path: |
|
||||
${{ env.CARGO_TARGET_DIR }}/release/${{ env.CARGO_BIN_NAME }}
|
||||
${{ env.CARGO_TARGET_DIR }}/release/${{ env.CARGO_BIN_NAME }}.exe
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release/${{ env.CARGO_BIN_NAME }}
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release/${{ env.CARGO_BIN_NAME }}.exe
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
name: Release
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ build ]
|
||||
needs: [ build-cli, build-gui ]
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Check git tag against Cargo version
|
||||
shell: bash
|
||||
run: |
|
||||
set -eou pipefail
|
||||
tag='${{github.ref}}'
|
||||
tag="${tag#refs/tags/}"
|
||||
version=$(grep '^version' Cargo.toml | head -1 | awk -F' = ' '{print $2}' | tr -d '"')
|
||||
version="v$version"
|
||||
if [ "$tag" != "$version" ]; then
|
||||
echo "::error::Git tag doesn't match the Cargo version! ($tag != $version)"
|
||||
exit 1
|
||||
fi
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
- name: Rename artifacts
|
||||
working-directory: artifacts
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir ../out
|
||||
for i in */*/release/$CARGO_BIN_NAME*; do
|
||||
mv "$i" "../out/$(sed -E "s/([^/]+)\/[^/]+\/release\/($CARGO_BIN_NAME)/\2-\1/" <<< "$i")"
|
||||
for dir in */; do
|
||||
for file in "$dir"*; do
|
||||
base=$(basename "$file")
|
||||
name="${base%.*}"
|
||||
ext="${base##*.}"
|
||||
if [ "$ext" = "$base" ]; then
|
||||
ext=""
|
||||
else
|
||||
ext=".$ext"
|
||||
fi
|
||||
arch="${dir%/}" # remove trailing slash
|
||||
arch="${arch##"$name-"}" # remove bin name
|
||||
dst="../out/${name}-${arch}${ext}"
|
||||
mv "$file" "$dst"
|
||||
done
|
||||
done
|
||||
ls -R ../out
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: out/*
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
|
||||
5
.gitignore
vendored
@@ -3,10 +3,6 @@ target/
|
||||
**/*.rs.bk
|
||||
generated/
|
||||
|
||||
# cargo-mobile
|
||||
.cargo/
|
||||
/gen
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
@@ -22,3 +18,4 @@ android.keystore
|
||||
*.frag
|
||||
*.vert
|
||||
*.metal
|
||||
.vscode/
|
||||
|
||||
5318
Cargo.lock
generated
77
Cargo.toml
@@ -1,62 +1,21 @@
|
||||
[package]
|
||||
name = "objdiff"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.62"
|
||||
[workspace]
|
||||
members = [
|
||||
"objdiff-cli",
|
||||
"objdiff-core",
|
||||
"objdiff-gui",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[profile.release-lto]
|
||||
inherits = "release"
|
||||
lto = "fat"
|
||||
strip = "debuginfo"
|
||||
codegen-units = 1
|
||||
|
||||
[workspace.package]
|
||||
version = "2.4.0"
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/encounter/objdiff"
|
||||
readme = "README.md"
|
||||
description = """
|
||||
A local diffing tool for decompilation projects.
|
||||
"""
|
||||
publish = false
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
strip = "debuginfo"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
cfg-if = "1.0.0"
|
||||
const_format = "0.2.30"
|
||||
cwdemangle = { git = "https://github.com/encounter/cwdemangle", rev = "286f3d1d29ee2457db89043782725631845c3e4c" }
|
||||
eframe = { version = "0.19.0", features = ["persistence"] } # , "wgpu"
|
||||
egui = "0.19.0"
|
||||
egui_extras = "0.19.0"
|
||||
flagset = "0.4.3"
|
||||
log = "0.4.17"
|
||||
memmap2 = "0.5.8"
|
||||
notify = "5.0.0"
|
||||
object = { version = "0.30.0", features = ["read_core", "std", "elf"], default-features = false }
|
||||
ppc750cl = { git = "https://github.com/encounter/ppc750cl", rev = "aa631a33de7882c679afca89350898b87cb3ba3f" }
|
||||
rabbitizer = { git = "https://github.com/encounter/rabbitizer-rs", rev = "10c279b2ef251c62885b1dcdcfe740b0db8e9956" }
|
||||
rfd = { version = "0.10.0" } # , default-features = false, features = ['xdg-portal']
|
||||
self_update = "0.32.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
thiserror = "1.0.37"
|
||||
time = { version = "0.3.17", features = ["formatting", "local-offset"] }
|
||||
toml = "0.5.9"
|
||||
twox-hash = "1.6.3"
|
||||
tempfile = "3.3.0"
|
||||
reqwest = "0.11.13"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
path-slash = "0.2.1"
|
||||
winapi = "0.3.9"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
exec = "0.3.1"
|
||||
|
||||
# native:
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tracing-subscriber = "0.3"
|
||||
|
||||
# web:
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
tracing-wasm = "0.2"
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = "1.0.66"
|
||||
vergen = { version = "7.4.3", features = ["build", "cargo", "git"], default-features = false }
|
||||
rust-version = "1.74"
|
||||
|
||||
180
README.md
@@ -3,26 +3,186 @@
|
||||
[Build Status]: https://github.com/encounter/objdiff/actions/workflows/build.yaml/badge.svg
|
||||
[actions]: https://github.com/encounter/objdiff/actions
|
||||
|
||||
A local diffing tool for decompilation projects.
|
||||
A local diffing tool for decompilation projects. Inspired by [decomp.me](https://decomp.me) and [asm-differ](https://github.com/simonlindholm/asm-differ).
|
||||
|
||||
Currently supports:
|
||||
- PowerPC 750CL (GameCube & Wii)
|
||||
- MIPS (Nintendo 64)
|
||||
Features:
|
||||
|
||||
- Compare entire object files: functions and data.
|
||||
- Built-in symbol demangling for C++. (CodeWarrior, Itanium & MSVC)
|
||||
- Automatic rebuild on source file changes.
|
||||
- Project integration via [configuration file](#configuration).
|
||||
- Search and filter all of a project's objects and quickly switch.
|
||||
- Click to highlight all instances of values and registers.
|
||||
|
||||
Supports:
|
||||
|
||||
- PowerPC 750CL (GameCube, Wii)
|
||||
- MIPS (N64, PS1, PS2, PSP)
|
||||
- x86 (COFF only at the moment)
|
||||
- ARM (GBA, DS, 3DS)
|
||||
- ARM64 (Switch, experimental)
|
||||
|
||||
See [Usage](#usage) for more information.
|
||||
|
||||
## Downloads
|
||||
|
||||
To build from source, see [Building](#building).
|
||||
|
||||
### GUI
|
||||
|
||||
- [Windows (x86_64)](https://github.com/encounter/objdiff/releases/latest/download/objdiff-windows-x86_64.exe)
|
||||
- [Linux (x86_64)](https://github.com/encounter/objdiff/releases/latest/download/objdiff-linux-x86_64)
|
||||
- [macOS (arm64)](https://github.com/encounter/objdiff/releases/latest/download/objdiff-macos-arm64)
|
||||
- [macOS (x86_64)](https://github.com/encounter/objdiff/releases/latest/download/objdiff-macos-x86_64)
|
||||
|
||||
For Linux and macOS, run `chmod +x objdiff-*` to make the binary executable.
|
||||
|
||||
### CLI
|
||||
|
||||
CLI binaries can be found on the [releases page](https://github.com/encounter/objdiff/releases).
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||
|
||||
### 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` 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.
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/encounter/objdiff/main/config.schema.json",
|
||||
"custom_make": "ninja",
|
||||
"custom_args": [
|
||||
"-d",
|
||||
"keeprsp"
|
||||
],
|
||||
"build_target": false,
|
||||
"build_base": true,
|
||||
"watch_patterns": [
|
||||
"*.c",
|
||||
"*.cp",
|
||||
"*.cpp",
|
||||
"*.cxx",
|
||||
"*.h",
|
||||
"*.hp",
|
||||
"*.hpp",
|
||||
"*.hxx",
|
||||
"*.s",
|
||||
"*.S",
|
||||
"*.asm",
|
||||
"*.inc",
|
||||
"*.py",
|
||||
"*.yml",
|
||||
"*.txt",
|
||||
"*.json"
|
||||
],
|
||||
"units": [
|
||||
{
|
||||
"name": "main/MetroTRK/mslsupp",
|
||||
"target_path": "build/asm/MetroTRK/mslsupp.o",
|
||||
"base_path": "build/src/MetroTRK/mslsupp.o",
|
||||
"metadata": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Schema
|
||||
|
||||
View [config.schema.json](config.schema.json) for all available options. The below list is a summary of the most important options.
|
||||
|
||||
`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.
|
||||
The build command will be `[custom_make] [custom_args] path/to/object.o`.
|
||||
|
||||
`custom_args` _(optional)_: Additional arguments to pass to the build command prior to the object path.
|
||||
|
||||
`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.
|
||||
|
||||
`build_base`: If true, objdiff will tell the build system to build the base objects before diffing (e.g. `make path/to/base.o`).
|
||||
It's unlikely you'll want to disable this, unless you're using an external tool to rebuild the base object on source file changes.
|
||||
|
||||
`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.
|
||||
|
||||
`units` _(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.
|
||||
>
|
||||
> `target_path`: Path to the "target" or "expected" object from the project root.
|
||||
> This object is the **intended result** of the match.
|
||||
>
|
||||
> `base_path`: Path to the "base" or "actual" object from the project root.
|
||||
> This object is built from the **current source code**.
|
||||
>
|
||||
> `metadata.auto_generated` _(optional)_: Hides the object from the object list, but still includes it in reports.
|
||||
>
|
||||
> `metadata.complete` _(optional)_: Marks the object as "complete" (or "linked") in the object list.
|
||||
> This is useful for marking objects that are fully decompiled. A value of `false` will mark the object as "incomplete".
|
||||
|
||||
## Building
|
||||
|
||||
Install Rust via [rustup](https://rustup.rs).
|
||||
|
||||
```shell
|
||||
$ git clone https://github.com/encounter/objdiff.git
|
||||
$ cd objdiff
|
||||
$ cargo run --release
|
||||
```
|
||||
|
||||
Or using `cargo install`.
|
||||
|
||||
```shell
|
||||
$ cargo install --locked --git https://github.com/encounter/objdiff.git objdiff-gui objdiff-cli
|
||||
```
|
||||
|
||||
The binaries will be installed to `~/.cargo/bin` as `objdiff` and `objdiff-cli`.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of
|
||||
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
||||
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>)
|
||||
- MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
|
||||
|
||||
at your option.
|
||||
|
||||
### Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any
|
||||
additional terms or conditions.
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as
|
||||
defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
|
||||
|
||||
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 144 KiB |
4
build.rs
@@ -1,4 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use vergen::{vergen, Config};
|
||||
|
||||
fn main() -> Result<()> { vergen(Config::default()) }
|
||||
228
config.schema.json
Normal file
@@ -0,0 +1,228 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft-07/schema",
|
||||
"$id": "https://raw.githubusercontent.com/encounter/objdiff/main/config.schema.json",
|
||||
"title": "objdiff configuration",
|
||||
"description": "Configuration file for objdiff",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"min_version": {
|
||||
"type": "string",
|
||||
"description": "Minimum version of objdiff required to load this configuration file.",
|
||||
"examples": [
|
||||
"1.0.0",
|
||||
"2.0.0-beta.1"
|
||||
]
|
||||
},
|
||||
"custom_make": {
|
||||
"type": "string",
|
||||
"description": "By default, objdiff will use make to build the project.\nIf the project uses a different build system (e.g. ninja), specify it here.\nThe build command will be `[custom_make] [custom_args] path/to/object.o`.",
|
||||
"examples": [
|
||||
"make",
|
||||
"ninja"
|
||||
],
|
||||
"default": "make"
|
||||
},
|
||||
"custom_args": {
|
||||
"type": "array",
|
||||
"description": "Additional arguments to pass to the build command prior to the object path.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"target_dir": {
|
||||
"type": "string",
|
||||
"description": "Relative from the root of the project, this where the \"target\" or \"expected\" objects are located.\nThese are the intended result of the match.",
|
||||
"deprecated": true
|
||||
},
|
||||
"base_dir": {
|
||||
"type": "string",
|
||||
"description": "Relative from the root of the project, this is where the \"base\" or \"actual\" objects are located.\nThese are objects built from the current source code.",
|
||||
"deprecated": true
|
||||
},
|
||||
"build_target": {
|
||||
"type": "boolean",
|
||||
"description": "If true, objdiff will tell the build system to build the target objects before diffing (e.g. `make path/to/target.o`).\nThis is useful if the target objects are not built by default or can change based on project configuration or edits to assembly files.\nRequires the build system to be configured properly.",
|
||||
"default": false
|
||||
},
|
||||
"build_base": {
|
||||
"type": "boolean",
|
||||
"description": "If true, objdiff will tell the build system to build the base objects before diffing (e.g. `make path/to/base.o`).\nIt's unlikely you'll want to disable this, unless you're using an external tool to rebuild the base object on source file changes.",
|
||||
"default": true
|
||||
},
|
||||
"watch_patterns": {
|
||||
"type": "array",
|
||||
"description": "List of glob patterns to watch for changes in the project.\nIf any of these files change, objdiff will automatically rebuild the objects and re-compare them.\nSupported syntax: https://docs.rs/globset/latest/globset/#syntax",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": [
|
||||
"*.c",
|
||||
"*.cp",
|
||||
"*.cpp",
|
||||
"*.cxx",
|
||||
"*.h",
|
||||
"*.hp",
|
||||
"*.hpp",
|
||||
"*.hxx",
|
||||
"*.s",
|
||||
"*.S",
|
||||
"*.asm",
|
||||
"*.inc",
|
||||
"*.py",
|
||||
"*.yml",
|
||||
"*.txt",
|
||||
"*.json"
|
||||
]
|
||||
},
|
||||
"objects": {
|
||||
"type": "array",
|
||||
"description": "Use units instead.",
|
||||
"deprecated": true,
|
||||
"items": {
|
||||
"$ref": "#/$defs/unit"
|
||||
}
|
||||
},
|
||||
"units": {
|
||||
"type": "array",
|
||||
"description": "If specified, objdiff will display a list of objects in the sidebar for easy navigation.",
|
||||
"items": {
|
||||
"$ref": "#/$defs/unit"
|
||||
}
|
||||
},
|
||||
"progress_categories": {
|
||||
"type": "array",
|
||||
"description": "Progress categories used for objdiff-cli report.",
|
||||
"items": {
|
||||
"$ref": "#/$defs/progress_category"
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"unit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the object in the UI. If not specified, the object's path will be used."
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Relative path to the object from the target_dir and base_dir.\nRequires target_dir and base_dir to be specified.",
|
||||
"deprecated": true
|
||||
},
|
||||
"target_path": {
|
||||
"type": "string",
|
||||
"description": "Path to the target object from the project root.\nRequired if path is not specified."
|
||||
},
|
||||
"base_path": {
|
||||
"type": "string",
|
||||
"description": "Path to the base object from the project root.\nRequired if path is not specified."
|
||||
},
|
||||
"reverse_fn_order": {
|
||||
"type": "boolean",
|
||||
"description": "Displays function symbols in reversed order.\nUsed to support MWCC's -inline deferred option, which reverses the order of functions in the object file.",
|
||||
"deprecated": true
|
||||
},
|
||||
"complete": {
|
||||
"type": "boolean",
|
||||
"description": "Marks the object as \"complete\" (or \"linked\") in the object list.\nThis is useful for marking objects that are fully decompiled. A value of `false` will mark the object as \"incomplete\".",
|
||||
"deprecated": true
|
||||
},
|
||||
"scratch": {
|
||||
"ref": "#/$defs/scratch"
|
||||
},
|
||||
"metadata": {
|
||||
"ref": "#/$defs/metadata"
|
||||
},
|
||||
"symbol_mappings": {
|
||||
"type": "object",
|
||||
"description": "Manual symbol mappings from target to base.",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scratch": {
|
||||
"type": "object",
|
||||
"description": "If present, objdiff will display a button to create a decomp.me scratch.",
|
||||
"properties": {
|
||||
"platform": {
|
||||
"type": "string",
|
||||
"description": "The decomp.me platform ID to use for the scratch.",
|
||||
"examples": [
|
||||
"gc_wii",
|
||||
"n64"
|
||||
]
|
||||
},
|
||||
"compiler": {
|
||||
"type": "string",
|
||||
"description": "The decomp.me compiler ID to use for the scratch.",
|
||||
"examples": [
|
||||
"mwcc_242_81",
|
||||
"ido7.1"
|
||||
]
|
||||
},
|
||||
"c_flags": {
|
||||
"type": "string",
|
||||
"description": "C flags to use for the scratch. Exclude any include paths."
|
||||
},
|
||||
"ctx_path": {
|
||||
"type": "string",
|
||||
"description": "Path to the context file to use for the scratch."
|
||||
},
|
||||
"build_ctx": {
|
||||
"type": "boolean",
|
||||
"description": "If true, objdiff will run the build command with the context file as an argument to generate it.",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"platform",
|
||||
"compiler"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"complete": {
|
||||
"type": "boolean",
|
||||
"description": "Marks the object as \"complete\" (or \"linked\") in the object list.\nThis is useful for marking objects that are fully decompiled. A value of `false` will mark the object as \"incomplete\"."
|
||||
},
|
||||
"reverse_fn_order": {
|
||||
"type": "boolean",
|
||||
"description": "Displays function symbols in reversed order.\nUsed to support MWCC's -inline deferred option, which reverses the order of functions in the object file."
|
||||
},
|
||||
"source_path": {
|
||||
"type": "string",
|
||||
"description": "Path to the source file that generated the object."
|
||||
},
|
||||
"progress_categories": {
|
||||
"type": "array",
|
||||
"description": "Progress categories used for objdiff-cli report.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier for the category. (See progress_categories)"
|
||||
}
|
||||
},
|
||||
"auto_generated": {
|
||||
"type": "boolean",
|
||||
"description": "Hides the object from the object list by default, but still includes it in reports."
|
||||
}
|
||||
}
|
||||
},
|
||||
"progress_category": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier for the category."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Human-readable name of the category."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
186
deny.toml
@@ -9,6 +9,11 @@
|
||||
# The values provided in this template are the default values that will be used
|
||||
# when any section or field is not specified in your own configuration
|
||||
|
||||
# Root options
|
||||
|
||||
# The graph table configures how the dependency graph is constructed and thus
|
||||
# which crates the checks are performed against
|
||||
[graph]
|
||||
# If 1 or more target triples (and optionally, target_features) are specified,
|
||||
# only the specified targets will be checked when running `cargo deny check`.
|
||||
# This means, if a particular package is only ever used as a target specific
|
||||
@@ -20,88 +25,86 @@
|
||||
targets = [
|
||||
# The triple can be any string, but only the target triples built in to
|
||||
# rustc (as of 1.40) can be checked against actual config expressions
|
||||
#{ triple = "x86_64-unknown-linux-musl" },
|
||||
#"x86_64-unknown-linux-musl",
|
||||
# You can also specify which target_features you promise are enabled for a
|
||||
# particular target. target_features are currently not validated against
|
||||
# the actual valid features supported by the target architecture.
|
||||
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
|
||||
]
|
||||
# When creating the dependency graph used as the source of truth when checks are
|
||||
# executed, this field can be used to prune crates from the graph, removing them
|
||||
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
|
||||
# is pruned from the graph, all of its dependencies will also be pruned unless
|
||||
# they are connected to another crate in the graph that hasn't been pruned,
|
||||
# so it should be used with care. The identifiers are [Package ID Specifications]
|
||||
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
|
||||
#exclude = []
|
||||
# If true, metadata will be collected with `--all-features`. Note that this can't
|
||||
# be toggled off if true, if you want to conditionally enable `--all-features` it
|
||||
# is recommended to pass `--all-features` on the cmd line instead
|
||||
all-features = false
|
||||
# If true, metadata will be collected with `--no-default-features`. The same
|
||||
# caveat with `all-features` applies
|
||||
no-default-features = false
|
||||
# If set, these feature will be enabled when collecting metadata. If `--features`
|
||||
# is specified on the cmd line they will take precedence over this option.
|
||||
#features = []
|
||||
|
||||
# The output table provides options for how/if diagnostics are outputted
|
||||
[output]
|
||||
# When outputting inclusion graphs in diagnostics that include features, this
|
||||
# option can be used to specify the depth at which feature edges will be added.
|
||||
# This option is included since the graphs can be quite large and the addition
|
||||
# of features from the crate(s) to all of the graph roots can be far too verbose.
|
||||
# This option can be overridden via `--feature-depth` on the cmd line
|
||||
feature-depth = 1
|
||||
|
||||
# This section is considered when running `cargo deny check advisories`
|
||||
# More documentation for the advisories section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
|
||||
[advisories]
|
||||
# The path where the advisory database is cloned/fetched into
|
||||
db-path = "~/.cargo/advisory-db"
|
||||
# The path where the advisory databases are cloned/fetched into
|
||||
#db-path = "$CARGO_HOME/advisory-dbs"
|
||||
# The url(s) of the advisory databases to use
|
||||
db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||
# The lint level for security vulnerabilities
|
||||
vulnerability = "deny"
|
||||
# The lint level for unmaintained crates
|
||||
unmaintained = "warn"
|
||||
# The lint level for crates that have been yanked from their source registry
|
||||
yanked = "warn"
|
||||
# The lint level for crates with security notices. Note that as of
|
||||
# 2019-12-17 there are no security notice advisories in
|
||||
# https://github.com/rustsec/advisory-db
|
||||
notice = "warn"
|
||||
#db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||
# output a note when they are encountered.
|
||||
ignore = [
|
||||
#"RUSTSEC-0000-0000",
|
||||
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
|
||||
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
|
||||
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
|
||||
{ id = "RUSTSEC-2024-0384", reason = "Unmaintained indirect dependency" },
|
||||
]
|
||||
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
|
||||
# lower than the range specified will be ignored. Note that ignored advisories
|
||||
# will still output a note when they are encountered.
|
||||
# * None - CVSS Score 0.0
|
||||
# * Low - CVSS Score 0.1 - 3.9
|
||||
# * Medium - CVSS Score 4.0 - 6.9
|
||||
# * High - CVSS Score 7.0 - 8.9
|
||||
# * Critical - CVSS Score 9.0 - 10.0
|
||||
#severity-threshold =
|
||||
# If this is true, then cargo deny will use the git executable to fetch advisory database.
|
||||
# If this is false, then it uses a built-in git library.
|
||||
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
|
||||
# See Git Authentication for more information about setting up git authentication.
|
||||
#git-fetch-with-cli = true
|
||||
|
||||
# This section is considered when running `cargo deny check licenses`
|
||||
# More documentation for the licenses section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
|
||||
[licenses]
|
||||
# The lint level for crates which do not have a detectable license
|
||||
unlicensed = "deny"
|
||||
# List of explictly allowed licenses
|
||||
# List of explicitly allowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||
allow = [
|
||||
"MIT",
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"ISC",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"BSL-1.0",
|
||||
"CC0-1.0",
|
||||
"MPL-2.0",
|
||||
"Unicode-DFS-2016",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
"0BSD",
|
||||
"OFL-1.1",
|
||||
"LicenseRef-UFL-1.0",
|
||||
"OpenSSL",
|
||||
]
|
||||
# List of explictly disallowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||
deny = [
|
||||
#"Nokia",
|
||||
]
|
||||
# Lint level for licenses considered copyleft
|
||||
copyleft = "warn"
|
||||
# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
|
||||
# * both - The license will be approved if it is both OSI-approved *AND* FSF
|
||||
# * either - The license will be approved if it is either OSI-approved *OR* FSF
|
||||
# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
|
||||
# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
|
||||
# * neither - This predicate is ignored and the default lint level is used
|
||||
allow-osi-fsf-free = "neither"
|
||||
# Lint level used when no other predicates are matched
|
||||
# 1. License isn't in the allow or deny lists
|
||||
# 2. License isn't copyleft
|
||||
# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
|
||||
default = "deny"
|
||||
# The confidence threshold for detecting a license from license text.
|
||||
# The higher the value, the more closely the license text must be to the
|
||||
# canonical license text of a valid SPDX license file.
|
||||
@@ -112,32 +115,32 @@ confidence-threshold = 0.8
|
||||
exceptions = [
|
||||
# Each entry is the crate and version constraint, and its specific allow
|
||||
# list
|
||||
#{ allow = ["Zlib"], name = "adler32", version = "*" },
|
||||
#{ allow = ["Zlib"], crate = "adler32" },
|
||||
]
|
||||
|
||||
# Some crates don't have (easily) machine readable licensing information,
|
||||
# adding a clarification entry for it allows you to manually specify the
|
||||
# licensing information
|
||||
#[[licenses.clarify]]
|
||||
# The name of the crate the clarification applies to
|
||||
#name = "ring"
|
||||
# The optional version constraint for the crate
|
||||
#version = "*"
|
||||
[[licenses.clarify]]
|
||||
# The package spec the clarification applies to
|
||||
crate = "ring"
|
||||
# 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
|
||||
# the license expression. If the contents match, the clarification will be used
|
||||
# when running the license check, otherwise the clarification will be ignored
|
||||
# and the crate will be checked normally, which may produce warnings or errors
|
||||
# depending on the rest of your configuration
|
||||
#license-files = [
|
||||
license-files = [
|
||||
# Each entry is a crate relative path, and the (opaque) hash of its contents
|
||||
#{ path = "LICENSE", hash = 0xbd0eed23 }
|
||||
#]
|
||||
{ path = "LICENSE", hash = 0xbd0eed23 }
|
||||
]
|
||||
|
||||
[licenses.private]
|
||||
# If true, ignores workspace crates that aren't published, or are only
|
||||
# published to private registries
|
||||
# published to private registries.
|
||||
# To see how to mark a crate as unpublished (to the official registry),
|
||||
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
|
||||
ignore = false
|
||||
# One or more private registries that you might publish crates to, if a crate
|
||||
# is only published to private registries, and ignore is true, the crate will
|
||||
@@ -151,7 +154,7 @@ registries = [
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
|
||||
[bans]
|
||||
# Lint level for when multiple versions of the same crate are detected
|
||||
multiple-versions = "warn"
|
||||
multiple-versions = "allow"
|
||||
# Lint level for when a crate version requirement is `*`
|
||||
wildcards = "allow"
|
||||
# The graph highlighting used when creating dotgraphs for crates
|
||||
@@ -160,30 +163,63 @@ wildcards = "allow"
|
||||
# * simplest-path - The path to the version with the fewest edges is highlighted
|
||||
# * all - Both lowest-version and simplest-path are used
|
||||
highlight = "all"
|
||||
# The default lint level for `default` features for crates that are members of
|
||||
# the workspace that is being checked. This can be overridden by allowing/denying
|
||||
# `default` on a crate-by-crate basis if desired.
|
||||
workspace-default-features = "allow"
|
||||
# The default lint level for `default` features for external crates that are not
|
||||
# members of the workspace. This can be overridden by allowing/denying `default`
|
||||
# on a crate-by-crate basis if desired.
|
||||
external-default-features = "allow"
|
||||
# List of crates that are allowed. Use with care!
|
||||
allow = [
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
|
||||
]
|
||||
# List of crates to deny
|
||||
deny = [
|
||||
# Each entry the name of a crate and a version range. If version is
|
||||
# not specified, all versions will be matched.
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
#
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
|
||||
# Wrapper crates can optionally be specified to allow the crate when it
|
||||
# is a direct dependency of the otherwise banned crate
|
||||
#{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
|
||||
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
|
||||
]
|
||||
|
||||
# List of features to allow/deny
|
||||
# Each entry the name of a crate and a version range. If version is
|
||||
# not specified, all versions will be matched.
|
||||
#[[bans.features]]
|
||||
#crate = "reqwest"
|
||||
# Features to not allow
|
||||
#deny = ["json"]
|
||||
# Features to allow
|
||||
#allow = [
|
||||
# "rustls",
|
||||
# "__rustls",
|
||||
# "__tls",
|
||||
# "hyper-rustls",
|
||||
# "rustls",
|
||||
# "rustls-pemfile",
|
||||
# "rustls-tls-webpki-roots",
|
||||
# "tokio-rustls",
|
||||
# "webpki-roots",
|
||||
#]
|
||||
# If true, the allowed features must exactly match the enabled feature set. If
|
||||
# this is set there is no point setting `deny`
|
||||
#exact = true
|
||||
|
||||
# Certain crates/versions that will be skipped when doing duplicate detection.
|
||||
skip = [
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
|
||||
]
|
||||
# Similarly to `skip` allows you to skip certain crates during duplicate
|
||||
# detection. Unlike skip, it also includes the entire tree of transitive
|
||||
# dependencies starting at the specified crate, up to a certain depth, which is
|
||||
# by default infinite
|
||||
# by default infinite.
|
||||
skip-tree = [
|
||||
#{ name = "ansi_term", version = "=0.11.0", depth = 20 },
|
||||
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
|
||||
#{ crate = "ansi_term@0.11.0", depth = 20 },
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check sources`.
|
||||
@@ -203,9 +239,9 @@ allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
allow-git = []
|
||||
|
||||
[sources.allow-org]
|
||||
# 1 or more github.com organizations to allow git sources for
|
||||
github = ["encounter"]
|
||||
# 1 or more gitlab.com organizations to allow git sources for
|
||||
#gitlab = [""]
|
||||
# 1 or more bitbucket.org organizations to allow git sources for
|
||||
#bitbucket = [""]
|
||||
# github.com organizations to allow git sources for
|
||||
github = ["notify-rs"]
|
||||
# gitlab.com organizations to allow git sources for
|
||||
gitlab = []
|
||||
# bitbucket.org organizations to allow git sources for
|
||||
bitbucket = []
|
||||
|
||||
33
objdiff-cli/Cargo.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "objdiff-cli"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
readme = "../README.md"
|
||||
description = """
|
||||
A local diffing tool for decompilation projects.
|
||||
"""
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
argp = "0.3"
|
||||
crossterm = "0.28"
|
||||
enable-ansi-support = "0.2"
|
||||
memmap2 = "0.9"
|
||||
objdiff-core = { path = "../objdiff-core", features = ["all"] }
|
||||
prost = "0.13"
|
||||
ratatui = "0.29"
|
||||
rayon = "1.10"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
supports-color = "3.0"
|
||||
time = { version = "0.3", features = ["formatting", "local-offset"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
[target.'cfg(target_env = "musl")'.dependencies]
|
||||
mimalloc = "0.1"
|
||||
63
objdiff-cli/src/argp_version.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
// Originally from https://gist.github.com/suluke/e0c672492126be0a4f3b4f0e1115d77c
|
||||
//! Extend `argp` to be better integrated with the `cargo` ecosystem
|
||||
//!
|
||||
//! For now, this only adds a --version/-V option which causes early-exit.
|
||||
use std::ffi::OsStr;
|
||||
|
||||
use argp::{parser::ParseGlobalOptions, EarlyExit, FromArgs, TopLevelCommand};
|
||||
|
||||
struct ArgsOrVersion<T>(T)
|
||||
where T: FromArgs;
|
||||
|
||||
impl<T> TopLevelCommand for ArgsOrVersion<T> where T: FromArgs {}
|
||||
|
||||
impl<T> FromArgs for ArgsOrVersion<T>
|
||||
where T: FromArgs
|
||||
{
|
||||
fn _from_args(
|
||||
command_name: &[&str],
|
||||
args: &[&OsStr],
|
||||
parent: Option<&mut dyn ParseGlobalOptions>,
|
||||
) -> Result<Self, EarlyExit> {
|
||||
/// Also use argp for catching `--version`-only invocations
|
||||
#[derive(FromArgs)]
|
||||
struct Version {
|
||||
/// Print version information and exit.
|
||||
#[argp(switch, short = 'V')]
|
||||
pub version: bool,
|
||||
}
|
||||
|
||||
match Version::from_args(command_name, args) {
|
||||
Ok(v) => {
|
||||
if v.version {
|
||||
println!(
|
||||
"{} {}",
|
||||
command_name.first().unwrap_or(&""),
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
);
|
||||
std::process::exit(0);
|
||||
} else {
|
||||
// Pass through empty arguments
|
||||
T::_from_args(command_name, args, parent).map(Self)
|
||||
}
|
||||
}
|
||||
Err(exit) => match exit {
|
||||
EarlyExit::Help(_help) => {
|
||||
// TODO: Chain help info from Version
|
||||
// For now, we just put the switch on T as well
|
||||
T::from_args(command_name, &["--help"]).map(Self)
|
||||
}
|
||||
EarlyExit::Err(_) => T::_from_args(command_name, args, parent).map(Self),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a `FromArgs` type from the current process’s `env::args`.
|
||||
///
|
||||
/// This function will exit early from the current process if argument parsing was unsuccessful or if information like `--help` was requested.
|
||||
/// Error messages will be printed to stderr, and `--help` output to stdout.
|
||||
pub fn from_env<T>() -> T
|
||||
where T: TopLevelCommand {
|
||||
argp::parse_args_or_exit::<ArgsOrVersion<T>>(argp::DEFAULT).0
|
||||
}
|
||||
969
objdiff-cli/src/cmd/diff.rs
Normal file
@@ -0,0 +1,969 @@
|
||||
use std::{
|
||||
fs,
|
||||
io::stdout,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use argp::FromArgs;
|
||||
use crossterm::{
|
||||
event,
|
||||
event::{
|
||||
DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseButton,
|
||||
MouseEventKind,
|
||||
},
|
||||
terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, SetTitle,
|
||||
},
|
||||
};
|
||||
use event::KeyModifiers;
|
||||
use objdiff_core::{
|
||||
bindings::diff::DiffResult,
|
||||
config::{ProjectConfig, ProjectObject},
|
||||
diff,
|
||||
diff::{
|
||||
display::{display_diff, DiffText, HighlightKind},
|
||||
DiffObjsResult, ObjDiff, ObjInsDiffKind, ObjSymbolDiff,
|
||||
},
|
||||
obj,
|
||||
obj::{ObjInfo, ObjSectionKind, ObjSymbol, SymbolRef},
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, Clear, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
|
||||
};
|
||||
|
||||
use crate::util::{
|
||||
output::{write_output, OutputFormat},
|
||||
term::crossterm_panic_handler,
|
||||
};
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// Diff two object files. (Interactive or one-shot mode)
|
||||
#[argp(subcommand, name = "diff")]
|
||||
pub struct Args {
|
||||
#[argp(option, short = '1')]
|
||||
/// Target object file
|
||||
target: Option<PathBuf>,
|
||||
#[argp(option, short = '2')]
|
||||
/// Base object file
|
||||
base: Option<PathBuf>,
|
||||
#[argp(option, short = 'p')]
|
||||
/// Project directory
|
||||
project: Option<PathBuf>,
|
||||
#[argp(option, short = 'u')]
|
||||
/// Unit name within project
|
||||
unit: Option<String>,
|
||||
#[argp(switch, short = 'x')]
|
||||
/// Relax relocation diffs
|
||||
relax_reloc_diffs: bool,
|
||||
#[argp(option, short = 'o')]
|
||||
/// Output file (one-shot mode) ("-" for stdout)
|
||||
output: Option<PathBuf>,
|
||||
#[argp(option)]
|
||||
/// Output format (json, json-pretty, proto) (default: json)
|
||||
format: Option<String>,
|
||||
#[argp(positional)]
|
||||
/// Function symbol to diff
|
||||
symbol: Option<String>,
|
||||
}
|
||||
|
||||
pub fn run(args: Args) -> Result<()> {
|
||||
let (target_path, base_path, project_config) = match (
|
||||
&args.target,
|
||||
&args.base,
|
||||
&args.project,
|
||||
&args.unit,
|
||||
) {
|
||||
(Some(t), Some(b), None, None) => (Some(t.clone()), Some(b.clone()), None),
|
||||
(None, None, p, u) => {
|
||||
let project = match p {
|
||||
Some(project) => project.clone(),
|
||||
_ => std::env::current_dir().context("Failed to get the current directory")?,
|
||||
};
|
||||
let Some((project_config, project_config_info)) =
|
||||
objdiff_core::config::try_project_config(&project)
|
||||
else {
|
||||
bail!("Project config not found in {}", &project.display())
|
||||
};
|
||||
let mut project_config = project_config.with_context(|| {
|
||||
format!("Reading project config {}", project_config_info.path.display())
|
||||
})?;
|
||||
let object = {
|
||||
let resolve_paths = |o: &mut ProjectObject| {
|
||||
o.resolve_paths(
|
||||
&project,
|
||||
project_config.target_dir.as_deref(),
|
||||
project_config.base_dir.as_deref(),
|
||||
)
|
||||
};
|
||||
if let Some(u) = u {
|
||||
let unit_path =
|
||||
PathBuf::from_str(u).ok().and_then(|p| fs::canonicalize(p).ok());
|
||||
|
||||
let Some(object) = project_config
|
||||
.units
|
||||
.as_deref_mut()
|
||||
.unwrap_or_default()
|
||||
.iter_mut()
|
||||
.find_map(|obj| {
|
||||
if obj.name.as_deref() == Some(u) {
|
||||
resolve_paths(obj);
|
||||
return Some(obj);
|
||||
}
|
||||
|
||||
let up = unit_path.as_deref()?;
|
||||
|
||||
resolve_paths(obj);
|
||||
|
||||
if [&obj.base_path, &obj.target_path]
|
||||
.into_iter()
|
||||
.filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok()))
|
||||
.any(|p| p == up)
|
||||
{
|
||||
return Some(obj);
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
else {
|
||||
bail!("Unit not found: {}", u)
|
||||
};
|
||||
|
||||
object
|
||||
} else if let Some(symbol_name) = &args.symbol {
|
||||
let mut idx = None;
|
||||
let mut count = 0usize;
|
||||
for (i, obj) in project_config
|
||||
.units
|
||||
.as_deref_mut()
|
||||
.unwrap_or_default()
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
{
|
||||
resolve_paths(obj);
|
||||
|
||||
if obj
|
||||
.target_path
|
||||
.as_deref()
|
||||
.map(|o| obj::read::has_function(o, symbol_name))
|
||||
.transpose()?
|
||||
.unwrap_or(false)
|
||||
{
|
||||
idx = Some(i);
|
||||
count += 1;
|
||||
if count > 1 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
match (count, idx) {
|
||||
(0, None) => bail!("Symbol not found: {}", symbol_name),
|
||||
(1, Some(i)) => &mut project_config.units_mut()[i],
|
||||
(2.., Some(_)) => bail!(
|
||||
"Multiple instances of {} were found, try specifying a unit",
|
||||
symbol_name
|
||||
),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else {
|
||||
bail!("Must specify one of: symbol, project and unit, target and base objects")
|
||||
}
|
||||
};
|
||||
let target_path = object.target_path.clone();
|
||||
let base_path = object.base_path.clone();
|
||||
(target_path, base_path, Some(project_config))
|
||||
}
|
||||
_ => bail!("Either target and base or project and unit must be specified"),
|
||||
};
|
||||
|
||||
if let Some(output) = &args.output {
|
||||
run_oneshot(&args, output, target_path.as_deref(), base_path.as_deref())
|
||||
} else {
|
||||
run_interactive(args, target_path, base_path, project_config)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_oneshot(
|
||||
args: &Args,
|
||||
output: &Path,
|
||||
target_path: Option<&Path>,
|
||||
base_path: Option<&Path>,
|
||||
) -> Result<()> {
|
||||
let output_format = OutputFormat::from_option(args.format.as_deref())?;
|
||||
let config = diff::DiffObjConfig {
|
||||
relax_reloc_diffs: args.relax_reloc_diffs,
|
||||
..Default::default() // TODO
|
||||
};
|
||||
let target = target_path
|
||||
.map(|p| obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display())))
|
||||
.transpose()?;
|
||||
let base = base_path
|
||||
.map(|p| obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display())))
|
||||
.transpose()?;
|
||||
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?;
|
||||
let left = target.as_ref().and_then(|o| result.left.as_ref().map(|d| (o, d)));
|
||||
let right = base.as_ref().and_then(|o| result.right.as_ref().map(|d| (o, d)));
|
||||
write_output(&DiffResult::new(left, right), Some(output), output_format)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_interactive(
|
||||
args: Args,
|
||||
target_path: Option<PathBuf>,
|
||||
base_path: Option<PathBuf>,
|
||||
project_config: Option<ProjectConfig>,
|
||||
) -> Result<()> {
|
||||
let Some(symbol_name) = &args.symbol else { bail!("Interactive mode requires a symbol name") };
|
||||
let time_format = time::format_description::parse_borrowed::<2>("[hour]:[minute]:[second]")
|
||||
.context("Failed to parse time format")?;
|
||||
let mut state = Box::new(FunctionDiffUi {
|
||||
relax_reloc_diffs: args.relax_reloc_diffs,
|
||||
left_highlight: HighlightKind::None,
|
||||
right_highlight: HighlightKind::None,
|
||||
scroll_x: 0,
|
||||
scroll_state_x: ScrollbarState::default(),
|
||||
scroll_y: 0,
|
||||
scroll_state_y: ScrollbarState::default(),
|
||||
per_page: 0,
|
||||
num_rows: 0,
|
||||
symbol_name: symbol_name.clone(),
|
||||
target_path,
|
||||
base_path,
|
||||
project_config,
|
||||
left_obj: None,
|
||||
right_obj: None,
|
||||
prev_obj: None,
|
||||
diff_result: DiffObjsResult::default(),
|
||||
left_sym: None,
|
||||
right_sym: None,
|
||||
prev_sym: None,
|
||||
reload_time: None,
|
||||
time_format,
|
||||
open_options: false,
|
||||
three_way: false,
|
||||
});
|
||||
state.reload()?;
|
||||
|
||||
crossterm_panic_handler();
|
||||
enable_raw_mode()?;
|
||||
crossterm::queue!(
|
||||
stdout(),
|
||||
EnterAlternateScreen,
|
||||
EnableMouseCapture,
|
||||
SetTitle(format!("{} - objdiff", symbol_name)),
|
||||
)?;
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
'outer: loop {
|
||||
let mut result = EventResult { redraw: true, ..Default::default() };
|
||||
loop {
|
||||
if result.redraw {
|
||||
terminal.draw(|f| loop {
|
||||
result.redraw = false;
|
||||
state.draw(f, &mut result);
|
||||
if state.open_options {
|
||||
state.draw_options(f, &mut result);
|
||||
}
|
||||
result.click_xy = None;
|
||||
if !result.redraw {
|
||||
break;
|
||||
}
|
||||
// Clear buffer on redraw
|
||||
f.buffer_mut().reset();
|
||||
})?;
|
||||
}
|
||||
match state.handle_event(event::read()?) {
|
||||
EventControlFlow::Break => break 'outer,
|
||||
EventControlFlow::Continue(r) => result = r,
|
||||
EventControlFlow::Reload => break,
|
||||
}
|
||||
}
|
||||
state.reload()?;
|
||||
}
|
||||
|
||||
// Reset terminal
|
||||
disable_raw_mode()?;
|
||||
crossterm::execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture)?;
|
||||
terminal.show_cursor()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_symbol(obj: Option<&ObjInfo>, sym: Option<SymbolRef>) -> Option<&ObjSymbol> {
|
||||
Some(obj?.section_symbol(sym?).1)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_symbol_diff(obj: Option<&ObjDiff>, sym: Option<SymbolRef>) -> Option<&ObjSymbolDiff> {
|
||||
Some(obj?.symbol_diff(sym?))
|
||||
}
|
||||
|
||||
fn find_function(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
|
||||
for (section_idx, section) in obj.sections.iter().enumerate() {
|
||||
if section.kind != ObjSectionKind::Code {
|
||||
continue;
|
||||
}
|
||||
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
|
||||
if symbol.name == name {
|
||||
return Some(SymbolRef { section_idx, symbol_idx });
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
struct FunctionDiffUi {
|
||||
relax_reloc_diffs: bool,
|
||||
left_highlight: HighlightKind,
|
||||
right_highlight: HighlightKind,
|
||||
scroll_x: usize,
|
||||
scroll_state_x: ScrollbarState,
|
||||
scroll_y: usize,
|
||||
scroll_state_y: ScrollbarState,
|
||||
per_page: usize,
|
||||
num_rows: usize,
|
||||
symbol_name: String,
|
||||
target_path: Option<PathBuf>,
|
||||
base_path: Option<PathBuf>,
|
||||
project_config: Option<ProjectConfig>,
|
||||
left_obj: Option<ObjInfo>,
|
||||
right_obj: Option<ObjInfo>,
|
||||
prev_obj: Option<ObjInfo>,
|
||||
diff_result: DiffObjsResult,
|
||||
left_sym: Option<SymbolRef>,
|
||||
right_sym: Option<SymbolRef>,
|
||||
prev_sym: Option<SymbolRef>,
|
||||
reload_time: Option<time::OffsetDateTime>,
|
||||
time_format: Vec<time::format_description::FormatItem<'static>>,
|
||||
open_options: bool,
|
||||
three_way: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct EventResult {
|
||||
redraw: bool,
|
||||
click_xy: Option<(u16, u16)>,
|
||||
}
|
||||
|
||||
enum EventControlFlow {
|
||||
Break,
|
||||
Continue(EventResult),
|
||||
Reload,
|
||||
}
|
||||
|
||||
impl FunctionDiffUi {
|
||||
fn draw(&mut self, f: &mut Frame, result: &mut EventResult) {
|
||||
let chunks = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).split(f.area());
|
||||
let header_chunks = Layout::horizontal([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(2),
|
||||
])
|
||||
.split(chunks[0]);
|
||||
let content_chunks = if self.three_way {
|
||||
Layout::horizontal([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(2),
|
||||
])
|
||||
.split(chunks[1])
|
||||
} else {
|
||||
Layout::horizontal([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(2),
|
||||
])
|
||||
.split(chunks[1])
|
||||
};
|
||||
|
||||
self.per_page = chunks[1].height.saturating_sub(2) as usize;
|
||||
let max_scroll_y = self.num_rows.saturating_sub(self.per_page);
|
||||
if self.scroll_y > max_scroll_y {
|
||||
self.scroll_y = max_scroll_y;
|
||||
}
|
||||
self.scroll_state_y =
|
||||
self.scroll_state_y.content_length(max_scroll_y).position(self.scroll_y);
|
||||
|
||||
let mut line_l = Line::default();
|
||||
line_l
|
||||
.spans
|
||||
.push(Span::styled(self.symbol_name.clone(), Style::new().fg(Color::White).bold()));
|
||||
f.render_widget(line_l, header_chunks[0]);
|
||||
|
||||
let mut line_r = Line::default();
|
||||
if let Some(percent) = get_symbol_diff(self.diff_result.right.as_ref(), self.right_sym)
|
||||
.and_then(|s| s.match_percent)
|
||||
{
|
||||
line_r.spans.push(Span::styled(
|
||||
format!("{:.2}% ", percent),
|
||||
Style::new().fg(match_percent_color(percent)),
|
||||
));
|
||||
}
|
||||
let reload_time = self
|
||||
.reload_time
|
||||
.as_ref()
|
||||
.and_then(|t| t.format(&self.time_format).ok())
|
||||
.unwrap_or_else(|| "N/A".to_string());
|
||||
line_r.spans.push(Span::styled(
|
||||
format!("Last reload: {}", reload_time),
|
||||
Style::new().fg(Color::White),
|
||||
));
|
||||
f.render_widget(line_r, header_chunks[2]);
|
||||
|
||||
let mut left_text = None;
|
||||
let mut left_highlight = None;
|
||||
let mut max_width = 0;
|
||||
if let (Some(symbol), Some(symbol_diff)) = (
|
||||
get_symbol(self.left_obj.as_ref(), self.left_sym),
|
||||
get_symbol_diff(self.diff_result.left.as_ref(), self.left_sym),
|
||||
) {
|
||||
let mut text = Text::default();
|
||||
let rect = content_chunks[0].inner(Margin::new(0, 1));
|
||||
left_highlight = self.print_sym(
|
||||
&mut text,
|
||||
symbol,
|
||||
symbol_diff,
|
||||
rect,
|
||||
&self.left_highlight,
|
||||
result,
|
||||
false,
|
||||
);
|
||||
max_width = max_width.max(text.width());
|
||||
left_text = Some(text);
|
||||
}
|
||||
|
||||
let mut right_text = None;
|
||||
let mut right_highlight = None;
|
||||
let mut margin_text = None;
|
||||
if let (Some(symbol), Some(symbol_diff)) = (
|
||||
get_symbol(self.right_obj.as_ref(), self.right_sym),
|
||||
get_symbol_diff(self.diff_result.right.as_ref(), self.right_sym),
|
||||
) {
|
||||
let mut text = Text::default();
|
||||
let rect = content_chunks[2].inner(Margin::new(0, 1));
|
||||
right_highlight = self.print_sym(
|
||||
&mut text,
|
||||
symbol,
|
||||
symbol_diff,
|
||||
rect,
|
||||
&self.right_highlight,
|
||||
result,
|
||||
false,
|
||||
);
|
||||
max_width = max_width.max(text.width());
|
||||
right_text = Some(text);
|
||||
|
||||
// Render margin
|
||||
let mut text = Text::default();
|
||||
let rect = content_chunks[1].inner(Margin::new(1, 1));
|
||||
self.print_margin(&mut text, symbol_diff, rect);
|
||||
margin_text = Some(text);
|
||||
}
|
||||
|
||||
let mut prev_text = None;
|
||||
let mut prev_margin_text = None;
|
||||
if self.three_way {
|
||||
if let (Some(symbol), Some(symbol_diff)) = (
|
||||
get_symbol(self.prev_obj.as_ref(), self.prev_sym),
|
||||
get_symbol_diff(self.diff_result.prev.as_ref(), self.prev_sym),
|
||||
) {
|
||||
let mut text = Text::default();
|
||||
let rect = content_chunks[4].inner(Margin::new(0, 1));
|
||||
self.print_sym(
|
||||
&mut text,
|
||||
symbol,
|
||||
symbol_diff,
|
||||
rect,
|
||||
&self.right_highlight,
|
||||
result,
|
||||
true,
|
||||
);
|
||||
max_width = max_width.max(text.width());
|
||||
prev_text = Some(text);
|
||||
|
||||
// Render margin
|
||||
let mut text = Text::default();
|
||||
let rect = content_chunks[3].inner(Margin::new(1, 1));
|
||||
self.print_margin(&mut text, symbol_diff, rect);
|
||||
prev_margin_text = Some(text);
|
||||
}
|
||||
}
|
||||
|
||||
let max_scroll_x =
|
||||
max_width.saturating_sub(content_chunks[0].width.min(content_chunks[2].width) as usize);
|
||||
if self.scroll_x > max_scroll_x {
|
||||
self.scroll_x = max_scroll_x;
|
||||
}
|
||||
self.scroll_state_x =
|
||||
self.scroll_state_x.content_length(max_scroll_x).position(self.scroll_x);
|
||||
|
||||
if let Some(text) = left_text {
|
||||
// Render left column
|
||||
f.render_widget(
|
||||
Paragraph::new(text)
|
||||
.block(
|
||||
Block::new()
|
||||
.borders(Borders::TOP)
|
||||
.border_style(Style::new().fg(Color::Gray))
|
||||
.title_style(Style::new().bold())
|
||||
.title("TARGET"),
|
||||
)
|
||||
.scroll((0, self.scroll_x as u16)),
|
||||
content_chunks[0],
|
||||
);
|
||||
}
|
||||
if let Some(text) = margin_text {
|
||||
f.render_widget(text, content_chunks[1].inner(Margin::new(1, 1)));
|
||||
}
|
||||
if let Some(text) = right_text {
|
||||
f.render_widget(
|
||||
Paragraph::new(text)
|
||||
.block(
|
||||
Block::new()
|
||||
.borders(Borders::TOP)
|
||||
.border_style(Style::new().fg(Color::Gray))
|
||||
.title_style(Style::new().bold())
|
||||
.title("CURRENT"),
|
||||
)
|
||||
.scroll((0, self.scroll_x as u16)),
|
||||
content_chunks[2],
|
||||
);
|
||||
}
|
||||
|
||||
if self.three_way {
|
||||
if let Some(text) = prev_margin_text {
|
||||
f.render_widget(text, content_chunks[3].inner(Margin::new(1, 1)));
|
||||
}
|
||||
let block = Block::new()
|
||||
.borders(Borders::TOP)
|
||||
.border_style(Style::new().fg(Color::Gray))
|
||||
.title_style(Style::new().bold())
|
||||
.title("SAVED");
|
||||
if let Some(text) = prev_text {
|
||||
f.render_widget(
|
||||
Paragraph::new(text).block(block.clone()).scroll((0, self.scroll_x as u16)),
|
||||
content_chunks[4],
|
||||
);
|
||||
} else {
|
||||
f.render_widget(block, content_chunks[4]);
|
||||
}
|
||||
}
|
||||
|
||||
// Render scrollbars
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::new(ScrollbarOrientation::VerticalRight).begin_symbol(None).end_symbol(None),
|
||||
chunks[1].inner(Margin::new(0, 1)),
|
||||
&mut self.scroll_state_y,
|
||||
);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol("■"),
|
||||
content_chunks[0],
|
||||
&mut self.scroll_state_x,
|
||||
);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol("■"),
|
||||
content_chunks[2],
|
||||
&mut self.scroll_state_x,
|
||||
);
|
||||
if self.three_way {
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol("■"),
|
||||
content_chunks[4],
|
||||
&mut self.scroll_state_x,
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(new_highlight) = left_highlight {
|
||||
if new_highlight == self.left_highlight {
|
||||
if self.left_highlight != self.right_highlight {
|
||||
self.right_highlight = self.left_highlight.clone();
|
||||
} else {
|
||||
self.left_highlight = HighlightKind::None;
|
||||
self.right_highlight = HighlightKind::None;
|
||||
}
|
||||
} else {
|
||||
self.left_highlight = new_highlight;
|
||||
}
|
||||
result.redraw = true;
|
||||
} else if let Some(new_highlight) = right_highlight {
|
||||
if new_highlight == self.right_highlight {
|
||||
if self.left_highlight != self.right_highlight {
|
||||
self.left_highlight = self.right_highlight.clone();
|
||||
} else {
|
||||
self.left_highlight = HighlightKind::None;
|
||||
self.right_highlight = HighlightKind::None;
|
||||
}
|
||||
} else {
|
||||
self.right_highlight = new_highlight;
|
||||
}
|
||||
result.redraw = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_options(&mut self, f: &mut Frame, _result: &mut EventResult) {
|
||||
let percent_x = 50;
|
||||
let percent_y = 50;
|
||||
let popup_rect = Layout::vertical([
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
Constraint::Percentage(percent_y),
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
])
|
||||
.split(f.area())[1];
|
||||
let popup_rect = Layout::horizontal([
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
Constraint::Percentage(percent_x),
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
])
|
||||
.split(popup_rect)[1];
|
||||
|
||||
let popup = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Options")
|
||||
.title_style(Style::default().fg(Color::White).bg(Color::Black));
|
||||
f.render_widget(Clear, popup_rect);
|
||||
f.render_widget(popup, popup_rect);
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, event: Event) -> EventControlFlow {
|
||||
let mut result = EventResult::default();
|
||||
match event {
|
||||
Event::Key(event)
|
||||
if matches!(event.kind, KeyEventKind::Press | KeyEventKind::Repeat) =>
|
||||
{
|
||||
match event.code {
|
||||
// Quit
|
||||
KeyCode::Esc | KeyCode::Char('q') => return EventControlFlow::Break,
|
||||
// Page up
|
||||
KeyCode::PageUp => {
|
||||
self.page_up(false);
|
||||
result.redraw = true;
|
||||
}
|
||||
// Page up (shift + space)
|
||||
KeyCode::Char(' ') if event.modifiers.contains(KeyModifiers::SHIFT) => {
|
||||
self.page_up(false);
|
||||
result.redraw = true;
|
||||
}
|
||||
// Page down
|
||||
KeyCode::Char(' ') | KeyCode::PageDown => {
|
||||
self.page_down(false);
|
||||
result.redraw = true;
|
||||
}
|
||||
// Page down (ctrl + f)
|
||||
KeyCode::Char('f') if event.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
self.page_down(false);
|
||||
result.redraw = true;
|
||||
}
|
||||
// Page up (ctrl + b)
|
||||
KeyCode::Char('b') if event.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
self.page_up(false);
|
||||
result.redraw = true;
|
||||
}
|
||||
// Half page down (ctrl + d)
|
||||
KeyCode::Char('d') if event.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
self.page_down(true);
|
||||
result.redraw = true;
|
||||
}
|
||||
// Half page up (ctrl + u)
|
||||
KeyCode::Char('u') if event.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
self.page_up(true);
|
||||
result.redraw = true;
|
||||
}
|
||||
// Scroll down
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
self.scroll_y += 1;
|
||||
result.redraw = true;
|
||||
}
|
||||
// Scroll up
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
self.scroll_y = self.scroll_y.saturating_sub(1);
|
||||
result.redraw = true;
|
||||
}
|
||||
// Scroll to start
|
||||
KeyCode::Char('g') => {
|
||||
self.scroll_y = 0;
|
||||
result.redraw = true;
|
||||
}
|
||||
// Scroll to end
|
||||
KeyCode::Char('G') => {
|
||||
self.scroll_y = self.num_rows;
|
||||
result.redraw = true;
|
||||
}
|
||||
// Reload
|
||||
KeyCode::Char('r') => {
|
||||
result.redraw = true;
|
||||
return EventControlFlow::Reload;
|
||||
}
|
||||
// Scroll right
|
||||
KeyCode::Right | KeyCode::Char('l') => {
|
||||
self.scroll_x += 1;
|
||||
result.redraw = true;
|
||||
}
|
||||
// Scroll left
|
||||
KeyCode::Left | KeyCode::Char('h') => {
|
||||
self.scroll_x = self.scroll_x.saturating_sub(1);
|
||||
result.redraw = true;
|
||||
}
|
||||
// Toggle relax relocation diffs
|
||||
KeyCode::Char('x') => {
|
||||
self.relax_reloc_diffs = !self.relax_reloc_diffs;
|
||||
result.redraw = true;
|
||||
return EventControlFlow::Reload;
|
||||
}
|
||||
// Toggle three-way diff
|
||||
KeyCode::Char('3') => {
|
||||
self.three_way = !self.three_way;
|
||||
result.redraw = true;
|
||||
}
|
||||
// Toggle options
|
||||
KeyCode::Char('o') => {
|
||||
self.open_options = !self.open_options;
|
||||
result.redraw = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Event::Mouse(event) => match event.kind {
|
||||
MouseEventKind::ScrollDown => {
|
||||
self.scroll_y += 3;
|
||||
result.redraw = true;
|
||||
}
|
||||
MouseEventKind::ScrollUp => {
|
||||
self.scroll_y = self.scroll_y.saturating_sub(3);
|
||||
result.redraw = true;
|
||||
}
|
||||
MouseEventKind::ScrollRight => {
|
||||
self.scroll_x += 3;
|
||||
result.redraw = true;
|
||||
}
|
||||
MouseEventKind::ScrollLeft => {
|
||||
self.scroll_x = self.scroll_x.saturating_sub(3);
|
||||
result.redraw = true;
|
||||
}
|
||||
MouseEventKind::Down(MouseButton::Left) => {
|
||||
result.click_xy = Some((event.column, event.row));
|
||||
result.redraw = true;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Event::Resize(_, _) => {
|
||||
result.redraw = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
EventControlFlow::Continue(result)
|
||||
}
|
||||
|
||||
fn page_up(&mut self, half: bool) {
|
||||
self.scroll_y = self.scroll_y.saturating_sub(self.per_page / if half { 2 } else { 1 });
|
||||
}
|
||||
|
||||
fn page_down(&mut self, half: bool) {
|
||||
self.scroll_y += self.per_page / if half { 2 } else { 1 };
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
fn print_sym(
|
||||
&self,
|
||||
out: &mut Text<'static>,
|
||||
symbol: &ObjSymbol,
|
||||
symbol_diff: &ObjSymbolDiff,
|
||||
rect: Rect,
|
||||
highlight: &HighlightKind,
|
||||
result: &EventResult,
|
||||
only_changed: bool,
|
||||
) -> Option<HighlightKind> {
|
||||
let base_addr = symbol.address;
|
||||
let mut new_highlight = None;
|
||||
for (y, ins_diff) in symbol_diff
|
||||
.instructions
|
||||
.iter()
|
||||
.skip(self.scroll_y)
|
||||
.take(rect.height as usize)
|
||||
.enumerate()
|
||||
{
|
||||
if only_changed && ins_diff.kind == ObjInsDiffKind::None {
|
||||
out.lines.push(Line::default());
|
||||
continue;
|
||||
}
|
||||
let mut sx = rect.x;
|
||||
let sy = rect.y + y as u16;
|
||||
let mut line = Line::default();
|
||||
display_diff(ins_diff, base_addr, |text| -> Result<()> {
|
||||
let label_text;
|
||||
let mut base_color = match ins_diff.kind {
|
||||
ObjInsDiffKind::None
|
||||
| ObjInsDiffKind::OpMismatch
|
||||
| ObjInsDiffKind::ArgMismatch => Color::Gray,
|
||||
ObjInsDiffKind::Replace => Color::Cyan,
|
||||
ObjInsDiffKind::Delete => Color::Red,
|
||||
ObjInsDiffKind::Insert => Color::Green,
|
||||
};
|
||||
let mut pad_to = 0;
|
||||
match text {
|
||||
DiffText::Basic(text) => {
|
||||
label_text = text.to_string();
|
||||
}
|
||||
DiffText::BasicColor(s, idx) => {
|
||||
label_text = s.to_string();
|
||||
base_color = COLOR_ROTATION[idx % COLOR_ROTATION.len()];
|
||||
}
|
||||
DiffText::Line(num) => {
|
||||
label_text = format!("{num} ");
|
||||
base_color = Color::DarkGray;
|
||||
pad_to = 5;
|
||||
}
|
||||
DiffText::Address(addr) => {
|
||||
label_text = format!("{:x}:", addr);
|
||||
pad_to = 5;
|
||||
}
|
||||
DiffText::Opcode(mnemonic, _op) => {
|
||||
label_text = mnemonic.to_string();
|
||||
if ins_diff.kind == ObjInsDiffKind::OpMismatch {
|
||||
base_color = Color::Blue;
|
||||
}
|
||||
pad_to = 8;
|
||||
}
|
||||
DiffText::Argument(arg, diff) => {
|
||||
label_text = arg.to_string();
|
||||
if let Some(diff) = diff {
|
||||
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
|
||||
}
|
||||
}
|
||||
DiffText::BranchDest(addr, diff) => {
|
||||
label_text = format!("{addr:x}");
|
||||
if let Some(diff) = diff {
|
||||
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
|
||||
}
|
||||
}
|
||||
DiffText::Symbol(sym, diff) => {
|
||||
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
|
||||
label_text = name.clone();
|
||||
if let Some(diff) = diff {
|
||||
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
|
||||
} else {
|
||||
base_color = Color::White;
|
||||
}
|
||||
}
|
||||
DiffText::Spacing(n) => {
|
||||
line.spans.push(Span::raw(" ".repeat(n)));
|
||||
sx += n as u16;
|
||||
return Ok(());
|
||||
}
|
||||
DiffText::Eol => {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
let len = label_text.len();
|
||||
let highlighted = *highlight == text;
|
||||
if let Some((cx, cy)) = result.click_xy {
|
||||
if cx >= sx && cx < sx + len as u16 && cy == sy {
|
||||
new_highlight = Some(text.into());
|
||||
}
|
||||
}
|
||||
let mut style = Style::new().fg(base_color);
|
||||
if highlighted {
|
||||
style = style.bg(Color::DarkGray);
|
||||
}
|
||||
line.spans.push(Span::styled(label_text, style));
|
||||
sx += len as u16;
|
||||
if pad_to > len {
|
||||
let pad = (pad_to - len) as u16;
|
||||
line.spans.push(Span::raw(" ".repeat(pad as usize)));
|
||||
sx += pad;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
out.lines.push(line);
|
||||
}
|
||||
new_highlight
|
||||
}
|
||||
|
||||
fn print_margin(&self, out: &mut Text, symbol: &ObjSymbolDiff, rect: Rect) {
|
||||
for ins_diff in symbol.instructions.iter().skip(self.scroll_y).take(rect.height as usize) {
|
||||
if ins_diff.kind != ObjInsDiffKind::None {
|
||||
out.lines.push(Line::raw(match ins_diff.kind {
|
||||
ObjInsDiffKind::Delete => "<",
|
||||
ObjInsDiffKind::Insert => ">",
|
||||
_ => "|",
|
||||
}));
|
||||
} else {
|
||||
out.lines.push(Line::raw(" "));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reload(&mut self) -> Result<()> {
|
||||
let prev = self.right_obj.take();
|
||||
let config = diff::DiffObjConfig {
|
||||
relax_reloc_diffs: self.relax_reloc_diffs,
|
||||
..Default::default() // TODO
|
||||
};
|
||||
let target = self
|
||||
.target_path
|
||||
.as_deref()
|
||||
.map(|p| {
|
||||
obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display()))
|
||||
})
|
||||
.transpose()?;
|
||||
let base = self
|
||||
.base_path
|
||||
.as_deref()
|
||||
.map(|p| {
|
||||
obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display()))
|
||||
})
|
||||
.transpose()?;
|
||||
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), prev.as_ref())?;
|
||||
|
||||
let left_sym = target.as_ref().and_then(|o| find_function(o, &self.symbol_name));
|
||||
let right_sym = base.as_ref().and_then(|o| find_function(o, &self.symbol_name));
|
||||
let prev_sym = prev.as_ref().and_then(|o| find_function(o, &self.symbol_name));
|
||||
self.num_rows = match (
|
||||
get_symbol_diff(result.left.as_ref(), left_sym),
|
||||
get_symbol_diff(result.right.as_ref(), right_sym),
|
||||
) {
|
||||
(Some(l), Some(r)) => l.instructions.len().max(r.instructions.len()),
|
||||
(Some(l), None) => l.instructions.len(),
|
||||
(None, Some(r)) => r.instructions.len(),
|
||||
(None, None) => bail!("Symbol not found: {}", self.symbol_name),
|
||||
};
|
||||
self.left_obj = target;
|
||||
self.right_obj = base;
|
||||
self.prev_obj = prev;
|
||||
self.diff_result = result;
|
||||
self.left_sym = left_sym;
|
||||
self.right_sym = right_sym;
|
||||
self.prev_sym = prev_sym;
|
||||
self.reload_time = time::OffsetDateTime::now_local().ok();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub const COLOR_ROTATION: [Color; 7] = [
|
||||
Color::Magenta,
|
||||
Color::Cyan,
|
||||
Color::Green,
|
||||
Color::Red,
|
||||
Color::Yellow,
|
||||
Color::Blue,
|
||||
Color::Green,
|
||||
];
|
||||
|
||||
pub fn match_percent_color(match_percent: f32) -> Color {
|
||||
if match_percent == 100.0 {
|
||||
Color::Green
|
||||
} else if match_percent >= 50.0 {
|
||||
Color::LightBlue
|
||||
} else {
|
||||
Color::LightRed
|
||||
}
|
||||
}
|
||||
2
objdiff-cli/src/cmd/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod diff;
|
||||
pub mod report;
|
||||
426
objdiff-cli/src/cmd/report.rs
Normal file
@@ -0,0 +1,426 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs::File,
|
||||
io::Read,
|
||||
path::{Path, PathBuf},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use argp::FromArgs;
|
||||
use objdiff_core::{
|
||||
bindings::report::{
|
||||
ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Measures, Report,
|
||||
ReportCategory, ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata,
|
||||
REPORT_VERSION,
|
||||
},
|
||||
config::ProjectObject,
|
||||
diff, obj,
|
||||
obj::{ObjSectionKind, ObjSymbolFlags},
|
||||
};
|
||||
use prost::Message;
|
||||
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::util::output::{write_output, OutputFormat};
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// Generate a progress report for a project.
|
||||
#[argp(subcommand, name = "report")]
|
||||
pub struct Args {
|
||||
#[argp(subcommand)]
|
||||
command: SubCommand,
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
#[argp(subcommand)]
|
||||
pub enum SubCommand {
|
||||
Generate(GenerateArgs),
|
||||
Changes(ChangesArgs),
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// Generate a progress report for a project.
|
||||
#[argp(subcommand, name = "generate")]
|
||||
pub struct GenerateArgs {
|
||||
#[argp(option, short = 'p')]
|
||||
/// Project directory
|
||||
project: Option<PathBuf>,
|
||||
#[argp(option, short = 'o')]
|
||||
/// Output file
|
||||
output: Option<PathBuf>,
|
||||
#[argp(switch, short = 'd')]
|
||||
/// Deduplicate global and weak symbols (runs single-threaded)
|
||||
deduplicate: bool,
|
||||
#[argp(option, short = 'f')]
|
||||
/// Output format (json, json-pretty, proto) (default: json)
|
||||
format: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// List any changes from a previous report.
|
||||
#[argp(subcommand, name = "changes")]
|
||||
pub struct ChangesArgs {
|
||||
#[argp(positional)]
|
||||
/// Previous report file
|
||||
previous: PathBuf,
|
||||
#[argp(positional)]
|
||||
/// Current report file
|
||||
current: PathBuf,
|
||||
#[argp(option, short = 'o')]
|
||||
/// Output file
|
||||
output: Option<PathBuf>,
|
||||
#[argp(option, short = 'f')]
|
||||
/// Output format (json, json-pretty, proto) (default: json)
|
||||
format: Option<String>,
|
||||
}
|
||||
|
||||
pub fn run(args: Args) -> Result<()> {
|
||||
match args.command {
|
||||
SubCommand::Generate(args) => generate(args),
|
||||
SubCommand::Changes(args) => changes(args),
|
||||
}
|
||||
}
|
||||
|
||||
fn generate(args: GenerateArgs) -> Result<()> {
|
||||
let output_format = OutputFormat::from_option(args.format.as_deref())?;
|
||||
let project_dir = args.project.as_deref().unwrap_or_else(|| Path::new("."));
|
||||
info!("Loading project {}", project_dir.display());
|
||||
|
||||
let mut project = match objdiff_core::config::try_project_config(project_dir) {
|
||||
Some((Ok(config), _)) => config,
|
||||
Some((Err(err), _)) => bail!("Failed to load project configuration: {}", err),
|
||||
None => bail!("No project configuration found"),
|
||||
};
|
||||
info!(
|
||||
"Generating report for {} units (using {} threads)",
|
||||
project.units().len(),
|
||||
if args.deduplicate { 1 } else { rayon::current_num_threads() }
|
||||
);
|
||||
|
||||
let start = Instant::now();
|
||||
let mut units = vec![];
|
||||
let mut existing_functions: HashSet<String> = HashSet::new();
|
||||
if args.deduplicate {
|
||||
// If deduplicating, we need to run single-threaded
|
||||
for object in project.units.as_deref_mut().unwrap_or_default() {
|
||||
if let Some(unit) = report_object(
|
||||
object,
|
||||
project_dir,
|
||||
project.target_dir.as_deref(),
|
||||
project.base_dir.as_deref(),
|
||||
Some(&mut existing_functions),
|
||||
)? {
|
||||
units.push(unit);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let vec = project
|
||||
.units
|
||||
.as_deref_mut()
|
||||
.unwrap_or_default()
|
||||
.par_iter_mut()
|
||||
.map(|object| {
|
||||
report_object(
|
||||
object,
|
||||
project_dir,
|
||||
project.target_dir.as_deref(),
|
||||
project.base_dir.as_deref(),
|
||||
None,
|
||||
)
|
||||
})
|
||||
.collect::<Result<Vec<Option<ReportUnit>>>>()?;
|
||||
units = vec.into_iter().flatten().collect();
|
||||
}
|
||||
let measures = units.iter().flat_map(|u| u.measures.into_iter()).collect();
|
||||
let mut categories = Vec::new();
|
||||
for category in project.progress_categories() {
|
||||
categories.push(ReportCategory {
|
||||
id: category.id.clone(),
|
||||
name: category.name.clone(),
|
||||
measures: Some(Default::default()),
|
||||
});
|
||||
}
|
||||
let mut report =
|
||||
Report { measures: Some(measures), units, version: REPORT_VERSION, categories };
|
||||
report.calculate_progress_categories();
|
||||
let duration = start.elapsed();
|
||||
info!("Report generated in {}.{:03}s", duration.as_secs(), duration.subsec_millis());
|
||||
write_output(&report, args.output.as_deref(), output_format)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn report_object(
|
||||
object: &mut ProjectObject,
|
||||
project_dir: &Path,
|
||||
target_dir: Option<&Path>,
|
||||
base_dir: Option<&Path>,
|
||||
mut existing_functions: Option<&mut HashSet<String>>,
|
||||
) -> Result<Option<ReportUnit>> {
|
||||
object.resolve_paths(project_dir, target_dir, base_dir);
|
||||
match (&object.target_path, &object.base_path) {
|
||||
(None, Some(_)) if !object.complete().unwrap_or(false) => {
|
||||
warn!("Skipping object without target: {}", object.name());
|
||||
return Ok(None);
|
||||
}
|
||||
(None, None) => {
|
||||
warn!("Skipping object without target or base: {}", object.name());
|
||||
return Ok(None);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let config = diff::DiffObjConfig { relax_reloc_diffs: true, ..Default::default() };
|
||||
let target = object
|
||||
.target_path
|
||||
.as_ref()
|
||||
.map(|p| {
|
||||
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
|
||||
})
|
||||
.transpose()?;
|
||||
let base = object
|
||||
.base_path
|
||||
.as_ref()
|
||||
.map(|p| {
|
||||
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
|
||||
})
|
||||
.transpose()?;
|
||||
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?;
|
||||
|
||||
let metadata = ReportUnitMetadata {
|
||||
complete: object.complete(),
|
||||
module_name: target
|
||||
.as_ref()
|
||||
.and_then(|o| o.split_meta.as_ref())
|
||||
.and_then(|m| m.module_name.clone()),
|
||||
module_id: target.as_ref().and_then(|o| o.split_meta.as_ref()).and_then(|m| m.module_id),
|
||||
source_path: object.metadata.as_ref().and_then(|m| m.source_path.clone()),
|
||||
progress_categories: object
|
||||
.metadata
|
||||
.as_ref()
|
||||
.and_then(|m| m.progress_categories.clone())
|
||||
.unwrap_or_default(),
|
||||
auto_generated: object.metadata.as_ref().and_then(|m| m.auto_generated),
|
||||
};
|
||||
let mut measures = Measures { total_units: 1, ..Default::default() };
|
||||
let mut sections = vec![];
|
||||
let mut functions = vec![];
|
||||
|
||||
let obj = target.as_ref().or(base.as_ref()).unwrap();
|
||||
let obj_diff = result.left.as_ref().or(result.right.as_ref()).unwrap();
|
||||
for (section, section_diff) in obj.sections.iter().zip(&obj_diff.sections) {
|
||||
let section_match_percent = section_diff.match_percent.unwrap_or_else(|| {
|
||||
// Support cases where we don't have a target object,
|
||||
// assume complete means 100% match
|
||||
if object.complete().unwrap_or(false) {
|
||||
100.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
});
|
||||
sections.push(ReportItem {
|
||||
name: section.name.clone(),
|
||||
fuzzy_match_percent: section_match_percent,
|
||||
size: section.size,
|
||||
metadata: Some(ReportItemMetadata {
|
||||
demangled_name: None,
|
||||
virtual_address: section.virtual_address,
|
||||
}),
|
||||
});
|
||||
|
||||
match section.kind {
|
||||
ObjSectionKind::Data | ObjSectionKind::Bss => {
|
||||
measures.total_data += section.size;
|
||||
if section_match_percent == 100.0 {
|
||||
measures.matched_data += section.size;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
ObjSectionKind::Code => (),
|
||||
}
|
||||
|
||||
for (symbol, symbol_diff) in section.symbols.iter().zip(§ion_diff.symbols) {
|
||||
if symbol.size == 0 || symbol.flags.0.contains(ObjSymbolFlags::Hidden) {
|
||||
continue;
|
||||
}
|
||||
if let Some(existing_functions) = &mut existing_functions {
|
||||
if (symbol.flags.0.contains(ObjSymbolFlags::Global)
|
||||
|| symbol.flags.0.contains(ObjSymbolFlags::Weak))
|
||||
&& !existing_functions.insert(symbol.name.clone())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let match_percent = symbol_diff.match_percent.unwrap_or_else(|| {
|
||||
// Support cases where we don't have a target object,
|
||||
// assume complete means 100% match
|
||||
if object.complete().unwrap_or(false) {
|
||||
100.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
});
|
||||
measures.fuzzy_match_percent += match_percent * symbol.size as f32;
|
||||
measures.total_code += symbol.size;
|
||||
if match_percent == 100.0 {
|
||||
measures.matched_code += symbol.size;
|
||||
}
|
||||
functions.push(ReportItem {
|
||||
name: symbol.name.clone(),
|
||||
size: symbol.size,
|
||||
fuzzy_match_percent: match_percent,
|
||||
metadata: Some(ReportItemMetadata {
|
||||
demangled_name: symbol.demangled_name.clone(),
|
||||
virtual_address: symbol.virtual_address,
|
||||
}),
|
||||
});
|
||||
if match_percent == 100.0 {
|
||||
measures.matched_functions += 1;
|
||||
}
|
||||
measures.total_functions += 1;
|
||||
}
|
||||
}
|
||||
if metadata.complete.unwrap_or(false) {
|
||||
measures.complete_code = measures.total_code;
|
||||
measures.complete_data = measures.total_data;
|
||||
measures.complete_units = 1;
|
||||
}
|
||||
measures.calc_fuzzy_match_percent();
|
||||
measures.calc_matched_percent();
|
||||
Ok(Some(ReportUnit {
|
||||
name: object.name().to_string(),
|
||||
measures: Some(measures),
|
||||
sections,
|
||||
functions,
|
||||
metadata: Some(metadata),
|
||||
}))
|
||||
}
|
||||
|
||||
fn changes(args: ChangesArgs) -> Result<()> {
|
||||
let output_format = OutputFormat::from_option(args.format.as_deref())?;
|
||||
let (previous, current) = if args.previous == Path::new("-") && args.current == Path::new("-") {
|
||||
// Special case for comparing two reports from stdin
|
||||
let mut data = vec![];
|
||||
std::io::stdin().read_to_end(&mut data)?;
|
||||
let input = ChangesInput::decode(data.as_slice())?;
|
||||
(input.from.unwrap(), input.to.unwrap())
|
||||
} else {
|
||||
let previous = read_report(&args.previous)?;
|
||||
let current = read_report(&args.current)?;
|
||||
(previous, current)
|
||||
};
|
||||
let mut changes = Changes { from: previous.measures, to: current.measures, units: vec![] };
|
||||
for prev_unit in &previous.units {
|
||||
let curr_unit = current.units.iter().find(|u| u.name == prev_unit.name);
|
||||
let sections = process_items(prev_unit, curr_unit, |u| &u.sections);
|
||||
let functions = process_items(prev_unit, curr_unit, |u| &u.functions);
|
||||
|
||||
let prev_measures = prev_unit.measures;
|
||||
let curr_measures = curr_unit.and_then(|u| u.measures);
|
||||
if !functions.is_empty() || prev_measures != curr_measures {
|
||||
changes.units.push(ChangeUnit {
|
||||
name: prev_unit.name.clone(),
|
||||
from: prev_measures,
|
||||
to: curr_measures,
|
||||
sections,
|
||||
functions,
|
||||
metadata: curr_unit
|
||||
.as_ref()
|
||||
.and_then(|u| u.metadata.clone())
|
||||
.or_else(|| prev_unit.metadata.clone()),
|
||||
});
|
||||
}
|
||||
}
|
||||
for curr_unit in ¤t.units {
|
||||
if !previous.units.iter().any(|u| u.name == curr_unit.name) {
|
||||
changes.units.push(ChangeUnit {
|
||||
name: curr_unit.name.clone(),
|
||||
from: None,
|
||||
to: curr_unit.measures,
|
||||
sections: process_new_items(&curr_unit.sections),
|
||||
functions: process_new_items(&curr_unit.functions),
|
||||
metadata: curr_unit.metadata.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
write_output(&changes, args.output.as_deref(), output_format)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_items<F: Fn(&ReportUnit) -> &Vec<ReportItem>>(
|
||||
prev_unit: &ReportUnit,
|
||||
curr_unit: Option<&ReportUnit>,
|
||||
getter: F,
|
||||
) -> Vec<ChangeItem> {
|
||||
let prev_items = getter(prev_unit);
|
||||
let mut items = vec![];
|
||||
if let Some(curr_unit) = curr_unit {
|
||||
let curr_items = getter(curr_unit);
|
||||
for prev_func in prev_items {
|
||||
let prev_func_info = ChangeItemInfo::from(prev_func);
|
||||
let curr_func = curr_items.iter().find(|f| f.name == prev_func.name);
|
||||
let curr_func_info = curr_func.map(ChangeItemInfo::from);
|
||||
if let Some(curr_func_info) = curr_func_info {
|
||||
if prev_func_info != curr_func_info {
|
||||
items.push(ChangeItem {
|
||||
name: prev_func.name.clone(),
|
||||
from: Some(prev_func_info),
|
||||
to: Some(curr_func_info),
|
||||
metadata: curr_func.as_ref().unwrap().metadata.clone(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
items.push(ChangeItem {
|
||||
name: prev_func.name.clone(),
|
||||
from: Some(prev_func_info),
|
||||
to: None,
|
||||
metadata: prev_func.metadata.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
for curr_func in curr_items {
|
||||
if !prev_items.iter().any(|f| f.name == curr_func.name) {
|
||||
items.push(ChangeItem {
|
||||
name: curr_func.name.clone(),
|
||||
from: None,
|
||||
to: Some(ChangeItemInfo::from(curr_func)),
|
||||
metadata: curr_func.metadata.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for prev_func in prev_items {
|
||||
items.push(ChangeItem {
|
||||
name: prev_func.name.clone(),
|
||||
from: Some(ChangeItemInfo::from(prev_func)),
|
||||
to: None,
|
||||
metadata: prev_func.metadata.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
items
|
||||
}
|
||||
|
||||
fn process_new_items(items: &[ReportItem]) -> Vec<ChangeItem> {
|
||||
items
|
||||
.iter()
|
||||
.map(|item| ChangeItem {
|
||||
name: item.name.clone(),
|
||||
from: None,
|
||||
to: Some(ChangeItemInfo::from(item)),
|
||||
metadata: item.metadata.clone(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn read_report(path: &Path) -> Result<Report> {
|
||||
if path == Path::new("-") {
|
||||
let mut data = vec![];
|
||||
std::io::stdin().read_to_end(&mut data)?;
|
||||
return Report::parse(&data).with_context(|| "Failed to load report from stdin");
|
||||
}
|
||||
let file = File::open(path).with_context(|| format!("Failed to open {}", path.display()))?;
|
||||
let mmap = unsafe { memmap2::Mmap::map(&file) }
|
||||
.with_context(|| format!("Failed to map {}", path.display()))?;
|
||||
Report::parse(mmap.as_ref())
|
||||
.with_context(|| format!("Failed to load report {}", path.display()))
|
||||
}
|
||||
147
objdiff-cli/src/main.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
mod argp_version;
|
||||
mod cmd;
|
||||
mod util;
|
||||
|
||||
// musl's allocator is very slow, so use mimalloc when targeting musl.
|
||||
// Otherwise, use the system allocator to avoid extra code size.
|
||||
#[cfg(target_env = "musl")]
|
||||
#[global_allocator]
|
||||
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
|
||||
use std::{env, ffi::OsStr, fmt::Display, path::PathBuf, str::FromStr};
|
||||
|
||||
use anyhow::{Error, Result};
|
||||
use argp::{FromArgValue, FromArgs};
|
||||
use enable_ansi_support::enable_ansi_support;
|
||||
use supports_color::Stream;
|
||||
use tracing_subscriber::{filter::LevelFilter, EnvFilter};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
enum LogLevel {
|
||||
Error,
|
||||
Warn,
|
||||
Info,
|
||||
Debug,
|
||||
Trace,
|
||||
}
|
||||
|
||||
impl FromStr for LogLevel {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match s {
|
||||
"error" => Self::Error,
|
||||
"warn" => Self::Warn,
|
||||
"info" => Self::Info,
|
||||
"debug" => Self::Debug,
|
||||
"trace" => Self::Trace,
|
||||
_ => return Err(()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LogLevel {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
LogLevel::Error => "error",
|
||||
LogLevel::Warn => "warn",
|
||||
LogLevel::Info => "info",
|
||||
LogLevel::Debug => "debug",
|
||||
LogLevel::Trace => "trace",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromArgValue for LogLevel {
|
||||
fn from_arg_value(value: &OsStr) -> Result<Self, String> {
|
||||
String::from_arg_value(value)
|
||||
.and_then(|s| Self::from_str(&s).map_err(|_| "Invalid log level".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// A local diffing tool for decompilation projects.
|
||||
struct TopLevel {
|
||||
#[argp(subcommand)]
|
||||
command: SubCommand,
|
||||
#[argp(option, short = 'C')]
|
||||
/// Change working directory.
|
||||
chdir: Option<PathBuf>,
|
||||
#[argp(option, short = 'L')]
|
||||
/// Minimum logging level. (Default: info)
|
||||
/// Possible values: error, warn, info, debug, trace
|
||||
log_level: Option<LogLevel>,
|
||||
/// Print version information and exit.
|
||||
#[argp(switch, short = 'V')]
|
||||
version: bool,
|
||||
/// Disable color output. (env: NO_COLOR)
|
||||
#[argp(switch)]
|
||||
no_color: bool,
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
#[argp(subcommand)]
|
||||
enum SubCommand {
|
||||
Diff(cmd::diff::Args),
|
||||
Report(cmd::report::Args),
|
||||
}
|
||||
|
||||
// Duplicated from supports-color so we can check early.
|
||||
fn env_no_color() -> bool {
|
||||
match env::var("NO_COLOR").as_deref() {
|
||||
Ok("") | Ok("0") | Err(_) => false,
|
||||
Ok(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: TopLevel = argp_version::from_env();
|
||||
let use_colors = if args.no_color || env_no_color() {
|
||||
false
|
||||
} else {
|
||||
// Try to enable ANSI support on Windows.
|
||||
let _ = enable_ansi_support();
|
||||
// Disable isatty check for supports-color. (e.g. when used with ninja)
|
||||
unsafe { env::set_var("IGNORE_IS_TERMINAL", "1") };
|
||||
supports_color::on(Stream::Stdout).is_some_and(|c| c.has_basic)
|
||||
};
|
||||
|
||||
let format =
|
||||
tracing_subscriber::fmt::format().with_ansi(use_colors).with_target(false).without_time();
|
||||
let builder = tracing_subscriber::fmt().event_format(format).with_writer(std::io::stderr);
|
||||
if let Some(level) = args.log_level {
|
||||
builder
|
||||
.with_max_level(match level {
|
||||
LogLevel::Error => LevelFilter::ERROR,
|
||||
LogLevel::Warn => LevelFilter::WARN,
|
||||
LogLevel::Info => LevelFilter::INFO,
|
||||
LogLevel::Debug => LevelFilter::DEBUG,
|
||||
LogLevel::Trace => LevelFilter::TRACE,
|
||||
})
|
||||
.init();
|
||||
} else {
|
||||
builder
|
||||
.with_env_filter(
|
||||
EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.from_env_lossy(),
|
||||
)
|
||||
.init();
|
||||
}
|
||||
|
||||
let mut result = Ok(());
|
||||
if let Some(dir) = &args.chdir {
|
||||
result = env::set_current_dir(dir).map_err(|e| {
|
||||
Error::new(e)
|
||||
.context(format!("Failed to change working directory to '{}'", dir.display()))
|
||||
});
|
||||
}
|
||||
result = result.and_then(|_| match args.command {
|
||||
SubCommand::Diff(c_args) => cmd::diff::run(c_args),
|
||||
SubCommand::Report(c_args) => cmd::report::run(c_args),
|
||||
});
|
||||
if let Err(e) = result {
|
||||
eprintln!("Failed: {e:?}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
2
objdiff-cli/src/util/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod output;
|
||||
pub mod term;
|
||||
84
objdiff-cli/src/util/output.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufWriter, Write},
|
||||
ops::DerefMut,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
|
||||
pub enum OutputFormat {
|
||||
#[default]
|
||||
Json,
|
||||
JsonPretty,
|
||||
Proto,
|
||||
}
|
||||
|
||||
impl OutputFormat {
|
||||
pub fn from_str(s: &str) -> Result<Self> {
|
||||
match s.to_ascii_lowercase().as_str() {
|
||||
"json" => Ok(Self::Json),
|
||||
"json-pretty" | "json_pretty" => Ok(Self::JsonPretty),
|
||||
"binpb" | "pb" | "proto" | "protobuf" => Ok(Self::Proto),
|
||||
_ => bail!("Invalid output format: {}", s),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_option(s: Option<&str>) -> Result<Self> {
|
||||
match s {
|
||||
Some(s) => Self::from_str(s),
|
||||
None => Ok(Self::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_output<T>(input: &T, output: Option<&Path>, format: OutputFormat) -> Result<()>
|
||||
where T: serde::Serialize + prost::Message {
|
||||
match output {
|
||||
Some(output) if output != Path::new("-") => {
|
||||
info!("Writing to {}", output.display());
|
||||
let file = File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(output)
|
||||
.with_context(|| format!("Failed to create file {}", output.display()))?;
|
||||
match format {
|
||||
OutputFormat::Json => {
|
||||
let mut output = BufWriter::new(file);
|
||||
serde_json::to_writer(&mut output, input)
|
||||
.context("Failed to write output file")?;
|
||||
output.flush().context("Failed to flush output file")?;
|
||||
}
|
||||
OutputFormat::JsonPretty => {
|
||||
let mut output = BufWriter::new(file);
|
||||
serde_json::to_writer_pretty(&mut output, input)
|
||||
.context("Failed to write output file")?;
|
||||
output.flush().context("Failed to flush output file")?;
|
||||
}
|
||||
OutputFormat::Proto => {
|
||||
file.set_len(input.encoded_len() as u64)?;
|
||||
let map = unsafe { memmap2::Mmap::map(&file) }
|
||||
.context("Failed to map output file")?;
|
||||
let mut output = map.make_mut().context("Failed to remap output file")?;
|
||||
input.encode(&mut output.deref_mut()).context("Failed to encode output")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => match format {
|
||||
OutputFormat::Json => {
|
||||
serde_json::to_writer(std::io::stdout(), input)?;
|
||||
}
|
||||
OutputFormat::JsonPretty => {
|
||||
serde_json::to_writer_pretty(std::io::stdout(), input)?;
|
||||
}
|
||||
OutputFormat::Proto => {
|
||||
std::io::stdout().write_all(&input.encode_to_vec())?;
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
16
objdiff-cli/src/util/term.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use std::{io::stdout, panic};
|
||||
|
||||
use crossterm::{
|
||||
cursor::Show,
|
||||
event::DisableMouseCapture,
|
||||
terminal::{disable_raw_mode, LeaveAlternateScreen},
|
||||
};
|
||||
|
||||
pub fn crossterm_panic_handler() {
|
||||
let original_hook = panic::take_hook();
|
||||
panic::set_hook(Box::new(move |panic_info| {
|
||||
let _ = crossterm::execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture, Show);
|
||||
let _ = disable_raw_mode();
|
||||
original_hook(panic_info);
|
||||
}));
|
||||
}
|
||||
86
objdiff-core/Cargo.toml
Normal file
@@ -0,0 +1,86 @@
|
||||
[package]
|
||||
name = "objdiff-core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
readme = "README.md"
|
||||
description = """
|
||||
A local diffing tool for decompilation projects.
|
||||
"""
|
||||
documentation = "https://docs.rs/objdiff-core"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "arm64", "bindings"]
|
||||
any-arch = ["config", "dep:bimap", "dep:strum", "dep:similar", "dep:flagset", "dep:log", "dep:memmap2", "dep:byteorder", "dep:num-traits"] # Implicit, used to check if any arch is enabled
|
||||
config = ["dep:bimap", "dep:globset", "dep:semver", "dep:serde_json", "dep:serde_yaml", "dep:serde", "dep:filetime"]
|
||||
dwarf = ["dep:gimli"]
|
||||
mips = ["any-arch", "dep:rabbitizer"]
|
||||
ppc = ["any-arch", "dep:cwdemangle", "dep:cwextab", "dep:ppc750cl"]
|
||||
x86 = ["any-arch", "dep:cpp_demangle", "dep:iced-x86", "dep:msvc-demangler"]
|
||||
arm = ["any-arch", "dep:cpp_demangle", "dep:unarm", "dep:arm-attr"]
|
||||
arm64 = ["any-arch", "dep:cpp_demangle", "dep:yaxpeax-arch", "dep:yaxpeax-arm"]
|
||||
bindings = ["dep:serde_json", "dep:prost", "dep:pbjson", "dep:serde", "dep:prost-build", "dep:pbjson-build"]
|
||||
wasm = ["bindings", "any-arch", "dep:console_error_panic_hook", "dep:console_log", "dep:wasm-bindgen", "dep:tsify-next", "dep:log"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["all"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
bimap = { version = "0.6", features = ["serde"], optional = true }
|
||||
byteorder = { version = "1.5", optional = true }
|
||||
filetime = { version = "0.2", optional = true }
|
||||
flagset = { version = "0.4", optional = true }
|
||||
log = { version = "0.4", optional = true }
|
||||
memmap2 = { version = "0.9", optional = true }
|
||||
num-traits = { version = "0.2", optional = true }
|
||||
object = { version = "0.36", features = ["read_core", "std", "elf", "pe"], default-features = false }
|
||||
pbjson = { version = "0.7", optional = true }
|
||||
prost = { version = "0.13", optional = true }
|
||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
similar = { version = "2.6", default-features = false, optional = true }
|
||||
strum = { version = "0.26", features = ["derive"], optional = true }
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
tsify-next = { version = "0.5", default-features = false, features = ["js"], optional = true }
|
||||
console_log = { version = "1.0", optional = true }
|
||||
console_error_panic_hook = { version = "0.1", optional = true }
|
||||
|
||||
# config
|
||||
globset = { version = "0.4", features = ["serde1"], optional = true }
|
||||
semver = { version = "1.0", optional = true }
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
serde_yaml = { version = "0.9", optional = true }
|
||||
|
||||
# dwarf
|
||||
gimli = { version = "0.31", default-features = false, features = ["read-all"], optional = true }
|
||||
|
||||
# ppc
|
||||
cwdemangle = { version = "1.0", optional = true }
|
||||
cwextab = { version = "1.0.2", optional = true }
|
||||
ppc750cl = { version = "0.3", optional = true }
|
||||
|
||||
# mips
|
||||
rabbitizer = { version = "1.12", optional = true }
|
||||
|
||||
# x86
|
||||
cpp_demangle = { version = "0.4", optional = true }
|
||||
iced-x86 = { version = "1.21", default-features = false, features = ["std", "decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums"], optional = true }
|
||||
msvc-demangler = { version = "0.10", optional = true }
|
||||
|
||||
# arm
|
||||
unarm = { version = "1.6", optional = true }
|
||||
arm-attr = { version = "0.1", optional = true }
|
||||
|
||||
# arm64
|
||||
yaxpeax-arch = { version = "0.3", default-features = false, features = ["std"], optional = true }
|
||||
yaxpeax-arm = { version = "0.3", default-features = false, features = ["std"], optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
prost-build = { version = "0.13", optional = true }
|
||||
pbjson-build = { version = "0.7", optional = true }
|
||||
15
objdiff-core/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# objdiff-core
|
||||
|
||||
objdiff-core contains the core functionality of [objdiff](https://github.com/encounter/objdiff), a tool for comparing object files in decompilation projects. See the main repository for more information.
|
||||
|
||||
## Crate feature flags
|
||||
|
||||
- **`all`**: Enables all main features.
|
||||
- **`config`**: Enables objdiff configuration file support.
|
||||
- **`dwarf`**: Enables extraction of line number information from DWARF debug sections.
|
||||
- **`mips`**: Enables the MIPS backend powered by [rabbitizer](https://github.com/Decompollaborate/rabbitizer). (Note: C library with Rust bindings)
|
||||
- **`ppc`**: Enables the PowerPC backend powered by [ppc750cl](https://github.com/encounter/ppc750cl).
|
||||
- **`x86`**: Enables the x86 backend powered by [iced-x86](https://crates.io/crates/iced-x86).
|
||||
- **`arm`**: Enables the ARM backend powered by [unarm](https://github.com/AetiasHax/unarm).
|
||||
- **`arm64`**: Enables the ARM64 backend powered by [yaxpeax-arm](https://github.com/iximeow/yaxpeax-arm).
|
||||
- **`bindings`**: Enables serialization and deserialization of objdiff data structures.
|
||||
59
objdiff-core/build.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
fn main() {
|
||||
#[cfg(feature = "bindings")]
|
||||
compile_protos();
|
||||
}
|
||||
|
||||
#[cfg(feature = "bindings")]
|
||||
fn compile_protos() {
|
||||
use std::path::{Path, PathBuf};
|
||||
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("protos");
|
||||
let descriptor_path = root.join("proto_descriptor.bin");
|
||||
println!("cargo:rerun-if-changed={}", descriptor_path.display());
|
||||
let descriptor_mtime = std::fs::metadata(&descriptor_path)
|
||||
.map(|m| m.modified().unwrap())
|
||||
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
|
||||
let mut run_protoc = false;
|
||||
let proto_files = vec![root.join("diff.proto"), root.join("report.proto")];
|
||||
for proto_file in &proto_files {
|
||||
println!("cargo:rerun-if-changed={}", proto_file.display());
|
||||
let mtime = match std::fs::metadata(proto_file) {
|
||||
Ok(m) => m.modified().unwrap(),
|
||||
Err(e) => panic!("Failed to stat proto file {}: {:?}", proto_file.display(), e),
|
||||
};
|
||||
if mtime > descriptor_mtime {
|
||||
run_protoc = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn prost_config(descriptor_path: &Path, run_protoc: bool) -> prost_build::Config {
|
||||
let mut config = prost_build::Config::new();
|
||||
config.file_descriptor_set_path(descriptor_path);
|
||||
// If our cached descriptor is up-to-date, we don't need to run protoc.
|
||||
// This is helpful so that users don't need to have protoc installed
|
||||
// unless they're updating the protos.
|
||||
if !run_protoc {
|
||||
config.skip_protoc_run();
|
||||
}
|
||||
config
|
||||
}
|
||||
if let Err(e) =
|
||||
prost_config(&descriptor_path, run_protoc).compile_protos(&proto_files, &[root.as_path()])
|
||||
{
|
||||
if e.kind() == std::io::ErrorKind::NotFound && e.to_string().contains("protoc") {
|
||||
eprintln!("protoc not found, skipping protobuf compilation");
|
||||
prost_config(&descriptor_path, false)
|
||||
.compile_protos(&proto_files, &[root.as_path()])
|
||||
.expect("Failed to compile protos");
|
||||
} else {
|
||||
panic!("Failed to compile protos: {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let descriptor_set = std::fs::read(descriptor_path).expect("Failed to read descriptor set");
|
||||
pbjson_build::Builder::new()
|
||||
.register_descriptors(&descriptor_set)
|
||||
.expect("Failed to register descriptors")
|
||||
.preserve_proto_field_names()
|
||||
.build(&[".objdiff"])
|
||||
.expect("Failed to build pbjson");
|
||||
}
|
||||
163
objdiff-core/protos/diff.proto
Normal file
@@ -0,0 +1,163 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package objdiff.diff;
|
||||
|
||||
// A symbol
|
||||
message Symbol {
|
||||
// Name of the symbol
|
||||
string name = 1;
|
||||
// Demangled name of the symbol
|
||||
optional string demangled_name = 2;
|
||||
// Symbol address
|
||||
uint64 address = 3;
|
||||
// Symbol size
|
||||
uint64 size = 4;
|
||||
// Bitmask of SymbolFlag
|
||||
uint32 flags = 5;
|
||||
}
|
||||
|
||||
// Symbol visibility flags
|
||||
enum SymbolFlag {
|
||||
SYMBOL_NONE = 0;
|
||||
SYMBOL_GLOBAL = 1;
|
||||
SYMBOL_LOCAL = 2;
|
||||
SYMBOL_WEAK = 3;
|
||||
SYMBOL_COMMON = 4;
|
||||
SYMBOL_HIDDEN = 5;
|
||||
}
|
||||
|
||||
// A single parsed instruction
|
||||
message Instruction {
|
||||
// Instruction address
|
||||
uint64 address = 1;
|
||||
// Instruction size
|
||||
uint32 size = 2;
|
||||
// Instruction opcode
|
||||
uint32 opcode = 3;
|
||||
// Instruction mnemonic
|
||||
string mnemonic = 4;
|
||||
// Instruction formatted string
|
||||
string formatted = 5;
|
||||
// Original (unsimplified) instruction string
|
||||
optional string original = 6;
|
||||
// Instruction arguments
|
||||
repeated Argument arguments = 7;
|
||||
// Instruction relocation
|
||||
optional Relocation relocation = 8;
|
||||
// Instruction branch destination
|
||||
optional uint64 branch_dest = 9;
|
||||
// Instruction line number
|
||||
optional uint32 line_number = 10;
|
||||
}
|
||||
|
||||
// An instruction argument
|
||||
message Argument {
|
||||
oneof value {
|
||||
// Plain text
|
||||
string plain_text = 1;
|
||||
// Value
|
||||
ArgumentValue argument = 2;
|
||||
// Relocation
|
||||
ArgumentRelocation relocation = 3;
|
||||
// Branch destination
|
||||
uint64 branch_dest = 4;
|
||||
}
|
||||
}
|
||||
|
||||
// An instruction argument value
|
||||
message ArgumentValue {
|
||||
oneof value {
|
||||
// Signed integer
|
||||
int64 signed = 1;
|
||||
// Unsigned integer
|
||||
uint64 unsigned = 2;
|
||||
// Opaque value
|
||||
string opaque = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// Marker type for relocation arguments
|
||||
message ArgumentRelocation {
|
||||
}
|
||||
|
||||
message Relocation {
|
||||
uint32 type = 1;
|
||||
string type_name = 2;
|
||||
RelocationTarget target = 3;
|
||||
}
|
||||
|
||||
message RelocationTarget {
|
||||
Symbol symbol = 1;
|
||||
int64 addend = 2;
|
||||
}
|
||||
|
||||
message InstructionDiff {
|
||||
DiffKind diff_kind = 1;
|
||||
optional Instruction instruction = 2;
|
||||
optional InstructionBranchFrom branch_from = 3;
|
||||
optional InstructionBranchTo branch_to = 4;
|
||||
repeated ArgumentDiff arg_diff = 5;
|
||||
}
|
||||
|
||||
message ArgumentDiff {
|
||||
optional uint32 diff_index = 1;
|
||||
}
|
||||
|
||||
enum DiffKind {
|
||||
DIFF_NONE = 0;
|
||||
DIFF_REPLACE = 1;
|
||||
DIFF_DELETE = 2;
|
||||
DIFF_INSERT = 3;
|
||||
DIFF_OP_MISMATCH = 4;
|
||||
DIFF_ARG_MISMATCH = 5;
|
||||
}
|
||||
|
||||
message InstructionBranchFrom {
|
||||
repeated uint32 instruction_index = 1;
|
||||
uint32 branch_index = 2;
|
||||
}
|
||||
|
||||
message InstructionBranchTo {
|
||||
uint32 instruction_index = 1;
|
||||
uint32 branch_index = 2;
|
||||
}
|
||||
|
||||
message FunctionDiff {
|
||||
Symbol symbol = 1;
|
||||
repeated InstructionDiff instructions = 2;
|
||||
optional float match_percent = 3;
|
||||
}
|
||||
|
||||
message DataDiff {
|
||||
DiffKind kind = 1;
|
||||
bytes data = 2;
|
||||
// May be larger than data
|
||||
uint64 size = 3;
|
||||
}
|
||||
|
||||
message SectionDiff {
|
||||
string name = 1;
|
||||
SectionKind kind = 2;
|
||||
uint64 size = 3;
|
||||
uint64 address = 4;
|
||||
repeated FunctionDiff functions = 5;
|
||||
repeated DataDiff data = 6;
|
||||
optional float match_percent = 7;
|
||||
}
|
||||
|
||||
enum SectionKind {
|
||||
SECTION_UNKNOWN = 0;
|
||||
SECTION_TEXT = 1;
|
||||
SECTION_DATA = 2;
|
||||
SECTION_BSS = 3;
|
||||
SECTION_COMMON = 4;
|
||||
}
|
||||
|
||||
message ObjectDiff {
|
||||
repeated SectionDiff sections = 1;
|
||||
}
|
||||
|
||||
message DiffResult {
|
||||
optional ObjectDiff left = 1;
|
||||
optional ObjectDiff right = 2;
|
||||
}
|
||||
BIN
objdiff-core/protos/proto_descriptor.bin
Normal file
164
objdiff-core/protos/report.proto
Normal file
@@ -0,0 +1,164 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package objdiff.report;
|
||||
|
||||
// Progress info for a report or unit
|
||||
message Measures {
|
||||
// Overall match percent, including partially matched functions and data
|
||||
float fuzzy_match_percent = 1;
|
||||
// Total size of code in bytes
|
||||
uint64 total_code = 2;
|
||||
// Fully matched code size in bytes
|
||||
uint64 matched_code = 3;
|
||||
// Fully matched code percent
|
||||
float matched_code_percent = 4;
|
||||
// Total size of data in bytes
|
||||
uint64 total_data = 5;
|
||||
// Fully matched data size in bytes
|
||||
uint64 matched_data = 6;
|
||||
// Fully matched data percent
|
||||
float matched_data_percent = 7;
|
||||
// Total number of functions
|
||||
uint32 total_functions = 8;
|
||||
// Fully matched functions
|
||||
uint32 matched_functions = 9;
|
||||
// Fully matched functions percent
|
||||
float matched_functions_percent = 10;
|
||||
// Completed (or "linked") code size in bytes
|
||||
uint64 complete_code = 11;
|
||||
// Completed (or "linked") code percent
|
||||
float complete_code_percent = 12;
|
||||
// Completed (or "linked") data size in bytes
|
||||
uint64 complete_data = 13;
|
||||
// Completed (or "linked") data percent
|
||||
float complete_data_percent = 14;
|
||||
// Total number of units
|
||||
uint32 total_units = 15;
|
||||
// Completed (or "linked") units
|
||||
uint32 complete_units = 16;
|
||||
}
|
||||
|
||||
// Project progress report
|
||||
message Report {
|
||||
// Overall progress info
|
||||
Measures measures = 1;
|
||||
// Units within this report
|
||||
repeated ReportUnit units = 2;
|
||||
// Report version
|
||||
uint32 version = 3;
|
||||
// Progress categories
|
||||
repeated ReportCategory categories = 4;
|
||||
}
|
||||
|
||||
message ReportCategory {
|
||||
// The ID of the category
|
||||
string id = 1;
|
||||
// The name of the category
|
||||
string name = 2;
|
||||
// Progress info for this category
|
||||
Measures measures = 3;
|
||||
}
|
||||
|
||||
// A unit of the report (usually a translation unit)
|
||||
message ReportUnit {
|
||||
// The name of the unit
|
||||
string name = 1;
|
||||
// Progress info for this unit
|
||||
Measures measures = 2;
|
||||
// Sections within this unit
|
||||
repeated ReportItem sections = 3;
|
||||
// Functions within this unit
|
||||
repeated ReportItem functions = 4;
|
||||
// Extra metadata for this unit
|
||||
optional ReportUnitMetadata metadata = 5;
|
||||
}
|
||||
|
||||
// Extra metadata for a unit
|
||||
message ReportUnitMetadata {
|
||||
// Whether this unit is marked as complete (or "linked")
|
||||
optional bool complete = 1;
|
||||
// The name of the module this unit belongs to
|
||||
optional string module_name = 2;
|
||||
// The ID of the module this unit belongs to
|
||||
optional uint32 module_id = 3;
|
||||
// The path to the source file of this unit
|
||||
optional string source_path = 4;
|
||||
// Progress categories for this unit
|
||||
repeated string progress_categories = 5;
|
||||
// Whether this unit is automatically generated (not user-provided)
|
||||
optional bool auto_generated = 6;
|
||||
}
|
||||
|
||||
// A section or function within a unit
|
||||
message ReportItem {
|
||||
// The name of the item
|
||||
string name = 1;
|
||||
// The size of the item in bytes
|
||||
uint64 size = 2;
|
||||
// The overall match percent for this item
|
||||
float fuzzy_match_percent = 3;
|
||||
// Extra metadata for this item
|
||||
optional ReportItemMetadata metadata = 4;
|
||||
}
|
||||
|
||||
// Extra metadata for an item
|
||||
message ReportItemMetadata {
|
||||
// The demangled name of the function
|
||||
optional string demangled_name = 1;
|
||||
// The virtual address of the function or section
|
||||
optional uint64 virtual_address = 2;
|
||||
}
|
||||
|
||||
// A pair of reports to compare and generate changes
|
||||
message ChangesInput {
|
||||
// The previous report
|
||||
Report from = 1;
|
||||
// The current report
|
||||
Report to = 2;
|
||||
}
|
||||
|
||||
// Changes between two reports
|
||||
message Changes {
|
||||
// The progress info for the previous report
|
||||
Measures from = 1;
|
||||
// The progress info for the current report
|
||||
Measures to = 2;
|
||||
// Units that changed
|
||||
repeated ChangeUnit units = 3;
|
||||
}
|
||||
|
||||
// A changed unit
|
||||
message ChangeUnit {
|
||||
// The name of the unit
|
||||
string name = 1;
|
||||
// The previous progress info (omitted if new)
|
||||
optional Measures from = 2;
|
||||
// The current progress info (omitted if removed)
|
||||
optional Measures to = 3;
|
||||
// Sections that changed
|
||||
repeated ChangeItem sections = 4;
|
||||
// Functions that changed
|
||||
repeated ChangeItem functions = 5;
|
||||
// Extra metadata for this unit
|
||||
optional ReportUnitMetadata metadata = 6;
|
||||
}
|
||||
|
||||
// A changed section or function
|
||||
message ChangeItem {
|
||||
// The name of the item
|
||||
string name = 1;
|
||||
// The previous progress info (omitted if new)
|
||||
optional ChangeItemInfo from = 2;
|
||||
// The current progress info (omitted if removed)
|
||||
optional ChangeItemInfo to = 3;
|
||||
// Extra metadata for this item
|
||||
optional ReportItemMetadata metadata = 4;
|
||||
}
|
||||
|
||||
// Progress info for a section or function
|
||||
message ChangeItemInfo {
|
||||
// The overall match percent for this item
|
||||
float fuzzy_match_percent = 1;
|
||||
// The size of the item in bytes
|
||||
uint64 size = 2;
|
||||
}
|
||||
441
objdiff-core/src/arch/arm.rs
Normal file
@@ -0,0 +1,441 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{BTreeMap, HashMap},
|
||||
};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use arm_attr::{enums::CpuArch, tag::Tag, BuildAttrs};
|
||||
use object::{
|
||||
elf::{self, SHT_ARM_ATTRIBUTES},
|
||||
Endian, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, SectionIndex,
|
||||
SectionKind, Symbol, SymbolKind,
|
||||
};
|
||||
use unarm::{
|
||||
args::{Argument, OffsetImm, OffsetReg, Register},
|
||||
parse::{ArmVersion, ParseMode, Parser},
|
||||
DisplayOptions, ParseFlags, ParsedIns, RegNames,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
arch::{ObjArch, ProcessCodeResult},
|
||||
diff::{ArmArchVersion, ArmR9Usage, DiffObjConfig},
|
||||
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection},
|
||||
};
|
||||
|
||||
pub struct ObjArchArm {
|
||||
/// Maps section index, to list of disasm modes (arm, thumb or data) sorted by address
|
||||
disasm_modes: HashMap<SectionIndex, Vec<DisasmMode>>,
|
||||
detected_version: Option<ArmVersion>,
|
||||
endianness: object::Endianness,
|
||||
}
|
||||
|
||||
impl ObjArchArm {
|
||||
pub fn new(file: &File) -> Result<Self> {
|
||||
let endianness = file.endianness();
|
||||
match file {
|
||||
File::Elf32(_) => {
|
||||
let disasm_modes = Self::elf_get_mapping_symbols(file);
|
||||
let detected_version = Self::elf_detect_arm_version(file)?;
|
||||
Ok(Self { disasm_modes, detected_version, endianness })
|
||||
}
|
||||
_ => bail!("Unsupported file format {:?}", file.format()),
|
||||
}
|
||||
}
|
||||
|
||||
fn elf_detect_arm_version(file: &File) -> Result<Option<ArmVersion>> {
|
||||
// Check ARM attributes
|
||||
if let Some(arm_attrs) = file.sections().find(|s| {
|
||||
s.kind() == SectionKind::Elf(SHT_ARM_ATTRIBUTES) && s.name() == Ok(".ARM.attributes")
|
||||
}) {
|
||||
let attr_data = arm_attrs.uncompressed_data()?;
|
||||
let build_attrs = BuildAttrs::new(&attr_data, match file.endianness() {
|
||||
object::Endianness::Little => arm_attr::Endian::Little,
|
||||
object::Endianness::Big => arm_attr::Endian::Big,
|
||||
})?;
|
||||
for subsection in build_attrs.subsections() {
|
||||
let subsection = subsection?;
|
||||
if !subsection.is_aeabi() {
|
||||
continue;
|
||||
}
|
||||
// Only checking first CpuArch tag. Others may exist, but that's very unlikely.
|
||||
let cpu_arch = subsection.into_public_tag_iter()?.find_map(|(_, tag)| {
|
||||
if let Tag::CpuArch(cpu_arch) = tag {
|
||||
Some(cpu_arch)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
match cpu_arch {
|
||||
Some(CpuArch::V4T) => return Ok(Some(ArmVersion::V4T)),
|
||||
Some(CpuArch::V5TE) => return Ok(Some(ArmVersion::V5Te)),
|
||||
Some(CpuArch::V6K) => return Ok(Some(ArmVersion::V6K)),
|
||||
Some(arch) => bail!("ARM arch {} not supported", arch),
|
||||
None => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn elf_get_mapping_symbols(file: &File) -> HashMap<SectionIndex, Vec<DisasmMode>> {
|
||||
file.sections()
|
||||
.filter(|s| s.kind() == SectionKind::Text)
|
||||
.map(|s| {
|
||||
let index = s.index();
|
||||
let mut mapping_symbols: Vec<_> = file
|
||||
.symbols()
|
||||
.filter(|s| s.section_index().map(|i| i == index).unwrap_or(false))
|
||||
.filter_map(|s| DisasmMode::from_symbol(&s))
|
||||
.collect();
|
||||
mapping_symbols.sort_unstable_by_key(|x| x.address);
|
||||
(s.index(), mapping_symbols)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjArch for ObjArchArm {
|
||||
fn symbol_address(&self, symbol: &Symbol) -> u64 {
|
||||
let address = symbol.address();
|
||||
if symbol.kind() == SymbolKind::Text {
|
||||
address & !1
|
||||
} else {
|
||||
address
|
||||
}
|
||||
}
|
||||
|
||||
fn process_code(
|
||||
&self,
|
||||
address: u64,
|
||||
code: &[u8],
|
||||
section_index: usize,
|
||||
relocations: &[ObjReloc],
|
||||
line_info: &BTreeMap<u64, u32>,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<ProcessCodeResult> {
|
||||
let start_addr = address as u32;
|
||||
let end_addr = start_addr + code.len() as u32;
|
||||
|
||||
// Mapping symbols decide what kind of data comes after it. $a for ARM code, $t for Thumb code and $d for data.
|
||||
let fallback_mappings = [DisasmMode { address: start_addr, mapping: ParseMode::Arm }];
|
||||
let mapping_symbols = self
|
||||
.disasm_modes
|
||||
.get(&SectionIndex(section_index))
|
||||
.map(|x| x.as_slice())
|
||||
.unwrap_or(&fallback_mappings);
|
||||
let first_mapping_idx = mapping_symbols
|
||||
.binary_search_by_key(&start_addr, |x| x.address)
|
||||
.unwrap_or_else(|idx| idx - 1);
|
||||
let first_mapping = mapping_symbols[first_mapping_idx].mapping;
|
||||
|
||||
let mut mappings_iter =
|
||||
mapping_symbols.iter().skip(first_mapping_idx + 1).take_while(|x| x.address < end_addr);
|
||||
let mut next_mapping = mappings_iter.next();
|
||||
|
||||
let ins_count = code.len() / first_mapping.instruction_size(start_addr);
|
||||
let mut ops = Vec::<u16>::with_capacity(ins_count);
|
||||
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
|
||||
|
||||
let version = match config.arm_arch_version {
|
||||
ArmArchVersion::Auto => self.detected_version.unwrap_or(ArmVersion::V5Te),
|
||||
ArmArchVersion::V4T => ArmVersion::V4T,
|
||||
ArmArchVersion::V5TE => ArmVersion::V5Te,
|
||||
ArmArchVersion::V6K => ArmVersion::V6K,
|
||||
};
|
||||
let endian = match self.endianness {
|
||||
object::Endianness::Little => unarm::Endian::Little,
|
||||
object::Endianness::Big => unarm::Endian::Big,
|
||||
};
|
||||
|
||||
let parse_flags = ParseFlags { ual: config.arm_unified_syntax, version };
|
||||
|
||||
let mut parser = Parser::new(first_mapping, start_addr, endian, parse_flags, code);
|
||||
|
||||
let display_options = DisplayOptions {
|
||||
reg_names: RegNames {
|
||||
av_registers: config.arm_av_registers,
|
||||
r9_use: match config.arm_r9_usage {
|
||||
ArmR9Usage::GeneralPurpose => unarm::R9Use::GeneralPurpose,
|
||||
ArmR9Usage::Sb => unarm::R9Use::Pid,
|
||||
ArmR9Usage::Tr => unarm::R9Use::Tls,
|
||||
},
|
||||
explicit_stack_limit: config.arm_sl_usage,
|
||||
frame_pointer: config.arm_fp_usage,
|
||||
ip: config.arm_ip_usage,
|
||||
},
|
||||
};
|
||||
|
||||
while let Some((address, ins, parsed_ins)) = parser.next() {
|
||||
if let Some(next) = next_mapping {
|
||||
let next_address = parser.address;
|
||||
if next_address >= next.address {
|
||||
// Change mapping
|
||||
parser.mode = next.mapping;
|
||||
next_mapping = mappings_iter.next();
|
||||
}
|
||||
}
|
||||
let line = line_info.range(..=address as u64).last().map(|(_, &b)| b);
|
||||
|
||||
let reloc = relocations.iter().find(|r| (r.address as u32 & !1) == address).cloned();
|
||||
|
||||
let mut reloc_arg = None;
|
||||
if let Some(reloc) = &reloc {
|
||||
match reloc.flags {
|
||||
// Calls
|
||||
RelocationFlags::Elf { r_type: elf::R_ARM_THM_XPC22 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_THM_PC22 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_PC24 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_XPC25 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_CALL } => {
|
||||
reloc_arg = parsed_ins
|
||||
.args
|
||||
.iter()
|
||||
.rposition(|a| matches!(a, Argument::BranchDest(_)));
|
||||
}
|
||||
// Data
|
||||
RelocationFlags::Elf { r_type: elf::R_ARM_ABS32 } => {
|
||||
reloc_arg =
|
||||
parsed_ins.args.iter().rposition(|a| matches!(a, Argument::UImm(_)));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
};
|
||||
|
||||
let (args, branch_dest) = if reloc.is_some() && parser.mode == ParseMode::Data {
|
||||
(vec![ObjInsArg::Reloc], None)
|
||||
} else {
|
||||
push_args(&parsed_ins, config, reloc_arg, address, display_options)?
|
||||
};
|
||||
|
||||
ops.push(ins.opcode_id());
|
||||
insts.push(ObjIns {
|
||||
address: address as u64,
|
||||
size: (parser.address - address) as u8,
|
||||
op: ins.opcode_id(),
|
||||
mnemonic: Cow::Borrowed(parsed_ins.mnemonic),
|
||||
args,
|
||||
reloc,
|
||||
branch_dest,
|
||||
line,
|
||||
formatted: parsed_ins.display(display_options).to_string(),
|
||||
orig: None,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ProcessCodeResult { ops, insts })
|
||||
}
|
||||
|
||||
fn implcit_addend(
|
||||
&self,
|
||||
_file: &File<'_>,
|
||||
section: &ObjSection,
|
||||
address: u64,
|
||||
reloc: &Relocation,
|
||||
) -> Result<i64> {
|
||||
let address = address as usize;
|
||||
Ok(match reloc.flags() {
|
||||
// ARM calls
|
||||
RelocationFlags::Elf { r_type: elf::R_ARM_PC24 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_XPC25 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_CALL } => {
|
||||
let data = section.data[address..address + 4].try_into()?;
|
||||
let addend = self.endianness.read_i32_bytes(data);
|
||||
let imm24 = addend & 0xffffff;
|
||||
(imm24 << 2) << 8 >> 8
|
||||
}
|
||||
|
||||
// Thumb calls
|
||||
RelocationFlags::Elf { r_type: elf::R_ARM_THM_PC22 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_THM_XPC22 } => {
|
||||
let data = section.data[address..address + 2].try_into()?;
|
||||
let high = self.endianness.read_i16_bytes(data) as i32;
|
||||
let data = section.data[address + 2..address + 4].try_into()?;
|
||||
let low = self.endianness.read_i16_bytes(data) as i32;
|
||||
|
||||
let imm22 = ((high & 0x7ff) << 11) | (low & 0x7ff);
|
||||
(imm22 << 1) << 9 >> 9
|
||||
}
|
||||
|
||||
// Data
|
||||
RelocationFlags::Elf { r_type: elf::R_ARM_ABS32 } => {
|
||||
let data = section.data[address..address + 4].try_into()?;
|
||||
self.endianness.read_i32_bytes(data)
|
||||
}
|
||||
|
||||
flags => bail!("Unsupported ARM implicit relocation {flags:?}"),
|
||||
} as i64)
|
||||
}
|
||||
|
||||
fn demangle(&self, name: &str) -> Option<String> {
|
||||
cpp_demangle::Symbol::new(name)
|
||||
.ok()
|
||||
.and_then(|s| s.demangle(&cpp_demangle::DemangleOptions::default()).ok())
|
||||
}
|
||||
|
||||
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
|
||||
Cow::Owned(format!("<{flags:?}>"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct DisasmMode {
|
||||
address: u32,
|
||||
mapping: ParseMode,
|
||||
}
|
||||
|
||||
impl DisasmMode {
|
||||
fn from_symbol<'a>(sym: &Symbol<'a, '_, &'a [u8]>) -> Option<Self> {
|
||||
if let Ok(name) = sym.name() {
|
||||
ParseMode::from_mapping_symbol(name)
|
||||
.map(|mapping| DisasmMode { address: sym.address() as u32, mapping })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn push_args(
|
||||
parsed_ins: &ParsedIns,
|
||||
config: &DiffObjConfig,
|
||||
reloc_arg: Option<usize>,
|
||||
cur_addr: u32,
|
||||
display_options: DisplayOptions,
|
||||
) -> Result<(Vec<ObjInsArg>, Option<u64>)> {
|
||||
let mut args = vec![];
|
||||
let mut branch_dest = None;
|
||||
let mut writeback = false;
|
||||
let mut deref = false;
|
||||
for (i, arg) in parsed_ins.args_iter().enumerate() {
|
||||
// Emit punctuation before separator
|
||||
if deref {
|
||||
match arg {
|
||||
Argument::OffsetImm(OffsetImm { post_indexed: true, value: _ })
|
||||
| Argument::OffsetReg(OffsetReg { add: _, post_indexed: true, reg: _ })
|
||||
| Argument::CoOption(_) => {
|
||||
deref = false;
|
||||
args.push(ObjInsArg::PlainText("]".into()));
|
||||
if writeback {
|
||||
writeback = false;
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
args.push(ObjInsArg::PlainText(config.separator().into()));
|
||||
}
|
||||
|
||||
if reloc_arg == Some(i) {
|
||||
args.push(ObjInsArg::Reloc);
|
||||
} else {
|
||||
match arg {
|
||||
Argument::None => {}
|
||||
Argument::Reg(reg) => {
|
||||
if reg.deref {
|
||||
deref = true;
|
||||
args.push(ObjInsArg::PlainText("[".into()));
|
||||
}
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
reg.reg.display(display_options.reg_names).to_string().into(),
|
||||
)));
|
||||
if reg.writeback {
|
||||
if reg.deref {
|
||||
writeback = true;
|
||||
} else {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
|
||||
}
|
||||
}
|
||||
}
|
||||
Argument::RegList(reg_list) => {
|
||||
args.push(ObjInsArg::PlainText("{".into()));
|
||||
let mut first = true;
|
||||
for i in 0..16 {
|
||||
if (reg_list.regs & (1 << i)) != 0 {
|
||||
if !first {
|
||||
args.push(ObjInsArg::PlainText(config.separator().into()));
|
||||
}
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
Register::parse(i)
|
||||
.display(display_options.reg_names)
|
||||
.to_string()
|
||||
.into(),
|
||||
)));
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
args.push(ObjInsArg::PlainText("}".into()));
|
||||
if reg_list.user_mode {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("^".to_string().into())));
|
||||
}
|
||||
}
|
||||
Argument::UImm(value) | Argument::CoOpcode(value) | Argument::SatImm(value) => {
|
||||
args.push(ObjInsArg::PlainText("#".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(*value as u64)));
|
||||
}
|
||||
Argument::SImm(value)
|
||||
| Argument::OffsetImm(OffsetImm { post_indexed: _, value }) => {
|
||||
args.push(ObjInsArg::PlainText("#".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Signed(*value as i64)));
|
||||
}
|
||||
Argument::BranchDest(value) => {
|
||||
let dest = cur_addr.wrapping_add_signed(*value) as u64;
|
||||
args.push(ObjInsArg::BranchDest(dest));
|
||||
branch_dest = Some(dest);
|
||||
}
|
||||
Argument::CoOption(value) => {
|
||||
args.push(ObjInsArg::PlainText("{".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(*value as u64)));
|
||||
args.push(ObjInsArg::PlainText("}".into()));
|
||||
}
|
||||
Argument::CoprocNum(value) => {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(format!("p{}", value).into())));
|
||||
}
|
||||
Argument::ShiftImm(shift) => {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(shift.op.to_string().into())));
|
||||
args.push(ObjInsArg::PlainText(" #".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(shift.imm as u64)));
|
||||
}
|
||||
Argument::ShiftReg(shift) => {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(shift.op.to_string().into())));
|
||||
args.push(ObjInsArg::PlainText(" ".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
shift.reg.display(display_options.reg_names).to_string().into(),
|
||||
)));
|
||||
}
|
||||
Argument::OffsetReg(offset) => {
|
||||
if !offset.add {
|
||||
args.push(ObjInsArg::PlainText("-".into()));
|
||||
}
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
offset.reg.display(display_options.reg_names).to_string().into(),
|
||||
)));
|
||||
}
|
||||
Argument::CpsrMode(mode) => {
|
||||
args.push(ObjInsArg::PlainText("#".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(mode.mode as u64)));
|
||||
if mode.writeback {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
|
||||
}
|
||||
}
|
||||
Argument::CoReg(_)
|
||||
| Argument::StatusReg(_)
|
||||
| Argument::StatusMask(_)
|
||||
| Argument::Shift(_)
|
||||
| Argument::CpsrFlags(_)
|
||||
| Argument::Endian(_) => args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
arg.display(display_options, None).to_string().into(),
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
if deref {
|
||||
args.push(ObjInsArg::PlainText("]".into()));
|
||||
if writeback {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
|
||||
}
|
||||
}
|
||||
Ok((args, branch_dest))
|
||||
}
|
||||
2840
objdiff-core/src/arch/arm64.rs
Normal file
316
objdiff-core/src/arch/mips.rs
Normal file
@@ -0,0 +1,316 @@
|
||||
use std::{borrow::Cow, collections::BTreeMap, sync::Mutex};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use object::{
|
||||
elf, Endian, Endianness, File, FileFlags, Object, ObjectSection, ObjectSymbol, Relocation,
|
||||
RelocationFlags, RelocationTarget,
|
||||
};
|
||||
use rabbitizer::{config, Abi, InstrCategory, Instruction, OperandType};
|
||||
|
||||
use crate::{
|
||||
arch::{ObjArch, ProcessCodeResult},
|
||||
diff::{DiffObjConfig, MipsAbi, MipsInstrCategory},
|
||||
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection},
|
||||
};
|
||||
|
||||
static RABBITIZER_MUTEX: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn configure_rabbitizer(abi: Abi) {
|
||||
unsafe {
|
||||
config::RabbitizerConfig_Cfg.reg_names.fpr_abi_names = abi;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ObjArchMips {
|
||||
pub endianness: Endianness,
|
||||
pub abi: Abi,
|
||||
pub instr_category: InstrCategory,
|
||||
pub ri_gp_value: i32,
|
||||
}
|
||||
|
||||
const EF_MIPS_ABI: u32 = 0x0000F000;
|
||||
const EF_MIPS_MACH: u32 = 0x00FF0000;
|
||||
|
||||
const EF_MIPS_MACH_ALLEGREX: u32 = 0x00840000;
|
||||
const EF_MIPS_MACH_5900: u32 = 0x00920000;
|
||||
|
||||
const R_MIPS15_S3: u32 = 119;
|
||||
|
||||
impl ObjArchMips {
|
||||
pub fn new(object: &File) -> Result<Self> {
|
||||
let mut abi = Abi::NUMERIC;
|
||||
let mut instr_category = InstrCategory::CPU;
|
||||
match object.flags() {
|
||||
FileFlags::None => {}
|
||||
FileFlags::Elf { e_flags, .. } => {
|
||||
abi = match e_flags & EF_MIPS_ABI {
|
||||
elf::EF_MIPS_ABI_O32 | elf::EF_MIPS_ABI_O64 => Abi::O32,
|
||||
elf::EF_MIPS_ABI_EABI32 | elf::EF_MIPS_ABI_EABI64 => Abi::N32,
|
||||
_ => {
|
||||
if e_flags & elf::EF_MIPS_ABI2 != 0 {
|
||||
Abi::N32
|
||||
} else {
|
||||
Abi::NUMERIC
|
||||
}
|
||||
}
|
||||
};
|
||||
instr_category = match e_flags & EF_MIPS_MACH {
|
||||
EF_MIPS_MACH_ALLEGREX => InstrCategory::R4000ALLEGREX,
|
||||
EF_MIPS_MACH_5900 => InstrCategory::R5900,
|
||||
_ => InstrCategory::CPU,
|
||||
};
|
||||
}
|
||||
_ => bail!("Unsupported MIPS file flags"),
|
||||
}
|
||||
|
||||
// Parse the ri_gp_value stored in .reginfo to be able to correctly
|
||||
// calculate R_MIPS_GPREL16 relocations later. The value is stored
|
||||
// 0x14 bytes into .reginfo (on 32 bit platforms)
|
||||
let ri_gp_value = object
|
||||
.section_by_name(".reginfo")
|
||||
.and_then(|section| section.data().ok())
|
||||
.and_then(|data| data.get(0x14..0x18))
|
||||
.and_then(|s| s.try_into().ok())
|
||||
.map(|bytes| object.endianness().read_i32_bytes(bytes))
|
||||
.unwrap_or(0);
|
||||
|
||||
Ok(Self { endianness: object.endianness(), abi, instr_category, ri_gp_value })
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjArch for ObjArchMips {
|
||||
fn process_code(
|
||||
&self,
|
||||
address: u64,
|
||||
code: &[u8],
|
||||
section_index: usize,
|
||||
relocations: &[ObjReloc],
|
||||
line_info: &BTreeMap<u64, u32>,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<ProcessCodeResult> {
|
||||
let _guard = RABBITIZER_MUTEX.lock().map_err(|e| anyhow!("Failed to lock mutex: {e}"))?;
|
||||
configure_rabbitizer(match config.mips_abi {
|
||||
MipsAbi::Auto => self.abi,
|
||||
MipsAbi::O32 => Abi::O32,
|
||||
MipsAbi::N32 => Abi::N32,
|
||||
MipsAbi::N64 => Abi::N64,
|
||||
});
|
||||
let instr_category = match config.mips_instr_category {
|
||||
MipsInstrCategory::Auto => self.instr_category,
|
||||
MipsInstrCategory::Cpu => InstrCategory::CPU,
|
||||
MipsInstrCategory::Rsp => InstrCategory::RSP,
|
||||
MipsInstrCategory::R3000Gte => InstrCategory::R3000GTE,
|
||||
MipsInstrCategory::R4000Allegrex => InstrCategory::R4000ALLEGREX,
|
||||
MipsInstrCategory::R5900 => InstrCategory::R5900,
|
||||
};
|
||||
|
||||
let start_address = address;
|
||||
let end_address = address + code.len() as u64;
|
||||
let ins_count = code.len() / 4;
|
||||
let mut ops = Vec::<u16>::with_capacity(ins_count);
|
||||
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
|
||||
let mut cur_addr = start_address as u32;
|
||||
for chunk in code.chunks_exact(4) {
|
||||
let reloc = relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
|
||||
let code = self.endianness.read_u32_bytes(chunk.try_into()?);
|
||||
let instruction = Instruction::new(code, cur_addr, instr_category);
|
||||
|
||||
let formatted = instruction.disassemble(None, 0);
|
||||
let op = instruction.unique_id as u16;
|
||||
ops.push(op);
|
||||
|
||||
let mnemonic = instruction.opcode_name();
|
||||
let is_branch = instruction.is_branch();
|
||||
let branch_offset = instruction.branch_offset();
|
||||
let mut branch_dest = if is_branch {
|
||||
cur_addr.checked_add_signed(branch_offset).map(|a| a as u64)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let operands = instruction.get_operands_slice();
|
||||
let mut args = Vec::with_capacity(operands.len() + 1);
|
||||
for (idx, op) in operands.iter().enumerate() {
|
||||
if idx > 0 {
|
||||
args.push(ObjInsArg::PlainText(config.separator().into()));
|
||||
}
|
||||
|
||||
match op {
|
||||
OperandType::cpu_immediate
|
||||
| OperandType::cpu_label
|
||||
| OperandType::cpu_branch_target_label => {
|
||||
if let Some(reloc) = reloc {
|
||||
// If the relocation target is within the current function, we can
|
||||
// convert it into a relative branch target. Note that we check
|
||||
// target_address > start_address instead of >= so that recursive
|
||||
// tail calls are not considered branch targets.
|
||||
let target_address =
|
||||
reloc.target.address.checked_add_signed(reloc.addend);
|
||||
if reloc.target.orig_section_index == Some(section_index)
|
||||
&& matches!(target_address, Some(addr) if addr > start_address && addr < end_address)
|
||||
{
|
||||
let target_address = target_address.unwrap();
|
||||
args.push(ObjInsArg::BranchDest(target_address));
|
||||
branch_dest = Some(target_address);
|
||||
} else {
|
||||
push_reloc(&mut args, reloc)?;
|
||||
branch_dest = None;
|
||||
}
|
||||
} else if let Some(branch_dest) = branch_dest {
|
||||
args.push(ObjInsArg::BranchDest(branch_dest));
|
||||
} else {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
op.disassemble(&instruction, None).into(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
OperandType::cpu_immediate_base => {
|
||||
if let Some(reloc) = reloc {
|
||||
push_reloc(&mut args, reloc)?;
|
||||
} else {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
OperandType::cpu_immediate.disassemble(&instruction, None).into(),
|
||||
)));
|
||||
}
|
||||
args.push(ObjInsArg::PlainText("(".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
OperandType::cpu_rs.disassemble(&instruction, None).into(),
|
||||
)));
|
||||
args.push(ObjInsArg::PlainText(")".into()));
|
||||
}
|
||||
// OperandType::r5900_immediate15 => match reloc {
|
||||
// Some(reloc)
|
||||
// if reloc.flags == RelocationFlags::Elf { r_type: R_MIPS15_S3 } =>
|
||||
// {
|
||||
// push_reloc(&mut args, reloc)?;
|
||||
// }
|
||||
// _ => {
|
||||
// args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
// op.disassemble(&instruction, None).into(),
|
||||
// )));
|
||||
// }
|
||||
// },
|
||||
_ => {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
op.disassemble(&instruction, None).into(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
let line = line_info.range(..=cur_addr as u64).last().map(|(_, &b)| b);
|
||||
insts.push(ObjIns {
|
||||
address: cur_addr as u64,
|
||||
size: 4,
|
||||
op,
|
||||
mnemonic: Cow::Borrowed(mnemonic),
|
||||
args,
|
||||
reloc: reloc.cloned(),
|
||||
branch_dest,
|
||||
line,
|
||||
formatted,
|
||||
orig: None,
|
||||
});
|
||||
cur_addr += 4;
|
||||
}
|
||||
Ok(ProcessCodeResult { ops, insts })
|
||||
}
|
||||
|
||||
fn implcit_addend(
|
||||
&self,
|
||||
file: &File<'_>,
|
||||
section: &ObjSection,
|
||||
address: u64,
|
||||
reloc: &Relocation,
|
||||
) -> Result<i64> {
|
||||
let data = section.data[address as usize..address as usize + 4].try_into()?;
|
||||
let addend = self.endianness.read_u32_bytes(data);
|
||||
Ok(match reloc.flags() {
|
||||
RelocationFlags::Elf { r_type: elf::R_MIPS_32 } => addend as i64,
|
||||
RelocationFlags::Elf { r_type: elf::R_MIPS_26 } => ((addend & 0x03FFFFFF) << 2) as i64,
|
||||
RelocationFlags::Elf { r_type: elf::R_MIPS_HI16 } => {
|
||||
((addend & 0x0000FFFF) << 16) as i32 as i64
|
||||
}
|
||||
RelocationFlags::Elf {
|
||||
r_type: elf::R_MIPS_LO16 | elf::R_MIPS_GOT16 | elf::R_MIPS_CALL16,
|
||||
} => (addend & 0x0000FFFF) as i16 as i64,
|
||||
RelocationFlags::Elf { r_type: elf::R_MIPS_GPREL16 | elf::R_MIPS_LITERAL } => {
|
||||
let RelocationTarget::Symbol(idx) = reloc.target() else {
|
||||
bail!("Unsupported R_MIPS_GPREL16 relocation against a non-symbol");
|
||||
};
|
||||
let sym = file.symbol_by_index(idx)?;
|
||||
|
||||
// if the symbol we are relocating against is in a local section we need to add
|
||||
// the ri_gp_value from .reginfo to the addend.
|
||||
if sym.section().index().is_some() {
|
||||
((addend & 0x0000FFFF) as i16 as i64) + self.ri_gp_value as i64
|
||||
} else {
|
||||
(addend & 0x0000FFFF) as i16 as i64
|
||||
}
|
||||
}
|
||||
RelocationFlags::Elf { r_type: elf::R_MIPS_PC16 } => 0, // PC-relative relocation
|
||||
RelocationFlags::Elf { r_type: R_MIPS15_S3 } => ((addend & 0x001FFFC0) >> 3) as i64,
|
||||
flags => bail!("Unsupported MIPS implicit relocation {flags:?}"),
|
||||
})
|
||||
}
|
||||
|
||||
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
|
||||
match flags {
|
||||
RelocationFlags::Elf { r_type } => match r_type {
|
||||
elf::R_MIPS_32 => Cow::Borrowed("R_MIPS_32"),
|
||||
elf::R_MIPS_26 => Cow::Borrowed("R_MIPS_26"),
|
||||
elf::R_MIPS_HI16 => Cow::Borrowed("R_MIPS_HI16"),
|
||||
elf::R_MIPS_LO16 => Cow::Borrowed("R_MIPS_LO16"),
|
||||
elf::R_MIPS_GPREL16 => Cow::Borrowed("R_MIPS_GPREL16"),
|
||||
elf::R_MIPS_LITERAL => Cow::Borrowed("R_MIPS_LITERAL"),
|
||||
elf::R_MIPS_GOT16 => Cow::Borrowed("R_MIPS_GOT16"),
|
||||
elf::R_MIPS_PC16 => Cow::Borrowed("R_MIPS_PC16"),
|
||||
elf::R_MIPS_CALL16 => Cow::Borrowed("R_MIPS_CALL16"),
|
||||
R_MIPS15_S3 => Cow::Borrowed("R_MIPS15_S3"),
|
||||
_ => Cow::Owned(format!("<{flags:?}>")),
|
||||
},
|
||||
_ => Cow::Owned(format!("<{flags:?}>")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {
|
||||
match reloc.flags {
|
||||
RelocationFlags::Elf { r_type } => match r_type {
|
||||
elf::R_MIPS_HI16 => {
|
||||
args.push(ObjInsArg::PlainText("%hi(".into()));
|
||||
args.push(ObjInsArg::Reloc);
|
||||
args.push(ObjInsArg::PlainText(")".into()));
|
||||
}
|
||||
elf::R_MIPS_LO16 => {
|
||||
args.push(ObjInsArg::PlainText("%lo(".into()));
|
||||
args.push(ObjInsArg::Reloc);
|
||||
args.push(ObjInsArg::PlainText(")".into()));
|
||||
}
|
||||
elf::R_MIPS_GOT16 => {
|
||||
args.push(ObjInsArg::PlainText("%got(".into()));
|
||||
args.push(ObjInsArg::Reloc);
|
||||
args.push(ObjInsArg::PlainText(")".into()));
|
||||
}
|
||||
elf::R_MIPS_CALL16 => {
|
||||
args.push(ObjInsArg::PlainText("%call16(".into()));
|
||||
args.push(ObjInsArg::Reloc);
|
||||
args.push(ObjInsArg::PlainText(")".into()));
|
||||
}
|
||||
elf::R_MIPS_GPREL16 => {
|
||||
args.push(ObjInsArg::PlainText("%gp_rel(".into()));
|
||||
args.push(ObjInsArg::Reloc);
|
||||
args.push(ObjInsArg::PlainText(")".into()));
|
||||
}
|
||||
elf::R_MIPS_32
|
||||
| elf::R_MIPS_26
|
||||
| elf::R_MIPS_LITERAL
|
||||
| elf::R_MIPS_PC16
|
||||
| R_MIPS15_S3 => {
|
||||
args.push(ObjInsArg::Reloc);
|
||||
}
|
||||
_ => bail!("Unsupported ELF MIPS relocation type {r_type}"),
|
||||
},
|
||||
flags => panic!("Unsupported MIPS relocation flags {flags:?}"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
174
objdiff-core/src/arch/mod.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
use std::{borrow::Cow, collections::BTreeMap, ffi::CStr};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use byteorder::ByteOrder;
|
||||
use object::{Architecture, File, Object, ObjectSymbol, Relocation, RelocationFlags, Symbol};
|
||||
|
||||
use crate::{
|
||||
diff::DiffObjConfig,
|
||||
obj::{ObjIns, ObjReloc, ObjSection},
|
||||
util::ReallySigned,
|
||||
};
|
||||
|
||||
#[cfg(feature = "arm")]
|
||||
mod arm;
|
||||
#[cfg(feature = "arm64")]
|
||||
mod arm64;
|
||||
#[cfg(feature = "mips")]
|
||||
pub mod mips;
|
||||
#[cfg(feature = "ppc")]
|
||||
pub mod ppc;
|
||||
#[cfg(feature = "x86")]
|
||||
pub mod x86;
|
||||
|
||||
/// Represents the type of data associated with an instruction
|
||||
pub enum DataType {
|
||||
Int8,
|
||||
Int16,
|
||||
Int32,
|
||||
Int64,
|
||||
Int128,
|
||||
Float,
|
||||
Double,
|
||||
Bytes,
|
||||
String,
|
||||
}
|
||||
|
||||
impl DataType {
|
||||
pub fn display_bytes<Endian: ByteOrder>(&self, bytes: &[u8]) -> Option<String> {
|
||||
// TODO: Attempt to interpret large symbols as arrays of a smaller type,
|
||||
// fallback to intrepreting it as bytes.
|
||||
// https://github.com/encounter/objdiff/issues/124
|
||||
if self.required_len().is_some_and(|l| bytes.len() != l) {
|
||||
log::warn!("Failed to display a symbol value for a symbol whose size doesn't match the instruction referencing it.");
|
||||
return None;
|
||||
}
|
||||
|
||||
match self {
|
||||
DataType::Int8 => {
|
||||
let i = i8::from_ne_bytes(bytes.try_into().unwrap());
|
||||
if i < 0 {
|
||||
format!("Int8: {:#x} ({:#x})", i, ReallySigned(i))
|
||||
} else {
|
||||
format!("Int8: {:#x}", i)
|
||||
}
|
||||
}
|
||||
DataType::Int16 => {
|
||||
let i = Endian::read_i16(bytes);
|
||||
if i < 0 {
|
||||
format!("Int16: {:#x} ({:#x})", i, ReallySigned(i))
|
||||
} else {
|
||||
format!("Int16: {:#x}", i)
|
||||
}
|
||||
}
|
||||
DataType::Int32 => {
|
||||
let i = Endian::read_i32(bytes);
|
||||
if i < 0 {
|
||||
format!("Int32: {:#x} ({:#x})", i, ReallySigned(i))
|
||||
} else {
|
||||
format!("Int32: {:#x}", i)
|
||||
}
|
||||
}
|
||||
DataType::Int64 => {
|
||||
let i = Endian::read_i64(bytes);
|
||||
if i < 0 {
|
||||
format!("Int64: {:#x} ({:#x})", i, ReallySigned(i))
|
||||
} else {
|
||||
format!("Int64: {:#x}", i)
|
||||
}
|
||||
}
|
||||
DataType::Int128 => {
|
||||
let i = Endian::read_i128(bytes);
|
||||
if i < 0 {
|
||||
format!("Int128: {:#x} ({:#x})", i, ReallySigned(i))
|
||||
} else {
|
||||
format!("Int128: {:#x}", i)
|
||||
}
|
||||
}
|
||||
DataType::Float => {
|
||||
format!("Float: {}", Endian::read_f32(bytes))
|
||||
}
|
||||
DataType::Double => {
|
||||
format!("Double: {}", Endian::read_f64(bytes))
|
||||
}
|
||||
DataType::Bytes => {
|
||||
format!("Bytes: {:#?}", bytes)
|
||||
}
|
||||
DataType::String => {
|
||||
format!("String: {:?}", CStr::from_bytes_until_nul(bytes).ok()?)
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
fn required_len(&self) -> Option<usize> {
|
||||
match self {
|
||||
DataType::Int8 => Some(1),
|
||||
DataType::Int16 => Some(2),
|
||||
DataType::Int32 => Some(4),
|
||||
DataType::Int64 => Some(8),
|
||||
DataType::Int128 => Some(16),
|
||||
DataType::Float => Some(4),
|
||||
DataType::Double => Some(8),
|
||||
DataType::Bytes => None,
|
||||
DataType::String => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ObjArch: Send + Sync {
|
||||
fn process_code(
|
||||
&self,
|
||||
address: u64,
|
||||
code: &[u8],
|
||||
section_index: usize,
|
||||
relocations: &[ObjReloc],
|
||||
line_info: &BTreeMap<u64, u32>,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<ProcessCodeResult>;
|
||||
|
||||
fn implcit_addend(
|
||||
&self,
|
||||
file: &File<'_>,
|
||||
section: &ObjSection,
|
||||
address: u64,
|
||||
reloc: &Relocation,
|
||||
) -> Result<i64>;
|
||||
|
||||
fn demangle(&self, _name: &str) -> Option<String> { None }
|
||||
|
||||
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str>;
|
||||
|
||||
fn symbol_address(&self, symbol: &Symbol) -> u64 { symbol.address() }
|
||||
|
||||
fn guess_data_type(&self, _instruction: &ObjIns) -> Option<DataType> { None }
|
||||
|
||||
fn display_data_type(&self, _ty: DataType, bytes: &[u8]) -> Option<String> {
|
||||
Some(format!("Bytes: {:#x?}", bytes))
|
||||
}
|
||||
|
||||
// Downcast methods
|
||||
#[cfg(feature = "ppc")]
|
||||
fn ppc(&self) -> Option<&ppc::ObjArchPpc> { None }
|
||||
}
|
||||
|
||||
pub struct ProcessCodeResult {
|
||||
pub ops: Vec<u16>,
|
||||
pub insts: Vec<ObjIns>,
|
||||
}
|
||||
|
||||
pub fn new_arch(object: &File) -> Result<Box<dyn ObjArch>> {
|
||||
Ok(match object.architecture() {
|
||||
#[cfg(feature = "ppc")]
|
||||
Architecture::PowerPc => Box::new(ppc::ObjArchPpc::new(object)?),
|
||||
#[cfg(feature = "mips")]
|
||||
Architecture::Mips => Box::new(mips::ObjArchMips::new(object)?),
|
||||
#[cfg(feature = "x86")]
|
||||
Architecture::I386 | Architecture::X86_64 => Box::new(x86::ObjArchX86::new(object)?),
|
||||
#[cfg(feature = "arm")]
|
||||
Architecture::Arm => Box::new(arm::ObjArchArm::new(object)?),
|
||||
#[cfg(feature = "arm64")]
|
||||
Architecture::Aarch64 => Box::new(arm64::ObjArchArm64::new(object)?),
|
||||
arch => bail!("Unsupported architecture: {arch:?}"),
|
||||
})
|
||||
}
|
||||
383
objdiff-core/src/arch/ppc.rs
Normal file
@@ -0,0 +1,383 @@
|
||||
use std::{borrow::Cow, collections::BTreeMap};
|
||||
|
||||
use anyhow::{bail, ensure, Result};
|
||||
use byteorder::BigEndian;
|
||||
use cwextab::{decode_extab, ExceptionTableData};
|
||||
use object::{
|
||||
elf, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, RelocationTarget,
|
||||
Symbol, SymbolKind,
|
||||
};
|
||||
use ppc750cl::{Argument, InsIter, Opcode, GPR};
|
||||
|
||||
use crate::{
|
||||
arch::{DataType, ObjArch, ProcessCodeResult},
|
||||
diff::DiffObjConfig,
|
||||
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection, ObjSymbol},
|
||||
};
|
||||
|
||||
// Relative relocation, can be Simm, Offset or BranchDest
|
||||
fn is_relative_arg(arg: &Argument) -> bool {
|
||||
matches!(arg, Argument::Simm(_) | Argument::Offset(_) | Argument::BranchDest(_))
|
||||
}
|
||||
|
||||
// Relative or absolute relocation, can be Uimm, Simm or Offset
|
||||
fn is_rel_abs_arg(arg: &Argument) -> bool {
|
||||
matches!(arg, Argument::Uimm(_) | Argument::Simm(_) | Argument::Offset(_))
|
||||
}
|
||||
|
||||
fn is_offset_arg(arg: &Argument) -> bool { matches!(arg, Argument::Offset(_)) }
|
||||
|
||||
pub struct ObjArchPpc {
|
||||
/// Exception info
|
||||
pub extab: Option<BTreeMap<usize, ExceptionInfo>>,
|
||||
}
|
||||
|
||||
impl ObjArchPpc {
|
||||
pub fn new(file: &File) -> Result<Self> { Ok(Self { extab: decode_exception_info(file)? }) }
|
||||
}
|
||||
|
||||
impl ObjArch for ObjArchPpc {
|
||||
fn process_code(
|
||||
&self,
|
||||
address: u64,
|
||||
code: &[u8],
|
||||
_section_index: usize,
|
||||
relocations: &[ObjReloc],
|
||||
line_info: &BTreeMap<u64, u32>,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<ProcessCodeResult> {
|
||||
let ins_count = code.len() / 4;
|
||||
let mut ops = Vec::<u16>::with_capacity(ins_count);
|
||||
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
|
||||
for (cur_addr, mut ins) in InsIter::new(code, address as u32) {
|
||||
let reloc = relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
|
||||
if let Some(reloc) = reloc {
|
||||
// Zero out relocations
|
||||
ins.code = match reloc.flags {
|
||||
RelocationFlags::Elf { r_type: elf::R_PPC_EMB_SDA21 } => ins.code & !0x1FFFFF,
|
||||
RelocationFlags::Elf { r_type: elf::R_PPC_REL24 } => ins.code & !0x3FFFFFC,
|
||||
RelocationFlags::Elf { r_type: elf::R_PPC_REL14 } => ins.code & !0xFFFC,
|
||||
RelocationFlags::Elf {
|
||||
r_type: elf::R_PPC_ADDR16_HI | elf::R_PPC_ADDR16_HA | elf::R_PPC_ADDR16_LO,
|
||||
} => ins.code & !0xFFFF,
|
||||
_ => ins.code,
|
||||
};
|
||||
}
|
||||
|
||||
let orig = ins.basic().to_string();
|
||||
let simplified = ins.simplified();
|
||||
let formatted = simplified.to_string();
|
||||
|
||||
let mut reloc_arg = None;
|
||||
if let Some(reloc) = reloc {
|
||||
match reloc.flags {
|
||||
RelocationFlags::Elf { r_type: elf::R_PPC_EMB_SDA21 } => {
|
||||
reloc_arg = Some(1);
|
||||
}
|
||||
RelocationFlags::Elf { r_type: elf::R_PPC_REL24 | elf::R_PPC_REL14 } => {
|
||||
reloc_arg = simplified.args.iter().rposition(is_relative_arg);
|
||||
}
|
||||
RelocationFlags::Elf {
|
||||
r_type: elf::R_PPC_ADDR16_HI | elf::R_PPC_ADDR16_HA | elf::R_PPC_ADDR16_LO,
|
||||
} => {
|
||||
reloc_arg = simplified.args.iter().rposition(is_rel_abs_arg);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let mut args = vec![];
|
||||
let mut branch_dest = None;
|
||||
let mut writing_offset = false;
|
||||
for (idx, arg) in simplified.args_iter().enumerate() {
|
||||
if idx > 0 && !writing_offset {
|
||||
args.push(ObjInsArg::PlainText(config.separator().into()));
|
||||
}
|
||||
|
||||
if reloc_arg == Some(idx) {
|
||||
let reloc = reloc.unwrap();
|
||||
push_reloc(&mut args, reloc)?;
|
||||
// For @sda21, we can omit the register argument
|
||||
if matches!(reloc.flags, RelocationFlags::Elf { r_type: elf::R_PPC_EMB_SDA21 })
|
||||
// Sanity check: the next argument should be r0
|
||||
&& matches!(simplified.args.get(idx + 1), Some(Argument::GPR(GPR(0))))
|
||||
{
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
match arg {
|
||||
Argument::Simm(simm) => {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Signed(simm.0 as i64)));
|
||||
}
|
||||
Argument::Uimm(uimm) => {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(uimm.0 as u64)));
|
||||
}
|
||||
Argument::Offset(offset) => {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Signed(offset.0 as i64)));
|
||||
}
|
||||
Argument::BranchDest(dest) => {
|
||||
let dest = cur_addr.wrapping_add_signed(dest.0) as u64;
|
||||
args.push(ObjInsArg::BranchDest(dest));
|
||||
branch_dest = Some(dest);
|
||||
}
|
||||
_ => {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
arg.to_string().into(),
|
||||
)));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if writing_offset {
|
||||
args.push(ObjInsArg::PlainText(")".into()));
|
||||
writing_offset = false;
|
||||
}
|
||||
if is_offset_arg(arg) {
|
||||
args.push(ObjInsArg::PlainText("(".into()));
|
||||
writing_offset = true;
|
||||
}
|
||||
}
|
||||
|
||||
ops.push(ins.op as u16);
|
||||
let line = line_info.range(..=cur_addr as u64).last().map(|(_, &b)| b);
|
||||
insts.push(ObjIns {
|
||||
address: cur_addr as u64,
|
||||
size: 4,
|
||||
mnemonic: Cow::Borrowed(simplified.mnemonic),
|
||||
args,
|
||||
reloc: reloc.cloned(),
|
||||
op: ins.op as u16,
|
||||
branch_dest,
|
||||
line,
|
||||
formatted,
|
||||
orig: Some(orig),
|
||||
});
|
||||
}
|
||||
Ok(ProcessCodeResult { ops, insts })
|
||||
}
|
||||
|
||||
fn implcit_addend(
|
||||
&self,
|
||||
_file: &File<'_>,
|
||||
_section: &ObjSection,
|
||||
address: u64,
|
||||
reloc: &Relocation,
|
||||
) -> Result<i64> {
|
||||
bail!("Unsupported PPC implicit relocation {:#x}:{:?}", address, reloc.flags())
|
||||
}
|
||||
|
||||
fn demangle(&self, name: &str) -> Option<String> {
|
||||
cwdemangle::demangle(name, &cwdemangle::DemangleOptions::default())
|
||||
}
|
||||
|
||||
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
|
||||
match flags {
|
||||
RelocationFlags::Elf { r_type } => match r_type {
|
||||
elf::R_PPC_ADDR16_LO => Cow::Borrowed("R_PPC_ADDR16_LO"),
|
||||
elf::R_PPC_ADDR16_HI => Cow::Borrowed("R_PPC_ADDR16_HI"),
|
||||
elf::R_PPC_ADDR16_HA => Cow::Borrowed("R_PPC_ADDR16_HA"),
|
||||
elf::R_PPC_EMB_SDA21 => Cow::Borrowed("R_PPC_EMB_SDA21"),
|
||||
elf::R_PPC_ADDR32 => Cow::Borrowed("R_PPC_ADDR32"),
|
||||
elf::R_PPC_UADDR32 => Cow::Borrowed("R_PPC_UADDR32"),
|
||||
elf::R_PPC_REL24 => Cow::Borrowed("R_PPC_REL24"),
|
||||
elf::R_PPC_REL14 => Cow::Borrowed("R_PPC_REL14"),
|
||||
_ => Cow::Owned(format!("<{flags:?}>")),
|
||||
},
|
||||
_ => Cow::Owned(format!("<{flags:?}>")),
|
||||
}
|
||||
}
|
||||
|
||||
fn guess_data_type(&self, instruction: &ObjIns) -> Option<super::DataType> {
|
||||
// Always shows the first string of the table. Not ideal, but it's really hard to find
|
||||
// the actual string being referenced.
|
||||
if instruction.reloc.as_ref().is_some_and(|r| r.target.name.starts_with("@stringBase")) {
|
||||
return Some(DataType::String);
|
||||
}
|
||||
|
||||
match Opcode::from(instruction.op as u8) {
|
||||
Opcode::Lbz | Opcode::Lbzu | Opcode::Lbzux | Opcode::Lbzx => Some(DataType::Int8),
|
||||
Opcode::Lhz | Opcode::Lhzu | Opcode::Lhzux | Opcode::Lhzx => Some(DataType::Int16),
|
||||
Opcode::Lha | Opcode::Lhau | Opcode::Lhaux | Opcode::Lhax => Some(DataType::Int16),
|
||||
Opcode::Lwz | Opcode::Lwzu | Opcode::Lwzux | Opcode::Lwzx => Some(DataType::Int32),
|
||||
Opcode::Lfs | Opcode::Lfsu | Opcode::Lfsux | Opcode::Lfsx => Some(DataType::Float),
|
||||
Opcode::Lfd | Opcode::Lfdu | Opcode::Lfdux | Opcode::Lfdx => Some(DataType::Double),
|
||||
|
||||
Opcode::Stb | Opcode::Stbu | Opcode::Stbux | Opcode::Stbx => Some(DataType::Int8),
|
||||
Opcode::Sth | Opcode::Sthu | Opcode::Sthux | Opcode::Sthx => Some(DataType::Int16),
|
||||
Opcode::Stw | Opcode::Stwu | Opcode::Stwux | Opcode::Stwx => Some(DataType::Int32),
|
||||
Opcode::Stfs | Opcode::Stfsu | Opcode::Stfsux | Opcode::Stfsx => Some(DataType::Float),
|
||||
Opcode::Stfd | Opcode::Stfdu | Opcode::Stfdux | Opcode::Stfdx => Some(DataType::Double),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn display_data_type(&self, ty: DataType, bytes: &[u8]) -> Option<String> {
|
||||
ty.display_bytes::<BigEndian>(bytes)
|
||||
}
|
||||
|
||||
fn ppc(&self) -> Option<&ObjArchPpc> { Some(self) }
|
||||
}
|
||||
|
||||
impl ObjArchPpc {
|
||||
pub fn extab_for_symbol(&self, symbol: &ObjSymbol) -> Option<&ExceptionInfo> {
|
||||
symbol.original_index.and_then(|i| self.extab.as_ref()?.get(&i))
|
||||
}
|
||||
}
|
||||
|
||||
fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {
|
||||
match reloc.flags {
|
||||
RelocationFlags::Elf { r_type } => match r_type {
|
||||
elf::R_PPC_ADDR16_LO => {
|
||||
args.push(ObjInsArg::Reloc);
|
||||
args.push(ObjInsArg::PlainText("@l".into()));
|
||||
}
|
||||
elf::R_PPC_ADDR16_HI => {
|
||||
args.push(ObjInsArg::Reloc);
|
||||
args.push(ObjInsArg::PlainText("@h".into()));
|
||||
}
|
||||
elf::R_PPC_ADDR16_HA => {
|
||||
args.push(ObjInsArg::Reloc);
|
||||
args.push(ObjInsArg::PlainText("@ha".into()));
|
||||
}
|
||||
elf::R_PPC_EMB_SDA21 => {
|
||||
args.push(ObjInsArg::Reloc);
|
||||
args.push(ObjInsArg::PlainText("@sda21".into()));
|
||||
}
|
||||
elf::R_PPC_ADDR32 | elf::R_PPC_UADDR32 | elf::R_PPC_REL24 | elf::R_PPC_REL14 => {
|
||||
args.push(ObjInsArg::Reloc);
|
||||
}
|
||||
_ => bail!("Unsupported ELF PPC relocation type {r_type}"),
|
||||
},
|
||||
flags => bail!("Unsupported PPC relocation kind: {flags:?}"),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExtabSymbolRef {
|
||||
pub original_index: usize,
|
||||
pub name: String,
|
||||
pub demangled_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExceptionInfo {
|
||||
pub eti_symbol: ExtabSymbolRef,
|
||||
pub etb_symbol: ExtabSymbolRef,
|
||||
pub data: ExceptionTableData,
|
||||
pub dtors: Vec<ExtabSymbolRef>,
|
||||
}
|
||||
|
||||
fn decode_exception_info(file: &File<'_>) -> Result<Option<BTreeMap<usize, ExceptionInfo>>> {
|
||||
let Some(extab_section) = file.section_by_name("extab") else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(extabindex_section) = file.section_by_name("extabindex") else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut result = BTreeMap::new();
|
||||
let extab_relocations = extab_section.relocations().collect::<BTreeMap<u64, Relocation>>();
|
||||
let extabindex_relocations =
|
||||
extabindex_section.relocations().collect::<BTreeMap<u64, Relocation>>();
|
||||
|
||||
for extabindex in file.symbols().filter(|symbol| {
|
||||
symbol.section_index() == Some(extabindex_section.index())
|
||||
&& symbol.kind() == SymbolKind::Data
|
||||
}) {
|
||||
if extabindex.size() != 12 {
|
||||
log::warn!("Invalid extabindex entry size {}", extabindex.size());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Each extabindex entry has two relocations:
|
||||
// - 0x0: The function that the exception table is for
|
||||
// - 0x8: The relevant entry in extab section
|
||||
let Some(extab_func_reloc) = extabindex_relocations.get(&extabindex.address()) else {
|
||||
log::warn!("Failed to find function relocation for extabindex entry");
|
||||
continue;
|
||||
};
|
||||
let Some(extab_reloc) = extabindex_relocations.get(&(extabindex.address() + 8)) else {
|
||||
log::warn!("Failed to find extab relocation for extabindex entry");
|
||||
continue;
|
||||
};
|
||||
|
||||
// Resolve the function and extab symbols
|
||||
let Some(extab_func) = relocation_symbol(file, extab_func_reloc)? else {
|
||||
log::warn!("Failed to find function symbol for extabindex entry");
|
||||
continue;
|
||||
};
|
||||
let extab_func_name = extab_func.name()?;
|
||||
let Some(extab) = relocation_symbol(file, extab_reloc)? else {
|
||||
log::warn!("Failed to find extab symbol for extabindex entry");
|
||||
continue;
|
||||
};
|
||||
|
||||
let extab_start_addr = extab.address() - extab_section.address();
|
||||
let extab_end_addr = extab_start_addr + extab.size();
|
||||
|
||||
// All relocations in the extab section are dtors
|
||||
let mut dtors: Vec<ExtabSymbolRef> = vec![];
|
||||
for (_, reloc) in extab_relocations.range(extab_start_addr..extab_end_addr) {
|
||||
let Some(symbol) = relocation_symbol(file, reloc)? else {
|
||||
log::warn!("Failed to find symbol for extab relocation");
|
||||
continue;
|
||||
};
|
||||
dtors.push(make_symbol_ref(&symbol)?);
|
||||
}
|
||||
|
||||
// Decode the extab data
|
||||
let Some(extab_data) = extab_section.data_range(extab_start_addr, extab.size())? else {
|
||||
log::warn!("Failed to get extab data for function {}", extab_func_name);
|
||||
continue;
|
||||
};
|
||||
let data = match decode_extab(extab_data) {
|
||||
Ok(decoded_data) => decoded_data,
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Exception table decoding failed for function {}, reason: {}",
|
||||
extab_func_name,
|
||||
e.to_string()
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
//Add the new entry to the list
|
||||
result.insert(extab_func.index().0, ExceptionInfo {
|
||||
eti_symbol: make_symbol_ref(&extabindex)?,
|
||||
etb_symbol: make_symbol_ref(&extab)?,
|
||||
data,
|
||||
dtors,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Some(result))
|
||||
}
|
||||
|
||||
fn relocation_symbol<'data, 'file>(
|
||||
file: &'file File<'data>,
|
||||
relocation: &Relocation,
|
||||
) -> Result<Option<Symbol<'data, 'file>>> {
|
||||
let addend = relocation.addend();
|
||||
match relocation.target() {
|
||||
RelocationTarget::Symbol(idx) => {
|
||||
ensure!(addend == 0, "Symbol relocations must have zero addend");
|
||||
Ok(Some(file.symbol_by_index(idx)?))
|
||||
}
|
||||
RelocationTarget::Section(idx) => {
|
||||
ensure!(addend >= 0, "Section relocations must have non-negative addend");
|
||||
let addend = addend as u64;
|
||||
Ok(file
|
||||
.symbols()
|
||||
.find(|symbol| symbol.section_index() == Some(idx) && symbol.address() == addend))
|
||||
}
|
||||
target => bail!("Unsupported relocation target: {target:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_symbol_ref(symbol: &Symbol) -> Result<ExtabSymbolRef> {
|
||||
let name = symbol.name()?.to_string();
|
||||
let demangled_name = cwdemangle::demangle(&name, &cwdemangle::DemangleOptions::default());
|
||||
Ok(ExtabSymbolRef { original_index: symbol.index().0, name, demangled_name })
|
||||
}
|
||||
344
objdiff-core/src/arch/x86.rs
Normal file
@@ -0,0 +1,344 @@
|
||||
use std::{borrow::Cow, collections::BTreeMap};
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Result};
|
||||
use iced_x86::{
|
||||
Decoder, DecoderOptions, DecoratorKind, Formatter, FormatterOutput, FormatterTextKind,
|
||||
GasFormatter, Instruction, IntelFormatter, MasmFormatter, NasmFormatter, NumberKind, OpKind,
|
||||
PrefixKind, Register,
|
||||
};
|
||||
use object::{pe, Endian, Endianness, File, Object, Relocation, RelocationFlags};
|
||||
|
||||
use crate::{
|
||||
arch::{ObjArch, ProcessCodeResult},
|
||||
diff::{DiffObjConfig, X86Formatter},
|
||||
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection},
|
||||
};
|
||||
|
||||
pub struct ObjArchX86 {
|
||||
bits: u32,
|
||||
endianness: Endianness,
|
||||
}
|
||||
|
||||
impl ObjArchX86 {
|
||||
pub fn new(object: &File) -> Result<Self> {
|
||||
Ok(Self { bits: if object.is_64() { 64 } else { 32 }, endianness: object.endianness() })
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjArch for ObjArchX86 {
|
||||
fn process_code(
|
||||
&self,
|
||||
address: u64,
|
||||
code: &[u8],
|
||||
_section_index: usize,
|
||||
relocations: &[ObjReloc],
|
||||
line_info: &BTreeMap<u64, u32>,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<ProcessCodeResult> {
|
||||
let mut result = ProcessCodeResult { ops: Vec::new(), insts: Vec::new() };
|
||||
let mut decoder = Decoder::with_ip(self.bits, code, address, DecoderOptions::NONE);
|
||||
let mut formatter: Box<dyn Formatter> = match config.x86_formatter {
|
||||
X86Formatter::Intel => Box::new(IntelFormatter::new()),
|
||||
X86Formatter::Gas => Box::new(GasFormatter::new()),
|
||||
X86Formatter::Nasm => Box::new(NasmFormatter::new()),
|
||||
X86Formatter::Masm => Box::new(MasmFormatter::new()),
|
||||
};
|
||||
formatter.options_mut().set_space_after_operand_separator(config.space_between_args);
|
||||
|
||||
let mut output = InstructionFormatterOutput {
|
||||
formatted: String::new(),
|
||||
ins: ObjIns {
|
||||
address: 0,
|
||||
size: 0,
|
||||
op: 0,
|
||||
mnemonic: Cow::Borrowed("<invalid>"),
|
||||
args: vec![],
|
||||
reloc: None,
|
||||
branch_dest: None,
|
||||
line: None,
|
||||
formatted: String::new(),
|
||||
orig: None,
|
||||
},
|
||||
error: None,
|
||||
ins_operands: vec![],
|
||||
};
|
||||
let mut instruction = Instruction::default();
|
||||
while decoder.can_decode() {
|
||||
decoder.decode_out(&mut instruction);
|
||||
|
||||
let address = instruction.ip();
|
||||
let op = instruction.mnemonic() as u16;
|
||||
let reloc = relocations
|
||||
.iter()
|
||||
.find(|r| r.address >= address && r.address < address + instruction.len() as u64);
|
||||
let line = line_info.range(..=address).last().map(|(_, &b)| b);
|
||||
output.ins = ObjIns {
|
||||
address,
|
||||
size: instruction.len() as u8,
|
||||
op,
|
||||
mnemonic: Cow::Borrowed("<invalid>"),
|
||||
args: vec![],
|
||||
reloc: reloc.cloned(),
|
||||
branch_dest: None,
|
||||
line,
|
||||
formatted: String::new(),
|
||||
orig: None,
|
||||
};
|
||||
// Run the formatter, which will populate output.ins
|
||||
formatter.format(&instruction, &mut output);
|
||||
if let Some(error) = output.error.take() {
|
||||
return Err(error);
|
||||
}
|
||||
ensure!(output.ins_operands.len() == output.ins.args.len());
|
||||
output.ins.formatted.clone_from(&output.formatted);
|
||||
|
||||
// Make sure we've put the relocation somewhere in the instruction
|
||||
if reloc.is_some() && !output.ins.args.iter().any(|a| matches!(a, ObjInsArg::Reloc)) {
|
||||
let mut found = replace_arg(
|
||||
OpKind::Memory,
|
||||
ObjInsArg::Reloc,
|
||||
&mut output.ins.args,
|
||||
&instruction,
|
||||
&output.ins_operands,
|
||||
)?;
|
||||
if !found {
|
||||
found = replace_arg(
|
||||
OpKind::Immediate32,
|
||||
ObjInsArg::Reloc,
|
||||
&mut output.ins.args,
|
||||
&instruction,
|
||||
&output.ins_operands,
|
||||
)?;
|
||||
}
|
||||
ensure!(found, "x86: Failed to find operand for Absolute relocation");
|
||||
}
|
||||
if reloc.is_some() && !output.ins.args.iter().any(|a| matches!(a, ObjInsArg::Reloc)) {
|
||||
bail!("Failed to find relocation in instruction");
|
||||
}
|
||||
|
||||
result.ops.push(op);
|
||||
result.insts.push(output.ins.clone());
|
||||
|
||||
// Clear for next iteration
|
||||
output.formatted.clear();
|
||||
output.ins_operands.clear();
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn implcit_addend(
|
||||
&self,
|
||||
_file: &File<'_>,
|
||||
section: &ObjSection,
|
||||
address: u64,
|
||||
reloc: &Relocation,
|
||||
) -> Result<i64> {
|
||||
match reloc.flags() {
|
||||
RelocationFlags::Coff { typ: pe::IMAGE_REL_I386_DIR32 | pe::IMAGE_REL_I386_REL32 } => {
|
||||
let data = section.data[address as usize..address as usize + 4].try_into()?;
|
||||
Ok(self.endianness.read_i32_bytes(data) as i64)
|
||||
}
|
||||
flags => bail!("Unsupported x86 implicit relocation {flags:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn demangle(&self, name: &str) -> Option<String> {
|
||||
if name.starts_with('?') {
|
||||
msvc_demangler::demangle(name, msvc_demangler::DemangleFlags::llvm()).ok()
|
||||
} else {
|
||||
cpp_demangle::Symbol::new(name)
|
||||
.ok()
|
||||
.and_then(|s| s.demangle(&cpp_demangle::DemangleOptions::default()).ok())
|
||||
}
|
||||
}
|
||||
|
||||
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
|
||||
match flags {
|
||||
RelocationFlags::Coff { typ } => match typ {
|
||||
pe::IMAGE_REL_I386_DIR32 => Cow::Borrowed("IMAGE_REL_I386_DIR32"),
|
||||
pe::IMAGE_REL_I386_REL32 => Cow::Borrowed("IMAGE_REL_I386_REL32"),
|
||||
_ => Cow::Owned(format!("<{flags:?}>")),
|
||||
},
|
||||
_ => Cow::Owned(format!("<{flags:?}>")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_arg(
|
||||
from: OpKind,
|
||||
to: ObjInsArg,
|
||||
args: &mut [ObjInsArg],
|
||||
instruction: &Instruction,
|
||||
ins_operands: &[Option<u32>],
|
||||
) -> Result<bool> {
|
||||
let mut replace = None;
|
||||
for i in 0..instruction.op_count() {
|
||||
let op_kind = instruction.op_kind(i);
|
||||
if op_kind == from {
|
||||
replace = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(i) = replace {
|
||||
for (j, arg) in args.iter_mut().enumerate() {
|
||||
if ins_operands[j] == Some(i) {
|
||||
*arg = to;
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
struct InstructionFormatterOutput {
|
||||
formatted: String,
|
||||
ins: ObjIns,
|
||||
error: Option<anyhow::Error>,
|
||||
ins_operands: Vec<Option<u32>>,
|
||||
}
|
||||
|
||||
impl InstructionFormatterOutput {
|
||||
fn push_signed(&mut self, value: i64) {
|
||||
// The formatter writes the '-' operator and then gives us a negative value,
|
||||
// so convert it to a positive value to avoid double negatives
|
||||
if value < 0
|
||||
&& matches!(self.ins.args.last(), Some(ObjInsArg::Arg(ObjInsArgValue::Opaque(v))) if v == "-")
|
||||
{
|
||||
self.ins.args.push(ObjInsArg::Arg(ObjInsArgValue::Signed(value.wrapping_abs())));
|
||||
} else {
|
||||
self.ins.args.push(ObjInsArg::Arg(ObjInsArgValue::Signed(value)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatterOutput for InstructionFormatterOutput {
|
||||
fn write(&mut self, text: &str, kind: FormatterTextKind) {
|
||||
self.formatted.push_str(text);
|
||||
// Skip whitespace after the mnemonic
|
||||
if self.ins.args.is_empty() && kind == FormatterTextKind::Text {
|
||||
return;
|
||||
}
|
||||
self.ins_operands.push(None);
|
||||
match kind {
|
||||
FormatterTextKind::Text | FormatterTextKind::Punctuation => {
|
||||
self.ins.args.push(ObjInsArg::PlainText(text.to_string().into()));
|
||||
}
|
||||
FormatterTextKind::Keyword | FormatterTextKind::Operator => {
|
||||
self.ins.args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(text.to_string().into())));
|
||||
}
|
||||
_ => {
|
||||
if self.error.is_none() {
|
||||
self.error = Some(anyhow!("x86: Unsupported FormatterTextKind {:?}", kind));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_prefix(&mut self, _instruction: &Instruction, text: &str, _prefix: PrefixKind) {
|
||||
self.formatted.push_str(text);
|
||||
self.ins_operands.push(None);
|
||||
self.ins.args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(text.to_string().into())));
|
||||
}
|
||||
|
||||
fn write_mnemonic(&mut self, _instruction: &Instruction, text: &str) {
|
||||
self.formatted.push_str(text);
|
||||
// TODO: can iced-x86 guarantee 'static here?
|
||||
self.ins.mnemonic = Cow::Owned(text.to_string());
|
||||
}
|
||||
|
||||
fn write_number(
|
||||
&mut self,
|
||||
_instruction: &Instruction,
|
||||
_operand: u32,
|
||||
instruction_operand: Option<u32>,
|
||||
text: &str,
|
||||
value: u64,
|
||||
number_kind: NumberKind,
|
||||
kind: FormatterTextKind,
|
||||
) {
|
||||
self.formatted.push_str(text);
|
||||
self.ins_operands.push(instruction_operand);
|
||||
|
||||
// Handle relocations
|
||||
match kind {
|
||||
FormatterTextKind::LabelAddress => {
|
||||
if let Some(reloc) = self.ins.reloc.as_ref() {
|
||||
if matches!(reloc.flags, RelocationFlags::Coff {
|
||||
typ: pe::IMAGE_REL_I386_DIR32 | pe::IMAGE_REL_I386_REL32
|
||||
}) {
|
||||
self.ins.args.push(ObjInsArg::Reloc);
|
||||
return;
|
||||
} else if self.error.is_none() {
|
||||
self.error = Some(anyhow!(
|
||||
"x86: Unsupported LabelAddress relocation flags {:?}",
|
||||
reloc.flags
|
||||
));
|
||||
}
|
||||
}
|
||||
self.ins.args.push(ObjInsArg::BranchDest(value));
|
||||
self.ins.branch_dest = Some(value);
|
||||
return;
|
||||
}
|
||||
FormatterTextKind::FunctionAddress => {
|
||||
if let Some(reloc) = self.ins.reloc.as_ref() {
|
||||
if matches!(reloc.flags, RelocationFlags::Coff {
|
||||
typ: pe::IMAGE_REL_I386_REL32
|
||||
}) {
|
||||
self.ins.args.push(ObjInsArg::Reloc);
|
||||
return;
|
||||
} else if self.error.is_none() {
|
||||
self.error = Some(anyhow!(
|
||||
"x86: Unsupported FunctionAddress relocation flags {:?}",
|
||||
reloc.flags
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match number_kind {
|
||||
NumberKind::Int8 => {
|
||||
self.push_signed(value as i8 as i64);
|
||||
}
|
||||
NumberKind::Int16 => {
|
||||
self.push_signed(value as i16 as i64);
|
||||
}
|
||||
NumberKind::Int32 => {
|
||||
self.push_signed(value as i32 as i64);
|
||||
}
|
||||
NumberKind::Int64 => {
|
||||
self.push_signed(value as i64);
|
||||
}
|
||||
NumberKind::UInt8 | NumberKind::UInt16 | NumberKind::UInt32 | NumberKind::UInt64 => {
|
||||
self.ins.args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(value)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_decorator(
|
||||
&mut self,
|
||||
_instruction: &Instruction,
|
||||
_operand: u32,
|
||||
instruction_operand: Option<u32>,
|
||||
text: &str,
|
||||
_decorator: DecoratorKind,
|
||||
) {
|
||||
self.formatted.push_str(text);
|
||||
self.ins_operands.push(instruction_operand);
|
||||
self.ins.args.push(ObjInsArg::PlainText(text.to_string().into()));
|
||||
}
|
||||
|
||||
fn write_register(
|
||||
&mut self,
|
||||
_instruction: &Instruction,
|
||||
_operand: u32,
|
||||
instruction_operand: Option<u32>,
|
||||
text: &str,
|
||||
_register: Register,
|
||||
) {
|
||||
self.formatted.push_str(text);
|
||||
self.ins_operands.push(instruction_operand);
|
||||
self.ins.args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(text.to_string().into())));
|
||||
}
|
||||
}
|
||||
246
objdiff-core/src/bindings/diff.rs
Normal file
@@ -0,0 +1,246 @@
|
||||
#![allow(clippy::needless_lifetimes)] // Generated serde code
|
||||
use crate::{
|
||||
diff::{
|
||||
ObjDataDiff, ObjDataDiffKind, ObjDiff, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo,
|
||||
ObjInsDiff, ObjInsDiffKind, ObjSectionDiff, ObjSymbolDiff,
|
||||
},
|
||||
obj::{
|
||||
ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSectionKind, ObjSymbol,
|
||||
ObjSymbolFlagSet, ObjSymbolFlags,
|
||||
},
|
||||
};
|
||||
|
||||
// Protobuf diff types
|
||||
include!(concat!(env!("OUT_DIR"), "/objdiff.diff.rs"));
|
||||
include!(concat!(env!("OUT_DIR"), "/objdiff.diff.serde.rs"));
|
||||
|
||||
impl DiffResult {
|
||||
pub fn new(left: Option<(&ObjInfo, &ObjDiff)>, right: Option<(&ObjInfo, &ObjDiff)>) -> Self {
|
||||
Self {
|
||||
left: left.map(|(obj, diff)| ObjectDiff::new(obj, diff)),
|
||||
right: right.map(|(obj, diff)| ObjectDiff::new(obj, diff)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectDiff {
|
||||
pub fn new(obj: &ObjInfo, diff: &ObjDiff) -> Self {
|
||||
Self {
|
||||
sections: diff
|
||||
.sections
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, d)| SectionDiff::new(obj, i, d))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SectionDiff {
|
||||
pub fn new(obj: &ObjInfo, section_index: usize, section_diff: &ObjSectionDiff) -> Self {
|
||||
let section = &obj.sections[section_index];
|
||||
let functions = section_diff.symbols.iter().map(|d| FunctionDiff::new(obj, d)).collect();
|
||||
let data = section_diff.data_diff.iter().map(|d| DataDiff::new(obj, d)).collect();
|
||||
Self {
|
||||
name: section.name.to_string(),
|
||||
kind: SectionKind::from(section.kind) as i32,
|
||||
size: section.size,
|
||||
address: section.address,
|
||||
functions,
|
||||
data,
|
||||
match_percent: section_diff.match_percent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ObjSectionKind> for SectionKind {
|
||||
fn from(value: ObjSectionKind) -> Self {
|
||||
match value {
|
||||
ObjSectionKind::Code => SectionKind::SectionText,
|
||||
ObjSectionKind::Data => SectionKind::SectionData,
|
||||
ObjSectionKind::Bss => SectionKind::SectionBss,
|
||||
// TODO common
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FunctionDiff {
|
||||
pub fn new(object: &ObjInfo, symbol_diff: &ObjSymbolDiff) -> Self {
|
||||
let (_section, symbol) = object.section_symbol(symbol_diff.symbol_ref);
|
||||
// let diff_symbol = symbol_diff.diff_symbol.map(|symbol_ref| {
|
||||
// let (_section, symbol) = object.section_symbol(symbol_ref);
|
||||
// Symbol::from(symbol)
|
||||
// });
|
||||
let instructions = symbol_diff
|
||||
.instructions
|
||||
.iter()
|
||||
.map(|ins_diff| InstructionDiff::new(object, ins_diff))
|
||||
.collect();
|
||||
Self {
|
||||
symbol: Some(Symbol::new(symbol)),
|
||||
// diff_symbol,
|
||||
instructions,
|
||||
match_percent: symbol_diff.match_percent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DataDiff {
|
||||
pub fn new(_object: &ObjInfo, data_diff: &ObjDataDiff) -> Self {
|
||||
Self {
|
||||
kind: DiffKind::from(data_diff.kind) as i32,
|
||||
data: data_diff.data.clone(),
|
||||
size: data_diff.len as u64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Symbol {
|
||||
pub fn new(value: &ObjSymbol) -> Self {
|
||||
Self {
|
||||
name: value.name.to_string(),
|
||||
demangled_name: value.demangled_name.clone(),
|
||||
address: value.address,
|
||||
size: value.size,
|
||||
flags: symbol_flags(value.flags),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn symbol_flags(value: ObjSymbolFlagSet) -> u32 {
|
||||
let mut flags = 0u32;
|
||||
if value.0.contains(ObjSymbolFlags::Global) {
|
||||
flags |= SymbolFlag::SymbolNone as u32;
|
||||
}
|
||||
if value.0.contains(ObjSymbolFlags::Local) {
|
||||
flags |= SymbolFlag::SymbolLocal as u32;
|
||||
}
|
||||
if value.0.contains(ObjSymbolFlags::Weak) {
|
||||
flags |= SymbolFlag::SymbolWeak as u32;
|
||||
}
|
||||
if value.0.contains(ObjSymbolFlags::Common) {
|
||||
flags |= SymbolFlag::SymbolCommon as u32;
|
||||
}
|
||||
if value.0.contains(ObjSymbolFlags::Hidden) {
|
||||
flags |= SymbolFlag::SymbolHidden as u32;
|
||||
}
|
||||
flags
|
||||
}
|
||||
|
||||
impl Instruction {
|
||||
pub fn new(object: &ObjInfo, instruction: &ObjIns) -> Self {
|
||||
Self {
|
||||
address: instruction.address,
|
||||
size: instruction.size as u32,
|
||||
opcode: instruction.op as u32,
|
||||
mnemonic: instruction.mnemonic.to_string(),
|
||||
formatted: instruction.formatted.clone(),
|
||||
arguments: instruction.args.iter().map(Argument::new).collect(),
|
||||
relocation: instruction.reloc.as_ref().map(|reloc| Relocation::new(object, reloc)),
|
||||
branch_dest: instruction.branch_dest,
|
||||
line_number: instruction.line,
|
||||
original: instruction.orig.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Argument {
|
||||
pub fn new(value: &ObjInsArg) -> Self {
|
||||
Self {
|
||||
value: Some(match value {
|
||||
ObjInsArg::PlainText(s) => argument::Value::PlainText(s.to_string()),
|
||||
ObjInsArg::Arg(v) => argument::Value::Argument(ArgumentValue::new(v)),
|
||||
ObjInsArg::Reloc => argument::Value::Relocation(ArgumentRelocation {}),
|
||||
ObjInsArg::BranchDest(dest) => argument::Value::BranchDest(*dest),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ArgumentValue {
|
||||
pub fn new(value: &ObjInsArgValue) -> Self {
|
||||
Self {
|
||||
value: Some(match value {
|
||||
ObjInsArgValue::Signed(v) => argument_value::Value::Signed(*v),
|
||||
ObjInsArgValue::Unsigned(v) => argument_value::Value::Unsigned(*v),
|
||||
ObjInsArgValue::Opaque(v) => argument_value::Value::Opaque(v.to_string()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Relocation {
|
||||
pub fn new(object: &ObjInfo, reloc: &ObjReloc) -> Self {
|
||||
Self {
|
||||
r#type: match reloc.flags {
|
||||
object::RelocationFlags::Elf { r_type } => r_type,
|
||||
object::RelocationFlags::MachO { r_type, .. } => r_type as u32,
|
||||
object::RelocationFlags::Coff { typ } => typ as u32,
|
||||
object::RelocationFlags::Xcoff { r_rtype, .. } => r_rtype as u32,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
type_name: object.arch.display_reloc(reloc.flags).into_owned(),
|
||||
target: Some(RelocationTarget {
|
||||
symbol: Some(Symbol::new(&reloc.target)),
|
||||
addend: reloc.addend,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InstructionDiff {
|
||||
pub fn new(object: &ObjInfo, instruction_diff: &ObjInsDiff) -> Self {
|
||||
Self {
|
||||
instruction: instruction_diff.ins.as_ref().map(|ins| Instruction::new(object, ins)),
|
||||
diff_kind: DiffKind::from(instruction_diff.kind) as i32,
|
||||
branch_from: instruction_diff.branch_from.as_ref().map(InstructionBranchFrom::new),
|
||||
branch_to: instruction_diff.branch_to.as_ref().map(InstructionBranchTo::new),
|
||||
arg_diff: instruction_diff.arg_diff.iter().map(ArgumentDiff::new).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ArgumentDiff {
|
||||
pub fn new(value: &Option<ObjInsArgDiff>) -> Self {
|
||||
Self { diff_index: value.as_ref().map(|v| v.idx as u32) }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ObjInsDiffKind> for DiffKind {
|
||||
fn from(value: ObjInsDiffKind) -> Self {
|
||||
match value {
|
||||
ObjInsDiffKind::None => DiffKind::DiffNone,
|
||||
ObjInsDiffKind::OpMismatch => DiffKind::DiffOpMismatch,
|
||||
ObjInsDiffKind::ArgMismatch => DiffKind::DiffArgMismatch,
|
||||
ObjInsDiffKind::Replace => DiffKind::DiffReplace,
|
||||
ObjInsDiffKind::Delete => DiffKind::DiffDelete,
|
||||
ObjInsDiffKind::Insert => DiffKind::DiffInsert,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ObjDataDiffKind> for DiffKind {
|
||||
fn from(value: ObjDataDiffKind) -> Self {
|
||||
match value {
|
||||
ObjDataDiffKind::None => DiffKind::DiffNone,
|
||||
ObjDataDiffKind::Replace => DiffKind::DiffReplace,
|
||||
ObjDataDiffKind::Delete => DiffKind::DiffDelete,
|
||||
ObjDataDiffKind::Insert => DiffKind::DiffInsert,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InstructionBranchFrom {
|
||||
pub fn new(value: &ObjInsBranchFrom) -> Self {
|
||||
Self {
|
||||
instruction_index: value.ins_idx.iter().map(|&x| x as u32).collect(),
|
||||
branch_index: value.branch_idx as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InstructionBranchTo {
|
||||
pub fn new(value: &ObjInsBranchTo) -> Self {
|
||||
Self { instruction_index: value.ins_idx as u32, branch_index: value.branch_idx as u32 }
|
||||
}
|
||||
}
|
||||
5
objdiff-core/src/bindings/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#[cfg(feature = "any-arch")]
|
||||
pub mod diff;
|
||||
pub mod report;
|
||||
#[cfg(feature = "wasm")]
|
||||
pub mod wasm;
|
||||
442
objdiff-core/src/bindings/report.rs
Normal file
@@ -0,0 +1,442 @@
|
||||
#![allow(clippy::needless_lifetimes)] // Generated serde code
|
||||
use std::ops::AddAssign;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use prost::Message;
|
||||
use serde_json::error::Category;
|
||||
|
||||
// Protobuf report types
|
||||
include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs"));
|
||||
include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs"));
|
||||
|
||||
pub const REPORT_VERSION: u32 = 2;
|
||||
|
||||
impl Report {
|
||||
/// Attempts to parse the report as binary protobuf or JSON.
|
||||
pub fn parse(data: &[u8]) -> Result<Self> {
|
||||
if data.is_empty() {
|
||||
bail!(std::io::Error::from(std::io::ErrorKind::UnexpectedEof));
|
||||
}
|
||||
let report = if data[0] == b'{' {
|
||||
// Load as JSON
|
||||
Self::from_json(data)?
|
||||
} else {
|
||||
// Load as binary protobuf
|
||||
Self::decode(data)?
|
||||
};
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
/// Attempts to parse the report as JSON, migrating from the legacy report format if necessary.
|
||||
fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
|
||||
match serde_json::from_slice::<Self>(bytes) {
|
||||
Ok(report) => Ok(report),
|
||||
Err(e) => {
|
||||
match e.classify() {
|
||||
Category::Io | Category::Eof | Category::Syntax => Err(e),
|
||||
Category::Data => {
|
||||
// Try to load as legacy report
|
||||
match serde_json::from_slice::<LegacyReport>(bytes) {
|
||||
Ok(legacy_report) => Ok(Report::from(legacy_report)),
|
||||
Err(_) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrates the report to the latest version.
|
||||
/// Fails if the report version is newer than supported.
|
||||
pub fn migrate(&mut self) -> Result<()> {
|
||||
if self.version == 0 {
|
||||
self.migrate_v0()?;
|
||||
}
|
||||
if self.version == 1 {
|
||||
self.migrate_v1()?;
|
||||
}
|
||||
if self.version != REPORT_VERSION {
|
||||
bail!("Unsupported report version: {}", self.version);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds `complete_code`, `complete_data`, `complete_code_percent`, and `complete_data_percent`
|
||||
/// to measures, and sets `progress_categories` in unit metadata.
|
||||
fn migrate_v0(&mut self) -> Result<()> {
|
||||
let Some(measures) = &mut self.measures else {
|
||||
bail!("Missing measures in report");
|
||||
};
|
||||
for unit in &mut self.units {
|
||||
let Some(unit_measures) = &mut unit.measures else {
|
||||
bail!("Missing measures in report unit");
|
||||
};
|
||||
let mut complete = false;
|
||||
if let Some(metadata) = &mut unit.metadata {
|
||||
if metadata.module_name.is_some() || metadata.module_id.is_some() {
|
||||
metadata.progress_categories = vec!["modules".to_string()];
|
||||
} else {
|
||||
metadata.progress_categories = vec!["dol".to_string()];
|
||||
}
|
||||
complete = metadata.complete.unwrap_or(false);
|
||||
};
|
||||
if complete {
|
||||
unit_measures.complete_code = unit_measures.total_code;
|
||||
unit_measures.complete_data = unit_measures.total_data;
|
||||
unit_measures.complete_code_percent = 100.0;
|
||||
unit_measures.complete_data_percent = 100.0;
|
||||
} else {
|
||||
unit_measures.complete_code = 0;
|
||||
unit_measures.complete_data = 0;
|
||||
unit_measures.complete_code_percent = 0.0;
|
||||
unit_measures.complete_data_percent = 0.0;
|
||||
}
|
||||
measures.complete_code += unit_measures.complete_code;
|
||||
measures.complete_data += unit_measures.complete_data;
|
||||
}
|
||||
measures.calc_matched_percent();
|
||||
self.calculate_progress_categories();
|
||||
self.version = 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds `total_units` and `complete_units` to measures.
|
||||
fn migrate_v1(&mut self) -> Result<()> {
|
||||
let Some(total_measures) = &mut self.measures else {
|
||||
bail!("Missing measures in report");
|
||||
};
|
||||
for unit in &mut self.units {
|
||||
let Some(measures) = &mut unit.measures else {
|
||||
bail!("Missing measures in report unit");
|
||||
};
|
||||
let complete = unit.metadata.as_ref().and_then(|m| m.complete).unwrap_or(false) as u32;
|
||||
let progress_categories =
|
||||
unit.metadata.as_ref().map(|m| m.progress_categories.as_slice()).unwrap_or(&[]);
|
||||
measures.total_units = 1;
|
||||
measures.complete_units = complete;
|
||||
total_measures.total_units += 1;
|
||||
total_measures.complete_units += complete;
|
||||
for id in progress_categories {
|
||||
if let Some(category) = self.categories.iter_mut().find(|c| &c.id == id) {
|
||||
let Some(measures) = &mut category.measures else {
|
||||
bail!("Missing measures in category");
|
||||
};
|
||||
measures.total_units += 1;
|
||||
measures.complete_units += complete;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.version = 2;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Calculate progress categories based on unit metadata.
|
||||
pub fn calculate_progress_categories(&mut self) {
|
||||
for unit in &self.units {
|
||||
let Some(metadata) = unit.metadata.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
let Some(measures) = unit.measures.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
for category_id in &metadata.progress_categories {
|
||||
let category = match self.categories.iter_mut().find(|c| &c.id == category_id) {
|
||||
Some(category) => category,
|
||||
None => {
|
||||
self.categories.push(ReportCategory {
|
||||
id: category_id.clone(),
|
||||
name: String::new(),
|
||||
measures: Some(Default::default()),
|
||||
});
|
||||
self.categories.last_mut().unwrap()
|
||||
}
|
||||
};
|
||||
*category.measures.get_or_insert_with(Default::default) += *measures;
|
||||
}
|
||||
}
|
||||
for category in &mut self.categories {
|
||||
let measures = category.measures.get_or_insert_with(Default::default);
|
||||
measures.calc_fuzzy_match_percent();
|
||||
measures.calc_matched_percent();
|
||||
}
|
||||
}
|
||||
|
||||
/// Split the report into multiple reports based on progress categories.
|
||||
/// Assumes progress categories are in the format `version`, `version.category`.
|
||||
/// This is a hack for projects that generate all versions in a single report.
|
||||
pub fn split(self) -> Vec<(String, Report)> {
|
||||
let mut reports = Vec::new();
|
||||
// Map units to Option to allow taking ownership
|
||||
let mut units = self.units.into_iter().map(Some).collect::<Vec<_>>();
|
||||
for category in &self.categories {
|
||||
if category.id.contains(".") {
|
||||
// Skip subcategories
|
||||
continue;
|
||||
}
|
||||
fn is_sub_category(id: &str, parent: &str, sep: char) -> bool {
|
||||
id.starts_with(parent) && id.get(parent.len()..).is_some_and(|s| s.starts_with(sep))
|
||||
}
|
||||
let mut sub_categories = self
|
||||
.categories
|
||||
.iter()
|
||||
.filter(|c| is_sub_category(&c.id, &category.id, '.'))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
// Remove category prefix
|
||||
for sub_category in &mut sub_categories {
|
||||
sub_category.id = sub_category.id[category.id.len() + 1..].to_string();
|
||||
}
|
||||
let mut sub_units = units
|
||||
.iter_mut()
|
||||
.filter_map(|opt| {
|
||||
let unit = opt.as_mut()?;
|
||||
let metadata = unit.metadata.as_ref()?;
|
||||
if metadata.progress_categories.contains(&category.id) {
|
||||
opt.take()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for sub_unit in &mut sub_units {
|
||||
// Remove leading version/ from unit name
|
||||
if let Some(name) =
|
||||
sub_unit.name.strip_prefix(&category.id).and_then(|s| s.strip_prefix('/'))
|
||||
{
|
||||
sub_unit.name = name.to_string();
|
||||
}
|
||||
// Filter progress categories
|
||||
let Some(metadata) = sub_unit.metadata.as_mut() else {
|
||||
continue;
|
||||
};
|
||||
metadata.progress_categories = metadata
|
||||
.progress_categories
|
||||
.iter()
|
||||
.filter(|c| is_sub_category(c, &category.id, '.'))
|
||||
.map(|c| c[category.id.len() + 1..].to_string())
|
||||
.collect();
|
||||
}
|
||||
reports.push((category.id.clone(), Report {
|
||||
measures: category.measures,
|
||||
units: sub_units,
|
||||
version: self.version,
|
||||
categories: sub_categories,
|
||||
}));
|
||||
}
|
||||
reports
|
||||
}
|
||||
}
|
||||
|
||||
impl Measures {
|
||||
/// Average the fuzzy match percentage over total code bytes.
|
||||
pub fn calc_fuzzy_match_percent(&mut self) {
|
||||
if self.total_code == 0 {
|
||||
self.fuzzy_match_percent = 100.0;
|
||||
} else {
|
||||
self.fuzzy_match_percent /= self.total_code as f32;
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the percentage of matched code, data, and functions.
|
||||
pub fn calc_matched_percent(&mut self) {
|
||||
self.matched_code_percent = if self.total_code == 0 {
|
||||
100.0
|
||||
} else {
|
||||
self.matched_code as f32 / self.total_code as f32 * 100.0
|
||||
};
|
||||
self.matched_data_percent = if self.total_data == 0 {
|
||||
100.0
|
||||
} else {
|
||||
self.matched_data as f32 / self.total_data as f32 * 100.0
|
||||
};
|
||||
self.matched_functions_percent = if self.total_functions == 0 {
|
||||
100.0
|
||||
} else {
|
||||
self.matched_functions as f32 / self.total_functions as f32 * 100.0
|
||||
};
|
||||
self.complete_code_percent = if self.total_code == 0 {
|
||||
100.0
|
||||
} else {
|
||||
self.complete_code as f32 / self.total_code as f32 * 100.0
|
||||
};
|
||||
self.complete_data_percent = if self.total_data == 0 {
|
||||
100.0
|
||||
} else {
|
||||
self.complete_data as f32 / self.total_data as f32 * 100.0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ReportItem> for ChangeItemInfo {
|
||||
fn from(value: &ReportItem) -> Self {
|
||||
Self { fuzzy_match_percent: value.fuzzy_match_percent, size: value.size }
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign for Measures {
|
||||
fn add_assign(&mut self, other: Self) {
|
||||
self.fuzzy_match_percent += other.fuzzy_match_percent * other.total_code as f32;
|
||||
self.total_code += other.total_code;
|
||||
self.matched_code += other.matched_code;
|
||||
self.total_data += other.total_data;
|
||||
self.matched_data += other.matched_data;
|
||||
self.total_functions += other.total_functions;
|
||||
self.matched_functions += other.matched_functions;
|
||||
self.complete_code += other.complete_code;
|
||||
self.complete_data += other.complete_data;
|
||||
self.total_units += other.total_units;
|
||||
self.complete_units += other.complete_units;
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows [collect](Iterator::collect) to be used on an iterator of [Measures].
|
||||
impl FromIterator<Measures> for Measures {
|
||||
fn from_iter<T>(iter: T) -> Self
|
||||
where T: IntoIterator<Item = Measures> {
|
||||
let mut measures = Measures::default();
|
||||
for other in iter {
|
||||
measures += other;
|
||||
}
|
||||
measures.calc_fuzzy_match_percent();
|
||||
measures.calc_matched_percent();
|
||||
measures
|
||||
}
|
||||
}
|
||||
|
||||
// Older JSON report types
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
struct LegacyReport {
|
||||
fuzzy_match_percent: f32,
|
||||
total_code: u64,
|
||||
matched_code: u64,
|
||||
matched_code_percent: f32,
|
||||
total_data: u64,
|
||||
matched_data: u64,
|
||||
matched_data_percent: f32,
|
||||
total_functions: u32,
|
||||
matched_functions: u32,
|
||||
matched_functions_percent: f32,
|
||||
units: Vec<LegacyReportUnit>,
|
||||
}
|
||||
|
||||
impl From<LegacyReport> for Report {
|
||||
fn from(value: LegacyReport) -> Self {
|
||||
Self {
|
||||
measures: Some(Measures {
|
||||
fuzzy_match_percent: value.fuzzy_match_percent,
|
||||
total_code: value.total_code,
|
||||
matched_code: value.matched_code,
|
||||
matched_code_percent: value.matched_code_percent,
|
||||
total_data: value.total_data,
|
||||
matched_data: value.matched_data,
|
||||
matched_data_percent: value.matched_data_percent,
|
||||
total_functions: value.total_functions,
|
||||
matched_functions: value.matched_functions,
|
||||
matched_functions_percent: value.matched_functions_percent,
|
||||
..Default::default()
|
||||
}),
|
||||
units: value.units.into_iter().map(ReportUnit::from).collect::<Vec<_>>(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
struct LegacyReportUnit {
|
||||
name: String,
|
||||
fuzzy_match_percent: f32,
|
||||
total_code: u64,
|
||||
matched_code: u64,
|
||||
total_data: u64,
|
||||
matched_data: u64,
|
||||
total_functions: u32,
|
||||
matched_functions: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
complete: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
module_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
module_id: Option<u32>,
|
||||
sections: Vec<LegacyReportItem>,
|
||||
functions: Vec<LegacyReportItem>,
|
||||
}
|
||||
|
||||
impl From<LegacyReportUnit> for ReportUnit {
|
||||
fn from(value: LegacyReportUnit) -> Self {
|
||||
let mut measures = Measures {
|
||||
fuzzy_match_percent: value.fuzzy_match_percent,
|
||||
total_code: value.total_code,
|
||||
matched_code: value.matched_code,
|
||||
total_data: value.total_data,
|
||||
matched_data: value.matched_data,
|
||||
total_functions: value.total_functions,
|
||||
matched_functions: value.matched_functions,
|
||||
..Default::default()
|
||||
};
|
||||
measures.calc_matched_percent();
|
||||
Self {
|
||||
name: value.name.clone(),
|
||||
measures: Some(measures),
|
||||
sections: value.sections.into_iter().map(ReportItem::from).collect(),
|
||||
functions: value.functions.into_iter().map(ReportItem::from).collect(),
|
||||
metadata: Some(ReportUnitMetadata {
|
||||
complete: value.complete,
|
||||
module_name: value.module_name.clone(),
|
||||
module_id: value.module_id,
|
||||
..Default::default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
struct LegacyReportItem {
|
||||
name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
demangled_name: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
serialize_with = "serialize_hex",
|
||||
deserialize_with = "deserialize_hex"
|
||||
)]
|
||||
address: Option<u64>,
|
||||
size: u64,
|
||||
fuzzy_match_percent: f32,
|
||||
}
|
||||
|
||||
impl From<LegacyReportItem> for ReportItem {
|
||||
fn from(value: LegacyReportItem) -> Self {
|
||||
Self {
|
||||
name: value.name,
|
||||
size: value.size,
|
||||
fuzzy_match_percent: value.fuzzy_match_percent,
|
||||
metadata: Some(ReportItemMetadata {
|
||||
demangled_name: value.demangled_name,
|
||||
virtual_address: value.address,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_hex<S>(x: &Option<u64>, s: S) -> Result<S::Ok, S::Error>
|
||||
where S: serde::Serializer {
|
||||
if let Some(x) = x {
|
||||
s.serialize_str(&format!("{:#x}", x))
|
||||
} else {
|
||||
s.serialize_none()
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_hex<'de, D>(d: D) -> Result<Option<u64>, D::Error>
|
||||
where D: serde::Deserializer<'de> {
|
||||
use serde::Deserialize;
|
||||
let s = String::deserialize(d)?;
|
||||
if s.is_empty() {
|
||||
Ok(None)
|
||||
} else if !s.starts_with("0x") {
|
||||
Err(serde::de::Error::custom("expected hex string"))
|
||||
} else {
|
||||
u64::from_str_radix(&s[2..], 16).map(Some).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
78
objdiff-core/src/bindings/wasm.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use prost::Message;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{bindings::diff::DiffResult, diff, obj};
|
||||
|
||||
fn parse_object(
|
||||
data: Option<Box<[u8]>>,
|
||||
config: &diff::DiffObjConfig,
|
||||
) -> Result<Option<obj::ObjInfo>, JsError> {
|
||||
data.as_ref().map(|data| obj::read::parse(data, config)).transpose().to_js()
|
||||
}
|
||||
|
||||
fn parse_and_run_diff(
|
||||
left: Option<Box<[u8]>>,
|
||||
right: Option<Box<[u8]>>,
|
||||
config: diff::DiffObjConfig,
|
||||
) -> Result<DiffResult, JsError> {
|
||||
let target = parse_object(left, &config)?;
|
||||
let base = parse_object(right, &config)?;
|
||||
run_diff(target.as_ref(), base.as_ref(), config)
|
||||
}
|
||||
|
||||
fn run_diff(
|
||||
left: Option<&obj::ObjInfo>,
|
||||
right: Option<&obj::ObjInfo>,
|
||||
config: diff::DiffObjConfig,
|
||||
) -> Result<DiffResult, JsError> {
|
||||
log::debug!("Running diff with config: {:?}", config);
|
||||
let result = diff::diff_objs(&config, left, right, None).to_js()?;
|
||||
let left = left.and_then(|o| result.left.as_ref().map(|d| (o, d)));
|
||||
let right = right.and_then(|o| result.right.as_ref().map(|d| (o, d)));
|
||||
Ok(DiffResult::new(left, right))
|
||||
}
|
||||
|
||||
// #[wasm_bindgen]
|
||||
// pub fn run_diff_json(
|
||||
// left: Option<Box<[u8]>>,
|
||||
// right: Option<Box<[u8]>>,
|
||||
// config: diff::DiffObjConfig,
|
||||
// ) -> Result<String, JsError> {
|
||||
// let out = run_diff_opt_box(left, right, config)?;
|
||||
// serde_json::to_string(&out).map_err(|e| JsError::new(&e.to_string()))
|
||||
// }
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn run_diff_proto(
|
||||
left: Option<Box<[u8]>>,
|
||||
right: Option<Box<[u8]>>,
|
||||
config: diff::DiffObjConfig,
|
||||
) -> Result<Box<[u8]>, JsError> {
|
||||
let out = parse_and_run_diff(left, right, config)?;
|
||||
Ok(out.encode_to_vec().into_boxed_slice())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
fn start() -> Result<(), JsError> {
|
||||
console_error_panic_hook::set_once();
|
||||
#[cfg(debug_assertions)]
|
||||
console_log::init_with_level(log::Level::Debug).to_js()?;
|
||||
#[cfg(not(debug_assertions))]
|
||||
console_log::init_with_level(log::Level::Info).to_js()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn to_js_error(e: impl std::fmt::Display) -> JsError { JsError::new(&e.to_string()) }
|
||||
|
||||
trait ToJsResult {
|
||||
type Output;
|
||||
|
||||
fn to_js(self) -> Result<Self::Output, JsError>;
|
||||
}
|
||||
|
||||
impl<T, E: std::fmt::Display> ToJsResult for Result<T, E> {
|
||||
type Output = T;
|
||||
|
||||
fn to_js(self) -> Result<T, JsError> { self.map_err(to_js_error) }
|
||||
}
|
||||
269
objdiff-core/src/config/mod.rs
Normal file
@@ -0,0 +1,269 @@
|
||||
use std::{
|
||||
fs,
|
||||
fs::File,
|
||||
io::{BufReader, BufWriter, Read},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use bimap::BiBTreeMap;
|
||||
use filetime::FileTime;
|
||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ProjectConfig {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub min_version: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub custom_make: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub custom_args: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub target_dir: Option<PathBuf>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub base_dir: Option<PathBuf>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub build_base: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub build_target: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub watch_patterns: Option<Vec<Glob>>,
|
||||
#[serde(default, alias = "objects", skip_serializing_if = "Option::is_none")]
|
||||
pub units: Option<Vec<ProjectObject>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub progress_categories: Option<Vec<ProjectProgressCategory>>,
|
||||
}
|
||||
|
||||
impl ProjectConfig {
|
||||
#[inline]
|
||||
pub fn units(&self) -> &[ProjectObject] { self.units.as_deref().unwrap_or_default() }
|
||||
|
||||
#[inline]
|
||||
pub fn units_mut(&mut self) -> &mut Vec<ProjectObject> {
|
||||
self.units.get_or_insert_with(Vec::new)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn progress_categories(&self) -> &[ProjectProgressCategory] {
|
||||
self.progress_categories.as_deref().unwrap_or_default()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn progress_categories_mut(&mut self) -> &mut Vec<ProjectProgressCategory> {
|
||||
self.progress_categories.get_or_insert_with(Vec::new)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ProjectObject {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub path: Option<PathBuf>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub target_path: Option<PathBuf>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub base_path: Option<PathBuf>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[deprecated(note = "Use metadata.reverse_fn_order")]
|
||||
pub reverse_fn_order: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[deprecated(note = "Use metadata.complete")]
|
||||
pub complete: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub scratch: Option<ScratchConfig>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub metadata: Option<ProjectObjectMetadata>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub symbol_mappings: Option<SymbolMappings>,
|
||||
}
|
||||
|
||||
pub type SymbolMappings = BiBTreeMap<String, String>;
|
||||
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ProjectObjectMetadata {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub complete: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub reverse_fn_order: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub source_path: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub progress_categories: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub auto_generated: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ProjectProgressCategory {
|
||||
#[serde(default)]
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
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]"
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_paths(
|
||||
&mut self,
|
||||
project_dir: &Path,
|
||||
target_obj_dir: Option<&Path>,
|
||||
base_obj_dir: Option<&Path>,
|
||||
) {
|
||||
if let (Some(target_obj_dir), Some(path), None) =
|
||||
(target_obj_dir, &self.path, &self.target_path)
|
||||
{
|
||||
self.target_path = Some(target_obj_dir.join(path));
|
||||
} else if let Some(path) = &self.target_path {
|
||||
self.target_path = Some(project_dir.join(path));
|
||||
}
|
||||
if let (Some(base_obj_dir), Some(path), None) = (base_obj_dir, &self.path, &self.base_path)
|
||||
{
|
||||
self.base_path = Some(base_obj_dir.join(path));
|
||||
} else if let Some(path) = &self.base_path {
|
||||
self.base_path = Some(project_dir.join(path));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn complete(&self) -> Option<bool> {
|
||||
#[expect(deprecated)]
|
||||
self.metadata.as_ref().and_then(|m| m.complete).or(self.complete)
|
||||
}
|
||||
|
||||
pub fn reverse_fn_order(&self) -> Option<bool> {
|
||||
#[expect(deprecated)]
|
||||
self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order)
|
||||
}
|
||||
|
||||
pub fn hidden(&self) -> bool {
|
||||
self.metadata.as_ref().and_then(|m| m.auto_generated).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn source_path(&self) -> Option<&String> {
|
||||
self.metadata.as_ref().and_then(|m| m.source_path.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct ScratchConfig {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub platform: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub compiler: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub c_flags: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ctx_path: Option<PathBuf>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub build_ctx: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub preset_id: Option<u32>,
|
||||
}
|
||||
|
||||
pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"];
|
||||
|
||||
pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[
|
||||
"*.c", "*.cp", "*.cpp", "*.cxx", "*.h", "*.hp", "*.hpp", "*.hxx", "*.s", "*.S", "*.asm",
|
||||
"*.inc", "*.py", "*.yml", "*.txt", "*.json",
|
||||
];
|
||||
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct ProjectConfigInfo {
|
||||
pub path: PathBuf,
|
||||
pub timestamp: Option<FileTime>,
|
||||
}
|
||||
|
||||
pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
|
||||
for filename in CONFIG_FILENAMES.iter() {
|
||||
let config_path = dir.join(filename);
|
||||
let Ok(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 mut reader = BufReader::new(file);
|
||||
let mut result = match filename.contains("json") {
|
||||
true => read_json_config(&mut reader),
|
||||
false => read_yml_config(&mut reader),
|
||||
};
|
||||
if let Ok(config) = &result {
|
||||
// Validate min_version if present
|
||||
if let Err(e) = validate_min_version(config) {
|
||||
result = Err(e);
|
||||
}
|
||||
}
|
||||
return Some((result, ProjectConfigInfo { path: config_path, timestamp: Some(ts) }));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn save_project_config(
|
||||
config: &ProjectConfig,
|
||||
info: &ProjectConfigInfo,
|
||||
) -> Result<ProjectConfigInfo> {
|
||||
if let Some(last_ts) = info.timestamp {
|
||||
// Check if the file has changed since we last read it
|
||||
if let Ok(metadata) = fs::metadata(&info.path) {
|
||||
let ts = FileTime::from_last_modification_time(&metadata);
|
||||
if ts != last_ts {
|
||||
return Err(anyhow!("Config file has changed since last read"));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut writer =
|
||||
BufWriter::new(File::create(&info.path).context("Failed to create config file")?);
|
||||
let ext = info.path.extension().and_then(|ext| ext.to_str()).unwrap_or("json");
|
||||
match ext {
|
||||
"json" => serde_json::to_writer_pretty(&mut writer, config).context("Failed to write JSON"),
|
||||
"yml" | "yaml" => {
|
||||
serde_yaml::to_writer(&mut writer, config).context("Failed to write YAML")
|
||||
}
|
||||
_ => Err(anyhow!("Unknown config file extension: {ext}")),
|
||||
}?;
|
||||
let file = writer.into_inner().context("Failed to flush file")?;
|
||||
let metadata = file.metadata().context("Failed to get file metadata")?;
|
||||
let ts = FileTime::from_last_modification_time(&metadata);
|
||||
Ok(ProjectConfigInfo { path: info.path.clone(), timestamp: Some(ts) })
|
||||
}
|
||||
|
||||
fn validate_min_version(config: &ProjectConfig) -> Result<()> {
|
||||
let Some(min_version) = &config.min_version else { return Ok(()) };
|
||||
let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))
|
||||
.context("Failed to parse package version")?;
|
||||
let min_version = semver::Version::parse(min_version).context("Failed to parse min_version")?;
|
||||
if version >= min_version {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("Project requires objdiff version {min_version} or higher"))
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
371
objdiff-core/src/diff/code.rs
Normal file
@@ -0,0 +1,371 @@
|
||||
use std::{cmp::max, collections::BTreeMap};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use similar::{capture_diff_slices_deadline, Algorithm};
|
||||
|
||||
use crate::{
|
||||
arch::ProcessCodeResult,
|
||||
diff::{
|
||||
DiffObjConfig, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjInsDiff, ObjInsDiffKind,
|
||||
ObjSymbolDiff,
|
||||
},
|
||||
obj::{ObjInfo, ObjInsArg, ObjReloc, ObjSymbolFlags, SymbolRef},
|
||||
};
|
||||
|
||||
pub fn process_code_symbol(
|
||||
obj: &ObjInfo,
|
||||
symbol_ref: SymbolRef,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<ProcessCodeResult> {
|
||||
let (section, symbol) = obj.section_symbol(symbol_ref);
|
||||
let section = section.ok_or_else(|| anyhow!("Code symbol section not found"))?;
|
||||
let code = §ion.data
|
||||
[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
|
||||
obj.arch.process_code(
|
||||
symbol.address,
|
||||
code,
|
||||
section.orig_index,
|
||||
§ion.relocations,
|
||||
§ion.line_info,
|
||||
config,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn no_diff_code(out: &ProcessCodeResult, symbol_ref: SymbolRef) -> Result<ObjSymbolDiff> {
|
||||
let mut diff = Vec::<ObjInsDiff>::new();
|
||||
for i in &out.insts {
|
||||
diff.push(ObjInsDiff {
|
||||
ins: Some(i.clone()),
|
||||
kind: ObjInsDiffKind::None,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
resolve_branches(&mut diff);
|
||||
Ok(ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: diff, match_percent: None })
|
||||
}
|
||||
|
||||
pub fn diff_code(
|
||||
left_obj: &ObjInfo,
|
||||
right_obj: &ObjInfo,
|
||||
left_out: &ProcessCodeResult,
|
||||
right_out: &ProcessCodeResult,
|
||||
left_symbol_ref: SymbolRef,
|
||||
right_symbol_ref: SymbolRef,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<(ObjSymbolDiff, ObjSymbolDiff)> {
|
||||
let mut left_diff = Vec::<ObjInsDiff>::new();
|
||||
let mut right_diff = Vec::<ObjInsDiff>::new();
|
||||
diff_instructions(&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_obj, right_obj, 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().max(right_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
|
||||
};
|
||||
|
||||
Ok((
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: left_symbol_ref,
|
||||
target_symbol: Some(right_symbol_ref),
|
||||
instructions: left_diff,
|
||||
match_percent: Some(percent),
|
||||
},
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: right_symbol_ref,
|
||||
target_symbol: Some(left_symbol_ref),
|
||||
instructions: right_diff,
|
||||
match_percent: Some(percent),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn diff_instructions(
|
||||
left_diff: &mut Vec<ObjInsDiff>,
|
||||
right_diff: &mut Vec<ObjInsDiff>,
|
||||
left_code: &ProcessCodeResult,
|
||||
right_code: &ProcessCodeResult,
|
||||
) -> Result<()> {
|
||||
let ops =
|
||||
capture_diff_slices_deadline(Algorithm::Patience, &left_code.ops, &right_code.ops, None);
|
||||
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 resolve_branches(vec: &mut [ObjInsDiff]) {
|
||||
let mut branch_idx = 0usize;
|
||||
// Map addresses to indices
|
||||
let mut addr_map = BTreeMap::<u64, 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 let Some(ins_idx) = ins.branch_dest.and_then(|a| addr_map.get(&a)) {
|
||||
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: &ObjReloc, right: &ObjReloc) -> bool {
|
||||
left.target.address as i64 + left.addend == right.target.address as i64 + right.addend
|
||||
}
|
||||
|
||||
fn section_name_eq(
|
||||
left_obj: &ObjInfo,
|
||||
right_obj: &ObjInfo,
|
||||
left_orig_section_index: usize,
|
||||
right_orig_section_index: usize,
|
||||
) -> bool {
|
||||
let Some(left_section) =
|
||||
left_obj.sections.iter().find(|s| s.orig_index == left_orig_section_index)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let Some(right_section) =
|
||||
right_obj.sections.iter().find(|s| s.orig_index == right_orig_section_index)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
left_section.name == right_section.name
|
||||
}
|
||||
|
||||
fn reloc_eq(
|
||||
config: &DiffObjConfig,
|
||||
left_obj: &ObjInfo,
|
||||
right_obj: &ObjInfo,
|
||||
left_reloc: Option<&ObjReloc>,
|
||||
right_reloc: Option<&ObjReloc>,
|
||||
) -> bool {
|
||||
let (Some(left), Some(right)) = (left_reloc, right_reloc) else {
|
||||
return false;
|
||||
};
|
||||
if left.flags != right.flags {
|
||||
return false;
|
||||
}
|
||||
if config.relax_reloc_diffs {
|
||||
return true;
|
||||
}
|
||||
|
||||
let symbol_name_matches = left.target.name == right.target.name;
|
||||
match (&left.target.orig_section_index, &right.target.orig_section_index) {
|
||||
(Some(sl), Some(sr)) => {
|
||||
// Match if section and name or address match
|
||||
section_name_eq(left_obj, right_obj, *sl, *sr)
|
||||
&& (symbol_name_matches || address_eq(left, right))
|
||||
}
|
||||
(Some(_), None) => false,
|
||||
(None, Some(_)) => {
|
||||
// Match if possibly stripped weak symbol
|
||||
symbol_name_matches && right.target.flags.0.contains(ObjSymbolFlags::Weak)
|
||||
}
|
||||
(None, None) => symbol_name_matches,
|
||||
}
|
||||
}
|
||||
|
||||
fn arg_eq(
|
||||
config: &DiffObjConfig,
|
||||
left_obj: &ObjInfo,
|
||||
right_obj: &ObjInfo,
|
||||
left: &ObjInsArg,
|
||||
right: &ObjInsArg,
|
||||
left_diff: &ObjInsDiff,
|
||||
right_diff: &ObjInsDiff,
|
||||
) -> bool {
|
||||
match left {
|
||||
ObjInsArg::PlainText(l) => match right {
|
||||
ObjInsArg::PlainText(r) => l == r,
|
||||
_ => false,
|
||||
},
|
||||
ObjInsArg::Arg(l) => match right {
|
||||
ObjInsArg::Arg(r) => l == r,
|
||||
// If relocations are relaxed, match if left is a constant and right is a reloc
|
||||
// Useful for instances where the target object is created without relocations
|
||||
ObjInsArg::Reloc => config.relax_reloc_diffs,
|
||||
_ => false,
|
||||
},
|
||||
ObjInsArg::Reloc => {
|
||||
matches!(right, ObjInsArg::Reloc)
|
||||
&& reloc_eq(
|
||||
config,
|
||||
left_obj,
|
||||
right_obj,
|
||||
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::BranchDest(_) => match right {
|
||||
// Compare dest instruction idx after diffing
|
||||
ObjInsArg::BranchDest(_) => {
|
||||
left_diff.branch_to.as_ref().map(|b| b.ins_idx)
|
||||
== right_diff.branch_to.as_ref().map(|b| b.ins_idx)
|
||||
}
|
||||
// If relocations are relaxed, match if left is a constant and right is a reloc
|
||||
// Useful for instances where the target object is created without relocations
|
||||
ObjInsArg::Reloc => config.relax_reloc_diffs,
|
||||
_ => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[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_obj: &ObjInfo,
|
||||
right_obj: &ObjInfo,
|
||||
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) {
|
||||
// Count only non-PlainText args
|
||||
let left_args_count = left_ins.iter_args().count();
|
||||
let right_args_count = right_ins.iter_args().count();
|
||||
if left_args_count != right_args_count || 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.iter_args().zip(right_ins.iter_args()) {
|
||||
if arg_eq(config, left_obj, right_obj, 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::PlainText(arg) => arg.to_string(),
|
||||
ObjInsArg::Arg(arg) => arg.to_string(),
|
||||
ObjInsArg::Reloc => left_ins
|
||||
.reloc
|
||||
.as_ref()
|
||||
.map_or_else(|| "<unknown>".to_string(), |r| r.target.name.clone()),
|
||||
ObjInsArg::BranchDest(arg) => arg.to_string(),
|
||||
};
|
||||
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::PlainText(arg) => arg.to_string(),
|
||||
ObjInsArg::Arg(arg) => arg.to_string(),
|
||||
ObjInsArg::Reloc => right_ins
|
||||
.reloc
|
||||
.as_ref()
|
||||
.map_or_else(|| "<unknown>".to_string(), |r| r.target.name.clone()),
|
||||
ObjInsArg::BranchDest(arg) => arg.to_string(),
|
||||
};
|
||||
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)
|
||||
}
|
||||
222
objdiff-core/src/diff/data.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
use std::cmp::{max, min, Ordering};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use similar::{capture_diff_slices_deadline, get_diff_ratio, Algorithm};
|
||||
|
||||
use crate::{
|
||||
diff::{ObjDataDiff, ObjDataDiffKind, ObjSectionDiff, ObjSymbolDiff},
|
||||
obj::{ObjInfo, ObjSection, SymbolRef},
|
||||
};
|
||||
|
||||
pub fn diff_bss_symbol(
|
||||
left_obj: &ObjInfo,
|
||||
right_obj: &ObjInfo,
|
||||
left_symbol_ref: SymbolRef,
|
||||
right_symbol_ref: SymbolRef,
|
||||
) -> Result<(ObjSymbolDiff, ObjSymbolDiff)> {
|
||||
let (_, left_symbol) = left_obj.section_symbol(left_symbol_ref);
|
||||
let (_, right_symbol) = right_obj.section_symbol(right_symbol_ref);
|
||||
let percent = if left_symbol.size == right_symbol.size { 100.0 } else { 50.0 };
|
||||
Ok((
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: left_symbol_ref,
|
||||
target_symbol: Some(right_symbol_ref),
|
||||
instructions: vec![],
|
||||
match_percent: Some(percent),
|
||||
},
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: right_symbol_ref,
|
||||
target_symbol: Some(left_symbol_ref),
|
||||
instructions: vec![],
|
||||
match_percent: Some(percent),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn no_diff_symbol(_obj: &ObjInfo, symbol_ref: SymbolRef) -> ObjSymbolDiff {
|
||||
ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: vec![], match_percent: None }
|
||||
}
|
||||
|
||||
/// Compare the data sections of two object files.
|
||||
pub fn diff_data_section(
|
||||
left: &ObjSection,
|
||||
right: &ObjSection,
|
||||
left_section_diff: &ObjSectionDiff,
|
||||
right_section_diff: &ObjSectionDiff,
|
||||
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
|
||||
let left_max =
|
||||
left.symbols.iter().map(|s| s.section_address + s.size).max().unwrap_or(0).min(left.size);
|
||||
let right_max =
|
||||
right.symbols.iter().map(|s| s.section_address + s.size).max().unwrap_or(0).min(right.size);
|
||||
let left_data = &left.data[..left_max as usize];
|
||||
let right_data = &right.data[..right_max as usize];
|
||||
let ops = capture_diff_slices_deadline(Algorithm::Patience, left_data, right_data, None);
|
||||
let match_percent = get_diff_ratio(&ops, left_data.len(), right_data.len()) * 100.0;
|
||||
|
||||
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 => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (mut left_section_diff, mut right_section_diff) =
|
||||
diff_generic_section(left, right, left_section_diff, right_section_diff)?;
|
||||
left_section_diff.data_diff = left_diff;
|
||||
right_section_diff.data_diff = right_diff;
|
||||
// Use the highest match percent between two options:
|
||||
// - Left symbols matching right symbols by name
|
||||
// - Diff of the data itself
|
||||
if left_section_diff.match_percent.unwrap_or(-1.0) < match_percent {
|
||||
left_section_diff.match_percent = Some(match_percent);
|
||||
right_section_diff.match_percent = Some(match_percent);
|
||||
}
|
||||
Ok((left_section_diff, right_section_diff))
|
||||
}
|
||||
|
||||
pub fn diff_data_symbol(
|
||||
left_obj: &ObjInfo,
|
||||
right_obj: &ObjInfo,
|
||||
left_symbol_ref: SymbolRef,
|
||||
right_symbol_ref: SymbolRef,
|
||||
) -> Result<(ObjSymbolDiff, ObjSymbolDiff)> {
|
||||
let (left_section, left_symbol) = left_obj.section_symbol(left_symbol_ref);
|
||||
let (right_section, right_symbol) = right_obj.section_symbol(right_symbol_ref);
|
||||
|
||||
let left_section = left_section.ok_or_else(|| anyhow!("Data symbol section not found"))?;
|
||||
let right_section = right_section.ok_or_else(|| anyhow!("Data symbol section not found"))?;
|
||||
|
||||
let left_data = &left_section.data[left_symbol.section_address as usize
|
||||
..(left_symbol.section_address + left_symbol.size) as usize];
|
||||
let right_data = &right_section.data[right_symbol.section_address as usize
|
||||
..(right_symbol.section_address + right_symbol.size) as usize];
|
||||
|
||||
let ops = capture_diff_slices_deadline(Algorithm::Patience, left_data, right_data, None);
|
||||
let match_percent = get_diff_ratio(&ops, left_data.len(), right_data.len()) * 100.0;
|
||||
|
||||
Ok((
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: left_symbol_ref,
|
||||
target_symbol: Some(right_symbol_ref),
|
||||
instructions: vec![],
|
||||
match_percent: Some(match_percent),
|
||||
},
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: right_symbol_ref,
|
||||
target_symbol: Some(left_symbol_ref),
|
||||
instructions: vec![],
|
||||
match_percent: Some(match_percent),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// Compares a section of two object files.
|
||||
/// This essentially adds up the match percentage of each symbol in the section.
|
||||
pub fn diff_generic_section(
|
||||
left: &ObjSection,
|
||||
_right: &ObjSection,
|
||||
left_diff: &ObjSectionDiff,
|
||||
_right_diff: &ObjSectionDiff,
|
||||
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
|
||||
let match_percent = if left_diff.symbols.iter().all(|d| d.match_percent == Some(100.0)) {
|
||||
100.0 // Avoid fp precision issues
|
||||
} else {
|
||||
left.symbols
|
||||
.iter()
|
||||
.zip(left_diff.symbols.iter())
|
||||
.map(|(s, d)| d.match_percent.unwrap_or(0.0) * s.size as f32)
|
||||
.sum::<f32>()
|
||||
/ left.size as f32
|
||||
};
|
||||
Ok((
|
||||
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
|
||||
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
|
||||
))
|
||||
}
|
||||
|
||||
/// Compare the addresses and sizes of each symbol in the BSS sections.
|
||||
pub fn diff_bss_section(
|
||||
left: &ObjSection,
|
||||
right: &ObjSection,
|
||||
left_diff: &ObjSectionDiff,
|
||||
right_diff: &ObjSectionDiff,
|
||||
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
|
||||
let left_sizes = left.symbols.iter().map(|s| (s.section_address, s.size)).collect::<Vec<_>>();
|
||||
let right_sizes = right.symbols.iter().map(|s| (s.section_address, s.size)).collect::<Vec<_>>();
|
||||
let ops = capture_diff_slices_deadline(Algorithm::Patience, &left_sizes, &right_sizes, None);
|
||||
let mut match_percent = get_diff_ratio(&ops, left_sizes.len(), right_sizes.len()) * 100.0;
|
||||
|
||||
// Use the highest match percent between two options:
|
||||
// - Left symbols matching right symbols by name
|
||||
// - Diff of the addresses and sizes of each symbol
|
||||
let (generic_diff, _) = diff_generic_section(left, right, left_diff, right_diff)?;
|
||||
if generic_diff.match_percent.unwrap_or(-1.0) > match_percent {
|
||||
match_percent = generic_diff.match_percent.unwrap();
|
||||
}
|
||||
|
||||
Ok((
|
||||
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
|
||||
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
|
||||
))
|
||||
}
|
||||
137
objdiff-core/src/diff/display.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use crate::{
|
||||
diff::{ObjInsArgDiff, ObjInsDiff},
|
||||
obj::{ObjInsArg, ObjInsArgValue, ObjReloc, ObjSymbol},
|
||||
};
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum DiffText<'a> {
|
||||
/// Basic text
|
||||
Basic(&'a str),
|
||||
/// Colored text
|
||||
BasicColor(&'a str, usize),
|
||||
/// Line number
|
||||
Line(u32),
|
||||
/// Instruction address
|
||||
Address(u64),
|
||||
/// Instruction mnemonic
|
||||
Opcode(&'a str, u16),
|
||||
/// Instruction argument
|
||||
Argument(&'a ObjInsArgValue, Option<&'a ObjInsArgDiff>),
|
||||
/// Branch destination
|
||||
BranchDest(u64, Option<&'a ObjInsArgDiff>),
|
||||
/// Symbol name
|
||||
Symbol(&'a ObjSymbol, Option<&'a ObjInsArgDiff>),
|
||||
/// Number of spaces
|
||||
Spacing(usize),
|
||||
/// End of line
|
||||
Eol,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub enum HighlightKind {
|
||||
#[default]
|
||||
None,
|
||||
Opcode(u16),
|
||||
Arg(ObjInsArgValue),
|
||||
Symbol(String),
|
||||
Address(u64),
|
||||
}
|
||||
|
||||
pub fn display_diff<E>(
|
||||
ins_diff: &ObjInsDiff,
|
||||
base_addr: u64,
|
||||
mut cb: impl FnMut(DiffText) -> Result<(), E>,
|
||||
) -> Result<(), E> {
|
||||
let Some(ins) = &ins_diff.ins else {
|
||||
cb(DiffText::Eol)?;
|
||||
return Ok(());
|
||||
};
|
||||
if let Some(line) = ins.line {
|
||||
cb(DiffText::Line(line))?;
|
||||
}
|
||||
cb(DiffText::Address(ins.address - base_addr))?;
|
||||
if let Some(branch) = &ins_diff.branch_from {
|
||||
cb(DiffText::BasicColor(" ~> ", branch.branch_idx))?;
|
||||
} else {
|
||||
cb(DiffText::Spacing(4))?;
|
||||
}
|
||||
cb(DiffText::Opcode(&ins.mnemonic, ins.op))?;
|
||||
let mut arg_diff_idx = 0; // non-PlainText index
|
||||
for (i, arg) in ins.args.iter().enumerate() {
|
||||
if i == 0 {
|
||||
cb(DiffText::Spacing(1))?;
|
||||
}
|
||||
let diff = ins_diff.arg_diff.get(arg_diff_idx).and_then(|o| o.as_ref());
|
||||
match arg {
|
||||
ObjInsArg::PlainText(s) => {
|
||||
cb(DiffText::Basic(s))?;
|
||||
}
|
||||
ObjInsArg::Arg(v) => {
|
||||
cb(DiffText::Argument(v, diff))?;
|
||||
arg_diff_idx += 1;
|
||||
}
|
||||
ObjInsArg::Reloc => {
|
||||
display_reloc_name(ins.reloc.as_ref().unwrap(), &mut cb, diff)?;
|
||||
arg_diff_idx += 1;
|
||||
}
|
||||
ObjInsArg::BranchDest(dest) => {
|
||||
if let Some(dest) = dest.checked_sub(base_addr) {
|
||||
cb(DiffText::BranchDest(dest, diff))?;
|
||||
} else {
|
||||
cb(DiffText::Basic("<unknown>"))?;
|
||||
}
|
||||
arg_diff_idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(branch) = &ins_diff.branch_to {
|
||||
cb(DiffText::BasicColor(" ~>", branch.branch_idx))?;
|
||||
}
|
||||
cb(DiffText::Eol)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_reloc_name<E>(
|
||||
reloc: &ObjReloc,
|
||||
mut cb: impl FnMut(DiffText) -> Result<(), E>,
|
||||
diff: Option<&ObjInsArgDiff>,
|
||||
) -> Result<(), E> {
|
||||
cb(DiffText::Symbol(&reloc.target, diff))?;
|
||||
match reloc.addend.cmp(&0i64) {
|
||||
Ordering::Greater => cb(DiffText::Basic(&format!("+{:#x}", reloc.addend))),
|
||||
Ordering::Less => cb(DiffText::Basic(&format!("-{:#x}", -reloc.addend))),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<DiffText<'_>> for HighlightKind {
|
||||
fn eq(&self, other: &DiffText) -> bool {
|
||||
match (self, other) {
|
||||
(HighlightKind::Opcode(a), DiffText::Opcode(_, b)) => a == b,
|
||||
(HighlightKind::Arg(a), DiffText::Argument(b, _)) => a.loose_eq(b),
|
||||
(HighlightKind::Symbol(a), DiffText::Symbol(b, _)) => a == &b.name,
|
||||
(HighlightKind::Address(a), DiffText::Address(b) | DiffText::BranchDest(b, _)) => {
|
||||
a == b
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<HighlightKind> for DiffText<'_> {
|
||||
fn eq(&self, other: &HighlightKind) -> bool { other.eq(self) }
|
||||
}
|
||||
|
||||
impl From<DiffText<'_>> for HighlightKind {
|
||||
fn from(value: DiffText<'_>) -> Self {
|
||||
match value {
|
||||
DiffText::Opcode(_, op) => HighlightKind::Opcode(op),
|
||||
DiffText::Argument(arg, _) => HighlightKind::Arg(arg.clone()),
|
||||
DiffText::Symbol(sym, _) => HighlightKind::Symbol(sym.name.to_string()),
|
||||
DiffText::Address(addr) | DiffText::BranchDest(addr, _) => HighlightKind::Address(addr),
|
||||
_ => HighlightKind::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
927
objdiff-core/src/diff/mod.rs
Normal file
@@ -0,0 +1,927 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{
|
||||
config::SymbolMappings,
|
||||
diff::{
|
||||
code::{diff_code, no_diff_code, process_code_symbol},
|
||||
data::{
|
||||
diff_bss_section, diff_bss_symbol, diff_data_section, diff_data_symbol,
|
||||
diff_generic_section, no_diff_symbol,
|
||||
},
|
||||
},
|
||||
obj::{ObjInfo, ObjIns, ObjSection, ObjSectionKind, ObjSymbol, SymbolRef, SECTION_COMMON},
|
||||
};
|
||||
|
||||
pub mod code;
|
||||
pub mod data;
|
||||
pub mod display;
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Copy,
|
||||
Clone,
|
||||
Default,
|
||||
Eq,
|
||||
PartialEq,
|
||||
serde::Deserialize,
|
||||
serde::Serialize,
|
||||
strum::VariantArray,
|
||||
strum::EnumMessage,
|
||||
)]
|
||||
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
|
||||
pub enum X86Formatter {
|
||||
#[default]
|
||||
#[strum(message = "Intel (default)")]
|
||||
Intel,
|
||||
#[strum(message = "AT&T")]
|
||||
Gas,
|
||||
#[strum(message = "NASM")]
|
||||
Nasm,
|
||||
#[strum(message = "MASM")]
|
||||
Masm,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Copy,
|
||||
Clone,
|
||||
Default,
|
||||
Eq,
|
||||
PartialEq,
|
||||
serde::Deserialize,
|
||||
serde::Serialize,
|
||||
strum::VariantArray,
|
||||
strum::EnumMessage,
|
||||
)]
|
||||
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
|
||||
pub enum MipsAbi {
|
||||
#[default]
|
||||
#[strum(message = "Auto (default)")]
|
||||
Auto,
|
||||
#[strum(message = "O32")]
|
||||
O32,
|
||||
#[strum(message = "N32")]
|
||||
N32,
|
||||
#[strum(message = "N64")]
|
||||
N64,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Copy,
|
||||
Clone,
|
||||
Default,
|
||||
Eq,
|
||||
PartialEq,
|
||||
serde::Deserialize,
|
||||
serde::Serialize,
|
||||
strum::VariantArray,
|
||||
strum::EnumMessage,
|
||||
)]
|
||||
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
|
||||
pub enum MipsInstrCategory {
|
||||
#[default]
|
||||
#[strum(message = "Auto (default)")]
|
||||
Auto,
|
||||
#[strum(message = "CPU")]
|
||||
Cpu,
|
||||
#[strum(message = "RSP (N64)")]
|
||||
Rsp,
|
||||
#[strum(message = "R3000 GTE (PS1)")]
|
||||
R3000Gte,
|
||||
#[strum(message = "R4000 ALLEGREX (PSP)")]
|
||||
R4000Allegrex,
|
||||
#[strum(message = "R5900 EE (PS2)")]
|
||||
R5900,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Copy,
|
||||
Clone,
|
||||
Default,
|
||||
Eq,
|
||||
PartialEq,
|
||||
serde::Deserialize,
|
||||
serde::Serialize,
|
||||
strum::VariantArray,
|
||||
strum::EnumMessage,
|
||||
)]
|
||||
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
|
||||
pub enum ArmArchVersion {
|
||||
#[default]
|
||||
#[strum(message = "Auto (default)")]
|
||||
Auto,
|
||||
#[strum(message = "ARMv4T (GBA)")]
|
||||
V4T,
|
||||
#[strum(message = "ARMv5TE (DS)")]
|
||||
V5TE,
|
||||
#[strum(message = "ARMv6K (3DS)")]
|
||||
V6K,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Copy,
|
||||
Clone,
|
||||
Default,
|
||||
Eq,
|
||||
PartialEq,
|
||||
serde::Deserialize,
|
||||
serde::Serialize,
|
||||
strum::VariantArray,
|
||||
strum::EnumMessage,
|
||||
)]
|
||||
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
|
||||
pub enum ArmR9Usage {
|
||||
#[default]
|
||||
#[strum(
|
||||
message = "R9 or V6 (default)",
|
||||
detailed_message = "Use R9 as a general-purpose register."
|
||||
)]
|
||||
GeneralPurpose,
|
||||
#[strum(
|
||||
message = "SB (static base)",
|
||||
detailed_message = "Used for position-independent data (PID)."
|
||||
)]
|
||||
Sb,
|
||||
#[strum(message = "TR (TLS register)", detailed_message = "Used for thread-local storage.")]
|
||||
Tr,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
const fn default_true() -> bool { true }
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
|
||||
#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))]
|
||||
#[serde(default)]
|
||||
pub struct DiffObjConfig {
|
||||
pub relax_reloc_diffs: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub space_between_args: bool,
|
||||
pub combine_data_sections: bool,
|
||||
#[serde(default)]
|
||||
pub symbol_mappings: MappingConfig,
|
||||
// x86
|
||||
pub x86_formatter: X86Formatter,
|
||||
// MIPS
|
||||
pub mips_abi: MipsAbi,
|
||||
pub mips_instr_category: MipsInstrCategory,
|
||||
// ARM
|
||||
pub arm_arch_version: ArmArchVersion,
|
||||
pub arm_unified_syntax: bool,
|
||||
pub arm_av_registers: bool,
|
||||
pub arm_r9_usage: ArmR9Usage,
|
||||
pub arm_sl_usage: bool,
|
||||
pub arm_fp_usage: bool,
|
||||
pub arm_ip_usage: bool,
|
||||
}
|
||||
|
||||
impl Default for DiffObjConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
relax_reloc_diffs: false,
|
||||
space_between_args: true,
|
||||
combine_data_sections: false,
|
||||
symbol_mappings: Default::default(),
|
||||
x86_formatter: Default::default(),
|
||||
mips_abi: Default::default(),
|
||||
mips_instr_category: Default::default(),
|
||||
arm_arch_version: Default::default(),
|
||||
arm_unified_syntax: true,
|
||||
arm_av_registers: false,
|
||||
arm_r9_usage: Default::default(),
|
||||
arm_sl_usage: false,
|
||||
arm_fp_usage: false,
|
||||
arm_ip_usage: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DiffObjConfig {
|
||||
pub fn separator(&self) -> &'static str {
|
||||
if self.space_between_args {
|
||||
", "
|
||||
} else {
|
||||
","
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjSectionDiff {
|
||||
pub symbols: Vec<ObjSymbolDiff>,
|
||||
pub data_diff: Vec<ObjDataDiff>,
|
||||
pub match_percent: Option<f32>,
|
||||
}
|
||||
|
||||
impl ObjSectionDiff {
|
||||
fn merge(&mut self, other: ObjSectionDiff) {
|
||||
// symbols ignored
|
||||
self.data_diff = other.data_diff;
|
||||
self.match_percent = other.match_percent;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ObjSymbolDiff {
|
||||
/// The symbol ref this object
|
||||
pub symbol_ref: SymbolRef,
|
||||
/// The symbol ref in the _other_ object that this symbol was diffed against
|
||||
pub target_symbol: Option<SymbolRef>,
|
||||
pub instructions: Vec<ObjInsDiff>,
|
||||
pub match_percent: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ObjInsDiff {
|
||||
pub ins: Option<ObjIns>,
|
||||
/// Diff kind
|
||||
pub kind: ObjInsDiffKind,
|
||||
/// Branches from instruction
|
||||
pub branch_from: Option<ObjInsBranchFrom>,
|
||||
/// Branches to instruction
|
||||
pub branch_to: Option<ObjInsBranchTo>,
|
||||
/// Arg diffs (only contains non-PlainText args)
|
||||
pub arg_diff: Vec<Option<ObjInsArgDiff>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
|
||||
pub enum ObjInsDiffKind {
|
||||
#[default]
|
||||
None,
|
||||
OpMismatch,
|
||||
ArgMismatch,
|
||||
Replace,
|
||||
Delete,
|
||||
Insert,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ObjDataDiff {
|
||||
pub data: Vec<u8>,
|
||||
pub kind: ObjDataDiffKind,
|
||||
pub len: usize,
|
||||
pub symbol: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
|
||||
pub enum ObjDataDiffKind {
|
||||
#[default]
|
||||
None,
|
||||
Replace,
|
||||
Delete,
|
||||
Insert,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ObjInsArgDiff {
|
||||
/// Incrementing index for coloring
|
||||
pub idx: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjInsBranchFrom {
|
||||
/// Source instruction indices
|
||||
pub ins_idx: Vec<usize>,
|
||||
/// Incrementing index for coloring
|
||||
pub branch_idx: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjInsBranchTo {
|
||||
/// Target instruction index
|
||||
pub ins_idx: usize,
|
||||
/// Incrementing index for coloring
|
||||
pub branch_idx: usize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ObjDiff {
|
||||
/// A list of all section diffs in the object.
|
||||
pub sections: Vec<ObjSectionDiff>,
|
||||
/// Common BSS symbols don't live in a section, so they're stored separately.
|
||||
pub common: Vec<ObjSymbolDiff>,
|
||||
/// If `selecting_left` or `selecting_right` is set, this is the list of symbols
|
||||
/// that are being mapped to the other object.
|
||||
pub mapping_symbols: Vec<ObjSymbolDiff>,
|
||||
}
|
||||
|
||||
impl ObjDiff {
|
||||
pub fn new_from_obj(obj: &ObjInfo) -> Self {
|
||||
let mut result = Self {
|
||||
sections: Vec::with_capacity(obj.sections.len()),
|
||||
common: Vec::with_capacity(obj.common.len()),
|
||||
mapping_symbols: vec![],
|
||||
};
|
||||
for (section_idx, section) in obj.sections.iter().enumerate() {
|
||||
let mut symbols = Vec::with_capacity(section.symbols.len());
|
||||
for (symbol_idx, _) in section.symbols.iter().enumerate() {
|
||||
symbols.push(ObjSymbolDiff {
|
||||
symbol_ref: SymbolRef { section_idx, symbol_idx },
|
||||
target_symbol: None,
|
||||
instructions: vec![],
|
||||
match_percent: None,
|
||||
});
|
||||
}
|
||||
result.sections.push(ObjSectionDiff {
|
||||
symbols,
|
||||
data_diff: vec![ObjDataDiff {
|
||||
data: section.data.clone(),
|
||||
kind: ObjDataDiffKind::None,
|
||||
len: section.data.len(),
|
||||
symbol: section.name.clone(),
|
||||
}],
|
||||
match_percent: None,
|
||||
});
|
||||
}
|
||||
for (symbol_idx, _) in obj.common.iter().enumerate() {
|
||||
result.common.push(ObjSymbolDiff {
|
||||
symbol_ref: SymbolRef { section_idx: SECTION_COMMON, symbol_idx },
|
||||
target_symbol: None,
|
||||
instructions: vec![],
|
||||
match_percent: None,
|
||||
});
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn section_diff(&self, section_idx: usize) -> &ObjSectionDiff {
|
||||
&self.sections[section_idx]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn section_diff_mut(&mut self, section_idx: usize) -> &mut ObjSectionDiff {
|
||||
&mut self.sections[section_idx]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn symbol_diff(&self, symbol_ref: SymbolRef) -> &ObjSymbolDiff {
|
||||
if symbol_ref.section_idx == SECTION_COMMON {
|
||||
&self.common[symbol_ref.symbol_idx]
|
||||
} else {
|
||||
&self.section_diff(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx]
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn symbol_diff_mut(&mut self, symbol_ref: SymbolRef) -> &mut ObjSymbolDiff {
|
||||
if symbol_ref.section_idx == SECTION_COMMON {
|
||||
&mut self.common[symbol_ref.symbol_idx]
|
||||
} else {
|
||||
&mut self.section_diff_mut(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DiffObjsResult {
|
||||
pub left: Option<ObjDiff>,
|
||||
pub right: Option<ObjDiff>,
|
||||
pub prev: Option<ObjDiff>,
|
||||
}
|
||||
|
||||
pub fn diff_objs(
|
||||
config: &DiffObjConfig,
|
||||
left: Option<&ObjInfo>,
|
||||
right: Option<&ObjInfo>,
|
||||
prev: Option<&ObjInfo>,
|
||||
) -> Result<DiffObjsResult> {
|
||||
let symbol_matches = matching_symbols(left, right, prev, &config.symbol_mappings)?;
|
||||
let section_matches = matching_sections(left, right)?;
|
||||
let mut left = left.map(|p| (p, ObjDiff::new_from_obj(p)));
|
||||
let mut right = right.map(|p| (p, ObjDiff::new_from_obj(p)));
|
||||
let mut prev = prev.map(|p| (p, ObjDiff::new_from_obj(p)));
|
||||
|
||||
for symbol_match in symbol_matches {
|
||||
match symbol_match {
|
||||
SymbolMatch {
|
||||
left: Some(left_symbol_ref),
|
||||
right: Some(right_symbol_ref),
|
||||
prev: prev_symbol_ref,
|
||||
section_kind,
|
||||
} => {
|
||||
let (left_obj, left_out) = left.as_mut().unwrap();
|
||||
let (right_obj, right_out) = right.as_mut().unwrap();
|
||||
match section_kind {
|
||||
ObjSectionKind::Code => {
|
||||
let left_code = process_code_symbol(left_obj, left_symbol_ref, config)?;
|
||||
let right_code = process_code_symbol(right_obj, right_symbol_ref, config)?;
|
||||
let (left_diff, right_diff) = diff_code(
|
||||
left_obj,
|
||||
right_obj,
|
||||
&left_code,
|
||||
&right_code,
|
||||
left_symbol_ref,
|
||||
right_symbol_ref,
|
||||
config,
|
||||
)?;
|
||||
*left_out.symbol_diff_mut(left_symbol_ref) = left_diff;
|
||||
*right_out.symbol_diff_mut(right_symbol_ref) = right_diff;
|
||||
|
||||
if let Some(prev_symbol_ref) = prev_symbol_ref {
|
||||
let (prev_obj, prev_out) = prev.as_mut().unwrap();
|
||||
let prev_code = process_code_symbol(prev_obj, prev_symbol_ref, config)?;
|
||||
let (_, prev_diff) = diff_code(
|
||||
left_obj,
|
||||
right_obj,
|
||||
&right_code,
|
||||
&prev_code,
|
||||
right_symbol_ref,
|
||||
prev_symbol_ref,
|
||||
config,
|
||||
)?;
|
||||
*prev_out.symbol_diff_mut(prev_symbol_ref) = prev_diff;
|
||||
}
|
||||
}
|
||||
ObjSectionKind::Data => {
|
||||
let (left_diff, right_diff) = diff_data_symbol(
|
||||
left_obj,
|
||||
right_obj,
|
||||
left_symbol_ref,
|
||||
right_symbol_ref,
|
||||
)?;
|
||||
*left_out.symbol_diff_mut(left_symbol_ref) = left_diff;
|
||||
*right_out.symbol_diff_mut(right_symbol_ref) = right_diff;
|
||||
}
|
||||
ObjSectionKind::Bss => {
|
||||
let (left_diff, right_diff) = diff_bss_symbol(
|
||||
left_obj,
|
||||
right_obj,
|
||||
left_symbol_ref,
|
||||
right_symbol_ref,
|
||||
)?;
|
||||
*left_out.symbol_diff_mut(left_symbol_ref) = left_diff;
|
||||
*right_out.symbol_diff_mut(right_symbol_ref) = right_diff;
|
||||
}
|
||||
}
|
||||
}
|
||||
SymbolMatch { left: Some(left_symbol_ref), right: None, prev: _, section_kind } => {
|
||||
let (left_obj, left_out) = left.as_mut().unwrap();
|
||||
match section_kind {
|
||||
ObjSectionKind::Code => {
|
||||
let code = process_code_symbol(left_obj, left_symbol_ref, config)?;
|
||||
*left_out.symbol_diff_mut(left_symbol_ref) =
|
||||
no_diff_code(&code, left_symbol_ref)?;
|
||||
}
|
||||
ObjSectionKind::Data | ObjSectionKind::Bss => {
|
||||
*left_out.symbol_diff_mut(left_symbol_ref) =
|
||||
no_diff_symbol(left_obj, left_symbol_ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
SymbolMatch { left: None, right: Some(right_symbol_ref), prev: _, section_kind } => {
|
||||
let (right_obj, right_out) = right.as_mut().unwrap();
|
||||
match section_kind {
|
||||
ObjSectionKind::Code => {
|
||||
let code = process_code_symbol(right_obj, right_symbol_ref, config)?;
|
||||
*right_out.symbol_diff_mut(right_symbol_ref) =
|
||||
no_diff_code(&code, right_symbol_ref)?;
|
||||
}
|
||||
ObjSectionKind::Data | ObjSectionKind::Bss => {
|
||||
*right_out.symbol_diff_mut(right_symbol_ref) =
|
||||
no_diff_symbol(right_obj, right_symbol_ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
SymbolMatch { left: None, right: None, .. } => {
|
||||
// Should not happen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for section_match in section_matches {
|
||||
if let SectionMatch {
|
||||
left: Some(left_section_idx),
|
||||
right: Some(right_section_idx),
|
||||
section_kind,
|
||||
} = section_match
|
||||
{
|
||||
let (left_obj, left_out) = left.as_mut().unwrap();
|
||||
let (right_obj, right_out) = right.as_mut().unwrap();
|
||||
let left_section = &left_obj.sections[left_section_idx];
|
||||
let right_section = &right_obj.sections[right_section_idx];
|
||||
match section_kind {
|
||||
ObjSectionKind::Code => {
|
||||
let left_section_diff = left_out.section_diff(left_section_idx);
|
||||
let right_section_diff = right_out.section_diff(right_section_idx);
|
||||
let (left_diff, right_diff) = diff_generic_section(
|
||||
left_section,
|
||||
right_section,
|
||||
left_section_diff,
|
||||
right_section_diff,
|
||||
)?;
|
||||
left_out.section_diff_mut(left_section_idx).merge(left_diff);
|
||||
right_out.section_diff_mut(right_section_idx).merge(right_diff);
|
||||
}
|
||||
ObjSectionKind::Data => {
|
||||
let left_section_diff = left_out.section_diff(left_section_idx);
|
||||
let right_section_diff = right_out.section_diff(right_section_idx);
|
||||
let (left_diff, right_diff) = diff_data_section(
|
||||
left_section,
|
||||
right_section,
|
||||
left_section_diff,
|
||||
right_section_diff,
|
||||
)?;
|
||||
left_out.section_diff_mut(left_section_idx).merge(left_diff);
|
||||
right_out.section_diff_mut(right_section_idx).merge(right_diff);
|
||||
}
|
||||
ObjSectionKind::Bss => {
|
||||
let left_section_diff = left_out.section_diff(left_section_idx);
|
||||
let right_section_diff = right_out.section_diff(right_section_idx);
|
||||
let (left_diff, right_diff) = diff_bss_section(
|
||||
left_section,
|
||||
right_section,
|
||||
left_section_diff,
|
||||
right_section_diff,
|
||||
)?;
|
||||
left_out.section_diff_mut(left_section_idx).merge(left_diff);
|
||||
right_out.section_diff_mut(right_section_idx).merge(right_diff);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some((right_obj, right_out)), Some((left_obj, left_out))) =
|
||||
(right.as_mut(), left.as_mut())
|
||||
{
|
||||
if let Some(right_name) = &config.symbol_mappings.selecting_left {
|
||||
generate_mapping_symbols(right_obj, right_name, left_obj, left_out, config)?;
|
||||
}
|
||||
if let Some(left_name) = &config.symbol_mappings.selecting_right {
|
||||
generate_mapping_symbols(left_obj, left_name, right_obj, right_out, config)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(DiffObjsResult {
|
||||
left: left.map(|(_, o)| o),
|
||||
right: right.map(|(_, o)| o),
|
||||
prev: prev.map(|(_, o)| o),
|
||||
})
|
||||
}
|
||||
|
||||
/// When we're selecting a symbol to use as a comparison, we'll create comparisons for all
|
||||
/// symbols in the other object that match the selected symbol's section and kind. This allows
|
||||
/// us to display match percentages for all symbols in the other object that could be selected.
|
||||
fn generate_mapping_symbols(
|
||||
base_obj: &ObjInfo,
|
||||
base_name: &str,
|
||||
target_obj: &ObjInfo,
|
||||
target_out: &mut ObjDiff,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<()> {
|
||||
let Some(base_symbol_ref) = symbol_ref_by_name(base_obj, base_name) else {
|
||||
return Ok(());
|
||||
};
|
||||
let (base_section, _base_symbol) = base_obj.section_symbol(base_symbol_ref);
|
||||
let Some(base_section) = base_section else {
|
||||
return Ok(());
|
||||
};
|
||||
let base_code = match base_section.kind {
|
||||
ObjSectionKind::Code => Some(process_code_symbol(base_obj, base_symbol_ref, config)?),
|
||||
_ => None,
|
||||
};
|
||||
for (target_section_index, target_section) in
|
||||
target_obj.sections.iter().enumerate().filter(|(_, s)| s.kind == base_section.kind)
|
||||
{
|
||||
for (target_symbol_index, _target_symbol) in target_section.symbols.iter().enumerate() {
|
||||
let target_symbol_ref =
|
||||
SymbolRef { section_idx: target_section_index, symbol_idx: target_symbol_index };
|
||||
match base_section.kind {
|
||||
ObjSectionKind::Code => {
|
||||
let target_code = process_code_symbol(target_obj, target_symbol_ref, config)?;
|
||||
let (left_diff, _right_diff) = diff_code(
|
||||
target_obj,
|
||||
base_obj,
|
||||
&target_code,
|
||||
base_code.as_ref().unwrap(),
|
||||
target_symbol_ref,
|
||||
base_symbol_ref,
|
||||
config,
|
||||
)?;
|
||||
target_out.mapping_symbols.push(left_diff);
|
||||
}
|
||||
ObjSectionKind::Data => {
|
||||
let (left_diff, _right_diff) =
|
||||
diff_data_symbol(target_obj, base_obj, target_symbol_ref, base_symbol_ref)?;
|
||||
target_out.mapping_symbols.push(left_diff);
|
||||
}
|
||||
ObjSectionKind::Bss => {
|
||||
let (left_diff, _right_diff) =
|
||||
diff_bss_symbol(target_obj, base_obj, target_symbol_ref, base_symbol_ref)?;
|
||||
target_out.mapping_symbols.push(left_diff);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
struct SymbolMatch {
|
||||
left: Option<SymbolRef>,
|
||||
right: Option<SymbolRef>,
|
||||
prev: Option<SymbolRef>,
|
||||
section_kind: ObjSectionKind,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
struct SectionMatch {
|
||||
left: Option<usize>,
|
||||
right: Option<usize>,
|
||||
section_kind: ObjSectionKind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Deserialize, serde::Serialize)]
|
||||
pub struct MappingConfig {
|
||||
/// Manual symbol mappings
|
||||
pub mappings: SymbolMappings,
|
||||
/// The right object symbol name that we're selecting a left symbol for
|
||||
pub selecting_left: Option<String>,
|
||||
/// The left object symbol name that we're selecting a right symbol for
|
||||
pub selecting_right: Option<String>,
|
||||
}
|
||||
|
||||
fn symbol_ref_by_name(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
|
||||
for (section_idx, section) in obj.sections.iter().enumerate() {
|
||||
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
|
||||
if symbol.name == name {
|
||||
return Some(SymbolRef { section_idx, symbol_idx });
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn apply_symbol_mappings(
|
||||
left: &ObjInfo,
|
||||
right: &ObjInfo,
|
||||
mapping_config: &MappingConfig,
|
||||
left_used: &mut HashSet<SymbolRef>,
|
||||
right_used: &mut HashSet<SymbolRef>,
|
||||
matches: &mut Vec<SymbolMatch>,
|
||||
) -> Result<()> {
|
||||
// If we're selecting a symbol to use as a comparison, mark it as used
|
||||
// This ensures that we don't match it to another symbol at any point
|
||||
if let Some(left_name) = &mapping_config.selecting_left {
|
||||
if let Some(left_symbol) = symbol_ref_by_name(left, left_name) {
|
||||
left_used.insert(left_symbol);
|
||||
}
|
||||
}
|
||||
if let Some(right_name) = &mapping_config.selecting_right {
|
||||
if let Some(right_symbol) = symbol_ref_by_name(right, right_name) {
|
||||
right_used.insert(right_symbol);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply manual symbol mappings
|
||||
for (left_name, right_name) in &mapping_config.mappings {
|
||||
let Some(left_symbol) = symbol_ref_by_name(left, left_name) else {
|
||||
continue;
|
||||
};
|
||||
if left_used.contains(&left_symbol) {
|
||||
continue;
|
||||
}
|
||||
let Some(right_symbol) = symbol_ref_by_name(right, right_name) else {
|
||||
continue;
|
||||
};
|
||||
if right_used.contains(&right_symbol) {
|
||||
continue;
|
||||
}
|
||||
let left_section = &left.sections[left_symbol.section_idx];
|
||||
let right_section = &right.sections[right_symbol.section_idx];
|
||||
if left_section.kind != right_section.kind {
|
||||
log::warn!(
|
||||
"Symbol section kind mismatch: {} ({:?}) vs {} ({:?})",
|
||||
left_name,
|
||||
left_section.kind,
|
||||
right_name,
|
||||
right_section.kind
|
||||
);
|
||||
continue;
|
||||
}
|
||||
matches.push(SymbolMatch {
|
||||
left: Some(left_symbol),
|
||||
right: Some(right_symbol),
|
||||
prev: None, // TODO
|
||||
section_kind: left_section.kind,
|
||||
});
|
||||
left_used.insert(left_symbol);
|
||||
right_used.insert(right_symbol);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Find matching symbols between each object.
|
||||
fn matching_symbols(
|
||||
left: Option<&ObjInfo>,
|
||||
right: Option<&ObjInfo>,
|
||||
prev: Option<&ObjInfo>,
|
||||
mappings: &MappingConfig,
|
||||
) -> Result<Vec<SymbolMatch>> {
|
||||
let mut matches = Vec::new();
|
||||
let mut left_used = HashSet::new();
|
||||
let mut right_used = HashSet::new();
|
||||
if let Some(left) = left {
|
||||
if let Some(right) = right {
|
||||
apply_symbol_mappings(
|
||||
left,
|
||||
right,
|
||||
mappings,
|
||||
&mut left_used,
|
||||
&mut right_used,
|
||||
&mut matches,
|
||||
)?;
|
||||
}
|
||||
for (section_idx, section) in left.sections.iter().enumerate() {
|
||||
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
|
||||
let symbol_ref = SymbolRef { section_idx, symbol_idx };
|
||||
if left_used.contains(&symbol_ref) {
|
||||
continue;
|
||||
}
|
||||
let symbol_match = SymbolMatch {
|
||||
left: Some(symbol_ref),
|
||||
right: find_symbol(right, symbol, section, Some(&right_used)),
|
||||
prev: find_symbol(prev, symbol, section, None),
|
||||
section_kind: section.kind,
|
||||
};
|
||||
matches.push(symbol_match);
|
||||
if let Some(right) = symbol_match.right {
|
||||
right_used.insert(right);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (symbol_idx, symbol) in left.common.iter().enumerate() {
|
||||
let symbol_ref = SymbolRef { section_idx: SECTION_COMMON, symbol_idx };
|
||||
if left_used.contains(&symbol_ref) {
|
||||
continue;
|
||||
}
|
||||
let symbol_match = SymbolMatch {
|
||||
left: Some(symbol_ref),
|
||||
right: find_common_symbol(right, symbol),
|
||||
prev: find_common_symbol(prev, symbol),
|
||||
section_kind: ObjSectionKind::Bss,
|
||||
};
|
||||
matches.push(symbol_match);
|
||||
if let Some(right) = symbol_match.right {
|
||||
right_used.insert(right);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(right) = right {
|
||||
for (section_idx, section) in right.sections.iter().enumerate() {
|
||||
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
|
||||
let symbol_ref = SymbolRef { section_idx, symbol_idx };
|
||||
if right_used.contains(&symbol_ref) {
|
||||
continue;
|
||||
}
|
||||
matches.push(SymbolMatch {
|
||||
left: None,
|
||||
right: Some(symbol_ref),
|
||||
prev: find_symbol(prev, symbol, section, None),
|
||||
section_kind: section.kind,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (symbol_idx, symbol) in right.common.iter().enumerate() {
|
||||
let symbol_ref = SymbolRef { section_idx: SECTION_COMMON, symbol_idx };
|
||||
if right_used.contains(&symbol_ref) {
|
||||
continue;
|
||||
}
|
||||
matches.push(SymbolMatch {
|
||||
left: None,
|
||||
right: Some(symbol_ref),
|
||||
prev: find_common_symbol(prev, symbol),
|
||||
section_kind: ObjSectionKind::Bss,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(matches)
|
||||
}
|
||||
|
||||
fn unmatched_symbols<'section, 'used>(
|
||||
section: &'section ObjSection,
|
||||
section_idx: usize,
|
||||
used: Option<&'used HashSet<SymbolRef>>,
|
||||
) -> impl Iterator<Item = (usize, &'section ObjSymbol)> + 'used
|
||||
where
|
||||
'section: 'used,
|
||||
{
|
||||
section.symbols.iter().enumerate().filter(move |&(symbol_idx, _)| {
|
||||
// Skip symbols that have already been matched
|
||||
!used.map(|u| u.contains(&SymbolRef { section_idx, symbol_idx })).unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
fn find_symbol(
|
||||
obj: Option<&ObjInfo>,
|
||||
in_symbol: &ObjSymbol,
|
||||
in_section: &ObjSection,
|
||||
used: Option<&HashSet<SymbolRef>>,
|
||||
) -> Option<SymbolRef> {
|
||||
let obj = obj?;
|
||||
// Try to find an exact name match
|
||||
for (section_idx, section) in obj.sections.iter().enumerate() {
|
||||
if section.kind != in_section.kind {
|
||||
continue;
|
||||
}
|
||||
if let Some((symbol_idx, _)) = unmatched_symbols(section, section_idx, used)
|
||||
.find(|(_, symbol)| symbol.name == in_symbol.name)
|
||||
{
|
||||
return Some(SymbolRef { section_idx, symbol_idx });
|
||||
}
|
||||
}
|
||||
// Match compiler-generated symbols against each other (e.g. @251 -> @60)
|
||||
// If they are at the same address in the same section
|
||||
if in_symbol.name.starts_with('@')
|
||||
&& matches!(in_section.kind, ObjSectionKind::Data | ObjSectionKind::Bss)
|
||||
{
|
||||
if let Some((section_idx, section)) =
|
||||
obj.sections.iter().enumerate().find(|(_, s)| s.name == in_section.name)
|
||||
{
|
||||
if let Some((symbol_idx, _)) =
|
||||
unmatched_symbols(section, section_idx, used).find(|(_, symbol)| {
|
||||
symbol.address == in_symbol.address && symbol.name.starts_with('@')
|
||||
})
|
||||
{
|
||||
return Some(SymbolRef { section_idx, symbol_idx });
|
||||
}
|
||||
}
|
||||
}
|
||||
// Match Metrowerks symbol$1234 against symbol$2345
|
||||
if let Some((prefix, suffix)) = in_symbol.name.split_once('$') {
|
||||
if !suffix.chars().all(char::is_numeric) {
|
||||
return None;
|
||||
}
|
||||
for (section_idx, section) in obj.sections.iter().enumerate() {
|
||||
if section.kind != in_section.kind {
|
||||
continue;
|
||||
}
|
||||
if let Some((symbol_idx, _)) =
|
||||
unmatched_symbols(section, section_idx, used).find(|&(_, symbol)| {
|
||||
if let Some((p, s)) = symbol.name.split_once('$') {
|
||||
prefix == p && s.chars().all(char::is_numeric)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
{
|
||||
return Some(SymbolRef { section_idx, symbol_idx });
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn find_common_symbol(obj: Option<&ObjInfo>, in_symbol: &ObjSymbol) -> Option<SymbolRef> {
|
||||
let obj = obj?;
|
||||
for (symbol_idx, symbol) in obj.common.iter().enumerate() {
|
||||
if symbol.name == in_symbol.name {
|
||||
return Some(SymbolRef { section_idx: SECTION_COMMON, symbol_idx });
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Find matching sections between each object.
|
||||
fn matching_sections(left: Option<&ObjInfo>, right: Option<&ObjInfo>) -> Result<Vec<SectionMatch>> {
|
||||
let mut matches = Vec::new();
|
||||
if let Some(left) = left {
|
||||
for (section_idx, section) in left.sections.iter().enumerate() {
|
||||
matches.push(SectionMatch {
|
||||
left: Some(section_idx),
|
||||
right: find_section(right, §ion.name, section.kind),
|
||||
section_kind: section.kind,
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(right) = right {
|
||||
for (section_idx, section) in right.sections.iter().enumerate() {
|
||||
if matches.iter().any(|m| m.right == Some(section_idx)) {
|
||||
continue;
|
||||
}
|
||||
matches.push(SectionMatch {
|
||||
left: None,
|
||||
right: Some(section_idx),
|
||||
section_kind: section.kind,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(matches)
|
||||
}
|
||||
|
||||
fn find_section(obj: Option<&ObjInfo>, name: &str, section_kind: ObjSectionKind) -> Option<usize> {
|
||||
for (section_idx, section) in obj?.sections.iter().enumerate() {
|
||||
if section.kind != section_kind {
|
||||
continue;
|
||||
}
|
||||
if section.name == name {
|
||||
return Some(section_idx);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
12
objdiff-core/src/lib.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
#[cfg(feature = "any-arch")]
|
||||
pub mod arch;
|
||||
#[cfg(feature = "bindings")]
|
||||
pub mod bindings;
|
||||
#[cfg(feature = "config")]
|
||||
pub mod config;
|
||||
#[cfg(feature = "any-arch")]
|
||||
pub mod diff;
|
||||
#[cfg(feature = "any-arch")]
|
||||
pub mod obj;
|
||||
#[cfg(feature = "any-arch")]
|
||||
pub mod util;
|
||||
190
objdiff-core/src/obj/mod.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
pub mod read;
|
||||
pub mod split_meta;
|
||||
|
||||
use std::{borrow::Cow, collections::BTreeMap, fmt, path::PathBuf};
|
||||
|
||||
use filetime::FileTime;
|
||||
use flagset::{flags, FlagSet};
|
||||
use object::RelocationFlags;
|
||||
use split_meta::SplitMeta;
|
||||
|
||||
use crate::{arch::ObjArch, util::ReallySigned};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
pub enum ObjSectionKind {
|
||||
Code,
|
||||
Data,
|
||||
Bss,
|
||||
}
|
||||
flags! {
|
||||
pub enum ObjSymbolFlags: u8 {
|
||||
Global,
|
||||
Local,
|
||||
Weak,
|
||||
Common,
|
||||
Hidden,
|
||||
/// Has extra data associated with the symbol
|
||||
/// (e.g. exception table entry)
|
||||
HasExtra,
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Copy, Clone, Default)]
|
||||
pub struct ObjSymbolFlagSet(pub FlagSet<ObjSymbolFlags>);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjSection {
|
||||
pub name: String,
|
||||
pub kind: ObjSectionKind,
|
||||
pub address: u64,
|
||||
pub size: u64,
|
||||
pub data: Vec<u8>,
|
||||
pub orig_index: usize,
|
||||
pub symbols: Vec<ObjSymbol>,
|
||||
pub relocations: Vec<ObjReloc>,
|
||||
pub virtual_address: Option<u64>,
|
||||
/// Line number info (.line or .debug_line section)
|
||||
pub line_info: BTreeMap<u64, u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum ObjInsArgValue {
|
||||
Signed(i64),
|
||||
Unsigned(u64),
|
||||
Opaque(Cow<'static, str>),
|
||||
}
|
||||
|
||||
impl ObjInsArgValue {
|
||||
pub fn loose_eq(&self, other: &ObjInsArgValue) -> bool {
|
||||
match (self, other) {
|
||||
(ObjInsArgValue::Signed(a), ObjInsArgValue::Signed(b)) => a == b,
|
||||
(ObjInsArgValue::Unsigned(a), ObjInsArgValue::Unsigned(b)) => a == b,
|
||||
(ObjInsArgValue::Signed(a), ObjInsArgValue::Unsigned(b))
|
||||
| (ObjInsArgValue::Unsigned(b), ObjInsArgValue::Signed(a)) => *a as u64 == *b,
|
||||
(ObjInsArgValue::Opaque(a), ObjInsArgValue::Opaque(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ObjInsArgValue {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ObjInsArgValue::Signed(v) => write!(f, "{:#x}", ReallySigned(*v)),
|
||||
ObjInsArgValue::Unsigned(v) => write!(f, "{:#x}", v),
|
||||
ObjInsArgValue::Opaque(v) => write!(f, "{}", v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum ObjInsArg {
|
||||
PlainText(Cow<'static, str>),
|
||||
Arg(ObjInsArgValue),
|
||||
Reloc,
|
||||
BranchDest(u64),
|
||||
}
|
||||
|
||||
impl ObjInsArg {
|
||||
#[inline]
|
||||
pub fn is_plain_text(&self) -> bool { matches!(self, ObjInsArg::PlainText(_)) }
|
||||
|
||||
pub fn loose_eq(&self, other: &ObjInsArg) -> bool {
|
||||
match (self, other) {
|
||||
(ObjInsArg::Arg(a), ObjInsArg::Arg(b)) => a.loose_eq(b),
|
||||
(ObjInsArg::Reloc, ObjInsArg::Reloc) => true,
|
||||
(ObjInsArg::BranchDest(a), ObjInsArg::BranchDest(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjIns {
|
||||
pub address: u64,
|
||||
pub size: u8,
|
||||
pub op: u16,
|
||||
pub mnemonic: Cow<'static, str>,
|
||||
pub args: Vec<ObjInsArg>,
|
||||
pub reloc: Option<ObjReloc>,
|
||||
pub branch_dest: Option<u64>,
|
||||
/// Line number
|
||||
pub line: Option<u32>,
|
||||
/// Formatted instruction
|
||||
pub formatted: String,
|
||||
/// Original (unsimplified) instruction
|
||||
pub orig: Option<String>,
|
||||
}
|
||||
|
||||
impl ObjIns {
|
||||
/// Iterate over non-PlainText arguments.
|
||||
#[inline]
|
||||
pub fn iter_args(&self) -> impl DoubleEndedIterator<Item = &ObjInsArg> {
|
||||
self.args.iter().filter(|a| !a.is_plain_text())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
|
||||
pub enum ObjSymbolKind {
|
||||
#[default]
|
||||
Unknown,
|
||||
Function,
|
||||
Object,
|
||||
Section,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjSymbol {
|
||||
pub name: String,
|
||||
pub demangled_name: Option<String>,
|
||||
pub address: u64,
|
||||
pub section_address: u64,
|
||||
pub size: u64,
|
||||
pub size_known: bool,
|
||||
pub kind: ObjSymbolKind,
|
||||
pub flags: ObjSymbolFlagSet,
|
||||
pub orig_section_index: Option<usize>,
|
||||
/// Original virtual address (from .note.split section)
|
||||
pub virtual_address: Option<u64>,
|
||||
/// Original index in object symbol table
|
||||
pub original_index: Option<usize>,
|
||||
pub bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct ObjInfo {
|
||||
pub arch: Box<dyn ObjArch>,
|
||||
pub path: Option<PathBuf>,
|
||||
pub timestamp: Option<FileTime>,
|
||||
pub sections: Vec<ObjSection>,
|
||||
/// Common BSS symbols
|
||||
pub common: Vec<ObjSymbol>,
|
||||
/// Split object metadata (.note.split section)
|
||||
pub split_meta: Option<SplitMeta>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjReloc {
|
||||
pub flags: RelocationFlags,
|
||||
pub address: u64,
|
||||
pub target: ObjSymbol,
|
||||
pub addend: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||
pub struct SymbolRef {
|
||||
pub section_idx: usize,
|
||||
pub symbol_idx: usize,
|
||||
}
|
||||
|
||||
pub const SECTION_COMMON: usize = usize::MAX - 1;
|
||||
|
||||
impl ObjInfo {
|
||||
pub fn section_symbol(&self, symbol_ref: SymbolRef) -> (Option<&ObjSection>, &ObjSymbol) {
|
||||
if symbol_ref.section_idx == SECTION_COMMON {
|
||||
let symbol = &self.common[symbol_ref.symbol_idx];
|
||||
return (None, symbol);
|
||||
}
|
||||
let section = &self.sections[symbol_ref.section_idx];
|
||||
let symbol = §ion.symbols[symbol_ref.symbol_idx];
|
||||
(Some(section), symbol)
|
||||
}
|
||||
}
|
||||
756
objdiff-core/src/obj/read.rs
Normal file
@@ -0,0 +1,756 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fs,
|
||||
io::Cursor,
|
||||
mem::size_of,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
use filetime::FileTime;
|
||||
use flagset::Flags;
|
||||
use object::{
|
||||
endian::LittleEndian as LE,
|
||||
pe::{ImageAuxSymbolFunctionBeginEnd, ImageLinenumber},
|
||||
read::coff::{CoffFile, CoffHeader, ImageSymbol},
|
||||
BinaryFormat, File, Object, ObjectSection, ObjectSymbol, RelocationTarget, Section,
|
||||
SectionIndex, SectionKind, Symbol, SymbolIndex, SymbolKind, SymbolScope,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
arch::{new_arch, ObjArch},
|
||||
diff::DiffObjConfig,
|
||||
obj::{
|
||||
split_meta::{SplitMeta, SPLITMETA_SECTION},
|
||||
ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags,
|
||||
ObjSymbolKind,
|
||||
},
|
||||
util::{read_u16, read_u32},
|
||||
};
|
||||
|
||||
fn to_obj_section_kind(kind: SectionKind) -> Option<ObjSectionKind> {
|
||||
match kind {
|
||||
SectionKind::Text => Some(ObjSectionKind::Code),
|
||||
SectionKind::Data | SectionKind::ReadOnlyData => Some(ObjSectionKind::Data),
|
||||
SectionKind::UninitializedData => Some(ObjSectionKind::Bss),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_obj_symbol(
|
||||
arch: &dyn ObjArch,
|
||||
obj_file: &File<'_>,
|
||||
symbol: &Symbol<'_, '_>,
|
||||
split_meta: Option<&SplitMeta>,
|
||||
) -> Result<ObjSymbol> {
|
||||
let mut name = symbol.name().context("Failed to process symbol name")?;
|
||||
if name.is_empty() {
|
||||
log::warn!("Found empty sym: {symbol:?}");
|
||||
name = "?";
|
||||
}
|
||||
let mut flags = ObjSymbolFlagSet(ObjSymbolFlags::none());
|
||||
if symbol.is_global() {
|
||||
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Global);
|
||||
}
|
||||
if symbol.is_local() {
|
||||
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Local);
|
||||
}
|
||||
if symbol.is_common() {
|
||||
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Common);
|
||||
}
|
||||
if symbol.is_weak() {
|
||||
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Weak);
|
||||
}
|
||||
if obj_file.format() == BinaryFormat::Elf && symbol.scope() == SymbolScope::Linkage {
|
||||
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Hidden);
|
||||
}
|
||||
#[cfg(feature = "ppc")]
|
||||
if arch.ppc().and_then(|a| a.extab.as_ref()).is_some_and(|e| e.contains_key(&symbol.index().0))
|
||||
{
|
||||
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::HasExtra);
|
||||
}
|
||||
let address = arch.symbol_address(symbol);
|
||||
let section_address = if let Some(section) =
|
||||
symbol.section_index().and_then(|idx| obj_file.section_by_index(idx).ok())
|
||||
{
|
||||
address - section.address()
|
||||
} else {
|
||||
address
|
||||
};
|
||||
let demangled_name = arch.demangle(name);
|
||||
// Find the virtual address for the symbol if available
|
||||
let virtual_address = split_meta
|
||||
.and_then(|m| m.virtual_addresses.as_ref())
|
||||
.and_then(|v| v.get(symbol.index().0).cloned());
|
||||
|
||||
let bytes = symbol
|
||||
.section_index()
|
||||
.and_then(|idx| obj_file.section_by_index(idx).ok())
|
||||
.and_then(|section| section.data().ok())
|
||||
.and_then(|data| {
|
||||
data.get(section_address as usize..(section_address + symbol.size()) as usize)
|
||||
})
|
||||
.unwrap_or(&[]);
|
||||
|
||||
let kind = match symbol.kind() {
|
||||
SymbolKind::Text => ObjSymbolKind::Function,
|
||||
SymbolKind::Data => ObjSymbolKind::Object,
|
||||
SymbolKind::Section => ObjSymbolKind::Section,
|
||||
_ => ObjSymbolKind::Unknown,
|
||||
};
|
||||
|
||||
Ok(ObjSymbol {
|
||||
name: name.to_string(),
|
||||
demangled_name,
|
||||
address,
|
||||
section_address,
|
||||
size: symbol.size(),
|
||||
size_known: symbol.size() != 0,
|
||||
kind,
|
||||
flags,
|
||||
orig_section_index: symbol.section_index().map(|i| i.0),
|
||||
virtual_address,
|
||||
original_index: Some(symbol.index().0),
|
||||
bytes: bytes.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
fn filter_sections(obj_file: &File<'_>, split_meta: Option<&SplitMeta>) -> Result<Vec<ObjSection>> {
|
||||
let mut result = Vec::<ObjSection>::new();
|
||||
for section in obj_file.sections() {
|
||||
if section.size() == 0 {
|
||||
continue;
|
||||
}
|
||||
let Some(kind) = to_obj_section_kind(section.kind()) else {
|
||||
continue;
|
||||
};
|
||||
let name = section.name().context("Failed to process section name")?;
|
||||
let data = section.uncompressed_data().context("Failed to read section data")?;
|
||||
|
||||
// Find the virtual address for the section symbol if available
|
||||
let section_symbol = obj_file.symbols().find(|s| {
|
||||
s.kind() == SymbolKind::Section && s.section_index() == Some(section.index())
|
||||
});
|
||||
let virtual_address = section_symbol.and_then(|s| {
|
||||
split_meta
|
||||
.and_then(|m| m.virtual_addresses.as_ref())
|
||||
.and_then(|v| v.get(s.index().0).cloned())
|
||||
});
|
||||
|
||||
result.push(ObjSection {
|
||||
name: name.to_string(),
|
||||
kind,
|
||||
address: section.address(),
|
||||
size: section.size(),
|
||||
data: data.to_vec(),
|
||||
orig_index: section.index().0,
|
||||
symbols: Vec::new(),
|
||||
relocations: Vec::new(),
|
||||
virtual_address,
|
||||
line_info: Default::default(),
|
||||
});
|
||||
}
|
||||
result.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn symbols_by_section(
|
||||
arch: &dyn ObjArch,
|
||||
obj_file: &File<'_>,
|
||||
section: &ObjSection,
|
||||
section_symbols: &[Symbol<'_, '_>],
|
||||
split_meta: Option<&SplitMeta>,
|
||||
name_counts: &mut HashMap<String, u32>,
|
||||
) -> Result<Vec<ObjSymbol>> {
|
||||
let mut result = Vec::<ObjSymbol>::new();
|
||||
for symbol in section_symbols {
|
||||
if symbol.kind() == SymbolKind::Section {
|
||||
continue;
|
||||
}
|
||||
if symbol.is_local() && section.kind == ObjSectionKind::Code {
|
||||
// TODO strip local syms in diff?
|
||||
let name = symbol.name().context("Failed to process symbol name")?;
|
||||
if symbol.size() == 0 || name.starts_with("lbl_") {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result.push(to_obj_symbol(arch, obj_file, symbol, split_meta)?);
|
||||
}
|
||||
result.sort_by(|a, b| a.address.cmp(&b.address).then(a.size.cmp(&b.size)));
|
||||
let mut iter = result.iter_mut().peekable();
|
||||
while let Some(symbol) = iter.next() {
|
||||
if symbol.size == 0 {
|
||||
if let Some(next_symbol) = iter.peek() {
|
||||
symbol.size = next_symbol.address - symbol.address;
|
||||
} else {
|
||||
symbol.size = (section.address + section.size) - symbol.address;
|
||||
}
|
||||
// Set symbol kind if we ended up with a non-zero size
|
||||
if symbol.kind == ObjSymbolKind::Unknown && symbol.size > 0 {
|
||||
symbol.kind = match section.kind {
|
||||
ObjSectionKind::Code => ObjSymbolKind::Function,
|
||||
ObjSectionKind::Data | ObjSectionKind::Bss => ObjSymbolKind::Object,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if result.is_empty() {
|
||||
// Dummy symbol for empty sections
|
||||
*name_counts.entry(section.name.clone()).or_insert(0) += 1;
|
||||
let current_count: u32 = *name_counts.get(§ion.name).unwrap();
|
||||
result.push(ObjSymbol {
|
||||
name: if current_count > 1 {
|
||||
format!("[{} ({})]", section.name, current_count)
|
||||
} else {
|
||||
format!("[{}]", section.name)
|
||||
},
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
section_address: 0,
|
||||
size: section.size,
|
||||
size_known: true,
|
||||
kind: match section.kind {
|
||||
ObjSectionKind::Code => ObjSymbolKind::Function,
|
||||
ObjSectionKind::Data | ObjSectionKind::Bss => ObjSymbolKind::Object,
|
||||
},
|
||||
flags: Default::default(),
|
||||
orig_section_index: Some(section.orig_index),
|
||||
virtual_address: None,
|
||||
original_index: None,
|
||||
bytes: Vec::new(),
|
||||
});
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn common_symbols(
|
||||
arch: &dyn ObjArch,
|
||||
obj_file: &File<'_>,
|
||||
split_meta: Option<&SplitMeta>,
|
||||
) -> Result<Vec<ObjSymbol>> {
|
||||
obj_file
|
||||
.symbols()
|
||||
.filter(Symbol::is_common)
|
||||
.map(|symbol| to_obj_symbol(arch, obj_file, &symbol, split_meta))
|
||||
.collect::<Result<Vec<ObjSymbol>>>()
|
||||
}
|
||||
|
||||
const LOW_PRIORITY_SYMBOLS: &[&str] =
|
||||
&["__gnu_compiled_c", "__gnu_compiled_cplusplus", "gcc2_compiled."];
|
||||
|
||||
fn best_symbol<'r, 'data, 'file>(
|
||||
symbols: &'r [Symbol<'data, 'file>],
|
||||
address: u64,
|
||||
) -> Option<&'r Symbol<'data, 'file>> {
|
||||
let mut closest_symbol_index = match symbols.binary_search_by_key(&address, |s| s.address()) {
|
||||
Ok(index) => Some(index),
|
||||
Err(index) => index.checked_sub(1),
|
||||
}?;
|
||||
// The binary search may not find the first symbol at the address, so work backwards
|
||||
let target_address = symbols[closest_symbol_index].address();
|
||||
while let Some(prev_index) = closest_symbol_index.checked_sub(1) {
|
||||
if symbols[prev_index].address() != target_address {
|
||||
break;
|
||||
}
|
||||
closest_symbol_index = prev_index;
|
||||
}
|
||||
let mut best_symbol: Option<&'r Symbol<'data, 'file>> = None;
|
||||
for symbol in symbols.iter().skip(closest_symbol_index) {
|
||||
if symbol.address() > address {
|
||||
break;
|
||||
}
|
||||
if symbol.kind() == SymbolKind::Section
|
||||
|| (symbol.size() > 0 && (symbol.address() + symbol.size()) <= address)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// TODO priority ranking with visibility, etc
|
||||
if let Some(best) = best_symbol {
|
||||
if LOW_PRIORITY_SYMBOLS.contains(&best.name().unwrap_or_default())
|
||||
&& !LOW_PRIORITY_SYMBOLS.contains(&symbol.name().unwrap_or_default())
|
||||
{
|
||||
best_symbol = Some(symbol);
|
||||
}
|
||||
} else {
|
||||
best_symbol = Some(symbol);
|
||||
}
|
||||
}
|
||||
best_symbol
|
||||
}
|
||||
|
||||
fn find_section_symbol(
|
||||
arch: &dyn ObjArch,
|
||||
obj_file: &File<'_>,
|
||||
section: &Section,
|
||||
section_symbols: &[Symbol<'_, '_>],
|
||||
address: u64,
|
||||
split_meta: Option<&SplitMeta>,
|
||||
) -> Result<ObjSymbol> {
|
||||
if let Some(symbol) = best_symbol(section_symbols, address) {
|
||||
return to_obj_symbol(arch, obj_file, symbol, split_meta);
|
||||
}
|
||||
// Fallback to section symbol
|
||||
Ok(ObjSymbol {
|
||||
name: section.name()?.to_string(),
|
||||
demangled_name: None,
|
||||
address: section.address(),
|
||||
section_address: 0,
|
||||
size: 0,
|
||||
size_known: false,
|
||||
kind: ObjSymbolKind::Section,
|
||||
flags: Default::default(),
|
||||
orig_section_index: Some(section.index().0),
|
||||
virtual_address: None,
|
||||
original_index: None,
|
||||
bytes: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn relocations_by_section(
|
||||
arch: &dyn ObjArch,
|
||||
obj_file: &File<'_>,
|
||||
section: &ObjSection,
|
||||
section_symbols: &[Vec<Symbol<'_, '_>>],
|
||||
split_meta: Option<&SplitMeta>,
|
||||
) -> Result<Vec<ObjReloc>> {
|
||||
let obj_section = obj_file.section_by_index(SectionIndex(section.orig_index))?;
|
||||
let mut relocations = Vec::<ObjReloc>::new();
|
||||
for (address, reloc) in obj_section.relocations() {
|
||||
let symbol = match reloc.target() {
|
||||
RelocationTarget::Symbol(idx) => {
|
||||
if idx.0 == u32::MAX as usize {
|
||||
// ???
|
||||
continue;
|
||||
}
|
||||
let Ok(symbol) = obj_file.symbol_by_index(idx) else {
|
||||
log::warn!(
|
||||
"Failed to locate relocation {:#x} target symbol {}",
|
||||
address,
|
||||
idx.0
|
||||
);
|
||||
continue;
|
||||
};
|
||||
symbol
|
||||
}
|
||||
RelocationTarget::Absolute => {
|
||||
log::warn!("Ignoring absolute relocation @ {}:{:#x}", section.name, address);
|
||||
continue;
|
||||
}
|
||||
_ => bail!("Unhandled relocation target: {:?}", reloc.target()),
|
||||
};
|
||||
let flags = reloc.flags(); // TODO validate reloc here?
|
||||
let mut addend = if reloc.has_implicit_addend() {
|
||||
arch.implcit_addend(obj_file, section, address, &reloc)?
|
||||
} else {
|
||||
reloc.addend()
|
||||
};
|
||||
let target = match symbol.kind() {
|
||||
SymbolKind::Text | SymbolKind::Data | SymbolKind::Label | SymbolKind::Unknown => {
|
||||
to_obj_symbol(arch, obj_file, &symbol, split_meta)?
|
||||
}
|
||||
SymbolKind::Section => {
|
||||
ensure!(addend >= 0, "Negative addend in section reloc: {addend}");
|
||||
let section_index = symbol
|
||||
.section_index()
|
||||
.ok_or_else(|| anyhow!("Section symbol {symbol:?} has no section index"))?;
|
||||
let section = obj_file.section_by_index(section_index)?;
|
||||
let symbol = find_section_symbol(
|
||||
arch,
|
||||
obj_file,
|
||||
§ion,
|
||||
§ion_symbols[section_index.0],
|
||||
addend as u64,
|
||||
split_meta,
|
||||
)?;
|
||||
// Adjust addend to be relative to the selected symbol
|
||||
addend = (symbol.address - section.address()) as i64;
|
||||
symbol
|
||||
}
|
||||
kind => bail!("Unhandled relocation symbol type {kind:?}"),
|
||||
};
|
||||
relocations.push(ObjReloc { flags, address, target, addend });
|
||||
}
|
||||
Ok(relocations)
|
||||
}
|
||||
|
||||
fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection], obj_data: &[u8]) -> Result<()> {
|
||||
// DWARF 1.1
|
||||
if let Some(section) = obj_file.section_by_name(".line") {
|
||||
let data = section.uncompressed_data()?;
|
||||
let mut reader = Cursor::new(data.as_ref());
|
||||
|
||||
let mut text_sections = obj_file.sections().filter(|s| s.kind() == SectionKind::Text);
|
||||
while reader.position() < data.len() as u64 {
|
||||
let text_section_index = text_sections
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("Next text section not found for line info"))?
|
||||
.index()
|
||||
.0;
|
||||
let start = reader.position();
|
||||
let size = read_u32(obj_file, &mut reader)?;
|
||||
let base_address = read_u32(obj_file, &mut reader)? as u64;
|
||||
let Some(out_section) =
|
||||
sections.iter_mut().find(|s| s.orig_index == text_section_index)
|
||||
else {
|
||||
// Skip line info for sections we filtered out
|
||||
reader.set_position(start + size as u64);
|
||||
continue;
|
||||
};
|
||||
let end = start + size as u64;
|
||||
while reader.position() < end {
|
||||
let line_number = read_u32(obj_file, &mut reader)?;
|
||||
let statement_pos = read_u16(obj_file, &mut reader)?;
|
||||
if statement_pos != 0xFFFF {
|
||||
log::warn!("Unhandled statement pos {}", statement_pos);
|
||||
}
|
||||
let address_delta = read_u32(obj_file, &mut reader)? as u64;
|
||||
out_section.line_info.insert(base_address + address_delta, line_number);
|
||||
log::debug!("Line: {:#x} -> {}", base_address + address_delta, line_number);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DWARF 2+
|
||||
#[cfg(feature = "dwarf")]
|
||||
{
|
||||
let dwarf_cow = gimli::DwarfSections::load(|id| {
|
||||
Ok::<_, gimli::Error>(
|
||||
obj_file
|
||||
.section_by_name(id.name())
|
||||
.and_then(|section| section.uncompressed_data().ok())
|
||||
.unwrap_or(std::borrow::Cow::Borrowed(&[][..])),
|
||||
)
|
||||
})?;
|
||||
let endian = match obj_file.endianness() {
|
||||
object::Endianness::Little => gimli::RunTimeEndian::Little,
|
||||
object::Endianness::Big => gimli::RunTimeEndian::Big,
|
||||
};
|
||||
let dwarf = dwarf_cow.borrow(|section| gimli::EndianSlice::new(section, endian));
|
||||
let mut iter = dwarf.units();
|
||||
if let Some(header) = iter.next()? {
|
||||
let unit = dwarf.unit(header)?;
|
||||
if let Some(program) = unit.line_program.clone() {
|
||||
let mut text_sections =
|
||||
obj_file.sections().filter(|s| s.kind() == SectionKind::Text);
|
||||
let section_index = text_sections.next().map(|s| s.index().0);
|
||||
let mut lines = section_index
|
||||
.and_then(|index| sections.iter_mut().find(|s| s.orig_index == index))
|
||||
.map(|s| &mut s.line_info);
|
||||
|
||||
let mut rows = program.rows();
|
||||
while let Some((_header, row)) = rows.next_row()? {
|
||||
if let (Some(line), Some(lines)) = (row.line(), &mut lines) {
|
||||
lines.insert(row.address(), line.get() as u32);
|
||||
}
|
||||
if row.end_sequence() {
|
||||
// The next row is the start of a new sequence, which means we must
|
||||
// advance to the next .text section.
|
||||
let section_index = text_sections.next().map(|s| s.index().0);
|
||||
lines = section_index
|
||||
.and_then(|index| sections.iter_mut().find(|s| s.orig_index == index))
|
||||
.map(|s| &mut s.line_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if iter.next()?.is_some() {
|
||||
log::warn!("Multiple units found in DWARF data, only processing the first");
|
||||
}
|
||||
}
|
||||
|
||||
// COFF
|
||||
if let File::Coff(coff) = obj_file {
|
||||
line_info_coff(coff, sections, obj_data)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn line_info_coff(coff: &CoffFile, sections: &mut [ObjSection], obj_data: &[u8]) -> Result<()> {
|
||||
let symbol_table = coff.coff_header().symbols(obj_data)?;
|
||||
|
||||
// Enumerate over all sections.
|
||||
for sect in coff.sections() {
|
||||
let ptr_linenums = sect.coff_section().pointer_to_linenumbers.get(LE) as usize;
|
||||
let num_linenums = sect.coff_section().number_of_linenumbers.get(LE) as usize;
|
||||
|
||||
// If we have no line number, skip this section.
|
||||
if num_linenums == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find this section in our out_section. If it's not in out_section,
|
||||
// skip it.
|
||||
let Some(out_section) = sections.iter_mut().find(|s| s.orig_index == sect.index().0) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Turn the line numbers into an ImageLinenumber slice.
|
||||
let Some(linenums) =
|
||||
&obj_data.get(ptr_linenums..ptr_linenums + num_linenums * size_of::<ImageLinenumber>())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Ok(linenums) = object::pod::slice_from_all_bytes::<ImageLinenumber>(linenums) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// In COFF, the line numbers are stored relative to the start of the
|
||||
// function. Because of this, we need to know the line number where the
|
||||
// function starts, so we can sum the two and get the line number
|
||||
// relative to the start of the file.
|
||||
//
|
||||
// This variable stores the line number where the function currently
|
||||
// being processed starts. It is set to None when we failed to find the
|
||||
// line number of the start of the function.
|
||||
let mut cur_fun_start_linenumber = None;
|
||||
for linenum in linenums {
|
||||
let line_number = linenum.linenumber.get(LE);
|
||||
if line_number == 0 {
|
||||
// Starting a new function. We need to find the line where that
|
||||
// function is located in the file. To do this, we need to find
|
||||
// the `.bf` symbol "associated" with this function. The .bf
|
||||
// symbol will have a Function Begin/End Auxillary Record, which
|
||||
// contains the line number of the start of the function.
|
||||
|
||||
// First, set cur_fun_start_linenumber to None. If we fail to
|
||||
// find the start of the function, this will make sure the
|
||||
// subsequent line numbers will be ignored until the next start
|
||||
// of function.
|
||||
cur_fun_start_linenumber = None;
|
||||
|
||||
// Get the symbol associated with this function. We'll need it
|
||||
// for logging purposes, but also to acquire its Function
|
||||
// Auxillary Record, which tells us where to find our .bf symbol.
|
||||
let symtable_entry = linenum.symbol_table_index_or_virtual_address.get(LE);
|
||||
let Ok(symbol) = symbol_table.symbol(SymbolIndex(symtable_entry as usize)) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(aux_fun) = symbol_table.aux_function(SymbolIndex(symtable_entry as usize))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Get the .bf symbol associated with this symbol. To do so, we
|
||||
// look at the Function Auxillary Record's tag_index, which is
|
||||
// an index in the symbol table pointing to our .bf symbol.
|
||||
if aux_fun.tag_index.get(LE) == 0 {
|
||||
continue;
|
||||
}
|
||||
let Ok(bf_symbol) =
|
||||
symbol_table.symbol(SymbolIndex(aux_fun.tag_index.get(LE) as usize))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
// Do some sanity checks that we are, indeed, looking at a .bf
|
||||
// symbol.
|
||||
if bf_symbol.name(symbol_table.strings()) != Ok(b".bf") {
|
||||
continue;
|
||||
}
|
||||
// Get the Function Begin/End Auxillary Record associated with
|
||||
// our .bf symbol, where we'll fine the linenumber of the start
|
||||
// of our function.
|
||||
let Ok(bf_aux) = symbol_table.get::<ImageAuxSymbolFunctionBeginEnd>(
|
||||
SymbolIndex(aux_fun.tag_index.get(LE) as usize),
|
||||
1,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
// Set cur_fun_start_linenumber so the following linenumber
|
||||
// records will know at what line the current function start.
|
||||
cur_fun_start_linenumber = Some(bf_aux.linenumber.get(LE) as u32);
|
||||
// Let's also synthesize a line number record from the start of
|
||||
// the function, as the linenumber records don't always cover it.
|
||||
out_section.line_info.insert(
|
||||
sect.address() + symbol.value() as u64,
|
||||
bf_aux.linenumber.get(LE) as u32,
|
||||
);
|
||||
} else if let Some(cur_linenumber) = cur_fun_start_linenumber {
|
||||
let vaddr = linenum.symbol_table_index_or_virtual_address.get(LE);
|
||||
out_section
|
||||
.line_info
|
||||
.insert(sect.address() + vaddr as u64, cur_linenumber + line_number as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_combined_symbol(symbol: ObjSymbol, address_change: i64) -> Result<ObjSymbol> {
|
||||
Ok(ObjSymbol {
|
||||
name: symbol.name,
|
||||
demangled_name: symbol.demangled_name,
|
||||
address: (symbol.address as i64 + address_change).try_into()?,
|
||||
section_address: (symbol.section_address as i64 + address_change).try_into()?,
|
||||
size: symbol.size,
|
||||
size_known: symbol.size_known,
|
||||
kind: symbol.kind,
|
||||
flags: symbol.flags,
|
||||
orig_section_index: symbol.orig_section_index,
|
||||
virtual_address: if let Some(virtual_address) = symbol.virtual_address {
|
||||
Some((virtual_address as i64 + address_change).try_into()?)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
original_index: symbol.original_index,
|
||||
bytes: symbol.bytes,
|
||||
})
|
||||
}
|
||||
|
||||
fn combine_sections(section: ObjSection, combine: ObjSection) -> Result<ObjSection> {
|
||||
let mut data = section.data;
|
||||
data.extend(combine.data);
|
||||
|
||||
let address_change: i64 = (section.address + section.size) as i64 - combine.address as i64;
|
||||
let mut symbols = section.symbols;
|
||||
for symbol in combine.symbols {
|
||||
symbols.push(update_combined_symbol(symbol, address_change)?);
|
||||
}
|
||||
|
||||
let mut relocations = section.relocations;
|
||||
for reloc in combine.relocations {
|
||||
relocations.push(ObjReloc {
|
||||
flags: reloc.flags,
|
||||
address: (reloc.address as i64 + address_change).try_into()?,
|
||||
target: reloc.target, // TODO: Should be updated?
|
||||
addend: reloc.addend,
|
||||
});
|
||||
}
|
||||
|
||||
let mut line_info = section.line_info;
|
||||
for (addr, line) in combine.line_info {
|
||||
let key = (addr as i64 + address_change).try_into()?;
|
||||
line_info.insert(key, line);
|
||||
}
|
||||
|
||||
Ok(ObjSection {
|
||||
name: section.name,
|
||||
kind: section.kind,
|
||||
address: section.address,
|
||||
size: section.size + combine.size,
|
||||
data,
|
||||
orig_index: section.orig_index,
|
||||
symbols,
|
||||
relocations,
|
||||
virtual_address: section.virtual_address,
|
||||
line_info,
|
||||
})
|
||||
}
|
||||
|
||||
fn combine_data_sections(sections: &mut Vec<ObjSection>) -> Result<()> {
|
||||
let names_to_combine: HashSet<_> = sections
|
||||
.iter()
|
||||
.filter(|s| s.kind == ObjSectionKind::Data)
|
||||
.map(|s| s.name.clone())
|
||||
.collect();
|
||||
|
||||
for name in names_to_combine {
|
||||
// Take section with lowest index
|
||||
let (mut section_index, _) = sections
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, s)| s.name == name)
|
||||
.min_by_key(|(_, s)| s.orig_index)
|
||||
// Should not happen
|
||||
.context("No combine section found with name")?;
|
||||
let mut section = sections.remove(section_index);
|
||||
|
||||
// Remove equally named sections
|
||||
let mut combines = vec![];
|
||||
for i in (0..sections.len()).rev() {
|
||||
if sections[i].name != name || sections[i].orig_index == section.orig_index {
|
||||
continue;
|
||||
}
|
||||
combines.push(sections.remove(i));
|
||||
if i < section_index {
|
||||
section_index -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Combine sections ordered by index
|
||||
combines.sort_unstable_by_key(|c| c.orig_index);
|
||||
for combine in combines {
|
||||
section = combine_sections(section, combine)?;
|
||||
}
|
||||
sections.insert(section_index, section);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read(obj_path: &Path, config: &DiffObjConfig) -> Result<ObjInfo> {
|
||||
let (data, timestamp) = {
|
||||
let file = fs::File::open(obj_path)?;
|
||||
let timestamp = FileTime::from_last_modification_time(&file.metadata()?);
|
||||
(unsafe { memmap2::Mmap::map(&file) }?, timestamp)
|
||||
};
|
||||
let mut obj = parse(&data, config)?;
|
||||
obj.path = Some(obj_path.to_owned());
|
||||
obj.timestamp = Some(timestamp);
|
||||
Ok(obj)
|
||||
}
|
||||
|
||||
pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result<ObjInfo> {
|
||||
let obj_file = File::parse(data)?;
|
||||
let arch = new_arch(&obj_file)?;
|
||||
let split_meta = split_meta(&obj_file)?;
|
||||
|
||||
// Create sorted symbol list for each section
|
||||
let mut section_symbols = Vec::with_capacity(obj_file.sections().count());
|
||||
for section in obj_file.sections() {
|
||||
let mut symbols = obj_file
|
||||
.symbols()
|
||||
.filter(|s| s.section_index() == Some(section.index()))
|
||||
.collect::<Vec<_>>();
|
||||
symbols.sort_by_key(|s| s.address());
|
||||
let section_index = section.index().0;
|
||||
if section_index >= section_symbols.len() {
|
||||
section_symbols.resize_with(section_index + 1, Vec::new);
|
||||
}
|
||||
section_symbols[section_index] = symbols;
|
||||
}
|
||||
|
||||
let mut sections = filter_sections(&obj_file, split_meta.as_ref())?;
|
||||
let mut section_name_counts: HashMap<String, u32> = HashMap::new();
|
||||
for section in &mut sections {
|
||||
section.symbols = symbols_by_section(
|
||||
arch.as_ref(),
|
||||
&obj_file,
|
||||
section,
|
||||
§ion_symbols[section.orig_index],
|
||||
split_meta.as_ref(),
|
||||
&mut section_name_counts,
|
||||
)?;
|
||||
section.relocations = relocations_by_section(
|
||||
arch.as_ref(),
|
||||
&obj_file,
|
||||
section,
|
||||
§ion_symbols,
|
||||
split_meta.as_ref(),
|
||||
)?;
|
||||
}
|
||||
if config.combine_data_sections {
|
||||
combine_data_sections(&mut sections)?;
|
||||
}
|
||||
line_info(&obj_file, &mut sections, data)?;
|
||||
let common = common_symbols(arch.as_ref(), &obj_file, split_meta.as_ref())?;
|
||||
Ok(ObjInfo { arch, path: None, timestamp: None, sections, common, split_meta })
|
||||
}
|
||||
|
||||
pub fn has_function(obj_path: &Path, symbol_name: &str) -> Result<bool> {
|
||||
let data = {
|
||||
let file = fs::File::open(obj_path)?;
|
||||
unsafe { memmap2::Mmap::map(&file) }?
|
||||
};
|
||||
Ok(File::parse(&*data)?
|
||||
.symbol_by_name(symbol_name)
|
||||
.filter(|o| o.kind() == SymbolKind::Text)
|
||||
.is_some())
|
||||
}
|
||||
|
||||
fn split_meta(obj_file: &File<'_>) -> Result<Option<SplitMeta>> {
|
||||
Ok(if let Some(section) = obj_file.section_by_name(SPLITMETA_SECTION) {
|
||||
Some(SplitMeta::from_section(section, obj_file.endianness(), obj_file.is_64())?)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
223
objdiff-core/src/obj/split_meta.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
use std::{io, io::Write};
|
||||
|
||||
use object::{elf::SHT_NOTE, Endian, ObjectSection};
|
||||
|
||||
pub const SPLITMETA_SECTION: &str = ".note.split";
|
||||
pub const SHT_SPLITMETA: u32 = SHT_NOTE;
|
||||
pub const ELF_NOTE_SPLIT: &[u8] = b"Split";
|
||||
|
||||
/// This is used to store metadata about the source of an object file,
|
||||
/// such as the original virtual addresses and the tool that wrote it.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct SplitMeta {
|
||||
/// The tool that generated the object. Informational only.
|
||||
pub generator: Option<String>,
|
||||
/// The name of the source module. (e.g. the DOL or REL name)
|
||||
pub module_name: Option<String>,
|
||||
/// The ID of the source module. (e.g. the DOL or REL ID)
|
||||
pub module_id: Option<u32>,
|
||||
/// Original virtual addresses of each symbol in the object.
|
||||
/// Index 0 is the ELF null symbol.
|
||||
pub virtual_addresses: Option<Vec<u64>>,
|
||||
}
|
||||
|
||||
const NT_SPLIT_GENERATOR: u32 = u32::from_be_bytes(*b"GENR");
|
||||
const NT_SPLIT_MODULE_NAME: u32 = u32::from_be_bytes(*b"MODN");
|
||||
const NT_SPLIT_MODULE_ID: u32 = u32::from_be_bytes(*b"MODI");
|
||||
const NT_SPLIT_VIRTUAL_ADDRESSES: u32 = u32::from_be_bytes(*b"VIRT");
|
||||
|
||||
impl SplitMeta {
|
||||
pub fn from_section<E>(section: object::Section, e: E, is_64: bool) -> io::Result<Self>
|
||||
where E: Endian {
|
||||
let mut result = SplitMeta::default();
|
||||
let data = section.uncompressed_data().map_err(object_io_error)?;
|
||||
let mut iter = NoteIterator::new(data.as_ref(), section.align(), e, is_64)?;
|
||||
while let Some(note) = iter.next(e)? {
|
||||
if note.name != ELF_NOTE_SPLIT {
|
||||
continue;
|
||||
}
|
||||
match note.n_type {
|
||||
NT_SPLIT_GENERATOR => {
|
||||
let string = String::from_utf8(note.desc.to_vec())
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
result.generator = Some(string);
|
||||
}
|
||||
NT_SPLIT_MODULE_NAME => {
|
||||
let string = String::from_utf8(note.desc.to_vec())
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
result.module_name = Some(string);
|
||||
}
|
||||
NT_SPLIT_MODULE_ID => {
|
||||
result.module_id =
|
||||
Some(e.read_u32_bytes(note.desc.try_into().map_err(|_| {
|
||||
io::Error::new(io::ErrorKind::InvalidData, "Invalid module ID size")
|
||||
})?));
|
||||
}
|
||||
NT_SPLIT_VIRTUAL_ADDRESSES => {
|
||||
let vec = if is_64 {
|
||||
let mut vec = vec![0u64; note.desc.len() / 8];
|
||||
for (i, v) in vec.iter_mut().enumerate() {
|
||||
*v =
|
||||
e.read_u64_bytes(note.desc[i * 8..(i + 1) * 8].try_into().unwrap());
|
||||
}
|
||||
vec
|
||||
} else {
|
||||
let mut vec = vec![0u64; note.desc.len() / 4];
|
||||
for (i, v) in vec.iter_mut().enumerate() {
|
||||
*v = e.read_u32_bytes(note.desc[i * 4..(i + 1) * 4].try_into().unwrap())
|
||||
as u64;
|
||||
}
|
||||
vec
|
||||
};
|
||||
result.virtual_addresses = Some(vec);
|
||||
}
|
||||
_ => {
|
||||
// Ignore unknown sections
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn to_writer<E, W>(&self, writer: &mut W, e: E, is_64: bool) -> io::Result<()>
|
||||
where
|
||||
E: Endian,
|
||||
W: Write + ?Sized,
|
||||
{
|
||||
if let Some(generator) = &self.generator {
|
||||
write_note_header(writer, e, NT_SPLIT_GENERATOR, generator.len())?;
|
||||
writer.write_all(generator.as_bytes())?;
|
||||
align_data_to_4(writer, generator.len())?;
|
||||
}
|
||||
if let Some(module_name) = &self.module_name {
|
||||
write_note_header(writer, e, NT_SPLIT_MODULE_NAME, module_name.len())?;
|
||||
writer.write_all(module_name.as_bytes())?;
|
||||
align_data_to_4(writer, module_name.len())?;
|
||||
}
|
||||
if let Some(module_id) = self.module_id {
|
||||
write_note_header(writer, e, NT_SPLIT_MODULE_ID, 4)?;
|
||||
writer.write_all(&e.write_u32_bytes(module_id))?;
|
||||
}
|
||||
if let Some(virtual_addresses) = &self.virtual_addresses {
|
||||
let count = virtual_addresses.len();
|
||||
let size = if is_64 { count * 8 } else { count * 4 };
|
||||
write_note_header(writer, e, NT_SPLIT_VIRTUAL_ADDRESSES, size)?;
|
||||
if is_64 {
|
||||
for &addr in virtual_addresses {
|
||||
writer.write_all(&e.write_u64_bytes(addr))?;
|
||||
}
|
||||
} else {
|
||||
for &addr in virtual_addresses {
|
||||
writer.write_all(&e.write_u32_bytes(addr as u32))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_size(&self, is_64: bool) -> usize {
|
||||
let mut size = 0;
|
||||
if let Some(generator) = self.generator.as_deref() {
|
||||
size += NOTE_HEADER_SIZE + generator.len();
|
||||
size = align_size_to_4(size);
|
||||
}
|
||||
if let Some(module_name) = self.module_name.as_deref() {
|
||||
size += NOTE_HEADER_SIZE + module_name.len();
|
||||
size = align_size_to_4(size);
|
||||
}
|
||||
if self.module_id.is_some() {
|
||||
size += NOTE_HEADER_SIZE + 4;
|
||||
size = align_size_to_4(size);
|
||||
}
|
||||
if let Some(virtual_addresses) = self.virtual_addresses.as_deref() {
|
||||
size += NOTE_HEADER_SIZE + if is_64 { 8 } else { 4 } * virtual_addresses.len();
|
||||
size = align_size_to_4(size);
|
||||
}
|
||||
size
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an object::read::Error to an io::Error.
|
||||
fn object_io_error(err: object::read::Error) -> io::Error {
|
||||
io::Error::new(io::ErrorKind::InvalidData, err)
|
||||
}
|
||||
|
||||
/// An ELF note entry.
|
||||
struct Note<'data> {
|
||||
n_type: u32,
|
||||
name: &'data [u8],
|
||||
desc: &'data [u8],
|
||||
}
|
||||
|
||||
/// object::read::elf::NoteIterator is awkward to use generically,
|
||||
/// so wrap it in our own iterator.
|
||||
enum NoteIterator<'data, E>
|
||||
where E: Endian
|
||||
{
|
||||
B32(object::read::elf::NoteIterator<'data, object::elf::FileHeader32<E>>),
|
||||
B64(object::read::elf::NoteIterator<'data, object::elf::FileHeader64<E>>),
|
||||
}
|
||||
|
||||
impl<'data, E> NoteIterator<'data, E>
|
||||
where E: Endian
|
||||
{
|
||||
fn new(data: &'data [u8], align: u64, e: E, is_64: bool) -> io::Result<Self> {
|
||||
Ok(if is_64 {
|
||||
NoteIterator::B64(
|
||||
object::read::elf::NoteIterator::new(e, align, data).map_err(object_io_error)?,
|
||||
)
|
||||
} else {
|
||||
NoteIterator::B32(
|
||||
object::read::elf::NoteIterator::new(e, align as u32, data)
|
||||
.map_err(object_io_error)?,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn next(&mut self, e: E) -> io::Result<Option<Note<'data>>> {
|
||||
match self {
|
||||
NoteIterator::B32(iter) => Ok(iter.next().map_err(object_io_error)?.map(|note| Note {
|
||||
n_type: note.n_type(e),
|
||||
name: note.name(),
|
||||
desc: note.desc(),
|
||||
})),
|
||||
NoteIterator::B64(iter) => Ok(iter.next().map_err(object_io_error)?.map(|note| Note {
|
||||
n_type: note.n_type(e),
|
||||
name: note.name(),
|
||||
desc: note.desc(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn align_size_to_4(size: usize) -> usize { (size + 3) & !3 }
|
||||
|
||||
fn align_data_to_4<W: Write + ?Sized>(writer: &mut W, len: usize) -> io::Result<()> {
|
||||
const ALIGN_BYTES: &[u8] = &[0; 4];
|
||||
if len % 4 != 0 {
|
||||
writer.write_all(&ALIGN_BYTES[..4 - len % 4])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ELF note format:
|
||||
// Name Size | 4 bytes (integer)
|
||||
// Desc Size | 4 bytes (integer)
|
||||
// Type | 4 bytes (usually interpreted as an integer)
|
||||
// Name | variable size, padded to a 4 byte boundary
|
||||
// Desc | variable size, padded to a 4 byte boundary
|
||||
const NOTE_HEADER_SIZE: usize = 12 + ((ELF_NOTE_SPLIT.len() + 4) & !3);
|
||||
|
||||
fn write_note_header<E, W>(writer: &mut W, e: E, kind: u32, desc_len: usize) -> io::Result<()>
|
||||
where
|
||||
E: Endian,
|
||||
W: Write + ?Sized,
|
||||
{
|
||||
writer.write_all(&e.write_u32_bytes(ELF_NOTE_SPLIT.len() as u32 + 1))?; // Name Size
|
||||
writer.write_all(&e.write_u32_bytes(desc_len as u32))?; // Desc Size
|
||||
writer.write_all(&e.write_u32_bytes(kind))?; // Type
|
||||
writer.write_all(ELF_NOTE_SPLIT)?; // Name
|
||||
writer.write_all(&[0; 1])?; // Null terminator
|
||||
align_data_to_4(writer, ELF_NOTE_SPLIT.len() + 1)?;
|
||||
Ok(())
|
||||
}
|
||||
38
objdiff-core/src/util.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use std::{
|
||||
fmt::{LowerHex, UpperHex},
|
||||
io::Read,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use byteorder::{NativeEndian, ReadBytesExt};
|
||||
use num_traits::PrimInt;
|
||||
use object::{Endian, Object};
|
||||
|
||||
// https://stackoverflow.com/questions/44711012/how-do-i-format-a-signed-integer-to-a-sign-aware-hexadecimal-representation
|
||||
pub struct ReallySigned<N: PrimInt>(pub(crate) N);
|
||||
|
||||
impl<N: PrimInt> LowerHex for ReallySigned<N> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
let num = self.0.to_i64().unwrap();
|
||||
let prefix = if f.alternate() { "0x" } else { "" };
|
||||
let bare_hex = format!("{:x}", num.abs());
|
||||
f.pad_integral(num >= 0, prefix, &bare_hex)
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: PrimInt> UpperHex for ReallySigned<N> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
let num = self.0.to_i64().unwrap();
|
||||
let prefix = if f.alternate() { "0x" } else { "" };
|
||||
let bare_hex = format!("{:X}", num.abs());
|
||||
f.pad_integral(num >= 0, prefix, &bare_hex)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_u32<R: Read>(obj_file: &object::File, reader: &mut R) -> Result<u32> {
|
||||
Ok(obj_file.endianness().read_u32(reader.read_u32::<NativeEndian>()?))
|
||||
}
|
||||
|
||||
pub fn read_u16<R: Read>(obj_file: &object::File, reader: &mut R) -> Result<u16> {
|
||||
Ok(obj_file.endianness().read_u16(reader.read_u16::<NativeEndian>()?))
|
||||
}
|
||||
109
objdiff-gui/Cargo.toml
Normal file
@@ -0,0 +1,109 @@
|
||||
[package]
|
||||
name = "objdiff-gui"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
readme = "../README.md"
|
||||
description = """
|
||||
A local diffing tool for decompilation projects.
|
||||
"""
|
||||
publish = false
|
||||
build = "build.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "objdiff"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = ["glow", "wgpu", "wsl"]
|
||||
glow = ["eframe/glow"]
|
||||
wgpu = ["eframe/wgpu", "dep:wgpu"]
|
||||
wsl = []
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
bytes = "1.9"
|
||||
cfg-if = "1.0"
|
||||
const_format = "0.2"
|
||||
cwdemangle = "1.0"
|
||||
cwextab = "1.0.2"
|
||||
dirs = "5.0"
|
||||
egui = "0.29"
|
||||
egui_extras = "0.29"
|
||||
filetime = "0.2"
|
||||
float-ord = "0.3"
|
||||
font-kit = "0.14"
|
||||
globset = { version = "0.4", features = ["serde1"] }
|
||||
log = "0.4"
|
||||
notify = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2" }
|
||||
objdiff-core = { path = "../objdiff-core", features = ["all"] }
|
||||
open = "5.3"
|
||||
png = "0.17"
|
||||
pollster = "0.4"
|
||||
regex = "1.11"
|
||||
rfd = { version = "0.15" } #, default-features = false, features = ['xdg-portal']
|
||||
rlwinmdec = "1.0"
|
||||
ron = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
shell-escape = "0.1"
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
tempfile = "3.14"
|
||||
time = { version = "0.3", features = ["formatting", "local-offset"] }
|
||||
|
||||
# Keep version in sync with egui
|
||||
[dependencies.eframe]
|
||||
version = "0.29"
|
||||
features = [
|
||||
"default_fonts",
|
||||
"persistence",
|
||||
"wayland",
|
||||
"x11",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
# Keep version in sync with eframe
|
||||
[dependencies.wgpu]
|
||||
version = "22.1"
|
||||
features = [
|
||||
"dx12",
|
||||
"metal",
|
||||
"webgpu",
|
||||
]
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
# For Linux static binaries, use rustls
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"] }
|
||||
self_update = { version = "0.41", default-features = false, features = ["rustls"] }
|
||||
|
||||
# For all other platforms, use native TLS
|
||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "default-tls"] }
|
||||
self_update = "0.41"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
path-slash = "0.2"
|
||||
winapi = "0.3"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
exec = "0.3"
|
||||
|
||||
# native:
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
# web:
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
console_error_panic_hook = "0.1"
|
||||
tracing-wasm = "0.2"
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = "1.0"
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
tauri-winres = "0.1"
|
||||
BIN
objdiff-gui/assets/icon.ico
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
objdiff-gui/assets/icon.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
objdiff-gui/assets/icon_64.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
12
objdiff-gui/build.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use anyhow::Result;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let mut res = tauri_winres::WindowsResource::new();
|
||||
res.set_icon("assets/icon.ico");
|
||||
res.set_language(0x0409); // US English
|
||||
res.compile()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
898
objdiff-gui/src/app.rs
Normal file
@@ -0,0 +1,898 @@
|
||||
use std::{
|
||||
default::Default,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex, RwLock,
|
||||
},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use filetime::FileTime;
|
||||
use globset::{Glob, GlobSet};
|
||||
use notify::{RecursiveMode, Watcher};
|
||||
use objdiff_core::{
|
||||
config::{
|
||||
build_globset, save_project_config, ProjectConfig, ProjectConfigInfo, ProjectObject,
|
||||
ScratchConfig, SymbolMappings, DEFAULT_WATCH_PATTERNS,
|
||||
},
|
||||
diff::DiffObjConfig,
|
||||
};
|
||||
use time::UtcOffset;
|
||||
|
||||
use crate::{
|
||||
app_config::{deserialize_config, AppConfigVersion},
|
||||
config::{load_project_config, ProjectObjectNode},
|
||||
jobs::{
|
||||
objdiff::{start_build, ObjDiffConfig},
|
||||
Job, JobQueue, JobResult, JobStatus,
|
||||
},
|
||||
views::{
|
||||
appearance::{appearance_window, Appearance},
|
||||
config::{
|
||||
arch_config_window, config_ui, project_window, ConfigViewState, CONFIG_DISABLED_TEXT,
|
||||
},
|
||||
data_diff::data_diff_ui,
|
||||
debug::debug_window,
|
||||
demangle::{demangle_window, DemangleViewState},
|
||||
extab_diff::extab_diff_ui,
|
||||
frame_history::FrameHistory,
|
||||
function_diff::function_diff_ui,
|
||||
graphics::{graphics_window, GraphicsConfig, GraphicsViewState},
|
||||
jobs::{jobs_menu_ui, jobs_window},
|
||||
rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState},
|
||||
symbol_diff::{symbol_diff_ui, DiffViewAction, DiffViewNavigation, DiffViewState, View},
|
||||
},
|
||||
};
|
||||
|
||||
pub struct ViewState {
|
||||
pub jobs: JobQueue,
|
||||
pub config_state: ConfigViewState,
|
||||
pub demangle_state: DemangleViewState,
|
||||
pub rlwinm_decode_state: RlwinmDecodeViewState,
|
||||
pub diff_state: DiffViewState,
|
||||
pub graphics_state: GraphicsViewState,
|
||||
pub frame_history: FrameHistory,
|
||||
pub show_appearance_config: bool,
|
||||
pub show_demangle: bool,
|
||||
pub show_rlwinm_decode: bool,
|
||||
pub show_project_config: bool,
|
||||
pub show_arch_config: bool,
|
||||
pub show_debug: bool,
|
||||
pub show_graphics: bool,
|
||||
pub show_jobs: bool,
|
||||
pub show_side_panel: bool,
|
||||
}
|
||||
|
||||
impl Default for ViewState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
jobs: Default::default(),
|
||||
config_state: Default::default(),
|
||||
demangle_state: Default::default(),
|
||||
rlwinm_decode_state: Default::default(),
|
||||
diff_state: Default::default(),
|
||||
graphics_state: Default::default(),
|
||||
frame_history: Default::default(),
|
||||
show_appearance_config: false,
|
||||
show_demangle: false,
|
||||
show_rlwinm_decode: false,
|
||||
show_project_config: false,
|
||||
show_arch_config: false,
|
||||
show_debug: false,
|
||||
show_graphics: false,
|
||||
show_jobs: false,
|
||||
show_side_panel: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The configuration for a single object file.
|
||||
#[derive(Default, 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>,
|
||||
pub source_path: Option<String>,
|
||||
#[serde(default)]
|
||||
pub symbol_mappings: SymbolMappings,
|
||||
}
|
||||
|
||||
impl From<&ProjectObject> for ObjectConfig {
|
||||
fn from(object: &ProjectObject) -> Self {
|
||||
Self {
|
||||
name: object.name().to_string(),
|
||||
target_path: object.target_path.clone(),
|
||||
base_path: object.base_path.clone(),
|
||||
reverse_fn_order: object.reverse_fn_order(),
|
||||
complete: object.complete(),
|
||||
scratch: object.scratch.clone(),
|
||||
source_path: object.source_path().cloned(),
|
||||
symbol_mappings: object.symbol_mappings.clone().unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn bool_true() -> bool { true }
|
||||
|
||||
#[inline]
|
||||
fn default_watch_patterns() -> Vec<Glob> {
|
||||
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
pub config: AppConfig,
|
||||
pub objects: Vec<ProjectObject>,
|
||||
pub object_nodes: Vec<ProjectObjectNode>,
|
||||
pub watcher_change: bool,
|
||||
pub config_change: bool,
|
||||
pub obj_change: bool,
|
||||
pub queue_build: bool,
|
||||
pub queue_reload: bool,
|
||||
pub current_project_config: Option<ProjectConfig>,
|
||||
pub project_config_info: Option<ProjectConfigInfo>,
|
||||
pub last_mod_check: Instant,
|
||||
/// The right object symbol name that we're selecting a left symbol for
|
||||
pub selecting_left: Option<String>,
|
||||
/// The left object symbol name that we're selecting a right symbol for
|
||||
pub selecting_right: Option<String>,
|
||||
pub config_error: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for AppState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
config: Default::default(),
|
||||
objects: vec![],
|
||||
object_nodes: vec![],
|
||||
watcher_change: false,
|
||||
config_change: false,
|
||||
obj_change: false,
|
||||
queue_build: false,
|
||||
queue_reload: false,
|
||||
current_project_config: None,
|
||||
project_config_info: None,
|
||||
last_mod_check: Instant::now(),
|
||||
selecting_left: None,
|
||||
selecting_right: None,
|
||||
config_error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 custom_args: Option<Vec<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 diff_obj_config: DiffObjConfig,
|
||||
}
|
||||
|
||||
impl Default for AppConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
version: AppConfigVersion::default().version,
|
||||
custom_make: None,
|
||||
custom_args: None,
|
||||
selected_wsl_distro: None,
|
||||
project_dir: None,
|
||||
target_obj_dir: None,
|
||||
base_obj_dir: None,
|
||||
selected_obj: None,
|
||||
build_base: true,
|
||||
build_target: false,
|
||||
rebuild_on_changes: true,
|
||||
auto_update_check: true,
|
||||
watch_patterns: DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(),
|
||||
recent_projects: vec![],
|
||||
diff_obj_config: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn set_project_dir(&mut self, path: PathBuf) {
|
||||
self.config.recent_projects.retain(|p| p != &path);
|
||||
if self.config.recent_projects.len() > 9 {
|
||||
self.config.recent_projects.truncate(9);
|
||||
}
|
||||
self.config.recent_projects.insert(0, path.clone());
|
||||
self.config.project_dir = Some(path);
|
||||
self.config.target_obj_dir = None;
|
||||
self.config.base_obj_dir = None;
|
||||
self.config.selected_obj = None;
|
||||
self.config.build_target = false;
|
||||
self.objects.clear();
|
||||
self.object_nodes.clear();
|
||||
self.watcher_change = true;
|
||||
self.config_change = true;
|
||||
self.obj_change = true;
|
||||
self.queue_build = false;
|
||||
self.current_project_config = None;
|
||||
self.project_config_info = None;
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
}
|
||||
|
||||
pub fn set_target_obj_dir(&mut self, path: PathBuf) {
|
||||
self.config.target_obj_dir = Some(path);
|
||||
self.config.selected_obj = None;
|
||||
self.obj_change = true;
|
||||
self.queue_build = false;
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
}
|
||||
|
||||
pub fn set_base_obj_dir(&mut self, path: PathBuf) {
|
||||
self.config.base_obj_dir = Some(path);
|
||||
self.config.selected_obj = None;
|
||||
self.obj_change = true;
|
||||
self.queue_build = false;
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
}
|
||||
|
||||
pub fn set_selected_obj(&mut self, config: ObjectConfig) {
|
||||
let mut unit_changed = true;
|
||||
if let Some(existing) = self.config.selected_obj.as_ref() {
|
||||
if existing == &config {
|
||||
// Don't reload the object if there were no changes
|
||||
return;
|
||||
}
|
||||
if existing.name == config.name {
|
||||
unit_changed = false;
|
||||
}
|
||||
}
|
||||
self.config.selected_obj = Some(config);
|
||||
if unit_changed {
|
||||
self.obj_change = true;
|
||||
self.queue_build = false;
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
} else {
|
||||
self.queue_build = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_selected_obj(&mut self) {
|
||||
self.config.selected_obj = None;
|
||||
self.obj_change = true;
|
||||
self.queue_build = false;
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
}
|
||||
|
||||
pub fn set_selecting_left(&mut self, right: &str) {
|
||||
let Some(object) = self.config.selected_obj.as_mut() else {
|
||||
return;
|
||||
};
|
||||
object.symbol_mappings.remove_by_right(right);
|
||||
self.selecting_left = Some(right.to_string());
|
||||
self.queue_reload = true;
|
||||
self.save_config();
|
||||
}
|
||||
|
||||
pub fn set_selecting_right(&mut self, left: &str) {
|
||||
let Some(object) = self.config.selected_obj.as_mut() else {
|
||||
return;
|
||||
};
|
||||
object.symbol_mappings.remove_by_left(left);
|
||||
self.selecting_right = Some(left.to_string());
|
||||
self.queue_reload = true;
|
||||
self.save_config();
|
||||
}
|
||||
|
||||
pub fn set_symbol_mapping(&mut self, left: String, right: String) {
|
||||
let Some(object) = self.config.selected_obj.as_mut() else {
|
||||
log::warn!("No selected object");
|
||||
return;
|
||||
};
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
if left == right {
|
||||
object.symbol_mappings.remove_by_left(&left);
|
||||
object.symbol_mappings.remove_by_right(&right);
|
||||
} else {
|
||||
object.symbol_mappings.insert(left.clone(), right.clone());
|
||||
}
|
||||
self.queue_reload = true;
|
||||
self.save_config();
|
||||
}
|
||||
|
||||
pub fn clear_selection(&mut self) {
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
self.queue_reload = true;
|
||||
}
|
||||
|
||||
pub fn clear_mappings(&mut self) {
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
if let Some(object) = self.config.selected_obj.as_mut() {
|
||||
object.symbol_mappings.clear();
|
||||
}
|
||||
self.queue_reload = true;
|
||||
self.save_config();
|
||||
}
|
||||
|
||||
pub fn is_selecting_symbol(&self) -> bool {
|
||||
self.selecting_left.is_some() || self.selecting_right.is_some()
|
||||
}
|
||||
|
||||
pub fn save_config(&mut self) {
|
||||
let (Some(config), Some(info)) =
|
||||
(self.current_project_config.as_mut(), self.project_config_info.as_mut())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
// Update the project config with the current state
|
||||
if let Some(object) = self.config.selected_obj.as_ref() {
|
||||
if let Some(existing) = config.units.as_mut().and_then(|v| {
|
||||
v.iter_mut().find(|u| u.name.as_ref().is_some_and(|n| n == &object.name))
|
||||
}) {
|
||||
existing.symbol_mappings = if object.symbol_mappings.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(object.symbol_mappings.clone())
|
||||
};
|
||||
}
|
||||
if let Some(existing) =
|
||||
self.objects.iter_mut().find(|u| u.name.as_ref().is_some_and(|n| n == &object.name))
|
||||
{
|
||||
existing.symbol_mappings = if object.symbol_mappings.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(object.symbol_mappings.clone())
|
||||
};
|
||||
}
|
||||
}
|
||||
// Save the updated project config
|
||||
match save_project_config(config, info) {
|
||||
Ok(new_info) => *info = new_info,
|
||||
Err(e) => {
|
||||
log::error!("Failed to save project config: {e}");
|
||||
self.config_error = Some(format!("Failed to save project config: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type AppStateRef = Arc<RwLock<AppState>>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct App {
|
||||
appearance: Appearance,
|
||||
view_state: ViewState,
|
||||
state: AppStateRef,
|
||||
modified: Arc<AtomicBool>,
|
||||
watcher: Option<notify::RecommendedWatcher>,
|
||||
app_path: Option<PathBuf>,
|
||||
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
|
||||
should_relaunch: bool,
|
||||
}
|
||||
|
||||
pub const APPEARANCE_KEY: &str = "appearance";
|
||||
pub const CONFIG_KEY: &str = "app_config";
|
||||
|
||||
impl App {
|
||||
/// Called once before the first frame.
|
||||
pub fn new(
|
||||
cc: &eframe::CreationContext<'_>,
|
||||
utc_offset: UtcOffset,
|
||||
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
|
||||
app_path: Option<PathBuf>,
|
||||
graphics_config: GraphicsConfig,
|
||||
graphics_config_path: Option<PathBuf>,
|
||||
) -> Self {
|
||||
// Load previous app state (if any).
|
||||
// Note that you must enable the `persistence` feature for this to work.
|
||||
let mut app = Self::default();
|
||||
if let Some(storage) = cc.storage {
|
||||
if let Some(appearance) = eframe::get_value::<Appearance>(storage, APPEARANCE_KEY) {
|
||||
app.appearance = appearance;
|
||||
}
|
||||
if let Some(config) = deserialize_config(storage) {
|
||||
let mut state = AppState { config, ..Default::default() };
|
||||
if state.config.project_dir.is_some() {
|
||||
state.config_change = true;
|
||||
state.watcher_change = true;
|
||||
}
|
||||
if state.config.selected_obj.is_some() {
|
||||
state.queue_build = true;
|
||||
}
|
||||
app.view_state.config_state.queue_check_update = state.config.auto_update_check;
|
||||
app.state = Arc::new(RwLock::new(state));
|
||||
}
|
||||
}
|
||||
app.appearance.init_fonts(&cc.egui_ctx);
|
||||
app.appearance.utc_offset = utc_offset;
|
||||
app.app_path = app_path;
|
||||
app.relaunch_path = relaunch_path;
|
||||
#[cfg(feature = "wgpu")]
|
||||
if let Some(wgpu_render_state) = &cc.wgpu_render_state {
|
||||
use eframe::egui_wgpu::wgpu::Backend;
|
||||
let info = wgpu_render_state.adapter.get_info();
|
||||
app.view_state.graphics_state.active_backend = match info.backend {
|
||||
Backend::Empty => "Unknown",
|
||||
Backend::Vulkan => "Vulkan",
|
||||
Backend::Metal => "Metal",
|
||||
Backend::Dx12 => "DirectX 12",
|
||||
Backend::Gl => "OpenGL",
|
||||
Backend::BrowserWebGpu => "WebGPU",
|
||||
}
|
||||
.to_string();
|
||||
app.view_state.graphics_state.active_device.clone_from(&info.name);
|
||||
}
|
||||
#[cfg(feature = "glow")]
|
||||
if let Some(gl) = &cc.gl {
|
||||
use eframe::glow::HasContext;
|
||||
app.view_state.graphics_state.active_backend = "OpenGL (Fallback)".to_string();
|
||||
app.view_state.graphics_state.active_device =
|
||||
unsafe { gl.get_parameter_string(0x1F01) }; // GL_RENDERER
|
||||
}
|
||||
app.view_state.graphics_state.graphics_config = graphics_config;
|
||||
app.view_state.graphics_state.graphics_config_path = graphics_config_path;
|
||||
app
|
||||
}
|
||||
|
||||
fn pre_update(&mut self, ctx: &egui::Context) {
|
||||
self.appearance.pre_update(ctx);
|
||||
|
||||
let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state;
|
||||
|
||||
let mut results = vec![];
|
||||
for (job, result) in jobs.iter_finished() {
|
||||
match result {
|
||||
Ok(result) => {
|
||||
log::info!("Job {} finished", job.id);
|
||||
match result {
|
||||
JobResult::None => {
|
||||
if let Some(err) = &job.context.status.read().unwrap().error {
|
||||
log::error!("{:?}", err);
|
||||
}
|
||||
}
|
||||
JobResult::Update(state) => {
|
||||
if let Ok(mut guard) = self.relaunch_path.lock() {
|
||||
*guard = Some(state.exe_path);
|
||||
self.should_relaunch = true;
|
||||
}
|
||||
}
|
||||
_ => results.push(result),
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let err = if let Some(msg) = err.downcast_ref::<&'static str>() {
|
||||
anyhow::Error::msg(*msg)
|
||||
} else if let Some(msg) = err.downcast_ref::<String>() {
|
||||
anyhow::Error::msg(msg.clone())
|
||||
} else {
|
||||
anyhow::Error::msg("Thread panicked")
|
||||
};
|
||||
let result = job.context.status.write();
|
||||
if let Ok(mut guard) = result {
|
||||
guard.error = Some(err);
|
||||
} else {
|
||||
drop(result);
|
||||
job.context.status = Arc::new(RwLock::new(JobStatus {
|
||||
title: "Error".to_string(),
|
||||
progress_percent: 0.0,
|
||||
progress_items: None,
|
||||
status: String::new(),
|
||||
error: Some(err),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
jobs.results.append(&mut results);
|
||||
jobs.clear_finished();
|
||||
|
||||
diff_state.pre_update(jobs, &self.state);
|
||||
config_state.pre_update(jobs, &self.state);
|
||||
debug_assert!(jobs.results.is_empty());
|
||||
}
|
||||
|
||||
fn post_update(&mut self, ctx: &egui::Context, action: Option<DiffViewAction>) {
|
||||
self.appearance.post_update(ctx);
|
||||
|
||||
let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state;
|
||||
config_state.post_update(ctx, jobs, &self.state);
|
||||
diff_state.post_update(action, ctx, jobs, &self.state);
|
||||
|
||||
let Ok(mut state) = self.state.write() else {
|
||||
return;
|
||||
};
|
||||
let state = &mut *state;
|
||||
|
||||
let mut mod_check = false;
|
||||
if state.last_mod_check.elapsed().as_millis() >= 500 {
|
||||
state.last_mod_check = Instant::now();
|
||||
mod_check = true;
|
||||
}
|
||||
|
||||
if mod_check {
|
||||
if let Some(info) = &state.project_config_info {
|
||||
if let Some(last_ts) = info.timestamp {
|
||||
if file_modified(&info.path, last_ts) {
|
||||
state.config_change = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if state.config_change {
|
||||
state.config_change = false;
|
||||
match load_project_config(state) {
|
||||
Ok(()) => state.config_error = None,
|
||||
Err(e) => {
|
||||
log::error!("Failed to load project config: {e}");
|
||||
state.config_error = Some(format!("{e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if state.watcher_change {
|
||||
drop(self.watcher.take());
|
||||
|
||||
if let Some(project_dir) = &state.config.project_dir {
|
||||
match build_globset(&state.config.watch_patterns)
|
||||
.map_err(anyhow::Error::new)
|
||||
.and_then(|globset| {
|
||||
create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset)
|
||||
.map_err(anyhow::Error::new)
|
||||
}) {
|
||||
Ok(watcher) => self.watcher = Some(watcher),
|
||||
Err(e) => log::error!("Failed to create watcher: {e}"),
|
||||
}
|
||||
state.watcher_change = false;
|
||||
}
|
||||
}
|
||||
|
||||
if state.obj_change {
|
||||
*diff_state = Default::default();
|
||||
if state.config.selected_obj.is_some() {
|
||||
state.queue_build = true;
|
||||
}
|
||||
state.obj_change = false;
|
||||
}
|
||||
|
||||
if self.modified.swap(false, Ordering::Relaxed) && state.config.rebuild_on_changes {
|
||||
state.queue_build = true;
|
||||
}
|
||||
|
||||
if let Some(result) = &diff_state.build {
|
||||
if mod_check {
|
||||
if let Some((obj, _)) = &result.first_obj {
|
||||
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
|
||||
if file_modified(path, timestamp) {
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((obj, _)) = &result.second_obj {
|
||||
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
|
||||
if file_modified(path, timestamp) {
|
||||
state.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 state.queue_build
|
||||
&& state.config.selected_obj.is_some()
|
||||
&& !jobs.is_running(Job::ObjDiff)
|
||||
{
|
||||
jobs.push(start_build(ctx, ObjDiffConfig::from_state(state)));
|
||||
state.queue_build = false;
|
||||
state.queue_reload = false;
|
||||
} else if state.queue_reload && !jobs.is_running(Job::ObjDiff) {
|
||||
let mut diff_config = ObjDiffConfig::from_state(state);
|
||||
// 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));
|
||||
state.queue_reload = false;
|
||||
}
|
||||
|
||||
if graphics_state.should_relaunch {
|
||||
if let Some(app_path) = &self.app_path {
|
||||
if let Ok(mut guard) = self.relaunch_path.lock() {
|
||||
*guard = Some(app_path.clone());
|
||||
self.should_relaunch = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 { state, appearance, view_state, .. } = self;
|
||||
let ViewState {
|
||||
jobs,
|
||||
config_state,
|
||||
demangle_state,
|
||||
rlwinm_decode_state,
|
||||
diff_state,
|
||||
graphics_state,
|
||||
frame_history,
|
||||
show_appearance_config,
|
||||
show_demangle,
|
||||
show_rlwinm_decode,
|
||||
show_project_config,
|
||||
show_arch_config,
|
||||
show_debug,
|
||||
show_graphics,
|
||||
show_jobs,
|
||||
show_side_panel,
|
||||
} = view_state;
|
||||
|
||||
frame_history.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
|
||||
|
||||
let side_panel_available = diff_state.current_view == View::SymbolDiff;
|
||||
|
||||
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
if ui
|
||||
.add_enabled(
|
||||
side_panel_available,
|
||||
egui::Button::new(if *show_side_panel { "⏴" } else { "⏵" }),
|
||||
)
|
||||
.on_hover_text("Toggle side panel")
|
||||
.clicked()
|
||||
{
|
||||
*show_side_panel = !*show_side_panel;
|
||||
}
|
||||
ui.separator();
|
||||
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) = state.read() {
|
||||
guard.config.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() {
|
||||
state.write().unwrap().config.recent_projects.clear();
|
||||
};
|
||||
ui.separator();
|
||||
for path in recent_projects {
|
||||
if ui.button(format!("{}", path.display())).clicked() {
|
||||
state.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("Graphics…").clicked() {
|
||||
*show_graphics = !*show_graphics;
|
||||
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();
|
||||
}
|
||||
if ui.button("Rlwinm Decoder…").clicked() {
|
||||
*show_rlwinm_decode = !*show_rlwinm_decode;
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
ui.menu_button("Diff Options", |ui| {
|
||||
if ui.button("Arch Settings…").clicked() {
|
||||
*show_arch_config = !*show_arch_config;
|
||||
ui.close_menu();
|
||||
}
|
||||
let mut state = state.write().unwrap();
|
||||
let response = ui
|
||||
.checkbox(&mut state.config.rebuild_on_changes, "Rebuild on changes")
|
||||
.on_hover_text("Automatically re-run the build & diff when files change.");
|
||||
if response.changed() {
|
||||
state.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 state.config.diff_obj_config.relax_reloc_diffs,
|
||||
"Relax relocation diffs",
|
||||
)
|
||||
.on_hover_text(
|
||||
"Ignores differences in relocation targets. (Address, name, etc)",
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
state.queue_reload = true;
|
||||
}
|
||||
if ui
|
||||
.checkbox(
|
||||
&mut state.config.diff_obj_config.space_between_args,
|
||||
"Space between args",
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
state.queue_reload = true;
|
||||
}
|
||||
if ui
|
||||
.checkbox(
|
||||
&mut state.config.diff_obj_config.combine_data_sections,
|
||||
"Combine data sections",
|
||||
)
|
||||
.on_hover_text("Combines data sections with equal names.")
|
||||
.changed()
|
||||
{
|
||||
state.queue_reload = true;
|
||||
}
|
||||
if ui.button("Clear custom symbol mappings").clicked() {
|
||||
state.clear_mappings();
|
||||
diff_state.post_build_nav = Some(DiffViewNavigation::symbol_diff());
|
||||
state.queue_reload = true;
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
if jobs_menu_ui(ui, jobs, appearance) {
|
||||
*show_jobs = !*show_jobs;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if side_panel_available {
|
||||
egui::SidePanel::left("side_panel").show_animated(ctx, *show_side_panel, |ui| {
|
||||
egui::ScrollArea::both().show(ui, |ui| {
|
||||
config_ui(ui, state, show_project_config, config_state, appearance);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let mut action = None;
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
let build_success = matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success);
|
||||
action = if diff_state.current_view == View::FunctionDiff && build_success {
|
||||
function_diff_ui(ui, diff_state, appearance)
|
||||
} else if diff_state.current_view == View::DataDiff && build_success {
|
||||
data_diff_ui(ui, diff_state, appearance)
|
||||
} else if diff_state.current_view == View::ExtabDiff && build_success {
|
||||
extab_diff_ui(ui, diff_state, appearance)
|
||||
} else {
|
||||
symbol_diff_ui(ui, diff_state, appearance)
|
||||
};
|
||||
});
|
||||
|
||||
project_window(ctx, state, show_project_config, config_state, appearance);
|
||||
appearance_window(ctx, show_appearance_config, appearance);
|
||||
demangle_window(ctx, show_demangle, demangle_state, appearance);
|
||||
rlwinm_decode_window(ctx, show_rlwinm_decode, rlwinm_decode_state, appearance);
|
||||
arch_config_window(ctx, state, show_arch_config, appearance);
|
||||
debug_window(ctx, show_debug, frame_history, appearance);
|
||||
graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance);
|
||||
jobs_window(ctx, show_jobs, jobs, appearance);
|
||||
|
||||
self.post_update(ctx, action);
|
||||
}
|
||||
|
||||
/// Called by the framework to save state before shutdown.
|
||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||
if let Ok(state) = self.state.read() {
|
||||
eframe::set_value(storage, CONFIG_KEY, &state.config);
|
||||
}
|
||||
eframe::set_value(storage, APPEARANCE_KEY, &self.appearance);
|
||||
}
|
||||
}
|
||||
|
||||
fn create_watcher(
|
||||
ctx: egui::Context,
|
||||
modified: Arc<AtomicBool>,
|
||||
project_dir: &Path,
|
||||
patterns: GlobSet,
|
||||
) -> notify::Result<notify::RecommendedWatcher> {
|
||||
let base_dir = project_dir.to_owned();
|
||||
let mut watcher =
|
||||
notify::recommended_watcher(move |res: notify::Result<notify::Event>| match res {
|
||||
Ok(event) => {
|
||||
if matches!(
|
||||
event.kind,
|
||||
notify::EventKind::Modify(..)
|
||||
| notify::EventKind::Create(..)
|
||||
| notify::EventKind::Remove(..)
|
||||
) {
|
||||
for path in &event.paths {
|
||||
let Ok(path) = path.strip_prefix(&base_dir) else {
|
||||
continue;
|
||||
};
|
||||
if patterns.is_match(path) {
|
||||
log::info!("File modified: {}", path.display());
|
||||
modified.store(true, Ordering::Relaxed);
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => log::error!("watch error: {e:?}"),
|
||||
})?;
|
||||
watcher.watch(project_dir, RecursiveMode::Recursive)?;
|
||||
Ok(watcher)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn file_modified(path: &Path, last_ts: FileTime) -> bool {
|
||||
if let Ok(metadata) = fs::metadata(path) {
|
||||
FileTime::from_last_modification_time(&metadata) != last_ts
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
276
objdiff-gui/src/app_config.rs
Normal file
@@ -0,0 +1,276 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use eframe::Storage;
|
||||
use globset::Glob;
|
||||
use objdiff_core::{
|
||||
config::ScratchConfig,
|
||||
diff::{ArmArchVersion, ArmR9Usage, DiffObjConfig, MipsAbi, MipsInstrCategory, X86Formatter},
|
||||
};
|
||||
|
||||
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: 2 } }
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
2 => from_str::<AppConfig>(&str),
|
||||
1 => from_str::<AppConfigV1>(&str).map(|c| c.into_config()),
|
||||
_ => {
|
||||
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 ScratchConfigV1 {
|
||||
#[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 ScratchConfigV1 {
|
||||
fn into_config(self) -> ScratchConfig {
|
||||
ScratchConfig {
|
||||
platform: self.platform,
|
||||
compiler: self.compiler,
|
||||
c_flags: self.c_flags,
|
||||
ctx_path: self.ctx_path,
|
||||
build_ctx: self.build_ctx.then_some(true),
|
||||
preset_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct ObjectConfigV1 {
|
||||
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<ScratchConfigV1>,
|
||||
pub source_path: Option<String>,
|
||||
}
|
||||
|
||||
impl ObjectConfigV1 {
|
||||
fn into_config(self) -> ObjectConfig {
|
||||
ObjectConfig {
|
||||
name: self.name,
|
||||
target_path: self.target_path,
|
||||
base_path: self.base_path,
|
||||
reverse_fn_order: self.reverse_fn_order,
|
||||
complete: self.complete,
|
||||
scratch: self.scratch.map(|scratch| scratch.into_config()),
|
||||
source_path: self.source_path,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct DiffObjConfigV1 {
|
||||
pub relax_reloc_diffs: bool,
|
||||
#[serde(default = "bool_true")]
|
||||
pub space_between_args: bool,
|
||||
pub combine_data_sections: bool,
|
||||
// x86
|
||||
pub x86_formatter: X86Formatter,
|
||||
// MIPS
|
||||
pub mips_abi: MipsAbi,
|
||||
pub mips_instr_category: MipsInstrCategory,
|
||||
// ARM
|
||||
pub arm_arch_version: ArmArchVersion,
|
||||
pub arm_unified_syntax: bool,
|
||||
pub arm_av_registers: bool,
|
||||
pub arm_r9_usage: ArmR9Usage,
|
||||
pub arm_sl_usage: bool,
|
||||
pub arm_fp_usage: bool,
|
||||
pub arm_ip_usage: bool,
|
||||
}
|
||||
|
||||
impl Default for DiffObjConfigV1 {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
relax_reloc_diffs: false,
|
||||
space_between_args: true,
|
||||
combine_data_sections: false,
|
||||
x86_formatter: Default::default(),
|
||||
mips_abi: Default::default(),
|
||||
mips_instr_category: Default::default(),
|
||||
arm_arch_version: Default::default(),
|
||||
arm_unified_syntax: true,
|
||||
arm_av_registers: false,
|
||||
arm_r9_usage: Default::default(),
|
||||
arm_sl_usage: false,
|
||||
arm_fp_usage: false,
|
||||
arm_ip_usage: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DiffObjConfigV1 {
|
||||
fn into_config(self) -> DiffObjConfig {
|
||||
DiffObjConfig {
|
||||
relax_reloc_diffs: self.relax_reloc_diffs,
|
||||
space_between_args: self.space_between_args,
|
||||
combine_data_sections: self.combine_data_sections,
|
||||
x86_formatter: self.x86_formatter,
|
||||
mips_abi: self.mips_abi,
|
||||
mips_instr_category: self.mips_instr_category,
|
||||
arm_arch_version: self.arm_arch_version,
|
||||
arm_unified_syntax: self.arm_unified_syntax,
|
||||
arm_av_registers: self.arm_av_registers,
|
||||
arm_r9_usage: self.arm_r9_usage,
|
||||
arm_sl_usage: self.arm_sl_usage,
|
||||
arm_fp_usage: self.arm_fp_usage,
|
||||
arm_ip_usage: self.arm_ip_usage,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn bool_true() -> bool { true }
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct AppConfigV1 {
|
||||
pub version: u32,
|
||||
#[serde(default)]
|
||||
pub custom_make: Option<String>,
|
||||
#[serde(default)]
|
||||
pub custom_args: Option<Vec<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<ObjectConfigV1>,
|
||||
#[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)]
|
||||
pub watch_patterns: Vec<Glob>,
|
||||
#[serde(default)]
|
||||
pub recent_projects: Vec<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub diff_obj_config: DiffObjConfigV1,
|
||||
}
|
||||
|
||||
impl AppConfigV1 {
|
||||
fn into_config(self) -> AppConfig {
|
||||
log::info!("Upgrading configuration from v1");
|
||||
AppConfig {
|
||||
custom_make: self.custom_make,
|
||||
custom_args: self.custom_args,
|
||||
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_base: self.build_base,
|
||||
build_target: self.build_target,
|
||||
rebuild_on_changes: self.rebuild_on_changes,
|
||||
auto_update_check: self.auto_update_check,
|
||||
watch_patterns: self.watch_patterns,
|
||||
recent_projects: self.recent_projects,
|
||||
diff_obj_config: self.diff_obj_config.into_config(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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()
|
||||
}
|
||||
}
|
||||
}
|
||||
127
objdiff-gui/src/config.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use std::path::{Component, Path};
|
||||
|
||||
use anyhow::Result;
|
||||
use globset::Glob;
|
||||
use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS};
|
||||
|
||||
use crate::app::{AppState, ObjectConfig};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ProjectObjectNode {
|
||||
Unit(String, usize),
|
||||
Dir(String, Vec<ProjectObjectNode>),
|
||||
}
|
||||
|
||||
fn join_single_dir_entries(nodes: &mut Vec<ProjectObjectNode>) {
|
||||
for node in nodes {
|
||||
if let ProjectObjectNode::Dir(my_name, my_nodes) = node {
|
||||
join_single_dir_entries(my_nodes);
|
||||
// If this directory consists of a single sub-directory...
|
||||
if let [ProjectObjectNode::Dir(sub_name, sub_nodes)] = &mut my_nodes[..] {
|
||||
// ... join the two names with a path separator and eliminate the layer
|
||||
*my_name += "/";
|
||||
*my_name += sub_name;
|
||||
*my_nodes = std::mem::take(sub_nodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
units: &mut [ProjectObject],
|
||||
project_dir: &Path,
|
||||
target_obj_dir: Option<&Path>,
|
||||
base_obj_dir: Option<&Path>,
|
||||
) -> Vec<ProjectObjectNode> {
|
||||
let mut nodes = vec![];
|
||||
for (idx, unit) in units.iter_mut().enumerate() {
|
||||
unit.resolve_paths(project_dir, target_obj_dir, base_obj_dir);
|
||||
let mut out_nodes = &mut nodes;
|
||||
let path = if let Some(name) = &unit.name {
|
||||
Path::new(name)
|
||||
} else if let Some(path) = &unit.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 filename = path.file_name().unwrap().to_str().unwrap().to_string();
|
||||
out_nodes.push(ProjectObjectNode::Unit(filename, idx));
|
||||
}
|
||||
// Within the top-level module directories, join paths. Leave the
|
||||
// top-level name intact though since it's the module name.
|
||||
for node in &mut nodes {
|
||||
if let ProjectObjectNode::Dir(_, sub_nodes) = node {
|
||||
join_single_dir_entries(sub_nodes);
|
||||
}
|
||||
}
|
||||
|
||||
nodes
|
||||
}
|
||||
|
||||
pub fn load_project_config(state: &mut AppState) -> Result<()> {
|
||||
let Some(project_dir) = &state.config.project_dir else {
|
||||
return Ok(());
|
||||
};
|
||||
if let Some((result, info)) = try_project_config(project_dir) {
|
||||
let project_config = result?;
|
||||
state.config.custom_make = project_config.custom_make.clone();
|
||||
state.config.custom_args = project_config.custom_args.clone();
|
||||
state.config.target_obj_dir =
|
||||
project_config.target_dir.as_deref().map(|p| project_dir.join(p));
|
||||
state.config.base_obj_dir = project_config.base_dir.as_deref().map(|p| project_dir.join(p));
|
||||
state.config.build_base = project_config.build_base.unwrap_or(true);
|
||||
state.config.build_target = project_config.build_target.unwrap_or(false);
|
||||
state.config.watch_patterns = project_config.watch_patterns.clone().unwrap_or_else(|| {
|
||||
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
|
||||
});
|
||||
state.watcher_change = true;
|
||||
state.objects = project_config.units.clone().unwrap_or_default();
|
||||
state.object_nodes = build_nodes(
|
||||
&mut state.objects,
|
||||
project_dir,
|
||||
state.config.target_obj_dir.as_deref(),
|
||||
state.config.base_obj_dir.as_deref(),
|
||||
);
|
||||
state.current_project_config = Some(project_config);
|
||||
state.project_config_info = Some(info);
|
||||
|
||||
// Reload selected object
|
||||
if let Some(selected_obj) = &state.config.selected_obj {
|
||||
if let Some(obj) = state.objects.iter().find(|o| o.name() == selected_obj.name) {
|
||||
let config = ObjectConfig::from(obj);
|
||||
state.set_selected_obj(config);
|
||||
} else {
|
||||
state.clear_selected_obj();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
146
objdiff-gui/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)
|
||||
}
|
||||
107
objdiff-gui/src/fonts/mod.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
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(())
|
||||
}
|
||||
39
objdiff-gui/src/jobs/check_update.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use std::sync::mpsc::Receiver;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use self_update::{cargo_crate_version, update::Release};
|
||||
|
||||
use crate::{
|
||||
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
|
||||
update::{build_updater, BIN_NAME_NEW, BIN_NAME_OLD},
|
||||
};
|
||||
|
||||
pub struct CheckUpdateResult {
|
||||
pub update_available: bool,
|
||||
pub latest_release: Release,
|
||||
pub found_binary: Option<String>,
|
||||
}
|
||||
|
||||
fn run_check_update(context: &JobContext, cancel: Receiver<()>) -> Result<Box<CheckUpdateResult>> {
|
||||
update_status(context, "Fetching latest release".to_string(), 0, 1, &cancel)?;
|
||||
let updater = build_updater().context("Failed to create release updater")?;
|
||||
let latest_release = updater.get_latest_release()?;
|
||||
let update_available =
|
||||
self_update::version::bump_is_greater(cargo_crate_version!(), &latest_release.version)?;
|
||||
// Find the binary name in the release assets
|
||||
let found_binary = latest_release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|a| a.name == BIN_NAME_NEW)
|
||||
.or_else(|| latest_release.assets.iter().find(|a| a.name == BIN_NAME_OLD))
|
||||
.map(|a| a.name.clone());
|
||||
|
||||
update_status(context, "Complete".to_string(), 1, 1, &cancel)?;
|
||||
Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary }))
|
||||
}
|
||||
|
||||
pub fn start_check_update(ctx: &egui::Context) -> JobState {
|
||||
start_job(ctx, "Check for updates", Job::CheckUpdate, move |context, cancel| {
|
||||
run_check_update(&context, cancel).map(|result| JobResult::CheckUpdate(Some(result)))
|
||||
})
|
||||
}
|
||||
139
objdiff-gui/src/jobs/create_scratch.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
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,
|
||||
pub preset_id: Option<u32>,
|
||||
}
|
||||
|
||||
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.unwrap_or(false),
|
||||
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(),
|
||||
preset_id: scratch_config.preset_id,
|
||||
})
|
||||
}
|
||||
|
||||
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 = "https://decomp.me";
|
||||
|
||||
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 mut 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");
|
||||
if let Some(preset) = config.preset_id {
|
||||
form = form.text("preset", preset.to_string());
|
||||
}
|
||||
form = form.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!("{API_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)))
|
||||
})
|
||||
}
|
||||
189
objdiff-gui/src/jobs/mod.rs
Normal file
@@ -0,0 +1,189 @@
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
mpsc::{Receiver, Sender, TryRecvError},
|
||||
Arc, RwLock,
|
||||
},
|
||||
thread::JoinHandle,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::jobs::{
|
||||
check_update::CheckUpdateResult, create_scratch::CreateScratchResult, objdiff::ObjDiffResult,
|
||||
update::UpdateResult,
|
||||
};
|
||||
|
||||
pub mod check_update;
|
||||
pub mod create_scratch;
|
||||
pub mod objdiff;
|
||||
pub mod update;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
pub enum Job {
|
||||
ObjDiff,
|
||||
CheckUpdate,
|
||||
Update,
|
||||
CreateScratch,
|
||||
}
|
||||
pub static JOB_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct JobQueue {
|
||||
pub jobs: Vec<JobState>,
|
||||
pub results: Vec<JobResult>,
|
||||
}
|
||||
|
||||
impl JobQueue {
|
||||
/// Adds a job to the queue.
|
||||
#[inline]
|
||||
pub fn push(&mut self, state: JobState) { self.jobs.push(state); }
|
||||
|
||||
/// Adds a job to the queue if a job of the given kind is not already running.
|
||||
#[inline]
|
||||
pub fn push_once(&mut self, job: Job, func: impl FnOnce() -> JobState) {
|
||||
if !self.is_running(job) {
|
||||
self.push(func());
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether a job of the given kind is running.
|
||||
pub fn is_running(&self, kind: Job) -> bool {
|
||||
self.jobs.iter().any(|j| j.kind == kind && j.handle.is_some())
|
||||
}
|
||||
|
||||
/// Returns whether any job is running.
|
||||
#[expect(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.handle.is_none() && job.context.status.read().unwrap().error.is_none())
|
||||
});
|
||||
}
|
||||
|
||||
/// Clears all errored jobs.
|
||||
pub fn clear_errored(&mut self) {
|
||||
self.jobs.retain(|job| job.context.status.read().unwrap().error.is_none());
|
||||
}
|
||||
|
||||
/// Removes a job from the queue given its ID.
|
||||
pub fn remove(&mut self, id: usize) { self.jobs.retain(|job| job.id != id); }
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JobContext {
|
||||
pub status: Arc<RwLock<JobStatus>>,
|
||||
pub egui: egui::Context,
|
||||
}
|
||||
|
||||
pub struct JobState {
|
||||
pub id: usize,
|
||||
pub kind: Job,
|
||||
pub handle: Option<JoinHandle<JobResult>>,
|
||||
pub context: JobContext,
|
||||
pub cancel: Sender<()>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct JobStatus {
|
||||
pub title: String,
|
||||
pub progress_percent: f32,
|
||||
pub progress_items: Option<[u32; 2]>,
|
||||
pub status: String,
|
||||
pub error: Option<anyhow::Error>,
|
||||
}
|
||||
|
||||
pub enum JobResult {
|
||||
None,
|
||||
ObjDiff(Option<Box<ObjDiffResult>>),
|
||||
CheckUpdate(Option<Box<CheckUpdateResult>>),
|
||||
Update(Box<UpdateResult>),
|
||||
CreateScratch(Option<Box<CreateScratchResult>>),
|
||||
}
|
||||
|
||||
fn should_cancel(rx: &Receiver<()>) -> bool {
|
||||
match rx.try_recv() {
|
||||
Ok(_) | Err(TryRecvError::Disconnected) => true,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn start_job(
|
||||
ctx: &egui::Context,
|
||||
title: &str,
|
||||
kind: Job,
|
||||
run: impl FnOnce(JobContext, Receiver<()>) -> Result<JobResult> + Send + 'static,
|
||||
) -> JobState {
|
||||
let status = Arc::new(RwLock::new(JobStatus {
|
||||
title: title.to_string(),
|
||||
progress_percent: 0.0,
|
||||
progress_items: None,
|
||||
status: String::new(),
|
||||
error: None,
|
||||
}));
|
||||
let context = JobContext { status: status.clone(), egui: ctx.clone() };
|
||||
let context_inner = JobContext { status: status.clone(), egui: ctx.clone() };
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let handle = std::thread::spawn(move || match run(context_inner, rx) {
|
||||
Ok(state) => state,
|
||||
Err(e) => {
|
||||
if let Ok(mut w) = status.write() {
|
||||
w.error = Some(e);
|
||||
}
|
||||
JobResult::None
|
||||
}
|
||||
});
|
||||
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
|
||||
log::info!("Started job {}", id);
|
||||
JobState { id, kind, handle: Some(handle), context, cancel: tx }
|
||||
}
|
||||
|
||||
fn update_status(
|
||||
context: &JobContext,
|
||||
str: String,
|
||||
count: u32,
|
||||
total: u32,
|
||||
cancel: &Receiver<()>,
|
||||
) -> Result<()> {
|
||||
let mut w =
|
||||
context.status.write().map_err(|_| anyhow::Error::msg("Failed to lock job status"))?;
|
||||
w.progress_items = Some([count, total]);
|
||||
w.progress_percent = count as f32 / total as f32;
|
||||
if should_cancel(cancel) {
|
||||
w.status = "Cancelled".to_string();
|
||||
return Err(anyhow::Error::msg("Cancelled"));
|
||||
} else {
|
||||
w.status = str;
|
||||
}
|
||||
drop(w);
|
||||
context.egui.request_repaint();
|
||||
Ok(())
|
||||
}
|
||||
328
objdiff-gui/src/jobs/objdiff.rs
Normal file
@@ -0,0 +1,328 @@
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
sync::mpsc::Receiver,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Error, Result};
|
||||
use objdiff_core::{
|
||||
diff::{diff_objs, DiffObjConfig, MappingConfig, ObjDiff},
|
||||
obj::{read, ObjInfo},
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::{
|
||||
app::{AppConfig, AppState, ObjectConfig},
|
||||
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
|
||||
};
|
||||
|
||||
pub struct BuildStatus {
|
||||
pub success: bool,
|
||||
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 custom_args: Option<Vec<String>>,
|
||||
#[allow(unused)]
|
||||
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(),
|
||||
custom_args: config.custom_args.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 diff_obj_config: DiffObjConfig,
|
||||
pub selecting_left: Option<String>,
|
||||
pub selecting_right: Option<String>,
|
||||
}
|
||||
|
||||
impl ObjDiffConfig {
|
||||
pub(crate) fn from_state(state: &AppState) -> Self {
|
||||
Self {
|
||||
build_config: BuildConfig::from_config(&state.config),
|
||||
build_base: state.config.build_base,
|
||||
build_target: state.config.build_target,
|
||||
selected_obj: state.config.selected_obj.clone(),
|
||||
diff_obj_config: state.config.diff_obj_config.clone(),
|
||||
selecting_left: state.selecting_left.clone(),
|
||||
selecting_right: state.selecting_right.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ObjDiffResult {
|
||||
pub first_status: BuildStatus,
|
||||
pub second_status: BuildStatus,
|
||||
pub first_obj: Option<(ObjInfo, ObjDiff)>,
|
||||
pub second_obj: Option<(ObjInfo, ObjDiff)>,
|
||||
pub time: OffsetDateTime,
|
||||
}
|
||||
|
||||
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()
|
||||
};
|
||||
};
|
||||
let make = config.custom_make.as_deref().unwrap_or("make");
|
||||
let make_args = config.custom_args.as_deref().unwrap_or(&[]);
|
||||
#[cfg(not(windows))]
|
||||
let mut command = {
|
||||
let mut command = Command::new(make);
|
||||
command.current_dir(cwd).args(make_args).arg(arg);
|
||||
command
|
||||
};
|
||||
#[cfg(windows)]
|
||||
let mut command = {
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
use path_slash::PathExt;
|
||||
let mut command = if config.selected_wsl_distro.is_some() {
|
||||
Command::new("wsl")
|
||||
} else {
|
||||
Command::new(make)
|
||||
};
|
||||
if let Some(distro) = &config.selected_wsl_distro {
|
||||
// Strip distro root prefix \\wsl.localhost\{distro}
|
||||
let wsl_path_prefix = format!("\\\\wsl.localhost\\{}", distro);
|
||||
let cwd = match cwd.strip_prefix(wsl_path_prefix) {
|
||||
Ok(new_cwd) => format!("/{}", new_cwd.to_slash_lossy().as_ref()),
|
||||
Err(_) => cwd.to_string_lossy().to_string(),
|
||||
};
|
||||
|
||||
command
|
||||
.arg("--cd")
|
||||
.arg(cwd)
|
||||
.arg("-d")
|
||||
.arg(distro)
|
||||
.arg("--")
|
||||
.arg(make)
|
||||
.args(make_args)
|
||||
.arg(arg.to_slash_lossy().as_ref());
|
||||
} else {
|
||||
command.current_dir(cwd).args(make_args).arg(arg.to_slash_lossy().as_ref());
|
||||
}
|
||||
command.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
|
||||
command
|
||||
};
|
||||
let mut cmdline = shell_escape::escape(command.get_program().to_string_lossy()).into_owned();
|
||||
for arg in command.get_args() {
|
||||
cmdline.push(' ');
|
||||
cmdline.push_str(shell_escape::escape(arg.to_string_lossy()).as_ref());
|
||||
}
|
||||
let output = match command.output() {
|
||||
Ok(output) => output,
|
||||
Err(e) => {
|
||||
return BuildStatus {
|
||||
success: false,
|
||||
cmdline,
|
||||
stdout: Default::default(),
|
||||
stderr: e.to_string(),
|
||||
};
|
||||
}
|
||||
};
|
||||
// Try from_utf8 first to avoid copying the buffer if it's valid, then fall back to from_utf8_lossy
|
||||
let stdout = String::from_utf8(output.stdout)
|
||||
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
|
||||
let stderr = String::from_utf8(output.stderr)
|
||||
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
|
||||
BuildStatus { success: output.status.success(), cmdline, stdout, stderr }
|
||||
}
|
||||
|
||||
fn run_build(
|
||||
context: &JobContext,
|
||||
cancel: Receiver<()>,
|
||||
mut config: ObjDiffConfig,
|
||||
) -> Result<Box<ObjDiffResult>> {
|
||||
let obj_config = config.selected_obj.ok_or_else(|| Error::msg("Missing obj path"))?;
|
||||
// Use the per-object symbol mappings, we don't set mappings globally
|
||||
config.diff_obj_config.symbol_mappings = MappingConfig {
|
||||
mappings: obj_config.symbol_mappings,
|
||||
selecting_left: config.selecting_left,
|
||||
selecting_right: config.selecting_right,
|
||||
};
|
||||
|
||||
let project_dir = config
|
||||
.build_config
|
||||
.project_dir
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::msg("Missing project dir"))?;
|
||||
let target_path_rel = if let Some(target_path) = &obj_config.target_path {
|
||||
Some(target_path.strip_prefix(project_dir).map_err(|_| {
|
||||
anyhow!(
|
||||
"Target path '{}' doesn't begin with '{}'",
|
||||
target_path.display(),
|
||||
project_dir.display()
|
||||
)
|
||||
})?)
|
||||
} else {
|
||||
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
|
||||
};
|
||||
|
||||
let mut total = 1;
|
||||
if config.build_target && target_path_rel.is_some() {
|
||||
total += 1;
|
||||
}
|
||||
if config.build_base && base_path_rel.is_some() {
|
||||
total += 1;
|
||||
}
|
||||
if target_path_rel.is_some() {
|
||||
total += 1;
|
||||
}
|
||||
if base_path_rel.is_some() {
|
||||
total += 1;
|
||||
}
|
||||
|
||||
let mut step_idx = 0;
|
||||
let mut first_status = match target_path_rel {
|
||||
Some(target_path_rel) if config.build_target => {
|
||||
update_status(
|
||||
context,
|
||||
format!("Building target {}", target_path_rel.display()),
|
||||
step_idx,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
step_idx += 1;
|
||||
run_make(&config.build_config, target_path_rel)
|
||||
}
|
||||
_ => BuildStatus::default(),
|
||||
};
|
||||
|
||||
let mut second_status = match base_path_rel {
|
||||
Some(base_path_rel) if config.build_base => {
|
||||
update_status(
|
||||
context,
|
||||
format!("Building base {}", base_path_rel.display()),
|
||||
step_idx,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
step_idx += 1;
|
||||
run_make(&config.build_config, base_path_rel)
|
||||
}
|
||||
_ => BuildStatus::default(),
|
||||
};
|
||||
|
||||
let time = OffsetDateTime::now_utc();
|
||||
|
||||
let first_obj = match &obj_config.target_path {
|
||||
Some(target_path) if first_status.success => {
|
||||
update_status(
|
||||
context,
|
||||
format!("Loading target {}", target_path_rel.unwrap().display()),
|
||||
step_idx,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
step_idx += 1;
|
||||
match read::read(target_path, &config.diff_obj_config) {
|
||||
Ok(obj) => Some(obj),
|
||||
Err(e) => {
|
||||
first_status = BuildStatus {
|
||||
success: false,
|
||||
stdout: format!("Loading object '{}'", target_path.display()),
|
||||
stderr: format!("{:#}", e),
|
||||
..Default::default()
|
||||
};
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
step_idx += 1;
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let 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()),
|
||||
step_idx,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
step_idx += 1;
|
||||
match read::read(base_path, &config.diff_obj_config) {
|
||||
Ok(obj) => Some(obj),
|
||||
Err(e) => {
|
||||
second_status = BuildStatus {
|
||||
success: false,
|
||||
stdout: format!("Loading object '{}'", base_path.display()),
|
||||
stderr: format!("{:#}", e),
|
||||
..Default::default()
|
||||
};
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
step_idx += 1;
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
update_status(context, "Performing diff".to_string(), step_idx, total, &cancel)?;
|
||||
step_idx += 1;
|
||||
let result = diff_objs(&config.diff_obj_config, first_obj.as_ref(), second_obj.as_ref(), None)?;
|
||||
|
||||
update_status(context, "Complete".to_string(), step_idx, total, &cancel)?;
|
||||
Ok(Box::new(ObjDiffResult {
|
||||
first_status,
|
||||
second_status,
|
||||
first_obj: first_obj.and_then(|o| result.left.map(|d| (o, d))),
|
||||
second_obj: second_obj.and_then(|o| result.right.map(|d| (o, d))),
|
||||
time,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn start_build(ctx: &egui::Context, config: ObjDiffConfig) -> JobState {
|
||||
start_job(ctx, "Build", Job::ObjDiff, move |context, cancel| {
|
||||
run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
|
||||
})
|
||||
}
|
||||
@@ -1,32 +1,34 @@
|
||||
use std::{
|
||||
env::{current_dir, current_exe},
|
||||
fs,
|
||||
fs::File,
|
||||
path::PathBuf,
|
||||
sync::mpsc::Receiver,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use const_format::formatcp;
|
||||
|
||||
use crate::{
|
||||
jobs::{queue_job, update_status, Job, JobResult, JobState, Status},
|
||||
update::{build_updater, BIN_NAME},
|
||||
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
|
||||
update::build_updater,
|
||||
};
|
||||
|
||||
pub struct UpdateResult {
|
||||
pub exe_path: PathBuf,
|
||||
}
|
||||
|
||||
fn run_update(status: &Status, cancel: Receiver<()>) -> Result<Box<UpdateResult>> {
|
||||
fn run_update(
|
||||
status: &JobContext,
|
||||
cancel: Receiver<()>,
|
||||
bin_name: String,
|
||||
) -> Result<Box<UpdateResult>> {
|
||||
update_status(status, "Fetching latest release".to_string(), 0, 3, &cancel)?;
|
||||
let updater = build_updater().context("Failed to create release updater")?;
|
||||
let latest_release = updater.get_latest_release()?;
|
||||
let asset = latest_release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|a| a.name == BIN_NAME)
|
||||
.ok_or_else(|| anyhow::Error::msg(formatcp!("No release asset for {}", BIN_NAME)))?;
|
||||
.find(|a| a.name == bin_name)
|
||||
.ok_or_else(|| anyhow::Error::msg(format!("No release asset for {bin_name}")))?;
|
||||
|
||||
update_status(status, "Downloading release".to_string(), 1, 3, &cancel)?;
|
||||
let tmp_dir = tempfile::Builder::new().prefix("update").tempdir_in(current_dir()?)?;
|
||||
@@ -34,7 +36,7 @@ fn run_update(status: &Status, cancel: Receiver<()>) -> Result<Box<UpdateResult>
|
||||
let tmp_file = File::create(&tmp_path)?;
|
||||
self_update::Download::from_url(&asset.download_url)
|
||||
.set_header(reqwest::header::ACCEPT, "application/octet-stream".parse()?)
|
||||
.download_to(&tmp_file)?;
|
||||
.download_to(tmp_file)?;
|
||||
|
||||
update_status(status, "Extracting release".to_string(), 2, 3, &cancel)?;
|
||||
let tmp_file = tmp_dir.path().join("replacement_tmp");
|
||||
@@ -44,18 +46,19 @@ fn run_update(status: &Status, cancel: Receiver<()>) -> Result<Box<UpdateResult>
|
||||
.to_dest(&target_file)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::{fs, os::unix::fs::PermissionsExt};
|
||||
let mut perms = fs::metadata(&target_file)?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&target_file, perms)?;
|
||||
}
|
||||
tmp_dir.close()?;
|
||||
|
||||
update_status(status, "Complete".to_string(), 3, 3, &cancel)?;
|
||||
Ok(Box::from(UpdateResult { exe_path: target_file }))
|
||||
}
|
||||
|
||||
pub fn queue_update() -> JobState {
|
||||
queue_job("Update app", Job::Update, move |status, cancel| {
|
||||
run_update(status, cancel).map(JobResult::Update)
|
||||
pub fn start_update(ctx: &egui::Context, bin_name: String) -> JobState {
|
||||
start_job(ctx, "Update app", Job::Update, move |context, cancel| {
|
||||
run_update(&context, cancel, bin_name).map(JobResult::Update)
|
||||
})
|
||||
}
|
||||
231
objdiff-gui/src/main.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||
|
||||
mod app;
|
||||
mod app_config;
|
||||
mod config;
|
||||
mod fonts;
|
||||
mod jobs;
|
||||
mod update;
|
||||
mod views;
|
||||
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
process::ExitCode,
|
||||
rc::Rc,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use anyhow::{ensure, Result};
|
||||
use cfg_if::cfg_if;
|
||||
use time::UtcOffset;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use crate::views::graphics::{load_graphics_config, GraphicsBackend, GraphicsConfig};
|
||||
|
||||
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)?;
|
||||
ensure!(info.bit_depth == png::BitDepth::Eight);
|
||||
ensure!(info.color_type == png::ColorType::Rgba);
|
||||
buf.truncate(info.buffer_size());
|
||||
Ok(egui::IconData { rgba: buf, width: info.width, height: info.height })
|
||||
}
|
||||
|
||||
const APP_NAME: &str = "objdiff";
|
||||
|
||||
// When compiling natively:
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn main() -> ExitCode {
|
||||
// Log to stdout (if you run with `RUST_LOG=debug`).
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::builder()
|
||||
// Default to info level
|
||||
.with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into())
|
||||
.from_env_lossy()
|
||||
// This module is noisy at info level
|
||||
.add_directive("wgpu_core::device::resource=warn".parse().unwrap()),
|
||||
)
|
||||
.init();
|
||||
|
||||
// Because localtime_r is unsound in multithreaded apps,
|
||||
// we must call this before initializing eframe.
|
||||
// https://github.com/time-rs/time/issues/293
|
||||
let utc_offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC);
|
||||
|
||||
let app_path = std::env::current_exe().ok();
|
||||
let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None));
|
||||
let mut native_options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default().with_app_id(APP_NAME),
|
||||
..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:?}");
|
||||
}
|
||||
}
|
||||
let mut graphics_config = GraphicsConfig::default();
|
||||
let mut graphics_config_path = None;
|
||||
if let Some(storage_dir) = eframe::storage_dir(APP_NAME) {
|
||||
let config_path = storage_dir.join("graphics.ron");
|
||||
match load_graphics_config(&config_path) {
|
||||
Ok(Some(config)) => {
|
||||
graphics_config = config;
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => {
|
||||
log::error!("Failed to load native config: {e:?}");
|
||||
}
|
||||
}
|
||||
graphics_config_path = Some(config_path);
|
||||
}
|
||||
#[cfg(feature = "wgpu")]
|
||||
{
|
||||
use eframe::egui_wgpu::wgpu::Backends;
|
||||
if graphics_config.desired_backend.is_supported() {
|
||||
native_options.wgpu_options.supported_backends = match graphics_config.desired_backend {
|
||||
GraphicsBackend::Auto => native_options.wgpu_options.supported_backends,
|
||||
GraphicsBackend::Dx12 => Backends::DX12,
|
||||
GraphicsBackend::Metal => Backends::METAL,
|
||||
GraphicsBackend::Vulkan => Backends::VULKAN,
|
||||
GraphicsBackend::OpenGL => Backends::GL,
|
||||
};
|
||||
}
|
||||
}
|
||||
let mut eframe_error = None;
|
||||
if let Err(e) = run_eframe(
|
||||
native_options.clone(),
|
||||
utc_offset,
|
||||
exec_path.clone(),
|
||||
app_path.clone(),
|
||||
graphics_config.clone(),
|
||||
graphics_config_path.clone(),
|
||||
) {
|
||||
eframe_error = Some(e);
|
||||
}
|
||||
#[cfg(feature = "wgpu")]
|
||||
if let Some(e) = eframe_error {
|
||||
// Attempt to relaunch using wgpu auto backend if the desired backend failed
|
||||
#[allow(unused_mut)]
|
||||
let mut should_relaunch = graphics_config.desired_backend != GraphicsBackend::Auto;
|
||||
#[cfg(feature = "glow")]
|
||||
{
|
||||
// If the desired backend is OpenGL, we should try to relaunch using the glow renderer
|
||||
should_relaunch &= graphics_config.desired_backend != GraphicsBackend::OpenGL;
|
||||
}
|
||||
if should_relaunch {
|
||||
log::warn!("Failed to launch application: {e:?}");
|
||||
log::warn!("Attempting to relaunch using auto-detected backend");
|
||||
native_options.wgpu_options.supported_backends = Default::default();
|
||||
if let Err(e) = run_eframe(
|
||||
native_options.clone(),
|
||||
utc_offset,
|
||||
exec_path.clone(),
|
||||
app_path.clone(),
|
||||
graphics_config.clone(),
|
||||
graphics_config_path.clone(),
|
||||
) {
|
||||
eframe_error = Some(e);
|
||||
} else {
|
||||
eframe_error = None;
|
||||
}
|
||||
} else {
|
||||
eframe_error = Some(e);
|
||||
}
|
||||
}
|
||||
#[cfg(all(feature = "wgpu", feature = "glow"))]
|
||||
if let Some(e) = eframe_error {
|
||||
// Attempt to relaunch using the glow renderer if the wgpu backend failed
|
||||
log::warn!("Failed to launch application: {e:?}");
|
||||
log::warn!("Attempting to relaunch using fallback OpenGL backend");
|
||||
native_options.renderer = eframe::Renderer::Glow;
|
||||
if let Err(e) = run_eframe(
|
||||
native_options,
|
||||
utc_offset,
|
||||
exec_path.clone(),
|
||||
app_path,
|
||||
graphics_config,
|
||||
graphics_config_path,
|
||||
) {
|
||||
eframe_error = Some(e);
|
||||
} else {
|
||||
eframe_error = None;
|
||||
}
|
||||
}
|
||||
if let Some(e) = eframe_error {
|
||||
log::error!("Failed to launch application: {e:?}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
// Attempt to relaunch application from the updated path
|
||||
if let Ok(mut guard) = exec_path.lock() {
|
||||
if let Some(path) = guard.take() {
|
||||
cfg_if! {
|
||||
if #[cfg(unix)] {
|
||||
let e = exec::Command::new(path)
|
||||
.args(&std::env::args().collect::<Vec<String>>())
|
||||
.exec();
|
||||
log::error!("Failed to relaunch: {e:?}");
|
||||
return ExitCode::FAILURE;
|
||||
} else {
|
||||
let result = std::process::Command::new(path)
|
||||
.args(std::env::args())
|
||||
.spawn();
|
||||
if let Err(e) = result {
|
||||
log::error!("Failed to relaunch: {e:?}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
fn run_eframe(
|
||||
native_options: eframe::NativeOptions,
|
||||
utc_offset: UtcOffset,
|
||||
exec_path_clone: Rc<Mutex<Option<PathBuf>>>,
|
||||
app_path: Option<PathBuf>,
|
||||
graphics_config: GraphicsConfig,
|
||||
graphics_config_path: Option<PathBuf>,
|
||||
) -> Result<(), eframe::Error> {
|
||||
eframe::run_native(
|
||||
APP_NAME,
|
||||
native_options,
|
||||
Box::new(move |cc| {
|
||||
Ok(Box::new(app::App::new(
|
||||
cc,
|
||||
utc_offset,
|
||||
exec_path_clone,
|
||||
app_path,
|
||||
graphics_config,
|
||||
graphics_config_path,
|
||||
)))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// when compiling to web using trunk.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn main() {
|
||||
// Make sure panics are logged using `console.error`.
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
// Redirect tracing to console.log and friends:
|
||||
tracing_wasm::set_as_global_default();
|
||||
|
||||
let web_options = eframe::WebOptions::default();
|
||||
eframe::start_web(
|
||||
"the_canvas_id", // hardcode it
|
||||
web_options,
|
||||
Box::new(|cc| Box::new(eframe_template::TemplateApp::new(cc))),
|
||||
)
|
||||
.expect("failed to start eframe");
|
||||
}
|
||||
@@ -20,8 +20,9 @@ cfg_if! {
|
||||
}
|
||||
pub const GITHUB_USER: &str = "encounter";
|
||||
pub const GITHUB_REPO: &str = "objdiff";
|
||||
pub const BIN_NAME: &str =
|
||||
formatcp!("{}-{}-{}{}", GITHUB_REPO, OS, ARCH, std::env::consts::EXE_SUFFIX);
|
||||
pub const BIN_NAME_NEW: &str =
|
||||
formatcp!("objdiff-gui-{}-{}{}", OS, ARCH, std::env::consts::EXE_SUFFIX);
|
||||
pub const BIN_NAME_OLD: &str = formatcp!("objdiff-{}-{}{}", OS, ARCH, std::env::consts::EXE_SUFFIX);
|
||||
pub const RELEASE_URL: &str =
|
||||
formatcp!("https://github.com/{}/{}/releases/latest", GITHUB_USER, GITHUB_REPO);
|
||||
|
||||
@@ -29,7 +30,8 @@ pub fn build_updater() -> self_update::errors::Result<Box<dyn ReleaseUpdate>> {
|
||||
self_update::backends::github::Update::configure()
|
||||
.repo_owner(GITHUB_USER)
|
||||
.repo_name(GITHUB_REPO)
|
||||
.bin_name(BIN_NAME)
|
||||
// bin_name is required, but unused?
|
||||
.bin_name(BIN_NAME_NEW)
|
||||
.no_confirm(true)
|
||||
.show_output(false)
|
||||
.current_version(cargo_crate_version!())
|
||||
313
objdiff-gui/src/views/appearance.rs
Normal file
@@ -0,0 +1,313 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use egui::{text::LayoutJob, Color32, FontFamily, FontId, TextFormat, 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: egui::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: egui::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 {
|
||||
egui::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);
|
||||
}
|
||||
egui::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);
|
||||
}
|
||||
}
|
||||
style.spacing.scroll = egui::style::ScrollStyle::solid();
|
||||
style.spacing.scroll.bar_width = 10.0;
|
||||
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 fn code_text_format(&self, base_color: Color32, highlight: bool) -> TextFormat {
|
||||
TextFormat {
|
||||
font_id: self.code_font.clone(),
|
||||
color: if highlight { self.emphasized_text_color } else { base_color },
|
||||
background: if highlight { self.deemphasized_text_color } else { Color32::TRANSPARENT },
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(128, 128, 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, egui::Theme::Dark, "Dark");
|
||||
ui.selectable_value(&mut appearance.theme, egui::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);
|
||||
}
|
||||
});
|
||||
}
|
||||
82
objdiff-gui/src/views/column_layout.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use egui::{Align, Layout, Sense, Vec2};
|
||||
use egui_extras::{Column, Size, StripBuilder, TableBuilder, TableRow};
|
||||
|
||||
pub fn render_header(
|
||||
ui: &mut egui::Ui,
|
||||
available_width: f32,
|
||||
num_columns: usize,
|
||||
mut add_contents: impl FnMut(&mut egui::Ui, usize),
|
||||
) {
|
||||
let column_width = available_width / num_columns as f32;
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: available_width, y: 100.0 },
|
||||
Layout::left_to_right(Align::Min),
|
||||
|ui| {
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
|
||||
for i in 0..num_columns {
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: column_width, y: 100.0 },
|
||||
Layout::top_down(Align::Min),
|
||||
|ui| {
|
||||
ui.set_width(column_width);
|
||||
add_contents(ui, i);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
ui.separator();
|
||||
}
|
||||
|
||||
pub fn render_table(
|
||||
ui: &mut egui::Ui,
|
||||
available_width: f32,
|
||||
num_columns: usize,
|
||||
row_height: f32,
|
||||
total_rows: usize,
|
||||
mut add_contents: impl FnMut(&mut TableRow, usize),
|
||||
) {
|
||||
ui.style_mut().interaction.selectable_labels = false;
|
||||
let column_width = available_width / num_columns as f32;
|
||||
let available_height = ui.available_height();
|
||||
let table = TableBuilder::new(ui)
|
||||
.striped(false)
|
||||
.cell_layout(Layout::left_to_right(Align::Min))
|
||||
.columns(Column::exact(column_width).clip(true), num_columns)
|
||||
.resizable(false)
|
||||
.auto_shrink([false, false])
|
||||
.min_scrolled_height(available_height)
|
||||
.sense(Sense::click());
|
||||
table.body(|body| {
|
||||
body.rows(row_height, total_rows, |mut row| {
|
||||
row.set_hovered(false); // Disable hover effect
|
||||
for i in 0..num_columns {
|
||||
add_contents(&mut row, i);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn render_strips(
|
||||
ui: &mut egui::Ui,
|
||||
available_width: f32,
|
||||
num_columns: usize,
|
||||
mut add_contents: impl FnMut(&mut egui::Ui, usize),
|
||||
) {
|
||||
let column_width = available_width / num_columns as f32;
|
||||
StripBuilder::new(ui).size(Size::remainder()).clip(true).vertical(|mut strip| {
|
||||
strip.strip(|builder| {
|
||||
builder.sizes(Size::exact(column_width), num_columns).clip(true).horizontal(
|
||||
|mut strip| {
|
||||
for i in 0..num_columns {
|
||||
strip.cell(|ui| {
|
||||
ui.push_id(i, |ui| {
|
||||
add_contents(ui, i);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
992
objdiff-gui/src/views/config.rs
Normal file
@@ -0,0 +1,992 @@
|
||||
#[cfg(all(windows, feature = "wsl"))]
|
||||
use std::string::FromUtf16Error;
|
||||
use std::{
|
||||
mem::take,
|
||||
path::{Path, PathBuf, MAIN_SEPARATOR},
|
||||
};
|
||||
|
||||
#[cfg(all(windows, feature = "wsl"))]
|
||||
use anyhow::{Context, Result};
|
||||
use egui::{
|
||||
output::OpenUrl, text::LayoutJob, CollapsingHeader, FontFamily, FontId, RichText,
|
||||
SelectableLabel, TextFormat, Widget,
|
||||
};
|
||||
use globset::Glob;
|
||||
use objdiff_core::{
|
||||
config::{ProjectObject, DEFAULT_WATCH_PATTERNS},
|
||||
diff::{ArmArchVersion, ArmR9Usage, MipsAbi, MipsInstrCategory, X86Formatter},
|
||||
};
|
||||
use strum::{EnumMessage, VariantArray};
|
||||
|
||||
use crate::{
|
||||
app::{AppConfig, AppState, AppStateRef, ObjectConfig},
|
||||
config::ProjectObjectNode,
|
||||
jobs::{
|
||||
check_update::{start_check_update, CheckUpdateResult},
|
||||
update::start_update,
|
||||
Job, JobQueue, JobResult,
|
||||
},
|
||||
update::RELEASE_URL,
|
||||
views::{
|
||||
appearance::Appearance,
|
||||
file::{FileDialogResult, FileDialogState},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ConfigViewState {
|
||||
pub check_update: Option<Box<CheckUpdateResult>>,
|
||||
pub check_update_running: bool,
|
||||
pub queue_check_update: bool,
|
||||
pub update_running: bool,
|
||||
pub queue_update: Option<String>,
|
||||
pub build_running: bool,
|
||||
pub queue_build: bool,
|
||||
pub watch_pattern_text: String,
|
||||
pub object_search: String,
|
||||
pub filter_diffable: bool,
|
||||
pub filter_incomplete: bool,
|
||||
pub show_hidden: bool,
|
||||
#[cfg(all(windows, feature = "wsl"))]
|
||||
pub available_wsl_distros: Option<Vec<String>>,
|
||||
pub file_dialog_state: FileDialogState,
|
||||
}
|
||||
|
||||
impl ConfigViewState {
|
||||
pub fn pre_update(&mut self, jobs: &mut JobQueue, state: &AppStateRef) {
|
||||
jobs.results.retain_mut(|result| {
|
||||
if let JobResult::CheckUpdate(result) = result {
|
||||
self.check_update = take(result);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
self.build_running = jobs.is_running(Job::ObjDiff);
|
||||
self.check_update_running = jobs.is_running(Job::CheckUpdate);
|
||||
self.update_running = jobs.is_running(Job::Update);
|
||||
|
||||
// Check async file dialog results
|
||||
match self.file_dialog_state.poll() {
|
||||
FileDialogResult::None => {}
|
||||
FileDialogResult::ProjectDir(path) => {
|
||||
let mut guard = state.write().unwrap();
|
||||
guard.set_project_dir(path.to_path_buf());
|
||||
}
|
||||
FileDialogResult::TargetDir(path) => {
|
||||
let mut guard = state.write().unwrap();
|
||||
guard.set_target_obj_dir(path.to_path_buf());
|
||||
}
|
||||
FileDialogResult::BaseDir(path) => {
|
||||
let mut guard = state.write().unwrap();
|
||||
guard.set_base_obj_dir(path.to_path_buf());
|
||||
}
|
||||
FileDialogResult::Object(path) => {
|
||||
let mut guard = state.write().unwrap();
|
||||
if let (Some(base_dir), Some(target_dir)) =
|
||||
(&guard.config.base_obj_dir, &guard.config.target_obj_dir)
|
||||
{
|
||||
if let Ok(obj_path) = path.strip_prefix(base_dir) {
|
||||
let target_path = target_dir.join(obj_path);
|
||||
guard.set_selected_obj(ObjectConfig {
|
||||
name: obj_path.display().to_string(),
|
||||
target_path: Some(target_path),
|
||||
base_path: Some(path),
|
||||
..Default::default()
|
||||
});
|
||||
} else if let Ok(obj_path) = path.strip_prefix(target_dir) {
|
||||
let base_path = base_dir.join(obj_path);
|
||||
guard.set_selected_obj(ObjectConfig {
|
||||
name: obj_path.display().to_string(),
|
||||
target_path: Some(path),
|
||||
base_path: Some(base_path),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn post_update(&mut self, ctx: &egui::Context, jobs: &mut JobQueue, state: &AppStateRef) {
|
||||
if self.queue_build {
|
||||
self.queue_build = false;
|
||||
if let Ok(mut state) = state.write() {
|
||||
state.queue_build = true;
|
||||
}
|
||||
}
|
||||
|
||||
if self.queue_check_update {
|
||||
self.queue_check_update = false;
|
||||
jobs.push_once(Job::CheckUpdate, || start_check_update(ctx));
|
||||
}
|
||||
|
||||
if let Some(bin_name) = self.queue_update.take() {
|
||||
jobs.push_once(Job::Update, || start_update(ctx, bin_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(windows, feature = "wsl"))]
|
||||
fn process_utf16(bytes: &[u8]) -> Result<String, FromUtf16Error> {
|
||||
let u16_bytes: Vec<u16> = bytes
|
||||
.chunks_exact(2)
|
||||
.filter_map(|c| Some(u16::from_ne_bytes(c.try_into().ok()?)))
|
||||
.collect();
|
||||
String::from_utf16(&u16_bytes)
|
||||
}
|
||||
|
||||
#[cfg(all(windows, feature = "wsl"))]
|
||||
fn wsl_cmd(args: &[&str]) -> Result<String> {
|
||||
use std::{os::windows::process::CommandExt, process::Command};
|
||||
let output = Command::new("wsl")
|
||||
.args(args)
|
||||
.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW)
|
||||
.output()
|
||||
.context("Failed to execute wsl")?;
|
||||
process_utf16(&output.stdout).context("Failed to process stdout")
|
||||
}
|
||||
|
||||
#[cfg(all(windows, feature = "wsl"))]
|
||||
fn fetch_wsl2_distros() -> Vec<String> {
|
||||
wsl_cmd(&["-l", "-q"])
|
||||
.map(|stdout| {
|
||||
stdout
|
||||
.split('\n')
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.map(|s| s.trim().to_string())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn config_ui(
|
||||
ui: &mut egui::Ui,
|
||||
state: &AppStateRef,
|
||||
show_config_window: &mut bool,
|
||||
config_state: &mut ConfigViewState,
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
let mut state_guard = state.write().unwrap();
|
||||
let AppState {
|
||||
config:
|
||||
AppConfig {
|
||||
project_dir, target_obj_dir, base_obj_dir, selected_obj, auto_update_check, ..
|
||||
},
|
||||
objects,
|
||||
object_nodes,
|
||||
..
|
||||
} = &mut *state_guard;
|
||||
|
||||
ui.heading("Updates");
|
||||
ui.checkbox(auto_update_check, "Check for updates on startup");
|
||||
if ui.add_enabled(!config_state.check_update_running, egui::Button::new("Check now")).clicked()
|
||||
{
|
||||
config_state.queue_check_update = true;
|
||||
}
|
||||
ui.label(format!("Current version: {}", env!("CARGO_PKG_VERSION")));
|
||||
if let Some(result) = &config_state.check_update {
|
||||
ui.label(format!("Latest version: {}", result.latest_release.version));
|
||||
if result.update_available {
|
||||
ui.colored_label(appearance.insert_color, "Update available");
|
||||
ui.horizontal(|ui| {
|
||||
if let Some(bin_name) = &result.found_binary {
|
||||
if ui
|
||||
.add_enabled(!config_state.update_running, egui::Button::new("Automatic"))
|
||||
.on_hover_text_at_pointer(
|
||||
"Automatically download and replace the current build",
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
config_state.queue_update = Some(bin_name.clone());
|
||||
}
|
||||
}
|
||||
if ui
|
||||
.button("Manual")
|
||||
.on_hover_text_at_pointer("Open a link to the latest release on GitHub")
|
||||
.clicked()
|
||||
{
|
||||
ui.output_mut(|output| {
|
||||
output.open_url =
|
||||
Some(OpenUrl { url: RELEASE_URL.to_string(), new_tab: true })
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.heading("Project");
|
||||
if ui.button(RichText::new("Settings")).clicked() {
|
||||
*show_config_window = true;
|
||||
}
|
||||
});
|
||||
|
||||
let selected_index = selected_obj.as_ref().and_then(|selected_obj| {
|
||||
objects.iter().position(|obj| obj.name.as_ref() == Some(&selected_obj.name))
|
||||
});
|
||||
let mut new_selected_index = selected_index;
|
||||
if objects.is_empty() {
|
||||
if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) {
|
||||
if ui.button("Select object").clicked() {
|
||||
config_state.file_dialog_state.queue(
|
||||
|| {
|
||||
Box::pin(
|
||||
rfd::AsyncFileDialog::new()
|
||||
.set_directory(target_dir)
|
||||
.add_filter("Object file", &["o", "elf", "obj"])
|
||||
.pick_file(),
|
||||
)
|
||||
},
|
||||
FileDialogResult::Object,
|
||||
);
|
||||
}
|
||||
if let Some(obj) = selected_obj {
|
||||
ui.label(
|
||||
RichText::new(&obj.name)
|
||||
.color(appearance.replace_color)
|
||||
.family(FontFamily::Monospace),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ui.colored_label(appearance.delete_color, "Missing project settings");
|
||||
}
|
||||
} else {
|
||||
let had_search = !config_state.object_search.is_empty();
|
||||
egui::TextEdit::singleline(&mut config_state.object_search).hint_text("Filter").ui(ui);
|
||||
|
||||
let mut root_open = None;
|
||||
let mut node_open = NodeOpen::Default;
|
||||
ui.horizontal(|ui| {
|
||||
if ui.small_button("⏶").on_hover_text_at_pointer("Collapse all").clicked() {
|
||||
root_open = Some(false);
|
||||
node_open = NodeOpen::Close;
|
||||
}
|
||||
if ui.small_button("⏷").on_hover_text_at_pointer("Expand all").clicked() {
|
||||
root_open = Some(true);
|
||||
node_open = NodeOpen::Open;
|
||||
}
|
||||
if ui
|
||||
.add_enabled(selected_obj.is_some(), egui::Button::new("⌖").small())
|
||||
.on_hover_text_at_pointer("Current object")
|
||||
.clicked()
|
||||
{
|
||||
root_open = Some(true);
|
||||
node_open = NodeOpen::Object;
|
||||
}
|
||||
let mut filters_text = RichText::new("Filter ⏷");
|
||||
if config_state.filter_diffable
|
||||
|| config_state.filter_incomplete
|
||||
|| config_state.show_hidden
|
||||
{
|
||||
filters_text = filters_text.color(appearance.replace_color);
|
||||
}
|
||||
egui::menu::menu_button(ui, filters_text, |ui| {
|
||||
ui.checkbox(&mut config_state.filter_diffable, "Diffable")
|
||||
.on_hover_text_at_pointer("Only show objects with a source file");
|
||||
ui.checkbox(&mut config_state.filter_incomplete, "Incomplete")
|
||||
.on_hover_text_at_pointer("Only show objects not marked complete");
|
||||
ui.checkbox(&mut config_state.show_hidden, "Hidden")
|
||||
.on_hover_text_at_pointer("Show hidden (auto-generated) objects");
|
||||
});
|
||||
});
|
||||
if config_state.object_search.is_empty() {
|
||||
if had_search {
|
||||
root_open = Some(true);
|
||||
node_open = NodeOpen::Object;
|
||||
}
|
||||
} else if !had_search {
|
||||
root_open = Some(true);
|
||||
node_open = NodeOpen::Open;
|
||||
}
|
||||
|
||||
CollapsingHeader::new(RichText::new("🗀 Objects").font(FontId {
|
||||
size: appearance.ui_font.size,
|
||||
family: appearance.code_font.family.clone(),
|
||||
}))
|
||||
.open(root_open)
|
||||
.default_open(true)
|
||||
.show(ui, |ui| {
|
||||
let search = config_state.object_search.to_ascii_lowercase();
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||
for node in object_nodes.iter().filter_map(|node| {
|
||||
filter_node(
|
||||
objects,
|
||||
node,
|
||||
&search,
|
||||
config_state.filter_diffable,
|
||||
config_state.filter_incomplete,
|
||||
config_state.show_hidden,
|
||||
)
|
||||
}) {
|
||||
display_node(
|
||||
ui,
|
||||
&mut new_selected_index,
|
||||
project_dir.as_deref(),
|
||||
objects,
|
||||
&node,
|
||||
appearance,
|
||||
node_open,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
if new_selected_index != selected_index {
|
||||
if let Some(idx) = new_selected_index {
|
||||
// Will set obj_changed, which will trigger a rebuild
|
||||
let config = ObjectConfig::from(&objects[idx]);
|
||||
state_guard.set_selected_obj(config);
|
||||
}
|
||||
}
|
||||
if state_guard.config.selected_obj.is_some()
|
||||
&& ui.add_enabled(!config_state.build_running, egui::Button::new("Build")).clicked()
|
||||
{
|
||||
config_state.queue_build = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn display_unit(
|
||||
ui: &mut egui::Ui,
|
||||
selected_obj: &mut Option<usize>,
|
||||
project_dir: Option<&Path>,
|
||||
name: &str,
|
||||
units: &[ProjectObject],
|
||||
index: usize,
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
let object = &units[index];
|
||||
let selected = *selected_obj == Some(index);
|
||||
let color = if selected {
|
||||
appearance.emphasized_text_color
|
||||
} else if let Some(complete) = object.complete() {
|
||||
if complete {
|
||||
appearance.insert_color
|
||||
} else {
|
||||
appearance.delete_color
|
||||
}
|
||||
} else {
|
||||
appearance.text_color
|
||||
};
|
||||
let response = SelectableLabel::new(
|
||||
selected,
|
||||
RichText::new(name)
|
||||
.font(FontId {
|
||||
size: appearance.ui_font.size,
|
||||
family: appearance.code_font.family.clone(),
|
||||
})
|
||||
.color(color),
|
||||
)
|
||||
.ui(ui);
|
||||
if get_source_path(project_dir, object).is_some() {
|
||||
response.context_menu(|ui| object_context_ui(ui, object, project_dir));
|
||||
}
|
||||
if response.clicked() {
|
||||
*selected_obj = Some(index);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_source_path(project_dir: Option<&Path>, object: &ProjectObject) -> Option<PathBuf> {
|
||||
project_dir.and_then(|dir| object.source_path().map(|path| dir.join(path)))
|
||||
}
|
||||
|
||||
fn object_context_ui(ui: &mut egui::Ui, object: &ProjectObject, project_dir: Option<&Path>) {
|
||||
if let Some(source_path) = get_source_path(project_dir, object) {
|
||||
if ui
|
||||
.button("Open source file")
|
||||
.on_hover_text("Open the source file in the default editor")
|
||||
.clicked()
|
||||
{
|
||||
log::info!("Opening file {}", source_path.display());
|
||||
if let Err(e) = open::that_detached(&source_path) {
|
||||
log::error!("Failed to open source file: {e}");
|
||||
}
|
||||
ui.close_menu();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Copy, Clone, PartialEq, Eq, Debug)]
|
||||
enum NodeOpen {
|
||||
#[default]
|
||||
Default,
|
||||
Open,
|
||||
Close,
|
||||
Object,
|
||||
}
|
||||
|
||||
fn display_node(
|
||||
ui: &mut egui::Ui,
|
||||
selected_obj: &mut Option<usize>,
|
||||
project_dir: Option<&Path>,
|
||||
units: &[ProjectObject],
|
||||
node: &ProjectObjectNode,
|
||||
appearance: &Appearance,
|
||||
node_open: NodeOpen,
|
||||
) {
|
||||
match node {
|
||||
ProjectObjectNode::Unit(name, idx) => {
|
||||
display_unit(ui, selected_obj, project_dir, name, units, *idx, appearance);
|
||||
}
|
||||
ProjectObjectNode::Dir(name, children) => {
|
||||
let contains_obj = selected_obj.map(|idx| contains_node(node, idx));
|
||||
let open = match node_open {
|
||||
NodeOpen::Default => None,
|
||||
NodeOpen::Open => Some(true),
|
||||
NodeOpen::Close => Some(false),
|
||||
NodeOpen::Object => contains_obj,
|
||||
};
|
||||
let color = if contains_obj == Some(true) {
|
||||
appearance.replace_color
|
||||
} else {
|
||||
appearance.text_color
|
||||
};
|
||||
CollapsingHeader::new(
|
||||
RichText::new(name)
|
||||
.font(FontId {
|
||||
size: appearance.ui_font.size,
|
||||
family: appearance.code_font.family.clone(),
|
||||
})
|
||||
.color(color),
|
||||
)
|
||||
.open(open)
|
||||
.show(ui, |ui| {
|
||||
for node in children {
|
||||
display_node(ui, selected_obj, project_dir, units, node, appearance, node_open);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn contains_node(node: &ProjectObjectNode, selected_obj: usize) -> bool {
|
||||
match node {
|
||||
ProjectObjectNode::Unit(_, idx) => *idx == selected_obj,
|
||||
ProjectObjectNode::Dir(_, children) => {
|
||||
children.iter().any(|node| contains_node(node, selected_obj))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn filter_node(
|
||||
units: &[ProjectObject],
|
||||
node: &ProjectObjectNode,
|
||||
search: &str,
|
||||
filter_diffable: bool,
|
||||
filter_incomplete: bool,
|
||||
show_hidden: bool,
|
||||
) -> Option<ProjectObjectNode> {
|
||||
match node {
|
||||
ProjectObjectNode::Unit(name, idx) => {
|
||||
let unit = &units[*idx];
|
||||
if (search.is_empty() || name.to_ascii_lowercase().contains(search))
|
||||
&& (!filter_diffable || (unit.base_path.is_some() && unit.target_path.is_some()))
|
||||
&& (!filter_incomplete || matches!(unit.complete(), None | Some(false)))
|
||||
&& (show_hidden || !unit.hidden())
|
||||
{
|
||||
Some(node.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
ProjectObjectNode::Dir(name, children) => {
|
||||
let new_children = children
|
||||
.iter()
|
||||
.filter_map(|child| {
|
||||
filter_node(
|
||||
units,
|
||||
child,
|
||||
search,
|
||||
filter_diffable,
|
||||
filter_incomplete,
|
||||
show_hidden,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !new_children.is_empty() {
|
||||
Some(ProjectObjectNode::Dir(name.clone(), new_children))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const HELP_ICON: &str = "ℹ";
|
||||
|
||||
fn subheading(ui: &mut egui::Ui, text: &str, appearance: &Appearance) {
|
||||
ui.label(
|
||||
RichText::new(text).size(appearance.ui_font.size).color(appearance.emphasized_text_color),
|
||||
);
|
||||
}
|
||||
|
||||
fn format_path(path: &Option<PathBuf>, appearance: &Appearance) -> RichText {
|
||||
let mut color = appearance.replace_color;
|
||||
let text = if let Some(dir) = path {
|
||||
if let Some(rel) = dirs::home_dir().and_then(|home| dir.strip_prefix(&home).ok()) {
|
||||
format!("~{}{}", MAIN_SEPARATOR, rel.display())
|
||||
} else {
|
||||
format!("{}", dir.display())
|
||||
}
|
||||
} else {
|
||||
color = appearance.delete_color;
|
||||
"[none]".to_string()
|
||||
};
|
||||
RichText::new(text).color(color).family(FontFamily::Monospace)
|
||||
}
|
||||
|
||||
pub const CONFIG_DISABLED_TEXT: &str =
|
||||
"Option disabled because it's set by the project configuration file.";
|
||||
|
||||
fn pick_folder_ui(
|
||||
ui: &mut egui::Ui,
|
||||
dir: &Option<PathBuf>,
|
||||
label: &str,
|
||||
tooltip: impl FnOnce(&mut egui::Ui),
|
||||
appearance: &Appearance,
|
||||
enabled: bool,
|
||||
) -> egui::Response {
|
||||
let response = ui.horizontal(|ui| {
|
||||
subheading(ui, label, appearance);
|
||||
ui.link(HELP_ICON).on_hover_ui(tooltip);
|
||||
ui.add_enabled(enabled, egui::Button::new("Select"))
|
||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||
});
|
||||
ui.label(format_path(dir, appearance));
|
||||
response.inner
|
||||
}
|
||||
|
||||
pub fn project_window(
|
||||
ctx: &egui::Context,
|
||||
state: &AppStateRef,
|
||||
show: &mut bool,
|
||||
config_state: &mut ConfigViewState,
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
let mut state_guard = state.write().unwrap();
|
||||
|
||||
egui::Window::new("Project").open(show).show(ctx, |ui| {
|
||||
split_obj_config_ui(ui, &mut state_guard, config_state, appearance);
|
||||
});
|
||||
|
||||
if let Some(error) = &state_guard.config_error {
|
||||
let mut open = true;
|
||||
egui::Window::new("Error").open(&mut open).show(ctx, |ui| {
|
||||
ui.label("Failed to load project config:");
|
||||
ui.colored_label(appearance.delete_color, error);
|
||||
});
|
||||
if !open {
|
||||
state_guard.config_error = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn split_obj_config_ui(
|
||||
ui: &mut egui::Ui,
|
||||
state: &mut AppState,
|
||||
config_state: &mut ConfigViewState,
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
let text_format = TextFormat::simple(appearance.ui_font.clone(), appearance.text_color);
|
||||
let code_format = TextFormat::simple(
|
||||
FontId { size: appearance.ui_font.size, family: appearance.code_font.family.clone() },
|
||||
appearance.emphasized_text_color,
|
||||
);
|
||||
|
||||
let response = pick_folder_ui(
|
||||
ui,
|
||||
&state.config.project_dir,
|
||||
"Project directory",
|
||||
|ui| {
|
||||
let mut job = LayoutJob::default();
|
||||
job.append("The root project directory.\n\n", 0.0, text_format.clone());
|
||||
job.append(
|
||||
"If a configuration file exists, it will be loaded automatically.",
|
||||
0.0,
|
||||
text_format.clone(),
|
||||
);
|
||||
ui.label(job);
|
||||
},
|
||||
appearance,
|
||||
true,
|
||||
);
|
||||
if response.clicked() {
|
||||
config_state.file_dialog_state.queue(
|
||||
|| Box::pin(rfd::AsyncFileDialog::new().pick_folder()),
|
||||
FileDialogResult::ProjectDir,
|
||||
);
|
||||
}
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
subheading(ui, "Build program", appearance);
|
||||
ui.link(HELP_ICON).on_hover_ui(|ui| {
|
||||
let mut job = LayoutJob::default();
|
||||
job.append("By default, objdiff will build with ", 0.0, text_format.clone());
|
||||
job.append("make", 0.0, code_format.clone());
|
||||
job.append(
|
||||
".\nIf the project uses a different build system (e.g. ",
|
||||
0.0,
|
||||
text_format.clone(),
|
||||
);
|
||||
job.append("ninja", 0.0, code_format.clone());
|
||||
job.append(
|
||||
"), specify it here.\nThe program must be in your ",
|
||||
0.0,
|
||||
text_format.clone(),
|
||||
);
|
||||
job.append("PATH", 0.0, code_format.clone());
|
||||
job.append(".", 0.0, text_format.clone());
|
||||
ui.label(job);
|
||||
});
|
||||
});
|
||||
let mut custom_make_str = state.config.custom_make.clone().unwrap_or_default();
|
||||
if ui
|
||||
.add_enabled(
|
||||
state.project_config_info.is_none(),
|
||||
egui::TextEdit::singleline(&mut custom_make_str).hint_text("make"),
|
||||
)
|
||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||
.changed()
|
||||
{
|
||||
if custom_make_str.is_empty() {
|
||||
state.config.custom_make = None;
|
||||
} else {
|
||||
state.config.custom_make = Some(custom_make_str);
|
||||
}
|
||||
}
|
||||
#[cfg(all(windows, feature = "wsl"))]
|
||||
{
|
||||
if config_state.available_wsl_distros.is_none() {
|
||||
config_state.available_wsl_distros = Some(fetch_wsl2_distros());
|
||||
}
|
||||
egui::ComboBox::from_label("Run in WSL2")
|
||||
.selected_text(
|
||||
state.config.selected_wsl_distro.as_ref().unwrap_or(&"Disabled".to_string()),
|
||||
)
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut state.config.selected_wsl_distro, None, "Disabled");
|
||||
for distro in config_state.available_wsl_distros.as_ref().unwrap() {
|
||||
ui.selectable_value(
|
||||
&mut state.config.selected_wsl_distro,
|
||||
Some(distro.clone()),
|
||||
distro,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
ui.separator();
|
||||
|
||||
if let Some(project_dir) = state.config.project_dir.clone() {
|
||||
let response = pick_folder_ui(
|
||||
ui,
|
||||
&state.config.target_obj_dir,
|
||||
"Target build directory",
|
||||
|ui| {
|
||||
let mut job = LayoutJob::default();
|
||||
job.append(
|
||||
"This contains the \"target\" or \"expected\" objects, which are the intended result of the match.\n\n",
|
||||
0.0,
|
||||
text_format.clone(),
|
||||
);
|
||||
job.append(
|
||||
"These are usually created by the project's build system or assembled.",
|
||||
0.0,
|
||||
text_format.clone(),
|
||||
);
|
||||
ui.label(job);
|
||||
},
|
||||
appearance,
|
||||
state.project_config_info.is_none(),
|
||||
);
|
||||
if response.clicked() {
|
||||
config_state.file_dialog_state.queue(
|
||||
|| Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()),
|
||||
FileDialogResult::TargetDir,
|
||||
);
|
||||
}
|
||||
ui.add_enabled(
|
||||
state.project_config_info.is_none(),
|
||||
egui::Checkbox::new(&mut state.config.build_target, "Build target objects"),
|
||||
)
|
||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||
.on_hover_ui(|ui| {
|
||||
let mut job = LayoutJob::default();
|
||||
job.append(
|
||||
"Tells the build system to produce the target object.\n",
|
||||
0.0,
|
||||
text_format.clone(),
|
||||
);
|
||||
job.append("For example, this would call ", 0.0, text_format.clone());
|
||||
job.append("make path/to/target.o", 0.0, code_format.clone());
|
||||
job.append(".\n\n", 0.0, text_format.clone());
|
||||
job.append(
|
||||
"This is useful if the target objects are not already built\n",
|
||||
0.0,
|
||||
text_format.clone(),
|
||||
);
|
||||
job.append(
|
||||
"or if they can change based on project configuration,\n",
|
||||
0.0,
|
||||
text_format.clone(),
|
||||
);
|
||||
job.append(
|
||||
"but requires that the build system is configured correctly.",
|
||||
0.0,
|
||||
text_format.clone(),
|
||||
);
|
||||
ui.label(job);
|
||||
});
|
||||
ui.separator();
|
||||
|
||||
let response = pick_folder_ui(
|
||||
ui,
|
||||
&state.config.base_obj_dir,
|
||||
"Base build directory",
|
||||
|ui| {
|
||||
let mut job = LayoutJob::default();
|
||||
job.append(
|
||||
"This contains the objects built from your decompiled code.",
|
||||
0.0,
|
||||
text_format.clone(),
|
||||
);
|
||||
ui.label(job);
|
||||
},
|
||||
appearance,
|
||||
state.project_config_info.is_none(),
|
||||
);
|
||||
if response.clicked() {
|
||||
config_state.file_dialog_state.queue(
|
||||
|| Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()),
|
||||
FileDialogResult::BaseDir,
|
||||
);
|
||||
}
|
||||
ui.add_enabled(
|
||||
state.project_config_info.is_none(),
|
||||
egui::Checkbox::new(&mut state.config.build_base, "Build base objects"),
|
||||
)
|
||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||
.on_hover_ui(|ui| {
|
||||
let mut job = LayoutJob::default();
|
||||
job.append(
|
||||
"Tells the build system to produce the base object.\n",
|
||||
0.0,
|
||||
text_format.clone(),
|
||||
);
|
||||
job.append("For example, this would call ", 0.0, text_format.clone());
|
||||
job.append("make path/to/base.o", 0.0, code_format.clone());
|
||||
job.append(".\n\n", 0.0, text_format.clone());
|
||||
job.append(
|
||||
"This can be disabled if you're running the build system\n",
|
||||
0.0,
|
||||
text_format.clone(),
|
||||
);
|
||||
job.append(
|
||||
"externally, and just want objdiff to reload the files\n",
|
||||
0.0,
|
||||
text_format.clone(),
|
||||
);
|
||||
job.append("when they change.", 0.0, text_format.clone());
|
||||
ui.label(job);
|
||||
});
|
||||
ui.separator();
|
||||
}
|
||||
|
||||
subheading(ui, "Watch settings", appearance);
|
||||
let response =
|
||||
ui.checkbox(&mut state.config.rebuild_on_changes, "Rebuild on changes").on_hover_ui(|ui| {
|
||||
let mut job = LayoutJob::default();
|
||||
job.append(
|
||||
"Automatically re-run the build & diff when files change.",
|
||||
0.0,
|
||||
text_format.clone(),
|
||||
);
|
||||
ui.label(job);
|
||||
});
|
||||
if response.changed() {
|
||||
state.watcher_change = true;
|
||||
};
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(RichText::new("File patterns").color(appearance.text_color));
|
||||
if ui
|
||||
.add_enabled(state.project_config_info.is_none(), egui::Button::new("Reset"))
|
||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||
.clicked()
|
||||
{
|
||||
state.config.watch_patterns =
|
||||
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect();
|
||||
state.watcher_change = true;
|
||||
}
|
||||
});
|
||||
let mut remove_at: Option<usize> = None;
|
||||
for (idx, glob) in state.config.watch_patterns.iter().enumerate() {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
RichText::new(format!("{}", glob))
|
||||
.color(appearance.text_color)
|
||||
.family(FontFamily::Monospace),
|
||||
);
|
||||
if ui
|
||||
.add_enabled(state.project_config_info.is_none(), egui::Button::new("-").small())
|
||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||
.clicked()
|
||||
{
|
||||
remove_at = Some(idx);
|
||||
}
|
||||
});
|
||||
}
|
||||
if let Some(idx) = remove_at {
|
||||
state.config.watch_patterns.remove(idx);
|
||||
state.watcher_change = true;
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_enabled(
|
||||
state.project_config_info.is_none(),
|
||||
egui::TextEdit::singleline(&mut config_state.watch_pattern_text).desired_width(100.0),
|
||||
)
|
||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT);
|
||||
if ui
|
||||
.add_enabled(state.project_config_info.is_none(), egui::Button::new("+").small())
|
||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||
.clicked()
|
||||
{
|
||||
if let Ok(glob) = Glob::new(&config_state.watch_pattern_text) {
|
||||
state.config.watch_patterns.push(glob);
|
||||
state.watcher_change = true;
|
||||
config_state.watch_pattern_text.clear();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn arch_config_window(
|
||||
ctx: &egui::Context,
|
||||
state: &AppStateRef,
|
||||
show: &mut bool,
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
let mut state_guard = state.write().unwrap();
|
||||
egui::Window::new("Arch Settings").open(show).show(ctx, |ui| {
|
||||
arch_config_ui(ui, &mut state_guard, appearance);
|
||||
});
|
||||
}
|
||||
|
||||
fn arch_config_ui(ui: &mut egui::Ui, state: &mut AppState, _appearance: &Appearance) {
|
||||
ui.heading("x86");
|
||||
egui::ComboBox::new("x86_formatter", "Format")
|
||||
.selected_text(state.config.diff_obj_config.x86_formatter.get_message().unwrap())
|
||||
.show_ui(ui, |ui| {
|
||||
for &formatter in X86Formatter::VARIANTS {
|
||||
if ui
|
||||
.selectable_label(
|
||||
state.config.diff_obj_config.x86_formatter == formatter,
|
||||
formatter.get_message().unwrap(),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
state.config.diff_obj_config.x86_formatter = formatter;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
ui.heading("MIPS");
|
||||
egui::ComboBox::new("mips_abi", "ABI")
|
||||
.selected_text(state.config.diff_obj_config.mips_abi.get_message().unwrap())
|
||||
.show_ui(ui, |ui| {
|
||||
for &abi in MipsAbi::VARIANTS {
|
||||
if ui
|
||||
.selectable_label(
|
||||
state.config.diff_obj_config.mips_abi == abi,
|
||||
abi.get_message().unwrap(),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
state.config.diff_obj_config.mips_abi = abi;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
egui::ComboBox::new("mips_instr_category", "Instruction Category")
|
||||
.selected_text(state.config.diff_obj_config.mips_instr_category.get_message().unwrap())
|
||||
.show_ui(ui, |ui| {
|
||||
for &category in MipsInstrCategory::VARIANTS {
|
||||
if ui
|
||||
.selectable_label(
|
||||
state.config.diff_obj_config.mips_instr_category == category,
|
||||
category.get_message().unwrap(),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
state.config.diff_obj_config.mips_instr_category = category;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
ui.heading("ARM");
|
||||
egui::ComboBox::new("arm_arch_version", "Architecture Version")
|
||||
.selected_text(state.config.diff_obj_config.arm_arch_version.get_message().unwrap())
|
||||
.show_ui(ui, |ui| {
|
||||
for &version in ArmArchVersion::VARIANTS {
|
||||
if ui
|
||||
.selectable_label(
|
||||
state.config.diff_obj_config.arm_arch_version == version,
|
||||
version.get_message().unwrap(),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
state.config.diff_obj_config.arm_arch_version = version;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
let response = ui
|
||||
.checkbox(&mut state.config.diff_obj_config.arm_unified_syntax, "Unified syntax")
|
||||
.on_hover_text("Disassemble as unified assembly language (UAL).");
|
||||
if response.changed() {
|
||||
state.queue_reload = true;
|
||||
}
|
||||
let response = ui
|
||||
.checkbox(&mut state.config.diff_obj_config.arm_av_registers, "Use A/V registers")
|
||||
.on_hover_text("Display R0-R3 as A1-A4 and R4-R11 as V1-V8");
|
||||
if response.changed() {
|
||||
state.queue_reload = true;
|
||||
}
|
||||
egui::ComboBox::new("arm_r9_usage", "Display R9 as")
|
||||
.selected_text(state.config.diff_obj_config.arm_r9_usage.get_message().unwrap())
|
||||
.show_ui(ui, |ui| {
|
||||
for &usage in ArmR9Usage::VARIANTS {
|
||||
if ui
|
||||
.selectable_label(
|
||||
state.config.diff_obj_config.arm_r9_usage == usage,
|
||||
usage.get_message().unwrap(),
|
||||
)
|
||||
.on_hover_text(usage.get_detailed_message().unwrap())
|
||||
.clicked()
|
||||
{
|
||||
state.config.diff_obj_config.arm_r9_usage = usage;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
let response = ui
|
||||
.checkbox(&mut state.config.diff_obj_config.arm_sl_usage, "Display R10 as SL")
|
||||
.on_hover_text("Used for explicit stack limits.");
|
||||
if response.changed() {
|
||||
state.queue_reload = true;
|
||||
}
|
||||
let response = ui
|
||||
.checkbox(&mut state.config.diff_obj_config.arm_fp_usage, "Display R11 as FP")
|
||||
.on_hover_text("Used for frame pointers.");
|
||||
if response.changed() {
|
||||
state.queue_reload = true;
|
||||
}
|
||||
let response = ui
|
||||
.checkbox(&mut state.config.diff_obj_config.arm_ip_usage, "Display R12 as IP")
|
||||
.on_hover_text("Used for interworking and long branches.");
|
||||
if response.changed() {
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
292
objdiff-gui/src/views/data_diff.rs
Normal file
@@ -0,0 +1,292 @@
|
||||
use std::{cmp::min, default::Default, mem::take};
|
||||
|
||||
use egui::{text::LayoutJob, Id, Label, RichText, Sense, Widget};
|
||||
use objdiff_core::{
|
||||
diff::{ObjDataDiff, ObjDataDiffKind, ObjDiff},
|
||||
obj::ObjInfo,
|
||||
};
|
||||
use time::format_description;
|
||||
|
||||
use crate::views::{
|
||||
appearance::Appearance,
|
||||
column_layout::{render_header, render_table},
|
||||
symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState},
|
||||
write_text,
|
||||
};
|
||||
|
||||
const BYTES_PER_ROW: usize = 16;
|
||||
|
||||
fn find_section(obj: &ObjInfo, section_name: &str) -> Option<usize> {
|
||||
obj.sections.iter().position(|section| section.name == section_name)
|
||||
}
|
||||
|
||||
fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appearance: &Appearance) {
|
||||
if diffs.iter().any(|d| d.kind != ObjDataDiffKind::None) {
|
||||
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
|
||||
}
|
||||
let mut job = LayoutJob::default();
|
||||
write_text(
|
||||
format!("{address:08x}: ").as_str(),
|
||||
appearance.text_color,
|
||||
&mut job,
|
||||
appearance.code_font.clone(),
|
||||
);
|
||||
let mut cur_addr = 0usize;
|
||||
for diff in diffs {
|
||||
let base_color = match diff.kind {
|
||||
ObjDataDiffKind::None => appearance.text_color,
|
||||
ObjDataDiffKind::Replace => appearance.replace_color,
|
||||
ObjDataDiffKind::Delete => appearance.delete_color,
|
||||
ObjDataDiffKind::Insert => appearance.insert_color,
|
||||
};
|
||||
if diff.data.is_empty() {
|
||||
let mut str = " ".repeat(diff.len);
|
||||
str.push_str(" ".repeat(diff.len / 8).as_str());
|
||||
write_text(str.as_str(), base_color, &mut job, appearance.code_font.clone());
|
||||
cur_addr += diff.len;
|
||||
} else {
|
||||
let mut text = String::new();
|
||||
for byte in &diff.data {
|
||||
text.push_str(format!("{byte:02x} ").as_str());
|
||||
cur_addr += 1;
|
||||
if cur_addr % 8 == 0 {
|
||||
text.push(' ');
|
||||
}
|
||||
}
|
||||
write_text(text.as_str(), base_color, &mut job, appearance.code_font.clone());
|
||||
}
|
||||
}
|
||||
if cur_addr < BYTES_PER_ROW {
|
||||
let n = BYTES_PER_ROW - cur_addr;
|
||||
let mut str = " ".to_string();
|
||||
str.push_str(" ".repeat(n).as_str());
|
||||
str.push_str(" ".repeat(n / 8).as_str());
|
||||
write_text(str.as_str(), appearance.text_color, &mut job, appearance.code_font.clone());
|
||||
}
|
||||
write_text(" ", appearance.text_color, &mut job, appearance.code_font.clone());
|
||||
for diff in diffs {
|
||||
let base_color = match diff.kind {
|
||||
ObjDataDiffKind::None => appearance.text_color,
|
||||
ObjDataDiffKind::Replace => appearance.replace_color,
|
||||
ObjDataDiffKind::Delete => appearance.delete_color,
|
||||
ObjDataDiffKind::Insert => appearance.insert_color,
|
||||
};
|
||||
if diff.data.is_empty() {
|
||||
write_text(
|
||||
" ".repeat(diff.len).as_str(),
|
||||
base_color,
|
||||
&mut job,
|
||||
appearance.code_font.clone(),
|
||||
);
|
||||
} else {
|
||||
let mut text = String::new();
|
||||
for byte in &diff.data {
|
||||
let c = char::from(*byte);
|
||||
if c.is_ascii() && !c.is_ascii_control() {
|
||||
text.push(c);
|
||||
} else {
|
||||
text.push('.');
|
||||
}
|
||||
}
|
||||
write_text(text.as_str(), base_color, &mut job, appearance.code_font.clone());
|
||||
}
|
||||
}
|
||||
Label::new(job).sense(Sense::click()).ui(ui);
|
||||
// .on_hover_ui_at_pointer(|ui| ins_hover_ui(ui, ins))
|
||||
// .context_menu(|ui| ins_context_menu(ui, ins));
|
||||
}
|
||||
|
||||
fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> {
|
||||
let mut split_diffs = Vec::<Vec<ObjDataDiff>>::new();
|
||||
let mut row_diffs = Vec::<ObjDataDiff>::new();
|
||||
let mut cur_addr = 0usize;
|
||||
for diff in diffs {
|
||||
let mut cur_len = 0usize;
|
||||
while cur_len < diff.len {
|
||||
let remaining_len = diff.len - cur_len;
|
||||
let mut remaining_in_row = BYTES_PER_ROW - (cur_addr % BYTES_PER_ROW);
|
||||
let len = min(remaining_len, remaining_in_row);
|
||||
row_diffs.push(ObjDataDiff {
|
||||
data: if diff.data.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
diff.data[cur_len..cur_len + len].to_vec()
|
||||
},
|
||||
kind: diff.kind,
|
||||
len,
|
||||
// TODO
|
||||
symbol: String::new(),
|
||||
});
|
||||
remaining_in_row -= len;
|
||||
cur_len += len;
|
||||
cur_addr += len;
|
||||
if remaining_in_row == 0 {
|
||||
split_diffs.push(take(&mut row_diffs));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !row_diffs.is_empty() {
|
||||
split_diffs.push(take(&mut row_diffs));
|
||||
}
|
||||
split_diffs
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct SectionDiffContext<'a> {
|
||||
obj: &'a ObjInfo,
|
||||
diff: &'a ObjDiff,
|
||||
section_index: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'a> SectionDiffContext<'a> {
|
||||
pub fn new(obj: Option<&'a (ObjInfo, ObjDiff)>, section_name: Option<&str>) -> Option<Self> {
|
||||
obj.map(|(obj, diff)| Self {
|
||||
obj,
|
||||
diff,
|
||||
section_index: section_name.and_then(|section_name| find_section(obj, section_name)),
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn has_section(&self) -> bool { self.section_index.is_some() }
|
||||
}
|
||||
|
||||
fn data_table_ui(
|
||||
ui: &mut egui::Ui,
|
||||
available_width: f32,
|
||||
left_ctx: Option<SectionDiffContext<'_>>,
|
||||
right_ctx: Option<SectionDiffContext<'_>>,
|
||||
config: &Appearance,
|
||||
) -> Option<()> {
|
||||
let left_section = left_ctx
|
||||
.and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i])));
|
||||
let right_section = right_ctx
|
||||
.and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i])));
|
||||
let total_bytes = left_section
|
||||
.or(right_section)?
|
||||
.1
|
||||
.data_diff
|
||||
.iter()
|
||||
.fold(0usize, |accum, item| accum + item.len);
|
||||
if total_bytes == 0 {
|
||||
return None;
|
||||
}
|
||||
let total_rows = (total_bytes - 1) / BYTES_PER_ROW + 1;
|
||||
|
||||
let left_diffs = left_section.map(|(_, section)| split_diffs(§ion.data_diff));
|
||||
let right_diffs = right_section.map(|(_, section)| split_diffs(§ion.data_diff));
|
||||
|
||||
render_table(ui, available_width, 2, config.code_font.size, total_rows, |row, column| {
|
||||
let i = row.index();
|
||||
let address = i * BYTES_PER_ROW;
|
||||
row.col(|ui| {
|
||||
if column == 0 {
|
||||
if let Some(left_diffs) = &left_diffs {
|
||||
data_row_ui(ui, address, &left_diffs[i], config);
|
||||
}
|
||||
} else if column == 1 {
|
||||
if let Some(right_diffs) = &right_diffs {
|
||||
data_row_ui(ui, address, &right_diffs[i], config);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Some(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn data_diff_ui(
|
||||
ui: &mut egui::Ui,
|
||||
state: &DiffViewState,
|
||||
appearance: &Appearance,
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
let Some(result) = &state.build else {
|
||||
return ret;
|
||||
};
|
||||
|
||||
let section_name =
|
||||
state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref()).or_else(
|
||||
|| state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref()),
|
||||
);
|
||||
let left_ctx = SectionDiffContext::new(result.first_obj.as_ref(), section_name);
|
||||
let right_ctx = SectionDiffContext::new(result.second_obj.as_ref(), section_name);
|
||||
|
||||
// If both sides are missing a symbol, switch to symbol diff view
|
||||
if !right_ctx.is_some_and(|ctx| ctx.has_section())
|
||||
&& !left_ctx.is_some_and(|ctx| ctx.has_section())
|
||||
{
|
||||
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
|
||||
// Header
|
||||
let available_width = ui.available_width();
|
||||
render_header(ui, available_width, 2, |ui, column| {
|
||||
if column == 0 {
|
||||
// Left column
|
||||
if ui.button("⏴ Back").clicked() {
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
|
||||
if let Some(section) =
|
||||
left_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i]))
|
||||
{
|
||||
ui.label(
|
||||
RichText::new(section.name.clone())
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Missing")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.replace_color),
|
||||
);
|
||||
}
|
||||
} else if column == 1 {
|
||||
// Right column
|
||||
ui.horizontal(|ui| {
|
||||
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
|
||||
ret = Some(DiffViewAction::Build);
|
||||
}
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
if state.build_running {
|
||||
ui.colored_label(appearance.replace_color, "Building…");
|
||||
} else {
|
||||
ui.label("Last built:");
|
||||
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||
ui.label(
|
||||
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if let Some(section) =
|
||||
right_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i]))
|
||||
{
|
||||
ui.label(
|
||||
RichText::new(section.name.clone())
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Missing")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.replace_color),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Table
|
||||
let id =
|
||||
Id::new(state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref()))
|
||||
.with(state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref()));
|
||||
ui.push_id(id, |ui| {
|
||||
data_table_ui(ui, available_width, left_ctx, right_ctx, appearance);
|
||||
});
|
||||
ret
|
||||
}
|
||||
20
objdiff-gui/src/views/debug.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
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) {
|
||||
if ui.button("Clear memory").clicked() {
|
||||
ui.memory_mut(|m| *m = Default::default());
|
||||
}
|
||||
ui.label(format!("Repainting the UI each frame. FPS: {:.1}", frame_history.fps()));
|
||||
frame_history.ui(ui);
|
||||
}
|
||||
34
objdiff-gui/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]");
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
248
objdiff-gui/src/views/extab_diff.rs
Normal file
@@ -0,0 +1,248 @@
|
||||
use egui::{RichText, ScrollArea};
|
||||
use objdiff_core::{
|
||||
arch::ppc::ExceptionInfo,
|
||||
obj::{ObjInfo, ObjSymbol},
|
||||
};
|
||||
use time::format_description;
|
||||
|
||||
use crate::views::{
|
||||
appearance::Appearance,
|
||||
column_layout::{render_header, render_strips},
|
||||
function_diff::FunctionDiffContext,
|
||||
symbol_diff::{
|
||||
match_color_for_symbol, DiffViewAction, DiffViewNavigation, DiffViewState, SymbolRefByName,
|
||||
View,
|
||||
},
|
||||
};
|
||||
|
||||
fn decode_extab(extab: &ExceptionInfo) -> String {
|
||||
let mut text = String::from("");
|
||||
|
||||
let mut dtor_names: Vec<String> = vec![];
|
||||
for dtor in &extab.dtors {
|
||||
//For each function name, use the demangled name by default,
|
||||
//and if not available fallback to the original name
|
||||
let name: String = match &dtor.demangled_name {
|
||||
Some(demangled_name) => demangled_name.to_string(),
|
||||
None => dtor.name.clone(),
|
||||
};
|
||||
dtor_names.push(name);
|
||||
}
|
||||
if let Some(decoded) = extab.data.to_string(dtor_names) {
|
||||
text += decoded.as_str();
|
||||
}
|
||||
|
||||
text
|
||||
}
|
||||
|
||||
fn find_extab_entry<'a>(obj: &'a ObjInfo, symbol: &ObjSymbol) -> Option<&'a ExceptionInfo> {
|
||||
obj.arch.ppc().and_then(|ppc| ppc.extab_for_symbol(symbol))
|
||||
}
|
||||
|
||||
fn extab_text_ui(
|
||||
ui: &mut egui::Ui,
|
||||
ctx: FunctionDiffContext<'_>,
|
||||
symbol: &ObjSymbol,
|
||||
appearance: &Appearance,
|
||||
) -> Option<()> {
|
||||
if let Some(extab_entry) = find_extab_entry(ctx.obj, symbol) {
|
||||
let text = decode_extab(extab_entry);
|
||||
ui.colored_label(appearance.replace_color, &text);
|
||||
return Some(());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn extab_ui(
|
||||
ui: &mut egui::Ui,
|
||||
ctx: FunctionDiffContext<'_>,
|
||||
appearance: &Appearance,
|
||||
_column: usize,
|
||||
) {
|
||||
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||
|
||||
if let Some((_section, symbol)) =
|
||||
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
|
||||
{
|
||||
extab_text_ui(ui, ctx, symbol, appearance);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn extab_diff_ui(
|
||||
ui: &mut egui::Ui,
|
||||
state: &DiffViewState,
|
||||
appearance: &Appearance,
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
let Some(result) = &state.build else {
|
||||
return ret;
|
||||
};
|
||||
|
||||
let mut left_ctx = FunctionDiffContext::new(
|
||||
result.first_obj.as_ref(),
|
||||
state.symbol_state.left_symbol.as_ref(),
|
||||
);
|
||||
let mut right_ctx = FunctionDiffContext::new(
|
||||
result.second_obj.as_ref(),
|
||||
state.symbol_state.right_symbol.as_ref(),
|
||||
);
|
||||
|
||||
// If one side is missing a symbol, but the diff process found a match, use that symbol
|
||||
let left_diff_symbol = left_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
|
||||
});
|
||||
let right_diff_symbol = right_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
|
||||
});
|
||||
if left_diff_symbol.is_some() && right_ctx.is_some_and(|ctx| !ctx.has_symbol()) {
|
||||
let (right_section, right_symbol) =
|
||||
right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap());
|
||||
let symbol_ref = SymbolRefByName::new(right_symbol, right_section);
|
||||
right_ctx = FunctionDiffContext::new(result.second_obj.as_ref(), Some(&symbol_ref));
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
|
||||
view: Some(View::FunctionDiff),
|
||||
left_symbol: state.symbol_state.left_symbol.clone(),
|
||||
right_symbol: Some(symbol_ref),
|
||||
}));
|
||||
} else if right_diff_symbol.is_some() && left_ctx.is_some_and(|ctx| !ctx.has_symbol()) {
|
||||
let (left_section, left_symbol) =
|
||||
left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap());
|
||||
let symbol_ref = SymbolRefByName::new(left_symbol, left_section);
|
||||
left_ctx = FunctionDiffContext::new(result.first_obj.as_ref(), Some(&symbol_ref));
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
|
||||
view: Some(View::FunctionDiff),
|
||||
left_symbol: Some(symbol_ref),
|
||||
right_symbol: state.symbol_state.right_symbol.clone(),
|
||||
}));
|
||||
}
|
||||
|
||||
// If both sides are missing a symbol, switch to symbol diff view
|
||||
if right_ctx.is_some_and(|ctx| !ctx.has_symbol())
|
||||
&& left_ctx.is_some_and(|ctx| !ctx.has_symbol())
|
||||
{
|
||||
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
|
||||
// Header
|
||||
let available_width = ui.available_width();
|
||||
render_header(ui, available_width, 2, |ui, column| {
|
||||
if column == 0 {
|
||||
// Left column
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("⏴ Back").clicked() {
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
ui.separator();
|
||||
if ui
|
||||
.add_enabled(
|
||||
!state.scratch_running
|
||||
&& state.scratch_available
|
||||
&& left_ctx.is_some_and(|ctx| ctx.has_symbol()),
|
||||
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()
|
||||
{
|
||||
if let Some((_section, symbol)) = left_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
|
||||
}) {
|
||||
ret = Some(DiffViewAction::CreateScratch(symbol.name.clone()));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some((_section, symbol)) = left_ctx
|
||||
.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)))
|
||||
{
|
||||
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
|
||||
ui.label(
|
||||
RichText::new(name)
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Missing")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.replace_color),
|
||||
);
|
||||
}
|
||||
} else if column == 1 {
|
||||
// Right column
|
||||
ui.horizontal(|ui| {
|
||||
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
|
||||
ret = Some(DiffViewAction::Build);
|
||||
}
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
if state.build_running {
|
||||
ui.colored_label(appearance.replace_color, "Building…");
|
||||
} else {
|
||||
ui.label("Last built:");
|
||||
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||
ui.label(
|
||||
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
|
||||
);
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
if ui
|
||||
.add_enabled(state.source_path_available, egui::Button::new("🖹 Source file"))
|
||||
.on_hover_text_at_pointer("Open the source file in the default editor")
|
||||
.on_disabled_hover_text("Source file metadata missing")
|
||||
.clicked()
|
||||
{
|
||||
ret = Some(DiffViewAction::OpenSourcePath);
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.map(|symbol_ref| {
|
||||
(ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref))
|
||||
})
|
||||
}) {
|
||||
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
|
||||
ui.label(
|
||||
RichText::new(name)
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
if let Some(match_percent) = symbol_diff.match_percent {
|
||||
ui.label(
|
||||
RichText::new(format!("{:.0}%", match_percent.floor()))
|
||||
.font(appearance.code_font.clone())
|
||||
.color(match_color_for_symbol(match_percent, appearance)),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Missing")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.replace_color),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Table
|
||||
render_strips(ui, available_width, 2, |ui, column| {
|
||||
if column == 0 {
|
||||
if let Some(ctx) = left_ctx {
|
||||
extab_ui(ui, ctx, appearance, column);
|
||||
}
|
||||
} else if column == 1 {
|
||||
if let Some(ctx) = right_ctx {
|
||||
extab_ui(ui, ctx, appearance, column);
|
||||
}
|
||||
}
|
||||
});
|
||||
ret
|
||||
}
|
||||
51
objdiff-gui/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
objdiff-gui/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
|
||||
}
|
||||
}
|
||||
834
objdiff-gui/src/views/function_diff.rs
Normal file
@@ -0,0 +1,834 @@
|
||||
use std::default::Default;
|
||||
|
||||
use egui::{text::LayoutJob, Id, Label, Response, RichText, Sense, Widget};
|
||||
use egui_extras::TableRow;
|
||||
use objdiff_core::{
|
||||
diff::{
|
||||
display::{display_diff, DiffText, HighlightKind},
|
||||
ObjDiff, ObjInsDiff, ObjInsDiffKind,
|
||||
},
|
||||
obj::{
|
||||
ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSectionKind, ObjSymbol,
|
||||
SymbolRef,
|
||||
},
|
||||
};
|
||||
use time::format_description;
|
||||
|
||||
use crate::views::{
|
||||
appearance::Appearance,
|
||||
column_layout::{render_header, render_strips, render_table},
|
||||
symbol_diff::{
|
||||
match_color_for_symbol, symbol_list_ui, DiffViewAction, DiffViewNavigation, DiffViewState,
|
||||
SymbolDiffContext, SymbolFilter, SymbolRefByName, SymbolViewState, View,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FunctionViewState {
|
||||
left_highlight: HighlightKind,
|
||||
right_highlight: HighlightKind,
|
||||
}
|
||||
|
||||
impl FunctionViewState {
|
||||
pub fn highlight(&self, column: usize) -> &HighlightKind {
|
||||
match column {
|
||||
0 => &self.left_highlight,
|
||||
1 => &self.right_highlight,
|
||||
_ => &HighlightKind::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_highlight(&mut self, column: usize, highlight: HighlightKind) {
|
||||
match column {
|
||||
0 => {
|
||||
if highlight == self.left_highlight {
|
||||
if highlight == self.right_highlight {
|
||||
self.left_highlight = HighlightKind::None;
|
||||
self.right_highlight = HighlightKind::None;
|
||||
} else {
|
||||
self.right_highlight = self.left_highlight.clone();
|
||||
}
|
||||
} else {
|
||||
self.left_highlight = highlight;
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
if highlight == self.right_highlight {
|
||||
if highlight == self.left_highlight {
|
||||
self.left_highlight = HighlightKind::None;
|
||||
self.right_highlight = HighlightKind::None;
|
||||
} else {
|
||||
self.left_highlight = self.right_highlight.clone();
|
||||
}
|
||||
} else {
|
||||
self.right_highlight = highlight;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_highlight(&mut self) {
|
||||
self.left_highlight = HighlightKind::None;
|
||||
self.right_highlight = HighlightKind::None;
|
||||
}
|
||||
}
|
||||
|
||||
fn ins_hover_ui(
|
||||
ui: &mut egui::Ui,
|
||||
obj: &ObjInfo,
|
||||
section: &ObjSection,
|
||||
ins: &ObjIns,
|
||||
symbol: &ObjSymbol,
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||
|
||||
let offset = ins.address - section.address;
|
||||
ui.label(format!(
|
||||
"{:02x?}",
|
||||
§ion.data[offset as usize..(offset + ins.size as u64) as usize]
|
||||
));
|
||||
|
||||
if let Some(virtual_address) = symbol.virtual_address {
|
||||
let offset = ins.address - symbol.address;
|
||||
ui.colored_label(
|
||||
appearance.replace_color,
|
||||
format!("Virtual address: {:#x}", virtual_address + offset),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(orig) = &ins.orig {
|
||||
ui.label(format!("Original: {}", orig));
|
||||
}
|
||||
|
||||
for arg in &ins.args {
|
||||
if let ObjInsArg::Arg(arg) = arg {
|
||||
match arg {
|
||||
ObjInsArgValue::Signed(v) => {
|
||||
ui.label(format!("{arg} == {v}"));
|
||||
}
|
||||
ObjInsArgValue::Unsigned(v) => {
|
||||
ui.label(format!("{arg} == {v}"));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(reloc) = &ins.reloc {
|
||||
ui.label(format!("Relocation type: {}", obj.arch.display_reloc(reloc.flags)));
|
||||
ui.colored_label(appearance.highlight_color, format!("Name: {}", reloc.target.name));
|
||||
if let Some(orig_section_index) = reloc.target.orig_section_index {
|
||||
if let Some(section) =
|
||||
obj.sections.iter().find(|s| s.orig_index == orig_section_index)
|
||||
{
|
||||
ui.colored_label(
|
||||
appearance.highlight_color,
|
||||
format!("Section: {}", section.name),
|
||||
);
|
||||
}
|
||||
ui.colored_label(
|
||||
appearance.highlight_color,
|
||||
format!("Address: {:x}", reloc.target.address),
|
||||
);
|
||||
ui.colored_label(
|
||||
appearance.highlight_color,
|
||||
format!("Size: {:x}", reloc.target.size),
|
||||
);
|
||||
if let Some(s) = obj
|
||||
.arch
|
||||
.guess_data_type(ins)
|
||||
.and_then(|ty| obj.arch.display_data_type(ty, &reloc.target.bytes))
|
||||
{
|
||||
ui.colored_label(appearance.highlight_color, s);
|
||||
}
|
||||
} else {
|
||||
ui.colored_label(appearance.highlight_color, "Extern".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(demangled) = rlwinmdec::decode(&ins.formatted) {
|
||||
ui.colored_label(appearance.highlight_color, demangled.trim());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn ins_context_menu(ui: &mut egui::Ui, section: &ObjSection, ins: &ObjIns, symbol: &ObjSymbol) {
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||
|
||||
if ui.button(format!("Copy \"{}\"", ins.formatted)).clicked() {
|
||||
ui.output_mut(|output| output.copied_text.clone_from(&ins.formatted));
|
||||
ui.close_menu();
|
||||
}
|
||||
|
||||
let mut hex_string = "0x".to_string();
|
||||
for byte in §ion.data[ins.address as usize..(ins.address + ins.size as u64) as usize] {
|
||||
hex_string.push_str(&format!("{:02x}", byte));
|
||||
}
|
||||
if ui.button(format!("Copy \"{hex_string}\" (instruction bytes)")).clicked() {
|
||||
ui.output_mut(|output| output.copied_text = hex_string);
|
||||
ui.close_menu();
|
||||
}
|
||||
|
||||
if let Some(virtual_address) = symbol.virtual_address {
|
||||
let offset = ins.address - symbol.address;
|
||||
let offset_string = format!("{:#x}", virtual_address + offset);
|
||||
if ui.button(format!("Copy \"{offset_string}\" (virtual address)")).clicked() {
|
||||
ui.output_mut(|output| output.copied_text = offset_string);
|
||||
ui.close_menu();
|
||||
}
|
||||
}
|
||||
|
||||
for arg in &ins.args {
|
||||
if let ObjInsArg::Arg(arg) = arg {
|
||||
match arg {
|
||||
ObjInsArgValue::Signed(v) => {
|
||||
if ui.button(format!("Copy \"{arg}\"")).clicked() {
|
||||
ui.output_mut(|output| output.copied_text = arg.to_string());
|
||||
ui.close_menu();
|
||||
}
|
||||
if ui.button(format!("Copy \"{v}\"")).clicked() {
|
||||
ui.output_mut(|output| output.copied_text = v.to_string());
|
||||
ui.close_menu();
|
||||
}
|
||||
}
|
||||
ObjInsArgValue::Unsigned(v) => {
|
||||
if ui.button(format!("Copy \"{arg}\"")).clicked() {
|
||||
ui.output_mut(|output| output.copied_text = arg.to_string());
|
||||
ui.close_menu();
|
||||
}
|
||||
if ui.button(format!("Copy \"{v}\"")).clicked() {
|
||||
ui.output_mut(|output| output.copied_text = v.to_string());
|
||||
ui.close_menu();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(reloc) = &ins.reloc {
|
||||
if let Some(name) = &reloc.target.demangled_name {
|
||||
if ui.button(format!("Copy \"{name}\"")).clicked() {
|
||||
ui.output_mut(|output| output.copied_text.clone_from(name));
|
||||
ui.close_menu();
|
||||
}
|
||||
}
|
||||
if ui.button(format!("Copy \"{}\"", reloc.target.name)).clicked() {
|
||||
ui.output_mut(|output| output.copied_text.clone_from(&reloc.target.name));
|
||||
ui.close_menu();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<SymbolRef> {
|
||||
for (section_idx, section) in obj.sections.iter().enumerate() {
|
||||
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
|
||||
if symbol.name == selected_symbol.symbol_name {
|
||||
return Some(SymbolRef { section_idx, symbol_idx });
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
fn diff_text_ui(
|
||||
ui: &mut egui::Ui,
|
||||
text: DiffText<'_>,
|
||||
ins_diff: &ObjInsDiff,
|
||||
appearance: &Appearance,
|
||||
ins_view_state: &FunctionViewState,
|
||||
column: usize,
|
||||
space_width: f32,
|
||||
response_cb: impl Fn(Response) -> Response,
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
let label_text;
|
||||
let mut base_color = match ins_diff.kind {
|
||||
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
|
||||
appearance.text_color
|
||||
}
|
||||
ObjInsDiffKind::Replace => appearance.replace_color,
|
||||
ObjInsDiffKind::Delete => appearance.delete_color,
|
||||
ObjInsDiffKind::Insert => appearance.insert_color,
|
||||
};
|
||||
let mut pad_to = 0;
|
||||
match text {
|
||||
DiffText::Basic(text) => {
|
||||
label_text = text.to_string();
|
||||
}
|
||||
DiffText::BasicColor(s, idx) => {
|
||||
label_text = s.to_string();
|
||||
base_color = appearance.diff_colors[idx % appearance.diff_colors.len()];
|
||||
}
|
||||
DiffText::Line(num) => {
|
||||
label_text = num.to_string();
|
||||
base_color = appearance.deemphasized_text_color;
|
||||
pad_to = 5;
|
||||
}
|
||||
DiffText::Address(addr) => {
|
||||
label_text = format!("{:x}:", addr);
|
||||
pad_to = 5;
|
||||
}
|
||||
DiffText::Opcode(mnemonic, _op) => {
|
||||
label_text = mnemonic.to_string();
|
||||
if ins_diff.kind == ObjInsDiffKind::OpMismatch {
|
||||
base_color = appearance.replace_color;
|
||||
}
|
||||
pad_to = 8;
|
||||
}
|
||||
DiffText::Argument(arg, diff) => {
|
||||
label_text = arg.to_string();
|
||||
if let Some(diff) = diff {
|
||||
base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
|
||||
}
|
||||
}
|
||||
DiffText::BranchDest(addr, diff) => {
|
||||
label_text = format!("{addr:x}");
|
||||
if let Some(diff) = diff {
|
||||
base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
|
||||
}
|
||||
}
|
||||
DiffText::Symbol(sym, diff) => {
|
||||
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
|
||||
label_text = name.clone();
|
||||
if let Some(diff) = diff {
|
||||
base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
|
||||
} else {
|
||||
base_color = appearance.emphasized_text_color;
|
||||
}
|
||||
}
|
||||
DiffText::Spacing(n) => {
|
||||
ui.add_space(n as f32 * space_width);
|
||||
return ret;
|
||||
}
|
||||
DiffText::Eol => {
|
||||
label_text = "\n".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
let len = label_text.len();
|
||||
let highlight = *ins_view_state.highlight(column) == text;
|
||||
let mut response = Label::new(LayoutJob::single_section(
|
||||
label_text,
|
||||
appearance.code_text_format(base_color, highlight),
|
||||
))
|
||||
.sense(Sense::click())
|
||||
.ui(ui);
|
||||
response = response_cb(response);
|
||||
if response.clicked() {
|
||||
ret = Some(DiffViewAction::SetDiffHighlight(column, text.into()));
|
||||
}
|
||||
if len < pad_to {
|
||||
ui.add_space((pad_to - len) as f32 * space_width);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn asm_row_ui(
|
||||
ui: &mut egui::Ui,
|
||||
ins_diff: &ObjInsDiff,
|
||||
symbol: &ObjSymbol,
|
||||
appearance: &Appearance,
|
||||
ins_view_state: &FunctionViewState,
|
||||
column: usize,
|
||||
response_cb: impl Fn(Response) -> Response,
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||
if ins_diff.kind != ObjInsDiffKind::None {
|
||||
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
|
||||
}
|
||||
let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' '));
|
||||
display_diff(ins_diff, symbol.address, |text| {
|
||||
if let Some(action) = diff_text_ui(
|
||||
ui,
|
||||
text,
|
||||
ins_diff,
|
||||
appearance,
|
||||
ins_view_state,
|
||||
column,
|
||||
space_width,
|
||||
&response_cb,
|
||||
) {
|
||||
ret = Some(action);
|
||||
}
|
||||
Ok::<_, ()>(())
|
||||
})
|
||||
.unwrap();
|
||||
ret
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn asm_col_ui(
|
||||
row: &mut TableRow<'_, '_>,
|
||||
ctx: FunctionDiffContext<'_>,
|
||||
appearance: &Appearance,
|
||||
ins_view_state: &FunctionViewState,
|
||||
column: usize,
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
let symbol_ref = ctx.symbol_ref?;
|
||||
let (section, symbol) = ctx.obj.section_symbol(symbol_ref);
|
||||
let section = section?;
|
||||
let ins_diff = &ctx.diff.symbol_diff(symbol_ref).instructions[row.index()];
|
||||
let response_cb = |response: Response| {
|
||||
if let Some(ins) = &ins_diff.ins {
|
||||
response.context_menu(|ui| ins_context_menu(ui, section, ins, symbol));
|
||||
response.on_hover_ui_at_pointer(|ui| {
|
||||
ins_hover_ui(ui, ctx.obj, section, ins, symbol, appearance)
|
||||
})
|
||||
} else {
|
||||
response
|
||||
}
|
||||
};
|
||||
let (_, response) = row.col(|ui| {
|
||||
if let Some(action) =
|
||||
asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state, column, response_cb)
|
||||
{
|
||||
ret = Some(action);
|
||||
}
|
||||
});
|
||||
response_cb(response);
|
||||
ret
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn asm_table_ui(
|
||||
ui: &mut egui::Ui,
|
||||
available_width: f32,
|
||||
left_ctx: Option<FunctionDiffContext<'_>>,
|
||||
right_ctx: Option<FunctionDiffContext<'_>>,
|
||||
appearance: &Appearance,
|
||||
ins_view_state: &FunctionViewState,
|
||||
symbol_state: &SymbolViewState,
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
let left_len = left_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len())
|
||||
});
|
||||
let right_len = right_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len())
|
||||
});
|
||||
let instructions_len = match (left_len, right_len) {
|
||||
(Some(left_len), Some(right_len)) => {
|
||||
if left_len != right_len {
|
||||
ui.label("Instruction count mismatch");
|
||||
return None;
|
||||
}
|
||||
left_len
|
||||
}
|
||||
(Some(left_len), None) => left_len,
|
||||
(None, Some(right_len)) => right_len,
|
||||
(None, None) => {
|
||||
ui.label("No symbol selected");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if left_len.is_some() && right_len.is_some() {
|
||||
// Joint view
|
||||
render_table(
|
||||
ui,
|
||||
available_width,
|
||||
2,
|
||||
appearance.code_font.size,
|
||||
instructions_len,
|
||||
|row, column| {
|
||||
if column == 0 {
|
||||
if let Some(ctx) = left_ctx {
|
||||
if let Some(action) =
|
||||
asm_col_ui(row, ctx, appearance, ins_view_state, column)
|
||||
{
|
||||
ret = Some(action);
|
||||
}
|
||||
}
|
||||
} else if column == 1 {
|
||||
if let Some(ctx) = right_ctx {
|
||||
if let Some(action) =
|
||||
asm_col_ui(row, ctx, appearance, ins_view_state, column)
|
||||
{
|
||||
ret = Some(action);
|
||||
}
|
||||
}
|
||||
if row.response().clicked() {
|
||||
ret = Some(DiffViewAction::ClearDiffHighlight);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// Split view, one side is the symbol list
|
||||
render_strips(ui, available_width, 2, |ui, column| {
|
||||
if column == 0 {
|
||||
if let Some(ctx) = left_ctx {
|
||||
if ctx.has_symbol() {
|
||||
render_table(
|
||||
ui,
|
||||
available_width / 2.0,
|
||||
1,
|
||||
appearance.code_font.size,
|
||||
instructions_len,
|
||||
|row, column| {
|
||||
if let Some(action) =
|
||||
asm_col_ui(row, ctx, appearance, ins_view_state, column)
|
||||
{
|
||||
ret = Some(action);
|
||||
}
|
||||
if row.response().clicked() {
|
||||
ret = Some(DiffViewAction::ClearDiffHighlight);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else if let Some((right_ctx, right_symbol_ref)) =
|
||||
right_ctx.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| (ctx, symbol_ref)))
|
||||
{
|
||||
if let Some(action) = symbol_list_ui(
|
||||
ui,
|
||||
SymbolDiffContext { obj: ctx.obj, diff: ctx.diff },
|
||||
None,
|
||||
symbol_state,
|
||||
SymbolFilter::Mapping(right_symbol_ref),
|
||||
appearance,
|
||||
column,
|
||||
) {
|
||||
match action {
|
||||
DiffViewAction::Navigate(DiffViewNavigation {
|
||||
left_symbol: Some(left_symbol_ref),
|
||||
..
|
||||
}) => {
|
||||
let (right_section, right_symbol) =
|
||||
right_ctx.obj.section_symbol(right_symbol_ref);
|
||||
ret = Some(DiffViewAction::SetMapping(
|
||||
match right_section.map(|s| s.kind) {
|
||||
Some(ObjSectionKind::Code) => View::FunctionDiff,
|
||||
_ => View::SymbolDiff,
|
||||
},
|
||||
left_symbol_ref,
|
||||
SymbolRefByName::new(right_symbol, right_section),
|
||||
));
|
||||
}
|
||||
DiffViewAction::SetSymbolHighlight(_, _) => {
|
||||
// Ignore
|
||||
}
|
||||
_ => {
|
||||
ret = Some(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ui.label("No left object");
|
||||
}
|
||||
} else if column == 1 {
|
||||
if let Some(ctx) = right_ctx {
|
||||
if ctx.has_symbol() {
|
||||
render_table(
|
||||
ui,
|
||||
available_width / 2.0,
|
||||
1,
|
||||
appearance.code_font.size,
|
||||
instructions_len,
|
||||
|row, column| {
|
||||
if let Some(action) =
|
||||
asm_col_ui(row, ctx, appearance, ins_view_state, column)
|
||||
{
|
||||
ret = Some(action);
|
||||
}
|
||||
if row.response().clicked() {
|
||||
ret = Some(DiffViewAction::ClearDiffHighlight);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else if let Some((left_ctx, left_symbol_ref)) =
|
||||
left_ctx.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| (ctx, symbol_ref)))
|
||||
{
|
||||
if let Some(action) = symbol_list_ui(
|
||||
ui,
|
||||
SymbolDiffContext { obj: ctx.obj, diff: ctx.diff },
|
||||
None,
|
||||
symbol_state,
|
||||
SymbolFilter::Mapping(left_symbol_ref),
|
||||
appearance,
|
||||
column,
|
||||
) {
|
||||
match action {
|
||||
DiffViewAction::Navigate(DiffViewNavigation {
|
||||
right_symbol: Some(right_symbol_ref),
|
||||
..
|
||||
}) => {
|
||||
let (left_section, left_symbol) =
|
||||
left_ctx.obj.section_symbol(left_symbol_ref);
|
||||
ret = Some(DiffViewAction::SetMapping(
|
||||
match left_section.map(|s| s.kind) {
|
||||
Some(ObjSectionKind::Code) => View::FunctionDiff,
|
||||
_ => View::SymbolDiff,
|
||||
},
|
||||
SymbolRefByName::new(left_symbol, left_section),
|
||||
right_symbol_ref,
|
||||
));
|
||||
}
|
||||
DiffViewAction::SetSymbolHighlight(_, _) => {
|
||||
// Ignore
|
||||
}
|
||||
_ => {
|
||||
ret = Some(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ui.label("No right object");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct FunctionDiffContext<'a> {
|
||||
pub obj: &'a ObjInfo,
|
||||
pub diff: &'a ObjDiff,
|
||||
pub symbol_ref: Option<SymbolRef>,
|
||||
}
|
||||
|
||||
impl<'a> FunctionDiffContext<'a> {
|
||||
pub fn new(
|
||||
obj: Option<&'a (ObjInfo, ObjDiff)>,
|
||||
selected_symbol: Option<&SymbolRefByName>,
|
||||
) -> Option<Self> {
|
||||
obj.map(|(obj, diff)| Self {
|
||||
obj,
|
||||
diff,
|
||||
symbol_ref: selected_symbol.and_then(|s| find_symbol(obj, s)),
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn has_symbol(&self) -> bool { self.symbol_ref.is_some() }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn function_diff_ui(
|
||||
ui: &mut egui::Ui,
|
||||
state: &DiffViewState,
|
||||
appearance: &Appearance,
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
let Some(result) = &state.build else {
|
||||
return ret;
|
||||
};
|
||||
|
||||
let mut left_ctx = FunctionDiffContext::new(
|
||||
result.first_obj.as_ref(),
|
||||
state.symbol_state.left_symbol.as_ref(),
|
||||
);
|
||||
let mut right_ctx = FunctionDiffContext::new(
|
||||
result.second_obj.as_ref(),
|
||||
state.symbol_state.right_symbol.as_ref(),
|
||||
);
|
||||
|
||||
// If one side is missing a symbol, but the diff process found a match, use that symbol
|
||||
let left_diff_symbol = left_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
|
||||
});
|
||||
let right_diff_symbol = right_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
|
||||
});
|
||||
if left_diff_symbol.is_some() && right_ctx.is_some_and(|ctx| !ctx.has_symbol()) {
|
||||
let (right_section, right_symbol) =
|
||||
right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap());
|
||||
let symbol_ref = SymbolRefByName::new(right_symbol, right_section);
|
||||
right_ctx = FunctionDiffContext::new(result.second_obj.as_ref(), Some(&symbol_ref));
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
|
||||
view: Some(View::FunctionDiff),
|
||||
left_symbol: state.symbol_state.left_symbol.clone(),
|
||||
right_symbol: Some(symbol_ref),
|
||||
}));
|
||||
} else if right_diff_symbol.is_some() && left_ctx.is_some_and(|ctx| !ctx.has_symbol()) {
|
||||
let (left_section, left_symbol) =
|
||||
left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap());
|
||||
let symbol_ref = SymbolRefByName::new(left_symbol, left_section);
|
||||
left_ctx = FunctionDiffContext::new(result.first_obj.as_ref(), Some(&symbol_ref));
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
|
||||
view: Some(View::FunctionDiff),
|
||||
left_symbol: Some(symbol_ref),
|
||||
right_symbol: state.symbol_state.right_symbol.clone(),
|
||||
}));
|
||||
}
|
||||
|
||||
// If both sides are missing a symbol, switch to symbol diff view
|
||||
if right_ctx.is_some_and(|ctx| !ctx.has_symbol())
|
||||
&& left_ctx.is_some_and(|ctx| !ctx.has_symbol())
|
||||
{
|
||||
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
|
||||
// Header
|
||||
let available_width = ui.available_width();
|
||||
render_header(ui, available_width, 2, |ui, column| {
|
||||
if column == 0 {
|
||||
// Left column
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("⏴ Back").clicked() {
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
ui.separator();
|
||||
if ui
|
||||
.add_enabled(
|
||||
!state.scratch_running
|
||||
&& state.scratch_available
|
||||
&& left_ctx.is_some_and(|ctx| ctx.has_symbol()),
|
||||
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()
|
||||
{
|
||||
if let Some((_section, symbol)) = left_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
|
||||
}) {
|
||||
ret = Some(DiffViewAction::CreateScratch(symbol.name.clone()));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some((_section, symbol)) = left_ctx
|
||||
.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)))
|
||||
{
|
||||
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
|
||||
ui.label(
|
||||
RichText::new(name)
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
if right_ctx.is_some_and(|m| m.has_symbol())
|
||||
&& ui
|
||||
.button("Change target")
|
||||
.on_hover_text_at_pointer("Choose a different symbol to use as the target")
|
||||
.clicked()
|
||||
{
|
||||
if let Some(symbol_ref) = state.symbol_state.right_symbol.as_ref() {
|
||||
ret = Some(DiffViewAction::SelectingLeft(symbol_ref.clone()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Missing")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.replace_color),
|
||||
);
|
||||
ui.label(
|
||||
RichText::new("Choose target symbol")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
}
|
||||
} else if column == 1 {
|
||||
// Right column
|
||||
ui.horizontal(|ui| {
|
||||
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
|
||||
ret = Some(DiffViewAction::Build);
|
||||
}
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
if state.build_running {
|
||||
ui.colored_label(appearance.replace_color, "Building…");
|
||||
} else {
|
||||
ui.label("Last built:");
|
||||
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||
ui.label(
|
||||
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
|
||||
);
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
if ui
|
||||
.add_enabled(state.source_path_available, egui::Button::new("🖹 Source file"))
|
||||
.on_hover_text_at_pointer("Open the source file in the default editor")
|
||||
.on_disabled_hover_text("Source file metadata missing")
|
||||
.clicked()
|
||||
{
|
||||
ret = Some(DiffViewAction::OpenSourcePath);
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.map(|symbol_ref| {
|
||||
(ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref))
|
||||
})
|
||||
}) {
|
||||
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
|
||||
ui.label(
|
||||
RichText::new(name)
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
ui.horizontal(|ui| {
|
||||
if let Some(match_percent) = symbol_diff.match_percent {
|
||||
ui.label(
|
||||
RichText::new(format!("{:.0}%", match_percent.floor()))
|
||||
.font(appearance.code_font.clone())
|
||||
.color(match_color_for_symbol(match_percent, appearance)),
|
||||
);
|
||||
}
|
||||
if left_ctx.is_some_and(|m| m.has_symbol()) {
|
||||
ui.separator();
|
||||
if ui
|
||||
.button("Change base")
|
||||
.on_hover_text_at_pointer(
|
||||
"Choose a different symbol to use as the base",
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
if let Some(symbol_ref) = state.symbol_state.left_symbol.as_ref() {
|
||||
ret = Some(DiffViewAction::SelectingRight(symbol_ref.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Missing")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.replace_color),
|
||||
);
|
||||
ui.label(
|
||||
RichText::new("Choose base symbol")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Table
|
||||
let id = Id::new(state.symbol_state.left_symbol.as_ref().map(|s| s.symbol_name.as_str()))
|
||||
.with(state.symbol_state.right_symbol.as_ref().map(|s| s.symbol_name.as_str()));
|
||||
if let Some(action) = ui
|
||||
.push_id(id, |ui| {
|
||||
asm_table_ui(
|
||||
ui,
|
||||
available_width,
|
||||
left_ctx,
|
||||
right_ctx,
|
||||
appearance,
|
||||
&state.function_state,
|
||||
&state.symbol_state,
|
||||
)
|
||||
})
|
||||
.inner
|
||||
{
|
||||
ret = Some(action);
|
||||
}
|
||||
ret
|
||||
}
|
||||
159
objdiff-gui/src/views/graphics.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufReader, BufWriter},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use egui::{text::LayoutJob, Context, FontId, RichText, TextFormat, TextStyle, Window};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{EnumIter, EnumMessage, IntoEnumIterator};
|
||||
|
||||
use crate::views::{appearance::Appearance, frame_history::FrameHistory};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GraphicsViewState {
|
||||
pub active_backend: String,
|
||||
pub active_device: String,
|
||||
pub graphics_config: GraphicsConfig,
|
||||
pub graphics_config_path: Option<PathBuf>,
|
||||
pub should_relaunch: bool,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Default, PartialEq, Eq, EnumIter, EnumMessage, Serialize, Deserialize,
|
||||
)]
|
||||
pub enum GraphicsBackend {
|
||||
#[default]
|
||||
#[strum(message = "Auto")]
|
||||
Auto,
|
||||
#[strum(message = "Vulkan")]
|
||||
Vulkan,
|
||||
#[strum(message = "Metal")]
|
||||
Metal,
|
||||
#[strum(message = "DirectX 12")]
|
||||
Dx12,
|
||||
#[strum(message = "OpenGL")]
|
||||
OpenGL,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||
pub struct GraphicsConfig {
|
||||
#[serde(default)]
|
||||
pub desired_backend: GraphicsBackend,
|
||||
}
|
||||
|
||||
pub fn load_graphics_config(path: &Path) -> Result<Option<GraphicsConfig>> {
|
||||
if !path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
let file = BufReader::new(File::open(path)?);
|
||||
let config: GraphicsConfig = ron::de::from_reader(file)?;
|
||||
Ok(Some(config))
|
||||
}
|
||||
|
||||
pub fn save_graphics_config(path: &Path, config: &GraphicsConfig) -> Result<()> {
|
||||
let file = BufWriter::new(File::create(path)?);
|
||||
ron::ser::to_writer(file, config)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl GraphicsBackend {
|
||||
pub fn is_supported(&self) -> bool {
|
||||
match self {
|
||||
GraphicsBackend::Auto => true,
|
||||
GraphicsBackend::Vulkan => {
|
||||
cfg!(all(feature = "wgpu", any(target_os = "windows", target_os = "linux")))
|
||||
}
|
||||
GraphicsBackend::Metal => cfg!(all(feature = "wgpu", target_os = "macos")),
|
||||
GraphicsBackend::Dx12 => cfg!(all(feature = "wgpu", target_os = "windows")),
|
||||
GraphicsBackend::OpenGL => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn graphics_window(
|
||||
ctx: &Context,
|
||||
show: &mut bool,
|
||||
frame_history: &mut FrameHistory,
|
||||
state: &mut GraphicsViewState,
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
Window::new("Graphics").open(show).show(ctx, |ui| {
|
||||
ui.label("Graphics backend:");
|
||||
ui.label(
|
||||
RichText::new(&state.active_backend)
|
||||
.color(appearance.emphasized_text_color)
|
||||
.text_style(TextStyle::Monospace),
|
||||
);
|
||||
ui.label("Graphics device:");
|
||||
ui.label(
|
||||
RichText::new(&state.active_device)
|
||||
.color(appearance.emphasized_text_color)
|
||||
.text_style(TextStyle::Monospace),
|
||||
);
|
||||
ui.label(format!("FPS: {:.1}", frame_history.fps()));
|
||||
|
||||
ui.separator();
|
||||
let mut job = LayoutJob::default();
|
||||
job.append(
|
||||
"WARNING: ",
|
||||
0.0,
|
||||
TextFormat::simple(appearance.ui_font.clone(), appearance.delete_color),
|
||||
);
|
||||
job.append(
|
||||
"Changing the graphics backend may cause the application\nto no longer start or display correctly. Use with caution!",
|
||||
0.0,
|
||||
TextFormat::simple(appearance.ui_font.clone(), appearance.emphasized_text_color),
|
||||
);
|
||||
if let Some(config_path) = &state.graphics_config_path {
|
||||
job.append(
|
||||
"\n\nDelete the following file to reset:\n",
|
||||
0.0,
|
||||
TextFormat::simple(appearance.ui_font.clone(), appearance.emphasized_text_color),
|
||||
);
|
||||
job.append(
|
||||
config_path.to_string_lossy().as_ref(),
|
||||
0.0,
|
||||
TextFormat::simple(
|
||||
FontId {
|
||||
family: appearance.code_font.family.clone(),
|
||||
size: appearance.ui_font.size,
|
||||
},
|
||||
appearance.emphasized_text_color,
|
||||
),
|
||||
);
|
||||
}
|
||||
job.append(
|
||||
"\n\nChanging the graphics backend will restart the application.",
|
||||
0.0,
|
||||
TextFormat::simple(appearance.ui_font.clone(), appearance.replace_color),
|
||||
);
|
||||
ui.label(job);
|
||||
|
||||
ui.add_enabled_ui(state.graphics_config_path.is_some(), |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Desired backend:");
|
||||
for backend in GraphicsBackend::iter().filter(GraphicsBackend::is_supported) {
|
||||
let selected = state.graphics_config.desired_backend == backend;
|
||||
if ui.selectable_label(selected, backend.get_message().unwrap()).clicked() {
|
||||
let prev_backend = state.graphics_config.desired_backend;
|
||||
state.graphics_config.desired_backend = backend;
|
||||
match save_graphics_config(
|
||||
state.graphics_config_path.as_ref().unwrap(),
|
||||
&state.graphics_config,
|
||||
) {
|
||||
Ok(()) => {
|
||||
state.should_relaunch = true;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to save graphics config: {:?}", e);
|
||||
state.graphics_config.desired_backend = prev_backend;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
162
objdiff-gui/src/views/jobs.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use egui::{ProgressBar, RichText, Widget};
|
||||
|
||||
use crate::{
|
||||
jobs::{JobQueue, JobStatus},
|
||||
views::appearance::Appearance,
|
||||
};
|
||||
|
||||
pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) {
|
||||
if ui.button("Clear").clicked() {
|
||||
jobs.clear_errored();
|
||||
}
|
||||
|
||||
let mut remove_job: Option<usize> = None;
|
||||
let mut any_jobs = false;
|
||||
for job in jobs.iter_mut() {
|
||||
let Ok(status) = job.context.status.read() else {
|
||||
continue;
|
||||
};
|
||||
any_jobs = true;
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(&status.title);
|
||||
if ui.small_button("✖").clicked() {
|
||||
if job.handle.is_some() {
|
||||
if let Err(e) = job.cancel.send(()) {
|
||||
log::error!("Failed to cancel job: {e:?}");
|
||||
}
|
||||
} else {
|
||||
remove_job = Some(job.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut bar = ProgressBar::new(status.progress_percent);
|
||||
if let Some(items) = &status.progress_items {
|
||||
bar = bar.text(format!("{} / {}", items[0], items[1]));
|
||||
}
|
||||
bar.ui(ui);
|
||||
const STATUS_LENGTH: usize = 80;
|
||||
if let Some(err) = &status.error {
|
||||
let err_string = format!("{:#}", err);
|
||||
ui.colored_label(
|
||||
appearance.delete_color,
|
||||
if err_string.len() > STATUS_LENGTH - 10 {
|
||||
format!("Error: {}…", &err_string[0..STATUS_LENGTH - 10])
|
||||
} else {
|
||||
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
|
||||
},
|
||||
)
|
||||
.on_hover_text_at_pointer(RichText::new(&err_string).color(appearance.delete_color))
|
||||
.context_menu(|ui| {
|
||||
if ui.button("Copy full message").clicked() {
|
||||
ui.output_mut(|o| o.copied_text = err_string);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ui.label(if status.status.len() > STATUS_LENGTH - 3 {
|
||||
format!("{}…", &status.status[0..STATUS_LENGTH - 3])
|
||||
} else {
|
||||
format!("{:width$}", &status.status, width = STATUS_LENGTH)
|
||||
})
|
||||
.on_hover_text_at_pointer(&status.status)
|
||||
.context_menu(|ui| {
|
||||
if ui.button("Copy full message").clicked() {
|
||||
ui.output_mut(|o| o.copied_text = status.status.clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if !any_jobs {
|
||||
ui.label("No jobs");
|
||||
}
|
||||
|
||||
if let Some(idx) = remove_job {
|
||||
jobs.remove(idx);
|
||||
}
|
||||
}
|
||||
|
||||
struct JobStatusDisplay {
|
||||
title: String,
|
||||
progress_items: Option<[u32; 2]>,
|
||||
error: bool,
|
||||
}
|
||||
|
||||
impl From<&JobStatus> for JobStatusDisplay {
|
||||
fn from(status: &JobStatus) -> Self {
|
||||
Self {
|
||||
title: status.title.clone(),
|
||||
progress_items: status.progress_items,
|
||||
error: status.error.is_some(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn jobs_menu_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) -> bool {
|
||||
ui.label("Jobs:");
|
||||
let mut statuses = Vec::new();
|
||||
for job in jobs.iter_mut() {
|
||||
let Ok(status) = job.context.status.read() else {
|
||||
continue;
|
||||
};
|
||||
statuses.push(JobStatusDisplay::from(&*status));
|
||||
}
|
||||
let running_jobs = statuses.iter().filter(|s| !s.error).count();
|
||||
let error_jobs = statuses.iter().filter(|s| s.error).count();
|
||||
|
||||
let mut clicked = false;
|
||||
let spinner =
|
||||
egui::Spinner::new().size(appearance.ui_font.size * 0.9).color(appearance.text_color);
|
||||
match running_jobs.cmp(&1) {
|
||||
Ordering::Equal => {
|
||||
spinner.ui(ui);
|
||||
let running_job = statuses.iter().find(|s| !s.error).unwrap();
|
||||
let text = if let Some(items) = running_job.progress_items {
|
||||
format!("{} ({}/{})", running_job.title, items[0], items[1])
|
||||
} else {
|
||||
running_job.title.clone()
|
||||
};
|
||||
clicked |= ui.link(RichText::new(text)).clicked();
|
||||
}
|
||||
Ordering::Greater => {
|
||||
spinner.ui(ui);
|
||||
clicked |= ui.link(format!("{} running", running_jobs)).clicked();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
match error_jobs.cmp(&1) {
|
||||
Ordering::Equal => {
|
||||
let error_job = statuses.iter().find(|s| s.error).unwrap();
|
||||
clicked |= ui
|
||||
.link(
|
||||
RichText::new(format!("{} error", error_job.title))
|
||||
.color(appearance.delete_color),
|
||||
)
|
||||
.clicked();
|
||||
}
|
||||
Ordering::Greater => {
|
||||
clicked |= ui
|
||||
.link(
|
||||
RichText::new(format!("{} errors", error_jobs)).color(appearance.delete_color),
|
||||
)
|
||||
.clicked();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
if running_jobs == 0 && error_jobs == 0 {
|
||||
clicked |= ui.link("None").clicked();
|
||||
}
|
||||
clicked
|
||||
}
|
||||
|
||||
pub fn jobs_window(
|
||||
ctx: &egui::Context,
|
||||
show: &mut bool,
|
||||
jobs: &mut JobQueue,
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
egui::Window::new("Jobs").open(show).show(ctx, |ui| {
|
||||
jobs_ui(ui, jobs, appearance);
|
||||
});
|
||||
}
|
||||
21
objdiff-gui/src/views/mod.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use egui::{text::LayoutJob, Color32, FontId, TextFormat};
|
||||
|
||||
pub(crate) mod appearance;
|
||||
pub(crate) mod column_layout;
|
||||
pub(crate) mod config;
|
||||
pub(crate) mod data_diff;
|
||||
pub(crate) mod debug;
|
||||
pub(crate) mod demangle;
|
||||
pub(crate) mod extab_diff;
|
||||
pub(crate) mod file;
|
||||
pub(crate) mod frame_history;
|
||||
pub(crate) mod function_diff;
|
||||
pub(crate) mod graphics;
|
||||
pub(crate) mod jobs;
|
||||
pub(crate) mod rlwinm;
|
||||
pub(crate) mod symbol_diff;
|
||||
|
||||
#[inline]
|
||||
fn write_text(str: &str, color: Color32, job: &mut LayoutJob, font_id: FontId) {
|
||||
job.append(str, 0.0, TextFormat::simple(font_id, color));
|
||||
}
|
||||
34
objdiff-gui/src/views/rlwinm.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use egui::TextStyle;
|
||||
|
||||
use crate::views::appearance::Appearance;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RlwinmDecodeViewState {
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
pub fn rlwinm_decode_window(
|
||||
ctx: &egui::Context,
|
||||
show: &mut bool,
|
||||
state: &mut RlwinmDecodeViewState,
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
egui::Window::new("Rlwinm Decoder").open(show).show(ctx, |ui| {
|
||||
ui.text_edit_singleline(&mut state.text);
|
||||
ui.add_space(10.0);
|
||||
if let Some(demangled) = rlwinmdec::decode(&state.text) {
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
|
||||
ui.colored_label(appearance.replace_color, demangled.trim());
|
||||
});
|
||||
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]");
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
938
objdiff-gui/src/views/symbol_diff.rs
Normal file
@@ -0,0 +1,938 @@
|
||||
use std::{collections::BTreeMap, mem::take};
|
||||
|
||||
use egui::{
|
||||
text::LayoutJob, CollapsingHeader, Color32, Id, OpenUrl, ScrollArea, SelectableLabel, TextEdit,
|
||||
Ui, Widget,
|
||||
};
|
||||
use objdiff_core::{
|
||||
arch::ObjArch,
|
||||
diff::{display::HighlightKind, ObjDiff, ObjSymbolDiff},
|
||||
obj::{
|
||||
ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags, SymbolRef, SECTION_COMMON,
|
||||
},
|
||||
};
|
||||
use regex::{Regex, RegexBuilder};
|
||||
|
||||
use crate::{
|
||||
app::AppStateRef,
|
||||
jobs::{
|
||||
create_scratch::{start_create_scratch, CreateScratchConfig, CreateScratchResult},
|
||||
objdiff::{BuildStatus, ObjDiffResult},
|
||||
Job, JobQueue, JobResult,
|
||||
},
|
||||
views::{
|
||||
appearance::Appearance,
|
||||
column_layout::{render_header, render_strips},
|
||||
function_diff::FunctionViewState,
|
||||
write_text,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SymbolRefByName {
|
||||
pub symbol_name: String,
|
||||
pub section_name: Option<String>,
|
||||
}
|
||||
|
||||
impl SymbolRefByName {
|
||||
pub fn new(symbol: &ObjSymbol, section: Option<&ObjSection>) -> Self {
|
||||
Self { symbol_name: symbol.name.clone(), section_name: section.map(|s| s.name.clone()) }
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(clippy::enum_variant_names)]
|
||||
#[derive(Debug, Default, Eq, PartialEq, Copy, Clone, Hash)]
|
||||
pub enum View {
|
||||
#[default]
|
||||
SymbolDiff,
|
||||
FunctionDiff,
|
||||
DataDiff,
|
||||
ExtabDiff,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DiffViewAction {
|
||||
/// Queue a rebuild of the current object(s)
|
||||
Build,
|
||||
/// Navigate to a new diff view
|
||||
Navigate(DiffViewNavigation),
|
||||
/// Set the highlighted symbols in the symbols view
|
||||
SetSymbolHighlight(Option<SymbolRef>, Option<SymbolRef>),
|
||||
/// Set the symbols view search filter
|
||||
SetSearch(String),
|
||||
/// Submit the current function to decomp.me
|
||||
CreateScratch(String),
|
||||
/// Open the source path of the current object
|
||||
OpenSourcePath,
|
||||
/// Set the highlight for a diff column
|
||||
SetDiffHighlight(usize, HighlightKind),
|
||||
/// Clear the highlight for all diff columns
|
||||
ClearDiffHighlight,
|
||||
/// Start selecting a left symbol for mapping.
|
||||
/// The symbol reference is the right symbol to map to.
|
||||
SelectingLeft(SymbolRefByName),
|
||||
/// Start selecting a right symbol for mapping.
|
||||
/// The symbol reference is the left symbol to map to.
|
||||
SelectingRight(SymbolRefByName),
|
||||
/// Set a symbol mapping.
|
||||
SetMapping(View, SymbolRefByName, SymbolRefByName),
|
||||
/// Set the show_mapped_symbols flag
|
||||
SetShowMappedSymbols(bool),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DiffViewNavigation {
|
||||
pub view: Option<View>,
|
||||
pub left_symbol: Option<SymbolRefByName>,
|
||||
pub right_symbol: Option<SymbolRefByName>,
|
||||
}
|
||||
|
||||
impl DiffViewNavigation {
|
||||
pub fn symbol_diff() -> Self {
|
||||
Self { view: Some(View::SymbolDiff), left_symbol: None, right_symbol: None }
|
||||
}
|
||||
|
||||
pub fn with_symbols(
|
||||
view: View,
|
||||
other_ctx: Option<SymbolDiffContext<'_>>,
|
||||
symbol: &ObjSymbol,
|
||||
section: &ObjSection,
|
||||
symbol_diff: &ObjSymbolDiff,
|
||||
column: usize,
|
||||
) -> Self {
|
||||
let symbol1 = Some(SymbolRefByName::new(symbol, Some(section)));
|
||||
let symbol2 = symbol_diff.target_symbol.and_then(|symbol_ref| {
|
||||
other_ctx.map(|ctx| {
|
||||
let (section, symbol) = ctx.obj.section_symbol(symbol_ref);
|
||||
SymbolRefByName::new(symbol, section)
|
||||
})
|
||||
});
|
||||
match column {
|
||||
0 => Self { view: Some(view), left_symbol: symbol1, right_symbol: symbol2 },
|
||||
1 => Self { view: Some(view), left_symbol: symbol2, right_symbol: symbol1 },
|
||||
_ => unreachable!("Invalid column index"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 search_regex: Option<Regex>,
|
||||
pub build_running: bool,
|
||||
pub scratch_available: bool,
|
||||
pub scratch_running: bool,
|
||||
pub source_path_available: bool,
|
||||
pub post_build_nav: Option<DiffViewNavigation>,
|
||||
pub object_name: String,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SymbolViewState {
|
||||
pub highlighted_symbol: (Option<SymbolRef>, Option<SymbolRef>),
|
||||
pub left_symbol: Option<SymbolRefByName>,
|
||||
pub right_symbol: Option<SymbolRefByName>,
|
||||
pub reverse_fn_order: bool,
|
||||
pub disable_reverse_fn_order: bool,
|
||||
pub show_hidden_symbols: bool,
|
||||
pub show_mapped_symbols: bool,
|
||||
}
|
||||
|
||||
impl DiffViewState {
|
||||
pub fn pre_update(&mut self, jobs: &mut JobQueue, state: &AppStateRef) {
|
||||
jobs.results.retain_mut(|result| match result {
|
||||
JobResult::ObjDiff(result) => {
|
||||
self.build = take(result);
|
||||
|
||||
// TODO: where should this go?
|
||||
if let Some(result) = self.post_build_nav.take() {
|
||||
if let Some(view) = result.view {
|
||||
self.current_view = view;
|
||||
}
|
||||
self.symbol_state.left_symbol = result.left_symbol;
|
||||
self.symbol_state.right_symbol = result.right_symbol;
|
||||
}
|
||||
|
||||
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(state) = state.read() {
|
||||
if let Some(obj_config) = &state.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.source_path_available = obj_config.source_path.is_some();
|
||||
} else {
|
||||
self.source_path_available = false;
|
||||
}
|
||||
self.scratch_available = CreateScratchConfig::is_available(&state.config);
|
||||
self.object_name =
|
||||
state.config.selected_obj.as_ref().map(|o| o.name.clone()).unwrap_or_default();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn post_update(
|
||||
&mut self,
|
||||
action: Option<DiffViewAction>,
|
||||
ctx: &egui::Context,
|
||||
jobs: &mut JobQueue,
|
||||
state: &AppStateRef,
|
||||
) {
|
||||
if let Some(result) = take(&mut self.scratch) {
|
||||
ctx.output_mut(|o| o.open_url = Some(OpenUrl::new_tab(result.scratch_url)));
|
||||
}
|
||||
|
||||
let Some(action) = action else {
|
||||
return;
|
||||
};
|
||||
match action {
|
||||
DiffViewAction::Build => {
|
||||
if let Ok(mut state) = state.write() {
|
||||
state.queue_build = true;
|
||||
}
|
||||
}
|
||||
DiffViewAction::Navigate(nav) => {
|
||||
if self.post_build_nav.is_some() {
|
||||
// Ignore action if we're already navigating
|
||||
return;
|
||||
}
|
||||
self.symbol_state.highlighted_symbol = (None, None);
|
||||
let Ok(mut state) = state.write() else {
|
||||
return;
|
||||
};
|
||||
if (nav.left_symbol.is_some() && nav.right_symbol.is_some())
|
||||
|| (nav.left_symbol.is_none() && nav.right_symbol.is_none())
|
||||
|| nav.view != Some(View::FunctionDiff)
|
||||
{
|
||||
// Regular navigation
|
||||
if state.is_selecting_symbol() {
|
||||
// Cancel selection and reload
|
||||
state.clear_selection();
|
||||
self.post_build_nav = Some(nav);
|
||||
} else {
|
||||
// Navigate immediately
|
||||
if let Some(view) = nav.view {
|
||||
self.current_view = view;
|
||||
}
|
||||
self.symbol_state.left_symbol = nav.left_symbol;
|
||||
self.symbol_state.right_symbol = nav.right_symbol;
|
||||
}
|
||||
} else {
|
||||
// Enter selection mode
|
||||
match (&nav.left_symbol, &nav.right_symbol) {
|
||||
(Some(left_ref), None) => {
|
||||
state.set_selecting_right(&left_ref.symbol_name);
|
||||
}
|
||||
(None, Some(right_ref)) => {
|
||||
state.set_selecting_left(&right_ref.symbol_name);
|
||||
}
|
||||
(Some(_), Some(_)) => unreachable!(),
|
||||
(None, None) => unreachable!(),
|
||||
}
|
||||
self.post_build_nav = Some(nav);
|
||||
}
|
||||
}
|
||||
DiffViewAction::SetSymbolHighlight(left, right) => {
|
||||
self.symbol_state.highlighted_symbol = (left, right);
|
||||
}
|
||||
DiffViewAction::SetSearch(search) => {
|
||||
self.search_regex = if search.is_empty() {
|
||||
None
|
||||
} else if let Ok(regex) = RegexBuilder::new(&search).case_insensitive(true).build()
|
||||
{
|
||||
Some(regex)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.search = search;
|
||||
}
|
||||
DiffViewAction::CreateScratch(function_name) => {
|
||||
let Ok(state) = state.read() else {
|
||||
return;
|
||||
};
|
||||
match CreateScratchConfig::from_config(&state.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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
DiffViewAction::OpenSourcePath => {
|
||||
let Ok(state) = state.read() else {
|
||||
return;
|
||||
};
|
||||
if let (Some(project_dir), Some(source_path)) = (
|
||||
&state.config.project_dir,
|
||||
state.config.selected_obj.as_ref().and_then(|obj| obj.source_path.as_ref()),
|
||||
) {
|
||||
let source_path = project_dir.join(source_path);
|
||||
log::info!("Opening file {}", source_path.display());
|
||||
open::that_detached(source_path).unwrap_or_else(|err| {
|
||||
log::error!("Failed to open source file: {err}");
|
||||
});
|
||||
}
|
||||
}
|
||||
DiffViewAction::SetDiffHighlight(column, kind) => {
|
||||
self.function_state.set_highlight(column, kind);
|
||||
}
|
||||
DiffViewAction::ClearDiffHighlight => {
|
||||
self.function_state.clear_highlight();
|
||||
}
|
||||
DiffViewAction::SelectingLeft(right_ref) => {
|
||||
if self.post_build_nav.is_some() {
|
||||
// Ignore action if we're already navigating
|
||||
return;
|
||||
}
|
||||
let Ok(mut state) = state.write() else {
|
||||
return;
|
||||
};
|
||||
state.set_selecting_left(&right_ref.symbol_name);
|
||||
self.post_build_nav = Some(DiffViewNavigation {
|
||||
view: Some(View::FunctionDiff),
|
||||
left_symbol: None,
|
||||
right_symbol: Some(right_ref),
|
||||
});
|
||||
}
|
||||
DiffViewAction::SelectingRight(left_ref) => {
|
||||
if self.post_build_nav.is_some() {
|
||||
// Ignore action if we're already navigating
|
||||
return;
|
||||
}
|
||||
let Ok(mut state) = state.write() else {
|
||||
return;
|
||||
};
|
||||
state.set_selecting_right(&left_ref.symbol_name);
|
||||
self.post_build_nav = Some(DiffViewNavigation {
|
||||
view: Some(View::FunctionDiff),
|
||||
left_symbol: Some(left_ref),
|
||||
right_symbol: None,
|
||||
});
|
||||
}
|
||||
DiffViewAction::SetMapping(view, left_ref, right_ref) => {
|
||||
if self.post_build_nav.is_some() {
|
||||
// Ignore action if we're already navigating
|
||||
return;
|
||||
}
|
||||
let Ok(mut state) = state.write() else {
|
||||
return;
|
||||
};
|
||||
state.set_symbol_mapping(
|
||||
left_ref.symbol_name.clone(),
|
||||
right_ref.symbol_name.clone(),
|
||||
);
|
||||
if view == View::SymbolDiff {
|
||||
self.post_build_nav = Some(DiffViewNavigation::symbol_diff());
|
||||
} else {
|
||||
self.post_build_nav = Some(DiffViewNavigation {
|
||||
view: Some(view),
|
||||
left_symbol: Some(left_ref),
|
||||
right_symbol: Some(right_ref),
|
||||
});
|
||||
}
|
||||
}
|
||||
DiffViewAction::SetShowMappedSymbols(value) => {
|
||||
self.symbol_state.show_mapped_symbols = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_color_for_symbol(match_percent: f32, appearance: &Appearance) -> Color32 {
|
||||
if match_percent == 100.0 {
|
||||
appearance.insert_color
|
||||
} else if match_percent >= 50.0 {
|
||||
appearance.replace_color
|
||||
} else {
|
||||
appearance.delete_color
|
||||
}
|
||||
}
|
||||
|
||||
fn symbol_context_menu_ui(
|
||||
ui: &mut Ui,
|
||||
ctx: SymbolDiffContext<'_>,
|
||||
other_ctx: Option<SymbolDiffContext<'_>>,
|
||||
symbol: &ObjSymbol,
|
||||
symbol_diff: &ObjSymbolDiff,
|
||||
section: Option<&ObjSection>,
|
||||
column: usize,
|
||||
) -> Option<DiffViewNavigation> {
|
||||
let mut ret = None;
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||
|
||||
if let Some(name) = &symbol.demangled_name {
|
||||
if ui.button(format!("Copy \"{name}\"")).clicked() {
|
||||
ui.output_mut(|output| output.copied_text.clone_from(name));
|
||||
ui.close_menu();
|
||||
}
|
||||
}
|
||||
if ui.button(format!("Copy \"{}\"", symbol.name)).clicked() {
|
||||
ui.output_mut(|output| output.copied_text.clone_from(&symbol.name));
|
||||
ui.close_menu();
|
||||
}
|
||||
if let Some(address) = symbol.virtual_address {
|
||||
if ui.button(format!("Copy \"{:#x}\" (virtual address)", address)).clicked() {
|
||||
ui.output_mut(|output| output.copied_text = format!("{:#x}", address));
|
||||
ui.close_menu();
|
||||
}
|
||||
}
|
||||
if let Some(section) = section {
|
||||
let has_extab =
|
||||
ctx.obj.arch.ppc().and_then(|ppc| ppc.extab_for_symbol(symbol)).is_some();
|
||||
if has_extab && ui.button("Decode exception table").clicked() {
|
||||
ret = Some(DiffViewNavigation::with_symbols(
|
||||
View::ExtabDiff,
|
||||
other_ctx,
|
||||
symbol,
|
||||
section,
|
||||
symbol_diff,
|
||||
column,
|
||||
));
|
||||
ui.close_menu();
|
||||
}
|
||||
|
||||
if ui.button("Map symbol").clicked() {
|
||||
let symbol_ref = SymbolRefByName::new(symbol, Some(section));
|
||||
if column == 0 {
|
||||
ret = Some(DiffViewNavigation {
|
||||
view: Some(View::FunctionDiff),
|
||||
left_symbol: Some(symbol_ref),
|
||||
right_symbol: None,
|
||||
});
|
||||
} else {
|
||||
ret = Some(DiffViewNavigation {
|
||||
view: Some(View::FunctionDiff),
|
||||
left_symbol: None,
|
||||
right_symbol: Some(symbol_ref),
|
||||
});
|
||||
}
|
||||
ui.close_menu();
|
||||
}
|
||||
}
|
||||
});
|
||||
ret
|
||||
}
|
||||
|
||||
fn symbol_hover_ui(ui: &mut Ui, arch: &dyn ObjArch, symbol: &ObjSymbol, appearance: &Appearance) {
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||
|
||||
ui.colored_label(appearance.highlight_color, format!("Name: {}", symbol.name));
|
||||
ui.colored_label(appearance.highlight_color, format!("Address: {:x}", symbol.address));
|
||||
if symbol.size_known {
|
||||
ui.colored_label(appearance.highlight_color, format!("Size: {:x}", symbol.size));
|
||||
} else {
|
||||
ui.colored_label(
|
||||
appearance.highlight_color,
|
||||
format!("Size: {:x} (assumed)", symbol.size),
|
||||
);
|
||||
}
|
||||
if let Some(address) = symbol.virtual_address {
|
||||
ui.colored_label(appearance.replace_color, format!("Virtual address: {:#x}", address));
|
||||
}
|
||||
if let Some(extab) = arch.ppc().and_then(|ppc| ppc.extab_for_symbol(symbol)) {
|
||||
ui.colored_label(
|
||||
appearance.highlight_color,
|
||||
format!("extab symbol: {}", &extab.etb_symbol.name),
|
||||
);
|
||||
ui.colored_label(
|
||||
appearance.highlight_color,
|
||||
format!("extabindex symbol: {}", &extab.eti_symbol.name),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
fn symbol_ui(
|
||||
ui: &mut Ui,
|
||||
ctx: SymbolDiffContext<'_>,
|
||||
other_ctx: Option<SymbolDiffContext<'_>>,
|
||||
symbol: &ObjSymbol,
|
||||
symbol_diff: &ObjSymbolDiff,
|
||||
section: Option<&ObjSection>,
|
||||
state: &SymbolViewState,
|
||||
appearance: &Appearance,
|
||||
column: usize,
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
if symbol.flags.0.contains(ObjSymbolFlags::Hidden) && !state.show_hidden_symbols {
|
||||
return ret;
|
||||
}
|
||||
let mut job = LayoutJob::default();
|
||||
let name: &str =
|
||||
if let Some(demangled) = &symbol.demangled_name { demangled } else { &symbol.name };
|
||||
let mut selected = false;
|
||||
if let Some(sym_ref) =
|
||||
if column == 0 { state.highlighted_symbol.0 } else { state.highlighted_symbol.1 }
|
||||
{
|
||||
selected = symbol_diff.symbol_ref == sym_ref;
|
||||
}
|
||||
if !symbol.flags.0.is_empty() {
|
||||
write_text("[", appearance.text_color, &mut job, appearance.code_font.clone());
|
||||
if symbol.flags.0.contains(ObjSymbolFlags::Common) {
|
||||
write_text("c", appearance.replace_color, &mut job, appearance.code_font.clone());
|
||||
} else if symbol.flags.0.contains(ObjSymbolFlags::Global) {
|
||||
write_text("g", appearance.insert_color, &mut job, appearance.code_font.clone());
|
||||
} else if symbol.flags.0.contains(ObjSymbolFlags::Local) {
|
||||
write_text("l", appearance.text_color, &mut job, appearance.code_font.clone());
|
||||
}
|
||||
if symbol.flags.0.contains(ObjSymbolFlags::Weak) {
|
||||
write_text("w", appearance.text_color, &mut job, appearance.code_font.clone());
|
||||
}
|
||||
if symbol.flags.0.contains(ObjSymbolFlags::HasExtra) {
|
||||
write_text("e", appearance.text_color, &mut job, appearance.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_diff.match_percent {
|
||||
write_text("(", appearance.text_color, &mut job, appearance.code_font.clone());
|
||||
write_text(
|
||||
&format!("{:.0}%", match_percent.floor()),
|
||||
match_color_for_symbol(match_percent, appearance),
|
||||
&mut job,
|
||||
appearance.code_font.clone(),
|
||||
);
|
||||
write_text(") ", appearance.text_color, &mut job, appearance.code_font.clone());
|
||||
}
|
||||
write_text(name, appearance.highlight_color, &mut job, appearance.code_font.clone());
|
||||
let response = SelectableLabel::new(selected, job).ui(ui).on_hover_ui_at_pointer(|ui| {
|
||||
symbol_hover_ui(ui, ctx.obj.arch.as_ref(), symbol, appearance)
|
||||
});
|
||||
response.context_menu(|ui| {
|
||||
if let Some(result) =
|
||||
symbol_context_menu_ui(ui, ctx, other_ctx, symbol, symbol_diff, section, column)
|
||||
{
|
||||
ret = Some(DiffViewAction::Navigate(result));
|
||||
}
|
||||
});
|
||||
if response.clicked() {
|
||||
if let Some(section) = section {
|
||||
match section.kind {
|
||||
ObjSectionKind::Code => {
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::with_symbols(
|
||||
View::FunctionDiff,
|
||||
other_ctx,
|
||||
symbol,
|
||||
section,
|
||||
symbol_diff,
|
||||
column,
|
||||
)));
|
||||
}
|
||||
ObjSectionKind::Data => {
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::with_symbols(
|
||||
View::DataDiff,
|
||||
other_ctx,
|
||||
symbol,
|
||||
section,
|
||||
symbol_diff,
|
||||
column,
|
||||
)));
|
||||
}
|
||||
ObjSectionKind::Bss => {}
|
||||
}
|
||||
}
|
||||
} else if response.hovered() {
|
||||
ret = Some(if let Some(target_symbol) = symbol_diff.target_symbol {
|
||||
if column == 0 {
|
||||
DiffViewAction::SetSymbolHighlight(
|
||||
Some(symbol_diff.symbol_ref),
|
||||
Some(target_symbol),
|
||||
)
|
||||
} else {
|
||||
DiffViewAction::SetSymbolHighlight(
|
||||
Some(target_symbol),
|
||||
Some(symbol_diff.symbol_ref),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
DiffViewAction::SetSymbolHighlight(None, None)
|
||||
});
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
fn symbol_matches_filter(
|
||||
symbol: &ObjSymbol,
|
||||
diff: &ObjSymbolDiff,
|
||||
filter: SymbolFilter<'_>,
|
||||
) -> bool {
|
||||
match filter {
|
||||
SymbolFilter::None => true,
|
||||
SymbolFilter::Search(regex) => {
|
||||
regex.is_match(&symbol.name)
|
||||
|| symbol.demangled_name.as_ref().map(|s| regex.is_match(s)).unwrap_or(false)
|
||||
}
|
||||
SymbolFilter::Mapping(symbol_ref) => diff.target_symbol == Some(symbol_ref),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum SymbolFilter<'a> {
|
||||
None,
|
||||
Search(&'a Regex),
|
||||
Mapping(SymbolRef),
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn symbol_list_ui(
|
||||
ui: &mut Ui,
|
||||
ctx: SymbolDiffContext<'_>,
|
||||
other_ctx: Option<SymbolDiffContext<'_>>,
|
||||
state: &SymbolViewState,
|
||||
filter: SymbolFilter<'_>,
|
||||
appearance: &Appearance,
|
||||
column: usize,
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
|
||||
let mut mapping = BTreeMap::new();
|
||||
if let SymbolFilter::Mapping(target_ref) = filter {
|
||||
let mut show_mapped_symbols = state.show_mapped_symbols;
|
||||
if ui.checkbox(&mut show_mapped_symbols, "Show mapped symbols").changed() {
|
||||
ret = Some(DiffViewAction::SetShowMappedSymbols(show_mapped_symbols));
|
||||
}
|
||||
for mapping_diff in &ctx.diff.mapping_symbols {
|
||||
if mapping_diff.target_symbol == Some(target_ref) {
|
||||
if !show_mapped_symbols {
|
||||
let symbol_diff = ctx.diff.symbol_diff(mapping_diff.symbol_ref);
|
||||
if symbol_diff.target_symbol.is_some() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
mapping.insert(mapping_diff.symbol_ref, mapping_diff);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (symbol, diff) in ctx.obj.common.iter().zip(&ctx.diff.common) {
|
||||
if !symbol_matches_filter(symbol, diff, filter) {
|
||||
continue;
|
||||
}
|
||||
mapping.insert(diff.symbol_ref, diff);
|
||||
}
|
||||
for (section, section_diff) in ctx.obj.sections.iter().zip(&ctx.diff.sections) {
|
||||
for (symbol, symbol_diff) in section.symbols.iter().zip(§ion_diff.symbols) {
|
||||
if !symbol_matches_filter(symbol, symbol_diff, filter) {
|
||||
continue;
|
||||
}
|
||||
mapping.insert(symbol_diff.symbol_ref, symbol_diff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||
|
||||
// Skip sections with all symbols filtered out
|
||||
if mapping.keys().any(|symbol_ref| symbol_ref.section_idx == SECTION_COMMON) {
|
||||
CollapsingHeader::new(".comm").default_open(true).show(ui, |ui| {
|
||||
for (symbol_ref, symbol_diff) in mapping
|
||||
.iter()
|
||||
.filter(|(symbol_ref, _)| symbol_ref.section_idx == SECTION_COMMON)
|
||||
{
|
||||
let symbol = ctx.obj.section_symbol(*symbol_ref).1;
|
||||
if let Some(result) = symbol_ui(
|
||||
ui,
|
||||
ctx,
|
||||
other_ctx,
|
||||
symbol,
|
||||
symbol_diff,
|
||||
None,
|
||||
state,
|
||||
appearance,
|
||||
column,
|
||||
) {
|
||||
ret = Some(result);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for ((section_index, section), section_diff) in
|
||||
ctx.obj.sections.iter().enumerate().zip(&ctx.diff.sections)
|
||||
{
|
||||
// Skip sections with all symbols filtered out
|
||||
if !mapping.keys().any(|symbol_ref| symbol_ref.section_idx == section_index) {
|
||||
continue;
|
||||
}
|
||||
let mut header = LayoutJob::simple_singleline(
|
||||
format!("{} ({:x})", section.name, section.size),
|
||||
appearance.code_font.clone(),
|
||||
Color32::PLACEHOLDER,
|
||||
);
|
||||
if let Some(match_percent) = section_diff.match_percent {
|
||||
write_text(
|
||||
" (",
|
||||
Color32::PLACEHOLDER,
|
||||
&mut header,
|
||||
appearance.code_font.clone(),
|
||||
);
|
||||
write_text(
|
||||
&format!("{:.0}%", match_percent.floor()),
|
||||
match_color_for_symbol(match_percent, appearance),
|
||||
&mut header,
|
||||
appearance.code_font.clone(),
|
||||
);
|
||||
write_text(
|
||||
")",
|
||||
Color32::PLACEHOLDER,
|
||||
&mut header,
|
||||
appearance.code_font.clone(),
|
||||
);
|
||||
}
|
||||
CollapsingHeader::new(header)
|
||||
.id_salt(Id::new(section.name.clone()).with(section.orig_index))
|
||||
.default_open(true)
|
||||
.show(ui, |ui| {
|
||||
if section.kind == ObjSectionKind::Code && state.reverse_fn_order {
|
||||
for (symbol, symbol_diff) in mapping
|
||||
.iter()
|
||||
.filter(|(symbol_ref, _)| symbol_ref.section_idx == section_index)
|
||||
.rev()
|
||||
{
|
||||
let symbol = ctx.obj.section_symbol(*symbol).1;
|
||||
if let Some(result) = symbol_ui(
|
||||
ui,
|
||||
ctx,
|
||||
other_ctx,
|
||||
symbol,
|
||||
symbol_diff,
|
||||
Some(section),
|
||||
state,
|
||||
appearance,
|
||||
column,
|
||||
) {
|
||||
ret = Some(result);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (symbol, symbol_diff) in mapping
|
||||
.iter()
|
||||
.filter(|(symbol_ref, _)| symbol_ref.section_idx == section_index)
|
||||
{
|
||||
let symbol = ctx.obj.section_symbol(*symbol).1;
|
||||
if let Some(result) = symbol_ui(
|
||||
ui,
|
||||
ctx,
|
||||
other_ctx,
|
||||
symbol,
|
||||
symbol_diff,
|
||||
Some(section),
|
||||
state,
|
||||
appearance,
|
||||
column,
|
||||
) {
|
||||
ret = Some(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
ret
|
||||
}
|
||||
|
||||
fn build_log_ui(ui: &mut Ui, status: &BuildStatus, appearance: &Appearance) {
|
||||
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
if !status.cmdline.is_empty() && ui.button("Copy command").clicked() {
|
||||
ui.output_mut(|output| output.copied_text.clone_from(&status.cmdline));
|
||||
}
|
||||
if ui.button("Copy log").clicked() {
|
||||
ui.output_mut(|output| {
|
||||
output.copied_text = format!("{}\n{}", status.stdout, status.stderr)
|
||||
});
|
||||
}
|
||||
});
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||
|
||||
if !status.cmdline.is_empty() {
|
||||
ui.label(&status.cmdline);
|
||||
}
|
||||
if !status.stdout.is_empty() {
|
||||
ui.colored_label(appearance.replace_color, &status.stdout);
|
||||
}
|
||||
if !status.stderr.is_empty() {
|
||||
ui.colored_label(appearance.delete_color, &status.stderr);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn missing_obj_ui(ui: &mut Ui, appearance: &Appearance) {
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||
|
||||
ui.colored_label(appearance.replace_color, "No object configured");
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct SymbolDiffContext<'a> {
|
||||
pub obj: &'a ObjInfo,
|
||||
pub diff: &'a ObjDiff,
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn symbol_diff_ui(
|
||||
ui: &mut Ui,
|
||||
state: &mut DiffViewState,
|
||||
appearance: &Appearance,
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
let Some(result) = &state.build else {
|
||||
return ret;
|
||||
};
|
||||
|
||||
// Header
|
||||
let available_width = ui.available_width();
|
||||
render_header(ui, available_width, 2, |ui, column| {
|
||||
if column == 0 {
|
||||
// Left column
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
|
||||
ui.label("Target object");
|
||||
if result.first_status.success {
|
||||
if result.first_obj.is_none() {
|
||||
ui.colored_label(appearance.replace_color, "Missing");
|
||||
} else {
|
||||
ui.colored_label(appearance.highlight_color, state.object_name.clone());
|
||||
}
|
||||
} else {
|
||||
ui.colored_label(appearance.delete_color, "Fail");
|
||||
}
|
||||
});
|
||||
|
||||
let mut search = state.search.clone();
|
||||
if TextEdit::singleline(&mut search).hint_text("Filter symbols").ui(ui).changed() {
|
||||
ret = Some(DiffViewAction::SetSearch(search));
|
||||
}
|
||||
} else if column == 1 {
|
||||
// Right column
|
||||
ui.horizontal(|ui| {
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.label("Base object");
|
||||
});
|
||||
ui.separator();
|
||||
if ui
|
||||
.add_enabled(state.source_path_available, egui::Button::new("🖹 Source file"))
|
||||
.on_hover_text_at_pointer("Open the source file in the default editor")
|
||||
.on_disabled_hover_text("Source file metadata missing")
|
||||
.clicked()
|
||||
{
|
||||
ret = Some(DiffViewAction::OpenSourcePath);
|
||||
}
|
||||
});
|
||||
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
if result.second_status.success {
|
||||
if result.second_obj.is_none() {
|
||||
ui.colored_label(appearance.replace_color, "Missing");
|
||||
} else {
|
||||
ui.colored_label(appearance.highlight_color, "OK");
|
||||
}
|
||||
} else {
|
||||
ui.colored_label(appearance.delete_color, "Fail");
|
||||
}
|
||||
});
|
||||
|
||||
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
|
||||
ret = Some(DiffViewAction::Build);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Table
|
||||
let filter = match &state.search_regex {
|
||||
Some(regex) => SymbolFilter::Search(regex),
|
||||
_ => SymbolFilter::None,
|
||||
};
|
||||
render_strips(ui, available_width, 2, |ui, column| {
|
||||
if column == 0 {
|
||||
// Left column
|
||||
if result.first_status.success {
|
||||
if let Some((obj, diff)) = &result.first_obj {
|
||||
if let Some(result) = symbol_list_ui(
|
||||
ui,
|
||||
SymbolDiffContext { obj, diff },
|
||||
result
|
||||
.second_obj
|
||||
.as_ref()
|
||||
.map(|(obj, diff)| SymbolDiffContext { obj, diff }),
|
||||
&state.symbol_state,
|
||||
filter,
|
||||
appearance,
|
||||
column,
|
||||
) {
|
||||
ret = Some(result);
|
||||
}
|
||||
} else {
|
||||
missing_obj_ui(ui, appearance);
|
||||
}
|
||||
} else {
|
||||
build_log_ui(ui, &result.first_status, appearance);
|
||||
}
|
||||
} else if column == 1 {
|
||||
// Right column
|
||||
if result.second_status.success {
|
||||
if let Some((obj, diff)) = &result.second_obj {
|
||||
if let Some(result) = symbol_list_ui(
|
||||
ui,
|
||||
SymbolDiffContext { obj, diff },
|
||||
result
|
||||
.first_obj
|
||||
.as_ref()
|
||||
.map(|(obj, diff)| SymbolDiffContext { obj, diff }),
|
||||
&state.symbol_state,
|
||||
filter,
|
||||
appearance,
|
||||
column,
|
||||
) {
|
||||
ret = Some(result);
|
||||
}
|
||||
} else {
|
||||
missing_obj_ui(ui, appearance);
|
||||
}
|
||||
} else {
|
||||
build_log_ui(ui, &result.second_status, appearance);
|
||||
}
|
||||
}
|
||||
});
|
||||
ret
|
||||
}
|
||||
4
objdiff-wasm/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
dist/
|
||||
gen/
|
||||
node_modules/
|
||||
pkg/
|
||||
28
objdiff-wasm/eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import globals from "globals";
|
||||
import pluginJs from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default [
|
||||
{files: ["**/*.{js,mjs,cjs,ts}"]},
|
||||
{languageOptions: {globals: globals.browser}},
|
||||
pluginJs.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
rules: {
|
||||
"semi": [2, "always"],
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
// https://typescript-eslint.io/rules/no-unused-vars/#benefits-over-typescript
|
||||
{
|
||||
"args": "all",
|
||||
"argsIgnorePattern": "^_",
|
||||
"caughtErrors": "all",
|
||||
"caughtErrorsIgnorePattern": "^_",
|
||||
"destructuredArrayIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_",
|
||||
"ignoreRestSiblings": true
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
];
|
||||
3519
objdiff-wasm/package-lock.json
generated
Normal file
39
objdiff-wasm/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "objdiff-wasm",
|
||||
"version": "2.0.0",
|
||||
"description": "A local diffing tool for decompilation projects.",
|
||||
"author": {
|
||||
"name": "Luke Street",
|
||||
"email": "luke@street.dev"
|
||||
},
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/encounter/objdiff.git"
|
||||
},
|
||||
"files": [
|
||||
"dist/*"
|
||||
],
|
||||
"main": "dist/main.js",
|
||||
"types": "dist/main.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"build:all": "npm run build:wasm && npm run build:proto && npm run build",
|
||||
"build:proto": "protoc --ts_out=gen --ts_opt add_pb_suffix,eslint_disable,ts_nocheck,use_proto_field_name --proto_path=../objdiff-core/protos ../objdiff-core/protos/*.proto",
|
||||
"build:wasm": "cd ../objdiff-core && wasm-pack build --out-dir ../objdiff-wasm/pkg --target web -- --features arm,dwarf,ppc,x86,wasm"
|
||||
},
|
||||
"dependencies": {
|
||||
"@protobuf-ts/runtime": "^2.9.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.0",
|
||||
"@protobuf-ts/plugin": "^2.9.4",
|
||||
"@types/node": "^22.4.1",
|
||||
"esbuild": "^0.23.1",
|
||||
"eslint": "^9.9.0",
|
||||
"globals": "^15.9.0",
|
||||
"tsup": "^8.2.4",
|
||||
"typescript-eslint": "^8.2.0"
|
||||
}
|
||||
}
|
||||
227
objdiff-wasm/src/main.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import {ArgumentValue, DiffResult, InstructionDiff, RelocationTarget} from "../gen/diff_pb";
|
||||
import type {
|
||||
ArmArchVersion,
|
||||
ArmR9Usage,
|
||||
DiffObjConfig,
|
||||
MipsAbi,
|
||||
MipsInstrCategory,
|
||||
X86Formatter
|
||||
} from '../pkg';
|
||||
import {AnyHandlerData, InMessage, OutMessage} from './worker';
|
||||
|
||||
// Export wasm types
|
||||
export {ArmArchVersion, ArmR9Usage, MipsAbi, MipsInstrCategory, X86Formatter, DiffObjConfig};
|
||||
|
||||
// Export protobuf types
|
||||
export * from '../gen/diff_pb';
|
||||
|
||||
interface PromiseCallbacks<T> {
|
||||
start: number;
|
||||
resolve: (value: T | PromiseLike<T>) => void;
|
||||
reject: (reason?: string) => void;
|
||||
}
|
||||
|
||||
let workerInit = false;
|
||||
let workerCallbacks: PromiseCallbacks<Worker>;
|
||||
const workerReady = new Promise<Worker>((resolve, reject) => {
|
||||
workerCallbacks = {start: performance.now(), resolve, reject};
|
||||
});
|
||||
|
||||
export async function initialize(data?: {
|
||||
workerUrl?: string | URL,
|
||||
wasmUrl?: string | URL, // Relative to worker URL
|
||||
}): Promise<Worker> {
|
||||
if (workerInit) {
|
||||
return workerReady;
|
||||
}
|
||||
workerInit = true;
|
||||
let {workerUrl, wasmUrl} = data || {};
|
||||
if (!workerUrl) {
|
||||
try {
|
||||
// Bundlers will convert this into an asset URL
|
||||
workerUrl = new URL('./worker.js', import.meta.url);
|
||||
} catch (_) {
|
||||
workerUrl = 'worker.js';
|
||||
}
|
||||
}
|
||||
if (!wasmUrl) {
|
||||
try {
|
||||
// Bundlers will convert this into an asset URL
|
||||
wasmUrl = new URL('./objdiff_core_bg.wasm', import.meta.url);
|
||||
} catch (_) {
|
||||
wasmUrl = 'objdiff_core_bg.js';
|
||||
}
|
||||
}
|
||||
const worker = new Worker(workerUrl, {
|
||||
name: 'objdiff',
|
||||
type: 'module',
|
||||
});
|
||||
worker.onmessage = onMessage;
|
||||
worker.onerror = (event) => {
|
||||
console.error("Worker error", event);
|
||||
workerCallbacks.reject("Worker failed to initialize, wrong URL?");
|
||||
};
|
||||
defer<void>({
|
||||
type: 'init',
|
||||
// URL can't be sent directly
|
||||
wasmUrl: wasmUrl.toString(),
|
||||
}, worker).then(() => {
|
||||
workerCallbacks.resolve(worker);
|
||||
}, (e) => {
|
||||
workerCallbacks.reject(e);
|
||||
});
|
||||
return workerReady;
|
||||
}
|
||||
|
||||
let globalMessageId = 0;
|
||||
const messageCallbacks = new Map<number, PromiseCallbacks<never>>();
|
||||
|
||||
function onMessage(event: MessageEvent<OutMessage>) {
|
||||
switch (event.data.type) {
|
||||
case 'result': {
|
||||
const {result, error, messageId} = event.data;
|
||||
const callbacks = messageCallbacks.get(messageId);
|
||||
if (callbacks) {
|
||||
const end = performance.now();
|
||||
console.debug(`Message ${messageId} took ${end - callbacks.start}ms`);
|
||||
messageCallbacks.delete(messageId);
|
||||
if (error != null) {
|
||||
callbacks.reject(error);
|
||||
} else {
|
||||
callbacks.resolve(result as never);
|
||||
}
|
||||
} else {
|
||||
console.warn(`Unknown message ID ${messageId}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function defer<T>(message: AnyHandlerData, worker?: Worker): Promise<T> {
|
||||
worker = worker || await initialize();
|
||||
const messageId = globalMessageId++;
|
||||
const promise = new Promise<T>((resolve, reject) => {
|
||||
messageCallbacks.set(messageId, {start: performance.now(), resolve, reject});
|
||||
});
|
||||
worker.postMessage({
|
||||
...message,
|
||||
messageId
|
||||
} as InMessage);
|
||||
return promise;
|
||||
}
|
||||
|
||||
export async function runDiff(left: Uint8Array | undefined, right: Uint8Array | undefined, config?: DiffObjConfig): Promise<DiffResult> {
|
||||
const data = await defer<Uint8Array>({
|
||||
type: 'run_diff_proto',
|
||||
left,
|
||||
right,
|
||||
config
|
||||
});
|
||||
const parseStart = performance.now();
|
||||
const result = DiffResult.fromBinary(data, {readUnknownField: false});
|
||||
const end = performance.now();
|
||||
console.debug(`Parsing message took ${end - parseStart}ms`);
|
||||
return result;
|
||||
}
|
||||
|
||||
export type DiffText =
|
||||
DiffTextBasic
|
||||
| DiffTextBasicColor
|
||||
| DiffTextAddress
|
||||
| DiffTextLine
|
||||
| DiffTextOpcode
|
||||
| DiffTextArgument
|
||||
| DiffTextSymbol
|
||||
| DiffTextBranchDest
|
||||
| DiffTextSpacing;
|
||||
|
||||
type DiffTextBase = {
|
||||
diff_index?: number,
|
||||
};
|
||||
export type DiffTextBasic = DiffTextBase & {
|
||||
type: 'basic',
|
||||
text: string,
|
||||
};
|
||||
export type DiffTextBasicColor = DiffTextBase & {
|
||||
type: 'basic_color',
|
||||
text: string,
|
||||
index: number,
|
||||
};
|
||||
export type DiffTextAddress = DiffTextBase & {
|
||||
type: 'address',
|
||||
address: bigint,
|
||||
};
|
||||
export type DiffTextLine = DiffTextBase & {
|
||||
type: 'line',
|
||||
line_number: number,
|
||||
};
|
||||
export type DiffTextOpcode = DiffTextBase & {
|
||||
type: 'opcode',
|
||||
mnemonic: string,
|
||||
opcode: number,
|
||||
};
|
||||
export type DiffTextArgument = DiffTextBase & {
|
||||
type: 'argument',
|
||||
value: ArgumentValue,
|
||||
};
|
||||
export type DiffTextSymbol = DiffTextBase & {
|
||||
type: 'symbol',
|
||||
target: RelocationTarget,
|
||||
};
|
||||
export type DiffTextBranchDest = DiffTextBase & {
|
||||
type: 'branch_dest',
|
||||
address: bigint,
|
||||
};
|
||||
export type DiffTextSpacing = DiffTextBase & {
|
||||
type: 'spacing',
|
||||
count: number,
|
||||
};
|
||||
|
||||
// Native JavaScript implementation of objdiff_core::diff::display::display_diff
|
||||
export function displayDiff(diff: InstructionDiff, baseAddr: bigint, cb: (text: DiffText) => void) {
|
||||
const ins = diff.instruction;
|
||||
if (!ins) {
|
||||
return;
|
||||
}
|
||||
if (ins.line_number != null) {
|
||||
cb({type: 'line', line_number: ins.line_number});
|
||||
}
|
||||
cb({type: 'address', address: ins.address - baseAddr});
|
||||
if (diff.branch_from) {
|
||||
cb({type: 'basic_color', text: ' ~> ', index: diff.branch_from.branch_index});
|
||||
} else {
|
||||
cb({type: 'spacing', count: 4});
|
||||
}
|
||||
cb({type: 'opcode', mnemonic: ins.mnemonic, opcode: ins.opcode});
|
||||
for (let i = 0; i < ins.arguments.length; i++) {
|
||||
if (i === 0) {
|
||||
cb({type: 'spacing', count: 1});
|
||||
}
|
||||
const arg = ins.arguments[i].value;
|
||||
const diff_index = diff.arg_diff[i]?.diff_index;
|
||||
switch (arg.oneofKind) {
|
||||
case "plain_text":
|
||||
cb({type: 'basic', text: arg.plain_text, diff_index});
|
||||
break;
|
||||
case "argument":
|
||||
cb({type: 'argument', value: arg.argument, diff_index});
|
||||
break;
|
||||
case "relocation": {
|
||||
const reloc = ins.relocation!;
|
||||
cb({type: 'symbol', target: reloc.target!, diff_index});
|
||||
break;
|
||||
}
|
||||
case "branch_dest":
|
||||
if (arg.branch_dest < baseAddr) {
|
||||
cb({type: 'basic', text: '<unknown>', diff_index});
|
||||
} else {
|
||||
cb({type: 'branch_dest', address: arg.branch_dest - baseAddr, diff_index});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (diff.branch_to) {
|
||||
cb({type: 'basic_color', text: ' ~> ', index: diff.branch_to.branch_index});
|
||||
}
|
||||
}
|
||||
93
objdiff-wasm/src/worker.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import wasmInit, * as exports from '../pkg';
|
||||
|
||||
const handlers = {
|
||||
init: init,
|
||||
// run_diff_json: run_diff_json,
|
||||
run_diff_proto: run_diff_proto,
|
||||
} as const;
|
||||
type ExtractData<T> = T extends (arg: infer U) => Promise<unknown> ? U : never;
|
||||
type HandlerData = {
|
||||
[K in keyof typeof handlers]: { type: K } & ExtractData<typeof handlers[K]>;
|
||||
};
|
||||
|
||||
let wasmReady: Promise<void> | null = null;
|
||||
|
||||
async function init({wasmUrl}: { wasmUrl?: string }): Promise<void> {
|
||||
if (wasmReady != null) {
|
||||
throw new Error('Already initialized');
|
||||
}
|
||||
wasmReady = wasmInit({module_or_path: wasmUrl})
|
||||
.then(() => {
|
||||
});
|
||||
return wasmReady;
|
||||
}
|
||||
|
||||
async function initIfNeeded() {
|
||||
if (wasmReady == null) {
|
||||
await init({});
|
||||
}
|
||||
return wasmReady;
|
||||
}
|
||||
|
||||
// async function run_diff_json({left, right, config}: {
|
||||
// left: Uint8Array | undefined,
|
||||
// right: Uint8Array | undefined,
|
||||
// config?: exports.DiffObjConfig,
|
||||
// }): Promise<string> {
|
||||
// config = config || exports.default_diff_obj_config();
|
||||
// return exports.run_diff_json(left, right, cfg);
|
||||
// }
|
||||
|
||||
async function run_diff_proto({left, right, config}: {
|
||||
left: Uint8Array | undefined,
|
||||
right: Uint8Array | undefined,
|
||||
config?: exports.DiffObjConfig,
|
||||
}): Promise<Uint8Array> {
|
||||
config = config || {};
|
||||
return exports.run_diff_proto(left, right, config);
|
||||
}
|
||||
|
||||
export type AnyHandlerData = HandlerData[keyof HandlerData];
|
||||
export type InMessage = AnyHandlerData & { messageId: number };
|
||||
|
||||
export type OutMessage = {
|
||||
type: 'result',
|
||||
result: unknown | null,
|
||||
error: string | null,
|
||||
messageId: number,
|
||||
};
|
||||
|
||||
self.onmessage = (event: MessageEvent<InMessage>) => {
|
||||
const data = event.data;
|
||||
const messageId = data?.messageId;
|
||||
(async () => {
|
||||
if (!data) {
|
||||
throw new Error('No data');
|
||||
}
|
||||
const handler = handlers[data.type];
|
||||
if (handler) {
|
||||
if (data.type !== 'init') {
|
||||
await initIfNeeded();
|
||||
}
|
||||
const start = performance.now();
|
||||
const result = await handler(data as never);
|
||||
const end = performance.now();
|
||||
console.debug(`Worker message ${data.messageId} took ${end - start}ms`);
|
||||
self.postMessage({
|
||||
type: 'result',
|
||||
result: result,
|
||||
error: null,
|
||||
messageId,
|
||||
} as OutMessage);
|
||||
} else {
|
||||
throw new Error(`No handler for ${data.type}`);
|
||||
}
|
||||
})().catch(error => {
|
||||
self.postMessage({
|
||||
type: 'result',
|
||||
result: null,
|
||||
error: error.toString(),
|
||||
messageId,
|
||||
} as OutMessage);
|
||||
});
|
||||
};
|
||||
9
objdiff-wasm/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"target": "ES2022",
|
||||
}
|
||||
}
|
||||
33
objdiff-wasm/tsup.config.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import {defineConfig} from 'tsup';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
export default defineConfig([
|
||||
// Build main library
|
||||
{
|
||||
entry: ['src/main.ts'],
|
||||
clean: true,
|
||||
dts: true,
|
||||
format: 'esm',
|
||||
outDir: 'dist',
|
||||
skipNodeModulesBundle: true,
|
||||
sourcemap: true,
|
||||
splitting: false,
|
||||
target: 'es2022',
|
||||
},
|
||||
// Build web worker
|
||||
{
|
||||
entry: ['src/worker.ts'],
|
||||
clean: true,
|
||||
dts: true,
|
||||
format: 'esm', // type: 'module'
|
||||
minify: true,
|
||||
outDir: 'dist',
|
||||
sourcemap: true,
|
||||
splitting: false,
|
||||
target: 'es2022',
|
||||
// https://github.com/egoist/tsup/issues/278
|
||||
async onSuccess() {
|
||||
await fs.copyFile('pkg/objdiff_core_bg.wasm', 'dist/objdiff_core_bg.wasm');
|
||||
}
|
||||
}
|
||||
]);
|
||||
539
src/app.rs
@@ -1,539 +0,0 @@
|
||||
use std::{
|
||||
default::Default,
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex, RwLock,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use egui::{Color32, FontFamily, FontId, TextStyle};
|
||||
use notify::{RecursiveMode, Watcher};
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
|
||||
use crate::{
|
||||
jobs::{
|
||||
check_update::{queue_check_update, CheckUpdateResult},
|
||||
objdiff::{queue_build, BuildStatus, ObjDiffResult},
|
||||
Job, JobResult, JobState, JobStatus,
|
||||
},
|
||||
views::{
|
||||
config::config_ui, data_diff::data_diff_ui, function_diff::function_diff_ui, jobs::jobs_ui,
|
||||
symbol_diff::symbol_diff_ui,
|
||||
},
|
||||
};
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[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 {
|
||||
#[serde(skip)]
|
||||
pub jobs: Vec<JobState>,
|
||||
#[serde(skip)]
|
||||
pub build: Option<Box<ObjDiffResult>>,
|
||||
#[serde(skip)]
|
||||
pub highlighted_symbol: Option<String>,
|
||||
#[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,
|
||||
#[serde(skip)]
|
||||
pub demangle_text: String,
|
||||
#[serde(skip)]
|
||||
pub diff_config: DiffConfig,
|
||||
#[serde(skip)]
|
||||
pub search: String,
|
||||
#[serde(skip)]
|
||||
pub utc_offset: UtcOffset,
|
||||
#[serde(skip)]
|
||||
pub check_update: Option<Box<CheckUpdateResult>>,
|
||||
// Config
|
||||
pub diff_kind: DiffKind,
|
||||
pub reverse_fn_order: bool,
|
||||
pub view_config: ViewConfig,
|
||||
}
|
||||
|
||||
impl Default for ViewState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
jobs: vec![],
|
||||
build: None,
|
||||
highlighted_symbol: None,
|
||||
selected_symbol: None,
|
||||
current_view: Default::default(),
|
||||
show_config: false,
|
||||
show_demangle: false,
|
||||
demangle_text: String::new(),
|
||||
diff_config: Default::default(),
|
||||
search: Default::default(),
|
||||
utc_offset: UtcOffset::UTC,
|
||||
check_update: None,
|
||||
diff_kind: Default::default(),
|
||||
reverse_fn_order: false,
|
||||
view_config: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct AppConfig {
|
||||
pub custom_make: Option<String>,
|
||||
// WSL2 settings
|
||||
#[serde(skip)]
|
||||
pub available_wsl_distros: Option<Vec<String>>,
|
||||
pub selected_wsl_distro: Option<String>,
|
||||
// Split obj
|
||||
pub project_dir: Option<PathBuf>,
|
||||
pub target_obj_dir: Option<PathBuf>,
|
||||
pub base_obj_dir: Option<PathBuf>,
|
||||
pub obj_path: Option<String>,
|
||||
pub build_target: bool,
|
||||
// Whole binary
|
||||
pub left_obj: Option<PathBuf>,
|
||||
pub right_obj: Option<PathBuf>,
|
||||
#[serde(skip)]
|
||||
pub project_dir_change: bool,
|
||||
#[serde(skip)]
|
||||
pub queue_update_check: bool,
|
||||
pub auto_update_check: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, serde::Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct ProjectConfig {
|
||||
pub custom_make: Option<String>,
|
||||
pub project_dir: Option<PathBuf>,
|
||||
pub target_obj_dir: Option<PathBuf>,
|
||||
pub base_obj_dir: Option<PathBuf>,
|
||||
pub build_target: bool,
|
||||
}
|
||||
|
||||
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct App {
|
||||
view_state: ViewState,
|
||||
#[serde(skip)]
|
||||
config: Arc<RwLock<AppConfig>>,
|
||||
#[serde(skip)]
|
||||
modified: Arc<AtomicBool>,
|
||||
#[serde(skip)]
|
||||
watcher: Option<notify::RecommendedWatcher>,
|
||||
#[serde(skip)]
|
||||
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
|
||||
#[serde(skip)]
|
||||
should_relaunch: bool,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> Self {
|
||||
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 {
|
||||
/// Called once before the first frame.
|
||||
pub fn new(
|
||||
cc: &eframe::CreationContext<'_>,
|
||||
utc_offset: UtcOffset,
|
||||
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
|
||||
) -> Self {
|
||||
// This is also where you can customized the look at feel of egui using
|
||||
// `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`.
|
||||
|
||||
// Load previous app state (if any).
|
||||
// Note that you must enable the `persistence` feature for this to work.
|
||||
if let Some(storage) = cc.storage {
|
||||
let mut app: App = eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
|
||||
let mut config: AppConfig = eframe::get_value(storage, CONFIG_KEY).unwrap_or_default();
|
||||
if config.project_dir.is_some() {
|
||||
config.project_dir_change = true;
|
||||
}
|
||||
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();
|
||||
app.view_state.utc_offset = utc_offset;
|
||||
app.relaunch_path = relaunch_path;
|
||||
app
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
/// Called each time the UI needs repainting, which may be many times per second.
|
||||
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
|
||||
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
if self.should_relaunch {
|
||||
frame.close();
|
||||
return;
|
||||
}
|
||||
|
||||
let Self { config, view_state, .. } = self;
|
||||
|
||||
{
|
||||
let config = &view_state.view_config;
|
||||
let mut style = (*ctx.style()).clone();
|
||||
style.text_styles.insert(TextStyle::Body, FontId {
|
||||
size: (config.ui_font.size * 0.75).floor(),
|
||||
family: config.ui_font.family.clone(),
|
||||
});
|
||||
style.text_styles.insert(TextStyle::Body, config.ui_font.clone());
|
||||
style.text_styles.insert(TextStyle::Button, config.ui_font.clone());
|
||||
style.text_styles.insert(TextStyle::Heading, FontId {
|
||||
size: (config.ui_font.size * 1.5).floor(),
|
||||
family: config.ui_font.family.clone(),
|
||||
});
|
||||
style.text_styles.insert(TextStyle::Monospace, config.code_font.clone());
|
||||
ctx.set_style(style);
|
||||
}
|
||||
|
||||
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
ui.menu_button("File", |ui| {
|
||||
if ui.button("Show config").clicked() {
|
||||
view_state.show_config = !view_state.show_config;
|
||||
}
|
||||
if ui.button("Quit").clicked() {
|
||||
frame.close();
|
||||
}
|
||||
});
|
||||
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) => {
|
||||
log::info!("Job {} finished", job.id);
|
||||
match result {
|
||||
JobResult::None => {
|
||||
if let Some(err) = &job.status.read().unwrap().error {
|
||||
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) => {
|
||||
if let Ok(mut guard) = self.relaunch_path.lock() {
|
||||
*guard = Some(state.exe_path);
|
||||
}
|
||||
self.should_relaunch = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let err = if let Some(msg) = err.downcast_ref::<&'static str>() {
|
||||
anyhow::Error::msg(*msg)
|
||||
} else if let Some(msg) = err.downcast_ref::<String>() {
|
||||
anyhow::Error::msg(msg.clone())
|
||||
} else {
|
||||
anyhow::Error::msg("Thread panicked")
|
||||
};
|
||||
let result = job.status.write();
|
||||
if let Ok(mut guard) = result {
|
||||
guard.error = Some(err);
|
||||
} else {
|
||||
drop(result);
|
||||
job.status = Arc::new(RwLock::new(JobStatus {
|
||||
title: "Error".to_string(),
|
||||
progress_percent: 0.0,
|
||||
progress_items: None,
|
||||
status: "".to_string(),
|
||||
error: Some(err),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.view_state.jobs.iter().any(|v| v.should_remove) {
|
||||
let mut i = 0;
|
||||
while i < self.view_state.jobs.len() {
|
||||
let job = &self.view_state.jobs[i];
|
||||
if job.should_remove
|
||||
&& job.handle.is_none()
|
||||
&& job.status.read().unwrap().error.is_none()
|
||||
{
|
||||
self.view_state.jobs.remove(i);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(mut config) = self.config.write() {
|
||||
if config.project_dir_change {
|
||||
drop(self.watcher.take());
|
||||
if let Some(project_dir) = &config.project_dir {
|
||||
match create_watcher(self.modified.clone(), project_dir) {
|
||||
Ok(watcher) => self.watcher = Some(watcher),
|
||||
Err(e) => eprintln!("Failed to create watcher: {e}"),
|
||||
}
|
||||
config.project_dir_change = false;
|
||||
self.modified.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
if config.obj_path.is_some() && self.modified.load(Ordering::Relaxed) {
|
||||
if !self
|
||||
.view_state
|
||||
.jobs
|
||||
.iter()
|
||||
.any(|j| j.job_type == Job::ObjDiff && j.handle.is_some())
|
||||
{
|
||||
self.view_state.jobs.push(queue_build(
|
||||
self.config.clone(),
|
||||
self.view_state.diff_config.clone(),
|
||||
));
|
||||
}
|
||||
self.modified.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
if config.queue_update_check {
|
||||
self.view_state.jobs.push(queue_check_update());
|
||||
config.queue_update_check = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_watcher(
|
||||
modified: Arc<AtomicBool>,
|
||||
project_dir: &Path,
|
||||
) -> notify::Result<notify::RecommendedWatcher> {
|
||||
let mut watcher =
|
||||
notify::recommended_watcher(move |res: notify::Result<notify::Event>| match res {
|
||||
Ok(event) => {
|
||||
if matches!(event.kind, notify::EventKind::Modify(..)) {
|
||||
let watch_extensions = &[
|
||||
Some(OsStr::new("c")),
|
||||
Some(OsStr::new("cp")),
|
||||
Some(OsStr::new("cpp")),
|
||||
Some(OsStr::new("h")),
|
||||
Some(OsStr::new("hpp")),
|
||||
Some(OsStr::new("s")),
|
||||
];
|
||||
if event.paths.iter().any(|p| watch_extensions.contains(&p.extension())) {
|
||||
modified.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => println!("watch error: {e:?}"),
|
||||
})?;
|
||||
watcher.watch(project_dir, RecursiveMode::Recursive)?;
|
||||
Ok(watcher)
|
||||
}
|
||||
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;
|
||||
}
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
use std::sync::mpsc::Receiver;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use self_update::{cargo_crate_version, update::Release};
|
||||
|
||||
use crate::{
|
||||
jobs::{queue_job, update_status, Job, JobResult, JobState, Status},
|
||||
update::{build_updater, BIN_NAME},
|
||||
};
|
||||
|
||||
pub struct CheckUpdateResult {
|
||||
pub update_available: bool,
|
||||
pub latest_release: Release,
|
||||
pub found_binary: bool,
|
||||
}
|
||||
|
||||
fn run_check_update(status: &Status, cancel: Receiver<()>) -> Result<Box<CheckUpdateResult>> {
|
||||
update_status(status, "Fetching latest release".to_string(), 0, 1, &cancel)?;
|
||||
let updater = build_updater().context("Failed to create release updater")?;
|
||||
let latest_release = updater.get_latest_release()?;
|
||||
let update_available =
|
||||
self_update::version::bump_is_greater(cargo_crate_version!(), &latest_release.version)?;
|
||||
let found_binary = latest_release.assets.iter().any(|a| a.name == BIN_NAME);
|
||||
|
||||
update_status(status, "Complete".to_string(), 1, 1, &cancel)?;
|
||||
Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary }))
|
||||
}
|
||||
|
||||
pub fn queue_check_update() -> JobState {
|
||||
queue_job("Check for updates", Job::CheckUpdate, move |status, cancel| {
|
||||
run_check_update(status, cancel).map(JobResult::CheckUpdate)
|
||||
})
|
||||
}
|
||||
117
src/jobs/mod.rs
@@ -1,117 +0,0 @@
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
mpsc::{Receiver, Sender, TryRecvError},
|
||||
Arc, RwLock,
|
||||
},
|
||||
thread::JoinHandle,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::jobs::{
|
||||
bindiff::BinDiffResult, check_update::CheckUpdateResult, objdiff::ObjDiffResult,
|
||||
update::UpdateResult,
|
||||
};
|
||||
|
||||
pub mod bindiff;
|
||||
pub mod check_update;
|
||||
pub mod objdiff;
|
||||
pub mod update;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
pub enum Job {
|
||||
ObjDiff,
|
||||
BinDiff,
|
||||
CheckUpdate,
|
||||
Update,
|
||||
}
|
||||
pub static JOB_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
pub struct JobState {
|
||||
pub id: usize,
|
||||
pub job_type: Job,
|
||||
pub handle: Option<JoinHandle<JobResult>>,
|
||||
pub status: Arc<RwLock<JobStatus>>,
|
||||
pub cancel: Sender<()>,
|
||||
pub should_remove: bool,
|
||||
}
|
||||
#[derive(Default)]
|
||||
pub struct JobStatus {
|
||||
pub title: String,
|
||||
pub progress_percent: f32,
|
||||
pub progress_items: Option<[u32; 2]>,
|
||||
pub status: String,
|
||||
pub error: Option<anyhow::Error>,
|
||||
}
|
||||
pub enum JobResult {
|
||||
None,
|
||||
ObjDiff(Box<ObjDiffResult>),
|
||||
BinDiff(Box<BinDiffResult>),
|
||||
CheckUpdate(Box<CheckUpdateResult>),
|
||||
Update(Box<UpdateResult>),
|
||||
}
|
||||
|
||||
fn should_cancel(rx: &Receiver<()>) -> bool {
|
||||
match rx.try_recv() {
|
||||
Ok(_) | Err(TryRecvError::Disconnected) => true,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
type Status = Arc<RwLock<JobStatus>>;
|
||||
|
||||
fn queue_job(
|
||||
title: &str,
|
||||
job_type: Job,
|
||||
run: impl FnOnce(&Status, Receiver<()>) -> Result<JobResult> + Send + 'static,
|
||||
) -> JobState {
|
||||
let status = Arc::new(RwLock::new(JobStatus {
|
||||
title: title.to_string(),
|
||||
progress_percent: 0.0,
|
||||
progress_items: None,
|
||||
status: String::new(),
|
||||
error: None,
|
||||
}));
|
||||
let status_clone = status.clone();
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let handle = std::thread::spawn(move || {
|
||||
return match run(&status, rx) {
|
||||
Ok(state) => state,
|
||||
Err(e) => {
|
||||
if let Ok(mut w) = status.write() {
|
||||
w.error = Some(e);
|
||||
}
|
||||
JobResult::None
|
||||
}
|
||||
};
|
||||
});
|
||||
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
|
||||
log::info!("Started job {}", id);
|
||||
JobState {
|
||||
id,
|
||||
job_type,
|
||||
handle: Some(handle),
|
||||
status: status_clone,
|
||||
cancel: tx,
|
||||
should_remove: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_status(
|
||||
status: &Status,
|
||||
str: String,
|
||||
count: u32,
|
||||
total: u32,
|
||||
cancel: &Receiver<()>,
|
||||
) -> Result<()> {
|
||||
let mut w = status.write().map_err(|_| anyhow::Error::msg("Failed to lock job status"))?;
|
||||
w.progress_items = Some([count, total]);
|
||||
w.progress_percent = count as f32 / total as f32;
|
||||
if should_cancel(cancel) {
|
||||
w.status = "Cancelled".to_string();
|
||||
return Err(anyhow::Error::msg("Cancelled"));
|
||||
} else {
|
||||
w.status = str;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
use std::{
|
||||
path::Path,
|
||||
process::Command,
|
||||
str::from_utf8,
|
||||
sync::{mpsc::Receiver, Arc, RwLock},
|
||||
};
|
||||
|
||||
use anyhow::{Context, Error, Result};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::{
|
||||
app::{AppConfig, DiffConfig},
|
||||
diff::diff_objs,
|
||||
jobs::{queue_job, update_status, Job, JobResult, JobState, Status},
|
||||
obj::{elf, ObjInfo},
|
||||
};
|
||||
|
||||
pub struct BuildStatus {
|
||||
pub success: bool,
|
||||
pub log: String,
|
||||
}
|
||||
|
||||
pub struct ObjDiffResult {
|
||||
pub first_status: BuildStatus,
|
||||
pub second_status: BuildStatus,
|
||||
pub first_obj: Option<ObjInfo>,
|
||||
pub second_obj: Option<ObjInfo>,
|
||||
pub time: OffsetDateTime,
|
||||
}
|
||||
|
||||
fn run_make(cwd: &Path, arg: &Path, config: &AppConfig) -> BuildStatus {
|
||||
match (|| -> Result<BuildStatus> {
|
||||
let make = config.custom_make.as_deref().unwrap_or("make");
|
||||
#[cfg(not(windows))]
|
||||
let mut command = {
|
||||
let mut command = Command::new(make);
|
||||
command.current_dir(cwd).arg(arg);
|
||||
command
|
||||
};
|
||||
#[cfg(windows)]
|
||||
let mut command = {
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
use path_slash::PathExt;
|
||||
let mut command = if config.selected_wsl_distro.is_some() {
|
||||
Command::new("wsl")
|
||||
} else {
|
||||
Command::new(make)
|
||||
};
|
||||
if let Some(distro) = &config.selected_wsl_distro {
|
||||
command
|
||||
.arg("--cd")
|
||||
.arg(cwd)
|
||||
.arg("-d")
|
||||
.arg(distro)
|
||||
.arg("--")
|
||||
.arg(make)
|
||||
.arg(arg.to_slash_lossy().as_ref());
|
||||
} else {
|
||||
command.current_dir(cwd).arg(arg.to_slash_lossy().as_ref());
|
||||
}
|
||||
command.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
|
||||
command
|
||||
};
|
||||
let output = command.output().context("Failed to execute build")?;
|
||||
let stdout = from_utf8(&output.stdout).context("Failed to process stdout")?;
|
||||
let stderr = from_utf8(&output.stderr).context("Failed to process stderr")?;
|
||||
Ok(BuildStatus {
|
||||
success: output.status.code().unwrap_or(-1) == 0,
|
||||
log: format!("{stdout}\n{stderr}"),
|
||||
})
|
||||
})() {
|
||||
Ok(status) => status,
|
||||
Err(e) => BuildStatus { success: false, log: e.to_string() },
|
||||
}
|
||||
}
|
||||
|
||||
fn run_build(
|
||||
status: &Status,
|
||||
cancel: Receiver<()>,
|
||||
config: Arc<RwLock<AppConfig>>,
|
||||
diff_config: DiffConfig,
|
||||
) -> Result<Box<ObjDiffResult>> {
|
||||
let config = config.read().map_err(|_| Error::msg("Failed to lock app config"))?.clone();
|
||||
let obj_path = config.obj_path.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?;
|
||||
let project_dir =
|
||||
config.project_dir.as_ref().ok_or_else(|| Error::msg("Missing project dir"))?;
|
||||
let mut target_path = config
|
||||
.target_obj_dir
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::msg("Missing target obj dir"))?
|
||||
.to_owned();
|
||||
target_path.push(obj_path);
|
||||
let mut base_path =
|
||||
config.base_obj_dir.as_ref().ok_or_else(|| Error::msg("Missing base obj dir"))?.to_owned();
|
||||
base_path.push(obj_path);
|
||||
let target_path_rel = target_path
|
||||
.strip_prefix(project_dir)
|
||||
.context("Failed to create relative target obj path")?;
|
||||
let base_path_rel =
|
||||
base_path.strip_prefix(project_dir).context("Failed to create relative base obj path")?;
|
||||
|
||||
let total = if config.build_target { 5 } else { 4 };
|
||||
let first_status = if config.build_target {
|
||||
update_status(status, format!("Building target {obj_path}"), 0, total, &cancel)?;
|
||||
run_make(project_dir, target_path_rel, &config)
|
||||
} else {
|
||||
BuildStatus { success: true, log: String::new() }
|
||||
};
|
||||
|
||||
update_status(status, format!("Building base {obj_path}"), 1, total, &cancel)?;
|
||||
let second_status = run_make(project_dir, base_path_rel, &config);
|
||||
|
||||
let time = OffsetDateTime::now_utc();
|
||||
|
||||
let mut first_obj = if first_status.success {
|
||||
update_status(status, format!("Loading target {obj_path}"), 2, total, &cancel)?;
|
||||
Some(elf::read(&target_path)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut second_obj = if second_status.success {
|
||||
update_status(status, format!("Loading base {obj_path}"), 3, total, &cancel)?;
|
||||
Some(elf::read(&base_path)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let (Some(first_obj), Some(second_obj)) = (&mut first_obj, &mut second_obj) {
|
||||
update_status(status, "Performing diff".to_string(), 4, total, &cancel)?;
|
||||
diff_objs(first_obj, second_obj, &diff_config)?;
|
||||
}
|
||||
|
||||
update_status(status, "Complete".to_string(), total, total, &cancel)?;
|
||||
Ok(Box::new(ObjDiffResult { first_status, second_status, first_obj, second_obj, time }))
|
||||
}
|
||||
|
||||
pub fn queue_build(config: Arc<RwLock<AppConfig>>, diff_config: DiffConfig) -> JobState {
|
||||
queue_job("Object diff", Job::ObjDiff, move |status, cancel| {
|
||||
run_build(status, cancel, config, diff_config).map(JobResult::ObjDiff)
|
||||
})
|
||||
}
|
||||
11
src/lib.rs
@@ -1,11 +0,0 @@
|
||||
#![warn(clippy::all, rust_2018_idioms)]
|
||||
|
||||
pub use app::App;
|
||||
|
||||
mod app;
|
||||
mod diff;
|
||||
mod editops;
|
||||
mod jobs;
|
||||
mod obj;
|
||||
mod update;
|
||||
mod views;
|
||||
70
src/main.rs
@@ -1,70 +0,0 @@
|
||||
#![warn(clippy::all, rust_2018_idioms)]
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||
|
||||
use std::{path::PathBuf, rc::Rc, sync::Mutex};
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
use time::UtcOffset;
|
||||
|
||||
// When compiling natively:
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn main() {
|
||||
// Log to stdout (if you run with `RUST_LOG=debug`).
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Because localtime_r is unsound in multithreaded apps,
|
||||
// we must call this before initializing eframe.
|
||||
// https://github.com/time-rs/time/issues/293
|
||||
let utc_offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC);
|
||||
|
||||
let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None));
|
||||
let exec_path_clone = exec_path.clone();
|
||||
let native_options = eframe::NativeOptions::default();
|
||||
// native_options.renderer = eframe::Renderer::Wgpu;
|
||||
eframe::run_native(
|
||||
"objdiff",
|
||||
native_options,
|
||||
Box::new(move |cc| Box::new(objdiff::App::new(cc, utc_offset, exec_path_clone))),
|
||||
);
|
||||
|
||||
// Attempt to relaunch application from the updated path
|
||||
if let Ok(mut guard) = exec_path.lock() {
|
||||
if let Some(path) = guard.take() {
|
||||
cfg_if! {
|
||||
if #[cfg(unix)] {
|
||||
let result = exec::Command::new(path)
|
||||
.args(&std::env::args().collect::<Vec<String>>())
|
||||
.exec();
|
||||
eprintln!("Failed to relaunch: {result:?}");
|
||||
} else {
|
||||
let result = std::process::Command::new(path)
|
||||
.args(std::env::args())
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait();
|
||||
if let Err(e) = result {
|
||||
eprintln!("Failed to relaunch: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// when compiling to web using trunk.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn main() {
|
||||
// Make sure panics are logged using `console.error`.
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
// Redirect tracing to console.log and friends:
|
||||
tracing_wasm::set_as_global_default();
|
||||
|
||||
let web_options = eframe::WebOptions::default();
|
||||
eframe::start_web(
|
||||
"the_canvas_id", // hardcode it
|
||||
web_options,
|
||||
Box::new(|cc| Box::new(eframe_template::TemplateApp::new(cc))),
|
||||
)
|
||||
.expect("failed to start eframe");
|
||||
}
|
||||
316
src/obj/elf.rs
@@ -1,316 +0,0 @@
|
||||
use std::{fs, path::Path};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use cwdemangle::demangle;
|
||||
use flagset::Flags;
|
||||
use object::{
|
||||
elf::{
|
||||
R_MIPS_26, R_MIPS_HI16, R_MIPS_LO16, R_PPC_ADDR16_HA, R_PPC_ADDR16_HI, R_PPC_ADDR16_LO,
|
||||
R_PPC_EMB_SDA21, R_PPC_REL14, R_PPC_REL24,
|
||||
},
|
||||
Architecture, File, Object, ObjectSection, ObjectSymbol, RelocationKind, RelocationTarget,
|
||||
SectionKind, Symbol, SymbolKind, SymbolSection,
|
||||
};
|
||||
|
||||
use crate::obj::{
|
||||
ObjArchitecture, ObjInfo, ObjReloc, ObjRelocKind, ObjSection, ObjSectionKind, ObjSymbol,
|
||||
ObjSymbolFlagSet, ObjSymbolFlags,
|
||||
};
|
||||
|
||||
fn to_obj_section_kind(kind: SectionKind) -> ObjSectionKind {
|
||||
match kind {
|
||||
SectionKind::Text => ObjSectionKind::Code,
|
||||
SectionKind::Data | SectionKind::ReadOnlyData => ObjSectionKind::Data,
|
||||
SectionKind::UninitializedData => ObjSectionKind::Bss,
|
||||
_ => panic!("Unhandled section kind {kind:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_obj_symbol(obj_file: &File<'_>, symbol: &Symbol<'_, '_>, addend: i64) -> Result<ObjSymbol> {
|
||||
let mut name = symbol.name().context("Failed to process symbol name")?;
|
||||
if name.is_empty() {
|
||||
println!("Found empty sym: {symbol:?}");
|
||||
name = "?";
|
||||
}
|
||||
let mut flags = ObjSymbolFlagSet(ObjSymbolFlags::none());
|
||||
if symbol.is_global() {
|
||||
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Global);
|
||||
}
|
||||
if symbol.is_local() {
|
||||
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Local);
|
||||
}
|
||||
if symbol.is_common() {
|
||||
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Common);
|
||||
}
|
||||
if symbol.is_weak() {
|
||||
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Weak);
|
||||
}
|
||||
let section_address = if let Some(section) =
|
||||
symbol.section_index().and_then(|idx| obj_file.section_by_index(idx).ok())
|
||||
{
|
||||
symbol.address() - section.address()
|
||||
} else {
|
||||
symbol.address()
|
||||
};
|
||||
Ok(ObjSymbol {
|
||||
name: name.to_string(),
|
||||
demangled_name: demangle(name, &Default::default()),
|
||||
address: symbol.address(),
|
||||
section_address,
|
||||
size: symbol.size(),
|
||||
size_known: symbol.size() != 0,
|
||||
flags,
|
||||
addend,
|
||||
diff_symbol: None,
|
||||
instructions: vec![],
|
||||
match_percent: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn filter_sections(obj_file: &File<'_>) -> Result<Vec<ObjSection>> {
|
||||
let mut result = Vec::<ObjSection>::new();
|
||||
for section in obj_file.sections() {
|
||||
if section.size() == 0 {
|
||||
continue;
|
||||
}
|
||||
if section.kind() != SectionKind::Text
|
||||
&& section.kind() != SectionKind::Data
|
||||
&& section.kind() != SectionKind::ReadOnlyData
|
||||
&& section.kind() != SectionKind::UninitializedData
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let name = section.name().context("Failed to process section name")?;
|
||||
let data = section.uncompressed_data().context("Failed to read section data")?;
|
||||
result.push(ObjSection {
|
||||
name: name.to_string(),
|
||||
kind: to_obj_section_kind(section.kind()),
|
||||
address: section.address(),
|
||||
size: section.size(),
|
||||
data: data.to_vec(),
|
||||
index: section.index().0,
|
||||
symbols: Vec::new(),
|
||||
relocations: Vec::new(),
|
||||
data_diff: vec![],
|
||||
match_percent: 0.0,
|
||||
});
|
||||
}
|
||||
result.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn symbols_by_section(obj_file: &File<'_>, section: &ObjSection) -> Result<Vec<ObjSymbol>> {
|
||||
let mut result = Vec::<ObjSymbol>::new();
|
||||
for symbol in obj_file.symbols() {
|
||||
if symbol.kind() == SymbolKind::Section {
|
||||
continue;
|
||||
}
|
||||
if let Some(index) = symbol.section().index() {
|
||||
if index.0 == section.index {
|
||||
if symbol.is_local() && section.kind == ObjSectionKind::Code {
|
||||
// TODO strip local syms in diff?
|
||||
let name = symbol.name().context("Failed to process symbol name")?;
|
||||
if symbol.size() == 0 || name.starts_with("lbl_") {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result.push(to_obj_symbol(obj_file, &symbol, 0)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
result.sort_by_key(|v| v.address);
|
||||
let mut iter = result.iter_mut().peekable();
|
||||
while let Some(symbol) = iter.next() {
|
||||
if symbol.size == 0 {
|
||||
if let Some(next_symbol) = iter.peek() {
|
||||
symbol.size = next_symbol.address - symbol.address;
|
||||
} else {
|
||||
symbol.size = (section.address + section.size) - symbol.address;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn common_symbols(obj_file: &File<'_>) -> Result<Vec<ObjSymbol>> {
|
||||
let mut result = Vec::<ObjSymbol>::new();
|
||||
for symbol in obj_file.symbols() {
|
||||
if symbol.is_common() {
|
||||
result.push(to_obj_symbol(obj_file, &symbol, 0)?);
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn find_section_symbol(
|
||||
obj_file: &File<'_>,
|
||||
target: &Symbol<'_, '_>,
|
||||
address: u64,
|
||||
) -> Result<ObjSymbol> {
|
||||
let section_index =
|
||||
target.section_index().ok_or_else(|| anyhow::Error::msg("Unknown section index"))?;
|
||||
let section = obj_file.section_by_index(section_index)?;
|
||||
let mut closest_symbol: Option<Symbol<'_, '_>> = None;
|
||||
for symbol in obj_file.symbols() {
|
||||
if !matches!(symbol.section_index(), Some(idx) if idx == section_index) {
|
||||
continue;
|
||||
}
|
||||
if symbol.kind() == SymbolKind::Section || symbol.address() != address {
|
||||
if symbol.address() < address
|
||||
&& symbol.size() != 0
|
||||
&& (closest_symbol.is_none()
|
||||
|| matches!(&closest_symbol, Some(s) if s.address() <= symbol.address()))
|
||||
{
|
||||
closest_symbol = Some(symbol);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return to_obj_symbol(obj_file, &symbol, 0);
|
||||
}
|
||||
let (name, offset) = closest_symbol
|
||||
.and_then(|s| s.name().map(|n| (n, s.address())).ok())
|
||||
.or_else(|| section.name().map(|n| (n, section.address())).ok())
|
||||
.unwrap_or(("<unknown>", 0));
|
||||
let offset_addr = address - offset;
|
||||
Ok(ObjSymbol {
|
||||
name: name.to_string(),
|
||||
demangled_name: None,
|
||||
address: offset,
|
||||
section_address: address - section.address(),
|
||||
size: 0,
|
||||
size_known: false,
|
||||
flags: Default::default(),
|
||||
addend: offset_addr as i64,
|
||||
diff_symbol: None,
|
||||
instructions: vec![],
|
||||
match_percent: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn relocations_by_section(
|
||||
arch: ObjArchitecture,
|
||||
obj_file: &File<'_>,
|
||||
section: &mut ObjSection,
|
||||
) -> Result<Vec<ObjReloc>> {
|
||||
let obj_section = obj_file
|
||||
.section_by_name(§ion.name)
|
||||
.ok_or_else(|| anyhow::Error::msg("Failed to locate section"))?;
|
||||
let mut relocations = Vec::<ObjReloc>::new();
|
||||
for (address, reloc) in obj_section.relocations() {
|
||||
let symbol = match reloc.target() {
|
||||
RelocationTarget::Symbol(idx) => obj_file
|
||||
.symbol_by_index(idx)
|
||||
.context("Failed to locate relocation target symbol")?,
|
||||
_ => {
|
||||
return Err(anyhow::Error::msg(format!(
|
||||
"Unhandled relocation target: {:?}",
|
||||
reloc.target()
|
||||
)));
|
||||
}
|
||||
};
|
||||
let kind = match reloc.kind() {
|
||||
RelocationKind::Absolute => ObjRelocKind::Absolute,
|
||||
RelocationKind::Elf(kind) => match arch {
|
||||
ObjArchitecture::PowerPc => match kind {
|
||||
R_PPC_ADDR16_LO => ObjRelocKind::PpcAddr16Lo,
|
||||
R_PPC_ADDR16_HI => ObjRelocKind::PpcAddr16Hi,
|
||||
R_PPC_ADDR16_HA => ObjRelocKind::PpcAddr16Ha,
|
||||
R_PPC_REL24 => ObjRelocKind::PpcRel24,
|
||||
R_PPC_REL14 => ObjRelocKind::PpcRel14,
|
||||
R_PPC_EMB_SDA21 => ObjRelocKind::PpcEmbSda21,
|
||||
_ => {
|
||||
return Err(anyhow::Error::msg(format!(
|
||||
"Unhandled PPC relocation type: {kind}"
|
||||
)))
|
||||
}
|
||||
},
|
||||
ObjArchitecture::Mips => match kind {
|
||||
R_MIPS_26 => ObjRelocKind::Mips26,
|
||||
R_MIPS_HI16 => ObjRelocKind::MipsHi16,
|
||||
R_MIPS_LO16 => ObjRelocKind::MipsLo16,
|
||||
_ => {
|
||||
return Err(anyhow::Error::msg(format!(
|
||||
"Unhandled MIPS relocation type: {kind}"
|
||||
)))
|
||||
}
|
||||
},
|
||||
},
|
||||
_ => {
|
||||
return Err(anyhow::Error::msg(format!(
|
||||
"Unhandled relocation type: {:?}",
|
||||
reloc.kind()
|
||||
)))
|
||||
}
|
||||
};
|
||||
let target_section = match symbol.section() {
|
||||
SymbolSection::Common => Some(".comm".to_string()),
|
||||
SymbolSection::Section(idx) => {
|
||||
obj_file.section_by_index(idx).and_then(|s| s.name().map(|s| s.to_string())).ok()
|
||||
}
|
||||
_ => 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 = u32::from_be_bytes(
|
||||
section.data[address as usize..address as usize + 4].try_into()?,
|
||||
);
|
||||
match kind {
|
||||
ObjRelocKind::Absolute => addend,
|
||||
ObjRelocKind::MipsHi16 | ObjRelocKind::MipsLo16 => addend & 0x0000FFFF,
|
||||
ObjRelocKind::Mips26 => (addend & 0x03FFFFFF) * 4,
|
||||
_ => todo!(),
|
||||
}
|
||||
} else {
|
||||
let addend = reloc.addend();
|
||||
if addend < 0 {
|
||||
return Err(anyhow::Error::msg(format!(
|
||||
"Negative addend in section reloc: {addend}"
|
||||
)));
|
||||
}
|
||||
addend as u32
|
||||
};
|
||||
find_section_symbol(obj_file, &symbol, addend as u64)
|
||||
}
|
||||
_ => Err(anyhow::Error::msg(format!(
|
||||
"Unhandled relocation symbol type {:?}",
|
||||
symbol.kind()
|
||||
))),
|
||||
}?;
|
||||
relocations.push(ObjReloc { kind, address, target, target_section });
|
||||
}
|
||||
Ok(relocations)
|
||||
}
|
||||
|
||||
pub fn read(obj_path: &Path) -> Result<ObjInfo> {
|
||||
let data = {
|
||||
let file = fs::File::open(obj_path)?;
|
||||
unsafe { memmap2::Mmap::map(&file) }?
|
||||
};
|
||||
let obj_file = File::parse(&*data)?;
|
||||
let architecture = match obj_file.architecture() {
|
||||
Architecture::PowerPc => ObjArchitecture::PowerPc,
|
||||
Architecture::Mips => ObjArchitecture::Mips,
|
||||
_ => {
|
||||
return Err(anyhow::Error::msg(format!(
|
||||
"Unsupported architecture: {:?}",
|
||||
obj_file.architecture()
|
||||
)))
|
||||
}
|
||||
};
|
||||
let mut result = ObjInfo {
|
||||
architecture,
|
||||
path: obj_path.to_owned(),
|
||||
sections: filter_sections(&obj_file)?,
|
||||
common: common_symbols(&obj_file)?,
|
||||
};
|
||||
for section in &mut result.sections {
|
||||
section.symbols = symbols_by_section(&obj_file, section)?;
|
||||
section.relocations = relocations_by_section(architecture, &obj_file, section)?;
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use rabbitizer::{config_set_register_fpr_abi_names, Abi, Instruction, SimpleOperandType};
|
||||
|
||||
use crate::obj::{ObjIns, ObjInsArg, ObjReloc};
|
||||
|
||||
pub fn process_code(
|
||||
data: &[u8],
|
||||
start_address: u64,
|
||||
end_address: u64,
|
||||
relocs: &[ObjReloc],
|
||||
) -> Result<(Vec<u8>, Vec<ObjIns>)> {
|
||||
config_set_register_fpr_abi_names(Abi::RABBITIZER_ABI_O32);
|
||||
|
||||
let ins_count = data.len() / 4;
|
||||
let mut ops = Vec::<u8>::with_capacity(ins_count);
|
||||
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
|
||||
let mut cur_addr = start_address as u32;
|
||||
for chunk in data.chunks_exact(4) {
|
||||
let reloc = relocs.iter().find(|r| (r.address as u32 & !3) == cur_addr);
|
||||
let code = u32::from_be_bytes(chunk.try_into()?);
|
||||
let mut instruction = Instruction::new(code, cur_addr);
|
||||
|
||||
let op = instruction.instr_id() as u8;
|
||||
ops.push(op);
|
||||
|
||||
let mnemonic = instruction.instr_id().get_opcode_name().unwrap_or_default().to_string();
|
||||
let is_branch = instruction.is_branch();
|
||||
let branch_offset = instruction.branch_offset();
|
||||
let branch_dest =
|
||||
if is_branch { Some((cur_addr as i32 + branch_offset) as u32) } else { None };
|
||||
let args = instruction
|
||||
.simple_operands()
|
||||
.iter()
|
||||
.map(|op| match op.kind {
|
||||
SimpleOperandType::Imm | SimpleOperandType::Label => {
|
||||
if is_branch {
|
||||
ObjInsArg::BranchOffset(branch_offset)
|
||||
} else if let Some(reloc) = reloc {
|
||||
if matches!(&reloc.target_section, Some(s) if s == ".text")
|
||||
&& reloc.target.address > start_address
|
||||
&& reloc.target.address < end_address
|
||||
{
|
||||
// Inter-function reloc, convert to branch offset
|
||||
ObjInsArg::BranchOffset(reloc.target.address as i32 - cur_addr as i32)
|
||||
} else {
|
||||
ObjInsArg::Reloc
|
||||
}
|
||||
} else {
|
||||
ObjInsArg::MipsArg(op.disassembled.clone())
|
||||
}
|
||||
}
|
||||
SimpleOperandType::ImmBase => {
|
||||
if reloc.is_some() {
|
||||
ObjInsArg::RelocWithBase
|
||||
} else {
|
||||
ObjInsArg::MipsArg(op.disassembled.clone())
|
||||
}
|
||||
}
|
||||
_ => ObjInsArg::MipsArg(op.disassembled.clone()),
|
||||
})
|
||||
.collect();
|
||||
insts.push(ObjIns {
|
||||
address: cur_addr,
|
||||
code,
|
||||
op,
|
||||
mnemonic,
|
||||
args,
|
||||
reloc: reloc.cloned(),
|
||||
branch_dest,
|
||||
});
|
||||
cur_addr += 4;
|
||||
}
|
||||
Ok((ops, insts))
|
||||
}
|
||||
165
src/obj/mod.rs
@@ -1,165 +0,0 @@
|
||||
pub mod elf;
|
||||
pub mod mips;
|
||||
pub mod ppc;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use flagset::{flags, FlagSet};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
pub enum ObjSectionKind {
|
||||
Code,
|
||||
Data,
|
||||
Bss,
|
||||
}
|
||||
flags! {
|
||||
pub enum ObjSymbolFlags: u8 {
|
||||
Global,
|
||||
Local,
|
||||
Weak,
|
||||
Common,
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Copy, Clone, Default)]
|
||||
pub struct ObjSymbolFlagSet(pub(crate) FlagSet<ObjSymbolFlags>);
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjSection {
|
||||
pub name: String,
|
||||
pub kind: ObjSectionKind,
|
||||
pub address: u64,
|
||||
pub size: u64,
|
||||
pub data: Vec<u8>,
|
||||
pub index: usize,
|
||||
pub symbols: Vec<ObjSymbol>,
|
||||
pub relocations: Vec<ObjReloc>,
|
||||
|
||||
// Diff
|
||||
pub data_diff: Vec<ObjDataDiff>,
|
||||
pub match_percent: f32,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ObjInsArg {
|
||||
PpcArg(ppc750cl::Argument),
|
||||
MipsArg(String),
|
||||
Reloc,
|
||||
RelocWithBase,
|
||||
BranchOffset(i32),
|
||||
}
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ObjInsArgDiff {
|
||||
/// Incrementing index for coloring
|
||||
pub idx: usize,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjInsBranchFrom {
|
||||
/// Source instruction indices
|
||||
pub ins_idx: Vec<usize>,
|
||||
/// Incrementing index for coloring
|
||||
pub branch_idx: usize,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjInsBranchTo {
|
||||
/// Target instruction index
|
||||
pub ins_idx: usize,
|
||||
/// Incrementing index for coloring
|
||||
pub branch_idx: usize,
|
||||
}
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
|
||||
pub enum ObjInsDiffKind {
|
||||
#[default]
|
||||
None,
|
||||
OpMismatch,
|
||||
ArgMismatch,
|
||||
Replace,
|
||||
Delete,
|
||||
Insert,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjIns {
|
||||
pub address: u32,
|
||||
pub code: u32,
|
||||
pub op: u8,
|
||||
pub mnemonic: String,
|
||||
pub args: Vec<ObjInsArg>,
|
||||
pub reloc: Option<ObjReloc>,
|
||||
pub branch_dest: Option<u32>,
|
||||
}
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ObjInsDiff {
|
||||
pub ins: Option<ObjIns>,
|
||||
/// Diff kind
|
||||
pub kind: ObjInsDiffKind,
|
||||
/// Branches from instruction
|
||||
pub branch_from: Option<ObjInsBranchFrom>,
|
||||
/// Branches to instruction
|
||||
pub branch_to: Option<ObjInsBranchTo>,
|
||||
/// Arg diffs
|
||||
pub arg_diff: Vec<Option<ObjInsArgDiff>>,
|
||||
}
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
|
||||
pub enum ObjDataDiffKind {
|
||||
#[default]
|
||||
None,
|
||||
Replace,
|
||||
Delete,
|
||||
Insert,
|
||||
}
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ObjDataDiff {
|
||||
pub data: Vec<u8>,
|
||||
pub kind: ObjDataDiffKind,
|
||||
pub len: usize,
|
||||
pub symbol: String,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjSymbol {
|
||||
pub name: String,
|
||||
pub demangled_name: Option<String>,
|
||||
pub address: u64,
|
||||
pub section_address: u64,
|
||||
pub size: u64,
|
||||
pub size_known: bool,
|
||||
pub flags: ObjSymbolFlagSet,
|
||||
pub addend: i64,
|
||||
|
||||
// Diff
|
||||
pub diff_symbol: Option<String>,
|
||||
pub instructions: Vec<ObjInsDiff>,
|
||||
pub match_percent: Option<f32>,
|
||||
}
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum ObjArchitecture {
|
||||
PowerPc,
|
||||
Mips,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjInfo {
|
||||
pub architecture: ObjArchitecture,
|
||||
pub path: PathBuf,
|
||||
pub sections: Vec<ObjSection>,
|
||||
pub common: Vec<ObjSymbol>,
|
||||
}
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
pub enum ObjRelocKind {
|
||||
Absolute,
|
||||
PpcAddr16Hi,
|
||||
PpcAddr16Ha,
|
||||
PpcAddr16Lo,
|
||||
// PpcAddr32,
|
||||
// PpcRel32,
|
||||
// PpcAddr24,
|
||||
PpcRel24,
|
||||
// PpcAddr14,
|
||||
PpcRel14,
|
||||
PpcEmbSda21,
|
||||
Mips26,
|
||||
MipsHi16,
|
||||
MipsLo16,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjReloc {
|
||||
pub kind: ObjRelocKind,
|
||||
pub address: u64,
|
||||
pub target: ObjSymbol,
|
||||
pub target_section: Option<String>,
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use ppc750cl::{disasm_iter, Argument};
|
||||
|
||||
use crate::obj::{ObjIns, ObjInsArg, ObjReloc, ObjRelocKind};
|
||||
|
||||
// Relative relocation, can be Simm or BranchOffset
|
||||
fn is_relative_arg(arg: &ObjInsArg) -> bool {
|
||||
matches!(arg, ObjInsArg::PpcArg(Argument::Simm(_)) | ObjInsArg::BranchOffset(_))
|
||||
}
|
||||
|
||||
// Relative or absolute relocation, can be Uimm, Simm or Offset
|
||||
fn is_rel_abs_arg(arg: &ObjInsArg) -> bool {
|
||||
matches!(arg, ObjInsArg::PpcArg(arg) if matches!(arg, Argument::Uimm(_) | Argument::Simm(_) | Argument::Offset(_)))
|
||||
}
|
||||
|
||||
fn is_offset_arg(arg: &ObjInsArg) -> bool { matches!(arg, ObjInsArg::PpcArg(Argument::Offset(_))) }
|
||||
|
||||
pub fn process_code(
|
||||
data: &[u8],
|
||||
address: u64,
|
||||
relocs: &[ObjReloc],
|
||||
) -> Result<(Vec<u8>, Vec<ObjIns>)> {
|
||||
let ins_count = data.len() / 4;
|
||||
let mut ops = Vec::<u8>::with_capacity(ins_count);
|
||||
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
|
||||
for mut ins in disasm_iter(data, address as u32) {
|
||||
let reloc = relocs.iter().find(|r| (r.address as u32 & !3) == ins.addr);
|
||||
if let Some(reloc) = reloc {
|
||||
// Zero out relocations
|
||||
ins.code = match reloc.kind {
|
||||
ObjRelocKind::PpcEmbSda21 => ins.code & !0x1FFFFF,
|
||||
ObjRelocKind::PpcRel24 => ins.code & !0x3FFFFFC,
|
||||
ObjRelocKind::PpcRel14 => ins.code & !0xFFFC,
|
||||
ObjRelocKind::PpcAddr16Hi
|
||||
| ObjRelocKind::PpcAddr16Ha
|
||||
| ObjRelocKind::PpcAddr16Lo => ins.code & !0xFFFF,
|
||||
_ => ins.code,
|
||||
};
|
||||
}
|
||||
let simplified = ins.simplified();
|
||||
let mut args: Vec<ObjInsArg> = simplified
|
||||
.args
|
||||
.iter()
|
||||
.map(|a| match a {
|
||||
Argument::BranchDest(dest) => ObjInsArg::BranchOffset(dest.0),
|
||||
_ => ObjInsArg::PpcArg(a.clone()),
|
||||
})
|
||||
.collect();
|
||||
if let Some(reloc) = reloc {
|
||||
match reloc.kind {
|
||||
ObjRelocKind::PpcEmbSda21 => {
|
||||
args = vec![args[0].clone(), ObjInsArg::Reloc];
|
||||
}
|
||||
ObjRelocKind::PpcRel24 | ObjRelocKind::PpcRel14 => {
|
||||
let arg = args
|
||||
.iter_mut()
|
||||
.rfind(|a| is_relative_arg(a))
|
||||
.ok_or_else(|| anyhow::Error::msg("Failed to locate rel arg for reloc"))?;
|
||||
*arg = ObjInsArg::Reloc;
|
||||
}
|
||||
ObjRelocKind::PpcAddr16Hi
|
||||
| ObjRelocKind::PpcAddr16Ha
|
||||
| ObjRelocKind::PpcAddr16Lo => {
|
||||
let arg = args.iter_mut().rfind(|a| is_rel_abs_arg(a)).ok_or_else(|| {
|
||||
anyhow::Error::msg("Failed to locate rel/abs arg for reloc")
|
||||
})?;
|
||||
*arg = if is_offset_arg(arg) {
|
||||
ObjInsArg::RelocWithBase
|
||||
} else {
|
||||
ObjInsArg::Reloc
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
ops.push(simplified.ins.op as u8);
|
||||
insts.push(ObjIns {
|
||||
address: simplified.ins.addr,
|
||||
code: simplified.ins.code,
|
||||
mnemonic: format!("{}{}", simplified.mnemonic, simplified.suffix),
|
||||
args,
|
||||
reloc: reloc.cloned(),
|
||||
op: 0,
|
||||
branch_dest: None,
|
||||
});
|
||||
}
|
||||
Ok((ops, insts))
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
#[cfg(windows)]
|
||||
use std::string::FromUtf16Error;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
#[cfg(windows)]
|
||||
use anyhow::{Context, Result};
|
||||
use const_format::formatcp;
|
||||
use egui::{output::OpenUrl, Color32};
|
||||
use self_update::cargo_crate_version;
|
||||
|
||||
use crate::{
|
||||
app::{AppConfig, DiffKind, ViewState},
|
||||
jobs::{bindiff::queue_bindiff, objdiff::queue_build, update::queue_update},
|
||||
update::RELEASE_URL,
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
fn process_utf16(bytes: &[u8]) -> Result<String, FromUtf16Error> {
|
||||
let u16_bytes: Vec<u16> = bytes
|
||||
.chunks_exact(2)
|
||||
.filter_map(|c| Some(u16::from_ne_bytes(c.try_into().ok()?)))
|
||||
.collect();
|
||||
String::from_utf16(&u16_bytes)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn wsl_cmd(args: &[&str]) -> Result<String> {
|
||||
use std::{os::windows::process::CommandExt, process::Command};
|
||||
let output = Command::new("wsl")
|
||||
.args(args)
|
||||
.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW)
|
||||
.output()
|
||||
.context("Failed to execute wsl")?;
|
||||
process_utf16(&output.stdout).context("Failed to process stdout")
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn fetch_wsl2_distros() -> Vec<String> {
|
||||
wsl_cmd(&["-l", "-q"])
|
||||
.map(|stdout| {
|
||||
stdout
|
||||
.split('\n')
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.map(|s| s.trim().to_string())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn config_ui(ui: &mut egui::Ui, config: &Arc<RwLock<AppConfig>>, view_state: &mut ViewState) {
|
||||
let mut config_guard = config.write().unwrap();
|
||||
let AppConfig {
|
||||
custom_make,
|
||||
available_wsl_distros,
|
||||
selected_wsl_distro,
|
||||
project_dir,
|
||||
target_obj_dir,
|
||||
base_obj_dir,
|
||||
obj_path,
|
||||
build_target,
|
||||
left_obj,
|
||||
right_obj,
|
||||
project_dir_change,
|
||||
queue_update_check,
|
||||
auto_update_check,
|
||||
} = &mut *config_guard;
|
||||
|
||||
ui.heading("Updates");
|
||||
ui.checkbox(auto_update_check, "Check for updates on startup");
|
||||
if ui.button("Check now").clicked() {
|
||||
*queue_update_check = true;
|
||||
}
|
||||
ui.label(format!("Current version: {}", cargo_crate_version!())).on_hover_ui_at_pointer(|ui| {
|
||||
ui.label(formatcp!("Git branch: {}", env!("VERGEN_GIT_BRANCH")));
|
||||
ui.label(formatcp!("Git commit: {}", env!("VERGEN_GIT_SHA")));
|
||||
ui.label(formatcp!("Build target: {}", env!("VERGEN_CARGO_TARGET_TRIPLE")));
|
||||
ui.label(formatcp!("Build type: {}", env!("VERGEN_CARGO_PROFILE")));
|
||||
});
|
||||
if let Some(state) = &view_state.check_update {
|
||||
ui.label(format!("Latest version: {}", state.latest_release.version));
|
||||
if state.update_available {
|
||||
ui.colored_label(Color32::LIGHT_GREEN, "Update available");
|
||||
ui.horizontal(|ui| {
|
||||
if state.found_binary && ui
|
||||
.button("Automatic")
|
||||
.on_hover_text_at_pointer(
|
||||
"Automatically download and replace the current build",
|
||||
)
|
||||
.clicked() {
|
||||
view_state.jobs.push(queue_update());
|
||||
}
|
||||
if ui
|
||||
.button("Manual")
|
||||
.on_hover_text_at_pointer("Open a link to the latest release on GitHub")
|
||||
.clicked()
|
||||
{
|
||||
ui.output().open_url =
|
||||
Some(OpenUrl { url: RELEASE_URL.to_string(), new_tab: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
|
||||
ui.heading("Build config");
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if available_wsl_distros.is_none() {
|
||||
*available_wsl_distros = Some(fetch_wsl2_distros());
|
||||
}
|
||||
egui::ComboBox::from_label("Run in WSL2")
|
||||
.selected_text(selected_wsl_distro.as_ref().unwrap_or(&"None".to_string()))
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(selected_wsl_distro, None, "None");
|
||||
for distro in available_wsl_distros.as_ref().unwrap() {
|
||||
ui.selectable_value(selected_wsl_distro, Some(distro.clone()), distro);
|
||||
}
|
||||
});
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let _ = available_wsl_distros;
|
||||
let _ = selected_wsl_distro;
|
||||
}
|
||||
|
||||
ui.label("Custom make program:");
|
||||
let mut custom_make_str = custom_make.clone().unwrap_or_default();
|
||||
if ui.text_edit_singleline(&mut custom_make_str).changed() {
|
||||
if custom_make_str.is_empty() {
|
||||
*custom_make = None;
|
||||
} else {
|
||||
*custom_make = Some(custom_make_str);
|
||||
}
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.heading("Project config");
|
||||
|
||||
if view_state.diff_kind == DiffKind::SplitObj {
|
||||
if ui.button("Select project dir").clicked() {
|
||||
if let Some(path) = rfd::FileDialog::new().pick_folder() {
|
||||
*project_dir = Some(path);
|
||||
*project_dir_change = true;
|
||||
*target_obj_dir = None;
|
||||
*base_obj_dir = None;
|
||||
*obj_path = None;
|
||||
}
|
||||
}
|
||||
if let Some(dir) = project_dir {
|
||||
ui.label(dir.to_string_lossy());
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
if let Some(project_dir) = project_dir {
|
||||
if ui.button("Select target build dir").clicked() {
|
||||
if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder()
|
||||
{
|
||||
*target_obj_dir = Some(path);
|
||||
*obj_path = None;
|
||||
}
|
||||
}
|
||||
if let Some(dir) = target_obj_dir {
|
||||
ui.label(dir.to_string_lossy());
|
||||
}
|
||||
ui.checkbox(build_target, "Build target");
|
||||
|
||||
ui.separator();
|
||||
|
||||
if ui.button("Select base build dir").clicked() {
|
||||
if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder()
|
||||
{
|
||||
*base_obj_dir = Some(path);
|
||||
*obj_path = None;
|
||||
}
|
||||
}
|
||||
if let Some(dir) = base_obj_dir {
|
||||
ui.label(dir.to_string_lossy());
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
}
|
||||
|
||||
if let Some(base_dir) = base_obj_dir {
|
||||
if ui.button("Select obj").clicked() {
|
||||
if let Some(path) = rfd::FileDialog::new()
|
||||
.set_directory(&base_dir)
|
||||
.add_filter("Object file", &["o", "elf"])
|
||||
.pick_file()
|
||||
{
|
||||
let mut new_build_obj: Option<String> = None;
|
||||
if let Ok(obj_path) = path.strip_prefix(&base_dir) {
|
||||
new_build_obj = Some(obj_path.display().to_string());
|
||||
} else if let Some(build_asm_dir) = target_obj_dir {
|
||||
if let Ok(obj_path) = path.strip_prefix(&build_asm_dir) {
|
||||
new_build_obj = Some(obj_path.display().to_string());
|
||||
}
|
||||
}
|
||||
if let Some(new_build_obj) = new_build_obj {
|
||||
*obj_path = Some(new_build_obj);
|
||||
view_state
|
||||
.jobs
|
||||
.push(queue_build(config.clone(), view_state.diff_config.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(obj) = obj_path {
|
||||
ui.label(&*obj);
|
||||
if ui.button("Build").clicked() {
|
||||
view_state
|
||||
.jobs
|
||||
.push(queue_build(config.clone(), view_state.diff_config.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
}
|
||||
} else if view_state.diff_kind == DiffKind::WholeBinary {
|
||||
if ui.button("Select left obj").clicked() {
|
||||
if let Some(path) =
|
||||
rfd::FileDialog::new().add_filter("Object file", &["o", "elf"]).pick_file()
|
||||
{
|
||||
*left_obj = Some(path);
|
||||
}
|
||||
}
|
||||
if let Some(obj) = left_obj {
|
||||
ui.label(obj.to_string_lossy());
|
||||
}
|
||||
|
||||
if ui.button("Select right obj").clicked() {
|
||||
if let Some(path) =
|
||||
rfd::FileDialog::new().add_filter("Object file", &["o", "elf"]).pick_file()
|
||||
{
|
||||
*right_obj = Some(path);
|
||||
}
|
||||
}
|
||||
if let Some(obj) = right_obj {
|
||||
ui.label(obj.to_string_lossy());
|
||||
}
|
||||
|
||||
if let (Some(_), Some(_)) = (left_obj, right_obj) {
|
||||
if ui.button("Build").clicked() {
|
||||
view_state.jobs.push(queue_bindiff(config.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.checkbox(&mut view_state.reverse_fn_order, "Reverse function order (deferred)");
|
||||
ui.separator();
|
||||
}
|
||||