Compare commits

..

41 Commits

Author SHA1 Message Date
a5d9d8282e Update all dependencies 2024-10-03 22:00:43 -06:00
Amber Brault
3287a0f65c Bump cwextab again to 1.0.2 (#114)
* Bump cwextab

* Updated cwextab to not error on null actions

* Bump cwextab again
2024-10-03 01:12:37 -06:00
Amber Brault
fab9c62dfb Bump cwextab (#113)
* Bump cwextab

* Updated cwextab to not error on null actions
2024-10-01 23:20:09 -06:00
08cd768260 Add total_units, complete_units to progress report 2024-09-30 21:41:57 -06:00
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
e1ae369d17 CI: Fix Cargo.toml version check 2024-09-04 23:51:42 -06:00
ce05d6d6c0 Version v2.0.0-beta.6 2024-09-04 23:36:41 -06:00
c16a926d9b objdiff-cli: Build static binary & for more arches 2024-09-04 23:33:52 -06:00
Robin Lambertz
a32d99923c Coff line number (#100)
* Update object to 0.36

* Add COFF line number support
2024-09-04 18:36:09 -06:00
68606dfdcb Add config.schema.json & update README.md 2024-09-03 20:48:45 -06:00
41 changed files with 3315 additions and 2025 deletions

View File

@@ -11,6 +11,7 @@ on:
env: env:
BUILD_PROFILE: release-lto BUILD_PROFILE: release-lto
CARGO_TARGET_DIR: target CARGO_TARGET_DIR: target
CARGO_INCREMENTAL: 0
jobs: jobs:
check: check:
@@ -29,18 +30,10 @@ jobs:
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
components: clippy components: clippy
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.4
- name: Cargo check - name: Cargo check
env: run: cargo check --all-features --all-targets
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
run: cargo check
- name: Cargo clippy - name: Cargo clippy
env: run: cargo clippy --all-features --all-targets
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
run: cargo clippy
fmt: fmt:
name: Format name: Format
@@ -92,16 +85,85 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Rust toolchain - name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.4
- name: Cargo test - name: Cargo test
env: run: cargo test --release --all-features
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
run: cargo test --release
build: build-cli:
name: Build name: Build objdiff-cli
env:
CARGO_BIN_NAME: objdiff-cli
strategy:
matrix:
include:
- platform: ubuntu-latest
target: x86_64-unknown-linux-musl
name: linux-x86_64
build: zigbuild
features: default
- platform: ubuntu-latest
target: i686-unknown-linux-musl
name: linux-i686
build: zigbuild
features: default
- platform: ubuntu-latest
target: aarch64-unknown-linux-musl
name: linux-aarch64
build: zigbuild
features: default
- platform: windows-latest
target: i686-pc-windows-msvc
name: windows-x86
build: build
features: default
- platform: windows-latest
target: x86_64-pc-windows-msvc
name: windows-x86_64
build: build
features: default
- platform: windows-latest
target: aarch64-pc-windows-msvc
name: windows-arm64
build: build
features: default
- platform: macos-latest
target: x86_64-apple-darwin
name: macos-x86_64
build: build
features: default
- platform: macos-latest
target: aarch64-apple-darwin
name: macos-arm64
build: build
features: default
fail-fast: false
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install cargo-zigbuild
if: matrix.build == 'zigbuild'
run: pip install ziglang==0.13.0 cargo-zigbuild==0.19.1
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Cargo build
run: >
cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
--bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }}
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }}
path: |
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
if-no-files-found: error
build-gui:
name: Build objdiff-gui
env:
CARGO_BIN_NAME: objdiff
strategy: strategy:
matrix: matrix:
include: include:
@@ -136,34 +198,41 @@ jobs:
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.4
- name: Cargo build - name: Cargo build
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
run: > run: >
cargo build --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }} cargo build --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
--bin objdiff-cli --bin objdiff --features ${{ matrix.features }} --bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }}
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: ${{ matrix.name }} name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }}
path: | path: |
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/objdiff-cli ${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/objdiff-cli.exe ${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/objdiff
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/objdiff.exe
if-no-files-found: error if-no-files-found: error
release: release:
name: Release name: Release
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ build ] needs: [ build-cli, build-gui ]
permissions: permissions:
contents: write contents: write
steps: steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check git tag against Cargo version
shell: bash
run: |
set -eou pipefail
tag='${{github.ref}}'
tag="${tag#refs/tags/}"
version=$(grep '^version' Cargo.toml | head -1 | awk -F' = ' '{print $2}' | tr -d '"')
version="v$version"
if [ "$tag" != "$version" ]; then
echo "::error::Git tag doesn't match the Cargo version! ($tag != $version)"
exit 1
fi
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
@@ -183,12 +252,16 @@ jobs:
else else
ext=".$ext" ext=".$ext"
fi fi
dst="../out/${name}-${dir%/}${ext}" arch="${dir%/}" # remove trailing slash
arch="${arch##"$name-"}" # remove bin name
dst="../out/${name}-${arch}${ext}"
mv "$file" "$dst" mv "$file" "$dst"
done done
done done
ls -R ../out ls -R ../out
- name: Release - name: Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v2
with: with:
files: out/* files: out/*
draft: true
generate_release_notes: true

2324
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

105
README.md
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). A local diffing tool for decompilation projects. Inspired by [decomp.me](https://decomp.me) and [asm-differ](https://github.com/simonlindholm/asm-differ).
Features: Features:
- Compare entire object files: functions and data. - Compare entire object files: functions and data.
- Built-in symbol demangling for C++. (CodeWarrior, Itanium & MSVC) - Built-in symbol demangling for C++. (CodeWarrior, Itanium & MSVC)
- Automatic rebuild on source file changes. - Automatic rebuild on source file changes.
@@ -14,6 +15,7 @@ Features:
- Click to highlight all instances of values and registers. - Click to highlight all instances of values and registers.
Supports: Supports:
- PowerPC 750CL (GameCube, Wii) - PowerPC 750CL (GameCube, Wii)
- MIPS (N64, PS1, PS2, PSP) - MIPS (N64, PS1, PS2, PSP)
- x86 (COFF only at the moment) - x86 (COFF only at the moment)
@@ -21,6 +23,25 @@ Supports:
See [Usage](#usage) for more information. See [Usage](#usage) for more information.
## Downloads
To build from source, see [Building](#building).
### GUI
- [Windows (x86_64)](https://github.com/encounter/objdiff/releases/latest/download/objdiff-windows-x86_64.exe)
- [Linux (x86_64)](https://github.com/encounter/objdiff/releases/latest/download/objdiff-linux-x86_64)
- [macOS (arm64)](https://github.com/encounter/objdiff/releases/latest/download/objdiff-macos-arm64)
- [macOS (x86_64)](https://github.com/encounter/objdiff/releases/latest/download/objdiff-macos-x86_64)
For Linux and macOS, run `chmod +x objdiff-*` to make the binary executable.
### CLI
CLI binaries can be found on the [releases page](https://github.com/encounter/objdiff/releases).
## Screenshots
![Symbol Screenshot](assets/screen-symbols.png) ![Symbol Screenshot](assets/screen-symbols.png)
![Diff Screenshot](assets/screen-diff.png) ![Diff Screenshot](assets/screen-diff.png)
@@ -49,91 +70,89 @@ See [Configuration](#configuration) for more information.
## Configuration ## Configuration
While **not required** (most settings can be specified in the UI), projects can add an `objdiff.json` (or While **not required** (most settings can be specified in the UI), projects can add an `objdiff.json` file to configure the tool automatically. The configuration file must be located in
`objdiff.yaml`, `objdiff.yml`) file to configure the tool automatically. The configuration file must be located in
the root project directory. the root project directory.
If your project has a generator script (e.g. `configure.py`), it's recommended to generate the objdiff configuration If your project has a generator script (e.g. `configure.py`), it's recommended to generate the objdiff configuration
file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it from being committed. file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it from being committed.
```json5 ```json
// objdiff.json
{ {
"$schema": "https://raw.githubusercontent.com/encounter/objdiff/main/config.schema.json",
"custom_make": "ninja", "custom_make": "ninja",
"custom_args": [ "custom_args": [
"-d", "-d",
"keeprsp" "keeprsp"
], ],
"build_target": false,
// Only required if objects use "path" instead of "target_path" and "base_path". "build_base": true,
"target_dir": "build/asm",
"base_dir": "build/src",
"build_target": true,
"watch_patterns": [ "watch_patterns": [
"*.c", "*.c",
"*.cp", "*.cp",
"*.cpp", "*.cpp",
"*.cxx",
"*.h", "*.h",
"*.hp",
"*.hpp", "*.hpp",
"*.py" "*.hxx",
"*.s",
"*.S",
"*.asm",
"*.inc",
"*.py",
"*.yml",
"*.txt",
"*.json"
], ],
"objects": [ "units": [
{ {
"name": "main/MetroTRK/mslsupp", "name": "main/MetroTRK/mslsupp",
// Option 1: Relative to target_dir and base_dir
"path": "MetroTRK/mslsupp.o",
// Option 2: Explicit paths from project root
// Useful for more complex directory layouts
"target_path": "build/asm/MetroTRK/mslsupp.o", "target_path": "build/asm/MetroTRK/mslsupp.o",
"base_path": "build/src/MetroTRK/mslsupp.o", "base_path": "build/src/MetroTRK/mslsupp.o",
"metadata": {}
"reverse_fn_order": false }
},
// ...
] ]
} }
``` ```
### Schema
View [config.schema.json](config.schema.json) for all available options. The below list is a summary of the most important options.
`custom_make` _(optional)_: By default, objdiff will use `make` to build the project. `custom_make` _(optional)_: By default, objdiff will use `make` to build the project.
If the project uses a different build system (e.g. `ninja`), specify it here. If the project uses a different build system (e.g. `ninja`), specify it here.
The build command will be `[custom_make] [custom_args] path/to/object.o`. The build command will be `[custom_make] [custom_args] path/to/object.o`.
`custom_args` _(optional)_: Additional arguments to pass to the build command prior to the object path. `custom_args` _(optional)_: Additional arguments to pass to the build command prior to the object path.
`target_dir` _(optional)_: Relative from the root of the project, this where the "target" or "expected" objects are located.
These are the **intended result** of the match.
`base_dir` _(optional)_: Relative from the root of the project, this is where the "base" or "actual" objects are located.
These are objects built from the **current source code**.
`build_target`: If true, objdiff will tell the build system to build the target objects before diffing (e.g. `build_target`: If true, objdiff will tell the build system to build the target objects before diffing (e.g.
`make path/to/target.o`). `make path/to/target.o`).
This is useful if the target objects are not built by default or can change based on project configuration or edits This is useful if the target objects are not built by default or can change based on project configuration or edits
to assembly files. to assembly files.
Requires the build system to be configured properly. Requires the build system to be configured properly.
`build_base`: If true, objdiff will tell the build system to build the base objects before diffing (e.g. `make path/to/base.o`).
It's unlikely you'll want to disable this, unless you're using an external tool to rebuild the base object on source file changes.
`watch_patterns` _(optional)_: A list of glob patterns to watch for changes. `watch_patterns` _(optional)_: A list of glob patterns to watch for changes.
([Supported syntax](https://docs.rs/globset/latest/globset/#syntax)) ([Supported syntax](https://docs.rs/globset/latest/globset/#syntax))
If any of these files change, objdiff will automatically rebuild the objects and re-compare them. If any of these files change, objdiff will automatically rebuild the objects and re-compare them.
If not specified, objdiff will use the default patterns listed above. If not specified, objdiff will use the default patterns listed above.
`objects` _(optional)_: If specified, objdiff will display a list of objects in the sidebar for easy navigation. `units` _(optional)_: If specified, objdiff will display a list of objects in the sidebar for easy navigation.
> `name` _(optional)_: The name of the object in the UI. If not specified, the object's `path` will be used. > `name` _(optional)_: The name of the object in the UI. If not specified, the object's `path` will be used.
> >
> `path`: Relative path to the object from the `target_dir` and `base_dir`. > `target_path`: Path to the "target" or "expected" object from the project root.
> Requires `target_dir` and `base_dir` to be specified. > This object is the **intended result** of the match.
> >
> `target_path`: Path to the target object from the project root. > `base_path`: Path to the "base" or "actual" object from the project root.
> Required if `path` is not specified. > This object is built from the **current source code**.
> >
> `base_path`: Path to the base object from the project root. > `metadata.auto_generated` _(optional)_: Hides the object from the object list, but still includes it in reports.
> Required if `path` is not specified.
> >
> `reverse_fn_order` _(optional)_: Displays function symbols in reversed order. > `metadata.complete` _(optional)_: Marks the object as "complete" (or "linked") in the object list.
Used to support MWCC's `-inline deferred` option, which reverses the order of functions in the object file. > This is useful for marking objects that are fully decompiled. A value of `false` will mark the object as "incomplete".
## Building ## Building
@@ -143,16 +162,22 @@ Install Rust via [rustup](https://rustup.rs).
$ git clone https://github.com/encounter/objdiff.git $ git clone https://github.com/encounter/objdiff.git
$ cd objdiff $ cd objdiff
$ cargo run --release $ cargo run --release
# or, for wgpu backend (recommended on macOS)
$ cargo run --release --features wgpu
``` ```
Or using `cargo install`.
```shell
$ cargo install --locked --git https://github.com/encounter/objdiff.git objdiff-gui objdiff-cli
```
The binaries will be installed to `~/.cargo/bin` as `objdiff` and `objdiff-cli`.
## License ## License
Licensed under either of Licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) - Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>)
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) - MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
at your option. at your option.

221
config.schema.json Normal file
View File

@@ -0,0 +1,221 @@
{
"$schema": "https://json-schema.org/draft-07/schema",
"$id": "https://raw.githubusercontent.com/encounter/objdiff/main/config.schema.json",
"title": "objdiff configuration",
"description": "Configuration file for objdiff",
"type": "object",
"properties": {
"min_version": {
"type": "string",
"description": "Minimum version of objdiff required to load this configuration file.",
"examples": [
"1.0.0",
"2.0.0-beta.1"
]
},
"custom_make": {
"type": "string",
"description": "By default, objdiff will use make to build the project.\nIf the project uses a different build system (e.g. ninja), specify it here.\nThe build command will be `[custom_make] [custom_args] path/to/object.o`.",
"examples": [
"make",
"ninja"
],
"default": "make"
},
"custom_args": {
"type": "array",
"description": "Additional arguments to pass to the build command prior to the object path.",
"items": {
"type": "string"
}
},
"target_dir": {
"type": "string",
"description": "Relative from the root of the project, this where the \"target\" or \"expected\" objects are located.\nThese are the intended result of the match.",
"deprecated": true
},
"base_dir": {
"type": "string",
"description": "Relative from the root of the project, this is where the \"base\" or \"actual\" objects are located.\nThese are objects built from the current source code.",
"deprecated": true
},
"build_target": {
"type": "boolean",
"description": "If true, objdiff will tell the build system to build the target objects before diffing (e.g. `make path/to/target.o`).\nThis is useful if the target objects are not built by default or can change based on project configuration or edits to assembly files.\nRequires the build system to be configured properly.",
"default": false
},
"build_base": {
"type": "boolean",
"description": "If true, objdiff will tell the build system to build the base objects before diffing (e.g. `make path/to/base.o`).\nIt's unlikely you'll want to disable this, unless you're using an external tool to rebuild the base object on source file changes.",
"default": true
},
"watch_patterns": {
"type": "array",
"description": "List of glob patterns to watch for changes in the project.\nIf any of these files change, objdiff will automatically rebuild the objects and re-compare them.\nSupported syntax: https://docs.rs/globset/latest/globset/#syntax",
"items": {
"type": "string"
},
"default": [
"*.c",
"*.cp",
"*.cpp",
"*.cxx",
"*.h",
"*.hp",
"*.hpp",
"*.hxx",
"*.s",
"*.S",
"*.asm",
"*.inc",
"*.py",
"*.yml",
"*.txt",
"*.json"
]
},
"objects": {
"type": "array",
"description": "Use units instead.",
"deprecated": true,
"items": {
"$ref": "#/$defs/unit"
}
},
"units": {
"type": "array",
"description": "If specified, objdiff will display a list of objects in the sidebar for easy navigation.",
"items": {
"$ref": "#/$defs/unit"
}
},
"progress_categories": {
"type": "array",
"description": "Progress categories used for objdiff-cli report.",
"items": {
"$ref": "#/$defs/progress_category"
}
}
},
"$defs": {
"unit": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the object in the UI. If not specified, the object's path will be used."
},
"path": {
"type": "string",
"description": "Relative path to the object from the target_dir and base_dir.\nRequires target_dir and base_dir to be specified.",
"deprecated": true
},
"target_path": {
"type": "string",
"description": "Path to the target object from the project root.\nRequired if path is not specified."
},
"base_path": {
"type": "string",
"description": "Path to the base object from the project root.\nRequired if path is not specified."
},
"reverse_fn_order": {
"type": "boolean",
"description": "Displays function symbols in reversed order.\nUsed to support MWCC's -inline deferred option, which reverses the order of functions in the object file.",
"deprecated": true
},
"complete": {
"type": "boolean",
"description": "Marks the object as \"complete\" (or \"linked\") in the object list.\nThis is useful for marking objects that are fully decompiled. A value of `false` will mark the object as \"incomplete\".",
"deprecated": true
},
"scratch": {
"ref": "#/$defs/scratch"
},
"metadata": {
"ref": "#/$defs/metadata"
}
}
},
"scratch": {
"type": "object",
"description": "If present, objdiff will display a button to create a decomp.me scratch.",
"properties": {
"platform": {
"type": "string",
"description": "The decomp.me platform ID to use for the scratch.",
"examples": [
"gc_wii",
"n64"
]
},
"compiler": {
"type": "string",
"description": "The decomp.me compiler ID to use for the scratch.",
"examples": [
"mwcc_242_81",
"ido7.1"
]
},
"c_flags": {
"type": "string",
"description": "C flags to use for the scratch. Exclude any include paths."
},
"ctx_path": {
"type": "string",
"description": "Path to the context file to use for the scratch."
},
"build_ctx": {
"type": "boolean",
"description": "If true, objdiff will run the build command with the context file as an argument to generate it.",
"default": false
}
},
"required": [
"platform",
"compiler"
]
},
"metadata": {
"type": "object",
"properties": {
"complete": {
"type": "boolean",
"description": "Marks the object as \"complete\" (or \"linked\") in the object list.\nThis is useful for marking objects that are fully decompiled. A value of `false` will mark the object as \"incomplete\"."
},
"reverse_fn_order": {
"type": "boolean",
"description": "Displays function symbols in reversed order.\nUsed to support MWCC's -inline deferred option, which reverses the order of functions in the object file."
},
"source_path": {
"type": "string",
"description": "Path to the source file that generated the object."
},
"progress_categories": {
"type": "array",
"description": "Progress categories used for objdiff-cli report.",
"items": {
"type": "string",
"description": "Unique identifier for the category. (See progress_categories)"
}
},
"auto_generated": {
"type": "boolean",
"description": "Hides the object from the object list by default, but still includes it in reports."
}
}
},
"progress_category": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the category."
},
"name": {
"type": "string",
"description": "Human-readable name of the category."
}
}
}
}
}

169
deny.toml
View File

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

View File

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

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) => { Ok(v) => {
if v.version { if v.version {
println!( println!(
"{} {} {}", "{} {}",
command_name.first().unwrap_or(&""), command_name.first().unwrap_or(&""),
env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_VERSION"),
env!("GIT_COMMIT_SHA"),
); );
std::process::exit(0); std::process::exit(0);
} else { } else {

View File

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

View File

@@ -87,9 +87,10 @@ fn generate(args: GenerateArgs) -> Result<()> {
let project_dir = args.project.as_deref().unwrap_or_else(|| Path::new(".")); let project_dir = args.project.as_deref().unwrap_or_else(|| Path::new("."));
info!("Loading project {}", project_dir.display()); info!("Loading project {}", project_dir.display());
let config = objdiff_core::config::try_project_config(project_dir); let mut project = match objdiff_core::config::try_project_config(project_dir) {
let Some((Ok(mut project), _)) = config else { Some((Ok(config), _)) => config,
bail!("No project configuration found"); Some((Err(err), _)) => bail!("Failed to load project configuration: {}", err),
None => bail!("No project configuration found"),
}; };
info!( info!(
"Generating report for {} units (using {} threads)", "Generating report for {} units (using {} threads)",
@@ -198,7 +199,7 @@ fn report_object(
.unwrap_or_default(), .unwrap_or_default(),
auto_generated: object.metadata.as_ref().and_then(|m| m.auto_generated), auto_generated: object.metadata.as_ref().and_then(|m| m.auto_generated),
}; };
let mut measures = Measures::default(); let mut measures = Measures { total_units: 1, ..Default::default() };
let mut sections = vec![]; let mut sections = vec![];
let mut functions = vec![]; let mut functions = vec![];
@@ -236,7 +237,7 @@ fn report_object(
} }
for (symbol, symbol_diff) in section.symbols.iter().zip(&section_diff.symbols) { 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; continue;
} }
if let Some(existing_functions) = &mut existing_functions { if let Some(existing_functions) = &mut existing_functions {
@@ -279,6 +280,7 @@ fn report_object(
if metadata.complete.unwrap_or(false) { if metadata.complete.unwrap_or(false) {
measures.complete_code = measures.total_code; measures.complete_code = measures.total_code;
measures.complete_data = measures.total_data; measures.complete_data = measures.total_data;
measures.complete_units = 1;
} }
measures.calc_fuzzy_match_percent(); measures.calc_fuzzy_match_percent();
measures.calc_matched_percent(); measures.calc_matched_percent();

View File

@@ -2,6 +2,12 @@ mod argp_version;
mod cmd; mod cmd;
mod util; 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 std::{env, ffi::OsStr, fmt::Display, path::PathBuf, str::FromStr};
use anyhow::{Error, Result}; use anyhow::{Error, Result};

View File

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

@@ -32,6 +32,10 @@ message Measures {
uint64 complete_data = 13; uint64 complete_data = 13;
// Completed (or "linked") data percent // Completed (or "linked") data percent
float complete_data_percent = 14; float complete_data_percent = 14;
// Total number of units
uint32 total_units = 15;
// Completed (or "linked") units
uint32 complete_units = 16;
} }
// Project progress report // Project progress report

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 anyhow::{bail, Result};
use byteorder::ByteOrder;
use object::{Architecture, File, Object, ObjectSymbol, Relocation, RelocationFlags, Symbol}; use object::{Architecture, File, Object, ObjectSymbol, Relocation, RelocationFlags, Symbol};
use crate::{ use crate::{
diff::DiffObjConfig, diff::DiffObjConfig,
obj::{ObjIns, ObjReloc, ObjSection}, obj::{ObjIns, ObjReloc, ObjSection},
util::ReallySigned,
}; };
#[cfg(feature = "arm")] #[cfg(feature = "arm")]
@@ -17,6 +19,97 @@ pub mod ppc;
#[cfg(feature = "x86")] #[cfg(feature = "x86")]
pub mod x86; pub mod x86;
/// Represents the type of data associated with an instruction
pub enum DataType {
Int8,
Int16,
Int32,
Int64,
Int128,
Float,
Double,
Bytes,
String,
}
impl DataType {
pub fn display_bytes<Endian: ByteOrder>(&self, bytes: &[u8]) -> Option<String> {
if self.required_len().is_some_and(|l| bytes.len() < l) {
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 { pub trait ObjArch: Send + Sync {
fn process_code( fn process_code(
&self, &self,
@@ -41,6 +134,16 @@ pub trait ObjArch: Send + Sync {
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str>; fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str>;
fn symbol_address(&self, symbol: &Symbol) -> u64 { symbol.address() } fn symbol_address(&self, symbol: &Symbol) -> u64 { symbol.address() }
fn guess_data_type(&self, _instruction: &ObjIns) -> Option<DataType> { None }
fn display_data_type(&self, _ty: DataType, bytes: &[u8]) -> Option<String> {
Some(format!("Bytes: {:#x?}", bytes))
}
// Downcast methods
#[cfg(feature = "ppc")]
fn ppc(&self) -> Option<&ppc::ObjArchPpc> { None }
} }
pub struct ProcessCodeResult { pub struct ProcessCodeResult {
@@ -48,7 +151,7 @@ pub struct ProcessCodeResult {
pub insts: Vec<ObjIns>, pub insts: Vec<ObjIns>,
} }
pub fn new_arch(object: &object::File) -> Result<Box<dyn ObjArch>> { pub fn new_arch(object: &File) -> Result<Box<dyn ObjArch>> {
Ok(match object.architecture() { Ok(match object.architecture() {
#[cfg(feature = "ppc")] #[cfg(feature = "ppc")]
Architecture::PowerPc => Box::new(ppc::ObjArchPpc::new(object)?), Architecture::PowerPc => Box::new(ppc::ObjArchPpc::new(object)?),

View File

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

@@ -8,9 +8,10 @@ use serde_json::error::Category;
include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs")); include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs"));
include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs")); include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs"));
pub const REPORT_VERSION: u32 = 1; pub const REPORT_VERSION: u32 = 2;
impl Report { impl Report {
/// Attempts to parse the report as binary protobuf or JSON.
pub fn parse(data: &[u8]) -> Result<Self> { pub fn parse(data: &[u8]) -> Result<Self> {
if data.is_empty() { if data.is_empty() {
bail!(std::io::Error::from(std::io::ErrorKind::UnexpectedEof)); bail!(std::io::Error::from(std::io::ErrorKind::UnexpectedEof));
@@ -25,6 +26,7 @@ impl Report {
Ok(report) Ok(report)
} }
/// Attempts to parse the report as JSON, migrating from the legacy report format if necessary.
fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> { fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
match serde_json::from_slice::<Self>(bytes) { match serde_json::from_slice::<Self>(bytes) {
Ok(report) => Ok(report), Ok(report) => Ok(report),
@@ -43,16 +45,23 @@ impl Report {
} }
} }
/// Migrates the report to the latest version.
/// Fails if the report version is newer than supported.
pub fn migrate(&mut self) -> Result<()> { pub fn migrate(&mut self) -> Result<()> {
if self.version == 0 { if self.version == 0 {
self.migrate_v0()?; self.migrate_v0()?;
} }
if self.version == 1 {
self.migrate_v1()?;
}
if self.version != REPORT_VERSION { if self.version != REPORT_VERSION {
bail!("Unsupported report version: {}", self.version); bail!("Unsupported report version: {}", self.version);
} }
Ok(()) Ok(())
} }
/// Adds `complete_code`, `complete_data`, `complete_code_percent`, and `complete_data_percent`
/// to measures, and sets `progress_categories` in unit metadata.
fn migrate_v0(&mut self) -> Result<()> { fn migrate_v0(&mut self) -> Result<()> {
let Some(measures) = &mut self.measures else { let Some(measures) = &mut self.measures else {
bail!("Missing measures in report"); bail!("Missing measures in report");
@@ -61,15 +70,16 @@ impl Report {
let Some(unit_measures) = &mut unit.measures else { let Some(unit_measures) = &mut unit.measures else {
bail!("Missing measures in report unit"); bail!("Missing measures in report unit");
}; };
let Some(metadata) = &mut unit.metadata else { let mut complete = false;
bail!("Missing metadata in report unit"); if let Some(metadata) = &mut unit.metadata {
};
if metadata.module_name.is_some() || metadata.module_id.is_some() { if metadata.module_name.is_some() || metadata.module_id.is_some() {
metadata.progress_categories = vec!["modules".to_string()]; metadata.progress_categories = vec!["modules".to_string()];
} else { } else {
metadata.progress_categories = vec!["dol".to_string()]; metadata.progress_categories = vec!["dol".to_string()];
} }
if metadata.complete.unwrap_or(false) { complete = metadata.complete.unwrap_or(false);
};
if complete {
unit_measures.complete_code = unit_measures.total_code; unit_measures.complete_code = unit_measures.total_code;
unit_measures.complete_data = unit_measures.total_data; unit_measures.complete_data = unit_measures.total_data;
unit_measures.complete_code_percent = 100.0; unit_measures.complete_code_percent = 100.0;
@@ -84,10 +94,42 @@ impl Report {
measures.complete_data += unit_measures.complete_data; measures.complete_data += unit_measures.complete_data;
} }
measures.calc_matched_percent(); measures.calc_matched_percent();
self.calculate_progress_categories();
self.version = 1; self.version = 1;
Ok(()) Ok(())
} }
/// Adds `total_units` and `complete_units` to measures.
fn migrate_v1(&mut self) -> Result<()> {
let Some(total_measures) = &mut self.measures else {
bail!("Missing measures in report");
};
for unit in &mut self.units {
let Some(measures) = &mut unit.measures else {
bail!("Missing measures in report unit");
};
let complete = unit.metadata.as_ref().and_then(|m| m.complete).unwrap_or(false) as u32;
let progress_categories =
unit.metadata.as_ref().map(|m| m.progress_categories.as_slice()).unwrap_or(&[]);
measures.total_units = 1;
measures.complete_units = complete;
total_measures.total_units += 1;
total_measures.complete_units += complete;
for id in progress_categories {
if let Some(category) = self.categories.iter_mut().find(|c| &c.id == id) {
let Some(measures) = &mut category.measures else {
bail!("Missing measures in category");
};
measures.total_units += 1;
measures.complete_units += complete;
}
}
}
self.version = 2;
Ok(())
}
/// Calculate progress categories based on unit metadata.
pub fn calculate_progress_categories(&mut self) { pub fn calculate_progress_categories(&mut self) {
for unit in &self.units { for unit in &self.units {
let Some(metadata) = unit.metadata.as_ref() else { let Some(metadata) = unit.metadata.as_ref() else {
@@ -117,6 +159,72 @@ impl Report {
measures.calc_matched_percent(); measures.calc_matched_percent();
} }
} }
/// Split the report into multiple reports based on progress categories.
/// Assumes progress categories are in the format `version`, `version.category`.
/// This is a hack for projects that generate all versions in a single report.
pub fn split(self) -> Vec<(String, Report)> {
let mut reports = Vec::new();
// Map units to Option to allow taking ownership
let mut units = self.units.into_iter().map(Some).collect::<Vec<_>>();
for category in &self.categories {
if category.id.contains(".") {
// Skip subcategories
continue;
}
fn is_sub_category(id: &str, parent: &str, sep: char) -> bool {
id.starts_with(parent)
&& id.get(parent.len()..).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 { impl Measures {
@@ -176,6 +284,8 @@ impl AddAssign for Measures {
self.matched_functions += other.matched_functions; self.matched_functions += other.matched_functions;
self.complete_code += other.complete_code; self.complete_code += other.complete_code;
self.complete_data += other.complete_data; self.complete_data += other.complete_data;
self.total_units += other.total_units;
self.complete_units += other.complete_units;
} }
} }

View File

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

View File

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

View File

@@ -1,12 +1,20 @@
use std::{collections::HashSet, fs, io::Cursor, path::Path}; use std::{
collections::{HashMap, HashSet},
fs,
io::Cursor,
mem::size_of,
path::Path,
};
use anyhow::{anyhow, bail, ensure, Context, Result}; use anyhow::{anyhow, bail, ensure, Context, Result};
use cwextab::decode_extab;
use filetime::FileTime; use filetime::FileTime;
use flagset::Flags; use flagset::Flags;
use object::{ use object::{
Architecture, BinaryFormat, File, Object, ObjectSection, ObjectSymbol, RelocationTarget, endian::LittleEndian as LE,
SectionIndex, SectionKind, Symbol, SymbolKind, SymbolScope, SymbolSection, pe::{ImageAuxSymbolFunctionBeginEnd, ImageLinenumber},
read::coff::{CoffFile, CoffHeader, ImageSymbol},
BinaryFormat, File, Object, ObjectSection, ObjectSymbol, RelocationTarget, SectionIndex,
SectionKind, Symbol, SymbolIndex, SymbolKind, SymbolScope, SymbolSection,
}; };
use crate::{ use crate::{
@@ -14,8 +22,7 @@ use crate::{
diff::DiffObjConfig, diff::DiffObjConfig,
obj::{ obj::{
split_meta::{SplitMeta, SPLITMETA_SECTION}, split_meta::{SplitMeta, SPLITMETA_SECTION},
ObjExtab, ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags,
ObjSymbolFlags,
}, },
util::{read_u16, read_u32}, util::{read_u16, read_u32},
}; };
@@ -57,6 +64,13 @@ fn to_obj_symbol(
if obj_file.format() == BinaryFormat::Elf && symbol.scope() == SymbolScope::Linkage { if obj_file.format() == BinaryFormat::Elf && symbol.scope() == SymbolScope::Linkage {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Hidden); flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Hidden);
} }
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 address = arch.symbol_address(symbol);
let section_address = if let Some(section) = let section_address = if let Some(section) =
symbol.section_index().and_then(|idx| obj_file.section_by_index(idx).ok()) symbol.section_index().and_then(|idx| obj_file.section_by_index(idx).ok())
@@ -70,12 +84,19 @@ fn to_obj_symbol(
let virtual_address = split_meta let virtual_address = split_meta
.and_then(|m| m.virtual_addresses.as_ref()) .and_then(|m| m.virtual_addresses.as_ref())
.and_then(|v| v.get(symbol.index().0).cloned()); .and_then(|v| v.get(symbol.index().0).cloned());
let bytes = symbol
.section_index()
.and_then(|idx| obj_file.section_by_index(idx).ok())
.and_then(|section| section.data().ok())
.and_then(|data| {
data.get(section_address as usize..(section_address + symbol.size()) as usize)
})
.unwrap_or(&[]);
Ok(ObjSymbol { Ok(ObjSymbol {
name: name.to_string(), name: name.to_string(),
demangled_name, demangled_name,
has_extab: false,
extab_name: None,
extabindex_name: None,
address, address,
section_address, section_address,
size: symbol.size(), size: symbol.size(),
@@ -83,6 +104,8 @@ fn to_obj_symbol(
flags, flags,
addend, addend,
virtual_address, virtual_address,
original_index: Some(symbol.index().0),
bytes: bytes.to_vec(),
}) })
} }
@@ -130,6 +153,7 @@ fn symbols_by_section(
obj_file: &File<'_>, obj_file: &File<'_>,
section: &ObjSection, section: &ObjSection,
split_meta: Option<&SplitMeta>, split_meta: Option<&SplitMeta>,
name_counts: &mut HashMap<String, u32>,
) -> Result<Vec<ObjSymbol>> { ) -> Result<Vec<ObjSymbol>> {
let mut result = Vec::<ObjSymbol>::new(); let mut result = Vec::<ObjSymbol>::new();
for symbol in obj_file.symbols() { for symbol in obj_file.symbols() {
@@ -162,12 +186,15 @@ fn symbols_by_section(
} }
if result.is_empty() { if result.is_empty() {
// Dummy symbol for empty sections // Dummy symbol for empty sections
*name_counts.entry(section.name.clone()).or_insert(0) += 1;
let current_count: u32 = *name_counts.get(&section.name).unwrap();
result.push(ObjSymbol { result.push(ObjSymbol {
name: format!("[{}]", section.name), name: if current_count > 1 {
format!("[{} ({})]", section.name, current_count)
} else {
format!("[{}]", section.name)
},
demangled_name: None, demangled_name: None,
has_extab: false,
extab_name: None,
extabindex_name: None,
address: 0, address: 0,
section_address: 0, section_address: 0,
size: section.size, size: section.size,
@@ -175,6 +202,8 @@ fn symbols_by_section(
flags: Default::default(), flags: Default::default(),
addend: 0, addend: 0,
virtual_address: None, virtual_address: None,
original_index: None,
bytes: Vec::new(),
}); });
} }
Ok(result) Ok(result)
@@ -192,111 +221,6 @@ fn common_symbols(
.collect::<Result<Vec<ObjSymbol>>>() .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( fn find_section_symbol(
arch: &dyn ObjArch, arch: &dyn ObjArch,
obj_file: &File<'_>, obj_file: &File<'_>,
@@ -332,9 +256,6 @@ fn find_section_symbol(
Ok(ObjSymbol { Ok(ObjSymbol {
name: name.to_string(), name: name.to_string(),
demangled_name: None, demangled_name: None,
has_extab: false,
extab_name: None,
extabindex_name: None,
address: offset, address: offset,
section_address: address - section.address(), section_address: address - section.address(),
size: 0, size: 0,
@@ -342,6 +263,8 @@ fn find_section_symbol(
flags: Default::default(), flags: Default::default(),
addend: offset_addr as i64, addend: offset_addr as i64,
virtual_address: None, virtual_address: None,
original_index: None,
bytes: Vec::new(),
}) })
} }
@@ -401,7 +324,7 @@ fn relocations_by_section(
Ok(relocations) Ok(relocations)
} }
fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection]) -> Result<()> { fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection], obj_data: &[u8]) -> Result<()> {
// DWARF 1.1 // DWARF 1.1
if let Some(section) = obj_file.section_by_name(".line") { if let Some(section) = obj_file.section_by_name(".line") {
let data = section.uncompressed_data()?; let data = section.uncompressed_data()?;
@@ -490,6 +413,121 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection]) -> Result<()> {
} }
} }
// COFF
if let File::Coff(coff) = obj_file {
line_info_coff(coff, sections, obj_data)?;
}
Ok(())
}
fn line_info_coff(coff: &CoffFile, sections: &mut [ObjSection], obj_data: &[u8]) -> Result<()> {
let symbol_table = coff.coff_header().symbols(obj_data)?;
// Enumerate over all sections.
for sect in coff.sections() {
let ptr_linenums = sect.coff_section().pointer_to_linenumbers.get(LE) as usize;
let num_linenums = sect.coff_section().number_of_linenumbers.get(LE) as usize;
// If we have no line number, skip this section.
if num_linenums == 0 {
continue;
}
// Find this section in our out_section. If it's not in out_section,
// skip it.
let Some(out_section) = sections.iter_mut().find(|s| s.orig_index == sect.index().0) else {
continue;
};
// Turn the line numbers into an ImageLinenumber slice.
let Some(linenums) =
&obj_data.get(ptr_linenums..ptr_linenums + num_linenums * size_of::<ImageLinenumber>())
else {
continue;
};
let Ok(linenums) = object::pod::slice_from_all_bytes::<ImageLinenumber>(linenums) else {
continue;
};
// In COFF, the line numbers are stored relative to the start of the
// function. Because of this, we need to know the line number where the
// function starts, so we can sum the two and get the line number
// relative to the start of the file.
//
// This variable stores the line number where the function currently
// being processed starts. It is set to None when we failed to find the
// line number of the start of the function.
let mut cur_fun_start_linenumber = None;
for linenum in linenums {
let line_number = linenum.linenumber.get(LE);
if line_number == 0 {
// Starting a new function. We need to find the line where that
// function is located in the file. To do this, we need to find
// the `.bf` symbol "associated" with this function. The .bf
// symbol will have a Function Begin/End Auxillary Record, which
// contains the line number of the start of the function.
// First, set cur_fun_start_linenumber to None. If we fail to
// find the start of the function, this will make sure the
// subsequent line numbers will be ignored until the next start
// of function.
cur_fun_start_linenumber = None;
// Get the symbol associated with this function. We'll need it
// for logging purposes, but also to acquire its Function
// Auxillary Record, which tells us where to find our .bf symbol.
let symtable_entry = linenum.symbol_table_index_or_virtual_address.get(LE);
let Ok(symbol) = symbol_table.symbol(SymbolIndex(symtable_entry as usize)) else {
continue;
};
let Ok(aux_fun) = symbol_table.aux_function(SymbolIndex(symtable_entry as usize))
else {
continue;
};
// Get the .bf symbol associated with this symbol. To do so, we
// look at the Function Auxillary Record's tag_index, which is
// an index in the symbol table pointing to our .bf symbol.
if aux_fun.tag_index.get(LE) == 0 {
continue;
}
let Ok(bf_symbol) =
symbol_table.symbol(SymbolIndex(aux_fun.tag_index.get(LE) as usize))
else {
continue;
};
// Do some sanity checks that we are, indeed, looking at a .bf
// symbol.
if bf_symbol.name(symbol_table.strings()) != Ok(b".bf") {
continue;
}
// Get the Function Begin/End Auxillary Record associated with
// our .bf symbol, where we'll fine the linenumber of the start
// of our function.
let Ok(bf_aux) = symbol_table.get::<ImageAuxSymbolFunctionBeginEnd>(
SymbolIndex(aux_fun.tag_index.get(LE) as usize),
1,
) else {
continue;
};
// Set cur_fun_start_linenumber so the following linenumber
// records will know at what line the current function start.
cur_fun_start_linenumber = Some(bf_aux.linenumber.get(LE) as u32);
// Let's also synthesize a line number record from the start of
// the function, as the linenumber records don't always cover it.
out_section.line_info.insert(
sect.address() + symbol.value() as u64,
bf_aux.linenumber.get(LE) as u32,
);
} else if let Some(cur_linenumber) = cur_fun_start_linenumber {
let vaddr = linenum.symbol_table_index_or_virtual_address.get(LE);
out_section
.line_info
.insert(sect.address() + vaddr as u64, cur_linenumber + line_number as u32);
}
}
}
Ok(()) Ok(())
} }
@@ -497,9 +535,6 @@ fn update_combined_symbol(symbol: ObjSymbol, address_change: i64) -> Result<ObjS
Ok(ObjSymbol { Ok(ObjSymbol {
name: symbol.name, name: symbol.name,
demangled_name: symbol.demangled_name, demangled_name: symbol.demangled_name,
has_extab: symbol.has_extab,
extab_name: symbol.extab_name,
extabindex_name: symbol.extabindex_name,
address: (symbol.address as i64 + address_change).try_into()?, address: (symbol.address as i64 + address_change).try_into()?,
section_address: (symbol.section_address as i64 + address_change).try_into()?, section_address: (symbol.section_address as i64 + address_change).try_into()?,
size: symbol.size, size: symbol.size,
@@ -511,6 +546,8 @@ fn update_combined_symbol(symbol: ObjSymbol, address_change: i64) -> Result<ObjS
} else { } else {
None None
}, },
original_index: symbol.original_index,
bytes: symbol.bytes,
}) })
} }
@@ -611,19 +648,24 @@ pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result<ObjInfo> {
let arch = new_arch(&obj_file)?; let arch = new_arch(&obj_file)?;
let split_meta = split_meta(&obj_file)?; let split_meta = split_meta(&obj_file)?;
let mut sections = filter_sections(&obj_file, split_meta.as_ref())?; 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 { for section in &mut sections {
section.symbols = section.symbols = symbols_by_section(
symbols_by_section(arch.as_ref(), &obj_file, section, split_meta.as_ref())?; arch.as_ref(),
&obj_file,
section,
split_meta.as_ref(),
&mut name_counts,
)?;
section.relocations = section.relocations =
relocations_by_section(arch.as_ref(), &obj_file, section, split_meta.as_ref())?; relocations_by_section(arch.as_ref(), &obj_file, section, split_meta.as_ref())?;
} }
if config.combine_data_sections { if config.combine_data_sections {
combine_data_sections(&mut sections)?; combine_data_sections(&mut sections)?;
} }
line_info(&obj_file, &mut sections)?; line_info(&obj_file, &mut sections, data)?;
let common = common_symbols(arch.as_ref(), &obj_file, split_meta.as_ref())?; let common = common_symbols(arch.as_ref(), &obj_file, split_meta.as_ref())?;
let extab = exception_tables(&mut sections, &obj_file)?; Ok(ObjInfo { arch, path: None, timestamp: None, sections, common, split_meta })
Ok(ObjInfo { arch, path: None, timestamp: None, sections, common, extab, split_meta })
} }
pub fn has_function(obj_path: &Path, symbol_name: &str) -> Result<bool> { pub fn has_function(obj_path: &Path, symbol_name: &str) -> Result<bool> {

View File

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

View File

@@ -1,10 +1,12 @@
use anyhow::Result; use anyhow::Result;
use vergen::EmitBuilder;
fn main() -> Result<()> { fn main() -> Result<()> {
#[cfg(windows)] #[cfg(windows)]
{ {
winres::WindowsResource::new().set_icon("assets/icon.ico").compile()?; let mut res = tauri_winres::WindowsResource::new();
res.set_icon("assets/icon.ico");
res.set_language(0x0409); // US English
res.compile()?;
} }
EmitBuilder::builder().fail_on_error().all_build().all_cargo().all_git().emit() Ok(())
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,10 @@
use std::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
process::Command, process::Command,
str::from_utf8,
sync::mpsc::Receiver, sync::mpsc::Receiver,
}; };
use anyhow::{anyhow, Context, Error, Result}; use anyhow::{anyhow, Error, Result};
use objdiff_core::{ use objdiff_core::{
diff::{diff_objs, DiffObjConfig, ObjDiff}, diff::{diff_objs, DiffObjConfig, ObjDiff},
obj::{read, ObjInfo}, obj::{read, ObjInfo},
@@ -91,13 +90,6 @@ pub(crate) fn run_make(config: &BuildConfig, arg: &Path) -> BuildStatus {
..Default::default() ..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 = config.custom_make.as_deref().unwrap_or("make");
let make_args = config.custom_args.as_deref().unwrap_or(&[]); let make_args = config.custom_args.as_deref().unwrap_or(&[]);
#[cfg(not(windows))] #[cfg(not(windows))]
@@ -144,15 +136,23 @@ fn run_make_cmd(config: &BuildConfig, cwd: &Path, arg: &Path) -> Result<BuildSta
cmdline.push(' '); cmdline.push(' ');
cmdline.push_str(shell_escape::escape(arg.to_string_lossy()).as_ref()); 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 output = match command.output() {
let stdout = from_utf8(&output.stdout).context("Failed to process stdout")?; Ok(output) => output,
let stderr = from_utf8(&output.stderr).context("Failed to process stderr")?; Err(e) => {
Ok(BuildStatus { return BuildStatus {
success: output.status.code().unwrap_or(-1) == 0, success: false,
cmdline, cmdline,
stdout: stdout.to_string(), stdout: Default::default(),
stderr: stderr.to_string(), 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( fn run_build(
@@ -189,36 +189,46 @@ fn run_build(
None None
}; };
let mut total = 3; let mut total = 1;
if config.build_target && target_path_rel.is_some() { if config.build_target && target_path_rel.is_some() {
total += 1; total += 1;
} }
if config.build_base && base_path_rel.is_some() { if config.build_base && base_path_rel.is_some() {
total += 1; 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 => { Some(target_path_rel) if config.build_target => {
update_status( update_status(
context, context,
format!("Building target {}", target_path_rel.display()), format!("Building target {}", target_path_rel.display()),
0, step_idx,
total, total,
&cancel, &cancel,
)?; )?;
step_idx += 1;
run_make(&config.build_config, target_path_rel) run_make(&config.build_config, target_path_rel)
} }
_ => BuildStatus::default(), _ => 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 => { Some(base_path_rel) if config.build_base => {
update_status( update_status(
context, context,
format!("Building base {}", base_path_rel.display()), format!("Building base {}", base_path_rel.display()),
0, step_idx,
total, total,
&cancel, &cancel,
)?; )?;
step_idx += 1;
run_make(&config.build_config, base_path_rel) run_make(&config.build_config, base_path_rel)
} }
_ => BuildStatus::default(), _ => BuildStatus::default(),
@@ -226,19 +236,32 @@ fn run_build(
let time = OffsetDateTime::now_utc(); let time = OffsetDateTime::now_utc();
let first_obj = let first_obj = match &obj_config.target_path {
match &obj_config.target_path {
Some(target_path) if first_status.success => { Some(target_path) if first_status.success => {
update_status( update_status(
context, context,
format!("Loading target {}", target_path_rel.unwrap().display()), format!("Loading target {}", target_path_rel.unwrap().display()),
2, step_idx,
total, total,
&cancel, &cancel,
)?; )?;
Some(read::read(target_path, &config.diff_obj_config).with_context(|| { step_idx += 1;
format!("Failed to read object '{}'", target_path.display()) match read::read(target_path, &config.diff_obj_config) {
})?) Ok(obj) => Some(obj),
Err(e) => {
first_status = BuildStatus {
success: false,
stdout: format!("Loading object '{}'", target_path.display()),
stderr: format!("{:#}", e),
..Default::default()
};
None
}
}
}
Some(_) => {
step_idx += 1;
None
} }
_ => None, _ => None,
}; };
@@ -248,22 +271,36 @@ fn run_build(
update_status( update_status(
context, context,
format!("Loading base {}", base_path_rel.unwrap().display()), format!("Loading base {}", base_path_rel.unwrap().display()),
3, step_idx,
total, total,
&cancel, &cancel,
)?; )?;
Some( step_idx += 1;
read::read(base_path, &config.diff_obj_config) match read::read(base_path, &config.diff_obj_config) {
.with_context(|| format!("Failed to read object '{}'", base_path.display()))?, 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, _ => 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)?; 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 { Ok(Box::new(ObjDiffResult {
first_status, first_status,
second_status, second_status,
@@ -274,7 +311,7 @@ fn run_build(
} }
pub fn start_build(ctx: &egui::Context, config: ObjDiffConfig) -> JobState { 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))) 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)?; let tmp_file = File::create(&tmp_path)?;
self_update::Download::from_url(&asset.download_url) self_update::Download::from_url(&asset.download_url)
.set_header(reqwest::header::ACCEPT, "application/octet-stream".parse()?) .set_header(reqwest::header::ACCEPT, "application/octet-stream".parse()?)
.download_to(&tmp_file)?; .download_to(tmp_file)?;
update_status(status, "Extracting release".to_string(), 2, 3, &cancel)?; update_status(status, "Extracting release".to_string(), 2, 3, &cancel)?;
let tmp_file = tmp_dir.path().join("replacement_tmp"); let tmp_file = tmp_dir.path().join("replacement_tmp");
@@ -51,6 +51,7 @@ fn run_update(
perms.set_mode(0o755); perms.set_mode(0o755);
fs::set_permissions(&target_file, perms)?; fs::set_permissions(&target_file, perms)?;
} }
tmp_dir.close()?;
update_status(status, "Complete".to_string(), 3, 3, &cancel)?; update_status(status, "Complete".to_string(), 3, 3, &cancel)?;
Ok(Box::from(UpdateResult { exe_path: target_file })) Ok(Box::from(UpdateResult { exe_path: target_file }))

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 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
mod app; mod app;
@@ -19,6 +18,7 @@ use std::{
use anyhow::{ensure, Result}; use anyhow::{ensure, Result};
use cfg_if::cfg_if; use cfg_if::cfg_if;
use time::UtcOffset; use time::UtcOffset;
use tracing_subscriber::EnvFilter;
use crate::views::graphics::{load_graphics_config, GraphicsBackend, GraphicsConfig}; use crate::views::graphics::{load_graphics_config, GraphicsBackend, GraphicsConfig};
@@ -40,7 +40,16 @@ const APP_NAME: &str = "objdiff";
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
fn main() -> ExitCode { fn main() -> ExitCode {
// Log to stdout (if you run with `RUST_LOG=debug`). // Log to stdout (if you run with `RUST_LOG=debug`).
tracing_subscriber::fmt::init(); tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::builder()
// Default to info level
.with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into())
.from_env_lossy()
// This module is noisy at info level
.add_directive("wgpu_core::device::resource=warn".parse().unwrap()),
)
.init();
// Because localtime_r is unsound in multithreaded apps, // Because localtime_r is unsound in multithreaded apps,
// we must call this before initializing eframe. // we must call this before initializing eframe.
@@ -49,8 +58,10 @@ fn main() -> ExitCode {
let app_path = std::env::current_exe().ok(); let app_path = std::env::current_exe().ok();
let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None)); let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None));
let mut native_options = let mut native_options = eframe::NativeOptions {
eframe::NativeOptions { follow_system_theme: false, ..Default::default() }; viewport: egui::ViewportBuilder::default().with_app_id(APP_NAME),
..Default::default()
};
match load_icon() { match load_icon() {
Ok(data) => { Ok(data) => {
native_options.viewport.icon = Some(Arc::new(data)); native_options.viewport.icon = Some(Arc::new(data));
@@ -189,14 +200,14 @@ fn run_eframe(
APP_NAME, APP_NAME,
native_options, native_options,
Box::new(move |cc| { Box::new(move |cc| {
Box::new(app::App::new( Ok(Box::new(app::App::new(
cc, cc,
utc_offset, utc_offset,
exec_path_clone, exec_path_clone,
app_path, app_path,
graphics_config, graphics_config,
graphics_config_path, graphics_config_path,
)) )))
}), }),
) )
} }

View File

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

View File

@@ -2,12 +2,11 @@
use std::string::FromUtf16Error; use std::string::FromUtf16Error;
use std::{ use std::{
mem::take, mem::take,
path::{PathBuf, MAIN_SEPARATOR}, path::{Path, PathBuf, MAIN_SEPARATOR},
}; };
#[cfg(all(windows, feature = "wsl"))] #[cfg(all(windows, feature = "wsl"))]
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use const_format::formatcp;
use egui::{ use egui::{
output::OpenUrl, text::LayoutJob, CollapsingHeader, FontFamily, FontId, RichText, output::OpenUrl, text::LayoutJob, CollapsingHeader, FontFamily, FontId, RichText,
SelectableLabel, TextFormat, Widget, SelectableLabel, TextFormat, Widget,
@@ -17,11 +16,10 @@ use objdiff_core::{
config::{ProjectObject, DEFAULT_WATCH_PATTERNS}, config::{ProjectObject, DEFAULT_WATCH_PATTERNS},
diff::{ArmArchVersion, ArmR9Usage, MipsAbi, MipsInstrCategory, X86Formatter}, diff::{ArmArchVersion, ArmR9Usage, MipsAbi, MipsInstrCategory, X86Formatter},
}; };
use self_update::cargo_crate_version;
use strum::{EnumMessage, VariantArray}; use strum::{EnumMessage, VariantArray};
use crate::{ use crate::{
app::{AppConfig, AppConfigRef, ObjectConfig}, app::{AppConfig, AppState, AppStateRef, ObjectConfig},
config::ProjectObjectNode, config::ProjectObjectNode,
jobs::{ jobs::{
check_update::{start_check_update, CheckUpdateResult}, check_update::{start_check_update, CheckUpdateResult},
@@ -56,7 +54,7 @@ pub struct ConfigViewState {
} }
impl ConfigViewState { impl ConfigViewState {
pub fn pre_update(&mut self, jobs: &mut JobQueue, config: &AppConfigRef) { pub fn pre_update(&mut self, jobs: &mut JobQueue, state: &AppStateRef) {
jobs.results.retain_mut(|result| { jobs.results.retain_mut(|result| {
if let JobResult::CheckUpdate(result) = result { if let JobResult::CheckUpdate(result) = result {
self.check_update = take(result); self.check_update = take(result);
@@ -73,21 +71,21 @@ impl ConfigViewState {
match self.file_dialog_state.poll() { match self.file_dialog_state.poll() {
FileDialogResult::None => {} FileDialogResult::None => {}
FileDialogResult::ProjectDir(path) => { FileDialogResult::ProjectDir(path) => {
let mut guard = config.write().unwrap(); let mut guard = state.write().unwrap();
guard.set_project_dir(path.to_path_buf()); guard.set_project_dir(path.to_path_buf());
} }
FileDialogResult::TargetDir(path) => { FileDialogResult::TargetDir(path) => {
let mut guard = config.write().unwrap(); let mut guard = state.write().unwrap();
guard.set_target_obj_dir(path.to_path_buf()); guard.set_target_obj_dir(path.to_path_buf());
} }
FileDialogResult::BaseDir(path) => { FileDialogResult::BaseDir(path) => {
let mut guard = config.write().unwrap(); let mut guard = state.write().unwrap();
guard.set_base_obj_dir(path.to_path_buf()); guard.set_base_obj_dir(path.to_path_buf());
} }
FileDialogResult::Object(path) => { FileDialogResult::Object(path) => {
let mut guard = config.write().unwrap(); let mut guard = state.write().unwrap();
if let (Some(base_dir), Some(target_dir)) = if let (Some(base_dir), Some(target_dir)) =
(&guard.base_obj_dir, &guard.target_obj_dir) (&guard.config.base_obj_dir, &guard.config.target_obj_dir)
{ {
if let Ok(obj_path) = path.strip_prefix(base_dir) { if let Ok(obj_path) = path.strip_prefix(base_dir) {
let target_path = target_dir.join(obj_path); let target_path = target_dir.join(obj_path);
@@ -98,6 +96,7 @@ impl ConfigViewState {
reverse_fn_order: None, reverse_fn_order: None,
complete: None, complete: None,
scratch: None, scratch: None,
source_path: None,
}); });
} else if let Ok(obj_path) = path.strip_prefix(target_dir) { } else if let Ok(obj_path) = path.strip_prefix(target_dir) {
let base_path = base_dir.join(obj_path); let base_path = base_dir.join(obj_path);
@@ -108,6 +107,7 @@ impl ConfigViewState {
reverse_fn_order: None, reverse_fn_order: None,
complete: None, complete: None,
scratch: 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 { if self.queue_build {
self.queue_build = false; self.queue_build = false;
if let Ok(mut config) = config.write() { if let Ok(mut state) = state.write() {
config.queue_build = true; state.queue_build = true;
} }
} }
@@ -169,47 +169,43 @@ fn fetch_wsl2_distros() -> Vec<String> {
pub fn config_ui( pub fn config_ui(
ui: &mut egui::Ui, ui: &mut egui::Ui,
config: &AppConfigRef, state: &AppStateRef,
show_config_window: &mut bool, show_config_window: &mut bool,
state: &mut ConfigViewState, config_state: &mut ConfigViewState,
appearance: &Appearance, appearance: &Appearance,
) { ) {
let mut config_guard = config.write().unwrap(); let mut state_guard = state.write().unwrap();
let AppConfig { let AppState {
target_obj_dir, config:
base_obj_dir, AppConfig {
selected_obj, project_dir, target_obj_dir, base_obj_dir, selected_obj, auto_update_check, ..
auto_update_check, },
objects, objects,
object_nodes, object_nodes,
.. ..
} = &mut *config_guard; } = &mut *state_guard;
ui.heading("Updates"); ui.heading("Updates");
ui.checkbox(auto_update_check, "Check for updates on startup"); ui.checkbox(auto_update_check, "Check for updates on startup");
if ui.add_enabled(!state.check_update_running, egui::Button::new("Check now")).clicked() { if ui.add_enabled(!config_state.check_update_running, egui::Button::new("Check now")).clicked()
state.queue_check_update = true; {
config_state.queue_check_update = true;
} }
ui.label(format!("Current version: {}", cargo_crate_version!())).on_hover_ui_at_pointer(|ui| { ui.label(format!("Current version: {}", env!("CARGO_PKG_VERSION")));
ui.label(formatcp!("Git branch: {}", env!("VERGEN_GIT_BRANCH"))); if let Some(result) = &config_state.check_update {
ui.label(formatcp!("Git commit: {}", env!("VERGEN_GIT_SHA")));
ui.label(formatcp!("Build target: {}", env!("VERGEN_CARGO_TARGET_TRIPLE")));
ui.label(formatcp!("Debug: {}", env!("VERGEN_CARGO_DEBUG")));
});
if let Some(result) = &state.check_update {
ui.label(format!("Latest version: {}", result.latest_release.version)); ui.label(format!("Latest version: {}", result.latest_release.version));
if result.update_available { if result.update_available {
ui.colored_label(appearance.insert_color, "Update available"); ui.colored_label(appearance.insert_color, "Update available");
ui.horizontal(|ui| { ui.horizontal(|ui| {
if let Some(bin_name) = &result.found_binary { if let Some(bin_name) = &result.found_binary {
if ui if ui
.add_enabled(!state.update_running, egui::Button::new("Automatic")) .add_enabled(!config_state.update_running, egui::Button::new("Automatic"))
.on_hover_text_at_pointer( .on_hover_text_at_pointer(
"Automatically download and replace the current build", "Automatically download and replace the current build",
) )
.clicked() .clicked()
{ {
state.queue_update = Some(bin_name.clone()); config_state.queue_update = Some(bin_name.clone());
} }
} }
if ui if ui
@@ -238,7 +234,7 @@ pub fn config_ui(
if objects.is_empty() { if objects.is_empty() {
if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) { if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) {
if ui.button("Select object").clicked() { if ui.button("Select object").clicked() {
state.file_dialog_state.queue( config_state.file_dialog_state.queue(
|| { || {
Box::pin( Box::pin(
rfd::AsyncFileDialog::new() rfd::AsyncFileDialog::new()
@@ -261,8 +257,8 @@ pub fn config_ui(
ui.colored_label(appearance.delete_color, "Missing project settings"); ui.colored_label(appearance.delete_color, "Missing project settings");
} }
} else { } else {
let had_search = !state.object_search.is_empty(); let had_search = !config_state.object_search.is_empty();
egui::TextEdit::singleline(&mut state.object_search).hint_text("Filter").ui(ui); egui::TextEdit::singleline(&mut config_state.object_search).hint_text("Filter").ui(ui);
let mut root_open = None; let mut root_open = None;
let mut node_open = NodeOpen::Default; let mut node_open = NodeOpen::Default;
@@ -284,19 +280,22 @@ pub fn config_ui(
node_open = NodeOpen::Object; node_open = NodeOpen::Object;
} }
let mut filters_text = RichText::new("Filter ⏷"); let mut filters_text = RichText::new("Filter ⏷");
if state.filter_diffable || state.filter_incomplete || state.show_hidden { if config_state.filter_diffable
|| config_state.filter_incomplete
|| config_state.show_hidden
{
filters_text = filters_text.color(appearance.replace_color); filters_text = filters_text.color(appearance.replace_color);
} }
egui::menu::menu_button(ui, filters_text, |ui| { egui::menu::menu_button(ui, filters_text, |ui| {
ui.checkbox(&mut state.filter_diffable, "Diffable") ui.checkbox(&mut config_state.filter_diffable, "Diffable")
.on_hover_text_at_pointer("Only show objects with a source file"); .on_hover_text_at_pointer("Only show objects with a source file");
ui.checkbox(&mut state.filter_incomplete, "Incomplete") ui.checkbox(&mut config_state.filter_incomplete, "Incomplete")
.on_hover_text_at_pointer("Only show objects not marked complete"); .on_hover_text_at_pointer("Only show objects not marked complete");
ui.checkbox(&mut state.show_hidden, "Hidden") ui.checkbox(&mut config_state.show_hidden, "Hidden")
.on_hover_text_at_pointer("Show hidden (auto-generated) objects"); .on_hover_text_at_pointer("Show hidden (auto-generated) objects");
}); });
}); });
if state.object_search.is_empty() { if config_state.object_search.is_empty() {
if had_search { if had_search {
root_open = Some(true); root_open = Some(true);
node_open = NodeOpen::Object; node_open = NodeOpen::Object;
@@ -313,39 +312,45 @@ pub fn config_ui(
.open(root_open) .open(root_open)
.default_open(true) .default_open(true)
.show(ui, |ui| { .show(ui, |ui| {
let search = state.object_search.to_ascii_lowercase(); let search = config_state.object_search.to_ascii_lowercase();
ui.style_mut().wrap = Some(false); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
for node in object_nodes.iter().filter_map(|node| { for node in object_nodes.iter().filter_map(|node| {
filter_node( filter_node(
node, node,
&search, &search,
state.filter_diffable, config_state.filter_diffable,
state.filter_incomplete, config_state.filter_incomplete,
state.show_hidden, config_state.show_hidden,
) )
}) { }) {
display_node(ui, &mut new_selected_obj, &node, appearance, node_open); display_node(
ui,
&mut new_selected_obj,
project_dir.as_deref(),
&node,
appearance,
node_open,
);
} }
}); });
} }
if new_selected_obj != *selected_obj { if new_selected_obj != *selected_obj {
if let Some(obj) = new_selected_obj { if let Some(obj) = new_selected_obj {
// Will set obj_changed, which will trigger a rebuild // 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() if state_guard.config.selected_obj.is_some()
&& ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() && ui.add_enabled(!config_state.build_running, egui::Button::new("Build")).clicked()
{ {
state.queue_build = true; config_state.queue_build = true;
} }
ui.separator();
} }
fn display_object( fn display_object(
ui: &mut egui::Ui, ui: &mut egui::Ui,
selected_obj: &mut Option<ObjectConfig>, selected_obj: &mut Option<ObjectConfig>,
project_dir: Option<&Path>,
name: &str, name: &str,
object: &ProjectObject, object: &ProjectObject,
appearance: &Appearance, appearance: &Appearance,
@@ -363,7 +368,7 @@ fn display_object(
} else { } else {
appearance.text_color appearance.text_color
}; };
let clicked = SelectableLabel::new( let response = SelectableLabel::new(
selected, selected,
RichText::new(name) RichText::new(name)
.font(FontId { .font(FontId {
@@ -372,11 +377,13 @@ fn display_object(
}) })
.color(color), .color(color),
) )
.ui(ui) .ui(ui);
.clicked(); 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. // Always recreate ObjectConfig if selected, in case the project config changed.
// ObjectConfig is compared using equality, so this won't unnecessarily trigger a rebuild. // ObjectConfig is compared using equality, so this won't unnecessarily trigger a rebuild.
if selected || clicked { if selected || response.clicked() {
*selected_obj = Some(ObjectConfig { *selected_obj = Some(ObjectConfig {
name: object_name.to_string(), name: object_name.to_string(),
target_path: object.target_path.clone(), target_path: object.target_path.clone(),
@@ -384,10 +391,31 @@ fn display_object(
reverse_fn_order: object.reverse_fn_order(), reverse_fn_order: object.reverse_fn_order(),
complete: object.complete(), complete: object.complete(),
scratch: object.scratch.clone(), 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)] #[derive(Default, Copy, Clone, PartialEq, Eq, Debug)]
enum NodeOpen { enum NodeOpen {
#[default] #[default]
@@ -400,13 +428,14 @@ enum NodeOpen {
fn display_node( fn display_node(
ui: &mut egui::Ui, ui: &mut egui::Ui,
selected_obj: &mut Option<ObjectConfig>, selected_obj: &mut Option<ObjectConfig>,
project_dir: Option<&Path>,
node: &ProjectObjectNode, node: &ProjectObjectNode,
appearance: &Appearance, appearance: &Appearance,
node_open: NodeOpen, node_open: NodeOpen,
) { ) {
match node { match node {
ProjectObjectNode::File(name, object) => { 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) => { ProjectObjectNode::Dir(name, children) => {
let contains_obj = selected_obj.as_ref().map(|path| contains_node(node, path)); let contains_obj = selected_obj.as_ref().map(|path| contains_node(node, path));
@@ -432,7 +461,7 @@ fn display_node(
.open(open) .open(open)
.show(ui, |ui| { .show(ui, |ui| {
for node in children { for node in children {
display_node(ui, selected_obj, node, appearance, node_open); display_node(ui, selected_obj, project_dir, node, appearance, node_open);
} }
}); });
} }
@@ -530,33 +559,33 @@ fn pick_folder_ui(
pub fn project_window( pub fn project_window(
ctx: &egui::Context, ctx: &egui::Context,
config: &AppConfigRef, state: &AppStateRef,
show: &mut bool, show: &mut bool,
state: &mut ConfigViewState, config_state: &mut ConfigViewState,
appearance: &Appearance, appearance: &Appearance,
) { ) {
let mut config_guard = config.write().unwrap(); let mut state_guard = state.write().unwrap();
egui::Window::new("Project").open(show).show(ctx, |ui| { egui::Window::new("Project").open(show).show(ctx, |ui| {
split_obj_config_ui(ui, &mut config_guard, state, appearance); split_obj_config_ui(ui, &mut state_guard, config_state, appearance);
}); });
if let Some(error) = &state.load_error { if let Some(error) = &config_state.load_error {
let mut open = true; let mut open = true;
egui::Window::new("Error").open(&mut open).show(ctx, |ui| { egui::Window::new("Error").open(&mut open).show(ctx, |ui| {
ui.label("Failed to load project config:"); ui.label("Failed to load project config:");
ui.colored_label(appearance.delete_color, error); ui.colored_label(appearance.delete_color, error);
}); });
if !open { if !open {
state.load_error = None; config_state.load_error = None;
} }
} }
} }
fn split_obj_config_ui( fn split_obj_config_ui(
ui: &mut egui::Ui, ui: &mut egui::Ui,
config: &mut AppConfig, state: &mut AppState,
state: &mut ConfigViewState, config_state: &mut ConfigViewState,
appearance: &Appearance, appearance: &Appearance,
) { ) {
let text_format = TextFormat::simple(appearance.ui_font.clone(), appearance.text_color); let text_format = TextFormat::simple(appearance.ui_font.clone(), appearance.text_color);
@@ -567,7 +596,7 @@ fn split_obj_config_ui(
let response = pick_folder_ui( let response = pick_folder_ui(
ui, ui,
&config.project_dir, &state.config.project_dir,
"Project directory", "Project directory",
|ui| { |ui| {
let mut job = LayoutJob::default(); let mut job = LayoutJob::default();
@@ -583,7 +612,7 @@ fn split_obj_config_ui(
true, true,
); );
if response.clicked() { if response.clicked() {
state.file_dialog_state.queue( config_state.file_dialog_state.queue(
|| Box::pin(rfd::AsyncFileDialog::new().pick_folder()), || Box::pin(rfd::AsyncFileDialog::new().pick_folder()),
FileDialogResult::ProjectDir, FileDialogResult::ProjectDir,
); );
@@ -612,33 +641,35 @@ fn split_obj_config_ui(
ui.label(job); ui.label(job);
}); });
}); });
let mut custom_make_str = config.custom_make.clone().unwrap_or_default(); let mut custom_make_str = state.config.custom_make.clone().unwrap_or_default();
if ui if ui
.add_enabled( .add_enabled(
config.project_config_info.is_none(), state.project_config_info.is_none(),
egui::TextEdit::singleline(&mut custom_make_str).hint_text("make"), egui::TextEdit::singleline(&mut custom_make_str).hint_text("make"),
) )
.on_disabled_hover_text(CONFIG_DISABLED_TEXT) .on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.changed() .changed()
{ {
if custom_make_str.is_empty() { if custom_make_str.is_empty() {
config.custom_make = None; state.config.custom_make = None;
} else { } else {
config.custom_make = Some(custom_make_str); state.config.custom_make = Some(custom_make_str);
} }
} }
#[cfg(all(windows, feature = "wsl"))] #[cfg(all(windows, feature = "wsl"))]
{ {
if state.available_wsl_distros.is_none() { if config_state.available_wsl_distros.is_none() {
state.available_wsl_distros = Some(fetch_wsl2_distros()); config_state.available_wsl_distros = Some(fetch_wsl2_distros());
} }
egui::ComboBox::from_label("Run in WSL2") egui::ComboBox::from_label("Run in WSL2")
.selected_text(config.selected_wsl_distro.as_ref().unwrap_or(&"Disabled".to_string())) .selected_text(
state.config.selected_wsl_distro.as_ref().unwrap_or(&"Disabled".to_string()),
)
.show_ui(ui, |ui| { .show_ui(ui, |ui| {
ui.selectable_value(&mut config.selected_wsl_distro, None, "Disabled"); ui.selectable_value(&mut state.config.selected_wsl_distro, None, "Disabled");
for distro in state.available_wsl_distros.as_ref().unwrap() { for distro in config_state.available_wsl_distros.as_ref().unwrap() {
ui.selectable_value( ui.selectable_value(
&mut config.selected_wsl_distro, &mut state.config.selected_wsl_distro,
Some(distro.clone()), Some(distro.clone()),
distro, distro,
); );
@@ -647,10 +678,10 @@ fn split_obj_config_ui(
} }
ui.separator(); ui.separator();
if let Some(project_dir) = config.project_dir.clone() { if let Some(project_dir) = state.config.project_dir.clone() {
let response = pick_folder_ui( let response = pick_folder_ui(
ui, ui,
&config.target_obj_dir, &state.config.target_obj_dir,
"Target build directory", "Target build directory",
|ui| { |ui| {
let mut job = LayoutJob::default(); let mut job = LayoutJob::default();
@@ -667,17 +698,17 @@ fn split_obj_config_ui(
ui.label(job); ui.label(job);
}, },
appearance, appearance,
config.project_config_info.is_none(), state.project_config_info.is_none(),
); );
if response.clicked() { if response.clicked() {
state.file_dialog_state.queue( config_state.file_dialog_state.queue(
|| Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()), || Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()),
FileDialogResult::TargetDir, FileDialogResult::TargetDir,
); );
} }
ui.add_enabled( ui.add_enabled(
config.project_config_info.is_none(), state.project_config_info.is_none(),
egui::Checkbox::new(&mut config.build_target, "Build target objects"), egui::Checkbox::new(&mut state.config.build_target, "Build target objects"),
) )
.on_disabled_hover_text(CONFIG_DISABLED_TEXT) .on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.on_hover_ui(|ui| { .on_hover_ui(|ui| {
@@ -711,7 +742,7 @@ fn split_obj_config_ui(
let response = pick_folder_ui( let response = pick_folder_ui(
ui, ui,
&config.base_obj_dir, &state.config.base_obj_dir,
"Base build directory", "Base build directory",
|ui| { |ui| {
let mut job = LayoutJob::default(); let mut job = LayoutJob::default();
@@ -723,17 +754,17 @@ fn split_obj_config_ui(
ui.label(job); ui.label(job);
}, },
appearance, appearance,
config.project_config_info.is_none(), state.project_config_info.is_none(),
); );
if response.clicked() { if response.clicked() {
state.file_dialog_state.queue( config_state.file_dialog_state.queue(
|| Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()), || Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()),
FileDialogResult::BaseDir, FileDialogResult::BaseDir,
); );
} }
ui.add_enabled( ui.add_enabled(
config.project_config_info.is_none(), state.project_config_info.is_none(),
egui::Checkbox::new(&mut config.build_base, "Build base objects"), egui::Checkbox::new(&mut state.config.build_base, "Build base objects"),
) )
.on_disabled_hover_text(CONFIG_DISABLED_TEXT) .on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.on_hover_ui(|ui| { .on_hover_ui(|ui| {
@@ -764,7 +795,7 @@ fn split_obj_config_ui(
subheading(ui, "Watch settings", appearance); subheading(ui, "Watch settings", appearance);
let response = let response =
ui.checkbox(&mut config.rebuild_on_changes, "Rebuild on changes").on_hover_ui(|ui| { ui.checkbox(&mut state.config.rebuild_on_changes, "Rebuild on changes").on_hover_ui(|ui| {
let mut job = LayoutJob::default(); let mut job = LayoutJob::default();
job.append( job.append(
"Automatically re-run the build & diff when files change.", "Automatically re-run the build & diff when files change.",
@@ -774,23 +805,23 @@ fn split_obj_config_ui(
ui.label(job); ui.label(job);
}); });
if response.changed() { if response.changed() {
config.watcher_change = true; state.watcher_change = true;
}; };
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(RichText::new("File patterns").color(appearance.text_color)); ui.label(RichText::new("File patterns").color(appearance.text_color));
if ui if ui
.add_enabled(config.project_config_info.is_none(), egui::Button::new("Reset")) .add_enabled(state.project_config_info.is_none(), egui::Button::new("Reset"))
.on_disabled_hover_text(CONFIG_DISABLED_TEXT) .on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.clicked() .clicked()
{ {
config.watch_patterns = state.config.watch_patterns =
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(); DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect();
config.watcher_change = true; state.watcher_change = true;
} }
}); });
let mut remove_at: Option<usize> = None; let mut remove_at: Option<usize> = None;
for (idx, glob) in config.watch_patterns.iter().enumerate() { for (idx, glob) in state.config.watch_patterns.iter().enumerate() {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label( ui.label(
RichText::new(format!("{}", glob)) RichText::new(format!("{}", glob))
@@ -798,7 +829,7 @@ fn split_obj_config_ui(
.family(FontFamily::Monospace), .family(FontFamily::Monospace),
); );
if ui if ui
.add_enabled(config.project_config_info.is_none(), egui::Button::new("-").small()) .add_enabled(state.project_config_info.is_none(), egui::Button::new("-").small())
.on_disabled_hover_text(CONFIG_DISABLED_TEXT) .on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.clicked() .clicked()
{ {
@@ -807,24 +838,24 @@ fn split_obj_config_ui(
}); });
} }
if let Some(idx) = remove_at { if let Some(idx) = remove_at {
config.watch_patterns.remove(idx); state.config.watch_patterns.remove(idx);
config.watcher_change = true; state.watcher_change = true;
} }
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add_enabled( ui.add_enabled(
config.project_config_info.is_none(), state.project_config_info.is_none(),
egui::TextEdit::singleline(&mut state.watch_pattern_text).desired_width(100.0), egui::TextEdit::singleline(&mut config_state.watch_pattern_text).desired_width(100.0),
) )
.on_disabled_hover_text(CONFIG_DISABLED_TEXT); .on_disabled_hover_text(CONFIG_DISABLED_TEXT);
if ui if ui
.add_enabled(config.project_config_info.is_none(), egui::Button::new("+").small()) .add_enabled(state.project_config_info.is_none(), egui::Button::new("+").small())
.on_disabled_hover_text(CONFIG_DISABLED_TEXT) .on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.clicked() .clicked()
{ {
if let Ok(glob) = Glob::new(&state.watch_pattern_text) { if let Ok(glob) = Glob::new(&config_state.watch_pattern_text) {
config.watch_patterns.push(glob); state.config.watch_patterns.push(glob);
config.watcher_change = true; state.watcher_change = true;
state.watch_pattern_text.clear(); config_state.watch_pattern_text.clear();
} }
} }
}); });
@@ -832,131 +863,131 @@ fn split_obj_config_ui(
pub fn arch_config_window( pub fn arch_config_window(
ctx: &egui::Context, ctx: &egui::Context,
config: &AppConfigRef, state: &AppStateRef,
show: &mut bool, show: &mut bool,
appearance: &Appearance, appearance: &Appearance,
) { ) {
let mut config_guard = config.write().unwrap(); let mut state_guard = state.write().unwrap();
egui::Window::new("Arch Settings").open(show).show(ctx, |ui| { egui::Window::new("Arch Settings").open(show).show(ctx, |ui| {
arch_config_ui(ui, &mut config_guard, appearance); arch_config_ui(ui, &mut state_guard, appearance);
}); });
} }
fn arch_config_ui(ui: &mut egui::Ui, config: &mut AppConfig, _appearance: &Appearance) { fn arch_config_ui(ui: &mut egui::Ui, state: &mut AppState, _appearance: &Appearance) {
ui.heading("x86"); ui.heading("x86");
egui::ComboBox::new("x86_formatter", "Format") 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| { .show_ui(ui, |ui| {
for &formatter in X86Formatter::VARIANTS { for &formatter in X86Formatter::VARIANTS {
if ui if ui
.selectable_label( .selectable_label(
config.diff_obj_config.x86_formatter == formatter, state.config.diff_obj_config.x86_formatter == formatter,
formatter.get_message().unwrap(), formatter.get_message().unwrap(),
) )
.clicked() .clicked()
{ {
config.diff_obj_config.x86_formatter = formatter; state.config.diff_obj_config.x86_formatter = formatter;
config.queue_reload = true; state.queue_reload = true;
} }
} }
}); });
ui.separator(); ui.separator();
ui.heading("MIPS"); ui.heading("MIPS");
egui::ComboBox::new("mips_abi", "ABI") 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| { .show_ui(ui, |ui| {
for &abi in MipsAbi::VARIANTS { for &abi in MipsAbi::VARIANTS {
if ui if ui
.selectable_label( .selectable_label(
config.diff_obj_config.mips_abi == abi, state.config.diff_obj_config.mips_abi == abi,
abi.get_message().unwrap(), abi.get_message().unwrap(),
) )
.clicked() .clicked()
{ {
config.diff_obj_config.mips_abi = abi; state.config.diff_obj_config.mips_abi = abi;
config.queue_reload = true; state.queue_reload = true;
} }
} }
}); });
egui::ComboBox::new("mips_instr_category", "Instruction Category") 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| { .show_ui(ui, |ui| {
for &category in MipsInstrCategory::VARIANTS { for &category in MipsInstrCategory::VARIANTS {
if ui if ui
.selectable_label( .selectable_label(
config.diff_obj_config.mips_instr_category == category, state.config.diff_obj_config.mips_instr_category == category,
category.get_message().unwrap(), category.get_message().unwrap(),
) )
.clicked() .clicked()
{ {
config.diff_obj_config.mips_instr_category = category; state.config.diff_obj_config.mips_instr_category = category;
config.queue_reload = true; state.queue_reload = true;
} }
} }
}); });
ui.separator(); ui.separator();
ui.heading("ARM"); ui.heading("ARM");
egui::ComboBox::new("arm_arch_version", "Architecture Version") 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| { .show_ui(ui, |ui| {
for &version in ArmArchVersion::VARIANTS { for &version in ArmArchVersion::VARIANTS {
if ui if ui
.selectable_label( .selectable_label(
config.diff_obj_config.arm_arch_version == version, state.config.diff_obj_config.arm_arch_version == version,
version.get_message().unwrap(), version.get_message().unwrap(),
) )
.clicked() .clicked()
{ {
config.diff_obj_config.arm_arch_version = version; state.config.diff_obj_config.arm_arch_version = version;
config.queue_reload = true; state.queue_reload = true;
} }
} }
}); });
let response = ui 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)."); .on_hover_text("Disassemble as unified assembly language (UAL).");
if response.changed() { if response.changed() {
config.queue_reload = true; state.queue_reload = true;
} }
let response = ui 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"); .on_hover_text("Display R0-R3 as A1-A4 and R4-R11 as V1-V8");
if response.changed() { if response.changed() {
config.queue_reload = true; state.queue_reload = true;
} }
egui::ComboBox::new("arm_r9_usage", "Display R9 as") 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| { .show_ui(ui, |ui| {
for &usage in ArmR9Usage::VARIANTS { for &usage in ArmR9Usage::VARIANTS {
if ui if ui
.selectable_label( .selectable_label(
config.diff_obj_config.arm_r9_usage == usage, state.config.diff_obj_config.arm_r9_usage == usage,
usage.get_message().unwrap(), usage.get_message().unwrap(),
) )
.on_hover_text(usage.get_detailed_message().unwrap()) .on_hover_text(usage.get_detailed_message().unwrap())
.clicked() .clicked()
{ {
config.diff_obj_config.arm_r9_usage = usage; state.config.diff_obj_config.arm_r9_usage = usage;
config.queue_reload = true; state.queue_reload = true;
} }
} }
}); });
let response = ui 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."); .on_hover_text("Used for explicit stack limits.");
if response.changed() { if response.changed() {
config.queue_reload = true; state.queue_reload = true;
} }
let response = ui 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."); .on_hover_text("Used for frame pointers.");
if response.changed() { if response.changed() {
config.queue_reload = true; state.queue_reload = true;
} }
let response = ui 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."); .on_hover_text("Used for interworking and long branches.");
if response.changed() { 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 }, Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min), Layout::left_to_right(Align::Min),
|ui| { |ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
// Left column // Left column
ui.allocate_ui_with_layout( ui.allocate_ui_with_layout(
Vec2 { x: column_width, y: 100.0 }, 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.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.colored_label(appearance.highlight_color, &selected_symbol.symbol_name); ui.colored_label(appearance.highlight_color, &selected_symbol.symbol_name);
ui.label("Diff target:"); 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.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
if state.build_running { if state.build_running {
ui.colored_label(appearance.replace_color, "Building…"); ui.colored_label(appearance.replace_color, "Building…");
} else { } else {
@@ -247,7 +247,6 @@ pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &A
ui.scope(|ui| { ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.label(""); ui.label("");
ui.label("Diff base:"); 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 egui_extras::{Size, StripBuilder};
use objdiff_core::{ use objdiff_core::{
arch::ppc::ExceptionInfo,
diff::ObjDiff, diff::ObjDiff,
obj::{ObjExtab, ObjInfo, ObjSymbol, SymbolRef}, obj::{ObjInfo, ObjSymbol, SymbolRef},
}; };
use time::format_description; use time::format_description;
@@ -22,38 +23,28 @@ fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<Symbo
None None
} }
fn decode_extab(extab: &ObjExtab) -> String { fn decode_extab(extab: &ExceptionInfo) -> String {
let mut text = String::from(""); let mut text = String::from("");
let mut dtor_names: Vec<&str> = vec![]; let mut dtor_names: Vec<String> = vec![];
for dtor in &extab.dtors { for dtor in &extab.dtors {
//For each function name, use the demangled name by default, //For each function name, use the demangled name by default,
//and if not available fallback to the original name //and if not available fallback to the original name
let name = match &dtor.demangled_name { let name: String = match &dtor.demangled_name {
Some(demangled_name) => demangled_name, Some(demangled_name) => demangled_name.to_string(),
None => &dtor.name, None => dtor.name.clone(),
}; };
dtor_names.push(name.as_str()); dtor_names.push(name);
} }
if let Some(decoded) = extab.data.to_string(&dtor_names) { if let Some(decoded) = extab.data.to_string(dtor_names) {
text += decoded.as_str(); text += decoded.as_str();
} }
text text
} }
fn find_extab_entry(obj: &ObjInfo, symbol: &ObjSymbol) -> Option<ObjExtab> { fn find_extab_entry<'a>(obj: &'a ObjInfo, symbol: &ObjSymbol) -> Option<&'a ExceptionInfo> {
if let Some(extab_array) = &obj.extab { obj.arch.ppc().and_then(|ppc| ppc.extab_for_symbol(symbol))
for extab_entry in extab_array {
if extab_entry.func.name == symbol.name {
return Some(extab_entry.clone());
}
}
} else {
return None;
}
None
} }
fn extab_text_ui( fn extab_text_ui(
@@ -65,7 +56,7 @@ fn extab_text_ui(
let (_section, symbol) = obj.0.section_symbol(symbol_ref); let (_section, symbol) = obj.0.section_symbol(symbol_ref);
if let Some(extab_entry) = find_extab_entry(&obj.0, symbol) { 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); ui.colored_label(appearance.replace_color, &text);
return Some(()); return Some(());
} }
@@ -83,7 +74,7 @@ fn extab_ui(
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
ui.scope(|ui| { ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
let symbol = obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol)); 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 }, Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min), Layout::left_to_right(Align::Min),
|ui| { |ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
// Left column // Left column
ui.allocate_ui_with_layout( ui.allocate_ui_with_layout(
Vec2 { x: column_width, y: 100.0 }, 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 .demangled_symbol_name
.as_deref() .as_deref()
.unwrap_or(&selected_symbol.symbol_name); .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.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.colored_label(appearance.highlight_color, name);
ui.label("Diff target:"); 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.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
if state.build_running { if state.build_running {
ui.colored_label(appearance.replace_color, "Building…"); ui.colored_label(appearance.replace_color, "Building…");
} else { } else {
@@ -189,7 +172,7 @@ pub fn extab_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &
{ {
ui.colored_label( ui.colored_label(
match_color_for_symbol(match_percent, appearance), match_color_for_symbol(match_percent, appearance),
&format!("{match_percent:.0}%"), format!("{match_percent:.0}%"),
); );
} else { } else {
ui.colored_label(appearance.replace_color, "Missing"); 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}, symbol_diff::{match_color_for_symbol, DiffViewState, SymbolRefByName, View},
}; };
#[derive(Copy, Clone, Eq, PartialEq)]
enum ColumnId {
Left,
Right,
}
#[derive(Default)] #[derive(Default)]
pub struct FunctionViewState { 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( fn ins_hover_ui(
@@ -32,7 +77,7 @@ fn ins_hover_ui(
) { ) {
ui.scope(|ui| { ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
let offset = ins.address - section.address; let offset = ins.address - section.address;
ui.label(format!( ui.label(format!(
@@ -79,6 +124,12 @@ fn ins_hover_ui(
appearance.highlight_color, appearance.highlight_color,
format!("Size: {:x}", reloc.target.size), 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 { } else {
ui.colored_label(appearance.highlight_color, "Extern".to_string()); 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) { fn ins_context_menu(ui: &mut egui::Ui, section: &ObjSection, ins: &ObjIns, symbol: &ObjSymbol) {
ui.scope(|ui| { ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
if ui.button(format!("Copy \"{}\"", ins.formatted)).clicked() { if ui.button(format!("Copy \"{}\"", ins.formatted)).clicked() {
ui.output_mut(|output| output.copied_text.clone_from(&ins.formatted)); ui.output_mut(|output| output.copied_text.clone_from(&ins.formatted));
@@ -167,12 +218,14 @@ fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<Symbo
None None
} }
#[allow(clippy::too_many_arguments)]
fn diff_text_ui( fn diff_text_ui(
ui: &mut egui::Ui, ui: &mut egui::Ui,
text: DiffText<'_>, text: DiffText<'_>,
ins_diff: &ObjInsDiff, ins_diff: &ObjInsDiff,
appearance: &Appearance, appearance: &Appearance,
ins_view_state: &mut FunctionViewState, ins_view_state: &mut FunctionViewState,
column: ColumnId,
space_width: f32, space_width: f32,
response_cb: impl Fn(Response) -> Response, response_cb: impl Fn(Response) -> Response,
) { ) {
@@ -237,7 +290,7 @@ fn diff_text_ui(
} }
let len = label_text.len(); let len = label_text.len();
let highlight = ins_view_state.highlight == text; let highlight = *ins_view_state.highlight(column) == text;
let mut response = Label::new(LayoutJob::single_section( let mut response = Label::new(LayoutJob::single_section(
label_text, label_text,
appearance.code_text_format(base_color, highlight), appearance.code_text_format(base_color, highlight),
@@ -246,11 +299,7 @@ fn diff_text_ui(
.ui(ui); .ui(ui);
response = response_cb(response); response = response_cb(response);
if response.clicked() { if response.clicked() {
if highlight { ins_view_state.set_highlight(column, text.into());
ins_view_state.highlight = HighlightKind::None;
} else {
ins_view_state.highlight = text.into();
}
} }
if len < pad_to { if len < pad_to {
ui.add_space((pad_to - len) as f32 * space_width); ui.add_space((pad_to - len) as f32 * space_width);
@@ -263,15 +312,26 @@ fn asm_row_ui(
symbol: &ObjSymbol, symbol: &ObjSymbol,
appearance: &Appearance, appearance: &Appearance,
ins_view_state: &mut FunctionViewState, ins_view_state: &mut FunctionViewState,
column: ColumnId,
response_cb: impl Fn(Response) -> Response, response_cb: impl Fn(Response) -> Response,
) { ) {
ui.spacing_mut().item_spacing.x = 0.0; ui.spacing_mut().item_spacing.x = 0.0;
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
if ins_diff.kind != ObjInsDiffKind::None { if ins_diff.kind != ObjInsDiffKind::None {
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color); ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
} }
let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' ')); let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' '));
display_diff(ins_diff, symbol.address, |text| { display_diff(ins_diff, symbol.address, |text| {
diff_text_ui(ui, text, ins_diff, appearance, ins_view_state, space_width, &response_cb); diff_text_ui(
ui,
text,
ins_diff,
appearance,
ins_view_state,
column,
space_width,
&response_cb,
);
Ok::<_, ()>(()) Ok::<_, ()>(())
}) })
.unwrap(); .unwrap();
@@ -283,6 +343,7 @@ fn asm_col_ui(
symbol_ref: SymbolRef, symbol_ref: SymbolRef,
appearance: &Appearance, appearance: &Appearance,
ins_view_state: &mut FunctionViewState, ins_view_state: &mut FunctionViewState,
column: ColumnId,
) { ) {
let (section, symbol) = obj.0.section_symbol(symbol_ref); let (section, symbol) = obj.0.section_symbol(symbol_ref);
let section = section.unwrap(); let section = section.unwrap();
@@ -298,7 +359,7 @@ fn asm_col_ui(
} }
}; };
let (_, response) = row.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); response_cb(response);
} }
@@ -337,12 +398,26 @@ fn asm_table_ui(
table.body(|body| { table.body(|body| {
body.rows(appearance.code_font.size, instructions_len, |mut row| { body.rows(appearance.code_font.size, instructions_len, |mut row| {
if let (Some(left_obj), Some(left_symbol_ref)) = (left_obj, left_symbol) { 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 { } else {
empty_col_ui(&mut row); empty_col_ui(&mut row);
} }
if let (Some(right_obj), Some(right_symbol_ref)) = (right_obj, right_symbol) { 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 { } else {
empty_col_ui(&mut row); 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 }, Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min), Layout::left_to_right(Align::Min),
|ui| { |ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
// Left column // Left column
ui.allocate_ui_with_layout( ui.allocate_ui_with_layout(
Vec2 { x: column_width, y: 100.0 }, 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 .demangled_symbol_name
.as_deref() .as_deref()
.unwrap_or(&selected_symbol.symbol_name); .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.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.colored_label(appearance.highlight_color, name);
ui.label("Diff target:"); 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.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
if state.build_running { if state.build_running {
ui.colored_label(appearance.replace_color, "Building…"); ui.colored_label(appearance.replace_color, "Building…");
} else { } 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| { ui.scope(|ui| {
@@ -458,7 +537,7 @@ pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance
{ {
ui.colored_label( ui.colored_label(
match_color_for_symbol(match_percent, appearance), match_color_for_symbol(match_percent, appearance),
&format!("{match_percent:.0}%"), format!("{match_percent:.0}%"),
); );
} else { } else {
ui.colored_label(appearance.replace_color, "Missing"); ui.colored_label(appearance.replace_color, "Missing");

View File

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

View File

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

View File

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

View File

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

View File

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