Compare commits
282 Commits
d1d6f1101b
...
v3.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f157f33a5 | |||
|
|
a1ea2919f8 | ||
| 9c31c82a37 | |||
| 4f34dfa194 | |||
| ae6d37a10b | |||
| 06e54f3456 | |||
| 6ed07bfaf1 | |||
| 80e939653a | |||
| 48305e0380 | |||
| 2eafbb218b | |||
| a1f2a535e5 | |||
| 8461b35cd7 | |||
| 95868f1d19 | |||
| 506c251d68 | |||
| f3c157ff06 | |||
|
|
6d3c63ccd8 | ||
| bbd8d9714f | |||
| 3c66ac3d54 | |||
| 561a9107e2 | |||
| e8de35b78e | |||
| d938988d43 | |||
|
|
3e6efb7736 | ||
|
|
674c942d7d | ||
|
|
6b7dcabbed | ||
| e202c3ef95 | |||
|
|
b7730b3d00 | ||
|
|
a4fdb61f04 | ||
|
|
2876be37a3 | ||
| 11171763eb | |||
| 6037a79ba2 | |||
| f7efe5fdff | |||
| 0692deac59 | |||
| c3e3d175c5 | |||
| c45f4bbc99 | |||
| b0c5431ac5 | |||
|
|
9ab246367b | ||
|
|
dcafe51eda | ||
| c65e87c382 | |||
| 1756b9f6c5 | |||
| 303f2938a2 | |||
| 526e031251 | |||
|
|
10b2a9c129 | ||
|
|
abe68ef2f2 | ||
|
|
304df96411 | ||
| 7aa878b48e | |||
| 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 |
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"
|
||||
243
.github/workflows/build.yaml
vendored
@@ -9,8 +9,8 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_BIN_NAME: objdiff
|
||||
CARGO_TARGET_DIR: target
|
||||
BUILD_PROFILE: release-lto
|
||||
CARGO_INCREMENTAL: 0
|
||||
|
||||
jobs:
|
||||
check:
|
||||
@@ -20,17 +20,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 --features all
|
||||
- name: Cargo clippy
|
||||
run: cargo clippy --all-features
|
||||
run: cargo clippy --features all
|
||||
|
||||
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,8 +63,8 @@ jobs:
|
||||
# Prevent new advisories from failing CI
|
||||
continue-on-error: ${{ matrix.checks == 'advisories' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
with:
|
||||
command: check ${{ matrix.checks }}
|
||||
|
||||
@@ -58,76 +78,233 @@ 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
|
||||
run: cargo test --release --features all
|
||||
|
||||
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-gnu
|
||||
target: x86_64-unknown-linux-musl
|
||||
name: linux-x86_64
|
||||
packages: libgtk-3-dev
|
||||
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.post1 cargo-zigbuild==0.19.8
|
||||
- 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: |
|
||||
target/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
|
||||
target/${{ 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:
|
||||
- platform: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu.2.31
|
||||
target_base: x86_64-unknown-linux-gnu
|
||||
name: linux-x86_64
|
||||
packages: libgtk-3-dev
|
||||
build: zigbuild
|
||||
features: default
|
||||
- platform: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
name: windows-x86_64
|
||||
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: 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: 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.post1 cargo-zigbuild==0.19.8
|
||||
- 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
|
||||
targets: ${{ matrix.target_base || matrix.target }}
|
||||
- name: Cache Rust workspace
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
name: ${{ matrix.name }}
|
||||
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 }}/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
|
||||
target/${{ matrix.target_base || matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
|
||||
target/${{ matrix.target_base || matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
|
||||
if-no-files-found: error
|
||||
|
||||
build-wasm:
|
||||
name: Build objdiff-wasm
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: rust-src
|
||||
- name: Cache Rust workspace
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Install dependencies
|
||||
run: npm -C objdiff-wasm install
|
||||
- name: Build
|
||||
run: npm -C objdiff-wasm run build
|
||||
|
||||
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/
|
||||
|
||||
6164
Cargo.lock
generated
78
Cargo.toml
@@ -1,62 +1,22 @@
|
||||
[package]
|
||||
name = "objdiff"
|
||||
version = "0.2.1"
|
||||
edition = "2021"
|
||||
rust-version = "1.62"
|
||||
[workspace]
|
||||
members = [
|
||||
"objdiff-cli",
|
||||
"objdiff-core",
|
||||
"objdiff-gui",
|
||||
"objdiff-wasm",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[profile.release-lto]
|
||||
inherits = "release"
|
||||
lto = "fat"
|
||||
strip = "debuginfo"
|
||||
codegen-units = 1
|
||||
|
||||
[workspace.package]
|
||||
version = "3.0.0-alpha.3"
|
||||
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.82"
|
||||
|
||||
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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
189
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,84 @@
|
||||
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" },
|
||||
]
|
||||
# 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",
|
||||
"Ubuntu-font-1.0",
|
||||
]
|
||||
# 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 +113,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 +152,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 +161,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 +237,14 @@ 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 = [
|
||||
"CelestialAmber", # cwextab, rlwinmdec
|
||||
"Decompollaborate", # rabbitizer
|
||||
"enarx", # flagset
|
||||
"encounter",
|
||||
]
|
||||
# gitlab.com organizations to allow git sources for
|
||||
gitlab = []
|
||||
# bitbucket.org organizations to allow git sources for
|
||||
bitbucket = []
|
||||
|
||||
34
objdiff-cli/Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
[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.4"
|
||||
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"] }
|
||||
typed-path = "0.10"
|
||||
|
||||
[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
|
||||
}
|
||||
470
objdiff-cli/src/cmd/diff.rs
Normal file
@@ -0,0 +1,470 @@
|
||||
use std::{
|
||||
io::stdout,
|
||||
mem,
|
||||
str::FromStr,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
task::{Wake, Waker},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use argp::FromArgs;
|
||||
use crossterm::{
|
||||
event,
|
||||
event::{DisableMouseCapture, EnableMouseCapture},
|
||||
terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, SetTitle,
|
||||
},
|
||||
};
|
||||
use objdiff_core::{
|
||||
bindings::diff::DiffResult,
|
||||
build::{
|
||||
watcher::{create_watcher, Watcher},
|
||||
BuildConfig,
|
||||
},
|
||||
config::{
|
||||
build_globset,
|
||||
path::{check_path_buf, platform_path, platform_path_serde_option},
|
||||
ProjectConfig, ProjectObject, ProjectObjectMetadata,
|
||||
},
|
||||
diff::{
|
||||
self, ConfigEnum, ConfigPropertyId, ConfigPropertyKind, DiffObjConfig, MappingConfig,
|
||||
ObjectDiff,
|
||||
},
|
||||
jobs::{
|
||||
objdiff::{start_build, ObjDiffConfig},
|
||||
Job, JobQueue, JobResult,
|
||||
},
|
||||
obj::{self, Object},
|
||||
};
|
||||
use ratatui::prelude::*;
|
||||
use typed_path::{Utf8PlatformPath, Utf8PlatformPathBuf};
|
||||
|
||||
use crate::{
|
||||
util::{
|
||||
output::{write_output, OutputFormat},
|
||||
term::crossterm_panic_handler,
|
||||
},
|
||||
views::{function_diff::FunctionDiffUi, EventControlFlow, EventResult, UiView},
|
||||
};
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// Diff two object files. (Interactive or one-shot mode)
|
||||
#[argp(subcommand, name = "diff")]
|
||||
pub struct Args {
|
||||
#[argp(option, short = '1', from_str_fn(platform_path))]
|
||||
/// Target object file
|
||||
target: Option<Utf8PlatformPathBuf>,
|
||||
#[argp(option, short = '2', from_str_fn(platform_path))]
|
||||
/// Base object file
|
||||
base: Option<Utf8PlatformPathBuf>,
|
||||
#[argp(option, short = 'p', from_str_fn(platform_path))]
|
||||
/// Project directory
|
||||
project: Option<Utf8PlatformPathBuf>,
|
||||
#[argp(option, short = 'u')]
|
||||
/// Unit name within project
|
||||
unit: Option<String>,
|
||||
#[argp(option, short = 'o', from_str_fn(platform_path))]
|
||||
/// Output file (one-shot mode) ("-" for stdout)
|
||||
output: Option<Utf8PlatformPathBuf>,
|
||||
#[argp(option)]
|
||||
/// Output format (json, json-pretty, proto) (default: json)
|
||||
format: Option<String>,
|
||||
#[argp(positional)]
|
||||
/// Function symbol to diff
|
||||
symbol: Option<String>,
|
||||
#[argp(option, short = 'c')]
|
||||
/// Configuration property (key=value)
|
||||
config: Vec<String>,
|
||||
#[argp(option, short = 'm')]
|
||||
/// Symbol mapping (target=base)
|
||||
mapping: Vec<String>,
|
||||
#[argp(option)]
|
||||
/// Left symbol name for selection
|
||||
selecting_left: Option<String>,
|
||||
#[argp(option)]
|
||||
/// Right symbol name for selection
|
||||
selecting_right: 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(_), Some(_), None, None)
|
||||
| (Some(_), None, None, None)
|
||||
| (None, Some(_), None, None) => (args.target.clone(), args.base.clone(), None),
|
||||
(None, None, p, u) => {
|
||||
let project = match p {
|
||||
Some(project) => project.clone(),
|
||||
_ => check_path_buf(
|
||||
std::env::current_dir().context("Failed to get the current directory")?,
|
||||
)
|
||||
.context("Current directory is not valid UTF-8")?,
|
||||
};
|
||||
let Some((project_config, project_config_info)) =
|
||||
objdiff_core::config::try_project_config(project.as_ref())
|
||||
else {
|
||||
bail!("Project config not found in {}", &project)
|
||||
};
|
||||
let project_config = project_config.with_context(|| {
|
||||
format!("Reading project config {}", project_config_info.path.display())
|
||||
})?;
|
||||
let target_obj_dir = project_config
|
||||
.target_dir
|
||||
.as_ref()
|
||||
.map(|p| project.join(p.with_platform_encoding()));
|
||||
let base_obj_dir = project_config
|
||||
.base_dir
|
||||
.as_ref()
|
||||
.map(|p| project.join(p.with_platform_encoding()));
|
||||
let objects = project_config
|
||||
.units
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|o| {
|
||||
ObjectConfig::new(
|
||||
o,
|
||||
&project,
|
||||
target_obj_dir.as_deref(),
|
||||
base_obj_dir.as_deref(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let object = if let Some(u) = u {
|
||||
objects
|
||||
.iter()
|
||||
.find(|obj| obj.name == *u)
|
||||
.ok_or_else(|| anyhow!("Unit not found: {}", u))?
|
||||
} else if let Some(symbol_name) = &args.symbol {
|
||||
let mut idx = None;
|
||||
let mut count = 0usize;
|
||||
for (i, obj) in objects.iter().enumerate() {
|
||||
if obj
|
||||
.target_path
|
||||
.as_deref()
|
||||
.map(|o| obj::read::has_function(o.as_ref(), 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)) => &objects[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 build_config_from_args(args: &Args) -> Result<(DiffObjConfig, MappingConfig)> {
|
||||
let mut diff_config = DiffObjConfig::default();
|
||||
for config in &args.config {
|
||||
let (key, value) = config.split_once('=').context("--config expects \"key=value\"")?;
|
||||
let property_id = ConfigPropertyId::from_str(key)
|
||||
.map_err(|()| anyhow!("Invalid configuration property: {}", key))?;
|
||||
diff_config.set_property_value_str(property_id, value).map_err(|()| {
|
||||
let mut options = String::new();
|
||||
match property_id.kind() {
|
||||
ConfigPropertyKind::Boolean => {
|
||||
options = "true, false".to_string();
|
||||
}
|
||||
ConfigPropertyKind::Choice(variants) => {
|
||||
for (i, variant) in variants.iter().enumerate() {
|
||||
if i > 0 {
|
||||
options.push_str(", ");
|
||||
}
|
||||
options.push_str(variant.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
anyhow!("Invalid value for {}. Expected one of: {}", property_id.name(), options)
|
||||
})?;
|
||||
}
|
||||
let mut mapping_config = MappingConfig {
|
||||
mappings: Default::default(),
|
||||
selecting_left: args.selecting_left.clone(),
|
||||
selecting_right: args.selecting_right.clone(),
|
||||
};
|
||||
for mapping in &args.mapping {
|
||||
let (target, base) =
|
||||
mapping.split_once('=').context("--mapping expects \"target=base\"")?;
|
||||
mapping_config.mappings.insert(target.to_string(), base.to_string());
|
||||
}
|
||||
Ok((diff_config, mapping_config))
|
||||
}
|
||||
|
||||
fn run_oneshot(
|
||||
args: &Args,
|
||||
output: &Utf8PlatformPath,
|
||||
target_path: Option<&Utf8PlatformPath>,
|
||||
base_path: Option<&Utf8PlatformPath>,
|
||||
) -> Result<()> {
|
||||
let output_format = OutputFormat::from_option(args.format.as_deref())?;
|
||||
let (diff_config, mapping_config) = build_config_from_args(args)?;
|
||||
let target = target_path
|
||||
.map(|p| {
|
||||
obj::read::read(p.as_ref(), &diff_config).with_context(|| format!("Loading {}", p))
|
||||
})
|
||||
.transpose()?;
|
||||
let base = base_path
|
||||
.map(|p| {
|
||||
obj::read::read(p.as_ref(), &diff_config).with_context(|| format!("Loading {}", p))
|
||||
})
|
||||
.transpose()?;
|
||||
let result =
|
||||
diff::diff_objs(target.as_ref(), base.as_ref(), None, &diff_config, &mapping_config)?;
|
||||
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(())
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
pub jobs: JobQueue,
|
||||
pub waker: Arc<TermWaker>,
|
||||
pub project_dir: Option<Utf8PlatformPathBuf>,
|
||||
pub project_config: Option<ProjectConfig>,
|
||||
pub target_path: Option<Utf8PlatformPathBuf>,
|
||||
pub base_path: Option<Utf8PlatformPathBuf>,
|
||||
pub left_obj: Option<(Object, ObjectDiff)>,
|
||||
pub right_obj: Option<(Object, ObjectDiff)>,
|
||||
pub prev_obj: Option<(Object, ObjectDiff)>,
|
||||
pub reload_time: Option<time::OffsetDateTime>,
|
||||
pub time_format: Vec<time::format_description::FormatItem<'static>>,
|
||||
pub watcher: Option<Watcher>,
|
||||
pub modified: Arc<AtomicBool>,
|
||||
pub diff_obj_config: DiffObjConfig,
|
||||
pub mapping_config: MappingConfig,
|
||||
}
|
||||
|
||||
fn create_objdiff_config(state: &AppState) -> ObjDiffConfig {
|
||||
ObjDiffConfig {
|
||||
build_config: BuildConfig {
|
||||
project_dir: state.project_dir.clone(),
|
||||
custom_make: state
|
||||
.project_config
|
||||
.as_ref()
|
||||
.and_then(|c| c.custom_make.as_ref())
|
||||
.cloned(),
|
||||
custom_args: state
|
||||
.project_config
|
||||
.as_ref()
|
||||
.and_then(|c| c.custom_args.as_ref())
|
||||
.cloned(),
|
||||
selected_wsl_distro: None,
|
||||
},
|
||||
build_base: state.project_config.as_ref().is_some_and(|p| p.build_base.unwrap_or(true)),
|
||||
build_target: state
|
||||
.project_config
|
||||
.as_ref()
|
||||
.is_some_and(|p| p.build_target.unwrap_or(false)),
|
||||
target_path: state.target_path.clone(),
|
||||
base_path: state.base_path.clone(),
|
||||
diff_obj_config: state.diff_obj_config.clone(),
|
||||
mapping_config: state.mapping_config.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The configuration for a single object file.
|
||||
#[derive(Default, Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct ObjectConfig {
|
||||
pub name: String,
|
||||
#[serde(default, with = "platform_path_serde_option")]
|
||||
pub target_path: Option<Utf8PlatformPathBuf>,
|
||||
#[serde(default, with = "platform_path_serde_option")]
|
||||
pub base_path: Option<Utf8PlatformPathBuf>,
|
||||
pub metadata: ProjectObjectMetadata,
|
||||
pub complete: Option<bool>,
|
||||
}
|
||||
|
||||
impl ObjectConfig {
|
||||
pub fn new(
|
||||
object: &ProjectObject,
|
||||
project_dir: &Utf8PlatformPath,
|
||||
target_obj_dir: Option<&Utf8PlatformPath>,
|
||||
base_obj_dir: Option<&Utf8PlatformPath>,
|
||||
) -> Self {
|
||||
let target_path = if let (Some(target_obj_dir), Some(path), None) =
|
||||
(target_obj_dir, &object.path, &object.target_path)
|
||||
{
|
||||
Some(target_obj_dir.join(path.with_platform_encoding()))
|
||||
} else {
|
||||
object.target_path.as_ref().map(|path| project_dir.join(path.with_platform_encoding()))
|
||||
};
|
||||
let base_path = if let (Some(base_obj_dir), Some(path), None) =
|
||||
(base_obj_dir, &object.path, &object.base_path)
|
||||
{
|
||||
Some(base_obj_dir.join(path.with_platform_encoding()))
|
||||
} else {
|
||||
object.base_path.as_ref().map(|path| project_dir.join(path.with_platform_encoding()))
|
||||
};
|
||||
Self {
|
||||
name: object.name().to_string(),
|
||||
target_path,
|
||||
base_path,
|
||||
metadata: object.metadata.clone().unwrap_or_default(),
|
||||
complete: object.complete(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
fn reload(&mut self) -> Result<()> {
|
||||
let config = create_objdiff_config(self);
|
||||
self.jobs.push_once(Job::ObjDiff, || start_build(Waker::from(self.waker.clone()), config));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_jobs(&mut self) -> Result<bool> {
|
||||
let mut redraw = false;
|
||||
self.jobs.collect_results();
|
||||
for result in mem::take(&mut self.jobs.results) {
|
||||
match result {
|
||||
JobResult::None => unreachable!("Unexpected JobResult::None"),
|
||||
JobResult::ObjDiff(result) => {
|
||||
let result = result.unwrap();
|
||||
self.left_obj = result.first_obj;
|
||||
self.right_obj = result.second_obj;
|
||||
self.reload_time = Some(result.time);
|
||||
redraw = true;
|
||||
}
|
||||
JobResult::CheckUpdate(_) => todo!("CheckUpdate"),
|
||||
JobResult::Update(_) => todo!("Update"),
|
||||
JobResult::CreateScratch(_) => todo!("CreateScratch"),
|
||||
}
|
||||
}
|
||||
Ok(redraw)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TermWaker(pub AtomicBool);
|
||||
|
||||
impl Wake for TermWaker {
|
||||
fn wake(self: Arc<Self>) { self.0.store(true, Ordering::Relaxed); }
|
||||
|
||||
fn wake_by_ref(self: &Arc<Self>) { self.0.store(true, Ordering::Relaxed); }
|
||||
}
|
||||
|
||||
fn run_interactive(
|
||||
args: Args,
|
||||
target_path: Option<Utf8PlatformPathBuf>,
|
||||
base_path: Option<Utf8PlatformPathBuf>,
|
||||
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 (diff_obj_config, mapping_config) = build_config_from_args(&args)?;
|
||||
let mut state = AppState {
|
||||
jobs: Default::default(),
|
||||
waker: Default::default(),
|
||||
project_dir: args.project.clone(),
|
||||
project_config,
|
||||
target_path,
|
||||
base_path,
|
||||
left_obj: None,
|
||||
right_obj: None,
|
||||
prev_obj: None,
|
||||
reload_time: None,
|
||||
time_format,
|
||||
watcher: None,
|
||||
modified: Default::default(),
|
||||
diff_obj_config,
|
||||
mapping_config,
|
||||
};
|
||||
if let (Some(project_dir), Some(project_config)) = (&state.project_dir, &state.project_config) {
|
||||
let watch_patterns = project_config.build_watch_patterns()?;
|
||||
state.watcher = Some(create_watcher(
|
||||
state.modified.clone(),
|
||||
project_dir.as_ref(),
|
||||
build_globset(&watch_patterns)?,
|
||||
Waker::from(state.waker.clone()),
|
||||
)?);
|
||||
}
|
||||
let mut view: Box<dyn UiView> =
|
||||
Box::new(FunctionDiffUi { symbol_name: symbol_name.clone(), ..Default::default() });
|
||||
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)?;
|
||||
|
||||
let mut result = EventResult { redraw: true, ..Default::default() };
|
||||
'outer: loop {
|
||||
if result.redraw {
|
||||
terminal.draw(|f| loop {
|
||||
result.redraw = false;
|
||||
view.draw(&state, f, &mut result);
|
||||
result.click_xy = None;
|
||||
if !result.redraw {
|
||||
break;
|
||||
}
|
||||
// Clear buffer on redraw
|
||||
f.buffer_mut().reset();
|
||||
})?;
|
||||
}
|
||||
loop {
|
||||
if event::poll(Duration::from_millis(100))? {
|
||||
match view.handle_event(&mut state, event::read()?) {
|
||||
EventControlFlow::Break => break 'outer,
|
||||
EventControlFlow::Continue(r) => result = r,
|
||||
EventControlFlow::Reload => {
|
||||
state.reload()?;
|
||||
result.redraw = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
} else if state.waker.0.swap(false, Ordering::Relaxed) {
|
||||
if state.modified.swap(false, Ordering::Relaxed) {
|
||||
state.reload()?;
|
||||
}
|
||||
result.redraw = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if state.check_jobs()? {
|
||||
result.redraw = true;
|
||||
view.reload(&state)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset terminal
|
||||
disable_raw_mode()?;
|
||||
crossterm::execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture)?;
|
||||
terminal.show_cursor()?;
|
||||
Ok(())
|
||||
}
|
||||
2
objdiff-cli/src/cmd/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod diff;
|
||||
pub mod report;
|
||||
423
objdiff-cli/src/cmd/report.rs
Normal file
@@ -0,0 +1,423 @@
|
||||
use std::{collections::HashSet, fs::File, io::Read, 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::path::platform_path,
|
||||
diff, obj,
|
||||
obj::{SectionKind, SymbolFlag},
|
||||
};
|
||||
use prost::Message;
|
||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||
use tracing::{info, warn};
|
||||
use typed_path::{Utf8PlatformPath, Utf8PlatformPathBuf};
|
||||
|
||||
use crate::{
|
||||
cmd::diff::ObjectConfig,
|
||||
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', from_str_fn(platform_path))]
|
||||
/// Project directory
|
||||
project: Option<Utf8PlatformPathBuf>,
|
||||
#[argp(option, short = 'o', from_str_fn(platform_path))]
|
||||
/// Output file
|
||||
output: Option<Utf8PlatformPathBuf>,
|
||||
#[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, from_str_fn(platform_path))]
|
||||
/// Previous report file
|
||||
previous: Utf8PlatformPathBuf,
|
||||
#[argp(positional, from_str_fn(platform_path))]
|
||||
/// Current report file
|
||||
current: Utf8PlatformPathBuf,
|
||||
#[argp(option, short = 'o', from_str_fn(platform_path))]
|
||||
/// Output file
|
||||
output: Option<Utf8PlatformPathBuf>,
|
||||
#[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(|| Utf8PlatformPath::new("."));
|
||||
info!("Loading project {}", project_dir);
|
||||
|
||||
let project = match objdiff_core::config::try_project_config(project_dir.as_ref()) {
|
||||
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 target_obj_dir =
|
||||
project.target_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding()));
|
||||
let base_obj_dir =
|
||||
project.base_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding()));
|
||||
let objects = project
|
||||
.units
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|o| {
|
||||
ObjectConfig::new(o, project_dir, target_obj_dir.as_deref(), base_obj_dir.as_deref())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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 &objects {
|
||||
if let Some(unit) = report_object(object, Some(&mut existing_functions))? {
|
||||
units.push(unit);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let vec = objects
|
||||
.par_iter()
|
||||
.map(|object| report_object(object, 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: &ObjectConfig,
|
||||
mut existing_functions: Option<&mut HashSet<String>>,
|
||||
) -> Result<Option<ReportUnit>> {
|
||||
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 diff_config = diff::DiffObjConfig {
|
||||
function_reloc_diffs: diff::FunctionRelocDiffs::None,
|
||||
..Default::default()
|
||||
};
|
||||
let mapping_config = diff::MappingConfig::default();
|
||||
let target = object
|
||||
.target_path
|
||||
.as_ref()
|
||||
.map(|p| {
|
||||
obj::read::read(p.as_ref(), &diff_config)
|
||||
.with_context(|| format!("Failed to open {}", p))
|
||||
})
|
||||
.transpose()?;
|
||||
let base = object
|
||||
.base_path
|
||||
.as_ref()
|
||||
.map(|p| {
|
||||
obj::read::read(p.as_ref(), &diff_config)
|
||||
.with_context(|| format!("Failed to open {}", p))
|
||||
})
|
||||
.transpose()?;
|
||||
let result =
|
||||
diff::diff_objs(target.as_ref(), base.as_ref(), None, &diff_config, &mapping_config)?;
|
||||
|
||||
let metadata = ReportUnitMetadata {
|
||||
complete: object.metadata.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.source_path.as_ref().map(|p| p.to_string()),
|
||||
progress_categories: object.metadata.progress_categories.clone().unwrap_or_default(),
|
||||
auto_generated: object.metadata.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_idx, section), section_diff) in
|
||||
obj.sections.iter().enumerate().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 {
|
||||
SectionKind::Data | SectionKind::Bss => {
|
||||
measures.total_data += section.size;
|
||||
if section_match_percent == 100.0 {
|
||||
measures.matched_data += section.size;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
for (symbol, symbol_diff) in obj.symbols.iter().zip(&obj_diff.symbols) {
|
||||
if symbol.section != Some(section_idx)
|
||||
|| symbol.size == 0
|
||||
|| symbol.flags.contains(SymbolFlag::Hidden)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if let Some(existing_functions) = &mut existing_functions {
|
||||
if (symbol.flags.contains(SymbolFlag::Global)
|
||||
|| symbol.flags.contains(SymbolFlag::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.clone(),
|
||||
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 == "-" && args.current == "-" {
|
||||
// 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: &Utf8PlatformPath) -> Result<Report> {
|
||||
if path == Utf8PlatformPath::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))?;
|
||||
let mmap =
|
||||
unsafe { memmap2::Mmap::map(&file) }.with_context(|| format!("Failed to map {}", path))?;
|
||||
Report::parse(mmap.as_ref()).with_context(|| format!("Failed to load report {}", path))
|
||||
}
|
||||
150
objdiff-cli/src/main.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
mod argp_version;
|
||||
mod cmd;
|
||||
mod util;
|
||||
mod views;
|
||||
|
||||
// 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;
|
||||
87
objdiff-cli/src/util/output.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
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, P>(input: &T, output: Option<P>, format: OutputFormat) -> Result<()>
|
||||
where
|
||||
T: serde::Serialize + prost::Message,
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
match output.as_ref().map(|p| p.as_ref()) {
|
||||
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);
|
||||
}));
|
||||
}
|
||||
638
objdiff-cli/src/views/function_diff.rs
Normal file
@@ -0,0 +1,638 @@
|
||||
use core::cmp::Ordering;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind};
|
||||
use objdiff_core::{
|
||||
diff::{
|
||||
display::{display_row, DiffText, DiffTextColor, HighlightKind},
|
||||
DiffObjConfig, FunctionRelocDiffs, InstructionDiffKind, ObjectDiff, SymbolDiff,
|
||||
},
|
||||
obj::Object,
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, Clear, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::{EventControlFlow, EventResult, UiView};
|
||||
use crate::cmd::diff::AppState;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Default)]
|
||||
pub struct FunctionDiffUi {
|
||||
pub symbol_name: String,
|
||||
pub left_highlight: HighlightKind,
|
||||
pub right_highlight: HighlightKind,
|
||||
pub scroll_x: usize,
|
||||
pub scroll_state_x: ScrollbarState,
|
||||
pub scroll_y: usize,
|
||||
pub scroll_state_y: ScrollbarState,
|
||||
pub per_page: usize,
|
||||
pub num_rows: usize,
|
||||
pub left_sym: Option<usize>,
|
||||
pub right_sym: Option<usize>,
|
||||
pub prev_sym: Option<usize>,
|
||||
pub open_options: bool,
|
||||
pub three_way: bool,
|
||||
}
|
||||
|
||||
impl UiView for FunctionDiffUi {
|
||||
fn draw(&mut self, state: &AppState, 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(state.right_obj.as_ref(), self.right_sym)
|
||||
.and_then(|(_, _, d)| d.match_percent)
|
||||
{
|
||||
line_r.spans.push(Span::styled(
|
||||
format!("{:.2}% ", percent),
|
||||
Style::new().fg(match_percent_color(percent)),
|
||||
));
|
||||
}
|
||||
let reload_time = state
|
||||
.reload_time
|
||||
.as_ref()
|
||||
.and_then(|t| t.format(&state.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),
|
||||
));
|
||||
line_r.spans.push(Span::styled(
|
||||
format!(" ({} jobs)", state.jobs.jobs.len()),
|
||||
Style::new().fg(Color::LightYellow),
|
||||
));
|
||||
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((obj, symbol_idx, symbol_diff)) =
|
||||
get_symbol(state.left_obj.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,
|
||||
obj,
|
||||
symbol_idx,
|
||||
symbol_diff,
|
||||
&state.diff_obj_config,
|
||||
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((obj, symbol_idx, symbol_diff)) =
|
||||
get_symbol(state.right_obj.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,
|
||||
obj,
|
||||
symbol_idx,
|
||||
symbol_diff,
|
||||
&state.diff_obj_config,
|
||||
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((obj, symbol_idx, symbol_diff)) =
|
||||
get_symbol(state.prev_obj.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,
|
||||
obj,
|
||||
symbol_idx,
|
||||
symbol_diff,
|
||||
&state.diff_obj_config,
|
||||
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;
|
||||
}
|
||||
|
||||
if self.open_options {
|
||||
self.draw_options(f, result);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, state: &mut AppState, 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;
|
||||
}
|
||||
// Cycle through function relocation diff mode
|
||||
KeyCode::Char('x') => {
|
||||
state.diff_obj_config.function_reloc_diffs =
|
||||
match state.diff_obj_config.function_reloc_diffs {
|
||||
FunctionRelocDiffs::None => FunctionRelocDiffs::NameAddress,
|
||||
FunctionRelocDiffs::NameAddress => FunctionRelocDiffs::DataValue,
|
||||
FunctionRelocDiffs::DataValue => FunctionRelocDiffs::All,
|
||||
FunctionRelocDiffs::All => FunctionRelocDiffs::None,
|
||||
};
|
||||
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 reload(&mut self, state: &AppState) -> Result<()> {
|
||||
let left_sym =
|
||||
state.left_obj.as_ref().and_then(|(o, _)| find_function(o, &self.symbol_name));
|
||||
let right_sym =
|
||||
state.right_obj.as_ref().and_then(|(o, _)| find_function(o, &self.symbol_name));
|
||||
let prev_sym =
|
||||
state.prev_obj.as_ref().and_then(|(o, _)| find_function(o, &self.symbol_name));
|
||||
self.num_rows = match (
|
||||
get_symbol(state.left_obj.as_ref(), left_sym),
|
||||
get_symbol(state.right_obj.as_ref(), right_sym),
|
||||
) {
|
||||
(Some((_l, _ls, ld)), Some((_r, _rs, rd))) => {
|
||||
ld.instruction_rows.len().max(rd.instruction_rows.len())
|
||||
}
|
||||
(Some((_l, _ls, ld)), None) => ld.instruction_rows.len(),
|
||||
(None, Some((_r, _rs, rd))) => rd.instruction_rows.len(),
|
||||
(None, None) => bail!("Symbol not found: {}", self.symbol_name),
|
||||
};
|
||||
self.left_sym = left_sym;
|
||||
self.right_sym = right_sym;
|
||||
self.prev_sym = prev_sym;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FunctionDiffUi {
|
||||
pub 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 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 };
|
||||
}
|
||||
|
||||
fn print_sym(
|
||||
&self,
|
||||
out: &mut Text<'static>,
|
||||
obj: &Object,
|
||||
symbol_index: usize,
|
||||
symbol_diff: &SymbolDiff,
|
||||
diff_config: &DiffObjConfig,
|
||||
rect: Rect,
|
||||
highlight: &HighlightKind,
|
||||
result: &EventResult,
|
||||
only_changed: bool,
|
||||
) -> Option<HighlightKind> {
|
||||
let mut new_highlight = None;
|
||||
for (y, ins_row) in symbol_diff
|
||||
.instruction_rows
|
||||
.iter()
|
||||
.skip(self.scroll_y)
|
||||
.take(rect.height as usize)
|
||||
.enumerate()
|
||||
{
|
||||
if only_changed && ins_row.kind == InstructionDiffKind::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_row(obj, symbol_index, ins_row, diff_config, |segment| {
|
||||
let highlight_kind = HighlightKind::from(&segment.text);
|
||||
let label_text = match segment.text {
|
||||
DiffText::Basic(text) => text.to_string(),
|
||||
DiffText::Line(num) => format!("{num} "),
|
||||
DiffText::Address(addr) => format!("{:x}:", addr),
|
||||
DiffText::Opcode(mnemonic, _op) => format!("{mnemonic} "),
|
||||
DiffText::Argument(arg) => arg.to_string(),
|
||||
DiffText::BranchDest(addr) => format!("{addr:x}"),
|
||||
DiffText::Symbol(sym) => {
|
||||
sym.demangled_name.as_ref().unwrap_or(&sym.name).clone()
|
||||
}
|
||||
DiffText::Addend(addend) => match addend.cmp(&0i64) {
|
||||
Ordering::Greater => format!("+{:#x}", addend),
|
||||
Ordering::Less => format!("-{:#x}", -addend),
|
||||
_ => String::new(),
|
||||
},
|
||||
DiffText::Spacing(n) => {
|
||||
line.spans.push(Span::raw(" ".repeat(n as usize)));
|
||||
sx += n as u16;
|
||||
return Ok(());
|
||||
}
|
||||
DiffText::Eol => return Ok(()),
|
||||
};
|
||||
|
||||
let len = label_text.len();
|
||||
let highlighted =
|
||||
highlight_kind != HighlightKind::None && *highlight == highlight_kind;
|
||||
if let Some((cx, cy)) = result.click_xy {
|
||||
if cx >= sx && cx < sx + len as u16 && cy == sy {
|
||||
new_highlight = Some(highlight_kind);
|
||||
}
|
||||
}
|
||||
let mut style = Style::new().fg(match segment.color {
|
||||
DiffTextColor::Normal => Color::Gray,
|
||||
DiffTextColor::Dim => Color::DarkGray,
|
||||
DiffTextColor::Bright => Color::White,
|
||||
DiffTextColor::Replace => Color::Cyan,
|
||||
DiffTextColor::Delete => Color::Red,
|
||||
DiffTextColor::Insert => Color::Green,
|
||||
DiffTextColor::Rotating(i) => COLOR_ROTATION[i as usize % COLOR_ROTATION.len()],
|
||||
});
|
||||
if highlighted {
|
||||
style = style.bg(Color::DarkGray);
|
||||
}
|
||||
line.spans.push(Span::styled(label_text, style));
|
||||
sx += len as u16;
|
||||
if segment.pad_to as usize > len {
|
||||
let pad = (segment.pad_to as usize - 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: &SymbolDiff, rect: Rect) {
|
||||
for ins_row in symbol.instruction_rows.iter().skip(self.scroll_y).take(rect.height as usize)
|
||||
{
|
||||
if ins_row.kind != InstructionDiffKind::None {
|
||||
out.lines.push(Line::raw(match ins_row.kind {
|
||||
InstructionDiffKind::Delete => "<",
|
||||
InstructionDiffKind::Insert => ">",
|
||||
_ => "|",
|
||||
}));
|
||||
} else {
|
||||
out.lines.push(Line::raw(" "));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_symbol(
|
||||
obj: Option<&(Object, ObjectDiff)>,
|
||||
sym: Option<usize>,
|
||||
) -> Option<(&Object, usize, &SymbolDiff)> {
|
||||
let (obj, diff) = obj?;
|
||||
let sym = sym?;
|
||||
Some((obj, sym, &diff.symbols[sym]))
|
||||
}
|
||||
|
||||
fn find_function(obj: &Object, name: &str) -> Option<usize> {
|
||||
for (symbol_idx, symbol) in obj.symbols.iter().enumerate() {
|
||||
if symbol.name == name {
|
||||
return Some(symbol_idx);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
25
objdiff-cli/src/views/mod.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use anyhow::Result;
|
||||
use crossterm::event::Event;
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::cmd::diff::AppState;
|
||||
|
||||
pub mod function_diff;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EventResult {
|
||||
pub redraw: bool,
|
||||
pub click_xy: Option<(u16, u16)>,
|
||||
}
|
||||
|
||||
pub enum EventControlFlow {
|
||||
Break,
|
||||
Continue(EventResult),
|
||||
Reload,
|
||||
}
|
||||
|
||||
pub trait UiView {
|
||||
fn draw(&mut self, state: &AppState, f: &mut Frame, result: &mut EventResult);
|
||||
fn handle_event(&mut self, state: &mut AppState, event: Event) -> EventControlFlow;
|
||||
fn reload(&mut self, state: &AppState) -> Result<()>;
|
||||
}
|
||||
195
objdiff-core/Cargo.toml
Normal file
@@ -0,0 +1,195 @@
|
||||
[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"
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
all = [
|
||||
# Features
|
||||
"bindings",
|
||||
"build",
|
||||
"config",
|
||||
"dwarf",
|
||||
"serde",
|
||||
# Architectures
|
||||
"arm",
|
||||
"arm64",
|
||||
"mips",
|
||||
"ppc",
|
||||
"x86",
|
||||
]
|
||||
# Implicit, used to check if any arch is enabled
|
||||
any-arch = [
|
||||
"dep:flagset",
|
||||
"dep:heck",
|
||||
"dep:log",
|
||||
"dep:num-traits",
|
||||
"dep:prettyplease",
|
||||
"dep:proc-macro2",
|
||||
"dep:quote",
|
||||
"dep:regex",
|
||||
"dep:similar",
|
||||
"dep:syn",
|
||||
]
|
||||
bindings = [
|
||||
"dep:prost",
|
||||
"dep:prost-build",
|
||||
]
|
||||
build = [
|
||||
"dep:notify",
|
||||
"dep:notify-debouncer-full",
|
||||
"dep:reqwest",
|
||||
"dep:self_update",
|
||||
"dep:shell-escape",
|
||||
"dep:tempfile",
|
||||
"dep:time",
|
||||
"dep:winapi",
|
||||
]
|
||||
config = [
|
||||
"dep:globset",
|
||||
"dep:semver",
|
||||
"dep:typed-path",
|
||||
]
|
||||
dwarf = ["dep:gimli"]
|
||||
serde = [
|
||||
"dep:pbjson",
|
||||
"dep:pbjson-build",
|
||||
"dep:serde",
|
||||
"dep:serde_json",
|
||||
]
|
||||
std = [
|
||||
"anyhow/std",
|
||||
"flagset?/std",
|
||||
"log?/std",
|
||||
"num-traits?/std",
|
||||
"object/std",
|
||||
"prost?/std",
|
||||
"serde?/std",
|
||||
"typed-path?/std",
|
||||
"dep:filetime",
|
||||
"dep:memmap2",
|
||||
]
|
||||
mips = [
|
||||
"any-arch",
|
||||
"dep:rabbitizer",
|
||||
]
|
||||
ppc = [
|
||||
"any-arch",
|
||||
"dep:cwdemangle",
|
||||
"dep:cwextab",
|
||||
"dep:ppc750cl",
|
||||
"dep:rlwinmdec",
|
||||
]
|
||||
x86 = [
|
||||
"any-arch",
|
||||
"dep:cpp_demangle",
|
||||
"dep:iced-x86",
|
||||
"dep:msvc-demangler",
|
||||
]
|
||||
arm = [
|
||||
"any-arch",
|
||||
"dep:arm-attr",
|
||||
"dep:cpp_demangle",
|
||||
"dep:unarm",
|
||||
]
|
||||
arm64 = [
|
||||
"any-arch",
|
||||
"dep:cpp_demangle",
|
||||
"dep:yaxpeax-arch",
|
||||
"dep:yaxpeax-arm",
|
||||
]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["all"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0", default-features = false }
|
||||
filetime = { version = "0.2", optional = true }
|
||||
flagset = { version = "0.4", default-features = false, optional = true, git = "https://github.com/enarx/flagset.git", rev = "a1fe9369b3741e43fec45da1998e83b9d78966a2" }
|
||||
itertools = { version = "0.14", default-features = false, features = ["use_alloc"] }
|
||||
log = { version = "0.4", default-features = false, optional = true }
|
||||
memmap2 = { version = "0.9", optional = true }
|
||||
num-traits = { version = "0.2", default-features = false, optional = true }
|
||||
object = { version = "0.36", default-features = false, features = ["read_core", "elf", "pe"] }
|
||||
pbjson = { version = "0.7", default-features = false, optional = true }
|
||||
prost = { version = "0.13", default-features = false, features = ["prost-derive"], optional = true }
|
||||
regex = { version = "1.11", default-features = false, features = [], optional = true }
|
||||
serde = { version = "1.0", default-features = false, features = ["derive"], optional = true }
|
||||
similar = { version = "2.7", default-features = false, optional = true, git = "https://github.com/encounter/similar.git", branch = "no_std" }
|
||||
typed-path = { version = "0.10", default-features = false, optional = true }
|
||||
|
||||
# config
|
||||
globset = { version = "0.4", default-features = false, optional = true }
|
||||
semver = { version = "1.0", default-features = false, optional = true }
|
||||
serde_json = { version = "1.0", default-features = false, features = ["alloc"], optional = true }
|
||||
|
||||
# dwarf
|
||||
gimli = { version = "0.31", default-features = false, features = ["read"], optional = true }
|
||||
|
||||
# ppc
|
||||
cwdemangle = { version = "1.0", optional = true }
|
||||
cwextab = { version = "1.0", optional = true, git = "https://github.com/CelestialAmber/cwextab.git" }
|
||||
ppc750cl = { version = "0.3", optional = true }
|
||||
rlwinmdec = { version = "1.1", optional = true, git = "https://github.com/CelestialAmber/rlwinmdec.git" }
|
||||
|
||||
# mips
|
||||
rabbitizer = { git = "https://github.com/Decompollaborate/rabbitizer.git", branch = "🦀", default-features = false, features = ["all_extensions"], optional = true }
|
||||
|
||||
# x86
|
||||
cpp_demangle = { version = "0.4", default-features = false, features = ["alloc"], optional = true }
|
||||
iced-x86 = { version = "1.21", default-features = false, features = ["decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums", "no_std"], optional = true }
|
||||
msvc-demangler = { version = "0.11", optional = true }
|
||||
|
||||
# arm
|
||||
unarm = { version = "1.7", optional = true }
|
||||
arm-attr = { version = "0.2", optional = true }
|
||||
|
||||
# arm64
|
||||
yaxpeax-arch = { version = "0.3", default-features = false, optional = true }
|
||||
yaxpeax-arm = { version = "0.3", default-features = false, optional = true }
|
||||
|
||||
# build
|
||||
notify = { version = "8.0.0", optional = true }
|
||||
notify-debouncer-full = { version = "0.5.0", optional = true }
|
||||
shell-escape = { version = "0.1", optional = true }
|
||||
tempfile = { version = "3.17", optional = true }
|
||||
time = { version = "0.3", optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winapi = { version = "0.3", optional = true }
|
||||
|
||||
# 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"], optional = true }
|
||||
self_update = { version = "0.42", default-features = false, features = ["rustls"], optional = true }
|
||||
|
||||
# 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"], optional = true }
|
||||
self_update = { version = "0.42", optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
heck = { version = "0.5", optional = true }
|
||||
pbjson-build = { version = "0.7", optional = true }
|
||||
prettyplease = { version = "0.2", optional = true }
|
||||
proc-macro2 = { version = "1.0", optional = true }
|
||||
prost-build = { version = "0.13", optional = true }
|
||||
quote = { version = "1.0", optional = true }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0" }
|
||||
syn = { version = "2.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# Enable all features for tests
|
||||
objdiff-core = { path = ".", features = ["all"] }
|
||||
insta = "1.42"
|
||||
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.
|
||||
68
objdiff-core/build.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
#[cfg(feature = "any-arch")]
|
||||
mod config_gen;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
#[cfg(feature = "bindings")]
|
||||
compile_protos();
|
||||
#[cfg(feature = "any-arch")]
|
||||
config_gen::generate_diff_config();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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("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:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
264
objdiff-core/config-schema.json
Normal file
@@ -0,0 +1,264 @@
|
||||
{
|
||||
"properties": [
|
||||
{
|
||||
"id": "functionRelocDiffs",
|
||||
"type": "choice",
|
||||
"default": "name_address",
|
||||
"name": "Function relocation diffs",
|
||||
"description": "How relocation targets will be diffed in the function view.",
|
||||
"items": [
|
||||
{
|
||||
"value": "none",
|
||||
"name": "None"
|
||||
},
|
||||
{
|
||||
"value": "name_address",
|
||||
"name": "Name or address"
|
||||
},
|
||||
{
|
||||
"value": "data_value",
|
||||
"name": "Data value"
|
||||
},
|
||||
{
|
||||
"value": "all",
|
||||
"name": "Name or address, data value"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "spaceBetweenArgs",
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"name": "Space between args",
|
||||
"description": "Adds a space between arguments in the diff output."
|
||||
},
|
||||
{
|
||||
"id": "combineDataSections",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"name": "Combine data sections",
|
||||
"description": "Combines data sections with equal names."
|
||||
},
|
||||
{
|
||||
"id": "combineTextSections",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"name": "Combine text sections",
|
||||
"description": "Combines all text sections into one."
|
||||
},
|
||||
{
|
||||
"id": "arm.archVersion",
|
||||
"type": "choice",
|
||||
"default": "auto",
|
||||
"name": "Architecture version",
|
||||
"description": "ARM architecture version to use for disassembly.",
|
||||
"items": [
|
||||
{
|
||||
"value": "auto",
|
||||
"name": "Auto"
|
||||
},
|
||||
{
|
||||
"value": "v4t",
|
||||
"name": "ARMv4T (GBA)"
|
||||
},
|
||||
{
|
||||
"value": "v5te",
|
||||
"name": "ARMv5TE (DS)"
|
||||
},
|
||||
{
|
||||
"value": "v6k",
|
||||
"name": "ARMv6K (3DS)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "arm.unifiedSyntax",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"name": "Unified syntax",
|
||||
"description": "Disassemble as unified assembly language (UAL)."
|
||||
},
|
||||
{
|
||||
"id": "arm.avRegisters",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"name": "Use A/V registers",
|
||||
"description": "Display R0-R3 as A1-A4 and R4-R11 as V1-V8."
|
||||
},
|
||||
{
|
||||
"id": "arm.r9Usage",
|
||||
"type": "choice",
|
||||
"default": "generalPurpose",
|
||||
"name": "Display R9 as",
|
||||
"items": [
|
||||
{
|
||||
"value": "generalPurpose",
|
||||
"name": "R9 or V6",
|
||||
"description": "Use R9 as a general-purpose register."
|
||||
},
|
||||
{
|
||||
"value": "sb",
|
||||
"name": "SB (static base)",
|
||||
"description": "Used for position-independent data (PID)."
|
||||
},
|
||||
{
|
||||
"value": "tr",
|
||||
"name": "TR (TLS register)",
|
||||
"description": "Used for thread-local storage."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "arm.slUsage",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"name": "Display R10 as SL",
|
||||
"description": "Used for explicit stack limits."
|
||||
},
|
||||
{
|
||||
"id": "arm.fpUsage",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"name": "Display R11 as FP",
|
||||
"description": "Used for frame pointers."
|
||||
},
|
||||
{
|
||||
"id": "arm.ipUsage",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"name": "Display R12 as IP",
|
||||
"description": "Used for interworking and long branches."
|
||||
},
|
||||
{
|
||||
"id": "mips.abi",
|
||||
"type": "choice",
|
||||
"default": "auto",
|
||||
"name": "ABI",
|
||||
"description": "MIPS ABI to use for disassembly.",
|
||||
"items": [
|
||||
{
|
||||
"value": "auto",
|
||||
"name": "Auto"
|
||||
},
|
||||
{
|
||||
"value": "o32",
|
||||
"name": "O32"
|
||||
},
|
||||
{
|
||||
"value": "n32",
|
||||
"name": "N32"
|
||||
},
|
||||
{
|
||||
"value": "n64",
|
||||
"name": "N64"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "mips.instrCategory",
|
||||
"type": "choice",
|
||||
"default": "auto",
|
||||
"name": "Instruction category",
|
||||
"description": "MIPS instruction category to use for disassembly.",
|
||||
"items": [
|
||||
{
|
||||
"value": "auto",
|
||||
"name": "Auto"
|
||||
},
|
||||
{
|
||||
"value": "cpu",
|
||||
"name": "CPU"
|
||||
},
|
||||
{
|
||||
"value": "rsp",
|
||||
"name": "RSP (N64)"
|
||||
},
|
||||
{
|
||||
"value": "r3000gte",
|
||||
"name": "R3000 GTE (PS1)"
|
||||
},
|
||||
{
|
||||
"value": "r4000allegrex",
|
||||
"name": "R4000 ALLEGREX (PSP)"
|
||||
},
|
||||
{
|
||||
"value": "r5900",
|
||||
"name": "R5900 EE (PS2)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "mips.registerPrefix",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"name": "Register '$' prefix",
|
||||
"description": "Display MIPS register names with a '$' prefix."
|
||||
},
|
||||
{
|
||||
"id": "x86.formatter",
|
||||
"type": "choice",
|
||||
"default": "intel",
|
||||
"name": "Format",
|
||||
"description": "x86 disassembly syntax.",
|
||||
"items": [
|
||||
{
|
||||
"value": "intel",
|
||||
"name": "Intel"
|
||||
},
|
||||
{
|
||||
"value": "gas",
|
||||
"name": "AT&T"
|
||||
},
|
||||
{
|
||||
"value": "nasm",
|
||||
"name": "NASM"
|
||||
},
|
||||
{
|
||||
"value": "masm",
|
||||
"name": "MASM"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": "general",
|
||||
"name": "General",
|
||||
"properties": [
|
||||
"functionRelocDiffs",
|
||||
"spaceBetweenArgs",
|
||||
"combineDataSections",
|
||||
"combineTextSections"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "arm",
|
||||
"name": "ARM",
|
||||
"properties": [
|
||||
"arm.archVersion",
|
||||
"arm.unifiedSyntax",
|
||||
"arm.avRegisters",
|
||||
"arm.r9Usage",
|
||||
"arm.slUsage",
|
||||
"arm.fpUsage",
|
||||
"arm.ipUsage"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "mips",
|
||||
"name": "MIPS",
|
||||
"properties": [
|
||||
"mips.abi",
|
||||
"mips.instrCategory",
|
||||
"mips.registerPrefix"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "x86",
|
||||
"name": "x86",
|
||||
"properties": [
|
||||
"x86.formatter"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
500
objdiff-core/config_gen.rs
Normal file
@@ -0,0 +1,500 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use heck::{ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct ConfigSchema {
|
||||
pub properties: Vec<ConfigProperty>,
|
||||
pub groups: Vec<ConfigGroup>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ConfigProperty {
|
||||
#[serde(rename = "boolean")]
|
||||
Boolean(ConfigPropertyBoolean),
|
||||
#[serde(rename = "choice")]
|
||||
Choice(ConfigPropertyChoice),
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct ConfigPropertyBase {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct ConfigPropertyBoolean {
|
||||
#[serde(flatten)]
|
||||
pub base: ConfigPropertyBase,
|
||||
pub default: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct ConfigPropertyChoice {
|
||||
#[serde(flatten)]
|
||||
pub base: ConfigPropertyBase,
|
||||
pub default: String,
|
||||
pub items: Vec<ConfigPropertyChoiceItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct ConfigPropertyChoiceItem {
|
||||
pub value: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct ConfigGroup {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub properties: Vec<String>,
|
||||
}
|
||||
|
||||
fn build_doc(name: &str, description: Option<&str>) -> TokenStream {
|
||||
let mut doc = format!(" {}", name);
|
||||
let mut out = quote! { #[doc = #doc] };
|
||||
if let Some(description) = description {
|
||||
doc = format!(" {}", description);
|
||||
out.extend(quote! { #[doc = ""] });
|
||||
out.extend(quote! { #[doc = #doc] });
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn generate_diff_config() {
|
||||
let schema_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("config-schema.json");
|
||||
println!("cargo:rerun-if-changed={}", schema_path.display());
|
||||
let schema_file = File::open(schema_path).expect("Failed to open config schema file");
|
||||
let schema: ConfigSchema =
|
||||
serde_json::from_reader(schema_file).expect("Failed to parse config schema");
|
||||
|
||||
let mut enums = TokenStream::new();
|
||||
for property in &schema.properties {
|
||||
let ConfigProperty::Choice(choice) = property else {
|
||||
continue;
|
||||
};
|
||||
let enum_ident = format_ident!("{}", choice.base.id.to_upper_camel_case());
|
||||
let mut variants = TokenStream::new();
|
||||
let mut full_variants = TokenStream::new();
|
||||
let mut variant_info = TokenStream::new();
|
||||
let mut variant_to_str = TokenStream::new();
|
||||
let mut variant_to_name = TokenStream::new();
|
||||
let mut variant_to_description = TokenStream::new();
|
||||
let mut variant_from_str = TokenStream::new();
|
||||
for item in &choice.items {
|
||||
let variant_name = item.value.to_upper_camel_case();
|
||||
let variant_ident = format_ident!("{}", variant_name);
|
||||
let is_default = item.value == choice.default;
|
||||
variants.extend(build_doc(&item.name, item.description.as_deref()));
|
||||
if is_default {
|
||||
variants.extend(quote! { #[default] });
|
||||
}
|
||||
let value = &item.value;
|
||||
variants.extend(quote! {
|
||||
#[cfg_attr(feature = "serde", serde(rename = #value, alias = #variant_name))]
|
||||
#variant_ident,
|
||||
});
|
||||
full_variants.extend(quote! { #enum_ident::#variant_ident, });
|
||||
variant_to_str.extend(quote! { #enum_ident::#variant_ident => #value, });
|
||||
let name = &item.name;
|
||||
variant_to_name.extend(quote! { #enum_ident::#variant_ident => #name, });
|
||||
if let Some(description) = &item.description {
|
||||
variant_to_description.extend(quote! {
|
||||
#enum_ident::#variant_ident => Some(#description),
|
||||
});
|
||||
} else {
|
||||
variant_to_description.extend(quote! {
|
||||
#enum_ident::#variant_ident => None,
|
||||
});
|
||||
}
|
||||
let description = if let Some(description) = &item.description {
|
||||
quote! { Some(#description) }
|
||||
} else {
|
||||
quote! { None }
|
||||
};
|
||||
variant_info.extend(quote! {
|
||||
ConfigEnumVariantInfo {
|
||||
value: #value,
|
||||
name: #name,
|
||||
description: #description,
|
||||
is_default: #is_default,
|
||||
},
|
||||
});
|
||||
variant_from_str.extend(quote! {
|
||||
if s.eq_ignore_ascii_case(#value) { return Ok(#enum_ident::#variant_ident); }
|
||||
});
|
||||
}
|
||||
enums.extend(quote! {
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum #enum_ident {
|
||||
#variants
|
||||
}
|
||||
impl ConfigEnum for #enum_ident {
|
||||
#[inline]
|
||||
fn variants() -> &'static [Self] {
|
||||
static VARIANTS: &[#enum_ident] = &[#full_variants];
|
||||
VARIANTS
|
||||
}
|
||||
#[inline]
|
||||
fn variant_info() -> &'static [ConfigEnumVariantInfo] {
|
||||
static VARIANT_INFO: &[ConfigEnumVariantInfo] = &[
|
||||
#variant_info
|
||||
];
|
||||
VARIANT_INFO
|
||||
}
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
#variant_to_str
|
||||
}
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
#variant_to_name
|
||||
}
|
||||
}
|
||||
fn description(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
#variant_to_description
|
||||
}
|
||||
}
|
||||
}
|
||||
impl core::str::FromStr for #enum_ident {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
#variant_from_str
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut groups = TokenStream::new();
|
||||
let mut group_idents = Vec::new();
|
||||
for group in &schema.groups {
|
||||
let ident = format_ident!("CONFIG_GROUP_{}", group.id.to_shouty_snake_case());
|
||||
let id = &group.id;
|
||||
let name = &group.name;
|
||||
let description = if let Some(description) = &group.description {
|
||||
quote! { Some(#description) }
|
||||
} else {
|
||||
quote! { None }
|
||||
};
|
||||
let properties =
|
||||
group.properties.iter().map(|p| format_ident!("{}", p.to_upper_camel_case()));
|
||||
groups.extend(quote! {
|
||||
ConfigPropertyGroup {
|
||||
id: #id,
|
||||
name: #name,
|
||||
description: #description,
|
||||
properties: &[#(ConfigPropertyId::#properties,)*],
|
||||
},
|
||||
});
|
||||
group_idents.push(ident);
|
||||
}
|
||||
|
||||
let mut property_idents = Vec::new();
|
||||
let mut property_variants = TokenStream::new();
|
||||
let mut variant_info = TokenStream::new();
|
||||
let mut config_property_id_to_str = TokenStream::new();
|
||||
let mut config_property_id_to_name = TokenStream::new();
|
||||
let mut config_property_id_to_description = TokenStream::new();
|
||||
let mut config_property_id_to_kind = TokenStream::new();
|
||||
let mut property_fields = TokenStream::new();
|
||||
let mut default_fields = TokenStream::new();
|
||||
let mut get_property_value_variants = TokenStream::new();
|
||||
let mut set_property_value_variants = TokenStream::new();
|
||||
let mut set_property_value_str_variants = TokenStream::new();
|
||||
let mut config_property_id_from_str = TokenStream::new();
|
||||
for property in &schema.properties {
|
||||
let base = match property {
|
||||
ConfigProperty::Boolean(b) => &b.base,
|
||||
ConfigProperty::Choice(c) => &c.base,
|
||||
};
|
||||
let id = &base.id;
|
||||
let enum_ident = format_ident!("{}", id.to_upper_camel_case());
|
||||
property_idents.push(enum_ident.clone());
|
||||
config_property_id_to_str.extend(quote! { Self::#enum_ident => #id, });
|
||||
let name = &base.name;
|
||||
config_property_id_to_name.extend(quote! { Self::#enum_ident => #name, });
|
||||
if let Some(description) = &base.description {
|
||||
config_property_id_to_description.extend(quote! {
|
||||
Self::#enum_ident => Some(#description),
|
||||
});
|
||||
} else {
|
||||
config_property_id_to_description.extend(quote! {
|
||||
Self::#enum_ident => None,
|
||||
});
|
||||
}
|
||||
let doc = build_doc(name, base.description.as_deref());
|
||||
property_variants.extend(quote! { #doc #enum_ident, });
|
||||
property_fields.extend(doc);
|
||||
let field_ident = format_ident!("{}", id.to_snake_case());
|
||||
match property {
|
||||
ConfigProperty::Boolean(b) => {
|
||||
let default = b.default;
|
||||
if default {
|
||||
property_fields.extend(quote! {
|
||||
#[cfg_attr(feature = "serde", serde(default = "default_true"))]
|
||||
});
|
||||
}
|
||||
property_fields.extend(quote! {
|
||||
pub #field_ident: bool,
|
||||
});
|
||||
default_fields.extend(quote! {
|
||||
#field_ident: #default,
|
||||
});
|
||||
}
|
||||
ConfigProperty::Choice(_) => {
|
||||
property_fields.extend(quote! {
|
||||
pub #field_ident: #enum_ident,
|
||||
});
|
||||
default_fields.extend(quote! {
|
||||
#field_ident: #enum_ident::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
let property_value = match property {
|
||||
ConfigProperty::Boolean(_) => {
|
||||
quote! { ConfigPropertyValue::Boolean(self.#field_ident) }
|
||||
}
|
||||
ConfigProperty::Choice(_) => {
|
||||
quote! { ConfigPropertyValue::Choice(self.#field_ident.as_str()) }
|
||||
}
|
||||
};
|
||||
get_property_value_variants.extend(quote! {
|
||||
ConfigPropertyId::#enum_ident => #property_value,
|
||||
});
|
||||
match property {
|
||||
ConfigProperty::Boolean(_) => {
|
||||
set_property_value_variants.extend(quote! {
|
||||
ConfigPropertyId::#enum_ident => {
|
||||
if let ConfigPropertyValue::Boolean(value) = value {
|
||||
self.#field_ident = value;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
},
|
||||
});
|
||||
set_property_value_str_variants.extend(quote! {
|
||||
ConfigPropertyId::#enum_ident => {
|
||||
if let Ok(value) = value.parse() {
|
||||
self.#field_ident = value;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
ConfigProperty::Choice(_) => {
|
||||
set_property_value_variants.extend(quote! {
|
||||
ConfigPropertyId::#enum_ident => {
|
||||
if let ConfigPropertyValue::Choice(value) = value {
|
||||
if let Ok(value) = value.parse() {
|
||||
self.#field_ident = value;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
},
|
||||
});
|
||||
set_property_value_str_variants.extend(quote! {
|
||||
ConfigPropertyId::#enum_ident => {
|
||||
if let Ok(value) = value.parse() {
|
||||
self.#field_ident = value;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
let description = if let Some(description) = &base.description {
|
||||
quote! { Some(#description) }
|
||||
} else {
|
||||
quote! { None }
|
||||
};
|
||||
variant_info.extend(quote! {
|
||||
ConfigEnumVariantInfo {
|
||||
value: #id,
|
||||
name: #name,
|
||||
description: #description,
|
||||
is_default: false,
|
||||
},
|
||||
});
|
||||
match property {
|
||||
ConfigProperty::Boolean(_) => {
|
||||
config_property_id_to_kind.extend(quote! {
|
||||
Self::#enum_ident => ConfigPropertyKind::Boolean,
|
||||
});
|
||||
}
|
||||
ConfigProperty::Choice(_) => {
|
||||
config_property_id_to_kind.extend(quote! {
|
||||
Self::#enum_ident => ConfigPropertyKind::Choice(#enum_ident::variant_info()),
|
||||
});
|
||||
}
|
||||
}
|
||||
let snake_id = id.to_snake_case();
|
||||
config_property_id_from_str.extend(quote! {
|
||||
if s.eq_ignore_ascii_case(#id) || s.eq_ignore_ascii_case(#snake_id) {
|
||||
return Ok(Self::#enum_ident);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let tokens = quote! {
|
||||
pub trait ConfigEnum: Sized {
|
||||
fn variants() -> &'static [Self];
|
||||
fn variant_info() -> &'static [ConfigEnumVariantInfo];
|
||||
fn as_str(&self) -> &'static str;
|
||||
fn name(&self) -> &'static str;
|
||||
fn description(&self) -> Option<&'static str>;
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ConfigEnumVariantInfo {
|
||||
pub value: &'static str,
|
||||
pub name: &'static str,
|
||||
pub description: Option<&'static str>,
|
||||
pub is_default: bool,
|
||||
}
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum ConfigPropertyId {
|
||||
#property_variants
|
||||
}
|
||||
impl ConfigEnum for ConfigPropertyId {
|
||||
#[inline]
|
||||
fn variants() -> &'static [Self] {
|
||||
static VARIANTS: &[ConfigPropertyId] = &[#(ConfigPropertyId::#property_idents,)*];
|
||||
VARIANTS
|
||||
}
|
||||
#[inline]
|
||||
fn variant_info() -> &'static [ConfigEnumVariantInfo] {
|
||||
static VARIANT_INFO: &[ConfigEnumVariantInfo] = &[
|
||||
#variant_info
|
||||
];
|
||||
VARIANT_INFO
|
||||
}
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
#config_property_id_to_str
|
||||
}
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
#config_property_id_to_name
|
||||
}
|
||||
}
|
||||
fn description(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
#config_property_id_to_description
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ConfigPropertyId {
|
||||
pub fn kind(&self) -> ConfigPropertyKind {
|
||||
match self {
|
||||
#config_property_id_to_kind
|
||||
}
|
||||
}
|
||||
}
|
||||
impl core::str::FromStr for ConfigPropertyId {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
#config_property_id_from_str
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ConfigPropertyGroup {
|
||||
pub id: &'static str,
|
||||
pub name: &'static str,
|
||||
pub description: Option<&'static str>,
|
||||
pub properties: &'static [ConfigPropertyId],
|
||||
}
|
||||
pub static CONFIG_GROUPS: &[ConfigPropertyGroup] = &[#groups];
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum ConfigPropertyValue {
|
||||
Boolean(bool),
|
||||
Choice(&'static str),
|
||||
}
|
||||
impl ConfigPropertyValue {
|
||||
#[cfg(feature = "serde")]
|
||||
pub fn to_json(&self) -> serde_json::Value {
|
||||
match self {
|
||||
ConfigPropertyValue::Boolean(value) => serde_json::Value::Bool(*value),
|
||||
ConfigPropertyValue::Choice(value) => serde_json::Value::String(value.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl core::fmt::Display for ConfigPropertyValue {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
ConfigPropertyValue::Boolean(value) => write!(f, "{}", value),
|
||||
ConfigPropertyValue::Choice(value) => write!(f, "{}", value),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ConfigPropertyKind {
|
||||
Boolean,
|
||||
Choice(&'static [ConfigEnumVariantInfo]),
|
||||
}
|
||||
#enums
|
||||
#[cfg(feature = "serde")]
|
||||
#[inline(always)]
|
||||
fn default_true() -> bool { true }
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize), serde(default))]
|
||||
pub struct DiffObjConfig {
|
||||
#property_fields
|
||||
}
|
||||
impl Default for DiffObjConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
#default_fields
|
||||
}
|
||||
}
|
||||
}
|
||||
impl DiffObjConfig {
|
||||
pub fn get_property_value(&self, id: ConfigPropertyId) -> ConfigPropertyValue {
|
||||
match id {
|
||||
#get_property_value_variants
|
||||
}
|
||||
}
|
||||
#[allow(clippy::result_unit_err)]
|
||||
pub fn set_property_value(&mut self, id: ConfigPropertyId, value: ConfigPropertyValue) -> Result<(), ()> {
|
||||
match id {
|
||||
#set_property_value_variants
|
||||
}
|
||||
}
|
||||
#[allow(clippy::result_unit_err)]
|
||||
pub fn set_property_value_str(&mut self, id: ConfigPropertyId, value: &str) -> Result<(), ()> {
|
||||
match id {
|
||||
#set_property_value_str_variants
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let file = syn::parse2(tokens).unwrap();
|
||||
let formatted = prettyplease::unparse(&file);
|
||||
std::fs::write(
|
||||
PathBuf::from(std::env::var_os("OUT_DIR").unwrap()).join("config.gen.rs"),
|
||||
formatted,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
165
objdiff-core/protos/diff.proto
Normal file
@@ -0,0 +1,165 @@
|
||||
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 = 4;
|
||||
SYMBOL_COMMON = 8;
|
||||
SYMBOL_HIDDEN = 16;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
uint32 symbol_index = 1;
|
||||
int64 addend = 2;
|
||||
}
|
||||
|
||||
message InstructionDiffRow {
|
||||
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 SymbolDiff {
|
||||
Symbol symbol = 1;
|
||||
repeated InstructionDiffRow instruction_rows = 2;
|
||||
optional float match_percent = 3;
|
||||
// The symbol index in the _other_ object that this symbol was diffed against
|
||||
optional uint32 target_symbol = 5;
|
||||
}
|
||||
|
||||
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;
|
||||
reserved 5;
|
||||
repeated DataDiff data = 6;
|
||||
optional float match_percent = 7;
|
||||
}
|
||||
|
||||
enum SectionKind {
|
||||
SECTION_UNKNOWN = 0;
|
||||
SECTION_TEXT = 1;
|
||||
SECTION_DATA = 2;
|
||||
SECTION_BSS = 3;
|
||||
}
|
||||
|
||||
message ObjectDiff {
|
||||
repeated SectionDiff sections = 1;
|
||||
repeated SymbolDiff symbols = 2;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
564
objdiff-core/src/arch/arm.rs
Normal file
@@ -0,0 +1,564 @@
|
||||
use alloc::{
|
||||
collections::BTreeMap,
|
||||
format,
|
||||
string::{String, ToString},
|
||||
vec::Vec,
|
||||
};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use arm_attr::{enums::CpuArch, tag::Tag, BuildAttrs};
|
||||
use object::{elf, Endian as _, Object as _, ObjectSection as _, ObjectSymbol as _};
|
||||
use unarm::{args, arm, thumb};
|
||||
|
||||
use crate::{
|
||||
arch::Arch,
|
||||
diff::{display::InstructionPart, ArmArchVersion, ArmR9Usage, DiffObjConfig},
|
||||
obj::{
|
||||
InstructionRef, RelocationFlags, ResolvedInstructionRef, ResolvedRelocation,
|
||||
ScannedInstruction, SymbolFlag, SymbolFlagSet, SymbolKind,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ArchArm {
|
||||
/// Maps section index, to list of disasm modes (arm, thumb or data) sorted by address
|
||||
disasm_modes: BTreeMap<usize, Vec<DisasmMode>>,
|
||||
detected_version: Option<unarm::ArmVersion>,
|
||||
endianness: object::Endianness,
|
||||
}
|
||||
|
||||
impl ArchArm {
|
||||
pub fn new(file: &object::File) -> Result<Self> {
|
||||
let endianness = file.endianness();
|
||||
match file {
|
||||
object::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: &object::File) -> Result<Option<unarm::ArmVersion>> {
|
||||
// Check ARM attributes
|
||||
if let Some(arm_attrs) = file.sections().find(|s| {
|
||||
s.kind() == object::SectionKind::Elf(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(unarm::ArmVersion::V4T)),
|
||||
Some(CpuArch::V5TE) => return Ok(Some(unarm::ArmVersion::V5Te)),
|
||||
Some(CpuArch::V6K) => return Ok(Some(unarm::ArmVersion::V6K)),
|
||||
Some(arch) => bail!("ARM arch {} not supported", arch),
|
||||
None => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn elf_get_mapping_symbols(file: &object::File) -> BTreeMap<usize, Vec<DisasmMode>> {
|
||||
file.sections()
|
||||
.filter(|s| s.kind() == object::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().0, mapping_symbols)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn endian(&self) -> unarm::Endian {
|
||||
match self.endianness {
|
||||
object::Endianness::Little => unarm::Endian::Little,
|
||||
object::Endianness::Big => unarm::Endian::Big,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_flags(&self, diff_config: &DiffObjConfig) -> unarm::ParseFlags {
|
||||
unarm::ParseFlags {
|
||||
ual: diff_config.arm_unified_syntax,
|
||||
version: match diff_config.arm_arch_version {
|
||||
ArmArchVersion::Auto => self.detected_version.unwrap_or(unarm::ArmVersion::V5Te),
|
||||
ArmArchVersion::V4t => unarm::ArmVersion::V4T,
|
||||
ArmArchVersion::V5te => unarm::ArmVersion::V5Te,
|
||||
ArmArchVersion::V6k => unarm::ArmVersion::V6K,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn display_options(&self, diff_config: &DiffObjConfig) -> unarm::DisplayOptions {
|
||||
unarm::DisplayOptions {
|
||||
reg_names: unarm::RegNames {
|
||||
av_registers: diff_config.arm_av_registers,
|
||||
r9_use: match diff_config.arm_r9_usage {
|
||||
ArmR9Usage::GeneralPurpose => unarm::R9Use::GeneralPurpose,
|
||||
ArmR9Usage::Sb => unarm::R9Use::Pid,
|
||||
ArmR9Usage::Tr => unarm::R9Use::Tls,
|
||||
},
|
||||
explicit_stack_limit: diff_config.arm_sl_usage,
|
||||
frame_pointer: diff_config.arm_fp_usage,
|
||||
ip: diff_config.arm_ip_usage,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_ins_ref(
|
||||
&self,
|
||||
ins_ref: InstructionRef,
|
||||
code: &[u8],
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> Result<(unarm::Ins, unarm::ParsedIns)> {
|
||||
let code = match (self.endianness, ins_ref.size) {
|
||||
(object::Endianness::Little, 2) => u16::from_le_bytes([code[0], code[1]]) as u32,
|
||||
(object::Endianness::Little, 4) => {
|
||||
u32::from_le_bytes([code[0], code[1], code[2], code[3]])
|
||||
}
|
||||
(object::Endianness::Big, 2) => u16::from_be_bytes([code[0], code[1]]) as u32,
|
||||
(object::Endianness::Big, 4) => {
|
||||
u32::from_be_bytes([code[0], code[1], code[2], code[3]])
|
||||
}
|
||||
_ => bail!("Invalid instruction size {}", ins_ref.size),
|
||||
};
|
||||
let (ins, parsed_ins) = if ins_ref.opcode == u16::MAX {
|
||||
let mut args = args::Arguments::default();
|
||||
args[0] = args::Argument::UImm(code);
|
||||
let mnemonic = if ins_ref.size == 4 { ".word" } else { ".hword" };
|
||||
(unarm::Ins::Data, unarm::ParsedIns { mnemonic, args })
|
||||
} else if ins_ref.opcode & (1 << 15) != 0 {
|
||||
let ins = arm::Ins { code, op: arm::Opcode::from(ins_ref.opcode as u8) };
|
||||
let parsed = ins.parse(&self.parse_flags(diff_config));
|
||||
(unarm::Ins::Arm(ins), parsed)
|
||||
} else {
|
||||
let ins = thumb::Ins { code, op: thumb::Opcode::from(ins_ref.opcode as u8) };
|
||||
let parsed = ins.parse(&self.parse_flags(diff_config));
|
||||
if ins.is_half_bl() {
|
||||
todo!("Combine thumb BL instructions");
|
||||
} else {
|
||||
(unarm::Ins::Thumb(ins), parsed)
|
||||
}
|
||||
};
|
||||
Ok((ins, parsed_ins))
|
||||
}
|
||||
}
|
||||
|
||||
impl Arch for ArchArm {
|
||||
fn scan_instructions(
|
||||
&self,
|
||||
address: u64,
|
||||
code: &[u8],
|
||||
section_index: usize,
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> Result<Vec<ScannedInstruction>> {
|
||||
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: unarm::ParseMode::Arm }];
|
||||
let mapping_symbols = self
|
||||
.disasm_modes
|
||||
.get(§ion_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::<ScannedInstruction>::with_capacity(ins_count);
|
||||
|
||||
let endian = self.endian();
|
||||
let parse_flags = self.parse_flags(diff_config);
|
||||
let mut parser = unarm::Parser::new(first_mapping, start_addr, endian, parse_flags, code);
|
||||
|
||||
while let Some((address, ins, _parsed_ins)) = parser.next() {
|
||||
let size = parser.mode.instruction_size(address);
|
||||
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 (opcode, branch_dest) = match ins {
|
||||
unarm::Ins::Arm(x) => {
|
||||
let opcode = x.op as u16 | (1 << 15);
|
||||
let branch_dest = match x.op {
|
||||
arm::Opcode::B | arm::Opcode::Bl => {
|
||||
address.checked_add_signed(x.field_branch_offset())
|
||||
}
|
||||
arm::Opcode::BlxI => address.checked_add_signed(x.field_blx_offset()),
|
||||
_ => None,
|
||||
};
|
||||
(opcode, branch_dest)
|
||||
}
|
||||
unarm::Ins::Thumb(x) => {
|
||||
let opcode = x.op as u16;
|
||||
let branch_dest = match x.op {
|
||||
thumb::Opcode::B | thumb::Opcode::Bl => {
|
||||
address.checked_add_signed(x.field_branch_offset_8())
|
||||
}
|
||||
thumb::Opcode::BLong => {
|
||||
address.checked_add_signed(x.field_branch_offset_11())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
(opcode, branch_dest)
|
||||
}
|
||||
unarm::Ins::Data => (u16::MAX, None),
|
||||
};
|
||||
ops.push(ScannedInstruction {
|
||||
ins_ref: InstructionRef { address: address as u64, size: size as u8, opcode },
|
||||
branch_dest: branch_dest.map(|x| x as u64),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ops)
|
||||
}
|
||||
|
||||
fn display_instruction(
|
||||
&self,
|
||||
resolved: ResolvedInstructionRef,
|
||||
diff_config: &DiffObjConfig,
|
||||
cb: &mut dyn FnMut(InstructionPart) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
let (ins, parsed_ins) = self.parse_ins_ref(resolved.ins_ref, resolved.code, diff_config)?;
|
||||
cb(InstructionPart::opcode(parsed_ins.mnemonic, resolved.ins_ref.opcode))?;
|
||||
if ins == unarm::Ins::Data && resolved.relocation.is_some() {
|
||||
cb(InstructionPart::reloc())?;
|
||||
} else {
|
||||
push_args(
|
||||
&parsed_ins,
|
||||
resolved.relocation,
|
||||
resolved.ins_ref.address as u32,
|
||||
self.display_options(diff_config),
|
||||
cb,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn implcit_addend(
|
||||
&self,
|
||||
_file: &object::File<'_>,
|
||||
section: &object::Section,
|
||||
address: u64,
|
||||
_relocation: &object::Relocation,
|
||||
flags: RelocationFlags,
|
||||
) -> Result<i64> {
|
||||
let section_data = section.data()?;
|
||||
let address = address as usize;
|
||||
Ok(match flags {
|
||||
// ARM calls
|
||||
RelocationFlags::Elf(elf::R_ARM_PC24)
|
||||
| RelocationFlags::Elf(elf::R_ARM_XPC25)
|
||||
| RelocationFlags::Elf(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(elf::R_ARM_THM_PC22)
|
||||
| RelocationFlags::Elf(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(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 reloc_name(&self, flags: RelocationFlags) -> Option<&'static str> {
|
||||
match flags {
|
||||
RelocationFlags::Elf(r_type) => match r_type {
|
||||
elf::R_ARM_NONE => Some("R_ARM_NONE"),
|
||||
elf::R_ARM_ABS32 => Some("R_ARM_ABS32"),
|
||||
elf::R_ARM_REL32 => Some("R_ARM_REL32"),
|
||||
elf::R_ARM_ABS16 => Some("R_ARM_ABS16"),
|
||||
elf::R_ARM_ABS8 => Some("R_ARM_ABS8"),
|
||||
elf::R_ARM_THM_PC22 => Some("R_ARM_THM_PC22"),
|
||||
elf::R_ARM_THM_XPC22 => Some("R_ARM_THM_XPC22"),
|
||||
elf::R_ARM_PC24 => Some("R_ARM_PC24"),
|
||||
elf::R_ARM_XPC25 => Some("R_ARM_XPC25"),
|
||||
elf::R_ARM_CALL => Some("R_ARM_CALL"),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn data_reloc_size(&self, flags: RelocationFlags) -> usize {
|
||||
match flags {
|
||||
RelocationFlags::Elf(r_type) => match r_type {
|
||||
elf::R_ARM_NONE => 0,
|
||||
elf::R_ARM_ABS32 => 4,
|
||||
elf::R_ARM_REL32 => 4,
|
||||
elf::R_ARM_ABS16 => 2,
|
||||
elf::R_ARM_ABS8 => 1,
|
||||
elf::R_ARM_THM_PC22 => 4,
|
||||
elf::R_ARM_THM_XPC22 => 4,
|
||||
elf::R_ARM_PC24 => 4,
|
||||
elf::R_ARM_XPC25 => 4,
|
||||
elf::R_ARM_CALL => 4,
|
||||
_ => 1,
|
||||
},
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn symbol_address(&self, address: u64, kind: SymbolKind) -> u64 {
|
||||
if kind == SymbolKind::Function {
|
||||
address & !1
|
||||
} else {
|
||||
address
|
||||
}
|
||||
}
|
||||
|
||||
fn extra_symbol_flags(&self, symbol: &object::Symbol) -> SymbolFlagSet {
|
||||
let mut flags = SymbolFlagSet::default();
|
||||
if DisasmMode::from_symbol(symbol).is_some() {
|
||||
flags |= SymbolFlag::Hidden;
|
||||
}
|
||||
flags
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct DisasmMode {
|
||||
address: u32,
|
||||
mapping: unarm::ParseMode,
|
||||
}
|
||||
|
||||
impl DisasmMode {
|
||||
fn from_symbol<'a>(sym: &object::Symbol<'a, '_, &'a [u8]>) -> Option<Self> {
|
||||
sym.name()
|
||||
.ok()
|
||||
.and_then(unarm::ParseMode::from_mapping_symbol)
|
||||
.map(|mapping| DisasmMode { address: sym.address() as u32, mapping })
|
||||
}
|
||||
}
|
||||
|
||||
fn push_args(
|
||||
parsed_ins: &unarm::ParsedIns,
|
||||
relocation: Option<ResolvedRelocation>,
|
||||
cur_addr: u32,
|
||||
display_options: unarm::DisplayOptions,
|
||||
mut arg_cb: impl FnMut(InstructionPart) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
let reloc_arg = find_reloc_arg(parsed_ins, relocation);
|
||||
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 {
|
||||
args::Argument::OffsetImm(args::OffsetImm { post_indexed: true, value: _ })
|
||||
| args::Argument::OffsetReg(args::OffsetReg {
|
||||
add: _,
|
||||
post_indexed: true,
|
||||
reg: _,
|
||||
})
|
||||
| args::Argument::CoOption(_) => {
|
||||
deref = false;
|
||||
arg_cb(InstructionPart::basic("]"))?;
|
||||
if writeback {
|
||||
writeback = false;
|
||||
arg_cb(InstructionPart::opaque("!"))?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
arg_cb(InstructionPart::separator())?;
|
||||
}
|
||||
|
||||
if reloc_arg == Some(i) {
|
||||
arg_cb(InstructionPart::reloc())?;
|
||||
} else {
|
||||
match arg {
|
||||
args::Argument::None => {}
|
||||
args::Argument::Reg(reg) => {
|
||||
if reg.deref {
|
||||
deref = true;
|
||||
arg_cb(InstructionPart::basic("["))?;
|
||||
}
|
||||
arg_cb(InstructionPart::opaque(
|
||||
reg.reg.display(display_options.reg_names).to_string(),
|
||||
))?;
|
||||
if reg.writeback {
|
||||
if reg.deref {
|
||||
writeback = true;
|
||||
} else {
|
||||
arg_cb(InstructionPart::opaque("!"))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
args::Argument::RegList(reg_list) => {
|
||||
arg_cb(InstructionPart::basic("{"))?;
|
||||
let mut first = true;
|
||||
for i in 0..16 {
|
||||
if (reg_list.regs & (1 << i)) != 0 {
|
||||
if !first {
|
||||
arg_cb(InstructionPart::separator())?;
|
||||
}
|
||||
arg_cb(InstructionPart::opaque(
|
||||
args::Register::parse(i)
|
||||
.display(display_options.reg_names)
|
||||
.to_string(),
|
||||
))?;
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
arg_cb(InstructionPart::basic("}"))?;
|
||||
if reg_list.user_mode {
|
||||
arg_cb(InstructionPart::opaque("^"))?;
|
||||
}
|
||||
}
|
||||
args::Argument::UImm(value)
|
||||
| args::Argument::CoOpcode(value)
|
||||
| args::Argument::SatImm(value) => {
|
||||
arg_cb(InstructionPart::basic("#"))?;
|
||||
arg_cb(InstructionPart::unsigned(*value))?;
|
||||
}
|
||||
args::Argument::SImm(value)
|
||||
| args::Argument::OffsetImm(args::OffsetImm { post_indexed: _, value }) => {
|
||||
arg_cb(InstructionPart::basic("#"))?;
|
||||
arg_cb(InstructionPart::signed(*value))?;
|
||||
}
|
||||
args::Argument::BranchDest(value) => {
|
||||
arg_cb(InstructionPart::branch_dest(cur_addr.wrapping_add_signed(*value)))?;
|
||||
}
|
||||
args::Argument::CoOption(value) => {
|
||||
arg_cb(InstructionPart::basic("{"))?;
|
||||
arg_cb(InstructionPart::unsigned(*value))?;
|
||||
arg_cb(InstructionPart::basic("}"))?;
|
||||
}
|
||||
args::Argument::CoprocNum(value) => {
|
||||
arg_cb(InstructionPart::opaque(format!("p{}", value)))?;
|
||||
}
|
||||
args::Argument::ShiftImm(shift) => {
|
||||
arg_cb(InstructionPart::opaque(shift.op.to_string()))?;
|
||||
arg_cb(InstructionPart::basic(" #"))?;
|
||||
arg_cb(InstructionPart::unsigned(shift.imm))?;
|
||||
}
|
||||
args::Argument::ShiftReg(shift) => {
|
||||
arg_cb(InstructionPart::opaque(shift.op.to_string()))?;
|
||||
arg_cb(InstructionPart::basic(" "))?;
|
||||
arg_cb(InstructionPart::opaque(
|
||||
shift.reg.display(display_options.reg_names).to_string(),
|
||||
))?;
|
||||
}
|
||||
args::Argument::OffsetReg(offset) => {
|
||||
if !offset.add {
|
||||
arg_cb(InstructionPart::basic("-"))?;
|
||||
}
|
||||
arg_cb(InstructionPart::opaque(
|
||||
offset.reg.display(display_options.reg_names).to_string(),
|
||||
))?;
|
||||
}
|
||||
args::Argument::CpsrMode(mode) => {
|
||||
arg_cb(InstructionPart::basic("#"))?;
|
||||
arg_cb(InstructionPart::unsigned(mode.mode))?;
|
||||
if mode.writeback {
|
||||
arg_cb(InstructionPart::opaque("!"))?;
|
||||
}
|
||||
}
|
||||
args::Argument::CoReg(_)
|
||||
| args::Argument::StatusReg(_)
|
||||
| args::Argument::StatusMask(_)
|
||||
| args::Argument::Shift(_)
|
||||
| args::Argument::CpsrFlags(_)
|
||||
| args::Argument::Endian(_) => {
|
||||
arg_cb(InstructionPart::opaque(
|
||||
arg.display(display_options, None).to_string(),
|
||||
))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if deref {
|
||||
arg_cb(InstructionPart::basic("]"))?;
|
||||
if writeback {
|
||||
arg_cb(InstructionPart::opaque("!"))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find_reloc_arg(
|
||||
parsed_ins: &unarm::ParsedIns,
|
||||
relocation: Option<ResolvedRelocation>,
|
||||
) -> Option<usize> {
|
||||
if let Some(resolved) = relocation {
|
||||
match resolved.relocation.flags {
|
||||
// Calls
|
||||
RelocationFlags::Elf(elf::R_ARM_THM_XPC22)
|
||||
| RelocationFlags::Elf(elf::R_ARM_THM_PC22)
|
||||
| RelocationFlags::Elf(elf::R_ARM_PC24)
|
||||
| RelocationFlags::Elf(elf::R_ARM_XPC25)
|
||||
| RelocationFlags::Elf(elf::R_ARM_CALL) => {
|
||||
parsed_ins.args.iter().rposition(|a| matches!(a, args::Argument::BranchDest(_)))
|
||||
}
|
||||
// Data
|
||||
RelocationFlags::Elf(elf::R_ARM_ABS32) => {
|
||||
parsed_ins.args.iter().rposition(|a| matches!(a, args::Argument::UImm(_)))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
2862
objdiff-core/src/arch/arm64.rs
Normal file
442
objdiff-core/src/arch/mips.rs
Normal file
@@ -0,0 +1,442 @@
|
||||
use alloc::{collections::BTreeMap, string::ToString, vec::Vec};
|
||||
use core::ops::Range;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use object::{elf, Endian as _, Object as _, ObjectSection as _, ObjectSymbol as _};
|
||||
use rabbitizer::{
|
||||
abi::Abi,
|
||||
operands::{ValuedOperand, IU16},
|
||||
registers_meta::Register,
|
||||
IsaExtension, IsaVersion, Vram,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
arch::Arch,
|
||||
diff::{display::InstructionPart, DiffObjConfig, MipsAbi, MipsInstrCategory},
|
||||
obj::{
|
||||
InstructionArg, InstructionArgValue, InstructionRef, Relocation, RelocationFlags,
|
||||
ResolvedInstructionRef, ResolvedRelocation, ScannedInstruction,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ArchMips {
|
||||
pub endianness: object::Endianness,
|
||||
pub abi: Abi,
|
||||
pub isa_extension: Option<IsaExtension>,
|
||||
pub ri_gp_value: i32,
|
||||
pub paired_relocations: Vec<BTreeMap<u64, i64>>,
|
||||
}
|
||||
|
||||
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 ArchMips {
|
||||
pub fn new(object: &object::File) -> Result<Self> {
|
||||
let mut abi = Abi::O32;
|
||||
let mut isa_extension = None;
|
||||
match object.flags() {
|
||||
object::FileFlags::None => {}
|
||||
object::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::O32
|
||||
}
|
||||
}
|
||||
};
|
||||
isa_extension = match e_flags & EF_MIPS_MACH {
|
||||
EF_MIPS_MACH_ALLEGREX => Some(IsaExtension::R4000ALLEGREX),
|
||||
EF_MIPS_MACH_5900 => Some(IsaExtension::R5900EE),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
_ => 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 endianness = object.endianness();
|
||||
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| endianness.read_i32_bytes(bytes))
|
||||
.unwrap_or(0);
|
||||
|
||||
// Parse all relocations to pair R_MIPS_HI16 and R_MIPS_LO16. Since the instructions only
|
||||
// have 16-bit immediate fields, the 32-bit addend is split across the two relocations.
|
||||
// R_MIPS_LO16 relocations without an immediately preceding R_MIPS_HI16 use the last seen
|
||||
// R_MIPS_HI16 addend.
|
||||
// See https://refspecs.linuxfoundation.org/elf/mipsabi.pdf pages 4-17 and 4-18
|
||||
let mut paired_relocations = Vec::with_capacity(object.sections().count() + 1);
|
||||
for obj_section in object.sections() {
|
||||
let data = obj_section.data()?;
|
||||
let mut last_hi = None;
|
||||
let mut last_hi_addend = 0;
|
||||
let mut addends = BTreeMap::new();
|
||||
for (addr, reloc) in obj_section.relocations() {
|
||||
if !reloc.has_implicit_addend() {
|
||||
continue;
|
||||
}
|
||||
match reloc.flags() {
|
||||
object::RelocationFlags::Elf { r_type: elf::R_MIPS_HI16 } => {
|
||||
let code = data[addr as usize..addr as usize + 4].try_into()?;
|
||||
let addend = ((endianness.read_u32_bytes(code) & 0x0000FFFF) << 16) as i32;
|
||||
last_hi = Some(addr);
|
||||
last_hi_addend = addend;
|
||||
}
|
||||
object::RelocationFlags::Elf { r_type: elf::R_MIPS_LO16 } => {
|
||||
let code = data[addr as usize..addr as usize + 4].try_into()?;
|
||||
let addend = (endianness.read_u32_bytes(code) & 0x0000FFFF) as i16 as i32;
|
||||
let full_addend = (last_hi_addend + addend) as i64;
|
||||
if let Some(hi_addr) = last_hi.take() {
|
||||
addends.insert(hi_addr, full_addend);
|
||||
}
|
||||
addends.insert(addr, full_addend);
|
||||
}
|
||||
_ => {
|
||||
last_hi = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
let section_index = obj_section.index().0;
|
||||
if section_index >= paired_relocations.len() {
|
||||
paired_relocations.resize_with(section_index + 1, BTreeMap::new);
|
||||
}
|
||||
paired_relocations[section_index] = addends;
|
||||
}
|
||||
|
||||
Ok(Self { endianness, abi, isa_extension, ri_gp_value, paired_relocations })
|
||||
}
|
||||
|
||||
fn instruction_flags(&self, diff_config: &DiffObjConfig) -> rabbitizer::InstructionFlags {
|
||||
let isa_extension = match diff_config.mips_instr_category {
|
||||
MipsInstrCategory::Auto => self.isa_extension,
|
||||
MipsInstrCategory::Cpu => None,
|
||||
MipsInstrCategory::Rsp => Some(IsaExtension::RSP),
|
||||
MipsInstrCategory::R3000gte => Some(IsaExtension::R3000GTE),
|
||||
MipsInstrCategory::R4000allegrex => Some(IsaExtension::R4000ALLEGREX),
|
||||
MipsInstrCategory::R5900 => Some(IsaExtension::R5900EE),
|
||||
};
|
||||
match isa_extension {
|
||||
Some(extension) => rabbitizer::InstructionFlags::new_extension(extension),
|
||||
None => rabbitizer::InstructionFlags::new_isa(IsaVersion::MIPS_III, None),
|
||||
}
|
||||
.with_abi(match diff_config.mips_abi {
|
||||
MipsAbi::Auto => self.abi,
|
||||
MipsAbi::O32 => Abi::O32,
|
||||
MipsAbi::N32 => Abi::N32,
|
||||
MipsAbi::N64 => Abi::N64,
|
||||
})
|
||||
}
|
||||
|
||||
fn instruction_display_flags(
|
||||
&self,
|
||||
_diff_config: &DiffObjConfig,
|
||||
) -> rabbitizer::InstructionDisplayFlags {
|
||||
rabbitizer::InstructionDisplayFlags::default().with_unknown_instr_comment(false)
|
||||
}
|
||||
|
||||
fn parse_ins_ref(
|
||||
&self,
|
||||
ins_ref: InstructionRef,
|
||||
code: &[u8],
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> Result<rabbitizer::Instruction> {
|
||||
Ok(rabbitizer::Instruction::new(
|
||||
self.endianness.read_u32_bytes(code.try_into()?),
|
||||
Vram::new(ins_ref.address as u32),
|
||||
self.instruction_flags(diff_config),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Arch for ArchMips {
|
||||
fn scan_instructions(
|
||||
&self,
|
||||
address: u64,
|
||||
code: &[u8],
|
||||
_section_index: usize,
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> Result<Vec<ScannedInstruction>> {
|
||||
let instruction_flags = self.instruction_flags(diff_config);
|
||||
let mut ops = Vec::<ScannedInstruction>::with_capacity(code.len() / 4);
|
||||
let mut cur_addr = address as u32;
|
||||
for chunk in code.chunks_exact(4) {
|
||||
let code = self.endianness.read_u32_bytes(chunk.try_into()?);
|
||||
let instruction =
|
||||
rabbitizer::Instruction::new(code, Vram::new(cur_addr), instruction_flags);
|
||||
let opcode = instruction.opcode() as u16;
|
||||
let branch_dest = instruction.get_branch_vram_generic().map(|v| v.inner() as u64);
|
||||
ops.push(ScannedInstruction {
|
||||
ins_ref: InstructionRef { address: cur_addr as u64, size: 4, opcode },
|
||||
branch_dest,
|
||||
});
|
||||
cur_addr += 4;
|
||||
}
|
||||
Ok(ops)
|
||||
}
|
||||
|
||||
fn display_instruction(
|
||||
&self,
|
||||
resolved: ResolvedInstructionRef,
|
||||
diff_config: &DiffObjConfig,
|
||||
cb: &mut dyn FnMut(InstructionPart) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
let instruction = self.parse_ins_ref(resolved.ins_ref, resolved.code, diff_config)?;
|
||||
let display_flags = self.instruction_display_flags(diff_config);
|
||||
let opcode = instruction.opcode();
|
||||
cb(InstructionPart::opcode(opcode.name(), opcode as u16))?;
|
||||
let start_address = resolved.symbol.address;
|
||||
let function_range = start_address..start_address + resolved.symbol.size;
|
||||
push_args(
|
||||
&instruction,
|
||||
resolved.relocation,
|
||||
function_range,
|
||||
resolved.section_index,
|
||||
&display_flags,
|
||||
diff_config,
|
||||
cb,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn implcit_addend(
|
||||
&self,
|
||||
file: &object::File<'_>,
|
||||
section: &object::Section,
|
||||
address: u64,
|
||||
reloc: &object::Relocation,
|
||||
flags: RelocationFlags,
|
||||
) -> Result<i64> {
|
||||
// Check for paired R_MIPS_HI16 and R_MIPS_LO16 relocations.
|
||||
if let RelocationFlags::Elf(elf::R_MIPS_HI16 | elf::R_MIPS_LO16) = flags {
|
||||
if let Some(addend) = self
|
||||
.paired_relocations
|
||||
.get(section.index().0)
|
||||
.and_then(|m| m.get(&address).copied())
|
||||
{
|
||||
return Ok(addend);
|
||||
}
|
||||
}
|
||||
|
||||
let data = section.data()?;
|
||||
let code = data[address as usize..address as usize + 4].try_into()?;
|
||||
let addend = self.endianness.read_u32_bytes(code);
|
||||
Ok(match flags {
|
||||
RelocationFlags::Elf(elf::R_MIPS_32) => addend as i64,
|
||||
RelocationFlags::Elf(elf::R_MIPS_26) => ((addend & 0x03FFFFFF) << 2) as i64,
|
||||
RelocationFlags::Elf(elf::R_MIPS_HI16) => ((addend & 0x0000FFFF) << 16) as i32 as i64,
|
||||
RelocationFlags::Elf(elf::R_MIPS_LO16 | elf::R_MIPS_GOT16 | elf::R_MIPS_CALL16) => {
|
||||
(addend & 0x0000FFFF) as i16 as i64
|
||||
}
|
||||
RelocationFlags::Elf(elf::R_MIPS_GPREL16 | elf::R_MIPS_LITERAL) => {
|
||||
let object::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(elf::R_MIPS_PC16) => 0, // PC-relative relocation
|
||||
RelocationFlags::Elf(R_MIPS15_S3) => ((addend & 0x001FFFC0) >> 3) as i64,
|
||||
flags => bail!("Unsupported MIPS implicit relocation {flags:?}"),
|
||||
})
|
||||
}
|
||||
|
||||
fn reloc_name(&self, flags: RelocationFlags) -> Option<&'static str> {
|
||||
match flags {
|
||||
RelocationFlags::Elf(r_type) => match r_type {
|
||||
elf::R_MIPS_NONE => Some("R_MIPS_NONE"),
|
||||
elf::R_MIPS_16 => Some("R_MIPS_16"),
|
||||
elf::R_MIPS_32 => Some("R_MIPS_32"),
|
||||
elf::R_MIPS_26 => Some("R_MIPS_26"),
|
||||
elf::R_MIPS_HI16 => Some("R_MIPS_HI16"),
|
||||
elf::R_MIPS_LO16 => Some("R_MIPS_LO16"),
|
||||
elf::R_MIPS_GPREL16 => Some("R_MIPS_GPREL16"),
|
||||
elf::R_MIPS_LITERAL => Some("R_MIPS_LITERAL"),
|
||||
elf::R_MIPS_GOT16 => Some("R_MIPS_GOT16"),
|
||||
elf::R_MIPS_PC16 => Some("R_MIPS_PC16"),
|
||||
elf::R_MIPS_CALL16 => Some("R_MIPS_CALL16"),
|
||||
R_MIPS15_S3 => Some("R_MIPS15_S3"),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn data_reloc_size(&self, flags: RelocationFlags) -> usize {
|
||||
match flags {
|
||||
RelocationFlags::Elf(r_type) => match r_type {
|
||||
elf::R_MIPS_16 => 2,
|
||||
elf::R_MIPS_32 => 4,
|
||||
_ => 1,
|
||||
},
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn push_args(
|
||||
instruction: &rabbitizer::Instruction,
|
||||
relocation: Option<ResolvedRelocation>,
|
||||
function_range: Range<u64>,
|
||||
section_index: usize,
|
||||
display_flags: &rabbitizer::InstructionDisplayFlags,
|
||||
diff_config: &DiffObjConfig,
|
||||
mut arg_cb: impl FnMut(InstructionPart) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
let operands = instruction.valued_operands_iter();
|
||||
for (idx, op) in operands.enumerate() {
|
||||
if idx > 0 {
|
||||
arg_cb(InstructionPart::separator())?;
|
||||
}
|
||||
|
||||
match op {
|
||||
ValuedOperand::core_immediate(imm) => {
|
||||
if let Some(resolved) = relocation {
|
||||
push_reloc(resolved.relocation, &mut arg_cb)?;
|
||||
} else {
|
||||
arg_cb(match imm {
|
||||
IU16::Integer(s) => InstructionPart::signed(s),
|
||||
IU16::Unsigned(u) => InstructionPart::unsigned(u),
|
||||
})?;
|
||||
}
|
||||
}
|
||||
ValuedOperand::core_label(..) | ValuedOperand::core_branch_target_label(..) => {
|
||||
if let Some(resolved) = relocation {
|
||||
// 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 =
|
||||
resolved.symbol.address.checked_add_signed(resolved.relocation.addend);
|
||||
if resolved.symbol.section == Some(section_index)
|
||||
&& target_address.is_some_and(|addr| {
|
||||
addr > function_range.start && addr < function_range.end
|
||||
})
|
||||
{
|
||||
// TODO move this logic up a level
|
||||
let target_address = target_address.unwrap();
|
||||
arg_cb(InstructionPart::branch_dest(target_address))?;
|
||||
} else {
|
||||
push_reloc(resolved.relocation, &mut arg_cb)?;
|
||||
}
|
||||
} else if let Some(branch_dest) = instruction
|
||||
.get_branch_offset_generic()
|
||||
.map(|o| (instruction.vram() + o).inner() as u64)
|
||||
{
|
||||
arg_cb(InstructionPart::branch_dest(branch_dest))?;
|
||||
} else {
|
||||
arg_cb(InstructionPart::opaque(
|
||||
op.display(instruction, display_flags, None::<&str>).to_string(),
|
||||
))?;
|
||||
}
|
||||
}
|
||||
ValuedOperand::core_immediate_base(imm, base) => {
|
||||
if let Some(resolved) = relocation {
|
||||
push_reloc(resolved.relocation, &mut arg_cb)?;
|
||||
} else {
|
||||
arg_cb(InstructionPart::Arg(InstructionArg::Value(match imm {
|
||||
IU16::Integer(s) => InstructionArgValue::Signed(s as i64),
|
||||
IU16::Unsigned(u) => InstructionArgValue::Unsigned(u as u64),
|
||||
})))?;
|
||||
}
|
||||
arg_cb(InstructionPart::basic("("))?;
|
||||
let mut value =
|
||||
base.either_name(instruction.flags().abi(), display_flags.named_gpr());
|
||||
if !diff_config.mips_register_prefix {
|
||||
if let Some(trimmed) = value.strip_prefix('$') {
|
||||
value = trimmed;
|
||||
}
|
||||
}
|
||||
arg_cb(InstructionPart::opaque(value))?;
|
||||
arg_cb(InstructionPart::basic(")"))?;
|
||||
}
|
||||
// ValuedOperand::r5900_immediate15(..) => match relocation {
|
||||
// Some(resolved)
|
||||
// if resolved.relocation.flags == RelocationFlags::Elf(R_MIPS15_S3) =>
|
||||
// {
|
||||
// push_reloc(&resolved.relocation, &mut arg_cb)?;
|
||||
// }
|
||||
// _ => {
|
||||
// arg_cb(InstructionPart::opaque(op.disassemble(&instruction, None)))?;
|
||||
// }
|
||||
// },
|
||||
_ => {
|
||||
let value = op.display(instruction, display_flags, None::<&str>).to_string();
|
||||
if !diff_config.mips_register_prefix {
|
||||
if let Some(value) = value.strip_prefix('$') {
|
||||
arg_cb(InstructionPart::opaque(value))?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
arg_cb(InstructionPart::opaque(value))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn push_reloc(
|
||||
reloc: &Relocation,
|
||||
mut arg_cb: impl FnMut(InstructionPart) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
match reloc.flags {
|
||||
RelocationFlags::Elf(r_type) => match r_type {
|
||||
elf::R_MIPS_HI16 => {
|
||||
arg_cb(InstructionPart::basic("%hi("))?;
|
||||
arg_cb(InstructionPart::reloc())?;
|
||||
arg_cb(InstructionPart::basic(")"))?;
|
||||
}
|
||||
elf::R_MIPS_LO16 => {
|
||||
arg_cb(InstructionPart::basic("%lo("))?;
|
||||
arg_cb(InstructionPart::reloc())?;
|
||||
arg_cb(InstructionPart::basic(")"))?;
|
||||
}
|
||||
elf::R_MIPS_GOT16 => {
|
||||
arg_cb(InstructionPart::basic("%got("))?;
|
||||
arg_cb(InstructionPart::reloc())?;
|
||||
arg_cb(InstructionPart::basic(")"))?;
|
||||
}
|
||||
elf::R_MIPS_CALL16 => {
|
||||
arg_cb(InstructionPart::basic("%call16("))?;
|
||||
arg_cb(InstructionPart::reloc())?;
|
||||
arg_cb(InstructionPart::basic(")"))?;
|
||||
}
|
||||
elf::R_MIPS_GPREL16 => {
|
||||
arg_cb(InstructionPart::basic("%gp_rel("))?;
|
||||
arg_cb(InstructionPart::reloc())?;
|
||||
arg_cb(InstructionPart::basic(")"))?;
|
||||
}
|
||||
elf::R_MIPS_32
|
||||
| elf::R_MIPS_26
|
||||
| elf::R_MIPS_LITERAL
|
||||
| elf::R_MIPS_PC16
|
||||
| R_MIPS15_S3 => {
|
||||
arg_cb(InstructionPart::reloc())?;
|
||||
}
|
||||
_ => bail!("Unsupported ELF MIPS relocation type {r_type}"),
|
||||
},
|
||||
flags => panic!("Unsupported MIPS relocation flags {flags:?}"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
317
objdiff-core/src/arch/mod.rs
Normal file
@@ -0,0 +1,317 @@
|
||||
use alloc::{borrow::Cow, boxed::Box, format, string::String, vec::Vec};
|
||||
use core::{ffi::CStr, fmt, fmt::Debug};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use object::Endian as _;
|
||||
|
||||
use crate::{
|
||||
diff::{
|
||||
display::{ContextItem, HoverItem, InstructionPart},
|
||||
DiffObjConfig,
|
||||
},
|
||||
obj::{
|
||||
InstructionArg, Object, ParsedInstruction, RelocationFlags, ResolvedInstructionRef,
|
||||
ScannedInstruction, SymbolFlagSet, SymbolKind,
|
||||
},
|
||||
util::ReallySigned,
|
||||
};
|
||||
|
||||
#[cfg(feature = "arm")]
|
||||
pub mod arm;
|
||||
#[cfg(feature = "arm64")]
|
||||
pub 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,
|
||||
Float,
|
||||
Double,
|
||||
Bytes,
|
||||
String,
|
||||
}
|
||||
|
||||
impl fmt::Display for DataType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
DataType::Int8 => write!(f, "Int8"),
|
||||
DataType::Int16 => write!(f, "Int16"),
|
||||
DataType::Int32 => write!(f, "Int32"),
|
||||
DataType::Int64 => write!(f, "Int64"),
|
||||
DataType::Float => write!(f, "Float"),
|
||||
DataType::Double => write!(f, "Double"),
|
||||
DataType::Bytes => write!(f, "Bytes"),
|
||||
DataType::String => write!(f, "String"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DataType {
|
||||
pub fn display_labels(&self, endian: object::Endianness, bytes: &[u8]) -> Vec<String> {
|
||||
let mut strs = Vec::new();
|
||||
for literal in self.display_literals(endian, bytes) {
|
||||
strs.push(format!("{}: {}", self, literal))
|
||||
}
|
||||
strs
|
||||
}
|
||||
|
||||
pub fn display_literals(&self, endian: object::Endianness, bytes: &[u8]) -> Vec<String> {
|
||||
let mut strs = Vec::new();
|
||||
if self.required_len().is_some_and(|l| bytes.len() < l) {
|
||||
log::warn!("Failed to display a symbol value for a symbol whose size is too small for instruction referencing it.");
|
||||
return strs;
|
||||
}
|
||||
let mut bytes = bytes;
|
||||
if self.required_len().is_some_and(|l| bytes.len() > l) {
|
||||
// If the symbol's size is larger a single instance of this data type, we take just the
|
||||
// bytes necessary for one of them in order to display the first element of the array.
|
||||
bytes = &bytes[0..self.required_len().unwrap()];
|
||||
// TODO: Attempt to interpret large symbols as arrays of a smaller type and show all
|
||||
// elements of the array instead. https://github.com/encounter/objdiff/issues/124
|
||||
// However, note that the stride of an array can not always be determined just by the
|
||||
// data type guessed by the single instruction accessing it. There can also be arrays of
|
||||
// structs that contain multiple elements of different types, so if other elements after
|
||||
// the first one were to be displayed in this manner, they may be inaccurate.
|
||||
}
|
||||
|
||||
match self {
|
||||
DataType::Int8 => {
|
||||
let i = i8::from_ne_bytes(bytes.try_into().unwrap());
|
||||
strs.push(format!("{:#x}", i));
|
||||
|
||||
if i < 0 {
|
||||
strs.push(format!("{:#x}", ReallySigned(i)));
|
||||
}
|
||||
}
|
||||
DataType::Int16 => {
|
||||
let i = endian.read_i16_bytes(bytes.try_into().unwrap());
|
||||
strs.push(format!("{:#x}", i));
|
||||
|
||||
if i < 0 {
|
||||
strs.push(format!("{:#x}", ReallySigned(i)));
|
||||
}
|
||||
}
|
||||
DataType::Int32 => {
|
||||
let i = endian.read_i32_bytes(bytes.try_into().unwrap());
|
||||
strs.push(format!("{:#x}", i));
|
||||
|
||||
if i < 0 {
|
||||
strs.push(format!("{:#x}", ReallySigned(i)));
|
||||
}
|
||||
}
|
||||
DataType::Int64 => {
|
||||
let i = endian.read_i64_bytes(bytes.try_into().unwrap());
|
||||
strs.push(format!("{:#x}", i));
|
||||
|
||||
if i < 0 {
|
||||
strs.push(format!("{:#x}", ReallySigned(i)));
|
||||
}
|
||||
}
|
||||
DataType::Float => {
|
||||
let bytes: [u8; 4] = bytes.try_into().unwrap();
|
||||
strs.push(format!("{:?}f", match endian {
|
||||
object::Endianness::Little => f32::from_le_bytes(bytes),
|
||||
object::Endianness::Big => f32::from_be_bytes(bytes),
|
||||
}));
|
||||
}
|
||||
DataType::Double => {
|
||||
let bytes: [u8; 8] = bytes.try_into().unwrap();
|
||||
strs.push(format!("{:?}", match endian {
|
||||
object::Endianness::Little => f64::from_le_bytes(bytes),
|
||||
object::Endianness::Big => f64::from_be_bytes(bytes),
|
||||
}));
|
||||
}
|
||||
DataType::Bytes => {
|
||||
strs.push(format!("{:#?}", bytes));
|
||||
}
|
||||
DataType::String => {
|
||||
if let Ok(cstr) = CStr::from_bytes_until_nul(bytes) {
|
||||
strs.push(format!("{:?}", cstr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
strs
|
||||
}
|
||||
|
||||
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::Float => Some(4),
|
||||
DataType::Double => Some(8),
|
||||
DataType::Bytes => None,
|
||||
DataType::String => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Arch: Send + Sync + Debug {
|
||||
/// Generate a list of instructions references (offset, size, opcode) from the given code.
|
||||
///
|
||||
/// The opcode IDs are used to generate the initial diff. Implementations should do as little
|
||||
/// parsing as possible here: just enough to identify the base instruction opcode, size, and
|
||||
/// possible branch destination (for visual representation). As needed, instructions are parsed
|
||||
/// via `process_instruction` to compare their arguments.
|
||||
fn scan_instructions(
|
||||
&self,
|
||||
address: u64,
|
||||
code: &[u8],
|
||||
section_index: usize,
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> Result<Vec<ScannedInstruction>>;
|
||||
|
||||
/// Parse an instruction to gather its mnemonic and arguments for more detailed comparison.
|
||||
///
|
||||
/// This is called only when we need to compare the arguments of an instruction.
|
||||
fn process_instruction(
|
||||
&self,
|
||||
resolved: ResolvedInstructionRef,
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> Result<ParsedInstruction> {
|
||||
let mut mnemonic = None;
|
||||
let mut args = Vec::with_capacity(8);
|
||||
self.display_instruction(resolved, diff_config, &mut |part| {
|
||||
match part {
|
||||
InstructionPart::Opcode(m, _) => mnemonic = Some(Cow::Owned(m.into_owned())),
|
||||
InstructionPart::Arg(arg) => args.push(arg.into_static()),
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
// If the instruction has a relocation, but we didn't format it in the display, add it to
|
||||
// the end of the arguments list.
|
||||
if resolved.relocation.is_some() && !args.contains(&InstructionArg::Reloc) {
|
||||
args.push(InstructionArg::Reloc);
|
||||
}
|
||||
Ok(ParsedInstruction {
|
||||
ins_ref: resolved.ins_ref,
|
||||
mnemonic: mnemonic.unwrap_or_default(),
|
||||
args,
|
||||
})
|
||||
}
|
||||
|
||||
/// Format an instruction for display.
|
||||
///
|
||||
/// Implementations should call the callback for each part of the instruction: usually the
|
||||
/// mnemonic and arguments, plus any separators and visual formatting.
|
||||
fn display_instruction(
|
||||
&self,
|
||||
resolved: ResolvedInstructionRef,
|
||||
diff_config: &DiffObjConfig,
|
||||
cb: &mut dyn FnMut(InstructionPart) -> Result<()>,
|
||||
) -> Result<()>;
|
||||
|
||||
fn implcit_addend(
|
||||
&self,
|
||||
file: &object::File<'_>,
|
||||
section: &object::Section,
|
||||
address: u64,
|
||||
relocation: &object::Relocation,
|
||||
flags: RelocationFlags,
|
||||
) -> Result<i64>;
|
||||
|
||||
fn demangle(&self, _name: &str) -> Option<String> { None }
|
||||
|
||||
fn reloc_name(&self, _flags: RelocationFlags) -> Option<&'static str> { None }
|
||||
|
||||
fn data_reloc_size(&self, flags: RelocationFlags) -> usize;
|
||||
|
||||
fn symbol_address(&self, address: u64, _kind: SymbolKind) -> u64 { address }
|
||||
|
||||
fn extra_symbol_flags(&self, _symbol: &object::Symbol) -> SymbolFlagSet {
|
||||
SymbolFlagSet::default()
|
||||
}
|
||||
|
||||
fn guess_data_type(&self, _resolved: ResolvedInstructionRef) -> Option<DataType> { None }
|
||||
|
||||
fn symbol_hover(&self, _obj: &Object, _symbol_index: usize) -> Vec<HoverItem> { Vec::new() }
|
||||
|
||||
fn symbol_context(&self, _obj: &Object, _symbol_index: usize) -> Vec<ContextItem> { Vec::new() }
|
||||
|
||||
fn instruction_hover(
|
||||
&self,
|
||||
_obj: &Object,
|
||||
_resolved: ResolvedInstructionRef,
|
||||
) -> Vec<HoverItem> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn instruction_context(
|
||||
&self,
|
||||
_obj: &Object,
|
||||
_resolved: ResolvedInstructionRef,
|
||||
) -> Vec<ContextItem> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_arch(object: &object::File) -> Result<Box<dyn Arch>> {
|
||||
use object::Object as _;
|
||||
Ok(match object.architecture() {
|
||||
#[cfg(feature = "ppc")]
|
||||
object::Architecture::PowerPc => Box::new(ppc::ArchPpc::new(object)?),
|
||||
#[cfg(feature = "mips")]
|
||||
object::Architecture::Mips => Box::new(mips::ArchMips::new(object)?),
|
||||
#[cfg(feature = "x86")]
|
||||
object::Architecture::I386 | object::Architecture::X86_64 => {
|
||||
Box::new(x86::ArchX86::new(object)?)
|
||||
}
|
||||
#[cfg(feature = "arm")]
|
||||
object::Architecture::Arm => Box::new(arm::ArchArm::new(object)?),
|
||||
#[cfg(feature = "arm64")]
|
||||
object::Architecture::Aarch64 => Box::new(arm64::ArchArm64::new(object)?),
|
||||
arch => bail!("Unsupported architecture: {arch:?}"),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ArchDummy {}
|
||||
|
||||
impl ArchDummy {
|
||||
pub fn new() -> Box<Self> { Box::new(Self {}) }
|
||||
}
|
||||
|
||||
impl Arch for ArchDummy {
|
||||
fn scan_instructions(
|
||||
&self,
|
||||
_address: u64,
|
||||
_code: &[u8],
|
||||
_section_index: usize,
|
||||
_diff_config: &DiffObjConfig,
|
||||
) -> Result<Vec<ScannedInstruction>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn display_instruction(
|
||||
&self,
|
||||
_resolved: ResolvedInstructionRef,
|
||||
_diff_config: &DiffObjConfig,
|
||||
_cb: &mut dyn FnMut(InstructionPart) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn implcit_addend(
|
||||
&self,
|
||||
_file: &object::File<'_>,
|
||||
_section: &object::Section,
|
||||
_address: u64,
|
||||
_relocation: &object::Relocation,
|
||||
_flags: RelocationFlags,
|
||||
) -> Result<i64> {
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
fn data_reloc_size(&self, _flags: RelocationFlags) -> usize { 0 }
|
||||
}
|
||||
831
objdiff-core/src/arch/ppc.rs
Normal file
@@ -0,0 +1,831 @@
|
||||
use alloc::{
|
||||
collections::BTreeMap,
|
||||
string::{String, ToString},
|
||||
vec,
|
||||
vec::Vec,
|
||||
};
|
||||
|
||||
use anyhow::{bail, ensure, Result};
|
||||
use cwextab::{decode_extab, ExceptionTableData};
|
||||
use flagset::Flags;
|
||||
use object::{elf, Object as _, ObjectSection as _, ObjectSymbol as _};
|
||||
|
||||
use crate::{
|
||||
arch::{Arch, DataType},
|
||||
diff::{
|
||||
display::{ContextItem, HoverItem, HoverItemColor, InstructionPart, SymbolNavigationKind},
|
||||
DiffObjConfig,
|
||||
},
|
||||
obj::{
|
||||
InstructionRef, Object, Relocation, RelocationFlags, ResolvedInstructionRef,
|
||||
ResolvedRelocation, ScannedInstruction, SymbolFlag, SymbolFlagSet,
|
||||
},
|
||||
};
|
||||
|
||||
// Relative relocation, can be Simm, Offset or BranchDest
|
||||
fn is_relative_arg(arg: &ppc750cl::Argument) -> bool {
|
||||
matches!(
|
||||
arg,
|
||||
ppc750cl::Argument::Simm(_)
|
||||
| ppc750cl::Argument::Offset(_)
|
||||
| ppc750cl::Argument::BranchDest(_)
|
||||
)
|
||||
}
|
||||
|
||||
// Relative or absolute relocation, can be Uimm, Simm or Offset
|
||||
fn is_rel_abs_arg(arg: &ppc750cl::Argument) -> bool {
|
||||
matches!(
|
||||
arg,
|
||||
ppc750cl::Argument::Uimm(_) | ppc750cl::Argument::Simm(_) | ppc750cl::Argument::Offset(_)
|
||||
)
|
||||
}
|
||||
|
||||
fn is_offset_arg(arg: &ppc750cl::Argument) -> bool { matches!(arg, ppc750cl::Argument::Offset(_)) }
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ArchPpc {
|
||||
/// Exception info
|
||||
pub extab: Option<BTreeMap<usize, ExceptionInfo>>,
|
||||
}
|
||||
|
||||
impl ArchPpc {
|
||||
pub fn new(file: &object::File) -> Result<Self> {
|
||||
Ok(Self { extab: decode_exception_info(file)? })
|
||||
}
|
||||
|
||||
fn parse_ins_ref(&self, resolved: ResolvedInstructionRef) -> Result<ppc750cl::Ins> {
|
||||
let mut code = u32::from_be_bytes(resolved.code.try_into()?);
|
||||
if let Some(reloc) = resolved.relocation {
|
||||
code = zero_reloc(code, reloc.relocation);
|
||||
}
|
||||
let op = ppc750cl::Opcode::from(resolved.ins_ref.opcode as u8);
|
||||
Ok(ppc750cl::Ins { code, op })
|
||||
}
|
||||
|
||||
fn find_reloc_arg(
|
||||
&self,
|
||||
ins: &ppc750cl::ParsedIns,
|
||||
resolved: Option<ResolvedRelocation>,
|
||||
) -> Option<usize> {
|
||||
match resolved?.relocation.flags {
|
||||
RelocationFlags::Elf(elf::R_PPC_EMB_SDA21) => Some(1),
|
||||
RelocationFlags::Elf(elf::R_PPC_REL24 | elf::R_PPC_REL14) => {
|
||||
ins.args.iter().rposition(is_relative_arg)
|
||||
}
|
||||
RelocationFlags::Elf(
|
||||
elf::R_PPC_ADDR16_HI | elf::R_PPC_ADDR16_HA | elf::R_PPC_ADDR16_LO,
|
||||
) => ins.args.iter().rposition(is_rel_abs_arg),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arch for ArchPpc {
|
||||
fn scan_instructions(
|
||||
&self,
|
||||
address: u64,
|
||||
code: &[u8],
|
||||
_section_index: usize,
|
||||
_diff_config: &DiffObjConfig,
|
||||
) -> Result<Vec<ScannedInstruction>> {
|
||||
ensure!(code.len() & 3 == 0, "Code length must be a multiple of 4");
|
||||
let ins_count = code.len() / 4;
|
||||
let mut insts = Vec::<ScannedInstruction>::with_capacity(ins_count);
|
||||
for (cur_addr, ins) in ppc750cl::InsIter::new(code, address as u32) {
|
||||
insts.push(ScannedInstruction {
|
||||
ins_ref: InstructionRef {
|
||||
address: cur_addr as u64,
|
||||
size: 4,
|
||||
opcode: u8::from(ins.op) as u16,
|
||||
},
|
||||
branch_dest: ins.branch_dest(cur_addr).map(u64::from),
|
||||
});
|
||||
}
|
||||
Ok(insts)
|
||||
}
|
||||
|
||||
fn display_instruction(
|
||||
&self,
|
||||
resolved: ResolvedInstructionRef,
|
||||
_diff_config: &DiffObjConfig,
|
||||
cb: &mut dyn FnMut(InstructionPart) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
let ins = self.parse_ins_ref(resolved)?.simplified();
|
||||
|
||||
cb(InstructionPart::opcode(ins.mnemonic, resolved.ins_ref.opcode))?;
|
||||
|
||||
let reloc_arg = self.find_reloc_arg(&ins, resolved.relocation);
|
||||
|
||||
let mut writing_offset = false;
|
||||
for (idx, arg) in ins.args_iter().enumerate() {
|
||||
if idx > 0 && !writing_offset {
|
||||
cb(InstructionPart::separator())?;
|
||||
}
|
||||
|
||||
if reloc_arg == Some(idx) {
|
||||
let reloc = resolved.relocation.unwrap();
|
||||
display_reloc(reloc, cb)?;
|
||||
// For @sda21, we can omit the register argument
|
||||
if matches!(reloc.relocation.flags, RelocationFlags::Elf(elf::R_PPC_EMB_SDA21))
|
||||
// Sanity check: the next argument should be r0
|
||||
&& matches!(ins.args.get(idx + 1), Some(ppc750cl::Argument::GPR(ppc750cl::GPR(0))))
|
||||
{
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
match arg {
|
||||
ppc750cl::Argument::Simm(simm) => cb(InstructionPart::signed(simm.0)),
|
||||
ppc750cl::Argument::Uimm(uimm) => cb(InstructionPart::unsigned(uimm.0)),
|
||||
ppc750cl::Argument::Offset(offset) => cb(InstructionPart::signed(offset.0)),
|
||||
ppc750cl::Argument::BranchDest(dest) => cb(InstructionPart::branch_dest(
|
||||
(resolved.ins_ref.address as u32).wrapping_add_signed(dest.0),
|
||||
)),
|
||||
_ => cb(InstructionPart::opaque(arg.to_string())),
|
||||
}?;
|
||||
}
|
||||
|
||||
if writing_offset {
|
||||
cb(InstructionPart::basic(")"))?;
|
||||
writing_offset = false;
|
||||
}
|
||||
if is_offset_arg(arg) {
|
||||
cb(InstructionPart::basic("("))?;
|
||||
writing_offset = true;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn implcit_addend(
|
||||
&self,
|
||||
_file: &object::File<'_>,
|
||||
_section: &object::Section,
|
||||
address: u64,
|
||||
_relocation: &object::Relocation,
|
||||
flags: RelocationFlags,
|
||||
) -> Result<i64> {
|
||||
bail!("Unsupported PPC implicit relocation {:#x}:{:?}", address, flags)
|
||||
}
|
||||
|
||||
fn demangle(&self, name: &str) -> Option<String> {
|
||||
cwdemangle::demangle(name, &cwdemangle::DemangleOptions::default())
|
||||
}
|
||||
|
||||
fn reloc_name(&self, flags: RelocationFlags) -> Option<&'static str> {
|
||||
match flags {
|
||||
RelocationFlags::Elf(r_type) => match r_type {
|
||||
elf::R_PPC_NONE => Some("R_PPC_NONE"), // We use this for fake pool relocs
|
||||
elf::R_PPC_ADDR16_LO => Some("R_PPC_ADDR16_LO"),
|
||||
elf::R_PPC_ADDR16_HI => Some("R_PPC_ADDR16_HI"),
|
||||
elf::R_PPC_ADDR16_HA => Some("R_PPC_ADDR16_HA"),
|
||||
elf::R_PPC_EMB_SDA21 => Some("R_PPC_EMB_SDA21"),
|
||||
elf::R_PPC_ADDR32 => Some("R_PPC_ADDR32"),
|
||||
elf::R_PPC_UADDR32 => Some("R_PPC_UADDR32"),
|
||||
elf::R_PPC_REL24 => Some("R_PPC_REL24"),
|
||||
elf::R_PPC_REL14 => Some("R_PPC_REL14"),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn data_reloc_size(&self, flags: RelocationFlags) -> usize {
|
||||
match flags {
|
||||
RelocationFlags::Elf(r_type) => match r_type {
|
||||
elf::R_PPC_ADDR32 => 4,
|
||||
elf::R_PPC_UADDR32 => 4,
|
||||
_ => 1,
|
||||
},
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn extra_symbol_flags(&self, symbol: &object::Symbol) -> SymbolFlagSet {
|
||||
if self.extab.as_ref().is_some_and(|extab| extab.contains_key(&(symbol.index().0 - 1))) {
|
||||
SymbolFlag::HasExtra.into()
|
||||
} else {
|
||||
SymbolFlag::none()
|
||||
}
|
||||
}
|
||||
|
||||
fn guess_data_type(&self, resolved: ResolvedInstructionRef) -> Option<DataType> {
|
||||
if resolved.relocation.is_some_and(|r| r.symbol.name.starts_with("@stringBase")) {
|
||||
return Some(DataType::String);
|
||||
}
|
||||
let opcode = ppc750cl::Opcode::from(resolved.ins_ref.opcode as u8);
|
||||
guess_data_type_from_load_store_inst_op(opcode)
|
||||
}
|
||||
|
||||
fn symbol_hover(&self, _obj: &Object, symbol_index: usize) -> Vec<HoverItem> {
|
||||
let mut out = Vec::new();
|
||||
if let Some(extab) = self.extab_for_symbol(symbol_index) {
|
||||
out.push(HoverItem::Text {
|
||||
label: "extab symbol".into(),
|
||||
value: extab.etb_symbol.name.clone(),
|
||||
color: HoverItemColor::Special,
|
||||
});
|
||||
out.push(HoverItem::Text {
|
||||
label: "extabindex symbol".into(),
|
||||
value: extab.eti_symbol.name.clone(),
|
||||
color: HoverItemColor::Special,
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn symbol_context(&self, _obj: &Object, symbol_index: usize) -> Vec<ContextItem> {
|
||||
let mut out = Vec::new();
|
||||
if let Some(_extab) = self.extab_for_symbol(symbol_index) {
|
||||
out.push(ContextItem::Navigate {
|
||||
label: "Decode exception table".to_string(),
|
||||
symbol_index,
|
||||
kind: SymbolNavigationKind::Extab,
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn instruction_hover(&self, _obj: &Object, resolved: ResolvedInstructionRef) -> Vec<HoverItem> {
|
||||
let Ok(ins) = self.parse_ins_ref(resolved) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let orig = ins.basic().to_string();
|
||||
let simplified = ins.simplified().to_string();
|
||||
let show_orig = orig != simplified;
|
||||
let rlwinm_decoded = rlwinmdec::decode(&orig);
|
||||
let mut out = Vec::with_capacity(2);
|
||||
if show_orig {
|
||||
out.push(HoverItem::Text {
|
||||
label: "Original".into(),
|
||||
value: orig,
|
||||
color: HoverItemColor::Normal,
|
||||
});
|
||||
}
|
||||
if let Some(decoded) = rlwinm_decoded {
|
||||
for line in decoded.lines() {
|
||||
out.push(HoverItem::Text {
|
||||
label: Default::default(),
|
||||
value: line.to_string(),
|
||||
color: HoverItemColor::Special,
|
||||
});
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn instruction_context(
|
||||
&self,
|
||||
_obj: &Object,
|
||||
resolved: ResolvedInstructionRef,
|
||||
) -> Vec<ContextItem> {
|
||||
let Ok(ins) = self.parse_ins_ref(resolved) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let orig = ins.basic().to_string();
|
||||
let simplified = ins.simplified().to_string();
|
||||
let show_orig = orig != simplified;
|
||||
let mut out = Vec::with_capacity(2);
|
||||
out.push(ContextItem::Copy { value: simplified, label: None });
|
||||
if show_orig {
|
||||
out.push(ContextItem::Copy { value: orig, label: Some("original".to_string()) });
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
impl ArchPpc {
|
||||
pub fn extab_for_symbol(&self, symbol_index: usize) -> Option<&ExceptionInfo> {
|
||||
self.extab.as_ref()?.get(&symbol_index)
|
||||
}
|
||||
}
|
||||
|
||||
fn zero_reloc(code: u32, reloc: &Relocation) -> u32 {
|
||||
match reloc.flags {
|
||||
RelocationFlags::Elf(elf::R_PPC_EMB_SDA21) => code & !0x1FFFFF,
|
||||
RelocationFlags::Elf(elf::R_PPC_REL24) => code & !0x3FFFFFC,
|
||||
RelocationFlags::Elf(elf::R_PPC_REL14) => code & !0xFFFC,
|
||||
RelocationFlags::Elf(
|
||||
elf::R_PPC_ADDR16_HI | elf::R_PPC_ADDR16_HA | elf::R_PPC_ADDR16_LO,
|
||||
) => code & !0xFFFF,
|
||||
_ => code,
|
||||
}
|
||||
}
|
||||
|
||||
fn display_reloc(
|
||||
resolved: ResolvedRelocation,
|
||||
cb: &mut dyn FnMut(InstructionPart) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
match resolved.relocation.flags {
|
||||
RelocationFlags::Elf(r_type) => match r_type {
|
||||
elf::R_PPC_ADDR16_LO => {
|
||||
cb(InstructionPart::reloc())?;
|
||||
cb(InstructionPart::basic("@l"))?;
|
||||
}
|
||||
elf::R_PPC_ADDR16_HI => {
|
||||
cb(InstructionPart::reloc())?;
|
||||
cb(InstructionPart::basic("@h"))?;
|
||||
}
|
||||
elf::R_PPC_ADDR16_HA => {
|
||||
cb(InstructionPart::reloc())?;
|
||||
cb(InstructionPart::basic("@ha"))?;
|
||||
}
|
||||
elf::R_PPC_EMB_SDA21 => {
|
||||
cb(InstructionPart::reloc())?;
|
||||
cb(InstructionPart::basic("@sda21"))?;
|
||||
}
|
||||
elf::R_PPC_ADDR32 | elf::R_PPC_UADDR32 | elf::R_PPC_REL24 | elf::R_PPC_REL14 => {
|
||||
cb(InstructionPart::reloc())?;
|
||||
}
|
||||
elf::R_PPC_NONE => {
|
||||
// Fake pool relocation.
|
||||
cb(InstructionPart::basic("<"))?;
|
||||
cb(InstructionPart::reloc())?;
|
||||
cb(InstructionPart::basic(">"))?;
|
||||
}
|
||||
_ => cb(InstructionPart::reloc())?,
|
||||
},
|
||||
_ => cb(InstructionPart::reloc())?,
|
||||
};
|
||||
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: &object::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, object::Relocation>>();
|
||||
let extabindex_relocations =
|
||||
extabindex_section.relocations().collect::<BTreeMap<u64, object::Relocation>>();
|
||||
|
||||
for extabindex in file.symbols().filter(|symbol| {
|
||||
symbol.section_index() == Some(extabindex_section.index())
|
||||
&& symbol.kind() == object::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 - 1, 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 object::File<'data>,
|
||||
relocation: &object::Relocation,
|
||||
) -> Result<Option<object::Symbol<'data, 'file>>> {
|
||||
let addend = relocation.addend();
|
||||
match relocation.target() {
|
||||
object::RelocationTarget::Symbol(idx) => {
|
||||
ensure!(addend == 0, "Symbol relocations must have zero addend");
|
||||
Ok(Some(file.symbol_by_index(idx)?))
|
||||
}
|
||||
object::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: &object::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 - 1, name, demangled_name })
|
||||
}
|
||||
|
||||
fn guess_data_type_from_load_store_inst_op(inst_op: ppc750cl::Opcode) -> Option<DataType> {
|
||||
use ppc750cl::Opcode;
|
||||
match inst_op {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// Given an instruction, determine if it could accessing data at the address in a register.
|
||||
// If so, return the offset added to the register's address, the register containing that address,
|
||||
// and (optionally) which destination register the address is being copied into.
|
||||
#[expect(unused)]
|
||||
fn get_offset_and_addr_gpr_for_possible_pool_reference(
|
||||
opcode: ppc750cl::Opcode,
|
||||
simplified: &ppc750cl::ParsedIns,
|
||||
) -> Option<(i16, ppc750cl::GPR, Option<ppc750cl::GPR>)> {
|
||||
use ppc750cl::{Argument, Opcode};
|
||||
let args = &simplified.args;
|
||||
if guess_data_type_from_load_store_inst_op(opcode).is_some() {
|
||||
match (args[1], args[2]) {
|
||||
(Argument::Offset(offset), Argument::GPR(addr_src_gpr)) => {
|
||||
// e.g. lwz. Immediate offset.
|
||||
Some((offset.0, addr_src_gpr, None))
|
||||
}
|
||||
(Argument::GPR(addr_src_gpr), Argument::GPR(_offset_gpr)) => {
|
||||
// e.g. lwzx. The offset is in a register and was likely calculated from an index.
|
||||
// Treat the offset as being 0 in this case to show the first element of the array.
|
||||
// It may be possible to show all elements by figuring out the stride of the array
|
||||
// from the calculations performed on the index before it's put into offset_gpr, but
|
||||
// this would be much more complicated, so it's not currently done.
|
||||
Some((0, addr_src_gpr, None))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
// If it's not a load/store instruction, there's two more possibilities we need to handle.
|
||||
// 1. It could be loading a pointer to a string.
|
||||
// 2. It could be moving the relocation address plus an offset into a different register to
|
||||
// load from later.
|
||||
// If either of these match, we also want to return the destination register that the
|
||||
// address is being copied into so that we can detect any future references to that new
|
||||
// register as well.
|
||||
match (opcode, args[0], args[1], args[2]) {
|
||||
(
|
||||
Opcode::Addi,
|
||||
Argument::GPR(addr_dst_gpr),
|
||||
Argument::GPR(addr_src_gpr),
|
||||
Argument::Simm(simm),
|
||||
) => Some((simm.0, addr_src_gpr, Some(addr_dst_gpr))),
|
||||
(
|
||||
// `mr` or `mr.`
|
||||
Opcode::Or,
|
||||
Argument::GPR(addr_dst_gpr),
|
||||
Argument::GPR(addr_src_gpr),
|
||||
Argument::None,
|
||||
) => Some((0, addr_src_gpr, Some(addr_dst_gpr))),
|
||||
(
|
||||
Opcode::Add,
|
||||
Argument::GPR(addr_dst_gpr),
|
||||
Argument::GPR(addr_src_gpr),
|
||||
Argument::GPR(_offset_gpr),
|
||||
) => Some((0, addr_src_gpr, Some(addr_dst_gpr))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the relocation we're keeping track of in a particular register when an instruction reuses
|
||||
// that register to hold some other value, unrelated to pool relocation addresses.
|
||||
#[expect(unused)]
|
||||
fn clear_overwritten_gprs(ins: ppc750cl::Ins, gpr_pool_relocs: &mut BTreeMap<u8, Relocation>) {
|
||||
use ppc750cl::{Argument, Arguments, Opcode};
|
||||
let mut def_args = Arguments::default();
|
||||
ins.parse_defs(&mut def_args);
|
||||
for arg in def_args {
|
||||
if let Argument::GPR(gpr) = arg {
|
||||
if ins.op == Opcode::Lmw {
|
||||
// `lmw` overwrites all registers from rd to r31.
|
||||
// ppc750cl only returns rd itself, so we manually clear the rest of them.
|
||||
for reg in gpr.0..31 {
|
||||
gpr_pool_relocs.remove(®);
|
||||
}
|
||||
break;
|
||||
}
|
||||
gpr_pool_relocs.remove(&gpr.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
// // We create a fake relocation for an instruction, vaguely simulating what the actual relocation
|
||||
// // might have looked like if it wasn't pooled. This is so minimal changes are needed to display
|
||||
// // pooled accesses vs non-pooled accesses. We set the relocation type to R_PPC_NONE to indicate that
|
||||
// // there isn't really a relocation here, as copying the pool relocation's type wouldn't make sense.
|
||||
// // Also, if this instruction is accessing the middle of a symbol instead of the start, we add an
|
||||
// // addend to indicate that.
|
||||
// fn make_fake_pool_reloc(offset: i16, cur_addr: u32, pool_reloc: &Relocation) -> Option<Relocation> {
|
||||
// let offset_from_pool = pool_reloc.addend + offset as i64;
|
||||
// let target_address = pool_reloc.sy.address.checked_add_signed(offset_from_pool)?;
|
||||
// let target;
|
||||
// let addend;
|
||||
// if pool_reloc.target.orig_section_index.is_some() {
|
||||
// // If the target symbol is within this current object, then we also need to create a fake
|
||||
// // target symbol to go inside our fake relocation. This is because we don't have access to
|
||||
// // list of all symbols in this section, so we can't find the real symbol within the pool
|
||||
// // based on its address yet. Instead we make a placeholder that has the correct
|
||||
// // `orig_section_index` and `address` fields, and then later on when this information is
|
||||
// // displayed to the user, we can find the real symbol by searching through the object's
|
||||
// // section's symbols for one that contains this address.
|
||||
// target = ObjSymbol {
|
||||
// name: "".to_string(),
|
||||
// demangled_name: None,
|
||||
// address: target_address,
|
||||
// section_address: 0,
|
||||
// size: 0,
|
||||
// size_known: false,
|
||||
// kind: Default::default(),
|
||||
// flags: Default::default(),
|
||||
// orig_section_index: pool_reloc.target.orig_section_index,
|
||||
// virtual_address: None,
|
||||
// original_index: None,
|
||||
// bytes: vec![],
|
||||
// };
|
||||
// // The addend is also fake because we don't know yet if the `target_address` here is the exact
|
||||
// // start of the symbol or if it's in the middle of it.
|
||||
// addend = 0;
|
||||
// } else {
|
||||
// // But if the target symbol is in a different object (extern), then we simply copy the pool
|
||||
// // relocation's target. This is because it won't be possible to locate the actual symbol
|
||||
// // later on based only off of an offset without knowing the object or section it's in. And
|
||||
// // doing that for external symbols would also be unnecessary, because when the compiler
|
||||
// // generates an instruction that accesses an external "pool" plus some offset, that won't be
|
||||
// // a normal pool that contains other symbols within it that we want to display. It will be
|
||||
// // something like a vtable for a class with multiple inheritance (for example, dCcD_Cyl in
|
||||
// // The Wind Waker). So just showing that vtable symbol plus an addend to represent the
|
||||
// // offset into it works fine in this case, no fake symbol to hold an address is necessary.
|
||||
// target = pool_reloc.target.clone();
|
||||
// addend = pool_reloc.addend;
|
||||
// };
|
||||
// Some(ObjReloc {
|
||||
// flags: RelocationFlags::Elf { r_type: elf::R_PPC_NONE },
|
||||
// address: cur_addr as u64,
|
||||
// target,
|
||||
// addend,
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// // Searches through all instructions in a function, determining which registers have the addresses
|
||||
// // of pooled data relocations in them, finding which instructions load data from those addresses,
|
||||
// // and constructing a mapping of the address of that instruction to a "fake pool relocation" that
|
||||
// // simulates what that instruction's relocation would look like if data hadn't been pooled.
|
||||
// // This method tries to follow the function's proper control flow. It keeps track of a queue of
|
||||
// // states it hasn't traversed yet, where each state holds an instruction address and a HashMap of
|
||||
// // which registers hold which pool relocations at that point.
|
||||
// // When a conditional or unconditional branch is encountered, the destination of the branch is added
|
||||
// // to the queue. Conditional branches will traverse both the path where the branch is taken and the
|
||||
// // one where it's not. Unconditional branches only follow the branch, ignoring any code immediately
|
||||
// // after the branch instruction.
|
||||
// // Limitations: This method cannot read jump tables. This is because the jump tables are located in
|
||||
// // the .data section, but ObjArch.process_code only has access to the .text section. In order to
|
||||
// // work around this limitation and avoid completely missing most code inside switch statements that
|
||||
// // use jump tables, we instead guess that any parts of a function we missed were switch cases, and
|
||||
// // traverse them as if the last `bctr` before that address had branched there. This should be fairly
|
||||
// // accurate in practice - in testing the only instructions it seems to miss are double branches that
|
||||
// // the compiler generates in error which can never be reached during normal execution anyway.
|
||||
// fn generate_fake_pool_reloc_for_addr_mapping(
|
||||
// func_address: u64,
|
||||
// code: &[u8],
|
||||
// relocations: &[ObjReloc],
|
||||
// ) -> BTreeMap<u32, ObjReloc> {
|
||||
// let mut visited_ins_addrs = BTreeSet::new();
|
||||
// let mut pool_reloc_for_addr = BTreeMap::new();
|
||||
// let mut ins_iters_with_gpr_state =
|
||||
// vec![(InsIter::new(code, func_address as u32), BTreeMap::new())];
|
||||
// let mut gpr_state_at_bctr = BTreeMap::new();
|
||||
// while let Some((ins_iter, mut gpr_pool_relocs)) = ins_iters_with_gpr_state.pop() {
|
||||
// for (cur_addr, ins) in ins_iter {
|
||||
// if visited_ins_addrs.contains(&cur_addr) {
|
||||
// // Avoid getting stuck in an infinite loop when following looping branches.
|
||||
// break;
|
||||
// }
|
||||
// visited_ins_addrs.insert(cur_addr);
|
||||
//
|
||||
// let simplified = ins.simplified();
|
||||
//
|
||||
// // First handle traversing the function's control flow.
|
||||
// let mut branch_dest = None;
|
||||
// for arg in simplified.args_iter() {
|
||||
// if let Argument::BranchDest(dest) = arg {
|
||||
// let dest = cur_addr.wrapping_add_signed(dest.0);
|
||||
// branch_dest = Some(dest);
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// if let Some(branch_dest) = branch_dest {
|
||||
// if branch_dest >= func_address as u32
|
||||
// && (branch_dest - func_address as u32) < code.len() as u32
|
||||
// {
|
||||
// let dest_offset_into_func = branch_dest - func_address as u32;
|
||||
// let dest_code_slice = &code[dest_offset_into_func as usize..];
|
||||
// match ins.op {
|
||||
// Opcode::Bc => {
|
||||
// // Conditional branch.
|
||||
// // Add the branch destination to the queue to do later.
|
||||
// ins_iters_with_gpr_state.push((
|
||||
// InsIter::new(dest_code_slice, branch_dest),
|
||||
// gpr_pool_relocs.clone(),
|
||||
// ));
|
||||
// // Then continue on with the current iterator.
|
||||
// }
|
||||
// Opcode::B => {
|
||||
// if simplified.mnemonic != "bl" {
|
||||
// // Unconditional branch.
|
||||
// // Add the branch destination to the queue.
|
||||
// ins_iters_with_gpr_state.push((
|
||||
// InsIter::new(dest_code_slice, branch_dest),
|
||||
// gpr_pool_relocs.clone(),
|
||||
// ));
|
||||
// // Break out of the current iterator so we can do the newly added one.
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// _ => unreachable!(),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// if let Opcode::Bcctr = ins.op {
|
||||
// if simplified.mnemonic == "bctr" {
|
||||
// // Unconditional branch to count register.
|
||||
// // Likely a jump table.
|
||||
// gpr_state_at_bctr.insert(cur_addr, gpr_pool_relocs.clone());
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Then handle keeping track of which GPR contains which pool relocation.
|
||||
// let reloc = relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
|
||||
// if let Some(reloc) = reloc {
|
||||
// // This instruction has a real relocation, so it may be a pool load we want to keep
|
||||
// // track of.
|
||||
// let args = &simplified.args;
|
||||
// match (ins.op, args[0], args[1], args[2]) {
|
||||
// (
|
||||
// // `lis` + `addi`
|
||||
// Opcode::Addi,
|
||||
// Argument::GPR(addr_dst_gpr),
|
||||
// Argument::GPR(_addr_src_gpr),
|
||||
// Argument::Simm(_simm),
|
||||
// ) => {
|
||||
// gpr_pool_relocs.insert(addr_dst_gpr.0, reloc.clone());
|
||||
// }
|
||||
// (
|
||||
// // `lis` + `ori`
|
||||
// Opcode::Ori,
|
||||
// Argument::GPR(addr_dst_gpr),
|
||||
// Argument::GPR(_addr_src_gpr),
|
||||
// Argument::Uimm(_uimm),
|
||||
// ) => {
|
||||
// gpr_pool_relocs.insert(addr_dst_gpr.0, reloc.clone());
|
||||
// }
|
||||
// (Opcode::B, _, _, _) => {
|
||||
// if simplified.mnemonic == "bl" {
|
||||
// // When encountering a function call, clear any active pool relocations from
|
||||
// // the volatile registers (r0, r3-r12), but not the nonvolatile registers.
|
||||
// gpr_pool_relocs.remove(&0);
|
||||
// for gpr in 3..12 {
|
||||
// gpr_pool_relocs.remove(&gpr);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// _ => {
|
||||
// clear_overwritten_gprs(ins, &mut gpr_pool_relocs);
|
||||
// }
|
||||
// }
|
||||
// } else if let Some((offset, addr_src_gpr, addr_dst_gpr)) =
|
||||
// get_offset_and_addr_gpr_for_possible_pool_reference(ins.op, &simplified)
|
||||
// {
|
||||
// // This instruction doesn't have a real relocation, so it may be a reference to one of
|
||||
// // the already-loaded pools.
|
||||
// if let Some(pool_reloc) = gpr_pool_relocs.get(&addr_src_gpr.0) {
|
||||
// if let Some(fake_pool_reloc) =
|
||||
// make_fake_pool_reloc(offset, cur_addr, pool_reloc)
|
||||
// {
|
||||
// pool_reloc_for_addr.insert(cur_addr, fake_pool_reloc);
|
||||
// }
|
||||
// if let Some(addr_dst_gpr) = addr_dst_gpr {
|
||||
// // If the address of the pool relocation got copied into another register, we
|
||||
// // need to keep track of it in that register too as future instructions may
|
||||
// // reference the symbol indirectly via this new register, instead of the
|
||||
// // register the symbol's address was originally loaded into.
|
||||
// // For example, the start of the function might `lis` + `addi` the start of the
|
||||
// // ...data pool into r25, and then later the start of a loop will `addi` r25
|
||||
// // with the offset within the .data section of an array variable into r21.
|
||||
// // Then the body of the loop will `lwzx` one of the array elements from r21.
|
||||
// let mut new_reloc = pool_reloc.clone();
|
||||
// new_reloc.addend += offset as i64;
|
||||
// gpr_pool_relocs.insert(addr_dst_gpr.0, new_reloc);
|
||||
// } else {
|
||||
// clear_overwritten_gprs(ins, &mut gpr_pool_relocs);
|
||||
// }
|
||||
// } else {
|
||||
// clear_overwritten_gprs(ins, &mut gpr_pool_relocs);
|
||||
// }
|
||||
// } else {
|
||||
// clear_overwritten_gprs(ins, &mut gpr_pool_relocs);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Finally, if we're about to finish the outer loop and don't have any more control flow to
|
||||
// // follow, we check if there are any instruction addresses in this function that we missed.
|
||||
// // If so, and if there were any `bctr` instructions before those points in this function,
|
||||
// // then we try to traverse those missing spots as switch cases.
|
||||
// if ins_iters_with_gpr_state.is_empty() {
|
||||
// let unseen_addrs = (func_address as u32..func_address as u32 + code.len() as u32)
|
||||
// .step_by(4)
|
||||
// .filter(|addr| !visited_ins_addrs.contains(addr));
|
||||
// for unseen_addr in unseen_addrs {
|
||||
// let prev_bctr_gpr_state = gpr_state_at_bctr
|
||||
// .iter()
|
||||
// .filter(|(&addr, _)| addr < unseen_addr)
|
||||
// .min_by_key(|(&addr, _)| addr)
|
||||
// .map(|(_, gpr_state)| gpr_state);
|
||||
// if let Some(gpr_pool_relocs) = prev_bctr_gpr_state {
|
||||
// let dest_offset_into_func = unseen_addr - func_address as u32;
|
||||
// let dest_code_slice = &code[dest_offset_into_func as usize..];
|
||||
// ins_iters_with_gpr_state.push((
|
||||
// InsIter::new(dest_code_slice, unseen_addr),
|
||||
// gpr_pool_relocs.clone(),
|
||||
// ));
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// pool_reloc_for_addr
|
||||
// }
|
||||
517
objdiff-core/src/arch/x86.rs
Normal file
@@ -0,0 +1,517 @@
|
||||
use alloc::{boxed::Box, string::String, vec::Vec};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use iced_x86::{
|
||||
Decoder, DecoderOptions, DecoratorKind, FormatterOutput, FormatterTextKind, GasFormatter,
|
||||
Instruction, IntelFormatter, MasmFormatter, NasmFormatter, NumberKind, OpKind, Register,
|
||||
};
|
||||
use object::{pe, Endian as _, Object as _, ObjectSection as _};
|
||||
|
||||
use crate::{
|
||||
arch::Arch,
|
||||
diff::{display::InstructionPart, DiffObjConfig, X86Formatter},
|
||||
obj::{InstructionRef, RelocationFlags, ResolvedInstructionRef, ScannedInstruction},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ArchX86 {
|
||||
bits: u32,
|
||||
endianness: object::Endianness,
|
||||
}
|
||||
|
||||
impl ArchX86 {
|
||||
pub fn new(object: &object::File) -> Result<Self> {
|
||||
Ok(Self { bits: if object.is_64() { 64 } else { 32 }, endianness: object.endianness() })
|
||||
}
|
||||
|
||||
fn decoder<'a>(&self, code: &'a [u8], address: u64) -> Decoder<'a> {
|
||||
Decoder::with_ip(self.bits, code, address, DecoderOptions::NONE)
|
||||
}
|
||||
|
||||
fn formatter(&self, diff_config: &DiffObjConfig) -> Box<dyn iced_x86::Formatter> {
|
||||
let mut formatter: Box<dyn iced_x86::Formatter> = match diff_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(diff_config.space_between_args);
|
||||
formatter
|
||||
}
|
||||
}
|
||||
|
||||
impl Arch for ArchX86 {
|
||||
fn scan_instructions(
|
||||
&self,
|
||||
address: u64,
|
||||
code: &[u8],
|
||||
_section_index: usize,
|
||||
_diff_config: &DiffObjConfig,
|
||||
) -> Result<Vec<ScannedInstruction>> {
|
||||
let mut out = Vec::with_capacity(code.len() / 2);
|
||||
let mut decoder = self.decoder(code, address);
|
||||
let mut instruction = Instruction::default();
|
||||
while decoder.can_decode() {
|
||||
decoder.decode_out(&mut instruction);
|
||||
let branch_dest = match instruction.op0_kind() {
|
||||
OpKind::NearBranch16 => Some(instruction.near_branch16() as u64),
|
||||
OpKind::NearBranch32 => Some(instruction.near_branch32() as u64),
|
||||
OpKind::NearBranch64 => Some(instruction.near_branch64()),
|
||||
_ => None,
|
||||
};
|
||||
out.push(ScannedInstruction {
|
||||
ins_ref: InstructionRef {
|
||||
address: instruction.ip(),
|
||||
size: instruction.len() as u8,
|
||||
opcode: instruction.mnemonic() as u16,
|
||||
},
|
||||
branch_dest,
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn display_instruction(
|
||||
&self,
|
||||
resolved: ResolvedInstructionRef,
|
||||
diff_config: &DiffObjConfig,
|
||||
cb: &mut dyn FnMut(InstructionPart) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
let mut decoder = self.decoder(resolved.code, resolved.ins_ref.address);
|
||||
let mut formatter = self.formatter(diff_config);
|
||||
let mut instruction = Instruction::default();
|
||||
decoder.decode_out(&mut instruction);
|
||||
|
||||
// Determine where to insert relocation in instruction output.
|
||||
// We replace the immediate or displacement with a placeholder value since the formatter
|
||||
// doesn't provide enough information to know which number is the displacement inside a
|
||||
// memory operand.
|
||||
let mut reloc_replace = None;
|
||||
if let Some(reloc) = resolved.relocation {
|
||||
const PLACEHOLDER: u64 = 0x7BDE3E7D; // chosen by fair dice roll.
|
||||
// guaranteed to be random.
|
||||
let reloc_offset = reloc.relocation.address - resolved.ins_ref.address;
|
||||
let reloc_size = reloc_size(reloc.relocation.flags).unwrap_or(usize::MAX);
|
||||
let offsets = decoder.get_constant_offsets(&instruction);
|
||||
if reloc_offset == offsets.displacement_offset() as u64
|
||||
&& reloc_size == offsets.displacement_size()
|
||||
{
|
||||
instruction.set_memory_displacement64(PLACEHOLDER);
|
||||
// Formatter always writes the displacement as Int32
|
||||
reloc_replace = Some((OpKind::Memory, NumberKind::Int32, PLACEHOLDER));
|
||||
} else if reloc_offset == offsets.immediate_offset() as u64
|
||||
&& reloc_size == offsets.immediate_size()
|
||||
{
|
||||
let is_branch = matches!(
|
||||
instruction.op0_kind(),
|
||||
OpKind::NearBranch16 | OpKind::NearBranch32 | OpKind::NearBranch64
|
||||
);
|
||||
let op_kind = if is_branch {
|
||||
instruction.op0_kind()
|
||||
} else {
|
||||
match reloc_size {
|
||||
2 => OpKind::Immediate16,
|
||||
4 => OpKind::Immediate32,
|
||||
8 => OpKind::Immediate64,
|
||||
_ => OpKind::default(),
|
||||
}
|
||||
};
|
||||
let number_kind = match reloc_size {
|
||||
2 => NumberKind::UInt16,
|
||||
4 => NumberKind::UInt32,
|
||||
8 => NumberKind::UInt64,
|
||||
_ => NumberKind::default(),
|
||||
};
|
||||
if is_branch {
|
||||
instruction.set_near_branch64(PLACEHOLDER);
|
||||
} else {
|
||||
instruction.set_immediate32(PLACEHOLDER as u32);
|
||||
}
|
||||
reloc_replace = Some((op_kind, number_kind, PLACEHOLDER));
|
||||
}
|
||||
}
|
||||
|
||||
let mut output =
|
||||
InstructionFormatterOutput { cb, reloc_replace, error: None, skip_next: false };
|
||||
formatter.format(&instruction, &mut output);
|
||||
if let Some(error) = output.error.take() {
|
||||
return Err(error);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn implcit_addend(
|
||||
&self,
|
||||
_file: &object::File<'_>,
|
||||
section: &object::Section,
|
||||
address: u64,
|
||||
_relocation: &object::Relocation,
|
||||
flags: RelocationFlags,
|
||||
) -> Result<i64> {
|
||||
match flags {
|
||||
RelocationFlags::Coff(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 reloc_name(&self, flags: RelocationFlags) -> Option<&'static str> {
|
||||
match flags {
|
||||
RelocationFlags::Coff(typ) => match typ {
|
||||
pe::IMAGE_REL_I386_DIR32 => Some("IMAGE_REL_I386_DIR32"),
|
||||
pe::IMAGE_REL_I386_REL32 => Some("IMAGE_REL_I386_REL32"),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn data_reloc_size(&self, flags: RelocationFlags) -> usize { reloc_size(flags).unwrap_or(1) }
|
||||
}
|
||||
|
||||
fn reloc_size(flags: RelocationFlags) -> Option<usize> {
|
||||
match flags {
|
||||
RelocationFlags::Coff(typ) => match typ {
|
||||
pe::IMAGE_REL_I386_DIR16 | pe::IMAGE_REL_I386_REL16 => Some(2),
|
||||
pe::IMAGE_REL_I386_DIR32 | pe::IMAGE_REL_I386_REL32 => Some(4),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
struct InstructionFormatterOutput<'a> {
|
||||
cb: &'a mut dyn FnMut(InstructionPart<'_>) -> Result<()>,
|
||||
reloc_replace: Option<(OpKind, NumberKind, u64)>,
|
||||
error: Option<anyhow::Error>,
|
||||
skip_next: bool,
|
||||
}
|
||||
|
||||
impl InstructionFormatterOutput<'_> {
|
||||
fn push_signed(&mut self, mut value: i64) {
|
||||
if self.error.is_some() {
|
||||
return;
|
||||
}
|
||||
// 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 {
|
||||
value = value.wrapping_abs();
|
||||
}
|
||||
if let Err(e) = (self.cb)(InstructionPart::signed(value)) {
|
||||
self.error = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatterOutput for InstructionFormatterOutput<'_> {
|
||||
fn write(&mut self, text: &str, kind: FormatterTextKind) {
|
||||
if self.error.is_some() {
|
||||
return;
|
||||
}
|
||||
// Skip whitespace after the mnemonic
|
||||
if self.skip_next {
|
||||
self.skip_next = false;
|
||||
if kind == FormatterTextKind::Text && text == " " {
|
||||
return;
|
||||
}
|
||||
}
|
||||
match kind {
|
||||
FormatterTextKind::Text | FormatterTextKind::Punctuation => {
|
||||
if let Err(e) = (self.cb)(InstructionPart::basic(text)) {
|
||||
self.error = Some(e);
|
||||
}
|
||||
}
|
||||
FormatterTextKind::Prefix
|
||||
| FormatterTextKind::Keyword
|
||||
| FormatterTextKind::Operator => {
|
||||
if let Err(e) = (self.cb)(InstructionPart::opaque(text)) {
|
||||
self.error = Some(e);
|
||||
}
|
||||
}
|
||||
_ => self.error = Some(anyhow!("x86: Unsupported FormatterTextKind {:?}", kind)),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_mnemonic(&mut self, instruction: &Instruction, text: &str) {
|
||||
if self.error.is_some() {
|
||||
return;
|
||||
}
|
||||
if let Err(e) = (self.cb)(InstructionPart::opcode(text, instruction.mnemonic() as u16)) {
|
||||
self.error = Some(e);
|
||||
}
|
||||
// Skip whitespace after the mnemonic
|
||||
self.skip_next = true;
|
||||
}
|
||||
|
||||
fn write_number(
|
||||
&mut self,
|
||||
instruction: &Instruction,
|
||||
_operand: u32,
|
||||
instruction_operand: Option<u32>,
|
||||
_text: &str,
|
||||
value: u64,
|
||||
number_kind: NumberKind,
|
||||
kind: FormatterTextKind,
|
||||
) {
|
||||
if self.error.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let (Some(operand), Some((target_op_kind, target_number_kind, target_value))) =
|
||||
(instruction_operand, self.reloc_replace)
|
||||
{
|
||||
if instruction.op_kind(operand) == target_op_kind
|
||||
&& number_kind == target_number_kind
|
||||
&& value == target_value
|
||||
{
|
||||
if let Err(e) = (self.cb)(InstructionPart::reloc()) {
|
||||
self.error = Some(e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let FormatterTextKind::LabelAddress | FormatterTextKind::FunctionAddress = kind {
|
||||
if let Err(e) = (self.cb)(InstructionPart::branch_dest(value)) {
|
||||
self.error = Some(e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
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 => {
|
||||
if let Err(e) = (self.cb)(InstructionPart::unsigned(value)) {
|
||||
self.error = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_decorator(
|
||||
&mut self,
|
||||
_instruction: &Instruction,
|
||||
_operand: u32,
|
||||
_instruction_operand: Option<u32>,
|
||||
text: &str,
|
||||
_decorator: DecoratorKind,
|
||||
) {
|
||||
if self.error.is_some() {
|
||||
return;
|
||||
}
|
||||
if let Err(e) = (self.cb)(InstructionPart::basic(text)) {
|
||||
self.error = Some(e);
|
||||
}
|
||||
}
|
||||
|
||||
fn write_register(
|
||||
&mut self,
|
||||
_instruction: &Instruction,
|
||||
_operand: u32,
|
||||
_instruction_operand: Option<u32>,
|
||||
text: &str,
|
||||
_register: Register,
|
||||
) {
|
||||
if self.error.is_some() {
|
||||
return;
|
||||
}
|
||||
if let Err(e) = (self.cb)(InstructionPart::opaque(text)) {
|
||||
self.error = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::obj::{Relocation, ResolvedRelocation};
|
||||
|
||||
#[test]
|
||||
fn test_scan_instructions() {
|
||||
let arch = ArchX86 { bits: 32, endianness: object::Endianness::Little };
|
||||
let code = [
|
||||
0xc7, 0x85, 0x68, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x8b, 0x04, 0x85, 0x00,
|
||||
0x00, 0x00, 0x00,
|
||||
];
|
||||
let scanned = arch.scan_instructions(0, &code, 0, &DiffObjConfig::default()).unwrap();
|
||||
assert_eq!(scanned.len(), 2);
|
||||
assert_eq!(scanned[0].ins_ref.address, 0);
|
||||
assert_eq!(scanned[0].ins_ref.size, 10);
|
||||
assert_eq!(scanned[0].ins_ref.opcode, iced_x86::Mnemonic::Mov as u16);
|
||||
assert_eq!(scanned[0].branch_dest, None);
|
||||
assert_eq!(scanned[1].ins_ref.address, 10);
|
||||
assert_eq!(scanned[1].ins_ref.size, 7);
|
||||
assert_eq!(scanned[1].ins_ref.opcode, iced_x86::Mnemonic::Mov as u16);
|
||||
assert_eq!(scanned[1].branch_dest, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_instruction() {
|
||||
let arch = ArchX86 { bits: 32, endianness: object::Endianness::Little };
|
||||
let code = [0xc7, 0x85, 0x68, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00];
|
||||
let opcode = iced_x86::Mnemonic::Mov as u16;
|
||||
let mut parts = Vec::new();
|
||||
arch.display_instruction(
|
||||
ResolvedInstructionRef {
|
||||
ins_ref: InstructionRef { address: 0x1234, size: 10, opcode },
|
||||
code: &code,
|
||||
..Default::default()
|
||||
},
|
||||
&DiffObjConfig::default(),
|
||||
&mut |part| {
|
||||
parts.push(part.into_static());
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(parts, &[
|
||||
InstructionPart::opcode("mov", opcode),
|
||||
InstructionPart::opaque("dword"),
|
||||
InstructionPart::basic(" "),
|
||||
InstructionPart::opaque("ptr"),
|
||||
InstructionPart::basic(" "),
|
||||
InstructionPart::basic("["),
|
||||
InstructionPart::opaque("ebp"),
|
||||
InstructionPart::opaque("-"),
|
||||
InstructionPart::signed(152i64),
|
||||
InstructionPart::basic("]"),
|
||||
InstructionPart::basic(","),
|
||||
InstructionPart::basic(" "),
|
||||
InstructionPart::unsigned(0u64),
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_instruction_with_reloc_1() {
|
||||
let arch = ArchX86 { bits: 32, endianness: object::Endianness::Little };
|
||||
let code = [0xc7, 0x85, 0x68, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00];
|
||||
let opcode = iced_x86::Mnemonic::Mov as u16;
|
||||
let mut parts = Vec::new();
|
||||
arch.display_instruction(
|
||||
ResolvedInstructionRef {
|
||||
ins_ref: InstructionRef { address: 0x1234, size: 10, opcode },
|
||||
code: &code,
|
||||
relocation: Some(ResolvedRelocation {
|
||||
relocation: &Relocation {
|
||||
flags: RelocationFlags::Coff(pe::IMAGE_REL_I386_DIR32),
|
||||
address: 0x1234 + 6,
|
||||
target_symbol: 0,
|
||||
addend: 0,
|
||||
},
|
||||
symbol: &Default::default(),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
&DiffObjConfig::default(),
|
||||
&mut |part| {
|
||||
parts.push(part.into_static());
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(parts, &[
|
||||
InstructionPart::opcode("mov", opcode),
|
||||
InstructionPart::opaque("dword"),
|
||||
InstructionPart::basic(" "),
|
||||
InstructionPart::opaque("ptr"),
|
||||
InstructionPart::basic(" "),
|
||||
InstructionPart::basic("["),
|
||||
InstructionPart::opaque("ebp"),
|
||||
InstructionPart::opaque("-"),
|
||||
InstructionPart::signed(152i64),
|
||||
InstructionPart::basic("]"),
|
||||
InstructionPart::basic(","),
|
||||
InstructionPart::basic(" "),
|
||||
InstructionPart::reloc(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_instruction_with_reloc_2() {
|
||||
let arch = ArchX86 { bits: 32, endianness: object::Endianness::Little };
|
||||
let code = [0x8b, 0x04, 0x85, 0x00, 0x00, 0x00, 0x00];
|
||||
let opcode = iced_x86::Mnemonic::Mov as u16;
|
||||
let mut parts = Vec::new();
|
||||
arch.display_instruction(
|
||||
ResolvedInstructionRef {
|
||||
ins_ref: InstructionRef { address: 0x1234, size: 7, opcode },
|
||||
code: &code,
|
||||
relocation: Some(ResolvedRelocation {
|
||||
relocation: &Relocation {
|
||||
flags: RelocationFlags::Coff(pe::IMAGE_REL_I386_DIR32),
|
||||
address: 0x1234 + 3,
|
||||
target_symbol: 0,
|
||||
addend: 0,
|
||||
},
|
||||
symbol: &Default::default(),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
&DiffObjConfig::default(),
|
||||
&mut |part| {
|
||||
parts.push(part.into_static());
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(parts, &[
|
||||
InstructionPart::opcode("mov", opcode),
|
||||
InstructionPart::opaque("eax"),
|
||||
InstructionPart::basic(","),
|
||||
InstructionPart::basic(" "),
|
||||
InstructionPart::basic("["),
|
||||
InstructionPart::opaque("eax"),
|
||||
InstructionPart::opaque("*"),
|
||||
InstructionPart::signed(4),
|
||||
InstructionPart::opaque("+"),
|
||||
InstructionPart::reloc(),
|
||||
InstructionPart::basic("]"),
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_instruction_with_reloc_3() {
|
||||
let arch = ArchX86 { bits: 32, endianness: object::Endianness::Little };
|
||||
let code = [0xe8, 0x00, 0x00, 0x00, 0x00];
|
||||
let opcode = iced_x86::Mnemonic::Call as u16;
|
||||
let mut parts = Vec::new();
|
||||
arch.display_instruction(
|
||||
ResolvedInstructionRef {
|
||||
ins_ref: InstructionRef { address: 0x1234, size: 5, opcode },
|
||||
code: &code,
|
||||
relocation: Some(ResolvedRelocation {
|
||||
relocation: &Relocation {
|
||||
flags: RelocationFlags::Coff(pe::IMAGE_REL_I386_REL32),
|
||||
address: 0x1234 + 1,
|
||||
target_symbol: 0,
|
||||
addend: 0,
|
||||
},
|
||||
symbol: &Default::default(),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
&DiffObjConfig::default(),
|
||||
&mut |part| {
|
||||
parts.push(part.into_static());
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(parts, &[InstructionPart::opcode("call", opcode), InstructionPart::reloc()]);
|
||||
}
|
||||
}
|
||||
242
objdiff-core/src/bindings/diff.rs
Normal file
@@ -0,0 +1,242 @@
|
||||
#![allow(clippy::needless_lifetimes)] // Generated serde code
|
||||
|
||||
use crate::{diff, obj};
|
||||
|
||||
// Protobuf diff types
|
||||
include!(concat!(env!("OUT_DIR"), "/objdiff.diff.rs"));
|
||||
#[cfg(feature = "serde")]
|
||||
include!(concat!(env!("OUT_DIR"), "/objdiff.diff.serde.rs"));
|
||||
|
||||
impl DiffResult {
|
||||
pub fn new(
|
||||
_left: Option<(&obj::Object, &diff::ObjectDiff)>,
|
||||
_right: Option<(&obj::Object, &diff::ObjectDiff)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
// TODO
|
||||
// left: left.map(|(obj, diff)| ObjectDiff::new(obj, diff)),
|
||||
// right: right.map(|(obj, diff)| ObjectDiff::new(obj, diff)),
|
||||
left: None,
|
||||
right: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// impl ObjectDiff {
|
||||
// pub fn new(obj: &obj::Object, diff: &diff::ObjectDiff) -> Self {
|
||||
// Self {
|
||||
// sections: diff
|
||||
// .sections
|
||||
// .iter()
|
||||
// .enumerate()
|
||||
// .map(|(i, d)| SectionDiff::new(obj, i, d))
|
||||
// .collect(),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// impl SectionDiff {
|
||||
// pub fn new(obj: &obj::Object, section_index: usize, section_diff: &diff::SectionDiff) -> Self {
|
||||
// let section = &obj.sections[section_index];
|
||||
// let symbols = section_diff.symbols.iter().map(|d| SymbolDiff::new(obj, d)).collect();
|
||||
// let data = section_diff.data_diff.iter().map(|d| DataDiff::new(obj, d)).collect();
|
||||
// // TODO: section_diff.reloc_diff
|
||||
// Self {
|
||||
// name: section.name.to_string(),
|
||||
// kind: SectionKind::from(section.kind) as i32,
|
||||
// size: section.size,
|
||||
// address: section.address,
|
||||
// symbols,
|
||||
// data,
|
||||
// match_percent: section_diff.match_percent,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// impl From<obj::SectionKind> for SectionKind {
|
||||
// fn from(value: obj::SectionKind) -> Self {
|
||||
// match value {
|
||||
// obj::SectionKind::Code => SectionKind::SectionText,
|
||||
// obj::SectionKind::Data => SectionKind::SectionData,
|
||||
// obj::SectionKind::Bss => SectionKind::SectionBss,
|
||||
// // TODO common
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// impl SymbolDiff {
|
||||
// pub fn new(object: &obj::Object, symbol_diff: &diff::SymbolDiff) -> Self {
|
||||
// let symbol = object.symbols[symbol_diff.symbol_index];
|
||||
// let instructions = symbol_diff
|
||||
// .instruction_rows
|
||||
// .iter()
|
||||
// .map(|ins_diff| InstructionDiff::new(object, ins_diff))
|
||||
// .collect();
|
||||
// Self {
|
||||
// symbol: Some(Symbol::new(symbol)),
|
||||
// instructions,
|
||||
// match_percent: symbol_diff.match_percent,
|
||||
// target: symbol_diff.target_symbol.map(SymbolRef::from),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// impl DataDiff {
|
||||
// pub fn new(_object: &obj::Object, data_diff: &diff::DataDiff) -> 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::SymbolGlobal 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: &obj::Object, 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: &obj::Object, 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: &obj::Object, 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 }
|
||||
// }
|
||||
// }
|
||||
3
objdiff-core/src/bindings/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
#[cfg(feature = "any-arch")]
|
||||
pub mod diff;
|
||||
pub mod report;
|
||||
463
objdiff-core/src/bindings/report.rs
Normal file
@@ -0,0 +1,463 @@
|
||||
#![allow(clippy::needless_lifetimes)] // Generated serde code
|
||||
|
||||
use alloc::{
|
||||
string::{String, ToString},
|
||||
vec,
|
||||
vec::Vec,
|
||||
};
|
||||
use core::ops::AddAssign;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use prost::Message;
|
||||
|
||||
// Protobuf report types
|
||||
include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs"));
|
||||
#[cfg(feature = "serde")]
|
||||
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!("Empty data");
|
||||
}
|
||||
let report = if data[0] == b'{' {
|
||||
// Load as JSON
|
||||
#[cfg(feature = "serde")]
|
||||
{
|
||||
Self::from_json(data)?
|
||||
}
|
||||
#[cfg(not(feature = "serde"))]
|
||||
bail!("JSON report parsing requires the `serde` feature")
|
||||
} else {
|
||||
// Load as binary protobuf
|
||||
Self::decode(data).map_err(|e| anyhow::Error::msg(e.to_string()))?
|
||||
};
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
/// 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) => {
|
||||
use serde_json::error::Category;
|
||||
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)]
|
||||
#[cfg_attr(feature = "serde", derive(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)]
|
||||
#[cfg_attr(feature = "serde", derive(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,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
complete: Option<bool>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
module_name: Option<String>,
|
||||
#[cfg_attr(feature = "serde", 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)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
struct LegacyReportItem {
|
||||
name: String,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
demangled_name: Option<String>,
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
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)
|
||||
}
|
||||
}
|
||||
104
objdiff-core/src/build/mod.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
pub mod watcher;
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
use typed_path::{Utf8PlatformPathBuf, Utf8UnixPath};
|
||||
|
||||
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<Utf8PlatformPathBuf>,
|
||||
pub custom_make: Option<String>,
|
||||
pub custom_args: Option<Vec<String>>,
|
||||
#[allow(unused)]
|
||||
pub selected_wsl_distro: Option<String>,
|
||||
}
|
||||
|
||||
pub fn run_make(config: &BuildConfig, arg: &Utf8UnixPath) -> 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;
|
||||
|
||||
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) => Utf8UnixPath::new("/").join(new_cwd.with_unix_encoding()),
|
||||
Err(_) => cwd.with_unix_encoding(),
|
||||
};
|
||||
|
||||
command
|
||||
.arg("--cd")
|
||||
.arg(cwd.as_str())
|
||||
.arg("-d")
|
||||
.arg(distro)
|
||||
.arg("--")
|
||||
.arg(make)
|
||||
.args(make_args)
|
||||
.arg(arg.as_str());
|
||||
} else {
|
||||
command.current_dir(cwd).args(make_args).arg(arg.as_str());
|
||||
}
|
||||
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 }
|
||||
}
|
||||
75
objdiff-core/src/build/watcher.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
task::Waker,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use globset::GlobSet;
|
||||
use notify::RecursiveMode;
|
||||
use notify_debouncer_full::{new_debouncer_opt, DebounceEventResult};
|
||||
|
||||
pub type Watcher = notify_debouncer_full::Debouncer<
|
||||
notify::RecommendedWatcher,
|
||||
notify_debouncer_full::RecommendedCache,
|
||||
>;
|
||||
|
||||
pub struct WatcherState {
|
||||
pub config_path: Option<PathBuf>,
|
||||
pub left_obj_path: Option<PathBuf>,
|
||||
pub right_obj_path: Option<PathBuf>,
|
||||
pub patterns: GlobSet,
|
||||
}
|
||||
|
||||
pub fn create_watcher(
|
||||
modified: Arc<AtomicBool>,
|
||||
project_dir: &Path,
|
||||
patterns: GlobSet,
|
||||
waker: Waker,
|
||||
) -> notify::Result<Watcher> {
|
||||
let base_dir = fs::canonicalize(project_dir)?;
|
||||
let base_dir_clone = base_dir.clone();
|
||||
let timeout = Duration::from_millis(200);
|
||||
let config = notify::Config::default().with_poll_interval(Duration::from_secs(2));
|
||||
let mut debouncer = new_debouncer_opt(
|
||||
timeout,
|
||||
None,
|
||||
move |result: DebounceEventResult| match result {
|
||||
Ok(events) => {
|
||||
let mut any_match = false;
|
||||
for event in events.iter() {
|
||||
if !matches!(
|
||||
event.kind,
|
||||
notify::EventKind::Modify(..)
|
||||
| notify::EventKind::Create(..)
|
||||
| notify::EventKind::Remove(..)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
for path in &event.paths {
|
||||
let Ok(path) = path.strip_prefix(&base_dir_clone) else {
|
||||
continue;
|
||||
};
|
||||
if patterns.is_match(path) {
|
||||
// log::info!("File modified: {}", path.display());
|
||||
any_match = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if any_match {
|
||||
modified.store(true, Ordering::Relaxed);
|
||||
waker.wake_by_ref();
|
||||
}
|
||||
}
|
||||
Err(errors) => errors.iter().for_each(|e| log::error!("Watch error: {e:?}")),
|
||||
},
|
||||
notify_debouncer_full::RecommendedCache::new(),
|
||||
config,
|
||||
)?;
|
||||
debouncer.watch(base_dir, RecursiveMode::Recursive)?;
|
||||
Ok(debouncer)
|
||||
}
|
||||
292
objdiff-core/src/config/mod.rs
Normal file
@@ -0,0 +1,292 @@
|
||||
pub mod path;
|
||||
|
||||
use alloc::{
|
||||
collections::BTreeMap,
|
||||
string::{String, ToString},
|
||||
vec::Vec,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||
use path::unix_path_serde_option;
|
||||
use typed_path::Utf8UnixPathBuf;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
|
||||
pub struct ProjectConfig {
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub min_version: Option<String>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub custom_make: Option<String>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub custom_args: Option<Vec<String>>,
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
|
||||
)]
|
||||
pub target_dir: Option<Utf8UnixPathBuf>,
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
|
||||
)]
|
||||
pub base_dir: Option<Utf8UnixPathBuf>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub build_base: Option<bool>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub build_target: Option<bool>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub watch_patterns: Option<Vec<String>>,
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(alias = "objects", skip_serializing_if = "Option::is_none")
|
||||
)]
|
||||
pub units: Option<Vec<ProjectObject>>,
|
||||
#[cfg_attr(feature = "serde", serde(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 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)
|
||||
}
|
||||
|
||||
pub fn build_watch_patterns(&self) -> Result<Vec<Glob>, globset::Error> {
|
||||
Ok(if let Some(watch_patterns) = &self.watch_patterns {
|
||||
watch_patterns
|
||||
.iter()
|
||||
.map(|s| Glob::new(s))
|
||||
.collect::<Result<Vec<Glob>, globset::Error>>()?
|
||||
} else {
|
||||
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
|
||||
pub struct ProjectObject {
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub name: Option<String>,
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
|
||||
)]
|
||||
pub path: Option<Utf8UnixPathBuf>,
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
|
||||
)]
|
||||
pub target_path: Option<Utf8UnixPathBuf>,
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
|
||||
)]
|
||||
pub base_path: Option<Utf8UnixPathBuf>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
#[deprecated(note = "Use metadata.reverse_fn_order")]
|
||||
pub reverse_fn_order: Option<bool>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
#[deprecated(note = "Use metadata.complete")]
|
||||
pub complete: Option<bool>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub scratch: Option<ScratchConfig>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub metadata: Option<ProjectObjectMetadata>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub symbol_mappings: Option<BTreeMap<String, String>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
|
||||
pub struct ProjectObjectMetadata {
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub complete: Option<bool>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub reverse_fn_order: Option<bool>,
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
|
||||
)]
|
||||
pub source_path: Option<Utf8UnixPathBuf>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub progress_categories: Option<Vec<String>>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub auto_generated: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
|
||||
pub struct ProjectProgressCategory {
|
||||
pub id: String,
|
||||
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.as_str()
|
||||
} else {
|
||||
"[unknown]"
|
||||
}
|
||||
}
|
||||
|
||||
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<&Utf8UnixPathBuf> {
|
||||
self.metadata.as_ref().and_then(|m| m.source_path.as_ref())
|
||||
}
|
||||
|
||||
pub fn progress_categories(&self) -> &[String] {
|
||||
self.metadata.as_ref().and_then(|m| m.progress_categories.as_deref()).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn auto_generated(&self) -> Option<bool> {
|
||||
self.metadata.as_ref().and_then(|m| m.auto_generated)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize), serde(default))]
|
||||
pub struct ScratchConfig {
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub platform: Option<String>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub compiler: Option<String>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub c_flags: Option<String>,
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
|
||||
)]
|
||||
pub ctx_path: Option<Utf8UnixPathBuf>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub build_ctx: Option<bool>,
|
||||
#[cfg_attr(feature = "serde", serde(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",
|
||||
];
|
||||
|
||||
pub fn default_watch_patterns() -> Vec<Glob> {
|
||||
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct ProjectConfigInfo {
|
||||
pub path: std::path::PathBuf,
|
||||
pub timestamp: Option<filetime::FileTime>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub fn try_project_config(
|
||||
dir: &std::path::Path,
|
||||
) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
|
||||
for filename in CONFIG_FILENAMES.iter() {
|
||||
let config_path = dir.join(filename);
|
||||
let Ok(file) = std::fs::File::open(&config_path) else {
|
||||
continue;
|
||||
};
|
||||
let metadata = file.metadata();
|
||||
if let Ok(metadata) = metadata {
|
||||
if !metadata.is_file() {
|
||||
continue;
|
||||
}
|
||||
let ts = filetime::FileTime::from_last_modification_time(&metadata);
|
||||
let mut reader = std::io::BufReader::new(file);
|
||||
let mut result = read_json_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
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
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) = std::fs::metadata(&info.path) {
|
||||
let ts = filetime::FileTime::from_last_modification_time(&metadata);
|
||||
if ts != last_ts {
|
||||
return Err(anyhow!("Config file has changed since last read"));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut writer = std::io::BufWriter::new(
|
||||
std::fs::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"),
|
||||
_ => 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::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"))
|
||||
.map_err(|e| anyhow::Error::msg(e.to_string()))
|
||||
.context("Failed to parse package version")?;
|
||||
let min_version = semver::Version::parse(min_version)
|
||||
.map_err(|e| anyhow::Error::msg(e.to_string()))
|
||||
.context("Failed to parse min_version")?;
|
||||
if version >= min_version {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("Project requires objdiff version {min_version} or higher"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
fn read_json_config<R: std::io::Read>(reader: &mut R) -> Result<ProjectConfig> {
|
||||
Ok(serde_json::from_reader(reader)?)
|
||||
}
|
||||
|
||||
pub fn build_globset(vec: &[Glob]) -> Result<GlobSet, globset::Error> {
|
||||
let mut builder = GlobSetBuilder::new();
|
||||
for glob in vec {
|
||||
builder.add(glob.clone());
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
65
objdiff-core/src/config/path.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
// For argp::FromArgs
|
||||
#[cfg(feature = "std")]
|
||||
pub fn platform_path(value: &str) -> Result<typed_path::Utf8PlatformPathBuf, String> {
|
||||
Ok(typed_path::Utf8PlatformPathBuf::from(value))
|
||||
}
|
||||
|
||||
/// Checks if the path is valid UTF-8 and returns it as a [`Utf8PlatformPath`].
|
||||
#[cfg(feature = "std")]
|
||||
pub fn check_path(
|
||||
path: &std::path::Path,
|
||||
) -> Result<&typed_path::Utf8PlatformPath, core::str::Utf8Error> {
|
||||
typed_path::Utf8PlatformPath::from_bytes_path(typed_path::PlatformPath::new(
|
||||
path.as_os_str().as_encoded_bytes(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Checks if the path is valid UTF-8 and returns it as a [`Utf8NativePathBuf`].
|
||||
#[cfg(feature = "std")]
|
||||
pub fn check_path_buf(
|
||||
path: std::path::PathBuf,
|
||||
) -> Result<typed_path::Utf8PlatformPathBuf, alloc::string::FromUtf8Error> {
|
||||
typed_path::Utf8PlatformPathBuf::from_bytes_path_buf(typed_path::PlatformPathBuf::from(
|
||||
path.into_os_string().into_encoded_bytes(),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
pub mod unix_path_serde_option {
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
use typed_path::Utf8UnixPathBuf;
|
||||
|
||||
pub fn serialize<S>(path: &Option<Utf8UnixPathBuf>, s: S) -> Result<S::Ok, S::Error>
|
||||
where S: Serializer {
|
||||
if let Some(path) = path {
|
||||
s.serialize_some(path.as_str())
|
||||
} else {
|
||||
s.serialize_none()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Utf8UnixPathBuf>, D::Error>
|
||||
where D: Deserializer<'de> {
|
||||
Ok(Option::<String>::deserialize(deserializer)?.map(Utf8UnixPathBuf::from))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "serde", feature = "std"))]
|
||||
pub mod platform_path_serde_option {
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
use typed_path::Utf8PlatformPathBuf;
|
||||
|
||||
pub fn serialize<S>(path: &Option<Utf8PlatformPathBuf>, s: S) -> Result<S::Ok, S::Error>
|
||||
where S: Serializer {
|
||||
if let Some(path) = path {
|
||||
s.serialize_some(path.as_str())
|
||||
} else {
|
||||
s.serialize_none()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Utf8PlatformPathBuf>, D::Error>
|
||||
where D: Deserializer<'de> {
|
||||
Ok(Option::<String>::deserialize(deserializer)?.map(Utf8PlatformPathBuf::from))
|
||||
}
|
||||
}
|
||||
518
objdiff-core/src/diff/code.rs
Normal file
@@ -0,0 +1,518 @@
|
||||
use alloc::{
|
||||
collections::{btree_map, BTreeMap},
|
||||
string::{String, ToString},
|
||||
vec,
|
||||
vec::Vec,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, ensure, Context, Result};
|
||||
|
||||
use super::{
|
||||
display::display_ins_data_literals, DiffObjConfig, FunctionRelocDiffs, InstructionArgDiffIndex,
|
||||
InstructionBranchFrom, InstructionBranchTo, InstructionDiffKind, InstructionDiffRow,
|
||||
SymbolDiff,
|
||||
};
|
||||
use crate::obj::{
|
||||
InstructionArg, InstructionArgValue, InstructionRef, Object, ResolvedInstructionRef,
|
||||
ResolvedRelocation, ScannedInstruction, SymbolFlag, SymbolKind,
|
||||
};
|
||||
|
||||
pub fn no_diff_code(
|
||||
obj: &Object,
|
||||
symbol_idx: usize,
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> Result<SymbolDiff> {
|
||||
let symbol = &obj.symbols[symbol_idx];
|
||||
let section_index = symbol.section.ok_or_else(|| anyhow!("Missing section for symbol"))?;
|
||||
let section = &obj.sections[section_index];
|
||||
let data = section.data_range(symbol.address, symbol.size as usize).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"Symbol data out of bounds: {:#x}..{:#x}",
|
||||
symbol.address,
|
||||
symbol.address + symbol.size
|
||||
)
|
||||
})?;
|
||||
let ops = obj.arch.scan_instructions(symbol.address, data, section_index, diff_config)?;
|
||||
let mut instruction_rows = Vec::<InstructionDiffRow>::new();
|
||||
for i in &ops {
|
||||
instruction_rows
|
||||
.push(InstructionDiffRow { ins_ref: Some(i.ins_ref), ..Default::default() });
|
||||
}
|
||||
resolve_branches(obj, section_index, &ops, &mut instruction_rows);
|
||||
Ok(SymbolDiff { target_symbol: None, match_percent: None, diff_score: None, instruction_rows })
|
||||
}
|
||||
|
||||
const PENALTY_IMM_DIFF: u64 = 1;
|
||||
const PENALTY_REG_DIFF: u64 = 5;
|
||||
const PENALTY_REPLACE: u64 = 60;
|
||||
const PENALTY_INSERT_DELETE: u64 = 100;
|
||||
|
||||
pub fn diff_code(
|
||||
left_obj: &Object,
|
||||
right_obj: &Object,
|
||||
left_symbol_idx: usize,
|
||||
right_symbol_idx: usize,
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> Result<(SymbolDiff, SymbolDiff)> {
|
||||
let left_symbol = &left_obj.symbols[left_symbol_idx];
|
||||
let right_symbol = &right_obj.symbols[right_symbol_idx];
|
||||
let left_section = left_symbol
|
||||
.section
|
||||
.and_then(|i| left_obj.sections.get(i))
|
||||
.ok_or_else(|| anyhow!("Missing section for symbol"))?;
|
||||
let right_section = right_symbol
|
||||
.section
|
||||
.and_then(|i| right_obj.sections.get(i))
|
||||
.ok_or_else(|| anyhow!("Missing section for symbol"))?;
|
||||
let left_data = left_section
|
||||
.data_range(left_symbol.address, left_symbol.size as usize)
|
||||
.ok_or_else(|| {
|
||||
anyhow!(
|
||||
"Symbol data out of bounds: {:#x}..{:#x}",
|
||||
left_symbol.address,
|
||||
left_symbol.address + left_symbol.size
|
||||
)
|
||||
})?;
|
||||
let right_data = right_section
|
||||
.data_range(right_symbol.address, right_symbol.size as usize)
|
||||
.ok_or_else(|| {
|
||||
anyhow!(
|
||||
"Symbol data out of bounds: {:#x}..{:#x}",
|
||||
right_symbol.address,
|
||||
right_symbol.address + right_symbol.size
|
||||
)
|
||||
})?;
|
||||
|
||||
let left_section_idx = left_symbol.section.unwrap();
|
||||
let right_section_idx = right_symbol.section.unwrap();
|
||||
let left_ops = left_obj.arch.scan_instructions(
|
||||
left_symbol.address,
|
||||
left_data,
|
||||
left_section_idx,
|
||||
diff_config,
|
||||
)?;
|
||||
let right_ops = left_obj.arch.scan_instructions(
|
||||
right_symbol.address,
|
||||
right_data,
|
||||
right_section_idx,
|
||||
diff_config,
|
||||
)?;
|
||||
let (mut left_rows, mut right_rows) = diff_instructions(&left_ops, &right_ops)?;
|
||||
resolve_branches(left_obj, left_section_idx, &left_ops, &mut left_rows);
|
||||
resolve_branches(right_obj, right_section_idx, &right_ops, &mut right_rows);
|
||||
|
||||
let mut diff_state = InstructionDiffState::default();
|
||||
for (left_row, right_row) in left_rows.iter_mut().zip(right_rows.iter_mut()) {
|
||||
let result = diff_instruction(
|
||||
left_obj,
|
||||
right_obj,
|
||||
left_symbol_idx,
|
||||
right_symbol_idx,
|
||||
left_row.ins_ref,
|
||||
right_row.ins_ref,
|
||||
left_row,
|
||||
right_row,
|
||||
diff_config,
|
||||
&mut diff_state,
|
||||
)?;
|
||||
left_row.kind = result.kind;
|
||||
right_row.kind = result.kind;
|
||||
left_row.arg_diff = result.left_args_diff;
|
||||
right_row.arg_diff = result.right_args_diff;
|
||||
}
|
||||
|
||||
let max_score = left_ops.len() as u64 * PENALTY_INSERT_DELETE;
|
||||
let diff_score = diff_state.diff_score.min(max_score);
|
||||
let match_percent = if max_score == 0 {
|
||||
100.0
|
||||
} else {
|
||||
((1.0 - (diff_score as f64 / max_score as f64)) * 100.0) as f32
|
||||
};
|
||||
|
||||
Ok((
|
||||
SymbolDiff {
|
||||
target_symbol: Some(right_symbol_idx),
|
||||
match_percent: Some(match_percent),
|
||||
diff_score: Some((diff_score, max_score)),
|
||||
instruction_rows: left_rows,
|
||||
},
|
||||
SymbolDiff {
|
||||
target_symbol: Some(left_symbol_idx),
|
||||
match_percent: Some(match_percent),
|
||||
diff_score: Some((diff_score, max_score)),
|
||||
instruction_rows: right_rows,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn diff_instructions(
|
||||
left_insts: &[ScannedInstruction],
|
||||
right_insts: &[ScannedInstruction],
|
||||
) -> Result<(Vec<InstructionDiffRow>, Vec<InstructionDiffRow>)> {
|
||||
let left_ops = left_insts.iter().map(|i| i.ins_ref.opcode).collect::<Vec<_>>();
|
||||
let right_ops = right_insts.iter().map(|i| i.ins_ref.opcode).collect::<Vec<_>>();
|
||||
let ops = similar::capture_diff_slices(similar::Algorithm::Patience, &left_ops, &right_ops);
|
||||
if ops.is_empty() {
|
||||
ensure!(left_insts.len() == right_insts.len());
|
||||
let left_diff = left_insts
|
||||
.iter()
|
||||
.map(|i| InstructionDiffRow { ins_ref: Some(i.ins_ref), ..Default::default() })
|
||||
.collect();
|
||||
let right_diff = right_insts
|
||||
.iter()
|
||||
.map(|i| InstructionDiffRow { ins_ref: Some(i.ins_ref), ..Default::default() })
|
||||
.collect();
|
||||
return Ok((left_diff, right_diff));
|
||||
}
|
||||
|
||||
let row_count = ops
|
||||
.iter()
|
||||
.map(|op| match *op {
|
||||
similar::DiffOp::Equal { len, .. } => len,
|
||||
similar::DiffOp::Delete { old_len, .. } => old_len,
|
||||
similar::DiffOp::Insert { new_len, .. } => new_len,
|
||||
similar::DiffOp::Replace { old_len, new_len, .. } => old_len.max(new_len),
|
||||
})
|
||||
.sum();
|
||||
let mut left_diff = Vec::<InstructionDiffRow>::with_capacity(row_count);
|
||||
let mut right_diff = Vec::<InstructionDiffRow>::with_capacity(row_count);
|
||||
for op in ops {
|
||||
let (_tag, left_range, right_range) = op.as_tag_tuple();
|
||||
let len = left_range.len().max(right_range.len());
|
||||
left_diff.extend(left_range.clone().map(|i| InstructionDiffRow {
|
||||
ins_ref: Some(left_insts[i].ins_ref),
|
||||
..Default::default()
|
||||
}));
|
||||
right_diff.extend(right_range.clone().map(|i| InstructionDiffRow {
|
||||
ins_ref: Some(right_insts[i].ins_ref),
|
||||
..Default::default()
|
||||
}));
|
||||
if left_range.len() < len {
|
||||
left_diff.extend((left_range.len()..len).map(|_| InstructionDiffRow::default()));
|
||||
}
|
||||
if right_range.len() < len {
|
||||
right_diff.extend((right_range.len()..len).map(|_| InstructionDiffRow::default()));
|
||||
}
|
||||
}
|
||||
Ok((left_diff, right_diff))
|
||||
}
|
||||
|
||||
fn arg_to_string(arg: &InstructionArg, reloc: Option<ResolvedRelocation>) -> String {
|
||||
match arg {
|
||||
InstructionArg::Value(arg) => arg.to_string(),
|
||||
InstructionArg::Reloc => {
|
||||
reloc.as_ref().map_or_else(|| "<unknown>".to_string(), |r| r.symbol.name.clone())
|
||||
}
|
||||
InstructionArg::BranchDest(arg) => arg.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_branches(
|
||||
obj: &Object,
|
||||
section_index: usize,
|
||||
ops: &[ScannedInstruction],
|
||||
rows: &mut [InstructionDiffRow],
|
||||
) {
|
||||
let section = &obj.sections[section_index];
|
||||
let mut branch_idx = 0u32;
|
||||
// Map addresses to indices
|
||||
let mut addr_map = BTreeMap::<u64, u32>::new();
|
||||
for (i, ins_diff) in rows.iter().enumerate() {
|
||||
if let Some(ins) = ins_diff.ins_ref {
|
||||
addr_map.insert(ins.address, i as u32);
|
||||
}
|
||||
}
|
||||
// Generate branches
|
||||
let mut branches = BTreeMap::<u32, InstructionBranchFrom>::new();
|
||||
for ((i, ins_diff), ins) in
|
||||
rows.iter_mut().enumerate().filter(|(_, row)| row.ins_ref.is_some()).zip(ops)
|
||||
{
|
||||
let branch_dest = if let Some(resolved) = section.relocation_at(obj, ins.ins_ref) {
|
||||
if resolved.symbol.section == Some(section_index) {
|
||||
// If the relocation target is in the same section, use it as the branch destination
|
||||
resolved.symbol.address.checked_add_signed(resolved.relocation.addend)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
ins.branch_dest
|
||||
};
|
||||
if let Some(ins_idx) = branch_dest.and_then(|a| addr_map.get(&a).copied()) {
|
||||
match branches.entry(ins_idx) {
|
||||
btree_map::Entry::Vacant(e) => {
|
||||
ins_diff.branch_to = Some(InstructionBranchTo { ins_idx, branch_idx });
|
||||
e.insert(InstructionBranchFrom { ins_idx: vec![i as u32], branch_idx });
|
||||
branch_idx += 1;
|
||||
}
|
||||
btree_map::Entry::Occupied(e) => {
|
||||
let branch = e.into_mut();
|
||||
ins_diff.branch_to =
|
||||
Some(InstructionBranchTo { ins_idx, branch_idx: branch.branch_idx });
|
||||
branch.ins_idx.push(i as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Store branch from
|
||||
for (i, branch) in branches {
|
||||
rows[i as usize].branch_from = Some(branch);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn address_eq(left: ResolvedRelocation, right: ResolvedRelocation) -> bool {
|
||||
if right.symbol.size == 0 && left.symbol.size != 0 {
|
||||
// The base relocation is against a pool but the target relocation isn't.
|
||||
// This can happen in rare cases where the compiler will generate a pool+addend relocation
|
||||
// in the base's data, but the one detected in the target is direct with no addend.
|
||||
// Just check that the final address is the same so these count as a match.
|
||||
left.symbol.address as i64 + left.relocation.addend
|
||||
== right.symbol.address as i64 + right.relocation.addend
|
||||
} else {
|
||||
// But otherwise, if the compiler isn't using a pool, we're more strict and check that the
|
||||
// target symbol address and relocation addend both match exactly.
|
||||
left.symbol.address == right.symbol.address
|
||||
&& left.relocation.addend == right.relocation.addend
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn section_name_eq(
|
||||
left_obj: &Object,
|
||||
right_obj: &Object,
|
||||
left_section_index: usize,
|
||||
right_section_index: usize,
|
||||
) -> bool {
|
||||
left_obj.sections.get(left_section_index).is_some_and(|left_section| {
|
||||
right_obj
|
||||
.sections
|
||||
.get(right_section_index)
|
||||
.is_some_and(|right_section| left_section.name == right_section.name)
|
||||
})
|
||||
}
|
||||
|
||||
fn reloc_eq(
|
||||
left_obj: &Object,
|
||||
right_obj: &Object,
|
||||
left_ins: ResolvedInstructionRef,
|
||||
right_ins: ResolvedInstructionRef,
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> bool {
|
||||
let relax_reloc_diffs = diff_config.function_reloc_diffs == FunctionRelocDiffs::None;
|
||||
let (left_reloc, right_reloc) = match (left_ins.relocation, right_ins.relocation) {
|
||||
(Some(left_reloc), Some(right_reloc)) => (left_reloc, right_reloc),
|
||||
// If relocations are relaxed, match if left is missing a reloc
|
||||
(None, Some(_)) => return relax_reloc_diffs,
|
||||
(None, None) => return true,
|
||||
_ => return false,
|
||||
};
|
||||
if left_reloc.relocation.flags != right_reloc.relocation.flags {
|
||||
return false;
|
||||
}
|
||||
if relax_reloc_diffs {
|
||||
return true;
|
||||
}
|
||||
|
||||
let symbol_name_addend_matches = left_reloc.symbol.name == right_reloc.symbol.name
|
||||
&& left_reloc.relocation.addend == right_reloc.relocation.addend;
|
||||
match (&left_reloc.symbol.section, &right_reloc.symbol.section) {
|
||||
(Some(sl), Some(sr)) => {
|
||||
// Match if section and name or address match
|
||||
section_name_eq(left_obj, right_obj, *sl, *sr)
|
||||
&& (diff_config.function_reloc_diffs == FunctionRelocDiffs::DataValue
|
||||
|| symbol_name_addend_matches
|
||||
|| address_eq(left_reloc, right_reloc))
|
||||
&& (diff_config.function_reloc_diffs == FunctionRelocDiffs::NameAddress
|
||||
|| left_reloc.symbol.kind != SymbolKind::Object
|
||||
|| display_ins_data_literals(left_obj, left_ins)
|
||||
== display_ins_data_literals(right_obj, right_ins))
|
||||
}
|
||||
(Some(_), None) => false,
|
||||
(None, Some(_)) => {
|
||||
// Match if possibly stripped weak symbol
|
||||
symbol_name_addend_matches && right_reloc.symbol.flags.contains(SymbolFlag::Weak)
|
||||
}
|
||||
(None, None) => symbol_name_addend_matches,
|
||||
}
|
||||
}
|
||||
|
||||
fn arg_eq(
|
||||
left_obj: &Object,
|
||||
right_obj: &Object,
|
||||
left_row: &InstructionDiffRow,
|
||||
right_row: &InstructionDiffRow,
|
||||
left_arg: &InstructionArg,
|
||||
right_arg: &InstructionArg,
|
||||
left_ins: ResolvedInstructionRef,
|
||||
right_ins: ResolvedInstructionRef,
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> bool {
|
||||
match left_arg {
|
||||
InstructionArg::Value(l) => match right_arg {
|
||||
InstructionArg::Value(r) => l.loose_eq(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
|
||||
InstructionArg::Reloc => diff_config.function_reloc_diffs == FunctionRelocDiffs::None,
|
||||
_ => false,
|
||||
},
|
||||
InstructionArg::Reloc => {
|
||||
matches!(right_arg, InstructionArg::Reloc)
|
||||
&& reloc_eq(left_obj, right_obj, left_ins, right_ins, diff_config)
|
||||
}
|
||||
InstructionArg::BranchDest(_) => match right_arg {
|
||||
// Compare dest instruction idx after diffing
|
||||
InstructionArg::BranchDest(_) => {
|
||||
left_row.branch_to.as_ref().map(|b| b.ins_idx)
|
||||
== right_row.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
|
||||
InstructionArg::Reloc => diff_config.function_reloc_diffs == FunctionRelocDiffs::None,
|
||||
_ => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct InstructionDiffState {
|
||||
diff_score: u64,
|
||||
left_arg_idx: u32,
|
||||
right_arg_idx: u32,
|
||||
left_args_idx: BTreeMap<String, u32>,
|
||||
right_args_idx: BTreeMap<String, u32>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct InstructionDiffResult {
|
||||
kind: InstructionDiffKind,
|
||||
left_args_diff: Vec<InstructionArgDiffIndex>,
|
||||
right_args_diff: Vec<InstructionArgDiffIndex>,
|
||||
}
|
||||
|
||||
impl InstructionDiffResult {
|
||||
#[inline]
|
||||
const fn new(kind: InstructionDiffKind) -> Self {
|
||||
Self { kind, left_args_diff: Vec::new(), right_args_diff: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
fn diff_instruction(
|
||||
left_obj: &Object,
|
||||
right_obj: &Object,
|
||||
left_symbol_idx: usize,
|
||||
right_symbol_idx: usize,
|
||||
l: Option<InstructionRef>,
|
||||
r: Option<InstructionRef>,
|
||||
left_row: &InstructionDiffRow,
|
||||
right_row: &InstructionDiffRow,
|
||||
diff_config: &DiffObjConfig,
|
||||
state: &mut InstructionDiffState,
|
||||
) -> Result<InstructionDiffResult> {
|
||||
let (l, r) = match (l, r) {
|
||||
(Some(l), Some(r)) => (l, r),
|
||||
(Some(_), None) => {
|
||||
state.diff_score += PENALTY_INSERT_DELETE;
|
||||
return Ok(InstructionDiffResult::new(InstructionDiffKind::Delete));
|
||||
}
|
||||
(None, Some(_)) => {
|
||||
state.diff_score += PENALTY_INSERT_DELETE;
|
||||
return Ok(InstructionDiffResult::new(InstructionDiffKind::Insert));
|
||||
}
|
||||
(None, None) => return Ok(InstructionDiffResult::new(InstructionDiffKind::None)),
|
||||
};
|
||||
|
||||
// If opcodes don't match, replace
|
||||
if l.opcode != r.opcode {
|
||||
state.diff_score += PENALTY_REPLACE;
|
||||
return Ok(InstructionDiffResult::new(InstructionDiffKind::Replace));
|
||||
}
|
||||
|
||||
let left_resolved = left_obj
|
||||
.resolve_instruction_ref(left_symbol_idx, l)
|
||||
.context("Failed to resolve left instruction")?;
|
||||
let right_resolved = right_obj
|
||||
.resolve_instruction_ref(right_symbol_idx, r)
|
||||
.context("Failed to resolve right instruction")?;
|
||||
|
||||
if left_resolved.code != right_resolved.code
|
||||
|| !reloc_eq(left_obj, right_obj, left_resolved, right_resolved, diff_config)
|
||||
{
|
||||
// If either the raw code bytes or relocations don't match, process instructions and compare args
|
||||
let left_ins = left_obj.arch.process_instruction(left_resolved, diff_config)?;
|
||||
let right_ins = left_obj.arch.process_instruction(right_resolved, diff_config)?;
|
||||
if left_ins.args.len() != right_ins.args.len() {
|
||||
state.diff_score += PENALTY_REPLACE;
|
||||
return Ok(InstructionDiffResult::new(InstructionDiffKind::Replace));
|
||||
}
|
||||
let mut result = InstructionDiffResult::new(InstructionDiffKind::None);
|
||||
if left_ins.mnemonic != right_ins.mnemonic {
|
||||
state.diff_score += PENALTY_REG_DIFF;
|
||||
result.kind = InstructionDiffKind::OpMismatch;
|
||||
}
|
||||
for (a, b) in left_ins.args.iter().zip(right_ins.args.iter()) {
|
||||
if arg_eq(
|
||||
left_obj,
|
||||
right_obj,
|
||||
left_row,
|
||||
right_row,
|
||||
a,
|
||||
b,
|
||||
left_resolved,
|
||||
right_resolved,
|
||||
diff_config,
|
||||
) {
|
||||
result.left_args_diff.push(InstructionArgDiffIndex::NONE);
|
||||
result.right_args_diff.push(InstructionArgDiffIndex::NONE);
|
||||
} else {
|
||||
state.diff_score += if let InstructionArg::Value(
|
||||
InstructionArgValue::Signed(_) | InstructionArgValue::Unsigned(_),
|
||||
) = a
|
||||
{
|
||||
PENALTY_IMM_DIFF
|
||||
} else {
|
||||
PENALTY_REG_DIFF
|
||||
};
|
||||
if result.kind == InstructionDiffKind::None {
|
||||
result.kind = InstructionDiffKind::ArgMismatch;
|
||||
}
|
||||
let a_str = arg_to_string(a, left_resolved.relocation);
|
||||
let a_diff = match state.left_args_idx.entry(a_str) {
|
||||
btree_map::Entry::Vacant(e) => {
|
||||
let idx = state.left_arg_idx;
|
||||
state.left_arg_idx = idx + 1;
|
||||
e.insert(idx);
|
||||
idx
|
||||
}
|
||||
btree_map::Entry::Occupied(e) => *e.get(),
|
||||
};
|
||||
let b_str = arg_to_string(b, right_resolved.relocation);
|
||||
let b_diff = match state.right_args_idx.entry(b_str) {
|
||||
btree_map::Entry::Vacant(e) => {
|
||||
let idx = state.right_arg_idx;
|
||||
state.right_arg_idx = idx + 1;
|
||||
e.insert(idx);
|
||||
idx
|
||||
}
|
||||
btree_map::Entry::Occupied(e) => *e.get(),
|
||||
};
|
||||
result.left_args_diff.push(InstructionArgDiffIndex::new(a_diff));
|
||||
result.right_args_diff.push(InstructionArgDiffIndex::new(b_diff));
|
||||
}
|
||||
}
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
Ok(InstructionDiffResult::new(InstructionDiffKind::None))
|
||||
}
|
||||
|
||||
// TODO
|
||||
// fn find_symbol_matching_fake_symbol_in_sections(
|
||||
// fake_symbol: &ObjSymbol,
|
||||
// sections: &[ObjSection],
|
||||
// ) -> Option<ObjSymbol> {
|
||||
// let orig_section_index = fake_symbol.orig_section_index?;
|
||||
// let section = sections.iter().find(|s| s.orig_index == orig_section_index)?;
|
||||
// let real_symbol = section
|
||||
// .symbols
|
||||
// .iter()
|
||||
// .find(|s| s.size > 0 && (s.address..s.address + s.size).contains(&fake_symbol.address))?;
|
||||
// Some(real_symbol.clone())
|
||||
// }
|
||||
493
objdiff-core/src/diff/data.rs
Normal file
@@ -0,0 +1,493 @@
|
||||
use alloc::{vec, vec::Vec};
|
||||
use core::{cmp::Ordering, ops::Range};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use similar::{capture_diff_slices, get_diff_ratio, Algorithm};
|
||||
|
||||
use super::{
|
||||
code::{address_eq, section_name_eq},
|
||||
DataDiff, DataDiffKind, DataRelocationDiff, ObjectDiff, SectionDiff, SymbolDiff,
|
||||
};
|
||||
use crate::obj::{Object, Relocation, ResolvedRelocation, SymbolFlag, SymbolKind};
|
||||
|
||||
pub fn diff_bss_symbol(
|
||||
left_obj: &Object,
|
||||
right_obj: &Object,
|
||||
left_symbol_ref: usize,
|
||||
right_symbol_ref: usize,
|
||||
) -> Result<(SymbolDiff, SymbolDiff)> {
|
||||
let left_symbol = &left_obj.symbols[left_symbol_ref];
|
||||
let right_symbol = &right_obj.symbols[right_symbol_ref];
|
||||
let percent = if left_symbol.size == right_symbol.size { 100.0 } else { 50.0 };
|
||||
Ok((
|
||||
SymbolDiff {
|
||||
target_symbol: Some(right_symbol_ref),
|
||||
match_percent: Some(percent),
|
||||
diff_score: None,
|
||||
instruction_rows: vec![],
|
||||
},
|
||||
SymbolDiff {
|
||||
target_symbol: Some(left_symbol_ref),
|
||||
match_percent: Some(percent),
|
||||
diff_score: None,
|
||||
instruction_rows: vec![],
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn reloc_eq(
|
||||
left_obj: &Object,
|
||||
right_obj: &Object,
|
||||
left: ResolvedRelocation,
|
||||
right: ResolvedRelocation,
|
||||
) -> bool {
|
||||
if left.relocation.flags != right.relocation.flags {
|
||||
return false;
|
||||
}
|
||||
|
||||
let symbol_name_addend_matches =
|
||||
left.symbol.name == right.symbol.name && left.relocation.addend == right.relocation.addend;
|
||||
match (left.symbol.section, right.symbol.section) {
|
||||
(Some(sl), Some(sr)) => {
|
||||
// Match if section and name+addend or address match
|
||||
section_name_eq(left_obj, right_obj, sl, sr)
|
||||
&& (symbol_name_addend_matches || address_eq(left, right))
|
||||
}
|
||||
(Some(_), None) => false,
|
||||
(None, Some(_)) => {
|
||||
// Match if possibly stripped weak symbol
|
||||
symbol_name_addend_matches && right.symbol.flags.contains(SymbolFlag::Weak)
|
||||
}
|
||||
(None, None) => symbol_name_addend_matches,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn resolve_relocation<'obj>(
|
||||
obj: &'obj Object,
|
||||
reloc: &'obj Relocation,
|
||||
) -> ResolvedRelocation<'obj> {
|
||||
let symbol = &obj.symbols[reloc.target_symbol];
|
||||
ResolvedRelocation { relocation: reloc, symbol }
|
||||
}
|
||||
|
||||
/// Compares relocations contained with a certain data range.
|
||||
fn diff_data_relocs_for_range<'left, 'right>(
|
||||
left_obj: &'left Object,
|
||||
right_obj: &'right Object,
|
||||
left_section_idx: usize,
|
||||
right_section_idx: usize,
|
||||
left_range: Range<usize>,
|
||||
right_range: Range<usize>,
|
||||
) -> Vec<(DataDiffKind, Option<ResolvedRelocation<'left>>, Option<ResolvedRelocation<'right>>)> {
|
||||
let left_section = &left_obj.sections[left_section_idx];
|
||||
let right_section = &right_obj.sections[right_section_idx];
|
||||
let mut diffs = Vec::new();
|
||||
for left_reloc in left_section.relocations.iter() {
|
||||
if !left_range.contains(&(left_reloc.address as usize)) {
|
||||
continue;
|
||||
}
|
||||
let left_offset = left_reloc.address as usize - left_range.start;
|
||||
let left_reloc = resolve_relocation(left_obj, left_reloc);
|
||||
let Some(right_reloc) = right_section.relocations.iter().find(|r| {
|
||||
if !right_range.contains(&(r.address as usize)) {
|
||||
return false;
|
||||
}
|
||||
let right_offset = r.address as usize - right_range.start;
|
||||
right_offset == left_offset
|
||||
}) else {
|
||||
diffs.push((DataDiffKind::Delete, Some(left_reloc), None));
|
||||
continue;
|
||||
};
|
||||
let right_reloc = resolve_relocation(right_obj, right_reloc);
|
||||
if reloc_eq(left_obj, right_obj, left_reloc, right_reloc) {
|
||||
diffs.push((DataDiffKind::None, Some(left_reloc), Some(right_reloc)));
|
||||
} else {
|
||||
diffs.push((DataDiffKind::Replace, Some(left_reloc), Some(right_reloc)));
|
||||
}
|
||||
}
|
||||
for right_reloc in right_section.relocations.iter() {
|
||||
if !right_range.contains(&(right_reloc.address as usize)) {
|
||||
continue;
|
||||
}
|
||||
let right_offset = right_reloc.address as usize - right_range.start;
|
||||
let right_reloc = resolve_relocation(right_obj, right_reloc);
|
||||
let Some(_) = left_section.relocations.iter().find(|r| {
|
||||
if !left_range.contains(&(r.address as usize)) {
|
||||
return false;
|
||||
}
|
||||
let left_offset = r.address as usize - left_range.start;
|
||||
left_offset == right_offset
|
||||
}) else {
|
||||
diffs.push((DataDiffKind::Insert, None, Some(right_reloc)));
|
||||
continue;
|
||||
};
|
||||
// No need to check the cases for relocations being deleted or matching again.
|
||||
// They were already handled in the loop over the left relocs.
|
||||
}
|
||||
diffs
|
||||
}
|
||||
|
||||
/// Compare the data sections of two object files.
|
||||
pub fn diff_data_section(
|
||||
left_obj: &Object,
|
||||
right_obj: &Object,
|
||||
left_diff: &ObjectDiff,
|
||||
right_diff: &ObjectDiff,
|
||||
left_section_idx: usize,
|
||||
right_section_idx: usize,
|
||||
) -> Result<(SectionDiff, SectionDiff)> {
|
||||
let left_section = &left_obj.sections[left_section_idx];
|
||||
let right_section = &right_obj.sections[right_section_idx];
|
||||
let left_max = left_obj
|
||||
.symbols
|
||||
.iter()
|
||||
.filter_map(|s| {
|
||||
if s.section != Some(left_section_idx) || s.kind == SymbolKind::Section {
|
||||
return None;
|
||||
}
|
||||
s.address.checked_sub(left_section.address).map(|a| a + s.size)
|
||||
})
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
.min(left_section.size);
|
||||
let right_max = right_obj
|
||||
.symbols
|
||||
.iter()
|
||||
.filter_map(|s| {
|
||||
if s.section != Some(right_section_idx) || s.kind == SymbolKind::Section {
|
||||
return None;
|
||||
}
|
||||
s.address.checked_sub(right_section.address).map(|a| a + s.size)
|
||||
})
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
.min(right_section.size);
|
||||
let left_data = &left_section.data[..left_max as usize];
|
||||
let right_data = &right_section.data[..right_max as usize];
|
||||
let ops = capture_diff_slices(Algorithm::Patience, left_data, right_data);
|
||||
let match_percent = get_diff_ratio(&ops, left_data.len(), right_data.len()) * 100.0;
|
||||
|
||||
let mut left_data_diff = Vec::<DataDiff>::new();
|
||||
let mut right_data_diff = Vec::<DataDiff>::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 = left_len.max(right_len);
|
||||
let kind = match tag {
|
||||
similar::DiffTag::Equal => DataDiffKind::None,
|
||||
similar::DiffTag::Delete => DataDiffKind::Delete,
|
||||
similar::DiffTag::Insert => DataDiffKind::Insert,
|
||||
similar::DiffTag::Replace => {
|
||||
// Ensure replacements are equal length
|
||||
len = left_len.min(right_len);
|
||||
DataDiffKind::Replace
|
||||
}
|
||||
};
|
||||
let left_data = &left_section.data[left_range];
|
||||
let right_data = &right_section.data[right_range];
|
||||
left_data_diff.push(DataDiff {
|
||||
data: left_data[..len.min(left_data.len())].to_vec(),
|
||||
kind,
|
||||
len,
|
||||
..Default::default()
|
||||
});
|
||||
right_data_diff.push(DataDiff {
|
||||
data: right_data[..len.min(right_data.len())].to_vec(),
|
||||
kind,
|
||||
len,
|
||||
..Default::default()
|
||||
});
|
||||
if kind == DataDiffKind::Replace {
|
||||
match left_len.cmp(&right_len) {
|
||||
Ordering::Less => {
|
||||
let len = right_len - left_len;
|
||||
left_data_diff.push(DataDiff {
|
||||
data: vec![],
|
||||
kind: DataDiffKind::Insert,
|
||||
len,
|
||||
..Default::default()
|
||||
});
|
||||
right_data_diff.push(DataDiff {
|
||||
data: right_data[left_len..right_len].to_vec(),
|
||||
kind: DataDiffKind::Insert,
|
||||
len,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
Ordering::Greater => {
|
||||
let len = left_len - right_len;
|
||||
left_data_diff.push(DataDiff {
|
||||
data: left_data[right_len..left_len].to_vec(),
|
||||
kind: DataDiffKind::Delete,
|
||||
len,
|
||||
..Default::default()
|
||||
});
|
||||
right_data_diff.push(DataDiff {
|
||||
data: vec![],
|
||||
kind: DataDiffKind::Delete,
|
||||
len,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
Ordering::Equal => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut left_reloc_diffs = Vec::new();
|
||||
let mut right_reloc_diffs = Vec::new();
|
||||
for (diff_kind, left_reloc, right_reloc) in diff_data_relocs_for_range(
|
||||
left_obj,
|
||||
right_obj,
|
||||
left_section_idx,
|
||||
right_section_idx,
|
||||
0..left_max as usize,
|
||||
0..right_max as usize,
|
||||
) {
|
||||
if let Some(left_reloc) = left_reloc {
|
||||
let len = left_obj.arch.data_reloc_size(left_reloc.relocation.flags);
|
||||
let range = left_reloc.relocation.address as usize
|
||||
..left_reloc.relocation.address as usize + len;
|
||||
left_reloc_diffs.push(DataRelocationDiff {
|
||||
reloc: left_reloc.relocation.clone(),
|
||||
kind: diff_kind,
|
||||
range,
|
||||
});
|
||||
}
|
||||
if let Some(right_reloc) = right_reloc {
|
||||
let len = right_obj.arch.data_reloc_size(right_reloc.relocation.flags);
|
||||
let range = right_reloc.relocation.address as usize
|
||||
..right_reloc.relocation.address as usize + len;
|
||||
right_reloc_diffs.push(DataRelocationDiff {
|
||||
reloc: right_reloc.relocation.clone(),
|
||||
kind: diff_kind,
|
||||
range,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let (mut left_section_diff, mut right_section_diff) = diff_generic_section(
|
||||
left_obj,
|
||||
right_obj,
|
||||
left_diff,
|
||||
right_diff,
|
||||
left_section_idx,
|
||||
right_section_idx,
|
||||
)?;
|
||||
let all_left_relocs_match = left_reloc_diffs.iter().all(|d| d.kind == DataDiffKind::None);
|
||||
left_section_diff.data_diff = left_data_diff;
|
||||
right_section_diff.data_diff = right_data_diff;
|
||||
left_section_diff.reloc_diff = left_reloc_diffs;
|
||||
right_section_diff.reloc_diff = right_reloc_diffs;
|
||||
if all_left_relocs_match {
|
||||
// Use the highest match percent between two options:
|
||||
// - Left symbols matching right symbols by name
|
||||
// - Diff of the data itself
|
||||
// We only do this when all relocations on the left side match.
|
||||
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: &Object,
|
||||
right_obj: &Object,
|
||||
left_symbol_idx: usize,
|
||||
right_symbol_idx: usize,
|
||||
) -> Result<(SymbolDiff, SymbolDiff)> {
|
||||
let left_symbol = &left_obj.symbols[left_symbol_idx];
|
||||
let right_symbol = &right_obj.symbols[right_symbol_idx];
|
||||
|
||||
let left_section_idx =
|
||||
left_symbol.section.ok_or_else(|| anyhow!("Data symbol section not found"))?;
|
||||
let right_section_idx =
|
||||
right_symbol.section.ok_or_else(|| anyhow!("Data symbol section not found"))?;
|
||||
|
||||
let left_section = &left_obj.sections[left_section_idx];
|
||||
let right_section = &right_obj.sections[right_section_idx];
|
||||
|
||||
let left_start = left_symbol
|
||||
.address
|
||||
.checked_sub(left_section.address)
|
||||
.ok_or_else(|| anyhow!("Symbol address out of section bounds"))?;
|
||||
let right_start = right_symbol
|
||||
.address
|
||||
.checked_sub(right_section.address)
|
||||
.ok_or_else(|| anyhow!("Symbol address out of section bounds"))?;
|
||||
let left_end = left_start + left_symbol.size;
|
||||
if left_end > left_section.size {
|
||||
return Err(anyhow!(
|
||||
"Symbol {} size out of section bounds ({} > {})",
|
||||
left_symbol.name,
|
||||
left_end,
|
||||
left_section.size
|
||||
));
|
||||
}
|
||||
let right_end = right_start + right_symbol.size;
|
||||
if right_end > right_section.size {
|
||||
return Err(anyhow!(
|
||||
"Symbol {} size out of section bounds ({} > {})",
|
||||
right_symbol.name,
|
||||
right_end,
|
||||
right_section.size
|
||||
));
|
||||
}
|
||||
let left_range = left_start as usize..left_end as usize;
|
||||
let right_range = right_start as usize..right_end as usize;
|
||||
let left_data = &left_section.data[left_range.clone()];
|
||||
let right_data = &right_section.data[right_range.clone()];
|
||||
|
||||
let reloc_diffs = diff_data_relocs_for_range(
|
||||
left_obj,
|
||||
right_obj,
|
||||
left_section_idx,
|
||||
right_section_idx,
|
||||
left_range,
|
||||
right_range,
|
||||
);
|
||||
|
||||
let ops = capture_diff_slices(Algorithm::Patience, left_data, right_data);
|
||||
let bytes_match_ratio = get_diff_ratio(&ops, left_data.len(), right_data.len());
|
||||
|
||||
let mut match_ratio = bytes_match_ratio;
|
||||
if !reloc_diffs.is_empty() {
|
||||
let mut total_reloc_bytes = 0;
|
||||
let mut matching_reloc_bytes = 0;
|
||||
for (diff_kind, left_reloc, right_reloc) in reloc_diffs {
|
||||
let reloc_diff_len = match (left_reloc, right_reloc) {
|
||||
(None, None) => unreachable!(),
|
||||
(None, Some(right_reloc)) => {
|
||||
right_obj.arch.data_reloc_size(right_reloc.relocation.flags)
|
||||
}
|
||||
(Some(left_reloc), _) => left_obj.arch.data_reloc_size(left_reloc.relocation.flags),
|
||||
};
|
||||
total_reloc_bytes += reloc_diff_len;
|
||||
if diff_kind == DataDiffKind::None {
|
||||
matching_reloc_bytes += reloc_diff_len;
|
||||
}
|
||||
}
|
||||
if total_reloc_bytes > 0 {
|
||||
let relocs_match_ratio = matching_reloc_bytes as f32 / total_reloc_bytes as f32;
|
||||
// Adjust the overall match ratio to include relocation differences.
|
||||
// We calculate it so that bytes that contain a relocation are counted twice: once for the
|
||||
// byte's raw value, and once for its relocation.
|
||||
// e.g. An 8 byte symbol that has 8 matching raw bytes and a single 4 byte relocation that
|
||||
// doesn't match would show as 66% (weighted average of 100% and 0%).
|
||||
match_ratio = ((bytes_match_ratio * (left_data.len() as f32))
|
||||
+ (relocs_match_ratio * total_reloc_bytes as f32))
|
||||
/ (left_data.len() + total_reloc_bytes) as f32;
|
||||
}
|
||||
}
|
||||
|
||||
let match_percent = match_ratio * 100.0;
|
||||
|
||||
Ok((
|
||||
SymbolDiff {
|
||||
target_symbol: Some(right_symbol_idx),
|
||||
match_percent: Some(match_percent),
|
||||
diff_score: None,
|
||||
instruction_rows: vec![],
|
||||
},
|
||||
SymbolDiff {
|
||||
target_symbol: Some(left_symbol_idx),
|
||||
match_percent: Some(match_percent),
|
||||
diff_score: None,
|
||||
instruction_rows: vec![],
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// 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_obj: &Object,
|
||||
_right_obj: &Object,
|
||||
left_diff: &ObjectDiff,
|
||||
_right_diff: &ObjectDiff,
|
||||
left_section_idx: usize,
|
||||
_right_section_idx: usize,
|
||||
) -> Result<(SectionDiff, SectionDiff)> {
|
||||
let match_percent = if left_obj
|
||||
.symbols
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, s)| s.section == Some(left_section_idx) && s.kind != SymbolKind::Section)
|
||||
.map(|(i, _)| &left_diff.symbols[i])
|
||||
.all(|d| d.match_percent == Some(100.0))
|
||||
{
|
||||
100.0 // Avoid fp precision issues
|
||||
} else {
|
||||
let (matched, total) = left_obj
|
||||
.symbols
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, s)| s.section == Some(left_section_idx) && s.kind != SymbolKind::Section)
|
||||
.map(|(i, s)| (s, &left_diff.symbols[i]))
|
||||
.fold((0.0, 0.0), |(matched, total), (s, d)| {
|
||||
(matched + d.match_percent.unwrap_or(0.0) * s.size as f32, total + s.size as f32)
|
||||
});
|
||||
if total == 0.0 {
|
||||
100.0
|
||||
} else {
|
||||
matched / total
|
||||
}
|
||||
};
|
||||
Ok((
|
||||
SectionDiff { match_percent: Some(match_percent), data_diff: vec![], reloc_diff: vec![] },
|
||||
SectionDiff { match_percent: Some(match_percent), data_diff: vec![], reloc_diff: vec![] },
|
||||
))
|
||||
}
|
||||
|
||||
/// Compare the addresses and sizes of each symbol in the BSS sections.
|
||||
pub fn diff_bss_section(
|
||||
left_obj: &Object,
|
||||
right_obj: &Object,
|
||||
left_diff: &ObjectDiff,
|
||||
right_diff: &ObjectDiff,
|
||||
left_section_idx: usize,
|
||||
right_section_idx: usize,
|
||||
) -> Result<(SectionDiff, SectionDiff)> {
|
||||
let left_section = &left_obj.sections[left_section_idx];
|
||||
let left_sizes = left_obj
|
||||
.symbols
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, s)| s.section == Some(left_section_idx) && s.kind != SymbolKind::Section)
|
||||
.filter_map(|(_, s)| s.address.checked_sub(left_section.address).map(|a| (a, s.size)))
|
||||
.collect::<Vec<_>>();
|
||||
let right_section = &right_obj.sections[right_section_idx];
|
||||
let right_sizes = right_obj
|
||||
.symbols
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, s)| s.section == Some(right_section_idx) && s.kind != SymbolKind::Section)
|
||||
.filter_map(|(_, s)| s.address.checked_sub(right_section.address).map(|a| (a, s.size)))
|
||||
.collect::<Vec<_>>();
|
||||
let ops = capture_diff_slices(Algorithm::Patience, &left_sizes, &right_sizes);
|
||||
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_obj,
|
||||
right_obj,
|
||||
left_diff,
|
||||
right_diff,
|
||||
left_section_idx,
|
||||
right_section_idx,
|
||||
)?;
|
||||
if generic_diff.match_percent.unwrap_or(-1.0) > match_percent {
|
||||
match_percent = generic_diff.match_percent.unwrap();
|
||||
}
|
||||
|
||||
Ok((
|
||||
SectionDiff { match_percent: Some(match_percent), data_diff: vec![], reloc_diff: vec![] },
|
||||
SectionDiff { match_percent: Some(match_percent), data_diff: vec![], reloc_diff: vec![] },
|
||||
))
|
||||
}
|
||||
759
objdiff-core/src/diff/display.rs
Normal file
@@ -0,0 +1,759 @@
|
||||
use alloc::{
|
||||
borrow::Cow,
|
||||
collections::BTreeSet,
|
||||
format,
|
||||
string::{String, ToString},
|
||||
vec::Vec,
|
||||
};
|
||||
use core::cmp::Ordering;
|
||||
|
||||
use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::{
|
||||
diff::{DiffObjConfig, InstructionDiffKind, InstructionDiffRow, ObjectDiff, SymbolDiff},
|
||||
obj::{
|
||||
InstructionArg, InstructionArgValue, Object, ParsedInstruction, ResolvedInstructionRef,
|
||||
ResolvedRelocation, SectionFlag, SectionKind, Symbol, SymbolFlag, SymbolKind,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DiffText<'a> {
|
||||
/// Basic text
|
||||
Basic(&'a str),
|
||||
/// Line number
|
||||
Line(u32),
|
||||
/// Instruction address
|
||||
Address(u64),
|
||||
/// Instruction mnemonic
|
||||
Opcode(&'a str, u16),
|
||||
/// Instruction argument
|
||||
Argument(InstructionArgValue<'a>),
|
||||
/// Branch destination
|
||||
BranchDest(u64),
|
||||
/// Symbol name
|
||||
Symbol(&'a Symbol),
|
||||
/// Relocation addend
|
||||
Addend(i64),
|
||||
/// Number of spaces
|
||||
Spacing(u8),
|
||||
/// End of line
|
||||
Eol,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash)]
|
||||
pub enum DiffTextColor {
|
||||
#[default]
|
||||
Normal, // Grey
|
||||
Dim, // Dark grey
|
||||
Bright, // White
|
||||
Replace, // Blue
|
||||
Delete, // Red
|
||||
Insert, // Green
|
||||
Rotating(u8),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DiffTextSegment<'a> {
|
||||
pub text: DiffText<'a>,
|
||||
pub color: DiffTextColor,
|
||||
pub pad_to: u8,
|
||||
}
|
||||
|
||||
impl<'a> DiffTextSegment<'a> {
|
||||
#[inline(always)]
|
||||
pub fn basic(text: &'a str, color: DiffTextColor) -> Self {
|
||||
Self { text: DiffText::Basic(text), color, pad_to: 0 }
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn spacing(spaces: u8) -> Self {
|
||||
Self { text: DiffText::Spacing(spaces), color: DiffTextColor::Normal, pad_to: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
const EOL_SEGMENT: DiffTextSegment<'static> =
|
||||
DiffTextSegment { text: DiffText::Eol, color: DiffTextColor::Normal, pad_to: 0 };
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub enum HighlightKind {
|
||||
#[default]
|
||||
None,
|
||||
Opcode(u16),
|
||||
Argument(InstructionArgValue<'static>),
|
||||
Symbol(String),
|
||||
Address(u64),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum InstructionPart<'a> {
|
||||
Basic(Cow<'a, str>),
|
||||
Opcode(Cow<'a, str>, u16),
|
||||
Arg(InstructionArg<'a>),
|
||||
Separator,
|
||||
}
|
||||
|
||||
impl<'a> InstructionPart<'a> {
|
||||
#[inline(always)]
|
||||
pub fn basic<T>(s: T) -> Self
|
||||
where T: Into<Cow<'a, str>> {
|
||||
InstructionPart::Basic(s.into())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn opcode<T>(s: T, o: u16) -> Self
|
||||
where T: Into<Cow<'a, str>> {
|
||||
InstructionPart::Opcode(s.into(), o)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn opaque<T>(s: T) -> Self
|
||||
where T: Into<Cow<'a, str>> {
|
||||
InstructionPart::Arg(InstructionArg::Value(InstructionArgValue::Opaque(s.into())))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn signed<T>(v: T) -> InstructionPart<'static>
|
||||
where T: Into<i64> {
|
||||
InstructionPart::Arg(InstructionArg::Value(InstructionArgValue::Signed(v.into())))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn unsigned<T>(v: T) -> InstructionPart<'static>
|
||||
where T: Into<u64> {
|
||||
InstructionPart::Arg(InstructionArg::Value(InstructionArgValue::Unsigned(v.into())))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn branch_dest<T>(v: T) -> InstructionPart<'static>
|
||||
where T: Into<u64> {
|
||||
InstructionPart::Arg(InstructionArg::BranchDest(v.into()))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn reloc() -> InstructionPart<'static> { InstructionPart::Arg(InstructionArg::Reloc) }
|
||||
|
||||
#[inline(always)]
|
||||
pub fn separator() -> InstructionPart<'static> { InstructionPart::Separator }
|
||||
|
||||
pub fn into_static(self) -> InstructionPart<'static> {
|
||||
match self {
|
||||
InstructionPart::Basic(s) => InstructionPart::Basic(Cow::Owned(s.into_owned())),
|
||||
InstructionPart::Opcode(s, o) => InstructionPart::Opcode(Cow::Owned(s.into_owned()), o),
|
||||
InstructionPart::Arg(a) => InstructionPart::Arg(a.into_static()),
|
||||
InstructionPart::Separator => InstructionPart::Separator,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_row(
|
||||
obj: &Object,
|
||||
symbol_index: usize,
|
||||
ins_row: &InstructionDiffRow,
|
||||
diff_config: &DiffObjConfig,
|
||||
mut cb: impl FnMut(DiffTextSegment) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
let Some(ins_ref) = ins_row.ins_ref else {
|
||||
cb(EOL_SEGMENT)?;
|
||||
return Ok(());
|
||||
};
|
||||
let Some(resolved) = obj.resolve_instruction_ref(symbol_index, ins_ref) else {
|
||||
cb(DiffTextSegment::basic("<invalid>", DiffTextColor::Delete))?;
|
||||
cb(EOL_SEGMENT)?;
|
||||
return Ok(());
|
||||
};
|
||||
let base_color = match ins_row.kind {
|
||||
InstructionDiffKind::Replace => DiffTextColor::Replace,
|
||||
InstructionDiffKind::Delete => DiffTextColor::Delete,
|
||||
InstructionDiffKind::Insert => DiffTextColor::Insert,
|
||||
_ => DiffTextColor::Normal,
|
||||
};
|
||||
if let Some(line) = resolved.section.line_info.range(..=ins_ref.address).last().map(|(_, &b)| b)
|
||||
{
|
||||
cb(DiffTextSegment { text: DiffText::Line(line), color: DiffTextColor::Dim, pad_to: 5 })?;
|
||||
}
|
||||
cb(DiffTextSegment {
|
||||
text: DiffText::Address(ins_ref.address.saturating_sub(resolved.symbol.address)),
|
||||
color: base_color,
|
||||
pad_to: 5,
|
||||
})?;
|
||||
if let Some(branch) = &ins_row.branch_from {
|
||||
cb(DiffTextSegment::basic(" ~> ", DiffTextColor::Rotating(branch.branch_idx as u8)))?;
|
||||
} else {
|
||||
cb(DiffTextSegment::spacing(4))?;
|
||||
}
|
||||
let mut arg_idx = 0;
|
||||
let mut displayed_relocation = false;
|
||||
obj.arch.display_instruction(resolved, diff_config, &mut |part| match part {
|
||||
InstructionPart::Basic(text) => {
|
||||
if text.chars().all(|c| c == ' ') {
|
||||
cb(DiffTextSegment::spacing(text.len() as u8))
|
||||
} else {
|
||||
cb(DiffTextSegment::basic(&text, base_color))
|
||||
}
|
||||
}
|
||||
InstructionPart::Opcode(mnemonic, opcode) => cb(DiffTextSegment {
|
||||
text: DiffText::Opcode(mnemonic.as_ref(), opcode),
|
||||
color: match ins_row.kind {
|
||||
InstructionDiffKind::OpMismatch => DiffTextColor::Replace,
|
||||
_ => base_color,
|
||||
},
|
||||
pad_to: 10,
|
||||
}),
|
||||
InstructionPart::Arg(arg) => {
|
||||
let diff_index = ins_row.arg_diff.get(arg_idx).copied().unwrap_or_default();
|
||||
arg_idx += 1;
|
||||
match arg {
|
||||
InstructionArg::Value(value) => cb(DiffTextSegment {
|
||||
text: DiffText::Argument(value),
|
||||
color: diff_index
|
||||
.get()
|
||||
.map_or(base_color, |i| DiffTextColor::Rotating(i as u8)),
|
||||
pad_to: 0,
|
||||
}),
|
||||
InstructionArg::Reloc => {
|
||||
displayed_relocation = true;
|
||||
let resolved = resolved.relocation.unwrap();
|
||||
let color = diff_index
|
||||
.get()
|
||||
.map_or(DiffTextColor::Bright, |i| DiffTextColor::Rotating(i as u8));
|
||||
cb(DiffTextSegment {
|
||||
text: DiffText::Symbol(resolved.symbol),
|
||||
color,
|
||||
pad_to: 0,
|
||||
})?;
|
||||
if resolved.relocation.addend != 0 {
|
||||
cb(DiffTextSegment {
|
||||
text: DiffText::Addend(resolved.relocation.addend),
|
||||
color,
|
||||
pad_to: 0,
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
InstructionArg::BranchDest(dest) => {
|
||||
if let Some(addr) = dest.checked_sub(resolved.symbol.address) {
|
||||
cb(DiffTextSegment {
|
||||
text: DiffText::BranchDest(addr),
|
||||
color: diff_index
|
||||
.get()
|
||||
.map_or(base_color, |i| DiffTextColor::Rotating(i as u8)),
|
||||
pad_to: 0,
|
||||
})
|
||||
} else {
|
||||
cb(DiffTextSegment {
|
||||
text: DiffText::Argument(InstructionArgValue::Opaque(Cow::Borrowed(
|
||||
"<invalid>",
|
||||
))),
|
||||
color: diff_index
|
||||
.get()
|
||||
.map_or(base_color, |i| DiffTextColor::Rotating(i as u8)),
|
||||
pad_to: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
InstructionPart::Separator => {
|
||||
cb(DiffTextSegment::basic(diff_config.separator(), base_color))
|
||||
}
|
||||
})?;
|
||||
// Fallback for relocation that wasn't displayed
|
||||
if resolved.relocation.is_some() && !displayed_relocation {
|
||||
cb(DiffTextSegment::basic(" <", base_color))?;
|
||||
let resolved = resolved.relocation.unwrap();
|
||||
let diff_index = ins_row.arg_diff.get(arg_idx).copied().unwrap_or_default();
|
||||
let color =
|
||||
diff_index.get().map_or(DiffTextColor::Bright, |i| DiffTextColor::Rotating(i as u8));
|
||||
cb(DiffTextSegment { text: DiffText::Symbol(resolved.symbol), color, pad_to: 0 })?;
|
||||
if resolved.relocation.addend != 0 {
|
||||
cb(DiffTextSegment {
|
||||
text: DiffText::Addend(resolved.relocation.addend),
|
||||
color,
|
||||
pad_to: 0,
|
||||
})?;
|
||||
}
|
||||
cb(DiffTextSegment::basic(">", base_color))?;
|
||||
}
|
||||
if let Some(branch) = &ins_row.branch_to {
|
||||
cb(DiffTextSegment::basic(" ~>", DiffTextColor::Rotating(branch.branch_idx as u8)))?;
|
||||
}
|
||||
cb(EOL_SEGMENT)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl PartialEq<DiffText<'_>> for HighlightKind {
|
||||
fn eq(&self, other: &DiffText) -> bool {
|
||||
match (self, other) {
|
||||
(HighlightKind::Opcode(a), DiffText::Opcode(_, b)) => a == b,
|
||||
(HighlightKind::Argument(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::Argument(arg.to_static()),
|
||||
DiffText::Symbol(sym) => HighlightKind::Symbol(sym.name.to_string()),
|
||||
DiffText::Address(addr) | DiffText::BranchDest(addr) => HighlightKind::Address(*addr),
|
||||
_ => HighlightKind::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ContextItem {
|
||||
Copy { value: String, label: Option<String> },
|
||||
Navigate { label: String, symbol_index: usize, kind: SymbolNavigationKind },
|
||||
Separator,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Eq, PartialEq)]
|
||||
pub enum SymbolNavigationKind {
|
||||
#[default]
|
||||
Normal,
|
||||
Extab,
|
||||
}
|
||||
|
||||
pub enum HoverItemColor {
|
||||
Normal, // Gray
|
||||
Emphasized, // White
|
||||
Special, // Blue
|
||||
}
|
||||
|
||||
pub enum HoverItem {
|
||||
Text { label: String, value: String, color: HoverItemColor },
|
||||
Separator,
|
||||
}
|
||||
|
||||
pub fn symbol_context(obj: &Object, symbol_index: usize) -> Vec<ContextItem> {
|
||||
let symbol = &obj.symbols[symbol_index];
|
||||
let mut out = Vec::new();
|
||||
out.push(ContextItem::Copy { value: symbol.name.clone(), label: None });
|
||||
if let Some(name) = &symbol.demangled_name {
|
||||
out.push(ContextItem::Copy { value: name.clone(), label: None });
|
||||
}
|
||||
if symbol.section.is_some() {
|
||||
if let Some(address) = symbol.virtual_address {
|
||||
out.push(ContextItem::Copy {
|
||||
value: format!("{:x}", address),
|
||||
label: Some("virtual address".to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
out.append(&mut obj.arch.symbol_context(obj, symbol_index));
|
||||
out
|
||||
}
|
||||
|
||||
pub fn symbol_hover(obj: &Object, symbol_index: usize, addend: i64) -> Vec<HoverItem> {
|
||||
let symbol = &obj.symbols[symbol_index];
|
||||
let addend_str = match addend.cmp(&0i64) {
|
||||
Ordering::Greater => format!("+{:x}", addend),
|
||||
Ordering::Less => format!("-{:x}", -addend),
|
||||
_ => String::new(),
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
out.push(HoverItem::Text {
|
||||
label: "Name".into(),
|
||||
value: format!("{}{}", symbol.name, addend_str),
|
||||
color: HoverItemColor::Normal,
|
||||
});
|
||||
if let Some(demangled_name) = &symbol.demangled_name {
|
||||
out.push(HoverItem::Text {
|
||||
label: "Demangled".into(),
|
||||
value: demangled_name.into(),
|
||||
color: HoverItemColor::Normal,
|
||||
});
|
||||
}
|
||||
if let Some(section) = symbol.section {
|
||||
out.push(HoverItem::Text {
|
||||
label: "Section".into(),
|
||||
value: obj.sections[section].name.clone(),
|
||||
color: HoverItemColor::Normal,
|
||||
});
|
||||
out.push(HoverItem::Text {
|
||||
label: "Address".into(),
|
||||
value: format!("{:x}{}", symbol.address, addend_str),
|
||||
color: HoverItemColor::Normal,
|
||||
});
|
||||
if symbol.flags.contains(SymbolFlag::SizeInferred) {
|
||||
out.push(HoverItem::Text {
|
||||
label: "Size".into(),
|
||||
value: format!("{:x} (inferred)", symbol.size),
|
||||
color: HoverItemColor::Normal,
|
||||
});
|
||||
} else {
|
||||
out.push(HoverItem::Text {
|
||||
label: "Size".into(),
|
||||
value: format!("{:x}", symbol.size),
|
||||
color: HoverItemColor::Normal,
|
||||
});
|
||||
}
|
||||
if let Some(align) = symbol.align {
|
||||
out.push(HoverItem::Text {
|
||||
label: "Alignment".into(),
|
||||
value: align.get().to_string(),
|
||||
color: HoverItemColor::Normal,
|
||||
});
|
||||
}
|
||||
if let Some(address) = symbol.virtual_address {
|
||||
out.push(HoverItem::Text {
|
||||
label: "Virtual address".into(),
|
||||
value: format!("{:x}", address),
|
||||
color: HoverItemColor::Special,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
out.push(HoverItem::Text {
|
||||
label: Default::default(),
|
||||
value: "Extern".into(),
|
||||
color: HoverItemColor::Emphasized,
|
||||
});
|
||||
}
|
||||
out.append(&mut obj.arch.symbol_hover(obj, symbol_index));
|
||||
out
|
||||
}
|
||||
|
||||
pub fn relocation_context(
|
||||
obj: &Object,
|
||||
reloc: ResolvedRelocation,
|
||||
ins: Option<ResolvedInstructionRef>,
|
||||
) -> Vec<ContextItem> {
|
||||
let mut out = Vec::new();
|
||||
out.append(&mut symbol_context(obj, reloc.relocation.target_symbol));
|
||||
if let Some(ins) = ins {
|
||||
let literals = display_ins_data_literals(obj, ins);
|
||||
if !literals.is_empty() {
|
||||
out.push(ContextItem::Separator);
|
||||
for literal in literals {
|
||||
out.push(ContextItem::Copy { value: literal, label: None });
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn relocation_hover(obj: &Object, reloc: ResolvedRelocation) -> Vec<HoverItem> {
|
||||
let mut out = Vec::new();
|
||||
if let Some(name) = obj.arch.reloc_name(reloc.relocation.flags) {
|
||||
out.push(HoverItem::Text {
|
||||
label: "Relocation".into(),
|
||||
value: name.to_string(),
|
||||
color: HoverItemColor::Normal,
|
||||
});
|
||||
} else {
|
||||
out.push(HoverItem::Text {
|
||||
label: "Relocation".into(),
|
||||
value: format!("<{:?}>", reloc.relocation.flags),
|
||||
color: HoverItemColor::Normal,
|
||||
});
|
||||
}
|
||||
out.append(&mut symbol_hover(obj, reloc.relocation.target_symbol, reloc.relocation.addend));
|
||||
out
|
||||
}
|
||||
|
||||
pub fn instruction_context(
|
||||
obj: &Object,
|
||||
resolved: ResolvedInstructionRef,
|
||||
ins: &ParsedInstruction,
|
||||
) -> Vec<ContextItem> {
|
||||
let mut out = Vec::new();
|
||||
let mut hex_string = String::new();
|
||||
for byte in resolved.code {
|
||||
hex_string.push_str(&format!("{:02x}", byte));
|
||||
}
|
||||
out.push(ContextItem::Copy { value: hex_string, label: Some("instruction bytes".to_string()) });
|
||||
out.append(&mut obj.arch.instruction_context(obj, resolved));
|
||||
if let Some(virtual_address) = resolved.symbol.virtual_address {
|
||||
let offset = resolved.ins_ref.address - resolved.symbol.address;
|
||||
out.push(ContextItem::Copy {
|
||||
value: format!("{:x}", virtual_address + offset),
|
||||
label: Some("virtual address".to_string()),
|
||||
});
|
||||
}
|
||||
for arg in &ins.args {
|
||||
if let InstructionArg::Value(arg) = arg {
|
||||
out.push(ContextItem::Copy { value: arg.to_string(), label: None });
|
||||
match arg {
|
||||
InstructionArgValue::Signed(v) => {
|
||||
out.push(ContextItem::Copy { value: v.to_string(), label: None });
|
||||
}
|
||||
InstructionArgValue::Unsigned(v) => {
|
||||
out.push(ContextItem::Copy { value: v.to_string(), label: None });
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(reloc) = resolved.relocation {
|
||||
out.push(ContextItem::Separator);
|
||||
out.append(&mut relocation_context(obj, reloc, Some(resolved)));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn instruction_hover(
|
||||
obj: &Object,
|
||||
resolved: ResolvedInstructionRef,
|
||||
ins: &ParsedInstruction,
|
||||
) -> Vec<HoverItem> {
|
||||
let mut out = Vec::new();
|
||||
out.push(HoverItem::Text {
|
||||
label: Default::default(),
|
||||
value: format!("{:02x?}", resolved.code),
|
||||
color: HoverItemColor::Normal,
|
||||
});
|
||||
out.append(&mut obj.arch.instruction_hover(obj, resolved));
|
||||
if let Some(virtual_address) = resolved.symbol.virtual_address {
|
||||
let offset = resolved.ins_ref.address - resolved.symbol.address;
|
||||
out.push(HoverItem::Text {
|
||||
label: "Virtual address".into(),
|
||||
value: format!("{:x}", virtual_address + offset),
|
||||
color: HoverItemColor::Special,
|
||||
});
|
||||
}
|
||||
for arg in &ins.args {
|
||||
if let InstructionArg::Value(arg) = arg {
|
||||
match arg {
|
||||
InstructionArgValue::Signed(v) => {
|
||||
out.push(HoverItem::Text {
|
||||
label: Default::default(),
|
||||
value: format!("{arg} == {v}"),
|
||||
color: HoverItemColor::Normal,
|
||||
});
|
||||
}
|
||||
InstructionArgValue::Unsigned(v) => {
|
||||
out.push(HoverItem::Text {
|
||||
label: Default::default(),
|
||||
value: format!("{arg} == {v}"),
|
||||
color: HoverItemColor::Normal,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(reloc) = resolved.relocation {
|
||||
out.push(HoverItem::Separator);
|
||||
out.append(&mut relocation_hover(obj, reloc));
|
||||
if let Some(ty) = obj.arch.guess_data_type(resolved) {
|
||||
let literals = display_ins_data_literals(obj, resolved);
|
||||
if !literals.is_empty() {
|
||||
out.push(HoverItem::Separator);
|
||||
for literal in literals {
|
||||
out.push(HoverItem::Text {
|
||||
label: format!("{}", ty),
|
||||
value: literal,
|
||||
color: HoverItemColor::Normal,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum SymbolFilter<'a> {
|
||||
None,
|
||||
Search(&'a Regex),
|
||||
Mapping(usize, Option<&'a Regex>),
|
||||
}
|
||||
|
||||
fn symbol_matches_filter(
|
||||
symbol: &Symbol,
|
||||
diff: &SymbolDiff,
|
||||
filter: SymbolFilter<'_>,
|
||||
show_hidden_symbols: bool,
|
||||
) -> bool {
|
||||
// Ignore absolute symbols
|
||||
if symbol.section.is_none() && !symbol.flags.contains(SymbolFlag::Common) {
|
||||
return false;
|
||||
}
|
||||
if !show_hidden_symbols && (symbol.size == 0 || symbol.flags.contains(SymbolFlag::Hidden)) {
|
||||
return false;
|
||||
}
|
||||
match filter {
|
||||
SymbolFilter::None => true,
|
||||
SymbolFilter::Search(regex) => {
|
||||
regex.is_match(&symbol.name)
|
||||
|| symbol.demangled_name.as_deref().is_some_and(|s| regex.is_match(s))
|
||||
}
|
||||
SymbolFilter::Mapping(symbol_ref, regex) => {
|
||||
diff.target_symbol == Some(symbol_ref)
|
||||
&& regex.is_none_or(|r| {
|
||||
r.is_match(&symbol.name)
|
||||
|| symbol.demangled_name.as_deref().is_some_and(|s| r.is_match(s))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub struct SectionDisplaySymbol {
|
||||
pub symbol: usize,
|
||||
pub is_mapping_symbol: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SectionDisplay {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub size: u64,
|
||||
pub match_percent: Option<f32>,
|
||||
pub symbols: Vec<SectionDisplaySymbol>,
|
||||
}
|
||||
|
||||
pub fn display_sections(
|
||||
obj: &Object,
|
||||
diff: &ObjectDiff,
|
||||
filter: SymbolFilter<'_>,
|
||||
show_hidden_symbols: bool,
|
||||
show_mapped_symbols: bool,
|
||||
reverse_fn_order: bool,
|
||||
) -> Vec<SectionDisplay> {
|
||||
let mut mapping = BTreeSet::new();
|
||||
let is_mapping_symbol = if let SymbolFilter::Mapping(_, _) = filter {
|
||||
for mapping_diff in &diff.mapping_symbols {
|
||||
let symbol = &obj.symbols[mapping_diff.symbol_index];
|
||||
if !symbol_matches_filter(
|
||||
symbol,
|
||||
&mapping_diff.symbol_diff,
|
||||
filter,
|
||||
show_hidden_symbols,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if !show_mapped_symbols {
|
||||
let symbol_diff = &diff.symbols[mapping_diff.symbol_index];
|
||||
if symbol_diff.target_symbol.is_some() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
mapping.insert((symbol.section, mapping_diff.symbol_index));
|
||||
}
|
||||
true
|
||||
} else {
|
||||
for (symbol_idx, (symbol, symbol_diff)) in obj.symbols.iter().zip(&diff.symbols).enumerate()
|
||||
{
|
||||
if !symbol_matches_filter(symbol, symbol_diff, filter, show_hidden_symbols) {
|
||||
continue;
|
||||
}
|
||||
mapping.insert((symbol.section, symbol_idx));
|
||||
}
|
||||
false
|
||||
};
|
||||
let num_sections = mapping.iter().map(|(section_idx, _)| *section_idx).dedup().count();
|
||||
let mut sections = Vec::with_capacity(num_sections);
|
||||
for (section_idx, group) in &mapping.iter().chunk_by(|(section_idx, _)| *section_idx) {
|
||||
let mut symbols = group
|
||||
.map(|&(_, symbol)| SectionDisplaySymbol { symbol, is_mapping_symbol })
|
||||
.collect::<Vec<_>>();
|
||||
if let Some(section_idx) = section_idx {
|
||||
let section = &obj.sections[section_idx];
|
||||
if section.kind == SectionKind::Unknown || section.flags.contains(SectionFlag::Hidden) {
|
||||
// Skip unknown and hidden sections
|
||||
continue;
|
||||
}
|
||||
let section_diff = &diff.sections[section_idx];
|
||||
if section.kind == SectionKind::Code && reverse_fn_order {
|
||||
symbols.sort_by(|a, b| {
|
||||
let a_symbol = &obj.symbols[a.symbol];
|
||||
let b_symbol = &obj.symbols[b.symbol];
|
||||
symbol_sort_reverse(a_symbol, b_symbol)
|
||||
});
|
||||
} else {
|
||||
symbols.sort_by(|a, b| {
|
||||
let a_symbol = &obj.symbols[a.symbol];
|
||||
let b_symbol = &obj.symbols[b.symbol];
|
||||
symbol_sort(a_symbol, b_symbol)
|
||||
});
|
||||
}
|
||||
sections.push(SectionDisplay {
|
||||
id: section.id.clone(),
|
||||
name: if section.flags.contains(SectionFlag::Combined) {
|
||||
format!("{} [combined]", section.name)
|
||||
} else {
|
||||
section.name.clone()
|
||||
},
|
||||
size: section.size,
|
||||
match_percent: section_diff.match_percent,
|
||||
symbols,
|
||||
});
|
||||
} else {
|
||||
// Don't sort, preserve order of absolute symbols
|
||||
sections.push(SectionDisplay {
|
||||
id: ".comm".to_string(),
|
||||
name: ".comm".to_string(),
|
||||
size: 0,
|
||||
match_percent: None,
|
||||
symbols,
|
||||
});
|
||||
}
|
||||
}
|
||||
sections.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
sections
|
||||
}
|
||||
|
||||
fn section_symbol_sort(a: &Symbol, b: &Symbol) -> Ordering {
|
||||
if a.kind == SymbolKind::Section {
|
||||
if b.kind != SymbolKind::Section {
|
||||
return Ordering::Less;
|
||||
}
|
||||
} else if b.kind == SymbolKind::Section {
|
||||
return Ordering::Greater;
|
||||
}
|
||||
Ordering::Equal
|
||||
}
|
||||
|
||||
fn symbol_sort(a: &Symbol, b: &Symbol) -> Ordering {
|
||||
section_symbol_sort(a, b).then(a.address.cmp(&b.address)).then(a.size.cmp(&b.size))
|
||||
}
|
||||
|
||||
fn symbol_sort_reverse(a: &Symbol, b: &Symbol) -> Ordering {
|
||||
section_symbol_sort(a, b).then(b.address.cmp(&a.address)).then(b.size.cmp(&a.size))
|
||||
}
|
||||
|
||||
pub fn display_ins_data_labels(obj: &Object, resolved: ResolvedInstructionRef) -> Vec<String> {
|
||||
let Some(reloc) = resolved.relocation else {
|
||||
return Vec::new();
|
||||
};
|
||||
if reloc.relocation.addend < 0 || reloc.relocation.addend as u64 >= reloc.symbol.size {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(data) = obj.symbol_data(reloc.relocation.target_symbol) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let bytes = &data[reloc.relocation.addend as usize..];
|
||||
obj.arch
|
||||
.guess_data_type(resolved)
|
||||
.map(|ty| ty.display_labels(obj.endianness, bytes))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn display_ins_data_literals(obj: &Object, resolved: ResolvedInstructionRef) -> Vec<String> {
|
||||
let Some(reloc) = resolved.relocation else {
|
||||
return Vec::new();
|
||||
};
|
||||
if reloc.relocation.addend < 0 || reloc.relocation.addend as u64 >= reloc.symbol.size {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(data) = obj.symbol_data(reloc.relocation.target_symbol) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let bytes = &data[reloc.relocation.addend as usize..];
|
||||
obj.arch
|
||||
.guess_data_type(resolved)
|
||||
.map(|ty| ty.display_literals(obj.endianness, bytes))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
690
objdiff-core/src/diff/mod.rs
Normal file
@@ -0,0 +1,690 @@
|
||||
use alloc::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
string::String,
|
||||
vec,
|
||||
vec::Vec,
|
||||
};
|
||||
use core::{num::NonZeroU32, ops::Range};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{
|
||||
diff::{
|
||||
code::{diff_code, no_diff_code},
|
||||
data::{
|
||||
diff_bss_section, diff_bss_symbol, diff_data_section, diff_data_symbol,
|
||||
diff_generic_section,
|
||||
},
|
||||
},
|
||||
obj::{InstructionRef, Object, Relocation, SectionKind, Symbol, SymbolFlag},
|
||||
};
|
||||
|
||||
pub mod code;
|
||||
pub mod data;
|
||||
pub mod display;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/config.gen.rs"));
|
||||
|
||||
impl DiffObjConfig {
|
||||
pub fn separator(&self) -> &'static str {
|
||||
if self.space_between_args {
|
||||
", "
|
||||
} else {
|
||||
","
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SectionDiff {
|
||||
// pub target_section: Option<usize>,
|
||||
pub match_percent: Option<f32>,
|
||||
pub data_diff: Vec<DataDiff>,
|
||||
pub reloc_diff: Vec<DataRelocationDiff>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SymbolDiff {
|
||||
/// The symbol index in the _other_ object that this symbol was diffed against
|
||||
pub target_symbol: Option<usize>,
|
||||
pub match_percent: Option<f32>,
|
||||
pub diff_score: Option<(u64, u64)>,
|
||||
pub instruction_rows: Vec<InstructionDiffRow>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MappingSymbolDiff {
|
||||
pub symbol_index: usize,
|
||||
pub symbol_diff: SymbolDiff,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct InstructionDiffRow {
|
||||
/// Instruction reference
|
||||
pub ins_ref: Option<InstructionRef>,
|
||||
/// Diff kind
|
||||
pub kind: InstructionDiffKind,
|
||||
/// Branches from instruction(s)
|
||||
pub branch_from: Option<InstructionBranchFrom>,
|
||||
/// Branches to instruction
|
||||
pub branch_to: Option<InstructionBranchTo>,
|
||||
/// Arg diffs
|
||||
pub arg_diff: Vec<InstructionArgDiffIndex>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
|
||||
pub enum InstructionDiffKind {
|
||||
#[default]
|
||||
None,
|
||||
OpMismatch,
|
||||
ArgMismatch,
|
||||
Replace,
|
||||
Delete,
|
||||
Insert,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DataDiff {
|
||||
pub data: Vec<u8>,
|
||||
pub kind: DataDiffKind,
|
||||
pub len: usize,
|
||||
pub symbol: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DataRelocationDiff {
|
||||
pub reloc: Relocation,
|
||||
pub kind: DataDiffKind,
|
||||
pub range: Range<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
|
||||
pub enum DataDiffKind {
|
||||
#[default]
|
||||
None,
|
||||
Replace,
|
||||
Delete,
|
||||
Insert,
|
||||
}
|
||||
|
||||
/// Index of the argument diff for coloring.
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Copy, Clone, Default)]
|
||||
pub struct InstructionArgDiffIndex(pub Option<NonZeroU32>);
|
||||
|
||||
impl InstructionArgDiffIndex {
|
||||
pub const NONE: Self = Self(None);
|
||||
|
||||
#[inline(always)]
|
||||
pub fn new(idx: u32) -> Self {
|
||||
Self(Some(unsafe { NonZeroU32::new_unchecked(idx.saturating_add(1)) }))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get(&self) -> Option<u32> { self.0.map(|idx| idx.get() - 1) }
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_some(&self) -> bool { self.0.is_some() }
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_none(&self) -> bool { self.0.is_none() }
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InstructionBranchFrom {
|
||||
/// Source instruction indices
|
||||
pub ins_idx: Vec<u32>,
|
||||
/// Incrementing index for coloring
|
||||
pub branch_idx: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InstructionBranchTo {
|
||||
/// Target instruction index
|
||||
pub ins_idx: u32,
|
||||
/// Incrementing index for coloring
|
||||
pub branch_idx: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ObjectDiff {
|
||||
/// A list of all symbol diffs in the object.
|
||||
pub symbols: Vec<SymbolDiff>,
|
||||
/// A list of all section diffs in the object.
|
||||
pub sections: Vec<SectionDiff>,
|
||||
/// 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<MappingSymbolDiff>,
|
||||
}
|
||||
|
||||
impl ObjectDiff {
|
||||
pub fn new_from_obj(obj: &Object) -> Self {
|
||||
let mut result = Self {
|
||||
symbols: Vec::with_capacity(obj.symbols.len()),
|
||||
sections: Vec::with_capacity(obj.sections.len()),
|
||||
mapping_symbols: vec![],
|
||||
};
|
||||
for _ in obj.symbols.iter() {
|
||||
result.symbols.push(SymbolDiff {
|
||||
target_symbol: None,
|
||||
match_percent: None,
|
||||
diff_score: None,
|
||||
instruction_rows: vec![],
|
||||
});
|
||||
}
|
||||
for _ in obj.sections.iter() {
|
||||
result.sections.push(SectionDiff {
|
||||
// target_section: None,
|
||||
match_percent: None,
|
||||
data_diff: vec![],
|
||||
reloc_diff: vec![],
|
||||
});
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DiffObjsResult {
|
||||
pub left: Option<ObjectDiff>,
|
||||
pub right: Option<ObjectDiff>,
|
||||
pub prev: Option<ObjectDiff>,
|
||||
}
|
||||
|
||||
pub fn diff_objs(
|
||||
left: Option<&Object>,
|
||||
right: Option<&Object>,
|
||||
prev: Option<&Object>,
|
||||
diff_config: &DiffObjConfig,
|
||||
mapping_config: &MappingConfig,
|
||||
) -> Result<DiffObjsResult> {
|
||||
let symbol_matches = matching_symbols(left, right, prev, mapping_config)?;
|
||||
let section_matches = matching_sections(left, right)?;
|
||||
let mut left = left.map(|p| (p, ObjectDiff::new_from_obj(p)));
|
||||
let mut right = right.map(|p| (p, ObjectDiff::new_from_obj(p)));
|
||||
let mut prev = prev.map(|p| (p, ObjectDiff::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 {
|
||||
SectionKind::Code => {
|
||||
let (left_diff, right_diff) = diff_code(
|
||||
left_obj,
|
||||
right_obj,
|
||||
left_symbol_ref,
|
||||
right_symbol_ref,
|
||||
diff_config,
|
||||
)?;
|
||||
left_out.symbols[left_symbol_ref] = left_diff;
|
||||
right_out.symbols[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_diff) = diff_code(
|
||||
left_obj,
|
||||
right_obj,
|
||||
right_symbol_ref,
|
||||
prev_symbol_ref,
|
||||
diff_config,
|
||||
)?;
|
||||
prev_out.symbols[prev_symbol_ref] = prev_diff;
|
||||
}
|
||||
}
|
||||
SectionKind::Data => {
|
||||
let (left_diff, right_diff) = diff_data_symbol(
|
||||
left_obj,
|
||||
right_obj,
|
||||
left_symbol_ref,
|
||||
right_symbol_ref,
|
||||
)?;
|
||||
left_out.symbols[left_symbol_ref] = left_diff;
|
||||
right_out.symbols[right_symbol_ref] = right_diff;
|
||||
}
|
||||
SectionKind::Bss | SectionKind::Common => {
|
||||
let (left_diff, right_diff) = diff_bss_symbol(
|
||||
left_obj,
|
||||
right_obj,
|
||||
left_symbol_ref,
|
||||
right_symbol_ref,
|
||||
)?;
|
||||
left_out.symbols[left_symbol_ref] = left_diff;
|
||||
right_out.symbols[right_symbol_ref] = right_diff;
|
||||
}
|
||||
SectionKind::Unknown => unreachable!(),
|
||||
}
|
||||
}
|
||||
SymbolMatch { left: Some(left_symbol_ref), right: None, prev: _, section_kind } => {
|
||||
let (left_obj, left_out) = left.as_mut().unwrap();
|
||||
match section_kind {
|
||||
SectionKind::Code => {
|
||||
left_out.symbols[left_symbol_ref] =
|
||||
no_diff_code(left_obj, left_symbol_ref, diff_config)?;
|
||||
}
|
||||
SectionKind::Data | SectionKind::Bss | SectionKind::Common => {
|
||||
// Nothing needs to be done
|
||||
}
|
||||
SectionKind::Unknown => unreachable!(),
|
||||
}
|
||||
}
|
||||
SymbolMatch { left: None, right: Some(right_symbol_ref), prev: _, section_kind } => {
|
||||
let (right_obj, right_out) = right.as_mut().unwrap();
|
||||
match section_kind {
|
||||
SectionKind::Code => {
|
||||
right_out.symbols[right_symbol_ref] =
|
||||
no_diff_code(right_obj, right_symbol_ref, diff_config)?;
|
||||
}
|
||||
SectionKind::Data | SectionKind::Bss | SectionKind::Common => {
|
||||
// Nothing needs to be done
|
||||
}
|
||||
SectionKind::Unknown => unreachable!(),
|
||||
}
|
||||
}
|
||||
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();
|
||||
match section_kind {
|
||||
SectionKind::Code => {
|
||||
let (left_diff, right_diff) = diff_generic_section(
|
||||
left_obj,
|
||||
right_obj,
|
||||
left_out,
|
||||
right_out,
|
||||
left_section_idx,
|
||||
right_section_idx,
|
||||
)?;
|
||||
left_out.sections[left_section_idx] = left_diff;
|
||||
right_out.sections[right_section_idx] = right_diff;
|
||||
}
|
||||
SectionKind::Data => {
|
||||
let (left_diff, right_diff) = diff_data_section(
|
||||
left_obj,
|
||||
right_obj,
|
||||
left_out,
|
||||
right_out,
|
||||
left_section_idx,
|
||||
right_section_idx,
|
||||
)?;
|
||||
left_out.sections[left_section_idx] = left_diff;
|
||||
right_out.sections[right_section_idx] = right_diff;
|
||||
}
|
||||
SectionKind::Bss | SectionKind::Common => {
|
||||
let (left_diff, right_diff) = diff_bss_section(
|
||||
left_obj,
|
||||
right_obj,
|
||||
left_out,
|
||||
right_out,
|
||||
left_section_idx,
|
||||
right_section_idx,
|
||||
)?;
|
||||
left_out.sections[left_section_idx] = left_diff;
|
||||
right_out.sections[right_section_idx] = right_diff;
|
||||
}
|
||||
SectionKind::Unknown => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some((right_obj, right_out)), Some((left_obj, left_out))) =
|
||||
(right.as_mut(), left.as_mut())
|
||||
{
|
||||
if let Some(right_name) = &mapping_config.selecting_left {
|
||||
generate_mapping_symbols(right_obj, right_name, left_obj, left_out, diff_config)?;
|
||||
}
|
||||
if let Some(left_name) = &mapping_config.selecting_right {
|
||||
generate_mapping_symbols(left_obj, left_name, right_obj, right_out, diff_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: &Object,
|
||||
base_name: &str,
|
||||
target_obj: &Object,
|
||||
target_out: &mut ObjectDiff,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<()> {
|
||||
let Some(base_symbol_ref) = symbol_ref_by_name(base_obj, base_name) else {
|
||||
return Ok(());
|
||||
};
|
||||
let base_section_kind = symbol_section_kind(base_obj, &base_obj.symbols[base_symbol_ref]);
|
||||
for (target_symbol_index, target_symbol) in target_obj.symbols.iter().enumerate() {
|
||||
if symbol_section_kind(target_obj, target_symbol) != base_section_kind {
|
||||
continue;
|
||||
}
|
||||
match base_section_kind {
|
||||
SectionKind::Code => {
|
||||
let (left_diff, _right_diff) =
|
||||
diff_code(target_obj, base_obj, target_symbol_index, base_symbol_ref, config)?;
|
||||
target_out.mapping_symbols.push(MappingSymbolDiff {
|
||||
symbol_index: target_symbol_index,
|
||||
symbol_diff: left_diff,
|
||||
});
|
||||
}
|
||||
SectionKind::Data => {
|
||||
let (left_diff, _right_diff) =
|
||||
diff_data_symbol(target_obj, base_obj, target_symbol_index, base_symbol_ref)?;
|
||||
target_out.mapping_symbols.push(MappingSymbolDiff {
|
||||
symbol_index: target_symbol_index,
|
||||
symbol_diff: left_diff,
|
||||
});
|
||||
}
|
||||
SectionKind::Bss | SectionKind::Common => {
|
||||
let (left_diff, _right_diff) =
|
||||
diff_bss_symbol(target_obj, base_obj, target_symbol_index, base_symbol_ref)?;
|
||||
target_out.mapping_symbols.push(MappingSymbolDiff {
|
||||
symbol_index: target_symbol_index,
|
||||
symbol_diff: left_diff,
|
||||
});
|
||||
}
|
||||
SectionKind::Unknown => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
struct SymbolMatch {
|
||||
left: Option<usize>,
|
||||
right: Option<usize>,
|
||||
prev: Option<usize>,
|
||||
section_kind: SectionKind,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
struct SectionMatch {
|
||||
left: Option<usize>,
|
||||
right: Option<usize>,
|
||||
section_kind: SectionKind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
|
||||
pub struct MappingConfig {
|
||||
/// Manual symbol mappings
|
||||
pub mappings: BTreeMap<String, String>,
|
||||
/// 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: &Object, name: &str) -> Option<usize> {
|
||||
obj.symbols.iter().position(|s| s.name == name)
|
||||
}
|
||||
|
||||
fn apply_symbol_mappings(
|
||||
left: &Object,
|
||||
right: &Object,
|
||||
mapping_config: &MappingConfig,
|
||||
left_used: &mut BTreeSet<usize>,
|
||||
right_used: &mut BTreeSet<usize>,
|
||||
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_index) = symbol_ref_by_name(left, left_name) else {
|
||||
continue;
|
||||
};
|
||||
if left_used.contains(&left_symbol_index) {
|
||||
continue;
|
||||
}
|
||||
let Some(right_symbol_index) = symbol_ref_by_name(right, right_name) else {
|
||||
continue;
|
||||
};
|
||||
if right_used.contains(&right_symbol_index) {
|
||||
continue;
|
||||
}
|
||||
let left_section_kind = left
|
||||
.symbols
|
||||
.get(left_symbol_index)
|
||||
.and_then(|s| s.section)
|
||||
.and_then(|section_index| left.sections.get(section_index))
|
||||
.map_or(SectionKind::Unknown, |s| s.kind);
|
||||
let right_section_kind = right
|
||||
.symbols
|
||||
.get(right_symbol_index)
|
||||
.and_then(|s| s.section)
|
||||
.and_then(|section_index| right.sections.get(section_index))
|
||||
.map_or(SectionKind::Unknown, |s| s.kind);
|
||||
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_index),
|
||||
right: Some(right_symbol_index),
|
||||
prev: None, // TODO
|
||||
section_kind: left_section_kind,
|
||||
});
|
||||
left_used.insert(left_symbol_index);
|
||||
right_used.insert(right_symbol_index);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Find matching symbols between each object.
|
||||
fn matching_symbols(
|
||||
left: Option<&Object>,
|
||||
right: Option<&Object>,
|
||||
prev: Option<&Object>,
|
||||
mappings: &MappingConfig,
|
||||
) -> Result<Vec<SymbolMatch>> {
|
||||
let mut matches = Vec::new();
|
||||
let mut left_used = BTreeSet::new();
|
||||
let mut right_used = BTreeSet::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 (symbol_idx, symbol) in left.symbols.iter().enumerate() {
|
||||
let section_kind = symbol_section_kind(left, symbol);
|
||||
if section_kind == SectionKind::Unknown {
|
||||
continue;
|
||||
}
|
||||
if left_used.contains(&symbol_idx) {
|
||||
continue;
|
||||
}
|
||||
let symbol_match = SymbolMatch {
|
||||
left: Some(symbol_idx),
|
||||
right: find_symbol(right, left, symbol, Some(&right_used)),
|
||||
prev: find_symbol(prev, left, symbol, None),
|
||||
section_kind,
|
||||
};
|
||||
matches.push(symbol_match);
|
||||
if let Some(right) = symbol_match.right {
|
||||
right_used.insert(right);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(right) = right {
|
||||
for (symbol_idx, symbol) in right.symbols.iter().enumerate() {
|
||||
let section_kind = symbol_section_kind(right, symbol);
|
||||
if section_kind == SectionKind::Unknown {
|
||||
continue;
|
||||
}
|
||||
if right_used.contains(&symbol_idx) {
|
||||
continue;
|
||||
}
|
||||
matches.push(SymbolMatch {
|
||||
left: None,
|
||||
right: Some(symbol_idx),
|
||||
prev: find_symbol(prev, right, symbol, None),
|
||||
section_kind,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(matches)
|
||||
}
|
||||
|
||||
fn unmatched_symbols<'obj, 'used>(
|
||||
obj: &'obj Object,
|
||||
used: Option<&'used BTreeSet<usize>>,
|
||||
) -> impl Iterator<Item = (usize, &'obj Symbol)> + 'used
|
||||
where
|
||||
'obj: 'used,
|
||||
{
|
||||
obj.symbols.iter().enumerate().filter(move |&(symbol_idx, _)| {
|
||||
// Skip symbols that have already been matched
|
||||
!used.is_some_and(|u| u.contains(&symbol_idx))
|
||||
})
|
||||
}
|
||||
|
||||
fn symbol_section<'obj>(obj: &'obj Object, symbol: &Symbol) -> Option<(&'obj str, SectionKind)> {
|
||||
if let Some(section) = symbol.section.and_then(|section_idx| obj.sections.get(section_idx)) {
|
||||
Some((section.name.as_str(), section.kind))
|
||||
} else if symbol.flags.contains(SymbolFlag::Common) {
|
||||
Some((".comm", SectionKind::Common))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn symbol_section_kind(obj: &Object, symbol: &Symbol) -> SectionKind {
|
||||
match symbol.section {
|
||||
Some(section_index) => obj.sections[section_index].kind,
|
||||
None if symbol.flags.contains(SymbolFlag::Common) => SectionKind::Common,
|
||||
None => SectionKind::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
fn find_symbol(
|
||||
obj: Option<&Object>,
|
||||
in_obj: &Object,
|
||||
in_symbol: &Symbol,
|
||||
used: Option<&BTreeSet<usize>>,
|
||||
) -> Option<usize> {
|
||||
let obj = obj?;
|
||||
let (section_name, section_kind) = symbol_section(in_obj, in_symbol)?;
|
||||
// Try to find an exact name match
|
||||
if let Some((symbol_idx, _)) = unmatched_symbols(obj, used).find(|(_, symbol)| {
|
||||
symbol.name == in_symbol.name && symbol_section_kind(obj, symbol) == section_kind
|
||||
}) {
|
||||
return Some(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!(section_kind, SectionKind::Data | SectionKind::Bss)
|
||||
{
|
||||
if let Some((symbol_idx, _)) = unmatched_symbols(obj, used).find(|(_, symbol)| {
|
||||
let Some(section_index) = symbol.section else {
|
||||
return false;
|
||||
};
|
||||
symbol.name.starts_with('@')
|
||||
&& symbol.address == in_symbol.address
|
||||
&& obj.sections[section_index].name == section_name
|
||||
}) {
|
||||
return Some(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;
|
||||
}
|
||||
if let Some((symbol_idx, _)) = unmatched_symbols(obj, used).find(|&(_, symbol)| {
|
||||
if let Some((p, s)) = symbol.name.split_once('$') {
|
||||
prefix == p
|
||||
&& s.chars().all(char::is_numeric)
|
||||
&& symbol_section_kind(obj, symbol) == section_kind
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}) {
|
||||
return Some(symbol_idx);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Find matching sections between each object.
|
||||
fn matching_sections(left: Option<&Object>, right: Option<&Object>) -> Result<Vec<SectionMatch>> {
|
||||
let mut matches = Vec::new();
|
||||
if let Some(left) = left {
|
||||
for (section_idx, section) in left.sections.iter().enumerate() {
|
||||
if section.kind == SectionKind::Unknown {
|
||||
continue;
|
||||
}
|
||||
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 section.kind == SectionKind::Unknown {
|
||||
continue;
|
||||
}
|
||||
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<&Object>, name: &str, section_kind: SectionKind) -> Option<usize> {
|
||||
obj?.sections.iter().position(|s| s.kind == section_kind && s.name == name)
|
||||
}
|
||||
50
objdiff-core/src/jobs/check_update.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use std::{sync::mpsc::Receiver, task::Waker};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use self_update::{
|
||||
cargo_crate_version,
|
||||
update::{Release, ReleaseUpdate},
|
||||
};
|
||||
|
||||
use crate::jobs::{start_job, update_status, Job, JobContext, JobResult, JobState};
|
||||
|
||||
pub struct CheckUpdateConfig {
|
||||
pub build_updater: fn() -> Result<Box<dyn ReleaseUpdate>>,
|
||||
pub bin_names: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct CheckUpdateResult {
|
||||
pub update_available: bool,
|
||||
pub latest_release: Release,
|
||||
pub found_binary: Option<String>,
|
||||
}
|
||||
|
||||
fn run_check_update(
|
||||
context: &JobContext,
|
||||
cancel: Receiver<()>,
|
||||
config: CheckUpdateConfig,
|
||||
) -> Result<Box<CheckUpdateResult>> {
|
||||
update_status(context, "Fetching latest release".to_string(), 0, 1, &cancel)?;
|
||||
let updater = (config.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 mut found_binary = None;
|
||||
for bin_name in &config.bin_names {
|
||||
if latest_release.assets.iter().any(|a| &a.name == bin_name) {
|
||||
found_binary = Some(bin_name.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
update_status(context, "Complete".to_string(), 1, 1, &cancel)?;
|
||||
Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary }))
|
||||
}
|
||||
|
||||
pub fn start_check_update(waker: Waker, config: CheckUpdateConfig) -> JobState {
|
||||
start_job(waker, "Check for updates", Job::CheckUpdate, move |context, cancel| {
|
||||
run_check_update(&context, cancel, config)
|
||||
.map(|result| JobResult::CheckUpdate(Some(result)))
|
||||
})
|
||||
}
|
||||
103
objdiff-core/src/jobs/create_scratch.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use std::{fs, sync::mpsc::Receiver, task::Waker};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use typed_path::{Utf8PlatformPathBuf, Utf8UnixPathBuf};
|
||||
|
||||
use crate::{
|
||||
build::{run_make, BuildConfig, BuildStatus},
|
||||
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateScratchConfig {
|
||||
pub build_config: BuildConfig,
|
||||
pub context_path: Option<Utf8UnixPathBuf>,
|
||||
pub build_context: bool,
|
||||
|
||||
// Scratch fields
|
||||
pub compiler: String,
|
||||
pub platform: String,
|
||||
pub compiler_flags: String,
|
||||
pub function_name: String,
|
||||
pub target_obj: Utf8PlatformPathBuf,
|
||||
pub preset_id: Option<u32>,
|
||||
}
|
||||
|
||||
#[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.as_ref()) {
|
||||
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.with_platform_encoding());
|
||||
context = Some(
|
||||
fs::read_to_string(&context_path)
|
||||
.map_err(|e| anyhow!("Failed to read {}: {}", context_path, 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)?;
|
||||
let file = reqwest::blocking::multipart::Part::file(&config.target_obj)
|
||||
.with_context(|| format!("Failed to open {}", config.target_obj))?;
|
||||
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(format!("{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(waker: Waker, config: CreateScratchConfig) -> JobState {
|
||||
start_job(waker, "Create scratch", Job::CreateScratch, move |context, cancel| {
|
||||
run_create_scratch(&context, cancel, config)
|
||||
.map(|result| JobResult::CreateScratch(Some(result)))
|
||||
})
|
||||
}
|
||||
230
objdiff-core/src/jobs/mod.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
mpsc::{Receiver, Sender, TryRecvError},
|
||||
Arc, RwLock,
|
||||
},
|
||||
task::Waker,
|
||||
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.
|
||||
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); }
|
||||
|
||||
/// Collects the results of all finished jobs and handles any errors.
|
||||
pub fn collect_results(&mut self) {
|
||||
let mut results = vec![];
|
||||
for (job, result) in self.iter_finished() {
|
||||
match result {
|
||||
Ok(result) => {
|
||||
match result {
|
||||
JobResult::None => {
|
||||
// Job context contains the error
|
||||
}
|
||||
_ => 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),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.results.append(&mut results);
|
||||
self.clear_finished();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JobContext {
|
||||
pub status: Arc<RwLock<JobStatus>>,
|
||||
pub waker: Waker,
|
||||
}
|
||||
|
||||
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(
|
||||
waker: Waker,
|
||||
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(), waker: waker.clone() };
|
||||
let context_inner = JobContext { status: status.clone(), waker };
|
||||
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); TODO
|
||||
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.waker.wake_by_ref();
|
||||
Ok(())
|
||||
}
|
||||
194
objdiff-core/src/jobs/objdiff.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
use std::{sync::mpsc::Receiver, task::Waker};
|
||||
|
||||
use anyhow::{bail, Error, Result};
|
||||
use time::OffsetDateTime;
|
||||
use typed_path::Utf8PlatformPathBuf;
|
||||
|
||||
use crate::{
|
||||
build::{run_make, BuildConfig, BuildStatus},
|
||||
diff::{diff_objs, DiffObjConfig, MappingConfig, ObjectDiff},
|
||||
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
|
||||
obj::{read, Object},
|
||||
};
|
||||
|
||||
pub struct ObjDiffConfig {
|
||||
pub build_config: BuildConfig,
|
||||
pub build_base: bool,
|
||||
pub build_target: bool,
|
||||
pub target_path: Option<Utf8PlatformPathBuf>,
|
||||
pub base_path: Option<Utf8PlatformPathBuf>,
|
||||
pub diff_obj_config: DiffObjConfig,
|
||||
pub mapping_config: MappingConfig,
|
||||
}
|
||||
|
||||
pub struct ObjDiffResult {
|
||||
pub first_status: BuildStatus,
|
||||
pub second_status: BuildStatus,
|
||||
pub first_obj: Option<(Object, ObjectDiff)>,
|
||||
pub second_obj: Option<(Object, ObjectDiff)>,
|
||||
pub time: OffsetDateTime,
|
||||
}
|
||||
|
||||
fn run_build(
|
||||
context: &JobContext,
|
||||
cancel: Receiver<()>,
|
||||
config: ObjDiffConfig,
|
||||
) -> Result<Box<ObjDiffResult>> {
|
||||
let mut target_path_rel = None;
|
||||
let mut base_path_rel = None;
|
||||
if config.build_target || config.build_base {
|
||||
let project_dir = config
|
||||
.build_config
|
||||
.project_dir
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::msg("Missing project dir"))?;
|
||||
if let Some(target_path) = &config.target_path {
|
||||
target_path_rel = match target_path.strip_prefix(project_dir) {
|
||||
Ok(p) => Some(p.with_unix_encoding()),
|
||||
Err(_) => {
|
||||
bail!("Target path '{}' doesn't begin with '{}'", target_path, project_dir);
|
||||
}
|
||||
};
|
||||
}
|
||||
if let Some(base_path) = &config.base_path {
|
||||
base_path_rel = match base_path.strip_prefix(project_dir) {
|
||||
Ok(p) => Some(p.with_unix_encoding()),
|
||||
Err(_) => {
|
||||
bail!("Base path '{}' doesn't begin with '{}'", base_path, project_dir);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
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 config.target_path.is_some() {
|
||||
total += 1;
|
||||
}
|
||||
if config.base_path.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),
|
||||
step_idx,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
step_idx += 1;
|
||||
run_make(&config.build_config, target_path_rel.as_ref())
|
||||
}
|
||||
_ => 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),
|
||||
step_idx,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
step_idx += 1;
|
||||
run_make(&config.build_config, base_path_rel.as_ref())
|
||||
}
|
||||
_ => BuildStatus::default(),
|
||||
};
|
||||
|
||||
let time = OffsetDateTime::now_utc();
|
||||
|
||||
let first_obj = match &config.target_path {
|
||||
Some(target_path) if first_status.success => {
|
||||
update_status(
|
||||
context,
|
||||
format!("Loading target {}", target_path),
|
||||
step_idx,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
step_idx += 1;
|
||||
match read::read(target_path.as_ref(), &config.diff_obj_config) {
|
||||
Ok(obj) => Some(obj),
|
||||
Err(e) => {
|
||||
first_status = BuildStatus {
|
||||
success: false,
|
||||
stdout: format!("Loading object '{}'", target_path),
|
||||
stderr: format!("{:#}", e),
|
||||
..Default::default()
|
||||
};
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
step_idx += 1;
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let second_obj = match &config.base_path {
|
||||
Some(base_path) if second_status.success => {
|
||||
update_status(
|
||||
context,
|
||||
format!("Loading base {}", base_path),
|
||||
step_idx,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
step_idx += 1;
|
||||
match read::read(base_path.as_ref(), &config.diff_obj_config) {
|
||||
Ok(obj) => Some(obj),
|
||||
Err(e) => {
|
||||
second_status = BuildStatus {
|
||||
success: false,
|
||||
stdout: format!("Loading object '{}'", base_path),
|
||||
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(
|
||||
first_obj.as_ref(),
|
||||
second_obj.as_ref(),
|
||||
None,
|
||||
&config.diff_obj_config,
|
||||
&config.mapping_config,
|
||||
)?;
|
||||
|
||||
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(waker: Waker, config: ObjDiffConfig) -> JobState {
|
||||
start_job(waker, "Build", Job::ObjDiff, move |context, cancel| {
|
||||
run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
|
||||
})
|
||||
}
|
||||
@@ -3,29 +3,36 @@ use std::{
|
||||
fs::File,
|
||||
path::PathBuf,
|
||||
sync::mpsc::Receiver,
|
||||
task::Waker,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use const_format::formatcp;
|
||||
pub use self_update; // Re-export self_update crate
|
||||
use self_update::update::ReleaseUpdate;
|
||||
|
||||
use crate::{
|
||||
jobs::{queue_job, update_status, Job, JobResult, JobState, Status},
|
||||
update::{build_updater, BIN_NAME},
|
||||
};
|
||||
use crate::jobs::{start_job, update_status, Job, JobContext, JobResult, JobState};
|
||||
|
||||
pub struct UpdateConfig {
|
||||
pub build_updater: fn() -> Result<Box<dyn ReleaseUpdate>>,
|
||||
pub bin_name: String,
|
||||
}
|
||||
|
||||
pub struct UpdateResult {
|
||||
pub exe_path: PathBuf,
|
||||
}
|
||||
|
||||
fn run_update(status: &Status, cancel: Receiver<()>) -> Result<Box<UpdateResult>> {
|
||||
fn run_update(
|
||||
status: &JobContext,
|
||||
cancel: Receiver<()>,
|
||||
config: UpdateConfig,
|
||||
) -> 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 updater = (config.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)))?;
|
||||
let asset =
|
||||
latest_release.assets.iter().find(|a| a.name == config.bin_name).ok_or_else(|| {
|
||||
anyhow::Error::msg(format!("No release asset for {}", config.bin_name))
|
||||
})?;
|
||||
|
||||
update_status(status, "Downloading release".to_string(), 1, 3, &cancel)?;
|
||||
let tmp_dir = tempfile::Builder::new().prefix("update").tempdir_in(current_dir()?)?;
|
||||
@@ -33,7 +40,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,17 +51,16 @@ fn run_update(status: &Status, cancel: Receiver<()>) -> Result<Box<UpdateResult>
|
||||
#[cfg(unix)]
|
||||
{
|
||||
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)?;
|
||||
fs::set_permissions(&target_file, fs::Permissions::from_mode(0o755))?;
|
||||
}
|
||||
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(waker: Waker, config: UpdateConfig) -> JobState {
|
||||
start_job(waker, "Update app", Job::Update, move |context, cancel| {
|
||||
run_update(&context, cancel, config).map(JobResult::Update)
|
||||
})
|
||||
}
|
||||
20
objdiff-core/src/lib.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
extern crate alloc;
|
||||
|
||||
#[cfg(feature = "any-arch")]
|
||||
pub mod arch;
|
||||
#[cfg(feature = "bindings")]
|
||||
pub mod bindings;
|
||||
#[cfg(feature = "build")]
|
||||
pub mod build;
|
||||
#[cfg(feature = "config")]
|
||||
pub mod config;
|
||||
#[cfg(feature = "any-arch")]
|
||||
pub mod diff;
|
||||
#[cfg(feature = "build")]
|
||||
pub mod jobs;
|
||||
#[cfg(feature = "any-arch")]
|
||||
pub mod obj;
|
||||
#[cfg(feature = "any-arch")]
|
||||
pub mod util;
|
||||
381
objdiff-core/src/obj/mod.rs
Normal file
@@ -0,0 +1,381 @@
|
||||
pub mod read;
|
||||
pub mod split_meta;
|
||||
|
||||
use alloc::{
|
||||
borrow::Cow,
|
||||
boxed::Box,
|
||||
collections::BTreeMap,
|
||||
string::{String, ToString},
|
||||
vec,
|
||||
vec::Vec,
|
||||
};
|
||||
use core::{fmt, num::NonZeroU32};
|
||||
|
||||
use flagset::{flags, FlagSet};
|
||||
|
||||
use crate::{
|
||||
arch::{Arch, ArchDummy},
|
||||
obj::split_meta::SplitMeta,
|
||||
util::ReallySigned,
|
||||
};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone, Default)]
|
||||
pub enum SectionKind {
|
||||
#[default]
|
||||
Unknown = -1,
|
||||
Code,
|
||||
Data,
|
||||
Bss,
|
||||
Common,
|
||||
}
|
||||
|
||||
flags! {
|
||||
#[derive(Hash)]
|
||||
pub enum SymbolFlag: u8 {
|
||||
Global,
|
||||
Local,
|
||||
Weak,
|
||||
Common,
|
||||
Hidden,
|
||||
/// Has extra data associated with the symbol
|
||||
/// (e.g. exception table entry)
|
||||
HasExtra,
|
||||
/// Symbol size was missing and was inferred
|
||||
SizeInferred,
|
||||
}
|
||||
}
|
||||
|
||||
pub type SymbolFlagSet = FlagSet<SymbolFlag>;
|
||||
|
||||
flags! {
|
||||
#[derive(Hash)]
|
||||
pub enum SectionFlag: u8 {
|
||||
/// Section combined from multiple input sections
|
||||
Combined,
|
||||
Hidden,
|
||||
}
|
||||
}
|
||||
|
||||
pub type SectionFlagSet = FlagSet<SectionFlag>;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Section {
|
||||
/// Unique section ID
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub address: u64,
|
||||
pub size: u64,
|
||||
pub kind: SectionKind,
|
||||
pub data: SectionData,
|
||||
pub flags: SectionFlagSet,
|
||||
pub relocations: Vec<Relocation>,
|
||||
/// Line number info (.line or .debug_line section)
|
||||
pub line_info: BTreeMap<u64, u32>,
|
||||
/// Original virtual address (from .note.split section)
|
||||
pub virtual_address: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
#[repr(transparent)]
|
||||
pub struct SectionData(pub Vec<u8>);
|
||||
|
||||
impl core::ops::Deref for SectionData {
|
||||
type Target = Vec<u8>;
|
||||
|
||||
fn deref(&self) -> &Self::Target { &self.0 }
|
||||
}
|
||||
|
||||
impl fmt::Debug for SectionData {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("SectionData").field(&self.0.len()).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Section {
|
||||
pub fn data_range(&self, address: u64, size: usize) -> Option<&[u8]> {
|
||||
let start = self.address;
|
||||
let end = start + self.size;
|
||||
if address >= start && address + size as u64 <= end {
|
||||
let offset = (address - start) as usize;
|
||||
Some(&self.data[offset..offset + size])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn relocation_at<'obj>(
|
||||
&'obj self,
|
||||
obj: &'obj Object,
|
||||
ins_ref: InstructionRef,
|
||||
) -> Option<ResolvedRelocation<'obj>> {
|
||||
match self.relocations.binary_search_by_key(&ins_ref.address, |r| r.address) {
|
||||
Ok(i) => self.relocations.get(i),
|
||||
Err(i) => self
|
||||
.relocations
|
||||
.get(i)
|
||||
.take_if(|r| r.address < ins_ref.address + ins_ref.size as u64),
|
||||
}
|
||||
.and_then(|relocation| {
|
||||
let symbol = obj.symbols.get(relocation.target_symbol)?;
|
||||
Some(ResolvedRelocation { relocation, symbol })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum InstructionArgValue<'a> {
|
||||
Signed(i64),
|
||||
Unsigned(u64),
|
||||
Opaque(Cow<'a, str>),
|
||||
}
|
||||
|
||||
impl InstructionArgValue<'_> {
|
||||
pub fn loose_eq(&self, other: &InstructionArgValue) -> bool {
|
||||
match (self, other) {
|
||||
(InstructionArgValue::Signed(a), InstructionArgValue::Signed(b)) => a == b,
|
||||
(InstructionArgValue::Unsigned(a), InstructionArgValue::Unsigned(b)) => a == b,
|
||||
(InstructionArgValue::Signed(a), InstructionArgValue::Unsigned(b))
|
||||
| (InstructionArgValue::Unsigned(b), InstructionArgValue::Signed(a)) => *a as u64 == *b,
|
||||
(InstructionArgValue::Opaque(a), InstructionArgValue::Opaque(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_static(&self) -> InstructionArgValue<'static> {
|
||||
match self {
|
||||
InstructionArgValue::Signed(v) => InstructionArgValue::Signed(*v),
|
||||
InstructionArgValue::Unsigned(v) => InstructionArgValue::Unsigned(*v),
|
||||
InstructionArgValue::Opaque(v) => InstructionArgValue::Opaque(v.to_string().into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_static(self) -> InstructionArgValue<'static> {
|
||||
match self {
|
||||
InstructionArgValue::Signed(v) => InstructionArgValue::Signed(v),
|
||||
InstructionArgValue::Unsigned(v) => InstructionArgValue::Unsigned(v),
|
||||
InstructionArgValue::Opaque(v) => {
|
||||
InstructionArgValue::Opaque(Cow::Owned(v.into_owned()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for InstructionArgValue<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
InstructionArgValue::Signed(v) => write!(f, "{:#x}", ReallySigned(*v)),
|
||||
InstructionArgValue::Unsigned(v) => write!(f, "{:#x}", v),
|
||||
InstructionArgValue::Opaque(v) => write!(f, "{}", v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum InstructionArg<'a> {
|
||||
Value(InstructionArgValue<'a>),
|
||||
Reloc,
|
||||
BranchDest(u64),
|
||||
}
|
||||
|
||||
impl InstructionArg<'_> {
|
||||
pub fn loose_eq(&self, other: &InstructionArg) -> bool {
|
||||
match (self, other) {
|
||||
(InstructionArg::Value(a), InstructionArg::Value(b)) => a.loose_eq(b),
|
||||
(InstructionArg::Reloc, InstructionArg::Reloc) => true,
|
||||
(InstructionArg::BranchDest(a), InstructionArg::BranchDest(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_static(&self) -> InstructionArg<'static> {
|
||||
match self {
|
||||
InstructionArg::Value(v) => InstructionArg::Value(v.to_static()),
|
||||
InstructionArg::Reloc => InstructionArg::Reloc,
|
||||
InstructionArg::BranchDest(v) => InstructionArg::BranchDest(*v),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_static(self) -> InstructionArg<'static> {
|
||||
match self {
|
||||
InstructionArg::Value(v) => InstructionArg::Value(v.into_static()),
|
||||
InstructionArg::Reloc => InstructionArg::Reloc,
|
||||
InstructionArg::BranchDest(v) => InstructionArg::BranchDest(v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub struct InstructionRef {
|
||||
pub address: u64,
|
||||
pub size: u8,
|
||||
pub opcode: u16,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct ScannedInstruction {
|
||||
pub ins_ref: InstructionRef,
|
||||
pub branch_dest: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParsedInstruction {
|
||||
pub ins_ref: InstructionRef,
|
||||
pub mnemonic: Cow<'static, str>,
|
||||
pub args: Vec<InstructionArg<'static>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
|
||||
pub enum SymbolKind {
|
||||
#[default]
|
||||
Unknown,
|
||||
Function,
|
||||
Object,
|
||||
Section,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
|
||||
pub struct Symbol {
|
||||
pub name: String,
|
||||
pub demangled_name: Option<String>,
|
||||
pub address: u64,
|
||||
pub size: u64,
|
||||
pub kind: SymbolKind,
|
||||
pub section: Option<usize>,
|
||||
pub flags: SymbolFlagSet,
|
||||
/// Alignment (from Metrowerks .comment section)
|
||||
pub align: Option<NonZeroU32>,
|
||||
/// Original virtual address (from .note.split section)
|
||||
pub virtual_address: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Object {
|
||||
pub arch: Box<dyn Arch>,
|
||||
pub endianness: object::Endianness,
|
||||
pub symbols: Vec<Symbol>,
|
||||
pub sections: Vec<Section>,
|
||||
/// Split object metadata (.note.split section)
|
||||
pub split_meta: Option<SplitMeta>,
|
||||
#[cfg(feature = "std")]
|
||||
pub path: Option<std::path::PathBuf>,
|
||||
#[cfg(feature = "std")]
|
||||
pub timestamp: Option<filetime::FileTime>,
|
||||
}
|
||||
|
||||
impl Default for Object {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
arch: ArchDummy::new(),
|
||||
endianness: object::Endianness::Little,
|
||||
symbols: vec![],
|
||||
sections: vec![],
|
||||
split_meta: None,
|
||||
#[cfg(feature = "std")]
|
||||
path: None,
|
||||
#[cfg(feature = "std")]
|
||||
timestamp: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Object {
|
||||
pub fn resolve_instruction_ref(
|
||||
&self,
|
||||
symbol_index: usize,
|
||||
ins_ref: InstructionRef,
|
||||
) -> Option<ResolvedInstructionRef> {
|
||||
let symbol = self.symbols.get(symbol_index)?;
|
||||
let section_index = symbol.section?;
|
||||
let section = self.sections.get(section_index)?;
|
||||
let offset = ins_ref.address.checked_sub(section.address)?;
|
||||
let code = section.data.get(offset as usize..offset as usize + ins_ref.size as usize)?;
|
||||
let relocation = section.relocation_at(self, ins_ref);
|
||||
Some(ResolvedInstructionRef {
|
||||
ins_ref,
|
||||
symbol_index,
|
||||
symbol,
|
||||
section,
|
||||
section_index,
|
||||
code,
|
||||
relocation,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn symbol_data(&self, symbol_index: usize) -> Option<&[u8]> {
|
||||
let symbol = self.symbols.get(symbol_index)?;
|
||||
let section_index = symbol.section?;
|
||||
let section = self.sections.get(section_index)?;
|
||||
let offset = symbol.address.checked_sub(section.address)?;
|
||||
section.data.get(offset as usize..offset as usize + symbol.size as usize)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Relocation {
|
||||
pub flags: RelocationFlags,
|
||||
pub address: u64,
|
||||
pub target_symbol: usize,
|
||||
pub addend: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum RelocationFlags {
|
||||
Elf(u32),
|
||||
Coff(u16),
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ResolvedRelocation<'a> {
|
||||
pub relocation: &'a Relocation,
|
||||
pub symbol: &'a Symbol,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ResolvedInstructionRef<'obj> {
|
||||
pub ins_ref: InstructionRef,
|
||||
pub symbol_index: usize,
|
||||
pub symbol: &'obj Symbol,
|
||||
pub section_index: usize,
|
||||
pub section: &'obj Section,
|
||||
pub code: &'obj [u8],
|
||||
pub relocation: Option<ResolvedRelocation<'obj>>,
|
||||
}
|
||||
|
||||
static DUMMY_SYMBOL: Symbol = Symbol {
|
||||
name: String::new(),
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: SymbolKind::Unknown,
|
||||
section: None,
|
||||
flags: SymbolFlagSet::empty(),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
};
|
||||
|
||||
static DUMMY_SECTION: Section = Section {
|
||||
id: String::new(),
|
||||
name: String::new(),
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: SectionKind::Unknown,
|
||||
data: SectionData(Vec::new()),
|
||||
flags: SectionFlagSet::empty(),
|
||||
relocations: Vec::new(),
|
||||
line_info: BTreeMap::new(),
|
||||
virtual_address: None,
|
||||
};
|
||||
|
||||
impl Default for ResolvedInstructionRef<'_> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ins_ref: InstructionRef::default(),
|
||||
symbol_index: 0,
|
||||
symbol: &DUMMY_SYMBOL,
|
||||
section_index: 0,
|
||||
section: &DUMMY_SECTION,
|
||||
code: &[],
|
||||
relocation: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
963
objdiff-core/src/obj/read.rs
Normal file
@@ -0,0 +1,963 @@
|
||||
use alloc::{
|
||||
collections::BTreeMap,
|
||||
format,
|
||||
string::{String, ToString},
|
||||
vec::Vec,
|
||||
};
|
||||
use core::cmp::Ordering;
|
||||
|
||||
use anyhow::{bail, ensure, Context, Result};
|
||||
use object::{Object as _, ObjectSection as _, ObjectSymbol as _};
|
||||
|
||||
use crate::{
|
||||
arch::{new_arch, Arch},
|
||||
diff::DiffObjConfig,
|
||||
obj::{
|
||||
split_meta::{SplitMeta, SPLITMETA_SECTION},
|
||||
Object, Relocation, RelocationFlags, Section, SectionData, SectionFlag, SectionKind,
|
||||
Symbol, SymbolFlag, SymbolKind,
|
||||
},
|
||||
util::{read_u16, read_u32},
|
||||
};
|
||||
|
||||
fn map_section_kind(section: &object::Section) -> SectionKind {
|
||||
match section.kind() {
|
||||
object::SectionKind::Text => SectionKind::Code,
|
||||
object::SectionKind::Data | object::SectionKind::ReadOnlyData => SectionKind::Data,
|
||||
object::SectionKind::UninitializedData => SectionKind::Bss,
|
||||
_ => SectionKind::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_symbol(
|
||||
arch: &dyn Arch,
|
||||
file: &object::File,
|
||||
symbol: &object::Symbol,
|
||||
section_indices: &[usize],
|
||||
split_meta: Option<&SplitMeta>,
|
||||
) -> Result<Symbol> {
|
||||
let mut name = symbol.name().context("Failed to process symbol name")?.to_string();
|
||||
let mut size = symbol.size();
|
||||
if let (object::SymbolKind::Section, Some(section)) =
|
||||
(symbol.kind(), symbol.section_index().and_then(|i| file.section_by_index(i).ok()))
|
||||
{
|
||||
let section_name = section.name().context("Failed to process section name")?;
|
||||
name = format!("[{}]", section_name);
|
||||
// For section symbols, set the size to zero. If the size is non-zero, it will be included
|
||||
// in the diff. Most of the time, this is duplicative, given that we'll have function or
|
||||
// object symbols that cover the same range. In the case of an empty section, the size
|
||||
// inference logic below will set the size back to the section size, thus acting as a
|
||||
// placeholder symbol.
|
||||
size = 0;
|
||||
}
|
||||
|
||||
let mut flags = arch.extra_symbol_flags(symbol);
|
||||
if symbol.is_global() {
|
||||
flags |= SymbolFlag::Global;
|
||||
}
|
||||
if symbol.is_local() {
|
||||
flags |= SymbolFlag::Local;
|
||||
}
|
||||
if symbol.is_common() {
|
||||
flags |= SymbolFlag::Common;
|
||||
}
|
||||
if symbol.is_weak() {
|
||||
flags |= SymbolFlag::Weak;
|
||||
}
|
||||
if file.format() == object::BinaryFormat::Elf && symbol.scope() == object::SymbolScope::Linkage
|
||||
{
|
||||
flags |= SymbolFlag::Hidden;
|
||||
}
|
||||
|
||||
let kind = match symbol.kind() {
|
||||
object::SymbolKind::Text => SymbolKind::Function,
|
||||
object::SymbolKind::Data => SymbolKind::Object,
|
||||
object::SymbolKind::Section => SymbolKind::Section,
|
||||
_ => SymbolKind::Unknown,
|
||||
};
|
||||
let address = arch.symbol_address(symbol.address(), kind);
|
||||
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 section = symbol.section_index().and_then(|i| section_indices.get(i.0).copied());
|
||||
|
||||
Ok(Symbol {
|
||||
name,
|
||||
demangled_name,
|
||||
address,
|
||||
size,
|
||||
kind,
|
||||
section,
|
||||
flags,
|
||||
align: None, // TODO parse .comment
|
||||
virtual_address,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_symbols(
|
||||
arch: &dyn Arch,
|
||||
obj_file: &object::File,
|
||||
sections: &[Section],
|
||||
section_indices: &[usize],
|
||||
split_meta: Option<&SplitMeta>,
|
||||
) -> Result<(Vec<Symbol>, Vec<usize>)> {
|
||||
let symbol_count = obj_file.symbols().count();
|
||||
let mut symbols = Vec::<Symbol>::with_capacity(symbol_count);
|
||||
let mut symbol_indices = Vec::<usize>::with_capacity(symbol_count + 1);
|
||||
for obj_symbol in obj_file.symbols() {
|
||||
if symbol_indices.len() <= obj_symbol.index().0 {
|
||||
symbol_indices.resize(obj_symbol.index().0 + 1, usize::MAX);
|
||||
}
|
||||
let symbol = map_symbol(arch, obj_file, &obj_symbol, section_indices, split_meta)?;
|
||||
symbol_indices[obj_symbol.index().0] = symbols.len();
|
||||
symbols.push(symbol);
|
||||
}
|
||||
|
||||
// Infer symbol sizes for 0-size symbols
|
||||
infer_symbol_sizes(&mut symbols, sections);
|
||||
|
||||
Ok((symbols, symbol_indices))
|
||||
}
|
||||
|
||||
fn infer_symbol_sizes(symbols: &mut [Symbol], sections: &[Section]) {
|
||||
// Create a sorted list of symbol indices by section
|
||||
let mut symbols_with_section = Vec::<usize>::with_capacity(symbols.len());
|
||||
for (i, symbol) in symbols.iter().enumerate() {
|
||||
if symbol.section.is_some() {
|
||||
symbols_with_section.push(i);
|
||||
}
|
||||
}
|
||||
symbols_with_section.sort_by(|a, b| {
|
||||
let a = &symbols[*a];
|
||||
let b = &symbols[*b];
|
||||
a.section
|
||||
.unwrap_or(usize::MAX)
|
||||
.cmp(&b.section.unwrap_or(usize::MAX))
|
||||
.then_with(|| {
|
||||
// Sort section symbols first
|
||||
if a.kind == SymbolKind::Section {
|
||||
Ordering::Less
|
||||
} else if b.kind == SymbolKind::Section {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
})
|
||||
.then_with(|| a.address.cmp(&b.address))
|
||||
.then_with(|| a.size.cmp(&b.size))
|
||||
});
|
||||
|
||||
// Set symbol sizes based on the next symbol's address
|
||||
let mut iter_idx = 0;
|
||||
let mut last_end = (0, 0);
|
||||
while iter_idx < symbols_with_section.len() {
|
||||
let symbol_idx = symbols_with_section[iter_idx];
|
||||
let symbol = &symbols[symbol_idx];
|
||||
let section_idx = symbol.section.unwrap();
|
||||
iter_idx += 1;
|
||||
if symbol.size != 0 {
|
||||
if symbol.kind != SymbolKind::Section {
|
||||
last_end = (section_idx, symbol.address + symbol.size);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Skip over symbols that are contained within the previous symbol
|
||||
if last_end.0 == section_idx && last_end.1 > symbol.address {
|
||||
continue;
|
||||
}
|
||||
let next_symbol = match symbol.kind {
|
||||
// For function/object symbols, find the next function/object symbol (in other words:
|
||||
// skip over labels)
|
||||
SymbolKind::Function | SymbolKind::Object => loop {
|
||||
if iter_idx >= symbols_with_section.len() {
|
||||
break None;
|
||||
}
|
||||
let next_symbol = &symbols[symbols_with_section[iter_idx]];
|
||||
if next_symbol.section != Some(section_idx) {
|
||||
break None;
|
||||
}
|
||||
if let SymbolKind::Function | SymbolKind::Object = next_symbol.kind {
|
||||
break Some(next_symbol);
|
||||
}
|
||||
iter_idx += 1;
|
||||
},
|
||||
// For labels (or anything else), simply use the next symbol's address
|
||||
SymbolKind::Unknown | SymbolKind::Section => symbols_with_section
|
||||
.get(iter_idx)
|
||||
.map(|&i| &symbols[i])
|
||||
.take_if(|s| s.section == Some(section_idx)),
|
||||
};
|
||||
let next_address = next_symbol.map(|s| s.address).unwrap_or_else(|| {
|
||||
let section = §ions[section_idx];
|
||||
section.address + section.size
|
||||
});
|
||||
let new_size = next_address.saturating_sub(symbol.address);
|
||||
if new_size > 0 {
|
||||
let symbol = &mut symbols[symbol_idx];
|
||||
symbol.size = new_size;
|
||||
if symbol.kind != SymbolKind::Section {
|
||||
symbol.flags |= SymbolFlag::SizeInferred;
|
||||
}
|
||||
// Set symbol kind if unknown and size is non-zero
|
||||
if symbol.kind == SymbolKind::Unknown {
|
||||
symbol.kind = match sections[section_idx].kind {
|
||||
SectionKind::Code => SymbolKind::Function,
|
||||
SectionKind::Data | SectionKind::Bss => SymbolKind::Object,
|
||||
_ => SymbolKind::Unknown,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn map_sections(
|
||||
_arch: &dyn Arch,
|
||||
obj_file: &object::File,
|
||||
split_meta: Option<&SplitMeta>,
|
||||
) -> Result<(Vec<Section>, Vec<usize>)> {
|
||||
let mut section_names = BTreeMap::<String, usize>::new();
|
||||
let section_count = obj_file.sections().count();
|
||||
let mut result = Vec::<Section>::with_capacity(section_count);
|
||||
let mut section_indices = Vec::<usize>::with_capacity(section_count + 1);
|
||||
for section in obj_file.sections() {
|
||||
let name = section.name().context("Failed to process section name")?;
|
||||
let kind = map_section_kind(§ion);
|
||||
let data = if kind == SectionKind::Unknown {
|
||||
// Don't need to read data for unknown sections
|
||||
Vec::new()
|
||||
} else {
|
||||
section.uncompressed_data().context("Failed to read section data")?.into_owned()
|
||||
};
|
||||
|
||||
// Find the virtual address for the section symbol if available
|
||||
let section_symbol = obj_file.symbols().find(|s| {
|
||||
s.kind() == object::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())
|
||||
});
|
||||
|
||||
let unique_id = section_names.entry(name.to_string()).or_insert(0);
|
||||
let id = format!("{}-{}", name, unique_id);
|
||||
*unique_id += 1;
|
||||
|
||||
if section_indices.len() <= section.index().0 {
|
||||
section_indices.resize(section.index().0 + 1, usize::MAX);
|
||||
}
|
||||
section_indices[section.index().0] = result.len();
|
||||
result.push(Section {
|
||||
id,
|
||||
name: name.to_string(),
|
||||
address: section.address(),
|
||||
size: section.size(),
|
||||
kind,
|
||||
data: SectionData(data),
|
||||
flags: Default::default(),
|
||||
relocations: Default::default(),
|
||||
virtual_address,
|
||||
line_info: Default::default(),
|
||||
});
|
||||
}
|
||||
Ok((result, section_indices))
|
||||
}
|
||||
|
||||
const LOW_PRIORITY_SYMBOLS: &[&str] =
|
||||
&["__gnu_compiled_c", "__gnu_compiled_cplusplus", "gcc2_compiled."];
|
||||
|
||||
fn best_symbol<'r, 'data, 'file>(
|
||||
symbols: &'r [object::Symbol<'data, 'file>],
|
||||
address: u64,
|
||||
) -> Option<(object::SymbolIndex, u64)> {
|
||||
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 object::Symbol<'data, 'file>> = None;
|
||||
for symbol in symbols.iter().skip(closest_symbol_index) {
|
||||
if symbol.address() > address {
|
||||
break;
|
||||
}
|
||||
if symbol.kind() == object::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.map(|s| (s.index(), s.address()))
|
||||
}
|
||||
|
||||
fn map_section_relocations(
|
||||
arch: &dyn Arch,
|
||||
obj_file: &object::File,
|
||||
obj_section: &object::Section,
|
||||
symbol_indices: &[usize],
|
||||
ordered_symbols: &[Vec<object::Symbol>],
|
||||
) -> Result<Vec<Relocation>> {
|
||||
let mut relocations = Vec::<Relocation>::with_capacity(obj_section.relocations().count());
|
||||
for (address, reloc) in obj_section.relocations() {
|
||||
let flags = match reloc.flags() {
|
||||
object::RelocationFlags::Elf { r_type } => RelocationFlags::Elf(r_type),
|
||||
object::RelocationFlags::Coff { typ } => RelocationFlags::Coff(typ),
|
||||
flags => {
|
||||
bail!("Unhandled relocation flags: {:?}", flags);
|
||||
}
|
||||
};
|
||||
// TODO validate reloc here?
|
||||
let mut addend = if reloc.has_implicit_addend() {
|
||||
arch.implcit_addend(obj_file, obj_section, address, &reloc, flags)?
|
||||
} else {
|
||||
reloc.addend()
|
||||
};
|
||||
let target_symbol = match reloc.target() {
|
||||
object::RelocationTarget::Symbol(idx) => {
|
||||
if idx.0 == u32::MAX as usize {
|
||||
// ???
|
||||
continue;
|
||||
}
|
||||
// If the target is a section symbol, try to resolve a better symbol as the target
|
||||
let idx = if let Some(section_symbol) = obj_file
|
||||
.symbol_by_index(idx)
|
||||
.ok()
|
||||
.take_if(|s| s.kind() == object::SymbolKind::Section)
|
||||
{
|
||||
let section_index =
|
||||
section_symbol.section_index().context("Section symbol without section")?;
|
||||
let target_address = section_symbol.address().wrapping_add_signed(addend);
|
||||
if let Some((new_idx, addr)) = ordered_symbols
|
||||
.get(section_index.0)
|
||||
.and_then(|symbols| best_symbol(symbols, target_address))
|
||||
{
|
||||
addend = target_address.wrapping_sub(addr) as i64;
|
||||
new_idx
|
||||
} else {
|
||||
idx
|
||||
}
|
||||
} else {
|
||||
idx
|
||||
};
|
||||
match symbol_indices.get(idx.0).copied() {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
log::warn!("Invalid symbol index {}", idx.0);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
object::RelocationTarget::Absolute => {
|
||||
let section_name = obj_section.name()?;
|
||||
log::warn!("Ignoring absolute relocation @ {}:{:#x}", section_name, address);
|
||||
continue;
|
||||
}
|
||||
_ => bail!("Unhandled relocation target: {:?}", reloc.target()),
|
||||
};
|
||||
relocations.push(Relocation { address, flags, target_symbol, addend });
|
||||
}
|
||||
relocations.sort_by_key(|r| r.address);
|
||||
Ok(relocations)
|
||||
}
|
||||
|
||||
fn map_relocations(
|
||||
arch: &dyn Arch,
|
||||
obj_file: &object::File,
|
||||
sections: &mut [Section],
|
||||
section_indices: &[usize],
|
||||
symbol_indices: &[usize],
|
||||
) -> Result<()> {
|
||||
// Generate a list of symbols for each section
|
||||
let mut ordered_symbols =
|
||||
Vec::<Vec<object::Symbol>>::with_capacity(obj_file.sections().count() + 1);
|
||||
for symbol in obj_file.symbols() {
|
||||
let Some(section_index) = symbol.section_index() else {
|
||||
continue;
|
||||
};
|
||||
if symbol.kind() == object::SymbolKind::Section {
|
||||
continue;
|
||||
}
|
||||
if section_index.0 >= ordered_symbols.len() {
|
||||
ordered_symbols.resize_with(section_index.0 + 1, Vec::new);
|
||||
}
|
||||
ordered_symbols[section_index.0].push(symbol);
|
||||
}
|
||||
// Sort symbols by address and size
|
||||
for vec in &mut ordered_symbols {
|
||||
vec.sort_by(|a, b| a.address().cmp(&b.address()).then(a.size().cmp(&b.size())));
|
||||
}
|
||||
// Map relocations for each section. Section-relative relocations use the ordered symbols list
|
||||
// to find a better target symbol, if available.
|
||||
for obj_section in obj_file.sections() {
|
||||
let section = &mut sections[section_indices[obj_section.index().0]];
|
||||
if section.kind != SectionKind::Unknown {
|
||||
section.relocations = map_section_relocations(
|
||||
arch,
|
||||
obj_file,
|
||||
&obj_section,
|
||||
symbol_indices,
|
||||
&ordered_symbols,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_line_info(
|
||||
obj_file: &object::File,
|
||||
sections: &mut [Section],
|
||||
section_indices: &[usize],
|
||||
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: &[u8] = data.as_ref();
|
||||
|
||||
let mut text_sections = sections.iter_mut().filter(|s| s.kind == SectionKind::Code);
|
||||
while !reader.is_empty() {
|
||||
let mut section_data = reader;
|
||||
let size = read_u32(obj_file, &mut section_data)? as usize;
|
||||
if size > reader.len() {
|
||||
bail!("Line info size {size} exceeds remaining size {}", reader.len());
|
||||
}
|
||||
(section_data, reader) = reader.split_at(size);
|
||||
|
||||
section_data = §ion_data[4..]; // Skip the size field
|
||||
let base_address = read_u32(obj_file, &mut section_data)? as u64;
|
||||
let out_section = text_sections.next().context("No text section for line info")?;
|
||||
while !section_data.is_empty() {
|
||||
let line_number = read_u32(obj_file, &mut section_data)?;
|
||||
let statement_pos = read_u16(obj_file, &mut section_data)?;
|
||||
if statement_pos != 0xFFFF {
|
||||
log::warn!("Unhandled statement pos {}", statement_pos);
|
||||
}
|
||||
let address_delta = read_u32(obj_file, &mut section_data)? as u64;
|
||||
out_section.line_info.insert(base_address + address_delta, line_number);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DWARF 2+
|
||||
#[cfg(feature = "dwarf")]
|
||||
{
|
||||
fn gimli_error(e: gimli::Error) -> anyhow::Error { anyhow::anyhow!("DWARF error: {e:?}") }
|
||||
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(alloc::borrow::Cow::Borrowed(&[][..])),
|
||||
)
|
||||
})
|
||||
.map_err(gimli_error)?;
|
||||
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().map_err(gimli_error)? {
|
||||
let unit = dwarf.unit(header).map_err(gimli_error)?;
|
||||
if let Some(program) = unit.line_program.clone() {
|
||||
let mut text_sections = sections.iter_mut().filter(|s| s.kind == SectionKind::Code);
|
||||
let mut lines = text_sections.next().map(|section| &mut section.line_info);
|
||||
|
||||
let mut rows = program.rows();
|
||||
while let Some((_header, row)) = rows.next_row().map_err(gimli_error)? {
|
||||
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.
|
||||
lines = text_sections.next().map(|section| &mut section.line_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if iter.next().map_err(gimli_error)?.is_some() {
|
||||
log::warn!("Multiple units found in DWARF data, only processing the first");
|
||||
}
|
||||
}
|
||||
|
||||
// COFF
|
||||
if let object::File::Coff(coff) = obj_file {
|
||||
parse_line_info_coff(coff, sections, section_indices, obj_data)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_line_info_coff(
|
||||
coff: &object::coff::CoffFile,
|
||||
sections: &mut [Section],
|
||||
section_indices: &[usize],
|
||||
obj_data: &[u8],
|
||||
) -> Result<()> {
|
||||
use object::{
|
||||
coff::{CoffHeader as _, ImageSymbol as _},
|
||||
endian::LittleEndian as LE,
|
||||
};
|
||||
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) =
|
||||
section_indices.get(sect.index().0).and_then(|&i| sections.get_mut(i))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Turn the line numbers into an ImageLinenumber slice.
|
||||
let Some(linenums) = &obj_data.get(
|
||||
ptr_linenums..ptr_linenums + num_linenums * size_of::<object::pe::ImageLinenumber>(),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(linenums) =
|
||||
object::pod::slice_from_all_bytes::<object::pe::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(object::SymbolIndex(symtable_entry as usize))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Ok(aux_fun) =
|
||||
symbol_table.aux_function(object::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(object::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::<object::pe::ImageAuxSymbolFunctionBeginEnd>(
|
||||
object::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 combine_sections(
|
||||
sections: &mut [Section],
|
||||
symbols: &mut [Symbol],
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<()> {
|
||||
let mut data_sections = BTreeMap::<String, Vec<usize>>::new();
|
||||
let mut text_sections = Vec::<usize>::new();
|
||||
for (i, section) in sections.iter().enumerate() {
|
||||
match section.kind {
|
||||
SectionKind::Data | SectionKind::Bss => {
|
||||
let base_name = if let Some(i) = section.name.rfind('$') {
|
||||
§ion.name[..i]
|
||||
} else {
|
||||
§ion.name
|
||||
};
|
||||
data_sections.entry(base_name.to_string()).or_default().push(i);
|
||||
}
|
||||
SectionKind::Code => {
|
||||
text_sections.push(i);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if config.combine_data_sections {
|
||||
for (combined_name, mut section_indices) in data_sections {
|
||||
do_combine_sections(sections, symbols, &mut section_indices, combined_name)?;
|
||||
}
|
||||
}
|
||||
if config.combine_text_sections {
|
||||
do_combine_sections(sections, symbols, &mut text_sections, ".text".to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_combine_sections(
|
||||
sections: &mut [Section],
|
||||
symbols: &mut [Symbol],
|
||||
section_indices: &mut [usize],
|
||||
combined_name: String,
|
||||
) -> Result<()> {
|
||||
if section_indices.len() < 2 {
|
||||
return Ok(());
|
||||
}
|
||||
// Sort sections lexicographically by name (for COFF section groups)
|
||||
section_indices.sort_by(|&a, &b| {
|
||||
let a_name = §ions[a].name;
|
||||
let b_name = §ions[b].name;
|
||||
// .text$di < .text$mn < .text
|
||||
if a_name.contains('$') && !b_name.contains('$') {
|
||||
return Ordering::Less;
|
||||
} else if !a_name.contains('$') && b_name.contains('$') {
|
||||
return Ordering::Greater;
|
||||
}
|
||||
a_name.cmp(b_name)
|
||||
});
|
||||
let first_section_idx = section_indices[0];
|
||||
|
||||
// Calculate the new offset for each section
|
||||
let mut offsets = Vec::<u64>::with_capacity(section_indices.len());
|
||||
let mut current_offset = 0;
|
||||
let mut data_size = 0;
|
||||
let mut num_relocations = 0;
|
||||
for i in section_indices.iter().copied() {
|
||||
let section = §ions[i];
|
||||
if section.address != 0 {
|
||||
bail!("Section {} ({}) has non-zero address", i, section.name);
|
||||
}
|
||||
offsets.push(current_offset);
|
||||
current_offset += section.size;
|
||||
data_size += section.data.len();
|
||||
num_relocations += section.relocations.len();
|
||||
}
|
||||
if data_size > 0 {
|
||||
ensure!(data_size == current_offset as usize, "Data size mismatch");
|
||||
}
|
||||
|
||||
// Combine section data
|
||||
let mut data = Vec::<u8>::with_capacity(data_size);
|
||||
let mut relocations = Vec::<Relocation>::with_capacity(num_relocations);
|
||||
let mut line_info = BTreeMap::<u64, u32>::new();
|
||||
for (&i, &offset) in section_indices.iter().zip(&offsets) {
|
||||
let section = &mut sections[i];
|
||||
section.size = 0;
|
||||
data.append(&mut section.data.0);
|
||||
section.relocations.iter_mut().for_each(|r| r.address += offset);
|
||||
relocations.append(&mut section.relocations);
|
||||
line_info.append(&mut section.line_info.iter().map(|(&a, &l)| (a + offset, l)).collect());
|
||||
section.line_info.clear();
|
||||
if offset > 0 {
|
||||
section.flags |= SectionFlag::Hidden;
|
||||
}
|
||||
}
|
||||
{
|
||||
let first_section = &mut sections[first_section_idx];
|
||||
first_section.id = format!("{combined_name}-combined");
|
||||
first_section.name = combined_name;
|
||||
first_section.size = current_offset;
|
||||
first_section.data = SectionData(data);
|
||||
first_section.flags |= SectionFlag::Combined;
|
||||
first_section.relocations = relocations;
|
||||
first_section.line_info = line_info;
|
||||
}
|
||||
|
||||
// Find all section symbols for the merged sections
|
||||
let mut section_symbols = symbols
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|&(_, s)| {
|
||||
s.kind == SymbolKind::Section && s.section.is_some_and(|i| section_indices.contains(&i))
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
.collect::<Vec<_>>();
|
||||
section_symbols.sort_by_key(|&i| symbols[i].section.unwrap());
|
||||
let target_section_symbol = section_symbols.first().copied();
|
||||
|
||||
// Adjust symbol addresses and section indices
|
||||
for symbol in symbols.iter_mut() {
|
||||
let Some(section_index) = symbol.section else {
|
||||
continue;
|
||||
};
|
||||
let Some(merge_index) = section_indices.iter().position(|&i| i == section_index) else {
|
||||
continue;
|
||||
};
|
||||
symbol.address += offsets[merge_index];
|
||||
symbol.section = Some(first_section_idx);
|
||||
}
|
||||
|
||||
// Adjust relocations to section symbols
|
||||
for relocation in sections.iter_mut().flat_map(|s| s.relocations.iter_mut()) {
|
||||
let target_symbol = &symbols[relocation.target_symbol];
|
||||
if target_symbol.kind != SymbolKind::Section {
|
||||
continue;
|
||||
}
|
||||
if !target_symbol.section.is_some_and(|i| section_indices.contains(&i)) {
|
||||
continue;
|
||||
}
|
||||
// The section symbol's address will have the offset applied
|
||||
relocation.target_symbol = target_section_symbol.context("No target section symbol")?;
|
||||
relocation.addend = relocation
|
||||
.addend
|
||||
.checked_add_unsigned(target_symbol.address)
|
||||
.context("Relocation addend overflow")?;
|
||||
}
|
||||
|
||||
// Reset section symbols
|
||||
for (i, &symbol_index) in section_symbols.iter().enumerate() {
|
||||
let symbol = &mut symbols[symbol_index];
|
||||
symbol.address = 0;
|
||||
if i > 0 {
|
||||
// Remove the section symbol
|
||||
symbol.kind = SymbolKind::Unknown;
|
||||
symbol.section = None;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub fn read(obj_path: &std::path::Path, config: &DiffObjConfig) -> Result<Object> {
|
||||
let (data, timestamp) = {
|
||||
let file = std::fs::File::open(obj_path)?;
|
||||
let timestamp = filetime::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_path_buf());
|
||||
obj.timestamp = Some(timestamp);
|
||||
Ok(obj)
|
||||
}
|
||||
|
||||
pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result<Object> {
|
||||
let obj_file = object::File::parse(data)?;
|
||||
let arch = new_arch(&obj_file)?;
|
||||
let split_meta = parse_split_meta(&obj_file)?;
|
||||
let (mut sections, section_indices) =
|
||||
map_sections(arch.as_ref(), &obj_file, split_meta.as_ref())?;
|
||||
let (mut symbols, symbol_indices) =
|
||||
map_symbols(arch.as_ref(), &obj_file, §ions, §ion_indices, split_meta.as_ref())?;
|
||||
map_relocations(arch.as_ref(), &obj_file, &mut sections, §ion_indices, &symbol_indices)?;
|
||||
parse_line_info(&obj_file, &mut sections, §ion_indices, data)?;
|
||||
if config.combine_data_sections || config.combine_text_sections {
|
||||
combine_sections(&mut sections, &mut symbols, config)?;
|
||||
}
|
||||
Ok(Object {
|
||||
arch,
|
||||
endianness: obj_file.endianness(),
|
||||
symbols,
|
||||
sections,
|
||||
split_meta,
|
||||
#[cfg(feature = "std")]
|
||||
path: None,
|
||||
#[cfg(feature = "std")]
|
||||
timestamp: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub fn has_function(obj_path: &std::path::Path, symbol_name: &str) -> Result<bool> {
|
||||
let data = {
|
||||
let file = std::fs::File::open(obj_path)?;
|
||||
unsafe { memmap2::Mmap::map(&file) }?
|
||||
};
|
||||
Ok(object::File::parse(&*data)?
|
||||
.symbol_by_name(symbol_name)
|
||||
.filter(|o| o.kind() == object::SymbolKind::Text)
|
||||
.is_some())
|
||||
}
|
||||
|
||||
fn parse_split_meta(obj_file: &object::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
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_combine_sections() {
|
||||
let mut sections = vec![
|
||||
Section {
|
||||
id: ".text-0".to_string(),
|
||||
name: ".text".to_string(),
|
||||
size: 8,
|
||||
kind: SectionKind::Code,
|
||||
data: SectionData(vec![0; 8]),
|
||||
relocations: vec![
|
||||
Relocation {
|
||||
address: 0,
|
||||
flags: RelocationFlags::Elf(0),
|
||||
target_symbol: 0,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
address: 2,
|
||||
flags: RelocationFlags::Elf(0),
|
||||
target_symbol: 1,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
address: 4,
|
||||
flags: RelocationFlags::Elf(0),
|
||||
target_symbol: 3,
|
||||
addend: 2,
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
Section {
|
||||
id: ".data-0".to_string(),
|
||||
name: ".data".to_string(),
|
||||
size: 4,
|
||||
kind: SectionKind::Data,
|
||||
data: SectionData(vec![1, 2, 3, 4]),
|
||||
relocations: vec![Relocation {
|
||||
address: 0,
|
||||
flags: RelocationFlags::Elf(0),
|
||||
target_symbol: 2,
|
||||
addend: 0,
|
||||
}],
|
||||
line_info: [(0, 1)].into_iter().collect(),
|
||||
..Default::default()
|
||||
},
|
||||
Section {
|
||||
id: ".data-1".to_string(),
|
||||
name: ".data".to_string(),
|
||||
size: 4,
|
||||
kind: SectionKind::Data,
|
||||
data: SectionData(vec![5, 6, 7, 8]),
|
||||
relocations: vec![Relocation {
|
||||
address: 0,
|
||||
flags: RelocationFlags::Elf(0),
|
||||
target_symbol: 2,
|
||||
addend: 0,
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
Section {
|
||||
id: ".data-2".to_string(),
|
||||
name: ".data".to_string(),
|
||||
size: 4,
|
||||
kind: SectionKind::Data,
|
||||
data: SectionData(vec![9, 10, 11, 12]),
|
||||
line_info: [(0, 2)].into_iter().collect(),
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
let mut symbols = vec![
|
||||
Symbol {
|
||||
name: ".data".to_string(),
|
||||
address: 0,
|
||||
kind: SymbolKind::Section,
|
||||
section: Some(2),
|
||||
..Default::default()
|
||||
},
|
||||
Symbol {
|
||||
name: "symbol".to_string(),
|
||||
address: 0,
|
||||
kind: SymbolKind::Object,
|
||||
size: 4,
|
||||
section: Some(2),
|
||||
..Default::default()
|
||||
},
|
||||
Symbol {
|
||||
name: "function".to_string(),
|
||||
address: 0,
|
||||
size: 8,
|
||||
kind: SymbolKind::Function,
|
||||
section: Some(0),
|
||||
..Default::default()
|
||||
},
|
||||
Symbol {
|
||||
name: ".data".to_string(),
|
||||
address: 0,
|
||||
kind: SymbolKind::Section,
|
||||
section: Some(3),
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
do_combine_sections(&mut sections, &mut symbols, &mut [1, 2, 3], ".data".to_string())
|
||||
.unwrap();
|
||||
assert_eq!(sections[1].data.0, (1..=12).collect::<Vec<_>>());
|
||||
insta::assert_debug_snapshot!((sections, symbols));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
---
|
||||
source: objdiff-core/src/obj/read.rs
|
||||
expression: "(sections, symbols)"
|
||||
---
|
||||
(
|
||||
[
|
||||
Section {
|
||||
id: ".text-0",
|
||||
name: ".text",
|
||||
address: 0,
|
||||
size: 8,
|
||||
kind: Code,
|
||||
data: SectionData(
|
||||
8,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
0,
|
||||
),
|
||||
address: 0,
|
||||
target_symbol: 0,
|
||||
addend: 4,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
0,
|
||||
),
|
||||
address: 2,
|
||||
target_symbol: 1,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
0,
|
||||
),
|
||||
address: 4,
|
||||
target_symbol: 0,
|
||||
addend: 10,
|
||||
},
|
||||
],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".data-combined",
|
||||
name: ".data",
|
||||
address: 0,
|
||||
size: 12,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
12,
|
||||
),
|
||||
flags: FlagSet(Combined),
|
||||
relocations: [
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
0,
|
||||
),
|
||||
address: 0,
|
||||
target_symbol: 2,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
0,
|
||||
),
|
||||
address: 4,
|
||||
target_symbol: 2,
|
||||
addend: 0,
|
||||
},
|
||||
],
|
||||
line_info: {
|
||||
0: 1,
|
||||
8: 2,
|
||||
},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".data-1",
|
||||
name: ".data",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".data-2",
|
||||
name: ".data",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
],
|
||||
[
|
||||
Symbol {
|
||||
name: ".data",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Section,
|
||||
section: Some(
|
||||
1,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "symbol",
|
||||
demangled_name: None,
|
||||
address: 4,
|
||||
size: 4,
|
||||
kind: Object,
|
||||
section: Some(
|
||||
1,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "function",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 8,
|
||||
kind: Function,
|
||||
section: Some(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: ".data",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Unknown,
|
||||
section: None,
|
||||
flags: FlagSet(),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
],
|
||||
)
|
||||
225
objdiff-core/src/obj/split_meta.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
use alloc::{string::String, vec, vec::Vec};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
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) -> Result<Self>
|
||||
where E: Endian {
|
||||
let mut result = SplitMeta::default();
|
||||
let data = section.uncompressed_data().map_err(object_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(anyhow::Error::new)?;
|
||||
result.generator = Some(string);
|
||||
}
|
||||
NT_SPLIT_MODULE_NAME => {
|
||||
let string =
|
||||
String::from_utf8(note.desc.to_vec()).map_err(anyhow::Error::new)?;
|
||||
result.module_name = Some(string);
|
||||
}
|
||||
NT_SPLIT_MODULE_ID => {
|
||||
result.module_id = Some(e.read_u32_bytes(
|
||||
note.desc.try_into().map_err(|_| anyhow!("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)
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub fn to_writer<E, W>(&self, writer: &mut W, e: E, is_64: bool) -> std::io::Result<()>
|
||||
where
|
||||
E: Endian,
|
||||
W: std::io::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 a String.
|
||||
#[inline]
|
||||
fn object_error(err: object::read::Error) -> anyhow::Error { anyhow::Error::new(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) -> Result<Self> {
|
||||
Ok(if is_64 {
|
||||
NoteIterator::B64(
|
||||
object::read::elf::NoteIterator::new(e, align, data).map_err(object_error)?,
|
||||
)
|
||||
} else {
|
||||
NoteIterator::B32(
|
||||
object::read::elf::NoteIterator::new(e, align as u32, data)
|
||||
.map_err(object_error)?,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn next(&mut self, e: E) -> Result<Option<Note<'data>>> {
|
||||
match self {
|
||||
NoteIterator::B32(iter) => Ok(iter.next().map_err(object_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_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 }
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
fn align_data_to_4<W: std::io::Write + ?Sized>(writer: &mut W, len: usize) -> std::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);
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
fn write_note_header<E, W>(writer: &mut W, e: E, kind: u32, desc_len: usize) -> std::io::Result<()>
|
||||
where
|
||||
E: Endian,
|
||||
W: std::io::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(())
|
||||
}
|
||||
41
objdiff-core/src/util.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use alloc::format;
|
||||
use core::fmt;
|
||||
|
||||
use anyhow::{ensure, Result};
|
||||
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 N);
|
||||
|
||||
impl<N: PrimInt> fmt::LowerHex for ReallySigned<N> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> 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> fmt::UpperHex for ReallySigned<N> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> 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(obj_file: &object::File, reader: &mut &[u8]) -> Result<u32> {
|
||||
ensure!(reader.len() >= 4, "Not enough bytes to read u32");
|
||||
let value = u32::from_ne_bytes(reader[..4].try_into()?);
|
||||
*reader = &reader[4..];
|
||||
Ok(obj_file.endianness().read_u32(value))
|
||||
}
|
||||
|
||||
pub fn read_u16(obj_file: &object::File, reader: &mut &[u8]) -> Result<u16> {
|
||||
ensure!(reader.len() >= 2, "Not enough bytes to read u16");
|
||||
let value = u16::from_ne_bytes(reader[..2].try_into()?);
|
||||
*reader = &reader[2..];
|
||||
Ok(obj_file.endianness().read_u16(value))
|
||||
}
|
||||
17
objdiff-core/tests/arch_arm.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use objdiff_core::{diff, obj};
|
||||
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "arm")]
|
||||
fn read_arm() {
|
||||
let diff_config = diff::DiffObjConfig { mips_register_prefix: true, ..Default::default() };
|
||||
let obj = obj::read::parse(include_object!("data/arm/LinkStateItem.o"), &diff_config).unwrap();
|
||||
insta::assert_debug_snapshot!(obj);
|
||||
let symbol_idx =
|
||||
obj.symbols.iter().position(|s| s.name == "_ZN13LinkStateItem12OnStateLeaveEi").unwrap();
|
||||
let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap();
|
||||
insta::assert_debug_snapshot!(diff.instruction_rows);
|
||||
let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config);
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
16
objdiff-core/tests/arch_mips.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use objdiff_core::{diff, obj};
|
||||
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "mips")]
|
||||
fn read_mips() {
|
||||
let diff_config = diff::DiffObjConfig { mips_register_prefix: true, ..Default::default() };
|
||||
let obj = obj::read::parse(include_object!("data/mips/main.c.o"), &diff_config).unwrap();
|
||||
insta::assert_debug_snapshot!(obj);
|
||||
let symbol_idx = obj.symbols.iter().position(|s| s.name == "ControlEntry").unwrap();
|
||||
let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap();
|
||||
insta::assert_debug_snapshot!(diff.instruction_rows);
|
||||
let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config);
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
87
objdiff-core/tests/arch_ppc.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use objdiff_core::{
|
||||
diff::{self, display},
|
||||
obj,
|
||||
obj::SectionKind,
|
||||
};
|
||||
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "ppc")]
|
||||
fn read_ppc() {
|
||||
let diff_config = diff::DiffObjConfig::default();
|
||||
let obj = obj::read::parse(include_object!("data/ppc/IObj.o"), &diff_config).unwrap();
|
||||
insta::assert_debug_snapshot!(obj);
|
||||
let symbol_idx =
|
||||
obj.symbols.iter().position(|s| s.name == "Type2Text__10SObjectTagFUi").unwrap();
|
||||
let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap();
|
||||
insta::assert_debug_snapshot!(diff.instruction_rows);
|
||||
let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config);
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "ppc")]
|
||||
fn read_dwarf1_line_info() {
|
||||
let diff_config = diff::DiffObjConfig::default();
|
||||
let obj = obj::read::parse(include_object!("data/ppc/m_Do_hostIO.o"), &diff_config).unwrap();
|
||||
let line_infos = obj
|
||||
.sections
|
||||
.iter()
|
||||
.filter(|s| s.kind == SectionKind::Code)
|
||||
.map(|s| s.line_info.clone())
|
||||
.collect::<Vec<_>>();
|
||||
insta::assert_debug_snapshot!(line_infos);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "ppc")]
|
||||
fn read_extab() {
|
||||
let diff_config = diff::DiffObjConfig::default();
|
||||
let obj = obj::read::parse(include_object!("data/ppc/NMWException.o"), &diff_config).unwrap();
|
||||
insta::assert_debug_snapshot!(obj);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "ppc")]
|
||||
fn diff_ppc() {
|
||||
let diff_config = diff::DiffObjConfig::default();
|
||||
let mapping_config = diff::MappingConfig::default();
|
||||
let target_obj =
|
||||
obj::read::parse(include_object!("data/ppc/CDamageVulnerability_target.o"), &diff_config)
|
||||
.unwrap();
|
||||
let base_obj =
|
||||
obj::read::parse(include_object!("data/ppc/CDamageVulnerability_base.o"), &diff_config)
|
||||
.unwrap();
|
||||
let diff =
|
||||
diff::diff_objs(Some(&target_obj), Some(&base_obj), None, &diff_config, &mapping_config)
|
||||
.unwrap();
|
||||
|
||||
let target_diff = diff.left.as_ref().unwrap();
|
||||
let base_diff = diff.right.as_ref().unwrap();
|
||||
let sections_display = display::display_sections(
|
||||
&target_obj,
|
||||
&target_diff,
|
||||
display::SymbolFilter::None,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
insta::assert_debug_snapshot!(sections_display);
|
||||
|
||||
let target_symbol_idx = target_obj
|
||||
.symbols
|
||||
.iter()
|
||||
.position(|s| s.name == "WeaponHurts__20CDamageVulnerabilityCFRC11CWeaponModei")
|
||||
.unwrap();
|
||||
let target_symbol_diff = &target_diff.symbols[target_symbol_idx];
|
||||
let base_symbol_idx = base_obj
|
||||
.symbols
|
||||
.iter()
|
||||
.position(|s| s.name == "WeaponHurts__20CDamageVulnerabilityCFRC11CWeaponModei")
|
||||
.unwrap();
|
||||
let base_symbol_diff = &base_diff.symbols[base_symbol_idx];
|
||||
assert_eq!(target_symbol_diff.target_symbol, Some(base_symbol_idx));
|
||||
assert_eq!(base_symbol_diff.target_symbol, Some(target_symbol_idx));
|
||||
insta::assert_debug_snapshot!((target_symbol_diff, base_symbol_diff));
|
||||
}
|
||||
28
objdiff-core/tests/arch_x86.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use objdiff_core::{diff, obj};
|
||||
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "x86")]
|
||||
fn read_x86() {
|
||||
let diff_config = diff::DiffObjConfig::default();
|
||||
let obj = obj::read::parse(include_object!("data/x86/staticdebug.obj"), &diff_config).unwrap();
|
||||
insta::assert_debug_snapshot!(obj);
|
||||
let symbol_idx = obj.symbols.iter().position(|s| s.name == "?PrintThing@@YAXXZ").unwrap();
|
||||
let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap();
|
||||
insta::assert_debug_snapshot!(diff.instruction_rows);
|
||||
let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config);
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "x86")]
|
||||
fn read_x86_combine_sections() {
|
||||
let diff_config = diff::DiffObjConfig {
|
||||
combine_data_sections: true,
|
||||
combine_text_sections: true,
|
||||
..Default::default()
|
||||
};
|
||||
let obj = obj::read::parse(include_object!("data/x86/rtest.obj"), &diff_config).unwrap();
|
||||
insta::assert_debug_snapshot!(obj.sections);
|
||||
}
|
||||
52
objdiff-core/tests/common.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use objdiff_core::{
|
||||
diff::{display::DiffTextSegment, DiffObjConfig, SymbolDiff},
|
||||
obj::Object,
|
||||
};
|
||||
|
||||
pub fn display_diff(
|
||||
obj: &Object,
|
||||
diff: &SymbolDiff,
|
||||
symbol_idx: usize,
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> String {
|
||||
let mut output = String::new();
|
||||
for row in &diff.instruction_rows {
|
||||
output.push('[');
|
||||
let mut separator = false;
|
||||
objdiff_core::diff::display::display_row(&obj, symbol_idx, row, &diff_config, |segment| {
|
||||
if separator {
|
||||
output.push_str(", ");
|
||||
} else {
|
||||
separator = true;
|
||||
}
|
||||
let DiffTextSegment { text, color, pad_to } = segment;
|
||||
output.push_str(&format!("({:?}, {:?}, {:?})", text, color, pad_to));
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
output.push_str("]\n");
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct AlignedAs<Align, Bytes: ?Sized> {
|
||||
pub _align: [Align; 0],
|
||||
pub bytes: Bytes,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! include_bytes_align_as {
|
||||
($align_ty:ty, $path:literal) => {{
|
||||
static ALIGNED: &common::AlignedAs<$align_ty, [u8]> =
|
||||
&common::AlignedAs { _align: [], bytes: *include_bytes!($path) };
|
||||
&ALIGNED.bytes
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! include_object {
|
||||
($path:literal) => {
|
||||
include_bytes_align_as!(u32, $path)
|
||||
};
|
||||
}
|
||||
BIN
objdiff-core/tests/data/arm/LinkStateItem.o
Normal file
BIN
objdiff-core/tests/data/mips/main.c.o
Normal file
BIN
objdiff-core/tests/data/ppc/CDamageVulnerability_base.o
Normal file
BIN
objdiff-core/tests/data/ppc/CDamageVulnerability_target.o
Normal file
BIN
objdiff-core/tests/data/ppc/IObj.o
Normal file
BIN
objdiff-core/tests/data/ppc/NMWException.o
Normal file
BIN
objdiff-core/tests/data/ppc/m_Do_hostIO.o
Normal file
BIN
objdiff-core/tests/data/x86/rtest.obj
Normal file
BIN
objdiff-core/tests/data/x86/staticdebug.obj
Normal file
1704
objdiff-core/tests/snapshots/arch_arm__read_arm-2.snap
Normal file
112
objdiff-core/tests/snapshots/arch_arm__read_arm-3.snap
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
source: objdiff-core/tests/arch_arm.rs
|
||||
expression: output
|
||||
---
|
||||
[(Address(0), Normal, 5), (Spacing(4), Normal, 0), (Opcode("stmdb", 32895), Normal, 10), (Argument(Opaque("sp")), Normal, 0), (Argument(Opaque("!")), Normal, 0), (Basic(", "), Normal, 0), (Basic("{"), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r6")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("lr")), Normal, 0), (Basic("}"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(4), Normal, 5), (Spacing(4), Normal, 0), (Opcode("mov", 32818), Normal, 10), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(8), Normal, 5), (Spacing(4), Normal, 0), (Opcode("mov", 32818), Normal, 10), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r1")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(12), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32774), Normal, 10), (Symbol(Symbol { name: "_ZN13LinkStateBase12OnStateLeaveEi", demangled_name: Some("LinkStateBase::OnStateLeave(int)"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(16), Normal, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 32799), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(20)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(20), Normal, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 32786), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(10)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(24), Normal, 5), (Spacing(4), Normal, 0), (Opcode("addls", 32770), Normal, 10), (Argument(Opaque("pc")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("lsl")), Normal, 0), (Basic(" #"), Normal, 0), (Argument(Unsigned(2)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(28), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
|
||||
[(Address(32), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
|
||||
[(Address(36), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
|
||||
[(Address(40), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
|
||||
[(Address(44), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(192), Normal, 0), (Basic(" ~>"), Rotating(1), 0), (Eol, Normal, 0)]
|
||||
[(Address(48), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(124), Normal, 0), (Basic(" ~>"), Rotating(2), 0), (Eol, Normal, 0)]
|
||||
[(Address(52), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
|
||||
[(Address(56), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(140), Normal, 0), (Basic(" ~>"), Rotating(3), 0), (Eol, Normal, 0)]
|
||||
[(Address(60), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(76), Normal, 0), (Basic(" ~>"), Rotating(4), 0), (Eol, Normal, 0)]
|
||||
[(Address(64), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(152), Normal, 0), (Basic(" ~>"), Rotating(5), 0), (Eol, Normal, 0)]
|
||||
[(Address(68), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(164), Normal, 0), (Basic(" ~>"), Rotating(6), 0), (Eol, Normal, 0)]
|
||||
[(Address(72), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(164), Normal, 0), (Basic(" ~>"), Rotating(6), 0), (Eol, Normal, 0)]
|
||||
[(Address(76), Normal, 5), (Basic(" ~> "), Rotating(4), 0), (Opcode("ldr", 32799), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(336)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(80), Normal, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 32799), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(0)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(84), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32774), Normal, 10), (Symbol(Symbol { name: "_ZN18UnkStruct_027e103c19func_ov000_020cf01cEv", demangled_name: Some("UnkStruct_027e103c::func_ov000_020cf01c()"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(88), Normal, 5), (Spacing(4), Normal, 0), (Opcode("ldrb", 32800), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(224)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(92), Normal, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 32786), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(0)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(96), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bne", 32773), Normal, 10), (BranchDest(108), Normal, 0), (Basic(" ~>"), Rotating(7), 0), (Eol, Normal, 0)]
|
||||
[(Address(100), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32774), Normal, 10), (Symbol(Symbol { name: "_ZN13LinkStateItem15GetEquipBombchuEv", demangled_name: Some("LinkStateItem::GetEquipBombchu()"), address: 472, size: 16, kind: Function, section: Some(0), flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Basic(" ~>"), Rotating(8), 0), (Eol, Normal, 0)]
|
||||
[(Address(104), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32774), Normal, 10), (Symbol(Symbol { name: "_ZN12EquipBombchu19func_ov014_0213ec64Ev", demangled_name: Some("EquipBombchu::func_ov014_0213ec64()"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(108), Normal, 5), (Basic(" ~> "), Rotating(7), 0), (Opcode("ldr", 32799), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(308)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(112), Normal, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 32799), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(0)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(116), Normal, 5), (Spacing(4), Normal, 0), (Opcode("blx", 32777), Normal, 10), (Symbol(Symbol { name: "_Z19func_ov014_0211fd04Pi", demangled_name: Some("func_ov014_0211fd04(int*)"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(120), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
|
||||
[(Address(124), Normal, 5), (Basic(" ~> "), Rotating(2), 0), (Opcode("mov", 32818), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(128), Normal, 5), (Spacing(4), Normal, 0), (Opcode("mov", 32818), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(132), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32774), Normal, 10), (Symbol(Symbol { name: "_ZN13LinkStateItem13StopUsingBombEi", demangled_name: Some("LinkStateItem::StopUsingBomb(int)"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(136), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
|
||||
[(Address(140), Normal, 5), (Basic(" ~> "), Rotating(3), 0), (Opcode("mov", 32818), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(144), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32774), Normal, 10), (Symbol(Symbol { name: "_ZN13LinkStateItem13StopUsingRopeEv", demangled_name: Some("LinkStateItem::StopUsingRope()"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(148), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
|
||||
[(Address(152), Normal, 5), (Basic(" ~> "), Rotating(5), 0), (Opcode("mov", 32818), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(156), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32774), Normal, 10), (Symbol(Symbol { name: "_ZN13LinkStateItem15StopUsingHammerEv", demangled_name: Some("LinkStateItem::StopUsingHammer()"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(160), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
|
||||
[(Address(164), Normal, 5), (Basic(" ~> "), Rotating(6), 0), (Opcode("ldr", 32799), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(248)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(168), Normal, 5), (Spacing(4), Normal, 0), (Opcode("mov", 32818), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(0)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(172), Normal, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 32799), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(0)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(176), Normal, 5), (Spacing(4), Normal, 0), (Opcode("mov", 32818), Normal, 10), (Argument(Opaque("r2")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r1")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(180), Normal, 5), (Spacing(4), Normal, 0), (Opcode("strb", 32899), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(42)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(184), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32774), Normal, 10), (Symbol(Symbol { name: "_ZN18UnkStruct_027e103c19func_ov000_020cf9dcEii", demangled_name: Some("UnkStruct_027e103c::func_ov000_020cf9dc(int, int)"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(188), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
|
||||
[(Address(192), Normal, 5), (Basic(" ~> "), Rotating(1), 0), (Opcode("mov", 32818), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(196), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32774), Normal, 10), (Symbol(Symbol { name: "_ZN13LinkStateItem14StopUsingScoopEv", demangled_name: Some("LinkStateItem::StopUsingScoop()"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(200), Normal, 5), (Basic(" ~> "), Rotating(0), 0), (Opcode("ldr", 32799), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(20)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(204), Normal, 5), (Spacing(4), Normal, 0), (Opcode("mvn", 32829), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(0)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(208), Normal, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 32786), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(212), Normal, 5), (Spacing(4), Normal, 0), (Opcode("beq", 32773), Normal, 10), (BranchDest(236), Normal, 0), (Basic(" ~>"), Rotating(9), 0), (Eol, Normal, 0)]
|
||||
[(Address(216), Normal, 5), (Spacing(4), Normal, 0), (Opcode("mov", 32818), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(220), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32774), Normal, 10), (Symbol(Symbol { name: "_ZN13LinkStateBase12GetEquipItemEi", demangled_name: Some("LinkStateBase::GetEquipItem(int)"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(224), Normal, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 32799), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(0)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(228), Normal, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 32799), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(28)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(232), Normal, 5), (Spacing(4), Normal, 0), (Opcode("blx", 32778), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(236), Normal, 5), (Basic(" ~> "), Rotating(9), 0), (Opcode("ldr", 32799), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(20)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(240), Normal, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 32786), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(9)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(244), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bgt", 32773), Normal, 10), (BranchDest(288), Normal, 0), (Basic(" ~>"), Rotating(10), 0), (Eol, Normal, 0)]
|
||||
[(Address(248), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bge", 32773), Normal, 10), (BranchDest(296), Normal, 0), (Basic(" ~>"), Rotating(11), 0), (Eol, Normal, 0)]
|
||||
[(Address(252), Normal, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 32786), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(1)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(256), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bgt", 32773), Normal, 10), (BranchDest(308), Normal, 0), (Basic(" ~>"), Rotating(12), 0), (Eol, Normal, 0)]
|
||||
[(Address(260), Normal, 5), (Spacing(4), Normal, 0), (Opcode("mvn", 32829), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(0)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(264), Normal, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 32786), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(268), Normal, 5), (Spacing(4), Normal, 0), (Opcode("blt", 32773), Normal, 10), (BranchDest(308), Normal, 0), (Basic(" ~>"), Rotating(12), 0), (Eol, Normal, 0)]
|
||||
[(Address(272), Normal, 5), (Spacing(4), Normal, 0), (Opcode("cmpne", 32786), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(0)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(276), Normal, 5), (Spacing(4), Normal, 0), (Opcode("cmpne", 32786), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(1)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(280), Normal, 5), (Spacing(4), Normal, 0), (Opcode("beq", 32773), Normal, 10), (BranchDest(340), Normal, 0), (Basic(" ~>"), Rotating(13), 0), (Eol, Normal, 0)]
|
||||
[(Address(284), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(308), Normal, 0), (Basic(" ~>"), Rotating(12), 0), (Eol, Normal, 0)]
|
||||
[(Address(288), Normal, 5), (Basic(" ~> "), Rotating(10), 0), (Opcode("cmp", 32786), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(10)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(292), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bne", 32773), Normal, 10), (BranchDest(308), Normal, 0), (Basic(" ~>"), Rotating(12), 0), (Eol, Normal, 0)]
|
||||
[(Address(296), Normal, 5), (Basic(" ~> "), Rotating(11), 0), (Opcode("mov", 32818), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(300), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32774), Normal, 10), (Symbol(Symbol { name: "_ZN13LinkStateBase18EquipItem_vfunc_28Ev", demangled_name: Some("LinkStateBase::EquipItem_vfunc_28()"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(304), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(340), Normal, 0), (Basic(" ~>"), Rotating(13), 0), (Eol, Normal, 0)]
|
||||
[(Address(308), Normal, 5), (Basic(" ~> "), Rotating(12), 0), (Opcode("mov", 32818), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(312), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32774), Normal, 10), (Symbol(Symbol { name: "_ZN13LinkStateBase18EquipItem_vfunc_28Ev", demangled_name: Some("LinkStateBase::EquipItem_vfunc_28()"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(316), Normal, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 32786), Normal, 10), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(4)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(320), Normal, 5), (Spacing(4), Normal, 0), (Opcode("cmpne", 32786), Normal, 10), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(2)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(324), Normal, 5), (Spacing(4), Normal, 0), (Opcode("beq", 32773), Normal, 10), (BranchDest(340), Normal, 0), (Basic(" ~>"), Rotating(13), 0), (Eol, Normal, 0)]
|
||||
[(Address(328), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32774), Normal, 10), (Symbol(Symbol { name: "_ZN13LinkStateItem16GetLinkStateMoveEv", demangled_name: Some("LinkStateItem::GetLinkStateMove()"), address: 488, size: 16, kind: Function, section: Some(0), flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(332), Normal, 5), (Spacing(4), Normal, 0), (Opcode("mov", 32818), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(1)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(336), Normal, 5), (Spacing(4), Normal, 0), (Opcode("strb", 32899), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(20)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(340), Normal, 5), (Basic(" ~> "), Rotating(13), 0), (Opcode("mvn", 32829), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(0)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(344), Normal, 5), (Spacing(4), Normal, 0), (Opcode("add", 32770), Normal, 10), (Argument(Opaque("r6")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(80)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(348), Normal, 5), (Spacing(4), Normal, 0), (Opcode("add", 32770), Normal, 10), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(88)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(352), Normal, 5), (Spacing(4), Normal, 0), (Opcode("str", 32898), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(24)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(356), Normal, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 32786), Normal, 10), (Argument(Opaque("r6")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(360), Normal, 5), (Spacing(4), Normal, 0), (Opcode("beq", 32773), Normal, 10), (BranchDest(384), Normal, 0), (Basic(" ~>"), Rotating(14), 0), (Eol, Normal, 0)]
|
||||
[(Address(364), Normal, 5), (Basic(" ~> "), Rotating(15), 0), (Opcode("mov", 32818), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r6")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(368), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32774), Normal, 10), (Symbol(Symbol { name: "_Z19func_ov000_020b7e6cPi", demangled_name: Some("func_ov000_020b7e6c(int*)"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(372), Normal, 5), (Spacing(4), Normal, 0), (Opcode("add", 32770), Normal, 10), (Argument(Opaque("r6")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r6")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(4)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(376), Normal, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 32786), Normal, 10), (Argument(Opaque("r6")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(380), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bne", 32773), Normal, 10), (BranchDest(364), Normal, 0), (Basic(" ~>"), Rotating(15), 0), (Eol, Normal, 0)]
|
||||
[(Address(384), Normal, 5), (Basic(" ~> "), Rotating(14), 0), (Opcode("ldr", 32799), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(36)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(388), Normal, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 32799), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(0)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(392), Normal, 5), (Spacing(4), Normal, 0), (Opcode("ldrb", 32800), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(128)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(396), Normal, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 32786), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(0)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(400), Normal, 5), (Spacing(4), Normal, 0), (Opcode("beq", 32773), Normal, 10), (BranchDest(408), Normal, 0), (Basic(" ~>"), Rotating(16), 0), (Eol, Normal, 0)]
|
||||
[(Address(404), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32774), Normal, 10), (Symbol(Symbol { name: "_ZN13PlayerControl13StopFollowingEv", demangled_name: Some("PlayerControl::StopFollowing()"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(408), Normal, 5), (Basic(" ~> "), Rotating(16), 0), (Opcode("mov", 32818), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(0)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(412), Normal, 5), (Spacing(4), Normal, 0), (Opcode("strb", 32899), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(38)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(416), Normal, 5), (Spacing(4), Normal, 0), (Opcode("ldmia", 32793), Normal, 10), (Argument(Opaque("sp")), Normal, 0), (Argument(Opaque("!")), Normal, 0), (Basic(", "), Normal, 0), (Basic("{"), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r6")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic("}"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(420), Normal, 5), (Spacing(4), Normal, 0), (Opcode("andeq", 32771), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(" <"), Normal, 0), (Symbol(Symbol { name: "data_027e103c", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Basic(">"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(424), Normal, 5), (Basic(" ~> "), Rotating(8), 0), (Opcode("andeq", 32771), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(" <"), Normal, 0), (Symbol(Symbol { name: "data_027e1098", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Basic(">"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(428), Normal, 5), (Spacing(4), Normal, 0), (Opcode("andeq", 32771), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(" <"), Normal, 0), (Symbol(Symbol { name: "gPlayerControl", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Basic(">"), Normal, 0), (Eol, Normal, 0)]
|
||||
1936
objdiff-core/tests/snapshots/arch_arm__read_arm.snap
Normal file
640
objdiff-core/tests/snapshots/arch_mips__read_mips-2.snap
Normal file
@@ -0,0 +1,640 @@
|
||||
---
|
||||
source: objdiff-core/tests/arch_mips.rs
|
||||
expression: diff.instruction_rows
|
||||
---
|
||||
[
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 0,
|
||||
size: 4,
|
||||
opcode: 12,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 4,
|
||||
size: 4,
|
||||
opcode: 44,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 8,
|
||||
size: 4,
|
||||
opcode: 44,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 12,
|
||||
size: 4,
|
||||
opcode: 44,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 16,
|
||||
size: 4,
|
||||
opcode: 44,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 20,
|
||||
size: 4,
|
||||
opcode: 2,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 24,
|
||||
size: 4,
|
||||
opcode: 113,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 28,
|
||||
size: 4,
|
||||
opcode: 26,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 32,
|
||||
size: 4,
|
||||
opcode: 20,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 36,
|
||||
size: 4,
|
||||
opcode: 97,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 40,
|
||||
size: 4,
|
||||
opcode: 2,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 44,
|
||||
size: 4,
|
||||
opcode: 12,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 48,
|
||||
size: 4,
|
||||
opcode: 20,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 52,
|
||||
size: 4,
|
||||
opcode: 26,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 56,
|
||||
size: 4,
|
||||
opcode: 2,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 60,
|
||||
size: 4,
|
||||
opcode: 12,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 64,
|
||||
size: 4,
|
||||
opcode: 26,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 68,
|
||||
size: 4,
|
||||
opcode: 97,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 72,
|
||||
size: 4,
|
||||
opcode: 2,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 76,
|
||||
size: 4,
|
||||
opcode: 97,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 80,
|
||||
size: 4,
|
||||
opcode: 2,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: Some(
|
||||
InstructionBranchFrom {
|
||||
ins_idx: [
|
||||
22,
|
||||
],
|
||||
branch_idx: 0,
|
||||
},
|
||||
),
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 84,
|
||||
size: 4,
|
||||
opcode: 97,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 88,
|
||||
size: 4,
|
||||
opcode: 56,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: Some(
|
||||
InstructionBranchTo {
|
||||
ins_idx: 20,
|
||||
branch_idx: 0,
|
||||
},
|
||||
),
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 92,
|
||||
size: 4,
|
||||
opcode: 113,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 96,
|
||||
size: 4,
|
||||
opcode: 2,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 100,
|
||||
size: 4,
|
||||
opcode: 20,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 104,
|
||||
size: 4,
|
||||
opcode: 2,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 108,
|
||||
size: 4,
|
||||
opcode: 12,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 112,
|
||||
size: 4,
|
||||
opcode: 2,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 116,
|
||||
size: 4,
|
||||
opcode: 16,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 120,
|
||||
size: 4,
|
||||
opcode: 20,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 124,
|
||||
size: 4,
|
||||
opcode: 12,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 128,
|
||||
size: 4,
|
||||
opcode: 2,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: Some(
|
||||
InstructionBranchFrom {
|
||||
ins_idx: [
|
||||
36,
|
||||
38,
|
||||
44,
|
||||
],
|
||||
branch_idx: 1,
|
||||
},
|
||||
),
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 132,
|
||||
size: 4,
|
||||
opcode: 12,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 136,
|
||||
size: 4,
|
||||
opcode: 2,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 140,
|
||||
size: 4,
|
||||
opcode: 113,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 144,
|
||||
size: 4,
|
||||
opcode: 55,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: Some(
|
||||
InstructionBranchTo {
|
||||
ins_idx: 32,
|
||||
branch_idx: 1,
|
||||
},
|
||||
),
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 148,
|
||||
size: 4,
|
||||
opcode: 90,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 152,
|
||||
size: 4,
|
||||
opcode: 3,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: Some(
|
||||
InstructionBranchTo {
|
||||
ins_idx: 32,
|
||||
branch_idx: 1,
|
||||
},
|
||||
),
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 156,
|
||||
size: 4,
|
||||
opcode: 113,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 160,
|
||||
size: 4,
|
||||
opcode: 2,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 164,
|
||||
size: 4,
|
||||
opcode: 60,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 168,
|
||||
size: 4,
|
||||
opcode: 77,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 172,
|
||||
size: 4,
|
||||
opcode: 113,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 176,
|
||||
size: 4,
|
||||
opcode: 54,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: Some(
|
||||
InstructionBranchTo {
|
||||
ins_idx: 32,
|
||||
branch_idx: 1,
|
||||
},
|
||||
),
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 180,
|
||||
size: 4,
|
||||
opcode: 113,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
]
|
||||
50
objdiff-core/tests/snapshots/arch_mips__read_mips-3.snap
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
source: objdiff-core/tests/arch_mips.rs
|
||||
expression: output
|
||||
---
|
||||
[(Address(0), Normal, 5), (Spacing(4), Normal, 0), (Opcode("addiu", 12), Normal, 10), (Argument(Opaque("$sp")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("$sp")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(-32)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(4), Normal, 5), (Spacing(4), Normal, 0), (Opcode("sd", 44), Normal, 10), (Argument(Opaque("$s0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(0)), Normal, 0), (Basic("("), Normal, 0), (Argument(Opaque("$sp")), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(8), Normal, 5), (Spacing(4), Normal, 0), (Opcode("sd", 44), Normal, 10), (Argument(Opaque("$s1")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(8)), Normal, 0), (Basic("("), Normal, 0), (Argument(Opaque("$sp")), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(12), Normal, 5), (Spacing(4), Normal, 0), (Opcode("sd", 44), Normal, 10), (Argument(Opaque("$s2")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(16)), Normal, 0), (Basic("("), Normal, 0), (Argument(Opaque("$sp")), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(16), Normal, 5), (Spacing(4), Normal, 0), (Opcode("sd", 44), Normal, 10), (Argument(Opaque("$ra")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(24)), Normal, 0), (Basic("("), Normal, 0), (Argument(Opaque("$sp")), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(20), Normal, 5), (Spacing(4), Normal, 0), (Opcode("jal", 2), Normal, 10), (Symbol(Symbol { name: "xglSleep", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(24), Normal, 5), (Spacing(4), Normal, 0), (Opcode("nop", 113), Normal, 10), (Eol, Normal, 0)]
|
||||
[(Address(28), Normal, 5), (Spacing(4), Normal, 0), (Opcode("lw", 26), Normal, 10), (Argument(Opaque("$a1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("%gp_rel("), Normal, 0), (Symbol(Symbol { name: "WorkEnd", demangled_name: None, address: 64, size: 4, kind: Object, section: Some(8), flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Basic(")"), Normal, 0), (Basic("("), Normal, 0), (Argument(Opaque("$gp")), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(32), Normal, 5), (Spacing(4), Normal, 0), (Opcode("lui", 20), Normal, 10), (Argument(Opaque("$a0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("%hi("), Normal, 0), (Symbol(Symbol { name: "[.sdata]", demangled_name: None, address: 0, size: 64, kind: Section, section: Some(8), flags: FlagSet(Local), align: None, virtual_address: None }), Bright, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(36), Normal, 5), (Spacing(4), Normal, 0), (Opcode("daddu", 97), Normal, 10), (Argument(Opaque("$a2")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("$zero")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("$zero")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(40), Normal, 5), (Spacing(4), Normal, 0), (Opcode("jal", 2), Normal, 10), (Symbol(Symbol { name: "xglSoundLoadEffect", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(44), Normal, 5), (Spacing(4), Normal, 0), (Opcode("addiu", 12), Normal, 10), (Argument(Opaque("$a0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("$a0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("%lo("), Normal, 0), (Symbol(Symbol { name: "[.sdata]", demangled_name: None, address: 0, size: 64, kind: Section, section: Some(8), flags: FlagSet(Local), align: None, virtual_address: None }), Bright, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(48), Normal, 5), (Spacing(4), Normal, 0), (Opcode("lui", 20), Normal, 10), (Argument(Opaque("$a0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("%hi("), Normal, 0), (Symbol(Symbol { name: "PacketBottomNewVu1DropMicroCode", demangled_name: None, address: 0, size: 12, kind: Object, section: Some(7), flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(52), Normal, 5), (Spacing(4), Normal, 0), (Opcode("lw", 26), Normal, 10), (Argument(Opaque("$a1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("%gp_rel("), Normal, 0), (Symbol(Symbol { name: "WorkEnd", demangled_name: None, address: 64, size: 4, kind: Object, section: Some(8), flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Basic(")"), Normal, 0), (Basic("("), Normal, 0), (Argument(Opaque("$gp")), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(56), Normal, 5), (Spacing(4), Normal, 0), (Opcode("jal", 2), Normal, 10), (Symbol(Symbol { name: "xglSoundLoadSwd", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(60), Normal, 5), (Spacing(4), Normal, 0), (Opcode("addiu", 12), Normal, 10), (Argument(Opaque("$a0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("$a0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("%lo("), Normal, 0), (Symbol(Symbol { name: "PacketBottomNewVu1DropMicroCode", demangled_name: None, address: 0, size: 12, kind: Object, section: Some(7), flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(64), Normal, 5), (Spacing(4), Normal, 0), (Opcode("lw", 26), Normal, 10), (Argument(Opaque("$a0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("%gp_rel("), Normal, 0), (Symbol(Symbol { name: "WorkEnd", demangled_name: None, address: 64, size: 4, kind: Object, section: Some(8), flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Basic(")"), Normal, 0), (Basic("("), Normal, 0), (Argument(Opaque("$gp")), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(68), Normal, 5), (Spacing(4), Normal, 0), (Opcode("daddu", 97), Normal, 10), (Argument(Opaque("$a1")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("$zero")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("$zero")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(72), Normal, 5), (Spacing(4), Normal, 0), (Opcode("jal", 2), Normal, 10), (Symbol(Symbol { name: "SsdAddWaveData", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(76), Normal, 5), (Spacing(4), Normal, 0), (Opcode("daddu", 97), Normal, 10), (Argument(Opaque("$a2")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("$zero")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("$zero")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(80), Normal, 5), (Basic(" ~> "), Rotating(0), 0), (Opcode("jal", 2), Normal, 10), (Symbol(Symbol { name: "SsdSpuDmaCompleted", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(84), Normal, 5), (Spacing(4), Normal, 0), (Opcode("daddu", 97), Normal, 10), (Argument(Opaque("$a0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("$zero")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("$zero")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(88), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bnez", 56), Normal, 10), (Argument(Opaque("$v0")), Normal, 0), (Basic(", "), Normal, 0), (BranchDest(80), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
|
||||
[(Address(92), Normal, 5), (Spacing(4), Normal, 0), (Opcode("nop", 113), Normal, 10), (Eol, Normal, 0)]
|
||||
[(Address(96), Normal, 5), (Spacing(4), Normal, 0), (Opcode("jal", 2), Normal, 10), (Symbol(Symbol { name: "xglRenderDispOn", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(100), Normal, 5), (Spacing(4), Normal, 0), (Opcode("lui", 20), Normal, 10), (Argument(Opaque("$s1")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Unsigned(255)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(104), Normal, 5), (Spacing(4), Normal, 0), (Opcode("jal", 2), Normal, 10), (Symbol(Symbol { name: "xglCdLoadOverlay", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(108), Normal, 5), (Spacing(4), Normal, 0), (Opcode("addiu", 12), Normal, 10), (Argument(Opaque("$a0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("$zero")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(2)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(112), Normal, 5), (Spacing(4), Normal, 0), (Opcode("jal", 2), Normal, 10), (Symbol(Symbol { name: "LogoFirst", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(116), Normal, 5), (Spacing(4), Normal, 0), (Opcode("ori", 16), Normal, 10), (Argument(Opaque("$s1")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("$s1")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Unsigned(65535)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(120), Normal, 5), (Spacing(4), Normal, 0), (Opcode("lui", 20), Normal, 10), (Argument(Opaque("$v0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("%hi("), Normal, 0), (Symbol(Symbol { name: "Title", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(124), Normal, 5), (Spacing(4), Normal, 0), (Opcode("addiu", 12), Normal, 10), (Argument(Opaque("$s2")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("$v0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("%lo("), Normal, 0), (Symbol(Symbol { name: "Title", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(128), Normal, 5), (Basic(" ~> "), Rotating(1), 0), (Opcode("jal", 2), Normal, 10), (Symbol(Symbol { name: "xglCdLoadOverlay", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(132), Normal, 5), (Spacing(4), Normal, 0), (Opcode("addiu", 12), Normal, 10), (Argument(Opaque("$a0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("$zero")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(2)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(136), Normal, 5), (Spacing(4), Normal, 0), (Opcode("jal", 2), Normal, 10), (Symbol(Symbol { name: "Title", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(140), Normal, 5), (Spacing(4), Normal, 0), (Opcode("nop", 113), Normal, 10), (Eol, Normal, 0)]
|
||||
[(Address(144), Normal, 5), (Spacing(4), Normal, 0), (Opcode("beqz", 55), Normal, 10), (Argument(Opaque("$v0")), Normal, 0), (Basic(", "), Normal, 0), (BranchDest(128), Normal, 0), (Basic(" ~>"), Rotating(1), 0), (Eol, Normal, 0)]
|
||||
[(Address(148), Normal, 5), (Spacing(4), Normal, 0), (Opcode("and", 90), Normal, 10), (Argument(Opaque("$s0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("$v0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("$s1")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(152), Normal, 5), (Spacing(4), Normal, 0), (Opcode("beq", 3), Normal, 10), (Argument(Opaque("$s0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("$s2")), Normal, 0), (Basic(", "), Normal, 0), (BranchDest(128), Normal, 0), (Basic(" ~>"), Rotating(1), 0), (Eol, Normal, 0)]
|
||||
[(Address(156), Normal, 5), (Spacing(4), Normal, 0), (Opcode("nop", 113), Normal, 10), (Eol, Normal, 0)]
|
||||
[(Address(160), Normal, 5), (Spacing(4), Normal, 0), (Opcode("jal", 2), Normal, 10), (Symbol(Symbol { name: "xglCdLoadOverlay", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(164), Normal, 5), (Spacing(4), Normal, 0), (Opcode("srl", 60), Normal, 10), (Argument(Opaque("$a0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("$v0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("24")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(168), Normal, 5), (Spacing(4), Normal, 0), (Opcode("jalr", 77), Normal, 10), (Argument(Opaque("$s0")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(172), Normal, 5), (Spacing(4), Normal, 0), (Opcode("nop", 113), Normal, 10), (Eol, Normal, 0)]
|
||||
[(Address(176), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 54), Normal, 10), (BranchDest(128), Normal, 0), (Basic(" ~>"), Rotating(1), 0), (Eol, Normal, 0)]
|
||||
[(Address(180), Normal, 5), (Spacing(4), Normal, 0), (Opcode("nop", 113), Normal, 10), (Eol, Normal, 0)]
|
||||
1456
objdiff-core/tests/snapshots/arch_mips__read_mips.snap
Normal file
4870
objdiff-core/tests/snapshots/arch_ppc__diff_ppc-2.snap
Normal file
86
objdiff-core/tests/snapshots/arch_ppc__diff_ppc.snap
Normal file
@@ -0,0 +1,86 @@
|
||||
---
|
||||
source: objdiff-core/tests/arch_ppc.rs
|
||||
expression: sections_display
|
||||
---
|
||||
[
|
||||
SectionDisplay {
|
||||
id: ".comm",
|
||||
name: ".comm",
|
||||
size: 0,
|
||||
match_percent: None,
|
||||
symbols: [
|
||||
SectionDisplaySymbol {
|
||||
symbol: 11,
|
||||
is_mapping_symbol: false,
|
||||
},
|
||||
SectionDisplaySymbol {
|
||||
symbol: 12,
|
||||
is_mapping_symbol: false,
|
||||
},
|
||||
SectionDisplaySymbol {
|
||||
symbol: 13,
|
||||
is_mapping_symbol: false,
|
||||
},
|
||||
SectionDisplaySymbol {
|
||||
symbol: 14,
|
||||
is_mapping_symbol: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
SectionDisplay {
|
||||
id: ".ctors-0",
|
||||
name: ".ctors",
|
||||
size: 4,
|
||||
match_percent: Some(
|
||||
100.0,
|
||||
),
|
||||
symbols: [
|
||||
SectionDisplaySymbol {
|
||||
symbol: 2,
|
||||
is_mapping_symbol: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
SectionDisplay {
|
||||
id: ".text-0",
|
||||
name: ".text",
|
||||
size: 3060,
|
||||
match_percent: Some(
|
||||
59.02353,
|
||||
),
|
||||
symbols: [
|
||||
SectionDisplaySymbol {
|
||||
symbol: 3,
|
||||
is_mapping_symbol: false,
|
||||
},
|
||||
SectionDisplaySymbol {
|
||||
symbol: 10,
|
||||
is_mapping_symbol: false,
|
||||
},
|
||||
SectionDisplaySymbol {
|
||||
symbol: 9,
|
||||
is_mapping_symbol: false,
|
||||
},
|
||||
SectionDisplaySymbol {
|
||||
symbol: 8,
|
||||
is_mapping_symbol: false,
|
||||
},
|
||||
SectionDisplaySymbol {
|
||||
symbol: 7,
|
||||
is_mapping_symbol: false,
|
||||
},
|
||||
SectionDisplaySymbol {
|
||||
symbol: 6,
|
||||
is_mapping_symbol: false,
|
||||
},
|
||||
SectionDisplaySymbol {
|
||||
symbol: 5,
|
||||
is_mapping_symbol: false,
|
||||
},
|
||||
SectionDisplaySymbol {
|
||||
symbol: 4,
|
||||
is_mapping_symbol: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
source: objdiff-core/tests/arch_ppc.rs
|
||||
expression: line_infos
|
||||
---
|
||||
[
|
||||
{
|
||||
0: 13,
|
||||
4: 16,
|
||||
32: 17,
|
||||
44: 18,
|
||||
60: 20,
|
||||
76: 21,
|
||||
84: 23,
|
||||
92: 25,
|
||||
108: 26,
|
||||
124: 27,
|
||||
136: 28,
|
||||
144: 29,
|
||||
152: 31,
|
||||
164: 34,
|
||||
184: 35,
|
||||
212: 39,
|
||||
228: 40,
|
||||
236: 41,
|
||||
260: 43,
|
||||
288: 44,
|
||||
292: 45,
|
||||
300: 48,
|
||||
436: 0,
|
||||
},
|
||||
{
|
||||
0: 48,
|
||||
132: 35,
|
||||
244: 26,
|
||||
304: 22,
|
||||
312: 23,
|
||||
316: 24,
|
||||
320: 0,
|
||||
},
|
||||
]
|
||||
521
objdiff-core/tests/snapshots/arch_ppc__read_extab.snap
Normal file
@@ -0,0 +1,521 @@
|
||||
---
|
||||
source: objdiff-core/tests/arch_ppc.rs
|
||||
expression: obj
|
||||
---
|
||||
Object {
|
||||
arch: ArchPpc {
|
||||
extab: Some(
|
||||
{
|
||||
10: ExceptionInfo {
|
||||
eti_symbol: ExtabSymbolRef {
|
||||
original_index: 5,
|
||||
name: "@31",
|
||||
demangled_name: None,
|
||||
},
|
||||
etb_symbol: ExtabSymbolRef {
|
||||
original_index: 4,
|
||||
name: "@30",
|
||||
demangled_name: None,
|
||||
},
|
||||
data: ExceptionTableData {
|
||||
flag_val: 8200,
|
||||
has_elf_vector: false,
|
||||
large_frame: true,
|
||||
has_frame_pointer: false,
|
||||
saved_cr: false,
|
||||
fpr_save_range: 0,
|
||||
gpr_save_range: 4,
|
||||
et_field: 0,
|
||||
pc_actions: [],
|
||||
exception_actions: [],
|
||||
relocations: [],
|
||||
},
|
||||
dtors: [],
|
||||
},
|
||||
11: ExceptionInfo {
|
||||
eti_symbol: ExtabSymbolRef {
|
||||
original_index: 7,
|
||||
name: "@52",
|
||||
demangled_name: None,
|
||||
},
|
||||
etb_symbol: ExtabSymbolRef {
|
||||
original_index: 6,
|
||||
name: "@51",
|
||||
demangled_name: None,
|
||||
},
|
||||
data: ExceptionTableData {
|
||||
flag_val: 8200,
|
||||
has_elf_vector: false,
|
||||
large_frame: true,
|
||||
has_frame_pointer: false,
|
||||
saved_cr: false,
|
||||
fpr_save_range: 0,
|
||||
gpr_save_range: 4,
|
||||
et_field: 0,
|
||||
pc_actions: [
|
||||
PCAction {
|
||||
start_pc: 96,
|
||||
end_pc: 96,
|
||||
action_offset: 16,
|
||||
},
|
||||
],
|
||||
exception_actions: [
|
||||
ExceptionAction {
|
||||
action_offset: 16,
|
||||
action_type: DestroyLocal,
|
||||
action_param: 0,
|
||||
has_end_bit: true,
|
||||
bytes: [
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
},
|
||||
],
|
||||
relocations: [
|
||||
Relocation {
|
||||
offset: 20,
|
||||
address: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
dtors: [
|
||||
ExtabSymbolRef {
|
||||
original_index: 12,
|
||||
name: "__dt__26__partial_array_destructorFv",
|
||||
demangled_name: Some(
|
||||
"__partial_array_destructor::~__partial_array_destructor()",
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
12: ExceptionInfo {
|
||||
eti_symbol: ExtabSymbolRef {
|
||||
original_index: 9,
|
||||
name: "@60",
|
||||
demangled_name: None,
|
||||
},
|
||||
etb_symbol: ExtabSymbolRef {
|
||||
original_index: 8,
|
||||
name: "@59",
|
||||
demangled_name: None,
|
||||
},
|
||||
data: ExceptionTableData {
|
||||
flag_val: 6152,
|
||||
has_elf_vector: false,
|
||||
large_frame: true,
|
||||
has_frame_pointer: false,
|
||||
saved_cr: false,
|
||||
fpr_save_range: 0,
|
||||
gpr_save_range: 3,
|
||||
et_field: 0,
|
||||
pc_actions: [],
|
||||
exception_actions: [],
|
||||
relocations: [],
|
||||
},
|
||||
dtors: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
endianness: Big,
|
||||
symbols: [
|
||||
Symbol {
|
||||
name: "NMWException.cpp",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Unknown,
|
||||
section: None,
|
||||
flags: FlagSet(Local),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "[.text]",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Section,
|
||||
section: Some(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Local),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "[extab]",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Section,
|
||||
section: Some(
|
||||
1,
|
||||
),
|
||||
flags: FlagSet(Local),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "[extabindex]",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Section,
|
||||
section: Some(
|
||||
2,
|
||||
),
|
||||
flags: FlagSet(Local),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "@30",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 8,
|
||||
kind: Object,
|
||||
section: Some(
|
||||
1,
|
||||
),
|
||||
flags: FlagSet(Local),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "@31",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 12,
|
||||
kind: Object,
|
||||
section: Some(
|
||||
2,
|
||||
),
|
||||
flags: FlagSet(Local),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "@51",
|
||||
demangled_name: None,
|
||||
address: 8,
|
||||
size: 24,
|
||||
kind: Object,
|
||||
section: Some(
|
||||
1,
|
||||
),
|
||||
flags: FlagSet(Local),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "@52",
|
||||
demangled_name: None,
|
||||
address: 12,
|
||||
size: 12,
|
||||
kind: Object,
|
||||
section: Some(
|
||||
2,
|
||||
),
|
||||
flags: FlagSet(Local),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "@59",
|
||||
demangled_name: None,
|
||||
address: 32,
|
||||
size: 8,
|
||||
kind: Object,
|
||||
section: Some(
|
||||
1,
|
||||
),
|
||||
flags: FlagSet(Local),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "@60",
|
||||
demangled_name: None,
|
||||
address: 24,
|
||||
size: 12,
|
||||
kind: Object,
|
||||
section: Some(
|
||||
2,
|
||||
),
|
||||
flags: FlagSet(Local),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "__destroy_arr",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 120,
|
||||
kind: Function,
|
||||
section: Some(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Global | HasExtra),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "__construct_array",
|
||||
demangled_name: None,
|
||||
address: 120,
|
||||
size: 248,
|
||||
kind: Function,
|
||||
section: Some(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Global | HasExtra),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "__dt__26__partial_array_destructorFv",
|
||||
demangled_name: Some(
|
||||
"__partial_array_destructor::~__partial_array_destructor()",
|
||||
),
|
||||
address: 368,
|
||||
size: 184,
|
||||
kind: Function,
|
||||
section: Some(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Global | Weak | HasExtra),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "__dl__FPv",
|
||||
demangled_name: Some(
|
||||
"operator delete(void*)",
|
||||
),
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Unknown,
|
||||
section: None,
|
||||
flags: FlagSet(Global),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
],
|
||||
sections: [
|
||||
Section {
|
||||
id: ".text-0",
|
||||
name: ".text",
|
||||
address: 0,
|
||||
size: 552,
|
||||
kind: Code,
|
||||
data: SectionData(
|
||||
552,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
10,
|
||||
),
|
||||
address: 516,
|
||||
target_symbol: 13,
|
||||
addend: 0,
|
||||
},
|
||||
],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: "extab-0",
|
||||
name: "extab",
|
||||
address: 0,
|
||||
size: 40,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
40,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
1,
|
||||
),
|
||||
address: 28,
|
||||
target_symbol: 12,
|
||||
addend: 0,
|
||||
},
|
||||
],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: "extabindex-0",
|
||||
name: "extabindex",
|
||||
address: 0,
|
||||
size: 36,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
36,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
1,
|
||||
),
|
||||
address: 0,
|
||||
target_symbol: 10,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
1,
|
||||
),
|
||||
address: 8,
|
||||
target_symbol: 4,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
1,
|
||||
),
|
||||
address: 12,
|
||||
target_symbol: 11,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
1,
|
||||
),
|
||||
address: 20,
|
||||
target_symbol: 6,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
1,
|
||||
),
|
||||
address: 24,
|
||||
target_symbol: 12,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
1,
|
||||
),
|
||||
address: 32,
|
||||
target_symbol: 8,
|
||||
addend: 0,
|
||||
},
|
||||
],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".rela.text-0",
|
||||
name: ".rela.text",
|
||||
address: 0,
|
||||
size: 12,
|
||||
kind: Unknown,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".relaextab-0",
|
||||
name: ".relaextab",
|
||||
address: 0,
|
||||
size: 12,
|
||||
kind: Unknown,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".relaextabindex-0",
|
||||
name: ".relaextabindex",
|
||||
address: 0,
|
||||
size: 72,
|
||||
kind: Unknown,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".symtab-0",
|
||||
name: ".symtab",
|
||||
address: 0,
|
||||
size: 240,
|
||||
kind: Unknown,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".strtab-0",
|
||||
name: ".strtab",
|
||||
address: 0,
|
||||
size: 121,
|
||||
kind: Unknown,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".shstrtab-0",
|
||||
name: ".shstrtab",
|
||||
address: 0,
|
||||
size: 97,
|
||||
kind: Unknown,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".comment-0",
|
||||
name: ".comment",
|
||||
address: 0,
|
||||
size: 164,
|
||||
kind: Unknown,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
],
|
||||
split_meta: None,
|
||||
path: None,
|
||||
timestamp: None,
|
||||
}
|
||||
1008
objdiff-core/tests/snapshots/arch_ppc__read_ppc-2.snap
Normal file
70
objdiff-core/tests/snapshots/arch_ppc__read_ppc-3.snap
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
source: objdiff-core/tests/arch_ppc.rs
|
||||
expression: output
|
||||
---
|
||||
[(Address(0), Normal, 5), (Spacing(4), Normal, 0), (Opcode("srwi", 60), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("24")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(4), Normal, 5), (Spacing(4), Normal, 0), (Opcode("cmpwi", 38), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(-1)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(8), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bne", 43), Normal, 10), (BranchDest(20), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
|
||||
[(Address(12), Normal, 5), (Spacing(4), Normal, 0), (Opcode("li", 41), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(-1)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(16), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 45), Normal, 10), (BranchDest(32), Normal, 0), (Basic(" ~>"), Rotating(1), 0), (Eol, Normal, 0)]
|
||||
[(Address(20), Normal, 5), (Basic(" ~> "), Rotating(0), 0), (Opcode("lis", 42), Normal, 10), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Symbol(Symbol { name: "__upper_map", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: Some(0) }), Bright, 0), (Basic("@ha"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(24), Normal, 5), (Spacing(4), Normal, 0), (Opcode("addi", 41), Normal, 10), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Symbol(Symbol { name: "__upper_map", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: Some(0) }), Bright, 0), (Basic("@l"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(28), Normal, 5), (Spacing(4), Normal, 0), (Opcode("lbzx", 94), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(32), Normal, 5), (Basic(" ~> "), Rotating(1), 0), (Opcode("extrwi", 60), Normal, 10), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("8")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("8")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(36), Normal, 5), (Spacing(4), Normal, 0), (Opcode("stb", 166), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Symbol(Symbol { name: "text$52", demangled_name: None, address: 8, size: 5, kind: Object, section: Some(2), flags: FlagSet(Local), align: None, virtual_address: Some(2153420056) }), Bright, 0), (Basic("@sda21"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(40), Normal, 5), (Spacing(4), Normal, 0), (Opcode("cmpwi", 38), Normal, 10), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(-1)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(44), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bne", 43), Normal, 10), (BranchDest(56), Normal, 0), (Basic(" ~>"), Rotating(2), 0), (Eol, Normal, 0)]
|
||||
[(Address(48), Normal, 5), (Spacing(4), Normal, 0), (Opcode("li", 41), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(-1)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(52), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 45), Normal, 10), (BranchDest(68), Normal, 0), (Basic(" ~>"), Rotating(3), 0), (Eol, Normal, 0)]
|
||||
[(Address(56), Normal, 5), (Basic(" ~> "), Rotating(2), 0), (Opcode("lis", 42), Normal, 10), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Symbol(Symbol { name: "__upper_map", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: Some(0) }), Bright, 0), (Basic("@ha"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(60), Normal, 5), (Spacing(4), Normal, 0), (Opcode("addi", 41), Normal, 10), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Symbol(Symbol { name: "__upper_map", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: Some(0) }), Bright, 0), (Basic("@l"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(64), Normal, 5), (Spacing(4), Normal, 0), (Opcode("lbzx", 94), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(68), Normal, 5), (Basic(" ~> "), Rotating(3), 0), (Opcode("extrwi", 60), Normal, 10), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("8")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("16")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(72), Normal, 5), (Spacing(4), Normal, 0), (Opcode("li", 41), Normal, 10), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Symbol(Symbol { name: "text$52", demangled_name: None, address: 8, size: 5, kind: Object, section: Some(2), flags: FlagSet(Local), align: None, virtual_address: Some(2153420056) }), Bright, 0), (Basic("@sda21"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(76), Normal, 5), (Spacing(4), Normal, 0), (Opcode("cmpwi", 38), Normal, 10), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(-1)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(80), Normal, 5), (Spacing(4), Normal, 0), (Opcode("stb", 166), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(1)), Normal, 0), (Basic("("), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(84), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bne", 43), Normal, 10), (BranchDest(96), Normal, 0), (Basic(" ~>"), Rotating(4), 0), (Eol, Normal, 0)]
|
||||
[(Address(88), Normal, 5), (Spacing(4), Normal, 0), (Opcode("li", 41), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(-1)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(92), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 45), Normal, 10), (BranchDest(108), Normal, 0), (Basic(" ~>"), Rotating(5), 0), (Eol, Normal, 0)]
|
||||
[(Address(96), Normal, 5), (Basic(" ~> "), Rotating(4), 0), (Opcode("lis", 42), Normal, 10), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Symbol(Symbol { name: "__upper_map", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: Some(0) }), Bright, 0), (Basic("@ha"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(100), Normal, 5), (Spacing(4), Normal, 0), (Opcode("addi", 41), Normal, 10), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Symbol(Symbol { name: "__upper_map", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: Some(0) }), Bright, 0), (Basic("@l"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(104), Normal, 5), (Spacing(4), Normal, 0), (Opcode("lbzx", 94), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(108), Normal, 5), (Basic(" ~> "), Rotating(5), 0), (Opcode("clrlwi", 60), Normal, 10), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("24")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(112), Normal, 5), (Spacing(4), Normal, 0), (Opcode("li", 41), Normal, 10), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Symbol(Symbol { name: "text$52", demangled_name: None, address: 8, size: 5, kind: Object, section: Some(2), flags: FlagSet(Local), align: None, virtual_address: Some(2153420056) }), Bright, 0), (Basic("@sda21"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(116), Normal, 5), (Spacing(4), Normal, 0), (Opcode("cmpwi", 38), Normal, 10), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(-1)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(120), Normal, 5), (Spacing(4), Normal, 0), (Opcode("stb", 166), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(2)), Normal, 0), (Basic("("), Normal, 0), (Argument(Opaque("r3")), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(124), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bne", 43), Normal, 10), (BranchDest(136), Normal, 0), (Basic(" ~>"), Rotating(6), 0), (Eol, Normal, 0)]
|
||||
[(Address(128), Normal, 5), (Spacing(4), Normal, 0), (Opcode("li", 41), Normal, 10), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(-1)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(132), Normal, 5), (Spacing(4), Normal, 0), (Opcode("b", 45), Normal, 10), (BranchDest(148), Normal, 0), (Basic(" ~>"), Rotating(7), 0), (Eol, Normal, 0)]
|
||||
[(Address(136), Normal, 5), (Basic(" ~> "), Rotating(6), 0), (Opcode("lis", 42), Normal, 10), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Symbol(Symbol { name: "__upper_map", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: Some(0) }), Bright, 0), (Basic("@ha"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(140), Normal, 5), (Spacing(4), Normal, 0), (Opcode("addi", 41), Normal, 10), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Symbol(Symbol { name: "__upper_map", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: Some(0) }), Bright, 0), (Basic("@l"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(144), Normal, 5), (Spacing(4), Normal, 0), (Opcode("lbzx", 94), Normal, 10), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(148), Normal, 5), (Basic(" ~> "), Rotating(7), 0), (Opcode("li", 41), Normal, 10), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Symbol(Symbol { name: "text$52", demangled_name: None, address: 8, size: 5, kind: Object, section: Some(2), flags: FlagSet(Local), align: None, virtual_address: Some(2153420056) }), Bright, 0), (Basic("@sda21"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(152), Normal, 5), (Spacing(4), Normal, 0), (Opcode("li", 41), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(0)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(156), Normal, 5), (Spacing(4), Normal, 0), (Opcode("stb", 166), Normal, 10), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(3)), Normal, 0), (Basic("("), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(160), Normal, 5), (Spacing(4), Normal, 0), (Opcode("lis", 42), Normal, 10), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Symbol(Symbol { name: "__ctype_map", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: Some(0) }), Bright, 0), (Basic("@ha"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(164), Normal, 5), (Spacing(4), Normal, 0), (Opcode("addi", 41), Normal, 10), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Symbol(Symbol { name: "__ctype_map", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: Some(0) }), Bright, 0), (Basic("@l"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(168), Normal, 5), (Spacing(4), Normal, 0), (Opcode("stb", 166), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(4)), Normal, 0), (Basic("("), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(172), Normal, 5), (Spacing(4), Normal, 0), (Opcode("li", 41), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(45)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(176), Normal, 5), (Spacing(4), Normal, 0), (Opcode("lbz", 162), Normal, 10), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Symbol(Symbol { name: "text$52", demangled_name: None, address: 8, size: 5, kind: Object, section: Some(2), flags: FlagSet(Local), align: None, virtual_address: Some(2153420056) }), Bright, 0), (Basic("@sda21"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(180), Normal, 5), (Spacing(4), Normal, 0), (Opcode("lbzx", 94), Normal, 10), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r3")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(184), Normal, 5), (Spacing(4), Normal, 0), (Opcode("andi.", 66), Normal, 10), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Unsigned(220)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(188), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bne", 43), Normal, 10), (BranchDest(196), Normal, 0), (Basic(" ~>"), Rotating(8), 0), (Eol, Normal, 0)]
|
||||
[(Address(192), Normal, 5), (Spacing(4), Normal, 0), (Opcode("stb", 166), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(0)), Normal, 0), (Basic("("), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(196), Normal, 5), (Basic(" ~> "), Rotating(8), 0), (Opcode("lbzu", 163), Normal, 10), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(1)), Normal, 0), (Basic("("), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(200), Normal, 5), (Spacing(4), Normal, 0), (Opcode("lbzx", 94), Normal, 10), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r3")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(204), Normal, 5), (Spacing(4), Normal, 0), (Opcode("andi.", 66), Normal, 10), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Unsigned(220)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(208), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bne", 43), Normal, 10), (BranchDest(216), Normal, 0), (Basic(" ~>"), Rotating(9), 0), (Eol, Normal, 0)]
|
||||
[(Address(212), Normal, 5), (Spacing(4), Normal, 0), (Opcode("stb", 166), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(0)), Normal, 0), (Basic("("), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(216), Normal, 5), (Basic(" ~> "), Rotating(9), 0), (Opcode("lbzu", 163), Normal, 10), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(1)), Normal, 0), (Basic("("), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(220), Normal, 5), (Spacing(4), Normal, 0), (Opcode("lbzx", 94), Normal, 10), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r3")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(224), Normal, 5), (Spacing(4), Normal, 0), (Opcode("andi.", 66), Normal, 10), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Unsigned(220)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(228), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bne", 43), Normal, 10), (BranchDest(236), Normal, 0), (Basic(" ~>"), Rotating(10), 0), (Eol, Normal, 0)]
|
||||
[(Address(232), Normal, 5), (Spacing(4), Normal, 0), (Opcode("stb", 166), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(0)), Normal, 0), (Basic("("), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(236), Normal, 5), (Basic(" ~> "), Rotating(10), 0), (Opcode("lbzu", 163), Normal, 10), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(1)), Normal, 0), (Basic("("), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(240), Normal, 5), (Spacing(4), Normal, 0), (Opcode("lbzx", 94), Normal, 10), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r3")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(244), Normal, 5), (Spacing(4), Normal, 0), (Opcode("andi.", 66), Normal, 10), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Unsigned(220)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(248), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bne", 43), Normal, 10), (BranchDest(256), Normal, 0), (Basic(" ~>"), Rotating(11), 0), (Eol, Normal, 0)]
|
||||
[(Address(252), Normal, 5), (Spacing(4), Normal, 0), (Opcode("stb", 166), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Signed(0)), Normal, 0), (Basic("("), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(256), Normal, 5), (Basic(" ~> "), Rotating(11), 0), (Opcode("li", 41), Normal, 10), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Symbol(Symbol { name: "text$52", demangled_name: None, address: 8, size: 5, kind: Object, section: Some(2), flags: FlagSet(Local), align: None, virtual_address: Some(2153420056) }), Bright, 0), (Basic("@sda21"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(260), Normal, 5), (Spacing(4), Normal, 0), (Opcode("blr", 47), Normal, 10), (Eol, Normal, 0)]
|
||||
490
objdiff-core/tests/snapshots/arch_ppc__read_ppc.snap
Normal file
@@ -0,0 +1,490 @@
|
||||
---
|
||||
source: objdiff-core/tests/arch_ppc.rs
|
||||
expression: obj
|
||||
---
|
||||
Object {
|
||||
arch: ArchPpc {
|
||||
extab: None,
|
||||
},
|
||||
endianness: Big,
|
||||
symbols: [
|
||||
Symbol {
|
||||
name: "IObj.cpp",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Unknown,
|
||||
section: None,
|
||||
flags: FlagSet(Local),
|
||||
align: None,
|
||||
virtual_address: Some(
|
||||
0,
|
||||
),
|
||||
},
|
||||
Symbol {
|
||||
name: "[.text]",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Section,
|
||||
section: Some(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Local),
|
||||
align: None,
|
||||
virtual_address: Some(
|
||||
2150895620,
|
||||
),
|
||||
},
|
||||
Symbol {
|
||||
name: "[.ctors]",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 4,
|
||||
kind: Section,
|
||||
section: Some(
|
||||
1,
|
||||
),
|
||||
flags: FlagSet(Local),
|
||||
align: None,
|
||||
virtual_address: Some(
|
||||
2151461704,
|
||||
),
|
||||
},
|
||||
Symbol {
|
||||
name: "[.sbss]",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Section,
|
||||
section: Some(
|
||||
2,
|
||||
),
|
||||
flags: FlagSet(Local),
|
||||
align: None,
|
||||
virtual_address: Some(
|
||||
2153420048,
|
||||
),
|
||||
},
|
||||
Symbol {
|
||||
name: "__sinit_IObj_cpp",
|
||||
demangled_name: None,
|
||||
address: 264,
|
||||
size: 20,
|
||||
kind: Function,
|
||||
section: Some(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Local),
|
||||
align: None,
|
||||
virtual_address: Some(
|
||||
2150895884,
|
||||
),
|
||||
},
|
||||
Symbol {
|
||||
name: "text$52",
|
||||
demangled_name: None,
|
||||
address: 8,
|
||||
size: 5,
|
||||
kind: Object,
|
||||
section: Some(
|
||||
2,
|
||||
),
|
||||
flags: FlagSet(Local),
|
||||
align: None,
|
||||
virtual_address: Some(
|
||||
2153420056,
|
||||
),
|
||||
},
|
||||
Symbol {
|
||||
name: "Type2Text__10SObjectTagFUi",
|
||||
demangled_name: Some(
|
||||
"SObjectTag::Type2Text(unsigned int)",
|
||||
),
|
||||
address: 0,
|
||||
size: 264,
|
||||
kind: Function,
|
||||
section: Some(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Global),
|
||||
align: None,
|
||||
virtual_address: Some(
|
||||
2150895620,
|
||||
),
|
||||
},
|
||||
Symbol {
|
||||
name: "gkInvalidObjectTag",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 8,
|
||||
kind: Object,
|
||||
section: Some(
|
||||
2,
|
||||
),
|
||||
flags: FlagSet(Global),
|
||||
align: None,
|
||||
virtual_address: Some(
|
||||
2153420048,
|
||||
),
|
||||
},
|
||||
Symbol {
|
||||
name: "__upper_map",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Unknown,
|
||||
section: None,
|
||||
flags: FlagSet(Global),
|
||||
align: None,
|
||||
virtual_address: Some(
|
||||
0,
|
||||
),
|
||||
},
|
||||
Symbol {
|
||||
name: "__ctype_map",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Unknown,
|
||||
section: None,
|
||||
flags: FlagSet(Global),
|
||||
align: None,
|
||||
virtual_address: Some(
|
||||
0,
|
||||
),
|
||||
},
|
||||
],
|
||||
sections: [
|
||||
Section {
|
||||
id: ".text-0",
|
||||
name: ".text",
|
||||
address: 0,
|
||||
size: 284,
|
||||
kind: Code,
|
||||
data: SectionData(
|
||||
284,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
6,
|
||||
),
|
||||
address: 22,
|
||||
target_symbol: 8,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
4,
|
||||
),
|
||||
address: 26,
|
||||
target_symbol: 8,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
109,
|
||||
),
|
||||
address: 36,
|
||||
target_symbol: 5,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
6,
|
||||
),
|
||||
address: 58,
|
||||
target_symbol: 8,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
4,
|
||||
),
|
||||
address: 62,
|
||||
target_symbol: 8,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
109,
|
||||
),
|
||||
address: 72,
|
||||
target_symbol: 5,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
6,
|
||||
),
|
||||
address: 98,
|
||||
target_symbol: 8,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
4,
|
||||
),
|
||||
address: 102,
|
||||
target_symbol: 8,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
109,
|
||||
),
|
||||
address: 112,
|
||||
target_symbol: 5,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
6,
|
||||
),
|
||||
address: 138,
|
||||
target_symbol: 8,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
4,
|
||||
),
|
||||
address: 142,
|
||||
target_symbol: 8,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
109,
|
||||
),
|
||||
address: 148,
|
||||
target_symbol: 5,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
6,
|
||||
),
|
||||
address: 162,
|
||||
target_symbol: 9,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
4,
|
||||
),
|
||||
address: 166,
|
||||
target_symbol: 9,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
109,
|
||||
),
|
||||
address: 176,
|
||||
target_symbol: 5,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
109,
|
||||
),
|
||||
address: 256,
|
||||
target_symbol: 5,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
109,
|
||||
),
|
||||
address: 268,
|
||||
target_symbol: 7,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
109,
|
||||
),
|
||||
address: 272,
|
||||
target_symbol: 7,
|
||||
addend: 0,
|
||||
},
|
||||
],
|
||||
line_info: {},
|
||||
virtual_address: Some(
|
||||
2150895620,
|
||||
),
|
||||
},
|
||||
Section {
|
||||
id: ".ctors-0",
|
||||
name: ".ctors",
|
||||
address: 0,
|
||||
size: 4,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
4,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
1,
|
||||
),
|
||||
address: 0,
|
||||
target_symbol: 4,
|
||||
addend: 0,
|
||||
},
|
||||
],
|
||||
line_info: {},
|
||||
virtual_address: Some(
|
||||
2151461704,
|
||||
),
|
||||
},
|
||||
Section {
|
||||
id: ".sbss-0",
|
||||
name: ".sbss",
|
||||
address: 0,
|
||||
size: 16,
|
||||
kind: Bss,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: Some(
|
||||
2153420048,
|
||||
),
|
||||
},
|
||||
Section {
|
||||
id: ".rela.text-0",
|
||||
name: ".rela.text",
|
||||
address: 0,
|
||||
size: 216,
|
||||
kind: Unknown,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".rela.ctors-0",
|
||||
name: ".rela.ctors",
|
||||
address: 0,
|
||||
size: 12,
|
||||
kind: Unknown,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".symtab-0",
|
||||
name: ".symtab",
|
||||
address: 0,
|
||||
size: 176,
|
||||
kind: Unknown,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".strtab-0",
|
||||
name: ".strtab",
|
||||
address: 0,
|
||||
size: 105,
|
||||
kind: Unknown,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".shstrtab-0",
|
||||
name: ".shstrtab",
|
||||
address: 0,
|
||||
size: 77,
|
||||
kind: Unknown,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".comment-0",
|
||||
name: ".comment",
|
||||
address: 0,
|
||||
size: 132,
|
||||
kind: Unknown,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".note.split-0",
|
||||
name: ".note.split",
|
||||
address: 0,
|
||||
size: 152,
|
||||
kind: Unknown,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
],
|
||||
split_meta: Some(
|
||||
SplitMeta {
|
||||
generator: Some(
|
||||
"decomp-toolkit 1.4.0",
|
||||
),
|
||||
module_name: Some(
|
||||
"main",
|
||||
),
|
||||
module_id: Some(
|
||||
0,
|
||||
),
|
||||
virtual_addresses: Some(
|
||||
[
|
||||
0,
|
||||
0,
|
||||
2150895620,
|
||||
2151461704,
|
||||
2153420048,
|
||||
2150895884,
|
||||
2153420056,
|
||||
2150895620,
|
||||
2153420048,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
),
|
||||
},
|
||||
),
|
||||
path: None,
|
||||
timestamp: None,
|
||||
}
|
||||
97
objdiff-core/tests/snapshots/arch_x86__read_x86-2.snap
Normal file
@@ -0,0 +1,97 @@
|
||||
---
|
||||
source: objdiff-core/tests/arch_x86.rs
|
||||
expression: diff.instruction_rows
|
||||
---
|
||||
[
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 0,
|
||||
size: 1,
|
||||
opcode: 640,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 1,
|
||||
size: 2,
|
||||
opcode: 414,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 3,
|
||||
size: 5,
|
||||
opcode: 640,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 8,
|
||||
size: 5,
|
||||
opcode: 59,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 13,
|
||||
size: 3,
|
||||
opcode: 7,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 16,
|
||||
size: 1,
|
||||
opcode: 590,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 17,
|
||||
size: 1,
|
||||
opcode: 662,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
]
|
||||
11
objdiff-core/tests/snapshots/arch_x86__read_x86-3.snap
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
source: objdiff-core/tests/arch_x86.rs
|
||||
expression: output
|
||||
---
|
||||
[(Address(0), Normal, 5), (Spacing(4), Normal, 0), (Opcode("push", 640), Normal, 10), (Argument(Opaque("ebp")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(1), Normal, 5), (Spacing(4), Normal, 0), (Opcode("mov", 414), Normal, 10), (Argument(Opaque("ebp")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Argument(Opaque("esp")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(3), Normal, 5), (Spacing(4), Normal, 0), (Opcode("push", 640), Normal, 10), (Symbol(Symbol { name: "$SG526", demangled_name: None, address: 4, size: 6, kind: Object, section: Some(1), flags: FlagSet(Local | SizeInferred), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(8), Normal, 5), (Spacing(4), Normal, 0), (Opcode("call", 59), Normal, 10), (Symbol(Symbol { name: "_printf", demangled_name: None, address: 0, size: 0, kind: Function, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(13), Normal, 5), (Spacing(4), Normal, 0), (Opcode("add", 7), Normal, 10), (Argument(Opaque("esp")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Argument(Unsigned(4)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(16), Normal, 5), (Spacing(4), Normal, 0), (Opcode("pop", 590), Normal, 10), (Argument(Opaque("ebp")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(17), Normal, 5), (Spacing(4), Normal, 0), (Opcode("ret", 662), Normal, 10), (Eol, Normal, 0)]
|
||||
201
objdiff-core/tests/snapshots/arch_x86__read_x86.snap
Normal file
@@ -0,0 +1,201 @@
|
||||
---
|
||||
source: objdiff-core/tests/arch_x86.rs
|
||||
expression: obj
|
||||
---
|
||||
Object {
|
||||
arch: ArchX86 {
|
||||
bits: 32,
|
||||
endianness: Little,
|
||||
},
|
||||
endianness: Little,
|
||||
symbols: [
|
||||
Symbol {
|
||||
name: "objdiffstaticdebug.cpp",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Unknown,
|
||||
section: None,
|
||||
flags: FlagSet(Local),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "@comp.id",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Object,
|
||||
section: None,
|
||||
flags: FlagSet(Local),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "[.drectve]",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 38,
|
||||
kind: Section,
|
||||
section: Some(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Local),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "[.data]",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Section,
|
||||
section: Some(
|
||||
1,
|
||||
),
|
||||
flags: FlagSet(Local),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "?a@@3PAXA",
|
||||
demangled_name: Some(
|
||||
"void *a",
|
||||
),
|
||||
address: 0,
|
||||
size: 4,
|
||||
kind: Object,
|
||||
section: Some(
|
||||
1,
|
||||
),
|
||||
flags: FlagSet(Global | SizeInferred),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "[.text]",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Section,
|
||||
section: Some(
|
||||
2,
|
||||
),
|
||||
flags: FlagSet(Local),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "?PrintThing@@YAXXZ",
|
||||
demangled_name: Some(
|
||||
"void __cdecl PrintThing(void)",
|
||||
),
|
||||
address: 0,
|
||||
size: 18,
|
||||
kind: Function,
|
||||
section: Some(
|
||||
2,
|
||||
),
|
||||
flags: FlagSet(Local | SizeInferred),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "_printf",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Function,
|
||||
section: None,
|
||||
flags: FlagSet(Global),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "$SG526",
|
||||
demangled_name: None,
|
||||
address: 4,
|
||||
size: 6,
|
||||
kind: Object,
|
||||
section: Some(
|
||||
1,
|
||||
),
|
||||
flags: FlagSet(Local | SizeInferred),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
],
|
||||
sections: [
|
||||
Section {
|
||||
id: ".drectve-0",
|
||||
name: ".drectve",
|
||||
address: 0,
|
||||
size: 38,
|
||||
kind: Unknown,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".data-0",
|
||||
name: ".data",
|
||||
address: 0,
|
||||
size: 10,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
10,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 0,
|
||||
target_symbol: 6,
|
||||
addend: 0,
|
||||
},
|
||||
],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".text-0",
|
||||
name: ".text",
|
||||
address: 0,
|
||||
size: 18,
|
||||
kind: Code,
|
||||
data: SectionData(
|
||||
18,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 4,
|
||||
target_symbol: 8,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
20,
|
||||
),
|
||||
address: 9,
|
||||
target_symbol: 7,
|
||||
addend: 0,
|
||||
},
|
||||
],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
],
|
||||
split_meta: None,
|
||||
path: None,
|
||||
timestamp: None,
|
||||
}
|
||||
@@ -0,0 +1,940 @@
|
||||
---
|
||||
source: objdiff-core/tests/arch_x86.rs
|
||||
expression: obj.sections
|
||||
---
|
||||
[
|
||||
Section {
|
||||
id: ".drectve-0",
|
||||
name: ".drectve",
|
||||
address: 0,
|
||||
size: 47,
|
||||
kind: Unknown,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".debug$S-0",
|
||||
name: ".debug$S",
|
||||
address: 0,
|
||||
size: 100,
|
||||
kind: Unknown,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".rdata-0",
|
||||
name: ".rdata",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".rdata-1",
|
||||
name: ".rdata",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".text$mn-0",
|
||||
name: ".text$mn",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Code,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".data-combined",
|
||||
name: ".data",
|
||||
address: 0,
|
||||
size: 56,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
56,
|
||||
),
|
||||
flags: FlagSet(Combined),
|
||||
relocations: [
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 0,
|
||||
target_symbol: 44,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 16,
|
||||
target_symbol: 44,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 32,
|
||||
target_symbol: 44,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 48,
|
||||
target_symbol: 6,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 52,
|
||||
target_symbol: 8,
|
||||
addend: 0,
|
||||
},
|
||||
],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".rdata-combined",
|
||||
name: ".rdata",
|
||||
address: 0,
|
||||
size: 295,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
295,
|
||||
),
|
||||
flags: FlagSet(Combined),
|
||||
relocations: [
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 12,
|
||||
target_symbol: 17,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 16,
|
||||
target_symbol: 19,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 21,
|
||||
target_symbol: 13,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 45,
|
||||
target_symbol: 15,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 61,
|
||||
target_symbol: 25,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 65,
|
||||
target_symbol: 27,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 70,
|
||||
target_symbol: 21,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 94,
|
||||
target_symbol: 23,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 110,
|
||||
target_symbol: 31,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 114,
|
||||
target_symbol: 33,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 130,
|
||||
target_symbol: 35,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 134,
|
||||
target_symbol: 37,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 138,
|
||||
target_symbol: 19,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 142,
|
||||
target_symbol: 39,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 147,
|
||||
target_symbol: 31,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 171,
|
||||
target_symbol: 33,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 175,
|
||||
target_symbol: 21,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 199,
|
||||
target_symbol: 23,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 215,
|
||||
target_symbol: 31,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 219,
|
||||
target_symbol: 33,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 235,
|
||||
target_symbol: 13,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 239,
|
||||
target_symbol: 15,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 255,
|
||||
target_symbol: 21,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 259,
|
||||
target_symbol: 23,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 263,
|
||||
target_symbol: 29,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 267,
|
||||
target_symbol: 11,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 271,
|
||||
target_symbol: 43,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 275,
|
||||
target_symbol: 41,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 279,
|
||||
target_symbol: 70,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 283,
|
||||
target_symbol: 56,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 287,
|
||||
target_symbol: 72,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 291,
|
||||
target_symbol: 59,
|
||||
addend: 0,
|
||||
},
|
||||
],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".rdata$r-1",
|
||||
name: ".rdata$r",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".rdata$r-2",
|
||||
name: ".rdata$r",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".data$rs-1",
|
||||
name: ".data$rs",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".rdata$r-3",
|
||||
name: ".rdata$r",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".rdata$r-4",
|
||||
name: ".rdata$r",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".rdata$r-5",
|
||||
name: ".rdata$r",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".rdata$r-6",
|
||||
name: ".rdata$r",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".data$rs-2",
|
||||
name: ".data$rs",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".rdata$r-7",
|
||||
name: ".rdata$r",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".rdata$r-8",
|
||||
name: ".rdata$r",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".rdata$r-9",
|
||||
name: ".rdata$r",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".rdata$r-10",
|
||||
name: ".rdata$r",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".text$mn-1",
|
||||
name: ".text$mn",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Code,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".rdata$r-11",
|
||||
name: ".rdata$r",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".text$mn-2",
|
||||
name: ".text$mn",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Code,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".text$mn-3",
|
||||
name: ".text$mn",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Code,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".text$mn-4",
|
||||
name: ".text$mn",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Code,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".text$mn-5",
|
||||
name: ".text$mn",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Code,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".text$mn-6",
|
||||
name: ".text$mn",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Code,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".text-combined",
|
||||
name: ".text",
|
||||
address: 0,
|
||||
size: 268,
|
||||
kind: Code,
|
||||
data: SectionData(
|
||||
268,
|
||||
),
|
||||
flags: FlagSet(Combined),
|
||||
relocations: [
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 4,
|
||||
target_symbol: 62,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
20,
|
||||
),
|
||||
address: 9,
|
||||
target_symbol: 53,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
20,
|
||||
),
|
||||
address: 29,
|
||||
target_symbol: 60,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
20,
|
||||
),
|
||||
address: 48,
|
||||
target_symbol: 52,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
20,
|
||||
),
|
||||
address: 68,
|
||||
target_symbol: 11,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 84,
|
||||
target_symbol: 64,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 104,
|
||||
target_symbol: 66,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 124,
|
||||
target_symbol: 6,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 134,
|
||||
target_symbol: 8,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
20,
|
||||
),
|
||||
address: 145,
|
||||
target_symbol: 57,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
20,
|
||||
),
|
||||
address: 153,
|
||||
target_symbol: 54,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
20,
|
||||
),
|
||||
address: 172,
|
||||
target_symbol: 54,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
20,
|
||||
),
|
||||
address: 191,
|
||||
target_symbol: 52,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
20,
|
||||
),
|
||||
address: 218,
|
||||
target_symbol: 57,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
20,
|
||||
),
|
||||
address: 237,
|
||||
target_symbol: 52,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 257,
|
||||
target_symbol: 68,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
20,
|
||||
),
|
||||
address: 262,
|
||||
target_symbol: 60,
|
||||
addend: 0,
|
||||
},
|
||||
],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".text$yd-0",
|
||||
name: ".text$yd",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Code,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".rdata-2",
|
||||
name: ".rdata",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".rdata-3",
|
||||
name: ".rdata",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".data-0",
|
||||
name: ".data",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".rdata$r-12",
|
||||
name: ".rdata$r",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".rdata$r-13",
|
||||
name: ".rdata$r",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(Hidden),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".CRT$XCU-0",
|
||||
name: ".CRT$XCU",
|
||||
address: 0,
|
||||
size: 4,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
4,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [
|
||||
Relocation {
|
||||
flags: Coff(
|
||||
6,
|
||||
),
|
||||
address: 0,
|
||||
target_symbol: 61,
|
||||
addend: 0,
|
||||
},
|
||||
],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".chks64-0",
|
||||
name: ".chks64",
|
||||
address: 0,
|
||||
size: 280,
|
||||
kind: Unknown,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
]
|
||||
88
objdiff-gui/Cargo.toml
Normal file
@@ -0,0 +1,88 @@
|
||||
[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"
|
||||
cfg-if = "1.0"
|
||||
const_format = "0.2"
|
||||
cwdemangle = "1.0"
|
||||
cwextab = { version = "1.0", git = "https://github.com/CelestialAmber/cwextab.git" }
|
||||
dirs = "6.0"
|
||||
egui = "0.31"
|
||||
egui_extras = "0.31"
|
||||
filetime = "0.2"
|
||||
float-ord = "0.3"
|
||||
font-kit = "0.14"
|
||||
globset = { version = "0.4", features = ["serde1"] }
|
||||
log = "0.4"
|
||||
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 = { version = "1.0", git = "https://github.com/CelestialAmber/rlwinmdec.git" }
|
||||
ron = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
shell-escape = "0.1"
|
||||
time = { version = "0.3", features = ["formatting", "local-offset"] }
|
||||
typed-path = "0.10"
|
||||
winit = { version = "0.30", features = ["wayland-csd-adwaita"] }
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
# Keep version in sync with egui
|
||||
[dependencies.eframe]
|
||||
version = "0.31"
|
||||
features = [
|
||||
"default_fonts",
|
||||
"persistence",
|
||||
"wayland",
|
||||
"x11",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
# Keep version in sync with eframe
|
||||
[dependencies.wgpu]
|
||||
version = "24.0"
|
||||
features = [
|
||||
"dx12",
|
||||
"metal",
|
||||
"webgpu",
|
||||
]
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winapi = "0.3"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
exec = "0.3"
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = "1.0"
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
tauri-winres = "0.3"
|
||||
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(())
|
||||
}
|
||||
818
objdiff-gui/src/app.rs
Normal file
@@ -0,0 +1,818 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
default::Default,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex, RwLock,
|
||||
},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use filetime::FileTime;
|
||||
use globset::Glob;
|
||||
use objdiff_core::{
|
||||
build::watcher::{create_watcher, Watcher},
|
||||
config::{
|
||||
build_globset, default_watch_patterns, path::platform_path_serde_option,
|
||||
save_project_config, ProjectConfig, ProjectConfigInfo, ProjectObject, ScratchConfig,
|
||||
DEFAULT_WATCH_PATTERNS,
|
||||
},
|
||||
diff::DiffObjConfig,
|
||||
jobs::{Job, JobQueue, JobResult},
|
||||
};
|
||||
use time::UtcOffset;
|
||||
use typed_path::{Utf8PlatformPath, Utf8PlatformPathBuf};
|
||||
|
||||
use crate::{
|
||||
app_config::{deserialize_config, AppConfigVersion},
|
||||
config::{load_project_config, ProjectObjectNode},
|
||||
jobs::{create_objdiff_config, egui_waker, start_build},
|
||||
views::{
|
||||
appearance::{appearance_window, Appearance},
|
||||
config::{
|
||||
arch_config_window, config_ui, general_config_ui, project_window, ConfigViewState,
|
||||
CONFIG_DISABLED_TEXT,
|
||||
},
|
||||
debug::debug_window,
|
||||
demangle::{demangle_window, DemangleViewState},
|
||||
diff::diff_view_ui,
|
||||
frame_history::FrameHistory,
|
||||
graphics::{graphics_window, GraphicsConfig, GraphicsViewState},
|
||||
jobs::{jobs_menu_ui, jobs_window},
|
||||
rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState},
|
||||
symbol_diff::{DiffViewAction, DiffViewState, ResolvedNavigation, 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,
|
||||
#[serde(default, with = "platform_path_serde_option")]
|
||||
pub target_path: Option<Utf8PlatformPathBuf>,
|
||||
#[serde(default, with = "platform_path_serde_option")]
|
||||
pub base_path: Option<Utf8PlatformPathBuf>,
|
||||
pub reverse_fn_order: Option<bool>,
|
||||
pub complete: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub hidden: bool,
|
||||
pub scratch: Option<ScratchConfig>,
|
||||
#[serde(default, with = "platform_path_serde_option")]
|
||||
pub source_path: Option<Utf8PlatformPathBuf>,
|
||||
#[serde(default)]
|
||||
pub symbol_mappings: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl ObjectConfig {
|
||||
pub fn new(
|
||||
object: &ProjectObject,
|
||||
project_dir: &Utf8PlatformPath,
|
||||
target_obj_dir: Option<&Utf8PlatformPath>,
|
||||
base_obj_dir: Option<&Utf8PlatformPath>,
|
||||
) -> Self {
|
||||
let target_path = if let (Some(target_obj_dir), Some(path), None) =
|
||||
(target_obj_dir, &object.path, &object.target_path)
|
||||
{
|
||||
Some(target_obj_dir.join(path.with_platform_encoding()))
|
||||
} else {
|
||||
object.target_path.as_ref().map(|path| project_dir.join(path.with_platform_encoding()))
|
||||
};
|
||||
let base_path = if let (Some(base_obj_dir), Some(path), None) =
|
||||
(base_obj_dir, &object.path, &object.base_path)
|
||||
{
|
||||
Some(base_obj_dir.join(path.with_platform_encoding()))
|
||||
} else {
|
||||
object.base_path.as_ref().map(|path| project_dir.join(path.with_platform_encoding()))
|
||||
};
|
||||
let source_path =
|
||||
object.source_path().map(|s| project_dir.join(s.with_platform_encoding()));
|
||||
Self {
|
||||
name: object.name().to_string(),
|
||||
target_path,
|
||||
base_path,
|
||||
reverse_fn_order: object.reverse_fn_order(),
|
||||
complete: object.complete(),
|
||||
hidden: object.hidden(),
|
||||
scratch: object.scratch.clone(),
|
||||
source_path,
|
||||
symbol_mappings: object.symbol_mappings.clone().unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn bool_true() -> bool { true }
|
||||
|
||||
pub struct AppState {
|
||||
pub config: AppConfig,
|
||||
pub objects: Vec<ObjectConfig>,
|
||||
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, with = "platform_path_serde_option")]
|
||||
pub project_dir: Option<Utf8PlatformPathBuf>,
|
||||
#[serde(default, with = "platform_path_serde_option")]
|
||||
pub target_obj_dir: Option<Utf8PlatformPathBuf>,
|
||||
#[serde(default, with = "platform_path_serde_option")]
|
||||
pub base_obj_dir: Option<Utf8PlatformPathBuf>,
|
||||
#[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<String>,
|
||||
#[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: Utf8PlatformPathBuf) {
|
||||
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.to_string());
|
||||
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: Utf8PlatformPathBuf) {
|
||||
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: Utf8PlatformPathBuf) {
|
||||
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.retain(|_, r| r != 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.retain(|l, _| l != 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;
|
||||
object.symbol_mappings.retain(|l, r| l != &left && r != &right);
|
||||
if left != right {
|
||||
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 == object.name) {
|
||||
existing.symbol_mappings = 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<Watcher>,
|
||||
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;
|
||||
|
||||
jobs.collect_results();
|
||||
jobs.results.retain(|result| match result {
|
||||
JobResult::Update(state) => {
|
||||
if let Ok(mut guard) = self.relaunch_path.lock() {
|
||||
*guard = Some(state.exe_path.clone());
|
||||
self.should_relaunch = true;
|
||||
}
|
||||
false
|
||||
}
|
||||
_ => true,
|
||||
});
|
||||
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>) {
|
||||
if action.is_some() {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
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(
|
||||
self.modified.clone(),
|
||||
project_dir.as_ref(),
|
||||
globset,
|
||||
egui_waker(ctx),
|
||||
)
|
||||
.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)
|
||||
{
|
||||
start_build(ctx, jobs, create_objdiff_config(state));
|
||||
state.queue_build = false;
|
||||
state.queue_reload = false;
|
||||
} else if state.queue_reload && !jobs.is_running(Job::ObjDiff) {
|
||||
let mut diff_config = create_objdiff_config(state);
|
||||
// Don't build, just reload the current files
|
||||
diff_config.build_base = false;
|
||||
diff_config.build_target = false;
|
||||
start_build(ctx, jobs, 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(&path).clicked() {
|
||||
state
|
||||
.write()
|
||||
.unwrap()
|
||||
.set_project_dir(Utf8PlatformPathBuf::from(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",
|
||||
);
|
||||
ui.separator();
|
||||
general_config_ui(ui, &mut state);
|
||||
ui.separator();
|
||||
if ui.button("Clear custom symbol mappings").clicked() {
|
||||
state.clear_mappings();
|
||||
diff_state.post_build_nav = Some(ResolvedNavigation::default());
|
||||
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 state = state.read().unwrap();
|
||||
action = diff_view_ui(ui, diff_state, appearance, &state.config.diff_obj_config);
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn file_modified<P: AsRef<Path>>(path: P, last_ts: FileTime) -> bool {
|
||||
if let Ok(metadata) = fs::metadata(path.as_ref()) {
|
||||
FileTime::from_last_modification_time(&metadata) != last_ts
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
399
objdiff-gui/src/app_config.rs
Normal file
@@ -0,0 +1,399 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use eframe::Storage;
|
||||
use globset::Glob;
|
||||
use objdiff_core::{
|
||||
config::ScratchConfig,
|
||||
diff::{
|
||||
ArmArchVersion, ArmR9Usage, DiffObjConfig, FunctionRelocDiffs, MipsAbi, MipsInstrCategory,
|
||||
X86Formatter,
|
||||
},
|
||||
};
|
||||
use typed_path::{Utf8PlatformPathBuf, Utf8UnixPathBuf};
|
||||
|
||||
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: 3 } }
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
3 => from_str::<AppConfig>(&str),
|
||||
2 => from_str::<AppConfigV2>(&str).map(|c| c.into_config()),
|
||||
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 ScratchConfigV2 {
|
||||
#[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<String>,
|
||||
#[serde(default)]
|
||||
pub build_ctx: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub preset_id: Option<u32>,
|
||||
}
|
||||
|
||||
impl ScratchConfigV2 {
|
||||
fn into_config(self) -> ScratchConfig {
|
||||
ScratchConfig {
|
||||
platform: self.platform,
|
||||
compiler: self.compiler,
|
||||
c_flags: self.c_flags,
|
||||
ctx_path: self.ctx_path.map(Utf8UnixPathBuf::from),
|
||||
build_ctx: self.build_ctx,
|
||||
preset_id: self.preset_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct ObjectConfigV2 {
|
||||
pub name: String,
|
||||
pub target_path: Option<String>,
|
||||
pub base_path: Option<String>,
|
||||
pub reverse_fn_order: Option<bool>,
|
||||
pub complete: Option<bool>,
|
||||
pub scratch: Option<ScratchConfigV2>,
|
||||
pub source_path: Option<String>,
|
||||
#[serde(default)]
|
||||
pub symbol_mappings: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl ObjectConfigV2 {
|
||||
fn into_config(self) -> ObjectConfig {
|
||||
ObjectConfig {
|
||||
name: self.name,
|
||||
target_path: self.target_path.map(Utf8PlatformPathBuf::from),
|
||||
base_path: self.base_path.map(Utf8PlatformPathBuf::from),
|
||||
reverse_fn_order: self.reverse_fn_order,
|
||||
complete: self.complete,
|
||||
hidden: false,
|
||||
scratch: self.scratch.map(|scratch| scratch.into_config()),
|
||||
source_path: None,
|
||||
symbol_mappings: self.symbol_mappings,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct AppConfigV2 {
|
||||
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<String>,
|
||||
#[serde(default)]
|
||||
pub target_obj_dir: Option<String>,
|
||||
#[serde(default)]
|
||||
pub base_obj_dir: Option<String>,
|
||||
#[serde(default)]
|
||||
pub selected_obj: Option<ObjectConfigV2>,
|
||||
#[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<String>,
|
||||
#[serde(default)]
|
||||
pub diff_obj_config: DiffObjConfigV1,
|
||||
}
|
||||
|
||||
impl AppConfigV2 {
|
||||
fn into_config(self) -> AppConfig {
|
||||
log::info!("Upgrading configuration from v2");
|
||||
AppConfig {
|
||||
custom_make: self.custom_make,
|
||||
custom_args: self.custom_args,
|
||||
selected_wsl_distro: self.selected_wsl_distro,
|
||||
project_dir: self.project_dir.map(Utf8PlatformPathBuf::from),
|
||||
target_obj_dir: self.target_obj_dir.map(Utf8PlatformPathBuf::from),
|
||||
base_obj_dir: self.base_obj_dir.map(Utf8PlatformPathBuf::from),
|
||||
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 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<String>,
|
||||
#[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.map(Utf8UnixPathBuf::from),
|
||||
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<String>,
|
||||
pub base_path: Option<String>,
|
||||
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.map(Utf8PlatformPathBuf::from),
|
||||
base_path: self.base_path.map(Utf8PlatformPathBuf::from),
|
||||
reverse_fn_order: self.reverse_fn_order,
|
||||
complete: self.complete,
|
||||
scratch: self.scratch.map(|scratch| scratch.into_config()),
|
||||
source_path: None,
|
||||
..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 {
|
||||
function_reloc_diffs: if self.relax_reloc_diffs {
|
||||
FunctionRelocDiffs::None
|
||||
} else {
|
||||
FunctionRelocDiffs::default()
|
||||
},
|
||||
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<String>,
|
||||
#[serde(default)]
|
||||
pub target_obj_dir: Option<String>,
|
||||
#[serde(default)]
|
||||
pub base_obj_dir: Option<String>,
|
||||
#[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<String>,
|
||||
#[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.map(Utf8PlatformPathBuf::from),
|
||||
target_obj_dir: self.target_obj_dir.map(Utf8PlatformPathBuf::from),
|
||||
base_obj_dir: self.base_obj_dir.map(Utf8PlatformPathBuf::from),
|
||||
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: String,
|
||||
pub base_path: String,
|
||||
pub reverse_fn_order: Option<bool>,
|
||||
}
|
||||
|
||||
impl ObjectConfigV0 {
|
||||
fn into_config(self) -> ObjectConfig {
|
||||
ObjectConfig {
|
||||
name: self.name,
|
||||
target_path: Some(Utf8PlatformPathBuf::from(self.target_path)),
|
||||
base_path: Some(Utf8PlatformPathBuf::from(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<String>,
|
||||
pub target_obj_dir: Option<String>,
|
||||
pub base_obj_dir: Option<String>,
|
||||
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.map(Utf8PlatformPathBuf::from),
|
||||
target_obj_dir: self.target_obj_dir.map(Utf8PlatformPathBuf::from),
|
||||
base_obj_dir: self.base_obj_dir.map(Utf8PlatformPathBuf::from),
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
131
objdiff-gui/src/config.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use anyhow::Result;
|
||||
use globset::Glob;
|
||||
use objdiff_core::config::{try_project_config, DEFAULT_WATCH_PATTERNS};
|
||||
use typed_path::{Utf8UnixComponent, Utf8UnixPath};
|
||||
|
||||
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 [ObjectConfig]) -> Vec<ProjectObjectNode> {
|
||||
let mut nodes = vec![];
|
||||
for (idx, unit) in units.iter_mut().enumerate() {
|
||||
let mut out_nodes = &mut nodes;
|
||||
let path = Utf8UnixPath::new(&unit.name);
|
||||
if let Some(parent) = path.parent() {
|
||||
for component in parent.components() {
|
||||
if let Utf8UnixComponent::Normal(name) = component {
|
||||
out_nodes = find_dir(name, out_nodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
let filename = path.file_name().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.as_ref()) {
|
||||
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.with_platform_encoding()));
|
||||
state.config.base_obj_dir = project_config
|
||||
.base_dir
|
||||
.as_deref()
|
||||
.map(|p| project_dir.join(p.with_platform_encoding()));
|
||||
state.config.build_base = project_config.build_base.unwrap_or(true);
|
||||
state.config.build_target = project_config.build_target.unwrap_or(false);
|
||||
if let Some(watch_patterns) = &project_config.watch_patterns {
|
||||
state.config.watch_patterns = watch_patterns
|
||||
.iter()
|
||||
.map(|s| Glob::new(s))
|
||||
.collect::<Result<Vec<Glob>, globset::Error>>()?;
|
||||
} else {
|
||||
state.config.watch_patterns =
|
||||
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect();
|
||||
}
|
||||
state.watcher_change = true;
|
||||
state.objects = project_config
|
||||
.units
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|o| {
|
||||
ObjectConfig::new(
|
||||
o,
|
||||
project_dir,
|
||||
state.config.target_obj_dir.as_deref(),
|
||||
state.config.base_obj_dir.as_deref(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
state.object_nodes = build_nodes(&mut state.objects);
|
||||
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) {
|
||||
state.set_selected_obj(obj.clone());
|
||||
} 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(), Arc::new(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(())
|
||||
}
|
||||
108
objdiff-gui/src/hotkeys.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use egui::{
|
||||
style::ScrollAnimation, vec2, Context, Key, KeyboardShortcut, Modifiers, PointerButton,
|
||||
};
|
||||
|
||||
fn any_widget_focused(ctx: &Context) -> bool { ctx.memory(|mem| mem.focused().is_some()) }
|
||||
|
||||
pub fn enter_pressed(ctx: &Context) -> bool {
|
||||
if any_widget_focused(ctx) {
|
||||
return false;
|
||||
}
|
||||
ctx.input_mut(|i| {
|
||||
i.key_pressed(Key::Enter)
|
||||
|| i.key_pressed(Key::Space)
|
||||
|| i.pointer.button_pressed(PointerButton::Extra2)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn back_pressed(ctx: &Context) -> bool {
|
||||
if any_widget_focused(ctx) {
|
||||
return false;
|
||||
}
|
||||
ctx.input_mut(|i| {
|
||||
i.key_pressed(Key::Backspace)
|
||||
|| i.key_pressed(Key::Escape)
|
||||
|| i.pointer.button_pressed(PointerButton::Extra1)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn up_pressed(ctx: &Context) -> bool {
|
||||
if any_widget_focused(ctx) {
|
||||
return false;
|
||||
}
|
||||
ctx.input_mut(|i| i.key_pressed(Key::ArrowUp) || i.key_pressed(Key::W))
|
||||
}
|
||||
|
||||
pub fn down_pressed(ctx: &Context) -> bool {
|
||||
if any_widget_focused(ctx) {
|
||||
return false;
|
||||
}
|
||||
ctx.input_mut(|i| i.key_pressed(Key::ArrowDown) || i.key_pressed(Key::S))
|
||||
}
|
||||
|
||||
pub fn page_up_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::PageUp)) }
|
||||
|
||||
pub fn page_down_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::PageDown)) }
|
||||
|
||||
pub fn home_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::Home)) }
|
||||
|
||||
pub fn end_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::End)) }
|
||||
|
||||
pub fn check_scroll_hotkeys(ui: &mut egui::Ui, include_small_increments: bool) {
|
||||
let ui_height = ui.available_rect_before_wrap().height();
|
||||
if up_pressed(ui.ctx()) && include_small_increments {
|
||||
ui.scroll_with_delta_animation(vec2(0.0, ui_height / 10.0), ScrollAnimation::none());
|
||||
} else if down_pressed(ui.ctx()) && include_small_increments {
|
||||
ui.scroll_with_delta_animation(vec2(0.0, -ui_height / 10.0), ScrollAnimation::none());
|
||||
} else if page_up_pressed(ui.ctx()) {
|
||||
ui.scroll_with_delta_animation(vec2(0.0, ui_height), ScrollAnimation::none());
|
||||
} else if page_down_pressed(ui.ctx()) {
|
||||
ui.scroll_with_delta_animation(vec2(0.0, -ui_height), ScrollAnimation::none());
|
||||
} else if home_pressed(ui.ctx()) {
|
||||
ui.scroll_with_delta_animation(vec2(0.0, f32::INFINITY), ScrollAnimation::none());
|
||||
} else if end_pressed(ui.ctx()) {
|
||||
ui.scroll_with_delta_animation(vec2(0.0, -f32::INFINITY), ScrollAnimation::none());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn consume_up_key(ctx: &Context) -> bool {
|
||||
if any_widget_focused(ctx) {
|
||||
return false;
|
||||
}
|
||||
ctx.input_mut(|i| {
|
||||
i.consume_key(Modifiers::NONE, Key::ArrowUp) || i.consume_key(Modifiers::NONE, Key::W)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn consume_down_key(ctx: &Context) -> bool {
|
||||
if any_widget_focused(ctx) {
|
||||
return false;
|
||||
}
|
||||
ctx.input_mut(|i| {
|
||||
i.consume_key(Modifiers::NONE, Key::ArrowDown) || i.consume_key(Modifiers::NONE, Key::S)
|
||||
})
|
||||
}
|
||||
|
||||
const OBJECT_FILTER_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::F);
|
||||
|
||||
pub fn consume_object_filter_shortcut(ctx: &Context) -> bool {
|
||||
ctx.input_mut(|i| i.consume_shortcut(&OBJECT_FILTER_SHORTCUT))
|
||||
}
|
||||
|
||||
const SYMBOL_FILTER_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::S);
|
||||
|
||||
pub fn consume_symbol_filter_shortcut(ctx: &Context) -> bool {
|
||||
ctx.input_mut(|i| i.consume_shortcut(&SYMBOL_FILTER_SHORTCUT))
|
||||
}
|
||||
|
||||
const CHANGE_TARGET_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::T);
|
||||
|
||||
pub fn consume_change_target_shortcut(ctx: &Context) -> bool {
|
||||
ctx.input_mut(|i| i.consume_shortcut(&CHANGE_TARGET_SHORTCUT))
|
||||
}
|
||||
|
||||
const CHANGE_BASE_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::B);
|
||||
|
||||
pub fn consume_change_base_shortcut(ctx: &Context) -> bool {
|
||||
ctx.input_mut(|i| i.consume_shortcut(&CHANGE_BASE_SHORTCUT))
|
||||
}
|
||||