mirror of
https://github.com/encounter/objdiff.git
synced 2025-12-19 18:05:35 +00:00
Compare commits
81 Commits
v2.0.0-bet
...
v2.7.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
5
.cargo/config.toml
Normal file
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"
|
||||||
88
.github/workflows/build.yaml
vendored
88
.github/workflows/build.yaml
vendored
@@ -11,6 +11,7 @@ on:
|
|||||||
env:
|
env:
|
||||||
BUILD_PROFILE: release-lto
|
BUILD_PROFILE: release-lto
|
||||||
CARGO_TARGET_DIR: target
|
CARGO_TARGET_DIR: target
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
@@ -25,37 +26,16 @@ jobs:
|
|||||||
sudo apt-get -y install libgtk-3-dev
|
sudo apt-get -y install libgtk-3-dev
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Check git tag against Cargo version
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -eou pipefail
|
|
||||||
tag='${{github.ref}}'
|
|
||||||
tag="${tag#refs/tags/}"
|
|
||||||
for file in */Cargo.toml; do
|
|
||||||
version=$(grep '^version' $file | 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
|
|
||||||
done
|
|
||||||
- name: Setup Rust toolchain
|
- name: Setup Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
components: clippy
|
components: clippy
|
||||||
- name: Setup sccache
|
- name: Cache Rust workspace
|
||||||
uses: mozilla-actions/sccache-action@v0.0.4
|
uses: Swatinem/rust-cache@v2
|
||||||
- name: Cargo check
|
- name: Cargo check
|
||||||
env:
|
run: cargo check --all-features --all-targets
|
||||||
RUSTC_WRAPPER: sccache
|
|
||||||
SCCACHE_GHA_ENABLED: "true"
|
|
||||||
run: cargo check
|
|
||||||
- name: Cargo clippy
|
- name: Cargo clippy
|
||||||
env:
|
run: cargo clippy --all-features --all-targets
|
||||||
RUSTC_WRAPPER: sccache
|
|
||||||
SCCACHE_GHA_ENABLED: "true"
|
|
||||||
run: cargo clippy
|
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
name: Format
|
name: Format
|
||||||
@@ -85,7 +65,7 @@ jobs:
|
|||||||
continue-on-error: ${{ matrix.checks == 'advisories' }}
|
continue-on-error: ${{ matrix.checks == 'advisories' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||||
with:
|
with:
|
||||||
command: check ${{ matrix.checks }}
|
command: check ${{ matrix.checks }}
|
||||||
|
|
||||||
@@ -107,13 +87,10 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Setup Rust toolchain
|
- name: Setup Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
- name: Setup sccache
|
- name: Cache Rust workspace
|
||||||
uses: mozilla-actions/sccache-action@v0.0.4
|
uses: Swatinem/rust-cache@v2
|
||||||
- name: Cargo test
|
- name: Cargo test
|
||||||
env:
|
run: cargo test --release --all-features
|
||||||
RUSTC_WRAPPER: sccache
|
|
||||||
SCCACHE_GHA_ENABLED: "true"
|
|
||||||
run: cargo test --release
|
|
||||||
|
|
||||||
build-cli:
|
build-cli:
|
||||||
name: Build objdiff-cli
|
name: Build objdiff-cli
|
||||||
@@ -137,10 +114,10 @@ jobs:
|
|||||||
name: linux-aarch64
|
name: linux-aarch64
|
||||||
build: zigbuild
|
build: zigbuild
|
||||||
features: default
|
features: default
|
||||||
- platform: ubuntu-latest
|
- platform: windows-latest
|
||||||
target: armv7-unknown-linux-musleabi
|
target: i686-pc-windows-msvc
|
||||||
name: linux-armv7l
|
name: windows-x86
|
||||||
build: zigbuild
|
build: build
|
||||||
features: default
|
features: default
|
||||||
- platform: windows-latest
|
- platform: windows-latest
|
||||||
target: x86_64-pc-windows-msvc
|
target: x86_64-pc-windows-msvc
|
||||||
@@ -169,11 +146,19 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Install cargo-zigbuild
|
- name: Install cargo-zigbuild
|
||||||
if: matrix.build == 'zigbuild'
|
if: matrix.build == 'zigbuild'
|
||||||
run: pip install ziglang==0.13.0 cargo-zigbuild==0.19.1
|
run: |
|
||||||
|
python3 -m venv .venv
|
||||||
|
. .venv/bin/activate
|
||||||
|
echo PATH=$PATH >> $GITHUB_ENV
|
||||||
|
pip install ziglang==0.13.0 cargo-zigbuild==0.19.1
|
||||||
- name: Setup Rust toolchain
|
- name: Setup Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
- name: Cache Rust workspace
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
key: ${{ matrix.target }}
|
||||||
- name: Cargo build
|
- name: Cargo build
|
||||||
run: >
|
run: >
|
||||||
cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
|
cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
|
||||||
@@ -225,12 +210,11 @@ jobs:
|
|||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
- name: Setup sccache
|
- name: Cache Rust workspace
|
||||||
uses: mozilla-actions/sccache-action@v0.0.4
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
key: ${{ matrix.target }}
|
||||||
- name: Cargo build
|
- name: Cargo build
|
||||||
env:
|
|
||||||
RUSTC_WRAPPER: sccache
|
|
||||||
SCCACHE_GHA_ENABLED: "true"
|
|
||||||
run: >
|
run: >
|
||||||
cargo build --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
|
cargo build --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
|
||||||
--bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }}
|
--bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }}
|
||||||
@@ -247,10 +231,24 @@ jobs:
|
|||||||
name: Release
|
name: Release
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ check, build-cli, build-gui ]
|
needs: [ build-cli, build-gui ]
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
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
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -278,6 +276,8 @@ jobs:
|
|||||||
done
|
done
|
||||||
ls -R ../out
|
ls -R ../out
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: out/*
|
files: out/*
|
||||||
|
draft: true
|
||||||
|
generate_release_notes: true
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -3,10 +3,6 @@ target/
|
|||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
generated/
|
generated/
|
||||||
|
|
||||||
# cargo-mobile
|
|
||||||
.cargo/
|
|
||||||
/gen
|
|
||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
@@ -22,4 +18,4 @@ android.keystore
|
|||||||
*.frag
|
*.frag
|
||||||
*.vert
|
*.vert
|
||||||
*.metal
|
*.metal
|
||||||
.vscode/launch.json
|
.vscode/
|
||||||
|
|||||||
3214
Cargo.lock
generated
3214
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@@ -8,5 +8,14 @@ resolver = "2"
|
|||||||
|
|
||||||
[profile.release-lto]
|
[profile.release-lto]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
lto = "thin"
|
lto = "fat"
|
||||||
strip = "debuginfo"
|
strip = "debuginfo"
|
||||||
|
codegen-units = 1
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "2.7.0"
|
||||||
|
authors = ["Luke Street <luke@street.dev>"]
|
||||||
|
edition = "2021"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
repository = "https://github.com/encounter/objdiff"
|
||||||
|
rust-version = "1.81"
|
||||||
|
|||||||
36
README.md
36
README.md
@@ -6,6 +6,7 @@
|
|||||||
A local diffing tool for decompilation projects. Inspired by [decomp.me](https://decomp.me) and [asm-differ](https://github.com/simonlindholm/asm-differ).
|
A local diffing tool for decompilation projects. Inspired by [decomp.me](https://decomp.me) and [asm-differ](https://github.com/simonlindholm/asm-differ).
|
||||||
|
|
||||||
Features:
|
Features:
|
||||||
|
|
||||||
- Compare entire object files: functions and data.
|
- Compare entire object files: functions and data.
|
||||||
- Built-in symbol demangling for C++. (CodeWarrior, Itanium & MSVC)
|
- Built-in symbol demangling for C++. (CodeWarrior, Itanium & MSVC)
|
||||||
- Automatic rebuild on source file changes.
|
- Automatic rebuild on source file changes.
|
||||||
@@ -14,13 +15,34 @@ Features:
|
|||||||
- Click to highlight all instances of values and registers.
|
- Click to highlight all instances of values and registers.
|
||||||
|
|
||||||
Supports:
|
Supports:
|
||||||
|
|
||||||
- PowerPC 750CL (GameCube, Wii)
|
- PowerPC 750CL (GameCube, Wii)
|
||||||
- MIPS (N64, PS1, PS2, PSP)
|
- MIPS (N64, PS1, PS2, PSP)
|
||||||
- x86 (COFF only at the moment)
|
- x86 (COFF only at the moment)
|
||||||
- ARM (GBA, DS, 3DS)
|
- ARM (GBA, DS, 3DS)
|
||||||
|
- ARM64 (Switch, experimental)
|
||||||
|
|
||||||
See [Usage](#usage) for more information.
|
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
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
@@ -141,16 +163,22 @@ Install Rust via [rustup](https://rustup.rs).
|
|||||||
$ git clone https://github.com/encounter/objdiff.git
|
$ git clone https://github.com/encounter/objdiff.git
|
||||||
$ cd objdiff
|
$ cd objdiff
|
||||||
$ cargo run --release
|
$ cargo run --release
|
||||||
# or, for wgpu backend (recommended on macOS)
|
|
||||||
$ cargo run --release --features wgpu
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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
|
## License
|
||||||
|
|
||||||
Licensed under either of
|
Licensed under either of
|
||||||
|
|
||||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
|
- 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)
|
- MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
|
||||||
|
|
||||||
at your option.
|
at your option.
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,13 @@
|
|||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"ref": "#/$defs/metadata"
|
"ref": "#/$defs/metadata"
|
||||||
|
},
|
||||||
|
"symbol_mappings": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Manual symbol mappings from target to base.",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
172
deny.toml
172
deny.toml
@@ -9,6 +9,11 @@
|
|||||||
# The values provided in this template are the default values that will be used
|
# 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
|
# 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,
|
# If 1 or more target triples (and optionally, target_features) are specified,
|
||||||
# only the specified targets will be checked when running `cargo deny check`.
|
# 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
|
# This means, if a particular package is only ever used as a target specific
|
||||||
@@ -20,51 +25,66 @@
|
|||||||
targets = [
|
targets = [
|
||||||
# The triple can be any string, but only the target triples built in to
|
# 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
|
# 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
|
# You can also specify which target_features you promise are enabled for a
|
||||||
# particular target. target_features are currently not validated against
|
# particular target. target_features are currently not validated against
|
||||||
# the actual valid features supported by the target architecture.
|
# the actual valid features supported by the target architecture.
|
||||||
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
|
#{ 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`
|
# This section is considered when running `cargo deny check advisories`
|
||||||
# More documentation for the advisories section can be found here:
|
# More documentation for the advisories section can be found here:
|
||||||
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
|
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
|
||||||
[advisories]
|
[advisories]
|
||||||
# The path where the advisory database is cloned/fetched into
|
# The path where the advisory databases are cloned/fetched into
|
||||||
db-path = "~/.cargo/advisory-db"
|
#db-path = "$CARGO_HOME/advisory-dbs"
|
||||||
# The url(s) of the advisory databases to use
|
# The url(s) of the advisory databases to use
|
||||||
db-urls = ["https://github.com/rustsec/advisory-db"]
|
#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"
|
|
||||||
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||||
# output a note when they are encountered.
|
# output a note when they are encountered.
|
||||||
ignore = []
|
ignore = [
|
||||||
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
|
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
|
||||||
# lower than the range specified will be ignored. Note that ignored advisories
|
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
|
||||||
# will still output a note when they are encountered.
|
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
|
||||||
# * None - CVSS Score 0.0
|
]
|
||||||
# * Low - CVSS Score 0.1 - 3.9
|
# If this is true, then cargo deny will use the git executable to fetch advisory database.
|
||||||
# * Medium - CVSS Score 4.0 - 6.9
|
# If this is false, then it uses a built-in git library.
|
||||||
# * High - CVSS Score 7.0 - 8.9
|
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
|
||||||
# * Critical - CVSS Score 9.0 - 10.0
|
# See Git Authentication for more information about setting up git authentication.
|
||||||
#severity-threshold =
|
#git-fetch-with-cli = true
|
||||||
|
|
||||||
# This section is considered when running `cargo deny check licenses`
|
# This section is considered when running `cargo deny check licenses`
|
||||||
# More documentation for the licenses section can be found here:
|
# More documentation for the licenses section can be found here:
|
||||||
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
|
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
|
||||||
[licenses]
|
[licenses]
|
||||||
# The lint level for crates which do not have a detectable license
|
# List of explicitly allowed licenses
|
||||||
unlicensed = "deny"
|
|
||||||
# List of explictly allowed licenses
|
|
||||||
# See https://spdx.org/licenses/ for list of possible licenses
|
# See https://spdx.org/licenses/ for list of possible licenses
|
||||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||||
allow = [
|
allow = [
|
||||||
@@ -77,34 +97,13 @@ allow = [
|
|||||||
"BSL-1.0",
|
"BSL-1.0",
|
||||||
"CC0-1.0",
|
"CC0-1.0",
|
||||||
"MPL-2.0",
|
"MPL-2.0",
|
||||||
"Unicode-DFS-2016",
|
"Unicode-3.0",
|
||||||
"Zlib",
|
"Zlib",
|
||||||
"0BSD",
|
"0BSD",
|
||||||
"OFL-1.1",
|
"OFL-1.1",
|
||||||
"LicenseRef-UFL-1.0",
|
"LicenseRef-UFL-1.0",
|
||||||
"OpenSSL",
|
"OpenSSL",
|
||||||
"GPL-3.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 confidence threshold for detecting a license from license text.
|
||||||
# The higher the value, the more closely the license text must be to the
|
# The higher the value, the more closely the license text must be to the
|
||||||
# canonical license text of a valid SPDX license file.
|
# canonical license text of a valid SPDX license file.
|
||||||
@@ -115,17 +114,15 @@ confidence-threshold = 0.8
|
|||||||
exceptions = [
|
exceptions = [
|
||||||
# Each entry is the crate and version constraint, and its specific allow
|
# Each entry is the crate and version constraint, and its specific allow
|
||||||
# list
|
# list
|
||||||
#{ allow = ["Zlib"], name = "adler32", version = "*" },
|
#{ allow = ["Zlib"], crate = "adler32" },
|
||||||
]
|
]
|
||||||
|
|
||||||
# Some crates don't have (easily) machine readable licensing information,
|
# Some crates don't have (easily) machine readable licensing information,
|
||||||
# adding a clarification entry for it allows you to manually specify the
|
# adding a clarification entry for it allows you to manually specify the
|
||||||
# licensing information
|
# licensing information
|
||||||
[[licenses.clarify]]
|
[[licenses.clarify]]
|
||||||
# The name of the crate the clarification applies to
|
# The package spec the clarification applies to
|
||||||
name = "ring"
|
crate = "ring"
|
||||||
# The optional version constraint for the crate
|
|
||||||
version = "*"
|
|
||||||
# The SPDX expression for the license requirements of the crate
|
# The SPDX expression for the license requirements of the crate
|
||||||
expression = "MIT AND ISC AND OpenSSL"
|
expression = "MIT AND ISC AND OpenSSL"
|
||||||
# One or more files in the crate's source used as the "source of truth" for
|
# One or more files in the crate's source used as the "source of truth" for
|
||||||
@@ -140,7 +137,9 @@ license-files = [
|
|||||||
|
|
||||||
[licenses.private]
|
[licenses.private]
|
||||||
# If true, ignores workspace crates that aren't published, or are only
|
# If true, ignores workspace crates that aren't published, or are only
|
||||||
# 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
|
ignore = false
|
||||||
# One or more private registries that you might publish crates to, if a crate
|
# 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
|
# is only published to private registries, and ignore is true, the crate will
|
||||||
@@ -163,30 +162,63 @@ wildcards = "allow"
|
|||||||
# * simplest-path - The path to the version with the fewest edges is highlighted
|
# * simplest-path - The path to the version with the fewest edges is highlighted
|
||||||
# * all - Both lowest-version and simplest-path are used
|
# * all - Both lowest-version and simplest-path are used
|
||||||
highlight = "all"
|
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!
|
# List of crates that are allowed. Use with care!
|
||||||
allow = [
|
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
|
# List of crates to deny
|
||||||
deny = [
|
deny = [
|
||||||
# Each entry the name of a crate and a version range. If version is
|
#"ansi_term@0.11.0",
|
||||||
# not specified, all versions will be matched.
|
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
|
||||||
#{ name = "ansi_term", version = "=0.11.0" },
|
|
||||||
#
|
|
||||||
# Wrapper crates can optionally be specified to allow the crate when it
|
# Wrapper crates can optionally be specified to allow the crate when it
|
||||||
# is a direct dependency of the otherwise banned crate
|
# 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.
|
# Certain crates/versions that will be skipped when doing duplicate detection.
|
||||||
skip = [
|
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
|
# Similarly to `skip` allows you to skip certain crates during duplicate
|
||||||
# detection. Unlike skip, it also includes the entire tree of transitive
|
# detection. Unlike skip, it also includes the entire tree of transitive
|
||||||
# dependencies starting at the specified crate, up to a certain depth, which is
|
# dependencies starting at the specified crate, up to a certain depth, which is
|
||||||
# by default infinite
|
# by default infinite.
|
||||||
skip-tree = [
|
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`.
|
# This section is considered when running `cargo deny check sources`.
|
||||||
@@ -206,9 +238,9 @@ allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
|||||||
allow-git = []
|
allow-git = []
|
||||||
|
|
||||||
[sources.allow-org]
|
[sources.allow-org]
|
||||||
# 1 or more github.com organizations to allow git sources for
|
# github.com organizations to allow git sources for
|
||||||
github = ["encounter"]
|
github = []
|
||||||
# 1 or more gitlab.com organizations to allow git sources for
|
# gitlab.com organizations to allow git sources for
|
||||||
#gitlab = [""]
|
gitlab = []
|
||||||
# 1 or more bitbucket.org organizations to allow git sources for
|
# bitbucket.org organizations to allow git sources for
|
||||||
#bitbucket = [""]
|
bitbucket = []
|
||||||
|
|||||||
@@ -1,31 +1,33 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "objdiff-cli"
|
name = "objdiff-cli"
|
||||||
version = "2.0.0-beta.6"
|
version.workspace = true
|
||||||
edition = "2021"
|
edition.workspace = true
|
||||||
rust-version = "1.70"
|
rust-version.workspace = true
|
||||||
authors = ["Luke Street <luke@street.dev>"]
|
authors.workspace = true
|
||||||
license = "MIT OR Apache-2.0"
|
license.workspace = true
|
||||||
repository = "https://github.com/encounter/objdiff"
|
repository.workspace = true
|
||||||
readme = "../README.md"
|
readme = "../README.md"
|
||||||
description = """
|
description = """
|
||||||
A local diffing tool for decompilation projects.
|
A local diffing tool for decompilation projects.
|
||||||
"""
|
"""
|
||||||
publish = false
|
publish = false
|
||||||
build = "build.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.82"
|
anyhow = "1.0"
|
||||||
argp = "0.3.0"
|
argp = "0.4"
|
||||||
crossterm = "0.27.0"
|
crossterm = "0.28"
|
||||||
enable-ansi-support = "0.2.1"
|
enable-ansi-support = "0.2"
|
||||||
memmap2 = "0.9.4"
|
memmap2 = "0.9"
|
||||||
objdiff-core = { path = "../objdiff-core", features = ["all"] }
|
objdiff-core = { path = "../objdiff-core", features = ["all"] }
|
||||||
prost = "0.13.1"
|
prost = "0.13"
|
||||||
ratatui = "0.26.2"
|
ratatui = "0.29"
|
||||||
rayon = "1.10.0"
|
rayon = "1.10"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0.116"
|
serde_json = "1.0"
|
||||||
supports-color = "3.0.0"
|
supports-color = "3.0"
|
||||||
time = { version = "0.3.36", features = ["formatting", "local-offset"] }
|
time = { version = "0.3", features = ["formatting", "local-offset"] }
|
||||||
tracing = "0.1.40"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_env = "musl")'.dependencies]
|
||||||
|
mimalloc = "0.1"
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
fn main() {
|
|
||||||
let output = std::process::Command::new("git")
|
|
||||||
.args(["rev-parse", "HEAD"])
|
|
||||||
.output()
|
|
||||||
.expect("Failed to execute git");
|
|
||||||
let rev = String::from_utf8(output.stdout).expect("Failed to parse git output");
|
|
||||||
println!("cargo:rustc-env=GIT_COMMIT_SHA={rev}");
|
|
||||||
println!("cargo:rustc-rerun-if-changed=.git/HEAD");
|
|
||||||
}
|
|
||||||
@@ -31,10 +31,9 @@ where T: FromArgs
|
|||||||
Ok(v) => {
|
Ok(v) => {
|
||||||
if v.version {
|
if v.version {
|
||||||
println!(
|
println!(
|
||||||
"{} {} {}",
|
"{} {}",
|
||||||
command_name.first().unwrap_or(&""),
|
command_name.first().unwrap_or(&""),
|
||||||
env!("CARGO_PKG_VERSION"),
|
env!("CARGO_PKG_VERSION"),
|
||||||
env!("GIT_COMMIT_SHA"),
|
|
||||||
);
|
);
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -94,7 +94,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
|||||||
};
|
};
|
||||||
info!(
|
info!(
|
||||||
"Generating report for {} units (using {} threads)",
|
"Generating report for {} units (using {} threads)",
|
||||||
project.objects.len(),
|
project.units().len(),
|
||||||
if args.deduplicate { 1 } else { rayon::current_num_threads() }
|
if args.deduplicate { 1 } else { rayon::current_num_threads() }
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
|||||||
let mut existing_functions: HashSet<String> = HashSet::new();
|
let mut existing_functions: HashSet<String> = HashSet::new();
|
||||||
if args.deduplicate {
|
if args.deduplicate {
|
||||||
// If deduplicating, we need to run single-threaded
|
// If deduplicating, we need to run single-threaded
|
||||||
for object in &mut project.objects {
|
for object in project.units.as_deref_mut().unwrap_or_default() {
|
||||||
if let Some(unit) = report_object(
|
if let Some(unit) = report_object(
|
||||||
object,
|
object,
|
||||||
project_dir,
|
project_dir,
|
||||||
@@ -116,7 +116,9 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let vec = project
|
let vec = project
|
||||||
.objects
|
.units
|
||||||
|
.as_deref_mut()
|
||||||
|
.unwrap_or_default()
|
||||||
.par_iter_mut()
|
.par_iter_mut()
|
||||||
.map(|object| {
|
.map(|object| {
|
||||||
report_object(
|
report_object(
|
||||||
@@ -132,7 +134,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
|||||||
}
|
}
|
||||||
let measures = units.iter().flat_map(|u| u.measures.into_iter()).collect();
|
let measures = units.iter().flat_map(|u| u.measures.into_iter()).collect();
|
||||||
let mut categories = Vec::new();
|
let mut categories = Vec::new();
|
||||||
for category in &project.progress_categories {
|
for category in project.progress_categories() {
|
||||||
categories.push(ReportCategory {
|
categories.push(ReportCategory {
|
||||||
id: category.id.clone(),
|
id: category.id.clone(),
|
||||||
name: category.name.clone(),
|
name: category.name.clone(),
|
||||||
@@ -167,22 +169,29 @@ fn report_object(
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
let config = diff::DiffObjConfig { relax_reloc_diffs: true, ..Default::default() };
|
let diff_config = diff::DiffObjConfig {
|
||||||
|
function_reloc_diffs: diff::FunctionRelocDiffs::None,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mapping_config = diff::MappingConfig::default();
|
||||||
let target = object
|
let target = object
|
||||||
.target_path
|
.target_path
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|p| {
|
.map(|p| {
|
||||||
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
|
obj::read::read(p, &diff_config)
|
||||||
|
.with_context(|| format!("Failed to open {}", p.display()))
|
||||||
})
|
})
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
let base = object
|
let base = object
|
||||||
.base_path
|
.base_path
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|p| {
|
.map(|p| {
|
||||||
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
|
obj::read::read(p, &diff_config)
|
||||||
|
.with_context(|| format!("Failed to open {}", p.display()))
|
||||||
})
|
})
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?;
|
let result =
|
||||||
|
diff::diff_objs(&diff_config, &mapping_config, target.as_ref(), base.as_ref(), None)?;
|
||||||
|
|
||||||
let metadata = ReportUnitMetadata {
|
let metadata = ReportUnitMetadata {
|
||||||
complete: object.complete(),
|
complete: object.complete(),
|
||||||
@@ -199,7 +208,7 @@ fn report_object(
|
|||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
auto_generated: object.metadata.as_ref().and_then(|m| m.auto_generated),
|
auto_generated: object.metadata.as_ref().and_then(|m| m.auto_generated),
|
||||||
};
|
};
|
||||||
let mut measures = Measures::default();
|
let mut measures = Measures { total_units: 1, ..Default::default() };
|
||||||
let mut sections = vec![];
|
let mut sections = vec![];
|
||||||
let mut functions = vec![];
|
let mut functions = vec![];
|
||||||
|
|
||||||
@@ -237,7 +246,7 @@ fn report_object(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (symbol, symbol_diff) in section.symbols.iter().zip(§ion_diff.symbols) {
|
for (symbol, symbol_diff) in section.symbols.iter().zip(§ion_diff.symbols) {
|
||||||
if symbol.size == 0 {
|
if symbol.size == 0 || symbol.flags.0.contains(ObjSymbolFlags::Hidden) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(existing_functions) = &mut existing_functions {
|
if let Some(existing_functions) = &mut existing_functions {
|
||||||
@@ -280,6 +289,7 @@ fn report_object(
|
|||||||
if metadata.complete.unwrap_or(false) {
|
if metadata.complete.unwrap_or(false) {
|
||||||
measures.complete_code = measures.total_code;
|
measures.complete_code = measures.total_code;
|
||||||
measures.complete_data = measures.total_data;
|
measures.complete_data = measures.total_data;
|
||||||
|
measures.complete_units = 1;
|
||||||
}
|
}
|
||||||
measures.calc_fuzzy_match_percent();
|
measures.calc_fuzzy_match_percent();
|
||||||
measures.calc_matched_percent();
|
measures.calc_matched_percent();
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
mod argp_version;
|
mod argp_version;
|
||||||
mod cmd;
|
mod cmd;
|
||||||
mod util;
|
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 std::{env, ffi::OsStr, fmt::Display, path::PathBuf, str::FromStr};
|
||||||
|
|
||||||
|
|||||||
658
objdiff-cli/src/views/function_diff.rs
Normal file
658
objdiff-cli/src/views/function_diff.rs
Normal file
@@ -0,0 +1,658 @@
|
|||||||
|
use anyhow::{bail, Result};
|
||||||
|
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind};
|
||||||
|
use objdiff_core::{
|
||||||
|
diff::{
|
||||||
|
display::{display_diff, DiffText, HighlightKind},
|
||||||
|
FunctionRelocDiffs, ObjDiff, ObjInsDiffKind, ObjSymbolDiff,
|
||||||
|
},
|
||||||
|
obj::{ObjInfo, ObjSectionKind, ObjSymbol, SymbolRef},
|
||||||
|
};
|
||||||
|
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<SymbolRef>,
|
||||||
|
pub right_sym: Option<SymbolRef>,
|
||||||
|
pub prev_sym: Option<SymbolRef>,
|
||||||
|
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((symbol, 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,
|
||||||
|
symbol,
|
||||||
|
symbol_diff,
|
||||||
|
rect,
|
||||||
|
&self.left_highlight,
|
||||||
|
result,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
max_width = max_width.max(text.width());
|
||||||
|
left_text = Some(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut right_text = None;
|
||||||
|
let mut right_highlight = None;
|
||||||
|
let mut margin_text = None;
|
||||||
|
if let Some((symbol, 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,
|
||||||
|
symbol,
|
||||||
|
symbol_diff,
|
||||||
|
rect,
|
||||||
|
&self.right_highlight,
|
||||||
|
result,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
max_width = max_width.max(text.width());
|
||||||
|
right_text = Some(text);
|
||||||
|
|
||||||
|
// Render margin
|
||||||
|
let mut text = Text::default();
|
||||||
|
let rect = content_chunks[1].inner(Margin::new(1, 1));
|
||||||
|
self.print_margin(&mut text, symbol_diff, rect);
|
||||||
|
margin_text = Some(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut prev_text = None;
|
||||||
|
let mut prev_margin_text = None;
|
||||||
|
if self.three_way {
|
||||||
|
if let Some((symbol, 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,
|
||||||
|
symbol,
|
||||||
|
symbol_diff,
|
||||||
|
rect,
|
||||||
|
&self.right_highlight,
|
||||||
|
result,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
max_width = max_width.max(text.width());
|
||||||
|
prev_text = Some(text);
|
||||||
|
|
||||||
|
// Render margin
|
||||||
|
let mut text = Text::default();
|
||||||
|
let rect = content_chunks[3].inner(Margin::new(1, 1));
|
||||||
|
self.print_margin(&mut text, symbol_diff, rect);
|
||||||
|
prev_margin_text = Some(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_scroll_x =
|
||||||
|
max_width.saturating_sub(content_chunks[0].width.min(content_chunks[2].width) as usize);
|
||||||
|
if self.scroll_x > max_scroll_x {
|
||||||
|
self.scroll_x = max_scroll_x;
|
||||||
|
}
|
||||||
|
self.scroll_state_x =
|
||||||
|
self.scroll_state_x.content_length(max_scroll_x).position(self.scroll_x);
|
||||||
|
|
||||||
|
if let Some(text) = left_text {
|
||||||
|
// Render left column
|
||||||
|
f.render_widget(
|
||||||
|
Paragraph::new(text)
|
||||||
|
.block(
|
||||||
|
Block::new()
|
||||||
|
.borders(Borders::TOP)
|
||||||
|
.border_style(Style::new().fg(Color::Gray))
|
||||||
|
.title_style(Style::new().bold())
|
||||||
|
.title("TARGET"),
|
||||||
|
)
|
||||||
|
.scroll((0, self.scroll_x as u16)),
|
||||||
|
content_chunks[0],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(text) = margin_text {
|
||||||
|
f.render_widget(text, content_chunks[1].inner(Margin::new(1, 1)));
|
||||||
|
}
|
||||||
|
if let Some(text) = right_text {
|
||||||
|
f.render_widget(
|
||||||
|
Paragraph::new(text)
|
||||||
|
.block(
|
||||||
|
Block::new()
|
||||||
|
.borders(Borders::TOP)
|
||||||
|
.border_style(Style::new().fg(Color::Gray))
|
||||||
|
.title_style(Style::new().bold())
|
||||||
|
.title("CURRENT"),
|
||||||
|
)
|
||||||
|
.scroll((0, self.scroll_x as u16)),
|
||||||
|
content_chunks[2],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.three_way {
|
||||||
|
if let Some(text) = prev_margin_text {
|
||||||
|
f.render_widget(text, content_chunks[3].inner(Margin::new(1, 1)));
|
||||||
|
}
|
||||||
|
let block = Block::new()
|
||||||
|
.borders(Borders::TOP)
|
||||||
|
.border_style(Style::new().fg(Color::Gray))
|
||||||
|
.title_style(Style::new().bold())
|
||||||
|
.title("SAVED");
|
||||||
|
if let Some(text) = prev_text {
|
||||||
|
f.render_widget(
|
||||||
|
Paragraph::new(text).block(block.clone()).scroll((0, self.scroll_x as u16)),
|
||||||
|
content_chunks[4],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
f.render_widget(block, content_chunks[4]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render scrollbars
|
||||||
|
f.render_stateful_widget(
|
||||||
|
Scrollbar::new(ScrollbarOrientation::VerticalRight).begin_symbol(None).end_symbol(None),
|
||||||
|
chunks[1].inner(Margin::new(0, 1)),
|
||||||
|
&mut self.scroll_state_y,
|
||||||
|
);
|
||||||
|
f.render_stateful_widget(
|
||||||
|
Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol("■"),
|
||||||
|
content_chunks[0],
|
||||||
|
&mut self.scroll_state_x,
|
||||||
|
);
|
||||||
|
f.render_stateful_widget(
|
||||||
|
Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol("■"),
|
||||||
|
content_chunks[2],
|
||||||
|
&mut self.scroll_state_x,
|
||||||
|
);
|
||||||
|
if self.three_way {
|
||||||
|
f.render_stateful_widget(
|
||||||
|
Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol("■"),
|
||||||
|
content_chunks[4],
|
||||||
|
&mut self.scroll_state_x,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(new_highlight) = left_highlight {
|
||||||
|
if new_highlight == self.left_highlight {
|
||||||
|
if self.left_highlight != self.right_highlight {
|
||||||
|
self.right_highlight = self.left_highlight.clone();
|
||||||
|
} else {
|
||||||
|
self.left_highlight = HighlightKind::None;
|
||||||
|
self.right_highlight = HighlightKind::None;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.left_highlight = new_highlight;
|
||||||
|
}
|
||||||
|
result.redraw = true;
|
||||||
|
} else if let Some(new_highlight) = right_highlight {
|
||||||
|
if new_highlight == self.right_highlight {
|
||||||
|
if self.left_highlight != self.right_highlight {
|
||||||
|
self.left_highlight = self.right_highlight.clone();
|
||||||
|
} else {
|
||||||
|
self.left_highlight = HighlightKind::None;
|
||||||
|
self.right_highlight = HighlightKind::None;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.right_highlight = new_highlight;
|
||||||
|
}
|
||||||
|
result.redraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, ld)), Some((_r, rd))) => ld.instructions.len().max(rd.instructions.len()),
|
||||||
|
(Some((_l, ld)), None) => ld.instructions.len(),
|
||||||
|
(None, Some((_r, rd))) => rd.instructions.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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn print_sym(
|
||||||
|
&self,
|
||||||
|
out: &mut Text<'static>,
|
||||||
|
symbol: &ObjSymbol,
|
||||||
|
symbol_diff: &ObjSymbolDiff,
|
||||||
|
rect: Rect,
|
||||||
|
highlight: &HighlightKind,
|
||||||
|
result: &EventResult,
|
||||||
|
only_changed: bool,
|
||||||
|
) -> Option<HighlightKind> {
|
||||||
|
let base_addr = symbol.address;
|
||||||
|
let mut new_highlight = None;
|
||||||
|
for (y, ins_diff) in symbol_diff
|
||||||
|
.instructions
|
||||||
|
.iter()
|
||||||
|
.skip(self.scroll_y)
|
||||||
|
.take(rect.height as usize)
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
if only_changed && ins_diff.kind == ObjInsDiffKind::None {
|
||||||
|
out.lines.push(Line::default());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut sx = rect.x;
|
||||||
|
let sy = rect.y + y as u16;
|
||||||
|
let mut line = Line::default();
|
||||||
|
display_diff(ins_diff, base_addr, |text| -> Result<()> {
|
||||||
|
let label_text;
|
||||||
|
let mut base_color = match ins_diff.kind {
|
||||||
|
ObjInsDiffKind::None
|
||||||
|
| ObjInsDiffKind::OpMismatch
|
||||||
|
| ObjInsDiffKind::ArgMismatch => Color::Gray,
|
||||||
|
ObjInsDiffKind::Replace => Color::Cyan,
|
||||||
|
ObjInsDiffKind::Delete => Color::Red,
|
||||||
|
ObjInsDiffKind::Insert => Color::Green,
|
||||||
|
};
|
||||||
|
let mut pad_to = 0;
|
||||||
|
match text {
|
||||||
|
DiffText::Basic(text) => {
|
||||||
|
label_text = text.to_string();
|
||||||
|
}
|
||||||
|
DiffText::BasicColor(s, idx) => {
|
||||||
|
label_text = s.to_string();
|
||||||
|
base_color = COLOR_ROTATION[idx % COLOR_ROTATION.len()];
|
||||||
|
}
|
||||||
|
DiffText::Line(num) => {
|
||||||
|
label_text = format!("{num} ");
|
||||||
|
base_color = Color::DarkGray;
|
||||||
|
pad_to = 5;
|
||||||
|
}
|
||||||
|
DiffText::Address(addr) => {
|
||||||
|
label_text = format!("{:x}:", addr);
|
||||||
|
pad_to = 5;
|
||||||
|
}
|
||||||
|
DiffText::Opcode(mnemonic, _op) => {
|
||||||
|
label_text = mnemonic.to_string();
|
||||||
|
if ins_diff.kind == ObjInsDiffKind::OpMismatch {
|
||||||
|
base_color = Color::Blue;
|
||||||
|
}
|
||||||
|
pad_to = 8;
|
||||||
|
}
|
||||||
|
DiffText::Argument(arg, diff) => {
|
||||||
|
label_text = arg.to_string();
|
||||||
|
if let Some(diff) = diff {
|
||||||
|
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DiffText::BranchDest(addr, diff) => {
|
||||||
|
label_text = format!("{addr:x}");
|
||||||
|
if let Some(diff) = diff {
|
||||||
|
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DiffText::Symbol(sym, diff) => {
|
||||||
|
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
|
||||||
|
label_text = name.clone();
|
||||||
|
if let Some(diff) = diff {
|
||||||
|
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
|
||||||
|
} else {
|
||||||
|
base_color = Color::White;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DiffText::Spacing(n) => {
|
||||||
|
line.spans.push(Span::raw(" ".repeat(n)));
|
||||||
|
sx += n as u16;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
DiffText::Eol => {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let len = label_text.len();
|
||||||
|
let highlighted = *highlight == text;
|
||||||
|
if let Some((cx, cy)) = result.click_xy {
|
||||||
|
if cx >= sx && cx < sx + len as u16 && cy == sy {
|
||||||
|
new_highlight = Some(text.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut style = Style::new().fg(base_color);
|
||||||
|
if highlighted {
|
||||||
|
style = style.bg(Color::DarkGray);
|
||||||
|
}
|
||||||
|
line.spans.push(Span::styled(label_text, style));
|
||||||
|
sx += len as u16;
|
||||||
|
if pad_to > len {
|
||||||
|
let pad = (pad_to - len) as u16;
|
||||||
|
line.spans.push(Span::raw(" ".repeat(pad as usize)));
|
||||||
|
sx += pad;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
out.lines.push(line);
|
||||||
|
}
|
||||||
|
new_highlight
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_margin(&self, out: &mut Text, symbol: &ObjSymbolDiff, rect: Rect) {
|
||||||
|
for ins_diff in symbol.instructions.iter().skip(self.scroll_y).take(rect.height as usize) {
|
||||||
|
if ins_diff.kind != ObjInsDiffKind::None {
|
||||||
|
out.lines.push(Line::raw(match ins_diff.kind {
|
||||||
|
ObjInsDiffKind::Delete => "<",
|
||||||
|
ObjInsDiffKind::Insert => ">",
|
||||||
|
_ => "|",
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
out.lines.push(Line::raw(" "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<&(ObjInfo, ObjDiff)>,
|
||||||
|
sym: Option<SymbolRef>,
|
||||||
|
) -> Option<(&ObjSymbol, &ObjSymbolDiff)> {
|
||||||
|
let (obj, diff) = obj?;
|
||||||
|
let sym = sym?;
|
||||||
|
Some((obj.section_symbol(sym).1, diff.symbol_diff(sym)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_function(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
|
||||||
|
for (section_idx, section) in obj.sections.iter().enumerate() {
|
||||||
|
if section.kind != ObjSectionKind::Code {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
|
||||||
|
if symbol.name == name {
|
||||||
|
return Some(SymbolRef { section_idx, symbol_idx });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
25
objdiff-cli/src/views/mod.rs
Normal file
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<()>;
|
||||||
|
}
|
||||||
@@ -1,76 +1,201 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "objdiff-core"
|
name = "objdiff-core"
|
||||||
version = "2.0.0-beta.6"
|
version.workspace = true
|
||||||
edition = "2021"
|
edition.workspace = true
|
||||||
rust-version = "1.70"
|
rust-version.workspace = true
|
||||||
authors = ["Luke Street <luke@street.dev>"]
|
authors.workspace = true
|
||||||
license = "MIT OR Apache-2.0"
|
license.workspace = true
|
||||||
repository = "https://github.com/encounter/objdiff"
|
repository.workspace = true
|
||||||
readme = "../README.md"
|
readme = "README.md"
|
||||||
description = """
|
description = """
|
||||||
A local diffing tool for decompilation projects.
|
A local diffing tool for decompilation projects.
|
||||||
"""
|
"""
|
||||||
|
documentation = "https://docs.rs/objdiff-core"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "bindings"]
|
all = [
|
||||||
any-arch = [] # Implicit, used to check if any arch is enabled
|
# Features
|
||||||
config = ["globset", "semver", "serde_json", "serde_yaml"]
|
"bindings",
|
||||||
dwarf = ["gimli"]
|
"build",
|
||||||
mips = ["any-arch", "rabbitizer"]
|
"config",
|
||||||
ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"]
|
"dwarf",
|
||||||
x86 = ["any-arch", "cpp_demangle", "iced-x86", "msvc-demangler"]
|
# Architectures
|
||||||
arm = ["any-arch", "cpp_demangle", "unarm", "arm-attr"]
|
"mips",
|
||||||
bindings = ["serde_json", "prost", "pbjson"]
|
"ppc",
|
||||||
wasm = ["bindings", "console_error_panic_hook", "console_log"]
|
"x86",
|
||||||
|
"arm",
|
||||||
|
"arm64",
|
||||||
|
]
|
||||||
|
# Implicit, used to check if any arch is enabled
|
||||||
|
any-arch = [
|
||||||
|
"config",
|
||||||
|
"dep:bimap",
|
||||||
|
"dep:byteorder",
|
||||||
|
"dep:flagset",
|
||||||
|
"dep:heck",
|
||||||
|
"dep:log",
|
||||||
|
"dep:memmap2",
|
||||||
|
"dep:num-traits",
|
||||||
|
"dep:prettyplease",
|
||||||
|
"dep:proc-macro2",
|
||||||
|
"dep:quote",
|
||||||
|
"dep:serde",
|
||||||
|
"dep:serde_json",
|
||||||
|
"dep:similar",
|
||||||
|
"dep:strum",
|
||||||
|
"dep:syn",
|
||||||
|
]
|
||||||
|
bindings = [
|
||||||
|
"dep:pbjson",
|
||||||
|
"dep:pbjson-build",
|
||||||
|
"dep:prost",
|
||||||
|
"dep:prost-build",
|
||||||
|
"dep:serde",
|
||||||
|
"dep:serde_json",
|
||||||
|
]
|
||||||
|
build = [
|
||||||
|
"dep:notify",
|
||||||
|
"dep:notify-debouncer-full",
|
||||||
|
"dep:path-slash",
|
||||||
|
"dep:reqwest",
|
||||||
|
"dep:self_update",
|
||||||
|
"dep:shell-escape",
|
||||||
|
"dep:tempfile",
|
||||||
|
"dep:time",
|
||||||
|
"dep:winapi",
|
||||||
|
]
|
||||||
|
config = [
|
||||||
|
"dep:bimap",
|
||||||
|
"dep:filetime",
|
||||||
|
"dep:globset",
|
||||||
|
"dep:semver",
|
||||||
|
"dep:serde",
|
||||||
|
"dep:serde_json",
|
||||||
|
"dep:serde_yaml",
|
||||||
|
]
|
||||||
|
dwarf = ["dep:gimli"]
|
||||||
|
mips = [
|
||||||
|
"any-arch",
|
||||||
|
"dep:rabbitizer",
|
||||||
|
]
|
||||||
|
ppc = [
|
||||||
|
"any-arch",
|
||||||
|
"dep:cwdemangle",
|
||||||
|
"dep:cwextab",
|
||||||
|
"dep:ppc750cl",
|
||||||
|
]
|
||||||
|
x86 = [
|
||||||
|
"any-arch",
|
||||||
|
"dep:cpp_demangle",
|
||||||
|
"dep:iced-x86",
|
||||||
|
"dep:msvc-demangler",
|
||||||
|
]
|
||||||
|
arm = [
|
||||||
|
"any-arch",
|
||||||
|
"dep:arm-attr",
|
||||||
|
"dep:cpp_demangle",
|
||||||
|
"dep:unarm",
|
||||||
|
]
|
||||||
|
arm64 = [
|
||||||
|
"any-arch",
|
||||||
|
"dep:cpp_demangle",
|
||||||
|
"dep:yaxpeax-arch",
|
||||||
|
"dep:yaxpeax-arm",
|
||||||
|
]
|
||||||
|
wasm = [
|
||||||
|
"any-arch",
|
||||||
|
"bindings",
|
||||||
|
"dep:console_error_panic_hook",
|
||||||
|
"dep:console_log",
|
||||||
|
"dep:log",
|
||||||
|
"dep:tsify-next",
|
||||||
|
"dep:wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
features = ["all"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.82"
|
anyhow = "1.0"
|
||||||
byteorder = "1.5.0"
|
bimap = { version = "0.6", features = ["serde"], optional = true }
|
||||||
filetime = "0.2.23"
|
byteorder = { version = "1.5", optional = true }
|
||||||
flagset = "0.4.5"
|
filetime = { version = "0.2", optional = true }
|
||||||
log = "0.4.21"
|
flagset = { version = "0.4", optional = true }
|
||||||
memmap2 = "0.9.4"
|
log = { version = "0.4", optional = true }
|
||||||
num-traits = "0.2.18"
|
memmap2 = { version = "0.9", optional = true }
|
||||||
object = { version = "0.36.0", features = ["read_core", "std", "elf", "pe"], default-features = false }
|
num-traits = { version = "0.2", optional = true }
|
||||||
pbjson = { version = "0.7.0", optional = true }
|
object = { version = "0.36", features = ["read_core", "std", "elf", "pe"], default-features = false }
|
||||||
prost = { version = "0.13.1", optional = true }
|
pbjson = { version = "0.7", optional = true }
|
||||||
serde = { version = "1", features = ["derive"] }
|
prost = { version = "0.13", optional = true }
|
||||||
similar = { version = "2.5.0", default-features = false }
|
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||||
strum = { version = "0.26.2", features = ["derive"] }
|
similar = { version = "2.6", default-features = false, optional = true }
|
||||||
wasm-bindgen = "0.2.93"
|
strum = { version = "0.26", features = ["derive"], optional = true }
|
||||||
tsify-next = { version = "0.5.4", default-features = false, features = ["js"] }
|
wasm-bindgen = { version = "0.2", optional = true }
|
||||||
console_log = { version = "1.0.0", optional = true }
|
tsify-next = { version = "0.5", default-features = false, features = ["js"], optional = true }
|
||||||
console_error_panic_hook = { version = "0.1.7", optional = true }
|
console_log = { version = "1.0", optional = true }
|
||||||
|
console_error_panic_hook = { version = "0.1", optional = true }
|
||||||
|
|
||||||
# config
|
# config
|
||||||
globset = { version = "0.4.14", features = ["serde1"], optional = true }
|
globset = { version = "0.4", features = ["serde1"], optional = true }
|
||||||
semver = { version = "1.0.22", optional = true }
|
semver = { version = "1.0", optional = true }
|
||||||
serde_json = { version = "1.0.116", optional = true }
|
serde_json = { version = "1.0", optional = true }
|
||||||
serde_yaml = { version = "0.9.34", optional = true }
|
serde_yaml = { version = "0.9", optional = true }
|
||||||
|
|
||||||
# dwarf
|
# dwarf
|
||||||
gimli = { version = "0.29.0", default-features = false, features = ["read-all"], optional = true }
|
gimli = { version = "0.31", default-features = false, features = ["read-all"], optional = true }
|
||||||
|
|
||||||
# ppc
|
# ppc
|
||||||
cwdemangle = { version = "1.0.0", optional = true }
|
cwdemangle = { version = "1.0", optional = true }
|
||||||
cwextab = { version = "0.2.3", optional = true }
|
cwextab = { version = "1.0", optional = true }
|
||||||
ppc750cl = { git = "https://github.com/encounter/ppc750cl", rev = "6cbd7d888c7082c2c860f66cbb9848d633f753ed", optional = true }
|
ppc750cl = { version = "0.3", optional = true }
|
||||||
|
|
||||||
# mips
|
# mips
|
||||||
rabbitizer = { version = "1.11.0", optional = true }
|
rabbitizer = { version = "1.12", optional = true }
|
||||||
|
|
||||||
# x86
|
# x86
|
||||||
cpp_demangle = { version = "0.4.3", optional = true }
|
cpp_demangle = { version = "0.4", optional = true }
|
||||||
iced-x86 = { version = "1.21.0", default-features = false, features = ["std", "decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums"], optional = true }
|
iced-x86 = { version = "1.21", default-features = false, features = ["std", "decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums"], optional = true }
|
||||||
msvc-demangler = { version = "0.10.0", optional = true }
|
msvc-demangler = { version = "0.10", optional = true }
|
||||||
|
|
||||||
# arm
|
# arm
|
||||||
unarm = { version = "1.5.0", optional = true }
|
unarm = { version = "1.6", optional = true }
|
||||||
arm-attr = { version = "0.1.1", optional = true }
|
arm-attr = { version = "0.1", optional = true }
|
||||||
|
|
||||||
|
# arm64
|
||||||
|
yaxpeax-arch = { version = "0.3", default-features = false, features = ["std"], optional = true }
|
||||||
|
yaxpeax-arm = { version = "0.3", default-features = false, features = ["std"], optional = true }
|
||||||
|
|
||||||
|
# build
|
||||||
|
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.15", optional = true }
|
||||||
|
time = { version = "0.3", optional = true }
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
path-slash = { version = "0.2", optional = true }
|
||||||
|
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]
|
[build-dependencies]
|
||||||
prost-build = "0.13.1"
|
heck = { version = "0.5", optional = true }
|
||||||
pbjson-build = "0.7.0"
|
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"], optional = true }
|
||||||
|
serde_json = { version = "1.0", optional = true }
|
||||||
|
syn = { version = "2.0", optional = true }
|
||||||
|
|||||||
15
objdiff-core/README.md
Normal file
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.
|
||||||
@@ -1,6 +1,16 @@
|
|||||||
use std::path::{Path, PathBuf};
|
#[cfg(feature = "any-arch")]
|
||||||
|
mod config_gen;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
#[cfg(feature = "bindings")]
|
||||||
|
compile_protos();
|
||||||
|
#[cfg(feature = "any-arch")]
|
||||||
|
config_gen::generate_diff_config();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "bindings")]
|
||||||
|
fn compile_protos() {
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("protos");
|
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("protos");
|
||||||
let descriptor_path = root.join("proto_descriptor.bin");
|
let descriptor_path = root.join("proto_descriptor.bin");
|
||||||
println!("cargo:rerun-if-changed={}", descriptor_path.display());
|
println!("cargo:rerun-if-changed={}", descriptor_path.display());
|
||||||
|
|||||||
248
objdiff-core/config-schema.json
Normal file
248
objdiff-core/config-schema.json
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
{
|
||||||
|
"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": "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": "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"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "x86",
|
||||||
|
"name": "x86",
|
||||||
|
"properties": [
|
||||||
|
"x86.formatter"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
491
objdiff-core/config_gen.rs
Normal file
491
objdiff-core/config_gen.rs
Normal file
@@ -0,0 +1,491 @@
|
|||||||
|
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! {
|
||||||
|
#[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, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
|
||||||
|
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 std::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! {
|
||||||
|
#[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 std::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 {
|
||||||
|
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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum ConfigPropertyKind {
|
||||||
|
Boolean,
|
||||||
|
Choice(&'static [ConfigEnumVariantInfo]),
|
||||||
|
}
|
||||||
|
#enums
|
||||||
|
#[inline(always)]
|
||||||
|
fn default_true() -> bool { true }
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
|
||||||
|
#[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();
|
||||||
|
}
|
||||||
@@ -21,9 +21,9 @@ enum SymbolFlag {
|
|||||||
SYMBOL_NONE = 0;
|
SYMBOL_NONE = 0;
|
||||||
SYMBOL_GLOBAL = 1;
|
SYMBOL_GLOBAL = 1;
|
||||||
SYMBOL_LOCAL = 2;
|
SYMBOL_LOCAL = 2;
|
||||||
SYMBOL_WEAK = 3;
|
SYMBOL_WEAK = 4;
|
||||||
SYMBOL_COMMON = 4;
|
SYMBOL_COMMON = 8;
|
||||||
SYMBOL_HIDDEN = 5;
|
SYMBOL_HIDDEN = 16;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A single parsed instruction
|
// A single parsed instruction
|
||||||
@@ -122,10 +122,17 @@ message InstructionBranchTo {
|
|||||||
uint32 branch_index = 2;
|
uint32 branch_index = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message FunctionDiff {
|
message SymbolRef {
|
||||||
|
optional uint32 section_index = 1;
|
||||||
|
uint32 symbol_index = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SymbolDiff {
|
||||||
Symbol symbol = 1;
|
Symbol symbol = 1;
|
||||||
repeated InstructionDiff instructions = 2;
|
repeated InstructionDiff instructions = 2;
|
||||||
optional float match_percent = 3;
|
optional float match_percent = 3;
|
||||||
|
// The symbol ref in the _other_ object that this symbol was diffed against
|
||||||
|
optional SymbolRef target = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message DataDiff {
|
message DataDiff {
|
||||||
@@ -140,7 +147,7 @@ message SectionDiff {
|
|||||||
SectionKind kind = 2;
|
SectionKind kind = 2;
|
||||||
uint64 size = 3;
|
uint64 size = 3;
|
||||||
uint64 address = 4;
|
uint64 address = 4;
|
||||||
repeated FunctionDiff functions = 5;
|
repeated SymbolDiff symbols = 5;
|
||||||
repeated DataDiff data = 6;
|
repeated DataDiff data = 6;
|
||||||
optional float match_percent = 7;
|
optional float match_percent = 7;
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -32,6 +32,10 @@ message Measures {
|
|||||||
uint64 complete_data = 13;
|
uint64 complete_data = 13;
|
||||||
// Completed (or "linked") data percent
|
// Completed (or "linked") data percent
|
||||||
float complete_data_percent = 14;
|
float complete_data_percent = 14;
|
||||||
|
// Total number of units
|
||||||
|
uint32 total_units = 15;
|
||||||
|
// Completed (or "linked") units
|
||||||
|
uint32 complete_units = 16;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Project progress report
|
// Project progress report
|
||||||
|
|||||||
@@ -124,11 +124,9 @@ impl ObjArch for ObjArchArm {
|
|||||||
.get(&SectionIndex(section_index))
|
.get(&SectionIndex(section_index))
|
||||||
.map(|x| x.as_slice())
|
.map(|x| x.as_slice())
|
||||||
.unwrap_or(&fallback_mappings);
|
.unwrap_or(&fallback_mappings);
|
||||||
let first_mapping_idx =
|
let first_mapping_idx = mapping_symbols
|
||||||
match mapping_symbols.binary_search_by_key(&start_addr, |x| x.address) {
|
.binary_search_by_key(&start_addr, |x| x.address)
|
||||||
Ok(idx) => idx,
|
.unwrap_or_else(|idx| idx - 1);
|
||||||
Err(idx) => idx - 1,
|
|
||||||
};
|
|
||||||
let first_mapping = mapping_symbols[first_mapping_idx].mapping;
|
let first_mapping = mapping_symbols[first_mapping_idx].mapping;
|
||||||
|
|
||||||
let mut mappings_iter =
|
let mut mappings_iter =
|
||||||
@@ -141,9 +139,9 @@ impl ObjArch for ObjArchArm {
|
|||||||
|
|
||||||
let version = match config.arm_arch_version {
|
let version = match config.arm_arch_version {
|
||||||
ArmArchVersion::Auto => self.detected_version.unwrap_or(ArmVersion::V5Te),
|
ArmArchVersion::Auto => self.detected_version.unwrap_or(ArmVersion::V5Te),
|
||||||
ArmArchVersion::V4T => ArmVersion::V4T,
|
ArmArchVersion::V4t => ArmVersion::V4T,
|
||||||
ArmArchVersion::V5TE => ArmVersion::V5Te,
|
ArmArchVersion::V5te => ArmVersion::V5Te,
|
||||||
ArmArchVersion::V6K => ArmVersion::V6K,
|
ArmArchVersion::V6k => ArmVersion::V6K,
|
||||||
};
|
};
|
||||||
let endian = match self.endianness {
|
let endian = match self.endianness {
|
||||||
object::Endianness::Little => unarm::Endian::Little,
|
object::Endianness::Little => unarm::Endian::Little,
|
||||||
@@ -215,7 +213,7 @@ impl ObjArch for ObjArchArm {
|
|||||||
address: address as u64,
|
address: address as u64,
|
||||||
size: (parser.address - address) as u8,
|
size: (parser.address - address) as u8,
|
||||||
op: ins.opcode_id(),
|
op: ins.opcode_id(),
|
||||||
mnemonic: parsed_ins.mnemonic.to_string(),
|
mnemonic: Cow::Borrowed(parsed_ins.mnemonic),
|
||||||
args,
|
args,
|
||||||
reloc,
|
reloc,
|
||||||
branch_dest,
|
branch_dest,
|
||||||
@@ -234,7 +232,7 @@ impl ObjArch for ObjArchArm {
|
|||||||
section: &ObjSection,
|
section: &ObjSection,
|
||||||
address: u64,
|
address: u64,
|
||||||
reloc: &Relocation,
|
reloc: &Relocation,
|
||||||
) -> anyhow::Result<i64> {
|
) -> Result<i64> {
|
||||||
let address = address as usize;
|
let address = address as usize;
|
||||||
Ok(match reloc.flags() {
|
Ok(match reloc.flags() {
|
||||||
// ARM calls
|
// ARM calls
|
||||||
@@ -278,6 +276,19 @@ impl ObjArch for ObjArchArm {
|
|||||||
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
|
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
|
||||||
Cow::Owned(format!("<{flags:?}>"))
|
Cow::Owned(format!("<{flags:?}>"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_reloc_byte_size(&self, flags: RelocationFlags) -> usize {
|
||||||
|
match flags {
|
||||||
|
RelocationFlags::Elf { r_type } => match r_type {
|
||||||
|
elf::R_ARM_ABS32 => 4,
|
||||||
|
elf::R_ARM_REL32 => 4,
|
||||||
|
elf::R_ARM_ABS16 => 2,
|
||||||
|
elf::R_ARM_ABS8 => 1,
|
||||||
|
_ => 1,
|
||||||
|
},
|
||||||
|
_ => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
|||||||
2855
objdiff-core/src/arch/arm64.rs
Normal file
2855
objdiff-core/src/arch/arm64.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -83,7 +83,7 @@ impl ObjArch for ObjArchMips {
|
|||||||
&self,
|
&self,
|
||||||
address: u64,
|
address: u64,
|
||||||
code: &[u8],
|
code: &[u8],
|
||||||
_section_index: usize,
|
section_index: usize,
|
||||||
relocations: &[ObjReloc],
|
relocations: &[ObjReloc],
|
||||||
line_info: &BTreeMap<u64, u32>,
|
line_info: &BTreeMap<u64, u32>,
|
||||||
config: &DiffObjConfig,
|
config: &DiffObjConfig,
|
||||||
@@ -99,8 +99,8 @@ impl ObjArch for ObjArchMips {
|
|||||||
MipsInstrCategory::Auto => self.instr_category,
|
MipsInstrCategory::Auto => self.instr_category,
|
||||||
MipsInstrCategory::Cpu => InstrCategory::CPU,
|
MipsInstrCategory::Cpu => InstrCategory::CPU,
|
||||||
MipsInstrCategory::Rsp => InstrCategory::RSP,
|
MipsInstrCategory::Rsp => InstrCategory::RSP,
|
||||||
MipsInstrCategory::R3000Gte => InstrCategory::R3000GTE,
|
MipsInstrCategory::R3000gte => InstrCategory::R3000GTE,
|
||||||
MipsInstrCategory::R4000Allegrex => InstrCategory::R4000ALLEGREX,
|
MipsInstrCategory::R4000allegrex => InstrCategory::R4000ALLEGREX,
|
||||||
MipsInstrCategory::R5900 => InstrCategory::R5900,
|
MipsInstrCategory::R5900 => InstrCategory::R5900,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ impl ObjArch for ObjArchMips {
|
|||||||
let op = instruction.unique_id as u16;
|
let op = instruction.unique_id as u16;
|
||||||
ops.push(op);
|
ops.push(op);
|
||||||
|
|
||||||
let mnemonic = instruction.opcode_name().to_string();
|
let mnemonic = instruction.opcode_name();
|
||||||
let is_branch = instruction.is_branch();
|
let is_branch = instruction.is_branch();
|
||||||
let branch_offset = instruction.branch_offset();
|
let branch_offset = instruction.branch_offset();
|
||||||
let mut branch_dest = if is_branch {
|
let mut branch_dest = if is_branch {
|
||||||
@@ -140,11 +140,18 @@ impl ObjArch for ObjArchMips {
|
|||||||
| OperandType::cpu_label
|
| OperandType::cpu_label
|
||||||
| OperandType::cpu_branch_target_label => {
|
| OperandType::cpu_branch_target_label => {
|
||||||
if let Some(reloc) = reloc {
|
if let Some(reloc) = reloc {
|
||||||
if matches!(&reloc.target_section, Some(s) if s == ".text")
|
// If the relocation target is within the current function, we can
|
||||||
&& reloc.target.address > start_address
|
// convert it into a relative branch target. Note that we check
|
||||||
&& reloc.target.address < end_address
|
// target_address > start_address instead of >= so that recursive
|
||||||
|
// tail calls are not considered branch targets.
|
||||||
|
let target_address =
|
||||||
|
reloc.target.address.checked_add_signed(reloc.addend);
|
||||||
|
if reloc.target.orig_section_index == Some(section_index)
|
||||||
|
&& matches!(target_address, Some(addr) if addr > start_address && addr < end_address)
|
||||||
{
|
{
|
||||||
args.push(ObjInsArg::BranchDest(reloc.target.address));
|
let target_address = target_address.unwrap();
|
||||||
|
args.push(ObjInsArg::BranchDest(target_address));
|
||||||
|
branch_dest = Some(target_address);
|
||||||
} else {
|
} else {
|
||||||
push_reloc(&mut args, reloc)?;
|
push_reloc(&mut args, reloc)?;
|
||||||
branch_dest = None;
|
branch_dest = None;
|
||||||
@@ -195,7 +202,7 @@ impl ObjArch for ObjArchMips {
|
|||||||
address: cur_addr as u64,
|
address: cur_addr as u64,
|
||||||
size: 4,
|
size: 4,
|
||||||
op,
|
op,
|
||||||
mnemonic,
|
mnemonic: Cow::Borrowed(mnemonic),
|
||||||
args,
|
args,
|
||||||
reloc: reloc.cloned(),
|
reloc: reloc.cloned(),
|
||||||
branch_dest,
|
branch_dest,
|
||||||
@@ -264,6 +271,17 @@ impl ObjArch for ObjArchMips {
|
|||||||
_ => Cow::Owned(format!("<{flags:?}>")),
|
_ => Cow::Owned(format!("<{flags:?}>")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_reloc_byte_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_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {
|
fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
use std::{borrow::Cow, collections::BTreeMap};
|
use std::{borrow::Cow, collections::BTreeMap, ffi::CStr};
|
||||||
|
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
|
use byteorder::ByteOrder;
|
||||||
use object::{Architecture, File, Object, ObjectSymbol, Relocation, RelocationFlags, Symbol};
|
use object::{Architecture, File, Object, ObjectSymbol, Relocation, RelocationFlags, Symbol};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
diff::DiffObjConfig,
|
diff::DiffObjConfig,
|
||||||
obj::{ObjIns, ObjReloc, ObjSection},
|
obj::{ObjIns, ObjReloc, ObjSection},
|
||||||
|
util::ReallySigned,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "arm")]
|
#[cfg(feature = "arm")]
|
||||||
mod arm;
|
mod arm;
|
||||||
|
#[cfg(feature = "arm64")]
|
||||||
|
mod arm64;
|
||||||
#[cfg(feature = "mips")]
|
#[cfg(feature = "mips")]
|
||||||
pub mod mips;
|
pub mod mips;
|
||||||
#[cfg(feature = "ppc")]
|
#[cfg(feature = "ppc")]
|
||||||
@@ -17,6 +21,110 @@ pub mod ppc;
|
|||||||
#[cfg(feature = "x86")]
|
#[cfg(feature = "x86")]
|
||||||
pub mod x86;
|
pub mod x86;
|
||||||
|
|
||||||
|
/// Represents the type of data associated with an instruction
|
||||||
|
pub enum DataType {
|
||||||
|
Int8,
|
||||||
|
Int16,
|
||||||
|
Int32,
|
||||||
|
Int64,
|
||||||
|
Int128,
|
||||||
|
Float,
|
||||||
|
Double,
|
||||||
|
Bytes,
|
||||||
|
String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataType {
|
||||||
|
pub fn display_bytes<Endian: ByteOrder>(&self, bytes: &[u8]) -> Option<String> {
|
||||||
|
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 None;
|
||||||
|
}
|
||||||
|
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());
|
||||||
|
if i < 0 {
|
||||||
|
format!("Int8: {:#x} ({:#x})", i, ReallySigned(i))
|
||||||
|
} else {
|
||||||
|
format!("Int8: {:#x}", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DataType::Int16 => {
|
||||||
|
let i = Endian::read_i16(bytes);
|
||||||
|
if i < 0 {
|
||||||
|
format!("Int16: {:#x} ({:#x})", i, ReallySigned(i))
|
||||||
|
} else {
|
||||||
|
format!("Int16: {:#x}", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DataType::Int32 => {
|
||||||
|
let i = Endian::read_i32(bytes);
|
||||||
|
if i < 0 {
|
||||||
|
format!("Int32: {:#x} ({:#x})", i, ReallySigned(i))
|
||||||
|
} else {
|
||||||
|
format!("Int32: {:#x}", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DataType::Int64 => {
|
||||||
|
let i = Endian::read_i64(bytes);
|
||||||
|
if i < 0 {
|
||||||
|
format!("Int64: {:#x} ({:#x})", i, ReallySigned(i))
|
||||||
|
} else {
|
||||||
|
format!("Int64: {:#x}", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DataType::Int128 => {
|
||||||
|
let i = Endian::read_i128(bytes);
|
||||||
|
if i < 0 {
|
||||||
|
format!("Int128: {:#x} ({:#x})", i, ReallySigned(i))
|
||||||
|
} else {
|
||||||
|
format!("Int128: {:#x}", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DataType::Float => {
|
||||||
|
format!("Float: {:?}f", Endian::read_f32(bytes))
|
||||||
|
}
|
||||||
|
DataType::Double => {
|
||||||
|
format!("Double: {:?}", Endian::read_f64(bytes))
|
||||||
|
}
|
||||||
|
DataType::Bytes => {
|
||||||
|
format!("Bytes: {:#?}", bytes)
|
||||||
|
}
|
||||||
|
DataType::String => {
|
||||||
|
format!("String: {:?}", CStr::from_bytes_until_nul(bytes).ok()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn required_len(&self) -> Option<usize> {
|
||||||
|
match self {
|
||||||
|
DataType::Int8 => Some(1),
|
||||||
|
DataType::Int16 => Some(2),
|
||||||
|
DataType::Int32 => Some(4),
|
||||||
|
DataType::Int64 => Some(8),
|
||||||
|
DataType::Int128 => Some(16),
|
||||||
|
DataType::Float => Some(4),
|
||||||
|
DataType::Double => Some(8),
|
||||||
|
DataType::Bytes => None,
|
||||||
|
DataType::String => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait ObjArch: Send + Sync {
|
pub trait ObjArch: Send + Sync {
|
||||||
fn process_code(
|
fn process_code(
|
||||||
&self,
|
&self,
|
||||||
@@ -40,7 +148,30 @@ pub trait ObjArch: Send + Sync {
|
|||||||
|
|
||||||
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str>;
|
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str>;
|
||||||
|
|
||||||
|
fn get_reloc_byte_size(&self, flags: RelocationFlags) -> usize;
|
||||||
|
|
||||||
fn symbol_address(&self, symbol: &Symbol) -> u64 { symbol.address() }
|
fn symbol_address(&self, symbol: &Symbol) -> u64 { symbol.address() }
|
||||||
|
|
||||||
|
fn guess_data_type(&self, _instruction: &ObjIns) -> Option<DataType> { None }
|
||||||
|
|
||||||
|
fn display_data_type(&self, _ty: DataType, bytes: &[u8]) -> Option<String> {
|
||||||
|
Some(format!("Bytes: {:#x?}", bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_ins_data(&self, ins: &ObjIns) -> Option<String> {
|
||||||
|
let reloc = ins.reloc.as_ref()?;
|
||||||
|
if reloc.addend >= 0 && reloc.target.bytes.len() > reloc.addend as usize {
|
||||||
|
self.guess_data_type(ins).and_then(|ty| {
|
||||||
|
self.display_data_type(ty, &reloc.target.bytes[reloc.addend as usize..])
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Downcast methods
|
||||||
|
#[cfg(feature = "ppc")]
|
||||||
|
fn ppc(&self) -> Option<&ppc::ObjArchPpc> { None }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ProcessCodeResult {
|
pub struct ProcessCodeResult {
|
||||||
@@ -48,7 +179,7 @@ pub struct ProcessCodeResult {
|
|||||||
pub insts: Vec<ObjIns>,
|
pub insts: Vec<ObjIns>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_arch(object: &object::File) -> Result<Box<dyn ObjArch>> {
|
pub fn new_arch(object: &File) -> Result<Box<dyn ObjArch>> {
|
||||||
Ok(match object.architecture() {
|
Ok(match object.architecture() {
|
||||||
#[cfg(feature = "ppc")]
|
#[cfg(feature = "ppc")]
|
||||||
Architecture::PowerPc => Box::new(ppc::ObjArchPpc::new(object)?),
|
Architecture::PowerPc => Box::new(ppc::ObjArchPpc::new(object)?),
|
||||||
@@ -58,6 +189,8 @@ pub fn new_arch(object: &object::File) -> Result<Box<dyn ObjArch>> {
|
|||||||
Architecture::I386 | Architecture::X86_64 => Box::new(x86::ObjArchX86::new(object)?),
|
Architecture::I386 | Architecture::X86_64 => Box::new(x86::ObjArchX86::new(object)?),
|
||||||
#[cfg(feature = "arm")]
|
#[cfg(feature = "arm")]
|
||||||
Architecture::Arm => Box::new(arm::ObjArchArm::new(object)?),
|
Architecture::Arm => Box::new(arm::ObjArchArm::new(object)?),
|
||||||
|
#[cfg(feature = "arm64")]
|
||||||
|
Architecture::Aarch64 => Box::new(arm64::ObjArchArm64::new(object)?),
|
||||||
arch => bail!("Unsupported architecture: {arch:?}"),
|
arch => bail!("Unsupported architecture: {arch:?}"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
use std::{borrow::Cow, collections::BTreeMap};
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
collections::{BTreeMap, HashMap, HashSet},
|
||||||
|
};
|
||||||
|
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, ensure, Result};
|
||||||
use object::{elf, File, Relocation, RelocationFlags};
|
use byteorder::BigEndian;
|
||||||
use ppc750cl::{Argument, InsIter, GPR};
|
use cwextab::{decode_extab, ExceptionTableData};
|
||||||
|
use object::{
|
||||||
|
elf, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, RelocationTarget,
|
||||||
|
Symbol, SymbolKind,
|
||||||
|
};
|
||||||
|
use ppc750cl::{Argument, Arguments, Ins, InsIter, Opcode, ParsedIns, GPR};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
arch::{ObjArch, ProcessCodeResult},
|
arch::{DataType, ObjArch, ProcessCodeResult},
|
||||||
diff::DiffObjConfig,
|
diff::DiffObjConfig,
|
||||||
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection},
|
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection, ObjSymbol},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Relative relocation, can be Simm, Offset or BranchDest
|
// Relative relocation, can be Simm, Offset or BranchDest
|
||||||
@@ -22,10 +30,13 @@ fn is_rel_abs_arg(arg: &Argument) -> bool {
|
|||||||
|
|
||||||
fn is_offset_arg(arg: &Argument) -> bool { matches!(arg, Argument::Offset(_)) }
|
fn is_offset_arg(arg: &Argument) -> bool { matches!(arg, Argument::Offset(_)) }
|
||||||
|
|
||||||
pub struct ObjArchPpc {}
|
pub struct ObjArchPpc {
|
||||||
|
/// Exception info
|
||||||
|
pub extab: Option<BTreeMap<usize, ExceptionInfo>>,
|
||||||
|
}
|
||||||
|
|
||||||
impl ObjArchPpc {
|
impl ObjArchPpc {
|
||||||
pub fn new(_file: &File) -> Result<Self> { Ok(Self {}) }
|
pub fn new(file: &File) -> Result<Self> { Ok(Self { extab: decode_exception_info(file)? }) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ObjArch for ObjArchPpc {
|
impl ObjArch for ObjArchPpc {
|
||||||
@@ -41,6 +52,8 @@ impl ObjArch for ObjArchPpc {
|
|||||||
let ins_count = code.len() / 4;
|
let ins_count = code.len() / 4;
|
||||||
let mut ops = Vec::<u16>::with_capacity(ins_count);
|
let mut ops = Vec::<u16>::with_capacity(ins_count);
|
||||||
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
|
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
|
||||||
|
let fake_pool_reloc_for_addr =
|
||||||
|
generate_fake_pool_reloc_for_addr_mapping(address, code, relocations);
|
||||||
for (cur_addr, mut ins) in InsIter::new(code, address as u32) {
|
for (cur_addr, mut ins) in InsIter::new(code, address as u32) {
|
||||||
let reloc = relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
|
let reloc = relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
|
||||||
if let Some(reloc) = reloc {
|
if let Some(reloc) = reloc {
|
||||||
@@ -130,14 +143,23 @@ impl ObjArch for ObjArchPpc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if reloc.is_none() {
|
||||||
|
if let Some(fake_pool_reloc) = fake_pool_reloc_for_addr.get(&cur_addr) {
|
||||||
|
// If this instruction has a fake pool relocation, show it as a fake argument
|
||||||
|
// at the end of the line.
|
||||||
|
args.push(ObjInsArg::PlainText(" ".into()));
|
||||||
|
push_reloc(&mut args, fake_pool_reloc)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ops.push(ins.op as u16);
|
ops.push(ins.op as u16);
|
||||||
let line = line_info.range(..=cur_addr as u64).last().map(|(_, &b)| b);
|
let line = line_info.range(..=cur_addr as u64).last().map(|(_, &b)| b);
|
||||||
insts.push(ObjIns {
|
insts.push(ObjIns {
|
||||||
address: cur_addr as u64,
|
address: cur_addr as u64,
|
||||||
size: 4,
|
size: 4,
|
||||||
mnemonic: simplified.mnemonic.to_string(),
|
mnemonic: Cow::Borrowed(simplified.mnemonic),
|
||||||
args,
|
args,
|
||||||
reloc: reloc.cloned(),
|
reloc: reloc.or(fake_pool_reloc_for_addr.get(&cur_addr)).cloned(),
|
||||||
op: ins.op as u16,
|
op: ins.op as u16,
|
||||||
branch_dest,
|
branch_dest,
|
||||||
line,
|
line,
|
||||||
@@ -165,6 +187,7 @@ impl ObjArch for ObjArchPpc {
|
|||||||
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
|
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
|
||||||
match flags {
|
match flags {
|
||||||
RelocationFlags::Elf { r_type } => match r_type {
|
RelocationFlags::Elf { r_type } => match r_type {
|
||||||
|
elf::R_PPC_NONE => Cow::Borrowed("R_PPC_NONE"), // We use this for fake pool relocs
|
||||||
elf::R_PPC_ADDR16_LO => Cow::Borrowed("R_PPC_ADDR16_LO"),
|
elf::R_PPC_ADDR16_LO => Cow::Borrowed("R_PPC_ADDR16_LO"),
|
||||||
elf::R_PPC_ADDR16_HI => Cow::Borrowed("R_PPC_ADDR16_HI"),
|
elf::R_PPC_ADDR16_HI => Cow::Borrowed("R_PPC_ADDR16_HI"),
|
||||||
elf::R_PPC_ADDR16_HA => Cow::Borrowed("R_PPC_ADDR16_HA"),
|
elf::R_PPC_ADDR16_HA => Cow::Borrowed("R_PPC_ADDR16_HA"),
|
||||||
@@ -178,6 +201,37 @@ impl ObjArch for ObjArchPpc {
|
|||||||
_ => Cow::Owned(format!("<{flags:?}>")),
|
_ => Cow::Owned(format!("<{flags:?}>")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_reloc_byte_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 guess_data_type(&self, instruction: &ObjIns) -> Option<super::DataType> {
|
||||||
|
if instruction.reloc.as_ref().is_some_and(|r| r.target.name.starts_with("@stringBase")) {
|
||||||
|
return Some(DataType::String);
|
||||||
|
}
|
||||||
|
|
||||||
|
guess_data_type_from_load_store_inst_op(Opcode::from(instruction.op as u8))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_data_type(&self, ty: DataType, bytes: &[u8]) -> Option<String> {
|
||||||
|
ty.display_bytes::<BigEndian>(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppc(&self) -> Option<&ObjArchPpc> { Some(self) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjArchPpc {
|
||||||
|
pub fn extab_for_symbol(&self, symbol: &ObjSymbol) -> Option<&ExceptionInfo> {
|
||||||
|
symbol.original_index.and_then(|i| self.extab.as_ref()?.get(&i))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {
|
fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {
|
||||||
@@ -202,9 +256,486 @@ fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {
|
|||||||
elf::R_PPC_ADDR32 | elf::R_PPC_UADDR32 | elf::R_PPC_REL24 | elf::R_PPC_REL14 => {
|
elf::R_PPC_ADDR32 | elf::R_PPC_UADDR32 | elf::R_PPC_REL24 | elf::R_PPC_REL14 => {
|
||||||
args.push(ObjInsArg::Reloc);
|
args.push(ObjInsArg::Reloc);
|
||||||
}
|
}
|
||||||
|
elf::R_PPC_NONE => {
|
||||||
|
// Fake pool relocation.
|
||||||
|
args.push(ObjInsArg::PlainText("<".into()));
|
||||||
|
args.push(ObjInsArg::Reloc);
|
||||||
|
args.push(ObjInsArg::PlainText(">".into()));
|
||||||
|
}
|
||||||
_ => bail!("Unsupported ELF PPC relocation type {r_type}"),
|
_ => bail!("Unsupported ELF PPC relocation type {r_type}"),
|
||||||
},
|
},
|
||||||
flags => bail!("Unsupported PPC relocation kind: {flags:?}"),
|
flags => bail!("Unsupported PPC relocation kind: {flags:?}"),
|
||||||
};
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ExtabSymbolRef {
|
||||||
|
pub original_index: usize,
|
||||||
|
pub name: String,
|
||||||
|
pub demangled_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ExceptionInfo {
|
||||||
|
pub eti_symbol: ExtabSymbolRef,
|
||||||
|
pub etb_symbol: ExtabSymbolRef,
|
||||||
|
pub data: ExceptionTableData,
|
||||||
|
pub dtors: Vec<ExtabSymbolRef>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_exception_info(file: &File<'_>) -> Result<Option<BTreeMap<usize, ExceptionInfo>>> {
|
||||||
|
let Some(extab_section) = file.section_by_name("extab") else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let Some(extabindex_section) = file.section_by_name("extabindex") else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut result = BTreeMap::new();
|
||||||
|
let extab_relocations = extab_section.relocations().collect::<BTreeMap<u64, Relocation>>();
|
||||||
|
let extabindex_relocations =
|
||||||
|
extabindex_section.relocations().collect::<BTreeMap<u64, Relocation>>();
|
||||||
|
|
||||||
|
for extabindex in file.symbols().filter(|symbol| {
|
||||||
|
symbol.section_index() == Some(extabindex_section.index())
|
||||||
|
&& symbol.kind() == SymbolKind::Data
|
||||||
|
}) {
|
||||||
|
if extabindex.size() != 12 {
|
||||||
|
log::warn!("Invalid extabindex entry size {}", extabindex.size());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each extabindex entry has two relocations:
|
||||||
|
// - 0x0: The function that the exception table is for
|
||||||
|
// - 0x8: The relevant entry in extab section
|
||||||
|
let Some(extab_func_reloc) = extabindex_relocations.get(&extabindex.address()) else {
|
||||||
|
log::warn!("Failed to find function relocation for extabindex entry");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(extab_reloc) = extabindex_relocations.get(&(extabindex.address() + 8)) else {
|
||||||
|
log::warn!("Failed to find extab relocation for extabindex entry");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resolve the function and extab symbols
|
||||||
|
let Some(extab_func) = relocation_symbol(file, extab_func_reloc)? else {
|
||||||
|
log::warn!("Failed to find function symbol for extabindex entry");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let extab_func_name = extab_func.name()?;
|
||||||
|
let Some(extab) = relocation_symbol(file, extab_reloc)? else {
|
||||||
|
log::warn!("Failed to find extab symbol for extabindex entry");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let extab_start_addr = extab.address() - extab_section.address();
|
||||||
|
let extab_end_addr = extab_start_addr + extab.size();
|
||||||
|
|
||||||
|
// All relocations in the extab section are dtors
|
||||||
|
let mut dtors: Vec<ExtabSymbolRef> = vec![];
|
||||||
|
for (_, reloc) in extab_relocations.range(extab_start_addr..extab_end_addr) {
|
||||||
|
let Some(symbol) = relocation_symbol(file, reloc)? else {
|
||||||
|
log::warn!("Failed to find symbol for extab relocation");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
dtors.push(make_symbol_ref(&symbol)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the extab data
|
||||||
|
let Some(extab_data) = extab_section.data_range(extab_start_addr, extab.size())? else {
|
||||||
|
log::warn!("Failed to get extab data for function {}", extab_func_name);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let data = match decode_extab(extab_data) {
|
||||||
|
Ok(decoded_data) => decoded_data,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!(
|
||||||
|
"Exception table decoding failed for function {}, reason: {}",
|
||||||
|
extab_func_name,
|
||||||
|
e.to_string()
|
||||||
|
);
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//Add the new entry to the list
|
||||||
|
result.insert(extab_func.index().0, ExceptionInfo {
|
||||||
|
eti_symbol: make_symbol_ref(&extabindex)?,
|
||||||
|
etb_symbol: make_symbol_ref(&extab)?,
|
||||||
|
data,
|
||||||
|
dtors,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn relocation_symbol<'data, 'file>(
|
||||||
|
file: &'file File<'data>,
|
||||||
|
relocation: &Relocation,
|
||||||
|
) -> Result<Option<Symbol<'data, 'file>>> {
|
||||||
|
let addend = relocation.addend();
|
||||||
|
match relocation.target() {
|
||||||
|
RelocationTarget::Symbol(idx) => {
|
||||||
|
ensure!(addend == 0, "Symbol relocations must have zero addend");
|
||||||
|
Ok(Some(file.symbol_by_index(idx)?))
|
||||||
|
}
|
||||||
|
RelocationTarget::Section(idx) => {
|
||||||
|
ensure!(addend >= 0, "Section relocations must have non-negative addend");
|
||||||
|
let addend = addend as u64;
|
||||||
|
Ok(file
|
||||||
|
.symbols()
|
||||||
|
.find(|symbol| symbol.section_index() == Some(idx) && symbol.address() == addend))
|
||||||
|
}
|
||||||
|
target => bail!("Unsupported relocation target: {target:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_symbol_ref(symbol: &Symbol) -> Result<ExtabSymbolRef> {
|
||||||
|
let name = symbol.name()?.to_string();
|
||||||
|
let demangled_name = cwdemangle::demangle(&name, &cwdemangle::DemangleOptions::default());
|
||||||
|
Ok(ExtabSymbolRef { original_index: symbol.index().0, name, demangled_name })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn guess_data_type_from_load_store_inst_op(inst_op: Opcode) -> Option<DataType> {
|
||||||
|
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.
|
||||||
|
fn get_offset_and_addr_gpr_for_possible_pool_reference(
|
||||||
|
opcode: Opcode,
|
||||||
|
simplified: &ParsedIns,
|
||||||
|
) -> Option<(i16, GPR, Option<GPR>)> {
|
||||||
|
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.
|
||||||
|
fn clear_overwritten_gprs(ins: Ins, gpr_pool_relocs: &mut HashMap<u8, ObjReloc>) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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: &ObjReloc) -> Option<ObjReloc> {
|
||||||
|
let offset_from_pool = pool_reloc.addend + offset as i64;
|
||||||
|
let target_address = pool_reloc.target.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],
|
||||||
|
) -> HashMap<u32, ObjReloc> {
|
||||||
|
let mut visited_ins_addrs = HashSet::new();
|
||||||
|
let mut pool_reloc_for_addr = HashMap::new();
|
||||||
|
let mut ins_iters_with_gpr_state =
|
||||||
|
vec![(InsIter::new(code, func_address as u32), HashMap::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
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ impl ObjArch for ObjArchX86 {
|
|||||||
address: 0,
|
address: 0,
|
||||||
size: 0,
|
size: 0,
|
||||||
op: 0,
|
op: 0,
|
||||||
mnemonic: String::new(),
|
mnemonic: Cow::Borrowed("<invalid>"),
|
||||||
args: vec![],
|
args: vec![],
|
||||||
reloc: None,
|
reloc: None,
|
||||||
branch_dest: None,
|
branch_dest: None,
|
||||||
@@ -76,7 +76,7 @@ impl ObjArch for ObjArchX86 {
|
|||||||
address,
|
address,
|
||||||
size: instruction.len() as u8,
|
size: instruction.len() as u8,
|
||||||
op,
|
op,
|
||||||
mnemonic: String::new(),
|
mnemonic: Cow::Borrowed("<invalid>"),
|
||||||
args: vec![],
|
args: vec![],
|
||||||
reloc: reloc.cloned(),
|
reloc: reloc.cloned(),
|
||||||
branch_dest: None,
|
branch_dest: None,
|
||||||
@@ -162,6 +162,19 @@ impl ObjArch for ObjArchX86 {
|
|||||||
_ => Cow::Owned(format!("<{flags:?}>")),
|
_ => Cow::Owned(format!("<{flags:?}>")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_reloc_byte_size(&self, flags: RelocationFlags) -> usize {
|
||||||
|
match flags {
|
||||||
|
RelocationFlags::Coff { typ } => match typ {
|
||||||
|
pe::IMAGE_REL_I386_DIR16 => 2,
|
||||||
|
pe::IMAGE_REL_I386_REL16 => 2,
|
||||||
|
pe::IMAGE_REL_I386_DIR32 => 4,
|
||||||
|
pe::IMAGE_REL_I386_REL32 => 4,
|
||||||
|
_ => 1,
|
||||||
|
},
|
||||||
|
_ => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replace_arg(
|
fn replace_arg(
|
||||||
@@ -242,7 +255,8 @@ impl FormatterOutput for InstructionFormatterOutput {
|
|||||||
|
|
||||||
fn write_mnemonic(&mut self, _instruction: &Instruction, text: &str) {
|
fn write_mnemonic(&mut self, _instruction: &Instruction, text: &str) {
|
||||||
self.formatted.push_str(text);
|
self.formatted.push_str(text);
|
||||||
self.ins.mnemonic = text.to_string();
|
// TODO: can iced-x86 guarantee 'static here?
|
||||||
|
self.ins.mnemonic = Cow::Owned(text.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_number(
|
fn write_number(
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
#![allow(clippy::needless_lifetimes)] // Generated serde code
|
||||||
use crate::{
|
use crate::{
|
||||||
diff::{
|
diff::{
|
||||||
ObjDataDiff, ObjDataDiffKind, ObjDiff, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo,
|
ObjDataDiff, ObjDataDiffKind, ObjDiff, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo,
|
||||||
ObjInsDiff, ObjInsDiffKind, ObjSectionDiff, ObjSymbolDiff,
|
ObjInsDiff, ObjInsDiffKind, ObjSectionDiff, ObjSymbolDiff,
|
||||||
},
|
},
|
||||||
|
obj,
|
||||||
obj::{
|
obj::{
|
||||||
ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSectionKind, ObjSymbol,
|
ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSectionKind, ObjSymbol,
|
||||||
ObjSymbolFlagSet, ObjSymbolFlags,
|
ObjSymbolFlagSet, ObjSymbolFlags,
|
||||||
@@ -38,14 +40,14 @@ impl ObjectDiff {
|
|||||||
impl SectionDiff {
|
impl SectionDiff {
|
||||||
pub fn new(obj: &ObjInfo, section_index: usize, section_diff: &ObjSectionDiff) -> Self {
|
pub fn new(obj: &ObjInfo, section_index: usize, section_diff: &ObjSectionDiff) -> Self {
|
||||||
let section = &obj.sections[section_index];
|
let section = &obj.sections[section_index];
|
||||||
let functions = section_diff.symbols.iter().map(|d| FunctionDiff::new(obj, d)).collect();
|
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();
|
let data = section_diff.data_diff.iter().map(|d| DataDiff::new(obj, d)).collect();
|
||||||
Self {
|
Self {
|
||||||
name: section.name.to_string(),
|
name: section.name.to_string(),
|
||||||
kind: SectionKind::from(section.kind) as i32,
|
kind: SectionKind::from(section.kind) as i32,
|
||||||
size: section.size,
|
size: section.size,
|
||||||
address: section.address,
|
address: section.address,
|
||||||
functions,
|
symbols,
|
||||||
data,
|
data,
|
||||||
match_percent: section_diff.match_percent,
|
match_percent: section_diff.match_percent,
|
||||||
}
|
}
|
||||||
@@ -63,19 +65,32 @@ impl From<ObjSectionKind> for SectionKind {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FunctionDiff {
|
impl From<obj::SymbolRef> for SymbolRef {
|
||||||
|
fn from(value: obj::SymbolRef) -> Self {
|
||||||
|
Self {
|
||||||
|
section_index: if value.section_idx == obj::SECTION_COMMON {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(value.section_idx as u32)
|
||||||
|
},
|
||||||
|
symbol_index: value.symbol_idx as u32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SymbolDiff {
|
||||||
pub fn new(object: &ObjInfo, symbol_diff: &ObjSymbolDiff) -> Self {
|
pub fn new(object: &ObjInfo, symbol_diff: &ObjSymbolDiff) -> Self {
|
||||||
let (_section, symbol) = object.section_symbol(symbol_diff.symbol_ref);
|
let (_section, symbol) = object.section_symbol(symbol_diff.symbol_ref);
|
||||||
// let diff_symbol = symbol_diff.diff_symbol.map(|symbol_ref| {
|
let instructions = symbol_diff
|
||||||
// let (_section, symbol) = object.section_symbol(symbol_ref);
|
.instructions
|
||||||
// Symbol::from(symbol)
|
.iter()
|
||||||
// });
|
.map(|ins_diff| InstructionDiff::new(object, ins_diff))
|
||||||
let instructions = symbol_diff.instructions.iter().map(InstructionDiff::from).collect();
|
.collect();
|
||||||
Self {
|
Self {
|
||||||
symbol: Some(Symbol::from(symbol)),
|
symbol: Some(Symbol::new(symbol)),
|
||||||
// diff_symbol,
|
|
||||||
instructions,
|
instructions,
|
||||||
match_percent: symbol_diff.match_percent,
|
match_percent: symbol_diff.match_percent,
|
||||||
|
target: symbol_diff.target_symbol.map(SymbolRef::from),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,8 +105,8 @@ impl DataDiff {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a ObjSymbol> for Symbol {
|
impl Symbol {
|
||||||
fn from(value: &'a ObjSymbol) -> Self {
|
pub fn new(value: &ObjSymbol) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: value.name.to_string(),
|
name: value.name.to_string(),
|
||||||
demangled_name: value.demangled_name.clone(),
|
demangled_name: value.demangled_name.clone(),
|
||||||
@@ -105,7 +120,7 @@ impl<'a> From<&'a ObjSymbol> for Symbol {
|
|||||||
fn symbol_flags(value: ObjSymbolFlagSet) -> u32 {
|
fn symbol_flags(value: ObjSymbolFlagSet) -> u32 {
|
||||||
let mut flags = 0u32;
|
let mut flags = 0u32;
|
||||||
if value.0.contains(ObjSymbolFlags::Global) {
|
if value.0.contains(ObjSymbolFlags::Global) {
|
||||||
flags |= SymbolFlag::SymbolNone as u32;
|
flags |= SymbolFlag::SymbolGlobal as u32;
|
||||||
}
|
}
|
||||||
if value.0.contains(ObjSymbolFlags::Local) {
|
if value.0.contains(ObjSymbolFlags::Local) {
|
||||||
flags |= SymbolFlag::SymbolLocal as u32;
|
flags |= SymbolFlag::SymbolLocal as u32;
|
||||||
@@ -122,29 +137,29 @@ fn symbol_flags(value: ObjSymbolFlagSet) -> u32 {
|
|||||||
flags
|
flags
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a ObjIns> for Instruction {
|
impl Instruction {
|
||||||
fn from(value: &'a ObjIns) -> Self {
|
pub fn new(object: &ObjInfo, instruction: &ObjIns) -> Self {
|
||||||
Self {
|
Self {
|
||||||
address: value.address,
|
address: instruction.address,
|
||||||
size: value.size as u32,
|
size: instruction.size as u32,
|
||||||
opcode: value.op as u32,
|
opcode: instruction.op as u32,
|
||||||
mnemonic: value.mnemonic.clone(),
|
mnemonic: instruction.mnemonic.to_string(),
|
||||||
formatted: value.formatted.clone(),
|
formatted: instruction.formatted.clone(),
|
||||||
arguments: value.args.iter().map(Argument::from).collect(),
|
arguments: instruction.args.iter().map(Argument::new).collect(),
|
||||||
relocation: value.reloc.as_ref().map(Relocation::from),
|
relocation: instruction.reloc.as_ref().map(|reloc| Relocation::new(object, reloc)),
|
||||||
branch_dest: value.branch_dest,
|
branch_dest: instruction.branch_dest,
|
||||||
line_number: value.line,
|
line_number: instruction.line,
|
||||||
original: value.orig.clone(),
|
original: instruction.orig.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a ObjInsArg> for Argument {
|
impl Argument {
|
||||||
fn from(value: &'a ObjInsArg) -> Self {
|
pub fn new(value: &ObjInsArg) -> Self {
|
||||||
Self {
|
Self {
|
||||||
value: Some(match value {
|
value: Some(match value {
|
||||||
ObjInsArg::PlainText(s) => argument::Value::PlainText(s.to_string()),
|
ObjInsArg::PlainText(s) => argument::Value::PlainText(s.to_string()),
|
||||||
ObjInsArg::Arg(v) => argument::Value::Argument(ArgumentValue::from(v)),
|
ObjInsArg::Arg(v) => argument::Value::Argument(ArgumentValue::new(v)),
|
||||||
ObjInsArg::Reloc => argument::Value::Relocation(ArgumentRelocation {}),
|
ObjInsArg::Reloc => argument::Value::Relocation(ArgumentRelocation {}),
|
||||||
ObjInsArg::BranchDest(dest) => argument::Value::BranchDest(*dest),
|
ObjInsArg::BranchDest(dest) => argument::Value::BranchDest(*dest),
|
||||||
}),
|
}),
|
||||||
@@ -152,8 +167,8 @@ impl<'a> From<&'a ObjInsArg> for Argument {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&ObjInsArgValue> for ArgumentValue {
|
impl ArgumentValue {
|
||||||
fn from(value: &ObjInsArgValue) -> Self {
|
pub fn new(value: &ObjInsArgValue) -> Self {
|
||||||
Self {
|
Self {
|
||||||
value: Some(match value {
|
value: Some(match value {
|
||||||
ObjInsArgValue::Signed(v) => argument_value::Value::Signed(*v),
|
ObjInsArgValue::Signed(v) => argument_value::Value::Signed(*v),
|
||||||
@@ -164,42 +179,39 @@ impl From<&ObjInsArgValue> for ArgumentValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a ObjReloc> for Relocation {
|
impl Relocation {
|
||||||
fn from(value: &ObjReloc) -> Self {
|
pub fn new(object: &ObjInfo, reloc: &ObjReloc) -> Self {
|
||||||
Self {
|
Self {
|
||||||
r#type: match value.flags {
|
r#type: match reloc.flags {
|
||||||
object::RelocationFlags::Elf { r_type } => r_type,
|
object::RelocationFlags::Elf { r_type } => r_type,
|
||||||
object::RelocationFlags::MachO { r_type, .. } => r_type as u32,
|
object::RelocationFlags::MachO { r_type, .. } => r_type as u32,
|
||||||
object::RelocationFlags::Coff { typ } => typ as u32,
|
object::RelocationFlags::Coff { typ } => typ as u32,
|
||||||
object::RelocationFlags::Xcoff { r_rtype, .. } => r_rtype as u32,
|
object::RelocationFlags::Xcoff { r_rtype, .. } => r_rtype as u32,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
},
|
},
|
||||||
type_name: String::new(), // TODO
|
type_name: object.arch.display_reloc(reloc.flags).into_owned(),
|
||||||
target: Some(RelocationTarget::from(&value.target)),
|
target: Some(RelocationTarget {
|
||||||
|
symbol: Some(Symbol::new(&reloc.target)),
|
||||||
|
addend: reloc.addend,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a ObjSymbol> for RelocationTarget {
|
impl InstructionDiff {
|
||||||
fn from(value: &'a ObjSymbol) -> Self {
|
pub fn new(object: &ObjInfo, instruction_diff: &ObjInsDiff) -> Self {
|
||||||
Self { symbol: Some(Symbol::from(value)), addend: value.addend }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<&'a ObjInsDiff> for InstructionDiff {
|
|
||||||
fn from(value: &'a ObjInsDiff) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
instruction: value.ins.as_ref().map(Instruction::from),
|
instruction: instruction_diff.ins.as_ref().map(|ins| Instruction::new(object, ins)),
|
||||||
diff_kind: DiffKind::from(value.kind) as i32,
|
diff_kind: DiffKind::from(instruction_diff.kind) as i32,
|
||||||
branch_from: value.branch_from.as_ref().map(InstructionBranchFrom::from),
|
branch_from: instruction_diff.branch_from.as_ref().map(InstructionBranchFrom::new),
|
||||||
branch_to: value.branch_to.as_ref().map(InstructionBranchTo::from),
|
branch_to: instruction_diff.branch_to.as_ref().map(InstructionBranchTo::new),
|
||||||
arg_diff: value.arg_diff.iter().map(ArgumentDiff::from).collect(),
|
arg_diff: instruction_diff.arg_diff.iter().map(ArgumentDiff::new).collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Option<ObjInsArgDiff>> for ArgumentDiff {
|
impl ArgumentDiff {
|
||||||
fn from(value: &Option<ObjInsArgDiff>) -> Self {
|
pub fn new(value: &Option<ObjInsArgDiff>) -> Self {
|
||||||
Self { diff_index: value.as_ref().map(|v| v.idx as u32) }
|
Self { diff_index: value.as_ref().map(|v| v.idx as u32) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,8 +240,8 @@ impl From<ObjDataDiffKind> for DiffKind {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a ObjInsBranchFrom> for InstructionBranchFrom {
|
impl InstructionBranchFrom {
|
||||||
fn from(value: &'a ObjInsBranchFrom) -> Self {
|
pub fn new(value: &ObjInsBranchFrom) -> Self {
|
||||||
Self {
|
Self {
|
||||||
instruction_index: value.ins_idx.iter().map(|&x| x as u32).collect(),
|
instruction_index: value.ins_idx.iter().map(|&x| x as u32).collect(),
|
||||||
branch_index: value.branch_idx as u32,
|
branch_index: value.branch_idx as u32,
|
||||||
@@ -237,8 +249,8 @@ impl<'a> From<&'a ObjInsBranchFrom> for InstructionBranchFrom {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a ObjInsBranchTo> for InstructionBranchTo {
|
impl InstructionBranchTo {
|
||||||
fn from(value: &'a ObjInsBranchTo) -> Self {
|
pub fn new(value: &ObjInsBranchTo) -> Self {
|
||||||
Self { instruction_index: value.ins_idx as u32, branch_index: value.branch_idx as u32 }
|
Self { instruction_index: value.ins_idx as u32, branch_index: value.branch_idx as u32 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#![allow(clippy::needless_lifetimes)] // Generated serde code
|
||||||
use std::ops::AddAssign;
|
use std::ops::AddAssign;
|
||||||
|
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
@@ -8,9 +9,10 @@ use serde_json::error::Category;
|
|||||||
include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs"));
|
include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs"));
|
||||||
include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs"));
|
include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs"));
|
||||||
|
|
||||||
pub const REPORT_VERSION: u32 = 1;
|
pub const REPORT_VERSION: u32 = 2;
|
||||||
|
|
||||||
impl Report {
|
impl Report {
|
||||||
|
/// Attempts to parse the report as binary protobuf or JSON.
|
||||||
pub fn parse(data: &[u8]) -> Result<Self> {
|
pub fn parse(data: &[u8]) -> Result<Self> {
|
||||||
if data.is_empty() {
|
if data.is_empty() {
|
||||||
bail!(std::io::Error::from(std::io::ErrorKind::UnexpectedEof));
|
bail!(std::io::Error::from(std::io::ErrorKind::UnexpectedEof));
|
||||||
@@ -25,6 +27,7 @@ impl Report {
|
|||||||
Ok(report)
|
Ok(report)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attempts to parse the report as JSON, migrating from the legacy report format if necessary.
|
||||||
fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
|
fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
|
||||||
match serde_json::from_slice::<Self>(bytes) {
|
match serde_json::from_slice::<Self>(bytes) {
|
||||||
Ok(report) => Ok(report),
|
Ok(report) => Ok(report),
|
||||||
@@ -43,16 +46,23 @@ impl Report {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Migrates the report to the latest version.
|
||||||
|
/// Fails if the report version is newer than supported.
|
||||||
pub fn migrate(&mut self) -> Result<()> {
|
pub fn migrate(&mut self) -> Result<()> {
|
||||||
if self.version == 0 {
|
if self.version == 0 {
|
||||||
self.migrate_v0()?;
|
self.migrate_v0()?;
|
||||||
}
|
}
|
||||||
|
if self.version == 1 {
|
||||||
|
self.migrate_v1()?;
|
||||||
|
}
|
||||||
if self.version != REPORT_VERSION {
|
if self.version != REPORT_VERSION {
|
||||||
bail!("Unsupported report version: {}", self.version);
|
bail!("Unsupported report version: {}", self.version);
|
||||||
}
|
}
|
||||||
Ok(())
|
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<()> {
|
fn migrate_v0(&mut self) -> Result<()> {
|
||||||
let Some(measures) = &mut self.measures else {
|
let Some(measures) = &mut self.measures else {
|
||||||
bail!("Missing measures in report");
|
bail!("Missing measures in report");
|
||||||
@@ -61,15 +71,16 @@ impl Report {
|
|||||||
let Some(unit_measures) = &mut unit.measures else {
|
let Some(unit_measures) = &mut unit.measures else {
|
||||||
bail!("Missing measures in report unit");
|
bail!("Missing measures in report unit");
|
||||||
};
|
};
|
||||||
let Some(metadata) = &mut unit.metadata else {
|
let mut complete = false;
|
||||||
bail!("Missing metadata in report unit");
|
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 metadata.module_name.is_some() || metadata.module_id.is_some() {
|
if complete {
|
||||||
metadata.progress_categories = vec!["modules".to_string()];
|
|
||||||
} else {
|
|
||||||
metadata.progress_categories = vec!["dol".to_string()];
|
|
||||||
}
|
|
||||||
if metadata.complete.unwrap_or(false) {
|
|
||||||
unit_measures.complete_code = unit_measures.total_code;
|
unit_measures.complete_code = unit_measures.total_code;
|
||||||
unit_measures.complete_data = unit_measures.total_data;
|
unit_measures.complete_data = unit_measures.total_data;
|
||||||
unit_measures.complete_code_percent = 100.0;
|
unit_measures.complete_code_percent = 100.0;
|
||||||
@@ -84,10 +95,42 @@ impl Report {
|
|||||||
measures.complete_data += unit_measures.complete_data;
|
measures.complete_data += unit_measures.complete_data;
|
||||||
}
|
}
|
||||||
measures.calc_matched_percent();
|
measures.calc_matched_percent();
|
||||||
|
self.calculate_progress_categories();
|
||||||
self.version = 1;
|
self.version = 1;
|
||||||
Ok(())
|
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) {
|
pub fn calculate_progress_categories(&mut self) {
|
||||||
for unit in &self.units {
|
for unit in &self.units {
|
||||||
let Some(metadata) = unit.metadata.as_ref() else {
|
let Some(metadata) = unit.metadata.as_ref() else {
|
||||||
@@ -117,6 +160,71 @@ impl Report {
|
|||||||
measures.calc_matched_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 {
|
impl Measures {
|
||||||
@@ -176,6 +284,8 @@ impl AddAssign for Measures {
|
|||||||
self.matched_functions += other.matched_functions;
|
self.matched_functions += other.matched_functions;
|
||||||
self.complete_code += other.complete_code;
|
self.complete_code += other.complete_code;
|
||||||
self.complete_data += other.complete_data;
|
self.complete_data += other.complete_data;
|
||||||
|
self.total_units += other.total_units;
|
||||||
|
self.complete_units += other.complete_units;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,20 +13,22 @@ fn parse_object(
|
|||||||
fn parse_and_run_diff(
|
fn parse_and_run_diff(
|
||||||
left: Option<Box<[u8]>>,
|
left: Option<Box<[u8]>>,
|
||||||
right: Option<Box<[u8]>>,
|
right: Option<Box<[u8]>>,
|
||||||
config: diff::DiffObjConfig,
|
diff_config: diff::DiffObjConfig,
|
||||||
|
mapping_config: diff::MappingConfig,
|
||||||
) -> Result<DiffResult, JsError> {
|
) -> Result<DiffResult, JsError> {
|
||||||
let target = parse_object(left, &config)?;
|
let target = parse_object(left, &diff_config)?;
|
||||||
let base = parse_object(right, &config)?;
|
let base = parse_object(right, &diff_config)?;
|
||||||
run_diff(target.as_ref(), base.as_ref(), config)
|
run_diff(target.as_ref(), base.as_ref(), diff_config, mapping_config)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_diff(
|
fn run_diff(
|
||||||
left: Option<&obj::ObjInfo>,
|
left: Option<&obj::ObjInfo>,
|
||||||
right: Option<&obj::ObjInfo>,
|
right: Option<&obj::ObjInfo>,
|
||||||
config: diff::DiffObjConfig,
|
diff_config: diff::DiffObjConfig,
|
||||||
|
mapping_config: diff::MappingConfig,
|
||||||
) -> Result<DiffResult, JsError> {
|
) -> Result<DiffResult, JsError> {
|
||||||
log::debug!("Running diff with config: {:?}", config);
|
log::debug!("Running diff with config: {:?}", diff_config);
|
||||||
let result = diff::diff_objs(&config, left, right, None).to_js()?;
|
let result = diff::diff_objs(&diff_config, &mapping_config, left, right, None).to_js()?;
|
||||||
let left = left.and_then(|o| result.left.as_ref().map(|d| (o, d)));
|
let left = left.and_then(|o| result.left.as_ref().map(|d| (o, d)));
|
||||||
let right = right.and_then(|o| result.right.as_ref().map(|d| (o, d)));
|
let right = right.and_then(|o| result.right.as_ref().map(|d| (o, d)));
|
||||||
Ok(DiffResult::new(left, right))
|
Ok(DiffResult::new(left, right))
|
||||||
@@ -46,9 +48,10 @@ fn run_diff(
|
|||||||
pub fn run_diff_proto(
|
pub fn run_diff_proto(
|
||||||
left: Option<Box<[u8]>>,
|
left: Option<Box<[u8]>>,
|
||||||
right: Option<Box<[u8]>>,
|
right: Option<Box<[u8]>>,
|
||||||
config: diff::DiffObjConfig,
|
diff_config: diff::DiffObjConfig,
|
||||||
|
mapping_config: diff::MappingConfig,
|
||||||
) -> Result<Box<[u8]>, JsError> {
|
) -> Result<Box<[u8]>, JsError> {
|
||||||
let out = parse_and_run_diff(left, right, config)?;
|
let out = parse_and_run_diff(left, right, diff_config, mapping_config)?;
|
||||||
Ok(out.encode_to_vec().into_boxed_slice())
|
Ok(out.encode_to_vec().into_boxed_slice())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
106
objdiff-core/src/build/mod.rs
Normal file
106
objdiff-core/src/build/mod.rs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
pub mod watcher;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
process::Command,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct BuildStatus {
|
||||||
|
pub success: bool,
|
||||||
|
pub cmdline: String,
|
||||||
|
pub stdout: String,
|
||||||
|
pub stderr: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BuildStatus {
|
||||||
|
fn default() -> Self {
|
||||||
|
BuildStatus {
|
||||||
|
success: true,
|
||||||
|
cmdline: String::new(),
|
||||||
|
stdout: String::new(),
|
||||||
|
stderr: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BuildConfig {
|
||||||
|
pub project_dir: Option<PathBuf>,
|
||||||
|
pub custom_make: Option<String>,
|
||||||
|
pub custom_args: Option<Vec<String>>,
|
||||||
|
#[allow(unused)]
|
||||||
|
pub selected_wsl_distro: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_make(config: &BuildConfig, arg: &Path) -> BuildStatus {
|
||||||
|
let Some(cwd) = &config.project_dir else {
|
||||||
|
return BuildStatus {
|
||||||
|
success: false,
|
||||||
|
stderr: "Missing project dir".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
};
|
||||||
|
let make = config.custom_make.as_deref().unwrap_or("make");
|
||||||
|
let make_args = config.custom_args.as_deref().unwrap_or(&[]);
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
let mut command = {
|
||||||
|
let mut command = Command::new(make);
|
||||||
|
command.current_dir(cwd).args(make_args).arg(arg);
|
||||||
|
command
|
||||||
|
};
|
||||||
|
#[cfg(windows)]
|
||||||
|
let mut command = {
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
|
||||||
|
use path_slash::PathExt;
|
||||||
|
let mut command = if config.selected_wsl_distro.is_some() {
|
||||||
|
Command::new("wsl")
|
||||||
|
} else {
|
||||||
|
Command::new(make)
|
||||||
|
};
|
||||||
|
if let Some(distro) = &config.selected_wsl_distro {
|
||||||
|
// Strip distro root prefix \\wsl.localhost\{distro}
|
||||||
|
let wsl_path_prefix = format!("\\\\wsl.localhost\\{}", distro);
|
||||||
|
let cwd = match cwd.strip_prefix(wsl_path_prefix) {
|
||||||
|
Ok(new_cwd) => format!("/{}", new_cwd.to_slash_lossy().as_ref()),
|
||||||
|
Err(_) => cwd.to_string_lossy().to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
command
|
||||||
|
.arg("--cd")
|
||||||
|
.arg(cwd)
|
||||||
|
.arg("-d")
|
||||||
|
.arg(distro)
|
||||||
|
.arg("--")
|
||||||
|
.arg(make)
|
||||||
|
.args(make_args)
|
||||||
|
.arg(arg.to_slash_lossy().as_ref());
|
||||||
|
} else {
|
||||||
|
command.current_dir(cwd).args(make_args).arg(arg.to_slash_lossy().as_ref());
|
||||||
|
}
|
||||||
|
command.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
|
||||||
|
command
|
||||||
|
};
|
||||||
|
let mut cmdline = shell_escape::escape(command.get_program().to_string_lossy()).into_owned();
|
||||||
|
for arg in command.get_args() {
|
||||||
|
cmdline.push(' ');
|
||||||
|
cmdline.push_str(shell_escape::escape(arg.to_string_lossy()).as_ref());
|
||||||
|
}
|
||||||
|
let output = match command.output() {
|
||||||
|
Ok(output) => output,
|
||||||
|
Err(e) => {
|
||||||
|
return BuildStatus {
|
||||||
|
success: false,
|
||||||
|
cmdline,
|
||||||
|
stdout: Default::default(),
|
||||||
|
stderr: e.to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Try from_utf8 first to avoid copying the buffer if it's valid, then fall back to from_utf8_lossy
|
||||||
|
let stdout = String::from_utf8(output.stdout)
|
||||||
|
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
|
||||||
|
let stderr = String::from_utf8(output.stderr)
|
||||||
|
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
|
||||||
|
BuildStatus { success: output.status.success(), cmdline, stdout, stderr }
|
||||||
|
}
|
||||||
75
objdiff-core/src/build/watcher.rs
Normal file
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)
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
use std::{
|
use std::{
|
||||||
|
collections::BTreeMap,
|
||||||
|
fs,
|
||||||
fs::File,
|
fs::File,
|
||||||
io::Read,
|
io::{BufReader, BufWriter, Read},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -8,70 +10,107 @@ use anyhow::{anyhow, Context, Result};
|
|||||||
use filetime::FileTime;
|
use filetime::FileTime;
|
||||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||||
|
|
||||||
#[inline]
|
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
fn bool_true() -> bool { true }
|
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
|
||||||
|
|
||||||
#[derive(Default, Clone, serde::Deserialize)]
|
|
||||||
pub struct ProjectConfig {
|
pub struct ProjectConfig {
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub min_version: Option<String>,
|
pub min_version: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub custom_make: Option<String>,
|
pub custom_make: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub custom_args: Option<Vec<String>>,
|
pub custom_args: Option<Vec<String>>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub target_dir: Option<PathBuf>,
|
pub target_dir: Option<PathBuf>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub base_dir: Option<PathBuf>,
|
pub base_dir: Option<PathBuf>,
|
||||||
#[serde(default = "bool_true")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub build_base: bool,
|
pub build_base: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub build_target: bool,
|
pub build_target: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub watch_patterns: Option<Vec<Glob>>,
|
pub watch_patterns: Option<Vec<String>>,
|
||||||
#[serde(default, alias = "units")]
|
#[serde(default, alias = "objects", skip_serializing_if = "Option::is_none")]
|
||||||
pub objects: Vec<ProjectObject>,
|
pub units: Option<Vec<ProjectObject>>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub progress_categories: Vec<ProjectProgressCategory>,
|
pub progress_categories: Option<Vec<ProjectProgressCategory>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, serde::Deserialize)]
|
impl ProjectConfig {
|
||||||
|
#[inline]
|
||||||
|
pub fn units(&self) -> &[ProjectObject] { self.units.as_deref().unwrap_or_default() }
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn units_mut(&mut self) -> &mut Vec<ProjectObject> {
|
||||||
|
self.units.get_or_insert_with(Vec::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn progress_categories(&self) -> &[ProjectProgressCategory] {
|
||||||
|
self.progress_categories.as_deref().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn progress_categories_mut(&mut self) -> &mut Vec<ProjectProgressCategory> {
|
||||||
|
self.progress_categories.get_or_insert_with(Vec::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
|
||||||
pub struct ProjectObject {
|
pub struct ProjectObject {
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub path: Option<PathBuf>,
|
pub path: Option<PathBuf>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub target_path: Option<PathBuf>,
|
pub target_path: Option<PathBuf>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub base_path: Option<PathBuf>,
|
pub base_path: Option<PathBuf>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
#[deprecated(note = "Use metadata.reverse_fn_order")]
|
#[deprecated(note = "Use metadata.reverse_fn_order")]
|
||||||
pub reverse_fn_order: Option<bool>,
|
pub reverse_fn_order: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
#[deprecated(note = "Use metadata.complete")]
|
#[deprecated(note = "Use metadata.complete")]
|
||||||
pub complete: Option<bool>,
|
pub complete: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub scratch: Option<ScratchConfig>,
|
pub scratch: Option<ScratchConfig>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub metadata: Option<ProjectObjectMetadata>,
|
pub metadata: Option<ProjectObjectMetadata>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub symbol_mappings: Option<SymbolMappings>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, serde::Deserialize)]
|
#[cfg_attr(feature = "wasm", tsify_next::declare)]
|
||||||
|
pub type SymbolMappings = BTreeMap<String, String>;
|
||||||
|
|
||||||
|
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
|
||||||
pub struct ProjectObjectMetadata {
|
pub struct ProjectObjectMetadata {
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub complete: Option<bool>,
|
pub complete: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub reverse_fn_order: Option<bool>,
|
pub reverse_fn_order: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub source_path: Option<String>,
|
pub source_path: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub progress_categories: Option<Vec<String>>,
|
pub progress_categories: Option<Vec<String>>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub auto_generated: Option<bool>,
|
pub auto_generated: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, serde::Deserialize)]
|
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
|
||||||
pub struct ProjectProgressCategory {
|
pub struct ProjectProgressCategory {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub id: String,
|
pub id: String,
|
||||||
@@ -112,32 +151,39 @@ impl ProjectObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn complete(&self) -> Option<bool> {
|
pub fn complete(&self) -> Option<bool> {
|
||||||
#[allow(deprecated)]
|
#[expect(deprecated)]
|
||||||
self.metadata.as_ref().and_then(|m| m.complete).or(self.complete)
|
self.metadata.as_ref().and_then(|m| m.complete).or(self.complete)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reverse_fn_order(&self) -> Option<bool> {
|
pub fn reverse_fn_order(&self) -> Option<bool> {
|
||||||
#[allow(deprecated)]
|
#[expect(deprecated)]
|
||||||
self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order)
|
self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hidden(&self) -> bool {
|
pub fn hidden(&self) -> bool {
|
||||||
self.metadata.as_ref().and_then(|m| m.auto_generated).unwrap_or(false)
|
self.metadata.as_ref().and_then(|m| m.auto_generated).unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn source_path(&self) -> Option<&String> {
|
||||||
|
self.metadata.as_ref().and_then(|m| m.source_path.as_ref())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
|
||||||
pub struct ScratchConfig {
|
pub struct ScratchConfig {
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub platform: Option<String>,
|
pub platform: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub compiler: Option<String>,
|
pub compiler: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub c_flags: Option<String>,
|
pub c_flags: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub ctx_path: Option<PathBuf>,
|
pub ctx_path: Option<PathBuf>,
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub build_ctx: bool,
|
pub build_ctx: Option<bool>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub preset_id: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"];
|
pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"];
|
||||||
@@ -147,16 +193,20 @@ pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[
|
|||||||
"*.inc", "*.py", "*.yml", "*.txt", "*.json",
|
"*.inc", "*.py", "*.yml", "*.txt", "*.json",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
pub fn default_watch_patterns() -> Vec<Glob> {
|
||||||
|
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Eq, PartialEq)]
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
pub struct ProjectConfigInfo {
|
pub struct ProjectConfigInfo {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
pub timestamp: FileTime,
|
pub timestamp: Option<FileTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
|
pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
|
||||||
for filename in CONFIG_FILENAMES.iter() {
|
for filename in CONFIG_FILENAMES.iter() {
|
||||||
let config_path = dir.join(filename);
|
let config_path = dir.join(filename);
|
||||||
let Ok(mut file) = File::open(&config_path) else {
|
let Ok(file) = File::open(&config_path) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let metadata = file.metadata();
|
let metadata = file.metadata();
|
||||||
@@ -165,9 +215,10 @@ pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectC
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let ts = FileTime::from_last_modification_time(&metadata);
|
let ts = FileTime::from_last_modification_time(&metadata);
|
||||||
|
let mut reader = BufReader::new(file);
|
||||||
let mut result = match filename.contains("json") {
|
let mut result = match filename.contains("json") {
|
||||||
true => read_json_config(&mut file),
|
true => read_json_config(&mut reader),
|
||||||
false => read_yml_config(&mut file),
|
false => read_yml_config(&mut reader),
|
||||||
};
|
};
|
||||||
if let Ok(config) = &result {
|
if let Ok(config) = &result {
|
||||||
// Validate min_version if present
|
// Validate min_version if present
|
||||||
@@ -175,12 +226,41 @@ pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectC
|
|||||||
result = Err(e);
|
result = Err(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Some((result, ProjectConfigInfo { path: config_path, timestamp: ts }));
|
return Some((result, ProjectConfigInfo { path: config_path, timestamp: Some(ts) }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn save_project_config(
|
||||||
|
config: &ProjectConfig,
|
||||||
|
info: &ProjectConfigInfo,
|
||||||
|
) -> Result<ProjectConfigInfo> {
|
||||||
|
if let Some(last_ts) = info.timestamp {
|
||||||
|
// Check if the file has changed since we last read it
|
||||||
|
if let Ok(metadata) = fs::metadata(&info.path) {
|
||||||
|
let ts = FileTime::from_last_modification_time(&metadata);
|
||||||
|
if ts != last_ts {
|
||||||
|
return Err(anyhow!("Config file has changed since last read"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut writer =
|
||||||
|
BufWriter::new(File::create(&info.path).context("Failed to create config file")?);
|
||||||
|
let ext = info.path.extension().and_then(|ext| ext.to_str()).unwrap_or("json");
|
||||||
|
match ext {
|
||||||
|
"json" => serde_json::to_writer_pretty(&mut writer, config).context("Failed to write JSON"),
|
||||||
|
"yml" | "yaml" => {
|
||||||
|
serde_yaml::to_writer(&mut writer, config).context("Failed to write YAML")
|
||||||
|
}
|
||||||
|
_ => Err(anyhow!("Unknown config file extension: {ext}")),
|
||||||
|
}?;
|
||||||
|
let file = writer.into_inner().context("Failed to flush file")?;
|
||||||
|
let metadata = file.metadata().context("Failed to get file metadata")?;
|
||||||
|
let ts = FileTime::from_last_modification_time(&metadata);
|
||||||
|
Ok(ProjectConfigInfo { path: info.path.clone(), timestamp: Some(ts) })
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_min_version(config: &ProjectConfig) -> Result<()> {
|
fn validate_min_version(config: &ProjectConfig) -> Result<()> {
|
||||||
let Some(min_version) = &config.min_version else { return Ok(()) };
|
let Some(min_version) = &config.min_version else { return Ok(()) };
|
||||||
let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))
|
let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))
|
||||||
|
|||||||
@@ -3,13 +3,17 @@ use std::{cmp::max, collections::BTreeMap};
|
|||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use similar::{capture_diff_slices_deadline, Algorithm};
|
use similar::{capture_diff_slices_deadline, Algorithm};
|
||||||
|
|
||||||
|
use super::FunctionRelocDiffs;
|
||||||
use crate::{
|
use crate::{
|
||||||
arch::ProcessCodeResult,
|
arch::ProcessCodeResult,
|
||||||
diff::{
|
diff::{
|
||||||
DiffObjConfig, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjInsDiff, ObjInsDiffKind,
|
DiffObjConfig, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjInsDiff, ObjInsDiffKind,
|
||||||
ObjSymbolDiff,
|
ObjSymbolDiff,
|
||||||
},
|
},
|
||||||
obj::{ObjInfo, ObjInsArg, ObjReloc, ObjSymbol, ObjSymbolFlags, SymbolRef},
|
obj::{
|
||||||
|
ObjInfo, ObjIns, ObjInsArg, ObjReloc, ObjSection, ObjSymbol, ObjSymbolFlags, ObjSymbolKind,
|
||||||
|
SymbolRef,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn process_code_symbol(
|
pub fn process_code_symbol(
|
||||||
@@ -21,14 +25,30 @@ pub fn process_code_symbol(
|
|||||||
let section = section.ok_or_else(|| anyhow!("Code symbol section not found"))?;
|
let section = section.ok_or_else(|| anyhow!("Code symbol section not found"))?;
|
||||||
let code = §ion.data
|
let code = §ion.data
|
||||||
[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
|
[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
|
||||||
obj.arch.process_code(
|
let mut res = obj.arch.process_code(
|
||||||
symbol.address,
|
symbol.address,
|
||||||
code,
|
code,
|
||||||
section.orig_index,
|
section.orig_index,
|
||||||
§ion.relocations,
|
§ion.relocations,
|
||||||
§ion.line_info,
|
§ion.line_info,
|
||||||
config,
|
config,
|
||||||
)
|
)?;
|
||||||
|
|
||||||
|
for inst in res.insts.iter_mut() {
|
||||||
|
if let Some(reloc) = &mut inst.reloc {
|
||||||
|
if reloc.target.size == 0 && reloc.target.name.is_empty() {
|
||||||
|
// Fake target symbol we added as a placeholder. We need to find the real one.
|
||||||
|
if let Some(real_target) =
|
||||||
|
find_symbol_matching_fake_symbol_in_sections(&reloc.target, &obj.sections)
|
||||||
|
{
|
||||||
|
reloc.addend = (reloc.target.address - real_target.address) as i64;
|
||||||
|
reloc.target = real_target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn no_diff_code(out: &ProcessCodeResult, symbol_ref: SymbolRef) -> Result<ObjSymbolDiff> {
|
pub fn no_diff_code(out: &ProcessCodeResult, symbol_ref: SymbolRef) -> Result<ObjSymbolDiff> {
|
||||||
@@ -41,10 +61,12 @@ pub fn no_diff_code(out: &ProcessCodeResult, symbol_ref: SymbolRef) -> Result<Ob
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
resolve_branches(&mut diff);
|
resolve_branches(&mut diff);
|
||||||
Ok(ObjSymbolDiff { symbol_ref, diff_symbol: None, instructions: diff, match_percent: None })
|
Ok(ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: diff, match_percent: None })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn diff_code(
|
pub fn diff_code(
|
||||||
|
left_obj: &ObjInfo,
|
||||||
|
right_obj: &ObjInfo,
|
||||||
left_out: &ProcessCodeResult,
|
left_out: &ProcessCodeResult,
|
||||||
right_out: &ProcessCodeResult,
|
right_out: &ProcessCodeResult,
|
||||||
left_symbol_ref: SymbolRef,
|
left_symbol_ref: SymbolRef,
|
||||||
@@ -60,14 +82,14 @@ pub fn diff_code(
|
|||||||
|
|
||||||
let mut diff_state = InsDiffState::default();
|
let mut diff_state = InsDiffState::default();
|
||||||
for (left, right) in left_diff.iter_mut().zip(right_diff.iter_mut()) {
|
for (left, right) in left_diff.iter_mut().zip(right_diff.iter_mut()) {
|
||||||
let result = compare_ins(config, left, right, &mut diff_state)?;
|
let result = compare_ins(config, left_obj, right_obj, left, right, &mut diff_state)?;
|
||||||
left.kind = result.kind;
|
left.kind = result.kind;
|
||||||
right.kind = result.kind;
|
right.kind = result.kind;
|
||||||
left.arg_diff = result.left_args_diff;
|
left.arg_diff = result.left_args_diff;
|
||||||
right.arg_diff = result.right_args_diff;
|
right.arg_diff = result.right_args_diff;
|
||||||
}
|
}
|
||||||
|
|
||||||
let total = left_out.insts.len();
|
let total = left_out.insts.len().max(right_out.insts.len());
|
||||||
let percent = if diff_state.diff_count >= total {
|
let percent = if diff_state.diff_count >= total {
|
||||||
0.0
|
0.0
|
||||||
} else {
|
} else {
|
||||||
@@ -77,13 +99,13 @@ pub fn diff_code(
|
|||||||
Ok((
|
Ok((
|
||||||
ObjSymbolDiff {
|
ObjSymbolDiff {
|
||||||
symbol_ref: left_symbol_ref,
|
symbol_ref: left_symbol_ref,
|
||||||
diff_symbol: Some(right_symbol_ref),
|
target_symbol: Some(right_symbol_ref),
|
||||||
instructions: left_diff,
|
instructions: left_diff,
|
||||||
match_percent: Some(percent),
|
match_percent: Some(percent),
|
||||||
},
|
},
|
||||||
ObjSymbolDiff {
|
ObjSymbolDiff {
|
||||||
symbol_ref: right_symbol_ref,
|
symbol_ref: right_symbol_ref,
|
||||||
diff_symbol: Some(left_symbol_ref),
|
target_symbol: Some(left_symbol_ref),
|
||||||
instructions: right_diff,
|
instructions: right_diff,
|
||||||
match_percent: Some(percent),
|
match_percent: Some(percent),
|
||||||
},
|
},
|
||||||
@@ -170,73 +192,114 @@ fn resolve_branches(vec: &mut [ObjInsDiff]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn address_eq(left: &ObjSymbol, right: &ObjSymbol) -> bool {
|
fn address_eq(left: &ObjReloc, right: &ObjReloc) -> bool {
|
||||||
left.address as i64 + left.addend == right.address as i64 + right.addend
|
left.target.address as i64 + left.addend == right.target.address as i64 + right.addend
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn section_name_eq(
|
||||||
|
left_obj: &ObjInfo,
|
||||||
|
right_obj: &ObjInfo,
|
||||||
|
left_orig_section_index: usize,
|
||||||
|
right_orig_section_index: usize,
|
||||||
|
) -> bool {
|
||||||
|
let Some(left_section) =
|
||||||
|
left_obj.sections.iter().find(|s| s.orig_index == left_orig_section_index)
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(right_section) =
|
||||||
|
right_obj.sections.iter().find(|s| s.orig_index == right_orig_section_index)
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
left_section.name == right_section.name
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reloc_eq(
|
fn reloc_eq(
|
||||||
config: &DiffObjConfig,
|
config: &DiffObjConfig,
|
||||||
left_reloc: Option<&ObjReloc>,
|
left_obj: &ObjInfo,
|
||||||
right_reloc: Option<&ObjReloc>,
|
right_obj: &ObjInfo,
|
||||||
|
left_ins: Option<&ObjIns>,
|
||||||
|
right_ins: Option<&ObjIns>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let (Some(left), Some(right)) = (left_reloc, right_reloc) else {
|
let (Some(left_ins), Some(right_ins)) = (left_ins, right_ins) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let (Some(left), Some(right)) = (&left_ins.reloc, &right_ins.reloc) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
if left.flags != right.flags {
|
if left.flags != right.flags {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if config.relax_reloc_diffs {
|
if config.function_reloc_diffs == FunctionRelocDiffs::None {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let name_matches = left.target.name == right.target.name;
|
let symbol_name_matches = left.target.name == right.target.name;
|
||||||
match (&left.target_section, &right.target_section) {
|
match (&left.target.orig_section_index, &right.target.orig_section_index) {
|
||||||
(Some(sl), Some(sr)) => {
|
(Some(sl), Some(sr)) => {
|
||||||
// Match if section and name or address match
|
// Match if section and name or address match
|
||||||
sl == sr && (name_matches || address_eq(&left.target, &right.target))
|
section_name_eq(left_obj, right_obj, *sl, *sr)
|
||||||
|
&& (config.function_reloc_diffs == FunctionRelocDiffs::DataValue
|
||||||
|
|| symbol_name_matches
|
||||||
|
|| address_eq(left, right))
|
||||||
|
&& (config.function_reloc_diffs == FunctionRelocDiffs::NameAddress
|
||||||
|
|| left.target.kind != ObjSymbolKind::Object
|
||||||
|
|| left_obj.arch.display_ins_data(left_ins)
|
||||||
|
== left_obj.arch.display_ins_data(right_ins))
|
||||||
}
|
}
|
||||||
(Some(_), None) => false,
|
(Some(_), None) => false,
|
||||||
(None, Some(_)) => {
|
(None, Some(_)) => {
|
||||||
// Match if possibly stripped weak symbol
|
// Match if possibly stripped weak symbol
|
||||||
name_matches && right.target.flags.0.contains(ObjSymbolFlags::Weak)
|
symbol_name_matches && right.target.flags.0.contains(ObjSymbolFlags::Weak)
|
||||||
}
|
}
|
||||||
(None, None) => name_matches,
|
(None, None) => symbol_name_matches,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn arg_eq(
|
fn arg_eq(
|
||||||
config: &DiffObjConfig,
|
config: &DiffObjConfig,
|
||||||
|
left_obj: &ObjInfo,
|
||||||
|
right_obj: &ObjInfo,
|
||||||
left: &ObjInsArg,
|
left: &ObjInsArg,
|
||||||
right: &ObjInsArg,
|
right: &ObjInsArg,
|
||||||
left_diff: &ObjInsDiff,
|
left_diff: &ObjInsDiff,
|
||||||
right_diff: &ObjInsDiff,
|
right_diff: &ObjInsDiff,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
return match left {
|
match left {
|
||||||
ObjInsArg::PlainText(l) => match right {
|
ObjInsArg::PlainText(l) => match right {
|
||||||
ObjInsArg::PlainText(r) => l == r,
|
ObjInsArg::PlainText(r) => l == r,
|
||||||
_ => false,
|
_ => false,
|
||||||
},
|
},
|
||||||
ObjInsArg::Arg(l) => match right {
|
ObjInsArg::Arg(l) => match right {
|
||||||
ObjInsArg::Arg(r) => l == r,
|
ObjInsArg::Arg(r) => l.loose_eq(r),
|
||||||
// If relocations are relaxed, match if left is a constant and right is a reloc
|
// 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
|
// Useful for instances where the target object is created without relocations
|
||||||
ObjInsArg::Reloc => config.relax_reloc_diffs,
|
ObjInsArg::Reloc => config.function_reloc_diffs == FunctionRelocDiffs::None,
|
||||||
_ => false,
|
_ => false,
|
||||||
},
|
},
|
||||||
ObjInsArg::Reloc => {
|
ObjInsArg::Reloc => {
|
||||||
matches!(right, ObjInsArg::Reloc)
|
matches!(right, ObjInsArg::Reloc)
|
||||||
&& reloc_eq(
|
&& reloc_eq(
|
||||||
config,
|
config,
|
||||||
left_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
|
left_obj,
|
||||||
right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
|
right_obj,
|
||||||
|
left_diff.ins.as_ref(),
|
||||||
|
right_diff.ins.as_ref(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ObjInsArg::BranchDest(_) => {
|
ObjInsArg::BranchDest(_) => match right {
|
||||||
// Compare dest instruction idx after diffing
|
// Compare dest instruction idx after diffing
|
||||||
left_diff.branch_to.as_ref().map(|b| b.ins_idx)
|
ObjInsArg::BranchDest(_) => {
|
||||||
== right_diff.branch_to.as_ref().map(|b| b.ins_idx)
|
left_diff.branch_to.as_ref().map(|b| b.ins_idx)
|
||||||
}
|
== right_diff.branch_to.as_ref().map(|b| b.ins_idx)
|
||||||
};
|
}
|
||||||
|
// If relocations are relaxed, match if left is a constant and right is a reloc
|
||||||
|
// Useful for instances where the target object is created without relocations
|
||||||
|
ObjInsArg::Reloc => config.function_reloc_diffs == FunctionRelocDiffs::None,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -257,21 +320,18 @@ struct InsDiffResult {
|
|||||||
|
|
||||||
fn compare_ins(
|
fn compare_ins(
|
||||||
config: &DiffObjConfig,
|
config: &DiffObjConfig,
|
||||||
|
left_obj: &ObjInfo,
|
||||||
|
right_obj: &ObjInfo,
|
||||||
left: &ObjInsDiff,
|
left: &ObjInsDiff,
|
||||||
right: &ObjInsDiff,
|
right: &ObjInsDiff,
|
||||||
state: &mut InsDiffState,
|
state: &mut InsDiffState,
|
||||||
) -> Result<InsDiffResult> {
|
) -> Result<InsDiffResult> {
|
||||||
let mut result = InsDiffResult::default();
|
let mut result = InsDiffResult::default();
|
||||||
if let (Some(left_ins), Some(right_ins)) = (&left.ins, &right.ins) {
|
if let (Some(left_ins), Some(right_ins)) = (&left.ins, &right.ins) {
|
||||||
if left_ins.args.len() != right_ins.args.len()
|
// Count only non-PlainText args
|
||||||
|| left_ins.op != right_ins.op
|
let left_args_count = left_ins.iter_args().count();
|
||||||
// Check if any PlainText segments differ (punctuation and spacing)
|
let right_args_count = right_ins.iter_args().count();
|
||||||
// This indicates a more significant difference than a simple arg mismatch
|
if left_args_count != right_args_count || left_ins.op != right_ins.op {
|
||||||
|| !left_ins.args.iter().zip(&right_ins.args).all(|(a, b)| match (a, b) {
|
|
||||||
(ObjInsArg::PlainText(l), ObjInsArg::PlainText(r)) => l == r,
|
|
||||||
_ => true,
|
|
||||||
})
|
|
||||||
{
|
|
||||||
// Totally different op
|
// Totally different op
|
||||||
result.kind = ObjInsDiffKind::Replace;
|
result.kind = ObjInsDiffKind::Replace;
|
||||||
state.diff_count += 1;
|
state.diff_count += 1;
|
||||||
@@ -282,8 +342,8 @@ fn compare_ins(
|
|||||||
result.kind = ObjInsDiffKind::OpMismatch;
|
result.kind = ObjInsDiffKind::OpMismatch;
|
||||||
state.diff_count += 1;
|
state.diff_count += 1;
|
||||||
}
|
}
|
||||||
for (a, b) in left_ins.args.iter().zip(&right_ins.args) {
|
for (a, b) in left_ins.iter_args().zip(right_ins.iter_args()) {
|
||||||
if arg_eq(config, a, b, left, right) {
|
if arg_eq(config, left_obj, right_obj, a, b, left, right) {
|
||||||
result.left_args_diff.push(None);
|
result.left_args_diff.push(None);
|
||||||
result.right_args_diff.push(None);
|
result.right_args_diff.push(None);
|
||||||
} else {
|
} else {
|
||||||
@@ -294,8 +354,11 @@ fn compare_ins(
|
|||||||
let a_str = match a {
|
let a_str = match a {
|
||||||
ObjInsArg::PlainText(arg) => arg.to_string(),
|
ObjInsArg::PlainText(arg) => arg.to_string(),
|
||||||
ObjInsArg::Arg(arg) => arg.to_string(),
|
ObjInsArg::Arg(arg) => arg.to_string(),
|
||||||
ObjInsArg::Reloc => String::new(),
|
ObjInsArg::Reloc => left_ins
|
||||||
ObjInsArg::BranchDest(arg) => format!("{arg}"),
|
.reloc
|
||||||
|
.as_ref()
|
||||||
|
.map_or_else(|| "<unknown>".to_string(), |r| r.target.name.clone()),
|
||||||
|
ObjInsArg::BranchDest(arg) => arg.to_string(),
|
||||||
};
|
};
|
||||||
let a_diff = if let Some(idx) = state.left_args_idx.get(&a_str) {
|
let a_diff = if let Some(idx) = state.left_args_idx.get(&a_str) {
|
||||||
ObjInsArgDiff { idx: *idx }
|
ObjInsArgDiff { idx: *idx }
|
||||||
@@ -308,8 +371,11 @@ fn compare_ins(
|
|||||||
let b_str = match b {
|
let b_str = match b {
|
||||||
ObjInsArg::PlainText(arg) => arg.to_string(),
|
ObjInsArg::PlainText(arg) => arg.to_string(),
|
||||||
ObjInsArg::Arg(arg) => arg.to_string(),
|
ObjInsArg::Arg(arg) => arg.to_string(),
|
||||||
ObjInsArg::Reloc => String::new(),
|
ObjInsArg::Reloc => right_ins
|
||||||
ObjInsArg::BranchDest(arg) => format!("{arg}"),
|
.reloc
|
||||||
|
.as_ref()
|
||||||
|
.map_or_else(|| "<unknown>".to_string(), |r| r.target.name.clone()),
|
||||||
|
ObjInsArg::BranchDest(arg) => arg.to_string(),
|
||||||
};
|
};
|
||||||
let b_diff = if let Some(idx) = state.right_args_idx.get(&b_str) {
|
let b_diff = if let Some(idx) = state.right_args_idx.get(&b_str) {
|
||||||
ObjInsArgDiff { idx: *idx }
|
ObjInsArgDiff { idx: *idx }
|
||||||
@@ -332,3 +398,16 @@ fn compare_ins(
|
|||||||
}
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
use std::cmp::{max, min, Ordering};
|
use std::{
|
||||||
|
cmp::{max, min, Ordering},
|
||||||
|
ops::Range,
|
||||||
|
};
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use similar::{capture_diff_slices_deadline, get_diff_ratio, Algorithm};
|
use similar::{capture_diff_slices_deadline, get_diff_ratio, Algorithm};
|
||||||
|
|
||||||
|
use super::code::section_name_eq;
|
||||||
use crate::{
|
use crate::{
|
||||||
diff::{ObjDataDiff, ObjDataDiffKind, ObjSectionDiff, ObjSymbolDiff},
|
diff::{ObjDataDiff, ObjDataDiffKind, ObjSectionDiff, ObjSymbolDiff},
|
||||||
obj::{ObjInfo, ObjSection, SymbolRef},
|
obj::{ObjInfo, ObjReloc, ObjSection, ObjSymbolFlags, SymbolRef},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn diff_bss_symbol(
|
pub fn diff_bss_symbol(
|
||||||
@@ -20,13 +24,13 @@ pub fn diff_bss_symbol(
|
|||||||
Ok((
|
Ok((
|
||||||
ObjSymbolDiff {
|
ObjSymbolDiff {
|
||||||
symbol_ref: left_symbol_ref,
|
symbol_ref: left_symbol_ref,
|
||||||
diff_symbol: Some(right_symbol_ref),
|
target_symbol: Some(right_symbol_ref),
|
||||||
instructions: vec![],
|
instructions: vec![],
|
||||||
match_percent: Some(percent),
|
match_percent: Some(percent),
|
||||||
},
|
},
|
||||||
ObjSymbolDiff {
|
ObjSymbolDiff {
|
||||||
symbol_ref: right_symbol_ref,
|
symbol_ref: right_symbol_ref,
|
||||||
diff_symbol: Some(left_symbol_ref),
|
target_symbol: Some(left_symbol_ref),
|
||||||
instructions: vec![],
|
instructions: vec![],
|
||||||
match_percent: Some(percent),
|
match_percent: Some(percent),
|
||||||
},
|
},
|
||||||
@@ -34,11 +38,113 @@ pub fn diff_bss_symbol(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn no_diff_symbol(_obj: &ObjInfo, symbol_ref: SymbolRef) -> ObjSymbolDiff {
|
pub fn no_diff_symbol(_obj: &ObjInfo, symbol_ref: SymbolRef) -> ObjSymbolDiff {
|
||||||
ObjSymbolDiff { symbol_ref, diff_symbol: None, instructions: vec![], match_percent: None }
|
ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: vec![], match_percent: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn address_eq(left: &ObjReloc, right: &ObjReloc) -> bool {
|
||||||
|
if right.target.size == 0 && left.target.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, 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.target.address as i64 + left.addend == right.target.address as i64 + right.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.target.address == right.target.address && left.addend == right.addend
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reloc_eq(left_obj: &ObjInfo, right_obj: &ObjInfo, left: &ObjReloc, right: &ObjReloc) -> bool {
|
||||||
|
if left.flags != right.flags {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let symbol_name_matches = left.target.name == right.target.name;
|
||||||
|
match (&left.target.orig_section_index, &right.target.orig_section_index) {
|
||||||
|
(Some(sl), Some(sr)) => {
|
||||||
|
// Match if section and name+addend or address match
|
||||||
|
section_name_eq(left_obj, right_obj, *sl, *sr)
|
||||||
|
&& ((symbol_name_matches && left.addend == right.addend) || address_eq(left, right))
|
||||||
|
}
|
||||||
|
(Some(_), None) => false,
|
||||||
|
(None, Some(_)) => {
|
||||||
|
// Match if possibly stripped weak symbol
|
||||||
|
(symbol_name_matches && left.addend == right.addend)
|
||||||
|
&& right.target.flags.0.contains(ObjSymbolFlags::Weak)
|
||||||
|
}
|
||||||
|
(None, None) => symbol_name_matches,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compares relocations contained with a certain data range.
|
||||||
|
/// The ObjDataDiffKind for each diff will either be `None`` (if the relocation matches),
|
||||||
|
/// or `Replace` (if a relocation was changed, added, or removed).
|
||||||
|
/// `Insert` and `Delete` are not used when a relocation is added or removed to avoid confusing diffs
|
||||||
|
/// where it looks like the bytes themselves were changed but actually only the relocations changed.
|
||||||
|
fn diff_data_relocs_for_range(
|
||||||
|
left_obj: &ObjInfo,
|
||||||
|
right_obj: &ObjInfo,
|
||||||
|
left: &ObjSection,
|
||||||
|
right: &ObjSection,
|
||||||
|
left_range: Range<usize>,
|
||||||
|
right_range: Range<usize>,
|
||||||
|
) -> Vec<(ObjDataDiffKind, Option<ObjReloc>, Option<ObjReloc>)> {
|
||||||
|
let mut diffs = Vec::new();
|
||||||
|
for left_reloc in left.relocations.iter() {
|
||||||
|
if !left_range.contains(&(left_reloc.address as usize)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let left_offset = left_reloc.address as usize - left_range.start;
|
||||||
|
let Some(right_reloc) = right.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((ObjDataDiffKind::Replace, Some(left_reloc.clone()), None));
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if reloc_eq(left_obj, right_obj, left_reloc, right_reloc) {
|
||||||
|
diffs.push((
|
||||||
|
ObjDataDiffKind::None,
|
||||||
|
Some(left_reloc.clone()),
|
||||||
|
Some(right_reloc.clone()),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
diffs.push((
|
||||||
|
ObjDataDiffKind::Replace,
|
||||||
|
Some(left_reloc.clone()),
|
||||||
|
Some(right_reloc.clone()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for right_reloc in right.relocations.iter() {
|
||||||
|
if !right_range.contains(&(right_reloc.address as usize)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let right_offset = right_reloc.address as usize - right_range.start;
|
||||||
|
let Some(_) = left.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((ObjDataDiffKind::Replace, None, Some(right_reloc.clone())));
|
||||||
|
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.
|
/// Compare the data sections of two object files.
|
||||||
pub fn diff_data_section(
|
pub fn diff_data_section(
|
||||||
|
left_obj: &ObjInfo,
|
||||||
|
right_obj: &ObjInfo,
|
||||||
left: &ObjSection,
|
left: &ObjSection,
|
||||||
right: &ObjSection,
|
right: &ObjSection,
|
||||||
left_section_diff: &ObjSectionDiff,
|
left_section_diff: &ObjSectionDiff,
|
||||||
@@ -70,6 +176,94 @@ pub fn diff_data_section(
|
|||||||
ObjDataDiffKind::Replace
|
ObjDataDiffKind::Replace
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if kind == ObjDataDiffKind::None {
|
||||||
|
let mut found_any_relocs = false;
|
||||||
|
let mut left_curr_addr = left_range.start;
|
||||||
|
let mut right_curr_addr = right_range.start;
|
||||||
|
for (diff_kind, left_reloc, right_reloc) in diff_data_relocs_for_range(
|
||||||
|
left_obj,
|
||||||
|
right_obj,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
left_range.clone(),
|
||||||
|
right_range.clone(),
|
||||||
|
) {
|
||||||
|
found_any_relocs = true;
|
||||||
|
|
||||||
|
if let Some(left_reloc) = left_reloc {
|
||||||
|
let left_reloc_addr = left_reloc.address as usize;
|
||||||
|
if left_reloc_addr > left_curr_addr {
|
||||||
|
let len = left_reloc_addr - left_curr_addr;
|
||||||
|
let left_data = &left.data[left_curr_addr..left_reloc_addr];
|
||||||
|
left_diff.push(ObjDataDiff {
|
||||||
|
data: left_data[..min(len, left_data.len())].to_vec(),
|
||||||
|
kind: ObjDataDiffKind::None,
|
||||||
|
len,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let reloc_diff_len = left_obj.arch.get_reloc_byte_size(left_reloc.flags);
|
||||||
|
let left_data = &left.data[left_reloc_addr..left_reloc_addr + reloc_diff_len];
|
||||||
|
left_diff.push(ObjDataDiff {
|
||||||
|
data: left_data[..min(reloc_diff_len, left_data.len())].to_vec(),
|
||||||
|
kind: diff_kind,
|
||||||
|
len: reloc_diff_len,
|
||||||
|
reloc: Some(left_reloc.clone()),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
left_curr_addr = left_reloc_addr + reloc_diff_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(right_reloc) = right_reloc {
|
||||||
|
let right_reloc_addr = right_reloc.address as usize;
|
||||||
|
if right_reloc_addr > right_curr_addr {
|
||||||
|
let len = right_reloc_addr - right_curr_addr;
|
||||||
|
let right_data = &right.data[right_curr_addr..right_reloc_addr];
|
||||||
|
right_diff.push(ObjDataDiff {
|
||||||
|
data: right_data[..min(len, right_data.len())].to_vec(),
|
||||||
|
kind: ObjDataDiffKind::None,
|
||||||
|
len,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let reloc_diff_len = right_obj.arch.get_reloc_byte_size(right_reloc.flags);
|
||||||
|
let right_data =
|
||||||
|
&right.data[right_reloc_addr..right_reloc_addr + reloc_diff_len];
|
||||||
|
right_diff.push(ObjDataDiff {
|
||||||
|
data: right_data[..min(reloc_diff_len, right_data.len())].to_vec(),
|
||||||
|
kind: diff_kind,
|
||||||
|
len: reloc_diff_len,
|
||||||
|
reloc: Some(right_reloc.clone()),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
right_curr_addr = right_reloc_addr + reloc_diff_len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if found_any_relocs {
|
||||||
|
if left_curr_addr < left_range.end - 1 {
|
||||||
|
let len = left_range.end - left_curr_addr;
|
||||||
|
let left_data = &left.data[left_curr_addr..left_range.end];
|
||||||
|
left_diff.push(ObjDataDiff {
|
||||||
|
data: left_data[..min(len, left_data.len())].to_vec(),
|
||||||
|
kind: ObjDataDiffKind::None,
|
||||||
|
len,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if right_curr_addr < right_range.end - 1 {
|
||||||
|
let len = right_range.end - right_curr_addr;
|
||||||
|
let right_data = &right.data[right_curr_addr..right_range.end];
|
||||||
|
right_diff.push(ObjDataDiff {
|
||||||
|
data: right_data[..min(len, right_data.len())].to_vec(),
|
||||||
|
kind: ObjDataDiffKind::None,
|
||||||
|
len,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
let left_data = &left.data[left_range];
|
let left_data = &left.data[left_range];
|
||||||
let right_data = &right.data[right_range];
|
let right_data = &right.data[right_range];
|
||||||
left_diff.push(ObjDataDiff {
|
left_diff.push(ObjDataDiff {
|
||||||
@@ -123,14 +317,19 @@ pub fn diff_data_section(
|
|||||||
|
|
||||||
let (mut left_section_diff, mut right_section_diff) =
|
let (mut left_section_diff, mut right_section_diff) =
|
||||||
diff_generic_section(left, right, left_section_diff, right_section_diff)?;
|
diff_generic_section(left, right, left_section_diff, right_section_diff)?;
|
||||||
|
let all_left_relocs_match =
|
||||||
|
left_diff.iter().all(|d| d.kind == ObjDataDiffKind::None || d.reloc.is_none());
|
||||||
left_section_diff.data_diff = left_diff;
|
left_section_diff.data_diff = left_diff;
|
||||||
right_section_diff.data_diff = right_diff;
|
right_section_diff.data_diff = right_diff;
|
||||||
// Use the highest match percent between two options:
|
if all_left_relocs_match {
|
||||||
// - Left symbols matching right symbols by name
|
// Use the highest match percent between two options:
|
||||||
// - Diff of the data itself
|
// - Left symbols matching right symbols by name
|
||||||
if left_section_diff.match_percent.unwrap_or(-1.0) < match_percent {
|
// - Diff of the data itself
|
||||||
left_section_diff.match_percent = Some(match_percent);
|
// We only do this when all relocations on the left side match.
|
||||||
right_section_diff.match_percent = Some(match_percent);
|
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))
|
Ok((left_section_diff, right_section_diff))
|
||||||
}
|
}
|
||||||
@@ -147,24 +346,65 @@ pub fn diff_data_symbol(
|
|||||||
let left_section = left_section.ok_or_else(|| anyhow!("Data symbol section not found"))?;
|
let left_section = left_section.ok_or_else(|| anyhow!("Data symbol section not found"))?;
|
||||||
let right_section = right_section.ok_or_else(|| anyhow!("Data symbol section not found"))?;
|
let right_section = right_section.ok_or_else(|| anyhow!("Data symbol section not found"))?;
|
||||||
|
|
||||||
let left_data = &left_section.data[left_symbol.section_address as usize
|
let left_range = left_symbol.section_address as usize
|
||||||
..(left_symbol.section_address + left_symbol.size) as usize];
|
..(left_symbol.section_address + left_symbol.size) as usize;
|
||||||
let right_data = &right_section.data[right_symbol.section_address as usize
|
let right_range = right_symbol.section_address as usize
|
||||||
..(right_symbol.section_address + right_symbol.size) as usize];
|
..(right_symbol.section_address + right_symbol.size) 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,
|
||||||
|
right_section,
|
||||||
|
left_range,
|
||||||
|
right_range,
|
||||||
|
);
|
||||||
|
|
||||||
let ops = capture_diff_slices_deadline(Algorithm::Patience, left_data, right_data, None);
|
let ops = capture_diff_slices_deadline(Algorithm::Patience, left_data, right_data, None);
|
||||||
let match_percent = get_diff_ratio(&ops, left_data.len(), right_data.len()) * 100.0;
|
let 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.get_reloc_byte_size(right_reloc.flags),
|
||||||
|
(Some(left_reloc), _) => left_obj.arch.get_reloc_byte_size(left_reloc.flags),
|
||||||
|
};
|
||||||
|
total_reloc_bytes += reloc_diff_len;
|
||||||
|
if diff_kind == ObjDataDiffKind::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((
|
Ok((
|
||||||
ObjSymbolDiff {
|
ObjSymbolDiff {
|
||||||
symbol_ref: left_symbol_ref,
|
symbol_ref: left_symbol_ref,
|
||||||
diff_symbol: Some(right_symbol_ref),
|
target_symbol: Some(right_symbol_ref),
|
||||||
instructions: vec![],
|
instructions: vec![],
|
||||||
match_percent: Some(match_percent),
|
match_percent: Some(match_percent),
|
||||||
},
|
},
|
||||||
ObjSymbolDiff {
|
ObjSymbolDiff {
|
||||||
symbol_ref: right_symbol_ref,
|
symbol_ref: right_symbol_ref,
|
||||||
diff_symbol: Some(left_symbol_ref),
|
target_symbol: Some(left_symbol_ref),
|
||||||
instructions: vec![],
|
instructions: vec![],
|
||||||
match_percent: Some(match_percent),
|
match_percent: Some(match_percent),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,14 +22,14 @@ pub enum DiffText<'a> {
|
|||||||
/// Branch destination
|
/// Branch destination
|
||||||
BranchDest(u64, Option<&'a ObjInsArgDiff>),
|
BranchDest(u64, Option<&'a ObjInsArgDiff>),
|
||||||
/// Symbol name
|
/// Symbol name
|
||||||
Symbol(&'a ObjSymbol),
|
Symbol(&'a ObjSymbol, Option<&'a ObjInsArgDiff>),
|
||||||
/// Number of spaces
|
/// Number of spaces
|
||||||
Spacing(usize),
|
Spacing(usize),
|
||||||
/// End of line
|
/// End of line
|
||||||
Eol,
|
Eol,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
pub enum HighlightKind {
|
pub enum HighlightKind {
|
||||||
#[default]
|
#[default]
|
||||||
None,
|
None,
|
||||||
@@ -58,20 +58,23 @@ pub fn display_diff<E>(
|
|||||||
cb(DiffText::Spacing(4))?;
|
cb(DiffText::Spacing(4))?;
|
||||||
}
|
}
|
||||||
cb(DiffText::Opcode(&ins.mnemonic, ins.op))?;
|
cb(DiffText::Opcode(&ins.mnemonic, ins.op))?;
|
||||||
|
let mut arg_diff_idx = 0; // non-PlainText index
|
||||||
for (i, arg) in ins.args.iter().enumerate() {
|
for (i, arg) in ins.args.iter().enumerate() {
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
cb(DiffText::Spacing(1))?;
|
cb(DiffText::Spacing(1))?;
|
||||||
}
|
}
|
||||||
let diff = ins_diff.arg_diff.get(i).and_then(|o| o.as_ref());
|
let diff = ins_diff.arg_diff.get(arg_diff_idx).and_then(|o| o.as_ref());
|
||||||
match arg {
|
match arg {
|
||||||
ObjInsArg::PlainText(s) => {
|
ObjInsArg::PlainText(s) => {
|
||||||
cb(DiffText::Basic(s))?;
|
cb(DiffText::Basic(s))?;
|
||||||
}
|
}
|
||||||
ObjInsArg::Arg(v) => {
|
ObjInsArg::Arg(v) => {
|
||||||
cb(DiffText::Argument(v, diff))?;
|
cb(DiffText::Argument(v, diff))?;
|
||||||
|
arg_diff_idx += 1;
|
||||||
}
|
}
|
||||||
ObjInsArg::Reloc => {
|
ObjInsArg::Reloc => {
|
||||||
display_reloc_name(ins.reloc.as_ref().unwrap(), &mut cb)?;
|
display_reloc_name(ins.reloc.as_ref().unwrap(), &mut cb, diff)?;
|
||||||
|
arg_diff_idx += 1;
|
||||||
}
|
}
|
||||||
ObjInsArg::BranchDest(dest) => {
|
ObjInsArg::BranchDest(dest) => {
|
||||||
if let Some(dest) = dest.checked_sub(base_addr) {
|
if let Some(dest) = dest.checked_sub(base_addr) {
|
||||||
@@ -79,6 +82,7 @@ pub fn display_diff<E>(
|
|||||||
} else {
|
} else {
|
||||||
cb(DiffText::Basic("<unknown>"))?;
|
cb(DiffText::Basic("<unknown>"))?;
|
||||||
}
|
}
|
||||||
|
arg_diff_idx += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,11 +96,12 @@ pub fn display_diff<E>(
|
|||||||
fn display_reloc_name<E>(
|
fn display_reloc_name<E>(
|
||||||
reloc: &ObjReloc,
|
reloc: &ObjReloc,
|
||||||
mut cb: impl FnMut(DiffText) -> Result<(), E>,
|
mut cb: impl FnMut(DiffText) -> Result<(), E>,
|
||||||
|
diff: Option<&ObjInsArgDiff>,
|
||||||
) -> Result<(), E> {
|
) -> Result<(), E> {
|
||||||
cb(DiffText::Symbol(&reloc.target))?;
|
cb(DiffText::Symbol(&reloc.target, diff))?;
|
||||||
match reloc.target.addend.cmp(&0i64) {
|
match reloc.addend.cmp(&0i64) {
|
||||||
Ordering::Greater => cb(DiffText::Basic(&format!("+{:#x}", reloc.target.addend))),
|
Ordering::Greater => cb(DiffText::Basic(&format!("+{:#x}", reloc.addend))),
|
||||||
Ordering::Less => cb(DiffText::Basic(&format!("-{:#x}", -reloc.target.addend))),
|
Ordering::Less => cb(DiffText::Basic(&format!("-{:#x}", -reloc.addend))),
|
||||||
_ => Ok(()),
|
_ => Ok(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,7 +111,7 @@ impl PartialEq<DiffText<'_>> for HighlightKind {
|
|||||||
match (self, other) {
|
match (self, other) {
|
||||||
(HighlightKind::Opcode(a), DiffText::Opcode(_, b)) => a == b,
|
(HighlightKind::Opcode(a), DiffText::Opcode(_, b)) => a == b,
|
||||||
(HighlightKind::Arg(a), DiffText::Argument(b, _)) => a.loose_eq(b),
|
(HighlightKind::Arg(a), DiffText::Argument(b, _)) => a.loose_eq(b),
|
||||||
(HighlightKind::Symbol(a), DiffText::Symbol(b)) => a == &b.name,
|
(HighlightKind::Symbol(a), DiffText::Symbol(b, _)) => a == &b.name,
|
||||||
(HighlightKind::Address(a), DiffText::Address(b) | DiffText::BranchDest(b, _)) => {
|
(HighlightKind::Address(a), DiffText::Address(b) | DiffText::BranchDest(b, _)) => {
|
||||||
a == b
|
a == b
|
||||||
}
|
}
|
||||||
@@ -124,7 +129,7 @@ impl From<DiffText<'_>> for HighlightKind {
|
|||||||
match value {
|
match value {
|
||||||
DiffText::Opcode(_, op) => HighlightKind::Opcode(op),
|
DiffText::Opcode(_, op) => HighlightKind::Opcode(op),
|
||||||
DiffText::Argument(arg, _) => HighlightKind::Arg(arg.clone()),
|
DiffText::Argument(arg, _) => HighlightKind::Arg(arg.clone()),
|
||||||
DiffText::Symbol(sym) => HighlightKind::Symbol(sym.name.to_string()),
|
DiffText::Symbol(sym, _) => HighlightKind::Symbol(sym.name.to_string()),
|
||||||
DiffText::Address(addr) | DiffText::BranchDest(addr, _) => HighlightKind::Address(addr),
|
DiffText::Address(addr) | DiffText::BranchDest(addr, _) => HighlightKind::Address(addr),
|
||||||
_ => HighlightKind::None,
|
_ => HighlightKind::None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::collections::HashSet;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
config::SymbolMappings,
|
||||||
diff::{
|
diff::{
|
||||||
code::{diff_code, no_diff_code, process_code_symbol},
|
code::{diff_code, no_diff_code, process_code_symbol},
|
||||||
data::{
|
data::{
|
||||||
@@ -10,191 +11,16 @@ use crate::{
|
|||||||
diff_generic_section, no_diff_symbol,
|
diff_generic_section, no_diff_symbol,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
obj::{ObjInfo, ObjIns, ObjSection, ObjSectionKind, ObjSymbol, SymbolRef},
|
obj::{
|
||||||
|
ObjInfo, ObjIns, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, SymbolRef, SECTION_COMMON,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod code;
|
pub mod code;
|
||||||
pub mod data;
|
pub mod data;
|
||||||
pub mod display;
|
pub mod display;
|
||||||
|
|
||||||
#[derive(
|
include!(concat!(env!("OUT_DIR"), "/config.gen.rs"));
|
||||||
Debug,
|
|
||||||
Copy,
|
|
||||||
Clone,
|
|
||||||
Default,
|
|
||||||
Eq,
|
|
||||||
PartialEq,
|
|
||||||
serde::Deserialize,
|
|
||||||
serde::Serialize,
|
|
||||||
strum::VariantArray,
|
|
||||||
strum::EnumMessage,
|
|
||||||
tsify_next::Tsify,
|
|
||||||
)]
|
|
||||||
pub enum X86Formatter {
|
|
||||||
#[default]
|
|
||||||
#[strum(message = "Intel (default)")]
|
|
||||||
Intel,
|
|
||||||
#[strum(message = "AT&T")]
|
|
||||||
Gas,
|
|
||||||
#[strum(message = "NASM")]
|
|
||||||
Nasm,
|
|
||||||
#[strum(message = "MASM")]
|
|
||||||
Masm,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Debug,
|
|
||||||
Copy,
|
|
||||||
Clone,
|
|
||||||
Default,
|
|
||||||
Eq,
|
|
||||||
PartialEq,
|
|
||||||
serde::Deserialize,
|
|
||||||
serde::Serialize,
|
|
||||||
strum::VariantArray,
|
|
||||||
strum::EnumMessage,
|
|
||||||
tsify_next::Tsify,
|
|
||||||
)]
|
|
||||||
pub enum MipsAbi {
|
|
||||||
#[default]
|
|
||||||
#[strum(message = "Auto (default)")]
|
|
||||||
Auto,
|
|
||||||
#[strum(message = "O32")]
|
|
||||||
O32,
|
|
||||||
#[strum(message = "N32")]
|
|
||||||
N32,
|
|
||||||
#[strum(message = "N64")]
|
|
||||||
N64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Debug,
|
|
||||||
Copy,
|
|
||||||
Clone,
|
|
||||||
Default,
|
|
||||||
Eq,
|
|
||||||
PartialEq,
|
|
||||||
serde::Deserialize,
|
|
||||||
serde::Serialize,
|
|
||||||
strum::VariantArray,
|
|
||||||
strum::EnumMessage,
|
|
||||||
tsify_next::Tsify,
|
|
||||||
)]
|
|
||||||
pub enum MipsInstrCategory {
|
|
||||||
#[default]
|
|
||||||
#[strum(message = "Auto (default)")]
|
|
||||||
Auto,
|
|
||||||
#[strum(message = "CPU")]
|
|
||||||
Cpu,
|
|
||||||
#[strum(message = "RSP (N64)")]
|
|
||||||
Rsp,
|
|
||||||
#[strum(message = "R3000 GTE (PS1)")]
|
|
||||||
R3000Gte,
|
|
||||||
#[strum(message = "R4000 ALLEGREX (PSP)")]
|
|
||||||
R4000Allegrex,
|
|
||||||
#[strum(message = "R5900 EE (PS2)")]
|
|
||||||
R5900,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Debug,
|
|
||||||
Copy,
|
|
||||||
Clone,
|
|
||||||
Default,
|
|
||||||
Eq,
|
|
||||||
PartialEq,
|
|
||||||
serde::Deserialize,
|
|
||||||
serde::Serialize,
|
|
||||||
strum::VariantArray,
|
|
||||||
strum::EnumMessage,
|
|
||||||
tsify_next::Tsify,
|
|
||||||
)]
|
|
||||||
pub enum ArmArchVersion {
|
|
||||||
#[default]
|
|
||||||
#[strum(message = "Auto (default)")]
|
|
||||||
Auto,
|
|
||||||
#[strum(message = "ARMv4T (GBA)")]
|
|
||||||
V4T,
|
|
||||||
#[strum(message = "ARMv5TE (DS)")]
|
|
||||||
V5TE,
|
|
||||||
#[strum(message = "ARMv6K (3DS)")]
|
|
||||||
V6K,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Debug,
|
|
||||||
Copy,
|
|
||||||
Clone,
|
|
||||||
Default,
|
|
||||||
Eq,
|
|
||||||
PartialEq,
|
|
||||||
serde::Deserialize,
|
|
||||||
serde::Serialize,
|
|
||||||
strum::VariantArray,
|
|
||||||
strum::EnumMessage,
|
|
||||||
tsify_next::Tsify,
|
|
||||||
)]
|
|
||||||
pub enum ArmR9Usage {
|
|
||||||
#[default]
|
|
||||||
#[strum(
|
|
||||||
message = "R9 or V6 (default)",
|
|
||||||
detailed_message = "Use R9 as a general-purpose register."
|
|
||||||
)]
|
|
||||||
GeneralPurpose,
|
|
||||||
#[strum(
|
|
||||||
message = "SB (static base)",
|
|
||||||
detailed_message = "Used for position-independent data (PID)."
|
|
||||||
)]
|
|
||||||
Sb,
|
|
||||||
#[strum(message = "TR (TLS register)", detailed_message = "Used for thread-local storage.")]
|
|
||||||
Tr,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
const fn default_true() -> bool { true }
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize, tsify_next::Tsify)]
|
|
||||||
#[tsify(from_wasm_abi)]
|
|
||||||
#[serde(default)]
|
|
||||||
pub struct DiffObjConfig {
|
|
||||||
pub relax_reloc_diffs: bool,
|
|
||||||
#[serde(default = "default_true")]
|
|
||||||
pub space_between_args: bool,
|
|
||||||
pub combine_data_sections: bool,
|
|
||||||
// x86
|
|
||||||
pub x86_formatter: X86Formatter,
|
|
||||||
// MIPS
|
|
||||||
pub mips_abi: MipsAbi,
|
|
||||||
pub mips_instr_category: MipsInstrCategory,
|
|
||||||
// ARM
|
|
||||||
pub arm_arch_version: ArmArchVersion,
|
|
||||||
pub arm_unified_syntax: bool,
|
|
||||||
pub arm_av_registers: bool,
|
|
||||||
pub arm_r9_usage: ArmR9Usage,
|
|
||||||
pub arm_sl_usage: bool,
|
|
||||||
pub arm_fp_usage: bool,
|
|
||||||
pub arm_ip_usage: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for DiffObjConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
relax_reloc_diffs: false,
|
|
||||||
space_between_args: true,
|
|
||||||
combine_data_sections: false,
|
|
||||||
x86_formatter: Default::default(),
|
|
||||||
mips_abi: Default::default(),
|
|
||||||
mips_instr_category: Default::default(),
|
|
||||||
arm_arch_version: Default::default(),
|
|
||||||
arm_unified_syntax: true,
|
|
||||||
arm_av_registers: false,
|
|
||||||
arm_r9_usage: Default::default(),
|
|
||||||
arm_sl_usage: false,
|
|
||||||
arm_fp_usage: false,
|
|
||||||
arm_ip_usage: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DiffObjConfig {
|
impl DiffObjConfig {
|
||||||
pub fn separator(&self) -> &'static str {
|
pub fn separator(&self) -> &'static str {
|
||||||
@@ -223,8 +49,10 @@ impl ObjSectionDiff {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct ObjSymbolDiff {
|
pub struct ObjSymbolDiff {
|
||||||
|
/// The symbol ref this object
|
||||||
pub symbol_ref: SymbolRef,
|
pub symbol_ref: SymbolRef,
|
||||||
pub diff_symbol: Option<SymbolRef>,
|
/// The symbol ref in the _other_ object that this symbol was diffed against
|
||||||
|
pub target_symbol: Option<SymbolRef>,
|
||||||
pub instructions: Vec<ObjInsDiff>,
|
pub instructions: Vec<ObjInsDiff>,
|
||||||
pub match_percent: Option<f32>,
|
pub match_percent: Option<f32>,
|
||||||
}
|
}
|
||||||
@@ -238,7 +66,7 @@ pub struct ObjInsDiff {
|
|||||||
pub branch_from: Option<ObjInsBranchFrom>,
|
pub branch_from: Option<ObjInsBranchFrom>,
|
||||||
/// Branches to instruction
|
/// Branches to instruction
|
||||||
pub branch_to: Option<ObjInsBranchTo>,
|
pub branch_to: Option<ObjInsBranchTo>,
|
||||||
/// Arg diffs
|
/// Arg diffs (only contains non-PlainText args)
|
||||||
pub arg_diff: Vec<Option<ObjInsArgDiff>>,
|
pub arg_diff: Vec<Option<ObjInsArgDiff>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,6 +87,7 @@ pub struct ObjDataDiff {
|
|||||||
pub kind: ObjDataDiffKind,
|
pub kind: ObjDataDiffKind,
|
||||||
pub len: usize,
|
pub len: usize,
|
||||||
pub symbol: String,
|
pub symbol: String,
|
||||||
|
pub reloc: Option<ObjReloc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
|
||||||
@@ -294,8 +123,13 @@ pub struct ObjInsBranchTo {
|
|||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ObjDiff {
|
pub struct ObjDiff {
|
||||||
|
/// A list of all section diffs in the object.
|
||||||
pub sections: Vec<ObjSectionDiff>,
|
pub sections: Vec<ObjSectionDiff>,
|
||||||
|
/// Common BSS symbols don't live in a section, so they're stored separately.
|
||||||
pub common: Vec<ObjSymbolDiff>,
|
pub common: Vec<ObjSymbolDiff>,
|
||||||
|
/// If `selecting_left` or `selecting_right` is set, this is the list of symbols
|
||||||
|
/// that are being mapped to the other object.
|
||||||
|
pub mapping_symbols: Vec<ObjSymbolDiff>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ObjDiff {
|
impl ObjDiff {
|
||||||
@@ -303,13 +137,14 @@ impl ObjDiff {
|
|||||||
let mut result = Self {
|
let mut result = Self {
|
||||||
sections: Vec::with_capacity(obj.sections.len()),
|
sections: Vec::with_capacity(obj.sections.len()),
|
||||||
common: Vec::with_capacity(obj.common.len()),
|
common: Vec::with_capacity(obj.common.len()),
|
||||||
|
mapping_symbols: vec![],
|
||||||
};
|
};
|
||||||
for (section_idx, section) in obj.sections.iter().enumerate() {
|
for (section_idx, section) in obj.sections.iter().enumerate() {
|
||||||
let mut symbols = Vec::with_capacity(section.symbols.len());
|
let mut symbols = Vec::with_capacity(section.symbols.len());
|
||||||
for (symbol_idx, _) in section.symbols.iter().enumerate() {
|
for (symbol_idx, _) in section.symbols.iter().enumerate() {
|
||||||
symbols.push(ObjSymbolDiff {
|
symbols.push(ObjSymbolDiff {
|
||||||
symbol_ref: SymbolRef { section_idx, symbol_idx },
|
symbol_ref: SymbolRef { section_idx, symbol_idx },
|
||||||
diff_symbol: None,
|
target_symbol: None,
|
||||||
instructions: vec![],
|
instructions: vec![],
|
||||||
match_percent: None,
|
match_percent: None,
|
||||||
});
|
});
|
||||||
@@ -321,14 +156,15 @@ impl ObjDiff {
|
|||||||
kind: ObjDataDiffKind::None,
|
kind: ObjDataDiffKind::None,
|
||||||
len: section.data.len(),
|
len: section.data.len(),
|
||||||
symbol: section.name.clone(),
|
symbol: section.name.clone(),
|
||||||
|
..Default::default()
|
||||||
}],
|
}],
|
||||||
match_percent: None,
|
match_percent: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (symbol_idx, _) in obj.common.iter().enumerate() {
|
for (symbol_idx, _) in obj.common.iter().enumerate() {
|
||||||
result.common.push(ObjSymbolDiff {
|
result.common.push(ObjSymbolDiff {
|
||||||
symbol_ref: SymbolRef { section_idx: obj.sections.len(), symbol_idx },
|
symbol_ref: SymbolRef { section_idx: SECTION_COMMON, symbol_idx },
|
||||||
diff_symbol: None,
|
target_symbol: None,
|
||||||
instructions: vec![],
|
instructions: vec![],
|
||||||
match_percent: None,
|
match_percent: None,
|
||||||
});
|
});
|
||||||
@@ -348,7 +184,7 @@ impl ObjDiff {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn symbol_diff(&self, symbol_ref: SymbolRef) -> &ObjSymbolDiff {
|
pub fn symbol_diff(&self, symbol_ref: SymbolRef) -> &ObjSymbolDiff {
|
||||||
if symbol_ref.section_idx == self.sections.len() {
|
if symbol_ref.section_idx == SECTION_COMMON {
|
||||||
&self.common[symbol_ref.symbol_idx]
|
&self.common[symbol_ref.symbol_idx]
|
||||||
} else {
|
} else {
|
||||||
&self.section_diff(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx]
|
&self.section_diff(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx]
|
||||||
@@ -357,7 +193,7 @@ impl ObjDiff {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn symbol_diff_mut(&mut self, symbol_ref: SymbolRef) -> &mut ObjSymbolDiff {
|
pub fn symbol_diff_mut(&mut self, symbol_ref: SymbolRef) -> &mut ObjSymbolDiff {
|
||||||
if symbol_ref.section_idx == self.sections.len() {
|
if symbol_ref.section_idx == SECTION_COMMON {
|
||||||
&mut self.common[symbol_ref.symbol_idx]
|
&mut self.common[symbol_ref.symbol_idx]
|
||||||
} else {
|
} else {
|
||||||
&mut self.section_diff_mut(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx]
|
&mut self.section_diff_mut(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx]
|
||||||
@@ -373,12 +209,13 @@ pub struct DiffObjsResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn diff_objs(
|
pub fn diff_objs(
|
||||||
config: &DiffObjConfig,
|
diff_config: &DiffObjConfig,
|
||||||
|
mapping_config: &MappingConfig,
|
||||||
left: Option<&ObjInfo>,
|
left: Option<&ObjInfo>,
|
||||||
right: Option<&ObjInfo>,
|
right: Option<&ObjInfo>,
|
||||||
prev: Option<&ObjInfo>,
|
prev: Option<&ObjInfo>,
|
||||||
) -> Result<DiffObjsResult> {
|
) -> Result<DiffObjsResult> {
|
||||||
let symbol_matches = matching_symbols(left, right, prev)?;
|
let symbol_matches = matching_symbols(left, right, prev, mapping_config)?;
|
||||||
let section_matches = matching_sections(left, right)?;
|
let section_matches = matching_sections(left, right)?;
|
||||||
let mut left = left.map(|p| (p, ObjDiff::new_from_obj(p)));
|
let mut left = left.map(|p| (p, ObjDiff::new_from_obj(p)));
|
||||||
let mut right = right.map(|p| (p, ObjDiff::new_from_obj(p)));
|
let mut right = right.map(|p| (p, ObjDiff::new_from_obj(p)));
|
||||||
@@ -396,27 +233,34 @@ pub fn diff_objs(
|
|||||||
let (right_obj, right_out) = right.as_mut().unwrap();
|
let (right_obj, right_out) = right.as_mut().unwrap();
|
||||||
match section_kind {
|
match section_kind {
|
||||||
ObjSectionKind::Code => {
|
ObjSectionKind::Code => {
|
||||||
let left_code = process_code_symbol(left_obj, left_symbol_ref, config)?;
|
let left_code =
|
||||||
let right_code = process_code_symbol(right_obj, right_symbol_ref, config)?;
|
process_code_symbol(left_obj, left_symbol_ref, diff_config)?;
|
||||||
|
let right_code =
|
||||||
|
process_code_symbol(right_obj, right_symbol_ref, diff_config)?;
|
||||||
let (left_diff, right_diff) = diff_code(
|
let (left_diff, right_diff) = diff_code(
|
||||||
|
left_obj,
|
||||||
|
right_obj,
|
||||||
&left_code,
|
&left_code,
|
||||||
&right_code,
|
&right_code,
|
||||||
left_symbol_ref,
|
left_symbol_ref,
|
||||||
right_symbol_ref,
|
right_symbol_ref,
|
||||||
config,
|
diff_config,
|
||||||
)?;
|
)?;
|
||||||
*left_out.symbol_diff_mut(left_symbol_ref) = left_diff;
|
*left_out.symbol_diff_mut(left_symbol_ref) = left_diff;
|
||||||
*right_out.symbol_diff_mut(right_symbol_ref) = right_diff;
|
*right_out.symbol_diff_mut(right_symbol_ref) = right_diff;
|
||||||
|
|
||||||
if let Some(prev_symbol_ref) = prev_symbol_ref {
|
if let Some(prev_symbol_ref) = prev_symbol_ref {
|
||||||
let (prev_obj, prev_out) = prev.as_mut().unwrap();
|
let (prev_obj, prev_out) = prev.as_mut().unwrap();
|
||||||
let prev_code = process_code_symbol(prev_obj, prev_symbol_ref, config)?;
|
let prev_code =
|
||||||
|
process_code_symbol(prev_obj, prev_symbol_ref, diff_config)?;
|
||||||
let (_, prev_diff) = diff_code(
|
let (_, prev_diff) = diff_code(
|
||||||
|
left_obj,
|
||||||
|
right_obj,
|
||||||
&right_code,
|
&right_code,
|
||||||
&prev_code,
|
&prev_code,
|
||||||
right_symbol_ref,
|
right_symbol_ref,
|
||||||
prev_symbol_ref,
|
prev_symbol_ref,
|
||||||
config,
|
diff_config,
|
||||||
)?;
|
)?;
|
||||||
*prev_out.symbol_diff_mut(prev_symbol_ref) = prev_diff;
|
*prev_out.symbol_diff_mut(prev_symbol_ref) = prev_diff;
|
||||||
}
|
}
|
||||||
@@ -447,7 +291,7 @@ pub fn diff_objs(
|
|||||||
let (left_obj, left_out) = left.as_mut().unwrap();
|
let (left_obj, left_out) = left.as_mut().unwrap();
|
||||||
match section_kind {
|
match section_kind {
|
||||||
ObjSectionKind::Code => {
|
ObjSectionKind::Code => {
|
||||||
let code = process_code_symbol(left_obj, left_symbol_ref, config)?;
|
let code = process_code_symbol(left_obj, left_symbol_ref, diff_config)?;
|
||||||
*left_out.symbol_diff_mut(left_symbol_ref) =
|
*left_out.symbol_diff_mut(left_symbol_ref) =
|
||||||
no_diff_code(&code, left_symbol_ref)?;
|
no_diff_code(&code, left_symbol_ref)?;
|
||||||
}
|
}
|
||||||
@@ -461,7 +305,7 @@ pub fn diff_objs(
|
|||||||
let (right_obj, right_out) = right.as_mut().unwrap();
|
let (right_obj, right_out) = right.as_mut().unwrap();
|
||||||
match section_kind {
|
match section_kind {
|
||||||
ObjSectionKind::Code => {
|
ObjSectionKind::Code => {
|
||||||
let code = process_code_symbol(right_obj, right_symbol_ref, config)?;
|
let code = process_code_symbol(right_obj, right_symbol_ref, diff_config)?;
|
||||||
*right_out.symbol_diff_mut(right_symbol_ref) =
|
*right_out.symbol_diff_mut(right_symbol_ref) =
|
||||||
no_diff_code(&code, right_symbol_ref)?;
|
no_diff_code(&code, right_symbol_ref)?;
|
||||||
}
|
}
|
||||||
@@ -505,6 +349,8 @@ pub fn diff_objs(
|
|||||||
let left_section_diff = left_out.section_diff(left_section_idx);
|
let left_section_diff = left_out.section_diff(left_section_idx);
|
||||||
let right_section_diff = right_out.section_diff(right_section_idx);
|
let right_section_diff = right_out.section_diff(right_section_idx);
|
||||||
let (left_diff, right_diff) = diff_data_section(
|
let (left_diff, right_diff) = diff_data_section(
|
||||||
|
left_obj,
|
||||||
|
right_obj,
|
||||||
left_section,
|
left_section,
|
||||||
right_section,
|
right_section,
|
||||||
left_section_diff,
|
left_section_diff,
|
||||||
@@ -529,6 +375,17 @@ pub fn diff_objs(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
Ok(DiffObjsResult {
|
||||||
left: left.map(|(_, o)| o),
|
left: left.map(|(_, o)| o),
|
||||||
right: right.map(|(_, o)| o),
|
right: right.map(|(_, o)| o),
|
||||||
@@ -536,6 +393,63 @@ pub fn diff_objs(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// When we're selecting a symbol to use as a comparison, we'll create comparisons for all
|
||||||
|
/// symbols in the other object that match the selected symbol's section and kind. This allows
|
||||||
|
/// us to display match percentages for all symbols in the other object that could be selected.
|
||||||
|
fn generate_mapping_symbols(
|
||||||
|
base_obj: &ObjInfo,
|
||||||
|
base_name: &str,
|
||||||
|
target_obj: &ObjInfo,
|
||||||
|
target_out: &mut ObjDiff,
|
||||||
|
config: &DiffObjConfig,
|
||||||
|
) -> Result<()> {
|
||||||
|
let Some(base_symbol_ref) = symbol_ref_by_name(base_obj, base_name) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let (base_section, _base_symbol) = base_obj.section_symbol(base_symbol_ref);
|
||||||
|
let Some(base_section) = base_section else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let base_code = match base_section.kind {
|
||||||
|
ObjSectionKind::Code => Some(process_code_symbol(base_obj, base_symbol_ref, config)?),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
for (target_section_index, target_section) in
|
||||||
|
target_obj.sections.iter().enumerate().filter(|(_, s)| s.kind == base_section.kind)
|
||||||
|
{
|
||||||
|
for (target_symbol_index, _target_symbol) in target_section.symbols.iter().enumerate() {
|
||||||
|
let target_symbol_ref =
|
||||||
|
SymbolRef { section_idx: target_section_index, symbol_idx: target_symbol_index };
|
||||||
|
match base_section.kind {
|
||||||
|
ObjSectionKind::Code => {
|
||||||
|
let target_code = process_code_symbol(target_obj, target_symbol_ref, config)?;
|
||||||
|
let (left_diff, _right_diff) = diff_code(
|
||||||
|
target_obj,
|
||||||
|
base_obj,
|
||||||
|
&target_code,
|
||||||
|
base_code.as_ref().unwrap(),
|
||||||
|
target_symbol_ref,
|
||||||
|
base_symbol_ref,
|
||||||
|
config,
|
||||||
|
)?;
|
||||||
|
target_out.mapping_symbols.push(left_diff);
|
||||||
|
}
|
||||||
|
ObjSectionKind::Data => {
|
||||||
|
let (left_diff, _right_diff) =
|
||||||
|
diff_data_symbol(target_obj, base_obj, target_symbol_ref, base_symbol_ref)?;
|
||||||
|
target_out.mapping_symbols.push(left_diff);
|
||||||
|
}
|
||||||
|
ObjSectionKind::Bss => {
|
||||||
|
let (left_diff, _right_diff) =
|
||||||
|
diff_bss_symbol(target_obj, base_obj, target_symbol_ref, base_symbol_ref)?;
|
||||||
|
target_out.mapping_symbols.push(left_diff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||||
struct SymbolMatch {
|
struct SymbolMatch {
|
||||||
left: Option<SymbolRef>,
|
left: Option<SymbolRef>,
|
||||||
@@ -551,19 +465,117 @@ struct SectionMatch {
|
|||||||
section_kind: ObjSectionKind,
|
section_kind: ObjSectionKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct MappingConfig {
|
||||||
|
/// Manual symbol mappings
|
||||||
|
pub mappings: SymbolMappings,
|
||||||
|
/// The right object symbol name that we're selecting a left symbol for
|
||||||
|
pub selecting_left: Option<String>,
|
||||||
|
/// The left object symbol name that we're selecting a right symbol for
|
||||||
|
pub selecting_right: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn symbol_ref_by_name(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
|
||||||
|
for (section_idx, section) in obj.sections.iter().enumerate() {
|
||||||
|
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
|
||||||
|
if symbol.name == name {
|
||||||
|
return Some(SymbolRef { section_idx, symbol_idx });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_symbol_mappings(
|
||||||
|
left: &ObjInfo,
|
||||||
|
right: &ObjInfo,
|
||||||
|
mapping_config: &MappingConfig,
|
||||||
|
left_used: &mut HashSet<SymbolRef>,
|
||||||
|
right_used: &mut HashSet<SymbolRef>,
|
||||||
|
matches: &mut Vec<SymbolMatch>,
|
||||||
|
) -> Result<()> {
|
||||||
|
// If we're selecting a symbol to use as a comparison, mark it as used
|
||||||
|
// This ensures that we don't match it to another symbol at any point
|
||||||
|
if let Some(left_name) = &mapping_config.selecting_left {
|
||||||
|
if let Some(left_symbol) = symbol_ref_by_name(left, left_name) {
|
||||||
|
left_used.insert(left_symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(right_name) = &mapping_config.selecting_right {
|
||||||
|
if let Some(right_symbol) = symbol_ref_by_name(right, right_name) {
|
||||||
|
right_used.insert(right_symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply manual symbol mappings
|
||||||
|
for (left_name, right_name) in &mapping_config.mappings {
|
||||||
|
let Some(left_symbol) = symbol_ref_by_name(left, left_name) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if left_used.contains(&left_symbol) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(right_symbol) = symbol_ref_by_name(right, right_name) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if right_used.contains(&right_symbol) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let left_section = &left.sections[left_symbol.section_idx];
|
||||||
|
let right_section = &right.sections[right_symbol.section_idx];
|
||||||
|
if left_section.kind != right_section.kind {
|
||||||
|
log::warn!(
|
||||||
|
"Symbol section kind mismatch: {} ({:?}) vs {} ({:?})",
|
||||||
|
left_name,
|
||||||
|
left_section.kind,
|
||||||
|
right_name,
|
||||||
|
right_section.kind
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
matches.push(SymbolMatch {
|
||||||
|
left: Some(left_symbol),
|
||||||
|
right: Some(right_symbol),
|
||||||
|
prev: None, // TODO
|
||||||
|
section_kind: left_section.kind,
|
||||||
|
});
|
||||||
|
left_used.insert(left_symbol);
|
||||||
|
right_used.insert(right_symbol);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Find matching symbols between each object.
|
/// Find matching symbols between each object.
|
||||||
fn matching_symbols(
|
fn matching_symbols(
|
||||||
left: Option<&ObjInfo>,
|
left: Option<&ObjInfo>,
|
||||||
right: Option<&ObjInfo>,
|
right: Option<&ObjInfo>,
|
||||||
prev: Option<&ObjInfo>,
|
prev: Option<&ObjInfo>,
|
||||||
|
mappings: &MappingConfig,
|
||||||
) -> Result<Vec<SymbolMatch>> {
|
) -> Result<Vec<SymbolMatch>> {
|
||||||
let mut matches = Vec::new();
|
let mut matches = Vec::new();
|
||||||
|
let mut left_used = HashSet::new();
|
||||||
let mut right_used = HashSet::new();
|
let mut right_used = HashSet::new();
|
||||||
if let Some(left) = left {
|
if let Some(left) = left {
|
||||||
|
if let Some(right) = right {
|
||||||
|
apply_symbol_mappings(
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
mappings,
|
||||||
|
&mut left_used,
|
||||||
|
&mut right_used,
|
||||||
|
&mut matches,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
for (section_idx, section) in left.sections.iter().enumerate() {
|
for (section_idx, section) in left.sections.iter().enumerate() {
|
||||||
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
|
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
|
||||||
|
let symbol_ref = SymbolRef { section_idx, symbol_idx };
|
||||||
|
if left_used.contains(&symbol_ref) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let symbol_match = SymbolMatch {
|
let symbol_match = SymbolMatch {
|
||||||
left: Some(SymbolRef { section_idx, symbol_idx }),
|
left: Some(symbol_ref),
|
||||||
right: find_symbol(right, symbol, section, Some(&right_used)),
|
right: find_symbol(right, symbol, section, Some(&right_used)),
|
||||||
prev: find_symbol(prev, symbol, section, None),
|
prev: find_symbol(prev, symbol, section, None),
|
||||||
section_kind: section.kind,
|
section_kind: section.kind,
|
||||||
@@ -575,8 +587,12 @@ fn matching_symbols(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (symbol_idx, symbol) in left.common.iter().enumerate() {
|
for (symbol_idx, symbol) in left.common.iter().enumerate() {
|
||||||
|
let symbol_ref = SymbolRef { section_idx: SECTION_COMMON, symbol_idx };
|
||||||
|
if left_used.contains(&symbol_ref) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let symbol_match = SymbolMatch {
|
let symbol_match = SymbolMatch {
|
||||||
left: Some(SymbolRef { section_idx: left.sections.len(), symbol_idx }),
|
left: Some(symbol_ref),
|
||||||
right: find_common_symbol(right, symbol),
|
right: find_common_symbol(right, symbol),
|
||||||
prev: find_common_symbol(prev, symbol),
|
prev: find_common_symbol(prev, symbol),
|
||||||
section_kind: ObjSectionKind::Bss,
|
section_kind: ObjSectionKind::Bss,
|
||||||
@@ -603,7 +619,7 @@ fn matching_symbols(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (symbol_idx, symbol) in right.common.iter().enumerate() {
|
for (symbol_idx, symbol) in right.common.iter().enumerate() {
|
||||||
let symbol_ref = SymbolRef { section_idx: right.sections.len(), symbol_idx };
|
let symbol_ref = SymbolRef { section_idx: SECTION_COMMON, symbol_idx };
|
||||||
if right_used.contains(&symbol_ref) {
|
if right_used.contains(&symbol_ref) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -696,7 +712,7 @@ fn find_common_symbol(obj: Option<&ObjInfo>, in_symbol: &ObjSymbol) -> Option<Sy
|
|||||||
let obj = obj?;
|
let obj = obj?;
|
||||||
for (symbol_idx, symbol) in obj.common.iter().enumerate() {
|
for (symbol_idx, symbol) in obj.common.iter().enumerate() {
|
||||||
if symbol.name == in_symbol.name {
|
if symbol.name == in_symbol.name {
|
||||||
return Some(SymbolRef { section_idx: obj.sections.len(), symbol_idx });
|
return Some(SymbolRef { section_idx: SECTION_COMMON, symbol_idx });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
|||||||
50
objdiff-core/src/jobs/check_update.rs
Normal file
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)))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,14 +1,10 @@
|
|||||||
use std::{fs, path::PathBuf, sync::mpsc::Receiver};
|
use std::{fs, path::PathBuf, sync::mpsc::Receiver, task::Waker};
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use const_format::formatcp;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::AppConfig,
|
build::{run_make, BuildConfig, BuildStatus},
|
||||||
jobs::{
|
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
|
||||||
objdiff::{run_make, BuildConfig, BuildStatus},
|
|
||||||
start_job, update_status, Job, JobContext, JobResult, JobState,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -23,37 +19,7 @@ pub struct CreateScratchConfig {
|
|||||||
pub compiler_flags: String,
|
pub compiler_flags: String,
|
||||||
pub function_name: String,
|
pub function_name: String,
|
||||||
pub target_obj: PathBuf,
|
pub target_obj: PathBuf,
|
||||||
}
|
pub preset_id: Option<u32>,
|
||||||
|
|
||||||
impl CreateScratchConfig {
|
|
||||||
pub(crate) fn from_config(config: &AppConfig, function_name: String) -> Result<Self> {
|
|
||||||
let Some(selected_obj) = &config.selected_obj else {
|
|
||||||
bail!("No object selected");
|
|
||||||
};
|
|
||||||
let Some(target_path) = &selected_obj.target_path else {
|
|
||||||
bail!("No target path for {}", selected_obj.name);
|
|
||||||
};
|
|
||||||
let Some(scratch_config) = &selected_obj.scratch else {
|
|
||||||
bail!("No scratch configuration for {}", selected_obj.name);
|
|
||||||
};
|
|
||||||
Ok(Self {
|
|
||||||
build_config: BuildConfig::from_config(config),
|
|
||||||
context_path: scratch_config.ctx_path.clone(),
|
|
||||||
build_context: scratch_config.build_ctx,
|
|
||||||
compiler: scratch_config.compiler.clone().unwrap_or_default(),
|
|
||||||
platform: scratch_config.platform.clone().unwrap_or_default(),
|
|
||||||
compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(),
|
|
||||||
function_name,
|
|
||||||
target_obj: target_path.to_path_buf(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_available(config: &AppConfig) -> bool {
|
|
||||||
let Some(selected_obj) = &config.selected_obj else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
selected_obj.target_path.is_some() && selected_obj.scratch.is_some()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
@@ -97,22 +63,25 @@ fn run_create_scratch(
|
|||||||
|
|
||||||
update_status(status, "Creating scratch".to_string(), 1, 2, &cancel)?;
|
update_status(status, "Creating scratch".to_string(), 1, 2, &cancel)?;
|
||||||
let diff_flags = [format!("--disassemble={}", config.function_name)];
|
let diff_flags = [format!("--disassemble={}", config.function_name)];
|
||||||
let diff_flags = serde_json::to_string(&diff_flags).unwrap();
|
let diff_flags = serde_json::to_string(&diff_flags)?;
|
||||||
let obj_path = project_dir.join(&config.target_obj);
|
let obj_path = project_dir.join(&config.target_obj);
|
||||||
let file = reqwest::blocking::multipart::Part::file(&obj_path)
|
let file = reqwest::blocking::multipart::Part::file(&obj_path)
|
||||||
.with_context(|| format!("Failed to open {}", obj_path.display()))?;
|
.with_context(|| format!("Failed to open {}", obj_path.display()))?;
|
||||||
let form = reqwest::blocking::multipart::Form::new()
|
let mut form = reqwest::blocking::multipart::Form::new()
|
||||||
.text("compiler", config.compiler.clone())
|
.text("compiler", config.compiler.clone())
|
||||||
.text("platform", config.platform.clone())
|
.text("platform", config.platform.clone())
|
||||||
.text("compiler_flags", config.compiler_flags.clone())
|
.text("compiler_flags", config.compiler_flags.clone())
|
||||||
.text("diff_label", config.function_name.clone())
|
.text("diff_label", config.function_name.clone())
|
||||||
.text("diff_flags", diff_flags)
|
.text("diff_flags", diff_flags)
|
||||||
.text("context", context.unwrap_or_default())
|
.text("context", context.unwrap_or_default())
|
||||||
.text("source_code", "// Move related code from Context tab to here")
|
.text("source_code", "// Move related code from Context tab to here");
|
||||||
.part("target_obj", file);
|
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 client = reqwest::blocking::Client::new();
|
||||||
let response = client
|
let response = client
|
||||||
.post(formatcp!("{API_HOST}/api/scratch"))
|
.post(format!("{API_HOST}/api/scratch"))
|
||||||
.multipart(form)
|
.multipart(form)
|
||||||
.send()
|
.send()
|
||||||
.map_err(|e| anyhow!("Failed to send request: {}", e))?;
|
.map_err(|e| anyhow!("Failed to send request: {}", e))?;
|
||||||
@@ -126,8 +95,8 @@ fn run_create_scratch(
|
|||||||
Ok(Box::from(CreateScratchResult { scratch_url }))
|
Ok(Box::from(CreateScratchResult { scratch_url }))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_create_scratch(ctx: &egui::Context, config: CreateScratchConfig) -> JobState {
|
pub fn start_create_scratch(waker: Waker, config: CreateScratchConfig) -> JobState {
|
||||||
start_job(ctx, "Create scratch", Job::CreateScratch, move |context, cancel| {
|
start_job(waker, "Create scratch", Job::CreateScratch, move |context, cancel| {
|
||||||
run_create_scratch(&context, cancel, config)
|
run_create_scratch(&context, cancel, config)
|
||||||
.map(|result| JobResult::CreateScratch(Some(result)))
|
.map(|result| JobResult::CreateScratch(Some(result)))
|
||||||
})
|
})
|
||||||
@@ -4,6 +4,7 @@ use std::{
|
|||||||
mpsc::{Receiver, Sender, TryRecvError},
|
mpsc::{Receiver, Sender, TryRecvError},
|
||||||
Arc, RwLock,
|
Arc, RwLock,
|
||||||
},
|
},
|
||||||
|
task::Waker,
|
||||||
thread::JoinHandle,
|
thread::JoinHandle,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,7 +54,6 @@ impl JobQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether any job is running.
|
/// Returns whether any job is running.
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn any_running(&self) -> bool {
|
pub fn any_running(&self) -> bool {
|
||||||
self.jobs.iter().any(|job| {
|
self.jobs.iter().any(|job| {
|
||||||
if let Some(handle) = &job.handle {
|
if let Some(handle) = &job.handle {
|
||||||
@@ -85,20 +85,64 @@ impl JobQueue {
|
|||||||
/// Clears all finished jobs.
|
/// Clears all finished jobs.
|
||||||
pub fn clear_finished(&mut self) {
|
pub fn clear_finished(&mut self) {
|
||||||
self.jobs.retain(|job| {
|
self.jobs.retain(|job| {
|
||||||
!(job.should_remove
|
!(job.handle.is_none() && job.context.status.read().unwrap().error.is_none())
|
||||||
&& 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.
|
/// Removes a job from the queue given its ID.
|
||||||
pub fn remove(&mut self, id: usize) { self.jobs.retain(|job| job.id != 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)]
|
#[derive(Clone)]
|
||||||
pub struct JobContext {
|
pub struct JobContext {
|
||||||
pub status: Arc<RwLock<JobStatus>>,
|
pub status: Arc<RwLock<JobStatus>>,
|
||||||
pub egui: egui::Context,
|
pub waker: Waker,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct JobState {
|
pub struct JobState {
|
||||||
@@ -107,7 +151,6 @@ pub struct JobState {
|
|||||||
pub handle: Option<JoinHandle<JobResult>>,
|
pub handle: Option<JoinHandle<JobResult>>,
|
||||||
pub context: JobContext,
|
pub context: JobContext,
|
||||||
pub cancel: Sender<()>,
|
pub cancel: Sender<()>,
|
||||||
pub should_remove: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -135,7 +178,7 @@ fn should_cancel(rx: &Receiver<()>) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn start_job(
|
fn start_job(
|
||||||
ctx: &egui::Context,
|
waker: Waker,
|
||||||
title: &str,
|
title: &str,
|
||||||
kind: Job,
|
kind: Job,
|
||||||
run: impl FnOnce(JobContext, Receiver<()>) -> Result<JobResult> + Send + 'static,
|
run: impl FnOnce(JobContext, Receiver<()>) -> Result<JobResult> + Send + 'static,
|
||||||
@@ -147,23 +190,21 @@ fn start_job(
|
|||||||
status: String::new(),
|
status: String::new(),
|
||||||
error: None,
|
error: None,
|
||||||
}));
|
}));
|
||||||
let context = JobContext { status: status.clone(), egui: ctx.clone() };
|
let context = JobContext { status: status.clone(), waker: waker.clone() };
|
||||||
let context_inner = JobContext { status: status.clone(), egui: ctx.clone() };
|
let context_inner = JobContext { status: status.clone(), waker };
|
||||||
let (tx, rx) = std::sync::mpsc::channel();
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
let handle = std::thread::spawn(move || {
|
let handle = std::thread::spawn(move || match run(context_inner, rx) {
|
||||||
return match run(context_inner, rx) {
|
Ok(state) => state,
|
||||||
Ok(state) => state,
|
Err(e) => {
|
||||||
Err(e) => {
|
if let Ok(mut w) = status.write() {
|
||||||
if let Ok(mut w) = status.write() {
|
w.error = Some(e);
|
||||||
w.error = Some(e);
|
|
||||||
}
|
|
||||||
JobResult::None
|
|
||||||
}
|
}
|
||||||
};
|
JobResult::None
|
||||||
|
}
|
||||||
});
|
});
|
||||||
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
|
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
|
||||||
log::info!("Started job {}", id);
|
// log::info!("Started job {}", id); TODO
|
||||||
JobState { id, kind, handle: Some(handle), context, cancel: tx, should_remove: true }
|
JobState { id, kind, handle: Some(handle), context, cancel: tx }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_status(
|
fn update_status(
|
||||||
@@ -184,6 +225,6 @@ fn update_status(
|
|||||||
w.status = str;
|
w.status = str;
|
||||||
}
|
}
|
||||||
drop(w);
|
drop(w);
|
||||||
context.egui.request_repaint();
|
context.waker.wake_by_ref();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
195
objdiff-core/src/jobs/objdiff.rs
Normal file
195
objdiff-core/src/jobs/objdiff.rs
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
use std::{path::PathBuf, sync::mpsc::Receiver, task::Waker};
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Error, Result};
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
build::{run_make, BuildConfig, BuildStatus},
|
||||||
|
diff::{diff_objs, DiffObjConfig, MappingConfig, ObjDiff},
|
||||||
|
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
|
||||||
|
obj::{read, ObjInfo},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct ObjDiffConfig {
|
||||||
|
pub build_config: BuildConfig,
|
||||||
|
pub build_base: bool,
|
||||||
|
pub build_target: bool,
|
||||||
|
pub target_path: Option<PathBuf>,
|
||||||
|
pub base_path: Option<PathBuf>,
|
||||||
|
pub diff_obj_config: DiffObjConfig,
|
||||||
|
pub mapping_config: MappingConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ObjDiffResult {
|
||||||
|
pub first_status: BuildStatus,
|
||||||
|
pub second_status: BuildStatus,
|
||||||
|
pub first_obj: Option<(ObjInfo, ObjDiff)>,
|
||||||
|
pub second_obj: Option<(ObjInfo, ObjDiff)>,
|
||||||
|
pub time: OffsetDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = Some(target_path.strip_prefix(project_dir).map_err(|_| {
|
||||||
|
anyhow!(
|
||||||
|
"Target path '{}' doesn't begin with '{}'",
|
||||||
|
target_path.display(),
|
||||||
|
project_dir.display()
|
||||||
|
)
|
||||||
|
})?);
|
||||||
|
}
|
||||||
|
if let Some(base_path) = &config.base_path {
|
||||||
|
base_path_rel = Some(base_path.strip_prefix(project_dir).map_err(|_| {
|
||||||
|
anyhow!(
|
||||||
|
"Base path '{}' doesn't begin with '{}'",
|
||||||
|
base_path.display(),
|
||||||
|
project_dir.display()
|
||||||
|
)
|
||||||
|
})?);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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.display()),
|
||||||
|
step_idx,
|
||||||
|
total,
|
||||||
|
&cancel,
|
||||||
|
)?;
|
||||||
|
step_idx += 1;
|
||||||
|
run_make(&config.build_config, target_path_rel)
|
||||||
|
}
|
||||||
|
_ => BuildStatus::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut second_status = match base_path_rel {
|
||||||
|
Some(base_path_rel) if config.build_base => {
|
||||||
|
update_status(
|
||||||
|
context,
|
||||||
|
format!("Building base {}", base_path_rel.display()),
|
||||||
|
step_idx,
|
||||||
|
total,
|
||||||
|
&cancel,
|
||||||
|
)?;
|
||||||
|
step_idx += 1;
|
||||||
|
run_make(&config.build_config, base_path_rel)
|
||||||
|
}
|
||||||
|
_ => BuildStatus::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let time = OffsetDateTime::now_utc();
|
||||||
|
|
||||||
|
let first_obj = match &config.target_path {
|
||||||
|
Some(target_path) if first_status.success => {
|
||||||
|
update_status(
|
||||||
|
context,
|
||||||
|
format!("Loading target {}", target_path.display()),
|
||||||
|
step_idx,
|
||||||
|
total,
|
||||||
|
&cancel,
|
||||||
|
)?;
|
||||||
|
step_idx += 1;
|
||||||
|
match read::read(target_path, &config.diff_obj_config) {
|
||||||
|
Ok(obj) => Some(obj),
|
||||||
|
Err(e) => {
|
||||||
|
first_status = BuildStatus {
|
||||||
|
success: false,
|
||||||
|
stdout: format!("Loading object '{}'", target_path.display()),
|
||||||
|
stderr: format!("{:#}", e),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(_) => {
|
||||||
|
step_idx += 1;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let second_obj = match &config.base_path {
|
||||||
|
Some(base_path) if second_status.success => {
|
||||||
|
update_status(
|
||||||
|
context,
|
||||||
|
format!("Loading base {}", base_path.display()),
|
||||||
|
step_idx,
|
||||||
|
total,
|
||||||
|
&cancel,
|
||||||
|
)?;
|
||||||
|
step_idx += 1;
|
||||||
|
match read::read(base_path, &config.diff_obj_config) {
|
||||||
|
Ok(obj) => Some(obj),
|
||||||
|
Err(e) => {
|
||||||
|
second_status = BuildStatus {
|
||||||
|
success: false,
|
||||||
|
stdout: format!("Loading object '{}'", base_path.display()),
|
||||||
|
stderr: format!("{:#}", e),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(_) => {
|
||||||
|
step_idx += 1;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
update_status(context, "Performing diff".to_string(), step_idx, total, &cancel)?;
|
||||||
|
step_idx += 1;
|
||||||
|
let result = diff_objs(
|
||||||
|
&config.diff_obj_config,
|
||||||
|
&config.mapping_config,
|
||||||
|
first_obj.as_ref(),
|
||||||
|
second_obj.as_ref(),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
update_status(context, "Complete".to_string(), step_idx, total, &cancel)?;
|
||||||
|
Ok(Box::new(ObjDiffResult {
|
||||||
|
first_status,
|
||||||
|
second_status,
|
||||||
|
first_obj: first_obj.and_then(|o| result.left.map(|d| (o, d))),
|
||||||
|
second_obj: second_obj.and_then(|o| result.right.map(|d| (o, d))),
|
||||||
|
time,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_build(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,14 +3,19 @@ use std::{
|
|||||||
fs::File,
|
fs::File,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::mpsc::Receiver,
|
sync::mpsc::Receiver,
|
||||||
|
task::Waker,
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
pub use self_update; // Re-export self_update crate
|
||||||
|
use self_update::update::ReleaseUpdate;
|
||||||
|
|
||||||
use crate::{
|
use crate::jobs::{start_job, update_status, Job, JobContext, JobResult, JobState};
|
||||||
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
|
|
||||||
update::build_updater,
|
pub struct UpdateConfig {
|
||||||
};
|
pub build_updater: fn() -> Result<Box<dyn ReleaseUpdate>>,
|
||||||
|
pub bin_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct UpdateResult {
|
pub struct UpdateResult {
|
||||||
pub exe_path: PathBuf,
|
pub exe_path: PathBuf,
|
||||||
@@ -19,16 +24,15 @@ pub struct UpdateResult {
|
|||||||
fn run_update(
|
fn run_update(
|
||||||
status: &JobContext,
|
status: &JobContext,
|
||||||
cancel: Receiver<()>,
|
cancel: Receiver<()>,
|
||||||
bin_name: String,
|
config: UpdateConfig,
|
||||||
) -> Result<Box<UpdateResult>> {
|
) -> Result<Box<UpdateResult>> {
|
||||||
update_status(status, "Fetching latest release".to_string(), 0, 3, &cancel)?;
|
update_status(status, "Fetching latest release".to_string(), 0, 3, &cancel)?;
|
||||||
let updater = build_updater().context("Failed to create release updater")?;
|
let updater = (config.build_updater)().context("Failed to create release updater")?;
|
||||||
let latest_release = updater.get_latest_release()?;
|
let latest_release = updater.get_latest_release()?;
|
||||||
let asset = latest_release
|
let asset =
|
||||||
.assets
|
latest_release.assets.iter().find(|a| a.name == config.bin_name).ok_or_else(|| {
|
||||||
.iter()
|
anyhow::Error::msg(format!("No release asset for {}", config.bin_name))
|
||||||
.find(|a| a.name == bin_name)
|
})?;
|
||||||
.ok_or_else(|| anyhow::Error::msg(format!("No release asset for {bin_name}")))?;
|
|
||||||
|
|
||||||
update_status(status, "Downloading release".to_string(), 1, 3, &cancel)?;
|
update_status(status, "Downloading release".to_string(), 1, 3, &cancel)?;
|
||||||
let tmp_dir = tempfile::Builder::new().prefix("update").tempdir_in(current_dir()?)?;
|
let tmp_dir = tempfile::Builder::new().prefix("update").tempdir_in(current_dir()?)?;
|
||||||
@@ -36,7 +40,7 @@ fn run_update(
|
|||||||
let tmp_file = File::create(&tmp_path)?;
|
let tmp_file = File::create(&tmp_path)?;
|
||||||
self_update::Download::from_url(&asset.download_url)
|
self_update::Download::from_url(&asset.download_url)
|
||||||
.set_header(reqwest::header::ACCEPT, "application/octet-stream".parse()?)
|
.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)?;
|
update_status(status, "Extracting release".to_string(), 2, 3, &cancel)?;
|
||||||
let tmp_file = tmp_dir.path().join("replacement_tmp");
|
let tmp_file = tmp_dir.path().join("replacement_tmp");
|
||||||
@@ -47,17 +51,16 @@ fn run_update(
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
use std::{fs, os::unix::fs::PermissionsExt};
|
use std::{fs, os::unix::fs::PermissionsExt};
|
||||||
let mut perms = fs::metadata(&target_file)?.permissions();
|
fs::set_permissions(&target_file, fs::Permissions::from_mode(0o755))?;
|
||||||
perms.set_mode(0o755);
|
|
||||||
fs::set_permissions(&target_file, perms)?;
|
|
||||||
}
|
}
|
||||||
|
tmp_dir.close()?;
|
||||||
|
|
||||||
update_status(status, "Complete".to_string(), 3, 3, &cancel)?;
|
update_status(status, "Complete".to_string(), 3, 3, &cancel)?;
|
||||||
Ok(Box::from(UpdateResult { exe_path: target_file }))
|
Ok(Box::from(UpdateResult { exe_path: target_file }))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_update(ctx: &egui::Context, bin_name: String) -> JobState {
|
pub fn start_update(waker: Waker, config: UpdateConfig) -> JobState {
|
||||||
start_job(ctx, "Update app", Job::Update, move |context, cancel| {
|
start_job(waker, "Update app", Job::Update, move |context, cancel| {
|
||||||
run_update(&context, cancel, bin_name).map(JobResult::Update)
|
run_update(&context, cancel, config).map(JobResult::Update)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,15 @@
|
|||||||
pub mod arch;
|
pub mod arch;
|
||||||
#[cfg(feature = "bindings")]
|
#[cfg(feature = "bindings")]
|
||||||
pub mod bindings;
|
pub mod bindings;
|
||||||
|
#[cfg(feature = "build")]
|
||||||
|
pub mod build;
|
||||||
#[cfg(feature = "config")]
|
#[cfg(feature = "config")]
|
||||||
pub mod config;
|
pub mod config;
|
||||||
#[cfg(feature = "any-arch")]
|
#[cfg(feature = "any-arch")]
|
||||||
pub mod diff;
|
pub mod diff;
|
||||||
|
#[cfg(feature = "build")]
|
||||||
|
pub mod jobs;
|
||||||
#[cfg(feature = "any-arch")]
|
#[cfg(feature = "any-arch")]
|
||||||
pub mod obj;
|
pub mod obj;
|
||||||
|
#[cfg(feature = "any-arch")]
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ pub mod split_meta;
|
|||||||
|
|
||||||
use std::{borrow::Cow, collections::BTreeMap, fmt, path::PathBuf};
|
use std::{borrow::Cow, collections::BTreeMap, fmt, path::PathBuf};
|
||||||
|
|
||||||
use cwextab::*;
|
|
||||||
use filetime::FileTime;
|
use filetime::FileTime;
|
||||||
use flagset::{flags, FlagSet};
|
use flagset::{flags, FlagSet};
|
||||||
use object::RelocationFlags;
|
use object::RelocationFlags;
|
||||||
@@ -24,6 +23,9 @@ flags! {
|
|||||||
Weak,
|
Weak,
|
||||||
Common,
|
Common,
|
||||||
Hidden,
|
Hidden,
|
||||||
|
/// Has extra data associated with the symbol
|
||||||
|
/// (e.g. exception table entry)
|
||||||
|
HasExtra,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[derive(Debug, Copy, Clone, Default)]
|
#[derive(Debug, Copy, Clone, Default)]
|
||||||
@@ -83,6 +85,9 @@ pub enum ObjInsArg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ObjInsArg {
|
impl ObjInsArg {
|
||||||
|
#[inline]
|
||||||
|
pub fn is_plain_text(&self) -> bool { matches!(self, ObjInsArg::PlainText(_)) }
|
||||||
|
|
||||||
pub fn loose_eq(&self, other: &ObjInsArg) -> bool {
|
pub fn loose_eq(&self, other: &ObjInsArg) -> bool {
|
||||||
match (self, other) {
|
match (self, other) {
|
||||||
(ObjInsArg::Arg(a), ObjInsArg::Arg(b)) => a.loose_eq(b),
|
(ObjInsArg::Arg(a), ObjInsArg::Arg(b)) => a.loose_eq(b),
|
||||||
@@ -98,7 +103,7 @@ pub struct ObjIns {
|
|||||||
pub address: u64,
|
pub address: u64,
|
||||||
pub size: u8,
|
pub size: u8,
|
||||||
pub op: u16,
|
pub op: u16,
|
||||||
pub mnemonic: String,
|
pub mnemonic: Cow<'static, str>,
|
||||||
pub args: Vec<ObjInsArg>,
|
pub args: Vec<ObjInsArg>,
|
||||||
pub reloc: Option<ObjReloc>,
|
pub reloc: Option<ObjReloc>,
|
||||||
pub branch_dest: Option<u64>,
|
pub branch_dest: Option<u64>,
|
||||||
@@ -110,28 +115,39 @@ pub struct ObjIns {
|
|||||||
pub orig: Option<String>,
|
pub orig: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ObjIns {
|
||||||
|
/// Iterate over non-PlainText arguments.
|
||||||
|
#[inline]
|
||||||
|
pub fn iter_args(&self) -> impl DoubleEndedIterator<Item = &ObjInsArg> {
|
||||||
|
self.args.iter().filter(|a| !a.is_plain_text())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
|
||||||
|
pub enum ObjSymbolKind {
|
||||||
|
#[default]
|
||||||
|
Unknown,
|
||||||
|
Function,
|
||||||
|
Object,
|
||||||
|
Section,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ObjSymbol {
|
pub struct ObjSymbol {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub demangled_name: Option<String>,
|
pub demangled_name: Option<String>,
|
||||||
pub has_extab: bool,
|
|
||||||
pub extab_name: Option<String>,
|
|
||||||
pub extabindex_name: Option<String>,
|
|
||||||
pub address: u64,
|
pub address: u64,
|
||||||
pub section_address: u64,
|
pub section_address: u64,
|
||||||
pub size: u64,
|
pub size: u64,
|
||||||
pub size_known: bool,
|
pub size_known: bool,
|
||||||
|
pub kind: ObjSymbolKind,
|
||||||
pub flags: ObjSymbolFlagSet,
|
pub flags: ObjSymbolFlagSet,
|
||||||
pub addend: i64,
|
pub orig_section_index: Option<usize>,
|
||||||
/// Original virtual address (from .note.split section)
|
/// Original virtual address (from .note.split section)
|
||||||
pub virtual_address: Option<u64>,
|
pub virtual_address: Option<u64>,
|
||||||
}
|
/// Original index in object symbol table
|
||||||
|
pub original_index: Option<usize>,
|
||||||
#[derive(Debug, Clone)]
|
pub bytes: Vec<u8>,
|
||||||
pub struct ObjExtab {
|
|
||||||
pub func: ObjSymbol,
|
|
||||||
pub data: ExceptionTableData,
|
|
||||||
pub dtors: Vec<ObjSymbol>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ObjInfo {
|
pub struct ObjInfo {
|
||||||
@@ -141,8 +157,6 @@ pub struct ObjInfo {
|
|||||||
pub sections: Vec<ObjSection>,
|
pub sections: Vec<ObjSection>,
|
||||||
/// Common BSS symbols
|
/// Common BSS symbols
|
||||||
pub common: Vec<ObjSymbol>,
|
pub common: Vec<ObjSymbol>,
|
||||||
/// Exception tables
|
|
||||||
pub extab: Option<Vec<ObjExtab>>,
|
|
||||||
/// Split object metadata (.note.split section)
|
/// Split object metadata (.note.split section)
|
||||||
pub split_meta: Option<SplitMeta>,
|
pub split_meta: Option<SplitMeta>,
|
||||||
}
|
}
|
||||||
@@ -152,7 +166,7 @@ pub struct ObjReloc {
|
|||||||
pub flags: RelocationFlags,
|
pub flags: RelocationFlags,
|
||||||
pub address: u64,
|
pub address: u64,
|
||||||
pub target: ObjSymbol,
|
pub target: ObjSymbol,
|
||||||
pub target_section: Option<String>,
|
pub addend: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||||
@@ -161,9 +175,11 @@ pub struct SymbolRef {
|
|||||||
pub symbol_idx: usize,
|
pub symbol_idx: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const SECTION_COMMON: usize = usize::MAX - 1;
|
||||||
|
|
||||||
impl ObjInfo {
|
impl ObjInfo {
|
||||||
pub fn section_symbol(&self, symbol_ref: SymbolRef) -> (Option<&ObjSection>, &ObjSymbol) {
|
pub fn section_symbol(&self, symbol_ref: SymbolRef) -> (Option<&ObjSection>, &ObjSymbol) {
|
||||||
if symbol_ref.section_idx == self.sections.len() {
|
if symbol_ref.section_idx == SECTION_COMMON {
|
||||||
let symbol = &self.common[symbol_ref.symbol_idx];
|
let symbol = &self.common[symbol_ref.symbol_idx];
|
||||||
return (None, symbol);
|
return (None, symbol);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
use std::{collections::HashSet, fs, io::Cursor, mem::size_of, path::Path};
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
fs,
|
||||||
|
io::Cursor,
|
||||||
|
mem::size_of,
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||||
use cwextab::decode_extab;
|
|
||||||
use filetime::FileTime;
|
use filetime::FileTime;
|
||||||
use flagset::Flags;
|
use flagset::Flags;
|
||||||
use object::{
|
use object::{
|
||||||
endian::LittleEndian as LE,
|
endian::LittleEndian as LE,
|
||||||
pe::{ImageAuxSymbolFunctionBeginEnd, ImageLinenumber},
|
pe::{ImageAuxSymbolFunctionBeginEnd, ImageLinenumber},
|
||||||
read::coff::{CoffFile, CoffHeader, ImageSymbol},
|
read::coff::{CoffFile, CoffHeader, ImageSymbol},
|
||||||
Architecture, BinaryFormat, File, Object, ObjectSection, ObjectSymbol, RelocationTarget,
|
BinaryFormat, File, Object, ObjectSection, ObjectSymbol, RelocationTarget, Section,
|
||||||
SectionIndex, SectionKind, Symbol, SymbolIndex, SymbolKind, SymbolScope, SymbolSection,
|
SectionIndex, SectionKind, Symbol, SymbolIndex, SymbolKind, SymbolScope,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -17,8 +22,8 @@ use crate::{
|
|||||||
diff::DiffObjConfig,
|
diff::DiffObjConfig,
|
||||||
obj::{
|
obj::{
|
||||||
split_meta::{SplitMeta, SPLITMETA_SECTION},
|
split_meta::{SplitMeta, SPLITMETA_SECTION},
|
||||||
ObjExtab, ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet,
|
ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags,
|
||||||
ObjSymbolFlags,
|
ObjSymbolKind,
|
||||||
},
|
},
|
||||||
util::{read_u16, read_u32},
|
util::{read_u16, read_u32},
|
||||||
};
|
};
|
||||||
@@ -36,7 +41,6 @@ fn to_obj_symbol(
|
|||||||
arch: &dyn ObjArch,
|
arch: &dyn ObjArch,
|
||||||
obj_file: &File<'_>,
|
obj_file: &File<'_>,
|
||||||
symbol: &Symbol<'_, '_>,
|
symbol: &Symbol<'_, '_>,
|
||||||
addend: i64,
|
|
||||||
split_meta: Option<&SplitMeta>,
|
split_meta: Option<&SplitMeta>,
|
||||||
) -> Result<ObjSymbol> {
|
) -> Result<ObjSymbol> {
|
||||||
let mut name = symbol.name().context("Failed to process symbol name")?;
|
let mut name = symbol.name().context("Failed to process symbol name")?;
|
||||||
@@ -60,6 +64,11 @@ fn to_obj_symbol(
|
|||||||
if obj_file.format() == BinaryFormat::Elf && symbol.scope() == SymbolScope::Linkage {
|
if obj_file.format() == BinaryFormat::Elf && symbol.scope() == SymbolScope::Linkage {
|
||||||
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Hidden);
|
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Hidden);
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "ppc")]
|
||||||
|
if arch.ppc().and_then(|a| a.extab.as_ref()).is_some_and(|e| e.contains_key(&symbol.index().0))
|
||||||
|
{
|
||||||
|
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::HasExtra);
|
||||||
|
}
|
||||||
let address = arch.symbol_address(symbol);
|
let address = arch.symbol_address(symbol);
|
||||||
let section_address = if let Some(section) =
|
let section_address = if let Some(section) =
|
||||||
symbol.section_index().and_then(|idx| obj_file.section_by_index(idx).ok())
|
symbol.section_index().and_then(|idx| obj_file.section_by_index(idx).ok())
|
||||||
@@ -73,19 +82,36 @@ fn to_obj_symbol(
|
|||||||
let virtual_address = split_meta
|
let virtual_address = split_meta
|
||||||
.and_then(|m| m.virtual_addresses.as_ref())
|
.and_then(|m| m.virtual_addresses.as_ref())
|
||||||
.and_then(|v| v.get(symbol.index().0).cloned());
|
.and_then(|v| v.get(symbol.index().0).cloned());
|
||||||
|
|
||||||
|
let bytes = symbol
|
||||||
|
.section_index()
|
||||||
|
.and_then(|idx| obj_file.section_by_index(idx).ok())
|
||||||
|
.and_then(|section| section.data().ok())
|
||||||
|
.and_then(|data| {
|
||||||
|
data.get(section_address as usize..(section_address + symbol.size()) as usize)
|
||||||
|
})
|
||||||
|
.unwrap_or(&[]);
|
||||||
|
|
||||||
|
let kind = match symbol.kind() {
|
||||||
|
SymbolKind::Text => ObjSymbolKind::Function,
|
||||||
|
SymbolKind::Data => ObjSymbolKind::Object,
|
||||||
|
SymbolKind::Section => ObjSymbolKind::Section,
|
||||||
|
_ => ObjSymbolKind::Unknown,
|
||||||
|
};
|
||||||
|
|
||||||
Ok(ObjSymbol {
|
Ok(ObjSymbol {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
demangled_name,
|
demangled_name,
|
||||||
has_extab: false,
|
|
||||||
extab_name: None,
|
|
||||||
extabindex_name: None,
|
|
||||||
address,
|
address,
|
||||||
section_address,
|
section_address,
|
||||||
size: symbol.size(),
|
size: symbol.size(),
|
||||||
size_known: symbol.size() != 0,
|
size_known: symbol.size() != 0,
|
||||||
|
kind,
|
||||||
flags,
|
flags,
|
||||||
addend,
|
orig_section_index: symbol.section_index().map(|i| i.0),
|
||||||
virtual_address,
|
virtual_address,
|
||||||
|
original_index: Some(symbol.index().0),
|
||||||
|
bytes: bytes.to_vec(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,25 +158,23 @@ fn symbols_by_section(
|
|||||||
arch: &dyn ObjArch,
|
arch: &dyn ObjArch,
|
||||||
obj_file: &File<'_>,
|
obj_file: &File<'_>,
|
||||||
section: &ObjSection,
|
section: &ObjSection,
|
||||||
|
section_symbols: &[Symbol<'_, '_>],
|
||||||
split_meta: Option<&SplitMeta>,
|
split_meta: Option<&SplitMeta>,
|
||||||
|
name_counts: &mut HashMap<String, u32>,
|
||||||
) -> Result<Vec<ObjSymbol>> {
|
) -> Result<Vec<ObjSymbol>> {
|
||||||
let mut result = Vec::<ObjSymbol>::new();
|
let mut result = Vec::<ObjSymbol>::new();
|
||||||
for symbol in obj_file.symbols() {
|
for symbol in section_symbols {
|
||||||
if symbol.kind() == SymbolKind::Section {
|
if symbol.kind() == SymbolKind::Section {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(index) = symbol.section().index() {
|
if symbol.is_local() && section.kind == ObjSectionKind::Code {
|
||||||
if index.0 == section.orig_index {
|
// TODO strip local syms in diff?
|
||||||
if symbol.is_local() && section.kind == ObjSectionKind::Code {
|
let name = symbol.name().context("Failed to process symbol name")?;
|
||||||
// TODO strip local syms in diff?
|
if symbol.size() == 0 || name.starts_with("lbl_") {
|
||||||
let name = symbol.name().context("Failed to process symbol name")?;
|
continue;
|
||||||
if symbol.size() == 0 || name.starts_with("lbl_") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.push(to_obj_symbol(arch, obj_file, &symbol, 0, split_meta)?);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
result.push(to_obj_symbol(arch, obj_file, symbol, split_meta)?);
|
||||||
}
|
}
|
||||||
result.sort_by(|a, b| a.address.cmp(&b.address).then(a.size.cmp(&b.size)));
|
result.sort_by(|a, b| a.address.cmp(&b.address).then(a.size.cmp(&b.size)));
|
||||||
let mut iter = result.iter_mut().peekable();
|
let mut iter = result.iter_mut().peekable();
|
||||||
@@ -161,23 +185,39 @@ fn symbols_by_section(
|
|||||||
} else {
|
} else {
|
||||||
symbol.size = (section.address + section.size) - symbol.address;
|
symbol.size = (section.address + section.size) - symbol.address;
|
||||||
}
|
}
|
||||||
|
// Set symbol kind if we ended up with a non-zero size
|
||||||
|
if symbol.kind == ObjSymbolKind::Unknown && symbol.size > 0 {
|
||||||
|
symbol.kind = match section.kind {
|
||||||
|
ObjSectionKind::Code => ObjSymbolKind::Function,
|
||||||
|
ObjSectionKind::Data | ObjSectionKind::Bss => ObjSymbolKind::Object,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if result.is_empty() {
|
if result.is_empty() {
|
||||||
// Dummy symbol for empty sections
|
// Dummy symbol for empty sections
|
||||||
|
*name_counts.entry(section.name.clone()).or_insert(0) += 1;
|
||||||
|
let current_count: u32 = *name_counts.get(§ion.name).unwrap();
|
||||||
result.push(ObjSymbol {
|
result.push(ObjSymbol {
|
||||||
name: format!("[{}]", section.name),
|
name: if current_count > 1 {
|
||||||
|
format!("[{} ({})]", section.name, current_count)
|
||||||
|
} else {
|
||||||
|
format!("[{}]", section.name)
|
||||||
|
},
|
||||||
demangled_name: None,
|
demangled_name: None,
|
||||||
has_extab: false,
|
|
||||||
extab_name: None,
|
|
||||||
extabindex_name: None,
|
|
||||||
address: 0,
|
address: 0,
|
||||||
section_address: 0,
|
section_address: 0,
|
||||||
size: section.size,
|
size: section.size,
|
||||||
size_known: true,
|
size_known: true,
|
||||||
|
kind: match section.kind {
|
||||||
|
ObjSectionKind::Code => ObjSymbolKind::Function,
|
||||||
|
ObjSectionKind::Data | ObjSectionKind::Bss => ObjSymbolKind::Object,
|
||||||
|
},
|
||||||
flags: Default::default(),
|
flags: Default::default(),
|
||||||
addend: 0,
|
orig_section_index: Some(section.orig_index),
|
||||||
virtual_address: None,
|
virtual_address: None,
|
||||||
|
original_index: None,
|
||||||
|
bytes: Vec::new(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
@@ -191,160 +231,78 @@ fn common_symbols(
|
|||||||
obj_file
|
obj_file
|
||||||
.symbols()
|
.symbols()
|
||||||
.filter(Symbol::is_common)
|
.filter(Symbol::is_common)
|
||||||
.map(|symbol| to_obj_symbol(arch, obj_file, &symbol, 0, split_meta))
|
.map(|symbol| to_obj_symbol(arch, obj_file, &symbol, split_meta))
|
||||||
.collect::<Result<Vec<ObjSymbol>>>()
|
.collect::<Result<Vec<ObjSymbol>>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn section_by_name<'a>(sections: &'a mut [ObjSection], name: &str) -> Option<&'a mut ObjSection> {
|
const LOW_PRIORITY_SYMBOLS: &[&str] =
|
||||||
sections.iter_mut().find(|section| section.name == name)
|
&["__gnu_compiled_c", "__gnu_compiled_cplusplus", "gcc2_compiled."];
|
||||||
}
|
|
||||||
|
|
||||||
fn exception_tables(
|
fn best_symbol<'r, 'data, 'file>(
|
||||||
sections: &mut [ObjSection],
|
symbols: &'r [Symbol<'data, 'file>],
|
||||||
obj_file: &File<'_>,
|
address: u64,
|
||||||
) -> Result<Option<Vec<ObjExtab>>> {
|
) -> Option<&'r Symbol<'data, 'file>> {
|
||||||
//PowerPC only
|
let mut closest_symbol_index = match symbols.binary_search_by_key(&address, |s| s.address()) {
|
||||||
if obj_file.architecture() != Architecture::PowerPc {
|
Ok(index) => Some(index),
|
||||||
return Ok(None);
|
Err(index) => index.checked_sub(1),
|
||||||
|
}?;
|
||||||
|
// The binary search may not find the first symbol at the address, so work backwards
|
||||||
|
let target_address = symbols[closest_symbol_index].address();
|
||||||
|
while let Some(prev_index) = closest_symbol_index.checked_sub(1) {
|
||||||
|
if symbols[prev_index].address() != target_address {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
closest_symbol_index = prev_index;
|
||||||
}
|
}
|
||||||
|
let mut best_symbol: Option<&'r Symbol<'data, 'file>> = None;
|
||||||
//Find the extab/extabindex sections
|
for symbol in symbols.iter().skip(closest_symbol_index) {
|
||||||
let extab_section = match section_by_name(sections, "extab") {
|
if symbol.address() > address {
|
||||||
Some(section) => section.clone(),
|
break;
|
||||||
None => {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
}
|
||||||
};
|
if symbol.kind() == SymbolKind::Section
|
||||||
let extabindex_section = match section_by_name(sections, "extabindex") {
|
|| (symbol.size() > 0 && (symbol.address() + symbol.size()) <= address)
|
||||||
Some(section) => section.clone(),
|
{
|
||||||
None => {
|
continue;
|
||||||
return Ok(None);
|
}
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
let text_section = match section_by_name(sections, ".text") {
|
|
||||||
Some(section) => section,
|
|
||||||
None => bail!(".text section is somehow missing, this should not happen"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut result: Vec<ObjExtab> = vec![];
|
|
||||||
let extab_symbol_count = extab_section.symbols.len();
|
|
||||||
let extabindex_symbol_count = extabindex_section.symbols.len();
|
|
||||||
let extab_reloc_count = extab_section.relocations.len();
|
|
||||||
let table_count = extab_symbol_count;
|
|
||||||
let mut extab_reloc_index: usize = 0;
|
|
||||||
|
|
||||||
//Make sure that the number of symbols in the extab/extabindex section matches. If not, exit early
|
|
||||||
if extab_symbol_count != extabindex_symbol_count {
|
|
||||||
bail!("Extab/Extabindex symbol counts do not match");
|
|
||||||
}
|
}
|
||||||
|
best_symbol
|
||||||
//Convert the extab/extabindex section data
|
|
||||||
|
|
||||||
//Go through each extabindex entry
|
|
||||||
for i in 0..table_count {
|
|
||||||
let extabindex = &extabindex_section.symbols[i];
|
|
||||||
|
|
||||||
/* Get the function symbol and extab symbol from the extabindex relocations array. Each extabindex
|
|
||||||
entry has two relocations (the first for the function, the second for the extab entry) */
|
|
||||||
let extab_func = extabindex_section.relocations[i * 2].target.clone();
|
|
||||||
let extab = &extabindex_section.relocations[(i * 2) + 1].target;
|
|
||||||
|
|
||||||
let extab_start_addr = extab.address;
|
|
||||||
let extab_end_addr = extab_start_addr + extab.size;
|
|
||||||
|
|
||||||
//Find the function in the text section, and set the has extab flag
|
|
||||||
for i in 0..text_section.symbols.len() {
|
|
||||||
let func = &mut text_section.symbols[i];
|
|
||||||
if func.name == extab_func.name {
|
|
||||||
func.has_extab = true;
|
|
||||||
func.extab_name = Some(extab.name.clone());
|
|
||||||
func.extabindex_name = Some(extabindex.name.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Iterate through the list of extab relocations, continuing until we hit a relocation
|
|
||||||
that isn't within the current extab symbol. Get the target dtor function symbol from
|
|
||||||
each relocation used, and add them to the list. */
|
|
||||||
let mut dtors: Vec<ObjSymbol> = vec![];
|
|
||||||
|
|
||||||
while extab_reloc_index < extab_reloc_count {
|
|
||||||
let extab_reloc = &extab_section.relocations[extab_reloc_index];
|
|
||||||
//If the current entry is past the current extab table, stop here
|
|
||||||
if extab_reloc.address >= extab_end_addr {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Otherwise, the current relocation is used by the current table
|
|
||||||
dtors.push(extab_reloc.target.clone());
|
|
||||||
//Go to the next entry
|
|
||||||
extab_reloc_index += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Decode the extab data
|
|
||||||
let start_index = extab_start_addr as usize;
|
|
||||||
let end_index = extab_end_addr as usize;
|
|
||||||
let extab_data = extab_section.data[start_index..end_index].try_into().unwrap();
|
|
||||||
let data = match decode_extab(extab_data) {
|
|
||||||
Some(decoded_data) => decoded_data,
|
|
||||||
None => {
|
|
||||||
log::warn!("Exception table decoding failed for function {}", extab_func.name);
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//Add the new entry to the list
|
|
||||||
let entry = ObjExtab { func: extab_func, data, dtors };
|
|
||||||
result.push(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(result))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_section_symbol(
|
fn find_section_symbol(
|
||||||
arch: &dyn ObjArch,
|
arch: &dyn ObjArch,
|
||||||
obj_file: &File<'_>,
|
obj_file: &File<'_>,
|
||||||
target: &Symbol<'_, '_>,
|
section: &Section,
|
||||||
|
section_symbols: &[Symbol<'_, '_>],
|
||||||
address: u64,
|
address: u64,
|
||||||
split_meta: Option<&SplitMeta>,
|
split_meta: Option<&SplitMeta>,
|
||||||
) -> Result<ObjSymbol> {
|
) -> Result<ObjSymbol> {
|
||||||
let section_index =
|
if let Some(symbol) = best_symbol(section_symbols, address) {
|
||||||
target.section_index().ok_or_else(|| anyhow::Error::msg("Unknown section index"))?;
|
return to_obj_symbol(arch, obj_file, symbol, split_meta);
|
||||||
let section = obj_file.section_by_index(section_index)?;
|
|
||||||
let mut closest_symbol: Option<Symbol<'_, '_>> = None;
|
|
||||||
for symbol in obj_file.symbols() {
|
|
||||||
if !matches!(symbol.section_index(), Some(idx) if idx == section_index) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if symbol.kind() == SymbolKind::Section || symbol.address() != address {
|
|
||||||
if symbol.address() < address
|
|
||||||
&& symbol.size() != 0
|
|
||||||
&& (closest_symbol.is_none()
|
|
||||||
|| matches!(&closest_symbol, Some(s) if s.address() <= symbol.address()))
|
|
||||||
{
|
|
||||||
closest_symbol = Some(symbol);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return to_obj_symbol(arch, obj_file, &symbol, 0, split_meta);
|
|
||||||
}
|
}
|
||||||
let (name, offset) = closest_symbol
|
// Fallback to section symbol
|
||||||
.and_then(|s| s.name().map(|n| (n, s.address())).ok())
|
|
||||||
.or_else(|| section.name().map(|n| (n, section.address())).ok())
|
|
||||||
.unwrap_or(("<unknown>", 0));
|
|
||||||
let offset_addr = address - offset;
|
|
||||||
Ok(ObjSymbol {
|
Ok(ObjSymbol {
|
||||||
name: name.to_string(),
|
name: section.name()?.to_string(),
|
||||||
demangled_name: None,
|
demangled_name: None,
|
||||||
has_extab: false,
|
address: section.address(),
|
||||||
extab_name: None,
|
section_address: 0,
|
||||||
extabindex_name: None,
|
|
||||||
address: offset,
|
|
||||||
section_address: address - section.address(),
|
|
||||||
size: 0,
|
size: 0,
|
||||||
size_known: false,
|
size_known: false,
|
||||||
|
kind: ObjSymbolKind::Section,
|
||||||
flags: Default::default(),
|
flags: Default::default(),
|
||||||
addend: offset_addr as i64,
|
orig_section_index: Some(section.index().0),
|
||||||
virtual_address: None,
|
virtual_address: None,
|
||||||
|
original_index: None,
|
||||||
|
bytes: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,6 +310,7 @@ fn relocations_by_section(
|
|||||||
arch: &dyn ObjArch,
|
arch: &dyn ObjArch,
|
||||||
obj_file: &File<'_>,
|
obj_file: &File<'_>,
|
||||||
section: &ObjSection,
|
section: &ObjSection,
|
||||||
|
section_symbols: &[Vec<Symbol<'_, '_>>],
|
||||||
split_meta: Option<&SplitMeta>,
|
split_meta: Option<&SplitMeta>,
|
||||||
) -> Result<Vec<ObjReloc>> {
|
) -> Result<Vec<ObjReloc>> {
|
||||||
let obj_section = obj_file.section_by_index(SectionIndex(section.orig_index))?;
|
let obj_section = obj_file.section_by_index(SectionIndex(section.orig_index))?;
|
||||||
@@ -373,33 +332,43 @@ fn relocations_by_section(
|
|||||||
};
|
};
|
||||||
symbol
|
symbol
|
||||||
}
|
}
|
||||||
|
RelocationTarget::Absolute => {
|
||||||
|
log::warn!("Ignoring absolute relocation @ {}:{:#x}", section.name, address);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
_ => bail!("Unhandled relocation target: {:?}", reloc.target()),
|
_ => bail!("Unhandled relocation target: {:?}", reloc.target()),
|
||||||
};
|
};
|
||||||
let flags = reloc.flags(); // TODO validate reloc here?
|
let flags = reloc.flags(); // TODO validate reloc here?
|
||||||
let target_section = match symbol.section() {
|
let mut addend = if reloc.has_implicit_addend() {
|
||||||
SymbolSection::Common => Some(".comm".to_string()),
|
|
||||||
SymbolSection::Section(idx) => {
|
|
||||||
obj_file.section_by_index(idx).and_then(|s| s.name().map(|s| s.to_string())).ok()
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
let addend = if reloc.has_implicit_addend() {
|
|
||||||
arch.implcit_addend(obj_file, section, address, &reloc)?
|
arch.implcit_addend(obj_file, section, address, &reloc)?
|
||||||
} else {
|
} else {
|
||||||
reloc.addend()
|
reloc.addend()
|
||||||
};
|
};
|
||||||
// println!("Reloc: {reloc:?}, symbol: {symbol:?}, addend: {addend:#x}");
|
|
||||||
let target = match symbol.kind() {
|
let target = match symbol.kind() {
|
||||||
SymbolKind::Text | SymbolKind::Data | SymbolKind::Label | SymbolKind::Unknown => {
|
SymbolKind::Text | SymbolKind::Data | SymbolKind::Label | SymbolKind::Unknown => {
|
||||||
to_obj_symbol(arch, obj_file, &symbol, addend, split_meta)
|
to_obj_symbol(arch, obj_file, &symbol, split_meta)?
|
||||||
}
|
}
|
||||||
SymbolKind::Section => {
|
SymbolKind::Section => {
|
||||||
ensure!(addend >= 0, "Negative addend in reloc: {addend}");
|
ensure!(addend >= 0, "Negative addend in section reloc: {addend}");
|
||||||
find_section_symbol(arch, obj_file, &symbol, addend as u64, split_meta)
|
let section_index = symbol
|
||||||
|
.section_index()
|
||||||
|
.ok_or_else(|| anyhow!("Section symbol {symbol:?} has no section index"))?;
|
||||||
|
let section = obj_file.section_by_index(section_index)?;
|
||||||
|
let symbol = find_section_symbol(
|
||||||
|
arch,
|
||||||
|
obj_file,
|
||||||
|
§ion,
|
||||||
|
§ion_symbols[section_index.0],
|
||||||
|
addend as u64,
|
||||||
|
split_meta,
|
||||||
|
)?;
|
||||||
|
// Adjust addend to be relative to the selected symbol
|
||||||
|
addend = (symbol.address - section.address()) as i64;
|
||||||
|
symbol
|
||||||
}
|
}
|
||||||
kind => Err(anyhow!("Unhandled relocation symbol type {kind:?}")),
|
kind => bail!("Unhandled relocation symbol type {kind:?}"),
|
||||||
}?;
|
};
|
||||||
relocations.push(ObjReloc { flags, address, target, target_section });
|
relocations.push(ObjReloc { flags, address, target, addend });
|
||||||
}
|
}
|
||||||
Ok(relocations)
|
Ok(relocations)
|
||||||
}
|
}
|
||||||
@@ -464,9 +433,9 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection], obj_data: &[u8])
|
|||||||
let mut text_sections =
|
let mut text_sections =
|
||||||
obj_file.sections().filter(|s| s.kind() == SectionKind::Text);
|
obj_file.sections().filter(|s| s.kind() == SectionKind::Text);
|
||||||
let section_index = text_sections.next().map(|s| s.index().0);
|
let section_index = text_sections.next().map(|s| s.index().0);
|
||||||
let mut lines = section_index.map(|index| {
|
let mut lines = section_index
|
||||||
&mut sections.iter_mut().find(|s| s.orig_index == index).unwrap().line_info
|
.and_then(|index| sections.iter_mut().find(|s| s.orig_index == index))
|
||||||
});
|
.map(|s| &mut s.line_info);
|
||||||
|
|
||||||
let mut rows = program.rows();
|
let mut rows = program.rows();
|
||||||
while let Some((_header, row)) = rows.next_row()? {
|
while let Some((_header, row)) = rows.next_row()? {
|
||||||
@@ -477,13 +446,9 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection], obj_data: &[u8])
|
|||||||
// The next row is the start of a new sequence, which means we must
|
// The next row is the start of a new sequence, which means we must
|
||||||
// advance to the next .text section.
|
// advance to the next .text section.
|
||||||
let section_index = text_sections.next().map(|s| s.index().0);
|
let section_index = text_sections.next().map(|s| s.index().0);
|
||||||
lines = section_index.map(|index| {
|
lines = section_index
|
||||||
&mut sections
|
.and_then(|index| sections.iter_mut().find(|s| s.orig_index == index))
|
||||||
.iter_mut()
|
.map(|s| &mut s.line_info);
|
||||||
.find(|s| s.orig_index == index)
|
|
||||||
.unwrap()
|
|
||||||
.line_info
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -615,20 +580,20 @@ fn update_combined_symbol(symbol: ObjSymbol, address_change: i64) -> Result<ObjS
|
|||||||
Ok(ObjSymbol {
|
Ok(ObjSymbol {
|
||||||
name: symbol.name,
|
name: symbol.name,
|
||||||
demangled_name: symbol.demangled_name,
|
demangled_name: symbol.demangled_name,
|
||||||
has_extab: symbol.has_extab,
|
|
||||||
extab_name: symbol.extab_name,
|
|
||||||
extabindex_name: symbol.extabindex_name,
|
|
||||||
address: (symbol.address as i64 + address_change).try_into()?,
|
address: (symbol.address as i64 + address_change).try_into()?,
|
||||||
section_address: (symbol.section_address as i64 + address_change).try_into()?,
|
section_address: (symbol.section_address as i64 + address_change).try_into()?,
|
||||||
size: symbol.size,
|
size: symbol.size,
|
||||||
size_known: symbol.size_known,
|
size_known: symbol.size_known,
|
||||||
|
kind: symbol.kind,
|
||||||
flags: symbol.flags,
|
flags: symbol.flags,
|
||||||
addend: symbol.addend,
|
orig_section_index: symbol.orig_section_index,
|
||||||
virtual_address: if let Some(virtual_address) = symbol.virtual_address {
|
virtual_address: if let Some(virtual_address) = symbol.virtual_address {
|
||||||
Some((virtual_address as i64 + address_change).try_into()?)
|
Some((virtual_address as i64 + address_change).try_into()?)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
|
original_index: symbol.original_index,
|
||||||
|
bytes: symbol.bytes,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,8 +612,8 @@ fn combine_sections(section: ObjSection, combine: ObjSection) -> Result<ObjSecti
|
|||||||
relocations.push(ObjReloc {
|
relocations.push(ObjReloc {
|
||||||
flags: reloc.flags,
|
flags: reloc.flags,
|
||||||
address: (reloc.address as i64 + address_change).try_into()?,
|
address: (reloc.address as i64 + address_change).try_into()?,
|
||||||
target: reloc.target, // TODO: Should be updated?
|
target: reloc.target, // TODO: Should be updated?
|
||||||
target_section: reloc.target_section, // TODO: Same as above
|
addend: reloc.addend,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -728,20 +693,47 @@ pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result<ObjInfo> {
|
|||||||
let obj_file = File::parse(data)?;
|
let obj_file = File::parse(data)?;
|
||||||
let arch = new_arch(&obj_file)?;
|
let arch = new_arch(&obj_file)?;
|
||||||
let split_meta = split_meta(&obj_file)?;
|
let split_meta = split_meta(&obj_file)?;
|
||||||
|
|
||||||
|
// Create sorted symbol list for each section
|
||||||
|
let mut section_symbols = Vec::with_capacity(obj_file.sections().count());
|
||||||
|
for section in obj_file.sections() {
|
||||||
|
let mut symbols = obj_file
|
||||||
|
.symbols()
|
||||||
|
.filter(|s| s.section_index() == Some(section.index()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
symbols.sort_by_key(|s| s.address());
|
||||||
|
let section_index = section.index().0;
|
||||||
|
if section_index >= section_symbols.len() {
|
||||||
|
section_symbols.resize_with(section_index + 1, Vec::new);
|
||||||
|
}
|
||||||
|
section_symbols[section_index] = symbols;
|
||||||
|
}
|
||||||
|
|
||||||
let mut sections = filter_sections(&obj_file, split_meta.as_ref())?;
|
let mut sections = filter_sections(&obj_file, split_meta.as_ref())?;
|
||||||
|
let mut section_name_counts: HashMap<String, u32> = HashMap::new();
|
||||||
for section in &mut sections {
|
for section in &mut sections {
|
||||||
section.symbols =
|
section.symbols = symbols_by_section(
|
||||||
symbols_by_section(arch.as_ref(), &obj_file, section, split_meta.as_ref())?;
|
arch.as_ref(),
|
||||||
section.relocations =
|
&obj_file,
|
||||||
relocations_by_section(arch.as_ref(), &obj_file, section, split_meta.as_ref())?;
|
section,
|
||||||
|
§ion_symbols[section.orig_index],
|
||||||
|
split_meta.as_ref(),
|
||||||
|
&mut section_name_counts,
|
||||||
|
)?;
|
||||||
|
section.relocations = relocations_by_section(
|
||||||
|
arch.as_ref(),
|
||||||
|
&obj_file,
|
||||||
|
section,
|
||||||
|
§ion_symbols,
|
||||||
|
split_meta.as_ref(),
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
if config.combine_data_sections {
|
if config.combine_data_sections {
|
||||||
combine_data_sections(&mut sections)?;
|
combine_data_sections(&mut sections)?;
|
||||||
}
|
}
|
||||||
line_info(&obj_file, &mut sections, data)?;
|
line_info(&obj_file, &mut sections, data)?;
|
||||||
let common = common_symbols(arch.as_ref(), &obj_file, split_meta.as_ref())?;
|
let common = common_symbols(arch.as_ref(), &obj_file, split_meta.as_ref())?;
|
||||||
let extab = exception_tables(&mut sections, &obj_file)?;
|
Ok(ObjInfo { arch, path: None, timestamp: None, sections, common, split_meta })
|
||||||
Ok(ObjInfo { arch, path: None, timestamp: None, sections, common, extab, split_meta })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_function(obj_path: &Path, symbol_name: &str) -> Result<bool> {
|
pub fn has_function(obj_path: &Path, symbol_name: &str) -> Result<bool> {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "objdiff-gui"
|
name = "objdiff-gui"
|
||||||
version = "2.0.0-beta.6"
|
version.workspace = true
|
||||||
edition = "2021"
|
edition.workspace = true
|
||||||
rust-version = "1.70"
|
rust-version.workspace = true
|
||||||
authors = ["Luke Street <luke@street.dev>"]
|
authors.workspace = true
|
||||||
license = "MIT OR Apache-2.0"
|
license.workspace = true
|
||||||
repository = "https://github.com/encounter/objdiff"
|
repository.workspace = true
|
||||||
readme = "../README.md"
|
readme = "../README.md"
|
||||||
description = """
|
description = """
|
||||||
A local diffing tool for decompilation projects.
|
A local diffing tool for decompilation projects.
|
||||||
@@ -24,38 +24,37 @@ wgpu = ["eframe/wgpu", "dep:wgpu"]
|
|||||||
wsl = []
|
wsl = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.82"
|
anyhow = "1.0"
|
||||||
bytes = "1.6.0"
|
bytes = "1.9"
|
||||||
cfg-if = "1.0.0"
|
cfg-if = "1.0"
|
||||||
const_format = "0.2.32"
|
const_format = "0.2"
|
||||||
cwdemangle = "1.0.0"
|
cwdemangle = "1.0"
|
||||||
cwextab = "0.2.3"
|
cwextab = "1.0"
|
||||||
dirs = "5.0.1"
|
dirs = "5.0"
|
||||||
egui = "0.27.2"
|
egui = "0.30"
|
||||||
egui_extras = "0.27.2"
|
egui_extras = "0.30"
|
||||||
filetime = "0.2.23"
|
filetime = "0.2"
|
||||||
float-ord = "0.3.2"
|
float-ord = "0.3"
|
||||||
font-kit = "0.13.0"
|
font-kit = "0.14"
|
||||||
globset = { version = "0.4.14", features = ["serde1"] }
|
globset = { version = "0.4", features = ["serde1"] }
|
||||||
log = "0.4.21"
|
log = "0.4"
|
||||||
notify = { git = "https://github.com/encounter/notify", rev = "4c1783e8e041b5f69d4cf1750b9f07e335a0771e" }
|
|
||||||
objdiff-core = { path = "../objdiff-core", features = ["all"] }
|
objdiff-core = { path = "../objdiff-core", features = ["all"] }
|
||||||
png = "0.17.13"
|
open = "5.3"
|
||||||
pollster = "0.3.0"
|
png = "0.17"
|
||||||
regex = "1.10.5"
|
pollster = "0.4"
|
||||||
rfd = { version = "0.14.1" } #, default-features = false, features = ['xdg-portal']
|
regex = "1.11"
|
||||||
rlwinmdec = "1.0.1"
|
rfd = { version = "0.15" } #, default-features = false, features = ['xdg-portal']
|
||||||
ron = "0.8.1"
|
rlwinmdec = "1.0"
|
||||||
serde = { version = "1", features = ["derive"] }
|
ron = "0.8"
|
||||||
serde_json = "1.0.116"
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
shell-escape = "0.1.5"
|
serde_json = "1.0"
|
||||||
strum = { version = "0.26.2", features = ["derive"] }
|
shell-escape = "0.1"
|
||||||
tempfile = "3.10.1"
|
strum = { version = "0.26", features = ["derive"] }
|
||||||
time = { version = "0.3.36", features = ["formatting", "local-offset"] }
|
time = { version = "0.3", features = ["formatting", "local-offset"] }
|
||||||
|
|
||||||
# Keep version in sync with egui
|
# Keep version in sync with egui
|
||||||
[dependencies.eframe]
|
[dependencies.eframe]
|
||||||
version = "0.27.2"
|
version = "0.30"
|
||||||
features = [
|
features = [
|
||||||
"default_fonts",
|
"default_fonts",
|
||||||
"persistence",
|
"persistence",
|
||||||
@@ -66,7 +65,7 @@ default-features = false
|
|||||||
|
|
||||||
# Keep version in sync with eframe
|
# Keep version in sync with eframe
|
||||||
[dependencies.wgpu]
|
[dependencies.wgpu]
|
||||||
version = "0.19.1"
|
version = "23.0"
|
||||||
features = [
|
features = [
|
||||||
"dx12",
|
"dx12",
|
||||||
"metal",
|
"metal",
|
||||||
@@ -75,35 +74,23 @@ features = [
|
|||||||
optional = true
|
optional = true
|
||||||
default-features = false
|
default-features = false
|
||||||
|
|
||||||
# For Linux static binaries, use rustls
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
|
||||||
reqwest = { version = "0.12.4", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"] }
|
|
||||||
self_update = { version = "0.40.0", default-features = false, features = ["rustls"] }
|
|
||||||
|
|
||||||
# For all other platforms, use native TLS
|
|
||||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
|
||||||
reqwest = { version = "0.12.4", default-features = false, features = ["blocking", "json", "multipart", "default-tls"] }
|
|
||||||
self_update = "0.40.0"
|
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
path-slash = "0.2.1"
|
winapi = "0.3"
|
||||||
winapi = "0.3.9"
|
|
||||||
|
|
||||||
[target.'cfg(windows)'.build-dependencies]
|
|
||||||
winres = "0.1.12"
|
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
exec = "0.3.1"
|
exec = "0.3"
|
||||||
|
|
||||||
# native:
|
# native:
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
# web:
|
# web:
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
console_error_panic_hook = "0.1.7"
|
console_error_panic_hook = "0.1"
|
||||||
tracing-wasm = "0.2"
|
tracing-wasm = "0.2"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
anyhow = "1.0.82"
|
anyhow = "1.0"
|
||||||
vergen = { version = "8.3.1", features = ["build", "cargo", "git", "gitcl"] }
|
|
||||||
|
[target.'cfg(windows)'.build-dependencies]
|
||||||
|
tauri-winres = "0.2"
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use vergen::EmitBuilder;
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
winres::WindowsResource::new().set_icon("assets/icon.ico").compile()?;
|
let mut res = tauri_winres::WindowsResource::new();
|
||||||
|
res.set_icon("assets/icon.ico");
|
||||||
|
res.set_language(0x0409); // US English
|
||||||
|
res.compile()?;
|
||||||
}
|
}
|
||||||
EmitBuilder::builder().fail_on_error().all_build().all_cargo().all_git().emit()
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,30 +7,31 @@ use std::{
|
|||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
Arc, Mutex, RwLock,
|
Arc, Mutex, RwLock,
|
||||||
},
|
},
|
||||||
|
time::Instant,
|
||||||
};
|
};
|
||||||
|
|
||||||
use filetime::FileTime;
|
use filetime::FileTime;
|
||||||
use globset::{Glob, GlobSet};
|
use globset::Glob;
|
||||||
use notify::{RecursiveMode, Watcher};
|
|
||||||
use objdiff_core::{
|
use objdiff_core::{
|
||||||
|
build::watcher::{create_watcher, Watcher},
|
||||||
config::{
|
config::{
|
||||||
build_globset, ProjectConfigInfo, ProjectObject, ScratchConfig, DEFAULT_WATCH_PATTERNS,
|
build_globset, default_watch_patterns, save_project_config, ProjectConfig,
|
||||||
|
ProjectConfigInfo, ProjectObject, ScratchConfig, SymbolMappings, DEFAULT_WATCH_PATTERNS,
|
||||||
},
|
},
|
||||||
diff::DiffObjConfig,
|
diff::DiffObjConfig,
|
||||||
|
jobs::{Job, JobQueue, JobResult},
|
||||||
};
|
};
|
||||||
use time::UtcOffset;
|
use time::UtcOffset;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_config::{deserialize_config, AppConfigVersion},
|
app_config::{deserialize_config, AppConfigVersion},
|
||||||
config::{load_project_config, ProjectObjectNode},
|
config::{load_project_config, ProjectObjectNode},
|
||||||
jobs::{
|
jobs::{create_objdiff_config, egui_waker, start_build},
|
||||||
objdiff::{start_build, ObjDiffConfig},
|
|
||||||
Job, JobQueue, JobResult, JobStatus,
|
|
||||||
},
|
|
||||||
views::{
|
views::{
|
||||||
appearance::{appearance_window, Appearance},
|
appearance::{appearance_window, Appearance},
|
||||||
config::{
|
config::{
|
||||||
arch_config_window, config_ui, project_window, ConfigViewState, CONFIG_DISABLED_TEXT,
|
arch_config_window, config_ui, general_config_ui, project_window, ConfigViewState,
|
||||||
|
CONFIG_DISABLED_TEXT,
|
||||||
},
|
},
|
||||||
data_diff::data_diff_ui,
|
data_diff::data_diff_ui,
|
||||||
debug::debug_window,
|
debug::debug_window,
|
||||||
@@ -39,13 +40,12 @@ use crate::{
|
|||||||
frame_history::FrameHistory,
|
frame_history::FrameHistory,
|
||||||
function_diff::function_diff_ui,
|
function_diff::function_diff_ui,
|
||||||
graphics::{graphics_window, GraphicsConfig, GraphicsViewState},
|
graphics::{graphics_window, GraphicsConfig, GraphicsViewState},
|
||||||
jobs::jobs_ui,
|
jobs::{jobs_menu_ui, jobs_window},
|
||||||
rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState},
|
rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState},
|
||||||
symbol_diff::{symbol_diff_ui, DiffViewState, View},
|
symbol_diff::{symbol_diff_ui, DiffViewAction, DiffViewNavigation, DiffViewState, View},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct ViewState {
|
pub struct ViewState {
|
||||||
pub jobs: JobQueue,
|
pub jobs: JobQueue,
|
||||||
pub config_state: ConfigViewState,
|
pub config_state: ConfigViewState,
|
||||||
@@ -61,10 +61,35 @@ pub struct ViewState {
|
|||||||
pub show_arch_config: bool,
|
pub show_arch_config: bool,
|
||||||
pub show_debug: bool,
|
pub show_debug: bool,
|
||||||
pub show_graphics: 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.
|
/// The configuration for a single object file.
|
||||||
#[derive(Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct ObjectConfig {
|
pub struct ObjectConfig {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub target_path: Option<PathBuf>,
|
pub target_path: Option<PathBuf>,
|
||||||
@@ -72,14 +97,67 @@ pub struct ObjectConfig {
|
|||||||
pub reverse_fn_order: Option<bool>,
|
pub reverse_fn_order: Option<bool>,
|
||||||
pub complete: Option<bool>,
|
pub complete: Option<bool>,
|
||||||
pub scratch: Option<ScratchConfig>,
|
pub scratch: Option<ScratchConfig>,
|
||||||
|
pub source_path: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub symbol_mappings: SymbolMappings,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&ProjectObject> for ObjectConfig {
|
||||||
|
fn from(object: &ProjectObject) -> Self {
|
||||||
|
Self {
|
||||||
|
name: object.name().to_string(),
|
||||||
|
target_path: object.target_path.clone(),
|
||||||
|
base_path: object.base_path.clone(),
|
||||||
|
reverse_fn_order: object.reverse_fn_order(),
|
||||||
|
complete: object.complete(),
|
||||||
|
scratch: object.scratch.clone(),
|
||||||
|
source_path: object.source_path().cloned(),
|
||||||
|
symbol_mappings: object.symbol_mappings.clone().unwrap_or_default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn bool_true() -> bool { true }
|
fn bool_true() -> bool { true }
|
||||||
|
|
||||||
#[inline]
|
pub struct AppState {
|
||||||
fn default_watch_patterns() -> Vec<Glob> {
|
pub config: AppConfig,
|
||||||
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
|
pub objects: Vec<ProjectObject>,
|
||||||
|
pub object_nodes: Vec<ProjectObjectNode>,
|
||||||
|
pub watcher_change: bool,
|
||||||
|
pub config_change: bool,
|
||||||
|
pub obj_change: bool,
|
||||||
|
pub queue_build: bool,
|
||||||
|
pub queue_reload: bool,
|
||||||
|
pub current_project_config: Option<ProjectConfig>,
|
||||||
|
pub project_config_info: Option<ProjectConfigInfo>,
|
||||||
|
pub last_mod_check: Instant,
|
||||||
|
/// The right object symbol name that we're selecting a left symbol for
|
||||||
|
pub selecting_left: Option<String>,
|
||||||
|
/// The left object symbol name that we're selecting a right symbol for
|
||||||
|
pub selecting_right: Option<String>,
|
||||||
|
pub config_error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AppState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
config: Default::default(),
|
||||||
|
objects: vec![],
|
||||||
|
object_nodes: vec![],
|
||||||
|
watcher_change: false,
|
||||||
|
config_change: false,
|
||||||
|
obj_change: false,
|
||||||
|
queue_build: false,
|
||||||
|
queue_reload: false,
|
||||||
|
current_project_config: None,
|
||||||
|
project_config_info: None,
|
||||||
|
last_mod_check: Instant::now(),
|
||||||
|
selecting_left: None,
|
||||||
|
selecting_right: None,
|
||||||
|
config_error: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, serde::Deserialize, serde::Serialize)]
|
#[derive(Clone, serde::Deserialize, serde::Serialize)]
|
||||||
@@ -116,23 +194,6 @@ pub struct AppConfig {
|
|||||||
pub recent_projects: Vec<PathBuf>,
|
pub recent_projects: Vec<PathBuf>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub diff_obj_config: DiffObjConfig,
|
pub diff_obj_config: DiffObjConfig,
|
||||||
|
|
||||||
#[serde(skip)]
|
|
||||||
pub objects: Vec<ProjectObject>,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub object_nodes: Vec<ProjectObjectNode>,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub watcher_change: bool,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub config_change: bool,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub obj_change: bool,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub queue_build: bool,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub queue_reload: bool,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub project_config_info: Option<ProjectConfigInfo>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AppConfig {
|
impl Default for AppConfig {
|
||||||
@@ -153,69 +214,184 @@ impl Default for AppConfig {
|
|||||||
watch_patterns: DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(),
|
watch_patterns: DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(),
|
||||||
recent_projects: vec![],
|
recent_projects: vec![],
|
||||||
diff_obj_config: Default::default(),
|
diff_obj_config: Default::default(),
|
||||||
objects: vec![],
|
|
||||||
object_nodes: vec![],
|
|
||||||
watcher_change: false,
|
|
||||||
config_change: false,
|
|
||||||
obj_change: false,
|
|
||||||
queue_build: false,
|
|
||||||
queue_reload: false,
|
|
||||||
project_config_info: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppConfig {
|
impl AppState {
|
||||||
pub fn set_project_dir(&mut self, path: PathBuf) {
|
pub fn set_project_dir(&mut self, path: PathBuf) {
|
||||||
self.recent_projects.retain(|p| p != &path);
|
self.config.recent_projects.retain(|p| p != &path);
|
||||||
if self.recent_projects.len() > 9 {
|
if self.config.recent_projects.len() > 9 {
|
||||||
self.recent_projects.truncate(9);
|
self.config.recent_projects.truncate(9);
|
||||||
}
|
}
|
||||||
self.recent_projects.insert(0, path.clone());
|
self.config.recent_projects.insert(0, path.clone());
|
||||||
self.project_dir = Some(path);
|
self.config.project_dir = Some(path);
|
||||||
self.target_obj_dir = None;
|
self.config.target_obj_dir = None;
|
||||||
self.base_obj_dir = None;
|
self.config.base_obj_dir = None;
|
||||||
self.selected_obj = None;
|
self.config.selected_obj = None;
|
||||||
self.build_target = false;
|
self.config.build_target = false;
|
||||||
self.objects.clear();
|
self.objects.clear();
|
||||||
self.object_nodes.clear();
|
self.object_nodes.clear();
|
||||||
self.watcher_change = true;
|
self.watcher_change = true;
|
||||||
self.config_change = true;
|
self.config_change = true;
|
||||||
self.obj_change = true;
|
self.obj_change = true;
|
||||||
self.queue_build = false;
|
self.queue_build = false;
|
||||||
|
self.current_project_config = None;
|
||||||
self.project_config_info = None;
|
self.project_config_info = None;
|
||||||
|
self.selecting_left = None;
|
||||||
|
self.selecting_right = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_target_obj_dir(&mut self, path: PathBuf) {
|
pub fn set_target_obj_dir(&mut self, path: PathBuf) {
|
||||||
self.target_obj_dir = Some(path);
|
self.config.target_obj_dir = Some(path);
|
||||||
self.selected_obj = None;
|
self.config.selected_obj = None;
|
||||||
self.obj_change = true;
|
self.obj_change = true;
|
||||||
self.queue_build = false;
|
self.queue_build = false;
|
||||||
|
self.selecting_left = None;
|
||||||
|
self.selecting_right = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_base_obj_dir(&mut self, path: PathBuf) {
|
pub fn set_base_obj_dir(&mut self, path: PathBuf) {
|
||||||
self.base_obj_dir = Some(path);
|
self.config.base_obj_dir = Some(path);
|
||||||
self.selected_obj = None;
|
self.config.selected_obj = None;
|
||||||
self.obj_change = true;
|
self.obj_change = true;
|
||||||
self.queue_build = false;
|
self.queue_build = false;
|
||||||
|
self.selecting_left = None;
|
||||||
|
self.selecting_right = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_selected_obj(&mut self, object: ObjectConfig) {
|
pub fn set_selected_obj(&mut self, config: ObjectConfig) {
|
||||||
self.selected_obj = Some(object);
|
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.obj_change = true;
|
||||||
self.queue_build = false;
|
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.as_ref().is_some_and(|n| n == &object.name))
|
||||||
|
{
|
||||||
|
existing.symbol_mappings = if object.symbol_mappings.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(object.symbol_mappings.clone())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Save the updated project config
|
||||||
|
match save_project_config(config, info) {
|
||||||
|
Ok(new_info) => *info = new_info,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to save project config: {e}");
|
||||||
|
self.config_error = Some(format!("Failed to save project config: {e}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type AppConfigRef = Arc<RwLock<AppConfig>>;
|
pub type AppStateRef = Arc<RwLock<AppState>>;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct App {
|
pub struct App {
|
||||||
appearance: Appearance,
|
appearance: Appearance,
|
||||||
view_state: ViewState,
|
view_state: ViewState,
|
||||||
config: AppConfigRef,
|
state: AppStateRef,
|
||||||
modified: Arc<AtomicBool>,
|
modified: Arc<AtomicBool>,
|
||||||
watcher: Option<notify::RecommendedWatcher>,
|
watcher: Option<Watcher>,
|
||||||
app_path: Option<PathBuf>,
|
app_path: Option<PathBuf>,
|
||||||
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
|
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
|
||||||
should_relaunch: bool,
|
should_relaunch: bool,
|
||||||
@@ -241,16 +417,17 @@ impl App {
|
|||||||
if let Some(appearance) = eframe::get_value::<Appearance>(storage, APPEARANCE_KEY) {
|
if let Some(appearance) = eframe::get_value::<Appearance>(storage, APPEARANCE_KEY) {
|
||||||
app.appearance = appearance;
|
app.appearance = appearance;
|
||||||
}
|
}
|
||||||
if let Some(mut config) = deserialize_config(storage) {
|
if let Some(config) = deserialize_config(storage) {
|
||||||
if config.project_dir.is_some() {
|
let mut state = AppState { config, ..Default::default() };
|
||||||
config.config_change = true;
|
if state.config.project_dir.is_some() {
|
||||||
config.watcher_change = true;
|
state.config_change = true;
|
||||||
|
state.watcher_change = true;
|
||||||
}
|
}
|
||||||
if config.selected_obj.is_some() {
|
if state.config.selected_obj.is_some() {
|
||||||
config.queue_build = true;
|
state.queue_build = true;
|
||||||
}
|
}
|
||||||
app.view_state.config_state.queue_check_update = config.auto_update_check;
|
app.view_state.config_state.queue_check_update = state.config.auto_update_check;
|
||||||
app.config = Arc::new(RwLock::new(config));
|
app.state = Arc::new(RwLock::new(state));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app.appearance.init_fonts(&cc.egui_ctx);
|
app.appearance.init_fonts(&cc.egui_ctx);
|
||||||
@@ -289,128 +466,108 @@ impl App {
|
|||||||
|
|
||||||
let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state;
|
let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state;
|
||||||
|
|
||||||
let mut results = vec![];
|
jobs.collect_results();
|
||||||
for (job, result) in jobs.iter_finished() {
|
jobs.results.retain(|result| match result {
|
||||||
match result {
|
JobResult::Update(state) => {
|
||||||
Ok(result) => {
|
if let Ok(mut guard) = self.relaunch_path.lock() {
|
||||||
log::info!("Job {} finished", job.id);
|
*guard = Some(state.exe_path.clone());
|
||||||
match result {
|
self.should_relaunch = true;
|
||||||
JobResult::None => {
|
|
||||||
if let Some(err) = &job.context.status.read().unwrap().error {
|
|
||||||
log::error!("{:?}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
JobResult::Update(state) => {
|
|
||||||
if let Ok(mut guard) = self.relaunch_path.lock() {
|
|
||||||
*guard = Some(state.exe_path);
|
|
||||||
self.should_relaunch = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => results.push(result),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
let err = if let Some(msg) = err.downcast_ref::<&'static str>() {
|
|
||||||
anyhow::Error::msg(*msg)
|
|
||||||
} else if let Some(msg) = err.downcast_ref::<String>() {
|
|
||||||
anyhow::Error::msg(msg.clone())
|
|
||||||
} else {
|
|
||||||
anyhow::Error::msg("Thread panicked")
|
|
||||||
};
|
|
||||||
let result = job.context.status.write();
|
|
||||||
if let Ok(mut guard) = result {
|
|
||||||
guard.error = Some(err);
|
|
||||||
} else {
|
|
||||||
drop(result);
|
|
||||||
job.context.status = Arc::new(RwLock::new(JobStatus {
|
|
||||||
title: "Error".to_string(),
|
|
||||||
progress_percent: 0.0,
|
|
||||||
progress_items: None,
|
|
||||||
status: String::new(),
|
|
||||||
error: Some(err),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
_ => true,
|
||||||
jobs.results.append(&mut results);
|
});
|
||||||
jobs.clear_finished();
|
diff_state.pre_update(jobs, &self.state);
|
||||||
|
config_state.pre_update(jobs, &self.state);
|
||||||
diff_state.pre_update(jobs, &self.config);
|
|
||||||
config_state.pre_update(jobs, &self.config);
|
|
||||||
debug_assert!(jobs.results.is_empty());
|
debug_assert!(jobs.results.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn post_update(&mut self, ctx: &egui::Context) {
|
fn post_update(&mut self, ctx: &egui::Context, action: Option<DiffViewAction>) {
|
||||||
|
if action.is_some() {
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
|
||||||
self.appearance.post_update(ctx);
|
self.appearance.post_update(ctx);
|
||||||
|
|
||||||
let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state;
|
let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state;
|
||||||
config_state.post_update(ctx, jobs, &self.config);
|
config_state.post_update(ctx, jobs, &self.state);
|
||||||
diff_state.post_update(ctx, jobs, &self.config);
|
diff_state.post_update(action, ctx, jobs, &self.state);
|
||||||
|
|
||||||
let Ok(mut config) = self.config.write() else {
|
let Ok(mut state) = self.state.write() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let config = &mut *config;
|
let state = &mut *state;
|
||||||
|
|
||||||
if let Some(info) = &config.project_config_info {
|
let mut mod_check = false;
|
||||||
if file_modified(&info.path, info.timestamp) {
|
if state.last_mod_check.elapsed().as_millis() >= 500 {
|
||||||
config.config_change = true;
|
state.last_mod_check = Instant::now();
|
||||||
}
|
mod_check = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.config_change {
|
if mod_check {
|
||||||
config.config_change = false;
|
if let Some(info) = &state.project_config_info {
|
||||||
match load_project_config(config) {
|
if let Some(last_ts) = info.timestamp {
|
||||||
Ok(()) => config_state.load_error = None,
|
if file_modified(&info.path, last_ts) {
|
||||||
Err(e) => {
|
state.config_change = true;
|
||||||
log::error!("Failed to load project config: {e}");
|
|
||||||
config_state.load_error = Some(format!("{e}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.watcher_change {
|
|
||||||
drop(self.watcher.take());
|
|
||||||
|
|
||||||
if let Some(project_dir) = &config.project_dir {
|
|
||||||
match build_globset(&config.watch_patterns).map_err(anyhow::Error::new).and_then(
|
|
||||||
|globset| {
|
|
||||||
create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset)
|
|
||||||
.map_err(anyhow::Error::new)
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Ok(watcher) => self.watcher = Some(watcher),
|
|
||||||
Err(e) => log::error!("Failed to create watcher: {e}"),
|
|
||||||
}
|
|
||||||
config.watcher_change = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.obj_change {
|
|
||||||
*diff_state = Default::default();
|
|
||||||
if config.selected_obj.is_some() {
|
|
||||||
config.queue_build = true;
|
|
||||||
}
|
|
||||||
config.obj_change = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.modified.swap(false, Ordering::Relaxed) && config.rebuild_on_changes {
|
|
||||||
config.queue_build = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(result) = &diff_state.build {
|
|
||||||
if let Some((obj, _)) = &result.first_obj {
|
|
||||||
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
|
|
||||||
if file_modified(path, timestamp) {
|
|
||||||
config.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) {
|
if state.config_change {
|
||||||
config.queue_reload = true;
|
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, 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,17 +575,20 @@ impl App {
|
|||||||
|
|
||||||
// Don't clear `queue_build` if a build is running. A file may have been modified during
|
// 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.
|
// the build, so we'll start another build after the current one finishes.
|
||||||
if config.queue_build && config.selected_obj.is_some() && !jobs.is_running(Job::ObjDiff) {
|
if state.queue_build
|
||||||
jobs.push(start_build(ctx, ObjDiffConfig::from_config(config)));
|
&& state.config.selected_obj.is_some()
|
||||||
config.queue_build = false;
|
&& !jobs.is_running(Job::ObjDiff)
|
||||||
config.queue_reload = false;
|
{
|
||||||
} else if config.queue_reload && !jobs.is_running(Job::ObjDiff) {
|
start_build(ctx, jobs, create_objdiff_config(state));
|
||||||
let mut diff_config = ObjDiffConfig::from_config(config);
|
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
|
// Don't build, just reload the current files
|
||||||
diff_config.build_base = false;
|
diff_config.build_base = false;
|
||||||
diff_config.build_target = false;
|
diff_config.build_target = false;
|
||||||
jobs.push(start_build(ctx, diff_config));
|
start_build(ctx, jobs, diff_config);
|
||||||
config.queue_reload = false;
|
state.queue_reload = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if graphics_state.should_relaunch {
|
if graphics_state.should_relaunch {
|
||||||
@@ -453,7 +613,7 @@ impl eframe::App for App {
|
|||||||
|
|
||||||
self.pre_update(ctx);
|
self.pre_update(ctx);
|
||||||
|
|
||||||
let Self { config, appearance, view_state, .. } = self;
|
let Self { state, appearance, view_state, .. } = self;
|
||||||
let ViewState {
|
let ViewState {
|
||||||
jobs,
|
jobs,
|
||||||
config_state,
|
config_state,
|
||||||
@@ -469,12 +629,27 @@ impl eframe::App for App {
|
|||||||
show_arch_config,
|
show_arch_config,
|
||||||
show_debug,
|
show_debug,
|
||||||
show_graphics,
|
show_graphics,
|
||||||
|
show_jobs,
|
||||||
|
show_side_panel,
|
||||||
} = view_state;
|
} = view_state;
|
||||||
|
|
||||||
frame_history.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
|
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::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
||||||
egui::menu::bar(ui, |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| {
|
ui.menu_button("File", |ui| {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
if ui.button("Debug…").clicked() {
|
if ui.button("Debug…").clicked() {
|
||||||
@@ -485,8 +660,8 @@ impl eframe::App for App {
|
|||||||
*show_project_config = !*show_project_config;
|
*show_project_config = !*show_project_config;
|
||||||
ui.close_menu();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
let recent_projects = if let Ok(guard) = config.read() {
|
let recent_projects = if let Ok(guard) = state.read() {
|
||||||
guard.recent_projects.clone()
|
guard.config.recent_projects.clone()
|
||||||
} else {
|
} else {
|
||||||
vec![]
|
vec![]
|
||||||
};
|
};
|
||||||
@@ -495,12 +670,12 @@ impl eframe::App for App {
|
|||||||
} else {
|
} else {
|
||||||
ui.menu_button("Recent Projects…", |ui| {
|
ui.menu_button("Recent Projects…", |ui| {
|
||||||
if ui.button("Clear").clicked() {
|
if ui.button("Clear").clicked() {
|
||||||
config.write().unwrap().recent_projects.clear();
|
state.write().unwrap().config.recent_projects.clear();
|
||||||
};
|
};
|
||||||
ui.separator();
|
ui.separator();
|
||||||
for path in recent_projects {
|
for path in recent_projects {
|
||||||
if ui.button(format!("{}", path.display())).clicked() {
|
if ui.button(format!("{}", path.display())).clicked() {
|
||||||
config.write().unwrap().set_project_dir(path);
|
state.write().unwrap().set_project_dir(path);
|
||||||
ui.close_menu();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -533,12 +708,12 @@ impl eframe::App for App {
|
|||||||
*show_arch_config = !*show_arch_config;
|
*show_arch_config = !*show_arch_config;
|
||||||
ui.close_menu();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
let mut config = config.write().unwrap();
|
let mut state = state.write().unwrap();
|
||||||
let response = ui
|
let response = ui
|
||||||
.checkbox(&mut config.rebuild_on_changes, "Rebuild on changes")
|
.checkbox(&mut state.config.rebuild_on_changes, "Rebuild on changes")
|
||||||
.on_hover_text("Automatically re-run the build & diff when files change.");
|
.on_hover_text("Automatically re-run the build & diff when files change.");
|
||||||
if response.changed() {
|
if response.changed() {
|
||||||
config.watcher_change = true;
|
state.watcher_change = true;
|
||||||
};
|
};
|
||||||
ui.add_enabled(
|
ui.add_enabled(
|
||||||
!diff_state.symbol_state.disable_reverse_fn_order,
|
!diff_state.symbol_state.disable_reverse_fn_order,
|
||||||
@@ -552,121 +727,65 @@ impl eframe::App for App {
|
|||||||
&mut diff_state.symbol_state.show_hidden_symbols,
|
&mut diff_state.symbol_state.show_hidden_symbols,
|
||||||
"Show hidden symbols",
|
"Show hidden symbols",
|
||||||
);
|
);
|
||||||
if ui
|
ui.separator();
|
||||||
.checkbox(
|
general_config_ui(ui, &mut state);
|
||||||
&mut config.diff_obj_config.relax_reloc_diffs,
|
ui.separator();
|
||||||
"Relax relocation diffs",
|
if ui.button("Clear custom symbol mappings").clicked() {
|
||||||
)
|
state.clear_mappings();
|
||||||
.on_hover_text(
|
diff_state.post_build_nav = Some(DiffViewNavigation::symbol_diff());
|
||||||
"Ignores differences in relocation targets. (Address, name, etc)",
|
state.queue_reload = true;
|
||||||
)
|
|
||||||
.changed()
|
|
||||||
{
|
|
||||||
config.queue_reload = true;
|
|
||||||
}
|
|
||||||
if ui
|
|
||||||
.checkbox(
|
|
||||||
&mut config.diff_obj_config.space_between_args,
|
|
||||||
"Space between args",
|
|
||||||
)
|
|
||||||
.changed()
|
|
||||||
{
|
|
||||||
config.queue_reload = true;
|
|
||||||
}
|
|
||||||
if ui
|
|
||||||
.checkbox(
|
|
||||||
&mut config.diff_obj_config.combine_data_sections,
|
|
||||||
"Combine data sections",
|
|
||||||
)
|
|
||||||
.on_hover_text("Combines data sections with equal names.")
|
|
||||||
.changed()
|
|
||||||
{
|
|
||||||
config.queue_reload = true;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
ui.separator();
|
||||||
|
if jobs_menu_ui(ui, jobs, appearance) {
|
||||||
|
*show_jobs = !*show_jobs;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
let build_success = matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success);
|
if side_panel_available {
|
||||||
if diff_state.current_view == View::FunctionDiff && build_success {
|
egui::SidePanel::left("side_panel").show_animated(ctx, *show_side_panel, |ui| {
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
|
||||||
function_diff_ui(ui, diff_state, appearance);
|
|
||||||
});
|
|
||||||
} else if diff_state.current_view == View::DataDiff && build_success {
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
|
||||||
data_diff_ui(ui, diff_state, appearance);
|
|
||||||
});
|
|
||||||
} else if diff_state.current_view == View::ExtabDiff && build_success {
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
|
||||||
extab_diff_ui(ui, diff_state, appearance);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
egui::SidePanel::left("side_panel").show(ctx, |ui| {
|
|
||||||
egui::ScrollArea::both().show(ui, |ui| {
|
egui::ScrollArea::both().show(ui, |ui| {
|
||||||
config_ui(ui, config, show_project_config, config_state, appearance);
|
config_ui(ui, state, show_project_config, config_state, appearance);
|
||||||
jobs_ui(ui, jobs, appearance);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
|
||||||
symbol_diff_ui(ui, diff_state, appearance);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
project_window(ctx, config, show_project_config, config_state, appearance);
|
let mut action = None;
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
let build_success = matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success);
|
||||||
|
action = if diff_state.current_view == View::FunctionDiff && build_success {
|
||||||
|
function_diff_ui(ui, diff_state, appearance)
|
||||||
|
} else if diff_state.current_view == View::DataDiff && build_success {
|
||||||
|
data_diff_ui(ui, diff_state, appearance)
|
||||||
|
} else if diff_state.current_view == View::ExtabDiff && build_success {
|
||||||
|
extab_diff_ui(ui, diff_state, appearance)
|
||||||
|
} else {
|
||||||
|
symbol_diff_ui(ui, diff_state, appearance)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
project_window(ctx, state, show_project_config, config_state, appearance);
|
||||||
appearance_window(ctx, show_appearance_config, appearance);
|
appearance_window(ctx, show_appearance_config, appearance);
|
||||||
demangle_window(ctx, show_demangle, demangle_state, appearance);
|
demangle_window(ctx, show_demangle, demangle_state, appearance);
|
||||||
rlwinm_decode_window(ctx, show_rlwinm_decode, rlwinm_decode_state, appearance);
|
rlwinm_decode_window(ctx, show_rlwinm_decode, rlwinm_decode_state, appearance);
|
||||||
arch_config_window(ctx, config, show_arch_config, appearance);
|
arch_config_window(ctx, state, show_arch_config, appearance);
|
||||||
debug_window(ctx, show_debug, frame_history, appearance);
|
debug_window(ctx, show_debug, frame_history, appearance);
|
||||||
graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance);
|
graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance);
|
||||||
|
jobs_window(ctx, show_jobs, jobs, appearance);
|
||||||
|
|
||||||
self.post_update(ctx);
|
self.post_update(ctx, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called by the frame work to save state before shutdown.
|
/// Called by the framework to save state before shutdown.
|
||||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||||
if let Ok(config) = self.config.read() {
|
if let Ok(state) = self.state.read() {
|
||||||
eframe::set_value(storage, CONFIG_KEY, &*config);
|
eframe::set_value(storage, CONFIG_KEY, &state.config);
|
||||||
}
|
}
|
||||||
eframe::set_value(storage, APPEARANCE_KEY, &self.appearance);
|
eframe::set_value(storage, APPEARANCE_KEY, &self.appearance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_watcher(
|
|
||||||
ctx: egui::Context,
|
|
||||||
modified: Arc<AtomicBool>,
|
|
||||||
project_dir: &Path,
|
|
||||||
patterns: GlobSet,
|
|
||||||
) -> notify::Result<notify::RecommendedWatcher> {
|
|
||||||
let base_dir = project_dir.to_owned();
|
|
||||||
let mut watcher =
|
|
||||||
notify::recommended_watcher(move |res: notify::Result<notify::Event>| match res {
|
|
||||||
Ok(event) => {
|
|
||||||
if matches!(
|
|
||||||
event.kind,
|
|
||||||
notify::EventKind::Modify(..)
|
|
||||||
| notify::EventKind::Create(..)
|
|
||||||
| notify::EventKind::Remove(..)
|
|
||||||
) {
|
|
||||||
for path in &event.paths {
|
|
||||||
let Ok(path) = path.strip_prefix(&base_dir) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if patterns.is_match(path) {
|
|
||||||
log::info!("File modified: {}", path.display());
|
|
||||||
modified.store(true, Ordering::Relaxed);
|
|
||||||
ctx.request_repaint();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => log::error!("watch error: {e:?}"),
|
|
||||||
})?;
|
|
||||||
watcher.watch(project_dir, RecursiveMode::Recursive)?;
|
|
||||||
Ok(watcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn file_modified(path: &Path, last_ts: FileTime) -> bool {
|
fn file_modified(path: &Path, last_ts: FileTime) -> bool {
|
||||||
if let Ok(metadata) = fs::metadata(path) {
|
if let Ok(metadata) = fs::metadata(path) {
|
||||||
|
|||||||
@@ -2,6 +2,13 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use eframe::Storage;
|
use eframe::Storage;
|
||||||
use globset::Glob;
|
use globset::Glob;
|
||||||
|
use objdiff_core::{
|
||||||
|
config::{ScratchConfig, SymbolMappings},
|
||||||
|
diff::{
|
||||||
|
ArmArchVersion, ArmR9Usage, DiffObjConfig, FunctionRelocDiffs, MipsAbi, MipsInstrCategory,
|
||||||
|
X86Formatter,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY};
|
use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY};
|
||||||
|
|
||||||
@@ -11,7 +18,7 @@ pub struct AppConfigVersion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AppConfigVersion {
|
impl Default for AppConfigVersion {
|
||||||
fn default() -> Self { Self { version: 1 } }
|
fn default() -> Self { Self { version: 3 } }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deserialize the AppConfig from storage, handling upgrades from older versions.
|
/// Deserialize the AppConfig from storage, handling upgrades from older versions.
|
||||||
@@ -19,7 +26,9 @@ pub fn deserialize_config(storage: &dyn Storage) -> Option<AppConfig> {
|
|||||||
let str = storage.get_string(CONFIG_KEY)?;
|
let str = storage.get_string(CONFIG_KEY)?;
|
||||||
match ron::from_str::<AppConfigVersion>(&str) {
|
match ron::from_str::<AppConfigVersion>(&str) {
|
||||||
Ok(version) => match version.version {
|
Ok(version) => match version.version {
|
||||||
1 => from_str::<AppConfig>(&str),
|
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);
|
log::warn!("Unknown config version: {}", version.version);
|
||||||
None
|
None
|
||||||
@@ -44,6 +53,297 @@ where T: serde::de::DeserializeOwned {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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<PathBuf>,
|
||||||
|
#[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,
|
||||||
|
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<PathBuf>,
|
||||||
|
pub base_path: Option<PathBuf>,
|
||||||
|
pub reverse_fn_order: Option<bool>,
|
||||||
|
pub complete: Option<bool>,
|
||||||
|
pub scratch: Option<ScratchConfigV2>,
|
||||||
|
pub source_path: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub symbol_mappings: SymbolMappings,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectConfigV2 {
|
||||||
|
fn into_config(self) -> ObjectConfig {
|
||||||
|
ObjectConfig {
|
||||||
|
name: self.name,
|
||||||
|
target_path: self.target_path,
|
||||||
|
base_path: self.base_path,
|
||||||
|
reverse_fn_order: self.reverse_fn_order,
|
||||||
|
complete: self.complete,
|
||||||
|
scratch: self.scratch.map(|scratch| scratch.into_config()),
|
||||||
|
source_path: self.source_path,
|
||||||
|
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<PathBuf>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub target_obj_dir: Option<PathBuf>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub base_obj_dir: Option<PathBuf>,
|
||||||
|
#[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<PathBuf>,
|
||||||
|
#[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,
|
||||||
|
target_obj_dir: self.target_obj_dir,
|
||||||
|
base_obj_dir: self.base_obj_dir,
|
||||||
|
selected_obj: self.selected_obj.map(|obj| obj.into_config()),
|
||||||
|
build_base: self.build_base,
|
||||||
|
build_target: self.build_target,
|
||||||
|
rebuild_on_changes: self.rebuild_on_changes,
|
||||||
|
auto_update_check: self.auto_update_check,
|
||||||
|
watch_patterns: self.watch_patterns,
|
||||||
|
recent_projects: self.recent_projects,
|
||||||
|
diff_obj_config: self.diff_obj_config.into_config(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct ScratchConfigV1 {
|
||||||
|
#[serde(default)]
|
||||||
|
pub platform: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub compiler: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub c_flags: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub ctx_path: Option<PathBuf>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub build_ctx: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScratchConfigV1 {
|
||||||
|
fn into_config(self) -> ScratchConfig {
|
||||||
|
ScratchConfig {
|
||||||
|
platform: self.platform,
|
||||||
|
compiler: self.compiler,
|
||||||
|
c_flags: self.c_flags,
|
||||||
|
ctx_path: self.ctx_path,
|
||||||
|
build_ctx: self.build_ctx.then_some(true),
|
||||||
|
preset_id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct ObjectConfigV1 {
|
||||||
|
pub name: String,
|
||||||
|
pub target_path: Option<PathBuf>,
|
||||||
|
pub base_path: Option<PathBuf>,
|
||||||
|
pub reverse_fn_order: Option<bool>,
|
||||||
|
pub complete: Option<bool>,
|
||||||
|
pub scratch: Option<ScratchConfigV1>,
|
||||||
|
pub source_path: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectConfigV1 {
|
||||||
|
fn into_config(self) -> ObjectConfig {
|
||||||
|
ObjectConfig {
|
||||||
|
name: self.name,
|
||||||
|
target_path: self.target_path,
|
||||||
|
base_path: self.base_path,
|
||||||
|
reverse_fn_order: self.reverse_fn_order,
|
||||||
|
complete: self.complete,
|
||||||
|
scratch: self.scratch.map(|scratch| scratch.into_config()),
|
||||||
|
source_path: self.source_path,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct DiffObjConfigV1 {
|
||||||
|
pub relax_reloc_diffs: bool,
|
||||||
|
#[serde(default = "bool_true")]
|
||||||
|
pub space_between_args: bool,
|
||||||
|
pub combine_data_sections: bool,
|
||||||
|
// x86
|
||||||
|
pub x86_formatter: X86Formatter,
|
||||||
|
// MIPS
|
||||||
|
pub mips_abi: MipsAbi,
|
||||||
|
pub mips_instr_category: MipsInstrCategory,
|
||||||
|
// ARM
|
||||||
|
pub arm_arch_version: ArmArchVersion,
|
||||||
|
pub arm_unified_syntax: bool,
|
||||||
|
pub arm_av_registers: bool,
|
||||||
|
pub arm_r9_usage: ArmR9Usage,
|
||||||
|
pub arm_sl_usage: bool,
|
||||||
|
pub arm_fp_usage: bool,
|
||||||
|
pub arm_ip_usage: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DiffObjConfigV1 {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
relax_reloc_diffs: false,
|
||||||
|
space_between_args: true,
|
||||||
|
combine_data_sections: false,
|
||||||
|
x86_formatter: Default::default(),
|
||||||
|
mips_abi: Default::default(),
|
||||||
|
mips_instr_category: Default::default(),
|
||||||
|
arm_arch_version: Default::default(),
|
||||||
|
arm_unified_syntax: true,
|
||||||
|
arm_av_registers: false,
|
||||||
|
arm_r9_usage: Default::default(),
|
||||||
|
arm_sl_usage: false,
|
||||||
|
arm_fp_usage: false,
|
||||||
|
arm_ip_usage: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiffObjConfigV1 {
|
||||||
|
fn into_config(self) -> DiffObjConfig {
|
||||||
|
DiffObjConfig {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn bool_true() -> bool { true }
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct AppConfigV1 {
|
||||||
|
pub version: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub custom_make: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub custom_args: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub selected_wsl_distro: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub project_dir: Option<PathBuf>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub target_obj_dir: Option<PathBuf>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub base_obj_dir: Option<PathBuf>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub selected_obj: Option<ObjectConfigV1>,
|
||||||
|
#[serde(default = "bool_true")]
|
||||||
|
pub build_base: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub build_target: bool,
|
||||||
|
#[serde(default = "bool_true")]
|
||||||
|
pub rebuild_on_changes: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub auto_update_check: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub watch_patterns: Vec<Glob>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub recent_projects: Vec<PathBuf>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub diff_obj_config: DiffObjConfigV1,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppConfigV1 {
|
||||||
|
fn into_config(self) -> AppConfig {
|
||||||
|
log::info!("Upgrading configuration from v1");
|
||||||
|
AppConfig {
|
||||||
|
custom_make: self.custom_make,
|
||||||
|
custom_args: self.custom_args,
|
||||||
|
selected_wsl_distro: self.selected_wsl_distro,
|
||||||
|
project_dir: self.project_dir,
|
||||||
|
target_obj_dir: self.target_obj_dir,
|
||||||
|
base_obj_dir: self.base_obj_dir,
|
||||||
|
selected_obj: self.selected_obj.map(|obj| obj.into_config()),
|
||||||
|
build_base: self.build_base,
|
||||||
|
build_target: self.build_target,
|
||||||
|
rebuild_on_changes: self.rebuild_on_changes,
|
||||||
|
auto_update_check: self.auto_update_check,
|
||||||
|
watch_patterns: self.watch_patterns,
|
||||||
|
recent_projects: self.recent_projects,
|
||||||
|
diff_obj_config: self.diff_obj_config.into_config(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize)]
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
pub struct ObjectConfigV0 {
|
pub struct ObjectConfigV0 {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -59,8 +359,7 @@ impl ObjectConfigV0 {
|
|||||||
target_path: Some(self.target_path),
|
target_path: Some(self.target_path),
|
||||||
base_path: Some(self.base_path),
|
base_path: Some(self.base_path),
|
||||||
reverse_fn_order: self.reverse_fn_order,
|
reverse_fn_order: self.reverse_fn_order,
|
||||||
complete: None,
|
..Default::default()
|
||||||
scratch: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,29 @@ use anyhow::Result;
|
|||||||
use globset::Glob;
|
use globset::Glob;
|
||||||
use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS};
|
use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS};
|
||||||
|
|
||||||
use crate::app::AppConfig;
|
use crate::app::{AppState, ObjectConfig};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum ProjectObjectNode {
|
pub enum ProjectObjectNode {
|
||||||
File(String, Box<ProjectObject>),
|
Unit(String, usize),
|
||||||
Dir(String, Vec<ProjectObjectNode>),
|
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>(
|
fn find_dir<'a>(
|
||||||
name: &str,
|
name: &str,
|
||||||
nodes: &'a mut Vec<ProjectObjectNode>,
|
nodes: &'a mut Vec<ProjectObjectNode>,
|
||||||
@@ -33,17 +48,18 @@ fn find_dir<'a>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_nodes(
|
fn build_nodes(
|
||||||
objects: &[ProjectObject],
|
units: &mut [ProjectObject],
|
||||||
project_dir: &Path,
|
project_dir: &Path,
|
||||||
target_obj_dir: Option<&Path>,
|
target_obj_dir: Option<&Path>,
|
||||||
base_obj_dir: Option<&Path>,
|
base_obj_dir: Option<&Path>,
|
||||||
) -> Vec<ProjectObjectNode> {
|
) -> Vec<ProjectObjectNode> {
|
||||||
let mut nodes = vec![];
|
let mut nodes = vec![];
|
||||||
for object in objects {
|
for (idx, unit) in units.iter_mut().enumerate() {
|
||||||
|
unit.resolve_paths(project_dir, target_obj_dir, base_obj_dir);
|
||||||
let mut out_nodes = &mut nodes;
|
let mut out_nodes = &mut nodes;
|
||||||
let path = if let Some(name) = &object.name {
|
let path = if let Some(name) = &unit.name {
|
||||||
Path::new(name)
|
Path::new(name)
|
||||||
} else if let Some(path) = &object.path {
|
} else if let Some(path) = &unit.path {
|
||||||
path
|
path
|
||||||
} else {
|
} else {
|
||||||
continue;
|
continue;
|
||||||
@@ -56,38 +72,62 @@ fn build_nodes(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut object = Box::new(object.clone());
|
|
||||||
object.resolve_paths(project_dir, target_obj_dir, base_obj_dir);
|
|
||||||
let filename = path.file_name().unwrap().to_str().unwrap().to_string();
|
let filename = path.file_name().unwrap().to_str().unwrap().to_string();
|
||||||
out_nodes.push(ProjectObjectNode::File(filename, object));
|
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
|
nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_project_config(config: &mut AppConfig) -> Result<()> {
|
pub fn load_project_config(state: &mut AppState) -> Result<()> {
|
||||||
let Some(project_dir) = &config.project_dir else {
|
let Some(project_dir) = &state.config.project_dir else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
if let Some((result, info)) = try_project_config(project_dir) {
|
if let Some((result, info)) = try_project_config(project_dir) {
|
||||||
let project_config = result?;
|
let project_config = result?;
|
||||||
config.custom_make = project_config.custom_make;
|
state.config.custom_make = project_config.custom_make.clone();
|
||||||
config.custom_args = project_config.custom_args;
|
state.config.custom_args = project_config.custom_args.clone();
|
||||||
config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p));
|
state.config.target_obj_dir =
|
||||||
config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p));
|
project_config.target_dir.as_deref().map(|p| project_dir.join(p));
|
||||||
config.build_base = project_config.build_base;
|
state.config.base_obj_dir = project_config.base_dir.as_deref().map(|p| project_dir.join(p));
|
||||||
config.build_target = project_config.build_target;
|
state.config.build_base = project_config.build_base.unwrap_or(true);
|
||||||
config.watch_patterns = project_config.watch_patterns.unwrap_or_else(|| {
|
state.config.build_target = project_config.build_target.unwrap_or(false);
|
||||||
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
|
if let Some(watch_patterns) = &project_config.watch_patterns {
|
||||||
});
|
state.config.watch_patterns = watch_patterns
|
||||||
config.watcher_change = true;
|
.iter()
|
||||||
config.objects = project_config.objects;
|
.map(|s| Glob::new(s))
|
||||||
config.object_nodes = build_nodes(
|
.collect::<Result<Vec<Glob>, globset::Error>>()?;
|
||||||
&config.objects,
|
} 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.clone().unwrap_or_default();
|
||||||
|
state.object_nodes = build_nodes(
|
||||||
|
&mut state.objects,
|
||||||
project_dir,
|
project_dir,
|
||||||
config.target_obj_dir.as_deref(),
|
state.config.target_obj_dir.as_deref(),
|
||||||
config.base_obj_dir.as_deref(),
|
state.config.base_obj_dir.as_deref(),
|
||||||
);
|
);
|
||||||
config.project_config_info = Some(info);
|
state.current_project_config = Some(project_config);
|
||||||
|
state.project_config_info = Some(info);
|
||||||
|
|
||||||
|
// Reload selected object
|
||||||
|
if let Some(selected_obj) = &state.config.selected_obj {
|
||||||
|
if let Some(obj) = state.objects.iter().find(|o| o.name() == selected_obj.name) {
|
||||||
|
let config = ObjectConfig::from(obj);
|
||||||
|
state.set_selected_obj(config);
|
||||||
|
} else {
|
||||||
|
state.clear_selected_obj();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ pub fn load_font_if_needed(
|
|||||||
let default_font = family.handles.get(family.default_index).unwrap();
|
let default_font = family.handles.get(family.default_index).unwrap();
|
||||||
let default_font_data = load_font(default_font).unwrap();
|
let default_font_data = load_font(default_font).unwrap();
|
||||||
log::info!("Loaded font family '{}'", family.family_name);
|
log::info!("Loaded font family '{}'", family.family_name);
|
||||||
fonts.font_data.insert(default_font_ref.full_name(), default_font_data.font_data);
|
fonts.font_data.insert(default_font_ref.full_name(), Arc::new(default_font_data.font_data));
|
||||||
fonts
|
fonts
|
||||||
.families
|
.families
|
||||||
.entry(egui::FontFamily::Name(Arc::from(family.family_name)))
|
.entry(egui::FontFamily::Name(Arc::from(family.family_name)))
|
||||||
|
|||||||
108
objdiff-gui/src/hotkeys.rs
Normal file
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))
|
||||||
|
}
|
||||||
141
objdiff-gui/src/jobs.rs
Normal file
141
objdiff-gui/src/jobs.rs
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
use std::{
|
||||||
|
sync::Arc,
|
||||||
|
task::{Wake, Waker},
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use jobs::create_scratch;
|
||||||
|
use objdiff_core::{
|
||||||
|
build::BuildConfig,
|
||||||
|
diff::MappingConfig,
|
||||||
|
jobs,
|
||||||
|
jobs::{check_update::CheckUpdateConfig, objdiff, update::UpdateConfig, Job, JobQueue},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::{AppConfig, AppState},
|
||||||
|
update::{build_updater, BIN_NAME_NEW, BIN_NAME_OLD},
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EguiWaker(egui::Context);
|
||||||
|
|
||||||
|
impl Wake for EguiWaker {
|
||||||
|
fn wake(self: Arc<Self>) { self.0.request_repaint(); }
|
||||||
|
|
||||||
|
fn wake_by_ref(self: &Arc<Self>) { self.0.request_repaint(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn egui_waker(ctx: &egui::Context) -> Waker { Waker::from(Arc::new(EguiWaker(ctx.clone()))) }
|
||||||
|
|
||||||
|
pub fn is_create_scratch_available(config: &AppConfig) -> bool {
|
||||||
|
let Some(selected_obj) = &config.selected_obj else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
selected_obj.target_path.is_some() && selected_obj.scratch.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_create_scratch(
|
||||||
|
ctx: &egui::Context,
|
||||||
|
jobs: &mut JobQueue,
|
||||||
|
state: &AppState,
|
||||||
|
function_name: String,
|
||||||
|
) {
|
||||||
|
match create_scratch_config(state, function_name) {
|
||||||
|
Ok(config) => {
|
||||||
|
jobs.push_once(Job::CreateScratch, || {
|
||||||
|
create_scratch::start_create_scratch(egui_waker(ctx), config)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("Failed to create scratch config: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_scratch_config(
|
||||||
|
state: &AppState,
|
||||||
|
function_name: String,
|
||||||
|
) -> Result<create_scratch::CreateScratchConfig> {
|
||||||
|
let Some(selected_obj) = &state.config.selected_obj else {
|
||||||
|
bail!("No object selected");
|
||||||
|
};
|
||||||
|
let Some(target_path) = &selected_obj.target_path else {
|
||||||
|
bail!("No target path for {}", selected_obj.name);
|
||||||
|
};
|
||||||
|
let Some(scratch_config) = &selected_obj.scratch else {
|
||||||
|
bail!("No scratch configuration for {}", selected_obj.name);
|
||||||
|
};
|
||||||
|
Ok(create_scratch::CreateScratchConfig {
|
||||||
|
build_config: BuildConfig::from(&state.config),
|
||||||
|
context_path: scratch_config.ctx_path.clone(),
|
||||||
|
build_context: scratch_config.build_ctx.unwrap_or(false),
|
||||||
|
compiler: scratch_config.compiler.clone().unwrap_or_default(),
|
||||||
|
platform: scratch_config.platform.clone().unwrap_or_default(),
|
||||||
|
compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(),
|
||||||
|
function_name,
|
||||||
|
target_obj: target_path.to_path_buf(),
|
||||||
|
preset_id: scratch_config.preset_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&AppConfig> for BuildConfig {
|
||||||
|
fn from(config: &AppConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
project_dir: config.project_dir.clone(),
|
||||||
|
custom_make: config.custom_make.clone(),
|
||||||
|
custom_args: config.custom_args.clone(),
|
||||||
|
selected_wsl_distro: config.selected_wsl_distro.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_objdiff_config(state: &AppState) -> objdiff::ObjDiffConfig {
|
||||||
|
objdiff::ObjDiffConfig {
|
||||||
|
build_config: BuildConfig::from(&state.config),
|
||||||
|
build_base: state.config.build_base,
|
||||||
|
build_target: state.config.build_target,
|
||||||
|
target_path: state
|
||||||
|
.config
|
||||||
|
.selected_obj
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|obj| obj.target_path.as_ref())
|
||||||
|
.cloned(),
|
||||||
|
base_path: state
|
||||||
|
.config
|
||||||
|
.selected_obj
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|obj| obj.base_path.as_ref())
|
||||||
|
.cloned(),
|
||||||
|
diff_obj_config: state.config.diff_obj_config.clone(),
|
||||||
|
mapping_config: MappingConfig {
|
||||||
|
mappings: state
|
||||||
|
.config
|
||||||
|
.selected_obj
|
||||||
|
.as_ref()
|
||||||
|
.map(|obj| &obj.symbol_mappings)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
selecting_left: state.selecting_left.clone(),
|
||||||
|
selecting_right: state.selecting_right.clone(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_build(ctx: &egui::Context, jobs: &mut JobQueue, config: objdiff::ObjDiffConfig) {
|
||||||
|
jobs.push_once(Job::ObjDiff, || objdiff::start_build(egui_waker(ctx), config));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_check_update(ctx: &egui::Context, jobs: &mut JobQueue) {
|
||||||
|
jobs.push_once(Job::Update, || {
|
||||||
|
jobs::check_update::start_check_update(egui_waker(ctx), CheckUpdateConfig {
|
||||||
|
build_updater,
|
||||||
|
bin_names: vec![BIN_NAME_NEW.to_string(), BIN_NAME_OLD.to_string()],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_update(ctx: &egui::Context, jobs: &mut JobQueue, bin_name: String) {
|
||||||
|
jobs.push_once(Job::Update, || {
|
||||||
|
jobs::update::start_update(egui_waker(ctx), UpdateConfig { build_updater, bin_name })
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
use std::sync::mpsc::Receiver;
|
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use self_update::{cargo_crate_version, update::Release};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
|
|
||||||
update::{build_updater, BIN_NAME_NEW, BIN_NAME_OLD},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct CheckUpdateResult {
|
|
||||||
pub update_available: bool,
|
|
||||||
pub latest_release: Release,
|
|
||||||
pub found_binary: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_check_update(context: &JobContext, cancel: Receiver<()>) -> Result<Box<CheckUpdateResult>> {
|
|
||||||
update_status(context, "Fetching latest release".to_string(), 0, 1, &cancel)?;
|
|
||||||
let updater = build_updater().context("Failed to create release updater")?;
|
|
||||||
let latest_release = updater.get_latest_release()?;
|
|
||||||
let update_available =
|
|
||||||
self_update::version::bump_is_greater(cargo_crate_version!(), &latest_release.version)?;
|
|
||||||
// Find the binary name in the release assets
|
|
||||||
let found_binary = latest_release
|
|
||||||
.assets
|
|
||||||
.iter()
|
|
||||||
.find(|a| a.name == BIN_NAME_NEW)
|
|
||||||
.or_else(|| latest_release.assets.iter().find(|a| a.name == BIN_NAME_OLD))
|
|
||||||
.map(|a| a.name.clone());
|
|
||||||
|
|
||||||
update_status(context, "Complete".to_string(), 1, 1, &cancel)?;
|
|
||||||
Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary }))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start_check_update(ctx: &egui::Context) -> JobState {
|
|
||||||
start_job(ctx, "Check for updates", Job::CheckUpdate, move |context, cancel| {
|
|
||||||
run_check_update(&context, cancel).map(|result| JobResult::CheckUpdate(Some(result)))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
use std::{
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
process::Command,
|
|
||||||
str::from_utf8,
|
|
||||||
sync::mpsc::Receiver,
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Error, Result};
|
|
||||||
use objdiff_core::{
|
|
||||||
diff::{diff_objs, DiffObjConfig, ObjDiff},
|
|
||||||
obj::{read, ObjInfo},
|
|
||||||
};
|
|
||||||
use time::OffsetDateTime;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
app::{AppConfig, ObjectConfig},
|
|
||||||
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct BuildStatus {
|
|
||||||
pub success: bool,
|
|
||||||
pub cmdline: String,
|
|
||||||
pub stdout: String,
|
|
||||||
pub stderr: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for BuildStatus {
|
|
||||||
fn default() -> Self {
|
|
||||||
BuildStatus {
|
|
||||||
success: true,
|
|
||||||
cmdline: String::new(),
|
|
||||||
stdout: String::new(),
|
|
||||||
stderr: String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct BuildConfig {
|
|
||||||
pub project_dir: Option<PathBuf>,
|
|
||||||
pub custom_make: Option<String>,
|
|
||||||
pub custom_args: Option<Vec<String>>,
|
|
||||||
#[allow(unused)]
|
|
||||||
pub selected_wsl_distro: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BuildConfig {
|
|
||||||
pub(crate) fn from_config(config: &AppConfig) -> Self {
|
|
||||||
Self {
|
|
||||||
project_dir: config.project_dir.clone(),
|
|
||||||
custom_make: config.custom_make.clone(),
|
|
||||||
custom_args: config.custom_args.clone(),
|
|
||||||
selected_wsl_distro: config.selected_wsl_distro.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ObjDiffConfig {
|
|
||||||
pub build_config: BuildConfig,
|
|
||||||
pub build_base: bool,
|
|
||||||
pub build_target: bool,
|
|
||||||
pub selected_obj: Option<ObjectConfig>,
|
|
||||||
pub diff_obj_config: DiffObjConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ObjDiffConfig {
|
|
||||||
pub(crate) fn from_config(config: &AppConfig) -> Self {
|
|
||||||
Self {
|
|
||||||
build_config: BuildConfig::from_config(config),
|
|
||||||
build_base: config.build_base,
|
|
||||||
build_target: config.build_target,
|
|
||||||
selected_obj: config.selected_obj.clone(),
|
|
||||||
diff_obj_config: config.diff_obj_config.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ObjDiffResult {
|
|
||||||
pub first_status: BuildStatus,
|
|
||||||
pub second_status: BuildStatus,
|
|
||||||
pub first_obj: Option<(ObjInfo, ObjDiff)>,
|
|
||||||
pub second_obj: Option<(ObjInfo, ObjDiff)>,
|
|
||||||
pub time: OffsetDateTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn run_make(config: &BuildConfig, arg: &Path) -> BuildStatus {
|
|
||||||
let Some(cwd) = &config.project_dir else {
|
|
||||||
return BuildStatus {
|
|
||||||
success: false,
|
|
||||||
stderr: "Missing project dir".to_string(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
};
|
|
||||||
match run_make_cmd(config, cwd, arg) {
|
|
||||||
Ok(status) => status,
|
|
||||||
Err(e) => BuildStatus { success: false, stderr: e.to_string(), ..Default::default() },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_make_cmd(config: &BuildConfig, cwd: &Path, arg: &Path) -> Result<BuildStatus> {
|
|
||||||
let make = config.custom_make.as_deref().unwrap_or("make");
|
|
||||||
let make_args = config.custom_args.as_deref().unwrap_or(&[]);
|
|
||||||
#[cfg(not(windows))]
|
|
||||||
let mut command = {
|
|
||||||
let mut command = Command::new(make);
|
|
||||||
command.current_dir(cwd).args(make_args).arg(arg);
|
|
||||||
command
|
|
||||||
};
|
|
||||||
#[cfg(windows)]
|
|
||||||
let mut command = {
|
|
||||||
use std::os::windows::process::CommandExt;
|
|
||||||
|
|
||||||
use path_slash::PathExt;
|
|
||||||
let mut command = if config.selected_wsl_distro.is_some() {
|
|
||||||
Command::new("wsl")
|
|
||||||
} else {
|
|
||||||
Command::new(make)
|
|
||||||
};
|
|
||||||
if let Some(distro) = &config.selected_wsl_distro {
|
|
||||||
// Strip distro root prefix \\wsl.localhost\{distro}
|
|
||||||
let wsl_path_prefix = format!("\\\\wsl.localhost\\{}", distro);
|
|
||||||
let cwd = match cwd.strip_prefix(wsl_path_prefix) {
|
|
||||||
Ok(new_cwd) => format!("/{}", new_cwd.to_slash_lossy().as_ref()),
|
|
||||||
Err(_) => cwd.to_string_lossy().to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
command
|
|
||||||
.arg("--cd")
|
|
||||||
.arg(cwd)
|
|
||||||
.arg("-d")
|
|
||||||
.arg(distro)
|
|
||||||
.arg("--")
|
|
||||||
.arg(make)
|
|
||||||
.args(make_args)
|
|
||||||
.arg(arg.to_slash_lossy().as_ref());
|
|
||||||
} else {
|
|
||||||
command.current_dir(cwd).args(make_args).arg(arg.to_slash_lossy().as_ref());
|
|
||||||
}
|
|
||||||
command.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
|
|
||||||
command
|
|
||||||
};
|
|
||||||
let mut cmdline = shell_escape::escape(command.get_program().to_string_lossy()).into_owned();
|
|
||||||
for arg in command.get_args() {
|
|
||||||
cmdline.push(' ');
|
|
||||||
cmdline.push_str(shell_escape::escape(arg.to_string_lossy()).as_ref());
|
|
||||||
}
|
|
||||||
let output = command.output().map_err(|e| anyhow!("Failed to execute build: {e}"))?;
|
|
||||||
let stdout = from_utf8(&output.stdout).context("Failed to process stdout")?;
|
|
||||||
let stderr = from_utf8(&output.stderr).context("Failed to process stderr")?;
|
|
||||||
Ok(BuildStatus {
|
|
||||||
success: output.status.code().unwrap_or(-1) == 0,
|
|
||||||
cmdline,
|
|
||||||
stdout: stdout.to_string(),
|
|
||||||
stderr: stderr.to_string(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_build(
|
|
||||||
context: &JobContext,
|
|
||||||
cancel: Receiver<()>,
|
|
||||||
config: ObjDiffConfig,
|
|
||||||
) -> Result<Box<ObjDiffResult>> {
|
|
||||||
let obj_config = config.selected_obj.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?;
|
|
||||||
let project_dir = config
|
|
||||||
.build_config
|
|
||||||
.project_dir
|
|
||||||
.as_ref()
|
|
||||||
.ok_or_else(|| Error::msg("Missing project dir"))?;
|
|
||||||
let target_path_rel = if let Some(target_path) = &obj_config.target_path {
|
|
||||||
Some(target_path.strip_prefix(project_dir).map_err(|_| {
|
|
||||||
anyhow!(
|
|
||||||
"Target path '{}' doesn't begin with '{}'",
|
|
||||||
target_path.display(),
|
|
||||||
project_dir.display()
|
|
||||||
)
|
|
||||||
})?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let base_path_rel = if let Some(base_path) = &obj_config.base_path {
|
|
||||||
Some(base_path.strip_prefix(project_dir).map_err(|_| {
|
|
||||||
anyhow!(
|
|
||||||
"Base path '{}' doesn't begin with '{}'",
|
|
||||||
base_path.display(),
|
|
||||||
project_dir.display()
|
|
||||||
)
|
|
||||||
})?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut total = 3;
|
|
||||||
if config.build_target && target_path_rel.is_some() {
|
|
||||||
total += 1;
|
|
||||||
}
|
|
||||||
if config.build_base && base_path_rel.is_some() {
|
|
||||||
total += 1;
|
|
||||||
}
|
|
||||||
let first_status = match target_path_rel {
|
|
||||||
Some(target_path_rel) if config.build_target => {
|
|
||||||
update_status(
|
|
||||||
context,
|
|
||||||
format!("Building target {}", target_path_rel.display()),
|
|
||||||
0,
|
|
||||||
total,
|
|
||||||
&cancel,
|
|
||||||
)?;
|
|
||||||
run_make(&config.build_config, target_path_rel)
|
|
||||||
}
|
|
||||||
_ => BuildStatus::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let second_status = match base_path_rel {
|
|
||||||
Some(base_path_rel) if config.build_base => {
|
|
||||||
update_status(
|
|
||||||
context,
|
|
||||||
format!("Building base {}", base_path_rel.display()),
|
|
||||||
0,
|
|
||||||
total,
|
|
||||||
&cancel,
|
|
||||||
)?;
|
|
||||||
run_make(&config.build_config, base_path_rel)
|
|
||||||
}
|
|
||||||
_ => BuildStatus::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let time = OffsetDateTime::now_utc();
|
|
||||||
|
|
||||||
let first_obj =
|
|
||||||
match &obj_config.target_path {
|
|
||||||
Some(target_path) if first_status.success => {
|
|
||||||
update_status(
|
|
||||||
context,
|
|
||||||
format!("Loading target {}", target_path_rel.unwrap().display()),
|
|
||||||
2,
|
|
||||||
total,
|
|
||||||
&cancel,
|
|
||||||
)?;
|
|
||||||
Some(read::read(target_path, &config.diff_obj_config).with_context(|| {
|
|
||||||
format!("Failed to read object '{}'", target_path.display())
|
|
||||||
})?)
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let second_obj = match &obj_config.base_path {
|
|
||||||
Some(base_path) if second_status.success => {
|
|
||||||
update_status(
|
|
||||||
context,
|
|
||||||
format!("Loading base {}", base_path_rel.unwrap().display()),
|
|
||||||
3,
|
|
||||||
total,
|
|
||||||
&cancel,
|
|
||||||
)?;
|
|
||||||
Some(
|
|
||||||
read::read(base_path, &config.diff_obj_config)
|
|
||||||
.with_context(|| format!("Failed to read object '{}'", base_path.display()))?,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
update_status(context, "Performing diff".to_string(), 4, total, &cancel)?;
|
|
||||||
let result = diff_objs(&config.diff_obj_config, first_obj.as_ref(), second_obj.as_ref(), None)?;
|
|
||||||
|
|
||||||
update_status(context, "Complete".to_string(), total, total, &cancel)?;
|
|
||||||
Ok(Box::new(ObjDiffResult {
|
|
||||||
first_status,
|
|
||||||
second_status,
|
|
||||||
first_obj: first_obj.and_then(|o| result.left.map(|d| (o, d))),
|
|
||||||
second_obj: second_obj.and_then(|o| result.right.map(|d| (o, d))),
|
|
||||||
time,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start_build(ctx: &egui::Context, config: ObjDiffConfig) -> JobState {
|
|
||||||
start_job(ctx, "Object diff", Job::ObjDiff, move |context, cancel| {
|
|
||||||
run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
#![warn(clippy::all, rust_2018_idioms)]
|
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
mod app_config;
|
mod app_config;
|
||||||
mod config;
|
mod config;
|
||||||
mod fonts;
|
mod fonts;
|
||||||
|
mod hotkeys;
|
||||||
mod jobs;
|
mod jobs;
|
||||||
mod update;
|
mod update;
|
||||||
mod views;
|
mod views;
|
||||||
@@ -19,6 +19,7 @@ use std::{
|
|||||||
use anyhow::{ensure, Result};
|
use anyhow::{ensure, Result};
|
||||||
use cfg_if::cfg_if;
|
use cfg_if::cfg_if;
|
||||||
use time::UtcOffset;
|
use time::UtcOffset;
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
use crate::views::graphics::{load_graphics_config, GraphicsBackend, GraphicsConfig};
|
use crate::views::graphics::{load_graphics_config, GraphicsBackend, GraphicsConfig};
|
||||||
|
|
||||||
@@ -40,7 +41,16 @@ const APP_NAME: &str = "objdiff";
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
// Log to stdout (if you run with `RUST_LOG=debug`).
|
// Log to stdout (if you run with `RUST_LOG=debug`).
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(
|
||||||
|
EnvFilter::builder()
|
||||||
|
// Default to info level
|
||||||
|
.with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into())
|
||||||
|
.from_env_lossy()
|
||||||
|
// This module is noisy at info level
|
||||||
|
.add_directive("wgpu_core::device::resource=warn".parse().unwrap()),
|
||||||
|
)
|
||||||
|
.init();
|
||||||
|
|
||||||
// Because localtime_r is unsound in multithreaded apps,
|
// Because localtime_r is unsound in multithreaded apps,
|
||||||
// we must call this before initializing eframe.
|
// we must call this before initializing eframe.
|
||||||
@@ -49,8 +59,10 @@ fn main() -> ExitCode {
|
|||||||
|
|
||||||
let app_path = std::env::current_exe().ok();
|
let app_path = std::env::current_exe().ok();
|
||||||
let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None));
|
let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None));
|
||||||
let mut native_options =
|
let mut native_options = eframe::NativeOptions {
|
||||||
eframe::NativeOptions { follow_system_theme: false, ..Default::default() };
|
viewport: egui::ViewportBuilder::default().with_app_id(APP_NAME),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
match load_icon() {
|
match load_icon() {
|
||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
native_options.viewport.icon = Some(Arc::new(data));
|
native_options.viewport.icon = Some(Arc::new(data));
|
||||||
@@ -76,14 +88,29 @@ fn main() -> ExitCode {
|
|||||||
}
|
}
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu")]
|
||||||
{
|
{
|
||||||
use eframe::egui_wgpu::wgpu::Backends;
|
use eframe::egui_wgpu::{wgpu::Backends, WgpuSetup};
|
||||||
if graphics_config.desired_backend.is_supported() {
|
if graphics_config.desired_backend.is_supported() {
|
||||||
native_options.wgpu_options.supported_backends = match graphics_config.desired_backend {
|
native_options.wgpu_options.wgpu_setup = match native_options.wgpu_options.wgpu_setup {
|
||||||
GraphicsBackend::Auto => native_options.wgpu_options.supported_backends,
|
WgpuSetup::CreateNew {
|
||||||
GraphicsBackend::Dx12 => Backends::DX12,
|
supported_backends: backends,
|
||||||
GraphicsBackend::Metal => Backends::METAL,
|
power_preference,
|
||||||
GraphicsBackend::Vulkan => Backends::VULKAN,
|
device_descriptor,
|
||||||
GraphicsBackend::OpenGL => Backends::GL,
|
} => {
|
||||||
|
let backend = match graphics_config.desired_backend {
|
||||||
|
GraphicsBackend::Auto => backends,
|
||||||
|
GraphicsBackend::Dx12 => Backends::DX12,
|
||||||
|
GraphicsBackend::Metal => Backends::METAL,
|
||||||
|
GraphicsBackend::Vulkan => Backends::VULKAN,
|
||||||
|
GraphicsBackend::OpenGL => Backends::GL,
|
||||||
|
};
|
||||||
|
WgpuSetup::CreateNew {
|
||||||
|
supported_backends: backend,
|
||||||
|
power_preference,
|
||||||
|
device_descriptor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// WgpuConfiguration::Default is CreateNew until we call run_eframe()
|
||||||
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,6 +127,8 @@ fn main() -> ExitCode {
|
|||||||
}
|
}
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu")]
|
||||||
if let Some(e) = eframe_error {
|
if let Some(e) = eframe_error {
|
||||||
|
use eframe::egui_wgpu::WgpuConfiguration;
|
||||||
|
|
||||||
// Attempt to relaunch using wgpu auto backend if the desired backend failed
|
// Attempt to relaunch using wgpu auto backend if the desired backend failed
|
||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
let mut should_relaunch = graphics_config.desired_backend != GraphicsBackend::Auto;
|
let mut should_relaunch = graphics_config.desired_backend != GraphicsBackend::Auto;
|
||||||
@@ -111,7 +140,7 @@ fn main() -> ExitCode {
|
|||||||
if should_relaunch {
|
if should_relaunch {
|
||||||
log::warn!("Failed to launch application: {e:?}");
|
log::warn!("Failed to launch application: {e:?}");
|
||||||
log::warn!("Attempting to relaunch using auto-detected backend");
|
log::warn!("Attempting to relaunch using auto-detected backend");
|
||||||
native_options.wgpu_options.supported_backends = Default::default();
|
native_options.wgpu_options.wgpu_setup = WgpuConfiguration::default().wgpu_setup;
|
||||||
if let Err(e) = run_eframe(
|
if let Err(e) = run_eframe(
|
||||||
native_options.clone(),
|
native_options.clone(),
|
||||||
utc_offset,
|
utc_offset,
|
||||||
@@ -189,14 +218,14 @@ fn run_eframe(
|
|||||||
APP_NAME,
|
APP_NAME,
|
||||||
native_options,
|
native_options,
|
||||||
Box::new(move |cc| {
|
Box::new(move |cc| {
|
||||||
Box::new(app::App::new(
|
Ok(Box::new(app::App::new(
|
||||||
cc,
|
cc,
|
||||||
utc_offset,
|
utc_offset,
|
||||||
exec_path_clone,
|
exec_path_clone,
|
||||||
app_path,
|
app_path,
|
||||||
graphics_config,
|
graphics_config,
|
||||||
graphics_config_path,
|
graphics_config_path,
|
||||||
))
|
)))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
use anyhow::Result;
|
||||||
use cfg_if::cfg_if;
|
use cfg_if::cfg_if;
|
||||||
use const_format::formatcp;
|
use const_format::formatcp;
|
||||||
|
use objdiff_core::jobs::update::self_update;
|
||||||
use self_update::{cargo_crate_version, update::ReleaseUpdate};
|
use self_update::{cargo_crate_version, update::ReleaseUpdate};
|
||||||
|
|
||||||
pub const OS: &str = std::env::consts::OS;
|
pub const OS: &str = std::env::consts::OS;
|
||||||
@@ -26,8 +28,8 @@ pub const BIN_NAME_OLD: &str = formatcp!("objdiff-{}-{}{}", OS, ARCH, std::env::
|
|||||||
pub const RELEASE_URL: &str =
|
pub const RELEASE_URL: &str =
|
||||||
formatcp!("https://github.com/{}/{}/releases/latest", GITHUB_USER, GITHUB_REPO);
|
formatcp!("https://github.com/{}/{}/releases/latest", GITHUB_USER, GITHUB_REPO);
|
||||||
|
|
||||||
pub fn build_updater() -> self_update::errors::Result<Box<dyn ReleaseUpdate>> {
|
pub fn build_updater() -> Result<Box<dyn ReleaseUpdate>> {
|
||||||
self_update::backends::github::Update::configure()
|
Ok(self_update::backends::github::Update::configure()
|
||||||
.repo_owner(GITHUB_USER)
|
.repo_owner(GITHUB_USER)
|
||||||
.repo_name(GITHUB_REPO)
|
.repo_name(GITHUB_REPO)
|
||||||
// bin_name is required, but unused?
|
// bin_name is required, but unused?
|
||||||
@@ -35,5 +37,5 @@ pub fn build_updater() -> self_update::errors::Result<Box<dyn ReleaseUpdate>> {
|
|||||||
.no_confirm(true)
|
.no_confirm(true)
|
||||||
.show_output(false)
|
.show_output(false)
|
||||||
.current_version(cargo_crate_version!())
|
.current_version(cargo_crate_version!())
|
||||||
.build()
|
.build()?)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ pub struct Appearance {
|
|||||||
pub ui_font: FontId,
|
pub ui_font: FontId,
|
||||||
pub code_font: FontId,
|
pub code_font: FontId,
|
||||||
pub diff_colors: Vec<Color32>,
|
pub diff_colors: Vec<Color32>,
|
||||||
pub theme: eframe::Theme,
|
pub theme: egui::Theme,
|
||||||
|
|
||||||
// Applied by theme
|
// Applied by theme
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
@@ -56,7 +56,7 @@ impl Default for Appearance {
|
|||||||
ui_font: DEFAULT_UI_FONT,
|
ui_font: DEFAULT_UI_FONT,
|
||||||
code_font: DEFAULT_CODE_FONT,
|
code_font: DEFAULT_CODE_FONT,
|
||||||
diff_colors: DEFAULT_COLOR_ROTATION.to_vec(),
|
diff_colors: DEFAULT_COLOR_ROTATION.to_vec(),
|
||||||
theme: eframe::Theme::Dark,
|
theme: egui::Theme::Dark,
|
||||||
text_color: Color32::GRAY,
|
text_color: Color32::GRAY,
|
||||||
emphasized_text_color: Color32::LIGHT_GRAY,
|
emphasized_text_color: Color32::LIGHT_GRAY,
|
||||||
deemphasized_text_color: Color32::DARK_GRAY,
|
deemphasized_text_color: Color32::DARK_GRAY,
|
||||||
@@ -98,7 +98,7 @@ impl Appearance {
|
|||||||
});
|
});
|
||||||
style.text_styles.insert(TextStyle::Monospace, self.code_font.clone());
|
style.text_styles.insert(TextStyle::Monospace, self.code_font.clone());
|
||||||
match self.theme {
|
match self.theme {
|
||||||
eframe::Theme::Dark => {
|
egui::Theme::Dark => {
|
||||||
style.visuals = egui::Visuals::dark();
|
style.visuals = egui::Visuals::dark();
|
||||||
self.text_color = Color32::GRAY;
|
self.text_color = Color32::GRAY;
|
||||||
self.emphasized_text_color = Color32::LIGHT_GRAY;
|
self.emphasized_text_color = Color32::LIGHT_GRAY;
|
||||||
@@ -108,7 +108,7 @@ impl Appearance {
|
|||||||
self.insert_color = Color32::GREEN;
|
self.insert_color = Color32::GREEN;
|
||||||
self.delete_color = Color32::from_rgb(200, 40, 41);
|
self.delete_color = Color32::from_rgb(200, 40, 41);
|
||||||
}
|
}
|
||||||
eframe::Theme::Light => {
|
egui::Theme::Light => {
|
||||||
style.visuals = egui::Visuals::light();
|
style.visuals = egui::Visuals::light();
|
||||||
self.text_color = Color32::GRAY;
|
self.text_color = Color32::GRAY;
|
||||||
self.emphasized_text_color = Color32::DARK_GRAY;
|
self.emphasized_text_color = Color32::DARK_GRAY;
|
||||||
@@ -205,7 +205,7 @@ pub const DEFAULT_COLOR_ROTATION: [Color32; 9] = [
|
|||||||
Color32::from_rgb(255, 0, 0),
|
Color32::from_rgb(255, 0, 0),
|
||||||
Color32::from_rgb(255, 255, 0),
|
Color32::from_rgb(255, 255, 0),
|
||||||
Color32::from_rgb(255, 192, 203),
|
Color32::from_rgb(255, 192, 203),
|
||||||
Color32::from_rgb(0, 0, 255),
|
Color32::from_rgb(128, 128, 255),
|
||||||
Color32::from_rgb(0, 255, 0),
|
Color32::from_rgb(0, 255, 0),
|
||||||
Color32::from_rgb(213, 138, 138),
|
Color32::from_rgb(213, 138, 138),
|
||||||
];
|
];
|
||||||
@@ -274,8 +274,8 @@ pub fn appearance_window(ctx: &egui::Context, show: &mut bool, appearance: &mut
|
|||||||
egui::ComboBox::from_label("Theme")
|
egui::ComboBox::from_label("Theme")
|
||||||
.selected_text(format!("{:?}", appearance.theme))
|
.selected_text(format!("{:?}", appearance.theme))
|
||||||
.show_ui(ui, |ui| {
|
.show_ui(ui, |ui| {
|
||||||
ui.selectable_value(&mut appearance.theme, eframe::Theme::Dark, "Dark");
|
ui.selectable_value(&mut appearance.theme, egui::Theme::Dark, "Dark");
|
||||||
ui.selectable_value(&mut appearance.theme, eframe::Theme::Light, "Light");
|
ui.selectable_value(&mut appearance.theme, egui::Theme::Light, "Light");
|
||||||
});
|
});
|
||||||
ui.separator();
|
ui.separator();
|
||||||
appearance.next_ui_font =
|
appearance.next_ui_font =
|
||||||
|
|||||||
82
objdiff-gui/src/views/column_layout.rs
Normal file
82
objdiff-gui/src/views/column_layout.rs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
use egui::{Align, Layout, Sense, Vec2};
|
||||||
|
use egui_extras::{Column, Size, StripBuilder, TableBuilder, TableRow};
|
||||||
|
|
||||||
|
pub fn render_header(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
available_width: f32,
|
||||||
|
num_columns: usize,
|
||||||
|
mut add_contents: impl FnMut(&mut egui::Ui, usize),
|
||||||
|
) {
|
||||||
|
let column_width = available_width / num_columns as f32;
|
||||||
|
ui.allocate_ui_with_layout(
|
||||||
|
Vec2 { x: available_width, y: 100.0 },
|
||||||
|
Layout::left_to_right(Align::Min),
|
||||||
|
|ui| {
|
||||||
|
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
|
||||||
|
for i in 0..num_columns {
|
||||||
|
ui.allocate_ui_with_layout(
|
||||||
|
Vec2 { x: column_width, y: 100.0 },
|
||||||
|
Layout::top_down(Align::Min),
|
||||||
|
|ui| {
|
||||||
|
ui.set_width(column_width);
|
||||||
|
add_contents(ui, i);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ui.separator();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_table(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
available_width: f32,
|
||||||
|
num_columns: usize,
|
||||||
|
row_height: f32,
|
||||||
|
total_rows: usize,
|
||||||
|
mut add_contents: impl FnMut(&mut TableRow, usize),
|
||||||
|
) {
|
||||||
|
ui.style_mut().interaction.selectable_labels = false;
|
||||||
|
let column_width = available_width / num_columns as f32;
|
||||||
|
let available_height = ui.available_height();
|
||||||
|
let table = TableBuilder::new(ui)
|
||||||
|
.striped(false)
|
||||||
|
.cell_layout(Layout::left_to_right(Align::Min))
|
||||||
|
.columns(Column::exact(column_width).clip(true), num_columns)
|
||||||
|
.resizable(false)
|
||||||
|
.auto_shrink([false, false])
|
||||||
|
.min_scrolled_height(available_height)
|
||||||
|
.sense(Sense::click());
|
||||||
|
table.body(|body| {
|
||||||
|
body.rows(row_height, total_rows, |mut row| {
|
||||||
|
row.set_hovered(false); // Disable hover effect
|
||||||
|
for i in 0..num_columns {
|
||||||
|
add_contents(&mut row, i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_strips(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
available_width: f32,
|
||||||
|
num_columns: usize,
|
||||||
|
mut add_contents: impl FnMut(&mut egui::Ui, usize),
|
||||||
|
) {
|
||||||
|
let column_width = available_width / num_columns as f32;
|
||||||
|
StripBuilder::new(ui).size(Size::remainder()).clip(true).vertical(|mut strip| {
|
||||||
|
strip.strip(|builder| {
|
||||||
|
builder.sizes(Size::exact(column_width), num_columns).clip(true).horizontal(
|
||||||
|
|mut strip| {
|
||||||
|
for i in 0..num_columns {
|
||||||
|
strip.cell(|ui| {
|
||||||
|
ui.push_id(i, |ui| {
|
||||||
|
add_contents(ui, i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,12 +2,11 @@
|
|||||||
use std::string::FromUtf16Error;
|
use std::string::FromUtf16Error;
|
||||||
use std::{
|
use std::{
|
||||||
mem::take,
|
mem::take,
|
||||||
path::{PathBuf, MAIN_SEPARATOR},
|
path::{Path, PathBuf, MAIN_SEPARATOR},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(all(windows, feature = "wsl"))]
|
#[cfg(all(windows, feature = "wsl"))]
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use const_format::formatcp;
|
|
||||||
use egui::{
|
use egui::{
|
||||||
output::OpenUrl, text::LayoutJob, CollapsingHeader, FontFamily, FontId, RichText,
|
output::OpenUrl, text::LayoutJob, CollapsingHeader, FontFamily, FontId, RichText,
|
||||||
SelectableLabel, TextFormat, Widget,
|
SelectableLabel, TextFormat, Widget,
|
||||||
@@ -15,19 +14,18 @@ use egui::{
|
|||||||
use globset::Glob;
|
use globset::Glob;
|
||||||
use objdiff_core::{
|
use objdiff_core::{
|
||||||
config::{ProjectObject, DEFAULT_WATCH_PATTERNS},
|
config::{ProjectObject, DEFAULT_WATCH_PATTERNS},
|
||||||
diff::{ArmArchVersion, ArmR9Usage, MipsAbi, MipsInstrCategory, X86Formatter},
|
diff::{
|
||||||
|
ConfigEnum, ConfigEnumVariantInfo, ConfigPropertyId, ConfigPropertyKind,
|
||||||
|
ConfigPropertyValue, CONFIG_GROUPS,
|
||||||
|
},
|
||||||
|
jobs::{check_update::CheckUpdateResult, Job, JobQueue, JobResult},
|
||||||
};
|
};
|
||||||
use self_update::cargo_crate_version;
|
|
||||||
use strum::{EnumMessage, VariantArray};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{AppConfig, AppConfigRef, ObjectConfig},
|
app::{AppConfig, AppState, AppStateRef, ObjectConfig},
|
||||||
config::ProjectObjectNode,
|
config::ProjectObjectNode,
|
||||||
jobs::{
|
hotkeys,
|
||||||
check_update::{start_check_update, CheckUpdateResult},
|
jobs::{start_check_update, start_update},
|
||||||
update::start_update,
|
|
||||||
Job, JobQueue, JobResult,
|
|
||||||
},
|
|
||||||
update::RELEASE_URL,
|
update::RELEASE_URL,
|
||||||
views::{
|
views::{
|
||||||
appearance::Appearance,
|
appearance::Appearance,
|
||||||
@@ -45,7 +43,6 @@ pub struct ConfigViewState {
|
|||||||
pub build_running: bool,
|
pub build_running: bool,
|
||||||
pub queue_build: bool,
|
pub queue_build: bool,
|
||||||
pub watch_pattern_text: String,
|
pub watch_pattern_text: String,
|
||||||
pub load_error: Option<String>,
|
|
||||||
pub object_search: String,
|
pub object_search: String,
|
||||||
pub filter_diffable: bool,
|
pub filter_diffable: bool,
|
||||||
pub filter_incomplete: bool,
|
pub filter_incomplete: bool,
|
||||||
@@ -56,7 +53,7 @@ pub struct ConfigViewState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigViewState {
|
impl ConfigViewState {
|
||||||
pub fn pre_update(&mut self, jobs: &mut JobQueue, config: &AppConfigRef) {
|
pub fn pre_update(&mut self, jobs: &mut JobQueue, state: &AppStateRef) {
|
||||||
jobs.results.retain_mut(|result| {
|
jobs.results.retain_mut(|result| {
|
||||||
if let JobResult::CheckUpdate(result) = result {
|
if let JobResult::CheckUpdate(result) = result {
|
||||||
self.check_update = take(result);
|
self.check_update = take(result);
|
||||||
@@ -73,21 +70,21 @@ impl ConfigViewState {
|
|||||||
match self.file_dialog_state.poll() {
|
match self.file_dialog_state.poll() {
|
||||||
FileDialogResult::None => {}
|
FileDialogResult::None => {}
|
||||||
FileDialogResult::ProjectDir(path) => {
|
FileDialogResult::ProjectDir(path) => {
|
||||||
let mut guard = config.write().unwrap();
|
let mut guard = state.write().unwrap();
|
||||||
guard.set_project_dir(path.to_path_buf());
|
guard.set_project_dir(path.to_path_buf());
|
||||||
}
|
}
|
||||||
FileDialogResult::TargetDir(path) => {
|
FileDialogResult::TargetDir(path) => {
|
||||||
let mut guard = config.write().unwrap();
|
let mut guard = state.write().unwrap();
|
||||||
guard.set_target_obj_dir(path.to_path_buf());
|
guard.set_target_obj_dir(path.to_path_buf());
|
||||||
}
|
}
|
||||||
FileDialogResult::BaseDir(path) => {
|
FileDialogResult::BaseDir(path) => {
|
||||||
let mut guard = config.write().unwrap();
|
let mut guard = state.write().unwrap();
|
||||||
guard.set_base_obj_dir(path.to_path_buf());
|
guard.set_base_obj_dir(path.to_path_buf());
|
||||||
}
|
}
|
||||||
FileDialogResult::Object(path) => {
|
FileDialogResult::Object(path) => {
|
||||||
let mut guard = config.write().unwrap();
|
let mut guard = state.write().unwrap();
|
||||||
if let (Some(base_dir), Some(target_dir)) =
|
if let (Some(base_dir), Some(target_dir)) =
|
||||||
(&guard.base_obj_dir, &guard.target_obj_dir)
|
(&guard.config.base_obj_dir, &guard.config.target_obj_dir)
|
||||||
{
|
{
|
||||||
if let Ok(obj_path) = path.strip_prefix(base_dir) {
|
if let Ok(obj_path) = path.strip_prefix(base_dir) {
|
||||||
let target_path = target_dir.join(obj_path);
|
let target_path = target_dir.join(obj_path);
|
||||||
@@ -95,9 +92,7 @@ impl ConfigViewState {
|
|||||||
name: obj_path.display().to_string(),
|
name: obj_path.display().to_string(),
|
||||||
target_path: Some(target_path),
|
target_path: Some(target_path),
|
||||||
base_path: Some(path),
|
base_path: Some(path),
|
||||||
reverse_fn_order: None,
|
..Default::default()
|
||||||
complete: None,
|
|
||||||
scratch: None,
|
|
||||||
});
|
});
|
||||||
} else if let Ok(obj_path) = path.strip_prefix(target_dir) {
|
} else if let Ok(obj_path) = path.strip_prefix(target_dir) {
|
||||||
let base_path = base_dir.join(obj_path);
|
let base_path = base_dir.join(obj_path);
|
||||||
@@ -105,9 +100,7 @@ impl ConfigViewState {
|
|||||||
name: obj_path.display().to_string(),
|
name: obj_path.display().to_string(),
|
||||||
target_path: Some(path),
|
target_path: Some(path),
|
||||||
base_path: Some(base_path),
|
base_path: Some(base_path),
|
||||||
reverse_fn_order: None,
|
..Default::default()
|
||||||
complete: None,
|
|
||||||
scratch: None,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,21 +108,21 @@ impl ConfigViewState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn post_update(&mut self, ctx: &egui::Context, jobs: &mut JobQueue, config: &AppConfigRef) {
|
pub fn post_update(&mut self, ctx: &egui::Context, jobs: &mut JobQueue, state: &AppStateRef) {
|
||||||
if self.queue_build {
|
if self.queue_build {
|
||||||
self.queue_build = false;
|
self.queue_build = false;
|
||||||
if let Ok(mut config) = config.write() {
|
if let Ok(mut state) = state.write() {
|
||||||
config.queue_build = true;
|
state.queue_build = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.queue_check_update {
|
if self.queue_check_update {
|
||||||
self.queue_check_update = false;
|
self.queue_check_update = false;
|
||||||
jobs.push_once(Job::CheckUpdate, || start_check_update(ctx));
|
start_check_update(ctx, jobs);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(bin_name) = self.queue_update.take() {
|
if let Some(bin_name) = self.queue_update.take() {
|
||||||
jobs.push_once(Job::Update, || start_update(ctx, bin_name));
|
start_update(ctx, jobs, bin_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,47 +162,43 @@ fn fetch_wsl2_distros() -> Vec<String> {
|
|||||||
|
|
||||||
pub fn config_ui(
|
pub fn config_ui(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
config: &AppConfigRef,
|
state: &AppStateRef,
|
||||||
show_config_window: &mut bool,
|
show_config_window: &mut bool,
|
||||||
state: &mut ConfigViewState,
|
config_state: &mut ConfigViewState,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
) {
|
) {
|
||||||
let mut config_guard = config.write().unwrap();
|
let mut state_guard = state.write().unwrap();
|
||||||
let AppConfig {
|
let AppState {
|
||||||
target_obj_dir,
|
config:
|
||||||
base_obj_dir,
|
AppConfig {
|
||||||
selected_obj,
|
project_dir, target_obj_dir, base_obj_dir, selected_obj, auto_update_check, ..
|
||||||
auto_update_check,
|
},
|
||||||
objects,
|
objects,
|
||||||
object_nodes,
|
object_nodes,
|
||||||
..
|
..
|
||||||
} = &mut *config_guard;
|
} = &mut *state_guard;
|
||||||
|
|
||||||
ui.heading("Updates");
|
ui.heading("Updates");
|
||||||
ui.checkbox(auto_update_check, "Check for updates on startup");
|
ui.checkbox(auto_update_check, "Check for updates on startup");
|
||||||
if ui.add_enabled(!state.check_update_running, egui::Button::new("Check now")).clicked() {
|
if ui.add_enabled(!config_state.check_update_running, egui::Button::new("Check now")).clicked()
|
||||||
state.queue_check_update = true;
|
{
|
||||||
|
config_state.queue_check_update = true;
|
||||||
}
|
}
|
||||||
ui.label(format!("Current version: {}", cargo_crate_version!())).on_hover_ui_at_pointer(|ui| {
|
ui.label(format!("Current version: {}", env!("CARGO_PKG_VERSION")));
|
||||||
ui.label(formatcp!("Git branch: {}", env!("VERGEN_GIT_BRANCH")));
|
if let Some(result) = &config_state.check_update {
|
||||||
ui.label(formatcp!("Git commit: {}", env!("VERGEN_GIT_SHA")));
|
|
||||||
ui.label(formatcp!("Build target: {}", env!("VERGEN_CARGO_TARGET_TRIPLE")));
|
|
||||||
ui.label(formatcp!("Debug: {}", env!("VERGEN_CARGO_DEBUG")));
|
|
||||||
});
|
|
||||||
if let Some(result) = &state.check_update {
|
|
||||||
ui.label(format!("Latest version: {}", result.latest_release.version));
|
ui.label(format!("Latest version: {}", result.latest_release.version));
|
||||||
if result.update_available {
|
if result.update_available {
|
||||||
ui.colored_label(appearance.insert_color, "Update available");
|
ui.colored_label(appearance.insert_color, "Update available");
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if let Some(bin_name) = &result.found_binary {
|
if let Some(bin_name) = &result.found_binary {
|
||||||
if ui
|
if ui
|
||||||
.add_enabled(!state.update_running, egui::Button::new("Automatic"))
|
.add_enabled(!config_state.update_running, egui::Button::new("Automatic"))
|
||||||
.on_hover_text_at_pointer(
|
.on_hover_text_at_pointer(
|
||||||
"Automatically download and replace the current build",
|
"Automatically download and replace the current build",
|
||||||
)
|
)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
state.queue_update = Some(bin_name.clone());
|
config_state.queue_update = Some(bin_name.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ui
|
if ui
|
||||||
@@ -234,11 +223,14 @@ pub fn config_ui(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut new_selected_obj = selected_obj.clone();
|
let selected_index = selected_obj.as_ref().and_then(|selected_obj| {
|
||||||
|
objects.iter().position(|obj| obj.name.as_ref() == Some(&selected_obj.name))
|
||||||
|
});
|
||||||
|
let mut new_selected_index = selected_index;
|
||||||
if objects.is_empty() {
|
if objects.is_empty() {
|
||||||
if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) {
|
if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) {
|
||||||
if ui.button("Select object").clicked() {
|
if ui.button("Select object").clicked() {
|
||||||
state.file_dialog_state.queue(
|
config_state.file_dialog_state.queue(
|
||||||
|| {
|
|| {
|
||||||
Box::pin(
|
Box::pin(
|
||||||
rfd::AsyncFileDialog::new()
|
rfd::AsyncFileDialog::new()
|
||||||
@@ -261,8 +253,12 @@ pub fn config_ui(
|
|||||||
ui.colored_label(appearance.delete_color, "Missing project settings");
|
ui.colored_label(appearance.delete_color, "Missing project settings");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let had_search = !state.object_search.is_empty();
|
let had_search = !config_state.object_search.is_empty();
|
||||||
egui::TextEdit::singleline(&mut state.object_search).hint_text("Filter").ui(ui);
|
let response =
|
||||||
|
egui::TextEdit::singleline(&mut config_state.object_search).hint_text("Filter").ui(ui);
|
||||||
|
if hotkeys::consume_object_filter_shortcut(ui.ctx()) {
|
||||||
|
response.request_focus();
|
||||||
|
}
|
||||||
|
|
||||||
let mut root_open = None;
|
let mut root_open = None;
|
||||||
let mut node_open = NodeOpen::Default;
|
let mut node_open = NodeOpen::Default;
|
||||||
@@ -284,19 +280,22 @@ pub fn config_ui(
|
|||||||
node_open = NodeOpen::Object;
|
node_open = NodeOpen::Object;
|
||||||
}
|
}
|
||||||
let mut filters_text = RichText::new("Filter ⏷");
|
let mut filters_text = RichText::new("Filter ⏷");
|
||||||
if state.filter_diffable || state.filter_incomplete || state.show_hidden {
|
if config_state.filter_diffable
|
||||||
|
|| config_state.filter_incomplete
|
||||||
|
|| config_state.show_hidden
|
||||||
|
{
|
||||||
filters_text = filters_text.color(appearance.replace_color);
|
filters_text = filters_text.color(appearance.replace_color);
|
||||||
}
|
}
|
||||||
egui::menu::menu_button(ui, filters_text, |ui| {
|
egui::menu::menu_button(ui, filters_text, |ui| {
|
||||||
ui.checkbox(&mut state.filter_diffable, "Diffable")
|
ui.checkbox(&mut config_state.filter_diffable, "Diffable")
|
||||||
.on_hover_text_at_pointer("Only show objects with a source file");
|
.on_hover_text_at_pointer("Only show objects with a source file");
|
||||||
ui.checkbox(&mut state.filter_incomplete, "Incomplete")
|
ui.checkbox(&mut config_state.filter_incomplete, "Incomplete")
|
||||||
.on_hover_text_at_pointer("Only show objects not marked complete");
|
.on_hover_text_at_pointer("Only show objects not marked complete");
|
||||||
ui.checkbox(&mut state.show_hidden, "Hidden")
|
ui.checkbox(&mut config_state.show_hidden, "Hidden")
|
||||||
.on_hover_text_at_pointer("Show hidden (auto-generated) objects");
|
.on_hover_text_at_pointer("Show hidden (auto-generated) objects");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if state.object_search.is_empty() {
|
if config_state.object_search.is_empty() {
|
||||||
if had_search {
|
if had_search {
|
||||||
root_open = Some(true);
|
root_open = Some(true);
|
||||||
node_open = NodeOpen::Object;
|
node_open = NodeOpen::Object;
|
||||||
@@ -313,45 +312,55 @@ pub fn config_ui(
|
|||||||
.open(root_open)
|
.open(root_open)
|
||||||
.default_open(true)
|
.default_open(true)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
let search = state.object_search.to_ascii_lowercase();
|
let search = config_state.object_search.to_ascii_lowercase();
|
||||||
ui.style_mut().wrap = Some(false);
|
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||||
for node in object_nodes.iter().filter_map(|node| {
|
for node in object_nodes.iter().filter_map(|node| {
|
||||||
filter_node(
|
filter_node(
|
||||||
|
objects,
|
||||||
node,
|
node,
|
||||||
&search,
|
&search,
|
||||||
state.filter_diffable,
|
config_state.filter_diffable,
|
||||||
state.filter_incomplete,
|
config_state.filter_incomplete,
|
||||||
state.show_hidden,
|
config_state.show_hidden,
|
||||||
)
|
)
|
||||||
}) {
|
}) {
|
||||||
display_node(ui, &mut new_selected_obj, &node, appearance, node_open);
|
display_node(
|
||||||
|
ui,
|
||||||
|
&mut new_selected_index,
|
||||||
|
project_dir.as_deref(),
|
||||||
|
objects,
|
||||||
|
&node,
|
||||||
|
appearance,
|
||||||
|
node_open,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if new_selected_obj != *selected_obj {
|
if new_selected_index != selected_index {
|
||||||
if let Some(obj) = new_selected_obj {
|
if let Some(idx) = new_selected_index {
|
||||||
// Will set obj_changed, which will trigger a rebuild
|
// Will set obj_changed, which will trigger a rebuild
|
||||||
config_guard.set_selected_obj(obj);
|
let config = ObjectConfig::from(&objects[idx]);
|
||||||
|
state_guard.set_selected_obj(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if config_guard.selected_obj.is_some()
|
if state_guard.config.selected_obj.is_some()
|
||||||
&& ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked()
|
&& ui.add_enabled(!config_state.build_running, egui::Button::new("Build")).clicked()
|
||||||
{
|
{
|
||||||
state.queue_build = true;
|
config_state.queue_build = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn display_object(
|
fn display_unit(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
selected_obj: &mut Option<ObjectConfig>,
|
selected_obj: &mut Option<usize>,
|
||||||
|
project_dir: Option<&Path>,
|
||||||
name: &str,
|
name: &str,
|
||||||
object: &ProjectObject,
|
units: &[ProjectObject],
|
||||||
|
index: usize,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
) {
|
) {
|
||||||
let object_name = object.name();
|
let object = &units[index];
|
||||||
let selected = matches!(selected_obj, Some(obj) if obj.name == object_name);
|
let selected = *selected_obj == Some(index);
|
||||||
let color = if selected {
|
let color = if selected {
|
||||||
appearance.emphasized_text_color
|
appearance.emphasized_text_color
|
||||||
} else if let Some(complete) = object.complete() {
|
} else if let Some(complete) = object.complete() {
|
||||||
@@ -363,7 +372,7 @@ fn display_object(
|
|||||||
} else {
|
} else {
|
||||||
appearance.text_color
|
appearance.text_color
|
||||||
};
|
};
|
||||||
let clicked = SelectableLabel::new(
|
let response = SelectableLabel::new(
|
||||||
selected,
|
selected,
|
||||||
RichText::new(name)
|
RichText::new(name)
|
||||||
.font(FontId {
|
.font(FontId {
|
||||||
@@ -372,19 +381,32 @@ fn display_object(
|
|||||||
})
|
})
|
||||||
.color(color),
|
.color(color),
|
||||||
)
|
)
|
||||||
.ui(ui)
|
.ui(ui);
|
||||||
.clicked();
|
if get_source_path(project_dir, object).is_some() {
|
||||||
// Always recreate ObjectConfig if selected, in case the project config changed.
|
response.context_menu(|ui| object_context_ui(ui, object, project_dir));
|
||||||
// ObjectConfig is compared using equality, so this won't unnecessarily trigger a rebuild.
|
}
|
||||||
if selected || clicked {
|
if response.clicked() {
|
||||||
*selected_obj = Some(ObjectConfig {
|
*selected_obj = Some(index);
|
||||||
name: object_name.to_string(),
|
}
|
||||||
target_path: object.target_path.clone(),
|
}
|
||||||
base_path: object.base_path.clone(),
|
|
||||||
reverse_fn_order: object.reverse_fn_order(),
|
fn get_source_path(project_dir: Option<&Path>, object: &ProjectObject) -> Option<PathBuf> {
|
||||||
complete: object.complete(),
|
project_dir.and_then(|dir| object.source_path().map(|path| dir.join(path)))
|
||||||
scratch: object.scratch.clone(),
|
}
|
||||||
});
|
|
||||||
|
fn object_context_ui(ui: &mut egui::Ui, object: &ProjectObject, project_dir: Option<&Path>) {
|
||||||
|
if let Some(source_path) = get_source_path(project_dir, object) {
|
||||||
|
if ui
|
||||||
|
.button("Open source file")
|
||||||
|
.on_hover_text("Open the source file in the default editor")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
log::info!("Opening file {}", source_path.display());
|
||||||
|
if let Err(e) = open::that_detached(&source_path) {
|
||||||
|
log::error!("Failed to open source file: {e}");
|
||||||
|
}
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,17 +421,19 @@ enum NodeOpen {
|
|||||||
|
|
||||||
fn display_node(
|
fn display_node(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
selected_obj: &mut Option<ObjectConfig>,
|
selected_obj: &mut Option<usize>,
|
||||||
|
project_dir: Option<&Path>,
|
||||||
|
units: &[ProjectObject],
|
||||||
node: &ProjectObjectNode,
|
node: &ProjectObjectNode,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
node_open: NodeOpen,
|
node_open: NodeOpen,
|
||||||
) {
|
) {
|
||||||
match node {
|
match node {
|
||||||
ProjectObjectNode::File(name, object) => {
|
ProjectObjectNode::Unit(name, idx) => {
|
||||||
display_object(ui, selected_obj, name, object, appearance);
|
display_unit(ui, selected_obj, project_dir, name, units, *idx, appearance);
|
||||||
}
|
}
|
||||||
ProjectObjectNode::Dir(name, children) => {
|
ProjectObjectNode::Dir(name, children) => {
|
||||||
let contains_obj = selected_obj.as_ref().map(|path| contains_node(node, path));
|
let contains_obj = selected_obj.map(|idx| contains_node(node, idx));
|
||||||
let open = match node_open {
|
let open = match node_open {
|
||||||
NodeOpen::Default => None,
|
NodeOpen::Default => None,
|
||||||
NodeOpen::Open => Some(true),
|
NodeOpen::Open => Some(true),
|
||||||
@@ -432,16 +456,16 @@ fn display_node(
|
|||||||
.open(open)
|
.open(open)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
for node in children {
|
for node in children {
|
||||||
display_node(ui, selected_obj, node, appearance, node_open);
|
display_node(ui, selected_obj, project_dir, units, node, appearance, node_open);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn contains_node(node: &ProjectObjectNode, selected_obj: &ObjectConfig) -> bool {
|
fn contains_node(node: &ProjectObjectNode, selected_obj: usize) -> bool {
|
||||||
match node {
|
match node {
|
||||||
ProjectObjectNode::File(_, object) => object.name() == selected_obj.name,
|
ProjectObjectNode::Unit(_, idx) => *idx == selected_obj,
|
||||||
ProjectObjectNode::Dir(_, children) => {
|
ProjectObjectNode::Dir(_, children) => {
|
||||||
children.iter().any(|node| contains_node(node, selected_obj))
|
children.iter().any(|node| contains_node(node, selected_obj))
|
||||||
}
|
}
|
||||||
@@ -449,6 +473,7 @@ fn contains_node(node: &ProjectObjectNode, selected_obj: &ObjectConfig) -> bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn filter_node(
|
fn filter_node(
|
||||||
|
units: &[ProjectObject],
|
||||||
node: &ProjectObjectNode,
|
node: &ProjectObjectNode,
|
||||||
search: &str,
|
search: &str,
|
||||||
filter_diffable: bool,
|
filter_diffable: bool,
|
||||||
@@ -456,12 +481,12 @@ fn filter_node(
|
|||||||
show_hidden: bool,
|
show_hidden: bool,
|
||||||
) -> Option<ProjectObjectNode> {
|
) -> Option<ProjectObjectNode> {
|
||||||
match node {
|
match node {
|
||||||
ProjectObjectNode::File(name, object) => {
|
ProjectObjectNode::Unit(name, idx) => {
|
||||||
|
let unit = &units[*idx];
|
||||||
if (search.is_empty() || name.to_ascii_lowercase().contains(search))
|
if (search.is_empty() || name.to_ascii_lowercase().contains(search))
|
||||||
&& (!filter_diffable
|
&& (!filter_diffable || (unit.base_path.is_some() && unit.target_path.is_some()))
|
||||||
|| (object.base_path.is_some() && object.target_path.is_some()))
|
&& (!filter_incomplete || matches!(unit.complete(), None | Some(false)))
|
||||||
&& (!filter_incomplete || matches!(object.complete(), None | Some(false)))
|
&& (show_hidden || !unit.hidden())
|
||||||
&& (show_hidden || !object.hidden())
|
|
||||||
{
|
{
|
||||||
Some(node.clone())
|
Some(node.clone())
|
||||||
} else {
|
} else {
|
||||||
@@ -472,7 +497,14 @@ fn filter_node(
|
|||||||
let new_children = children
|
let new_children = children
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|child| {
|
.filter_map(|child| {
|
||||||
filter_node(child, search, filter_diffable, filter_incomplete, show_hidden)
|
filter_node(
|
||||||
|
units,
|
||||||
|
child,
|
||||||
|
search,
|
||||||
|
filter_diffable,
|
||||||
|
filter_incomplete,
|
||||||
|
show_hidden,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
if !new_children.is_empty() {
|
if !new_children.is_empty() {
|
||||||
@@ -530,33 +562,33 @@ fn pick_folder_ui(
|
|||||||
|
|
||||||
pub fn project_window(
|
pub fn project_window(
|
||||||
ctx: &egui::Context,
|
ctx: &egui::Context,
|
||||||
config: &AppConfigRef,
|
state: &AppStateRef,
|
||||||
show: &mut bool,
|
show: &mut bool,
|
||||||
state: &mut ConfigViewState,
|
config_state: &mut ConfigViewState,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
) {
|
) {
|
||||||
let mut config_guard = config.write().unwrap();
|
let mut state_guard = state.write().unwrap();
|
||||||
|
|
||||||
egui::Window::new("Project").open(show).show(ctx, |ui| {
|
egui::Window::new("Project").open(show).show(ctx, |ui| {
|
||||||
split_obj_config_ui(ui, &mut config_guard, state, appearance);
|
split_obj_config_ui(ui, &mut state_guard, config_state, appearance);
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(error) = &state.load_error {
|
if let Some(error) = &state_guard.config_error {
|
||||||
let mut open = true;
|
let mut open = true;
|
||||||
egui::Window::new("Error").open(&mut open).show(ctx, |ui| {
|
egui::Window::new("Error").open(&mut open).show(ctx, |ui| {
|
||||||
ui.label("Failed to load project config:");
|
ui.label("Failed to load project config:");
|
||||||
ui.colored_label(appearance.delete_color, error);
|
ui.colored_label(appearance.delete_color, error);
|
||||||
});
|
});
|
||||||
if !open {
|
if !open {
|
||||||
state.load_error = None;
|
state_guard.config_error = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn split_obj_config_ui(
|
fn split_obj_config_ui(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
config: &mut AppConfig,
|
state: &mut AppState,
|
||||||
state: &mut ConfigViewState,
|
config_state: &mut ConfigViewState,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
) {
|
) {
|
||||||
let text_format = TextFormat::simple(appearance.ui_font.clone(), appearance.text_color);
|
let text_format = TextFormat::simple(appearance.ui_font.clone(), appearance.text_color);
|
||||||
@@ -567,7 +599,7 @@ fn split_obj_config_ui(
|
|||||||
|
|
||||||
let response = pick_folder_ui(
|
let response = pick_folder_ui(
|
||||||
ui,
|
ui,
|
||||||
&config.project_dir,
|
&state.config.project_dir,
|
||||||
"Project directory",
|
"Project directory",
|
||||||
|ui| {
|
|ui| {
|
||||||
let mut job = LayoutJob::default();
|
let mut job = LayoutJob::default();
|
||||||
@@ -583,7 +615,7 @@ fn split_obj_config_ui(
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
if response.clicked() {
|
if response.clicked() {
|
||||||
state.file_dialog_state.queue(
|
config_state.file_dialog_state.queue(
|
||||||
|| Box::pin(rfd::AsyncFileDialog::new().pick_folder()),
|
|| Box::pin(rfd::AsyncFileDialog::new().pick_folder()),
|
||||||
FileDialogResult::ProjectDir,
|
FileDialogResult::ProjectDir,
|
||||||
);
|
);
|
||||||
@@ -612,33 +644,35 @@ fn split_obj_config_ui(
|
|||||||
ui.label(job);
|
ui.label(job);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
let mut custom_make_str = config.custom_make.clone().unwrap_or_default();
|
let mut custom_make_str = state.config.custom_make.clone().unwrap_or_default();
|
||||||
if ui
|
if ui
|
||||||
.add_enabled(
|
.add_enabled(
|
||||||
config.project_config_info.is_none(),
|
state.project_config_info.is_none(),
|
||||||
egui::TextEdit::singleline(&mut custom_make_str).hint_text("make"),
|
egui::TextEdit::singleline(&mut custom_make_str).hint_text("make"),
|
||||||
)
|
)
|
||||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||||
.changed()
|
.changed()
|
||||||
{
|
{
|
||||||
if custom_make_str.is_empty() {
|
if custom_make_str.is_empty() {
|
||||||
config.custom_make = None;
|
state.config.custom_make = None;
|
||||||
} else {
|
} else {
|
||||||
config.custom_make = Some(custom_make_str);
|
state.config.custom_make = Some(custom_make_str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(all(windows, feature = "wsl"))]
|
#[cfg(all(windows, feature = "wsl"))]
|
||||||
{
|
{
|
||||||
if state.available_wsl_distros.is_none() {
|
if config_state.available_wsl_distros.is_none() {
|
||||||
state.available_wsl_distros = Some(fetch_wsl2_distros());
|
config_state.available_wsl_distros = Some(fetch_wsl2_distros());
|
||||||
}
|
}
|
||||||
egui::ComboBox::from_label("Run in WSL2")
|
egui::ComboBox::from_label("Run in WSL2")
|
||||||
.selected_text(config.selected_wsl_distro.as_ref().unwrap_or(&"Disabled".to_string()))
|
.selected_text(
|
||||||
|
state.config.selected_wsl_distro.as_ref().unwrap_or(&"Disabled".to_string()),
|
||||||
|
)
|
||||||
.show_ui(ui, |ui| {
|
.show_ui(ui, |ui| {
|
||||||
ui.selectable_value(&mut config.selected_wsl_distro, None, "Disabled");
|
ui.selectable_value(&mut state.config.selected_wsl_distro, None, "Disabled");
|
||||||
for distro in state.available_wsl_distros.as_ref().unwrap() {
|
for distro in config_state.available_wsl_distros.as_ref().unwrap() {
|
||||||
ui.selectable_value(
|
ui.selectable_value(
|
||||||
&mut config.selected_wsl_distro,
|
&mut state.config.selected_wsl_distro,
|
||||||
Some(distro.clone()),
|
Some(distro.clone()),
|
||||||
distro,
|
distro,
|
||||||
);
|
);
|
||||||
@@ -647,10 +681,10 @@ fn split_obj_config_ui(
|
|||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
if let Some(project_dir) = config.project_dir.clone() {
|
if let Some(project_dir) = state.config.project_dir.clone() {
|
||||||
let response = pick_folder_ui(
|
let response = pick_folder_ui(
|
||||||
ui,
|
ui,
|
||||||
&config.target_obj_dir,
|
&state.config.target_obj_dir,
|
||||||
"Target build directory",
|
"Target build directory",
|
||||||
|ui| {
|
|ui| {
|
||||||
let mut job = LayoutJob::default();
|
let mut job = LayoutJob::default();
|
||||||
@@ -667,17 +701,17 @@ fn split_obj_config_ui(
|
|||||||
ui.label(job);
|
ui.label(job);
|
||||||
},
|
},
|
||||||
appearance,
|
appearance,
|
||||||
config.project_config_info.is_none(),
|
state.project_config_info.is_none(),
|
||||||
);
|
);
|
||||||
if response.clicked() {
|
if response.clicked() {
|
||||||
state.file_dialog_state.queue(
|
config_state.file_dialog_state.queue(
|
||||||
|| Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()),
|
|| Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()),
|
||||||
FileDialogResult::TargetDir,
|
FileDialogResult::TargetDir,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ui.add_enabled(
|
ui.add_enabled(
|
||||||
config.project_config_info.is_none(),
|
state.project_config_info.is_none(),
|
||||||
egui::Checkbox::new(&mut config.build_target, "Build target objects"),
|
egui::Checkbox::new(&mut state.config.build_target, "Build target objects"),
|
||||||
)
|
)
|
||||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||||
.on_hover_ui(|ui| {
|
.on_hover_ui(|ui| {
|
||||||
@@ -711,7 +745,7 @@ fn split_obj_config_ui(
|
|||||||
|
|
||||||
let response = pick_folder_ui(
|
let response = pick_folder_ui(
|
||||||
ui,
|
ui,
|
||||||
&config.base_obj_dir,
|
&state.config.base_obj_dir,
|
||||||
"Base build directory",
|
"Base build directory",
|
||||||
|ui| {
|
|ui| {
|
||||||
let mut job = LayoutJob::default();
|
let mut job = LayoutJob::default();
|
||||||
@@ -723,17 +757,17 @@ fn split_obj_config_ui(
|
|||||||
ui.label(job);
|
ui.label(job);
|
||||||
},
|
},
|
||||||
appearance,
|
appearance,
|
||||||
config.project_config_info.is_none(),
|
state.project_config_info.is_none(),
|
||||||
);
|
);
|
||||||
if response.clicked() {
|
if response.clicked() {
|
||||||
state.file_dialog_state.queue(
|
config_state.file_dialog_state.queue(
|
||||||
|| Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()),
|
|| Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()),
|
||||||
FileDialogResult::BaseDir,
|
FileDialogResult::BaseDir,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ui.add_enabled(
|
ui.add_enabled(
|
||||||
config.project_config_info.is_none(),
|
state.project_config_info.is_none(),
|
||||||
egui::Checkbox::new(&mut config.build_base, "Build base objects"),
|
egui::Checkbox::new(&mut state.config.build_base, "Build base objects"),
|
||||||
)
|
)
|
||||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||||
.on_hover_ui(|ui| {
|
.on_hover_ui(|ui| {
|
||||||
@@ -764,7 +798,7 @@ fn split_obj_config_ui(
|
|||||||
|
|
||||||
subheading(ui, "Watch settings", appearance);
|
subheading(ui, "Watch settings", appearance);
|
||||||
let response =
|
let response =
|
||||||
ui.checkbox(&mut config.rebuild_on_changes, "Rebuild on changes").on_hover_ui(|ui| {
|
ui.checkbox(&mut state.config.rebuild_on_changes, "Rebuild on changes").on_hover_ui(|ui| {
|
||||||
let mut job = LayoutJob::default();
|
let mut job = LayoutJob::default();
|
||||||
job.append(
|
job.append(
|
||||||
"Automatically re-run the build & diff when files change.",
|
"Automatically re-run the build & diff when files change.",
|
||||||
@@ -774,23 +808,23 @@ fn split_obj_config_ui(
|
|||||||
ui.label(job);
|
ui.label(job);
|
||||||
});
|
});
|
||||||
if response.changed() {
|
if response.changed() {
|
||||||
config.watcher_change = true;
|
state.watcher_change = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(RichText::new("File patterns").color(appearance.text_color));
|
ui.label(RichText::new("File patterns").color(appearance.text_color));
|
||||||
if ui
|
if ui
|
||||||
.add_enabled(config.project_config_info.is_none(), egui::Button::new("Reset"))
|
.add_enabled(state.project_config_info.is_none(), egui::Button::new("Reset"))
|
||||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
config.watch_patterns =
|
state.config.watch_patterns =
|
||||||
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect();
|
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect();
|
||||||
config.watcher_change = true;
|
state.watcher_change = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let mut remove_at: Option<usize> = None;
|
let mut remove_at: Option<usize> = None;
|
||||||
for (idx, glob) in config.watch_patterns.iter().enumerate() {
|
for (idx, glob) in state.config.watch_patterns.iter().enumerate() {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(
|
ui.label(
|
||||||
RichText::new(format!("{}", glob))
|
RichText::new(format!("{}", glob))
|
||||||
@@ -798,7 +832,7 @@ fn split_obj_config_ui(
|
|||||||
.family(FontFamily::Monospace),
|
.family(FontFamily::Monospace),
|
||||||
);
|
);
|
||||||
if ui
|
if ui
|
||||||
.add_enabled(config.project_config_info.is_none(), egui::Button::new("-").small())
|
.add_enabled(state.project_config_info.is_none(), egui::Button::new("-").small())
|
||||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
@@ -807,24 +841,24 @@ fn split_obj_config_ui(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if let Some(idx) = remove_at {
|
if let Some(idx) = remove_at {
|
||||||
config.watch_patterns.remove(idx);
|
state.config.watch_patterns.remove(idx);
|
||||||
config.watcher_change = true;
|
state.watcher_change = true;
|
||||||
}
|
}
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.add_enabled(
|
ui.add_enabled(
|
||||||
config.project_config_info.is_none(),
|
state.project_config_info.is_none(),
|
||||||
egui::TextEdit::singleline(&mut state.watch_pattern_text).desired_width(100.0),
|
egui::TextEdit::singleline(&mut config_state.watch_pattern_text).desired_width(100.0),
|
||||||
)
|
)
|
||||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT);
|
.on_disabled_hover_text(CONFIG_DISABLED_TEXT);
|
||||||
if ui
|
if ui
|
||||||
.add_enabled(config.project_config_info.is_none(), egui::Button::new("+").small())
|
.add_enabled(state.project_config_info.is_none(), egui::Button::new("+").small())
|
||||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
if let Ok(glob) = Glob::new(&state.watch_pattern_text) {
|
if let Ok(glob) = Glob::new(&config_state.watch_pattern_text) {
|
||||||
config.watch_patterns.push(glob);
|
state.config.watch_patterns.push(glob);
|
||||||
config.watcher_change = true;
|
state.watcher_change = true;
|
||||||
state.watch_pattern_text.clear();
|
config_state.watch_pattern_text.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -832,131 +866,112 @@ fn split_obj_config_ui(
|
|||||||
|
|
||||||
pub fn arch_config_window(
|
pub fn arch_config_window(
|
||||||
ctx: &egui::Context,
|
ctx: &egui::Context,
|
||||||
config: &AppConfigRef,
|
state: &AppStateRef,
|
||||||
show: &mut bool,
|
show: &mut bool,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
) {
|
) {
|
||||||
let mut config_guard = config.write().unwrap();
|
let mut state_guard = state.write().unwrap();
|
||||||
egui::Window::new("Arch Settings").open(show).show(ctx, |ui| {
|
egui::Window::new("Arch Settings").open(show).show(ctx, |ui| {
|
||||||
arch_config_ui(ui, &mut config_guard, appearance);
|
arch_config_ui(ui, &mut state_guard, appearance);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn arch_config_ui(ui: &mut egui::Ui, config: &mut AppConfig, _appearance: &Appearance) {
|
fn config_property_ui(
|
||||||
ui.heading("x86");
|
ui: &mut egui::Ui,
|
||||||
egui::ComboBox::new("x86_formatter", "Format")
|
state: &mut AppState,
|
||||||
.selected_text(config.diff_obj_config.x86_formatter.get_message().unwrap())
|
property_id: ConfigPropertyId,
|
||||||
.show_ui(ui, |ui| {
|
) -> bool {
|
||||||
for &formatter in X86Formatter::VARIANTS {
|
let mut changed = false;
|
||||||
if ui
|
let current_value = state.config.diff_obj_config.get_property_value(property_id);
|
||||||
.selectable_label(
|
match (property_id.kind(), current_value) {
|
||||||
config.diff_obj_config.x86_formatter == formatter,
|
(ConfigPropertyKind::Boolean, ConfigPropertyValue::Boolean(mut checked)) => {
|
||||||
formatter.get_message().unwrap(),
|
let mut response = ui.checkbox(&mut checked, property_id.name());
|
||||||
)
|
if let Some(description) = property_id.description() {
|
||||||
.clicked()
|
response = response.on_hover_text(description);
|
||||||
{
|
}
|
||||||
config.diff_obj_config.x86_formatter = formatter;
|
if response.changed() {
|
||||||
config.queue_reload = true;
|
state
|
||||||
|
.config
|
||||||
|
.diff_obj_config
|
||||||
|
.set_property_value(property_id, ConfigPropertyValue::Boolean(checked))
|
||||||
|
.expect("Failed to set property value");
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(ConfigPropertyKind::Choice(variants), ConfigPropertyValue::Choice(selected)) => {
|
||||||
|
fn variant_name(variant: &ConfigEnumVariantInfo) -> String {
|
||||||
|
if variant.is_default {
|
||||||
|
format!("{} (default)", variant.name)
|
||||||
|
} else {
|
||||||
|
variant.name.to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
let selected_variant = variants
|
||||||
ui.separator();
|
.iter()
|
||||||
ui.heading("MIPS");
|
.find(|v| v.value == selected)
|
||||||
egui::ComboBox::new("mips_abi", "ABI")
|
.or_else(|| variants.iter().find(|v| v.is_default))
|
||||||
.selected_text(config.diff_obj_config.mips_abi.get_message().unwrap())
|
.expect("Invalid choice variant");
|
||||||
.show_ui(ui, |ui| {
|
let response = egui::ComboBox::new(property_id.name(), property_id.name())
|
||||||
for &abi in MipsAbi::VARIANTS {
|
.selected_text(variant_name(selected_variant))
|
||||||
if ui
|
.show_ui(ui, |ui| {
|
||||||
.selectable_label(
|
for variant in variants {
|
||||||
config.diff_obj_config.mips_abi == abi,
|
let mut response =
|
||||||
abi.get_message().unwrap(),
|
ui.selectable_label(selected == variant.value, variant_name(variant));
|
||||||
)
|
if let Some(description) = variant.description {
|
||||||
.clicked()
|
response = response.on_hover_text(description);
|
||||||
{
|
}
|
||||||
config.diff_obj_config.mips_abi = abi;
|
if response.clicked() {
|
||||||
config.queue_reload = true;
|
state
|
||||||
}
|
.config
|
||||||
|
.diff_obj_config
|
||||||
|
.set_property_value(
|
||||||
|
property_id,
|
||||||
|
ConfigPropertyValue::Choice(variant.value),
|
||||||
|
)
|
||||||
|
.expect("Failed to set property value");
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.response;
|
||||||
|
if let Some(description) = property_id.description() {
|
||||||
|
response.on_hover_text(description);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
egui::ComboBox::new("mips_instr_category", "Instruction Category")
|
_ => panic!("Incompatible property kind and value"),
|
||||||
.selected_text(config.diff_obj_config.mips_instr_category.get_message().unwrap())
|
|
||||||
.show_ui(ui, |ui| {
|
|
||||||
for &category in MipsInstrCategory::VARIANTS {
|
|
||||||
if ui
|
|
||||||
.selectable_label(
|
|
||||||
config.diff_obj_config.mips_instr_category == category,
|
|
||||||
category.get_message().unwrap(),
|
|
||||||
)
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
config.diff_obj_config.mips_instr_category = category;
|
|
||||||
config.queue_reload = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ui.separator();
|
|
||||||
ui.heading("ARM");
|
|
||||||
egui::ComboBox::new("arm_arch_version", "Architecture Version")
|
|
||||||
.selected_text(config.diff_obj_config.arm_arch_version.get_message().unwrap())
|
|
||||||
.show_ui(ui, |ui| {
|
|
||||||
for &version in ArmArchVersion::VARIANTS {
|
|
||||||
if ui
|
|
||||||
.selectable_label(
|
|
||||||
config.diff_obj_config.arm_arch_version == version,
|
|
||||||
version.get_message().unwrap(),
|
|
||||||
)
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
config.diff_obj_config.arm_arch_version = version;
|
|
||||||
config.queue_reload = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let response = ui
|
|
||||||
.checkbox(&mut config.diff_obj_config.arm_unified_syntax, "Unified syntax")
|
|
||||||
.on_hover_text("Disassemble as unified assembly language (UAL).");
|
|
||||||
if response.changed() {
|
|
||||||
config.queue_reload = true;
|
|
||||||
}
|
}
|
||||||
let response = ui
|
changed
|
||||||
.checkbox(&mut config.diff_obj_config.arm_av_registers, "Use A/V registers")
|
}
|
||||||
.on_hover_text("Display R0-R3 as A1-A4 and R4-R11 as V1-V8");
|
|
||||||
if response.changed() {
|
fn arch_config_ui(ui: &mut egui::Ui, state: &mut AppState, _appearance: &Appearance) {
|
||||||
config.queue_reload = true;
|
let mut first = true;
|
||||||
|
let mut changed = false;
|
||||||
|
for group in CONFIG_GROUPS {
|
||||||
|
if group.id == "general" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if first {
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
ui.separator();
|
||||||
|
}
|
||||||
|
ui.heading(group.name);
|
||||||
|
for property_id in group.properties.iter().cloned() {
|
||||||
|
changed |= config_property_ui(ui, state, property_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
egui::ComboBox::new("arm_r9_usage", "Display R9 as")
|
if changed {
|
||||||
.selected_text(config.diff_obj_config.arm_r9_usage.get_message().unwrap())
|
state.queue_reload = true;
|
||||||
.show_ui(ui, |ui| {
|
}
|
||||||
for &usage in ArmR9Usage::VARIANTS {
|
}
|
||||||
if ui
|
|
||||||
.selectable_label(
|
pub fn general_config_ui(ui: &mut egui::Ui, state: &mut AppState) {
|
||||||
config.diff_obj_config.arm_r9_usage == usage,
|
let mut changed = false;
|
||||||
usage.get_message().unwrap(),
|
let group = CONFIG_GROUPS.iter().find(|group| group.id == "general").unwrap();
|
||||||
)
|
for property_id in group.properties.iter().cloned() {
|
||||||
.on_hover_text(usage.get_detailed_message().unwrap())
|
changed |= config_property_ui(ui, state, property_id);
|
||||||
.clicked()
|
}
|
||||||
{
|
if changed {
|
||||||
config.diff_obj_config.arm_r9_usage = usage;
|
state.queue_reload = true;
|
||||||
config.queue_reload = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let response = ui
|
|
||||||
.checkbox(&mut config.diff_obj_config.arm_sl_usage, "Display R10 as SL")
|
|
||||||
.on_hover_text("Used for explicit stack limits.");
|
|
||||||
if response.changed() {
|
|
||||||
config.queue_reload = true;
|
|
||||||
}
|
|
||||||
let response = ui
|
|
||||||
.checkbox(&mut config.diff_obj_config.arm_fp_usage, "Display R11 as FP")
|
|
||||||
.on_hover_text("Used for frame pointers.");
|
|
||||||
if response.changed() {
|
|
||||||
config.queue_reload = true;
|
|
||||||
}
|
|
||||||
let response = ui
|
|
||||||
.checkbox(&mut config.diff_obj_config.arm_ip_usage, "Display R12 as IP")
|
|
||||||
.on_hover_text("Used for interworking and long branches.");
|
|
||||||
if response.changed() {
|
|
||||||
config.queue_reload = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,114 @@
|
|||||||
use std::{cmp::min, default::Default, mem::take};
|
use std::{
|
||||||
|
cmp::{min, Ordering},
|
||||||
|
default::Default,
|
||||||
|
mem::take,
|
||||||
|
};
|
||||||
|
|
||||||
use egui::{text::LayoutJob, Align, Label, Layout, Sense, Vec2, Widget};
|
use egui::{text::LayoutJob, Id, Label, RichText, Sense, Widget};
|
||||||
use egui_extras::{Column, TableBuilder};
|
|
||||||
use objdiff_core::{
|
use objdiff_core::{
|
||||||
diff::{ObjDataDiff, ObjDataDiffKind, ObjDiff},
|
diff::{ObjDataDiff, ObjDataDiffKind, ObjDiff},
|
||||||
obj::ObjInfo,
|
obj::ObjInfo,
|
||||||
};
|
};
|
||||||
use time::format_description;
|
use time::format_description;
|
||||||
|
|
||||||
use crate::views::{
|
use crate::{
|
||||||
appearance::Appearance,
|
hotkeys,
|
||||||
symbol_diff::{DiffViewState, SymbolRefByName, View},
|
views::{
|
||||||
write_text,
|
appearance::Appearance,
|
||||||
|
column_layout::{render_header, render_table},
|
||||||
|
symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState},
|
||||||
|
write_text,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const BYTES_PER_ROW: usize = 16;
|
const BYTES_PER_ROW: usize = 16;
|
||||||
|
|
||||||
fn find_section(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<usize> {
|
fn find_section(obj: &ObjInfo, section_name: &str) -> Option<usize> {
|
||||||
obj.sections.iter().position(|section| section.name == selected_symbol.section_name)
|
obj.sections.iter().position(|section| section.name == section_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appearance: &Appearance) {
|
fn data_row_hover_ui(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
obj: &ObjInfo,
|
||||||
|
diffs: &[ObjDataDiff],
|
||||||
|
appearance: &Appearance,
|
||||||
|
) {
|
||||||
|
ui.scope(|ui| {
|
||||||
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
|
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||||
|
|
||||||
|
for diff in diffs {
|
||||||
|
let Some(reloc) = &diff.reloc else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use a slightly different font color for nonmatching relocations so they stand out.
|
||||||
|
let color = match diff.kind {
|
||||||
|
ObjDataDiffKind::None => appearance.highlight_color,
|
||||||
|
_ => appearance.replace_color,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Most of this code is copy-pasted from ins_hover_ui.
|
||||||
|
// Try to separate this out into a shared function.
|
||||||
|
ui.label(format!("Relocation type: {}", obj.arch.display_reloc(reloc.flags)));
|
||||||
|
let addend_str = match reloc.addend.cmp(&0i64) {
|
||||||
|
Ordering::Greater => format!("+{:x}", reloc.addend),
|
||||||
|
Ordering::Less => format!("-{:x}", -reloc.addend),
|
||||||
|
_ => "".to_string(),
|
||||||
|
};
|
||||||
|
ui.colored_label(color, format!("Name: {}{}", reloc.target.name, addend_str));
|
||||||
|
if let Some(orig_section_index) = reloc.target.orig_section_index {
|
||||||
|
if let Some(section) =
|
||||||
|
obj.sections.iter().find(|s| s.orig_index == orig_section_index)
|
||||||
|
{
|
||||||
|
ui.colored_label(color, format!("Section: {}", section.name));
|
||||||
|
}
|
||||||
|
ui.colored_label(
|
||||||
|
color,
|
||||||
|
format!("Address: {:x}{}", reloc.target.address, addend_str),
|
||||||
|
);
|
||||||
|
ui.colored_label(color, format!("Size: {:x}", reloc.target.size));
|
||||||
|
if reloc.addend >= 0 && reloc.target.bytes.len() > reloc.addend as usize {}
|
||||||
|
} else {
|
||||||
|
ui.colored_label(color, "Extern".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data_row_context_menu(ui: &mut egui::Ui, diffs: &[ObjDataDiff]) {
|
||||||
|
ui.scope(|ui| {
|
||||||
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
|
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||||
|
|
||||||
|
for diff in diffs {
|
||||||
|
let Some(reloc) = &diff.reloc else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: This code is copy-pasted from ins_context_menu.
|
||||||
|
// Try to separate this out into a shared function.
|
||||||
|
if let Some(name) = &reloc.target.demangled_name {
|
||||||
|
if ui.button(format!("Copy \"{name}\"")).clicked() {
|
||||||
|
ui.output_mut(|output| output.copied_text.clone_from(name));
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ui.button(format!("Copy \"{}\"", reloc.target.name)).clicked() {
|
||||||
|
ui.output_mut(|output| output.copied_text.clone_from(&reloc.target.name));
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data_row_ui(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
obj: Option<&ObjInfo>,
|
||||||
|
address: usize,
|
||||||
|
diffs: &[ObjDataDiff],
|
||||||
|
appearance: &Appearance,
|
||||||
|
) {
|
||||||
if diffs.iter().any(|d| d.kind != ObjDataDiffKind::None) {
|
if diffs.iter().any(|d| d.kind != ObjDataDiffKind::None) {
|
||||||
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
|
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
|
||||||
}
|
}
|
||||||
@@ -91,9 +179,13 @@ fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appeara
|
|||||||
write_text(text.as_str(), base_color, &mut job, appearance.code_font.clone());
|
write_text(text.as_str(), base_color, &mut job, appearance.code_font.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Label::new(job).sense(Sense::click()).ui(ui);
|
|
||||||
// .on_hover_ui_at_pointer(|ui| ins_hover_ui(ui, ins))
|
let response = Label::new(job).sense(Sense::click()).ui(ui);
|
||||||
// .context_menu(|ui| ins_context_menu(ui, ins));
|
if let Some(obj) = obj {
|
||||||
|
response
|
||||||
|
.on_hover_ui_at_pointer(|ui| data_row_hover_ui(ui, obj, diffs, appearance))
|
||||||
|
.context_menu(|ui| data_row_context_menu(ui, diffs));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> {
|
fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> {
|
||||||
@@ -114,8 +206,8 @@ fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> {
|
|||||||
},
|
},
|
||||||
kind: diff.kind,
|
kind: diff.kind,
|
||||||
len,
|
len,
|
||||||
// TODO
|
symbol: String::new(), // TODO
|
||||||
symbol: String::new(),
|
reloc: diff.reloc.clone(),
|
||||||
});
|
});
|
||||||
remaining_in_row -= len;
|
remaining_in_row -= len;
|
||||||
cur_len += len;
|
cur_len += len;
|
||||||
@@ -131,20 +223,39 @@ fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> {
|
|||||||
split_diffs
|
split_diffs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct SectionDiffContext<'a> {
|
||||||
|
obj: &'a ObjInfo,
|
||||||
|
diff: &'a ObjDiff,
|
||||||
|
section_index: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> SectionDiffContext<'a> {
|
||||||
|
pub fn new(obj: Option<&'a (ObjInfo, ObjDiff)>, section_name: Option<&str>) -> Option<Self> {
|
||||||
|
obj.map(|(obj, diff)| Self {
|
||||||
|
obj,
|
||||||
|
diff,
|
||||||
|
section_index: section_name.and_then(|section_name| find_section(obj, section_name)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn has_section(&self) -> bool { self.section_index.is_some() }
|
||||||
|
}
|
||||||
|
|
||||||
fn data_table_ui(
|
fn data_table_ui(
|
||||||
table: TableBuilder<'_>,
|
ui: &mut egui::Ui,
|
||||||
left_obj: Option<&(ObjInfo, ObjDiff)>,
|
available_width: f32,
|
||||||
right_obj: Option<&(ObjInfo, ObjDiff)>,
|
left_ctx: Option<SectionDiffContext<'_>>,
|
||||||
selected_symbol: &SymbolRefByName,
|
right_ctx: Option<SectionDiffContext<'_>>,
|
||||||
config: &Appearance,
|
config: &Appearance,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
let left_section = left_obj.and_then(|(obj, diff)| {
|
let left_obj = left_ctx.map(|ctx| ctx.obj);
|
||||||
find_section(obj, selected_symbol).map(|i| (&obj.sections[i], &diff.sections[i]))
|
let right_obj = right_ctx.map(|ctx| ctx.obj);
|
||||||
});
|
let left_section = left_ctx
|
||||||
let right_section = right_obj.and_then(|(obj, diff)| {
|
.and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i])));
|
||||||
find_section(obj, selected_symbol).map(|i| (&obj.sections[i], &diff.sections[i]))
|
let right_section = right_ctx
|
||||||
});
|
.and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i])));
|
||||||
|
|
||||||
let total_bytes = left_section
|
let total_bytes = left_section
|
||||||
.or(right_section)?
|
.or(right_section)?
|
||||||
.1
|
.1
|
||||||
@@ -159,119 +270,119 @@ fn data_table_ui(
|
|||||||
let left_diffs = left_section.map(|(_, section)| split_diffs(§ion.data_diff));
|
let left_diffs = left_section.map(|(_, section)| split_diffs(§ion.data_diff));
|
||||||
let right_diffs = right_section.map(|(_, section)| split_diffs(§ion.data_diff));
|
let right_diffs = right_section.map(|(_, section)| split_diffs(§ion.data_diff));
|
||||||
|
|
||||||
table.body(|body| {
|
hotkeys::check_scroll_hotkeys(ui, true);
|
||||||
body.rows(config.code_font.size, total_rows, |mut row| {
|
|
||||||
let row_index = row.index();
|
render_table(ui, available_width, 2, config.code_font.size, total_rows, |row, column| {
|
||||||
let address = row_index * BYTES_PER_ROW;
|
let i = row.index();
|
||||||
row.col(|ui| {
|
let address = i * BYTES_PER_ROW;
|
||||||
|
row.col(|ui| {
|
||||||
|
if column == 0 {
|
||||||
if let Some(left_diffs) = &left_diffs {
|
if let Some(left_diffs) = &left_diffs {
|
||||||
data_row_ui(ui, address, &left_diffs[row_index], config);
|
data_row_ui(ui, left_obj, address, &left_diffs[i], config);
|
||||||
}
|
}
|
||||||
});
|
} else if column == 1 {
|
||||||
row.col(|ui| {
|
|
||||||
if let Some(right_diffs) = &right_diffs {
|
if let Some(right_diffs) = &right_diffs {
|
||||||
data_row_ui(ui, address, &right_diffs[row_index], config);
|
data_row_ui(ui, right_obj, address, &right_diffs[i], config);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Some(())
|
Some(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
|
#[must_use]
|
||||||
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
|
pub fn data_diff_ui(
|
||||||
else {
|
ui: &mut egui::Ui,
|
||||||
return;
|
state: &DiffViewState,
|
||||||
|
appearance: &Appearance,
|
||||||
|
) -> Option<DiffViewAction> {
|
||||||
|
let mut ret = None;
|
||||||
|
let Some(result) = &state.build else {
|
||||||
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let section_name =
|
||||||
|
state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref()).or_else(
|
||||||
|
|| state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref()),
|
||||||
|
);
|
||||||
|
let left_ctx = SectionDiffContext::new(result.first_obj.as_ref(), section_name);
|
||||||
|
let right_ctx = SectionDiffContext::new(result.second_obj.as_ref(), section_name);
|
||||||
|
|
||||||
|
// If both sides are missing a symbol, switch to symbol diff view
|
||||||
|
if !right_ctx.is_some_and(|ctx| ctx.has_section())
|
||||||
|
&& !left_ctx.is_some_and(|ctx| ctx.has_section())
|
||||||
|
{
|
||||||
|
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||||
|
}
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
let available_width = ui.available_width();
|
let available_width = ui.available_width();
|
||||||
let column_width = available_width / 2.0;
|
render_header(ui, available_width, 2, |ui, column| {
|
||||||
ui.allocate_ui_with_layout(
|
if column == 0 {
|
||||||
Vec2 { x: available_width, y: 100.0 },
|
|
||||||
Layout::left_to_right(Align::Min),
|
|
||||||
|ui| {
|
|
||||||
// Left column
|
// Left column
|
||||||
ui.allocate_ui_with_layout(
|
if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) {
|
||||||
Vec2 { x: column_width, y: 100.0 },
|
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||||
Layout::top_down(Align::Min),
|
}
|
||||||
|ui| {
|
|
||||||
ui.set_width(column_width);
|
|
||||||
|
|
||||||
if ui.button("⏴ Back").clicked() {
|
|
||||||
state.current_view = View::SymbolDiff;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.scope(|ui| {
|
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
|
||||||
ui.style_mut().wrap = Some(false);
|
|
||||||
ui.colored_label(appearance.highlight_color, &selected_symbol.symbol_name);
|
|
||||||
ui.label("Diff target:");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
|
if let Some(section) =
|
||||||
|
left_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i]))
|
||||||
|
{
|
||||||
|
ui.label(
|
||||||
|
RichText::new(section.name.clone())
|
||||||
|
.font(appearance.code_font.clone())
|
||||||
|
.color(appearance.highlight_color),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ui.label(
|
||||||
|
RichText::new("Missing")
|
||||||
|
.font(appearance.code_font.clone())
|
||||||
|
.color(appearance.replace_color),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if column == 1 {
|
||||||
// Right column
|
// Right column
|
||||||
ui.allocate_ui_with_layout(
|
ui.horizontal(|ui| {
|
||||||
Vec2 { x: column_width, y: 100.0 },
|
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
|
||||||
Layout::top_down(Align::Min),
|
ret = Some(DiffViewAction::Build);
|
||||||
|ui| {
|
}
|
||||||
ui.set_width(column_width);
|
ui.scope(|ui| {
|
||||||
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
|
if state.build_running {
|
||||||
|
ui.colored_label(appearance.replace_color, "Building…");
|
||||||
|
} else {
|
||||||
|
ui.label("Last built:");
|
||||||
|
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||||
|
ui.label(
|
||||||
|
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
if let Some(section) =
|
||||||
if ui
|
right_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i]))
|
||||||
.add_enabled(!state.build_running, egui::Button::new("Build"))
|
{
|
||||||
.clicked()
|
ui.label(
|
||||||
{
|
RichText::new(section.name.clone())
|
||||||
state.queue_build = true;
|
.font(appearance.code_font.clone())
|
||||||
}
|
.color(appearance.highlight_color),
|
||||||
ui.scope(|ui| {
|
);
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
} else {
|
||||||
ui.style_mut().wrap = Some(false);
|
ui.label(
|
||||||
if state.build_running {
|
RichText::new("Missing")
|
||||||
ui.colored_label(appearance.replace_color, "Building…");
|
.font(appearance.code_font.clone())
|
||||||
} else {
|
.color(appearance.replace_color),
|
||||||
ui.label("Last built:");
|
);
|
||||||
let format =
|
}
|
||||||
format_description::parse("[hour]:[minute]:[second]").unwrap();
|
}
|
||||||
ui.label(
|
});
|
||||||
result
|
|
||||||
.time
|
|
||||||
.to_offset(appearance.utc_offset)
|
|
||||||
.format(&format)
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.scope(|ui| {
|
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
|
||||||
ui.style_mut().wrap = Some(false);
|
|
||||||
ui.label("");
|
|
||||||
ui.label("Diff base:");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
// Table
|
// Table
|
||||||
ui.style_mut().interaction.selectable_labels = false;
|
let id =
|
||||||
let available_height = ui.available_height();
|
Id::new(state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref()))
|
||||||
let table = TableBuilder::new(ui)
|
.with(state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref()));
|
||||||
.striped(false)
|
ui.push_id(id, |ui| {
|
||||||
.cell_layout(Layout::left_to_right(Align::Min))
|
data_table_ui(ui, available_width, left_ctx, right_ctx, appearance);
|
||||||
.columns(Column::exact(column_width).clip(true), 2)
|
});
|
||||||
.resizable(false)
|
ret
|
||||||
.auto_shrink([false, false])
|
|
||||||
.min_scrolled_height(available_height);
|
|
||||||
data_table_ui(
|
|
||||||
table,
|
|
||||||
result.first_obj.as_ref(),
|
|
||||||
result.second_obj.as_ref(),
|
|
||||||
selected_symbol,
|
|
||||||
appearance,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +1,55 @@
|
|||||||
use egui::{text::LayoutJob, Align, Layout, ScrollArea, Ui, Vec2};
|
use egui::{RichText, ScrollArea};
|
||||||
use egui_extras::{Size, StripBuilder};
|
|
||||||
use objdiff_core::{
|
use objdiff_core::{
|
||||||
diff::ObjDiff,
|
arch::ppc::ExceptionInfo,
|
||||||
obj::{ObjExtab, ObjInfo, ObjSymbol, SymbolRef},
|
obj::{ObjInfo, ObjSymbol},
|
||||||
};
|
};
|
||||||
use time::format_description;
|
use time::format_description;
|
||||||
|
|
||||||
use crate::views::{
|
use crate::{
|
||||||
appearance::Appearance,
|
hotkeys,
|
||||||
symbol_diff::{match_color_for_symbol, DiffViewState, SymbolRefByName, View},
|
views::{
|
||||||
|
appearance::Appearance,
|
||||||
|
column_layout::{render_header, render_strips},
|
||||||
|
function_diff::FunctionDiffContext,
|
||||||
|
symbol_diff::{
|
||||||
|
match_color_for_symbol, DiffViewAction, DiffViewNavigation, DiffViewState,
|
||||||
|
SymbolRefByName, View,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<SymbolRef> {
|
fn decode_extab(extab: &ExceptionInfo) -> String {
|
||||||
for (section_idx, section) in obj.sections.iter().enumerate() {
|
|
||||||
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
|
|
||||||
if symbol.name == selected_symbol.symbol_name {
|
|
||||||
return Some(SymbolRef { section_idx, symbol_idx });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decode_extab(extab: &ObjExtab) -> String {
|
|
||||||
let mut text = String::from("");
|
let mut text = String::from("");
|
||||||
|
|
||||||
let mut dtor_names: Vec<&str> = vec![];
|
let mut dtor_names: Vec<String> = vec![];
|
||||||
for dtor in &extab.dtors {
|
for dtor in &extab.dtors {
|
||||||
//For each function name, use the demangled name by default,
|
//For each function name, use the demangled name by default,
|
||||||
//and if not available fallback to the original name
|
//and if not available fallback to the original name
|
||||||
let name = match &dtor.demangled_name {
|
let name: String = match &dtor.demangled_name {
|
||||||
Some(demangled_name) => demangled_name,
|
Some(demangled_name) => demangled_name.to_string(),
|
||||||
None => &dtor.name,
|
None => dtor.name.clone(),
|
||||||
};
|
};
|
||||||
dtor_names.push(name.as_str());
|
dtor_names.push(name);
|
||||||
}
|
}
|
||||||
if let Some(decoded) = extab.data.to_string(&dtor_names) {
|
if let Some(decoded) = extab.data.to_string(dtor_names) {
|
||||||
text += decoded.as_str();
|
text += decoded.as_str();
|
||||||
}
|
}
|
||||||
|
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_extab_entry(obj: &ObjInfo, symbol: &ObjSymbol) -> Option<ObjExtab> {
|
fn find_extab_entry<'a>(obj: &'a ObjInfo, symbol: &ObjSymbol) -> Option<&'a ExceptionInfo> {
|
||||||
if let Some(extab_array) = &obj.extab {
|
obj.arch.ppc().and_then(|ppc| ppc.extab_for_symbol(symbol))
|
||||||
for extab_entry in extab_array {
|
|
||||||
if extab_entry.func.name == symbol.name {
|
|
||||||
return Some(extab_entry.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extab_text_ui(
|
fn extab_text_ui(
|
||||||
ui: &mut Ui,
|
ui: &mut egui::Ui,
|
||||||
obj: &(ObjInfo, ObjDiff),
|
ctx: FunctionDiffContext<'_>,
|
||||||
symbol_ref: SymbolRef,
|
symbol: &ObjSymbol,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
let (_section, symbol) = obj.0.section_symbol(symbol_ref);
|
if let Some(extab_entry) = find_extab_entry(ctx.obj, symbol) {
|
||||||
|
let text = decode_extab(extab_entry);
|
||||||
if let Some(extab_entry) = find_extab_entry(&obj.0, symbol) {
|
|
||||||
let text = decode_extab(&extab_entry);
|
|
||||||
ui.colored_label(appearance.replace_color, &text);
|
ui.colored_label(appearance.replace_color, &text);
|
||||||
return Some(());
|
return Some(());
|
||||||
}
|
}
|
||||||
@@ -74,145 +58,196 @@ fn extab_text_ui(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn extab_ui(
|
fn extab_ui(
|
||||||
ui: &mut Ui,
|
ui: &mut egui::Ui,
|
||||||
obj: Option<&(ObjInfo, ObjDiff)>,
|
ctx: FunctionDiffContext<'_>,
|
||||||
selected_symbol: &SymbolRefByName,
|
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
_left: bool,
|
_column: usize,
|
||||||
) {
|
) {
|
||||||
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
|
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
|
||||||
ui.scope(|ui| {
|
ui.scope(|ui| {
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
ui.style_mut().wrap = Some(false);
|
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||||
|
|
||||||
let symbol = obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol));
|
if let Some((_section, symbol)) =
|
||||||
|
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
|
||||||
if let (Some(object), Some(symbol_ref)) = (obj, symbol) {
|
{
|
||||||
extab_text_ui(ui, object, symbol_ref, appearance);
|
extab_text_ui(ui, ctx, symbol, appearance);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extab_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
|
#[must_use]
|
||||||
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
|
pub fn extab_diff_ui(
|
||||||
else {
|
ui: &mut egui::Ui,
|
||||||
return;
|
state: &DiffViewState,
|
||||||
|
appearance: &Appearance,
|
||||||
|
) -> Option<DiffViewAction> {
|
||||||
|
let mut ret = None;
|
||||||
|
let Some(result) = &state.build else {
|
||||||
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut left_ctx = FunctionDiffContext::new(
|
||||||
|
result.first_obj.as_ref(),
|
||||||
|
state.symbol_state.left_symbol.as_ref(),
|
||||||
|
);
|
||||||
|
let mut right_ctx = FunctionDiffContext::new(
|
||||||
|
result.second_obj.as_ref(),
|
||||||
|
state.symbol_state.right_symbol.as_ref(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// If one side is missing a symbol, but the diff process found a match, use that symbol
|
||||||
|
let left_diff_symbol = left_ctx.and_then(|ctx| {
|
||||||
|
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
|
||||||
|
});
|
||||||
|
let right_diff_symbol = right_ctx.and_then(|ctx| {
|
||||||
|
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
|
||||||
|
});
|
||||||
|
if left_diff_symbol.is_some() && right_ctx.is_some_and(|ctx| !ctx.has_symbol()) {
|
||||||
|
let (right_section, right_symbol) =
|
||||||
|
right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap());
|
||||||
|
let symbol_ref = SymbolRefByName::new(right_symbol, right_section);
|
||||||
|
right_ctx = FunctionDiffContext::new(result.second_obj.as_ref(), Some(&symbol_ref));
|
||||||
|
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
|
||||||
|
view: Some(View::FunctionDiff),
|
||||||
|
left_symbol: state.symbol_state.left_symbol.clone(),
|
||||||
|
right_symbol: Some(symbol_ref),
|
||||||
|
}));
|
||||||
|
} else if right_diff_symbol.is_some() && left_ctx.is_some_and(|ctx| !ctx.has_symbol()) {
|
||||||
|
let (left_section, left_symbol) =
|
||||||
|
left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap());
|
||||||
|
let symbol_ref = SymbolRefByName::new(left_symbol, left_section);
|
||||||
|
left_ctx = FunctionDiffContext::new(result.first_obj.as_ref(), Some(&symbol_ref));
|
||||||
|
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
|
||||||
|
view: Some(View::FunctionDiff),
|
||||||
|
left_symbol: Some(symbol_ref),
|
||||||
|
right_symbol: state.symbol_state.right_symbol.clone(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If both sides are missing a symbol, switch to symbol diff view
|
||||||
|
if right_ctx.is_some_and(|ctx| !ctx.has_symbol())
|
||||||
|
&& left_ctx.is_some_and(|ctx| !ctx.has_symbol())
|
||||||
|
{
|
||||||
|
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||||
|
}
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
let available_width = ui.available_width();
|
let available_width = ui.available_width();
|
||||||
let column_width = available_width / 2.0;
|
render_header(ui, available_width, 2, |ui, column| {
|
||||||
ui.allocate_ui_with_layout(
|
if column == 0 {
|
||||||
Vec2 { x: available_width, y: 100.0 },
|
|
||||||
Layout::left_to_right(Align::Min),
|
|
||||||
|ui| {
|
|
||||||
// Left column
|
// Left column
|
||||||
ui.allocate_ui_with_layout(
|
ui.horizontal(|ui| {
|
||||||
Vec2 { x: column_width, y: 100.0 },
|
if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) {
|
||||||
Layout::top_down(Align::Min),
|
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||||
|ui| {
|
}
|
||||||
ui.set_width(column_width);
|
ui.separator();
|
||||||
|
if ui
|
||||||
ui.horizontal(|ui| {
|
.add_enabled(
|
||||||
if ui.button("⏴ Back").clicked() {
|
!state.scratch_running
|
||||||
state.current_view = View::SymbolDiff;
|
&& state.scratch_available
|
||||||
}
|
&& left_ctx.is_some_and(|ctx| ctx.has_symbol()),
|
||||||
});
|
egui::Button::new("📲 decomp.me"),
|
||||||
|
)
|
||||||
let name = selected_symbol
|
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
|
||||||
.demangled_symbol_name
|
.on_disabled_hover_text("Scratch configuration missing")
|
||||||
.as_deref()
|
.clicked()
|
||||||
.unwrap_or(&selected_symbol.symbol_name);
|
{
|
||||||
let mut job = LayoutJob::simple(
|
if let Some((_section, symbol)) = left_ctx.and_then(|ctx| {
|
||||||
name.to_string(),
|
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
|
||||||
appearance.code_font.clone(),
|
}) {
|
||||||
appearance.highlight_color,
|
ret = Some(DiffViewAction::CreateScratch(symbol.name.clone()));
|
||||||
column_width,
|
}
|
||||||
);
|
}
|
||||||
job.wrap.break_anywhere = true;
|
});
|
||||||
job.wrap.max_rows = 1;
|
|
||||||
ui.label(job);
|
|
||||||
|
|
||||||
ui.scope(|ui| {
|
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
|
||||||
ui.label("Diff target:");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
|
if let Some((_section, symbol)) = left_ctx
|
||||||
|
.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)))
|
||||||
|
{
|
||||||
|
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
|
||||||
|
ui.label(
|
||||||
|
RichText::new(name)
|
||||||
|
.font(appearance.code_font.clone())
|
||||||
|
.color(appearance.highlight_color),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ui.label(
|
||||||
|
RichText::new("Missing")
|
||||||
|
.font(appearance.code_font.clone())
|
||||||
|
.color(appearance.replace_color),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if column == 1 {
|
||||||
// Right column
|
// Right column
|
||||||
ui.allocate_ui_with_layout(
|
ui.horizontal(|ui| {
|
||||||
Vec2 { x: column_width, y: 100.0 },
|
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
|
||||||
Layout::top_down(Align::Min),
|
ret = Some(DiffViewAction::Build);
|
||||||
|ui| {
|
}
|
||||||
ui.set_width(column_width);
|
ui.scope(|ui| {
|
||||||
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
|
if state.build_running {
|
||||||
|
ui.colored_label(appearance.replace_color, "Building…");
|
||||||
|
} else {
|
||||||
|
ui.label("Last built:");
|
||||||
|
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||||
|
ui.label(
|
||||||
|
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.separator();
|
||||||
|
if ui
|
||||||
|
.add_enabled(state.source_path_available, egui::Button::new("🖹 Source file"))
|
||||||
|
.on_hover_text_at_pointer("Open the source file in the default editor")
|
||||||
|
.on_disabled_hover_text("Source file metadata missing")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
ret = Some(DiffViewAction::OpenSourcePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| {
|
||||||
if ui
|
ctx.symbol_ref.map(|symbol_ref| {
|
||||||
.add_enabled(!state.build_running, egui::Button::new("Build"))
|
(ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref))
|
||||||
.clicked()
|
})
|
||||||
{
|
}) {
|
||||||
state.queue_build = true;
|
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
|
||||||
}
|
ui.label(
|
||||||
ui.scope(|ui| {
|
RichText::new(name)
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
.font(appearance.code_font.clone())
|
||||||
ui.style_mut().wrap = Some(false);
|
.color(appearance.highlight_color),
|
||||||
if state.build_running {
|
);
|
||||||
ui.colored_label(appearance.replace_color, "Building…");
|
if let Some(match_percent) = symbol_diff.match_percent {
|
||||||
} else {
|
ui.label(
|
||||||
ui.label("Last built:");
|
RichText::new(format!("{:.0}%", match_percent.floor()))
|
||||||
let format =
|
.font(appearance.code_font.clone())
|
||||||
format_description::parse("[hour]:[minute]:[second]").unwrap();
|
.color(match_color_for_symbol(match_percent, appearance)),
|
||||||
ui.label(
|
);
|
||||||
result
|
}
|
||||||
.time
|
} else {
|
||||||
.to_offset(appearance.utc_offset)
|
ui.label(
|
||||||
.format(&format)
|
RichText::new("Missing")
|
||||||
.unwrap(),
|
.font(appearance.code_font.clone())
|
||||||
);
|
.color(appearance.replace_color),
|
||||||
}
|
);
|
||||||
});
|
}
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ui.scope(|ui| {
|
hotkeys::check_scroll_hotkeys(ui, true);
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
|
||||||
if let Some(match_percent) = result
|
|
||||||
.second_obj
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|(obj, diff)| {
|
|
||||||
find_symbol(obj, selected_symbol).map(|sref| {
|
|
||||||
&diff.sections[sref.section_idx].symbols[sref.symbol_idx]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.and_then(|symbol| symbol.match_percent)
|
|
||||||
{
|
|
||||||
ui.colored_label(
|
|
||||||
match_color_for_symbol(match_percent, appearance),
|
|
||||||
&format!("{match_percent:.0}%"),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ui.colored_label(appearance.replace_color, "Missing");
|
|
||||||
}
|
|
||||||
ui.label("Diff base:");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
// Table
|
// Table
|
||||||
StripBuilder::new(ui).size(Size::remainder()).vertical(|mut strip| {
|
render_strips(ui, available_width, 2, |ui, column| {
|
||||||
strip.strip(|builder| {
|
if column == 0 {
|
||||||
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
|
if let Some(ctx) = left_ctx {
|
||||||
strip.cell(|ui| {
|
extab_ui(ui, ctx, appearance, column);
|
||||||
extab_ui(ui, result.first_obj.as_ref(), selected_symbol, appearance, true);
|
}
|
||||||
});
|
} else if column == 1 {
|
||||||
strip.cell(|ui| {
|
if let Some(ctx) = right_ctx {
|
||||||
extab_ui(ui, result.second_obj.as_ref(), selected_symbol, appearance, false);
|
extab_ui(ui, ctx, appearance, column);
|
||||||
});
|
}
|
||||||
});
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
ret
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,85 @@
|
|||||||
use std::default::Default;
|
use std::{cmp::Ordering, default::Default};
|
||||||
|
|
||||||
use egui::{text::LayoutJob, Align, Label, Layout, Response, Sense, Vec2, Widget};
|
use egui::{text::LayoutJob, Id, Label, Layout, Response, RichText, Sense, Widget};
|
||||||
use egui_extras::{Column, TableBuilder, TableRow};
|
use egui_extras::TableRow;
|
||||||
use objdiff_core::{
|
use objdiff_core::{
|
||||||
arch::ObjArch,
|
|
||||||
diff::{
|
diff::{
|
||||||
display::{display_diff, DiffText, HighlightKind},
|
display::{display_diff, DiffText, HighlightKind},
|
||||||
ObjDiff, ObjInsDiff, ObjInsDiffKind,
|
ObjDiff, ObjInsDiff, ObjInsDiffKind,
|
||||||
},
|
},
|
||||||
obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSymbol, SymbolRef},
|
obj::{
|
||||||
|
ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSectionKind, ObjSymbol,
|
||||||
|
SymbolRef,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use time::format_description;
|
use time::format_description;
|
||||||
|
|
||||||
use crate::views::{
|
use crate::{
|
||||||
appearance::Appearance,
|
hotkeys,
|
||||||
symbol_diff::{match_color_for_symbol, DiffViewState, SymbolRefByName, View},
|
views::{
|
||||||
|
appearance::Appearance,
|
||||||
|
column_layout::{render_header, render_strips, render_table},
|
||||||
|
symbol_diff::{
|
||||||
|
match_color_for_symbol, symbol_list_ui, DiffViewAction, DiffViewNavigation,
|
||||||
|
DiffViewState, SymbolDiffContext, SymbolFilter, SymbolRefByName, SymbolViewState, View,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct FunctionViewState {
|
pub struct FunctionViewState {
|
||||||
pub highlight: HighlightKind,
|
left_highlight: HighlightKind,
|
||||||
|
right_highlight: HighlightKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FunctionViewState {
|
||||||
|
pub fn highlight(&self, column: usize) -> &HighlightKind {
|
||||||
|
match column {
|
||||||
|
0 => &self.left_highlight,
|
||||||
|
1 => &self.right_highlight,
|
||||||
|
_ => &HighlightKind::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_highlight(&mut self, column: usize, highlight: HighlightKind) {
|
||||||
|
match column {
|
||||||
|
0 => {
|
||||||
|
if highlight == self.left_highlight {
|
||||||
|
if highlight == self.right_highlight {
|
||||||
|
self.left_highlight = HighlightKind::None;
|
||||||
|
self.right_highlight = HighlightKind::None;
|
||||||
|
} else {
|
||||||
|
self.right_highlight = self.left_highlight.clone();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.left_highlight = highlight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
if highlight == self.right_highlight {
|
||||||
|
if highlight == self.left_highlight {
|
||||||
|
self.left_highlight = HighlightKind::None;
|
||||||
|
self.right_highlight = HighlightKind::None;
|
||||||
|
} else {
|
||||||
|
self.left_highlight = self.right_highlight.clone();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.right_highlight = highlight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_highlight(&mut self) {
|
||||||
|
self.left_highlight = HighlightKind::None;
|
||||||
|
self.right_highlight = HighlightKind::None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ins_hover_ui(
|
fn ins_hover_ui(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
arch: &dyn ObjArch,
|
obj: &ObjInfo,
|
||||||
section: &ObjSection,
|
section: &ObjSection,
|
||||||
ins: &ObjIns,
|
ins: &ObjIns,
|
||||||
symbol: &ObjSymbol,
|
symbol: &ObjSymbol,
|
||||||
@@ -32,7 +87,7 @@ fn ins_hover_ui(
|
|||||||
) {
|
) {
|
||||||
ui.scope(|ui| {
|
ui.scope(|ui| {
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
ui.style_mut().wrap = Some(false);
|
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||||
|
|
||||||
let offset = ins.address - section.address;
|
let offset = ins.address - section.address;
|
||||||
ui.label(format!(
|
ui.label(format!(
|
||||||
@@ -67,29 +122,51 @@ fn ins_hover_ui(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(reloc) = &ins.reloc {
|
if let Some(reloc) = &ins.reloc {
|
||||||
ui.label(format!("Relocation type: {}", arch.display_reloc(reloc.flags)));
|
ui.label(format!("Relocation type: {}", obj.arch.display_reloc(reloc.flags)));
|
||||||
ui.colored_label(appearance.highlight_color, format!("Name: {}", reloc.target.name));
|
let addend_str = match reloc.addend.cmp(&0i64) {
|
||||||
if let Some(section) = &reloc.target_section {
|
Ordering::Greater => format!("+{:x}", reloc.addend),
|
||||||
ui.colored_label(appearance.highlight_color, format!("Section: {section}"));
|
Ordering::Less => format!("-{:x}", -reloc.addend),
|
||||||
|
_ => "".to_string(),
|
||||||
|
};
|
||||||
|
ui.colored_label(
|
||||||
|
appearance.highlight_color,
|
||||||
|
format!("Name: {}{}", reloc.target.name, addend_str),
|
||||||
|
);
|
||||||
|
if let Some(orig_section_index) = reloc.target.orig_section_index {
|
||||||
|
if let Some(section) =
|
||||||
|
obj.sections.iter().find(|s| s.orig_index == orig_section_index)
|
||||||
|
{
|
||||||
|
ui.colored_label(
|
||||||
|
appearance.highlight_color,
|
||||||
|
format!("Section: {}", section.name),
|
||||||
|
);
|
||||||
|
}
|
||||||
ui.colored_label(
|
ui.colored_label(
|
||||||
appearance.highlight_color,
|
appearance.highlight_color,
|
||||||
format!("Address: {:x}", reloc.target.address),
|
format!("Address: {:x}{}", reloc.target.address, addend_str),
|
||||||
);
|
);
|
||||||
ui.colored_label(
|
ui.colored_label(
|
||||||
appearance.highlight_color,
|
appearance.highlight_color,
|
||||||
format!("Size: {:x}", reloc.target.size),
|
format!("Size: {:x}", reloc.target.size),
|
||||||
);
|
);
|
||||||
|
if let Some(s) = obj.arch.display_ins_data(ins) {
|
||||||
|
ui.colored_label(appearance.highlight_color, s);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ui.colored_label(appearance.highlight_color, "Extern".to_string());
|
ui.colored_label(appearance.highlight_color, "Extern".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(decoded) = rlwinmdec::decode(&ins.formatted) {
|
||||||
|
ui.colored_label(appearance.highlight_color, decoded.trim());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ins_context_menu(ui: &mut egui::Ui, section: &ObjSection, ins: &ObjIns, symbol: &ObjSymbol) {
|
fn ins_context_menu(ui: &mut egui::Ui, section: &ObjSection, ins: &ObjIns, symbol: &ObjSymbol) {
|
||||||
ui.scope(|ui| {
|
ui.scope(|ui| {
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
ui.style_mut().wrap = Some(false);
|
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||||
|
|
||||||
if ui.button(format!("Copy \"{}\"", ins.formatted)).clicked() {
|
if ui.button(format!("Copy \"{}\"", ins.formatted)).clicked() {
|
||||||
ui.output_mut(|output| output.copied_text.clone_from(&ins.formatted));
|
ui.output_mut(|output| output.copied_text.clone_from(&ins.formatted));
|
||||||
@@ -167,15 +244,19 @@ fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<Symbo
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
#[expect(clippy::too_many_arguments)]
|
||||||
fn diff_text_ui(
|
fn diff_text_ui(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
text: DiffText<'_>,
|
text: DiffText<'_>,
|
||||||
ins_diff: &ObjInsDiff,
|
ins_diff: &ObjInsDiff,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
ins_view_state: &mut FunctionViewState,
|
ins_view_state: &FunctionViewState,
|
||||||
|
column: usize,
|
||||||
space_width: f32,
|
space_width: f32,
|
||||||
response_cb: impl Fn(Response) -> Response,
|
response_cb: impl Fn(Response) -> Response,
|
||||||
) {
|
) -> Option<DiffViewAction> {
|
||||||
|
let mut ret = None;
|
||||||
let label_text;
|
let label_text;
|
||||||
let mut base_color = match ins_diff.kind {
|
let mut base_color = match ins_diff.kind {
|
||||||
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
|
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
|
||||||
@@ -222,14 +303,18 @@ fn diff_text_ui(
|
|||||||
base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
|
base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DiffText::Symbol(sym) => {
|
DiffText::Symbol(sym, diff) => {
|
||||||
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
|
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
|
||||||
label_text = name.clone();
|
label_text = name.clone();
|
||||||
base_color = appearance.emphasized_text_color;
|
if let Some(diff) = diff {
|
||||||
|
base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
|
||||||
|
} else {
|
||||||
|
base_color = appearance.emphasized_text_color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
DiffText::Spacing(n) => {
|
DiffText::Spacing(n) => {
|
||||||
ui.add_space(n as f32 * space_width);
|
ui.add_space(n as f32 * space_width);
|
||||||
return;
|
return ret;
|
||||||
}
|
}
|
||||||
DiffText::Eol => {
|
DiffText::Eol => {
|
||||||
label_text = "\n".to_string();
|
label_text = "\n".to_string();
|
||||||
@@ -237,7 +322,7 @@ fn diff_text_ui(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let len = label_text.len();
|
let len = label_text.len();
|
||||||
let highlight = ins_view_state.highlight == text;
|
let highlight = *ins_view_state.highlight(column) == text;
|
||||||
let mut response = Label::new(LayoutJob::single_section(
|
let mut response = Label::new(LayoutJob::single_section(
|
||||||
label_text,
|
label_text,
|
||||||
appearance.code_text_format(base_color, highlight),
|
appearance.code_text_format(base_color, highlight),
|
||||||
@@ -246,247 +331,542 @@ fn diff_text_ui(
|
|||||||
.ui(ui);
|
.ui(ui);
|
||||||
response = response_cb(response);
|
response = response_cb(response);
|
||||||
if response.clicked() {
|
if response.clicked() {
|
||||||
if highlight {
|
ret = Some(DiffViewAction::SetDiffHighlight(column, text.into()));
|
||||||
ins_view_state.highlight = HighlightKind::None;
|
|
||||||
} else {
|
|
||||||
ins_view_state.highlight = text.into();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if len < pad_to {
|
if len < pad_to {
|
||||||
ui.add_space((pad_to - len) as f32 * space_width);
|
ui.add_space((pad_to - len) as f32 * space_width);
|
||||||
}
|
}
|
||||||
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
fn asm_row_ui(
|
fn asm_row_ui(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
ins_diff: &ObjInsDiff,
|
ins_diff: &ObjInsDiff,
|
||||||
symbol: &ObjSymbol,
|
symbol: &ObjSymbol,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
ins_view_state: &mut FunctionViewState,
|
ins_view_state: &FunctionViewState,
|
||||||
|
column: usize,
|
||||||
response_cb: impl Fn(Response) -> Response,
|
response_cb: impl Fn(Response) -> Response,
|
||||||
) {
|
) -> Option<DiffViewAction> {
|
||||||
|
let mut ret = None;
|
||||||
ui.spacing_mut().item_spacing.x = 0.0;
|
ui.spacing_mut().item_spacing.x = 0.0;
|
||||||
|
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||||
if ins_diff.kind != ObjInsDiffKind::None {
|
if ins_diff.kind != ObjInsDiffKind::None {
|
||||||
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
|
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
|
||||||
}
|
}
|
||||||
let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' '));
|
let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' '));
|
||||||
display_diff(ins_diff, symbol.address, |text| {
|
display_diff(ins_diff, symbol.address, |text| {
|
||||||
diff_text_ui(ui, text, ins_diff, appearance, ins_view_state, space_width, &response_cb);
|
if let Some(action) = diff_text_ui(
|
||||||
|
ui,
|
||||||
|
text,
|
||||||
|
ins_diff,
|
||||||
|
appearance,
|
||||||
|
ins_view_state,
|
||||||
|
column,
|
||||||
|
space_width,
|
||||||
|
&response_cb,
|
||||||
|
) {
|
||||||
|
ret = Some(action);
|
||||||
|
}
|
||||||
Ok::<_, ()>(())
|
Ok::<_, ()>(())
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
fn asm_col_ui(
|
fn asm_col_ui(
|
||||||
row: &mut TableRow<'_, '_>,
|
row: &mut TableRow<'_, '_>,
|
||||||
obj: &(ObjInfo, ObjDiff),
|
ctx: FunctionDiffContext<'_>,
|
||||||
symbol_ref: SymbolRef,
|
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
ins_view_state: &mut FunctionViewState,
|
ins_view_state: &FunctionViewState,
|
||||||
) {
|
column: usize,
|
||||||
let (section, symbol) = obj.0.section_symbol(symbol_ref);
|
) -> Option<DiffViewAction> {
|
||||||
let section = section.unwrap();
|
let mut ret = None;
|
||||||
let ins_diff = &obj.1.symbol_diff(symbol_ref).instructions[row.index()];
|
let symbol_ref = ctx.symbol_ref?;
|
||||||
|
let (section, symbol) = ctx.obj.section_symbol(symbol_ref);
|
||||||
|
let section = section?;
|
||||||
|
let ins_diff = &ctx.diff.symbol_diff(symbol_ref).instructions[row.index()];
|
||||||
let response_cb = |response: Response| {
|
let response_cb = |response: Response| {
|
||||||
if let Some(ins) = &ins_diff.ins {
|
if let Some(ins) = &ins_diff.ins {
|
||||||
response.context_menu(|ui| ins_context_menu(ui, section, ins, symbol));
|
response.context_menu(|ui| ins_context_menu(ui, section, ins, symbol));
|
||||||
response.on_hover_ui_at_pointer(|ui| {
|
response.on_hover_ui_at_pointer(|ui| {
|
||||||
ins_hover_ui(ui, obj.0.arch.as_ref(), section, ins, symbol, appearance)
|
ins_hover_ui(ui, ctx.obj, section, ins, symbol, appearance)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let (_, response) = row.col(|ui| {
|
let (_, response) = row.col(|ui| {
|
||||||
asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state, response_cb);
|
if let Some(action) =
|
||||||
|
asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state, column, response_cb)
|
||||||
|
{
|
||||||
|
ret = Some(action);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
response_cb(response);
|
response_cb(response);
|
||||||
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
fn empty_col_ui(row: &mut TableRow<'_, '_>) {
|
#[must_use]
|
||||||
row.col(|ui| {
|
#[expect(clippy::too_many_arguments)]
|
||||||
ui.label("");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn asm_table_ui(
|
fn asm_table_ui(
|
||||||
table: TableBuilder<'_>,
|
ui: &mut egui::Ui,
|
||||||
left_obj: Option<&(ObjInfo, ObjDiff)>,
|
available_width: f32,
|
||||||
right_obj: Option<&(ObjInfo, ObjDiff)>,
|
left_ctx: Option<FunctionDiffContext<'_>>,
|
||||||
selected_symbol: &SymbolRefByName,
|
right_ctx: Option<FunctionDiffContext<'_>>,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
ins_view_state: &mut FunctionViewState,
|
ins_view_state: &FunctionViewState,
|
||||||
) -> Option<()> {
|
symbol_state: &SymbolViewState,
|
||||||
let left_symbol = left_obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol));
|
open_sections: (Option<bool>, Option<bool>),
|
||||||
let right_symbol = right_obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol));
|
) -> Option<DiffViewAction> {
|
||||||
let instructions_len = match (left_symbol, right_symbol) {
|
let mut ret = None;
|
||||||
(Some(left_symbol_ref), Some(right_symbol_ref)) => {
|
let left_len = left_ctx.and_then(|ctx| {
|
||||||
let left_len = left_obj.unwrap().1.symbol_diff(left_symbol_ref).instructions.len();
|
ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len())
|
||||||
let right_len = right_obj.unwrap().1.symbol_diff(right_symbol_ref).instructions.len();
|
});
|
||||||
debug_assert_eq!(left_len, right_len);
|
let right_len = right_ctx.and_then(|ctx| {
|
||||||
|
ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len())
|
||||||
|
});
|
||||||
|
let instructions_len = match (left_len, right_len) {
|
||||||
|
(Some(left_len), Some(right_len)) => {
|
||||||
|
if left_len != right_len {
|
||||||
|
ui.label("Instruction count mismatch");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
left_len
|
left_len
|
||||||
}
|
}
|
||||||
(Some(left_symbol_ref), None) => {
|
(Some(left_len), None) => left_len,
|
||||||
left_obj.unwrap().1.symbol_diff(left_symbol_ref).instructions.len()
|
(None, Some(right_len)) => right_len,
|
||||||
|
(None, None) => {
|
||||||
|
ui.label("No symbol selected");
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
(None, Some(right_symbol_ref)) => {
|
|
||||||
right_obj.unwrap().1.symbol_diff(right_symbol_ref).instructions.len()
|
|
||||||
}
|
|
||||||
(None, None) => return None,
|
|
||||||
};
|
};
|
||||||
table.body(|body| {
|
if left_len.is_some() && right_len.is_some() {
|
||||||
body.rows(appearance.code_font.size, instructions_len, |mut row| {
|
// Joint view
|
||||||
if let (Some(left_obj), Some(left_symbol_ref)) = (left_obj, left_symbol) {
|
hotkeys::check_scroll_hotkeys(ui, true);
|
||||||
asm_col_ui(&mut row, left_obj, left_symbol_ref, appearance, ins_view_state);
|
render_table(
|
||||||
} else {
|
ui,
|
||||||
empty_col_ui(&mut row);
|
available_width,
|
||||||
}
|
2,
|
||||||
if let (Some(right_obj), Some(right_symbol_ref)) = (right_obj, right_symbol) {
|
appearance.code_font.size,
|
||||||
asm_col_ui(&mut row, right_obj, right_symbol_ref, appearance, ins_view_state);
|
instructions_len,
|
||||||
} else {
|
|row, column| {
|
||||||
empty_col_ui(&mut row);
|
if column == 0 {
|
||||||
|
if let Some(ctx) = left_ctx {
|
||||||
|
if let Some(action) =
|
||||||
|
asm_col_ui(row, ctx, appearance, ins_view_state, column)
|
||||||
|
{
|
||||||
|
ret = Some(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if column == 1 {
|
||||||
|
if let Some(ctx) = right_ctx {
|
||||||
|
if let Some(action) =
|
||||||
|
asm_col_ui(row, ctx, appearance, ins_view_state, column)
|
||||||
|
{
|
||||||
|
ret = Some(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if row.response().clicked() {
|
||||||
|
ret = Some(DiffViewAction::ClearDiffHighlight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Split view, one side is the symbol list
|
||||||
|
render_strips(ui, available_width, 2, |ui, column| {
|
||||||
|
if column == 0 {
|
||||||
|
if let Some(ctx) = left_ctx {
|
||||||
|
if ctx.has_symbol() {
|
||||||
|
hotkeys::check_scroll_hotkeys(ui, false);
|
||||||
|
render_table(
|
||||||
|
ui,
|
||||||
|
available_width / 2.0,
|
||||||
|
1,
|
||||||
|
appearance.code_font.size,
|
||||||
|
instructions_len,
|
||||||
|
|row, column| {
|
||||||
|
if let Some(action) =
|
||||||
|
asm_col_ui(row, ctx, appearance, ins_view_state, column)
|
||||||
|
{
|
||||||
|
ret = Some(action);
|
||||||
|
}
|
||||||
|
if row.response().clicked() {
|
||||||
|
ret = Some(DiffViewAction::ClearDiffHighlight);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if let Some((right_ctx, right_symbol_ref)) =
|
||||||
|
right_ctx.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| (ctx, symbol_ref)))
|
||||||
|
{
|
||||||
|
if let Some(action) = symbol_list_ui(
|
||||||
|
ui,
|
||||||
|
SymbolDiffContext { obj: ctx.obj, diff: ctx.diff },
|
||||||
|
None,
|
||||||
|
symbol_state,
|
||||||
|
SymbolFilter::Mapping(right_symbol_ref),
|
||||||
|
appearance,
|
||||||
|
column,
|
||||||
|
open_sections.0,
|
||||||
|
) {
|
||||||
|
match action {
|
||||||
|
DiffViewAction::Navigate(DiffViewNavigation {
|
||||||
|
left_symbol: Some(left_symbol_ref),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
let (right_section, right_symbol) =
|
||||||
|
right_ctx.obj.section_symbol(right_symbol_ref);
|
||||||
|
ret = Some(DiffViewAction::SetMapping(
|
||||||
|
match right_section.map(|s| s.kind) {
|
||||||
|
Some(ObjSectionKind::Code) => View::FunctionDiff,
|
||||||
|
_ => View::SymbolDiff,
|
||||||
|
},
|
||||||
|
left_symbol_ref,
|
||||||
|
SymbolRefByName::new(right_symbol, right_section),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
ret = Some(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ui.label("No left object");
|
||||||
|
}
|
||||||
|
} else if column == 1 {
|
||||||
|
if let Some(ctx) = right_ctx {
|
||||||
|
if ctx.has_symbol() {
|
||||||
|
hotkeys::check_scroll_hotkeys(ui, false);
|
||||||
|
render_table(
|
||||||
|
ui,
|
||||||
|
available_width / 2.0,
|
||||||
|
1,
|
||||||
|
appearance.code_font.size,
|
||||||
|
instructions_len,
|
||||||
|
|row, column| {
|
||||||
|
if let Some(action) =
|
||||||
|
asm_col_ui(row, ctx, appearance, ins_view_state, column)
|
||||||
|
{
|
||||||
|
ret = Some(action);
|
||||||
|
}
|
||||||
|
if row.response().clicked() {
|
||||||
|
ret = Some(DiffViewAction::ClearDiffHighlight);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if let Some((left_ctx, left_symbol_ref)) =
|
||||||
|
left_ctx.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| (ctx, symbol_ref)))
|
||||||
|
{
|
||||||
|
if let Some(action) = symbol_list_ui(
|
||||||
|
ui,
|
||||||
|
SymbolDiffContext { obj: ctx.obj, diff: ctx.diff },
|
||||||
|
None,
|
||||||
|
symbol_state,
|
||||||
|
SymbolFilter::Mapping(left_symbol_ref),
|
||||||
|
appearance,
|
||||||
|
column,
|
||||||
|
open_sections.1,
|
||||||
|
) {
|
||||||
|
match action {
|
||||||
|
DiffViewAction::Navigate(DiffViewNavigation {
|
||||||
|
right_symbol: Some(right_symbol_ref),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
let (left_section, left_symbol) =
|
||||||
|
left_ctx.obj.section_symbol(left_symbol_ref);
|
||||||
|
ret = Some(DiffViewAction::SetMapping(
|
||||||
|
match left_section.map(|s| s.kind) {
|
||||||
|
Some(ObjSectionKind::Code) => View::FunctionDiff,
|
||||||
|
_ => View::SymbolDiff,
|
||||||
|
},
|
||||||
|
SymbolRefByName::new(left_symbol, left_section),
|
||||||
|
right_symbol_ref,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
ret = Some(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ui.label("No right object");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
Some(())
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
|
#[derive(Clone, Copy)]
|
||||||
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
|
pub struct FunctionDiffContext<'a> {
|
||||||
else {
|
pub obj: &'a ObjInfo,
|
||||||
return;
|
pub diff: &'a ObjDiff,
|
||||||
|
pub symbol_ref: Option<SymbolRef>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> FunctionDiffContext<'a> {
|
||||||
|
pub fn new(
|
||||||
|
obj: Option<&'a (ObjInfo, ObjDiff)>,
|
||||||
|
selected_symbol: Option<&SymbolRefByName>,
|
||||||
|
) -> Option<Self> {
|
||||||
|
obj.map(|(obj, diff)| Self {
|
||||||
|
obj,
|
||||||
|
diff,
|
||||||
|
symbol_ref: selected_symbol.and_then(|s| find_symbol(obj, s)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn has_symbol(&self) -> bool { self.symbol_ref.is_some() }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn function_diff_ui(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
state: &DiffViewState,
|
||||||
|
appearance: &Appearance,
|
||||||
|
) -> Option<DiffViewAction> {
|
||||||
|
let mut ret = None;
|
||||||
|
let Some(result) = &state.build else {
|
||||||
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut left_ctx = FunctionDiffContext::new(
|
||||||
|
result.first_obj.as_ref(),
|
||||||
|
state.symbol_state.left_symbol.as_ref(),
|
||||||
|
);
|
||||||
|
let mut right_ctx = FunctionDiffContext::new(
|
||||||
|
result.second_obj.as_ref(),
|
||||||
|
state.symbol_state.right_symbol.as_ref(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// If one side is missing a symbol, but the diff process found a match, use that symbol
|
||||||
|
let left_diff_symbol = left_ctx.and_then(|ctx| {
|
||||||
|
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
|
||||||
|
});
|
||||||
|
let right_diff_symbol = right_ctx.and_then(|ctx| {
|
||||||
|
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
|
||||||
|
});
|
||||||
|
if left_diff_symbol.is_some() && right_ctx.is_some_and(|ctx| !ctx.has_symbol()) {
|
||||||
|
let (right_section, right_symbol) =
|
||||||
|
right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap());
|
||||||
|
let symbol_ref = SymbolRefByName::new(right_symbol, right_section);
|
||||||
|
right_ctx = FunctionDiffContext::new(result.second_obj.as_ref(), Some(&symbol_ref));
|
||||||
|
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
|
||||||
|
view: Some(View::FunctionDiff),
|
||||||
|
left_symbol: state.symbol_state.left_symbol.clone(),
|
||||||
|
right_symbol: Some(symbol_ref),
|
||||||
|
}));
|
||||||
|
} else if right_diff_symbol.is_some() && left_ctx.is_some_and(|ctx| !ctx.has_symbol()) {
|
||||||
|
let (left_section, left_symbol) =
|
||||||
|
left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap());
|
||||||
|
let symbol_ref = SymbolRefByName::new(left_symbol, left_section);
|
||||||
|
left_ctx = FunctionDiffContext::new(result.first_obj.as_ref(), Some(&symbol_ref));
|
||||||
|
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
|
||||||
|
view: Some(View::FunctionDiff),
|
||||||
|
left_symbol: Some(symbol_ref),
|
||||||
|
right_symbol: state.symbol_state.right_symbol.clone(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If both sides are missing a symbol, switch to symbol diff view
|
||||||
|
if right_ctx.is_some_and(|ctx| !ctx.has_symbol())
|
||||||
|
&& left_ctx.is_some_and(|ctx| !ctx.has_symbol())
|
||||||
|
{
|
||||||
|
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||||
|
}
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
let available_width = ui.available_width();
|
let available_width = ui.available_width();
|
||||||
let column_width = available_width / 2.0;
|
let mut open_sections = (None, None);
|
||||||
ui.allocate_ui_with_layout(
|
render_header(ui, available_width, 2, |ui, column| {
|
||||||
Vec2 { x: available_width, y: 100.0 },
|
if column == 0 {
|
||||||
Layout::left_to_right(Align::Min),
|
|
||||||
|ui| {
|
|
||||||
// Left column
|
// Left column
|
||||||
ui.allocate_ui_with_layout(
|
ui.horizontal(|ui| {
|
||||||
Vec2 { x: column_width, y: 100.0 },
|
if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) {
|
||||||
Layout::top_down(Align::Min),
|
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||||
|ui| {
|
}
|
||||||
ui.set_width(column_width);
|
ui.separator();
|
||||||
|
if ui
|
||||||
|
.add_enabled(
|
||||||
|
!state.scratch_running
|
||||||
|
&& state.scratch_available
|
||||||
|
&& left_ctx.is_some_and(|ctx| ctx.has_symbol()),
|
||||||
|
egui::Button::new("📲 decomp.me"),
|
||||||
|
)
|
||||||
|
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
|
||||||
|
.on_disabled_hover_text("Scratch configuration missing")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
if let Some((_section, symbol)) = left_ctx.and_then(|ctx| {
|
||||||
|
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
|
||||||
|
}) {
|
||||||
|
ret = Some(DiffViewAction::CreateScratch(symbol.name.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
if let Some((_section, symbol)) = left_ctx
|
||||||
if ui.button("⏴ Back").clicked() {
|
.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)))
|
||||||
state.current_view = View::SymbolDiff;
|
{
|
||||||
|
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
|
||||||
|
ui.label(
|
||||||
|
RichText::new(name)
|
||||||
|
.font(appearance.code_font.clone())
|
||||||
|
.color(appearance.highlight_color),
|
||||||
|
);
|
||||||
|
if right_ctx.is_some_and(|m| m.has_symbol())
|
||||||
|
&& (ui
|
||||||
|
.button("Change target")
|
||||||
|
.on_hover_text_at_pointer("Choose a different symbol to use as the target")
|
||||||
|
.clicked()
|
||||||
|
|| hotkeys::consume_change_target_shortcut(ui.ctx()))
|
||||||
|
{
|
||||||
|
if let Some(symbol_ref) = state.symbol_state.right_symbol.as_ref() {
|
||||||
|
ret = Some(DiffViewAction::SelectingLeft(symbol_ref.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ui.label(
|
||||||
|
RichText::new("Missing")
|
||||||
|
.font(appearance.code_font.clone())
|
||||||
|
.color(appearance.replace_color),
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new("Choose target symbol")
|
||||||
|
.font(appearance.code_font.clone())
|
||||||
|
.color(appearance.highlight_color),
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.with_layout(Layout::right_to_left(egui::Align::TOP), |ui| {
|
||||||
|
if ui.small_button("⏷").on_hover_text_at_pointer("Expand all").clicked() {
|
||||||
|
open_sections.0 = Some(true);
|
||||||
}
|
}
|
||||||
|
if ui.small_button("⏶").on_hover_text_at_pointer("Collapse all").clicked()
|
||||||
|
{
|
||||||
|
open_sections.0 = Some(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if column == 1 {
|
||||||
|
// Right column
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
|
||||||
|
ret = Some(DiffViewAction::Build);
|
||||||
|
}
|
||||||
|
ui.scope(|ui| {
|
||||||
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
|
if state.build_running {
|
||||||
|
ui.colored_label(appearance.replace_color, "Building…");
|
||||||
|
} else {
|
||||||
|
ui.label("Last built:");
|
||||||
|
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||||
|
ui.label(
|
||||||
|
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.separator();
|
||||||
|
if ui
|
||||||
|
.add_enabled(state.source_path_available, egui::Button::new("🖹 Source file"))
|
||||||
|
.on_hover_text_at_pointer("Open the source file in the default editor")
|
||||||
|
.on_disabled_hover_text("Source file metadata missing")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
ret = Some(DiffViewAction::OpenSourcePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| {
|
||||||
|
ctx.symbol_ref.map(|symbol_ref| {
|
||||||
|
(ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref))
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
|
||||||
|
ui.label(
|
||||||
|
RichText::new(name)
|
||||||
|
.font(appearance.code_font.clone())
|
||||||
|
.color(appearance.highlight_color),
|
||||||
|
);
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if let Some(match_percent) = symbol_diff.match_percent {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(format!("{:.0}%", match_percent.floor()))
|
||||||
|
.font(appearance.code_font.clone())
|
||||||
|
.color(match_color_for_symbol(match_percent, appearance)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if left_ctx.is_some_and(|m| m.has_symbol()) {
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if ui
|
if ui
|
||||||
.add_enabled(
|
.button("Change base")
|
||||||
!state.scratch_running && state.scratch_available,
|
.on_hover_text_at_pointer(
|
||||||
egui::Button::new("📲 decomp.me"),
|
"Choose a different symbol to use as the base",
|
||||||
)
|
)
|
||||||
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
|
|
||||||
.on_disabled_hover_text("Scratch configuration missing")
|
|
||||||
.clicked()
|
.clicked()
|
||||||
|
|| hotkeys::consume_change_base_shortcut(ui.ctx())
|
||||||
{
|
{
|
||||||
state.queue_scratch = true;
|
if let Some(symbol_ref) = state.symbol_state.left_symbol.as_ref() {
|
||||||
}
|
ret = Some(DiffViewAction::SelectingRight(symbol_ref.clone()));
|
||||||
});
|
|
||||||
|
|
||||||
let name = selected_symbol
|
|
||||||
.demangled_symbol_name
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or(&selected_symbol.symbol_name);
|
|
||||||
let mut job = LayoutJob::simple(
|
|
||||||
name.to_string(),
|
|
||||||
appearance.code_font.clone(),
|
|
||||||
appearance.highlight_color,
|
|
||||||
column_width,
|
|
||||||
);
|
|
||||||
job.wrap.break_anywhere = true;
|
|
||||||
job.wrap.max_rows = 1;
|
|
||||||
ui.label(job);
|
|
||||||
|
|
||||||
ui.scope(|ui| {
|
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
|
||||||
ui.label("Diff target:");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Right column
|
|
||||||
ui.allocate_ui_with_layout(
|
|
||||||
Vec2 { x: column_width, y: 100.0 },
|
|
||||||
Layout::top_down(Align::Min),
|
|
||||||
|ui| {
|
|
||||||
ui.set_width(column_width);
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
if ui
|
|
||||||
.add_enabled(!state.build_running, egui::Button::new("Build"))
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
state.queue_build = true;
|
|
||||||
}
|
|
||||||
ui.scope(|ui| {
|
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
|
||||||
ui.style_mut().wrap = Some(false);
|
|
||||||
if state.build_running {
|
|
||||||
ui.colored_label(appearance.replace_color, "Building…");
|
|
||||||
} else {
|
|
||||||
ui.label("Last built:");
|
|
||||||
let format =
|
|
||||||
format_description::parse("[hour]:[minute]:[second]").unwrap();
|
|
||||||
ui.label(
|
|
||||||
result
|
|
||||||
.time
|
|
||||||
.to_offset(appearance.utc_offset)
|
|
||||||
.format(&format)
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.scope(|ui| {
|
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
|
||||||
if let Some(match_percent) = result
|
|
||||||
.second_obj
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|(obj, diff)| {
|
|
||||||
find_symbol(obj, selected_symbol).map(|sref| {
|
|
||||||
&diff.sections[sref.section_idx].symbols[sref.symbol_idx]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.and_then(|symbol| symbol.match_percent)
|
|
||||||
{
|
|
||||||
ui.colored_label(
|
|
||||||
match_color_for_symbol(match_percent, appearance),
|
|
||||||
&format!("{match_percent:.0}%"),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ui.colored_label(appearance.replace_color, "Missing");
|
|
||||||
}
|
}
|
||||||
ui.label("Diff base:");
|
}
|
||||||
});
|
});
|
||||||
},
|
} else {
|
||||||
);
|
ui.label(
|
||||||
},
|
RichText::new("Missing")
|
||||||
);
|
.font(appearance.code_font.clone())
|
||||||
ui.separator();
|
.color(appearance.replace_color),
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new("Choose base symbol")
|
||||||
|
.font(appearance.code_font.clone())
|
||||||
|
.color(appearance.highlight_color),
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.with_layout(Layout::right_to_left(egui::Align::TOP), |ui| {
|
||||||
|
if ui.small_button("⏷").on_hover_text_at_pointer("Expand all").clicked() {
|
||||||
|
open_sections.1 = Some(true);
|
||||||
|
}
|
||||||
|
if ui.small_button("⏶").on_hover_text_at_pointer("Collapse all").clicked()
|
||||||
|
{
|
||||||
|
open_sections.1 = Some(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Table
|
// Table
|
||||||
ui.style_mut().interaction.selectable_labels = false;
|
let id = Id::new(state.symbol_state.left_symbol.as_ref().map(|s| s.symbol_name.as_str()))
|
||||||
let available_height = ui.available_height();
|
.with(state.symbol_state.right_symbol.as_ref().map(|s| s.symbol_name.as_str()));
|
||||||
let table = TableBuilder::new(ui)
|
if let Some(action) = ui
|
||||||
.striped(false)
|
.push_id(id, |ui| {
|
||||||
.cell_layout(Layout::left_to_right(Align::Min))
|
asm_table_ui(
|
||||||
.columns(Column::exact(column_width).clip(true), 2)
|
ui,
|
||||||
.resizable(false)
|
available_width,
|
||||||
.auto_shrink([false, false])
|
left_ctx,
|
||||||
.min_scrolled_height(available_height);
|
right_ctx,
|
||||||
asm_table_ui(
|
appearance,
|
||||||
table,
|
&state.function_state,
|
||||||
result.first_obj.as_ref(),
|
&state.symbol_state,
|
||||||
result.second_obj.as_ref(),
|
open_sections,
|
||||||
selected_symbol,
|
)
|
||||||
appearance,
|
})
|
||||||
&mut state.function_state,
|
.inner
|
||||||
);
|
{
|
||||||
|
ret = Some(action);
|
||||||
|
}
|
||||||
|
ret
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fs::File,
|
fs::File,
|
||||||
|
io::{BufReader, BufWriter},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -46,13 +47,13 @@ pub fn load_graphics_config(path: &Path) -> Result<Option<GraphicsConfig>> {
|
|||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let file = File::open(path)?;
|
let file = BufReader::new(File::open(path)?);
|
||||||
let config: GraphicsConfig = ron::de::from_reader(file)?;
|
let config: GraphicsConfig = ron::de::from_reader(file)?;
|
||||||
Ok(Some(config))
|
Ok(Some(config))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_graphics_config(path: &Path, config: &GraphicsConfig) -> Result<()> {
|
pub fn save_graphics_config(path: &Path, config: &GraphicsConfig) -> Result<()> {
|
||||||
let file = File::create(path)?;
|
let file = BufWriter::new(File::create(path)?);
|
||||||
ron::ser::to_writer(file, config)?;
|
ron::ser::to_writer(file, config)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,160 @@
|
|||||||
use egui::{ProgressBar, RichText, Widget};
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
use crate::{jobs::JobQueue, views::appearance::Appearance};
|
use egui::{ProgressBar, RichText, Widget};
|
||||||
|
use objdiff_core::jobs::{JobQueue, JobStatus};
|
||||||
|
|
||||||
|
use crate::views::appearance::Appearance;
|
||||||
|
|
||||||
pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) {
|
pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) {
|
||||||
ui.label("Jobs");
|
if ui.button("Clear").clicked() {
|
||||||
|
jobs.clear_errored();
|
||||||
|
}
|
||||||
|
|
||||||
let mut remove_job: Option<usize> = None;
|
let mut remove_job: Option<usize> = None;
|
||||||
|
let mut any_jobs = false;
|
||||||
for job in jobs.iter_mut() {
|
for job in jobs.iter_mut() {
|
||||||
let Ok(status) = job.context.status.read() else {
|
let Ok(status) = job.context.status.read() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
ui.group(|ui| {
|
any_jobs = true;
|
||||||
ui.horizontal(|ui| {
|
ui.separator();
|
||||||
ui.label(&status.title);
|
ui.horizontal(|ui| {
|
||||||
if ui.small_button("✖").clicked() {
|
ui.label(&status.title);
|
||||||
if job.handle.is_some() {
|
if ui.small_button("✖").clicked() {
|
||||||
job.should_remove = true;
|
if job.handle.is_some() {
|
||||||
if let Err(e) = job.cancel.send(()) {
|
if let Err(e) = job.cancel.send(()) {
|
||||||
log::error!("Failed to cancel job: {e:?}");
|
log::error!("Failed to cancel job: {e:?}");
|
||||||
}
|
|
||||||
} else {
|
|
||||||
remove_job = Some(job.id);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
let mut bar = ProgressBar::new(status.progress_percent);
|
|
||||||
if let Some(items) = &status.progress_items {
|
|
||||||
bar = bar.text(format!("{} / {}", items[0], items[1]));
|
|
||||||
}
|
|
||||||
bar.ui(ui);
|
|
||||||
const STATUS_LENGTH: usize = 80;
|
|
||||||
if let Some(err) = &status.error {
|
|
||||||
let err_string = format!("{:#}", err);
|
|
||||||
ui.colored_label(
|
|
||||||
appearance.delete_color,
|
|
||||||
if err_string.len() > STATUS_LENGTH - 10 {
|
|
||||||
format!("Error: {}…", &err_string[0..STATUS_LENGTH - 10])
|
|
||||||
} else {
|
|
||||||
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.on_hover_text_at_pointer(RichText::new(err_string).color(appearance.delete_color));
|
|
||||||
} else {
|
|
||||||
ui.label(if status.status.len() > STATUS_LENGTH - 3 {
|
|
||||||
format!("{}…", &status.status[0..STATUS_LENGTH - 3])
|
|
||||||
} else {
|
} else {
|
||||||
format!("{:width$}", &status.status, width = STATUS_LENGTH)
|
remove_job = Some(job.id);
|
||||||
})
|
}
|
||||||
.on_hover_text_at_pointer(&status.status);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
let mut bar = ProgressBar::new(status.progress_percent);
|
||||||
|
if let Some(items) = &status.progress_items {
|
||||||
|
bar = bar.text(format!("{} / {}", items[0], items[1]));
|
||||||
|
}
|
||||||
|
bar.ui(ui);
|
||||||
|
const STATUS_LENGTH: usize = 80;
|
||||||
|
if let Some(err) = &status.error {
|
||||||
|
let err_string = format!("{:#}", err);
|
||||||
|
ui.colored_label(
|
||||||
|
appearance.delete_color,
|
||||||
|
if err_string.len() > STATUS_LENGTH - 10 {
|
||||||
|
format!("Error: {}…", &err_string[0..STATUS_LENGTH - 10])
|
||||||
|
} else {
|
||||||
|
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.on_hover_text_at_pointer(RichText::new(&err_string).color(appearance.delete_color))
|
||||||
|
.context_menu(|ui| {
|
||||||
|
if ui.button("Copy full message").clicked() {
|
||||||
|
ui.output_mut(|o| o.copied_text = err_string);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ui.label(if status.status.len() > STATUS_LENGTH - 3 {
|
||||||
|
format!("{}…", &status.status[0..STATUS_LENGTH - 3])
|
||||||
|
} else {
|
||||||
|
format!("{:width$}", &status.status, width = STATUS_LENGTH)
|
||||||
|
})
|
||||||
|
.on_hover_text_at_pointer(&status.status)
|
||||||
|
.context_menu(|ui| {
|
||||||
|
if ui.button("Copy full message").clicked() {
|
||||||
|
ui.output_mut(|o| o.copied_text = status.status.clone());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !any_jobs {
|
||||||
|
ui.label("No jobs");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(idx) = remove_job {
|
if let Some(idx) = remove_job {
|
||||||
jobs.remove(idx);
|
jobs.remove(idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct JobStatusDisplay {
|
||||||
|
title: String,
|
||||||
|
progress_items: Option<[u32; 2]>,
|
||||||
|
error: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&JobStatus> for JobStatusDisplay {
|
||||||
|
fn from(status: &JobStatus) -> Self {
|
||||||
|
Self {
|
||||||
|
title: status.title.clone(),
|
||||||
|
progress_items: status.progress_items,
|
||||||
|
error: status.error.is_some(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn jobs_menu_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) -> bool {
|
||||||
|
ui.label("Jobs:");
|
||||||
|
let mut statuses = Vec::new();
|
||||||
|
for job in jobs.iter_mut() {
|
||||||
|
let Ok(status) = job.context.status.read() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
statuses.push(JobStatusDisplay::from(&*status));
|
||||||
|
}
|
||||||
|
let running_jobs = statuses.iter().filter(|s| !s.error).count();
|
||||||
|
let error_jobs = statuses.iter().filter(|s| s.error).count();
|
||||||
|
|
||||||
|
let mut clicked = false;
|
||||||
|
let spinner =
|
||||||
|
egui::Spinner::new().size(appearance.ui_font.size * 0.9).color(appearance.text_color);
|
||||||
|
match running_jobs.cmp(&1) {
|
||||||
|
Ordering::Equal => {
|
||||||
|
spinner.ui(ui);
|
||||||
|
let running_job = statuses.iter().find(|s| !s.error).unwrap();
|
||||||
|
let text = if let Some(items) = running_job.progress_items {
|
||||||
|
format!("{} ({}/{})", running_job.title, items[0], items[1])
|
||||||
|
} else {
|
||||||
|
running_job.title.clone()
|
||||||
|
};
|
||||||
|
clicked |= ui.link(RichText::new(text)).clicked();
|
||||||
|
}
|
||||||
|
Ordering::Greater => {
|
||||||
|
spinner.ui(ui);
|
||||||
|
clicked |= ui.link(format!("{} running", running_jobs)).clicked();
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
match error_jobs.cmp(&1) {
|
||||||
|
Ordering::Equal => {
|
||||||
|
let error_job = statuses.iter().find(|s| s.error).unwrap();
|
||||||
|
clicked |= ui
|
||||||
|
.link(
|
||||||
|
RichText::new(format!("{} error", error_job.title))
|
||||||
|
.color(appearance.delete_color),
|
||||||
|
)
|
||||||
|
.clicked();
|
||||||
|
}
|
||||||
|
Ordering::Greater => {
|
||||||
|
clicked |= ui
|
||||||
|
.link(
|
||||||
|
RichText::new(format!("{} errors", error_jobs)).color(appearance.delete_color),
|
||||||
|
)
|
||||||
|
.clicked();
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
if running_jobs == 0 && error_jobs == 0 {
|
||||||
|
clicked |= ui.link("None").clicked();
|
||||||
|
}
|
||||||
|
clicked
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn jobs_window(
|
||||||
|
ctx: &egui::Context,
|
||||||
|
show: &mut bool,
|
||||||
|
jobs: &mut JobQueue,
|
||||||
|
appearance: &Appearance,
|
||||||
|
) {
|
||||||
|
egui::Window::new("Jobs").open(show).show(ctx, |ui| {
|
||||||
|
jobs_ui(ui, jobs, appearance);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use egui::{text::LayoutJob, Color32, FontId, TextFormat};
|
use egui::{text::LayoutJob, Color32, FontId, TextFormat};
|
||||||
|
|
||||||
pub(crate) mod appearance;
|
pub(crate) mod appearance;
|
||||||
|
pub(crate) mod column_layout;
|
||||||
pub(crate) mod config;
|
pub(crate) mod config;
|
||||||
pub(crate) mod data_diff;
|
pub(crate) mod data_diff;
|
||||||
pub(crate) mod debug;
|
pub(crate) mod debug;
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ pub fn rlwinm_decode_window(
|
|||||||
egui::Window::new("Rlwinm Decoder").open(show).show(ctx, |ui| {
|
egui::Window::new("Rlwinm Decoder").open(show).show(ctx, |ui| {
|
||||||
ui.text_edit_singleline(&mut state.text);
|
ui.text_edit_singleline(&mut state.text);
|
||||||
ui.add_space(10.0);
|
ui.add_space(10.0);
|
||||||
if let Some(demangled) = rlwinmdec::decode(&state.text) {
|
if let Some(decoded) = rlwinmdec::decode(&state.text) {
|
||||||
ui.scope(|ui| {
|
ui.scope(|ui| {
|
||||||
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
|
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
|
||||||
ui.colored_label(appearance.replace_color, &demangled);
|
ui.colored_label(appearance.replace_color, decoded.trim());
|
||||||
});
|
});
|
||||||
if ui.button("Copy").clicked() {
|
if ui.button("Copy").clicked() {
|
||||||
ui.output_mut(|output| output.copied_text = demangled);
|
ui.output_mut(|output| output.copied_text = decoded);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ui.scope(|ui| {
|
ui.scope(|ui| {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
4
objdiff-wasm/package-lock.json
generated
4
objdiff-wasm/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "objdiff-wasm",
|
"name": "objdiff-wasm",
|
||||||
"version": "2.0.0-beta.10",
|
"version": "2.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "objdiff-wasm",
|
"name": "objdiff-wasm",
|
||||||
"version": "2.0.0-beta.10",
|
"version": "2.0.0",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@protobuf-ts/runtime": "^2.9.4"
|
"@protobuf-ts/runtime": "^2.9.4"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "objdiff-wasm",
|
"name": "objdiff-wasm",
|
||||||
"version": "2.0.0-beta.10",
|
"version": "2.6.0",
|
||||||
"description": "A local diffing tool for decompilation projects.",
|
"description": "A local diffing tool for decompilation projects.",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Luke Street",
|
"name": "Luke Street",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"build": "tsup",
|
"build": "tsup",
|
||||||
"build:all": "npm run build:wasm && npm run build:proto && npm run build",
|
"build:all": "npm run build:wasm && npm run build:proto && npm run build",
|
||||||
"build:proto": "protoc --ts_out=gen --ts_opt add_pb_suffix,eslint_disable,ts_nocheck,use_proto_field_name --proto_path=../objdiff-core/protos ../objdiff-core/protos/*.proto",
|
"build:proto": "protoc --ts_out=gen --ts_opt add_pb_suffix,eslint_disable,ts_nocheck,use_proto_field_name --proto_path=../objdiff-core/protos ../objdiff-core/protos/*.proto",
|
||||||
"build:wasm": "cd ../objdiff-core && wasm-pack build --out-dir ../objdiff-wasm/pkg --target web -- --features arm,dwarf,ppc,x86,wasm"
|
"build:wasm": "cd ../objdiff-core && wasm-pack build --out-dir ../objdiff-wasm/pkg --target web -- --features arm,arm64,dwarf,config,ppc,x86,wasm"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@protobuf-ts/runtime": "^2.9.4"
|
"@protobuf-ts/runtime": "^2.9.4"
|
||||||
|
|||||||
@@ -111,12 +111,12 @@ async function defer<T>(message: AnyHandlerData, worker?: Worker): Promise<T> {
|
|||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runDiff(left: Uint8Array | undefined, right: Uint8Array | undefined, config?: DiffObjConfig): Promise<DiffResult> {
|
export async function runDiff(left: Uint8Array | undefined, right: Uint8Array | undefined, diff_config?: DiffObjConfig): Promise<DiffResult> {
|
||||||
const data = await defer<Uint8Array>({
|
const data = await defer<Uint8Array>({
|
||||||
type: 'run_diff_proto',
|
type: 'run_diff_proto',
|
||||||
left,
|
left,
|
||||||
right,
|
right,
|
||||||
config
|
diff_config
|
||||||
});
|
});
|
||||||
const parseStart = performance.now();
|
const parseStart = performance.now();
|
||||||
const result = DiffResult.fromBinary(data, {readUnknownField: false});
|
const result = DiffResult.fromBinary(data, {readUnknownField: false});
|
||||||
@@ -194,12 +194,17 @@ export function displayDiff(diff: InstructionDiff, baseAddr: bigint, cb: (text:
|
|||||||
cb({type: 'spacing', count: 4});
|
cb({type: 'spacing', count: 4});
|
||||||
}
|
}
|
||||||
cb({type: 'opcode', mnemonic: ins.mnemonic, opcode: ins.opcode});
|
cb({type: 'opcode', mnemonic: ins.mnemonic, opcode: ins.opcode});
|
||||||
|
let arg_diff_idx = 0; // non-PlainText argument index
|
||||||
for (let i = 0; i < ins.arguments.length; i++) {
|
for (let i = 0; i < ins.arguments.length; i++) {
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
cb({type: 'spacing', count: 1});
|
cb({type: 'spacing', count: 1});
|
||||||
}
|
}
|
||||||
const arg = ins.arguments[i].value;
|
const arg = ins.arguments[i].value;
|
||||||
const diff_index = diff.arg_diff[i]?.diff_index;
|
let diff_index: number | undefined;
|
||||||
|
if (arg.oneofKind !== 'plain_text') {
|
||||||
|
diff_index = diff.arg_diff[arg_diff_idx]?.diff_index;
|
||||||
|
arg_diff_idx++;
|
||||||
|
}
|
||||||
switch (arg.oneofKind) {
|
switch (arg.oneofKind) {
|
||||||
case "plain_text":
|
case "plain_text":
|
||||||
cb({type: 'basic', text: arg.plain_text, diff_index});
|
cb({type: 'basic', text: arg.plain_text, diff_index});
|
||||||
|
|||||||
@@ -38,13 +38,15 @@ async function initIfNeeded() {
|
|||||||
// return exports.run_diff_json(left, right, cfg);
|
// return exports.run_diff_json(left, right, cfg);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
async function run_diff_proto({left, right, config}: {
|
async function run_diff_proto({left, right, diff_config, mapping_config}: {
|
||||||
left: Uint8Array | undefined,
|
left: Uint8Array | undefined,
|
||||||
right: Uint8Array | undefined,
|
right: Uint8Array | undefined,
|
||||||
config?: exports.DiffObjConfig,
|
diff_config?: exports.DiffObjConfig,
|
||||||
|
mapping_config?: exports.MappingConfig,
|
||||||
}): Promise<Uint8Array> {
|
}): Promise<Uint8Array> {
|
||||||
config = config || {};
|
diff_config = diff_config || {};
|
||||||
return exports.run_diff_proto(left, right, config);
|
mapping_config = mapping_config || {};
|
||||||
|
return exports.run_diff_proto(left, right, diff_config, mapping_config);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AnyHandlerData = HandlerData[keyof HandlerData];
|
export type AnyHandlerData = HandlerData[keyof HandlerData];
|
||||||
@@ -73,12 +75,19 @@ self.onmessage = (event: MessageEvent<InMessage>) => {
|
|||||||
const result = await handler(data as never);
|
const result = await handler(data as never);
|
||||||
const end = performance.now();
|
const end = performance.now();
|
||||||
console.debug(`Worker message ${data.messageId} took ${end - start}ms`);
|
console.debug(`Worker message ${data.messageId} took ${end - start}ms`);
|
||||||
|
let transfer: Transferable[] = [];
|
||||||
|
if (result instanceof Uint8Array) {
|
||||||
|
console.log("Transferring!", result.byteLength);
|
||||||
|
transfer = [result.buffer];
|
||||||
|
} else {
|
||||||
|
console.log("Didn't transfer", typeof result);
|
||||||
|
}
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
type: 'result',
|
type: 'result',
|
||||||
result: result,
|
result: result,
|
||||||
error: null,
|
error: null,
|
||||||
messageId,
|
messageId,
|
||||||
} as OutMessage);
|
} as OutMessage, {transfer});
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`No handler for ${data.type}`);
|
throw new Error(`No handler for ${data.type}`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user