Compare commits

..

44 Commits

Author SHA1 Message Date
711f40b591 I forgot to bump the Cargo.toml version, oops 2023-09-10 00:24:53 -04:00
26932b2e44 Support min_version field in objdiff.json 2023-09-09 23:54:25 -04:00
192a06bc0b Project configuration improvements
- Support `completed` field for objects in project config. In object tree, displays red for incomplete, green for complete.
- Add support for one-sided diffs. A project can include objects without an associated source file for viewing.
- Add versioning to AppConfig, supporting upgrades without losing user configuration.
2023-09-09 23:43:12 -04:00
5bfa47fce9 Update webpki, rustls-webpki 2023-09-03 09:42:26 -04:00
1d9b9b6893 clippy fix 2023-09-03 09:31:12 -04:00
6b8e469261 Project configuration fixes & improvements
- Allow config to specify object "target_path" and "base_path" explicitly, rather than relying on relative path from the "target_dir" and "base_dir". Useful for more complex directory layouts.
- Fix watch_patterns in project config not using default.
- Fix "Rebuild on changes" not defaulting to true.
- Keep watching project config updates even when "Rebuild on changes" is false.
- Disable some configuration options when loaded from project config file.
2023-09-03 09:28:46 -04:00
bf3ba48539 Match watch_patterns with project-relative paths 2023-08-14 00:21:56 -04:00
21cdf268f0 Update README.md 2023-08-12 14:41:19 -04:00
3970bc8acf Document configuration file & more cleanup 2023-08-12 14:18:09 -04:00
eaf0fabc2d Updates to Objects pane & config improvements 2023-08-09 21:53:04 -04:00
91d11c83d6 Refactor state & config structs, various cleanup 2023-08-09 21:53:04 -04:00
94924047b7 Job state handling cleanup 2023-08-09 19:39:06 -04:00
f5f6869029 Start project config file support & rework UI 2023-08-07 20:11:56 -04:00
b02e32f2b7 Add dark/light theme toggle (light theme WIP) 2023-07-15 11:17:59 -04:00
c7a326b160 Update all dependencies (again) 2023-07-06 10:37:57 -04:00
100f8f8ac5 Update all dependencies 2023-05-11 02:47:57 -04:00
2f778932a4 Version 0.3.1 2023-02-06 17:40:42 -05:00
42601b4750 Update cwdemangle 2023-02-06 17:40:42 -05:00
636a8e00c5 Fix diffing across mismatched .text sections 2023-02-06 17:40:42 -05:00
Nick Condron
cd46be7726 Simplify common_symbols by using iterators (#28) 2023-01-26 00:19:20 -05:00
Nick Condron
019493f944 Remove LevEditType::Keep variant (#27) 2023-01-22 13:20:50 -05:00
319b1c35c0 Move reverse_fn_order into ViewConfig 2023-01-21 13:01:21 -05:00
634e007cbc Update default configuration 2023-01-21 12:59:46 -05:00
6ee11ca640 Add optional wgpu feature 2023-01-21 12:56:29 -05:00
8278d5d207 Support MIPS PIC relocations 2023-01-21 12:41:41 -05:00
09bbc534bd Remove debug print 2023-01-21 10:52:21 -05:00
fa28352e08 Fix MIPS operands with base 2023-01-21 10:49:47 -05:00
2ab519d361 Update rabbitizer, deny.toml 2023-01-21 01:36:32 -05:00
Nick Condron
3406c76973 Simplify Affix::find (#24)
* Rewrite Affix::find to be much simpler

* Rename Affix::find parameters to not be string

* Remove unused `LevMatchingBlock` struct

* Make `Affix` type simpler
2023-01-21 01:28:33 -05:00
Nick Condron
6afc535fad Replace panic! with Option (#25) 2023-01-21 01:27:37 -05:00
Anghelo Carvajal
ec062bf5ca User rabbitizer crate (#22)
* Start using rabbitizer crate

* Fix reference problem

* bump rabbitizer version
2023-01-21 01:27:09 -05:00
500965aacb Clippy fix 2023-01-21 01:14:16 -05:00
a8c2514377 Changes for egui/object upgrades 2023-01-21 01:13:20 -05:00
4b58f69461 Upgrade all dependencies 2023-01-21 00:54:54 -05:00
cd01b6254c Use rustls on Linux 2023-01-21 00:06:22 -05:00
bea0a0007d Initial support for line number info 2023-01-21 00:03:56 -05:00
ba74d63a99 Fix data diffing 2023-01-17 19:33:31 -05:00
Nick Condron
20dcc50695 Let-else reformatting (#23)
* Use let-else in App::post_rendering

* Use let-else in diff::reloc_eq

* Use let-else in diff::diff_objs

* Use let-else in views::data_diff::data_diff_ui

* Use let-else in views::function_diff::function_diff_ui

* Use let-else in views::function_diff::asm_row_ui

* Use let-else in views::jobs::jobs_ui

* Update rust-version in Cargo.toml
2023-01-16 16:51:40 -05:00
c7b6ec83d7 ci: Update before apt-get install 2023-01-16 10:55:26 -05:00
e2fde3dbce Actually increment the version number 2022-12-12 01:17:03 -05:00
613e84ecf2 Version 0.2.3
- Fix regression when diffing symbols
  across mismatched section indexes
2022-12-10 20:28:01 -05:00
7219e72acf Version 0.2.2
- Add application icon
- Fixes for objects containing multiple
  sections with the same name
2022-12-10 10:34:03 -05:00
d1d6f1101b Version 0.2.1 2022-12-08 01:51:32 -05:00
bc7cce7226 Open "Target" dir for "Select obj" 2022-12-08 01:49:21 -05:00
35 changed files with 4898 additions and 2557 deletions

View File

@@ -20,7 +20,9 @@ jobs:
RUSTFLAGS: -D warnings RUSTFLAGS: -D warnings
steps: steps:
- name: Install dependencies - name: Install dependencies
run: sudo apt-get -y install libgtk-3-dev run: |
sudo apt-get update
sudo apt-get -y install libgtk-3-dev
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Setup Rust toolchain - name: Setup Rust toolchain
@@ -58,7 +60,9 @@ jobs:
steps: steps:
- name: Install dependencies - name: Install dependencies
if: matrix.platform == 'ubuntu-latest' if: matrix.platform == 'ubuntu-latest'
run: sudo apt-get -y install libgtk-3-dev run: |
sudo apt-get update
sudo apt-get -y install libgtk-3-dev
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Setup Rust toolchain - name: Setup Rust toolchain
@@ -89,7 +93,9 @@ jobs:
steps: steps:
- name: Install dependencies - name: Install dependencies
if: matrix.packages != '' if: matrix.packages != ''
run: sudo apt-get -y install ${{ matrix.packages }} run: |
sudo apt-get update
sudo apt-get -y install ${{ matrix.packages }}
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Setup Rust toolchain - name: Setup Rust toolchain

2999
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
[package] [package]
name = "objdiff" name = "objdiff"
version = "0.2.0" version = "0.4.3"
edition = "2021" edition = "2021"
rust-version = "1.62" rust-version = "1.65"
authors = ["Luke Street <luke@street.dev>"] authors = ["Luke Street <luke@street.dev>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff" repository = "https://github.com/encounter/objdiff"
@@ -11,40 +11,65 @@ description = """
A local diffing tool for decompilation projects. A local diffing tool for decompilation projects.
""" """
publish = false publish = false
build = "build.rs"
[profile.release] [profile.release]
lto = "thin" lto = "thin"
strip = "debuginfo" strip = "debuginfo"
[features]
default = []
wgpu = ["eframe/wgpu"]
[dependencies] [dependencies]
anyhow = "1.0.66" anyhow = "1.0.71"
byteorder = "1.4.3"
bytes = "1.4.0"
cfg-if = "1.0.0" cfg-if = "1.0.0"
const_format = "0.2.30" const_format = "0.2.31"
cwdemangle = { git = "https://github.com/encounter/cwdemangle", rev = "286f3d1d29ee2457db89043782725631845c3e4c" } cwdemangle = "0.1.6"
eframe = { version = "0.19.0", features = ["persistence"] } # , "wgpu" dirs = "5.0.1"
egui = "0.19.0" eframe = { version = "0.22.0", features = ["persistence"] }
egui_extras = "0.19.0" egui = "0.22.0"
egui_extras = "0.22.0"
flagset = "0.4.3" flagset = "0.4.3"
log = "0.4.17" globset = { version = "0.4.13", features = ["serde1"] }
memmap2 = "0.5.8" log = "0.4.19"
notify = "5.0.0" memmap2 = "0.7.1"
object = { version = "0.30.0", features = ["read_core", "std", "elf"], default-features = false } notify = "6.0.1"
ppc750cl = { git = "https://github.com/encounter/ppc750cl", rev = "aa631a33de7882c679afca89350898b87cb3ba3f" } object = { version = "0.31.1", features = ["read_core", "std", "elf"], default-features = false }
rabbitizer = { git = "https://github.com/encounter/rabbitizer-rs", rev = "10c279b2ef251c62885b1dcdcfe740b0db8e9956" } png = "0.17.9"
rfd = { version = "0.10.0" } # , default-features = false, features = ['xdg-portal'] ppc750cl = { git = "https://github.com/terorie/ppc750cl", rev = "9ae36eef34aa6d74e00972c7671f547a2acfd0aa" }
self_update = "0.32.0" rabbitizer = "1.7.4"
rfd = { version = "0.11.4" } #, default-features = false, features = ['xdg-portal']
ron = "0.8.0"
semver = "1.0.17"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
thiserror = "1.0.37" serde_json = "1.0.104"
time = { version = "0.3.17", features = ["formatting", "local-offset"] } serde_yaml = "0.9.25"
toml = "0.5.9" tempfile = "3.6.0"
thiserror = "1.0.41"
time = { version = "0.3.22", features = ["formatting", "local-offset"] }
toml = "0.7.6"
twox-hash = "1.6.3" twox-hash = "1.6.3"
tempfile = "3.3.0"
reqwest = "0.11.13" # For Linux static binaries, use rustls
[target.'cfg(target_os = "linux")'.dependencies]
reqwest = { version = "0.11.18", default-features = false, features = ["blocking", "json", "rustls"] }
self_update = { version = "0.37.0", default-features = false, features = ["rustls"] }
# For all other platforms, use native TLS
[target.'cfg(not(target_os = "linux"))'.dependencies]
reqwest = "0.11.18"
self_update = "0.37.0"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
path-slash = "0.2.1" path-slash = "0.2.1"
winapi = "0.3.9" winapi = "0.3.9"
[target.'cfg(windows)'.build-dependencies]
winres = "0.1.12"
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
exec = "0.3.1" exec = "0.3.1"
@@ -58,5 +83,5 @@ console_error_panic_hook = "0.1.7"
tracing-wasm = "0.2" tracing-wasm = "0.2"
[build-dependencies] [build-dependencies]
anyhow = "1.0.66" anyhow = "1.0.71"
vergen = { version = "7.4.3", features = ["build", "cargo", "git"], default-features = false } vergen = { version = "8.2.4", features = ["build", "cargo", "git", "gitcl"] }

117
README.md
View File

@@ -5,14 +5,122 @@
A local diffing tool for decompilation projects. A local diffing tool for decompilation projects.
Currently supports: Supports:
- PowerPC 750CL (GameCube & Wii) - PowerPC 750CL (GameCube & Wii)
- MIPS (Nintendo 64) - MIPS (Nintendo 64)
See [Usage](#usage) for more information.
![Symbol Screenshot](assets/screen-symbols.png) ![Symbol Screenshot](assets/screen-symbols.png)
![Diff Screenshot](assets/screen-diff.png) ![Diff Screenshot](assets/screen-diff.png)
### License ## Usage
objdiff works by comparing two relocatable object files (`.o`). The objects are expected to have the same relative path
from the "target" and "base" directories.
For example, if the target ("expected") object is located at `build/asm/MetroTRK/mslsupp.o` and the base ("actual")
object is located at `build/src/MetroTRK/mslsupp.o`, the following configuration would be used:
- Target build directory: `build/asm`
- Base build directory: `build/src`
- Object: `MetroTRK/mslsupp.o`
objdiff will then execute the build system from the project directory to build both objects:
```sh
$ make build/asm/MetroTRK/mslsupp.o # Only if "Build target object" is enabled
$ make build/src/MetroTRK/mslsupp.o
```
The objects will then be compared and the results will be displayed in the UI.
See [Configuration](#configuration) for more information.
## Configuration
While **not required** (most settings can be specified in the UI), projects can add an `objdiff.json` (or
`objdiff.yaml`, `objdiff.yml`) 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
{
"custom_make": "ninja",
// Only required if objects use "path" instead of "target_path" and "base_path".
"target_dir": "build/asm",
"base_dir": "build/src",
"build_target": true,
"watch_patterns": [
"*.c",
"*.cp",
"*.cpp",
"*.h",
"*.hpp",
"*.py"
],
"objects": [
{
"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
},
// ...
]
}
```
`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.
`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.
`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.
> `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 object from the project root.
> Required if `path` is not specified.
>
> `base_path`: Path to the base object from the project root.
> Required if `path` is not specified.
>
> `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.
## License
Licensed under either of Licensed under either of
@@ -23,6 +131,5 @@ at your option.
### Contribution ### Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
additional terms or conditions.

BIN
assets/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
assets/icon_64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 144 KiB

View File

@@ -1,4 +1,10 @@
use anyhow::Result; use anyhow::Result;
use vergen::{vergen, Config}; use vergen::EmitBuilder;
fn main() -> Result<()> { vergen(Config::default()) } fn main() -> Result<()> {
#[cfg(windows)]
{
winres::WindowsResource::new().set_icon("assets/icon.ico").compile()?;
}
EmitBuilder::builder().fail_on_error().all_build().all_cargo().all_git().emit()
}

View File

@@ -48,7 +48,11 @@ notice = "warn"
# A list of advisory IDs to ignore. Note that ignored advisories will still # A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered. # output a note when they are encountered.
ignore = [ ignore = [
#"RUSTSEC-0000-0000", "RUSTSEC-2023-0022",
"RUSTSEC-2023-0023",
"RUSTSEC-2023-0024",
"RUSTSEC-2023-0034",
"RUSTSEC-2023-0044",
] ]
# Threshold for security vulnerabilities, any vulnerability with a CVSS score # Threshold for security vulnerabilities, any vulnerability with a CVSS score
# lower than the range specified will be ignored. Note that ignored advisories # lower than the range specified will be ignored. Note that ignored advisories
@@ -81,6 +85,10 @@ allow = [
"Unicode-DFS-2016", "Unicode-DFS-2016",
"Zlib", "Zlib",
"0BSD", "0BSD",
"OFL-1.1",
"LicenseRef-UFL-1.0",
"OpenSSL",
"GPL-3.0",
] ]
# List of explictly disallowed licenses # List of explictly disallowed licenses
# See https://spdx.org/licenses/ for list of possible licenses # See https://spdx.org/licenses/ for list of possible licenses
@@ -118,22 +126,22 @@ exceptions = [
# Some crates don't have (easily) machine readable licensing information, # Some crates don't have (easily) machine readable licensing information,
# adding a clarification entry for it allows you to manually specify the # adding a clarification entry for it allows you to manually specify the
# licensing information # licensing information
#[[licenses.clarify]] [[licenses.clarify]]
# The name of the crate the clarification applies to # The name of the crate the clarification applies to
#name = "ring" name = "ring"
# The optional version constraint for the crate # The optional version constraint for the crate
#version = "*" version = "*"
# The SPDX expression for the license requirements of the crate # The SPDX expression for the license requirements of the crate
#expression = "MIT AND ISC AND OpenSSL" expression = "MIT AND ISC AND OpenSSL"
# One or more files in the crate's source used as the "source of truth" for # One or more files in the crate's source used as the "source of truth" for
# the license expression. If the contents match, the clarification will be used # the license expression. If the contents match, the clarification will be used
# when running the license check, otherwise the clarification will be ignored # when running the license check, otherwise the clarification will be ignored
# and the crate will be checked normally, which may produce warnings or errors # and the crate will be checked normally, which may produce warnings or errors
# depending on the rest of your configuration # depending on the rest of your configuration
#license-files = [ license-files = [
# Each entry is a crate relative path, and the (opaque) hash of its contents # Each entry is a crate relative path, and the (opaque) hash of its contents
#{ path = "LICENSE", hash = 0xbd0eed23 } { path = "LICENSE", hash = 0xbd0eed23 }
#] ]
[licenses.private] [licenses.private]
# If true, ignores workspace crates that aren't published, or are only # If true, ignores workspace crates that aren't published, or are only

View File

@@ -1,6 +1,5 @@
use std::{ use std::{
default::Default, default::Default,
ffi::OsStr,
path::{Path, PathBuf}, path::{Path, PathBuf},
rc::Rc, rc::Rc,
sync::{ sync::{
@@ -10,195 +9,163 @@ use std::{
time::Duration, time::Duration,
}; };
use egui::{Color32, FontFamily, FontId, TextStyle}; use globset::{Glob, GlobSet, GlobSetBuilder};
use notify::{RecursiveMode, Watcher}; use notify::{RecursiveMode, Watcher};
use time::{OffsetDateTime, UtcOffset}; use time::UtcOffset;
use crate::{ use crate::{
jobs::{ app_config::{deserialize_config, AppConfigVersion},
check_update::{queue_check_update, CheckUpdateResult}, config::{
objdiff::{queue_build, BuildStatus, ObjDiffResult}, build_globset, load_project_config, ProjectObject, ProjectObjectNode, CONFIG_FILENAMES,
Job, JobResult, JobState, JobStatus,
}, },
jobs::{objdiff::start_build, Job, JobQueue, JobResult, JobStatus},
views::{ views::{
config::config_ui, data_diff::data_diff_ui, function_diff::function_diff_ui, jobs::jobs_ui, appearance::{appearance_window, Appearance},
symbol_diff::symbol_diff_ui, config::{config_ui, project_window, ConfigViewState, DEFAULT_WATCH_PATTERNS},
data_diff::data_diff_ui,
demangle::{demangle_window, DemangleViewState},
function_diff::function_diff_ui,
jobs::jobs_ui,
symbol_diff::{symbol_diff_ui, DiffViewState, View},
}, },
}; };
#[allow(clippy::enum_variant_names)] #[derive(Default)]
#[derive(Default, Eq, PartialEq)]
pub enum View {
#[default]
SymbolDiff,
FunctionDiff,
DataDiff,
}
#[derive(Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum DiffKind {
#[default]
SplitObj,
WholeBinary,
}
#[derive(Default, Clone)]
pub struct DiffConfig {
// TODO
// pub stripped_symbols: Vec<String>,
// pub mapped_symbols: HashMap<String, String>,
}
const DEFAULT_COLOR_ROTATION: [Color32; 9] = [
Color32::from_rgb(255, 0, 255),
Color32::from_rgb(0, 255, 255),
Color32::from_rgb(0, 128, 0),
Color32::from_rgb(255, 0, 0),
Color32::from_rgb(255, 255, 0),
Color32::from_rgb(255, 192, 203),
Color32::from_rgb(0, 0, 255),
Color32::from_rgb(0, 255, 0),
Color32::from_rgb(128, 128, 128),
];
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct ViewConfig {
pub ui_font: FontId,
pub code_font: FontId,
pub diff_colors: Vec<Color32>,
}
impl Default for ViewConfig {
fn default() -> Self {
Self {
ui_font: FontId { size: 14.0, family: FontFamily::Proportional },
code_font: FontId { size: 14.0, family: FontFamily::Monospace },
diff_colors: DEFAULT_COLOR_ROTATION.to_vec(),
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct ViewState { pub struct ViewState {
#[serde(skip)] pub jobs: JobQueue,
pub jobs: Vec<JobState>, pub config_state: ConfigViewState,
#[serde(skip)] pub demangle_state: DemangleViewState,
pub build: Option<Box<ObjDiffResult>>, pub diff_state: DiffViewState,
#[serde(skip)] pub show_appearance_config: bool,
pub highlighted_symbol: Option<String>,
#[serde(skip)]
pub selected_symbol: Option<String>,
#[serde(skip)]
pub current_view: View,
#[serde(skip)]
pub show_config: bool,
#[serde(skip)]
pub show_demangle: bool, pub show_demangle: bool,
#[serde(skip)] pub show_project_config: bool,
pub demangle_text: String,
#[serde(skip)]
pub diff_config: DiffConfig,
#[serde(skip)]
pub search: String,
#[serde(skip)]
pub utc_offset: UtcOffset,
#[serde(skip)]
pub check_update: Option<Box<CheckUpdateResult>>,
// Config
pub diff_kind: DiffKind,
pub reverse_fn_order: bool,
pub view_config: ViewConfig,
} }
impl Default for ViewState { /// The configuration for a single object file.
#[derive(Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ObjectConfig {
pub name: String,
pub target_path: Option<PathBuf>,
pub base_path: Option<PathBuf>,
pub reverse_fn_order: Option<bool>,
pub complete: Option<bool>,
}
#[inline]
fn bool_true() -> bool { true }
#[derive(Clone, serde::Deserialize, serde::Serialize)]
pub struct AppConfig {
// TODO: https://github.com/ron-rs/ron/pull/455
// #[serde(flatten)]
// pub version: AppConfigVersion,
pub version: u32,
pub custom_make: Option<String>,
pub selected_wsl_distro: Option<String>,
pub project_dir: Option<PathBuf>,
pub target_obj_dir: Option<PathBuf>,
pub base_obj_dir: Option<PathBuf>,
pub selected_obj: Option<ObjectConfig>,
pub build_target: bool,
#[serde(default = "bool_true")]
pub rebuild_on_changes: bool,
pub auto_update_check: bool,
pub watch_patterns: Vec<Glob>,
#[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 project_config_loaded: bool,
}
impl Default for AppConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
jobs: vec![], version: AppConfigVersion::default().version,
build: None, custom_make: None,
highlighted_symbol: None, selected_wsl_distro: None,
selected_symbol: None, project_dir: None,
current_view: Default::default(), target_obj_dir: None,
show_config: false, base_obj_dir: None,
show_demangle: false, selected_obj: None,
demangle_text: String::new(), build_target: false,
diff_config: Default::default(), rebuild_on_changes: true,
search: Default::default(), auto_update_check: true,
utc_offset: UtcOffset::UTC, watch_patterns: DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(),
check_update: None, objects: vec![],
diff_kind: Default::default(), object_nodes: vec![],
reverse_fn_order: false, watcher_change: false,
view_config: Default::default(), config_change: false,
obj_change: false,
queue_build: false,
project_config_loaded: false,
} }
} }
} }
#[derive(Default, Clone, serde::Deserialize, serde::Serialize)] impl AppConfig {
#[serde(default)] pub fn set_project_dir(&mut self, path: PathBuf) {
pub struct AppConfig { self.project_dir = Some(path);
pub custom_make: Option<String>, self.target_obj_dir = None;
// WSL2 settings self.base_obj_dir = None;
#[serde(skip)] self.selected_obj = None;
pub available_wsl_distros: Option<Vec<String>>, self.build_target = false;
pub selected_wsl_distro: Option<String>, self.objects.clear();
// Split obj self.object_nodes.clear();
pub project_dir: Option<PathBuf>, self.watcher_change = true;
pub target_obj_dir: Option<PathBuf>, self.config_change = true;
pub base_obj_dir: Option<PathBuf>, self.obj_change = true;
pub obj_path: Option<String>, self.queue_build = false;
pub build_target: bool, self.project_config_loaded = false;
// Whole binary }
pub left_obj: Option<PathBuf>,
pub right_obj: Option<PathBuf>, pub fn set_target_obj_dir(&mut self, path: PathBuf) {
#[serde(skip)] self.target_obj_dir = Some(path);
pub project_dir_change: bool, self.selected_obj = None;
#[serde(skip)] self.obj_change = true;
pub queue_update_check: bool, self.queue_build = false;
pub auto_update_check: bool, }
pub fn set_base_obj_dir(&mut self, path: PathBuf) {
self.base_obj_dir = Some(path);
self.selected_obj = None;
self.obj_change = true;
self.queue_build = false;
}
pub fn set_selected_obj(&mut self, object: ObjectConfig) {
self.selected_obj = Some(object);
self.obj_change = true;
self.queue_build = false;
}
} }
#[derive(Default, Clone, serde::Deserialize)] pub type AppConfigRef = Arc<RwLock<AppConfig>>;
#[serde(default)]
pub struct ProjectConfig {
pub custom_make: Option<String>,
pub project_dir: Option<PathBuf>,
pub target_obj_dir: Option<PathBuf>,
pub base_obj_dir: Option<PathBuf>,
pub build_target: bool,
}
/// We derive Deserialize/Serialize so we can persist app state on shutdown. #[derive(Default)]
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct App { pub struct App {
appearance: Appearance,
view_state: ViewState, view_state: ViewState,
#[serde(skip)] config: AppConfigRef,
config: Arc<RwLock<AppConfig>>,
#[serde(skip)]
modified: Arc<AtomicBool>, modified: Arc<AtomicBool>,
#[serde(skip)] config_modified: Arc<AtomicBool>,
watcher: Option<notify::RecommendedWatcher>, watcher: Option<notify::RecommendedWatcher>,
#[serde(skip)]
relaunch_path: Rc<Mutex<Option<PathBuf>>>, relaunch_path: Rc<Mutex<Option<PathBuf>>>,
#[serde(skip)]
should_relaunch: bool, should_relaunch: bool,
} }
impl Default for App { pub const APPEARANCE_KEY: &str = "appearance";
fn default() -> Self { pub const CONFIG_KEY: &str = "app_config";
Self {
view_state: ViewState::default(),
config: Arc::new(Default::default()),
modified: Arc::new(Default::default()),
watcher: None,
relaunch_path: Default::default(),
should_relaunch: false,
}
}
}
const CONFIG_KEY: &str = "app_config";
impl App { impl App {
/// Called once before the first frame. /// Called once before the first frame.
@@ -212,22 +179,147 @@ impl App {
// Load previous app state (if any). // Load previous app state (if any).
// Note that you must enable the `persistence` feature for this to work. // Note that you must enable the `persistence` feature for this to work.
let mut app = Self::default();
if let Some(storage) = cc.storage { if let Some(storage) = cc.storage {
let mut app: App = eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default(); if let Some(appearance) = eframe::get_value::<Appearance>(storage, APPEARANCE_KEY) {
let mut config: AppConfig = eframe::get_value(storage, CONFIG_KEY).unwrap_or_default(); app.appearance = appearance;
if config.project_dir.is_some() {
config.project_dir_change = true;
} }
config.queue_update_check = config.auto_update_check; if let Some(mut config) = deserialize_config(storage) {
app.config = Arc::new(RwLock::new(config)); if config.project_dir.is_some() {
app.view_state.utc_offset = utc_offset; config.config_change = true;
app.relaunch_path = relaunch_path; config.watcher_change = true;
app app.modified.store(true, Ordering::Relaxed);
} else { }
let mut app = Self::default(); app.view_state.config_state.queue_check_update = config.auto_update_check;
app.view_state.utc_offset = utc_offset; app.config = Arc::new(RwLock::new(config));
app.relaunch_path = relaunch_path; }
app }
app.appearance.utc_offset = utc_offset;
app.relaunch_path = relaunch_path;
app
}
fn pre_update(&mut self) {
let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state;
let mut results = vec![];
for (job, result) in jobs.iter_finished() {
match result {
Ok(result) => {
log::info!("Job {} finished", job.id);
match result {
JobResult::None => {
if let Some(err) = &job.status.read().unwrap().error {
log::error!("{:?}", err);
}
}
JobResult::Update(state) => {
if let Ok(mut guard) = self.relaunch_path.lock() {
*guard = Some(state.exe_path);
}
self.should_relaunch = true;
}
_ => results.push(result),
}
}
Err(err) => {
let err = if let Some(msg) = err.downcast_ref::<&'static str>() {
anyhow::Error::msg(*msg)
} else if let Some(msg) = err.downcast_ref::<String>() {
anyhow::Error::msg(msg.clone())
} else {
anyhow::Error::msg("Thread panicked")
};
let result = job.status.write();
if let Ok(mut guard) = result {
guard.error = Some(err);
} else {
drop(result);
job.status = Arc::new(RwLock::new(JobStatus {
title: "Error".to_string(),
progress_percent: 0.0,
progress_items: None,
status: "".to_string(),
error: Some(err),
}));
}
}
}
}
jobs.results.append(&mut results);
jobs.clear_finished();
diff_state.pre_update(jobs, &self.config);
config_state.pre_update(jobs);
debug_assert!(jobs.results.is_empty());
}
fn post_update(&mut self) {
let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state;
config_state.post_update(jobs, &self.config);
diff_state.post_update(jobs, &self.config);
let Ok(mut config) = self.config.write() else {
return;
};
let config = &mut *config;
if self.config_modified.swap(false, Ordering::Relaxed) {
config.config_change = 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 {
if !config.watch_patterns.is_empty() {
match build_globset(&config.watch_patterns)
.map_err(anyhow::Error::new)
.and_then(|globset| {
create_watcher(
self.modified.clone(),
self.config_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;
}
// 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(self.config.clone()));
config.queue_build = false;
} }
} }
} }
@@ -241,142 +333,72 @@ impl eframe::App for App {
return; return;
} }
let Self { config, view_state, .. } = self; self.pre_update();
{ let Self { config, appearance, view_state, .. } = self;
let config = &view_state.view_config; ctx.set_style(appearance.apply(ctx.style().as_ref()));
let mut style = (*ctx.style()).clone();
style.text_styles.insert(TextStyle::Body, FontId { let ViewState {
size: (config.ui_font.size * 0.75).floor(), jobs,
family: config.ui_font.family.clone(), show_appearance_config,
}); demangle_state,
style.text_styles.insert(TextStyle::Body, config.ui_font.clone()); show_demangle,
style.text_styles.insert(TextStyle::Button, config.ui_font.clone()); diff_state,
style.text_styles.insert(TextStyle::Heading, FontId { config_state,
size: (config.ui_font.size * 1.5).floor(), show_project_config,
family: config.ui_font.family.clone(), } = view_state;
});
style.text_styles.insert(TextStyle::Monospace, config.code_font.clone());
ctx.set_style(style);
}
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| { egui::menu::bar(ui, |ui| {
ui.menu_button("File", |ui| { ui.menu_button("File", |ui| {
if ui.button("Show config").clicked() { if ui.button("Appearance…").clicked() {
view_state.show_config = !view_state.show_config; *show_appearance_config = !*show_appearance_config;
ui.close_menu();
} }
if ui.button("Quit").clicked() { if ui.button("Quit").clicked() {
frame.close(); frame.close();
} }
}); });
ui.menu_button("Tools", |ui| { ui.menu_button("Tools", |ui| {
if ui.button("Demangle").clicked() { if ui.button("Demangle").clicked() {
view_state.show_demangle = !view_state.show_demangle; *show_demangle = !*show_demangle;
ui.close_menu();
} }
}); });
}); });
}); });
if view_state.current_view == View::FunctionDiff let build_success = matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success);
&& matches!(&view_state.build, Some(b) if b.first_status.success && b.second_status.success) if diff_state.current_view == View::FunctionDiff && build_success {
{
// egui::SidePanel::left("side_panel").show(ctx, |ui| {
// if ui.button("Back").clicked() {
// view_state.current_view = View::SymbolDiff;
// }
// ui.separator();
// jobs_ui(ui, view_state);
// });
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
if function_diff_ui(ui, view_state) { function_diff_ui(ui, diff_state, appearance);
view_state
.jobs
.push(queue_build(config.clone(), view_state.diff_config.clone()));
}
}); });
} else if view_state.current_view == View::DataDiff } else if diff_state.current_view == View::DataDiff && build_success {
&& matches!(&view_state.build, Some(b) if b.first_status.success && b.second_status.success)
{
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
if data_diff_ui(ui, view_state) { data_diff_ui(ui, diff_state, appearance);
view_state
.jobs
.push(queue_build(config.clone(), view_state.diff_config.clone()));
}
}); });
} else { } else {
egui::SidePanel::left("side_panel").show(ctx, |ui| { egui::SidePanel::left("side_panel").show(ctx, |ui| {
config_ui(ui, config, view_state); egui::ScrollArea::both().show(ui, |ui| {
jobs_ui(ui, view_state); config_ui(ui, config, show_project_config, config_state, appearance);
jobs_ui(ui, jobs, appearance);
});
}); });
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
symbol_diff_ui(ui, view_state); symbol_diff_ui(ui, diff_state, appearance);
}); });
} }
egui::Window::new("Config").open(&mut view_state.show_config).show(ctx, |ui| { project_window(ctx, config, show_project_config, config_state, appearance);
ui.label("UI font:"); appearance_window(ctx, show_appearance_config, appearance);
egui::introspection::font_id_ui(ui, &mut view_state.view_config.ui_font); demangle_window(ctx, show_demangle, demangle_state, appearance);
ui.separator();
ui.label("Code font:");
egui::introspection::font_id_ui(ui, &mut view_state.view_config.code_font);
ui.separator();
ui.label("Diff colors:");
if ui.button("Reset").clicked() {
view_state.view_config.diff_colors = DEFAULT_COLOR_ROTATION.to_vec();
}
let mut remove_at: Option<usize> = None;
let num_colors = view_state.view_config.diff_colors.len();
for (idx, color) in view_state.view_config.diff_colors.iter_mut().enumerate() {
ui.horizontal(|ui| {
ui.color_edit_button_srgba(color);
if num_colors > 1 && ui.small_button("-").clicked() {
remove_at = Some(idx);
}
});
}
if let Some(idx) = remove_at {
view_state.view_config.diff_colors.remove(idx);
}
if ui.small_button("+").clicked() {
view_state.view_config.diff_colors.push(Color32::BLACK);
}
});
egui::Window::new("Demangle").open(&mut view_state.show_demangle).show(ctx, |ui| { self.post_update();
ui.text_edit_singleline(&mut view_state.demangle_text);
ui.add_space(10.0);
if let Some(demangled) =
cwdemangle::demangle(&view_state.demangle_text, &Default::default())
{
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
ui.colored_label(Color32::LIGHT_BLUE, &demangled);
});
if ui.button("Copy").clicked() {
ui.output().copied_text = demangled;
}
} else {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
ui.colored_label(Color32::LIGHT_RED, "[invalid]");
});
}
});
// Windows + request_repaint_after breaks dialogs: // Windows + request_repaint_after breaks dialogs:
// https://github.com/emilk/egui/issues/2003 // https://github.com/emilk/egui/issues/2003
if cfg!(windows) if cfg!(windows) || self.view_state.jobs.any_running() {
|| view_state.jobs.iter().any(|job| {
if let Some(handle) = &job.handle {
return !handle.is_finished();
}
false
})
{
ctx.request_repaint(); ctx.request_repaint();
} else { } else {
ctx.request_repaint_after(Duration::from_millis(100)); ctx.request_repaint_after(Duration::from_millis(100));
@@ -388,151 +410,45 @@ impl eframe::App for App {
if let Ok(config) = self.config.read() { if let Ok(config) = self.config.read() {
eframe::set_value(storage, CONFIG_KEY, &*config); eframe::set_value(storage, CONFIG_KEY, &*config);
} }
eframe::set_value(storage, eframe::APP_KEY, self); eframe::set_value(storage, APPEARANCE_KEY, &self.appearance);
}
fn post_rendering(&mut self, _window_size_px: [u32; 2], _frame: &eframe::Frame) {
for job in &mut self.view_state.jobs {
if let Some(handle) = &job.handle {
if !handle.is_finished() {
continue;
}
match job.handle.take().unwrap().join() {
Ok(result) => {
log::info!("Job {} finished", job.id);
match result {
JobResult::None => {
if let Some(err) = &job.status.read().unwrap().error {
log::error!("{:?}", err);
}
}
JobResult::ObjDiff(state) => {
self.view_state.build = Some(state);
}
JobResult::BinDiff(state) => {
self.view_state.build = Some(Box::new(ObjDiffResult {
first_status: BuildStatus {
success: true,
log: "".to_string(),
},
second_status: BuildStatus {
success: true,
log: "".to_string(),
},
first_obj: Some(state.first_obj),
second_obj: Some(state.second_obj),
time: OffsetDateTime::now_utc(),
}));
}
JobResult::CheckUpdate(state) => {
self.view_state.check_update = Some(state);
}
JobResult::Update(state) => {
if let Ok(mut guard) = self.relaunch_path.lock() {
*guard = Some(state.exe_path);
}
self.should_relaunch = true;
}
}
}
Err(err) => {
let err = if let Some(msg) = err.downcast_ref::<&'static str>() {
anyhow::Error::msg(*msg)
} else if let Some(msg) = err.downcast_ref::<String>() {
anyhow::Error::msg(msg.clone())
} else {
anyhow::Error::msg("Thread panicked")
};
let result = job.status.write();
if let Ok(mut guard) = result {
guard.error = Some(err);
} else {
drop(result);
job.status = Arc::new(RwLock::new(JobStatus {
title: "Error".to_string(),
progress_percent: 0.0,
progress_items: None,
status: "".to_string(),
error: Some(err),
}));
}
}
}
}
}
if self.view_state.jobs.iter().any(|v| v.should_remove) {
let mut i = 0;
while i < self.view_state.jobs.len() {
let job = &self.view_state.jobs[i];
if job.should_remove
&& job.handle.is_none()
&& job.status.read().unwrap().error.is_none()
{
self.view_state.jobs.remove(i);
} else {
i += 1;
}
}
}
if let Ok(mut config) = self.config.write() {
if config.project_dir_change {
drop(self.watcher.take());
if let Some(project_dir) = &config.project_dir {
match create_watcher(self.modified.clone(), project_dir) {
Ok(watcher) => self.watcher = Some(watcher),
Err(e) => eprintln!("Failed to create watcher: {e}"),
}
config.project_dir_change = false;
self.modified.store(true, Ordering::Relaxed);
}
}
if config.obj_path.is_some() && self.modified.load(Ordering::Relaxed) {
if !self
.view_state
.jobs
.iter()
.any(|j| j.job_type == Job::ObjDiff && j.handle.is_some())
{
self.view_state.jobs.push(queue_build(
self.config.clone(),
self.view_state.diff_config.clone(),
));
}
self.modified.store(false, Ordering::Relaxed);
}
if config.queue_update_check {
self.view_state.jobs.push(queue_check_update());
config.queue_update_check = false;
}
}
} }
} }
fn create_watcher( fn create_watcher(
modified: Arc<AtomicBool>, modified: Arc<AtomicBool>,
config_modified: Arc<AtomicBool>,
project_dir: &Path, project_dir: &Path,
patterns: GlobSet,
) -> notify::Result<notify::RecommendedWatcher> { ) -> notify::Result<notify::RecommendedWatcher> {
let mut config_patterns = GlobSetBuilder::new();
for filename in CONFIG_FILENAMES {
config_patterns.add(Glob::new(filename).unwrap());
}
let config_patterns = config_patterns.build().unwrap();
let base_dir = project_dir.to_owned();
let mut watcher = let mut watcher =
notify::recommended_watcher(move |res: notify::Result<notify::Event>| match res { notify::recommended_watcher(move |res: notify::Result<notify::Event>| match res {
Ok(event) => { Ok(event) => {
if matches!(event.kind, notify::EventKind::Modify(..)) { if matches!(
let watch_extensions = &[ event.kind,
Some(OsStr::new("c")), notify::EventKind::Modify(..)
Some(OsStr::new("cp")), | notify::EventKind::Create(..)
Some(OsStr::new("cpp")), | notify::EventKind::Remove(..)
Some(OsStr::new("h")), ) {
Some(OsStr::new("hpp")), for path in &event.paths {
Some(OsStr::new("s")), let Ok(path) = path.strip_prefix(&base_dir) else {
]; continue;
if event.paths.iter().any(|p| watch_extensions.contains(&p.extension())) { };
modified.store(true, Ordering::Relaxed); if config_patterns.is_match(path) {
config_modified.store(true, Ordering::Relaxed);
} else if patterns.is_match(path) {
modified.store(true, Ordering::Relaxed);
}
} }
} }
} }
Err(e) => println!("watch error: {e:?}"), Err(e) => log::error!("watch error: {e:?}"),
})?; })?;
watcher.watch(project_dir, RecursiveMode::Recursive)?; watcher.watch(project_dir, RecursiveMode::Recursive)?;
Ok(watcher) Ok(watcher)

96
src/app_config.rs Normal file
View File

@@ -0,0 +1,96 @@
use std::path::PathBuf;
use eframe::Storage;
use globset::Glob;
use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY};
#[derive(Clone, serde::Deserialize, serde::Serialize)]
pub struct AppConfigVersion {
pub version: u32,
}
impl Default for AppConfigVersion {
fn default() -> Self { Self { version: 1 } }
}
/// Deserialize the AppConfig from storage, handling upgrades from older versions.
pub fn deserialize_config(storage: &dyn Storage) -> Option<AppConfig> {
let str = storage.get_string(CONFIG_KEY)?;
match ron::from_str::<AppConfigVersion>(&str) {
Ok(version) => match version.version {
1 => from_str::<AppConfig>(&str),
_ => {
log::warn!("Unknown config version: {}", version.version);
None
}
},
Err(e) => {
log::warn!("Failed to decode config version: {e}");
// Try to decode as v0
from_str::<AppConfigV0>(&str).map(|c| c.into_config())
}
}
}
fn from_str<T>(str: &str) -> Option<T>
where T: serde::de::DeserializeOwned {
match ron::from_str(str) {
Ok(config) => Some(config),
Err(err) => {
log::warn!("Failed to decode config: {err}");
None
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct ObjectConfigV0 {
pub name: String,
pub target_path: PathBuf,
pub base_path: PathBuf,
pub reverse_fn_order: Option<bool>,
}
impl ObjectConfigV0 {
fn into_config(self) -> ObjectConfig {
ObjectConfig {
name: self.name,
target_path: Some(self.target_path),
base_path: Some(self.base_path),
reverse_fn_order: self.reverse_fn_order,
complete: None,
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct AppConfigV0 {
pub custom_make: Option<String>,
pub selected_wsl_distro: Option<String>,
pub project_dir: Option<PathBuf>,
pub target_obj_dir: Option<PathBuf>,
pub base_obj_dir: Option<PathBuf>,
pub selected_obj: Option<ObjectConfigV0>,
pub build_target: bool,
pub auto_update_check: bool,
pub watch_patterns: Vec<Glob>,
}
impl AppConfigV0 {
fn into_config(self) -> AppConfig {
log::info!("Upgrading configuration from v0");
AppConfig {
custom_make: self.custom_make,
selected_wsl_distro: self.selected_wsl_distro,
project_dir: self.project_dir,
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_target: self.build_target,
auto_update_check: self.auto_update_check,
watch_patterns: self.watch_patterns,
..Default::default()
}
}
}

180
src/config.rs Normal file
View File

@@ -0,0 +1,180 @@
use std::{
fs::File,
path::{Component, Path, PathBuf},
};
use anyhow::{bail, Context, Result};
use globset::{Glob, GlobSet, GlobSetBuilder};
use crate::{app::AppConfig, views::config::DEFAULT_WATCH_PATTERNS};
#[derive(Default, Clone, serde::Deserialize)]
#[serde(default)]
pub struct ProjectConfig {
pub min_version: Option<String>,
pub custom_make: Option<String>,
pub target_dir: Option<PathBuf>,
pub base_dir: Option<PathBuf>,
pub build_target: bool,
pub watch_patterns: Option<Vec<Glob>>,
#[serde(alias = "units")]
pub objects: Vec<ProjectObject>,
}
#[derive(Default, Clone, serde::Deserialize)]
pub struct ProjectObject {
pub name: Option<String>,
pub path: Option<PathBuf>,
pub target_path: Option<PathBuf>,
pub base_path: Option<PathBuf>,
pub reverse_fn_order: Option<bool>,
pub complete: Option<bool>,
}
impl ProjectObject {
pub fn name(&self) -> &str {
if let Some(name) = &self.name {
name
} else if let Some(path) = &self.path {
path.to_str().unwrap_or("[invalid path]")
} else {
"[unknown]"
}
}
}
#[derive(Clone)]
pub enum ProjectObjectNode {
File(String, ProjectObject),
Dir(String, Vec<ProjectObjectNode>),
}
fn find_dir<'a>(
name: &str,
nodes: &'a mut Vec<ProjectObjectNode>,
) -> &'a mut Vec<ProjectObjectNode> {
if let Some(index) = nodes
.iter()
.position(|node| matches!(node, ProjectObjectNode::Dir(dir_name, _) if dir_name == name))
{
if let ProjectObjectNode::Dir(_, children) = &mut nodes[index] {
return children;
}
} else {
nodes.push(ProjectObjectNode::Dir(name.to_string(), vec![]));
if let Some(ProjectObjectNode::Dir(_, children)) = nodes.last_mut() {
return children;
}
}
unreachable!();
}
fn build_nodes(
objects: &[ProjectObject],
project_dir: &Path,
target_obj_dir: &Option<PathBuf>,
base_obj_dir: &Option<PathBuf>,
) -> Vec<ProjectObjectNode> {
let mut nodes = vec![];
for object in objects {
let mut out_nodes = &mut nodes;
let path = if let Some(name) = &object.name {
Path::new(name)
} else if let Some(path) = &object.path {
path
} else {
continue;
};
if let Some(parent) = path.parent() {
for component in parent.components() {
if let Component::Normal(name) = component {
let name = name.to_str().unwrap();
out_nodes = find_dir(name, out_nodes);
}
}
}
let mut object = object.clone();
if let (Some(target_obj_dir), Some(path), None) =
(target_obj_dir, &object.path, &object.target_path)
{
object.target_path = Some(target_obj_dir.join(path));
} else if let Some(path) = &object.target_path {
object.target_path = Some(project_dir.join(path));
}
if let (Some(base_obj_dir), Some(path), None) =
(base_obj_dir, &object.path, &object.base_path)
{
object.base_path = Some(base_obj_dir.join(path));
} else if let Some(path) = &object.base_path {
object.base_path = Some(project_dir.join(path));
}
let filename = path.file_name().unwrap().to_str().unwrap().to_string();
out_nodes.push(ProjectObjectNode::File(filename, object));
}
nodes
}
pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.yml", "objdiff.yaml", "objdiff.json"];
pub fn load_project_config(config: &mut AppConfig) -> Result<()> {
let Some(project_dir) = &config.project_dir else {
return Ok(());
};
if let Some(result) = try_project_config(project_dir) {
let project_config = result?;
if let Some(min_version) = &project_config.min_version {
let version_str = env!("CARGO_PKG_VERSION");
let version = semver::Version::parse(version_str).unwrap();
let version_req = semver::VersionReq::parse(&format!(">={min_version}"))?;
if !version_req.matches(&version) {
bail!("Project requires objdiff version {} or higher", min_version);
}
}
config.custom_make = project_config.custom_make;
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_target = project_config.build_target;
config.watch_patterns = project_config.watch_patterns.unwrap_or_else(|| {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
});
config.watcher_change = true;
config.objects = project_config.objects;
config.object_nodes =
build_nodes(&config.objects, project_dir, &config.target_obj_dir, &config.base_obj_dir);
config.project_config_loaded = true;
}
Ok(())
}
fn try_project_config(dir: &Path) -> Option<Result<ProjectConfig>> {
for filename in CONFIG_FILENAMES.iter() {
let config_path = dir.join(filename);
if config_path.is_file() {
return match filename.contains("json") {
true => Some(read_json_config(&config_path)),
false => Some(read_yml_config(&config_path)),
};
}
}
None
}
fn read_yml_config(config_path: &Path) -> Result<ProjectConfig> {
let mut reader = File::open(config_path)
.with_context(|| format!("Failed to open config file '{}'", config_path.display()))?;
Ok(serde_yaml::from_reader(&mut reader)?)
}
fn read_json_config(config_path: &Path) -> Result<ProjectConfig> {
let mut reader = File::open(config_path)
.with_context(|| format!("Failed to open config file '{}'", config_path.display()))?;
Ok(serde_json::from_reader(&mut reader)?)
}
pub fn build_globset(vec: &[Glob]) -> std::result::Result<GlobSet, globset::Error> {
let mut builder = GlobSetBuilder::new();
for glob in vec {
builder.add(glob.clone());
}
builder.build()
}

View File

@@ -3,7 +3,6 @@ use std::{collections::BTreeMap, mem::take};
use anyhow::Result; use anyhow::Result;
use crate::{ use crate::{
app::DiffConfig,
editops::{editops_find, LevEditType}, editops::{editops_find, LevEditType},
obj::{ obj::{
mips, ppc, ObjArchitecture, ObjDataDiff, ObjDataDiffKind, ObjInfo, ObjInsArg, mips, ppc, ObjArchitecture, ObjDataDiff, ObjDataDiffKind, ObjInfo, ObjInsArg,
@@ -17,14 +16,19 @@ fn no_diff_code(
data: &[u8], data: &[u8],
symbol: &mut ObjSymbol, symbol: &mut ObjSymbol,
relocs: &[ObjReloc], relocs: &[ObjReloc],
line_info: &Option<BTreeMap<u32, u32>>,
) -> Result<()> { ) -> Result<()> {
let code = let code =
&data[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize]; &data[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
let (_, ins) = match arch { let (_, ins) = match arch {
ObjArchitecture::PowerPc => ppc::process_code(code, symbol.address, relocs)?, ObjArchitecture::PowerPc => ppc::process_code(code, symbol.address, relocs, line_info)?,
ObjArchitecture::Mips => { ObjArchitecture::Mips => mips::process_code(
mips::process_code(code, symbol.address, symbol.address + symbol.size, relocs)? code,
} symbol.address,
symbol.address + symbol.size,
relocs,
line_info,
)?,
}; };
let mut diff = Vec::<ObjInsDiff>::new(); let mut diff = Vec::<ObjInsDiff>::new();
@@ -36,6 +40,7 @@ fn no_diff_code(
Ok(()) Ok(())
} }
#[allow(clippy::too_many_arguments)]
pub fn diff_code( pub fn diff_code(
arch: ObjArchitecture, arch: ObjArchitecture,
left_data: &[u8], left_data: &[u8],
@@ -44,6 +49,8 @@ pub fn diff_code(
right_symbol: &mut ObjSymbol, right_symbol: &mut ObjSymbol,
left_relocs: &[ObjReloc], left_relocs: &[ObjReloc],
right_relocs: &[ObjReloc], right_relocs: &[ObjReloc],
left_line_info: &Option<BTreeMap<u32, u32>>,
right_line_info: &Option<BTreeMap<u32, u32>>,
) -> Result<()> { ) -> Result<()> {
let left_code = &left_data[left_symbol.section_address as usize let left_code = &left_data[left_symbol.section_address as usize
..(left_symbol.section_address + left_symbol.size) as usize]; ..(left_symbol.section_address + left_symbol.size) as usize];
@@ -51,8 +58,8 @@ pub fn diff_code(
..(right_symbol.section_address + right_symbol.size) as usize]; ..(right_symbol.section_address + right_symbol.size) as usize];
let ((left_ops, left_insts), (right_ops, right_insts)) = match arch { let ((left_ops, left_insts), (right_ops, right_insts)) = match arch {
ObjArchitecture::PowerPc => ( ObjArchitecture::PowerPc => (
ppc::process_code(left_code, left_symbol.address, left_relocs)?, ppc::process_code(left_code, left_symbol.address, left_relocs, left_line_info)?,
ppc::process_code(right_code, right_symbol.address, right_relocs)?, ppc::process_code(right_code, right_symbol.address, right_relocs, right_line_info)?,
), ),
ObjArchitecture::Mips => ( ObjArchitecture::Mips => (
mips::process_code( mips::process_code(
@@ -60,12 +67,14 @@ pub fn diff_code(
left_symbol.address, left_symbol.address,
left_symbol.address + left_symbol.size, left_symbol.address + left_symbol.size,
left_relocs, left_relocs,
left_line_info,
)?, )?,
mips::process_code( mips::process_code(
right_code, right_code,
right_symbol.address, right_symbol.address,
left_symbol.address + left_symbol.size, left_symbol.address + left_symbol.size,
right_relocs, right_relocs,
right_line_info,
)?, )?,
), ),
}; };
@@ -123,7 +132,6 @@ pub fn diff_code(
right_diff.push(ObjInsDiff::default()); right_diff.push(ObjInsDiff::default());
cur_left = left_iter.next(); cur_left = left_iter.next();
} }
LevEditType::Keep => unreachable!(),
} }
} else { } else {
break; break;
@@ -211,25 +219,25 @@ fn address_eq(left: &ObjSymbol, right: &ObjSymbol) -> bool {
} }
fn reloc_eq(left_reloc: Option<&ObjReloc>, right_reloc: Option<&ObjReloc>) -> bool { fn reloc_eq(left_reloc: Option<&ObjReloc>, right_reloc: Option<&ObjReloc>) -> bool {
if let (Some(left), Some(right)) = (left_reloc, right_reloc) { let (Some(left), Some(right)) = (left_reloc, right_reloc) else {
if left.kind != right.kind { return false;
return false; };
if left.kind != right.kind {
return false;
}
let name_matches = left.target.name == right.target.name;
match (&left.target_section, &right.target_section) {
(Some(sl), Some(sr)) => {
// Match if section and name or address match
sl == sr && (name_matches || address_eq(&left.target, &right.target))
} }
let name_matches = left.target.name == right.target.name; (Some(_), None) => false,
match (&left.target_section, &right.target_section) { (None, Some(_)) => {
(Some(sl), Some(sr)) => { // Match if possibly stripped weak symbol
// Match if section and name or address match name_matches && right.target.flags.0.contains(ObjSymbolFlags::Weak)
sl == sr && (name_matches || address_eq(&left.target, &right.target))
}
(Some(_), None) => false,
(None, Some(_)) => {
// Match if possibly stripped weak symbol
name_matches && right.target.flags.0.contains(ObjSymbolFlags::Weak)
}
(None, None) => name_matches,
} }
} else { (None, None) => name_matches,
false
} }
} }
@@ -258,8 +266,8 @@ fn arg_eq(
right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()), right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
) )
} }
ObjInsArg::MipsArg(ls) => { ObjInsArg::MipsArg(ls) | ObjInsArg::MipsArgWithBase(ls) => {
matches!(right, ObjInsArg::MipsArg(rs) if ls == rs) matches!(right, ObjInsArg::MipsArg(rs) | ObjInsArg::MipsArgWithBase(rs) if ls == rs)
} }
ObjInsArg::BranchOffset(_) => { ObjInsArg::BranchOffset(_) => {
// Compare dest instruction idx after diffing // Compare dest instruction idx after diffing
@@ -314,7 +322,7 @@ fn compare_ins(
let a_str = match a { let a_str = match a {
ObjInsArg::PpcArg(arg) => format!("{arg}"), ObjInsArg::PpcArg(arg) => format!("{arg}"),
ObjInsArg::Reloc | ObjInsArg::RelocWithBase => String::new(), ObjInsArg::Reloc | ObjInsArg::RelocWithBase => String::new(),
ObjInsArg::MipsArg(str) => str.clone(), ObjInsArg::MipsArg(str) | ObjInsArg::MipsArgWithBase(str) => str.clone(),
ObjInsArg::BranchOffset(arg) => format!("{arg}"), ObjInsArg::BranchOffset(arg) => format!("{arg}"),
}; };
let a_diff = if let Some(idx) = state.left_args_idx.get(&a_str) { let a_diff = if let Some(idx) = state.left_args_idx.get(&a_str) {
@@ -328,7 +336,7 @@ fn compare_ins(
let b_str = match b { let b_str = match b {
ObjInsArg::PpcArg(arg) => format!("{arg}"), ObjInsArg::PpcArg(arg) => format!("{arg}"),
ObjInsArg::Reloc | ObjInsArg::RelocWithBase => String::new(), ObjInsArg::Reloc | ObjInsArg::RelocWithBase => String::new(),
ObjInsArg::MipsArg(str) => str.clone(), ObjInsArg::MipsArg(str) | ObjInsArg::MipsArgWithBase(str) => str.clone(),
ObjInsArg::BranchOffset(arg) => format!("{arg}"), ObjInsArg::BranchOffset(arg) => format!("{arg}"),
}; };
let b_diff = if let Some(idx) = state.right_args_idx.get(&b_str) { let b_diff = if let Some(idx) = state.right_args_idx.get(&b_str) {
@@ -353,22 +361,29 @@ fn compare_ins(
Ok(result) Ok(result)
} }
fn find_section<'a>(obj: &'a mut ObjInfo, name: &str) -> Option<&'a mut ObjSection> { fn find_section_and_symbol(obj: &ObjInfo, name: &str) -> Option<(usize, usize)> {
obj.sections.iter_mut().find(|s| s.name == name) for (section_idx, section) in obj.sections.iter().enumerate() {
let symbol_idx = match section.symbols.iter().position(|symbol| symbol.name == name) {
Some(symbol_idx) => symbol_idx,
None => continue,
};
return Some((section_idx, symbol_idx));
}
None
} }
fn find_symbol<'a>(symbols: &'a mut [ObjSymbol], name: &str) -> Option<&'a mut ObjSymbol> { pub fn diff_objs(mut left: Option<&mut ObjInfo>, mut right: Option<&mut ObjInfo>) -> Result<()> {
symbols.iter_mut().find(|s| s.name == name) if let Some(left) = left.as_mut() {
} for left_section in &mut left.sections {
pub fn diff_objs(left: &mut ObjInfo, right: &mut ObjInfo, _diff_config: &DiffConfig) -> Result<()> {
for left_section in &mut left.sections {
if let Some(right_section) = find_section(right, &left_section.name) {
if left_section.kind == ObjSectionKind::Code { if left_section.kind == ObjSectionKind::Code {
for left_symbol in &mut left_section.symbols { for left_symbol in &mut left_section.symbols {
if let Some(right_symbol) = if let Some((right, (right_section_idx, right_symbol_idx))) =
find_symbol(&mut right_section.symbols, &left_symbol.name) right.as_mut().and_then(|obj| {
find_section_and_symbol(obj, &left_symbol.name).map(|s| (obj, s))
})
{ {
let right_section = &mut right.sections[right_section_idx];
let right_symbol = &mut right_section.symbols[right_symbol_idx];
left_symbol.diff_symbol = Some(right_symbol.name.clone()); left_symbol.diff_symbol = Some(right_symbol.name.clone());
right_symbol.diff_symbol = Some(left_symbol.name.clone()); right_symbol.diff_symbol = Some(left_symbol.name.clone());
diff_code( diff_code(
@@ -379,6 +394,8 @@ pub fn diff_objs(left: &mut ObjInfo, right: &mut ObjInfo, _diff_config: &DiffCon
right_symbol, right_symbol,
&left_section.relocations, &left_section.relocations,
&right_section.relocations, &right_section.relocations,
&left.line_info,
&right.line_info,
)?; )?;
} else { } else {
no_diff_code( no_diff_code(
@@ -386,34 +403,55 @@ pub fn diff_objs(left: &mut ObjInfo, right: &mut ObjInfo, _diff_config: &DiffCon
&left_section.data, &left_section.data,
left_symbol, left_symbol,
&left_section.relocations, &left_section.relocations,
&left.line_info,
)?; )?;
} }
} }
for right_symbol in &mut right_section.symbols { } else if let Some(right_section) = right
if right_symbol.instructions.is_empty() { .as_mut()
no_diff_code( .and_then(|obj| obj.sections.iter_mut().find(|s| s.name == left_section.name))
left.architecture, {
&right_section.data, if left_section.kind == ObjSectionKind::Data {
right_symbol, diff_data(left_section, right_section);
&right_section.relocations, // diff_data_symbols(left_section, right_section)?;
)?; } else if left_section.kind == ObjSectionKind::Bss {
} diff_bss_symbols(&mut left_section.symbols, &mut right_section.symbols)?;
} }
} else if left_section.kind == ObjSectionKind::Data { } else if left_section.kind == ObjSectionKind::Data {
diff_data(left_section, right_section); no_diff_data(left_section);
// diff_data_symbols(left_section, right_section)?;
} else if left_section.kind == ObjSectionKind::Bss {
diff_bss_symbols(&mut left_section.symbols, &mut right_section.symbols)?;
} }
} }
} }
diff_bss_symbols(&mut left.common, &mut right.common)?; if let Some(right) = right.as_mut() {
for right_section in right.sections.iter_mut() {
if right_section.kind == ObjSectionKind::Code {
for right_symbol in &mut right_section.symbols {
if right_symbol.instructions.is_empty() {
no_diff_code(
right.architecture,
&right_section.data,
right_symbol,
&right_section.relocations,
&right.line_info,
)?;
}
}
} else if right_section.kind == ObjSectionKind::Data
&& right_section.data_diff.is_empty()
{
no_diff_data(right_section);
}
}
}
if let (Some(left), Some(right)) = (left, right) {
diff_bss_symbols(&mut left.common, &mut right.common)?;
}
Ok(()) Ok(())
} }
fn diff_bss_symbols(left_symbols: &mut [ObjSymbol], right_symbols: &mut [ObjSymbol]) -> Result<()> { fn diff_bss_symbols(left_symbols: &mut [ObjSymbol], right_symbols: &mut [ObjSymbol]) -> Result<()> {
for left_symbol in left_symbols { for left_symbol in left_symbols {
if let Some(right_symbol) = find_symbol(right_symbols, &left_symbol.name) { if let Some(right_symbol) = right_symbols.iter_mut().find(|s| s.name == left_symbol.name) {
left_symbol.diff_symbol = Some(right_symbol.name.clone()); left_symbol.diff_symbol = Some(right_symbol.name.clone());
right_symbol.diff_symbol = Some(left_symbol.name.clone()); right_symbol.diff_symbol = Some(left_symbol.name.clone());
let percent = if left_symbol.size == right_symbol.size { 100.0 } else { 50.0 }; let percent = if left_symbol.size == right_symbol.size { 100.0 } else { 50.0 };
@@ -500,13 +538,12 @@ fn diff_data(left: &mut ObjSection, right: &mut ObjSection) {
let mut right_diff = Vec::<ObjDataDiff>::new(); let mut right_diff = Vec::<ObjDataDiff>::new();
let mut left_cur = 0usize; let mut left_cur = 0usize;
let mut right_cur = 0usize; let mut right_cur = 0usize;
let mut cur_op = LevEditType::Keep; let mut cur_op = LevEditType::Replace;
let mut cur_left_data = Vec::<u8>::new(); let mut cur_left_data = Vec::<u8>::new();
let mut cur_right_data = Vec::<u8>::new(); let mut cur_right_data = Vec::<u8>::new();
for op in edit_ops { for op in edit_ops {
if cur_op != op.op_type || left_cur < op.first_start || right_cur < op.second_start { if cur_op != op.op_type || left_cur < op.first_start || right_cur < op.second_start {
match cur_op { match cur_op {
LevEditType::Keep => {}
LevEditType::Replace => { LevEditType::Replace => {
let left_data = take(&mut cur_left_data); let left_data = take(&mut cur_left_data);
let right_data = take(&mut cur_right_data); let right_data = take(&mut cur_right_data);
@@ -592,7 +629,6 @@ fn diff_data(left: &mut ObjSection, right: &mut ObjSection) {
cur_left_data.push(left.data[left_cur]); cur_left_data.push(left.data[left_cur]);
left_cur += 1; left_cur += 1;
} }
LevEditType::Keep => unreachable!(),
} }
cur_op = op.op_type; cur_op = op.op_type;
} }
@@ -616,7 +652,6 @@ fn diff_data(left: &mut ObjSection, right: &mut ObjSection) {
// TODO: merge with above // TODO: merge with above
match cur_op { match cur_op {
LevEditType::Keep => {}
LevEditType::Replace => { LevEditType::Replace => {
let left_data = take(&mut cur_left_data); let left_data = take(&mut cur_left_data);
let right_data = take(&mut cur_right_data); let right_data = take(&mut cur_right_data);
@@ -689,3 +724,12 @@ fn diff_data(left: &mut ObjSection, right: &mut ObjSection) {
left.data_diff = left_diff; left.data_diff = left_diff;
right.data_diff = right_diff; right.data_diff = right_diff;
} }
fn no_diff_data(section: &mut ObjSection) {
section.data_diff = vec![ObjDataDiff {
data: section.data.clone(),
kind: ObjDataDiffKind::None,
len: section.data.len(),
symbol: String::new(),
}];
}

View File

@@ -27,7 +27,6 @@
#[derive(Debug, PartialEq, Eq, Copy, Clone)] #[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum LevEditType { pub enum LevEditType {
Keep,
Replace, Replace,
Insert, Insert,
Delete, Delete,
@@ -40,25 +39,15 @@ pub struct LevEditOp {
pub second_start: usize, /* destination position */ pub second_start: usize, /* destination position */
} }
#[derive(Debug, PartialEq, Eq)]
pub struct LevMatchingBlock {
pub first_start: usize,
pub second_start: usize,
pub len: usize,
}
pub fn editops_find<T>(query: &[T], choice: &[T]) -> Vec<LevEditOp> pub fn editops_find<T>(query: &[T], choice: &[T]) -> Vec<LevEditOp>
where T: PartialEq { where T: PartialEq {
let string_affix = Affix::find(query, choice); let Affix { prefix_len, suffix_len } = Affix::find(query, choice);
let first_string_len = string_affix.first_string_len; let first_string = &query[prefix_len..query.len() - suffix_len];
let second_string_len = string_affix.second_string_len; let second_string = &choice[prefix_len..choice.len() - suffix_len];
let prefix_len = string_affix.prefix_len;
let first_string = &query[prefix_len..prefix_len + first_string_len];
let second_string = &choice[prefix_len..prefix_len + second_string_len];
let matrix_columns = first_string_len + 1; let matrix_columns = first_string.len() + 1;
let matrix_rows = second_string_len + 1; let matrix_rows = second_string.len() + 1;
// TODO maybe use an actual matrix for readability // TODO maybe use an actual matrix for readability
let mut cache_matrix: Vec<usize> = vec![0; matrix_rows * matrix_columns]; let mut cache_matrix: Vec<usize> = vec![0; matrix_rows * matrix_columns];
@@ -87,98 +76,66 @@ where T: PartialEq {
cache_matrix[current + 1 + p] = x; cache_matrix[current + 1 + p] = x;
} }
} }
editops_from_cost_matrix( editops_from_cost_matrix(matrix_columns, matrix_rows, prefix_len, cache_matrix)
first_string,
second_string,
matrix_columns,
matrix_rows,
prefix_len,
cache_matrix,
)
} }
fn editops_from_cost_matrix<T>( fn editops_from_cost_matrix(
string1: &[T],
string2: &[T],
len1: usize, len1: usize,
len2: usize, len2: usize,
prefix_len: usize, prefix_len: usize,
cache_matrix: Vec<usize>, cache_matrix: Vec<usize>,
) -> Vec<LevEditOp> ) -> Vec<LevEditOp> {
where let mut ops = Vec::with_capacity(cache_matrix[len1 * len2 - 1]);
T: PartialEq,
{
let mut dir = 0; let mut dir = 0;
let mut ops: Vec<LevEditOp> = vec![];
ops.reserve(cache_matrix[len1 * len2 - 1]);
let mut i = len1 - 1; let mut i = len1 - 1;
let mut j = len2 - 1; let mut j = len2 - 1;
let mut p = len1 * len2 - 1; let mut p = len1 * len2 - 1;
// let string1_chars: Vec<char> = string1.chars().collect();
// let string2_chars: Vec<char> = string2.chars().collect();
//TODO this is still pretty ugly //TODO this is still pretty ugly
while i > 0 || j > 0 { while i > 0 || j > 0 {
let current_value = cache_matrix[p]; let current_value = cache_matrix[p];
let op_type; // More than one operation can be possible at a time. We use `dir` to
// decide when ambiguous.
let is_insert = j > 0 && current_value == cache_matrix[p - 1] + 1;
let is_delete = i > 0 && current_value == cache_matrix[p - len2] + 1;
let is_replace = i > 0 && j > 0 && current_value == cache_matrix[p - len2 - 1] + 1;
if dir == -1 && j > 0 && current_value == cache_matrix[p - 1] + 1 { let (op_type, new_dir) = match (dir, is_insert, is_delete, is_replace) {
op_type = LevEditType::Insert; (_, false, false, false) => (None, 0),
} else if dir == 1 && i > 0 && current_value == cache_matrix[p - len2] + 1 { (-1, true, _, _) => (Some(LevEditType::Insert), -1),
op_type = LevEditType::Delete; (1, _, true, _) => (Some(LevEditType::Delete), 1),
} else if i > 0 (_, _, _, true) => (Some(LevEditType::Replace), 0),
&& j > 0 (0, true, _, _) => (Some(LevEditType::Insert), -1),
&& current_value == cache_matrix[p - len2 - 1] (0, _, true, _) => (Some(LevEditType::Delete), 1),
&& string1[i - 1] == string2[j - 1] _ => panic!("something went terribly wrong"),
{
op_type = LevEditType::Keep;
} else if i > 0 && j > 0 && current_value == cache_matrix[p - len2 - 1] + 1 {
op_type = LevEditType::Replace;
}
/* we can't turn directly from -1 to 1, in this case it would be better
* to go diagonally, but check it (dir == 0) */
else if dir == 0 && j > 0 && current_value == cache_matrix[p - 1] + 1 {
op_type = LevEditType::Insert;
} else if dir == 0 && i > 0 && current_value == cache_matrix[p - len2] + 1 {
op_type = LevEditType::Delete;
} else {
panic!("something went terribly wrong");
}
match op_type {
LevEditType::Insert => {
j -= 1;
p -= 1;
dir = -1;
}
LevEditType::Delete => {
i -= 1;
p -= len2;
dir = 1;
}
LevEditType::Replace => {
i -= 1;
j -= 1;
p -= len2 + 1;
dir = 0;
}
LevEditType::Keep => {
i -= 1;
j -= 1;
p -= len2 + 1;
dir = 0;
/* LevEditKeep does not has to be stored */
continue;
}
}; };
let edit_op = match new_dir {
LevEditOp { op_type, first_start: i + prefix_len, second_start: j + prefix_len }; -1 => {
ops.insert(0, edit_op); j -= 1;
p -= 1;
}
1 => {
i -= 1;
p -= len2;
}
0 => {
i -= 1;
j -= 1;
p -= len2 + 1;
}
_ => panic!("something went terribly wrong"),
};
dir = new_dir;
if let Some(op_type) = op_type {
ops.insert(0, LevEditOp {
op_type,
first_start: i + prefix_len,
second_start: j + prefix_len,
});
}
} }
ops ops
@@ -186,73 +143,20 @@ where
pub struct Affix { pub struct Affix {
pub prefix_len: usize, pub prefix_len: usize,
pub first_string_len: usize, pub suffix_len: usize,
pub second_string_len: usize,
} }
impl Affix { impl Affix {
pub fn find<T>(first_string: &[T], second_string: &[T]) -> Affix pub fn find<T>(s1: &[T], s2: &[T]) -> Affix
where T: PartialEq { where T: PartialEq {
// remove common prefix and suffix (linear vs square runtime for levensthein) let prefix_len = s1.iter().zip(s2.iter()).take_while(|t| t.0 == t.1).count();
let mut first_iter = first_string.iter(); let suffix_len = s1[prefix_len..]
let mut second_iter = second_string.iter(); .iter()
.rev()
.zip(s2[prefix_len..].iter().rev())
.take_while(|t| t.0 == t.1)
.count();
let mut limit_start = 0; Affix { prefix_len, suffix_len }
let mut first_iter_char = first_iter.next();
let mut second_iter_char = second_iter.next();
while first_iter_char.is_some() && first_iter_char == second_iter_char {
first_iter_char = first_iter.next();
second_iter_char = second_iter.next();
limit_start += 1;
}
// save char since the iterator was already consumed
let first_iter_cache = first_iter_char;
let second_iter_cache = second_iter_char;
if second_iter_char.is_some() && first_iter_char.is_some() {
first_iter_char = first_iter.next_back();
second_iter_char = second_iter.next_back();
while first_iter_char.is_some() && first_iter_char == second_iter_char {
first_iter_char = first_iter.next_back();
second_iter_char = second_iter.next_back();
}
}
match (first_iter_char, second_iter_char) {
(None, None) => {
// characters might not match even though they were consumed
let remaining_char = (first_iter_cache != second_iter_cache) as usize;
Affix {
prefix_len: limit_start,
first_string_len: remaining_char,
second_string_len: remaining_char,
}
}
(None, _) => {
let remaining_char =
(first_iter_cache.is_some() && first_iter_cache != second_iter_char) as usize;
Affix {
prefix_len: limit_start,
first_string_len: remaining_char,
second_string_len: second_iter.count() + 1 + remaining_char,
}
}
(_, None) => {
let remaining_char =
(second_iter_cache.is_some() && second_iter_cache != first_iter_char) as usize;
Affix {
prefix_len: limit_start,
first_string_len: first_iter.count() + 1 + remaining_char,
second_string_len: remaining_char,
}
}
_ => Affix {
prefix_len: limit_start,
first_string_len: first_iter.count() + 2,
second_string_len: second_iter.count() + 2,
},
}
} }
} }

View File

@@ -1,44 +0,0 @@
use std::sync::{mpsc::Receiver, Arc, RwLock};
use anyhow::{Error, Result};
use crate::{
app::{AppConfig, DiffConfig},
diff::diff_objs,
jobs::{queue_job, update_status, Job, JobResult, JobState, Status},
obj::{elf, ObjInfo},
};
pub struct BinDiffResult {
pub first_obj: ObjInfo,
pub second_obj: ObjInfo,
}
fn run_build(
status: &Status,
cancel: Receiver<()>,
config: Arc<RwLock<AppConfig>>,
) -> Result<Box<BinDiffResult>> {
let config = config.read().map_err(|_| Error::msg("Failed to lock app config"))?.clone();
let target_path =
config.left_obj.as_ref().ok_or_else(|| Error::msg("Missing target obj path"))?;
let base_path = config.right_obj.as_ref().ok_or_else(|| Error::msg("Missing base obj path"))?;
update_status(status, "Loading target obj".to_string(), 0, 3, &cancel)?;
let mut left_obj = elf::read(target_path)?;
update_status(status, "Loading base obj".to_string(), 1, 3, &cancel)?;
let mut right_obj = elf::read(base_path)?;
update_status(status, "Performing diff".to_string(), 2, 3, &cancel)?;
diff_objs(&mut left_obj, &mut right_obj, &DiffConfig::default() /* TODO */)?;
update_status(status, "Complete".to_string(), 3, 3, &cancel)?;
Ok(Box::new(BinDiffResult { first_obj: left_obj, second_obj: right_obj }))
}
pub fn queue_bindiff(config: Arc<RwLock<AppConfig>>) -> JobState {
queue_job("Binary diff", Job::BinDiff, move |status, cancel| {
run_build(status, cancel, config).map(JobResult::BinDiff)
})
}

View File

@@ -4,7 +4,7 @@ use anyhow::{Context, Result};
use self_update::{cargo_crate_version, update::Release}; use self_update::{cargo_crate_version, update::Release};
use crate::{ use crate::{
jobs::{queue_job, update_status, Job, JobResult, JobState, Status}, jobs::{start_job, update_status, Job, JobResult, JobState, JobStatusRef},
update::{build_updater, BIN_NAME}, update::{build_updater, BIN_NAME},
}; };
@@ -14,7 +14,7 @@ pub struct CheckUpdateResult {
pub found_binary: bool, pub found_binary: bool,
} }
fn run_check_update(status: &Status, cancel: Receiver<()>) -> Result<Box<CheckUpdateResult>> { fn run_check_update(status: &JobStatusRef, cancel: Receiver<()>) -> Result<Box<CheckUpdateResult>> {
update_status(status, "Fetching latest release".to_string(), 0, 1, &cancel)?; update_status(status, "Fetching latest release".to_string(), 0, 1, &cancel)?;
let updater = build_updater().context("Failed to create release updater")?; let updater = build_updater().context("Failed to create release updater")?;
let latest_release = updater.get_latest_release()?; let latest_release = updater.get_latest_release()?;
@@ -26,8 +26,8 @@ fn run_check_update(status: &Status, cancel: Receiver<()>) -> Result<Box<CheckUp
Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary })) Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary }))
} }
pub fn queue_check_update() -> JobState { pub fn start_check_update() -> JobState {
queue_job("Check for updates", Job::CheckUpdate, move |status, cancel| { start_job("Check for updates", Job::CheckUpdate, move |status, cancel| {
run_check_update(status, cancel).map(JobResult::CheckUpdate) run_check_update(status, cancel).map(|result| JobResult::CheckUpdate(Some(result)))
}) })
} }

View File

@@ -9,12 +9,8 @@ use std::{
use anyhow::Result; use anyhow::Result;
use crate::jobs::{ use crate::jobs::{check_update::CheckUpdateResult, objdiff::ObjDiffResult, update::UpdateResult};
bindiff::BinDiffResult, check_update::CheckUpdateResult, objdiff::ObjDiffResult,
update::UpdateResult,
};
pub mod bindiff;
pub mod check_update; pub mod check_update;
pub mod objdiff; pub mod objdiff;
pub mod update; pub mod update;
@@ -22,19 +18,88 @@ pub mod update;
#[derive(Debug, Eq, PartialEq, Copy, Clone)] #[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum Job { pub enum Job {
ObjDiff, ObjDiff,
BinDiff,
CheckUpdate, CheckUpdate,
Update, Update,
} }
pub static JOB_ID: AtomicUsize = AtomicUsize::new(0); pub static JOB_ID: AtomicUsize = AtomicUsize::new(0);
#[derive(Default)]
pub struct JobQueue {
pub jobs: Vec<JobState>,
pub results: Vec<JobResult>,
}
impl JobQueue {
/// Adds a job to the queue.
#[inline]
pub fn push(&mut self, state: JobState) { self.jobs.push(state); }
/// Adds a job to the queue if a job of the given kind is not already running.
#[inline]
pub fn push_once(&mut self, job: Job, func: impl FnOnce() -> JobState) {
if !self.is_running(job) {
self.push(func());
}
}
/// Returns whether a job of the given kind is running.
pub fn is_running(&self, kind: Job) -> bool {
self.jobs.iter().any(|j| j.kind == kind && j.handle.is_some())
}
/// Returns whether any job is running.
pub fn any_running(&self) -> bool {
self.jobs.iter().any(|job| {
if let Some(handle) = &job.handle {
return !handle.is_finished();
}
false
})
}
/// Iterates over all jobs mutably.
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut JobState> + '_ { self.jobs.iter_mut() }
/// Iterates over all finished jobs, returning the job state and the result.
pub fn iter_finished(
&mut self,
) -> impl Iterator<Item = (&mut JobState, std::thread::Result<JobResult>)> + '_ {
self.jobs.iter_mut().filter_map(|job| {
if let Some(handle) = &job.handle {
if !handle.is_finished() {
return None;
}
let result = job.handle.take().unwrap().join();
return Some((job, result));
}
None
})
}
/// Clears all finished jobs.
pub fn clear_finished(&mut self) {
self.jobs.retain(|job| {
!(job.should_remove
&& job.handle.is_none()
&& job.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); }
}
pub type JobStatusRef = Arc<RwLock<JobStatus>>;
pub struct JobState { pub struct JobState {
pub id: usize, pub id: usize,
pub job_type: Job, pub kind: Job,
pub handle: Option<JoinHandle<JobResult>>, pub handle: Option<JoinHandle<JobResult>>,
pub status: Arc<RwLock<JobStatus>>, pub status: JobStatusRef,
pub cancel: Sender<()>, pub cancel: Sender<()>,
pub should_remove: bool, pub should_remove: bool,
} }
#[derive(Default)] #[derive(Default)]
pub struct JobStatus { pub struct JobStatus {
pub title: String, pub title: String,
@@ -43,11 +108,11 @@ pub struct JobStatus {
pub status: String, pub status: String,
pub error: Option<anyhow::Error>, pub error: Option<anyhow::Error>,
} }
pub enum JobResult { pub enum JobResult {
None, None,
ObjDiff(Box<ObjDiffResult>), ObjDiff(Option<Box<ObjDiffResult>>),
BinDiff(Box<BinDiffResult>), CheckUpdate(Option<Box<CheckUpdateResult>>),
CheckUpdate(Box<CheckUpdateResult>),
Update(Box<UpdateResult>), Update(Box<UpdateResult>),
} }
@@ -58,12 +123,10 @@ fn should_cancel(rx: &Receiver<()>) -> bool {
} }
} }
type Status = Arc<RwLock<JobStatus>>; fn start_job(
fn queue_job(
title: &str, title: &str,
job_type: Job, kind: Job,
run: impl FnOnce(&Status, Receiver<()>) -> Result<JobResult> + Send + 'static, run: impl FnOnce(&JobStatusRef, Receiver<()>) -> Result<JobResult> + Send + 'static,
) -> JobState { ) -> JobState {
let status = Arc::new(RwLock::new(JobStatus { let status = Arc::new(RwLock::new(JobStatus {
title: title.to_string(), title: title.to_string(),
@@ -89,7 +152,7 @@ fn queue_job(
log::info!("Started job {}", id); log::info!("Started job {}", id);
JobState { JobState {
id, id,
job_type, kind,
handle: Some(handle), handle: Some(handle),
status: status_clone, status: status_clone,
cancel: tx, cancel: tx,
@@ -98,7 +161,7 @@ fn queue_job(
} }
fn update_status( fn update_status(
status: &Status, status: &JobStatusRef,
str: String, str: String,
count: u32, count: u32,
total: u32, total: u32,

View File

@@ -1,17 +1,12 @@
use std::{ use std::{path::Path, process::Command, str::from_utf8, sync::mpsc::Receiver};
path::Path,
process::Command,
str::from_utf8,
sync::{mpsc::Receiver, Arc, RwLock},
};
use anyhow::{Context, Error, Result}; use anyhow::{anyhow, Context, Error, Result};
use time::OffsetDateTime; use time::OffsetDateTime;
use crate::{ use crate::{
app::{AppConfig, DiffConfig}, app::{AppConfig, AppConfigRef},
diff::diff_objs, diff::diff_objs,
jobs::{queue_job, update_status, Job, JobResult, JobState, Status}, jobs::{start_job, update_status, Job, JobResult, JobState, JobStatusRef},
obj::{elf, ObjInfo}, obj::{elf, ObjInfo},
}; };
@@ -76,68 +71,116 @@ fn run_make(cwd: &Path, arg: &Path, config: &AppConfig) -> BuildStatus {
} }
fn run_build( fn run_build(
status: &Status, status: &JobStatusRef,
cancel: Receiver<()>, cancel: Receiver<()>,
config: Arc<RwLock<AppConfig>>, config: AppConfigRef,
diff_config: DiffConfig,
) -> Result<Box<ObjDiffResult>> { ) -> Result<Box<ObjDiffResult>> {
let config = config.read().map_err(|_| Error::msg("Failed to lock app config"))?.clone(); let config = config.read().map_err(|_| Error::msg("Failed to lock app config"))?.clone();
let obj_path = config.obj_path.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?; let obj_config = config.selected_obj.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?;
let project_dir = let project_dir =
config.project_dir.as_ref().ok_or_else(|| Error::msg("Missing project dir"))?; config.project_dir.as_ref().ok_or_else(|| Error::msg("Missing project dir"))?;
let mut target_path = config let target_path_rel = if let Some(target_path) = &obj_config.target_path {
.target_obj_dir Some(target_path.strip_prefix(project_dir).map_err(|_| {
.as_ref() anyhow!(
.ok_or_else(|| Error::msg("Missing target obj dir"))? "Target path '{}' doesn't begin with '{}'",
.to_owned(); target_path.display(),
target_path.push(obj_path); project_dir.display()
let mut base_path = )
config.base_obj_dir.as_ref().ok_or_else(|| Error::msg("Missing base obj dir"))?.to_owned(); })?)
base_path.push(obj_path); } else {
let target_path_rel = target_path None
.strip_prefix(project_dir) };
.context("Failed to create relative target obj path")?; let base_path_rel = if let Some(base_path) = &obj_config.base_path {
let base_path_rel = Some(base_path.strip_prefix(project_dir).map_err(|_| {
base_path.strip_prefix(project_dir).context("Failed to create relative base obj path")?; anyhow!(
"Base path '{}' doesn't begin with '{}'",
base_path.display(),
project_dir.display()
)
})?)
} else {
None
};
let total = if config.build_target { 5 } else { 4 }; let mut total = 3;
let first_status = if config.build_target { if config.build_target && target_path_rel.is_some() {
update_status(status, format!("Building target {obj_path}"), 0, total, &cancel)?; total += 1;
run_make(project_dir, target_path_rel, &config) }
if base_path_rel.is_some() {
total += 1;
}
let first_status = match target_path_rel {
Some(target_path_rel) if config.build_target => {
update_status(
status,
format!("Building target {}", target_path_rel.display()),
0,
total,
&cancel,
)?;
run_make(project_dir, target_path_rel, &config)
}
_ => BuildStatus { success: true, log: String::new() },
};
let second_status = if let Some(base_path_rel) = base_path_rel {
update_status(
status,
format!("Building base {}", base_path_rel.display()),
1,
total,
&cancel,
)?;
run_make(project_dir, base_path_rel, &config)
} else { } else {
BuildStatus { success: true, log: String::new() } BuildStatus { success: true, log: String::new() }
}; };
update_status(status, format!("Building base {obj_path}"), 1, total, &cancel)?;
let second_status = run_make(project_dir, base_path_rel, &config);
let time = OffsetDateTime::now_utc(); let time = OffsetDateTime::now_utc();
let mut first_obj = if first_status.success { let mut first_obj =
update_status(status, format!("Loading target {obj_path}"), 2, total, &cancel)?; match &obj_config.target_path {
Some(elf::read(&target_path)?) Some(target_path) if first_status.success => {
} else { update_status(
None status,
format!("Loading target {}", target_path_rel.unwrap().display()),
2,
total,
&cancel,
)?;
Some(elf::read(target_path).with_context(|| {
format!("Failed to read object '{}'", target_path.display())
})?)
}
_ => None,
};
let mut second_obj = match &obj_config.base_path {
Some(base_path) if second_status.success => {
update_status(
status,
format!("Loading base {}", base_path_rel.unwrap().display()),
3,
total,
&cancel,
)?;
Some(
elf::read(base_path)
.with_context(|| format!("Failed to read object '{}'", base_path.display()))?,
)
}
_ => None,
}; };
let mut second_obj = if second_status.success { update_status(status, "Performing diff".to_string(), 4, total, &cancel)?;
update_status(status, format!("Loading base {obj_path}"), 3, total, &cancel)?; diff_objs(first_obj.as_mut(), second_obj.as_mut())?;
Some(elf::read(&base_path)?)
} else {
None
};
if let (Some(first_obj), Some(second_obj)) = (&mut first_obj, &mut second_obj) {
update_status(status, "Performing diff".to_string(), 4, total, &cancel)?;
diff_objs(first_obj, second_obj, &diff_config)?;
}
update_status(status, "Complete".to_string(), total, total, &cancel)?; update_status(status, "Complete".to_string(), total, total, &cancel)?;
Ok(Box::new(ObjDiffResult { first_status, second_status, first_obj, second_obj, time })) Ok(Box::new(ObjDiffResult { first_status, second_status, first_obj, second_obj, time }))
} }
pub fn queue_build(config: Arc<RwLock<AppConfig>>, diff_config: DiffConfig) -> JobState { pub fn start_build(config: AppConfigRef) -> JobState {
queue_job("Object diff", Job::ObjDiff, move |status, cancel| { start_job("Object diff", Job::ObjDiff, move |status, cancel| {
run_build(status, cancel, config, diff_config).map(JobResult::ObjDiff) run_build(status, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
}) })
} }

View File

@@ -1,6 +1,5 @@
use std::{ use std::{
env::{current_dir, current_exe}, env::{current_dir, current_exe},
fs,
fs::File, fs::File,
path::PathBuf, path::PathBuf,
sync::mpsc::Receiver, sync::mpsc::Receiver,
@@ -10,7 +9,7 @@ use anyhow::{Context, Result};
use const_format::formatcp; use const_format::formatcp;
use crate::{ use crate::{
jobs::{queue_job, update_status, Job, JobResult, JobState, Status}, jobs::{start_job, update_status, Job, JobResult, JobState, JobStatusRef},
update::{build_updater, BIN_NAME}, update::{build_updater, BIN_NAME},
}; };
@@ -18,7 +17,7 @@ pub struct UpdateResult {
pub exe_path: PathBuf, pub exe_path: PathBuf,
} }
fn run_update(status: &Status, cancel: Receiver<()>) -> Result<Box<UpdateResult>> { fn run_update(status: &JobStatusRef, cancel: Receiver<()>) -> Result<Box<UpdateResult>> {
update_status(status, "Fetching latest release".to_string(), 0, 3, &cancel)?; update_status(status, "Fetching latest release".to_string(), 0, 3, &cancel)?;
let updater = build_updater().context("Failed to create release updater")?; let updater = build_updater().context("Failed to create release updater")?;
let latest_release = updater.get_latest_release()?; let latest_release = updater.get_latest_release()?;
@@ -44,7 +43,7 @@ fn run_update(status: &Status, cancel: Receiver<()>) -> Result<Box<UpdateResult>
.to_dest(&target_file)?; .to_dest(&target_file)?;
#[cfg(unix)] #[cfg(unix)]
{ {
use std::os::unix::fs::PermissionsExt; use std::{fs, os::unix::fs::PermissionsExt};
let mut perms = fs::metadata(&target_file)?.permissions(); let mut perms = fs::metadata(&target_file)?.permissions();
perms.set_mode(0o755); perms.set_mode(0o755);
fs::set_permissions(&target_file, perms)?; fs::set_permissions(&target_file, perms)?;
@@ -54,8 +53,8 @@ fn run_update(status: &Status, cancel: Receiver<()>) -> Result<Box<UpdateResult>
Ok(Box::from(UpdateResult { exe_path: target_file })) Ok(Box::from(UpdateResult { exe_path: target_file }))
} }
pub fn queue_update() -> JobState { pub fn start_update() -> JobState {
queue_job("Update app", Job::Update, move |status, cancel| { start_job("Update app", Job::Update, move |status, cancel| {
run_update(status, cancel).map(JobResult::Update) run_update(status, cancel).map(JobResult::Update)
}) })
} }

View File

@@ -3,6 +3,8 @@
pub use app::App; pub use app::App;
mod app; mod app;
mod app_config;
mod config;
mod diff; mod diff;
mod editops; mod editops;
mod jobs; mod jobs;

View File

@@ -3,9 +3,27 @@
use std::{path::PathBuf, rc::Rc, sync::Mutex}; use std::{path::PathBuf, rc::Rc, sync::Mutex};
use anyhow::{Error, Result};
use cfg_if::cfg_if; use cfg_if::cfg_if;
use eframe::IconData;
use time::UtcOffset; use time::UtcOffset;
fn load_icon() -> Result<IconData> {
use bytes::Buf;
let decoder = png::Decoder::new(include_bytes!("../assets/icon_64.png").reader());
let mut reader = decoder.read_info()?;
let mut buf = vec![0; reader.output_buffer_size()];
let info = reader.next_frame(&mut buf)?;
if info.bit_depth != png::BitDepth::Eight {
return Err(Error::msg("Invalid bit depth"));
}
if info.color_type != png::ColorType::Rgba {
return Err(Error::msg("Invalid color type"));
}
buf.truncate(info.buffer_size());
Ok(IconData { rgba: buf, width: info.width, height: info.height })
}
// When compiling natively: // When compiling natively:
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
fn main() { fn main() {
@@ -19,13 +37,26 @@ fn main() {
let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None)); let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None));
let exec_path_clone = exec_path.clone(); let exec_path_clone = exec_path.clone();
let native_options = eframe::NativeOptions::default(); let mut native_options =
// native_options.renderer = eframe::Renderer::Wgpu; eframe::NativeOptions { follow_system_theme: false, ..Default::default() };
match load_icon() {
Ok(data) => {
native_options.icon_data = Some(data);
}
Err(e) => {
log::warn!("Failed to load application icon: {}", e);
}
}
#[cfg(feature = "wgpu")]
{
native_options.renderer = eframe::Renderer::Wgpu;
}
eframe::run_native( eframe::run_native(
"objdiff", "objdiff",
native_options, native_options,
Box::new(move |cc| Box::new(objdiff::App::new(cc, utc_offset, exec_path_clone))), Box::new(move |cc| Box::new(objdiff::App::new(cc, utc_offset, exec_path_clone))),
); )
.expect("Failed to run eframe application");
// Attempt to relaunch application from the updated path // Attempt to relaunch application from the updated path
if let Ok(mut guard) = exec_path.lock() { if let Ok(mut guard) = exec_path.lock() {
@@ -35,7 +66,7 @@ fn main() {
let result = exec::Command::new(path) let result = exec::Command::new(path)
.args(&std::env::args().collect::<Vec<String>>()) .args(&std::env::args().collect::<Vec<String>>())
.exec(); .exec();
eprintln!("Failed to relaunch: {result:?}"); log::error!("Failed to relaunch: {result:?}");
} else { } else {
let result = std::process::Command::new(path) let result = std::process::Command::new(path)
.args(std::env::args()) .args(std::env::args())
@@ -43,7 +74,7 @@ fn main() {
.unwrap() .unwrap()
.wait(); .wait();
if let Err(e) = result { if let Err(e) = result {
eprintln!("Failed to relaunch: {:?}", e); log::error!("Failed to relaunch: {:?}", e);
} }
} }
} }

View File

@@ -1,15 +1,12 @@
use std::{fs, path::Path}; use std::{collections::BTreeMap, fs, io::Cursor, path::Path};
use anyhow::{Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use byteorder::{BigEndian, ReadBytesExt};
use cwdemangle::demangle; use cwdemangle::demangle;
use flagset::Flags; use flagset::Flags;
use object::{ use object::{
elf::{ elf, Architecture, File, Object, ObjectSection, ObjectSymbol, RelocationKind, RelocationTarget,
R_MIPS_26, R_MIPS_HI16, R_MIPS_LO16, R_PPC_ADDR16_HA, R_PPC_ADDR16_HI, R_PPC_ADDR16_LO, SectionIndex, SectionKind, Symbol, SymbolKind, SymbolSection,
R_PPC_EMB_SDA21, R_PPC_REL14, R_PPC_REL24,
},
Architecture, File, Object, ObjectSection, ObjectSymbol, RelocationKind, RelocationTarget,
SectionKind, Symbol, SymbolKind, SymbolSection,
}; };
use crate::obj::{ use crate::obj::{
@@ -17,19 +14,19 @@ use crate::obj::{
ObjSymbolFlagSet, ObjSymbolFlags, ObjSymbolFlagSet, ObjSymbolFlags,
}; };
fn to_obj_section_kind(kind: SectionKind) -> ObjSectionKind { fn to_obj_section_kind(kind: SectionKind) -> Option<ObjSectionKind> {
match kind { match kind {
SectionKind::Text => ObjSectionKind::Code, SectionKind::Text => Some(ObjSectionKind::Code),
SectionKind::Data | SectionKind::ReadOnlyData => ObjSectionKind::Data, SectionKind::Data | SectionKind::ReadOnlyData => Some(ObjSectionKind::Data),
SectionKind::UninitializedData => ObjSectionKind::Bss, SectionKind::UninitializedData => Some(ObjSectionKind::Bss),
_ => panic!("Unhandled section kind {kind:?}"), _ => None,
} }
} }
fn to_obj_symbol(obj_file: &File<'_>, symbol: &Symbol<'_, '_>, addend: i64) -> Result<ObjSymbol> { fn to_obj_symbol(obj_file: &File<'_>, symbol: &Symbol<'_, '_>, addend: i64) -> Result<ObjSymbol> {
let mut name = symbol.name().context("Failed to process symbol name")?; let mut name = symbol.name().context("Failed to process symbol name")?;
if name.is_empty() { if name.is_empty() {
println!("Found empty sym: {symbol:?}"); log::warn!("Found empty sym: {symbol:?}");
name = "?"; name = "?";
} }
let mut flags = ObjSymbolFlagSet(ObjSymbolFlags::none()); let mut flags = ObjSymbolFlagSet(ObjSymbolFlags::none());
@@ -73,18 +70,14 @@ fn filter_sections(obj_file: &File<'_>) -> Result<Vec<ObjSection>> {
if section.size() == 0 { if section.size() == 0 {
continue; continue;
} }
if section.kind() != SectionKind::Text let Some(kind) = to_obj_section_kind(section.kind()) else {
&& section.kind() != SectionKind::Data
&& section.kind() != SectionKind::ReadOnlyData
&& section.kind() != SectionKind::UninitializedData
{
continue; continue;
} };
let name = section.name().context("Failed to process section name")?; let name = section.name().context("Failed to process section name")?;
let data = section.uncompressed_data().context("Failed to read section data")?; let data = section.uncompressed_data().context("Failed to read section data")?;
result.push(ObjSection { result.push(ObjSection {
name: name.to_string(), name: name.to_string(),
kind: to_obj_section_kind(section.kind()), kind,
address: section.address(), address: section.address(),
size: section.size(), size: section.size(),
data: data.to_vec(), data: data.to_vec(),
@@ -133,13 +126,11 @@ fn symbols_by_section(obj_file: &File<'_>, section: &ObjSection) -> Result<Vec<O
} }
fn common_symbols(obj_file: &File<'_>) -> Result<Vec<ObjSymbol>> { fn common_symbols(obj_file: &File<'_>) -> Result<Vec<ObjSymbol>> {
let mut result = Vec::<ObjSymbol>::new(); obj_file
for symbol in obj_file.symbols() { .symbols()
if symbol.is_common() { .filter(Symbol::is_common)
result.push(to_obj_symbol(obj_file, &symbol, 0)?); .map(|symbol| to_obj_symbol(obj_file, &symbol, 0))
} .collect::<Result<Vec<ObjSymbol>>>()
}
Ok(result)
} }
fn find_section_symbol( fn find_section_symbol(
@@ -190,11 +181,9 @@ fn find_section_symbol(
fn relocations_by_section( fn relocations_by_section(
arch: ObjArchitecture, arch: ObjArchitecture,
obj_file: &File<'_>, obj_file: &File<'_>,
section: &mut ObjSection, section: &ObjSection,
) -> Result<Vec<ObjReloc>> { ) -> Result<Vec<ObjReloc>> {
let obj_section = obj_file let obj_section = obj_file.section_by_index(SectionIndex(section.index))?;
.section_by_name(&section.name)
.ok_or_else(|| anyhow::Error::msg("Failed to locate section"))?;
let mut relocations = Vec::<ObjReloc>::new(); let mut relocations = Vec::<ObjReloc>::new();
for (address, reloc) in obj_section.relocations() { for (address, reloc) in obj_section.relocations() {
let symbol = match reloc.target() { let symbol = match reloc.target() {
@@ -212,12 +201,12 @@ fn relocations_by_section(
RelocationKind::Absolute => ObjRelocKind::Absolute, RelocationKind::Absolute => ObjRelocKind::Absolute,
RelocationKind::Elf(kind) => match arch { RelocationKind::Elf(kind) => match arch {
ObjArchitecture::PowerPc => match kind { ObjArchitecture::PowerPc => match kind {
R_PPC_ADDR16_LO => ObjRelocKind::PpcAddr16Lo, elf::R_PPC_ADDR16_LO => ObjRelocKind::PpcAddr16Lo,
R_PPC_ADDR16_HI => ObjRelocKind::PpcAddr16Hi, elf::R_PPC_ADDR16_HI => ObjRelocKind::PpcAddr16Hi,
R_PPC_ADDR16_HA => ObjRelocKind::PpcAddr16Ha, elf::R_PPC_ADDR16_HA => ObjRelocKind::PpcAddr16Ha,
R_PPC_REL24 => ObjRelocKind::PpcRel24, elf::R_PPC_REL24 => ObjRelocKind::PpcRel24,
R_PPC_REL14 => ObjRelocKind::PpcRel14, elf::R_PPC_REL14 => ObjRelocKind::PpcRel14,
R_PPC_EMB_SDA21 => ObjRelocKind::PpcEmbSda21, elf::R_PPC_EMB_SDA21 => ObjRelocKind::PpcEmbSda21,
_ => { _ => {
return Err(anyhow::Error::msg(format!( return Err(anyhow::Error::msg(format!(
"Unhandled PPC relocation type: {kind}" "Unhandled PPC relocation type: {kind}"
@@ -225,14 +214,14 @@ fn relocations_by_section(
} }
}, },
ObjArchitecture::Mips => match kind { ObjArchitecture::Mips => match kind {
R_MIPS_26 => ObjRelocKind::Mips26, elf::R_MIPS_26 => ObjRelocKind::Mips26,
R_MIPS_HI16 => ObjRelocKind::MipsHi16, elf::R_MIPS_HI16 => ObjRelocKind::MipsHi16,
R_MIPS_LO16 => ObjRelocKind::MipsLo16, elf::R_MIPS_LO16 => ObjRelocKind::MipsLo16,
_ => { elf::R_MIPS_GOT16 => ObjRelocKind::MipsGot16,
return Err(anyhow::Error::msg(format!( elf::R_MIPS_CALL16 => ObjRelocKind::MipsCall16,
"Unhandled MIPS relocation type: {kind}" elf::R_MIPS_GPREL16 => ObjRelocKind::MipsGpRel16,
))) elf::R_MIPS_GPREL32 => ObjRelocKind::MipsGpRel32,
} _ => bail!("Unhandled MIPS relocation type: {kind}"),
}, },
}, },
_ => { _ => {
@@ -249,43 +238,68 @@ fn relocations_by_section(
} }
_ => None, _ => None,
}; };
// println!("Reloc: {:?}, symbol: {:?}", reloc, symbol); let addend = if reloc.has_implicit_addend() {
let addend = u32::from_be_bytes(
section.data[address as usize..address as usize + 4].try_into()?,
);
match kind {
ObjRelocKind::Absolute => addend as i64,
ObjRelocKind::MipsHi16 => ((addend & 0x0000FFFF) << 16) as i32 as i64,
ObjRelocKind::MipsLo16
| ObjRelocKind::MipsGot16
| ObjRelocKind::MipsCall16
| ObjRelocKind::MipsGpRel16 => (addend & 0x0000FFFF) as i16 as i64,
ObjRelocKind::MipsGpRel32 => addend as i32 as i64,
ObjRelocKind::Mips26 => ((addend & 0x03FFFFFF) << 2) as i64,
_ => bail!("Unsupported implicit relocation {kind:?}"),
}
} else {
reloc.addend()
};
// println!("Reloc: {reloc:?}, symbol: {symbol:?}, addend: {addend:#X}");
let target = match symbol.kind() { let target = match symbol.kind() {
SymbolKind::Text | SymbolKind::Data | SymbolKind::Unknown => { SymbolKind::Text | SymbolKind::Data | SymbolKind::Label | SymbolKind::Unknown => {
to_obj_symbol(obj_file, &symbol, reloc.addend()) to_obj_symbol(obj_file, &symbol, addend)
} }
SymbolKind::Section => { SymbolKind::Section => {
let addend = if reloc.has_implicit_addend() { if addend < 0 {
let addend = u32::from_be_bytes( return Err(anyhow::Error::msg(format!("Negative addend in reloc: {addend}")));
section.data[address as usize..address as usize + 4].try_into()?, }
);
match kind {
ObjRelocKind::Absolute => addend,
ObjRelocKind::MipsHi16 | ObjRelocKind::MipsLo16 => addend & 0x0000FFFF,
ObjRelocKind::Mips26 => (addend & 0x03FFFFFF) * 4,
_ => todo!(),
}
} else {
let addend = reloc.addend();
if addend < 0 {
return Err(anyhow::Error::msg(format!(
"Negative addend in section reloc: {addend}"
)));
}
addend as u32
};
find_section_symbol(obj_file, &symbol, addend as u64) find_section_symbol(obj_file, &symbol, addend as u64)
} }
_ => Err(anyhow::Error::msg(format!( kind => Err(anyhow!("Unhandled relocation symbol type {kind:?}")),
"Unhandled relocation symbol type {:?}",
symbol.kind()
))),
}?; }?;
relocations.push(ObjReloc { kind, address, target, target_section }); relocations.push(ObjReloc { kind, address, target, target_section });
} }
Ok(relocations) Ok(relocations)
} }
fn line_info(obj_file: &File<'_>) -> Result<Option<BTreeMap<u32, u32>>> {
if let Some(section) = obj_file.section_by_name(".line") {
if section.size() == 0 {
return Ok(None);
}
let data = section.uncompressed_data()?;
let mut reader = Cursor::new(data.as_ref());
let mut map = BTreeMap::new();
let size = reader.read_u32::<BigEndian>()?;
let base_address = reader.read_u32::<BigEndian>()?;
while reader.position() < size as u64 {
let line_number = reader.read_u32::<BigEndian>()?;
let statement_pos = reader.read_u16::<BigEndian>()?;
if statement_pos != 0xFFFF {
log::warn!("Unhandled statement pos {}", statement_pos);
}
let address_delta = reader.read_u32::<BigEndian>()?;
map.insert(base_address + address_delta, line_number);
}
log::debug!("Line info: {map:#X?}");
return Ok(Some(map));
}
Ok(None)
}
pub fn read(obj_path: &Path) -> Result<ObjInfo> { pub fn read(obj_path: &Path) -> Result<ObjInfo> {
let data = { let data = {
let file = fs::File::open(obj_path)?; let file = fs::File::open(obj_path)?;
@@ -307,6 +321,7 @@ pub fn read(obj_path: &Path) -> Result<ObjInfo> {
path: obj_path.to_owned(), path: obj_path.to_owned(),
sections: filter_sections(&obj_file)?, sections: filter_sections(&obj_file)?,
common: common_symbols(&obj_file)?, common: common_symbols(&obj_file)?,
line_info: line_info(&obj_file)?,
}; };
for section in &mut result.sections { for section in &mut result.sections {
section.symbols = symbols_by_section(&obj_file, section)?; section.symbols = symbols_by_section(&obj_file, section)?;

View File

@@ -1,15 +1,24 @@
use std::collections::BTreeMap;
use anyhow::Result; use anyhow::Result;
use rabbitizer::{config_set_register_fpr_abi_names, Abi, Instruction, SimpleOperandType}; use rabbitizer::{config, Abi, InstrCategory, Instruction, OperandType};
use crate::obj::{ObjIns, ObjInsArg, ObjReloc}; use crate::obj::{ObjIns, ObjInsArg, ObjReloc};
fn configure_rabbitizer() {
unsafe {
config::RabbitizerConfig_Cfg.reg_names.fpr_abi_names = Abi::O32;
}
}
pub fn process_code( pub fn process_code(
data: &[u8], data: &[u8],
start_address: u64, start_address: u64,
end_address: u64, end_address: u64,
relocs: &[ObjReloc], relocs: &[ObjReloc],
line_info: &Option<BTreeMap<u32, u32>>,
) -> Result<(Vec<u8>, Vec<ObjIns>)> { ) -> Result<(Vec<u8>, Vec<ObjIns>)> {
config_set_register_fpr_abi_names(Abi::RABBITIZER_ABI_O32); configure_rabbitizer();
let ins_count = data.len() / 4; let ins_count = data.len() / 4;
let mut ops = Vec::<u8>::with_capacity(ins_count); let mut ops = Vec::<u8>::with_capacity(ins_count);
@@ -18,47 +27,61 @@ pub fn process_code(
for chunk in data.chunks_exact(4) { for chunk in data.chunks_exact(4) {
let reloc = relocs.iter().find(|r| (r.address as u32 & !3) == cur_addr); let reloc = relocs.iter().find(|r| (r.address as u32 & !3) == cur_addr);
let code = u32::from_be_bytes(chunk.try_into()?); let code = u32::from_be_bytes(chunk.try_into()?);
let mut instruction = Instruction::new(code, cur_addr); let instruction = Instruction::new(code, cur_addr, InstrCategory::CPU);
let op = instruction.instr_id() as u8; let op = instruction.unique_id as u8;
ops.push(op); ops.push(op);
let mnemonic = instruction.instr_id().get_opcode_name().unwrap_or_default().to_string(); let mnemonic = instruction.opcode_name().to_string();
let is_branch = instruction.is_branch(); let is_branch = instruction.is_branch();
let branch_offset = instruction.branch_offset(); let branch_offset = instruction.branch_offset();
let branch_dest = let branch_dest =
if is_branch { Some((cur_addr as i32 + branch_offset) as u32) } else { None }; if is_branch { Some((cur_addr as i32 + branch_offset) as u32) } else { None };
let args = instruction
.simple_operands() let operands = instruction.get_operands_slice();
.iter() let mut args = Vec::with_capacity(operands.len() + 1);
.map(|op| match op.kind { for op in operands {
SimpleOperandType::Imm | SimpleOperandType::Label => { match op {
OperandType::cpu_immediate
| OperandType::cpu_label
| OperandType::cpu_branch_target_label => {
if is_branch { if is_branch {
ObjInsArg::BranchOffset(branch_offset) args.push(ObjInsArg::BranchOffset(branch_offset));
} else if let Some(reloc) = reloc { } else if let Some(reloc) = reloc {
if matches!(&reloc.target_section, Some(s) if s == ".text") if matches!(&reloc.target_section, Some(s) if s == ".text")
&& reloc.target.address > start_address && reloc.target.address > start_address
&& reloc.target.address < end_address && reloc.target.address < end_address
{ {
// Inter-function reloc, convert to branch offset // Inter-function reloc, convert to branch offset
ObjInsArg::BranchOffset(reloc.target.address as i32 - cur_addr as i32) args.push(ObjInsArg::BranchOffset(
reloc.target.address as i32 - cur_addr as i32,
));
} else { } else {
ObjInsArg::Reloc args.push(ObjInsArg::Reloc);
} }
} else { } else {
ObjInsArg::MipsArg(op.disassembled.clone()) args.push(ObjInsArg::MipsArg(op.disassemble(&instruction, None)));
} }
} }
SimpleOperandType::ImmBase => { OperandType::cpu_immediate_base => {
if reloc.is_some() { if reloc.is_some() {
ObjInsArg::RelocWithBase args.push(ObjInsArg::RelocWithBase);
} else { } else {
ObjInsArg::MipsArg(op.disassembled.clone()) args.push(ObjInsArg::MipsArgWithBase(
OperandType::cpu_immediate.disassemble(&instruction, None),
));
} }
args.push(ObjInsArg::MipsArg(
OperandType::cpu_rs.disassemble(&instruction, None),
));
} }
_ => ObjInsArg::MipsArg(op.disassembled.clone()), _ => {
}) args.push(ObjInsArg::MipsArg(op.disassemble(&instruction, None)));
.collect(); }
}
}
let line =
line_info.as_ref().and_then(|map| map.range(..=cur_addr).last().map(|(_, &b)| b));
insts.push(ObjIns { insts.push(ObjIns {
address: cur_addr, address: cur_addr,
code, code,
@@ -67,6 +90,7 @@ pub fn process_code(
args, args,
reloc: reloc.cloned(), reloc: reloc.cloned(),
branch_dest, branch_dest,
line,
}); });
cur_addr += 4; cur_addr += 4;
} }

View File

@@ -2,7 +2,7 @@ pub mod elf;
pub mod mips; pub mod mips;
pub mod ppc; pub mod ppc;
use std::path::PathBuf; use std::{collections::BTreeMap, path::PathBuf};
use flagset::{flags, FlagSet}; use flagset::{flags, FlagSet};
@@ -41,6 +41,7 @@ pub struct ObjSection {
pub enum ObjInsArg { pub enum ObjInsArg {
PpcArg(ppc750cl::Argument), PpcArg(ppc750cl::Argument),
MipsArg(String), MipsArg(String),
MipsArgWithBase(String),
Reloc, Reloc,
RelocWithBase, RelocWithBase,
BranchOffset(i32), BranchOffset(i32),
@@ -83,6 +84,8 @@ pub struct ObjIns {
pub args: Vec<ObjInsArg>, pub args: Vec<ObjInsArg>,
pub reloc: Option<ObjReloc>, pub reloc: Option<ObjReloc>,
pub branch_dest: Option<u32>, pub branch_dest: Option<u32>,
/// Line info
pub line: Option<u32>,
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ObjInsDiff { pub struct ObjInsDiff {
@@ -138,6 +141,7 @@ pub struct ObjInfo {
pub path: PathBuf, pub path: PathBuf,
pub sections: Vec<ObjSection>, pub sections: Vec<ObjSection>,
pub common: Vec<ObjSymbol>, pub common: Vec<ObjSymbol>,
pub line_info: Option<BTreeMap<u32, u32>>,
} }
#[derive(Debug, Eq, PartialEq, Copy, Clone)] #[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum ObjRelocKind { pub enum ObjRelocKind {
@@ -155,6 +159,10 @@ pub enum ObjRelocKind {
Mips26, Mips26,
MipsHi16, MipsHi16,
MipsLo16, MipsLo16,
MipsGot16,
MipsCall16,
MipsGpRel16,
MipsGpRel32,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ObjReloc { pub struct ObjReloc {

View File

@@ -1,3 +1,5 @@
use std::collections::BTreeMap;
use anyhow::Result; use anyhow::Result;
use ppc750cl::{disasm_iter, Argument}; use ppc750cl::{disasm_iter, Argument};
@@ -19,6 +21,7 @@ pub fn process_code(
data: &[u8], data: &[u8],
address: u64, address: u64,
relocs: &[ObjReloc], relocs: &[ObjReloc],
line_info: &Option<BTreeMap<u32, u32>>,
) -> Result<(Vec<u8>, Vec<ObjIns>)> { ) -> Result<(Vec<u8>, Vec<ObjIns>)> {
let ins_count = data.len() / 4; let ins_count = data.len() / 4;
let mut ops = Vec::<u8>::with_capacity(ins_count); let mut ops = Vec::<u8>::with_capacity(ins_count);
@@ -74,6 +77,9 @@ pub fn process_code(
} }
} }
ops.push(simplified.ins.op as u8); ops.push(simplified.ins.op as u8);
let line = line_info
.as_ref()
.and_then(|map| map.range(..=simplified.ins.addr).last().map(|(_, &b)| b));
insts.push(ObjIns { insts.push(ObjIns {
address: simplified.ins.addr, address: simplified.ins.addr,
code: simplified.ins.code, code: simplified.ins.code,
@@ -82,6 +88,7 @@ pub fn process_code(
reloc: reloc.cloned(), reloc: reloc.cloned(),
op: 0, op: 0,
branch_dest: None, branch_dest: None,
line,
}); });
} }
Ok((ops, insts)) Ok((ops, insts))

139
src/views/appearance.rs Normal file
View File

@@ -0,0 +1,139 @@
use egui::{Color32, FontFamily, FontId, TextStyle};
use time::UtcOffset;
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct Appearance {
pub ui_font: FontId,
pub code_font: FontId,
pub diff_colors: Vec<Color32>,
pub theme: eframe::Theme,
// Applied by theme
#[serde(skip)]
pub text_color: Color32, // GRAY
#[serde(skip)]
pub emphasized_text_color: Color32, // LIGHT_GRAY
#[serde(skip)]
pub deemphasized_text_color: Color32, // DARK_GRAY
#[serde(skip)]
pub highlight_color: Color32, // WHITE
#[serde(skip)]
pub replace_color: Color32, // LIGHT_BLUE
#[serde(skip)]
pub insert_color: Color32, // GREEN
#[serde(skip)]
pub delete_color: Color32, // RED
// Global
#[serde(skip)]
pub utc_offset: UtcOffset,
}
impl Default for Appearance {
fn default() -> Self {
Self {
ui_font: FontId { size: 12.0, family: FontFamily::Proportional },
code_font: FontId { size: 14.0, family: FontFamily::Monospace },
diff_colors: DEFAULT_COLOR_ROTATION.to_vec(),
theme: eframe::Theme::Dark,
text_color: Color32::GRAY,
emphasized_text_color: Color32::LIGHT_GRAY,
deemphasized_text_color: Color32::DARK_GRAY,
highlight_color: Color32::WHITE,
replace_color: Color32::LIGHT_BLUE,
insert_color: Color32::GREEN,
delete_color: Color32::from_rgb(200, 40, 41),
utc_offset: UtcOffset::UTC,
}
}
}
impl Appearance {
pub fn apply(&mut self, style: &egui::Style) -> egui::Style {
let mut style = style.clone();
style.text_styles.insert(TextStyle::Body, FontId {
size: (self.ui_font.size * 0.75).floor(),
family: self.ui_font.family.clone(),
});
style.text_styles.insert(TextStyle::Body, self.ui_font.clone());
style.text_styles.insert(TextStyle::Button, self.ui_font.clone());
style.text_styles.insert(TextStyle::Heading, FontId {
size: (self.ui_font.size * 1.5).floor(),
family: self.ui_font.family.clone(),
});
style.text_styles.insert(TextStyle::Monospace, self.code_font.clone());
match self.theme {
eframe::Theme::Dark => {
style.visuals = egui::Visuals::dark();
self.text_color = Color32::GRAY;
self.emphasized_text_color = Color32::LIGHT_GRAY;
self.deemphasized_text_color = Color32::DARK_GRAY;
self.highlight_color = Color32::WHITE;
self.replace_color = Color32::LIGHT_BLUE;
self.insert_color = Color32::GREEN;
self.delete_color = Color32::from_rgb(200, 40, 41);
}
eframe::Theme::Light => {
style.visuals = egui::Visuals::light();
self.text_color = Color32::GRAY;
self.emphasized_text_color = Color32::DARK_GRAY;
self.deemphasized_text_color = Color32::LIGHT_GRAY;
self.highlight_color = Color32::BLACK;
self.replace_color = Color32::DARK_BLUE;
self.insert_color = Color32::DARK_GREEN;
self.delete_color = Color32::from_rgb(200, 40, 41);
}
}
style
}
}
pub const DEFAULT_COLOR_ROTATION: [Color32; 9] = [
Color32::from_rgb(255, 0, 255),
Color32::from_rgb(0, 255, 255),
Color32::from_rgb(0, 128, 0),
Color32::from_rgb(255, 0, 0),
Color32::from_rgb(255, 255, 0),
Color32::from_rgb(255, 192, 203),
Color32::from_rgb(0, 0, 255),
Color32::from_rgb(0, 255, 0),
Color32::from_rgb(213, 138, 138),
];
pub fn appearance_window(ctx: &egui::Context, show: &mut bool, appearance: &mut Appearance) {
egui::Window::new("Appearance").open(show).show(ctx, |ui| {
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.label("UI font:");
egui::introspection::font_id_ui(ui, &mut appearance.ui_font);
ui.separator();
ui.label("Code font:");
egui::introspection::font_id_ui(ui, &mut appearance.code_font);
ui.separator();
ui.label("Diff colors:");
if ui.button("Reset").clicked() {
appearance.diff_colors = DEFAULT_COLOR_ROTATION.to_vec();
}
let mut remove_at: Option<usize> = None;
let num_colors = appearance.diff_colors.len();
for (idx, color) in appearance.diff_colors.iter_mut().enumerate() {
ui.horizontal(|ui| {
ui.color_edit_button_srgba(color);
if num_colors > 1 && ui.small_button("-").clicked() {
remove_at = Some(idx);
}
});
}
if let Some(idx) = remove_at {
appearance.diff_colors.remove(idx);
}
if ui.small_button("+").clicked() {
appearance.diff_colors.push(Color32::BLACK);
}
});
}

View File

@@ -1,19 +1,90 @@
#[cfg(windows)] #[cfg(windows)]
use std::string::FromUtf16Error; use std::string::FromUtf16Error;
use std::sync::{Arc, RwLock}; use std::{
borrow::Cow,
mem::take,
path::{PathBuf, MAIN_SEPARATOR},
};
#[cfg(windows)] #[cfg(windows)]
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use const_format::formatcp; use const_format::formatcp;
use egui::{output::OpenUrl, Color32}; use egui::{
output::OpenUrl, text::LayoutJob, CollapsingHeader, FontFamily, FontId, RichText,
SelectableLabel, TextFormat, Widget,
};
use globset::Glob;
use self_update::cargo_crate_version; use self_update::cargo_crate_version;
use crate::{ use crate::{
app::{AppConfig, DiffKind, ViewState}, app::{AppConfig, AppConfigRef, ObjectConfig},
jobs::{bindiff::queue_bindiff, objdiff::queue_build, update::queue_update}, config::{ProjectObject, ProjectObjectNode},
jobs::{
check_update::{start_check_update, CheckUpdateResult},
update::start_update,
Job, JobQueue, JobResult,
},
update::RELEASE_URL, update::RELEASE_URL,
views::appearance::Appearance,
}; };
#[derive(Default)]
pub struct ConfigViewState {
pub check_update: Option<Box<CheckUpdateResult>>,
pub check_update_running: bool,
pub queue_check_update: bool,
pub update_running: bool,
pub queue_update: bool,
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,
#[cfg(windows)]
pub available_wsl_distros: Option<Vec<String>>,
}
impl ConfigViewState {
pub fn pre_update(&mut self, jobs: &mut JobQueue) {
jobs.results.retain_mut(|result| {
if let JobResult::CheckUpdate(result) = result {
self.check_update = take(result);
false
} else {
true
}
});
self.build_running = jobs.is_running(Job::ObjDiff);
self.check_update_running = jobs.is_running(Job::CheckUpdate);
self.update_running = jobs.is_running(Job::Update);
}
pub fn post_update(&mut self, jobs: &mut JobQueue, config: &AppConfigRef) {
if self.queue_build {
self.queue_build = false;
if let Ok(mut config) = config.write() {
config.queue_build = true;
}
}
if self.queue_check_update {
self.queue_check_update = false;
jobs.push_once(Job::CheckUpdate, start_check_update);
}
if self.queue_update {
self.queue_update = false;
jobs.push_once(Job::Update, start_update);
}
}
}
pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[
"*.c", "*.cp", "*.cpp", "*.cxx", "*.h", "*.hp", "*.hpp", "*.hxx", "*.s", "*.S", "*.asm",
"*.inc", "*.py", "*.yml", "*.txt", "*.json",
];
#[cfg(windows)] #[cfg(windows)]
fn process_utf16(bytes: &[u8]) -> Result<String, FromUtf16Error> { fn process_utf16(bytes: &[u8]) -> Result<String, FromUtf16Error> {
let u16_bytes: Vec<u16> = bytes let u16_bytes: Vec<u16> = bytes
@@ -47,206 +118,612 @@ fn fetch_wsl2_distros() -> Vec<String> {
.unwrap_or_default() .unwrap_or_default()
} }
pub fn config_ui(ui: &mut egui::Ui, config: &Arc<RwLock<AppConfig>>, view_state: &mut ViewState) { pub fn config_ui(
ui: &mut egui::Ui,
config: &AppConfigRef,
show_config_window: &mut bool,
state: &mut ConfigViewState,
appearance: &Appearance,
) {
let mut config_guard = config.write().unwrap(); let mut config_guard = config.write().unwrap();
let AppConfig { let AppConfig {
custom_make,
available_wsl_distros,
selected_wsl_distro, selected_wsl_distro,
project_dir,
target_obj_dir, target_obj_dir,
base_obj_dir, base_obj_dir,
obj_path, selected_obj,
build_target,
left_obj,
right_obj,
project_dir_change,
queue_update_check,
auto_update_check, auto_update_check,
objects,
object_nodes,
..
} = &mut *config_guard; } = &mut *config_guard;
ui.heading("Updates"); ui.heading("Updates");
ui.checkbox(auto_update_check, "Check for updates on startup"); ui.checkbox(auto_update_check, "Check for updates on startup");
if ui.button("Check now").clicked() { if ui.add_enabled(!state.check_update_running, egui::Button::new("Check now")).clicked() {
*queue_update_check = true; state.queue_check_update = true;
} }
ui.label(format!("Current version: {}", cargo_crate_version!())).on_hover_ui_at_pointer(|ui| { 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 branch: {}", env!("VERGEN_GIT_BRANCH")));
ui.label(formatcp!("Git commit: {}", env!("VERGEN_GIT_SHA"))); ui.label(formatcp!("Git commit: {}", env!("VERGEN_GIT_SHA")));
ui.label(formatcp!("Build target: {}", env!("VERGEN_CARGO_TARGET_TRIPLE"))); ui.label(formatcp!("Build target: {}", env!("VERGEN_CARGO_TARGET_TRIPLE")));
ui.label(formatcp!("Build type: {}", env!("VERGEN_CARGO_PROFILE"))); ui.label(formatcp!("Debug: {}", env!("VERGEN_CARGO_DEBUG")));
}); });
if let Some(state) = &view_state.check_update { if let Some(result) = &state.check_update {
ui.label(format!("Latest version: {}", state.latest_release.version)); ui.label(format!("Latest version: {}", result.latest_release.version));
if state.update_available { if result.update_available {
ui.colored_label(Color32::LIGHT_GREEN, "Update available"); ui.colored_label(appearance.insert_color, "Update available");
ui.horizontal(|ui| { ui.horizontal(|ui| {
if state.found_binary && ui if result.found_binary
.button("Automatic") && ui
.add_enabled(!state.update_running, egui::Button::new("Automatic"))
.on_hover_text_at_pointer( .on_hover_text_at_pointer(
"Automatically download and replace the current build", "Automatically download and replace the current build",
) )
.clicked() { .clicked()
view_state.jobs.push(queue_update()); {
state.queue_update = true;
} }
if ui if ui
.button("Manual") .button("Manual")
.on_hover_text_at_pointer("Open a link to the latest release on GitHub") .on_hover_text_at_pointer("Open a link to the latest release on GitHub")
.clicked() .clicked()
{ {
ui.output().open_url = ui.output_mut(|output| {
Some(OpenUrl { url: RELEASE_URL.to_string(), new_tab: true }); output.open_url =
Some(OpenUrl { url: RELEASE_URL.to_string(), new_tab: true })
});
} }
}); });
} }
} }
ui.separator(); ui.separator();
ui.heading("Build config");
#[cfg(windows)] #[cfg(windows)]
{ {
if available_wsl_distros.is_none() { ui.heading("Build");
*available_wsl_distros = Some(fetch_wsl2_distros()); if state.available_wsl_distros.is_none() {
state.available_wsl_distros = Some(fetch_wsl2_distros());
} }
egui::ComboBox::from_label("Run in WSL2") egui::ComboBox::from_label("Run in WSL2")
.selected_text(selected_wsl_distro.as_ref().unwrap_or(&"None".to_string())) .selected_text(selected_wsl_distro.as_ref().unwrap_or(&"Disabled".to_string()))
.show_ui(ui, |ui| { .show_ui(ui, |ui| {
ui.selectable_value(selected_wsl_distro, None, "None"); ui.selectable_value(selected_wsl_distro, None, "Disabled");
for distro in available_wsl_distros.as_ref().unwrap() { for distro in state.available_wsl_distros.as_ref().unwrap() {
ui.selectable_value(selected_wsl_distro, Some(distro.clone()), distro); ui.selectable_value(selected_wsl_distro, Some(distro.clone()), distro);
} }
}); });
ui.separator();
} }
#[cfg(not(windows))] #[cfg(not(windows))]
{ {
let _ = available_wsl_distros;
let _ = selected_wsl_distro; let _ = selected_wsl_distro;
} }
ui.label("Custom make program:"); ui.horizontal(|ui| {
let mut custom_make_str = custom_make.clone().unwrap_or_default(); ui.heading("Project");
if ui.text_edit_singleline(&mut custom_make_str).changed() { if ui.button(RichText::new("Settings")).clicked() {
if custom_make_str.is_empty() { *show_config_window = true;
*custom_make = None;
} else {
*custom_make = Some(custom_make_str);
} }
} });
ui.separator(); let mut new_selected_obj = selected_obj.clone();
if objects.is_empty() {
ui.heading("Project config"); if let (Some(base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) {
if ui.button("Select object").clicked() {
if view_state.diff_kind == DiffKind::SplitObj {
if ui.button("Select project dir").clicked() {
if let Some(path) = rfd::FileDialog::new().pick_folder() {
*project_dir = Some(path);
*project_dir_change = true;
*target_obj_dir = None;
*base_obj_dir = None;
*obj_path = None;
}
}
if let Some(dir) = project_dir {
ui.label(dir.to_string_lossy());
}
ui.separator();
if let Some(project_dir) = project_dir {
if ui.button("Select target build dir").clicked() {
if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder()
{
*target_obj_dir = Some(path);
*obj_path = None;
}
}
if let Some(dir) = target_obj_dir {
ui.label(dir.to_string_lossy());
}
ui.checkbox(build_target, "Build target");
ui.separator();
if ui.button("Select base build dir").clicked() {
if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder()
{
*base_obj_dir = Some(path);
*obj_path = None;
}
}
if let Some(dir) = base_obj_dir {
ui.label(dir.to_string_lossy());
}
ui.separator();
}
if let Some(base_dir) = base_obj_dir {
if ui.button("Select obj").clicked() {
if let Some(path) = rfd::FileDialog::new() if let Some(path) = rfd::FileDialog::new()
.set_directory(&base_dir) .set_directory(&target_dir)
.add_filter("Object file", &["o", "elf"]) .add_filter("Object file", &["o", "elf"])
.pick_file() .pick_file()
{ {
let mut new_build_obj: Option<String> = None;
if let Ok(obj_path) = path.strip_prefix(&base_dir) { if let Ok(obj_path) = path.strip_prefix(&base_dir) {
new_build_obj = Some(obj_path.display().to_string()); let target_path = target_dir.join(obj_path);
} else if let Some(build_asm_dir) = target_obj_dir { new_selected_obj = Some(ObjectConfig {
if let Ok(obj_path) = path.strip_prefix(&build_asm_dir) { name: obj_path.display().to_string(),
new_build_obj = Some(obj_path.display().to_string()); target_path: Some(target_path),
} base_path: Some(path),
} reverse_fn_order: None,
if let Some(new_build_obj) = new_build_obj { complete: None,
*obj_path = Some(new_build_obj); });
view_state } else if let Ok(obj_path) = path.strip_prefix(&target_dir) {
.jobs let base_path = base_dir.join(obj_path);
.push(queue_build(config.clone(), view_state.diff_config.clone())); new_selected_obj = Some(ObjectConfig {
name: obj_path.display().to_string(),
target_path: Some(path),
base_path: Some(base_path),
reverse_fn_order: None,
complete: None,
});
} }
} }
} }
if let Some(obj) = obj_path { if let Some(obj) = selected_obj {
ui.label(&*obj); ui.label(
if ui.button("Build").clicked() { RichText::new(&obj.name)
view_state .color(appearance.replace_color)
.jobs .family(FontFamily::Monospace),
.push(queue_build(config.clone(), view_state.diff_config.clone())); );
}
} else {
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 mut root_open = None;
let mut node_open = NodeOpen::Default;
ui.horizontal(|ui| {
if ui.small_button("").on_hover_text_at_pointer("Collapse all").clicked() {
root_open = Some(false);
node_open = NodeOpen::Close;
}
if ui.small_button("").on_hover_text_at_pointer("Expand all").clicked() {
root_open = Some(true);
node_open = NodeOpen::Open;
}
if ui
.add_enabled(selected_obj.is_some(), egui::Button::new("").small())
.on_hover_text_at_pointer("Current object")
.clicked()
{
root_open = Some(true);
node_open = NodeOpen::Object;
}
if ui
.selectable_label(state.filter_diffable, "Diffable")
.on_hover_text_at_pointer("Only show objects with a source file")
.clicked()
{
state.filter_diffable = !state.filter_diffable;
}
});
if state.object_search.is_empty() {
if had_search {
root_open = Some(true);
node_open = NodeOpen::Object;
}
} else if !had_search {
root_open = Some(true);
node_open = NodeOpen::Open;
}
CollapsingHeader::new(RichText::new("🗀 Objects").font(FontId {
size: appearance.ui_font.size,
family: appearance.code_font.family.clone(),
}))
.open(root_open)
.default_open(true)
.show(ui, |ui| {
let mut nodes = Cow::Borrowed(object_nodes);
if !state.object_search.is_empty() || state.filter_diffable {
let search = state.object_search.to_ascii_lowercase();
nodes = Cow::Owned(
object_nodes
.iter()
.filter_map(|node| filter_node(node, &search, state.filter_diffable))
.collect(),
);
}
ui.style_mut().wrap = Some(false);
for node in nodes.iter() {
display_node(ui, &mut new_selected_obj, node, appearance, node_open);
}
});
}
if new_selected_obj != *selected_obj {
if let Some(obj) = new_selected_obj {
// Will set obj_changed, which will trigger a rebuild
config_guard.set_selected_obj(obj);
}
}
if config_guard.selected_obj.is_some()
&& ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked()
{
state.queue_build = true;
}
ui.separator();
}
fn display_object(
ui: &mut egui::Ui,
selected_obj: &mut Option<ObjectConfig>,
name: &str,
object: &ProjectObject,
appearance: &Appearance,
) {
let object_name = object.name();
let selected = matches!(selected_obj, Some(obj) if obj.name == object_name);
let color = if selected {
appearance.emphasized_text_color
} else if let Some(complete) = object.complete {
if complete {
appearance.insert_color
} else {
appearance.delete_color
}
} else {
appearance.text_color
};
let clicked = SelectableLabel::new(
selected,
RichText::new(name)
.font(FontId {
size: appearance.ui_font.size,
family: appearance.code_font.family.clone(),
})
.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,
});
}
}
#[derive(Default, Copy, Clone, PartialEq, Eq, Debug)]
enum NodeOpen {
#[default]
Default,
Open,
Close,
Object,
}
fn display_node(
ui: &mut egui::Ui,
selected_obj: &mut Option<ObjectConfig>,
node: &ProjectObjectNode,
appearance: &Appearance,
node_open: NodeOpen,
) {
match node {
ProjectObjectNode::File(name, object) => {
display_object(ui, selected_obj, name, object, appearance);
}
ProjectObjectNode::Dir(name, children) => {
let contains_obj = selected_obj.as_ref().map(|path| contains_node(node, path));
let open = match node_open {
NodeOpen::Default => None,
NodeOpen::Open => Some(true),
NodeOpen::Close => Some(false),
NodeOpen::Object => contains_obj,
};
let color = if contains_obj == Some(true) {
appearance.replace_color
} else {
appearance.text_color
};
CollapsingHeader::new(
RichText::new(name)
.font(FontId {
size: appearance.ui_font.size,
family: appearance.code_font.family.clone(),
})
.color(color),
)
.open(open)
.show(ui, |ui| {
for node in children {
display_node(ui, selected_obj, node, appearance, node_open);
} }
} });
ui.separator();
} }
} else if view_state.diff_kind == DiffKind::WholeBinary { }
if ui.button("Select left obj").clicked() { }
if let Some(path) =
rfd::FileDialog::new().add_filter("Object file", &["o", "elf"]).pick_file() fn contains_node(node: &ProjectObjectNode, selected_obj: &ObjectConfig) -> bool {
match node {
ProjectObjectNode::File(_, object) => object.name() == selected_obj.name,
ProjectObjectNode::Dir(_, children) => {
children.iter().any(|node| contains_node(node, selected_obj))
}
}
}
fn filter_node(
node: &ProjectObjectNode,
search: &str,
filter_diffable: bool,
) -> Option<ProjectObjectNode> {
match node {
ProjectObjectNode::File(name, object) => {
if (search.is_empty() || name.to_ascii_lowercase().contains(search))
&& (!filter_diffable || object.base_path.is_some())
{ {
*left_obj = Some(path); Some(node.clone())
} else {
None
} }
} }
if let Some(obj) = left_obj { ProjectObjectNode::Dir(name, children) => {
ui.label(obj.to_string_lossy()); if (search.is_empty() || name.to_ascii_lowercase().contains(search)) && !filter_diffable
}
if ui.button("Select right obj").clicked() {
if let Some(path) =
rfd::FileDialog::new().add_filter("Object file", &["o", "elf"]).pick_file()
{ {
*right_obj = Some(path); return Some(node.clone());
} }
} let new_children = children
if let Some(obj) = right_obj { .iter()
ui.label(obj.to_string_lossy()); .filter_map(|child| filter_node(child, search, filter_diffable))
} .collect::<Vec<_>>();
if !new_children.is_empty() {
if let (Some(_), Some(_)) = (left_obj, right_obj) { Some(ProjectObjectNode::Dir(name.clone(), new_children))
if ui.button("Build").clicked() { } else {
view_state.jobs.push(queue_bindiff(config.clone())); None
} }
} }
} }
}
ui.checkbox(&mut view_state.reverse_fn_order, "Reverse function order (deferred)");
ui.separator(); const HELP_ICON: &str = "";
fn subheading(ui: &mut egui::Ui, text: &str, appearance: &Appearance) {
ui.label(
RichText::new(text).size(appearance.ui_font.size).color(appearance.emphasized_text_color),
);
}
fn format_path(path: &Option<PathBuf>, appearance: &Appearance) -> RichText {
let mut color = appearance.replace_color;
let text = if let Some(dir) = path {
if let Some(rel) = dirs::home_dir().and_then(|home| dir.strip_prefix(&home).ok()) {
format!("~{}{}", MAIN_SEPARATOR, rel.display())
} else {
format!("{}", dir.display())
}
} else {
color = appearance.delete_color;
"[none]".to_string()
};
RichText::new(text).color(color).family(FontFamily::Monospace)
}
fn pick_folder_ui(
ui: &mut egui::Ui,
dir: &Option<PathBuf>,
label: &str,
tooltip: impl FnOnce(&mut egui::Ui),
appearance: &Appearance,
enabled: bool,
) -> egui::Response {
let response = ui.horizontal(|ui| {
subheading(ui, label, appearance);
ui.link(HELP_ICON).on_hover_ui(tooltip);
ui.add_enabled(enabled, egui::Button::new("Select"))
});
ui.label(format_path(dir, appearance));
response.inner
}
pub fn project_window(
ctx: &egui::Context,
config: &AppConfigRef,
show: &mut bool,
state: &mut ConfigViewState,
appearance: &Appearance,
) {
let mut config_guard = config.write().unwrap();
egui::Window::new("Project").open(show).show(ctx, |ui| {
split_obj_config_ui(ui, &mut config_guard, state, appearance);
});
if let Some(error) = &state.load_error {
let mut open = true;
egui::Window::new("Error").open(&mut open).show(ctx, |ui| {
ui.label("Failed to load project config:");
ui.colored_label(appearance.delete_color, error);
});
if !open {
state.load_error = None;
}
}
}
fn split_obj_config_ui(
ui: &mut egui::Ui,
config: &mut AppConfig,
state: &mut ConfigViewState,
appearance: &Appearance,
) {
let text_format = TextFormat::simple(appearance.ui_font.clone(), appearance.text_color);
let code_format = TextFormat::simple(
FontId { size: appearance.ui_font.size, family: appearance.code_font.family.clone() },
appearance.emphasized_text_color,
);
let response = pick_folder_ui(
ui,
&config.project_dir,
"Project directory",
|ui| {
let mut job = LayoutJob::default();
job.append("The root project directory.\n\n", 0.0, text_format.clone());
job.append(
"If a configuration file exists, it will be loaded automatically.",
0.0,
text_format.clone(),
);
ui.label(job);
},
appearance,
true,
);
if response.clicked() {
if let Some(path) = rfd::FileDialog::new().pick_folder() {
config.set_project_dir(path);
}
}
ui.separator();
ui.horizontal(|ui| {
subheading(ui, "Custom make program", appearance);
ui.link(HELP_ICON).on_hover_ui(|ui| {
let mut job = LayoutJob::default();
job.append("By default, objdiff will build with ", 0.0, text_format.clone());
job.append("make", 0.0, code_format.clone());
job.append(
".\nIf the project uses a different build system (e.g. ",
0.0,
text_format.clone(),
);
job.append("ninja", 0.0, code_format.clone());
job.append(
"), specify it here.\nThe program must be in your ",
0.0,
text_format.clone(),
);
job.append("PATH", 0.0, code_format.clone());
job.append(".", 0.0, text_format.clone());
ui.label(job);
});
});
let mut custom_make_str = config.custom_make.clone().unwrap_or_default();
if ui.text_edit_singleline(&mut custom_make_str).changed() {
if custom_make_str.is_empty() {
config.custom_make = None;
} else {
config.custom_make = Some(custom_make_str);
}
}
ui.separator();
if let Some(project_dir) = config.project_dir.clone() {
let response = pick_folder_ui(
ui,
&config.target_obj_dir,
"Target build directory",
|ui| {
let mut job = LayoutJob::default();
job.append(
"This contains the \"target\" or \"expected\" objects, which are the intended result of the match.\n\n",
0.0,
text_format.clone(),
);
job.append(
"These are usually created by the project's build system or assembled.",
0.0,
text_format.clone(),
);
ui.label(job);
},
appearance,
!config.project_config_loaded,
);
if response.clicked() {
if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() {
config.set_target_obj_dir(path);
}
}
ui.checkbox(&mut config.build_target, "Build target objects").on_hover_ui(|ui| {
let mut job = LayoutJob::default();
job.append(
"Tells the build system to produce the target object.\n",
0.0,
text_format.clone(),
);
job.append("For example, this would call ", 0.0, text_format.clone());
job.append("make path/to/target.o", 0.0, code_format.clone());
job.append(".\n\n", 0.0, text_format.clone());
job.append(
"This is useful if the target objects are not already built\n",
0.0,
text_format.clone(),
);
job.append(
"or if they can change based on project configuration,\n",
0.0,
text_format.clone(),
);
job.append(
"but requires that the build system is configured correctly.",
0.0,
text_format.clone(),
);
ui.label(job);
});
ui.separator();
let response = pick_folder_ui(
ui,
&config.base_obj_dir,
"Base build directory",
|ui| {
let mut job = LayoutJob::default();
job.append(
"This contains the objects built from your decompiled code.",
0.0,
text_format.clone(),
);
ui.label(job);
},
appearance,
!config.project_config_loaded,
);
if response.clicked() {
if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() {
config.set_base_obj_dir(path);
}
}
ui.separator();
}
subheading(ui, "Watch settings", appearance);
let response =
ui.checkbox(&mut 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.",
0.0,
text_format.clone(),
);
ui.label(job);
});
if response.changed() {
config.watcher_change = true;
};
ui.horizontal(|ui| {
ui.label(RichText::new("File patterns").color(appearance.text_color));
if ui.button("Reset").clicked() {
config.watch_patterns =
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect();
config.watcher_change = true;
}
});
let mut remove_at: Option<usize> = None;
for (idx, glob) in config.watch_patterns.iter().enumerate() {
ui.horizontal(|ui| {
ui.label(
RichText::new(format!("{}", glob))
.color(appearance.text_color)
.family(FontFamily::Monospace),
);
if ui.small_button("-").clicked() {
remove_at = Some(idx);
}
});
}
if let Some(idx) = remove_at {
config.watch_patterns.remove(idx);
config.watcher_change = true;
}
ui.horizontal(|ui| {
egui::TextEdit::singleline(&mut state.watch_pattern_text).desired_width(100.0).show(ui);
if ui.small_button("+").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();
}
}
});
} }

View File

@@ -1,45 +1,47 @@
use std::{cmp::min, default::Default, mem::take}; use std::{cmp::min, default::Default, mem::take};
use egui::{text::LayoutJob, Color32, Label, Sense}; use egui::{text::LayoutJob, Align, Label, Layout, Sense, Vec2};
use egui_extras::{Size, StripBuilder, TableBuilder}; use egui_extras::{Column, TableBuilder};
use time::format_description; use time::format_description;
use crate::{ use crate::{
app::{View, ViewConfig, ViewState},
jobs::Job,
obj::{ObjDataDiff, ObjDataDiffKind, ObjInfo, ObjSection}, obj::{ObjDataDiff, ObjDataDiffKind, ObjInfo, ObjSection},
views::{write_text, COLOR_RED}, views::{
appearance::Appearance,
symbol_diff::{DiffViewState, SymbolReference, View},
write_text,
},
}; };
const BYTES_PER_ROW: usize = 16; const BYTES_PER_ROW: usize = 16;
fn find_section<'a>(obj: &'a ObjInfo, section_name: &str) -> Option<&'a ObjSection> { fn find_section<'a>(obj: &'a ObjInfo, selected_symbol: &SymbolReference) -> Option<&'a ObjSection> {
obj.sections.iter().find(|s| s.name == section_name) obj.sections.iter().find(|section| section.name == selected_symbol.section_name)
} }
fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], config: &ViewConfig) { fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appearance: &Appearance) {
if diffs.iter().any(|d| d.kind != ObjDataDiffKind::None) { if diffs.iter().any(|d| d.kind != ObjDataDiffKind::None) {
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color); ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
} }
let mut job = LayoutJob::default(); let mut job = LayoutJob::default();
write_text( write_text(
format!("{address:08X}: ").as_str(), format!("{address:08X}: ").as_str(),
Color32::GRAY, appearance.text_color,
&mut job, &mut job,
config.code_font.clone(), appearance.code_font.clone(),
); );
let mut cur_addr = 0usize; let mut cur_addr = 0usize;
for diff in diffs { for diff in diffs {
let base_color = match diff.kind { let base_color = match diff.kind {
ObjDataDiffKind::None => Color32::GRAY, ObjDataDiffKind::None => appearance.text_color,
ObjDataDiffKind::Replace => Color32::LIGHT_BLUE, ObjDataDiffKind::Replace => appearance.replace_color,
ObjDataDiffKind::Delete => COLOR_RED, ObjDataDiffKind::Delete => appearance.delete_color,
ObjDataDiffKind::Insert => Color32::GREEN, ObjDataDiffKind::Insert => appearance.insert_color,
}; };
if diff.data.is_empty() { if diff.data.is_empty() {
let mut str = " ".repeat(diff.len); let mut str = " ".repeat(diff.len);
str.push_str(" ".repeat(diff.len / 8).as_str()); str.push_str(" ".repeat(diff.len / 8).as_str());
write_text(str.as_str(), base_color, &mut job, config.code_font.clone()); write_text(str.as_str(), base_color, &mut job, appearance.code_font.clone());
cur_addr += diff.len; cur_addr += diff.len;
} else { } else {
let mut text = String::new(); let mut text = String::new();
@@ -50,7 +52,7 @@ fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], config:
text.push(' '); text.push(' ');
} }
} }
write_text(text.as_str(), base_color, &mut job, config.code_font.clone()); write_text(text.as_str(), base_color, &mut job, appearance.code_font.clone());
} }
} }
if cur_addr < BYTES_PER_ROW { if cur_addr < BYTES_PER_ROW {
@@ -58,22 +60,22 @@ fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], config:
let mut str = " ".to_string(); let mut str = " ".to_string();
str.push_str(" ".repeat(n).as_str()); str.push_str(" ".repeat(n).as_str());
str.push_str(" ".repeat(n / 8).as_str()); str.push_str(" ".repeat(n / 8).as_str());
write_text(str.as_str(), Color32::GRAY, &mut job, config.code_font.clone()); write_text(str.as_str(), appearance.text_color, &mut job, appearance.code_font.clone());
} }
write_text(" ", Color32::GRAY, &mut job, config.code_font.clone()); write_text(" ", appearance.text_color, &mut job, appearance.code_font.clone());
for diff in diffs { for diff in diffs {
let base_color = match diff.kind { let base_color = match diff.kind {
ObjDataDiffKind::None => Color32::GRAY, ObjDataDiffKind::None => appearance.text_color,
ObjDataDiffKind::Replace => Color32::LIGHT_BLUE, ObjDataDiffKind::Replace => appearance.replace_color,
ObjDataDiffKind::Delete => COLOR_RED, ObjDataDiffKind::Delete => appearance.delete_color,
ObjDataDiffKind::Insert => Color32::GREEN, ObjDataDiffKind::Insert => appearance.insert_color,
}; };
if diff.data.is_empty() { if diff.data.is_empty() {
write_text( write_text(
" ".repeat(diff.len).as_str(), " ".repeat(diff.len).as_str(),
base_color, base_color,
&mut job, &mut job,
config.code_font.clone(), appearance.code_font.clone(),
); );
} else { } else {
let mut text = String::new(); let mut text = String::new();
@@ -85,7 +87,7 @@ fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], config:
text.push('.'); text.push('.');
} }
} }
write_text(text.as_str(), base_color, &mut job, config.code_font.clone()); write_text(text.as_str(), base_color, &mut job, appearance.code_font.clone());
} }
} }
ui.add(Label::new(job).sense(Sense::click())); ui.add(Label::new(job).sense(Sense::click()));
@@ -130,132 +132,138 @@ fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> {
fn data_table_ui( fn data_table_ui(
table: TableBuilder<'_>, table: TableBuilder<'_>,
left_obj: &ObjInfo, left_obj: Option<&ObjInfo>,
right_obj: &ObjInfo, right_obj: Option<&ObjInfo>,
section_name: &str, selected_symbol: &SymbolReference,
config: &ViewConfig, config: &Appearance,
) -> Option<()> { ) -> Option<()> {
let left_section = find_section(left_obj, section_name)?; let left_section = left_obj.and_then(|obj| find_section(obj, selected_symbol));
let right_section = find_section(right_obj, section_name)?; let right_section = right_obj.and_then(|obj| find_section(obj, selected_symbol));
let total_bytes = left_section.data_diff.iter().fold(0usize, |accum, item| accum + item.len); let total_bytes = left_section
.or(right_section)?
.data_diff
.iter()
.fold(0usize, |accum, item| accum + item.len);
if total_bytes == 0 { if total_bytes == 0 {
return None; return None;
} }
let total_rows = (total_bytes - 1) / BYTES_PER_ROW + 1; let total_rows = (total_bytes - 1) / BYTES_PER_ROW + 1;
let left_diffs = split_diffs(&left_section.data_diff); let left_diffs = left_section.map(|section| split_diffs(&section.data_diff));
let right_diffs = split_diffs(&right_section.data_diff); let right_diffs = right_section.map(|section| split_diffs(&section.data_diff));
table.body(|body| { table.body(|body| {
body.rows(config.code_font.size, total_rows, |row_index, mut row| { body.rows(config.code_font.size, total_rows, |row_index, mut row| {
let address = row_index * BYTES_PER_ROW; let address = row_index * BYTES_PER_ROW;
row.col(|ui| { row.col(|ui| {
data_row_ui(ui, address, &left_diffs[row_index], config); if let Some(left_diffs) = &left_diffs {
data_row_ui(ui, address, &left_diffs[row_index], config);
}
}); });
row.col(|ui| { row.col(|ui| {
data_row_ui(ui, address, &right_diffs[row_index], config); if let Some(right_diffs) = &right_diffs {
data_row_ui(ui, address, &right_diffs[row_index], config);
}
}); });
}); });
}); });
Some(()) Some(())
} }
pub fn data_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool { pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
let mut rebuild = false; let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
if let (Some(result), Some(selected_symbol)) = (&view_state.build, &view_state.selected_symbol) else {
{ return;
StripBuilder::new(ui) };
.size(Size::exact(20.0))
.size(Size::exact(40.0)) // Header
.size(Size::remainder()) let available_width = ui.available_width();
.vertical(|mut strip| { let column_width = available_width / 2.0;
strip.strip(|builder| { ui.allocate_ui_with_layout(
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| { Vec2 { x: available_width, y: 100.0 },
strip.cell(|ui| { Layout::left_to_right(Align::Min),
ui.horizontal(|ui| { |ui| {
if ui.button("Back").clicked() { // Left column
view_state.current_view = View::SymbolDiff; ui.allocate_ui_with_layout(
} Vec2 { x: column_width, y: 100.0 },
}); Layout::top_down(Align::Min),
}); |ui| {
strip.cell(|ui| { ui.set_width(column_width);
ui.horizontal(|ui| {
if ui.button("Build").clicked() { if ui.button("Back").clicked() {
rebuild = true; state.current_view = View::SymbolDiff;
}
ui.scope(|ui| {
ui.style_mut().override_text_style =
Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
if view_state
.jobs
.iter()
.any(|job| job.job_type == Job::ObjDiff)
{
ui.label("Building...");
} else {
ui.label("Last built:");
let format =
format_description::parse("[hour]:[minute]:[second]")
.unwrap();
ui.label(
result
.time
.to_offset(view_state.utc_offset)
.format(&format)
.unwrap(),
);
}
});
});
});
});
});
strip.strip(|builder| {
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
strip.cell(|ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style =
Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.colored_label(Color32::WHITE, selected_symbol);
ui.label("Diff target:");
ui.separator();
});
});
strip.cell(|ui| {
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();
});
});
});
});
strip.cell(|ui| {
if let (Some(left_obj), Some(right_obj)) =
(&result.first_obj, &result.second_obj)
{
let table = TableBuilder::new(ui)
.striped(false)
.cell_layout(egui::Layout::left_to_right(egui::Align::Min))
.column(Size::relative(0.5))
.column(Size::relative(0.5))
.resizable(false);
data_table_ui(
table,
left_obj,
right_obj,
selected_symbol,
&view_state.view_config,
);
} }
});
}); ui.scope(|ui| {
} ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
rebuild ui.style_mut().wrap = Some(false);
ui.colored_label(appearance.highlight_color, &selected_symbol.symbol_name);
ui.label("Diff target:");
});
},
);
// Right column
ui.allocate_ui_with_layout(
Vec2 { x: column_width, y: 100.0 },
Layout::top_down(Align::Min),
|ui| {
ui.set_width(column_width);
ui.horizontal(|ui| {
if ui
.add_enabled(!state.build_running, egui::Button::new("Build"))
.clicked()
{
state.queue_build = true;
}
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
if state.build_running {
ui.colored_label(appearance.replace_color, "Building…");
} else {
ui.label("Last built:");
let format =
format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label(
result
.time
.to_offset(appearance.utc_offset)
.format(&format)
.unwrap(),
);
}
});
});
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.label("");
ui.label("Diff base:");
});
},
);
},
);
ui.separator();
// Table
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,
);
} }

34
src/views/demangle.rs Normal file
View File

@@ -0,0 +1,34 @@
use egui::TextStyle;
use crate::views::appearance::Appearance;
#[derive(Default)]
pub struct DemangleViewState {
pub text: String,
}
pub fn demangle_window(
ctx: &egui::Context,
show: &mut bool,
state: &mut DemangleViewState,
appearance: &Appearance,
) {
egui::Window::new("Demangle").open(show).show(ctx, |ui| {
ui.text_edit_singleline(&mut state.text);
ui.add_space(10.0);
if let Some(demangled) = cwdemangle::demangle(&state.text, &Default::default()) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
ui.colored_label(appearance.replace_color, &demangled);
});
if ui.button("Copy").clicked() {
ui.output_mut(|output| output.copied_text = demangled);
}
} else {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
ui.colored_label(appearance.replace_color, "[invalid]");
});
}
});
}

View File

@@ -1,62 +1,98 @@
use std::default::Default; use std::{cmp::Ordering, default::Default};
use cwdemangle::demangle; use cwdemangle::demangle;
use egui::{text::LayoutJob, Color32, FontId, Label, Sense}; use eframe::emath::Align;
use egui_extras::{Size, StripBuilder, TableBuilder}; use egui::{text::LayoutJob, Color32, FontId, Label, Layout, Sense, Vec2};
use egui_extras::{Column, TableBuilder};
use ppc750cl::Argument; use ppc750cl::Argument;
use time::format_description; use time::format_description;
use crate::{ use crate::{
app::{View, ViewConfig, ViewState},
jobs::Job,
obj::{ obj::{
ObjInfo, ObjIns, ObjInsArg, ObjInsArgDiff, ObjInsDiff, ObjInsDiffKind, ObjReloc, ObjInfo, ObjIns, ObjInsArg, ObjInsArgDiff, ObjInsDiff, ObjInsDiffKind, ObjReloc,
ObjRelocKind, ObjSymbol, ObjRelocKind, ObjSymbol,
}, },
views::{symbol_diff::match_color_for_symbol, write_text, COLOR_RED}, views::{
appearance::Appearance,
symbol_diff::{match_color_for_symbol, DiffViewState, SymbolReference, View},
write_text,
},
}; };
fn write_reloc_name(reloc: &ObjReloc, color: Color32, job: &mut LayoutJob, font_id: FontId) { fn write_reloc_name(
reloc: &ObjReloc,
color: Color32,
job: &mut LayoutJob,
font_id: FontId,
appearance: &Appearance,
) {
let name = reloc.target.demangled_name.as_ref().unwrap_or(&reloc.target.name); let name = reloc.target.demangled_name.as_ref().unwrap_or(&reloc.target.name);
write_text(name, Color32::LIGHT_GRAY, job, font_id.clone()); write_text(name, appearance.emphasized_text_color, job, font_id.clone());
if reloc.target.addend != 0 { match reloc.target.addend.cmp(&0i64) {
write_text(&format!("+{:X}", reloc.target.addend), color, job, font_id); Ordering::Greater => {
write_text(&format!("+{:#X}", reloc.target.addend), color, job, font_id)
}
Ordering::Less => {
write_text(&format!("-{:#X}", -reloc.target.addend), color, job, font_id);
}
_ => {}
} }
} }
fn write_reloc(reloc: &ObjReloc, color: Color32, job: &mut LayoutJob, font_id: FontId) { fn write_reloc(
reloc: &ObjReloc,
color: Color32,
job: &mut LayoutJob,
font_id: FontId,
appearance: &Appearance,
) {
match reloc.kind { match reloc.kind {
ObjRelocKind::PpcAddr16Lo => { ObjRelocKind::PpcAddr16Lo => {
write_reloc_name(reloc, color, job, font_id.clone()); write_reloc_name(reloc, color, job, font_id.clone(), appearance);
write_text("@l", color, job, font_id); write_text("@l", color, job, font_id);
} }
ObjRelocKind::PpcAddr16Hi => { ObjRelocKind::PpcAddr16Hi => {
write_reloc_name(reloc, color, job, font_id.clone()); write_reloc_name(reloc, color, job, font_id.clone(), appearance);
write_text("@h", color, job, font_id); write_text("@h", color, job, font_id);
} }
ObjRelocKind::PpcAddr16Ha => { ObjRelocKind::PpcAddr16Ha => {
write_reloc_name(reloc, color, job, font_id.clone()); write_reloc_name(reloc, color, job, font_id.clone(), appearance);
write_text("@ha", color, job, font_id); write_text("@ha", color, job, font_id);
} }
ObjRelocKind::PpcEmbSda21 => { ObjRelocKind::PpcEmbSda21 => {
write_reloc_name(reloc, color, job, font_id.clone()); write_reloc_name(reloc, color, job, font_id.clone(), appearance);
write_text("@sda21", color, job, font_id); write_text("@sda21", color, job, font_id);
} }
ObjRelocKind::MipsHi16 => { ObjRelocKind::MipsHi16 => {
write_text("%hi(", color, job, font_id.clone()); write_text("%hi(", color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone()); write_reloc_name(reloc, color, job, font_id.clone(), appearance);
write_text(")", color, job, font_id); write_text(")", color, job, font_id);
} }
ObjRelocKind::MipsLo16 => { ObjRelocKind::MipsLo16 => {
write_text("%lo(", color, job, font_id.clone()); write_text("%lo(", color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone()); write_reloc_name(reloc, color, job, font_id.clone(), appearance);
write_text(")", color, job, font_id); write_text(")", color, job, font_id);
} }
ObjRelocKind::Absolute ObjRelocKind::MipsGot16 => {
| ObjRelocKind::PpcRel24 write_text("%got(", color, job, font_id.clone());
| ObjRelocKind::PpcRel14 write_reloc_name(reloc, color, job, font_id.clone(), appearance);
| ObjRelocKind::Mips26 => { write_text(")", color, job, font_id);
write_reloc_name(reloc, color, job, font_id); }
ObjRelocKind::MipsCall16 => {
write_text("%call16(", color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone(), appearance);
write_text(")", color, job, font_id);
}
ObjRelocKind::MipsGpRel16 => {
write_text("%gp_rel(", color, job, font_id.clone());
write_reloc_name(reloc, color, job, font_id.clone(), appearance);
write_text(")", color, job, font_id);
}
ObjRelocKind::PpcRel24 | ObjRelocKind::PpcRel14 | ObjRelocKind::Mips26 => {
write_reloc_name(reloc, color, job, font_id, appearance);
}
ObjRelocKind::Absolute | ObjRelocKind::MipsGpRel32 => {
write_text("[INVALID]", color, job, font_id);
} }
}; };
} }
@@ -67,59 +103,71 @@ fn write_ins(
args: &[Option<ObjInsArgDiff>], args: &[Option<ObjInsArgDiff>],
base_addr: u32, base_addr: u32,
job: &mut LayoutJob, job: &mut LayoutJob,
config: &ViewConfig, appearance: &Appearance,
) { ) {
let base_color = match diff_kind { let base_color = match diff_kind {
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => { ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
Color32::GRAY appearance.text_color
} }
ObjInsDiffKind::Replace => Color32::LIGHT_BLUE, ObjInsDiffKind::Replace => appearance.replace_color,
ObjInsDiffKind::Delete => COLOR_RED, ObjInsDiffKind::Delete => appearance.delete_color,
ObjInsDiffKind::Insert => Color32::GREEN, ObjInsDiffKind::Insert => appearance.insert_color,
}; };
write_text( write_text(
&format!("{:<11}", ins.mnemonic), &format!("{:<11}", ins.mnemonic),
match diff_kind { match diff_kind {
ObjInsDiffKind::OpMismatch => Color32::LIGHT_BLUE, ObjInsDiffKind::OpMismatch => appearance.replace_color,
_ => base_color, _ => base_color,
}, },
job, job,
config.code_font.clone(), appearance.code_font.clone(),
); );
let mut writing_offset = false; let mut writing_offset = false;
for (i, arg) in ins.args.iter().enumerate() { for (i, arg) in ins.args.iter().enumerate() {
if i == 0 { if i == 0 {
write_text(" ", base_color, job, config.code_font.clone()); write_text(" ", base_color, job, appearance.code_font.clone());
} }
if i > 0 && !writing_offset { if i > 0 && !writing_offset {
write_text(", ", base_color, job, config.code_font.clone()); write_text(", ", base_color, job, appearance.code_font.clone());
} }
let color = if let Some(diff) = args.get(i).and_then(|a| a.as_ref()) { let color = if let Some(diff) = args.get(i).and_then(|a| a.as_ref()) {
config.diff_colors[diff.idx % config.diff_colors.len()] appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
} else { } else {
base_color base_color
}; };
match arg { match arg {
ObjInsArg::PpcArg(arg) => match arg { ObjInsArg::PpcArg(arg) => match arg {
Argument::Offset(val) => { Argument::Offset(val) => {
write_text(&format!("{val}"), color, job, config.code_font.clone()); write_text(&format!("{val}"), color, job, appearance.code_font.clone());
write_text("(", base_color, job, config.code_font.clone()); write_text("(", base_color, job, appearance.code_font.clone());
writing_offset = true; writing_offset = true;
continue; continue;
} }
Argument::Uimm(_) | Argument::Simm(_) => { Argument::Uimm(_) | Argument::Simm(_) => {
write_text(&format!("{arg}"), color, job, config.code_font.clone()); write_text(&format!("{arg}"), color, job, appearance.code_font.clone());
} }
_ => { _ => {
write_text(&format!("{arg}"), color, job, config.code_font.clone()); write_text(&format!("{arg}"), color, job, appearance.code_font.clone());
} }
}, },
ObjInsArg::Reloc => { ObjInsArg::Reloc => {
write_reloc(ins.reloc.as_ref().unwrap(), base_color, job, config.code_font.clone()); write_reloc(
ins.reloc.as_ref().unwrap(),
base_color,
job,
appearance.code_font.clone(),
appearance,
);
} }
ObjInsArg::RelocWithBase => { ObjInsArg::RelocWithBase => {
write_reloc(ins.reloc.as_ref().unwrap(), base_color, job, config.code_font.clone()); write_reloc(
write_text("(", base_color, job, config.code_font.clone()); ins.reloc.as_ref().unwrap(),
base_color,
job,
appearance.code_font.clone(),
appearance,
);
write_text("(", base_color, job, appearance.code_font.clone());
writing_offset = true; writing_offset = true;
continue; continue;
} }
@@ -128,22 +176,33 @@ fn write_ins(
str.strip_prefix('$').unwrap_or(str), str.strip_prefix('$').unwrap_or(str),
color, color,
job, job,
config.code_font.clone(), appearance.code_font.clone(),
); );
} }
ObjInsArg::MipsArgWithBase(str) => {
write_text(
str.strip_prefix('$').unwrap_or(str),
color,
job,
appearance.code_font.clone(),
);
write_text("(", base_color, job, appearance.code_font.clone());
writing_offset = true;
continue;
}
ObjInsArg::BranchOffset(offset) => { ObjInsArg::BranchOffset(offset) => {
let addr = offset + ins.address as i32 - base_addr as i32; let addr = offset + ins.address as i32 - base_addr as i32;
write_text(&format!("{addr:x}"), color, job, config.code_font.clone()); write_text(&format!("{addr:x}"), color, job, appearance.code_font.clone());
} }
} }
if writing_offset { if writing_offset {
write_text(")", base_color, job, config.code_font.clone()); write_text(")", base_color, job, appearance.code_font.clone());
writing_offset = false; writing_offset = false;
} }
} }
} }
fn ins_hover_ui(ui: &mut egui::Ui, ins: &ObjIns) { fn ins_hover_ui(ui: &mut egui::Ui, ins: &ObjIns, appearance: &Appearance) {
ui.scope(|ui| { ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false); ui.style_mut().wrap = Some(false);
@@ -169,13 +228,19 @@ fn ins_hover_ui(ui: &mut egui::Ui, ins: &ObjIns) {
if let Some(reloc) = &ins.reloc { if let Some(reloc) = &ins.reloc {
ui.label(format!("Relocation type: {:?}", reloc.kind)); ui.label(format!("Relocation type: {:?}", reloc.kind));
ui.colored_label(Color32::WHITE, format!("Name: {}", reloc.target.name)); ui.colored_label(appearance.highlight_color, format!("Name: {}", reloc.target.name));
if let Some(section) = &reloc.target_section { if let Some(section) = &reloc.target_section {
ui.colored_label(Color32::WHITE, format!("Section: {section}")); ui.colored_label(appearance.highlight_color, format!("Section: {section}"));
ui.colored_label(Color32::WHITE, format!("Address: {:x}", reloc.target.address)); ui.colored_label(
ui.colored_label(Color32::WHITE, format!("Size: {:x}", reloc.target.size)); appearance.highlight_color,
format!("Address: {:x}", reloc.target.address),
);
ui.colored_label(
appearance.highlight_color,
format!("Size: {:x}", reloc.target.size),
);
} else { } else {
ui.colored_label(Color32::WHITE, "Extern".to_string()); ui.colored_label(appearance.highlight_color, "Extern".to_string());
} }
} }
}); });
@@ -193,31 +258,31 @@ fn ins_context_menu(ui: &mut egui::Ui, ins: &ObjIns) {
match arg { match arg {
Argument::Uimm(v) => { Argument::Uimm(v) => {
if ui.button(format!("Copy \"{v}\"")).clicked() { if ui.button(format!("Copy \"{v}\"")).clicked() {
ui.output().copied_text = format!("{v}"); ui.output_mut(|output| output.copied_text = format!("{v}"));
ui.close_menu(); ui.close_menu();
} }
if ui.button(format!("Copy \"{}\"", v.0)).clicked() { if ui.button(format!("Copy \"{}\"", v.0)).clicked() {
ui.output().copied_text = format!("{}", v.0); ui.output_mut(|output| output.copied_text = format!("{}", v.0));
ui.close_menu(); ui.close_menu();
} }
} }
Argument::Simm(v) => { Argument::Simm(v) => {
if ui.button(format!("Copy \"{v}\"")).clicked() { if ui.button(format!("Copy \"{v}\"")).clicked() {
ui.output().copied_text = format!("{v}"); ui.output_mut(|output| output.copied_text = format!("{v}"));
ui.close_menu(); ui.close_menu();
} }
if ui.button(format!("Copy \"{}\"", v.0)).clicked() { if ui.button(format!("Copy \"{}\"", v.0)).clicked() {
ui.output().copied_text = format!("{}", v.0); ui.output_mut(|output| output.copied_text = format!("{}", v.0));
ui.close_menu(); ui.close_menu();
} }
} }
Argument::Offset(v) => { Argument::Offset(v) => {
if ui.button(format!("Copy \"{v}\"")).clicked() { if ui.button(format!("Copy \"{v}\"")).clicked() {
ui.output().copied_text = format!("{v}"); ui.output_mut(|output| output.copied_text = format!("{v}"));
ui.close_menu(); ui.close_menu();
} }
if ui.button(format!("Copy \"{}\"", v.0)).clicked() { if ui.button(format!("Copy \"{}\"", v.0)).clicked() {
ui.output().copied_text = format!("{}", v.0); ui.output_mut(|output| output.copied_text = format!("{}", v.0));
ui.close_menu(); ui.close_menu();
} }
} }
@@ -228,90 +293,108 @@ fn ins_context_menu(ui: &mut egui::Ui, ins: &ObjIns) {
if let Some(reloc) = &ins.reloc { if let Some(reloc) = &ins.reloc {
if let Some(name) = &reloc.target.demangled_name { if let Some(name) = &reloc.target.demangled_name {
if ui.button(format!("Copy \"{name}\"")).clicked() { if ui.button(format!("Copy \"{name}\"")).clicked() {
ui.output().copied_text = name.clone(); ui.output_mut(|output| output.copied_text = name.clone());
ui.close_menu(); ui.close_menu();
} }
} }
if ui.button(format!("Copy \"{}\"", reloc.target.name)).clicked() { if ui.button(format!("Copy \"{}\"", reloc.target.name)).clicked() {
ui.output().copied_text = reloc.target.name.clone(); ui.output_mut(|output| output.copied_text = reloc.target.name.clone());
ui.close_menu(); ui.close_menu();
} }
} }
}); });
} }
fn find_symbol<'a>(obj: &'a ObjInfo, section_name: &str, name: &str) -> Option<&'a ObjSymbol> { fn find_symbol<'a>(obj: &'a ObjInfo, selected_symbol: &SymbolReference) -> Option<&'a ObjSymbol> {
let section = obj.sections.iter().find(|s| s.name == section_name)?; obj.sections.iter().find_map(|section| {
section.symbols.iter().find(|s| s.name == name) section.symbols.iter().find(|symbol| symbol.name == selected_symbol.symbol_name)
})
} }
fn asm_row_ui(ui: &mut egui::Ui, ins_diff: &ObjInsDiff, symbol: &ObjSymbol, config: &ViewConfig) { fn asm_row_ui(
ui: &mut egui::Ui,
ins_diff: &ObjInsDiff,
symbol: &ObjSymbol,
appearance: &Appearance,
) {
if ins_diff.kind != ObjInsDiffKind::None { if ins_diff.kind != ObjInsDiffKind::None {
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color); ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
} }
let mut job = LayoutJob::default(); let mut job = LayoutJob::default();
if let Some(ins) = &ins_diff.ins { let Some(ins) = &ins_diff.ins else {
let base_color = match ins_diff.kind {
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
Color32::GRAY
}
ObjInsDiffKind::Replace => Color32::LIGHT_BLUE,
ObjInsDiffKind::Delete => COLOR_RED,
ObjInsDiffKind::Insert => Color32::GREEN,
};
write_text(
&format!("{:<6}", format!("{:x}:", ins.address - symbol.address as u32)),
base_color,
&mut job,
config.code_font.clone(),
);
if let Some(branch) = &ins_diff.branch_from {
write_text(
"~> ",
config.diff_colors[branch.branch_idx % config.diff_colors.len()],
&mut job,
config.code_font.clone(),
);
} else {
write_text(" ", base_color, &mut job, config.code_font.clone());
}
write_ins(ins, &ins_diff.kind, &ins_diff.arg_diff, symbol.address as u32, &mut job, config);
if let Some(branch) = &ins_diff.branch_to {
write_text(
" ~>",
config.diff_colors[branch.branch_idx % config.diff_colors.len()],
&mut job,
config.code_font.clone(),
);
}
ui.add(Label::new(job).sense(Sense::click()))
.on_hover_ui_at_pointer(|ui| ins_hover_ui(ui, ins))
.context_menu(|ui| ins_context_menu(ui, ins));
} else {
ui.label(""); ui.label("");
return;
};
let base_color = match ins_diff.kind {
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
appearance.text_color
}
ObjInsDiffKind::Replace => appearance.replace_color,
ObjInsDiffKind::Delete => appearance.delete_color,
ObjInsDiffKind::Insert => appearance.insert_color,
};
let mut pad = 6;
if let Some(line) = ins.line {
let line_str = format!("{line} ");
write_text(
&line_str,
appearance.deemphasized_text_color,
&mut job,
appearance.code_font.clone(),
);
pad = 12 - line_str.len();
} }
write_text(
&format!("{:<1$}", format!("{:x}: ", ins.address - symbol.address as u32), pad),
base_color,
&mut job,
appearance.code_font.clone(),
);
if let Some(branch) = &ins_diff.branch_from {
write_text(
"~> ",
appearance.diff_colors[branch.branch_idx % appearance.diff_colors.len()],
&mut job,
appearance.code_font.clone(),
);
} else {
write_text(" ", base_color, &mut job, appearance.code_font.clone());
}
write_ins(ins, &ins_diff.kind, &ins_diff.arg_diff, symbol.address as u32, &mut job, appearance);
if let Some(branch) = &ins_diff.branch_to {
write_text(
" ~>",
appearance.diff_colors[branch.branch_idx % appearance.diff_colors.len()],
&mut job,
appearance.code_font.clone(),
);
}
ui.add(Label::new(job).sense(Sense::click()))
.on_hover_ui_at_pointer(|ui| ins_hover_ui(ui, ins, appearance))
.context_menu(|ui| ins_context_menu(ui, ins));
} }
fn asm_table_ui( fn asm_table_ui(
table: TableBuilder<'_>, table: TableBuilder<'_>,
left_obj: &ObjInfo, left_obj: Option<&ObjInfo>,
right_obj: &ObjInfo, right_obj: Option<&ObjInfo>,
fn_name: &str, selected_symbol: &SymbolReference,
config: &ViewConfig, appearance: &Appearance,
) -> Option<()> { ) -> Option<()> {
let left_symbol = find_symbol(left_obj, ".text", fn_name); let left_symbol = left_obj.and_then(|obj| find_symbol(obj, selected_symbol));
let right_symbol = find_symbol(right_obj, ".text", fn_name); let right_symbol = right_obj.and_then(|obj| find_symbol(obj, selected_symbol));
let instructions_len = left_symbol.or(right_symbol).map(|s| s.instructions.len())?; let instructions_len = left_symbol.or(right_symbol).map(|s| s.instructions.len())?;
table.body(|body| { table.body(|body| {
body.rows(config.code_font.size, instructions_len, |row_index, mut row| { body.rows(appearance.code_font.size, instructions_len, |row_index, mut row| {
row.col(|ui| { row.col(|ui| {
if let Some(symbol) = left_symbol { if let Some(symbol) = left_symbol {
asm_row_ui(ui, &symbol.instructions[row_index], symbol, config); asm_row_ui(ui, &symbol.instructions[row_index], symbol, appearance);
} }
}); });
row.col(|ui| { row.col(|ui| {
if let Some(symbol) = right_symbol { if let Some(symbol) = right_symbol {
asm_row_ui(ui, &symbol.instructions[row_index], symbol, config); asm_row_ui(ui, &symbol.instructions[row_index], symbol, appearance);
} }
}); });
}); });
@@ -319,115 +402,120 @@ fn asm_table_ui(
Some(()) Some(())
} }
pub fn function_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) -> bool { pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
let mut rebuild = false; let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
if let (Some(result), Some(selected_symbol)) = (&view_state.build, &view_state.selected_symbol) else {
{ return;
StripBuilder::new(ui) };
.size(Size::exact(20.0))
.size(Size::exact(40.0)) // Header
.size(Size::remainder()) let available_width = ui.available_width();
.vertical(|mut strip| { let column_width = available_width / 2.0;
strip.strip(|builder| { ui.allocate_ui_with_layout(
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| { Vec2 { x: available_width, y: 100.0 },
strip.cell(|ui| { Layout::left_to_right(Align::Min),
ui.horizontal(|ui| { |ui| {
if ui.button("Back").clicked() { // Left column
view_state.current_view = View::SymbolDiff; ui.allocate_ui_with_layout(
} Vec2 { x: column_width, y: 100.0 },
}); Layout::top_down(Align::Min),
}); |ui| {
strip.cell(|ui| { ui.set_width(column_width);
ui.horizontal(|ui| {
if ui.button("Build").clicked() { if ui.button("Back").clicked() {
rebuild = true; state.current_view = View::SymbolDiff;
}
ui.scope(|ui| {
ui.style_mut().override_text_style =
Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
if view_state
.jobs
.iter()
.any(|job| job.job_type == Job::ObjDiff)
{
ui.label("Building...");
} else {
ui.label("Last built:");
let format =
format_description::parse("[hour]:[minute]:[second]")
.unwrap();
ui.label(
result
.time
.to_offset(view_state.utc_offset)
.format(&format)
.unwrap(),
);
}
});
});
});
});
});
strip.strip(|builder| {
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
let demangled = demangle(selected_symbol, &Default::default());
strip.cell(|ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style =
Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.colored_label(
Color32::WHITE,
demangled.as_ref().unwrap_or(selected_symbol),
);
ui.label("Diff target:");
ui.separator();
});
});
strip.cell(|ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style =
Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
if let Some(match_percent) = result
.second_obj
.as_ref()
.and_then(|obj| find_symbol(obj, ".text", selected_symbol))
.and_then(|symbol| symbol.match_percent)
{
ui.colored_label(
match_color_for_symbol(match_percent),
&format!("{match_percent:.0}%"),
);
}
ui.label("Diff base:");
ui.separator();
});
});
});
});
strip.cell(|ui| {
if let (Some(left_obj), Some(right_obj)) =
(&result.first_obj, &result.second_obj)
{
let table = TableBuilder::new(ui)
.striped(false)
.cell_layout(egui::Layout::left_to_right(egui::Align::Min))
.column(Size::relative(0.5))
.column(Size::relative(0.5))
.resizable(false);
asm_table_ui(
table,
left_obj,
right_obj,
selected_symbol,
&view_state.view_config,
);
} }
});
}); let demangled = demangle(&selected_symbol.symbol_name, &Default::default());
} let name = demangled.as_deref().unwrap_or(&selected_symbol.symbol_name);
rebuild let mut job = LayoutJob::simple(
name.to_string(),
appearance.code_font.clone(),
appearance.highlight_color,
column_width,
);
job.wrap.break_anywhere = true;
job.wrap.max_rows = 1;
ui.label(job);
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.label("Diff target:");
});
},
);
// Right column
ui.allocate_ui_with_layout(
Vec2 { x: column_width, y: 100.0 },
Layout::top_down(Align::Min),
|ui| {
ui.set_width(column_width);
ui.horizontal(|ui| {
if ui
.add_enabled(!state.build_running, egui::Button::new("Build"))
.clicked()
{
state.queue_build = true;
}
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
if state.build_running {
ui.colored_label(appearance.replace_color, "Building…");
} else {
ui.label("Last built:");
let format =
format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label(
result
.time
.to_offset(appearance.utc_offset)
.format(&format)
.unwrap(),
);
}
});
});
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
if let Some(match_percent) = result
.second_obj
.as_ref()
.and_then(|obj| find_symbol(obj, selected_symbol))
.and_then(|symbol| symbol.match_percent)
{
ui.colored_label(
match_color_for_symbol(match_percent, appearance),
&format!("{match_percent:.0}%"),
);
} else {
ui.colored_label(appearance.replace_color, "Missing");
}
ui.label("Diff base:");
});
},
);
},
);
ui.separator();
// Table
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,
);
} }

View File

@@ -1,55 +1,58 @@
use egui::{Color32, ProgressBar, Widget}; use egui::{ProgressBar, RichText, Widget};
use crate::app::ViewState; use crate::{jobs::JobQueue, views::appearance::Appearance};
pub fn jobs_ui(ui: &mut egui::Ui, view_state: &mut ViewState) { pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) {
ui.label("Jobs"); ui.label("Jobs");
let mut remove_job: Option<usize> = None; let mut remove_job: Option<usize> = None;
for (idx, job) in view_state.jobs.iter_mut().enumerate() { for job in jobs.iter_mut() {
if let Ok(status) = job.status.read() { let Ok(status) = job.status.read() else {
ui.group(|ui| { continue;
ui.horizontal(|ui| { };
ui.label(&status.title); ui.group(|ui| {
if ui.small_button("").clicked() { ui.horizontal(|ui| {
if job.handle.is_some() { ui.label(&status.title);
job.should_remove = true; if ui.small_button("").clicked() {
if let Err(e) = job.cancel.send(()) { if job.handle.is_some() {
eprintln!("Failed to cancel job: {e:?}"); job.should_remove = true;
} if let Err(e) = job.cancel.send(()) {
} else { log::error!("Failed to cancel job: {e:?}");
remove_job = Some(idx);
} }
}
});
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 = err.to_string();
ui.colored_label(
Color32::from_rgb(255, 0, 0),
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)
},
);
} else {
ui.label(if status.status.len() > STATUS_LENGTH - 3 {
format!("{}...", &status.status[0..STATUS_LENGTH - 3])
} else { } else {
format!("{:width$}", &status.status, width = STATUS_LENGTH) remove_job = Some(job.id);
}); }
} }
}); });
} 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);
}
});
} }
if let Some(idx) = remove_job { if let Some(idx) = remove_job {
view_state.jobs.remove(idx); jobs.remove(idx);
} }
} }

View File

@@ -1,13 +1,14 @@
use egui::{text::LayoutJob, Color32, FontId, TextFormat}; use egui::{text::LayoutJob, Color32, FontId, TextFormat};
pub(crate) mod appearance;
pub(crate) mod config; pub(crate) mod config;
pub(crate) mod data_diff; pub(crate) mod data_diff;
pub(crate) mod demangle;
pub(crate) mod function_diff; pub(crate) mod function_diff;
pub(crate) mod jobs; pub(crate) mod jobs;
pub(crate) mod symbol_diff; pub(crate) mod symbol_diff;
const COLOR_RED: Color32 = Color32::from_rgb(200, 40, 41); #[inline]
fn write_text(str: &str, color: Color32, job: &mut LayoutJob, font_id: FontId) { fn write_text(str: &str, color: Color32, job: &mut LayoutJob, font_id: FontId) {
job.append(str, 0.0, TextFormat { font_id, color, ..Default::default() }); job.append(str, 0.0, TextFormat::simple(font_id, color));
} }

View File

@@ -1,22 +1,93 @@
use std::mem::take;
use egui::{ use egui::{
text::LayoutJob, CollapsingHeader, Color32, Rgba, ScrollArea, SelectableLabel, Ui, Widget, text::LayoutJob, Align, CollapsingHeader, Color32, Layout, ScrollArea, SelectableLabel,
TextEdit, Ui, Vec2, Widget,
}; };
use egui_extras::{Size, StripBuilder}; use egui_extras::{Size, StripBuilder};
use crate::{ use crate::{
app::{View, ViewConfig, ViewState}, app::AppConfigRef,
jobs::objdiff::BuildStatus, jobs::{
objdiff::{BuildStatus, ObjDiffResult},
Job, JobQueue, JobResult,
},
obj::{ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags}, obj::{ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags},
views::write_text, views::{appearance::Appearance, write_text},
}; };
pub fn match_color_for_symbol(match_percent: f32) -> Color32 { pub struct SymbolReference {
pub symbol_name: String,
pub section_name: String,
}
#[allow(clippy::enum_variant_names)]
#[derive(Default, Eq, PartialEq, Copy, Clone)]
pub enum View {
#[default]
SymbolDiff,
FunctionDiff,
DataDiff,
}
#[derive(Default)]
pub struct DiffViewState {
pub build: Option<Box<ObjDiffResult>>,
pub current_view: View,
pub symbol_state: SymbolViewState,
pub search: String,
pub queue_build: bool,
pub build_running: bool,
}
#[derive(Default)]
pub struct SymbolViewState {
pub highlighted_symbol: Option<String>,
pub selected_symbol: Option<SymbolReference>,
pub reverse_fn_order: bool,
pub disable_reverse_fn_order: bool,
}
impl DiffViewState {
pub fn pre_update(&mut self, jobs: &mut JobQueue, config: &AppConfigRef) {
jobs.results.retain_mut(|result| {
if let JobResult::ObjDiff(result) = result {
self.build = take(result);
false
} else {
true
}
});
self.build_running = jobs.is_running(Job::ObjDiff);
self.symbol_state.disable_reverse_fn_order = false;
if let Ok(config) = config.read() {
if let Some(obj_config) = &config.selected_obj {
if let Some(value) = obj_config.reverse_fn_order {
self.symbol_state.reverse_fn_order = value;
self.symbol_state.disable_reverse_fn_order = true;
}
}
}
}
pub fn post_update(&mut self, _jobs: &mut JobQueue, config: &AppConfigRef) {
if self.queue_build {
self.queue_build = false;
if let Ok(mut config) = config.write() {
config.queue_build = true;
}
}
}
}
pub fn match_color_for_symbol(match_percent: f32, appearance: &Appearance) -> Color32 {
if match_percent == 100.0 { if match_percent == 100.0 {
Color32::GREEN appearance.insert_color
} else if match_percent >= 50.0 { } else if match_percent >= 50.0 {
Color32::LIGHT_BLUE appearance.replace_color
} else { } else {
Color32::RED appearance.delete_color
} }
} }
@@ -27,88 +98,98 @@ fn symbol_context_menu_ui(ui: &mut Ui, symbol: &ObjSymbol) {
if let Some(name) = &symbol.demangled_name { if let Some(name) = &symbol.demangled_name {
if ui.button(format!("Copy \"{name}\"")).clicked() { if ui.button(format!("Copy \"{name}\"")).clicked() {
ui.output().copied_text = name.clone(); ui.output_mut(|output| output.copied_text = name.clone());
ui.close_menu(); ui.close_menu();
} }
} }
if ui.button(format!("Copy \"{}\"", symbol.name)).clicked() { if ui.button(format!("Copy \"{}\"", symbol.name)).clicked() {
ui.output().copied_text = symbol.name.clone(); ui.output_mut(|output| output.copied_text = symbol.name.clone());
ui.close_menu(); ui.close_menu();
} }
}); });
} }
fn symbol_hover_ui(ui: &mut Ui, symbol: &ObjSymbol) { fn symbol_hover_ui(ui: &mut Ui, symbol: &ObjSymbol, appearance: &Appearance) {
ui.scope(|ui| { ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false); ui.style_mut().wrap = Some(false);
ui.colored_label(Color32::WHITE, format!("Name: {}", symbol.name)); ui.colored_label(appearance.highlight_color, format!("Name: {}", symbol.name));
ui.colored_label(Color32::WHITE, format!("Address: {:x}", symbol.address)); ui.colored_label(appearance.highlight_color, format!("Address: {:x}", symbol.address));
if symbol.size_known { if symbol.size_known {
ui.colored_label(Color32::WHITE, format!("Size: {:x}", symbol.size)); ui.colored_label(appearance.highlight_color, format!("Size: {:x}", symbol.size));
} else { } else {
ui.colored_label(Color32::WHITE, format!("Size: {:x} (assumed)", symbol.size)); ui.colored_label(
appearance.highlight_color,
format!("Size: {:x} (assumed)", symbol.size),
);
} }
}); });
} }
#[must_use]
fn symbol_ui( fn symbol_ui(
ui: &mut Ui, ui: &mut Ui,
symbol: &ObjSymbol, symbol: &ObjSymbol,
section: Option<&ObjSection>, section: Option<&ObjSection>,
highlighted_symbol: &mut Option<String>, state: &mut SymbolViewState,
selected_symbol: &mut Option<String>, appearance: &Appearance,
current_view: &mut View, ) -> Option<View> {
config: &ViewConfig, let mut ret = None;
) {
let mut job = LayoutJob::default(); let mut job = LayoutJob::default();
let name: &str = let name: &str =
if let Some(demangled) = &symbol.demangled_name { demangled } else { &symbol.name }; if let Some(demangled) = &symbol.demangled_name { demangled } else { &symbol.name };
let mut selected = false; let mut selected = false;
if let Some(sym) = highlighted_symbol { if let Some(sym) = &state.highlighted_symbol {
selected = sym == &symbol.name; selected = sym == &symbol.name;
} }
write_text("[", Color32::GRAY, &mut job, config.code_font.clone()); write_text("[", appearance.text_color, &mut job, appearance.code_font.clone());
if symbol.flags.0.contains(ObjSymbolFlags::Common) { if symbol.flags.0.contains(ObjSymbolFlags::Common) {
write_text("c", Color32::from_rgb(0, 255, 255), &mut job, config.code_font.clone()); write_text("c", appearance.replace_color, &mut job, appearance.code_font.clone());
} else if symbol.flags.0.contains(ObjSymbolFlags::Global) { } else if symbol.flags.0.contains(ObjSymbolFlags::Global) {
write_text("g", Color32::GREEN, &mut job, config.code_font.clone()); write_text("g", appearance.insert_color, &mut job, appearance.code_font.clone());
} else if symbol.flags.0.contains(ObjSymbolFlags::Local) { } else if symbol.flags.0.contains(ObjSymbolFlags::Local) {
write_text("l", Color32::GRAY, &mut job, config.code_font.clone()); write_text("l", appearance.text_color, &mut job, appearance.code_font.clone());
} }
if symbol.flags.0.contains(ObjSymbolFlags::Weak) { if symbol.flags.0.contains(ObjSymbolFlags::Weak) {
write_text("w", Color32::GRAY, &mut job, config.code_font.clone()); write_text("w", appearance.text_color, &mut job, appearance.code_font.clone());
} }
write_text("] ", Color32::GRAY, &mut job, config.code_font.clone()); write_text("] ", appearance.text_color, &mut job, appearance.code_font.clone());
if let Some(match_percent) = symbol.match_percent { if let Some(match_percent) = symbol.match_percent {
write_text("(", Color32::GRAY, &mut job, config.code_font.clone()); write_text("(", appearance.text_color, &mut job, appearance.code_font.clone());
write_text( write_text(
&format!("{match_percent:.0}%"), &format!("{match_percent:.0}%"),
match_color_for_symbol(match_percent), match_color_for_symbol(match_percent, appearance),
&mut job, &mut job,
config.code_font.clone(), appearance.code_font.clone(),
); );
write_text(") ", Color32::GRAY, &mut job, config.code_font.clone()); write_text(") ", appearance.text_color, &mut job, appearance.code_font.clone());
} }
write_text(name, Color32::WHITE, &mut job, config.code_font.clone()); write_text(name, appearance.highlight_color, &mut job, appearance.code_font.clone());
let response = SelectableLabel::new(selected, job) let response = SelectableLabel::new(selected, job)
.ui(ui) .ui(ui)
.context_menu(|ui| symbol_context_menu_ui(ui, symbol)) .context_menu(|ui| symbol_context_menu_ui(ui, symbol))
.on_hover_ui_at_pointer(|ui| symbol_hover_ui(ui, symbol)); .on_hover_ui_at_pointer(|ui| symbol_hover_ui(ui, symbol, appearance));
if response.clicked() { if response.clicked() {
if let Some(section) = section { if let Some(section) = section {
if section.kind == ObjSectionKind::Code { if section.kind == ObjSectionKind::Code {
*selected_symbol = Some(symbol.name.clone()); state.selected_symbol = Some(SymbolReference {
*current_view = View::FunctionDiff; symbol_name: symbol.name.clone(),
section_name: section.name.clone(),
});
ret = Some(View::FunctionDiff);
} else if section.kind == ObjSectionKind::Data { } else if section.kind == ObjSectionKind::Data {
*selected_symbol = Some(section.name.clone()); state.selected_symbol = Some(SymbolReference {
*current_view = View::DataDiff; symbol_name: section.name.clone(),
section_name: section.name.clone(),
});
ret = Some(View::DataDiff);
} }
} }
} else if response.hovered() { } else if response.hovered() {
*highlighted_symbol = Some(symbol.name.clone()); state.highlighted_symbol = Some(symbol.name.clone());
} }
ret
} }
fn symbol_matches_search(symbol: &ObjSymbol, search_str: &str) -> bool { fn symbol_matches_search(symbol: &ObjSymbol, search_str: &str) -> bool {
@@ -121,20 +202,15 @@ fn symbol_matches_search(symbol: &ObjSymbol, search_str: &str) -> bool {
.unwrap_or(false) .unwrap_or(false)
} }
#[allow(clippy::too_many_arguments)] #[must_use]
fn symbol_list_ui( fn symbol_list_ui(
ui: &mut Ui, ui: &mut Ui,
obj: &ObjInfo, obj: &ObjInfo,
highlighted_symbol: &mut Option<String>, state: &mut SymbolViewState,
selected_symbol: &mut Option<String>, lower_search: &str,
current_view: &mut View, appearance: &Appearance,
reverse_function_order: bool, ) -> Option<View> {
search: &mut String, let mut ret = None;
config: &ViewConfig,
) {
ui.text_edit_singleline(search);
let lower_search = search.to_ascii_lowercase();
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
ui.scope(|ui| { ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
@@ -143,15 +219,7 @@ fn symbol_list_ui(
if !obj.common.is_empty() { if !obj.common.is_empty() {
CollapsingHeader::new(".comm").default_open(true).show(ui, |ui| { CollapsingHeader::new(".comm").default_open(true).show(ui, |ui| {
for symbol in &obj.common { for symbol in &obj.common {
symbol_ui( ret = ret.or(symbol_ui(ui, symbol, None, state, appearance));
ui,
symbol,
None,
highlighted_symbol,
selected_symbol,
current_view,
config,
);
} }
}); });
} }
@@ -160,143 +228,175 @@ fn symbol_list_ui(
CollapsingHeader::new(format!("{} ({:x})", section.name, section.size)) CollapsingHeader::new(format!("{} ({:x})", section.name, section.size))
.default_open(true) .default_open(true)
.show(ui, |ui| { .show(ui, |ui| {
if section.name == ".text" && reverse_function_order { if section.kind == ObjSectionKind::Code && state.reverse_fn_order {
for symbol in section.symbols.iter().rev() { for symbol in section.symbols.iter().rev() {
if !symbol_matches_search(symbol, &lower_search) { if !symbol_matches_search(symbol, lower_search) {
continue; continue;
} }
symbol_ui( ret =
ui, ret.or(symbol_ui(ui, symbol, Some(section), state, appearance));
symbol,
Some(section),
highlighted_symbol,
selected_symbol,
current_view,
config,
);
} }
} else { } else {
for symbol in &section.symbols { for symbol in &section.symbols {
if !symbol_matches_search(symbol, &lower_search) { if !symbol_matches_search(symbol, lower_search) {
continue; continue;
} }
symbol_ui( ret =
ui, ret.or(symbol_ui(ui, symbol, Some(section), state, appearance));
symbol,
Some(section),
highlighted_symbol,
selected_symbol,
current_view,
config,
);
} }
} }
}); });
} }
}); });
}); });
ret
} }
fn build_log_ui(ui: &mut Ui, status: &BuildStatus) { fn build_log_ui(ui: &mut Ui, status: &BuildStatus, appearance: &Appearance) {
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
ui.scope(|ui| { ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false); ui.style_mut().wrap = Some(false);
ui.colored_label(Color32::from_rgb(255, 0, 0), &status.log); ui.colored_label(appearance.replace_color, &status.log);
}); });
}); });
} }
pub fn symbol_diff_ui(ui: &mut Ui, view_state: &mut ViewState) { fn missing_obj_ui(ui: &mut Ui, appearance: &Appearance) {
if let (Some(result), highlighted_symbol, selected_symbol, current_view, search) = ( ui.scope(|ui| {
&view_state.build, ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
&mut view_state.highlighted_symbol, ui.style_mut().wrap = Some(false);
&mut view_state.selected_symbol,
&mut view_state.current_view,
&mut view_state.search,
) {
StripBuilder::new(ui).size(Size::exact(40.0)).size(Size::remainder()).vertical(
|mut strip| {
strip.strip(|builder| {
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
strip.cell(|ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style =
Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.label("Build target:"); ui.colored_label(appearance.replace_color, "No object configured");
if result.first_status.success { });
ui.label("OK"); }
} else {
ui.colored_label(Rgba::from_rgb(1.0, 0.0, 0.0), "Fail");
}
});
ui.separator();
});
strip.cell(|ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style =
Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.label("Build base:"); pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appearance) {
if result.second_status.success { let DiffViewState { build, current_view, symbol_state, search, .. } = state;
ui.label("OK"); let Some(result) = build else {
} else { return;
ui.colored_label(Rgba::from_rgb(1.0, 0.0, 0.0), "Fail"); };
}
}); // Header
ui.separator(); 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| {
// 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.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.label("Build target:");
if result.first_status.success {
if result.first_obj.is_none() {
ui.colored_label(appearance.replace_color, "Missing");
} else {
ui.label("OK");
}
} else {
ui.colored_label(appearance.delete_color, "Fail");
}
});
TextEdit::singleline(search).hint_text("Filter symbols").ui(ui);
},
);
// 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.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.label("Build base:");
if result.second_status.success {
if result.second_obj.is_none() {
ui.colored_label(appearance.replace_color, "Missing");
} else {
ui.label("OK");
}
} else {
ui.colored_label(appearance.delete_color, "Fail");
}
});
ui.add_enabled(
!symbol_state.disable_reverse_fn_order,
egui::Checkbox::new(
&mut symbol_state.reverse_fn_order,
"Reverse function order (-inline deferred)",
),
);
},
);
},
);
ui.separator();
// Table
let mut ret = None;
let lower_search = search.to_ascii_lowercase();
StripBuilder::new(ui).size(Size::remainder()).vertical(|mut strip| {
strip.strip(|builder| {
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
strip.cell(|ui| {
ui.push_id("left", |ui| {
if result.first_status.success {
if let Some(obj) = &result.first_obj {
ret = ret.or(symbol_list_ui(
ui,
obj,
symbol_state,
&lower_search,
appearance,
));
} else {
missing_obj_ui(ui, appearance);
}
} else {
build_log_ui(ui, &result.first_status, appearance);
}
}); });
}); });
strip.strip(|builder| { strip.cell(|ui| {
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| { ui.push_id("right", |ui| {
strip.cell(|ui| { if result.second_status.success {
if result.first_status.success { if let Some(obj) = &result.second_obj {
if let Some(obj) = &result.first_obj { ret = ret.or(symbol_list_ui(
ui.push_id("left", |ui| { ui,
symbol_list_ui( obj,
ui, symbol_state,
obj, &lower_search,
highlighted_symbol, appearance,
selected_symbol, ));
current_view,
view_state.reverse_fn_order,
search,
&view_state.view_config,
);
});
}
} else { } else {
build_log_ui(ui, &result.first_status); missing_obj_ui(ui, appearance);
} }
}); } else {
strip.cell(|ui| { build_log_ui(ui, &result.second_status, appearance);
if result.second_status.success { }
if let Some(obj) = &result.second_obj {
ui.push_id("right", |ui| {
symbol_list_ui(
ui,
obj,
highlighted_symbol,
selected_symbol,
current_view,
view_state.reverse_fn_order,
search,
&view_state.view_config,
);
});
}
} else {
build_log_ui(ui, &result.second_status);
}
});
}); });
}); });
}, });
); });
});
if let Some(view) = ret {
*current_view = view;
} }
} }