Compare commits

..

32 Commits

Author SHA1 Message Date
8acaaf528c Version v2.2.0 2024-09-29 12:26:41 -06:00
6e881a74e1 Remove armv7-unknown-linux-musleabi build 2024-09-29 11:57:13 -06:00
cc1bc44e69 Use mimalloc when targeting musl 2024-09-29 11:52:04 -06:00
c7b85518ab Rework jobs view & error handling improvements
Job status is now shown in the top menu bar,
with a new Jobs window that can be toggled.

Build and diff errors are now handled more
gracefully.

Fixes #40
2024-09-28 12:14:20 -06:00
bb039a1445 Add "Open source file" option
Available when right-clicking an object in
the object list or when viewing an object

Resolves #99
2024-09-28 11:50:56 -06:00
8fc142d316 Debounce loaded object modification check
Before, this was running 2 fs::metadata
calls every frame. We don't need to do it
nearly that often, so now it only checks
once every 500ms.

This required refactoring AppConfig into
a separate AppState that holds transient
runtime state along with the loaded
AppConfig.
2024-09-28 10:55:22 -06:00
b0123b3f83 Improve build log message when command doesn't exist
Before, it didn't include the actual command
that was attempted to run.
2024-09-28 10:55:09 -06:00
2ec17aee9b Improve config read/write performance
We were accidentally using unbuffered readers
and writers before, leading to long pauses on
the main thread on slow filesystems. (e.g.
FUSE or WSL)
2024-09-28 10:54:54 -06:00
ec9731e1e5 Set app_id in eframe NativeOptions
Fixes missing WM_CLASS on Wayland
2024-09-28 10:53:58 -06:00
OndrikB
a06382c27e Disambiguate dummy symbols (#107)
* Disambiguate dummy symbols

* Small formatting improvement

* Put HashMap logic into symbol creation
2024-09-27 00:33:36 -06:00
e013638c5a clippy fixes 2024-09-27 00:30:30 -06:00
70ab82f1f7 gui: Highlight registers in columns separately
This matches the behavior of decomp.me and the
CLI.

Resolves #71
2024-09-27 00:27:36 -06:00
c5896689cf Use ppc750cl Opcode::from 2024-09-27 00:12:21 -06:00
67719dd93e report: Exclude "hidden" functions
Fixes #111
2024-09-27 00:12:21 -06:00
258e141017 Upgrade all dependencies 2024-09-27 00:12:16 -06:00
dbdda55065 Add Report::split
A hack for supporting games that build
all versions at once.
2024-09-26 23:47:03 -06:00
Steven Casper
a43320af1f PPC: Guess reloc data type based on the instruction. (#108)
* Guess reloc data type based on the instruction.

Adds an entry to the reloc tooltip to show the inferred data type
and value.

* Fix clippy warning

* Match on Opcode rather than mnemonic string
2024-09-25 23:45:37 -06:00
Amber Brault
35bbd40f5d Actually update extab stuff (#110)
* Update cwextab

* Update

* Update ppc.rs

* Make fmt shut up
2024-09-24 09:16:14 -06:00
Amber Brault
c1cb4b0b19 Update cwextab (#109) 2024-09-23 21:24:33 -06:00
2379853faa Remove unused imports 2024-09-10 23:29:22 -06:00
5e1aff180f Remove vergen / GIT_COMMIT_SHA handling 2024-09-10 23:22:40 -06:00
3846a7d315 Version v2.0.0 2024-09-09 20:18:56 -06:00
dcf209aac5 Cleanup & move extab code into ppc arch 2024-09-09 19:43:10 -06:00
c7e6394628 Try to resolve deleting autoupdate tmp dir 2024-09-09 19:42:01 -06:00
235dc7f517 Use released ppc750cl & update README.md 2024-09-09 19:41:29 -06:00
Robin Avery
199c07e975 Add cargo install instructions to README (#105) 2024-09-09 19:38:06 -06:00
56a5a61825 Updates to CI workflow & README.md 2024-09-09 19:34:50 -06:00
3d2236de82 Use workspace keys in Cargo.toml 2024-09-09 19:32:22 -06:00
bcc5871cd8 Update all dependencies 2024-09-09 19:26:46 -06:00
Robin Lambertz
7d0d7df54c Add 32-bit windows objdiff-cli build (#102)
* Revert "Add 32-bit windows builds (#101)"

This reverts commit bc687173c0.

* Add 32-bit objdiff-cli build
2024-09-06 19:25:18 -06:00
0221a2d54d clippy fix 2024-09-05 17:52:43 -06:00
Robin Lambertz
bc687173c0 Add 32-bit windows builds (#101) 2024-09-05 17:50:37 -06:00
38 changed files with 2786 additions and 1965 deletions

View File

@@ -11,6 +11,7 @@ on:
env:
BUILD_PROFILE: release-lto
CARGO_TARGET_DIR: target
CARGO_INCREMENTAL: 0
jobs:
check:
@@ -25,37 +26,14 @@ jobs:
sudo apt-get -y install libgtk-3-dev
- name: Checkout
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
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.4
- name: Cargo check
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
run: cargo check
run: cargo check --all-features --all-targets
- name: Cargo clippy
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
run: cargo clippy
run: cargo clippy --all-features --all-targets
fmt:
name: Format
@@ -107,13 +85,8 @@ jobs:
uses: actions/checkout@v4
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.4
- name: Cargo test
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
run: cargo test --release
run: cargo test --release --all-features
build-cli:
name: Build objdiff-cli
@@ -137,10 +110,10 @@ jobs:
name: linux-aarch64
build: zigbuild
features: default
- platform: ubuntu-latest
target: armv7-unknown-linux-musleabi
name: linux-armv7l
build: zigbuild
- platform: windows-latest
target: i686-pc-windows-msvc
name: windows-x86
build: build
features: default
- platform: windows-latest
target: x86_64-pc-windows-msvc
@@ -225,12 +198,7 @@ jobs:
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.4
- name: Cargo build
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
run: >
cargo build --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
--bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }}
@@ -247,10 +215,24 @@ jobs:
name: Release
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
needs: [ check, build-cli, build-gui ]
needs: [ build-cli, build-gui ]
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check git tag against Cargo version
shell: bash
run: |
set -eou pipefail
tag='${{github.ref}}'
tag="${tag#refs/tags/}"
version=$(grep '^version' Cargo.toml | head -1 | awk -F' = ' '{print $2}' | tr -d '"')
version="v$version"
if [ "$tag" != "$version" ]; then
echo "::error::Git tag doesn't match the Cargo version! ($tag != $version)"
exit 1
fi
- name: Download artifacts
uses: actions/download-artifact@v4
with:
@@ -278,6 +260,8 @@ jobs:
done
ls -R ../out
- name: Release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
files: out/*
draft: true
generate_release_notes: true

2305
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,5 +8,14 @@ resolver = "2"
[profile.release-lto]
inherits = "release"
lto = "thin"
lto = "fat"
strip = "debuginfo"
codegen-units = 1
[workspace.package]
version = "2.2.0"
authors = ["Luke Street <luke@street.dev>"]
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff"
rust-version = "1.74"

View File

@@ -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).
Features:
- Compare entire object files: functions and data.
- Built-in symbol demangling for C++. (CodeWarrior, Itanium & MSVC)
- Automatic rebuild on source file changes.
@@ -14,6 +15,7 @@ Features:
- Click to highlight all instances of values and registers.
Supports:
- PowerPC 750CL (GameCube, Wii)
- MIPS (N64, PS1, PS2, PSP)
- x86 (COFF only at the moment)
@@ -21,6 +23,25 @@ Supports:
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
![Symbol Screenshot](assets/screen-symbols.png)
![Diff Screenshot](assets/screen-diff.png)
@@ -141,16 +162,22 @@ Install Rust via [rustup](https://rustup.rs).
$ git clone https://github.com/encounter/objdiff.git
$ cd objdiff
$ cargo run --release
# or, for wgpu backend (recommended on macOS)
$ cargo run --release --features wgpu
```
Or using `cargo install`.
```shell
$ cargo install --locked --git https://github.com/encounter/objdiff.git objdiff-gui objdiff-cli
```
The binaries will be installed to `~/.cargo/bin` as `objdiff` and `objdiff-cli`.
## License
Licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
at your option.

169
deny.toml
View File

@@ -9,6 +9,11 @@
# The values provided in this template are the default values that will be used
# when any section or field is not specified in your own configuration
# Root options
# The graph table configures how the dependency graph is constructed and thus
# which crates the checks are performed against
[graph]
# If 1 or more target triples (and optionally, target_features) are specified,
# only the specified targets will be checked when running `cargo deny check`.
# This means, if a particular package is only ever used as a target specific
@@ -20,51 +25,67 @@
targets = [
# The triple can be any string, but only the target triples built in to
# rustc (as of 1.40) can be checked against actual config expressions
#{ triple = "x86_64-unknown-linux-musl" },
#"x86_64-unknown-linux-musl",
# You can also specify which target_features you promise are enabled for a
# particular target. target_features are currently not validated against
# the actual valid features supported by the target architecture.
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
]
# When creating the dependency graph used as the source of truth when checks are
# executed, this field can be used to prune crates from the graph, removing them
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
# is pruned from the graph, all of its dependencies will also be pruned unless
# they are connected to another crate in the graph that hasn't been pruned,
# so it should be used with care. The identifiers are [Package ID Specifications]
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
#exclude = []
# If true, metadata will be collected with `--all-features`. Note that this can't
# be toggled off if true, if you want to conditionally enable `--all-features` it
# is recommended to pass `--all-features` on the cmd line instead
all-features = false
# If true, metadata will be collected with `--no-default-features`. The same
# caveat with `all-features` applies
no-default-features = false
# If set, these feature will be enabled when collecting metadata. If `--features`
# is specified on the cmd line they will take precedence over this option.
#features = []
# The output table provides options for how/if diagnostics are outputted
[output]
# When outputting inclusion graphs in diagnostics that include features, this
# option can be used to specify the depth at which feature edges will be added.
# This option is included since the graphs can be quite large and the addition
# of features from the crate(s) to all of the graph roots can be far too verbose.
# This option can be overridden via `--feature-depth` on the cmd line
feature-depth = 1
# This section is considered when running `cargo deny check advisories`
# More documentation for the advisories section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
[advisories]
# The path where the advisory database is cloned/fetched into
db-path = "~/.cargo/advisory-db"
# The path where the advisory databases are cloned/fetched into
#db-path = "$CARGO_HOME/advisory-dbs"
# The url(s) of the advisory databases to use
db-urls = ["https://github.com/rustsec/advisory-db"]
# The lint level for security vulnerabilities
vulnerability = "deny"
# The lint level for unmaintained crates
unmaintained = "warn"
# The lint level for crates that have been yanked from their source registry
yanked = "warn"
# The lint level for crates with security notices. Note that as of
# 2019-12-17 there are no security notice advisories in
# https://github.com/rustsec/advisory-db
notice = "warn"
#db-urls = ["https://github.com/rustsec/advisory-db"]
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
ignore = []
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
# lower than the range specified will be ignored. Note that ignored advisories
# will still output a note when they are encountered.
# * None - CVSS Score 0.0
# * Low - CVSS Score 0.1 - 3.9
# * Medium - CVSS Score 4.0 - 6.9
# * High - CVSS Score 7.0 - 8.9
# * Critical - CVSS Score 9.0 - 10.0
#severity-threshold =
ignore = [
"RUSTSEC-2024-0370",
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
]
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
# See Git Authentication for more information about setting up git authentication.
#git-fetch-with-cli = true
# This section is considered when running `cargo deny check licenses`
# More documentation for the licenses section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
[licenses]
# The lint level for crates which do not have a detectable license
unlicensed = "deny"
# List of explictly allowed licenses
# List of explicitly allowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
allow = [
@@ -83,28 +104,7 @@ allow = [
"OFL-1.1",
"LicenseRef-UFL-1.0",
"OpenSSL",
"GPL-3.0",
]
# List of explictly disallowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
deny = [
#"Nokia",
]
# Lint level for licenses considered copyleft
copyleft = "warn"
# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
# * both - The license will be approved if it is both OSI-approved *AND* FSF
# * either - The license will be approved if it is either OSI-approved *OR* FSF
# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
# * neither - This predicate is ignored and the default lint level is used
allow-osi-fsf-free = "neither"
# Lint level used when no other predicates are matched
# 1. License isn't in the allow or deny lists
# 2. License isn't copyleft
# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
default = "deny"
# The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the
# canonical license text of a valid SPDX license file.
@@ -115,17 +115,15 @@ confidence-threshold = 0.8
exceptions = [
# Each entry is the crate and version constraint, and its specific allow
# list
#{ allow = ["Zlib"], name = "adler32", version = "*" },
#{ allow = ["Zlib"], crate = "adler32" },
]
# Some crates don't have (easily) machine readable licensing information,
# adding a clarification entry for it allows you to manually specify the
# licensing information
[[licenses.clarify]]
# The name of the crate the clarification applies to
name = "ring"
# The optional version constraint for the crate
version = "*"
# The package spec the clarification applies to
crate = "ring"
# The SPDX expression for the license requirements of the crate
expression = "MIT AND ISC AND OpenSSL"
# One or more files in the crate's source used as the "source of truth" for
@@ -140,7 +138,9 @@ license-files = [
[licenses.private]
# If true, ignores workspace crates that aren't published, or are only
# published to private registries
# published to private registries.
# To see how to mark a crate as unpublished (to the official registry),
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
ignore = false
# One or more private registries that you might publish crates to, if a crate
# is only published to private registries, and ignore is true, the crate will
@@ -163,30 +163,63 @@ wildcards = "allow"
# * simplest-path - The path to the version with the fewest edges is highlighted
# * all - Both lowest-version and simplest-path are used
highlight = "all"
# The default lint level for `default` features for crates that are members of
# the workspace that is being checked. This can be overridden by allowing/denying
# `default` on a crate-by-crate basis if desired.
workspace-default-features = "allow"
# The default lint level for `default` features for external crates that are not
# members of the workspace. This can be overridden by allowing/denying `default`
# on a crate-by-crate basis if desired.
external-default-features = "allow"
# List of crates that are allowed. Use with care!
allow = [
#{ name = "ansi_term", version = "=0.11.0" },
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
]
# List of crates to deny
deny = [
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#{ name = "ansi_term", version = "=0.11.0" },
#
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
# Wrapper crates can optionally be specified to allow the crate when it
# is a direct dependency of the otherwise banned crate
#{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
]
# List of features to allow/deny
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#[[bans.features]]
#crate = "reqwest"
# Features to not allow
#deny = ["json"]
# Features to allow
#allow = [
# "rustls",
# "__rustls",
# "__tls",
# "hyper-rustls",
# "rustls",
# "rustls-pemfile",
# "rustls-tls-webpki-roots",
# "tokio-rustls",
# "webpki-roots",
#]
# If true, the allowed features must exactly match the enabled feature set. If
# this is set there is no point setting `deny`
#exact = true
# Certain crates/versions that will be skipped when doing duplicate detection.
skip = [
#{ name = "ansi_term", version = "=0.11.0" },
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
]
# Similarly to `skip` allows you to skip certain crates during duplicate
# detection. Unlike skip, it also includes the entire tree of transitive
# dependencies starting at the specified crate, up to a certain depth, which is
# by default infinite
# by default infinite.
skip-tree = [
#{ name = "ansi_term", version = "=0.11.0", depth = 20 },
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
#{ crate = "ansi_term@0.11.0", depth = 20 },
]
# This section is considered when running `cargo deny check sources`.
@@ -206,9 +239,9 @@ allow-registry = ["https://github.com/rust-lang/crates.io-index"]
allow-git = []
[sources.allow-org]
# 1 or more github.com organizations to allow git sources for
# github.com organizations to allow git sources for
github = ["encounter"]
# 1 or more gitlab.com organizations to allow git sources for
#gitlab = [""]
# 1 or more bitbucket.org organizations to allow git sources for
#bitbucket = [""]
# gitlab.com organizations to allow git sources for
gitlab = []
# bitbucket.org organizations to allow git sources for
bitbucket = []

View File

@@ -1,31 +1,33 @@
[package]
name = "objdiff-cli"
version = "2.0.0-beta.6"
edition = "2021"
rust-version = "1.70"
authors = ["Luke Street <luke@street.dev>"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
readme = "../README.md"
description = """
A local diffing tool for decompilation projects.
"""
publish = false
build = "build.rs"
[dependencies]
anyhow = "1.0.82"
argp = "0.3.0"
crossterm = "0.27.0"
enable-ansi-support = "0.2.1"
memmap2 = "0.9.4"
anyhow = "1.0"
argp = "0.3"
crossterm = "0.28"
enable-ansi-support = "0.2"
memmap2 = "0.9"
objdiff-core = { path = "../objdiff-core", features = ["all"] }
prost = "0.13.1"
ratatui = "0.26.2"
rayon = "1.10.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1.0.116"
supports-color = "3.0.0"
time = { version = "0.3.36", features = ["formatting", "local-offset"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
prost = "0.13"
ratatui = "0.28"
rayon = "1.10"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
supports-color = "3.0"
time = { version = "0.3", features = ["formatting", "local-offset"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[target.'cfg(target_env = "musl")'.dependencies]
mimalloc = "0.1"

View File

@@ -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");
}

View File

@@ -31,10 +31,9 @@ where T: FromArgs
Ok(v) => {
if v.version {
println!(
"{} {} {}",
"{} {}",
command_name.first().unwrap_or(&""),
env!("CARGO_PKG_VERSION"),
env!("GIT_COMMIT_SHA"),
);
std::process::exit(0);
} else {

View File

@@ -345,7 +345,7 @@ enum EventControlFlow {
impl FunctionDiffUi {
fn draw(&mut self, f: &mut Frame, result: &mut EventResult) {
let chunks = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).split(f.size());
let chunks = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).split(f.area());
let header_chunks = Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(3),
@@ -415,7 +415,7 @@ impl FunctionDiffUi {
get_symbol_diff(self.diff_result.left.as_ref(), self.left_sym),
) {
let mut text = Text::default();
let rect = content_chunks[0].inner(&Margin::new(0, 1));
let rect = content_chunks[0].inner(Margin::new(0, 1));
left_highlight = self.print_sym(
&mut text,
symbol,
@@ -437,7 +437,7 @@ impl FunctionDiffUi {
get_symbol_diff(self.diff_result.right.as_ref(), self.right_sym),
) {
let mut text = Text::default();
let rect = content_chunks[2].inner(&Margin::new(0, 1));
let rect = content_chunks[2].inner(Margin::new(0, 1));
right_highlight = self.print_sym(
&mut text,
symbol,
@@ -452,7 +452,7 @@ impl FunctionDiffUi {
// Render margin
let mut text = Text::default();
let rect = content_chunks[1].inner(&Margin::new(1, 1));
let rect = content_chunks[1].inner(Margin::new(1, 1));
self.print_margin(&mut text, symbol_diff, rect);
margin_text = Some(text);
}
@@ -465,7 +465,7 @@ impl FunctionDiffUi {
get_symbol_diff(self.diff_result.prev.as_ref(), self.prev_sym),
) {
let mut text = Text::default();
let rect = content_chunks[4].inner(&Margin::new(0, 1));
let rect = content_chunks[4].inner(Margin::new(0, 1));
self.print_sym(
&mut text,
symbol,
@@ -480,7 +480,7 @@ impl FunctionDiffUi {
// Render margin
let mut text = Text::default();
let rect = content_chunks[3].inner(&Margin::new(1, 1));
let rect = content_chunks[3].inner(Margin::new(1, 1));
self.print_margin(&mut text, symbol_diff, rect);
prev_margin_text = Some(text);
}
@@ -498,18 +498,30 @@ impl FunctionDiffUi {
// Render left column
f.render_widget(
Paragraph::new(text)
.block(Block::new().borders(Borders::TOP).gray().title("TARGET".bold()))
.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)));
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).gray().title("CURRENT".bold()))
.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],
);
@@ -517,9 +529,13 @@ impl FunctionDiffUi {
if self.three_way {
if let Some(text) = prev_margin_text {
f.render_widget(text, content_chunks[3].inner(&Margin::new(1, 1)));
f.render_widget(text, content_chunks[3].inner(Margin::new(1, 1)));
}
let block = Block::new().borders(Borders::TOP).gray().title("SAVED".bold());
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)),
@@ -533,7 +549,7 @@ impl FunctionDiffUi {
// Render scrollbars
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight).begin_symbol(None).end_symbol(None),
chunks[1].inner(&Margin::new(0, 1)),
chunks[1].inner(Margin::new(0, 1)),
&mut self.scroll_state_y,
);
f.render_stateful_widget(
@@ -589,7 +605,7 @@ impl FunctionDiffUi {
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(f.size())[1];
.split(f.area())[1];
let popup_rect = Layout::horizontal([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),

View File

@@ -237,7 +237,7 @@ fn report_object(
}
for (symbol, symbol_diff) in section.symbols.iter().zip(&section_diff.symbols) {
if symbol.size == 0 {
if symbol.size == 0 || symbol.flags.0.contains(ObjSymbolFlags::Hidden) {
continue;
}
if let Some(existing_functions) = &mut existing_functions {

View File

@@ -2,6 +2,12 @@ mod argp_version;
mod cmd;
mod util;
// musl's allocator is very slow, so use mimalloc when targeting musl.
// Otherwise, use the system allocator to avoid extra code size.
#[cfg(target_env = "musl")]
#[global_allocator]
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
use std::{env, ffi::OsStr, fmt::Display, path::PathBuf, str::FromStr};
use anyhow::{Error, Result};

View File

@@ -1,15 +1,16 @@
[package]
name = "objdiff-core"
version = "2.0.0-beta.6"
edition = "2021"
rust-version = "1.70"
authors = ["Luke Street <luke@street.dev>"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff"
readme = "../README.md"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
readme = "README.md"
description = """
A local diffing tool for decompilation projects.
"""
documentation = "https://docs.rs/objdiff-core"
[lib]
crate-type = ["cdylib", "rlib"]
@@ -26,51 +27,54 @@ arm = ["any-arch", "cpp_demangle", "unarm", "arm-attr"]
bindings = ["serde_json", "prost", "pbjson"]
wasm = ["bindings", "console_error_panic_hook", "console_log"]
[package.metadata.docs.rs]
features = ["all"]
[dependencies]
anyhow = "1.0.82"
byteorder = "1.5.0"
filetime = "0.2.23"
flagset = "0.4.5"
log = "0.4.21"
memmap2 = "0.9.4"
num-traits = "0.2.18"
object = { version = "0.36.0", features = ["read_core", "std", "elf", "pe"], default-features = false }
pbjson = { version = "0.7.0", optional = true }
prost = { version = "0.13.1", optional = true }
serde = { version = "1", features = ["derive"] }
similar = { version = "2.5.0", default-features = false }
strum = { version = "0.26.2", features = ["derive"] }
wasm-bindgen = "0.2.93"
tsify-next = { version = "0.5.4", default-features = false, features = ["js"] }
console_log = { version = "1.0.0", optional = true }
console_error_panic_hook = { version = "0.1.7", optional = true }
anyhow = "1.0"
byteorder = "1.5"
filetime = "0.2"
flagset = "0.4"
log = "0.4"
memmap2 = "0.9"
num-traits = "0.2"
object = { version = "0.36", features = ["read_core", "std", "elf", "pe"], default-features = false }
pbjson = { version = "0.7", optional = true }
prost = { version = "0.13", optional = true }
serde = { version = "1.0", features = ["derive"] }
similar = { version = "2.6", default-features = false }
strum = { version = "0.26", features = ["derive"] }
wasm-bindgen = "0.2"
tsify-next = { version = "0.5", default-features = false, features = ["js"] }
console_log = { version = "1.0", optional = true }
console_error_panic_hook = { version = "0.1", optional = true }
# config
globset = { version = "0.4.14", features = ["serde1"], optional = true }
semver = { version = "1.0.22", optional = true }
serde_json = { version = "1.0.116", optional = true }
serde_yaml = { version = "0.9.34", optional = true }
globset = { version = "0.4", features = ["serde1"], optional = true }
semver = { version = "1.0", optional = true }
serde_json = { version = "1.0", optional = true }
serde_yaml = { version = "0.9", optional = true }
# dwarf
gimli = { version = "0.29.0", default-features = false, features = ["read-all"], optional = true }
gimli = { version = "0.31", default-features = false, features = ["read-all"], optional = true }
# ppc
cwdemangle = { version = "1.0.0", optional = true }
cwextab = { version = "0.2.3", optional = true }
ppc750cl = { git = "https://github.com/encounter/ppc750cl", rev = "6cbd7d888c7082c2c860f66cbb9848d633f753ed", optional = true }
cwdemangle = { version = "1.0", optional = true }
cwextab = { version = "0.3", optional = true }
ppc750cl = { version = "0.3", optional = true }
# mips
rabbitizer = { version = "1.11.0", optional = true }
rabbitizer = { version = "1.12", optional = true }
# x86
cpp_demangle = { version = "0.4.3", optional = true }
iced-x86 = { version = "1.21.0", default-features = false, features = ["std", "decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums"], optional = true }
msvc-demangler = { version = "0.10.0", optional = true }
cpp_demangle = { version = "0.4", optional = true }
iced-x86 = { version = "1.21", default-features = false, features = ["std", "decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums"], optional = true }
msvc-demangler = { version = "0.10", optional = true }
# arm
unarm = { version = "1.5.0", optional = true }
arm-attr = { version = "0.1.1", optional = true }
unarm = { version = "1.6", optional = true }
arm-attr = { version = "0.1", optional = true }
[build-dependencies]
prost-build = "0.13.1"
pbjson-build = "0.7.0"
prost-build = "0.13"
pbjson-build = "0.7"

14
objdiff-core/README.md Normal file
View File

@@ -0,0 +1,14 @@
# 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).
- **`bindings`**: Enables serialization and deserialization of objdiff data structures.

View File

@@ -1,11 +1,13 @@
use std::{borrow::Cow, collections::BTreeMap};
use std::{borrow::Cow, collections::BTreeMap, ffi::CStr};
use anyhow::{bail, Result};
use byteorder::ByteOrder;
use object::{Architecture, File, Object, ObjectSymbol, Relocation, RelocationFlags, Symbol};
use crate::{
diff::DiffObjConfig,
obj::{ObjIns, ObjReloc, ObjSection},
util::ReallySigned,
};
#[cfg(feature = "arm")]
@@ -17,6 +19,97 @@ pub mod ppc;
#[cfg(feature = "x86")]
pub mod x86;
/// Represents the type of data associated with an instruction
pub enum DataType {
Int8,
Int16,
Int32,
Int64,
Int128,
Float,
Double,
Bytes,
String,
}
impl DataType {
pub fn display_bytes<Endian: ByteOrder>(&self, bytes: &[u8]) -> Option<String> {
if self.required_len().is_some_and(|l| bytes.len() < l) {
return None;
}
match self {
DataType::Int8 => {
let i = i8::from_ne_bytes(bytes.try_into().unwrap());
if i < 0 {
format!("Int8: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int8: {:#x}", i)
}
}
DataType::Int16 => {
let i = Endian::read_i16(bytes);
if i < 0 {
format!("Int16: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int16: {:#x}", i)
}
}
DataType::Int32 => {
let i = Endian::read_i32(bytes);
if i < 0 {
format!("Int32: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int32: {:#x}", i)
}
}
DataType::Int64 => {
let i = Endian::read_i64(bytes);
if i < 0 {
format!("Int64: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int64: {:#x}", i)
}
}
DataType::Int128 => {
let i = Endian::read_i128(bytes);
if i < 0 {
format!("Int128: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int128: {:#x}", i)
}
}
DataType::Float => {
format!("Float: {}", Endian::read_f32(bytes))
}
DataType::Double => {
format!("Double: {}", Endian::read_f64(bytes))
}
DataType::Bytes => {
format!("Bytes: {:#?}", bytes)
}
DataType::String => {
format!("String: {:?}", CStr::from_bytes_until_nul(bytes).ok()?)
}
}
.into()
}
fn required_len(&self) -> Option<usize> {
match self {
DataType::Int8 => Some(1),
DataType::Int16 => Some(2),
DataType::Int32 => Some(4),
DataType::Int64 => Some(8),
DataType::Int128 => Some(16),
DataType::Float => Some(4),
DataType::Double => Some(8),
DataType::Bytes => None,
DataType::String => None,
}
}
}
pub trait ObjArch: Send + Sync {
fn process_code(
&self,
@@ -41,6 +134,16 @@ pub trait ObjArch: Send + Sync {
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str>;
fn symbol_address(&self, symbol: &Symbol) -> u64 { symbol.address() }
fn guess_data_type(&self, _instruction: &ObjIns) -> Option<DataType> { None }
fn display_data_type(&self, _ty: DataType, bytes: &[u8]) -> Option<String> {
Some(format!("Bytes: {:#x?}", bytes))
}
// Downcast methods
#[cfg(feature = "ppc")]
fn ppc(&self) -> Option<&ppc::ObjArchPpc> { None }
}
pub struct ProcessCodeResult {
@@ -48,7 +151,7 @@ pub struct ProcessCodeResult {
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() {
#[cfg(feature = "ppc")]
Architecture::PowerPc => Box::new(ppc::ObjArchPpc::new(object)?),

View File

@@ -1,13 +1,18 @@
use std::{borrow::Cow, collections::BTreeMap};
use anyhow::{bail, Result};
use object::{elf, File, Relocation, RelocationFlags};
use ppc750cl::{Argument, InsIter, GPR};
use anyhow::{bail, ensure, Result};
use byteorder::BigEndian;
use cwextab::{decode_extab, ExceptionTableData};
use object::{
elf, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, RelocationTarget,
Symbol, SymbolKind,
};
use ppc750cl::{Argument, InsIter, Opcode, GPR};
use crate::{
arch::{ObjArch, ProcessCodeResult},
arch::{DataType, ObjArch, ProcessCodeResult},
diff::DiffObjConfig,
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection},
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection, ObjSymbol},
};
// Relative relocation, can be Simm, Offset or BranchDest
@@ -22,10 +27,13 @@ fn is_rel_abs_arg(arg: &Argument) -> bool {
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 {
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 {
@@ -178,6 +186,42 @@ impl ObjArch for ObjArchPpc {
_ => Cow::Owned(format!("<{flags:?}>")),
}
}
fn guess_data_type(&self, instruction: &ObjIns) -> Option<super::DataType> {
// Always shows the first string of the table. Not ideal, but it's really hard to find
// the actual string being referenced.
if instruction.reloc.as_ref().is_some_and(|r| r.target.name.starts_with("@stringBase")) {
return Some(DataType::String);
}
match Opcode::from(instruction.op as u8) {
Opcode::Lbz | Opcode::Lbzu | Opcode::Lbzux | Opcode::Lbzx => Some(DataType::Int8),
Opcode::Lhz | Opcode::Lhzu | Opcode::Lhzux | Opcode::Lhzx => Some(DataType::Int16),
Opcode::Lha | Opcode::Lhau | Opcode::Lhaux | Opcode::Lhax => Some(DataType::Int16),
Opcode::Lwz | Opcode::Lwzu | Opcode::Lwzux | Opcode::Lwzx => Some(DataType::Int32),
Opcode::Lfs | Opcode::Lfsu | Opcode::Lfsux | Opcode::Lfsx => Some(DataType::Float),
Opcode::Lfd | Opcode::Lfdu | Opcode::Lfdux | Opcode::Lfdx => Some(DataType::Double),
Opcode::Stb | Opcode::Stbu | Opcode::Stbux | Opcode::Stbx => Some(DataType::Int8),
Opcode::Sth | Opcode::Sthu | Opcode::Sthux | Opcode::Sthx => Some(DataType::Int16),
Opcode::Stw | Opcode::Stwu | Opcode::Stwux | Opcode::Stwx => Some(DataType::Int32),
Opcode::Stfs | Opcode::Stfsu | Opcode::Stfsux | Opcode::Stfsx => Some(DataType::Float),
Opcode::Stfd | Opcode::Stfdu | Opcode::Stfdux | Opcode::Stfdx => Some(DataType::Double),
_ => None,
}
}
fn display_data_type(&self, ty: DataType, bytes: &[u8]) -> Option<String> {
ty.display_bytes::<BigEndian>(bytes)
}
fn ppc(&self) -> Option<&ObjArchPpc> { Some(self) }
}
impl ObjArchPpc {
pub fn extab_for_symbol(&self, symbol: &ObjSymbol) -> Option<&ExceptionInfo> {
symbol.original_index.and_then(|i| self.extab.as_ref()?.get(&i))
}
}
fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {
@@ -208,3 +252,132 @@ fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {
};
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 })
}

View File

@@ -117,6 +117,72 @@ impl Report {
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()..).map_or(false, |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 {

View File

@@ -1,6 +1,6 @@
use std::{
fs::File,
io::Read,
io::{BufReader, Read},
path::{Path, PathBuf},
};
@@ -124,6 +124,10 @@ impl ProjectObject {
pub fn hidden(&self) -> bool {
self.metadata.as_ref().and_then(|m| m.auto_generated).unwrap_or(false)
}
pub fn source_path(&self) -> Option<&String> {
self.metadata.as_ref().and_then(|m| m.source_path.as_ref())
}
}
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
@@ -156,7 +160,7 @@ pub struct ProjectConfigInfo {
pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
for filename in CONFIG_FILENAMES.iter() {
let config_path = dir.join(filename);
let Ok(mut file) = File::open(&config_path) else {
let Ok(file) = File::open(&config_path) else {
continue;
};
let metadata = file.metadata();
@@ -165,9 +169,10 @@ pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectC
continue;
}
let ts = FileTime::from_last_modification_time(&metadata);
let mut reader = BufReader::new(file);
let mut result = match filename.contains("json") {
true => read_json_config(&mut file),
false => read_yml_config(&mut file),
true => read_json_config(&mut reader),
false => read_yml_config(&mut reader),
};
if let Ok(config) = &result {
// Validate min_version if present

View File

@@ -3,7 +3,6 @@ pub mod split_meta;
use std::{borrow::Cow, collections::BTreeMap, fmt, path::PathBuf};
use cwextab::*;
use filetime::FileTime;
use flagset::{flags, FlagSet};
use object::RelocationFlags;
@@ -24,6 +23,9 @@ flags! {
Weak,
Common,
Hidden,
/// Has extra data associated with the symbol
/// (e.g. exception table entry)
HasExtra,
}
}
#[derive(Debug, Copy, Clone, Default)]
@@ -114,9 +116,6 @@ pub struct ObjIns {
pub struct ObjSymbol {
pub name: String,
pub demangled_name: Option<String>,
pub has_extab: bool,
pub extab_name: Option<String>,
pub extabindex_name: Option<String>,
pub address: u64,
pub section_address: u64,
pub size: u64,
@@ -125,13 +124,9 @@ pub struct ObjSymbol {
pub addend: i64,
/// Original virtual address (from .note.split section)
pub virtual_address: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct ObjExtab {
pub func: ObjSymbol,
pub data: ExceptionTableData,
pub dtors: Vec<ObjSymbol>,
/// Original index in object symbol table
pub original_index: Option<usize>,
pub bytes: Vec<u8>,
}
pub struct ObjInfo {
@@ -141,8 +136,6 @@ pub struct ObjInfo {
pub sections: Vec<ObjSection>,
/// Common BSS symbols
pub common: Vec<ObjSymbol>,
/// Exception tables
pub extab: Option<Vec<ObjExtab>>,
/// Split object metadata (.note.split section)
pub split_meta: Option<SplitMeta>,
}

View File

@@ -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 cwextab::decode_extab;
use filetime::FileTime;
use flagset::Flags;
use object::{
endian::LittleEndian as LE,
pe::{ImageAuxSymbolFunctionBeginEnd, ImageLinenumber},
read::coff::{CoffFile, CoffHeader, ImageSymbol},
Architecture, BinaryFormat, File, Object, ObjectSection, ObjectSymbol, RelocationTarget,
SectionIndex, SectionKind, Symbol, SymbolIndex, SymbolKind, SymbolScope, SymbolSection,
BinaryFormat, File, Object, ObjectSection, ObjectSymbol, RelocationTarget, SectionIndex,
SectionKind, Symbol, SymbolIndex, SymbolKind, SymbolScope, SymbolSection,
};
use crate::{
@@ -17,8 +22,7 @@ use crate::{
diff::DiffObjConfig,
obj::{
split_meta::{SplitMeta, SPLITMETA_SECTION},
ObjExtab, ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet,
ObjSymbolFlags,
ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags,
},
util::{read_u16, read_u32},
};
@@ -60,6 +64,13 @@ fn to_obj_symbol(
if obj_file.format() == BinaryFormat::Elf && symbol.scope() == SymbolScope::Linkage {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Hidden);
}
if arch
.ppc()
.and_then(|a| a.extab.as_ref())
.map_or(false, |e| e.contains_key(&symbol.index().0))
{
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::HasExtra);
}
let address = arch.symbol_address(symbol);
let section_address = if let Some(section) =
symbol.section_index().and_then(|idx| obj_file.section_by_index(idx).ok())
@@ -73,12 +84,19 @@ fn to_obj_symbol(
let virtual_address = split_meta
.and_then(|m| m.virtual_addresses.as_ref())
.and_then(|v| v.get(symbol.index().0).cloned());
let bytes = symbol
.section_index()
.and_then(|idx| obj_file.section_by_index(idx).ok())
.and_then(|section| section.data().ok())
.and_then(|data| {
data.get(section_address as usize..(section_address + symbol.size()) as usize)
})
.unwrap_or(&[]);
Ok(ObjSymbol {
name: name.to_string(),
demangled_name,
has_extab: false,
extab_name: None,
extabindex_name: None,
address,
section_address,
size: symbol.size(),
@@ -86,6 +104,8 @@ fn to_obj_symbol(
flags,
addend,
virtual_address,
original_index: Some(symbol.index().0),
bytes: bytes.to_vec(),
})
}
@@ -133,6 +153,7 @@ fn symbols_by_section(
obj_file: &File<'_>,
section: &ObjSection,
split_meta: Option<&SplitMeta>,
name_counts: &mut HashMap<String, u32>,
) -> Result<Vec<ObjSymbol>> {
let mut result = Vec::<ObjSymbol>::new();
for symbol in obj_file.symbols() {
@@ -165,12 +186,15 @@ fn symbols_by_section(
}
if result.is_empty() {
// Dummy symbol for empty sections
*name_counts.entry(section.name.clone()).or_insert(0) += 1;
let current_count: u32 = *name_counts.get(&section.name).unwrap();
result.push(ObjSymbol {
name: format!("[{}]", section.name),
name: if current_count > 1 {
format!("[{} ({})]", section.name, current_count)
} else {
format!("[{}]", section.name)
},
demangled_name: None,
has_extab: false,
extab_name: None,
extabindex_name: None,
address: 0,
section_address: 0,
size: section.size,
@@ -178,6 +202,8 @@ fn symbols_by_section(
flags: Default::default(),
addend: 0,
virtual_address: None,
original_index: None,
bytes: Vec::new(),
});
}
Ok(result)
@@ -195,111 +221,6 @@ fn common_symbols(
.collect::<Result<Vec<ObjSymbol>>>()
}
fn section_by_name<'a>(sections: &'a mut [ObjSection], name: &str) -> Option<&'a mut ObjSection> {
sections.iter_mut().find(|section| section.name == name)
}
fn exception_tables(
sections: &mut [ObjSection],
obj_file: &File<'_>,
) -> Result<Option<Vec<ObjExtab>>> {
//PowerPC only
if obj_file.architecture() != Architecture::PowerPc {
return Ok(None);
}
//Find the extab/extabindex sections
let extab_section = match section_by_name(sections, "extab") {
Some(section) => section.clone(),
None => {
return Ok(None);
}
};
let extabindex_section = match section_by_name(sections, "extabindex") {
Some(section) => section.clone(),
None => {
return Ok(None);
}
};
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");
}
//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(
arch: &dyn ObjArch,
obj_file: &File<'_>,
@@ -335,9 +256,6 @@ fn find_section_symbol(
Ok(ObjSymbol {
name: name.to_string(),
demangled_name: None,
has_extab: false,
extab_name: None,
extabindex_name: None,
address: offset,
section_address: address - section.address(),
size: 0,
@@ -345,6 +263,8 @@ fn find_section_symbol(
flags: Default::default(),
addend: offset_addr as i64,
virtual_address: None,
original_index: None,
bytes: Vec::new(),
})
}
@@ -615,9 +535,6 @@ fn update_combined_symbol(symbol: ObjSymbol, address_change: i64) -> Result<ObjS
Ok(ObjSymbol {
name: symbol.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()?,
section_address: (symbol.section_address as i64 + address_change).try_into()?,
size: symbol.size,
@@ -629,6 +546,8 @@ fn update_combined_symbol(symbol: ObjSymbol, address_change: i64) -> Result<ObjS
} else {
None
},
original_index: symbol.original_index,
bytes: symbol.bytes,
})
}
@@ -729,9 +648,15 @@ pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result<ObjInfo> {
let arch = new_arch(&obj_file)?;
let split_meta = split_meta(&obj_file)?;
let mut sections = filter_sections(&obj_file, split_meta.as_ref())?;
let mut name_counts: HashMap<String, u32> = HashMap::new();
for section in &mut sections {
section.symbols =
symbols_by_section(arch.as_ref(), &obj_file, section, split_meta.as_ref())?;
section.symbols = symbols_by_section(
arch.as_ref(),
&obj_file,
section,
split_meta.as_ref(),
&mut name_counts,
)?;
section.relocations =
relocations_by_section(arch.as_ref(), &obj_file, section, split_meta.as_ref())?;
}
@@ -740,8 +665,7 @@ pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result<ObjInfo> {
}
line_info(&obj_file, &mut sections, data)?;
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, extab, split_meta })
Ok(ObjInfo { arch, path: None, timestamp: None, sections, common, split_meta })
}
pub fn has_function(obj_path: &Path, symbol_name: &str) -> Result<bool> {

View File

@@ -1,11 +1,11 @@
[package]
name = "objdiff-gui"
version = "2.0.0-beta.6"
edition = "2021"
rust-version = "1.70"
authors = ["Luke Street <luke@street.dev>"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
readme = "../README.md"
description = """
A local diffing tool for decompilation projects.
@@ -24,38 +24,39 @@ wgpu = ["eframe/wgpu", "dep:wgpu"]
wsl = []
[dependencies]
anyhow = "1.0.82"
bytes = "1.6.0"
cfg-if = "1.0.0"
const_format = "0.2.32"
cwdemangle = "1.0.0"
cwextab = "0.2.3"
dirs = "5.0.1"
egui = "0.27.2"
egui_extras = "0.27.2"
filetime = "0.2.23"
float-ord = "0.3.2"
font-kit = "0.13.0"
globset = { version = "0.4.14", features = ["serde1"] }
log = "0.4.21"
notify = { git = "https://github.com/encounter/notify", rev = "4c1783e8e041b5f69d4cf1750b9f07e335a0771e" }
anyhow = "1.0"
bytes = "1.7"
cfg-if = "1.0"
const_format = "0.2"
cwdemangle = "1.0"
cwextab = "0.3.1"
dirs = "5.0"
egui = "0.29"
egui_extras = "0.29"
filetime = "0.2"
float-ord = "0.3"
font-kit = "0.14"
globset = { version = "0.4", features = ["serde1"] }
log = "0.4"
notify = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2" }
objdiff-core = { path = "../objdiff-core", features = ["all"] }
png = "0.17.13"
pollster = "0.3.0"
regex = "1.10.5"
rfd = { version = "0.14.1" } #, default-features = false, features = ['xdg-portal']
rlwinmdec = "1.0.1"
ron = "0.8.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1.0.116"
shell-escape = "0.1.5"
strum = { version = "0.26.2", features = ["derive"] }
tempfile = "3.10.1"
time = { version = "0.3.36", features = ["formatting", "local-offset"] }
open = "5.3"
png = "0.17"
pollster = "0.3"
regex = "1.10"
rfd = { version = "0.15" } #, default-features = false, features = ['xdg-portal']
rlwinmdec = "1.0"
ron = "0.8"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
shell-escape = "0.1"
strum = { version = "0.26", features = ["derive"] }
tempfile = "3.12"
time = { version = "0.3", features = ["formatting", "local-offset"] }
# Keep version in sync with egui
[dependencies.eframe]
version = "0.27.2"
version = "0.29"
features = [
"default_fonts",
"persistence",
@@ -66,7 +67,7 @@ default-features = false
# Keep version in sync with eframe
[dependencies.wgpu]
version = "0.19.1"
version = "22.1"
features = [
"dx12",
"metal",
@@ -77,23 +78,20 @@ 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"] }
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"] }
self_update = { version = "0.41", default-features = false, features = ["rustls"] }
# For all other platforms, use native TLS
[target.'cfg(not(target_os = "linux"))'.dependencies]
reqwest = { version = "0.12.4", default-features = false, features = ["blocking", "json", "multipart", "default-tls"] }
self_update = "0.40.0"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "default-tls"] }
self_update = "0.41"
[target.'cfg(windows)'.dependencies]
path-slash = "0.2.1"
winapi = "0.3.9"
[target.'cfg(windows)'.build-dependencies]
winres = "0.1.12"
path-slash = "0.2"
winapi = "0.3"
[target.'cfg(unix)'.dependencies]
exec = "0.3.1"
exec = "0.3"
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
@@ -101,9 +99,11 @@ tracing-subscriber = "0.3"
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.7"
console_error_panic_hook = "0.1"
tracing-wasm = "0.2"
[build-dependencies]
anyhow = "1.0.82"
vergen = { version = "8.3.1", features = ["build", "cargo", "git", "gitcl"] }
anyhow = "1.0"
[target.'cfg(windows)'.build-dependencies]
tauri-winres = "0.1"

View File

@@ -1,10 +1,12 @@
use anyhow::Result;
use vergen::EmitBuilder;
fn main() -> Result<()> {
#[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(())
}

View File

@@ -7,6 +7,7 @@ use std::{
atomic::{AtomicBool, Ordering},
Arc, Mutex, RwLock,
},
time::Instant,
};
use filetime::FileTime;
@@ -39,7 +40,7 @@ use crate::{
frame_history::FrameHistory,
function_diff::function_diff_ui,
graphics::{graphics_window, GraphicsConfig, GraphicsViewState},
jobs::jobs_ui,
jobs::{jobs_menu_ui, jobs_window},
rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState},
symbol_diff::{symbol_diff_ui, DiffViewState, View},
},
@@ -61,6 +62,7 @@ pub struct ViewState {
pub show_arch_config: bool,
pub show_debug: bool,
pub show_graphics: bool,
pub show_jobs: bool,
}
/// The configuration for a single object file.
@@ -72,6 +74,7 @@ pub struct ObjectConfig {
pub reverse_fn_order: Option<bool>,
pub complete: Option<bool>,
pub scratch: Option<ScratchConfig>,
pub source_path: Option<String>,
}
#[inline]
@@ -82,6 +85,36 @@ fn default_watch_patterns() -> Vec<Glob> {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
}
pub struct AppState {
pub config: AppConfig,
pub objects: Vec<ProjectObject>,
pub object_nodes: Vec<ProjectObjectNode>,
pub watcher_change: bool,
pub config_change: bool,
pub obj_change: bool,
pub queue_build: bool,
pub queue_reload: bool,
pub project_config_info: Option<ProjectConfigInfo>,
pub last_mod_check: Instant,
}
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,
project_config_info: None,
last_mod_check: Instant::now(),
}
}
}
#[derive(Clone, serde::Deserialize, serde::Serialize)]
pub struct AppConfig {
// TODO: https://github.com/ron-rs/ron/pull/455
@@ -116,23 +149,6 @@ pub struct AppConfig {
pub recent_projects: Vec<PathBuf>,
#[serde(default)]
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 {
@@ -153,30 +169,22 @@ impl Default for AppConfig {
watch_patterns: DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(),
recent_projects: vec![],
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) {
self.recent_projects.retain(|p| p != &path);
if self.recent_projects.len() > 9 {
self.recent_projects.truncate(9);
self.config.recent_projects.retain(|p| p != &path);
if self.config.recent_projects.len() > 9 {
self.config.recent_projects.truncate(9);
}
self.recent_projects.insert(0, path.clone());
self.project_dir = Some(path);
self.target_obj_dir = None;
self.base_obj_dir = None;
self.selected_obj = None;
self.build_target = false;
self.config.recent_projects.insert(0, path.clone());
self.config.project_dir = Some(path);
self.config.target_obj_dir = None;
self.config.base_obj_dir = None;
self.config.selected_obj = None;
self.config.build_target = false;
self.objects.clear();
self.object_nodes.clear();
self.watcher_change = true;
@@ -187,33 +195,33 @@ impl AppConfig {
}
pub fn set_target_obj_dir(&mut self, path: PathBuf) {
self.target_obj_dir = Some(path);
self.selected_obj = None;
self.config.target_obj_dir = Some(path);
self.config.selected_obj = None;
self.obj_change = true;
self.queue_build = false;
}
pub fn set_base_obj_dir(&mut self, path: PathBuf) {
self.base_obj_dir = Some(path);
self.selected_obj = None;
self.config.base_obj_dir = Some(path);
self.config.selected_obj = None;
self.obj_change = true;
self.queue_build = false;
}
pub fn set_selected_obj(&mut self, object: ObjectConfig) {
self.selected_obj = Some(object);
self.config.selected_obj = Some(object);
self.obj_change = true;
self.queue_build = false;
}
}
pub type AppConfigRef = Arc<RwLock<AppConfig>>;
pub type AppStateRef = Arc<RwLock<AppState>>;
#[derive(Default)]
pub struct App {
appearance: Appearance,
view_state: ViewState,
config: AppConfigRef,
state: AppStateRef,
modified: Arc<AtomicBool>,
watcher: Option<notify::RecommendedWatcher>,
app_path: Option<PathBuf>,
@@ -241,16 +249,17 @@ impl App {
if let Some(appearance) = eframe::get_value::<Appearance>(storage, APPEARANCE_KEY) {
app.appearance = appearance;
}
if let Some(mut config) = deserialize_config(storage) {
if config.project_dir.is_some() {
config.config_change = true;
config.watcher_change = true;
if let Some(config) = deserialize_config(storage) {
let mut state = AppState { config, ..Default::default() };
if state.config.project_dir.is_some() {
state.config_change = true;
state.watcher_change = true;
}
if config.selected_obj.is_some() {
config.queue_build = true;
if state.config.selected_obj.is_some() {
state.queue_build = true;
}
app.view_state.config_state.queue_check_update = config.auto_update_check;
app.config = Arc::new(RwLock::new(config));
app.view_state.config_state.queue_check_update = state.config.auto_update_check;
app.state = Arc::new(RwLock::new(state));
}
}
app.appearance.init_fonts(&cc.egui_ctx);
@@ -336,8 +345,8 @@ impl App {
jobs.results.append(&mut results);
jobs.clear_finished();
diff_state.pre_update(jobs, &self.config);
config_state.pre_update(jobs, &self.config);
diff_state.pre_update(jobs, &self.state);
config_state.pre_update(jobs, &self.state);
debug_assert!(jobs.results.is_empty());
}
@@ -345,23 +354,23 @@ impl App {
self.appearance.post_update(ctx);
let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state;
config_state.post_update(ctx, jobs, &self.config);
diff_state.post_update(ctx, jobs, &self.config);
config_state.post_update(ctx, jobs, &self.state);
diff_state.post_update(ctx, jobs, &self.state);
let Ok(mut config) = self.config.write() else {
let Ok(mut state) = self.state.write() else {
return;
};
let config = &mut *config;
let state = &mut *state;
if let Some(info) = &config.project_config_info {
if let Some(info) = &state.project_config_info {
if file_modified(&info.path, info.timestamp) {
config.config_change = true;
state.config_change = true;
}
}
if config.config_change {
config.config_change = false;
match load_project_config(config) {
if state.config_change {
state.config_change = false;
match load_project_config(state) {
Ok(()) => config_state.load_error = None,
Err(e) => {
log::error!("Failed to load project config: {e}");
@@ -370,47 +379,50 @@ impl App {
}
}
if config.watcher_change {
if state.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| {
if let Some(project_dir) = &state.config.project_dir {
match build_globset(&state.config.watch_patterns)
.map_err(anyhow::Error::new)
.and_then(|globset| {
create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset)
.map_err(anyhow::Error::new)
},
) {
}) {
Ok(watcher) => self.watcher = Some(watcher),
Err(e) => log::error!("Failed to create watcher: {e}"),
}
config.watcher_change = false;
state.watcher_change = false;
}
}
if config.obj_change {
if state.obj_change {
*diff_state = Default::default();
if config.selected_obj.is_some() {
config.queue_build = true;
if state.config.selected_obj.is_some() {
state.queue_build = true;
}
config.obj_change = false;
state.obj_change = false;
}
if self.modified.swap(false, Ordering::Relaxed) && config.rebuild_on_changes {
config.queue_build = true;
if self.modified.swap(false, Ordering::Relaxed) && state.config.rebuild_on_changes {
state.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 state.last_mod_check.elapsed().as_millis() >= 500 {
state.last_mod_check = Instant::now();
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) {
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) {
state.queue_reload = true;
}
}
}
}
@@ -418,17 +430,20 @@ impl App {
// Don't clear `queue_build` if a build is running. A file may have been modified during
// the build, so we'll start another build after the current one finishes.
if config.queue_build && config.selected_obj.is_some() && !jobs.is_running(Job::ObjDiff) {
jobs.push(start_build(ctx, ObjDiffConfig::from_config(config)));
config.queue_build = false;
config.queue_reload = false;
} else if config.queue_reload && !jobs.is_running(Job::ObjDiff) {
let mut diff_config = ObjDiffConfig::from_config(config);
if state.queue_build
&& state.config.selected_obj.is_some()
&& !jobs.is_running(Job::ObjDiff)
{
jobs.push(start_build(ctx, ObjDiffConfig::from_config(&state.config)));
state.queue_build = false;
state.queue_reload = false;
} else if state.queue_reload && !jobs.is_running(Job::ObjDiff) {
let mut diff_config = ObjDiffConfig::from_config(&state.config);
// Don't build, just reload the current files
diff_config.build_base = false;
diff_config.build_target = false;
jobs.push(start_build(ctx, diff_config));
config.queue_reload = false;
state.queue_reload = false;
}
if graphics_state.should_relaunch {
@@ -453,7 +468,7 @@ impl eframe::App for App {
self.pre_update(ctx);
let Self { config, appearance, view_state, .. } = self;
let Self { state, appearance, view_state, .. } = self;
let ViewState {
jobs,
config_state,
@@ -469,6 +484,7 @@ impl eframe::App for App {
show_arch_config,
show_debug,
show_graphics,
show_jobs,
} = view_state;
frame_history.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
@@ -485,8 +501,8 @@ impl eframe::App for App {
*show_project_config = !*show_project_config;
ui.close_menu();
}
let recent_projects = if let Ok(guard) = config.read() {
guard.recent_projects.clone()
let recent_projects = if let Ok(guard) = state.read() {
guard.config.recent_projects.clone()
} else {
vec![]
};
@@ -495,12 +511,12 @@ impl eframe::App for App {
} else {
ui.menu_button("Recent Projects…", |ui| {
if ui.button("Clear").clicked() {
config.write().unwrap().recent_projects.clear();
state.write().unwrap().config.recent_projects.clear();
};
ui.separator();
for path in recent_projects {
if ui.button(format!("{}", path.display())).clicked() {
config.write().unwrap().set_project_dir(path);
state.write().unwrap().set_project_dir(path);
ui.close_menu();
}
}
@@ -533,12 +549,12 @@ impl eframe::App for App {
*show_arch_config = !*show_arch_config;
ui.close_menu();
}
let mut config = config.write().unwrap();
let mut state = state.write().unwrap();
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.");
if response.changed() {
config.watcher_change = true;
state.watcher_change = true;
};
ui.add_enabled(
!diff_state.symbol_state.disable_reverse_fn_order,
@@ -554,7 +570,7 @@ impl eframe::App for App {
);
if ui
.checkbox(
&mut config.diff_obj_config.relax_reloc_diffs,
&mut state.config.diff_obj_config.relax_reloc_diffs,
"Relax relocation diffs",
)
.on_hover_text(
@@ -562,28 +578,32 @@ impl eframe::App for App {
)
.changed()
{
config.queue_reload = true;
state.queue_reload = true;
}
if ui
.checkbox(
&mut config.diff_obj_config.space_between_args,
&mut state.config.diff_obj_config.space_between_args,
"Space between args",
)
.changed()
{
config.queue_reload = true;
state.queue_reload = true;
}
if ui
.checkbox(
&mut config.diff_obj_config.combine_data_sections,
&mut state.config.diff_obj_config.combine_data_sections,
"Combine data sections",
)
.on_hover_text("Combines data sections with equal names.")
.changed()
{
config.queue_reload = true;
state.queue_reload = true;
}
});
ui.separator();
if jobs_menu_ui(ui, jobs, appearance) {
*show_jobs = !*show_jobs;
}
});
});
@@ -603,8 +623,7 @@ impl eframe::App for App {
} else {
egui::SidePanel::left("side_panel").show(ctx, |ui| {
egui::ScrollArea::both().show(ui, |ui| {
config_ui(ui, config, show_project_config, config_state, appearance);
jobs_ui(ui, jobs, appearance);
config_ui(ui, state, show_project_config, config_state, appearance);
});
});
@@ -613,21 +632,22 @@ impl eframe::App for App {
});
}
project_window(ctx, config, show_project_config, config_state, appearance);
project_window(ctx, state, show_project_config, config_state, appearance);
appearance_window(ctx, show_appearance_config, appearance);
demangle_window(ctx, show_demangle, demangle_state, appearance);
rlwinm_decode_window(ctx, show_rlwinm_decode, rlwinm_decode_state, appearance);
arch_config_window(ctx, config, show_arch_config, appearance);
arch_config_window(ctx, state, show_arch_config, appearance);
debug_window(ctx, show_debug, frame_history, appearance);
graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance);
jobs_window(ctx, show_jobs, jobs, appearance);
self.post_update(ctx);
}
/// Called by the frame work to save state before shutdown.
fn save(&mut self, storage: &mut dyn eframe::Storage) {
if let Ok(config) = self.config.read() {
eframe::set_value(storage, CONFIG_KEY, &*config);
if let Ok(state) = self.state.read() {
eframe::set_value(storage, CONFIG_KEY, &state.config);
}
eframe::set_value(storage, APPEARANCE_KEY, &self.appearance);
}

View File

@@ -61,6 +61,7 @@ impl ObjectConfigV0 {
reverse_fn_order: self.reverse_fn_order,
complete: None,
scratch: None,
source_path: None,
}
}
}

View File

@@ -4,7 +4,7 @@ use anyhow::Result;
use globset::Glob;
use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS};
use crate::app::AppConfig;
use crate::app::AppState;
#[derive(Clone)]
pub enum ProjectObjectNode {
@@ -64,30 +64,30 @@ fn build_nodes(
nodes
}
pub fn load_project_config(config: &mut AppConfig) -> Result<()> {
let Some(project_dir) = &config.project_dir else {
pub fn load_project_config(state: &mut AppState) -> Result<()> {
let Some(project_dir) = &state.config.project_dir else {
return Ok(());
};
if let Some((result, info)) = try_project_config(project_dir) {
let project_config = result?;
config.custom_make = project_config.custom_make;
config.custom_args = project_config.custom_args;
config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p));
config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p));
config.build_base = project_config.build_base;
config.build_target = project_config.build_target;
config.watch_patterns = project_config.watch_patterns.unwrap_or_else(|| {
state.config.custom_make = project_config.custom_make;
state.config.custom_args = project_config.custom_args;
state.config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p));
state.config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p));
state.config.build_base = project_config.build_base;
state.config.build_target = project_config.build_target;
state.config.watch_patterns = project_config.watch_patterns.unwrap_or_else(|| {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
});
config.watcher_change = true;
config.objects = project_config.objects;
config.object_nodes = build_nodes(
&config.objects,
state.watcher_change = true;
state.objects = project_config.objects;
state.object_nodes = build_nodes(
&state.objects,
project_dir,
config.target_obj_dir.as_deref(),
config.base_obj_dir.as_deref(),
state.config.target_obj_dir.as_deref(),
state.config.base_obj_dir.as_deref(),
);
config.project_config_info = Some(info);
state.project_config_info = Some(info);
}
Ok(())
}

View File

@@ -85,12 +85,15 @@ impl JobQueue {
/// Clears all finished jobs.
pub fn clear_finished(&mut self) {
self.jobs.retain(|job| {
!(job.should_remove
&& job.handle.is_none()
&& job.context.status.read().unwrap().error.is_none())
!(job.handle.is_none() && job.context.status.read().unwrap().error.is_none())
});
}
/// Clears all errored jobs.
pub fn clear_errored(&mut self) {
self.jobs.retain(|job| job.context.status.read().unwrap().error.is_none());
}
/// Removes a job from the queue given its ID.
pub fn remove(&mut self, id: usize) { self.jobs.retain(|job| job.id != id); }
}
@@ -107,7 +110,6 @@ pub struct JobState {
pub handle: Option<JoinHandle<JobResult>>,
pub context: JobContext,
pub cancel: Sender<()>,
pub should_remove: bool,
}
#[derive(Default)]
@@ -163,7 +165,7 @@ fn start_job(
});
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
log::info!("Started job {}", id);
JobState { id, kind, handle: Some(handle), context, cancel: tx, should_remove: true }
JobState { id, kind, handle: Some(handle), context, cancel: tx }
}
fn update_status(

View File

@@ -1,11 +1,10 @@
use std::{
path::{Path, PathBuf},
process::Command,
str::from_utf8,
sync::mpsc::Receiver,
};
use anyhow::{anyhow, Context, Error, Result};
use anyhow::{anyhow, Error, Result};
use objdiff_core::{
diff::{diff_objs, DiffObjConfig, ObjDiff},
obj::{read, ObjInfo},
@@ -91,13 +90,6 @@ pub(crate) fn run_make(config: &BuildConfig, arg: &Path) -> BuildStatus {
..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))]
@@ -144,15 +136,23 @@ fn run_make_cmd(config: &BuildConfig, cwd: &Path, arg: &Path) -> Result<BuildSta
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(),
})
let output = match command.output() {
Ok(output) => output,
Err(e) => {
return BuildStatus {
success: false,
cmdline,
stdout: Default::default(),
stderr: e.to_string(),
};
}
};
// Try from_utf8 first to avoid copying the buffer if it's valid, then fall back to from_utf8_lossy
let stdout = String::from_utf8(output.stdout)
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
let stderr = String::from_utf8(output.stderr)
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
BuildStatus { success: output.status.success(), cmdline, stdout, stderr }
}
fn run_build(
@@ -189,36 +189,46 @@ fn run_build(
None
};
let mut total = 3;
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;
}
let first_status = match target_path_rel {
if target_path_rel.is_some() {
total += 1;
}
if base_path_rel.is_some() {
total += 1;
}
let mut step_idx = 0;
let mut first_status = match target_path_rel {
Some(target_path_rel) if config.build_target => {
update_status(
context,
format!("Building target {}", target_path_rel.display()),
0,
step_idx,
total,
&cancel,
)?;
step_idx += 1;
run_make(&config.build_config, target_path_rel)
}
_ => BuildStatus::default(),
};
let second_status = match base_path_rel {
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()),
0,
step_idx,
total,
&cancel,
)?;
step_idx += 1;
run_make(&config.build_config, base_path_rel)
}
_ => BuildStatus::default(),
@@ -226,44 +236,71 @@ fn run_build(
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())
})?)
let first_obj = match &obj_config.target_path {
Some(target_path) if first_status.success => {
update_status(
context,
format!("Loading target {}", target_path_rel.unwrap().display()),
step_idx,
total,
&cancel,
)?;
step_idx += 1;
match read::read(target_path, &config.diff_obj_config) {
Ok(obj) => Some(obj),
Err(e) => {
first_status = BuildStatus {
success: false,
stdout: format!("Loading object '{}'", target_path.display()),
stderr: format!("{:#}", e),
..Default::default()
};
None
}
}
_ => None,
};
}
Some(_) => {
step_idx += 1;
None
}
_ => None,
};
let second_obj = match &obj_config.base_path {
Some(base_path) if second_status.success => {
update_status(
context,
format!("Loading base {}", base_path_rel.unwrap().display()),
3,
step_idx,
total,
&cancel,
)?;
Some(
read::read(base_path, &config.diff_obj_config)
.with_context(|| format!("Failed to read object '{}'", base_path.display()))?,
)
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(), 4, total, &cancel)?;
update_status(context, "Performing diff".to_string(), step_idx, total, &cancel)?;
step_idx += 1;
let result = diff_objs(&config.diff_obj_config, first_obj.as_ref(), second_obj.as_ref(), None)?;
update_status(context, "Complete".to_string(), total, total, &cancel)?;
update_status(context, "Complete".to_string(), step_idx, total, &cancel)?;
Ok(Box::new(ObjDiffResult {
first_status,
second_status,
@@ -274,7 +311,7 @@ fn run_build(
}
pub fn start_build(ctx: &egui::Context, config: ObjDiffConfig) -> JobState {
start_job(ctx, "Object diff", Job::ObjDiff, move |context, cancel| {
start_job(ctx, "Build", Job::ObjDiff, move |context, cancel| {
run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
})
}

View File

@@ -36,7 +36,7 @@ fn run_update(
let tmp_file = File::create(&tmp_path)?;
self_update::Download::from_url(&asset.download_url)
.set_header(reqwest::header::ACCEPT, "application/octet-stream".parse()?)
.download_to(&tmp_file)?;
.download_to(tmp_file)?;
update_status(status, "Extracting release".to_string(), 2, 3, &cancel)?;
let tmp_file = tmp_dir.path().join("replacement_tmp");
@@ -51,6 +51,7 @@ fn run_update(
perms.set_mode(0o755);
fs::set_permissions(&target_file, perms)?;
}
tmp_dir.close()?;
update_status(status, "Complete".to_string(), 3, 3, &cancel)?;
Ok(Box::from(UpdateResult { exe_path: target_file }))

View File

@@ -1,4 +1,3 @@
#![warn(clippy::all, rust_2018_idioms)]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
mod app;
@@ -19,6 +18,7 @@ use std::{
use anyhow::{ensure, Result};
use cfg_if::cfg_if;
use time::UtcOffset;
use tracing_subscriber::EnvFilter;
use crate::views::graphics::{load_graphics_config, GraphicsBackend, GraphicsConfig};
@@ -40,7 +40,16 @@ const APP_NAME: &str = "objdiff";
#[cfg(not(target_arch = "wasm32"))]
fn main() -> ExitCode {
// 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,
// we must call this before initializing eframe.
@@ -49,8 +58,10 @@ fn main() -> ExitCode {
let app_path = std::env::current_exe().ok();
let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None));
let mut native_options =
eframe::NativeOptions { follow_system_theme: false, ..Default::default() };
let mut native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_app_id(APP_NAME),
..Default::default()
};
match load_icon() {
Ok(data) => {
native_options.viewport.icon = Some(Arc::new(data));
@@ -189,14 +200,14 @@ fn run_eframe(
APP_NAME,
native_options,
Box::new(move |cc| {
Box::new(app::App::new(
Ok(Box::new(app::App::new(
cc,
utc_offset,
exec_path_clone,
app_path,
graphics_config,
graphics_config_path,
))
)))
}),
)
}

View File

@@ -11,7 +11,7 @@ pub struct Appearance {
pub ui_font: FontId,
pub code_font: FontId,
pub diff_colors: Vec<Color32>,
pub theme: eframe::Theme,
pub theme: egui::Theme,
// Applied by theme
#[serde(skip)]
@@ -56,7 +56,7 @@ impl Default for Appearance {
ui_font: DEFAULT_UI_FONT,
code_font: DEFAULT_CODE_FONT,
diff_colors: DEFAULT_COLOR_ROTATION.to_vec(),
theme: eframe::Theme::Dark,
theme: egui::Theme::Dark,
text_color: Color32::GRAY,
emphasized_text_color: Color32::LIGHT_GRAY,
deemphasized_text_color: Color32::DARK_GRAY,
@@ -98,7 +98,7 @@ impl Appearance {
});
style.text_styles.insert(TextStyle::Monospace, self.code_font.clone());
match self.theme {
eframe::Theme::Dark => {
egui::Theme::Dark => {
style.visuals = egui::Visuals::dark();
self.text_color = Color32::GRAY;
self.emphasized_text_color = Color32::LIGHT_GRAY;
@@ -108,7 +108,7 @@ impl Appearance {
self.insert_color = Color32::GREEN;
self.delete_color = Color32::from_rgb(200, 40, 41);
}
eframe::Theme::Light => {
egui::Theme::Light => {
style.visuals = egui::Visuals::light();
self.text_color = Color32::GRAY;
self.emphasized_text_color = Color32::DARK_GRAY;
@@ -274,8 +274,8 @@ pub fn appearance_window(ctx: &egui::Context, show: &mut bool, appearance: &mut
egui::ComboBox::from_label("Theme")
.selected_text(format!("{:?}", appearance.theme))
.show_ui(ui, |ui| {
ui.selectable_value(&mut appearance.theme, eframe::Theme::Dark, "Dark");
ui.selectable_value(&mut appearance.theme, eframe::Theme::Light, "Light");
ui.selectable_value(&mut appearance.theme, egui::Theme::Dark, "Dark");
ui.selectable_value(&mut appearance.theme, egui::Theme::Light, "Light");
});
ui.separator();
appearance.next_ui_font =

View File

@@ -2,12 +2,11 @@
use std::string::FromUtf16Error;
use std::{
mem::take,
path::{PathBuf, MAIN_SEPARATOR},
path::{Path, PathBuf, MAIN_SEPARATOR},
};
#[cfg(all(windows, feature = "wsl"))]
use anyhow::{Context, Result};
use const_format::formatcp;
use egui::{
output::OpenUrl, text::LayoutJob, CollapsingHeader, FontFamily, FontId, RichText,
SelectableLabel, TextFormat, Widget,
@@ -17,11 +16,10 @@ use objdiff_core::{
config::{ProjectObject, DEFAULT_WATCH_PATTERNS},
diff::{ArmArchVersion, ArmR9Usage, MipsAbi, MipsInstrCategory, X86Formatter},
};
use self_update::cargo_crate_version;
use strum::{EnumMessage, VariantArray};
use crate::{
app::{AppConfig, AppConfigRef, ObjectConfig},
app::{AppConfig, AppState, AppStateRef, ObjectConfig},
config::ProjectObjectNode,
jobs::{
check_update::{start_check_update, CheckUpdateResult},
@@ -56,7 +54,7 @@ pub struct 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| {
if let JobResult::CheckUpdate(result) = result {
self.check_update = take(result);
@@ -73,21 +71,21 @@ impl ConfigViewState {
match self.file_dialog_state.poll() {
FileDialogResult::None => {}
FileDialogResult::ProjectDir(path) => {
let mut guard = config.write().unwrap();
let mut guard = state.write().unwrap();
guard.set_project_dir(path.to_path_buf());
}
FileDialogResult::TargetDir(path) => {
let mut guard = config.write().unwrap();
let mut guard = state.write().unwrap();
guard.set_target_obj_dir(path.to_path_buf());
}
FileDialogResult::BaseDir(path) => {
let mut guard = config.write().unwrap();
let mut guard = state.write().unwrap();
guard.set_base_obj_dir(path.to_path_buf());
}
FileDialogResult::Object(path) => {
let mut guard = config.write().unwrap();
let mut guard = state.write().unwrap();
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) {
let target_path = target_dir.join(obj_path);
@@ -98,6 +96,7 @@ impl ConfigViewState {
reverse_fn_order: None,
complete: None,
scratch: None,
source_path: None,
});
} else if let Ok(obj_path) = path.strip_prefix(target_dir) {
let base_path = base_dir.join(obj_path);
@@ -108,6 +107,7 @@ impl ConfigViewState {
reverse_fn_order: None,
complete: None,
scratch: None,
source_path: None,
});
}
}
@@ -115,11 +115,11 @@ 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 {
self.queue_build = false;
if let Ok(mut config) = config.write() {
config.queue_build = true;
if let Ok(mut state) = state.write() {
state.queue_build = true;
}
}
@@ -169,47 +169,43 @@ fn fetch_wsl2_distros() -> Vec<String> {
pub fn config_ui(
ui: &mut egui::Ui,
config: &AppConfigRef,
state: &AppStateRef,
show_config_window: &mut bool,
state: &mut ConfigViewState,
config_state: &mut ConfigViewState,
appearance: &Appearance,
) {
let mut config_guard = config.write().unwrap();
let AppConfig {
target_obj_dir,
base_obj_dir,
selected_obj,
auto_update_check,
let mut state_guard = state.write().unwrap();
let AppState {
config:
AppConfig {
project_dir, target_obj_dir, base_obj_dir, selected_obj, auto_update_check, ..
},
objects,
object_nodes,
..
} = &mut *config_guard;
} = &mut *state_guard;
ui.heading("Updates");
ui.checkbox(auto_update_check, "Check for updates on startup");
if ui.add_enabled(!state.check_update_running, egui::Button::new("Check now")).clicked() {
state.queue_check_update = true;
if ui.add_enabled(!config_state.check_update_running, egui::Button::new("Check now")).clicked()
{
config_state.queue_check_update = true;
}
ui.label(format!("Current version: {}", cargo_crate_version!())).on_hover_ui_at_pointer(|ui| {
ui.label(formatcp!("Git branch: {}", env!("VERGEN_GIT_BRANCH")));
ui.label(formatcp!("Git commit: {}", env!("VERGEN_GIT_SHA")));
ui.label(formatcp!("Build target: {}", env!("VERGEN_CARGO_TARGET_TRIPLE")));
ui.label(formatcp!("Debug: {}", env!("VERGEN_CARGO_DEBUG")));
});
if let Some(result) = &state.check_update {
ui.label(format!("Current version: {}", env!("CARGO_PKG_VERSION")));
if let Some(result) = &config_state.check_update {
ui.label(format!("Latest version: {}", result.latest_release.version));
if result.update_available {
ui.colored_label(appearance.insert_color, "Update available");
ui.horizontal(|ui| {
if let Some(bin_name) = &result.found_binary {
if ui
.add_enabled(!state.update_running, egui::Button::new("Automatic"))
.add_enabled(!config_state.update_running, egui::Button::new("Automatic"))
.on_hover_text_at_pointer(
"Automatically download and replace the current build",
)
.clicked()
{
state.queue_update = Some(bin_name.clone());
config_state.queue_update = Some(bin_name.clone());
}
}
if ui
@@ -238,7 +234,7 @@ pub fn config_ui(
if objects.is_empty() {
if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) {
if ui.button("Select object").clicked() {
state.file_dialog_state.queue(
config_state.file_dialog_state.queue(
|| {
Box::pin(
rfd::AsyncFileDialog::new()
@@ -261,8 +257,8 @@ pub fn config_ui(
ui.colored_label(appearance.delete_color, "Missing project settings");
}
} else {
let had_search = !state.object_search.is_empty();
egui::TextEdit::singleline(&mut state.object_search).hint_text("Filter").ui(ui);
let had_search = !config_state.object_search.is_empty();
egui::TextEdit::singleline(&mut config_state.object_search).hint_text("Filter").ui(ui);
let mut root_open = None;
let mut node_open = NodeOpen::Default;
@@ -284,19 +280,22 @@ pub fn config_ui(
node_open = NodeOpen::Object;
}
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);
}
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");
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");
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");
});
});
if state.object_search.is_empty() {
if config_state.object_search.is_empty() {
if had_search {
root_open = Some(true);
node_open = NodeOpen::Object;
@@ -313,39 +312,45 @@ pub fn config_ui(
.open(root_open)
.default_open(true)
.show(ui, |ui| {
let search = state.object_search.to_ascii_lowercase();
ui.style_mut().wrap = Some(false);
let search = config_state.object_search.to_ascii_lowercase();
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
for node in object_nodes.iter().filter_map(|node| {
filter_node(
node,
&search,
state.filter_diffable,
state.filter_incomplete,
state.show_hidden,
config_state.filter_diffable,
config_state.filter_incomplete,
config_state.show_hidden,
)
}) {
display_node(ui, &mut new_selected_obj, &node, appearance, node_open);
display_node(
ui,
&mut new_selected_obj,
project_dir.as_deref(),
&node,
appearance,
node_open,
);
}
});
}
if new_selected_obj != *selected_obj {
if let Some(obj) = new_selected_obj {
// Will set obj_changed, which will trigger a rebuild
config_guard.set_selected_obj(obj);
state_guard.set_selected_obj(obj);
}
}
if config_guard.selected_obj.is_some()
&& ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked()
if state_guard.config.selected_obj.is_some()
&& 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(
ui: &mut egui::Ui,
selected_obj: &mut Option<ObjectConfig>,
project_dir: Option<&Path>,
name: &str,
object: &ProjectObject,
appearance: &Appearance,
@@ -363,7 +368,7 @@ fn display_object(
} else {
appearance.text_color
};
let clicked = SelectableLabel::new(
let response = SelectableLabel::new(
selected,
RichText::new(name)
.font(FontId {
@@ -372,11 +377,13 @@ fn display_object(
})
.color(color),
)
.ui(ui)
.clicked();
.ui(ui);
if get_source_path(project_dir, object).is_some() {
response.context_menu(|ui| object_context_ui(ui, object, project_dir));
}
// Always recreate ObjectConfig if selected, in case the project config changed.
// ObjectConfig is compared using equality, so this won't unnecessarily trigger a rebuild.
if selected || clicked {
if selected || response.clicked() {
*selected_obj = Some(ObjectConfig {
name: object_name.to_string(),
target_path: object.target_path.clone(),
@@ -384,10 +391,31 @@ fn display_object(
reverse_fn_order: object.reverse_fn_order(),
complete: object.complete(),
scratch: object.scratch.clone(),
source_path: object.source_path().cloned(),
});
}
}
fn get_source_path(project_dir: Option<&Path>, object: &ProjectObject) -> Option<PathBuf> {
project_dir.and_then(|dir| object.source_path().map(|path| dir.join(path)))
}
fn object_context_ui(ui: &mut egui::Ui, object: &ProjectObject, project_dir: Option<&Path>) {
if let Some(source_path) = get_source_path(project_dir, object) {
if ui
.button("Open source file")
.on_hover_text("Open the source file in the default editor")
.clicked()
{
log::info!("Opening file {}", source_path.display());
if let Err(e) = open::that_detached(&source_path) {
log::error!("Failed to open source file: {e}");
}
ui.close_menu();
}
}
}
#[derive(Default, Copy, Clone, PartialEq, Eq, Debug)]
enum NodeOpen {
#[default]
@@ -400,13 +428,14 @@ enum NodeOpen {
fn display_node(
ui: &mut egui::Ui,
selected_obj: &mut Option<ObjectConfig>,
project_dir: Option<&Path>,
node: &ProjectObjectNode,
appearance: &Appearance,
node_open: NodeOpen,
) {
match node {
ProjectObjectNode::File(name, object) => {
display_object(ui, selected_obj, name, object, appearance);
display_object(ui, selected_obj, project_dir, name, object, appearance);
}
ProjectObjectNode::Dir(name, children) => {
let contains_obj = selected_obj.as_ref().map(|path| contains_node(node, path));
@@ -432,7 +461,7 @@ fn display_node(
.open(open)
.show(ui, |ui| {
for node in children {
display_node(ui, selected_obj, node, appearance, node_open);
display_node(ui, selected_obj, project_dir, node, appearance, node_open);
}
});
}
@@ -530,33 +559,33 @@ fn pick_folder_ui(
pub fn project_window(
ctx: &egui::Context,
config: &AppConfigRef,
state: &AppStateRef,
show: &mut bool,
state: &mut ConfigViewState,
config_state: &mut ConfigViewState,
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| {
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) = &config_state.load_error {
let mut open = true;
egui::Window::new("Error").open(&mut open).show(ctx, |ui| {
ui.label("Failed to load project config:");
ui.colored_label(appearance.delete_color, error);
});
if !open {
state.load_error = None;
config_state.load_error = None;
}
}
}
fn split_obj_config_ui(
ui: &mut egui::Ui,
config: &mut AppConfig,
state: &mut ConfigViewState,
state: &mut AppState,
config_state: &mut ConfigViewState,
appearance: &Appearance,
) {
let text_format = TextFormat::simple(appearance.ui_font.clone(), appearance.text_color);
@@ -567,7 +596,7 @@ fn split_obj_config_ui(
let response = pick_folder_ui(
ui,
&config.project_dir,
&state.config.project_dir,
"Project directory",
|ui| {
let mut job = LayoutJob::default();
@@ -583,7 +612,7 @@ fn split_obj_config_ui(
true,
);
if response.clicked() {
state.file_dialog_state.queue(
config_state.file_dialog_state.queue(
|| Box::pin(rfd::AsyncFileDialog::new().pick_folder()),
FileDialogResult::ProjectDir,
);
@@ -612,33 +641,35 @@ fn split_obj_config_ui(
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
.add_enabled(
config.project_config_info.is_none(),
state.project_config_info.is_none(),
egui::TextEdit::singleline(&mut custom_make_str).hint_text("make"),
)
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.changed()
{
if custom_make_str.is_empty() {
config.custom_make = None;
state.config.custom_make = None;
} else {
config.custom_make = Some(custom_make_str);
state.config.custom_make = Some(custom_make_str);
}
}
#[cfg(all(windows, feature = "wsl"))]
{
if state.available_wsl_distros.is_none() {
state.available_wsl_distros = Some(fetch_wsl2_distros());
if config_state.available_wsl_distros.is_none() {
config_state.available_wsl_distros = Some(fetch_wsl2_distros());
}
egui::ComboBox::from_label("Run in WSL2")
.selected_text(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| {
ui.selectable_value(&mut config.selected_wsl_distro, None, "Disabled");
for distro in state.available_wsl_distros.as_ref().unwrap() {
ui.selectable_value(&mut state.config.selected_wsl_distro, None, "Disabled");
for distro in config_state.available_wsl_distros.as_ref().unwrap() {
ui.selectable_value(
&mut config.selected_wsl_distro,
&mut state.config.selected_wsl_distro,
Some(distro.clone()),
distro,
);
@@ -647,10 +678,10 @@ fn split_obj_config_ui(
}
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(
ui,
&config.target_obj_dir,
&state.config.target_obj_dir,
"Target build directory",
|ui| {
let mut job = LayoutJob::default();
@@ -667,17 +698,17 @@ fn split_obj_config_ui(
ui.label(job);
},
appearance,
config.project_config_info.is_none(),
state.project_config_info.is_none(),
);
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()),
FileDialogResult::TargetDir,
);
}
ui.add_enabled(
config.project_config_info.is_none(),
egui::Checkbox::new(&mut config.build_target, "Build target objects"),
state.project_config_info.is_none(),
egui::Checkbox::new(&mut state.config.build_target, "Build target objects"),
)
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.on_hover_ui(|ui| {
@@ -711,7 +742,7 @@ fn split_obj_config_ui(
let response = pick_folder_ui(
ui,
&config.base_obj_dir,
&state.config.base_obj_dir,
"Base build directory",
|ui| {
let mut job = LayoutJob::default();
@@ -723,17 +754,17 @@ fn split_obj_config_ui(
ui.label(job);
},
appearance,
config.project_config_info.is_none(),
state.project_config_info.is_none(),
);
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()),
FileDialogResult::BaseDir,
);
}
ui.add_enabled(
config.project_config_info.is_none(),
egui::Checkbox::new(&mut config.build_base, "Build base objects"),
state.project_config_info.is_none(),
egui::Checkbox::new(&mut state.config.build_base, "Build base objects"),
)
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.on_hover_ui(|ui| {
@@ -764,7 +795,7 @@ fn split_obj_config_ui(
subheading(ui, "Watch settings", appearance);
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();
job.append(
"Automatically re-run the build & diff when files change.",
@@ -774,23 +805,23 @@ fn split_obj_config_ui(
ui.label(job);
});
if response.changed() {
config.watcher_change = true;
state.watcher_change = true;
};
ui.horizontal(|ui| {
ui.label(RichText::new("File patterns").color(appearance.text_color));
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)
.clicked()
{
config.watch_patterns =
state.config.watch_patterns =
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;
for (idx, glob) in config.watch_patterns.iter().enumerate() {
for (idx, glob) in state.config.watch_patterns.iter().enumerate() {
ui.horizontal(|ui| {
ui.label(
RichText::new(format!("{}", glob))
@@ -798,7 +829,7 @@ fn split_obj_config_ui(
.family(FontFamily::Monospace),
);
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)
.clicked()
{
@@ -807,24 +838,24 @@ fn split_obj_config_ui(
});
}
if let Some(idx) = remove_at {
config.watch_patterns.remove(idx);
config.watcher_change = true;
state.config.watch_patterns.remove(idx);
state.watcher_change = true;
}
ui.horizontal(|ui| {
ui.add_enabled(
config.project_config_info.is_none(),
egui::TextEdit::singleline(&mut state.watch_pattern_text).desired_width(100.0),
state.project_config_info.is_none(),
egui::TextEdit::singleline(&mut config_state.watch_pattern_text).desired_width(100.0),
)
.on_disabled_hover_text(CONFIG_DISABLED_TEXT);
if ui
.add_enabled(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)
.clicked()
{
if let Ok(glob) = Glob::new(&state.watch_pattern_text) {
config.watch_patterns.push(glob);
config.watcher_change = true;
state.watch_pattern_text.clear();
if let Ok(glob) = Glob::new(&config_state.watch_pattern_text) {
state.config.watch_patterns.push(glob);
state.watcher_change = true;
config_state.watch_pattern_text.clear();
}
}
});
@@ -832,131 +863,131 @@ fn split_obj_config_ui(
pub fn arch_config_window(
ctx: &egui::Context,
config: &AppConfigRef,
state: &AppStateRef,
show: &mut bool,
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| {
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 arch_config_ui(ui: &mut egui::Ui, state: &mut AppState, _appearance: &Appearance) {
ui.heading("x86");
egui::ComboBox::new("x86_formatter", "Format")
.selected_text(config.diff_obj_config.x86_formatter.get_message().unwrap())
.selected_text(state.config.diff_obj_config.x86_formatter.get_message().unwrap())
.show_ui(ui, |ui| {
for &formatter in X86Formatter::VARIANTS {
if ui
.selectable_label(
config.diff_obj_config.x86_formatter == formatter,
state.config.diff_obj_config.x86_formatter == formatter,
formatter.get_message().unwrap(),
)
.clicked()
{
config.diff_obj_config.x86_formatter = formatter;
config.queue_reload = true;
state.config.diff_obj_config.x86_formatter = formatter;
state.queue_reload = true;
}
}
});
ui.separator();
ui.heading("MIPS");
egui::ComboBox::new("mips_abi", "ABI")
.selected_text(config.diff_obj_config.mips_abi.get_message().unwrap())
.selected_text(state.config.diff_obj_config.mips_abi.get_message().unwrap())
.show_ui(ui, |ui| {
for &abi in MipsAbi::VARIANTS {
if ui
.selectable_label(
config.diff_obj_config.mips_abi == abi,
state.config.diff_obj_config.mips_abi == abi,
abi.get_message().unwrap(),
)
.clicked()
{
config.diff_obj_config.mips_abi = abi;
config.queue_reload = true;
state.config.diff_obj_config.mips_abi = abi;
state.queue_reload = true;
}
}
});
egui::ComboBox::new("mips_instr_category", "Instruction Category")
.selected_text(config.diff_obj_config.mips_instr_category.get_message().unwrap())
.selected_text(state.config.diff_obj_config.mips_instr_category.get_message().unwrap())
.show_ui(ui, |ui| {
for &category in MipsInstrCategory::VARIANTS {
if ui
.selectable_label(
config.diff_obj_config.mips_instr_category == category,
state.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;
state.config.diff_obj_config.mips_instr_category = category;
state.queue_reload = true;
}
}
});
ui.separator();
ui.heading("ARM");
egui::ComboBox::new("arm_arch_version", "Architecture Version")
.selected_text(config.diff_obj_config.arm_arch_version.get_message().unwrap())
.selected_text(state.config.diff_obj_config.arm_arch_version.get_message().unwrap())
.show_ui(ui, |ui| {
for &version in ArmArchVersion::VARIANTS {
if ui
.selectable_label(
config.diff_obj_config.arm_arch_version == version,
state.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;
state.config.diff_obj_config.arm_arch_version = version;
state.queue_reload = true;
}
}
});
let response = ui
.checkbox(&mut config.diff_obj_config.arm_unified_syntax, "Unified syntax")
.checkbox(&mut state.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;
state.queue_reload = true;
}
let response = ui
.checkbox(&mut config.diff_obj_config.arm_av_registers, "Use A/V registers")
.checkbox(&mut state.config.diff_obj_config.arm_av_registers, "Use A/V registers")
.on_hover_text("Display R0-R3 as A1-A4 and R4-R11 as V1-V8");
if response.changed() {
config.queue_reload = true;
state.queue_reload = true;
}
egui::ComboBox::new("arm_r9_usage", "Display R9 as")
.selected_text(config.diff_obj_config.arm_r9_usage.get_message().unwrap())
.selected_text(state.config.diff_obj_config.arm_r9_usage.get_message().unwrap())
.show_ui(ui, |ui| {
for &usage in ArmR9Usage::VARIANTS {
if ui
.selectable_label(
config.diff_obj_config.arm_r9_usage == usage,
state.config.diff_obj_config.arm_r9_usage == usage,
usage.get_message().unwrap(),
)
.on_hover_text(usage.get_detailed_message().unwrap())
.clicked()
{
config.diff_obj_config.arm_r9_usage = usage;
config.queue_reload = true;
state.config.diff_obj_config.arm_r9_usage = usage;
state.queue_reload = true;
}
}
});
let response = ui
.checkbox(&mut config.diff_obj_config.arm_sl_usage, "Display R10 as SL")
.checkbox(&mut state.config.diff_obj_config.arm_sl_usage, "Display R10 as SL")
.on_hover_text("Used for explicit stack limits.");
if response.changed() {
config.queue_reload = true;
state.queue_reload = true;
}
let response = ui
.checkbox(&mut config.diff_obj_config.arm_fp_usage, "Display R11 as FP")
.checkbox(&mut state.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;
state.queue_reload = true;
}
let response = ui
.checkbox(&mut config.diff_obj_config.arm_ip_usage, "Display R12 as IP")
.checkbox(&mut state.config.diff_obj_config.arm_ip_usage, "Display R12 as IP")
.on_hover_text("Used for interworking and long branches.");
if response.changed() {
config.queue_reload = true;
state.queue_reload = true;
}
}

View File

@@ -191,6 +191,8 @@ pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &A
Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min),
|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
// Left column
ui.allocate_ui_with_layout(
Vec2 { x: column_width, y: 100.0 },
@@ -204,7 +206,6 @@ pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &A
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:");
});
@@ -227,7 +228,6 @@ pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &A
}
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 {
@@ -247,7 +247,6 @@ pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &A
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:");
});

View File

@@ -1,8 +1,9 @@
use egui::{text::LayoutJob, Align, Layout, ScrollArea, Ui, Vec2};
use egui::{Align, Layout, ScrollArea, Ui, Vec2};
use egui_extras::{Size, StripBuilder};
use objdiff_core::{
arch::ppc::ExceptionInfo,
diff::ObjDiff,
obj::{ObjExtab, ObjInfo, ObjSymbol, SymbolRef},
obj::{ObjInfo, ObjSymbol, SymbolRef},
};
use time::format_description;
@@ -22,7 +23,7 @@ fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<Symbo
None
}
fn decode_extab(extab: &ObjExtab) -> String {
fn decode_extab(extab: &ExceptionInfo) -> String {
let mut text = String::from("");
let mut dtor_names: Vec<&str> = vec![];
@@ -42,18 +43,8 @@ fn decode_extab(extab: &ObjExtab) -> String {
text
}
fn find_extab_entry(obj: &ObjInfo, symbol: &ObjSymbol) -> Option<ObjExtab> {
if let Some(extab_array) = &obj.extab {
for extab_entry in extab_array {
if extab_entry.func.name == symbol.name {
return Some(extab_entry.clone());
}
}
} else {
return None;
}
None
fn find_extab_entry<'a>(obj: &'a ObjInfo, symbol: &ObjSymbol) -> Option<&'a ExceptionInfo> {
obj.arch.ppc().and_then(|ppc| ppc.extab_for_symbol(symbol))
}
fn extab_text_ui(
@@ -65,7 +56,7 @@ fn extab_text_ui(
let (_section, symbol) = obj.0.section_symbol(symbol_ref);
if let Some(extab_entry) = find_extab_entry(&obj.0, symbol) {
let text = decode_extab(&extab_entry);
let text = decode_extab(extab_entry);
ui.colored_label(appearance.replace_color, &text);
return Some(());
}
@@ -83,7 +74,7 @@ fn extab_ui(
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
let symbol = obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol));
@@ -107,6 +98,8 @@ pub fn extab_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &
Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min),
|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
// Left column
ui.allocate_ui_with_layout(
Vec2 { x: column_width, y: 100.0 },
@@ -124,18 +117,9 @@ pub fn extab_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &
.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.colored_label(appearance.highlight_color, name);
ui.label("Diff target:");
});
},
@@ -157,7 +141,6 @@ pub fn extab_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &
}
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 {
@@ -189,7 +172,7 @@ pub fn extab_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &
{
ui.colored_label(
match_color_for_symbol(match_percent, appearance),
&format!("{match_percent:.0}%"),
format!("{match_percent:.0}%"),
);
} else {
ui.colored_label(appearance.replace_color, "Missing");

View File

@@ -17,9 +17,54 @@ use crate::views::{
symbol_diff::{match_color_for_symbol, DiffViewState, SymbolRefByName, View},
};
#[derive(Copy, Clone, Eq, PartialEq)]
enum ColumnId {
Left,
Right,
}
#[derive(Default)]
pub struct FunctionViewState {
pub highlight: HighlightKind,
left_highlight: HighlightKind,
right_highlight: HighlightKind,
}
impl FunctionViewState {
fn highlight(&self, column: ColumnId) -> &HighlightKind {
match column {
ColumnId::Left => &self.left_highlight,
ColumnId::Right => &self.right_highlight,
}
}
fn set_highlight(&mut self, column: ColumnId, highlight: HighlightKind) {
match column {
ColumnId::Left => {
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;
}
}
ColumnId::Right => {
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;
}
}
}
}
}
fn ins_hover_ui(
@@ -32,7 +77,7 @@ fn ins_hover_ui(
) {
ui.scope(|ui| {
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;
ui.label(format!(
@@ -79,6 +124,12 @@ fn ins_hover_ui(
appearance.highlight_color,
format!("Size: {:x}", reloc.target.size),
);
if let Some(s) = arch
.guess_data_type(ins)
.and_then(|ty| arch.display_data_type(ty, &reloc.target.bytes))
{
ui.colored_label(appearance.highlight_color, s);
}
} else {
ui.colored_label(appearance.highlight_color, "Extern".to_string());
}
@@ -89,7 +140,7 @@ fn ins_hover_ui(
fn ins_context_menu(ui: &mut egui::Ui, section: &ObjSection, ins: &ObjIns, symbol: &ObjSymbol) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
if ui.button(format!("Copy \"{}\"", ins.formatted)).clicked() {
ui.output_mut(|output| output.copied_text.clone_from(&ins.formatted));
@@ -167,12 +218,14 @@ fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<Symbo
None
}
#[allow(clippy::too_many_arguments)]
fn diff_text_ui(
ui: &mut egui::Ui,
text: DiffText<'_>,
ins_diff: &ObjInsDiff,
appearance: &Appearance,
ins_view_state: &mut FunctionViewState,
column: ColumnId,
space_width: f32,
response_cb: impl Fn(Response) -> Response,
) {
@@ -237,7 +290,7 @@ fn diff_text_ui(
}
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(
label_text,
appearance.code_text_format(base_color, highlight),
@@ -246,11 +299,7 @@ fn diff_text_ui(
.ui(ui);
response = response_cb(response);
if response.clicked() {
if highlight {
ins_view_state.highlight = HighlightKind::None;
} else {
ins_view_state.highlight = text.into();
}
ins_view_state.set_highlight(column, text.into());
}
if len < pad_to {
ui.add_space((pad_to - len) as f32 * space_width);
@@ -263,15 +312,26 @@ fn asm_row_ui(
symbol: &ObjSymbol,
appearance: &Appearance,
ins_view_state: &mut FunctionViewState,
column: ColumnId,
response_cb: impl Fn(Response) -> Response,
) {
ui.spacing_mut().item_spacing.x = 0.0;
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
if ins_diff.kind != ObjInsDiffKind::None {
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
}
let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' '));
display_diff(ins_diff, symbol.address, |text| {
diff_text_ui(ui, text, ins_diff, appearance, ins_view_state, space_width, &response_cb);
diff_text_ui(
ui,
text,
ins_diff,
appearance,
ins_view_state,
column,
space_width,
&response_cb,
);
Ok::<_, ()>(())
})
.unwrap();
@@ -283,6 +343,7 @@ fn asm_col_ui(
symbol_ref: SymbolRef,
appearance: &Appearance,
ins_view_state: &mut FunctionViewState,
column: ColumnId,
) {
let (section, symbol) = obj.0.section_symbol(symbol_ref);
let section = section.unwrap();
@@ -298,7 +359,7 @@ fn asm_col_ui(
}
};
let (_, response) = row.col(|ui| {
asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state, response_cb);
asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state, column, response_cb);
});
response_cb(response);
}
@@ -337,12 +398,26 @@ fn asm_table_ui(
table.body(|body| {
body.rows(appearance.code_font.size, instructions_len, |mut row| {
if let (Some(left_obj), Some(left_symbol_ref)) = (left_obj, left_symbol) {
asm_col_ui(&mut row, left_obj, left_symbol_ref, appearance, ins_view_state);
asm_col_ui(
&mut row,
left_obj,
left_symbol_ref,
appearance,
ins_view_state,
ColumnId::Left,
);
} else {
empty_col_ui(&mut row);
}
if let (Some(right_obj), Some(right_symbol_ref)) = (right_obj, right_symbol) {
asm_col_ui(&mut row, right_obj, right_symbol_ref, appearance, ins_view_state);
asm_col_ui(
&mut row,
right_obj,
right_symbol_ref,
appearance,
ins_view_state,
ColumnId::Right,
);
} else {
empty_col_ui(&mut row);
}
@@ -364,6 +439,8 @@ pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance
Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min),
|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
// Left column
ui.allocate_ui_with_layout(
Vec2 { x: column_width, y: 100.0 },
@@ -393,18 +470,9 @@ pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance
.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.colored_label(appearance.highlight_color, name);
ui.label("Diff target:");
});
},
@@ -426,7 +494,6 @@ pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance
}
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 {
@@ -442,6 +509,18 @@ pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance
);
}
});
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()
{
state.queue_open_source_path = true;
}
});
ui.scope(|ui| {
@@ -458,7 +537,7 @@ pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance
{
ui.colored_label(
match_color_for_symbol(match_percent, appearance),
&format!("{match_percent:.0}%"),
format!("{match_percent:.0}%"),
);
} else {
ui.colored_label(appearance.replace_color, "Missing");

View File

@@ -1,5 +1,6 @@
use std::{
fs::File,
io::{BufReader, BufWriter},
path::{Path, PathBuf},
};
@@ -46,13 +47,13 @@ pub fn load_graphics_config(path: &Path) -> Result<Option<GraphicsConfig>> {
if !path.exists() {
return Ok(None);
}
let file = File::open(path)?;
let file = BufReader::new(File::open(path)?);
let config: GraphicsConfig = ron::de::from_reader(file)?;
Ok(Some(config))
}
pub fn save_graphics_config(path: &Path, config: &GraphicsConfig) -> Result<()> {
let file = File::create(path)?;
let file = BufWriter::new(File::create(path)?);
ron::ser::to_writer(file, config)?;
Ok(())
}

View File

@@ -1,58 +1,162 @@
use std::cmp::Ordering;
use egui::{ProgressBar, RichText, Widget};
use crate::{jobs::JobQueue, views::appearance::Appearance};
use crate::{
jobs::{JobQueue, JobStatus},
views::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 any_jobs = false;
for job in jobs.iter_mut() {
let Ok(status) = job.context.status.read() else {
continue;
};
ui.group(|ui| {
ui.horizontal(|ui| {
ui.label(&status.title);
if ui.small_button("").clicked() {
if job.handle.is_some() {
job.should_remove = true;
if let Err(e) = job.cancel.send(()) {
log::error!("Failed to cancel job: {e:?}");
}
} else {
remove_job = Some(job.id);
any_jobs = true;
ui.separator();
ui.horizontal(|ui| {
ui.label(&status.title);
if ui.small_button("").clicked() {
if job.handle.is_some() {
if let Err(e) = job.cancel.send(()) {
log::error!("Failed to cancel job: {e:?}");
}
}
});
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 {
format!("{:width$}", &status.status, width = STATUS_LENGTH)
})
.on_hover_text_at_pointer(&status.status);
remove_job = Some(job.id);
}
}
});
let mut bar = ProgressBar::new(status.progress_percent);
if let Some(items) = &status.progress_items {
bar = bar.text(format!("{} / {}", items[0], items[1]));
}
bar.ui(ui);
const STATUS_LENGTH: usize = 80;
if let Some(err) = &status.error {
let err_string = format!("{:#}", err);
ui.colored_label(
appearance.delete_color,
if err_string.len() > STATUS_LENGTH - 10 {
format!("Error: {}", &err_string[0..STATUS_LENGTH - 10])
} else {
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
},
)
.on_hover_text_at_pointer(RichText::new(&err_string).color(appearance.delete_color))
.context_menu(|ui| {
if ui.button("Copy full message").clicked() {
ui.output_mut(|o| o.copied_text = err_string);
}
});
} else {
ui.label(if status.status.len() > STATUS_LENGTH - 3 {
format!("{}", &status.status[0..STATUS_LENGTH - 3])
} else {
format!("{:width$}", &status.status, width = STATUS_LENGTH)
})
.on_hover_text_at_pointer(&status.status)
.context_menu(|ui| {
if ui.button("Copy full message").clicked() {
ui.output_mut(|o| o.copied_text = status.status.clone());
}
});
}
}
if !any_jobs {
ui.label("No jobs");
}
if let Some(idx) = remove_job {
jobs.remove(idx);
}
}
struct JobStatusDisplay {
title: String,
progress_items: Option<[u32; 2]>,
error: bool,
}
impl From<&JobStatus> for JobStatusDisplay {
fn from(status: &JobStatus) -> Self {
Self {
title: status.title.clone(),
progress_items: status.progress_items,
error: status.error.is_some(),
}
}
}
pub fn jobs_menu_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) -> bool {
ui.label("Jobs:");
let mut statuses = Vec::new();
for job in jobs.iter_mut() {
let Ok(status) = job.context.status.read() else {
continue;
};
statuses.push(JobStatusDisplay::from(&*status));
}
let running_jobs = statuses.iter().filter(|s| !s.error).count();
let error_jobs = statuses.iter().filter(|s| s.error).count();
let mut clicked = false;
let spinner =
egui::Spinner::new().size(appearance.ui_font.size * 0.9).color(appearance.text_color);
match running_jobs.cmp(&1) {
Ordering::Equal => {
spinner.ui(ui);
let running_job = statuses.iter().find(|s| !s.error).unwrap();
let text = if let Some(items) = running_job.progress_items {
format!("{} ({}/{})", running_job.title, items[0], items[1])
} else {
running_job.title.clone()
};
clicked |= ui.link(RichText::new(text)).clicked();
}
Ordering::Greater => {
spinner.ui(ui);
clicked |= ui.link(format!("{} running", running_jobs)).clicked();
}
_ => (),
}
match error_jobs.cmp(&1) {
Ordering::Equal => {
let error_job = statuses.iter().find(|s| s.error).unwrap();
clicked |= ui
.link(
RichText::new(format!("{} error", error_job.title))
.color(appearance.delete_color),
)
.clicked();
}
Ordering::Greater => {
clicked |= ui
.link(
RichText::new(format!("{} errors", error_jobs)).color(appearance.delete_color),
)
.clicked();
}
_ => (),
}
if running_jobs == 0 && error_jobs == 0 {
clicked |= ui.link("None").clicked();
}
clicked
}
pub fn jobs_window(
ctx: &egui::Context,
show: &mut bool,
jobs: &mut JobQueue,
appearance: &Appearance,
) {
egui::Window::new("Jobs").open(show).show(ctx, |ui| {
jobs_ui(ui, jobs, appearance);
});
}

View File

@@ -6,13 +6,14 @@ use egui::{
};
use egui_extras::{Size, StripBuilder};
use objdiff_core::{
arch::ObjArch,
diff::{ObjDiff, ObjSymbolDiff},
obj::{ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags, SymbolRef},
};
use regex::{Regex, RegexBuilder};
use crate::{
app::AppConfigRef,
app::AppStateRef,
jobs::{
create_scratch::{start_create_scratch, CreateScratchConfig, CreateScratchResult},
objdiff::{BuildStatus, ObjDiffResult},
@@ -51,6 +52,8 @@ pub struct DiffViewState {
pub scratch_available: bool,
pub queue_scratch: bool,
pub scratch_running: bool,
pub source_path_available: bool,
pub queue_open_source_path: bool,
}
#[derive(Default)]
@@ -60,11 +63,10 @@ pub struct SymbolViewState {
pub reverse_fn_order: bool,
pub disable_reverse_fn_order: bool,
pub show_hidden_symbols: bool,
pub queue_extab_decode: bool,
}
impl DiffViewState {
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| match result {
JobResult::ObjDiff(result) => {
self.build = take(result);
@@ -80,26 +82,29 @@ impl DiffViewState {
self.scratch_running = jobs.is_running(Job::CreateScratch);
self.symbol_state.disable_reverse_fn_order = false;
if let Ok(config) = config.read() {
if let Some(obj_config) = &config.selected_obj {
if let Ok(state) = state.read() {
if let Some(obj_config) = &state.config.selected_obj {
if let Some(value) = obj_config.reverse_fn_order {
self.symbol_state.reverse_fn_order = value;
self.symbol_state.disable_reverse_fn_order = true;
}
self.source_path_available = obj_config.source_path.is_some();
} else {
self.source_path_available = false;
}
self.scratch_available = CreateScratchConfig::is_available(&config);
self.scratch_available = CreateScratchConfig::is_available(&state.config);
}
}
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 let Some(result) = take(&mut self.scratch) {
ctx.output_mut(|o| o.open_url = Some(OpenUrl::new_tab(result.scratch_url)));
}
if self.queue_build {
self.queue_build = false;
if let Ok(mut config) = config.write() {
config.queue_build = true;
if let Ok(mut state) = state.write() {
state.queue_build = true;
}
}
@@ -108,8 +113,8 @@ impl DiffViewState {
if let Some(function_name) =
self.symbol_state.selected_symbol.as_ref().map(|sym| sym.symbol_name.clone())
{
if let Ok(config) = config.read() {
match CreateScratchConfig::from_config(&config, function_name) {
if let Ok(state) = state.read() {
match CreateScratchConfig::from_config(&state.config, function_name) {
Ok(config) => {
jobs.push_once(Job::CreateScratch, || {
start_create_scratch(ctx, config)
@@ -122,6 +127,22 @@ impl DiffViewState {
}
}
}
if self.queue_open_source_path {
self.queue_open_source_path = false;
if let Ok(state) = state.read() {
if let (Some(project_dir), Some(source_path)) = (
&state.config.project_dir,
state.config.selected_obj.as_ref().and_then(|obj| obj.source_path.as_ref()),
) {
let source_path = project_dir.join(source_path);
log::info!("Opening file {}", source_path.display());
open::that_detached(source_path).unwrap_or_else(|err| {
log::error!("Failed to open source file: {err}");
});
}
}
}
}
}
@@ -138,12 +159,14 @@ pub fn match_color_for_symbol(match_percent: f32, appearance: &Appearance) -> Co
fn symbol_context_menu_ui(
ui: &mut Ui,
state: &mut SymbolViewState,
arch: &dyn ObjArch,
symbol: &ObjSymbol,
section: Option<&ObjSection>,
) {
) -> Option<View> {
let mut ret = None;
ui.scope(|ui| {
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 let Some(name) = &symbol.demangled_name {
if ui.button(format!("Copy \"{name}\"")).clicked() {
@@ -162,23 +185,25 @@ fn symbol_context_menu_ui(
}
}
if let Some(section) = section {
if symbol.has_extab && ui.button("Decode exception table").clicked() {
state.queue_extab_decode = true;
let has_extab = arch.ppc().and_then(|ppc| ppc.extab_for_symbol(symbol)).is_some();
if has_extab && ui.button("Decode exception table").clicked() {
state.selected_symbol = Some(SymbolRefByName {
symbol_name: symbol.name.clone(),
demangled_symbol_name: symbol.demangled_name.clone(),
section_name: section.name.clone(),
});
ret = Some(View::ExtabDiff);
ui.close_menu();
}
}
});
ret
}
fn symbol_hover_ui(ui: &mut Ui, symbol: &ObjSymbol, appearance: &Appearance) {
fn symbol_hover_ui(ui: &mut Ui, arch: &dyn ObjArch, symbol: &ObjSymbol, appearance: &Appearance) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
ui.colored_label(appearance.highlight_color, format!("Name: {}", symbol.name));
ui.colored_label(appearance.highlight_color, format!("Address: {:x}", symbol.address));
@@ -193,26 +218,24 @@ fn symbol_hover_ui(ui: &mut Ui, symbol: &ObjSymbol, appearance: &Appearance) {
if let Some(address) = symbol.virtual_address {
ui.colored_label(appearance.replace_color, format!("Virtual address: {:#x}", address));
}
if symbol.has_extab {
if let (Some(extab_name), Some(extabindex_name)) =
(&symbol.extab_name, &symbol.extabindex_name)
{
ui.colored_label(
appearance.highlight_color,
format!("Extab Symbol: {}", extab_name),
);
ui.colored_label(
appearance.highlight_color,
format!("Extabindex Symbol: {}", extabindex_name),
);
}
if let Some(extab) = arch.ppc().and_then(|ppc| ppc.extab_for_symbol(symbol)) {
ui.colored_label(
appearance.highlight_color,
format!("extab symbol: {}", &extab.etb_symbol.name),
);
ui.colored_label(
appearance.highlight_color,
format!("extabindex symbol: {}", &extab.eti_symbol.name),
);
}
});
}
#[must_use]
#[allow(clippy::too_many_arguments)]
fn symbol_ui(
ui: &mut Ui,
arch: &dyn ObjArch,
symbol: &ObjSymbol,
symbol_diff: &ObjSymbolDiff,
section: Option<&ObjSection>,
@@ -245,6 +268,9 @@ fn symbol_ui(
if symbol.flags.0.contains(ObjSymbolFlags::Weak) {
write_text("w", appearance.text_color, &mut job, appearance.code_font.clone());
}
if symbol.flags.0.contains(ObjSymbolFlags::HasExtra) {
write_text("e", appearance.text_color, &mut job, appearance.code_font.clone());
}
if symbol.flags.0.contains(ObjSymbolFlags::Hidden) {
write_text(
"h",
@@ -268,8 +294,10 @@ fn symbol_ui(
write_text(name, appearance.highlight_color, &mut job, appearance.code_font.clone());
let response = SelectableLabel::new(selected, job)
.ui(ui)
.on_hover_ui_at_pointer(|ui| symbol_hover_ui(ui, symbol, appearance));
response.context_menu(|ui| symbol_context_menu_ui(ui, state, symbol, section));
.on_hover_ui_at_pointer(|ui| symbol_hover_ui(ui, arch, symbol, appearance));
response.context_menu(|ui| {
ret = ret.or(symbol_context_menu_ui(ui, state, arch, symbol, section));
});
if response.clicked() {
if let Some(section) = section {
if section.kind == ObjSectionKind::Code {
@@ -299,13 +327,6 @@ fn symbol_ui(
(None, None)
};
}
//If the decode extab context menu option was clicked, switch to the extab view
if state.queue_extab_decode {
ret = Some(View::ExtabDiff);
state.queue_extab_decode = false;
}
ret
}
@@ -328,10 +349,11 @@ fn symbol_list_ui(
left: bool,
) -> Option<View> {
let mut ret = None;
let arch = obj.0.arch.as_ref();
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
if !obj.0.common.is_empty() {
CollapsingHeader::new(".comm").default_open(true).show(ui, |ui| {
@@ -341,6 +363,7 @@ fn symbol_list_ui(
}
ret = ret.or(symbol_ui(
ui,
arch,
symbol,
symbol_diff,
None,
@@ -379,7 +402,7 @@ fn symbol_list_ui(
);
}
CollapsingHeader::new(header)
.id_source(Id::new(section.name.clone()).with(section.orig_index))
.id_salt(Id::new(section.name.clone()).with(section.orig_index))
.default_open(true)
.show(ui, |ui| {
if section.kind == ObjSectionKind::Code && state.reverse_fn_order {
@@ -391,6 +414,7 @@ fn symbol_list_ui(
}
ret = ret.or(symbol_ui(
ui,
arch,
symbol,
symbol_diff,
Some(section),
@@ -408,6 +432,7 @@ fn symbol_list_ui(
}
ret = ret.or(symbol_ui(
ui,
arch,
symbol,
symbol_diff,
Some(section),
@@ -427,7 +452,7 @@ fn symbol_list_ui(
fn build_log_ui(ui: &mut Ui, status: &BuildStatus, appearance: &Appearance) {
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
ui.horizontal(|ui| {
if ui.button("Copy command").clicked() {
if !status.cmdline.is_empty() && ui.button("Copy command").clicked() {
ui.output_mut(|output| output.copied_text.clone_from(&status.cmdline));
}
if ui.button("Copy log").clicked() {
@@ -438,11 +463,17 @@ fn build_log_ui(ui: &mut Ui, status: &BuildStatus, appearance: &Appearance) {
});
ui.scope(|ui| {
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);
ui.label(&status.cmdline);
ui.colored_label(appearance.replace_color, &status.stdout);
ui.colored_label(appearance.delete_color, &status.stderr);
if !status.cmdline.is_empty() {
ui.label(&status.cmdline);
}
if !status.stdout.is_empty() {
ui.colored_label(appearance.replace_color, &status.stdout);
}
if !status.stderr.is_empty() {
ui.colored_label(appearance.delete_color, &status.stderr);
}
});
});
}
@@ -450,7 +481,7 @@ fn build_log_ui(ui: &mut Ui, status: &BuildStatus, appearance: &Appearance) {
fn missing_obj_ui(ui: &mut Ui, appearance: &Appearance) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
ui.colored_label(appearance.replace_color, "No object configured");
});
@@ -469,6 +500,8 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min),
|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
// Left column
ui.allocate_ui_with_layout(
Vec2 { x: column_width, y: 100.0 },
@@ -478,7 +511,6 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.label("Build target:");
if result.first_status.success {
@@ -513,11 +545,27 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
|ui| {
ui.set_width(column_width);
ui.horizontal(|ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.label("Build base:");
});
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()
{
state.queue_open_source_path = true;
}
});
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.label("Build base:");
if result.second_status.success {
if result.second_obj.is_none() {
ui.colored_label(appearance.replace_color, "Missing");

View File

@@ -1,12 +1,12 @@
{
"name": "objdiff-wasm",
"version": "2.0.0-beta.10",
"version": "2.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "objdiff-wasm",
"version": "2.0.0-beta.10",
"version": "2.0.0",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@protobuf-ts/runtime": "^2.9.4"

View File

@@ -1,6 +1,6 @@
{
"name": "objdiff-wasm",
"version": "2.0.0-beta.10",
"version": "2.0.0",
"description": "A local diffing tool for decompilation projects.",
"author": {
"name": "Luke Street",