mirror of
https://github.com/encounter/objdiff.git
synced 2025-12-15 16:16:15 +00:00
Compare commits
46 Commits
v2.0.0-bet
...
v2.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 741d93e211 | |||
| 603dbd6882 | |||
| 6fb0a63de2 | |||
| ab2e84a2c6 | |||
| 9596051cb4 | |||
| a5d9d8282e | |||
|
|
3287a0f65c | ||
|
|
fab9c62dfb | ||
| 08cd768260 | |||
| 8acaaf528c | |||
| 6e881a74e1 | |||
| cc1bc44e69 | |||
| c7b85518ab | |||
| bb039a1445 | |||
| 8fc142d316 | |||
| b0123b3f83 | |||
| 2ec17aee9b | |||
| ec9731e1e5 | |||
|
|
a06382c27e | ||
| e013638c5a | |||
| 70ab82f1f7 | |||
| c5896689cf | |||
| 67719dd93e | |||
| 258e141017 | |||
| dbdda55065 | |||
|
|
a43320af1f | ||
|
|
35bbd40f5d | ||
|
|
c1cb4b0b19 | ||
| 2379853faa | |||
| 5e1aff180f | |||
| 3846a7d315 | |||
| dcf209aac5 | |||
| c7e6394628 | |||
| 235dc7f517 | |||
|
|
199c07e975 | ||
| 56a5a61825 | |||
| 3d2236de82 | |||
| bcc5871cd8 | |||
|
|
7d0d7df54c | ||
| 0221a2d54d | |||
|
|
bc687173c0 | ||
| e1ae369d17 | |||
| ce05d6d6c0 | |||
| c16a926d9b | |||
|
|
a32d99923c | ||
| 68606dfdcb |
137
.github/workflows/build.yaml
vendored
137
.github/workflows/build.yaml
vendored
@@ -11,6 +11,7 @@ on:
|
||||
env:
|
||||
BUILD_PROFILE: release-lto
|
||||
CARGO_TARGET_DIR: target
|
||||
CARGO_INCREMENTAL: 0
|
||||
|
||||
jobs:
|
||||
check:
|
||||
@@ -29,18 +30,10 @@ jobs:
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.4
|
||||
- name: Cargo check
|
||||
env:
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
run: cargo check
|
||||
run: cargo check --all-features --all-targets
|
||||
- name: Cargo clippy
|
||||
env:
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
run: cargo clippy
|
||||
run: cargo clippy --all-features --all-targets
|
||||
|
||||
fmt:
|
||||
name: Format
|
||||
@@ -92,16 +85,85 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.4
|
||||
- name: Cargo test
|
||||
env:
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
run: cargo test --release
|
||||
run: cargo test --release --all-features
|
||||
|
||||
build:
|
||||
name: Build
|
||||
build-cli:
|
||||
name: Build objdiff-cli
|
||||
env:
|
||||
CARGO_BIN_NAME: objdiff-cli
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- platform: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
name: linux-x86_64
|
||||
build: zigbuild
|
||||
features: default
|
||||
- platform: ubuntu-latest
|
||||
target: i686-unknown-linux-musl
|
||||
name: linux-i686
|
||||
build: zigbuild
|
||||
features: default
|
||||
- platform: ubuntu-latest
|
||||
target: aarch64-unknown-linux-musl
|
||||
name: linux-aarch64
|
||||
build: zigbuild
|
||||
features: default
|
||||
- platform: windows-latest
|
||||
target: i686-pc-windows-msvc
|
||||
name: windows-x86
|
||||
build: build
|
||||
features: default
|
||||
- platform: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
name: windows-x86_64
|
||||
build: build
|
||||
features: default
|
||||
- platform: windows-latest
|
||||
target: aarch64-pc-windows-msvc
|
||||
name: windows-arm64
|
||||
build: build
|
||||
features: default
|
||||
- platform: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
name: macos-x86_64
|
||||
build: build
|
||||
features: default
|
||||
- platform: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
name: macos-arm64
|
||||
build: build
|
||||
features: default
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install cargo-zigbuild
|
||||
if: matrix.build == 'zigbuild'
|
||||
run: 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:
|
||||
matrix:
|
||||
include:
|
||||
@@ -136,34 +198,41 @@ jobs:
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.4
|
||||
- name: Cargo build
|
||||
env:
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
run: >
|
||||
cargo build --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
|
||||
--bin objdiff-cli --bin objdiff --features ${{ matrix.features }}
|
||||
--bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.name }}
|
||||
name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }}
|
||||
path: |
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/objdiff-cli
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/objdiff-cli.exe
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/objdiff
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/objdiff.exe
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
name: Release
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ build ]
|
||||
needs: [ build-cli, build-gui ]
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Check git tag against Cargo version
|
||||
shell: bash
|
||||
run: |
|
||||
set -eou pipefail
|
||||
tag='${{github.ref}}'
|
||||
tag="${tag#refs/tags/}"
|
||||
version=$(grep '^version' Cargo.toml | head -1 | awk -F' = ' '{print $2}' | tr -d '"')
|
||||
version="v$version"
|
||||
if [ "$tag" != "$version" ]; then
|
||||
echo "::error::Git tag doesn't match the Cargo version! ($tag != $version)"
|
||||
exit 1
|
||||
fi
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -183,12 +252,16 @@ jobs:
|
||||
else
|
||||
ext=".$ext"
|
||||
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"
|
||||
done
|
||||
done
|
||||
ls -R ../out
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: out/*
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
|
||||
2336
Cargo.lock
generated
2336
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@@ -8,5 +8,14 @@ resolver = "2"
|
||||
|
||||
[profile.release-lto]
|
||||
inherits = "release"
|
||||
lto = "thin"
|
||||
lto = "fat"
|
||||
strip = "debuginfo"
|
||||
codegen-units = 1
|
||||
|
||||
[workspace.package]
|
||||
version = "2.3.0"
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/encounter/objdiff"
|
||||
rust-version = "1.74"
|
||||
|
||||
105
README.md
105
README.md
@@ -6,6 +6,7 @@
|
||||
A local diffing tool for decompilation projects. Inspired by [decomp.me](https://decomp.me) and [asm-differ](https://github.com/simonlindholm/asm-differ).
|
||||
|
||||
Features:
|
||||
|
||||
- Compare entire object files: functions and data.
|
||||
- Built-in symbol demangling for C++. (CodeWarrior, Itanium & MSVC)
|
||||
- Automatic rebuild on source file changes.
|
||||
@@ -14,6 +15,7 @@ Features:
|
||||
- Click to highlight all instances of values and registers.
|
||||
|
||||
Supports:
|
||||
|
||||
- PowerPC 750CL (GameCube, Wii)
|
||||
- MIPS (N64, PS1, PS2, PSP)
|
||||
- x86 (COFF only at the moment)
|
||||
@@ -21,6 +23,25 @@ Supports:
|
||||
|
||||
See [Usage](#usage) for more information.
|
||||
|
||||
## Downloads
|
||||
|
||||
To build from source, see [Building](#building).
|
||||
|
||||
### GUI
|
||||
|
||||
- [Windows (x86_64)](https://github.com/encounter/objdiff/releases/latest/download/objdiff-windows-x86_64.exe)
|
||||
- [Linux (x86_64)](https://github.com/encounter/objdiff/releases/latest/download/objdiff-linux-x86_64)
|
||||
- [macOS (arm64)](https://github.com/encounter/objdiff/releases/latest/download/objdiff-macos-arm64)
|
||||
- [macOS (x86_64)](https://github.com/encounter/objdiff/releases/latest/download/objdiff-macos-x86_64)
|
||||
|
||||
For Linux and macOS, run `chmod +x objdiff-*` to make the binary executable.
|
||||
|
||||
### CLI
|
||||
|
||||
CLI binaries can be found on the [releases page](https://github.com/encounter/objdiff/releases).
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -49,91 +70,89 @@ See [Configuration](#configuration) for more information.
|
||||
|
||||
## Configuration
|
||||
|
||||
While **not required** (most settings can be specified in the UI), projects can add an `objdiff.json` (or
|
||||
`objdiff.yaml`, `objdiff.yml`) file to configure the tool automatically. The configuration file must be located in
|
||||
While **not required** (most settings can be specified in the UI), projects can add an `objdiff.json` file to configure the tool automatically. The configuration file must be located in
|
||||
the root project directory.
|
||||
|
||||
If your project has a generator script (e.g. `configure.py`), it's recommended to generate the objdiff configuration
|
||||
file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it from being committed.
|
||||
|
||||
```json5
|
||||
// objdiff.json
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/encounter/objdiff/main/config.schema.json",
|
||||
"custom_make": "ninja",
|
||||
"custom_args": [
|
||||
"-d",
|
||||
"keeprsp"
|
||||
],
|
||||
|
||||
// Only required if objects use "path" instead of "target_path" and "base_path".
|
||||
"target_dir": "build/asm",
|
||||
"base_dir": "build/src",
|
||||
|
||||
"build_target": true,
|
||||
"build_target": false,
|
||||
"build_base": true,
|
||||
"watch_patterns": [
|
||||
"*.c",
|
||||
"*.cp",
|
||||
"*.cpp",
|
||||
"*.cxx",
|
||||
"*.h",
|
||||
"*.hp",
|
||||
"*.hpp",
|
||||
"*.py"
|
||||
"*.hxx",
|
||||
"*.s",
|
||||
"*.S",
|
||||
"*.asm",
|
||||
"*.inc",
|
||||
"*.py",
|
||||
"*.yml",
|
||||
"*.txt",
|
||||
"*.json"
|
||||
],
|
||||
"objects": [
|
||||
"units": [
|
||||
{
|
||||
"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",
|
||||
"base_path": "build/src/MetroTRK/mslsupp.o",
|
||||
|
||||
"reverse_fn_order": false
|
||||
},
|
||||
// ...
|
||||
"metadata": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Schema
|
||||
|
||||
View [config.schema.json](config.schema.json) for all available options. The below list is a summary of the most important options.
|
||||
|
||||
`custom_make` _(optional)_: By default, objdiff will use `make` to build the project.
|
||||
If the project uses a different build system (e.g. `ninja`), specify it here.
|
||||
The build command will be `[custom_make] [custom_args] path/to/object.o`.
|
||||
|
||||
`custom_args` _(optional)_: Additional arguments to pass to the build command prior to the object path.
|
||||
|
||||
`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.
|
||||
`make path/to/target.o`).
|
||||
This is useful if the target objects are not built by default or can change based on project configuration or edits
|
||||
to assembly files.
|
||||
Requires the build system to be configured properly.
|
||||
|
||||
`build_base`: If true, objdiff will tell the build system to build the base objects before diffing (e.g. `make path/to/base.o`).
|
||||
It's unlikely you'll want to disable this, unless you're using an external tool to rebuild the base object on source file changes.
|
||||
|
||||
`watch_patterns` _(optional)_: A list of glob patterns to watch for changes.
|
||||
([Supported syntax](https://docs.rs/globset/latest/globset/#syntax))
|
||||
If any of these files change, objdiff will automatically rebuild the objects and re-compare them.
|
||||
If not specified, objdiff will use the default patterns listed above.
|
||||
|
||||
`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.
|
||||
>
|
||||
> `path`: Relative path to the object from the `target_dir` and `base_dir`.
|
||||
> Requires `target_dir` and `base_dir` to be specified.
|
||||
> `target_path`: Path to the "target" or "expected" object from the project root.
|
||||
> This object is the **intended result** of the match.
|
||||
>
|
||||
> `target_path`: Path to the target object from the project root.
|
||||
> Required if `path` is not specified.
|
||||
> `base_path`: Path to the "base" or "actual" object from the project root.
|
||||
> This object is built from the **current source code**.
|
||||
>
|
||||
> `base_path`: Path to the base object from the project root.
|
||||
> Required if `path` is not specified.
|
||||
> `metadata.auto_generated` _(optional)_: Hides the object from the object list, but still includes it in reports.
|
||||
>
|
||||
> `reverse_fn_order` _(optional)_: Displays function symbols in reversed order.
|
||||
Used to support MWCC's `-inline deferred` option, which reverses the order of functions in the object file.
|
||||
> `metadata.complete` _(optional)_: Marks the object as "complete" (or "linked") in the object list.
|
||||
> This is useful for marking objects that are fully decompiled. A value of `false` will mark the object as "incomplete".
|
||||
|
||||
## Building
|
||||
|
||||
@@ -143,16 +162,22 @@ Install Rust via [rustup](https://rustup.rs).
|
||||
$ git clone https://github.com/encounter/objdiff.git
|
||||
$ cd objdiff
|
||||
$ cargo run --release
|
||||
# or, for wgpu backend (recommended on macOS)
|
||||
$ cargo run --release --features wgpu
|
||||
```
|
||||
|
||||
Or using `cargo install`.
|
||||
|
||||
```shell
|
||||
$ cargo install --locked --git https://github.com/encounter/objdiff.git objdiff-gui objdiff-cli
|
||||
```
|
||||
|
||||
The binaries will be installed to `~/.cargo/bin` as `objdiff` and `objdiff-cli`.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of
|
||||
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
||||
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>)
|
||||
- MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
|
||||
|
||||
at your option.
|
||||
|
||||
|
||||
228
config.schema.json
Normal file
228
config.schema.json
Normal file
@@ -0,0 +1,228 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft-07/schema",
|
||||
"$id": "https://raw.githubusercontent.com/encounter/objdiff/main/config.schema.json",
|
||||
"title": "objdiff configuration",
|
||||
"description": "Configuration file for objdiff",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"min_version": {
|
||||
"type": "string",
|
||||
"description": "Minimum version of objdiff required to load this configuration file.",
|
||||
"examples": [
|
||||
"1.0.0",
|
||||
"2.0.0-beta.1"
|
||||
]
|
||||
},
|
||||
"custom_make": {
|
||||
"type": "string",
|
||||
"description": "By default, objdiff will use make to build the project.\nIf the project uses a different build system (e.g. ninja), specify it here.\nThe build command will be `[custom_make] [custom_args] path/to/object.o`.",
|
||||
"examples": [
|
||||
"make",
|
||||
"ninja"
|
||||
],
|
||||
"default": "make"
|
||||
},
|
||||
"custom_args": {
|
||||
"type": "array",
|
||||
"description": "Additional arguments to pass to the build command prior to the object path.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"target_dir": {
|
||||
"type": "string",
|
||||
"description": "Relative from the root of the project, this where the \"target\" or \"expected\" objects are located.\nThese are the intended result of the match.",
|
||||
"deprecated": true
|
||||
},
|
||||
"base_dir": {
|
||||
"type": "string",
|
||||
"description": "Relative from the root of the project, this is where the \"base\" or \"actual\" objects are located.\nThese are objects built from the current source code.",
|
||||
"deprecated": true
|
||||
},
|
||||
"build_target": {
|
||||
"type": "boolean",
|
||||
"description": "If true, objdiff will tell the build system to build the target objects before diffing (e.g. `make path/to/target.o`).\nThis is useful if the target objects are not built by default or can change based on project configuration or edits to assembly files.\nRequires the build system to be configured properly.",
|
||||
"default": false
|
||||
},
|
||||
"build_base": {
|
||||
"type": "boolean",
|
||||
"description": "If true, objdiff will tell the build system to build the base objects before diffing (e.g. `make path/to/base.o`).\nIt's unlikely you'll want to disable this, unless you're using an external tool to rebuild the base object on source file changes.",
|
||||
"default": true
|
||||
},
|
||||
"watch_patterns": {
|
||||
"type": "array",
|
||||
"description": "List of glob patterns to watch for changes in the project.\nIf any of these files change, objdiff will automatically rebuild the objects and re-compare them.\nSupported syntax: https://docs.rs/globset/latest/globset/#syntax",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": [
|
||||
"*.c",
|
||||
"*.cp",
|
||||
"*.cpp",
|
||||
"*.cxx",
|
||||
"*.h",
|
||||
"*.hp",
|
||||
"*.hpp",
|
||||
"*.hxx",
|
||||
"*.s",
|
||||
"*.S",
|
||||
"*.asm",
|
||||
"*.inc",
|
||||
"*.py",
|
||||
"*.yml",
|
||||
"*.txt",
|
||||
"*.json"
|
||||
]
|
||||
},
|
||||
"objects": {
|
||||
"type": "array",
|
||||
"description": "Use units instead.",
|
||||
"deprecated": true,
|
||||
"items": {
|
||||
"$ref": "#/$defs/unit"
|
||||
}
|
||||
},
|
||||
"units": {
|
||||
"type": "array",
|
||||
"description": "If specified, objdiff will display a list of objects in the sidebar for easy navigation.",
|
||||
"items": {
|
||||
"$ref": "#/$defs/unit"
|
||||
}
|
||||
},
|
||||
"progress_categories": {
|
||||
"type": "array",
|
||||
"description": "Progress categories used for objdiff-cli report.",
|
||||
"items": {
|
||||
"$ref": "#/$defs/progress_category"
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"unit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the object in the UI. If not specified, the object's path will be used."
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Relative path to the object from the target_dir and base_dir.\nRequires target_dir and base_dir to be specified.",
|
||||
"deprecated": true
|
||||
},
|
||||
"target_path": {
|
||||
"type": "string",
|
||||
"description": "Path to the target object from the project root.\nRequired if path is not specified."
|
||||
},
|
||||
"base_path": {
|
||||
"type": "string",
|
||||
"description": "Path to the base object from the project root.\nRequired if path is not specified."
|
||||
},
|
||||
"reverse_fn_order": {
|
||||
"type": "boolean",
|
||||
"description": "Displays function symbols in reversed order.\nUsed to support MWCC's -inline deferred option, which reverses the order of functions in the object file.",
|
||||
"deprecated": true
|
||||
},
|
||||
"complete": {
|
||||
"type": "boolean",
|
||||
"description": "Marks the object as \"complete\" (or \"linked\") in the object list.\nThis is useful for marking objects that are fully decompiled. A value of `false` will mark the object as \"incomplete\".",
|
||||
"deprecated": true
|
||||
},
|
||||
"scratch": {
|
||||
"ref": "#/$defs/scratch"
|
||||
},
|
||||
"metadata": {
|
||||
"ref": "#/$defs/metadata"
|
||||
},
|
||||
"symbol_mappings": {
|
||||
"type": "object",
|
||||
"description": "Manual symbol mappings from target to base.",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scratch": {
|
||||
"type": "object",
|
||||
"description": "If present, objdiff will display a button to create a decomp.me scratch.",
|
||||
"properties": {
|
||||
"platform": {
|
||||
"type": "string",
|
||||
"description": "The decomp.me platform ID to use for the scratch.",
|
||||
"examples": [
|
||||
"gc_wii",
|
||||
"n64"
|
||||
]
|
||||
},
|
||||
"compiler": {
|
||||
"type": "string",
|
||||
"description": "The decomp.me compiler ID to use for the scratch.",
|
||||
"examples": [
|
||||
"mwcc_242_81",
|
||||
"ido7.1"
|
||||
]
|
||||
},
|
||||
"c_flags": {
|
||||
"type": "string",
|
||||
"description": "C flags to use for the scratch. Exclude any include paths."
|
||||
},
|
||||
"ctx_path": {
|
||||
"type": "string",
|
||||
"description": "Path to the context file to use for the scratch."
|
||||
},
|
||||
"build_ctx": {
|
||||
"type": "boolean",
|
||||
"description": "If true, objdiff will run the build command with the context file as an argument to generate it.",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"platform",
|
||||
"compiler"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"complete": {
|
||||
"type": "boolean",
|
||||
"description": "Marks the object as \"complete\" (or \"linked\") in the object list.\nThis is useful for marking objects that are fully decompiled. A value of `false` will mark the object as \"incomplete\"."
|
||||
},
|
||||
"reverse_fn_order": {
|
||||
"type": "boolean",
|
||||
"description": "Displays function symbols in reversed order.\nUsed to support MWCC's -inline deferred option, which reverses the order of functions in the object file."
|
||||
},
|
||||
"source_path": {
|
||||
"type": "string",
|
||||
"description": "Path to the source file that generated the object."
|
||||
},
|
||||
"progress_categories": {
|
||||
"type": "array",
|
||||
"description": "Progress categories used for objdiff-cli report.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier for the category. (See progress_categories)"
|
||||
}
|
||||
},
|
||||
"auto_generated": {
|
||||
"type": "boolean",
|
||||
"description": "Hides the object from the object list by default, but still includes it in reports."
|
||||
}
|
||||
}
|
||||
},
|
||||
"progress_category": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier for the category."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Human-readable name of the category."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
169
deny.toml
169
deny.toml
@@ -9,6 +9,11 @@
|
||||
# The values provided in this template are the default values that will be used
|
||||
# when any section or field is not specified in your own configuration
|
||||
|
||||
# Root options
|
||||
|
||||
# The graph table configures how the dependency graph is constructed and thus
|
||||
# which crates the checks are performed against
|
||||
[graph]
|
||||
# If 1 or more target triples (and optionally, target_features) are specified,
|
||||
# only the specified targets will be checked when running `cargo deny check`.
|
||||
# This means, if a particular package is only ever used as a target specific
|
||||
@@ -20,51 +25,67 @@
|
||||
targets = [
|
||||
# The triple can be any string, but only the target triples built in to
|
||||
# rustc (as of 1.40) can be checked against actual config expressions
|
||||
#{ triple = "x86_64-unknown-linux-musl" },
|
||||
#"x86_64-unknown-linux-musl",
|
||||
# You can also specify which target_features you promise are enabled for a
|
||||
# particular target. target_features are currently not validated against
|
||||
# the actual valid features supported by the target architecture.
|
||||
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
|
||||
]
|
||||
# When creating the dependency graph used as the source of truth when checks are
|
||||
# executed, this field can be used to prune crates from the graph, removing them
|
||||
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
|
||||
# is pruned from the graph, all of its dependencies will also be pruned unless
|
||||
# they are connected to another crate in the graph that hasn't been pruned,
|
||||
# so it should be used with care. The identifiers are [Package ID Specifications]
|
||||
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
|
||||
#exclude = []
|
||||
# If true, metadata will be collected with `--all-features`. Note that this can't
|
||||
# be toggled off if true, if you want to conditionally enable `--all-features` it
|
||||
# is recommended to pass `--all-features` on the cmd line instead
|
||||
all-features = false
|
||||
# If true, metadata will be collected with `--no-default-features`. The same
|
||||
# caveat with `all-features` applies
|
||||
no-default-features = false
|
||||
# If set, these feature will be enabled when collecting metadata. If `--features`
|
||||
# is specified on the cmd line they will take precedence over this option.
|
||||
#features = []
|
||||
|
||||
# The output table provides options for how/if diagnostics are outputted
|
||||
[output]
|
||||
# When outputting inclusion graphs in diagnostics that include features, this
|
||||
# option can be used to specify the depth at which feature edges will be added.
|
||||
# This option is included since the graphs can be quite large and the addition
|
||||
# of features from the crate(s) to all of the graph roots can be far too verbose.
|
||||
# This option can be overridden via `--feature-depth` on the cmd line
|
||||
feature-depth = 1
|
||||
|
||||
# This section is considered when running `cargo deny check advisories`
|
||||
# More documentation for the advisories section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
|
||||
[advisories]
|
||||
# The path where the advisory database is cloned/fetched into
|
||||
db-path = "~/.cargo/advisory-db"
|
||||
# The path where the advisory databases are cloned/fetched into
|
||||
#db-path = "$CARGO_HOME/advisory-dbs"
|
||||
# The url(s) of the advisory databases to use
|
||||
db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||
# The lint level for security vulnerabilities
|
||||
vulnerability = "deny"
|
||||
# The lint level for unmaintained crates
|
||||
unmaintained = "warn"
|
||||
# The lint level for crates that have been yanked from their source registry
|
||||
yanked = "warn"
|
||||
# The lint level for crates with security notices. Note that as of
|
||||
# 2019-12-17 there are no security notice advisories in
|
||||
# https://github.com/rustsec/advisory-db
|
||||
notice = "warn"
|
||||
#db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||
# output a note when they are encountered.
|
||||
ignore = []
|
||||
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
|
||||
# lower than the range specified will be ignored. Note that ignored advisories
|
||||
# will still output a note when they are encountered.
|
||||
# * None - CVSS Score 0.0
|
||||
# * Low - CVSS Score 0.1 - 3.9
|
||||
# * Medium - CVSS Score 4.0 - 6.9
|
||||
# * High - CVSS Score 7.0 - 8.9
|
||||
# * Critical - CVSS Score 9.0 - 10.0
|
||||
#severity-threshold =
|
||||
ignore = [
|
||||
"RUSTSEC-2024-0370",
|
||||
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
|
||||
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
|
||||
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
|
||||
]
|
||||
# If this is true, then cargo deny will use the git executable to fetch advisory database.
|
||||
# If this is false, then it uses a built-in git library.
|
||||
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
|
||||
# See Git Authentication for more information about setting up git authentication.
|
||||
#git-fetch-with-cli = true
|
||||
|
||||
# This section is considered when running `cargo deny check licenses`
|
||||
# More documentation for the licenses section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
|
||||
[licenses]
|
||||
# The lint level for crates which do not have a detectable license
|
||||
unlicensed = "deny"
|
||||
# List of explictly allowed licenses
|
||||
# List of explicitly allowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||
allow = [
|
||||
@@ -83,28 +104,7 @@ allow = [
|
||||
"OFL-1.1",
|
||||
"LicenseRef-UFL-1.0",
|
||||
"OpenSSL",
|
||||
"GPL-3.0",
|
||||
]
|
||||
# List of explictly disallowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||
deny = [
|
||||
#"Nokia",
|
||||
]
|
||||
# Lint level for licenses considered copyleft
|
||||
copyleft = "warn"
|
||||
# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
|
||||
# * both - The license will be approved if it is both OSI-approved *AND* FSF
|
||||
# * either - The license will be approved if it is either OSI-approved *OR* FSF
|
||||
# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
|
||||
# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
|
||||
# * neither - This predicate is ignored and the default lint level is used
|
||||
allow-osi-fsf-free = "neither"
|
||||
# Lint level used when no other predicates are matched
|
||||
# 1. License isn't in the allow or deny lists
|
||||
# 2. License isn't copyleft
|
||||
# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
|
||||
default = "deny"
|
||||
# The confidence threshold for detecting a license from license text.
|
||||
# The higher the value, the more closely the license text must be to the
|
||||
# canonical license text of a valid SPDX license file.
|
||||
@@ -115,17 +115,15 @@ confidence-threshold = 0.8
|
||||
exceptions = [
|
||||
# Each entry is the crate and version constraint, and its specific allow
|
||||
# list
|
||||
#{ allow = ["Zlib"], name = "adler32", version = "*" },
|
||||
#{ allow = ["Zlib"], crate = "adler32" },
|
||||
]
|
||||
|
||||
# Some crates don't have (easily) machine readable licensing information,
|
||||
# adding a clarification entry for it allows you to manually specify the
|
||||
# licensing information
|
||||
[[licenses.clarify]]
|
||||
# The name of the crate the clarification applies to
|
||||
name = "ring"
|
||||
# The optional version constraint for the crate
|
||||
version = "*"
|
||||
# The package spec the clarification applies to
|
||||
crate = "ring"
|
||||
# The SPDX expression for the license requirements of the crate
|
||||
expression = "MIT AND ISC AND OpenSSL"
|
||||
# One or more files in the crate's source used as the "source of truth" for
|
||||
@@ -140,7 +138,9 @@ license-files = [
|
||||
|
||||
[licenses.private]
|
||||
# If true, ignores workspace crates that aren't published, or are only
|
||||
# published to private registries
|
||||
# published to private registries.
|
||||
# To see how to mark a crate as unpublished (to the official registry),
|
||||
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
|
||||
ignore = false
|
||||
# One or more private registries that you might publish crates to, if a crate
|
||||
# is only published to private registries, and ignore is true, the crate will
|
||||
@@ -163,30 +163,63 @@ wildcards = "allow"
|
||||
# * simplest-path - The path to the version with the fewest edges is highlighted
|
||||
# * all - Both lowest-version and simplest-path are used
|
||||
highlight = "all"
|
||||
# The default lint level for `default` features for crates that are members of
|
||||
# the workspace that is being checked. This can be overridden by allowing/denying
|
||||
# `default` on a crate-by-crate basis if desired.
|
||||
workspace-default-features = "allow"
|
||||
# The default lint level for `default` features for external crates that are not
|
||||
# members of the workspace. This can be overridden by allowing/denying `default`
|
||||
# on a crate-by-crate basis if desired.
|
||||
external-default-features = "allow"
|
||||
# List of crates that are allowed. Use with care!
|
||||
allow = [
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
|
||||
]
|
||||
# List of crates to deny
|
||||
deny = [
|
||||
# Each entry the name of a crate and a version range. If version is
|
||||
# not specified, all versions will be matched.
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
#
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
|
||||
# Wrapper crates can optionally be specified to allow the crate when it
|
||||
# is a direct dependency of the otherwise banned crate
|
||||
#{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
|
||||
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
|
||||
]
|
||||
|
||||
# List of features to allow/deny
|
||||
# Each entry the name of a crate and a version range. If version is
|
||||
# not specified, all versions will be matched.
|
||||
#[[bans.features]]
|
||||
#crate = "reqwest"
|
||||
# Features to not allow
|
||||
#deny = ["json"]
|
||||
# Features to allow
|
||||
#allow = [
|
||||
# "rustls",
|
||||
# "__rustls",
|
||||
# "__tls",
|
||||
# "hyper-rustls",
|
||||
# "rustls",
|
||||
# "rustls-pemfile",
|
||||
# "rustls-tls-webpki-roots",
|
||||
# "tokio-rustls",
|
||||
# "webpki-roots",
|
||||
#]
|
||||
# If true, the allowed features must exactly match the enabled feature set. If
|
||||
# this is set there is no point setting `deny`
|
||||
#exact = true
|
||||
|
||||
# Certain crates/versions that will be skipped when doing duplicate detection.
|
||||
skip = [
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
|
||||
]
|
||||
# Similarly to `skip` allows you to skip certain crates during duplicate
|
||||
# detection. Unlike skip, it also includes the entire tree of transitive
|
||||
# dependencies starting at the specified crate, up to a certain depth, which is
|
||||
# by default infinite
|
||||
# by default infinite.
|
||||
skip-tree = [
|
||||
#{ name = "ansi_term", version = "=0.11.0", depth = 20 },
|
||||
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
|
||||
#{ crate = "ansi_term@0.11.0", depth = 20 },
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check sources`.
|
||||
@@ -206,9 +239,9 @@ allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
allow-git = []
|
||||
|
||||
[sources.allow-org]
|
||||
# 1 or more github.com organizations to allow git sources for
|
||||
# github.com organizations to allow git sources for
|
||||
github = ["encounter"]
|
||||
# 1 or more gitlab.com organizations to allow git sources for
|
||||
#gitlab = [""]
|
||||
# 1 or more bitbucket.org organizations to allow git sources for
|
||||
#bitbucket = [""]
|
||||
# gitlab.com organizations to allow git sources for
|
||||
gitlab = []
|
||||
# bitbucket.org organizations to allow git sources for
|
||||
bitbucket = []
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
[package]
|
||||
name = "objdiff-cli"
|
||||
version = "2.0.0-beta.5"
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/encounter/objdiff"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
readme = "../README.md"
|
||||
description = """
|
||||
A local diffing tool for decompilation projects.
|
||||
"""
|
||||
publish = false
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.82"
|
||||
argp = "0.3.0"
|
||||
crossterm = "0.27.0"
|
||||
enable-ansi-support = "0.2.1"
|
||||
memmap2 = "0.9.4"
|
||||
anyhow = "1.0"
|
||||
argp = "0.3"
|
||||
crossterm = "0.28"
|
||||
enable-ansi-support = "0.2"
|
||||
memmap2 = "0.9"
|
||||
objdiff-core = { path = "../objdiff-core", features = ["all"] }
|
||||
prost = "0.13.1"
|
||||
ratatui = "0.26.2"
|
||||
rayon = "1.10.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1.0.116"
|
||||
supports-color = "3.0.0"
|
||||
time = { version = "0.3.36", features = ["formatting", "local-offset"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
prost = "0.13"
|
||||
ratatui = "0.28"
|
||||
rayon = "1.10"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
supports-color = "3.0"
|
||||
time = { version = "0.3", features = ["formatting", "local-offset"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
[target.'cfg(target_env = "musl")'.dependencies]
|
||||
mimalloc = "0.1"
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
fn main() {
|
||||
let output = std::process::Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.output()
|
||||
.expect("Failed to execute git");
|
||||
let rev = String::from_utf8(output.stdout).expect("Failed to parse git output");
|
||||
println!("cargo:rustc-env=GIT_COMMIT_SHA={rev}");
|
||||
println!("cargo:rustc-rerun-if-changed=.git/HEAD");
|
||||
}
|
||||
@@ -31,10 +31,9 @@ where T: FromArgs
|
||||
Ok(v) => {
|
||||
if v.version {
|
||||
println!(
|
||||
"{} {} {}",
|
||||
"{} {}",
|
||||
command_name.first().unwrap_or(&""),
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
env!("GIT_COMMIT_SHA"),
|
||||
);
|
||||
std::process::exit(0);
|
||||
} else {
|
||||
|
||||
@@ -102,26 +102,32 @@ pub fn run(args: Args) -> Result<()> {
|
||||
let unit_path =
|
||||
PathBuf::from_str(u).ok().and_then(|p| fs::canonicalize(p).ok());
|
||||
|
||||
let Some(object) = project_config.objects.iter_mut().find_map(|obj| {
|
||||
if obj.name.as_deref() == Some(u) {
|
||||
let Some(object) = project_config
|
||||
.units
|
||||
.as_deref_mut()
|
||||
.unwrap_or_default()
|
||||
.iter_mut()
|
||||
.find_map(|obj| {
|
||||
if obj.name.as_deref() == Some(u) {
|
||||
resolve_paths(obj);
|
||||
return Some(obj);
|
||||
}
|
||||
|
||||
let up = unit_path.as_deref()?;
|
||||
|
||||
resolve_paths(obj);
|
||||
return Some(obj);
|
||||
}
|
||||
|
||||
let up = unit_path.as_deref()?;
|
||||
if [&obj.base_path, &obj.target_path]
|
||||
.into_iter()
|
||||
.filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok()))
|
||||
.any(|p| p == up)
|
||||
{
|
||||
return Some(obj);
|
||||
}
|
||||
|
||||
resolve_paths(obj);
|
||||
|
||||
if [&obj.base_path, &obj.target_path]
|
||||
.into_iter()
|
||||
.filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok()))
|
||||
.any(|p| p == up)
|
||||
{
|
||||
return Some(obj);
|
||||
}
|
||||
|
||||
None
|
||||
}) else {
|
||||
None
|
||||
})
|
||||
else {
|
||||
bail!("Unit not found: {}", u)
|
||||
};
|
||||
|
||||
@@ -129,7 +135,13 @@ pub fn run(args: Args) -> Result<()> {
|
||||
} else if let Some(symbol_name) = &args.symbol {
|
||||
let mut idx = None;
|
||||
let mut count = 0usize;
|
||||
for (i, obj) in project_config.objects.iter_mut().enumerate() {
|
||||
for (i, obj) in project_config
|
||||
.units
|
||||
.as_deref_mut()
|
||||
.unwrap_or_default()
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
{
|
||||
resolve_paths(obj);
|
||||
|
||||
if obj
|
||||
@@ -148,7 +160,7 @@ pub fn run(args: Args) -> Result<()> {
|
||||
}
|
||||
match (count, idx) {
|
||||
(0, None) => bail!("Symbol not found: {}", symbol_name),
|
||||
(1, Some(i)) => &mut project_config.objects[i],
|
||||
(1, Some(i)) => &mut project_config.units_mut()[i],
|
||||
(2.., Some(_)) => bail!(
|
||||
"Multiple instances of {} were found, try specifying a unit",
|
||||
symbol_name
|
||||
@@ -303,7 +315,7 @@ fn find_function(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
|
||||
None
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
struct FunctionDiffUi {
|
||||
relax_reloc_diffs: bool,
|
||||
left_highlight: HighlightKind,
|
||||
@@ -345,7 +357,7 @@ enum EventControlFlow {
|
||||
|
||||
impl FunctionDiffUi {
|
||||
fn draw(&mut self, f: &mut Frame, result: &mut EventResult) {
|
||||
let chunks = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).split(f.size());
|
||||
let chunks = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).split(f.area());
|
||||
let header_chunks = Layout::horizontal([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(3),
|
||||
@@ -415,7 +427,7 @@ impl FunctionDiffUi {
|
||||
get_symbol_diff(self.diff_result.left.as_ref(), self.left_sym),
|
||||
) {
|
||||
let mut text = Text::default();
|
||||
let rect = content_chunks[0].inner(&Margin::new(0, 1));
|
||||
let rect = content_chunks[0].inner(Margin::new(0, 1));
|
||||
left_highlight = self.print_sym(
|
||||
&mut text,
|
||||
symbol,
|
||||
@@ -437,7 +449,7 @@ impl FunctionDiffUi {
|
||||
get_symbol_diff(self.diff_result.right.as_ref(), self.right_sym),
|
||||
) {
|
||||
let mut text = Text::default();
|
||||
let rect = content_chunks[2].inner(&Margin::new(0, 1));
|
||||
let rect = content_chunks[2].inner(Margin::new(0, 1));
|
||||
right_highlight = self.print_sym(
|
||||
&mut text,
|
||||
symbol,
|
||||
@@ -452,7 +464,7 @@ impl FunctionDiffUi {
|
||||
|
||||
// Render margin
|
||||
let mut text = Text::default();
|
||||
let rect = content_chunks[1].inner(&Margin::new(1, 1));
|
||||
let rect = content_chunks[1].inner(Margin::new(1, 1));
|
||||
self.print_margin(&mut text, symbol_diff, rect);
|
||||
margin_text = Some(text);
|
||||
}
|
||||
@@ -465,7 +477,7 @@ impl FunctionDiffUi {
|
||||
get_symbol_diff(self.diff_result.prev.as_ref(), self.prev_sym),
|
||||
) {
|
||||
let mut text = Text::default();
|
||||
let rect = content_chunks[4].inner(&Margin::new(0, 1));
|
||||
let rect = content_chunks[4].inner(Margin::new(0, 1));
|
||||
self.print_sym(
|
||||
&mut text,
|
||||
symbol,
|
||||
@@ -480,7 +492,7 @@ impl FunctionDiffUi {
|
||||
|
||||
// Render margin
|
||||
let mut text = Text::default();
|
||||
let rect = content_chunks[3].inner(&Margin::new(1, 1));
|
||||
let rect = content_chunks[3].inner(Margin::new(1, 1));
|
||||
self.print_margin(&mut text, symbol_diff, rect);
|
||||
prev_margin_text = Some(text);
|
||||
}
|
||||
@@ -498,18 +510,30 @@ impl FunctionDiffUi {
|
||||
// Render left column
|
||||
f.render_widget(
|
||||
Paragraph::new(text)
|
||||
.block(Block::new().borders(Borders::TOP).gray().title("TARGET".bold()))
|
||||
.block(
|
||||
Block::new()
|
||||
.borders(Borders::TOP)
|
||||
.border_style(Style::new().fg(Color::Gray))
|
||||
.title_style(Style::new().bold())
|
||||
.title("TARGET"),
|
||||
)
|
||||
.scroll((0, self.scroll_x as u16)),
|
||||
content_chunks[0],
|
||||
);
|
||||
}
|
||||
if let Some(text) = margin_text {
|
||||
f.render_widget(text, content_chunks[1].inner(&Margin::new(1, 1)));
|
||||
f.render_widget(text, content_chunks[1].inner(Margin::new(1, 1)));
|
||||
}
|
||||
if let Some(text) = right_text {
|
||||
f.render_widget(
|
||||
Paragraph::new(text)
|
||||
.block(Block::new().borders(Borders::TOP).gray().title("CURRENT".bold()))
|
||||
.block(
|
||||
Block::new()
|
||||
.borders(Borders::TOP)
|
||||
.border_style(Style::new().fg(Color::Gray))
|
||||
.title_style(Style::new().bold())
|
||||
.title("CURRENT"),
|
||||
)
|
||||
.scroll((0, self.scroll_x as u16)),
|
||||
content_chunks[2],
|
||||
);
|
||||
@@ -517,9 +541,13 @@ impl FunctionDiffUi {
|
||||
|
||||
if self.three_way {
|
||||
if let Some(text) = prev_margin_text {
|
||||
f.render_widget(text, content_chunks[3].inner(&Margin::new(1, 1)));
|
||||
f.render_widget(text, content_chunks[3].inner(Margin::new(1, 1)));
|
||||
}
|
||||
let block = Block::new().borders(Borders::TOP).gray().title("SAVED".bold());
|
||||
let block = Block::new()
|
||||
.borders(Borders::TOP)
|
||||
.border_style(Style::new().fg(Color::Gray))
|
||||
.title_style(Style::new().bold())
|
||||
.title("SAVED");
|
||||
if let Some(text) = prev_text {
|
||||
f.render_widget(
|
||||
Paragraph::new(text).block(block.clone()).scroll((0, self.scroll_x as u16)),
|
||||
@@ -533,7 +561,7 @@ impl FunctionDiffUi {
|
||||
// Render scrollbars
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::new(ScrollbarOrientation::VerticalRight).begin_symbol(None).end_symbol(None),
|
||||
chunks[1].inner(&Margin::new(0, 1)),
|
||||
chunks[1].inner(Margin::new(0, 1)),
|
||||
&mut self.scroll_state_y,
|
||||
);
|
||||
f.render_stateful_widget(
|
||||
@@ -589,7 +617,7 @@ impl FunctionDiffUi {
|
||||
Constraint::Percentage(percent_y),
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
])
|
||||
.split(f.size())[1];
|
||||
.split(f.area())[1];
|
||||
let popup_rect = Layout::horizontal([
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
Constraint::Percentage(percent_x),
|
||||
@@ -742,7 +770,7 @@ impl FunctionDiffUi {
|
||||
self.scroll_y += self.per_page / if half { 2 } else { 1 };
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
fn print_sym(
|
||||
&self,
|
||||
out: &mut Text<'static>,
|
||||
|
||||
@@ -87,13 +87,14 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||
let project_dir = args.project.as_deref().unwrap_or_else(|| Path::new("."));
|
||||
info!("Loading project {}", project_dir.display());
|
||||
|
||||
let config = objdiff_core::config::try_project_config(project_dir);
|
||||
let Some((Ok(mut project), _)) = config else {
|
||||
bail!("No project configuration found");
|
||||
let mut project = match objdiff_core::config::try_project_config(project_dir) {
|
||||
Some((Ok(config), _)) => config,
|
||||
Some((Err(err), _)) => bail!("Failed to load project configuration: {}", err),
|
||||
None => bail!("No project configuration found"),
|
||||
};
|
||||
info!(
|
||||
"Generating report for {} units (using {} threads)",
|
||||
project.objects.len(),
|
||||
project.units().len(),
|
||||
if args.deduplicate { 1 } else { rayon::current_num_threads() }
|
||||
);
|
||||
|
||||
@@ -102,7 +103,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||
let mut existing_functions: HashSet<String> = HashSet::new();
|
||||
if args.deduplicate {
|
||||
// If deduplicating, we need to run single-threaded
|
||||
for object in &mut project.objects {
|
||||
for object in project.units.as_deref_mut().unwrap_or_default() {
|
||||
if let Some(unit) = report_object(
|
||||
object,
|
||||
project_dir,
|
||||
@@ -115,7 +116,9 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||
}
|
||||
} else {
|
||||
let vec = project
|
||||
.objects
|
||||
.units
|
||||
.as_deref_mut()
|
||||
.unwrap_or_default()
|
||||
.par_iter_mut()
|
||||
.map(|object| {
|
||||
report_object(
|
||||
@@ -131,7 +134,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||
}
|
||||
let measures = units.iter().flat_map(|u| u.measures.into_iter()).collect();
|
||||
let mut categories = Vec::new();
|
||||
for category in &project.progress_categories {
|
||||
for category in project.progress_categories() {
|
||||
categories.push(ReportCategory {
|
||||
id: category.id.clone(),
|
||||
name: category.name.clone(),
|
||||
@@ -198,7 +201,7 @@ fn report_object(
|
||||
.unwrap_or_default(),
|
||||
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 functions = vec![];
|
||||
|
||||
@@ -236,7 +239,7 @@ fn report_object(
|
||||
}
|
||||
|
||||
for (symbol, symbol_diff) in section.symbols.iter().zip(§ion_diff.symbols) {
|
||||
if symbol.size == 0 {
|
||||
if symbol.size == 0 || symbol.flags.0.contains(ObjSymbolFlags::Hidden) {
|
||||
continue;
|
||||
}
|
||||
if let Some(existing_functions) = &mut existing_functions {
|
||||
@@ -279,6 +282,7 @@ fn report_object(
|
||||
if metadata.complete.unwrap_or(false) {
|
||||
measures.complete_code = measures.total_code;
|
||||
measures.complete_data = measures.total_data;
|
||||
measures.complete_units = 1;
|
||||
}
|
||||
measures.calc_fuzzy_match_percent();
|
||||
measures.calc_matched_percent();
|
||||
|
||||
@@ -2,6 +2,12 @@ mod argp_version;
|
||||
mod cmd;
|
||||
mod util;
|
||||
|
||||
// musl's allocator is very slow, so use mimalloc when targeting musl.
|
||||
// Otherwise, use the system allocator to avoid extra code size.
|
||||
#[cfg(target_env = "musl")]
|
||||
#[global_allocator]
|
||||
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
|
||||
use std::{env, ffi::OsStr, fmt::Display, path::PathBuf, str::FromStr};
|
||||
|
||||
use anyhow::{Error, Result};
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
[package]
|
||||
name = "objdiff-core"
|
||||
version = "2.0.0-beta.5"
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/encounter/objdiff"
|
||||
readme = "../README.md"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
readme = "README.md"
|
||||
description = """
|
||||
A local diffing tool for decompilation projects.
|
||||
"""
|
||||
documentation = "https://docs.rs/objdiff-core"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "bindings"]
|
||||
any-arch = [] # Implicit, used to check if any arch is enabled
|
||||
config = ["globset", "semver", "serde_json", "serde_yaml"]
|
||||
any-arch = ["bimap"] # Implicit, used to check if any arch is enabled
|
||||
config = ["bimap", "globset", "semver", "serde_json", "serde_yaml"]
|
||||
dwarf = ["gimli"]
|
||||
mips = ["any-arch", "rabbitizer"]
|
||||
ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"]
|
||||
@@ -26,51 +27,55 @@ arm = ["any-arch", "cpp_demangle", "unarm", "arm-attr"]
|
||||
bindings = ["serde_json", "prost", "pbjson"]
|
||||
wasm = ["bindings", "console_error_panic_hook", "console_log"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["all"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.82"
|
||||
byteorder = "1.5.0"
|
||||
filetime = "0.2.23"
|
||||
flagset = "0.4.5"
|
||||
log = "0.4.21"
|
||||
memmap2 = "0.9.4"
|
||||
num-traits = "0.2.18"
|
||||
object = { version = "0.35.0", features = ["read_core", "std", "elf", "pe"], default-features = false }
|
||||
pbjson = { version = "0.7.0", optional = true }
|
||||
prost = { version = "0.13.1", optional = true }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
similar = { version = "2.5.0", default-features = false }
|
||||
strum = { version = "0.26.2", features = ["derive"] }
|
||||
wasm-bindgen = "0.2.93"
|
||||
tsify-next = { version = "0.5.4", default-features = false, features = ["js"] }
|
||||
console_log = { version = "1.0.0", optional = true }
|
||||
console_error_panic_hook = { version = "0.1.7", optional = true }
|
||||
anyhow = "1.0"
|
||||
bimap = { version = "0.6", features = ["serde"], optional = true }
|
||||
byteorder = "1.5"
|
||||
filetime = "0.2"
|
||||
flagset = "0.4"
|
||||
log = "0.4"
|
||||
memmap2 = "0.9"
|
||||
num-traits = "0.2"
|
||||
object = { version = "0.36", features = ["read_core", "std", "elf", "pe"], default-features = false }
|
||||
pbjson = { version = "0.7", optional = true }
|
||||
prost = { version = "0.13", optional = true }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
similar = { version = "2.6", default-features = false }
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
wasm-bindgen = "0.2"
|
||||
tsify-next = { version = "0.5", default-features = false, features = ["js"] }
|
||||
console_log = { version = "1.0", optional = true }
|
||||
console_error_panic_hook = { version = "0.1", optional = true }
|
||||
|
||||
# config
|
||||
globset = { version = "0.4.14", features = ["serde1"], optional = true }
|
||||
semver = { version = "1.0.22", optional = true }
|
||||
serde_json = { version = "1.0.116", optional = true }
|
||||
serde_yaml = { version = "0.9.34", optional = true }
|
||||
globset = { version = "0.4", features = ["serde1"], optional = true }
|
||||
semver = { version = "1.0", optional = true }
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
serde_yaml = { version = "0.9", optional = true }
|
||||
|
||||
# dwarf
|
||||
gimli = { version = "0.29.0", default-features = false, features = ["read-all"], optional = true }
|
||||
gimli = { version = "0.31", default-features = false, features = ["read-all"], optional = true }
|
||||
|
||||
# ppc
|
||||
cwdemangle = { version = "1.0.0", optional = true }
|
||||
cwextab = { version = "0.2.3", optional = true }
|
||||
ppc750cl = { git = "https://github.com/encounter/ppc750cl", rev = "6cbd7d888c7082c2c860f66cbb9848d633f753ed", optional = true }
|
||||
cwdemangle = { version = "1.0", optional = true }
|
||||
cwextab = { version = "1.0.2", optional = true }
|
||||
ppc750cl = { version = "0.3", optional = true }
|
||||
|
||||
# mips
|
||||
rabbitizer = { version = "1.11.0", optional = true }
|
||||
rabbitizer = { version = "1.12", optional = true }
|
||||
|
||||
# x86
|
||||
cpp_demangle = { version = "0.4.3", optional = true }
|
||||
iced-x86 = { version = "1.21.0", default-features = false, features = ["std", "decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums"], optional = true }
|
||||
msvc-demangler = { version = "0.10.0", optional = true }
|
||||
cpp_demangle = { version = "0.4", optional = true }
|
||||
iced-x86 = { version = "1.21", default-features = false, features = ["std", "decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums"], optional = true }
|
||||
msvc-demangler = { version = "0.10", optional = true }
|
||||
|
||||
# arm
|
||||
unarm = { version = "1.5.0", optional = true }
|
||||
arm-attr = { version = "0.1.1", optional = true }
|
||||
unarm = { version = "1.6", optional = true }
|
||||
arm-attr = { version = "0.1", optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
prost-build = "0.13.1"
|
||||
pbjson-build = "0.7.0"
|
||||
prost-build = "0.13"
|
||||
pbjson-build = "0.7"
|
||||
|
||||
14
objdiff-core/README.md
Normal file
14
objdiff-core/README.md
Normal 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.
|
||||
Binary file not shown.
@@ -32,6 +32,10 @@ message Measures {
|
||||
uint64 complete_data = 13;
|
||||
// Completed (or "linked") data percent
|
||||
float complete_data_percent = 14;
|
||||
// Total number of units
|
||||
uint32 total_units = 15;
|
||||
// Completed (or "linked") units
|
||||
uint32 complete_units = 16;
|
||||
}
|
||||
|
||||
// Project progress report
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use std::{borrow::Cow, collections::BTreeMap};
|
||||
use std::{borrow::Cow, collections::BTreeMap, ffi::CStr};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use byteorder::ByteOrder;
|
||||
use object::{Architecture, File, Object, ObjectSymbol, Relocation, RelocationFlags, Symbol};
|
||||
|
||||
use crate::{
|
||||
diff::DiffObjConfig,
|
||||
obj::{ObjIns, ObjReloc, ObjSection},
|
||||
util::ReallySigned,
|
||||
};
|
||||
|
||||
#[cfg(feature = "arm")]
|
||||
@@ -17,6 +19,97 @@ pub mod ppc;
|
||||
#[cfg(feature = "x86")]
|
||||
pub mod x86;
|
||||
|
||||
/// Represents the type of data associated with an instruction
|
||||
pub enum DataType {
|
||||
Int8,
|
||||
Int16,
|
||||
Int32,
|
||||
Int64,
|
||||
Int128,
|
||||
Float,
|
||||
Double,
|
||||
Bytes,
|
||||
String,
|
||||
}
|
||||
|
||||
impl DataType {
|
||||
pub fn display_bytes<Endian: ByteOrder>(&self, bytes: &[u8]) -> Option<String> {
|
||||
if self.required_len().is_some_and(|l| bytes.len() < l) {
|
||||
return None;
|
||||
}
|
||||
|
||||
match self {
|
||||
DataType::Int8 => {
|
||||
let i = i8::from_ne_bytes(bytes.try_into().unwrap());
|
||||
if i < 0 {
|
||||
format!("Int8: {:#x} ({:#x})", i, ReallySigned(i))
|
||||
} else {
|
||||
format!("Int8: {:#x}", i)
|
||||
}
|
||||
}
|
||||
DataType::Int16 => {
|
||||
let i = Endian::read_i16(bytes);
|
||||
if i < 0 {
|
||||
format!("Int16: {:#x} ({:#x})", i, ReallySigned(i))
|
||||
} else {
|
||||
format!("Int16: {:#x}", i)
|
||||
}
|
||||
}
|
||||
DataType::Int32 => {
|
||||
let i = Endian::read_i32(bytes);
|
||||
if i < 0 {
|
||||
format!("Int32: {:#x} ({:#x})", i, ReallySigned(i))
|
||||
} else {
|
||||
format!("Int32: {:#x}", i)
|
||||
}
|
||||
}
|
||||
DataType::Int64 => {
|
||||
let i = Endian::read_i64(bytes);
|
||||
if i < 0 {
|
||||
format!("Int64: {:#x} ({:#x})", i, ReallySigned(i))
|
||||
} else {
|
||||
format!("Int64: {:#x}", i)
|
||||
}
|
||||
}
|
||||
DataType::Int128 => {
|
||||
let i = Endian::read_i128(bytes);
|
||||
if i < 0 {
|
||||
format!("Int128: {:#x} ({:#x})", i, ReallySigned(i))
|
||||
} else {
|
||||
format!("Int128: {:#x}", i)
|
||||
}
|
||||
}
|
||||
DataType::Float => {
|
||||
format!("Float: {}", Endian::read_f32(bytes))
|
||||
}
|
||||
DataType::Double => {
|
||||
format!("Double: {}", Endian::read_f64(bytes))
|
||||
}
|
||||
DataType::Bytes => {
|
||||
format!("Bytes: {:#?}", bytes)
|
||||
}
|
||||
DataType::String => {
|
||||
format!("String: {:?}", CStr::from_bytes_until_nul(bytes).ok()?)
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
fn required_len(&self) -> Option<usize> {
|
||||
match self {
|
||||
DataType::Int8 => Some(1),
|
||||
DataType::Int16 => Some(2),
|
||||
DataType::Int32 => Some(4),
|
||||
DataType::Int64 => Some(8),
|
||||
DataType::Int128 => Some(16),
|
||||
DataType::Float => Some(4),
|
||||
DataType::Double => Some(8),
|
||||
DataType::Bytes => None,
|
||||
DataType::String => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ObjArch: Send + Sync {
|
||||
fn process_code(
|
||||
&self,
|
||||
@@ -41,6 +134,16 @@ pub trait ObjArch: Send + Sync {
|
||||
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str>;
|
||||
|
||||
fn symbol_address(&self, symbol: &Symbol) -> u64 { symbol.address() }
|
||||
|
||||
fn guess_data_type(&self, _instruction: &ObjIns) -> Option<DataType> { None }
|
||||
|
||||
fn display_data_type(&self, _ty: DataType, bytes: &[u8]) -> Option<String> {
|
||||
Some(format!("Bytes: {:#x?}", bytes))
|
||||
}
|
||||
|
||||
// Downcast methods
|
||||
#[cfg(feature = "ppc")]
|
||||
fn ppc(&self) -> Option<&ppc::ObjArchPpc> { None }
|
||||
}
|
||||
|
||||
pub struct ProcessCodeResult {
|
||||
@@ -48,7 +151,7 @@ pub struct ProcessCodeResult {
|
||||
pub insts: Vec<ObjIns>,
|
||||
}
|
||||
|
||||
pub fn new_arch(object: &object::File) -> Result<Box<dyn ObjArch>> {
|
||||
pub fn new_arch(object: &File) -> Result<Box<dyn ObjArch>> {
|
||||
Ok(match object.architecture() {
|
||||
#[cfg(feature = "ppc")]
|
||||
Architecture::PowerPc => Box::new(ppc::ObjArchPpc::new(object)?),
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
use std::{borrow::Cow, collections::BTreeMap};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use object::{elf, File, Relocation, RelocationFlags};
|
||||
use ppc750cl::{Argument, InsIter, GPR};
|
||||
use anyhow::{bail, ensure, Result};
|
||||
use byteorder::BigEndian;
|
||||
use cwextab::{decode_extab, ExceptionTableData};
|
||||
use object::{
|
||||
elf, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, RelocationTarget,
|
||||
Symbol, SymbolKind,
|
||||
};
|
||||
use ppc750cl::{Argument, InsIter, Opcode, GPR};
|
||||
|
||||
use crate::{
|
||||
arch::{ObjArch, ProcessCodeResult},
|
||||
arch::{DataType, ObjArch, ProcessCodeResult},
|
||||
diff::DiffObjConfig,
|
||||
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection},
|
||||
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection, ObjSymbol},
|
||||
};
|
||||
|
||||
// Relative relocation, can be Simm, Offset or BranchDest
|
||||
@@ -22,10 +27,13 @@ fn is_rel_abs_arg(arg: &Argument) -> bool {
|
||||
|
||||
fn is_offset_arg(arg: &Argument) -> bool { matches!(arg, Argument::Offset(_)) }
|
||||
|
||||
pub struct ObjArchPpc {}
|
||||
pub struct ObjArchPpc {
|
||||
/// Exception info
|
||||
pub extab: Option<BTreeMap<usize, ExceptionInfo>>,
|
||||
}
|
||||
|
||||
impl ObjArchPpc {
|
||||
pub fn new(_file: &File) -> Result<Self> { Ok(Self {}) }
|
||||
pub fn new(file: &File) -> Result<Self> { Ok(Self { extab: decode_exception_info(file)? }) }
|
||||
}
|
||||
|
||||
impl ObjArch for ObjArchPpc {
|
||||
@@ -178,6 +186,42 @@ impl ObjArch for ObjArchPpc {
|
||||
_ => Cow::Owned(format!("<{flags:?}>")),
|
||||
}
|
||||
}
|
||||
|
||||
fn guess_data_type(&self, instruction: &ObjIns) -> Option<super::DataType> {
|
||||
// Always shows the first string of the table. Not ideal, but it's really hard to find
|
||||
// the actual string being referenced.
|
||||
if instruction.reloc.as_ref().is_some_and(|r| r.target.name.starts_with("@stringBase")) {
|
||||
return Some(DataType::String);
|
||||
}
|
||||
|
||||
match Opcode::from(instruction.op as u8) {
|
||||
Opcode::Lbz | Opcode::Lbzu | Opcode::Lbzux | Opcode::Lbzx => Some(DataType::Int8),
|
||||
Opcode::Lhz | Opcode::Lhzu | Opcode::Lhzux | Opcode::Lhzx => Some(DataType::Int16),
|
||||
Opcode::Lha | Opcode::Lhau | Opcode::Lhaux | Opcode::Lhax => Some(DataType::Int16),
|
||||
Opcode::Lwz | Opcode::Lwzu | Opcode::Lwzux | Opcode::Lwzx => Some(DataType::Int32),
|
||||
Opcode::Lfs | Opcode::Lfsu | Opcode::Lfsux | Opcode::Lfsx => Some(DataType::Float),
|
||||
Opcode::Lfd | Opcode::Lfdu | Opcode::Lfdux | Opcode::Lfdx => Some(DataType::Double),
|
||||
|
||||
Opcode::Stb | Opcode::Stbu | Opcode::Stbux | Opcode::Stbx => Some(DataType::Int8),
|
||||
Opcode::Sth | Opcode::Sthu | Opcode::Sthux | Opcode::Sthx => Some(DataType::Int16),
|
||||
Opcode::Stw | Opcode::Stwu | Opcode::Stwux | Opcode::Stwx => Some(DataType::Int32),
|
||||
Opcode::Stfs | Opcode::Stfsu | Opcode::Stfsux | Opcode::Stfsx => Some(DataType::Float),
|
||||
Opcode::Stfd | Opcode::Stfdu | Opcode::Stfdux | Opcode::Stfdx => Some(DataType::Double),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn display_data_type(&self, ty: DataType, bytes: &[u8]) -> Option<String> {
|
||||
ty.display_bytes::<BigEndian>(bytes)
|
||||
}
|
||||
|
||||
fn ppc(&self) -> Option<&ObjArchPpc> { Some(self) }
|
||||
}
|
||||
|
||||
impl ObjArchPpc {
|
||||
pub fn extab_for_symbol(&self, symbol: &ObjSymbol) -> Option<&ExceptionInfo> {
|
||||
symbol.original_index.and_then(|i| self.extab.as_ref()?.get(&i))
|
||||
}
|
||||
}
|
||||
|
||||
fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {
|
||||
@@ -208,3 +252,132 @@ fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExtabSymbolRef {
|
||||
pub original_index: usize,
|
||||
pub name: String,
|
||||
pub demangled_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExceptionInfo {
|
||||
pub eti_symbol: ExtabSymbolRef,
|
||||
pub etb_symbol: ExtabSymbolRef,
|
||||
pub data: ExceptionTableData,
|
||||
pub dtors: Vec<ExtabSymbolRef>,
|
||||
}
|
||||
|
||||
fn decode_exception_info(file: &File<'_>) -> Result<Option<BTreeMap<usize, ExceptionInfo>>> {
|
||||
let Some(extab_section) = file.section_by_name("extab") else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(extabindex_section) = file.section_by_name("extabindex") else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut result = BTreeMap::new();
|
||||
let extab_relocations = extab_section.relocations().collect::<BTreeMap<u64, Relocation>>();
|
||||
let extabindex_relocations =
|
||||
extabindex_section.relocations().collect::<BTreeMap<u64, Relocation>>();
|
||||
|
||||
for extabindex in file.symbols().filter(|symbol| {
|
||||
symbol.section_index() == Some(extabindex_section.index())
|
||||
&& symbol.kind() == SymbolKind::Data
|
||||
}) {
|
||||
if extabindex.size() != 12 {
|
||||
log::warn!("Invalid extabindex entry size {}", extabindex.size());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Each extabindex entry has two relocations:
|
||||
// - 0x0: The function that the exception table is for
|
||||
// - 0x8: The relevant entry in extab section
|
||||
let Some(extab_func_reloc) = extabindex_relocations.get(&extabindex.address()) else {
|
||||
log::warn!("Failed to find function relocation for extabindex entry");
|
||||
continue;
|
||||
};
|
||||
let Some(extab_reloc) = extabindex_relocations.get(&(extabindex.address() + 8)) else {
|
||||
log::warn!("Failed to find extab relocation for extabindex entry");
|
||||
continue;
|
||||
};
|
||||
|
||||
// Resolve the function and extab symbols
|
||||
let Some(extab_func) = relocation_symbol(file, extab_func_reloc)? else {
|
||||
log::warn!("Failed to find function symbol for extabindex entry");
|
||||
continue;
|
||||
};
|
||||
let extab_func_name = extab_func.name()?;
|
||||
let Some(extab) = relocation_symbol(file, extab_reloc)? else {
|
||||
log::warn!("Failed to find extab symbol for extabindex entry");
|
||||
continue;
|
||||
};
|
||||
|
||||
let extab_start_addr = extab.address() - extab_section.address();
|
||||
let extab_end_addr = extab_start_addr + extab.size();
|
||||
|
||||
// All relocations in the extab section are dtors
|
||||
let mut dtors: Vec<ExtabSymbolRef> = vec![];
|
||||
for (_, reloc) in extab_relocations.range(extab_start_addr..extab_end_addr) {
|
||||
let Some(symbol) = relocation_symbol(file, reloc)? else {
|
||||
log::warn!("Failed to find symbol for extab relocation");
|
||||
continue;
|
||||
};
|
||||
dtors.push(make_symbol_ref(&symbol)?);
|
||||
}
|
||||
|
||||
// Decode the extab data
|
||||
let Some(extab_data) = extab_section.data_range(extab_start_addr, extab.size())? else {
|
||||
log::warn!("Failed to get extab data for function {}", extab_func_name);
|
||||
continue;
|
||||
};
|
||||
let data = match decode_extab(extab_data) {
|
||||
Ok(decoded_data) => decoded_data,
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Exception table decoding failed for function {}, reason: {}",
|
||||
extab_func_name,
|
||||
e.to_string()
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
//Add the new entry to the list
|
||||
result.insert(extab_func.index().0, ExceptionInfo {
|
||||
eti_symbol: make_symbol_ref(&extabindex)?,
|
||||
etb_symbol: make_symbol_ref(&extab)?,
|
||||
data,
|
||||
dtors,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Some(result))
|
||||
}
|
||||
|
||||
fn relocation_symbol<'data, 'file>(
|
||||
file: &'file File<'data>,
|
||||
relocation: &Relocation,
|
||||
) -> Result<Option<Symbol<'data, 'file>>> {
|
||||
let addend = relocation.addend();
|
||||
match relocation.target() {
|
||||
RelocationTarget::Symbol(idx) => {
|
||||
ensure!(addend == 0, "Symbol relocations must have zero addend");
|
||||
Ok(Some(file.symbol_by_index(idx)?))
|
||||
}
|
||||
RelocationTarget::Section(idx) => {
|
||||
ensure!(addend >= 0, "Section relocations must have non-negative addend");
|
||||
let addend = addend as u64;
|
||||
Ok(file
|
||||
.symbols()
|
||||
.find(|symbol| symbol.section_index() == Some(idx) && symbol.address() == addend))
|
||||
}
|
||||
target => bail!("Unsupported relocation target: {target:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_symbol_ref(symbol: &Symbol) -> Result<ExtabSymbolRef> {
|
||||
let name = symbol.name()?.to_string();
|
||||
let demangled_name = cwdemangle::demangle(&name, &cwdemangle::DemangleOptions::default());
|
||||
Ok(ExtabSymbolRef { original_index: symbol.index().0, name, demangled_name })
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@ use serde_json::error::Category;
|
||||
include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs"));
|
||||
include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs"));
|
||||
|
||||
pub const REPORT_VERSION: u32 = 1;
|
||||
pub const REPORT_VERSION: u32 = 2;
|
||||
|
||||
impl Report {
|
||||
/// Attempts to parse the report as binary protobuf or JSON.
|
||||
pub fn parse(data: &[u8]) -> Result<Self> {
|
||||
if data.is_empty() {
|
||||
bail!(std::io::Error::from(std::io::ErrorKind::UnexpectedEof));
|
||||
@@ -25,6 +26,7 @@ impl 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> {
|
||||
match serde_json::from_slice::<Self>(bytes) {
|
||||
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<()> {
|
||||
if self.version == 0 {
|
||||
self.migrate_v0()?;
|
||||
}
|
||||
if self.version == 1 {
|
||||
self.migrate_v1()?;
|
||||
}
|
||||
if self.version != REPORT_VERSION {
|
||||
bail!("Unsupported report version: {}", self.version);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds `complete_code`, `complete_data`, `complete_code_percent`, and `complete_data_percent`
|
||||
/// to measures, and sets `progress_categories` in unit metadata.
|
||||
fn migrate_v0(&mut self) -> Result<()> {
|
||||
let Some(measures) = &mut self.measures else {
|
||||
bail!("Missing measures in report");
|
||||
@@ -61,15 +70,16 @@ impl Report {
|
||||
let Some(unit_measures) = &mut unit.measures else {
|
||||
bail!("Missing measures in report unit");
|
||||
};
|
||||
let Some(metadata) = &mut unit.metadata else {
|
||||
bail!("Missing metadata in report unit");
|
||||
let mut complete = false;
|
||||
if let Some(metadata) = &mut unit.metadata {
|
||||
if metadata.module_name.is_some() || metadata.module_id.is_some() {
|
||||
metadata.progress_categories = vec!["modules".to_string()];
|
||||
} else {
|
||||
metadata.progress_categories = vec!["dol".to_string()];
|
||||
}
|
||||
complete = metadata.complete.unwrap_or(false);
|
||||
};
|
||||
if metadata.module_name.is_some() || metadata.module_id.is_some() {
|
||||
metadata.progress_categories = vec!["modules".to_string()];
|
||||
} else {
|
||||
metadata.progress_categories = vec!["dol".to_string()];
|
||||
}
|
||||
if metadata.complete.unwrap_or(false) {
|
||||
if complete {
|
||||
unit_measures.complete_code = unit_measures.total_code;
|
||||
unit_measures.complete_data = unit_measures.total_data;
|
||||
unit_measures.complete_code_percent = 100.0;
|
||||
@@ -84,10 +94,42 @@ impl Report {
|
||||
measures.complete_data += unit_measures.complete_data;
|
||||
}
|
||||
measures.calc_matched_percent();
|
||||
self.calculate_progress_categories();
|
||||
self.version = 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds `total_units` and `complete_units` to measures.
|
||||
fn migrate_v1(&mut self) -> Result<()> {
|
||||
let Some(total_measures) = &mut self.measures else {
|
||||
bail!("Missing measures in report");
|
||||
};
|
||||
for unit in &mut self.units {
|
||||
let Some(measures) = &mut unit.measures else {
|
||||
bail!("Missing measures in report unit");
|
||||
};
|
||||
let complete = unit.metadata.as_ref().and_then(|m| m.complete).unwrap_or(false) as u32;
|
||||
let progress_categories =
|
||||
unit.metadata.as_ref().map(|m| m.progress_categories.as_slice()).unwrap_or(&[]);
|
||||
measures.total_units = 1;
|
||||
measures.complete_units = complete;
|
||||
total_measures.total_units += 1;
|
||||
total_measures.complete_units += complete;
|
||||
for id in progress_categories {
|
||||
if let Some(category) = self.categories.iter_mut().find(|c| &c.id == id) {
|
||||
let Some(measures) = &mut category.measures else {
|
||||
bail!("Missing measures in category");
|
||||
};
|
||||
measures.total_units += 1;
|
||||
measures.complete_units += complete;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.version = 2;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Calculate progress categories based on unit metadata.
|
||||
pub fn calculate_progress_categories(&mut self) {
|
||||
for unit in &self.units {
|
||||
let Some(metadata) = unit.metadata.as_ref() else {
|
||||
@@ -117,6 +159,72 @@ impl Report {
|
||||
measures.calc_matched_percent();
|
||||
}
|
||||
}
|
||||
|
||||
/// Split the report into multiple reports based on progress categories.
|
||||
/// Assumes progress categories are in the format `version`, `version.category`.
|
||||
/// This is a hack for projects that generate all versions in a single report.
|
||||
pub fn split(self) -> Vec<(String, Report)> {
|
||||
let mut reports = Vec::new();
|
||||
// Map units to Option to allow taking ownership
|
||||
let mut units = self.units.into_iter().map(Some).collect::<Vec<_>>();
|
||||
for category in &self.categories {
|
||||
if category.id.contains(".") {
|
||||
// Skip subcategories
|
||||
continue;
|
||||
}
|
||||
fn is_sub_category(id: &str, parent: &str, sep: char) -> bool {
|
||||
id.starts_with(parent)
|
||||
&& id.get(parent.len()..).map_or(false, |s| s.starts_with(sep))
|
||||
}
|
||||
let mut sub_categories = self
|
||||
.categories
|
||||
.iter()
|
||||
.filter(|c| is_sub_category(&c.id, &category.id, '.'))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
// Remove category prefix
|
||||
for sub_category in &mut sub_categories {
|
||||
sub_category.id = sub_category.id[category.id.len() + 1..].to_string();
|
||||
}
|
||||
let mut sub_units = units
|
||||
.iter_mut()
|
||||
.filter_map(|opt| {
|
||||
let unit = opt.as_mut()?;
|
||||
let metadata = unit.metadata.as_ref()?;
|
||||
if metadata.progress_categories.contains(&category.id) {
|
||||
opt.take()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for sub_unit in &mut sub_units {
|
||||
// Remove leading version/ from unit name
|
||||
if let Some(name) =
|
||||
sub_unit.name.strip_prefix(&category.id).and_then(|s| s.strip_prefix('/'))
|
||||
{
|
||||
sub_unit.name = name.to_string();
|
||||
}
|
||||
// Filter progress categories
|
||||
let Some(metadata) = sub_unit.metadata.as_mut() else {
|
||||
continue;
|
||||
};
|
||||
metadata.progress_categories = metadata
|
||||
.progress_categories
|
||||
.iter()
|
||||
.filter(|c| is_sub_category(c, &category.id, '.'))
|
||||
.map(|c| c[category.id.len() + 1..].to_string())
|
||||
.collect();
|
||||
}
|
||||
reports.push((category.id.clone(), Report {
|
||||
measures: category.measures,
|
||||
units: sub_units,
|
||||
version: self.version,
|
||||
categories: sub_categories,
|
||||
}));
|
||||
}
|
||||
reports
|
||||
}
|
||||
}
|
||||
|
||||
impl Measures {
|
||||
@@ -176,6 +284,8 @@ impl AddAssign for Measures {
|
||||
self.matched_functions += other.matched_functions;
|
||||
self.complete_code += other.complete_code;
|
||||
self.complete_data += other.complete_data;
|
||||
self.total_units += other.total_units;
|
||||
self.complete_units += other.complete_units;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,77 +1,100 @@
|
||||
use std::{
|
||||
fs,
|
||||
fs::File,
|
||||
io::Read,
|
||||
io::{BufReader, BufWriter, Read},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use bimap::BiBTreeMap;
|
||||
use filetime::FileTime;
|
||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||
|
||||
#[inline]
|
||||
fn bool_true() -> bool { true }
|
||||
|
||||
#[derive(Default, Clone, serde::Deserialize)]
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ProjectConfig {
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub min_version: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub custom_make: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub custom_args: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub target_dir: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub base_dir: Option<PathBuf>,
|
||||
#[serde(default = "bool_true")]
|
||||
pub build_base: bool,
|
||||
#[serde(default)]
|
||||
pub build_target: bool,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub build_base: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub build_target: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub watch_patterns: Option<Vec<Glob>>,
|
||||
#[serde(default, alias = "units")]
|
||||
pub objects: Vec<ProjectObject>,
|
||||
#[serde(default)]
|
||||
pub progress_categories: Vec<ProjectProgressCategory>,
|
||||
#[serde(default, alias = "objects", skip_serializing_if = "Option::is_none")]
|
||||
pub units: Option<Vec<ProjectObject>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub progress_categories: Option<Vec<ProjectProgressCategory>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, serde::Deserialize)]
|
||||
impl ProjectConfig {
|
||||
#[inline]
|
||||
pub fn units(&self) -> &[ProjectObject] { self.units.as_deref().unwrap_or_default() }
|
||||
|
||||
#[inline]
|
||||
pub fn units_mut(&mut self) -> &mut Vec<ProjectObject> {
|
||||
self.units.get_or_insert_with(Vec::new)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn progress_categories(&self) -> &[ProjectProgressCategory] {
|
||||
self.progress_categories.as_deref().unwrap_or_default()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn progress_categories_mut(&mut self) -> &mut Vec<ProjectProgressCategory> {
|
||||
self.progress_categories.get_or_insert_with(Vec::new)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ProjectObject {
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub path: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub target_path: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub base_path: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[deprecated(note = "Use metadata.reverse_fn_order")]
|
||||
pub reverse_fn_order: Option<bool>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[deprecated(note = "Use metadata.complete")]
|
||||
pub complete: Option<bool>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub scratch: Option<ScratchConfig>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub metadata: Option<ProjectObjectMetadata>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub symbol_mappings: Option<SymbolMappings>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, serde::Deserialize)]
|
||||
pub type SymbolMappings = BiBTreeMap<String, String>;
|
||||
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ProjectObjectMetadata {
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub complete: Option<bool>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub reverse_fn_order: Option<bool>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub source_path: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub progress_categories: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub auto_generated: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, serde::Deserialize)]
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ProjectProgressCategory {
|
||||
#[serde(default)]
|
||||
pub id: String,
|
||||
@@ -112,32 +135,36 @@ impl ProjectObject {
|
||||
}
|
||||
|
||||
pub fn complete(&self) -> Option<bool> {
|
||||
#[allow(deprecated)]
|
||||
#[expect(deprecated)]
|
||||
self.metadata.as_ref().and_then(|m| m.complete).or(self.complete)
|
||||
}
|
||||
|
||||
pub fn reverse_fn_order(&self) -> Option<bool> {
|
||||
#[allow(deprecated)]
|
||||
#[expect(deprecated)]
|
||||
self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order)
|
||||
}
|
||||
|
||||
pub fn hidden(&self) -> bool {
|
||||
self.metadata.as_ref().and_then(|m| m.auto_generated).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn source_path(&self) -> Option<&String> {
|
||||
self.metadata.as_ref().and_then(|m| m.source_path.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct ScratchConfig {
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub platform: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub compiler: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub c_flags: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ctx_path: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub build_ctx: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub build_ctx: Option<bool>,
|
||||
}
|
||||
|
||||
pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"];
|
||||
@@ -150,13 +177,13 @@ pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct ProjectConfigInfo {
|
||||
pub path: PathBuf,
|
||||
pub timestamp: FileTime,
|
||||
pub timestamp: Option<FileTime>,
|
||||
}
|
||||
|
||||
pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
|
||||
for filename in CONFIG_FILENAMES.iter() {
|
||||
let config_path = dir.join(filename);
|
||||
let Ok(mut file) = File::open(&config_path) else {
|
||||
let Ok(file) = File::open(&config_path) else {
|
||||
continue;
|
||||
};
|
||||
let metadata = file.metadata();
|
||||
@@ -165,9 +192,10 @@ pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectC
|
||||
continue;
|
||||
}
|
||||
let ts = FileTime::from_last_modification_time(&metadata);
|
||||
let mut reader = BufReader::new(file);
|
||||
let mut result = match filename.contains("json") {
|
||||
true => read_json_config(&mut file),
|
||||
false => read_yml_config(&mut file),
|
||||
true => read_json_config(&mut reader),
|
||||
false => read_yml_config(&mut reader),
|
||||
};
|
||||
if let Ok(config) = &result {
|
||||
// Validate min_version if present
|
||||
@@ -175,12 +203,41 @@ pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectC
|
||||
result = Err(e);
|
||||
}
|
||||
}
|
||||
return Some((result, ProjectConfigInfo { path: config_path, timestamp: ts }));
|
||||
return Some((result, ProjectConfigInfo { path: config_path, timestamp: Some(ts) }));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn save_project_config(
|
||||
config: &ProjectConfig,
|
||||
info: &ProjectConfigInfo,
|
||||
) -> Result<ProjectConfigInfo> {
|
||||
if let Some(last_ts) = info.timestamp {
|
||||
// Check if the file has changed since we last read it
|
||||
if let Ok(metadata) = fs::metadata(&info.path) {
|
||||
let ts = FileTime::from_last_modification_time(&metadata);
|
||||
if ts != last_ts {
|
||||
return Err(anyhow!("Config file has changed since last read"));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut writer =
|
||||
BufWriter::new(File::create(&info.path).context("Failed to create config file")?);
|
||||
let ext = info.path.extension().and_then(|ext| ext.to_str()).unwrap_or("json");
|
||||
match ext {
|
||||
"json" => serde_json::to_writer_pretty(&mut writer, config).context("Failed to write JSON"),
|
||||
"yml" | "yaml" => {
|
||||
serde_yaml::to_writer(&mut writer, config).context("Failed to write YAML")
|
||||
}
|
||||
_ => Err(anyhow!("Unknown config file extension: {ext}")),
|
||||
}?;
|
||||
let file = writer.into_inner().context("Failed to flush file")?;
|
||||
let metadata = file.metadata().context("Failed to get file metadata")?;
|
||||
let ts = FileTime::from_last_modification_time(&metadata);
|
||||
Ok(ProjectConfigInfo { path: info.path.clone(), timestamp: Some(ts) })
|
||||
}
|
||||
|
||||
fn validate_min_version(config: &ProjectConfig) -> Result<()> {
|
||||
let Some(min_version) = &config.min_version else { return Ok(()) };
|
||||
let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))
|
||||
|
||||
@@ -41,7 +41,7 @@ pub fn no_diff_code(out: &ProcessCodeResult, symbol_ref: SymbolRef) -> Result<Ob
|
||||
});
|
||||
}
|
||||
resolve_branches(&mut diff);
|
||||
Ok(ObjSymbolDiff { symbol_ref, diff_symbol: None, instructions: diff, match_percent: None })
|
||||
Ok(ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: diff, match_percent: None })
|
||||
}
|
||||
|
||||
pub fn diff_code(
|
||||
@@ -67,7 +67,7 @@ pub fn diff_code(
|
||||
right.arg_diff = result.right_args_diff;
|
||||
}
|
||||
|
||||
let total = left_out.insts.len();
|
||||
let total = left_out.insts.len().max(right_out.insts.len());
|
||||
let percent = if diff_state.diff_count >= total {
|
||||
0.0
|
||||
} else {
|
||||
@@ -77,13 +77,13 @@ pub fn diff_code(
|
||||
Ok((
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: left_symbol_ref,
|
||||
diff_symbol: Some(right_symbol_ref),
|
||||
target_symbol: Some(right_symbol_ref),
|
||||
instructions: left_diff,
|
||||
match_percent: Some(percent),
|
||||
},
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: right_symbol_ref,
|
||||
diff_symbol: Some(left_symbol_ref),
|
||||
target_symbol: Some(left_symbol_ref),
|
||||
instructions: right_diff,
|
||||
match_percent: Some(percent),
|
||||
},
|
||||
@@ -211,7 +211,7 @@ fn arg_eq(
|
||||
left_diff: &ObjInsDiff,
|
||||
right_diff: &ObjInsDiff,
|
||||
) -> bool {
|
||||
return match left {
|
||||
match left {
|
||||
ObjInsArg::PlainText(l) => match right {
|
||||
ObjInsArg::PlainText(r) => l == r,
|
||||
_ => false,
|
||||
@@ -236,7 +236,7 @@ fn arg_eq(
|
||||
left_diff.branch_to.as_ref().map(|b| b.ins_idx)
|
||||
== right_diff.branch_to.as_ref().map(|b| b.ins_idx)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
||||
@@ -20,13 +20,13 @@ pub fn diff_bss_symbol(
|
||||
Ok((
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: left_symbol_ref,
|
||||
diff_symbol: Some(right_symbol_ref),
|
||||
target_symbol: Some(right_symbol_ref),
|
||||
instructions: vec![],
|
||||
match_percent: Some(percent),
|
||||
},
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: right_symbol_ref,
|
||||
diff_symbol: Some(left_symbol_ref),
|
||||
target_symbol: Some(left_symbol_ref),
|
||||
instructions: vec![],
|
||||
match_percent: Some(percent),
|
||||
},
|
||||
@@ -34,7 +34,7 @@ pub fn diff_bss_symbol(
|
||||
}
|
||||
|
||||
pub fn no_diff_symbol(_obj: &ObjInfo, symbol_ref: SymbolRef) -> ObjSymbolDiff {
|
||||
ObjSymbolDiff { symbol_ref, diff_symbol: None, instructions: vec![], match_percent: None }
|
||||
ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: vec![], match_percent: None }
|
||||
}
|
||||
|
||||
/// Compare the data sections of two object files.
|
||||
@@ -158,13 +158,13 @@ pub fn diff_data_symbol(
|
||||
Ok((
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: left_symbol_ref,
|
||||
diff_symbol: Some(right_symbol_ref),
|
||||
target_symbol: Some(right_symbol_ref),
|
||||
instructions: vec![],
|
||||
match_percent: Some(match_percent),
|
||||
},
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: right_symbol_ref,
|
||||
diff_symbol: Some(left_symbol_ref),
|
||||
target_symbol: Some(left_symbol_ref),
|
||||
instructions: vec![],
|
||||
match_percent: Some(match_percent),
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@ pub enum DiffText<'a> {
|
||||
Eol,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub enum HighlightKind {
|
||||
#[default]
|
||||
None,
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::collections::HashSet;
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{
|
||||
config::SymbolMappings,
|
||||
diff::{
|
||||
code::{diff_code, no_diff_code, process_code_symbol},
|
||||
data::{
|
||||
@@ -161,6 +162,8 @@ pub struct DiffObjConfig {
|
||||
#[serde(default = "default_true")]
|
||||
pub space_between_args: bool,
|
||||
pub combine_data_sections: bool,
|
||||
#[serde(default)]
|
||||
pub symbol_mappings: MappingConfig,
|
||||
// x86
|
||||
pub x86_formatter: X86Formatter,
|
||||
// MIPS
|
||||
@@ -182,6 +185,7 @@ impl Default for DiffObjConfig {
|
||||
relax_reloc_diffs: false,
|
||||
space_between_args: true,
|
||||
combine_data_sections: false,
|
||||
symbol_mappings: Default::default(),
|
||||
x86_formatter: Default::default(),
|
||||
mips_abi: Default::default(),
|
||||
mips_instr_category: Default::default(),
|
||||
@@ -223,8 +227,10 @@ impl ObjSectionDiff {
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ObjSymbolDiff {
|
||||
/// The symbol ref this object
|
||||
pub symbol_ref: SymbolRef,
|
||||
pub diff_symbol: Option<SymbolRef>,
|
||||
/// The symbol ref in the _other_ object that this symbol was diffed against
|
||||
pub target_symbol: Option<SymbolRef>,
|
||||
pub instructions: Vec<ObjInsDiff>,
|
||||
pub match_percent: Option<f32>,
|
||||
}
|
||||
@@ -294,8 +300,13 @@ pub struct ObjInsBranchTo {
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ObjDiff {
|
||||
/// A list of all section diffs in the object.
|
||||
pub sections: Vec<ObjSectionDiff>,
|
||||
/// Common BSS symbols don't live in a section, so they're stored separately.
|
||||
pub common: Vec<ObjSymbolDiff>,
|
||||
/// If `selecting_left` or `selecting_right` is set, this is the list of symbols
|
||||
/// that are being mapped to the other object.
|
||||
pub mapping_symbols: Vec<ObjSymbolDiff>,
|
||||
}
|
||||
|
||||
impl ObjDiff {
|
||||
@@ -303,13 +314,14 @@ impl ObjDiff {
|
||||
let mut result = Self {
|
||||
sections: Vec::with_capacity(obj.sections.len()),
|
||||
common: Vec::with_capacity(obj.common.len()),
|
||||
mapping_symbols: vec![],
|
||||
};
|
||||
for (section_idx, section) in obj.sections.iter().enumerate() {
|
||||
let mut symbols = Vec::with_capacity(section.symbols.len());
|
||||
for (symbol_idx, _) in section.symbols.iter().enumerate() {
|
||||
symbols.push(ObjSymbolDiff {
|
||||
symbol_ref: SymbolRef { section_idx, symbol_idx },
|
||||
diff_symbol: None,
|
||||
target_symbol: None,
|
||||
instructions: vec![],
|
||||
match_percent: None,
|
||||
});
|
||||
@@ -328,7 +340,7 @@ impl ObjDiff {
|
||||
for (symbol_idx, _) in obj.common.iter().enumerate() {
|
||||
result.common.push(ObjSymbolDiff {
|
||||
symbol_ref: SymbolRef { section_idx: obj.sections.len(), symbol_idx },
|
||||
diff_symbol: None,
|
||||
target_symbol: None,
|
||||
instructions: vec![],
|
||||
match_percent: None,
|
||||
});
|
||||
@@ -378,7 +390,7 @@ pub fn diff_objs(
|
||||
right: Option<&ObjInfo>,
|
||||
prev: Option<&ObjInfo>,
|
||||
) -> Result<DiffObjsResult> {
|
||||
let symbol_matches = matching_symbols(left, right, prev)?;
|
||||
let symbol_matches = matching_symbols(left, right, prev, &config.symbol_mappings)?;
|
||||
let section_matches = matching_sections(left, right)?;
|
||||
let mut left = left.map(|p| (p, ObjDiff::new_from_obj(p)));
|
||||
let mut right = right.map(|p| (p, ObjDiff::new_from_obj(p)));
|
||||
@@ -529,6 +541,17 @@ pub fn diff_objs(
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some((right_obj, right_out)), Some((left_obj, left_out))) =
|
||||
(right.as_mut(), left.as_mut())
|
||||
{
|
||||
if let Some(right_name) = &config.symbol_mappings.selecting_left {
|
||||
generate_mapping_symbols(right_obj, right_name, left_obj, left_out, config)?;
|
||||
}
|
||||
if let Some(left_name) = &config.symbol_mappings.selecting_right {
|
||||
generate_mapping_symbols(left_obj, left_name, right_obj, right_out, config)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(DiffObjsResult {
|
||||
left: left.map(|(_, o)| o),
|
||||
right: right.map(|(_, o)| o),
|
||||
@@ -536,6 +559,63 @@ pub fn diff_objs(
|
||||
})
|
||||
}
|
||||
|
||||
/// When we're selecting a symbol to use as a comparison, we'll create comparisons for all
|
||||
/// symbols in the other object that match the selected symbol's section and kind. This allows
|
||||
/// us to display match percentages for all symbols in the other object that could be selected.
|
||||
fn generate_mapping_symbols(
|
||||
base_obj: &ObjInfo,
|
||||
base_name: &str,
|
||||
target_obj: &ObjInfo,
|
||||
target_out: &mut ObjDiff,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<()> {
|
||||
let Some(base_symbol_ref) = symbol_ref_by_name(base_obj, base_name) else {
|
||||
return Ok(());
|
||||
};
|
||||
let (base_section, base_symbol) = base_obj.section_symbol(base_symbol_ref);
|
||||
let Some(base_section) = base_section else {
|
||||
return Ok(());
|
||||
};
|
||||
let base_code = match base_section.kind {
|
||||
ObjSectionKind::Code => Some(process_code_symbol(base_obj, base_symbol_ref, config)?),
|
||||
_ => None,
|
||||
};
|
||||
for (target_section_index, target_section) in
|
||||
target_obj.sections.iter().enumerate().filter(|(_, s)| s.kind == base_section.kind)
|
||||
{
|
||||
for (target_symbol_index, _target_symbol) in
|
||||
target_section.symbols.iter().enumerate().filter(|(_, s)| s.kind == base_symbol.kind)
|
||||
{
|
||||
let target_symbol_ref =
|
||||
SymbolRef { section_idx: target_section_index, symbol_idx: target_symbol_index };
|
||||
match base_section.kind {
|
||||
ObjSectionKind::Code => {
|
||||
let target_code = process_code_symbol(target_obj, target_symbol_ref, config)?;
|
||||
let (left_diff, _right_diff) = diff_code(
|
||||
&target_code,
|
||||
base_code.as_ref().unwrap(),
|
||||
target_symbol_ref,
|
||||
base_symbol_ref,
|
||||
config,
|
||||
)?;
|
||||
target_out.mapping_symbols.push(left_diff);
|
||||
}
|
||||
ObjSectionKind::Data => {
|
||||
let (left_diff, _right_diff) =
|
||||
diff_data_symbol(target_obj, base_obj, target_symbol_ref, base_symbol_ref)?;
|
||||
target_out.mapping_symbols.push(left_diff);
|
||||
}
|
||||
ObjSectionKind::Bss => {
|
||||
let (left_diff, _right_diff) =
|
||||
diff_bss_symbol(target_obj, base_obj, target_symbol_ref, base_symbol_ref)?;
|
||||
target_out.mapping_symbols.push(left_diff);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
struct SymbolMatch {
|
||||
left: Option<SymbolRef>,
|
||||
@@ -551,19 +631,115 @@ struct SectionMatch {
|
||||
section_kind: ObjSectionKind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Deserialize, serde::Serialize)]
|
||||
pub struct MappingConfig {
|
||||
/// Manual symbol mappings
|
||||
pub mappings: SymbolMappings,
|
||||
/// The right object symbol name that we're selecting a left symbol for
|
||||
pub selecting_left: Option<String>,
|
||||
/// The left object symbol name that we're selecting a right symbol for
|
||||
pub selecting_right: Option<String>,
|
||||
}
|
||||
|
||||
fn symbol_ref_by_name(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
|
||||
for (section_idx, section) in obj.sections.iter().enumerate() {
|
||||
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
|
||||
if symbol.name == name {
|
||||
return Some(SymbolRef { section_idx, symbol_idx });
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn apply_symbol_mappings(
|
||||
left: &ObjInfo,
|
||||
right: &ObjInfo,
|
||||
mapping_config: &MappingConfig,
|
||||
left_used: &mut HashSet<SymbolRef>,
|
||||
right_used: &mut HashSet<SymbolRef>,
|
||||
matches: &mut Vec<SymbolMatch>,
|
||||
) -> Result<()> {
|
||||
// If we're selecting a symbol to use as a comparison, mark it as used
|
||||
// This ensures that we don't match it to another symbol at any point
|
||||
if let Some(left_name) = &mapping_config.selecting_left {
|
||||
if let Some(left_symbol) = symbol_ref_by_name(left, left_name) {
|
||||
left_used.insert(left_symbol);
|
||||
}
|
||||
}
|
||||
if let Some(right_name) = &mapping_config.selecting_right {
|
||||
if let Some(right_symbol) = symbol_ref_by_name(right, right_name) {
|
||||
right_used.insert(right_symbol);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply manual symbol mappings
|
||||
for (left_name, right_name) in &mapping_config.mappings {
|
||||
let Some(left_symbol) = symbol_ref_by_name(left, left_name) else {
|
||||
continue;
|
||||
};
|
||||
if left_used.contains(&left_symbol) {
|
||||
continue;
|
||||
}
|
||||
let Some(right_symbol) = symbol_ref_by_name(right, right_name) else {
|
||||
continue;
|
||||
};
|
||||
if right_used.contains(&right_symbol) {
|
||||
continue;
|
||||
}
|
||||
let left_section = &left.sections[left_symbol.section_idx];
|
||||
let right_section = &right.sections[right_symbol.section_idx];
|
||||
if left_section.kind != right_section.kind {
|
||||
log::warn!(
|
||||
"Symbol section kind mismatch: {} ({:?}) vs {} ({:?})",
|
||||
left_name,
|
||||
left_section.kind,
|
||||
right_name,
|
||||
right_section.kind
|
||||
);
|
||||
continue;
|
||||
}
|
||||
matches.push(SymbolMatch {
|
||||
left: Some(left_symbol),
|
||||
right: Some(right_symbol),
|
||||
prev: None, // TODO
|
||||
section_kind: left_section.kind,
|
||||
});
|
||||
left_used.insert(left_symbol);
|
||||
right_used.insert(right_symbol);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Find matching symbols between each object.
|
||||
fn matching_symbols(
|
||||
left: Option<&ObjInfo>,
|
||||
right: Option<&ObjInfo>,
|
||||
prev: Option<&ObjInfo>,
|
||||
mappings: &MappingConfig,
|
||||
) -> Result<Vec<SymbolMatch>> {
|
||||
let mut matches = Vec::new();
|
||||
let mut left_used = HashSet::new();
|
||||
let mut right_used = HashSet::new();
|
||||
if let Some(left) = left {
|
||||
if let Some(right) = right {
|
||||
apply_symbol_mappings(
|
||||
left,
|
||||
right,
|
||||
mappings,
|
||||
&mut left_used,
|
||||
&mut right_used,
|
||||
&mut matches,
|
||||
)?;
|
||||
}
|
||||
for (section_idx, section) in left.sections.iter().enumerate() {
|
||||
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
|
||||
let symbol_ref = SymbolRef { section_idx, symbol_idx };
|
||||
if left_used.contains(&symbol_ref) {
|
||||
continue;
|
||||
}
|
||||
let symbol_match = SymbolMatch {
|
||||
left: Some(SymbolRef { section_idx, symbol_idx }),
|
||||
left: Some(symbol_ref),
|
||||
right: find_symbol(right, symbol, section, Some(&right_used)),
|
||||
prev: find_symbol(prev, symbol, section, None),
|
||||
section_kind: section.kind,
|
||||
@@ -575,8 +751,12 @@ fn matching_symbols(
|
||||
}
|
||||
}
|
||||
for (symbol_idx, symbol) in left.common.iter().enumerate() {
|
||||
let symbol_ref = SymbolRef { section_idx: left.sections.len(), symbol_idx };
|
||||
if left_used.contains(&symbol_ref) {
|
||||
continue;
|
||||
}
|
||||
let symbol_match = SymbolMatch {
|
||||
left: Some(SymbolRef { section_idx: left.sections.len(), symbol_idx }),
|
||||
left: Some(symbol_ref),
|
||||
right: find_common_symbol(right, symbol),
|
||||
prev: find_common_symbol(prev, symbol),
|
||||
section_kind: ObjSectionKind::Bss,
|
||||
|
||||
@@ -3,7 +3,6 @@ pub mod split_meta;
|
||||
|
||||
use std::{borrow::Cow, collections::BTreeMap, fmt, path::PathBuf};
|
||||
|
||||
use cwextab::*;
|
||||
use filetime::FileTime;
|
||||
use flagset::{flags, FlagSet};
|
||||
use object::RelocationFlags;
|
||||
@@ -24,6 +23,9 @@ flags! {
|
||||
Weak,
|
||||
Common,
|
||||
Hidden,
|
||||
/// Has extra data associated with the symbol
|
||||
/// (e.g. exception table entry)
|
||||
HasExtra,
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Copy, Clone, Default)]
|
||||
@@ -110,28 +112,31 @@ pub struct ObjIns {
|
||||
pub orig: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
|
||||
pub enum ObjSymbolKind {
|
||||
#[default]
|
||||
Unknown,
|
||||
Function,
|
||||
Object,
|
||||
Section,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjSymbol {
|
||||
pub name: String,
|
||||
pub demangled_name: Option<String>,
|
||||
pub has_extab: bool,
|
||||
pub extab_name: Option<String>,
|
||||
pub extabindex_name: Option<String>,
|
||||
pub address: u64,
|
||||
pub section_address: u64,
|
||||
pub size: u64,
|
||||
pub size_known: bool,
|
||||
pub kind: ObjSymbolKind,
|
||||
pub flags: ObjSymbolFlagSet,
|
||||
pub addend: i64,
|
||||
/// Original virtual address (from .note.split section)
|
||||
pub virtual_address: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjExtab {
|
||||
pub func: ObjSymbol,
|
||||
pub data: ExceptionTableData,
|
||||
pub dtors: Vec<ObjSymbol>,
|
||||
/// Original index in object symbol table
|
||||
pub original_index: Option<usize>,
|
||||
pub bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct ObjInfo {
|
||||
@@ -141,8 +146,6 @@ pub struct ObjInfo {
|
||||
pub sections: Vec<ObjSection>,
|
||||
/// Common BSS symbols
|
||||
pub common: Vec<ObjSymbol>,
|
||||
/// Exception tables
|
||||
pub extab: Option<Vec<ObjExtab>>,
|
||||
/// Split object metadata (.note.split section)
|
||||
pub split_meta: Option<SplitMeta>,
|
||||
}
|
||||
|
||||
@@ -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 cwextab::decode_extab;
|
||||
use filetime::FileTime;
|
||||
use flagset::Flags;
|
||||
use object::{
|
||||
Architecture, BinaryFormat, File, Object, ObjectSection, ObjectSymbol, RelocationTarget,
|
||||
SectionIndex, SectionKind, Symbol, SymbolKind, SymbolScope, SymbolSection,
|
||||
endian::LittleEndian as LE,
|
||||
pe::{ImageAuxSymbolFunctionBeginEnd, ImageLinenumber},
|
||||
read::coff::{CoffFile, CoffHeader, ImageSymbol},
|
||||
BinaryFormat, File, Object, ObjectSection, ObjectSymbol, RelocationTarget, SectionIndex,
|
||||
SectionKind, Symbol, SymbolIndex, SymbolKind, SymbolScope, SymbolSection,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -14,8 +22,8 @@ use crate::{
|
||||
diff::DiffObjConfig,
|
||||
obj::{
|
||||
split_meta::{SplitMeta, SPLITMETA_SECTION},
|
||||
ObjExtab, ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet,
|
||||
ObjSymbolFlags,
|
||||
ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags,
|
||||
ObjSymbolKind,
|
||||
},
|
||||
util::{read_u16, read_u32},
|
||||
};
|
||||
@@ -57,6 +65,13 @@ fn to_obj_symbol(
|
||||
if obj_file.format() == BinaryFormat::Elf && symbol.scope() == SymbolScope::Linkage {
|
||||
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Hidden);
|
||||
}
|
||||
if arch
|
||||
.ppc()
|
||||
.and_then(|a| a.extab.as_ref())
|
||||
.map_or(false, |e| e.contains_key(&symbol.index().0))
|
||||
{
|
||||
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::HasExtra);
|
||||
}
|
||||
let address = arch.symbol_address(symbol);
|
||||
let section_address = if let Some(section) =
|
||||
symbol.section_index().and_then(|idx| obj_file.section_by_index(idx).ok())
|
||||
@@ -70,19 +85,36 @@ fn to_obj_symbol(
|
||||
let virtual_address = split_meta
|
||||
.and_then(|m| m.virtual_addresses.as_ref())
|
||||
.and_then(|v| v.get(symbol.index().0).cloned());
|
||||
|
||||
let bytes = symbol
|
||||
.section_index()
|
||||
.and_then(|idx| obj_file.section_by_index(idx).ok())
|
||||
.and_then(|section| section.data().ok())
|
||||
.and_then(|data| {
|
||||
data.get(section_address as usize..(section_address + symbol.size()) as usize)
|
||||
})
|
||||
.unwrap_or(&[]);
|
||||
|
||||
let kind = match symbol.kind() {
|
||||
SymbolKind::Text => ObjSymbolKind::Function,
|
||||
SymbolKind::Data => ObjSymbolKind::Object,
|
||||
SymbolKind::Section => ObjSymbolKind::Section,
|
||||
_ => ObjSymbolKind::Unknown,
|
||||
};
|
||||
|
||||
Ok(ObjSymbol {
|
||||
name: name.to_string(),
|
||||
demangled_name,
|
||||
has_extab: false,
|
||||
extab_name: None,
|
||||
extabindex_name: None,
|
||||
address,
|
||||
section_address,
|
||||
size: symbol.size(),
|
||||
size_known: symbol.size() != 0,
|
||||
kind,
|
||||
flags,
|
||||
addend,
|
||||
virtual_address,
|
||||
original_index: Some(symbol.index().0),
|
||||
bytes: bytes.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -129,52 +161,66 @@ fn symbols_by_section(
|
||||
arch: &dyn ObjArch,
|
||||
obj_file: &File<'_>,
|
||||
section: &ObjSection,
|
||||
section_symbols: &[Symbol<'_, '_>],
|
||||
split_meta: Option<&SplitMeta>,
|
||||
name_counts: &mut HashMap<String, u32>,
|
||||
) -> Result<Vec<ObjSymbol>> {
|
||||
let mut result = Vec::<ObjSymbol>::new();
|
||||
for symbol in obj_file.symbols() {
|
||||
for symbol in section_symbols {
|
||||
if symbol.kind() == SymbolKind::Section {
|
||||
continue;
|
||||
}
|
||||
if let Some(index) = symbol.section().index() {
|
||||
if index.0 == section.orig_index {
|
||||
if symbol.is_local() && section.kind == ObjSectionKind::Code {
|
||||
// TODO strip local syms in diff?
|
||||
let name = symbol.name().context("Failed to process symbol name")?;
|
||||
if symbol.size() == 0 || name.starts_with("lbl_") {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result.push(to_obj_symbol(arch, obj_file, &symbol, 0, split_meta)?);
|
||||
if symbol.is_local() && section.kind == ObjSectionKind::Code {
|
||||
// TODO strip local syms in diff?
|
||||
let name = symbol.name().context("Failed to process symbol name")?;
|
||||
if symbol.size() == 0 || name.starts_with("lbl_") {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result.push(to_obj_symbol(arch, obj_file, symbol, 0, split_meta)?);
|
||||
}
|
||||
result.sort_by(|a, b| a.address.cmp(&b.address).then(a.size.cmp(&b.size)));
|
||||
let mut iter = result.iter_mut().peekable();
|
||||
while let Some(symbol) = iter.next() {
|
||||
if symbol.size == 0 {
|
||||
if symbol.kind == ObjSymbolKind::Unknown && symbol.size == 0 {
|
||||
if let Some(next_symbol) = iter.peek() {
|
||||
symbol.size = next_symbol.address - symbol.address;
|
||||
} else {
|
||||
symbol.size = (section.address + section.size) - symbol.address;
|
||||
}
|
||||
// Set symbol kind if we ended up with a non-zero size
|
||||
if symbol.size > 0 {
|
||||
symbol.kind = match section.kind {
|
||||
ObjSectionKind::Code => ObjSymbolKind::Function,
|
||||
ObjSectionKind::Data | ObjSectionKind::Bss => ObjSymbolKind::Object,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if result.is_empty() {
|
||||
// Dummy symbol for empty sections
|
||||
*name_counts.entry(section.name.clone()).or_insert(0) += 1;
|
||||
let current_count: u32 = *name_counts.get(§ion.name).unwrap();
|
||||
result.push(ObjSymbol {
|
||||
name: format!("[{}]", section.name),
|
||||
name: if current_count > 1 {
|
||||
format!("[{} ({})]", section.name, current_count)
|
||||
} else {
|
||||
format!("[{}]", section.name)
|
||||
},
|
||||
demangled_name: None,
|
||||
has_extab: false,
|
||||
extab_name: None,
|
||||
extabindex_name: None,
|
||||
address: 0,
|
||||
section_address: 0,
|
||||
size: section.size,
|
||||
size_known: true,
|
||||
kind: match section.kind {
|
||||
ObjSectionKind::Code => ObjSymbolKind::Function,
|
||||
ObjSectionKind::Data | ObjSectionKind::Bss => ObjSymbolKind::Object,
|
||||
},
|
||||
flags: Default::default(),
|
||||
addend: 0,
|
||||
virtual_address: None,
|
||||
original_index: None,
|
||||
bytes: Vec::new(),
|
||||
});
|
||||
}
|
||||
Ok(result)
|
||||
@@ -192,156 +238,75 @@ fn common_symbols(
|
||||
.collect::<Result<Vec<ObjSymbol>>>()
|
||||
}
|
||||
|
||||
fn section_by_name<'a>(sections: &'a mut [ObjSection], name: &str) -> Option<&'a mut ObjSection> {
|
||||
sections.iter_mut().find(|section| section.name == name)
|
||||
}
|
||||
const LOW_PRIORITY_SYMBOLS: &[&str] =
|
||||
&["__gnu_compiled_c", "__gnu_compiled_cplusplus", "gcc2_compiled."];
|
||||
|
||||
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);
|
||||
fn best_symbol<'r, 'data, 'file>(
|
||||
symbols: &'r [Symbol<'data, 'file>],
|
||||
address: u64,
|
||||
) -> Option<&'r Symbol<'data, 'file>> {
|
||||
let closest_symbol_index = match symbols.binary_search_by_key(&address, |s| s.address()) {
|
||||
Ok(index) => Some(index),
|
||||
Err(index) => index.checked_sub(1),
|
||||
}?;
|
||||
let mut best_symbol: Option<&'r Symbol<'data, 'file>> = None;
|
||||
for symbol in symbols.iter().skip(closest_symbol_index) {
|
||||
if symbol.address() > address {
|
||||
break;
|
||||
}
|
||||
};
|
||||
let extabindex_section = match section_by_name(sections, "extabindex") {
|
||||
Some(section) => section.clone(),
|
||||
None => {
|
||||
return Ok(None);
|
||||
if symbol.kind() == SymbolKind::Section
|
||||
|| (symbol.size() > 0 && (symbol.address() + symbol.size()) <= address)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
};
|
||||
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());
|
||||
// TODO priority ranking with visibility, etc
|
||||
if let Some(best) = best_symbol {
|
||||
if LOW_PRIORITY_SYMBOLS.contains(&best.name().unwrap_or_default())
|
||||
&& !LOW_PRIORITY_SYMBOLS.contains(&symbol.name().unwrap_or_default())
|
||||
{
|
||||
best_symbol = Some(symbol);
|
||||
}
|
||||
} else {
|
||||
best_symbol = Some(symbol);
|
||||
}
|
||||
|
||||
/* 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))
|
||||
best_symbol
|
||||
}
|
||||
|
||||
fn find_section_symbol(
|
||||
arch: &dyn ObjArch,
|
||||
obj_file: &File<'_>,
|
||||
section_symbols: &[Symbol<'_, '_>],
|
||||
target: &Symbol<'_, '_>,
|
||||
address: u64,
|
||||
split_meta: Option<&SplitMeta>,
|
||||
) -> Result<ObjSymbol> {
|
||||
if let Some(symbol) = best_symbol(section_symbols, address) {
|
||||
return to_obj_symbol(
|
||||
arch,
|
||||
obj_file,
|
||||
symbol,
|
||||
address as i64 - symbol.address() as i64,
|
||||
split_meta,
|
||||
);
|
||||
}
|
||||
// Fallback to section symbol
|
||||
let section_index =
|
||||
target.section_index().ok_or_else(|| anyhow::Error::msg("Unknown section index"))?;
|
||||
let section = obj_file.section_by_index(section_index)?;
|
||||
let mut closest_symbol: Option<Symbol<'_, '_>> = None;
|
||||
for symbol in obj_file.symbols() {
|
||||
if !matches!(symbol.section_index(), Some(idx) if idx == section_index) {
|
||||
continue;
|
||||
}
|
||||
if symbol.kind() == SymbolKind::Section || symbol.address() != address {
|
||||
if symbol.address() < address
|
||||
&& symbol.size() != 0
|
||||
&& (closest_symbol.is_none()
|
||||
|| matches!(&closest_symbol, Some(s) if s.address() <= symbol.address()))
|
||||
{
|
||||
closest_symbol = Some(symbol);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return to_obj_symbol(arch, obj_file, &symbol, 0, split_meta);
|
||||
}
|
||||
let (name, offset) = closest_symbol
|
||||
.and_then(|s| s.name().map(|n| (n, s.address())).ok())
|
||||
.or_else(|| section.name().map(|n| (n, section.address())).ok())
|
||||
.unwrap_or(("<unknown>", 0));
|
||||
let offset_addr = address - offset;
|
||||
Ok(ObjSymbol {
|
||||
name: name.to_string(),
|
||||
name: section.name()?.to_string(),
|
||||
demangled_name: None,
|
||||
has_extab: false,
|
||||
extab_name: None,
|
||||
extabindex_name: None,
|
||||
address: offset,
|
||||
section_address: address - section.address(),
|
||||
address: section.address(),
|
||||
section_address: 0,
|
||||
size: 0,
|
||||
size_known: false,
|
||||
kind: ObjSymbolKind::Section,
|
||||
flags: Default::default(),
|
||||
addend: offset_addr as i64,
|
||||
addend: address as i64 - section.address() as i64,
|
||||
virtual_address: None,
|
||||
original_index: None,
|
||||
bytes: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -349,6 +314,7 @@ fn relocations_by_section(
|
||||
arch: &dyn ObjArch,
|
||||
obj_file: &File<'_>,
|
||||
section: &ObjSection,
|
||||
section_symbols: &[Symbol<'_, '_>],
|
||||
split_meta: Option<&SplitMeta>,
|
||||
) -> Result<Vec<ObjReloc>> {
|
||||
let obj_section = obj_file.section_by_index(SectionIndex(section.orig_index))?;
|
||||
@@ -392,7 +358,14 @@ fn relocations_by_section(
|
||||
}
|
||||
SymbolKind::Section => {
|
||||
ensure!(addend >= 0, "Negative addend in reloc: {addend}");
|
||||
find_section_symbol(arch, obj_file, &symbol, addend as u64, split_meta)
|
||||
find_section_symbol(
|
||||
arch,
|
||||
obj_file,
|
||||
section_symbols,
|
||||
&symbol,
|
||||
addend as u64,
|
||||
split_meta,
|
||||
)
|
||||
}
|
||||
kind => Err(anyhow!("Unhandled relocation symbol type {kind:?}")),
|
||||
}?;
|
||||
@@ -401,7 +374,7 @@ fn relocations_by_section(
|
||||
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
|
||||
if let Some(section) = obj_file.section_by_name(".line") {
|
||||
let data = section.uncompressed_data()?;
|
||||
@@ -490,6 +463,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(())
|
||||
}
|
||||
|
||||
@@ -497,13 +585,11 @@ fn update_combined_symbol(symbol: ObjSymbol, address_change: i64) -> Result<ObjS
|
||||
Ok(ObjSymbol {
|
||||
name: symbol.name,
|
||||
demangled_name: symbol.demangled_name,
|
||||
has_extab: symbol.has_extab,
|
||||
extab_name: symbol.extab_name,
|
||||
extabindex_name: symbol.extabindex_name,
|
||||
address: (symbol.address as i64 + address_change).try_into()?,
|
||||
section_address: (symbol.section_address as i64 + address_change).try_into()?,
|
||||
size: symbol.size,
|
||||
size_known: symbol.size_known,
|
||||
kind: symbol.kind,
|
||||
flags: symbol.flags,
|
||||
addend: symbol.addend,
|
||||
virtual_address: if let Some(virtual_address) = symbol.virtual_address {
|
||||
@@ -511,6 +597,8 @@ fn update_combined_symbol(symbol: ObjSymbol, address_change: i64) -> Result<ObjS
|
||||
} else {
|
||||
None
|
||||
},
|
||||
original_index: symbol.original_index,
|
||||
bytes: symbol.bytes,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -611,19 +699,35 @@ pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result<ObjInfo> {
|
||||
let arch = new_arch(&obj_file)?;
|
||||
let split_meta = split_meta(&obj_file)?;
|
||||
let mut sections = filter_sections(&obj_file, split_meta.as_ref())?;
|
||||
let mut name_counts: HashMap<String, u32> = HashMap::new();
|
||||
for section in &mut sections {
|
||||
section.symbols =
|
||||
symbols_by_section(arch.as_ref(), &obj_file, section, split_meta.as_ref())?;
|
||||
section.relocations =
|
||||
relocations_by_section(arch.as_ref(), &obj_file, section, split_meta.as_ref())?;
|
||||
let mut symbols = obj_file
|
||||
.symbols()
|
||||
.filter(|s| s.section_index() == Some(SectionIndex(section.orig_index)))
|
||||
.collect::<Vec<_>>();
|
||||
symbols.sort_by_key(|s| s.address());
|
||||
section.symbols = symbols_by_section(
|
||||
arch.as_ref(),
|
||||
&obj_file,
|
||||
section,
|
||||
&symbols,
|
||||
split_meta.as_ref(),
|
||||
&mut name_counts,
|
||||
)?;
|
||||
section.relocations = relocations_by_section(
|
||||
arch.as_ref(),
|
||||
&obj_file,
|
||||
section,
|
||||
&symbols,
|
||||
split_meta.as_ref(),
|
||||
)?;
|
||||
}
|
||||
if config.combine_data_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 extab = exception_tables(&mut sections, &obj_file)?;
|
||||
Ok(ObjInfo { arch, path: None, timestamp: None, sections, common, extab, split_meta })
|
||||
Ok(ObjInfo { arch, path: None, timestamp: None, sections, common, split_meta })
|
||||
}
|
||||
|
||||
pub fn has_function(obj_path: &Path, symbol_name: &str) -> Result<bool> {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
[package]
|
||||
name = "objdiff-gui"
|
||||
version = "2.0.0-beta.5"
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/encounter/objdiff"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
readme = "../README.md"
|
||||
description = """
|
||||
A local diffing tool for decompilation projects.
|
||||
@@ -24,38 +24,39 @@ wgpu = ["eframe/wgpu", "dep:wgpu"]
|
||||
wsl = []
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.82"
|
||||
bytes = "1.6.0"
|
||||
cfg-if = "1.0.0"
|
||||
const_format = "0.2.32"
|
||||
cwdemangle = "1.0.0"
|
||||
cwextab = "0.2.3"
|
||||
dirs = "5.0.1"
|
||||
egui = "0.27.2"
|
||||
egui_extras = "0.27.2"
|
||||
filetime = "0.2.23"
|
||||
float-ord = "0.3.2"
|
||||
font-kit = "0.13.0"
|
||||
globset = { version = "0.4.14", features = ["serde1"] }
|
||||
log = "0.4.21"
|
||||
notify = { git = "https://github.com/encounter/notify", rev = "4c1783e8e041b5f69d4cf1750b9f07e335a0771e" }
|
||||
anyhow = "1.0"
|
||||
bytes = "1.7"
|
||||
cfg-if = "1.0"
|
||||
const_format = "0.2"
|
||||
cwdemangle = "1.0"
|
||||
cwextab = "1.0.2"
|
||||
dirs = "5.0"
|
||||
egui = "0.29"
|
||||
egui_extras = "0.29"
|
||||
filetime = "0.2"
|
||||
float-ord = "0.3"
|
||||
font-kit = "0.14"
|
||||
globset = { version = "0.4", features = ["serde1"] }
|
||||
log = "0.4"
|
||||
notify = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2" }
|
||||
objdiff-core = { path = "../objdiff-core", features = ["all"] }
|
||||
png = "0.17.13"
|
||||
pollster = "0.3.0"
|
||||
regex = "1.10.5"
|
||||
rfd = { version = "0.14.1" } #, default-features = false, features = ['xdg-portal']
|
||||
rlwinmdec = "1.0.1"
|
||||
ron = "0.8.1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1.0.116"
|
||||
shell-escape = "0.1.5"
|
||||
strum = { version = "0.26.2", features = ["derive"] }
|
||||
tempfile = "3.10.1"
|
||||
time = { version = "0.3.36", features = ["formatting", "local-offset"] }
|
||||
open = "5.3"
|
||||
png = "0.17"
|
||||
pollster = "0.3"
|
||||
regex = "1.11"
|
||||
rfd = { version = "0.15" } #, default-features = false, features = ['xdg-portal']
|
||||
rlwinmdec = "1.0"
|
||||
ron = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
shell-escape = "0.1"
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
tempfile = "3.13"
|
||||
time = { version = "0.3", features = ["formatting", "local-offset"] }
|
||||
|
||||
# Keep version in sync with egui
|
||||
[dependencies.eframe]
|
||||
version = "0.27.2"
|
||||
version = "0.29"
|
||||
features = [
|
||||
"default_fonts",
|
||||
"persistence",
|
||||
@@ -66,7 +67,7 @@ default-features = false
|
||||
|
||||
# Keep version in sync with eframe
|
||||
[dependencies.wgpu]
|
||||
version = "0.19.1"
|
||||
version = "22.1"
|
||||
features = [
|
||||
"dx12",
|
||||
"metal",
|
||||
@@ -77,23 +78,20 @@ default-features = false
|
||||
|
||||
# For Linux static binaries, use rustls
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
reqwest = { version = "0.12.4", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"] }
|
||||
self_update = { version = "0.40.0", default-features = false, features = ["rustls"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"] }
|
||||
self_update = { version = "0.41", default-features = false, features = ["rustls"] }
|
||||
|
||||
# For all other platforms, use native TLS
|
||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||
reqwest = { version = "0.12.4", default-features = false, features = ["blocking", "json", "multipart", "default-tls"] }
|
||||
self_update = "0.40.0"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "default-tls"] }
|
||||
self_update = "0.41"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
path-slash = "0.2.1"
|
||||
winapi = "0.3.9"
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
winres = "0.1.12"
|
||||
path-slash = "0.2"
|
||||
winapi = "0.3"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
exec = "0.3.1"
|
||||
exec = "0.3"
|
||||
|
||||
# native:
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
@@ -101,9 +99,11 @@ tracing-subscriber = "0.3"
|
||||
|
||||
# web:
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_error_panic_hook = "0.1"
|
||||
tracing-wasm = "0.2"
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = "1.0.82"
|
||||
vergen = { version = "8.3.1", features = ["build", "cargo", "git", "gitcl"] }
|
||||
anyhow = "1.0"
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
tauri-winres = "0.1"
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use anyhow::Result;
|
||||
use vergen::EmitBuilder;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
winres::WindowsResource::new().set_icon("assets/icon.ico").compile()?;
|
||||
let mut res = tauri_winres::WindowsResource::new();
|
||||
res.set_icon("assets/icon.ico");
|
||||
res.set_language(0x0409); // US English
|
||||
res.compile()?;
|
||||
}
|
||||
EmitBuilder::builder().fail_on_error().all_build().all_cargo().all_git().emit()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex, RwLock,
|
||||
},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use filetime::FileTime;
|
||||
@@ -14,7 +15,8 @@ use globset::{Glob, GlobSet};
|
||||
use notify::{RecursiveMode, Watcher};
|
||||
use objdiff_core::{
|
||||
config::{
|
||||
build_globset, ProjectConfigInfo, ProjectObject, ScratchConfig, DEFAULT_WATCH_PATTERNS,
|
||||
build_globset, save_project_config, ProjectConfig, ProjectConfigInfo, ProjectObject,
|
||||
ScratchConfig, SymbolMappings, DEFAULT_WATCH_PATTERNS,
|
||||
},
|
||||
diff::DiffObjConfig,
|
||||
};
|
||||
@@ -39,13 +41,12 @@ use crate::{
|
||||
frame_history::FrameHistory,
|
||||
function_diff::function_diff_ui,
|
||||
graphics::{graphics_window, GraphicsConfig, GraphicsViewState},
|
||||
jobs::jobs_ui,
|
||||
jobs::{jobs_menu_ui, jobs_window},
|
||||
rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState},
|
||||
symbol_diff::{symbol_diff_ui, DiffViewState, View},
|
||||
symbol_diff::{symbol_diff_ui, DiffViewAction, DiffViewNavigation, DiffViewState, View},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ViewState {
|
||||
pub jobs: JobQueue,
|
||||
pub config_state: ConfigViewState,
|
||||
@@ -61,10 +62,35 @@ pub struct ViewState {
|
||||
pub show_arch_config: bool,
|
||||
pub show_debug: bool,
|
||||
pub show_graphics: bool,
|
||||
pub show_jobs: bool,
|
||||
pub show_side_panel: bool,
|
||||
}
|
||||
|
||||
impl Default for ViewState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
jobs: Default::default(),
|
||||
config_state: Default::default(),
|
||||
demangle_state: Default::default(),
|
||||
rlwinm_decode_state: Default::default(),
|
||||
diff_state: Default::default(),
|
||||
graphics_state: Default::default(),
|
||||
frame_history: Default::default(),
|
||||
show_appearance_config: false,
|
||||
show_demangle: false,
|
||||
show_rlwinm_decode: false,
|
||||
show_project_config: false,
|
||||
show_arch_config: false,
|
||||
show_debug: false,
|
||||
show_graphics: false,
|
||||
show_jobs: false,
|
||||
show_side_panel: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The configuration for a single object file.
|
||||
#[derive(Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct ObjectConfig {
|
||||
pub name: String,
|
||||
pub target_path: Option<PathBuf>,
|
||||
@@ -72,6 +98,24 @@ pub struct ObjectConfig {
|
||||
pub reverse_fn_order: Option<bool>,
|
||||
pub complete: Option<bool>,
|
||||
pub scratch: Option<ScratchConfig>,
|
||||
pub source_path: Option<String>,
|
||||
#[serde(default)]
|
||||
pub symbol_mappings: SymbolMappings,
|
||||
}
|
||||
|
||||
impl From<&ProjectObject> for ObjectConfig {
|
||||
fn from(object: &ProjectObject) -> Self {
|
||||
Self {
|
||||
name: object.name().to_string(),
|
||||
target_path: object.target_path.clone(),
|
||||
base_path: object.base_path.clone(),
|
||||
reverse_fn_order: object.reverse_fn_order(),
|
||||
complete: object.complete(),
|
||||
scratch: object.scratch.clone(),
|
||||
source_path: object.source_path().cloned(),
|
||||
symbol_mappings: object.symbol_mappings.clone().unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -82,6 +126,46 @@ fn default_watch_patterns() -> Vec<Glob> {
|
||||
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
pub config: AppConfig,
|
||||
pub objects: Vec<ProjectObject>,
|
||||
pub object_nodes: Vec<ProjectObjectNode>,
|
||||
pub watcher_change: bool,
|
||||
pub config_change: bool,
|
||||
pub obj_change: bool,
|
||||
pub queue_build: bool,
|
||||
pub queue_reload: bool,
|
||||
pub current_project_config: Option<ProjectConfig>,
|
||||
pub project_config_info: Option<ProjectConfigInfo>,
|
||||
pub last_mod_check: Instant,
|
||||
/// The right object symbol name that we're selecting a left symbol for
|
||||
pub selecting_left: Option<String>,
|
||||
/// The left object symbol name that we're selecting a right symbol for
|
||||
pub selecting_right: Option<String>,
|
||||
pub config_error: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for AppState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
config: Default::default(),
|
||||
objects: vec![],
|
||||
object_nodes: vec![],
|
||||
watcher_change: false,
|
||||
config_change: false,
|
||||
obj_change: false,
|
||||
queue_build: false,
|
||||
queue_reload: false,
|
||||
current_project_config: None,
|
||||
project_config_info: None,
|
||||
last_mod_check: Instant::now(),
|
||||
selecting_left: None,
|
||||
selecting_right: None,
|
||||
config_error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct AppConfig {
|
||||
// TODO: https://github.com/ron-rs/ron/pull/455
|
||||
@@ -116,23 +200,6 @@ pub struct AppConfig {
|
||||
pub recent_projects: Vec<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub diff_obj_config: DiffObjConfig,
|
||||
|
||||
#[serde(skip)]
|
||||
pub objects: Vec<ProjectObject>,
|
||||
#[serde(skip)]
|
||||
pub object_nodes: Vec<ProjectObjectNode>,
|
||||
#[serde(skip)]
|
||||
pub watcher_change: bool,
|
||||
#[serde(skip)]
|
||||
pub config_change: bool,
|
||||
#[serde(skip)]
|
||||
pub obj_change: bool,
|
||||
#[serde(skip)]
|
||||
pub queue_build: bool,
|
||||
#[serde(skip)]
|
||||
pub queue_reload: bool,
|
||||
#[serde(skip)]
|
||||
pub project_config_info: Option<ProjectConfigInfo>,
|
||||
}
|
||||
|
||||
impl Default for AppConfig {
|
||||
@@ -153,67 +220,174 @@ impl Default for AppConfig {
|
||||
watch_patterns: DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(),
|
||||
recent_projects: vec![],
|
||||
diff_obj_config: Default::default(),
|
||||
objects: vec![],
|
||||
object_nodes: vec![],
|
||||
watcher_change: false,
|
||||
config_change: false,
|
||||
obj_change: false,
|
||||
queue_build: false,
|
||||
queue_reload: false,
|
||||
project_config_info: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
impl AppState {
|
||||
pub fn set_project_dir(&mut self, path: PathBuf) {
|
||||
self.recent_projects.retain(|p| p != &path);
|
||||
if self.recent_projects.len() > 9 {
|
||||
self.recent_projects.truncate(9);
|
||||
self.config.recent_projects.retain(|p| p != &path);
|
||||
if self.config.recent_projects.len() > 9 {
|
||||
self.config.recent_projects.truncate(9);
|
||||
}
|
||||
self.recent_projects.insert(0, path.clone());
|
||||
self.project_dir = Some(path);
|
||||
self.target_obj_dir = None;
|
||||
self.base_obj_dir = None;
|
||||
self.selected_obj = None;
|
||||
self.build_target = false;
|
||||
self.config.recent_projects.insert(0, path.clone());
|
||||
self.config.project_dir = Some(path);
|
||||
self.config.target_obj_dir = None;
|
||||
self.config.base_obj_dir = None;
|
||||
self.config.selected_obj = None;
|
||||
self.config.build_target = false;
|
||||
self.objects.clear();
|
||||
self.object_nodes.clear();
|
||||
self.watcher_change = true;
|
||||
self.config_change = true;
|
||||
self.obj_change = true;
|
||||
self.queue_build = false;
|
||||
self.current_project_config = None;
|
||||
self.project_config_info = None;
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
}
|
||||
|
||||
pub fn set_target_obj_dir(&mut self, path: PathBuf) {
|
||||
self.target_obj_dir = Some(path);
|
||||
self.selected_obj = None;
|
||||
self.config.target_obj_dir = Some(path);
|
||||
self.config.selected_obj = None;
|
||||
self.obj_change = true;
|
||||
self.queue_build = false;
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
}
|
||||
|
||||
pub fn set_base_obj_dir(&mut self, path: PathBuf) {
|
||||
self.base_obj_dir = Some(path);
|
||||
self.selected_obj = None;
|
||||
self.config.base_obj_dir = Some(path);
|
||||
self.config.selected_obj = None;
|
||||
self.obj_change = true;
|
||||
self.queue_build = false;
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
}
|
||||
|
||||
pub fn set_selected_obj(&mut self, object: ObjectConfig) {
|
||||
self.selected_obj = Some(object);
|
||||
pub fn set_selected_obj(&mut self, config: ObjectConfig) {
|
||||
if self.config.selected_obj.as_ref().is_some_and(|existing| existing == &config) {
|
||||
// Don't reload the object if there were no changes
|
||||
return;
|
||||
}
|
||||
self.config.selected_obj = Some(config);
|
||||
self.obj_change = true;
|
||||
self.queue_build = false;
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
}
|
||||
|
||||
pub fn clear_selected_obj(&mut self) {
|
||||
self.config.selected_obj = None;
|
||||
self.obj_change = true;
|
||||
self.queue_build = false;
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
}
|
||||
|
||||
pub fn set_selecting_left(&mut self, right: &str) {
|
||||
let Some(object) = self.config.selected_obj.as_mut() else {
|
||||
return;
|
||||
};
|
||||
object.symbol_mappings.remove_by_right(right);
|
||||
self.selecting_left = Some(right.to_string());
|
||||
self.queue_reload = true;
|
||||
self.save_config();
|
||||
}
|
||||
|
||||
pub fn set_selecting_right(&mut self, left: &str) {
|
||||
let Some(object) = self.config.selected_obj.as_mut() else {
|
||||
return;
|
||||
};
|
||||
object.symbol_mappings.remove_by_left(left);
|
||||
self.selecting_right = Some(left.to_string());
|
||||
self.queue_reload = true;
|
||||
self.save_config();
|
||||
}
|
||||
|
||||
pub fn set_symbol_mapping(&mut self, left: String, right: String) {
|
||||
let Some(object) = self.config.selected_obj.as_mut() else {
|
||||
log::warn!("No selected object");
|
||||
return;
|
||||
};
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
if left == right {
|
||||
object.symbol_mappings.remove_by_left(&left);
|
||||
object.symbol_mappings.remove_by_right(&right);
|
||||
} else {
|
||||
object.symbol_mappings.insert(left.clone(), right.clone());
|
||||
}
|
||||
self.queue_reload = true;
|
||||
self.save_config();
|
||||
}
|
||||
|
||||
pub fn clear_selection(&mut self) {
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
self.queue_reload = true;
|
||||
}
|
||||
|
||||
pub fn clear_mappings(&mut self) {
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
if let Some(object) = self.config.selected_obj.as_mut() {
|
||||
object.symbol_mappings.clear();
|
||||
}
|
||||
self.queue_reload = true;
|
||||
self.save_config();
|
||||
}
|
||||
|
||||
pub fn is_selecting_symbol(&self) -> bool {
|
||||
self.selecting_left.is_some() || self.selecting_right.is_some()
|
||||
}
|
||||
|
||||
pub fn save_config(&mut self) {
|
||||
let (Some(config), Some(info)) =
|
||||
(self.current_project_config.as_mut(), self.project_config_info.as_mut())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
// Update the project config with the current state
|
||||
if let Some(object) = self.config.selected_obj.as_ref() {
|
||||
if let Some(existing) = config.units.as_mut().and_then(|v| {
|
||||
v.iter_mut().find(|u| u.name.as_ref().is_some_and(|n| n == &object.name))
|
||||
}) {
|
||||
existing.symbol_mappings = if object.symbol_mappings.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(object.symbol_mappings.clone())
|
||||
};
|
||||
}
|
||||
if let Some(existing) =
|
||||
self.objects.iter_mut().find(|u| u.name.as_ref().is_some_and(|n| n == &object.name))
|
||||
{
|
||||
existing.symbol_mappings = if object.symbol_mappings.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(object.symbol_mappings.clone())
|
||||
};
|
||||
}
|
||||
}
|
||||
// Save the updated project config
|
||||
match save_project_config(config, info) {
|
||||
Ok(new_info) => *info = new_info,
|
||||
Err(e) => {
|
||||
log::error!("Failed to save project config: {e}");
|
||||
self.config_error = Some(format!("Failed to save project config: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type AppConfigRef = Arc<RwLock<AppConfig>>;
|
||||
pub type AppStateRef = Arc<RwLock<AppState>>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct App {
|
||||
appearance: Appearance,
|
||||
view_state: ViewState,
|
||||
config: AppConfigRef,
|
||||
state: AppStateRef,
|
||||
modified: Arc<AtomicBool>,
|
||||
watcher: Option<notify::RecommendedWatcher>,
|
||||
app_path: Option<PathBuf>,
|
||||
@@ -241,16 +415,17 @@ impl App {
|
||||
if let Some(appearance) = eframe::get_value::<Appearance>(storage, APPEARANCE_KEY) {
|
||||
app.appearance = appearance;
|
||||
}
|
||||
if let Some(mut config) = deserialize_config(storage) {
|
||||
if config.project_dir.is_some() {
|
||||
config.config_change = true;
|
||||
config.watcher_change = true;
|
||||
if let Some(config) = deserialize_config(storage) {
|
||||
let mut state = AppState { config, ..Default::default() };
|
||||
if state.config.project_dir.is_some() {
|
||||
state.config_change = true;
|
||||
state.watcher_change = true;
|
||||
}
|
||||
if config.selected_obj.is_some() {
|
||||
config.queue_build = true;
|
||||
if state.config.selected_obj.is_some() {
|
||||
state.queue_build = true;
|
||||
}
|
||||
app.view_state.config_state.queue_check_update = config.auto_update_check;
|
||||
app.config = Arc::new(RwLock::new(config));
|
||||
app.view_state.config_state.queue_check_update = state.config.auto_update_check;
|
||||
app.state = Arc::new(RwLock::new(state));
|
||||
}
|
||||
}
|
||||
app.appearance.init_fonts(&cc.egui_ctx);
|
||||
@@ -336,81 +511,93 @@ impl App {
|
||||
jobs.results.append(&mut results);
|
||||
jobs.clear_finished();
|
||||
|
||||
diff_state.pre_update(jobs, &self.config);
|
||||
config_state.pre_update(jobs, &self.config);
|
||||
diff_state.pre_update(jobs, &self.state);
|
||||
config_state.pre_update(jobs, &self.state);
|
||||
debug_assert!(jobs.results.is_empty());
|
||||
}
|
||||
|
||||
fn post_update(&mut self, ctx: &egui::Context) {
|
||||
fn post_update(&mut self, ctx: &egui::Context, action: Option<DiffViewAction>) {
|
||||
self.appearance.post_update(ctx);
|
||||
|
||||
let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state;
|
||||
config_state.post_update(ctx, jobs, &self.config);
|
||||
diff_state.post_update(ctx, jobs, &self.config);
|
||||
config_state.post_update(ctx, jobs, &self.state);
|
||||
diff_state.post_update(action, ctx, jobs, &self.state);
|
||||
|
||||
let Ok(mut config) = self.config.write() else {
|
||||
let Ok(mut state) = self.state.write() else {
|
||||
return;
|
||||
};
|
||||
let config = &mut *config;
|
||||
let state = &mut *state;
|
||||
|
||||
if let Some(info) = &config.project_config_info {
|
||||
if file_modified(&info.path, info.timestamp) {
|
||||
config.config_change = true;
|
||||
}
|
||||
let mut mod_check = false;
|
||||
if state.last_mod_check.elapsed().as_millis() >= 500 {
|
||||
state.last_mod_check = Instant::now();
|
||||
mod_check = true;
|
||||
}
|
||||
|
||||
if config.config_change {
|
||||
config.config_change = false;
|
||||
match load_project_config(config) {
|
||||
Ok(()) => config_state.load_error = None,
|
||||
Err(e) => {
|
||||
log::error!("Failed to load project config: {e}");
|
||||
config_state.load_error = Some(format!("{e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if config.watcher_change {
|
||||
drop(self.watcher.take());
|
||||
|
||||
if let Some(project_dir) = &config.project_dir {
|
||||
match build_globset(&config.watch_patterns).map_err(anyhow::Error::new).and_then(
|
||||
|globset| {
|
||||
create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset)
|
||||
.map_err(anyhow::Error::new)
|
||||
},
|
||||
) {
|
||||
Ok(watcher) => self.watcher = Some(watcher),
|
||||
Err(e) => log::error!("Failed to create watcher: {e}"),
|
||||
}
|
||||
config.watcher_change = false;
|
||||
}
|
||||
}
|
||||
|
||||
if config.obj_change {
|
||||
*diff_state = Default::default();
|
||||
if config.selected_obj.is_some() {
|
||||
config.queue_build = true;
|
||||
}
|
||||
config.obj_change = false;
|
||||
}
|
||||
|
||||
if self.modified.swap(false, Ordering::Relaxed) && config.rebuild_on_changes {
|
||||
config.queue_build = true;
|
||||
}
|
||||
|
||||
if let Some(result) = &diff_state.build {
|
||||
if let Some((obj, _)) = &result.first_obj {
|
||||
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
|
||||
if file_modified(path, timestamp) {
|
||||
config.queue_reload = true;
|
||||
if mod_check {
|
||||
if let Some(info) = &state.project_config_info {
|
||||
if let Some(last_ts) = info.timestamp {
|
||||
if file_modified(&info.path, last_ts) {
|
||||
state.config_change = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((obj, _)) = &result.second_obj {
|
||||
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
|
||||
if file_modified(path, timestamp) {
|
||||
config.queue_reload = true;
|
||||
}
|
||||
|
||||
if state.config_change {
|
||||
state.config_change = false;
|
||||
match load_project_config(state) {
|
||||
Ok(()) => state.config_error = None,
|
||||
Err(e) => {
|
||||
log::error!("Failed to load project config: {e}");
|
||||
state.config_error = Some(format!("{e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if state.watcher_change {
|
||||
drop(self.watcher.take());
|
||||
|
||||
if let Some(project_dir) = &state.config.project_dir {
|
||||
match build_globset(&state.config.watch_patterns)
|
||||
.map_err(anyhow::Error::new)
|
||||
.and_then(|globset| {
|
||||
create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset)
|
||||
.map_err(anyhow::Error::new)
|
||||
}) {
|
||||
Ok(watcher) => self.watcher = Some(watcher),
|
||||
Err(e) => log::error!("Failed to create watcher: {e}"),
|
||||
}
|
||||
state.watcher_change = false;
|
||||
}
|
||||
}
|
||||
|
||||
if state.obj_change {
|
||||
*diff_state = Default::default();
|
||||
if state.config.selected_obj.is_some() {
|
||||
state.queue_build = true;
|
||||
}
|
||||
state.obj_change = false;
|
||||
}
|
||||
|
||||
if self.modified.swap(false, Ordering::Relaxed) && state.config.rebuild_on_changes {
|
||||
state.queue_build = true;
|
||||
}
|
||||
|
||||
if let Some(result) = &diff_state.build {
|
||||
if mod_check {
|
||||
if let Some((obj, _)) = &result.first_obj {
|
||||
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
|
||||
if file_modified(path, timestamp) {
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((obj, _)) = &result.second_obj {
|
||||
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
|
||||
if file_modified(path, timestamp) {
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,17 +605,20 @@ impl App {
|
||||
|
||||
// Don't clear `queue_build` if a build is running. A file may have been modified during
|
||||
// the build, so we'll start another build after the current one finishes.
|
||||
if config.queue_build && config.selected_obj.is_some() && !jobs.is_running(Job::ObjDiff) {
|
||||
jobs.push(start_build(ctx, ObjDiffConfig::from_config(config)));
|
||||
config.queue_build = false;
|
||||
config.queue_reload = false;
|
||||
} else if config.queue_reload && !jobs.is_running(Job::ObjDiff) {
|
||||
let mut diff_config = ObjDiffConfig::from_config(config);
|
||||
if state.queue_build
|
||||
&& state.config.selected_obj.is_some()
|
||||
&& !jobs.is_running(Job::ObjDiff)
|
||||
{
|
||||
jobs.push(start_build(ctx, ObjDiffConfig::from_state(state)));
|
||||
state.queue_build = false;
|
||||
state.queue_reload = false;
|
||||
} else if state.queue_reload && !jobs.is_running(Job::ObjDiff) {
|
||||
let mut diff_config = ObjDiffConfig::from_state(state);
|
||||
// Don't build, just reload the current files
|
||||
diff_config.build_base = false;
|
||||
diff_config.build_target = false;
|
||||
jobs.push(start_build(ctx, diff_config));
|
||||
config.queue_reload = false;
|
||||
state.queue_reload = false;
|
||||
}
|
||||
|
||||
if graphics_state.should_relaunch {
|
||||
@@ -453,7 +643,7 @@ impl eframe::App for App {
|
||||
|
||||
self.pre_update(ctx);
|
||||
|
||||
let Self { config, appearance, view_state, .. } = self;
|
||||
let Self { state, appearance, view_state, .. } = self;
|
||||
let ViewState {
|
||||
jobs,
|
||||
config_state,
|
||||
@@ -469,12 +659,27 @@ impl eframe::App for App {
|
||||
show_arch_config,
|
||||
show_debug,
|
||||
show_graphics,
|
||||
show_jobs,
|
||||
show_side_panel,
|
||||
} = view_state;
|
||||
|
||||
frame_history.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
|
||||
|
||||
let side_panel_available = diff_state.current_view == View::SymbolDiff;
|
||||
|
||||
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
if ui
|
||||
.add_enabled(
|
||||
side_panel_available,
|
||||
egui::Button::new(if *show_side_panel { "⏴" } else { "⏵" }),
|
||||
)
|
||||
.on_hover_text("Toggle side panel")
|
||||
.clicked()
|
||||
{
|
||||
*show_side_panel = !*show_side_panel;
|
||||
}
|
||||
ui.separator();
|
||||
ui.menu_button("File", |ui| {
|
||||
#[cfg(debug_assertions)]
|
||||
if ui.button("Debug…").clicked() {
|
||||
@@ -485,8 +690,8 @@ impl eframe::App for App {
|
||||
*show_project_config = !*show_project_config;
|
||||
ui.close_menu();
|
||||
}
|
||||
let recent_projects = if let Ok(guard) = config.read() {
|
||||
guard.recent_projects.clone()
|
||||
let recent_projects = if let Ok(guard) = state.read() {
|
||||
guard.config.recent_projects.clone()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
@@ -495,12 +700,12 @@ impl eframe::App for App {
|
||||
} else {
|
||||
ui.menu_button("Recent Projects…", |ui| {
|
||||
if ui.button("Clear").clicked() {
|
||||
config.write().unwrap().recent_projects.clear();
|
||||
state.write().unwrap().config.recent_projects.clear();
|
||||
};
|
||||
ui.separator();
|
||||
for path in recent_projects {
|
||||
if ui.button(format!("{}", path.display())).clicked() {
|
||||
config.write().unwrap().set_project_dir(path);
|
||||
state.write().unwrap().set_project_dir(path);
|
||||
ui.close_menu();
|
||||
}
|
||||
}
|
||||
@@ -533,12 +738,12 @@ impl eframe::App for App {
|
||||
*show_arch_config = !*show_arch_config;
|
||||
ui.close_menu();
|
||||
}
|
||||
let mut config = config.write().unwrap();
|
||||
let mut state = state.write().unwrap();
|
||||
let response = ui
|
||||
.checkbox(&mut config.rebuild_on_changes, "Rebuild on changes")
|
||||
.checkbox(&mut state.config.rebuild_on_changes, "Rebuild on changes")
|
||||
.on_hover_text("Automatically re-run the build & diff when files change.");
|
||||
if response.changed() {
|
||||
config.watcher_change = true;
|
||||
state.watcher_change = true;
|
||||
};
|
||||
ui.add_enabled(
|
||||
!diff_state.symbol_state.disable_reverse_fn_order,
|
||||
@@ -554,7 +759,7 @@ impl eframe::App for App {
|
||||
);
|
||||
if ui
|
||||
.checkbox(
|
||||
&mut config.diff_obj_config.relax_reloc_diffs,
|
||||
&mut state.config.diff_obj_config.relax_reloc_diffs,
|
||||
"Relax relocation diffs",
|
||||
)
|
||||
.on_hover_text(
|
||||
@@ -562,72 +767,78 @@ impl eframe::App for App {
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
config.queue_reload = true;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
if ui
|
||||
.checkbox(
|
||||
&mut config.diff_obj_config.space_between_args,
|
||||
&mut state.config.diff_obj_config.space_between_args,
|
||||
"Space between args",
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
config.queue_reload = true;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
if ui
|
||||
.checkbox(
|
||||
&mut config.diff_obj_config.combine_data_sections,
|
||||
&mut state.config.diff_obj_config.combine_data_sections,
|
||||
"Combine data sections",
|
||||
)
|
||||
.on_hover_text("Combines data sections with equal names.")
|
||||
.changed()
|
||||
{
|
||||
config.queue_reload = true;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
if ui.button("Clear custom symbol mappings").clicked() {
|
||||
state.clear_mappings();
|
||||
diff_state.post_build_nav = Some(DiffViewNavigation::symbol_diff());
|
||||
state.queue_reload = true;
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
if jobs_menu_ui(ui, jobs, appearance) {
|
||||
*show_jobs = !*show_jobs;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let build_success = matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success);
|
||||
if diff_state.current_view == View::FunctionDiff && build_success {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
function_diff_ui(ui, diff_state, appearance);
|
||||
});
|
||||
} else if diff_state.current_view == View::DataDiff && build_success {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
data_diff_ui(ui, diff_state, appearance);
|
||||
});
|
||||
} else if diff_state.current_view == View::ExtabDiff && build_success {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
extab_diff_ui(ui, diff_state, appearance);
|
||||
});
|
||||
} else {
|
||||
egui::SidePanel::left("side_panel").show(ctx, |ui| {
|
||||
if side_panel_available {
|
||||
egui::SidePanel::left("side_panel").show_animated(ctx, *show_side_panel, |ui| {
|
||||
egui::ScrollArea::both().show(ui, |ui| {
|
||||
config_ui(ui, config, show_project_config, config_state, appearance);
|
||||
jobs_ui(ui, jobs, appearance);
|
||||
config_ui(ui, state, show_project_config, config_state, appearance);
|
||||
});
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
symbol_diff_ui(ui, diff_state, appearance);
|
||||
});
|
||||
}
|
||||
|
||||
project_window(ctx, config, show_project_config, config_state, appearance);
|
||||
let mut action = None;
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
let build_success = matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success);
|
||||
action = if diff_state.current_view == View::FunctionDiff && build_success {
|
||||
function_diff_ui(ui, diff_state, appearance)
|
||||
} else if diff_state.current_view == View::DataDiff && build_success {
|
||||
data_diff_ui(ui, diff_state, appearance)
|
||||
} else if diff_state.current_view == View::ExtabDiff && build_success {
|
||||
extab_diff_ui(ui, diff_state, appearance)
|
||||
} else {
|
||||
symbol_diff_ui(ui, diff_state, appearance)
|
||||
};
|
||||
});
|
||||
|
||||
project_window(ctx, state, show_project_config, config_state, appearance);
|
||||
appearance_window(ctx, show_appearance_config, appearance);
|
||||
demangle_window(ctx, show_demangle, demangle_state, appearance);
|
||||
rlwinm_decode_window(ctx, show_rlwinm_decode, rlwinm_decode_state, appearance);
|
||||
arch_config_window(ctx, config, show_arch_config, appearance);
|
||||
arch_config_window(ctx, state, show_arch_config, appearance);
|
||||
debug_window(ctx, show_debug, frame_history, appearance);
|
||||
graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance);
|
||||
jobs_window(ctx, show_jobs, jobs, appearance);
|
||||
|
||||
self.post_update(ctx);
|
||||
self.post_update(ctx, action);
|
||||
}
|
||||
|
||||
/// Called by the frame work to save state before shutdown.
|
||||
/// Called by the framework to save state before shutdown.
|
||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||
if let Ok(config) = self.config.read() {
|
||||
eframe::set_value(storage, CONFIG_KEY, &*config);
|
||||
if let Ok(state) = self.state.read() {
|
||||
eframe::set_value(storage, CONFIG_KEY, &state.config);
|
||||
}
|
||||
eframe::set_value(storage, APPEARANCE_KEY, &self.appearance);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@ use std::path::PathBuf;
|
||||
|
||||
use eframe::Storage;
|
||||
use globset::Glob;
|
||||
use objdiff_core::{
|
||||
config::ScratchConfig,
|
||||
diff::{ArmArchVersion, ArmR9Usage, DiffObjConfig, MipsAbi, MipsInstrCategory, X86Formatter},
|
||||
};
|
||||
|
||||
use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY};
|
||||
|
||||
@@ -11,7 +15,7 @@ pub struct AppConfigVersion {
|
||||
}
|
||||
|
||||
impl Default for AppConfigVersion {
|
||||
fn default() -> Self { Self { version: 1 } }
|
||||
fn default() -> Self { Self { version: 2 } }
|
||||
}
|
||||
|
||||
/// Deserialize the AppConfig from storage, handling upgrades from older versions.
|
||||
@@ -19,7 +23,8 @@ pub fn deserialize_config(storage: &dyn Storage) -> Option<AppConfig> {
|
||||
let str = storage.get_string(CONFIG_KEY)?;
|
||||
match ron::from_str::<AppConfigVersion>(&str) {
|
||||
Ok(version) => match version.version {
|
||||
1 => from_str::<AppConfig>(&str),
|
||||
2 => from_str::<AppConfig>(&str),
|
||||
1 => from_str::<AppConfigV1>(&str).map(|c| c.into_config()),
|
||||
_ => {
|
||||
log::warn!("Unknown config version: {}", version.version);
|
||||
None
|
||||
@@ -44,6 +49,180 @@ where T: serde::de::DeserializeOwned {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct ScratchConfigV1 {
|
||||
#[serde(default)]
|
||||
pub platform: Option<String>,
|
||||
#[serde(default)]
|
||||
pub compiler: Option<String>,
|
||||
#[serde(default)]
|
||||
pub c_flags: Option<String>,
|
||||
#[serde(default)]
|
||||
pub ctx_path: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub build_ctx: bool,
|
||||
}
|
||||
|
||||
impl ScratchConfigV1 {
|
||||
fn into_config(self) -> ScratchConfig {
|
||||
ScratchConfig {
|
||||
platform: self.platform,
|
||||
compiler: self.compiler,
|
||||
c_flags: self.c_flags,
|
||||
ctx_path: self.ctx_path,
|
||||
build_ctx: self.build_ctx.then_some(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct ObjectConfigV1 {
|
||||
pub name: String,
|
||||
pub target_path: Option<PathBuf>,
|
||||
pub base_path: Option<PathBuf>,
|
||||
pub reverse_fn_order: Option<bool>,
|
||||
pub complete: Option<bool>,
|
||||
pub scratch: Option<ScratchConfigV1>,
|
||||
pub source_path: Option<String>,
|
||||
}
|
||||
|
||||
impl ObjectConfigV1 {
|
||||
fn into_config(self) -> ObjectConfig {
|
||||
ObjectConfig {
|
||||
name: self.name,
|
||||
target_path: self.target_path,
|
||||
base_path: self.base_path,
|
||||
reverse_fn_order: self.reverse_fn_order,
|
||||
complete: self.complete,
|
||||
scratch: self.scratch.map(|scratch| scratch.into_config()),
|
||||
source_path: self.source_path,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct DiffObjConfigV1 {
|
||||
pub relax_reloc_diffs: bool,
|
||||
#[serde(default = "bool_true")]
|
||||
pub space_between_args: bool,
|
||||
pub combine_data_sections: bool,
|
||||
// x86
|
||||
pub x86_formatter: X86Formatter,
|
||||
// MIPS
|
||||
pub mips_abi: MipsAbi,
|
||||
pub mips_instr_category: MipsInstrCategory,
|
||||
// ARM
|
||||
pub arm_arch_version: ArmArchVersion,
|
||||
pub arm_unified_syntax: bool,
|
||||
pub arm_av_registers: bool,
|
||||
pub arm_r9_usage: ArmR9Usage,
|
||||
pub arm_sl_usage: bool,
|
||||
pub arm_fp_usage: bool,
|
||||
pub arm_ip_usage: bool,
|
||||
}
|
||||
|
||||
impl Default for DiffObjConfigV1 {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
relax_reloc_diffs: false,
|
||||
space_between_args: true,
|
||||
combine_data_sections: false,
|
||||
x86_formatter: Default::default(),
|
||||
mips_abi: Default::default(),
|
||||
mips_instr_category: Default::default(),
|
||||
arm_arch_version: Default::default(),
|
||||
arm_unified_syntax: true,
|
||||
arm_av_registers: false,
|
||||
arm_r9_usage: Default::default(),
|
||||
arm_sl_usage: false,
|
||||
arm_fp_usage: false,
|
||||
arm_ip_usage: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DiffObjConfigV1 {
|
||||
fn into_config(self) -> DiffObjConfig {
|
||||
DiffObjConfig {
|
||||
relax_reloc_diffs: self.relax_reloc_diffs,
|
||||
space_between_args: self.space_between_args,
|
||||
combine_data_sections: self.combine_data_sections,
|
||||
x86_formatter: self.x86_formatter,
|
||||
mips_abi: self.mips_abi,
|
||||
mips_instr_category: self.mips_instr_category,
|
||||
arm_arch_version: self.arm_arch_version,
|
||||
arm_unified_syntax: self.arm_unified_syntax,
|
||||
arm_av_registers: self.arm_av_registers,
|
||||
arm_r9_usage: self.arm_r9_usage,
|
||||
arm_sl_usage: self.arm_sl_usage,
|
||||
arm_fp_usage: self.arm_fp_usage,
|
||||
arm_ip_usage: self.arm_ip_usage,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn bool_true() -> bool { true }
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct AppConfigV1 {
|
||||
pub version: u32,
|
||||
#[serde(default)]
|
||||
pub custom_make: Option<String>,
|
||||
#[serde(default)]
|
||||
pub custom_args: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub selected_wsl_distro: Option<String>,
|
||||
#[serde(default)]
|
||||
pub project_dir: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub target_obj_dir: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub base_obj_dir: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub selected_obj: Option<ObjectConfigV1>,
|
||||
#[serde(default = "bool_true")]
|
||||
pub build_base: bool,
|
||||
#[serde(default)]
|
||||
pub build_target: bool,
|
||||
#[serde(default = "bool_true")]
|
||||
pub rebuild_on_changes: bool,
|
||||
#[serde(default)]
|
||||
pub auto_update_check: bool,
|
||||
#[serde(default)]
|
||||
pub watch_patterns: Vec<Glob>,
|
||||
#[serde(default)]
|
||||
pub recent_projects: Vec<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub diff_obj_config: DiffObjConfigV1,
|
||||
}
|
||||
|
||||
impl AppConfigV1 {
|
||||
fn into_config(self) -> AppConfig {
|
||||
log::info!("Upgrading configuration from v1");
|
||||
AppConfig {
|
||||
custom_make: self.custom_make,
|
||||
custom_args: self.custom_args,
|
||||
selected_wsl_distro: self.selected_wsl_distro,
|
||||
project_dir: self.project_dir,
|
||||
target_obj_dir: self.target_obj_dir,
|
||||
base_obj_dir: self.base_obj_dir,
|
||||
selected_obj: self.selected_obj.map(|obj| obj.into_config()),
|
||||
build_base: self.build_base,
|
||||
build_target: self.build_target,
|
||||
rebuild_on_changes: self.rebuild_on_changes,
|
||||
auto_update_check: self.auto_update_check,
|
||||
watch_patterns: self.watch_patterns,
|
||||
recent_projects: self.recent_projects,
|
||||
diff_obj_config: self.diff_obj_config.into_config(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct ObjectConfigV0 {
|
||||
pub name: String,
|
||||
@@ -59,8 +238,7 @@ impl ObjectConfigV0 {
|
||||
target_path: Some(self.target_path),
|
||||
base_path: Some(self.base_path),
|
||||
reverse_fn_order: self.reverse_fn_order,
|
||||
complete: None,
|
||||
scratch: None,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ use anyhow::Result;
|
||||
use globset::Glob;
|
||||
use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS};
|
||||
|
||||
use crate::app::AppConfig;
|
||||
use crate::app::{AppState, ObjectConfig};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ProjectObjectNode {
|
||||
File(String, Box<ProjectObject>),
|
||||
Unit(String, usize),
|
||||
Dir(String, Vec<ProjectObjectNode>),
|
||||
}
|
||||
|
||||
@@ -33,17 +33,18 @@ fn find_dir<'a>(
|
||||
}
|
||||
|
||||
fn build_nodes(
|
||||
objects: &[ProjectObject],
|
||||
units: &mut [ProjectObject],
|
||||
project_dir: &Path,
|
||||
target_obj_dir: Option<&Path>,
|
||||
base_obj_dir: Option<&Path>,
|
||||
) -> Vec<ProjectObjectNode> {
|
||||
let mut nodes = vec![];
|
||||
for object in objects {
|
||||
for (idx, unit) in units.iter_mut().enumerate() {
|
||||
unit.resolve_paths(project_dir, target_obj_dir, base_obj_dir);
|
||||
let mut out_nodes = &mut nodes;
|
||||
let path = if let Some(name) = &object.name {
|
||||
let path = if let Some(name) = &unit.name {
|
||||
Path::new(name)
|
||||
} else if let Some(path) = &object.path {
|
||||
} else if let Some(path) = &unit.path {
|
||||
path
|
||||
} else {
|
||||
continue;
|
||||
@@ -56,38 +57,48 @@ fn build_nodes(
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut object = Box::new(object.clone());
|
||||
object.resolve_paths(project_dir, target_obj_dir, base_obj_dir);
|
||||
let filename = path.file_name().unwrap().to_str().unwrap().to_string();
|
||||
out_nodes.push(ProjectObjectNode::File(filename, object));
|
||||
out_nodes.push(ProjectObjectNode::Unit(filename, idx));
|
||||
}
|
||||
nodes
|
||||
}
|
||||
|
||||
pub fn load_project_config(config: &mut AppConfig) -> Result<()> {
|
||||
let Some(project_dir) = &config.project_dir else {
|
||||
pub fn load_project_config(state: &mut AppState) -> Result<()> {
|
||||
let Some(project_dir) = &state.config.project_dir else {
|
||||
return Ok(());
|
||||
};
|
||||
if let Some((result, info)) = try_project_config(project_dir) {
|
||||
let project_config = result?;
|
||||
config.custom_make = project_config.custom_make;
|
||||
config.custom_args = project_config.custom_args;
|
||||
config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p));
|
||||
config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p));
|
||||
config.build_base = project_config.build_base;
|
||||
config.build_target = project_config.build_target;
|
||||
config.watch_patterns = project_config.watch_patterns.unwrap_or_else(|| {
|
||||
state.config.custom_make = project_config.custom_make.clone();
|
||||
state.config.custom_args = project_config.custom_args.clone();
|
||||
state.config.target_obj_dir =
|
||||
project_config.target_dir.as_deref().map(|p| project_dir.join(p));
|
||||
state.config.base_obj_dir = project_config.base_dir.as_deref().map(|p| project_dir.join(p));
|
||||
state.config.build_base = project_config.build_base.unwrap_or(true);
|
||||
state.config.build_target = project_config.build_target.unwrap_or(false);
|
||||
state.config.watch_patterns = project_config.watch_patterns.clone().unwrap_or_else(|| {
|
||||
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
|
||||
});
|
||||
config.watcher_change = true;
|
||||
config.objects = project_config.objects;
|
||||
config.object_nodes = build_nodes(
|
||||
&config.objects,
|
||||
state.watcher_change = true;
|
||||
state.objects = project_config.units.clone().unwrap_or_default();
|
||||
state.object_nodes = build_nodes(
|
||||
&mut state.objects,
|
||||
project_dir,
|
||||
config.target_obj_dir.as_deref(),
|
||||
config.base_obj_dir.as_deref(),
|
||||
state.config.target_obj_dir.as_deref(),
|
||||
state.config.base_obj_dir.as_deref(),
|
||||
);
|
||||
config.project_config_info = Some(info);
|
||||
state.current_project_config = Some(project_config);
|
||||
state.project_config_info = Some(info);
|
||||
|
||||
// Reload selected object
|
||||
if let Some(selected_obj) = &state.config.selected_obj {
|
||||
if let Some(obj) = state.objects.iter().find(|o| o.name() == selected_obj.name) {
|
||||
let config = ObjectConfig::from(obj);
|
||||
state.set_selected_obj(config);
|
||||
} else {
|
||||
state.clear_selected_obj();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ impl CreateScratchConfig {
|
||||
Ok(Self {
|
||||
build_config: BuildConfig::from_config(config),
|
||||
context_path: scratch_config.ctx_path.clone(),
|
||||
build_context: scratch_config.build_ctx,
|
||||
build_context: scratch_config.build_ctx.unwrap_or(false),
|
||||
compiler: scratch_config.compiler.clone().unwrap_or_default(),
|
||||
platform: scratch_config.platform.clone().unwrap_or_default(),
|
||||
compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(),
|
||||
|
||||
@@ -53,7 +53,7 @@ impl JobQueue {
|
||||
}
|
||||
|
||||
/// Returns whether any job is running.
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
pub fn any_running(&self) -> bool {
|
||||
self.jobs.iter().any(|job| {
|
||||
if let Some(handle) = &job.handle {
|
||||
@@ -85,12 +85,15 @@ impl JobQueue {
|
||||
/// Clears all finished jobs.
|
||||
pub fn clear_finished(&mut self) {
|
||||
self.jobs.retain(|job| {
|
||||
!(job.should_remove
|
||||
&& job.handle.is_none()
|
||||
&& job.context.status.read().unwrap().error.is_none())
|
||||
!(job.handle.is_none() && job.context.status.read().unwrap().error.is_none())
|
||||
});
|
||||
}
|
||||
|
||||
/// Clears all errored jobs.
|
||||
pub fn clear_errored(&mut self) {
|
||||
self.jobs.retain(|job| job.context.status.read().unwrap().error.is_none());
|
||||
}
|
||||
|
||||
/// Removes a job from the queue given its ID.
|
||||
pub fn remove(&mut self, id: usize) { self.jobs.retain(|job| job.id != id); }
|
||||
}
|
||||
@@ -107,7 +110,6 @@ pub struct JobState {
|
||||
pub handle: Option<JoinHandle<JobResult>>,
|
||||
pub context: JobContext,
|
||||
pub cancel: Sender<()>,
|
||||
pub should_remove: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -163,7 +165,7 @@ fn start_job(
|
||||
});
|
||||
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
|
||||
log::info!("Started job {}", id);
|
||||
JobState { id, kind, handle: Some(handle), context, cancel: tx, should_remove: true }
|
||||
JobState { id, kind, handle: Some(handle), context, cancel: tx }
|
||||
}
|
||||
|
||||
fn update_status(
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
str::from_utf8,
|
||||
sync::mpsc::Receiver,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Error, Result};
|
||||
use anyhow::{anyhow, Error, Result};
|
||||
use objdiff_core::{
|
||||
diff::{diff_objs, DiffObjConfig, ObjDiff},
|
||||
diff::{diff_objs, DiffObjConfig, MappingConfig, ObjDiff},
|
||||
obj::{read, ObjInfo},
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::{
|
||||
app::{AppConfig, ObjectConfig},
|
||||
app::{AppConfig, AppState, ObjectConfig},
|
||||
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
|
||||
};
|
||||
|
||||
@@ -61,16 +60,20 @@ pub struct ObjDiffConfig {
|
||||
pub build_target: bool,
|
||||
pub selected_obj: Option<ObjectConfig>,
|
||||
pub diff_obj_config: DiffObjConfig,
|
||||
pub selecting_left: Option<String>,
|
||||
pub selecting_right: Option<String>,
|
||||
}
|
||||
|
||||
impl ObjDiffConfig {
|
||||
pub(crate) fn from_config(config: &AppConfig) -> Self {
|
||||
pub(crate) fn from_state(state: &AppState) -> Self {
|
||||
Self {
|
||||
build_config: BuildConfig::from_config(config),
|
||||
build_base: config.build_base,
|
||||
build_target: config.build_target,
|
||||
selected_obj: config.selected_obj.clone(),
|
||||
diff_obj_config: config.diff_obj_config.clone(),
|
||||
build_config: BuildConfig::from_config(&state.config),
|
||||
build_base: state.config.build_base,
|
||||
build_target: state.config.build_target,
|
||||
selected_obj: state.config.selected_obj.clone(),
|
||||
diff_obj_config: state.config.diff_obj_config.clone(),
|
||||
selecting_left: state.selecting_left.clone(),
|
||||
selecting_right: state.selecting_right.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,13 +94,6 @@ pub(crate) fn run_make(config: &BuildConfig, arg: &Path) -> BuildStatus {
|
||||
..Default::default()
|
||||
};
|
||||
};
|
||||
match run_make_cmd(config, cwd, arg) {
|
||||
Ok(status) => status,
|
||||
Err(e) => BuildStatus { success: false, stderr: e.to_string(), ..Default::default() },
|
||||
}
|
||||
}
|
||||
|
||||
fn run_make_cmd(config: &BuildConfig, cwd: &Path, arg: &Path) -> Result<BuildStatus> {
|
||||
let make = config.custom_make.as_deref().unwrap_or("make");
|
||||
let make_args = config.custom_args.as_deref().unwrap_or(&[]);
|
||||
#[cfg(not(windows))]
|
||||
@@ -144,23 +140,38 @@ fn run_make_cmd(config: &BuildConfig, cwd: &Path, arg: &Path) -> Result<BuildSta
|
||||
cmdline.push(' ');
|
||||
cmdline.push_str(shell_escape::escape(arg.to_string_lossy()).as_ref());
|
||||
}
|
||||
let output = command.output().map_err(|e| anyhow!("Failed to execute build: {e}"))?;
|
||||
let stdout = from_utf8(&output.stdout).context("Failed to process stdout")?;
|
||||
let stderr = from_utf8(&output.stderr).context("Failed to process stderr")?;
|
||||
Ok(BuildStatus {
|
||||
success: output.status.code().unwrap_or(-1) == 0,
|
||||
cmdline,
|
||||
stdout: stdout.to_string(),
|
||||
stderr: stderr.to_string(),
|
||||
})
|
||||
let output = match command.output() {
|
||||
Ok(output) => output,
|
||||
Err(e) => {
|
||||
return BuildStatus {
|
||||
success: false,
|
||||
cmdline,
|
||||
stdout: Default::default(),
|
||||
stderr: e.to_string(),
|
||||
};
|
||||
}
|
||||
};
|
||||
// Try from_utf8 first to avoid copying the buffer if it's valid, then fall back to from_utf8_lossy
|
||||
let stdout = String::from_utf8(output.stdout)
|
||||
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
|
||||
let stderr = String::from_utf8(output.stderr)
|
||||
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
|
||||
BuildStatus { success: output.status.success(), cmdline, stdout, stderr }
|
||||
}
|
||||
|
||||
fn run_build(
|
||||
context: &JobContext,
|
||||
cancel: Receiver<()>,
|
||||
config: ObjDiffConfig,
|
||||
mut config: ObjDiffConfig,
|
||||
) -> Result<Box<ObjDiffResult>> {
|
||||
let obj_config = config.selected_obj.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?;
|
||||
let obj_config = config.selected_obj.ok_or_else(|| Error::msg("Missing obj path"))?;
|
||||
// Use the per-object symbol mappings, we don't set mappings globally
|
||||
config.diff_obj_config.symbol_mappings = MappingConfig {
|
||||
mappings: obj_config.symbol_mappings,
|
||||
selecting_left: config.selecting_left,
|
||||
selecting_right: config.selecting_right,
|
||||
};
|
||||
|
||||
let project_dir = config
|
||||
.build_config
|
||||
.project_dir
|
||||
@@ -189,36 +200,46 @@ fn run_build(
|
||||
None
|
||||
};
|
||||
|
||||
let mut total = 3;
|
||||
let mut total = 1;
|
||||
if config.build_target && target_path_rel.is_some() {
|
||||
total += 1;
|
||||
}
|
||||
if config.build_base && base_path_rel.is_some() {
|
||||
total += 1;
|
||||
}
|
||||
let first_status = match target_path_rel {
|
||||
if target_path_rel.is_some() {
|
||||
total += 1;
|
||||
}
|
||||
if base_path_rel.is_some() {
|
||||
total += 1;
|
||||
}
|
||||
|
||||
let mut step_idx = 0;
|
||||
let mut first_status = match target_path_rel {
|
||||
Some(target_path_rel) if config.build_target => {
|
||||
update_status(
|
||||
context,
|
||||
format!("Building target {}", target_path_rel.display()),
|
||||
0,
|
||||
step_idx,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
step_idx += 1;
|
||||
run_make(&config.build_config, target_path_rel)
|
||||
}
|
||||
_ => BuildStatus::default(),
|
||||
};
|
||||
|
||||
let second_status = match base_path_rel {
|
||||
let mut second_status = match base_path_rel {
|
||||
Some(base_path_rel) if config.build_base => {
|
||||
update_status(
|
||||
context,
|
||||
format!("Building base {}", base_path_rel.display()),
|
||||
0,
|
||||
step_idx,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
step_idx += 1;
|
||||
run_make(&config.build_config, base_path_rel)
|
||||
}
|
||||
_ => BuildStatus::default(),
|
||||
@@ -226,44 +247,71 @@ fn run_build(
|
||||
|
||||
let time = OffsetDateTime::now_utc();
|
||||
|
||||
let first_obj =
|
||||
match &obj_config.target_path {
|
||||
Some(target_path) if first_status.success => {
|
||||
update_status(
|
||||
context,
|
||||
format!("Loading target {}", target_path_rel.unwrap().display()),
|
||||
2,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
Some(read::read(target_path, &config.diff_obj_config).with_context(|| {
|
||||
format!("Failed to read object '{}'", target_path.display())
|
||||
})?)
|
||||
let first_obj = match &obj_config.target_path {
|
||||
Some(target_path) if first_status.success => {
|
||||
update_status(
|
||||
context,
|
||||
format!("Loading target {}", target_path_rel.unwrap().display()),
|
||||
step_idx,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
step_idx += 1;
|
||||
match read::read(target_path, &config.diff_obj_config) {
|
||||
Ok(obj) => Some(obj),
|
||||
Err(e) => {
|
||||
first_status = BuildStatus {
|
||||
success: false,
|
||||
stdout: format!("Loading object '{}'", target_path.display()),
|
||||
stderr: format!("{:#}", e),
|
||||
..Default::default()
|
||||
};
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
Some(_) => {
|
||||
step_idx += 1;
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let second_obj = match &obj_config.base_path {
|
||||
Some(base_path) if second_status.success => {
|
||||
update_status(
|
||||
context,
|
||||
format!("Loading base {}", base_path_rel.unwrap().display()),
|
||||
3,
|
||||
step_idx,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
Some(
|
||||
read::read(base_path, &config.diff_obj_config)
|
||||
.with_context(|| format!("Failed to read object '{}'", base_path.display()))?,
|
||||
)
|
||||
step_idx += 1;
|
||||
match read::read(base_path, &config.diff_obj_config) {
|
||||
Ok(obj) => Some(obj),
|
||||
Err(e) => {
|
||||
second_status = BuildStatus {
|
||||
success: false,
|
||||
stdout: format!("Loading object '{}'", base_path.display()),
|
||||
stderr: format!("{:#}", e),
|
||||
..Default::default()
|
||||
};
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
step_idx += 1;
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
update_status(context, "Performing diff".to_string(), 4, total, &cancel)?;
|
||||
update_status(context, "Performing diff".to_string(), step_idx, total, &cancel)?;
|
||||
step_idx += 1;
|
||||
let result = diff_objs(&config.diff_obj_config, first_obj.as_ref(), second_obj.as_ref(), None)?;
|
||||
|
||||
update_status(context, "Complete".to_string(), total, total, &cancel)?;
|
||||
update_status(context, "Complete".to_string(), step_idx, total, &cancel)?;
|
||||
Ok(Box::new(ObjDiffResult {
|
||||
first_status,
|
||||
second_status,
|
||||
@@ -274,7 +322,7 @@ fn run_build(
|
||||
}
|
||||
|
||||
pub fn start_build(ctx: &egui::Context, config: ObjDiffConfig) -> JobState {
|
||||
start_job(ctx, "Object diff", Job::ObjDiff, move |context, cancel| {
|
||||
start_job(ctx, "Build", Job::ObjDiff, move |context, cancel| {
|
||||
run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ fn run_update(
|
||||
let tmp_file = File::create(&tmp_path)?;
|
||||
self_update::Download::from_url(&asset.download_url)
|
||||
.set_header(reqwest::header::ACCEPT, "application/octet-stream".parse()?)
|
||||
.download_to(&tmp_file)?;
|
||||
.download_to(tmp_file)?;
|
||||
|
||||
update_status(status, "Extracting release".to_string(), 2, 3, &cancel)?;
|
||||
let tmp_file = tmp_dir.path().join("replacement_tmp");
|
||||
@@ -51,6 +51,7 @@ fn run_update(
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&target_file, perms)?;
|
||||
}
|
||||
tmp_dir.close()?;
|
||||
|
||||
update_status(status, "Complete".to_string(), 3, 3, &cancel)?;
|
||||
Ok(Box::from(UpdateResult { exe_path: target_file }))
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![warn(clippy::all, rust_2018_idioms)]
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||
|
||||
mod app;
|
||||
@@ -19,6 +18,7 @@ use std::{
|
||||
use anyhow::{ensure, Result};
|
||||
use cfg_if::cfg_if;
|
||||
use time::UtcOffset;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use crate::views::graphics::{load_graphics_config, GraphicsBackend, GraphicsConfig};
|
||||
|
||||
@@ -40,7 +40,16 @@ const APP_NAME: &str = "objdiff";
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn main() -> ExitCode {
|
||||
// Log to stdout (if you run with `RUST_LOG=debug`).
|
||||
tracing_subscriber::fmt::init();
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::builder()
|
||||
// Default to info level
|
||||
.with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into())
|
||||
.from_env_lossy()
|
||||
// This module is noisy at info level
|
||||
.add_directive("wgpu_core::device::resource=warn".parse().unwrap()),
|
||||
)
|
||||
.init();
|
||||
|
||||
// Because localtime_r is unsound in multithreaded apps,
|
||||
// we must call this before initializing eframe.
|
||||
@@ -49,8 +58,10 @@ fn main() -> ExitCode {
|
||||
|
||||
let app_path = std::env::current_exe().ok();
|
||||
let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None));
|
||||
let mut native_options =
|
||||
eframe::NativeOptions { follow_system_theme: false, ..Default::default() };
|
||||
let mut native_options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default().with_app_id(APP_NAME),
|
||||
..Default::default()
|
||||
};
|
||||
match load_icon() {
|
||||
Ok(data) => {
|
||||
native_options.viewport.icon = Some(Arc::new(data));
|
||||
@@ -189,14 +200,14 @@ fn run_eframe(
|
||||
APP_NAME,
|
||||
native_options,
|
||||
Box::new(move |cc| {
|
||||
Box::new(app::App::new(
|
||||
Ok(Box::new(app::App::new(
|
||||
cc,
|
||||
utc_offset,
|
||||
exec_path_clone,
|
||||
app_path,
|
||||
graphics_config,
|
||||
graphics_config_path,
|
||||
))
|
||||
)))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ pub struct Appearance {
|
||||
pub ui_font: FontId,
|
||||
pub code_font: FontId,
|
||||
pub diff_colors: Vec<Color32>,
|
||||
pub theme: eframe::Theme,
|
||||
pub theme: egui::Theme,
|
||||
|
||||
// Applied by theme
|
||||
#[serde(skip)]
|
||||
@@ -56,7 +56,7 @@ impl Default for Appearance {
|
||||
ui_font: DEFAULT_UI_FONT,
|
||||
code_font: DEFAULT_CODE_FONT,
|
||||
diff_colors: DEFAULT_COLOR_ROTATION.to_vec(),
|
||||
theme: eframe::Theme::Dark,
|
||||
theme: egui::Theme::Dark,
|
||||
text_color: Color32::GRAY,
|
||||
emphasized_text_color: Color32::LIGHT_GRAY,
|
||||
deemphasized_text_color: Color32::DARK_GRAY,
|
||||
@@ -98,7 +98,7 @@ impl Appearance {
|
||||
});
|
||||
style.text_styles.insert(TextStyle::Monospace, self.code_font.clone());
|
||||
match self.theme {
|
||||
eframe::Theme::Dark => {
|
||||
egui::Theme::Dark => {
|
||||
style.visuals = egui::Visuals::dark();
|
||||
self.text_color = Color32::GRAY;
|
||||
self.emphasized_text_color = Color32::LIGHT_GRAY;
|
||||
@@ -108,7 +108,7 @@ impl Appearance {
|
||||
self.insert_color = Color32::GREEN;
|
||||
self.delete_color = Color32::from_rgb(200, 40, 41);
|
||||
}
|
||||
eframe::Theme::Light => {
|
||||
egui::Theme::Light => {
|
||||
style.visuals = egui::Visuals::light();
|
||||
self.text_color = Color32::GRAY;
|
||||
self.emphasized_text_color = Color32::DARK_GRAY;
|
||||
@@ -274,8 +274,8 @@ pub fn appearance_window(ctx: &egui::Context, show: &mut bool, appearance: &mut
|
||||
egui::ComboBox::from_label("Theme")
|
||||
.selected_text(format!("{:?}", appearance.theme))
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut appearance.theme, eframe::Theme::Dark, "Dark");
|
||||
ui.selectable_value(&mut appearance.theme, eframe::Theme::Light, "Light");
|
||||
ui.selectable_value(&mut appearance.theme, egui::Theme::Dark, "Dark");
|
||||
ui.selectable_value(&mut appearance.theme, egui::Theme::Light, "Light");
|
||||
});
|
||||
ui.separator();
|
||||
appearance.next_ui_font =
|
||||
|
||||
82
objdiff-gui/src/views/column_layout.rs
Normal file
82
objdiff-gui/src/views/column_layout.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use egui::{Align, Layout, Sense, Vec2};
|
||||
use egui_extras::{Column, Size, StripBuilder, TableBuilder, TableRow};
|
||||
|
||||
pub fn render_header(
|
||||
ui: &mut egui::Ui,
|
||||
available_width: f32,
|
||||
num_columns: usize,
|
||||
mut add_contents: impl FnMut(&mut egui::Ui, usize),
|
||||
) {
|
||||
let column_width = available_width / num_columns as f32;
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: available_width, y: 100.0 },
|
||||
Layout::left_to_right(Align::Min),
|
||||
|ui| {
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
|
||||
for i in 0..num_columns {
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: column_width, y: 100.0 },
|
||||
Layout::top_down(Align::Min),
|
||||
|ui| {
|
||||
ui.set_width(column_width);
|
||||
add_contents(ui, i);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
ui.separator();
|
||||
}
|
||||
|
||||
pub fn render_table(
|
||||
ui: &mut egui::Ui,
|
||||
available_width: f32,
|
||||
num_columns: usize,
|
||||
row_height: f32,
|
||||
total_rows: usize,
|
||||
mut add_contents: impl FnMut(&mut TableRow, usize),
|
||||
) {
|
||||
ui.style_mut().interaction.selectable_labels = false;
|
||||
let column_width = available_width / num_columns as f32;
|
||||
let available_height = ui.available_height();
|
||||
let table = TableBuilder::new(ui)
|
||||
.striped(false)
|
||||
.cell_layout(Layout::left_to_right(Align::Min))
|
||||
.columns(Column::exact(column_width).clip(true), num_columns)
|
||||
.resizable(false)
|
||||
.auto_shrink([false, false])
|
||||
.min_scrolled_height(available_height)
|
||||
.sense(Sense::click());
|
||||
table.body(|body| {
|
||||
body.rows(row_height, total_rows, |mut row| {
|
||||
row.set_hovered(false); // Disable hover effect
|
||||
for i in 0..num_columns {
|
||||
add_contents(&mut row, i);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn render_strips(
|
||||
ui: &mut egui::Ui,
|
||||
available_width: f32,
|
||||
num_columns: usize,
|
||||
mut add_contents: impl FnMut(&mut egui::Ui, usize),
|
||||
) {
|
||||
let column_width = available_width / num_columns as f32;
|
||||
StripBuilder::new(ui).size(Size::remainder()).clip(true).vertical(|mut strip| {
|
||||
strip.strip(|builder| {
|
||||
builder.sizes(Size::exact(column_width), num_columns).clip(true).horizontal(
|
||||
|mut strip| {
|
||||
for i in 0..num_columns {
|
||||
strip.cell(|ui| {
|
||||
ui.push_id(i, |ui| {
|
||||
add_contents(ui, i);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -2,12 +2,11 @@
|
||||
use std::string::FromUtf16Error;
|
||||
use std::{
|
||||
mem::take,
|
||||
path::{PathBuf, MAIN_SEPARATOR},
|
||||
path::{Path, PathBuf, MAIN_SEPARATOR},
|
||||
};
|
||||
|
||||
#[cfg(all(windows, feature = "wsl"))]
|
||||
use anyhow::{Context, Result};
|
||||
use const_format::formatcp;
|
||||
use egui::{
|
||||
output::OpenUrl, text::LayoutJob, CollapsingHeader, FontFamily, FontId, RichText,
|
||||
SelectableLabel, TextFormat, Widget,
|
||||
@@ -17,11 +16,10 @@ use objdiff_core::{
|
||||
config::{ProjectObject, DEFAULT_WATCH_PATTERNS},
|
||||
diff::{ArmArchVersion, ArmR9Usage, MipsAbi, MipsInstrCategory, X86Formatter},
|
||||
};
|
||||
use self_update::cargo_crate_version;
|
||||
use strum::{EnumMessage, VariantArray};
|
||||
|
||||
use crate::{
|
||||
app::{AppConfig, AppConfigRef, ObjectConfig},
|
||||
app::{AppConfig, AppState, AppStateRef, ObjectConfig},
|
||||
config::ProjectObjectNode,
|
||||
jobs::{
|
||||
check_update::{start_check_update, CheckUpdateResult},
|
||||
@@ -45,7 +43,6 @@ pub struct ConfigViewState {
|
||||
pub build_running: bool,
|
||||
pub queue_build: bool,
|
||||
pub watch_pattern_text: String,
|
||||
pub load_error: Option<String>,
|
||||
pub object_search: String,
|
||||
pub filter_diffable: bool,
|
||||
pub filter_incomplete: bool,
|
||||
@@ -56,7 +53,7 @@ pub struct ConfigViewState {
|
||||
}
|
||||
|
||||
impl ConfigViewState {
|
||||
pub fn pre_update(&mut self, jobs: &mut JobQueue, config: &AppConfigRef) {
|
||||
pub fn pre_update(&mut self, jobs: &mut JobQueue, state: &AppStateRef) {
|
||||
jobs.results.retain_mut(|result| {
|
||||
if let JobResult::CheckUpdate(result) = result {
|
||||
self.check_update = take(result);
|
||||
@@ -73,21 +70,21 @@ impl ConfigViewState {
|
||||
match self.file_dialog_state.poll() {
|
||||
FileDialogResult::None => {}
|
||||
FileDialogResult::ProjectDir(path) => {
|
||||
let mut guard = config.write().unwrap();
|
||||
let mut guard = state.write().unwrap();
|
||||
guard.set_project_dir(path.to_path_buf());
|
||||
}
|
||||
FileDialogResult::TargetDir(path) => {
|
||||
let mut guard = config.write().unwrap();
|
||||
let mut guard = state.write().unwrap();
|
||||
guard.set_target_obj_dir(path.to_path_buf());
|
||||
}
|
||||
FileDialogResult::BaseDir(path) => {
|
||||
let mut guard = config.write().unwrap();
|
||||
let mut guard = state.write().unwrap();
|
||||
guard.set_base_obj_dir(path.to_path_buf());
|
||||
}
|
||||
FileDialogResult::Object(path) => {
|
||||
let mut guard = config.write().unwrap();
|
||||
let mut guard = state.write().unwrap();
|
||||
if let (Some(base_dir), Some(target_dir)) =
|
||||
(&guard.base_obj_dir, &guard.target_obj_dir)
|
||||
(&guard.config.base_obj_dir, &guard.config.target_obj_dir)
|
||||
{
|
||||
if let Ok(obj_path) = path.strip_prefix(base_dir) {
|
||||
let target_path = target_dir.join(obj_path);
|
||||
@@ -95,9 +92,7 @@ impl ConfigViewState {
|
||||
name: obj_path.display().to_string(),
|
||||
target_path: Some(target_path),
|
||||
base_path: Some(path),
|
||||
reverse_fn_order: None,
|
||||
complete: None,
|
||||
scratch: None,
|
||||
..Default::default()
|
||||
});
|
||||
} else if let Ok(obj_path) = path.strip_prefix(target_dir) {
|
||||
let base_path = base_dir.join(obj_path);
|
||||
@@ -105,9 +100,7 @@ impl ConfigViewState {
|
||||
name: obj_path.display().to_string(),
|
||||
target_path: Some(path),
|
||||
base_path: Some(base_path),
|
||||
reverse_fn_order: None,
|
||||
complete: None,
|
||||
scratch: None,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -115,11 +108,11 @@ impl ConfigViewState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn post_update(&mut self, ctx: &egui::Context, jobs: &mut JobQueue, config: &AppConfigRef) {
|
||||
pub fn post_update(&mut self, ctx: &egui::Context, jobs: &mut JobQueue, state: &AppStateRef) {
|
||||
if self.queue_build {
|
||||
self.queue_build = false;
|
||||
if let Ok(mut config) = config.write() {
|
||||
config.queue_build = true;
|
||||
if let Ok(mut state) = state.write() {
|
||||
state.queue_build = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,47 +162,43 @@ fn fetch_wsl2_distros() -> Vec<String> {
|
||||
|
||||
pub fn config_ui(
|
||||
ui: &mut egui::Ui,
|
||||
config: &AppConfigRef,
|
||||
state: &AppStateRef,
|
||||
show_config_window: &mut bool,
|
||||
state: &mut ConfigViewState,
|
||||
config_state: &mut ConfigViewState,
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
let mut config_guard = config.write().unwrap();
|
||||
let AppConfig {
|
||||
target_obj_dir,
|
||||
base_obj_dir,
|
||||
selected_obj,
|
||||
auto_update_check,
|
||||
let mut state_guard = state.write().unwrap();
|
||||
let AppState {
|
||||
config:
|
||||
AppConfig {
|
||||
project_dir, target_obj_dir, base_obj_dir, selected_obj, auto_update_check, ..
|
||||
},
|
||||
objects,
|
||||
object_nodes,
|
||||
..
|
||||
} = &mut *config_guard;
|
||||
} = &mut *state_guard;
|
||||
|
||||
ui.heading("Updates");
|
||||
ui.checkbox(auto_update_check, "Check for updates on startup");
|
||||
if ui.add_enabled(!state.check_update_running, egui::Button::new("Check now")).clicked() {
|
||||
state.queue_check_update = true;
|
||||
if ui.add_enabled(!config_state.check_update_running, egui::Button::new("Check now")).clicked()
|
||||
{
|
||||
config_state.queue_check_update = true;
|
||||
}
|
||||
ui.label(format!("Current version: {}", cargo_crate_version!())).on_hover_ui_at_pointer(|ui| {
|
||||
ui.label(formatcp!("Git branch: {}", env!("VERGEN_GIT_BRANCH")));
|
||||
ui.label(formatcp!("Git commit: {}", env!("VERGEN_GIT_SHA")));
|
||||
ui.label(formatcp!("Build target: {}", env!("VERGEN_CARGO_TARGET_TRIPLE")));
|
||||
ui.label(formatcp!("Debug: {}", env!("VERGEN_CARGO_DEBUG")));
|
||||
});
|
||||
if let Some(result) = &state.check_update {
|
||||
ui.label(format!("Current version: {}", env!("CARGO_PKG_VERSION")));
|
||||
if let Some(result) = &config_state.check_update {
|
||||
ui.label(format!("Latest version: {}", result.latest_release.version));
|
||||
if result.update_available {
|
||||
ui.colored_label(appearance.insert_color, "Update available");
|
||||
ui.horizontal(|ui| {
|
||||
if let Some(bin_name) = &result.found_binary {
|
||||
if ui
|
||||
.add_enabled(!state.update_running, egui::Button::new("Automatic"))
|
||||
.add_enabled(!config_state.update_running, egui::Button::new("Automatic"))
|
||||
.on_hover_text_at_pointer(
|
||||
"Automatically download and replace the current build",
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
state.queue_update = Some(bin_name.clone());
|
||||
config_state.queue_update = Some(bin_name.clone());
|
||||
}
|
||||
}
|
||||
if ui
|
||||
@@ -234,11 +223,14 @@ pub fn config_ui(
|
||||
}
|
||||
});
|
||||
|
||||
let mut new_selected_obj = selected_obj.clone();
|
||||
let selected_index = selected_obj.as_ref().and_then(|selected_obj| {
|
||||
objects.iter().position(|obj| obj.name.as_ref() == Some(&selected_obj.name))
|
||||
});
|
||||
let mut new_selected_index = selected_index;
|
||||
if objects.is_empty() {
|
||||
if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) {
|
||||
if ui.button("Select object").clicked() {
|
||||
state.file_dialog_state.queue(
|
||||
config_state.file_dialog_state.queue(
|
||||
|| {
|
||||
Box::pin(
|
||||
rfd::AsyncFileDialog::new()
|
||||
@@ -261,8 +253,8 @@ pub fn config_ui(
|
||||
ui.colored_label(appearance.delete_color, "Missing project settings");
|
||||
}
|
||||
} else {
|
||||
let had_search = !state.object_search.is_empty();
|
||||
egui::TextEdit::singleline(&mut state.object_search).hint_text("Filter").ui(ui);
|
||||
let had_search = !config_state.object_search.is_empty();
|
||||
egui::TextEdit::singleline(&mut config_state.object_search).hint_text("Filter").ui(ui);
|
||||
|
||||
let mut root_open = None;
|
||||
let mut node_open = NodeOpen::Default;
|
||||
@@ -284,19 +276,22 @@ pub fn config_ui(
|
||||
node_open = NodeOpen::Object;
|
||||
}
|
||||
let mut filters_text = RichText::new("Filter ⏷");
|
||||
if state.filter_diffable || state.filter_incomplete || state.show_hidden {
|
||||
if config_state.filter_diffable
|
||||
|| config_state.filter_incomplete
|
||||
|| config_state.show_hidden
|
||||
{
|
||||
filters_text = filters_text.color(appearance.replace_color);
|
||||
}
|
||||
egui::menu::menu_button(ui, filters_text, |ui| {
|
||||
ui.checkbox(&mut state.filter_diffable, "Diffable")
|
||||
ui.checkbox(&mut config_state.filter_diffable, "Diffable")
|
||||
.on_hover_text_at_pointer("Only show objects with a source file");
|
||||
ui.checkbox(&mut state.filter_incomplete, "Incomplete")
|
||||
ui.checkbox(&mut config_state.filter_incomplete, "Incomplete")
|
||||
.on_hover_text_at_pointer("Only show objects not marked complete");
|
||||
ui.checkbox(&mut state.show_hidden, "Hidden")
|
||||
ui.checkbox(&mut config_state.show_hidden, "Hidden")
|
||||
.on_hover_text_at_pointer("Show hidden (auto-generated) objects");
|
||||
});
|
||||
});
|
||||
if state.object_search.is_empty() {
|
||||
if config_state.object_search.is_empty() {
|
||||
if had_search {
|
||||
root_open = Some(true);
|
||||
node_open = NodeOpen::Object;
|
||||
@@ -313,45 +308,55 @@ pub fn config_ui(
|
||||
.open(root_open)
|
||||
.default_open(true)
|
||||
.show(ui, |ui| {
|
||||
let search = state.object_search.to_ascii_lowercase();
|
||||
ui.style_mut().wrap = Some(false);
|
||||
let search = config_state.object_search.to_ascii_lowercase();
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||
for node in object_nodes.iter().filter_map(|node| {
|
||||
filter_node(
|
||||
objects,
|
||||
node,
|
||||
&search,
|
||||
state.filter_diffable,
|
||||
state.filter_incomplete,
|
||||
state.show_hidden,
|
||||
config_state.filter_diffable,
|
||||
config_state.filter_incomplete,
|
||||
config_state.show_hidden,
|
||||
)
|
||||
}) {
|
||||
display_node(ui, &mut new_selected_obj, &node, appearance, node_open);
|
||||
display_node(
|
||||
ui,
|
||||
&mut new_selected_index,
|
||||
project_dir.as_deref(),
|
||||
objects,
|
||||
&node,
|
||||
appearance,
|
||||
node_open,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
if new_selected_obj != *selected_obj {
|
||||
if let Some(obj) = new_selected_obj {
|
||||
if new_selected_index != selected_index {
|
||||
if let Some(idx) = new_selected_index {
|
||||
// Will set obj_changed, which will trigger a rebuild
|
||||
config_guard.set_selected_obj(obj);
|
||||
let config = ObjectConfig::from(&objects[idx]);
|
||||
state_guard.set_selected_obj(config);
|
||||
}
|
||||
}
|
||||
if config_guard.selected_obj.is_some()
|
||||
&& ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked()
|
||||
if state_guard.config.selected_obj.is_some()
|
||||
&& ui.add_enabled(!config_state.build_running, egui::Button::new("Build")).clicked()
|
||||
{
|
||||
state.queue_build = true;
|
||||
config_state.queue_build = true;
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
}
|
||||
|
||||
fn display_object(
|
||||
fn display_unit(
|
||||
ui: &mut egui::Ui,
|
||||
selected_obj: &mut Option<ObjectConfig>,
|
||||
selected_obj: &mut Option<usize>,
|
||||
project_dir: Option<&Path>,
|
||||
name: &str,
|
||||
object: &ProjectObject,
|
||||
units: &[ProjectObject],
|
||||
index: usize,
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
let object_name = object.name();
|
||||
let selected = matches!(selected_obj, Some(obj) if obj.name == object_name);
|
||||
let object = &units[index];
|
||||
let selected = *selected_obj == Some(index);
|
||||
let color = if selected {
|
||||
appearance.emphasized_text_color
|
||||
} else if let Some(complete) = object.complete() {
|
||||
@@ -363,7 +368,7 @@ fn display_object(
|
||||
} else {
|
||||
appearance.text_color
|
||||
};
|
||||
let clicked = SelectableLabel::new(
|
||||
let response = SelectableLabel::new(
|
||||
selected,
|
||||
RichText::new(name)
|
||||
.font(FontId {
|
||||
@@ -372,19 +377,32 @@ fn display_object(
|
||||
})
|
||||
.color(color),
|
||||
)
|
||||
.ui(ui)
|
||||
.clicked();
|
||||
// Always recreate ObjectConfig if selected, in case the project config changed.
|
||||
// ObjectConfig is compared using equality, so this won't unnecessarily trigger a rebuild.
|
||||
if selected || clicked {
|
||||
*selected_obj = Some(ObjectConfig {
|
||||
name: object_name.to_string(),
|
||||
target_path: object.target_path.clone(),
|
||||
base_path: object.base_path.clone(),
|
||||
reverse_fn_order: object.reverse_fn_order(),
|
||||
complete: object.complete(),
|
||||
scratch: object.scratch.clone(),
|
||||
});
|
||||
.ui(ui);
|
||||
if get_source_path(project_dir, object).is_some() {
|
||||
response.context_menu(|ui| object_context_ui(ui, object, project_dir));
|
||||
}
|
||||
if response.clicked() {
|
||||
*selected_obj = Some(index);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_source_path(project_dir: Option<&Path>, object: &ProjectObject) -> Option<PathBuf> {
|
||||
project_dir.and_then(|dir| object.source_path().map(|path| dir.join(path)))
|
||||
}
|
||||
|
||||
fn object_context_ui(ui: &mut egui::Ui, object: &ProjectObject, project_dir: Option<&Path>) {
|
||||
if let Some(source_path) = get_source_path(project_dir, object) {
|
||||
if ui
|
||||
.button("Open source file")
|
||||
.on_hover_text("Open the source file in the default editor")
|
||||
.clicked()
|
||||
{
|
||||
log::info!("Opening file {}", source_path.display());
|
||||
if let Err(e) = open::that_detached(&source_path) {
|
||||
log::error!("Failed to open source file: {e}");
|
||||
}
|
||||
ui.close_menu();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,17 +417,19 @@ enum NodeOpen {
|
||||
|
||||
fn display_node(
|
||||
ui: &mut egui::Ui,
|
||||
selected_obj: &mut Option<ObjectConfig>,
|
||||
selected_obj: &mut Option<usize>,
|
||||
project_dir: Option<&Path>,
|
||||
units: &[ProjectObject],
|
||||
node: &ProjectObjectNode,
|
||||
appearance: &Appearance,
|
||||
node_open: NodeOpen,
|
||||
) {
|
||||
match node {
|
||||
ProjectObjectNode::File(name, object) => {
|
||||
display_object(ui, selected_obj, name, object, appearance);
|
||||
ProjectObjectNode::Unit(name, idx) => {
|
||||
display_unit(ui, selected_obj, project_dir, name, units, *idx, appearance);
|
||||
}
|
||||
ProjectObjectNode::Dir(name, children) => {
|
||||
let contains_obj = selected_obj.as_ref().map(|path| contains_node(node, path));
|
||||
let contains_obj = selected_obj.map(|idx| contains_node(node, idx));
|
||||
let open = match node_open {
|
||||
NodeOpen::Default => None,
|
||||
NodeOpen::Open => Some(true),
|
||||
@@ -432,16 +452,16 @@ fn display_node(
|
||||
.open(open)
|
||||
.show(ui, |ui| {
|
||||
for node in children {
|
||||
display_node(ui, selected_obj, node, appearance, node_open);
|
||||
display_node(ui, selected_obj, project_dir, units, node, appearance, node_open);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn contains_node(node: &ProjectObjectNode, selected_obj: &ObjectConfig) -> bool {
|
||||
fn contains_node(node: &ProjectObjectNode, selected_obj: usize) -> bool {
|
||||
match node {
|
||||
ProjectObjectNode::File(_, object) => object.name() == selected_obj.name,
|
||||
ProjectObjectNode::Unit(_, idx) => *idx == selected_obj,
|
||||
ProjectObjectNode::Dir(_, children) => {
|
||||
children.iter().any(|node| contains_node(node, selected_obj))
|
||||
}
|
||||
@@ -449,6 +469,7 @@ fn contains_node(node: &ProjectObjectNode, selected_obj: &ObjectConfig) -> bool
|
||||
}
|
||||
|
||||
fn filter_node(
|
||||
units: &[ProjectObject],
|
||||
node: &ProjectObjectNode,
|
||||
search: &str,
|
||||
filter_diffable: bool,
|
||||
@@ -456,12 +477,12 @@ fn filter_node(
|
||||
show_hidden: bool,
|
||||
) -> Option<ProjectObjectNode> {
|
||||
match node {
|
||||
ProjectObjectNode::File(name, object) => {
|
||||
ProjectObjectNode::Unit(name, idx) => {
|
||||
let unit = &units[*idx];
|
||||
if (search.is_empty() || name.to_ascii_lowercase().contains(search))
|
||||
&& (!filter_diffable
|
||||
|| (object.base_path.is_some() && object.target_path.is_some()))
|
||||
&& (!filter_incomplete || matches!(object.complete(), None | Some(false)))
|
||||
&& (show_hidden || !object.hidden())
|
||||
&& (!filter_diffable || (unit.base_path.is_some() && unit.target_path.is_some()))
|
||||
&& (!filter_incomplete || matches!(unit.complete(), None | Some(false)))
|
||||
&& (show_hidden || !unit.hidden())
|
||||
{
|
||||
Some(node.clone())
|
||||
} else {
|
||||
@@ -472,7 +493,14 @@ fn filter_node(
|
||||
let new_children = children
|
||||
.iter()
|
||||
.filter_map(|child| {
|
||||
filter_node(child, search, filter_diffable, filter_incomplete, show_hidden)
|
||||
filter_node(
|
||||
units,
|
||||
child,
|
||||
search,
|
||||
filter_diffable,
|
||||
filter_incomplete,
|
||||
show_hidden,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !new_children.is_empty() {
|
||||
@@ -530,33 +558,33 @@ fn pick_folder_ui(
|
||||
|
||||
pub fn project_window(
|
||||
ctx: &egui::Context,
|
||||
config: &AppConfigRef,
|
||||
state: &AppStateRef,
|
||||
show: &mut bool,
|
||||
state: &mut ConfigViewState,
|
||||
config_state: &mut ConfigViewState,
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
let mut config_guard = config.write().unwrap();
|
||||
let mut state_guard = state.write().unwrap();
|
||||
|
||||
egui::Window::new("Project").open(show).show(ctx, |ui| {
|
||||
split_obj_config_ui(ui, &mut config_guard, state, appearance);
|
||||
split_obj_config_ui(ui, &mut state_guard, config_state, appearance);
|
||||
});
|
||||
|
||||
if let Some(error) = &state.load_error {
|
||||
if let Some(error) = &state_guard.config_error {
|
||||
let mut open = true;
|
||||
egui::Window::new("Error").open(&mut open).show(ctx, |ui| {
|
||||
ui.label("Failed to load project config:");
|
||||
ui.colored_label(appearance.delete_color, error);
|
||||
});
|
||||
if !open {
|
||||
state.load_error = None;
|
||||
state_guard.config_error = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn split_obj_config_ui(
|
||||
ui: &mut egui::Ui,
|
||||
config: &mut AppConfig,
|
||||
state: &mut ConfigViewState,
|
||||
state: &mut AppState,
|
||||
config_state: &mut ConfigViewState,
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
let text_format = TextFormat::simple(appearance.ui_font.clone(), appearance.text_color);
|
||||
@@ -567,7 +595,7 @@ fn split_obj_config_ui(
|
||||
|
||||
let response = pick_folder_ui(
|
||||
ui,
|
||||
&config.project_dir,
|
||||
&state.config.project_dir,
|
||||
"Project directory",
|
||||
|ui| {
|
||||
let mut job = LayoutJob::default();
|
||||
@@ -583,7 +611,7 @@ fn split_obj_config_ui(
|
||||
true,
|
||||
);
|
||||
if response.clicked() {
|
||||
state.file_dialog_state.queue(
|
||||
config_state.file_dialog_state.queue(
|
||||
|| Box::pin(rfd::AsyncFileDialog::new().pick_folder()),
|
||||
FileDialogResult::ProjectDir,
|
||||
);
|
||||
@@ -612,33 +640,35 @@ fn split_obj_config_ui(
|
||||
ui.label(job);
|
||||
});
|
||||
});
|
||||
let mut custom_make_str = config.custom_make.clone().unwrap_or_default();
|
||||
let mut custom_make_str = state.config.custom_make.clone().unwrap_or_default();
|
||||
if ui
|
||||
.add_enabled(
|
||||
config.project_config_info.is_none(),
|
||||
state.project_config_info.is_none(),
|
||||
egui::TextEdit::singleline(&mut custom_make_str).hint_text("make"),
|
||||
)
|
||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||
.changed()
|
||||
{
|
||||
if custom_make_str.is_empty() {
|
||||
config.custom_make = None;
|
||||
state.config.custom_make = None;
|
||||
} else {
|
||||
config.custom_make = Some(custom_make_str);
|
||||
state.config.custom_make = Some(custom_make_str);
|
||||
}
|
||||
}
|
||||
#[cfg(all(windows, feature = "wsl"))]
|
||||
{
|
||||
if state.available_wsl_distros.is_none() {
|
||||
state.available_wsl_distros = Some(fetch_wsl2_distros());
|
||||
if config_state.available_wsl_distros.is_none() {
|
||||
config_state.available_wsl_distros = Some(fetch_wsl2_distros());
|
||||
}
|
||||
egui::ComboBox::from_label("Run in WSL2")
|
||||
.selected_text(config.selected_wsl_distro.as_ref().unwrap_or(&"Disabled".to_string()))
|
||||
.selected_text(
|
||||
state.config.selected_wsl_distro.as_ref().unwrap_or(&"Disabled".to_string()),
|
||||
)
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut config.selected_wsl_distro, None, "Disabled");
|
||||
for distro in state.available_wsl_distros.as_ref().unwrap() {
|
||||
ui.selectable_value(&mut state.config.selected_wsl_distro, None, "Disabled");
|
||||
for distro in config_state.available_wsl_distros.as_ref().unwrap() {
|
||||
ui.selectable_value(
|
||||
&mut config.selected_wsl_distro,
|
||||
&mut state.config.selected_wsl_distro,
|
||||
Some(distro.clone()),
|
||||
distro,
|
||||
);
|
||||
@@ -647,10 +677,10 @@ fn split_obj_config_ui(
|
||||
}
|
||||
ui.separator();
|
||||
|
||||
if let Some(project_dir) = config.project_dir.clone() {
|
||||
if let Some(project_dir) = state.config.project_dir.clone() {
|
||||
let response = pick_folder_ui(
|
||||
ui,
|
||||
&config.target_obj_dir,
|
||||
&state.config.target_obj_dir,
|
||||
"Target build directory",
|
||||
|ui| {
|
||||
let mut job = LayoutJob::default();
|
||||
@@ -667,17 +697,17 @@ fn split_obj_config_ui(
|
||||
ui.label(job);
|
||||
},
|
||||
appearance,
|
||||
config.project_config_info.is_none(),
|
||||
state.project_config_info.is_none(),
|
||||
);
|
||||
if response.clicked() {
|
||||
state.file_dialog_state.queue(
|
||||
config_state.file_dialog_state.queue(
|
||||
|| Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()),
|
||||
FileDialogResult::TargetDir,
|
||||
);
|
||||
}
|
||||
ui.add_enabled(
|
||||
config.project_config_info.is_none(),
|
||||
egui::Checkbox::new(&mut config.build_target, "Build target objects"),
|
||||
state.project_config_info.is_none(),
|
||||
egui::Checkbox::new(&mut state.config.build_target, "Build target objects"),
|
||||
)
|
||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||
.on_hover_ui(|ui| {
|
||||
@@ -711,7 +741,7 @@ fn split_obj_config_ui(
|
||||
|
||||
let response = pick_folder_ui(
|
||||
ui,
|
||||
&config.base_obj_dir,
|
||||
&state.config.base_obj_dir,
|
||||
"Base build directory",
|
||||
|ui| {
|
||||
let mut job = LayoutJob::default();
|
||||
@@ -723,17 +753,17 @@ fn split_obj_config_ui(
|
||||
ui.label(job);
|
||||
},
|
||||
appearance,
|
||||
config.project_config_info.is_none(),
|
||||
state.project_config_info.is_none(),
|
||||
);
|
||||
if response.clicked() {
|
||||
state.file_dialog_state.queue(
|
||||
config_state.file_dialog_state.queue(
|
||||
|| Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()),
|
||||
FileDialogResult::BaseDir,
|
||||
);
|
||||
}
|
||||
ui.add_enabled(
|
||||
config.project_config_info.is_none(),
|
||||
egui::Checkbox::new(&mut config.build_base, "Build base objects"),
|
||||
state.project_config_info.is_none(),
|
||||
egui::Checkbox::new(&mut state.config.build_base, "Build base objects"),
|
||||
)
|
||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||
.on_hover_ui(|ui| {
|
||||
@@ -764,7 +794,7 @@ fn split_obj_config_ui(
|
||||
|
||||
subheading(ui, "Watch settings", appearance);
|
||||
let response =
|
||||
ui.checkbox(&mut config.rebuild_on_changes, "Rebuild on changes").on_hover_ui(|ui| {
|
||||
ui.checkbox(&mut state.config.rebuild_on_changes, "Rebuild on changes").on_hover_ui(|ui| {
|
||||
let mut job = LayoutJob::default();
|
||||
job.append(
|
||||
"Automatically re-run the build & diff when files change.",
|
||||
@@ -774,23 +804,23 @@ fn split_obj_config_ui(
|
||||
ui.label(job);
|
||||
});
|
||||
if response.changed() {
|
||||
config.watcher_change = true;
|
||||
state.watcher_change = true;
|
||||
};
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(RichText::new("File patterns").color(appearance.text_color));
|
||||
if ui
|
||||
.add_enabled(config.project_config_info.is_none(), egui::Button::new("Reset"))
|
||||
.add_enabled(state.project_config_info.is_none(), egui::Button::new("Reset"))
|
||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||
.clicked()
|
||||
{
|
||||
config.watch_patterns =
|
||||
state.config.watch_patterns =
|
||||
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect();
|
||||
config.watcher_change = true;
|
||||
state.watcher_change = true;
|
||||
}
|
||||
});
|
||||
let mut remove_at: Option<usize> = None;
|
||||
for (idx, glob) in config.watch_patterns.iter().enumerate() {
|
||||
for (idx, glob) in state.config.watch_patterns.iter().enumerate() {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
RichText::new(format!("{}", glob))
|
||||
@@ -798,7 +828,7 @@ fn split_obj_config_ui(
|
||||
.family(FontFamily::Monospace),
|
||||
);
|
||||
if ui
|
||||
.add_enabled(config.project_config_info.is_none(), egui::Button::new("-").small())
|
||||
.add_enabled(state.project_config_info.is_none(), egui::Button::new("-").small())
|
||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||
.clicked()
|
||||
{
|
||||
@@ -807,24 +837,24 @@ fn split_obj_config_ui(
|
||||
});
|
||||
}
|
||||
if let Some(idx) = remove_at {
|
||||
config.watch_patterns.remove(idx);
|
||||
config.watcher_change = true;
|
||||
state.config.watch_patterns.remove(idx);
|
||||
state.watcher_change = true;
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_enabled(
|
||||
config.project_config_info.is_none(),
|
||||
egui::TextEdit::singleline(&mut state.watch_pattern_text).desired_width(100.0),
|
||||
state.project_config_info.is_none(),
|
||||
egui::TextEdit::singleline(&mut config_state.watch_pattern_text).desired_width(100.0),
|
||||
)
|
||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT);
|
||||
if ui
|
||||
.add_enabled(config.project_config_info.is_none(), egui::Button::new("+").small())
|
||||
.add_enabled(state.project_config_info.is_none(), egui::Button::new("+").small())
|
||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||
.clicked()
|
||||
{
|
||||
if let Ok(glob) = Glob::new(&state.watch_pattern_text) {
|
||||
config.watch_patterns.push(glob);
|
||||
config.watcher_change = true;
|
||||
state.watch_pattern_text.clear();
|
||||
if let Ok(glob) = Glob::new(&config_state.watch_pattern_text) {
|
||||
state.config.watch_patterns.push(glob);
|
||||
state.watcher_change = true;
|
||||
config_state.watch_pattern_text.clear();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -832,131 +862,131 @@ fn split_obj_config_ui(
|
||||
|
||||
pub fn arch_config_window(
|
||||
ctx: &egui::Context,
|
||||
config: &AppConfigRef,
|
||||
state: &AppStateRef,
|
||||
show: &mut bool,
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
let mut config_guard = config.write().unwrap();
|
||||
let mut state_guard = state.write().unwrap();
|
||||
egui::Window::new("Arch Settings").open(show).show(ctx, |ui| {
|
||||
arch_config_ui(ui, &mut config_guard, appearance);
|
||||
arch_config_ui(ui, &mut state_guard, appearance);
|
||||
});
|
||||
}
|
||||
|
||||
fn arch_config_ui(ui: &mut egui::Ui, config: &mut AppConfig, _appearance: &Appearance) {
|
||||
fn arch_config_ui(ui: &mut egui::Ui, state: &mut AppState, _appearance: &Appearance) {
|
||||
ui.heading("x86");
|
||||
egui::ComboBox::new("x86_formatter", "Format")
|
||||
.selected_text(config.diff_obj_config.x86_formatter.get_message().unwrap())
|
||||
.selected_text(state.config.diff_obj_config.x86_formatter.get_message().unwrap())
|
||||
.show_ui(ui, |ui| {
|
||||
for &formatter in X86Formatter::VARIANTS {
|
||||
if ui
|
||||
.selectable_label(
|
||||
config.diff_obj_config.x86_formatter == formatter,
|
||||
state.config.diff_obj_config.x86_formatter == formatter,
|
||||
formatter.get_message().unwrap(),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
config.diff_obj_config.x86_formatter = formatter;
|
||||
config.queue_reload = true;
|
||||
state.config.diff_obj_config.x86_formatter = formatter;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
ui.heading("MIPS");
|
||||
egui::ComboBox::new("mips_abi", "ABI")
|
||||
.selected_text(config.diff_obj_config.mips_abi.get_message().unwrap())
|
||||
.selected_text(state.config.diff_obj_config.mips_abi.get_message().unwrap())
|
||||
.show_ui(ui, |ui| {
|
||||
for &abi in MipsAbi::VARIANTS {
|
||||
if ui
|
||||
.selectable_label(
|
||||
config.diff_obj_config.mips_abi == abi,
|
||||
state.config.diff_obj_config.mips_abi == abi,
|
||||
abi.get_message().unwrap(),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
config.diff_obj_config.mips_abi = abi;
|
||||
config.queue_reload = true;
|
||||
state.config.diff_obj_config.mips_abi = abi;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
egui::ComboBox::new("mips_instr_category", "Instruction Category")
|
||||
.selected_text(config.diff_obj_config.mips_instr_category.get_message().unwrap())
|
||||
.selected_text(state.config.diff_obj_config.mips_instr_category.get_message().unwrap())
|
||||
.show_ui(ui, |ui| {
|
||||
for &category in MipsInstrCategory::VARIANTS {
|
||||
if ui
|
||||
.selectable_label(
|
||||
config.diff_obj_config.mips_instr_category == category,
|
||||
state.config.diff_obj_config.mips_instr_category == category,
|
||||
category.get_message().unwrap(),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
config.diff_obj_config.mips_instr_category = category;
|
||||
config.queue_reload = true;
|
||||
state.config.diff_obj_config.mips_instr_category = category;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
ui.heading("ARM");
|
||||
egui::ComboBox::new("arm_arch_version", "Architecture Version")
|
||||
.selected_text(config.diff_obj_config.arm_arch_version.get_message().unwrap())
|
||||
.selected_text(state.config.diff_obj_config.arm_arch_version.get_message().unwrap())
|
||||
.show_ui(ui, |ui| {
|
||||
for &version in ArmArchVersion::VARIANTS {
|
||||
if ui
|
||||
.selectable_label(
|
||||
config.diff_obj_config.arm_arch_version == version,
|
||||
state.config.diff_obj_config.arm_arch_version == version,
|
||||
version.get_message().unwrap(),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
config.diff_obj_config.arm_arch_version = version;
|
||||
config.queue_reload = true;
|
||||
state.config.diff_obj_config.arm_arch_version = version;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
let response = ui
|
||||
.checkbox(&mut config.diff_obj_config.arm_unified_syntax, "Unified syntax")
|
||||
.checkbox(&mut state.config.diff_obj_config.arm_unified_syntax, "Unified syntax")
|
||||
.on_hover_text("Disassemble as unified assembly language (UAL).");
|
||||
if response.changed() {
|
||||
config.queue_reload = true;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
let response = ui
|
||||
.checkbox(&mut config.diff_obj_config.arm_av_registers, "Use A/V registers")
|
||||
.checkbox(&mut state.config.diff_obj_config.arm_av_registers, "Use A/V registers")
|
||||
.on_hover_text("Display R0-R3 as A1-A4 and R4-R11 as V1-V8");
|
||||
if response.changed() {
|
||||
config.queue_reload = true;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
egui::ComboBox::new("arm_r9_usage", "Display R9 as")
|
||||
.selected_text(config.diff_obj_config.arm_r9_usage.get_message().unwrap())
|
||||
.selected_text(state.config.diff_obj_config.arm_r9_usage.get_message().unwrap())
|
||||
.show_ui(ui, |ui| {
|
||||
for &usage in ArmR9Usage::VARIANTS {
|
||||
if ui
|
||||
.selectable_label(
|
||||
config.diff_obj_config.arm_r9_usage == usage,
|
||||
state.config.diff_obj_config.arm_r9_usage == usage,
|
||||
usage.get_message().unwrap(),
|
||||
)
|
||||
.on_hover_text(usage.get_detailed_message().unwrap())
|
||||
.clicked()
|
||||
{
|
||||
config.diff_obj_config.arm_r9_usage = usage;
|
||||
config.queue_reload = true;
|
||||
state.config.diff_obj_config.arm_r9_usage = usage;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
let response = ui
|
||||
.checkbox(&mut config.diff_obj_config.arm_sl_usage, "Display R10 as SL")
|
||||
.checkbox(&mut state.config.diff_obj_config.arm_sl_usage, "Display R10 as SL")
|
||||
.on_hover_text("Used for explicit stack limits.");
|
||||
if response.changed() {
|
||||
config.queue_reload = true;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
let response = ui
|
||||
.checkbox(&mut config.diff_obj_config.arm_fp_usage, "Display R11 as FP")
|
||||
.checkbox(&mut state.config.diff_obj_config.arm_fp_usage, "Display R11 as FP")
|
||||
.on_hover_text("Used for frame pointers.");
|
||||
if response.changed() {
|
||||
config.queue_reload = true;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
let response = ui
|
||||
.checkbox(&mut config.diff_obj_config.arm_ip_usage, "Display R12 as IP")
|
||||
.checkbox(&mut state.config.diff_obj_config.arm_ip_usage, "Display R12 as IP")
|
||||
.on_hover_text("Used for interworking and long branches.");
|
||||
if response.changed() {
|
||||
config.queue_reload = true;
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::{cmp::min, default::Default, mem::take};
|
||||
|
||||
use egui::{text::LayoutJob, Align, Label, Layout, Sense, Vec2, Widget};
|
||||
use egui_extras::{Column, TableBuilder};
|
||||
use egui::{text::LayoutJob, Id, Label, RichText, Sense, Widget};
|
||||
use objdiff_core::{
|
||||
diff::{ObjDataDiff, ObjDataDiffKind, ObjDiff},
|
||||
obj::ObjInfo,
|
||||
@@ -10,14 +9,15 @@ use time::format_description;
|
||||
|
||||
use crate::views::{
|
||||
appearance::Appearance,
|
||||
symbol_diff::{DiffViewState, SymbolRefByName, View},
|
||||
column_layout::{render_header, render_table},
|
||||
symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState},
|
||||
write_text,
|
||||
};
|
||||
|
||||
const BYTES_PER_ROW: usize = 16;
|
||||
|
||||
fn find_section(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<usize> {
|
||||
obj.sections.iter().position(|section| section.name == selected_symbol.section_name)
|
||||
fn find_section(obj: &ObjInfo, section_name: &str) -> Option<usize> {
|
||||
obj.sections.iter().position(|section| section.name == section_name)
|
||||
}
|
||||
|
||||
fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appearance: &Appearance) {
|
||||
@@ -131,20 +131,37 @@ fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> {
|
||||
split_diffs
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct SectionDiffContext<'a> {
|
||||
obj: &'a ObjInfo,
|
||||
diff: &'a ObjDiff,
|
||||
section_index: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'a> SectionDiffContext<'a> {
|
||||
pub fn new(obj: Option<&'a (ObjInfo, ObjDiff)>, section_name: Option<&str>) -> Option<Self> {
|
||||
obj.map(|(obj, diff)| Self {
|
||||
obj,
|
||||
diff,
|
||||
section_index: section_name.and_then(|section_name| find_section(obj, section_name)),
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn has_section(&self) -> bool { self.section_index.is_some() }
|
||||
}
|
||||
|
||||
fn data_table_ui(
|
||||
table: TableBuilder<'_>,
|
||||
left_obj: Option<&(ObjInfo, ObjDiff)>,
|
||||
right_obj: Option<&(ObjInfo, ObjDiff)>,
|
||||
selected_symbol: &SymbolRefByName,
|
||||
ui: &mut egui::Ui,
|
||||
available_width: f32,
|
||||
left_ctx: Option<SectionDiffContext<'_>>,
|
||||
right_ctx: Option<SectionDiffContext<'_>>,
|
||||
config: &Appearance,
|
||||
) -> Option<()> {
|
||||
let left_section = left_obj.and_then(|(obj, diff)| {
|
||||
find_section(obj, selected_symbol).map(|i| (&obj.sections[i], &diff.sections[i]))
|
||||
});
|
||||
let right_section = right_obj.and_then(|(obj, diff)| {
|
||||
find_section(obj, selected_symbol).map(|i| (&obj.sections[i], &diff.sections[i]))
|
||||
});
|
||||
|
||||
let left_section = left_ctx
|
||||
.and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i])));
|
||||
let right_section = right_ctx
|
||||
.and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i])));
|
||||
let total_bytes = left_section
|
||||
.or(right_section)?
|
||||
.1
|
||||
@@ -159,119 +176,117 @@ fn data_table_ui(
|
||||
let left_diffs = left_section.map(|(_, section)| split_diffs(§ion.data_diff));
|
||||
let right_diffs = right_section.map(|(_, section)| split_diffs(§ion.data_diff));
|
||||
|
||||
table.body(|body| {
|
||||
body.rows(config.code_font.size, total_rows, |mut row| {
|
||||
let row_index = row.index();
|
||||
let address = row_index * BYTES_PER_ROW;
|
||||
row.col(|ui| {
|
||||
render_table(ui, available_width, 2, config.code_font.size, total_rows, |row, column| {
|
||||
let i = row.index();
|
||||
let address = i * BYTES_PER_ROW;
|
||||
row.col(|ui| {
|
||||
if column == 0 {
|
||||
if let Some(left_diffs) = &left_diffs {
|
||||
data_row_ui(ui, address, &left_diffs[row_index], config);
|
||||
data_row_ui(ui, address, &left_diffs[i], config);
|
||||
}
|
||||
});
|
||||
row.col(|ui| {
|
||||
} else if column == 1 {
|
||||
if let Some(right_diffs) = &right_diffs {
|
||||
data_row_ui(ui, address, &right_diffs[row_index], config);
|
||||
data_row_ui(ui, address, &right_diffs[i], config);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
Some(())
|
||||
}
|
||||
|
||||
pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
|
||||
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
|
||||
else {
|
||||
return;
|
||||
#[must_use]
|
||||
pub fn data_diff_ui(
|
||||
ui: &mut egui::Ui,
|
||||
state: &DiffViewState,
|
||||
appearance: &Appearance,
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
let Some(result) = &state.build else {
|
||||
return ret;
|
||||
};
|
||||
|
||||
let section_name =
|
||||
state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref()).or_else(
|
||||
|| state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref()),
|
||||
);
|
||||
let left_ctx = SectionDiffContext::new(result.first_obj.as_ref(), section_name);
|
||||
let right_ctx = SectionDiffContext::new(result.second_obj.as_ref(), section_name);
|
||||
|
||||
// If both sides are missing a symbol, switch to symbol diff view
|
||||
if !right_ctx.map_or(false, |ctx| ctx.has_section())
|
||||
&& !left_ctx.map_or(false, |ctx| ctx.has_section())
|
||||
{
|
||||
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
|
||||
// Header
|
||||
let available_width = ui.available_width();
|
||||
let column_width = available_width / 2.0;
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: available_width, y: 100.0 },
|
||||
Layout::left_to_right(Align::Min),
|
||||
|ui| {
|
||||
render_header(ui, available_width, 2, |ui, column| {
|
||||
if column == 0 {
|
||||
// Left column
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: column_width, y: 100.0 },
|
||||
Layout::top_down(Align::Min),
|
||||
|ui| {
|
||||
ui.set_width(column_width);
|
||||
|
||||
if ui.button("⏴ Back").clicked() {
|
||||
state.current_view = View::SymbolDiff;
|
||||
}
|
||||
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.style_mut().wrap = Some(false);
|
||||
ui.colored_label(appearance.highlight_color, &selected_symbol.symbol_name);
|
||||
ui.label("Diff target:");
|
||||
});
|
||||
},
|
||||
);
|
||||
if ui.button("⏴ Back").clicked() {
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
|
||||
if let Some(section) =
|
||||
left_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i]))
|
||||
{
|
||||
ui.label(
|
||||
RichText::new(section.name.clone())
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Missing")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.replace_color),
|
||||
);
|
||||
}
|
||||
} else if column == 1 {
|
||||
// Right column
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: column_width, y: 100.0 },
|
||||
Layout::top_down(Align::Min),
|
||||
|ui| {
|
||||
ui.set_width(column_width);
|
||||
ui.horizontal(|ui| {
|
||||
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
|
||||
ret = Some(DiffViewAction::Build);
|
||||
}
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
if state.build_running {
|
||||
ui.colored_label(appearance.replace_color, "Building…");
|
||||
} else {
|
||||
ui.label("Last built:");
|
||||
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||
ui.label(
|
||||
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui
|
||||
.add_enabled(!state.build_running, egui::Button::new("Build"))
|
||||
.clicked()
|
||||
{
|
||||
state.queue_build = true;
|
||||
}
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.style_mut().wrap = Some(false);
|
||||
if state.build_running {
|
||||
ui.colored_label(appearance.replace_color, "Building…");
|
||||
} else {
|
||||
ui.label("Last built:");
|
||||
let format =
|
||||
format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||
ui.label(
|
||||
result
|
||||
.time
|
||||
.to_offset(appearance.utc_offset)
|
||||
.format(&format)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.style_mut().wrap = Some(false);
|
||||
ui.label("");
|
||||
ui.label("Diff base:");
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
ui.separator();
|
||||
if let Some(section) =
|
||||
right_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i]))
|
||||
{
|
||||
ui.label(
|
||||
RichText::new(section.name.clone())
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Missing")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.replace_color),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Table
|
||||
ui.style_mut().interaction.selectable_labels = false;
|
||||
let available_height = ui.available_height();
|
||||
let table = TableBuilder::new(ui)
|
||||
.striped(false)
|
||||
.cell_layout(Layout::left_to_right(Align::Min))
|
||||
.columns(Column::exact(column_width).clip(true), 2)
|
||||
.resizable(false)
|
||||
.auto_shrink([false, false])
|
||||
.min_scrolled_height(available_height);
|
||||
data_table_ui(
|
||||
table,
|
||||
result.first_obj.as_ref(),
|
||||
result.second_obj.as_ref(),
|
||||
selected_symbol,
|
||||
appearance,
|
||||
);
|
||||
let id =
|
||||
Id::new(state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref()))
|
||||
.with(state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref()));
|
||||
ui.push_id(id, |ui| {
|
||||
data_table_ui(ui, available_width, left_ctx, right_ctx, appearance);
|
||||
});
|
||||
ret
|
||||
}
|
||||
|
||||
@@ -1,71 +1,52 @@
|
||||
use egui::{text::LayoutJob, Align, Layout, ScrollArea, Ui, Vec2};
|
||||
use egui_extras::{Size, StripBuilder};
|
||||
use egui::{RichText, ScrollArea};
|
||||
use objdiff_core::{
|
||||
diff::ObjDiff,
|
||||
obj::{ObjExtab, ObjInfo, ObjSymbol, SymbolRef},
|
||||
arch::ppc::ExceptionInfo,
|
||||
obj::{ObjInfo, ObjSymbol},
|
||||
};
|
||||
use time::format_description;
|
||||
|
||||
use crate::views::{
|
||||
appearance::Appearance,
|
||||
symbol_diff::{match_color_for_symbol, DiffViewState, SymbolRefByName, View},
|
||||
column_layout::{render_header, render_strips},
|
||||
function_diff::FunctionDiffContext,
|
||||
symbol_diff::{
|
||||
match_color_for_symbol, DiffViewAction, DiffViewNavigation, DiffViewState, SymbolRefByName,
|
||||
View,
|
||||
},
|
||||
};
|
||||
|
||||
fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<SymbolRef> {
|
||||
for (section_idx, section) in obj.sections.iter().enumerate() {
|
||||
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
|
||||
if symbol.name == selected_symbol.symbol_name {
|
||||
return Some(SymbolRef { section_idx, symbol_idx });
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn decode_extab(extab: &ObjExtab) -> String {
|
||||
fn decode_extab(extab: &ExceptionInfo) -> String {
|
||||
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 each function name, use the demangled name by default,
|
||||
//and if not available fallback to the original name
|
||||
let name = match &dtor.demangled_name {
|
||||
Some(demangled_name) => demangled_name,
|
||||
None => &dtor.name,
|
||||
let name: String = match &dtor.demangled_name {
|
||||
Some(demangled_name) => demangled_name.to_string(),
|
||||
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
|
||||
}
|
||||
|
||||
fn find_extab_entry(obj: &ObjInfo, symbol: &ObjSymbol) -> Option<ObjExtab> {
|
||||
if let Some(extab_array) = &obj.extab {
|
||||
for extab_entry in extab_array {
|
||||
if extab_entry.func.name == symbol.name {
|
||||
return Some(extab_entry.clone());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
|
||||
None
|
||||
fn find_extab_entry<'a>(obj: &'a ObjInfo, symbol: &ObjSymbol) -> Option<&'a ExceptionInfo> {
|
||||
obj.arch.ppc().and_then(|ppc| ppc.extab_for_symbol(symbol))
|
||||
}
|
||||
|
||||
fn extab_text_ui(
|
||||
ui: &mut Ui,
|
||||
obj: &(ObjInfo, ObjDiff),
|
||||
symbol_ref: SymbolRef,
|
||||
ui: &mut egui::Ui,
|
||||
ctx: FunctionDiffContext<'_>,
|
||||
symbol: &ObjSymbol,
|
||||
appearance: &Appearance,
|
||||
) -> Option<()> {
|
||||
let (_section, symbol) = obj.0.section_symbol(symbol_ref);
|
||||
|
||||
if let Some(extab_entry) = find_extab_entry(&obj.0, symbol) {
|
||||
let text = decode_extab(&extab_entry);
|
||||
if let Some(extab_entry) = find_extab_entry(ctx.obj, symbol) {
|
||||
let text = decode_extab(extab_entry);
|
||||
ui.colored_label(appearance.replace_color, &text);
|
||||
return Some(());
|
||||
}
|
||||
@@ -74,145 +55,194 @@ fn extab_text_ui(
|
||||
}
|
||||
|
||||
fn extab_ui(
|
||||
ui: &mut Ui,
|
||||
obj: Option<&(ObjInfo, ObjDiff)>,
|
||||
selected_symbol: &SymbolRefByName,
|
||||
ui: &mut egui::Ui,
|
||||
ctx: FunctionDiffContext<'_>,
|
||||
appearance: &Appearance,
|
||||
_left: bool,
|
||||
_column: usize,
|
||||
) {
|
||||
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.style_mut().wrap = Some(false);
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||
|
||||
let symbol = obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol));
|
||||
|
||||
if let (Some(object), Some(symbol_ref)) = (obj, symbol) {
|
||||
extab_text_ui(ui, object, symbol_ref, appearance);
|
||||
if let Some((_section, symbol)) =
|
||||
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
|
||||
{
|
||||
extab_text_ui(ui, ctx, symbol, appearance);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn extab_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
|
||||
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
|
||||
else {
|
||||
return;
|
||||
#[must_use]
|
||||
pub fn extab_diff_ui(
|
||||
ui: &mut egui::Ui,
|
||||
state: &DiffViewState,
|
||||
appearance: &Appearance,
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
let Some(result) = &state.build else {
|
||||
return ret;
|
||||
};
|
||||
|
||||
let mut left_ctx = FunctionDiffContext::new(
|
||||
result.first_obj.as_ref(),
|
||||
state.symbol_state.left_symbol.as_ref(),
|
||||
);
|
||||
let mut right_ctx = FunctionDiffContext::new(
|
||||
result.second_obj.as_ref(),
|
||||
state.symbol_state.right_symbol.as_ref(),
|
||||
);
|
||||
|
||||
// If one side is missing a symbol, but the diff process found a match, use that symbol
|
||||
let left_diff_symbol = left_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
|
||||
});
|
||||
let right_diff_symbol = right_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
|
||||
});
|
||||
if left_diff_symbol.is_some() && right_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
|
||||
let (right_section, right_symbol) =
|
||||
right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap());
|
||||
let symbol_ref = SymbolRefByName::new(right_symbol, right_section);
|
||||
right_ctx = FunctionDiffContext::new(result.second_obj.as_ref(), Some(&symbol_ref));
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
|
||||
view: Some(View::FunctionDiff),
|
||||
left_symbol: state.symbol_state.left_symbol.clone(),
|
||||
right_symbol: Some(symbol_ref),
|
||||
}));
|
||||
} else if right_diff_symbol.is_some() && left_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
|
||||
let (left_section, left_symbol) =
|
||||
left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap());
|
||||
let symbol_ref = SymbolRefByName::new(left_symbol, left_section);
|
||||
left_ctx = FunctionDiffContext::new(result.first_obj.as_ref(), Some(&symbol_ref));
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
|
||||
view: Some(View::FunctionDiff),
|
||||
left_symbol: Some(symbol_ref),
|
||||
right_symbol: state.symbol_state.right_symbol.clone(),
|
||||
}));
|
||||
}
|
||||
|
||||
// If both sides are missing a symbol, switch to symbol diff view
|
||||
if right_ctx.map_or(false, |ctx| !ctx.has_symbol())
|
||||
&& left_ctx.map_or(false, |ctx| !ctx.has_symbol())
|
||||
{
|
||||
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
|
||||
// Header
|
||||
let available_width = ui.available_width();
|
||||
let column_width = available_width / 2.0;
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: available_width, y: 100.0 },
|
||||
Layout::left_to_right(Align::Min),
|
||||
|ui| {
|
||||
render_header(ui, available_width, 2, |ui, column| {
|
||||
if column == 0 {
|
||||
// Left column
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: column_width, y: 100.0 },
|
||||
Layout::top_down(Align::Min),
|
||||
|ui| {
|
||||
ui.set_width(column_width);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("⏴ Back").clicked() {
|
||||
state.current_view = View::SymbolDiff;
|
||||
}
|
||||
});
|
||||
|
||||
let name = selected_symbol
|
||||
.demangled_symbol_name
|
||||
.as_deref()
|
||||
.unwrap_or(&selected_symbol.symbol_name);
|
||||
let mut job = LayoutJob::simple(
|
||||
name.to_string(),
|
||||
appearance.code_font.clone(),
|
||||
appearance.highlight_color,
|
||||
column_width,
|
||||
);
|
||||
job.wrap.break_anywhere = true;
|
||||
job.wrap.max_rows = 1;
|
||||
ui.label(job);
|
||||
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.label("Diff target:");
|
||||
});
|
||||
},
|
||||
);
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("⏴ Back").clicked() {
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
ui.separator();
|
||||
if ui
|
||||
.add_enabled(
|
||||
!state.scratch_running
|
||||
&& state.scratch_available
|
||||
&& left_ctx.map_or(false, |ctx| ctx.has_symbol()),
|
||||
egui::Button::new("📲 decomp.me"),
|
||||
)
|
||||
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
|
||||
.on_disabled_hover_text("Scratch configuration missing")
|
||||
.clicked()
|
||||
{
|
||||
if let Some((_section, symbol)) = left_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
|
||||
}) {
|
||||
ret = Some(DiffViewAction::CreateScratch(symbol.name.clone()));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some((_section, symbol)) = left_ctx
|
||||
.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)))
|
||||
{
|
||||
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
|
||||
ui.label(
|
||||
RichText::new(name)
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Missing")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.replace_color),
|
||||
);
|
||||
}
|
||||
} else if column == 1 {
|
||||
// Right column
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: column_width, y: 100.0 },
|
||||
Layout::top_down(Align::Min),
|
||||
|ui| {
|
||||
ui.set_width(column_width);
|
||||
ui.horizontal(|ui| {
|
||||
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
|
||||
ret = Some(DiffViewAction::Build);
|
||||
}
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
if state.build_running {
|
||||
ui.colored_label(appearance.replace_color, "Building…");
|
||||
} else {
|
||||
ui.label("Last built:");
|
||||
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||
ui.label(
|
||||
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
|
||||
);
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
if ui
|
||||
.add_enabled(state.source_path_available, egui::Button::new("🖹 Source file"))
|
||||
.on_hover_text_at_pointer("Open the source file in the default editor")
|
||||
.on_disabled_hover_text("Source file metadata missing")
|
||||
.clicked()
|
||||
{
|
||||
ret = Some(DiffViewAction::OpenSourcePath);
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui
|
||||
.add_enabled(!state.build_running, egui::Button::new("Build"))
|
||||
.clicked()
|
||||
{
|
||||
state.queue_build = true;
|
||||
}
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.style_mut().wrap = Some(false);
|
||||
if state.build_running {
|
||||
ui.colored_label(appearance.replace_color, "Building…");
|
||||
} else {
|
||||
ui.label("Last built:");
|
||||
let format =
|
||||
format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||
ui.label(
|
||||
result
|
||||
.time
|
||||
.to_offset(appearance.utc_offset)
|
||||
.format(&format)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
if let Some(match_percent) = result
|
||||
.second_obj
|
||||
.as_ref()
|
||||
.and_then(|(obj, diff)| {
|
||||
find_symbol(obj, selected_symbol).map(|sref| {
|
||||
&diff.sections[sref.section_idx].symbols[sref.symbol_idx]
|
||||
})
|
||||
})
|
||||
.and_then(|symbol| symbol.match_percent)
|
||||
{
|
||||
ui.colored_label(
|
||||
match_color_for_symbol(match_percent, appearance),
|
||||
&format!("{match_percent:.0}%"),
|
||||
);
|
||||
} else {
|
||||
ui.colored_label(appearance.replace_color, "Missing");
|
||||
}
|
||||
ui.label("Diff base:");
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
ui.separator();
|
||||
if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.map(|symbol_ref| {
|
||||
(ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref))
|
||||
})
|
||||
}) {
|
||||
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
|
||||
ui.label(
|
||||
RichText::new(name)
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
if let Some(match_percent) = symbol_diff.match_percent {
|
||||
ui.label(
|
||||
RichText::new(format!("{:.0}%", match_percent.floor()))
|
||||
.font(appearance.code_font.clone())
|
||||
.color(match_color_for_symbol(match_percent, appearance)),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Missing")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.replace_color),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Table
|
||||
StripBuilder::new(ui).size(Size::remainder()).vertical(|mut strip| {
|
||||
strip.strip(|builder| {
|
||||
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
|
||||
strip.cell(|ui| {
|
||||
extab_ui(ui, result.first_obj.as_ref(), selected_symbol, appearance, true);
|
||||
});
|
||||
strip.cell(|ui| {
|
||||
extab_ui(ui, result.second_obj.as_ref(), selected_symbol, appearance, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
render_strips(ui, available_width, 2, |ui, column| {
|
||||
if column == 0 {
|
||||
if let Some(ctx) = left_ctx {
|
||||
extab_ui(ui, ctx, appearance, column);
|
||||
}
|
||||
} else if column == 1 {
|
||||
if let Some(ctx) = right_ctx {
|
||||
extab_ui(ui, ctx, appearance, column);
|
||||
}
|
||||
}
|
||||
});
|
||||
ret
|
||||
}
|
||||
|
||||
@@ -1,25 +1,78 @@
|
||||
use std::default::Default;
|
||||
|
||||
use egui::{text::LayoutJob, Align, Label, Layout, Response, Sense, Vec2, Widget};
|
||||
use egui_extras::{Column, TableBuilder, TableRow};
|
||||
use egui::{text::LayoutJob, Id, Label, Response, RichText, Sense, Widget};
|
||||
use egui_extras::TableRow;
|
||||
use objdiff_core::{
|
||||
arch::ObjArch,
|
||||
diff::{
|
||||
display::{display_diff, DiffText, HighlightKind},
|
||||
ObjDiff, ObjInsDiff, ObjInsDiffKind,
|
||||
},
|
||||
obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSymbol, SymbolRef},
|
||||
obj::{
|
||||
ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSectionKind, ObjSymbol,
|
||||
SymbolRef,
|
||||
},
|
||||
};
|
||||
use time::format_description;
|
||||
|
||||
use crate::views::{
|
||||
appearance::Appearance,
|
||||
symbol_diff::{match_color_for_symbol, DiffViewState, SymbolRefByName, View},
|
||||
column_layout::{render_header, render_strips, render_table},
|
||||
symbol_diff::{
|
||||
match_color_for_symbol, symbol_list_ui, DiffViewAction, DiffViewNavigation, DiffViewState,
|
||||
SymbolDiffContext, SymbolFilter, SymbolRefByName, SymbolViewState, View,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FunctionViewState {
|
||||
pub highlight: HighlightKind,
|
||||
left_highlight: HighlightKind,
|
||||
right_highlight: HighlightKind,
|
||||
}
|
||||
|
||||
impl FunctionViewState {
|
||||
pub fn highlight(&self, column: usize) -> &HighlightKind {
|
||||
match column {
|
||||
0 => &self.left_highlight,
|
||||
1 => &self.right_highlight,
|
||||
_ => &HighlightKind::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_highlight(&mut self, column: usize, highlight: HighlightKind) {
|
||||
match column {
|
||||
0 => {
|
||||
if highlight == self.left_highlight {
|
||||
if highlight == self.right_highlight {
|
||||
self.left_highlight = HighlightKind::None;
|
||||
self.right_highlight = HighlightKind::None;
|
||||
} else {
|
||||
self.right_highlight = self.left_highlight.clone();
|
||||
}
|
||||
} else {
|
||||
self.left_highlight = highlight;
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
if highlight == self.right_highlight {
|
||||
if highlight == self.left_highlight {
|
||||
self.left_highlight = HighlightKind::None;
|
||||
self.right_highlight = HighlightKind::None;
|
||||
} else {
|
||||
self.left_highlight = self.right_highlight.clone();
|
||||
}
|
||||
} else {
|
||||
self.right_highlight = highlight;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_highlight(&mut self) {
|
||||
self.left_highlight = HighlightKind::None;
|
||||
self.right_highlight = HighlightKind::None;
|
||||
}
|
||||
}
|
||||
|
||||
fn ins_hover_ui(
|
||||
@@ -32,7 +85,7 @@ fn ins_hover_ui(
|
||||
) {
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.style_mut().wrap = Some(false);
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||
|
||||
let offset = ins.address - section.address;
|
||||
ui.label(format!(
|
||||
@@ -79,6 +132,12 @@ fn ins_hover_ui(
|
||||
appearance.highlight_color,
|
||||
format!("Size: {:x}", reloc.target.size),
|
||||
);
|
||||
if let Some(s) = arch
|
||||
.guess_data_type(ins)
|
||||
.and_then(|ty| arch.display_data_type(ty, &reloc.target.bytes))
|
||||
{
|
||||
ui.colored_label(appearance.highlight_color, s);
|
||||
}
|
||||
} else {
|
||||
ui.colored_label(appearance.highlight_color, "Extern".to_string());
|
||||
}
|
||||
@@ -89,7 +148,7 @@ fn ins_hover_ui(
|
||||
fn ins_context_menu(ui: &mut egui::Ui, section: &ObjSection, ins: &ObjIns, symbol: &ObjSymbol) {
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.style_mut().wrap = Some(false);
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||
|
||||
if ui.button(format!("Copy \"{}\"", ins.formatted)).clicked() {
|
||||
ui.output_mut(|output| output.copied_text.clone_from(&ins.formatted));
|
||||
@@ -167,15 +226,19 @@ fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<Symbo
|
||||
None
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
fn diff_text_ui(
|
||||
ui: &mut egui::Ui,
|
||||
text: DiffText<'_>,
|
||||
ins_diff: &ObjInsDiff,
|
||||
appearance: &Appearance,
|
||||
ins_view_state: &mut FunctionViewState,
|
||||
ins_view_state: &FunctionViewState,
|
||||
column: usize,
|
||||
space_width: f32,
|
||||
response_cb: impl Fn(Response) -> Response,
|
||||
) {
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
let label_text;
|
||||
let mut base_color = match ins_diff.kind {
|
||||
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
|
||||
@@ -229,7 +292,7 @@ fn diff_text_ui(
|
||||
}
|
||||
DiffText::Spacing(n) => {
|
||||
ui.add_space(n as f32 * space_width);
|
||||
return;
|
||||
return ret;
|
||||
}
|
||||
DiffText::Eol => {
|
||||
label_text = "\n".to_string();
|
||||
@@ -237,7 +300,7 @@ fn diff_text_ui(
|
||||
}
|
||||
|
||||
let len = label_text.len();
|
||||
let highlight = ins_view_state.highlight == text;
|
||||
let highlight = *ins_view_state.highlight(column) == text;
|
||||
let mut response = Label::new(LayoutJob::single_section(
|
||||
label_text,
|
||||
appearance.code_text_format(base_color, highlight),
|
||||
@@ -246,247 +309,511 @@ fn diff_text_ui(
|
||||
.ui(ui);
|
||||
response = response_cb(response);
|
||||
if response.clicked() {
|
||||
if highlight {
|
||||
ins_view_state.highlight = HighlightKind::None;
|
||||
} else {
|
||||
ins_view_state.highlight = text.into();
|
||||
}
|
||||
ret = Some(DiffViewAction::SetDiffHighlight(column, text.into()));
|
||||
}
|
||||
if len < pad_to {
|
||||
ui.add_space((pad_to - len) as f32 * space_width);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn asm_row_ui(
|
||||
ui: &mut egui::Ui,
|
||||
ins_diff: &ObjInsDiff,
|
||||
symbol: &ObjSymbol,
|
||||
appearance: &Appearance,
|
||||
ins_view_state: &mut FunctionViewState,
|
||||
ins_view_state: &FunctionViewState,
|
||||
column: usize,
|
||||
response_cb: impl Fn(Response) -> Response,
|
||||
) {
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||
if ins_diff.kind != ObjInsDiffKind::None {
|
||||
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
|
||||
}
|
||||
let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' '));
|
||||
display_diff(ins_diff, symbol.address, |text| {
|
||||
diff_text_ui(ui, text, ins_diff, appearance, ins_view_state, space_width, &response_cb);
|
||||
if let Some(action) = diff_text_ui(
|
||||
ui,
|
||||
text,
|
||||
ins_diff,
|
||||
appearance,
|
||||
ins_view_state,
|
||||
column,
|
||||
space_width,
|
||||
&response_cb,
|
||||
) {
|
||||
ret = Some(action);
|
||||
}
|
||||
Ok::<_, ()>(())
|
||||
})
|
||||
.unwrap();
|
||||
ret
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn asm_col_ui(
|
||||
row: &mut TableRow<'_, '_>,
|
||||
obj: &(ObjInfo, ObjDiff),
|
||||
symbol_ref: SymbolRef,
|
||||
ctx: FunctionDiffContext<'_>,
|
||||
appearance: &Appearance,
|
||||
ins_view_state: &mut FunctionViewState,
|
||||
) {
|
||||
let (section, symbol) = obj.0.section_symbol(symbol_ref);
|
||||
let section = section.unwrap();
|
||||
let ins_diff = &obj.1.symbol_diff(symbol_ref).instructions[row.index()];
|
||||
ins_view_state: &FunctionViewState,
|
||||
column: usize,
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
let symbol_ref = ctx.symbol_ref?;
|
||||
let (section, symbol) = ctx.obj.section_symbol(symbol_ref);
|
||||
let section = section?;
|
||||
let ins_diff = &ctx.diff.symbol_diff(symbol_ref).instructions[row.index()];
|
||||
let response_cb = |response: Response| {
|
||||
if let Some(ins) = &ins_diff.ins {
|
||||
response.context_menu(|ui| ins_context_menu(ui, section, ins, symbol));
|
||||
response.on_hover_ui_at_pointer(|ui| {
|
||||
ins_hover_ui(ui, obj.0.arch.as_ref(), section, ins, symbol, appearance)
|
||||
ins_hover_ui(ui, ctx.obj.arch.as_ref(), section, ins, symbol, appearance)
|
||||
})
|
||||
} else {
|
||||
response
|
||||
}
|
||||
};
|
||||
let (_, response) = row.col(|ui| {
|
||||
asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state, response_cb);
|
||||
if let Some(action) =
|
||||
asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state, column, response_cb)
|
||||
{
|
||||
ret = Some(action);
|
||||
}
|
||||
});
|
||||
response_cb(response);
|
||||
ret
|
||||
}
|
||||
|
||||
fn empty_col_ui(row: &mut TableRow<'_, '_>) {
|
||||
row.col(|ui| {
|
||||
ui.label("");
|
||||
});
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn asm_table_ui(
|
||||
table: TableBuilder<'_>,
|
||||
left_obj: Option<&(ObjInfo, ObjDiff)>,
|
||||
right_obj: Option<&(ObjInfo, ObjDiff)>,
|
||||
selected_symbol: &SymbolRefByName,
|
||||
ui: &mut egui::Ui,
|
||||
available_width: f32,
|
||||
left_ctx: Option<FunctionDiffContext<'_>>,
|
||||
right_ctx: Option<FunctionDiffContext<'_>>,
|
||||
appearance: &Appearance,
|
||||
ins_view_state: &mut FunctionViewState,
|
||||
) -> Option<()> {
|
||||
let left_symbol = left_obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol));
|
||||
let right_symbol = right_obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol));
|
||||
let instructions_len = match (left_symbol, right_symbol) {
|
||||
(Some(left_symbol_ref), Some(right_symbol_ref)) => {
|
||||
let left_len = left_obj.unwrap().1.symbol_diff(left_symbol_ref).instructions.len();
|
||||
let right_len = right_obj.unwrap().1.symbol_diff(right_symbol_ref).instructions.len();
|
||||
debug_assert_eq!(left_len, right_len);
|
||||
ins_view_state: &FunctionViewState,
|
||||
symbol_state: &SymbolViewState,
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
let left_len = left_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len())
|
||||
});
|
||||
let right_len = right_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len())
|
||||
});
|
||||
let instructions_len = match (left_len, right_len) {
|
||||
(Some(left_len), Some(right_len)) => {
|
||||
if left_len != right_len {
|
||||
ui.label("Instruction count mismatch");
|
||||
return None;
|
||||
}
|
||||
left_len
|
||||
}
|
||||
(Some(left_symbol_ref), None) => {
|
||||
left_obj.unwrap().1.symbol_diff(left_symbol_ref).instructions.len()
|
||||
(Some(left_len), None) => left_len,
|
||||
(None, Some(right_len)) => right_len,
|
||||
(None, None) => {
|
||||
ui.label("No symbol selected");
|
||||
return None;
|
||||
}
|
||||
(None, Some(right_symbol_ref)) => {
|
||||
right_obj.unwrap().1.symbol_diff(right_symbol_ref).instructions.len()
|
||||
}
|
||||
(None, None) => return None,
|
||||
};
|
||||
table.body(|body| {
|
||||
body.rows(appearance.code_font.size, instructions_len, |mut row| {
|
||||
if let (Some(left_obj), Some(left_symbol_ref)) = (left_obj, left_symbol) {
|
||||
asm_col_ui(&mut row, left_obj, left_symbol_ref, appearance, ins_view_state);
|
||||
} else {
|
||||
empty_col_ui(&mut row);
|
||||
}
|
||||
if let (Some(right_obj), Some(right_symbol_ref)) = (right_obj, right_symbol) {
|
||||
asm_col_ui(&mut row, right_obj, right_symbol_ref, appearance, ins_view_state);
|
||||
} else {
|
||||
empty_col_ui(&mut row);
|
||||
if left_len.is_some() && right_len.is_some() {
|
||||
// Joint view
|
||||
render_table(
|
||||
ui,
|
||||
available_width,
|
||||
2,
|
||||
appearance.code_font.size,
|
||||
instructions_len,
|
||||
|row, column| {
|
||||
if column == 0 {
|
||||
if let Some(ctx) = left_ctx {
|
||||
if let Some(action) =
|
||||
asm_col_ui(row, ctx, appearance, ins_view_state, column)
|
||||
{
|
||||
ret = Some(action);
|
||||
}
|
||||
}
|
||||
} else if column == 1 {
|
||||
if let Some(ctx) = right_ctx {
|
||||
if let Some(action) =
|
||||
asm_col_ui(row, ctx, appearance, ins_view_state, column)
|
||||
{
|
||||
ret = Some(action);
|
||||
}
|
||||
}
|
||||
if row.response().clicked() {
|
||||
ret = Some(DiffViewAction::ClearDiffHighlight);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// Split view, one side is the symbol list
|
||||
render_strips(ui, available_width, 2, |ui, column| {
|
||||
if column == 0 {
|
||||
if let Some(ctx) = left_ctx {
|
||||
if ctx.has_symbol() {
|
||||
render_table(
|
||||
ui,
|
||||
available_width / 2.0,
|
||||
1,
|
||||
appearance.code_font.size,
|
||||
instructions_len,
|
||||
|row, column| {
|
||||
if let Some(action) =
|
||||
asm_col_ui(row, ctx, appearance, ins_view_state, column)
|
||||
{
|
||||
ret = Some(action);
|
||||
}
|
||||
if row.response().clicked() {
|
||||
ret = Some(DiffViewAction::ClearDiffHighlight);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else if let Some((right_ctx, right_symbol_ref)) =
|
||||
right_ctx.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| (ctx, symbol_ref)))
|
||||
{
|
||||
if let Some(action) = symbol_list_ui(
|
||||
ui,
|
||||
SymbolDiffContext { obj: ctx.obj, diff: ctx.diff },
|
||||
None,
|
||||
symbol_state,
|
||||
SymbolFilter::Mapping(right_symbol_ref),
|
||||
appearance,
|
||||
column,
|
||||
) {
|
||||
match action {
|
||||
DiffViewAction::Navigate(DiffViewNavigation {
|
||||
left_symbol: Some(left_symbol_ref),
|
||||
..
|
||||
}) => {
|
||||
let (right_section, right_symbol) =
|
||||
right_ctx.obj.section_symbol(right_symbol_ref);
|
||||
ret = Some(DiffViewAction::SetMapping(
|
||||
match right_section.map(|s| s.kind) {
|
||||
Some(ObjSectionKind::Code) => View::FunctionDiff,
|
||||
_ => View::SymbolDiff,
|
||||
},
|
||||
left_symbol_ref,
|
||||
SymbolRefByName::new(right_symbol, right_section),
|
||||
));
|
||||
}
|
||||
DiffViewAction::SetSymbolHighlight(_, _) => {
|
||||
// Ignore
|
||||
}
|
||||
_ => {
|
||||
ret = Some(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ui.label("No left object");
|
||||
}
|
||||
} else if column == 1 {
|
||||
if let Some(ctx) = right_ctx {
|
||||
if ctx.has_symbol() {
|
||||
render_table(
|
||||
ui,
|
||||
available_width / 2.0,
|
||||
1,
|
||||
appearance.code_font.size,
|
||||
instructions_len,
|
||||
|row, column| {
|
||||
if let Some(action) =
|
||||
asm_col_ui(row, ctx, appearance, ins_view_state, column)
|
||||
{
|
||||
ret = Some(action);
|
||||
}
|
||||
if row.response().clicked() {
|
||||
ret = Some(DiffViewAction::ClearDiffHighlight);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else if let Some((left_ctx, left_symbol_ref)) =
|
||||
left_ctx.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| (ctx, symbol_ref)))
|
||||
{
|
||||
if let Some(action) = symbol_list_ui(
|
||||
ui,
|
||||
SymbolDiffContext { obj: ctx.obj, diff: ctx.diff },
|
||||
None,
|
||||
symbol_state,
|
||||
SymbolFilter::Mapping(left_symbol_ref),
|
||||
appearance,
|
||||
column,
|
||||
) {
|
||||
match action {
|
||||
DiffViewAction::Navigate(DiffViewNavigation {
|
||||
right_symbol: Some(right_symbol_ref),
|
||||
..
|
||||
}) => {
|
||||
let (left_section, left_symbol) =
|
||||
left_ctx.obj.section_symbol(left_symbol_ref);
|
||||
ret = Some(DiffViewAction::SetMapping(
|
||||
match left_section.map(|s| s.kind) {
|
||||
Some(ObjSectionKind::Code) => View::FunctionDiff,
|
||||
_ => View::SymbolDiff,
|
||||
},
|
||||
SymbolRefByName::new(left_symbol, left_section),
|
||||
right_symbol_ref,
|
||||
));
|
||||
}
|
||||
DiffViewAction::SetSymbolHighlight(_, _) => {
|
||||
// Ignore
|
||||
}
|
||||
_ => {
|
||||
ret = Some(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ui.label("No right object");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Some(())
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
|
||||
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
|
||||
else {
|
||||
return;
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct FunctionDiffContext<'a> {
|
||||
pub obj: &'a ObjInfo,
|
||||
pub diff: &'a ObjDiff,
|
||||
pub symbol_ref: Option<SymbolRef>,
|
||||
}
|
||||
|
||||
impl<'a> FunctionDiffContext<'a> {
|
||||
pub fn new(
|
||||
obj: Option<&'a (ObjInfo, ObjDiff)>,
|
||||
selected_symbol: Option<&SymbolRefByName>,
|
||||
) -> Option<Self> {
|
||||
obj.map(|(obj, diff)| Self {
|
||||
obj,
|
||||
diff,
|
||||
symbol_ref: selected_symbol.and_then(|s| find_symbol(obj, s)),
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn has_symbol(&self) -> bool { self.symbol_ref.is_some() }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn function_diff_ui(
|
||||
ui: &mut egui::Ui,
|
||||
state: &DiffViewState,
|
||||
appearance: &Appearance,
|
||||
) -> Option<DiffViewAction> {
|
||||
let mut ret = None;
|
||||
let Some(result) = &state.build else {
|
||||
return ret;
|
||||
};
|
||||
|
||||
let mut left_ctx = FunctionDiffContext::new(
|
||||
result.first_obj.as_ref(),
|
||||
state.symbol_state.left_symbol.as_ref(),
|
||||
);
|
||||
let mut right_ctx = FunctionDiffContext::new(
|
||||
result.second_obj.as_ref(),
|
||||
state.symbol_state.right_symbol.as_ref(),
|
||||
);
|
||||
|
||||
// If one side is missing a symbol, but the diff process found a match, use that symbol
|
||||
let left_diff_symbol = left_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
|
||||
});
|
||||
let right_diff_symbol = right_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
|
||||
});
|
||||
if left_diff_symbol.is_some() && right_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
|
||||
let (right_section, right_symbol) =
|
||||
right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap());
|
||||
let symbol_ref = SymbolRefByName::new(right_symbol, right_section);
|
||||
right_ctx = FunctionDiffContext::new(result.second_obj.as_ref(), Some(&symbol_ref));
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
|
||||
view: Some(View::FunctionDiff),
|
||||
left_symbol: state.symbol_state.left_symbol.clone(),
|
||||
right_symbol: Some(symbol_ref),
|
||||
}));
|
||||
} else if right_diff_symbol.is_some() && left_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
|
||||
let (left_section, left_symbol) =
|
||||
left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap());
|
||||
let symbol_ref = SymbolRefByName::new(left_symbol, left_section);
|
||||
left_ctx = FunctionDiffContext::new(result.first_obj.as_ref(), Some(&symbol_ref));
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
|
||||
view: Some(View::FunctionDiff),
|
||||
left_symbol: Some(symbol_ref),
|
||||
right_symbol: state.symbol_state.right_symbol.clone(),
|
||||
}));
|
||||
}
|
||||
|
||||
// If both sides are missing a symbol, switch to symbol diff view
|
||||
if right_ctx.map_or(false, |ctx| !ctx.has_symbol())
|
||||
&& left_ctx.map_or(false, |ctx| !ctx.has_symbol())
|
||||
{
|
||||
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
|
||||
// Header
|
||||
let available_width = ui.available_width();
|
||||
let column_width = available_width / 2.0;
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: available_width, y: 100.0 },
|
||||
Layout::left_to_right(Align::Min),
|
||||
|ui| {
|
||||
render_header(ui, available_width, 2, |ui, column| {
|
||||
if column == 0 {
|
||||
// Left column
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: column_width, y: 100.0 },
|
||||
Layout::top_down(Align::Min),
|
||||
|ui| {
|
||||
ui.set_width(column_width);
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("⏴ Back").clicked() {
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
ui.separator();
|
||||
if ui
|
||||
.add_enabled(
|
||||
!state.scratch_running
|
||||
&& state.scratch_available
|
||||
&& left_ctx.map_or(false, |ctx| ctx.has_symbol()),
|
||||
egui::Button::new("📲 decomp.me"),
|
||||
)
|
||||
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
|
||||
.on_disabled_hover_text("Scratch configuration missing")
|
||||
.clicked()
|
||||
{
|
||||
if let Some((_section, symbol)) = left_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
|
||||
}) {
|
||||
ret = Some(DiffViewAction::CreateScratch(symbol.name.clone()));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("⏴ Back").clicked() {
|
||||
state.current_view = View::SymbolDiff;
|
||||
}
|
||||
if let Some((_section, symbol)) = left_ctx
|
||||
.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)))
|
||||
{
|
||||
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
|
||||
ui.label(
|
||||
RichText::new(name)
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
if right_ctx.map_or(false, |m| m.has_symbol())
|
||||
&& ui
|
||||
.button("Change target")
|
||||
.on_hover_text_at_pointer("Choose a different symbol to use as the target")
|
||||
.clicked()
|
||||
{
|
||||
if let Some(symbol_ref) = state.symbol_state.right_symbol.as_ref() {
|
||||
ret = Some(DiffViewAction::SelectingLeft(symbol_ref.clone()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Missing")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.replace_color),
|
||||
);
|
||||
ui.label(
|
||||
RichText::new("Choose target symbol")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
}
|
||||
} else if column == 1 {
|
||||
// Right column
|
||||
ui.horizontal(|ui| {
|
||||
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
|
||||
ret = Some(DiffViewAction::Build);
|
||||
}
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
if state.build_running {
|
||||
ui.colored_label(appearance.replace_color, "Building…");
|
||||
} else {
|
||||
ui.label("Last built:");
|
||||
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||
ui.label(
|
||||
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
|
||||
);
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
if ui
|
||||
.add_enabled(state.source_path_available, egui::Button::new("🖹 Source file"))
|
||||
.on_hover_text_at_pointer("Open the source file in the default editor")
|
||||
.on_disabled_hover_text("Source file metadata missing")
|
||||
.clicked()
|
||||
{
|
||||
ret = Some(DiffViewAction::OpenSourcePath);
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.map(|symbol_ref| {
|
||||
(ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref))
|
||||
})
|
||||
}) {
|
||||
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
|
||||
ui.label(
|
||||
RichText::new(name)
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
ui.horizontal(|ui| {
|
||||
if let Some(match_percent) = symbol_diff.match_percent {
|
||||
ui.label(
|
||||
RichText::new(format!("{:.0}%", match_percent.floor()))
|
||||
.font(appearance.code_font.clone())
|
||||
.color(match_color_for_symbol(match_percent, appearance)),
|
||||
);
|
||||
}
|
||||
if left_ctx.map_or(false, |m| m.has_symbol()) {
|
||||
ui.separator();
|
||||
if ui
|
||||
.add_enabled(
|
||||
!state.scratch_running && state.scratch_available,
|
||||
egui::Button::new("📲 decomp.me"),
|
||||
.button("Change base")
|
||||
.on_hover_text_at_pointer(
|
||||
"Choose a different symbol to use as the base",
|
||||
)
|
||||
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
|
||||
.on_disabled_hover_text("Scratch configuration missing")
|
||||
.clicked()
|
||||
{
|
||||
state.queue_scratch = true;
|
||||
}
|
||||
});
|
||||
|
||||
let name = selected_symbol
|
||||
.demangled_symbol_name
|
||||
.as_deref()
|
||||
.unwrap_or(&selected_symbol.symbol_name);
|
||||
let mut job = LayoutJob::simple(
|
||||
name.to_string(),
|
||||
appearance.code_font.clone(),
|
||||
appearance.highlight_color,
|
||||
column_width,
|
||||
);
|
||||
job.wrap.break_anywhere = true;
|
||||
job.wrap.max_rows = 1;
|
||||
ui.label(job);
|
||||
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.label("Diff target:");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Right column
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: column_width, y: 100.0 },
|
||||
Layout::top_down(Align::Min),
|
||||
|ui| {
|
||||
ui.set_width(column_width);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui
|
||||
.add_enabled(!state.build_running, egui::Button::new("Build"))
|
||||
.clicked()
|
||||
{
|
||||
state.queue_build = true;
|
||||
}
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.style_mut().wrap = Some(false);
|
||||
if state.build_running {
|
||||
ui.colored_label(appearance.replace_color, "Building…");
|
||||
} else {
|
||||
ui.label("Last built:");
|
||||
let format =
|
||||
format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||
ui.label(
|
||||
result
|
||||
.time
|
||||
.to_offset(appearance.utc_offset)
|
||||
.format(&format)
|
||||
.unwrap(),
|
||||
);
|
||||
if let Some(symbol_ref) = state.symbol_state.left_symbol.as_ref() {
|
||||
ret = Some(DiffViewAction::SelectingRight(symbol_ref.clone()));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
if let Some(match_percent) = result
|
||||
.second_obj
|
||||
.as_ref()
|
||||
.and_then(|(obj, diff)| {
|
||||
find_symbol(obj, selected_symbol).map(|sref| {
|
||||
&diff.sections[sref.section_idx].symbols[sref.symbol_idx]
|
||||
})
|
||||
})
|
||||
.and_then(|symbol| symbol.match_percent)
|
||||
{
|
||||
ui.colored_label(
|
||||
match_color_for_symbol(match_percent, appearance),
|
||||
&format!("{match_percent:.0}%"),
|
||||
);
|
||||
} else {
|
||||
ui.colored_label(appearance.replace_color, "Missing");
|
||||
}
|
||||
ui.label("Diff base:");
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
ui.separator();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Missing")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.replace_color),
|
||||
);
|
||||
ui.label(
|
||||
RichText::new("Choose base symbol")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Table
|
||||
ui.style_mut().interaction.selectable_labels = false;
|
||||
let available_height = ui.available_height();
|
||||
let table = TableBuilder::new(ui)
|
||||
.striped(false)
|
||||
.cell_layout(Layout::left_to_right(Align::Min))
|
||||
.columns(Column::exact(column_width).clip(true), 2)
|
||||
.resizable(false)
|
||||
.auto_shrink([false, false])
|
||||
.min_scrolled_height(available_height);
|
||||
asm_table_ui(
|
||||
table,
|
||||
result.first_obj.as_ref(),
|
||||
result.second_obj.as_ref(),
|
||||
selected_symbol,
|
||||
appearance,
|
||||
&mut state.function_state,
|
||||
);
|
||||
let id = Id::new(state.symbol_state.left_symbol.as_ref().map(|s| s.symbol_name.as_str()))
|
||||
.with(state.symbol_state.right_symbol.as_ref().map(|s| s.symbol_name.as_str()));
|
||||
if let Some(action) = ui
|
||||
.push_id(id, |ui| {
|
||||
asm_table_ui(
|
||||
ui,
|
||||
available_width,
|
||||
left_ctx,
|
||||
right_ctx,
|
||||
appearance,
|
||||
&state.function_state,
|
||||
&state.symbol_state,
|
||||
)
|
||||
})
|
||||
.inner
|
||||
{
|
||||
ret = Some(action);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufReader, BufWriter},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
@@ -46,13 +47,13 @@ pub fn load_graphics_config(path: &Path) -> Result<Option<GraphicsConfig>> {
|
||||
if !path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
let file = File::open(path)?;
|
||||
let file = BufReader::new(File::open(path)?);
|
||||
let config: GraphicsConfig = ron::de::from_reader(file)?;
|
||||
Ok(Some(config))
|
||||
}
|
||||
|
||||
pub fn save_graphics_config(path: &Path, config: &GraphicsConfig) -> Result<()> {
|
||||
let file = File::create(path)?;
|
||||
let file = BufWriter::new(File::create(path)?);
|
||||
ron::ser::to_writer(file, config)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,58 +1,162 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use egui::{ProgressBar, RichText, Widget};
|
||||
|
||||
use crate::{jobs::JobQueue, views::appearance::Appearance};
|
||||
use crate::{
|
||||
jobs::{JobQueue, JobStatus},
|
||||
views::appearance::Appearance,
|
||||
};
|
||||
|
||||
pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) {
|
||||
ui.label("Jobs");
|
||||
if ui.button("Clear").clicked() {
|
||||
jobs.clear_errored();
|
||||
}
|
||||
|
||||
let mut remove_job: Option<usize> = None;
|
||||
let mut any_jobs = false;
|
||||
for job in jobs.iter_mut() {
|
||||
let Ok(status) = job.context.status.read() else {
|
||||
continue;
|
||||
};
|
||||
ui.group(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(&status.title);
|
||||
if ui.small_button("✖").clicked() {
|
||||
if job.handle.is_some() {
|
||||
job.should_remove = true;
|
||||
if let Err(e) = job.cancel.send(()) {
|
||||
log::error!("Failed to cancel job: {e:?}");
|
||||
}
|
||||
} else {
|
||||
remove_job = Some(job.id);
|
||||
any_jobs = true;
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(&status.title);
|
||||
if ui.small_button("✖").clicked() {
|
||||
if job.handle.is_some() {
|
||||
if let Err(e) = job.cancel.send(()) {
|
||||
log::error!("Failed to cancel job: {e:?}");
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut bar = ProgressBar::new(status.progress_percent);
|
||||
if let Some(items) = &status.progress_items {
|
||||
bar = bar.text(format!("{} / {}", items[0], items[1]));
|
||||
}
|
||||
bar.ui(ui);
|
||||
const STATUS_LENGTH: usize = 80;
|
||||
if let Some(err) = &status.error {
|
||||
let err_string = format!("{:#}", err);
|
||||
ui.colored_label(
|
||||
appearance.delete_color,
|
||||
if err_string.len() > STATUS_LENGTH - 10 {
|
||||
format!("Error: {}…", &err_string[0..STATUS_LENGTH - 10])
|
||||
} else {
|
||||
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
|
||||
},
|
||||
)
|
||||
.on_hover_text_at_pointer(RichText::new(err_string).color(appearance.delete_color));
|
||||
} else {
|
||||
ui.label(if status.status.len() > STATUS_LENGTH - 3 {
|
||||
format!("{}…", &status.status[0..STATUS_LENGTH - 3])
|
||||
} else {
|
||||
format!("{:width$}", &status.status, width = STATUS_LENGTH)
|
||||
})
|
||||
.on_hover_text_at_pointer(&status.status);
|
||||
remove_job = Some(job.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut bar = ProgressBar::new(status.progress_percent);
|
||||
if let Some(items) = &status.progress_items {
|
||||
bar = bar.text(format!("{} / {}", items[0], items[1]));
|
||||
}
|
||||
bar.ui(ui);
|
||||
const STATUS_LENGTH: usize = 80;
|
||||
if let Some(err) = &status.error {
|
||||
let err_string = format!("{:#}", err);
|
||||
ui.colored_label(
|
||||
appearance.delete_color,
|
||||
if err_string.len() > STATUS_LENGTH - 10 {
|
||||
format!("Error: {}…", &err_string[0..STATUS_LENGTH - 10])
|
||||
} else {
|
||||
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
|
||||
},
|
||||
)
|
||||
.on_hover_text_at_pointer(RichText::new(&err_string).color(appearance.delete_color))
|
||||
.context_menu(|ui| {
|
||||
if ui.button("Copy full message").clicked() {
|
||||
ui.output_mut(|o| o.copied_text = err_string);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ui.label(if status.status.len() > STATUS_LENGTH - 3 {
|
||||
format!("{}…", &status.status[0..STATUS_LENGTH - 3])
|
||||
} else {
|
||||
format!("{:width$}", &status.status, width = STATUS_LENGTH)
|
||||
})
|
||||
.on_hover_text_at_pointer(&status.status)
|
||||
.context_menu(|ui| {
|
||||
if ui.button("Copy full message").clicked() {
|
||||
ui.output_mut(|o| o.copied_text = status.status.clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if !any_jobs {
|
||||
ui.label("No jobs");
|
||||
}
|
||||
|
||||
if let Some(idx) = remove_job {
|
||||
jobs.remove(idx);
|
||||
}
|
||||
}
|
||||
|
||||
struct JobStatusDisplay {
|
||||
title: String,
|
||||
progress_items: Option<[u32; 2]>,
|
||||
error: bool,
|
||||
}
|
||||
|
||||
impl From<&JobStatus> for JobStatusDisplay {
|
||||
fn from(status: &JobStatus) -> Self {
|
||||
Self {
|
||||
title: status.title.clone(),
|
||||
progress_items: status.progress_items,
|
||||
error: status.error.is_some(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn jobs_menu_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) -> bool {
|
||||
ui.label("Jobs:");
|
||||
let mut statuses = Vec::new();
|
||||
for job in jobs.iter_mut() {
|
||||
let Ok(status) = job.context.status.read() else {
|
||||
continue;
|
||||
};
|
||||
statuses.push(JobStatusDisplay::from(&*status));
|
||||
}
|
||||
let running_jobs = statuses.iter().filter(|s| !s.error).count();
|
||||
let error_jobs = statuses.iter().filter(|s| s.error).count();
|
||||
|
||||
let mut clicked = false;
|
||||
let spinner =
|
||||
egui::Spinner::new().size(appearance.ui_font.size * 0.9).color(appearance.text_color);
|
||||
match running_jobs.cmp(&1) {
|
||||
Ordering::Equal => {
|
||||
spinner.ui(ui);
|
||||
let running_job = statuses.iter().find(|s| !s.error).unwrap();
|
||||
let text = if let Some(items) = running_job.progress_items {
|
||||
format!("{} ({}/{})", running_job.title, items[0], items[1])
|
||||
} else {
|
||||
running_job.title.clone()
|
||||
};
|
||||
clicked |= ui.link(RichText::new(text)).clicked();
|
||||
}
|
||||
Ordering::Greater => {
|
||||
spinner.ui(ui);
|
||||
clicked |= ui.link(format!("{} running", running_jobs)).clicked();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
match error_jobs.cmp(&1) {
|
||||
Ordering::Equal => {
|
||||
let error_job = statuses.iter().find(|s| s.error).unwrap();
|
||||
clicked |= ui
|
||||
.link(
|
||||
RichText::new(format!("{} error", error_job.title))
|
||||
.color(appearance.delete_color),
|
||||
)
|
||||
.clicked();
|
||||
}
|
||||
Ordering::Greater => {
|
||||
clicked |= ui
|
||||
.link(
|
||||
RichText::new(format!("{} errors", error_jobs)).color(appearance.delete_color),
|
||||
)
|
||||
.clicked();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
if running_jobs == 0 && error_jobs == 0 {
|
||||
clicked |= ui.link("None").clicked();
|
||||
}
|
||||
clicked
|
||||
}
|
||||
|
||||
pub fn jobs_window(
|
||||
ctx: &egui::Context,
|
||||
show: &mut bool,
|
||||
jobs: &mut JobQueue,
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
egui::Window::new("Jobs").open(show).show(ctx, |ui| {
|
||||
jobs_ui(ui, jobs, appearance);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use egui::{text::LayoutJob, Color32, FontId, TextFormat};
|
||||
|
||||
pub(crate) mod appearance;
|
||||
pub(crate) mod column_layout;
|
||||
pub(crate) mod config;
|
||||
pub(crate) mod data_diff;
|
||||
pub(crate) mod debug;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
4
objdiff-wasm/package-lock.json
generated
4
objdiff-wasm/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "objdiff-wasm",
|
||||
"version": "2.0.0-beta.10",
|
||||
"version": "2.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "objdiff-wasm",
|
||||
"version": "2.0.0-beta.10",
|
||||
"version": "2.0.0",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@protobuf-ts/runtime": "^2.9.4"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "objdiff-wasm",
|
||||
"version": "2.0.0-beta.10",
|
||||
"version": "2.0.0",
|
||||
"description": "A local diffing tool for decompilation projects.",
|
||||
"author": {
|
||||
"name": "Luke Street",
|
||||
|
||||
Reference in New Issue
Block a user